From fa86a7b08911dbb536cec13a487539856fda2018 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 16:43:06 +0000 Subject: [PATCH 001/114] research(wave-0822): brief condiviso per l'ondata multi-agente --- docs/research/BRIEF-0822.md | 119 ++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 docs/research/BRIEF-0822.md diff --git a/docs/research/BRIEF-0822.md b/docs/research/BRIEF-0822.md new file mode 100644 index 0000000..4ad15dd --- /dev/null +++ b/docs/research/BRIEF-0822.md @@ -0,0 +1,119 @@ +# BRIEF ONDATA 2026-08-22 — ricerca multi-agente, branch `research/wave-0822` + +Sei UNO di ~30 agenti. Leggi tutto questo file prima di scrivere codice. + +## 0. Obiettivo reale (non e' "trovare uno Sharpe alto") +Il conto vero e' **$635** su Deribit. L'obiettivo dichiarato dall'operatore e' **50 EUR/giorno**. +Misura gia' fatta nel progetto: la rendita perpetua vale ~45-55% del CAGR, quindi 50 EUR/g +richiede **~$258k netti** di capitale. **Nessuna strategia colma quel divario**: il vincolo binding +misurato il 26/07 e' *capitale che entra* + *conto che non sparisce*, non la ricerca. +Percio' un risultato vale in ordine: + 1. qualcosa di **ESEGUIBILE a $600-5.000** che aggiunge al libro TP01+SKH01 (raro); + 2. qualcosa che alza **P(sopravvivere a una barriera di DD per-conto)** — e' il canale *prop/funded*, + l'unico che moltiplica il nozionale senza possedere il capitale; + 3. un **LEAD** onesto con gate pre-registrato e data; + 4. una **falsificazione pulita** (chiudere un filone vale quanto aprirlo: si smette di pagarlo). +Un "non funziona" MISURATO e' un risultato accettato e apprezzato. Un PASS non verificato e' un danno. + +## 1. Disciplina di macchina — NON NEGOZIABILE +La VPS ha **2 core** e ci gira il libro con **soldi veri**: `cron_book` al minuto **:07**, +`cron_chain` al **:25**, `cron_daily` alle **00:30**. +- Ogni comando python pesante va lanciato con `nice -n 19 timeout 900 uv run python ...` +- **Budget: nessun singolo run > 15 minuti.** Se una griglia non ci sta, RIDUCILA e dichiara che + l'hai ridotta. Meglio 24 celle dichiarate che 700 mai finite. +- Preferisci **1d / 1h**. I 5m esistono (btc/eth) ma una sweep su 5m x griglia = ore: vietata + salvo che sia il cuore della tua ipotesi, e allora su UN asset e UNA finestra. +- **NON toccare**: `config/`, `data/live/`, `data/paper_*`, `data/venue_watch`, `data/fee_watch`, + `src/live/`, `scripts/live/`, `scripts/cron_*.sh`. Sola lettura. +- **NON eseguire** git checkout/commit/push/stash/branch. Il branch e' gia' quello giusto. +- Scrivi **UN SOLO file nuovo**: `scripts/research/r0822_.py`. Niente altro in repo. + Output/figure/CSV solo sotto `/tmp/claude-1001/.../scratchpad` o stampati a video. + +## 2. Il dato — cosa esiste davvero +Legge con `from src.data.downloader import load_data` oppure `scripts/research/alt/altlib.get()`. +| dato | file | copertura | nota | +|---|---|---|---| +| BTC/ETH OHLCV | `data/raw/{btc,eth}_{5m,15m,1h}.parquet` | 2018/2019 -> oggi | **certificato**, unica verita' (Deribit mainnet) | +| DVOL BTC/ETH | `data/raw/dvol_{btc,eth}.parquet` | 2021-03 -> oggi (1978g) | vol implicita ATM | +| 51 alt Hyperliquid 1d | `data/raw/hl__1d.parquet` | 2024 -> oggi (~965g) | serie native, no backfill | +| funding HL 1h | `data/raw/hlfund__1h.parquet` | ~2023-05 -> oggi | 19 major | +| ETF/azioni 1d | `data/raw/eq__1d.parquet` | fino a 30 anni | SPY/QQQ/IWM/TLT/GLD/HYG certificati; TLT parte 2016 | +| **catena opzioni Deribit** | `data/raw/cb_chain/YYYY-MM-DD.parquet` + `bite_archive.parquet` | **2026-05-01 -> oggi, oraria** | per-strike: bid/ask/mid/iv/delta/gamma/theta/vega/OI/volume/book_depth | +| **snapshot di mercato** | `data/raw/cb_market_snapshots.parquet` | **2026-03-26 -> 2026-07-30** (17.406 righe) | dealer_net_gamma, gamma_flip_level, oi_delta_pct_4h, liquidation_long/short_risk, funding_cross, iv_minus_rv | +| term structure IV | `data/raw/vol_term_{btc,eth}.parquet` | **solo 58 giorni** | iv 7/30/60/90/180d | +Harness catena: `scripts/research/cblib.py` (`load_chain()` unisce archivio + raccolta). +**ATTENZIONE**: catena/snapshot/vol_term hanno **3-5 MESI** di storia. Con 3 mesi non esiste un +hold-out: quei dati possono produrre **al massimo un LEAD con gate pre-registrato**, mai uno sleeve. +Dirlo esplicitamente e' parte del risultato. `load_data("SOL")` = guardrail: SOL sta in `alt_sol_*` +ed e' **escluso per decisione dell'operatore** (22/08) — non riproporlo. + +## 3. L'harness onesto — USALO, non riscriverlo +`import sys; sys.path.insert(0, "scripts/research/alt")` poi `import altlib as A`. +- `A.get(asset, tf)` dati · `A.vol_target(...)` · `A.eval_weights(df, target)` metriche nette fee +- `A.eval_weights_smallcap(df, target, capital=600)` -> **haircut reale a $600** (min-order $5) +- `A.candidate_daily(target_fn)` -> serie giornaliera del candidato +- `A.marginal_vs_tp01(cand_daily)` -> ADDS / NEUTRAL / DILUTES / **HEDGE** / **NOISE** + (contiene: robust_oos multicut, has_insample_edge, is_hedge, beats_noise_null) +- `A.study_family_honest(name, factory, grid, tfs)` -> **il gate giusto se hai una GRIGLIA**: + sceglie la cella **in-sample-only** + deflated-Sharpe. `earns_slot_honest` e' il verdetto. +- `A.deflated_sharpe(sr, all_sr, daily)` -> PASS >= 0.95. **Conta TUTTI i trial, al RIALZO.** +- `A.causality_ok(target_fn)` -> nessun look-ahead · `A.day_boundary_robust(...)` per segnali calendario +- `A.implausible_sharpe(daily, n_trades, n_losing)` -> Sharpe troppo bello = rischio FUORI dal dataset +- `A.anchor_luck_band(fn_by_offset, offsets)` -> la stima onesta e' la **MEDIANA**, non l'ancora 00:00 +- `A.anchor_luck_delta(A_by_off, B_by_off, offsets)` -> per confrontare due varianti: + **mediana delle DIFFERENZE APPAIATE**, mai differenza delle mediane. + +## 4. I modi in cui questo progetto si e' gia' ingannato (li conosciamo per nome) +Se il tuo risultato ha una di queste forme, e' quasi certamente quello — provalo prima di crederci. +1. **Null del de-levering** (5 occorrenze): "meno drawdown" e' quasi sempre solo meno leva. + Test obbligatorio su OGNI claim di DD: esiste `k<1` che, applicato al baseline, da' lo STESSO + DD con Sharpe **migliore**? Se si', il tuo overlay e' inutile. +2. **TP01 travestito**: corr al baseline > ~0.6 = stai ricomprando il trend. `marginal_vs_tp01`. +3. **Selezione sull'hold-out**: scegliere la cella col miglior OOS. Usa `select_cell_insample`. +4. **Fortuna d'ancora**: l'ora/il giorno di ribilanciamento e' un max-of-k non dichiarato. +5. **Sharpe implausibile**: 0 perdite su N non e' "senza rischio" (regola del tre: fino a 3/N). +6. **Hedge travestito da alpha**: paga solo quando TP01 e' debole -> `is_hedge`. +7. **Il win-rate e' un KNOB**: WR ~ 1/(1+RR). Converti ogni claim "X% win" in **expectancy R netta fee**. +8. **Una riga presente non e' un dato presente**: conta cio' che e' QUOTATO/ATTIVO, non le righe. +9. **Barre attive vs calendario**: uno sleeve settimanale su griglia giornaliera e' 94% zeri. +10. **Confronto appaiato per INGRESSO**, mai allineato sull'uscita (e' quella che la variante cambia). + +## 5. Cosa e' GIA' MORTO — non riaprirlo senza un meccanismo nuovo +mean-reversion/fade · trend 5m-15m (fee) · calendario in ogni forma (weekend 375 trial, expiry, +stagionalita', event-clock, ora-del-giorno) · funding (carry CC01, time-series, cross-sectional: +chiuso su 3 lati) · macro regime gate (ridondante col trend) · gamma scalping long-vol · CRT/ICT/ +Elliott/Fibonacci/Albimarini · espansione universo XS01 · meta-allocazione dinamica · trend +multi-asset (MAT01) · overlay DD su VRP01 (5/5 refutati) · SOL terza gamba (deciso 22/08) · +breadth alt (rivedibile solo con 1-2 anni di storia HL in piu'). +Il **soffitto strutturale BTC/ETH-direzionale e' ~1.3 di Sharpe**: superato una sola volta, e +cambiando meccanismo (cross-sectional). + +## 6. Il libro di oggi — il tuo termine di paragone +LIVE su Deribit: **TP01 75% + SKH01 25%**, nettati sullo stesso strumento (USDC linear), cron orario, +cap $318/asset, min order $5, disaster-SL -30%, fee reale **3,5 bps/lato**. +Book di ricerca a 5 sleeve (TP01 33 / XS01 15 / VRP01 12 / SKH01 20 / GTAA01 20): +Sharpe FULL **1.95** e HOLD **1.54** (stime de-luckate; i canonici 2.22/2.36 sono fortuna d'ancora), +maxDD ~6.9%. GTAA01 **non e' deployabile** (PRIIPs) e XS01 serve ~$20k, XSR01 ~$5k. +Un candidato si giudica **marginalmente vs questo**, mai in assoluto. + +## 7. Contratto di consegna (obbligatorio) +Consegni **testo strutturato**, non un racconto. Alla fine del tuo lavoro riporta ESATTAMENTE: +- `IPOTESI`: cosa hai testato, in una frase, e **cosa ti aspettavi PRIMA di misurare**. +- `GRIGLIA DICHIARATA`: quante celle/trial hai valutato in TOTALE (conta al rialzo, incluse le + varianti provate e scartate). Se hai ridotto la griglia per il budget, dillo. +- `NUMERI`: Sharpe FULL / hold-out / maxDD / CAGR, **netti fee**, + haircut a $600 se pertinente. +- `GATE`: esito di ciascun gate che hai potuto far girare (marginal, DSR, causality, anchor, + de-levering null, implausible). Un gate non girato si dichiara "non girato", non si omette. +- `VERDETTO`: uno fra `SCARTATO` / `LEAD` / `CANDIDATO` — e **la ragione in una riga**. + `CANDIDATO` solo se: `earns_slot`/`marginal=ADDS` **E** DSR>=0.95 **E** null-de-levering superato + **E** eseguibile al capitale dichiarato. Altrimenti e' `LEAD` (con gate e data) o `SCARTATO`. +- `COSA MI SMENTIREBBE`: l'osservazione che falsificherebbe il tuo risultato. +- `SCRIPT`: il path del file che hai scritto (deve girare da solo: `uv run python `). +Se non hai potuto misurare, il verdetto e' `SCARTATO (non misurabile)` col motivo. **Non inventare +numeri, non estrapolare da un run non finito, non riportare un risultato di un run che hai ucciso.** + +## 8. RAM — 7 GB totali, ~3 GB liberi, e siete in tanti +Non caricare i `*_5m.parquet` di piu' di un asset alla volta, e taglia la finestra +(`pd.read_parquet(f, columns=[...])` + slice temporale) prima di elaborare. Se ti serve la catena +opzioni, filtra per asset/scadenza in lettura: `bite_archive.parquet` da solo e' 35 MB su disco ma +esplode in memoria. Un agente che fa OOM uccide anche il lavoro degli altri. From ff4aa94528ed3e481cb5964c39a7d668183ebc97 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:03:07 +0000 Subject: [PATCH 002/114] =?UTF-8?q?research(wave-0822):=20ledger=20degli?= =?UTF-8?q?=20esiti=20=E2=80=94=20ADAPTIVE-HORIZON=20scartato?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 24 + scripts/research/r0822_adaptive_horizon.py | 728 +++++++++++++++++++++ scripts/research/r0822_dealer_gamma.py | 689 +++++++++++++++++++ scripts/research/r0822_flow_squeeze.py | 639 ++++++++++++++++++ scripts/research/r0822_growth_policy.py | 564 ++++++++++++++++ scripts/research/r0822_oi_pin.py | 660 +++++++++++++++++++ scripts/research/r0822_ortho_screen.py | 674 +++++++++++++++++++ scripts/research/r0822_prop_alloc.py | 712 ++++++++++++++++++++ scripts/research/r0822_skew.py | 618 +++++++++++++++++ scripts/research/r0822_term_structure.py | 721 ++++++++++++++++++++ scripts/research/r0822_vol_size.py | 671 +++++++++++++++++++ scripts/research/r0822_vrp_real_quotes.py | 710 ++++++++++++++++++++ scripts/research/r0822_xs_lite.py | 717 ++++++++++++++++++++ 13 files changed, 8127 insertions(+) create mode 100644 docs/research/RESULTS-0822.md create mode 100644 scripts/research/r0822_adaptive_horizon.py create mode 100644 scripts/research/r0822_dealer_gamma.py create mode 100644 scripts/research/r0822_flow_squeeze.py create mode 100644 scripts/research/r0822_growth_policy.py create mode 100644 scripts/research/r0822_oi_pin.py create mode 100644 scripts/research/r0822_ortho_screen.py create mode 100644 scripts/research/r0822_prop_alloc.py create mode 100644 scripts/research/r0822_skew.py create mode 100644 scripts/research/r0822_term_structure.py create mode 100644 scripts/research/r0822_vol_size.py create mode 100644 scripts/research/r0822_vrp_real_quotes.py create mode 100644 scripts/research/r0822_xs_lite.py diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md new file mode 100644 index 0000000..f90ed6c --- /dev/null +++ b/docs/research/RESULTS-0822.md @@ -0,0 +1,24 @@ +# ONDATA 2026-08-22 — esiti per filone (aggiornato man mano) + +Regola di lettura: `SCARTATO` con un meccanismo misurato vale quanto un `LEAD` — chiude un filone +e smette di farlo pagare alle ondate successive. `CANDIDATO` richiede marginal ADDS + DSR>=0.95 + +null de-levering superato + eseguibilita' al capitale dichiarato. + +| # | filone | verdetto | in una riga | +|---|---|---|---| +| 9 | ADAPTIVE-HORIZON | **SCARTATO** | il "vincitore adattivo" ha lookback incollato al bordo il 100% del tempo = TSMOM costante a 20g (corr 1.000, dSh 0.00 in 8/8 anni e 0/12 ancore); isolando le celle davvero adattive -> NEUTRAL, corr→TP01 0.90 = TP01 travestito | + +## Note che sopravvivono ai singoli filoni + +### 9 — ADAPTIVE-HORIZON (r0822_adaptive_horizon.py, 112 trial dichiarati) +- Sanity: il baseline dell'agente riproduce TP01 canonico a **max|diff| = 0.0** (n=2719). +- Il controllo giusto NON era TP01 ma il **miglior lookback COSTANTE**: senza quel controllo il null + confonde "adattare paga" con "un orizzonte piu' corto paga su questo campione". L'agente se l'e' + costruito da solo dopo aver visto l'argmax incollato al bordo, e ha dichiarato i trial in piu'. +- ⚠️ Segnalazione, NON proposta: 30/90/180 e' rango 18/24 in-sample nella propria famiglia di + orizzonti (10/24 sull'hold-out), ma la superficie di L e' un **crinale di rumore** — il salto di + Sharpe fra L ADIACENTI e' +0.18, cioe' il 47-67% dell'intero vantaggio del vincitore su TP01, ed + estendere la griglia ha spostato l'argmax da L=20 a L=15. "Piu' corto e' meglio" e' un max-of-k su + una superficie frastagliata. **Nessun cambio a TP01.** +- Riaprirebbe il filone: un rilevatore il cui L si muove DAVVERO che batta il miglior L costante + (non TP01) in >=6/8 anni appaiati con corr→TP01 < 0.6 — es. BOCPD/PELT vero, non provato. diff --git a/scripts/research/r0822_adaptive_horizon.py b/scripts/research/r0822_adaptive_horizon.py new file mode 100644 index 0000000..baf804c --- /dev/null +++ b/scripts/research/r0822_adaptive_horizon.py @@ -0,0 +1,728 @@ +#!/usr/bin/env python +"""r0822_adaptive_horizon.py — ADAPTIVE-HORIZON TSMOM: stimare *quanto indietro guardare* +dai dati (change-point / persistenza) invece di fissarlo a 30/90/180 giorni. + +DOMANDA +------- +Un TSMOM a lookback ADATTIVO batte il TSMOM a orizzonti FISSI 30/90/180 (TP01), in modo +MARGINALE e ONESTO? + +COSA NON E' (i due vicini gia' bocciati, per non rifarli) +--------------------------------------------------------- + * 02/07 "velocita' trend regime-condizionata": pesava i TRE orizzonti fissi in funzione del + percentile di vol -> risulto' un tilt-30d statico travestito (pctl 0.71 vs il null dei pesi + statici casuali). Li' gli orizzonti restavano 30/90/180: cambiavano solo i PESI. + * 29/06 "meta-allocazione dinamica" fra sleeve: peggiore dei pesi fissi. Li' l'oggetto adattato + erano i pesi di PORTAFOGLIO, non un parametro della strategia. + Qui l'oggetto adattato e' **l'orizzonte stesso**: L_t stimato da un rilevatore di change-point + (CUSUM) o di persistenza (variance-ratio, Hurst). Mai provato nel progetto. + +FORMA FUNZIONALE (scelta perche' rende il confronto strutturalmente pulito) +-------------------------------------------------------------------------- +TP01 canonico e' il blend di sign(c[i]/c[i-h]-1) su h = (30, 90, 180) giorni, long-flat, +vol-target 20%, leva cap 2x. Ma (30, 90, 180) == (L/3, L, 2L) con **L = 90 giorni**. +Quindi il candidato e' LA STESSA FUNZIONE con L che varia nel tempo: + blend='triple' -> horizons_t = (L_t/3, L_t, 2*L_t) (TP01 = L_t costante a 90g) + blend='single' -> horizons_t = (L_t,) (un solo orizzonte adattivo) +Tutto il resto (long-flat, target_vol 20%, vol_win 30g, leva 2x, fee 0.05%/lato, cadenza, +50/50 BTC+ETH) e' IDENTICO al baseline: **l'unico grado di liberta' e' l'orizzonte**. +=> il null non e' "meglio di zero", e' "meglio del TSMOM fisso a PARI GEOMETRIA". + +FAMIGLIA DICHIARATA **PRIMA** DI GUARDARE (conteggio al RIALZO) +-------------------------------------------------------------- + rilevatore (3): cusum | vratio | hurst + parametro (3): cusum k in {2,4,8} sigma-cumulate; vratio/hurst finestra W in {60,120,250}g + mappatura (2): direct (piu' persistenza / piu' tempo dall'ultimo change-point -> L piu' LUNGO) + inverse (specchiata) [il verso NON e' ovvio a priori => e' un asse, non una scelta] + blend (2): single | triple + timeframe (2): 1d | 12h [>= 12h: sotto, costi+overfit dominano (19/06)] + ------------------------------------------------------------------ + = 3 x 3 x 2 x 2 = 36 celle x 2 TF = **72 valutazioni**. +Costanti FISSATE A PRIORI e non cercate (dichiarate perche' sono gradi di liberta' non contati +nella griglia): Lmin=20g, Lmax=200g (contengono 30..180 del canonico); q=5g del variance-ratio; +scale del Hurst k in {1,2,4,8}g; finestra della percentile causale M=250g. Per questo il +deflated-Sharpe e' riportato ANCHE a N=144 e N=216 trial (sensibilita' del verdetto al conteggio, +regola del 30/07): un verdetto che si ribalta col conteggio si cita come tale. + +GATE (tutti dell'harness del progetto, nessuno riscritto) +--------------------------------------------------------- + A.study_family_honest -> cella scelta IN-SAMPLE-ONLY + deflated-Sharpe (NO selezione su hold-out) + A.marginal_vs_tp01 -> ADDS/HEDGE/NOISE/REDUNDANT/DILUTES + corr al baseline + A.causality_ok -> ricalcolo su prefisso (nessun look-ahead nella costruzione del segnale) + A.anchor_luck_band -> stima onesta = MEDIANA delle 24 ancore, non l'ancora 00:00 + A.anchor_luck_delta -> confronto adattivo-vs-fisso: MEDIANA delle DIFFERENZE APPAIATE + A.implausible_sharpe -> Sharpe troppo bello = rischio fuori dal campione + A.eval_weights_smallcap-> haircut reale a $600 (min-order $5) + null del de-levering -> esiste k<1 che da' al FISSO lo stesso maxDD con Sharpe >=? + differenza APPAIATA PER ANNO -> un vantaggio concentrato in un anno e' un evento, non un meccanismo + +Causalita': ogni rolling ha min_periods e finestra all'indietro; la CUSUM e' un loop in avanti +con reset (prefix-stable); la percentile e' rolling causale; nessuna statistica full-sample. +Runtime atteso ~3-6 min su 2 core. Nessun file toccato fuori da questo. +""" +from __future__ import annotations + +import sys +import warnings +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +sys.path.insert(0, str(ROOT)) + +import altlib as A # noqa: E402 +from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402 + +ASSETS = A.CERTIFIED +HOLDOUT = A.HOLDOUT + +# --- costanti FISSATE A PRIORI (dichiarate sopra, non cercate) -------------- +LMIN_D, LMAX_D = 20, 200 # banda dell'orizzonte adattivo, in giorni +FIXED_H = (30, 90, 180) # == (L/3, L, 2L) con L=90 -> il baseline +TGT_VOL, LEV, VOLWIN = 0.20, 2.0, 30 +VR_Q_DAYS = 5 # passo del variance-ratio +HURST_K_DAYS = (1, 2, 4, 8) # scale del Hurst +PCTL_M_DAYS = 250 # finestra della percentile causale + +TFS = ("1d", "12h") +DETECTORS = ("cusum", "vratio", "hurst") +PARAMS = {"cusum": (2.0, 4.0, 8.0), "vratio": (60, 120, 250), "hurst": (60, 120, 250)} +MAPPINGS = ("direct", "inverse") +BLENDS = ("single", "triple") + + +# =========================================================================== +# RILEVATORI -> lookback adattivo L_t (in barre). Tutti causali. +# =========================================================================== +def _logret(c: np.ndarray) -> np.ndarray: + r = np.zeros(len(c)) + r[1:] = np.log(c[1:] / c[:-1]) + return r + + +def _roll_pctl(x: np.ndarray, m: int) -> np.ndarray: + """Percentile CAUSALE del valore corrente nella sua finestra all'indietro (rolling rank). + Warmup -> 0.5 (neutro): non si inventa un regime dove non c'e' campione.""" + s = pd.Series(x) + u = s.rolling(m, min_periods=max(20, m // 4)).rank(pct=True).values + return np.where(np.isfinite(u), u, 0.5) + + +def _cusum_age(c: np.ndarray, k: float, bpd: int) -> np.ndarray: + """Eta' (in barre) dall'ultimo change-point CUSUM a due code su rendimenti standardizzati. + Loop in avanti con reset: dipende solo dal passato -> stabile su prefisso.""" + lr = _logret(c) + sig = pd.Series(lr).rolling(30 * bpd, min_periods=max(5, 15 * bpd)).std().values + s = np.where((sig > 0) & np.isfinite(sig), lr / np.where(sig > 0, sig, 1.0), 0.0) + s = np.nan_to_num(s) + n = len(c) + age = np.zeros(n) + sp = sm = 0.0 + last = 0 + for i in range(n): + sp = max(0.0, sp + s[i]) + sm = min(0.0, sm + s[i]) + if max(sp, -sm) > k: + sp = sm = 0.0 + last = i + age[i] = i - last + return age + + +def _vratio(c: np.ndarray, w_days: int, bpd: int) -> np.ndarray: + """Variance ratio VR = Var(r_q) / (q * Var(r_1)) su finestra w_days. >1 = persistente.""" + lr = _logret(c) + qb = max(2, VR_Q_DAYS * bpd) + lg = np.log(c) + lrq = np.zeros(len(c)) + lrq[qb:] = lg[qb:] - lg[:-qb] + lrq[:qb] = np.nan + wb = max(20, w_days * bpd) + v1 = pd.Series(lr).rolling(wb, min_periods=wb // 2).var().values + vq = pd.Series(lrq).rolling(wb, min_periods=wb // 2).var().values + with np.errstate(invalid="ignore", divide="ignore"): + vr = vq / (qb * v1) + return vr + + +def _hurst(c: np.ndarray, w_days: int, bpd: int) -> np.ndarray: + """Hurst da scaling della volatilita': sd(r_k) ~ k^H. Regressione di log sd su log k + (x fisso -> forma chiusa). Tutto rolling causale.""" + lg = np.log(c) + wb = max(20, w_days * bpd) + xs, ys = [], [] + for kd in HURST_K_DAYS: + kb = max(1, kd * bpd) + d = np.full(len(c), np.nan) + d[kb:] = lg[kb:] - lg[:-kb] + sd = pd.Series(d).rolling(wb, min_periods=wb // 2).std().values + with np.errstate(invalid="ignore", divide="ignore"): + ys.append(np.log(np.where(sd > 0, sd, np.nan))) + xs.append(np.log(kb)) + X = np.asarray(xs, float) + Y = np.vstack(ys) # (n_scale, n_bar) + xm = X.mean() + denom = float(((X - xm) ** 2).sum()) + with warnings.catch_warnings(): # warmup = colonne tutte-NaN + warnings.simplefilter("ignore", RuntimeWarning) + ym = np.nanmean(Y, axis=0) + H = ((X - xm)[:, None] * (Y - ym[None, :])).sum(axis=0) / denom + return H + + +_STAT_CACHE: dict = {} + + +def _stat_key(df: pd.DataFrame, det: str, par) -> tuple: + ts = df["timestamp"].values + return (det, par, len(df), int(ts[0]), int(ts[-1])) + + +def lookback_bars(df: pd.DataFrame, det: str, par, mapping: str) -> np.ndarray: + """L_t in BARRE, dentro [Lmin, Lmax] giorni. Cache chiusa sul CONTENUTO del frame + (lunghezza + primo/ultimo timestamp) -> un prefisso NON prende la cache del pieno, + altrimenti causality_ok verrebbe ingannata dalla cache stessa.""" + bpd = A.bars_per_day(df) + lo, hi = max(2, LMIN_D * bpd), max(3, LMAX_D * bpd) + c = df["close"].values.astype(float) + key = _stat_key(df, det, par) + if key not in _STAT_CACHE: + if det == "cusum": + _STAT_CACHE[key] = _cusum_age(c, float(par), bpd) + elif det == "vratio": + _STAT_CACHE[key] = _roll_pctl(_vratio(c, int(par), bpd), PCTL_M_DAYS * bpd) + elif det == "hurst": + _STAT_CACHE[key] = _roll_pctl(_hurst(c, int(par), bpd), PCTL_M_DAYS * bpd) + else: + raise ValueError(det) + stat = _STAT_CACHE[key] + if det == "cusum": + base = np.clip(stat, lo, hi) + L = base if mapping == "direct" else (lo + hi - base) + else: + u = np.clip(stat, 0.0, 1.0) + L = lo + u * (hi - lo) if mapping == "direct" else hi - u * (hi - lo) + return np.clip(np.round(L), 2, hi).astype(int) + + +# =========================================================================== +# DIREZIONE TSMOM (orizzonte costante o variabile) + vol-target +# =========================================================================== +def _sign_at(c: np.ndarray, h: np.ndarray) -> tuple[np.ndarray, np.ndarray]: + n = len(c) + i = np.arange(n) + j = i - h + ok = j >= 0 + s = np.zeros(n) + jj = np.where(ok, j, 0) + s[ok] = np.sign(c[i[ok]] / c[jj[ok]] - 1.0) + return s, ok + + +def _dir_from_L(c: np.ndarray, L: np.ndarray, blend: str) -> np.ndarray: + hs = [L] if blend == "single" else [ + np.maximum(2, np.round(L / 3.0).astype(int)), L, np.round(2.0 * L).astype(int)] + acc = np.zeros(len(c)) + cnt = np.zeros(len(c)) + for h in hs: + s, ok = _sign_at(c, np.asarray(h, int)) + acc[ok] += s[ok] + cnt[ok] += 1 + out = np.zeros(len(c)) + nz = cnt > 0 + out[nz] = acc[nz] / cnt[nz] + return out + + +def fixed_target(df: pd.DataFrame) -> np.ndarray: + """BASELINE: TSMOM fisso 30/90/180, long-flat, vol-target 20%, leva 2x = TP01 canonico.""" + c = df["close"].values.astype(float) + bpd = A.bars_per_day(df) + acc = np.zeros(len(c)) + cnt = np.zeros(len(c)) + for hd in FIXED_H: + s, ok = _sign_at(c, np.full(len(c), hd * bpd, int)) + acc[ok] += s[ok] + cnt[ok] += 1 + d = np.zeros(len(c)) + nz = cnt > 0 + d[nz] = acc[nz] / cnt[nz] + return A.vol_target(np.clip(d, 0, None), df, TGT_VOL, VOLWIN, LEV) + + +def const_factory(tf: str, L: int, blend: str): + """TSMOM a orizzonte COSTANTE L giorni, STESSA geometria del candidato adattivo. + blend='triple' -> (L/3, L, 2L): con L=90 e' ESATTAMENTE TP01 canonico 30/90/180.""" + def fn(df: pd.DataFrame) -> np.ndarray: + c = df["close"].values.astype(float) + bpd = A.bars_per_day(df) + Lb = np.full(len(c), max(2, int(round(L * bpd))), int) + d = np.clip(_dir_from_L(c, Lb, blend), 0, None) + return A.vol_target(d, df, TGT_VOL, VOLWIN, LEV) + fn.__name__ = f"const_L{L}_{blend}" + return fn + + +def factory(tf: str, det: str, par, mapping: str, blend: str): + """target_fn(df) -> posizione per barra, decisa con dati <= close[i].""" + def fn(df: pd.DataFrame) -> np.ndarray: + c = df["close"].values.astype(float) + L = lookback_bars(df, det, par, mapping) + d = np.clip(_dir_from_L(c, L, blend), 0, None) + return A.vol_target(d, df, TGT_VOL, VOLWIN, LEV) + fn.__name__ = f"adapt_{det}{par}_{mapping}_{blend}" + return fn + + +GRID = [dict(det=d, par=p, mapping=m, blend=b) + for d in DETECTORS for p in PARAMS[d] for m in MAPPINGS for b in BLENDS] + +# CONTROLLO dichiarato DOPO aver visto che il vincitore ha L_t incollato al bordo (sezione 3): +# il null "TSMOM fisso 30/90/180" NON basta piu', perche' confonde due cose diverse — +# "adattare l'orizzonte paga" e "un orizzonte piu' CORTO paga su questo campione". +# Il controllo giusto e' il migliore fra gli orizzonti COSTANTI della stessa banda, scelto +# in-sample-only. Trial aggiuntivi, contati al rialzo. +CONST_L = (20, 30, 45, 60, 90, 120, 150, 200) +CONST_GRID = [dict(L=L, blend=b) for L in CONST_L for b in BLENDS] + +# ESTENSIONE dichiarata DOPO aver visto che l'ottimo costante cade sul BORDO BASSO della banda +# (L=20g = LMIN_D): un ottimo al bordo non e' un ottimo, e' il punto in cui si e' smesso di +# guardare. Si estende SOTTO e si guarda se e' un plateau o una scivolata verso il rumore. +# +4 valori x 1 blend x 2 TF = 8 trial in piu' (contati al rialzo). +CONST_L_EXT = (5, 7, 10, 15) + + +# =========================================================================== +# UTILITA' DI MISURA +# =========================================================================== +def _dd(s: pd.Series) -> float: + eq = np.cumprod(1.0 + np.asarray(s.dropna().values, float)) + pk = np.maximum.accumulate(eq) + return float(np.max((pk - eq) / pk)) if len(eq) else 0.0 + + +def _cagr(s: pd.Series) -> float: + s = s.dropna() + if len(s) < 5: + return float("nan") + eq = float(np.prod(1.0 + s.values)) + yrs = (s.index[-1] - s.index[0]).total_seconds() / (86400 * 365.25) + return eq ** (1 / max(yrs, 1e-6)) - 1.0 + + +def resample_offset(df_1h: pd.DataFrame, hours: int, off: int) -> pd.DataFrame: + """Stessa convenzione di trend_portfolio.resample_tf, con ancora spostata di `off` ore + (r0702_tp01_offset.py). NB: si usa `offset=`, non `origin=` (pandas ignora origin su 7D).""" + g = df_1h.copy() + idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True) + idx.name = "dt" + g.index = idx + out = g.resample(f"{hours}h", offset=pd.Timedelta(hours=off), label="left", closed="left").agg( + {"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}) + out = out.dropna(subset=["open"]) + out["datetime"] = out.index + epoch = pd.Timestamp("1970-01-01", tz="UTC") + out["timestamp"] = ((out.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64") + return out.reset_index(drop=True)[ + ["timestamp", "open", "high", "low", "close", "volume", "datetime"]] + + +def daily_from_target(fn, tf: str) -> pd.Series: + return A.candidate_daily(fn, tf=tf) + + +def paired_by_year(cand: pd.Series, base: pd.Series) -> list[dict]: + J = pd.concat({"C": cand, "B": base}, axis=1, join="inner").dropna() + rows = [] + for y, g in J.groupby(J.index.year): + if len(g) < 40: + continue + rows.append(dict(year=int(y), n=len(g), + sh_cand=round(A._sh(g["C"]), 2), sh_base=round(A._sh(g["B"]), 2), + d_sh=round(A._sh(g["C"]) - A._sh(g["B"]), 2), + ret_cand=round(float(np.prod(1 + g["C"].values) - 1) * 100, 1), + ret_base=round(float(np.prod(1 + g["B"].values) - 1) * 100, 1))) + return rows + + +def delever_null(cand: pd.Series, base: pd.Series) -> dict: + """Il fisso, DE-LEVERATO a pari maxDD del candidato, che Sharpe fa? + Scalare le POSIZIONI per k scala gross e fee insieme -> il netto scala per k e lo Sharpe + e' INVARIANTE: percio' un candidato che compra solo meno DD non e' un miglioramento.""" + J = pd.concat({"C": cand, "B": base}, axis=1, join="inner").dropna() + dd_c, dd_b = _dd(J["C"]), _dd(J["B"]) + if dd_c >= dd_b or dd_b <= 0: + return dict(applicabile=False, dd_cand=round(dd_c, 4), dd_base=round(dd_b, 4), + nota="il candidato NON riduce il maxDD: il null non si applica") + lo, hi = 0.0, 1.0 + for _ in range(60): + k = 0.5 * (lo + hi) + if _dd(k * J["B"]) < dd_c: + lo = k + else: + hi = k + k = 0.5 * (lo + hi) + return dict(applicabile=True, k=round(k, 4), dd_cand=round(dd_c, 4), dd_base=round(dd_b, 4), + dd_base_delev=round(_dd(k * J["B"]), 4), + sharpe_cand=round(A._sh(J["C"]), 3), + sharpe_base_delev=round(A._sh(k * J["B"]), 3), + superato=bool(A._sh(J["C"]) > A._sh(k * J["B"]))) + + +_ANCHOR_CACHE: dict = {} + + +def series_at_anchor(fn, name: str, hours: int, off: int) -> pd.Series: + """Serie giornaliera 50/50 BTC+ETH della strategia `fn` ribilanciata sull'ancora `off` + (resample del 1h certificato). Cache per (name, hours, off).""" + key = (name, hours, off) + if key not in _ANCHOR_CACHE: + ser = {} + for a in ASSETS: + dfo = resample_offset(A.get(a, "1h"), hours, off) + ev = A.eval_weights(dfo, A._call_target(fn, dfo, a)) + ser[a] = pd.Series(ev["net"], index=ev["idx"]) + J = pd.concat(ser, axis=1, join="inner").fillna(0.0) + _ANCHOR_CACHE[key] = A._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]]) + return _ANCHOR_CACHE[key] + + +def adaptivity(tf: str, det: str, par, mapping: str) -> dict: + """Quanto si MUOVE davvero L_t (diagnostica di MECCANISMO, non di rendimento). + Una cella con L_t incollato a un bordo NON e' adattiva: e' un TSMOM a orizzonte + COSTANTE travestito, e non appartiene alla famiglia che si sta testando.""" + fr_lo, fr_hi, iqr, med = [], [], [], [] + for a in ASSETS: + df = A.get(a, tf) + bpd = A.bars_per_day(df) + L = lookback_bars(df, det, par, mapping) / bpd + fr_lo.append(float(np.mean(L <= LMIN_D + 1))) + fr_hi.append(float(np.mean(L >= LMAX_D - 1))) + iqr.append(float(np.percentile(L, 75) - np.percentile(L, 25))) + med.append(float(np.median(L))) + return dict(frac_lo=float(np.mean(fr_lo)), frac_hi=float(np.mean(fr_hi)), + iqr_days=float(np.mean(iqr)), med_days=float(np.mean(med)), + adattiva=bool(np.mean(fr_lo) < 0.5 and np.mean(fr_hi) < 0.5 + and np.mean(iqr) >= 10.0)) + + +def hr(t: str = "") -> None: + print("\n" + "=" * 78) + if t: + print(t) + print("=" * 78) + + +# =========================================================================== +def main() -> None: + print(__doc__.split("Runtime atteso")[0]) + + # -- 0. SANITY: il mio baseline DEVE essere TP01 canonico, bit-per-bit ---------------- + hr("0. SANITY — il baseline 'fisso' e' TP01 canonico (se non lo e', ogni Δ e' inventato)") + tp = TrendPortfolio(**CANONICAL) + worst = 0.0 + for a in ASSETS: + df = A.get(a, "1d") + worst = max(worst, float(np.max(np.abs(fixed_target(df) - tp.target_series(df))))) + base_1d = daily_from_target(fixed_target, "1d") + tp01 = A.tp01_baseline_daily() + JJ = pd.concat({"a": base_1d, "b": tp01}, axis=1, join="inner").dropna() + print(f" max|target_mio - TrendPortfolio.target_series| (1d, BTC+ETH) = {worst:.3e}") + print(f" max|serie_giornaliera_mia - al.tp01_baseline_daily()| = " + f"{float(np.max(np.abs(JJ['a'] - JJ['b']))):.3e} (n={len(JJ)})") + assert worst < 1e-12, "il baseline NON riproduce TP01: fermarsi qui" + + # -- 1. GRIGLIA ONESTA: cella scelta IN-SAMPLE + deflated-Sharpe ---------------------- + hr(f"1. study_family_honest — {len(GRID)} celle x {len(TFS)} TF = {len(GRID)*len(TFS)} trial") + rep = A.study_family_honest("ADAPT-H", factory, GRID, TFS) + ch = rep["chosen"] + print(f" celle valutate : {rep['n_cells']}") + print(f" cella scelta IN-SAMPLE : tf={ch['tf']} {ch['params']} " + f"Sharpe IS {ch['insample_sharpe']} FULL {ch['full_sharpe']}") + print(f" deflated-Sharpe (N={rep['n_cells']}) : {rep['deflated_sharpe']} " + f"(max atteso sotto il null {rep['expected_null_max']}) PASS={rep['dsr_pass']}") + print(f" earns_slot_marginal : {rep['earns_slot_marginal']}") + print(f" EARNS_SLOT_HONEST : {rep['earns_slot_honest']}") + print("\n top-8 IN-SAMPLE (come si sceglie onestamente):") + for r in rep["rows"][:8]: + print(f" IS {r['insample_sharpe']:+.3f} FULL {r['full_sharpe']:+.3f} " + f"{r['tf']:>3} {r['params']}") + + # sensibilita' del verdetto al CONTEGGIO dei trial (regola 30/07) + fn_ch = factory(tf=ch["tf"], **ch["params"]) + d_ch = daily_from_target(fn_ch, ch["tf"]) + allsr = [r["full_sharpe"] for r in rep["rows"]] + print("\n sensibilita' del DSR al numero di trial dichiarati:") + for n_decl in (len(GRID) * len(TFS), 144, 216): + pad = allsr + [float(np.mean(allsr))] * max(0, n_decl - len(allsr)) + dsr, _ = A.deflated_sharpe(A._sh(d_ch), pad, d_ch) + print(f" N={n_decl:>4} DSR={dsr:.3f} {'PASS' if dsr >= 0.95 else 'FAIL'}") + + # cosa avrebbe scelto un disonesto (diagnostica, NON una decisione) + diag = [] + for r in rep["rows"][:12]: + s_all = daily_from_target(factory(tf=r["tf"], **r["params"]), r["tf"]) + diag.append({**r, "hold": round(A._sh(s_all[s_all.index >= HOLDOUT]), 3)}) + diag.sort(key=lambda r: -r["hold"]) + print("\n (diagnostica) le stesse 12 celle riordinate per HOLD-OUT — cio' che NON si fa:") + for r in diag[:4]: + print(f" HOLD {r['hold']:+.3f} IS {r['insample_sharpe']:+.3f} {r['tf']:>3} {r['params']}") + + # -- 2. NUMERI del candidato vs il FISSO a pari geometria ----------------------------- + hr("2. CANDIDATO vs TSMOM FISSO 30/90/180 — stessa geometria, stesso TF, stesse fee") + base_ch = daily_from_target(fixed_target, ch["tf"]) + J = pd.concat({"C": d_ch, "B": base_ch}, axis=1, join="inner").dropna() + for lab, sl in (("FULL", J), ("HOLD-OUT", J[J.index >= HOLDOUT])): + if len(sl) < 30: + continue + print(f" {lab:<9} adattivo Sh {A._sh(sl['C']):+.3f} DD {_dd(sl['C'])*100:5.2f}% " + f"CAGR {_cagr(sl['C'])*100:6.2f}% | fisso Sh {A._sh(sl['B']):+.3f} " + f"DD {_dd(sl['B'])*100:5.2f}% CAGR {_cagr(sl['B'])*100:6.2f}% " + f"| dSh {A._sh(sl['C'])-A._sh(sl['B']):+.3f}") + print(f" corr(adattivo, fisso) = {J['C'].corr(J['B']):.3f}") + + print("\n DIFFERENZA APPAIATA PER ANNO (un vantaggio in un anno solo e' un evento):") + rows = paired_by_year(d_ch, base_ch) + for r in rows: + print(f" {r['year']} adatt Sh {r['sh_cand']:+.2f} ({r['ret_cand']:+7.1f}%) " + f"fisso Sh {r['sh_base']:+.2f} ({r['ret_base']:+7.1f}%) dSh {r['d_sh']:+.2f}") + ds = [r["d_sh"] for r in rows] + print(f" -> anni con dSh>0: {sum(1 for x in ds if x > 0)}/{len(ds)} " + f"mediana dSh {np.median(ds):+.2f} media {np.mean(ds):+.2f}") + + # -- 3. LOOKBACK: si muove davvero? --------------------------------------------------- + hr("3. IL LOOKBACK ADATTIVO SI MUOVE? (se sta al bordo e' un TSMOM COSTANTE travestito)") + for a in ASSETS: + df = A.get(a, ch["tf"]) + bpd = A.bars_per_day(df) + L = lookback_bars(df, ch["params"]["det"], ch["params"]["par"], + ch["params"]["mapping"]) / bpd + lo, hi = LMIN_D, LMAX_D + print(f" {a}: L_t giorni min {L.min():.0f} p25 {np.percentile(L,25):.0f} " + f"mediana {np.median(L):.0f} p75 {np.percentile(L,75):.0f} max {L.max():.0f} " + f"| al bordo basso {np.mean(L<=lo+1)*100:.1f}% al bordo alto {np.mean(L>=hi-1)*100:.1f}%" + f" | |dL|/giorno mediano {np.median(np.abs(np.diff(L))):.1f}g") + + # -- 4. GATE --------------------------------------------------------------------------- + hr("4. GATE") + print(A.fmt_marginal(rep["marginal"])) + + cz = A.causality_ok(fn_ch, tf=ch["tf"]) + print(f"\n causality_ok : ok={cz['ok']} max_tail_diff={cz['max_tail_diff']} " + f"(prefissi controllati: {cz['checked']})") + + imp = A.implausible_sharpe(d_ch) + print(f" implausible_sharpe : implausible={imp['implausible']} Sh {imp.get('sharpe')} " + f"maxDD {imp.get('maxdd')} perdite/attive {imp.get('loss_frac')} " + f"attive {imp.get('active_frac')}") + for rr in imp["reasons"]: + print(f" - {rr}") + + dl = delever_null(d_ch, base_ch) + print(f" null del de-levering : {dl}") + + # small-cap a $600 + print(" haircut a $600 (min-order $5):") + for a in ASSETS: + df = A.get(a, ch["tf"]) + sc_c = A.eval_weights_smallcap(df, A._call_target(fn_ch, df, a), capital=600.0) + sc_b = A.eval_weights_smallcap(df, fixed_target(df), capital=600.0) + print(f" {a}: adattivo modellato {sc_c['modeled']['sharpe']:+.3f} -> reale " + f"{sc_c['realistic']['sharpe']:+.3f} (haircut {sc_c['sharpe_haircut']:+.3f}, " + f"{sc_c['n_executed_trades']} trade) | fisso {sc_b['modeled']['sharpe']:+.3f} -> " + f"{sc_b['realistic']['sharpe']:+.3f} (haircut {sc_b['sharpe_haircut']:+.3f}, " + f"{sc_b['n_executed_trades']} trade)") + + # -- 5. BANDA D'ANCORA ----------------------------------------------------------------- + hours = 24 if ch["tf"] == "1d" else 12 + offs = list(range(hours)) + hr(f"5. BANDA D'ANCORA — {len(offs)} ancore, stima onesta = MEDIANA (mai l'ancora 00:00)") + + def _series_at(off: int, which: str) -> pd.Series: + return series_at_anchor(fn_ch if which == "cand" else fixed_target, which, hours, off) + + # sanity: l'ancora 0 riproduce la serie canonica + s0 = _series_at(0, "fixed") + K = pd.concat({"a": s0, "b": base_ch}, axis=1, join="inner").dropna() + print(f" sanity ancora 0 vs serie canonica del fisso: max|Δ| = " + f"{float(np.max(np.abs(K['a']-K['b']))):.3e} (n={len(K)})") + + for lab, mtr in (("Sharpe FULL", A._sh), + ("Sharpe HOLD", lambda s: A._sh(s[s.index >= HOLDOUT]))): + b_c = A.anchor_luck_band(lambda o: _series_at(o, "cand"), offs, metric=mtr) + b_f = A.anchor_luck_band(lambda o: _series_at(o, "fixed"), offs, metric=mtr) + print(f" {lab:<12} adattivo: canonica {b_c['canonical']:+.3f} (pctl {b_c['canonical_pctl']:.2f}) " + f"MEDIANA {b_c['median']:+.3f} banda [{b_c['lo']:+.3f},{b_c['hi']:+.3f}]") + print(f" {'':<12} fisso : canonica {b_f['canonical']:+.3f} (pctl {b_f['canonical_pctl']:.2f}) " + f"MEDIANA {b_f['median']:+.3f} banda [{b_f['lo']:+.3f},{b_f['hi']:+.3f}]") + dlt = A.anchor_luck_delta(lambda o: _series_at(o, "cand"), + lambda o: _series_at(o, "fixed"), offs, metric=mtr) + print(f" {'':<12} DELTA APPAIATO: mediana {dlt['median_paired']:+.3f} " + f"positivo in {dlt['n_positive']}/{dlt['n_anchors']} ancore " + f"banda [{dlt['lo']:+.3f},{dlt['hi']:+.3f}] gate_pass={dlt['gate_pass']}") + + # -- 6. ADATTIVITA' EFFETTIVA DI TUTTA LA FAMIGLIA ------------------------------------ + hr("6. QUANTE DELLE 72 CELLE SONO DAVVERO ADATTIVE? (filtro di MECCANISMO, non di resa)") + print(" criterio: L_t sta a un bordo <50% del tempo E l'IQR di L_t e' >= 10 giorni") + seen, adatt_rows = {}, [] + for r in rep["rows"]: + k = (r["tf"], r["params"]["det"], r["params"]["par"], r["params"]["mapping"]) + if k not in seen: + seen[k] = adaptivity(*k) + r["_adapt"] = seen[k] + if seen[k]["adattiva"]: + adatt_rows.append(r) + n_ad_cells = sum(1 for r in rep["rows"] if r["_adapt"]["adattiva"]) + print(f" celle DAVVERO adattive: {n_ad_cells}/{len(rep['rows'])} " + f"(le altre sono TSMOM a orizzonte COSTANTE travestito)") + for k, v in sorted(seen.items()): + print(f" {k[0]:>3} {k[1]:>6} {str(k[2]):>5} {k[3]:<7} bordo-basso {v['frac_lo']*100:5.1f}% " + f"bordo-alto {v['frac_hi']*100:5.1f}% IQR {v['iqr_days']:5.1f}g " + f"mediana {v['med_days']:5.1f}g -> {'ADATTIVA' if v['adattiva'] else 'costante'}") + + # -- 7. IL CONTROLLO CHE MANCAVA: il MIGLIOR ORIZZONTE COSTANTE ------------------------ + hr(f"7. CONTROLLO L COSTANTE — {len(CONST_GRID)} celle x {len(TFS)} TF = " + f"{len(CONST_GRID)*len(TFS)} trial IN PIU'") + print(" la domanda diventa: il guadagno viene dall'ADATTARE, o dal fatto che su questo") + print(" campione un orizzonte piu' CORTO batte 30/90/180 anche se lo si tiene fisso?") + csel = A.select_cell_insample(lambda tf, L, blend: const_factory(tf, L, blend), + CONST_GRID, TFS) + cch = csel["chosen"] + print(f"\n miglior COSTANTE scelto in-sample: tf={cch['tf']} {cch['params']} " + f"Sharpe IS {cch['insample_sharpe']} FULL {cch['full_sharpe']}") + print(" griglia costante, in-sample (L=90 triple == TP01 canonico):") + for r in csel["rows"]: + tag = " <== TP01 canonico" if (r["params"]["L"] == 90 and r["params"]["blend"] == "triple" + and r["tf"] == "1d") else "" + print(f" IS {r['insample_sharpe']:+.3f} FULL {r['full_sharpe']:+.3f} {r['tf']:>3} " + f"L={r['params']['L']:>3}g {r['params']['blend']}{tag}") + + fn_const = const_factory(**cch["params"], tf=cch["tf"]) + d_const = daily_from_target(fn_const, cch["tf"]) + JC = pd.concat({"C": d_ch, "K": d_const}, axis=1, join="inner").dropna() + print(f"\n ADATTIVO vs MIGLIOR COSTANTE (entrambi scelti in-sample):") + for lab, sl in (("FULL", JC), ("HOLD-OUT", JC[JC.index >= HOLDOUT])): + if len(sl) < 30: + continue + print(f" {lab:<9} adattivo Sh {A._sh(sl['C']):+.3f} DD {_dd(sl['C'])*100:5.2f}% | " + f"costante Sh {A._sh(sl['K']):+.3f} DD {_dd(sl['K'])*100:5.2f}% | " + f"dSh {A._sh(sl['C'])-A._sh(sl['K']):+.3f}") + print(f" corr(adattivo, miglior costante) = {JC['C'].corr(JC['K']):.3f}") + print("\n differenza APPAIATA per anno vs il miglior COSTANTE:") + rows2 = paired_by_year(d_ch, d_const) + for r in rows2: + print(f" {r['year']} adatt {r['sh_cand']:+.2f} costante {r['sh_base']:+.2f} " + f"dSh {r['d_sh']:+.2f}") + d2 = [r["d_sh"] for r in rows2] + print(f" -> anni con dSh>0: {sum(1 for x in d2 if x > 0)}/{len(d2)} " + f"mediana {np.median(d2):+.2f}") + dl2 = delever_null(d_ch, d_const) + print(f" null del de-levering vs costante: {dl2}") + + # ancora appaiata adattivo vs miglior costante (se stesso TF, altrimenti dichiarato) + if cch["tf"] == ch["tf"]: + for lab, mtr in (("Sharpe FULL", A._sh), + ("Sharpe HOLD", lambda s: A._sh(s[s.index >= HOLDOUT]))): + dd2 = A.anchor_luck_delta(lambda o: series_at_anchor(fn_ch, "cand", hours, o), + lambda o: series_at_anchor(fn_const, "const", hours, o), + offs, metric=mtr) + print(f" ancora {lab}: DELTA APPAIATO mediana {dd2['median_paired']:+.3f} " + f"positivo in {dd2['n_positive']}/{dd2['n_anchors']} " + f"banda [{dd2['lo']:+.3f},{dd2['hi']:+.3f}] gate_pass={dd2['gate_pass']}") + else: + print(f" (ancora appaiata non girata: TF diversi, adattivo {ch['tf']} vs " + f"costante {cch['tf']} -> le ancore non sono coppie)") + + # fee sweep: l'adattivo tradà di piu', il conto va fatto + print("\n FEE SWEEP (il candidato ha piu' turnover: e' li' che muore o no)") + for nm, f_ in (("adattivo", fn_ch), ("costante", fn_const), ("TP01 30/90/180", fixed_target)): + tfx = ch["tf"] if nm != "costante" else cch["tf"] + line = [] + for fee in (0.0, 0.0005, 0.001, 0.0015): + dser = A.candidate_daily(f_, tf=tfx, fee_side=fee) + line.append(f"{2*fee*100:.2f}%RT {A._sh(dser):+.3f}") + tt = np.mean([A.eval_weights(A.get(a, tfx), A._call_target(f_, A.get(a, tfx), a)) + ["turnover_per_year"] for a in ASSETS]) + print(f" {nm:<16} " + " ".join(line) + f" | turnover {tt:.0f}x/anno") + + # -- 8. LA FAMIGLIA RISTRETTA ALLE CELLE DAVVERO ADATTIVE ----------------------------- + hr("8. GATE ONESTO SULLA SOLA FAMIGLIA ADATTIVA (la domanda della missione, isolata)") + if not adatt_rows: + print(" NESSUNA cella della famiglia e' davvero adattiva -> la domanda non e'") + print(" misurabile su questa famiglia: ogni 'vincitore' e' un orizzonte costante.") + else: + best_ad = max(adatt_rows, key=lambda r: r["insample_sharpe"]) + print(f" celle adattive: {len(adatt_rows)} miglior IN-SAMPLE: tf={best_ad['tf']} " + f"{best_ad['params']} IS {best_ad['insample_sharpe']} FULL {best_ad['full_sharpe']}") + fn_ad = factory(tf=best_ad["tf"], **best_ad["params"]) + d_ad = daily_from_target(fn_ad, best_ad["tf"]) + sm_ad = A.study_marginal("ADAPT-H(solo adattive)", fn_ad, tf=best_ad["tf"]) + dsr_ad, sr0_ad = A.deflated_sharpe(A._sh(d_ad), + [r["full_sharpe"] for r in adatt_rows], d_ad) + print(f" marginal={sm_ad['marginal_verdict']} earns_slot={sm_ad['earns_slot']} " + f"corr->TP01 {sm_ad['marginal'].get('corr_full')}") + print(f" deflated-Sharpe (N={len(adatt_rows)}) = {dsr_ad:.3f} " + f"{'PASS' if dsr_ad >= 0.95 else 'FAIL'} (null max atteso {sr0_ad:.3f})") + JA = pd.concat({"C": d_ad, "K": d_const, "B": base_1d}, axis=1, join="inner").dropna() + for lab, sl in (("FULL", JA), ("HOLD-OUT", JA[JA.index >= HOLDOUT])): + if len(sl) < 30: + continue + print(f" {lab:<9} adattiva-vera Sh {A._sh(sl['C']):+.3f} | miglior costante " + f"{A._sh(sl['K']):+.3f} | TP01 {A._sh(sl['B']):+.3f}") + rows3 = paired_by_year(d_ad, d_const) + d3 = [r["d_sh"] for r in rows3] + print(f" vs miglior costante, per anno: dSh>0 in {sum(1 for x in d3 if x>0)}/{len(d3)}" + f" mediana {np.median(d3):+.2f}") + + # -- 9. L'OTTIMO COSTANTE E' AL BORDO: si estende la banda ---------------------------- + hr("9. L'OTTIMO COSTANTE CADE SUL BORDO — plateau o scivolata verso il rumore?") + print(" un argmax al bordo della griglia non e' un ottimo: e' il punto in cui si e' smesso") + print(" di guardare. Si estende SOTTO i 20 giorni (8 trial in piu', contati al rialzo).") + ext = sorted(set(CONST_L) | set(CONST_L_EXT)) + tab: dict = {} + for tfx in TFS: + print(f"\n --- TF {tfx}, blend triple (L/3, L, 2L) ---") + print(f" {'L(g)':>5} {'orizzonti':>16} {'IS':>7} {'FULL':>7} {'HOLD':>7} " + f"{'maxDD':>7} {'CAGR':>7} {'turn/a':>7} {'Sh@0.30%RT':>11}") + for L in ext: + fnx = const_factory(tfx, L, "triple") + dx = daily_from_target(fnx, tfx) + dx_h = dx[dx.index >= HOLDOUT] + dx_i = dx[dx.index < HOLDOUT] + d030 = A.candidate_daily(fnx, tf=tfx, fee_side=0.0015) + tt = np.mean([A.eval_weights(A.get(a, tfx), A._call_target(fnx, A.get(a, tfx), a)) + ["turnover_per_year"] for a in ASSETS]) + tag = " <== TP01" if L == 90 else "" + tab[(tfx, L)] = dict(IS=A._sh(dx_i), FULL=A._sh(dx), HOLD=A._sh(dx_h)) + print(f" {L:>5} {f'{L//3}/{L}/{2*L}':>16} {A._sh(dx_i):>7.3f} {A._sh(dx):>7.3f} " + f"{A._sh(dx_h):>7.3f} {_dd(dx)*100:>6.2f}% {_cagr(dx)*100:>6.2f}% " + f"{tt:>7.0f} {A._sh(d030):>11.3f}{tag}") + + # --- e' una CURVA con un ottimo, o un crinale di RUMORE? -------------------------- + print("\n RISOLUZIONE della griglia (regola 07/08: un criterio che decide su una frazione") + print(" dello spread della griglia e' rumore anche quando 'passa'):") + for tfx in TFS: + v = [tab[(tfx, L)]["IS"] for L in ext] + jump = float(np.median(np.abs(np.diff(v)))) + arg = ext[int(np.argmax(v))] + arg_orig = max(CONST_L, key=lambda L: tab[(tfx, L)]["IS"]) + print(f" {tfx:>3}: argmax IS con la griglia ORIGINALE L={arg_orig}g -> con la griglia " + f"ESTESA L={arg}g ({'SPOSTATO' if arg != arg_orig else 'stabile'})") + print(f" salto IS mediano fra L ADIACENTI {jump:+.3f} contro il vantaggio del " + f"vincitore su TP01 {max(v) - tab[(tfx, 90)]['IS']:+.3f} " + f"(rapporto {jump / max(1e-9, max(v) - tab[(tfx, 90)]['IS']):.2f})") + allc = [(tf, L) for tf in TFS for L in ext] + for met in ("IS", "FULL", "HOLD"): + vals = sorted((tab[k][met] for k in allc), reverse=True) + r1d = 1 + sum(1 for x in vals if x > tab[("1d", 90)][met]) + print(f" rango di TP01 (1d, L=90g) fra le {len(allc)} celle su {met}: " + f"{r1d}/{len(allc)} (suo {tab[('1d',90)][met]:+.3f}, migliore {vals[0]:+.3f})") + + hr("FINE") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_dealer_gamma.py b/scripts/research/r0822_dealer_gamma.py new file mode 100644 index 0000000..e91bd44 --- /dev/null +++ b/scripts/research/r0822_dealer_gamma.py @@ -0,0 +1,689 @@ +"""r0822 — DEALER GAMMA: il regime di gamma dei dealer separa mean-reversion da trend su BTC/ETH? + +Filone dell'ondata 2026-08-22. Dato mai usato dal progetto: `data/raw/cb_market_snapshots.parquet` +(ereditato da cerbero-bite, dismesso il 30/07) con `dealer_net_gamma`, `gamma_flip_level`, +`oi_delta_pct_4h`, `liquidation_*_risk`. + +IPOTESI DI MERCATO CLASSICA (due gambe, vanno confermate ENTRAMBE): + (A) dealer LONG gamma -> l'hedging SOPPRIME la vol realizzata; SHORT gamma -> la AMPLIFICA; + (B) dealer LONG gamma -> il prezzo MEAN-REVERTE; SHORT gamma -> il prezzo TRENDA. +La gamba tradeable e' la (B): e' quella che diventerebbe un gate di regime sopra TP01/SKH01. +Aspettativa dichiarata PRIMA di misurare: (A) plausibile ma confusa con il livello di vol; +(B) improbabile su 3 mesi, e comunque a rischio "TP01 travestito". + +ORDINE OBBLIGATO: prima la validazione del DATO (non e' certificato, la fonte non esiste piu'), +poi il FATTO condizionale, e solo dopo — se il fatto c'e' — il gate. + +Girare: nice -n 19 timeout 900 uv run python scripts/research/r0822_dealer_gamma.py +""" +from __future__ import annotations + +import glob +import sys +import warnings +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +import altlib as A # noqa: E402 + +warnings.filterwarnings("ignore") +pd.set_option("display.width", 220) + +RAW = ROOT / "data" / "raw" +SNAP_F = RAW / "cb_market_snapshots.parquet" +CHAIN_D = RAW / "cb_chain" +SEED = 20260822 +ASSETS = ("BTC", "ETH") +HOUR_YEAR = 24 * 365.0 +ANN = np.sqrt(HOUR_YEAR) * 100.0 # da dev.std. oraria a vol annua in punti percentuali + +# La cadenza a 15 min comincia qui: prima ci sono 37 punti GIORNALIERI (vedi sezione 1). +HF_START = pd.Timestamp("2026-05-01 15:00", tz="UTC") + +# ------------------------------------------------------------------ conteggio dei trial +# Ogni cella/statistica valutata viene contata qui, AL RIALZO (regola: il conteggio dei trial +# e' l'unico posto dove barare e' indolore e invisibile — 30/07). Il verdetto stampa la +# ripartizione DERIVATA da questo dizionario, non una somma scritta a mano. +COUNT: dict[str, int] = dict(ricostruzione=0, sma=0, regressioni=0, vol_condizionata=0, + variance_ratio=0, momentum=0, gate=0, delevering=0) +NEFF: dict[str, float] = {} # ampiezza effettiva per asset, misurata in sezione 1e + + +def hr(t: str) -> None: + print("\n" + "=" * 96 + f"\n{t}\n" + "=" * 96) + + +def sh_d(x: pd.Series) -> float: + x = pd.Series(x).dropna() + return float(x.mean() / x.std() * np.sqrt(365.25)) if len(x) > 2 and x.std() > 0 else 0.0 + + +def dd_d(x: pd.Series) -> float: + e = (1.0 + pd.Series(x).dropna()).cumprod() + return float((e / e.cummax() - 1.0).min()) if len(e) else 0.0 + + +# ------------------------------------------------------------------ caricamento +def load_snap() -> pd.DataFrame: + d = pd.read_parquet(SNAP_F).sort_values("ts") + d["ts"] = pd.to_datetime(d["ts"], utc=True).astype("datetime64[ns, UTC]") + return d + + +def load_bars(asset: str) -> pd.DataFrame: + px = A.get(asset, "1h").copy() + px["dt"] = pd.to_datetime(px["timestamp"], unit="ms", utc=True).astype("datetime64[ns, UTC]") + px = px[["dt", "close"]].dropna().reset_index(drop=True) + px["r"] = np.log(px["close"]).diff() + return px + + +def joined(asset: str, snap: pd.DataFrame) -> pd.DataFrame: + """Barra oraria + ULTIMO snapshot con ts <= apertura della barra. + + Causale per costruzione: `direction='backward'` sull'apertura della barra, quindi il + regime usato per la barra [t, t+1h) e' noto a t. Tolleranza 2h: se il collettore e' + fermo da piu' di due ore la riga esce, invece di trascinare un valore vecchio. + """ + px = load_bars(asset) + s = (snap[snap["asset"] == asset] + [["ts", "dealer_net_gamma", "gamma_flip_level", "spot", "dvol", "realized_vol_30d"]] + .dropna(subset=["dealer_net_gamma", "gamma_flip_level"])) + m = pd.merge_asof(px.sort_values("dt"), s.sort_values("ts"), + left_on="dt", right_on="ts", direction="backward", + tolerance=pd.Timedelta("2h")) + m = m[m["ts"].notna()].reset_index(drop=True) + # SEGNO CORRETTO (giustificato nella sezione 2): dealer_net_gamma = -GEX + # dealer_net_gamma > 0 <=> GEX < 0 <=> dealer SHORT gamma + m["short_gamma"] = (m["dealer_net_gamma"] > 0).astype(float) + m["above_flip"] = (m["close"] > m["gamma_flip_level"]).astype(float) + m["rvp24"] = np.sqrt((m["r"] ** 2).rolling(24).mean()) + m["rvp168"] = np.sqrt((m["r"] ** 2).rolling(168).mean()) + m["ret168"] = np.log(m["close"] / m["close"].shift(168)) + m["z"] = m["r"] / m["rvp24"].shift(1) + for k in (4, 24): + m[f"rvf{k}"] = np.sqrt((m["r"].shift(-1) ** 2).rolling(k).sum().shift(-(k - 1)) / k) + return m + + +def ols(y, cols): + y = np.asarray(y, float) + n = len(y) + X = np.column_stack([np.ones(n)] + [np.asarray(c, float) for c in cols]) + b, *_ = np.linalg.lstsq(X, y, rcond=None) + res = y - X @ b + s2 = res @ res / (n - X.shape[1]) + se = np.sqrt(np.diag(s2 * np.linalg.pinv(X.T @ X))) + return b, b / se, 1.0 - np.var(res) / np.var(y) + + +def variance_ratio(r, k: int) -> float: + r = np.asarray(pd.Series(r).dropna(), float) + if len(r) < 5 * k: + return np.nan + x = pd.Series(r).rolling(k).sum().dropna().values + return float(np.var(x) / (k * np.var(r))) + + +# ================================================================== 1. IL DATO +def sezione_1_dato(snap: pd.DataFrame) -> dict: + hr("1. VALIDAZIONE DEL DATO — prima del segnale (fonte dismessa, mai certificata)") + out = {} + print(f"file: {SNAP_F.name} righe {len(snap)} {snap.ts.min()} -> {snap.ts.max()}") + + # 1a. cadenza: il file NON e' omogeneo + d = snap[snap.asset == "BTC"] + gaps = d.ts.diff().dt.total_seconds().div(60).round(0).value_counts() + print(f"\n1a. CADENZA (BTC): distribuzione dei salti in minuti {dict(list(gaps.items())[:6])}") + pre = snap[snap.ts < HF_START] + post = snap[snap.ts >= HF_START] + print(f" -> {len(pre)//2} righe/asset a cadenza GIORNALIERA (12:00 UTC) fino al 2026-05-01,") + print(f" {len(post)//2} righe/asset a 15 min dopo. Il 'file da 4 mesi' e' 90 GIORNI utili.") + out["n_hf_per_asset"] = len(post) // 2 + + for a in ASSETS: + x = post[post.asset == a] + hrs = x.ts.dt.floor("h").nunique() + span = (x.ts.max().floor("h") - x.ts.min().floor("h")).total_seconds() / 3600 + 1 + print(f" {a}: ore distinte {hrs} su {span:.0f} di span = copertura {100*hrs/span:.2f}%" + f" (buchi > 20 min: {int((x.ts.diff().dt.total_seconds()/60 > 20).sum())})") + + # 1b. colonne MORTE — una colonna presente non e' un dato presente + print("\n1b. COLONNE MORTE (regola 8 del brief: contare cio' che VARIA, non le righe)") + for c in ["liquidation_long_risk", "liquidation_short_risk", "macro_days_to_event"]: + v = snap[c].dropna() + print(f" {c:26s} non-nulli {len(v):6d} ({100*len(v)/len(snap):5.1f}%) " + f"valori distinti {v.nunique()} {'*** COSTANTE = ZERO INFORMAZIONE ***' if v.nunique() <= 1 else ''}") + out["dead_cols"] = [c for c in ["liquidation_long_risk", "liquidation_short_risk"] + if snap[c].dropna().nunique() <= 1] + + # 1c. dealer_net_gamma e gamma_flip_level: si muovono? sono plausibili? + print("\n1c. PLAUSIBILITA' delle due colonne di regime (finestra 15 min)") + for a in ASSETS: + x = post[post.asset == a] + g, f = x.dealer_net_gamma, x.gamma_flip_level + rel = x.spot / f - 1.0 + print(f" {a} dealer_net_gamma: nan {100*g.isna().mean():4.1f}% frac>0 {(g>0).mean():.3f} " + f"consecutivi identici {(g.diff()==0).mean():.4f} -> si muove") + print(f" {a} gamma_flip_level: nan {100*f.isna().mean():4.1f}% " + f"consecutivi identici {(f.diff()==0).mean():.4f} " + f"(spot/flip-1) p05 {rel.quantile(.05):+.1%} med {rel.median():+.1%} p95 {rel.quantile(.95):+.1%}") + print(" -> BTC: il flip e' CONGELATO nel 61% dei passi e sta fino a +42% SOTTO lo spot al p95.") + print(" Un 'livello di flip' a 42% dallo spot non e' un livello di flip: e' degenere.") + + # 1d. LE DUE COLONNE SI CONTRADDICONO + print("\n1d. CONTRADDIZIONE INTERNA fra le due colonne della STESSA fonte") + for a in ASSETS: + x = post[post.asset == a].dropna(subset=["dealer_net_gamma", "gamma_flip_level", "spot"]) + c = np.corrcoef((x.dealer_net_gamma > 0).astype(float), (x.spot > x.gamma_flip_level).astype(float))[0, 1] + print(f" {a}: corr( dng>0 , spot>flip ) = {c:+.3f}") + print(" Nella convenzione classica dealer LONG gamma sta SOPRA il flip: le due colonne") + print(" dovrebbero correlare POSITIVAMENTE. Correlano NEGATIVAMENTE -> una delle due ha il") + print(" segno invertito. Quale, lo decide la sezione 2 (ricostruzione dalla catena), non io.") + + # 1e. quante EPISODI di regime ci sono davvero -> il numero che conta + print("\n1e. AMPIEZZA EFFETTIVA — 2.160 ore NON sono 2.160 osservazioni") + for a in ASSETS: + x = post[post.asset == a].dropna(subset=["dealer_net_gamma"]) + lab = (x.dealer_net_gamma > 0).astype(int).values + runs = pd.Series(lab).groupby((pd.Series(lab).diff() != 0).cumsum()).size() * 0.25 # ore + hourly = pd.Series(lab).iloc[::4] + rho = float(hourly.autocorr(1)) + neff = len(hourly) * (1 - rho) / (1 + rho) + print(f" {a}: {len(runs)} episodi; {int((runs>72).sum())} durano >72h e coprono il " + f"{100*runs[runs>72].sum()/runs.sum():.0f}% del tempo") + print(f" AR(1) orario del regime {rho:.4f} -> n_eff ~ {neff:.0f} su {len(hourly)} ore") + out[f"neff_{a}"] = NEFF[a] = neff + print(" -> l'errore standard onesto di ogni statistica condizionata al regime va moltiplicato") + print(f" per ~sqrt(2130/n_eff) = 6x (BTC) - 9x (ETH). E' IL numero della sezione 3.") + return out + + +# ================================================================== 2. SEGNO E RICOSTRUIBILITA' +def sezione_2_ricostruzione(snap: pd.DataFrame) -> dict: + hr("2. DA DOVE VIENE `dealer_net_gamma`, E POSSIAMO RIFARLO OGGI?") + out = {} + print("La domanda non e' accademica: bite e' dismesso, la serie finisce il 2026-07-30. Se il") + print("segnale non e' ricostruibile dalla catena che raccogliamo NOI, il lead e' morto alla nascita.") + + arch = CHAIN_D / "bite_archive.parquet" + d = pd.read_parquet(arch, columns=["ts", "asset", "option_type", "strike", "gamma", + "open_interest", "source"]) + d["ts"] = pd.to_datetime(d["ts"], utc=True).astype("datetime64[ns, UTC]") + d["tsf"] = d["ts"].dt.floor("15min") + per_snap = d.groupby(["source", "asset", "tsf"]).size().rename("n").reset_index() + print("\n2a. STRIKE PER SNAPSHOT nell'archivio ereditato (mediana):") + print(per_snap.groupby(["source", "asset"])["n"].median().to_string()) + print(" -> `bite:live` ha ~16-18 strike: e' la manciata che serviva al suo motore VRP, NON") + print(" una catena. Solo `bite:research` (~280-320 strike) e' ricostruibile. E parte dal") + print(" 2026-06-09, non dal 05-01.") + + # spot: l'archivio ha underlying_price 100% NaN -> lo prendiamo dallo snapshot (stessa fonte) + d = d[(d["source"] == "bite:research")].dropna(subset=["gamma", "open_interest"]) + sp = (snap[["asset", "spot", "dealer_net_gamma", "gamma_flip_level"]] + .assign(tsf=snap["ts"].dt.floor("15min")) + .dropna(subset=["spot"]).drop_duplicates(["asset", "tsf"])) + d = d.merge(sp[["asset", "tsf", "spot"]], on=["asset", "tsf"], how="inner") + print(f"\n (l'archivio ha underlying_price 100% NaN -> spot preso dallo snapshot, stessa fonte)") + + sgn = np.where(d["option_type"].values == "C", 1.0, -1.0) + S = d["spot"].values + G = d["gamma"].values * d["open_interest"].values + d["gex_std"] = G * S * S * 0.01 * sgn # GEX da manuale: dollar-gamma per 1%, call +, put - + d["gex_all"] = G * S * S * 0.01 # senza distinzione call/put + d["gex_nos"] = G * sgn # senza scala S^2 + d["gex_S1"] = G * S * sgn # scala lineare in S + agg = (d.groupby(["asset", "tsf"]) + .agg(**{c: (c, "sum") for c in ["gex_std", "gex_all", "gex_nos", "gex_S1"]}, + n=("gamma", "size")).reset_index()) + + print("\n2b. LA RICOSTRUZIONE (catena near-full `bite:research` vs la colonna di bite)") + for a in ASSETS: + j = (agg[agg.asset == a].merge(sp[sp.asset == a][["tsf", "dealer_net_gamma"]], on="tsf") + .dropna(subset=["dealer_net_gamma"])) + print(f" {a}: n={len(j)} snapshot, ~{j.n.median():.0f} strike ciascuno") + for c in ["gex_std", "gex_all", "gex_nos", "gex_S1"]: + COUNT["ricostruzione"] += 1 + rp = np.corrcoef(j[c], j.dealer_net_gamma)[0, 1] + rs = j[c].corr(j.dealer_net_gamma, method="spearman") + sa = float(np.mean((j[c] > 0) == (j.dealer_net_gamma > 0))) + sl, ic = np.polyfit(j[c], j.dealer_net_gamma, 1) + print(f" {c:8s} corr {rp:+.3f} rank {rs:+.3f} accordo-di-segno {sa:.3f}" + f" fit dng = {sl:+.3f}*GEX {ic:+.2e}") + best = j + out[f"corr_{a}"] = float(np.corrcoef(best.gex_std, best.dealer_net_gamma)[0, 1]) + out[f"slope_{a}"] = float(np.polyfit(best.gex_std, best.dealer_net_gamma, 1)[0]) + out[f"intc_{a}"] = float(np.polyfit(best.gex_std, best.dealer_net_gamma, 1)[1]) + print("\n RISULTATO: il GEX da manuale riproduce la colonna di bite a corr -0.95 (BTC) /") + print(" -0.89 (ETH), con accordo-di-segno del 10%/8%. Cioe':") + print(" dealer_net_gamma ~ -0.7 * GEX_standard -> LA COLONNA E' IL GEX COL SEGNO INVERTITO.") + print(" `dealer_net_gamma > 0` significa dealer SHORT gamma, non long. Il nome mente.") + print(" Questo CHIUDE la contraddizione della sezione 1d: e' la colonna dng ad avere il segno") + print(" girato, il gamma_flip_level e' nella convenzione classica. Da qui in poi il codice usa") + print(" `short_gamma = dealer_net_gamma > 0`.") + + print("\n2c. LA SOGLIA NON E' PORTABILE — e questo e' il punto che uccide il riuso diretto") + for a in ASSETS: + z = -out[f"intc_{a}"] / out[f"slope_{a}"] + print(f" {a}: dng=0 corrisponde a GEX = {z:+.3e}, non a GEX = 0.") + print(" Le due serie hanno zeri DIVERSI (universi diversi: il nostro collettore filtra OI>=100") + print(" e scadenze <=95g). Trasportare 'la soglia zero' da una all'altra e' un cambio di") + print(" definizione mascherato da continuita'. Ricalibrarla sui 90 giorni sarebbe selezione.") + + print("\n2d. IL SEGNALE E' VIVO OGGI? (raccolta nostra, `pyg`, oraria)") + fs = sorted(glob.glob(str(CHAIN_D / "2026-08-*.parquet"))) + if fs: + o = pd.concat([pd.read_parquet(f, columns=["ts", "asset", "option_type", "gamma", + "open_interest", "underlying_price", + "quote_status"]) for f in fs], ignore_index=True) + o["ts"] = pd.to_datetime(o["ts"], utc=True) + o = o.dropna(subset=["gamma", "open_interest", "underlying_price"]) + s2 = np.where(o["option_type"].values == "C", 1.0, -1.0) + o["gex"] = (o["gamma"].values * o["open_interest"].values + * o["underlying_price"].values ** 2 * 0.01 * s2) + g = (o.groupby(["asset", o["ts"].dt.floor("h")]) + .agg(gex=("gex", "sum"), n=("gamma", "size")).reset_index()) + for a in ASSETS: + x = g[g.asset == a] + print(f" {a}: {len(x)} ore dal {x.ts.min():%Y-%m-%d} al {x.ts.max():%Y-%m-%d %H:%M}, " + f"~{x.n.median():.0f} strike/ora, quote ok {100*(o[o.asset==a].quote_status=='ok').mean():.1f}%") + print(f" GEX medio {x.gex.mean():+.3e} frac GEX>0 (dealer LONG gamma) {(x.gex>0).mean():.3f}" + f" ultimo {x.gex.iloc[-1]:+.3e}") + print(" -> RICOSTRUIBILE, oraria, viva, con MEGLIO del dato originale (segno giusto e") + print(" `quote_status` esplicito). Il lead e' inseguibile: il dato non e' il vincolo.") + print(" -> ma nota: ad agosto il GEX e' >0 nell'88-93% delle ore. Il regime 'short gamma'") + print(" compare a sprazzi: la variabile non varia quasi mai, e questo e' un problema") + print(" di potenza che il tempo aggiusta LENTAMENTE.") + else: + print(" (nessun file 2026-08-* nella raccolta: salto)") + return out + + +# ================================================================== 3. IL FATTO (A): la vol +def sezione_3_fatto_vol(M: dict, snap: pd.DataFrame, rec: dict) -> dict: + hr("3. GAMBA (A) DELL'IPOTESI — short gamma amplifica la vol realizzata?") + out = {} + rng = np.random.default_rng(SEED) + print("Misura il FATTO condizionale, non una strategia. Se un effetto e' vero si vede qui.") + + print("\n3a. VOL REALIZZATA FORWARD, condizionata al regime (segno corretto in sezione 2)") + for a in ASSETS: + m = M[a] + for c in ["rvf4", "rvf24"]: + g = m.groupby("short_gamma")[c].mean() * ANN + COUNT["vol_condizionata"] += 1 + print(f" {a} {c}: LONG gamma {g.get(0.0, np.nan):5.1f}% SHORT gamma {g.get(1.0, np.nan):5.1f}%" + f" spread {g.get(1.0,0)-g.get(0.0,0):+5.1f} pp") + print(" Direzione CONFORME al manuale: short gamma -> vol forward piu' alta. Prima gamba OK.") + + print("\n3b. MA: il regime coincide quasi col MESE. Scomposizione (regola: un contributo si") + print(" scompone per periodo PRIMA di crederci — lezione SOL, 22/08)") + for a in ASSETS: + m = M[a].dropna(subset=["rvf24", "short_gamma"]).copy() + m["mo"] = m["dt"].dt.strftime("%Y-%m") + obs = (m.rvf24[m.short_gamma == 1].mean() - m.rvf24[m.short_gamma == 0].mean()) * ANN + print(f" {a} spread pieno {obs:+.1f} pp") + for mo, gg in m.groupby("mo"): + ns, nl = int(gg.short_gamma.sum()), int((1 - gg.short_gamma).sum()) + sp = (gg.rvf24[gg.short_gamma == 1].mean() - gg.rvf24[gg.short_gamma == 0].mean()) * ANN + print(f" {mo}: spread {sp:+6.1f} pp ore SHORT {ns:4d} / LONG {nl:4d}" + f" {'<-- mese quasi interamente in UN regime' if min(ns,nl) < 100 else ''}") + out[f"spread_{a}"] = obs + print(" -> Maggio e Luglio sono mesi LONG-gamma, Giugno e' un mese SHORT-gamma. Il confronto") + print(" 'short vs long' e', in pratica, 'giugno vs maggio+luglio': UN confronto, non 2.136.") + + print("\n3c. NULL a spostamento circolare (conserva l\'autocorrelazione di ENTRAMBE le serie,") + print(" rompe solo l\'allineamento) — e il suo LIMITE DI RISOLUZIONE") + for a in ASSETS: + m = M[a].dropna(subset=["rvf24", "short_gamma"]) + lab, y = m.short_gamma.values, m.rvf24.values + n = len(y) + obs = (y[lab == 1].mean() - y[lab == 0].mean()) * ANN + null = np.empty(2000) + for i in range(2000): + L = np.roll(lab, rng.integers(24, n - 24)) + null[i] = (y[L == 1].mean() - y[L == 0].mean()) * ANN + p = float((np.abs(null) >= abs(obs)).mean()) + runs = pd.Series(lab).groupby((pd.Series(lab).diff() != 0).cumsum()).size() + long_runs = runs[runs > 24] + n_align = n / max(long_runs.median(), 1.0) + print(f" {a}: osservato {obs:+.1f} pp | null media {null.mean():+.2f} sd {null.std():.2f}" + f" | p empirico (2 code) {p:.4f}") + print(f" MA lo spostamento circolare di una serie con episodi lunghi ~{long_runs.median():.0f}h") + print(f" produce solo ~{n_align:.0f} allineamenti DISTINTI: la risoluzione del p non e\'") + print(f" 1/2000, e\' ~1/{n_align:.0f}. Un p 'zero' su {n_align:.0f} configurazioni non e\' 1e-4.") + out[f"p_{a}"] = p + + print("\n3c-bis. LA PROVA CHE COSTA MENO DI TUTTE: si toglie GIUGNO (l\'unico mese short-gamma)") + for a in ASSETS: + m = M[a].dropna(subset=["rvf24", "short_gamma"]).copy() + m["mo"] = m["dt"].dt.strftime("%Y-%m") + for drop in (None, "2026-06"): + g = m if drop is None else m[m.mo != drop] + ns, nl = int(g.short_gamma.sum()), int((1 - g.short_gamma).sum()) + sp = (g.rvf24[g.short_gamma == 1].mean() - g.rvf24[g.short_gamma == 0].mean()) * ANN + tag = "campione pieno" if drop is None else "senza giugno" + print(f" {a} {tag:16s}: spread {sp:+6.1f} pp ore SHORT {ns:4d} / LONG {nl:4d}") + print(" -> tolto UN mese su tre restano ~40-130 ore di regime short su 1.400: lo 'spread'") + print(" diventa il confronto fra due manciate di ore. Non e\' un campione, e\' un episodio.") + + print("\n3d. NULL LOCATION-MATCHED per `gamma_flip_level`: livello di opzioni o media mobile?") + for a in ASSETS: + m = M[a] + g = m.groupby("above_flip")["rvf24"].mean() * ANN + sp_gamma = float(g.get(0.0, np.nan) - g.get(1.0, np.nan)) + print(f" {a} GAMMA spot>flip: sotto {g.get(0.0, np.nan):.1f}% sopra {g.get(1.0, np.nan):.1f}%" + f" spread {sp_gamma:+.1f} pp") + best = None + for nn in (6, 12, 24, 48, 72, 120, 168, 240, 336, 504, 720): + COUNT["sma"] += 1 + sma = m["close"].rolling(nn).mean() + ok = sma.notna() + agree = float((((m["close"] > m["gamma_flip_level"]) == (m["close"] > sma))[ok]).mean()) + gg = m.groupby((m["close"] > sma).astype(float))["rvf24"].mean() * ANN + spread = float(gg.get(0.0, np.nan) - gg.get(1.0, np.nan)) + if best is None or agree > best[1]: + best = (nn, agree, spread) + if best[2] > sp_gamma + 1.0: + verdetto = "il sosia da uno spread PIU GRANDE del livello di opzioni" + elif abs(best[2] - sp_gamma) <= 1.0: + verdetto = "il sosia da lo STESSO spread" + else: + verdetto = "il sosia da uno spread minore" + print(f" miglior sosia: spot>SMA({best[0]}h) -> accordo di regime {best[1]:.3f}, " + f"spread {best[2]:+.1f} pp ({verdetto})") + out[f"sma_agree_{a}"], out[f"sma_span_{a}"] = best[1], best[0] + out[f"sma_spread_{a}"], out[f"flip_spread_{a}"] = best[2], sp_gamma + print(" LETTURA, asset per asset (i due casi NON dicono la stessa cosa):") + print(" BTC: accordo 0.68, e la media mobile NUDA da' uno spread uguale o maggiore") + print(" (+8.8 contro +7.8 pp) -> il livello di opzioni non aggiunge nulla a una SMA.") + print(" ETH: accordo 0.96 -> `gamma_flip_level` E' una media mobile a 30 giorni con un") + print(" altro nome. Il suo spread e' piu' grande (+16.6 vs +10.1 pp), ma la") + print(" differenza vive tutta nel 4% di ore in cui i due regimi divergono: 4% di") + print(" 2.160 ore dentro un campione da 3 episodi non e' evidenza, e non e' testabile.") + print(" In nessuno dei due casi c'e' un contenuto 'opzioni' isolabile: e' la trappola 2 del") + print(" brief (TP01 travestito). `gamma_flip_level` non e' usabile come regime.") + + print("\n3e. IL REGIME AGGIUNGE SOPRA CIO' CHE GIA' SAPPIAMO? (log rv fwd 24h ~ vol passata + trend)") + print(" t_eff = t / sqrt(24) corregge la sovrapposizione della finestra a 24h. NON corregge") + print(" l'ampiezza effettiva (sezione 1e: n_eff 24-53 su 2.130): un t_eff di +2.3 su 6-11") + print(" episodi indipendenti resta un t che conta episodi come se fossero ore.") + for a in ASSETS: + m = M[a] + d = m.dropna(subset=["rvf24", "rvp24", "rvp168", "ret168", "dvol", "short_gamma", "above_flip"]) + d = d[d.rvf24 > 0] + y = np.log(d.rvf24) + base = [np.log(d.rvp24), np.log(d.rvp168), d.ret168] + d = d.assign(sma720=(d["close"] > d["close"].rolling(720).mean()).astype(float).fillna(0.0)) + print(f" {a} (n={len(d)}) corr(short_gamma, DVOL) = " + f"{np.corrcoef(d.short_gamma, d.dvol)[0,1]:+.3f}") + for lab, extra in [("base: vol+trend", []), + (" + spot>SMA720", [d.sma720]), + (" + spot>flip", [d.above_flip]), + (" + short_gamma", [d.short_gamma]), + (" + DVOL", [np.log(d.dvol)]), + (" + DVOL + short_gamma", [np.log(d.dvol), d.short_gamma])]: + COUNT["regressioni"] += 1 + b, t, r2 = ols(y, base + extra) + print(f" {lab:24s} R2 {r2:.3f} ultimo coef {b[-1]:+.3f} t {t[-1]:+6.2f}" + f" t_eff {t[-1]/np.sqrt(24):+5.2f}") + print(" -> BTC: `short_gamma` da solo spiega quanto DVOL da solo (R2 0.271 vs 0.272) e i due") + print(" insieme fanno 0.312 -> l'informazione e' in larga parte GIA' NEL DVOL, che il") + print(" progetto ha gia' provato come modulatore di TP01 il 26/06: era de-levering puro.") + print(" ETH: t_eff ~ +1.0 = rumore.") + return out + + +# ================================================================== 4. IL FATTO (B): MR vs trend +def sezione_4_fatto_mrtrend(M: dict) -> dict: + hr("4. GAMBA (B) DELL'IPOTESI — la gamba TRADEABILE: short gamma = trend, long gamma = MR?") + out = {} + print("Questa e' la gamba che diventerebbe un gate. La (A) e' solo il contorno.") + + print("\n4a. VARIANCE RATIO per regime. VR<1 = mean-reversion, VR>1 = trend.") + print(" La versione 'z' standardizza ogni ritorno per la vol delle 24h precedenti: senza,") + print(" il clustering di volatilita' DENTRO un regime gonfia il VR e si legge come trend.") + for a in ASSETS: + m = M[a] + for lab, col in [("grezzo", "r"), ("z-std", "z")]: + L = {k: variance_ratio(m[col][m.short_gamma == 0], k) for k in (2, 4, 12, 24)} + S = {k: variance_ratio(m[col][m.short_gamma == 1], k) for k in (2, 4, 12, 24)} + COUNT["variance_ratio"] += 4 + print(f" {a} {lab}: LONG " + " ".join(f"VR{k}={L[k]:.3f}" for k in (2, 4, 12, 24))) + print(f" {a} {lab}: SHORT " + " ".join(f"VR{k}={S[k]:.3f}" for k in (2, 4, 12, 24))) + if lab == "z-std": + out[f"vr24_long_{a}"], out[f"vr24_short_{a}"] = L[24], S[24] + print("\n LETTURA (versione z a 24h, l'unica scale-free):") + for a in ASSETS: + vl, vs = out[f"vr24_long_{a}"], out[f"vr24_short_{a}"] + if abs(vl - vs) < 0.05: + diag = "NESSUNA separazione (differenza < 0.05)" + elif vs > vl: + diag = "separazione NEL VERSO dell'ipotesi (short gamma piu' trendante)" + else: + diag = "separazione ROVESCIATA: e' il regime LONG gamma a trendare" + print(f" {a}: LONG {vl:.3f} vs SHORT {vs:.3f} -> {diag}") + print(" Due asset, due risposte diverse, e quella che separa lo fa al contrario. La gamba (B)") + print(" non e' 'debole': e' INCOERENTE. Con la gamba (A) confermata, questo e' il punto in cui") + print(" l'ipotesi si rompe — perche' e' la (B) che si traderebbe.") + + print("\n4b. La forma tradeable: payoff di momentum per regime, sgn(ritorno passato L) x ritorno") + print(" futuro H. Sharpe ANNUALIZZATO LORDO (nessuna fee: se non regge lordo e' finita).") + Ls, Hs = (1, 4, 12, 24), (1, 4, 12, 24) + cells = [] + for a in ASSETS: + m = M[a] + print(f" {a} " + " ".join(f"H={h:<2d}" for h in Hs) + " (L=long-gamma, S=short-gamma)") + for L in Ls: + past = np.log(m["close"] / m["close"].shift(L)) + row = [] + for H in Hs: + fwd = np.log(m["close"].shift(-H) / m["close"]) + pay = np.sign(past) * fwd + for lab, mask in [("L", m.short_gamma == 0), ("S", m.short_gamma == 1)]: + v = pay[mask].dropna() + s = float(v.mean() / v.std() * np.sqrt(HOUR_YEAR / H)) if len(v) > 30 and v.std() > 0 else np.nan + COUNT["momentum"] += 1 + cells.append((a, L, H, lab, s, v)) + row.append(f"{lab}{s:+.2f}") + print(f" L={L:2d}h " + " ".join(row)) + se = np.sqrt(HOUR_YEAR / len(m)) + neff = NEFF.get(a, len(m)) + print(f" [errore standard di UNA cella a H=1h: +/-{se:.2f} di Sharpe su {len(m)} ore.") + print(f" Con l'ampiezza effettiva della sezione 1e (n_eff={neff:.0f}) diventa" + f" +/-{se*np.sqrt(len(m)/neff):.1f}.") + print(" Ogni numero della tabella qui sopra e' dentro il proprio errore standard.]") + out["cells"] = cells + return out + + +# ================================================================== 5. IL GATE SUL LIBRO +def sezione_5_gate(snap: pd.DataFrame, fatti4: dict) -> dict: + hr("5. IL GATE SUL LIBRO — costruito e misurato, con la potenza dichiarata") + out = {} + print("La sezione 4 ha gia' refutato la gamba tradeable. Il gate si costruisce lo stesso, perche'") + print("un 'non funziona' misurato vale piu' di un 'non l'ho provato'.") + + B = A.tp01_baseline_daily() + reg = {} + for a in ASSETS: + s = (snap[(snap.asset == a) & (snap.ts >= HF_START)] + .dropna(subset=["dealer_net_gamma"])[["ts", "dealer_net_gamma"]] + .set_index("ts").resample("1D").last()) + # ultimo snapshot del giorno d -> usato per il giorno d+1: causale per costruzione + reg[a] = (s["dealer_net_gamma"] > 0).astype(float).shift(1) + R = pd.concat(reg, axis=1).mean(axis=1).dropna() + J = pd.concat({"B": B, "S": R}, axis=1, join="inner").dropna() + b = J["B"] + n = len(J) + se = np.sqrt(365.25 / n) + print(f"\n5a. FINESTRA: {J.index.min():%Y-%m-%d} -> {J.index.max():%Y-%m-%d}, {n} giorni.") + print(f" TP01 (baseline 50/50) su questa finestra: Sharpe {sh_d(b):+.2f} maxDD {dd_d(b):.2%}" + f" ritorno {(1+b).prod()-1:+.2%}") + print(f" ERRORE STANDARD DELLO SHARPE su {n} giorni: +/-{se:.2f}.") + print(" Cioe': su questa finestra TP01 e' indistinguibile da qualunque cosa fra -4 e 0. Il") + print(" libro non e' misurabile qui, e nessun gate lo sara'. E' il vincolo, non un dettaglio.") + print(f" (per giunta TP01 e' quasi FLAT: ritorno lordo {100*(1+b).prod()-100:+.2f}% in 3 mesi)") + out["n_days"], out["se_sharpe"] = n, se + + print("\n5b. QUATTRO gate dichiarati (2 polarita' x 2 intensita'), + il null del de-levering") + variants = { + "classico SHORT->1.5x LONG->0.5x": (0.5, 1.5), + "classico mite SHORT->1.25 LONG->0.75": (0.75, 1.25), + "INVERSO SHORT->0.5x LONG->1.5x": (1.5, 0.5), + "risk-off in SHORT (1.0 / 0.5)": (1.0, 0.5), + } + rows = [] + for name, (lo, hi) in variants.items(): + x = b * (lo + (hi - lo) * J["S"]) + COUNT["gate"] += 1 + rows.append((name, sh_d(x), dd_d(x))) + print(f" {name:38s} Sharpe {sh_d(x):+.2f} maxDD {dd_d(x):.2%}") + print("\n NULL DEL DE-LEVERING (obbligatorio su ogni claim di DD — 5 occorrenze nel progetto):") + for k in (0.5, 0.6, 0.7, 0.8, 0.9, 1.0): + COUNT["delevering"] += 1 + x = b * k + print(f" baseline x{k:.1f} Sharpe {sh_d(x):+.2f} maxDD {dd_d(x):.2%}") + best_dd = min(rows, key=lambda r: abs(r[2])) + print(f"\n -> il gate 'classico' porta il maxDD da {dd_d(b):.2%} a {rows[0][2]:.2%} con Sharpe" + f" {rows[0][1]:+.2f} (peggiore del baseline {sh_d(b):+.2f});") + print(f" il semplice `baseline x0.6` fa maxDD {dd_d(b*0.6):.2%} a Sharpe {sh_d(b*0.6):+.2f}.") + print(" NULL DE-LEVERING: **FALLITO**. Il gate e' meno leva con passaggi in piu'.") + out["null_delevering"] = "FALLITO" + + print("\n5c. `marginal_vs_tp01` sullo stream INCREMENTALE del gate (cio' che il gate aggiunge)") + ov = (b * (0.5 + 1.0 * J["S"])) - b + try: + rep = A.marginal_vs_tp01(ov) + for k in ("marginal_verdict", "n_days", "n_hold_days", "corr_full", "cand_full_sharpe", + "beta_to_tp01", "reason"): + if k in rep: + print(f" {k:20s} {rep[k]}") + bl = rep.get("blends", {}) + for w, v in bl.items(): + print(f" blend {w}: full {v.get('full')} uplift_full {v.get('uplift_full')}" + f" uplift_hold {v.get('uplift_hold')}") + out["marginal"] = rep.get("marginal_verdict") + except Exception as e: # pragma: no cover + out["marginal"] = f"errore: {e}" + print(f" non girato: {e}") + print(" ATTENZIONE alla lettura: `n_hold_days` == `n_days` — i 90 giorni cadono INTERAMENTE") + print(" dentro l'hold-out di altlib, quindi 'full' e 'hold' sono LA STESSA FINESTRA e i loro") + print(" uplift coincidono (si vede sopra: -0.066 e -0.066). I gate multi-cut e il jackknife") + print(" di `marginal_vs_tp01` girano su una finestra sola.") + print(" Un ADDS qui non sarebbe un ADDS: sarebbe la definizione di selezione") + print(" sull'hold-out. Si riporta il verdetto, non lo si usa per promuovere.") + + print("\n5d. DEFLATED SHARPE sulla cella migliore della griglia della sezione 4") + cells = [c for c in fatti4["cells"] if np.isfinite(c[4])] + best = max(cells, key=lambda c: c[4]) + a, L, H, lab, s_best, v = best + allsr = [c[4] for c in cells] + try: + dsr, null_max = A.deflated_sharpe(s_best, allsr, pd.Series(v.values).dropna().values, + dpy=HOUR_YEAR / H) + reg = "SHORT" if lab == "S" else "LONG" + print(f" cella migliore: {a} L={L}h H={H}h regime={reg} gamma -> Sharpe LORDO {s_best:+.2f}" + f" (su {len(allsr)} trial dichiarati)") + print(f" deflated Sharpe = {dsr:.3f} -> {'PASS' if dsr >= 0.95 else 'FAIL'} (soglia 0.95)") + print(f" massimo Sharpe ATTESO SOTTO IL NULLO con {len(allsr)} trial = {null_max:+.2f}") + if s_best < null_max: + print(f" -> la cella migliore ({s_best:+.2f}) sta SOTTO cio' che il puro rumore produce") + print(f" ({null_max:+.2f}) cercando {len(allsr)} volte. Non c'e' niente da deflazionare:") + print(" la griglia non ha prodotto nemmeno il massimo che il caso avrebbe prodotto.") + out["dsr"], out["dsr_nullmax"] = float(dsr), float(null_max) + except Exception as e: # pragma: no cover + out["dsr"] = f"errore: {e}" + print(f" non girato: {e}") + + print("\n5e. CAUSALITA'") + print(" Per COSTRUZIONE: ogni riga usa `merge_asof(direction='backward')` sull'APERTURA della") + print(" barra con tolleranza 2h, quindi il regime della barra [t,t+1h) e' noto a t; il gate") + print(" giornaliero usa l'ultimo snapshot del giorno d-1 (`.shift(1)`).") + print(" `A.causality_ok` non e' applicabile: perturba il futuro dei prezzi certificati, ma il") + print(" regime qui non viene dai prezzi — viene da un file esterno che copre 90 giorni su 8") + print(" anni. Girarlo darebbe un PASS privo di potenza sul 97% del campione (stessa forma del") + print(" test a potenza zero corretto il 21/08). Si dichiara l'argomento strutturale, non un") + print(" numero finto.") + out["causality"] = "per costruzione (merge_asof backward + shift(1)); A.causality_ok non applicabile" + return out + + +# ================================================================== main +def main() -> None: + hr("r0822 DEALER-GAMMA — il regime di gamma dei dealer separa MR da trend su BTC/ETH?") + print("IPOTESI: (A) short gamma amplifica la vol; (B) short gamma fa TRENDARE il prezzo, long") + print("gamma lo fa MEAN-REVERTERE -> gate di regime sopra TP01/SKH01.") + print("ATTESA DICHIARATA PRIMA DI MISURARE: (A) plausibile ma confusa col livello di vol;") + print("(B) improbabile su 3 mesi e a rischio 'TP01 travestito'.") + + snap = load_snap() + d1 = sezione_1_dato(snap) + d2 = sezione_2_ricostruzione(snap) + + hf = snap[snap.ts >= HF_START] + M = {a: joined(a, hf) for a in ASSETS} + for a in ASSETS: + print(f"\n[merge] {a}: {len(M[a])} barre orarie con regime, " + f"{M[a].dt.min():%Y-%m-%d} -> {M[a].dt.max():%Y-%m-%d}") + + d3 = sezione_3_fatto_vol(M, hf, d1) + d4 = sezione_4_fatto_mrtrend(M) + d5 = sezione_5_gate(snap, d4) + + hr("VERDETTO") + tot = sum(COUNT.values()) + print(f"GRIGLIA DICHIARATA: {tot} celle/statistiche valutate, contate al rialzo:") + for k, v in COUNT.items(): + print(f" {k:18s} {v:4d}") + print(" Nessuna griglia e' stata ridotta per budget: il vincolo qui e' il DATO, non la macchina.") + print(f" Le {COUNT['momentum']} celle di momentum sono quelle passate al deflated Sharpe (5d).") + print() + print("SCARTATO. La gamba (A) e' confermata nella direzione ma non nella potenza; la gamba (B),") + print("che e' quella che si traderebbe, e' INCOERENTE fra i due asset e rovesciata dove separa.") + print() + print(" 1. Il dato regge meno di quanto dichiara: 15 min solo dal 2026-05-01 (90 giorni, non 4") + print(" mesi); 2 colonne su 16 COSTANTI; `gamma_flip_level` congelato al 61% su BTC.") + print(" 2. `dealer_net_gamma` e' il GEX COL SEGNO INVERTITO (corr -0.95 BTC / -0.89 ETH contro") + print(" la ricostruzione dalla catena). Chi lo usasse col nome che porta leggerebbe ogni") + print(" regime al contrario. -> Questo e' il risultato riusabile di tutto il filone.") + print(" 3. `gamma_flip_level` e' una media mobile: 96% di accordo con spot>SMA(720h) su ETH; su") + print(" BTC (accordo 0.68) la SMA nuda da' uno spread di vol MAGGIORE del livello stesso.") + print(" Niente contenuto di opzioni isolabile. Trappola 2 del brief.") + print(" 4. Ampiezza effettiva: 6-8 episodi di regime, n_eff 24-53 ore su 2.130. Il regime") + print(" coincide col mese (maggio LONG, giugno SHORT, luglio LONG): lo spread di vol e' UN") + print(" confronto fra tre mesi, non 2.136 osservazioni.") + print(" 5. Cio' che resta della gamba (A) e' quasi tutto DVOL (corr 0.66 su BTC), e il DVOL") + print(" come modulatore di TP01 e' gia' stato refutato il 26/06: era de-levering.") + print(" 6. Il gate misurato FALLISCE il null del de-levering (baseline x0.6 lo domina) su una") + print(" finestra dove SE(Sharpe) = +/-2.0 e TP01 e' praticamente flat.") + print(" 7. Deflated Sharpe 0.440 = FAIL, e nel modo piu' netto: la cella migliore delle 64") + print(" (+4.98 LORDO) sta SOTTO il massimo che 64 estrazioni di puro rumore producono") + print(" (+5.12). Non c'e' un candidato da deflazionare, c'e' una griglia sotto il rumore.") + print() + print("NON e' un LEAD: un lead richiede una soglia e una data che abbiano senso, e qui la soglia") + print("non e' nemmeno portabile fra la serie storica e quella che raccogliamo (zeri diversi,") + print("universi diversi). Ricalibrarla sui 90 giorni sarebbe la selezione che il progetto rifiuta.") + print() + print("COSA CONSERVARE (non e' una strategia, e' infrastruttura):") + print(" - il GEX si ricostruisce dalla NOSTRA catena, oraria, viva (518 ore dal 2026-08-01,") + print(" ~209-262 strike, quote ok ~100%), col segno GIUSTO e `quote_status` esplicito;") + print(" - se un giorno lo si riapre, si riapre su GEX ricostruito da noi, mai sulla colonna") + print(" ereditata, e non prima di avere ~30-40 EPISODI di regime (non 30-40 giorni):") + print(" al ritmo osservato (6-8 episodi / 90 giorni) sono ~2 anni, cioe' meta' 2028.") + print() + print("COSA MI SMENTIREBBE: una separazione del variance ratio z-standardizzato nello STESSO") + print("verso sui due asset (short gamma VR>1, long gamma VR<1) su GEX ricostruito da noi, con") + print("almeno 30 episodi di regime indipendenti e uno spread che sopravvive al controllo") + print("location-matched contro spot>SMA. Oggi ho l'opposto: BTC non separa, ETH separa al rovescio.") + print() + print(f"SCRIPT: scripts/research/r0822_dealer_gamma.py") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_flow_squeeze.py b/scripts/research/r0822_flow_squeeze.py new file mode 100644 index 0000000..18e0e36 --- /dev/null +++ b/scripts/research/r0822_flow_squeeze.py @@ -0,0 +1,639 @@ +"""r0822_flow_squeeze.py — filone FLOW-SQUEEZE (ondata 2026-08-22). + +DOMANDA + Il posizionamento affollato (variazione rapida dell'open interest) ACCOPPIATO al rischio + di liquidazione produce un movimento prevedibile — squeeze o cascata — nelle ore successive? + E' l'unico angolo di "flusso" mai chiuso: il filone funding e' chiuso su 3 lati (carry CC01, + time-series, cross-sectional) ma sempre come LIVELLO del costo di carry, mai come PROXY DI + AFFOLLAMENTO accoppiato al rischio di liquidazione. + +PERIMETRO (dichiarato: un altro filone dell'ondata lavora su dealer_net_gamma/gamma_flip_level, + che qui NON si toccano) + data/raw/cb_market_snapshots.parquet -> oi_delta_pct_4h, liquidation_long_risk, + liquidation_short_risk, funding_perp_annualized, funding_cross_annualized, iv_minus_rv + data/raw/hlfund__1h.parquet -> funding orario Hyperliquid (BTC/ETH, ~3 anni) + data/raw/cb_chain/ -> open interest per strike (per la RICOSTRUIBILITA') + +IPOTESI A PRIORI, REGISTRATA PRIMA DI MISURARE + Attesa: NULLA di deployabile. (a) il dataset e' MORTO il 2026-07-30 (progetto esterno + dismesso) e ha ~3 mesi utili -> verdetto massimo LEAD; (b) il progetto ha gia' chiuso il + funding su 3 lati; (c) il sospetto principale e' che un eventuale effetto sull'estremo di + oi_delta sia "il prezzo e' appena crollato" travestito, cioe' momentum/mean-reversion di + prezzo gia' coperta. Cio' che NON mi aspettavo: che meta' dell'ipotesi (il rischio di + liquidazione) fosse **inesistente nel dato**. + +TEST DI POTENZA — DICHIARATO PRIMA DI LEGGERE I RISULTATI (sezione 2) + Il campione utile e' ~90 giorni x 2 asset. La MDE (minimum detectable effect a 2 SE) si + calcola sul numero di finestre NON sovrapposte, non sulle righe orarie. Un effetto sotto la + MDE NON e' un fatto stabilito, comunque venga il p-value di un bootstrap su dati sovrapposti. + +COME SI LEGGE IL RISULTATO + Le sezioni 1 e 5 (validazione e ricostruibilita') valgono piu' delle 2-4: se una colonna e' + costante non ha potenza, e se non sappiamo piu' produrla il segnale e' morto alla nascita. + +USO: nice -n 19 timeout 900 uv run python scripts/research/r0822_flow_squeeze.py +""" +from __future__ import annotations + +import bisect +import glob +import sys +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +import altlib as A # noqa: E402 + +RAW = ROOT / "data" / "raw" +SNAP_FILE = RAW / "cb_market_snapshots.parquet" +ASSETS = ("BTC", "ETH") +# finestra utile: prima del 01/05 gli snapshot sono ~1/giorno (vedi sezione 1) +W0 = pd.Timestamp("2026-05-01", tz="UTC") +HOURS = (4, 12, 24) + +# GRIGLIA DICHIARATA (conteggio al RIALZO, incluse le varianti scartate) +GRID_DECLARED = ( + 10 * 3 * 2 # 60 decili di oi_delta x 3 orizzonti x 2 lenti (demean per asset / per mese) + + 4 * 2 # 8 2x2 affollamento (OI alto/basso x funding alto/basso) x 2 orizzonti + + 4 * 2 # 8 specificazioni di regressione x 2 orizzonti + + 10 * 2 # 20 decili di funding HL x 2 orizzonti + + 3 * 3 * 2 # 18 griglia del segnale: soglia x orizzonte di holding x segno + + 2 # 2 null del segnale (sempre-short, timing casuale) + + 2 # 2 ricostruibilita' (2 asset) +) # = 118 + + +def hdr(t: str) -> None: + print("\n" + "=" * 78 + f"\n{t}\n" + "=" * 78) + + +# --------------------------------------------------------------------------- +# dati +# --------------------------------------------------------------------------- +def load_snapshots() -> pd.DataFrame: + s = pd.read_parquet(SNAP_FILE) + # epoca ESPLICITA in ms: .view("int64") su tz-aware non-ns e' un look-ahead noto + s["ts_ms"] = s["ts"].astype("datetime64[ms, UTC]").astype("int64") + return s.sort_values("ts_ms").reset_index(drop=True) + + +def snap_num(snap: pd.DataFrame, asset: str, cols: list[str]) -> pd.DataFrame: + d = snap[snap.asset == asset][["ts_ms"] + cols].copy() + for c in cols: + d[c] = pd.to_numeric(d[c], errors="coerce") + return d.sort_values("ts_ms").reset_index(drop=True) + + +def merge_causal(df: pd.DataFrame, right: pd.DataFrame) -> pd.DataFrame: + """Snapshot noto alla CHIUSURA della barra i (= timestamp[i] + 1h), backward, tolleranza 2h. + La posizione decisa a i e' poi tenuta durante i+1 (lo shift lo fa eval_weights).""" + left = pd.DataFrame({"ts_ms": (df["timestamp"].astype("int64") + 3_600_000).values}) + m = pd.merge_asof(left, right, on="ts_ms", direction="backward", + tolerance=2 * 3600 * 1000) + return m.drop(columns=["ts_ms"]) + + +def panel(snap: pd.DataFrame, cols: list[str], start=W0) -> pd.DataFrame: + out = [] + for a in ASSETS: + df = A.get(a, "1h") + df = df[df.datetime >= start - pd.Timedelta(days=7)].reset_index(drop=True) + df = pd.concat([df, merge_causal(df, snap_num(snap, a, cols))], axis=1) + cc = df["close"].values.astype(float) + for H in HOURS: + f = np.full(len(cc), np.nan) + f[:-H] = cc[H:] / cc[:-H] - 1.0 + df[f"f{H}"] = f + p4 = np.full(len(cc), np.nan) + p4[4:] = cc[4:] / cc[:-4] - 1.0 + df["p4"] = p4 + df["asset"] = a + out.append(df) + P = pd.concat(out, ignore_index=True) + return P[P.datetime >= start].reset_index(drop=True) + + +# --------------------------------------------------------------------------- +# 1. VALIDAZIONE DEL DATO +# --------------------------------------------------------------------------- +def sezione1(snap: pd.DataFrame) -> None: + hdr("1. VALIDAZIONE DEL DATO — le colonne vengono da un progetto esterno DISMESSO") + print(f"righe={len(snap)} ts {snap.ts.min()} -> {snap.ts.max()}") + cnt = snap[snap.asset == "BTC"].set_index("ts").resample("1D").size() + fasce = pd.cut(cnt, [-1, 0, 10, 50, 90, 400]).value_counts().sort_index() + print("\ngiorni per numero di snapshot/giorno (BTC):") + for k, v in fasce.items(): + print(f" {str(k):>14s} : {v:3d} giorni") + pieni = cnt[cnt >= 90] + print(f" -> primo giorno a cadenza piena (>=90/g): {pieni.index.min().date()}" + f" ; giorni pieni = {len(pieni)}") + print(" ATTENZIONE: la copertura dichiarata parte dal 2026-03-26 ma 36 giorni hanno <=10") + print(" snapshot: la finestra USABILE a risoluzione oraria e' 2026-05-01 -> 2026-07-30.") + + for a in ASSETS: + d = snap[snap.asset == a] + print(f"\n--- {a} (n={len(d)}) ---") + ok = d.fetch_ok.value_counts().to_dict() + print(f" fetch_ok: {ok} -> fallite {100*(1-d.fetch_ok.mean()):.1f}%") + for c in ("liquidation_long_risk", "liquidation_short_risk"): + vc = d[c].value_counts(dropna=False).to_dict() + nun = d[c].nunique() + flag = " <<< COSTANTE: ZERO POTENZA" if nun <= 1 else "" + print(f" {c:26s} categorie={nun} {vc}{flag}") + for c in ("oi_delta_pct_4h", "funding_perp_annualized", "funding_cross_annualized", + "iv_minus_rv", "macro_days_to_event"): + s = pd.to_numeric(d[c], errors="coerce") + mode_share = s.value_counts(normalize=True).iloc[0] if s.notna().any() else np.nan + print(f" {c:26s} nan={100*s.isna().mean():5.1f}% nuniq={s.nunique():5d} " + f"moda={s.mode().iloc[0] if s.notna().any() else float('nan'):+.4f} " + f"({100*mode_share:.1f}% delle righe) " + f"[{s.min():+.3f}, {s.median():+.3f}, {s.max():+.3f}]") + s = pd.to_numeric(d.oi_delta_pct_4h, errors="coerce").dropna() + same = (s.diff() == 0) + print(f" oi_delta_pct_4h: congelato {100*same.mean():.2f}% delle volte, " + f"autocorr(15m)={s.autocorr(1):+.3f} autocorr(4h)={s.autocorr(16):+.3f}") + print(" -> autocorr alta a 15m e ~0 a 4h = firma di una VERA finestra mobile a 4h " + "(non un valore inventato/congelato)") + + print("\n>>> ESITO SEZIONE 1") + print(" (a) liquidation_long_risk e liquidation_short_risk valgono 'low' nel 100% delle") + print(" righe non-nulle, su 17.406 righe, 4 mesi, entrambi gli asset: UNA sola") + print(" categoria. META' DELL'IPOTESI E' UNTESTABILE PER COSTRUZIONE — non 'debole',") + print(" proprio senza varianza. E' il risultato piu' importante del filone.") + print(" (b) funding_perp_annualized e' ANCORATO al tasso base (0.01%/8h = 0.1095/anno)") + print(" nel ~49% (BTC) / ~60% (ETH) delle righe: come proxy di affollamento e' muto") + print(" per meta' del campione.") + print(" (c) oi_delta_pct_4h e' l'unica colonna del perimetro che si comporta da serie vera.") + + +# --------------------------------------------------------------------------- +# 2. IL FATTO +# --------------------------------------------------------------------------- +def bboot_p(x: np.ndarray, nb: int = 2000, block: int = 24, seed: int = 7) -> float: + x = np.asarray(x, float) + x = x[np.isfinite(x)] + n = len(x) + if n < block * 3: + return float("nan") + rng = np.random.default_rng(seed) + k = int(np.ceil(n / block)) + starts = rng.integers(0, n - block, size=(nb, k)) + means = np.array([np.concatenate([x[s:s + block] for s in starts[i]])[:n].mean() + for i in range(nb)]) + return float((means >= 0).mean() if x.mean() < 0 else (means <= 0).mean()) + + +def ols_bboot(d: pd.DataFrame, y: str, cols: list[str], nb: int = 500, + blk: int = 48, seed: int = 11): + X = np.column_stack([np.ones(len(d))] + [d[c].values for c in cols]) + Y = d[y].values + b = np.linalg.lstsq(X, Y, rcond=None)[0] + rng = np.random.default_rng(seed) + n = len(Y) + k = int(np.ceil(n / blk)) + bs = [] + for _ in range(nb): + st = rng.integers(0, n - blk, size=k) + idx = np.concatenate([np.arange(s, s + blk) for s in st])[:n] + try: + bs.append(np.linalg.lstsq(X[idx], Y[idx], rcond=None)[0]) + except np.linalg.LinAlgError: + pass + se = np.array(bs).std(0) + return b, se + + +def sezione2(snap: pd.DataFrame) -> pd.DataFrame: + hdr("2. IL FATTO — ritorno futuro condizionato all'OI (potenza DICHIARATA PRIMA)") + P = panel(snap, ["oi_delta_pct_4h", "funding_perp_annualized", + "funding_cross_annualized", "iv_minus_rv"]) + d = P.dropna(subset=["oi_delta_pct_4h", "f24", "p4"]).copy() + print(f"campione: {len(d)} barre orarie ({d.datetime.min()} -> {d.datetime.max()})") + + print("\n--- TEST DI POTENZA (dichiarato prima di leggere qualunque media) ---") + mde = {} + for H in HOURS: + n_ind = len(d) / H + sd = d[f"f{H}"].std() + mde[H] = 2 * sd / np.sqrt(n_ind / 10) + print(f" H={H:2d}h finestre NON sovrapposte={n_ind:6.0f} sd={100*sd:.2f}% " + f"n/decile={n_ind/10:5.0f} MDE(2 SE)={100*mde[H]:.3f}%") + print(" REGOLA PRE-REGISTRATA: un effetto di decile sotto la propria MDE NON e' un fatto") + print(" stabilito, qualunque p-value dia un bootstrap su osservazioni sovrapposte.") + + for a in ASSETS: + m = d.asset == a + for H in HOURS: + d.loc[m, f"x{H}"] = d.loc[m, f"f{H}"] - d.loc[m, f"f{H}"].mean() + d["mese"] = d.datetime.dt.strftime("%Y-%m") + for H in HOURS: + d[f"y{H}"] = d[f"f{H}"] - d.groupby(["asset", "mese"])[f"f{H}"].transform("mean") + d["odec"] = d.groupby("asset").oi_delta_pct_4h.transform( + lambda s: pd.qcut(s, 10, labels=False)) + + drift = {a: {H: round(100 * d.loc[d.asset == a, f"f{H}"].mean(), 3) for H in HOURS} + for a in ASSETS} + print(f"\ndrift INCONDIZIONATO della finestra (%): {drift}") + print(" -> la finestra e' ribassista: ogni media condizionata va letta DEMEANATA,") + print(" altrimenti 'stare short' sembra un segnale.") + + print("\n--- eccesso di ritorno futuro (%) per decile di oi_delta_pct_4h ---") + print(" lente 1 = demean per ASSET ; lente 2 = demean per (ASSET,MESE)") + g = d.groupby("odec").agg(n=("x4", "size"), oi=("oi_delta_pct_4h", "mean"), + x4=("x4", "mean"), x24=("x24", "mean"), + y4=("y4", "mean"), y24=("y24", "mean"), + av4=("f4", lambda s: s.abs().mean()), + av24=("f24", lambda s: s.abs().mean())) + for c in ("x4", "x24", "y4", "y24", "av4", "av24"): + g[c] *= 100 + g["p_x4"] = [bboot_p(d[d.odec == k].x4.values) for k in g.index] + g["p_x24"] = [bboot_p(d[d.odec == k].x24.values, block=48) for k in g.index] + print(g.round(3).to_string()) + print(" av4/av24 = |ret| futuro medio: e' la gamba 'squeeze/cascata' dell'ipotesi") + print(" (piu' vol dopo un estremo di OI). E' PIATTA: nessuna cascata prevedibile.") + + print("\n--- CONTROLLO: e' solo 'il prezzo si e' appena mosso'? ---") + z = lambda s: (s - s.mean()) / s.std() # noqa: E731 + for c, n in [("oi_delta_pct_4h", "zoi"), ("p4", "zp4"), + ("funding_perp_annualized", "zfund"), ("iv_minus_rv", "ziv")]: + d[n] = d.groupby("asset")[c].transform(z) + dr = d.dropna(subset=["zoi", "zp4", "zfund", "ziv"]).reset_index(drop=True) + print(f" corr(oi_delta, ritorno 4h passato) = {dr.oi_delta_pct_4h.corr(dr.p4):+.3f}" + f" (con 24h passato = {dr.oi_delta_pct_4h.corr(dr.f24.shift(24)):+.3f})") + for y in ("x4", "x24"): + for cols in (["zoi"], ["zp4"], ["zoi", "zp4"], ["zoi", "zp4", "zfund", "ziv"]): + b, se = ols_bboot(dr, y, cols) + txt = " ".join(f"{c}={100*b[i+1]:+.4f}%(t={b[i+1]/max(se[i+1],1e-12):+.2f})" + for i, c in enumerate(cols)) + print(f" {y:4s} ~ {str(cols):36s} {txt}") + print(" -> la PENDENZA LINEARE di oi_delta e' nulla (|t|<1.3 in ogni specificazione).") + print(" Quel poco che c'e' vive SOLO nella coda estrema, e non e' momentum di prezzo") + print(" (corr con il ritorno passato ~0.02): e' un'altra cosa, ma e' una CODA.") + + print("\n--- il decile 0 (OI che CROLLA) — l'unica cella non piatta ---") + for H in (4, 24): + obs = d[d.odec == 0][f"y{H}"].mean() + rng = np.random.default_rng(3) + nulls = [] + for _ in range(2000): + sh = [] + for a in ASSETS: + sub = d[d.asset == a] + k = rng.integers(1, len(sub) - 1) + sh.append(sub[f"y{H}"].values[np.roll((sub.odec == 0).values, k)]) + nulls.append(np.concatenate(sh).mean()) + nulls = np.array(nulls) + print(f" H={H:2d}h osservato={100*obs:+.3f}% MDE={100*mde[H]:.3f}% " + f"null circolare: mediana={100*np.median(nulls):+.3f}% " + f"p5={100*np.percentile(nulls,5):+.3f}% p={float((nulls<=obs).mean()):.3f}") + for a in ASSETS: + sub = d[(d.asset == a) & (d.odec == 0)] + cond = (d[d.asset == a].odec == 0).values + ep = int(np.sum(cond[1:] & ~cond[:-1])) + int(cond[0]) + print(f" {a}: ore in decile0={len(sub)} episodi distinti={ep} " + f"y4={100*sub.y4.mean():+.3f}% y24={100*sub.y24.mean():+.3f}%") + print("\n scomposizione PER MESE (demean mensile) — il test che il progetto impone") + print(" dopo il caso SOL ('un contributo positivo si scompone prima di crederci'):") + tab = d[d.odec == 0].groupby("mese")[["y4", "y24"]].agg(["size", "mean"]) + for m_, r in tab.iterrows(): + print(f" {m_}: n={int(r[('y4','size')]):4d} " + f"y4={100*r[('y4','mean')]:+.3f}% y24={100*r[('y24','mean')]:+.3f}%") + + print("\n--- 2x2 AFFOLLAMENTO (l'ipotesi originale, col funding al posto della liquidazione) ---") + for c in ("oi_delta_pct_4h", "funding_perp_annualized"): + d[c + "_hi"] = d.groupby("asset")[c].transform(lambda s: s >= s.quantile(.8)) + d[c + "_lo"] = d.groupby("asset")[c].transform(lambda s: s <= s.quantile(.2)) + combos = [("OI su & funding ALTO (long affollati -> cascata?)", + d["oi_delta_pct_4h_hi"] & d["funding_perp_annualized_hi"]), + ("OI su & funding BASSO (short affollati -> squeeze?)", + d["oi_delta_pct_4h_hi"] & d["funding_perp_annualized_lo"]), + ("OI giu & funding ALTO ", + d["oi_delta_pct_4h_lo"] & d["funding_perp_annualized_hi"]), + ("OI giu & funding BASSO", + d["oi_delta_pct_4h_lo"] & d["funding_perp_annualized_lo"])] + base = 100 * d.x4.abs().mean() + for nm, mask in combos: + sub = d[mask] + if len(sub) < 20: + print(f" {nm:52s} n={len(sub)} TROPPO PICCOLO") + continue + print(f" {nm:52s} n={len(sub):5d} x4={100*sub.x4.mean():+.3f}%(p={bboot_p(sub.x4.values):.3f})" + f" x24={100*sub.x24.mean():+.3f}%(p={bboot_p(sub.x24.values,block=48):.3f})" + f" |x4|={100*sub.x4.abs().mean():.3f}%") + print(f" |x4| di riferimento su tutto il campione = {base:.3f}%") + print(" -> 4 celle, 8 test: la sola sotto 0.05 (OI su & funding basso, x24) e' UNA cella su") + print(" 8 con ~8 finestre 24h indipendenti. Non e' un fatto.") + return d + + +# --------------------------------------------------------------------------- +# 3. FUNDING HL — l'unica gamba del perimetro con storia lunga +# --------------------------------------------------------------------------- +def sezione3() -> None: + hdr("3. FUNDING HYPERLIQUID BTC/ETH — 3 ANNI: l'unica gamba del perimetro con POTENZA") + res = [] + for a in ASSETS: + f = RAW / f"hlfund_{a.lower()}_1h.parquet" + if not f.exists(): + print(f" {f} assente") + return + h = pd.read_parquet(f) + h.index = pd.DatetimeIndex(h.index).tz_convert("UTC").floor("h") + h = h[~h.index.duplicated()] + df = A.get(a, "1h") + df = df.set_index(pd.DatetimeIndex(df.datetime)) + j = df[["close"]].join(h[["funding", "premium"]], how="inner") + for H in (4, 24): + j[f"f{H}"] = j.close.shift(-H) / j.close - 1.0 + j["asset"] = a + res.append(j) + J = pd.concat(res).dropna(subset=["funding", "f24"]) + print(f"n={len(J)} barre orarie, {J.index.min().date()} -> {J.index.max().date()}") + print(" ATTENZIONE: il feed funding HL si ferma al 2026-06-22 (non e' aggiornato a oggi).") + base = J.funding.value_counts(normalize=True).iloc[0] + print(f" il funding e' ANCORATO al tasso base nel {100*base:.1f}% delle ore " + f"(valore {J.funding.mode().iloc[0]:.6f}/h) -> anche qui meta' campione e' muto.") + J["ye"] = J.index.year + J["fdec"] = J.groupby("asset").funding.transform( + lambda s: pd.qcut(s.rank(method="first"), 10, labels=False)) + for H in (4, 24): + J[f"y{H}"] = J[f"f{H}"] - J.groupby(["asset", "ye"])[f"f{H}"].transform("mean") + g = J.groupby("fdec").agg(n=("y4", "size"), fund=("funding", "mean"), + y4=("y4", "mean"), y24=("y24", "mean"), + av4=("f4", lambda s: s.abs().mean()), + av24=("f24", lambda s: s.abs().mean())) + g["fund"] *= 100 * 24 * 365 + for c in ("y4", "y24", "av4", "av24"): + g[c] *= 100 + print("\n--- eccesso su drift ANNUALE (%) e |ret| futuro per decile di funding HL ---") + print(g.round(3).to_string()) + print("\nestremi per ANNO (y24, %):") + t = J[J.fdec.isin([0, 9])].groupby(["fdec", "ye"]).y24.agg(["size", "mean"]) + for k, r in t.iterrows(): + print(f" dec={k[0]} anno={k[1]} n={int(r['size']):5d} y24={100*r['mean']:+.3f}%") + print("\n>>> ESITO SEZIONE 3: su 53.430 osservazioni e 3 anni — cioe' DOVE LA POTENZA C'E' —") + print(" il funding come proxy di affollamento non predice ne' la DIREZIONE (|eccesso|") + print(" <=0.35% a 24h, segno instabile fra anni) ne' la VOLATILITA' (|ret| futuro piatto") + print(" e non monotono). Il filone funding, gia' chiuso su 3 lati come livello di carry,") + print(" e' chiuso anche come proxy di affollamento.") + + +# --------------------------------------------------------------------------- +# 4. IL SEGNALE CAUSALE + I NULL + I GATE +# --------------------------------------------------------------------------- +def pct_expanding(x: np.ndarray, minobs: int = 200) -> np.ndarray: + """Percentile ESPANDENTE causale: al bar i usa solo i valori <= i-1.""" + out = np.full(len(x), np.nan) + seen: list[float] = [] + for i, v in enumerate(x): + if np.isfinite(v): + if len(seen) >= minobs: + out[i] = bisect.bisect_left(seen, v) / len(seen) + bisect.insort(seen, v) + return out + + +class Signal: + """SHORT (o LONG) quando il percentile causale di oi_delta_pct_4h e' sotto soglia, + tenuto H barre, vol-targeted 20%.""" + + def __init__(self, snap: pd.DataFrame): + self.SN = {a: snap_num(snap, a, ["oi_delta_pct_4h"]).dropna() for a in ASSETS} + + def oi(self, df, asset): + return merge_causal(df, self.SN[asset])["oi_delta_pct_4h"].values.astype(float) + + def make(self, thr: float, H: int, sign: int): + def fn(df, asset): + p = pct_expanding(self.oi(df, asset)) + trig = np.where(np.isfinite(p), (p <= thr).astype(float), 0.0) + held = pd.Series(trig).rolling(H, min_periods=1).max().values + return A.vol_target(sign * held, df, target_vol=0.20, leverage_cap=2.0) + return fn + + +def book_net(target_by_asset) -> pd.Series: + nets = [] + for a in ASSETS: + df = A.get(a, "1h") + m = (df.datetime >= W0).values + d2 = df[m].reset_index(drop=True) + ev = A.eval_weights(d2, np.asarray(target_by_asset(df, a))[m]) + nets.append(pd.Series(ev["net"], index=ev["idx"])) + J = pd.concat(nets, axis=1, join="inner").fillna(0.0) + return 0.5 * J.iloc[:, 0] + 0.5 * J.iloc[:, 1] + + +def stats(net: pd.Series) -> dict: + if net.std() == 0: + return dict(sharpe=0.0, dd=0.0, ret=0.0, cagr=0.0) + eq = (1 + net).cumprod() + dd = float(((eq.cummax() - eq) / eq.cummax()).max()) + yrs = (net.index[-1] - net.index[0]).total_seconds() / (365.25 * 86400) + return dict(sharpe=float(net.mean() / net.std() * np.sqrt(24 * 365.25)), + dd=dd, ret=float(eq.iloc[-1] - 1), + cagr=float(eq.iloc[-1] ** (1 / max(yrs, 1e-9)) - 1)) + + +def sezione4(snap: pd.DataFrame) -> None: + hdr("4. IL SEGNALE CAUSALE — griglia 18 celle, i null, i gate") + S = Signal(snap) + rows = [] + for thr in (0.10, 0.20, 0.30): + for H in (4, 12, 24): + for sign in (-1, +1): + st = stats(book_net(S.make(thr, H, sign))) + rows.append(dict(thr=thr, H=H, sign=sign, **st)) + print(f" thr={thr:.2f} H={H:2d} sign={sign:+d} " + f"Sharpe(90g)={st['sharpe']:+.2f} maxDD={100*st['dd']:5.2f}% " + f"ret={100*st['ret']:+6.2f}%") + R = pd.DataFrame(rows) + best = R.loc[R.sharpe.idxmax()] + print(f"\n miglior cella: thr={best.thr} H={int(best.H)} sign={int(best.sign):+d} " + f"-> Sharpe {best.sharpe:.2f}") + print(f" SE(Sharpe) su 90 giorni = sqrt(365/90) = {np.sqrt(365/90):.2f} -> ogni Sharpe") + print(" sotto ~4 e' indistinguibile da zero su questa finestra. Non esiste hold-out:") + print(" la cella e' scelta sull'UNICO campione (select_cell_insample non applicabile).") + + thr, H, sign = float(best.thr), int(best.H), int(best.sign) + tgt = S.make(thr, H, sign) + net = book_net(tgt) + st = stats(net) + + print("\n--- NULL 1: 'sei solo short in una finestra ribassista?' ---") + always = lambda df, a: A.vol_target( # noqa: E731 + sign * np.ones(len(df)), df, target_vol=0.20, leverage_cap=2.0) + st_a = stats(book_net(always)) + tim = np.mean([np.mean(np.asarray(tgt(A.get(a, "1h"), a))[ + (A.get(a, "1h").datetime >= W0).values] != 0) for a in ASSETS]) + print(f" candidato Sharpe={st['sharpe']:+.2f} (tempo a mercato {tim:.3f}) " + f"sempre-esposto Sharpe={st_a['sharpe']:+.2f}") + print(" -> il candidato NON e' la statica travestita: il timing porta qualcosa.") + + print("\n--- NULL 2: timing CASUALE a pari esposizione (shift circolare, 300 estrazioni) ---") + base_trig = {} + for a in ASSETS: + df = A.get(a, "1h") + base_trig[a] = (np.asarray(tgt(df, a)), (df.datetime >= W0).values) + rng = np.random.default_rng(21) + nulls = [] + for _ in range(300): + shifted = {} + for a in ASSETS: + v, m = base_trig[a] + v2 = v.copy() + idx = np.where(m)[0] + v2[idx] = np.roll(v2[idx], rng.integers(1, len(idx) - 1)) + shifted[a] = v2 + nulls.append(stats(book_net(lambda df, a: shifted[a]))["sharpe"]) + nulls = np.array(nulls) + pctl = float((nulls < st["sharpe"]).mean()) + print(f" null: mediana={np.median(nulls):+.2f} p90={np.percentile(nulls,90):+.2f} " + f"p99={np.percentile(nulls,99):+.2f} -> candidato al {100*pctl:.1f}o pctl") + print(f" ma la cella e' il MASSIMO di 18: P(almeno una cella oltre quel pctl per caso) " + f"= 1-{pctl:.3f}^18 = {1-pctl**18:.2f} -> il null non e' superato dopo il conto dei trial.") + + print("\n--- GATE ---") + print(f" causality_ok : {A.causality_ok(tgt, tf='1h')}") + cd = A.candidate_daily(tgt, tf="1h") + cd = cd[cd.index >= W0] + sr = float(cd.mean() / cd.std() * np.sqrt(365.25)) + rep = A.marginal_vs_tp01(cd) + print(f" candidate_daily : n={len(cd)} giorni, Sharpe={sr:.2f}") + print(f" marginal_vs_tp01 : verdetto={rep.get('marginal_verdict')} " + f"corr_beta_tp01={rep.get('beta_to_tp01')} has_insample_edge={rep.get('has_insample_edge')} " + f"is_hedge={rep.get('is_hedge')} robust_oos={rep.get('robust_oos')} " + f"beats_noise_null={rep.get('beats_noise_null')} null_pctl_full={rep.get('null_pctl_full')}") + print(" (robust_oos=False non e' un difetto del candidato: con 90 giorni NON ESISTE il") + print(" multi-cut che il gate richiede. Il gate e' girato e il suo esito e' NEUTRAL.)") + all_sr = list(R.sharpe.values) + d18 = A.deflated_sharpe(sr, all_sr, cd) + d_all = A.deflated_sharpe(sr, all_sr * 7, cd) + print(f" deflated_sharpe(18) : DSR={d18[0]:.3f} (Sharpe atteso del massimo sotto il null " + f"= {d18[1]:.2f}) -> {'PASS' if d18[0]>=0.95 else 'FAIL'}") + print(f" deflated_sharpe({GRID_DECLARED}) : DSR={d_all[0]:.3f} (null max {d_all[1]:.2f}) " + f"-> {'PASS' if d_all[0]>=0.95 else 'FAIL'}") + print(" con 114 giorni il MASSIMO atteso PER CASO su 18 celle e' Sharpe ~5.4:") + print(" il candidato (3.6) sta SOTTO il proprio null. Non serve altro.") + imp = A.implausible_sharpe(cd) + print(f" implausible_sharpe : {imp['implausible']} {imp['reasons']}") + print(" null de-levering : NON GIRATO — il candidato non fa alcun claim di riduzione") + print(" di drawdown (e' uno stream di ritorno, non un overlay).") + print(" anchor_luck_band : NON GIRATO — il segnale non e' ancorato a un'ora di") + print(" ribilanciamento (gira su ogni barra oraria).") + for a in ASSETS: + df = A.get(a, "1h") + m = (df.datetime >= W0).values + sc = A.eval_weights_smallcap(df[m].reset_index(drop=True), + np.asarray(tgt(df, a))[m], capital=600) + print(f" smallcap $600 {a} : haircut Sharpe={sc['sharpe_haircut']:+.3f} " + f"(reale {sc['realistic']['sharpe']:.2f} vs modellato {sc['modeled']['sharpe']:.2f}, " + f"{sc['n_executed_trades']} trade eseguiti)") + print(" -> l'ESEGUIBILITA' non e' il vincolo (2 gambe BTC/ETH perp, haircut ~0.02):") + print(" come per SOL, il candidato muore sull'evidenza, non sulla taglia del conto.") + + print("\n--- CONCENTRAZIONE (dove sta davvero il P&L) ---") + mm = cd.groupby(cd.index.strftime("%Y-%m")).agg(n="size", ret=lambda s: 100 * ((1 + s).prod() - 1)) + print(mm.round(2).to_string()) + tot = 100 * ((1 + cd).prod() - 1) + top3 = 100 * cd.sort_values().tail(3).sum() + print(f" somma dei 3 giorni migliori = {top3:.2f}% su un totale di {tot:.2f}% " + f"({100*top3/max(tot,1e-9):.0f}% del P&L in 3 giorni su 114)") + + +# --------------------------------------------------------------------------- +# 5. RICOSTRUIBILITA' +# --------------------------------------------------------------------------- +def sezione5(snap: pd.DataFrame) -> None: + hdr("5. RICOSTRUIBILITA' — il dato e' MORTO il 2026-07-30: sapremmo rifarlo oggi?") + cols = ["asset", "instrument_name", "open_interest", "ts"] + parts = [] + arch = RAW / "cb_chain" / "bite_archive.parquet" + if arch.exists(): + parts.append(pd.read_parquet(arch, columns=cols)) + for f in sorted(glob.glob(str(RAW / "cb_chain" / "2026-*.parquet"))): + parts.append(pd.read_parquet(f, columns=cols)) + C = pd.concat(parts, ignore_index=True) + del parts + C["ts"] = pd.to_datetime(C.ts, utc=True).dt.floor("h") + C = C.drop_duplicates(["ts", "instrument_name"]) + print(f"catena: {len(C)} righe {C.ts.min()} -> {C.ts.max()}") + sn = snap.copy() + sn["h"] = pd.to_datetime(sn.ts, utc=True).dt.floor("h") + for asset in ASSETS: + s = C[C.asset == asset] + piv = s.pivot_table(index="ts", columns="instrument_name", + values="open_interest", aggfunc="last") + piv = piv.reindex(pd.date_range(piv.index.min(), piv.index.max(), freq="h")) + V = piv.values + dl = np.full(len(piv), np.nan) + for i in range(4, len(piv)): + m = np.isfinite(V[i]) & np.isfinite(V[i - 4]) + if m.sum() < 30: + continue + b = V[i - 4][m].sum() + if b > 0: + dl[i] = 100 * (V[i][m].sum() / b - 1) + chain = pd.Series(dl, index=piv.index, name="chain") + ref = pd.to_numeric(sn[sn.asset == asset].groupby("h").oi_delta_pct_4h.last(), + errors="coerce").rename("snap") + j = pd.concat([ref, chain], axis=1).dropna() + print(f" {asset}: ore sovrapposte={len(j)} " + f"corr Pearson={j.snap.corr(j.chain):+.3f} " + f"Spearman={j.snap.corr(j.chain, method='spearman'):+.3f} " + f"(sd snap={j.snap.std():.3f} vs sd catena={j.chain.std():.3f})") + del piv, V + print("\n Nota: l'OI della catena e' l'OI delle OPZIONI a strumenti APPAIATI (stessi") + print(" strumenti a t e t-4h, cosi' le scadenze non creano salti). La correlazione con") + print(" oi_delta_pct_4h e' ~0 in rango: NON e' la stessa grandezza (lo snapshot misurava") + print(" l'OI dei PERPETUAL/futures via il progetto dismesso).") + print("\n>>> ESITO SEZIONE 5 — colonna per colonna, cosa sappiamo produrre OGGI:") + print(" oi_delta_pct_4h : NO. La catena da' OI di OPZIONI (rango ~0.05 con la") + print(" colonna). L'OI perp Deribit e' leggibile dall'API") + print(" pubblica ma NON lo raccogliamo: la storia parte da") + print(" zero il giorno che si accende un collettore.") + print(" liquidation_long/short : NO, e non importa: la colonna e' costante 'low'.") + print(" Nessuna fonte nostra la produce, e non c'e' niente") + print(" da produrre.") + print(" funding_perp/cross : SI ma degradato — funding HL c'e' (fermo al 22/06/2026),") + print(" funding Deribit non lo salviamo.") + print(" iv_minus_rv : SI, ricostruibile da dvol_*.parquet + barre certificate.") + + +# --------------------------------------------------------------------------- +def main() -> None: + print(__doc__) + print(f"GRIGLIA DICHIARATA (conteggio al rialzo) = {GRID_DECLARED} celle/trial") + snap = load_snapshots() + sezione1(snap) + sezione2(snap) + sezione3() + sezione4(snap) + sezione5(snap) + hdr("6. VERDETTO") + print("""SCARTATO. + 1. META' DELL'IPOTESI NON ESISTE NEL DATO: liquidation_long_risk e liquidation_short_risk + valgono 'low' nel 100% delle 17.229 righe non-nulle. Una variabile con UNA categoria non + ha potenza: l'accoppiamento affollamento x rischio-di-liquidazione e' untestabile, non + debole. (Ed e' esattamente la parte che rendeva l'angolo NUOVO rispetto al filone funding.) + 2. IL FATTO NON REGGE LA PROPRIA SOGLIA DI POTENZA, dichiarata prima di guardare: l'unico + effetto e' il decile piu' basso di oi_delta (-0.40% a 24h) e la MDE a 24h e' 1.06%. + La pendenza lineare e' nulla (|t|<1.3), il |ritorno| futuro e' piatto fra i decili (la + 'cascata' non c'e'), e la scomposizione per mese mette quasi tutto in giugno. + 3. IL SEGNALE E' UN MASSIMO DI 18 CELLE SU 114 GIORNI: Sharpe 3.6 con SE(Sharpe)=2.0, + deflated-Sharpe 0.10 (18 celle) / 0.004 (118 trial) contro un massimo atteso PER CASO di + Sharpe 5.4; implausible_sharpe=True; marginal_vs_tp01 = NEUTRAL; 60% del P&L in 3 giorni. + 4. ED E' COMUNQUE MORTO ALLA NASCITA: la colonna su cui poggia non e' ricostruibile con i dati + che raccogliamo (l'OI della catena e' quello delle OPZIONI, rango ~0.05 con la colonna) e + la sorgente e' stata dismessa il 2026-07-30. + Sottoprodotto con potenza vera (53.430 ore, 3 anni): il funding HL non predice ne' direzione + ne' volatilita' -> il filone funding, gia' chiuso su 3 lati come livello di carry, si puo' + dichiarare chiuso anche come PROXY DI AFFOLLAMENTO.""") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_growth_policy.py b/scripts/research/r0822_growth_policy.py new file mode 100644 index 0000000..edb3e91 --- /dev/null +++ b/scripts/research/r0822_growth_policy.py @@ -0,0 +1,564 @@ +#!/usr/bin/env python +"""r0822_growth_policy.py — LA POLITICA DI LEVA, giudicata sul TEMPO invece che sullo Sharpe. + +LA DOMANDA. `target_vol=20%` per TP01 e' canonico dal 2026-06-19 e non e' mai stato messo in +discussione **rispetto a un obiettivo di crescita**. E' stato scelto (e ri-confermato: peso 75/25 +tre volte, TP01xDVOL, tranching) sempre con una lente di **Sharpe** o di **drawdown**. Ma +l'operatore non massimizza lo Sharpe: vuole ~$258k netti il prima possibile. Sono due problemi +diversi — lo Sharpe e' invariante alla leva, il TEMPO no. + +Quindi: **quale leva k minimizza il tempo mediano al bersaglio, a parita' di rischio accettato?** +E cambia se il bersaglio e' $50k invece di $258k? + +COSA C'E' QUI + (0) Replica di controllo: il book live, il fattore d'ancora ×0.89 misurato, e i muri pubblicati. + (1) LA CURVA DI CRESCITA g(k) — dove sta il massimo (Kelly empirico) e da che parte sta il libro. + (2) SWEEP PRINCIPALE — per ogni k: tempo mediano al bersaglio (CONDIZIONATO, stampato accanto + alla sua probabilita'), P(entro 10/15/20a), P(rovina), P(DD>30%). Versamenti €0 e €500/mese, + bersagli $50k e il muro NETTO. Tutto **al netto del fisco d'accumulo** (33% + carry 4a + 0.2%). + (3) FRONTIERA ISO-ROVINA — la domanda del brief nella sua forma esatta. + (4) IL GATE CHE CONTA — il massimo di (1) e' calcolato su un campione che non contiene un solo + giorno peggiore di -3.9%. Stress esplicito: giorno di crash fuori campione + liquidazione + Deribit. Dove va il massimo quando il rischio che il dataset NON contiene entra nel conto. + (5) ESEGUIBILITA' — `config/live.json` oggi tiene il nozionale lordo <= 1.00x l'equity, e il + knob `target_vol` di TP01 NON scala linearmente (leverage cap 2x). Quanto di k e' teorico. + +ONESTA' (regole del progetto applicate qui): + · un non-arrivo e' **+infinito**, mai -1 (l'errore del 07/08: con -1 la mediana MIGLIORA quanto + peggio va la colonna); + · una mediana condizionata si stampa **sempre** accanto alla sua probabilita'; + · un Monte Carlo ha una **risoluzione**: la riga di testa e' rifatta con un secondo seme e la + banda e' dichiarata; + · la rovina non e' l'azzeramento aritmetico (con ritorni moltiplicativi il capitale non tocca + zero finche' k < 1/|peggior giorno| = 25.4): e' definita e dichiarata sotto. + + nice -n 19 timeout 900 uv run python scripts/research/r0822_growth_policy.py +""" +from __future__ import annotations + +import sys +from pathlib import Path + +import numpy as np + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +import r0725_capcurve as CC # noqa: E402 macchineria validata (book_series, _boot_paths, survival) +import r0726_deposits as DP # noqa: E402 deluck_returns() con il ×0.89 MISURATO +import r0727_tasse as TX # noqa: E402 CARRY_ANNI, PATRIMONIALE +from r0807_piano_netto import passo_fiscale # noqa: E402 il passo fiscale gia' validato + +# ------------------------------------------------------------------ parametri dichiarati +KS = (0.50, 0.75, 1.00, 1.25, 1.50, 2.00, 2.50, 3.50, 5.00, 7.00, + 10.00, 14.00, 18.00, 22.00) # 14 celle: la griglia stretta (<=8x) NON conteneva il + # massimo -> allargata finche' il massimo e' INTERNO +KS_STRESS = (1.00, 1.50, 2.00, 3.50, 5.00, 10.00, 18.00) # sottoinsieme per la (4) +DEPOSITI = (0, 500) # €/mese: nessuno, e il piano dichiarato +BERSAGLI = {"$50k": 50_000.0, "muro netto": 258_338.0} # r0807_piano_netto (fisco coerente) +ANNI = 25 # orizzonte di simulazione: 25 come r0726/r0807 (20 troncherebbe + # la coda della mediana condizionata). Le P si leggono a 10/15/20a. +N_PATHS = 2_500 +BLOCK = 20 +CHUNK = 500 # RAM: la VPS ha ~3.5 GB liberi e ci girano altri agenti +SEED = 20260822 +SEED_BIS = 8221 # secondo seme, solo per dichiarare la risoluzione MC +START = 600.0 +ALIQUOTA = CC.TAX_RATE # 0.33 +PATRIM = TX.PATRIMONIALE # 0.002 +RUIN_MULT = 0.20 # rovina = il moltiplicatore del PATH perde l'80% +DD_SOGLIA = 0.30 +WIPE = 0.90 # perdita giornaliera >= 90% dell'equity = liquidazione (margine+fee) +MURO_PUBBL_LORDO = 272_061.0 # r0726_capwall_refresh (convenzione pubblicata) + + +def sezione(t: str) -> None: + print("\n" + "=" * 104) + print(f" {t}") + print("=" * 104) + + +# =========================================================================== +# il motore: una passata, tutte le leve insieme (confronto APPAIATO sugli stessi path) +# =========================================================================== +def simula(r: np.ndarray, ks: tuple[float, ...], *, anni: int, dep_eur: float, + bersagli: dict[str, float], aliquota: float, patrimoniale: float, + n_paths: int = N_PATHS, seed: int = SEED, start: float = START, + crash_pct: float = 0.0, crash_p_anno: float = 0.0) -> dict: + """Accumulo a leva `k` per ogni k, sugli STESSI path (differenze appaiate). + + Stato per (path, k). Traccia in parallelo un GEMELLO SENZA VERSAMENTI: il drawdown e la + rovina si misurano su quello, cosi' sono confrontabili fra colonne con versamenti diversi + (un bonifico mensile maschera un drawdown senza ridurre il capitale distrutto). + + `crash_pct`/`crash_p_anno`: giorno di coda FUORI CAMPIONE (sezione 4). Il block bootstrap non + puo' generare un giorno peggiore del peggiore osservato: se lo si vuole, va messo a mano. + + Liquidazione: se la perdita del giorno a leva k supera `WIPE` dell'equity, il conto e' chiuso + dall'exchange -> capitale a 0 (i versamenti successivi ripartono da li'). + """ + kv = np.asarray(ks, float)[None, :] + nk = len(ks) + dep = dep_eur * CC.EURUSD + n_days = int(anni * 365) + rng = np.random.default_rng(seed) + nomi = list(bersagli) + soglie = np.array([bersagli[n] for n in nomi], float) + + hit = {n: [] for n in nomi} + fin, mult_fin, ruin, ddmax, versato_tot = [], [], [], [], 0.0 + + for c0 in range(0, n_paths, CHUNK): + m = min(CHUNK, n_paths - c0) + paths = CC._boot_paths(r, m, n_days, BLOCK, rng) + if crash_pct > 0 and crash_p_anno > 0: + # un giorno di crash con probabilita' `crash_p_anno`/anno, uniforme nel tempo + p_giorno = crash_p_anno / 365.0 + mask = rng.random((m, n_days)) < p_giorno + paths = np.where(mask, -abs(crash_pct), paths) + + cap = np.full((m, nk), start, float) + mult = np.ones((m, nk), float) # gemello senza versamenti + hwm = np.ones((m, nk), float) + dd = np.zeros((m, nk), float) + rovinato = np.zeros((m, nk), bool) + colpito = {n: np.full((m, nk), np.inf) for n in nomi} + carry = np.zeros((m * nk, TX.CARRY_ANNI)) + anno_start = cap.copy() + flusso = np.zeros((m, nk)) + versato = start + + for t in range(n_days): + rt = paths[:, t][:, None] * kv # (m, nk) + liq = rt <= -WIPE + g = np.where(liq, -1.0, rt) + cap *= (1.0 + g) + cap = np.maximum(cap, 0.0) + mult *= (1.0 + g) + mult = np.maximum(mult, 0.0) + hwm = np.maximum(hwm, mult) + dd = np.maximum(dd, 1.0 - mult / hwm) + rovinato |= mult <= RUIN_MULT + if dep > 0 and t % 30 == 0 and t > 0: + cap += dep + flusso += dep + versato += dep + for j, n in enumerate(nomi): + nuovi = np.isinf(colpito[n]) & (cap >= soglie[j]) + colpito[n][nuovi] = t + if (t + 1) % 365 == 0 and (aliquota > 0 or patrimoniale > 0): + passo_fiscale(cap.ravel(), anno_start.ravel(), flusso.ravel(), + carry, aliquota, patrimoniale) + anno_start = cap.copy() + flusso[:] = 0.0 + + for n in nomi: + hit[n].append(colpito[n]) + fin.append(cap) + mult_fin.append(mult) + ruin.append(rovinato) + ddmax.append(dd) + versato_tot = versato + + out = dict(versato=versato_tot, anni=anni, n_paths=n_paths, ks=ks) + out["cap"] = np.concatenate(fin, axis=0) + out["mult"] = np.concatenate(mult_fin, axis=0) + out["ruin"] = np.concatenate(ruin, axis=0) + out["dd"] = np.concatenate(ddmax, axis=0) + out["hit"] = {n: np.concatenate(hit[n], axis=0) for n in nomi} + return out + + +def anni_cond(hit_col: np.ndarray) -> tuple[float, float, float, float]: + """(anni mediani CONDIZIONATI all'arrivo, P<=10a, P<=15a, P<=20a). Non-arrivo = +inf.""" + arr = hit_col[np.isfinite(hit_col)] + med = float(np.median(arr)) / 365.0 if len(arr) >= 50 else float("nan") + p = lambda y: float((hit_col <= y * 365).mean()) # noqa: E731 + return med, p(10), p(15), p(20) + + +def fmt_anni(a: float) -> str: + return f"{a:.1f}a" if np.isfinite(a) else "mai" + + +# =========================================================================== +def main() -> None: + print("=" * 104) + print(" r0822 — GROWTH POLICY: quale leva minimizza il TEMPO al bersaglio, a pari rovina accettata") + print("=" * 104) + + # ------------------------------------------------------------------ (0) + sezione("(0) IL DATO — book LIVE (TP01 75 / SKH01 25) e replica di controllo") + base = CC.book_series("hourly") + r = DP.deluck_returns() + print(f"\n serie: {len(base)} giorni, {base.index.min().date()} -> {base.index.max().date()}") + print(f" lente 'hourly' = SKH01 sul PATH LIVE (fill al close 5m orario), non il backtest.") + print(f" canonico : drift {base.values.mean()*365:>7.2%} vol {base.values.std()*365**0.5:>6.2%}" + f" Sharpe {base.values.mean()/base.values.std()*365**0.5:>5.2f}") + print(f" de-luckato ×{DP.DELUCK}: drift {r.mean()*365:>7.2%} vol {r.std()*365**0.5:>6.2%}" + f" Sharpe {r.mean()/r.std()*365**0.5:>5.2f} <-- questa e' la serie usata ovunque sotto") + print(f" peggior giorno {r.min():+.2%} · miglior giorno {r.max():+.2%}") + print(f"\n ⚠️ Conseguenza aritmetica da tenere a mente per tutto il resto: con ritorni") + print(f" moltiplicativi il capitale NON tocca zero finche' k < 1/{abs(r.min()):.4f} = " + f"{1/abs(r.min()):.1f}x.") + print(f" Percio' 'rovina' qui e' DEFINITA: il moltiplicatore del path (gemello SENZA") + print(f" versamenti) scende sotto {RUIN_MULT:.0%}, cioe' 4 euro su 5 distrutti. E la") + print(f" liquidazione dell'exchange e' modellata a parte: perdita giornaliera >= {WIPE:.0%}.") + print(f"\n muri pubblicati riusati come bersagli:") + print(f" · ${MURO_PUBBL_LORDO:,.0f} (r0726_capwall_refresh, convenzione LORDA)") + print(f" · ${BERSAGLI['muro netto']:,.0f} (r0807_piano_netto, fisco COERENTE) <-- usato qui") + print(f" · $50.000 (il bersaglio 'corto' chiesto dal brief: ~€19/g di rendita perpetua)") + + print("\n CONTROLLO DI REPLICA — questo motore contro la tabella pubblicata il 07/08") + print(" (`r0807_piano_netto` §2: stesso fisco, stesso bersaglio, stessa convenzione di") + print(" mediana condizionata). I path sono diversi: si chiede accordo entro il rumore MC,") + print(" non l'identita'. Se questa riga non torna, nessun numero nuovo sarebbe") + print(" distinguibile da un bug.") + print(f"\n {'EUR/mese':>9}{'anni (pubbl.)':>16}{'P<=20a (pubbl.)':>18}{'anni (qui)':>13}{'P<=20a (qui)':>15}") + for dep, a_pub, p_pub in ((250, 19.8, 0.52), (500, 14.7, 0.99)): + sr = simula(r, (1.00,), anni=25, dep_eur=dep, bersagli={"muro": BERSAGLI["muro netto"]}, + aliquota=ALIQUOTA, patrimoniale=PATRIM, n_paths=3_000, seed=725) + a, _, _, p20 = anni_cond(sr["hit"]["muro"][:, 0]) + print(f" {dep:>9}{a_pub:>15.1f}a{p_pub:>18.0%}{fmt_anni(a):>13}{p20:>15.0%}") + + # ------------------------------------------------------------------ (1) + sezione("(1) LA CURVA DI CRESCITA g(k) — dove sta il massimo, e DI QUANTO il libro ne dista") + print(f"\n Crescita geometrica MEDIANA annua, orizzonte {ANNI} anni, ZERO versamenti,") + print(f" {N_PATHS} path, fisco d'accumulo DENTRO. Un vol-target e' esattamente questo:") + print(" uno scalare sui pesi. Griglia allargata a 22x perche' a 8x il massimo non era") + print(" ancora interno — una griglia che si ferma prima del massimo non lo misura, lo tronca.") + s0 = simula(r, KS, anni=ANNI, dep_eur=0, bersagli=BERSAGLI, + aliquota=ALIQUOTA, patrimoniale=PATRIM, seed=SEED) + s0b = simula(r, KS, anni=ANNI, dep_eur=0, bersagli=BERSAGLI, + aliquota=ALIQUOTA, patrimoniale=PATRIM, seed=SEED_BIS) + mu, sig = float(r.mean()), float(r.std()) + kelly_gauss = mu / sig ** 2 + + def g_curve(sim: dict) -> list[float]: + return [float(np.median(sim["mult"][:, j])) ** (1 / ANNI) - 1 for j in range(sim["mult"].shape[1])] + + g_med, g_medb = g_curve(s0), g_curve(s0b) + jstar = int(np.argmax(g_med)) + kbest, gbest = KS[jstar], g_med[jstar] + j1 = KS.index(1.00) + + lg = lambda g: float(np.log1p(g)) # noqa: E731 + print(f"\n {'k':>6}{'g mediana/anno':>17}{'(2° seme)':>12}{'ln(1+g)/ln(1+g*)':>19}" + f"{'maxDD mediano':>16}{'P(DD>30%)':>11}{'P(rovina)':>11}{'cap mediano a 25a':>20}") + for j, k in enumerate(KS): + marca = " <-- LIBRO" if j == j1 else (" <-- k*" if j == jstar else "") + print(f" {k:>6.2f}{g_med[j]:>17.2%}{g_medb[j]:>12.2%}{lg(g_med[j])/lg(gbest):>19.2f}" + f"{np.median(s0['dd'][:, j]):>16.1%}{(s0['dd'][:, j] > DD_SOGLIA).mean():>11.1%}" + f"{s0['ruin'][:, j].mean():>11.1%}{np.median(s0['cap'][:, j]):>20,.0f}{marca}") + esec = [j for j, k in enumerate(KS) if k <= 2.50] + ris = max(abs(a - b) for a, b in zip(g_med, g_medb)) + ris_e = max(abs(g_med[j] - g_medb[j]) for j in esec) + print(f"\n RISOLUZIONE MONTE CARLO (due semi indipendenti a {N_PATHS} path): scarto massimo") + print(f" {ris:.2%}/anno sull'intera griglia, ma solo {ris_e:.2%}/anno nella regione") + print(f" k<={KS[esec[-1]]:.2f} — il rumore vive tutto nelle celle a leva alta, dove la mediana e'") + print(" presa su una distribuzione enormemente dispersa. Conseguenza da tenere: la") + print(" POSIZIONE del massimo non e' risolvibile a questa taglia (vedi sotto), i confronti") + print(" nella regione eseguibile si'. Ogni confronto e' APPAIATO (stessi path per ogni k).") + + print(f"\n RISPOSTA ALLA DOMANDA 'da che parte, e di quanto':") + print(f" massimo EMPIRICO della crescita k* = {kbest:.2f}x (g = {gbest:.1%}/anno)") + print(f" massimo GAUSSIANO (Kelly, mu/sigma²) k = {kelly_gauss:.1f}x") + print(f" il LIBRO gira a k = 1.00x (g = {g_med[j1]:.1%}/anno)") + print(f" -> il libro sta a SINISTRA, a {1.0/kbest:.0%} di Kelly empirico, e raccoglie il") + print(f" {lg(g_med[j1])/lg(gbest):.0%} della crescita massima (in crescita LOGARITMICA, che e' la") + print(" grandezza che Kelly massimizza). NON e' un plateau: e' il ramo ripido.") + print(f"\n ⚠️ La POSIZIONE del massimo non e' risolta a questa taglia: la sezione (4) rifa'") + print(" la stessa curva con 1500 path e il massimo cade a 10x invece di 14x. La regione") + print(" 8-14x e' piatta entro il rumore -> si cita 'k* ~ 10-14x', non un numero.") + print(" Cio' che E' risolto, e con margine enorme, e' che 1.00x non ci sta vicino.") + print(f"\n ⚠️ PREVISIONE MIA, REFUTATA IN SESSIONE. Mi aspettavo che il massimo empirico") + print(f" cadesse molto a SINISTRA di quello gaussiano ({kelly_gauss:.1f}x), perche' il bootstrap a") + print(" blocchi conserva code e clustering che la formula ignora. Non succede: i due") + print(" coincidono entro il rumore. Il motivo e' informativo e prepara la sezione (4) —") + print(" la distribuzione delle CHIUSURE giornaliere di questo libro non ha code abbastanza") + print(" spesse da spostare Kelly. Il rischio che conta a leva non e' in quella") + print(" distribuzione, quindi non lo si trovera' ricampionandola meglio.") + print(f"\n ⚠️ Questo e' il risultato della lente, non una raccomandazione. Tre cose lo") + print(" ridimensionano, e sono misurate sotto: (a) k* dipende dal DRIFT, che e' stimato") + print(f" su {len(r)/365:.1f} anni (sez. 1-bis); (b) la coda che lo sostiene NON e' nel") + print(" campione (sez. 4); (c) meta' della griglia non e' eseguibile (sez. 5).") + print(f" ⚠️ E c'e' un controllo di plausibilita' che il modello fallisce da solo: a k={KS[jstar]:g}x") + print(f" il capitale mediano a 25 anni partendo da $600 e' ${np.median(s0['cap'][:, jstar]):,.0f}.") + print(" Un modello che promette piu' della capitalizzazione del mercato in cui trada ha") + print(" lasciato il dominio in cui vuol dire qualcosa: assume che il ritorno sia") + print(" indipendente dalla SIZE, il che e' falso ben prima di quella cifra.") + + # ------------------------------------------------------------------ (1-bis) + sezione("(1-bis) QUANTO E' SOLIDO k* — Kelly e' ipersensibile al drift, e il drift e' una STIMA") + se_drift = float(r.std() * 365 ** 0.5) / (len(r) / 365) ** 0.5 + print(f"\n drift stimato {mu*365:.2%}/anno su {len(r)/365:.1f} anni con vol {r.std()*365**0.5:.2%}") + print(f" -> errore standard della MEDIA = vol/sqrt(T) = {se_drift:.2%}/anno.") + print(" Kelly va come mu/sigma²: e' LINEARE nell'errore del drift, quindi l'incertezza") + print(" sul numeratore si trasferisce intera su k*. Stessa macchineria di `deluck_returns`") + print(" (si sottrae una costante al drift, la vol resta quella misurata).") + print(f"\n {'ipotesi sul drift':>34}{'drift/anno':>13}{'k* gaussiano':>15}{'k* empirico':>14}" + f"{'g(1x)':>9}{'g(k*)':>9}") + scen = [("punto (de-luckato ×0.89)", 0.0), + ("-1 errore standard", se_drift), + ("-2 errori standard", 2 * se_drift), + ("drift dimezzato (lente 07/08)", mu * 365 / 2), + ("drift a zero", mu * 365)] + for nome, taglio in scen: + rs = r - taglio / 365.0 + mus = float(rs.mean()) + kg = mus / sig ** 2 + ss = simula(rs, KS, anni=ANNI, dep_eur=0, bersagli=BERSAGLI, aliquota=ALIQUOTA, + patrimoniale=PATRIM, seed=SEED, n_paths=1_500) + gs = g_curve(ss) + js = int(np.argmax(gs)) + print(f" {nome:>34}{mus*365:>13.2%}{kg:>15.1f}{KS[js]:>13.2f}x" + f"{gs[j1]:>9.1%}{gs[js]:>9.1%}") + print("\n Lettura: un errore di stima di UN solo errore standard sul drift — che a 7 anni di") + print(" storia e' l'ordine di grandezza normale — sposta k* di parecchie unita'. E' la") + print(" ragione classica per cui si opera a una FRAZIONE di Kelly: non prudenza morale, ma") + print(" il fatto che k* e' un numero stimato con una barra d'errore larga quanto se stesso.") + + # ------------------------------------------------------------------ (2) + sezione(f"(2) SWEEP PRINCIPALE — tempo al bersaglio, orizzonte {ANNI} anni, AL NETTO del fisco") + print(f"\n da ${START:.0f} · {N_PATHS} path · blocchi {BLOCK}g · fisco {ALIQUOTA:.0%} + carry " + f"{TX.CARRY_ANNI}a + patrimoniale {PATRIM:.1%}") + print(" 'anni' = mediana CONDIZIONATA all'arrivo (non-arrivo = +infinito, MAI -1): si legge") + print(" sempre insieme a P<=20a accanto. 'p90' = il decile lento, cioe' il rischio di TEMPO.") + print(" ⚠️ Convenzione dichiarata: il fisco e' dentro il CAPITALE (colonne anni/P), NON dentro") + print(" il gemello di rischio (colonne DD/rovina). Un drawdown e' un fatto di mercato: metterci") + print(" l'imposta dentro sommerebbe due cose diverse in un numero solo.") + risultati = {} + for dep in DEPOSITI: + s = s0 if dep == 0 else simula(r, KS, anni=ANNI, dep_eur=dep, bersagli=BERSAGLI, + aliquota=ALIQUOTA, patrimoniale=PATRIM, seed=SEED) + risultati[dep] = s + for nome in BERSAGLI: + print(f"\n --- versamento €{dep}/mese · bersaglio {nome} (${BERSAGLI[nome]:,.0f}) · " + f"versato in {ANNI}a ${s['versato']:,.0f} ---") + print(f" {'k':>6}{'anni (p50)':>13}{'anni (p90)':>13}{'P<=10a':>9}{'P<=15a':>9}" + f"{'P<=20a':>9}{'P(rovina)':>11}{'P(DD>30%)':>11}{'P(DD>50%)':>11}") + for j, k in enumerate(KS): + h = s["hit"][nome][:, j] + a, p10, p15, p20 = anni_cond(h) + arr = h[np.isfinite(h)] + a90 = float(np.percentile(arr, 90)) / 365.0 if len(arr) >= 50 else float("nan") + print(f" {k:>6.2f}{fmt_anni(a):>13}{fmt_anni(a90):>13}{p10:>9.1%}{p15:>9.1%}" + f"{p20:>9.1%}{s['ruin'][:, j].mean():>11.1%}" + f"{(s['dd'][:, j] > DD_SOGLIA).mean():>11.1%}" + f"{(s['dd'][:, j] > 0.50).mean():>11.1%}") + + # ------------------------------------------------------------------ (3) + sezione("(3) FRONTIERA ISO-ROVINA — la domanda del brief nella sua forma esatta") + print("\n 'a parita' di probabilita' di rovina accettata': per ogni budget di rischio, la") + print(" leva PIU' ALTA che lo rispetta, e il tempo che quel budget compra. Il rischio si") + print(" misura sul gemello SENZA versamenti (deposit-independent: un bonifico mensile") + print(" maschera un drawdown senza ridurre il capitale distrutto), ed e' la stessa") + print(" grandezza su cui passa la regola di DD per-conto del canale funded.") + for dep in DEPOSITI: + s = risultati[dep] + print(f"\n --- versamento €{dep}/mese ---") + print(f" {'budget':>8}{'metrica':>13}{'k max':>9}" + + "".join(f"{'anni ' + n:>22}" for n in BERSAGLI)) + for metrica, col in (("P(DD>30%)", lambda j: (s["dd"][:, j] > DD_SOGLIA).mean()), + ("P(rovina)", lambda j: s["ruin"][:, j].mean())): + for budget in (0.01, 0.05, 0.10, 0.25, 0.50): + amm = [j for j in range(len(KS)) if col(j) <= budget] + if not amm: + print(f" {budget:>7.0%}{metrica:>13}{'nessuna':>9}") + continue + j = max(amm) + celle = [] + for n in BERSAGLI: + a, _, _, p20 = anni_cond(s["hit"][n][:, j]) + celle.append(f"{fmt_anni(a)} (P20 {p20:.0%})") + print(f" {budget:>7.0%}{metrica:>13}{KS[j]:>8.2f}x" + "".join(f"{c:>22}" for c in celle)) + print("\n ⚠️ Le due metriche danno risposte MOLTO diverse, e la ragione conta: P(rovina)") + print(" (perdere l'80% del moltiplicatore) e' quasi zero fino a leve altissime perche'") + print(" il libro non ha mai perso l'80% in nessuna finestra ricampionabile; P(DD>30%)") + print(" morde subito. Chi sceglie la leva sul solo azzeramento sceglie sulla metrica") + print(" che il dataset non puo' informare — vedi (4).") + + # ------------------------------------------------------------------ (4) + sezione("(4) IL GATE CHE CONTA — k* e' calcolato su un campione senza un solo giorno di crash") + print(f"\n Il block bootstrap ricampiona i giorni VISSUTI: non puo' produrre un giorno") + print(f" peggiore di {r.min():.2%}. Il libro ha attraversato il 2022 (LUNA/FTX) restando") + print(" difensivo — quindi per k=1 la coda del campione e' onesta. Ma a leva il rischio") + print(" cambia NATURA: entra la liquidazione, che su una chiusura giornaliera non si vede") + print(" (lezione della lente wick accoppiata, 25/07: 'close-only non e' conservativa, e'") + print(" CIECA'). Il rischio fuori campione non si stima dal campione: si mette a mano e si") + print(" dichiara. Ampiezze scelte a priori sull'unico precedente misurato nel progetto:") + print(" il gap-through-stop di SKH01 (sl 2% modellato -> -11/-23% realizzato).") + print(f"\n liquidazione se la perdita a leva supera {WIPE:.0%} dell'equity in un giorno.") + print(f"\n {'scenario':>22}" + "".join(f"{f'k={k:g}':>10}" for k in KS_STRESS) + f"{' k*':>8}") + s0s = simula(r, KS_STRESS, anni=ANNI, dep_eur=0, bersagli=BERSAGLI, aliquota=ALIQUOTA, + patrimoniale=PATRIM, seed=SEED, n_paths=1_500) + g0s = [float(np.median(s0s["mult"][:, j])) ** (1 / ANNI) - 1 for j in range(len(KS_STRESS))] + print(f" {'nessuno stress':>22}" + "".join(f"{g:>10.1%}" for g in g0s) + + f"{f'{KS_STRESS[int(np.argmax(g0s))]:g}x':>8}") + for cpct in (0.10, 0.15, 0.20): + for panno in (0.20, 1.00): + ss = simula(r, KS_STRESS, anni=ANNI, dep_eur=0, bersagli=BERSAGLI, aliquota=ALIQUOTA, + patrimoniale=PATRIM, seed=SEED, crash_pct=cpct, crash_p_anno=panno, + n_paths=1_500) + gg = [float(np.median(ss["mult"][:, j])) ** (1 / ANNI) - 1 for j in range(len(KS_STRESS))] + print(f" {f'-{cpct:.0%} ogni {1/panno:.0f}a':>22}" + + "".join(f"{g:>10.1%}" for g in gg) + + f"{f'{KS_STRESS[int(np.argmax(gg))]:g}x':>8}") + print("\n Lettura: basta UN giorno fuori campione all'anno perche' il massimo collassi da") + print(" due cifre a poche unita', e il ramo destro della curva e' il primo ad andare a") + print(" -100%. La crescita a leva alta e' comprata vendendo la coda che il dataset non") + print(" contiene: e' la stessa firma che il gate `implausible_sharpe` cerca (rischio FUORI") + print(" dal dataset), applicata alla politica invece che alla strategia.") + + # ------------------------------------------------------------------ (5) + sezione("(5) ESEGUIBILITA' — quanto di questa griglia esiste davvero sul conto") + import json + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + frac = cfg["max_notional_per_asset_frac"] + lordo_max = frac * 2 # 2 asset (BTC, ETH) + print(f"\n (a) IL CAP DI CONFIG. max_notional_per_asset_frac = {frac} su 2 asset") + print(f" -> nozionale LORDO massimo = {lordo_max:.2f}x l'equity. **k > {lordo_max:.2f}x NON e'") + print(" eseguibile senza toccare `config/live.json`**, e il 26/07 il progetto ha legato") + print(" una conclusione a quel parametro con un test apposta") + print(" (`test_leva_massima_da_config_resta_sotto_o_uguale_a_1x`): alzare il cap ROMPE") + print(" quel test, che e' esattamente il suo scopo. Quindi 11 delle 14 celle di questa") + print(" griglia sono TEORICHE per costruzione.") + print(f" min_order ${cfg['min_order_usd']} · disaster-SL {cfg['disaster_sl_pct']:.0%} · " + f"cap $ {cfg['max_notional_per_asset_usd']:,} (binding: min(quello, equity×{frac}))") + + print("\n (b) IL KNOB `target_vol` DI TP01 — ipotesi mia: il leverage cap 2x lo strozza.") + from src.data.downloader import load_data # noqa: E402 + from src.strategies.trend_portfolio import ( # noqa: E402 + CANONICAL, TrendPortfolio, resample_1d, simple_returns) + import pandas as pd # noqa: E402 + dfs = {a: resample_1d(load_data(a, "1h")) for a in ("BTC", "ETH")} + def tp01_knob(tv: float, lev: float) -> tuple[float, float, float]: + """(vol realizzata annua, Sharpe, quota di barre incollate al leverage cap) del solo TP01 + 50/50 BTC-ETH, netto fee, con `target_vol=tv` e `leverage=lev`.""" + serie, atcap = {}, [] + for a, df in dfs.items(): + tp = TrendPortfolio(**{**CANONICAL, "target_vol": tv, "leverage": lev}) + tgt = np.nan_to_num(np.asarray(tp.target_series(df), float)) + ret = simple_returns(df["close"].values.astype(float)) + pos = np.zeros(len(tgt)); pos[1:] = tgt[:-1] + net = pos * ret - 0.0005 * np.abs(np.diff(pos, prepend=0.0)) + net[0] = 0.0 + serie[a] = pd.Series(net, index=pd.to_datetime(df["datetime"])) + atcap.append(float((np.abs(tgt) >= lev - 1e-9).mean())) + J = pd.concat(serie, axis=1, join="inner").fillna(0.0) # le due serie hanno lunghezze + x = (0.5 * J["BTC"] + 0.5 * J["ETH"]).values # diverse: si allineano sulle date + return (float(x.std() * 365 ** 0.5), float(x.mean() / x.std() * 365 ** 0.5), + float(np.mean(atcap))) + + rif = tp01_knob(0.20, 2.0)[0] # il canonico, calcolato PRIMA di usarlo come riferimento + print(f"\n {'target_vol':>11}{'lev cap':>9}{'x NOMINALE':>12}{'vol realizz.':>14}" + f"{'x OTTENUTO':>12}{'Sharpe':>9}{'quota barre al cap':>21}") + for lev in (2.0, 4.0): + for tv in (0.10, 0.20, 0.30, 0.40, 0.60): + v, sh, ac = tp01_knob(tv, lev) + print(f" {tv:>11.0%}{lev:>9.1f}{tv/0.20:>12.2f}{v:>14.2%}{v/rif:>12.2f}{sh:>9.2f}" + f"{ac:>21.1%}") + v20, sh20, _ = tp01_knob(0.20, 2.0) + print(f"\n ❌ IPOTESI REFUTATA, e la refutazione e' piu' utile dell'ipotesi. 'x OTTENUTO'") + print(" segue 'x NOMINALE' fino a target_vol 60% (3.00 nominale -> 2.98 ottenuto) e le") + print(" barre incollate al cap sono l'1.0%: **il leverage cap 2x NON e' binding**, quindi") + print(" il knob e' uno scalare lineare pulito. Lo Sharpe e' invariante a ogni cella") + print(f" (1.31 ovunque) — che e' esattamente il punto: la lente con cui `target_vol=20%`") + print(" fu scelto NON puo' distinguere queste righe, e infatti non le distingue.") + print(f"\n ⚠️ Sottoprodotto da registrare: a `target_vol=20%` la vol REALIZZATA di TP01 e'") + print(f" {v20:.2%}, non 20%. Il target vale sulla posizione quando c'e'; TP01 e' long-flat e") + print(" sta flat una parte del tempo, quindi l'etichetta sovrastima il rischio preso di") + print(" ~40%. Chi legge '20%' e lo confronta con un altro sistema sta confrontando un") + print(" parametro con una misura.") + print("\n Cio' che resta binding e' altro: (i) il cap di nozionale di config, punto (a);") + print(" (ii) SKH01 (25% del libro) NON e' vol-targeted affatto (misurato 26/07) — li'") + print(" l'unico modo di alzare la leva e' moltiplicare la size, cioe' margine e") + print(" liquidazione vere, non un parametro di sizing.") + + print("\n (c) COSA COSTA LA LEVA E NON E' NEL MODELLO SOPRA (tutti a sfavore di k alto):") + print(" · la fee scala LINEARMENTE con k (3,5 bps/lato oggi) e SKH01 e' ~4x piu'") + print(" fee-sensibile di TP01 (curva misurata il 26/07: -0.017 Sharpe/bps di book);") + print(" · il funding dei perp e il costo del margine oltre 1x non sono modellati;") + print(" · il min_order $5 su un conto da $635 e la granularita' che ne consegue;") + print(" · l'impatto: il modello assume ritorno indipendente dalla size (vedi sez. 1).") + print(" -> i numeri a k>1 sono un TETTO, non una stima.") + + # ------------------------------------------------------------------ verdetto + sezione("VERDETTO") + s500 = risultati[500] + jj = {k: KS.index(k) for k in (1.00, 1.50, 2.00)} + righe = [] + for nome in BERSAGLI: + for k, j in jj.items(): + a, _, _, p20 = anni_cond(s500["hit"][nome][:, j]) + righe.append((nome, k, a, p20, (s500["dd"][:, j] > DD_SOGLIA).mean())) + print(f"\n A €500/mese (il piano dichiarato), zona ESEGUIBILE e primo passo oltre:") + print(f" {'bersaglio':>14}{'k':>7}{'anni (p50)':>13}{'P<=20a':>9}{'P(DD>30%)':>11}") + for nome, k, a, p20, pdd in righe: + print(f" {nome:>14}{k:>6.2f}x{fmt_anni(a):>13}{p20:>9.0%}{pdd:>11.0%}") + print(f""" + GATE — cosa e' girato e cosa no (un gate non girato si dichiara, non si omette): + · `marginal_vs_tp01` NON APPLICABILE. Non c'e' un flusso di ritorni nuovo da giudicare + marginalmente: k e' uno SCALARE sul libro esistente, quindi la + correlazione col baseline e' 1.00 per costruzione. + · `deflated_sharpe` NON APPLICABILE, e vale la pena dire perche': lo Sharpe e' + INVARIANTE a k (misurato, sez. 5b: 1.31 a ogni cella del knob). + Un gate costruito sullo Sharpe da' lo stesso verdetto su tutta la + griglia — non e' che lo passi, e' che non vede la variabile. + · null del de-levering NON e' un gate da superare qui: e' L'OGGETTO della misura, percorso + in entrambi i versi (k<1 e k>1). E lo conferma: ogni 'meno DD' via + leva e' de-levering per definizione, ogni 'piu' crescita' e' leva. + · fortuna d'ancora GESTITA col fattore MISURATO ×{DP.DELUCK} sul drift (26/07), non con una + banda nuova: la leva non ha una propria ancora, moltiplica il drift. + · `implausible_sharpe` Non girato come funzione (darebbe lo stesso verdetto a ogni k, vedi + sopra); il controllo di plausibilita' e' stato fatto sull'OUTPUT ed + e' FALLITO dal modello a leva alta: capitale mediano ${np.median(s0['cap'][:, jstar]):,.0f} + da $600 in 25 anni. La firma 'rischio fuori dal dataset' c'e', ma + vive nella POLITICA, non nella strategia. + · causalita' Ereditata: la serie viene da `CC.book_series('hourly')`, macchineria + gia' validata; qui non si costruisce nessun segnale nuovo. + · risoluzione MC DICHIARATA: {ris_e:.2%}/anno nella regione eseguibile, {ris:.2%}/anno sulla + griglia intera. La posizione del massimo NON e' risolta. + + 1. IL LIBRO STA A SINISTRA DEL MASSIMO, E DI MOLTO: k* empirico ~10-14x (banda, non un + numero: a 1500 path cade a 10x, a 2500 a {kbest:.0f}x), il libro gira a 1x = {1.0/kbest:.0%} di Kelly + e raccoglie il {lg(g_med[j1])/lg(gbest):.0%} della crescita massima in log. Non e' un plateau + (come lo era il peso 75/25): e' il ramo ripido della curva. Sulla lente della CRESCITA + la scelta canonica `target_vol=20%` non e' ottimale — ed e' un fatto, perche' quella + scelta fu fatta su una lente di Sharpe, che alla leva e' INVARIANTE. + + 2. MA k* NON E' UN NUMERO SU CUI SI PUO' AGIRE, per tre ragioni misurate qui: + (a) e' lineare nell'errore del drift, stimato su {len(r)/365:.1f} anni (sez. 1-bis); + (b) e' comprato vendendo una coda che il dataset NON contiene — un giorno di crash + all'anno lo riporta a poche unita' (sez. 4); + (c) 11 celle su 14 non sono eseguibili: il cap di config tiene il nozionale lordo a + {lordo_max:.2f}x l'equity. (Il knob `target_vol` invece FUNZIONA — misurato, sez. 5b: il + leverage cap 2x non morde. L'ostacolo e' la config e SKH01, non il sizing di TP01.) + + 3. IL BERSAGLIO CAMBIA LA RISPOSTA, e nel verso opposto a quello che la domanda suggerisce. + Su $50k a €500/mese la leva quasi non serve: a k=1 ci si arriva in ~5 anni con P=100%, + perche' fino a quella taglia il capitale lo portano i BONIFICI, non il rendimento + (misura indipendente del 26/07: al traguardo l'80% viene dal rendimento, ma i primi + anni no). Sul muro netto la leva morde: e' proprio dove il rischio di rovina diventa + non-recuperabile, perche' zero e' assorbente e si ricomincia con lo stesso stipendio. + + 4. IL BUCO STRUTTURALE, che e' il risultato piu' trasferibile: **ogni gate di questo + progetto e' invariante alla leva**. `study_family_honest`, il deflated-Sharpe, + `marginal_vs_tp01`, `implausible_sharpe`, il null del de-levering, `weights_tilt_null` + — tutti giudicano forma della distribuzione, correlazioni o PESI RELATIVI, e nessuno + vede la SCALA. Per questo `target_vol=20%` e' sopravvissuto due mesi di scrutinio + senza mai essere esaminato: non c'era una lente che potesse bocciarlo. + + 5. COSA RESTA DI OPERATIVO: nulla da cambiare oggi. Il gradino ESEGUIBILE (k da 1.00x a + 1.25-1.50x, cioe' alzare il cap di config) esiste e non e' rumore MC, ma passa per una + modifica a un file di produzione su un conto con soldi veri, e nessun gate del progetto + lo autorizza: `weights_tilt_null` giudica i PESI, non la scala, e non esiste un gate + 'politica di leva'. Il candidato naturale sarebbe misurare la stessa curva sul canale + FUNDED, dove il vincolo binding e' gia' una regola di DD per-conto e il nozionale non + e' il proprio. + """) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_oi_pin.py b/scripts/research/r0822_oi_pin.py new file mode 100644 index 0000000..87b1f52 --- /dev/null +++ b/scripts/research/r0822_oi_pin.py @@ -0,0 +1,660 @@ +#!/usr/bin/env python +"""r0822_oi_pin.py — OI-PIN: il prezzo e' attratto verso gli strike a massimo open interest +nelle ore che precedono la scadenza Deribit? (ondata 2026-08-22, filone OI-PIN) + +CONTESTO / PERCHE' QUESTO FILONE NON E' "CALENDARIO GIA' MORTO" +-------------------------------------------------------------- +Il 02/07 il progetto ha bocciato il **calendario** delle scadenze Deribit (drift post-expiry +weekly/monthly/quarterly): 0/24 celle a Bonferroni, e il pattern si INVERTE proprio sul +quarterly, dove l'open interest massimo dovrebbe amplificarlo. Quel test guardava le DATE. +Qui si guarda l'OPEN INTEREST VERO, per strike, ora per ora (`data/raw/cb_chain/`): +il meccanismo proposto non e' "il venerdi' e' speciale" ma "il market maker corto gamma +ricompra/rivende verso lo strike dove sta la sua esposizione". E' un meccanismo NON di +calendario, quindi ammissibile secondo la sezione 5 del brief. + +IPOTESI (dichiarata PRIMA di misurare) +-------------------------------------- +H1 Il prezzo deriva VERSO il livello a massima concentrazione di OI / gamma / max-pain nelle + ultime ore prima della scadenza (statistica `toward = sign(K - S_t) * r_{t->T}` > 0). +H2 L'effetto e' piu' forte a poche ore dalla scadenza e sulle scadenze grandi (venerdi'). +H0-ATTESA DELL'AUTORE: **negativa**. Motivi dichiarati in anticipo: + (a) il crypto e' 24/7, senza il "chiusura di borsa" che genera il pinning azionario classico; + (b) l'OI Deribit e' grande ma il perp/spot che lo dovrebbe muovere e' molto piu' grande; + (c) il progetto ha gia' misurato che ogni effetto di finestra su BTC/ETH e' rumore; + (d) e soprattutto: `K_oi` sta quasi sempre VICINO allo spot, quindi qualunque statistica + "il prezzo va verso K" e' contaminata dalla POSIZIONE del livello, non dal suo OI. +Per questo il null location-matched (regola codificata il 02/07 sui livelli di Fibonacci) +non e' un contorno: e' IL test. + +CONVENZIONE CAUSALE (verificata da un assert nel codice) +-------------------------------------------------------- +Barre 1h open-labeled: la barra etichettata `tau` copre [tau, tau+1h) e il suo close e' il +prezzo a `tau+1h`. + * snapshot catena all'ora `hh` -> righe con ts in [hh, hh+1h) + * decisione a fine barra `hh` -> prezzo eseguibile = close(hh) = prezzo a hh+1h + * quindi entrata a `hte` ore dalla scadenza <=> snapshot a dte = hte+1 ore + * uscita al prezzo di regolamento = close della barra (E-1h) = prezzo a E (08:00 UTC) +`A.causality_ok` NON e' applicabile (il segnale non e' funzione del solo feed OHLC): la +causalita' e' garantita per costruzione e verificata da `_assert_causalita()`. + +Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822_oi_pin.py +""" +from __future__ import annotations + +import glob +import sys +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +import altlib as A # noqa: E402 + +STORE = ROOT / "data" / "raw" / "cb_chain" +ASSETS = ("BTC", "ETH") +FEE_SIDE_REAL = 0.00035 # 3,5 bps/lato = fee Deribit REALE del libro (7 bps RT) +FEE_SIDE_CONS = A.FEE_SIDE # 5 bps/lato = convenzione conservativa dei backtest del progetto +MAX_H = 50 # ore prima della scadenza da tenere in memoria +HTE_GRID = (1, 2, 3, 6, 12, 24, 36, 48) # ore-alla-scadenza all'INGRESSO +RULES = ("oi", "gex", "mp") # max OI · max gamma*OI · max pain +PLACEBOS = ("rand", "adj", "jit", "mid", "mr7") +SEED = 20260822 +RNG = np.random.default_rng(SEED) + + +# =========================================================================== dati +def _files() -> list[str]: + fs = sorted(glob.glob(str(STORE / "*.parquet"))) + if not fs: + raise FileNotFoundError(f"{STORE} vuoto o assente") + return fs + + +def load_near(max_h: int = MAX_H) -> pd.DataFrame: + """Catena filtrata alle sole ore vicine a scadenza, letta file per file (RAM: 7 GB in due).""" + cols = ["asset", "strike", "option_type", "ts", "exp", + "open_interest", "gamma", "quote_status"] + out = [] + for p in _files(): + d = pd.read_parquet(p, columns=cols) + d["ts"] = pd.to_datetime(d["ts"], utc=True) + d["exp"] = pd.to_datetime(d["exp"], utc=True) + d["hh"] = d["ts"].dt.floor("h") + d["dte_h"] = (d["exp"] - d["hh"]).dt.total_seconds() / 3600.0 + d = d[(d["dte_h"] > 0) & (d["dte_h"] <= max_h)] + if len(d): + out.append(d) + del d + d = pd.concat(out, ignore_index=True) + # una riga presente non e' un dato presente: OI mancante o quota rotta non conta + d = d[d["open_interest"].notna() & (d["open_interest"] > 0)] + d = (d.sort_values("ts") + .drop_duplicates(subset=["asset", "exp", "hh", "strike", "option_type"], keep="last") + .reset_index(drop=True)) + return d + + +def spot_1h() -> dict[str, pd.Series]: + s = {} + for a in ASSETS: + df = A.get(a, "1h") + s[a] = pd.Series(df["close"].to_numpy(float), + index=pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))) + return s + + +# =========================================================================== livelli +def _max_pain(K: np.ndarray, oi_c: np.ndarray, oi_p: np.ndarray) -> float: + """Strike che minimizza il payout complessivo verso i detentori (max pain classico).""" + d = K[None, :] - K[:, None] # d[i,j] = K_j - K_i (K_i = candidato) + pay = (np.maximum(-d, 0.0) * oi_c[None, :]).sum(1) + (np.maximum(d, 0.0) * oi_p[None, :]).sum(1) + return float(K[int(np.argmin(pay))]) + + +def levels_table(chain: pd.DataFrame, spot: dict[str, pd.Series]) -> pd.DataFrame: + """Un record per (asset, scadenza, ora): i 3 livelli candidati + i 4 placebo di strike + + le misure di concentrazione. I placebo sono LOCATION-MATCHED (regola 02/07).""" + rows = [] + piv = (chain.groupby(["asset", "exp", "hh", "strike", "option_type"], sort=False) + .agg(oi=("open_interest", "sum"), gm=("gamma", "mean")).reset_index()) + for (asset, exp, hh), g in piv.groupby(["asset", "exp", "hh"], sort=False): + S = spot[asset].asof(hh) # prezzo a hh+1h = close(barra hh) + if not np.isfinite(S) or S <= 0: + continue + K = np.sort(g["strike"].unique().astype(float)) + if len(K) < 3: + continue + oi_c = np.zeros(len(K)); oi_p = np.zeros(len(K)); gx = np.zeros(len(K)) + idx = {k: i for i, k in enumerate(K)} + for k, ot, o, gm in zip(g["strike"].to_numpy(float), g["option_type"].to_numpy(), + g["oi"].to_numpy(float), g["gm"].to_numpy(float)): + i = idx[float(k)] + (oi_c if ot == "C" else oi_p)[i] += o + gx[i] += o * (gm if np.isfinite(gm) else 0.0) + oi = oi_c + oi_p + tot = oi.sum() + if tot <= 0: + continue + i_oi = int(np.argmax(oi)) + lv = { + "oi": float(K[i_oi]), + "gex": float(K[int(np.argmax(gx))]) if gx.max() > 0 else np.nan, + "mp": _max_pain(K, oi_c, oi_p), + } + # ---- placebo location-matched ------------------------------------------------- + # rand: permutare l'OI fra gli strike e riprendere l'argmax equivale ESATTAMENTE a + # estrarre uno strike uniforme dalla griglia (l'argmax di un vettore permutato + # e' uniforme sulle posizioni) -> lo si implementa cosi', ed e' esatto. + p_rand = float(K[RNG.integers(len(K))]) + # adj: lo strike ADIACENTE al max-OI (stesso quartiere, OI diverso) + j = i_oi + (1 if (i_oi == 0 or (i_oi < len(K) - 1 and RNG.random() < 0.5)) else -1) + p_adj = float(K[int(np.clip(j, 0, len(K) - 1))]) + # jit: strike arbitrario alla STESSA distanza dallo spot ("0.618 vs 0.58?") + dist = abs(K - S) + d0 = abs(lv["oi"] - S) + cand = np.where((dist >= 0.5 * d0) & (dist <= 1.5 * d0) & (K != lv["oi"]))[0] + p_jit = float(K[cand[RNG.integers(len(cand))]]) if len(cand) else np.nan + # mid: punto medio della griglia quotata = puro artefatto di copertura, zero info OI + p_mid = float(0.5 * (K[0] + K[-1])) + rows.append(dict( + asset=asset, exp=exp, hh=hh, dte_h=float((exp - hh).total_seconds() / 3600.0), + S=float(S), n_k=len(K), tot_oi=float(tot), + k_oi=lv["oi"], k_gex=lv["gex"], k_mp=lv["mp"], + p_rand=p_rand, p_adj=p_adj, p_jit=p_jit, p_mid=p_mid, + top_share=float(oi[i_oi] / tot), + hhi=float(((oi / tot) ** 2).sum()), + k_step=float(np.median(np.diff(K))) if len(K) > 1 else np.nan, + )) + return pd.DataFrame(rows) + + +# =========================================================================== il FATTO +def build_events(lev: pd.DataFrame, spot: dict[str, pd.Series]) -> pd.DataFrame: + """Un evento per (asset, scadenza, hte): livelli, prezzo d'ingresso, prezzo di regolamento.""" + ev = [] + mr7 = {a: spot[a].rolling(24 * 7, min_periods=24).mean() for a in ASSETS} + for a in ASSETS: + s = spot[a] + sub = lev[lev["asset"] == a] + for exp, g in sub.groupby("exp"): + t_set = pd.Timestamp(exp) - pd.Timedelta(hours=1) # barra il cui close e' a E + if t_set not in s.index: + continue + S_T = float(s.loc[t_set]) + gi = g.set_index("hh") + for hte in HTE_GRID: + hh = pd.Timestamp(exp) - pd.Timedelta(hours=hte + 1) + if hh not in gi.index or hh not in s.index: + continue + r = gi.loc[hh] + S = float(s.loc[hh]) # close(hh) = prezzo a E-hte + if not np.isfinite(S) or S <= 0: + continue + rec = dict(asset=a, exp=exp, hte=hte, hh=hh, S=S, S_T=S_T, + ret=S_T / S - 1.0, n_k=int(r["n_k"]), tot_oi=float(r["tot_oi"]), + top_share=float(r["top_share"]), hhi=float(r["hhi"]), + k_step=float(r["k_step"]), + dow=int(pd.Timestamp(exp).dayofweek)) + lvls = {"oi": r["k_oi"], "gex": r["k_gex"], "mp": r["k_mp"], + "rand": r["p_rand"], "adj": r["p_adj"], "jit": r["p_jit"], + "mid": r["p_mid"], "mr7": float(mr7[a].asof(hh))} + for nm, K in lvls.items(): + K = float(K) if K is not None else np.nan + if not np.isfinite(K) or K <= 0: + rec[f"d_{nm}"] = np.nan; rec[f"tw_{nm}"] = np.nan + rec[f"sh_{nm}"] = np.nan + continue + d = (K - S) / S + rec[f"K_{nm}"] = K + rec[f"d_{nm}"] = d + rec[f"tw_{nm}"] = np.sign(d) * rec["ret"] # verso il livello + rec[f"sh_{nm}"] = (abs(S - K) - abs(S_T - K)) / S # avvicinamento + ev.append(rec) + E = pd.DataFrame(ev) + # NULL STATICO CAUSALE (lezione 25/07: il primo null statico di STATARB usava + # sign(mean(segnale)) su tutto il campione = look-ahead; qui la maggioranza e' ESPANDENTE + # e shiftata). Risponde a: "il candidato e' solo 'sempre nello stesso verso' prima della + # scadenza?" — che su 3,7 mesi di mercato direzionale e' l'ipotesi piu' probabile. + E = E.sort_values(["asset", "hte", "hh"]).reset_index(drop=True) + sgn = np.sign(E["d_mp"].to_numpy(float)) + maj = (pd.Series(sgn).groupby([E["asset"], E["hte"]]) + .transform(lambda x: x.expanding().mean().shift(1))) + E["d_maj"] = np.where(np.isfinite(maj), np.sign(maj) * 0.01, np.nan) + E["tw_maj"] = np.sign(E["d_maj"]) * E["ret"] + E["sh_maj"] = np.nan + E["d_long"] = 0.01; E["tw_long"] = E["ret"]; E["sh_long"] = np.nan + E["d_short"] = -0.01; E["tw_short"] = -E["ret"]; E["sh_short"] = np.nan + return E + + +def _assert_causalita(chain: pd.DataFrame, ev: pd.DataFrame) -> str: + """Il livello usato a `hh` deve venire da quote osservate PRIMA del prezzo d'ingresso.""" + bad = int((chain["ts"] < chain["hh"]).sum() + + (chain["ts"] >= chain["hh"] + pd.Timedelta("1h")).sum()) + assert bad == 0, "snapshot fuori dalla propria ora" + dte = (pd.to_datetime(ev["exp"]) - pd.to_datetime(ev["hh"])).dt.total_seconds() / 3600.0 + assert bool((dte == ev["hte"] + 1).all()), "hte non coerente con lo snapshot" + assert bool(((pd.to_datetime(ev["hh"]) + pd.Timedelta(hours=1)) + <= pd.to_datetime(ev["exp"])).all()), "ingresso oltre la scadenza" + return (f"OK — 0/{len(chain)} righe di catena fuori dalla propria ora; " + f"{len(ev)} eventi con dte == hte+1 (ingresso = close(hh), quota vista in [hh,hh+1h))") + + +# =========================================================================== inferenza +def cluster_boot(x: np.ndarray, clusters: np.ndarray, n: int = 2000, seed: int = SEED): + """IC95 della media con ricampionamento dei CLUSTER (una scadenza = 1 cluster: BTC ed ETH + della stessa scadenza non sono osservazioni indipendenti).""" + x = np.asarray(x, float) + m = np.isfinite(x) + x, clusters = x[m], np.asarray(clusters)[m] + if len(x) < 5: + return np.nan, np.nan, np.nan, 0 + uc = np.unique(clusters) + groups = [x[clusters == c] for c in uc] + rng = np.random.default_rng(seed) + draws = np.empty(n) + for i in range(n): + pick = rng.integers(0, len(groups), len(groups)) + draws[i] = np.mean(np.concatenate([groups[j] for j in pick])) + return float(np.mean(x)), float(np.percentile(draws, 2.5)), float(np.percentile(draws, 97.5)), len(uc) + + +def mde(x: np.ndarray, clusters: np.ndarray) -> float: + """Effetto minimo rilevabile (alpha .05 bilaterale, potenza 80%) con questo campione.""" + x = np.asarray(x, float) + m = np.isfinite(x) + x, cl = x[m], np.asarray(clusters)[m] + if len(x) < 3: + return np.nan + nc = max(len(np.unique(cl)), 1) + return float(2.802 * np.std(x) / np.sqrt(nc)) + + +# =========================================================================== strategia +_W: dict = {} + + +def win_1h(asset: str, lo: pd.Timestamp, hi: pd.Timestamp) -> pd.DataFrame: + """Feed 1h tagliato alla finestra della catena: fuori da li' la posizione e' zero per + costruzione, e valutare 70k barre 288 volte costerebbe minuti per nulla.""" + key = (asset, lo, hi) + if key not in _W: + df = A.get(asset, "1h") + dt = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + m = (dt >= lo - pd.Timedelta("2D")) & (dt <= hi + pd.Timedelta("2D")) + _W[key] = df.loc[m].reset_index(drop=True) + return _W[key] + + +def hourly_target(ev: pd.DataFrame, asset: str, rule: str, hte: int, + band: tuple[float, float] | None, sizing: str, + subsample: str, df1h: pd.DataFrame) -> np.ndarray: + """Posizione oraria: entra `hte` ore prima della scadenza nel verso del livello, esce a + scadenza. Se piu' finestre si sovrappongono vince la scadenza PIU' VICINA (una posizione + alla volta = cio' che il conto Deribit puo' davvero fare: strumento unico, netting).""" + idx = pd.DatetimeIndex(pd.to_datetime(df1h["datetime"], utc=True)) + pos = np.zeros(len(idx)) + loc = pd.Series(np.arange(len(idx)), index=idx) + e = ev[(ev["asset"] == asset) & (ev["hte"] == hte)].copy() + if subsample == "fri": + e = e[e["dow"] == 4] + elif subsample == "big": + e = e[e["tot_oi"] >= e["tot_oi"].median()] + d = e[f"d_{rule}"].to_numpy(float) + keep = np.isfinite(d) + if band is not None: + keep &= (np.abs(d) >= band[0]) & (np.abs(d) <= band[1]) + e = e[keep] + d = d[keep] + if len(e) == 0: + return pos + w = np.sign(d) if sizing == "sign" else np.clip(d / 0.01, -1.0, 1.0) + order = np.argsort(e["exp"].to_numpy())[::-1] # lontane prima: la scadenza VICINA sovrascrive + for j in order: + hh = pd.Timestamp(e["hh"].iloc[j]); exp = pd.Timestamp(e["exp"].iloc[j]) + last = exp - pd.Timedelta(hours=2) + if hh not in loc.index or last not in loc.index: + continue + i0, i1 = int(loc[hh]), int(loc[last]) + if i1 >= i0: + pos[i0:i1 + 1] = w[j] + return pos + + +def strat_daily(ev: pd.DataFrame, rule: str, hte: int, band, sizing: str, subsample: str, + fee_side: float) -> tuple[pd.Series, dict]: + """Serie giornaliera netta 50/50 BTC+ETH + metriche, sulla sola finestra della catena.""" + nets, info = {}, {} + lo_w = pd.Timestamp(ev["hh"].min()); hi_w = pd.Timestamp(ev["exp"].max()) + for a in ASSETS: + df = win_1h(a, lo_w, hi_w) + tgt = hourly_target(ev, a, rule, hte, band, sizing, subsample, df) + r = A.eval_weights(df, tgt, fee_side=fee_side) + nets[a] = pd.Series(r["net"], index=r["idx"]) + info[a] = int(np.sum(np.abs(np.diff(np.r_[0.0, tgt])) > 1e-12)) + J = pd.concat(nets, axis=1, join="inner").fillna(0.0) + daily = A._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]]) + lo = pd.Timestamp(ev["hh"].min()).normalize() + daily = daily[daily.index >= lo] + return daily, info + + +def metr(s: pd.Series) -> dict: + r = s.dropna() + if len(r) < 5: + return dict(sharpe=np.nan, dd=np.nan, cagr=np.nan, n=len(r)) + eq = np.cumprod(1 + r.to_numpy(float)); pk = np.maximum.accumulate(eq) + yrs = max((r.index[-1] - r.index[0]).total_seconds() / 86400 / 365.25, 1e-6) + return dict(sharpe=A._sh(r), dd=float(np.max((pk - eq) / pk)), + cagr=float(eq[-1] ** (1 / yrs) - 1), n=int(len(r))) + + +# =========================================================================== main +def main() -> None: + P = print + P("=" * 100) + P("OI-PIN — pinning verso gli strike a massimo open interest (catena Deribit oraria)") + P("=" * 100) + + # ---------------------------------------------------------------- 1. DATO + chain = load_near() + spot = spot_1h() + lev = levels_table(chain, spot) + ev = build_events(lev, spot) + P("\n[1] DATO") + P(f" righe catena a <= {MAX_H}h dalla scadenza : {len(chain):,}") + P(f" scadenze distinte : {ev.groupby('asset').exp.nunique().to_dict()}") + P(f" finestra : {ev.hh.min()} -> {ev.exp.max()}") + P(f" eventi (asset x scadenza x hte) : {len(ev)}") + P(f" strike per snapshot mediana/min : {lev.n_k.median():.0f} / {lev.n_k.min():.0f}") + P(f" causalita' : {_assert_causalita(chain, ev)}") + P(" ⚠ il collettore scarta gli strumenti con OI<100 -> la griglia e' TRONCATA sulle code:") + P(" il max-OI ne soffre poco (e' un argmax), il MAX PAIN si', perche' e' una somma su tutta") + P(" la catena. Il verdetto su 'mp' va letto come 'max pain sulla catena OI>=100'.") + + # ---------------------------------------------------------------- 2. POTENZA (PRIMA del risultato) + P("\n[2] POTENZA DICHIARATA — calcolata PRIMA di leggere qualunque effetto") + P(" Unita' statistica = una SCADENZA (BTC ed ETH della stessa data non sono indipendenti).") + P(f" {'hte':>4} {'n oss':>6} {'n clus':>7} {'sd(ret)':>9} {'MDE medio/trade':>16} {'MDE ann.':>10}") + for hte in HTE_GRID: + e = ev[ev["hte"] == hte] + m = mde(e["ret"].to_numpy(float), e["exp"].to_numpy()) + ann = m * (365.25 * 24 / hte) if np.isfinite(m) else np.nan + P(f" {hte:>4} {len(e):>6} {e.exp.nunique():>7} {np.nanstd(e['ret']):>9.4f}" + f" {m:>16.4f} {ann:>9.1%}") + P(" LETTURA OBBLIGATORIA: il campione rileva solo effetti ENORMI. A 24h l'MDE e' ~0,5-0,7%") + P(" per trade contro 0,07% di fee: un pinning realistico (pochi bps) e' INVISIBILE qui.") + P(" Quindi un 'non significativo' NON prova assenza; un 'significativo' sarebbe cosi' grande") + P(" da essere sospetto. Il verdetto massimo raggiungibile e' LEAD.") + + # ---------------------------------------------------------------- 3. IL FATTO + P("\n[3] IL FATTO — dove sta il livello e dove va il prezzo") + P(" |ret| verso la scadenza e distanza |d| del livello (mediane):") + P(f" {'hte':>4} {'|ret| med':>10} {'|d_oi| med':>11} {'|d_gex|':>9} {'|d_mp|':>9}" + f" {'d_oi<0':>8} {'top_share':>10}") + for hte in HTE_GRID: + e = ev[ev["hte"] == hte] + P(f" {hte:>4} {e['ret'].abs().median():>10.4f} {e['d_oi'].abs().median():>11.4f}" + f" {e['d_gex'].abs().median():>9.4f} {e['d_mp'].abs().median():>9.4f}" + f" {(e['d_oi'] < 0).mean():>8.2f} {e['top_share'].median():>10.3f}") + P(" ⚠ se 'd_oi<0' e' lontano da 0.50 il livello ha un BIAS DI SEGNO (griglia troncata o") + P(" mercato in trend): una statistica 'il prezzo va verso K' lo erediterebbe.") + + P("\n Statistica di pinning tw = sign(K-S)*ret (media, IC95 cluster-bootstrap su scadenze):") + P(f" {'hte':>4} {'rule':>5} {'media tw':>10} {'IC95 lo':>9} {'IC95 hi':>9} {'sig':>4}" + f" {'shrink med':>11} {'n':>5}") + fact = [] + for hte in HTE_GRID: + e = ev[ev["hte"] == hte] + for rl in RULES: + m, lo, hi, nc = cluster_boot(e[f"tw_{rl}"].to_numpy(float), e["exp"].to_numpy()) + sg = "***" if (np.isfinite(lo) and lo > 0) else ("neg" if (np.isfinite(hi) and hi < 0) else "-") + P(f" {hte:>4} {rl:>5} {m:>10.5f} {lo:>9.5f} {hi:>9.5f} {sg:>4}" + f" {np.nanmedian(e[f'sh_{rl}']):>11.5f} {int(np.isfinite(e[f'tw_{rl}']).sum()):>5}") + fact.append(dict(hte=hte, rule=rl, mean=m, lo=lo, hi=hi)) + F = pd.DataFrame(fact) + P(f" celle con IC95 tutto sopra zero: {int((F.lo > 0).sum())}/{len(F)}" + f" | tutto sotto zero: {int((F.hi < 0).sum())}/{len(F)}") + + P("\n LA STESSA STATISTICA SUI LIVELLI PLACEBO — se sale anche li', NON e' pinning:") + P(f" {'hte':>4} " + " ".join(f"{('tw_' + n):>10}" for n in + ("oi", "mp", "rand", "jit", "mid", "mr7", "maj", "short"))) + for hte in HTE_GRID: + e = ev[ev["hte"] == hte] + P(f" {hte:>4} " + " ".join( + f"{np.nanmean(e[f'tw_{n}']):>10.5f}" for n in + ("oi", "mp", "rand", "jit", "mid", "mr7", "maj", "short"))) + P("\n QUANTO SONO LO STESSO SEGNALE? accordo del SEGNO di (K-S) fra max-pain e i placebo:") + for n in ("mid", "mr7", "oi", "maj"): + agr = [float(np.nanmean(np.sign(ev.loc[ev.hte == h, "d_mp"]) + == np.sign(ev.loc[ev.hte == h, f"d_{n}"]))) for h in HTE_GRID] + P(f" mp vs {n:<5}: " + " ".join(f"h{h}={a:.2f}" for h, a in zip(HTE_GRID, agr))) + P(f" base rate sign<0 : " + " ".join( + f"{n}={np.nanmean(np.sign(ev[f'd_{n}']) < 0):.2f}" for n in ("oi", "gex", "mp", "mid", "mr7"))) + for a in ASSETS: + sp_ = spot[a] + w = sp_[(sp_.index >= ev.hh.min()) & (sp_.index <= ev.exp.max())] + P(f" deriva {a} nella finestra: {w.iloc[-1] / w.iloc[0] - 1:+.1%}" + f" (se il livello sta quasi sempre da un lato, il candidato e' una scommessa direzionale)") + + # ---------------------------------------------------------------- 4. NULL LOCATION-MATCHED + P("\n[4] NULL LOCATION-MATCHED (regola 02/07: e' la POSIZIONE o e' l'OI?)") + P(" Confronto APPAIATO per evento: tw(livello speciale) - tw(placebo alla stessa distanza).") + P(" placebo: rand=strike uniforme (== permutazione dell'OI, equivalenza esatta) · adj=strike") + P(" adiacente al max-OI · jit=strike arbitrario a distanza 0.5-1.5x (analogo Fib±jitter) ·") + P(" mid=centro della griglia quotata (zero info OI) · mr7=media 7g dello spot (mean-reversion).") + P(f" {'hte':>4} {'rule':>5} " + " ".join(f"{('Δ vs ' + p):>12}" for p in PLACEBOS)) + npos = ntot = 0 + for hte in HTE_GRID: + e = ev[ev["hte"] == hte] + for rl in RULES: + cells = [] + for p in PLACEBOS: + dd = e[f"tw_{rl}"].to_numpy(float) - e[f"tw_{p}"].to_numpy(float) + m, lo, hi, _ = cluster_boot(dd, e["exp"].to_numpy()) + mark = "*" if (np.isfinite(lo) and lo > 0) else (" ") + cells.append(f"{m:>11.5f}{mark}") + ntot += 1 + npos += int(np.isfinite(lo) and lo > 0) + P(f" {hte:>4} {rl:>5} " + " ".join(cells)) + P(f" celle (rule x hte x placebo) in cui lo SPECIALE batte il placebo con IC95>0:" + f" {npos}/{ntot} (atteso per caso ~{0.025 * ntot:.0f})") + + # ---------------------------------------------------------------- 5. CONDIZIONAMENTO + P("\n[5] CONDIZIONAMENTO — la concentrazione di OI e le scadenze grandi cambiano qualcosa?") + P(f" {'hte':>4} {'gruppo':>16} {'media tw_oi':>12} {'IC95 lo':>9} {'IC95 hi':>9} {'n':>4}") + for hte in (3, 12, 24): + e = ev[ev["hte"] == hte] + hi_c = e["top_share"] >= e["top_share"].median() + big = e["tot_oi"] >= e["tot_oi"].quantile(0.75) + for nm, sel in (("conc ALTA", hi_c), ("conc BASSA", ~hi_c), + ("OI top-25%", big), ("venerdi'", e["dow"] == 4)): + x = e.loc[sel, "tw_oi"].to_numpy(float) + m, lo, hh_, _ = cluster_boot(x, e.loc[sel, "exp"].to_numpy()) + P(f" {hte:>4} {nm:>16} {m:>12.5f} {lo:>9.5f} {hh_:>9.5f} {int(np.isfinite(x).sum()):>4}") + + # ---------------------------------------------------------------- 6. STRATEGIA + P("\n[6] STRATEGIA — expectancy netta fee (3,5 bps/lato reali) e serie giornaliera") + bands = {"nessuna": None, "0.2-3%": (0.002, 0.03)} + subs = ("all", "big", "fri") + sizings = ("sign", "prop") + cells, n_trials = [], 0 + for rl in RULES: + for hte in HTE_GRID: + for bn, bd in bands.items(): + for sz in sizings: + for sb in subs: + n_trials += 1 + d, info = strat_daily(ev, rl, hte, bd, sz, sb, FEE_SIDE_REAL) + mm = metr(d) + cells.append(dict(rule=rl, hte=hte, band=bn, sizing=sz, sub=sb, + **mm, ntrade=sum(info.values()))) + C = pd.DataFrame(cells) + P(f" griglia strategia valutata: {n_trials} celle " + f"({len(RULES)} rule x {len(HTE_GRID)} hte x {len(bands)} bande x {len(sizings)} sizing x {len(subs)} sottocampioni)") + P(f" celle con Sharpe > 0: {(C.sharpe > 0).sum()}/{len(C)} (una famiglia di RUMORE sta a ~50%)") + P(" migliori 8 celle per Sharpe (NON e' una selezione: e' il MASSIMO della griglia, che serve al DSR):") + P(C.sort_values("sharpe", ascending=False).head(8).to_string(index=False, + float_format=lambda v: f"{v:.3f}")) + P(" peggiori 3:") + P(C.sort_values("sharpe").head(3).to_string(index=False, float_format=lambda v: f"{v:.3f}")) + + P("\n [6-bis] LA STESSA MACCHINA SUI LIVELLI PLACEBO (stesse finestre, stessa fee, stesso") + P(" sizing): se un placebo regge quanto il livello 'speciale', l'OI non aggiunge nulla.") + nulls = [] + for rl in ("mp", "oi", "mid", "mr7", "rand", "jit", "maj", "short", "long"): + for hte in (3, 6, 12, 24): + for bn, bd in bands.items(): + d, _i = strat_daily(ev, rl, hte, bd, "sign", "all", FEE_SIDE_REAL) + mm = metr(d) + nulls.append(dict(rule=rl, hte=hte, band=bn, sharpe=mm["sharpe"], + cagr=mm["cagr"], dd=mm["dd"])) + N = pd.DataFrame(nulls) + piv = N.pivot_table(index=["hte", "band"], columns="rule", values="sharpe") + P(piv.reindex(columns=["mp", "oi", "mid", "mr7", "rand", "jit", "maj", "short", "long"]) + .to_string(float_format=lambda v: f"{v:6.2f}")) + P(f" celle placebo valutate: {len(N)} (contano come trial: la griglia dichiarata sale)") + + best = C.sort_values("sharpe", ascending=False).iloc[0] + P(f"\n CELLA MIGLIORE = {best['rule']}/hte{int(best['hte'])}/{best['band']}/{best['sizing']}/{best['sub']}") + + # expectancy per trade, in % e in R, sulla cella migliore + e = ev[ev["hte"] == int(best["hte"])].copy() + if best["sub"] == "big": + e = e[e["tot_oi"] >= e["tot_oi"].median()] + elif best["sub"] == "fri": + e = e[e["dow"] == 4] + dd = e[f"d_{best['rule']}"].to_numpy(float) + keep = np.isfinite(dd) + if best["band"] != "nessuna": + keep &= (np.abs(dd) >= 0.002) & (np.abs(dd) <= 0.03) + tw = e.loc[keep, f"tw_{best['rule']}"].to_numpy(float) + dk = np.abs(dd[keep]) + net = tw - 2 * FEE_SIDE_REAL + wins, losses = net[net > 0], net[net < 0] + rr = (wins.mean() / abs(losses.mean())) if len(wins) and len(losses) else np.nan + P(f" trade: {len(net)} · WR {np.mean(net > 0):.1%} · RR medio {rr:.2f}" + f" · WR-knob atteso 1/(1+RR) = {1 / (1 + rr):.1%}" if np.isfinite(rr) else " RR n.d.") + P(f" expectancy NETTA per trade: {np.mean(net):+.5f} ({np.mean(net) * 100:+.3f}%)" + f" | in R (R = distanza dal livello): {np.mean(net / np.maximum(dk, 1e-6)):+.3f} R") + P(f" a fee ZERO: {np.mean(tw):+.5f} -> {'l edge lordo non esiste' if np.mean(tw) <= 0 else 'edge lordo positivo, la fee lo mangia?'}") + + dbest, info = strat_daily(ev, best["rule"], int(best["hte"]), + None if best["band"] == "nessuna" else (0.002, 0.03), + best["sizing"], best["sub"], FEE_SIDE_REAL) + dcons, _ = strat_daily(ev, best["rule"], int(best["hte"]), + None if best["band"] == "nessuna" else (0.002, 0.03), + best["sizing"], best["sub"], FEE_SIDE_CONS) + mb, mc = metr(dbest), metr(dcons) + P(f" serie giornaliera cella migliore fee 3,5bps: Sharpe {mb['sharpe']:+.2f}" + f" · maxDD {mb['dd']:.2%} · CAGR {mb['cagr']:+.2%} · n={mb['n']}g") + P(f" fee 5,0bps: Sharpe {mc['sharpe']:+.2f}" + f" · maxDD {mc['dd']:.2%} · CAGR {mc['cagr']:+.2%}") + # scomposizione: per mese e per ERA DI COLLETTORE (bite:research fino al 30/07, poi pyg) + P("\n SCOMPOSIZIONE (regola 22/08: un contributo positivo si scompone prima di crederci)") + mo = dbest.groupby([dbest.index.year, dbest.index.month]) + P(" " + " · ".join(f"{y}-{m:02d}: Sh {A._sh(g):+.2f} ret {float(np.prod(1 + g) - 1):+.2%} ({len(g)}g)" + for (y, m), g in mo)) + cut = pd.Timestamp("2026-07-30", tz="UTC") + e1, e2 = dbest[dbest.index < cut], dbest[dbest.index >= cut] + P(f" era collettore bite:research (<30/07): Sh {A._sh(e1):+.2f} ret {float(np.prod(1 + e1) - 1):+.2%} ({len(e1)}g)" + f" | pyg (>=30/07): Sh {A._sh(e2):+.2f} ret {float(np.prod(1 + e2) - 1):+.2%} ({len(e2)}g)") + top5 = dbest.sort_values(ascending=False).head(5) + P(f" concentrazione: i 5 giorni migliori valgono {float(np.prod(1 + top5) - 1):+.2%}" + f" su {float(np.prod(1 + dbest) - 1):+.2%} totali" + f" = {100 * np.log1p(top5).sum() / max(np.log1p(dbest).sum(), 1e-9):.0f}% del log-equity") + P(" ⚠ HOLD-OUT: NON ESISTE. La catena parte dal 2026-05-01 e l'hold-out del progetto e'") + P(" 2025-01-01: TUTTA la serie sta dentro l'hold-out, quindi non c'e' nessuna finestra") + P(" fuori campione da mostrare. Questo NON e' un dettaglio: e' il motivo per cui") + P(" 3,7 mesi di catena non possono produrre uno sleeve.") + + # ---------------------------------------------------------------- 7. GATE + P("\n[7] GATE") + P(f" causalita' : {_assert_causalita(chain, ev)}") + P(" A.causality_ok : NON GIRATO — non applicabile (il segnale non e' funzione del solo") + P(" feed OHLC; la causalita' e' garantita dalla convenzione hte=dte-1") + P(" e verificata dall'assert qui sopra).") + + # DSR: conta TUTTI i trial al rialzo + all_sr = [float(v) for v in C["sharpe"].to_numpy(float) if np.isfinite(v)] + all_sr_wide = all_sr + [float(v) for v in N["sharpe"].to_numpy(float) if np.isfinite(v)] + n_fact = len(HTE_GRID) * len(RULES) * (1 + len(PLACEBOS)) + 3 * 4 # celle del fatto + condiz. + trials_tot = len(all_sr_wide) + n_fact + dsr, sr0 = A.deflated_sharpe(mb["sharpe"], all_sr, dbest, dpy=365.25) + dsr_w, _ = A.deflated_sharpe(mb["sharpe"], all_sr_wide, dbest, dpy=365.25) + P(f" deflated Sharpe : DSR {dsr:.3f} (soglia 0.95) · Sharpe-null atteso {sr0:.2f}" + f" · trial contati {len(all_sr)} (griglia strategia)") + P(f" DSR contando ANCHE le celle placebo ({len(all_sr_wide)} trial): {dsr_w:.3f}") + P(f" trial TOTALI dichiarati al rialzo (fatto+null+condiz.+strategia): {trials_tot}") + + # marginal vs TP01 + mv = A.marginal_vs_tp01(dbest) + P(f" marginal vs TP01 : {mv.get('marginal_verdict')} · corr {mv.get('corr_full')}" + f" · uplift w25 full {mv.get('blends', {}).get('w25', {}).get('uplift_full')}" + f" · giorni in comune {mv.get('n_days')}") + P(" ⚠ il gate NON PUO' dare ADDS con questo campione: `multicut_persistent`") + P(" richiede >=2 tagli annuali da >=120 giorni e la serie ne ha ~113 in UN") + P(" anno solo; e `has_insample_edge` e' True per DEFAULT perche' non c'e'") + P(" alcun pre-2025. Il verdetto marginale qui e' strutturale, non informativo.") + + # null del de-levering (obbligatorio su ogni claim di DD) + B = A.tp01_baseline_daily() + J = pd.concat({"B": B, "C": dbest}, axis=1, join="inner").dropna() + if len(J) > 20: + bl = 0.75 * J["B"] + 0.25 * J["C"] + dd_bl = A._dd_ret(bl) + ks = np.linspace(0.05, 1.0, 96) + ok = [(k, A._sh(k * J["B"]), A._dd_ret(k * J["B"])) for k in ks] + cand = [(k, s) for k, s, d_ in ok if d_ <= dd_bl] + kbest = max(cand, key=lambda t: t[1]) if cand else None + P(f" null de-levering : blend 0.75TP01+0.25cand -> Sharpe {A._sh(bl):+.2f} / DD {dd_bl:.2%}" + f" | k*TP01 a pari DD -> k={kbest[0]:.2f} Sharpe {kbest[1]:+.2f}" + if kbest else " null de-levering : non calcolabile") + if kbest: + P(f" esito: {'REFUTED (il de-levering fa meglio)' if kbest[1] >= A._sh(bl) else 'superato'}") + else: + P(" null de-levering : NON GIRATO (sovrapposizione con TP01 troppo corta)") + + # implausible + imp = A.implausible_sharpe(dbest, n_trades=int(best["ntrade"])) + P(f" implausible_sharpe : implausible={imp['implausible']} · {imp.get('reasons')}") + + # anchor: l'ANCORA di questa strategia e' hte (quale ora prima della scadenza si entra) + ab = A.anchor_luck_band( + lambda h: strat_daily(ev, best["rule"], int(h), + None if best["band"] == "nessuna" else (0.002, 0.03), + best["sizing"], best["sub"], FEE_SIDE_REAL)[0], + offsets=list(HTE_GRID), canonical=int(best["hte"])) + P(f" anchor (hte) : canonico {ab.get('canonical'):.2f} al {100 * ab.get('canonical_pctl', np.nan):.0f}° pctl" + f" · MEDIANA ONESTA {ab.get('median'):.2f} · banda [{ab.get('lo'):.2f},{ab.get('hi'):.2f}]" + f" · positive {ab.get('frac_positive'):.0%} · gate_pass={ab.get('gate_pass')}") + + # eseguibilita' a $600 + P(" eseguibilita' $600 :", end=" ") + hc = [] + for a in ASSETS: + df = win_1h(a, pd.Timestamp(ev["hh"].min()), pd.Timestamp(ev["exp"].max())) + tgt = hourly_target(ev, a, best["rule"], int(best["hte"]), + None if best["band"] == "nessuna" else (0.002, 0.03), + best["sizing"], best["sub"], df) + sc = A.eval_weights_smallcap(df, tgt, capital=300.0, fee_side=FEE_SIDE_REAL) + hc.append(f"{a} haircut {sc['sharpe_haircut']:+.3f} ({sc['n_executed_trades']} trade eseguiti)") + P(" · ".join(hc)) + P(" (300$/asset = il cap per-asset del libro live a questo capitale;") + P(" una gamba ±1 muove 300$ >> min-order 5$ -> nessun ordine saltato)") + + # ---------------------------------------------------------------- 8. SINTESI + P("\n[8] SINTESI") + n_fact_pos = int((F.lo > 0).sum()) + P(f" fatto : {n_fact_pos}/{len(F)} celle (rule x hte) con pinning significativo a IC95") + P(f" null loc. : {npos}/{ntot} confronti appaiati in cui il livello 'speciale' batte il placebo") + P(f" strategia : miglior Sharpe della griglia {mb['sharpe']:+.2f} su {mb['n']} giorni, DSR {dsr:.3f}") + P(f" marginale : {mv.get('marginal_verdict')} (strutturalmente non ADDS-abile a 3,7 mesi)") + P("=" * 100) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_ortho_screen.py b/scripts/research/r0822_ortho_screen.py new file mode 100644 index 0000000..b91e181 --- /dev/null +++ b/scripts/research/r0822_ortho_screen.py @@ -0,0 +1,674 @@ +#!/usr/bin/env python3 +"""r0822_ortho_screen.py — ONDATA 2026-08-22, filone ORTHO-SCREEN (la rete larga, onesta). + +NON e' un'ipotesi: e' un SETACCIO. Sette famiglie di segnale mai (o solo parzialmente) +coperte dalle ondate precedenti, passate TUTTE dallo stesso protocollo, senza eccezioni: + + 1. famiglia DICHIARATA prima (parametri + celle), trial contati AL RIALZO; + 2. A.study_family_honest -> cella scelta IN-SAMPLE-ONLY + deflated-Sharpe di famiglia; + 3. A.marginal_vs_tp01 -> ADDS / NEUTRAL / DILUTES / HEDGE / NOISE / REDUNDANT; + 4. A.causality_ok (+ A.day_boundary_robust dove ha senso, vedi NOTA CONFINE); + 5. banda d'ancora su 8 offset orari (bar 1d ricostruite da 1h) -> la stima onesta e' la MEDIANA; + 6. null del DE-LEVERING su ogni claim di drawdown; + 7. A.eval_weights_smallcap($600) + A.implausible_sharpe sulla cella scelta; + 8. **DSR di SCREEN**: oltre al deflated-Sharpe di famiglia, il deflated-Sharpe calcolato sul + POOL di TUTTE le celle di TUTTE le famiglie. Se scelgo il migliore fra 7 famiglie, i trial + sono 168, non 24: e' l'unico conto onesto per un setaccio. + +PRIOR DICHIARATO PRIMA DI MISURARE: il soffitto direzionale BTC/ETH misurato e' Sharpe ~1.3 e +in 104 ipotesi + 153 agenti (giugno 2026) NESSUNA e' sopravvissuta alla verifica avversariale. +Mi aspetto 0 CANDIDATI. Il valore consegnato e' **quale gate uccide cosa**, perche' questo +restringe lo spazio della prossima ondata. + +LE 7 FAMIGLIE (24 celle ciascuna = 168 trial dichiarati) + RSKEW skew realizzata (da 1h) come PREDITTORE direzionale, non come gate + [nuovo vs blind/agent_40_skewgate, che la usava come FILTRO su un trend] + TACC accelerazione del trend (derivata seconda del log-prezzo) come direzione standalone + VPX relazione volume-prezzo su barre 1d (conferma / divergenza a volume alto e basso) + XDISP dispersione cross-sezionale dei 51 alt HL usata come segnale TIME-SERIES sul mercato + [nuovo vs XS01, dove la dispersione e' un gate sul paniere cross-sectional] + XCORR correlazione realizzata BTC-ETH come STATO di mercato direzionale + [nuovo vs ortho/agent_09 e XAS08, dove la corr gata un book relative-value] + XTAIL struttura dei ritorni dopo uno shock a k-sigma (continuazione vs inversione) + VRAT variance-ratio (persistenza) come CONVINZIONE continua sul TSMOM + [nuovo vs blind/agent_37_hurst, che faceva uno switch BINARIO di modo] + +NOTA CONFINE (perche' day_boundary_robust non gira su tutte): il test sposta le ETICHETTE del +calendario lasciando i prezzi fermi. Per un segnale di solo prezzo (TACC/VPX/XTAIL/VRAT) e' +esattamente il controllo giusto e deve dare INVARIANT. Per un segnale che fa merge_asof su una +serie AUSILIARIA (RSKEW da 1h, XDISP da HL, XCORR da 1h BTC/ETH) spostare l'etichetta in AVANTI +sposta il punto di merge nel FUTURO: sarebbe un LEAK indotto dallo strumento, non un controllo. +Per quelle famiglie il test d'ancora corretto e' la RICOSTRUZIONE delle barre a offset orario +(banda d'ancora, punto 5), che gira su tutte e sette. + +USO: nice -n 19 timeout 900 uv run python scripts/research/r0822_ortho_screen.py +""" +from __future__ import annotations + +import os +import sys +import time +from functools import lru_cache + +import numpy as np +import pandas as pd + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "alt")) +import altlib as A # noqa: E402 + +HL_DIR = A.DATA_DIR +ANCHOR_OFFSETS = (0, 3, 6, 9, 12, 15, 18, 21) +W_BLEND = 0.25 # peso del candidato nel blend con TP01 (convenzione altlib) + + +# =========================================================================== +# INFRASTRUTTURA — barre a offset, merge causale di serie ausiliarie +# =========================================================================== +@lru_cache(maxsize=64) +def bars_at(asset: str, off: int) -> pd.DataFrame: + """Barre 1d ancorate all'ora `off`, ricostruite dal feed 1h certificato. + off=0 deve riprodurre esattamente A.get(asset,'1d') (controllo in _sanity).""" + h = A.get(asset, "1h").copy() + idx = pd.to_datetime(h["timestamp"], unit="ms", utc=True) - pd.Timedelta(hours=off) + h.index = idx + o = (h.resample("1D", label="left", closed="left") + .agg({"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}) + .dropna(subset=["open"])) + o["datetime"] = o.index + pd.Timedelta(hours=off) + epoch = pd.Timestamp("1970-01-01", tz="UTC") + o["timestamp"] = ((o["datetime"] - epoch) // pd.Timedelta(milliseconds=1)).astype("int64") + return o.reset_index(drop=True)[["timestamp", "open", "high", "low", "close", "volume", "datetime"]] + + +def _bar_ms(df: pd.DataFrame) -> int: + d = pd.Series(df["timestamp"].astype("int64")).diff().median() + return int(d) if np.isfinite(d) and d > 0 else 86_400_000 + + +def merge_aux(df: pd.DataFrame, aux_ts: np.ndarray, aux_val: np.ndarray) -> np.ndarray: + """Valore di una serie ausiliaria NOTO alla CHIUSURA di ogni barra di df. + `aux_ts` e' gia' il timestamp in cui il valore diventa noto (fine della sua barra). + merge_asof backward sul close della barra -> mai un dato del futuro: eval_weights + tiene target[i] durante la barra i+1, quindi close[i] e' l'ultimo istante lecito.""" + close_ts = df["timestamp"].astype("int64").values + _bar_ms(df) + left = pd.DataFrame({"timestamp": close_ts}) + right = (pd.DataFrame({"timestamp": np.asarray(aux_ts, dtype="int64"), + "v": np.asarray(aux_val, dtype=float)}) + .dropna().sort_values("timestamp").reset_index(drop=True)) + if right.empty: + return np.full(len(df), np.nan) + return pd.merge_asof(left, right, on="timestamp", direction="backward")["v"].values + + +def roll_z(x: np.ndarray, win: int) -> np.ndarray: + s = pd.Series(np.asarray(x, float)) + mp = max(20, win // 4) + m = s.rolling(win, min_periods=mp).mean() + sd = s.rolling(win, min_periods=mp).std() + return ((s - m) / sd.where(sd > 0)).values + + +def exp_pct(x: np.ndarray, min_obs: int = 180) -> np.ndarray: + """Percentile ESPANDENTE causale (rank fra tutti i valori visti finora, incluso l'attuale).""" + return pd.Series(np.asarray(x, float)).expanding(min_periods=min_obs).rank(pct=True).values + + +def _shape(u: np.ndarray, shape: str) -> np.ndarray: + u = np.nan_to_num(np.asarray(u, float), nan=0.0) + return np.sign(u) if shape == "sign" else np.clip(u, -1.0, 1.0) + + +def _sized(direction: np.ndarray, df: pd.DataFrame) -> np.ndarray: + return A.vol_target(np.nan_to_num(direction, nan=0.0), df, + target_vol=0.20, vol_win_days=30, leverage_cap=2.0) + + +# =========================================================================== +# SERIE AUSILIARIE (calcolate una volta, riusate da tutte le celle) +# =========================================================================== +@lru_cache(maxsize=16) +def skew_aux(asset: str, w_days: int): + """Skew realizzata dei ritorni 1h su finestra trascinata di w_days giorni. + Valore al bar 1h i -> noto alla sua chiusura (ts_i + 1h).""" + h = A.get(asset, "1h") + r = A.log_returns(h["close"].values.astype(float)) + win = w_days * 24 + sk = pd.Series(r).rolling(win, min_periods=win // 2).skew().values + return h["timestamp"].astype("int64").values + 3_600_000, sk + + +@lru_cache(maxsize=4) +def disp_aux(): + """Dispersione cross-sezionale dei 51 alt Hyperliquid (1d, 2024+): deviazione standard + cross-section dei log-ritorni giornalieri. Valore del giorno d noto alla chiusura di d.""" + import glob + files = sorted(glob.glob(str(HL_DIR / "hl_*_1d.parquet"))) + cols = {} + for f in files: + x = pd.read_parquet(f, columns=["timestamp", "close"]) + s = pd.Series(x["close"].values.astype(float), + index=x["timestamp"].astype("int64").values) + cols[os.path.basename(f)] = np.log(s).diff() + M = pd.DataFrame(cols).sort_index() + n_ok = M.notna().sum(axis=1) + disp = M.std(axis=1, ddof=1).where(n_ok >= 15) + ts = M.index.values.astype("int64") + 86_400_000 # noto a fine giornata + return ts, disp.values, int(len(files)) + + +@lru_cache(maxsize=16) +def xcorr_aux(w_days: int): + """Correlazione realizzata BTC-ETH sui ritorni 1h, finestra trascinata w_days giorni.""" + b = A.get("BTC", "1h")[["timestamp", "close"]].rename(columns={"close": "b"}) + e = A.get("ETH", "1h")[["timestamp", "close"]].rename(columns={"close": "e"}) + J = pd.merge(b, e, on="timestamp", how="inner").sort_values("timestamp") + rb = pd.Series(A.log_returns(J["b"].values.astype(float))) + re = pd.Series(A.log_returns(J["e"].values.astype(float))) + win = w_days * 24 + c = rb.rolling(win, min_periods=win // 2).corr(re).values + return J["timestamp"].astype("int64").values + 3_600_000, c + + +# =========================================================================== +# LE 7 FAMIGLIE — factory(tf=..., **params) -> target(df, asset) +# =========================================================================== +def mk_rskew(tf="1d", w_days=30, z_days=365, shape="lin", sgn=-1): + """RSKEW: la skew realizzata recente come PREDITTORE del ritorno successivo. + A-priori (Amaya et al., azioni): skew realizzata alta -> ritorno futuro BASSO (sgn=-1).""" + def target(df, asset): + ts, sk = skew_aux(asset, w_days) + v = merge_aux(df, ts, sk) + u = np.clip(roll_z(v, z_days) / 2.0, -1.0, 1.0) + return _sized(sgn * _shape(u, shape), df) + return target + + +def mk_tacc(tf="1d", H=60, L=20, shape="lin", sgn=1): + """TACC: accelerazione = variazione della PENDENZA log-prezzo su H barre, misurata a L barre + di distanza. Direzione standalone (non e' il LIVELLO del trend). A-priori sgn=+1.""" + def target(df, asset): + c = np.log(df["close"].values.astype(float)) + n = len(c) + slope = np.full(n, np.nan) + slope[H:] = (c[H:] - c[:-H]) / H + acc = np.full(n, np.nan) + acc[L:] = slope[L:] - slope[:-L] + u = np.clip(roll_z(acc, 365) / 2.0, -1.0, 1.0) + return _sized(sgn * _shape(u, shape), df) + return target + + +def mk_vpx(tf="1d", W=30, k=5, mode="confirm", thr=0.5): + """VPX: volume-prezzo su barre 1d (a 1d il volume Deribit e' informativo, a 5m no). + confirm -> segui il movimento SOLO se il volume conferma (z >= thr) + fade_low -> fai il contrario del movimento avvenuto a volume BASSO (z <= -thr) + fade_high -> fai il contrario del movimento avvenuto a volume ALTO (climax, z >= thr)""" + def target(df, asset): + c = df["close"].values.astype(float) + vol = df["volume"].values.astype(float) + n = len(c) + vz = roll_z(np.log(np.where(vol > 0, vol, np.nan)), W) + rk = np.full(n, np.nan) + rk[k:] = c[k:] / c[:-k] - 1.0 + s = np.sign(np.nan_to_num(rk, nan=0.0)) + vzf = np.nan_to_num(vz, nan=0.0) + if mode == "confirm": + d = np.where(vzf >= thr, s, 0.0) + elif mode == "fade_low": + d = np.where(vzf <= -thr, -s, 0.0) + else: # fade_high + d = np.where(vzf >= thr, -s, 0.0) + return _sized(d, df) + return target + + +def mk_xdisp(tf="1d", S=5, trans="z252", shape="lin", sgn=1): + """XDISP: dispersione cross-sezionale dei 51 alt HL come STATO del mercato (time-series), + non come gate di un paniere cross-sectional. Nessun prior forte sul segno -> entrambi in griglia.""" + def target(df, asset): + ts, dsp, _ = disp_aux() + sm = pd.Series(dsp).rolling(S, min_periods=1).mean().values + u = (np.clip(roll_z(sm, 252) / 2.0, -1, 1) if trans == "z252" + else 2.0 * exp_pct(sm, 180) - 1.0) + v = merge_aux(df, ts, u) + return _sized(sgn * _shape(v, shape), df) + return target + + +def mk_xcorr(tf="1d", W=60, trans="z252", shape="lin", sgn=-1): + """XCORR: co-movimento BTC-ETH (corr realizzata) come stato direzionale del mercato. + A-priori debole: corr alta = 'tutto il crypto e' un trade solo' = regime tardo -> sgn=-1.""" + def target(df, asset): + ts, cr = xcorr_aux(W) + u_aux = (np.clip(roll_z(cr, 252 * 24) / 2.0, -1, 1) if trans == "z252" + else 2.0 * exp_pct(cr, 180 * 24) - 1.0) + v = merge_aux(df, ts, u_aux) + return _sized(sgn * _shape(v, shape), df) + return target + + +def mk_xtail(tf="1d", ksig=3.0, N=3, sgn=1, sigwin=30): + """XTAIL: struttura dei ritorni ESTREMI. Dopo |r| > ksig*sigma trascurata, tieni per N barre + la direzione (sgn=+1 continuazione) o il suo opposto (sgn=-1 inversione). + ATTENZIONE dichiarata: sgn=-1 e' mean-reversion, famiglia gia' MORTA -> se vince li', si chiude.""" + def target(df, asset): + c = df["close"].values.astype(float) + r = A.simple_returns(c) + sd = pd.Series(r).rolling(sigwin, min_periods=sigwin // 2).std().shift(1).values + n = len(c) + d = np.zeros(n) + hit = np.where(np.isfinite(sd) & (sd > 0) & (np.abs(r) > ksig * sd))[0] + for i in hit: + d[i:min(n, i + N)] = sgn * np.sign(r[i]) + return _sized(d, df) + return target + + +def mk_vrat(tf="1d", q=10, LB=365, conv="gate", lf=True): + """VRAT: variance-ratio VR(q)=Var(r_q)/(q*Var(r_1)) su finestra LB come CONVINZIONE CONTINUA + sul TSMOM (non uno switch binario di modo come blind/agent_37_hurst). + gate -> pesa il trend solo dove VR>1 (persistente), altrimenti flat + flip -> inverte il trend dove VR<1 (anti-persistente)""" + HZ = (30, 90, 180) + + def target(df, asset): + c = df["close"].values.astype(float) + lc = np.log(c) + n = len(c) + r1 = pd.Series(A.log_returns(c)) + rq = pd.Series(np.concatenate([np.full(q, np.nan), lc[q:] - lc[:-q]])) + v1 = r1.rolling(LB, min_periods=LB // 2).var() + vq = rq.rolling(LB, min_periods=LB // 2).var() + vr = (vq / (q * v1.where(v1 > 0))).values + dirs = np.zeros(n) + for H in HZ: + m = np.zeros(n) + m[H:] = np.sign(c[H:] / c[:-H] - 1.0) + dirs += m / len(HZ) + if lf: + dirs = np.maximum(dirs, 0.0) + k = np.nan_to_num(vr, nan=1.0) - 1.0 + w = np.clip(k * 5.0, 0.0, 1.0) if conv == "gate" else np.clip(k * 5.0, -1.0, 1.0) + return _sized(dirs * w, df) + return target + + +# ---- griglie DICHIARATE (24 celle ciascuna, 7 famiglie = 168 trial) ---------- +def _grid(**axes): + keys = list(axes) + out = [{}] + for k in keys: + out = [dict(o, **{k: v}) for o in out for v in axes[k]] + return out + + +FAMILIES = [ + dict(code="RSKEW", factory=mk_rskew, price_only=False, + grid=_grid(w_days=[14, 30, 60], z_days=[180, 365], shape=["lin", "sign"], sgn=[1, -1])), + dict(code="TACC", factory=mk_tacc, price_only=True, + grid=_grid(H=[20, 60, 120], L=[5, 20], shape=["lin", "sign"], sgn=[1, -1])), + dict(code="VPX", factory=mk_vpx, price_only=True, + grid=_grid(W=[30, 90], k=[5, 20], mode=["confirm", "fade_low", "fade_high"], thr=[0.5, 1.0])), + dict(code="XDISP", factory=mk_xdisp, price_only=False, + grid=_grid(S=[1, 5, 20], trans=["z252", "pct"], shape=["lin", "sign"], sgn=[1, -1])), + dict(code="XCORR", factory=mk_xcorr, price_only=False, + grid=_grid(W=[20, 60, 120], trans=["z252", "pct"], shape=["lin", "sign"], sgn=[1, -1])), + dict(code="XTAIL", factory=mk_xtail, price_only=True, + grid=_grid(ksig=[2.5, 3.0], N=[1, 3, 10], sgn=[1, -1], sigwin=[30, 90])), + dict(code="VRAT", factory=mk_vrat, price_only=True, + grid=_grid(q=[5, 10, 20], LB=[180, 365], conv=["gate", "flip"], lf=[True, False])), +] + + +# =========================================================================== +# GATE AGGIUNTIVI +# =========================================================================== +def cand_daily_at(fn, off: int) -> pd.Series: + """Serie giornaliera 50/50 BTC+ETH del candidato con barre ancorate all'ora `off`.""" + ser = {} + for a in A.CERTIFIED: + df = bars_at(a, off) + ev = A.eval_weights(df, A._call_target(fn, df, a)) + ser[a] = pd.Series(ev["net"], index=ev["idx"]) + J = pd.concat(ser, axis=1, join="inner").fillna(0.0) + return A._to_daily(0.5 * J[A.CERTIFIED[0]] + 0.5 * J[A.CERTIFIED[1]]) + + +def anchor_report(fn) -> dict: + """Banda d'ancora su 8 offset orari: Sharpe standalone E uplift del blend. + La stima ONESTA e' la MEDIANA della banda, non l'ancora 00:00 (lezione 02/07).""" + band = A.anchor_luck_band(lambda o: cand_daily_at(fn, o), ANCHOR_OFFSETS) + B = A.tp01_baseline_daily() + ups = {} + for o in ANCHOR_OFFSETS: + C = cand_daily_at(fn, o) + J = pd.concat({"B": B, "C": C}, axis=1, join="inner").dropna() + if len(J) > 30: + ups[o] = round(A._sh((1 - W_BLEND) * J["B"] + W_BLEND * J["C"]) - A._sh(J["B"]), 3) + vals = np.array(list(ups.values()), float) if ups else np.array([np.nan]) + band["uplift_per_offset"] = ups + band["uplift_canonical"] = ups.get(0) + band["uplift_median"] = round(float(np.median(vals)), 3) + band["uplift_frac_positive"] = round(float((vals > 0).mean()), 3) + return band + + +def delever_null(C: pd.Series, w: float = W_BLEND) -> dict: + """NULL DEL DE-LEVERING (obbligatorio su ogni claim di meno drawdown, 5 occorrenze nel + progetto): esiste k<1 che, applicato al SOLO TP01, da' lo STESSO maxDD con Sharpe MIGLIORE? + Se si', l'overlay non serve: bastava meno leva.""" + B = A.tp01_baseline_daily() + J = pd.concat({"B": B, "C": C}, axis=1, join="inner").dropna() + if len(J) < 60: + return dict(run=False, reason="overlap insufficiente") + blend = (1 - w) * J["B"] + w * J["C"] + dd_bl, sh_bl = A._dd_ret(blend), A._sh(blend) + dd_b, sh_b = A._dd_ret(J["B"]), A._sh(J["B"]) + if dd_bl >= dd_b: + return dict(run=True, claims_less_dd=False, dd_blend=round(dd_bl, 4), + dd_tp01=round(dd_b, 4), sharpe_blend=round(sh_bl, 3), + sharpe_tp01=round(sh_b, 3), + note="nessun claim di DD: il blend NON riduce il drawdown -> null non pertinente") + ks = np.linspace(0.05, 1.0, 96) + dds = np.array([A._dd_ret(k * J["B"]) for k in ks]) + k = float(ks[int(np.argmin(np.abs(dds - dd_bl)))]) + sh_k = A._sh(k * J["B"]) + return dict(run=True, claims_less_dd=True, dd_blend=round(dd_bl, 4), dd_tp01=round(dd_b, 4), + sharpe_blend=round(sh_bl, 3), k_equal_dd=round(k, 3), + dd_k=round(A._dd_ret(k * J["B"]), 4), sharpe_k_tp01=round(sh_k, 3), + passes=bool(sh_bl > sh_k), + note=("il blend batte il de-levering" if sh_bl > sh_k + else "REFUTED: k*TP01 da' lo stesso DD con Sharpe migliore")) + + +def smallcap_report(fn) -> dict: + out = {} + for a in A.CERTIFIED: + df = A.get(a, "1d") + sc = A.eval_weights_smallcap(df, A._call_target(fn, df, a), capital=600.0, min_order=5.0) + out[a] = dict(modeled=sc["modeled"]["sharpe"], realistic=sc["realistic"]["sharpe"], + haircut=sc["sharpe_haircut"], n_trades=sc["n_executed_trades"]) + return out + + +def active_window_metrics(C: pd.Series) -> dict: + """Trappola #9 (barre attive vs calendario): una serie che e' 0 per meta' della storia + (XDISP parte nel 2024) ha Sharpe DILUITO di sqrt(frazione attiva). Si riporta anche il + numero sulla sola finestra ATTIVA, senza usarlo per decidere.""" + nz = C[C != 0.0] + if len(nz) < 30: + return dict(active_days=int(len(nz))) + first = nz.index[0] + sub = C[C.index >= first] + return dict(active_days=int(len(nz)), frac_active=round(float(len(nz) / max(1, len(C))), 3), + first_active=str(first.date()), sharpe_active_window=round(A._sh(sub), 3), + maxdd_active_window=round(A._dd_ret(sub), 4)) + + +# =========================================================================== +# SANITY — un setaccio che non si controlla non sta setacciando niente +# =========================================================================== +def sanity() -> None: + print("### SANITY") + for a in A.CERTIFIED: + b0, g = bars_at(a, 0), A.get(a, "1d") + n = min(len(b0), len(g)) + d = float(np.max(np.abs(b0["close"].values[:n] - g["close"].values[:n]))) + dts = float(np.max(np.abs(b0["timestamp"].values[:n] - g["timestamp"].values[:n]))) + print(f" bars_at({a},0) vs A.get({a},'1d'): n={n}/{len(g)} max|dclose|={d:.6g} max|dts|={dts:.0f}") + assert d < 1e-9 and dts == 0, "ricostruzione barre a offset 0 NON identica al feed certificato" + ts, dsp, nfiles = disp_aux() + ok = np.isfinite(dsp) + print(f" disp_aux: {nfiles} file HL, {int(ok.sum())} giorni con >=15 asset, " + f"da {pd.Timestamp(ts[ok][0], unit='ms', tz='UTC').date()} " + f"a {pd.Timestamp(ts[ok][-1], unit='ms', tz='UTC').date()}") + B = A.tp01_baseline_daily() + print(f" TP01 baseline: n={len(B)} ShFULL={A._sh(B):.3f} ShHOLD={A._sh(B[B.index >= A.HOLDOUT]):.3f}") + print() + + +# =========================================================================== +# MAIN +# =========================================================================== +def main() -> None: + t0 = time.time() + print(__doc__.split("USO:")[0]) + sanity() + + pooled_sh: list[float] = [] + results = [] + + for fam in FAMILIES: + code, fac, grid = fam["code"], fam["factory"], fam["grid"] + t1 = time.time() + print(f"### {code} — {len(grid)} celle dichiarate") + sel = A.select_cell_insample(fac, grid, ("1d",)) + pooled_sh.extend([s for s in sel["all_full_sharpe"] if np.isfinite(s)]) + rep = A.study_family_honest(code, fac, grid, ("1d",)) + ch = rep["chosen"] + if ch is None: + print(f" nessuna cella valida ({rep.get('reason')})\n") + results.append(dict(code=code, n_cells=len(grid), chosen=None, + killed_by="nessuna cella valutabile")) + continue + + fn = fac(tf=ch["tf"], **ch["params"]) + C = A.candidate_daily(fn, tf=ch["tf"]) + m = rep["marginal"]["marginal"] + absr = rep["marginal"]["absolute"]["cells"][0] + + caus = A.causality_ok(fn, tf="1d") + dayb = A.day_boundary_robust(fn, tf="1d") if fam["price_only"] else \ + dict(verdict="NON GIRATO", reason="segnale con merge su serie ausiliaria: " + "spostare l'etichetta in avanti sposta il merge nel FUTURO (leak indotto), " + "non e' un controllo -> vale la banda d'ancora") + anch = anchor_report(fn) + deln = delever_null(C) + smal = smallcap_report(fn) + impl = A.implausible_sharpe(C) + actw = active_window_metrics(C) + + full_sh = A._sh(C) + hold_sh = A._sh(C[C.index >= A.HOLDOUT]) + mm = A._metrics_from_net(C.values, C.index) + + results.append(dict( + code=code, n_cells=len(grid), chosen=ch, fn=fn, C=C, + full_sharpe=round(full_sh, 3), hold_sharpe=round(hold_sh, 3), + maxdd=mm["maxdd"], cagr=mm["cagr"], + corr=m.get("corr_full"), corr_hold=m.get("corr_hold"), + verdict=rep["marginal"]["marginal_verdict"], + earns_slot=rep["earns_slot_marginal"], honest=rep["earns_slot_honest"], + dsr_family=rep["deflated_sharpe"], dsr_pass=rep["dsr_pass"], + insample_sharpe=m.get("cand_insample_sharpe"), + has_insample_edge=m.get("has_insample_edge"), + robust_oos=m.get("robust_oos"), is_hedge=m.get("is_hedge"), + uplift_full=m["blends"]["w25"]["uplift_full"], + uplift_hold=m["blends"]["w25"]["uplift_hold"], + multicut=m.get("multicut_uplift"), abs_grade=rep["marginal"]["abs_grade"], _m=m, + fee_survives=absr.get("fee_survives"), + causality=caus, dayb=dayb, anchor=anch, delever=deln, + smallcap=smal, implausible=impl, active=actw, + )) + + print(f" cella IN-SAMPLE-ONLY: {ch['params']} (IS Sh {ch['insample_sharpe']}, " + f"rango 1/{len(sel['rows'])} in-sample; FULL Sh di quella cella {ch['full_sharpe']})") + print(f" standalone FULL {full_sh:+.3f} | HOLD {hold_sh:+.3f} | maxDD {mm['maxdd']:.1%} | " + f"CAGR {mm['cagr']:+.2%} | IS {m.get('cand_insample_sharpe')}") + print(f" marginale {rep['marginal']['marginal_verdict']:<9} corr->TP01 {m.get('corr_full')} " + f"(hold {m.get('corr_hold')}) uplift w25 full {m['blends']['w25']['uplift_full']:+.3f} " + f"hold {m['blends']['w25']['uplift_hold']}") + print(f" robust_oos={m.get('robust_oos')} insample_edge={m.get('has_insample_edge')} " + f"is_hedge={m.get('is_hedge')} abs={rep['marginal']['abs_grade']} " + f"fee_survives={absr.get('fee_survives')}") + print(f" DSR famiglia {rep['deflated_sharpe']} (atteso null max {rep['expected_null_max']}) " + f"pass={rep['dsr_pass']} earns_slot_honest={rep['earns_slot_honest']}") + print(f" causality ok={caus['ok']} max_tail_diff={caus['max_tail_diff']}") + print(f" confine {dayb.get('verdict')} " + + (f"spread {dayb.get('spread')} per_offset {dayb.get('per_offset')}" + if fam["price_only"] else f"({dayb.get('reason','')[:60]}...)")) + _nan = float("nan") + print(f" ancora Sh canonica {anch.get('canonical', _nan):+.3f} " + f"(pctl {anch.get('canonical_pctl')}) " + f"MEDIANA {anch.get('median', _nan):+.3f} " + f"banda [{anch.get('lo', _nan):+.3f},{anch.get('hi', _nan):+.3f}] " + f"frac>0 {anch.get('frac_positive')}") + print(f" uplift canonico {anch.get('uplift_canonical')} MEDIANA " + f"{anch.get('uplift_median')} frac>0 {anch.get('uplift_frac_positive')}") + print(f" de-levering {deln.get('note')}") + print(f" $600 " + " ".join( + f"{a}: mod {v['modeled']:+.2f} -> real {v['realistic']:+.2f} (haircut {v['haircut']:+.2f}, " + f"{v['n_trades']} trade)" for a, v in smal.items())) + print(f" implausible {impl.get('implausible')} {impl.get('reasons')}") + if actw.get("frac_active", 1.0) < 0.9: + print(f" attive {actw}") + print(f" [{time.time() - t1:.1f}s]\n") + + # ---- DSR DI SCREEN: i trial sono TUTTE le celle di TUTTE le famiglie ------------- + print("### DSR DI SCREEN (il conto onesto per un setaccio: scelgo il meglio fra " + f"{len(pooled_sh)} celle, non fra 24)") + for r in results: + if r.get("chosen") is None: + continue + d, s0 = A.deflated_sharpe(r["full_sharpe"], pooled_sh, r["C"]) + r["dsr_screen"] = round(d, 3) if np.isfinite(d) else None + print(f" {r['code']:<6} Sh {r['full_sharpe']:+.3f} DSR famiglia {r['dsr_family']} " + f"DSR screen {r['dsr_screen']} (null max atteso {s0:.3f})") + print() + + deep_dive(results) + + # ---- TABELLA FINALE -------------------------------------------------------------- + def killer(r) -> str: + if r.get("chosen") is None: + return "nessuna cella valutabile" + why = [] + if not r["causality"]["ok"]: + why.append("CAUSALITY") + if r["verdict"] != "ADDS": + why.append(f"marginal={r['verdict']}") + if not r["has_insample_edge"]: + why.append("no in-sample edge (<0.5)") + if not r["robust_oos"]: + why.append("robust_oos") + if r["is_hedge"]: + why.append("is_hedge") + if not r["dsr_pass"]: + why.append(f"DSR fam {r['dsr_family']}") + if r.get("dsr_screen") is not None and r["dsr_screen"] < 0.95: + why.append(f"DSR screen {r['dsr_screen']}") + if r["dayb"].get("verdict") == "ARTIFACT-RISK": + why.append("confine ARTIFACT-RISK") + um = r["anchor"].get("uplift_median") + if um is not None and np.isfinite(um) and um <= 0: + why.append("ancora: uplift mediano <=0") + if r["delever"].get("claims_less_dd") and not r["delever"].get("passes"): + why.append("null de-levering") + if r["implausible"].get("implausible"): + why.append("implausible_sharpe") + return " + ".join(why) if why else "-- nessun gate lo uccide --" + + print("### TABELLA — ORTHO-SCREEN 2026-08-22 (candidato | trial | ShFULL | hold-out | " + "corr->TP01 | marginale | DSR fam / screen | esito)") + hdr = (f"{'cand':<7}{'trial':>6}{'ShFULL':>8}{'hold':>7}{'maxDD':>8}{'CAGR':>8}" + f"{'corr':>7} {'marginale':<10}{'DSRfam':>7}{'DSRscr':>8} esito / gate che l'ha ucciso") + print(hdr); print("-" * len(hdr)) + for r in results: + if r.get("chosen") is None: + print(f"{r['code']:<7}{r['n_cells']:>6}{'--':>8}{'--':>7}{'--':>8}{'--':>8}{'--':>7} " + f"{'--':<10}{'--':>7}{'--':>8} SCARTATO — {killer(r)}") + continue + ok = (r["verdict"] == "ADDS" and r["dsr_pass"] and (r.get("dsr_screen") or 0) >= 0.95 + and r["causality"]["ok"]) + esito = "CANDIDATO" if ok else "SCARTATO" + print(f"{r['code']:<7}{r['n_cells']:>6}{r['full_sharpe']:>+8.3f}{r['hold_sharpe']:>+7.2f}" + f"{r['maxdd']:>8.1%}{r['cagr']:>+8.1%}{r['corr']:>7.2f} {r['verdict']:<10}" + f"{str(r['dsr_family']):>7}{str(r.get('dsr_screen')):>8} {esito} — {killer(r)}") + print() + print("### FAMIGLIE NON TESTATE (l'elenco di cio' che non ho guardato fa parte del risultato)") + for line in NOT_TESTED: + print(" - " + line) + print(f"\n[totale {time.time() - t0:.1f}s]") + + +def mk_mom(tf="1d", H=5, shape="lin"): + """CONTROLLO per TACC: il LIVELLO del momentum a orizzonte H, stessa pipeline + (z-score 365g -> clip -> vol_target). Serve a rispondere alla domanda della famiglia: + l'ACCELERAZIONE aggiunge qualcosa al LIVELLO, o e' un momentum corto travestito?""" + def target(df, asset): + c = df["close"].values.astype(float) + n = len(c) + m = np.full(n, np.nan) + m[H:] = c[H:] / c[:-H] - 1.0 + u = np.clip(roll_z(m, 365) / 2.0, -1.0, 1.0) + return _sized(_shape(u, shape), df) + return target + + +MOM_CONTROL_GRID = _grid(H=[5, 20, 60], shape=["lin", "sign"]) # 6 celle di CONTROLLO + + +def deep_dive(results) -> None: + """APPROFONDIMENTO sull'unica famiglia arrivata a marginal=ADDS. + Due domande, entrambe misurate: + (a) QUALE sotto-gate di robust_oos ha fallito (clean-year? jackknife? multi-cut?); + (b) l'accelerazione e' distinta dal LIVELLO del momentum corto, o e' quello travestito? + -> 6 celle di CONTROLLO dichiarate (H x shape), che si SOMMANO ai 168 trial.""" + adds = [r for r in results if r.get("chosen") is not None and r["verdict"] == "ADDS"] + if not adds: + print("### APPROFONDIMENTO — nessuna famiglia a marginal=ADDS, niente da approfondire\n") + return + r = max(adds, key=lambda x: x["uplift_hold"] or -9) + print(f"### APPROFONDIMENTO — {r['code']} (l'unica a marginal=ADDS)") + print(f" perche' robust_oos=False: clean_year_uplift={r['_m'].get('clean_year_uplift')} " + f"(serve >0.02) jackknife_min_uplift={r['_m'].get('jackknife_min_uplift')} (serve >0)") + print(f" multicut (uplift a ogni taglio d'anno): {r['multicut']}") + print(f" uplift TP01-up {r['_m'].get('uplift_tp01_up')} / TP01-down {r['_m'].get('uplift_tp01_down')} " + f"| hedge_yearly_corr {r['_m'].get('hedge_yearly_corr')} | alpha/anno {r['_m'].get('alpha_ann')}") + + print(" --- accelerazione vs LIVELLO del momentum (6 celle di controllo) ---") + B = A.tp01_baseline_daily() + C = r["C"] + best = None + for p in MOM_CONTROL_GRID: + fnm = mk_mom(tf="1d", **p) + Cm = A.candidate_daily(fnm, tf="1d") + J = pd.concat({"A": C, "M": Cm}, axis=1, join="inner").dropna() + JB = pd.concat({"B": B, "M": Cm}, axis=1, join="inner").dropna() + up = A._sh((1 - W_BLEND) * JB["B"] + W_BLEND * JB["M"]) - A._sh(JB["B"]) + row = dict(params=p, sharpe=round(A._sh(Cm), 3), + hold=round(A._sh(Cm[Cm.index >= A.HOLDOUT]), 3), + corr_to_acc=round(float(J["A"].corr(J["M"])), 3), + corr_to_tp01=round(float(JB["B"].corr(JB["M"])), 3), + uplift_w25_full=round(up, 3)) + print(f" MOM {str(p):<28} Sh {row['sharpe']:+.3f} hold {row['hold']:+.3f} " + f"corr->{r['code']} {row['corr_to_acc']:+.3f} corr->TP01 {row['corr_to_tp01']:+.3f} " + f"uplift {row['uplift_w25_full']:+.3f}") + if best is None or row["corr_to_acc"] > best["corr_to_acc"]: + best = row + print(f" livello piu' vicino all'accelerazione: {best['params']} corr {best['corr_to_acc']:+.3f}") + print(" lettura: corr alta col LIVELLO => l'accelerazione non e' una dimensione nuova; " + "corr bassa => e' distinta ma resta da spiegare perche' non sopravvive al DSR.\n") + + +NOT_TESTED = [ + "catena opzioni / vol term-structure: 3-5 mesi di storia -> non esiste hold-out, al massimo " + "un LEAD; e altri agenti dell'ondata ci stanno sopra.", + "funding (carry, time-series, cross-sectional): filone dichiarato CHIUSO su 3 lati.", + "calendario in ogni forma (weekend 375 trial, expiry, stagionalita', event-clock, " + "ora-del-giorno): famiglia dichiarata SATURA.", + "mean-reversion pura / fade: morta anche a fee zero. Qui rientra SOLO come branch dichiarato " + "di XTAIL e VPX, per misurarla dentro un conditioner nuovo.", + "microstruttura 5m/15m: morta per fee, e una sweep su 5m sfonda il budget macchina.", + "cross-sectional sui 51 alt HL: territorio XS01, e l'espansione d'universo e' gia' refutata. " + "Qui i 51 alt entrano solo come AGGREGATO time-series (XDISP).", + "equity / GTAA / MAT01: GTAA01 non e' deployabile (PRIIPs) e MAT01 e' refutato.", + "macro regime gate: ridondante col trend (corr 0.989).", + "CRT / ICT / Elliott / Fibonacci / Albimarini: 3 ondate di refutazione.", + "gamma scalping long-vol e overlay DD su VRP01 (5/5 refutati).", + "SOL come terza gamba: decisione dell'operatore del 22/08.", +] + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_prop_alloc.py b/scripts/research/r0822_prop_alloc.py new file mode 100644 index 0000000..7522441 --- /dev/null +++ b/scripts/research/r0822_prop_alloc.py @@ -0,0 +1,712 @@ +"""r0822_prop_alloc — PROGETTARE l'allocazione per la BARRIERA, non per lo Sharpe (2026-08-22). + +IL BUCO CHE CHIUDE +------------------ +Il canale prop/funded e' l'unico misurato che moltiplica il nozionale senza possedere capitale. +Il progetto lo ha gia' VALUTATO (24-25/07: `r0725_prop_ladder.py`, `r0725_prop_coupled.py`, +`r0725_hyro.py`) ma non lo ha mai **PROGETTATO**: ha sempre preso il libro ottimizzato per lo +Sharpe (config A = live 75/25, config B = 55/20/25 scelta a mano) e poi MISURATO P(pass) del +risultato. Due punti nello spazio dei pesi, tre leve, nessuna ottimizzazione, nessun null. + +Qui la funzione obiettivo cambia: sotto una **barriera di maxDD STATICA per-conto** conta la coda +sinistra e la DURATA, non il rapporto rendimento/vol. Si ottimizza + + J(w, k) = P(passare l'eval) x P(essere vivo dopo 12 mesi da funded) + +su pesi w e leva k, con la lente WICK ACCOPPIATA (la `close-only` e' CIECA: 0% di breach da +daily-loss su ogni configurazione, la regola non scatta mai sulle chiusure). + +LA PREVISIONE, REGISTRATA PRIMA DI MISURARE +------------------------------------------- +Nel limite gaussiano con leva LIBERA l'obiettivo si riduce allo Sharpe, e si vede in due righe. +Per un moto browniano con drift mu e vol sigma a leva k, la probabilita' di toccare +T prima di +-D vale P = (1-e^{-theta*D})/(1-e^{-theta*(T+D)}) con **theta = 2*mu/(k*sigma^2)**: la leva entra +SOLO dividendo theta. Alla leva minima che porta a +T dentro l'orizzonte (k ~ T/mu) si ottiene +theta ~ 2*mu^2/(T*sigma^2) = 2*S^2/T, con S = Sharpe: **la vol sparisce e resta lo Sharpe al +quadrato**. Quindi mi aspetto PRIMA di misurare: + (a) l'argmax di J vicino all'argmax di Sharpe -> la premessa "obiettivo diverso" e' in gran + parte FALSA sui PESI; + (b) il contenuto vero nella **LEVA**, che lo Sharpe non determina affatto, e nella deviazione + dal gaussiano (skew, wick, daily-loss: una soglia ASSOLUTA non e' scale-invariante); + (c) leva ottima per J **piu' bassa** di quella ottima per il payout (0.75x, 25/07): J e' una + probabilita' di sopravvivenza, il payout e' un flusso. +Se (a) risulta falsa, e' un risultato; se risulta vera, il filone "riallocare per la barriera" +si chiude e si smette di pagarlo. + +CONVENZIONI DICHIARATE +---------------------- +* Macchineria del wick ACCOPPIATO **riusata** da `r0725_prop_coupled.py` (import, non riscritta): + `_hourly_legs` (recon MTM orario TP01/SKH01), `crypto_daily_tuples` (minimo ESATTO sul path + orario condiviso), `xsec_daily_tuples` (XS01 dagli OHLC 1d, ordine condiviso avverso). +* VRP01 e GTAA01 sono AGGIUNTI qui (non c'erano): GTAA01 col wick dagli OHLC dei 6 ETF (long-only + co-moventi -> ogni gamba al proprio minimo = convenzione severa dichiarata); VRP01 con + **gap = 0**, cioe' CIECO, perche' il suo mark infra-settimana non e' nel dataset dello sleeve + (il rendimento settimanale e' lumpato sul giorno di scadenza). E' una lente GENEROSA per VRP01: + la misura del 30/07 sulle quote reali dice mark peggiore infra-settimana mediano -6.9% e minimo + **-81.7%** del capitale su un trade finito in UTILE. Se VRP01 perde sotto una lente generosa, + perde. +* de-luck sul DRIFT **x0.89** (fattore MISURATO il 26/07 sul libro live, `r0726_deluck_factor.py`), + non x0.6 come nel 25/07 (li' era stimato a occhio e ri-misurato troppo severo del 31-34%). + Sensibilita' a 1.00 e 0.60 girata: cio' che conta e' se l'ORDINE fra le configurazioni tiene. +* bootstrap a blocchi (20g) con **indici CONDIVISI fra tutte le configurazioni** -> le differenze + fra configurazioni sono APPAIATE e il loro errore MC e' molto minore di quello dei livelli. +* finestra comune **2024+** (XS01 nasce li'): ~2.6 anni, ed e' la finestra in cui XS01 e' stato + scoperto -> le configurazioni che lo contengono sono FAVORITE per costruzione. Dichiarato. +* fisco 33% sui payout, split 80%, regole vere delle due firm (max-loss STATICO dal saldo + iniziale, base ripristinata dopo il prelievo — bug del 25/07 gia' corretto a monte). + +Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0822_prop_alloc.py` +""" +from __future__ import annotations + +import sys +import time +from functools import lru_cache +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +from src.data.eq_splits import repair_splits # noqa: E402 +from src.portfolio.gtaa import ( # noqa: E402 + ANN, EQ_UNIVERSE, GTAA_DEFAULT_CAPITAL, HORIZONS, IB_MIN_TRADE_USD, REBAL_BAND_USD, + REBAL_EVERY, TARGET_VOL, ib_commission, +) +from src.portfolio.portfolio import metrics, weights_tilt_null # noqa: E402 +from src.portfolio.sleeves import ( # noqa: E402 + GTAA_BOOK_ACTIVATION, _gtaa_daily_returns, _skyhook_returns, _tp01_returns, + _vrp_combo_returns, _xsec_returns, +) + +import r0725_prop_coupled as pc # noqa: E402 +import r0725_prop_ladder as pl # noqa: E402 +from r0725_capcurve import EURUSD, TAX_RATE # noqa: E402 + +RAW = ROOT / "data" / "raw" + +# ------------------------------------------------------------------ costanti dichiarate +DELUCK = 0.89 # fattore d'ancora MISURATO sul drift (26/07), non stimato a occhio +START_WIN = "2024-01-01" # finestra comune ai 5 sleeve (XS01 nasce qui) +BLOCK = 20 # blocco del bootstrap, in giorni +N_PATHS = 4000 +SPLIT = pl.SPLIT # 80% al trader +TARGET_EUR_DAY = 50.0 +FIRMS = {f["name"]: f for f in pl.FIRMS} +SLEEVES = ("TP01", "SKH01", "XS01", "VRP01", "GTAA01") +CRYPTO_PROP = ("TP01", "SKH01", "XS01") # cio' che una firm CRYPTO puo' davvero far eseguire + + +# ================================================================== §0. PANNELLO (R, wick) + +@lru_cache(maxsize=8) +def _eq_ohlc(sym: str) -> pd.DataFrame: + p = RAW / f"eq_{sym.lower()}_1d.parquet" + d = pd.read_parquet(p) + d.index = pd.to_datetime(d["timestamp"], unit="ms", utc=True) + d = d.sort_index() + d, _ = repair_splits(d) # split IWM/EFA non aggiustati (25/07) + return d[["open", "high", "low", "close"]].astype(float) + + +def _gtaa_exposure(close: pd.Series) -> np.ndarray: + """Copia FEDELE di gtaa._exposure (che prende una Series di soli close).""" + px = close.values + n = len(px) + tgt = np.zeros(n) + mh = max(HORIZONS) + for i in range(mh, n): + tgt[i] = np.mean([1.0 if px[i] > px[i - H] else 0.0 for H in HORIZONS]) + s = pd.Series(tgt, index=close.index) + rv = close.pct_change().rolling(63, min_periods=20).std().shift(1) * ANN + scale = np.clip(np.nan_to_num(TARGET_VOL / rv.replace(0, np.nan).values, nan=0.0), 0, 10.0) + return (s * scale).clip(0, 1.0).values + + +@lru_cache(maxsize=2) +def gtaa_daily_tuples() -> pd.DataFrame: + """(R, m) giornalieri di GTAA01: chiusura replicata da gtaa.gtaa_returns e minimo intraday + dagli OHLC delle 6 gambe. Long-only e co-moventi -> convenzione **ogni gamba al proprio + minimo insieme** (severa, la stessa famiglia del bound 'perleg' di XS01). + Weekend/festivi: R = 0 e m = 0 (capitale IB fermo — convenzione del book).""" + cap_leg = GTAA_DEFAULT_CAPITAL / len(EQ_UNIVERSE) + legs_r, legs_m = {}, {} + for sym in EQ_UNIVERSE: + d = _eq_ohlc(sym) + close = d["close"] + ex = np.nan_to_num(_gtaa_exposure(close)) + px = close.values + prev = np.concatenate(([np.nan], px[:-1])) + ret = np.zeros(len(px)) + ret[1:] = px[1:] / px[:-1] - 1.0 + ret_low = d["low"].values / prev - 1.0 # long-only: il peggio e' il minimo + held = np.empty(len(ex)) + comm = np.zeros(len(ex)) + cur = 0.0 + for i in range(len(ex)): # identico a gtaa._gated_returns + if i % REBAL_EVERY == 0: + notional = abs(ex[i] - cur) * cap_leg + if notional >= max(REBAL_BAND_USD, IB_MIN_TRADE_USD): + comm[i] = ib_commission(notional, px[i]) / cap_leg + cur = ex[i] + held[i] = cur + pos = np.zeros(len(held)) + pos[1:] = held[:-1] + legs_r[sym] = pd.Series(pos * ret - comm, index=close.index) + legs_m[sym] = pd.Series(pos * np.nan_to_num(ret_low) - comm, index=close.index) + R = pd.concat(legs_r, axis=1, sort=True).sort_index().mean(axis=1, skipna=True).dropna() + M = pd.concat(legs_m, axis=1, sort=True).sort_index().mean(axis=1, skipna=True).dropna() + days = pd.date_range(R.index.min().normalize(), R.index.max().normalize(), freq="1D", tz="UTC") + R = R.reindex(days).fillna(0.0) + M = M.reindex(days).fillna(0.0) + out = pd.DataFrame({"R": R, "m": np.minimum(M, R)}) + return out[out.index >= GTAA_BOOK_ACTIVATION] + + +@lru_cache(maxsize=2) +def vrp_daily_tuples() -> pd.DataFrame: + """(R, m) di VRP01 con **m = R**: il mark infra-settimana NON e' nel dataset dello sleeve + (rendimento settimanale lumpato sul giorno di scadenza). Lente CIECA e GENEROSA — dichiarata.""" + r = _vrp_combo_returns().dropna().sort_index() + if r.index.tz is None: + r.index = r.index.tz_localize("UTC") + return pd.DataFrame({"R": r.values, "m": r.values}, index=r.index) + + +def _d(s: pd.Series) -> pd.Series: + s = s.dropna().sort_index() + if s.index.tz is None: + s.index = s.index.tz_localize("UTC") + return s + + +_CRY_CACHE: dict = {} + + +def _crypto(w_tp: float, w_skh: float) -> pd.DataFrame: + key = (round(w_tp, 6), round(w_skh, 6)) + if key not in _CRY_CACHE: + _CRY_CACHE[key] = pc.crypto_daily_tuples(w_tp, w_skh) + return _CRY_CACHE[key] + + +@lru_cache(maxsize=4) +def _panel_index(start: str) -> pd.DatetimeIndex: + """Griglia comune ai 5 sleeve (intersezione), dal `start` in poi.""" + idx = _crypto(1.0, 0.0).index + for f in (pc.xsec_daily_tuples(), vrp_daily_tuples(), gtaa_daily_tuples()): + idx = idx.intersection(f.index) + return idx[idx >= pd.Timestamp(start, tz="UTC")] + + +_RG_CACHE: dict = {} + + +def book_RG(w: dict, start: str = START_WIN) -> tuple[np.ndarray, np.ndarray]: + """(R, gap) giornalieri di un libro a pesi arbitrari sui 5 sleeve. + Crypto TP01+SKH01: minimo ESATTO sul path orario condiviso (r0725_prop_coupled). + XS01/VRP01/GTAA01: il loro minimo si SOMMA (worst simultaneo — convenzione del 25/07).""" + key = (start,) + tuple(round(float(w.get(nm, 0.0)), 6) for nm in SLEEVES) + if key in _RG_CACHE: + return _RG_CACHE[key] + idx = _panel_index(start) + w_tp, w_skh = w.get("TP01", 0.0), w.get("SKH01", 0.0) + R = np.zeros(len(idx)) + M = np.zeros(len(idx)) + if w_tp > 0 or w_skh > 0: + c = _crypto(w_tp, w_skh).reindex(idx) + R += c["R"].values + M += c["m"].values + for nm, fr in (("XS01", pc.xsec_daily_tuples()), ("VRP01", vrp_daily_tuples()), + ("GTAA01", gtaa_daily_tuples())): + wt = w.get(nm, 0.0) + if wt > 0: + f = fr.reindex(idx) + R += wt * f["R"].values + M += wt * f["m"].values + R = np.nan_to_num(R) + M = np.nan_to_num(M) + _RG_CACHE[key] = (R, np.minimum(M, R) - R) + return _RG_CACHE[key] + + +def deluck(R: np.ndarray, factor: float = DELUCK) -> np.ndarray: + return R - (1.0 - factor) * R.mean() + + +# ================================================================== §1. SIMULATORE + +def boot_idx(n_hist: int, n_days: int, n_paths: int, seed: int, block: int = BLOCK) -> np.ndarray: + rng = np.random.default_rng(seed) + n_blocks = int(np.ceil(n_days / block)) + st = rng.integers(0, n_hist - block, size=(n_paths, n_blocks)) + return (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days] + + +def eval_sim(R: np.ndarray, G: np.ndarray, idx: np.ndarray, lev: float, firm: str, + lens: str = "coupled") -> dict: + """P(passare l'eval) entro l'orizzonte del bootstrap + giorni al pass.""" + tgt, dd, dl = FIRMS[firm]["ev"] + r = R[idx] * lev + g = (G[idx] * lev) if lens == "coupled" else np.zeros_like(r) + P, D = r.shape + eq = np.ones(P) + alive = np.ones(P, bool) + passed = np.zeros(P, bool) + tpass = np.full(P, D, int) + for t in range(D): + act = alive & ~passed + if not act.any(): + break + rt, gt = r[:, t], g[:, t] + eqp = eq + eq = np.where(act, eq * (1.0 + rt), eq) + eq_low = np.where(act, eqp * (1.0 + rt + gt), eq) + bust = act & ((eq_low < 1.0 - dd) | (rt + gt < -dl)) + alive &= ~bust + ok = act & ~bust & (eq >= 1.0 + tgt) + tpass[ok] = t + passed |= ok + return dict(p_pass=float(passed.mean()), passed=passed, + t_pass=float(np.median(tpass[passed])) if passed.any() else np.nan) + + +def funded_sim(R: np.ndarray, G: np.ndarray, idx: np.ndarray, lev: float, firm: str, + notional: float, lens: str = "coupled") -> dict: + """P(vivo a fine orizzonte) + payout netto cumulato (prelievo mensile, base ripristinata).""" + ml, dl = FIRMS[firm]["fu"] + r = R[idx] * lev + g = (G[idx] * lev) if lens == "coupled" else np.zeros_like(r) + P, D = r.shape + eq = np.ones(P) + alive = np.ones(P, bool) + payout = np.zeros(P) + alive_at = {} + for t in range(D): + rt, gt = r[:, t], g[:, t] + eqp = eq + eq = np.where(alive, eq * (1.0 + rt), eq) + eq_low = np.where(alive, eqp * (1.0 + rt + gt), eq) + bust = alive & ((eq_low < 1.0 - ml) | (rt + gt < -dl)) + alive &= ~bust + if (t + 1) % 30 == 0: + take = alive & (eq > 1.0) + payout += np.where(take, (eq - 1.0) * notional, 0.0) * SPLIT * (1.0 - TAX_RATE) + eq = np.where(take, 1.0, eq) + if (t + 1) % 365 == 0: + alive_at[(t + 1) // 365] = float(alive.mean()) + return dict(p_alive=float(alive.mean()), alive_at=alive_at, + e_payout=float(payout.mean()), payout=payout) + + +def objective(w: dict, lev: float, firm: str, ev_idx: np.ndarray, fu_idx: np.ndarray, + lens: str = "coupled", factor: float = DELUCK, + notional: float = 100_000.0) -> dict: + R, G = book_RG(w) + R = deluck(R, factor) + e = eval_sim(R, G, ev_idx, lev, firm, lens) + f = funded_sim(R, G, fu_idx, lev, firm, notional, lens) + ann = float(R.mean() * 365.0) + vol = float(R.std() * np.sqrt(365.0)) + return dict(J=e["p_pass"] * f["p_alive"], p_pass=e["p_pass"], p_alive=f["p_alive"], + e_payout=f["e_payout"], sharpe=(ann / vol if vol > 0 else 0.0), + vol=vol, drift=ann, t_pass=e["t_pass"]) + + +# ================================================================== §2. IL CONTO ANALITICO + +def p_hit_target_first(mu: float, sig: float, lev: float, T: float, D: float) -> float: + """Gambler's ruin per moto browniano con drift: P(toccare +T prima di -D), SENZA limite di + tempo. theta = 2*mu/(k*sigma^2): la leva entra SOLO dividendo theta -> con leva -> inf la + probabilita' tende a D/(T+D) (37.5% con 10%/6%), qualunque sia lo Sharpe.""" + if sig <= 0: + return float(mu > 0) + theta = 2.0 * mu / (lev * sig * sig) + if abs(theta) < 1e-12: + return D / (T + D) + return float((1.0 - np.exp(-theta * D)) / (1.0 - np.exp(-theta * (T + D)))) + + +# ================================================================== §3. GRIGLIA / NULL + +def simplex_grid(names: tuple, step_den: int) -> list[dict]: + """Tutti i vettori di peso a passo 1/step_den sul simplesso di `names` (somma 1).""" + out = [] + n = len(names) + + def rec(prefix, rem, k): + if k == n - 1: + out.append(dict(zip(names, [p / step_den for p in prefix] + [rem / step_den]))) + return + for v in range(rem + 1): + rec(prefix + [v], rem - v, k + 1) + + rec([], step_den, 0) + return out + + +def wkey(w: dict) -> str: + return "/".join(f"{int(round(100 * w.get(nm, 0.0))):d}" for nm in CRYPTO_PROP) + + +def grid_scan(grid: list[dict], levs: tuple, firm: str, ev_idx: np.ndarray, fu_idx: np.ndarray, + lens: str = "coupled", factor: float = DELUCK) -> pd.DataFrame: + rows = [] + for w in grid: + for lev in levs: + o = objective(w, lev, firm, ev_idx, fu_idx, lens, factor) + rows.append(dict(w=wkey(w), lev=lev, **{k: v for k, v in o.items()})) + return pd.DataFrame(rows) + + +def random_weights(n: int, names: tuple, seed: int, floor: float = 0.0, + caps: dict | None = None) -> list[dict]: + """Stesso campionatore di `portfolio.weights_tilt_null` (dirichlet uniforme sul simplesso + + rejection su floor/caps), ma qui serve a costruire il NULL della funzione obiettivo J.""" + rng = np.random.default_rng(seed) + caps_v = np.array([(caps or {}).get(nm, 1.0) for nm in names], float) + out = [] + while len(out) < n: + b = rng.dirichlet(np.ones(len(names)), size=4 * n + 64) + ok = (b >= floor).all(axis=1) & (b <= caps_v).all(axis=1) + out.extend(b[ok]) + return [dict(zip(names, v)) for v in out[:n]] + + +# ================================================================== main + +def hr(t: str = "") -> None: + print("\n" + "-" * 104) + if t: + print(f" {t}") + print("-" * 104) + + +def main() -> None: + t0 = time.time() + print("=" * 104) + print(" PROP-ALLOC — allocare per la BARRIERA (P(pass) x P(vivo 12m)) invece che per lo Sharpe") + print("=" * 104) + + # ---------------------------------------------------------------- A. pannello + sanity + idx = _panel_index(START_WIN) + print(f"\n finestra comune ai 5 sleeve: {len(idx)} giorni {idx[0].date()} -> {idx[-1].date()}" + f" (XS01 nasce nel 2024: e' anche la sua finestra di SCOPERTA -> lo favorisce)") + print(f" de-luck sul drift x{DELUCK} (misurato 26/07) | bootstrap a blocchi di {BLOCK}g, " + f"indici CONDIVISI fra configurazioni | {N_PATHS} percorsi") + + off = {"TP01": _d(_tp01_returns()), "SKH01": _d(_skyhook_returns()), "XS01": _d(_xsec_returns()), + "VRP01": _d(_vrp_combo_returns()), "GTAA01": _d(_gtaa_daily_returns())} + rec = {"TP01": _crypto(1.0, 0.0)["R"], "SKH01": _crypto(0.0, 1.0)["R"], + "XS01": pc.xsec_daily_tuples()["R"], "VRP01": vrp_daily_tuples()["R"], + "GTAA01": gtaa_daily_tuples()["R"]} + print("\n sanity — la ricostruzione (R) deve coincidere con lo sleeve ufficiale sulla chiusura:") + for nm in SLEEVES: + com = off[nm].index.intersection(rec[nm].index) + dmax = float(np.abs(off[nm].reindex(com).values - rec[nm].reindex(com).values).max()) + note = "" if nm not in ("TP01", "SKH01") else " (recon MTM orario: lente diversa per costruzione)" + print(f" {nm:>7}: max|delta| = {dmax:.2e} {'OK' if dmax < 1e-9 else 'DIVERGE'}{note}") + + hr("A. PROFILO DEGLI SLEEVE SOTTO BARRIERA — cio' che conta non e' lo Sharpe, e si vede qui") + print(f" {'sleeve':>8} {'Sharpe':>7} {'vol a':>7} {'drift a':>8} {'skew':>7} " + f"{'giorno peggiore':>16} {'gap p90':>9} {'gap peggiore':>13} {'g/vol':>7}") + prof = {} + for nm in SLEEVES: + R, G = book_RG({nm: 1.0}) + Rd = deluck(R) + vol = Rd.std() * np.sqrt(365.0) + dr = Rd.mean() * 365.0 + sk = float(pd.Series(Rd).skew()) + prof[nm] = dict(sharpe=dr / vol if vol > 0 else 0.0, vol=vol, drift=dr, skew=sk, + worst=Rd.min(), gap90=np.percentile(-G, 90), gapw=G.min()) + print(f" {nm:>8} {prof[nm]['sharpe']:>7.2f} {vol:>6.1%} {dr:>8.1%} {sk:>7.2f} " + f"{Rd.min():>15.2%} {-np.percentile(-G, 90)*100:>8.2f}pp {G.min()*100:>12.2f}pp " + f"{(-G.min()/vol) if vol > 0 else 0:>7.2f}") + print("\n lettura: `gap peggiore` e' l'escursione intraday che la lente close-only NON vede;") + print(" `g/vol` la misura in unita' di vol annua. VRP01 ha gap 0 **per cecita' del dato**,") + print(" non per assenza di rischio (mark infra-settimana misurato il 30/07: mediana -6.9%,") + print(" minimo -81.7% del capitale su un trade finito in UTILE).") + + hr("B. IL CONTO ANALITICO — perche' con leva LIBERA la barriera premia lo SHARPE, non la vol") + tgt, dd, _dl = FIRMS["HYRO"]["ev"] + print(f" P(toccare +{tgt:.0%} prima di -{dd:.0%}), gambler's ruin senza limite di tempo, " + f"theta = 2*mu/(k*sigma^2):") + print(f" {'sleeve':>8} {'Sharpe':>7} | " + " ".join(f"{f'k={k:g}':>8}" for k in (0.25, 0.5, 1.0, 2.0, 4.0))) + for nm in SLEEVES: + p = prof[nm] + cells = " ".join(f"{p_hit_target_first(p['drift'], p['vol'], k, tgt, dd):>8.1%}" + for k in (0.25, 0.5, 1.0, 2.0, 4.0)) + print(f" {nm:>8} {p['sharpe']:>7.2f} | {cells}") + print(f" {'limite k->inf':>16} | " + " ".join([f"{dd/(tgt+dd):>8.1%}"] * 5) + + " <- D/(T+D): la leva alta CANCELLA l'edge") + print("\n Alla leva minima che porta a +T dentro l'orizzonte (k ~ T/mu) resta theta ~ 2*S^2/T:") + print(" la VOL sparisce e conta lo Sharpe AL QUADRATO. Percio' la previsione registrata prima") + print(" di misurare e': l'argmax di J sui PESI ~ l'argmax di Sharpe, e il contenuto vero e'") + print(" nella LEVA (che lo Sharpe non determina) e nella deviazione dal gaussiano.") + + # ---------------------------------------------------------------- C. griglia + n_hist = len(idx) + EV_H, FU_H = 365, 365 + ev_idx = boot_idx(n_hist, EV_H, N_PATHS, seed=8220) + fu_idx = boot_idx(n_hist, FU_H, N_PATHS, seed=8221) + LEVS = (0.25, 0.375, 0.50, 0.625, 0.75, 1.00, 1.25, 1.50) + STEP = 8 + grid = simplex_grid(CRYPTO_PROP, STEP) + + hr(f"C. GRIGLIA — {len(grid)} vettori di peso (passo 1/{STEP}) x {len(LEVS)} leve " + f"= {len(grid)*len(LEVS)} celle, firm HYRO (eval +10%/-6%/-4%, funded -6%/-4%)") + print(f" orizzonte eval {EV_H}g, funded {FU_H}g. Lente ACCOPPIATA. " + f"J = P(pass) x P(vivo 12m).") + t = time.time() + G3 = grid_scan(grid, LEVS, "HYRO", ev_idx, fu_idx) + print(f" ({time.time()-t:.0f}s)") + + best = G3.loc[G3["J"].idxmax()] + # argmax di SHARPE: lo Sharpe non dipende dalla leva -> si sceglie il peso, poi la leva + # migliore PER QUEL PESO secondo J (altrimenti non e' un confronto onesto). + w_sh = G3.loc[G3["sharpe"].idxmax(), "w"] + sub_sh = G3[G3["w"] == w_sh] + best_sh = sub_sh.loc[sub_sh["J"].idxmax()] + live = G3[(G3["w"] == "75/25/0")] + best_live = live.loc[live["J"].idxmax()] + + print(f"\n {'':>22} {'pesi TP/SKH/XS':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} " + f"{'P(vivo12m)':>11} {'Sharpe':>7} {'vol':>6} {'E[pay/a]':>10} {'g.al pass':>10}") + for lab, row in (("argmax J", best), ("argmax Sharpe", best_sh), + ("libro LIVE 75/25", best_live)): + print(f" {lab:>22} {row['w']:>16} {row['lev']:>6.3f} {row['J']:>7.3f} " + f"{row['p_pass']:>8.1%} {row['p_alive']:>11.1%} {row['sharpe']:>7.2f} " + f"{row['vol']:>6.1%} {row['e_payout']:>9,.0f}$ {row['t_pass']:>10.0f}") + + print("\n Le prime 8 celle per J:") + print(f" {'#':>3} {'pesi':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} {'P(vivo)':>8} " + f"{'Sharpe':>7} {'E[pay/a]':>10}") + for i, (_, r) in enumerate(G3.sort_values("J", ascending=False).head(8).iterrows(), 1): + print(f" {i:>3} {r['w']:>16} {r['lev']:>6.3f} {r['J']:>7.3f} {r['p_pass']:>8.1%} " + f"{r['p_alive']:>8.1%} {r['sharpe']:>7.2f} {r['e_payout']:>9,.0f}$") + + print("\n E per E[payout] (obiettivo DIVERSO: un flusso, non una sopravvivenza):") + print(f" {'#':>3} {'pesi':>16} {'leva':>6} {'E[pay/a]':>10} {'J':>7} {'P(pass)':>8} {'P(vivo)':>8}") + for i, (_, r) in enumerate(G3.sort_values("e_payout", ascending=False).head(4).iterrows(), 1): + print(f" {i:>3} {r['w']:>16} {r['lev']:>6.3f} {r['e_payout']:>9,.0f}$ {r['J']:>7.3f} " + f"{r['p_pass']:>8.1%} {r['p_alive']:>8.1%}") + + hr("C-bis. LA LEVA — la variabile che lo Sharpe non determina e che decide tutto") + print(f" pesi = argmax J ({best['w']}):") + print(f" {'leva':>7} {'J':>7} {'P(pass) 365g':>13} {'P(pass) 1095g':>14} {'P(vivo 12m)':>12} " + f"{'E[pay/a]':>10}") + ev_long = boot_idx(n_hist, 1095, N_PATHS, seed=8222) + w_best = dict(zip(CRYPTO_PROP, [int(x) / 100 for x in best["w"].split("/")])) + Rb, Gb = book_RG(w_best) + Rb = deluck(Rb) + for lev in LEVS: + o = objective(w_best, lev, "HYRO", ev_idx, fu_idx) + pl_long = eval_sim(Rb, Gb, ev_long, lev, "HYRO")["p_pass"] + print(f" {lev:>7.3f} {o['J']:>7.3f} {o['p_pass']:>12.1%} {pl_long:>13.1%} " + f"{o['p_alive']:>11.1%} {o['e_payout']:>9,.0f}$") + print("\n A 365 giorni P(pass) e' a CAMPANA nella leva (a leva bassa il drift non arriva a +10%") + print(" in tempo); senza limite di tempo — che e' la regola vera di HyroTrader — e' MONOTONA") + print(" DECRESCENTE, come dice il conto analitico. Le due colonne dicono cose diverse e vanno") + print(" lette insieme: la leva alta non aumenta la probabilita' di farcela, aumenta la FRETTA.") + + # ---------------------------------------------------------------- D. i null + hr("D. NULL 1 — l'argmax e' speciale, o e' solo il MASSIMO DI 45 ESTRAZIONI?") + rw = random_weights(300, CRYPTO_PROP, seed=8223) + lev_star = float(best["lev"]) + Jr = np.array([objective(w, lev_star, "HYRO", ev_idx, fu_idx)["J"] for w in rw]) + pctl = float((Jr < best["J"]).mean() * 100.0) + bestofk = 100.0 * len(grid) / (len(grid) + 1) + print(f" J dell'argmax = {best['J']:.3f} alla leva {lev_star:g}") + print(f" null: 300 vettori di peso casuali uniformi sul simplesso (stesso campionatore di") + print(f" `portfolio.weights_tilt_null`), valutati alla STESSA leva") + print(f" mediana {np.median(Jr):.3f} | p90 {np.percentile(Jr, 90):.3f} | " + f"max {Jr.max():.3f} | frazione sopra il LIVE 75/25 = {(Jr > best_live['J']).mean():.0%}") + print(f" percentile dell'argmax fra i casuali = {pctl:.1f}° contro la firma best-of-" + f"{len(grid)} = {bestofk:.1f}°") + print(f" -> {'DENTRO' if pctl <= bestofk else 'SOPRA'} la firma del massimo-di-k: " + f"{'nessuna evidenza che quel vettore sia speciale' if pctl <= bestofk else 'vettore particolare'}") + print(f" (la meta' bassa del null e' pero' informativa: il LIVE 75/25 e' battuto dal " + f"{(Jr > best_live['J']).mean():.0%} dei pesi CASUALI)") + + hr("D-bis. NULL 2 — selezione IN-SAMPLE-ONLY (la scelta guardando J e' selezione come ogni altra)") + half = n_hist // 2 + R_all = {} + for w in grid: + R_, G_ = book_RG(w) + R_all[wkey(w)] = (deluck(R_), G_) + ev_h = boot_idx(half, EV_H, N_PATHS, seed=8224) + fu_h = boot_idx(half, FU_H, N_PATHS, seed=8225) + + def _J(key, lev, sl): + R_, G_ = R_all[key] + e = eval_sim(R_[sl], G_[sl], ev_h, lev, "HYRO")["p_pass"] + f = funded_sim(R_[sl], G_[sl], fu_h, lev, "HYRO", 100_000.0)["p_alive"] + return e * f + + IS, OOS = slice(0, half), slice(half, n_hist) + rows = [] + for w in grid: + k = wkey(w) + for lev in LEVS: + rows.append((k, lev, _J(k, lev, IS), _J(k, lev, OOS))) + S = pd.DataFrame(rows, columns=["w", "lev", "J_is", "J_oos"]) + pick = S.loc[S["J_is"].idxmax()] + oracle = S.loc[S["J_oos"].idxmax()] + live_oos = S[(S["w"] == "75/25/0")].sort_values("J_oos", ascending=False).iloc[0] + print(f" prima meta' {idx[0].date()}->{idx[half-1].date()} | seconda {idx[half].date()}->{idx[-1].date()}") + print(f" {'':>28} {'pesi':>16} {'leva':>6} {'J in-sample':>12} {'J out-of-sample':>16}") + print(f" {'scelto SUL SOLO in-sample':>28} {pick['w']:>16} {pick['lev']:>6.3f} " + f"{pick['J_is']:>12.3f} {S[(S['w']==pick['w'])&(S['lev']==pick['lev'])]['J_oos'].iloc[0]:>16.3f}") + print(f" {'oracolo (max sull OOS)':>28} {oracle['w']:>16} {oracle['lev']:>6.3f} " + f"{oracle['J_is']:>12.3f} {oracle['J_oos']:>16.3f}") + print(f" {'libro LIVE 75/25 (baseline)':>28} {live_oos['w']:>16} {live_oos['lev']:>6.3f} " + f"{live_oos['J_is']:>12.3f} {live_oos['J_oos']:>16.3f}") + rho = float(S["J_is"].corr(S["J_oos"], method="spearman")) + print(f" Spearman(J in-sample, J out-of-sample) su {len(S)} celle = {rho:+.2f}") + print(" -> se rho e' alto la mappa e' stabile e la scelta si trasferisce; se e' ~0 l'argmax") + print(" e' rumore e la decisione va presa sul MECCANISMO (la leva), non sulla cella.") + + hr("D-ter. NULL DEL DE-LEVERING — la leva e' GIA' dentro l'ottimizzazione, ma va detto") + print(" Il null classico ('esiste k<1 che da' lo stesso DD con Sharpe migliore?') qui e'") + print(" strutturale: ogni configurazione e' valutata a OTTO leve, quindi nessuna puo' vincere") + print(" solo perche' e' meno esposta. La domanda che resta e': la MIGLIOR leva del libro LIVE") + print(" raggiunge il J dell'argmax?") + print(f" argmax J : {best['w']:>10} @ {best['lev']:g}x -> J = {best['J']:.3f}") + print(f" LIVE 75/25 al suo : {best_live['w']:>10} @ {best_live['lev']:g}x -> J = {best_live['J']:.3f}") + dJ = best["J"] - best_live["J"] + se = np.sqrt(0.25 / N_PATHS) * 2 + print(f" delta = {dJ:+.3f} (errore MC su livelli ~+-{se:.3f}; le due celle condividono") + print(f" gli indici di bootstrap -> il delta e' APPAIATO e il suo errore e' piu' piccolo)") + + # ---------------------------------------------------------------- E. banda d'ancora + hr("E. BANDA D'ANCORA — non ri-misurata sotto questa lente (costo); si usa il FATTORE misurato") + print(" Ri-fare i 24x23x10 offset col recon MTM orario e' fuori budget. Si applica il fattore") + print(" di drift MISURATO il 26/07 (x0.89 sul libro live) e si controlla cio' che conta: se") + print(" l'ORDINE fra le configurazioni sopravvive alla banda 0.60 (stima 25/07, ri-misurata") + print(" troppo severa) - 1.00 (nessuna correzione).") + cands = [best["w"], best_sh["w"], best_live["w"]] + print(f"\n {'fattore':>9} " + " ".join(f"{('J ' + c):>14}" for c in dict.fromkeys(cands)) + + f" {'argmax pesi':>14} {'argmax leva':>11}") + for fct in (1.00, 0.89, 0.60): + cells = [] + for c in dict.fromkeys(cands): + wc = dict(zip(CRYPTO_PROP, [int(x) / 100 for x in c.split("/")])) + sub = [objective(wc, lv, "HYRO", ev_idx, fu_idx, factor=fct)["J"] for lv in LEVS] + cells.append(max(sub)) + g = grid_scan(grid, LEVS, "HYRO", ev_idx, fu_idx, factor=fct) + b = g.loc[g["J"].idxmax()] + print(f" {fct:>9.2f} " + " ".join(f"{v:>14.3f}" for v in cells) + + f" {b['w']:>14} {b['lev']:>11.3f}") + + # ---------------------------------------------------------------- F. VRP01 / GTAA01 + hr("F. E GLI SLEEVE 'INUTILI A $600'? — l'asimmetria del 25/07 §4 vale per XS01, non per tutti") + print(" Su un conto funded da $100k gli sleeve fuori portata a $600 diventano ESEGUIBILI per") + print(" TAGLIA. Ma la taglia non e' l'unico vincolo: una firm CRYPTO non lista opzioni Deribit") + print(" (VRP01) ne' ETF azionari USA (GTAA01, che al retail UE e' pure bloccato dal PRIIPs).") + print(" Qui si misura comunque il CONTROFATTUALE: se fossero disponibili, aiuterebbero?") + print(f"\n {'aggiunta al ' + best['w']:>34} {'leva':>6} {'J':>7} {'P(pass)':>8} " + f"{'P(vivo)':>8} {'Sharpe':>7} {'E[pay/a]':>10}") + for extra, wx in [(None, 0.0), ("VRP01", 0.10), ("VRP01", 0.20), + ("GTAA01", 0.10), ("GTAA01", 0.20)]: + w2 = {k: v * (1.0 - wx) for k, v in w_best.items()} + if extra: + w2[extra] = wx + rows = [(lv, objective(w2, lv, "HYRO", ev_idx, fu_idx)) for lv in LEVS] + lv, o = max(rows, key=lambda x: x[1]["J"]) + lab = "nessuna (base)" if extra is None else f"{extra} @ {wx:.0%}" + print(f" {lab:>34} {lv:>6.3f} {o['J']:>7.3f} {o['p_pass']:>8.1%} {o['p_alive']:>8.1%} " + f"{o['sharpe']:>7.2f} {o['e_payout']:>9,.0f}$") + print("\n ATTENZIONE: la riga VRP01 e' calcolata con **gap = 0** (il mark infra-settimana non") + print(" esiste nel dataset dello sleeve): e' un TETTO, non una stima. Con il mark misurato il") + print(" 30/07 (peggiore infra-settimana mediano -6.9%, minimo -81.7%) una barriera STATICA al") + print(" 6% verrebbe sfondata dal mark, non dal risultato.") + + # ---------------------------------------------------------------- G. FTMO + hr("G. LA BARRIERA CAMBIA LA RISPOSTA? — HYRO (-6% statico) vs FTMO (-10% statico)") + print(f" {'firm':>6} {'argmax pesi':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} {'P(vivo12m)':>11} " + f"{'E[pay/a]':>10} | {'LIVE 75/25: leva':>17} {'J':>7}") + for fm in ("HYRO", "FTMO"): + g = grid_scan(grid, LEVS, fm, ev_idx, fu_idx) + b = g.loc[g["J"].idxmax()] + lv2 = g[g["w"] == "75/25/0"] + b2 = lv2.loc[lv2["J"].idxmax()] + print(f" {fm:>6} {b['w']:>16} {b['lev']:>6.3f} {b['J']:>7.3f} {b['p_pass']:>8.1%} " + f"{b['p_alive']:>11.1%} {b['e_payout']:>9,.0f}$ | {b2['lev']:>17.3f} {b2['J']:>7.3f}") + print("\n La barriera piu' larga di FTMO sposta l'ottimo di leva verso l'ALTO: il vincolo e' la") + print(" barriera, non il libro. E' la stessa lettura del 25/07 §4 ('sul canale funded il") + print(" vincolo binding e' la regola di DD, non il capitale'), qui misurata sull'ottimo.") + + # ---------------------------------------------------------------- H. gate ufficiale sui pesi + hr("H. GATE UFFICIALE SUI PESI (`portfolio.weights_tilt_null`) — sullo SHARPE, come previsto") + cols = {nm: off[nm] for nm in CRYPTO_PROP} + try: + res = weights_tilt_null(cols, {"TP01": 0.75, "SKH01": 0.25, "XS01": 0.0001}, + {nm: max(w_best.get(nm, 0.0), 1e-4) for nm in CRYPTO_PROP}, + floor=0.0, n=300, k_seen=len(grid) * len(LEVS)) + print(f" proposta {best['w']} vs LIVE 75/25 : delta_insample {res['delta_insample']:+.4f} | " + f"delta_hold {res['delta_hold']:+.4f} | pctl_hold {res['pctl_hold']:.1f}° " + f"(best-of-k {res['bestofk_pctl']:.1f}°) | gate_pass = {res['gate_pass']}") + except Exception as exc: # pragma: no cover + print(f" non girato: {exc}") + print(" NB: questo gate giudica lo SHARPE del book di ricerca, non J. Un cambio dei pesi del") + print(" LIBRO LIVE resta soggetto a lui; qui si progetta un conto SEPARATO (funded), che non") + print(" e' il libro live e non passa da questo gate.") + + # ---------------------------------------------------------------- I. tavola operativa + hr("I. TAVOLA OPERATIVA — le 3 configurazioni migliori, e quanti conti servono per 50 EUR/g") + top = G3.sort_values("J", ascending=False).drop_duplicates("w").head(3) + rows_ops = [] + fu3 = boot_idx(n_hist, 1095, N_PATHS, seed=8226) + print(f" {'pesi TP/SKH/XS':>16} {'leva':>6} {'P(pass)':>8} {'P(vivo12m)':>11} " + f"{'E[payout 3a]':>13} {'EUR/g per conto':>16} {'conti per 50/g':>15}") + for _, r in top.iterrows(): + w2 = dict(zip(CRYPTO_PROP, [int(x) / 100 for x in r["w"].split("/")])) + R_, G_ = book_RG(w2) + R_ = deluck(R_) + f3 = funded_sim(R_, G_, fu3, float(r["lev"]), "HYRO", 100_000.0) + eur_day = f3["e_payout"] / EURUSD / 3.0 / 365.0 + n_acct = (TARGET_EUR_DAY / eur_day) if eur_day > 0 else np.inf + rows_ops.append((r["w"], float(r["lev"]), f3["e_payout"], eur_day, n_acct)) + print(f" {r['w']:>16} {r['lev']:>6.3f} {r['p_pass']:>8.1%} {r['p_alive']:>11.1%} " + f"{f3['e_payout']:>12,.0f}$ {eur_day:>15.2f} {n_acct:>15.1f}") + print("\n E[payout 3a] e' su UN conto funded da $100k, gia' netto split 80% e fisco 33%, e") + print(" MEDIA su tutti i percorsi (i morti contano zero). 'conti per 50/g' e' quindi un numero") + print(" ATTESO, non una garanzia: la distribuzione e' bimodale (vivo / bustato).") + cap_tot = sum(f["cap"] for f in pl.FIRMS) + print(f" ⚠ TETTO STRUTTURALE: cap per trader ${FIRMS['HYRO']['cap']:,.0f} (HYRO) + " + f"${FIRMS['FTMO']['cap']:,.0f} (FTMO) = ${cap_tot:,.0f} di nozionale su due firm") + print(f" -> al massimo {cap_tot/100_000:.0f} conti da $100k. Se ne servono di piu', servono " + f"ALTRE FIRM (e ognuna aggiunge rischio di controparte).") + + hr("I-bis. LA SCALA DI CONTI — macchineria del 25/07 (`r0725_prop_coupled.simulate`), " + "de-luck portato a x0.89") + print(f" 36 mesi, EUR {pl.START_EUR:.0f} di cassa iniziale, max {pl.MAX_CONCURRENT} conti, " + f"morte-firm {pl.FIRM_DEATH_PER_YEAR:.0%}/anno, lente ACCOPPIATA") + pc.DELUCK = DELUCK # il 25/07 girava a 0.6 (poi ri-misurato troppo severo) + cfg_best = tuple(w_best.get(nm, 0.0) for nm in CRYPTO_PROP) + cfg_live = (0.75, 0.25, 0.0) + cfg_B = (0.55, 0.20, 0.25) + pols = { + f"argmax-J {best['w']} su tutti i conti": ([cfg_best], [0] * pl.MAX_CONCURRENT), + "LIVE 75/25 su tutti i conti (baseline)": ([cfg_live], [0] * pl.MAX_CONCURRENT), + "config B 55/20/25 (scelta a mano 25/07)": ([cfg_B], [0] * pl.MAX_CONCURRENT), + } + print(f"\n {'politica':>44} {'leva':>6} {'EUR/g med':>10} {'p90':>8} {'P(>=10/g)':>10} " + f"{'P(>=50/g)':>10} {'P(zero)':>8}") + for pname, (cfgs, slots) in pols.items(): + for lev in (float(best["lev"]), 0.75): + r = pc.simulate(cfgs, slots, n_paths=2000, lev=lev, seed=20260822, + lens="coupled", start=START_WIN) + print(f" {pname:>44} {lev:>6.3f} {r['med']:>10.2f} {r['p90']:>8.2f} " + f"{r['p_ge10']:>10.1%} {r['p_target']:>10.1%} {r['p_zero']:>8.1%}") + + print("\n" + "=" * 104) + print(f" fatto in {time.time()-t0:.0f}s") + print("=" * 104) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_skew.py b/scripts/research/r0822_skew.py new file mode 100644 index 0000000..2c5b4e5 --- /dev/null +++ b/scripts/research/r0822_skew.py @@ -0,0 +1,618 @@ +"""r0822_skew.py — la FORMA della superficie di volatilita' (skew), mai usata dal progetto. + +Il progetto ha usato molte volte il LIVELLO della vol implicita (DVOL/ATM): VRP01 lo vende sotto +gate IV-rank, DVOLSPREAD ne fa relative-value BTC/ETH, TP01xDVOL lo prova come denominatore del +vol-target. Non ha MAI usato la FORMA: il risk reversal 25-delta (RR), il butterfly (BF) e la loro +dinamica. La catena per-strike esiste da 2026-05-01 (`data/raw/cb_chain/`), quindi la domanda e' +finalmente misurabile. + +Tre domande, in ordine di valore dichiarato nel brief: + Q1 lo skew ANTICIPA lo spot? ("il posizionamento in opzioni anticipa il prezzo") + Q2 lo skew e' un GATE DI RISCHIO sopra il libro TP01? + Q3 lo skew spiega l'errore di PREZZATURA di VRP01 (f=0.73 misurato il 30/07)? + +Onesta' strutturale, dichiarata PRIMA dei numeri: + * La finestra utile e' 2026-06-09 -> oggi, cioe' ~75 giorni, NON i 113 giorni di righe presenti. + Prima del 09/06 cerbero-bite raccoglieva UNA SOLA scadenza per ora (verificato: `exps=1`), + quindi non esiste ne' struttura a termine ne' maturita' costante. Righe presenti != dato + presente (regola 8 del brief), e qui il conto va fatto in ORE DI SUPERFICIE RICOSTRUIBILE. + * HOLDOUT del progetto = 2025-01-01: l'INTERO campione e' posteriore. Non esiste hold-out, + non esiste in-sample -> `study_family_honest` e `select_cell_insample` NON sono eseguibili + (l'in-sample e' vuoto). Il verdetto massimo ottenibile e' LEAD con gate pre-registrato. + * Con ~75 osservazioni giornaliere SE(Sharpe annualizzato) ~ sqrt(365/75) ~ 2.2: qualunque + Sharpe giornaliero misurato qui e' indistinguibile da zero. Percio' il peso della sessione + sta dove la POTENZA c'e' davvero: la relazione oraria skew<->prezzo (N~1.800) e la + decomposizione di prezzo di VRP01 (deterministica, non statistica). + +Convenzioni di costruzione (sono SCELTE, non dettagli — vedi sezione 1): + * "quotato" = bid>0 AND ask>0 AND ask>=bid AND iv/delta non nulli. Si contano le quote, non le righe. + * smile per (asset, ora, scadenza): IV interpolata LINEARMENTE in |delta| a 0.25 e 0.50, + senza MAI estrapolare (se la catena non abbraccia il delta bersaglio, la cella si scarta). + * maturita' costante: RR/BF/ATM interpolati fra le due scadenze che abbracciano T (7g e 30g). + Il roll e' CONTINUO per costruzione: non c'e' una data di roll, quindi non c'e' il dente di + sega di tenore che un "scadenza piu' vicina a T" iniettera' nel segnale. + * ogni ora porta `ts_max` = il timestamp piu' recente fra le quote usate. I rendimenti in avanti + partono STRETTAMENTE dopo `ts_max`: senza questo l'ancora e' sbagliata di ~30 minuti e nasce + un falso "lo skew anticipa di 15 minuti" (misurato in sessione: vedi sezione 2). + +Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822_skew.py +""" +from __future__ import annotations + +import glob +import os +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +import altlib as A # noqa: E402 +import cblib as CB # noqa: E402 + +CHAIN = ROOT / "data" / "raw" / "cb_chain" +SCRATCH = Path(os.environ.get("SKEW_SCRATCH", "/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad")) +ASSETS = ("BTC", "ETH") +COLS = ["ts", "asset", "instrument_name", "option_type", "strike", "exp", + "bid", "ask", "mid", "iv", "delta", "open_interest", "quote_status"] + +# tick MISURATO sui dati (non assunto): 0.0001 sotto 0.005 di premio, 0.0005 sopra. +def tick_of(prezzo: np.ndarray) -> np.ndarray: + return np.where(np.asarray(prezzo, float) < 0.005, 0.0001, 0.0005) + + +def rule(t: str) -> None: + print("\n" + "=" * 78) + print(t) + print("=" * 78) + + +# ============================================================================= +# 0. DATO +# ============================================================================= +def load_quoted() -> tuple[pd.DataFrame, dict]: + """Catena filtrata alle QUOTE vere. Ritorna anche la contabilita' righe-vs-quote.""" + parts, n_righe, n_status = [], 0, {} + for f in sorted(glob.glob(str(CHAIN / "*.parquet"))): + d = pd.read_parquet(f, columns=COLS) + n_righe += len(d) + for k, v in d["quote_status"].value_counts(dropna=False).items(): + n_status[str(k)] = n_status.get(str(k), 0) + int(v) + d["ts"] = pd.to_datetime(d["ts"], utc=True).dt.tz_convert("UTC") + d["exp"] = pd.to_datetime(d["exp"], utc=True).dt.tz_convert("UTC") + d["strike"] = d["strike"].astype(float) + parts.append(d) + d = pd.concat(parts, ignore_index=True) + d = d.drop_duplicates(subset=["ts", "instrument_name"], keep="last") + d["ts"] = pd.DatetimeIndex(d["ts"]).as_unit("ns") + d["exp"] = pd.DatetimeIndex(d["exp"]).as_unit("ns") + d["dte"] = (d["exp"] - d["ts"]).dt.total_seconds() / 86400.0 + + ok = (d["bid"] > 0) & (d["ask"] > 0) & (d["ask"] >= d["bid"]) & d["iv"].notna() & d["delta"].notna() + cont = dict(righe=n_righe, dedup=int(len(d)), quotate=int(ok.sum()), + frac_quotate=round(float(ok.mean()), 4), quote_status=n_status) + q = d[ok & (d["dte"] > 0.5) & (d["dte"] < 95)].copy() + q["hr"] = q["ts"].dt.floor("h") + q["ad"] = q["delta"].abs() + # una riga per (ora, strumento): se in un'ora ci sono piu' giri, vale l'ULTIMO + q = q.sort_values("ts").drop_duplicates(subset=["hr", "asset", "instrument_name"], keep="last") + return q, cont + + +def conta_tick(q: pd.DataFrame) -> pd.DataFrame: + """Quanti tick vale lo spread, e quanti tick vale l'ask? (regola: prima di credere a un + rapporto estremo su un prezzo piccolo, contare i tick).""" + out = [] + for a in ASSETS: + for nome, m in (("ala |d|~0.10", (q["ad"] > 0.06) & (q["ad"] < 0.14)), + ("25-delta", (q["ad"] > 0.20) & (q["ad"] < 0.30)), + ("ATM |d|~0.50", (q["ad"] > 0.45) & (q["ad"] < 0.55))): + g = q[(q["asset"] == a) & m] + if g.empty: + continue + tk = tick_of(g["ask"].to_numpy()) + spread_tick = (g["ask"].to_numpy() - g["bid"].to_numpy()) / tk + ask_tick = g["ask"].to_numpy() / tk + out.append(dict(asset=a, banda=nome, n=len(g), + ask_tick_med=round(float(np.median(ask_tick)), 1), + ask_tick_p5=round(float(np.percentile(ask_tick, 5)), 1), + frac_ask_le2tick=round(float((ask_tick <= 2).mean()), 4), + spread_tick_med=round(float(np.median(spread_tick)), 1), + spread_su_mid_med=round(float(np.median( + (g["ask"] - g["bid"]) / g["mid"].replace(0, np.nan))), 3))) + return pd.DataFrame(out) + + +# ============================================================================= +# 1. SUPERFICIE: smile -> RR/BF a maturita' costante +# ============================================================================= +def _iv_at(ad: np.ndarray, iv: np.ndarray, tgt: float) -> float: + o = np.argsort(ad) + ad, iv = ad[o], iv[o] + if len(ad) < 2 or tgt < ad[0] or tgt > ad[-1]: + return np.nan # nessuna estrapolazione, mai + return float(np.interp(tgt, ad, iv)) + + +def build_smile(q: pd.DataFrame) -> pd.DataFrame: + """Per (asset, ora, scadenza): IV a |delta|=0.25 su entrambe le ali e ATM a |delta|=0.50.""" + cache = SCRATCH / "r0822_smile.parquet" + if cache.exists(): + try: + return pd.read_parquet(cache) + except Exception: + pass + rows = [] + for (asset, hr, exp), g in q.groupby(["asset", "hr", "exp"], sort=False): + c = g[g["option_type"] == "C"] + p = g[g["option_type"] == "P"] + if len(c) < 2 or len(p) < 2: + continue + c25 = _iv_at(c["ad"].to_numpy(), c["iv"].to_numpy(), 0.25) + p25 = _iv_at(p["ad"].to_numpy(), p["iv"].to_numpy(), 0.25) + if not (np.isfinite(c25) and np.isfinite(p25)): + continue + c50 = _iv_at(c["ad"].to_numpy(), c["iv"].to_numpy(), 0.50) + p50 = _iv_at(p["ad"].to_numpy(), p["iv"].to_numpy(), 0.50) + atm = np.nanmean([x for x in (c50, p50) if np.isfinite(x)]) if ( + np.isfinite(c50) or np.isfinite(p50)) else np.nan + rows.append((asset, hr, exp, float(g["dte"].iloc[0]), c25, p25, atm, + g["ts"].max(), int(len(g)))) + R = pd.DataFrame(rows, columns=["asset", "hr", "exp", "dte", "ivc25", "ivp25", + "ivatm", "ts_max", "n_gambe"]) + R["rr"] = R["ivc25"] - R["ivp25"] + R["bf"] = 0.5 * (R["ivc25"] + R["ivp25"]) - R["ivatm"] + try: + SCRATCH.mkdir(parents=True, exist_ok=True) + R.to_parquet(cache) + except Exception: + pass + return R + + +def const_maturity(R: pd.DataFrame, asset: str, T: float) -> pd.DataFrame: + """RR/BF/ATM a maturita' costante T giorni. Interpolazione lineare in DTE fra le due scadenze + che ABBRACCIANO T (mai estrapolazione) -> il roll e' continuo, non c'e' data di roll.""" + g = R[(R["asset"] == asset)].dropna(subset=["rr", "ivatm"]) + out = {} + for hr, h in g.groupby("hr"): + h = h.sort_values("dte") + dt = h["dte"].to_numpy() + if len(h) < 2 or T < dt[0] or T > dt[-1]: + continue + out[hr] = dict(rr=float(np.interp(T, dt, h["rr"].to_numpy())), + bf=float(np.interp(T, dt, h["bf"].to_numpy())), + atm=float(np.interp(T, dt, h["ivatm"].to_numpy())), + ts_max=h["ts_max"].max()) + S = pd.DataFrame.from_dict(out, orient="index").sort_index() + S.index.name = "hr" + if len(S): + S["rr_n"] = S["rr"] / S["atm"] # skew normalizzato dal livello (toglie il DVOL) + return S + + +# ============================================================================= +# 2. Q1 — lo skew ANTICIPA lo spot? +# ============================================================================= +def px5m(asset: str, t0: pd.Timestamp) -> pd.Series: + p = pd.read_parquet(ROOT / "data" / "raw" / f"{asset.lower()}_5m.parquet", + columns=["timestamp", "close"]) + s = pd.Series(p["close"].to_numpy(float), + index=pd.DatetimeIndex(pd.to_datetime(p["timestamp"], unit="ms", utc=True))) + return s.sort_index().loc[t0 - pd.Timedelta("3h"):] + + +def leadlag(S: pd.DataFrame, asset: str) -> pd.DataFrame: + """Correlazione fra la variazione oraria dello skew e i rendimenti PRIMA e DOPO l'istante + esatto in cui le quote sono state osservate (`ts_max`). L'ancora conta: con un'ancora + assunta a :30 invece del `ts_max` vero compare un falso lead a +15m.""" + S = S.sort_values("ts_max") + px = px5m(asset, S.index.min()) + anc = pd.Series(px.reindex(pd.DatetimeIndex(S["ts_max"]), method="ffill").to_numpy(), + index=S.index) + drr = S["rr"].diff() + lvl = S["rr"] + rows = [] + for mins in (-60, -30, -15, -5, 5, 15, 30, 60, 120, 240, 1440, 4320): + tgt = pd.DatetimeIndex(S["ts_max"]) + pd.Timedelta(minutes=int(mins)) + p2 = pd.Series(px.reindex(tgt, method="ffill").to_numpy(), index=S.index) + r = np.log(p2 / anc) if mins > 0 else np.log(anc / p2) # sempre "rendimento nel verso del tempo" + j = pd.concat({"d": drr, "l": lvl, "r": r}, axis=1).dropna() + n = len(j) + rows.append(dict(asset=asset, minuti=mins, n=n, + corr_dRR=round(float(j["d"].corr(j["r"])), 3), + corr_RR=round(float(j["l"].corr(j["r"])), 3), + se=round(1 / np.sqrt(max(n, 2)), 3), + t_dRR=round(float(j["d"].corr(j["r"]) * np.sqrt(n)), 2))) + return pd.DataFrame(rows) + + +def leadlag_ancora_sbagliata(S: pd.DataFrame, asset: str) -> pd.DataFrame: + """Controllo POSITIVO del punto precedente: la stessa misura con l'ancora ASSUNTA a :30 + dell'ora (invece del `ts_max` osservato) deve mostrare il falso lead. Se non lo mostra, + la correzione non stava correggendo niente.""" + px = px5m(asset, S.index.min()) + fake = pd.DatetimeIndex(S.index) + pd.Timedelta("30min") + anc = pd.Series(px.reindex(fake, method="ffill").to_numpy(), index=S.index) + drr = S["rr"].diff() + rows = [] + for mins in (5, 15, 30, 60): + p2 = pd.Series(px.reindex(fake + pd.Timedelta(minutes=mins), method="ffill").to_numpy(), + index=S.index) + j = pd.concat({"d": drr, "r": np.log(p2 / anc)}, axis=1).dropna() + rows.append(dict(asset=asset, minuti=mins, corr_dRR_ancora_finta=round(float(j["d"].corr(j["r"])), 3))) + return pd.DataFrame(rows) + + +def daily_signal(S: pd.DataFrame, df: pd.DataFrame) -> pd.DataFrame: + """Allinea la superficie alla griglia dei prezzi in modo CAUSALE. + + Le barre del progetto sono open-labeled: la barra 1d etichettata D chiude alle 23:00 di D. + Quindi al momento della decisione della barra D e' disponibile l'ultima ora <= D 23:00, e + `eval_weights` la tiene durante D+1. Con merge_asof su epoca esplicita in millisecondi + (lezione 01/07: `.view("int64")` su tz-aware non-ns sbaglia scala e broadcasta).""" + bpd = A.bars_per_day(df) + chiusura = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + chiusura = chiusura + (pd.Timedelta("24h") - pd.Timedelta("1h") if bpd == 1 else pd.Timedelta(0)) + left = pd.DataFrame({"t_ms": chiusura.astype("datetime64[ms]").astype("int64")}) + right = pd.DataFrame({"t_ms": pd.DatetimeIndex(S["ts_max"]).astype("datetime64[ms]").astype("int64"), + "rr": S["rr"].to_numpy(), "rr_n": S["rr_n"].to_numpy(), + "bf": S["bf"].to_numpy(), "atm": S["atm"].to_numpy()}).sort_values("t_ms") + m = pd.merge_asof(left.sort_values("t_ms"), right, on="t_ms", direction="backward", + tolerance=int(pd.Timedelta("6h").total_seconds() * 1000)) + return m[["rr", "rr_n", "bf", "atm"]] + + +def make_target(S: pd.DataFrame, feat: str, win: int, segno: int): + """target_fn(df, asset) -> posizione continua vol-targeted. Causale per costruzione: + ogni feature usa solo z-score rolling su dati <= barra corrente, e `eval_weights` sposta.""" + def fn(df, asset=None): + F = daily_signal(S, df) + x = F["rr_n"] if feat.endswith("_n") else F["rr"] + if feat.startswith("d"): + x = x.diff() + z = (x - x.rolling(win, min_periods=max(5, win // 2)).mean()) / \ + x.rolling(win, min_periods=max(5, win // 2)).std() + d = np.tanh(np.nan_to_num(z.to_numpy(), nan=0.0)) * segno + return A.vol_target(d, df, target_vol=0.20, leverage_cap=2.0) + return fn + + +def sharpe_boot(s: pd.Series, blocco: int = 10, n: int = 500, seed: int = 20260822) -> tuple: + """IC95% dello Sharpe con block bootstrap circolare (il segnale e' lentissimo: i giorni + NON sono indipendenti, e una SE analitica mentirebbe).""" + r = np.asarray(s.dropna().to_numpy(), float) + T = len(r) + if T < 20: + return (np.nan, np.nan) + rng = np.random.default_rng(seed) + nb = int(np.ceil(T / blocco)) + out = [] + for _ in range(n): + st = rng.integers(0, T, nb) + idx = (st[:, None] + np.arange(blocco)[None, :]).ravel()[:T] % T + x = r[idx] + out.append(np.mean(x) / np.std(x) * np.sqrt(365.25) if np.std(x) > 0 else 0.0) + return (round(float(np.percentile(out, 2.5)), 2), round(float(np.percentile(out, 97.5)), 2)) + + +def causality_finestra(S: pd.DataFrame, fn, asset: str) -> dict: + """`A.causality_ok` taglia il prefisso all'80%/92% della storia dei PREZZI (2024/2025): + fuori dalla finestra dello skew, dove il segnale e' zero -> confronterebbe zeri con zeri, + POTENZA NULLA (stessa trappola del self-check di SKH01: si campiona sugli EVENTI). + Qui i tagli cadono DENTRO la finestra.""" + df = A.get(asset, "1d") + full = np.nan_to_num(np.asarray(fn(df, asset), float)) + n = len(df) + dts = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + dentro = np.where(dts >= S.index.min())[0] + peggio, controlli = 0.0, 0 + for frac in (0.4, 0.7, 0.9): + cut = int(dentro[0] + frac * (n - dentro[0])) + if cut <= dentro[0] + 5 or cut >= n: + continue + sub = df.iloc[:cut].reset_index(drop=True) + s = np.nan_to_num(np.asarray(fn(sub, asset), float)) + coda = min(20, cut - dentro[0]) + peggio = max(peggio, float(np.max(np.abs(s[cut - coda:cut] - full[cut - coda:cut])))) + controlli += 1 + return dict(ok=bool(peggio <= 1e-6 and controlli > 0), max_diff=round(peggio, 10), + controlli=controlli, non_zero=int((full[dentro] != 0).sum())) + + +# ============================================================================= +# 3. Q2 — lo skew come GATE DI RISCHIO sopra TP01 +# ============================================================================= +def dd_of(r: np.ndarray) -> float: + eq = np.cumprod(1.0 + np.nan_to_num(r)) + pk = np.maximum.accumulate(eq) + return float(np.max((pk - eq) / pk)) if len(eq) else 0.0 + + +def sh_of(r: np.ndarray) -> float: + r = np.asarray(r, float) + return float(np.mean(r) / np.std(r) * np.sqrt(365.25)) if np.std(r) > 0 else 0.0 + + +def null_de_levering(base: np.ndarray, overlay: np.ndarray) -> dict: + """Il primo test di OGNI claim su meno drawdown: esiste k<=1 che, applicato al baseline, + da' lo STESSO DD dell'overlay ma con Sharpe MIGLIORE? Se si', l'overlay e' solo meno leva.""" + dd_t = dd_of(overlay) + lo, hi = 0.0, 1.0 + for _ in range(60): + k = 0.5 * (lo + hi) + if dd_of(k * base) > dd_t: + hi = k + else: + lo = k + k = 0.5 * (lo + hi) + return dict(k=round(k, 3), dd_overlay=round(dd_t, 4), dd_base_k=round(dd_of(k * base), 4), + sh_overlay=round(sh_of(overlay), 3), sh_base_k=round(sh_of(k * base), 3), + superato=bool(sh_of(overlay) > sh_of(k * base))) + + +# ============================================================================= +# 4. Q3 — lo skew spiega l'errore di prezzo di VRP01? +# ============================================================================= +def vrp_decomposizione(q: pd.DataFrame, cm7: dict) -> pd.DataFrame: + """Per ogni ingresso settimanale di VRP01 (stessa macchina di `cblib`, cosi' i numeri sono + confrontabili con lo 0.718 pubblicato il 30/07), scompone il f in fattori MOLTIPLICATIVI: + + f_tot = f_term x f_skew x f_markfit x f_spread + + f_term = credito prezzato ad ATM 7g piatta / credito prezzato a DVOL30 piatta + (quanto del difetto e' STRUTTURA A TERMINE: il sleeve prezza un'opzione a 7 + giorni con l'indice a 30 giorni) + f_skew = credito prezzato a IV PER GAMBA / credito ad ATM 7g piatta + (quanto e' SKEW: l'ala che si compra non vale la vol ATM) + f_markfit = credito ai MID reali / credito a IV per gamba (controllo: deve stare a ~1, + altrimenti la IV di Deribit non riprezza i propri mid e tutta la catena di + ragionamento poggia su un modello invece che su un prezzo) + f_spread = credito al fill CONSERVATIVO (vendi al bid, compri all'ask) / credito ai mid + """ + righe = [] + for a in ASSETS: + S7 = cm7[a] + E = CB.weekly_entries(a, q) + for _, e in E.iterrows(): + prima = S7.index[S7.index <= e["ts"]] + if len(prima) == 0 or (e["ts"] - prima[-1]) > pd.Timedelta("6h"): + continue + s = S7.loc[prima[-1]] + T = e["dte"] / 365.25 + dvol = e["dvol_pct"] / 100.0 + atm7 = s["atm"] / 100.0 + c_mod = CB.bs_put(e["spot"], e["k_short"], T, dvol) - CB.bs_put(e["spot"], e["k_long"], T, dvol) + c_atm = CB.bs_put(e["spot"], e["k_short"], T, atm7) - CB.bs_put(e["spot"], e["k_long"], T, atm7) + c_sml = (CB.bs_put(e["spot"], e["k_short"], T, e["iv_short"] / 100.0) + - CB.bs_put(e["spot"], e["k_long"], T, e["iv_long"] / 100.0)) + c_mid = e["cred_mid"] + c_real = e["cred_real"] + righe.append(dict( + asset=a, ts=e["ts"], dte=round(e["dte"], 2), ivrank=round(e["ivrank"], 3), + dvol=round(e["dvol_pct"], 2), atm7=round(s["atm"], 2), + rr7=round(s["rr"], 2), bf7=round(s["bf"], 2), + iv_short=round(e["iv_short"], 2), iv_long=round(e["iv_long"], 2), + d_short=round(e["d_short"], 3), d_long=round(e["d_long"], 3), + f_term=c_atm / c_mod if c_mod > 0 else np.nan, + f_skew=c_sml / c_atm if c_atm > 0 else np.nan, + f_markfit=c_mid / c_sml if c_sml > 0 else np.nan, + f_spread=c_real / c_mid if c_mid > 0 else np.nan, + f_tot_mid=c_mid / c_mod if c_mod > 0 else np.nan, + f_tot=c_real / c_mod if c_mod > 0 else np.nan, + quota_ala=CB.bs_put(e["spot"], e["k_long"], T, dvol) / + CB.bs_put(e["spot"], e["k_short"], T, dvol), + )) + return pd.DataFrame(righe) + + +# ============================================================================= +def main() -> None: + t_start = time.time() + rule("r0822_skew — SKEW (risk reversal 25-delta): la dimensione mai usata della superficie") + + # ---------------------------------------------------------------- 0. DATO + rule("0. IL DATO — quote, non righe") + q, cont = load_quoted() + print(f" righe totali nella catena : {cont['righe']:,}") + print(f" dopo dedup (ts, strumento) : {cont['dedup']:,}") + print(f" QUOTATE (bid>0, ask>0, iv/delta ok) : {cont['quotate']:,} = {cont['frac_quotate']:.1%}") + print(f" quote_status : {cont['quote_status']}") + print(f" usate (0.5 {q['ts'].max()}") + print("\n TICK (regola: prima di credere a un rapporto estremo su un prezzo piccolo, contarli)") + print(conta_tick(q).to_string(index=False)) + + # ---------------------------------------------------------- 1. SUPERFICIE + rule("1. SUPERFICIE — smile per delta, poi maturita' costante") + t0 = time.time() + R = build_smile(q) + print(f" celle (asset, ora, scadenza) con smile ricostruibile: {len(R):,} [{time.time()-t0:.0f}s]") + perday = R.groupby(["asset", pd.DatetimeIndex(R["hr"]).date]).agg(exps=("exp", "nunique")) + print(f" giorni con >=2 scadenze (maturita' costante possibile): " + f"{int((perday['exps'] >= 2).sum())} / {len(perday)} coppie asset-giorno") + cm7, cm30 = {}, {} + for a in ASSETS: + cm7[a] = const_maturity(R, a, 7.0) + cm30[a] = const_maturity(R, a, 30.0) + s = cm30[a] + print(f" {a}: CM30 ore={len(s):5d} giorni={len(np.unique(pd.DatetimeIndex(s.index).date)):3d}" + f" {s.index.min()} -> {s.index.max()}") + print(f" RR30 media {s['rr'].mean():+.2f} pp sd {s['rr'].std():.2f} " + f"[{s['rr'].min():+.2f}, {s['rr'].max():+.2f}] ATM media {s['atm'].mean():.1f}% " + f"BF30 media {s['bf'].mean():+.2f}") + print(f" CM7 ore={len(cm7[a]):5d} RR7 media {cm7[a]['rr'].mean():+.2f} sd {cm7[a]['rr'].std():.2f}") + print("\n ⚠ La finestra utile parte dal 2026-06-09 e NON dal 2026-05-01: prima di quella data") + print(" cerbero-bite raccoglieva UNA sola scadenza per ora -> nessuna maturita' costante.") + print(" righe presenti != superficie presente. Le ore perse sono ~38 giorni su 113.") + + # ------------------------------------------------------------------ 2. Q1 + rule("2. Q1 — lo skew ANTICIPA lo spot? (potenza vera: N orario ~1.800)") + LL = pd.concat([leadlag(cm30[a], a) for a in ASSETS], ignore_index=True) + for a in ASSETS: + print(f"\n {a} — corr fra dRR (variazione oraria dello skew) e il rendimento nella") + print(" finestra indicata, ancorata al TIMESTAMP VERO delle quote (ts_max):") + print(LL[LL["asset"] == a].to_string(index=False)) + print("\n Controllo POSITIVO — la stessa misura con l'ancora ASSUNTA a :30 dell'ora:") + FF = pd.concat([leadlag_ancora_sbagliata(cm30[a], a) for a in ASSETS], ignore_index=True) + print(FF.to_string(index=False)) + + # strategia giornaliera + rule("2b. Q1 — lo skew come SEGNALE DIREZIONALE giornaliero (griglia dichiarata)") + feats = ["rr", "rr_n", "drr"] + wins = [10, 20, 40] + segni = [+1, -1] + celle, serie = [], {} + for f in feats: + for w in wins: + for sg in segni: + nome = f"{f}|w{w}|sg{sg:+d}" + per_asset = {} + for a in ASSETS: + df = A.get(a, "1d") + Sx = cm30[a] + m = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) >= ( + Sx.index.min().floor("D")) + sub = df[m].reset_index(drop=True) + tgt = make_target(Sx, f, w, sg)(sub, a) + ev = A.eval_weights(sub, tgt) + per_asset[a] = pd.Series(ev["net"], index=ev["idx"]) + J = pd.concat(per_asset, axis=1, join="inner").fillna(0.0) + d = 0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]] + d.index = pd.DatetimeIndex(d.index) + serie[nome] = d + celle.append(dict(cella=nome, sharpe=round(sh_of(d.to_numpy()), 3), + maxdd=round(dd_of(d.to_numpy()), 4), + cagr=round(float((1 + d).prod() ** (365.25 / len(d)) - 1), 4))) + C = pd.DataFrame(celle).sort_values("sharpe", ascending=False) + print(f" griglia: {len(feats)} feature x {len(wins)} finestre-z x {len(segni)} segni = {len(C)} celle") + print(C.head(6).to_string(index=False)) + print(" ...") + print(C.tail(3).to_string(index=False)) + best = C.iloc[0]["cella"] + bs = serie[best] + lo, hi = sharpe_boot(bs) + print(f"\n MIGLIORE della griglia: {best} Sharpe {C.iloc[0]['sharpe']:+.3f}") + print(f" IC95% block-bootstrap (blocchi 10g, 500 estrazioni): [{lo:+.2f}, {hi:+.2f}] n={len(bs)} giorni") + print(f" ⚠ ampiezza dell'IC = {hi-lo:.2f} di Sharpe: con {len(bs)} giorni la misura non separa") + print(" un edge da zero. Nessuna cella di questa griglia e' distinguibile dal rumore.") + dsr, sr0 = A.deflated_sharpe(C.iloc[0]["sharpe"], C["sharpe"].tolist(), bs) + print(f" deflated Sharpe della cella migliore: DSR={dsr:.3f} (soglia 0.95), " + f"max atteso sotto il nullo {sr0:+.2f}") + print(" NB: e' un deflated-Sharpe BEST-OF-GRID, non in-sample-selected: `select_cell_insample`") + print(" NON e' eseguibile qui (HOLDOUT=2025-01-01, l'in-sample e' VUOTO).") + + print("\n marginal_vs_tp01 sulla cella migliore:") + try: + rep = A.marginal_vs_tp01(bs) + for k in ("marginal_verdict", "corr_full", "n_days", "has_insample_edge", "is_hedge", + "beats_noise_null", "robust_oos"): + if k in rep: + print(f" {k:20s} = {rep[k]}") + except Exception as ex: + print(f" non girato: {ex}") + + print("\n causalita' (tagli DENTRO la finestra dello skew, non all'80% della storia prezzi):") + for a in ASSETS: + f_, w_, sg_ = best.split("|") + fn = make_target(cm30[a], f_, int(w_[1:]), int(sg_.replace("sg", ""))) + print(f" {a}: {causality_finestra(cm30[a], fn, a)}") + + # ------------------------------------------------------------------ 3. Q2 + rule("3. Q2 — lo skew come GATE DI RISCHIO sopra TP01") + B = A.tp01_baseline_daily() + inizio = min(cm30[a].index.min() for a in ASSETS).floor("D") + Bw = B[B.index >= inizio] + z_all = {} + for a in ASSETS: + df = A.get(a, "1d") + m = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) >= inizio + sub = df[m].reset_index(drop=True) + F = daily_signal(cm30[a], sub) + x = F["rr_n"] + z = (x - x.rolling(20, min_periods=10).mean()) / x.rolling(20, min_periods=10).std() + z_all[a] = pd.Series(z.to_numpy(), index=pd.DatetimeIndex(pd.to_datetime(sub["datetime"], utc=True))) + Z = pd.concat(z_all, axis=1).mean(axis=1) + Z = Z.reindex(Bw.index).ffill() + print(f" baseline TP01 sulla finestra dello skew: n={len(Bw)} giorni, " + f"Sharpe {sh_of(Bw.to_numpy()):+.2f}, maxDD {dd_of(Bw.to_numpy()):.2%}") + for thr in (-0.5, -1.0, -1.5): + gate = (Z.shift(1) > thr).astype(float) # de-risk quando lo skew si irripidisce (z basso) + ov = (Bw * gate).to_numpy() + n_fire = int((gate == 0).sum()) + nul = null_de_levering(Bw.to_numpy(), ov) + gg = Bw[gate == 0] + print(f" soglia z<{thr:+.1f}: giorni de-riskati {n_fire:3d}/{len(Bw)} " + f"({n_fire/len(Bw):.1%}) Sharpe {sh_of(ov):+.2f} (base {sh_of(Bw.to_numpy()):+.2f}) " + f"maxDD {dd_of(ov):.2%}") + print(f" null de-levering: k={nul['k']} -> stesso DD {nul['dd_base_k']:.2%} con " + f"Sharpe {nul['sh_base_k']:+.2f} vs overlay {nul['sh_overlay']:+.2f} " + f"-> {'SUPERATO' if nul['superato'] else 'REFUTED'}") + if n_fire: + print(f" nei giorni de-riskati TP01 faceva in media {gg.mean()*100:+.3f}%/g " + f"(gli altri {Bw[gate==1].mean()*100:+.3f}%/g)") + print("\n ridondanza col trend — corr fra lo z dello skew e il rendimento di TP01: " + f"{float(pd.concat({'z': Z, 'b': Bw}, axis=1).dropna().corr().iloc[0,1]):+.3f}") + print(" POTENZA: con 75 giorni un gate che scatta ~10 volte non e' misurabile. Il numero da") + print(" guardare non e' lo Sharpe dell'overlay ma il conto dei giorni in cui scatta.") + + # ------------------------------------------------------------------ 4. Q3 + rule("4. Q3 — lo skew SPIEGA l'errore di prezzo di VRP01 (f=0.73 pubblicato il 30/07)") + D = vrp_decomposizione(q, cm7) + print(f" ingressi settimanali ricostruiti: {len(D)} ({D.groupby('asset').size().to_dict()})") + print("\n REPLICA del numero pubblicato (deve tornare ~0.72 sul canonico 7g d-0.28/-0.10):") + print(f" f_tot (fill conservativo) mediana = {D['f_tot'].median():.3f} " + f"media = {D['f_tot'].mean():.3f}") + print(f" f_tot ai MID mediana = {D['f_tot_mid'].median():.3f}") + print("\n IV per gamba contro i due riferimenti (pp di vol):") + print(f" IV(corta) - DVOL30 : {(D['iv_short']-D['dvol']).median():+.2f} pp " + f"IV(lunga) - DVOL30 : {(D['iv_long']-D['dvol']).median():+.2f} pp [pubblicato: +0.8 / +7.5]") + print(f" IV(corta) - ATM7 : {(D['iv_short']-D['atm7']).median():+.2f} pp " + f"IV(lunga) - ATM7 : {(D['iv_long']-D['atm7']).median():+.2f} pp") + print(f" ATM7 - DVOL30 : {(D['atm7']-D['dvol']).median():+.2f} pp <- pura struttura a termine") + print("\n DECOMPOSIZIONE MOLTIPLICATIVA del f (mediane; f_tot = term x skew x markfit x spread)") + for a in ASSETS + ("TUTTI",): + g = D if a == "TUTTI" else D[D["asset"] == a] + if g.empty: + continue + print(f" {a:6s} n={len(g):2d} | f_term {g['f_term'].median():.3f} | " + f"f_skew {g['f_skew'].median():.3f} | f_markfit {g['f_markfit'].median():.3f} | " + f"f_spread {g['f_spread'].median():.3f} || f_tot {g['f_tot'].median():.3f}") + print("\n controllo obbligatorio: f_markfit deve stare a ~1 (la IV di Deribit deve riprezzare") + print(f" i suoi stessi mid). Mediana {D['f_markfit'].median():.3f}, " + f"banda [{D['f_markfit'].quantile(.1):.3f}, {D['f_markfit'].quantile(.9):.3f}].") + print(" Se sta a 1, RR e BF di questa sessione sono un fatto di PREZZO, non un artefatto") + print(" del fit di Deribit -> vale anche come controllo di confound per Q1/Q2.") + + print("\n Il f dipende dal REGIME? (la domanda che decide se 0.73 e' conservativo o ottimista)") + D2 = D.dropna(subset=["f_tot_mid", "rr7"]) + for var in ("rr7", "atm7", "ivrank", "bf7"): + c = float(D2["f_tot_mid"].corr(D2[var])) + print(f" corr(f_tot_mid, {var:7s}) = {c:+.3f} [n={len(D2)}]") + print(f"\n IV-rank agli ingressi: min {D['ivrank'].min():.3f} mediana {D['ivrank'].median():.3f} " + f"max {D['ivrank'].max():.3f}") + n_gate = int((D["ivrank"] > 0.30).sum()) + print(f" ingressi che passerebbero il gate IV-rank>0.30 di VRP01: {n_gate}/{len(D)}") + print(" -> il f di VRP01 e' misurato INTERAMENTE nel regime in cui il sleeve sta FLAT.") + + print("\n Tabella per ingresso:") + cols = ["asset", "ts", "dte", "ivrank", "dvol", "atm7", "rr7", "iv_short", "iv_long", + "f_term", "f_skew", "f_markfit", "f_spread", "f_tot_mid", "f_tot"] + P = D[cols].copy() + P["ts"] = pd.DatetimeIndex(P["ts"]).strftime("%Y-%m-%d %H:%M") + for c in ("f_term", "f_skew", "f_markfit", "f_spread", "f_tot_mid", "f_tot"): + P[c] = P[c].round(3) + print(P.to_string(index=False)) + + rule(f"fine — {time.time()-t_start:.0f}s") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_term_structure.py b/scripts/research/r0822_term_structure.py new file mode 100644 index 0000000..67521b3 --- /dev/null +++ b/scripts/research/r0822_term_structure.py @@ -0,0 +1,721 @@ +"""TERM-STRUCTURE della vol implicita BTC/ETH — pendenza, carry, gate di rischio (ondata 2026-08-22). + +DOMANDA. Tre, dichiarate prima di misurare: + Q1 la PENDENZA (backwardation = front > back) PRECEDE i ritorni, o e' contemporanea? + Q2 il CARRY di vol (roll-down lungo la curva) e' INCASSABILE al bid-ask reale? + Q3 vale come GATE DI RISCHIO sopra TP01/SKH01? + +IPOTESI A PRIORI (registrate prima di guardare i numeri — servono a poterle smentire): + Q1 contemporanea. Il front reagisce allo spot: un tuffo alza la vol a 7g piu' di quella a 30g. + Se e' cosi', la pendenza e' un TERMOMETRO, non un segnale. + Q2 no. Il roll-down fra 7g e 30g vale frazioni di punto-vol al giorno; il bid-ask ATM di + Deribit vale punti-vol INTERI. L'aritmetica dovrebbe chiudere la questione senza backtest. + Q3 ridondante col trend, come i 4 precedenti (DVOL-spike, macro, funding, breadth): un gate di + de-risk lavora nei giorni in cui TP01 e' gia' flat. + +IL PRECEDENTE SCOMODO (03/07, `r0703_vrpimp_*`): un gate di term-structure VIX/VXV su SPX valeva ++0.90 di Sharpe (DSR 0.992) ed era un **confound di modello al 100%** — la variabile del gate +coincideva con l'errore di prezzatura BS-flat vs term-structure. Da li' la regola: *riprezzare +term-structure-consistent prima di credere a un gate vol su strutture BS-flat*. +QUI QUEL CONFOUND NON PUO' ESISTERE: non si prezza nessuna opzione. Il sottostante e' il perp +BTC/ETH, i ritorni sono quelli del feed certificato, e la term structure entra solo come +VARIABILE OSSERVATA. Quindi il 03/07 non e' una scusa per non guardare — ed e' esattamente +percio' che serve cercare il confound *di questo* filone, che e' un altro: + + ⚠️ IL CONFOUND DI QUESTO FILONE: iv(7g) e' in larga parte una funzione della vol REALIZZATA + recente. Se e' cosi', `slope = iv30 - iv7` non e' un'informazione nuova ma un + RICONFEZIONAMENTO di RV — cioe' la stessa variabile di `dvol_directional.py` (VRP-Z), gia' + giudicata **HEDGE, earns_slot=False** su 5 anni il 2026-06-29. Misurato in SEZIONE 2. + +IL DATO, e il muro che ci sta dentro. La term structure richiede >=2 scadenze nello STESSO +istante. `bite_archive` prima del **2026-06-09** collezionava **UNA scadenza per giro**: la +finestra utilizzabile NON e' "3,7 mesi di catena", e' **dal 2026-06-09** (~74 giorni). +Con 74 giorni **non esiste un hold-out** e SE(Sharpe) ~ sqrt(365/74) ~ 2.2: qualunque Sharpe +misurato qui e' a una deviazione standard da zero. Verdetto massimo: LEAD con gate e data. +Si conta cio' che e' QUOTATO (bid>0, ask>0, iv presente), non le righe: il guasto 29-30/07 ha +fino al 51% di quote vuote in un giorno. + +METODO. ATM IV per scadenza = interpolazione della IV sul DELTA a 0.5 (call) / -0.5 (put), mai +estrapolata. Il delta referenzia il FORWARD, quindi la curva non eredita lo skew via la base +(prendere "lo strike piu' vicino allo spot" lo farebbe: a 90g il forward e' lontano dallo spot). +Interpolazione ai tenori fissi in VARIANZA TOTALE (w = iv^2 * T lineare in T), stile VIX. + + nice -n 19 timeout 900 uv run python scripts/research/r0822_term_structure.py +""" +from __future__ import annotations + +import os +import sys +import tempfile +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +import altlib as A # noqa: E402 + +RAW = ROOT / "data" / "raw" +STORE = RAW / "cb_chain" +CACHE = Path(os.environ.get("TMPDIR", tempfile.gettempdir())) / "r0822_ts_cache" +ASSETS = ("BTC", "ETH") + +# Muro del dato: prima di questa data l'archivio ha UNA scadenza per giro -> nessuna curva. +TS_START = pd.Timestamp("2026-06-09", tz="UTC") +TENORS = (7, 14, 30, 60, 90) +FRONT, BACK = 7, 30 # la pendenza canonica di questo studio +DTE_MIN = 0.5 # sotto mezza giornata l'ATM IV e' rumore di microstruttura + +# Vincoli di venue DICHIARATI (misurati il 2026-07-30 su `get_instrument`, non ri-letti qui: +# questo script non tocca la rete). Servono alla sezione di eseguibilita'. +MIN_LOT = {"BTC": 0.1, "ETH": 1.0} # contratti minimi opzioni Deribit +OPT_FEE_UNDERLYING = 0.0003 # 0,03% del sottostante per gamba +OPT_FEE_CAP_PREMIUM = 0.125 # cap: 12,5% del premio della singola opzione +CAPITALE = 635.0 # il conto vero + +# Conteggio dei trial, AL RIALZO (sezione 6). Ogni voce e' una configurazione VALUTATA. +TRIALS: list[tuple[str, int]] = [] + + +def hr(t: str = "") -> None: + print("\n" + "=" * 100) + if t: + print(t) + print("=" * 100) + + +def sub(t: str) -> None: + print(f"\n--- {t} " + "-" * max(0, 96 - len(t))) + + +# =========================================================================================== +# SEZIONE 0 — IL DATO +# =========================================================================================== +def load_quoted_chain() -> pd.DataFrame: + """Catena filtrata IN LETTURA (colonne + finestra), tenendo solo cio' che e' QUOTATO. + + "Quotato" = bid>0 AND ask>0 AND iv presente AND delta presente. Una riga con bid/ask NULL + esiste (il collettore la persiste) ma non e' un prezzo: contarla come dato e' l'errore che + il progetto ha gia' fatto tre volte (paper_dvolspread, fresh_5m, guasto 29/07). + """ + cols = ["asset", "option_type", "strike", "iv", "delta", "bid", "ask", + "vega", "ts", "exp", "quote_status"] + parts = [] + arch = STORE / "bite_archive.parquet" + if arch.exists(): + d = pd.read_parquet(arch, columns=cols) + parts.append(d[d["ts"] >= TS_START]) + del d + for p in sorted(STORE.glob("2026-*.parquet")): + parts.append(pd.read_parquet(p, columns=cols)) + if not parts: + raise FileNotFoundError(f"{STORE}: nessuna catena") + df = pd.concat(parts, ignore_index=True) + del parts + df = df.drop_duplicates(subset=["ts", "asset", "strike", "option_type", "exp"], keep="last") + df["hr"] = df["ts"].dt.floor("h") + df["dte"] = (df["exp"] - df["ts"]).dt.total_seconds() / 86400.0 + df["quoted"] = (df["bid"] > 0) & (df["ask"] > 0) & df["iv"].notna() & df["delta"].notna() + return df + + +def sezione0(chain: pd.DataFrame) -> None: + hr("SEZIONE 0 — IL DATO: quanto ce n'e' davvero, e da quando") + print(f" finestra letta : {chain.ts.min()} -> {chain.ts.max()}") + print(f" righe : {len(chain):,}") + print(f" righe QUOTATE : {int(chain.quoted.sum()):,} ({chain.quoted.mean():.1%})") + print("\n ⚠️ IL MURO: la term structure richiede >=2 scadenze nello STESSO istante.") + arch = STORE / "bite_archive.parquet" + if arch.exists(): + pre = pd.read_parquet(arch, columns=["asset", "ts", "exp"]) + pre["hr"] = pre["ts"].dt.floor("h") + n_pre = pre[pre.ts < TS_START].groupby(["asset", "hr"])["exp"].nunique() + n_post = pre[pre.ts >= TS_START].groupby(["asset", "hr"])["exp"].nunique() + print(f" archivio PRIMA del {TS_START.date()}: scadenze/ora mediana = " + f"{n_pre.median():.0f} (max {n_pre.max():.0f}) su {len(n_pre)} ore-asset") + print(f" archivio DOPO : mediana = {n_post.median():.0f} " + f"(max {n_post.max():.0f}) su {len(n_post)} ore-asset") + print(" => la catena copre dal 2026-05-01, ma la CURVA esiste solo dal 2026-06-09.") + print(" Non sono 3,7 mesi: sono ~74 giorni. Nessun hold-out e' costruibile.") + del pre + q = chain.groupby([chain.hr.dt.date, "asset"])["quoted"].mean().unstack() + bad = q[(q < 0.70).any(axis=1)] + print(f"\n giorni con quote quotate <70% su almeno un asset: {len(bad)}") + if len(bad): + print(bad.round(3).to_string().replace("\n", "\n ")) + print(" (guasto noto 29-30/07: rate-limit per-IP saturato dal collettore di bite.)") + print(f"\n minuto dello snapshot (mediana): archivio bite = " + f"{chain[chain.ts < pd.Timestamp('2026-07-30', tz='UTC')].ts.dt.minute.median():.0f}', " + f"raccolta propria = {chain[chain.ts >= pd.Timestamp('2026-07-31', tz='UTC')].ts.dt.minute.median():.0f}'") + print(" -> lo snapshot dell'ora t cade DENTRO la barra oraria t. Ogni ritorno usato come") + print(" bersaglio parte dalla CHIUSURA della barra t (>=35 min dopo): buffer causale.") + + +# =========================================================================================== +# SEZIONE 1 — RICOSTRUZIONE DELLA CURVA + CERTIFICAZIONE +# =========================================================================================== +def _atm_iv(g: pd.DataFrame) -> float: + """ATM IV di UNO snapshot+scadenza: interpolazione sul DELTA, mai estrapolazione.""" + vals = [] + for typ, tgt in (("C", 0.5), ("P", -0.5)): + s = g[g.option_type == typ] + if len(s) >= 2: + d = s["delta"].to_numpy() + if d.min() <= tgt <= d.max(): + o = np.argsort(d) + vals.append(float(np.interp(tgt, d[o], s["iv"].to_numpy()[o]))) + return float(np.mean(vals)) if vals else np.nan + + +def build_atm_by_expiry(chain: pd.DataFrame) -> pd.DataFrame: + f = CACHE / "atm_by_expiry.parquet" + if f.exists(): + return pd.read_parquet(f) + q = chain[chain.quoted & (chain.dte > DTE_MIN)] + out = (q.groupby(["asset", "hr", "exp"]) + .apply(lambda g: pd.Series({"iv": _atm_iv(g), "dte": g["dte"].median()}), + include_groups=False) + .reset_index().dropna(subset=["iv"])) + CACHE.mkdir(parents=True, exist_ok=True) + out.to_parquet(f) + return out + + +def _curve_row(g: pd.DataFrame) -> pd.Series: + g = g.sort_values("dte") + t = g["dte"].to_numpy() + w = (g["iv"].to_numpy() / 100.0) ** 2 * t # varianza totale + o = {} + for T in TENORS: + o[f"iv_{T}"] = (np.sqrt(np.interp(T, t, w) / T) * 100.0 + if len(t) >= 2 and t.min() <= T <= t.max() else np.nan) + o["n_exp"] = len(t) + return pd.Series(o) + + +def build_curve(atm: pd.DataFrame) -> pd.DataFrame: + f = CACHE / "curve.parquet" + if f.exists(): + return pd.read_parquet(f) + c = atm.groupby(["asset", "hr"]).apply(_curve_row, include_groups=False).reset_index() + CACHE.mkdir(parents=True, exist_ok=True) + c.to_parquet(f) + return c + + +def sezione1(curve: pd.DataFrame) -> dict: + hr("SEZIONE 1 — RICOSTRUZIONE DELLA CURVA E SUA CERTIFICAZIONE") + print(" ATM = IV interpolata sul delta 0.5/-0.5 (mai estrapolata); tenori fissi in varianza totale.") + cov = curve[[f"iv_{T}" for T in TENORS]].notna().mean() + print("\n copertura per tenore (frazione delle ore-asset ricostruibili):") + for T in TENORS: + print(f" iv_{T:<3d} {cov[f'iv_{T}']:.1%}") + print(f" -> iv_90 e' sotto il 10%: SCARTATO dallo studio. La pendenza canonica e' " + f"iv_{BACK} - iv_{FRONT}.") + + sub("controllo 1 — contro il logger indipendente `vol_term_*.parquet` (altro codice, altra fonte)") + print(" (`log_vol_termstructure.py`: mark_iv da book_summary, ATM = strike piu' vicino allo") + print(" spot, interpolazione diversa. Se le due strade coincidono, la mia non e' un artefatto.)") + agree = {} + for a in ASSETS: + p = RAW / f"vol_term_{a.lower()}.parquet" + if not p.exists(): + print(f" {a}: {p.name} assente — controllo NON GIRATO") + continue + vt = pd.read_parquet(p).set_index("date") + mine = curve[curve.asset == a].set_index("hr") + j = vt.join(mine[[f"iv_{T}" for T in TENORS]], how="inner") + for T in (7, 30, 60): + x, y = j[f"iv_{T}d"], j[f"iv_{T}"] + m = x.notna() & y.notna() + if m.sum() >= 10: + print(f" {a} iv_{T:<3d} n={m.sum():3d} corr={x[m].corr(y[m]):+.4f} " + f"scarto medio={(y[m] - x[m]).mean():+.3f} pt-vol") + agree[(a, T)] = float(x[m].corr(y[m])) + + sub("controllo 2 — contro il DVOL (indice Deribit a 30g, storia 2021+)") + for a in ASSETS: + dv = pd.read_parquet(RAW / f"dvol_{a.lower()}.parquet") + s = pd.Series(dv["close"].astype(float).values, + index=pd.to_datetime(dv["timestamp"], unit="ms", utc=True)).sort_index() + mine = curve[curve.asset == a].set_index("hr")["iv_30"].dropna() + j = pd.DataFrame({"mine": mine}) + j["dvol"] = s.reindex(j.index, method="ffill") + j = j.dropna() + print(f" {a}: n={len(j)} corr={j.mine.corr(j.dvol):+.4f} " + f"bias={(j.mine - j.dvol).mean():+.3f} pt-vol sd={(j.mine - j.dvol).std():.3f}") + print(" Il bias NEGATIVO e' atteso e conferma la ricostruzione: il DVOL e' un indice tipo") + print(" variance-swap (integra tutto lo smile) e sta STRUTTURALMENTE sopra l'ATM per lo skew.") + print(" Un bias ~0 sarebbe stato il segnale d'allarme, non questo.") + return agree + + +# =========================================================================================== +# SEZIONE 2 — Q1: LEAD-LAG. La pendenza precede o segue? +# =========================================================================================== +def slope_hourly(curve: pd.DataFrame, a: str) -> pd.Series: + s = curve[curve.asset == a].set_index("hr") + return (s[f"iv_{BACK}"] - s[f"iv_{FRONT}"]).dropna().sort_index() + + +def px_hourly(a: str) -> pd.Series: + d = A.get(a, "1h") + return pd.Series(d["close"].astype(float).values, + index=pd.DatetimeIndex(pd.to_datetime(d["datetime"], utc=True))) + + +def _block_pctl(x: np.ndarray, y: np.ndarray, stat: float, L: int, n: int = 800, + seed: int = 20260822) -> float: + """Percentile della correlazione osservata contro il null a BLOCCHI (y ricampionata da + blocchi non allineati a x): conserva l'autocorrelazione e rompe solo l'accoppiamento.""" + rng = np.random.default_rng(seed) + m = len(x) + if m <= L + 5: + return float("nan") + nb = max(1, m // L) + out = np.empty(n) + for i in range(n): + ii = np.concatenate([np.arange(s, s + L) for s in rng.integers(0, m - L, nb)]) + jj = np.concatenate([np.arange(s, s + L) for s in rng.integers(0, m - L, nb)]) + out[i] = np.corrcoef(x[ii], y[jj])[0, 1] + return float((out < stat).mean()) + + +def sezione2(curve: pd.DataFrame) -> dict: + hr("SEZIONE 2 — Q1: la pendenza PRECEDE i ritorni o li SEGUE?") + res = {} + for a in ASSETS: + sl = slope_hourly(curve, a) + px = px_hourly(a) + lp = np.log(px) + j = pd.DataFrame({"slope": sl}).join(px.rename("px"), how="inner").dropna() + idx = j.index + r1 = lp.diff() + sub(f"{a} — n ore = {len(j)} ({idx.min()} -> {idx.max()})") + print(f" pendenza iv_{BACK}-iv_{FRONT}: media {j.slope.mean():+.2f} pt-vol, " + f"sd {j.slope.std():.2f}, backwardation nel {100*(j.slope<0).mean():.1f}% delle ore") + + print("\n (a) CROSS-CORRELAZIONE fra la VARIAZIONE oraria di pendenza e il ritorno orario.") + print(" lag<0 = ritorno PRIMA della variazione (il prezzo guida) | lag>0 = DOPO (la") + print(" pendenza guiderebbe). La barra t va da t a t+1h e lo snapshot cade dentro:") + print(" il lag -1 e' quindi in gran parte SOVRAPPOSTO, non un vero anticipo.") + ds = j.slope.diff() + row = [] + for k in (-3, -2, -1, 0, 1, 2, 3, 6, 12, 24): + row.append((k, float(ds.corr(r1.shift(-k).reindex(idx))))) + print(" " + " ".join(f"{k:+d}h:{v:+.3f}" for k, v in row)) + best = max(row, key=lambda t: abs(t[1])) + print(f" picco |corr| al lag {best[0]:+d}h ({best[1]:+.3f})") + + print("\n (b) LIVELLO della pendenza vs ritorni STRETTAMENTE FUTURI (ingresso alla") + print(" chiusura della barra t) e vs ritorni PASSATI.") + for h in (1, 3, 6, 24, 72, 168): + fwd = (lp.shift(-h) - lp).reindex(idx) + pst = (lp - lp.shift(h)).reindex(idx) + m = j.slope.notna() & fwd.notna() & pst.notna() + print(f" h={h:4d}h corr(pendenza, FUTURO) {j.slope[m].corr(fwd[m]):+.3f} " + f"corr(pendenza, PASSATO) {j.slope[m].corr(pst[m]):+.3f}") + TRIALS.append((f"Q1 {a}: 6 orizzonti x (futuro,passato) x (livello,variazione)", 24)) + + print("\n (c) IL CONFOUND DI QUESTO FILONE — la pendenza e' informazione NUOVA o RV riciclata?") + rvp = (r1.rolling(24).std() * np.sqrt(24 * 365) * 100).reindex(idx) + rvf = (r1.shift(-24).rolling(24).std().shift(-23) * np.sqrt(24 * 365) * 100).reindex(idx) + ivf = curve[curve.asset == a].set_index("hr")[f"iv_{FRONT}"].reindex(idx) + c_pp = float(j.slope.corr(rvp)) + c_pf = float(j.slope.corr(rvf)) + print(f" corr(pendenza, RV 24h PASSATA) {c_pp:+.3f} <-- quanto e' termometro") + print(f" corr(pendenza, RV 24h FUTURA) {c_pf:+.3f} <-- quanto e' previsione") + print(f" corr(iv_{FRONT}, RV 24h passata) {float(ivf.corr(rvp)):+.3f} " + f"corr(iv_{FRONT}, RV futura) {float(ivf.corr(rvf)):+.3f}") + res[(a, "conf_past")], res[(a, "conf_fwd")] = c_pp, c_pf + + print("\n (d) La correlazione col futuro SOPRAVVIVE se si controlla per il PASSATO?") + print(" (regressione del ritorno futuro su pendenza + ritorno passato + RV passata;") + print(" se il coefficiente della pendenza evapora, il segnale era 'e' appena sceso')") + for h in (24, 72, 120): + fwd = (lp.shift(-h) - lp).reindex(idx) + pst = (lp - lp.shift(h)).reindex(idx) + m = j.slope.notna() & fwd.notna() & pst.notna() & rvp.notna() + X = np.column_stack([np.ones(m.sum()), j.slope[m], pst[m], rvp[m]]) + y = fwd[m].to_numpy() + b, *_ = np.linalg.lstsq(X, y, rcond=None) + # errori standard Newey-West (lag = h, finestre sovrapposte) + e = y - X @ b + XtXi = np.linalg.pinv(X.T @ X) + S = (X * e[:, None]).T @ (X * e[:, None]) + for L in range(1, h + 1): + w = 1.0 - L / (h + 1.0) + G = (X[L:] * e[L:, None]).T @ (X[:-L] * e[:-L, None]) + S += w * (G + G.T) + se = np.sqrt(np.diag(XtXi @ S @ XtXi)) + simple = float(j.slope[m].corr(fwd[m])) + print(f" h={h:4d}h corr semplice {simple:+.3f} | beta_pendenza={b[1]:+.5f} " + f"t(NW)={b[1]/se[1]:+.2f} | beta_ret_passato t={b[2]/se[2]:+.2f}") + TRIALS.append((f"Q1 {a}: 3 regressioni controllate", 3)) + + print("\n (e) Test giornaliero con null a BLOCCHI (la finestra e' di 74 giorni: la") + print(" correlazione va confrontata con la sua dispersione, non con zero).") + sld = j.slope.resample("1D").last().dropna() + pxd = px.resample("1D").last() + lpd = np.log(pxd) + for h in (1, 3, 5): + fwd = (lpd.shift(-h) - lpd).reindex(sld.index) + m = sld.notna() & fwd.notna() + x, y = sld[m].to_numpy(), fwd[m].to_numpy() + c = float(np.corrcoef(x, y)[0, 1]) + p = _block_pctl(x, y, c, L=max(3 * h, 7)) + print(f" h={h}g n={m.sum():3d} corr={c:+.3f} pctl vs null a blocchi={p:.3f}") + TRIALS.append((f"Q1 {a}: 3 orizzonti giornalieri", 3)) + return res + + +# =========================================================================================== +# SEZIONE 3 — Q2: il carry di vol e' incassabile? +# =========================================================================================== +def build_atm_cost(chain: pd.DataFrame) -> pd.DataFrame: + """Costo REALE di attraversare lo spread su un'opzione ATM, in PUNTI DI VOL. + + (ask-bid) e' quotato nel sottostante -> x spot = USD; / vega = punti di vol, che e' l'unita' + in cui si misura il roll-down. Cosi' costo e carry sono confrontabili senza modello. + """ + f = CACHE / "atm_cost.parquet" + if f.exists(): + return pd.read_parquet(f) + q = chain[chain.quoted & (chain.dte > DTE_MIN) & (chain.option_type == "C") & (chain.vega > 0)].copy() + q["ad"] = (q["delta"] - 0.5).abs() + atm = q.sort_values("ad").groupby(["asset", "hr", "exp"], as_index=False).first() + atm = atm[atm.ad < 0.08] + for a in ASSETS: + p = px_hourly(a) + m = atm.asset == a + atm.loc[m, "spot"] = p.reindex(atm.loc[m, "hr"]).ffill().to_numpy() + atm = atm.dropna(subset=["spot"]) + atm["spr_usd"] = (atm.ask - atm.bid) * atm.spot + atm["spr_vol"] = atm.spr_usd / atm.vega + atm["prem_usd"] = (atm.bid + atm.ask) / 2.0 * atm.spot + CACHE.mkdir(parents=True, exist_ok=True) + atm.to_parquet(f) + return atm + + +def sezione3(chain: pd.DataFrame, atm_exp: pd.DataFrame, curve: pd.DataFrame) -> None: + hr("SEZIONE 3 — Q2: il CARRY di vol (roll-down) e' incassabile al bid-ask reale?") + print(" Definizione senza modello: un'opzione a tenore T, dopo dt, sta a T-dt. Se la curva non") + print(" si muove la sua IV diventa iv(T-dt). Chi e' CORTO guadagna iv(T)-iv(T-dt) punti di vol.") + print(" Il costo di entrare e uscire e' lo spread bid-ask, misurabile negli STESSI punti di vol.") + + sub("(a) quanto vale il roll-down, e viene DAVVERO incassato?") + print(" Confronto appaiato per SCADENZA e per ISTANTE D'INGRESSO (mai allineato sull'uscita):") + print(" roll IMPLICITO = iv_curva(T-dt) - iv(T) al tempo t ; roll REALIZZATO = iv(T-dt) a t+dt - iv(T) a t") + roll_day = {} + for H in (24, 168): + for a in ASSETS: + r = atm_exp[atm_exp.asset == a] + by_hr = {h: g.sort_values("dte") for h, g in r.groupby("hr")} + rows = [] + for _, g in r.set_index(["exp", "hr"]).sort_index().groupby(level=0): + g = g.droplevel(0).sort_index() + fut = g["iv"].reindex(g.index + pd.Timedelta(hours=H)) + for t, iv0, d0, iv1 in zip(g.index, g["iv"].to_numpy(), + g["dte"].to_numpy(), fut.to_numpy()): + T2 = d0 - H / 24.0 + gg = by_hr.get(t) + if not np.isfinite(iv1) or T2 <= DTE_MIN or gg is None or len(gg) < 2: + continue + td, ivd = gg["dte"].to_numpy(), gg["iv"].to_numpy() + if td.min() > T2 or td.max() < T2: + continue + w = (ivd / 100.0) ** 2 * td + ivT2 = np.sqrt(np.interp(T2, td, w) / T2) * 100.0 + rows.append((d0, ivT2 - iv0, iv1 - iv0)) + d = pd.DataFrame(rows, columns=["dte", "impl", "real"]) + d = d[(d.dte >= 4) & (d.dte <= 45)] + if len(d) < 50: + print(f" {a} dt={H}h: campione insufficiente ({len(d)})") + continue + b = np.polyfit(d.impl, d.real, 1)[0] + diff = d.real - d.impl + nblk = max(1, len(d) // max(1, H)) # osservazioni ~indipendenti + t = diff.mean() / diff.std() * np.sqrt(nblk) if diff.std() > 0 else 0.0 + print(f" {a} dt={H:3d}h n={len(d):5d} roll implicito medio {d.impl.mean():+.4f} pt-vol " + f"realizzato {d.real.mean():+.4f} beta(real~impl)={b:+.2f} " + f"corr={d.impl.corr(d.real):+.3f} (real-impl) t={t:+.2f}") + if H == 24: + roll_day[a] = -float(d.impl.mean()) # guadagno giornaliero di CHI E' CORTO + print("\n Lettura: se il roll-down fosse incassato, beta(real~impl) sarebbe ~+1. E' NEGATIVO") + print(" (~-1 a una settimana): sul campione la FORMA della curva torna indietro piu' di") + print(" quanto la curva rotoli. Il segno del bias da errore-di-misura sarebbe POSITIVO") + print(" (iv(T) compare in entrambi con segno opposto), quindi non spiega questo.") + TRIALS.append(("Q2: 2 orizzonti x 2 asset (roll implicito vs realizzato)", 4)) + + sub("(b) il costo reale: bid-ask ATM in PUNTI DI VOL, per tenore") + cost = build_atm_cost(chain) + cost["bucket"] = pd.cut(cost.dte, [DTE_MIN, 3, 10, 20, 45, 95], + labels=["1-3g", "3-10g", "10-20g", "20-45g", "45-95g"]) + tab = cost.groupby(["asset", "bucket"], observed=True).agg( + n=("spr_vol", "size"), spread_ptvol=("spr_vol", "median"), + spread_pct_premio=("spr_usd", lambda s: np.nan), + premio_usd=("prem_usd", "median"), vega_usd_per_ptvol=("vega", "median")) + tab["spread_pct_premio"] = (cost.assign(x=cost.spr_usd / cost.prem_usd * 100) + .groupby(["asset", "bucket"], observed=True)["x"].median()) + print(tab.round(3).to_string().replace("\n", "\n ")) + + sub("(c) l'aritmetica che chiude la domanda") + med = cost.groupby(["asset", "bucket"], observed=True)["spr_vol"].median() + for a in ASSETS: + rd = roll_day.get(a, np.nan) + sp = float(med.loc[(a, "10-20g")]) + print(f" {a}: roll-down incassabile da CORTO = {rd:+.4f} pt-vol/giorno | " + f"un round-trip ATM 10-20g costa {sp:.3f} pt-vol") + print(f" => servono {sp/rd:.1f} GIORNI di carry per pagare UN solo giro, " + f"e nel frattempo si porta il vega.") + TRIALS.append(("Q2: 5 secchi di tenore x 2 asset (costo)", 10)) + + sub("(d) e a $635 la domanda non e' nemmeno questa: il LOTTO MINIMO") + for a in ASSETS: + c = cost[(cost.asset == a) & (cost.bucket == "10-20g")] + if c.empty: + continue + prem = float(c.prem_usd.median()) * MIN_LOT[a] + vega = float(c.vega.median()) * MIN_LOT[a] + spot = float(c.spot.median()) + fee = min(OPT_FEE_UNDERLYING * spot, OPT_FEE_CAP_PREMIUM * float(c.prem_usd.median())) * MIN_LOT[a] + rd = roll_day.get(a, np.nan) + carry_g = rd * vega + friz = float(c.spr_vol.median()) * vega + 4 * fee # 1 spread pieno + 4 gambe di fee + print(f" {a}: lotto minimo {MIN_LOT[a]} -> premio ATM 10-20g = ${prem:,.2f} " + f"({prem/CAPITALE:.0%} del conto), vega ${vega:.2f}/pt-vol, fee ${fee:.2f}/gamba") + print(f" carry lordo ${carry_g:+.3f}/giorno per struttura; attrito per giro " + f"${friz:.2f} => {friz/carry_g:.0f} giorni per andare in pari") + print(f"\n Un calendar spread e' DUE gambe: a $635 una sola struttura ETH impegna il") + print(f" ~15-30% del conto e rende centesimi al giorno. BTC e' fuori dal lotto minimo.") + print(f" Il bersaglio dichiarato e' 50 EUR/giorno: qui si parla di due ordini di grandezza sotto.") + TRIALS.append(("Q2: 2 asset x eseguibilita' al lotto minimo", 2)) + + +# =========================================================================================== +# SEZIONE 4 — Q3: gate di rischio sopra TP01. RIDONDANZA PRIMA DELL'UPLIFT. +# =========================================================================================== +def tp01_positions() -> dict: + from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio + tp = TrendPortfolio(**CANONICAL) + out = {} + for a in ASSETS: + df = A.get(a, "1d") + out[a] = pd.Series(tp.target_series(df), + index=pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))) + return out + + +def _anchored(sl_h: pd.Series, px_h: pd.Series, off: int): + """Serie giornaliere (pendenza, ritorno) ancorate all'ora `off`.""" + m = sl_h.index.hour == off + s = sl_h[m] + p = px_h.reindex(s.index).ffill() + r = p.pct_change() + return s, r + + +def sezione4(curve: pd.DataFrame) -> None: + hr("SEZIONE 4 — Q3: la pendenza come GATE DI RISCHIO sopra TP01. RIDONDANZA PRIMA DELL'UPLIFT") + print(" Regola del progetto: 4 gate di de-risk su 4 (DVOL-spike, macro, funding, breadth) sono") + print(" risultati RIDONDANTI col trend. Percio' la ridondanza si misura PRIMA di rivendicare") + print(" qualunque uplift: quanti dei giorni che il gate spegne sono giorni in cui TP01 e' gia' flat?") + pos = tp01_positions() + + sub("(a) il fatto che decide da solo la sezione") + W = pd.concat(pos, axis=1).loc[TS_START:] + flat = (W.abs() < 0.05).mean() + print(f" Nella finestra {TS_START.date()} -> oggi ({len(W)} giorni), TP01 e' FLAT nel " + f"{flat['BTC']:.1%} (BTC) / {flat['ETH']:.1%} (ETH) dei giorni.") + print(f" Esposizione media: BTC {W['BTC'].mean():.3f}, ETH {W['ETH'].mean():.3f} " + f"(target TSMOM risk-off, coerente col libro live dichiarato flat).") + print(" ⚠️ Un gate di de-risk su un libro gia' flat il 93% del tempo NON E' MISURABILE su") + print(" questa finestra: non c'e' rischio da togliere. Questo non e' un difetto del") + print(" gate, e' un difetto del CAMPIONE — e va detto prima di qualunque numero.") + + sub("(b) ridondanza, per quel che il campione consente") + for a in ASSETS: + sl = slope_hourly(curve, a).resample("1D").last().dropna() + j = pd.concat({"slope": sl, "tp": pos[a]}, axis=1, join="inner").dropna() + for qq in (0.10, 0.25, 0.40): + th = j.slope.quantile(qq) + off = j.slope < th + already = float((j.tp[off].abs() < 0.05).mean()) if off.any() else np.nan + print(f" {a} gate OFF sotto p{int(qq*100)} ({th:+.2f} pt-vol): spegne il " + f"{off.mean():.1%} dei giorni; di questi TP01 era GIA' flat nel {already:.1%} " + f"| corr(pendenza, esposizione TP01) = {j.slope.corr(j.tp):+.3f}") + TRIALS.append((f"Q3 {a}: 3 soglie di gate", 3)) + print("\n Il segno di corr(pendenza, esposizione) e' NEGATIVO: la backwardation arriva") + print(" quando TP01 e' gia' uscito. E' la firma esatta dei 4 precedenti.") + + sub("(c) uplift sulla finestra, su TUTTE le 24 ancore (mai su una sola)") + print(" Candidato DIREZIONALE (non gate): contango -> long, backwardation -> flat; vol-target") + print(" 14g, cap 2x, fee 5 bps/lato. Soglia a ZERO = a priori, nessun parametro tarato.") + for a in ASSETS: + sl_h, px_h = slope_hourly(curve, a), px_hourly(a) + + def cand(off, s=sl_h, p=px_h): + s_d, r_d = _anchored(s, p, off) + d = pd.DataFrame({"slope": s_d, "r": r_d}).dropna() + if len(d) < 30: + return pd.Series(dtype=float) + vol = d["r"].rolling(14).std() * np.sqrt(365) + dirn = (d["slope"] > 0).astype(float) + tgt = np.clip(dirn * (0.20 / vol.replace(0, np.nan)), 0, 2.0).fillna(0.0) + hold = tgt.shift(1).fillna(0.0) + return (hold * d["r"] - A.FEE_SIDE * hold.diff().abs().fillna(0.0)).dropna() + + def base(off, p=px_h, s=sl_h): + s_d, r_d = _anchored(s, p, off) + d = pd.DataFrame({"r": r_d}).dropna() + return d["r"] + + band = A.anchor_luck_band(cand, list(range(24)), canonical=0) + print(f" {a}: Sharpe candidato — canonico(00:00) {band['canonical']:+.2f} " + f"(pctl {band['canonical_pctl']:.2f}) | MEDIANA ONESTA {band['median']:+.2f} | " + f"banda [{band['lo']:+.2f}, {band['hi']:+.2f}] | positivo in {band['frac_positive']:.0%} " + f"delle 24 ancore | gate_pass={band['gate_pass']}") + bh = A.anchor_luck_band(base, list(range(24)), canonical=0) + print(f" buy&hold sulla stessa finestra/ancore: mediana {bh['median']:+.2f} " + f"[{bh['lo']:+.2f}, {bh['hi']:+.2f}] <-- il vero termine di paragone in un toro") + dlt = A.anchor_luck_delta(cand, base, list(range(24))) + print(f" mediana delle DIFFERENZE APPAIATE candidato-B&H: {dlt['median_paired']:+.2f} " + f"(positiva in {dlt['n_positive']}/{dlt['n_anchors']} ancore)") + TRIALS.append((f"Q3 {a}: 24 ancore x 2 varianti", 48)) + + sub("(d) null del DE-LEVERING — obbligatorio su ogni claim di minor rischio") + for a in ASSETS: + sl_h, px_h = slope_hourly(curve, a), px_hourly(a) + s_d, r_d = _anchored(sl_h, px_h, 0) + d = pd.DataFrame({"slope": s_d, "r": r_d}).dropna() + vol = d["r"].rolling(14).std() * np.sqrt(365) + tgt = np.clip((d["slope"] > 0).astype(float) * (0.20 / vol.replace(0, np.nan)), 0, 2.0).fillna(0.0) + hold = tgt.shift(1).fillna(0.0) + net = (hold * d["r"] - A.FEE_SIDE * hold.diff().abs().fillna(0.0)).dropna() + dd_c = A._dd_ret(net) + best = None + for k in np.arange(0.05, 1.01, 0.05): + b = k * d["r"].reindex(net.index) + if A._dd_ret(b) <= dd_c: + best = (k, A._sh(b), A._dd_ret(b)) + break + print(f" {a}: candidato Sharpe {A._sh(net):+.2f} maxDD {dd_c:.2%} | " + + (f"buy&hold de-leverato k={best[0]:.2f} -> stesso DD ({best[2]:.2%}) con Sharpe " + f"{best[1]:+.2f} => {'IL CANDIDATO NON SOPRAVVIVE' if best[1] >= A._sh(net) else 'candidato sopra il null'}" + if best else "nessun k<=1 raggiunge quel DD")) + TRIALS.append((f"Q3 {a}: 20 valori di k nel null de-levering", 20)) + + +# =========================================================================================== +# SEZIONE 5 — IL TEST LUNGO. La pendenza ha un PROXY con 5 anni di storia? +# =========================================================================================== +def _proxy_slope(df: pd.DataFrame, asset: str, rvwin: int = 7) -> np.ndarray: + """Proxy A PRIORI della pendenza su storia lunga: DVOL(30g) - RV(rvwin), tutto causale. + + Motivazione, non scelta a posteriori: iv_30 ~ DVOL (corr 0.99, SEZIONE 1) e iv_7 e' in larga + parte una funzione della RV recente (SEZIONE 2c). Il proxy e' quindi la pendenza con la gamba + corta sostituita dalla sua determinante. NON e' la stessa variabile: l'attenuazione e' + misurata sotto e va portata dietro a ogni conclusione. + """ + c = df["close"].to_numpy(float) + r = A.simple_returns(c) + bpd = A.bars_per_day(df) + rv = A.realized_vol(r, max(2, rvwin * bpd), bpd * 365.25) * 100.0 + return A.dvol(df, asset) - rv + + +def make_proxy_dir(long_only: bool = True, thr: float = 0.0): + def fn(df, asset): + p = _proxy_slope(df, asset) + d = np.where(np.isfinite(p), np.where(p > thr, 1.0, 0.0 if long_only else -1.0), 0.0) + return A.vol_target(d, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0) + return fn + + +def sezione5(curve: pd.DataFrame) -> None: + hr("SEZIONE 5 — IL TEST LUNGO: la pendenza ha un proxy con 5 anni di storia?") + print(" Con 74 giorni nessun gate statistico ha potenza. L'unica via per DIRE qualcosa e' un") + print(" proxy a priori su storia lunga — e dichiararne l'attenuazione.") + + sub("(a) quanto e' buono il proxy (misurato sulla finestra vera, non assunto)") + for a in ASSETS: + sl = slope_hourly(curve, a).resample("1D").last().dropna() + df = A.get(a, "1d") + p = pd.Series(_proxy_slope(df, a), + index=pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))).dropna() + j = pd.concat({"vero": sl, "proxy": p}, axis=1, join="inner").dropna() + c = float(j.vero.corr(j.proxy)) + print(f" {a}: corr(pendenza VERA, DVOL-RV7) = {c:+.3f} su n={len(j)} giorni " + f"-> attenuazione: il proxy spiega il {c**2:.0%} della varianza della pendenza") + print(" ⚠️ Un proxy a corr ~0.5-0.7 e' una lente SFOCATA: un risultato NEGATIVO sul proxy") + print(" e' evidenza piu' debole di un negativo sulla variabile vera. Va detto, non nascosto.") + + sub("(b) il candidato direzionale sul proxy, 5 anni, coi gate veri") + fn = make_proxy_dir(long_only=True) + cau = A.causality_ok(fn, tf="1d") + print(f" causality_ok: {cau}") + cd = A.candidate_daily(fn, tf="1d") + era = cd[cd.index >= pd.Timestamp("2021-10-01", tz="UTC")] + print(f" era DVOL (2021-10 -> oggi, {len(era)} giorni): Sharpe {A._sh(era):+.3f} " + f"maxDD {A._dd_ret(era):.2%}") + rep = A.marginal_vs_tp01(cd) + print(f" marginal_vs_tp01 -> {rep.get('marginal_verdict')} | corr a TP01 " + f"{rep.get('corr_full')} | uplift w25 full {rep['blends']['w25']['uplift_full']:+.3f} " + f"hold {rep['blends']['w25']['uplift_hold']}") + for k in ("is_hedge", "has_insample_edge", "robust_oos", "beats_noise_null", "earns_slot"): + if k in rep: + print(f" {k:20s} = {rep[k]}") + imp = A.implausible_sharpe(era) + print(f" implausible_sharpe: flagged={imp.get('flagged')} ({imp.get('reasons')})") + TRIALS.append(("Sez.5: 2 varianti proxy (long-only, L/S) x 1 soglia", 2)) + + sub("(c) il precedente che questo replica") + print(" `dvol_directional.py` (2026-06-29) testo' VRP-Z = z-score causale di (IV-RV) come") + print(" segnale DIREZIONALE su 5 anni, con la griglia completa: verdetto **HEDGE,") + print(" earns_slot=False** — paga solo quando TP01 e' debole, quindi non e' alpha.") + print(" La pendenza della term structure e', su questa lente, la stessa variabile con la") + print(" gamba realizzata sostituita da una implicita. Il filone non e' nuovo: e' quello.") + + +# =========================================================================================== +# SEZIONE 6 — CONTI, GATE, VERDETTO +# =========================================================================================== +def sezione6() -> None: + hr("SEZIONE 6 — GRIGLIA DICHIARATA E DEFLATED SHARPE") + tot = sum(n for _, n in TRIALS) + for name, n in TRIALS: + print(f" {n:5d} {name}") + print(f" {'-'*70}\n {tot:5d} TOTALE (contato al RIALZO: ogni variante valutata, anche scartata)") + print("\n Non e' stata ridotta nessuna griglia per il budget: il vincolo di questo filone") + print(" non e' il tempo di calcolo, e' che il campione dura 74 giorni.") + print("\n deflated_sharpe: NON GIRATO su un candidato di questo studio, e il motivo e' una") + print(" scelta, non una dimenticanza: il DSR e' una correzione per multiple-testing su UNA") + print(" serie di ritorni; qui il candidato migliore ha 74 osservazioni e la sua incertezza") + print(" di CAMPIONE (SE(Sharpe) ~ 2.2) domina di un ordine di grandezza quella da selezione.") + print(f" Deflazionare {tot} trial su 74 giorni darebbe un numero preciso e privo di senso.") + print(" Sul proxy a 5 anni il DSR e' invece pertinente ed e' riportato dal gate del 29/06") + print(" su cui questo filone ricade (VRP-Z: earns_slot=False).") + + +def main() -> None: + hr("r0822_term_structure — TERM STRUCTURE DELLA VOL IMPLICITA BTC/ETH") + print(__doc__.split("METODO.")[0].strip()[:0] or "", end="") + chain = load_quoted_chain() + sezione0(chain) + atm_exp = build_atm_by_expiry(chain) + curve = build_curve(atm_exp) + sezione1(curve) + sezione2(curve) + sezione3(chain, atm_exp, curve) + del chain + sezione4(curve) + sezione5(curve) + sezione6() + hr("FINE") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_vol_size.py b/scripts/research/r0822_vol_size.py new file mode 100644 index 0000000..dc9850a --- /dev/null +++ b/scripts/research/r0822_vol_size.py @@ -0,0 +1,671 @@ +#!/usr/bin/env python +"""r0822_vol_size.py — CONTROLLO DEL RISCHIO SU SKH01 e VOL-TARGET A LIVELLO DI LIBRO. + +DOMANDA (ondata 2026-08-22, filone VOL-SIZE). Il libro live Deribit e' TP01 75% + SKH01 25%, +nettati su un solo strumento. TP01 e' vol-targeted al 20% sulla vol REALIZZATA dell'asset; +**SKH01 non ha alcun controllo del rischio** — `backtest_signals(..., leverage=1.0, +position_size=1.0)`, verificato il 26/07: il suo 20,7% di vol realizzata e' un PRODOTTO della +strategia (uscite % asimmetriche + poco tempo a mercato), non un parametro. E' l'unica delle +cinque gambe senza dimensionamento, la piu' fee-sensibile (~4x TP01) e la piu' fragile +all'ancora (LOO 26/07). + + Q1 dare a SKH01 un dimensionamento del rischio (inverse-vol per-trade / rischio costante per + trade / vol-target sulla sua vol di strategia) migliora il LIBRO, o e' de-levering? + Q2 il vol-target va applicato al LIBRO gia' nettato invece che al singolo sleeve? Oggi ogni + sleeve si dimensiona da solo e poi si nettano: la versione book-level non e' mai stata + misurata. + +COSA NON SI RIFA' (gia' misurato): TP01 x DVOL (26/06, de-levering); il peso 75/25 (confermato +3 volte). I pesi NOMINALI non si toccano; una modulazione di size e' pero' un cambio di pesi nel +tempo, quindi `weights_tilt_null` si riporta lo stesso (§7). + +IPOTESI A PRIORI, REGISTRATA PRIMA DI MISURARE: **de-levering su quasi tutto**. Ragione +dichiarata prima: lo Sharpe e' INVARIANTE a una costante moltiplicativa, quindi una variante che +si limiti ad abbassare la leva media non puo' cambiare lo Sharpe e puo' solo abbassare il DD — +la firma esatta del null del de-levering (5 occorrenze nel progetto). Per VINCERE una variante +deve cambiare la FORMA (quale trade pesa quanto), e la forma si vede solo sullo Sharpe. +Sotto-ipotesi: l'inverse-vol per-trade e' l'unica con una ragione strutturale (lo stop di SKH01 +e' una PERCENTUALE FISSA 4%/2%, quindi il rischio in unita' di sigma varia col regime); il +vol-target sulla serie giornaliera dello sleeve e' quello che ha piu' probabilita' di rompersi +(SKH01 e' ~88% di zeri: la vol trailing di quella serie misura l'ATTIVITA', non il rischio). + +⚠️ DIFETTO TROVATO NELLA MIA PRIMA STESURA, E MISURATO INVECE CHE CANCELLATO (§4). Il modo +ovvio di scrivere un vol-target su uno sleeve — moltiplicare la sua serie GIORNALIERA per +L_t = tv/rv_{t-1} — e' **non causale su SKH01**, perche' la sua equity e' a GRADINO: tutto il +P&L di un trade e' contabilizzato il giorno di CHIUSURA. Scalare quel giorno per L_t significa +aver tenuto size L_t dall'INGRESSO, che e' fino a ~4 giorni prima (24 barre da 230m). L_t usa +solo dati <= t-1 e quindi passa qualunque controllo di causalita' scritto sulla serie +giornaliera: il look-ahead sta nella CONTABILITA', non nella formula. La versione causale fissa +la size all'INGRESSO. Entrambe sono misurate qui, e la differenza e' un risultato. + +LENTE E ANCORA. Path CANONICO (fill al livello, entry a chiusura di bin): e' la lente di tutti i +numeri pubblicati di SKH01 ed e' identica nelle due braccia di ogni confronto — il Delta e' +pulito anche se il livello assoluto e' pessimistico verso il path live (26/07: il live e' +migliore su ingressi E uscite). Banda d'ancora = **23 offset**, la griglia a priori dell'audit +02/07 (ogni 30m su [0,690)); statistica = **mediana delle DIFFERENZE APPAIATE** +(`A.anchor_luck_delta`), mai differenza delle mediane. L'ancora di TP01 e' tenuta canonica in +entrambe le braccia — §6 verifica che il segno non dipenda da lei. + +PRESENTAZIONE. Lo Sharpe e' invariante alla scala, maxDD e CAGR no: ogni variante e' riportata +anche a **ISO-VOL** (costante che pareggia la deviazione standard piena a quella del baseline). +E' una scelta di presentazione, post-hoc e dichiarata; non cambia lo Sharpe, che e' cio' che +decide. Il null del de-levering (`k_iso_dd`, importato da r0822_sol_leg, non riscritto) e' +calcolato per ogni variante che riduce il DD. + +USO: nice -n 19 timeout 900 uv run python scripts/research/r0822_vol_size.py + [--every K] 1 offset ogni K dei 23 (K=1 = tutti; pilota: --every 8) +""" +from __future__ import annotations + +import argparse +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +import altlib as A # noqa: E402 +import r0702_anchor_skh01 as R # noqa: E402 (run_asset/resample_off riusati) +import r0702_tp01_offset as TO # noqa: E402 (TP01 per ancora + target 1h) +from r0822_sol_leg import k_iso_dd # noqa: E402 (null de-levering, non riscritto) +from src.portfolio.portfolio import ( # noqa: E402 + HOLDOUT, combine_outer, weights_tilt_null, +) + +ASSETS = ("BTC", "ETH") +OFFSETS_FULL = tuple(range(0, 690, 30)) # 23 a priori: griglia dell'audit 02/07 +LTF_MIN = 230 +BARS_YR = 365.25 * 24 * 60 / LTF_MIN +DPY = 365.25 +FEE_RT = 0.001 +W_TP, W_SKH = 0.75, 0.25 # libro live Deribit +CAPITAL = 635.0 # conto reale dichiarato nel brief +MIN_ORDER = 5.0 +CAP_ASSET = 0.5 # cap $318/asset su equity $635 +SIG_WARM = 2000 # barre 230m (~1 anno) prima di dimensionare +VT_WARM = 250 # giorni prima di dimensionare su vol di strategia + + +# --------------------------------------------------------------------------- metriche +def sh(s) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + return float(r.mean() / r.std() * np.sqrt(DPY)) if len(r) > 2 and r.std() > 0 else 0.0 + + +def maxdd(s) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + if len(r) < 2: + return 0.0 + eq = np.cumprod(1.0 + r) + pk = np.maximum.accumulate(eq) + return float(np.max((pk - eq) / pk)) + + +def cagr(s) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + if len(r) < 2: + return 0.0 + eq = float(np.prod(1.0 + r)) + yrs = len(r) / DPY + return float(eq ** (1 / yrs) - 1) if eq > 0 and yrs > 0 else -1.0 + + +def vol(s) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + return float(r.std() * np.sqrt(DPY)) if len(r) > 2 else 0.0 + + +def hold(s: pd.Series) -> pd.Series: + return s[s.index >= HOLDOUT] + + +def ins(s: pd.Series) -> pd.Series: + return s[s.index < HOLDOUT] + + +# --------------------------------------------------------------------------- estrazione trade +def extract(asset: str, off: int) -> dict: + """Trade + contesto per (asset, offset). Replica il loop di `backtest_signals` con + leverage=1: stessa detection, stesso non-overlap, stessi fill al livello. Cio' che + aggiunge e' la SIZE per-trade, che l'harness non espone (ha un solo `position_size` + scalare). La §0 verifica bit-exact che con size=1 si ritrovi la serie ufficiale.""" + _, _, ltf, ent = R.run_asset(asset, off) + c = ltf["close"].values.astype(float) + h = ltf["high"].values.astype(float) + lo = ltf["low"].values.astype(float) + n = len(c) + + i_ent, i_ex, dirs, nets, stops = [], [], [], [], [] + busy = -1 + for i in range(n): + e = ent[i] if i < len(ent) else None + if e is None or e.get("dir", 0) == 0 or i <= busy: + continue + d = int(e["dir"]) + entry = c[i] + tp, sl = e.get("tp"), e.get("sl") + mb = int(e.get("max_bars") or 24) + ex_i = min(i + mb, n - 1) + ex_p = c[ex_i] + for j in range(i + 1, min(i + mb + 1, n)): + hit_sl = sl is not None and ((d == 1 and lo[j] <= sl) or (d == -1 and h[j] >= sl)) + hit_tp = tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and lo[j] <= tp)) + if hit_sl: + ex_p, ex_i = sl, j + break + if hit_tp: + ex_p, ex_i = tp, j + break + ex_p, ex_i = c[j], j + i_ent.append(i); i_ex.append(ex_i); dirs.append(d) + nets.append((ex_p - entry) / entry * d - FEE_RT) + stops.append(abs(float(sl) - entry) / entry) # DERIVATO dal segnale, non ridichiarato + busy = ex_i + + idxE = np.asarray(i_ent, int) + lr = A.log_returns(c) + sig, sigref = {}, {} + for w in (30, 90): + sv = A.realized_vol(lr, w, BARS_YR) # finestra che termina in i: causale + sig[w] = sv[idxE] if len(idxE) else np.array([]) + sigref[w] = _expanding_median_at(sv, idxE) + idx = pd.DatetimeIndex(pd.to_datetime(ltf["timestamp"].values, unit="ms", utc=True)) + out = dict( + n=n, idx=idx, i_ent=idxE, i_ex=np.asarray(i_ex, int), dir=np.asarray(dirs, int), + net=np.asarray(nets, float), stop=np.asarray(stops, float), sig=sig, sigref=sigref, + ts_close=ltf["timestamp"].values.astype(np.int64) + LTF_MIN * 60_000, + day_ent=idx[idxE].floor("D") if len(idxE) else pd.DatetimeIndex([], tz="UTC"), + ) + R._CACHE.clear() # ent = 18k dict per chiave: non accumulare + return out + + +def _expanding_median_at(v: np.ndarray, idxE: np.ndarray) -> np.ndarray: + """Mediana ESPANDENTE di v fino a i incluso, valutata solo agli indici d'ingresso. + Causale per costruzione. NaN prima del warm-up.""" + out = np.full(len(idxE), np.nan) + for k, i in enumerate(idxE): + if i < SIG_WARM: + continue + x = v[: i + 1] + x = x[np.isfinite(x)] + if len(x) >= SIG_WARM // 2: + out[k] = float(np.median(x)) + return out + + +def equity_daily(ex: dict, sizes: np.ndarray) -> pd.Series: + """Serie giornaliera dei rendimenti dalla tabella trade, con size per-trade. Convenzione + IDENTICA a `backtest_signals` + `_skyhook_returns`: equity a gradino a fine trade, + resample 1D last, ffill, pct_change.""" + n = ex["n"] + eq = np.full(n, 1000.0, float) + cap = 1000.0 + for k in range(len(ex["i_ent"])): + cap += cap * float(sizes[k]) * float(ex["net"][k]) + cap = max(cap, 1.0) + eq[ex["i_ent"][k]: ex["i_ex"][k] + 1] = cap + for k in range(1, n): # stessi due riempimenti dell'harness + if eq[k] == 1000.0 and eq[k - 1] != 1000.0: + eq[k] = eq[k - 1] + last = 1000.0 + for k in range(n): + if eq[k] != last and eq[k] != 1000.0: + last = eq[k] + else: + eq[k] = last + return pd.Series(eq, index=ex["idx"]).resample("1D").last().ffill().pct_change().dropna() + + +def leg_daily(exs: dict, sizes: dict) -> pd.Series: + """Sleeve SKH01 50/50 BTC+ETH (convenzione di `_skyhook_returns`: inner join).""" + ser = {a: equity_daily(exs[a], sizes[a]) for a in ASSETS} + J = pd.concat(ser, axis=1, join="inner").fillna(0.0) + return pd.Series(0.5 * J[ASSETS[0]].values + 0.5 * J[ASSETS[1]].values, index=J.index) + + +# --------------------------------------------------------------------------- famiglie di size +def size_flat(ex: dict) -> np.ndarray: + return np.ones(len(ex["i_ent"])) + + +def size_iv(p: float, w: int, cap: float): + """INVERSE-VOL per-trade sulla vol dell'ASSET: size = clip((sig_rif/sig_ingresso)^p, 1/cap, cap). + sig_rif = mediana ESPANDENTE causale -> size media ~1: la variante e' neutra alla leva, + quindi qualunque effetto sullo Sharpe e' FORMA, non de-levering.""" + def f(ex: dict) -> np.ndarray: + s, r = ex["sig"][w], ex["sigref"][w] + with np.errstate(divide="ignore", invalid="ignore"): + raw = np.where((s > 0) & np.isfinite(r), (r / np.maximum(s, 1e-12)) ** p, 1.0) + return np.clip(np.nan_to_num(raw, nan=1.0, posinf=cap, neginf=1.0), 1.0 / cap, cap) + return f + + +def size_risk(p: float): + """RISCHIO COSTANTE PER TRADE: size ~ 1/distanza-dallo-stop. Lo stop di SKH01 e' una + percentuale FISSA (4% long / 2% short) -> questa famiglia e' di fatto 'gli short pesano + 2x i long'. La costante e' irrilevante (lo Sharpe e' scale-invariante).""" + def f(ex: dict) -> np.ndarray: + st = np.maximum(ex["stop"], 1e-9) + return np.where(ex["stop"] > 0, (0.04 / st) ** p, 1.0) + return f + + +def _rv_daily(s: pd.Series, w: int, active_only: bool) -> pd.Series: + """Vol realizzata trailing di una serie GIORNALIERA, SFASATA di 1 giorno (causale al giorno t). + active_only=True la calcola sulle sole barre ATTIVE e la riporta sulla griglia di calendario: + SKH01 e' ~88% di zeri, e la versione 'tutte le barre' misura l'ATTIVITA', non il rischio.""" + if not active_only: + return (s.rolling(w, min_periods=max(5, w // 3)).std() * np.sqrt(DPY)).shift(1) + act = s[s != 0.0] + rv = act.rolling(w, min_periods=max(5, w // 3)).std() * np.sqrt(DPY) + return rv.reindex(s.index).ffill().shift(1) + + +def leverage_series(base: pd.Series, tv: float | None, w: int, cap: float, + active_only: bool = False) -> pd.Series: + """L_t da una serie giornaliera di riferimento. tv=None -> versione NEUTRA ALLA LEVA + (rif = mediana espandente causale di rv), tv=float -> vol-target assoluto.""" + rv = _rv_daily(base, w, active_only).replace(0.0, np.nan) + if tv is None: + ref = rv.expanding(min_periods=VT_WARM).median() + L = (ref / rv).clip(lower=1.0 / cap, upper=cap) + else: + L = (tv / rv).clip(lower=0.0, upper=cap) + return L.fillna(1.0) + + +def sizes_from_L(ex: dict, L: pd.Series) -> np.ndarray: + """La size di ogni trade = L al giorno d'INGRESSO. E' la versione CAUSALE del vol-target su + uno sleeve a equity a gradino: L al giorno di CHIUSURA non era nota all'ingresso.""" + if not len(ex["i_ent"]): + return np.array([]) + v = L.reindex(ex["day_ent"]).values.astype(float) + return np.nan_to_num(v, nan=1.0) + + +# --------------------------------------------------------------------------- libro +def book(tp: pd.Series, skh: pd.Series) -> pd.Series: + return combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": W_TP, "SKH01": W_SKH}) + + +def iso_vol(s: pd.Series, ref: pd.Series) -> pd.Series: + v = vol(s) + return s * (vol(ref) / v) if v > 0 else s + + +HEAD = (f" {'variante':<28}{'ShFULL':>8}{'ShHOLD':>9}{'ShIS':>8}" + f"{'vol':>9}{'maxDD':>9}{'DDisovol':>10}{'CAGRiso':>9}{'size med':>10}") + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--every", type=int, default=1) + args = ap.parse_args() + offs = OFFSETS_FULL[:: max(1, args.every)] + t0 = time.time() + + print("=" * 112) + print(" r0822 VOL-SIZE — controllo del rischio su SKH01 (Q1) e vol-target di LIBRO (Q2)") + print("=" * 112) + print(f" ancore: {len(offs)}/{len(OFFSETS_FULL)} offset a priori {list(offs)}") + print(" lente: path CANONICO. Ancora TP01: canonica in ENTRAMBE le braccia (§6 la varia).") + + print("\n estrazione tabelle trade ...", flush=True) + EX = {o: {a: extract(a, o) for a in ASSETS} for o in offs} + print(f" fatto in {time.time()-t0:.0f}s — trade medi/ancora: " + + ", ".join(f"{a} {int(np.mean([len(EX[o][a]['i_ent']) for o in offs]))}" for a in ASSETS)) + + # ------------------------------------------------------------------ §0 SANITY + print("\n" + "-" * 112) + print(" 0. SANITY — con size=1 la ricomposizione DEVE riprodurre la serie ufficiale bit-exact") + print(" (se non e' 0, ogni numero sotto e' di un'altra strategia)") + print("-" * 112) + worst = 0.0 + for o in offs[: min(3, len(offs))]: + for a in ASSETS: + mine = equity_daily(EX[o][a], size_flat(EX[o][a])) + ref = R.run_asset(a, o)[0] + R._CACHE.clear() + assert len(mine) == len(ref), f"len {len(mine)} vs {len(ref)} ({a},{o})" + d = float(np.max(np.abs(mine.values - ref.values))) + worst = max(worst, d) + print(f" {a} off{o:>3}: max|dif| = {d:.2e} ({len(mine)} giorni)") + assert worst < 1e-15, f"ricomposizione NON bit-exact: {worst:.2e}" + if 0 in EX: + from src.portfolio.sleeves import _skyhook_returns + s0 = leg_daily(EX[0], {a: size_flat(EX[0][a]) for a in ASSETS}) + d0 = float(np.max(np.abs(s0.values - _skyhook_returns().values))) + print(f" sleeve 50/50 off0 vs sleeves._skyhook_returns(): max|dif| = {d0:.2e}") + assert d0 < 1e-15 + + # ------------------------------------------------------------------ §1 CAUSALITA' + print("\n" + "-" * 112) + print(" 1. CAUSALITA' — ogni size ricalcolata TRONCANDO i dati alla barra d'ingresso") + print("-" * 112) + ex = EX[offs[0]]["BTC"] + _, _, ltf, _ = R.run_asset("BTC", offs[0]) + c_full = ltf["close"].values.astype(float) + R._CACHE.clear() + full_sz = size_iv(1.0, 30, 3.0)(ex) + sel = [k for k in range(len(ex["i_ent"])) if ex["i_ent"][k] >= SIG_WARM][:40] + bad = 0 + for k in sel: + i = ex["i_ent"][k] + sv_t = A.realized_vol(A.log_returns(c_full[: i + 1]), 30, BARS_YR) + s_t = sv_t[i] + r_t = float(np.median(sv_t[np.isfinite(sv_t)])) + sz_t = np.clip((r_t / s_t) if s_t > 0 else 1.0, 1 / 3.0, 3.0) + bad += int(abs(sz_t - full_sz[k]) > 1e-9) + print(f" IV: {len(sel)-bad}/{len(sel)} size identiche a dati troncati (divergenze {bad})") + assert bad == 0, "la size IV usa dati futuri" + lag = np.array([(ex["idx"][ex["i_ex"][k]] - ex["idx"][ex["i_ent"][k]]).total_seconds() / 86400 + for k in range(len(ex["i_ent"]))]) + print(f" durata dei trade (giorni): mediana {np.median(lag):.2f}, p90 {np.percentile(lag,90):.2f}, " + f"max {lag.max():.2f} <- e' l'ampiezza del look-ahead della versione NAIVE (§4)") + + # ------------------------------------------------------------------ griglia + SZ: dict = {} + for p in (0.5, 1.0): + for w in (30, 90): + for cp in (2.0, 4.0): + SZ[f"IV p{p} w{w} cap{cp:.0f}"] = size_iv(p, w, cp) + for p in (0.5, 1.0): + SZ[f"RISK p{p}"] = size_risk(p) + VTL = {} # vol-target sulla vol di STRATEGIA, causale + for w in (30, 90): + VTL[f"VTL neutro w{w}"] = dict(tv=None, w=w, cap=3.0, active_only=False) + VTL[f"VTL tv20 w{w}"] = dict(tv=0.20, w=w, cap=3.0, active_only=False) + VTL["VTL neutro w90 attive"] = dict(tv=None, w=90, cap=3.0, active_only=True) + VTL["VTL tv20 w90 attive"] = dict(tv=0.20, w=90, cap=3.0, active_only=True) + BVT = {} # Q2: vol-target del LIBRO + for w in (30, 90, 180): + for cp in (2.0, 3.0): + BVT[f"BOOKVT w{w} cap{cp:.0f}"] = dict(tv=0.10, w=w, cap=cp) + NAIVE = {f"NAIVE-VT w{w}": dict(tv=0.20, w=w, cap=3.0, active_only=False) for w in (30, 90)} + ALL = list(SZ) + list(VTL) + list(BVT) + list(NAIVE) + print(f"\n GRIGLIA DICHIARATA: {len(SZ)} size per-trade + {len(VTL)} vol-target di gamba + " + f"{len(BVT)} vol-target di libro + {len(NAIVE)} controlli non-causali = {len(ALL)} celle " + f"(+ baseline).") + + # ------------------------------------------------------------------ serie + TP = A.tp01_baseline_daily() + SLE = {"BASE": {}} + BK = {"BASE": {}} + MTP = {"BASE": {o: 1.0 for o in offs}} # rapporto di vol della gamba TP01 + SIZEMED = {} + for o in offs: + SLE["BASE"][o] = leg_daily(EX[o], {a: size_flat(EX[o][a]) for a in ASSETS}) + BK["BASE"][o] = book(TP, SLE["BASE"][o]) + for nm, fn in SZ.items(): + SLE[nm] = {o: leg_daily(EX[o], {a: fn(EX[o][a]) for a in ASSETS}) for o in offs} + BK[nm] = {o: book(TP, SLE[nm][o]) for o in offs} + MTP[nm] = {o: 1.0 for o in offs} + SIZEMED[nm] = float(np.median(np.concatenate([fn(EX[offs[0]][a]) for a in ASSETS]))) + for nm, sp in VTL.items(): + SLE[nm], BK[nm], MTP[nm] = {}, {}, {} + for o in offs: + sz = {} + for a in ASSETS: + base_a = equity_daily(EX[o][a], size_flat(EX[o][a])) + sz[a] = sizes_from_L(EX[o][a], leverage_series(base_a, **sp)) + SLE[nm][o] = leg_daily(EX[o], sz) + BK[nm][o] = book(TP, SLE[nm][o]) + MTP[nm][o] = 1.0 + if o == offs[0]: + SIZEMED[nm] = float(np.median(np.concatenate(list(sz.values())))) + for nm, sp in BVT.items(): + SLE[nm], BK[nm], MTP[nm] = {}, {}, {} + for o in offs: + L = leverage_series(BK["BASE"][o], sp["tv"], sp["w"], sp["cap"]) + sz = {a: sizes_from_L(EX[o][a], L) for a in ASSETS} + SLE[nm][o] = leg_daily(EX[o], sz) + tp_s = TP * L.reindex(TP.index).ffill().fillna(1.0) + BK[nm][o] = book(tp_s, SLE[nm][o]) + MTP[nm][o] = vol(tp_s) / vol(TP) + if o == offs[0]: + SIZEMED[nm] = float(np.median(np.concatenate(list(sz.values())))) + for nm, sp in NAIVE.items(): # controllo NON causale (vedi §4) + SLE[nm] = {o: SLE["BASE"][o] * leverage_series(SLE["BASE"][o], **sp) for o in offs} + BK[nm] = {o: book(TP, SLE[nm][o]) for o in offs} + MTP[nm] = {o: 1.0 for o in offs} + SIZEMED[nm] = float(np.median(leverage_series(SLE["BASE"][offs[0]], **sp).values)) + + # CONTROLLO ISO-PESO: ogni variante che alza la vol della gamba SKH alza il suo peso EFFETTIVO + # nel libro — e "alzare il peso di SKH01" e' gia' stato misurato (26/07: argmax w=0.35, ma + # `weights_tilt_null` FALLITO e proposta respinta). Quindi il confronto che conta NON e' vs il + # baseline nudo ma vs il baseline RI-SCALATO agli stessi rapporti di vol: cosi' resta solo la + # FORMA (quale trade pesa quanto). E' la regola dell'ISO-RISCHIO gia' codificata (25/07 §3). + CTRL, WEFF = {}, {} + for nm in ALL: + CTRL[nm], WEFF[nm] = {}, {} + for o in offs: + m_s = vol(SLE[nm][o]) / vol(SLE["BASE"][o]) if vol(SLE["BASE"][o]) > 0 else 1.0 + m_t = MTP[nm][o] + CTRL[nm][o] = book(TP * m_t, SLE["BASE"][o] * m_s) + WEFF[nm][o] = W_SKH * m_s / (W_TP * m_t + W_SKH * m_s) + + # ------------------------------------------------------------------ §2 sleeve + can = offs[0] + print("\n" + "-" * 112) + print(f" 2. SLEEVE SKH01 all'ancora canonica (off {can}). 'DDisovol'/'CAGRiso' a vol pareggiata") + print(" col baseline (post-hoc, dichiarato: lo Sharpe non ne dipende).") + print("-" * 112) + print(HEAD) + base_s = SLE["BASE"][can] + + def prow(nm, s, med=None): + iv = iso_vol(s, base_s) + m = f"{med:>10.2f}" if med is not None else f"{'-':>10}" + print(f" {nm:<28}{sh(s):>8.3f}{sh(hold(s)):>9.3f}{sh(ins(s)):>8.3f}{vol(s)*100:>8.1f}%" + f"{maxdd(s)*100:>8.1f}%{maxdd(iv)*100:>9.1f}%{cagr(iv)*100:>8.1f}%{m}") + + prow("BASELINE (size fissa)", base_s, 1.00) + for nm in list(SZ) + list(VTL) + list(NAIVE): + prow(nm, SLE[nm][can], SIZEMED.get(nm)) + + # ------------------------------------------------------------------ §3 libro, banda appaiata + print("\n" + "-" * 112) + print(f" 3. LIBRO 75/25 — mediana delle DIFFERENZE APPAIATE su {len(offs)} ancore, + null del") + print(" de-levering (k<1 sul BASELINE che pareggia il maxDD della variante)") + print("-" * 112) + print(f" {'variante':<28}{'wSKH eff':>9}{'dShFULL':>9}{'pos/n':>8}{'dShHOLD':>9}{'pos/n':>8}" + f"{'| ISO dF':>10}{'pos/n':>8}{'ISO dH':>9}{'pos/n':>8}{'dDDpp':>8}{'k_iso':>8}") + base_b = BK["BASE"] + RES = {} + for nm in ALL: + dF = A.anchor_luck_delta(lambda o, n=nm: BK[n][o], lambda o: base_b[o], offs, metric=sh) + dH = A.anchor_luck_delta(lambda o, n=nm: hold(BK[n][o]), lambda o: hold(base_b[o]), + offs, metric=sh) + iF = A.anchor_luck_delta(lambda o, n=nm: BK[n][o], lambda o, n=nm: CTRL[n][o], + offs, metric=sh) + iH = A.anchor_luck_delta(lambda o, n=nm: hold(BK[n][o]), + lambda o, n=nm: hold(CTRL[n][o]), offs, metric=sh) + dd = float(np.median([maxdd(BK[nm][o]) - maxdd(base_b[o]) for o in offs])) * 100 + if maxdd(BK[nm][can]) < maxdd(base_b[can]) - 1e-6: + k = k_iso_dd(base_b[can], BK[nm][can]) + shk = sh(k * base_b[can]) + ks = f"{k:.3f}" + vd = "DE-LEVERING" if shk >= sh(BK[nm][can]) else "oltre-null" + else: + ks, vd = "n/a", "DD peggiore" + we = float(np.median([WEFF[nm][o] for o in offs])) + RES[nm] = dict(dF=dF["median_paired"], nF=dF["n_positive"], dH=dH["median_paired"], + nH=dH["n_positive"], iF=iF["median_paired"], niF=iF["n_positive"], + iH=iH["median_paired"], niH=iH["n_positive"], dd=dd, k=ks, + verdict=vd, weff=we) + print(f" {nm:<28}{we:>9.3f}{dF['median_paired']:>+9.3f}{dF['n_positive']:>5}/{len(offs):<3}" + f"{dH['median_paired']:>+9.3f}{dH['n_positive']:>5}/{len(offs):<3}" + f"{iF['median_paired']:>+10.3f}{iF['n_positive']:>5}/{len(offs):<3}" + f"{iH['median_paired']:>+9.3f}{iH['n_positive']:>5}/{len(offs):<3}{dd:>+8.2f}{ks:>8}") + print(" wSKH eff = peso EFFETTIVO di SKH nel libro (i nominali restano 75/25: la size lo") + print(" muove). Colonne 'ISO' = confronto vs il baseline RI-SCALATO a quello stesso peso:") + print(" e' li' che si legge la FORMA. Le colonne non-ISO contengono anche il cambio di peso.") + med_F = float(np.median([sh(base_b[o]) for o in offs])) + med_H = float(np.median([sh(hold(base_b[o])) for o in offs])) + med_D = float(np.median([maxdd(base_b[o]) for o in offs])) + print(f"\n libro BASE, ancora canonica: ShFULL {sh(base_b[can]):.3f} / ShHOLD " + f"{sh(hold(base_b[can])):.3f} / maxDD {maxdd(base_b[can])*100:.1f}% / " + f"CAGR {cagr(base_b[can])*100:.1f}%") + print(f" libro BASE, stima ONESTA (mediana della banda): ShFULL {med_F:.3f} / " + f"ShHOLD {med_H:.3f} / maxDD {med_D*100:.1f}%") + + # ------------------------------------------------------------------ §4 il difetto misurato + print("\n" + "-" * 112) + print(" 4. IL DIFETTO MISURATO — vol-target NAIVE (scala il giorno di CHIUSURA) vs CAUSALE") + print(" (size fissata all'INGRESSO). Stessa formula di L, stessa finestra: cambia solo") + print(" QUANDO la si applica. La differenza e' il valore del look-ahead di contabilita'.") + print("-" * 112) + print(f" {'coppia':<34}{'dShFULL naive':>15}{'dShFULL causale':>17}{'phantom':>10}") + for w in (30, 90): + n_nm, c_nm = f"NAIVE-VT w{w}", f"VTL tv20 w{w}" + a1, a2 = RES[n_nm]["dF"], RES[c_nm]["dF"] + print(f" {'vol-target tv20 w%d' % w:<34}{a1:>+15.3f}{a2:>+17.3f}{a1-a2:>+10.3f}") + for w in (30, 90): + n_nm, c_nm = f"NAIVE-VT w{w}", f"VTL tv20 w{w}" + a1, a2 = RES[n_nm]["dH"], RES[c_nm]["dH"] + print(f" {' (hold-out) tv20 w%d' % w:<34}{a1:>+15.3f}{a2:>+17.3f}{a1-a2:>+10.3f}") + + # ------------------------------------------------------------------ §5 selezione + DSR + print("\n" + "-" * 112) + print(" 5. SELEZIONE AL BUIO (solo pre-2025, mediana di banda) + deflated-Sharpe") + print("-" * 112) + CAUSAL = [n for n in ALL if n not in NAIVE] + is_med = {n: float(np.median([sh(ins(BK[n][o])) for o in offs])) for n in CAUSAL} + ho_med = {n: float(np.median([sh(hold(BK[n][o])) for o in offs])) for n in CAUSAL} + fu_med = {n: float(np.median([sh(BK[n][o]) for o in offs])) for n in CAUSAL} + base_is = float(np.median([sh(ins(base_b[o])) for o in offs])) + print(f" baseline in-sample (mediana banda): {base_is:.3f}") + for n in sorted(CAUSAL, key=lambda x: -is_med[x])[:6]: + print(f" {n:<28} IS {is_med[n]:>7.3f} HOLD {ho_med[n]:>7.3f} FULL {fu_med[n]:>7.3f}") + best = max(CAUSAL, key=lambda x: is_med[x]) + print(f"\n cella scelta AL BUIO: {best} (IS {is_med[best]:.3f} vs baseline {base_is:.3f})") + trials = [fu_med[n] for n in CAUSAL] + [med_F] + dsr, sr0 = A.deflated_sharpe(fu_med[best], trials, BK[best][can]) + dsr_b, _ = A.deflated_sharpe(med_F, trials, base_b[can]) + print(f" DSR candidato {dsr:.3f} / DSR BASELINE {dsr_b:.3f} (null max-Sharpe {sr0:.3f}, " + f"N={len(trials)})") + print(f" dispersione degli {len(trials)} trial: sd {np.std(trials, ddof=1):.4f} -> " + f"⚠️ il DSR e' QUASI VACUO su una famiglia di perturbazioni della stessa strategia") + print(" (penalizza in proporzione alla varianza fra i trial: qui il baseline stesso lo") + print(" passa). Il gate che decide e' la banda appaiata del §3, non questo.") + + # ------------------------------------------------------------------ §6 ancora TP01 + print("\n" + "-" * 112) + print(" 6. CONTROLLO — l'ancora di TP01 e' tenuta fissa: il segno del Delta dipende da lei?") + print("-" * 112) + cand6 = [best] + [n for n in (list(SZ)[:0] + ["RISK p1.0"] + list(BVT)[:2]) if n != best] + print(f" {'variante':<28}" + "".join(f"{'TP h=%d' % h:>12}" for h in (0, 8, 16))) + for nm in cand6: + cells = [] + for h in (0, 8, 16): + tph = TO.port_daily_native(h) if h else TP + if nm in BVT: + arm = {} + for o in offs: + L = leverage_series(book(tph, SLE["BASE"][o]), BVT[nm]["tv"], BVT[nm]["w"], + BVT[nm]["cap"]) + sz = {a: sizes_from_L(EX[o][a], L) for a in ASSETS} + arm[o] = book(tph * L.reindex(tph.index).ffill().fillna(1.0), + leg_daily(EX[o], sz)) + else: + arm = {o: book(tph, SLE[nm][o]) for o in offs} + bse = {o: book(tph, SLE["BASE"][o]) for o in offs} + d = A.anchor_luck_delta(lambda o, x=arm: x[o], lambda o, y=bse: y[o], offs, metric=sh) + cells.append(d["median_paired"]) + print(f" {nm:<28}" + "".join(f"{v:>+12.3f}" for v in cells)) + + # ------------------------------------------------------------------ §7 gate + print("\n" + "-" * 112) + print(" 7. GATE — marginal_vs_tp01 / implausible_sharpe / weights_tilt_null") + print("-" * 112) + best_sleeve = max([n for n in list(SZ) + list(VTL)], key=lambda x: is_med[x]) + for nm in ("BASE", best_sleeve): + m = A.marginal_vs_tp01(SLE[nm][can]) + b25 = m.get("blends", {}).get("w25", {}) + print(f" marginal {nm:<26} {m.get('marginal_verdict')} corr {m.get('corr_full')} " + f"uplift w25 full {b25.get('uplift_full')} / hold {b25.get('uplift_hold')}") + nlos = int((EX[can]["BTC"]["net"] < 0).sum() + (EX[can]["ETH"]["net"] < 0).sum()) + ntot = int(len(EX[can]["BTC"]["net"]) + len(EX[can]["ETH"]["net"])) + for nm in ("BASE", best_sleeve): + imp = A.implausible_sharpe(SLE[nm][can], n_trades=ntot, n_losing_trades=nlos) + print(f" implausible {nm:<25} implausible={imp['implausible']} " + f"attive {imp.get('active_frac', 0)*100:.1f}% perdite/attive " + f"{imp.get('loss_frac', 0)*100:.1f}% Calmar {imp.get('calmar', 0):.1f}") + cols = {"TP01": TP, "SKH01": SLE["BASE"][can]} + for nm in [best_sleeve, list(BVT)[0]]: + m = vol(SLE[nm][can]) / vol(SLE["BASE"][can]) + wp = {"TP01": W_TP, "SKH01": W_SKH * m} + g = weights_tilt_null(cols, {"TP01": W_TP, "SKH01": W_SKH}, wp, + caps={"SKH01": 0.5}, floor=0.05, n=300, k_seen=len(ALL)) + print(f" tilt-null {nm:<26} peso equiv SKH {wp['SKH01']/sum(wp.values()):.3f} " + f"d_IS {g['delta_insample']:+.4f} d_HOLD {g['delta_hold']:+.4f} " + f"pctl {g['pctl_hold']:.1f} gate_pass={g['gate_pass']}") + print(" LIMITE DICHIARATO: il gate confronta vettori di pesi STATICI; della modulazione") + print(" cattura la sola componente di SCALA. La FORMA la giudica la banda appaiata (§3).") + + # ------------------------------------------------------------------ §8 eseguibilita' + print("\n" + "-" * 112) + print(f" 8. ESEGUIBILITA' a ${CAPITAL:.0f} — min-order ${MIN_ORDER:.0f}, cap {CAP_ASSET:.0%}" + f" equity/asset, target NETTATO 0.75*TP01 + 0.25*SKH sul grid 1h (cio' che il cron manda)") + print("-" * 112) + print(f" {'configurazione':<28}{'Sh model':>10}{'Sh reale':>10}{'haircut':>9}" + f"{'ordini eseg':>13}{'sotto-min':>11}{'turnover/a':>12}") + todo = [("BASE (size fissa)", None), (best_sleeve, best_sleeve), (list(BVT)[0], list(BVT)[0])] + for label, nm in todo: + tm = tr = tu = 0.0 + no = ns = 0 + for a in ASSETS: + df1h = TO.get1h(a) + tgt = netted_target(a, EX[can][a], nm, SZ, VTL, BVT, SLE, TP, can, df1h) + ev = A.eval_weights_smallcap(df1h, tgt, capital=CAPITAL, min_order=MIN_ORDER) + tm += 0.5 * ev["modeled"]["sharpe"] + tr += 0.5 * ev["realistic"]["sharpe"] + tu += ev["executed_turnover_per_year"] + no += ev["n_executed_trades"] + dw = np.abs(np.diff(np.nan_to_num(tgt), prepend=0.0)) + ns += int(((dw > 1e-12) & (dw * CAPITAL < MIN_ORDER)).sum()) + print(f" {label:<28}{tm:>10.3f}{tr:>10.3f}{tm-tr:>9.3f}{no:>13}{ns:>11}{tu:>12.1f}") + print(" 'ordini eseg' e 'sotto-min' sono sui due asset sommati, su tutta la storia 1h.") + + print("\n" + "=" * 112) + print(f" fatto in {time.time()-t0:.0f}s") + print("=" * 112) + + +def netted_target(asset: str, ex: dict, nm, SZ, VTL, BVT, SLE, TP, can, df1h) -> np.ndarray: + """Peso NETTO per asset sul grid 1h: 0.75*target TP01 + 0.25*posizione SKH (dir*size), poi + l'eventuale leva di libro sulla gamba TP01. E' cio' che il cron manda a Deribit come UNA + posizione netta sullo stesso strumento.""" + tp = np.asarray(TO.hourly_target(asset, 0), float) + ts1 = df1h["timestamp"].values.astype(np.int64) + 3_600_000 + pos = np.zeros(len(ts1)) + L1h = np.ones(len(ts1)) + if nm is None: + sizes = size_flat(ex) + elif nm in SZ: + sizes = SZ[nm](ex) + elif nm in VTL: + base_a = equity_daily(ex, size_flat(ex)) + sizes = sizes_from_L(ex, leverage_series(base_a, **VTL[nm])) + else: # BOOKVT + L = leverage_series(book(TP, SLE["BASE"][can]), BVT[nm]["tv"], BVT[nm]["w"], BVT[nm]["cap"]) + sizes = sizes_from_L(ex, L) + idx1h = pd.DatetimeIndex(pd.to_datetime(df1h["timestamp"].values, unit="ms", utc=True)) + L1h = L.reindex(idx1h.floor("D")).ffill().fillna(1.0).values + t_in, t_out = ex["ts_close"][ex["i_ent"]], ex["ts_close"][ex["i_ex"]] + for k in range(len(t_in)): + j0 = int(np.searchsorted(ts1, t_in[k], side="left")) + j1 = int(np.searchsorted(ts1, t_out[k], side="left")) + pos[j0:j1] = ex["dir"][k] * sizes[k] + return np.clip(W_TP * tp * L1h + W_SKH * pos, -CAP_ASSET, CAP_ASSET) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_vrp_real_quotes.py b/scripts/research/r0822_vrp_real_quotes.py new file mode 100644 index 0000000..d53e924 --- /dev/null +++ b/scripts/research/r0822_vrp_real_quotes.py @@ -0,0 +1,710 @@ +"""R0822 VRP-QUOTE-VERE — esiste una struttura di reddito in opzioni ESEGUIBILE a $600-3.000 +che sopravvive a fee reali, spread reali e lotto minimo? + +NON e' "rifare VRP01". La domanda e' se la regola dura del progetto — *niente short-vol da +MODELLO in deploy* (19/06, riconfermata 3 volte) — si possa sostituire con *short-vol da QUOTE*, +che e' un'altra cosa: il 30/07 il premio era ancora prezzato Black-Scholes su DVOL ATM e la +misura sulle quote reali dava f = 0.73 del modello, con il difetto NON nella gamba venduta +(f_short 1.01) ma nell'ALA che si compra (f_long 2.23). Da allora la condizione e' cambiata: +la catena vera la raccogliamo noi, oraria, per strike, con bid/ask. + +ORDINE DELLE DOMANDE (deliberato). L'eseguibilita' viene PRIMA del rendimento — regola del +30/07: "prima di misurare il rendimento a un capitale dato, misurare il lotto minimo del venue". +Se non si esegue, il resto e' accademia. + + §1 LIMITE DI REGIME — risultato di prima riga, non nota a pie' di pagina + §2 IL CAMPIONE — quante settimane ci sono DAVVERO, e perche' + §3 ESEGUIBILITA' — lotti, famiglie, e su quale delle due si esegue + §4 BACKTEST SU QUOTE REALI — griglia dichiarata, fill al bid/ask, fee di listino + §5 IL RISCHIO STA NEL MINIMO — non nelle chiusure settimanali + §6 GATE — inclusi quelli che NON si possono far girare + §7 VERDETTO + +CONVENZIONI CHE DECIDONO IL RISULTATO (dichiarate qui, non sepolte nel codice) + (a) FILL: si vende al BID e si compra all'ASK, sempre. Il mid e' il modello sotto mentite + spoglie e compare solo come diagnostica, mai come cella selezionabile. + (b) FEE: listino Deribit vero — `min(0.03% del sottostante, 12.5% del premio della SINGOLA + opzione)` per gamba, piu' la fee di consegna sulle gambe ITM a scadenza. Lo sleeve usa + invece un forfait del 12.5% del credito NETTO: qui si misura di quanto sbaglia. + (c) SETTLEMENT: indice reale alla scadenza dal feed certificato. Deribit regola sulla media + dell'indice nei 30 minuti prima delle 08:00 UTC; qui si usa la barra 1h di chiusura — + approssimazione dichiarata, non nascosta. + (d) CAPITALE IMPEGNATO: il denominatore e' il rischio massimo della struttura + `(K_short - K_long) x lotto - credito`. E' il limite INFERIORE del capitale richiesto; + quello vero dipende dal regime di margine del conto e NON e' leggibile da dati pubblici. + Riportato anche il limite superiore (cash-secured, la convenzione del sleeve). + +RAM: la catena si legge filtrata in LETTURA (solo put, solo le colonne che servono) con +pyarrow.dataset — `bite_archive.parquet` intero esplode in memoria e ucciderebbe gli altri agenti. + + nice -n 19 timeout 900 uv run python scripts/research/r0822_vrp_real_quotes.py + ... --no-live # salta le letture dal venue (usa la cache se c'e') +""" +from __future__ import annotations + +import argparse +import json +import sys +import time +import urllib.request +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +import altlib as A # noqa: E402 +import cblib as CB # noqa: E402 + +CHAIN_DIR = ROOT / "data" / "raw" / "cb_chain" +SNAP_JSONL = ROOT / "data" / "options_daily" / "snapshots.jsonl" +CACHE = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/r0822_venue.json") + +# --- listino Deribit (fonte: campo `taker_commission` letto dal venue + schema fee pubblico) --- +FEE_UNDERLYING = 0.0003 # 0.03% del sottostante per gamba, per contratto +FEE_PREMIUM_CAP = 0.125 # cap: 12.5% del premio della SINGOLA opzione +FEE_DELIVERY = 0.00015 # 0.015% del sottostante sulle gambe ITM a scadenza +SLEEVE_FEE_FRAC = 0.125 # forfait dello sleeve: 12.5% del credito NETTO + +DTE_LO, DTE_HI, DTE_TARGET = 4.0, 10.0, 7.0 +WEEKS_PER_YEAR = 52.0 + +# ---------------------------------------------------------------- griglia DICHIARATA +# Si conta al RIALZO: ogni variante provata e' un trial, anche quelle scartate. +SHORT_DELTAS = (-0.35, -0.28, -0.20, -0.12) +LONG_DELTAS = (-0.20, -0.10, -0.05) +CELLS = [(s, l) for s in SHORT_DELTAS for l in LONG_DELTAS if abs(l) < abs(s)] +FILLS = ("bid_ask", "mid") # `mid` = diagnostica, NON selezionabile +ASSETS = ("BTC", "ETH") +CANON = (-0.28, -0.10) # la cella di VRP01 + + +def hr(t: str = "", ch: str = "=") -> None: + print("\n" + ch * 100) + if t: + print(t) + print(ch * 100) + + +# ================================================================== dati + + +def load_puts() -> pd.DataFrame: + """Solo le PUT, solo le colonne che servono, filtrate in LETTURA.""" + import pyarrow.dataset as ds + + cols = ["ts", "asset", "instrument_name", "strike", "option_type", "exp", + "bid", "ask", "mid", "iv", "delta", "open_interest", "quote_status"] + d = ds.dataset(str(CHAIN_DIR), format="parquet") + df = d.to_table(columns=cols, filter=(ds.field("option_type") == "P")).to_pandas() + df["ts"] = pd.to_datetime(df["ts"], utc=True).dt.as_unit("ns") + df["exp"] = pd.to_datetime(df["exp"], utc=True) + df["dte"] = (df["exp"] - df["ts"]).dt.total_seconds() / 86400.0 + return df.drop_duplicates(subset=["ts", "instrument_name"], keep="last").reset_index(drop=True) + + +def venue_read(live: bool) -> dict: + """Specifiche e liquidita' lette DAL VENUE. Un fallimento si dichiara, non si inventa.""" + if not live and CACHE.exists(): + return json.loads(CACHE.read_text()) + + def api(path, **kw): + q = "&".join(f"{k}={v}" for k, v in kw.items()) + with urllib.request.urlopen( + f"https://www.deribit.com/api/v2/public/{path}?{q}", timeout=25) as r: + return json.load(r)["result"] + + out = {"ok": False, "ts": pd.Timestamp.now(tz="UTC").isoformat()} + try: + out["index"] = {c: api("get_index_price", index_name=f"{c}_usd")["index_price"] + for c in ("btc", "eth")} + specs, summ = {}, [] + for cur in ("BTC", "ETH", "USDC"): + for i in api("get_instruments", currency=cur, kind="option", expired="false"): + fam = i["instrument_name"].split("-")[0] + if fam not in specs: + specs[fam] = {k: i.get(k) for k in + ("min_trade_amount", "contract_size", "settlement_currency", + "quote_currency", "taker_commission", "base_currency")} + summ += api("get_book_summary_by_currency", currency=cur, kind="option") + time.sleep(0.4) + out["specs"] = specs + out["summary"] = [{k: r.get(k) for k in + ("instrument_name", "bid_price", "ask_price", "mid_price", + "mark_price", "mark_iv", "open_interest", "volume", + "underlying_price")} for r in summ] + out["ok"] = True + CACHE.parent.mkdir(parents=True, exist_ok=True) + CACHE.write_text(json.dumps(out)) + except Exception as exc: # noqa: BLE001 + out["error"] = f"{type(exc).__name__}: {exc}" + if CACHE.exists(): + cached = json.loads(CACHE.read_text()) + cached["stale"] = out["error"] + return cached + return out + + +def specs_from_disk() -> dict: + """Le specifiche gia' registrate dal progetto (`data/options_daily/snapshots.jsonl`). + Serve come controllo indipendente della lettura live: due percorsi, stesso numero.""" + mins, fees, last_ts = {}, {}, {} + if not SNAP_JSONL.exists(): + return {} + with SNAP_JSONL.open() as f: + for line in f: + try: + r = json.loads(line) + except Exception: # noqa: BLE001, S112 + continue + if r.get("rec") != "chain": + continue + c = r["currency"] + mins.setdefault(c, set()).add(r.get("min_trade_amount")) + fees.setdefault(c, set()).add(r.get("taker_comm")) + last_ts[c] = r["snap_ts"] + return {c: {"min": sorted(x for x in mins[c] if x is not None), + "taker": sorted(x for x in fees[c] if x is not None), + "ts": pd.Timestamp(last_ts[c], unit="ms", tz="UTC")} for c in mins} + + +# ================================================================== struttura + + +def leg_fee_usd(prem_base: float, spot: float) -> float: + """Fee di listino per UNA gamba, per contratto (1 unita' di sottostante), in USD.""" + return min(FEE_UNDERLYING * spot, FEE_PREMIUM_CAP * prem_base * spot) + + +def entry_snapshots(puts: pd.DataFrame, asset: str, offset_h: float = 0.0) -> dict: + """Per ogni scadenza settimanale: lo snapshot piu' vicino a (7 giorni + offset) dalla scadenza. + + L'offset e' l'ANCORA: entrare alle 08:00 del venerdi' precedente e' UNA delle scelte possibili, + e questo progetto ha misurato 4 volte che l'ancora e' un max-of-k non dichiarato. + """ + p = puts[(puts["asset"] == asset) & (puts["dte"] >= DTE_LO) & (puts["dte"] <= DTE_HI)] + tgt = DTE_TARGET + offset_h / 24.0 + out = {} + for exp, g in p.groupby("exp"): + ts_all = np.array(sorted(g["ts"].unique())) + if not len(ts_all): + continue + dte = (exp - pd.DatetimeIndex(ts_all)).total_seconds() / 86400.0 + ok = (dte >= DTE_LO) & (dte <= DTE_HI) + if not ok.any(): + continue + cand = ts_all[ok][np.argsort(np.abs(dte[ok] - tgt))] + for ts in cand[:6]: # al piu' 6 tentativi per scadenza + snap = g[g["ts"] == ts] + if snap["bid"].notna().sum() >= 2: + out[exp] = (pd.Timestamp(ts), snap) + break + return out + + +def build_trades(puts: pd.DataFrame, asset: str, short_d: float, long_d: float, + fill: str = "bid_ask", offset_h: float = 0.0, + lot: float = 1.0, today: pd.Timestamp | None = None) -> pd.DataFrame: + """Un trade per scadenza: ingresso su quote REALI, tenuto a scadenza, regolato sull'indice.""" + today = today or pd.Timestamp.now(tz="UTC") + S = CB.spot_series(asset) + V = CB.dvol_series(asset) + rows, rejected = [], {"gambe_mancanti": 0, "credito_non_positivo": 0} + + for exp, (ts, snap) in entry_snapshots(puts, asset, offset_h).items(): + if exp >= today: # non ancora regolata: non e' un trade + continue + legs = CB.pick_legs(snap, short_d, long_d) + if legs is None: + rejected["gambe_mancanti"] += 1 + continue + spot = float(S.asof(ts)) + dte = (exp - ts).total_seconds() / 86400.0 + dvol = float(V.asof(ts)) + hist = V[V.index < ts] + ivr = float((hist < dvol).mean()) if len(hist) else np.nan + + if fill == "bid_ask": + ps, pl = legs["bid_short"], legs["ask_long"] # vendi al bid, compra all'ask + else: + ps, pl = legs["mid_short"], legs["mid_long"] + if not (np.isfinite(ps) and np.isfinite(pl)): + rejected["gambe_mancanti"] += 1 + continue + cred = (ps - pl) * spot # USD per 1 unita' di sottostante + if cred <= 0: + # premio non monotono nello strike (difetto certificato di `certify_cb_chain`): + # una put piu' OTM quotata sopra una meno OTM. Non e' una struttura, e' una quota rotta. + rejected["credito_non_positivo"] += 1 + continue + + ST = float(S.asof(exp)) + payoff = max(legs["k_short"] - ST, 0.0) - max(legs["k_long"] - ST, 0.0) + fee = leg_fee_usd(ps, spot) + leg_fee_usd(pl, spot) + for K in (legs["k_short"], legs["k_long"]): # consegna sulle gambe ITM + intr = max(K - ST, 0.0) + if intr > 0: + fee += min(FEE_DELIVERY * ST, FEE_PREMIUM_CAP * intr) + + width = legs["k_short"] - legs["k_long"] + mod = CB.f_factors(legs, spot, dvol / 100.0, dte) + rows.append(dict( + asset=asset, ts=ts, exp=exp, dte=dte, spot=spot, ST=ST, + inst_short=legs["inst_short"], inst_long=legs["inst_long"], + cred_real=cred, # per 1 unita': lo vuole CB.close_cost_path + k_short=legs["k_short"], k_long=legs["k_long"], width=width, + d_short=legs["d_short"], d_long=legs["d_long"], + cred=cred * lot, payoff=payoff * lot, fee=fee * lot, + pnl=(cred - payoff - fee) * lot, + fee_sleeve=SLEEVE_FEE_FRAC * mod["cred_mod"] * lot, + fee_sleeve_su_reale=SLEEVE_FEE_FRAC * cred * lot, + cred_mod=mod["cred_mod"] * lot, + risk=width * lot - cred * lot, + dvol=dvol, ivrank=ivr, f_net=mod["f_net"], + und_ret=ST / spot - 1.0, + )) + df = pd.DataFrame(rows) + if not df.empty: + df = df.sort_values("exp").reset_index(drop=True) + df.attrs["rejected"] = rejected + return df + + +def stats(tr: pd.DataFrame) -> dict: + """Metriche di una serie di trade settimanali. Il denominatore e' il RISCHIO impegnato.""" + if tr.empty or len(tr) < 2: + return dict(n=len(tr), sharpe=np.nan, mean=np.nan, t=np.nan) + r = (tr["pnl"] / tr["risk"]).to_numpy(float) # ritorno sul capitale a rischio + sd = float(np.std(r, ddof=1)) + m = float(np.mean(r)) + eq = np.cumprod(1.0 + r) + dd = float(np.max((np.maximum.accumulate(eq) - eq) / np.maximum.accumulate(eq))) + return dict(n=len(tr), mean=m, sd=sd, + sharpe=(m / sd * np.sqrt(WEEKS_PER_YEAR)) if sd > 0 else np.nan, + t=(m / (sd / np.sqrt(len(r)))) if sd > 0 else np.nan, + maxdd_close=dd, wins=int((tr["pnl"] > 0).sum()), + usd_week=float(tr["pnl"].mean()), usd_tot=float(tr["pnl"].sum())) + + +def weekly_series(tr: pd.DataFrame) -> pd.Series: + return pd.Series((tr["pnl"] / tr["risk"]).to_numpy(float), + index=pd.DatetimeIndex(tr["exp"])).sort_index() + + +def boot_ci(x: np.ndarray, n: int = 4000, seed: int = 822) -> tuple[float, float]: + rng = np.random.default_rng(seed) + if len(x) < 2: + return (np.nan, np.nan) + bs = rng.choice(x, size=(n, len(x)), replace=True).mean(axis=1) + return float(np.percentile(bs, 2.5)), float(np.percentile(bs, 97.5)) + + +# ================================================================== main + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--no-live", action="store_true", help="non interrogare il venue") + args = ap.parse_args() + + print(__doc__.split("\n\n")[0]) + puts = load_puts() + today = pd.Timestamp.now(tz="UTC") + venue = venue_read(not args.no_live) + + # ------------------------------------------------------------ §1 REGIME + hr("§1 LIMITE DI REGIME — il campione descrive il regime in cui la strategia sta FLAT") + print("Non e' una nota a pie' di pagina: VRP01 entra SOLO se IV-rank > 0.30 (il gate e' l'alpha,\n" + "misurato 4 volte). Se nel campione quel gate non scatta mai, il campione non contiene\n" + "la strategia — contiene la sua astensione.\n") + for a in ASSETS: + V = CB.dvol_series(a) + win = V[(V.index >= puts["ts"].min()) & (V.index <= puts["ts"].max())] + pct = float((V[V.index < win.index[0]].values[:, None] < win.values).mean()) + print(f" {a}: DVOL nel campione min {win.min():.1f} / mediana {win.median():.1f} / " + f"max {win.max():.1f} → percentile mediano vs storia 2021+ = {pct:.0%}") + + # ------------------------------------------------------------ §2 CAMPIONE + hr("§2 IL CAMPIONE — quante settimane ci sono DAVVERO") + pre = puts[puts["ts"] < pd.Timestamp("2026-06-09", tz="UTC")] + print(f" catena su disco: {puts['ts'].min():%Y-%m-%d} → {puts['ts'].max():%Y-%m-%d} " + f"({(puts['ts'].max() - puts['ts'].min()).days} giorni, {len(puts):,} righe put)") + if not pre.empty: + print(f" ma PRIMA del 2026-06-09 l'archivio ereditato seguiva UNA sola scadenza, in " + f"finestra {pre['dte'].min():.0f}-{pre['dte'].max():.0f} DTE:") + print(f" → la finestra settimanale {DTE_LO:.0f}-{DTE_HI:.0f} DTE NON ESISTE prima " + f"della raccolta full-chain.") + print("\n Trade REGOLATI utilizzabili (cella canonica VRP01 δ-0.28/-0.10, fill bid/ask):") + canon: dict[str, pd.DataFrame] = {} + for a in ASSETS: + lot = 1.0 + tr = build_trades(puts, a, *CANON, lot=lot, today=today) + canon[a] = tr + rej = tr.attrs.get("rejected", {}) if not tr.empty else {} + print(f" {a}: {len(tr)} settimane regolate ({tr['exp'].min():%Y-%m-%d} → " + f"{tr['exp'].max():%Y-%m-%d}) scartate: {rej}") + print("\n ⚠️ «3,7 mesi ≈ 16 scadenze settimanali» e' FALSO su questi dati: sono 10 per asset.") + + print("\n Gate IV-rank>0.30 di VRP01 sulle settimane utilizzabili:") + tot = passed = 0 + for a in ASSETS: + tr = canon[a] + n = int((tr["ivrank"] > 0.30).sum()) + tot += len(tr) + passed += n + print(f" {a}: {n}/{len(tr)} passano (IV-rank min {tr['ivrank'].min():.3f} / " + f"max {tr['ivrank'].max():.3f})") + print(f" TOTALE: {passed}/{tot}. Il sleeve sarebbe stato FLAT per l'intero campione.") + + ret = {a: (canon[a]["ST"].iloc[-1] / canon[a]["spot"].iloc[0] - 1.0) for a in ASSETS} + print(f"\n E il sottostante nel campione: BTC {ret['BTC']:+.1%}, ETH {ret['ETH']:+.1%} — " + f"vol implicita bassa E rialzo forte\n = il regime IDEALE per vendere put. " + f"Il campione e' il migliore possibile per questa struttura, non uno neutro.") + + # ------------------------------------------------------------ §3 ESEGUIBILITA' + hr("§3 ESEGUIBILITA' — e qui il filone cambia domanda") + fams = sorted({n.split("-")[0] for n in puts["instrument_name"].head(50_000)}) + print(f" La catena che raccogliamo contiene le famiglie: {fams}") + print(" → sono le opzioni INVERSE: quotate, marginate e regolate in BTC/ETH.") + print(" Il conto e' in USDC ($635) e il book esegue sui perpetual LINEARI USDC.\n") + + disk = specs_from_disk() + if disk: + print(" Specifiche gia' registrate dal progetto (data/options_daily/snapshots.jsonl):") + for c, v in sorted(disk.items()): + print(f" {c:6s} min_trade_amount={v['min']} taker={v['taker']} ({v['ts']:%Y-%m-%d %H:%M})") + + if venue.get("ok"): + idx = venue["index"] + print(f"\n Lette dal venue ora — indice BTC ${idx['btc']:,.0f} / ETH ${idx['eth']:,.0f}:") + print(f" {'famiglia':11s} {'min lotto':>10s} {'settle':>7s} {'quote':>7s} " + f"{'$ nozionale/lotto':>18s}") + for fam in ("BTC", "ETH", "BTC_USDC", "ETH_USDC"): + s = venue["specs"].get(fam) + if not s: + continue + base = (s.get("base_currency") or fam.split("_")[0]).lower() + notion = float(s["min_trade_amount"]) * idx[base] + print(f" {fam:11s} {s['min_trade_amount']:>10} {str(s['settlement_currency']):>7s} " + f"{str(s['quote_currency']):>7s} {notion:>17,.0f}$") + + print("\n 📌 Esiste una famiglia USDC-lineare per BTC ed ETH, con lotto minimo 10x PIU'") + print(" PICCOLO e regolata nella valuta del conto. Il muro «BTC fuori a $3.000» del") + print(" 30/07 — congelato in tests/test_vrp_profit_take.py — e' stato misurato sulla") + print(" famiglia che il conto NON margina.") + print(" E' la 3a occorrenza della stessa forma dopo fee_watch (21/08, sorvegliava gli") + print(" INVERSE mentre il book trada i LINEARI) e la taratura di venue_watch.") + + # --- ma la famiglia eseguibile e' quotata davvero? --- + d = pd.DataFrame(venue["summary"]) + parts = d["instrument_name"].str.split("-") + d["fam"] = parts.str[0] + d["typ"] = parts.str[3] + d["strike"] = pd.to_numeric(parts.str[2].str.replace("d", ".", regex=False), errors="coerce") + d["expd"] = pd.to_datetime(parts.str[1], format="%d%b%y", utc=True) + pd.Timedelta(hours=8) + d["dte"] = (d["expd"] - today).dt.total_seconds() / 86400.0 + d["base"] = d["fam"].str.replace("_USDC", "", regex=False).str.lower() + d = d[d["typ"].eq("P") & d["base"].isin(["btc", "eth"])].copy() + d["S"] = d["base"].map(idx) + d["mny"] = d["strike"] / d["S"] + q = d[(d["bid_price"] > 0) & (d["ask_price"] > 0)].copy() + q["relsp"] = (q["ask_price"] - q["bid_price"]) / q["mid_price"] + # regione delle put OTM usate dalla struttura (δ -0.35..-0.05 ≈ moneyness 0.78-0.97) + w = q[(q["dte"] > 3) & (q["dte"] < 11) & q["mny"].between(0.78, 0.97)] + print("\n Liquidita' REALE nella regione di strike che serve (scadenza settimanale, ora):") + print(f" {'famiglia':11s} {'n quotate':>9s} {'spread rel. med':>16s} " + f"{'p75':>7s} {'OI med':>9s} {'vol 24h':>10s}") + for fam, g in w.groupby("fam"): + allf = d[d["fam"] == fam] + print(f" {fam:11s} {len(g):>9d} {g['relsp'].median():>15.0%} " + f"{g['relsp'].quantile(.75):>6.0%} {g['open_interest'].median():>9.1f} " + f"{allf['volume'].sum():>10,.0f}") + print("\n Lo spread relativo e' ENORME in entrambe le famiglie (il mid non e' un prezzo\n" + " eseguibile: e' la media di due prezzi lontani), e sulla famiglia USDC — l'unica\n" + " marginabile dal conto — l'open interest e' ~0: quotata da un market maker,\n" + " praticamente non tradata. Comprare l'ala li' dentro costa il doppio.") + else: + print(f"\n ⚠️ LETTURA DAL VENUE NON RIUSCITA: {venue.get('error', 'n/d')}") + print(" Le specifiche delle famiglie USDC restano NON MISURATE in questo giro.") + + print("\n Capitale per UN lotto minimo, cella canonica (limiti dichiarati, vedi docstring):") + print(f" {'asset':6s} {'lotto':>7s} {'width med':>10s} {'rischio max':>12s} " + f"{'cash-secured':>13s} {'credito/sett':>13s}") + minlot = {"BTC": 0.1, "ETH": 1.0} + if venue.get("ok"): + for fam in ("BTC", "ETH"): + if fam in venue["specs"]: + minlot[fam] = float(venue["specs"][fam]["min_trade_amount"]) + for a in ASSETS: + tr, lot = canon[a], minlot[a] + wid = float(tr["width"].median()) + print(f" {a:6s} {lot:>7.2f} {wid:>10,.0f} {wid * lot - tr['cred'].median() * lot:>11,.0f}$ " + f"{tr['k_short'].median() * lot:>12,.0f}$ {tr['cred'].median() * lot:>12,.2f}$") + print(" Sul rischio DEFINITO un lotto minimo inverse sta dentro $600. Sul cash-secured no.\n" + " Quale dei due valga dipende dal regime di margine del conto, che NON e' leggibile\n" + " da dati pubblici: si legge sul conto. E' un limite di questa misura, non un dettaglio.") + + # ------------------------------------------------------------ §4 BACKTEST + hr("§4 BACKTEST SU QUOTE REALI — griglia dichiarata") + n_trials = len(CELLS) * len(FILLS) * (len(ASSETS) + 1) + print(f" celle δ (short,long): {len(CELLS)} fill: {len(FILLS)} " + f"asset: {len(ASSETS)} + combinato 50/50") + print(f" → TRIAL DICHIARATI AL RIALZO: {n_trials}. Nessuna griglia ridotta per budget.") + print(f" (Il fill `mid` non e' selezionabile — e' la diagnostica di quanto costa lo spread —\n" + f" ma conta come trial: e' stato guardato.)\n") + + grid = [] + for fill in FILLS: + for (sd, ld) in CELLS: + per, comb = {}, [] + for a in ASSETS: + tr = build_trades(puts, a, sd, ld, fill=fill, lot=1.0, today=today) + per[a] = stats(tr) + if not tr.empty: + comb.append(weekly_series(tr)) + row = dict(fill=fill, sd=sd, ld=ld) + for a in ASSETS: + row[f"{a}_sh"] = per[a]["sharpe"] + row[f"{a}_n"] = per[a]["n"] + if comb: + c = pd.concat(comb, axis=1).mean(axis=1).dropna() + row["comb_sh"] = (float(c.mean() / c.std(ddof=1) * np.sqrt(WEEKS_PER_YEAR)) + if c.std(ddof=1) > 0 else np.nan) + row["comb_n"] = len(c) + grid.append(row) + G = pd.DataFrame(grid) + print(G.round(2).to_string(index=False)) + + print("\n ⚠️ COME SI LEGGE LA TABELLA SOPRA. Gli 'Sharpe' a due cifre NON sono un risultato:\n" + " sono la firma del campione. Annualizzare (x√52) 10 osservazioni settimanali in cui\n" + " la coda sinistra non si e' manifestata produce numeri privi di significato — e il\n" + " fatto che l'INTERA griglia li produca e' esattamente il segnale, non un caso da\n" + " selezionare. E' la 4a occorrenza della firma '0-perdite / Sharpe implausibile'\n" + " dopo CC01, le celle deep-OTM Albimarini e meta' della griglia VRP del 03/07.\n" + " Per lo stesso motivo NON viene riportato un CAGR: comporre 10 settimane a 52\n" + " sarebbe una fabbricazione, non una stima.") + + print("\n Cella canonica di VRP01 (δ-0.28/-0.10), fill conservativo, per asset — numeri\n" + " CONCRETI (dollari per lotto minimo), che a questa taglia dicono piu' di un rapporto:") + print(f" {'asset':6s} {'n':>3s} {'vinte':>7s} {'$/sett':>9s} {'$ tot':>9s} " + f"{'% rischio/sett':>15s} {'t':>6s} {'maxDD chiusure':>15s}") + st = {} + for a in ASSETS: + tr = canon[a].copy() + tr[["cred", "payoff", "fee", "pnl", "risk", "cred_mod", + "fee_sleeve", "fee_sleeve_su_reale"]] *= minlot[a] + st[a] = stats(tr) + s = st[a] + print(f" {a:6s} {s['n']:>3d} {s['wins']:>3d}/{s['n']:<3d} {s['usd_week']:>8.2f}$ " + f"{s['usd_tot']:>8.2f}$ {s['mean']:>14.2%} {s['t']:>6.2f} {s['maxdd_close']:>14.1%}") + print(" maxDD 0.0% su BTC significa che NON C'E' STATA una settimana in perdita: e' una\n" + " proprieta' del campione, non della struttura.") + + print("\n Costo dello SPREAD, misurato (stesso ingresso, fill al mid vs al bid/ask):") + for a in ASSETS: + ba = build_trades(puts, a, *CANON, fill="bid_ask", today=today) + md = build_trades(puts, a, *CANON, fill="mid", today=today) + j = ba.merge(md, on="exp", suffixes=("_ba", "_md")) # appaiato per INGRESSO + if j.empty: + continue + print(f" {a}: credito medio al mid ${j['cred_md'].mean():.2f} → al bid/ask " + f"${j['cred_ba'].mean():.2f} = si perde il " + f"{1 - j['cred_ba'].mean() / j['cred_md'].mean():.0%} del credito solo attraversando " + f"lo spread") + + print("\n Il f del 30/07, ri-misurato su 10 settimane invece di 8 (fill conservativo):") + for a in ASSETS: + f = canon[a]["f_net"].dropna() + lo, hi = boot_ci(f.to_numpy()) + print(f" {a}: f_net mediana {f.median():.3f} media {f.mean():.3f} " + f"IC95 [{lo:.3f}, {hi:.3f}] ≥1.0 in {int((f >= 1).sum())}/{len(f)}") + allf = pd.concat([canon[a]["f_net"] for a in ASSETS]).dropna() + lo, hi = boot_ci(allf.to_numpy()) + print(f" pooled: {allf.median():.3f} (IC95 [{lo:.3f}, {hi:.3f}], n={len(allf)}) — " + f"replica indipendente dello 0.73 del 30/07") + + print("\n Fee: listino vero contro il forfait dello sleeve (12.5% del credito NETTO).") + print(" ⚠️ Il confronto giusto e' col credito MODELLATO — e' quello a cui il sleeve applica\n" + " il forfait. Misurarlo sul credito reale (piu' piccolo di ~30%) sottostima l'errore:") + for a in ASSETS: + tr = canon[a] + print(f" {a}: fee reale ${tr['fee'].mean():.2f}/settimana per contratto vs forfait sul\n" + f" credito MODELLATO ${tr['fee_sleeve'].mean():.2f} (= {tr['fee_sleeve'].mean() / tr['fee'].mean():.2f}x)" + f" · sul credito reale ${tr['fee_sleeve_su_reale'].mean():.2f} " + f"(= {tr['fee_sleeve_su_reale'].mean() / tr['fee'].mean():.2f}x)") + print(" Il '~2x' pubblicato il 30/07 non si replica qui: su queste 19 settimane il forfait\n" + " sovrastima le fee di 1.1-1.9x a seconda dell'asset e della base di confronto.\n" + " Resta vero il segno (il forfait e' CONSERVATIVO), non la taglia.") + + # ------------------------------------------------------------ §5 IL MINIMO + hr("§5 IL RISCHIO STA NEL MINIMO — le chiusure settimanali non lo vedono") + print(" Regola del 30/07: un rischio valutato sul minimo non si misura sulle chiusure.\n" + " Qui il mark infra-settimana viene dalle standing quotes ORARIE delle STESSE due gambe\n" + " (ricomprare la corta all'ask, rivendere la lunga al bid: di nuovo conservativo).\n") + worst_rows = [] + for a in ASSETS: + print(f" {a} (lotto {minlot[a]})") + for _, r in canon[a].iterrows(): + path = CB.close_cost_path(r, puts) + if path.empty: + continue + lot = minlot[a] + risk = r["width"] * lot - r["cred"] * lot + worst = float(path["pnl_aperto"].min()) * lot + worst_rows.append(dict(asset=a, exp=r["exp"], worst=worst, risk=risk, + frac=worst / risk, pnl=r["pnl"] * lot, + n_marks=len(path))) + flag = " ← chiusa in UTILE" if r["pnl"] > 0 and worst < 0 else "" + print(f" {r['exp']:%Y-%m-%d} marks={len(path):3d} " + f"peggior mark {worst:>9.2f}$ = {worst / risk:>7.1%} del rischio " + f"esito {r['pnl'] * lot:>7.2f}${flag}") + W = pd.DataFrame(worst_rows) + if not W.empty: + print(f"\n Peggior mark infra-settimana: mediana {W['frac'].median():.1%} del rischio, " + f"minimo {W['frac'].min():.1%}") + wl = W[W["pnl"] > 0] + print(f" Fra le sole settimane chiuse in UTILE ({len(wl)}/{len(W)}): mediana " + f"{wl['frac'].median():.1%}, minimo {wl['frac'].min():.1%}") + print(" → il maxDD sulle chiusure e' una lente che non vede il rischio che si e' corso.") + + # ------------------------------------------------------------ §6 GATE + hr("§6 GATE — compresi quelli che NON si possono far girare") + + print(" (a) implausible_sharpe — il gate del 26/07 sul rischio FUORI dal campione") + for a in ASSETS: + tr = canon[a] + daily = pd.Series((tr["pnl"] / tr["risk"]).to_numpy(float), + index=pd.DatetimeIndex(tr["exp"])) + rep = A.implausible_sharpe(daily, n_trades=len(tr), n_losing_trades=int((tr["pnl"] <= 0).sum())) + print(f" {a}: implausible={rep['implausible']} vinte {int((tr['pnl'] > 0).sum())}/{len(tr)}") + for why in rep["reasons"]: + print(f" · {why}") + if int((tr["pnl"] <= 0).sum()) == 0: + print(f" · regola del tre: 0 perdite su {len(tr)} lascia un tasso VERO fino a " + f"{3 / len(tr):.0%}. Su un payoff che perde {tr['risk'].median() / tr['cred'].median():.1f}x " + f"il credito, basta a ribaltare l'expectancy.") + + print("\n (b) deflated_sharpe — NON CALCOLABILE, e il perche' e' il risultato") + for a in ASSETS: + s = weekly_series(canon[a]) + dsr, _ = A.deflated_sharpe(st[a]["sharpe"], [r["comb_sh"] for r in grid if + np.isfinite(r.get("comb_sh", np.nan))], + s, dpy=WEEKS_PER_YEAR) + print(f" {a}: DSR = {dsr} (T={len(s)} osservazioni settimanali; la funzione " + f"richiede T>=30)") + print(" Costruire una serie GIORNALIERA per superare il T>=30 sarebbe barare: uno sleeve\n" + " settimanale su griglia giornaliera e' ~94% di zeri (lezione 26/07 su VRP01), e il\n" + " T gonfiato entra nel deflated-Sharpe come se fossero osservazioni indipendenti.\n" + " Statistica onesta a questa taglia = il t con il suo intervallo:") + for a in ASSETS: + r = (canon[a]["pnl"] / canon[a]["risk"]).to_numpy(float) + lo, hi = boot_ci(r) + print(f" {a}: media settimanale {r.mean():+.2%} del rischio, IC95 " + f"[{lo:+.2%}, {hi:+.2%}], t={st[a]['t']:.2f}, n={len(r)}") + + print("\n (c) anchor_luck_band — l'ora d'ingresso e' un'ancora (max-of-k mai dichiarato)") + offsets = list(range(-24, 25, 6)) + for a in ASSETS: + def by_off(o, _a=a): + return weekly_series(build_trades(puts, _a, *CANON, offset_h=float(o), today=today)) + band = A.anchor_luck_band( + by_off, offsets, canonical=0, + metric=lambda s: (float(s.mean() / s.std(ddof=1) * np.sqrt(WEEKS_PER_YEAR)) + if len(s) > 2 and s.std(ddof=1) > 0 else 0.0)) + if band.get("reason"): + print(f" {a}: {band['reason']}") + continue + print(f" {a}: canonica {band['canonical']:.2f} (pctl {band['canonical_pctl']:.0%}) · " + f"MEDIANA ONESTA {band['median']:.2f} · banda [{band['lo']:.2f}, {band['hi']:.2f}] · " + f"positiva in {band['frac_positive']:.0%} di {band['n_anchors']} ancore · " + f"fortuna {band['luck']:+.2f}") + + print("\n (d) null del de-levering / beta — 'e' reddito da volatilita' o crypto travestito?'") + print(" Lo spread ha delta positivo (≈ +0.18 all'ingresso): in un campione che sale del") + print(" 20-50% guadagna PER COSTRUZIONE. Regressione del PnL settimanale sul ritorno") + print(" del sottostante nella stessa settimana:") + for a in ASSETS: + tr = canon[a] + x = tr["und_ret"].to_numpy(float) + y = (tr["pnl"] / tr["risk"]).to_numpy(float) + if len(x) < 4: + continue + b, alpha = np.polyfit(x, y, 1) + resid = y - (b * x + alpha) + se = float(np.std(resid, ddof=2) / (np.std(x) * np.sqrt(len(x)))) + print(f" {a}: beta {b:+.3f} (t={b / se:.2f}) alpha {alpha:+.2%}/settimana " + f"corr {np.corrcoef(x, y)[0, 1]:+.2f}") + k = abs(b) + lev = pd.Series(k * x, index=pd.DatetimeIndex(tr["exp"])) + sh_lev = (float(lev.mean() / lev.std(ddof=1) * np.sqrt(WEEKS_PER_YEAR)) + if lev.std(ddof=1) > 0 else np.nan) + print(f" null: essere semplicemente LONG il sottostante a leva {k:.2f} " + f"(stesso beta) da' Sharpe {sh_lev:.2f} contro {st[a]['sharpe']:.2f} della struttura") + + print("\n (e) marginal_vs_tp01 — girato, ma NON interpretabile a questa taglia") + try: + comb = pd.concat([weekly_series(canon[a]) for a in ASSETS], axis=1).mean(axis=1).dropna() + dly = comb.resample("1D").sum().fillna(0.0) + rep = A.marginal_vs_tp01(dly) + act = int((dly != 0).sum()) + print(f" verdetto={rep.get('verdict')} corr={rep.get('corr'):.3f} " + f"(barre ATTIVE {act}/{len(dly)} = {act / len(dly):.0%})") + print(f" ⚠️ {len(comb)} settimane su un hold-out che non esiste: il verdetto e' " + f"rumore, si riporta per non ometterlo.") + except Exception as exc: # noqa: BLE001 + print(f" NON GIRATO: {type(exc).__name__}: {exc}") + + print("\n (f) causality_ok — non girato su questo candidato, e perche'") + print(" `A.causality_ok` valuta target_fn su OHLCV BTC/ETH; qui il segnale non e' una") + print(" serie di prezzo ma una selezione di strumenti da uno snapshot. Verifica") + print(" equivalente eseguita per costruzione e controllata qui sotto:") + bad = 0 + for a in ASSETS: + for _, r in canon[a].iterrows(): + if r["ts"] >= r["exp"] or not (DTE_LO <= r["dte"] <= DTE_HI): + bad += 1 + print(f" ingressi con ts >= scadenza o fuori finestra DTE: {bad}/" + f"{sum(len(canon[a]) for a in ASSETS)} (le gambe si scelgono da UNO snapshot allo\n" + f" stesso ts; il regolamento usa l'indice alla scadenza, mai prima)") + + # ------------------------------------------------------------ §7 VERDETTO + hr("§7 VERDETTO") + print(""" SCARTATO — su questi dati la domanda non e' decidibile, e i tre motivi sono + indipendenti l'uno dall'altro: + + 1. IL CAMPIONE NON CONTIENE LA STRATEGIA. 0 settimane su 19 passano il gate IV-rank>0.30 + che E' l'alpha di VRP01. Il campione e' vol implicita al 24-28 percentile E un rialzo + del +25%/+57%: il regime migliore possibile per vendere put, non uno neutro. + Le 10 settimane per asset (non 16: prima del 2026-06-09 la finestra 4-10 DTE non + esiste) danno 10/10 vincenti su BTC. La lente giusta non e' lo Sharpe, e' la regola + del tre: 0 perdite su 10 lascia un tasso vero fino al 30%, su un payoff che perde + ~7x il credito. + + 2. LA FAMIGLIA CHE HA STORIA NON E' QUELLA CHE SI PUO' ESEGUIRE. Le quote raccolte + sono le opzioni INVERSE (margine e regolamento in BTC/ETH); il conto e' in USDC. + La famiglia USDC-lineare esiste per BTC ed ETH ed ha un lotto minimo 10x piu' piccolo + (BTC 0.01 invece di 0.1) — quindi il muro «BTC fuori a $3.000» del 30/07, congelato + in un test, e' misurato sulla famiglia sbagliata. Ma di quella famiglia abbiamo ZERO + ore di storia e l'open interest e' ~0. + + 3. LO SPREAD E' IL COSTO DOMINANTE E NON E' MODELLABILE DA UN MID. Attraversare il + bid/ask costa da solo una quota grossa del credito, e l'ala che si COMPRA e' la gamba + dove il libro e' piu' largo — lo stesso meccanismo che il 30/07 misurava come f=0.73. + + Cio' che NON si conclude: che la struttura non funzioni. Non e' stata osservata nel regime + in cui lavora. La regola «niente short-vol da MODELLO in deploy» NON diventa «short-vol da + QUOTE»: le quote ora ci sono, ma sono quote della famiglia sbagliata, in un regime solo. + + AZIONE RACCOMANDATA (non eseguita — tocca la produzione, e la decisione e' dell'operatore): + far raccogliere a `collect_chain.py` anche `currency=USDC` (famiglie BTC_USDC/ETH_USDC). + Costa ~2 chiamate in piu' per giro; senza, fra sei mesi ci ritroveremo con un anno di + storia della famiglia che non possiamo tradare e zero di quella che potremmo.""") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_xs_lite.py b/scripts/research/r0822_xs_lite.py new file mode 100644 index 0000000..605af8b --- /dev/null +++ b/scripts/research/r0822_xs_lite.py @@ -0,0 +1,717 @@ +"""XS-LITE — esiste una versione CONCENTRATA (2-8 gambe) degli edge cross-sectional del progetto +che conservi una frazione utile dell'edge ed e' ESEGUIBILE OGGI a $600-5.000? + +LA DOMANDA (mai posta in questa forma). Il progetto ha due edge cross-sectional che dichiara di non +poter eseguire per TAGLIA, non per debolezza: + XS01 — 19 major HL, long 5 / short 5, ribilancio ogni 10g. Dichiarato eseguibile da ~$20k. + XSR01 — 50 alt HL, residuo OLS su BTC demeanato cross-sezionalmente. Dichiarato ~$5k. +Il vincolo dichiarato e' il **min order** moltiplicato per il numero di gambe. Se si riducono le +gambe il vincolo si allenta — ma l'ampiezza (il numero di scommesse indipendenti) crolla, e +l'ampiezza e' l'unica ragione per cui una strategia cross-sectional ha uno Sharpe alto. +Questo script misura il CAMBIO, non lo assume. E misura anche la premessa: che il muro sia il +min-order. + +IPOTESI PRE-REGISTRATE (scritte prima di guardare i numeri): + (a) lo Sharpe scende con k come ~sqrt(ampiezza): k=2-3 conserva il 40-60% dell'edge; + (b) il min-order NON e' il muro di XS01 a $600 (a k=5 il ticket e' 0.5/5 del capitale dello + sleeve), e il "serve $20k" viene dal TURNOVER del vol-target, non dalla taglia della posizione; + (c) XSR01 concentrato perde molto di piu', perche' il suo edge E' l'ampiezza (il progetto ha gia' + misurato che il demeaning porta l'ampiezza 4.5 -> 37.4: e' quel numero, non il segnale, a + fare lo Sharpe 1.82). +Con 2.6 anni di storia monoregime il verdetto atteso e' LEAD o SCARTATO, mai CANDIDATO. + +MECCANISMI CONGELATI — non si rifitta nulla. + XS01 : blend lookback (30,90), gate dispersione p30 espandente causale, H=10, vol-target 20%, + cap 3x, fee 5 bps per unita' di turnover. UNICO parametro variato: k (gambe per lato). + Replica bit-exact di src.portfolio.sleeves._xsec_returns verificata al passo (0). + XSR01: W=45, sgn=+1, residuo OLS rolling causale su BTC, z-score, tanh, vol-target 20%, cap 2x, + demean cross-sezionale giornaliero, fee 5 bps/gamba. Segnale IMPORTATO da + r0725_statarb_multi, paniere da r0725_statarb_basket_gate (nessuna reimplementazione). + UNICO parametro variato: k. + Il LOOKBACK NON e' stato riaperto DI PROPOSITO: riaprire un parametro riapre la sua famiglia + (lezione VRP-tenore, 30/07) e moltiplicherebbe i trial. Qui si varia solo la CONCENTRAZIONE. + +GRIGLIA DICHIARATA PRIMA DI MISURARE: 2 famiglie x 2 universi x 6 valori di k (+ il "pieno" di +XSR01) = 26 celle valutabili. Le 10 fasi del ciclo H=10 di XS01 sono una banda d'ANCORA, non una +griglia di parametri (regola del progetto: il deflated-Sharpe non le conta) -> riportate come +mediana onesta e come differenze APPAIATE. Il deflated-Sharpe e' comunque pubblicato a N=26 E a +N=156 (sensibilita' del verdetto al conteggio, lezione 30/07). + + nice -n 19 timeout 900 uv run python scripts/research/r0822_xs_lite.py +""" +from __future__ import annotations + +import sys +import warnings +from pathlib import Path + +import numpy as np +import pandas as pd + +warnings.filterwarnings("ignore") + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +import altlib as A # noqa: E402 +from src.portfolio.sleeves import XS_CFG, XS_UNIVERSE, _xsec_returns # noqa: E402 +from r0725_statarb_multi import BASE, load_hl, pnl, signal # noqa: E402 +from r0725_statarb_basket_gate import basket_from_positions, pair_frames # noqa: E402 + +RAW = ROOT / "data" / "raw" +ANN = np.sqrt(365.25) +HOLDOUT = A.HOLDOUT # 2025-01-01 (convenzione altlib) +MIN_ORDER_HL = 10.0 # Hyperliquid: min order $10 (r0702_capital_scaling) +MIN_ORDER_5 = 5.0 # il $5 di Deribit, citato nella missione +KS = (1, 2, 3, 4, 5, 6) # 5 = canonico XS01 +COST_SWEEP = (0.0, 5.0, 10.0, 20.0, 30.0, 50.0) # bps per gamba TOTALI (fee + slippage) +# capitali dello SLEEVE. Il progetto tiene XS01 al 15% del book -> a book $600 lo sleeve ha $90. +# Si misurano entrambe le letture, perche' e' esattamente qui che nasce il numero "$20k". +SLEEVE_CAPS = (90.0, 300.0, 600.0, 750.0, 2000.0, 5000.0) + + +# ===================================================================================== +# misure +# ===================================================================================== +def sh(s) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + return float(r.mean() / r.std() * ANN) if len(r) > 30 and r.std() > 0 else 0.0 + + +def dd(s) -> float: + r = np.nan_to_num(np.asarray(pd.Series(s).values, float)) + eq = np.cumprod(1.0 + np.clip(r, -0.99, None)) + return float(np.min(eq / np.maximum.accumulate(eq) - 1.0)) + + +def cagr(s) -> float: + x = pd.Series(s).dropna() + if len(x) < 30: + return 0.0 + yrs = (x.index[-1] - x.index[0]).days / 365.25 + eq = float(np.prod(1.0 + np.clip(x.values, -0.99, None))) + return float(eq ** (1.0 / yrs) - 1.0) if yrs > 0.2 and eq > 0 else 0.0 + + +def split(s): + x = pd.Series(s).dropna() + return x[x.index < HOLDOUT], x[x.index >= HOLDOUT] + + +HDR = f" {'cella':<20}{'ShFULL':>8}{'Sh IS':>8}{'Sh OOS':>8}{'maxDD':>9}{'CAGR':>9}" + + +def row(name, s, extra=""): + ins, out = split(s) + return (f" {name:<20}{sh(s):>8.2f}{sh(ins):>8.2f}{sh(out):>8.2f}" + f"{dd(s)*100:>8.1f}%{cagr(s)*100:>8.1f}%{extra}") + + +# ===================================================================================== +# (A) XS01 congelato, con k e fase parametrici e universo a copertura variabile +# ===================================================================================== +def load_closes(syms, how="inner") -> pd.DataFrame: + cols = {} + for s in syms: + p = RAW / f"hl_{s.lower()}_1d.parquet" + if not p.exists(): + continue + d = pd.read_parquet(p, columns=["timestamp", "close"]) + cols[s] = pd.Series(d["close"].values.astype(float), + index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)) + C = pd.concat(cols, axis=1, join=("inner" if how == "inner" else "outer")).sort_index() + return C.dropna() if how == "inner" else C + + +def xs_run(C: pd.DataFrame, k: int, phase: int = 0): + """Replica di sleeves._xsec_returns con k e fase parametrici, tollerante ai simboli con + copertura piu' corta (si valutano solo quelli con storia sufficiente a quella data: senza + questo, un outer-join su 51 alt riduce la finestra comune a ~2/3 e cancella l'in-sample). + Con tutte le colonne valide il percorso e' NUMERICAMENTE IDENTICO all'originale — provato al + passo (0) con max|diff| = 0. + Ritorna (serie netta lente-sleeve, W [n x A], scale [n], dret [n x A]).""" + px = C.values.astype(float) + n, na = px.shape + lookbacks, H, mode, tv = XS_CFG["lookbacks"], XS_CFG["H"], XS_CFG["mode"], XS_CFG["target_vol"] + disp_pct, minhist = XS_CFG["disp_pct"], XS_CFG["disp_minhist"] + mlb = max(lookbacks) + fin = np.isfinite(px) + prev = np.vstack([np.full(na, np.nan), px[:-1]]) + dret = np.where(fin & np.isfinite(prev), px / prev - 1.0, 0.0) + dret = np.nan_to_num(dret) + W = np.zeros((n, na)) + w = np.zeros(na) + disp_hist = [] + for i in range(n): + if i >= mlb and (i - phase) % H == 0: + ok = fin[i].copy() + for L in lookbacks: + ok &= fin[i - L] + if ok.sum() >= 2 * k: + cols = np.where(ok)[0] + rLs = [px[i, cols] / px[i - L, cols] - 1.0 for L in lookbacks] + disp_i = float(np.mean([r.std() for r in rLs])) + thr = (np.percentile(disp_hist, disp_pct) + if (disp_pct > 0 and len(disp_hist) >= minhist) else -np.inf) + if disp_i >= thr: + score = np.zeros(len(cols)); cnt = 0 + for rL in rLs: + sd = rL.std() + if sd > 0: + score += (rL - rL.mean()) / sd; cnt += 1 + if cnt: + score /= cnt + order = np.argsort(score) + w = np.zeros(na) + lo, hi = cols[order[:k]], cols[order[-k:]] + if mode == "mom": w[hi] = 0.5 / k; w[lo] = -0.5 / k + else: w[lo] = 0.5 / k; w[hi] = -0.5 / k + else: + w = np.zeros(na) + disp_hist.append(disp_i) + W[i] = w + gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1) + turn = np.zeros(n); turn[0] = np.abs(W[0]).sum() + turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1) + net = gross - turn * (0.001 / 2.0) + s = pd.Series(net, index=C.index) + rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25) + scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0) + return pd.Series(s.values * scale, index=C.index), W, scale, dret + + +def xs_effective_weights(W: np.ndarray, scale: np.ndarray) -> np.ndarray: + """Peso EFFETTIVO tenuto durante la barra i = W[i-1]*scale[i]. Entrambi noti a close[i-1] + (scale usa una rolling std gia' shiftata) -> causale.""" + Weff = np.zeros_like(W) + Weff[1:] = W[:-1] * scale[1:, None] + return Weff + + +# ===================================================================================== +# (B) XSR01 congelato, con k parametrico +# ===================================================================================== +def xsr_positions(P: pd.DataFrame, k: int | None) -> pd.DataFrame: + """Q = posizioni demeanate cross-sezionalmente (meccanismo congelato). + Se k e' dato: tiene le k piu' positive e le k piu' negative di ogni riga, azzera il resto e + RI-demeana sulle sopravvissute (resta dollar-neutral).""" + Q = P.sub(P.mean(axis=1), axis=0) + if k is None: + return Q + V = Q.to_numpy(float) + out = np.zeros_like(V) + for i in range(V.shape[0]): + v = V[i] + m = np.isfinite(v) + if m.sum() < 2 * k or not np.any(v[m] != 0.0): + continue + idx = np.where(m)[0] + order = idx[np.argsort(v[idx])] + keep = np.concatenate([order[:k], order[-k:]]) + r = np.zeros_like(v) + r[keep] = v[keep] + r[keep] -= r[keep].mean() + out[i] = r + return pd.DataFrame(out, index=P.index, columns=P.columns) + + +def xsr_legs(Q: pd.DataFrame, S: pd.DataFrame) -> pd.DataFrame: + """Matrice [data x gamba] dei pnl per gamba, nella lente PUBBLICATA (fee su 2 gambe per + coppia: alt + copertura BTC dedicata).""" + return pd.DataFrame({c: pnl(Q[c].to_numpy(float), S[c].to_numpy(float)) for c in Q.columns}, + index=Q.index) + + +def xsr_basket(Q: pd.DataFrame, S: pd.DataFrame, norm) -> pd.Series: + """norm = 'perrow' riproduce ESATTAMENTE basket_from_positions (media per-riga sui simboli + con dato: il numero di simboli cambia nel tempo). norm = intero -> normalizzazione COSTANTE, + l'unica che rende confrontabili celle con k diverso.""" + M = xsr_legs(Q, S) + if norm == "perrow": + return M.mean(axis=1).dropna() + return (M.fillna(0.0).sum(axis=1) / float(norm)).dropna() + + +# ===================================================================================== +# (C) simulatore di libro con MIN-ORDER (lente eseguibile, comune alle due famiglie) +# ===================================================================================== +def sim_book(Weff: np.ndarray, R: np.ndarray, idx, r_hedge=None, capital=None, + min_order=0.0, cost_bps=5.0): + """Weff[i] = pesi TARGET da tenere durante la barra i (decisi a close[i-1]). + R[i] = ritorni per gamba nella barra i. r_hedge = ritorno della gamba di copertura (si tiene + -sum(w)); None = libro gia' neutrale fra le gambe. min_order>0: una variazione di NOZIONALE + sotto la soglia NON si esegue (si tiene la vecchia). cost_bps = costo per unita' di turnover.""" + n, na = Weff.shape + c = cost_bps / 1e4 + held = np.zeros(na) + hedge = 0.0 + net = np.zeros(n) + n_fill = n_skip = 0 + tickets, gross = [], [] + for i in range(n): + tgt = Weff[i] + d_not = np.abs(tgt - held) * (capital or 1.0) + if min_order > 0.0 and capital: + move = d_not >= min_order + else: + move = d_not > 1e-12 + new = np.where(move, tgt, held) + n_fill += int(move.sum()) + n_skip += int(((~move) & (d_not > 1e-12)).sum()) + tickets.extend(d_not[move].tolist()) + turn = float(np.abs(new - held).sum()) + r = float(np.dot(new, R[i])) + if r_hedge is not None: + h = -float(new.sum()) + if min_order > 0.0 and capital and abs(h - hedge) * capital < min_order: + h = hedge + turn += abs(h - hedge) + hedge = h + r += hedge * float(r_hedge[i]) + net[i] = r - c * turn + held = new + gross.append(float(np.abs(new).sum()) + abs(hedge)) + return pd.Series(net, index=idx), dict( + n_fill=n_fill, n_skip=n_skip, + exec_share=(n_fill / (n_fill + n_skip)) if (n_fill + n_skip) else 1.0, + med_ticket=float(np.median(tickets)) if tickets else 0.0, + med_gross=float(np.median(gross)), + orders_py=(n_fill / (len(idx) / 365.25)) if len(idx) else 0.0) + + +# ===================================================================================== +# (D) ampiezza +# ===================================================================================== +def breadth(legs: pd.DataFrame, n_simult: int, min_overlap: int = 30): + """Due misure, perche' una sola mente. + (1) N_eff = N/(1+(N-1)*rbar), N = gambe SIMULTANEE, rbar = correlazione media fra i contributi + di due gambe CONDIZIONATA a essere entrambe attive. E' la formula pubblicata dal progetto + (4.5 -> 37.4), qui condizionata: senza condizionare, concentrando si conterebbe come + 'ampiezza' la ROTAZIONE nel tempo, che nello Sharpe sta gia' dentro sqrt(T). + INSTABILE per costruzione: rbar leggermente NEGATIVO (normale in un libro dollar-neutral) + la fa esplodere. Si legge insieme a rbar, mai da sola. + (2) N_hhi = numero effettivo di gambe che PORTANO il rischio, giorno per giorno: + 1/sum(share_i^2) sulle quote |contributo|, mediato sui giorni attivi. Limitato da N, + stabile, e non dipende dal segno delle correlazioni.""" + M = legs.to_numpy(float) + M = np.nan_to_num(M) + act = M != 0.0 + rs = [] + for a in range(M.shape[1]): + for b in range(a + 1, M.shape[1]): + m = act[:, a] & act[:, b] + if m.sum() < min_overlap: + continue + xa, xb = M[m, a], M[m, b] + if xa.std() > 0 and xb.std() > 0: + rs.append(float(np.corrcoef(xa, xb)[0, 1])) + rbar = float(np.mean(rs)) if rs else float("nan") + N = float(n_simult) + den = 1.0 + (N - 1) * rbar if rs else float("nan") + neff = (N / den) if (rs and den > 1e-3) else float("nan") + hh = [] + for i in range(M.shape[0]): + v = np.abs(M[i][act[i]]) + t = v.sum() + if t > 0 and len(v) > 0: + sHH = float(np.sum((v / t) ** 2)) + if sHH > 0: + hh.append(1.0 / sHH) + return neff, rbar, (float(np.mean(hh)) if hh else float("nan")), len(rs) + + +# ===================================================================================== +def main() -> None: + print("=" * 108) + print(" XS-LITE — una versione CONCENTRATA degli edge cross-sectional e' eseguibile a $600-5.000?") + print("=" * 108) + + maj19 = list(XS_UNIVERSE) + all51 = sorted({p.stem.replace("hl_", "").replace("_1d", "").upper() + for p in RAW.glob("hl_*_1d.parquet")}) + + # ---------------------------------------------------------------- (0) repliche + print("\n" + "-" * 108) + print(" (0) REPLICA DEI MECCANISMI CONGELATI — prima di ogni delta") + print("-" * 108) + C19 = load_closes(maj19, "inner") + C51 = load_closes(all51, "outer") + off, _, _, _ = xs_run(C19, k=XS_CFG["k"], phase=0) + ref = _xsec_returns() + J = pd.concat({"mio": off, "sleeve": ref}, axis=1, join="inner").dropna() + d_xs = float(np.max(np.abs(J["mio"] - J["sleeve"]))) + print(f" XS01 k=5 fase 0 vs sleeves._xsec_returns : max|diff| = {d_xs:.3e} " + f"({len(J)} barre) {'OK' if d_xs < 1e-12 else 'DIVERGE'}") + + P, S = pair_frames() + Qfull = xsr_positions(P, None) + xsr_perrow = xsr_basket(Qfull, S, "perrow") + xsr_ref = basket_from_positions(P, S, cols=None, demean=True) + J2 = pd.concat({"mio": xsr_perrow, "ref": xsr_ref}, axis=1, join="inner").dropna() + d_xsr = float(np.max(np.abs(J2["mio"] - J2["ref"]))) + print(f" XSR01 pieno vs basket_from_positions(demean): max|diff| = {d_xsr:.3e} " + f"({len(J2)} barre) {'OK' if d_xsr < 1e-12 else 'DIVERGE'}") + xsr_const = xsr_basket(Qfull, S, P.shape[1]) + print(f" XSR01 pieno, lente pubblicata (media per-riga sui simboli CON DATO): " + f"Sharpe {sh(xsr_perrow):.2f} maxDD {dd(xsr_perrow)*100:.1f}% (pubblicati 1.82 / -2.6%)") + print(f" XSR01 pieno, normalizzazione COSTANTE 1/{P.shape[1]} : " + f"Sharpe {sh(xsr_const):.2f} maxDD {dd(xsr_const)*100:.1f}%") + print(" ^ le due lenti coincidono qui (la copertura e' quasi piena); da qui in poi si usa la") + print(" COSTANTE, l'unica che rende confrontabili celle con k diverso, e i numeri XSR01 vanno") + print(f" letti contro {sh(xsr_const):.2f}.") + disc = pd.Timestamp("2026-07-25", tz="UTC") # giorno di scoperta/inception monitor + xsr_disc = xsr_perrow[xsr_perrow.index <= disc] + xsr_fwd = xsr_perrow[xsr_perrow.index > disc] + print(f" XSR01 sulla FINESTRA DI SCOPERTA (<= {disc.date()}, {len(xsr_disc)} barre): " + f"Sharpe {sh(xsr_disc):.2f} maxDD {dd(xsr_disc)*100:.1f}%") + print(" ⚠ IL NUMERO PUBBLICATO (1.82) NON SI RIPRODUCE OGGI, e non e' la mia implementazione:") + print(" le due repliche qui sopra sono bit-exact col codice del progetto. Sulla finestra") + print(f" IDENTICA a quella di scoperta la lente 'paniere' da' {sh(xsr_disc):.2f} e la lente 'libro'") + print(" (fee 1 gamba alt + copertura BTC NETTA, convenzione di paper_xsr) da' 2.23: l'1.82 non") + print(" e' nessuna delle due. Spiegazione piu' probabile: data/raw e' gitignored e il cron") + print(" riscrive i parquet HL -> stesso codice, dati diversi (identico allo scoperto del 07/08") + print(" su GTAA/ADJUSTED_LAST). NON l'ho inseguito: tutte le mie conclusioni sono confronti") + print(" RELATIVI dentro UNA lente. Ma tocca un numero pubblicato e il gate del 23/10.") + print(f" XSR01 dal 25/07 a oggi (forward, {len(xsr_fwd)} barre, stessa lente): " + f"ritorno {(np.prod(1+xsr_fwd.values)-1)*100:+.2f}% " + f"Sharpe {sh(xsr_fwd) if len(xsr_fwd) > 30 else float('nan'):.2f} " + f"{'(campione troppo corto per uno Sharpe: si legge il ritorno)' if len(xsr_fwd) <= 30 else ''}") + print(" ^ la differenza fra 1.82 e il numero FULL di oggi NON e' una lente diversa (le repliche") + print(" sono bit-exact): e' un mese in piu' di dati. Il gate pre-registrato resta il 23/10.") + print(f" finestra MAJ19: {C19.index[0].date()} -> {C19.index[-1].date()} ({len(C19)} barre); " + f"in-sample = pre {HOLDOUT.date()} = {int((C19.index < HOLDOUT).sum())} barre") + cov51 = np.isfinite(C51.values).sum(axis=1) + print(f" finestra ALL51: {C51.index[0].date()} -> {C51.index[-1].date()} ({len(C51)} barre); " + f"simboli con dato: da {cov51.min()} a {cov51.max()} (outer-join: l'inner-join") + print(" taglierebbe la finestra a 627 barre e CANCELLEREBBE l'in-sample).") + print(" ⚠ l'in-sample e' UN ANNO SOLO e monoregime: ogni Sh IS qui sotto va letto cosi'.") + + # ---------------------------------------------------------------- (1) XS-LITE + print("\n" + "-" * 108) + print(" (1) FAMIGLIA A — XS-LITE: XS01 congelato, varia solo k (gambe per lato). k=5 = canonico") + print("-" * 108) + print(HDR + f"{'N_eff':>8}{'rbar':>8}{'N_hhi':>8}") + xs_series, xs_cells = {}, {} + for uni, Cx in (("MAJ19", C19), ("ALL51", C51)): + for k in KS: + s, W, sc, dr = xs_run(Cx, k=k, phase=0) + Weff = xs_effective_weights(W, sc) + legpnl = pd.DataFrame(Weff * dr, index=Cx.index, columns=Cx.columns) + ne, rbar, nhhi, _ = breadth(legpnl, 2 * k) + nm = f"{uni} k={k}" + ("*" if (uni == "MAJ19" and k == 5) else "") + xs_series[nm] = s + xs_cells[nm] = dict(uni=uni, k=k, C=Cx, W=W, scale=sc, dret=dr, + neff=ne, rbar=rbar, nhhi=nhhi) + print(row(nm, s, f"{ne:>8.1f}{rbar:>8.3f}{nhhi:>8.1f}")) + print(" (*) = configurazione canonica del sleeve XS01") + + # ---------------------------------------------------------------- (2) XSR-LITE + print("\n" + "-" * 108) + print(" (2) FAMIGLIA B — XSR-LITE: XSR01 congelato (demean), varia solo k. 'pieno' = tutte le gambe") + print("-" * 108) + maj18 = [s for s in maj19 if s != BASE] + Pm = P[[c for c in maj18 if c in P.columns]] + Sm = S[[c for c in maj18 if c in S.columns]] + print(f" ALL50: {P.shape[1]} gambe MAJ18: {Pm.shape[1]} gambe") + print(HDR + f"{'N_eff':>8}{'rbar':>8}{'N_hhi':>8}") + xsr_series, xsr_cells = {}, {} + for uni, (Pu, Su) in (("ALL50", (P, S)), ("MAJ18", (Pm, Sm))): + for k in list(KS) + [None]: + if k is not None and 2 * k > Pu.shape[1]: + continue + Q = xsr_positions(Pu, k) + norm = (2 * k) if k is not None else Pu.shape[1] + s = xsr_basket(Q, Su, norm) + legs = xsr_legs(Q, Su) / float(norm) + ne, rbar, nhhi, _ = breadth(legs, norm) + nm = f"{uni} k={k if k else 'pieno'}" + ("*" if (uni == "ALL50" and k is None) else "") + xsr_series[nm] = s + xsr_cells[nm] = dict(uni=uni, k=k, Q=Q, S=Su, norm=norm, + neff=ne, rbar=rbar, nhhi=nhhi) + print(row(nm, s, f"{ne:>8.1f}{rbar:>8.3f}{nhhi:>8.1f}")) + print(" (*) = XSR01 pieno (50 gambe demeanate), qui a normalizzazione costante") + + # banda di fase calcolata PRIMA della curva: la stima onesta di uno sleeve ancorato e' la + # mediana della banda, quindi e' quella che deve stare nella curva (la fase 0 e' un max-of-10). + base_by_phase = {ph: xs_run(C19, k=5, phase=ph)[0] for ph in range(XS_CFG["H"])} + phase_med, phase_rows = {}, {} + for k in KS: + by_ph = {ph: (base_by_phase[ph] if k == 5 else xs_run(C19, k=k, phase=ph)[0]) + for ph in range(XS_CFG["H"])} + vals = np.array([sh(by_ph[p_]) for p_ in range(XS_CFG["H"])]) + diffs = np.array([sh(by_ph[p_]) - sh(base_by_phase[p_]) for p_ in range(XS_CFG["H"])]) + phase_med[k] = float(np.median(vals)) + phase_rows[k] = (vals, diffs) + + # ---------------------------------------------------------------- (3) curva ampiezza + print("\n" + "-" * 108) + print(" (3) LA CURVA AMPIEZZA-vs-k — la domanda centrale: quanto ne resta concentrando?") + print("-" * 108) + sh_xs_star = sh(xs_series["MAJ19 k=5*"]) + sh_xsr_star = sh(xsr_series["ALL50 k=pieno*"]) + print(f" {'k':>4}{'gambe':>7} {'--------- XS-LITE MAJ19 ---------':>34} " + f"{'--------- XSR-LITE ALL50 --------':>34}") + print(f" {'':>4}{'':>7} {'N_hhi':>9}{'N_eff':>9}{'Sh f0':>8}{'Sh MED':>8}{'MED/MED*':>10} " + f"{'N_hhi':>9}{'N_eff':>9}{'Sh':>8}{'Sh/Sh*':>9}") + med5 = phase_med[5] + for k in KS: + an = f"MAJ19 k={k}" + ("*" if k == 5 else "") + bn = f"ALL50 k={k}" + ca, cb = xs_cells[an], xsr_cells[bn] + sa, sb = xs_series[an], xsr_series[bn] + print(f" {k:>4}{2*k:>7} {ca['nhhi']:>9.1f}{ca['neff']:>9.1f}{sh(sa):>8.2f}" + f"{phase_med[k]:>8.2f}{(phase_med[k]/med5 if med5 else 0):>10.0%} " + f"{cb['nhhi']:>9.1f}{cb['neff']:>9.1f}{sh(sb):>8.2f}" + f"{(sh(sb)/sh_xsr_star if sh_xsr_star else 0):>9.0%}") + cp = xsr_cells["ALL50 k=pieno*"] + print(f" {'pieno':>4}{cp['norm']:>7} {'':>9}{'':>9}{'':>8}{'':>8}{'':>10} " + f"{cp['nhhi']:>9.1f}{cp['neff']:>9.1f}{sh_xsr_star:>8.2f}{1.0:>9.0%}") + print(" 'Sh MED' = mediana delle 10 fasi d'ancora di XS01 = la stima ONESTA (la fase 0 e' un") + print(" max-of-10 non dichiarato). XSR01 e' giornaliero: non ha fase, la sua colonna e' unica.") + print(" N_hhi = gambe che PORTANO il rischio (stabile). N_eff = formula pubblicata, esplode") + print(" quando rbar e' negativo: si legge solo insieme a rbar, mai da sola.") + + # ---------------------------------------------------------------- (4) banda d'ancora + print("\n" + "-" * 108) + print(" (4) BANDA D'ANCORA — le 10 fasi del ciclo H=10 di XS01 (XSR01 e' giornaliero: nessuna") + print(" fase). Stima onesta = MEDIANA della banda, non la fase 0 con cui e' stato scoperto.") + print("-" * 108) + print(f" {'cella':<14}{'fase0':>8}{'mediana':>9}{'p10':>8}{'p90':>8}{'pctl f0':>9}" + f" d vs k=5: mediana APPAIATA (fasi >0)") + for k in KS: + vals, diffs = phase_rows[k] + pctl = float((vals <= vals[0]).mean() * 100) + print(f" MAJ19 k={k:<6}{vals[0]:>8.2f}{phase_med[k]:>9.2f}{np.percentile(vals,10):>8.2f}" + f"{np.percentile(vals,90):>8.2f}{pctl:>8.0f}% {np.median(diffs):>+8.3f}" + f" {int((diffs>0).sum())}/10") + print(" 'd vs k=5' = mediana delle DIFFERENZE APPAIATE per fase (mai differenza di mediane).") + + # ---------------------------------------------------------------- (5) eseguibilita' + print("\n" + "-" * 108) + print(" (5) ESEGUIBILITA' — contabilita' min-order per gamba, lente 'libro'") + print(" Il capitale in colonna e' quello dello SLEEVE. XS01 sta al 15% del book:") + print(" sleeve $90 = book $600, sleeve $300 = book $2.000, sleeve $750 = book $5.000,") + print(" sleeve $3.000 = book $20.000 (la soglia dichiarata dal progetto).") + print("-" * 108) + print(f" {'cella':<13}{'sleeve$':>8}{'minord':>7}{'Sh model':>10}{'Sh reale':>10}" + f"{'haircut':>9}{'gambe eseg':>12}{'tick med$':>11}{'ord/anno':>10}" + f"{'lordo/lordo*':>14}") + print(" ⚠ 'lordo/lordo*' = lordo mediano ESEGUITO / lordo mediano modellato. Sotto ~0.8 il") + print(" libro ha smesso di seguire il proprio target, e allora lo Sharpe NON e' la lettura") + print(" giusta (puo' perfino MIGLIORARE): e' il null del de-levering in veste di 'meno") + print(" costi'. Lezione GTAA-banda 27/07, riapplicata qui.") + + def xs_book_inputs(nm): + cell = xs_cells[nm] + return xs_effective_weights(cell["W"], cell["scale"]), cell["dret"], cell["C"].index, None + + def xsr_book_inputs(nm): + cell = xsr_cells[nm] + Q, Su, norm = cell["Q"], cell["S"], cell["norm"] + Wt = np.nan_to_num(Q.to_numpy(float)) / float(norm) + Weff = np.zeros_like(Wt); Weff[1:] = Wt[:-1] + rb = load_hl(BASE).reindex(Q.index).pct_change().fillna(0.0).to_numpy(float) + Ralt = np.zeros_like(Wt) + for j, c in enumerate(Q.columns): + Ralt[:, j] = np.nan_to_num(Su[c].to_numpy(float)) + rb + return Weff, Ralt, Q.index, rb + + for tag, names, getter in (("XS", ["MAJ19 k=1", "MAJ19 k=2", "MAJ19 k=3", "MAJ19 k=5*"], + xs_book_inputs), + ("XSR", ["ALL50 k=1", "ALL50 k=2", "ALL50 k=3", "ALL50 k=pieno*"], + xsr_book_inputs)): + for nm in names: + Weff, R, idx, rh = getter(nm) + base, dg0 = sim_book(Weff, R, idx, r_hedge=rh, capital=None, min_order=0.0) + for cap in SLEEVE_CAPS: + for mo in (MIN_ORDER_HL,): + real, dg = sim_book(Weff, R, idx, r_hedge=rh, capital=cap, min_order=mo) + lbl = f"{tag} {nm.split()[1]}" + trk = (dg["med_gross"] / dg0["med_gross"]) if dg0["med_gross"] > 0 else float("nan") + print(f" {lbl:<13}{cap:>8.0f}{mo:>7.0f}{sh(base):>10.2f}{sh(real):>10.2f}" + f"{sh(base)-sh(real):>9.2f}{dg['exec_share']*100:>11.0f}%" + f"{dg['med_ticket']:>11.0f}{dg['orders_py']:>10.0f}" + f"{trk:>12.2f}{' ⚠' if trk < 0.8 else ' '}") + print() + print(" 'gambe eseg' = quota delle VARIAZIONI di posizione sopra il min-order (la diagnostica") + print(" su cui si basa la soglia '$20k'). 'haircut' = quanto ne costa in Sharpe. Sono cose") + print(" DIVERSE: il turnover saltato e' la deriva del vol-target, non il segnale.") + print(" Con min-order $5 invece di $10 (Deribit invece di HL) l'haircut si dimezza per") + print(" costruzione; il caso $10 e' quello vero per Hyperliquid ed e' quello riportato.") + + # ---------------------------------------------------------------- (6) slippage + print("\n" + "-" * 108) + print(" (6) SLIPPAGE — costo TOTALE per gamba (i 5 bps di fee taker sono gia' dentro).") + print(" A che costo muore l'edge? E' il rischio #1 dichiarato di XSR01.") + print("-" * 108) + print(f" {'cella':<16}" + "".join(f"{int(c):>10}bps" for c in COST_SWEEP) + f"{'break-even':>13}") + + def cost_curve(label, Weff, R, idx, rh): + vals = [] + for c in COST_SWEEP: + s, _ = sim_book(Weff, R, idx, r_hedge=rh, capital=None, min_order=0.0, cost_bps=c) + vals.append(sh(s)) + be = None + for i in range(1, len(COST_SWEEP)): + if vals[i - 1] > 0 >= vals[i]: + x0, x1, y0, y1 = COST_SWEEP[i-1], COST_SWEEP[i], vals[i-1], vals[i] + be = x0 + (x1 - x0) * y0 / (y0 - y1) + break + print(f" {label:<16}" + "".join(f"{v:>13.2f}" for v in vals) + + (f"{be:>13.0f}" if be else f"{'>'+str(int(COST_SWEEP[-1])):>13}")) + return be + + be = {} + for nm in ("MAJ19 k=1", "MAJ19 k=2", "MAJ19 k=3", "MAJ19 k=5*"): + be["XS " + nm] = cost_curve("XS " + nm.split()[1], *xs_book_inputs(nm)) + for nm in ("ALL50 k=1", "ALL50 k=2", "ALL50 k=3", "ALL50 k=pieno*"): + be["XSR " + nm] = cost_curve("XSR " + nm.split()[1], *xsr_book_inputs(nm)) + + # ---------------------------------------------------------------- (7) selezione + DSR + print("\n" + "-" * 108) + print(" (7) SELEZIONE IN-SAMPLE-ONLY (mai sull'hold-out) + DEFLATED SHARPE") + print("-" * 108) + allcells = {**{f"XS {n}": s for n, s in xs_series.items()}, + **{f"XSR {n}": s for n, s in xsr_series.items()}} + ins_rank = sorted(allcells.items(), key=lambda kv: -sh(split(kv[1])[0])) + print(f" celle valutate: {len(allcells)} (2 famiglie x 2 universi x 6 k, + i 2 'pieno' di") + print(" XSR01, meno le celle impossibili 2k>gambe). Lookback NON riaperto -> nessun trial in piu'.") + print(f"\n {'rank':>5} {'cella':<20}{'Sh IS':>8}{'Sh OOS':>8}{'Sh FULL':>9}") + for i, (n, s) in enumerate(ins_rank[:8], 1): + ins, out = split(s) + print(f" {i:>5} {n:<20}{sh(ins):>8.2f}{sh(out):>8.2f}{sh(s):>9.2f}") + best_name, best_s = ins_rank[0] + all_sr = [sh(s) for s in allcells.values()] + print(f"\n cella scelta AL BUIO (solo in-sample): {best_name}") + for mult, label in ((1, "trial dichiarati = celle"), + (6, "conteggio conservativo x6 (se avessi riaperto il lookback)")): + pad = all_sr * mult + d, s0 = A.deflated_sharpe(sh(best_s), pad, pd.Series(best_s).dropna()) + print(f" DSR a N={len(pad):<4} ({label}): {d:.3f} " + f"{'PASS' if d >= 0.95 else 'FAIL'} (Sharpe-soglia del null {s0:.2f})") + conc = {n: s for n, s in allcells.items() if any(f" k={j}" in n for j in (1, 2, 3))} + cbest_name, cbest_s = max(conc.items(), key=lambda kv: sh(split(kv[1])[0])) + d2, _ = A.deflated_sharpe(sh(cbest_s), all_sr, pd.Series(cbest_s).dropna()) + print(f" cella CONCENTRATA (k<=3, cioe' <=6 gambe) scelta al buio: {cbest_name} " + f"Sh IS {sh(split(cbest_s)[0]):.2f} OOS {sh(split(cbest_s)[1]):.2f} FULL {sh(cbest_s):.2f}") + print(f" DSR della concentrata a N={len(all_sr)}: {d2:.3f} {'PASS' if d2 >= 0.95 else 'FAIL'}") + + # ---------------------------------------------------------------- (8) gate marginali + print("\n" + "-" * 108) + print(" (8) GATE MARGINALE vs TP01 + RIDONDANZA vs gli sleeve che gia' esistono") + print("-" * 108) + order = [best_name, cbest_name, "XS MAJ19 k=2", "XS MAJ19 k=3", "XS MAJ19 k=5*", + "XSR ALL50 k=2", "XSR ALL50 k=3", "XSR ALL50 k=pieno*"] + to_score = {} + for n in order: + if n in allcells and n not in to_score: + to_score[n] = allcells[n] + for n, s in to_score.items(): + ss = pd.Series(s).dropna(); ss.index = pd.to_datetime(ss.index, utc=True) + m = A.marginal_vs_tp01(ss) + b = m.get("blends", {}).get("w25", {}) + print(f" {n:<20} {str(m.get('marginal_verdict')):<10} corr {str(m.get('corr_full')):>7}" + f" uplift w25 full {str(b.get('uplift_full')):>7} hold {str(b.get('uplift_hold')):>7}" + f" robust={m.get('robust_oos')} noise_null={m.get('beats_noise_null')}" + f" is_edge={m.get('has_insample_edge')} hedge={m.get('is_hedge')}") + print("\n correlazione col materiale gia' in casa (un proxy eseguibile DEVE assomigliare al suo") + print(" originale: qui una corr ALTA e' l'obiettivo, non un difetto)") + ref_map = {"XS01 (sleeve)": ref, "XSR01 (pieno)": xsr_const, "TP01": A.tp01_baseline_daily()} + print(f" {'cella':<20}" + "".join(f"{k:>16}" for k in ref_map)) + for n, s in to_score.items(): + line = f" {n:<20}" + for _, r in ref_map.items(): + Jc = pd.concat({"a": pd.Series(s).dropna(), "b": r}, axis=1, join="inner").dropna() + line += f"{(Jc['a'].corr(Jc['b']) if len(Jc) > 30 else float('nan')):>16.3f}" + print(line) + print(" ⚠ corr a SKH01/VRP01/GTAA01 NON misurata: SKH01 richiede i 5m di DUE asset e il brief") + print(" vieta di caricarli insieme. Prior del progetto (25/07): XSR01 sta a |0.001|-|0.071|") + print(" da tutti e 5 gli sleeve; un suo sottoinsieme non puo' esserne lontano.") + + # ---------------------------------------------------------------- (9) null del de-levering + print("\n" + "-" * 108) + print(" (9) NULL DEL DE-LEVERING — obbligatorio su ogni confronto di maxDD (5 occorrenze note)") + print(" Domanda: esiste una LEVA del canonico che dia lo STESSO maxDD della variante?") + print(" Se a quel maxDD il canonico ha Sharpe >= alla variante, la variante non aggiunge.") + print("-" * 108) + def lev_for_dd(canon: pd.Series, target_abs: float, lo=0.01, hi=5.0): + """Leva k tale che |maxDD(canon*k)| = target. |maxDD| e' monotono crescente in k.""" + if abs(dd(canon * hi)) < target_abs: + return None # nemmeno a leva 5x si arriva al DD + for _ in range(60): + mid = 0.5 * (lo + hi) + if abs(dd(canon * mid)) < target_abs: lo = mid + else: hi = mid + return 0.5 * (lo + hi) + + def delever_null(variants: dict, canon: pd.Series, label: str): + print(f" {label:<22}{'maxDD':>9}{'Sh':>8} {'k_leva canonico':>16}{'Sh canonico':>13} esito") + for nm, s in variants.items(): + kl = lev_for_dd(canon, abs(dd(s))) + if kl is None: + print(f" {nm:<22}{dd(s)*100:>8.1f}%{sh(s):>8.2f} {'n.d. (>5x)':>16}" + f"{'':>13} maxDD irraggiungibile levando il canonico") + continue + sc_ = sh(canon * kl) + print(f" {nm:<22}{dd(s)*100:>8.1f}%{sh(s):>8.2f} {kl:>16.3f}{sc_:>13.2f} " + f"{'il canonico DOMINA' if sc_ >= sh(s) else 'la variante REGGE'}") + delever_null({n: xs_series[n] for n in xs_series if n.startswith("MAJ19") and "k=5" not in n}, + xs_series["MAJ19 k=5*"], "XS-LITE vs canonico") + print() + delever_null({n: xsr_series[n] for n in xsr_series if n.startswith("ALL50") and "pieno" not in n}, + xsr_series["ALL50 k=pieno*"], "XSR-LITE vs pieno") + + # ---------------------------------------------------------------- (10) implausible + causalita' + print("\n" + "-" * 108) + print(" (10) implausible_sharpe + CAUSALITA' (prefisso troncato + decadimento con lag)") + print("-" * 108) + for n in dict.fromkeys([best_name, cbest_name, "XS MAJ19 k=2", "XS MAJ19 k=5*", + "XSR ALL50 k=2"]): + if n not in allcells: + continue + ss = pd.Series(allcells[n]).dropna(); ss.index = pd.to_datetime(ss.index, utc=True) + r = A.implausible_sharpe(ss) + print(f" {n:<20} implausible={str(r['implausible']):<6} Sh {r.get('sharpe',0):>5.2f} " + f"perdite su barre ATTIVE {r.get('loss_frac',float('nan'))*100:>5.1f}% " + f"attive {r.get('active_frac',0)*100:>3.0f}% {'; '.join(r['reasons'])[:38]}") + cut = int(len(C19) * 0.85) + Wf = xs_run(C19, k=2, phase=0)[1] + Wp = xs_run(C19.iloc[:cut], k=2, phase=0)[1] + dmax = float(np.max(np.abs(Wp[cut - 80:cut] - Wf[cut - 80:cut]))) + print(f" XS k=2: pesi ricalcolati su prefisso troncato (cut={cut}) vs pieno, ultime 80 barre: " + f"max|diff| = {dmax:.2e} {'CAUSALE' if dmax < 1e-12 else 'LOOK-AHEAD'}") + Wq = xs_run(C19, k=2, phase=0) + Weff2 = xs_effective_weights(Wq[1], Wq[2]) + lags = [] + for lag in (0, 1, 2, 3, 5): + Wl = np.zeros_like(Weff2) + if lag: Wl[lag:] = Weff2[:-lag] + else: Wl = Weff2 + s, _ = sim_book(Wl, Wq[3], C19.index) + lags.append(sh(s)) + print(" XS k=2 decadimento con lag (0/1/2/3/5 giorni): " + + " / ".join(f"{v:.2f}" for v in lags) + + " (dolce = segnale lento, non firma di look-ahead)") + + # ---------------------------------------------------------------- (11) sintesi + print("\n" + "=" * 108) + print(" SINTESI — quanto costa concentrare, e dove sta davvero il muro") + print("=" * 108) + k2, k5 = xs_series["MAJ19 k=2"], xs_series["MAJ19 k=5*"] + print(f" XS-LITE k=2 (4 gambe): Sh fase0 {sh(k2):.2f} / fase-MEDIANA {phase_med[2]:.2f}" + f" vs canonico k=5 (10 gambe): {sh(k5):.2f} / {phase_med[5]:.2f}") + print(f" XSR-LITE k=2 (4 gambe): Sh {sh(xsr_series['ALL50 k=2']):.2f} " + f"(IS {sh(split(xsr_series['ALL50 k=2'])[0]):.2f}) vs pieno " + f"{sh_xsr_star:.2f} (IS {sh(split(xsr_series['ALL50 k=pieno*'])[0]):.2f})") + def _be(x): + return f"{x:.0f} bps" if x else f">{int(COST_SWEEP[-1])} bps" + print(f" break-even di costo per gamba: XS k=2 {_be(be.get('XS MAJ19 k=2'))}, " + f"XS k=5 {_be(be.get('XS MAJ19 k=5*'))}, XSR k=2 {_be(be.get('XSR ALL50 k=2'))}, " + f"XSR pieno {_be(be.get('XSR ALL50 k=pieno*'))}") + print("=" * 108) + + +if __name__ == "__main__": + main() From 3b206c8e6d2c66aa7e7dfdc338fe3e11576a874f Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:08:20 +0000 Subject: [PATCH 003/114] =?UTF-8?q?research(wave-0822):=20DEALER-GAMMA=20s?= =?UTF-8?q?cartato=20=E2=80=94=20e=20dealer=5Fnet=5Fgamma=20e'=20il=20GEX?= =?UTF-8?q?=20col=20segno=20invertito?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 25 +++- scripts/research/r0822_dealer_gamma.py | 8 +- scripts/research/r0822_flow_squeeze.py | 9 +- scripts/research/r0822_ortho_screen.py | 18 ++- scripts/research/r0822_term_structure.py | 133 ++++++++++++++++------ scripts/research/r0822_vol_size.py | 71 ++++++++++-- scripts/research/r0822_vrp_real_quotes.py | 25 +++- scripts/research/r0822_xs_lite.py | 53 +++++++++ 8 files changed, 283 insertions(+), 59 deletions(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index f90ed6c..f2ff662 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -6,7 +6,7 @@ null de-levering superato + eseguibilita' al capitale dichiarato. | # | filone | verdetto | in una riga | |---|---|---|---| -| 9 | ADAPTIVE-HORIZON | **SCARTATO** | il "vincitore adattivo" ha lookback incollato al bordo il 100% del tempo = TSMOM costante a 20g (corr 1.000, dSh 0.00 in 8/8 anni e 0/12 ancore); isolando le celle davvero adattive -> NEUTRAL, corr→TP01 0.90 = TP01 travestito | +| 9 | ADAPTIVE-HORIZON | **SCARTATO** | il "vincitore adattivo" ha lookback incollato al bordo il 100% del tempo = TSMOM costante a 20g (corr 1.000, dSh 0.00 in 8/8 anni e 0/12 ancore); isolando le celle davvero adattive -> NEUTRAL, corr→TP01 0.90 = TP01 travestito || 4 | DEALER-GAMMA | **SCARTATO** | la gamba tradeable e' INCOERENTE (BTC non separa, ETH separa al ROVESCIO); cio' che resta e' quasi tutto DVOL (corr 0.66), gia' refutato il 26/06. DSR 0.440 e la cella migliore sta SOTTO il massimo atteso dal rumore | ## Note che sopravvivono ai singoli filoni @@ -22,3 +22,26 @@ null de-levering superato + eseguibilita' al capitale dichiarato. una superficie frastagliata. **Nessun cambio a TP01.** - Riaprirebbe il filone: un rilevatore il cui L si muove DAVVERO che batta il miglior L costante (non TP01) in >=6/8 anni appaiati con corr→TP01 < 0.6 — es. BOCPD/PELT vero, non provato. + +### 4 — DEALER-GAMMA (r0822_dealer_gamma.py, 136 statistiche dichiarate) +⚠️ **DIFETTO DI DATO, il risultato piu' riusabile del filone: `dealer_net_gamma` e' il GEX col SEGNO +INVERTITO** — corr **-0.95 BTC / -0.89 ETH** contro la ricostruzione da manuale sulla catena +(962/1116 snapshot, ~280-316 strike), fit `dng ~ -0.7*GEX`. **Chi lo usasse col nome che porta +leggerebbe ogni regime al contrario.** Spiega anche la contraddizione interna del file (corr fra +`dng>0` e `spot>flip` = -0.28 / -0.92). +⚠️ **Il file dichiara piu' di quel che ha:** cadenza 15 min solo dal **2026-05-01**; i 37 giorni +prima sono **un punto al giorno** -> **90 giorni, non 4 mesi**. `liquidation_long_risk` e +`liquidation_short_risk` sono **COSTANTI** ('low' in 17.229 righe su 17.229 non nulle) = zero +informazione. `gamma_flip_level` congelato nel **61%** dei passi su BTC. +- Gate: **null de-levering FALLITO** (baseline x0.6 stesso maxDD con Sharpe migliore); **DSR 0.440** + su 64 trial, e nel modo piu' netto — la cella migliore (+4.98 lordo) sta **sotto** il massimo + atteso dal puro rumore (+5.12). `marginal_vs_tp01` inutilizzabile: su 90 giorni full e hold-out + COINCIDONO (`n_hold_days == n_days == 90`). +- **Null location-matched** (aggiunto dall'agente): `gamma_flip_level` bocciato — 96% di accordo con + `spot > SMA(720h)` su ETH; su BTC la SMA nuda da' uno spread MAGGIORE. +- **Ampiezza effettiva**: 73/59 episodi di regime ma 6-8 coprono il 78-86% del tempo -> **n_eff 53 + (BTC) / 24 (ETH) ore** su 2.130. Il regime coincide col MESE (maggio long, giugno short, luglio long). +- ✅ Ricostruibile oggi dalla NOSTRA catena, meglio dell'originale (518 ore dal 01/08, ~209-262 + strike/ora, quote_status ok ~100%, segno giusto) — **ma la soglia non e' portabile** (`dng=0` + corrisponde a `GEX=+5.8e6` su BTC): ricalibrarla sui 90 giorni sarebbe la selezione che il + progetto rifiuta. Riapertura non prima di **~30-40 episodi di regime = ~2 anni (meta' 2028)**. diff --git a/scripts/research/r0822_dealer_gamma.py b/scripts/research/r0822_dealer_gamma.py index e91bd44..bc6df2b 100644 --- a/scripts/research/r0822_dealer_gamma.py +++ b/scripts/research/r0822_dealer_gamma.py @@ -655,9 +655,11 @@ def main() -> None: print(" 3. `gamma_flip_level` e' una media mobile: 96% di accordo con spot>SMA(720h) su ETH; su") print(" BTC (accordo 0.68) la SMA nuda da' uno spread di vol MAGGIORE del livello stesso.") print(" Niente contenuto di opzioni isolabile. Trappola 2 del brief.") - print(" 4. Ampiezza effettiva: 6-8 episodi di regime, n_eff 24-53 ore su 2.130. Il regime") - print(" coincide col mese (maggio LONG, giugno SHORT, luglio LONG): lo spread di vol e' UN") - print(" confronto fra tre mesi, non 2.136 osservazioni.") + print(" 4. Ampiezza effettiva: 73 (BTC) / 59 (ETH) episodi di regime, ma SEI e OTTO di essi") + print(" coprono il 78-86% del tempo; n_eff 53 e 24 ore su 2.130. Il regime coincide col") + print(" MESE (maggio LONG, giugno SHORT, luglio LONG): lo spread di vol e' un confronto") + print(" fra tre mesi, non fra 2.136 osservazioni. Tolto giugno cala del 46% (BTC) e del") + print(" 71% (ETH) e resta poggiato su 134 / 366 ore.") print(" 5. Cio' che resta della gamba (A) e' quasi tutto DVOL (corr 0.66 su BTC), e il DVOL") print(" come modulatore di TP01 e' gia' stato refutato il 26/06: era de-levering.") print(" 6. Il gate misurato FALLISCE il null del de-levering (baseline x0.6 lo domina) su una") diff --git a/scripts/research/r0822_flow_squeeze.py b/scripts/research/r0822_flow_squeeze.py index 18e0e36..072c9fa 100644 --- a/scripts/research/r0822_flow_squeeze.py +++ b/scripts/research/r0822_flow_squeeze.py @@ -622,11 +622,14 @@ def main() -> None: debole. (Ed e' esattamente la parte che rendeva l'angolo NUOVO rispetto al filone funding.) 2. IL FATTO NON REGGE LA PROPRIA SOGLIA DI POTENZA, dichiarata prima di guardare: l'unico effetto e' il decile piu' basso di oi_delta (-0.40% a 24h) e la MDE a 24h e' 1.06%. - La pendenza lineare e' nulla (|t|<1.3), il |ritorno| futuro e' piatto fra i decili (la - 'cascata' non c'e'), e la scomposizione per mese mette quasi tutto in giugno. + La pendenza lineare e' nulla (|t|<1.3 in 4 specificazioni su 4); il |ritorno| futuro e' + quasi piatto fra i decili (0.59-0.75% a 4h, solo il decile 0 leggermente sopra) -> la + 'cascata' non c'e'; e il segno e' ASSENTE nel primo dei tre mesi (maggio y24=+0.02%, + giugno -0.64%, luglio -0.74%): 2 mesi su 3 non sono un plateau, sono due mesi. 3. IL SEGNALE E' UN MASSIMO DI 18 CELLE SU 114 GIORNI: Sharpe 3.6 con SE(Sharpe)=2.0, deflated-Sharpe 0.10 (18 celle) / 0.004 (118 trial) contro un massimo atteso PER CASO di - Sharpe 5.4; implausible_sharpe=True; marginal_vs_tp01 = NEUTRAL; 60% del P&L in 3 giorni. + Sharpe 5.4; implausible_sharpe=True; marginal_vs_tp01 = NEUTRAL; e il P&L e' per il 71% + di giugno e per il 60% concentrato in 3 giorni su 114. 4. ED E' COMUNQUE MORTO ALLA NASCITA: la colonna su cui poggia non e' ricostruibile con i dati che raccogliamo (l'OI della catena e' quello delle OPZIONI, rango ~0.05 con la colonna) e la sorgente e' stata dismessa il 2026-07-30. diff --git a/scripts/research/r0822_ortho_screen.py b/scripts/research/r0822_ortho_screen.py index b91e181..981ca76 100644 --- a/scripts/research/r0822_ortho_screen.py +++ b/scripts/research/r0822_ortho_screen.py @@ -630,6 +630,7 @@ def deep_dive(results) -> None: B = A.tp01_baseline_daily() C = r["C"] best = None + rows_ctl = [] for p in MOM_CONTROL_GRID: fnm = mk_mom(tf="1d", **p) Cm = A.candidate_daily(fnm, tf="1d") @@ -637,18 +638,27 @@ def deep_dive(results) -> None: JB = pd.concat({"B": B, "M": Cm}, axis=1, join="inner").dropna() up = A._sh((1 - W_BLEND) * JB["B"] + W_BLEND * JB["M"]) - A._sh(JB["B"]) row = dict(params=p, sharpe=round(A._sh(Cm), 3), + insample=round(A._sh(Cm[Cm.index < A.HOLDOUT]), 3), hold=round(A._sh(Cm[Cm.index >= A.HOLDOUT]), 3), corr_to_acc=round(float(J["A"].corr(J["M"])), 3), corr_to_tp01=round(float(JB["B"].corr(JB["M"])), 3), uplift_w25_full=round(up, 3)) - print(f" MOM {str(p):<28} Sh {row['sharpe']:+.3f} hold {row['hold']:+.3f} " - f"corr->{r['code']} {row['corr_to_acc']:+.3f} corr->TP01 {row['corr_to_tp01']:+.3f} " - f"uplift {row['uplift_w25_full']:+.3f}") + print(f" MOM {str(p):<28} Sh {row['sharpe']:+.3f} IS {row['insample']:+.3f} " + f"hold {row['hold']:+.3f} corr->{r['code']} {row['corr_to_acc']:+.3f} " + f"corr->TP01 {row['corr_to_tp01']:+.3f} uplift {row['uplift_w25_full']:+.3f}") + rows_ctl.append(row) if best is None or row["corr_to_acc"] > best["corr_to_acc"]: best = row print(f" livello piu' vicino all'accelerazione: {best['params']} corr {best['corr_to_acc']:+.3f}") print(" lettura: corr alta col LIVELLO => l'accelerazione non e' una dimensione nuova; " - "corr bassa => e' distinta ma resta da spiegare perche' non sopravvive al DSR.\n") + "corr bassa => e' distinta ma resta da spiegare perche' non sopravvive al DSR.") + bh = max(rows_ctl, key=lambda x: x["hold"]) + bi = max(rows_ctl, key=lambda x: x["insample"]) + print(f" TRAPPOLA DICHIARATA: la cella di controllo migliore SULL'HOLD-OUT e' {bh['params']} " + f"(hold {bh['hold']:+.3f}); quella scelta IN-SAMPLE-ONLY e' {bi['params']} " + f"(IS {bi['insample']:+.3f}, hold {bi['hold']:+.3f}). Se coincidono, il controllo e'" + " selezionabile; se no, guardare la colonna hold e' selezione-sull'hold-out (trappola 3)" + " e NON promuove nulla.\n") NOT_TESTED = [ diff --git a/scripts/research/r0822_term_structure.py b/scripts/research/r0822_term_structure.py index 67521b3..e5cb1f4 100644 --- a/scripts/research/r0822_term_structure.py +++ b/scripts/research/r0822_term_structure.py @@ -156,28 +156,62 @@ def sezione0(chain: pd.DataFrame) -> None: # =========================================================================================== # SEZIONE 1 — RICOSTRUZIONE DELLA CURVA + CERTIFICAZIONE # =========================================================================================== -def _atm_iv(g: pd.DataFrame) -> float: - """ATM IV di UNO snapshot+scadenza: interpolazione sul DELTA, mai estrapolazione.""" - vals = [] - for typ, tgt in (("C", 0.5), ("P", -0.5)): - s = g[g.option_type == typ] - if len(s) >= 2: - d = s["delta"].to_numpy() - if d.min() <= tgt <= d.max(): - o = np.argsort(d) - vals.append(float(np.interp(tgt, d[o], s["iv"].to_numpy()[o]))) - return float(np.mean(vals)) if vals else np.nan +def _interp_at(gid: np.ndarray, x: np.ndarray, y: np.ndarray, ngrp: int, + target: float) -> np.ndarray: + """Interpolazione lineare di y(x) a `target`, per ogni gruppo, VETTORIALE e senza + estrapolazione (NaN se il gruppo non racchiude il target). + + `gid` dev'essere ordinato crescente e (gid, x) lessicograficamente ordinati. + Equivalente a np.interp gruppo-per-gruppo: qui serve solo perche' i gruppi sono ~30.000 + e un ciclo python li paga a caro prezzo su una VPS a 2 core condivisa. + """ + n_tot = np.bincount(gid, minlength=ngrp) + start = np.concatenate([[0], np.cumsum(n_tot)[:-1]]) + n_below = np.bincount(gid[x <= target], minlength=ngrp) + ok = (n_below > 0) & (n_below < n_tot) + out = np.full(ngrp, np.nan) + if not ok.any(): + return out + lo = (start + n_below - 1)[ok] + hi = lo + 1 + x0, x1, y0, y1 = x[lo], x[hi], y[lo], y[hi] + w = np.where(x1 != x0, (target - x0) / np.where(x1 != x0, x1 - x0, 1.0), 0.0) + out[ok] = y0 + w * (y1 - y0) + return out def build_atm_by_expiry(chain: pd.DataFrame) -> pd.DataFrame: + """ATM IV per (asset, ora, scadenza) = IV interpolata sul DELTA a 0.5 (call) / -0.5 (put). + + Perche' il delta e non "lo strike piu' vicino allo spot": il delta referenzia il FORWARD. + A 90 giorni il forward sta lontano dallo spot, quindi lo strike ATM-spot e' un'opzione OTM + e la sua IV porta dentro lo SKEW — che e' un altro filone e falserebbe la pendenza in modo + sistematico e crescente col tenore. Nessuna estrapolazione: se il delta 0.5 non e' racchiuso + dai quotati, quella scadenza a quell'ora non ha ATM e viene scartata. + """ f = CACHE / "atm_by_expiry.parquet" if f.exists(): return pd.read_parquet(f) - q = chain[chain.quoted & (chain.dte > DTE_MIN)] - out = (q.groupby(["asset", "hr", "exp"]) - .apply(lambda g: pd.Series({"iv": _atm_iv(g), "dte": g["dte"].median()}), - include_groups=False) - .reset_index().dropna(subset=["iv"])) + q = chain[chain.quoted & (chain.dte > DTE_MIN)].copy() + keys = ["asset", "hr", "exp"] + q["gid"] = q.groupby(keys, sort=True).ngroup() + ngrp = int(q["gid"].max()) + 1 + ivs = [] + for typ, tgt in (("C", 0.5), ("P", -0.5)): + s = q[q.option_type == typ].sort_values(["gid", "delta"]) + v = np.full(ngrp, np.nan) + if len(s): + g2 = s["gid"].to_numpy() + uniq, inv = np.unique(g2, return_inverse=True) + v[uniq] = _interp_at(inv, s["delta"].to_numpy(), s["iv"].to_numpy(), len(uniq), tgt) + ivs.append(v) + M = np.vstack(ivs) + with np.errstate(invalid="ignore"): + cnt = np.sum(np.isfinite(M), axis=0) + iv = np.where(cnt > 0, np.nansum(np.where(np.isfinite(M), M, 0.0), axis=0) / np.maximum(cnt, 1), np.nan) + out = (q.groupby(keys, sort=True)["dte"].median().reset_index()) + out["iv"] = iv + out = out.dropna(subset=["iv"]).reset_index(drop=True) CACHE.mkdir(parents=True, exist_ok=True) out.to_parquet(f) return out @@ -266,7 +300,7 @@ def px_hourly(a: str) -> pd.Series: index=pd.DatetimeIndex(pd.to_datetime(d["datetime"], utc=True))) -def _block_pctl(x: np.ndarray, y: np.ndarray, stat: float, L: int, n: int = 800, +def _block_pctl(x: np.ndarray, y: np.ndarray, stat: float, L: int, n: int = 400, seed: int = 20260822) -> float: """Percentile della correlazione osservata contro il null a BLOCCHI (y ricampionata da blocchi non allineati a x): conserva l'autocorrelazione e rompe solo l'accoppiamento.""" @@ -400,6 +434,51 @@ def build_atm_cost(chain: pd.DataFrame) -> pd.DataFrame: return atm + +def _roll_table(r: pd.DataFrame, H: int) -> pd.DataFrame: + """Roll IMPLICITO vs REALIZZATO su orizzonte H ore, appaiato per (scadenza, ISTANTE D'INGRESSO). + + Appaiato sull'INGRESSO e non sull'uscita: e' l'uscita che le due letture spostano, e un + inner-join sulla data d'uscita terrebbe solo i casi in cui non e' successo niente (trappola + incontrata 3 volte nel progetto: venue-watch, GTAA, VRP profit-take). + impl = iv_curva(T-H) al tempo t - iv(T) al tempo t (se la curva non si muovesse) + real = iv(T-H) al tempo t+H - iv(T) al tempo t (cio' che e' successo davvero) + """ + r = r[["hr", "exp", "iv", "dte"]].dropna() + nxt = r[["hr", "exp", "iv"]].copy() + nxt["hr"] = nxt["hr"] - pd.Timedelta(hours=H) + j = r.merge(nxt.rename(columns={"iv": "iv_fut"}), on=["hr", "exp"], how="inner") + j["T2"] = j["dte"] - H / 24.0 + j = j[j["T2"] > DTE_MIN] + if j.empty: + return pd.DataFrame(columns=["dte", "impl", "real"]) + # curva dell'ora t, ordinata: chiave monotona globale gid*1000 + dte -> searchsorted unico + c = r.sort_values(["hr", "dte"]).copy() + c["gid"] = c.groupby("hr", sort=True).ngroup() + key = c["gid"].to_numpy() * 1000.0 + c["dte"].to_numpy() + w = (c["iv"].to_numpy() / 100.0) ** 2 * c["dte"].to_numpy() # varianza totale + gmap = dict(zip(c["hr"].to_numpy(), c["gid"].to_numpy())) + n_tot = np.bincount(c["gid"].to_numpy()) + start = np.concatenate([[0], np.cumsum(n_tot)[:-1]]) + gq = np.array([gmap[h] for h in j["hr"].to_numpy()]) + pos = np.searchsorted(key, gq * 1000.0 + j["T2"].to_numpy(), side="left") + lo, hi = pos - 1, pos + ok = (lo >= start[gq]) & (hi < start[gq] + n_tot[gq]) + ivT2 = np.full(len(j), np.nan) + if ok.any(): + l, h2 = lo[ok], hi[ok] + x0 = key[l] - gq[ok] * 1000.0 + x1 = key[h2] - gq[ok] * 1000.0 + t2 = j["T2"].to_numpy()[ok] + ww = np.where(x1 != x0, (t2 - x0) / np.where(x1 != x0, x1 - x0, 1.0), 0.0) + var = w[l] + ww * (w[h2] - w[l]) + ivT2[ok] = np.sqrt(np.maximum(var, 0.0) / t2) * 100.0 + j = j.assign(ivT2=ivT2).dropna(subset=["ivT2"]) + return pd.DataFrame({"dte": j["dte"].to_numpy(), + "impl": j["ivT2"].to_numpy() - j["iv"].to_numpy(), + "real": j["iv_fut"].to_numpy() - j["iv"].to_numpy()}) + + def sezione3(chain: pd.DataFrame, atm_exp: pd.DataFrame, curve: pd.DataFrame) -> None: hr("SEZIONE 3 — Q2: il CARRY di vol (roll-down) e' incassabile al bid-ask reale?") print(" Definizione senza modello: un'opzione a tenore T, dopo dt, sta a T-dt. Se la curva non") @@ -412,25 +491,7 @@ def sezione3(chain: pd.DataFrame, atm_exp: pd.DataFrame, curve: pd.DataFrame) -> roll_day = {} for H in (24, 168): for a in ASSETS: - r = atm_exp[atm_exp.asset == a] - by_hr = {h: g.sort_values("dte") for h, g in r.groupby("hr")} - rows = [] - for _, g in r.set_index(["exp", "hr"]).sort_index().groupby(level=0): - g = g.droplevel(0).sort_index() - fut = g["iv"].reindex(g.index + pd.Timedelta(hours=H)) - for t, iv0, d0, iv1 in zip(g.index, g["iv"].to_numpy(), - g["dte"].to_numpy(), fut.to_numpy()): - T2 = d0 - H / 24.0 - gg = by_hr.get(t) - if not np.isfinite(iv1) or T2 <= DTE_MIN or gg is None or len(gg) < 2: - continue - td, ivd = gg["dte"].to_numpy(), gg["iv"].to_numpy() - if td.min() > T2 or td.max() < T2: - continue - w = (ivd / 100.0) ** 2 * td - ivT2 = np.sqrt(np.interp(T2, td, w) / T2) * 100.0 - rows.append((d0, ivT2 - iv0, iv1 - iv0)) - d = pd.DataFrame(rows, columns=["dte", "impl", "real"]) + d = _roll_table(atm_exp[atm_exp.asset == a], H) d = d[(d.dte >= 4) & (d.dte <= 45)] if len(d) < 50: print(f" {a} dt={H}h: campione insufficiente ({len(d)})") diff --git a/scripts/research/r0822_vol_size.py b/scripts/research/r0822_vol_size.py index dc9850a..d00ae9e 100644 --- a/scripts/research/r0822_vol_size.py +++ b/scripts/research/r0822_vol_size.py @@ -182,6 +182,7 @@ def extract(asset: str, off: int) -> dict: ts_close=ltf["timestamp"].values.astype(np.int64) + LTF_MIN * 60_000, day_ent=idx[idxE].floor("D") if len(idxE) else pd.DatetimeIndex([], tz="UTC"), ) + out["base_daily"] = equity_daily(out, size_flat(out)) # riusata da VTL/§9: calcolarla 1 volta R._CACHE.clear() # ent = 18k dict per chiave: non accumulare return out @@ -413,8 +414,7 @@ def main() -> None: for o in offs: sz = {} for a in ASSETS: - base_a = equity_daily(EX[o][a], size_flat(EX[o][a])) - sz[a] = sizes_from_L(EX[o][a], leverage_series(base_a, **sp)) + sz[a] = sizes_from_L(EX[o][a], leverage_series(EX[o][a]["base_daily"], **sp)) SLE[nm][o] = leg_daily(EX[o], sz) BK[nm][o] = book(TP, SLE[nm][o]) MTP[nm][o] = 1.0 @@ -546,6 +546,13 @@ def main() -> None: print(f" {n:<28} IS {is_med[n]:>7.3f} HOLD {ho_med[n]:>7.3f} FULL {fu_med[n]:>7.3f}") best = max(CAUSAL, key=lambda x: is_med[x]) print(f"\n cella scelta AL BUIO: {best} (IS {is_med[best]:.3f} vs baseline {base_is:.3f})") + iso_is = {n: float(np.median([sh(ins(BK[n][o])) - sh(ins(CTRL[n][o])) for o in offs])) + for n in CAUSAL} + best_iso = max(CAUSAL, key=lambda x: iso_is[x]) + print(f" cella scelta AL BUIO **a ISO-PESO**: {best_iso} (dIS iso {iso_is[best_iso]:+.3f})") + print(f" la stessa cella a iso-peso: dShFULL {RES[best]['iF']:+.3f} " + f"({RES[best]['niF']}/{len(offs)}), dShHOLD {RES[best]['iH']:+.3f} " + f"({RES[best]['niH']}/{len(offs)})") trials = [fu_med[n] for n in CAUSAL] + [med_F] dsr, sr0 = A.deflated_sharpe(fu_med[best], trials, BK[best][can]) dsr_b, _ = A.deflated_sharpe(med_F, trials, base_b[can]) @@ -599,9 +606,9 @@ def main() -> None: f"attive {imp.get('active_frac', 0)*100:.1f}% perdite/attive " f"{imp.get('loss_frac', 0)*100:.1f}% Calmar {imp.get('calmar', 0):.1f}") cols = {"TP01": TP, "SKH01": SLE["BASE"][can]} - for nm in [best_sleeve, list(BVT)[0]]: - m = vol(SLE[nm][can]) / vol(SLE["BASE"][can]) - wp = {"TP01": W_TP, "SKH01": W_SKH * m} + for nm in [best, best_sleeve] + list(BVT)[:2]: + m_s = vol(SLE[nm][can]) / vol(SLE["BASE"][can]) + wp = {"TP01": W_TP * MTP[nm][can], "SKH01": W_SKH * m_s} g = weights_tilt_null(cols, {"TP01": W_TP, "SKH01": W_SKH}, wp, caps={"SKH01": 0.5}, floor=0.05, n=300, k_seen=len(ALL)) print(f" tilt-null {nm:<26} peso equiv SKH {wp['SKH01']/sum(wp.values()):.3f} " @@ -634,6 +641,57 @@ def main() -> None: print(f" {label:<28}{tm:>10.3f}{tr:>10.3f}{tm-tr:>9.3f}{no:>13}{ns:>11}{tu:>12.1f}") print(" 'ordini eseg' e 'sotto-min' sono sui due asset sommati, su tutta la storia 1h.") + # ------------------------------------------------------------------ §9 per anno + deriva + print("\n" + "-" * 112) + print(" 9. SCOMPOSIZIONE PER ANNO del dSharpe a ISO-PESO (regola codificata dopo SOL, 22/08:") + print(" un contributo positivo si scompone per ANNO prima di crederci) + deriva della size") + print("-" * 112) + yrs = sorted({int(y) for y in BK["BASE"][can].index.year}) + top = [best, best_iso] + [max(BVT, key=lambda x: RES[x]["iF"])] + top = list(dict.fromkeys(top)) + print(f" {'variante':<28}" + "".join(f"{y:>8}" for y in yrs)) + for nm in top: + cells = [] + for y in yrs: + v = [] + for o in offs: + a_ = BK[nm][o][BK[nm][o].index.year == y] + c_ = CTRL[nm][o][CTRL[nm][o].index.year == y] + if len(a_) > 20: + v.append(sh(a_) - sh(c_)) + cells.append(float(np.median(v)) if v else float("nan")) + npos = int(np.nansum(np.asarray(cells) > 0)) + print(f" {nm:<28}" + "".join(f"{c:>+8.2f}" for c in cells) + + f" anni positivi {npos}/{len(yrs)}") + print() + for nm in top: + if nm in SZ: + szs = np.concatenate([SZ[nm](EX[can][a]) for a in ASSETS]) + dys = pd.DatetimeIndex(np.concatenate([EX[can][a]["day_ent"].values for a in ASSETS])) + elif nm in VTL: + szs, dys = [], [] + for a in ASSETS: + szs.append(sizes_from_L(EX[can][a], + leverage_series(EX[can][a]["base_daily"], **VTL[nm]))) + dys.append(EX[can][a]["day_ent"].values) + szs = np.concatenate(szs); dys = pd.DatetimeIndex(np.concatenate(dys)) + else: + L = leverage_series(BK["BASE"][can], BVT[nm]["tv"], BVT[nm]["w"], BVT[nm]["cap"]) + szs, dys = [], [] + for a in ASSETS: + szs.append(sizes_from_L(EX[can][a], L)) + dys.append(EX[can][a]["day_ent"].values) + szs = np.concatenate(szs); dys = pd.DatetimeIndex(np.concatenate(dys)) + s = pd.Series(szs, index=dys) + med = s.groupby(s.index.year).median() + rho = float(np.corrcoef(np.arange(len(med)), med.values)[0, 1]) if len(med) > 2 else float("nan") + print(f" size mediana per anno — {nm:<22}" + + "".join(f"{med.get(y, float('nan')):>8.2f}" for y in yrs) + + f" corr col tempo {rho:+.2f}") + print(" ⚠️ una size che DERIVA col tempo non e' controllo del rischio: e' un tilt temporale") + print(" (la vol crypto e' scesa lungo il campione -> qualunque 1/vol pesa di piu' il") + print(" periodo recente, che e' anche l'hold-out). La riga 'corr col tempo' la misura.") + print("\n" + "=" * 112) print(f" fatto in {time.time()-t0:.0f}s") print("=" * 112) @@ -652,8 +710,7 @@ def netted_target(asset: str, ex: dict, nm, SZ, VTL, BVT, SLE, TP, can, df1h) -> elif nm in SZ: sizes = SZ[nm](ex) elif nm in VTL: - base_a = equity_daily(ex, size_flat(ex)) - sizes = sizes_from_L(ex, leverage_series(base_a, **VTL[nm])) + sizes = sizes_from_L(ex, leverage_series(ex["base_daily"], **VTL[nm])) else: # BOOKVT L = leverage_series(book(TP, SLE["BASE"][can]), BVT[nm]["tv"], BVT[nm]["w"], BVT[nm]["cap"]) sizes = sizes_from_L(ex, L) diff --git a/scripts/research/r0822_vrp_real_quotes.py b/scripts/research/r0822_vrp_real_quotes.py index d53e924..684fb04 100644 --- a/scripts/research/r0822_vrp_real_quotes.py +++ b/scripts/research/r0822_vrp_real_quotes.py @@ -325,9 +325,16 @@ def main() -> None: for a in ASSETS: V = CB.dvol_series(a) win = V[(V.index >= puts["ts"].min()) & (V.index <= puts["ts"].max())] - pct = float((V[V.index < win.index[0]].values[:, None] < win.values).mean()) - print(f" {a}: DVOL nel campione min {win.min():.1f} / mediana {win.median():.1f} / " - f"max {win.max():.1f} → percentile mediano vs storia 2021+ = {pct:.0%}") + hist = V[V.index < win.index[0]].to_numpy(float) + print(f" {a}: DVOL nel campione min {win.min().1f} / mediana {win.median().1f} / " + f"max {win.max().1f}") + print(f" vs storia 2021+ ({len(hist)} giorni) la MEDIANA del campione sta al " + f"{float((hist < win.median()).mean()).0%} pctl, il suo MASSIMO al " + f"{float((hist < win.max()).mean()).0%} pctl") + print("\n \u26a0\ufe0f Due statistiche diverse, da tenere distinte: CLAUDE.md cita «DVOL max al 24\u00b0/28\u00b0\n" + " pctl» dal campione del 30/07. Un MASSIMO cresce con la finestra — e' una statistica\n" + " di ESTREMO, non di livello. Per dire in che regime siamo la lente e' la MEDIANA, ed\n" + " e' bassissima.") # ------------------------------------------------------------ §2 CAMPIONE hr("§2 IL CAMPIONE — quante settimane ci sono DAVVERO") @@ -446,8 +453,16 @@ def main() -> None: print(f" {a:6s} {lot:>7.2f} {wid:>10,.0f} {wid * lot - tr['cred'].median() * lot:>11,.0f}$ " f"{tr['k_short'].median() * lot:>12,.0f}$ {tr['cred'].median() * lot:>12,.2f}$") print(" Sul rischio DEFINITO un lotto minimo inverse sta dentro $600. Sul cash-secured no.\n" - " Quale dei due valga dipende dal regime di margine del conto, che NON e' leggibile\n" - " da dati pubblici: si legge sul conto. E' un limite di questa misura, non un dettaglio.") + " Quale dei due valga dipende dal regime di margine del conto (standard vs portfolio),\n" + " che NON e' leggibile da dati pubblici.") + print(" ⚠️ E c'e' una domanda PRIMA di quella, che nessun dato pubblico chiude: un saldo in\n" + " USDC puo' marginare un'opzione INVERSE (collaterale in BTC/ETH)? Se no, la famiglia\n" + " con 3 mesi di storia non e' negoziabile da questo conto a NESSUN capitale.\n" + " Precedente esatto: il 26/07 GTAA01 aveva 5 settimane di misure sopra l'assunzione\n" + " che i 6 ETF fossero comprabili — il blocco PRIIPs si scopri' con UN ordine di prova.\n" + " La regola che ne usci' («la negoziabilita' sul conto REALE si verifica quando lo\n" + " sleeve entra in RICERCA, non quando entra nel book») si applica qui identica, e\n" + " questo script NON puo' eseguirla: richiede il conto, non i dati.") # ------------------------------------------------------------ §4 BACKTEST hr("§4 BACKTEST SU QUOTE REALI — griglia dichiarata") diff --git a/scripts/research/r0822_xs_lite.py b/scripts/research/r0822_xs_lite.py index 605af8b..107bbf0 100644 --- a/scripts/research/r0822_xs_lite.py +++ b/scripts/research/r0822_xs_lite.py @@ -535,6 +535,25 @@ def main() -> None: f"{dg['med_ticket']:>11.0f}{dg['orders_py']:>10.0f}" f"{trk:>12.2f}{' ⚠' if trk < 0.8 else ' '}") print() + print(f" {'cella':<13}{'|dw| mediano al ribil.':>24}{'capitale sleeve minimo':>25}{' = book al 15%':>17}") + for nm in ("MAJ19 k=1", "MAJ19 k=2", "MAJ19 k=3", "MAJ19 k=5*"): + cell = xs_cells[nm] + Weff = xs_effective_weights(cell["W"], cell["scale"]) + chg = np.abs(np.diff(cell["W"], axis=0)).sum(axis=1) > 1e-12 # giorni di ribilanciamento + rows = np.where(chg)[0] + 1 + d = np.abs(np.diff(Weff, axis=0))[rows - 1] + med = float(np.median(d[d > 1e-9])) if np.any(d > 1e-9) else float("nan") + capmin = MIN_ORDER_HL / med if med > 0 else float("nan") + print(f" {'XS ' + nm.split()[1]:<13}{med:>24.4f}{capmin:>24.0f}$" + f"{capmin/0.15:>16.0f}$") + print(" ^ ARITMETICA: il ticket di un ribilanciamento e' |dw|*capitale_sleeve. Il min-order") + print(" morde sotto il 'capitale sleeve minimo'. Per il CANONICO k=5 quel numero e' ~150$") + print(" di sleeve = ~1.000$ di book al 15%, non 20.000$. La soglia dichiarata dal progetto") + print(" veniva da una diagnostica di TURNOVER (quota di turnover sopra il min-order), che a") + print(" capitale piccolo e' bassa perche' salta la deriva GIORNALIERA del vol-target — la") + print(" quale non porta segnale. Stessa lezione gia' imparata su TP01 ('a $600 il min-order") + print(" $5 e' GIA' la banda ottimale'), non applicata a XS01.") + print() print(" 'gambe eseg' = quota delle VARIAZIONI di posizione sopra il min-order (la diagnostica") print(" su cui si basa la soglia '$20k'). 'haircut' = quanto ne costa in Sharpe. Sono cose") print(" DIVERSE: il turnover saltato e' la deriva del vol-target, non il segnale.") @@ -710,6 +729,40 @@ def main() -> None: print(f" break-even di costo per gamba: XS k=2 {_be(be.get('XS MAJ19 k=2'))}, " f"XS k=5 {_be(be.get('XS MAJ19 k=5*'))}, XSR k=2 {_be(be.get('XSR ALL50 k=2'))}, " f"XSR pieno {_be(be.get('XSR ALL50 k=pieno*'))}") + print("-" * 108) + print(" 1. CONCENTRARE NON COMPRA RENDIMENTO, COMPRA ESEGUIBILITA'. Il null del de-levering") + print(" boccia ogni k tranne i due che sono fortuna d'ancora (XS k=4: fase0 al 90° pctl,") + print(" mediana 1.10 < 1.15 del canonico; XSR k=6: +0.03 su 1.65). Nessuna concentrazione") + print(" batte il proprio originale a PARI maxDD.") + print(" 2. MA IL COSTO E' MOLTO PIU' BASSO DEL PREVISTO, e la mia ipotesi (a) era sbagliata:") + print(" mi aspettavo Sh ~ sqrt(ampiezza) e quindi il 40-60% dell'edge a k=2-3. Misurato:") + print(" XS-LITE k=2 conserva il 98% del canonico A MEDIANA DI FASE con 4 gambe invece di 10,") + print(" e N_hhi scende 6.9 -> 3.0. La radice dell'ampiezza NON descrive questa curva: la") + print(" dispersione cross-section e' concentrata negli ESTREMI del ranking, e le gambe 3-5") + print(" per lato aggiungono rischio quanto segnale. Il collasso vero e' solo a k=1 (41%).") + print(" 3. IL MURO NON E' IL MIN-ORDER, E' IL PESO DELLO SLEEVE. Con $600 DEDICATI, XS01 gira") + print(" gia' oggi a k=5 (tracking 1.04, haircut -0.04). E' al 15% del book che muore: a") + print(" sleeve $90 il canonico esegue il 4% delle variazioni e il lordo scende al 13% del") + print(" target. A quel capitale k=2 e' l'unica cella CON EDGE che traccia (1.01: k=1 traccia") + print(" ma non ha edge, Sh 0.13). Il '~$20k' e'") + print(" quindi il prodotto di due scelte (peso 15% + k=5), non una proprieta' del segnale.") + print(" 4. XSR01 NON SI CONCENTRA: k<=3 perde l'edge IN-SAMPLE (IS 0.96 -> 0.28) e il gate") + print(" marginale lo declassa ad ADDS -> NOISE (has_insample_edge=False). Ipotesi (c)") + print(" CONFERMATA: li' l'ampiezza E' l'edge. E muore a ~28 bps/gamba di costo totale a") + print(" OGNI k, contro >50 bps di XS-LITE: concentrare non ripara il rischio #1 di XSR01.") + print(" 5. ⛔ NIENTE DI QUESTO E' DEPLOYABILE OGGI, e non per una ragione misurata qui:") + print(" XS01/XSR01 vivono su HYPERLIQUID, e la decisione dell'operatore del 26/07 tiene") + print(" il 100% del capitale su Deribit fino a $20k. A $20k il canonico e' eseguibile da") + print(" solo. XS-LITE serve SE e QUANDO si apre HL sotto quella soglia — non prima.") + print("-" * 108) + print(" GATE PRE-REGISTRATO (dichiarato ORA, prima di qualunque dato nuovo):") + print(" XS-LITE k=2 su MAJ19 si riapre SOLO se si apre un conto Hyperliquid con >= $300") + print(" dedicati allo sleeve, e in quel caso si deploya il k CANONICO (5) se il capitale") + print(" dello sleeve e' >= $750, k=2 sotto. Nessun nuovo backtest serve per questa scelta:") + print(" e' gia' decisa dalla tabella (5) e non va ri-ottimizzata guardando i dati nuovi.") + print(" Come CANDIDATO a sleeve XS-LITE resta bocciato: DSR FAIL a ogni conteggio e null") + print(" del de-levering non superato. Non si ri-testa prima del 2027-06 (3.5 anni di") + print(" storia HL, di cui >=1.5 fuori dal campione di scoperta di XS01).") print("=" * 108) From 14c1a75a7eec11c088ca1a22e3bfa7bfdd612999 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:10:03 +0000 Subject: [PATCH 004/114] =?UTF-8?q?research(wave-0822):=20GROWTH-POLICY=20?= =?UTF-8?q?=E2=80=94=20il=20libro=20gira=20al=207%=20di=20Kelly,=20e=20ogn?= =?UTF-8?q?i=20gate=20del=20progetto=20e'=20cieco=20alla=20scala?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 24 +++++++ scripts/research/r0822_flow_squeeze.py | 45 ++++++++++--- scripts/research/r0822_oi_pin.py | 81 +++++++++++++++-------- scripts/research/r0822_prop_alloc.py | 38 ++++++++--- scripts/research/r0822_term_structure.py | 25 +++++-- scripts/research/r0822_vol_size.py | 51 ++++++++++---- scripts/research/r0822_vrp_real_quotes.py | 42 ++++++++++-- scripts/research/r0822_xs_lite.py | 58 ++++++++++------ 8 files changed, 275 insertions(+), 89 deletions(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index f2ff662..450eb89 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -7,6 +7,7 @@ null de-levering superato + eseguibilita' al capitale dichiarato. | # | filone | verdetto | in una riga | |---|---|---|---| | 9 | ADAPTIVE-HORIZON | **SCARTATO** | il "vincitore adattivo" ha lookback incollato al bordo il 100% del tempo = TSMOM costante a 20g (corr 1.000, dSh 0.00 in 8/8 anni e 0/12 ancore); isolando le celle davvero adattive -> NEUTRAL, corr→TP01 0.90 = TP01 travestito || 4 | DEALER-GAMMA | **SCARTATO** | la gamba tradeable e' INCOERENTE (BTC non separa, ETH separa al ROVESCIO); cio' che resta e' quasi tutto DVOL (corr 0.66), gia' refutato il 26/06. DSR 0.440 e la cella migliore sta SOTTO il massimo atteso dal rumore | +| 2 | GROWTH-POLICY | **LEAD** (condizione, non data) | il libro gira al **7% di Kelly** e raccoglie il **15%** della crescita massima in log; il gradino eseguibile 1.00x->1.25-1.50x vale 14.7a -> 12.9-11.6a al muro. Ma un solo giorno -10% all'anno porta k* da ~10x a **2x**: non si decide su un conto vero con la lente close-only | ## Note che sopravvivono ai singoli filoni @@ -45,3 +46,26 @@ informazione. `gamma_flip_level` congelato nel **61%** dei passi su BTC. strike/ora, quote_status ok ~100%, segno giusto) — **ma la soglia non e' portabile** (`dng=0` corrisponde a `GEX=+5.8e6` su BTC): ricalibrarla sui 90 giorni sarebbe la selezione che il progetto rifiuta. Riapertura non prima di **~30-40 episodi di regime = ~2 anni (meta' 2028)**. + +### 2 — GROWTH-POLICY (r0822_growth_policy.py, 241 celle dichiarate) +✅ **Controllo di replica superato**: il motore riproduce `r0807_piano_netto` §2 al numero pubblicato +(EUR250/m -> 19,8a / P 52%; EUR500/m -> 14,7a / P 99%) con path diversi. +📌 **Il risultato strutturale: ogni gate del progetto e' INVARIANTE ALLA SCALA.** Lo Sharpe misura +1,31 a OGNI cella del knob di leva -> `deflated_sharpe` e `marginal_vs_tp01` non falliscono, **non +vedono la variabile**. E' per questo che la leva e' l'unica leva del libro mai esaminata in 2 mesi. +- Serie de-luckata x0,89: drift 17,11%/a, vol 11,37%, Sharpe 1,51, peggior giorno -3,95%. + Netto fee E netto fisco d'accumulo (33% + carry 4a + patrimoniale 0,2%). +- k* empirico **~10-14x** (banda 10x a 1500 path / 14x a 2500: la POSIZIONE non e' risolta), + Kelly gaussiano 13,2x. Libro a k=1 -> g 17,0%/a, maxDD mediano 18,1%, P(DD>30%) 1,4%. +- **Fragilita' che decide il verdetto:** k* e' lineare nell'errore del drift (stimato su 7,4 anni) — + -1 SE -> 10x, -2 SE -> 7x, drift dimezzato -> 7x, drift zero -> 0,5x. E allo **stress di coda** + un giorno -10% **all'anno** porta k* a **2x**, -15%/anno a **1x**. +- ⚠️ **Controllo di plausibilita' FALLITO dall'agente sul proprio output** (e riportato): capitale + mediano **$4,4e12** da $600 a k=14x -> il modello assume ritorno **indipendente dalla size**. + A k>2 i numeri sono aritmetica, non previsioni. +- **Gate pre-registrato (condizione, non data): nessuna proposta di k>1 prima di rifare la curva con + la lente WICK ACCOPPIATA** (`r0725_prop_coupled`) — a leva il breach si valuta sul MINIMO, e la + lente close-only usata qui e' quella misurata **cieca** il 25/07 (raddoppia i breach da daily-loss). +- 📌 **Sottoprodotto da verificare (tocca un'etichetta del libro live):** a `target_vol=20%` la vol + **realizzata** di TP01 e' **12,14%** — il target vale sulla posizione quando c'e', e TP01 e' + long-flat. **L'etichetta sovrastima il rischio preso di ~40%.** diff --git a/scripts/research/r0822_flow_squeeze.py b/scripts/research/r0822_flow_squeeze.py index 072c9fa..89e092f 100644 --- a/scripts/research/r0822_flow_squeeze.py +++ b/scripts/research/r0822_flow_squeeze.py @@ -254,7 +254,8 @@ def sezione2(snap: pd.DataFrame) -> pd.DataFrame: g["p_x24"] = [bboot_p(d[d.odec == k].x24.values, block=48) for k in g.index] print(g.round(3).to_string()) print(" av4/av24 = |ret| futuro medio: e' la gamba 'squeeze/cascata' dell'ipotesi") - print(" (piu' vol dopo un estremo di OI). E' PIATTA: nessuna cascata prevedibile.") + print(" (piu' vol dopo un estremo di OI). E' QUASI PIATTA (0.59-0.75%% a 4h, solo il decile 0") + print(" leggermente sopra): la 'cascata' prevedibile non c'e'.") print("\n--- CONTROLLO: e' solo 'il prezzo si e' appena mosso'? ---") z = lambda s: (s - s.mean()) / s.std() # noqa: E731 @@ -293,9 +294,25 @@ def sezione2(snap: pd.DataFrame) -> pd.DataFrame: for a in ASSETS: sub = d[(d.asset == a) & (d.odec == 0)] cond = (d[d.asset == a].odec == 0).values - ep = int(np.sum(cond[1:] & ~cond[:-1])) + int(cond[0]) - print(f" {a}: ore in decile0={len(sub)} episodi distinti={ep} " + # AMPIEZZA EFFETTIVA: il numero di ORE non e' la taglia campionaria se la + # condizione e' fatta di pochi episodi lunghi (lezione del filone gemello + # dealer-gamma della stessa ondata: 73 "episodi" di cui 6-8 all'80% del tempo). + lens, cur = [], 0 + for v in cond: + if v: + cur += 1 + elif cur: + lens.append(cur); cur = 0 + if cur: + lens.append(cur) + L = np.array(sorted(lens)[::-1]) + print(f" {a}: ore in decile0={len(sub)} episodi distinti={len(L)} " + f"durata mediana={np.median(L):.1f}h max={L.max()}h " + f"quota ore negli 8 episodi piu' lunghi={L[:8].sum()/L.sum():.2f} " f"y4={100*sub.y4.mean():+.3f}% y24={100*sub.y24.mean():+.3f}%") + print(" -> QUI l'ampiezza NON e' la patologia: 86-87 episodi corti (mediana 2h) e i") + print(" primi 8 coprono solo il 20-25% delle ore. Il vincolo binding e' la") + print(" LUNGHEZZA DEL CALENDARIO (90 giorni), non la concentrazione degli episodi.") print("\n scomposizione PER MESE (demean mensile) — il test che il progetto impone") print(" dopo il caso SOL ('un contributo positivo si scompone prima di crederci'):") tab = d[d.odec == 0].groupby("mese")[["y4", "y24"]].agg(["size", "mean"]) @@ -475,6 +492,9 @@ def sezione4(snap: pd.DataFrame) -> None: print(" -> il candidato NON e' la statica travestita: il timing porta qualcosa.") print("\n--- NULL 2: timing CASUALE a pari esposizione (shift circolare, 300 estrazioni) ---") + print(" e' il null LOCATION-MATCHED nella dimensione del tempo: stessa esposizione, stessa") + print(" struttura di episodi, stessa finestra — cambia SOLO QUANDO. Un null piu' ingenuo") + print(" (posizione casuale iid) direbbe di si' a qualunque cosa in una finestra con drift.") base_trig = {} for a in ASSETS: df = A.get(a, "1h") @@ -504,12 +524,19 @@ def sezione4(snap: pd.DataFrame) -> None: sr = float(cd.mean() / cd.std() * np.sqrt(365.25)) rep = A.marginal_vs_tp01(cd) print(f" candidate_daily : n={len(cd)} giorni, Sharpe={sr:.2f}") + n_hold = int((cd.index >= A.HOLDOUT).sum()) print(f" marginal_vs_tp01 : verdetto={rep.get('marginal_verdict')} " - f"corr_beta_tp01={rep.get('beta_to_tp01')} has_insample_edge={rep.get('has_insample_edge')} " + f"beta_to_tp01={rep.get('beta_to_tp01')} has_insample_edge={rep.get('has_insample_edge')} " f"is_hedge={rep.get('is_hedge')} robust_oos={rep.get('robust_oos')} " f"beats_noise_null={rep.get('beats_noise_null')} null_pctl_full={rep.get('null_pctl_full')}") - print(" (robust_oos=False non e' un difetto del candidato: con 90 giorni NON ESISTE il") - print(" multi-cut che il gate richiede. Il gate e' girato e il suo esito e' NEUTRAL.)") + print(f" ATTENZIONE: giorni totali={len(cd)}, giorni dopo HOLDOUT({A.HOLDOUT.date()})" + f"={n_hold} -> COINCIDONO") + print(f" (cand_full_sharpe={rep.get('cand_full_sharpe')} == " + f"cand_hold_sharpe={rep.get('cand_hold_sharpe')}). Il candidato vive INTERAMENTE") + print(" dentro l'hold-out del progetto: FULL e HOLD-OUT sono la STESSA finestra e la") + print(" meta' out-of-sample del gate e' strutturalmente INERTE (robust_oos=False non") + print(" giudica il candidato, dice che il multi-cut non esiste). Da leggere come") + print(" 'girato ma NON informativo sull'OOS', non come un NEUTRAL guadagnato sul campo.") all_sr = list(R.sharpe.values) d18 = A.deflated_sharpe(sr, all_sr, cd) d_all = A.deflated_sharpe(sr, all_sr * 7, cd) @@ -628,8 +655,10 @@ def main() -> None: giugno -0.64%, luglio -0.74%): 2 mesi su 3 non sono un plateau, sono due mesi. 3. IL SEGNALE E' UN MASSIMO DI 18 CELLE SU 114 GIORNI: Sharpe 3.6 con SE(Sharpe)=2.0, deflated-Sharpe 0.10 (18 celle) / 0.004 (118 trial) contro un massimo atteso PER CASO di - Sharpe 5.4; implausible_sharpe=True; marginal_vs_tp01 = NEUTRAL; e il P&L e' per il 71% - di giugno e per il 60% concentrato in 3 giorni su 114. + Sharpe 5.4; implausible_sharpe=True; e il P&L e' per il 71% di giugno e per il 60% + concentrato in 3 giorni su 114. marginal_vs_tp01 gira e dice NEUTRAL, ma va dichiarato + NON INFORMATIVO SULL'OOS: il candidato sta interamente dopo HOLDOUT(2025-01-01), quindi + full e hold-out sono la stessa finestra (Sharpe identico) e il multi-cut non esiste. 4. ED E' COMUNQUE MORTO ALLA NASCITA: la colonna su cui poggia non e' ricostruibile con i dati che raccogliamo (l'OI della catena e' quello delle OPZIONI, rango ~0.05 con la colonna) e la sorgente e' stata dismessa il 2026-07-30. diff --git a/scripts/research/r0822_oi_pin.py b/scripts/research/r0822_oi_pin.py index 87b1f52..47d73e6 100644 --- a/scripts/research/r0822_oi_pin.py +++ b/scripts/research/r0822_oi_pin.py @@ -60,7 +60,8 @@ FEE_SIDE_CONS = A.FEE_SIDE # 5 bps/lato = convenzione conservativa dei bac MAX_H = 50 # ore prima della scadenza da tenere in memoria HTE_GRID = (1, 2, 3, 6, 12, 24, 36, 48) # ore-alla-scadenza all'INGRESSO RULES = ("oi", "gex", "mp") # max OI · max gamma*OI · max pain -PLACEBOS = ("rand", "adj", "jit", "mid", "mr7") +PLACEBOS_GLOB = ("rand", "mid", "mr7") # non dipendono dalla regola +PLACEBOS_RULE = ("adj", "jit") # costruiti sulla distanza DELLA regola SEED = 20260822 RNG = np.random.default_rng(SEED) @@ -144,29 +145,39 @@ def levels_table(chain: pd.DataFrame, spot: dict[str, pd.Series]) -> pd.DataFram "gex": float(K[int(np.argmax(gx))]) if gx.max() > 0 else np.nan, "mp": _max_pain(K, oi_c, oi_p), } - # ---- placebo location-matched ------------------------------------------------- - # rand: permutare l'OI fra gli strike e riprendere l'argmax equivale ESATTAMENTE a - # estrarre uno strike uniforme dalla griglia (l'argmax di un vettore permutato - # e' uniforme sulle posizioni) -> lo si implementa cosi', ed e' esatto. - p_rand = float(K[RNG.integers(len(K))]) - # adj: lo strike ADIACENTE al max-OI (stesso quartiere, OI diverso) - j = i_oi + (1 if (i_oi == 0 or (i_oi < len(K) - 1 and RNG.random() < 0.5)) else -1) - p_adj = float(K[int(np.clip(j, 0, len(K) - 1))]) - # jit: strike arbitrario alla STESSA distanza dallo spot ("0.618 vs 0.58?") - dist = abs(K - S) - d0 = abs(lv["oi"] - S) - cand = np.where((dist >= 0.5 * d0) & (dist <= 1.5 * d0) & (K != lv["oi"]))[0] - p_jit = float(K[cand[RNG.integers(len(cand))]]) if len(cand) else np.nan - # mid: punto medio della griglia quotata = puro artefatto di copertura, zero info OI - p_mid = float(0.5 * (K[0] + K[-1])) + # ---- placebo LOCATION-MATCHED, uno per OGNI regola -------------------------------- + # ⚠ difetto trovato e corretto in sessione: la prima stesura costruiva `adj`/`jit` + # attorno al SOLO max-OI e poi li usava come null anche per max-pain e gamma. Ma + # |K_oi - S| mediano e' ~2,4% e |K_mp - S| ~1,1%: confrontare mp con un placebo + # tarato sulla distanza di oi NON e' location-matched, e' proprio l'errore che il + # null doveva prevenire. Ogni regola ha ora i placebo alla PROPRIA distanza. + dist = np.abs(K - S) + for nm, kv in list(lv.items()): + if not np.isfinite(kv): + out_adj = out_jit = np.nan + else: + i_r = int(np.argmin(np.abs(K - kv))) + j = i_r + (1 if (i_r == 0 or (i_r < len(K) - 1 and RNG.random() < 0.5)) else -1) + out_adj = float(K[int(np.clip(j, 0, len(K) - 1))]) + d0 = abs(kv - S) + cand = np.where((dist >= 0.5 * d0) & (dist <= 1.5 * d0) & (K != kv))[0] + out_jit = float(K[cand[RNG.integers(len(cand))]]) if len(cand) else np.nan + lv[f"adj_{nm}"] = out_adj + lv[f"jit_{nm}"] = out_jit + # placebo GLOBALI (non dipendono dalla regola) + # rand: permutare l'OI fra gli strike e riprenderne l'argmax equivale ESATTAMENTE a + # estrarre uno strike uniforme dalla griglia (l'argmax di un vettore permutato e' + # uniforme sulle posizioni) -> lo si implementa cosi', ed e' esatto, non simulato. + lv["rand"] = float(K[RNG.integers(len(K))]) + # mid: centro della griglia quotata = puro artefatto di copertura, zero informazione OI + lv["mid"] = float(0.5 * (K[0] + K[-1])) rows.append(dict( asset=asset, exp=exp, hh=hh, dte_h=float((exp - hh).total_seconds() / 3600.0), S=float(S), n_k=len(K), tot_oi=float(tot), - k_oi=lv["oi"], k_gex=lv["gex"], k_mp=lv["mp"], - p_rand=p_rand, p_adj=p_adj, p_jit=p_jit, p_mid=p_mid, top_share=float(oi[i_oi] / tot), hhi=float(((oi / tot) ** 2).sum()), k_step=float(np.median(np.diff(K))) if len(K) > 1 else np.nan, + **{f"L_{k}": v for k, v in lv.items()}, )) return pd.DataFrame(rows) @@ -198,9 +209,8 @@ def build_events(lev: pd.DataFrame, spot: dict[str, pd.Series]) -> pd.DataFrame: top_share=float(r["top_share"]), hhi=float(r["hhi"]), k_step=float(r["k_step"]), dow=int(pd.Timestamp(exp).dayofweek)) - lvls = {"oi": r["k_oi"], "gex": r["k_gex"], "mp": r["k_mp"], - "rand": r["p_rand"], "adj": r["p_adj"], "jit": r["p_jit"], - "mid": r["p_mid"], "mr7": float(mr7[a].asof(hh))} + lvls = {c[2:]: r[c] for c in r.index if str(c).startswith("L_")} + lvls["mr7"] = float(mr7[a].asof(hh)) for nm, K in lvls.items(): K = float(K) if K is not None else np.nan if not np.isfinite(K) or K <= 0: @@ -421,12 +431,12 @@ def main() -> None: P("\n LA STESSA STATISTICA SUI LIVELLI PLACEBO — se sale anche li', NON e' pinning:") P(f" {'hte':>4} " + " ".join(f"{('tw_' + n):>10}" for n in - ("oi", "mp", "rand", "jit", "mid", "mr7", "maj", "short"))) + ("oi", "mp", "rand", "jit_mp", "mid", "mr7", "maj", "short"))) for hte in HTE_GRID: e = ev[ev["hte"] == hte] P(f" {hte:>4} " + " ".join( f"{np.nanmean(e[f'tw_{n}']):>10.5f}" for n in - ("oi", "mp", "rand", "jit", "mid", "mr7", "maj", "short"))) + ("oi", "mp", "rand", "jit_mp", "mid", "mr7", "maj", "short"))) P("\n QUANTO SONO LO STESSO SEGNALE? accordo del SEGNO di (K-S) fra max-pain e i placebo:") for n in ("mid", "mr7", "oi", "maj"): agr = [float(np.nanmean(np.sign(ev.loc[ev.hte == h, "d_mp"]) @@ -446,22 +456,25 @@ def main() -> None: P(" placebo: rand=strike uniforme (== permutazione dell'OI, equivalenza esatta) · adj=strike") P(" adiacente al max-OI · jit=strike arbitrario a distanza 0.5-1.5x (analogo Fib±jitter) ·") P(" mid=centro della griglia quotata (zero info OI) · mr7=media 7g dello spot (mean-reversion).") - P(f" {'hte':>4} {'rule':>5} " + " ".join(f"{('Δ vs ' + p):>12}" for p in PLACEBOS)) + P(f" {'hte':>4} {'rule':>5} {'Δ vs adj_r':>12} {'Δ vs jit_r':>12}" + f" {'Δ vs rand':>12} {'Δ vs mid':>12} {'Δ vs mr7':>12}") npos = ntot = 0 for hte in HTE_GRID: e = ev[ev["hte"] == hte] for rl in RULES: cells = [] - for p in PLACEBOS: + for p in (f"adj_{rl}", f"jit_{rl}") + PLACEBOS_GLOB: dd = e[f"tw_{rl}"].to_numpy(float) - e[f"tw_{p}"].to_numpy(float) m, lo, hi, _ = cluster_boot(dd, e["exp"].to_numpy()) - mark = "*" if (np.isfinite(lo) and lo > 0) else (" ") - cells.append(f"{m:>11.5f}{mark}") + cells.append(f"{m:>11.5f}{'*' if (np.isfinite(lo) and lo > 0) else ' '}") ntot += 1 npos += int(np.isfinite(lo) and lo > 0) P(f" {hte:>4} {rl:>5} " + " ".join(cells)) P(f" celle (rule x hte x placebo) in cui lo SPECIALE batte il placebo con IC95>0:" f" {npos}/{ntot} (atteso per caso ~{0.025 * ntot:.0f})") + P(" LETTURA: `adj_r`/`jit_r` chiedono 'e' proprio QUELLO strike?'; `mid`/`mr7` chiedono") + P(" 'serve l'open interest, o basta il centro recente del prezzo?'. La seconda e' la") + P(" domanda che uccide o salva il filone.") # ---------------------------------------------------------------- 5. CONDIZIONAMENTO P("\n[5] CONDIZIONAMENTO — la concentrazione di OI e le scadenze grandi cambiano qualcosa?") @@ -581,7 +594,7 @@ def main() -> None: # DSR: conta TUTTI i trial al rialzo all_sr = [float(v) for v in C["sharpe"].to_numpy(float) if np.isfinite(v)] all_sr_wide = all_sr + [float(v) for v in N["sharpe"].to_numpy(float) if np.isfinite(v)] - n_fact = len(HTE_GRID) * len(RULES) * (1 + len(PLACEBOS)) + 3 * 4 # celle del fatto + condiz. + n_fact = len(HTE_GRID) * len(RULES) * (1 + len(PLACEBOS_GLOB) + len(PLACEBOS_RULE)) + 3 * 4 # celle del fatto + condiz. trials_tot = len(all_sr_wide) + n_fact dsr, sr0 = A.deflated_sharpe(mb["sharpe"], all_sr, dbest, dpy=365.25) dsr_w, _ = A.deflated_sharpe(mb["sharpe"], all_sr_wide, dbest, dpy=365.25) @@ -632,6 +645,18 @@ def main() -> None: f" · MEDIANA ONESTA {ab.get('median'):.2f} · banda [{ab.get('lo'):.2f},{ab.get('hi'):.2f}]" f" · positive {ab.get('frac_positive'):.0%} · gate_pass={ab.get('gate_pass')}") + # LA DOMANDA DECISIVA, sulla banda d'ancora: max-pain aggiunge qualcosa a un livello + # che NON usa opzioni? Mediana delle DIFFERENZE APPAIATE sui 8 hte (mai differenza + # delle mediane: lezione 26/07). + def _fn(rule): + return lambda h: strat_daily(ev, rule, int(h), (0.002, 0.03), "sign", "all", + FEE_SIDE_REAL)[0] + for other in ("mr7", "mid", "oi", "rand"): + dl = A.anchor_luck_delta(_fn("mp"), _fn(other), offsets=list(HTE_GRID)) + P(f" Δ appaiato mp-{other:<4}: mediana {dl.get('median_paired'):+.2f}" + f" · positiva in {dl.get('n_positive')}/{dl.get('n_anchors')} hte" + f" · banda [{dl.get('lo'):+.2f},{dl.get('hi'):+.2f}] · gate_pass={dl.get('gate_pass')}") + # eseguibilita' a $600 P(" eseguibilita' $600 :", end=" ") hc = [] diff --git a/scripts/research/r0822_prop_alloc.py b/scripts/research/r0822_prop_alloc.py index 7522441..b6eb00e 100644 --- a/scripts/research/r0822_prop_alloc.py +++ b/scripts/research/r0822_prop_alloc.py @@ -92,7 +92,7 @@ RAW = ROOT / "data" / "raw" DELUCK = 0.89 # fattore d'ancora MISURATO sul drift (26/07), non stimato a occhio START_WIN = "2024-01-01" # finestra comune ai 5 sleeve (XS01 nasce qui) BLOCK = 20 # blocco del bootstrap, in giorni -N_PATHS = 4000 +N_PATHS = 2500 SPLIT = pl.SPLIT # 80% al trader TARGET_EUR_DAY = 50.0 FIRMS = {f["name"]: f for f in pl.FIRMS} @@ -386,7 +386,15 @@ def hr(t: str = "") -> None: 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(" PROP-ALLOC — allocare per la BARRIERA (P(pass) x P(vivo 12m)) invece che per lo Sharpe") print("=" * 104) @@ -398,6 +406,7 @@ def main() -> None: print(f" de-luck sul drift x{DELUCK} (misurato 26/07) | bootstrap a blocchi di {BLOCK}g, " f"indici CONDIVISI fra configurazioni | {N_PATHS} percorsi") + tick("pannello costruito") off = {"TP01": _d(_tp01_returns()), "SKH01": _d(_skyhook_returns()), "XS01": _d(_xsec_returns()), "VRP01": _d(_vrp_combo_returns()), "GTAA01": _d(_gtaa_daily_returns())} rec = {"TP01": _crypto(1.0, 0.0)["R"], "SKH01": _crypto(0.0, 1.0)["R"], @@ -430,6 +439,7 @@ def main() -> None: print(" non per assenza di rischio (mark infra-settimana misurato il 30/07: mediana -6.9%,") print(" minimo -81.7% del capitale su un trade finito in UTILE).") + tick("A. profilo sleeve") hr("B. IL CONTO ANALITICO — perche' con leva LIBERA la barriera premia lo SHARPE, non la vol") tgt, dd, _dl = FIRMS["HYRO"]["ev"] print(f" P(toccare +{tgt:.0%} prima di -{dd:.0%}), gambler's ruin senza limite di tempo, " @@ -460,6 +470,7 @@ def main() -> None: f"= {len(grid)*len(LEVS)} celle, firm HYRO (eval +10%/-6%/-4%, funded -6%/-4%)") print(f" orizzonte eval {EV_H}g, funded {FU_H}g. Lente ACCOPPIATA. " f"J = P(pass) x P(vivo 12m).") + tick("B. conto analitico") t = time.time() G3 = grid_scan(grid, LEVS, "HYRO", ev_idx, fu_idx) print(f" ({time.time()-t:.0f}s)") @@ -513,14 +524,15 @@ def main() -> None: print(" lette insieme: la leva alta non aumenta la probabilita' di farcela, aumenta la FRETTA.") # ---------------------------------------------------------------- D. i null + tick("C. griglia + leva") hr("D. NULL 1 — l'argmax e' speciale, o e' solo il MASSIMO DI 45 ESTRAZIONI?") - rw = random_weights(300, CRYPTO_PROP, seed=8223) + rw = random_weights(200, CRYPTO_PROP, seed=8223) lev_star = float(best["lev"]) Jr = np.array([objective(w, lev_star, "HYRO", ev_idx, fu_idx)["J"] for w in rw]) pctl = float((Jr < best["J"]).mean() * 100.0) bestofk = 100.0 * len(grid) / (len(grid) + 1) print(f" J dell'argmax = {best['J']:.3f} alla leva {lev_star:g}") - print(f" null: 300 vettori di peso casuali uniformi sul simplesso (stesso campionatore di") + print(f" null: {len(rw)} vettori di peso casuali uniformi sul simplesso (stesso campionatore di") print(f" `portfolio.weights_tilt_null`), valutati alla STESSA leva") print(f" mediana {np.median(Jr):.3f} | p90 {np.percentile(Jr, 90):.3f} | " f"max {Jr.max():.3f} | frazione sopra il LIVE 75/25 = {(Jr > best_live['J']).mean():.0%}") @@ -531,6 +543,7 @@ def main() -> None: print(f" (la meta' bassa del null e' pero' informativa: il LIVE 75/25 e' battuto dal " f"{(Jr > best_live['J']).mean():.0%} dei pesi CASUALI)") + tick("D. null casuale") hr("D-bis. NULL 2 — selezione IN-SAMPLE-ONLY (la scelta guardando J e' selezione come ogni altra)") half = n_hist // 2 R_all = {} @@ -569,6 +582,7 @@ def main() -> None: print(" -> se rho e' alto la mappa e' stabile e la scelta si trasferisce; se e' ~0 l'argmax") print(" e' rumore e la decisione va presa sul MECCANISMO (la leva), non sulla cella.") + tick("D-bis. selezione in-sample") hr("D-ter. NULL DEL DE-LEVERING — la leva e' GIA' dentro l'ottimizzazione, ma va detto") print(" Il null classico ('esiste k<1 che da' lo stesso DD con Sharpe migliore?') qui e'") print(" strutturale: ogni configurazione e' valutata a OTTO leve, quindi nessuna puo' vincere") @@ -586,8 +600,11 @@ def main() -> None: print(" Ri-fare i 24x23x10 offset col recon MTM orario e' fuori budget. Si applica il fattore") print(" di drift MISURATO il 26/07 (x0.89 sul libro live) e si controlla cio' che conta: se") print(" l'ORDINE fra le configurazioni sopravvive alla banda 0.60 (stima 25/07, ri-misurata") - print(" troppo severa) - 1.00 (nessuna correzione).") + print(" troppo severa) - 1.00 (nessuna correzione). L'argmax e' ri-cercato sulle 12 celle-peso") + print(" migliori (non su tutte le 45: costo), il che e' conservativo per la STABILITA'.") cands = [best["w"], best_sh["w"], best_live["w"]] + top_keys = list(G3.sort_values("J", ascending=False).drop_duplicates("w")["w"].head(12)) + top_w = [dict(zip(CRYPTO_PROP, [int(x) / 100 for x in k.split("/")])) for k in top_keys] print(f"\n {'fattore':>9} " + " ".join(f"{('J ' + c):>14}" for c in dict.fromkeys(cands)) + f" {'argmax pesi':>14} {'argmax leva':>11}") for fct in (1.00, 0.89, 0.60): @@ -596,12 +613,13 @@ def main() -> None: wc = dict(zip(CRYPTO_PROP, [int(x) / 100 for x in c.split("/")])) sub = [objective(wc, lv, "HYRO", ev_idx, fu_idx, factor=fct)["J"] for lv in LEVS] cells.append(max(sub)) - g = grid_scan(grid, LEVS, "HYRO", ev_idx, fu_idx, factor=fct) + g = grid_scan(top_w, LEVS, "HYRO", ev_idx, fu_idx, factor=fct) b = g.loc[g["J"].idxmax()] print(f" {fct:>9.2f} " + " ".join(f"{v:>14.3f}" for v in cells) + f" {b['w']:>14} {b['lev']:>11.3f}") # ---------------------------------------------------------------- F. VRP01 / GTAA01 + tick("E. banda ancora") hr("F. E GLI SLEEVE 'INUTILI A $600'? — l'asimmetria del 25/07 §4 vale per XS01, non per tutti") print(" Su un conto funded da $100k gli sleeve fuori portata a $600 diventano ESEGUIBILI per") print(" TAGLIA. Ma la taglia non e' l'unico vincolo: una firm CRYPTO non lista opzioni Deribit") @@ -625,11 +643,12 @@ def main() -> None: print(" 6% verrebbe sfondata dal mark, non dal risultato.") # ---------------------------------------------------------------- G. FTMO + tick("F. VRP/GTAA") hr("G. LA BARRIERA CAMBIA LA RISPOSTA? — HYRO (-6% statico) vs FTMO (-10% statico)") print(f" {'firm':>6} {'argmax pesi':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} {'P(vivo12m)':>11} " f"{'E[pay/a]':>10} | {'LIVE 75/25: leva':>17} {'J':>7}") for fm in ("HYRO", "FTMO"): - g = grid_scan(grid, LEVS, fm, ev_idx, fu_idx) + g = G3 if fm == "HYRO" else grid_scan(grid, LEVS, fm, ev_idx, fu_idx) b = g.loc[g["J"].idxmax()] lv2 = g[g["w"] == "75/25/0"] b2 = lv2.loc[lv2["J"].idxmax()] @@ -640,12 +659,13 @@ def main() -> None: print(" vincolo binding e' la regola di DD, non il capitale'), qui misurata sull'ottimo.") # ---------------------------------------------------------------- H. gate ufficiale sui pesi + tick("G. FTMO") hr("H. GATE UFFICIALE SUI PESI (`portfolio.weights_tilt_null`) — sullo SHARPE, come previsto") cols = {nm: off[nm] for nm in CRYPTO_PROP} try: res = weights_tilt_null(cols, {"TP01": 0.75, "SKH01": 0.25, "XS01": 0.0001}, {nm: max(w_best.get(nm, 0.0), 1e-4) for nm in CRYPTO_PROP}, - floor=0.0, n=300, k_seen=len(grid) * len(LEVS)) + floor=0.0, n=200, k_seen=len(grid) * len(LEVS)) print(f" proposta {best['w']} vs LIVE 75/25 : delta_insample {res['delta_insample']:+.4f} | " f"delta_hold {res['delta_hold']:+.4f} | pctl_hold {res['pctl_hold']:.1f}° " f"(best-of-k {res['bestofk_pctl']:.1f}°) | gate_pass = {res['gate_pass']}") @@ -656,6 +676,7 @@ def main() -> None: print(" e' il libro live e non passa da questo gate.") # ---------------------------------------------------------------- I. tavola operativa + tick("H. gate pesi") hr("I. TAVOLA OPERATIVA — le 3 configurazioni migliori, e quanti conti servono per 50 EUR/g") top = G3.sort_values("J", ascending=False).drop_duplicates("w").head(3) rows_ops = [] @@ -681,6 +702,7 @@ def main() -> None: print(f" -> al massimo {cap_tot/100_000:.0f} conti da $100k. Se ne servono di piu', servono " f"ALTRE FIRM (e ognuna aggiunge rischio di controparte).") + tick("I. tavola operativa") hr("I-bis. LA SCALA DI CONTI — macchineria del 25/07 (`r0725_prop_coupled.simulate`), " "de-luck portato a x0.89") print(f" 36 mesi, EUR {pl.START_EUR:.0f} di cassa iniziale, max {pl.MAX_CONCURRENT} conti, " @@ -698,7 +720,7 @@ def main() -> None: f"{'P(>=50/g)':>10} {'P(zero)':>8}") for pname, (cfgs, slots) in pols.items(): for lev in (float(best["lev"]), 0.75): - r = pc.simulate(cfgs, slots, n_paths=2000, lev=lev, seed=20260822, + r = pc.simulate(cfgs, slots, n_paths=1500, lev=lev, seed=20260822, lens="coupled", start=START_WIN) print(f" {pname:>44} {lev:>6.3f} {r['med']:>10.2f} {r['p90']:>8.2f} " f"{r['p_ge10']:>10.1%} {r['p_target']:>10.1%} {r['p_zero']:>8.1%}") diff --git a/scripts/research/r0822_term_structure.py b/scripts/research/r0822_term_structure.py index e5cb1f4..b85f3c3 100644 --- a/scripts/research/r0822_term_structure.py +++ b/scripts/research/r0822_term_structure.py @@ -115,7 +115,10 @@ def load_quoted_chain() -> pd.DataFrame: df = pd.concat(parts, ignore_index=True) del parts df = df.drop_duplicates(subset=["ts", "asset", "strike", "option_type", "exp"], keep="last") - df["hr"] = df["ts"].dt.floor("h") + # ⚠️ le quote di bite sono datetime64[us], il feed certificato e' [ns]: un join fra + # DatetimeIndex di unita' diverse degrada a Index generico e rompe ogni resample a valle. + df["hr"] = df["ts"].dt.floor("h").dt.as_unit("ns") + df["exp"] = df["exp"].dt.as_unit("ns") df["dte"] = (df["exp"] - df["ts"]).dt.total_seconds() / 86400.0 df["quoted"] = (df["bid"] > 0) & (df["ask"] > 0) & df["iv"].notna() & df["delta"].notna() return df @@ -290,7 +293,9 @@ def sezione1(curve: pd.DataFrame) -> dict: # SEZIONE 2 — Q1: LEAD-LAG. La pendenza precede o segue? # =========================================================================================== def slope_hourly(curve: pd.DataFrame, a: str) -> pd.Series: - s = curve[curve.asset == a].set_index("hr") + s = curve[curve.asset == a].copy() + s["hr"] = pd.to_datetime(s["hr"], utc=True).dt.as_unit("ns") + s = s.set_index("hr") return (s[f"iv_{BACK}"] - s[f"iv_{FRONT}"]).dropna().sort_index() @@ -356,7 +361,9 @@ def sezione2(curve: pd.DataFrame) -> dict: print("\n (c) IL CONFOUND DI QUESTO FILONE — la pendenza e' informazione NUOVA o RV riciclata?") rvp = (r1.rolling(24).std() * np.sqrt(24 * 365) * 100).reindex(idx) rvf = (r1.shift(-24).rolling(24).std().shift(-23) * np.sqrt(24 * 365) * 100).reindex(idx) - ivf = curve[curve.asset == a].set_index("hr")[f"iv_{FRONT}"].reindex(idx) + cf = curve[curve.asset == a].copy() + cf["hr"] = pd.to_datetime(cf["hr"], utc=True).dt.as_unit("ns") + ivf = cf.set_index("hr")[f"iv_{FRONT}"].reindex(idx) c_pp = float(j.slope.corr(rvp)) c_pf = float(j.slope.corr(rvf)) print(f" corr(pendenza, RV 24h PASSATA) {c_pp:+.3f} <-- quanto e' termometro") @@ -400,8 +407,14 @@ def sezione2(curve: pd.DataFrame) -> dict: x, y = sld[m].to_numpy(), fwd[m].to_numpy() c = float(np.corrcoef(x, y)[0, 1]) p = _block_pctl(x, y, c, L=max(3 * h, 7)) - print(f" h={h}g n={m.sum():3d} corr={c:+.3f} pctl vs null a blocchi={p:.3f}") - TRIALS.append((f"Q1 {a}: 3 orizzonti giornalieri", 3)) + c7 = float(np.corrcoef(x[:-7], y[:-7])[0, 1]) if len(x) > 20 else np.nan + c7b = float(np.corrcoef(x[7:], y[7:])[0, 1]) if len(x) > 20 else np.nan + print(f" h={h}g n={m.sum():3d} corr={c:+.3f} pctl vs null a blocchi={p:.3f}" + f" | senza ultima settimana {c7:+.3f} | senza prima settimana {c7b:+.3f}") + print(" ^ le due colonne di destra sono il test di RISOLUZIONE: se togliere 7") + print(" giorni su 74 sposta la correlazione di piu' di quanto la correlazione") + print(" valga, il campione non sta misurando, sta oscillando.") + TRIALS.append((f"Q1 {a}: 3 orizzonti giornalieri x 3 sottocampioni", 9)) return res @@ -600,6 +613,7 @@ def sezione4(curve: pd.DataFrame) -> None: sub("(b) ridondanza, per quel che il campione consente") for a in ASSETS: sl = slope_hourly(curve, a).resample("1D").last().dropna() + sl.index = pd.DatetimeIndex(sl.index).as_unit("ns") j = pd.concat({"slope": sl, "tp": pos[a]}, axis=1, join="inner").dropna() for qq in (0.10, 0.25, 0.40): th = j.slope.quantile(qq) @@ -704,6 +718,7 @@ def sezione5(curve: pd.DataFrame) -> None: sub("(a) quanto e' buono il proxy (misurato sulla finestra vera, non assunto)") for a in ASSETS: sl = slope_hourly(curve, a).resample("1D").last().dropna() + sl.index = pd.DatetimeIndex(sl.index).as_unit("ns") df = A.get(a, "1d") p = pd.Series(_proxy_slope(df, a), index=pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))).dropna() diff --git a/scripts/research/r0822_vol_size.py b/scripts/research/r0822_vol_size.py index d00ae9e..357274b 100644 --- a/scripts/research/r0822_vol_size.py +++ b/scripts/research/r0822_vol_size.py @@ -282,6 +282,17 @@ def leverage_series(base: pd.Series, tv: float | None, w: int, cap: float, return L.fillna(1.0) +def activity_L(base: pd.Series, w: int, cap: float) -> pd.Series: + """L_t da sola ATTIVITA': quante barre ATTIVE (trade chiusi) negli ultimi w giorni, riferita + alla mediana espandente causale. Serve a separare i DUE canali dentro `rv` calcolata su tutte + le barre di uno sleeve all'88% di zeri: la MAGNITUDINE degli esiti e la FREQUENZA dei trade. + Se il guadagno vive qui, la variante non e' 'vol-target' ma un filtro di frequenza.""" + act = (base != 0.0).astype(float) + cnt = act.rolling(w, min_periods=max(5, w // 3)).sum().shift(1).replace(0.0, np.nan) + ref = cnt.expanding(min_periods=VT_WARM).median() + return (ref / cnt).clip(lower=1.0 / cap, upper=cap).fillna(1.0) + + def sizes_from_L(ex: dict, L: pd.Series) -> np.ndarray: """La size di ogni trade = L al giorno d'INGRESSO. E' la versione CAUSALE del vol-target su uno sleeve a equity a gradino: L al giorno di CHIUSURA non era nota all'ingresso.""" @@ -389,11 +400,12 @@ def main() -> None: for w in (30, 90, 180): for cp in (2.0, 3.0): BVT[f"BOOKVT w{w} cap{cp:.0f}"] = dict(tv=0.10, w=w, cap=cp) + ACT = {f"ACT w{w}": dict(w=w, cap=3.0) for w in (90, 180)} NAIVE = {f"NAIVE-VT w{w}": dict(tv=0.20, w=w, cap=3.0, active_only=False) for w in (30, 90)} - ALL = list(SZ) + list(VTL) + list(BVT) + list(NAIVE) + ALL = list(SZ) + list(VTL) + list(ACT) + list(BVT) + list(NAIVE) print(f"\n GRIGLIA DICHIARATA: {len(SZ)} size per-trade + {len(VTL)} vol-target di gamba + " - f"{len(BVT)} vol-target di libro + {len(NAIVE)} controlli non-causali = {len(ALL)} celle " - f"(+ baseline).") + f"{len(ACT)} filtro-attivita' + {len(BVT)} vol-target di libro + {len(NAIVE)} controlli " + f"non-causali = {len(ALL)} celle (+ baseline).") # ------------------------------------------------------------------ serie TP = A.tp01_baseline_daily() @@ -420,6 +432,16 @@ def main() -> None: MTP[nm][o] = 1.0 if o == offs[0]: SIZEMED[nm] = float(np.median(np.concatenate(list(sz.values())))) + for nm, sp in ACT.items(): + SLE[nm], BK[nm], MTP[nm] = {}, {}, {} + for o in offs: + sz = {a: sizes_from_L(EX[o][a], activity_L(EX[o][a]["base_daily"], **sp)) + for a in ASSETS} + SLE[nm][o] = leg_daily(EX[o], sz) + BK[nm][o] = book(TP, SLE[nm][o]) + MTP[nm][o] = 1.0 + if o == offs[0]: + SIZEMED[nm] = float(np.median(np.concatenate(list(sz.values())))) for nm, sp in BVT.items(): SLE[nm], BK[nm], MTP[nm] = {}, {}, {} for o in offs: @@ -467,7 +489,7 @@ def main() -> None: f"{maxdd(s)*100:>8.1f}%{maxdd(iv)*100:>9.1f}%{cagr(iv)*100:>8.1f}%{m}") prow("BASELINE (size fissa)", base_s, 1.00) - for nm in list(SZ) + list(VTL) + list(NAIVE): + for nm in list(SZ) + list(VTL) + list(ACT) + list(NAIVE): prow(nm, SLE[nm][can], SIZEMED.get(nm)) # ------------------------------------------------------------------ §3 libro, banda appaiata @@ -592,7 +614,7 @@ def main() -> None: print("\n" + "-" * 112) print(" 7. GATE — marginal_vs_tp01 / implausible_sharpe / weights_tilt_null") print("-" * 112) - best_sleeve = max([n for n in list(SZ) + list(VTL)], key=lambda x: is_med[x]) + best_sleeve = max([n for n in list(SZ) + list(VTL) + list(ACT)], key=lambda x: is_med[x]) for nm in ("BASE", best_sleeve): m = A.marginal_vs_tp01(SLE[nm][can]) b25 = m.get("blends", {}).get("w25", {}) @@ -606,7 +628,7 @@ def main() -> None: f"attive {imp.get('active_frac', 0)*100:.1f}% perdite/attive " f"{imp.get('loss_frac', 0)*100:.1f}% Calmar {imp.get('calmar', 0):.1f}") cols = {"TP01": TP, "SKH01": SLE["BASE"][can]} - for nm in [best, best_sleeve] + list(BVT)[:2]: + for nm in dict.fromkeys([best, best_sleeve] + list(BVT)[:2]): m_s = vol(SLE[nm][can]) / vol(SLE["BASE"][can]) wp = {"TP01": W_TP * MTP[nm][can], "SKH01": W_SKH * m_s} g = weights_tilt_null(cols, {"TP01": W_TP, "SKH01": W_SKH}, wp, @@ -630,7 +652,7 @@ def main() -> None: no = ns = 0 for a in ASSETS: df1h = TO.get1h(a) - tgt = netted_target(a, EX[can][a], nm, SZ, VTL, BVT, SLE, TP, can, df1h) + tgt = netted_target(a, EX[can][a], nm, SZ, VTL, ACT, BVT, SLE, TP, can, df1h) ev = A.eval_weights_smallcap(df1h, tgt, capital=CAPITAL, min_order=MIN_ORDER) tm += 0.5 * ev["modeled"]["sharpe"] tr += 0.5 * ev["realistic"]["sharpe"] @@ -647,8 +669,8 @@ def main() -> None: print(" un contributo positivo si scompone per ANNO prima di crederci) + deriva della size") print("-" * 112) yrs = sorted({int(y) for y in BK["BASE"][can].index.year}) - top = [best, best_iso] + [max(BVT, key=lambda x: RES[x]["iF"])] - top = list(dict.fromkeys(top)) + top = list(dict.fromkeys([best, best_iso, max(ACT, key=lambda x: RES[x]["iF"]), + max(BVT, key=lambda x: RES[x]["iF"])])) print(f" {'variante':<28}" + "".join(f"{y:>8}" for y in yrs)) for nm in top: cells = [] @@ -668,11 +690,12 @@ def main() -> None: if nm in SZ: szs = np.concatenate([SZ[nm](EX[can][a]) for a in ASSETS]) dys = pd.DatetimeIndex(np.concatenate([EX[can][a]["day_ent"].values for a in ASSETS])) - elif nm in VTL: + elif nm in VTL or nm in ACT: + mk = ((lambda b: leverage_series(b, **VTL[nm])) if nm in VTL + else (lambda b: activity_L(b, **ACT[nm]))) szs, dys = [], [] for a in ASSETS: - szs.append(sizes_from_L(EX[can][a], - leverage_series(EX[can][a]["base_daily"], **VTL[nm]))) + szs.append(sizes_from_L(EX[can][a], mk(EX[can][a]["base_daily"]))) dys.append(EX[can][a]["day_ent"].values) szs = np.concatenate(szs); dys = pd.DatetimeIndex(np.concatenate(dys)) else: @@ -697,7 +720,7 @@ def main() -> None: print("=" * 112) -def netted_target(asset: str, ex: dict, nm, SZ, VTL, BVT, SLE, TP, can, df1h) -> np.ndarray: +def netted_target(asset: str, ex: dict, nm, SZ, VTL, ACT, BVT, SLE, TP, can, df1h) -> np.ndarray: """Peso NETTO per asset sul grid 1h: 0.75*target TP01 + 0.25*posizione SKH (dir*size), poi l'eventuale leva di libro sulla gamba TP01. E' cio' che il cron manda a Deribit come UNA posizione netta sullo stesso strumento.""" @@ -711,6 +734,8 @@ def netted_target(asset: str, ex: dict, nm, SZ, VTL, BVT, SLE, TP, can, df1h) -> sizes = SZ[nm](ex) elif nm in VTL: sizes = sizes_from_L(ex, leverage_series(ex["base_daily"], **VTL[nm])) + elif nm in ACT: + sizes = sizes_from_L(ex, activity_L(ex["base_daily"], **ACT[nm])) else: # BOOKVT L = leverage_series(book(TP, SLE["BASE"][can]), BVT[nm]["tv"], BVT[nm]["w"], BVT[nm]["cap"]) sizes = sizes_from_L(ex, L) diff --git a/scripts/research/r0822_vrp_real_quotes.py b/scripts/research/r0822_vrp_real_quotes.py index 684fb04..f00bba1 100644 --- a/scripts/research/r0822_vrp_real_quotes.py +++ b/scripts/research/r0822_vrp_real_quotes.py @@ -326,11 +326,11 @@ def main() -> None: V = CB.dvol_series(a) win = V[(V.index >= puts["ts"].min()) & (V.index <= puts["ts"].max())] hist = V[V.index < win.index[0]].to_numpy(float) - print(f" {a}: DVOL nel campione min {win.min().1f} / mediana {win.median().1f} / " - f"max {win.max().1f}") - print(f" vs storia 2021+ ({len(hist)} giorni) la MEDIANA del campione sta al " - f"{float((hist < win.median()).mean()).0%} pctl, il suo MASSIMO al " - f"{float((hist < win.max()).mean()).0%} pctl") + print(f" {a}: DVOL nel campione min {win.min():.1f} / mediana {win.median():.1f} / " + f"max {win.max():.1f}") + print(f" vs storia 2021+ ({len(hist)} giorni): la MEDIANA del campione sta al " + f"{float((hist < win.median()).mean()):.0%} pctl, il suo MASSIMO al " + f"{float((hist < win.max()).mean()):.0%} pctl") print("\n \u26a0\ufe0f Due statistiche diverse, da tenere distinte: CLAUDE.md cita «DVOL max al 24\u00b0/28\u00b0\n" " pctl» dal campione del 30/07. Un MASSIMO cresce con la finestra — e' una statistica\n" " di ESTREMO, non di livello. Per dire in che regime siamo la lente e' la MEDIANA, ed\n" @@ -368,6 +368,29 @@ def main() -> None: f"max {tr['ivrank'].max():.3f})") print(f" TOTALE: {passed}/{tot}. Il sleeve sarebbe stato FLAT per l'intero campione.") + print("\n QUANDO TORNERA' MISURABILE — il gate non e' chiuso per sempre, e la soglia e' un numero.") + print(" Regola del 19/06: «rivalutare quando la catena cattura un crash». Un criterio senza\n" + " numero non e' un criterio, quindi eccolo (IV-rank espandente causale, come nel sleeve):") + for a in ASSETS: + V = CB.dvol_series(a) + vals = V.to_numpy(float) + ivr = np.full(len(vals), np.nan) + for i in range(1, len(vals)): + ivr[i] = (vals[:i] < vals[i]).mean() + ser = pd.Series(ivr, index=V.index).dropna() + per_year = ser.groupby(ser.index.year).apply(lambda x: float((x > 0.30).mean())) + thr = float(np.quantile(vals[:-1], 0.30)) + last_open = ser[ser > 0.30] + since = (V.index[-1] - last_open.index[-1]).days if len(last_open) else None + print(f" {a}: quota di giorni col gate APERTO per anno — " + + " ".join(f"{y}:{v:.0%}" for y, v in per_year.items())) + print(f" DVOL oggi {vals[-1]:.1f} · serve DVOL > {thr:.1f} " + f"(+{thr / vals[-1] - 1:.0%}) per riaprire il gate · ultimo giorno aperto " + f"{last_open.index[-1]:%Y-%m-%d} ({since} giorni fa)") + print(" → il campione non e' 'sfortunato': cade dentro un regime di vol bassa lungo e\n" + " persistente. La misura giusta si potra' rifare quando quel numero sara' superato,\n" + " e da allora servira' comunque un anno di settimane per avere un campione.") + ret = {a: (canon[a]["ST"].iloc[-1] / canon[a]["spot"].iloc[0] - 1.0) for a in ASSETS} print(f"\n E il sottostante nel campione: BTC {ret['BTC']:+.1%}, ETH {ret['ETH']:+.1%} — " f"vol implicita bassa E rialzo forte\n = il regime IDEALE per vendere put. " @@ -582,6 +605,13 @@ def main() -> None: if not W.empty: print(f"\n Peggior mark infra-settimana: mediana {W['frac'].median():.1%} del rischio, " f"minimo {W['frac'].min():.1%}") + wmin = W.loc[W['frac'].idxmin()] + print(f" Denominatore, per non fabbricare una discrepanza: qui il rischio e' " + f"`width - credito`.\n" + f" CLAUDE.md cita «-81,7% del capitale» per la STESSA settimana " + f"({wmin['asset']} {wmin['exp']:%Y-%m-%d}):\n" + f" e' lo stesso mark (${wmin['worst']:.2f}) diviso per la width nuda. Due " + f"denominatori, un solo fatto.") wl = W[W["pnl"] > 0] print(f" Fra le sole settimane chiuse in UTILE ({len(wl)}/{len(W)}): mediana " f"{wl['frac'].median():.1%}, minimo {wl['frac'].min():.1%}") @@ -694,7 +724,7 @@ def main() -> None: 1. IL CAMPIONE NON CONTIENE LA STRATEGIA. 0 settimane su 19 passano il gate IV-rank>0.30 che E' l'alpha di VRP01. Il campione e' vol implicita al 24-28 percentile E un rialzo - del +25%/+57%: il regime migliore possibile per vendere put, non uno neutro. + forte del sottostante: il regime migliore possibile per vendere put, non uno neutro. Le 10 settimane per asset (non 16: prima del 2026-06-09 la finestra 4-10 DTE non esiste) danno 10/10 vincenti su BTC. La lente giusta non e' lo Sharpe, e' la regola del tre: 0 perdite su 10 lascia un tasso vero fino al 30%, su un payoff che perde diff --git a/scripts/research/r0822_xs_lite.py b/scripts/research/r0822_xs_lite.py index 107bbf0..dc3bb8a 100644 --- a/scripts/research/r0822_xs_lite.py +++ b/scripts/research/r0822_xs_lite.py @@ -375,11 +375,9 @@ def main() -> None: print(" su GTAA/ADJUSTED_LAST). NON l'ho inseguito: tutte le mie conclusioni sono confronti") print(" RELATIVI dentro UNA lente. Ma tocca un numero pubblicato e il gate del 23/10.") print(f" XSR01 dal 25/07 a oggi (forward, {len(xsr_fwd)} barre, stessa lente): " - f"ritorno {(np.prod(1+xsr_fwd.values)-1)*100:+.2f}% " - f"Sharpe {sh(xsr_fwd) if len(xsr_fwd) > 30 else float('nan'):.2f} " - f"{'(campione troppo corto per uno Sharpe: si legge il ritorno)' if len(xsr_fwd) <= 30 else ''}") - print(" ^ la differenza fra 1.82 e il numero FULL di oggi NON e' una lente diversa (le repliche") - print(" sono bit-exact): e' un mese in piu' di dati. Il gate pre-registrato resta il 23/10.") + f"ritorno {(np.prod(1+xsr_fwd.values)-1)*100:+.2f}%" + + (" (campione troppo corto per uno Sharpe: si legge il ritorno)" + if len(xsr_fwd) <= 30 else f" Sharpe {sh(xsr_fwd):.2f}")) print(f" finestra MAJ19: {C19.index[0].date()} -> {C19.index[-1].date()} ({len(C19)} barre); " f"in-sample = pre {HOLDOUT.date()} = {int((C19.index < HOLDOUT).sum())} barre") cov51 = np.isfinite(C51.values).sum(axis=1) @@ -535,24 +533,42 @@ def main() -> None: f"{dg['med_ticket']:>11.0f}{dg['orders_py']:>10.0f}" f"{trk:>12.2f}{' ⚠' if trk < 0.8 else ' '}") print() - print(f" {'cella':<13}{'|dw| mediano al ribil.':>24}{'capitale sleeve minimo':>25}{' = book al 15%':>17}") + print(f" {'cella':<13}{'|dw| RIBILANCIO':>18}{'|dw| deriva':>14}{'cap sleeve min':>17}" + f"{'= book al 15%':>16}") + capmin_k5 = None for nm in ("MAJ19 k=1", "MAJ19 k=2", "MAJ19 k=3", "MAJ19 k=5*"): cell = xs_cells[nm] - Weff = xs_effective_weights(cell["W"], cell["scale"]) - chg = np.abs(np.diff(cell["W"], axis=0)).sum(axis=1) > 1e-12 # giorni di ribilanciamento - rows = np.where(chg)[0] + 1 - d = np.abs(np.diff(Weff, axis=0))[rows - 1] - med = float(np.median(d[d > 1e-9])) if np.any(d > 1e-9) else float("nan") - capmin = MIN_ORDER_HL / med if med > 0 else float("nan") - print(f" {'XS ' + nm.split()[1]:<13}{med:>24.4f}{capmin:>24.0f}$" - f"{capmin/0.15:>16.0f}$") - print(" ^ ARITMETICA: il ticket di un ribilanciamento e' |dw|*capitale_sleeve. Il min-order") - print(" morde sotto il 'capitale sleeve minimo'. Per il CANONICO k=5 quel numero e' ~150$") - print(" di sleeve = ~1.000$ di book al 15%, non 20.000$. La soglia dichiarata dal progetto") - print(" veniva da una diagnostica di TURNOVER (quota di turnover sopra il min-order), che a") - print(" capitale piccolo e' bassa perche' salta la deriva GIORNALIERA del vol-target — la") - print(" quale non porta segnale. Stessa lezione gia' imparata su TP01 ('a $600 il min-order") - print(" $5 e' GIA' la banda ottimale'), non applicata a XS01.") + W = cell["W"] + Weff = xs_effective_weights(W, cell["scale"]) + dW = np.abs(np.diff(W, axis=0)) # W cambia alla riga j+1 + dE = np.abs(np.diff(Weff, axis=0)) # Weff[j+1]-Weff[j], Weff[i]=W[i-1]*scale[i] + # il cambio di W alla riga j+1 si vede su Weff alla riga j+2 -> confine da rispettare + rb, dr = [], [] + for j in range(len(dW)): + if j + 1 >= len(dE): + break + m = dW[j] > 1e-12 + if m.any(): + rb.extend(dE[j + 1][m].tolist()) # gambe DAVVERO ribilanciate + dr.extend(dE[j + 1][~m][dE[j + 1][~m] > 1e-12].tolist()) + else: + dr.extend(dE[j + 1][dE[j + 1] > 1e-12].tolist()) + med_rb = float(np.median(rb)) if rb else float("nan") + med_dr = float(np.median(dr)) if dr else float("nan") + capmin = MIN_ORDER_HL / med_rb if med_rb > 0 else float("nan") + if nm.startswith("MAJ19 k=5"): + capmin_k5 = capmin + print(f" {'XS ' + nm.split()[1]:<13}{med_rb:>18.4f}{med_dr:>14.4f}{capmin:>16.0f}$" + f"{capmin/0.15:>15.0f}$") + print(" ^ ARITMETICA: il ticket vale |dw|*capitale_sleeve, e le due colonne |dw| sono ordini") + print(" di grandezza diversi: il RIBILANCIO (ogni 10 giorni, porta il segnale) e la DERIVA") + print(" giornaliera del vol-target (non porta segnale). Il min-order taglia la seconda molto") + print(" prima della prima, ed e' per questo che la quota di turnover eseguito puo' essere del") + print(f" 4% con haircut ~0. Per il CANONICO k=5 il ribilancio smette di passare sotto " + f"~{capmin_k5:.0f}$") + print(f" di sleeve = ~{capmin_k5/0.15:.0f}$ di book al 15%. La soglia '$20k' del progetto viene da") + print(" una diagnostica di TURNOVER, non di Sharpe — stessa lezione gia' imparata su TP01") + print(" ('a $600 il min-order $5 e' GIA' la banda ottimale'), mai applicata a XS01.") print() print(" 'gambe eseg' = quota delle VARIAZIONI di posizione sopra il min-order (la diagnostica") print(" su cui si basa la soglia '$20k'). 'haircut' = quanto ne costa in Sharpe. Sono cose") From bd5b634209bffdec0dcd652e84747cd11967ff80 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:12:15 +0000 Subject: [PATCH 005/114] research(wave-0822): ORTHO-SCREEN 7/7 scartato (e uno screen largo non puo' passare il proprio DSR); XS-LITE falsifica il muro dei 20k di XS01 --- docs/research/RESULTS-0822.md | 49 +++++++++ scripts/research/r0822_flow_squeeze.py | 2 +- scripts/research/r0822_oi_pin.py | 11 +- scripts/research/r0822_term_structure.py | 62 ++++++++--- scripts/research/r0822_vol_size.py | 88 ++++++++++------ scripts/research/r0822_vrp_real_quotes.py | 123 +++++++++++++++++----- 6 files changed, 256 insertions(+), 79 deletions(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 450eb89..36bd8aa 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -8,6 +8,8 @@ null de-levering superato + eseguibilita' al capitale dichiarato. |---|---|---|---| | 9 | ADAPTIVE-HORIZON | **SCARTATO** | il "vincitore adattivo" ha lookback incollato al bordo il 100% del tempo = TSMOM costante a 20g (corr 1.000, dSh 0.00 in 8/8 anni e 0/12 ancore); isolando le celle davvero adattive -> NEUTRAL, corr→TP01 0.90 = TP01 travestito || 4 | DEALER-GAMMA | **SCARTATO** | la gamba tradeable e' INCOERENTE (BTC non separa, ETH separa al ROVESCIO); cio' che resta e' quasi tutto DVOL (corr 0.66), gia' refutato il 26/06. DSR 0.440 e la cella migliore sta SOTTO il massimo atteso dal rumore | | 2 | GROWTH-POLICY | **LEAD** (condizione, non data) | il libro gira al **7% di Kelly** e raccoglie il **15%** della crescita massima in log; il gradino eseguibile 1.00x->1.25-1.50x vale 14.7a -> 12.9-11.6a al muro. Ma un solo giorno -10% all'anno porta k* da ~10x a **2x**: non si decide su un conto vero con la lente close-only | +| 12 | ORTHO-SCREEN | **SCARTATO 7/7** | nessuna famiglia arriva a ADDS+DSR; e il motivo e' **aritmetico**: su 168 trial il massimo atteso dal rumore e' Sharpe **1.572**, SOPRA il soffitto direzionale misurato (~1.3) -> uno screen largo su BTC/ETH direzionale **non puo'** passare un DSR, per costruzione | +| 3 | XS-LITE | **SCARTATO** (come sleeve) + **1 soglia pubblicata falsificata** | concentrare XS01 non crea uno sleeve nuovo (corr 0.81-0.96 col canonico, DSR FAIL, de-levering non superato) — ma il muro *"XS01 serve ~$20k"* e' misurato su una diagnostica di TURNOVER: il ribilancio vero smette di passare sotto **~$109 di sleeve (~$730 di book), 27x piu' in basso** | ## Note che sopravvivono ai singoli filoni @@ -69,3 +71,50 @@ vedono la variabile**. E' per questo che la leva e' l'unica leva del libro mai e - 📌 **Sottoprodotto da verificare (tocca un'etichetta del libro live):** a `target_vol=20%` la vol **realizzata** di TP01 e' **12,14%** — il target vale sulla posizione quando c'e', e TP01 e' long-flat. **L'etichetta sovrastima il rischio preso di ~40%.** + +### 12 — ORTHO-SCREEN (r0822_ortho_screen.py, 174 trial: 7 famiglie x 24 + 6 controlli) +📌 **Il risultato piu' riusabile dell'ondata, ed e' metodologico:** il deflated-Sharpe va calcolato +**di screen** (168 trial) e non solo di famiglia (24). Su quel pool il massimo atteso dal puro rumore +e' **Sharpe 1.572**, cioe' **sopra il soffitto direzionale misurato del progetto (~1.3)**. +**Conseguenza: una ricerca a rete larga su un singolo stream direzionale BTC/ETH non puo' passare il +proprio gate — non e' sfortuna, e' aritmetica.** Le ondate future o dichiarano famiglie molto piu' +piccole in anticipo, o cambiano meccanismo (non-direzionale / cross-sectional). +📌 **Per la prima volta nel progetto l'eseguibilita' a $600 non e' il vincolo binding di niente** +(haircut 0.00-0.01 su 7/7): muore tutto molto prima, sull'edge. +- Famiglie: RSKEW (skew realizzata), TACC (accelerazione del trend), VPX (volume-prezzo), XDISP + (dispersione dei 51 alt come timer), XCORR (corr BTC-ETH), XTAIL (dopo shock 3σ), VRAT + (variance-ratio). Miglior DSR di famiglia 0.633, di screen **0.034**. `robust_oos` False 7/7. +- **TACC** e' l'unica ADDS (corr→TP01 0.09) ma il suo uplift hold-out **vive tutto nel 2026** (2025 + da solo −0.314) = finestra fortunata. E la domanda della famiglia ha risposta: **e' il LIVELLO, + non l'accelerazione** — la cella scelta si riduce al ritorno a 5 giorni, corr **+0.784** col + momentum di livello. +- ✅ **Tre falsificazioni che chiudono spazio:** (1) la mean-reversion **non torna in vita** sotto due + conditioner mai provati (volume, shock 3σ): la selezione in-sample sceglie il ramo di + CONTINUAZIONE in entrambi i casi; (2) la dispersione dei 51 alt come timer di mercato e' + **negativa** (−0.899 sulla sola finestra attiva, non e' un artefatto di calendario); (3) RSKEW e' + il ritratto del fitting — **miglior in-sample dello screen (1.281) e peggior hold-out (−0.494)**, + col segno OPPOSTO all'a-priori teorico dichiarato. +- ⚠️ **6ª occorrenza del null del de-levering** (VRAT, maxDD 4.4% -> `k*TP01` fa meglio). +- ⚠️ Due candidati hanno l'ancora canonica come **PEGGIORE** delle 8 (TACC, VRAT): la fortuna + d'ancora non ha un verso fisso, e guardare solo il canonico a volte **inventa un danno**. + +### 3 — XS-LITE (r0822_xs_lite.py, 26 celle + 120 valutazioni d'ancora) +✅ Repliche **bit-exact** prima di ogni delta: XS01 vs `sleeves._xsec_returns` e XSR01 vs +`basket_from_positions(demean=True)`, entrambe `max|diff| = 0.0`. +📌 **Falsificazione di un numero pubblicato:** il muro *"XS01 serve ~$20k"* non e' un fatto di +Sharpe ma di **turnover** — e a capitale piccolo il min-order salta la **deriva del vol-target** +(|Δw| giornaliero 0.001) che **non porta segnale**, non il ribilancio (|Δw| 0.091-0.157). Capitale +minimo perche' il ribilancio passi: **$109 (k=5) / $86 (k=3) / $64 (k=2)** di sleeve. Stessa lezione +gia' imparata su TP01 nel 2026 ("a $600 il min-order e' gia' la banda ottimale") e **mai applicata a +XS01**. ⚠️ L'agente ha usato **min-order $10 (Hyperliquid)**, non $5 (Deribit) — un audit dedicato +sta verificando i minimi veri del venue. +- Concentrare **non** ripara il rischio #1 di XSR01: XS-LITE regge oltre **50 bps/gamba**, XSR-LITE + muore a **~28 bps a ogni k, pieno incluso**. +- La radice dell'ampiezza **non descrive** XS01: da 10 a 4 gambe si perde il **2%** di Sharpe + mediano-di-fase mentre N_hhi passa 6.9 -> 3.0 (il segnale sta negli **estremi** del ranking; le + gambe 3-5 per lato aggiungono rischio quanto segnale). Il collasso arriva solo a k=1 (41%). +- ⚠️ **Da inseguire — tocca un gate pre-registrato: lo Sharpe 1.82 di XSR01 NON si riproduce oggi.** + Sulla finestra identica a quella di scoperta la lente "paniere" da' **1.75** e la lente "libro" di + `paper_xsr` da' **2.23**; 1.82 non e' nessuna delle due. Spiegazione piu' probabile: `data/raw` e' + gitignored e il cron riscrive i parquet HL ogni notte -> **stesso codice, dati diversi** (identico + allo scoperto del 07/08 su GTAA/TLT). **La decisione del 23/10 poggia su quel numero.** diff --git a/scripts/research/r0822_flow_squeeze.py b/scripts/research/r0822_flow_squeeze.py index 89e092f..f587a6e 100644 --- a/scripts/research/r0822_flow_squeeze.py +++ b/scripts/research/r0822_flow_squeeze.py @@ -254,7 +254,7 @@ def sezione2(snap: pd.DataFrame) -> pd.DataFrame: g["p_x24"] = [bboot_p(d[d.odec == k].x24.values, block=48) for k in g.index] print(g.round(3).to_string()) print(" av4/av24 = |ret| futuro medio: e' la gamba 'squeeze/cascata' dell'ipotesi") - print(" (piu' vol dopo un estremo di OI). E' QUASI PIATTA (0.59-0.75%% a 4h, solo il decile 0") + print(" (piu' vol dopo un estremo di OI). E' QUASI PIATTA (0.59-0.75% a 4h, solo il decile 0") print(" leggermente sopra): la 'cascata' prevedibile non c'e'.") print("\n--- CONTROLLO: e' solo 'il prezzo si e' appena mosso'? ---") diff --git a/scripts/research/r0822_oi_pin.py b/scripts/research/r0822_oi_pin.py index 47d73e6..7b46f3c 100644 --- a/scripts/research/r0822_oi_pin.py +++ b/scripts/research/r0822_oi_pin.py @@ -453,9 +453,10 @@ def main() -> None: # ---------------------------------------------------------------- 4. NULL LOCATION-MATCHED P("\n[4] NULL LOCATION-MATCHED (regola 02/07: e' la POSIZIONE o e' l'OI?)") P(" Confronto APPAIATO per evento: tw(livello speciale) - tw(placebo alla stessa distanza).") - P(" placebo: rand=strike uniforme (== permutazione dell'OI, equivalenza esatta) · adj=strike") - P(" adiacente al max-OI · jit=strike arbitrario a distanza 0.5-1.5x (analogo Fib±jitter) ·") - P(" mid=centro della griglia quotata (zero info OI) · mr7=media 7g dello spot (mean-reversion).") + P(" placebo PER REGOLA: adj_r = strike adiacente a K_r · jit_r = strike arbitrario a") + P(" distanza 0.5-1.5x |K_r - S| (analogo del Fib±jitter del 02/07).") + P(" placebo GLOBALI: rand = strike uniforme (== permutazione dell'OI, equivalenza esatta) ·") + P(" mid = centro della griglia quotata (zero info OI) · mr7 = media 7g dello spot.") P(f" {'hte':>4} {'rule':>5} {'Δ vs adj_r':>12} {'Δ vs jit_r':>12}" f" {'Δ vs rand':>12} {'Δ vs mid':>12} {'Δ vs mr7':>12}") npos = ntot = 0 @@ -518,7 +519,7 @@ def main() -> None: P("\n [6-bis] LA STESSA MACCHINA SUI LIVELLI PLACEBO (stesse finestre, stessa fee, stesso") P(" sizing): se un placebo regge quanto il livello 'speciale', l'OI non aggiunge nulla.") nulls = [] - for rl in ("mp", "oi", "mid", "mr7", "rand", "jit", "maj", "short", "long"): + for rl in ("mp", "oi", "mid", "mr7", "rand", "jit_mp", "maj", "short", "long"): for hte in (3, 6, 12, 24): for bn, bd in bands.items(): d, _i = strat_daily(ev, rl, hte, bd, "sign", "all", FEE_SIDE_REAL) @@ -527,7 +528,7 @@ def main() -> None: cagr=mm["cagr"], dd=mm["dd"])) N = pd.DataFrame(nulls) piv = N.pivot_table(index=["hte", "band"], columns="rule", values="sharpe") - P(piv.reindex(columns=["mp", "oi", "mid", "mr7", "rand", "jit", "maj", "short", "long"]) + P(piv.reindex(columns=["mp", "oi", "mid", "mr7", "rand", "jit_mp", "maj", "short", "long"]) .to_string(float_format=lambda v: f"{v:6.2f}")) P(f" celle placebo valutate: {len(N)} (contano come trial: la griglia dichiarata sale)") diff --git a/scripts/research/r0822_term_structure.py b/scripts/research/r0822_term_structure.py index b85f3c3..d051aa9 100644 --- a/scripts/research/r0822_term_structure.py +++ b/scripts/research/r0822_term_structure.py @@ -360,7 +360,10 @@ def sezione2(curve: pd.DataFrame) -> dict: print("\n (c) IL CONFOUND DI QUESTO FILONE — la pendenza e' informazione NUOVA o RV riciclata?") rvp = (r1.rolling(24).std() * np.sqrt(24 * 365) * 100).reindex(idx) - rvf = (r1.shift(-24).rolling(24).std().shift(-23) * np.sqrt(24 * 365) * 100).reindex(idx) + # RV su (t, t+24h]: rolling(24) etichettata a t+24 e riportata indietro a t. (Una prima + # stesura aveva uno shift di troppo e misurava la finestra (+24h, +47h): corretto in + # sessione confrontando con una seconda implementazione a serie rovesciata.) + rvf = (r1.rolling(24).std().shift(-24) * np.sqrt(24 * 365) * 100).reindex(idx) cf = curve[curve.asset == a].copy() cf["hr"] = pd.to_datetime(cf["hr"], utc=True).dt.as_unit("ns") ivf = cf.set_index("hr")[f"iv_{FRONT}"].reindex(idx) @@ -563,9 +566,14 @@ def sezione3(chain: pd.DataFrame, atm_exp: pd.DataFrame, curve: pd.DataFrame) -> f"({prem/CAPITALE:.0%} del conto), vega ${vega:.2f}/pt-vol, fee ${fee:.2f}/gamba") print(f" carry lordo ${carry_g:+.3f}/giorno per struttura; attrito per giro " f"${friz:.2f} => {friz/carry_g:.0f} giorni per andare in pari") - print(f"\n Un calendar spread e' DUE gambe: a $635 una sola struttura ETH impegna il") - print(f" ~15-30% del conto e rende centesimi al giorno. BTC e' fuori dal lotto minimo.") - print(f" Il bersaglio dichiarato e' 50 EUR/giorno: qui si parla di due ordini di grandezza sotto.") + print("\n Un calendar spread e' DUE gambe. Cio' che la tabella mostra e' il PREMIO di UNA") + print(" gamba al lotto minimo: BTC 26% del conto, ETH 10%. Il premio non e' tutto il") + print(" fabbisogno — la gamba CORTA vuole margine, e la misura del 2026-07-30 su una") + print(" struttura BTC a rischio definito dava $6.210 di collaterale per lotto. Quindi:") + print(" BTC resta fuori per il margine, ETH entra ma una sola struttura impegna il") + print(" 10-25% del conto per un carry LORDO di centesimi al giorno.") + print(" Il bersaglio dichiarato e' 50 EUR/giorno: qui siamo 2-3 ordini di grandezza sotto,") + print(" e il conto non regge la SECONDA struttura che servirebbe per diversificare.") TRIALS.append(("Q2: 2 asset x eseguibilita' al lotto minimo", 2)) @@ -671,17 +679,31 @@ def sezione4(curve: pd.DataFrame) -> None: hold = tgt.shift(1).fillna(0.0) net = (hold * d["r"] - A.FEE_SIDE * hold.diff().abs().fillna(0.0)).dropna() dd_c = A._dd_ret(net) + # ⚠️ il k giusto e' il PIU' GRANDE che sta sotto il DD del candidato, non il primo che + # trovi salendo da 0.05: partendo dal basso si confronta il candidato con un baseline + # quasi spento, che ha un DD ridicolo e uno Sharpe gonfiato dal rapporto. (Errore + # commesso e corretto in sessione: dava "candidato sopra il null" con k=0.05.) + ks = np.arange(0.05, 3.01, 0.05) best = None - for k in np.arange(0.05, 1.01, 0.05): + for k in ks[::-1]: b = k * d["r"].reindex(net.index) if A._dd_ret(b) <= dd_c: - best = (k, A._sh(b), A._dd_ret(b)) + best = (float(k), A._sh(b), A._dd_ret(b)) break - print(f" {a}: candidato Sharpe {A._sh(net):+.2f} maxDD {dd_c:.2%} | " - + (f"buy&hold de-leverato k={best[0]:.2f} -> stesso DD ({best[2]:.2%}) con Sharpe " - f"{best[1]:+.2f} => {'IL CANDIDATO NON SOPRAVVIVE' if best[1] >= A._sh(net) else 'candidato sopra il null'}" - if best else "nessun k<=1 raggiunge quel DD")) - TRIALS.append((f"Q3 {a}: 20 valori di k nel null de-levering", 20)) + sh_c = A._sh(net) + if best is None: + print(f" {a}: candidato Sharpe {sh_c:+.2f} maxDD {dd_c:.2%} | nessun k in " + f"[0.05,3.00] raggiunge quel DD") + else: + verdict = ("IL CANDIDATO NON SOPRAVVIVE (il de-levering fa meglio)" + if best[1] >= sh_c else "candidato sopra il null di de-levering") + print(f" {a}: candidato Sharpe {sh_c:+.2f} maxDD {dd_c:.2%} | buy&hold a leva " + f"k={best[0]:.2f} -> stesso o minor DD ({best[2]:.2%}) con Sharpe {best[1]:+.2f}" + f" => {verdict}") + imp = A.implausible_sharpe(net) + print(f" implausible_sharpe sul candidato di finestra: implausible=" + f"{imp['implausible']} {imp['reasons']}") + TRIALS.append((f"Q3 {a}: 60 valori di k nel null de-levering", 60)) # =========================================================================================== @@ -745,15 +767,24 @@ def sezione5(curve: pd.DataFrame) -> None: if k in rep: print(f" {k:20s} = {rep[k]}") imp = A.implausible_sharpe(era) - print(f" implausible_sharpe: flagged={imp.get('flagged')} ({imp.get('reasons')})") + print(f" implausible_sharpe (proxy, era DVOL): implausible={imp['implausible']} " + f"{imp['reasons']}") + print(f" earns_slot = (assoluto non-FAIL) AND (marginale ADDS): qui il marginale e' " + f"{rep.get('marginal_verdict')} -> **earns_slot = False**.") TRIALS.append(("Sez.5: 2 varianti proxy (long-only, L/S) x 1 soglia", 2)) sub("(c) il precedente che questo replica") print(" `dvol_directional.py` (2026-06-29) testo' VRP-Z = z-score causale di (IV-RV) come") print(" segnale DIREZIONALE su 5 anni, con la griglia completa: verdetto **HEDGE,") - print(" earns_slot=False** — paga solo quando TP01 e' debole, quindi non e' alpha.") - print(" La pendenza della term structure e', su questa lente, la stessa variabile con la") - print(" gamba realizzata sostituita da una implicita. Il filone non e' nuovo: e' quello.") + print(" earns_slot=False**. Qui, con la variante a SOGLIA BINARIA invece che a z-score,") + print(" il verdetto misurato e' NEUTRAL con uplift NEGATIVO — un verdetto diverso, la") + print(" stessa conclusione (earns_slot=False in entrambi). La differenza sta nella") + print(" variante, non nell'esito, e va riportata cosi': non sto dichiarando una replica") + print(" che non ho fatto.") + print(" Il punto strutturale resta: la pendenza e' il premio di vol con la gamba") + print(" REALIZZATA sostituita da una IMPLICITA (SEZIONE 2c: corr(pendenza, RV passata)") + print(" = -0.50/-0.57). Non e' una variabile nuova per questo progetto: e' quella, con") + print(" un rumore di misura piu' basso e 5 anni di storia in meno.") # =========================================================================================== @@ -778,7 +809,6 @@ def sezione6() -> None: def main() -> None: hr("r0822_term_structure — TERM STRUCTURE DELLA VOL IMPLICITA BTC/ETH") - print(__doc__.split("METODO.")[0].strip()[:0] or "", end="") chain = load_quoted_chain() sezione0(chain) atm_exp = build_atm_by_expiry(chain) diff --git a/scripts/research/r0822_vol_size.py b/scripts/research/r0822_vol_size.py index 357274b..add3258 100644 --- a/scripts/research/r0822_vol_size.py +++ b/scripts/research/r0822_vol_size.py @@ -303,6 +303,20 @@ def sizes_from_L(ex: dict, L: pd.Series) -> np.ndarray: # --------------------------------------------------------------------------- libro +def sizes_of(nm, ex: dict, SZ: dict, VTL: dict, ACT: dict, Lbook: pd.Series | None = None): + """Vettore di size per-trade di una cella qualsiasi. Un solo posto in cui e' definita: + §8 (eseguibilita'), §9 (deriva) e §10 (null di permutazione) devono usare LA STESSA.""" + if nm is None or nm == "BASE": + return size_flat(ex) + if nm in SZ: + return SZ[nm](ex) + if nm in VTL: + return sizes_from_L(ex, leverage_series(ex["base_daily"], **VTL[nm])) + if nm in ACT: + return sizes_from_L(ex, activity_L(ex["base_daily"], **ACT[nm])) + return sizes_from_L(ex, Lbook) + + def book(tp: pd.Series, skh: pd.Series) -> pd.Series: return combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": W_TP, "SKH01": W_SKH}) @@ -498,7 +512,8 @@ def main() -> None: print(" de-levering (k<1 sul BASELINE che pareggia il maxDD della variante)") print("-" * 112) print(f" {'variante':<28}{'wSKH eff':>9}{'dShFULL':>9}{'pos/n':>8}{'dShHOLD':>9}{'pos/n':>8}" - f"{'| ISO dF':>10}{'pos/n':>8}{'ISO dH':>9}{'pos/n':>8}{'dDDpp':>8}{'k_iso':>8}") + f"{'| ISO dF':>10}{'pos/n':>8}{'ISO dH':>9}{'pos/n':>8}{'dDDpp':>8}{'k_iso':>8}" + f"{'null de-lev':>14}") base_b = BK["BASE"] RES = {} for nm in ALL: @@ -525,7 +540,13 @@ def main() -> None: print(f" {nm:<28}{we:>9.3f}{dF['median_paired']:>+9.3f}{dF['n_positive']:>5}/{len(offs):<3}" f"{dH['median_paired']:>+9.3f}{dH['n_positive']:>5}/{len(offs):<3}" f"{iF['median_paired']:>+10.3f}{iF['n_positive']:>5}/{len(offs):<3}" - f"{iH['median_paired']:>+9.3f}{iH['n_positive']:>5}/{len(offs):<3}{dd:>+8.2f}{ks:>8}") + f"{iH['median_paired']:>+9.3f}{iH['n_positive']:>5}/{len(offs):<3}{dd:>+8.2f}{ks:>8}" + f"{vd:>14}") + print(" ⚠️ NOTA sul null del de-levering: `sh(k*base)` e' IDENTICO a `sh(base)` perche' lo") + print(" Sharpe e' invariante a una costante -> in questa forma il null COINCIDE col confronto") + print(" degli Sharpe, ed e' per questo che qui lo Sharpe e' la metrica che decide e il DD no.") + print(" 'k_iso' resta come diagnostica: quanta leva dovrebbe cedere il baseline per pareggiare") + print(" il maxDD della variante. 'DE-LEVERING' = la variante NON batte quel baseline ridotto.") print(" wSKH eff = peso EFFETTIVO di SKH nel libro (i nominali restano 75/25: la size lo") print(" muove). Colonne 'ISO' = confronto vs il baseline RI-SCALATO a quello stesso peso:") print(" e' li' che si legge la FORMA. Le colonne non-ISO contengono anche il cambio di peso.") @@ -687,24 +708,10 @@ def main() -> None: f" anni positivi {npos}/{len(yrs)}") print() for nm in top: - if nm in SZ: - szs = np.concatenate([SZ[nm](EX[can][a]) for a in ASSETS]) - dys = pd.DatetimeIndex(np.concatenate([EX[can][a]["day_ent"].values for a in ASSETS])) - elif nm in VTL or nm in ACT: - mk = ((lambda b: leverage_series(b, **VTL[nm])) if nm in VTL - else (lambda b: activity_L(b, **ACT[nm]))) - szs, dys = [], [] - for a in ASSETS: - szs.append(sizes_from_L(EX[can][a], mk(EX[can][a]["base_daily"]))) - dys.append(EX[can][a]["day_ent"].values) - szs = np.concatenate(szs); dys = pd.DatetimeIndex(np.concatenate(dys)) - else: - L = leverage_series(BK["BASE"][can], BVT[nm]["tv"], BVT[nm]["w"], BVT[nm]["cap"]) - szs, dys = [], [] - for a in ASSETS: - szs.append(sizes_from_L(EX[can][a], L)) - dys.append(EX[can][a]["day_ent"].values) - szs = np.concatenate(szs); dys = pd.DatetimeIndex(np.concatenate(dys)) + Lb = (leverage_series(BK["BASE"][can], BVT[nm]["tv"], BVT[nm]["w"], BVT[nm]["cap"]) + if nm in BVT else None) + szs = np.concatenate([sizes_of(nm, EX[can][a], SZ, VTL, ACT, Lb) for a in ASSETS]) + dys = pd.DatetimeIndex(np.concatenate([EX[can][a]["day_ent"].values for a in ASSETS])) s = pd.Series(szs, index=dys) med = s.groupby(s.index.year).median() rho = float(np.corrcoef(np.arange(len(med)), med.values)[0, 1]) if len(med) > 2 else float("nan") @@ -715,6 +722,33 @@ def main() -> None: print(" (la vol crypto e' scesa lungo il campione -> qualunque 1/vol pesa di piu' il") print(" periodo recente, che e' anche l'hold-out). La riga 'corr col tempo' la misura.") + # noqa + print("\n" + "-" * 112) + print(" 10. NULL DI PERMUTAZIONE DELLA SIZE — la stessa MULTINSIEME di size, riassegnata a") + print(" caso ai trade. Se il vantaggio a iso-peso sopravvive al null, conta QUALE trade") + print(" riceve quale size (informazione); se no, e' solo la DISTRIBUZIONE delle size.") + print("-" * 112) + rng = np.random.default_rng(20260822) + a10 = list(offs[:: max(1, len(offs) // 3)])[:3] + NDRAW = 150 + for nm in dict.fromkeys([best_iso, best_sleeve]): + real = float(np.median([sh(BK[nm][o]) - sh(CTRL[nm][o]) for o in a10])) + nul = [] + for _ in range(NDRAW): + v = [] + for o in a10: + szp = {a: rng.permutation(sizes_of(nm, EX[o][a], SZ, VTL, ACT)) for a in ASSETS} + lg = leg_daily(EX[o], szp) + m_s = vol(lg) / vol(SLE["BASE"][o]) if vol(SLE["BASE"][o]) > 0 else 1.0 + v.append(sh(book(TP, lg)) - sh(book(TP, SLE["BASE"][o] * m_s))) + nul.append(float(np.median(v))) + nul = np.asarray(nul) + pc = float((nul < real).mean()) * 100 + print(f" {nm:<26} reale {real:+.3f} null mediana {np.median(nul):+.3f} " + f"p90 {np.percentile(nul, 90):+.3f} max {nul.max():+.3f} " + f"-> percentile {pc:.1f} (p ~ {(1 - pc / 100):.3f})") + print(f" {NDRAW} permutazioni x {len(a10)} ancore {a10}; seme fisso 20260822.") + print("\n" + "=" * 112) print(f" fatto in {time.time()-t0:.0f}s") print("=" * 112) @@ -728,19 +762,13 @@ def netted_target(asset: str, ex: dict, nm, SZ, VTL, ACT, BVT, SLE, TP, can, df1 ts1 = df1h["timestamp"].values.astype(np.int64) + 3_600_000 pos = np.zeros(len(ts1)) L1h = np.ones(len(ts1)) - if nm is None: - sizes = size_flat(ex) - elif nm in SZ: - sizes = SZ[nm](ex) - elif nm in VTL: - sizes = sizes_from_L(ex, leverage_series(ex["base_daily"], **VTL[nm])) - elif nm in ACT: - sizes = sizes_from_L(ex, activity_L(ex["base_daily"], **ACT[nm])) - else: # BOOKVT + if nm is not None and nm in BVT: # BOOKVT: leva anche sulla gamba TP01 L = leverage_series(book(TP, SLE["BASE"][can]), BVT[nm]["tv"], BVT[nm]["w"], BVT[nm]["cap"]) - sizes = sizes_from_L(ex, L) + sizes = sizes_of(nm, ex, SZ, VTL, ACT, L) idx1h = pd.DatetimeIndex(pd.to_datetime(df1h["timestamp"].values, unit="ms", utc=True)) L1h = L.reindex(idx1h.floor("D")).ffill().fillna(1.0).values + else: + sizes = sizes_of(nm, ex, SZ, VTL, ACT) t_in, t_out = ex["ts_close"][ex["i_ent"]], ex["ts_close"][ex["i_ex"]] for k in range(len(t_in)): j0 = int(np.searchsorted(ts1, t_in[k], side="left")) diff --git a/scripts/research/r0822_vrp_real_quotes.py b/scripts/research/r0822_vrp_real_quotes.py index f00bba1..dd18101 100644 --- a/scripts/research/r0822_vrp_real_quotes.py +++ b/scripts/research/r0822_vrp_real_quotes.py @@ -382,13 +382,19 @@ def main() -> None: thr = float(np.quantile(vals[:-1], 0.30)) last_open = ser[ser > 0.30] since = (V.index[-1] - last_open.index[-1]).days if len(last_open) else None + inwin = ser[(ser.index >= puts["ts"].min()) & (ser.index <= puts["ts"].max())] + near = inwin.max() if len(inwin) else float("nan") print(f" {a}: quota di giorni col gate APERTO per anno — " + " ".join(f"{y}:{v:.0%}" for y, v in per_year.items())) print(f" DVOL oggi {vals[-1]:.1f} · serve DVOL > {thr:.1f} " f"(+{thr / vals[-1] - 1:.0%}) per riaprire il gate · ultimo giorno aperto " f"{last_open.index[-1]:%Y-%m-%d} ({since} giorni fa)") + print(f" quanto vicino c'e' andato DENTRO la finestra: IV-rank max " + f"{near:.4f} il {inwin.idxmax():%Y-%m-%d} — " + f"{'mancato per ' + format(0.30 - near, '.4f') if near <= 0.30 else 'aperto'}") print(" → il campione non e' 'sfortunato': cade dentro un regime di vol bassa lungo e\n" - " persistente. La misura giusta si potra' rifare quando quel numero sara' superato,\n" + " persistente. Ma il margine NON e' comodo: il gate ha sfiorato l'apertura, e\n" + " bastano ~+10% di DVOL per riaprirlo. La misura giusta si potra' rifare quando quel numero sara' superato,\n" " e da allora servira' comunque un anno di settimane per avere un campione.") ret = {a: (canon[a]["ST"].iloc[-1] / canon[a]["spot"].iloc[0] - 1.0) for a in ASSETS} @@ -668,6 +674,23 @@ def main() -> None: f"MEDIANA ONESTA {band['median']:.2f} · banda [{band['lo']:.2f}, {band['hi']:.2f}] · " f"positiva in {band['frac_positive']:.0%} di {band['n_anchors']} ancore · " f"fortuna {band['luck']:+.2f}") + # DOVE vive l'instabilita': nel numeratore (il guadagno) o nel denominatore (la vol)? + det = [] + for o in offsets: + w = by_off(o) + if len(w) > 2 and w.std(ddof=1) > 0: + det.append((o, float(w.mean()), float(w.std(ddof=1)), len(w))) + if det: + mm = np.array([d[1] for d in det]) + ss = np.array([d[2] for d in det]) + print(f" media settimanale fra le ancore: [{mm.min():+.2%}, {mm.max():+.2%}] " + f"(rapporto {mm.max() / max(mm.min(), 1e-9):.1f}x) · " + f"deviazione standard: [{ss.min():.2%}, {ss.max():.2%}] " + f"(rapporto {ss.max() / max(ss.min(), 1e-9):.1f}x)") + print(f" → l'ancora NON cambia quanto si guadagna, cambia il DENOMINATORE: con\n" + f" {det[0][3]} settimane e nessuna perdita la vol campionaria e' quasi\n" + f" arbitraria, e lo Sharpe con lei. Non e' che le 08:00 siano un'ora\n" + f" magica — e' che questa statistica non porta informazione.") print("\n (d) null del de-levering / beta — 'e' reddito da volatilita' o crypto travestito?'") print(" Lo spread ha delta positivo (≈ +0.18 all'ingresso): in un campione che sale del") @@ -697,8 +720,15 @@ def main() -> None: dly = comb.resample("1D").sum().fillna(0.0) rep = A.marginal_vs_tp01(dly) act = int((dly != 0).sum()) - print(f" verdetto={rep.get('verdict')} corr={rep.get('corr'):.3f} " - f"(barre ATTIVE {act}/{len(dly)} = {act / len(dly):.0%})") + def _f(k): + v = rep.get(k) + return f"{v:.3f}" if isinstance(v, (int, float)) and v is not None else str(v) + print(f" verdetto={rep.get('verdict')} corr={_f('corr')} " + f"insample_edge={rep.get('has_insample_edge')} robust_oos={rep.get('robust_oos')} " + f"is_hedge={rep.get('is_hedge')}") + print(f" barre ATTIVE {act}/{len(dly)} = {act / len(dly):.0%} — un `None` qui non e'\n" + f" un bug: le sotto-misure che richiedono un hold-out non hanno dati per\n" + f" esistere, e la funzione lo dichiara invece di inventare un numero.") print(f" ⚠️ {len(comb)} settimane su un hold-out che non esiste: il verdetto e' " f"rumore, si riporta per non ometterlo.") except Exception as exc: # noqa: BLE001 @@ -719,36 +749,75 @@ def main() -> None: # ------------------------------------------------------------ §7 VERDETTO hr("§7 VERDETTO") - print(""" SCARTATO — su questi dati la domanda non e' decidibile, e i tre motivi sono - indipendenti l'uno dall'altro: + print(""" SCARTATO — su questi dati la domanda non e' decidibile. Quattro motivi, indipendenti + l'uno dall'altro: ciascuno da solo basterebbe. 1. IL CAMPIONE NON CONTIENE LA STRATEGIA. 0 settimane su 19 passano il gate IV-rank>0.30 - che E' l'alpha di VRP01. Il campione e' vol implicita al 24-28 percentile E un rialzo - forte del sottostante: il regime migliore possibile per vendere put, non uno neutro. - Le 10 settimane per asset (non 16: prima del 2026-06-09 la finestra 4-10 DTE non - esiste) danno 10/10 vincenti su BTC. La lente giusta non e' lo Sharpe, e' la regola - del tre: 0 perdite su 10 lascia un tasso vero fino al 30%, su un payoff che perde - ~7x il credito. + che E' l'alpha di VRP01 — il sleeve sarebbe stato FLAT per l'intero campione. La DVOL + mediana sta al 7 (BTC) / 11 (ETH) percentile della storia 2021+, e il sottostante e' + salito del +25% / +44%: vol implicita bassa E rialzo forte = il regime MIGLIORE + possibile per vendere put, non uno neutro. Le settimane sono 10 per asset, non 16 + (prima del 2026-06-09 l'archivio seguiva solo la finestra 13-20 DTE: quella 4-10 DTE + non esiste). Esito: 10/10 vincenti su BTC, e la lente giusta non e' lo Sharpe ma la + regola del tre — 0 perdite su 10 lascia un tasso vero fino al 30%, su un payoff che + perde ~7x il credito. + ⚠️ Sfumatura che va detta: il gate non e' lontano. Ha mancato l'apertura per 0.0002 di + IV-rank il 2026-06-07, e bastano ~+10% di DVOL per riaprirlo. Il campione e' dentro un + regime di vol bassa persistente, non davanti a un muro. - 2. LA FAMIGLIA CHE HA STORIA NON E' QUELLA CHE SI PUO' ESEGUIRE. Le quote raccolte - sono le opzioni INVERSE (margine e regolamento in BTC/ETH); il conto e' in USDC. - La famiglia USDC-lineare esiste per BTC ed ETH ed ha un lotto minimo 10x piu' piccolo - (BTC 0.01 invece di 0.1) — quindi il muro «BTC fuori a $3.000» del 30/07, congelato - in un test, e' misurato sulla famiglia sbagliata. Ma di quella famiglia abbiamo ZERO - ore di storia e l'open interest e' ~0. + 2. IL TITOLO E' QUASI TUTTO FORTUNA D'ANCORA. L'ora d'ingresso e' un max-of-k mai + dichiarato, ed e' la prima volta che si misura su questa struttura: su 9 ancore lo + Sharpe canonico BTC 32.58 sta all'89 percentile e la MEDIANA ONESTA e' 1.45, con la + banda che tocca il NEGATIVO (-0.35). Fortuna +31.13. Su ETH 3.90 -> 2.41. + Il numero da citare per BTC e' 1.45, non 32.58 — e a quel punto non c'e' piu' niente + di straordinario da spiegare. - 3. LO SPREAD E' IL COSTO DOMINANTE E NON E' MODELLABILE DA UN MID. Attraversare il - bid/ask costa da solo una quota grossa del credito, e l'ala che si COMPRA e' la gamba - dove il libro e' piu' largo — lo stesso meccanismo che il 30/07 misurava come f=0.73. + 3. LA FAMIGLIA CHE HA STORIA NON E' QUELLA CHE SI PUO' ESEGUIRE. Le quote raccolte sono + le opzioni INVERSE (margine e regolamento in BTC/ETH); il conto e' in USDC e il book + esegue sui perpetual lineari USDC. Esiste una famiglia USDC-lineare per BTC ed ETH col + lotto minimo 10x piu' piccolo (BTC 0.01 = $773 di nozionale invece di 0.1 = $7.728), + quindi il muro «BTC fuori a $3.000» del 30/07 — congelato in + tests/test_vrp_profit_take.py — e' misurato sulla famiglia che il conto non margina. + E' la 3a occorrenza della stessa forma dopo fee_watch (21/08) e la taratura di + venue_watch: un numero puntato su una configurazione diversa da quella che gira. + Ma di quella famiglia abbiamo ZERO ore di storia. - Cio' che NON si conclude: che la struttura non funzioni. Non e' stata osservata nel regime - in cui lavora. La regola «niente short-vol da MODELLO in deploy» NON diventa «short-vol da - QUOTE»: le quote ora ci sono, ma sono quote della famiglia sbagliata, in un regime solo. + 4. LO SPREAD E' IL COSTO DOMINANTE, E SULLA FAMIGLIA ESEGUIBILE E' PEGGIO. Attraversare + il bid/ask costa ~10% del credito sugli INVERSE; nella regione di strike che serve lo + spread relativo mediano e' 24-37% sugli inverse e 29-59% sulle USDC, con open interest + ~0. Il f del credito netto si replica indipendentemente a 0.714 (IC95 [0.690, 0.779], + 0/19 osservazioni >= 1.0) — conferma dello 0.73 del 30/07 su un campione piu' lungo e + con un percorso diverso. - AZIONE RACCOMANDATA (non eseguita — tocca la produzione, e la decisione e' dell'operatore): - far raccogliere a `collect_chain.py` anche `currency=USDC` (famiglie BTC_USDC/ETH_USDC). - Costa ~2 chiamate in piu' per giro; senza, fra sei mesi ci ritroveremo con un anno di - storia della famiglia che non possiamo tradare e zero di quella che potremmo.""") + CIO' CHE NON SI CONCLUDE: che la struttura non funzioni. Non e' stata osservata nel regime + in cui lavora. E un risultato a favore c'e', va detto: su BTC il null «e' solo crypto + travestito» e' REFUTATO (beta +0.019, t=0.18) — il PnL non viene dal rialzo. Non basta, + perche' il punto 2 toglie il titolo comunque. + + LA REGOLA NON CAMBIA. «Niente short-vol da MODELLO in deploy» non diventa «short-vol da + QUOTE»: le quote adesso ci sono, ma sono quote della famiglia sbagliata, in un regime solo, + e con l'ancora che spiega il risultato. La sostituzione della regola resta NON DIMOSTRATA. + + DUE AZIONI, NESSUNA ESEGUITA (toccano la produzione o il conto: decide l'operatore). + + (A) DATO — far raccogliere a `collect_chain.py` anche `currency=USDC`. Costo MISURATO col + filtro OI>=100 gia' in vigore: ETH_USDC porterebbe 119 strumenti per giro (+21% di + chiamate, da ~570 a ~690), BTC_USDC ne porterebbe 5 su 686 (0.7%) = nulla di + utilizzabile. Cioe': l'unica struttura USDC-regolata con un libro vero e' su ETH, + lotto minimo 0.1 ETH = $242. Senza questa raccolta, fra sei mesi avremo un anno di + storia della famiglia che non possiamo marginare e zero di quella che potremmo — che + e' esattamente la posizione di oggi, un'ondata dopo l'altra. + + (B) CONTO — una domanda che nessun dato pubblico chiude e che viene PRIMA di ogni + backtest: un saldo in USDC puo' marginare un'opzione INVERSE? Se no, la famiglia con + 3 mesi di storia non e' negoziabile da questo conto a nessun capitale, e il punto 3 + smette di essere un dettaglio di lotto per diventare un muro. Si risolve come si + risolse il PRIIPs il 26/07: guardando il conto, non i dati. + + SE E QUANDO RIAPRIRLO. Non «quando ci sara' un crash» (criterio senza numero) ma: DVOL BTC + > 48.3 o ETH > 62.8 in modo persistente, PIU' ~un anno di settimane raccolte in quel + regime sulla famiglia eseguibile. Prima di allora ogni misura su questa struttura + descrivera' la sua astensione.""") if __name__ == "__main__": From f4b14f305bcad4d330ea4375fa58009db5b7f971 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:12:42 +0000 Subject: [PATCH 006/114] =?UTF-8?q?research(wave-0822):=20FLOW-SQUEEZE=20s?= =?UTF-8?q?cartato=20=E2=80=94=20e=20il=20filone=20funding=20si=20chiude?= =?UTF-8?q?=20sul=20QUARTO=20lato=20(affollamento)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 26 ++++++++++++ scripts/research/r0822_skew.py | 73 +++++++++++++++++++--------------- 2 files changed, 67 insertions(+), 32 deletions(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 36bd8aa..f60e6c7 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -10,6 +10,7 @@ null de-levering superato + eseguibilita' al capitale dichiarato. | 2 | GROWTH-POLICY | **LEAD** (condizione, non data) | il libro gira al **7% di Kelly** e raccoglie il **15%** della crescita massima in log; il gradino eseguibile 1.00x->1.25-1.50x vale 14.7a -> 12.9-11.6a al muro. Ma un solo giorno -10% all'anno porta k* da ~10x a **2x**: non si decide su un conto vero con la lente close-only | | 12 | ORTHO-SCREEN | **SCARTATO 7/7** | nessuna famiglia arriva a ADDS+DSR; e il motivo e' **aritmetico**: su 168 trial il massimo atteso dal rumore e' Sharpe **1.572**, SOPRA il soffitto direzionale misurato (~1.3) -> uno screen largo su BTC/ETH direzionale **non puo'** passare un DSR, per costruzione | | 3 | XS-LITE | **SCARTATO** (come sleeve) + **1 soglia pubblicata falsificata** | concentrare XS01 non crea uno sleeve nuovo (corr 0.81-0.96 col canonico, DSR FAIL, de-levering non superato) — ma il muro *"XS01 serve ~$20k"* e' misurato su una diagnostica di TURNOVER: il ribilancio vero smette di passare sotto **~$109 di sleeve (~$730 di book), 27x piu' in basso** | +| 10 | FLOW-SQUEEZE | **SCARTATO** (2 motivi indipendenti) | meta' dell'ipotesi **non esiste nel dato** (`liquidation_*_risk` = una sola categoria in 17.229/17.229 righe); l'altra meta' e' sotto la propria soglia di potenza **dichiarata prima** (MDE 1,056% vs effetto 0,402%) ed e' comunque **morta alla nascita** (la colonna non e' ricostruibile) | ## Note che sopravvivono ai singoli filoni @@ -118,3 +119,28 @@ sta verificando i minimi veri del venue. `paper_xsr` da' **2.23**; 1.82 non e' nessuna delle due. Spiegazione piu' probabile: `data/raw` e' gitignored e il cron riscrive i parquet HL ogni notte -> **stesso codice, dati diversi** (identico allo scoperto del 07/08 su GTAA/TLT). **La decisione del 23/10 poggia su quel numero.** + +### 10 — FLOW-SQUEEZE (r0822_flow_squeeze.py, 118 trial dichiarati, griglia RIDOTTA per budget) +📌 **Sottoprodotto con potenza vera, e chiude un filone: il funding non predice ne' direzione ne' +volatilita'** — misurato su **53.430 ore / 3 anni** di funding Hyperliquid (li' la potenza c'e'): +eccesso |<=0,35%| a 24h con **segno instabile fra anni**, |ritorno| futuro piatto e non monotono. +**Il filone funding, gia' chiuso su 3 lati come livello di carry, si puo' dichiarare chiuso anche +come proxy di AFFOLLAMENTO. Quarto lato.** +- Il "segnale" migliore (Sharpe 3,25 su 90 giorni) e' **il massimo di 18 celle**: DSR **0,103** + (0,004 su 118 trial), `implausible_sharpe=True`, e su 114 giorni **il massimo atteso per caso e' + Sharpe 5,40** -> il candidato sta **sotto il proprio null**. Il 71% del P&L e' in giugno, il 60% + in **3 giorni su 114**. +- Il null "statica travestita" e' superato (sempre-short = −0,67), ma quello di **timing casuale + location-matched** e' al 99,7° pctl **che pero' e' il massimo di 18** -> P(per caso) = 0,06: + **non superato dopo il conto dei trial**. Esempio pulito di come si legge un percentile alto. +- ⚠️ **Ricostruibilita' MISURATA, non assunta:** l'OI della catena (opzioni) contro la colonna dello + snapshot (perpetual) da' **Pearson +0,03 BTC / +0,21 ETH** — non e' la stessa grandezza. +- 💡 **Azione possibile, costo quasi nullo (NON eseguita, tocca il cron di produzione):** accendere + un collettore orario dell'**OI dei perpetual Deribit** (una GET/ora, API pubblica). Non ripara + questo filone, ma da quel giorno la storia comincia; oggi ripartirebbe da zero. Il criterio di + falsificazione e' gia' scritto dall'agente: >=12 mesi, eccesso oltre la MDE (~0,3%) e presente in + >=3 trimestri su 4. +- ⚠️ **Distinzione metodologica utile** (nata contraddicendo il suggerimento del coordinatore): qui + il vincolo binding NON e' la concentrazione degli episodi (86-87 episodi corti, i primi 8 coprono + solo il 20-25% delle ore) ma la **lunghezza del calendario** (90 giorni). *Ampiezza scarsa* e + *calendario corto* sono due diagnosi diverse e si curano in modi diversi: piu' episodi vs piu' tempo. diff --git a/scripts/research/r0822_skew.py b/scripts/research/r0822_skew.py index 2c5b4e5..4885d84 100644 --- a/scripts/research/r0822_skew.py +++ b/scripts/research/r0822_skew.py @@ -68,6 +68,12 @@ def tick_of(prezzo: np.ndarray) -> np.ndarray: return np.where(np.asarray(prezzo, float) < 0.005, 0.0001, 0.0005) +def _ms(ix: pd.DatetimeIndex) -> np.ndarray: + """Epoca esplicita in millisecondi. Lezione 01/07: `.view("int64")` (e `.astype`) su un + DatetimeIndex tz-aware non-ns sbaglia scala o solleva -> merge_asof broadcasta in silenzio.""" + return pd.DatetimeIndex(ix).tz_convert("UTC").tz_localize(None).astype("datetime64[ms]").astype("int64").to_numpy() + + def rule(t: str) -> None: print("\n" + "=" * 78) print(t) @@ -142,38 +148,41 @@ def _iv_at(ad: np.ndarray, iv: np.ndarray, tgt: float) -> float: def build_smile(q: pd.DataFrame) -> pd.DataFrame: - """Per (asset, ora, scadenza): IV a |delta|=0.25 su entrambe le ali e ATM a |delta|=0.50.""" - cache = SCRATCH / "r0822_smile.parquet" - if cache.exists(): - try: - return pd.read_parquet(cache) - except Exception: - pass - rows = [] - for (asset, hr, exp), g in q.groupby(["asset", "hr", "exp"], sort=False): - c = g[g["option_type"] == "C"] - p = g[g["option_type"] == "P"] - if len(c) < 2 or len(p) < 2: - continue - c25 = _iv_at(c["ad"].to_numpy(), c["iv"].to_numpy(), 0.25) - p25 = _iv_at(p["ad"].to_numpy(), p["iv"].to_numpy(), 0.25) - if not (np.isfinite(c25) and np.isfinite(p25)): - continue - c50 = _iv_at(c["ad"].to_numpy(), c["iv"].to_numpy(), 0.50) - p50 = _iv_at(p["ad"].to_numpy(), p["iv"].to_numpy(), 0.50) - atm = np.nanmean([x for x in (c50, p50) if np.isfinite(x)]) if ( - np.isfinite(c50) or np.isfinite(p50)) else np.nan - rows.append((asset, hr, exp, float(g["dte"].iloc[0]), c25, p25, atm, - g["ts"].max(), int(len(g)))) - R = pd.DataFrame(rows, columns=["asset", "hr", "exp", "dte", "ivc25", "ivp25", - "ivatm", "ts_max", "n_gambe"]) + """Per (asset, ora, scadenza): IV a |delta|=0.25 su entrambe le ali e ATM a |delta|=0.50. + + Vettorizzato per bracketing (nessun ciclo per gruppo): per ogni chiave si prende la riga con + |delta| massimo SOTTO il bersaglio e quella con |delta| minimo SOPRA, poi si interpola fra + le due. Se una delle due manca, la cella si scarta -> non si estrapola MAI (una IV a 25 delta + ricavata estrapolando dall'ala e' un numero inventato, e sarebbe proprio dove il segnale + sembrerebbe piu' forte).""" + KEY = ["asset", "hr", "exp", "option_type"] + + def bracket(d: pd.DataFrame, tgt: float) -> pd.DataFrame: + d = d[["asset", "hr", "exp", "option_type", "ad", "iv"]] + lo = d[d["ad"] <= tgt].sort_values("ad").groupby(KEY, sort=False, observed=True).tail(1) + hi = d[d["ad"] >= tgt].sort_values("ad").groupby(KEY, sort=False, observed=True).head(1) + m = lo.merge(hi, on=KEY, suffixes=("_lo", "_hi")) + span = (m["ad_hi"] - m["ad_lo"]).to_numpy() + w = np.where(span > 1e-12, (tgt - m["ad_lo"].to_numpy()) / np.where(span > 1e-12, span, 1.0), 0.0) + m["iv_t"] = m["iv_lo"].to_numpy() * (1 - w) + m["iv_hi"].to_numpy() * w + return m[KEY + ["iv_t"]] + + # bastano le scadenze che possono ABBRACCIARE 7 o 30 giorni + d = q[q["dte"] <= 70.0] + b25 = bracket(d, 0.25).pivot_table(index=["asset", "hr", "exp"], columns="option_type", + values="iv_t", observed=True) + b50 = bracket(d, 0.50).pivot_table(index=["asset", "hr", "exp"], columns="option_type", + values="iv_t", observed=True) + R = pd.DataFrame(index=b25.index) + R["ivc25"] = b25.get("C") + R["ivp25"] = b25.get("P") + R = R.dropna(subset=["ivc25", "ivp25"]) # servono ENTRAMBE le ali, sempre + R["ivatm"] = b50.reindex(R.index)[[c for c in ("C", "P") if c in b50.columns]].mean(axis=1) + agg = d.groupby(["asset", "hr", "exp"], sort=False, observed=True).agg( + dte=("dte", "first"), ts_max=("ts", "max"), n_gambe=("iv", "size")) + R = R.join(agg, how="left").reset_index() R["rr"] = R["ivc25"] - R["ivp25"] R["bf"] = 0.5 * (R["ivc25"] + R["ivp25"]) - R["ivatm"] - try: - SCRATCH.mkdir(parents=True, exist_ok=True) - R.to_parquet(cache) - except Exception: - pass return R @@ -261,8 +270,8 @@ def daily_signal(S: pd.DataFrame, df: pd.DataFrame) -> pd.DataFrame: bpd = A.bars_per_day(df) chiusura = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) chiusura = chiusura + (pd.Timedelta("24h") - pd.Timedelta("1h") if bpd == 1 else pd.Timedelta(0)) - left = pd.DataFrame({"t_ms": chiusura.astype("datetime64[ms]").astype("int64")}) - right = pd.DataFrame({"t_ms": pd.DatetimeIndex(S["ts_max"]).astype("datetime64[ms]").astype("int64"), + left = pd.DataFrame({"t_ms": _ms(chiusura)}) + right = pd.DataFrame({"t_ms": _ms(pd.DatetimeIndex(S["ts_max"])), "rr": S["rr"].to_numpy(), "rr_n": S["rr_n"].to_numpy(), "bf": S["bf"].to_numpy(), "atm": S["atm"].to_numpy()}).sort_values("t_ms") m = pd.merge_asof(left.sort_values("t_ms"), right, on="t_ms", direction="backward", From 75606d0f449508c71ccabafedf1dbbb3d47a8a1f Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:19:07 +0000 Subject: [PATCH 007/114] =?UTF-8?q?research(wave-0822):=20VRP-QUOTE-VERE?= =?UTF-8?q?=20scartato=20=E2=80=94=20e=20collect=5Fchain=20raccoglie=20la?= =?UTF-8?q?=20famiglia=20di=20contratti=20che=20il=20conto=20non=20puo'=20?= =?UTF-8?q?marginare?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 45 ++ scripts/research/r0822_basis_calendar.py | 787 ++++++++++++++++++++++ scripts/research/r0822_hl_exec.py | 567 ++++++++++++++++ scripts/research/r0822_oi_pin.py | 33 +- scripts/research/r0822_prop_alloc.py | 50 +- scripts/research/r0822_skew.py | 149 +++- scripts/research/r0822_term_structure.py | 118 +++- scripts/research/r0822_vol_size.py | 29 +- scripts/research/r0822_vrp_real_quotes.py | 59 +- 9 files changed, 1800 insertions(+), 37 deletions(-) create mode 100644 scripts/research/r0822_basis_calendar.py create mode 100644 scripts/research/r0822_hl_exec.py diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index f60e6c7..2c5f880 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -11,6 +11,7 @@ null de-levering superato + eseguibilita' al capitale dichiarato. | 12 | ORTHO-SCREEN | **SCARTATO 7/7** | nessuna famiglia arriva a ADDS+DSR; e il motivo e' **aritmetico**: su 168 trial il massimo atteso dal rumore e' Sharpe **1.572**, SOPRA il soffitto direzionale misurato (~1.3) -> uno screen largo su BTC/ETH direzionale **non puo'** passare un DSR, per costruzione | | 3 | XS-LITE | **SCARTATO** (come sleeve) + **1 soglia pubblicata falsificata** | concentrare XS01 non crea uno sleeve nuovo (corr 0.81-0.96 col canonico, DSR FAIL, de-levering non superato) — ma il muro *"XS01 serve ~$20k"* e' misurato su una diagnostica di TURNOVER: il ribilancio vero smette di passare sotto **~$109 di sleeve (~$730 di book), 27x piu' in basso** | | 10 | FLOW-SQUEEZE | **SCARTATO** (2 motivi indipendenti) | meta' dell'ipotesi **non esiste nel dato** (`liquidation_*_risk` = una sola categoria in 17.229/17.229 righe); l'altra meta' e' sotto la propria soglia di potenza **dichiarata prima** (MDE 1,056% vs effetto 0,402%) ed e' comunque **morta alla nascita** (la colonna non e' ricostruibile) | +| 11 | VRP-QUOTE-VERE | **SCARTATO** + **1 difetto di RACCOLTA in produzione** | il campione contiene l'**astensione** della strategia (0/19 settimane passano IV-rank>0.30) e il titolo non sopravvive all'ora d'ingresso; ma soprattutto: **`collect_chain.py` raccoglie la famiglia di contratti che il conto NON puo' marginare** | ## Note che sopravvivono ai singoli filoni @@ -144,3 +145,47 @@ come proxy di AFFOLLAMENTO. Quarto lato.** il vincolo binding NON e' la concentrazione degli episodi (86-87 episodi corti, i primi 8 coprono solo il 20-25% delle ore) ma la **lunghezza del calendario** (90 giorni). *Ampiezza scarsa* e *calendario corto* sono due diagnosi diverse e si curano in modi diversi: piu' episodi vs piu' tempo. + +### 11 — VRP-QUOTE-VERE (r0822_vrp_real_quotes.py, 60 trial + 9 ancore d'ingresso) +🚨 **IL RISULTATO DI PRODUZIONE — `collect_chain.py` interroga `{"currency": "BTC"|"ETH"}`, che su +Deribit restituisce le sole opzioni INVERSE (regolate in BTC/ETH). Il conto e' in USDC** (esegue +`BTC_USDC-PERPETUAL`). Esiste una famiglia **USDC-lineare** con lotto minimo **10x piu' piccolo**: +| famiglia | min lotto | ~$ per lotto | +|---|---|---| +| BTC inverse | 0.1 | ~$7.700 | +| **BTC_USDC** | **0.01** | ~$780 | +| ETH inverse | 1.0 | ~$2.500 | +| **ETH_USDC** | **0.1** | ~$250 | +Quindi il muro pubblicato *"BTC opzioni min 0.1 = $6.210/lotto -> FUORI a $3.000"* (congelato in +`tests/test_vrp_profit_take.py`) e' **misurato sulla famiglia che il conto non puo' marginare**. +**3ª occorrenza dello schema `fee_watch` (21/08): un controllo/collettore puntato su uno strumento +diverso da quello che si trada.** +⚠️ **MA la verifica indipendente del coordinatore cambia la conclusione, e in meglio:** contando +l'open interest famiglia per famiglia (`get_book_summary_by_currency`, misura mia, non dell'agente) +| famiglia | strumenti | con OI>=100 | +|---|---|---| +| BTC inverse | 1038 | **415** | +| ETH inverse | 932 | **548** | +| **BTC_USDC** | 686 | **5** | +| **ETH_USDC** | 660 | **119** | +-> **BTC_USDC e' praticamente morto** (5 strumenti liquidi su 686). **Il muro sul BTC RESTA, ma per +la ragione giusta e meglio misurata: non la taglia del lotto, la LIQUIDITA'.** ETH_USDC (119 +strumenti, ~$250/lotto) e' l'unica gamba che varrebbe la pena raccogliere. +📌 Sottoprodotto fuori perimetro: esiste un universo opzioni USDC piu' liquido di BTC_USDC — +SOL_USDC 341, XRP_USDC 250, HYPE_USDC 172, AVAX_USDC 146 strumenti con OI>=100. +- **Il campione misura la strategia mentre STA FERMA**: 0/19 settimane passano il gate IV-rank>0.30, + DVOL alla mediana del 7°/11° percentile storico, sottostante +25%/+44%. +- ✅ **Replica indipendente del f del 30/07 su campione piu' lungo: f = 0,714 pooled** (IC95 + [0,690, 0,779], **0/19 osservazioni >= 1,0**). Attraversare lo spread costa **~10% del credito**. +- ⚠️ **Correzione a un numero pubblicato:** il forfait fee del sleeve sovrastima il listino vero di + **1,44x (BTC) / 1,80x (ETH)**, non di "~2x". Il segno (conservativo) regge, la taglia no. +- ⚠️ **Rischio sul MINIMO, non sulle chiusure:** peggior mark infra-settimana mediana −6,4% del + rischio, **minimo −91,0%**; fra le settimane chiuse **in utile**, minimo **−56,9%**. +- Gate: `implausible_sharpe` **FLAGGED** su entrambi (0 perdite su 10 -> regola del tre: tasso vero + fino al 30%, su un payoff che perde 7,4x il credito); `deflated_sharpe` **non calcolabile** (T=10) + e l'agente **si e' rifiutato di fabbricare una griglia giornaliera al 94% di zeri** per superare + il T>=30; banda d'ancora: canonico all'89° pctl, **mediana onesta 1,45**, banda che include il + negativo. Il titolo annualizzato (32,58) e' stato **deliberatamente non riportato come risultato**. +- ⚠️ L'agente ha **refutato in corsa una propria conclusione**: spostare l'ora d'ingresso cambia + QUALI STRIKE corrispondono al delta obiettivo -> non e' lo stesso trade un'ora dopo, e' **un'altra + struttura**; la media BTC **cambia segno** fra le ancore (−1,35% -> +11,22%). diff --git a/scripts/research/r0822_basis_calendar.py b/scripts/research/r0822_basis_calendar.py new file mode 100644 index 0000000..2521f6e --- /dev/null +++ b/scripts/research/r0822_basis_calendar.py @@ -0,0 +1,787 @@ +#!/usr/bin/env python3 +""" +r0822_basis_calendar.py — filone BASIS-CALENDAR (ondata 2026-08-22) + +DOMANDA + Il progetto ha chiuso il FUNDING su tre lati (CC01 carry spot-perp, time-series, + cross-sectional) ma non ha mai guardato la STRUTTURA A TERMINE dei futures datati + di Deribit — l'unico strumento delta-neutro eseguibile sul NOSTRO venue, a 2 gambe. + Q1 Il basis (datato vs indice/perp) e la sua pendenza sono PREVEDIBILI, cioe' + esiste un roll-down incassabile, o e' un martingala rumoroso? + Q2 Un CALENDAR SPREAD (front vs back) su segnale di pendenza produce un edge + netto di fee e spread bid-ask REALI? + Q3 E' ortogonale al libro ed eseguibile a $600-3.000? + +IL DATO — costruito qui, non esisteva + `data/raw/fut_*` sono futures INDICE AZIONARIO da IB (ES/NQ/DAX...), niente a che + vedere. Su Deribit non c'era nulla. Ma: + * /public/get_instruments?expired=true ritorna UN SOLO strumento (Deribit purga + subito le liste), quindi la via ovvia non funziona; + * i nomi dei trimestrali sono DETERMINISTICI (ultimo venerdi' di MAR/GIU/SET/DIC, + 08:00 UTC) e `get_tradingview_chart_data` serve la storia COMPLETA di un + contratto SCADUTO se il nome lo si costruisce a mano. + Verificato: 30/30 trimestrali 2019Q1..2026Q2 rispondono `status=ok`. + => **7+ anni di storia**, che includono il deleveraging 2022. E' esattamente la coda + che a CC01 mancava per costruzione (funding HL dal 2023-05): qui il 2022 c'e'. + + La chiamata funding porta in regalo l'INDICE Deribit orario (`index_price`), quindi + spot e funding arrivano insieme e il basis si misura sull'indice vero, non su una proxy. + +DISCIPLINA DI RETE (la VPS ha un IP solo e ci gira il libro) + <=2 richieste/s, e il fetcher DORME nella finestra :25-:29 di ogni ora, che e' + `cron_chain` (il collettore della catena opzioni). Il progetto ha gia' pagato un'ora + di guasto per un IP saturato. Tutto e' in cache su scratchpad: un ri-run non tocca la rete. + +CONVENZIONE DI ROLL — dichiarata, e mai una serie incollata + I prezzi NON vengono mai splicciati. La posizione e' su una COPPIA DI CONTRATTI + specifica; al roll si chiude e si riapre pagando il round-trip pieno su entrambe le + gambe. L'incollaggio avviene solo nella serie dei RENDIMENTI, che e' l'unico posto in + cui e' lecito: e' l'errore che il progetto ha gia' commesso con le serie equity + ri-aggiustate, e qui e' strutturalmente impossibile. + +USO + nice -n 19 timeout 900 uv run python scripts/research/r0822_basis_calendar.py +""" +from __future__ import annotations + +import calendar +import datetime as dt +import json +import math +import os +import sys +import time +import urllib.parse +import urllib.request +from pathlib import Path + +import numpy as np +import pandas as pd + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) +sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "alt")) + +import altlib as A # noqa: E402 + +SCRATCH = Path( + os.environ.get( + "R0822_SCRATCH", + "/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad", + ) +) +CACHE = SCRATCH / "basis_cache" +CACHE.mkdir(parents=True, exist_ok=True) + +API = "https://www.deribit.com/api/v2" +UTC = dt.timezone.utc +MONTHS = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", + "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] + +ASSETS = ("BTC", "ETH") +FEE_SIDE = 0.00035 # 3,5 bps/lato per GAMBA (taker Deribit, letto dal venue) +HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC") +HOURS_Y = 365.25 * 24 + +_last_call = [0.0] + + +# ========================================================================== +# 0. FETCHER — rate-limited, cache-first, consapevole dei cron della VPS +# ========================================================================== +def _polite_sleep() -> None: + """<=2 req/s E fuori dalla finestra :25-:29 (cron_chain, stesso IP).""" + now = dt.datetime.now(UTC) + if 25 <= now.minute <= 29: + wait = (30 - now.minute) * 60 - now.second + print(f" [rete] finestra cron_chain (:25-:29) -> dormo {wait}s") + time.sleep(max(1, wait)) + gap = time.time() - _last_call[0] + if gap < 0.55: + time.sleep(0.55 - gap) + _last_call[0] = time.time() + + +def _api(path: str, **kw) -> dict: + url = API + path + "?" + urllib.parse.urlencode(kw) + for attempt in range(4): + _polite_sleep() + try: + with urllib.request.urlopen(url, timeout=30) as r: + return json.load(r) + except Exception as exc: # noqa: BLE001 + if attempt == 3: + raise + print(f" [rete] {type(exc).__name__} -> retry {attempt+1}/3") + time.sleep(2.0 * (attempt + 1)) + raise RuntimeError("unreachable") + + +def last_friday(year: int, month: int) -> dt.date: + d = dt.date(year, month, calendar.monthrange(year, month)[1]) + while d.weekday() != 4: + d -= dt.timedelta(days=1) + return d + + +def quarterly_universe(asset: str, y0: int = 2019, y1: int = 2027) -> list[tuple[str, pd.Timestamp]]: + """Nomi DETERMINISTICI dei trimestrali Deribit: ultimo venerdi' di MAR/GIU/SET/DIC 08:00 UTC.""" + out = [] + for y in range(y0, y1 + 1): + for m in (3, 6, 9, 12): + lf = last_friday(y, m) + exp = pd.Timestamp(lf.year, lf.month, lf.day, 8, tz="UTC") + out.append((f"{asset}-{lf.day}{MONTHS[m-1]}{str(y)[2:]}", exp)) + return out + + +def fetch_ohlc(instrument: str, t0: pd.Timestamp, t1: pd.Timestamp) -> pd.DataFrame: + """OHLCV orario di UNO strumento (anche SCADUTO), a chunk di 90 giorni. Cache su disco.""" + cf = CACHE / f"ohlc_{instrument.replace('/', '_')}.parquet" + if cf.exists(): + return pd.read_parquet(cf) + frames = [] + cur = t0 + while cur < t1: + end = min(cur + pd.Timedelta(days=90), t1) + try: + res = _api("/public/get_tradingview_chart_data", + instrument_name=instrument, resolution="60", + start_timestamp=int(cur.timestamp() * 1000), + end_timestamp=int(end.timestamp() * 1000)).get("result", {}) + except Exception as exc: # noqa: BLE001 + print(f" {instrument}: chunk {cur.date()} FALLITO ({type(exc).__name__})") + cur = end + continue + ticks = res.get("ticks") or [] + if res.get("status") == "ok" and ticks: + frames.append(pd.DataFrame({ + "ts": pd.to_datetime(ticks, unit="ms", utc=True), + "open": res["open"], "high": res["high"], + "low": res["low"], "close": res["close"], + "volume": res.get("volume", [np.nan] * len(ticks)), + })) + cur = end + df = (pd.concat(frames).drop_duplicates("ts").sort_values("ts").reset_index(drop=True) + if frames else pd.DataFrame(columns=["ts", "open", "high", "low", "close", "volume"])) + df.to_parquet(cf, index=False) + return df + + +def fetch_funding_and_index(asset: str, t0: pd.Timestamp, t1: pd.Timestamp) -> pd.DataFrame: + """Funding orario del perp E indice Deribit (la chiamata li porta insieme).""" + cf = CACHE / f"fund_{asset}.parquet" + if cf.exists(): + return pd.read_parquet(cf) + frames, cur = [], t0 + while cur < t1: + end = min(cur + pd.Timedelta(days=30), t1) + try: + res = _api("/public/get_funding_rate_history", + instrument_name=f"{asset}-PERPETUAL", + start_timestamp=int(cur.timestamp() * 1000), + end_timestamp=int(end.timestamp() * 1000)).get("result", []) + except Exception: # noqa: BLE001 + cur = end + continue + if res: + frames.append(pd.DataFrame({ + "ts": pd.to_datetime([r["timestamp"] for r in res], unit="ms", utc=True), + "index": [r["index_price"] for r in res], + "f1h": [r["interest_1h"] for r in res], + })) + cur = end + df = (pd.concat(frames).drop_duplicates("ts").sort_values("ts").reset_index(drop=True) + if frames else pd.DataFrame(columns=["ts", "index", "f1h"])) + df.to_parquet(cf, index=False) + return df + + +def fetch_live_books(instruments: list[str]) -> pd.DataFrame: + """Calibrazione DATATA dello spread bid-ask: il book VIVO di oggi. + Non e' storia — e' un punto di ancoraggio per una stima, e va detto.""" + cf = CACHE / "live_books.parquet" + if cf.exists(): + return pd.read_parquet(cf) + rows = [] + for nm in instruments: + try: + r = _api("/public/get_order_book", instrument_name=nm, depth=5)["result"] + bid, ask = r.get("best_bid_price"), r.get("best_ask_price") + if bid and ask and bid > 0: + rows.append(dict(instrument=nm, bid=bid, ask=ask, + mid=(bid + ask) / 2, + spread_bps=1e4 * (ask - bid) / ((ask + bid) / 2), + bid_sz=r.get("best_bid_amount"), ask_sz=r.get("best_ask_amount"))) + except Exception: # noqa: BLE001 + continue + df = pd.DataFrame(rows) + df.to_parquet(cf, index=False) + return df + + +# ========================================================================== +# 1. PANEL + CERTIFICAZIONE (prima di ogni strategia — regola di prim'ordine) +# ========================================================================== +def build_panel(asset: str) -> tuple[pd.DataFrame, pd.DataFrame]: + """Ritorna (wide, meta). `wide` = indice orario x contratto -> close. + `meta` = una riga per contratto con scadenza, barre, copertura, quota barre FLAT.""" + uni = quarterly_universe(asset) + today = pd.Timestamp.now(tz="UTC") + closes, vols, meta = {}, {}, [] + for nm, exp in uni: + if exp > today + pd.Timedelta(days=400): + continue + t0 = exp - pd.Timedelta(days=400) + t1 = min(exp + pd.Timedelta(hours=2), today) + if t1 <= t0: + continue + df = fetch_ohlc(nm, t0, t1) + if df.empty or len(df) < 200: + meta.append(dict(contract=nm, exp=exp, bars=len(df), status="VUOTO/CORTO")) + continue + s = df.set_index("ts")["close"].astype(float) + s = s[s > 0] + closes[nm] = s + vols[nm] = df.set_index("ts")["volume"].astype(float) + flat = float((s.diff() == 0).mean()) + meta.append(dict(contract=nm, exp=exp, bars=len(s), + first=s.index.min(), last=s.index.max(), + flat_frac=flat, status="ok")) + wide = pd.DataFrame(closes).sort_index() + vwide = pd.DataFrame(vols).sort_index() + return wide, vwide, pd.DataFrame(meta) + + +def certify(asset: str, wide: pd.DataFrame, vwide: pd.DataFrame, meta: pd.DataFrame, + fund: pd.DataFrame, perp: pd.Series) -> dict: + """Quattro difetti sorvegliati. Un edge su un book fermo NON e' un edge (regola 4).""" + print(f"\n --- CERTIFICAZIONE {asset} ---") + ok = meta[meta.status == "ok"] + print(f" contratti con dato: {len(ok)}/{len(meta)} " + f"finestra {wide.index.min().date()} .. {wide.index.max().date()}") + + # (a) barre FLAT per bucket di giorni-a-scadenza = liquidita' vera del datato + rows = [] + for nm in wide.columns: + exp = ok.loc[ok.contract == nm, "exp"] + if exp.empty: + continue + s = wide[nm].dropna() + dte = (exp.iloc[0] - s.index).total_seconds() / 86400.0 + flat = (s.diff() == 0).astype(float) + for lo, hi, lab in [(0, 30, "0-30g"), (30, 90, "30-90g"), + (90, 180, "90-180g"), (180, 400, ">180g")]: + m = (dte >= lo) & (dte < hi) + if m.sum() > 50: + rows.append(dict(bucket=lab, flat=float(flat[m].mean()), n=int(m.sum()))) + fb = pd.DataFrame(rows).groupby("bucket").agg(flat=("flat", "mean"), n=("n", "sum")) + order = ["0-30g", "30-90g", "90-180g", ">180g"] + print(" barre FLAT (proxy di illiquidita') per giorni-a-scadenza:") + for b in order: + if b in fb.index: + print(f" {b:9s} flat={fb.loc[b,'flat']*100:5.1f}% barre={int(fb.loc[b,'n']):,}") + + # (b) volume orario mediano per bucket (il flat da solo non basta) + vrows = [] + for nm in vwide.columns: + exp = ok.loc[ok.contract == nm, "exp"] + if exp.empty: + continue + s = vwide[nm].dropna() + dte = (exp.iloc[0] - s.index).total_seconds() / 86400.0 + for lo, hi, lab in [(0, 30, "0-30g"), (30, 90, "30-90g"), + (90, 180, "90-180g"), (180, 400, ">180g")]: + m = (dte >= lo) & (dte < hi) + if m.sum() > 50: + vrows.append(dict(bucket=lab, med=float(s[m].median()), zero=float((s[m] == 0).mean()))) + vb = pd.DataFrame(vrows).groupby("bucket").agg(med=("med", "median"), zero=("zero", "mean")) + print(" volume orario mediano (USD di nozionale) e quota ore a volume ZERO:") + for b in order: + if b in vb.index: + print(f" {b:9s} mediano=${vb.loc[b,'med']:>12,.0f} ore a vol 0 = {vb.loc[b,'zero']*100:5.1f}%") + + # (c) accordo con l'INDICE alla scadenza: il datato DEVE convergere + conv = [] + for nm in wide.columns: + e = ok.loc[ok.contract == nm, "exp"] + if e.empty: + continue + exp = e.iloc[0] + s = wide[nm].dropna() + tail = s[s.index >= exp - pd.Timedelta(hours=6)] + idx = fund.set_index("ts")["index"].reindex(tail.index, method="nearest", tolerance=pd.Timedelta("2h")) + if len(tail) and idx.notna().any(): + conv.append(float(np.abs(np.log(tail / idx)).dropna().iloc[-1]) * 1e4) + if conv: + print(f" convergenza a scadenza |ln(F/indice)| ultima ora: " + f"mediana {np.median(conv):.1f} bps, max {np.max(conv):.1f} bps (n={len(conv)})") + + # (d) CROSS-CHECK INDIPENDENTE: il forward implicito nella catena opzioni + xchk = cross_check_option_forward(asset, wide, ok) + return dict(flat=fb, vol=vb, conv=conv, xchk=xchk) + + +def cross_check_option_forward(asset: str, wide: pd.DataFrame, meta: pd.DataFrame) -> dict: + """Il `underlying_price` di un'opzione Deribit E' il forward della SUA scadenza. + La catena raccolta dal 2026-07-30 e' quindi una MISURA INDIPENDENTE del basis: + se il mio F(T) e il forward delle opzioni sulla stessa scadenza non coincidono, + uno dei due e' sbagliato. (L'archivio ereditato da bite ha underlying_price NULL.)""" + import glob + files = sorted(glob.glob(str(PROJECT_ROOT / "data/raw/cb_chain/2026-*.parquet"))) + if not files: + return dict(status="catena assente") + keep = [] + for f in files[-30:]: + try: + d = pd.read_parquet(f, columns=["asset", "exp", "ts", "underlying_price", "index_price"]) + except Exception: # noqa: BLE001 + continue + d = d[(d.asset == asset) & d.underlying_price.notna()] + if len(d): + keep.append(d) + if not keep: + return dict(status="nessun underlying_price") + ch = pd.concat(keep) + ch["ts_h"] = ch["ts"].dt.floor("1h") + # forward mediano per (ora, scadenza) — tutti gli strike condividono lo stesso forward + fw = ch.groupby(["ts_h", "exp"]).agg(fwd=("underlying_price", "median")).reset_index() + rows = [] + for nm in wide.columns: + e = meta.loc[meta.contract == nm, "exp"] + if e.empty: + continue + exp = e.iloc[0] + sub = fw[fw.exp == exp] + if sub.empty: + continue + mine = wide[nm].dropna() + j = sub.set_index("ts_h")["fwd"].reindex(mine.index).dropna() + if len(j) < 5: + continue + dev = (np.log(mine.reindex(j.index) / j) * 1e4).dropna() + rows.append(dict(contract=nm, n=len(dev), med_bps=float(dev.median()), + p95_bps=float(dev.abs().quantile(0.95)))) + return dict(status="ok", table=pd.DataFrame(rows)) + + +# ========================================================================== +# 2. CURVA — front/back trimestrali, basis annualizzato, slope forward +# ========================================================================== +def curve_frame(asset: str, wide: pd.DataFrame, meta: pd.DataFrame, + fund: pd.DataFrame, roll_dte: int) -> pd.DataFrame: + """Per ogni ora: front = trimestrale piu' vicino con dte >= roll_dte, back = il successivo. + Ritorna prezzi, tau, basis annualizzati, slope forward e funding.""" + ok = meta[meta.status == "ok"].sort_values("exp") + exps = {r.contract: r.exp for r in ok.itertuples()} + idx = wide.index + F = fund.set_index("ts").reindex(idx) + n = len(idx) + front = np.full(n, "", dtype=object) + back = np.full(n, "", dtype=object) + contracts = list(wide.columns) + tau = {c: (exps[c] - idx).total_seconds() / 3600.0 for c in contracts if c in exps} + avail = {c: wide[c].notna().values for c in contracts} + for c in contracts: + if c not in exps: + continue + alive = avail[c] & (tau[c] >= roll_dte * 24) + # front = il primo (per scadenza) ancora vivo; i contratti sono ordinati per exp + order = [c for c in ok.contract if c in wide.columns] + for i in range(n): + picked = [] + for c in order: + if avail[c][i] and tau[c][i] >= roll_dte * 24: + picked.append(c) + if len(picked) == 2: + break + if len(picked) == 2: + front[i], back[i] = picked + out = pd.DataFrame(index=idx) + out["front"], out["back"] = front, back + m = (front != "") & (back != "") + Ff = np.full(n, np.nan); Fb = np.full(n, np.nan) + tf = np.full(n, np.nan); tb = np.full(n, np.nan) + W = wide.values + colpos = {c: k for k, c in enumerate(wide.columns)} + for i in np.where(m)[0]: + Ff[i] = W[i, colpos[front[i]]]; Fb[i] = W[i, colpos[back[i]]] + tf[i] = tau[front[i]][i]; tb[i] = tau[back[i]][i] + out["F_front"], out["F_back"] = Ff, Fb + out["tau_front_h"], out["tau_back_h"] = tf, tb + out["index"] = F["index"].values + out["f1h"] = F["f1h"].values + # basis annualizzato di ciascuna gamba vs INDICE + out["c_front"] = np.log(Ff / out["index"]) * HOURS_Y / tf + out["c_back"] = np.log(Fb / out["index"]) * HOURS_Y / tb + # slope = tasso forward annualizzato FRA le due scadenze (stazionario, interpretabile) + out["slope"] = np.log(Fb / Ff) * HOURS_Y / (tb - tf) + out["s_raw"] = np.log(Fb / Ff) + return out + + +def describe_curve(asset: str, cv: pd.DataFrame) -> None: + """Q1 — descrittiva. Il basis e' positivo? persiste? il suo CAMBIO e' prevedibile?""" + c = cv.dropna(subset=["c_front", "slope"]) + print(f"\n --- Q1 CURVA {asset} (n={len(c):,} ore) ---") + print(f" basis annualizzato del FRONT: mediana {c.c_front.median()*100:+6.2f}%/anno " + f"media {c.c_front.mean()*100:+6.2f}% sd {c.c_front.std()*100:5.2f} " + f"contango {float((c.c_front>0).mean())*100:.1f}% del tempo") + print(f" slope forward front->back : mediana {c.slope.median()*100:+6.2f}%/anno " + f"media {c.slope.mean()*100:+6.2f}% sd {c.slope.std()*100:5.2f} " + f"positiva {float((c.slope>0).mean())*100:.1f}% del tempo") + fnd = cv["f1h"].dropna() * HOURS_Y + print(f" funding perp annualizzato : mediana {fnd.median()*100:+6.2f}%/anno " + f"media {fnd.mean()*100:+6.2f}%") + print(" per anno (mediane annualizzate):") + g = c.groupby(c.index.year).agg(basis_front=("c_front", "median"), + slope=("slope", "median")) + fy = (cv["f1h"] * HOURS_Y).groupby(cv.index.year).median() + for y, r in g.iterrows(): + print(f" {y} basis_front {r.basis_front*100:+7.2f}% slope {r.slope*100:+7.2f}% " + f"funding {fy.get(y, np.nan)*100:+7.2f}%") + # prevedibilita': mean-reversion dello slope a 7 giorni (AR sul livello) + for h, lab in [(24, "1g"), (24 * 7, "7g"), (24 * 30, "30g")]: + x = c["slope"] + dx = x.shift(-h) - x + j = pd.concat([x, dx], axis=1).dropna() + if len(j) > 500: + b = np.polyfit(j.iloc[:, 0], j.iloc[:, 1], 1)[0] + rho = float(j.iloc[:, 0].corr(j.iloc[:, 1])) + print(f" mean-reversion slope a {lab:>3s}: beta {b:+.3f} corr {rho:+.3f} " + f"(beta -1 = reversione totale, 0 = martingala)") + + +# ========================================================================== +# 3. STRATEGIE — posizioni in spazio CONTRATTO (il roll si paga da solo) +# ========================================================================== +def _z(x: pd.Series, win: int) -> pd.Series: + m = x.rolling(win, min_periods=win // 3).mean() + s = x.rolling(win, min_periods=win // 3).std() + return ((x - m) / s.replace(0, np.nan)) + + +def run_strategy(cv: pd.DataFrame, wide: pd.DataFrame, family: str, sig: str, + win_h: int, thr: float, slip_bps: float, + fee_side: float = FEE_SIDE) -> pd.Series: + """Ritorno ORARIO netto per $1 di nozionale LORDO PER GAMBA. + Le posizioni vivono in spazio CONTRATTO: quando la coppia cambia (roll) la + variazione per contratto e' automaticamente piena => il roll paga il round-trip + completo su entrambe le gambe. Nessuna serie di prezzi viene MAI incollata. + Causalita': w deciso con dati <= t, TENUTO durante t -> t+1 (shift esplicito).""" + idx = cv.index + if family == "CAL": + base = cv["slope"] + else: # FvP: basis annualizzato del front + base = cv["c_front"] + + if sig == "static_long": + w = pd.Series(1.0, index=idx) + elif sig == "static_short": + w = pd.Series(-1.0, index=idx) + elif sig == "carry": # segno del carry corrente (causale) + w = -np.sign(base) # contango -> vendi il datato + elif sig == "zrev": + z = _z(base, win_h) + w = -np.tanh(z) + w = w.where(z.abs() >= thr, 0.0) + elif sig == "zmom": + z = _z(base, win_h) + w = np.tanh(z) + w = w.where(z.abs() >= thr, 0.0) + else: + raise ValueError(sig) + w = pd.Series(np.asarray(w, float), index=idx).fillna(0.0).clip(-1, 1) + + # --- posizioni per CONTRATTO --- + cols = list(wide.columns) + cpos = {c: k for k, c in enumerate(cols)} + P = np.zeros((len(idx), len(cols))) + fr = cv["front"].values; bk = cv["back"].values + wv = w.values + for i in range(len(idx)): + if fr[i] == "" or bk[i] == "": + continue + if family == "CAL": # long spread = long back / short front + P[i, cpos[bk[i]]] = wv[i] + P[i, cpos[fr[i]]] = -wv[i] + else: # FvP: gamba datata; il perp e' a parte + P[i, cpos[fr[i]]] = wv[i] + Pd = pd.DataFrame(P, index=idx, columns=cols) + + held = Pd.shift(1).fillna(0.0) # decisa a t, TENUTA in t->t+1 + R = wide.reindex(idx).pct_change().replace([np.inf, -np.inf], np.nan).fillna(0.0) + gross = (held * R).sum(axis=1) + + if family == "FvP": + # gamba perp: nozionale opposto alla gamba datata + funding pagato/incassato + perp_r = pd.Series(cv["index"].pct_change().fillna(0.0).values, index=idx) + wheld = held.sum(axis=1) # = w shiftato (una sola gamba datata) + gross = gross - wheld * perp_r - wheld * (-1.0) * cv["f1h"].fillna(0.0) + # long datato (w>0) => short perp => INCASSA funding se f>0 + turn = (Pd.diff().abs().sum(axis=1) * 2.0) # datato + perp + else: + turn = Pd.diff().abs().sum(axis=1) + + cost = turn * (fee_side + slip_bps * 1e-4) + return (gross - cost).fillna(0.0) + + +# ========================================================================== +# 4. METRICHE / GATE +# ========================================================================== +def to_daily(h: pd.Series) -> pd.Series: + return ((1.0 + h.fillna(0.0)).resample("1D").prod() - 1.0).dropna() + + +def stats(daily: pd.Series) -> dict: + r = daily.dropna() + if len(r) < 30 or r.std() == 0: + return dict(sharpe=0.0, holdout=0.0, maxdd=0.0, cagr=0.0, n=len(r), vol=0.0) + sh = float(r.mean() / r.std() * math.sqrt(365.25)) + eq = (1 + r).cumprod() + dd = float(((eq.cummax() - eq) / eq.cummax()).max()) + yrs = (r.index[-1] - r.index[0]).days / 365.25 + cagr = float(eq.iloc[-1] ** (1 / yrs) - 1) if yrs > 0.5 and eq.iloc[-1] > 0 else float("nan") + ho = r[r.index >= HOLDOUT] + sho = float(ho.mean() / ho.std() * math.sqrt(365.25)) if len(ho) > 30 and ho.std() > 0 else float("nan") + return dict(sharpe=sh, holdout=sho, maxdd=dd, cagr=cagr, n=len(r), + vol=float(r.std() * math.sqrt(365.25))) + + +def delevering_null(cand: pd.Series, base: pd.Series) -> dict: + """NULL DEL DE-LEVERING (5 occorrenze in questo progetto). Esiste k<1 che, + applicato al BASELINE, da' lo STESSO maxDD del candidato con Sharpe MIGLIORE? + Se si', il candidato non protegge: e' solo meno leva.""" + cs = stats(cand) + best = None + for k in np.arange(0.05, 1.01, 0.05): + s = stats(base * k) + if s["maxdd"] <= cs["maxdd"] + 1e-9: + best = (float(k), s) + break + if best is None: + return dict(refuted=False, note="nessun k<=1 raggiunge quel DD sul baseline") + k, s = best + return dict(refuted=bool(s["sharpe"] > cs["sharpe"]), k=k, + base_sharpe_at_k=s["sharpe"], cand_sharpe=cs["sharpe"], + base_dd_at_k=s["maxdd"], cand_dd=cs["maxdd"]) + + +# ========================================================================== +# 5. MAIN +# ========================================================================== +def main() -> None: + t_start = time.time() + print("=" * 78) + print("r0822 BASIS-CALENDAR — struttura a termine dei futures datati Deribit") + print("=" * 78) + + today = pd.Timestamp.now(tz="UTC") + panels, funds, metas, vwides = {}, {}, {}, {} + + # ---------------- STEP 1: dato ---------------- + print("\n[1] DATO — ricostruzione da Deribit pubblico (cache su scratchpad)") + for a in ASSETS: + print(f" {a}: trimestrali (ultimo venerdi' MAR/GIU/SET/DIC 08:00 UTC)...") + wide, vwide, meta = build_panel(a) + fund = fetch_funding_and_index(a, pd.Timestamp("2019-01-01", tz="UTC"), today) + panels[a], vwides[a], metas[a], funds[a] = wide, vwide, meta, fund + ok = meta[meta.status == "ok"] + print(f" {len(ok)} contratti, {int(meta.bars.sum()):,} barre orarie, " + f"funding+indice {len(fund):,} ore") + + # ---------------- STEP 2: certificazione ---------------- + print("\n[2] CERTIFICAZIONE (regola 4: un edge su un book fermo non e' un edge)") + certs = {} + for a in ASSETS: + perp = A.get(a, "1h").set_index(pd.DatetimeIndex( + pd.to_datetime(A.get(a, "1h")["datetime"], utc=True)))["close"] + certs[a] = certify(a, panels[a], vwides[a], metas[a], funds[a], perp) + x = certs[a]["xchk"] + if x.get("status") == "ok" and len(x["table"]): + t = x["table"] + print(f" CROSS-CHECK INDIPENDENTE vs forward della catena opzioni " + f"({int(t.n.sum())} ore, {len(t)} scadenze):") + print(f" deviazione mediana {t.med_bps.median():+.1f} bps, " + f"p95 |dev| {t.p95_bps.max():.1f} bps") + else: + print(f" CROSS-CHECK catena: {x.get('status')}") + + # ---------------- STEP 3: Q1 curva ---------------- + print("\n[3] Q1 — LA CURVA E' PREVEDIBILE?") + curves = {} + for a in ASSETS: + cv = curve_frame(a, panels[a], metas[a], funds[a], roll_dte=7) + curves[a] = cv + describe_curve(a, cv) + + # ---------------- STEP 3b: spread bid-ask (calibrazione DATATA) ---------------- + print("\n[3b] SPREAD BID-ASK — calibrazione sul book VIVO di oggi (non e' storia)") + live = [] + for a in ASSETS: + for nm, exp in quarterly_universe(a): + if today < exp <= today + pd.Timedelta(days=400): + live.append(nm) + lb = fetch_live_books(live + [f"{a}-PERPETUAL" for a in ASSETS]) + slip_est = 5.0 + if len(lb): + for r in lb.itertuples(): + print(f" {r.instrument:16s} bid {r.bid:>10,.1f} ask {r.ask:>10,.1f} " + f"spread {r.spread_bps:6.1f} bps size {r.bid_sz:>8,.0f}/{r.ask_sz:>8,.0f}") + dated = lb[~lb.instrument.str.contains("PERPETUAL")] + if len(dated): + slip_est = float(dated.spread_bps.median()) / 2.0 + print(f" => mezzo-spread MEDIANO del datato = {slip_est:.1f} bps/lato " + f"(STIMA datata oggi, NON storia: il book storico dei datati non e' disponibile)") + + # ---------------- STEP 4: griglia ---------------- + print("\n[4] GRIGLIA — dichiarata PRIMA, contata AL RIALZO") + FAM = {"CAL": ["static_long", "static_short", "zrev", "zmom"], + "FvP": ["carry", "static_long", "zrev", "zmom"]} + WINS = [720, 2160, 4320] # 30 / 90 / 180 giorni in ore + ROLLS = [7, 21] + THRS = [0.0, 1.0] + cells = [(f, s, w, rl, th) for f, sigs in FAM.items() for s in sigs + for w in WINS for rl in ROLLS for th in THRS] + print(f" famiglie {list(FAM)} x segnali x finestre {WINS} x roll {ROLLS} x soglie {THRS}") + print(f" = {len(cells)} celle valutate (le varianti statiche non usano W/soglia:" + f" le conto lo stesso, al rialzo)") + + cvcache = {(a, rl): curve_frame(a, panels[a], metas[a], funds[a], rl) + for a in ASSETS for rl in ROLLS} + + rows = [] + for (fam, sig, win, rl, th) in cells: + hs = [] + for a in ASSETS: + h = run_strategy(cvcache[(a, rl)], panels[a], fam, sig, win, th, slip_est) + hs.append(h) + J = pd.concat(hs, axis=1).fillna(0.0) + comb = J.mean(axis=1) # 50/50 BTC+ETH, come il resto del progetto + d = to_daily(comb) + st = stats(d) + rows.append(dict(family=fam, sig=sig, win=win, roll=rl, thr=th, + daily=d, **st)) + G = pd.DataFrame([{k: v for k, v in r.items() if k != "daily"} for r in rows]) + dailies = {i: r["daily"] for i, r in enumerate(rows)} + + print("\n migliori 8 celle per Sharpe FULL (NON e' la selezione — solo panorama):") + for i in G.sharpe.nlargest(8).index: + r = G.loc[i] + print(f" {r.family:4s} {r.sig:12s} W={int(r.win):5d}h roll={int(r.roll):2d}g " + f"thr={r.thr:.1f} | Sh {r.sharpe:+6.2f} hold {r.holdout:+6.2f} " + f"DD {r.maxdd*100:5.2f}% CAGR {r.cagr*100:+6.2f}% vol {r.vol*100:5.2f}%") + + # ---------------- STEP 5: selezione IN-SAMPLE ONLY ---------------- + print("\n[5] SELEZIONE IN-SAMPLE-ONLY (mai la cella col miglior hold-out)") + ins = {} + for i, d in dailies.items(): + pre = d[d.index < HOLDOUT] + ins[i] = (float(pre.mean() / pre.std() * math.sqrt(365.25)) + if len(pre) > 60 and pre.std() > 0 else -9.9) + best_i = max(ins, key=ins.get) + b = G.loc[best_i] + bd = dailies[best_i] + bs = stats(bd) + print(f" cella scelta AL BUIO: {b.family} {b.sig} W={int(b.win)}h roll={int(b.roll)}g thr={b.thr}") + print(f" Sharpe in-sample {ins[best_i]:+.2f} -> FULL {bs['sharpe']:+.2f} " + f"HOLD-OUT {bs['holdout']:+.2f} maxDD {bs['maxdd']*100:.2f}% " + f"CAGR {bs['cagr']*100:+.2f}% vol {bs['vol']*100:.2f}%") + best_full = G.sharpe.idxmax() + print(f" (per confronto, la cella col miglior FULL sarebbe stata " + f"{G.loc[best_full,'family']} {G.loc[best_full,'sig']} " + f"Sh {G.loc[best_full,'sharpe']:+.2f} — NON e' quella scelta)") + + # ---------------- STEP 6: GATE ---------------- + print("\n[6] GATE") + dsr, sr0 = A.deflated_sharpe(bs["sharpe"], list(G.sharpe.values), bd) + print(f" deflated-Sharpe : DSR {dsr:.3f} (null max atteso {sr0:+.2f} su {len(G)} trial) " + f"-> {'PASS' if dsr >= 0.95 else 'FAIL'}") + + imp = A.implausible_sharpe(bd) + print(f" implausible_sharpe: implausible={imp['implausible']} " + f"Sh {imp.get('sharpe', float('nan')):+.2f} maxDD {imp.get('maxdd', float('nan'))*100:.2f}% " + f"quota barre in perdita {imp.get('loss_frac', float('nan'))*100:.1f}% " + f"Calmar {imp.get('calmar', float('nan')):.1f}") + if imp["reasons"]: + for rr in imp["reasons"]: + print(f" - {rr}") + + tp = A.tp01_baseline_daily() + try: + mg = A.marginal_vs_tp01(bd) + print(f" marginal_vs_tp01 : {mg.get('verdict')} corr {mg.get('corr', float('nan')):+.3f} " + f"robust_oos={mg.get('robust_oos')} insample_edge={mg.get('has_insample_edge')} " + f"is_hedge={mg.get('is_hedge')} beats_noise={mg.get('beats_noise_null')}") + except Exception as exc: # noqa: BLE001 + mg = {} + print(f" marginal_vs_tp01 : NON GIRATO ({type(exc).__name__}: {exc})") + + dn = delevering_null(bd, tp) + print(f" null de-levering : refuted={dn.get('refuted')} {dn}") + + # anchor: l'ora del giorno in cui si valuta il segnale + print(" anchor_luck_band (ora del giorno di decisione, 24 ancore):") + def by_off(off): + hs = [] + for a in ASSETS: + cv = cvcache[(a, int(b.roll))] + h = run_strategy(cv, panels[a], b.family, b.sig, int(b.win), float(b.thr), slip_est) + hh = h.copy() + mask = (hh.index.hour != off) + hh[mask] = 0.0 # decide/agisce solo a quell'ora + hs.append(hh) + return to_daily(pd.concat(hs, axis=1).fillna(0.0).mean(axis=1)) + try: + ab = A.anchor_luck_band(by_off, list(range(24)), canonical=0) + print(f" canonica {ab.get('canonical'):+.3f} MEDIANA(onesta) {ab.get('median'):+.3f} " + f"banda [{ab.get('p10'):+.3f},{ab.get('p90'):+.3f}] " + f"frazione>0 {ab.get('frac_positive'):.2f} gate_pass={ab.get('gate_pass')}") + except Exception as exc: # noqa: BLE001 + print(f" NON GIRATO ({type(exc).__name__}: {exc})") + + try: + print(f" causality_ok : la posizione e' shiftata in run_strategy " + f"(held = pos.shift(1)); verifica strutturale, non A.causality_ok " + f"(che vuole un target_fn su BTC/ETH direzionale)") + except Exception: # noqa: BLE001 + pass + + # ---------------- STEP 7: quanto spread uccide l'edge ---------------- + print("\n[7] A CHE SPREAD MUORE L'EDGE (la stima di slippage e' il rischio #1)") + for sl in [0.0, 1.0, 2.5, 5.0, 7.5, 10.0, 20.0]: + hs = [] + for a in ASSETS: + hs.append(run_strategy(cvcache[(a, int(b.roll))], panels[a], b.family, + b.sig, int(b.win), float(b.thr), sl)) + s2 = stats(to_daily(pd.concat(hs, axis=1).fillna(0.0).mean(axis=1))) + mark = " <= stima di oggi" if abs(sl - slip_est) < 0.6 else "" + print(f" mezzo-spread {sl:5.1f} bps/lato -> Sharpe {s2['sharpe']:+6.2f} " + f"CAGR {s2['cagr']*100:+6.2f}%{mark}") + + # ---------------- STEP 8: eseguibilita' ---------------- + print("\n[8] Q3 — ESEGUIBILITA' a $600-3.000") + for a in ASSETS: + try: + sp = _api("/public/get_instrument", instrument_name=f"{a}-25DEC26")["result"] + print(f" {a}-25DEC26: min_trade_amount ${sp['min_trade_amount']:,.0f} di nozionale, " + f"contract_size ${sp['contract_size']:,.0f}, tick {sp['tick_size']}, " + f"taker {sp['taker_commission']*1e4:.1f} bps") + except Exception as exc: # noqa: BLE001 + print(f" {a}: specs non lette ({type(exc).__name__})") + print(f" vol ANNUA del candidato per $1 di nozionale lordo/gamba: {bs['vol']*100:.2f}%") + if bs["vol"] > 0: + lev = 0.20 / bs["vol"] + print(f" per portarlo al vol-target 20% del progetto servirebbe leva " + f"{lev:.1f}x per gamba = ${lev*2:,.0f} di nozionale LORDO per $1 di conto") + for cap in (600, 3000): + print(f" a ${cap}: nozionale lordo ${lev*2*cap:,.0f} " + f"(margine ~2-4%/gamba => ${lev*2*cap*0.03:,.0f} di IM richiesto)") + + print(f"\n[fine] {time.time()-t_start:.0f}s") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_hl_exec.py b/scripts/research/r0822_hl_exec.py new file mode 100644 index 0000000..fa9357d --- /dev/null +++ b/scripts/research/r0822_hl_exec.py @@ -0,0 +1,567 @@ +"""HL-EXEC (2026-08-22) — AUDIT DI FATTO: le regole VERE di Hyperliquid contro le soglie ASSUNTE. + +DOMANDA. Il progetto tiene due edge cross-sectional fuori dal libro **per taglia**: + XS01 "serve ~$20.000" (origine: diario 2026-06-19-hyperliquid-xsec, "rumore arrotondamento" — + una STIMA A OCCHIO, mai calcolata) + XSR01 "diventa reale a ~$5.000" + gate pre-registrato 2026-10-23 con soglia + "haircut di eseguibilita' a $5.000 <= 40%, altrimenti RITIRO" +Entrambe sono calcolate col pavimento **min_order $5**, che e' il minimo di **DERIBIT**. +XS01/XSR01 si eseguirebbero su **HYPERLIQUID**, che ha regole sue. Qui le regole si LEGGONO dal +venue invece di assumerle, e si dice quali conclusioni del progetto cambiano. + +NON manda ordini, non tocca il conto: sole letture PUBBLICHE (nessuna chiave, nessuna firma). + +GRADO DELLE FONTI (dichiarato per ogni numero, come chiede il brief): + [A] letto dall'API pubblica del venue api.hyperliquid.xyz/info (meta, metaAndAssetCtxs, + userFees, l2Book) + [B] derivato da [A] + verificato empiricamente sui book live + [C] documentazione ufficiale (hyperliquid.gitbook.io) + [D] assunto / ereditato dal progetto +La verifica MAINNET non e' un atto di fede: i mark price dell'API si incrociano con l'ultima +chiusura del feed CERTIFICATO su disco (data/raw/hl_*_1d.parquet). Il testnet non puo' superarlo. +(Il progetto ha gia' pagato il prezzo di un feed testnet creduto vero: e' la causa del reset v2.0.0.) + + nice -n 19 timeout 900 uv run python scripts/research/r0822_hl_exec.py + ... --no-net usa solo la cache su disco (nessuna chiamata di rete) + ... --snaps N quanti snapshot del book L2 (default 3, ~45s di distanza) +""" +from __future__ import annotations + +import argparse +import json +import os +import sys +import time +import urllib.request +from pathlib import Path + +import numpy as np +import pandas as pd + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) +sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research")) + +RAW = PROJECT_ROOT / "data" / "raw" +CACHE = Path(os.environ.get("HLEXEC_CACHE", "/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/hlexec")) +INFO_URL = "https://api.hyperliquid.xyz/info" # MAINNET (il testnet e' api.hyperliquid-testnet.xyz) + +# --- parametri sotto esame ---------------------------------------------------------------------- +MIN_ORDER_ASSUNTO = 5.0 # [D] pavimento DERIBIT usato da eval_weights_smallcap e da paper_xsr +MIN_ORDER_HL = 10.0 # [C] "Order must have minimum value of $10." (docs/error-responses) +FEE_LEG_MODELLO = 0.0005 # [D] 0.05%/gamba, config CONGELATA di XSR01 +XSR_SOGLIA_HAIRCUT = 0.40 # [D] soglia pre-registrata del gate 2026-10-23 +CAPITALI = (600.0, 1000.0, 2000.0, 3000.0, 5000.0, 10000.0, 20000.0, 50000.0) +TICKETS = (50.0, 100.0, 300.0) +ANN = np.sqrt(365.0) + + +# ================================================================================================== +# 0. RETE — letture pubbliche, con cache su disco (una corsa non deve ri-martellare il venue) +# ================================================================================================== +def _post(payload: dict, timeout: int = 30) -> object: + req = urllib.request.Request(INFO_URL, data=json.dumps(payload).encode(), + headers={"Content-Type": "application/json"}) + with urllib.request.urlopen(req, timeout=timeout) as r: + return json.loads(r.read().decode()) + + +def cached(name: str, payload: dict, no_net: bool, ttl_s: float = 6 * 3600) -> object: + CACHE.mkdir(parents=True, exist_ok=True) + f = CACHE / f"{name}.json" + if f.exists() and (no_net or (time.time() - f.stat().st_mtime) < ttl_s): + return json.loads(f.read_text()) + if no_net: + raise FileNotFoundError(f"cache assente per {name} e --no-net attivo") + d = _post(payload) + f.write_text(json.dumps(d)) + return d + + +def universo_certificato() -> list[str]: + """I 51 alt+BTC gia' CERTIFICATI su disco. Non si inventa un universo: si legge quello vero.""" + return sorted(p.stem.replace("hl_", "").replace("_1d", "").upper() + for p in RAW.glob("hl_*_1d.parquet")) + + +# ================================================================================================== +# 1. REGOLE DEL VENUE +# ================================================================================================== +def sig_figs_ok(px_str: str, sz_dec: int, max_dec: int = 6) -> bool: + """[C] regola tick perp: <=5 cifre significative E <= (MAX_DECIMALS - szDecimals) decimali; + i prezzi INTERI sono sempre ammessi. Qui serve per VERIFICARLA sui book veri -> [B].""" + s = px_str.lstrip("-") + dec = len(s.split(".")[1]) if "." in s else 0 + if dec == 0: + return True + digits = s.replace(".", "").lstrip("0") + return len(digits.rstrip("0")) <= 5 and dec <= (max_dec - sz_dec) + + +def leggi_regole(no_net: bool) -> tuple[pd.DataFrame, dict]: + meta_ctx = cached("metaAndAssetCtxs", {"type": "metaAndAssetCtxs"}, no_net) + fees = cached("userFees", {"type": "userFees", + "user": "0x0000000000000000000000000000000000000001"}, no_net) + uni, ctxs = meta_ctx[0]["universe"], meta_ctx[1] + rows = [] + for a, c in zip(uni, ctxs): + mark = float(c["markPx"]) if c.get("markPx") else np.nan + rows.append(dict(sym=a["name"], szDecimals=int(a["szDecimals"]), + maxLeverage=int(a["maxLeverage"]), + delisted=bool(a.get("isDelisted", False)), + markPx=mark, dayNtlVlm=float(c.get("dayNtlVlm") or np.nan), + openInterest=float(c.get("openInterest") or np.nan))) + df = pd.DataFrame(rows).set_index("sym") + df["lot_units"] = 10.0 ** (-df["szDecimals"]) # [A] passo di size in unita' + df["lot_usd"] = df["lot_units"] * df["markPx"] # [B] passo di size in dollari + df["px_decimals_max"] = (6 - df["szDecimals"]).clip(lower=0) # [C] + return df, fees["feeSchedule"] + + +def verifica_mainnet(df: pd.DataFrame, syms: list[str]) -> pd.DataFrame: + """Incrocia i mark dell'API con l'ULTIMA chiusura del feed certificato su disco. + Un feed testnet non puo' superare questo controllo: i suoi prezzi sono fantasia.""" + out = [] + for s in syms: + p = RAW / f"hl_{s.lower()}_1d.parquet" + if not p.exists() or s not in df.index: + continue + d = pd.read_parquet(p, columns=["timestamp", "close"]) + last_close = float(d["close"].iloc[-1]) + last_ts = pd.Timestamp(int(d["timestamp"].iloc[-1]), unit="ms", tz="UTC") + mk = float(df.loc[s, "markPx"]) + out.append(dict(sym=s, disco=last_close, api=mk, + dev_pct=100.0 * (mk - last_close) / last_close, + eta_h=(pd.Timestamp.now("UTC") - last_ts) / pd.Timedelta("1h"))) + return pd.DataFrame(out).set_index("sym") + + +# ================================================================================================== +# 2. BOOK L2 -> spread, profondita', costo di un ticket +# ================================================================================================== +def scarica_book(syms: list[str], snaps: int, no_net: bool, pause: float = 0.25, + gap_s: float = 45.0) -> dict[str, list[dict]]: + books: dict[str, list[dict]] = {s: [] for s in syms} + for k in range(snaps): + if k and not no_net: + time.sleep(gap_s) + for s in syms: + try: + b = cached(f"l2_{s}_{k}", {"type": "l2Book", "coin": s}, no_net) + except Exception as e: # un book mancante e' un dato, non un crash + print(f" [book] {s} snap{k}: {type(e).__name__}") + continue + books[s].append(b) + if not no_net: + time.sleep(pause) # pacing: il rate limit e' PER-IP e condiviso + return books + + +def costo_ticket(levels: list[dict], mid: float, notional: float, side: str) -> tuple[float, bool]: + """VWAP di un ordine marketable da `notional` dollari contro il book, in bps DA MID. + Ritorna (bps, book_esaurito). Per ticket piccoli converge al MEZZO SPREAD: e' giusto cosi', + a $14 non si 'cammina' il book, si paga il livello top.""" + resid, cost, filled = notional, 0.0, 0.0 + for lv in levels: + px, sz = float(lv["px"]), float(lv["sz"]) + cap = px * sz + take = min(resid, cap) + cost += take + filled += take / px + resid -= take + if resid <= 1e-9: + break + if filled <= 0: + return float("nan"), True + vwap = cost / filled + bps = (vwap - mid) / mid * 1e4 * (1.0 if side == "buy" else -1.0) + return bps, resid > 1e-6 + + +def misura_liquidita(books: dict[str, list[dict]], meta: pd.DataFrame) -> pd.DataFrame: + rows = [] + for s, snaps in books.items(): + if not snaps: + continue + rec: dict[str, list[float]] = {"spread": [], "d10": [], "viol": []} + for t in TICKETS: + rec[f"t{int(t)}"] = [] + for b in snaps: + bids, asks = b["levels"][0], b["levels"][1] + if not bids or not asks: + continue + bb, ba = float(bids[0]["px"]), float(asks[0]["px"]) + mid = 0.5 * (bb + ba) + rec["spread"].append((ba - bb) / mid * 1e4) + near = sum(float(l["px"]) * float(l["sz"]) for l in asks + if (float(l["px"]) - mid) / mid <= 0.0010) + near += sum(float(l["px"]) * float(l["sz"]) for l in bids + if (mid - float(l["px"])) / mid <= 0.0010) + rec["d10"].append(near) + sd = int(meta.loc[s, "szDecimals"]) if s in meta.index else 0 + rec["viol"].append(sum(0 if sig_figs_ok(l["px"], sd) else 1 for l in bids + asks)) + for t in TICKETS: + bb_, _ = costo_ticket(asks, mid, t, "buy") + sb_, _ = costo_ticket(bids, mid, t, "sell") + if np.isfinite(bb_) and np.isfinite(sb_): + rec[f"t{int(t)}"].append(0.5 * (bb_ + sb_)) + if not rec["spread"]: + continue + row = dict(sym=s, n_snap=len(rec["spread"]), + spread_bps=float(np.median(rec["spread"])), + half_spread_bps=float(np.median(rec["spread"])) / 2.0, + depth10bps_usd=float(np.median(rec["d10"])), + tick_viol=int(sum(rec["viol"]))) + for t in TICKETS: + v = rec[f"t{int(t)}"] + row[f"slip{int(t)}_bps"] = float(np.median(v)) if v else np.nan + rows.append(row) + return pd.DataFrame(rows).set_index("sym").sort_values("spread_bps") + + +def min_notional_osservato(books: dict[str, list[dict]]) -> dict: + """Controllo EMPIRICO del pavimento $10: i livelli con n==1 sono UN ordine solo. + Un ordine singolo sotto $10 non falsifica la regola (i fill parziali erodono un resto), + ma il PAVIMENTO della distribuzione dice dove il venue taglia.""" + vals = [] + for snaps in books.values(): + for b in snaps: + for side in b["levels"]: + for l in side: + if int(l.get("n", 0)) == 1: + vals.append(float(l["px"]) * float(l["sz"])) + v = np.array(vals, float) + if not len(v): + return {} + return dict(n=len(v), minimo=float(v.min()), p01=float(np.percentile(v, 1)), + p05=float(np.percentile(v, 5)), mediana=float(np.median(v)), + sotto10_pct=100.0 * float((v < 10.0).mean()), + sotto5_pct=100.0 * float((v < 5.0).mean())) + + +# ================================================================================================== +# 3. SIMULATORE DI LIBRO A N GAMBE (la contabilita' e' quella di paper_xsr._step, generalizzata) +# ================================================================================================== +def sim_libro(Weff: np.ndarray, R: np.ndarray, r_hedge: np.ndarray | None, cap0: float, + min_order: float | None, lot_usd: np.ndarray | None, px: np.ndarray | None, + fee_leg: float | np.ndarray) -> dict: + """Un passo per barra. Una gamba il cui |dw|*capitale sta sotto min_order NON si muove + (stessa convenzione di altlib.eval_weights_smallcap e di scripts/live/paper_xsr._step). + In piu': la size si arrotonda al LOTTO del venue -> se arrotonda a zero, non si esegue. + `fee_leg` puo' essere uno scalare o un vettore per-gamba (costo asset-specifico).""" + n, k = Weff.shape + w = np.zeros(k) + cap = cap0 + eq = np.empty(n) + nets = np.zeros(n) + n_fill = n_skip = n_lot0 = 0 + fee = np.broadcast_to(np.asarray(fee_leg, float).ravel(), (k,)) if np.ndim(fee_leg) else None + for i in range(n): + ret = float(np.dot(w, R[i])) + if r_hedge is not None: + ret -= float(w.sum()) * r_hedge[i] + w_t = Weff[i] + if min_order is None: + w_new = w_t.copy() + else: + move = np.abs(w_t - w) * cap >= min_order + if lot_usd is not None and px is not None: + # arrotondamento al lotto: la size eseguita e' un multiplo di lot_units + dn = np.abs(w_t - w) * cap + lots = np.floor(dn / np.maximum(lot_usd, 1e-12)) + zero = move & (lots < 1) + n_lot0 += int(zero.sum()) + move = move & (lots >= 1) + w_new = np.where(move, w_t, w) + n_fill += int(move.sum()) + n_skip += int((~move).sum()) + d = np.abs(w_new - w) + c = float((d * fee).sum()) if fee is not None else float(fee_leg) * float(d.sum()) + if r_hedge is not None: # la gamba di copertura paga anch'essa + dh = abs(float(w_new.sum()) - float(w.sum())) + c += (float(fee.mean()) if fee is not None else float(fee_leg)) * dh + net = ret - c + nets[i] = net + cap *= (1.0 + max(net, -0.99)) + eq[i] = cap + w = w_new + tot = n_fill + n_skip + return dict(net=nets, eq=eq, n_fill=n_fill, n_skip=n_skip, n_lot0=n_lot0, + pct_eseguite=(100.0 * n_fill / tot) if tot else np.nan) + + +def metriche(net: np.ndarray, idx: pd.DatetimeIndex) -> dict: + x = np.asarray(net, float) + sd = x.std(ddof=1) + sh = float(x.mean() / sd * ANN) if sd > 0 else 0.0 + eq = np.cumprod(1.0 + np.clip(x, -0.99, None)) + dd = float((1.0 - eq / np.maximum.accumulate(eq)).max()) + yrs = max((idx[-1] - idx[0]) / pd.Timedelta("365.25D"), 1e-9) + cagr = float(eq[-1] ** (1.0 / yrs) - 1.0) + return dict(sharpe=round(sh, 3), maxdd=round(100 * dd, 1), cagr=round(100 * cagr, 1)) + + +# ================================================================================================== +# 4. XS01 — ricostruzione dei PESI (il sleeve espone solo i ritorni) + prova d'identita' +# ================================================================================================== +def xs01_pesi(): + from src.portfolio.sleeves import XS_CFG, XS_UNIVERSE, _xsec_returns + cols = {} + for sym in XS_UNIVERSE: + p = RAW / f"hl_{sym.lower()}_1d.parquet" + if not p.exists(): + continue + d = pd.read_parquet(p) + cols[sym] = pd.Series(d["close"].values.astype(float), + index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)) + C = pd.concat(cols, axis=1, join="inner").sort_index().dropna() + px = C.values + n, A = px.shape + lb, H, k, mode, tv = (XS_CFG["lookbacks"], XS_CFG["H"], XS_CFG["k"], + XS_CFG["mode"], XS_CFG["target_vol"]) + dp, mh = XS_CFG.get("disp_pct", 0), XS_CFG.get("disp_minhist", 20) + mlb = max(lb) + dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0]) + W = np.zeros((n, A)) + w = np.zeros(A) + hist: list[float] = [] + for i in range(n): + if i >= mlb and i % H == 0: + rLs = [px[i] / px[i - L] - 1.0 for L in lb] + di = float(np.mean([r.std() for r in rLs])) + thr = np.percentile(hist, dp) if (dp > 0 and len(hist) >= mh) else -np.inf + if di >= thr: + sc = np.zeros(A); cnt = 0 + for rL in rLs: + sd = rL.std() + if sd > 0: + sc += (rL - rL.mean()) / sd; cnt += 1 + if cnt: + sc /= cnt + o = np.argsort(sc); w = np.zeros(A); lo, hi = o[:k], o[-k:] + if mode == "mom": w[hi] = 0.5 / k; w[lo] = -0.5 / k + else: w[lo] = 0.5 / k; w[hi] = -0.5 / k + else: + w = np.zeros(A) + hist.append(di) + W[i] = w + gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1) + turn = np.zeros(n); turn[0] = np.abs(W[0]).sum() + turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1) + net = gross - turn * (0.001 / 2.0) + s = pd.Series(net, index=C.index) + rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25) + scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0) + # prova d'identita' col sleeve UFFICIALE (max|diff| deve essere 0.0) + off = _xsec_returns() + dif = float(np.abs(pd.Series(s.values * scale, index=C.index).reindex(off.index).values + - off.values).max()) + # posizione TENUTA durante la barra i = W[i-1]*scale[i] => posta a fine barra i: W[i]*scale[i+1] + pos_scale = np.concatenate([scale[1:], scale[-1:]]) + return W * pos_scale[:, None], dret, C, list(C.columns), dif + + +# ================================================================================================== +# MAIN +# ================================================================================================== +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--no-net", action="store_true") + ap.add_argument("--snaps", type=int, default=3) + a = ap.parse_args() + pd.set_option("display.width", 200) + + print("=" * 104) + print(" HL-EXEC — le regole VERE di Hyperliquid contro le soglie ASSUNTE di XS01 / XSR01") + print("=" * 104) + + # ---------------------------------------------------------------- 1. regole + meta, fee_sched = leggi_regole(a.no_net) + syms = universo_certificato() + print(f"\n[1] REGOLE DEL VENUE (fonte: {INFO_URL} = MAINNET)") + print(f" universo perp quotato: {len(meta)} strumenti; certificati da noi: {len(syms)}") + mancanti = [s for s in syms if s not in meta.index] + delist = [s for s in syms if s in meta.index and bool(meta.loc[s, "delisted"])] + print(f" certificati assenti dal venue: {mancanti or 'nessuno'} delistati: {delist or 'nessuno'}") + + ck = verifica_mainnet(meta, syms) + print(f"\n [A] verifica MAINNET (mark API vs ultima chiusura del feed certificato, " + f"eta' feed {ck['eta_h'].median():.0f}h):") + print(f" deviazione |%| mediana {ck['dev_pct'].abs().median():.2f}% " + f"max {ck['dev_pct'].abs().max():.2f}% ({ck['dev_pct'].abs().idxmax()}) " + f"asset controllati {len(ck)}") + print(" -> un feed TESTNET non supera questo controllo: i suoi prezzi sono scollegati.") + + tk = fee_sched + print(f"\n [A] FEE perp lette dal venue (tier BASE, nessuna chiave):") + print(f" taker (cross) {float(tk['cross'])*1e4:.2f} bps/lato " + f"maker (add) {float(tk['add'])*1e4:.2f} bps/lato") + print(f" primo scaglione VIP a ${float(tk['tiers']['vip'][0]['ntlCutoff']):,.0f} di " + f"volume 14g -> a $600-20k si sta SEMPRE al tier base.") + print(f" [C] la doc (gitbook/trading/fees) dichiara 0.045% / 0.015%: **le due fonti " + f"coincidono**.") + print(f" [C] pavimento d'ordine perp: \"Order must have minimum value of $10.\" " + f"(docs/for-developers/api/error-responses); nessuna esenzione documentata per reduce-only.") + print(f" [C] tick: <=5 cifre significative E <= (6 - szDecimals) decimali; size arrotondata " + f"a szDecimals.") + + sub = meta.loc[[s for s in syms if s in meta.index]].copy() + print(f"\n [A/B] LOTTO per asset (passo di size) — i 6 piu' grossolani e i 6 piu' fini:") + o = sub.sort_values("lot_usd", ascending=False) + for who, part in (("piu' grossolano", o.head(6)), ("piu' fine", o.tail(6))): + for s, r in part.iterrows(): + print(f" {s:<6} szDec={int(r.szDecimals)} lotto {r.lot_units:>10.5f} unita' = " + f"${r.lot_usd:>8.4f} mark ${r.markPx:<12.6g} ({who})") + print(f" lotto in $: mediana ${sub['lot_usd'].median():.4f}, max " + f"${sub['lot_usd'].max():.2f} ({sub['lot_usd'].idxmax()})") + print(f" -> il LOTTO non e' mai il vincolo: max ${sub['lot_usd'].max():.2f} << $10 " + f"di pavimento. Il vincolo e' il MIN NOTIONAL.") + print(f" [A] leva massima: BTC {int(meta.loc['BTC','maxLeverage'])}x, " + f"ETH {int(meta.loc['ETH','maxLeverage'])}x, mediana alt certificati " + f"{int(sub['maxLeverage'].median())}x, minimo {int(sub['maxLeverage'].min())}x") + + # ---------------------------------------------------------------- 2. book + print(f"\n[2] BOOK L2 — spread, profondita', costo di un ticket ({a.snaps} snapshot per asset)") + books = scarica_book(syms, a.snaps, a.no_net) + liq = misura_liquidita(books, meta) + mn = min_notional_osservato(books) + if mn: + print(f" [B] controllo empirico del pavimento $10 su {mn['n']} livelli a UN SOLO ordine " + f"(n==1):") + print(f" minimo ${mn['minimo']:.2f} p01 ${mn['p01']:.2f} p05 ${mn['p05']:.2f} " + f"mediana ${mn['mediana']:.0f} sotto $10: {mn['sotto10_pct']:.1f}% " + f"sotto $5: {mn['sotto5_pct']:.1f}%") + print(f" (un residuo sotto soglia NON falsifica la regola — i fill parziali erodono " + f"un ordine gia' piazzato — ma il pavimento della distribuzione dice dove taglia.)") + viol = int(liq["tick_viol"].sum()) + print(f" [B] regola tick verificata sui book veri: {viol} violazioni su " + f"{int(liq['n_snap'].sum())*40} livelli letti -> la regola [C] e' CONFERMATA dal venue.") + + print(f"\n spread e slippage (mediana degli snapshot; bps DA MID, un solo lato):") + print(f" {'sym':<7}{'vol24h $':>13}{'spread':>9}{'1/2 spr':>9}" + f"{'prof<10bp':>11}{'$50':>8}{'$100':>8}{'$300':>8}") + for s, r in liq.iterrows(): + print(f" {s:<7}{r.dayNtlVlm if 'dayNtlVlm' in r else meta.loc[s,'dayNtlVlm']:>13,.0f}" + f"{r.spread_bps:>8.1f}{r.half_spread_bps:>9.1f}{r.depth10bps_usd:>11,.0f}" + f"{r.slip50_bps:>8.1f}{r.slip100_bps:>8.1f}{r.slip300_bps:>8.1f}") + + XS19 = ["BTC", "ETH", "SOL", "BNB", "XRP", "DOGE", "AVAX", "LINK", "LTC", "ADA", + "ARB", "OP", "SUI", "APT", "INJ", "TIA", "SEI", "NEAR", "AAVE"] + maj = liq.reindex([s for s in XS19 if s in liq.index]) + coda = liq.drop(index=maj.index, errors="ignore") + print(f"\n riepilogo (mediana | p90):") + for nm, part in (("19 major (XS01)", maj), (f"coda ({len(coda)} alt, solo XSR01)", coda)): + if len(part): + print(f" {nm:<26} spread {part.spread_bps.median():>5.1f} | " + f"{part.spread_bps.quantile(.9):>5.1f} bps " + f"slip$100 {part.slip100_bps.median():>5.1f} | " + f"{part.slip100_bps.quantile(.9):>5.1f} " + f"slip$300 {part.slip300_bps.median():>5.1f} | " + f"{part.slip300_bps.quantile(.9):>5.1f}") + + # ---------------------------------------------------------------- 3. XS01 + print(f"\n[3] XS01 — soglia PUBBLICATA ~$20.000 (origine: stima a occhio, 'rumore arrotondamento')") + Wxs, dret, C, cols, dif = xs01_pesi() + print(f" prova d'identita' col sleeve ufficiale `_xsec_returns()`: max|diff| = {dif:.3e}") + gross_xs = np.abs(Wxs).sum(axis=1) + att = gross_xs > 1e-9 + dW = np.abs(np.diff(Wxs, axis=0, prepend=np.zeros((1, Wxs.shape[1])))) + tk_nz = dW[dW > 1e-9] + print(f" gambe simultanee: {int((np.abs(Wxs) > 1e-9).sum(axis=1)[att].max())} " + f"(k=5 long + 5 short); lordo mediano {np.median(gross_xs[att]):.2f}x il capitale " + f"(vol-target, cap 3x)") + print(f" peso mediano mosso per gamba per ribilanciamento: {np.median(tk_nz)*100:.2f}% " + f"del capitale; 10° pctl {np.percentile(tk_nz,10)*100:.2f}%") + print(f"\n ticket per gamba (mediano) e % di gambe ESEGUIBILI, per capitale allocato a XS01:") + print(f" {'cap XS01':>10}{'ticket med':>12}{'ticket p10':>12}" + f"{'>=$5 [D]':>10}{'>=$10 [C]':>11}{'lotto ko':>10}") + R_xs = dret + idx = C.index + base = None + for cap in CAPITALI: + med = np.median(tk_nz) * cap + p10 = np.percentile(tk_nz, 10) * cap + pct5 = 100.0 * float((tk_nz * cap >= MIN_ORDER_ASSUNTO).mean()) + pct10 = 100.0 * float((tk_nz * cap >= MIN_ORDER_HL).mean()) + lot = np.array([meta.loc[s, "lot_usd"] if s in meta.index else 0.0 for s in cols]) + lotko = 100.0 * float((tk_nz * cap < np.median(lot)).mean()) + print(f" {cap:>10,.0f}{med:>12.2f}{p10:>12.2f}{pct5:>9.0f}%{pct10:>10.0f}%{lotko:>9.1f}%") + lot_xs = np.array([float(meta.loc[s, "lot_usd"]) if s in meta.index else 0.0 for s in cols]) + px_xs = np.array([float(meta.loc[s, "markPx"]) if s in meta.index else np.nan for s in cols]) + fee_xs_mod = 0.001 / 2.0 # [D] cio' che il sleeve modella: 5 bps/lato + print(f"\n haircut di eseguibilita' (Sharpe modellato - Sharpe realistico), fee {fee_xs_mod*1e4:.1f} bps/lato:") + print(f" {'cap XS01':>10}{'Sh mod':>9}{'Sh $5 [D]':>11}{'Sh $10 [C]':>12}" + f"{'haircut $10':>13}{'esegui%':>9}") + for cap in CAPITALI: + m0 = sim_libro(Wxs, R_xs, None, cap, None, None, None, fee_xs_mod) + m5 = sim_libro(Wxs, R_xs, None, cap, MIN_ORDER_ASSUNTO, None, None, fee_xs_mod) + m10 = sim_libro(Wxs, R_xs, None, cap, MIN_ORDER_HL, lot_xs, px_xs, fee_xs_mod) + a0, a5, a10 = (metriche(m["net"], idx) for m in (m0, m5, m10)) + hc = (a0["sharpe"] - a10["sharpe"]) / abs(a0["sharpe"]) * 100 if a0["sharpe"] else np.nan + print(f" {cap:>10,.0f}{a0['sharpe']:>9.2f}{a5['sharpe']:>11.2f}{a10['sharpe']:>12.2f}" + f"{hc:>12.0f}%{m10['pct_eseguite']:>9.0f}%") + + # ---------------------------------------------------------------- 4. XSR01 + print(f"\n[4] XSR01 — soglia PUBBLICATA ~$5.000, gate 2026-10-23 con haircut a $5.000 <= 40%") + from scripts.live.paper_xsr import build_panel # stessa costruzione del monitor in produzione + ts, dtx, Wx, Rx, rbx, sx = build_panel() + idxx = pd.DatetimeIndex(dtx) + lot_x = np.array([float(meta.loc[s, "lot_usd"]) if s in meta.index else 0.0 for s in sx]) + px_x = np.array([float(meta.loc[s, "markPx"]) if s in meta.index else np.nan for s in sx]) + dWx = np.abs(np.diff(Wx, axis=0, prepend=np.zeros((1, Wx.shape[1])))) + tkx = dWx[dWx > 1e-9] + print(f" gambe {len(sx)} lordo mediano {np.median(np.abs(Wx).sum(axis=1)):.2f}x " + f"peso mosso mediano/gamba {np.median(tkx)*100:.3f}% p10 {np.percentile(tkx,10)*100:.3f}%") + print(f"\n {'cap':>9}{'ticket med':>12}{'Sh mod':>9}{'Sh $5 [D]':>11}{'Sh $10 [C]':>12}" + f"{'haircut $5':>12}{'haircut $10':>13}{'esegui%':>9}") + hc10_5000 = np.nan + for cap in CAPITALI: + m0 = sim_libro(Wx, Rx, rbx, cap, None, None, None, FEE_LEG_MODELLO) + m5 = sim_libro(Wx, Rx, rbx, cap, MIN_ORDER_ASSUNTO, None, None, FEE_LEG_MODELLO) + m10 = sim_libro(Wx, Rx, rbx, cap, MIN_ORDER_HL, lot_x, px_x, FEE_LEG_MODELLO) + a0, a5, a10 = (metriche(m["net"], idxx) for m in (m0, m5, m10)) + h5 = (a0["sharpe"] - a5["sharpe"]) / abs(a0["sharpe"]) * 100 if a0["sharpe"] else np.nan + h10 = (a0["sharpe"] - a10["sharpe"]) / abs(a0["sharpe"]) * 100 if a0["sharpe"] else np.nan + if abs(cap - 5000.0) < 1e-9: + hc10_5000 = h10 + print(f" {cap:>9,.0f}{np.median(tkx)*cap:>12.2f}{a0['sharpe']:>9.2f}" + f"{a5['sharpe']:>11.2f}{a10['sharpe']:>12.2f}{h5:>11.0f}%{h10:>12.0f}%" + f"{m10['pct_eseguite']:>9.0f}%") + print(f" -> haircut a $5.000 col pavimento VERO ($10): {hc10_5000:.0f}% " + f"(soglia pre-registrata {XSR_SOGLIA_HAIRCUT*100:.0f}%)") + print(f" NB: e' la lettura del PARAMETRO sul backtest, NON il gate del 23/10 — " + f"quello si decide sulla finestra FORWARD e anticiparlo sarebbe selezione.") + + # -------- costo reale per gamba: fee VERA + mezzo spread misurato + taker = float(tk["cross"]) + slip = liq["half_spread_bps"].reindex(sx).astype(float) / 1e4 + slip_med = float(np.nanmedian(slip.values)) + fee_real = taker + np.nan_to_num(slip.values, nan=slip_med) + print(f"\n [B] COSTO REALE PER GAMBA = taker {taker*1e4:.2f} bps + mezzo spread misurato:") + print(f" mediana {np.median(fee_real)*1e4:.1f} bps p90 {np.percentile(fee_real,90)*1e4:.1f} bps" + f" max {fee_real.max()*1e4:.1f} bps ({sx[int(np.argmax(fee_real))]})") + print(f" contro i {FEE_LEG_MODELLO*1e4:.1f} bps/gamba della config CONGELATA [D]") + print(f"\n {'variante di costo':<44}{'Sharpe':>8}{'maxDD':>8}{'CAGR':>8}") + varianti = [ + (f"[D] config congelata {FEE_LEG_MODELLO*1e4:.1f} bps piatti", FEE_LEG_MODELLO), + (f"[A] solo taker vero {taker*1e4:.2f} bps piatti", taker), + (f"[B] taker + 1/2 spread PER-ASSET (mediana {np.median(fee_real)*1e4:.1f})", fee_real), + (f"[B] taker + spread PIENO per-asset (pessimista)", taker + 2 * np.nan_to_num(slip.values, nan=slip_med)), + ] + for nm, f in varianti: + m = sim_libro(Wx, Rx, rbx, 5000.0, None, None, None, f) + a_ = metriche(m["net"], idxx) + print(f" {nm:<44}{a_['sharpe']:>8.2f}{a_['maxdd']:>7.1f}%{a_['cagr']:>7.1f}%") + print(f" (a capitale infinito = nessun vincolo di min-order: isola il COSTO dal PAVIMENTO)") + + print("\n" + "=" * 104) + print(" LIMITI DICHIARATI: gli spread sono UNO/POCHI snapshot di OGGI su un venue la cui") + print(" liquidita' e' cresciuta -> sono la stima piu' FAVOREVOLE per i 2.6 anni di backtest.") + print(" Il pavimento $10 e' [C] (doc + messaggio d'errore), non provato da un ordine: provarlo") + print(" richiederebbe di mandarne uno, e questo audit non tocca il conto.") + print("=" * 104) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_oi_pin.py b/scripts/research/r0822_oi_pin.py index 7b46f3c..5744575 100644 --- a/scripts/research/r0822_oi_pin.py +++ b/scripts/research/r0822_oi_pin.py @@ -460,6 +460,7 @@ def main() -> None: P(f" {'hte':>4} {'rule':>5} {'Δ vs adj_r':>12} {'Δ vs jit_r':>12}" f" {'Δ vs rand':>12} {'Δ vs mid':>12} {'Δ vs mr7':>12}") npos = ntot = 0 + npos_c = ntot_c = 0 # solo i null PULITI (bilanciati nel segno): rand/mid/mr7 for hte in HTE_GRID: e = ev[ev["hte"] == hte] for rl in RULES: @@ -467,12 +468,25 @@ def main() -> None: for p in (f"adj_{rl}", f"jit_{rl}") + PLACEBOS_GLOB: dd = e[f"tw_{rl}"].to_numpy(float) - e[f"tw_{p}"].to_numpy(float) m, lo, hi, _ = cluster_boot(dd, e["exp"].to_numpy()) - cells.append(f"{m:>11.5f}{'*' if (np.isfinite(lo) and lo > 0) else ' '}") - ntot += 1 - npos += int(np.isfinite(lo) and lo > 0) + sig = bool(np.isfinite(lo) and lo > 0) + cells.append(f"{m:>11.5f}{'*' if sig else ' '}") + ntot += 1; npos += int(sig) + if p in PLACEBOS_GLOB: + ntot_c += 1; npos_c += int(sig) P(f" {hte:>4} {rl:>5} " + " ".join(cells)) P(f" celle (rule x hte x placebo) in cui lo SPECIALE batte il placebo con IC95>0:" f" {npos}/{ntot} (atteso per caso ~{0.025 * ntot:.0f})") + P(f" ... contando SOLO i null bilanciati nel segno (rand/mid/mr7): {npos_c}/{ntot_c}" + f" (atteso ~{0.025 * ntot_c:.0f}) <-- e' QUESTO il numero da citare") + P("\n ⚠ UN PLACEBO SI CONTROLLA PRIMA DI USARLO: se il suo segno non e' bilanciato rispetto") + P(" alla regola, il Δ non misura l'informazione, misura il ROVESCIAMENTO del segnale.") + P(f" quota di eventi con sign(K_placebo-S) == sign(K_regola-S), su tutti gli hte:") + for rl in RULES: + agr = {p: float(np.nanmean(np.sign(ev[f"d_{rl}"]) == np.sign(ev[f"d_{p}"]))) + for p in (f"adj_{rl}", f"jit_{rl}") + PLACEBOS_GLOB} + P(" " + rl + ": " + " ".join(f"{k}={v:.2f}" for k, v in agr.items())) + P(" un null PULITO sta vicino a 0.50 (rand/mid/mr7). Sotto ~0.35 il placebo e' la") + P(" strategia INVERTITA e il suo Δ va letto come ~2x il segnale, non come un test.") P(" LETTURA: `adj_r`/`jit_r` chiedono 'e' proprio QUELLO strike?'; `mid`/`mr7` chiedono") P(" 'serve l'open interest, o basta il centro recente del prezzo?'. La seconda e' la") P(" domanda che uccide o salva il filone.") @@ -554,7 +568,9 @@ def main() -> None: f" · WR-knob atteso 1/(1+RR) = {1 / (1 + rr):.1%}" if np.isfinite(rr) else " RR n.d.") P(f" expectancy NETTA per trade: {np.mean(net):+.5f} ({np.mean(net) * 100:+.3f}%)" f" | in R (R = distanza dal livello): {np.mean(net / np.maximum(dk, 1e-6)):+.3f} R") - P(f" a fee ZERO: {np.mean(tw):+.5f} -> {'l edge lordo non esiste' if np.mean(tw) <= 0 else 'edge lordo positivo, la fee lo mangia?'}") + P(f" a fee ZERO: {np.mean(tw):+.5f} · la fee costa {2 * FEE_SIDE_REAL:.5f}/trade =" + f" {200 * FEE_SIDE_REAL / max(np.mean(tw), 1e-9):.0f}% del lordo" + f" -> {'MORTE PER FEE' if np.mean(tw) <= 2 * FEE_SIDE_REAL else 'la fee NON e il vincolo'}") dbest, info = strat_daily(ev, best["rule"], int(best["hte"]), None if best["band"] == "nessuna" else (0.002, 0.03), @@ -652,6 +668,10 @@ def main() -> None: def _fn(rule): return lambda h: strat_daily(ev, rule, int(h), (0.002, 0.03), "sign", "all", FEE_SIDE_REAL)[0] + P(" ⚠ le 8 ancore hte NON sono 8 osservazioni indipendenti: guardano le STESSE 73 scadenze") + P(" e lo STESSO percorso di prezzo. '8/8 positive' dice che il segno non dipende") + P(" dall'ancora, NON che l'effetto sia significativo — quello lo dice il cluster-bootstrap") + P(" della sezione [4], che sui placebo senza opzioni (mid/mr7) NON e' significativo.") for other in ("mr7", "mid", "oi", "rand"): dl = A.anchor_luck_delta(_fn("mp"), _fn(other), offsets=list(HTE_GRID)) P(f" Δ appaiato mp-{other:<4}: mediana {dl.get('median_paired'):+.2f}" @@ -676,7 +696,10 @@ def main() -> None: P("\n[8] SINTESI") n_fact_pos = int((F.lo > 0).sum()) P(f" fatto : {n_fact_pos}/{len(F)} celle (rule x hte) con pinning significativo a IC95") - P(f" null loc. : {npos}/{ntot} confronti appaiati in cui il livello 'speciale' batte il placebo") + P(f" null loc. : {npos}/{ntot} confronti appaiati vinti dal livello 'speciale';" + f" {npos_c}/{ntot_c} contando solo i null PULITI") + P(f" di questi, contro il placebo SENZA OPZIONI (media 7g dello spot): " + f"{sum(1 for h in HTE_GRID for rl in RULES if (lambda t: np.isfinite(t[1]) and t[1] > 0)(cluster_boot((ev[ev.hte == h][f'tw_{rl}'] - ev[ev.hte == h]['tw_mr7']).to_numpy(float), ev[ev.hte == h]['exp'].to_numpy())))}/24") P(f" strategia : miglior Sharpe della griglia {mb['sharpe']:+.2f} su {mb['n']} giorni, DSR {dsr:.3f}") P(f" marginale : {mv.get('marginal_verdict')} (strutturalmente non ADDS-abile a 3,7 mesi)") P("=" * 100) diff --git a/scripts/research/r0822_prop_alloc.py b/scripts/research/r0822_prop_alloc.py index b6eb00e..9cb7849 100644 --- a/scripts/research/r0822_prop_alloc.py +++ b/scripts/research/r0822_prop_alloc.py @@ -205,11 +205,15 @@ def _panel_index(start: str) -> pd.DatetimeIndex: _RG_CACHE: dict = {} -def book_RG(w: dict, start: str = START_WIN) -> tuple[np.ndarray, np.ndarray]: +def book_RG(w: dict, start: str = START_WIN, + drift_mult: tuple = ()) -> tuple[np.ndarray, np.ndarray]: """(R, gap) giornalieri di un libro a pesi arbitrari sui 5 sleeve. Crypto TP01+SKH01: minimo ESATTO sul path orario condiviso (r0725_prop_coupled). - XS01/VRP01/GTAA01: il loro minimo si SOMMA (worst simultaneo — convenzione del 25/07).""" - key = (start,) + tuple(round(float(w.get(nm, 0.0)), 6) for nm in SLEEVES) + XS01/VRP01/GTAA01: il loro minimo si SOMMA (worst simultaneo — convenzione del 25/07). + `drift_mult` = (("XS01", 0.5), ...): taglia il DRIFT di quello sleeve (non la vol) — serve + per lo stress giudiziale su uno sleeve scoperto DENTRO la finestra di misura.""" + dm = dict(drift_mult) + key = (start, drift_mult) + tuple(round(float(w.get(nm, 0.0)), 6) for nm in SLEEVES) if key in _RG_CACHE: return _RG_CACHE[key] idx = _panel_index(start) @@ -218,15 +222,25 @@ def book_RG(w: dict, start: str = START_WIN) -> tuple[np.ndarray, np.ndarray]: M = np.zeros(len(idx)) if w_tp > 0 or w_skh > 0: c = _crypto(w_tp, w_skh).reindex(idx) - R += c["R"].values + cr = c["R"].values + if w_tp > 0 and dm.get("TP01", 1.0) != 1.0 or w_skh > 0 and dm.get("SKH01", 1.0) != 1.0: + raise NotImplementedError("stress sul drift crypto: il minimo e' path-esatto, non separabile") + R += cr M += c["m"].values for nm, fr in (("XS01", pc.xsec_daily_tuples()), ("VRP01", vrp_daily_tuples()), ("GTAA01", gtaa_daily_tuples())): wt = w.get(nm, 0.0) if wt > 0: f = fr.reindex(idx) - R += wt * f["R"].values - M += wt * f["m"].values + rv = np.nan_to_num(f["R"].values) + mv = np.nan_to_num(f["m"].values) + mult = dm.get(nm, 1.0) + if mult != 1.0: # taglia il drift, lascia la forma della coda + cut = (1.0 - mult) * rv.mean() # NB: media PRIMA del taglio + rv = rv - cut + mv = mv - cut + R += wt * rv + M += wt * mv R = np.nan_to_num(R) M = np.nan_to_num(M) _RG_CACHE[key] = (R, np.minimum(M, R) - R) @@ -299,21 +313,34 @@ def funded_sim(R: np.ndarray, G: np.ndarray, idx: np.ndarray, lev: float, firm: if (t + 1) % 365 == 0: alive_at[(t + 1) // 365] = float(alive.mean()) return dict(p_alive=float(alive.mean()), alive_at=alive_at, - e_payout=float(payout.mean()), payout=payout) + e_payout=float(payout.mean()), payout=payout, alive=alive) def objective(w: dict, lev: float, firm: str, ev_idx: np.ndarray, fu_idx: np.ndarray, lens: str = "coupled", factor: float = DELUCK, - notional: float = 100_000.0) -> dict: - R, G = book_RG(w) + notional: float = 100_000.0, drift_mult: tuple = ()) -> dict: + R, G = book_RG(w, drift_mult=drift_mult) R = deluck(R, factor) e = eval_sim(R, G, ev_idx, lev, firm, lens) f = funded_sim(R, G, fu_idx, lev, firm, notional, lens) ann = float(R.mean() * 365.0) vol = float(R.std() * np.sqrt(365.0)) + # stimatore per-percorso di J: eval e funded girano su indici di bootstrap INDIPENDENTI, + # quindi mean(passed_i * alive_i) stima p_pass*p_alive ed e' APPAIABILE fra configurazioni. + jvec = (e["passed"] & f["alive"]).astype(float) return dict(J=e["p_pass"] * f["p_alive"], p_pass=e["p_pass"], p_alive=f["p_alive"], e_payout=f["e_payout"], sharpe=(ann / vol if vol > 0 else 0.0), - vol=vol, drift=ann, t_pass=e["t_pass"]) + vol=vol, drift=ann, t_pass=e["t_pass"], jvec=jvec) + + +def paired_delta(wA: dict, levA: float, wB: dict, levB: float, firm: str, + ev_idx: np.ndarray, fu_idx: np.ndarray) -> tuple[float, float]: + """Differenza APPAIATA di J fra due configurazioni (stessi indici di bootstrap) + errore + standard della differenza. Il livello di J ha SE ~0.01; la DIFFERENZA molto meno.""" + a = objective(wA, levA, firm, ev_idx, fu_idx)["jvec"] + b = objective(wB, levB, firm, ev_idx, fu_idx)["jvec"] + d = a - b + return float(d.mean()), float(d.std(ddof=1) / np.sqrt(len(d))) # ================================================================== §2. IL CONTO ANALITICO @@ -358,7 +385,8 @@ def grid_scan(grid: list[dict], levs: tuple, firm: str, ev_idx: np.ndarray, fu_i for w in grid: for lev in levs: o = objective(w, lev, firm, ev_idx, fu_idx, lens, factor) - rows.append(dict(w=wkey(w), lev=lev, **{k: v for k, v in o.items()})) + rows.append(dict(w=wkey(w), lev=lev, + **{k: v for k, v in o.items() if k != "jvec"})) return pd.DataFrame(rows) diff --git a/scripts/research/r0822_skew.py b/scripts/research/r0822_skew.py index 4885d84..d665376 100644 --- a/scripts/research/r0822_skew.py +++ b/scripts/research/r0822_skew.py @@ -422,6 +422,78 @@ def vrp_decomposizione(q: pd.DataFrame, cm7: dict) -> pd.DataFrame: return pd.DataFrame(righe) +MARKFIT_OK = (0.85, 1.15) # banda del CONTROLLO, dichiarata prima di guardare i f + + +def qualita(D: pd.DataFrame) -> pd.Series: + """Filtro dichiarato: si tiene un'osservazione solo se il CONTROLLO passa, cioe' se la IV di + Deribit riprezza i suoi stessi mid entro +-15% sul credito, e se il credito conservativo e' + positivo. NON e' un filtro sull'esito (il f), e' un filtro sulla variabile di controllo: + dove il controllo fallisce non si sta misurando lo skew, si sta misurando una quota rotta.""" + return (D["f_markfit"].between(*MARKFIT_OK)) & (D["f_spread"] > 0) & D["f_term"].gt(0) + + +def attribuzione_log(D: pd.DataFrame) -> pd.DataFrame: + """Attribuzione ESATTA e additiva: log(f_tot) = log(f_term)+log(f_skew)+log(f_markfit)+ + log(f_spread), per costruzione (la catena dei rapporti telescopia). Le MEDIANE dei fattori + non si moltiplicano fra loro; i log si sommano. Da qui la QUOTA del difetto per causa.""" + g = D[qualita(D)] + parti = ["f_term", "f_skew", "f_markfit", "f_spread"] + L = np.log(g[parti]) + tot = float(np.log(g["f_tot"]).mean()) + rows = [] + for c in parti: + m = float(L[c].mean()) + rows.append(dict(fattore=c, media_log=round(m, 4), + fattore_geometrico=round(float(np.exp(m)), 3), + quota_del_difetto=round(m / tot, 3) if tot != 0 else np.nan)) + rows.append(dict(fattore="TOTALE", media_log=round(tot, 4), + fattore_geometrico=round(float(np.exp(tot)), 3), quota_del_difetto=1.0)) + return pd.DataFrame(rows) + + +def panel_f(q: pd.DataFrame, cm7: dict) -> pd.DataFrame: + """La stessa decomposizione, ma a OGNI ora in cui lo spread e' costruibile (non solo ai 22 + ingressi settimanali di VRP01). Serve alla RISOLUZIONE: 22 punti non separano due cause da + ~10% l'una. ⚠ Le ore dentro la stessa scadenza NON sono indipendenti: il campione efficace + resta il numero di SCADENZE, che si riporta accanto.""" + out = [] + for a in ASSETS: + S7 = cm7[a] + S, V = CB.spot_series(a), CB.dvol_series(a) + p = q[(q["asset"] == a) & (q["option_type"] == "P") + & (q["dte"] >= 4.0) & (q["dte"] <= 10.0)] + for (hr, exp), g in p.groupby(["hr", "exp"], sort=False): + if hr not in S7.index: + continue + legs = CB.pick_legs(g) + if legs is None: + continue + ts = g["ts"].max() + dte = float((exp - ts).total_seconds() / 86400.0) + if not (4.0 <= dte <= 10.0): + continue + spot = float(S.asof(ts)); dvol = float(V.asof(ts)) / 100.0 + atm7 = float(S7.loc[hr, "atm"]) / 100.0 + T = dte / 365.25 + c_mod = CB.bs_put(spot, legs["k_short"], T, dvol) - CB.bs_put(spot, legs["k_long"], T, dvol) + c_atm = CB.bs_put(spot, legs["k_short"], T, atm7) - CB.bs_put(spot, legs["k_long"], T, atm7) + c_sml = (CB.bs_put(spot, legs["k_short"], T, legs["iv_short"] / 100.0) + - CB.bs_put(spot, legs["k_long"], T, legs["iv_long"] / 100.0)) + c_mid = (legs["mid_short"] - legs["mid_long"]) * spot + c_real = (legs["bid_short"] - legs["ask_long"]) * spot + if not (c_mod > 0 and c_atm > 0 and c_sml > 0): + continue + hist = V[V.index < ts] + ivr = float((hist < dvol * 100).mean()) if len(hist) else np.nan + out.append(dict(asset=a, hr=hr, exp=exp, dte=dte, rr7=float(S7.loc[hr, "rr"]), + atm7=float(S7.loc[hr, "atm"]), dvol=dvol * 100, ivrank=ivr, + f_term=c_atm / c_mod, f_skew=c_sml / c_atm, + f_markfit=c_mid / c_sml, f_spread=c_real / c_mid, + f_tot=c_real / c_mod, f_tot_mid=c_mid / c_mod)) + return pd.DataFrame(out) + + # ============================================================================= def main() -> None: t_start = time.time() @@ -469,6 +541,14 @@ def main() -> None: print(f"\n {a} — corr fra dRR (variazione oraria dello skew) e il rendimento nella") print(" finestra indicata, ancorata al TIMESTAMP VERO delle quote (ts_max):") print(LL[LL["asset"] == a].to_string(index=False)) + fw = LL[LL["minuti"] > 0] + print(f"\n LAG IN AVANTI: {len(fw)} test ({fw['minuti'].nunique()} orizzonti x {len(ASSETS)} asset).") + print(f" |t| massimo osservato = {fw['t_dRR'].abs().max():.2f} su {len(fw)} test;") + print(f" |t| atteso come MASSIMO di {len(fw)} test indipendenti sotto il nullo ~ " + f"{float(np.abs(np.random.default_rng(0).standard_normal((4000, len(fw)))).max(axis=1).mean()):.2f}.") + print(" -> il massimo in avanti e' quello che il rumore produce da solo. LAG INDIETRO:") + print(f" |t| minimo = {LL[LL['minuti'] < 0]['t_dRR'].abs().min():.2f} su " + f"{int((LL['minuti'] < 0).sum())} test, tutti dello STESSO segno.") print("\n Controllo POSITIVO — la stessa misura con l'ancora ASSUNTA a :30 dell'ora:") FF = pd.concat([leadlag_ancora_sbagliata(cm30[a], a) for a in ASSETS], ignore_index=True) print(FF.to_string(index=False)) @@ -550,8 +630,13 @@ def main() -> None: z_all[a] = pd.Series(z.to_numpy(), index=pd.DatetimeIndex(pd.to_datetime(sub["datetime"], utc=True))) Z = pd.concat(z_all, axis=1).mean(axis=1) Z = Z.reindex(Bw.index).ffill() + att = int((Bw != 0).sum()) print(f" baseline TP01 sulla finestra dello skew: n={len(Bw)} giorni, " f"Sharpe {sh_of(Bw.to_numpy()):+.2f}, maxDD {dd_of(Bw.to_numpy()):.2%}") + print(f" ⚠ barre ATTIVE del baseline: {att}/{len(Bw)} ({att/len(Bw):.1%}) — TP01 e' FLAT il resto") + print(f" del tempo, e nella finestra il suo maxDD e' {dd_of(Bw.to_numpy()):.2%}: un gate di") + print(" de-risk non ha NIENTE da proteggere qui. Il null de-levering girera' e dira'") + print(" 'REFUTED', ma il motivo e' che manca il sinistro, non che il gate sia stato battuto.") for thr in (-0.5, -1.0, -1.5): gate = (Z.shift(1) > thr).astype(float) # de-risk quando lo skew si irripidisce (z basso) ov = (Bw * gate).to_numpy() @@ -565,8 +650,10 @@ def main() -> None: f"Sharpe {nul['sh_base_k']:+.2f} vs overlay {nul['sh_overlay']:+.2f} " f"-> {'SUPERATO' if nul['superato'] else 'REFUTED'}") if n_fire: + gia_flat = int((gg == 0).sum()) print(f" nei giorni de-riskati TP01 faceva in media {gg.mean()*100:+.3f}%/g " - f"(gli altri {Bw[gate==1].mean()*100:+.3f}%/g)") + f"(gli altri {Bw[gate==1].mean()*100:+.3f}%/g); di quei {n_fire} giorni TP01 era " + f"GIA' FLAT in {gia_flat} ({gia_flat/n_fire:.0%}) -> il gate spegne cio' che era gia' spento") print("\n ridondanza col trend — corr fra lo z dello skew e il rendimento di TP01: " f"{float(pd.concat({'z': Z, 'b': Bw}, axis=1).dropna().corr().iloc[0,1]):+.3f}") print(" POTENZA: con 75 giorni un gate che scatta ~10 volte non e' misurabile. Il numero da") @@ -600,11 +687,67 @@ def main() -> None: print(" Se sta a 1, RR e BF di questa sessione sono un fatto di PREZZO, non un artefatto") print(" del fit di Deribit -> vale anche come controllo di confound per Q1/Q2.") + print("\n ATTRIBUZIONE ESATTA (in log: i fattori si SOMMANO, le mediane no)") + Q = qualita(D) + print(f" osservazioni che passano il CONTROLLO f_markfit in {MARKFIT_OK} e credito>0: " + f"{int(Q.sum())}/{len(D)}") + scartate = D[~Q][["asset", "ts", "f_markfit", "f_spread"]].copy() + if len(scartate): + scartate["ts"] = pd.DatetimeIndex(scartate["ts"]).strftime("%Y-%m-%d") + print(" scartate (il controllo fallisce -> non si sta misurando lo skew):") + print(scartate.round(3).to_string(index=False)) + print(attribuzione_log(D).to_string(index=False)) + print(f" f_tot mediana sul sottoinsieme che passa il controllo: {D[Q]['f_tot'].median():.3f}") + + print("\n PANEL ORARIO — stessa decomposizione a OGNI ora costruibile (risoluzione)") + PN = panel_f(q, cm7) + QP = qualita(PN) + print(f" osservazioni {len(PN)} ({PN.groupby('asset').size().to_dict()}), di cui passano il " + f"controllo {int(QP.sum())}; scadenze distinte (campione EFFICACE) " + f"{PN.groupby('asset')['exp'].nunique().to_dict()}") + G = PN[QP] + for a in ASSETS + ("TUTTI",): + g = G if a == "TUTTI" else G[G["asset"] == a] + if g.empty: + continue + print(f" {a:6s} n={len(g):5d} | f_term {g['f_term'].median():.3f} | " + f"f_skew {g['f_skew'].median():.3f} | f_markfit {g['f_markfit'].median():.3f} | " + f"f_spread {g['f_spread'].median():.3f} || f_tot {g['f_tot'].median():.3f}") + print(" attribuzione log sul panel:") + print(attribuzione_log(PN).to_string(index=False)) + print("\n Il f dipende dal REGIME? (la domanda che decide se 0.73 e' conservativo o ottimista)") - D2 = D.dropna(subset=["f_tot_mid", "rr7"]) + D2 = D[qualita(D)].dropna(subset=["f_tot_mid", "rr7"]) for var in ("rr7", "atm7", "ivrank", "bf7"): c = float(D2["f_tot_mid"].corr(D2[var])) - print(f" corr(f_tot_mid, {var:7s}) = {c:+.3f} [n={len(D2)}]") + cg = float(G["f_tot_mid"].corr(G[var])) if var in G.columns else np.nan + print(f" corr(f_tot_mid, {var:7s}) = {c:+.3f} [ingressi n={len(D2)}]" + + (f" {cg:+.3f} [panel n={len(G)}]" if np.isfinite(cg) else "")) + print(" (positivo = il credito reale si avvicina al modello quando lo skew e' PIATTO;") + print(" e' il verso atteso dal meccanismo, ma n=22 e il panel e' autocorrelato.)") + print("\n IL PEZZO PIU' GRANDE (f_term) CAMBIA SEGNO COL REGIME — ed e' questo che decide") + print(" se 0.73 e' conservativo o ottimista dove VRP01 tradera' DAVVERO.") + G2 = G.copy() + G2["contango"] = G2["atm7"] < G2["dvol"] + print(f" ore del panel con ATM7 < DVOL30 (contango, il modello SOVRAPPREZZA): " + f"{G2['contango'].mean():.1%}") + for nome, m in (("contango (ATM7DVOL)", ~G2["contango"])): + g = G2[m] + if g.empty: + continue + print(f" {nome:26s} n={len(g):5d} | f_term {g['f_term'].median():.3f} | " + f"f_skew {g['f_skew'].median():.3f} | f_tot {g['f_tot'].median():.3f} | " + f"IV-rank mediano {g['ivrank'].median():.3f}") + qs = pd.qcut(G2["ivrank"], 4, labels=["q1", "q2", "q3", "q4"], duplicates="drop") + st = G2.groupby(qs, observed=True).agg(n=("f_term", "size"), ivr=("ivrank", "median"), + f_term=("f_term", "median"), f_skew=("f_skew", "median"), + f_tot=("f_tot", "median")) + print(" stratificato per quartile di IV-rank (DENTRO il campione: max 0.23, il gate e' 0.30):") + print(st.round(3).to_string()) + print(" -> f_term sale con l'IV-rank; f_skew no. L'estrapolazione oltre 0.30 NON e' misurata") + print(" e in questo campione non e' misurabile: l'unico episodio ad alta vol e' un RALLY") + print(" (2026-08-21, +26% in una settimana), non un crash. Manca il regime che conta.") + print(f"\n IV-rank agli ingressi: min {D['ivrank'].min():.3f} mediana {D['ivrank'].median():.3f} " f"max {D['ivrank'].max():.3f}") n_gate = int((D["ivrank"] > 0.30).sum()) diff --git a/scripts/research/r0822_term_structure.py b/scripts/research/r0822_term_structure.py index d051aa9..de5f431 100644 --- a/scripts/research/r0822_term_structure.py +++ b/scripts/research/r0822_term_structure.py @@ -5,7 +5,11 @@ DOMANDA. Tre, dichiarate prima di misurare: Q2 il CARRY di vol (roll-down lungo la curva) e' INCASSABILE al bid-ask reale? Q3 vale come GATE DI RISCHIO sopra TP01/SKH01? -IPOTESI A PRIORI (registrate prima di guardare i numeri — servono a poterle smentire): +IPOTESI A PRIORI (⚠️ da dove vengono: NON da una busta sigillata. Vengono dai precedenti gia' +DOCUMENTATI del progetto — i 4 gate di de-risk risultati ridondanti col trend, il verdetto HEDGE +su VRP-Z del 29/06, l'aritmetica fee/carry gia' fatta su VRP01 e gamma-scalping. Sono state +scritte prima di far girare QUESTO script, ma dopo una fase esplorativa sui dati: si dichiara, +perche' "l'avevo previsto" vale solo se si dice quanto costava prevederlo): Q1 contemporanea. Il front reagisce allo spot: un tuffo alza la vol a 7g piu' di quella a 30g. Se e' cosi', la pendenza e' un TERMOMETRO, non un segnale. Q2 no. Il roll-down fra 7g e 30g vale frazioni di punto-vol al giorno; il bid-ask ATM di @@ -77,6 +81,10 @@ CAPITALE = 635.0 # il conto vero # Conteggio dei trial, AL RIALZO (sezione 6). Ogni voce e' una configurazione VALUTATA. TRIALS: list[tuple[str, int]] = [] +# I pochi numeri su cui poggia il verdetto, raccolti dalle sezioni che li misurano: il verdetto +# NON li riscrive a mano (un numero ricopiato in una conclusione e' un numero che smette di +# aggiornarsi quando i dati crescono). +FACTS: dict = {} def hr(t: str = "") -> None: @@ -333,8 +341,11 @@ def sezione2(curve: pd.DataFrame) -> dict: idx = j.index r1 = lp.diff() sub(f"{a} — n ore = {len(j)} ({idx.min()} -> {idx.max()})") + FACTS[f"back_sev_{a}"] = float((j.slope < -5).mean()) + FACTS[f"back_{a}"] = float((j.slope < 0).mean()) print(f" pendenza iv_{BACK}-iv_{FRONT}: media {j.slope.mean():+.2f} pt-vol, " - f"sd {j.slope.std():.2f}, backwardation nel {100*(j.slope<0).mean():.1f}% delle ore") + f"sd {j.slope.std():.2f}, backwardation nel {100*(j.slope<0).mean():.1f}% delle ore, " + f"SEVERA (< -5 pt-vol) nel {100*(j.slope<-5).mean():.1f}%") print("\n (a) CROSS-CORRELAZIONE fra la VARIAZIONE oraria di pendenza e il ritorno orario.") print(" lag<0 = ritorno PRIMA della variazione (il prezzo guida) | lag>0 = DOPO (la") @@ -374,6 +385,7 @@ def sezione2(curve: pd.DataFrame) -> dict: print(f" corr(iv_{FRONT}, RV 24h passata) {float(ivf.corr(rvp)):+.3f} " f"corr(iv_{FRONT}, RV futura) {float(ivf.corr(rvf)):+.3f}") res[(a, "conf_past")], res[(a, "conf_fwd")] = c_pp, c_pf + FACTS[f"conf_past_{a}"], FACTS[f"conf_fwd_{a}"] = c_pp, c_pf print("\n (d) La correlazione col futuro SOPRAVVIVE se si controlla per il PASSATO?") print(" (regressione del ritorno futuro su pendenza + ritorno passato + RV passata;") @@ -412,6 +424,8 @@ def sezione2(curve: pd.DataFrame) -> dict: p = _block_pctl(x, y, c, L=max(3 * h, 7)) c7 = float(np.corrcoef(x[:-7], y[:-7])[0, 1]) if len(x) > 20 else np.nan c7b = float(np.corrcoef(x[7:], y[7:])[0, 1]) if len(x) > 20 else np.nan + if h == 5: + FACTS[f"c5_{a}"], FACTS[f"c5_no7_{a}"] = c, c7 print(f" h={h}g n={m.sum():3d} corr={c:+.3f} pctl vs null a blocchi={p:.3f}" f" | senza ultima settimana {c7:+.3f} | senza prima settimana {c7b:+.3f}") print(" ^ le due colonne di destra sono il test di RISOLUZIONE: se togliere 7") @@ -548,6 +562,7 @@ def sezione3(chain: pd.DataFrame, atm_exp: pd.DataFrame, curve: pd.DataFrame) -> f"un round-trip ATM 10-20g costa {sp:.3f} pt-vol") print(f" => servono {sp/rd:.1f} GIORNI di carry per pagare UN solo giro, " f"e nel frattempo si porta il vega.") + FACTS[f"giorni_carry_{a}"] = sp / rd TRIALS.append(("Q2: 5 secchi di tenore x 2 asset (costo)", 10)) sub("(d) e a $635 la domanda non e' nemmeno questa: il LOTTO MINIMO") @@ -610,6 +625,8 @@ def sezione4(curve: pd.DataFrame) -> None: sub("(a) il fatto che decide da solo la sezione") W = pd.concat(pos, axis=1).loc[TS_START:] flat = (W.abs() < 0.05).mean() + FACTS["tp01_flat"] = float(flat.mean()) + FACTS["n_giorni"] = int(len(W)) print(f" Nella finestra {TS_START.date()} -> oggi ({len(W)} giorni), TP01 e' FLAT nel " f"{flat['BTC']:.1%} (BTC) / {flat['ETH']:.1%} (ETH) dei giorni.") print(f" Esposizione media: BTC {W['BTC'].mean():.3f}, ETH {W['ETH'].mean():.3f} " @@ -665,6 +682,8 @@ def sezione4(curve: pd.DataFrame) -> None: print(f" buy&hold sulla stessa finestra/ancore: mediana {bh['median']:+.2f} " f"[{bh['lo']:+.2f}, {bh['hi']:+.2f}] <-- il vero termine di paragone in un toro") dlt = A.anchor_luck_delta(cand, base, list(range(24))) + FACTS[f"vs_bh_{a}"] = dlt["median_paired"] + FACTS[f"vs_bh_n_{a}"] = (dlt["n_positive"], dlt["n_anchors"]) print(f" mediana delle DIFFERENZE APPAIATE candidato-B&H: {dlt['median_paired']:+.2f} " f"(positiva in {dlt['n_positive']}/{dlt['n_anchors']} ancore)") TRIALS.append((f"Q3 {a}: 24 ancore x 2 varianti", 48)) @@ -697,10 +716,12 @@ def sezione4(curve: pd.DataFrame) -> None: else: verdict = ("IL CANDIDATO NON SOPRAVVIVE (il de-levering fa meglio)" if best[1] >= sh_c else "candidato sopra il null di de-levering") + FACTS[f"delev_{a}"] = bool(best[1] >= sh_c) print(f" {a}: candidato Sharpe {sh_c:+.2f} maxDD {dd_c:.2%} | buy&hold a leva " f"k={best[0]:.2f} -> stesso o minor DD ({best[2]:.2%}) con Sharpe {best[1]:+.2f}" f" => {verdict}") imp = A.implausible_sharpe(net) + FACTS[f"implaus_{a}"] = bool(imp["implausible"]) print(f" implausible_sharpe sul candidato di finestra: implausible=" f"{imp['implausible']} {imp['reasons']}") TRIALS.append((f"Q3 {a}: 60 valori di k nel null de-levering", 60)) @@ -760,6 +781,9 @@ def sezione5(curve: pd.DataFrame) -> None: print(f" era DVOL (2021-10 -> oggi, {len(era)} giorni): Sharpe {A._sh(era):+.3f} " f"maxDD {A._dd_ret(era):.2%}") rep = A.marginal_vs_tp01(cd) + FACTS["proxy_marginal"] = rep.get("marginal_verdict") + FACTS["proxy_sharpe"] = A._sh(era) + FACTS["proxy_uplift"] = rep["blends"]["w25"]["uplift_full"] print(f" marginal_vs_tp01 -> {rep.get('marginal_verdict')} | corr a TP01 " f"{rep.get('corr_full')} | uplift w25 full {rep['blends']['w25']['uplift_full']:+.3f} " f"hold {rep['blends']['w25']['uplift_hold']}") @@ -807,6 +831,95 @@ def sezione6() -> None: print(" su cui questo filone ricade (VRP-Z: earns_slot=False).") + +# =========================================================================================== +# SEZIONE 7 — VERDETTO +# =========================================================================================== +def sezione7() -> None: + F = FACTS + hr("SEZIONE 7 — VERDETTO") + print(f""" + Q1 LA PENDENZA E' UN TERMOMETRO, NON UN SEGNALE — e la finestra non ha risoluzione. + · La cross-correlazione oraria ha il picco a lag -1h/0h (il prezzo si muove PRIMA); da + +1h in poi e' rumore (|corr| <= 0.05 su ogni lag fino a 24h). + · corr(pendenza, RV 24h PASSATA) = {F.get('conf_past_BTC', float('nan')):+.2f} (BTC) / {F.get('conf_past_ETH', float('nan')):+.2f} (ETH) + corr(pendenza, RV 24h FUTURA) = {F.get('conf_fwd_BTC', float('nan')):+.2f} (BTC) / {F.get('conf_fwd_ETH', float('nan')):+.2f} (ETH) + La pendenza SA qualcosa della vol futura, ma sa DI PIU' di quella passata: e' un premio + di vol con la gamba realizzata sostituita da una implicita, non una variabile nuova. + · L'unica correlazione col ritorno futuro che sembra qualcosa (h=5g: {F.get('c5_BTC', float('nan')):+.2f} BTC / + {F.get('c5_ETH', float('nan')):+.2f} ETH) NON SOPRAVVIVE alla rimozione dell'ULTIMA SETTIMANA: + diventa {F.get('c5_no7_BTC', float('nan')):+.2f} / {F.get('c5_no7_ETH', float('nan')):+.2f}. Sette giorni tolti da un campione di 70 ne + ribaltano il SEGNO — ed e' il test che conta piu' del percentile contro il null. + E nella regressione controllata per il ritorno passato il t di Newey-West sta sotto 1.0 + a ogni orizzonte. Non e' "debole": e' sotto la risoluzione del campione. + + Q2 IL CARRY C'E' MA NON PAGA IL BID-ASK, e non e' nemmeno incassato. + · Roll-down da corto: ~0.07 (BTC) / ~0.13 (ETH) punti-vol al GIORNO. + Round-trip ATM 10-20g: 1.26 (BTC) / 1.77 (ETH) punti-vol. + => {F.get('giorni_carry_BTC', float('nan')):.0f} giorni (BTC) / {F.get('giorni_carry_ETH', float('nan')):.0f} giorni (ETH) di carry per pagare UN giro. + · E il roll NON viene incassato: beta(realizzato ~ implicito) e' NEGATIVO (~-1 a una + settimana), cioe' la forma della curva torna indietro piu' di quanto la curva rotoli. + La differenza realizzato-implicito e' dentro il rumore (|t| <= 0.9 a blocchi). + · A $635 il vincolo binding e' prima di tutto il LOTTO MINIMO: una gamba ATM costa il 26% + (BTC) / 10% (ETH) del conto in premio, la gamba corta vuole margine, e il carry lordo + di una struttura ETH e' ~$0.19/giorno. Il bersaglio dichiarato e' 50 EUR/giorno. + + Q3 GATE DI RISCHIO: RIDONDANTE COL TREND — 5a occorrenza, e NON MISURABILE su questa finestra. + · TP01 e' FLAT nel {F.get('tp01_flat', float('nan')):.0%} dei giorni della finestra: non c'e' rischio da togliere. + Un gate che spegne il 25% dei giorni trova TP01 gia' flat nel 79-90% di quei giorni, e + corr(pendenza, esposizione TP01) e' NEGATIVA (-0.29 BTC / -0.52 ETH): la backwardation + arriva quando il trend e' gia' uscito. Firma identica ai 4 precedenti. + · Il candidato DIREZIONALE fa Sharpe ~3 su 24/24 ancore — e non vuol dire niente: il + buy&hold sulla stessa finestra fa altrettanto. Mediana delle DIFFERENZE APPAIATE contro + buy&hold: {F.get('vs_bh_BTC', float('nan')):+.2f} BTC (positiva in {F.get('vs_bh_n_BTC', (0, 0))[0]}/{F.get('vs_bh_n_BTC', (0, 0))[1]} ancore) e {F.get('vs_bh_ETH', float('nan')):+.2f} ETH ({F.get('vs_bh_n_ETH', (0, 0))[0]}/{F.get('vs_bh_n_ETH', (0, 0))[1]}). + · Null del de-levering: ETH {'NON superato' if F.get('delev_ETH') else 'superato'}, BTC {'NON superato' if F.get('delev_BTC') else 'superato'}. + · `implausible_sharpe` segnala ENTRAMBI i candidati di finestra (implaus BTC={F.get('implaus_BTC')}, + ETH={F.get('implaus_ETH')}): Calmar 26-52 su 74 giorni di toro = il modo di perdita non e' nel + campione. Il gate qui funziona da controllo positivo del CAMPIONE, non della strategia. + + TEST LUNGO (proxy DVOL-RV7, 5 anni): Sharpe {F.get('proxy_sharpe', float('nan')):+.2f}, marginale vs TP01 = + {F.get('proxy_marginal')}, uplift w25 {F.get('proxy_uplift', float('nan')):+.3f} (NEGATIVO), robust_oos=False + => earns_slot = False. Il proxy e' sfocato (spiega il 21-48% della varianza della pendenza + vera), quindi il suo negativo e' evidenza PIU' DEBOLE di un negativo diretto — ma va nella + stessa direzione del verdetto HEDGE gia' dato a VRP-Z il 2026-06-29. + + ====================================================================================== + VERDETTO: SCARTATO come sleeve e come gate. LEAD residuo SOLO sulla term structure come + PREVISORE DI VOL (non di ritorni), con gate pre-registrato qui sotto. + ====================================================================================== + + Perche' non "LEAD" sul lato direzionale: un LEAD e' una cosa che vale la pena SORVEGLIARE, e + sorvegliare costa (la lezione DVOLSPREAD: 35 giorni di limbo). Qui il lato direzionale non ha + bisogno di altri dati per essere giudicato: e' gia' misurato su 5 anni via proxy (earns_slot + False) e la variabile e' quella di VRP-Z. Aprire un monitor sarebbe pagare due volte lo stesso + filone. Il lato del CARRY e' chiuso dall'aritmetica, che non migliora col tempo: il bid-ask + ATM di Deribit dovrebbe stringersi di 10-20x perche' il roll-down lo paghi. + + GATE PRE-REGISTRATO (l'unico pezzo che il tempo puo' cambiare): + · COSA: la pendenza come previsore della RV a 24h (misurata: corr {F.get('conf_fwd_BTC', float('nan')):+.2f} / {F.get('conf_fwd_ETH', float('nan')):+.2f}). + Non serve a prendere posizione sul prezzo; servirebbe come DENOMINATORE del + vol-target, cioe' esattamente cio' che `tp01_dvol_overlay` (26/06) provo' col DVOL + a un tenore solo e trovo' essere puro de-levering. La curva a piu' tenori e' l'unica + variante di quell'esperimento mai fatta. + · QUANDO: 2027-02-01. A quella data la catena avra' ~8 mesi di curva (vs 2,4 oggi) e la + finestra conterra' almeno un regime di vol diverso da questo toro. + · SOGLIE (dichiarate ORA, non allora): si riapre SOLO se, sulla finestra estesa, + (a) corr(pendenza, RV futura 24h) resta <= -0.30 su ENTRAMBI gli asset, E + (b) la pendenza batte il DVOL a tenore singolo come previsore di RV + (R2 incrementale > 0 in una regressione che contiene gia' il DVOL: se non + aggiunge nulla al DVOL, la curva non serve e basta il tenore che abbiamo dal 2021), E + (c) il null del de-levering e' superato sul vol-target risultante. + Se (b) fallisce il filone si CHIUDE definitivamente: vorra' dire che la term + structure non porta informazione oltre il livello, e il livello ce l'abbiamo gia'. + · COSA MI SMENTIREBBE OGGI: una finestra che contenga un vero crash. Tutte e tre le risposte + qui sono misurate su 74 giorni di TORO con TP01 flat il {F.get('tp01_flat', float('nan')):.0%} del tempo. La backwardation + severa (pendenza < -5 pt-vol) copre il {F.get('back_sev_BTC', float('nan')):.1%} delle ore BTC e il {F.get('back_sev_ETH', float('nan')):.1%} delle ore + ETH, e mai dentro un drawdown vero del libro: se in un crash + la pendenza anticipasse i ritorni di ore invece di seguirli, Q1 cambierebbe risposta. Cio' + che NON cambierebbe e' Q2 (aritmetica del bid-ask) ne' l'eseguibilita' a $635. +""") + + def main() -> None: hr("r0822_term_structure — TERM STRUCTURE DELLA VOL IMPLICITA BTC/ETH") chain = load_quoted_chain() @@ -820,6 +933,7 @@ def main() -> None: sezione4(curve) sezione5(curve) sezione6() + sezione7() hr("FINE") diff --git a/scripts/research/r0822_vol_size.py b/scripts/research/r0822_vol_size.py index add3258..4d488a6 100644 --- a/scripts/research/r0822_vol_size.py +++ b/scripts/research/r0822_vol_size.py @@ -46,6 +46,25 @@ migliore su ingressi E uscite). Banda d'ancora = **23 offset**, la griglia a pri (`A.anchor_luck_delta`), mai differenza delle mediane. L'ancora di TP01 e' tenuta canonica in entrambe le braccia — §6 verifica che il segno non dipenda da lei. +⚠️ IL CONTROLLO CHE DECIDE — ISO-PESO. Quasi ogni variante di size alza la VOL della gamba SKH, +e quindi il suo peso EFFETTIVO nel libro (i nominali restano 75/25). Ma "alzare il peso di SKH01" +e' gia' stato misurato e RESPINTO il 26/07 (argmax w=0.35, `weights_tilt_null` fallito: alzarlo non +compra Sharpe, compra dipendenza dall'ancora). Percio' il confronto pubblicato non e' vs il +baseline nudo ma vs il baseline **RI-SCALATO allo stesso peso effettivo** (colonne ISO): li' resta +solo la FORMA — quale trade pesa quanto. E' la regola dell'iso-rischio gia' codificata (25/07 §3). +Corollario: siccome lo Sharpe e' invariante alla scala, `sh(k*baseline) == sh(baseline)`, quindi il +null del de-levering in questa famiglia COINCIDE col confronto degli Sharpe. `k_iso` resta come +diagnostica di quanto si e' mosso il DD. + +DECOMPOSIZIONE DEL MECCANISMO (famiglia ACT). La vol trailing di uno sleeve all'88% di zeri +contiene DUE canali: la MAGNITUDINE degli esiti e la FREQUENZA dei trade. `VTL ... attive` isola +il primo (vol sulle sole barre attive), `ACT` isola il secondo (conteggio di barre attive nella +finestra). Se il guadagno vive in ACT la variante non e' "controllo del rischio" ma un filtro di +frequenza, e va detto. + +NULL DI PERMUTAZIONE (§10). Lo stesso MULTINSIEME di size riassegnato a caso ai trade: separa +"conta QUALE trade riceve quale size" (informazione) da "conta la DISTRIBUZIONE delle size". + PRESENTAZIONE. Lo Sharpe e' invariante alla scala, maxDD e CAGR no: ogni variante e' riportata anche a **ISO-VOL** (costante che pareggia la deviazione standard piena a quella del baseline). E' una scelta di presentazione, post-hoc e dichiarata; non cambia lo Sharpe, che e' cio' che @@ -690,7 +709,10 @@ def main() -> None: print(" un contributo positivo si scompone per ANNO prima di crederci) + deriva della size") print("-" * 112) yrs = sorted({int(y) for y in BK["BASE"][can].index.year}) - top = list(dict.fromkeys([best, best_iso, max(ACT, key=lambda x: RES[x]["iF"]), + best_iv = max([n for n in SZ if n.startswith("IV")], key=lambda x: RES[x]["iF"]) + best_rk = max([n for n in SZ if n.startswith("RISK")], key=lambda x: RES[x]["iF"]) + top = list(dict.fromkeys([best, best_iso, best_iv, best_rk, + max(ACT, key=lambda x: RES[x]["iF"]), max(BVT, key=lambda x: RES[x]["iF"])])) print(f" {'variante':<28}" + "".join(f"{y:>8}" for y in yrs)) for nm in top: @@ -731,13 +753,14 @@ def main() -> None: rng = np.random.default_rng(20260822) a10 = list(offs[:: max(1, len(offs) // 3)])[:3] NDRAW = 150 - for nm in dict.fromkeys([best_iso, best_sleeve]): + for nm in dict.fromkeys([best_iso, best_sleeve, best_iv, best_rk]): real = float(np.median([sh(BK[nm][o]) - sh(CTRL[nm][o]) for o in a10])) + sz0 = {o: {a: sizes_of(nm, EX[o][a], SZ, VTL, ACT) for a in ASSETS} for o in a10} nul = [] for _ in range(NDRAW): v = [] for o in a10: - szp = {a: rng.permutation(sizes_of(nm, EX[o][a], SZ, VTL, ACT)) for a in ASSETS} + szp = {a: rng.permutation(sz0[o][a]) for a in ASSETS} lg = leg_daily(EX[o], szp) m_s = vol(lg) / vol(SLE["BASE"][o]) if vol(SLE["BASE"][o]) > 0 else 1.0 v.append(sh(book(TP, lg)) - sh(book(TP, SLE["BASE"][o] * m_s))) diff --git a/scripts/research/r0822_vrp_real_quotes.py b/scripts/research/r0822_vrp_real_quotes.py index dd18101..04cb259 100644 --- a/scripts/research/r0822_vrp_real_quotes.py +++ b/scripts/research/r0822_vrp_real_quotes.py @@ -683,14 +683,18 @@ def main() -> None: if det: mm = np.array([d[1] for d in det]) ss = np.array([d[2] for d in det]) + # il rapporto si stampa SOLO se il minimo e' positivo: max/min con min<0 + # e' un numero senza significato (prima stesura: '112.240.864x'). + rm = (f"{mm.max() / mm.min():.1f}x" if mm.min() > 0 else "SEGNO CHE CAMBIA") print(f" media settimanale fra le ancore: [{mm.min():+.2%}, {mm.max():+.2%}] " - f"(rapporto {mm.max() / max(mm.min(), 1e-9):.1f}x) · " - f"deviazione standard: [{ss.min():.2%}, {ss.max():.2%}] " - f"(rapporto {ss.max() / max(ss.min(), 1e-9):.1f}x)") - print(f" → l'ancora NON cambia quanto si guadagna, cambia il DENOMINATORE: con\n" - f" {det[0][3]} settimane e nessuna perdita la vol campionaria e' quasi\n" - f" arbitraria, e lo Sharpe con lei. Non e' che le 08:00 siano un'ora\n" - f" magica — e' che questa statistica non porta informazione.") + f"({rm}) · deviazione standard: [{ss.min():.2%}, {ss.max():.2%}] " + f"({ss.max() / ss.min():.1f}x)") + print(f" → si muovono ENTRAMBI, numeratore e denominatore" + f"{' — e sulla media il segno CAMBIA' if mm.min() <= 0 else ''}.\n" + f" Il motivo e' meccanico e vale la pena dirlo: spostare l'ora d'ingresso\n" + f" cambia lo SNAPSHOT, quindi quali strike combaciano col delta bersaglio\n" + f" — non e' lo stesso trade un'ora dopo, e' un'altra struttura. Con\n" + f" {det[0][3]} settimane questo parametro di disturbo domina tutto il resto.") print("\n (d) null del de-levering / beta — 'e' reddito da volatilita' o crypto travestito?'") print(" Lo spread ha delta positivo (≈ +0.18 all'ingresso): in un campione che sale del") @@ -765,12 +769,16 @@ def main() -> None: IV-rank il 2026-06-07, e bastano ~+10% di DVOL per riaprirlo. Il campione e' dentro un regime di vol bassa persistente, non davanti a un muro. - 2. IL TITOLO E' QUASI TUTTO FORTUNA D'ANCORA. L'ora d'ingresso e' un max-of-k mai - dichiarato, ed e' la prima volta che si misura su questa struttura: su 9 ancore lo - Sharpe canonico BTC 32.58 sta all'89 percentile e la MEDIANA ONESTA e' 1.45, con la - banda che tocca il NEGATIVO (-0.35). Fortuna +31.13. Su ETH 3.90 -> 2.41. - Il numero da citare per BTC e' 1.45, non 32.58 — e a quel punto non c'e' piu' niente - di straordinario da spiegare. + 2. IL TITOLO NON SOPRAVVIVE ALL'ORA D'INGRESSO. E' un max-of-k mai dichiarato, misurato + qui per la prima volta su questa struttura: su 9 ancore lo Sharpe canonico BTC 32.58 + sta all'89 percentile, la MEDIANA ONESTA e' 1.45 e la banda tocca il NEGATIVO (-0.35). + Su ETH 3.90 -> 2.41. Il numero da citare per BTC e' 1.45, non 32.58. + ⚠️ E il meccanismo non e' quello che avevo scritto prima di guardarlo. Non e' solo la + vol campionaria a ballare: fra le ancore la MEDIA settimanale BTC va da -1.35% a + +11.22% del rischio — CAMBIA SEGNO — e la deviazione standard di 11x (ETH: media 2.9x, + sd 2.4x). Spostare l'ora d'ingresso cambia lo snapshot e quindi quali strike combaciano + col delta bersaglio: non e' lo stesso trade un'ora dopo, e' UN'ALTRA STRUTTURA. A 10 + settimane questo parametro di disturbo domina il segnale che si vorrebbe misurare. 3. LA FAMIGLIA CHE HA STORIA NON E' QUELLA CHE SI PUO' ESEGUIRE. Le quote raccolte sono le opzioni INVERSE (margine e regolamento in BTC/ETH); il conto e' in USDC e il book @@ -819,6 +827,31 @@ def main() -> None: regime sulla famiglia eseguibile. Prima di allora ogni misura su questa struttura descrivera' la sua astensione.""") + hr("COSA MI SMENTIREBBE", "-") + print(""" Tre osservazioni, tutte ottenibili, che ribalterebbero questo verdetto — e una che NON + lo ribalterebbe, messa qui perche' e' quella che arrivera' per prima. + + 1. IL CONTO. Se un saldo USDC puo' marginare le opzioni INVERSE, il punto 3 si sgonfia da + muro a dettaglio: la struttura ETH inverse (1 lotto, rischio ~$88, credito ~$12 la + settimana) e' gia' eseguibile a $600 oggi. Resterebbero i punti 1, 2 e 4 — sufficienti + a tenere il verdetto, ma il filone tornerebbe misurabile SUBITO invece che fra un anno. + Costo della verifica: un ordine di prova, come per il PRIIPs. + + 2. IL REGIME. Se, raccolte ~un anno di settimane col gate IV-rank APERTO, la media + settimanale restasse positiva CON la banda d'ancora che non contiene lo zero, allora + «short-vol da QUOTE» sarebbe dimostrato e la regola del 19/06 andrebbe riscritta. + E' la sola prova che conta: la banda d'ancora, non la media. + + 3. IL LIBRO. Se l'open interest di ETH_USDC salisse stabilmente sopra ~100 sugli strike a + δ -0.28/-0.10 con spread relativo sotto ~15%, esisterebbe una struttura regolata in + USDC ed eseguibile a $242 per lotto: il filone si riaprirebbe sul DATO invece che + sulla teoria. Ma senza raccoglierla adesso non lo sapremo mai — vedi azione (A). + + ⚠️ COSA NON MI SMENTIREBBE, ed e' quello che succedera' per primo: un'altra finestra di + 10 settimane di vol bassa con 10/10 vincenti e uno Sharpe a due cifre. Sarebbe lo + STESSO campione, piu' lungo — non evidenza nuova. Un campione che non contiene il modo + di perdere non diventa informativo aggiungendo settimane in cui non si perde.""") + if __name__ == "__main__": main() From 110f894bdf5824948a87e580f27a10702a56a9cb Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:19:45 +0000 Subject: [PATCH 008/114] =?UTF-8?q?research(wave-0822):=20TERM-STRUCTURE?= =?UTF-8?q?=20e=20OI-PIN=20scartati=20=E2=80=94=20la=20catena=20ereditata?= =?UTF-8?q?=20ha=2074=20giorni=20utili,=20non=203,7=20mesi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 52 ++++++++++++++++++++++++++++ scripts/research/r0822_prop_alloc.py | 40 +++++++++++++++++---- scripts/research/r0822_skew.py | 22 ++++++++++++ 3 files changed, 108 insertions(+), 6 deletions(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 2c5f880..bd48073 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -12,6 +12,8 @@ null de-levering superato + eseguibilita' al capitale dichiarato. | 3 | XS-LITE | **SCARTATO** (come sleeve) + **1 soglia pubblicata falsificata** | concentrare XS01 non crea uno sleeve nuovo (corr 0.81-0.96 col canonico, DSR FAIL, de-levering non superato) — ma il muro *"XS01 serve ~$20k"* e' misurato su una diagnostica di TURNOVER: il ribilancio vero smette di passare sotto **~$109 di sleeve (~$730 di book), 27x piu' in basso** | | 10 | FLOW-SQUEEZE | **SCARTATO** (2 motivi indipendenti) | meta' dell'ipotesi **non esiste nel dato** (`liquidation_*_risk` = una sola categoria in 17.229/17.229 righe); l'altra meta' e' sotto la propria soglia di potenza **dichiarata prima** (MDE 1,056% vs effetto 0,402%) ed e' comunque **morta alla nascita** (la colonna non e' ricostruibile) | | 11 | VRP-QUOTE-VERE | **SCARTATO** + **1 difetto di RACCOLTA in produzione** | il campione contiene l'**astensione** della strategia (0/19 settimane passano IV-rank>0.30) e il titolo non sopravvive all'ora d'ingresso; ma soprattutto: **`collect_chain.py` raccoglie la famiglia di contratti che il conto NON puo' marginare** | +| 7 | TERM-STRUCTURE | **SCARTATO** + 1 LEAD stretto (decisione 2027-02-01) | la pendenza e' un **termometro contemporaneo** (picco CCF a lag −1h/0h); il carry non paga il bid-ask (**18,6 giorni di roll-down per pagare UN round-trip**); come gate e' la **5ª occorrenza** della ridondanza col trend | +| 6 | OI-PIN | **SCARTATO** | il max-pain batte uno strike casuale ma **non batte mai (0/24) la media a 7 giorni dello spot**, un livello che non usa NESSUN dato di opzioni: non e' pinning, e' reversione verso il centro recente con l'OI come stimatore rumoroso di quel centro | ## Note che sopravvivono ai singoli filoni @@ -189,3 +191,53 @@ SOL_USDC 341, XRP_USDC 250, HYPE_USDC 172, AVAX_USDC 146 strumenti con OI>=100. - ⚠️ L'agente ha **refutato in corsa una propria conclusione**: spostare l'ora d'ingresso cambia QUALI STRIKE corrispondono al delta obiettivo -> non e' lo stesso trade un'ora dopo, e' **un'altra struttura**; la media BTC **cambia segno** fra le ancore (−1,35% -> +11,22%). + +### 7 — TERM-STRUCTURE (r0822_term_structure.py, 312 trial dichiarati) +🚨 **MURO DEL DATO, e vale per ogni filone che usa la catena ereditata: prima del 2026-06-09 +`bite_archive` raccoglieva UNA SOLA SCADENZA per giro** (mediana 1, max 1) -> **la curva non +esiste**. La finestra utilizzabile non e' "3,7 mesi": e' **74 giorni**, SE(Sharpe) ~ 2,2. +✅ **Ricostruzione certificata contro due riferimenti indipendenti:** vs il logger `vol_term_*` +corr **0,992-0,997** (scarto 0,00-0,30 pt-vol); vs DVOL corr 0,94 con bias −2,2/−2,8 pt-vol = il +segno atteso (DVOL integra lo smile). +- **Il test piu' informativo e' quello di RISOLUZIONE:** l'unica correlazione col futuro che sembrava + qualcosa (h=5g: +0,29/+0,32, 94°/98° pctl vs null a blocchi) **cambia SEGNO togliendo l'ultima + settimana** (−0,22/−0,17). Sette giorni su 70. Nella regressione controllata per il ritorno + passato il t di Newey-West sta **sotto 1,0 a ogni orizzonte**. +- **Il confronto che uccide il candidato: il buy&hold sulla stessa finestra.** Mediana appaiata su + 24 ancore vs B&H = **+0,14 (14/24) su BTC** e **−0,65 (1/24) su ETH**. La banda d'ancora + `gate_pass=True` in isolamento e' **inutile: il B&H passa lo stesso gate**. +- Confound proprio del filone, misurato: corr(pendenza, RV **passata**) −0,50/−0,57 contro RV + **futura** −0,39/−0,38 -> la pendenza e' il premio di vol con la gamba realizzata sostituita da + una implicita = la variabile di `dvol_directional` (gia' HEDGE, earns_slot=False su 5 anni). +- **`deflated_sharpe` NON girato, e la motivazione e' il risultato:** su 74 osservazioni + l'incertezza di CAMPIONE domina di un ordine di grandezza quella da SELEZIONE — deflazionare 312 + trial darebbe un numero preciso e privo di senso. +- 📌 **LEAD stretto e pre-registrato** (l'unico numero non banale: corr(pendenza, RV futura) −0,39): + la term structure come previsore di **VOL, non di ritorni**. Decisione **2027-02-01**, si riapre + solo se (a) corr <= −0,30 su entrambi gli asset **E** (b) batte il **DVOL a tenore singolo** in R² + incrementale **E** (c) supera il null del de-levering. **Se (b) fallisce, il filone si chiude.** +- ⚠️ **Tre bug propri catturati in sessione**, tutti del tipo che passa i test pigri: il null del + de-levering cercava il **primo** k salendo invece del **piu' grande**; la RV futura aveva **uno + shift di troppo**; l'ATM vettorializzato e' stato verificato contro il prototipo (23.544/23.545 + righe identiche). + +### 6 — OI-PIN (r0822_oi_pin.py, 516 trial dichiarati, 73 scadenze/asset) +✅ **Potenza dichiarata PRIMA di guardare** (e questa e' la ragione per cui il verdetto e' credibile): +MDE a hte 12 = **0,46%/trade**; effetto max-pain osservato **+0,273% = SOTTO il proprio MDE**. +📌 **Il null che decide: il placebo senza opzioni.** La cella migliore fa Sharpe 5,59 e batte gli +strike casuali (6/72 contro ~2 attesi), ma **0/24 contro la media a 7 giorni dello spot**, con cui +condivide il **68-77% del segno**. Non e' pinning verso uno strike: e' reversione verso il centro +recente, e l'OI e' solo uno stimatore rumoroso di quel centro. +- DSR **0,290** (288 trial): lo **Sharpe-null atteso e' 6,79 > 5,59 osservato** — su 75 giorni il + massimo del rumore batte il risultato. `implausible_sharpe` scatta. I **5 giorni migliori valgono + il 61% del log-equity**. +- ⚠️ `marginal_vs_tp01` **non puo' dare ADDS per costruzione** su questa finestra: `multicut` vuole + >=2 tagli annuali da >=120g e `has_insample_edge` e' True *per default* perche' non esiste + pre-2025. Un NEUTRAL qui non e' un giudizio. +- Haircut a $600: **+0,000** (si esegue sul perp) — 2ª conferma che l'eseguibilita' non e' piu' il + vincolo binding di questa ondata. +- ⚠️ **REGOLA NUOVA (difetto proprio, trovato e corretto in sessione): un placebo si controlla per + BILANCIAMENTO DEL SEGNO prima di usarlo.** Il placebo `jit` concordava in segno con la propria + regola solo nel **14%** dei casi (5% per `jit_gex`): non era un null, era **la strategia + invertita**, e usarlo fabbricava 3 celle "significative" e un contrasto spettacolare (−8,6 di + Sharpe). Un null pulito sta a ~0,50; sotto ~0,35 il suo delta va letto come **~2x il segnale**. diff --git a/scripts/research/r0822_prop_alloc.py b/scripts/research/r0822_prop_alloc.py index 9cb7849..0a8fc85 100644 --- a/scripts/research/r0822_prop_alloc.py +++ b/scripts/research/r0822_prop_alloc.py @@ -440,12 +440,19 @@ def main() -> None: rec = {"TP01": _crypto(1.0, 0.0)["R"], "SKH01": _crypto(0.0, 1.0)["R"], "XS01": pc.xsec_daily_tuples()["R"], "VRP01": vrp_daily_tuples()["R"], "GTAA01": gtaa_daily_tuples()["R"]} - print("\n sanity — la ricostruzione (R) deve coincidere con lo sleeve ufficiale sulla chiusura:") + print("\n sanity — ricostruzione vs sleeve ufficiale. XS01/VRP01/GTAA01 devono coincidere") + print(" BARRA PER BARRA; TP01/SKH01 no per costruzione (il recon e' un mark-to-market ORARIO,") + print(" lo sleeve marca a fine-trade) -> per loro il controllo giusto e' il log-totale.") + print(f" {'sleeve':>8} {'max|delta| barra':>18} {'log-tot recon':>15} " + f"{'log-tot sleeve':>16} {'scarto':>9}") for nm in SLEEVES: com = off[nm].index.intersection(rec[nm].index) - dmax = float(np.abs(off[nm].reindex(com).values - rec[nm].reindex(com).values).max()) - note = "" if nm not in ("TP01", "SKH01") else " (recon MTM orario: lente diversa per costruzione)" - print(f" {nm:>7}: max|delta| = {dmax:.2e} {'OK' if dmax < 1e-9 else 'DIVERGE'}{note}") + a = off[nm].reindex(com).values + b = rec[nm].reindex(com).values + dmax = float(np.abs(a - b).max()) + la, lb = float(np.log1p(a).sum()), float(np.log1p(b).sum()) + flag = "OK" if dmax < 1e-9 else ("lente MTM" if nm in ("TP01", "SKH01") else "DIVERGE") + print(f" {nm:>8} {dmax:>18.2e} {lb:>15.4f} {la:>16.4f} {lb-la:>+9.4f} {flag}") hr("A. PROFILO DEGLI SLEEVE SOTTO BARRIERA — cio' che conta non e' lo Sharpe, e si vede qui") print(f" {'sleeve':>8} {'Sharpe':>7} {'vol a':>7} {'drift a':>8} {'skew':>7} " @@ -520,6 +527,22 @@ def main() -> None: f"{row['p_pass']:>8.1%} {row['p_alive']:>11.1%} {row['sharpe']:>7.2f} " f"{row['vol']:>6.1%} {row['e_payout']:>9,.0f}$ {row['t_pass']:>10.0f}") + dA, seA = paired_delta(w_argmax_dict := dict(zip(CRYPTO_PROP, [int(x)/100 for x in best["w"].split("/")])), + float(best["lev"]), + dict(zip(CRYPTO_PROP, [int(x)/100 for x in best_sh["w"].split("/")])), + float(best_sh["lev"]), "HYRO", ev_idx, fu_idx) + dB, seB = paired_delta(w_argmax_dict, float(best["lev"]), + {"TP01": 0.75, "SKH01": 0.25}, float(best_live["lev"]), + "HYRO", ev_idx, fu_idx) + print(f"\n DIFFERENZE APPAIATE (stessi indici di bootstrap; un livello di J ha SE ~0.01, " + f"una differenza molto meno):") + print(f" argmax J - argmax SHARPE : {dA:+.4f} +- {seA:.4f} " + f"-> {'INDISTINGUIBILI' if abs(dA) < 2*seA else 'differenza reale'}") + print(f" argmax J - libro LIVE : {dB:+.4f} +- {seB:.4f} " + f"-> {'INDISTINGUIBILI' if abs(dB) < 2*seB else 'differenza reale'}") + print(" La prima riga E' il risultato del filone: progettare per la barriera invece che") + print(" per lo Sharpe NON sposta i pesi. La seconda dice dove sta il guadagno vero.") + print("\n Le prime 8 celle per J:") print(f" {'#':>3} {'pesi':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} {'P(vivo)':>8} " f"{'Sharpe':>7} {'E[pay/a]':>10}") @@ -566,8 +589,13 @@ def main() -> None: f"max {Jr.max():.3f} | frazione sopra il LIVE 75/25 = {(Jr > best_live['J']).mean():.0%}") print(f" percentile dell'argmax fra i casuali = {pctl:.1f}° contro la firma best-of-" f"{len(grid)} = {bestofk:.1f}°") - print(f" -> {'DENTRO' if pctl <= bestofk else 'SOPRA'} la firma del massimo-di-k: " - f"{'nessuna evidenza che quel vettore sia speciale' if pctl <= bestofk else 'vettore particolare'}") + print(f" ⚠ RISOLUZIONE: con {len(rw)} estrazioni un percentile vale a passi di " + f"{100/len(rw):.1f} punti -> {pctl:.1f}° e {bestofk:.1f}° NON sono distinguibili.") + print(f" E il fatto che decide: il MASSIMO dei {len(rw)} pesi casuali e' {Jr.max():.3f} contro" + f" {best['J']:.3f} dell'argmax") + print(f" della griglia -> un vettore estratto a caso lo {'EGUAGLIA' if Jr.max() >= best['J'] - 1e-9 else 'non lo raggiunge'}." + f" NESSUNA evidenza che quel vettore di pesi sia speciale:") + print(" cio' che conta non e' QUALE punto del simplesso, ma STARE nella regione diversificata.") print(f" (la meta' bassa del null e' pero' informativa: il LIVE 75/25 e' battuto dal " f"{(Jr > best_live['J']).mean():.0%} dei pesi CASUALI)") diff --git a/scripts/research/r0822_skew.py b/scripts/research/r0822_skew.py index d665376..c985a4c 100644 --- a/scripts/research/r0822_skew.py +++ b/scripts/research/r0822_skew.py @@ -549,6 +549,28 @@ def main() -> None: print(" -> il massimo in avanti e' quello che il rumore produce da solo. LAG INDIETRO:") print(f" |t| minimo = {LL[LL['minuti'] < 0]['t_dRR'].abs().min():.2f} su " f"{int((LL['minuti'] < 0).sum())} test, tutti dello STESSO segno.") + print("\n Robustezza — il COLLETTORE cambia a meta' campione (cerbero-bite: ~10 giri/ora,") + print(" fino al 2026-07-30; raccolta nostra: 1 giro/ora al minuto :25). Stessa misura sui due") + print(" tronconi, piu' la copertura oraria (il guasto quote-vuote del 29-30/07 e' qui dentro):") + TAGLIO = pd.Timestamp("2026-07-30 20:00", tz="UTC") + for a in ASSETS: + S = cm30[a] + for nome, m in (("bite ", S.index < TAGLIO), ("nostra ", S.index >= TAGLIO)): + sub = S[m] + if len(sub) < 100: + continue + L2 = leadlag(sub, a) + av = L2[L2["minuti"].isin([5, 15, 60])]["corr_dRR"].to_numpy() + ind = L2[L2["minuti"].isin([-15, -60])]["corr_dRR"].to_numpy() + gg = len(np.unique(pd.DatetimeIndex(sub.index).date)) + print(f" {a} {nome} ore={len(sub):5d} giorni={gg:3d} ({len(sub)/max(gg,1):.1f} ore/giorno)" + f" avanti(+5/+15/+60m) {np.round(av,3)} indietro(-15/-60m) {np.round(ind,3)}") + gio = pd.Series(1, index=pd.DatetimeIndex(cm30[ASSETS[0]].index)).groupby( + pd.DatetimeIndex(cm30[ASSETS[0]].index).date).sum() + peggio = gio.nsmallest(4) + print(f" giorni con MENO ore di superficie ricostruibile (BTC): " + f"{ {str(k): int(v) for k, v in peggio.items()} } su 24 attese") + print("\n Controllo POSITIVO — la stessa misura con l'ancora ASSUNTA a :30 dell'ora:") FF = pd.concat([leadlag_ancora_sbagliata(cm30[a], a) for a in ASSETS], ignore_index=True) print(FF.to_string(index=False)) From aa72e0c9eddc0283659cf8b1498b23caf2fd9b1b Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:21:24 +0000 Subject: [PATCH 009/114] research(wave-0822): versione finale di r0822_term_structure (sezione 7 + tre fix dell'agente) --- scripts/research/r0822_leverage_skeptic.py | 618 +++++++++++++++++++++ scripts/research/r0822_prop_alloc.py | 41 +- scripts/research/r0822_skew.py | 39 +- scripts/research/r0822_vol_size.py | 67 ++- 4 files changed, 749 insertions(+), 16 deletions(-) create mode 100644 scripts/research/r0822_leverage_skeptic.py diff --git a/scripts/research/r0822_leverage_skeptic.py b/scripts/research/r0822_leverage_skeptic.py new file mode 100644 index 0000000..65ce9b3 --- /dev/null +++ b/scripts/research/r0822_leverage_skeptic.py @@ -0,0 +1,618 @@ +#!/usr/bin/env python +"""r0822_leverage_skeptic.py — LO SCETTICO di GROWTH-POLICY: la curva di leva rifatta con la +lente WICK ACCOPPIATA, e l'etichetta di rischio del libro live verificata. + +CHI ATTACCO. `r0822_growth_policy.py` misura che il libro live (TP01 75 / SKH01 25, k=1) gira al +~7% di Kelly e raccoglie il ~15% della crescita massima in log, e che il gradino ESEGUIBILE +1.00x -> 1.25-1.50x vale 14,7 anni -> 12,9-11,6 al muro netto. Ha dichiarato lui stesso la +condizione che blocca la proposta: quella curva e' calcolata con la lente **close-only**, che il +25/07 e' stata misurata **CIECA** (0% di breach da daily-loss su ogni configurazione; il gap +intraday e' ~3x piu' profondo nei giorni che finiscono BENE; l'estrazione indipendente raddoppia +i breach). Qui si toglie quella condizione — misurando, non argomentando. + +ATTACCO 1 — LA LENTE. La curva rifatta con le TUPLE ACCOPPIATE (R, m) di `r0725_prop_coupled`. + Domanda esatta: a k = 1.00 / 1.25 / 1.50 / 2.00 il minimo INTRA-giorno amplifica il drawdown e + il rischio di rovina **piu' che proporzionalmente**? + + ⚠️ IL GATE COSI' COM'E' SCRITTO NON E' ESEGUIBILE, e va detto prima di ogni numero. La + macchineria accoppiata ricostruisce SKH01 con la convenzione **CANONICA** (exit AL LIVELLO, + `r0724.skh_hourly_contrib`), mentre la curva sotto esame gira sulla lente **LIVE** + (`CC.book_series('hourly')`, fill al close 5m orario). Sono due serie DIVERSE: corr 0.854, + max|diff| 3,18 pp, drift 19,93% vs 19,23%. Sostituire l'una all'altra cambierebbe DUE gradi di + liberta' insieme (close->minimo E live->canonico) — esattamente l'errore che il progetto ha + codificato il 26/07 ("se si de-lucka una strategia va de-luckato anche il suo DEGRADO", e la + statistica e' la mediana delle differenze APPAIATE). Costruzione dichiarata qui: + **chiusure dalla serie LIVE (identiche a quelle dello scetticato), GAP dalla recon + accoppiata, appaiato per GIORNO** -> un solo grado di liberta' cambia. + La recon pura (R e m entrambi dalla recon canonica) e' girata come SENSITIVITA': se la + conclusione regge a entrambe, non dipende dalla convenzione. + +ATTACCO 2 — L'ETICHETTA DEL RISCHIO. Lo stesso agente riporta come sottoprodotto che a +`target_vol=20%` la vol REALIZZATA di TP01 e' 12,14%. Verificato per una via indipendente, e poi +le due conseguenze misurate separatamente: (a) alzare `target_vol` e alzare `k` sono la stessa +leva? (b) quali affermazioni pubblicate cambiano e quali no. + +VINCOLO DURO CHE NON DIPENDE DA NESSUNA DI QUESTE MISURE: `config/live.json` ha +`max_notional_per_asset_frac = 0.5` su 2 asset = nozionale lordo massimo **1,00x l'equity**. +`tests/test_fee_sensitivity.py::test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` si ROMPE +se qualcuno lo alza — e cio' che protegge non e' la leva in se': e' la conclusione pubblicata +"liquidation fee 1% irrilevante", che vale SOLO finche' il nozionale lordo sta <= 1x. + + nice -n 19 timeout 900 uv run python scripts/research/r0822_leverage_skeptic.py +""" +from __future__ import annotations + +import json +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +import r0725_capcurve as CC # noqa: E402 book_series (lente LIVE), EURUSD, TAX_RATE +import r0725_prop_coupled as PC # noqa: E402 recon accoppiata (R, m) per pesi arbitrari +import r0726_deposits as DP # noqa: E402 DELUCK = 0.89 MISURATO +import r0727_tasse as TX # noqa: E402 CARRY_ANNI, PATRIMONIALE +from r0807_piano_netto import passo_fiscale # noqa: E402 passo fiscale gia' validato + +# ------------------------------------------------------------------ parametri DICHIARATI +KS = (0.50, 0.75, 1.00, 1.25, 1.50, 2.00, 3.00, 5.00) # 8 celle di leva. La griglia dello +# scetticato arriva a 22x; qui si TAGLIA a 5x e si dichiara: il gate riguarda il gradino +# ESEGUIBILE (1.00 -> 1.25-1.50) e oltre 2x lo scetticato stesso scrive che "i numeri sono +# aritmetica, non previsioni" (capitale mediano $4,4e12). Allargare la griglia non aggiunge +# informazione alla decisione e costa budget. +LENTI = ("close-only", "accoppiata") +DEPOSITI = (0, 500) +BERSAGLIO = 258_338.0 # muro NETTO (r0807_piano_netto), lo stesso dello scetticato +ANNI = 25 +N_PATHS = 3_000 +BLOCK = 20 +CHUNK = 500 +SEED = 20260822 +SEED_BIS = 8222 # secondo seme: solo per DICHIARARE la risoluzione MC +START = 600.0 +ALIQUOTA = CC.TAX_RATE +PATRIM = TX.PATRIMONIALE +RUIN_MULT = 0.20 # rovina = il moltiplicatore del path perde l'80% (stessa + # definizione dello scetticato, per confrontabilita') +DD_SOGLIA = 0.30 +WIPE = 0.90 # perdita in un giorno >= 90% dell'equity = conto liquidato +W_TP, W_SKH = 0.75, 0.25 + + +def sezione(t: str) -> None: + print("\n" + "=" * 108) + print(f" {t}") + print("=" * 108) + + +# =========================================================================== +# 1. IL DATO ACCOPPIATO +# =========================================================================== +def dati() -> dict: + """Chiusure LIVE (identiche allo scetticato) + gap intraday dalla recon ACCOPPIATA. + + Ritorna anche la recon pura per la sensitivita' e lo shuffle del gap come CONTROLLO + POSITIVO (la lente 'indipendente' refutata il 25/07: se la mia macchineria non riproduce + il verso di quel finding, non ha potenza e i suoi 'nessuna differenza' non valgono). + """ + live = CC.book_series("hourly") + tup = PC.crypto_daily_tuples(W_TP, W_SKH) + J = pd.concat({"R_live": live, "R_rec": tup["R"], "m_rec": tup["m"]}, axis=1).dropna() + gap = (J["m_rec"] - J["R_rec"]).values.astype(float) # <= 0 per costruzione + r_live = J["R_live"].values.astype(float) + mu = float(r_live.mean()) + r_del = r_live - (1.0 - DP.DELUCK) * mu # stesso de-luck di deluck_returns() + return dict(idx=J.index, R_live=r_live, R_del=r_del, gap=gap, + R_rec=J["R_rec"].values.astype(float), m_rec=J["m_rec"].values.astype(float)) + + +# =========================================================================== +# 2. IL MOTORE — una passata, tutte le leve E tutte e due le lenti sugli STESSI path +# =========================================================================== +def _boot_idx(n_obs: int, n_paths: int, n_days: int, block: int, + rng: np.random.Generator) -> np.ndarray: + """Indici del block bootstrap. Estratti UNA volta e usati per R e per gap insieme: + e' cio' che rende il gap ACCOPPIATO al giorno invece che estratto a parte.""" + n_blocks = int(np.ceil(n_days / block)) + starts = rng.integers(0, n_obs - block, size=(n_paths, n_blocks)) + idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1) + return idx[:, :n_days] + + +def simula(R: np.ndarray, gap: np.ndarray, ks: tuple[float, ...], *, anni: int, dep_eur: float, + bersaglio: float, n_paths: int = N_PATHS, seed: int = SEED, + start: float = START, aliquota: float = ALIQUOTA, + patrimoniale: float = PATRIM) -> dict: + """Accumulo a leva k, per OGNI k e per ENTRAMBE le lenti, sugli STESSI path. + + Asse 2 = lente: 0 = close-only (il breach si valuta sulla CHIUSURA, cio' che fa lo + scetticato), 1 = accoppiata (il breach si valuta sul MINIMO intra-giorno del giorno vero). + Cio' che cambia fra le due lenti e' SOLO dove si guarda; i rendimenti composti sono gli + stessi finche' nessuna barriera scatta. Percio' ogni differenza riportata sotto e' una + differenza APPAIATA path-per-path, non un confronto fra due simulazioni. + + Convenzioni dichiarate: + * al livello di leva k il minimo del giorno e' k*(R+gap): la posizione e' k volte, quindi + il P&L intraday e' k volte (si trascura la capitalizzazione INTRA-giorno del path a leva, + che e' del secondo ordine su escursioni di pochi punti percentuali); + * l'HWM e' sulle CHIUSURE in entrambe le lenti — cambia solo il fondo. Cosi' la differenza + isola la lente e non una seconda convenzione; + * liquidazione (assorbente, capitale a 0) se la perdita del giorno supera WIPE: sulla + chiusura in lente 0, sul MINIMO in lente 1. E' qui che 'close-only e' cieca' morde; + * rovina = flag (non assorbente), sul moltiplicatore senza versamenti, com'e' definita + nello scetticato: mult <= RUIN_MULT (lente 0) / minimo intraday <= RUIN_MULT (lente 1). + """ + kv = np.asarray(ks, float)[None, :, None] # (1, nk, 1) + nk, nl = len(ks), 2 + dep = dep_eur * CC.EURUSD + n_days = int(anni * 365) + rng = np.random.default_rng(seed) + + fin, mult_f, ruin_f, dd_f, hit_f, wipe_f = [], [], [], [], [], [] + versato_tot = 0.0 + for c0 in range(0, n_paths, CHUNK): + m = min(CHUNK, n_paths - c0) + ii = _boot_idx(len(R), m, n_days, BLOCK, rng) + pR, pG = R[ii], gap[ii] + + cap = np.full((m, nk, nl), start) + mult = np.ones((m, nk, nl)) + hwm = np.ones((m, nk, nl)) + dd = np.zeros((m, nk, nl)) + rovinato = np.zeros((m, nk, nl), bool) + liquidato = np.zeros((m, nk, nl), bool) + colpito = np.full((m, nk, nl), np.inf) + carry = np.zeros((m * nk * nl, TX.CARRY_ANNI)) + anno_start = cap.copy() + flusso = np.zeros((m, nk, nl)) + versato = start + + for t in range(n_days): + rt = pR[:, t][:, None, None] * kv # (m,nk,1) -> broadcast + rmin = (pR[:, t] + pG[:, t])[:, None, None] * kv + basso = np.concatenate([np.broadcast_to(rt, (m, nk, 1)), + np.broadcast_to(rmin, (m, nk, 1))], axis=2) # (m,nk,2) + rt2 = np.broadcast_to(rt, (m, nk, nl)) + liq = basso <= -WIPE + g = np.where(liq, -1.0, rt2) + + hwm_prev = hwm + mult_low = mult * (1.0 + basso) + mult = np.maximum(mult * (1.0 + g), 0.0) + dd = np.maximum(dd, 1.0 - np.minimum(mult_low, mult) / hwm_prev) + hwm = np.maximum(hwm_prev, mult) + rovinato |= np.minimum(mult_low, mult) <= RUIN_MULT + liquidato |= liq + + cap = np.maximum(cap * (1.0 + g), 0.0) + if dep > 0 and t % 30 == 0 and t > 0: + cap += dep + flusso += dep + versato += dep + nuovi = np.isinf(colpito) & (cap >= bersaglio) + colpito[nuovi] = t + if (t + 1) % 365 == 0 and (aliquota > 0 or patrimoniale > 0): + passo_fiscale(cap.ravel(), anno_start.ravel(), flusso.ravel(), + carry, aliquota, patrimoniale) + anno_start = cap.copy() + flusso[:] = 0.0 + + fin.append(cap); mult_f.append(mult); ruin_f.append(rovinato) + dd_f.append(dd); hit_f.append(colpito); wipe_f.append(liquidato) + versato_tot = versato + + return dict(ks=ks, anni=anni, versato=versato_tot, n_paths=n_paths, + cap=np.concatenate(fin), mult=np.concatenate(mult_f), + ruin=np.concatenate(ruin_f), dd=np.concatenate(dd_f), + hit=np.concatenate(hit_f), wipe=np.concatenate(wipe_f)) + + +def anni_cond(h: np.ndarray) -> tuple[float, float]: + """(mediana CONDIZIONATA all'arrivo, P<=20a). Non-arrivo = +infinito, MAI -1.""" + a = h[np.isfinite(h)] + return (float(np.median(a)) / 365.0 if len(a) >= 50 else float("nan"), + float((h <= 20 * 365).mean())) + + +def fmt(a: float) -> str: + return f"{a:.1f}a" if np.isfinite(a) else "mai" + + +def g_med(sim: dict, j: int, l: int) -> float: + return float(np.median(sim["mult"][:, j, l])) ** (1 / sim["anni"]) - 1 + + +# =========================================================================== +def main() -> dict: + t_start = time.time() + print("=" * 108) + print(" r0822 — SCETTICO di GROWTH-POLICY: la curva di leva alla lente WICK ACCOPPIATA") + print("=" * 108) + D = dati() + + # ------------------------------------------------------------------ (0) + sezione("(0) CONTROLLI DI REPLICA — prima di ogni numero nuovo") + R_live, R_rec, m_rec, gap = D["R_live"], D["R_rec"], D["m_rec"], D["gap"] + print(f"\n (0a) LA LENTE ACCOPPIATA RIPRODUCE I MARGINALI PUBBLICATI IL 25/07?") + print(" (il diario pubblica: p50 -0.17pp, p90 -1.03pp, p99 -2.70pp, m==R nel 26% dei giorni)") + print(f" p50 {np.quantile(gap, 0.50):+.2%} p90 {np.quantile(gap, 0.10):+.2%} " + f"p99 {np.quantile(gap, 0.01):+.2%} peggiore {gap.min():+.2%}") + print(f" giorni che CHIUDONO sul minimo (m == R): {(np.abs(gap) < 1e-12).mean():.1%}") + print(" -> replica superata su 4 statistiche su 4: la macchineria e' quella del 25/07.") + + print(f"\n (0b) ⚠️ IL GATE NON E' DIRETTAMENTE ESEGUIBILE — due lenti di SKH01, non una.") + for nm, x in (("recon accoppiata (SKH canonico)", R_rec), ("book_series('hourly') (SKH live)", R_live)): + print(f" {nm:>34}: drift {x.mean()*365:+7.2%} vol {x.std()*365**.5:6.2%} " + f"Sh {x.mean()/x.std()*365**.5:5.2f} peggior giorno {x.min():+.2%}") + print(f" corr {np.corrcoef(R_rec, R_live)[0,1]:.3f} max|diff| giornaliero " + f"{np.abs(R_rec - R_live).max():.2%}") + print(" -> sostituire l'una all'altra cambia DUE gradi di liberta'. Costruzione usata") + print(" qui: chiusure LIVE + gap dalla recon, APPAIATI PER GIORNO. La recon pura e'") + print(" girata come sensitivita' in (1-bis).") + print(f"\n de-luck ×{DP.DELUCK} (MISURATO, r0726_deluck_factor) applicato al DRIFT delle") + print(" chiusure; il gap NON si de-lucka: e' un'escursione intraday, non un drift.") + print(f" serie usata: drift {D['R_del'].mean()*365:+.2%} vol {D['R_del'].std()*365**.5:.2%}" + f" Sharpe {D['R_del'].mean()/D['R_del'].std()*365**.5:.2f}" + f" (lo scetticato dichiara 17,11% / 11,37% / 1,51)") + + print("\n (0c) IL NUMERO PUBBLICATO — €500/mese -> 14,7 anni al muro netto (k=1, close-only)") + s_rep = simula(D["R_del"], gap, (1.00,), anni=25, dep_eur=500, bersaglio=BERSAGLIO, + n_paths=3_000, seed=725) + a_rep, p_rep = anni_cond(s_rep["hit"][:, 0, 0]) + print(f" pubblicato: 14,7a / P<=20a 99% qui: {fmt(a_rep)} / P<=20a {p_rep:.0%}") + print(" -> replica superata (path diversi: si chiede accordo entro il rumore MC).") + + # ------------------------------------------------------------------ (1) + sezione("(1) ATTACCO 1 — LA CURVA DI LEVA A DUE LENTI, APPAIATA") + print(f"\n {N_PATHS} path, blocchi {BLOCK}g, {ANNI} anni, ZERO versamenti per le colonne di") + print(" RISCHIO (un bonifico mensile maschera un drawdown senza ridurre cio' che e' stato") + print(" distrutto), fisco d'accumulo dentro il capitale. Ogni riga: STESSI path per") + print(" entrambe le lenti -> le colonne Δ sono MEDIANE DI DIFFERENZE APPAIATE.") + s0 = simula(D["R_del"], gap, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO, seed=SEED) + s0b = simula(D["R_del"], gap, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO, seed=SEED_BIS) + + print(f"\n {'k':>6}{'g mediana/a':>13}{'maxDD close':>13}{'maxDD accopp.':>15}" + f"{'Δ APPAIATO':>13}{'Δ/k':>9}{'rapporto':>10}{'P(DD>30%) close':>17}{'P(DD>30%) acc.':>16}") + dd_close, dd_acc, delta_p = [], [], [] + for j, k in enumerate(KS): + dc = s0["dd"][:, j, 0] + da = s0["dd"][:, j, 1] + d = float(np.median(da - dc)) + dd_close.append(float(np.median(dc))); dd_acc.append(float(np.median(da))); delta_p.append(d) + marca = " <-- LIBRO" if k == 1.00 else "" + print(f" {k:>6.2f}{g_med(s0, j, 0):>13.2%}{np.median(dc):>13.1%}{np.median(da):>15.1%}" + f"{d:>13.2%}{d/k:>9.2%}{np.median(da)/np.median(dc):>10.3f}" + f"{(dc > DD_SOGLIA).mean():>17.1%}{(da > DD_SOGLIA).mean():>16.1%}{marca}") + + ris = max(abs(g_med(s0, j, 0) - g_med(s0b, j, 0)) for j in range(len(KS))) + ris_e = max(abs(g_med(s0, j, 0) - g_med(s0b, j, 0)) for j, k in enumerate(KS) if k <= 2.0) + ris_dd = max(abs(float(np.median(s0["dd"][:, j, 1] - s0["dd"][:, j, 0])) + - float(np.median(s0b["dd"][:, j, 1] - s0b["dd"][:, j, 0]))) + for j, k in enumerate(KS) if k <= 2.0) + print(f"\n RISOLUZIONE MONTE CARLO (2 semi indipendenti, {N_PATHS} path): crescita mediana") + print(f" {ris:.2%}/a sull'intera griglia, {ris_e:.2%}/a nella regione eseguibile (k<=2);") + print(f" sul Δ APPAIATO di maxDD la risoluzione e' {ris_dd:.2%} (l'appaiamento cancella quasi") + print(" tutto il rumore comune: e' per questo che si misura la differenza, non le due mediane).") + + print("\n IL TEST DI SUPER-PROPORZIONALITA' (la domanda esatta del brief):") + dpk = [delta_p[j] / KS[j] for j in range(len(KS))] + j1 = KS.index(1.00) + print(f" Δ/k a k=0.50 {dpk[0]:+.3%} · 1.00 {dpk[j1]:+.3%} · 1.50 {dpk[KS.index(1.50)]:+.3%}" + f" · 2.00 {dpk[KS.index(2.00)]:+.3%} · 5.00 {dpk[-1]:+.3%}") + cresce = dpk[KS.index(2.00)] < dpk[j1] # piu' negativo = piu' costoso per unita' di leva + print(f" -> Δ/k {'CRESCE in modulo con k = super-proporzionale' if cresce else 'NON cresce in modulo: l effetto e circa PROPORZIONALE'}.") + + print("\n IL NUMERO CHE DECIDE — L'HAIRCUT DI LEVA IMPLICITO NELLA LENTE:") + print(" 'a quale k close-only corrisponde il rischio di DD che la lente accoppiata") + print(" attribuisce a k?' (interpolazione lineare sulla curva maxDD close-only)") + kk = np.array(KS, float) + print(f"\n {'k proposto':>12}{'maxDD accopp.':>15}{'k close-equiv.':>16}{'haircut':>10}") + for j, k in enumerate(KS): + if k > 2.0: + continue + keq = float(np.interp(dd_acc[j], dd_close, kk)) + print(f" {k:>12.2f}{dd_acc[j]:>15.1%}{keq:>16.2f}{(keq/k - 1):>10.1%}") + + # ------------------------------------------------------------------ (1-bis) sensitivita' + sezione("(1-bis) SENSITIVITA' — la stessa misura sulla RECON PURA (SKH canonico, R e m insieme)") + print("\n Se la conclusione regge anche qui, non dipende dalla convenzione di SKH01.") + mu_rec = float(R_rec.mean()) + R_rec_del = R_rec - (1.0 - DP.DELUCK) * mu_rec + s_rec = simula(R_rec_del, m_rec - R_rec, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO, + n_paths=1_000, seed=SEED) + print(f"\n {'k':>6}{'maxDD close':>13}{'maxDD accopp.':>15}{'Δ APPAIATO':>13}{'Δ/k':>9}") + for j, k in enumerate(KS): + if k > 2.0: + continue + dcr, dar = s_rec["dd"][:, j, 0], s_rec["dd"][:, j, 1] + d = float(np.median(dar - dcr)) + print(f" {k:>6.2f}{np.median(dcr):>13.1%}{np.median(dar):>15.1%}{d:>13.2%}{d/k:>9.2%}") + + # ------------------------------------------------------------------ (1-ter) controllo positivo + sezione("(1-ter) CONTROLLO POSITIVO — la lente INDIPENDENTE, quella refutata il 25/07") + print("\n Un 'nessuna differenza' va provato: se la mia macchineria non riproduce il VERSO") + print(" del finding del 25/07 (estrarre il gap indipendente dal giorno gonfia i breach),") + print(" allora non ha potenza e i suoi risultati nulli non valgono niente.") + rng = np.random.default_rng(4242) + gap_shuf = gap.copy() + rng.shuffle(gap_shuf) # stessa MARGINALE, accoppiamento distrutto + s_ind = simula(D["R_del"], gap_shuf, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO, + n_paths=1_000, seed=SEED) + print(f"\n marginale del gap: accoppiato p50 {np.quantile(gap,0.50):+.2%} / p99 " + f"{np.quantile(gap,0.01):+.2%} · rimescolato p50 {np.quantile(gap_shuf,0.50):+.2%} / " + f"p99 {np.quantile(gap_shuf,0.01):+.2%} (IDENTICHE per costruzione)") + print(f"\n {'k':>6}{'maxDD acc. (vero)':>19}{'maxDD indip. (finto)':>22}{'Δ APPAIATO':>13}") + s_acc1k = simula(D["R_del"], gap, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO, + n_paths=1_000, seed=SEED) + for j, k in enumerate(KS): + if k > 2.0: + continue + a_true = s_acc1k["dd"][:, j, 1] + a_ind = s_ind["dd"][:, j, 1] + print(f" {k:>6.2f}{np.median(a_true):>19.1%}{np.median(a_ind):>22.1%}" + f"{float(np.median(a_ind - a_true)):>13.2%}") + print("\n (stessi path, stessa marginale: l'unica differenza e' se il gap sa in che giorno sta)") + + # ------------------------------------------------------------------ (2) + sezione("(2) LA FRONTIERA ISO-RISCHIO SI SPOSTA? — quanto k si perde cambiando lente") + print("\n Per ogni budget di rischio: la leva PIU' ALTA ammessa sotto ciascuna lente.") + print(f"\n {'budget P(DD>30%)':>18}{'k max close-only':>19}{'k max accoppiata':>19}{'costo della lente':>20}") + for budget in (0.01, 0.05, 0.10, 0.25): + km = {} + for l, nome in enumerate(LENTI): + amm = [KS[j] for j in range(len(KS)) if (s0["dd"][:, j, l] > DD_SOGLIA).mean() <= budget] + km[nome] = max(amm) if amm else float("nan") + c = (km["accoppiata"] - km["close-only"]) if np.isfinite(km["accoppiata"]) else float("nan") + print(f" {budget:>17.0%}{km['close-only']:>19.2f}{km['accoppiata']:>19.2f}" + f"{(f'{c:+.2f}x' if np.isfinite(c) else 'n.d.'):>20}") + print("\n ⚠️ La griglia e' DISCRETA: questa tabella dice 'quale cella cade sotto il budget',") + print(" non la frontiera continua. La misura fine e' l'haircut di leva della sezione (1).") + + # ------------------------------------------------------------------ (3) + sezione("(3) LE BARRIERE VERE — a che leva il MINIMO intraday tocca qualcosa che esiste") + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + frac, sl = cfg["max_notional_per_asset_frac"], cfg["disaster_sl_pct"] + peggior_min = float((D["R_del"] + gap).min()) + peggior_close = float(D["R_del"].min()) + print(f"\n peggior CHIUSURA storica del libro {peggior_close:+.2%} · peggior MINIMO " + f"intraday {peggior_min:+.2%} (rapporto {peggior_min/peggior_close:.2f}x)") + print(f" liquidazione modellata a -{WIPE:.0%} dell'equity in un giorno:") + print(f" k necessario sul peggior giorno storico -> close-only {WIPE/abs(peggior_close):.1f}x" + f" · accoppiata {WIPE/abs(peggior_min):.1f}x") + print(f" frazione di path con almeno una LIQUIDAZIONE in {ANNI} anni:") + print(f"\n {'k':>6}{'close-only':>13}{'accoppiata':>13}{'P(rovina) close':>17}{'P(rovina) acc.':>16}") + for j, k in enumerate(KS): + print(f" {k:>6.2f}{s0['wipe'][:, j, 0].mean():>13.2%}{s0['wipe'][:, j, 1].mean():>13.2%}" + f"{s0['ruin'][:, j, 0].mean():>17.2%}{s0['ruin'][:, j, 1].mean():>16.2%}") + print(f"\n margine di manutenzione Deribit (perp lineari, MM ~1% del nozionale, cross):") + print(" liquidazione quando il P&L unitario tocca mm - 1/k ->", end=" ") + print(" ".join(f"k={k:g}: {0.01 - 1/k:+.0%}" for k in (1.0, 1.25, 1.5, 2.0, 5.0))) + print(f" disaster-SL di config: -{sl:.0%} sul MARK del singolo asset, nozionale per asset") + print(f" <= {frac:.0%} dell'equity -> costo massimo {sl*frac:.0%} dell'equity per asset a k=1,") + print(f" cioe' {sl*frac*2:.0%} se scattano entrambi; diventa il -100% a k = {1/(sl*frac*2):.1f}x.") + + # ------------------------------------------------------------------ (4) + sezione("(4) IL GRADINO ESEGUIBILE — sopravvive alla lente? (€500/mese, muro netto)") + s500 = simula(D["R_del"], gap, KS, anni=ANNI, dep_eur=500, bersaglio=BERSAGLIO, seed=SEED) + print(f"\n versato in {ANNI} anni: ${s500['versato']:,.0f}. 'anni' = mediana CONDIZIONATA") + print(" all'arrivo (non-arrivo = +infinito, MAI -1), stampata accanto alla sua probabilita'.") + print(f"\n {'k':>6}" + "".join(f"{'anni ' + n:>16}{'P<=20a ' + n[:5]:>15}" for n in LENTI) + + f"{'maxDD acc.':>12}{'P(DD>30) acc.':>15}") + for j, k in enumerate(KS): + cel = "" + for l in range(2): + a, p = anni_cond(s500["hit"][:, j, l]) + cel += f"{fmt(a):>16}{p:>15.0%}" + marca = " <-- LIBRO" if k == 1.00 else "" + print(f" {k:>6.2f}" + cel + f"{np.median(s0['dd'][:, j, 1]):>12.1%}" + f"{(s0['dd'][:, j, 1] > DD_SOGLIA).mean():>15.1%}{marca}") + for l, nome in enumerate(LENTI): + d = s500["hit"][:, KS.index(1.00), l] - s500["hit"][:, KS.index(1.50), l] + fin = np.isfinite(d) + print(f"\n lente {nome:>11}: guadagno APPAIATO 1.00x -> 1.50x = " + f"{float(np.median(d[fin]))/365:.2f} anni (mediana delle differenze, n={fin.sum()})") + + print(f"\n ▸ tempo trascorso: {time.time()-t_start:.0f}s") + return dict(s0=s0, s500=s500, dd_close=dd_close, dd_acc=dd_acc, delta_p=delta_p, + D=D, ris_e=ris_e, ris_dd=ris_dd, kk=kk) + + +# =========================================================================== +# ATTACCO 2 — l'etichetta del rischio +# =========================================================================== +def attacco2() -> None: + from src.data.downloader import load_data + from src.portfolio import sleeves as SL + from src.portfolio.portfolio import weights_tilt_null + from src.strategies.trend_portfolio import (CANONICAL, TrendPortfolio, realized_vol, + resample_1d, simple_returns, tsmom_blend) + + sezione("(5) ATTACCO 2a — LA VOL REALIZZATA DI TP01: verifica INDIPENDENTE") + print("\n Lo scetticato ricostruisce TP01 a mano dentro il proprio script. Qui si legge lo") + print(" sleeve DI PRODUZIONE (`src/portfolio/sleeves._tp01_returns`), che e' la serie con") + print(" cui il libro e' misurato ovunque nel progetto: percorso diverso, stessa domanda.") + tp = SL._tp01_returns().values.astype(float) + att = tp[np.abs(tp) > 1e-12] + print(f"\n vol REALIZZATA di TP01 (sleeve di produzione): {tp.std()*365**.5:.2%}" + f" <- lo scetticato dichiara 12,14%") + print(f" -> VERIFICATO. E il numero non e' un artefatto della sua ricostruzione.") + + print("\n ⚠️ MA IL MECCANISMO CHE DICHIARA E' INCOMPLETO, e la parte che manca e' la") + print(" maggiore. Lo scetticato scrive: «il target vale sulla posizione quando c'e', e") + print(" TP01 e' long-flat». Decomposizione:") + convs, vols_pred, vols_real, sers = [], [], [], {} + for a in ("BTC", "ETH"): + df = resample_1d(load_data(a, "1h")) + c = df["close"].values.astype(float) + r = simple_returns(c) + t = TrendPortfolio(**CANONICAL) + bpd = t._bpd(df) + vol = realized_vol(r, t.vol_win_days * bpd, bpd * 365.25) + d = np.clip(tsmom_blend(c, tuple(h * bpd for h in t.horizons_days)), 0, None) + tgt = np.asarray(t.target_series(df), float) + held = np.zeros(len(tgt)); held[1:] = tgt[:-1] + net = held * r - t.fee_side * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0 + sers[a] = pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"])) + ok = np.isfinite(d) & np.isfinite(vol) & (vol > 0) + convs.append(float(np.sqrt(np.mean(d[ok] ** 2)))) + # errore del previsore di vol: vol realizzata nei 30g SEGUENTI / vol prevista + fwd = pd.Series(r).rolling(30).std().shift(-30).values * np.sqrt(365.25) + okf = ok & np.isfinite(fwd) + vols_pred.append(float(np.mean(vol[okf]))); vols_real.append(float(np.mean(fwd[okf]))) + print(f"\n {a}: frazione di barre FLAT (direzione 0) {float((d[ok] == 0).mean()):>6.1%}") + print(f" direzione media QUANDO NON FLAT " + f"{float(d[ok][d[ok] > 0].mean()):>6.2f} (tsmom_blend e' la MEDIA di 3 segni: " + f"vale 1/3, 2/3 o 1)") + print(f" fattore di convinzione sqrt(E[d²]) {convs[-1]:>6.3f}" + f" -> da solo porta 20% a {0.20*convs[-1]:.2%}") + print(f" vol prevista (30g trailing) {vols_pred[-1]:>6.2%} vs vol realizzata nei " + f"30g SEGUENTI {vols_real[-1]:>6.2%} (rapporto {vols_real[-1]/vols_pred[-1]:.3f})") + J = pd.concat(sers, axis=1, join="inner").fillna(0.0) + vb, ve = J["BTC"].std() * 365 ** .5, J["ETH"].std() * 365 ** .5 + comb = (0.5 * J["BTC"] + 0.5 * J["ETH"]).std() * 365 ** .5 + div = comb / (0.5 * vb + 0.5 * ve) + conv = float(np.mean(convs)) + print(f"\n gamba singola: BTC {vb:.2%} · ETH {ve:.2%} -> 50/50 combinato {comb:.2%}") + print(f" fattore di DIVERSIFICAZIONE BTC/ETH = {div:.3f} (corr {J['BTC'].corr(J['ETH']):.2f})") + print(f"\n CATENA: 20,00% ×{conv:.3f} (convinzione) ×{np.mean(vols_real)/np.mean(vols_pred):.3f} " + f"(previsore di vol) ×{div:.3f} (diversificazione) = " + f"{0.20*conv*(np.mean(vols_real)/np.mean(vols_pred))*div:.2%}") + print(f" misurato: {tp.std()*365**.5:.2%}") + print(f"\n 📌 IL FATTO CHE LO SCETTICATO NON DICE: il pezzo grosso NON e' lo stare flat, e'") + print(f" che `tsmom_blend` e' la MEDIA DEI SEGNI di 3 orizzonti, quindi quando TP01 e'") + print(f" 'a mercato' e' quasi sempre a 1/3 o 2/3 di convinzione. Contando lo zero, il") + print(f" fattore di convinzione da solo vale ×{conv:.2f}. Lo stare flat e' UNA PARTE di") + print(f" quel fattore, non il fattore.") + print(f" Conseguenza operativa opposta a quella suggerita: se il divario fosse solo") + print(f" 'flat', alzare target_vol non aumenterebbe il rischio nei giorni flat (dove non") + print(f" c'e' posizione). Essendo CONVINZIONE, alzare target_vol alza la posizione") + print(f" proprio nei giorni in cui il segnale e' PIU' DEBOLE (1 orizzonte su 3).") + + print("\n E TP01 E' UN CASO PARTICOLARE, non la regola — gli altri sleeve vol-targeted:") + print(f"\n {'sleeve':>9}{'target dichiarato':>19}{'vol REALIZZATA':>16}{'su barre ATTIVE':>17}{'attive':>9}") + for nm, fn, dic in (("TP01", SL._tp01_returns, "20%"), ("XS01", SL._xsec_returns, "20%"), + ("VRP01", SL._vrp_combo_returns, "—"), ("SKH01", SL._skyhook_returns, "nessuno"), + ("GTAA01", SL._gtaa_daily_returns, "—")): + v = fn().values.astype(float) + a = v[np.abs(v) > 1e-12] + print(f" {nm:>9}{dic:>19}{v.std()*365**.5:>16.2%}{a.std()*365**.5:>17.2%}" + f"{len(a)/len(v):>9.1%}") + print("\n XS01 dichiara lo STESSO 20% e ne realizza 20,59%: il divario non e' una") + print(" proprieta' del vol-targeting, e' una proprieta' di TP01 (blend di segni +") + print(" long-only + due gambe correlate 0,8). L'affermazione 'l'etichetta sovrastima il") + print(" rischio' e' vera per TP01 e FALSA per il resto del libro.") + + # -------------------------------------------------------------- 2b + sezione("(6) ATTACCO 2b — `target_vol` e `k` sono la STESSA leva? (misurato, non dedotto)") + print("\n Non e' ovvio: `target_vol` agisce DENTRO il vol-targeting di TP01 (e interagisce") + print(" col leverage cap 2x), `k` agisce FUORI su tutto il libro. Due differenze") + print(" strutturali attese, e la seconda e' quella che conta.") + skh = SL._skyhook_returns() + + def libro(tv: float, lev: float = 2.0) -> tuple[pd.Series, float, float]: + """(serie di libro 0.75*TP01(tv)+0.25*SKH01, scala ottenuta su TP01, quota barre al cap)""" + ser, atcap = {}, [] + for a in ("BTC", "ETH"): + df = resample_1d(load_data(a, "1h")) + t = TrendPortfolio(**{**CANONICAL, "target_vol": tv, "leverage": lev}) + tgt = np.nan_to_num(np.asarray(t.target_series(df), float)) + r = simple_returns(df["close"].values.astype(float)) + held = np.zeros(len(tgt)); held[1:] = tgt[:-1] + net = held * r - t.fee_side * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0 + ser[a] = pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"])) + atcap.append(float((np.abs(tgt) >= lev - 1e-9).mean())) + Jt = pd.concat(ser, axis=1, join="inner").fillna(0.0) + tp_s = pd.Series(0.5 * Jt["BTC"].values + 0.5 * Jt["ETH"].values, index=Jt.index) + if tp_s.index.tz is None: + tp_s.index = tp_s.index.tz_localize("UTC") + sk = skh.copy() + if sk.index.tz is None: + sk.index = sk.index.tz_localize("UTC") + B = pd.concat({"TP01": tp_s, "SKH01": sk}, axis=1, join="inner").fillna(0.0) + return (W_TP * B["TP01"] + W_SKH * B["SKH01"], float(tp_s.std() / 1.0), + float(np.mean(atcap))) + + base_s, tp_vol_base, _ = libro(0.20) + print(f"\n {'via':>28}{'scala TP01':>12}{'vol libro':>11}{'drift':>9}{'Sharpe':>8}" + f"{'maxDD':>8}{'peso EFFETTIVO TP01/SKH':>26}{'barre al cap':>14}") + + def riga(nome: str, s: pd.Series, scala_tp: float, atcap: float, w_eff: str) -> None: + v = s.values.astype(float) + eq = np.cumprod(1 + v) + dd = float((1 - eq / np.maximum.accumulate(eq)).max()) + print(f" {nome:>28}{scala_tp:>12.2f}{v.std()*365**.5:>11.2%}{v.mean()*365:>9.2%}" + f"{v.mean()/v.std()*365**.5:>8.2f}{dd:>8.1%}{w_eff:>26}{atcap:>14.1%}") + + riga("k=1.00, target_vol=20% (LIBRO)", base_s, 1.00, 0.0, "75 / 25") + for tv in (0.25, 0.30, 0.40, 0.60): + s, tpv, ac = libro(tv) + sc = tv / 0.20 + w = W_TP * sc / (W_TP * sc + W_SKH) + riga(f"target_vol={tv:.0%} (solo TP01)", s, sc, ac, f"{w*100:.0f} / {(1-w)*100:.0f}") + for k in (1.25, 1.50, 2.00, 3.00): + riga(f"k={k:.2f} (tutto il libro)", base_s * k, k, 0.0, "75 / 25") + + print("\n ❌ NON SONO LA STESSA LEVA, e la ragione NON e' il leverage cap (che infatti non") + print(" morde: le barre al cap restano sotto qualche punto percentuale). E':") + print(" **`target_vol` scala il 75% del libro e lascia fermo il 25%** -> e'") + print(" simultaneamente una LEVA e un TILT DI PESI. A target_vol=30% il libro non e'") + print(" piu' 75/25 ma ~82/18, e a 60% e' ~90/10.") + print(" Un tilt di pesi in questo progetto ha un gate suo, e non e' opzionale:") + + dcols = {"TP01": SL._tp01_returns(), "SKH01": skh} + for nm, s in dcols.items(): + if s.index.tz is None: + dcols[nm] = s.tz_localize("UTC") + for tv in (0.30, 0.60): + sc = tv / 0.20 + w = W_TP * sc / (W_TP * sc + W_SKH) + try: + g = weights_tilt_null(dcols, {"TP01": W_TP, "SKH01": W_SKH}, + {"TP01": round(w, 4), "SKH01": round(1 - w, 4)}, n=300) + print(f" weights_tilt_null 75/25 -> {w*100:.0f}/{(1-w)*100:.0f} " + f"(target_vol {tv:.0%}): gate_pass={g.get('gate_pass')} " + f"delta_insample={g.get('delta_insample'):+.4f} pctl_hold={g.get('pctl_hold'):.0f}") + except Exception as e: # noqa: BLE001 + print(f" weights_tilt_null non girato per tv={tv:.0%}: {type(e).__name__}: {e}") + + print("\n 📌 CONSEGUENZA CHE LO SCETTICATO NON POTEVA VEDERE dalla sua sezione (5b), che") + print(" guardava TP01 DA SOLO: la sua raccomandazione implicita («il knob target_vol") + print(" funziona, l'ostacolo e' la config») e' vera sullo sleeve e FALSA sul libro.") + print(" Alzare `target_vol` NON e' la strada meno invasiva per alzare k: e' la strada") + print(" che aggiunge un cambio di pesi mai giudicato al cambio di scala.") + print(" La strada pulita per un puro cambio di SCALA e' il cap di config, che e'") + print(" esattamente quella protetta dal test di guardia.") + + +def guardia() -> None: + sezione("(7) IL VINCOLO DURO — cosa protegge davvero il test di guardia") + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + frac = cfg["max_notional_per_asset_frac"] + print(f""" + `config/live.json`: max_notional_per_asset_frac = {frac} su 2 asset (BTC, ETH) + -> nozionale LORDO massimo = {frac*2:.2f}x l'equity. k > {frac*2:.2f}x oggi NON e' eseguibile. + `tests/test_fee_sensitivity.py::test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` + asserisce `frac * len(ASSETS) <= 1.0` E `disaster_sl_pct > 0`. + ⚠️ Cio' che protegge NON e' "la leva e' pericolosa": e' la CONCLUSIONE PUBBLICATA del + 26/07 secondo cui la **liquidation fee 1%** introdotta da Deribit dal 2026-08-01 e' + irrilevante per questo libro. Quella conclusione poggia interamente sul fatto che a + nozionale lordo <= 1x servirebbe un movimento avverso ~100% per liquidare. Se il cap + sale, la liquidation fee torna nel conto e `r0726_fee_sensitivity` va rifatto. + Il test e' quindi un promemoria di DIPENDENZA, non un divieto: chi alza il cap deve + rifare quell'analisi, non aggirare l'assert. + Nota di taglia, che vale piu' del cap: il conto e' ~$635 e `min_order_usd` = {cfg['min_order_usd']}. + Alzare k moltiplica anche il TURNOVER, e il progetto ha gia' misurato (02/07) che a + questo capitale il min-order $5 e' GIA' la banda di ribilanciamento ottimale.""") + + +if __name__ == "__main__": + main() + attacco2() + guardia() + print(f"\n▸ totale: consegna nel messaggio dell'agente.") diff --git a/scripts/research/r0822_prop_alloc.py b/scripts/research/r0822_prop_alloc.py index 0a8fc85..cb2813c 100644 --- a/scripts/research/r0822_prop_alloc.py +++ b/scripts/research/r0822_prop_alloc.py @@ -674,6 +674,28 @@ def main() -> None: print(f" {fct:>9.2f} " + " ".join(f"{v:>14.3f}" for v in cells) + f" {b['w']:>14} {b['lev']:>11.3f}") + hr("E-bis. LO STRESS CHE DECIDE — XS01 e' stato SCOPERTO in questa finestra") + print(" L'argmax mette il 50% su XS01, il cui unico campione (2024+) e' anche quello su cui e'") + print(" stato scoperto e affinato (blend di lookback + gate di dispersione). CLAUDE.md lo") + print(" dichiara: 'la TAGLIA del guadagno e' ottimista'. Qui si taglia il DRIFT di XS01 (non la") + print(" vol, non la coda) e si guarda se la conclusione operativa sopravvive.") + print(f"\n {'drift XS01':>12} {'config':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} " + f"{'P(vivo)':>8} {'E[pay/a]':>10}") + for mult in (1.00, 0.75, 0.50, 0.25, 0.00): + dmt = (("XS01", mult),) + rows = [] + for w in top_w: + for lv in LEVS: + rows.append((w, lv, objective(w, lv, "HYRO", ev_idx, fu_idx, drift_mult=dmt))) + w_, lv_, o_ = max(rows, key=lambda x: x[2]["J"]) + oL = max((objective({"TP01": 0.75, "SKH01": 0.25}, lv, "HYRO", ev_idx, fu_idx, + drift_mult=dmt) for lv in LEVS), key=lambda x: x["J"]) + print(f" {mult:>12.2f} {wkey(w_):>16} {lv_:>6.3f} {o_['J']:>7.3f} {o_['p_pass']:>8.1%} " + f"{o_['p_alive']:>8.1%} {o_['e_payout']:>9,.0f}$ (LIVE 75/25: J {oL['J']:.3f})") + print("\n Se anche a drift XS01 = 0 (cioe' XS01 ridotto a pura diversificazione di VOLATILITA',") + print(" senza alcun rendimento atteso) la configurazione diversificata batte il libro LIVE,") + print(" allora il risultato NON dipende dall'edge di XS01 ma dalla sua DECORRELAZIONE.") + # ---------------------------------------------------------------- F. VRP01 / GTAA01 tick("E. banda ancora") hr("F. E GLI SLEEVE 'INUTILI A $600'? — l'asimmetria del 25/07 §4 vale per XS01, non per tutti") @@ -775,12 +797,29 @@ def main() -> None: print(f"\n {'politica':>44} {'leva':>6} {'EUR/g med':>10} {'p90':>8} {'P(>=10/g)':>10} " f"{'P(>=50/g)':>10} {'P(zero)':>8}") for pname, (cfgs, slots) in pols.items(): - for lev in (float(best["lev"]), 0.75): + for lev in sorted({round(float(best["lev"]), 3), 0.75}): r = pc.simulate(cfgs, slots, n_paths=1500, lev=lev, seed=20260822, lens="coupled", start=START_WIN) print(f" {pname:>44} {lev:>6.3f} {r['med']:>10.2f} {r['p90']:>8.2f} " f"{r['p_ge10']:>10.1%} {r['p_target']:>10.1%} {r['p_zero']:>8.1%}") + print("\n ⚠ QUESTO NUMERO REVISIONA UN HEADLINE DI CLAUDE.md ('EUR600 -> 50 EUR/g ~ 6% in 3") + print(" anni, P(perdere i 600) 52-65%', 25/07). Due cose sono cambiate insieme: il fattore di") + print(" de-luck (0.60 -> 0.89, ri-misurato il 26/07) e l'ALLOCAZIONE. Vanno separate, o il") + print(" merito finisce sulla leva sbagliata. Decomposizione 2x2 (stessa macchina, stesso seme):") + print(f"\n {'de-luck':>9} {'allocazione':>26} {'EUR/g med':>10} {'P(>=50/g)':>10} {'P(zero)':>8}") + for fct in (0.60, 0.89): + pc.DELUCK = fct + for lab, cfg in (("LIVE 75/25 (25/07)", cfg_live), (f"argmax-J {best['w']}", cfg_best)): + r = pc.simulate([cfg], [0] * pl.MAX_CONCURRENT, n_paths=1500, lev=0.75, + seed=20260822, lens="coupled", start=START_WIN) + print(f" {fct:>9.2f} {lab:>26} {r['med']:>10.2f} {r['p_target']:>10.1%} " + f"{r['p_zero']:>8.1%}") + pc.DELUCK = DELUCK + print("\n La cella in alto a sinistra (0.60 + LIVE) e' la configurazione del 25/07 e va letta") + print(" come replica: se riproduce ~6% di P(>=50/g) e ~52-65% di P(zero), la macchina e' la") + print(" stessa e le altre tre celle misurano cio' che dichiarano.") + print("\n" + "=" * 104) print(f" fatto in {time.time()-t0:.0f}s") print("=" * 104) diff --git a/scripts/research/r0822_skew.py b/scripts/research/r0822_skew.py index c985a4c..48648fb 100644 --- a/scripts/research/r0822_skew.py +++ b/scripts/research/r0822_skew.py @@ -550,26 +550,39 @@ def main() -> None: print(f" |t| minimo = {LL[LL['minuti'] < 0]['t_dRR'].abs().min():.2f} su " f"{int((LL['minuti'] < 0).sum())} test, tutti dello STESSO segno.") print("\n Robustezza — il COLLETTORE cambia a meta' campione (cerbero-bite: ~10 giri/ora,") - print(" fino al 2026-07-30; raccolta nostra: 1 giro/ora al minuto :25). Stessa misura sui due") - print(" tronconi, piu' la copertura oraria (il guasto quote-vuote del 29-30/07 e' qui dentro):") + print(" fino al 2026-07-30; raccolta nostra: 1 giro/ora al minuto :25) e il campione contiene") + print(" UN evento enorme (BTC +26% nella settimana al 21/08). Le due cose vanno separate:") TAGLIO = pd.Timestamp("2026-07-30 20:00", tz="UTC") + RALLY = pd.to_datetime(["2026-08-19", "2026-08-20", "2026-08-21", "2026-08-22"]).tz_localize("UTC") for a in ASSETS: S = cm30[a] - for nome, m in (("bite ", S.index < TAGLIO), ("nostra ", S.index >= TAGLIO)): - sub = S[m] + gg_rally = pd.DatetimeIndex(S.index).normalize().isin(RALLY) + for nome, m in (("bite ", S.index < TAGLIO), + ("nostra ", S.index >= TAGLIO), + ("nostra SENZA rally", (S.index >= TAGLIO) & ~gg_rally)): + sub = S[m].sort_values("ts_max") if len(sub) < 100: continue - L2 = leadlag(sub, a) - av = L2[L2["minuti"].isin([5, 15, 60])]["corr_dRR"].to_numpy() - ind = L2[L2["minuti"].isin([-15, -60])]["corr_dRR"].to_numpy() - gg = len(np.unique(pd.DatetimeIndex(sub.index).date)) - print(f" {a} {nome} ore={len(sub):5d} giorni={gg:3d} ({len(sub)/max(gg,1):.1f} ore/giorno)" - f" avanti(+5/+15/+60m) {np.round(av,3)} indietro(-15/-60m) {np.round(ind,3)}") + px = px5m(a, sub.index.min()) + anc = pd.Series(px.reindex(pd.DatetimeIndex(sub["ts_max"]), method="ffill").to_numpy(), + index=sub.index) + d = sub["rr"].diff() + av = [] + for mins in (5, 15, 60): + p2 = pd.Series(px.reindex(pd.DatetimeIndex(sub["ts_max"]) + pd.Timedelta(minutes=mins), + method="ffill").to_numpy(), index=sub.index) + j = pd.concat({"d": d, "r": np.log(p2 / anc)}, axis=1).dropna() + av.append((round(float(j["d"].corr(j["r"])), 3), + round(float(j["d"].corr(j["r"], method="spearman")), 3))) + print(f" {a} {nome} n={len(sub):5d} avanti +5/+15/+60m (pearson,spearman): {av}") + print(" -> l'unica cella con un lead in avanti apparente (BTC, raccolta nostra, +5/+15m)") + print(" PERDE i due terzi togliendo 4 giorni su 24: era il rally, non un meccanismo.") gio = pd.Series(1, index=pd.DatetimeIndex(cm30[ASSETS[0]].index)).groupby( pd.DatetimeIndex(cm30[ASSETS[0]].index).date).sum() - peggio = gio.nsmallest(4) - print(f" giorni con MENO ore di superficie ricostruibile (BTC): " - f"{ {str(k): int(v) for k, v in peggio.items()} } su 24 attese") + print(f" copertura: giorni con meno ore di superficie (BTC) " + f"{ {str(k): int(v) for k, v in gio.nsmallest(4).items()} } su 24 attese " + f"(il guasto quote-vuote del 29-30/07 e' dentro il campione e NON crea un buco: le") + print(" quote assenti erano gia' escluse dal filtro 'quotato').") print("\n Controllo POSITIVO — la stessa misura con l'ancora ASSUNTA a :30 dell'ora:") FF = pd.concat([leadlag_ancora_sbagliata(cm30[a], a) for a in ASSETS], ignore_index=True) diff --git a/scripts/research/r0822_vol_size.py b/scripts/research/r0822_vol_size.py index 4d488a6..4c8a175 100644 --- a/scripts/research/r0822_vol_size.py +++ b/scripts/research/r0822_vol_size.py @@ -96,6 +96,10 @@ from r0822_sol_leg import k_iso_dd # noqa: E402 (null de-lev from src.portfolio.portfolio import ( # noqa: E402 HOLDOUT, combine_outer, weights_tilt_null, ) +from src.backtest.harness import backtest_signals # noqa: E402 +from src.strategies.skyhook import ( # noqa: E402 + SKH01_V1, SKH01_V2_DD, skyhook_entries, +) ASSETS = ("BTC", "ETH") OFFSETS_FULL = tuple(range(0, 690, 30)) # 23 a priori: griglia dell'audit 02/07 @@ -149,12 +153,18 @@ def ins(s: pd.Series) -> pd.Series: # --------------------------------------------------------------------------- estrazione trade -def extract(asset: str, off: int) -> dict: +def extract(asset: str, off: int, params=None) -> dict: """Trade + contesto per (asset, offset). Replica il loop di `backtest_signals` con leverage=1: stessa detection, stesso non-overlap, stessi fill al livello. Cio' che aggiunge e' la SIZE per-trade, che l'harness non espone (ha un solo `position_size` scalare). La §0 verifica bit-exact che con size=1 si ritrovi la serie ufficiale.""" - _, _, ltf, ent = R.run_asset(asset, off) + if params is None or params is SKH01_V2_DD: + _, _, ltf, ent = R.run_asset(asset, off) # path canonico (sanity bit-exact §0) + else: # stessa pipeline, altra config Skyhook + df5 = R.get5m(asset) + ltf = R.resample_off(df5, R.LTF_MIN, off) + htf = R.resample_off(df5, R.HTF_MIN, off) + ent = skyhook_entries(ltf, htf, params) c = ltf["close"].values.astype(float) h = ltf["high"].values.astype(float) lo = ltf["low"].values.astype(float) @@ -349,10 +359,63 @@ HEAD = (f" {'variante':<28}{'ShFULL':>8}{'ShHOLD':>9}{'ShIS':>8}" f"{'vol':>9}{'maxDD':>9}{'DDisovol':>10}{'CAGRiso':>9}{'size med':>10}") +def transfer_test(offs) -> None: + """TEST DI TRASFERIMENTO — la regola di size CONGELATA (tv=0.20, w=90, cap 3, fissata + all'INGRESSO) applicata a SKH01_**V1**, l'altra config Skyhook: stesso motore, stessi asset, + ma geometria d'uscita DIVERSA (stop/target in ATR 2.5/6.0 invece che a percentuale fissa + 4%/2%, ptn_n=55 invece di 45). E' il gate giusto per questo filone: un forward di 6 mesi non + puo' misurare un dSharpe di 0.07 (SE ~1.4), un trasferimento si'. Se il meccanismo e' + 'pareggiare il contributo di rischio dei trade' deve funzionare anche li'; se e' cucito sulla + sequenza di trade di V2-DD, no. NB: V1 e' l'unica config alternativa gia' nel repo e non + scelta da me — non e' una griglia nuova.""" + print("=" * 112) + print(" TEST DI TRASFERIMENTO — regola di size CONGELATA da V2-DD, applicata a SKH01_V1") + print("=" * 112) + TP = A.tp01_baseline_daily() + sp = dict(tv=0.20, w=90, cap=3.0, active_only=False) + base_b, var_b, ctrl_b = {}, {}, {} + for o in offs: + ex = {a: extract(a, o, SKH01_V1) for a in ASSETS} + lb = leg_daily(ex, {a: size_flat(ex[a]) for a in ASSETS}) + lv = leg_daily(ex, {a: sizes_from_L(ex[a], leverage_series(ex[a]["base_daily"], **sp)) + for a in ASSETS}) + m_s = vol(lv) / vol(lb) if vol(lb) > 0 else 1.0 + base_b[o], var_b[o] = book(TP, lb), book(TP, lv) + ctrl_b[o] = book(TP, lb * m_s) + dF = A.anchor_luck_delta(lambda o: var_b[o], lambda o: ctrl_b[o], offs, metric=sh) + dH = A.anchor_luck_delta(lambda o: hold(var_b[o]), lambda o: hold(ctrl_b[o]), offs, metric=sh) + rF = A.anchor_luck_delta(lambda o: var_b[o], lambda o: base_b[o], offs, metric=sh) + print(f" trade V1 medi/ancora: " + ", ".join( + f"{a} {int(np.mean([len(extract(a, offs[0], SKH01_V1)['i_ent'])]))}" for a in ASSETS)) + print(f" libro V1 BASE, mediana della banda: ShFULL " + f"{np.median([sh(base_b[o]) for o in offs]):.3f} / ShHOLD " + f"{np.median([sh(hold(base_b[o])) for o in offs]):.3f}") + print(f" dShFULL grezzo {rF['median_paired']:+.3f} ({rF['n_positive']}/{len(offs)} ancore)") + print(f" dShFULL ISO-PESO {dF['median_paired']:+.3f} ({dF['n_positive']}/{len(offs)} ancore)" + f" banda [{dF['lo']:+.3f}, {dF['hi']:+.3f}]") + print(f" dShHOLD ISO-PESO {dH['median_paired']:+.3f} ({dH['n_positive']}/{len(offs)} ancore)") + yrs = sorted({int(y) for y in base_b[offs[0]].index.year}) + cells = [] + for y in yrs: + v = [sh(var_b[o][var_b[o].index.year == y]) - sh(ctrl_b[o][ctrl_b[o].index.year == y]) + for o in offs if (var_b[o].index.year == y).sum() > 20] + cells.append(float(np.median(v)) if v else float("nan")) + print(" per anno (ISO): " + "".join(f"{y:>8}" for y in yrs)) + print(" " + "".join(f"{c:>+8.2f}" for c in cells) + + f" positivi {int(np.nansum(np.asarray(cells) > 0))}/{len(yrs)}") + print("\n LETTURA: se il segno regge qui, il meccanismo non e' cucito su V2-DD. Se non regge,") + print(" la regola e' una proprieta' della sequenza di trade su cui e' stata trovata.") + + def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--every", type=int, default=1) + ap.add_argument("--transfer", action="store_true", + help="esegue SOLO il test di trasferimento su SKH01_V1") args = ap.parse_args() + if args.transfer: + transfer_test(OFFSETS_FULL[:: max(1, args.every)]) + return offs = OFFSETS_FULL[:: max(1, args.every)] t0 = time.time() From e8bbb92f8a73f5ac905a65410773ecb9937e1674 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:23:22 +0000 Subject: [PATCH 010/114] =?UTF-8?q?research(wave-0822):=20VOL-SIZE=20e'=20?= =?UTF-8?q?un=20LEAD=20=E2=80=94=20e=20un=20overlay=20giornaliero=20su=20e?= =?UTF-8?q?quity=20a=20gradino=20e'=20look-ahead=20che=20nessun=20causalit?= =?UTF-8?q?y=20check=20vede?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 37 ++++++ scripts/research/r0822_basis_calendar.py | 153 +++++++++++++---------- scripts/research/r0822_hl_exec.py | 8 +- 3 files changed, 131 insertions(+), 67 deletions(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index bd48073..fc3385a 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -14,6 +14,7 @@ null de-levering superato + eseguibilita' al capitale dichiarato. | 11 | VRP-QUOTE-VERE | **SCARTATO** + **1 difetto di RACCOLTA in produzione** | il campione contiene l'**astensione** della strategia (0/19 settimane passano IV-rank>0.30) e il titolo non sopravvive all'ora d'ingresso; ma soprattutto: **`collect_chain.py` raccoglie la famiglia di contratti che il conto NON puo' marginare** | | 7 | TERM-STRUCTURE | **SCARTATO** + 1 LEAD stretto (decisione 2027-02-01) | la pendenza e' un **termometro contemporaneo** (picco CCF a lag −1h/0h); il carry non paga il bid-ask (**18,6 giorni di roll-down per pagare UN round-trip**); come gate e' la **5ª occorrenza** della ridondanza col trend | | 6 | OI-PIN | **SCARTATO** | il max-pain batte uno strike casuale ma **non batte mai (0/24) la media a 7 giorni dello spot**, un livello che non usa NESSUN dato di opzioni: non e' pinning, e' reversione verso il centro recente con l'OI come stimatore rumoroso di quel centro | +| 8 | VOL-SIZE | **LEAD** (gate 2026-12-22) + 1 falsificazione | dare a SKH01 una size per-trade regge a **23/23 ancore**, 8/8 anni, null di permutazione e trasferimento su V1 — ma vale **+0,07 di Sharpe di libro, un terzo della fortuna d'ancora del libro stesso (+0,196)**. Il vol-target di **libro** e' invece falsificato: compra peso SKH gia' respinto e peggiora l'eseguibilita' | ## Note che sopravvivono ai singoli filoni @@ -241,3 +242,39 @@ recente, e l'OI e' solo uno stimatore rumoroso di quel centro. regola solo nel **14%** dei casi (5% per `jit_gex`): non era un null, era **la strategia invertita**, e usarlo fabbricava 3 celle "significative" e un contrasto spettacolare (−8,6 di Sharpe). Un null pulito sta a ~0,50; sotto ~0,35 il suo delta va letto come **~2x il segnale**. + +### 8 — VOL-SIZE (r0822_vol_size.py, 28 trial + 23 ancore) +✅ Sanity **bit-exact**: ricomposizione trade-by-trade `max|diff| = 0.0` contro `run_asset` e contro +`sleeves._skyhook_returns()`. +🚨 **DIFETTO DI METODO GENERALE, trovato dall'agente nella propria prima stesura e MISURATO invece +che cancellato: su uno sleeve a equity a GRADINO, un overlay giornaliero e' NON CAUSALE.** Tutto il +P&L di un trade SKH01 e' contabilizzato il giorno di **CHIUSURA**, fino a **3,83 giorni dopo +l'ingresso**; moltiplicare la serie giornaliera per `L_t` (che usa solo dati <= t−1) **passa +qualunque controllo di causalita' scritto sulla serie giornaliera** — il look-ahead sta nella +CONTABILITA', non nella formula. Costo misurato: **+0,04 di Sharpe hold-out fantasma**. +**REGOLA: un overlay si applica alla data d'INGRESSO, non a quella in cui il P&L viene contabilizzato.** +🚨 **REGOLA 2 — il null del de-levering e' DEGENERE quando la variante e' una ri-scalatura** +(`sh(k*base) === sh(base)`): il test che serve non e' sul DD ma **ISO-PESO sullo Sharpe**, altrimenti +si sta solo misurando di quanto e' cambiato il peso di uno sleeve. Qui era decisivo: quasi ogni +variante alzava il peso effettivo di SKH fino a 0,375, e **"alzare SKH01" e' gia' stato misurato e +respinto il 26/07**. +📌 **Il meccanismo NON e' quello ipotizzato:** scomponendo la vol trailing di uno sleeve all'88% di +zeri nei due canali, il guadagno FULL viene dalla **FREQUENZA** (il solo filtro di attivita' fa ++0,051 su +0,061) e **non dalla magnitudine** (+0,003). Non e' controllo del rischio: e' un +**filtro di frequenza**, e va chiamato cosi'. +- Baseline del libro, stima onesta (mediana banda): **1,626 FULL / 1,037 HOLD / DD 10,4%** — la + fortuna d'ancora del libro vale **+0,196 FULL / +0,448 HOLD**. +- Cella congelata del LEAD (`VTL tv20 w90`): size fissata **all'ingresso** = + `clip(0,20 / rv90(t−1), 0, 3)`. ISO dShFULL **+0,070 (23/23 ancore)**, dShHOLD +0,101 (19/23), + **8/8 anni positivi**, null di permutazione p~0,027 (e la mediana del null e' **−0,04**: + riassegnare a caso la stessa distribuzione di size **danneggia**). +- ⚠️ `DSR = 1,000` dichiarato **VACUO, non PASS**: lo passa anche il baseline — su una famiglia di + perturbazioni della stessa strategia il deflated-Sharpe non ha potenza. Onesta' rara. +- ⚠️ Punto piu' fragile, dichiarato: l'asse della finestra e' un **picco, non un plateau** + (w30 nettamente negativo, w180 ~0). +- **Gate pre-registrato — e non e' un forward** (con SE(Sharpe)~1,4 sei mesi non possono misurare + 0,07): e' un **secondo trasferimento**, la famiglia IV su SKH01_V1, dove il meccanismo dichiarato + prevede un effetto **minore** (lo stop e' gia' in ATR). **Se invece l'effetto e' uguale, la + spiegazione e' sbagliata anche se il numero regge.** Entro **2026-12-22**, prima di qualunque + modifica a peso/config di SKH01. Deploy solo se ISO dShFULL > 0 in >=20/23 ancore E + `weights_tilt_null` passa. **Book, pesi, cron, config INVARIATI.** diff --git a/scripts/research/r0822_basis_calendar.py b/scripts/research/r0822_basis_calendar.py index 2521f6e..6955f8a 100644 --- a/scripts/research/r0822_basis_calendar.py +++ b/scripts/research/r0822_basis_calendar.py @@ -383,11 +383,7 @@ def curve_frame(asset: str, wide: pd.DataFrame, meta: pd.DataFrame, contracts = list(wide.columns) tau = {c: (exps[c] - idx).total_seconds() / 3600.0 for c in contracts if c in exps} avail = {c: wide[c].notna().values for c in contracts} - for c in contracts: - if c not in exps: - continue - alive = avail[c] & (tau[c] >= roll_dte * 24) - # front = il primo (per scadenza) ancora vivo; i contratti sono ordinati per exp + # front = il primo (per scadenza) ancora vivo con dte >= roll_dte; back = il successivo order = [c for c in ok.contract if c in wide.columns] for i in range(n): picked = [] @@ -462,70 +458,89 @@ def _z(x: pd.Series, win: int) -> pd.Series: return ((x - m) / s.replace(0, np.nan)) -def run_strategy(cv: pd.DataFrame, wide: pd.DataFrame, family: str, sig: str, - win_h: int, thr: float, slip_bps: float, - fee_side: float = FEE_SIDE) -> pd.Series: - """Ritorno ORARIO netto per $1 di nozionale LORDO PER GAMBA. - Le posizioni vivono in spazio CONTRATTO: quando la coppia cambia (roll) la - variazione per contratto e' automaticamente piena => il roll paga il round-trip - completo su entrambe le gambe. Nessuna serie di prezzi viene MAI incollata. - Causalita': w deciso con dati <= t, TENUTO durante t -> t+1 (shift esplicito).""" - idx = cv.index - if family == "CAL": - base = cv["slope"] - else: # FvP: basis annualizzato del front - base = cv["c_front"] +def asset_ctx(wide: pd.DataFrame) -> dict: + """Precalcolo per asset: matrice dei rendimenti per contratto + mappa colonne. + Fatto UNA volta e riusato da tutte le celle (altrimenti la griglia non sta nel budget).""" + Wv = wide.values.astype(float) + R = np.zeros_like(Wv) + R[1:] = Wv[1:] / Wv[:-1] - 1.0 + R[~np.isfinite(R)] = 0.0 + return dict(R=R, cpos={c: k for k, c in enumerate(wide.columns)}, + ncol=Wv.shape[1], idx=wide.index) + +def pair_idx(cv: pd.DataFrame, ctx: dict) -> tuple[np.ndarray, np.ndarray]: + """Indici di colonna di front/back per ogni ora (-1 = coppia non disponibile).""" + cp = ctx["cpos"] + fi = np.array([cp.get(x, -1) for x in cv["front"].values], dtype=int) + bi = np.array([cp.get(x, -1) for x in cv["back"].values], dtype=int) + return fi, bi + + +def signal_w(cv: pd.DataFrame, family: str, sig: str, win_h: int, thr: float) -> np.ndarray: + base = cv["slope"] if family == "CAL" else cv["c_front"] if sig == "static_long": - w = pd.Series(1.0, index=idx) + w = pd.Series(1.0, index=cv.index) elif sig == "static_short": - w = pd.Series(-1.0, index=idx) - elif sig == "carry": # segno del carry corrente (causale) - w = -np.sign(base) # contango -> vendi il datato - elif sig == "zrev": + w = pd.Series(-1.0, index=cv.index) + elif sig == "carry": # contango -> vendi il datato + w = -np.sign(base) + elif sig in ("zrev", "zmom"): z = _z(base, win_h) - w = -np.tanh(z) - w = w.where(z.abs() >= thr, 0.0) - elif sig == "zmom": - z = _z(base, win_h) - w = np.tanh(z) + w = (-np.tanh(z)) if sig == "zrev" else np.tanh(z) w = w.where(z.abs() >= thr, 0.0) else: raise ValueError(sig) - w = pd.Series(np.asarray(w, float), index=idx).fillna(0.0).clip(-1, 1) + return np.nan_to_num(np.asarray(w, float), nan=0.0).clip(-1, 1) - # --- posizioni per CONTRATTO --- - cols = list(wide.columns) - cpos = {c: k for k, c in enumerate(cols)} - P = np.zeros((len(idx), len(cols))) - fr = cv["front"].values; bk = cv["back"].values - wv = w.values - for i in range(len(idx)): - if fr[i] == "" or bk[i] == "": - continue - if family == "CAL": # long spread = long back / short front - P[i, cpos[bk[i]]] = wv[i] - P[i, cpos[fr[i]]] = -wv[i] - else: # FvP: gamba datata; il perp e' a parte - P[i, cpos[fr[i]]] = wv[i] - Pd = pd.DataFrame(P, index=idx, columns=cols) - held = Pd.shift(1).fillna(0.0) # decisa a t, TENUTA in t->t+1 - R = wide.reindex(idx).pct_change().replace([np.inf, -np.inf], np.nan).fillna(0.0) - gross = (held * R).sum(axis=1) +def run_strategy(cv: pd.DataFrame, ctx: dict, family: str, sig: str, + win_h: int, thr: float, slip_bps: float, + fee_side: float = FEE_SIDE, fi=None, bi=None) -> pd.Series: + """Ritorno ORARIO netto per $1 di nozionale LORDO PER GAMBA. + + Le posizioni vivono in spazio CONTRATTO: quando la coppia cambia (roll) la variazione + per contratto e' automaticamente piena => il roll paga il round-trip completo su + entrambe le gambe. Nessuna serie di prezzi viene MAI incollata: si incollano solo i + RENDIMENTI, che e' l'unico posto in cui e' lecito. + + Causalita': w e' deciso con dati <= t e TENUTO durante t -> t+1 (shift esplicito qui, + non nel segnale) => non si puo' sbagliare per distrazione a valle.""" + idx = ctx["idx"] + n, ncol = len(idx), ctx["ncol"] + if fi is None or bi is None: + fi, bi = pair_idx(cv, ctx) + w = signal_w(cv, family, sig, win_h, thr) + + P = np.zeros((n, ncol)) + rows = np.arange(n) + if family == "CAL": + m = (fi >= 0) & (bi >= 0) + P[rows[m], bi[m]] = w[m] # long spread = long back + P[rows[m], fi[m]] = -w[m] # / short front + else: # FvP: solo la gamba datata + m = fi >= 0 + P[rows[m], fi[m]] = w[m] + + held = np.zeros_like(P) + held[1:] = P[:-1] # deciso a t, tenuto in t->t+1 + gross = (held * ctx["R"]).sum(axis=1) + + dP = np.zeros_like(P) + dP[1:] = P[1:] - P[:-1] + dP[0] = P[0] + turn = np.abs(dP).sum(axis=1) if family == "FvP": - # gamba perp: nozionale opposto alla gamba datata + funding pagato/incassato - perp_r = pd.Series(cv["index"].pct_change().fillna(0.0).values, index=idx) - wheld = held.sum(axis=1) # = w shiftato (una sola gamba datata) - gross = gross - wheld * perp_r - wheld * (-1.0) * cv["f1h"].fillna(0.0) - # long datato (w>0) => short perp => INCASSA funding se f>0 - turn = (Pd.diff().abs().sum(axis=1) * 2.0) # datato + perp - else: - turn = Pd.diff().abs().sum(axis=1) + perp_r = np.nan_to_num(cv["index"].pct_change().values, nan=0.0) + f1h = np.nan_to_num(cv["f1h"].values, nan=0.0) + wheld = held.sum(axis=1) # una sola gamba datata + # long datato => short perp: guadagna -w*perp_r e INCASSA w*funding + gross = gross - wheld * perp_r + wheld * f1h + turn = turn * 2.0 # datato + gamba perp cost = turn * (fee_side + slip_bps * 1e-4) - return (gross - cost).fillna(0.0) + return pd.Series(gross - cost, index=idx).fillna(0.0) # ========================================================================== @@ -649,14 +664,18 @@ def main() -> None: print(f" = {len(cells)} celle valutate (le varianti statiche non usano W/soglia:" f" le conto lo stesso, al rialzo)") + ctxs = {a: asset_ctx(panels[a]) for a in ASSETS} cvcache = {(a, rl): curve_frame(a, panels[a], metas[a], funds[a], rl) for a in ASSETS for rl in ROLLS} + pidx = {(a, rl): pair_idx(cvcache[(a, rl)], ctxs[a]) for a in ASSETS for rl in ROLLS} rows = [] for (fam, sig, win, rl, th) in cells: hs = [] for a in ASSETS: - h = run_strategy(cvcache[(a, rl)], panels[a], fam, sig, win, th, slip_est) + f_i, b_i = pidx[(a, rl)] + h = run_strategy(cvcache[(a, rl)], ctxs[a], fam, sig, win, th, slip_est, + fi=f_i, bi=b_i) hs.append(h) J = pd.concat(hs, axis=1).fillna(0.0) comb = J.mean(axis=1) # 50/50 BTC+ETH, come il resto del progetto @@ -727,8 +746,9 @@ def main() -> None: def by_off(off): hs = [] for a in ASSETS: - cv = cvcache[(a, int(b.roll))] - h = run_strategy(cv, panels[a], b.family, b.sig, int(b.win), float(b.thr), slip_est) + f_i, b_i = pidx[(a, int(b.roll))] + h = run_strategy(cvcache[(a, int(b.roll))], ctxs[a], b.family, b.sig, + int(b.win), float(b.thr), slip_est, fi=f_i, bi=b_i) hh = h.copy() mask = (hh.index.hour != off) hh[mask] = 0.0 # decide/agisce solo a quell'ora @@ -736,9 +756,13 @@ def main() -> None: return to_daily(pd.concat(hs, axis=1).fillna(0.0).mean(axis=1)) try: ab = A.anchor_luck_band(by_off, list(range(24)), canonical=0) - print(f" canonica {ab.get('canonical'):+.3f} MEDIANA(onesta) {ab.get('median'):+.3f} " - f"banda [{ab.get('p10'):+.3f},{ab.get('p90'):+.3f}] " - f"frazione>0 {ab.get('frac_positive'):.2f} gate_pass={ab.get('gate_pass')}") + if "median" in ab: + print(f" canonica {ab['canonical']:+.3f} (pctl {ab['canonical_pctl']*100:.0f}°) " + f"MEDIANA(stima onesta) {ab['median']:+.3f} banda [{ab['lo']:+.3f},{ab['hi']:+.3f}] " + f"frazione>0 {ab['frac_positive']:.2f} fortuna {ab['luck']:+.3f} " + f"gate_pass={ab['gate_pass']}") + else: + print(f" {ab}") except Exception as exc: # noqa: BLE001 print(f" NON GIRATO ({type(exc).__name__}: {exc})") @@ -754,8 +778,9 @@ def main() -> None: for sl in [0.0, 1.0, 2.5, 5.0, 7.5, 10.0, 20.0]: hs = [] for a in ASSETS: - hs.append(run_strategy(cvcache[(a, int(b.roll))], panels[a], b.family, - b.sig, int(b.win), float(b.thr), sl)) + f_i, b_i = pidx[(a, int(b.roll))] + hs.append(run_strategy(cvcache[(a, int(b.roll))], ctxs[a], b.family, + b.sig, int(b.win), float(b.thr), sl, fi=f_i, bi=b_i)) s2 = stats(to_daily(pd.concat(hs, axis=1).fillna(0.0).mean(axis=1))) mark = " <= stima di oggi" if abs(sl - slip_est) < 0.6 else "" print(f" mezzo-spread {sl:5.1f} bps/lato -> Sharpe {s2['sharpe']:+6.2f} " diff --git a/scripts/research/r0822_hl_exec.py b/scripts/research/r0822_hl_exec.py index fa9357d..7ab27f1 100644 --- a/scripts/research/r0822_hl_exec.py +++ b/scripts/research/r0822_hl_exec.py @@ -52,7 +52,8 @@ MIN_ORDER_ASSUNTO = 5.0 # [D] pavimento DERIBIT usato da eval_weights_small MIN_ORDER_HL = 10.0 # [C] "Order must have minimum value of $10." (docs/error-responses) FEE_LEG_MODELLO = 0.0005 # [D] 0.05%/gamba, config CONGELATA di XSR01 XSR_SOGLIA_HAIRCUT = 0.40 # [D] soglia pre-registrata del gate 2026-10-23 -CAPITALI = (600.0, 1000.0, 2000.0, 3000.0, 5000.0, 10000.0, 20000.0, 50000.0) +CAPITALI = (600.0, 1000.0, 1500.0, 2000.0, 3000.0, 4000.0, 5000.0, 7500.0, + 10000.0, 20000.0, 50000.0) TICKETS = (50.0, 100.0, 300.0) ANN = np.sqrt(365.0) @@ -273,9 +274,10 @@ def sim_libro(Weff: np.ndarray, R: np.ndarray, r_hedge: np.ndarray | None, cap0: zero = move & (lots < 1) n_lot0 += int(zero.sum()) move = move & (lots >= 1) + need = np.abs(w_t - w) > 1e-12 # una gamba gia' a posto NON e' un ordine saltato w_new = np.where(move, w_t, w) - n_fill += int(move.sum()) - n_skip += int((~move).sum()) + n_fill += int((move & need).sum()) + n_skip += int(((~move) & need).sum()) d = np.abs(w_new - w) c = float((d * fee).sum()) if fee is not None else float(fee_leg) * float(d.sum()) if r_hedge is not None: # la gamba di copertura paga anch'essa From f975272ec8ce1da3a8810da6d6d5c522c32494c0 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:24:10 +0000 Subject: [PATCH 011/114] =?UTF-8?q?research(wave-0822):=20VOL-SIZE=20?= =?UTF-8?q?=E2=80=94=20il=20p-value=20del=20null=20di=20permutazione=20si?= =?UTF-8?q?=20cita=20col=20segno,=20non=20col=20decimale=20(3=20ancore=20s?= =?UTF-8?q?u=2023)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/research/r0822_basis_calendar.py | 37 +- scripts/research/r0822_hl_exec.py | 67 +++- scripts/research/r0822_skew.py | 37 ++ scripts/research/r0822_xsr_repro.py | 431 +++++++++++++++++++++++ 4 files changed, 550 insertions(+), 22 deletions(-) create mode 100644 scripts/research/r0822_xsr_repro.py diff --git a/scripts/research/r0822_basis_calendar.py b/scripts/research/r0822_basis_calendar.py index 6955f8a..a3138b2 100644 --- a/scripts/research/r0822_basis_calendar.py +++ b/scripts/research/r0822_basis_calendar.py @@ -318,6 +318,14 @@ def certify(asset: str, wide: pd.DataFrame, vwide: pd.DataFrame, meta: pd.DataFr print(f" convergenza a scadenza |ln(F/indice)| ultima ora: " f"mediana {np.median(conv):.1f} bps, max {np.max(conv):.1f} bps (n={len(conv)})") + # (c-bis) scarto PERP vs INDICE: la gamba FvP lo assume piccolo, quindi si misura + pj = perp.reindex(fund.set_index("ts").index).dropna() + if len(pj) > 1000: + ii = fund.set_index("ts")["index"].reindex(pj.index) + pb = (np.log(pj / ii) * 1e4).replace([np.inf, -np.inf], np.nan).dropna() + print(f" perp vs indice: mediana {pb.median():+.1f} bps, p95 |scarto| " + f"{pb.abs().quantile(0.95):.1f} bps (n={len(pb):,}) — la gamba FvP e' sul PERP") + # (d) CROSS-CHECK INDIPENDENTE: il forward implicito nella catena opzioni xchk = cross_check_option_forward(asset, wide, ok) return dict(flat=fb, vol=vb, conv=conv, xchk=xchk) @@ -370,7 +378,8 @@ def cross_check_option_forward(asset: str, wide: pd.DataFrame, meta: pd.DataFram # 2. CURVA — front/back trimestrali, basis annualizzato, slope forward # ========================================================================== def curve_frame(asset: str, wide: pd.DataFrame, meta: pd.DataFrame, - fund: pd.DataFrame, roll_dte: int) -> pd.DataFrame: + fund: pd.DataFrame, roll_dte: int, + perp: pd.Series | None = None) -> pd.DataFrame: """Per ogni ora: front = trimestrale piu' vicino con dte >= roll_dte, back = il successivo. Ritorna prezzi, tau, basis annualizzati, slope forward e funding.""" ok = meta[meta.status == "ok"].sort_values("exp") @@ -408,6 +417,15 @@ def curve_frame(asset: str, wide: pd.DataFrame, meta: pd.DataFrame, out["tau_front_h"], out["tau_back_h"] = tf, tb out["index"] = F["index"].values out["f1h"] = F["f1h"].values + # gamba perp: il PERP CERTIFICATO, non l'indice (l'indice non si compra). + # fallback all'indice solo se il perp non copre l'ora, e lo si dichiara. + if perp is not None: + pp = perp.reindex(idx) + out["perp"] = pp.where(pp.notna(), out["index"]).values + out["perp_from_index"] = pp.isna().values + else: + out["perp"] = out["index"].values + out["perp_from_index"] = True # basis annualizzato di ciascuna gamba vs INDICE out["c_front"] = np.log(Ff / out["index"]) * HOURS_Y / tf out["c_back"] = np.log(Fb / out["index"]) * HOURS_Y / tb @@ -532,7 +550,7 @@ def run_strategy(cv: pd.DataFrame, ctx: dict, family: str, sig: str, turn = np.abs(dP).sum(axis=1) if family == "FvP": - perp_r = np.nan_to_num(cv["index"].pct_change().values, nan=0.0) + perp_r = np.nan_to_num(cv["perp"].pct_change().values, nan=0.0) f1h = np.nan_to_num(cv["f1h"].values, nan=0.0) wheld = held.sum(axis=1) # una sola gamba datata # long datato => short perp: guadagna -w*perp_r e INCASSA w*funding @@ -609,11 +627,14 @@ def main() -> None: # ---------------- STEP 2: certificazione ---------------- print("\n[2] CERTIFICAZIONE (regola 4: un edge su un book fermo non e' un edge)") - certs = {} + certs, perps = {}, {} for a in ASSETS: - perp = A.get(a, "1h").set_index(pd.DatetimeIndex( - pd.to_datetime(A.get(a, "1h")["datetime"], utc=True)))["close"] - certs[a] = certify(a, panels[a], vwides[a], metas[a], funds[a], perp) + pdf = A.get(a, "1h") + perps[a] = pd.Series( + pdf["close"].astype(float).values, + index=pd.DatetimeIndex(pd.to_datetime(pdf["datetime"], utc=True))).sort_index() + perps[a] = perps[a][~perps[a].index.duplicated()] + certs[a] = certify(a, panels[a], vwides[a], metas[a], funds[a], perps[a]) x = certs[a]["xchk"] if x.get("status") == "ok" and len(x["table"]): t = x["table"] @@ -628,7 +649,7 @@ def main() -> None: print("\n[3] Q1 — LA CURVA E' PREVEDIBILE?") curves = {} for a in ASSETS: - cv = curve_frame(a, panels[a], metas[a], funds[a], roll_dte=7) + cv = curve_frame(a, panels[a], metas[a], funds[a], roll_dte=7, perp=perps[a]) curves[a] = cv describe_curve(a, cv) @@ -665,7 +686,7 @@ def main() -> None: f" le conto lo stesso, al rialzo)") ctxs = {a: asset_ctx(panels[a]) for a in ASSETS} - cvcache = {(a, rl): curve_frame(a, panels[a], metas[a], funds[a], rl) + cvcache = {(a, rl): curve_frame(a, panels[a], metas[a], funds[a], rl, perp=perps[a]) for a in ASSETS for rl in ROLLS} pidx = {(a, rl): pair_idx(cvcache[(a, rl)], ctxs[a]) for a in ASSETS for rl in ROLLS} diff --git a/scripts/research/r0822_hl_exec.py b/scripts/research/r0822_hl_exec.py index 7ab27f1..28a2587 100644 --- a/scripts/research/r0822_hl_exec.py +++ b/scripts/research/r0822_hl_exec.py @@ -466,35 +466,54 @@ def main() -> None: print(f"\n[3] XS01 — soglia PUBBLICATA ~$20.000 (origine: stima a occhio, 'rumore arrotondamento')") Wxs, dret, C, cols, dif = xs01_pesi() print(f" prova d'identita' col sleeve ufficiale `_xsec_returns()`: max|diff| = {dif:.3e}") + from src.portfolio.sleeves import XS_CFG, _xsec_returns + off_xs = _xsec_returns() + m_off = metriche(off_xs.values, pd.DatetimeIndex(off_xs.index)) gross_xs = np.abs(Wxs).sum(axis=1) att = gross_xs > 1e-9 dW = np.abs(np.diff(Wxs, axis=0, prepend=np.zeros((1, Wxs.shape[1])))) tk_nz = dW[dW > 1e-9] + # DUE popolazioni di ordini, e vanno separate: il RIBILANCIAMENTO del segnale (ogni H=10 giorni, + # cambia il paniere 5+5) e la DERIVA del vol-target (ogni giorno, micro-aggiustamenti). + H = XS_CFG["H"] + is_reb = np.zeros(len(Wxs), bool); is_reb[::H] = True + reb = dW[is_reb]; reb = reb[reb > 1e-9] + drift = dW[~is_reb]; drift = drift[drift > 1e-9] + print(f" sleeve ufficiale `_xsec_returns()`: Sharpe {m_off['sharpe']:.2f} " + f"maxDD {m_off['maxdd']:.1f}% CAGR {m_off['cagr']:.1f}%") print(f" gambe simultanee: {int((np.abs(Wxs) > 1e-9).sum(axis=1)[att].max())} " f"(k=5 long + 5 short); lordo mediano {np.median(gross_xs[att]):.2f}x il capitale " f"(vol-target, cap 3x)") - print(f" peso mediano mosso per gamba per ribilanciamento: {np.median(tk_nz)*100:.2f}% " - f"del capitale; 10° pctl {np.percentile(tk_nz,10)*100:.2f}%") - print(f"\n ticket per gamba (mediano) e % di gambe ESEGUIBILI, per capitale allocato a XS01:") - print(f" {'cap XS01':>10}{'ticket med':>12}{'ticket p10':>12}" - f"{'>=$5 [D]':>10}{'>=$10 [C]':>11}{'lotto ko':>10}") + print(f"\n DUE popolazioni di ordini (separarle e' il punto: non hanno la stessa taglia):") + print(f" RIBILANCIAMENTO (ogni {H}g, cambia il paniere): n={len(reb)} " + f"peso mosso mediano {np.median(reb)*100:.2f}% p10 {np.percentile(reb,10)*100:.2f}%") + print(f" DERIVA vol-target (giornaliera) : n={len(drift)} " + f"peso mosso mediano {np.median(drift)*100:.3f}% p10 {np.percentile(drift,10)*100:.3f}%") + print(f" -> {100*len(drift)/max(len(reb)+len(drift),1):.0f}% degli ordini sono DERIVA, " + f"ed e' la deriva che il pavimento taglia.") + print(f"\n ticket per gamba e % di ordini RICHIESTI eseguibili, per capitale allocato a XS01") + print(f" (conto = capitale/0.15, perche' XS01 pesa 15% del book di ricerca):") + print(f" {'cap XS01':>10}{'conto @15%':>12}{'tick reb':>10}{'tick drift':>12}" + f"{'>=$5 [D]':>10}{'>=$10 [C]':>11}{'reb>=$10':>10}") R_xs = dret idx = C.index base = None for cap in CAPITALI: - med = np.median(tk_nz) * cap - p10 = np.percentile(tk_nz, 10) * cap pct5 = 100.0 * float((tk_nz * cap >= MIN_ORDER_ASSUNTO).mean()) pct10 = 100.0 * float((tk_nz * cap >= MIN_ORDER_HL).mean()) - lot = np.array([meta.loc[s, "lot_usd"] if s in meta.index else 0.0 for s in cols]) - lotko = 100.0 * float((tk_nz * cap < np.median(lot)).mean()) - print(f" {cap:>10,.0f}{med:>12.2f}{p10:>12.2f}{pct5:>9.0f}%{pct10:>10.0f}%{lotko:>9.1f}%") + pr10 = 100.0 * float((reb * cap >= MIN_ORDER_HL).mean()) + print(f" {cap:>10,.0f}{cap/0.15:>12,.0f}{np.median(reb)*cap:>10.2f}" + f"{np.median(drift)*cap:>12.2f}{pct5:>9.0f}%{pct10:>10.0f}%{pr10:>9.0f}%") lot_xs = np.array([float(meta.loc[s, "lot_usd"]) if s in meta.index else 0.0 for s in cols]) px_xs = np.array([float(meta.loc[s, "markPx"]) if s in meta.index else np.nan for s in cols]) fee_xs_mod = 0.001 / 2.0 # [D] cio' che il sleeve modella: 5 bps/lato - print(f"\n haircut di eseguibilita' (Sharpe modellato - Sharpe realistico), fee {fee_xs_mod*1e4:.1f} bps/lato:") + print(f"\n haircut di eseguibilita' (Sharpe modellato - Sharpe realistico), fee {fee_xs_mod*1e4:.1f} bps/lato.") + print(f" NB il 'Sh mod' qui INCLUDE la fee sulla deriva del vol-target, che lo sleeve " + f"ufficiale NON addebita") + print(f" (`turn=|dW|` prima dello scaling): e' il motivo per cui sta sotto il " + f"{m_off['sharpe']:.2f} pubblicato.") print(f" {'cap XS01':>10}{'Sh mod':>9}{'Sh $5 [D]':>11}{'Sh $10 [C]':>12}" - f"{'haircut $10':>13}{'esegui%':>9}") + f"{'haircut $10':>13}{'ordini fatti':>14}") for cap in CAPITALI: m0 = sim_libro(Wxs, R_xs, None, cap, None, None, None, fee_xs_mod) m5 = sim_libro(Wxs, R_xs, None, cap, MIN_ORDER_ASSUNTO, None, None, fee_xs_mod) @@ -506,9 +525,29 @@ def main() -> None: # ---------------------------------------------------------------- 4. XSR01 print(f"\n[4] XSR01 — soglia PUBBLICATA ~$5.000, gate 2026-10-23 con haircut a $5.000 <= 40%") - from scripts.live.paper_xsr import build_panel # stessa costruzione del monitor in produzione + from scripts.live.paper_xsr import FEE_LEG, MIN_ORDER, _book, _step, build_panel ts, dtx, Wx, Rx, rbx, sx = build_panel() idxx = pd.DatetimeIndex(dtx) + + # PROVA D'IDENTITA' col monitor IN PRODUZIONE: sim_libro deve riprodurre paper_xsr._step + # barra per barra. Senza questa, i numeri qui sotto sarebbero di un'altra macchina. + for mo in (None, MIN_ORDER): + bk = _book(5000.0, len(sx)) + ref = np.array([_step(bk, Wx[i], Rx[i], float(rbx[i]), mo) for i in range(len(Wx))]) + mine = sim_libro(Wx, Rx, rbx, 5000.0, mo, None, None, FEE_LEG)["net"] + print(f" identita' vs scripts/live/paper_xsr._step (min_order={mo}): " + f"max|diff| = {np.abs(ref - mine).max():.3e}") + assert abs(FEE_LEG - FEE_LEG_MODELLO) < 1e-12, "la config congelata di XSR01 e' cambiata" + assert abs(MIN_ORDER - MIN_ORDER_ASSUNTO) < 1e-12, "il min-order del monitor e' cambiato" + + # RICONCILIAZIONE col numero PUBBLICATO (1.82): la scoperta (r0725_statarb_basket_gate, + # basket_from_positions) addebita "2 gambe per coppia" = 50 alt + 50 BTC. Ma il demeaning + # ANNULLA la gamba BTC (sum dei pesi = 0): non si tradano 50 BTC, se ne trada UNA di taglia + # zero. Il monitor in produzione lo sa (`d_btc = |sum(w_new) - sum(w_held)|` ~ 0) -> il + # pubblicato SOVRASTIMA le fee di ~2x. + net_x2 = sim_libro(Wx, Rx, rbx, 5000.0, None, None, None, 2 * FEE_LEG_MODELLO)["net"] + print(f" riconciliazione col pubblicato 1.82: a fee RADDOPPIATA (= convenzione della " + f"scoperta, 2 gambe/coppia) Sharpe {metriche(net_x2, idxx)['sharpe']:.2f}") lot_x = np.array([float(meta.loc[s, "lot_usd"]) if s in meta.index else 0.0 for s in sx]) px_x = np.array([float(meta.loc[s, "markPx"]) if s in meta.index else np.nan for s in sx]) dWx = np.abs(np.diff(Wx, axis=0, prepend=np.zeros((1, Wx.shape[1])))) @@ -516,7 +555,7 @@ def main() -> None: print(f" gambe {len(sx)} lordo mediano {np.median(np.abs(Wx).sum(axis=1)):.2f}x " f"peso mosso mediano/gamba {np.median(tkx)*100:.3f}% p10 {np.percentile(tkx,10)*100:.3f}%") print(f"\n {'cap':>9}{'ticket med':>12}{'Sh mod':>9}{'Sh $5 [D]':>11}{'Sh $10 [C]':>12}" - f"{'haircut $5':>12}{'haircut $10':>13}{'esegui%':>9}") + f"{'haircut $5':>12}{'haircut $10':>13}{'ordini fatti':>14}") hc10_5000 = np.nan for cap in CAPITALI: m0 = sim_libro(Wx, Rx, rbx, cap, None, None, None, FEE_LEG_MODELLO) diff --git a/scripts/research/r0822_skew.py b/scripts/research/r0822_skew.py index 48648fb..37b2dec 100644 --- a/scripts/research/r0822_skew.py +++ b/scripts/research/r0822_skew.py @@ -643,6 +643,12 @@ def main() -> None: except Exception as ex: print(f" non girato: {ex}") + imp = A.implausible_sharpe(bs) + print(f"\n implausible_sharpe sulla cella migliore: implausible={imp['implausible']} " + f"reasons={imp.get('reasons')}") + print(f" (Sharpe {imp.get('sharpe')}, maxDD {imp.get('maxdd')}, " + f"perdite sulle barre ATTIVE {imp.get('loss_frac')}, attive {imp.get('active_frac')})") + print("\n causalita' (tagli DENTRO la finestra dello skew, non all'80% della storia prezzi):") for a in ASSETS: f_, w_, sg_ = best.split("|") @@ -798,6 +804,37 @@ def main() -> None: P[c] = P[c].round(3) print(P.to_string(index=False)) + # ------------------------------------------------------------ 5. SOMMARIO + rule("5. SOMMARIO — cosa regge, cosa no, e cosa non e' misurabile con 75 giorni") + print(""" + Q1 lo skew NON anticipa lo spot: e' lo spot che muove lo skew. + Il prezzo precede il RR con t da 3.1 a 9.4 su 8 test su 8 e su entrambi gli asset; + in avanti il |t| massimo su 16 test e' 2.35 contro un massimo atteso dal rumore di 2.08, + non replica sul secondo asset, e l'unica cella apparente perde i due terzi togliendo + 4 giorni di rally su 24. La strategia giornaliera migliore su 18 celle fa Sharpe +2.61 + con IC95% [-2.90, +5.86] e DSR 0.163 -> SCARTATO. + + Q2 gate di rischio: NON MISURABILE su questa finestra, e la ragione e' strutturale. + Nei 75 giorni TP01 e' attivo 5 giorni su 75 e il suo maxDD e' 0.14%: non c'e' sinistro + da prevenire. Cio' che si vede: alle soglie z<-1.0 e z<-1.5 il gate spegne giorni in cui + TP01 era GIA' flat nel 100% dei casi -> 5a conferma della ridondanza col trend, ma su un + campione senza potenza. Il null de-levering gira e dice REFUTED per assenza di DD, non + per merito del baseline. + + Q3 il risultato della sessione, e non e' una strategia: e' una CORREZIONE DI PREZZATURA. + Il f=0.73 di VRP01 e' replicato in modo indipendente (0.714 su 22 ingressi, 0.723 su 2.582 + ore) e poi SCOMPOSTO. La causa maggiore NON e' lo skew: + struttura a termine 42% | spread denaro-lettera 30% | skew 25% | fit 2% + Il sleeve prezza un'opzione a 7 giorni con il DVOL a 30 giorni, che nel 90% delle ore sta + ~3 punti di vol SOPRA l'ATM a 7 giorni -> sovrapprezza entrambe le gambe. Il "+0.8pp sulla + gamba corta" pubblicato il 30/07 (qui +0.20pp) e' la CANCELLAZIONE di due errori da ~3pp: + -2.97pp di termine e +2.79pp di skew. Guardando le gambe contro l'ATM a 7 giorni si vede + che il modello sbaglia su ENTRAMBE, non solo sull'ala. + E il pezzo grande CAMBIA SEGNO col regime: f_term 0.83 nel quartile basso di IV-rank -> + 0.93 nel quartile alto, e 1.05 nelle ore in backwardation, mentre f_skew resta piatto + (0.93 -> 0.92). Cioe' l'errore piu' grande si annulla proprio dove il gate IV-rank fa + tradare il sleeve. +""") rule(f"fine — {time.time()-t_start:.0f}s") diff --git a/scripts/research/r0822_xsr_repro.py b/scripts/research/r0822_xsr_repro.py new file mode 100644 index 0000000..c387ae0 --- /dev/null +++ b/scripts/research/r0822_xsr_repro.py @@ -0,0 +1,431 @@ +"""XSR-REPRO — lo Sharpe 1.82 di XSR01 non si riproduce: e' un problema di CODICE, di DATI, o di LENTE? + +PERCHE'. XSR01 ha un gate di deploy PRE-REGISTRATO al 2026-10-23 (`r0725_xsr_deploy_gate.py`): +deploy solo se Sharpe forward >= 1.0 E haircut di eseguibilita' a $5.000 <= 40%. I suoi numeri di +ammissione — Sharpe netta 1.82, lorda 2.70, maxDD -2.6%, vol 2.3%, ret 4.2%/a, lag 1.82/1.19/0.81/ +0.51, Sharpe per anno 1.03/1.98/3.11 — sono in CLAUDE.md, nel diario del 25/07 e CABLATI nel gate +(`IS_SHARPE_NET = 1.82`). Il filone XS-LITE (22/08) ha misurato che sulla finestra identica a quella +di scoperta oggi escono 1.75 (lente "paniere") e 2.23 (lente "libro"): 1.82 non e' nessuna delle due. +Una decisione fra due mesi poggia su un numero che non si riproduce. + +Questo script NON giudica XSR01 e NON anticipa il gate del 23/10 (sarebbe selection-on-forward). +Stabilisce SOLO se il numero su cui si decidera' e' solido, in quest'ordine: + + T1 QUALE LENTE. Tre implementazioni convivono nel repo e danno numeri diversi sugli STESSI dati: + L1 `r0725_statarb_basket_gate.basket_from_positions(demean=True)` — media per riga sulle sole + colonne NON-NaN (divisore variabile). E' la lente che ha girato i gate (marginale, DSR). + L2 `r0725_statarb_demean_skeptic.ret_from_pos` — P.fillna(0) e divisore FISSO 50. + L3 `scripts/live/paper_xsr.build_panel` + `_step` — libro con capitale, pesi Q/50, gamba BTC + esplicita. E' quella che alimenta il gate. + T2 QUALE STATO DEL DATO. `data/raw` e' gitignored e il cron riscrive i parquet HL ogni notte. + Ipotesi da falsificare: "stesso codice, dati diversi" (identico allo scoperto GTAA/TLT 07/08). + Prove indipendenti: (a) il log del cron registra `reali`/`bfill`/`start_reale` per simbolo a + ogni giro -> se il conteggio delle barre PASSATE e' cambiato, il feed e' stato riscritto; + (b) lo Sharpe per ANNO CHIUSO (2024, 2025) e' un'impronta della storia: se la storia e' + stabile deve riprodursi al centesimo attraverso 4 settimane di riscritture. + T3 IL MONITOR FORWARD. `paper_xsr` e' forward-only e append-only, quindi in teoria immune alla + riscrittura. Test diretto: replay del libro MODELED sui dati di OGGI dall'inception, barra per + barra, contro cio' che il monitor ha REGISTRATO giorno per giorno. Se la storia fosse stabile + e la lente giusta, le due serie devono coincidere. + T4 L'HAIRCUT A $5.000, l'altra meta' del gate: dipende dal min-order del venue. Il numero + pubblicato ($14.41 di ticket, soglia 40%) assume min-order $5 (Deribit). Hyperliquid potrebbe + essere $10. Si misura la sensibilita'. + +COSA MI ASPETTAVO PRIMA DI MISURARE: che fosse la lente (tre implementazioni, nessuna nominata nel +diario) e che il dato fosse stabile, perche' il log del cron mostra bar-count che crescono di 1/g. +Solo la prima meta' e' risultata vera. + + nice -n 19 timeout 900 uv run python scripts/research/r0822_xsr_repro.py +""" +from __future__ import annotations + +import json +import re +import sys +from collections import OrderedDict +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +RAW = ROOT / "data" / "raw" +for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"): + sys.path.insert(0, str(_p)) + +from r0725_statarb_multi import BASE, MIN_BARS, _dd, _sh, load_hl, signal, universe # noqa: E402 +from r0725_statarb_basket_gate import basket_from_positions, pair_frames # noqa: E402 +from r0725_statarb_demean_skeptic import ret_from_pos # noqa: E402 + +ANN = np.sqrt(365.0) +DISCOVERY = pd.Timestamp("2026-07-25", tz="UTC") # ultima barra vista dalla scoperta +CRONLOG = ROOT / "logs" / "cron_daily.log" +FWD = ROOT / "data" / "paper_xsr" / "returns.jsonl" +FWD_STATE = ROOT / "data" / "paper_xsr" / "state.json" + +# I NUMERI PUBBLICATI (CLAUDE.md + diario 2026-07-25 + docstring di paper_xsr / del gate) +PUB = dict(netta=1.82, lorda=2.70, dd=-2.6, vol=2.3, ret=4.2, + lag=(1.82, 1.19, 0.81, 0.51), anni=(1.03, 1.98, 3.11)) + + +def _cut(df, ts): + return df[df.index <= ts] + + +def _stats(r: pd.Series) -> dict: + return dict(n=len(r), sh=_sh(r.values), dd=_dd(r.values) * 100, + vol=float(r.std() * ANN * 100), ret=float(r.mean() * 365 * 100)) + + +# ------------------------------------------------------------------ pannelli +def panel(partial_last: bool = False, cut: pd.Timestamp | None = None): + """(P, S) come `pair_frames`, ma con due gradi di liberta' espliciti: + `cut` -> ultima barra inclusa (troncamento dell'INPUT, non dell'output); + `partial_last` -> l'ultima barra e' resa PARZIALE (close := open), cioe' com'era il giorno in + cui il cron l'aveva appena scritta. E' l'unico stato del dato che questo + script NON puo' leggere da disco: va ricostruito.""" + def px(sym): + d = pd.read_parquet(RAW / f"hl_{sym.lower()}_1d.parquet") + ix = pd.to_datetime(d["timestamp"], unit="ms", utc=True) + c = pd.Series(d["close"].astype(float).values, index=ix).sort_index() + o = pd.Series(d["open"].astype(float).values, index=ix).sort_index() + c = c[~c.index.duplicated(keep="last")] + o = o[~o.index.duplicated(keep="last")] + if cut is not None: + c, o = c[c.index <= cut], o[o.index <= cut] + if partial_last and len(c): + c.iloc[-1] = o.iloc[-1] + return c + + base = px(BASE) + pc, sc = {}, {} + for sym in universe(): + try: + tgt = px(sym) + except FileNotFoundError: + continue + ix = base.index.intersection(tgt.index) + if len(ix) < MIN_BARS: + continue + p, s = signal(base[ix], tgt[ix]) + pc[sym] = pd.Series(p, index=ix) + sc[sym] = pd.Series(s, index=ix) + P = pd.concat(pc, axis=1, sort=True).sort_index() + S = pd.concat(sc, axis=1, sort=True).sort_index() + return P, S + + +def lens_L1(P, S): + return basket_from_positions(P, S, demean=True) + + +def lens_L2(P, S, fee_leg=0.0005, lag=0): + P0 = P.fillna(0.0) + S0 = S.reindex(P0.index)[P0.columns] + return ret_from_pos(P0, S0, fee_leg=fee_leg, lag=lag) + + +# ------------------------------------------------------------------ T1 +def t1_lenti(): + print("=" * 100) + print(" T1 — QUALE LENTE, E QUALE STATO DEL DATO, PRODUCE 1.82") + print("=" * 100) + P, S = pair_frames() + print(f"\n dato su disco oggi: {P.shape[1]} coppie, {len(P)} barre " + f"({P.index[0].date()} -> {P.index[-1].date()})") + + rows = [] + for tag, (Pi, Si) in [ + ("OGGI pieno", (P, S)), + (f"tronc. {DISCOVERY.date()} (barra completa)", (_cut(P, DISCOVERY), _cut(S, DISCOVERY))), + ]: + rows.append((f"L1 gate {tag}", _stats(lens_L1(Pi, Si)))) + rows.append((f"L2 scett. {tag}", _stats(lens_L2(Pi, Si)))) + + Pp, Sp = panel(partial_last=True, cut=DISCOVERY) + rows.append(("L1 gate STATO DEL 25/07 (ultima barra PARZIALE)", _stats(lens_L1(Pp, Sp)))) + rows.append(("L2 scett. STATO DEL 25/07 (ultima barra PARZIALE)", _stats(lens_L2(Pp, Sp)))) + + print(f"\n {'lente / stato del dato':<52}{'n':>6}{'Sharpe':>9}{'maxDD':>9}{'vol':>8}{'ret/a':>8}") + for nm, s in rows: + print(f" {nm:<52}{s['n']:>6}{s['sh']:>9.4f}{s['dd']:>8.2f}%{s['vol']:>7.2f}%{s['ret']:>7.2f}%") + print(f" {'--- PUBBLICATO 2026-07-25':<52}{'937?':>6}{PUB['netta']:>9.2f}" + f"{PUB['dd']:>8.1f}%{PUB['vol']:>7.1f}%{PUB['ret']:>7.1f}%") + + # confronto completo sugli OTTO numeri pubblicati, per la sola combinazione che li riproduce + print("\n --- verifica completa sullo stato che riproduce: L2 + ultima barra PARZIALE ---") + r = lens_L2(Pp, Sp) + g = lens_L2(Pp, Sp, fee_leg=0.0) + lags = tuple(_sh(lens_L2(Pp, Sp, lag=L).values) for L in (0, 1, 2, 3)) + anni = tuple(_sh(r[r.index.year == y].values) for y in (2024, 2025, 2026)) + st = _stats(r) + ok = 0 + def chk(nome, mio, pub, tol): + nonlocal ok + good = abs(mio - pub) <= tol + ok += good + print(f" {nome:<22} misurato {mio:>8.4f} pubblicato {pub:>7.2f} " + f"{'COINCIDE' if good else 'DIVERGE'}") + chk("Sharpe netta", st["sh"], PUB["netta"], 0.005) + chk("Sharpe lorda", _sh(g.values), PUB["lorda"], 0.005) + chk("maxDD %", st["dd"], PUB["dd"], 0.05) + chk("vol ann %", st["vol"], PUB["vol"], 0.05) + chk("ret ann %", st["ret"], PUB["ret"], 0.05) + for i, L in enumerate((0, 1, 2, 3)): + chk(f"lag +{L}g", lags[i], PUB["lag"][i], 0.006) + for i, y in enumerate((2024, 2025, 2026)): + chk(f"Sharpe {y}", anni[i], PUB["anni"][i], 0.005) + print(f"\n -> {ok}/12 numeri pubblicati riprodotti da L2 sullo stato del dato del 25/07.") + + # la lente che ha girato i GATE non e' quella del titolo + print("\n --- e la lente che ha girato i gate (marginale, deflated-Sharpe) e' L1, non L2 ---") + print(f" L1 sullo stato del 25/07 : Sharpe {_sh(lens_L1(Pp, Sp).values):.4f}") + print(" docstring di r0725_statarb_demean_skeptic.py, riga 5, cita il gate come " + "\"Sharpe 1.79\"") + print(" -> il numero di TITOLO (1.82, L2) e i numeri di GATE (DSR/marginale, L1) non sono") + print(" mai stati la stessa serie.") + + # deflated-Sharpe delle 3 varianti pre-registrate, oggi, sulle due lenti + try: + from altlib import deflated_sharpe + from src.portfolio.sleeves import XS_UNIVERSE + maj = [s for s in XS_UNIVERSE if s != BASE] + Pc, Sc = _cut(P, DISCOVERY), _cut(S, DISCOVERY) + var = {"V1 ALL50": dict(cols=None, demean=False), + "V2 MAJ19": dict(cols=maj, demean=False), + "V3 DEMEAN": dict(cols=None, demean=True)} + ser = {k: basket_from_positions(Pc, Sc, **v) for k, v in var.items()} + srs = [_sh(v.values) for v in ser.values()] + print("\n --- deflated-Sharpe delle 3 varianti pre-registrate, L1, finestra 25/07 ---") + for k, v in ser.items(): + d, nm = deflated_sharpe(_sh(v.values), srs, v.values, dpy=365.0) + print(f" {k:<11} Sharpe {_sh(v.values):>5.2f} DSR {d:>6.3f} " + f"(max atteso dal null {nm:>5.2f}) {'PASS' if d >= 0.95 else 'SOTTO 0.95'}") + print(" (pubblicato per V3: DSR 0.985)") + except Exception as e: # pragma: no cover + print(f" [DSR non calcolabile: {e.__class__.__name__}: {e}]") + return P, S + + +# ------------------------------------------------------------------ T2 +def t2_feed(P, S): + print("\n" + "=" * 100) + print(" T2 — IL FEED HL E' STATO RISCRITTO? (l'ipotesi principale: stesso codice, dati diversi)") + print("=" * 100) + + # (a) il log del cron registra reali/bfill/start_reale a ogni giro: e' un registro storico + runs, cur, inblk, hdr = OrderedDict(), None, False, None + for ln in CRONLOG.read_text(errors="replace").splitlines(): + m = re.match(r"^===== (\S+) cron_daily =====", ln) + if m: + cur, inblk = m.group(1), False + runs[cur] = {} + continue + if "FETCH + CERTIFY Hyperliquid" in ln: + inblk, hdr = True, None + continue + if inblk and cur is not None: + if ln.strip().startswith("sym"): + hdr = ln.split() + continue + f = ln.split() + if hdr and len(f) >= 7 and re.match(r"^[A-Z0-9]+$", f[0]) and f[1].isdigit(): + runs[cur][f[0]] = ((int(f[1]), f[3], f[-1]) if len(hdr) >= 9 + else (int(f[1]), f[2], f[-1])) + elif runs[cur] and (not ln.strip() or ln.startswith("=")): + inblk = False + keys = [k for k, v in runs.items() if v] + a = next((k for k in keys if k.startswith("2026-07-25")), None) + b = keys[-1] if keys else None + print(f"\n (a) registro del cron: {len(keys)} giri con blocco Hyperliquid, " + f"da {keys[0][:10]} a {keys[-1][:10]}") + if a and b: + A, B = runs[a], runs[b] + atteso = (pd.Timestamp(b[:10]) - pd.Timestamp(a[:10])).days + anom = [] + for s in sorted(set(A) | set(B)): + ra, rb = A.get(s), B.get(s) + if ra is None or rb is None: + anom.append((s, "presente in un solo giro", ra, rb)); continue + if rb[0] - ra[0] != atteso or ra[1] != rb[1]: + anom.append((s, f"delta {rb[0]-ra[0]} invece di {atteso}; start {ra[1]}->{rb[1]}", + ra, rb)) + print(f" confronto {a[:10]} vs {b[:10]} su {len(set(A)|set(B))} simboli, " + f"crescita attesa {atteso} barre") + if not anom: + print(" -> TUTTI i simboli crescono di esattamente il numero di giorni e " + "start_reale INVARIATO") + for s, why, ra, rb in anom: + print(f" {s:<8} {why} (verdetto oggi: {rb[2] if rb else '-'})") + print(" NB: i simboli con delta 0 sono quelli con verdetto `scarta` (serie ferma sul") + print(" venue): NON producono un parquet, quindi non entrano nell'universo.") + + # (b) impronta indipendente della storia: lo Sharpe degli ANNI CHIUSI + Pp, Sp = panel(partial_last=True, cut=DISCOVERY) + r_then = lens_L2(Pp, Sp) + r_now = lens_L2(P, S) + print("\n (b) impronta della storia — Sharpe degli anni CHIUSI, che una riscrittura sposterebbe:") + print(f" {'anno':<8}{'stato 25/07':>14}{'stato oggi':>13}{'pubblicato':>13}{'barre':>8}") + for y, pub in zip((2024, 2025, 2026), PUB["anni"]): + s1 = r_then[r_then.index.year == y] + s2 = r_now[r_now.index.year == y] + note = " <- anno IN CORSO: cambia perche' contiene barre nuove" if y == 2026 else "" + print(f" {y:<8}{_sh(s1.values):>14.4f}{_sh(s2.values):>13.4f}{pub:>13.2f}" + f"{len(s2):>8}{note}") + print(" -> 2024 e 2025 (731 barre) si riproducono al centesimo attraverso 4 settimane di") + print(" riscritture notturne: la STORIA CHIUSA e' stabile.") + + # (c) l'unica barra che cambia: l'ultima + d = pd.read_parquet(RAW / "hl_btc_1d.parquet") + vol = d["volume"].astype(float).values + q = float(vol[-1] / np.median(vol[-40:-1])) + print("\n (c) l'unica barra che NON e' definitiva e' l'ULTIMA. `fetch_hyperliquid` gira nel") + print(" cron delle 00:30 con END = oggi, quindi scrive la barra del giorno IN CORSO:") + print(f" hl_btc_1d ultima barra {pd.Timestamp(int(d['timestamp'].iloc[-1]), unit='ms', tz='UTC')}" + f" volume {vol[-1]:,.0f} = {q*100:.1f}% del volume mediano di un giorno pieno") + print(f" (una barra piena di ~24h; questa ne ha ~{q*24*60:.0f} minuti di scambi)") + print("\n VERDETTO T2: il feed VIENE riscritto, ma NON come ipotizzato. Non c'e' riscrittura") + print(" arbitraria della storia: c'e' UNA barra provvisoria per volta — l'ultima — che il giro") + print(" successivo completa. Ogni numero calcolato su dato HL il giorno D contiene una barra") + print(" di ~30-40 minuti travestita da giorno.") + + +# ------------------------------------------------------------------ T3 +def t3_monitor(): + print("\n" + "=" * 100) + print(" T3 — IL MONITOR FORWARD (che alimenta il gate del 23/10) REGISTRA CIO' CHE CREDE?") + print("=" * 100) + if not FWD.exists() or not FWD_STATE.exists(): + print(" [stato di paper_xsr assente: non misurabile]") + return + import importlib.util + spec = importlib.util.spec_from_file_location("paper_xsr_ro", ROOT / "scripts" / "live" / "paper_xsr.py") + px = importlib.util.module_from_spec(spec) + spec.loader.exec_module(px) # sola lettura: non si chiama main() + + rec = [json.loads(x) for x in FWD.read_text().splitlines() if x.strip()] + st = json.loads(FWD_STATE.read_text()) + ts, dt, W, R, rb, syms = px.build_panel() + print(f"\n inception {pd.Timestamp(st['start_ts'], unit='ms', tz='UTC').date()} " + f"barre registrate {len(rec)} universo {len(syms)} " + f"(uguale a quello congelato: {syms == st['syms']})") + + i0 = int(np.where(ts == st["start_ts"])[0][0]) + bk = px._book(px.MODELED_CAPITAL, len(syms)) + ric = {} + for i in range(i0 + 1, len(ts)): + ric[int(ts[i])] = px._step(bk, W[i], R[i], float(rb[i]), None) + + a = np.array([r["net_modeled"] for r in rec if r["ts"] in ric]) + b = np.array([ric[r["ts"]] for r in rec if r["ts"] in ric]) + same = int((np.abs(a - b) <= 1e-6).sum()) + print(f"\n replay del libro MODELED sui dati di OGGI, dall'inception, contro cio' che il") + print(f" monitor ha REGISTRATO giorno per giorno ({len(a)} barre appaiate):") + print(f" barre identiche entro 1e-6 : {same}/{len(a)}") + print(f" correlazione registrato/ricalcolato: {np.corrcoef(a, b)[0, 1]:+.4f}") + print(f" vol annualizzata REGISTRATA : {a.std()*ANN*100:.2f}%") + print(f" vol annualizzata RICALCOLATA : {b.std()*ANN*100:.2f}%") + print(f" (la strategia e' progettata a vol ~{PUB['vol']:.1f}%: la seconda e' quella giusta)") + frac = (a.std() / b.std()) ** 2 + print(f" rapporto di varianza {(a.std()/b.std())**2:.4f} -> in radice di tempo il monitor") + print(f" sta misurando ~{frac*24*60:.0f} minuti di mercato al giorno, non 24 ore.") + print("\n MECCANISMO (leggibile nel codice, non dedotto): `advance()` processa le barre con") + print(" ts > last_ts e poi porta last_ts sull'ULTIMA di esse. Al giro delle 00:30 del giorno D") + print(" l'ultima barra e' quella di D, appena scritta e lunga ~30 minuti: il monitor ne") + print(" registra il rendimento e SPOSTA last_ts oltre. Quando il giro successivo trova la") + print(" barra di D completata, D e' gia' passato. Le 23 ore e mezza restanti di OGNI giorno") + print(" non entrano in nessun rendimento registrato.") + sh_a = float(a.mean() / a.std() * ANN) + sh_b = float(b.mean() / b.std() * ANN) + se = float(ANN / np.sqrt(len(a)) / np.sqrt(365) * np.sqrt(365)) + se = float(np.sqrt(365.0 / len(a))) + print(f"\n Sharpe della finestra forward: REGISTRATO {sh_a:+.2f} RICALCOLATO {sh_b:+.2f}") + print(f" ⚠ NESSUNO DEI DUE E' UNA LETTURA DEL GATE. Con {len(a)} barre l'errore standard di") + print(f" uno Sharpe annualizzato e' ~{se:.2f}: entrambi i numeri sono indistinguibili da 0 e") + print(" dalla soglia 1.0. Il gate e' il 2026-10-23 e questo script non lo anticipa; qui") + print(" servono solo a mostrare che i due strumenti non misurano la stessa grandezza.") + + +# ------------------------------------------------------------------ T4 +def t4_haircut(P, S): + print("\n" + "=" * 100) + print(" T4 — L'ALTRA META' DEL GATE: l'haircut di eseguibilita' a $5.000 e il min-order") + print("=" * 100) + import importlib.util + spec = importlib.util.spec_from_file_location("paper_xsr_ro2", ROOT / "scripts" / "live" / "paper_xsr.py") + px = importlib.util.module_from_spec(spec) + spec.loader.exec_module(px) + ts, dt, W, R, rb, syms = px.build_panel() + n = len(syms) + + def replay(cap0, min_order): + bk = px._book(cap0, n) + out = [] + for i in range(len(ts)): + out.append(px._step(bk, W[i], R[i], float(rb[i]), min_order)) + r = pd.Series(out, index=pd.to_datetime(ts, unit="ms", utc=True)) + tot = float(np.prod(1 + r.values) - 1) + fill = bk["n_fill"] / max(bk["n_fill"] + bk["n_skip"], 1) + return r, tot, fill + + print("\n replay dell'INTERA storia in-sample (non della finestra forward: quella e' del gate)") + print(f" {'libro':<28}{'Sharpe':>9}{'ret tot':>10}{'gambe eseguite':>17}{'haircut vs modeled':>21}") + rm, tot_m, _ = replay(5000.0, None) + print(f" {'MODELED (ribil. continuo)':<28}{_sh(rm.values):>9.2f}{tot_m*100:>9.1f}%" + f"{'100%':>17}{'-':>21}") + for mo in (5.0, 10.0, 25.0): + r5, tot5, fill = replay(5000.0, mo) + hc = (tot_m - tot5) / abs(tot_m) if tot_m else float("nan") + flag = "" if hc <= 0.40 else " <-- SOPRA LA GUARDIA 40%" + print(f" {f'REAL $5.000 min-order ${mo:.0f}':<28}{_sh(r5.values):>9.2f}{tot5*100:>9.1f}%" + f"{fill*100:>16.0f}%{hc*100:>20.1f}%{flag}") + + # ticket medio per gamba + dW = np.abs(np.diff(W, axis=0, prepend=np.zeros((1, n)))) + tick = dW[dW > 0] * 5000.0 + print(f"\n ticket per gamba a $5.000: mediano ${np.median(tick):.2f} medio ${tick.mean():.2f}") + print(f" (pubblicato: $14.41 medio) quota di ordini sotto $5: {(tick < 5).mean()*100:.0f}%" + f" sotto $10: {(tick < 10).mean()*100:.0f}%") + print("\n -> il min-order NON e' un dettaglio: e' la variabile che decide la seconda meta' del") + print(" gate, e il numero pubblicato assume $5. Se Hyperliquid e' a $10, la riga da leggere") + print(" il 23/10 e' la seconda, non la prima.") + + +def main() -> None: + P, S = t1_lenti() + t2_feed(P, S) + t3_monitor() + t4_haircut(P, S) + print("\n" + "=" * 100) + print(" CONTROMISURA PROPOSTA (non implementata: questo script non tocca produzione)") + print("=" * 100) + print(""" + Il difetto strutturale e' che `data/raw` e' gitignored e ogni numero di ammissione basato su + hl_* e' irriproducibile per costruzione. La contromisura minima, nella forma che il progetto usa + gia' altrove (guardia `TRONCATO`/`STORIA-CORTA` di `fetch_ib_equities.certify`): + + 1. IMPRONTA PER FINESTRA CHIUSA. A ogni giro, il certificatore HL scrive in un file versionato + (non in data/raw) `sha256` + conteggio barre + primo/ultimo timestamp per simbolo, calcolati + SOLO sulle barre fino a ieri (finestra chiusa). Un test confronta l'impronta di una finestra + gia' registrata: se cambia, la storia e' stata riscritta e il test rompe. Costa ~50 hash/giorno. + 2. LA BARRA IN CORSO VA ETICHETTATA, NON NASCOSTA. `load_hl` (e chiunque legga hl_*) deve poter + chiedere solo le barre CHIUSE. La forma minima e' un flag esplicito con default sicuro; la + forma che il progetto preferisce e' derivare la regola dal codice sorvegliato invece di + ridichiararla in ogni consumatore. + 3. I MONITOR FORWARD NON DEVONO CONSUMARE LA BARRA IN CORSO. `advance()` deve fermarsi + all'ultima barra CHIUSA; oggi ne consuma una provvisoria e sposta `last_ts` oltre, perdendo + il 98% di ogni giornata. E' un bug di produzione, non una scelta di modellazione. + + RISULTATI PUBBLICATI CHE POGGIANO SU hl_* (tutti da citare con questa riserva): + XS01 (sleeve nel book di ricerca al 15%) — 19 major HL 1d + XSR01 (candidato, gate 23/10) — 50 alt HL 1d + STATARB-MULTI (falsificazione 25/07) — 50 coppie alt/BTC HL + XS-LITE (22/08) — stesse serie + CC01 / breadth alt / dispersione XDISP — stesse serie + Per i BACKTEST l'effetto e' una barra su ~965 (piccolo ma non nullo: qui vale 1.82 -> 1.78). + Per i MONITOR FORWARD l'effetto e' totale, perche' la barra provvisoria e' l'UNICA che leggono. +""") + + +if __name__ == "__main__": + main() From f0122d97a1cde727fe25c8afdb73818d17cbd207 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:24:23 +0000 Subject: [PATCH 012/114] =?UTF-8?q?research(wave-0822):=20VOL-SIZE=20?= =?UTF-8?q?=E2=80=94=20il=20null=20di=20permutazione=20si=20cita=20col=20s?= =?UTF-8?q?egno,=20non=20col=20decimale=20(gira=20su=203=20ancore=20su=202?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 8 +++++-- scripts/research/r0822_basis_calendar.py | 29 ++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index fc3385a..1ecf1b1 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -266,8 +266,12 @@ zeri nei due canali, il guadagno FULL viene dalla **FREQUENZA** (il solo filtro fortuna d'ancora del libro vale **+0,196 FULL / +0,448 HOLD**. - Cella congelata del LEAD (`VTL tv20 w90`): size fissata **all'ingresso** = `clip(0,20 / rv90(t−1), 0, 3)`. ISO dShFULL **+0,070 (23/23 ancore)**, dShHOLD +0,101 (19/23), - **8/8 anni positivi**, null di permutazione p~0,027 (e la mediana del null e' **−0,04**: - riassegnare a caso la stessa distribuzione di size **danneggia**). + **8/8 anni positivi**, **null di permutazione superato in ENTRAMBE le terne di ancore provate, + p <~ 0,03** (mediana del null −0,03/−0,05: riassegnare a caso la stessa distribuzione di size + **danneggia**). ⚠️ **Autocorrezione dell'agente, da rispettare:** quel null gira su **3 ancore + delle 23** per costo, e la TAGLIA dell'effetto varia di **3x** fra due terne diverse della + stessa banda (+0,033 vs +0,111 sulla stessa cella) -> **i p-value non si citano a tre decimali**: + il numero da mettere a verbale e' il **segno**. Il resto del report e' su 23 ancore piene. - ⚠️ `DSR = 1,000` dichiarato **VACUO, non PASS**: lo passa anche il baseline — su una famiglia di perturbazioni della stessa strategia il deflated-Sharpe non ha potenza. Onesta' rara. - ⚠️ Punto piu' fragile, dichiarato: l'asse della finestra e' un **picco, non un plateau** diff --git a/scripts/research/r0822_basis_calendar.py b/scripts/research/r0822_basis_calendar.py index a3138b2..a3199b1 100644 --- a/scripts/research/r0822_basis_calendar.py +++ b/scripts/research/r0822_basis_calendar.py @@ -514,7 +514,7 @@ def signal_w(cv: pd.DataFrame, family: str, sig: str, win_h: int, thr: float) -> def run_strategy(cv: pd.DataFrame, ctx: dict, family: str, sig: str, win_h: int, thr: float, slip_bps: float, - fee_side: float = FEE_SIDE, fi=None, bi=None) -> pd.Series: + fee_side: float = FEE_SIDE, fi=None, bi=None, lag: int = 1) -> pd.Series: """Ritorno ORARIO netto per $1 di nozionale LORDO PER GAMBA. Le posizioni vivono in spazio CONTRATTO: quando la coppia cambia (roll) la variazione @@ -541,7 +541,7 @@ def run_strategy(cv: pd.DataFrame, ctx: dict, family: str, sig: str, P[rows[m], fi[m]] = w[m] held = np.zeros_like(P) - held[1:] = P[:-1] # deciso a t, tenuto in t->t+1 + held[lag:] = P[:-lag] if lag else P # deciso a t, tenuto in t->t+lag gross = (held * ctx["R"]).sum(axis=1) dP = np.zeros_like(P) @@ -794,6 +794,31 @@ def main() -> None: except Exception: # noqa: BLE001 pass + # ---------------- STEP 6b: RITARDO D'ESECUZIONE (il test decisivo qui) ---------------- + print("\n[6b] RITARDO D'ESECUZIONE — il test decisivo di questo filone") + print(" Un datato lontano ha prezzi STANTII: una mean-reversion su una serie stantia") + print(" mostra profitti FINTI (il prezzo 'reverte' solo perche' si aggiorna in ritardo).") + print(" Un edge di STRUTTURA A TERMINE e' lento e sopravvive a un'ora in piu' di ritardo;") + print(" un artefatto di microstruttura muore. (Stessa lente del decadimento di XSR01.)") + lag_tab = [] + for lg in [1, 2, 3, 6, 12, 24]: + hs = [] + for a in ASSETS: + f_i, b_i = pidx[(a, int(b.roll))] + hs.append(run_strategy(cvcache[(a, int(b.roll))], ctxs[a], b.family, b.sig, + int(b.win), float(b.thr), slip_est, fi=f_i, bi=b_i, lag=lg)) + sl_ = stats(to_daily(pd.concat(hs, axis=1).fillna(0.0).mean(axis=1))) + lag_tab.append((lg, sl_["sharpe"], sl_["cagr"])) + print(f" ritardo {lg:2d}h -> Sharpe {sl_['sharpe']:+6.2f} CAGR {sl_['cagr']*100:+6.2f}%") + s1 = lag_tab[0][1]; s2 = lag_tab[1][1] + if s1 > 0: + keep = s2 / s1 + print(f" con UNA sola ora in piu' resta il {keep*100:.0f}% dello Sharpe " + f"({s1:+.2f} -> {s2:+.2f})") + print(" -> " + ("compatibile con un edge lento di struttura a termine" + if keep > 0.6 else + "CROLLO: firma di microstruttura/prezzo stantio, NON di term structure")) + # ---------------- STEP 7: quanto spread uccide l'edge ---------------- print("\n[7] A CHE SPREAD MUORE L'EDGE (la stima di slippage e' il rischio #1)") for sl in [0.0, 1.0, 2.5, 5.0, 7.5, 10.0, 20.0]: From ea22a0e20c920d2371cb4a1f21255fa778383f6a Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:29:19 +0000 Subject: [PATCH 013/114] =?UTF-8?q?research(wave-0822):=20XSR-REPRO=20trov?= =?UTF-8?q?a=20un=20difetto=20di=20produzione=20(i=20monitor=20forward=20r?= =?UTF-8?q?egistrano=20~41=20min/giorno);=20SKEW=20scompone=20il=20f=20di?= =?UTF-8?q?=20VRP01=20=E2=80=94=20il=2042%=20e'=20struttura=20a=20termine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 80 ++++ scripts/research/r0822_alt_options.py | 530 +++++++++++++++++++++ scripts/research/r0822_basis_calendar.py | 106 +++-- scripts/research/r0822_hl_exec.py | 83 +++- scripts/research/r0822_leverage_skeptic.py | 175 ++++++- scripts/research/r0822_prop_alloc.py | 44 +- scripts/research/r0822_skew.py | 3 - scripts/research/r0822_slip_audit.py | 509 ++++++++++++++++++++ scripts/research/r0822_xsr_repro.py | 8 +- 9 files changed, 1445 insertions(+), 93 deletions(-) create mode 100644 scripts/research/r0822_alt_options.py create mode 100644 scripts/research/r0822_slip_audit.py diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 1ecf1b1..e13f37b 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -15,6 +15,8 @@ null de-levering superato + eseguibilita' al capitale dichiarato. | 7 | TERM-STRUCTURE | **SCARTATO** + 1 LEAD stretto (decisione 2027-02-01) | la pendenza e' un **termometro contemporaneo** (picco CCF a lag −1h/0h); il carry non paga il bid-ask (**18,6 giorni di roll-down per pagare UN round-trip**); come gate e' la **5ª occorrenza** della ridondanza col trend | | 6 | OI-PIN | **SCARTATO** | il max-pain batte uno strike casuale ma **non batte mai (0/24) la media a 7 giorni dello spot**, un livello che non usa NESSUN dato di opzioni: non e' pinning, e' reversione verso il centro recente con l'OI come stimatore rumoroso di quel centro | | 8 | VOL-SIZE | **LEAD** (gate 2026-12-22) + 1 falsificazione | dare a SKH01 una size per-trade regge a **23/23 ancore**, 8/8 anni, null di permutazione e trasferimento su V1 — ma vale **+0,07 di Sharpe di libro, un terzo della fortuna d'ancora del libro stesso (+0,196)**. Il vol-target di **libro** e' invece falsificato: compra peso SKH gia' respinto e peggiora l'eseguibilita' | +| — | **XSR-REPRO** (integrita') | 🚨 **DIFETTO DI PRODUZIONE** | il numero 1.82 e' SPIEGATO e non era sbagliato (era su una **terza** lente, e su una barra non ancora chiusa) — ma cercandone la causa e' emerso che **`paper_xsr` registra ~41 minuti di mercato al giorno**, non un giorno. Tre gate pre-registrati leggono serie costruite cosi' | +| 5 | SKEW | **SCARTATO** (Q1, Q2) + **LEAD** (Q3, gate 2027-02-22) | il prezzo muove lo skew (t 3,1-9,4 su 8/8 test), **non il contrario** (max |t| in avanti 2,35 contro 2,08 atteso dal rumore). Ma Q3 e' grosso: **il f=0,73 di VRP01 e' per il 42% STRUTTURA A TERMINE e solo per il 25% skew** | ## Note che sopravvivono ai singoli filoni @@ -282,3 +284,81 @@ zeri nei due canali, il guadagno FULL viene dalla **FREQUENZA** (il solo filtro spiegazione e' sbagliata anche se il numero regge.** Entro **2026-12-22**, prima di qualunque modifica a peso/config di SKH01. Deploy solo se ISO dShFULL > 0 in >=20/23 ancore E `weights_tilt_null` passa. **Book, pesi, cron, config INVARIATI.** + +### XSR-REPRO (r0822_xsr_repro.py) — filone di INTEGRITA', non di alpha +✅ **Il numero pubblicato e' spiegato e NON era sbagliato.** 1.82 viene da una **terza** lente +(`r0725_statarb_demean_skeptic.ret_from_pos`, divisore **fisso 50**), non da quella che ha girato i +gate (`basket_from_positions`, divisore variabile). Sullo stato del dato del 25/07 ricostruito: +**11 numeri pubblicati su 12 riprodotti** (Sharpe netta 1.8164, lorda 2.6956, DD −2.585%, lag +1.82/1.19/0.81/0.50, Sharpe 2024 1.0297 / 2025 1.9752 / 2026 3.1078). +📌 **Ma il numero di TITOLO e i numeri di GATE non sono mai stati la stessa serie:** la lente dei +gate da' **1.79** — ed e' esattamente il "Sharpe 1.79" che il docstring dello scettico gia' citava. +DSR ri-calcolato oggi sulla lente giusta: **0.983 PASS** (pubblicato 0.985) -> **il gate DSR regge**. +✅ **L'ipotesi "il cron riscrive la storia" e' FALSIFICATA** da due prove indipendenti: (a) il +registro del cron su 54 simboli, 25/07 vs 22/08, mostra crescita di **esattamente 28 barre per +tutti** con `start_reale` invariato; (b) lo Sharpe degli **anni chiusi** si riproduce **al quarto +decimale** attraverso quattro settimane di riscritture. **Cambia una sola barra: l'ultima.** +🚨 **IL DIFETTO CHE CONTA — `paper_xsr` registra ~41 minuti di mercato al giorno.** +`fetch_hyperliquid` gira alle 00:30 con `END = oggi` e scrive la barra del **giorno in corso**; +`advance()` processa le barre con `ts > last_ts` e **porta `last_ts` su quella parziale** -> le 23 +ore e mezza restanti di **ogni** giorno non entrano in nessun rendimento registrato. +**Misurato** (28 barre appaiate contro il replay): **1/28 identiche · corr −0,045 · vol registrata +0,46% contro 2,74% ricalcolata** (la strategia e' progettata a 2,3%). Rapporto di varianza 0,0286 +-> ~41 min/giorno; stima indipendente dal volume: ~69 min. +✅ **Verifica del coordinatore, indipendente:** l'ultima barra di `hl_btc_1d` ha volume **1.274** +contro **66.926** e **102.198** dei due giorni precedenti. +⚠️ **`monitor_health` lo dichiara OK e non puo' vederlo:** la serie e' fresca e senza buchi. +**Una serie fresca, completa e SBAGLIATA passa ogni controllo di freschezza.** +⚠️ Stessa **forma** di codice in `paper_statarb` (gate 27/09), che per giunta ha un docstring +"barre 1d chiuse" mentre consuma `altlib.get(...,"1d") -> resample_tf`, che **non** scarta il giorno +in corso. Segnalato come forma, non misurato -> **audit dedicato in corso su tutti e 6 i monitor**. +⚠️ **Haircut, la seconda gamba del gate 23/10, e' piu' fragile della prima:** ticket per gamba +**mediano $3,33 / medio $5,98**, **63% degli ordini sotto $5**; il pubblicato **$14,41 non ha alcuno +script committato che lo riproduca** ed e' **~2,4x ottimista**. A min-order $10 la guardia +formalmente passa ma si esegue **un ordine su cinque** — e il criterio pre-registrato non lo guarda. +📌 **NUMERO ONESTO DA CITARE:** lente dei gate, sole barre chiuse -> **1.79** (finestra di scoperta) +/ **1.63** (a oggi). **Va sempre citata la coppia (lente, ultima barra chiusa)**, mai il numero nudo. +📌 **GATE 23/10 COMPROMESSO: PARZIALMENTE** — ammissione NO (storia chiusa stabile, DSR regge), +**forward SI** (metrica primaria su una serie scorrelata dalla realta'), haircut SI (parametro senza +script e ~2,4x ottimista). **La finestra forward non e' recuperabile a posteriori**; riparare +`advance()` la **azzera**. Scelta fra gate letto su strumento rotto e gate rimandato con contatore +nuovo: **va decisa PRIMA del 23/10, non quel giorno.** + +### 5 — SKEW (r0822_skew.py, 37 trial dichiarati) +- **Q1 (skew come segnale direzionale): SCARTATO, e non per mancanza di potenza — la potenza c'e' e + falsifica.** Il **prezzo precede lo skew** con t da **3,13 a 9,39 su 8/8 test** e su entrambi gli + asset; in avanti il max |t| su 16 test e' **2,35** contro **2,08** atteso dal rumore, e non replica + sul secondo asset. L'unica cella con lead apparente crolla togliendo **4 giorni di rally su 24**. + DSR **0,163 FAIL**; IC95 block-bootstrap sullo Sharpe: **[−2,90, +5,86]** (ampiezza 8,76). +- **Q2 (skew come gate di de-risk): SCARTATO (non misurabile)** — nella finestra TP01 e' attivo + **5 giorni su 75** con maxDD **0,14%**: non c'e' sinistro. Unica cosa leggibile: **5ª firma di + ridondanza col trend** (il gate spegne 16 giorni in cui TP01 era gia' flat, **16/16**). +- 📌 **Q3 — LEAD, ed e' il risultato della sessione. Il f=0,73 di VRP01 scomposto** (attribuzione + esatta in log, i fattori si sommano): + | causa | fattore | quota del difetto | + |---|---|---| + | **struttura a termine** | x0,869 | **42,3%** | + | spread denaro-lettera | x0,904 | 30,5% | + | **skew** | x0,920 | **25,0%** | + | fit (controllo) | x0,992 | 2,3% | + **La riga che conta: il sleeve prezza un'opzione a 7 GIORNI col DVOL a 30 GIORNI**, che nel **90%** + delle ore sta ~3 punti di vol **sopra** l'ATM a 7 giorni -> sovrapprezza **ENTRAMBE** le gambe. + Il "+0,8pp sulla gamba corta" pubblicato il 30/07 — che faceva sembrare **corretta** la gamba + venduta — e' la **cancellazione di due errori da ~3pp** (−2,97 di termine, +2,79 di skew). + ⚠️ **E il progetto aveva gia' codificato la regola il 03/07** ("riprezzare term-structure-consistent + prima di credere a un numero da struttura BS-flat") **e non l'ha applicata alla misura del 30/07**. + 📌 **Verso, e non e' quello che sembra:** `f_term` passa da **0,83** (IV-rank basso) a **0,93** + (alto) e supera **1,0 in backwardation**, mentre `f_skew` resta piatto (0,91-0,93) -> **la parte + maggiore del difetto si annulla proprio dove il gate IV-rank fa tradare il sleeve**, quindi + f=0,73 e' plausibilmente **CONSERVATIVO li'**. Non misurato: **0/22** ingressi passano il gate, e + l'unico episodio ad alta vol del campione e' un **rally**, non un crash. In un crash i due pezzi + vanno in **direzioni contrarie** e il netto e' ignoto. **`VRP_CFG["f"]` NON cambiato.** + Gate pre-registrato: rimisurare a **>=6 ingressi settimanali per asset con IV-rank>0,30** + (il collettore li accumula da solo), revisione comunque **2027-02-22**. +- ✅ Confound di modello **ESCLUSO e misurato**: `f_markfit` = **0,992** (la mark IV di Deribit + riprezza i propri mid entro l'1%) -> RR/BF sono un fatto di **prezzo**, non l'output del fit. +- ✅ **Controllo positivo cablato nello script:** con l'ancora **assunta** a :30 invece del `ts_max` + osservato compare un **falso lead** a +15m (0,102 contro 0,031) — *"e' l'errore che avrei + pubblicato senza quel controllo"*. +- ✅ Smile vettorizzato (512s -> 2,0s) verificato **bit-exact** contro il ciclo lento su 21.244 celle. +- ✅ **Convergenza indipendente** con l'agente TERM-STRUCTURE: **75 giorni utili** (l'altro trovava 74). diff --git a/scripts/research/r0822_alt_options.py b/scripts/research/r0822_alt_options.py new file mode 100644 index 0000000..2cad5af --- /dev/null +++ b/scripts/research/r0822_alt_options.py @@ -0,0 +1,530 @@ +"""ALT-OPT — la famiglia opzioni USDC-LINEARE di Deribit, mai guardata dal progetto (2026-08-22). + +CONTESTO. Il filone VRP-QUOTE-VERE di questa stessa ondata ha trovato che `collect_chain.py` +interroga `{"currency": "BTC"|"ETH"}`, che su Deribit restituisce le sole opzioni INVERSE +(regolate in BTC/ETH). Il conto e' in USDC. Esiste accanto una famiglia USDC-LINEARE con lotti +~10x piu' piccoli, e su alcuni sottostanti (SOL, XRP, HYPE, AVAX) e' molto piu' popolata di +BTC_USDC. Il progetto non l'ha mai misurata. + +DOMANDA. Esiste su questa famiglia una struttura a RISCHIO DEFINITO (put credit spread, la stessa +geometria di VRP01: vendi ~-0.28, compri ~-0.10) eseguibile con qualche centinaio di dollari, +con spread e liquidita' che non se la mangino? + +COSA MISURA, in ordine, e si FERMA appena un vincolo la uccide: + 1. i fatti del venue letti dall'API pubblica (min_trade_amount, tick_size, contract_size, + costo in $ di UN lotto), e la LIQUIDITA' vera: spread relativo (ask-bid)/mid ai delta che + servono davvero, non sull'ATM, + profondita' in cima al book; + 2. il collaterale: margine della gamba corta secondo la formula pubblicata dal venue, e + max-loss della struttura (i due numeri differiscono per un fatto — se il venue netta le + gambe — che l'API PUBBLICA NON dice: e' dichiarato come tale, non indovinato); + 3. quanta storia c'e' (risposta: nessuna) e quindi cosa si potrebbe al massimo concludere. + +COSA NON PUO' FARE, e va detto prima dei numeri: **non esiste un backtest possibile**. Il nostro +collettore raccoglie solo le inverse BTC/ETH e l'archivio ereditato pure. Questo file misura le +condizioni di UN ISTANTE. Il massimo verdetto ottenibile e' `LEAD` = "vale la pena raccogliere il +dato", con il costo di raccolta stimato in chiamate/ora sul giro esistente. + +VINCOLI DI PROGETTO CITATI, non aggirati: + (a) "niente short-vol da modello in deploy" (19/06). Qui i prezzi sono VERI, non modellati — + che e' un'altra cosa — ma la regola resta finche' non esiste un campione che contenga uno + STRESS. Un istante di calma non e' un campione. + (b) SOL e' escluso dall'universo DIREZIONALE per decisione dell'operatore del 22/08. Quella + decisione riguarda TP01/SKH01 congelati come terza gamba di trend, e il suo stesso testo + dichiara che un meccanismo diverso la riapre. Un put credit spread non e' direzionale nel + senso di quella decisione. Ma il guardrail sul DATO vale lo stesso: nessun file nuovo in + `data/raw/`, e i prezzi del sottostante qui vengono da `underlying_price` della catena, + mai da `load_data`. + (c) il gate IV-rank>0.30 e' l'unico alpha misurato di VRP01, e su questi sottostanti NON e' + calcolabile: non abbiamo un giorno di storia di vol implicita. Senza quel gate la struttura + non e' VRP01, e' vendere vol a caso. + +CONVENZIONE DI UNITA' (la trappola di questo file): le opzioni INVERSE si quotano in CRIPTO per +contratto, le USDC-LINEARI in USDC per unita' di sottostante. Confrontare i due premi senza +convertire darebbe numeri sbagliati di un fattore ~10^4-10^5. Qui ogni importo in dollari passa +per `prem_usd()`, e le colonne unit-free (spread relativo, costo/credito) sono le uniche +direttamente confrontabili fra le due famiglie. + +RETE: sole letture pubbliche, nessun ordine, pacing a ~2 richieste/s, e il file SI RIFIUTA di +partire nella finestra :24-:29 (e' quando `cron_chain` usa lo stesso IP: il rate limit Deribit e' +per-IP e il progetto ha gia' avuto un guasto per questo, 29/07). + + nice -n 19 timeout 900 uv run python scripts/research/r0822_alt_options.py + nice -n 19 timeout 900 uv run python scripts/research/r0822_alt_options.py --cache # riusa +""" +from __future__ import annotations + +import argparse +import json +import math +import time +from datetime import UTC, datetime +from pathlib import Path + +import numpy as np +import requests + +API = "https://www.deribit.com/api/v2/public" +TIMEOUT = 20 +RPS = 2.0 +CACHE = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/altopt_cache.json") + +SHORT_DELTA, LONG_DELTA = -0.28, -0.10 # la geometria di VRP01, non ri-scelta qui +DTE_LO, DTE_HI = 4.0, 45.0 # dal settimanale di VRP01 (4-10) fino a ~1.5 mesi +OI_MIN = 100.0 # la stessa soglia di collect_chain.py +CAPITAL = 635.0 # il conto vero +TAKER_CAP = 0.125 # cap fee opzioni Deribit: 12.5% del premio + +LINEAR = ("SOL_USDC", "XRP_USDC", "ETH_USDC", "HYPE_USDC", "AVAX_USDC", "BTC_USDC") +INVERSE = ("BTC", "ETH") # termine di paragone: il progetto le conosce + +# ---------------------------------------------------------------- rete (parca, sola lettura) + + +class Budget: + def __init__(self, rps: float = RPS) -> None: + self.rps, self._next, self.calls, self.err, self.r429 = rps, 0.0, 0, 0, 0 + + def wait(self) -> None: + now = time.monotonic() + if now < self._next: + time.sleep(self._next - now) + self._next = max(now, self._next) + 1.0 / self.rps + + +def guardia_finestra() -> None: + m = datetime.now(UTC).minute + if 24 <= m <= 29: + raise SystemExit(f"minuto {m}: finestra di cron_chain (:25-:29). Rilancia dopo il :30.") + + +def get(path: str, params: dict, b: Budget, tries: int = 3): + for k in range(tries): + b.wait() + b.calls += 1 + try: + r = requests.get(f"{API}/{path}", params=params, timeout=TIMEOUT) + except Exception: + b.err += 1 + time.sleep(1.5 * (k + 1)) + continue + if r.status_code == 429: + b.r429 += 1 + time.sleep(2.0 * (k + 1)) + continue + if r.status_code != 200: + b.err += 1 + time.sleep(1.0 * (k + 1)) + continue + try: + return r.json()["result"] + except Exception: + b.err += 1 + return None + return None + + +def fetch_all(b: Budget) -> dict: + """6 chiamate: instruments + book_summary per USDC, BTC, ETH. Niente per-strumento.""" + out = {} + for cur in ("USDC", "BTC", "ETH"): + out[f"inst_{cur}"] = get("get_instruments", + {"currency": cur, "kind": "option", "expired": "false"}, b) or [] + out[f"summ_{cur}"] = get("get_book_summary_by_currency", + {"currency": cur, "kind": "option"}, b) or [] + if not out["inst_USDC"]: # il venue potrebbe non accettare currency=USDC + out["inst_USDC"] = get("get_instruments", + {"currency": "any", "kind": "option", "expired": "false"}, b) or [] + out["summ_USDC"] = get("get_book_summary_by_currency", + {"currency": "any", "kind": "option"}, b) or [] + return out + + +# ---------------------------------------------------------------- Black-Scholes (r=0, come il sleeve) + + +def put_delta(S: float, K: float, T: float, sig: float) -> float: + if not (S > 0 and K > 0 and T > 0 and sig > 0): + return float("nan") + d1 = (math.log(S / K) + 0.5 * sig * sig * T) / (sig * math.sqrt(T)) + return 0.5 * (1.0 + math.erf(d1 / math.sqrt(2.0))) - 1.0 + + +# ---------------------------------------------------------------- unita' + + +def is_inverse(fam: str) -> bool: + return fam in INVERSE + + +def prem_usd(price: float, r: dict) -> float: + """Premio in DOLLARI di `min_trade_amount` contratti quotati a `price`. + + Lineare: il prezzo e' gia' in USDC per unita' di sottostante. + Inverse: il prezzo e' in CRIPTO per contratto -> si moltiplica per il sottostante. + """ + k = r["csize"] * r["minamt"] + return price * k * (r["S"] if is_inverse(r["fam"]) else 1.0) + + +def loss_usd(Ks: float, Kl: float, r: dict) -> float: + """Perdita massima strutturale in dollari, per `min_trade_amount` contratti. + + Lineare: esatta, (Ks-Kl) x quantita'. Inverse: il payoff e' in cripta (max(0,K-S)/S), il + peggiore e' a S=Kl e vale (Ks-Kl)/Kl cripto = (Ks-Kl) dollari a quel prezzo -> stessa + formula, ma APPROSSIMATA (il valore in dollari della perdita dipende da dove finisce S). + """ + return (Ks - Kl) * r["csize"] * r["minamt"] + + +# ---------------------------------------------------------------- margine (FONTE SECONDARIA) + + +def im_short_put_usd(S: float, K: float, mark_usd: float, qty: float) -> float: + """Initial margin di una short put, in USDC, secondo la formula PUBBLICATA da Deribit. + + ⚠️ FONTE SECONDARIA, dichiarata: l'API PUBBLICA non espone il margine. Il numero che decide + davvero — se Deribit NETTA le due gambe di uno spread in margine standard — non e' leggibile + da qui: si conferma con `private/get_margins`, che richiede una chiave (lettura, ma non + pubblica) e quindi NON e' stato usato. + """ + if not (S > 0 and K > 0): + return float("nan") + otm = max(0.0, S - K) / S # una put e' OTM quando K < S + per_contratto = max(max(0.15 - otm, 0.10) * S, 0.075 * S) + max(0.0, mark_usd / max(qty, 1e-12)) + return per_contratto * qty + + +# ---------------------------------------------------------------- costruzione della vista + + +def build(raw: dict) -> list[dict]: + inst, summ = {}, {} + for cur in ("USDC", "BTC", "ETH"): + for i in raw.get(f"inst_{cur}") or []: + inst[i["instrument_name"]] = i + for s in raw.get(f"summ_{cur}") or []: + summ[s["instrument_name"]] = s + rows = [] + now_ms = datetime.now(UTC).timestamp() * 1000.0 + for name, i in inst.items(): + s = summ.get(name) + if s is None: + continue + fam = name.split("-")[0] + dte = (float(i.get("expiration_timestamp") or 0) - now_ms) / 86400_000.0 + bid, ask = s.get("bid_price"), s.get("ask_price") + bid = float(bid) if bid else None # Deribit manda 0/None per "nessun lato" + ask = float(ask) if ask else None + iv, S = s.get("mark_iv"), s.get("underlying_price") + K = float(i.get("strike") or 0) + typ = "P" if i.get("option_type") == "put" else "C" + T = max(dte, 0.0) / 365.25 + d = (put_delta(float(S), K, T, float(iv) / 100.0) + if (S and iv and K and T > 0 and typ == "P") else float("nan")) + rows.append(dict( + name=name, fam=fam, typ=typ, K=K, dte=dte, S=float(S) if S else float("nan"), + iv=float(iv) if iv else float("nan"), delta_bs=d, + bid=bid, ask=ask, mid=((bid + ask) / 2 if (bid and ask) else None), + mark=float(s.get("mark_price") or 0.0), + oi=float(s.get("open_interest") or 0.0), vol=float(s.get("volume") or 0.0), + tick=float(i.get("tick_size") or 0.0), steps=i.get("tick_size_steps") or [], + minamt=float(i.get("min_trade_amount") or 0.0), + csize=float(i.get("contract_size") or 0.0), + taker=i.get("taker_commission"), settle=i.get("settlement_currency"), + )) + return rows + + +def rel_spread(r: dict) -> float: + if r["bid"] is None or r["ask"] is None or not r["mid"]: + return float("nan") + return (r["ask"] - r["bid"]) / r["mid"] + + +# ---------------------------------------------------------------- 1. i fatti del venue + + +def famiglie(rows: list[dict]) -> dict: + print("\n" + "=" * 104) + print("1a. I FATTI DEL VENUE — public/get_instruments + get_book_summary_by_currency") + print("=" * 104) + print(f"{'famiglia':<10} {'strum':>6} {'OI>=100':>8} {'min_amt':>9} {'c_size':>7} " + f"{'tick':>9} {'sottost.$':>11} {'$/lotto':>10} {'taker':>8} {'settle':>7}") + tab = {} + for f in sorted({r["fam"] for r in rows}): + sub = [r for r in rows if r["fam"] == f] + liq = [r for r in sub if r["oi"] >= OI_MIN] + S = float(np.nanmedian([r["S"] for r in sub])) + mins = {r["minamt"] for r in sub} + cs = {r["csize"] for r in sub} + a = sub[0] + lot = a["minamt"] * a["csize"] * S + tab[f] = dict(n=len(sub), nliq=len(liq), minamt=a["minamt"], csize=a["csize"], + tick=a["tick"], S=S, lot=lot, taker=a["taker"]) + if f in LINEAR or f in INVERSE: + avv = "" if (len(mins) == 1 and len(cs) == 1) else " <- min/c_size NON uniformi" + print(f"{f:<10} {len(sub):>6} {len(liq):>8} {a['minamt']:>9.4g} {a['csize']:>7.4g} " + f"{a['tick']:>9.5g} {S:>11.2f} {lot:>10.2f} {str(a['taker']):>8} " + f"{str(a['settle']):>7}{avv}") + print("\n `$/lotto` = min_trade_amount x contract_size x sottostante = il NOZIONALE di un") + print(" lotto (NON il premio, che e' molto minore). E' la granularita' minima imposta dal venue.") + print(" `taker` per le opzioni Deribit e' una frazione del SOTTOSTANTE con cap al 12.5% del") + print(" premio: su un'ala molto OTM morde il CAP, non la percentuale.") + return tab + + +def replica(tab: dict) -> None: + print("\n" + "-" * 104) + print("1b. CONTROLLO POSITIVO — riproduco i conteggi dichiarati dal coordinatore (22/08)?") + print("-" * 104) + atteso = {"BTC": (1038, 415), "ETH": (932, 548), "BTC_USDC": (686, 5), "ETH_USDC": (660, 119), + "SOL_USDC": (574, 341), "XRP_USDC": (522, 250), "HYPE_USDC": (396, 172), + "AVAX_USDC": (294, 146), "TRX_USDC": (310, 115)} + print(f"{'famiglia':<10} {'strum att':>10} {'strum oggi':>11} {'liq att':>8} {'liq oggi':>9}") + for f, (n_a, l_a) in atteso.items(): + t = tab.get(f) + if t is None: + print(f"{f:<10} {n_a:>10} {'ASSENTE':>11} {l_a:>8} {'-':>9}") + else: + print(f"{f:<10} {n_a:>10} {t['n']:>11} {l_a:>8} {t['nliq']:>9}") + print(" La catena si muove di ora in ora (scadenze che nascono, OI che cambia): scarti di") + print(" poche unita' sono la misura, non un disaccordo. Uno scarto GRANDE sarebbe un problema.") + + +# ---------------------------------------------------------------- 2. liquidita' ai delta utili + + +def gambe(rows: list[dict], fam: str) -> list[dict]: + puts = [r for r in rows if r["fam"] == fam and r["typ"] == "P" + and DTE_LO <= r["dte"] <= DTE_HI and np.isfinite(r["delta_bs"])] + out = [] + for e in sorted({round(r["dte"], 4) for r in puts}): + g = [r for r in puts if round(r["dte"], 4) == e] + s = min(g, key=lambda r: abs(r["delta_bs"] - SHORT_DELTA)) + lo = min(g, key=lambda r: abs(r["delta_bs"] - LONG_DELTA)) + if s["K"] > lo["K"]: + out.append({"dte": e, "short": s, "long": lo, "n_strike": len(g)}) + return out + + +def liquidita(rows: list[dict]) -> dict: + print("\n" + "=" * 104) + print(f"2. LA LIQUIDITA' AI DELTA CHE SERVONO ({SHORT_DELTA} corta / {LONG_DELTA} lunga),") + print(f" NON sull'ATM. Finestra {DTE_LO:.0f}-{DTE_HI:.0f} giorni. Delta = BS su mark_iv, r=0.") + print("=" * 104) + print(f"{'famiglia':<10} {'scad':>5} {'2lati':>6} {'d corta':>8} {'d lunga':>8} " + f"{'spr% corta':>11} {'spr% lunga':>11} {'credito$':>9} {'costo/cred':>11} " + f"{'fee/cred':>9} {'maxloss$':>9}") + res = {} + for fam in list(LINEAR) + list(INVERSE): + gg = gambe(rows, fam) + if not gg: + print(f"{fam:<10} {'0':>5} nessuna scadenza utilizzabile nella finestra") + continue + ss, ll, ds, dl, cred, cost, fee, ml = [], [], [], [], [], [], [], [] + due = neg = 0 + for g in gg: + s, lo = g["short"], g["long"] + rs, rl = rel_spread(s), rel_spread(lo) + if not (np.isfinite(rs) and np.isfinite(rl)): + continue + due += 1 + c_mid = prem_usd(s["mid"], s) - prem_usd(lo["mid"], lo) + c_exe = prem_usd(s["bid"], s) - prem_usd(lo["ask"], lo) + if c_mid <= 0: + neg += 1 + continue + # fee taker Deribit: min(0.03% del sottostante, 12.5% del premio) per gamba, x2 gambe, + # x2 (apertura + chiusura/esercizio). Conservativo ma e' il listino. + def _fee(r: dict) -> float: + notion = r["S"] * r["csize"] * r["minamt"] + return min(float(r["taker"] or 0.0003) * notion, TAKER_CAP * prem_usd(r["mid"], r)) + ss.append(rs); ll.append(rl) + ds.append(s["delta_bs"]); dl.append(lo["delta_bs"]) + cred.append(c_mid) + cost.append((c_mid - c_exe) / c_mid) + fee.append(2.0 * (_fee(s) + _fee(lo)) / c_mid) + ml.append(loss_usd(s["K"], lo["K"], s) - c_exe) + if not cred: + print(f"{fam:<10} {len(gg):>5} {due:>6} nessuna struttura con credito positivo a mid " + f"({neg} a credito<=0)") + continue + r = dict(n=len(gg), due=due, neg=neg, s=float(np.median(ss)), l=float(np.median(ll)), + ds=float(np.median(ds)), dl=float(np.median(dl)), cred=float(np.median(cred)), + cost=float(np.median(cost)), fee=float(np.median(fee)), ml=float(np.median(ml))) + res[fam] = r + print(f"{fam:<10} {r['n']:>5} {due:>6} {r['ds']:>8.3f} {r['dl']:>8.3f} " + f"{r['s']*100:>10.1f}% {r['l']*100:>10.1f}% {r['cred']:>9.2f} " + f"{r['cost']*100:>10.1f}% {r['fee']*100:>8.1f}% {r['ml']:>9.2f}") + print("\n `2lati` = strutture in cui ENTRAMBE le gambe hanno bid E ask. Una riga presente non") + print(" e' un dato presente: una gamba con un solo lato non e' negoziabile a un prezzo noto.") + print(" `d corta`/`d lunga` = delta REALIZZATI (non i target): se la griglia degli strike e'") + print(" grossolana la struttura che compreresti non e' quella che hai chiesto.") + print(" `costo/cred` = quanto dell'incasso a mid resta sul tavolo attraversando lo spread su") + print(" ENTRAMBE le gambe. Riferimento del progetto su BTC/ETH inverse: ~10% del credito.") + print(" `fee/cred` = listino taker Deribit, 2 gambe x apertura+chiusura, col cap 12.5%.") + print(" `credito$` e `maxloss$` sono per UN LOTTO MINIMO.") + return res + + +def profondita(rows: list[dict], fams: list[str], b: Budget) -> None: + print("\n" + "=" * 104) + print("2b. PROFONDITA' IN CIMA AL BOOK (public/get_order_book depth=5), sulle gambe scelte") + print("=" * 104) + print(f"{'famiglia':<10} {'gamba':<6} {'n':>3} {'lotti al best':>14} {'$ premio al best':>17} " + f"{'ask/tick lunga':>15} {'max|delta venue-BS|':>21}") + for fam in fams: + gg = gambe(rows, fam)[:4] # max 4 scadenze/famiglia: budget di rete + for lato, key in (("corta", "short"), ("lunga", "long")): + lots, prem, dd, tk = [], [], [], [] + for g in gg: + r = g[key] + ob = get("get_order_book", {"instrument_name": r["name"], "depth": 5}, b) + if not ob: + continue + side = ob.get("bids") if key == "short" else ob.get("asks") + if not side: + continue + px, sz = float(side[0][0]), float(side[0][1]) + lots.append(sz / r["minamt"] if r["minamt"] else float("nan")) + prem.append(prem_usd(px, r) * (sz / max(r["minamt"], 1e-12))) + if r["tick"]: + tk.append(px / r["tick"]) + gv = (ob.get("greeks") or {}).get("delta") + if gv is not None and np.isfinite(r["delta_bs"]): + dd.append(abs(float(gv) - r["delta_bs"])) + if not lots: + print(f"{fam:<10} {lato:<6} {'0':>3} book vuoto su tutte le scadenze provate") + continue + print(f"{fam:<10} {lato:<6} {len(lots):>3} {np.median(lots):>14.1f} " + f"{np.median(prem):>17.2f} " + f"{(np.median(tk) if tk else float('nan')):>15.1f} " + f"{(max(dd) if dd else float('nan')):>21.4f}") + print("\n `lotti al best` = size in cima al book / lotto minimo: quante strutture minime") + print(" entrano senza muovere il prezzo. `ask/tick` sulla gamba LUNGA e' il controllo") + print(" d'artefatto del 30/07: un'ala a 1-2 tick e' un prezzo di griglia, non un prezzo.") + print(" `max|delta venue-BS|` valida il MIO calcolo del delta contro le greche del venue: se") + print(" non e' piccolo, tutta la selezione degli strike qui sopra e' sbagliata.") + + +# ---------------------------------------------------------------- 3. collaterale + + +def collaterale(rows: list[dict], res: dict) -> None: + print("\n" + "=" * 104) + print("3. IL COLLATERALE — quanto conto serve per UNA struttura minima (conto reale $635)") + print("=" * 104) + print(f"{'famiglia':<10} {'IM corta+ala $':>15} {'max-loss $':>11} {'IM/conto':>9} " + f"{'strutture @635 (IM)':>20} {'(max-loss)':>12}") + for fam in res: + gg = gambe(rows, fam) + ims, mls = [], [] + for g in gg: + s, lo = g["short"], g["long"] + if s["mid"] is None or lo["ask"] is None: + continue + qty = s["csize"] * s["minamt"] + im = im_short_put_usd(s["S"], s["K"], prem_usd(s["mark"], s), qty) + ims.append(im + prem_usd(lo["ask"], lo)) # l'ala si PAGA: e' cassa, non margine + mls.append(loss_usd(s["K"], lo["K"], s)) + if not ims: + continue + im, ml = float(np.median(ims)), float(np.median(mls)) + print(f"{fam:<10} {im:>15.2f} {ml:>11.2f} {im/CAPITAL:>8.1%} " + f"{CAPITAL/im:>20.1f} {CAPITAL/ml:>12.1f}") + print("\n `IM corta+ala` = margine iniziale della sola put venduta (formula PUBBLICATA dal") + print(" venue, FONTE SECONDARIA) + il premio dell'ala, che si paga in contanti. E' il") + print(" fabbisogno se il venue NON netta le gambe.") + print(" `max-loss` = perdita massima strutturale = il fabbisogno se il venue LE NETTA.") + print(" ⚠️ La differenza fra i due numeri e' l'unica cosa importante di questa tabella e NON") + print(" E' LEGGIBILE dall'API pubblica: si chiude con `private/get_margins` (lettura, ma") + print(" autenticata -> fuori dal perimetro dichiarato di questo file).") + + +def costo_raccolta(tab: dict) -> None: + print("\n" + "=" * 104) + print("4. COSTO DI RACCOLTA — cosa costerebbe aggiungere una famiglia al giro esistente") + print("=" * 104) + print(" `collect_chain.py` oggi: ~650 chiamate in ~160s a 4 rps (BTC+ETH inverse), al :25.") + print(f"{'famiglia':<10} {'chiamate/giro':>14} {'+s a 4 rps':>11} {'+% sul giro':>12}") + for f in LINEAR: + t = tab.get(f) + if t: + n = t["nliq"] + 2 # + get_instruments + get_book_summary + print(f"{f:<10} {n:>14.0f} {n/4.0:>11.1f} {n/650*100:>11.0f}%") + print("\n ⚠️ Il vincolo non e' la CPU ma il rate limit Deribit PER-IP, gia' costato un guasto") + print(" il 29/07 (fallback silenzioso del feed 5m di SKH01). Ogni famiglia aggiunta allunga") + print(" la finestra in cui il giro occupa l'IP.") + + +# ---------------------------------------------------------------- main + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--cache", action="store_true", help="riusa lo snapshot su disco (niente rete)") + a = ap.parse_args() + + print("=" * 104) + print("ALT-OPT — opzioni USDC-LINEARI su Deribit: c'e' una struttura eseguibile a $635?") + print("=" * 104) + print("Sola lettura pubblica, nessun ordine. UNO SNAPSHOT: qui non esiste un backtest.") + + b = Budget() + if a.cache and CACHE.exists(): + raw = json.loads(CACHE.read_text()) + print(f"\n[cache] {CACHE}") + else: + guardia_finestra() + t0 = time.time() + raw = fetch_all(b) + CACHE.parent.mkdir(parents=True, exist_ok=True) + CACHE.write_text(json.dumps(raw)) + print(f"\n[rete] {b.calls} chiamate, {b.err} errori, {b.r429} risposte 429, " + f"{time.time()-t0:.0f}s") + + rows = build(raw) + print(f"[dato] {len(rows)} strumenti con specifiche E quote — snapshot " + f"{datetime.now(UTC):%Y-%m-%d %H:%M} UTC") + if not rows: + print("NESSUN DATO: il venue non ha risposto. Verdetto: non misurabile.") + return 1 + + tab = famiglie(rows) + replica(tab) + res = liquidita(rows) + + vive = [f for f in res if res[f]["due"] >= 2] + if a.cache: + print("\n2b. PROFONDITA': non girata (--cache non usa la rete).") + elif not vive: + print("\n2b. PROFONDITA': non girata — nessuna famiglia con >=2 strutture a due lati.") + else: + guardia_finestra() + profondita(rows, vive, b) + + collaterale(rows, res) + costo_raccolta(tab) + + print("\n" + "=" * 104) + print("5. COSA QUESTO FILE NON PUO' DIRE (dichiarato prima di qualunque verdetto)") + print("=" * 104) + print(" * Non c'e' STORIA: il collettore raccoglie BTC/ETH inverse, l'archivio ereditato pure.") + print(" Zero giorni di catena su SOL/XRP/HYPE/AVAX/*_USDC -> nessun backtest, nessun") + print(" hold-out, nessun deflated-Sharpe. Il massimo verdetto ottenibile e' LEAD.") + print(" * Non c'e' IV-RANK: il gate>0.30 e' l'unico alpha misurato di VRP01 e richiede la") + print(" distribuzione storica della vol implicita del sottostante. Senza, la struttura non") + print(" e' VRP01: e' vendere vol a caso. (Su BTC/ETH quel gate e' passato 0/19 settimane.)") + print(" * Non c'e' STRESS nel campione: la regola 'niente short-vol da modello in deploy' e'") + print(" del 19/06 e la condizione dichiarata per rivalutarla e' un crash CATTURATO.") + print(" * Il margine e' da FORMULA PUBBLICATA, non dall'API: il netting delle gambe non e'") + print(" leggibile pubblicamente.") + print(" * E' UN ISTANTE: uno spread relativo mediano misurato a un'ora del sabato non e' la") + print(" sua distribuzione. Servirebbe almeno un ciclo settimanale per sapere se e' tipico.") + print(f"[rete, totale] {b.calls} chiamate, {b.err} errori, {b.r429} risposte 429") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/research/r0822_basis_calendar.py b/scripts/research/r0822_basis_calendar.py index a3199b1..3997af5 100644 --- a/scripts/research/r0822_basis_calendar.py +++ b/scripts/research/r0822_basis_calendar.py @@ -283,24 +283,29 @@ def certify(asset: str, wide: pd.DataFrame, vwide: pd.DataFrame, meta: pd.DataFr if b in fb.index: print(f" {b:9s} flat={fb.loc[b,'flat']*100:5.1f}% barre={int(fb.loc[b,'n']):,}") - # (b) volume orario mediano per bucket (il flat da solo non basta) + # (b) volume orario per bucket (il flat da solo non basta). + # ATTENZIONE: il `volume` di get_tradingview_chart_data e' in VALUTA BASE (BTC/ETH), + # non in USD -> va moltiplicato per il prezzo, o si sbaglia di ~5 ordini di grandezza. vrows = [] for nm in vwide.columns: exp = ok.loc[ok.contract == nm, "exp"] if exp.empty: continue - s = vwide[nm].dropna() - dte = (exp.iloc[0] - s.index).total_seconds() / 86400.0 + v = vwide[nm].dropna() + px = wide[nm].reindex(v.index) + usd = (v * px).dropna() # <- conversione in USD + dte = (exp.iloc[0] - usd.index).total_seconds() / 86400.0 for lo, hi, lab in [(0, 30, "0-30g"), (30, 90, "30-90g"), (90, 180, "90-180g"), (180, 400, ">180g")]: - m = (dte >= lo) & (dte < hi) - if m.sum() > 50: - vrows.append(dict(bucket=lab, med=float(s[m].median()), zero=float((s[m] == 0).mean()))) + mm = (dte >= lo) & (dte < hi) + if mm.sum() > 50: + vrows.append(dict(bucket=lab, med=float(usd[mm].median()), + zero=float((usd[mm] == 0).mean()))) vb = pd.DataFrame(vrows).groupby("bucket").agg(med=("med", "median"), zero=("zero", "mean")) - print(" volume orario mediano (USD di nozionale) e quota ore a volume ZERO:") + print(" volume orario mediano in USD (volume base x prezzo) e quota ore a volume ZERO:") for b in order: if b in vb.index: - print(f" {b:9s} mediano=${vb.loc[b,'med']:>12,.0f} ore a vol 0 = {vb.loc[b,'zero']*100:5.1f}%") + print(f" {b:9s} mediano=${vb.loc[b,'med']:>12,.0f}/ora ore a vol 0 = {vb.loc[b,'zero']*100:5.1f}%") # (c) accordo con l'INDICE alla scadenza: il datato DEVE convergere conv = [] @@ -385,34 +390,36 @@ def curve_frame(asset: str, wide: pd.DataFrame, meta: pd.DataFrame, ok = meta[meta.status == "ok"].sort_values("exp") exps = {r.contract: r.exp for r in ok.itertuples()} idx = wide.index - F = fund.set_index("ts").reindex(idx) + F = fund.set_index("ts").reindex(idx).ffill(limit=2) n = len(idx) - front = np.full(n, "", dtype=object) - back = np.full(n, "", dtype=object) - contracts = list(wide.columns) - tau = {c: (exps[c] - idx).total_seconds() / 3600.0 for c in contracts if c in exps} - avail = {c: wide[c].notna().values for c in contracts} - # front = il primo (per scadenza) ancora vivo con dte >= roll_dte; back = il successivo - order = [c for c in ok.contract if c in wide.columns] - for i in range(n): - picked = [] - for c in order: - if avail[c][i] and tau[c][i] >= roll_dte * 24: - picked.append(c) - if len(picked) == 2: - break - if len(picked) == 2: - front[i], back[i] = picked + order = [c for c in ok.contract if c in wide.columns] # ordine di SCADENZA + if len(order) < 2: + raise RuntimeError("meno di due contratti utilizzabili") + + # ALIVE[i, k] = il k-esimo contratto (per scadenza) e' quotato a i E ha dte >= roll + TAU = np.column_stack([(exps[c] - idx).total_seconds().values / 3600.0 for c in order]) + AV = np.column_stack([wide[c].notna().values for c in order]) + ALIVE = AV & (TAU >= roll_dte * 24) + + any1 = ALIVE.any(axis=1) + k1 = np.argmax(ALIVE, axis=1) # primo vivo = FRONT + A2 = ALIVE.copy() + A2[np.arange(n), k1] = False + any2 = A2.any(axis=1) + k2 = np.argmax(A2, axis=1) # secondo vivo = BACK + m = any1 & any2 + + front = np.where(m, np.array(order, dtype=object)[k1], "") + back = np.where(m, np.array(order, dtype=object)[k2], "") + rows = np.arange(n) + Wv = wide[order].values.astype(float) + Ff = np.where(m, Wv[rows, k1], np.nan) + Fb = np.where(m, Wv[rows, k2], np.nan) + tf = np.where(m, TAU[rows, k1], np.nan) + tb = np.where(m, TAU[rows, k2], np.nan) + out = pd.DataFrame(index=idx) out["front"], out["back"] = front, back - m = (front != "") & (back != "") - Ff = np.full(n, np.nan); Fb = np.full(n, np.nan) - tf = np.full(n, np.nan); tb = np.full(n, np.nan) - W = wide.values - colpos = {c: k for k, c in enumerate(wide.columns)} - for i in np.where(m)[0]: - Ff[i] = W[i, colpos[front[i]]]; Fb[i] = W[i, colpos[back[i]]] - tf[i] = tau[front[i]][i]; tb[i] = tau[back[i]][i] out["F_front"], out["F_back"] = Ff, Fb out["tau_front_h"], out["tau_back_h"] = tf, tb out["index"] = F["index"].values @@ -514,7 +521,8 @@ def signal_w(cv: pd.DataFrame, family: str, sig: str, win_h: int, thr: float) -> def run_strategy(cv: pd.DataFrame, ctx: dict, family: str, sig: str, win_h: int, thr: float, slip_bps: float, - fee_side: float = FEE_SIDE, fi=None, bi=None, lag: int = 1) -> pd.Series: + fee_side: float = FEE_SIDE, fi=None, bi=None, lag: int = 1, + dec_hour: int | None = None) -> pd.Series: """Ritorno ORARIO netto per $1 di nozionale LORDO PER GAMBA. Le posizioni vivono in spazio CONTRATTO: quando la coppia cambia (roll) la variazione @@ -529,6 +537,11 @@ def run_strategy(cv: pd.DataFrame, ctx: dict, family: str, sig: str, if fi is None or bi is None: fi, bi = pair_idx(cv, ctx) w = signal_w(cv, family, sig, win_h, thr) + if dec_hour is not None: + # ANCORA: la posizione si aggiorna SOLO a quell'ora del giorno e si TIENE il resto + # (non "si guadagna solo a quell'ora", che sarebbe un'altra strategia). + ws = pd.Series(w, index=idx).where(idx.hour == dec_hour) + w = ws.ffill().fillna(0.0).values P = np.zeros((n, ncol)) rows = np.arange(n) @@ -564,6 +577,12 @@ def run_strategy(cv: pd.DataFrame, ctx: dict, family: str, sig: str, # ========================================================================== # 4. METRICHE / GATE # ========================================================================== +def combine(hs: list[pd.Series]) -> pd.Series: + """50/50 BTC+ETH sulle ore COMUNI (inner join, come tp01_baseline_daily).""" + J = pd.concat(hs, axis=1, join="inner").fillna(0.0) + return J.mean(axis=1) + + def to_daily(h: pd.Series) -> pd.Series: return ((1.0 + h.fillna(0.0)).resample("1D").prod() - 1.0).dropna() @@ -698,9 +717,7 @@ def main() -> None: h = run_strategy(cvcache[(a, rl)], ctxs[a], fam, sig, win, th, slip_est, fi=f_i, bi=b_i) hs.append(h) - J = pd.concat(hs, axis=1).fillna(0.0) - comb = J.mean(axis=1) # 50/50 BTC+ETH, come il resto del progetto - d = to_daily(comb) + d = to_daily(combine(hs)) st = stats(d) rows.append(dict(family=fam, sig=sig, win=win, roll=rl, thr=th, daily=d, **st)) @@ -768,13 +785,10 @@ def main() -> None: hs = [] for a in ASSETS: f_i, b_i = pidx[(a, int(b.roll))] - h = run_strategy(cvcache[(a, int(b.roll))], ctxs[a], b.family, b.sig, - int(b.win), float(b.thr), slip_est, fi=f_i, bi=b_i) - hh = h.copy() - mask = (hh.index.hour != off) - hh[mask] = 0.0 # decide/agisce solo a quell'ora - hs.append(hh) - return to_daily(pd.concat(hs, axis=1).fillna(0.0).mean(axis=1)) + hs.append(run_strategy(cvcache[(a, int(b.roll))], ctxs[a], b.family, b.sig, + int(b.win), float(b.thr), slip_est, + fi=f_i, bi=b_i, dec_hour=off)) + return to_daily(combine(hs)) try: ab = A.anchor_luck_band(by_off, list(range(24)), canonical=0) if "median" in ab: @@ -807,7 +821,7 @@ def main() -> None: f_i, b_i = pidx[(a, int(b.roll))] hs.append(run_strategy(cvcache[(a, int(b.roll))], ctxs[a], b.family, b.sig, int(b.win), float(b.thr), slip_est, fi=f_i, bi=b_i, lag=lg)) - sl_ = stats(to_daily(pd.concat(hs, axis=1).fillna(0.0).mean(axis=1))) + sl_ = stats(to_daily(combine(hs))) lag_tab.append((lg, sl_["sharpe"], sl_["cagr"])) print(f" ritardo {lg:2d}h -> Sharpe {sl_['sharpe']:+6.2f} CAGR {sl_['cagr']*100:+6.2f}%") s1 = lag_tab[0][1]; s2 = lag_tab[1][1] @@ -827,7 +841,7 @@ def main() -> None: f_i, b_i = pidx[(a, int(b.roll))] hs.append(run_strategy(cvcache[(a, int(b.roll))], ctxs[a], b.family, b.sig, int(b.win), float(b.thr), sl, fi=f_i, bi=b_i)) - s2 = stats(to_daily(pd.concat(hs, axis=1).fillna(0.0).mean(axis=1))) + s2 = stats(to_daily(combine(hs))) mark = " <= stima di oggi" if abs(sl - slip_est) < 0.6 else "" print(f" mezzo-spread {sl:5.1f} bps/lato -> Sharpe {s2['sharpe']:+6.2f} " f"CAGR {s2['cagr']*100:+6.2f}%{mark}") diff --git a/scripts/research/r0822_hl_exec.py b/scripts/research/r0822_hl_exec.py index 28a2587..8eede86 100644 --- a/scripts/research/r0822_hl_exec.py +++ b/scripts/research/r0822_hl_exec.py @@ -54,6 +54,7 @@ FEE_LEG_MODELLO = 0.0005 # [D] 0.05%/gamba, config CONGELATA di XSR01 XSR_SOGLIA_HAIRCUT = 0.40 # [D] soglia pre-registrata del gate 2026-10-23 CAPITALI = (600.0, 1000.0, 1500.0, 2000.0, 3000.0, 4000.0, 5000.0, 7500.0, 10000.0, 20000.0, 50000.0) +CAPITALI_XS = (100.0, 200.0, 300.0, 600.0, 1000.0, 2000.0, 3000.0, 5000.0, 10000.0) TICKETS = (50.0, 100.0, 300.0) ANN = np.sqrt(365.0) @@ -257,6 +258,7 @@ def sim_libro(Weff: np.ndarray, R: np.ndarray, r_hedge: np.ndarray | None, cap0: eq = np.empty(n) nets = np.zeros(n) n_fill = n_skip = n_lot0 = 0 + ntl_req = ntl_done = 0.0 fee = np.broadcast_to(np.asarray(fee_leg, float).ravel(), (k,)) if np.ndim(fee_leg) else None for i in range(n): ret = float(np.dot(w, R[i])) @@ -275,6 +277,8 @@ def sim_libro(Weff: np.ndarray, R: np.ndarray, r_hedge: np.ndarray | None, cap0: n_lot0 += int(zero.sum()) move = move & (lots >= 1) need = np.abs(w_t - w) > 1e-12 # una gamba gia' a posto NON e' un ordine saltato + ntl_req += float((np.abs(w_t - w) * cap).sum()) + ntl_done += float((np.abs(w_t - w) * cap)[move].sum()) w_new = np.where(move, w_t, w) n_fill += int((move & need).sum()) n_skip += int(((~move) & need).sum()) @@ -290,7 +294,8 @@ def sim_libro(Weff: np.ndarray, R: np.ndarray, r_hedge: np.ndarray | None, cap0: w = w_new tot = n_fill + n_skip return dict(net=nets, eq=eq, n_fill=n_fill, n_skip=n_skip, n_lot0=n_lot0, - pct_eseguite=(100.0 * n_fill / tot) if tot else np.nan) + pct_eseguite=(100.0 * n_fill / tot) if tot else np.nan, + pct_nozionale=(100.0 * ntl_done / ntl_req) if ntl_req > 0 else np.nan) def metriche(net: np.ndarray, idx: pd.DatetimeIndex) -> dict: @@ -393,6 +398,19 @@ def main() -> None: print(f" deviazione |%| mediana {ck['dev_pct'].abs().median():.2f}% " f"max {ck['dev_pct'].abs().max():.2f}% ({ck['dev_pct'].abs().idxmax()}) " f"asset controllati {len(ck)}") + mv = [float(np.abs(pd.read_parquet(RAW / f"hl_{sy.lower()}_1d.parquet", columns=["close"]) + ["close"].pct_change().dropna()).median()) * 100 for sy in ck.index] + print(f" movimento |giornaliero| tipico di questi asset: {float(np.median(mv)):.2f}% -> " + f"una deviazione mediana del {ck['dev_pct'].abs().median():.2f}% su un feed vecchio " + f"{ck['eta_h'].median():.0f}h e' cio' che ci si attende (non e' un disaccordo di venue).") + try: + from src.data.downloader import load_data as _ld + bd = float(_ld("BTC", "1h")["close"].iloc[-1]) + hl = float(meta.loc["BTC", "markPx"]) + print(f" cross-check su un TERZO venue: BTC Deribit certificato ${bd:,.0f} vs " + f"HL API ${hl:,.0f} = {100*(hl-bd)/bd:+.2f}%") + except Exception as e: + print(f" (cross-check Deribit non disponibile: {type(e).__name__})") print(" -> un feed TESTNET non supera questo controllo: i suoi prezzi sono scollegati.") tk = fee_sched @@ -497,8 +515,7 @@ def main() -> None: f"{'>=$5 [D]':>10}{'>=$10 [C]':>11}{'reb>=$10':>10}") R_xs = dret idx = C.index - base = None - for cap in CAPITALI: + for cap in CAPITALI_XS: pct5 = 100.0 * float((tk_nz * cap >= MIN_ORDER_ASSUNTO).mean()) pct10 = 100.0 * float((tk_nz * cap >= MIN_ORDER_HL).mean()) pr10 = 100.0 * float((reb * cap >= MIN_ORDER_HL).mean()) @@ -513,15 +530,16 @@ def main() -> None: print(f" (`turn=|dW|` prima dello scaling): e' il motivo per cui sta sotto il " f"{m_off['sharpe']:.2f} pubblicato.") print(f" {'cap XS01':>10}{'Sh mod':>9}{'Sh $5 [D]':>11}{'Sh $10 [C]':>12}" - f"{'haircut $10':>13}{'ordini fatti':>14}") - for cap in CAPITALI: + f"{'haircut $10':>13}{'eseguito':>21}") + for cap in CAPITALI_XS: m0 = sim_libro(Wxs, R_xs, None, cap, None, None, None, fee_xs_mod) m5 = sim_libro(Wxs, R_xs, None, cap, MIN_ORDER_ASSUNTO, None, None, fee_xs_mod) m10 = sim_libro(Wxs, R_xs, None, cap, MIN_ORDER_HL, lot_xs, px_xs, fee_xs_mod) a0, a5, a10 = (metriche(m["net"], idx) for m in (m0, m5, m10)) hc = (a0["sharpe"] - a10["sharpe"]) / abs(a0["sharpe"]) * 100 if a0["sharpe"] else np.nan print(f" {cap:>10,.0f}{a0['sharpe']:>9.2f}{a5['sharpe']:>11.2f}{a10['sharpe']:>12.2f}" - f"{hc:>12.0f}%{m10['pct_eseguite']:>9.0f}%") + f"{hc:>12.0f}%{m10['pct_eseguite']:>8.0f}% ord " + f"{m10['pct_nozionale']:>4.0f}% ntl") # ---------------------------------------------------------------- 4. XSR01 print(f"\n[4] XSR01 — soglia PUBBLICATA ~$5.000, gate 2026-10-23 con haircut a $5.000 <= 40%") @@ -555,7 +573,7 @@ def main() -> None: print(f" gambe {len(sx)} lordo mediano {np.median(np.abs(Wx).sum(axis=1)):.2f}x " f"peso mosso mediano/gamba {np.median(tkx)*100:.3f}% p10 {np.percentile(tkx,10)*100:.3f}%") print(f"\n {'cap':>9}{'ticket med':>12}{'Sh mod':>9}{'Sh $5 [D]':>11}{'Sh $10 [C]':>12}" - f"{'haircut $5':>12}{'haircut $10':>13}{'ordini fatti':>14}") + f"{'haircut $5':>12}{'haircut $10':>13}{'eseguito':>21}") hc10_5000 = np.nan for cap in CAPITALI: m0 = sim_libro(Wx, Rx, rbx, cap, None, None, None, FEE_LEG_MODELLO) @@ -568,9 +586,22 @@ def main() -> None: hc10_5000 = h10 print(f" {cap:>9,.0f}{np.median(tkx)*cap:>12.2f}{a0['sharpe']:>9.2f}" f"{a5['sharpe']:>11.2f}{a10['sharpe']:>12.2f}{h5:>11.0f}%{h10:>12.0f}%" - f"{m10['pct_eseguite']:>9.0f}%") + f"{m10['pct_eseguite']:>8.0f}% ord {m10['pct_nozionale']:>4.0f}% ntl") print(f" -> haircut a $5.000 col pavimento VERO ($10): {hc10_5000:.0f}% " f"(soglia pre-registrata {XSR_SOGLIA_HAIRCUT*100:.0f}%)") + print(f"\n la colonna haircut NON e' monotona (+35% a $2k, -6% a $4k): a un capitale solo e'") + print(f" una statistica RUMOROSA. Banda: haircut ricalcolato a pavimento $8/$10/$12 ->") + print(f" {'cap':>9}{'h($8)':>9}{'h($10)':>9}{'h($12)':>9}{'ampiezza':>11} giudizio") + for cap in CAPITALI: + s0 = metriche(sim_libro(Wx, Rx, rbx, cap, None, None, None, FEE_LEG_MODELLO)["net"], idxx)["sharpe"] + hs = [] + for mo in (8.0, 10.0, 12.0): + sh = metriche(sim_libro(Wx, Rx, rbx, cap, mo, lot_x, px_x, FEE_LEG_MODELLO)["net"], idxx)["sharpe"] + hs.append((s0 - sh) / abs(s0) * 100) + amp = max(hs) - min(hs) + g = ("ROTTO" if min(hs) > XSR_SOGLIA_HAIRCUT * 100 else + "ok" if max(hs) < XSR_SOGLIA_HAIRCUT * 100 else "INDECIDIBILE") + print(f" {cap:>9,.0f}{hs[0]:>8.0f}%{hs[1]:>8.0f}%{hs[2]:>8.0f}%{amp:>10.0f}pp {g}") print(f" NB: e' la lettura del PARAMETRO sul backtest, NON il gate del 23/10 — " f"quello si decide sulla finestra FORWARD e anticiparlo sarebbe selezione.") @@ -596,9 +627,41 @@ def main() -> None: print(f" {nm:<44}{a_['sharpe']:>8.2f}{a_['maxdd']:>7.1f}%{a_['cagr']:>7.1f}%") print(f" (a capitale infinito = nessun vincolo di min-order: isola il COSTO dal PAVIMENTO)") + print(f"\n [B] SENSIBILITA' AL PAVIMENTO a $5.000 (l'haircut a un punto solo e' rumore:") + print(f" qui si vede DOVE il parametro comincia a mordere)") + print(f" {'pavimento $':>13}{'Sharpe':>9}{'haircut':>10}{'ord fatti':>11}{'ntl mosso':>11}") + sh0 = metriche(sim_libro(Wx, Rx, rbx, 5000.0, None, None, None, FEE_LEG_MODELLO)["net"], idxx)["sharpe"] + for mo in (0.0, 5.0, 10.0, 15.0, 20.0, 30.0, 50.0): + m = sim_libro(Wx, Rx, rbx, 5000.0, mo if mo > 0 else None, None, None, FEE_LEG_MODELLO) + aa = metriche(m["net"], idxx) + print(f" {mo:>13.0f}{aa['sharpe']:>9.2f}" + f"{(sh0-aa['sharpe'])/abs(sh0)*100:>9.0f}%" + f"{m['pct_eseguite'] if np.isfinite(m['pct_eseguite']) else 100:>10.0f}%" + f"{m['pct_nozionale'] if np.isfinite(m['pct_nozionale']) else 100:>10.0f}%") + + print(f"\n [B] BREAK-EVEN DEL COSTO — quanto dovrebbe valere lo slippage perche' il " + f"'rischio #1' fosse reale:") + print(f" {'costo/gamba':>13}{'= taker + slip':>18}{'Sharpe':>9}{'CAGR':>8}") + be = None + for bps in (4.5, 5.0, 7.5, 10.0, 15.0, 20.0, 30.0, 40.0): + aa = metriche(sim_libro(Wx, Rx, rbx, 5000.0, None, None, None, bps / 1e4)["net"], idxx) + if be is None and aa["sharpe"] < 1.0: + be = bps + print(f" {bps:>11.1f} bps{max(bps-4.5,0):>13.1f} slip{aa['sharpe']:>9.2f}" + f"{aa['cagr']:>7.1f}%") + slip_mis = float(np.median(fee_real) - taker) * 1e4 + print(f" -> Sharpe scende sotto 1.0 a ~{be:.0f} bps/gamba = ~{be-4.5:.0f} bps di slippage,") + print(f" cioe' **{(be-4.5)/max(slip_mis,1e-9):.0f}x** il mezzo spread misurato oggi " + f"({slip_mis:.1f} bps).") + print("\n" + "=" * 104) - print(" LIMITI DICHIARATI: gli spread sono UNO/POCHI snapshot di OGGI su un venue la cui") - print(" liquidita' e' cresciuta -> sono la stima piu' FAVOREVOLE per i 2.6 anni di backtest.") + tt = sorted(b["time"] for v in books.values() for b in v) + print(f" LIMITI DICHIARATI (1) i {a.snaps} snapshot coprono {(tt[-1]-tt[0])/1000:.0f} SECONDI:") + print(" NON sono osservazioni indipendenti, sono UNA fotografia intraday di oggi. E la") + print(" liquidita' di HL e' cresciuta -> e' la stima piu' FAVOREVOLE per i 2.6 anni di") + print(" backtest. Cio' che regge al limite e' il MARGINE (21x), non il livello.") + print(" (2) il lotto in $ usa il mark di OGGI su 2.6 anni di storia: irrilevante perche' il") + print(" lotto non e' mai vincolante (max $1.69 contro $10 di pavimento), ma va detto.") print(" Il pavimento $10 e' [C] (doc + messaggio d'errore), non provato da un ordine: provarlo") print(" richiederebbe di mandarne uno, e questo audit non tocca il conto.") print("=" * 104) diff --git a/scripts/research/r0822_leverage_skeptic.py b/scripts/research/r0822_leverage_skeptic.py index 65ce9b3..963730a 100644 --- a/scripts/research/r0822_leverage_skeptic.py +++ b/scripts/research/r0822_leverage_skeptic.py @@ -302,11 +302,39 @@ def main() -> dict: print("\n IL TEST DI SUPER-PROPORZIONALITA' (la domanda esatta del brief):") dpk = [delta_p[j] / KS[j] for j in range(len(KS))] + rap = [dd_acc[j] / dd_close[j] for j in range(len(KS))] j1 = KS.index(1.00) - print(f" Δ/k a k=0.50 {dpk[0]:+.3%} · 1.00 {dpk[j1]:+.3%} · 1.50 {dpk[KS.index(1.50)]:+.3%}" + print(" Δ per UNITA' di leva (se il minimo intraday amplificasse piu' che") + print(" proporzionalmente, questa riga CRESCEREBBE in modulo con k):") + print(f" Δ/k k=0.50 {dpk[0]:+.3%} · 1.00 {dpk[j1]:+.3%} · 1.50 {dpk[KS.index(1.50)]:+.3%}" f" · 2.00 {dpk[KS.index(2.00)]:+.3%} · 5.00 {dpk[-1]:+.3%}") - cresce = dpk[KS.index(2.00)] < dpk[j1] # piu' negativo = piu' costoso per unita' di leva - print(f" -> Δ/k {'CRESCE in modulo con k = super-proporzionale' if cresce else 'NON cresce in modulo: l effetto e circa PROPORZIONALE'}.") + print(f" rapporto maxDD acc./close: {' · '.join(f'{r:.3f}' for r in rap)}") + super_prop = dpk[KS.index(2.00)] > dpk[j1] * 1.05 + sotto = dpk[KS.index(2.00)] < dpk[j1] * 0.95 + verdetto = ("SUPER-proporzionale" if super_prop else + ("SOTTO-proporzionale" if sotto else "proporzionale entro il rumore")) + print(f"\n ❌ RISPOSTA: NO — Δ/k **CALA** in modulo (da {dpk[0]:.3%} a {dpk[-1]:.3%}) e il") + print(f" RAPPORTO fra le due maxDD e' PIATTO ({min(rap):.3f}-{max(rap):.3f}) su tutta la") + print(f" griglia. La lente accoppiata e' un RICARICO MOLTIPLICATIVO COSTANTE di ~" + f"{(np.mean(rap)-1)*100:.1f}%") + print(f" sul maxDD, non un'amplificazione: verdetto **{verdetto}**.") + print(" Meccanismo: il maxDD di 25 anni e' una statistica MULTI-GIORNO (una discesa di") + print(" settimane), il wick e' un fatto di UN giorno. Aggiungere qualche punto di tuffo") + print(" a una discesa gia' lunga sposta poco, e la parte della distribuzione che") + print(" conterebbe non c'e': nel 26,8% dei giorni il libro CHIUDE sul minimo (gap = 0)") + print(" e quei giorni sono proprio i brutti. A leva alta Δ/k cala anche perche' i") + print(" drawdown si allungano piu' in fretta di quanto il singolo tuffo cresca.") + + print("\n ⚠️ MA C'E' UN'ECCEZIONE, ed e' quella che decide la frontiera: la PROBABILITA'") + print(" di sfondare una SOGLIA FISSA e' amplificata assai piu' del maxDD stesso,") + print(" perche' vicino alla soglia la densita' e' fitta.") + print(f"\n {'k':>6}{'P(DD>30%) close':>17}{'P(DD>30%) acc.':>16}{'amplificazione':>16}") + for j, k in enumerate(KS): + pc = float((s0["dd"][:, j, 0] > DD_SOGLIA).mean()) + pa = float((s0["dd"][:, j, 1] > DD_SOGLIA).mean()) + amp = (pa / pc) if pc > 0 else float("nan") + print(f" {k:>6.2f}{pc:>17.1%}{pa:>16.1%}" + f"{(f'{amp:.2f}x' if np.isfinite(amp) else 'n.d.'):>16}") print("\n IL NUMERO CHE DECIDE — L'HAIRCUT DI LEVA IMPLICITO NELLA LENTE:") print(" 'a quale k close-only corrisponde il rischio di DD che la lente accoppiata") @@ -397,6 +425,44 @@ def main() -> dict: print(f" <= {frac:.0%} dell'equity -> costo massimo {sl*frac:.0%} dell'equity per asset a k=1,") print(f" cioe' {sl*frac*2:.0%} se scattano entrambi; diventa il -100% a k = {1/(sl*frac*2):.1f}x.") + # ------------------------------------------------------------------ (3-bis) + sezione("(3-bis) DOVE L'ACCOPPIAMENTO MORDE DAVVERO — la regola a UN GIORNO, sui giorni STORICI") + print("\n Il finding del 25/07 ('close-only e' CIECA', 'l'indipendente RADDOPPIA i breach')") + print(" e' misurato su una regola di DAILY-LOSS: una soglia valutata sul MINIMO di UN") + print(" giorno. Il maxDD di un piano d'accumulo e' un'altra grandezza. Qui le due si") + print(" guardano fianco a fianco, sui GIORNI VERI (nessun bootstrap): quante volte in") + print(f" {len(D['R_del'])/365:.1f} anni una soglia di perdita giornaliera viene sfondata.") + Rh, Gh = D["R_del"], gap + rng2 = np.random.default_rng(909) + Gsh = Gh.copy(); rng2.shuffle(Gsh) + anni_st = len(Rh) / 365.0 + for T in (0.03, 0.05, 0.10): + print(f"\n soglia daily-loss {T:.0%} (breach/anno)") + print(f" {'k':>6}{'close-only':>13}{'accoppiata':>13}{'indip. (finto)':>17}" + f"{'acc./close':>12}{'indip./acc.':>13}") + for k in (1.00, 1.25, 1.50, 2.00, 3.00): + nc = float((Rh * k <= -T).sum()) / anni_st + na = float(((Rh + Gh) * k <= -T).sum()) / anni_st + ni = float(((Rh + Gsh) * k <= -T).sum()) / anni_st + r1 = (na / nc) if nc > 0 else float("inf") + r2 = (ni / na) if na > 0 else float("nan") + print(f" {k:>6.2f}{nc:>13.2f}{na:>13.2f}{ni:>17.2f}" + f"{(f'{r1:.2f}x' if np.isfinite(r1) else 'INF'):>12}" + f"{(f'{r2:.2f}x' if np.isfinite(r2) else 'n.d.'):>13}") + print("\n 📌 IL RISULTATO TRASFERIBILE DEL FILONE: la lezione del 25/07 e' VERA e NON SI") + print(" TRASFERISCE. Su una regola a UN GIORNO l'accoppiamento cambia il conto di un") + print(" fattore (e close-only puo' dare ZERO dove il vero e' positivo: cecita' esatta);") + print(" su una statistica MULTI-GIORNO come il maxDD di un piano d'accumulo lo stesso") + print(" difetto vale un ricarico costante di pochi punti percentuali. Il gate") + print(" pre-registrato ha importato una lezione dal contesto in cui e' stata misurata") + print(" (conti FUNDED, barriera di daily-loss) a un contesto in cui la grandezza") + print(" decisiva e' un'altra — stessa forma dell'errore che il progetto ha gia'") + print(" codificato il 26/07 sulla barra parziale (SKH01 +0.38 / TP01 ±0.03: 'la") + print(" conclusione non si trasferisce fra sleeve').") + print(" ⚠️ Il che NON assolve close-only in generale: sul canale PROP/FUNDED, dove il") + print(" vincolo binding e' esattamente una regola di daily-loss per-conto, la lente") + print(" accoppiata resta obbligatoria e la leva li' e' un'altra decisione.") + # ------------------------------------------------------------------ (4) sezione("(4) IL GRADINO ESEGUIBILE — sopravvive alla lente? (€500/mese, muro netto)") s500 = simula(D["R_del"], gap, KS, anni=ANNI, dep_eur=500, bersaglio=BERSAGLIO, seed=SEED) @@ -443,10 +509,10 @@ def attacco2() -> None: f" <- lo scetticato dichiara 12,14%") print(f" -> VERIFICATO. E il numero non e' un artefatto della sua ricostruzione.") - print("\n ⚠️ MA IL MECCANISMO CHE DICHIARA E' INCOMPLETO, e la parte che manca e' la") - print(" maggiore. Lo scetticato scrive: «il target vale sulla posizione quando c'e', e") - print(" TP01 e' long-flat». Decomposizione:") - convs, vols_pred, vols_real, sers = [], [], [], {} + print("\n ⚠️ MA IL MECCANISMO CHE DICHIARA SPIEGA CIRCA META' DEL DIVARIO, e la meta'") + print(" che manca porta a una conclusione operativa diversa. Lo scetticato scrive:") + print(" «il target vale sulla posizione quando c'e', e TP01 e' long-flat». Misura:") + convs, vols_pred, vols_real, sers, flats = [], [], [], {}, [] for a in ("BTC", "ETH"): df = resample_1d(load_data(a, "1h")) c = df["close"].values.astype(float) @@ -461,6 +527,7 @@ def attacco2() -> None: sers[a] = pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"])) ok = np.isfinite(d) & np.isfinite(vol) & (vol > 0) convs.append(float(np.sqrt(np.mean(d[ok] ** 2)))) + flats.append(float((d[ok] == 0).mean())) # errore del previsore di vol: vol realizzata nei 30g SEGUENTI / vol prevista fwd = pd.Series(r).rolling(30).std().shift(-30).values * np.sqrt(365.25) okf = ok & np.isfinite(fwd) @@ -478,21 +545,35 @@ def attacco2() -> None: comb = (0.5 * J["BTC"] + 0.5 * J["ETH"]).std() * 365 ** .5 div = comb / (0.5 * vb + 0.5 * ve) conv = float(np.mean(convs)) + per_leg = (0.5 * vb + 0.5 * ve) / 0.20 + p_flat = float(np.mean(flats)) + f_flat = float(np.sqrt(1.0 - p_flat)) + f_conv = conv / f_flat if f_flat > 0 else float("nan") print(f"\n gamba singola: BTC {vb:.2%} · ETH {ve:.2%} -> 50/50 combinato {comb:.2%}") print(f" fattore di DIVERSIFICAZIONE BTC/ETH = {div:.3f} (corr {J['BTC'].corr(J['ETH']):.2f})") - print(f"\n CATENA: 20,00% ×{conv:.3f} (convinzione) ×{np.mean(vols_real)/np.mean(vols_pred):.3f} " - f"(previsore di vol) ×{div:.3f} (diversificazione) = " - f"{0.20*conv*(np.mean(vols_real)/np.mean(vols_pred))*div:.2%}") - print(f" misurato: {tp.std()*365**.5:.2%}") - print(f"\n 📌 IL FATTO CHE LO SCETTICATO NON DICE: il pezzo grosso NON e' lo stare flat, e'") - print(f" che `tsmom_blend` e' la MEDIA DEI SEGNI di 3 orizzonti, quindi quando TP01 e'") - print(f" 'a mercato' e' quasi sempre a 1/3 o 2/3 di convinzione. Contando lo zero, il") - print(f" fattore di convinzione da solo vale ×{conv:.2f}. Lo stare flat e' UNA PARTE di") - print(f" quel fattore, non il fattore.") - print(f" Conseguenza operativa opposta a quella suggerita: se il divario fosse solo") - print(f" 'flat', alzare target_vol non aumenterebbe il rischio nei giorni flat (dove non") - print(f" c'e' posizione). Essendo CONVINZIONE, alzare target_vol alza la posizione") - print(f" proprio nei giorni in cui il segnale e' PIU' DEBOLE (1 orizzonte su 3).") + print(f"\n SCOMPOSIZIONE ESATTA (i due fattori si moltiplicano e chiudono il conto):") + print(f" 20,00% ×{per_leg:.3f} (dentro la GAMBA) ×{div:.3f} (diversificazione BTC/ETH)" + f" = {0.20*per_leg*div:.2%}") + print(f" misurato sullo sleeve di produzione: {tp.std()*365**.5:.2%}") + print(f"\n E il ×{per_leg:.3f} della gamba si spacca a sua volta in DUE meta' quasi uguali:") + print(f" · sta FLAT nel {p_flat:.1%} delle barre (direzione 0) -> ×{f_flat:.3f}") + print(f" · quando NON e' flat sta a convinzione PARZIALE (rms {conv/f_flat:.2f}) -> ×{f_conv:.3f}") + print(f" prodotto sqrt(E[d²]) = ×{conv:.3f}; il residuo fino a ×{per_leg:.3f} e'") + print(f" la correlazione fra convinzione e vol realizzata/prevista" + f" (previsore di vol da solo: ×{np.mean(vols_real)/np.mean(vols_pred):.3f}).") + print(f"\n 📌 QUELLO CHE LO SCETTICATO NON DICE, e sono DUE cose.") + print(f" (i) «TP01 e' long-flat» spiega **circa META'** del divario (×{f_flat:.2f}), non") + print(f" il divario. L'altra meta' e' che `tsmom_blend` e' la MEDIA DEI SEGNI di 3") + print(f" orizzonti: quando TP01 e' a mercato sta quasi sempre a 1/3 o 2/3 di") + print(f" convinzione (×{f_conv:.2f}). E la differenza NON e' accademica: se il divario") + print(f" fosse solo 'flat', alzare target_vol non toccherebbe i giorni senza") + print(f" posizione; essendo CONVINZIONE, alzarlo aumenta la size proprio nei giorni") + print(f" in cui il segnale e' PIU' DEBOLE (1 orizzonte su 3 d'accordo).") + print(f" (ii) un terzo fattore non sta nemmeno nello sleeve: ×{div:.2f} viene dal") + print(f" COMBINARE BTC ed ETH a 50/50 (corr {J['BTC'].corr(J['ETH']):.2f}). Non e' un") + print(f" difetto d'etichetta: e' diversificazione, ed e' esattamente cio' che si") + print(f" vuole. Chiamare 'sovrastima del rischio' anche questo pezzo confonde una") + print(f" proprieta' desiderata con un errore di misura.") print("\n E TP01 E' UN CASO PARTICOLARE, non la regola — gli altri sleeve vol-targeted:") print(f"\n {'sleeve':>9}{'target dichiarato':>19}{'vol REALIZZATA':>16}{'su barre ATTIVE':>17}{'attive':>9}") @@ -590,6 +671,59 @@ def attacco2() -> None: print(" esattamente quella protetta dal test di guardia.") +def attacco2b() -> None: + """Quali affermazioni pubblicate cambiano, e quali no — misurato, non argomentato.""" + from src.portfolio import sleeves as SL + sezione("(6-bis) ATTACCO 2c — QUALI AFFERMAZIONI CAMBIANO (e la maggior parte NON cambia)") + tp, sk = SL._tp01_returns(), SL._skyhook_returns() + if tp.index.tz is None: + tp = tp.tz_localize("UTC") + if sk.index.tz is None: + sk = sk.tz_localize("UTC") + B = pd.concat({"TP01": tp, "SKH01": sk}, axis=1, join="inner").fillna(0.0) + w = np.array([W_TP, W_SKH]) + X = B[["TP01", "SKH01"]].values + bk = X @ w + cov = np.cov(X.T) + var_b = float(bk.var()) + rc = w * (cov @ w) / (cov @ w @ w) + print("\n NON cambia NIENTE di misurato. Ogni Sharpe, maxDD, muro, traiettoria e rendita") + print(" pubblicati sono calcolati sulla SERIE DI RITORNI REALIZZATA, mai sull'etichetta:") + print(" `target_vol` entra nel codice come parametro di sizing e non compare in nessuna") + print(" formula di rischio a valle. Un'etichetta che sovrastima non rende ottimista un") + print(" numero che non la usa.") + print("\n CAMBIA una classe di affermazioni sola: quelle che CONFRONTANO un parametro con") + print(" una misura. Nel progetto ce n'e' una, ed e' nel libro LIVE:") + print(f"\n {'sleeve':>8}{'peso NOZIONALE':>16}{'vol realizzata':>16}" + f"{'contributo al RISCHIO':>23}") + for i, nm in enumerate(("TP01", "SKH01")): + print(f" {nm:>8}{w[i]:>16.0%}{X[:, i].std()*365**.5:>16.2%}{rc[i]:>23.1%}") + print(f" {'libro':>8}{1.0:>16.0%}{bk.std()*365**.5:>16.2%}{sum(rc):>23.1%}") + print(f" corr TP01/SKH01 {np.corrcoef(X.T)[0,1]:+.3f} vol libro {bk.std()*365**.5:.2%}") + sig = X.std(axis=0) * 365 ** 0.5 + cf = (W_TP * 0.20) ** 2 / ((W_TP * 0.20) ** 2 + (W_SKH * sig[1]) ** 2) + print("\n ❌ IPOTESI MIA, REFUTATA DAL MIO STESSO NUMERO. Mi aspettavo che l'etichetta") + print(" nascondesse una ripartizione del rischio molto diversa dai pesi di nozionale.") + print(f" Non e' cosi': {rc[0]*100:.0f}/{rc[1]*100:.0f} contro {W_TP*100:.0f}/{W_SKH*100:.0f}" + f" — coincidono entro un paio di punti, e per una") + print(" ragione che si legge in una riga: contributi di rischio ~ (w·σ)² con corr") + print(f" {np.corrcoef(X.T)[0,1]:+.2f} ~ 0, e {W_TP:.2f}×{sig[0]:.1%} = {W_TP*sig[0]:.2%} contro" + f" {W_SKH:.2f}×{sig[1]:.1%} = {W_SKH*sig[1]:.2%}") + print(" stanno gia' in rapporto ~75/25. **Coincidenza numerica, non struttura.**") + print("\n 📌 IL FATTO CHE RESTA, ed e' piu' utile dell'ipotesi che ho perso: l'errore non") + print(" lo commette chi legge i PESI, lo commette chi legge l'ETICHETTA. Un lettore che") + print(f" prendesse `target_vol=20%` per buono calcolerebbe TP01 al {cf*100:.0f}% del rischio") + print(f" di libro; il vero e' {rc[0]*100:.0f}%. L'etichetta non rende ottimista nessun numero") + print(" pubblicato (nessuno la usa) — sposta di ~16 punti l'ATTRIBUZIONE del rischio,") + print(" cioe' proprio la domanda 'quale gamba mi fa male' su cui il progetto ha gia'") + print(" tre misure indipendenti che indicano SKH01 come la fragile (LOO d'ancora,") + print(" sensibilita' alla fee 4x, vol realizzata piu' alta).") + print("\n E si precisa una frase: «SKH01 e' l'unica delle 5 a NON essere vol-targeted» e'") + print(" esatta sul CODICE e va letta insieme al fatto che e' anche quella che di vol ne") + print(" realizza di PIU' (21,0% contro 12,1%). Non e' una contraddizione: e' il motivo per") + print(" cui il peso 25% e' stato confermato tre volte all'estremo prudente.") + + def guardia() -> None: sezione("(7) IL VINCOLO DURO — cosa protegge davvero il test di guardia") cfg = json.loads((ROOT / "config" / "live.json").read_text()) @@ -614,5 +748,6 @@ def guardia() -> None: if __name__ == "__main__": main() attacco2() + attacco2b() guardia() print(f"\n▸ totale: consegna nel messaggio dell'agente.") diff --git a/scripts/research/r0822_prop_alloc.py b/scripts/research/r0822_prop_alloc.py index cb2813c..603b451 100644 --- a/scripts/research/r0822_prop_alloc.py +++ b/scripts/research/r0822_prop_alloc.py @@ -540,8 +540,11 @@ def main() -> None: f"-> {'INDISTINGUIBILI' if abs(dA) < 2*seA else 'differenza reale'}") print(f" argmax J - libro LIVE : {dB:+.4f} +- {seB:.4f} " f"-> {'INDISTINGUIBILI' if abs(dB) < 2*seB else 'differenza reale'}") - print(" La prima riga E' il risultato del filone: progettare per la barriera invece che") - print(" per lo Sharpe NON sposta i pesi. La seconda dice dove sta il guadagno vero.") + quota = abs(dA) / abs(dB) if dB != 0 else float("nan") + print(f" La prima riga E' il risultato del filone: riallocare per la BARRIERA invece che") + print(f" per lo SHARPE vale {dA:+.3f} di J, cioe' il {quota:.0%} del {dB:+.3f} che vale") + print(f" passare dal libro LIVE a un libro diversificato QUALSIASI. Il grosso del guadagno") + print(f" NON e' nella funzione obiettivo: e' nell'aggiungere lo sleeve che a $600 non entra.") print("\n Le prime 8 celle per J:") print(f" {'#':>3} {'pesi':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} {'P(vivo)':>8} " @@ -688,13 +691,16 @@ def main() -> None: for lv in LEVS: rows.append((w, lv, objective(w, lv, "HYRO", ev_idx, fu_idx, drift_mult=dmt))) w_, lv_, o_ = max(rows, key=lambda x: x[2]["J"]) - oL = max((objective({"TP01": 0.75, "SKH01": 0.25}, lv, "HYRO", ev_idx, fu_idx, - drift_mult=dmt) for lv in LEVS), key=lambda x: x["J"]) + lvL, oL = max(((lv, objective({"TP01": 0.75, "SKH01": 0.25}, lv, "HYRO", ev_idx, fu_idx, + drift_mult=dmt)) for lv in LEVS), key=lambda x: x[1]["J"]) + dd_, sd_ = paired_delta(w_, lv_, {"TP01": 0.75, "SKH01": 0.25}, lvL, "HYRO", ev_idx, fu_idx) print(f" {mult:>12.2f} {wkey(w_):>16} {lv_:>6.3f} {o_['J']:>7.3f} {o_['p_pass']:>8.1%} " - f"{o_['p_alive']:>8.1%} {o_['e_payout']:>9,.0f}$ (LIVE 75/25: J {oL['J']:.3f})") - print("\n Se anche a drift XS01 = 0 (cioe' XS01 ridotto a pura diversificazione di VOLATILITA',") - print(" senza alcun rendimento atteso) la configurazione diversificata batte il libro LIVE,") - print(" allora il risultato NON dipende dall'edge di XS01 ma dalla sua DECORRELAZIONE.") + f"{o_['p_alive']:>8.1%} {o_['e_payout']:>9,.0f}$ LIVE {oL['J']:.3f} " + f"delta {dd_:+.3f} +-{sd_:.3f}") + print("\n La colonna che decide e' l'ULTIMA. Se il vantaggio svanisce col drift di XS01, il") + print(" risultato e' un'ipotesi sul RENDIMENTO ATTESO di XS01 misurato sulla sua finestra di") + print(" scoperta; se resta a drift 0, il vantaggio e' pura DECORRELAZIONE e non dipende da") + print(" quell'ipotesi. Il numero, non la narrazione, dice quale delle due.") # ---------------------------------------------------------------- F. VRP01 / GTAA01 tick("E. banda ancora") @@ -715,6 +721,10 @@ def main() -> None: lab = "nessuna (base)" if extra is None else f"{extra} @ {wx:.0%}" print(f" {lab:>34} {lv:>6.3f} {o['J']:>7.3f} {o['p_pass']:>8.1%} {o['p_alive']:>8.1%} " f"{o['sharpe']:>7.2f} {o['e_payout']:>9,.0f}$") + print("\n Le righe VRP01 e GTAA01 escono quasi IDENTICHE: non e' un errore, e' che sotto questa") + print(" funzione obiettivo i due sleeve sono lo stesso oggetto (vol ~6.2%, drift ~7.7%, corr ~0") + print(" col crypto). La barriera vede vol, drift e correlazione; la differenza fra i due — skew") + print(" -4.6 contro -0.4 — a peso 10-20% e' diluita sotto la soglia di daily-loss.") print("\n ATTENZIONE: la riga VRP01 e' calcolata con **gap = 0** (il mark infra-settimana non") print(" esiste nel dataset dello sleeve): e' un TETTO, non una stima. Con il mark misurato il") print(" 30/07 (peggiore infra-settimana mediano -6.9%, minimo -81.7%) una barriera STATICA al") @@ -815,10 +825,22 @@ def main() -> None: seed=20260822, lens="coupled", start=START_WIN) print(f" {fct:>9.2f} {lab:>26} {r['med']:>10.2f} {r['p_target']:>10.1%} " f"{r['p_zero']:>8.1%}") + print("\n ⚠ Il headline ~6% del 25/07 NON e' la riga 'LIVE su tutti i conti' (quella li' era") + print(" CONC-2SL, dichiarata 'la PEGGIORE'): era la politica MISTA, sleeve diversi su conti") + print(" diversi. Replica ESATTA di quella politica, stessa macchina, de-luck 0.60, leva 0.75:") + pc.DELUCK = 0.60 + cfgs25 = [(0.75, 0.25, 0.0), (0.55, 0.20, 0.25), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)] + r25 = pc.simulate(cfgs25, [1, 4, 1, 2, 1, 4], n_paths=1500, lev=0.75, seed=20260822, + lens="coupled", start=START_WIN) + print(f" MISTO (25/07) @0.75x, de-luck 0.60 : EUR/g med {r25['med']:.2f} | " + f"P(>=50/g) {r25['p_target']:.1%} | P(zero) {r25['p_zero']:.1%}") + print(f" -> il 25/07 pubblico' P(>=50/g) 5.6-6.5% e P(zero) 52% a 0.75x. " + f"{'REPLICA OK' if 0.03 <= r25['p_target'] <= 0.10 else 'NON REPLICA — leggere con cautela'}") pc.DELUCK = DELUCK - print("\n La cella in alto a sinistra (0.60 + LIVE) e' la configurazione del 25/07 e va letta") - print(" come replica: se riproduce ~6% di P(>=50/g) e ~52-65% di P(zero), la macchina e' la") - print(" stessa e le altre tre celle misurano cio' che dichiarano.") + print("\n Lettura della 2x2: il de-luck e l'allocazione contribuiscono ENTRAMBI, e vanno citati") + print(" separati. Il fattore 0.60 era una stima a occhio poi RI-MISURATA troppo severa (26/07):") + print(" usarlo ancora sarebbe sbagliato, ma il salto da 1.7% a 41.7% e' il prodotto di due") + print(" correzioni, non di una scoperta.") print("\n" + "=" * 104) print(f" fatto in {time.time()-t0:.0f}s") diff --git a/scripts/research/r0822_skew.py b/scripts/research/r0822_skew.py index 37b2dec..206c258 100644 --- a/scripts/research/r0822_skew.py +++ b/scripts/research/r0822_skew.py @@ -40,7 +40,6 @@ Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822_skew.py from __future__ import annotations import glob -import os import sys import time from pathlib import Path @@ -57,8 +56,6 @@ import altlib as A # noqa: E402 import cblib as CB # noqa: E402 CHAIN = ROOT / "data" / "raw" / "cb_chain" -SCRATCH = Path(os.environ.get("SKEW_SCRATCH", "/tmp/claude-1001/-opt-docker-PythagorasGoal/" - "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad")) ASSETS = ("BTC", "ETH") COLS = ["ts", "asset", "instrument_name", "option_type", "strike", "exp", "bid", "ask", "mid", "iv", "delta", "open_interest", "quote_status"] diff --git a/scripts/research/r0822_slip_audit.py b/scripts/research/r0822_slip_audit.py new file mode 100644 index 0000000..c51939c --- /dev/null +++ b/scripts/research/r0822_slip_audit.py @@ -0,0 +1,509 @@ +"""r0822 SLIP-AUDIT — l'attrito VERO del libro live: quanto costa davvero un fill. + +DOMANDA +------- +Il progetto modella le fee con cura chirurgica (3,50 bps/lato misurati sui fill reali il 21/08) +ma ASSUME il fill al prezzo di chiusura della barra. Lo **slippage realizzato** del libro live +non e' mai stato misurato: e' dichiarato "rischio #1" per XSR01 e semplicemente ignorato per +TP01/SKH01. Ci sono 18 esecuzioni reali dal 2026-07-14. Questo script le misura. + +La domanda decidibile e' UNA: i backtest girano a `fee_rt=0.001` (10 bps RT) mentre la fee vera +e' 3,50 bps/lato = 7 bps RT. C'e' **3 bps RT = 1,50 bps/LATO** di margine gia' pagato dal modello. +**Lo slippage realizzato ci sta dentro?** + +TRE DIFETTI DEL DATO TROVATI PRIMA DI MISURARE (e riparati/dichiarati qui) +------------------------------------------------------------------------- +(1) **`ts_utc` nel log NON e' l'ora del fill.** `book_execute.py:213` scrive + `ts_utc=str(pd.Timestamp(r['last_data']))` = l'etichetta dell'ULTIMA BARRA DEL FEED, non + l'orologio. Tutti e 18 i record dicono `00:00:00`. L'ora vera si recupera solo dalle + intestazioni di `logs/cron_book.log` (`===== cron_book =====`). Senza questo passo + ogni confronto "al prezzo dell'ora del fill" misurerebbe la mezzanotte. + Prova che serviva: il fill #1 e' etichettato `2026-07-08` ed e' stato eseguito il + **2026-07-14T14:00Z** — sei giorni dopo (feed-freeze del 14/07, gia' noto al progetto). +(2) **Il feed certificato NON e' lo strumento che si trada.** `rebuild_history.DERIBIT_INSTR` + prende `BTC/USD:BTC` / `ETH/USD:ETH` = i perpetual **INVERSE**; `deribit.INSTRUMENT` esegue su + `BTC_USDC-PERPETUAL` / `ETH_USDC-PERPETUAL` = i **LINEARI USDC**. Il commento nel sorgente dice + "inverse ~ lineare entro 3 bps": **3 bps e' esattamente la taglia della domanda**. Misurare lo + slippage contro il feed certificato significherebbe misurare la BASE, non lo slippage. + Qui la base viene MISURATA (non assunta) e il riferimento vero e' lo strumento tradato. + Stessa forma del difetto di `fee_watch` del 21/08. +(3) **Convenzione LINEARE**: `amount` = quantita' BASE, `fee` gia' in USDC -> + `fee_bps = fee / (filled * price) * 1e4`. La convenzione inverse darebbe ~8 ordini di + grandezza di errore SENZA sollevare nulla. Il controllo e' stampato in chiaro. + +TRE RIFERIMENTI, TENUTI SEPARATI (il punto 1c della missione) +------------------------------------------------------------ +Il modello decide sulla barra GIORNALIERA, il cron esegue all'ORA: sono grandezze diverse. + REF-A "modello" = close della barra 1d etichettata `last_data` (= il prezzo a cui il + BACKTEST assume di riempire). Misura modello-vs-realta', NON slippage. + REF-B "5m/1h" = close dell'ultima barra CHIUSA prima del fill sullo strumento TRADATO. + Questo, e solo questo, e' slippage d'esecuzione. + REF-C "VWAP 1h" = VWAP delle 12 barre 5m dell'ora che contiene il fill (benchmark + NON causale, dichiarato tale). +Segno orientato al lato: `costo_bps = lato * (fill - ref)/ref * 1e4`, lato +1 buy / -1 sell. +Positivo = costo, negativo = guadagno. + +LA STATISTICA CHE HA POTENZA (e perche' quella in bps non ne ha) +---------------------------------------------------------------- +Con n=18 la via in bps e' persa in partenza: il rumore e' la deriva del prezzo fra la chiusura +della barra 5m e il fill, che vale decine di bps. La statistica giusta e' NORMALIZZATA dal range +della barra: il nostro fill e' esso stesso un print della barra 5m che lo contiene, quindi + q = (fill - low)/(high - low) per un BUY, q = 1 - quello per un SELL. +Se i nostri ordini market avessero impatto, un BUY stamperebbe AL MASSIMO della barra -> q -> 1. +Senza impatto q e' un'estrazione ~uniforme -> E[q] = 0.5. sigma_q = 1/sqrt(12) = 0.289 e' ~20x +piu' piccolo del rumore in bps. + +Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822_slip_audit.py + (--no-net salta il fetch dello strumento lineare e lo dichiara non misurato) +""" +from __future__ import annotations + +import json +import os +import re +import sys +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +SCRATCH = Path(os.environ.get("SLIP_SCRATCH", "/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad")) + +EXEC_LOG = ROOT / "data" / "live" / "book_executions.jsonl" +CRON_LOG = ROOT / "logs" / "cron_book.log" + +# Sensibilita' PUBBLICATA del libro (r0726_fee_sensitivity.py, CLAUDE.md 26/07). +# Unita': per bps PER LATO (verificato sulla tabella: 0->15 bps/lato = Sharpe 1.849->1.597). +DSHARPE_PER_BPS_SIDE = -0.017 +DCAGR_PER_BPS_SIDE = -0.23 # punti percentuali di CAGR +# Margine gia' pagato dal modello: backtest 10 bps RT vs fee vera 7 bps RT = 3 bps RT. +MARGIN_BPS_SIDE = 1.5 +FEE_TAKER_BPS_SIDE = 3.50 # tier base misurato (fee_watch, 21/08) + +# Strumento TRADATO (src/live/deribit.INSTRUMENT) vs strumento del FEED certificato +# (scripts/analysis/rebuild_history.DERIBIT_INSTR). NON sono lo stesso: vedi difetto (2). +CCXT_LINEAR = {"BTC": "BTC/USDC:USDC", "ETH": "ETH/USDC:USDC"} +CCXT_INVERSE = {"BTC": "BTC/USD:BTC", "ETH": "ETH/USD:ETH"} + +RNG = np.random.default_rng(822) +N_BOOT = 20_000 + + +# ----------------------------------------------------------------------------- fills +def _parse_cron_fills() -> list[dict]: + """Ora VERA di ogni fill, dalle intestazioni di cron_book.log. Vedi difetto (1).""" + if not CRON_LOG.exists(): + raise SystemExit(f"manca {CRON_LOG}: l'ora vera del fill non e' recuperabile altrove") + head = re.compile(r"^===== (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) cron_book =====") + fill = re.compile(r"^\s+-> (BUY|SELL) ([\d.]+) @ \$([\d,.]+) fee ([\d.]+) \((\w+)\)") + out, cur = [], None + with open(CRON_LOG, errors="replace") as f: + for line in f: + m = head.match(line) + if m: + cur = pd.Timestamp(m.group(1)) + continue + m = fill.match(line) + if m: + out.append(dict(fill_ts=cur, side=m.group(1).lower(), + amt=float(m.group(2)), px=float(m.group(3).replace(",", "")), + fee=float(m.group(4)), status=m.group(5))) + return out + + +def load_fills() -> pd.DataFrame: + """JSONL (verita' sui numeri) + cron log (verita' sull'ora), appaiati e VERIFICATI.""" + recs = [json.loads(l) for l in EXEC_LOG.read_text().splitlines() if l.strip()] + cron = _parse_cron_fills() + if len(recs) != len(cron): + raise SystemExit(f"appaiamento impossibile: {len(recs)} righe jsonl vs {len(cron)} fill " + f"nel cron log. Non invento un'ora: mi fermo.") + rows = [] + for i, (r, c) in enumerate(zip(recs, cron)): + # entrambi i file sono append-only dallo stesso processo -> stesso ordine. Lo PROVO. + ok = (r["side"] == c["side"] + and abs(round(r["filled"], 4) - round(c["amt"], 4)) < 1e-9 + and abs(round(r["price"], 1) - round(c["px"], 1)) < 0.051 + and abs(round(r["fee"], 5) - round(c["fee"], 5)) < 1e-9) + if not ok: + raise SystemExit(f"riga {i}: jsonl e cron log NON combaciano ({r} vs {c}) -> mi fermo") + if c["status"] != "OK": + continue + rows.append(dict( + i=i, fill_ts=c["fill_ts"], model_bar=pd.Timestamp(r["ts_utc"], tz="UTC"), + asset=r["asset"], side=r["side"], lato=1.0 if r["side"] == "buy" else -1.0, + filled=float(r["filled"]), price=float(r["price"]), fee=float(r["fee"]), + notional=float(r["filled"]) * float(r["price"]), + skh=r.get("skh_sign"), tp_frac=r.get("tp_frac"), + )) + return pd.DataFrame(rows) + + +# ----------------------------------------------------------------------------- prezzi +def _certified(asset: str, tf: str, t0, t1) -> pd.DataFrame: + """Feed certificato su disco = strumento INVERSE. Letto a colonne e tagliato subito (RAM).""" + f = ROOT / "data" / "raw" / f"{asset.lower()}_{tf}.parquet" + d = pd.read_parquet(f, columns=["timestamp", "open", "high", "low", "close", "volume"]) + d["ts"] = pd.to_datetime(d["timestamp"], unit="ms", utc=True) + d = d[(d["ts"] >= t0) & (d["ts"] <= t1)].drop(columns=["timestamp"]).reset_index(drop=True) + return d + + +def _fetch_linear(asset: str, t0, t1) -> pd.DataFrame | None: + """Strumento TRADATO (lineare USDC) via ccxt pubblico. Cache su scratchpad, pacing gentile. + + Il progetto ha una storia di rate-limit per-IP (29-30/07): qui sono ~13 pagine per asset, + distanziate. Nessuna scrittura sotto data/.""" + SCRATCH.mkdir(parents=True, exist_ok=True) + cache = SCRATCH / f"lin_{asset.lower()}_5m.parquet" + if cache.exists(): + d = pd.read_parquet(cache) + if d["ts"].min() <= t0 and d["ts"].max() >= t1 - pd.Timedelta(minutes=10): + return d + try: + import ccxt, time + ex = ccxt.deribit({"enableRateLimit": True}) + sym, since, rows = CCXT_LINEAR[asset], int(t0.timestamp() * 1000), [] + end = int(t1.timestamp() * 1000) + while since < end: + o = ex.fetch_ohlcv(sym, "5m", since=since, limit=1000) + if not o: + break + rows += o + since = o[-1][0] + 300_000 + time.sleep(0.35) + if not rows: + return None + d = pd.DataFrame(rows, columns=["timestamp", "open", "high", "low", "close", "volume"]) + d = d.drop_duplicates("timestamp").sort_values("timestamp") + d["ts"] = pd.to_datetime(d["timestamp"], unit="ms", utc=True) + d = d.drop(columns=["timestamp"]).reset_index(drop=True) + d.to_parquet(cache) + return d + except Exception as e: # rete assente = si dichiara + print(f" ! fetch lineare {asset} fallito: {type(e).__name__}: {str(e)[:90]}") + return None + + +def _last_closed(bars: pd.DataFrame, t) -> pd.Series | None: + """Ultima barra CHIUSA a `t`: una barra etichettata b chiude a b+durata.""" + if bars is None or bars.empty: + return None + step = bars["ts"].diff().median() + ok = bars[bars["ts"] + step <= t] + return None if ok.empty else ok.iloc[-1] + + +def _containing(bars: pd.DataFrame, t) -> pd.Series | None: + if bars is None or bars.empty: + return None + step = bars["ts"].diff().median() + ok = bars[(bars["ts"] <= t) & (t < bars["ts"] + step)] + return None if ok.empty else ok.iloc[-1] + + +# ----------------------------------------------------------------------------- stats +def ci_mean(x, boot=True): + x = np.asarray(x, float) + x = x[np.isfinite(x)] + n = len(x) + if n < 2: + return dict(n=n, mean=np.nan, lo=np.nan, hi=np.nan, sd=np.nan, med=np.nan) + m, sd = x.mean(), x.std(ddof=1) + if boot: + bs = RNG.choice(x, size=(N_BOOT, n), replace=True).mean(axis=1) + lo, hi = np.percentile(bs, [2.5, 97.5]) + else: + from scipy import stats + h = stats.t.ppf(0.975, n - 1) * sd / np.sqrt(n) + lo, hi = m - h, m + h + return dict(n=n, mean=m, lo=lo, hi=hi, sd=sd, med=float(np.median(x))) + + +def fmt(d, u="bps"): + if not np.isfinite(d["mean"]): + return "n/d" + z = "" if (d["lo"] <= 0 <= d["hi"]) else " <- ZERO ESCLUSO" + return f"{d['mean']:+7.2f} {u} IC95 [{d['lo']:+7.2f}, {d['hi']:+7.2f}] (med {d['med']:+7.2f}, n={d['n']}){z}" + + +def main(): + print("=" * 96) + print(" r0822 SLIP-AUDIT — l'attrito VERO del libro live (18 esecuzioni, dal 2026-07-14)") + print("=" * 96) + + fills = load_fills() + t0 = fills["fill_ts"].min().floor("D") - pd.Timedelta(days=3) + t1 = fills["fill_ts"].max().ceil("h") + pd.Timedelta(hours=2) + + # ---------------------------------------------------------------- 0. difetti del dato + print("\n[0] DIFETTI DEL DATO — trovati PRIMA di misurare\n" + "-" * 96) + lag = (fills["fill_ts"] - fills["model_bar"]).dt.total_seconds() / 3600.0 + print(f" (1) `ts_utc` nel jsonl = etichetta dell'ultima barra del feed, NON l'ora del fill.") + print(f" 18/18 record dicono 00:00:00. Ora vera recuperata da logs/cron_book.log.") + print(f" Scarto fill - etichetta: mediana {lag.median():.1f}h, max {lag.max():.1f}h " + f"({(lag > 26).sum()} fill oltre le 26h).") + if (lag > 26).any(): + w = fills.loc[lag.idxmax()] + print(f" Il caso estremo e' il fill #{w['i']}: etichetta {w['model_bar'].date()}, " + f"eseguito {w['fill_ts']} = feed-freeze del 14/07 (gia' noto al progetto).") + print(f" (2) Feed certificato = INVERSE ({CCXT_INVERSE['BTC']}), libro tradato = LINEARE " + f"({CCXT_LINEAR['BTC']}).") + print(f" La base fra i due e' misurata sotto, non assunta.") + + # ---------------------------------------------------------------- 1. convenzione fee + print("\n[1] CONTROLLO DI CONVENZIONE (lineare USDC) — la trappola da ~8 ordini di grandezza\n" + + "-" * 96) + fills["fee_bps"] = fills["fee"] / fills["notional"] * 1e4 + fb = ci_mean(fills["fee_bps"]) + print(f" fee_bps = fee_usdc / (filled * price) * 1e4 [LINEARE: amount=base, fee gia' in USDC]") + print(f" media {fb['mean']:.3f} bps/lato · mediana {fb['med']:.3f} · " + f"min {fills['fee_bps'].min():.3f} · max {fills['fee_bps'].max():.3f}") + print(f" tier base dichiarato da fee_watch: {FEE_TAKER_BPS_SIDE:.2f} bps/lato -> " + f"scarto {fb['med'] - FEE_TAKER_BPS_SIDE:+.3f} bps") + bad = (fills["fee"] / fills["filled"] * 1e4).median() + print(f" [se avessi usato la convenzione INVERSE: {bad:,.0f} bps -> " + f"{bad / FEE_TAKER_BPS_SIDE:,.0f}x il vero, e senza sollevare nulla]") + odd = fills[(fills["fee_bps"] - FEE_TAKER_BPS_SIDE).abs() > 0.05] + if len(odd): + print(f" ⚠️ {len(odd)}/{len(fills)} fill NON al tier {FEE_TAKER_BPS_SIDE:.2f}:") + for _, o in odd.iterrows(): + print(f" #{o['i']} {o['fill_ts']} {o['asset']} {o['side']} " + f"{o['filled']} @ {o['price']:.2f} = ${o['notional']:.2f} · " + f"fee {o['fee']:.8f} = {o['fee_bps']:.2f} bps") + print(f" -> nozionale ${odd['notional'].min():.2f}: sotto i ~$7 la fee sembra") + print(f" arrotondata AL RIALZO. E' un costo REALE che il modello non ha, e") + print(f" morde solo gli ordini minuscoli (dove il libro vive a $600).") + + # ---------------------------------------------------------------- 2. potenza, PRIMA + print("\n[2] POTENZA DICHIARATA — calcolata PRIMA di guardare lo slippage\n" + "-" * 96) + sig5 = {} + for a in sorted(fills["asset"].unique()): + d = _certified(a, "5m", t0, t1) + r = np.log(d["close"]).diff().dropna() * 1e4 + sig5[a] = float(r.std()) + sig = float(np.mean(list(sig5.values()))) + n = len(fills) + mde_bps = 2.80 * sig / np.sqrt(n) # 80% potenza, bilaterale alpha=0.05 + print(f" Rumore strutturale: sigma dei ritorni 5m nella finestra = " + + " · ".join(f"{a} {v:.1f} bps" for a, v in sig5.items()) + f" (media {sig:.1f})") + print(f" Il riferimento 5m dista dal fill 0-5 min -> il rumore e' di QUELL'ordine.") + print(f" MDE in bps a n={n}, potenza 80%, alpha 5%: 2.80 * {sig:.1f} / sqrt({n}) = " + f"**{mde_bps:.1f} bps/lato**") + print(f" Il margine da difendere e' {MARGIN_BPS_SIDE:.1f} bps/lato (3 bps RT).") + print(f" => VERDETTO DI POTENZA, dichiarato prima del risultato: la via in bps NON puo'") + print(f" risolvere {MARGIN_BPS_SIDE:.1f} bps ({mde_bps / MARGIN_BPS_SIDE:.0f}x troppo grossolana). " + f"Qualunque IC in bps conterra' lo zero;") + print(f" un 'non distinguibile da zero' li' NON e' una prova di assenza di slippage.") + mde_q = 2.80 * (1 / np.sqrt(12)) / np.sqrt(n) + print(f" Statistica normalizzata q (posizione del fill nella barra 5m che lo contiene):") + print(f" sigma_q = 1/sqrt(12) = 0.289 -> MDE_q = {mde_q:.3f} di range di barra.") + print(f" E' la sola con potenza utile, e ha un'interpretazione fisica: un market order con") + print(f" impatto stampa AL MASSIMO della barra (q->1), perche' il nostro print E' nella barra.") + + # ---------------------------------------------------------------- 3. base inverse/lineare + print("\n[3] BASE inverse-vs-lineare — MISURATA, non assunta (difetto 2)\n" + "-" * 96) + lin, inv = {}, {} + use_net = "--no-net" not in sys.argv + for a in sorted(fills["asset"].unique()): + inv[a] = _certified(a, "5m", t0, t1) + lin[a] = _fetch_linear(a, t0, t1) if use_net else None + basis_ok = all(lin[a] is not None for a in lin) + if basis_ok: + for a in sorted(lin): + m = pd.merge(inv[a][["ts", "close"]], lin[a][["ts", "close"]], on="ts", + suffixes=("_inv", "_lin")) + b = (m["close_lin"] - m["close_inv"]) / m["close_inv"] * 1e4 + print(f" {a}: n={len(m):,} barre 5m · base mediana {b.median():+.2f} bps · " + f"media {b.mean():+.2f} · IQR [{b.quantile(.25):+.2f}, {b.quantile(.75):+.2f}] · " + f"|base| p95 {b.abs().quantile(.95):.2f}") + print(f" => Il commento nel sorgente ('inverse ~ lineare entro 3 bps') e' CONFERMATO come") + print(f" ordine di grandezza, ed e' esattamente la taglia del margine da difendere:") + print(f" misurare lo slippage sul feed certificato avrebbe misurato la BASE.") + else: + print(" NON MISURATA (nessuna rete / --no-net): i riferimenti sullo strumento tradato") + print(" non sono disponibili e i numeri sotto restano sul feed INVERSE = contaminati.") + + # ---------------------------------------------------------------- 4. i tre riferimenti + print("\n[4] I TRE RIFERIMENTI — segno orientato al lato (positivo = COSTO)\n" + "-" * 96) + inv1h = {a: _certified(a, "1h", t0 - pd.Timedelta(days=10), t1) + for a in sorted(fills["asset"].unique())} + daily = {} + for a, d in inv1h.items(): + g = d.set_index("ts").resample("1D") + daily[a] = pd.DataFrame(dict(close=g["close"].last())).dropna() + + rows = [] + for _, f in fills.iterrows(): + a, t, s, px = f["asset"], f["fill_ts"], f["lato"], f["price"] + r = dict(f) + # REF-A: close della barra 1d etichettata last_data = il fill che il BACKTEST assume + dd = daily[a] + key = f["model_bar"].tz_convert("UTC").normalize() + r["ref_A"] = float(dd.loc[key, "close"]) if key in dd.index else np.nan + src = lin[a] if basis_ok else inv[a] + b5 = _last_closed(src, t) + r["ref_B5"] = float(b5["close"]) if b5 is not None else np.nan + # barra 5m CONTENENTE il fill -> statistica q + cb = _containing(src, t) + if cb is not None and cb["high"] > cb["low"]: + p = (px - cb["low"]) / (cb["high"] - cb["low"]) + r["q"] = p if s > 0 else 1.0 - p + r["in_bar"] = bool(cb["low"] - 1e-9 <= px <= cb["high"] + 1e-9) + r["bar_rng_bps"] = (cb["high"] - cb["low"]) / cb["close"] * 1e4 + # quota della NOSTRA size sul volume della barra che ci contiene: se siamo + # una frazione trascurabile del nastro, l'impatto e' strutturalmente implausibile. + r["partecip"] = f["filled"] / cb["volume"] if cb["volume"] > 0 else np.nan + else: + r["q"], r["in_bar"], r["bar_rng_bps"], r["partecip"] = np.nan, None, np.nan, np.nan + # REF-B1h e REF-C VWAP dell'ora contenente + h0 = t.floor("h") + hb = src[(src["ts"] >= h0) & (src["ts"] < h0 + pd.Timedelta(hours=1))] + r["ref_C"] = float((hb["close"] * hb["volume"]).sum() / hb["volume"].sum()) \ + if len(hb) and hb["volume"].sum() > 0 else np.nan + prev = src[src["ts"] + pd.Timedelta(minutes=5) <= h0] + r["ref_B1"] = float(prev.iloc[-1]["close"]) if len(prev) else np.nan + for k in ("A", "B5", "B1", "C"): + v = r.get(f"ref_{k}") + r[f"slip_{k}"] = s * (px - v) / v * 1e4 if v and np.isfinite(v) else np.nan + rows.append(r) + R = pd.DataFrame(rows) + + lab = {"A": "REF-A barra 1d del modello (= fill assunto dal BACKTEST)", + "B5": "REF-B5 close ultima barra 5m CHIUSA (= SLIPPAGE d'esecuzione)", + "B1": "REF-B1 close ultima barra 1h CHIUSA", + "C": "REF-C VWAP dell'ora contenente (non causale)"} + strumento = "LINEARE (tradato)" if basis_ok else "INVERSE (certificato) — CONTAMINATO da base" + print(f" riferimenti B5/B1/C calcolati sullo strumento {strumento}\n") + for k in ("A", "B5", "B1", "C"): + print(f" {lab[k]}\n {fmt(ci_mean(R[f'slip_{k}']))}") + + print(f"\n ⚠️ REF-A NON e' slippage e il suo SEGNO non e' un guadagno. La barra 1d etichettata") + print(f" `last_data` chiude fino a ~23h DOPO il fill: su questa finestra (BTC 66k->78k in") + print(f" 4 giorni) un trend-follower che compra alle 01:07 batte per costruzione la") + print(f" chiusura serale, ma il backtest tiene la posizione DA quella chiusura — e' una") + print(f" differenza di FASE, non un attrito. La domanda 'quanto costa la barra parziale'") + print(f" e' gia' stata misurata bene il 26/07 (r0726_tp01_partial_day, 24 ancore appaiate:") + print(f" ±0.03 di Sharpe per TP01): n=18 su 5 settimane non la migliora e non la smentisce.") + print(f"\n Per asset e per lato (REF-B5, lo slippage vero):") + for a in sorted(R["asset"].unique()): + for sd in ("buy", "sell"): + x = R[(R["asset"] == a) & (R["side"] == sd)]["slip_B5"] + if len(x): + print(f" {a} {sd:4s} n={len(x):2d} media {x.mean():+7.2f} med {x.median():+7.2f}") + + # ---------------------------------------------------------------- 5. la statistica con potenza + print("\n[5] LA STATISTICA CON POTENZA — q, posizione del fill nella barra 5m\n" + "-" * 96) + q = R["q"].dropna() + dq = ci_mean(q) + print(f" q = (fill-low)/(high-low) per i BUY, 1-quello per i SELL. E[q]=0.5 senza impatto.") + print(f" media q {dq['mean']:.3f} IC95 [{dq['lo']:.3f}, {dq['hi']:.3f}] " + f"(med {dq['med']:.3f}, n={dq['n']})") + print(f" scostamento da 0.5: {dq['mean'] - 0.5:+.3f} (MDE dichiarata {mde_q:.3f})") + outside = (R["in_bar"] == False).sum() + print(f" fill FUORI dal range della propria barra: {outside}/{dq['n']} " + f"(un fill fuori sarebbe strumento o ora sbagliati)") + print(f" q=1.00 esatti (fill AL massimo = firma di impatto): {(q >= 0.999).sum()}/{dq['n']}" + f" · q=0.00 esatti: {(q <= 0.001).sum()}/{dq['n']}") + rng_med = float(R["bar_rng_bps"].median()) + print(f" range mediano di barra 5m: {rng_med:.1f} bps -> lo scostamento misurato vale") + print(f" ~{(dq['mean'] - 0.5) * rng_med:+.2f} bps/lato " + f"[IC95 {(dq['lo'] - 0.5) * rng_med:+.2f}, {(dq['hi'] - 0.5) * rng_med:+.2f}]") + + # -- null EMPIRICO: "0.5" e' un'assunzione (uniforme). Qui il null e' MISURATO sulle barre + # vere: dove sta il close di una barra 5m dentro il proprio range? Segno randomizzato + # per imitare l'orientamento al lato. + qc = [] + for a in sorted(fills["asset"].unique()): + src = lin[a] if basis_ok else inv[a] + m = src[src["high"] > src["low"]] + qc.append(((m["close"] - m["low"]) / (m["high"] - m["low"])).to_numpy()) + qc = np.concatenate(qc) + flip = RNG.random((N_BOOT, dq["n"])) < 0.5 + draw = RNG.choice(qc, size=(N_BOOT, dq["n"]), replace=True) + null = np.where(flip, draw, 1.0 - draw).mean(axis=1) + pval = float((null >= dq["mean"]).mean()) + print(f"\n NULL EMPIRICO (non 'uniforme per assunzione'): posizione del close nel proprio") + print(f" range, su {len(qc):,} barre 5m vere, segno randomizzato.") + print(f" null: media {null.mean():.3f} · sd {null.std():.3f} " + f"(l'uniforme darebbe sd {(1 / np.sqrt(12)) / np.sqrt(dq['n']):.3f})") + print(f" p(one-sided, impatto = q piu' alto del null) = {pval:.3f}" + f" -> {'IMPATTO RILEVATO' if pval < 0.05 else 'nessun impatto rilevabile'}") + + # -- argomento FISICO, indipendente dal campione: quanto siamo grandi e quanto vale un tick + from src.live.deribit import _CONTRACT, INSTRUMENT + print(f"\n ARGOMENTO STRUTTURALE (non dipende da n=18):") + pr = R["partecip"].dropna() + print(f" la nostra size sul volume della barra 5m che ci contiene: mediana " + f"{pr.median() * 100:.2f}% · max {pr.max() * 100:.2f}%") + for a in sorted(R["asset"].unique()): + tick = _CONTRACT[INSTRUMENT[a]]["tick"] + px = float(R[R["asset"] == a]["price"].median()) + print(f" {a}: tick {tick} su ~${px:,.0f} = {tick / px * 1e4:.4f} bps -> anche uno") + print(f" spread di 5 tick costerebbe {2.5 * tick / px * 1e4:.3f} bps di mezzo-spread") + print(f" => il pavimento fisico del costo d'esecuzione e' ~0.1 bps, DUE ordini di") + print(f" grandezza sotto il margine di {MARGIN_BPS_SIDE:.1f} bps. Perche' lo slippage") + print(f" sfondi il margine il libro dovrebbe CAMMINARE il book — e a <1% del nastro") + print(f" con 0/18 fill al massimo di barra non c'e' traccia che lo faccia.") + + # ---------------------------------------------------------------- 6. conversione + print("\n[6] CONVERSIONE IN SHARPE/CAGR — curva PUBBLICATA, non ricalcolata\n" + "-" * 96) + print(f" r0726_fee_sensitivity: {DSHARPE_PER_BPS_SIDE:+.3f} Sharpe/bps-lato · " + f"{DCAGR_PER_BPS_SIDE:+.2f} pp CAGR/bps-lato (SKH01 ~4x piu' sensibile di TP01)") + b5 = ci_mean(R["slip_B5"]) + qs_mean = (dq["mean"] - 0.5) * rng_med + qs_lo, qs_hi = (dq["lo"] - 0.5) * rng_med, (dq["hi"] - 0.5) * rng_med + for nm, m, lo, hi in (("REF-B5 (bps, senza potenza)", b5["mean"], b5["lo"], b5["hi"]), + ("q normalizzata (con potenza)", qs_mean, qs_lo, qs_hi)): + print(f" {nm:<30} slip {m:+6.2f} bps -> dSharpe {DSHARPE_PER_BPS_SIDE * m:+.4f} " + f"· dCAGR {DCAGR_PER_BPS_SIDE * m:+.3f} pp " + f"[IC95 dSharpe {DSHARPE_PER_BPS_SIDE * hi:+.4f}..{DSHARPE_PER_BPS_SIDE * lo:+.4f}]") + print(f"\n Margine gia' pagato dal modello: {MARGIN_BPS_SIDE:.1f} bps/lato " + f"(backtest 10 bps RT - fee vera 7 bps RT).") + print(f" Slippage con potenza (q) dentro il margine? " + f"{'SI' if qs_hi <= MARGIN_BPS_SIDE else 'NON DIMOSTRATO'} " + f"(estremo alto IC95 {qs_hi:+.2f} vs {MARGIN_BPS_SIDE:.1f})") + + # ---------------------------------------------------------------- 7. taglia e ora + print("\n[7] DIPENDENZA DA TAGLIA E ORA — il campione lo permette?\n" + "-" * 96) + from scipy import stats + sub = R.dropna(subset=["q"]) + rho, p = stats.spearmanr(sub["notional"], sub["q"]) + print(f" taglia ESEGUITA ${R['notional'].min():.0f}-${R['notional'].max():.0f} " + f"(mediana ${R['notional'].median():.0f}). NB: e' l'INCREMENTO mandato al venue, non il") + print(f" target di posizione (che arriva a $189): il libro riconcilia, non ri-apre.") + print(f" Spearman(notional, q) = {rho:+.3f} (p={p:.2f})") + big = sub[sub["notional"] >= sub["notional"].median()]["q"] + small = sub[sub["notional"] < sub["notional"].median()]["q"] + print(f" meta' grande n={len(big)} q={big.mean():.3f} · meta' piccola n={len(small)} " + f"q={small.mean():.3f} · differenza {big.mean() - small.mean():+.3f} " + f"(MDE su un confronto a meta' campione ~{2.80 * (1 / np.sqrt(12)) * np.sqrt(2 / (n / 2)):.3f})") + hrs = sorted(int(h) for h in R["fill_ts"].dt.hour.unique()) + print(f" ore del giorno coperte: {hrs} ({len(hrs)} ore distinte su 24, " + f"{R['fill_ts'].dt.hour.value_counts().max()} fill nell'ora piu' popolata)") + print(f" => con n={n} e {len(hrs)} ore distinte, l'effetto ora NON e' stimabile: " + f"lo dichiaro non misurato, non 'assente'.") + + # ---------------------------------------------------------------- 8. netting + print("\n[8] NETTING TP01/SKH01 — cosa NON e' misurabile da questi dati\n" + "-" * 96) + print(f" Il libro netta TP01 e SKH01 su UNO strumento: ogni fill e' la RISULTANTE, mai") + print(f" il trade di uno sleeve. Quindi lo slippage misurato e' quello del LIBRO, e non si") + print(f" puo' attribuire a TP01 o a SKH01 — proprio mentre SKH01 e' ~4x piu' sensibile.") + print(f" Il netting RIDUCE il turnover eseguito: {n} ordini in " + f"{(fills['fill_ts'].max() - fills['fill_ts'].min()).days} giorni, " + f"nozionale totale ${R['notional'].sum():,.0f}.") + + print("\n" + "=" * 96) + out = SCRATCH / "r0822_slip_fills.csv" + SCRATCH.mkdir(parents=True, exist_ok=True) + R.to_csv(out, index=False) + print(f" dettaglio per fill: {out}") + print("=" * 96) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_xsr_repro.py b/scripts/research/r0822_xsr_repro.py index c387ae0..134318f 100644 --- a/scripts/research/r0822_xsr_repro.py +++ b/scripts/research/r0822_xsr_repro.py @@ -58,7 +58,8 @@ from r0725_statarb_basket_gate import basket_from_positions, pair_frames from r0725_statarb_demean_skeptic import ret_from_pos # noqa: E402 ANN = np.sqrt(365.0) -DISCOVERY = pd.Timestamp("2026-07-25", tz="UTC") # ultima barra vista dalla scoperta +DISCOVERY = pd.Timestamp("2026-07-25", tz="UTC") # ultima barra vista dalla scoperta (PROVVISORIA) +CLOSED_AT_DISCOVERY = pd.Timestamp("2026-07-24", tz="UTC") # ultima barra CHIUSA quel giorno CRONLOG = ROOT / "logs" / "cron_daily.log" FWD = ROOT / "data" / "paper_xsr" / "returns.jsonl" FWD_STATE = ROOT / "data" / "paper_xsr" / "state.json" @@ -138,6 +139,8 @@ def t1_lenti(): for tag, (Pi, Si) in [ ("OGGI pieno", (P, S)), (f"tronc. {DISCOVERY.date()} (barra completa)", (_cut(P, DISCOVERY), _cut(S, DISCOVERY))), + ("tronc. 2026-07-24 = ultima barra CHIUSA il 25/07", + (_cut(P, CLOSED_AT_DISCOVERY), _cut(S, CLOSED_AT_DISCOVERY))), ]: rows.append((f"L1 gate {tag}", _stats(lens_L1(Pi, Si)))) rows.append((f"L2 scett. {tag}", _stats(lens_L2(Pi, Si)))) @@ -337,8 +340,7 @@ def t3_monitor(): print(" non entrano in nessun rendimento registrato.") sh_a = float(a.mean() / a.std() * ANN) sh_b = float(b.mean() / b.std() * ANN) - se = float(ANN / np.sqrt(len(a)) / np.sqrt(365) * np.sqrt(365)) - se = float(np.sqrt(365.0 / len(a))) + se = float(np.sqrt(365.0 / len(a))) # SE di uno Sharpe annualizzato su n barre giornaliere print(f"\n Sharpe della finestra forward: REGISTRATO {sh_a:+.2f} RICALCOLATO {sh_b:+.2f}") print(f" ⚠ NESSUNO DEI DUE E' UNA LETTURA DEL GATE. Con {len(a)} barre l'errore standard di") print(f" uno Sharpe annualizzato e' ~{se:.2f}: entrambi i numeri sono indistinguibili da 0 e") From 090129359bc8185c8673d7bc2c1d6be2fb01838d Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:33:55 +0000 Subject: [PATCH 014/114] =?UTF-8?q?research(wave-0822):=20HL-EXEC=20refuta?= =?UTF-8?q?=20il=20muro=20dei=2020k=20di=20XS01=20e=20lo=20slippage=20come?= =?UTF-8?q?=20rischio=20#1=20=E2=80=94=20e=20il=2020k=20di=20venue=20e'=20?= =?UTF-8?q?un'altra=20cosa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 45 ++ scripts/research/r0822_basis_calendar.py | 113 +++- scripts/research/r0822_hl_exec.py | 36 +- scripts/research/r0822_leverage_skeptic.py | 26 + scripts/research/r0822_prop_alloc.py | 35 ++ scripts/research/r0822_slip_audit.py | 118 ++++- scripts/research/r0822_volsize_skeptic.py | 578 +++++++++++++++++++++ 7 files changed, 920 insertions(+), 31 deletions(-) create mode 100644 scripts/research/r0822_volsize_skeptic.py diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index e13f37b..a70296c 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -17,6 +17,7 @@ null de-levering superato + eseguibilita' al capitale dichiarato. | 8 | VOL-SIZE | **LEAD** (gate 2026-12-22) + 1 falsificazione | dare a SKH01 una size per-trade regge a **23/23 ancore**, 8/8 anni, null di permutazione e trasferimento su V1 — ma vale **+0,07 di Sharpe di libro, un terzo della fortuna d'ancora del libro stesso (+0,196)**. Il vol-target di **libro** e' invece falsificato: compra peso SKH gia' respinto e peggiora l'eseguibilita' | | — | **XSR-REPRO** (integrita') | 🚨 **DIFETTO DI PRODUZIONE** | il numero 1.82 e' SPIEGATO e non era sbagliato (era su una **terza** lente, e su una barra non ancora chiusa) — ma cercandone la causa e' emerso che **`paper_xsr` registra ~41 minuti di mercato al giorno**, non un giorno. Tre gate pre-registrati leggono serie costruite cosi' | | 5 | SKEW | **SCARTATO** (Q1, Q2) + **LEAD** (Q3, gate 2027-02-22) | il prezzo muove lo skew (t 3,1-9,4 su 8/8 test), **non il contrario** (max |t| in avanti 2,35 contro 2,08 atteso dal rumore). Ma Q3 e' grosso: **il f=0,73 di VRP01 e' per il 42% STRUTTURA A TERMINE e solo per il 25% skew** | +| — | **HL-EXEC** (audit di fatto) | **3 falsificazioni misurate** | il pavimento vero e' **$10 (non $5)** e il taker **4,50 bps (non 5,0)** — ma il *"XS01 serve ~$20k"* e' **refutato del tutto** (nessuna soglia da min-order), il *"XSR01 ~$5.000"* e' **conservativo di 1,7x** (vero ~$3.000), e lo **slippage "rischio #1" di XSR01 e' refutato** con margine **21x** | ## Note che sopravvivono ai singoli filoni @@ -362,3 +363,47 @@ nuovo: **va decisa PRIMA del 23/10, non quel giorno.** pubblicato senza quel controllo"*. - ✅ Smile vettorizzato (512s -> 2,0s) verificato **bit-exact** contro il ciclo lento su 21.244 celle. - ✅ **Convergenza indipendente** con l'agente TERM-STRUCTURE: **75 giorni utili** (l'altro trovava 74). + +### HL-EXEC (r0822_hl_exec.py, 132 simulazioni, 0 celle di segnale = 0 selezione) +✅ Identita' con la produzione **prima** di ogni delta: `sim_libro` == `paper_xsr._step` a +**max|diff| 5,4e-20**; XS01 == `_xsec_returns()` a **max|diff| 0,0**. +✅ **Verifica MAINNET** (il progetto e' nato da un feed testnet): 51/51 asset, deviazione mediana +2,21% su feed vecchio 17h contro un movimento tipico del 2,81%; cross-check su terzo venue +BTC Deribit $77.997 vs HL $77.339 = −0,84%. +**Parametri assunti -> misurati:** min order **$5 -> $10** (docs + **3.827 livelli a un ordine: +minimo $10,63, p01 $12,02, 0,0% sotto $10**); taker **5,0 -> 4,50 bps** (due fonti indipendenti +coincidono); slippage **non modellato -> 1/2 spread 0,7 bps (19 major) / 1,0 bps (coda), max 4,5**; +tier VIP: a $600-20k **sempre tier base**. +📌 **XS01: la soglia dei $20k non esiste.** Origine del numero pubblicato: *"rumore di +arrotondamento"*, stima a occhio del diario 19/06. Misurato: haircut ~0 gia' a **$200-600 allocati**; +a $600 lo Sharpe passa **1,33 -> 1,30**. **E il meccanismo spiega perche':** gli ordini sono **due +popolazioni** — il ribilanciamento del segnale (ogni 10g) e' il **13% degli ordini ma il 75% del +nozionale**, ticket mediano $13,65 -> passa sempre; la deriva del vol-target giornaliera e' l'**87% +degli ordini ma il 25% del nozionale**, ticket $0,64 -> il pavimento la taglia, **ed e' gratis**. +*Contare gli ORDINI da' 16% e sembra un disastro; contare il NOZIONALE da' 75% e spiega perche' lo +Sharpe non si muove.* (Convergenza indipendente con XS-LITE, che era arrivato allo stesso meccanismo +con numeri diversi.) +✅ **Fortuna di fase girata anche qui** (l'haircut e' un Δ): 10 fasi, mediana delle differenze +appaiate **+0,007**, banda [−0,060, +0,061], **0/10 fasi con danno > 0,10**. +📌 **XSR01: soglia vera ~$3.000** (non $5.000 — sbagliata nella direzione **sicura**). Banda sui +pavimenti $8/$10/$12: ROTTO <=$1.000, INDECIDIBILE $1.500-2.000, **ok >=$3.000**. +📌 **Slippage refutato come rischio #1** al livello di liquidita' odierno: lo Sharpe scende sotto +1,0 a ~20 bps/gamba = **~16 bps di slippage = 21x quello misurato**. ⚠️ L'agente dichiara il limite: +3 snapshot = **172 secondi di oggi**, una fotografia, non tre osservazioni. **E' il MARGINE (21x) che +regge la conclusione, non il livello.** +🚨 **Due conseguenze che l'operatore deve vedere:** +1. **Il "$20k" di XS01 e il "$20k" della DECISIONE DI VENUE (26/07) sono due cose diverse che + CLAUDE.md ha conflato.** Quello di XS01 **cade**; **quello di venue REGGE intatto** — e' una + decisione dell'operatore su un altro asse (la rovina da fallimento exchange), non sull'eseguibilita'. + XS01 resta fuori dal live **per la decisione di venue, non per taglia**. +2. **La gamba di eseguibilita' del gate 2026-10-23 e' tarata su un vincolo che a $5.000 non morde** + (haircut li' −7%...+2%): *una condizione pre-registrata che non puo' fallire non e' una + condizione*. L'agente **non ha anticipato il gate**: la soglia Sharpe>=1,0 resta al 23/10. +⚠️ **Terza spiegazione dell'1,82, complementare alle altre due:** `r0725_statarb_basket_gate` +addebita *"2 gambe per coppia"* = 50 alt + **50 gambe BTC fantasma**, ma il demeaning **annulla +algebricamente** la gamba BTC (`sum(w)=0`) — che e' la ragione stessa per cui XSR01 esiste. +*L'intuizione del demeaning era stata applicata all'AMPIEZZA e non ai COSTI.* +📌 **Quadro conciliato delle tre lenti** (i due agenti concordano sul fatto, non sull'attribuzione +del singolo decimale): titolo `demean_skeptic` **1,82** · gate `basket_gate` **1,75-1,79** (fee +raddoppiate su una gamba inesistente) · monitor `paper_xsr` **2,21**. **La lente che ha girato i +gate e' quella PESSIMISTICA** -> il **DSR 0,983 PASS e' stato calcolato su una serie conservativa**. diff --git a/scripts/research/r0822_basis_calendar.py b/scripts/research/r0822_basis_calendar.py index 3997af5..b36fac0 100644 --- a/scripts/research/r0822_basis_calendar.py +++ b/scripts/research/r0822_basis_calendar.py @@ -474,6 +474,46 @@ def describe_curve(asset: str, cv: pd.DataFrame) -> None: f"(beta -1 = reversione totale, 0 = martingala)") +def basis_vs_funding(asset: str, cv: pd.DataFrame) -> None: + """Q1, nella forma decidibile: il basis del front e' il premio ANNUALIZZATO che + incassi se vendi il datato e lo tieni a scadenza. La copertura e' il perp, su cui + PAGHI il funding realizzato nello stesso periodo. Quindi il roll-down incassabile + NON e' il basis: e' `basis - funding realizzato fino a scadenza`. + Se il basis e' un previsore NON DISTORTO del funding (a~0, b~1) il premio a termine + esiste come LIVELLO ma non e' estraibile: e' gia' il prezzo della copertura.""" + d = cv[["c_front", "f1h", "tau_front_h"]].dropna() + if len(d) < 5000: + print(f" {asset}: campione insufficiente") + return + f = d["f1h"].values + cum = np.concatenate([[0.0], np.cumsum(f)]) # somma cumulata del funding orario + n = len(d) + tau = d["tau_front_h"].values + end = np.minimum(np.arange(n) + tau.astype(int), n - 1) + hrs = np.maximum(end - np.arange(n), 1) + realized = (cum[end] - cum[np.arange(n)]) / hrs * HOURS_Y # funding realizzato ANNUALIZZATO + implied = d["c_front"].values + m = np.isfinite(realized) & np.isfinite(implied) & (hrs > 24 * 5) + x, y = implied[m], realized[m] + b1, b0 = np.polyfit(x, y, 1) + prem = x - y # premio effettivamente incassabile + # IC bootstrap a BLOCCHI (le ore sono fortemente autocorrelate: un IC iid mentirebbe) + rng = np.random.default_rng(822) + bl = 24 * 30 + nb = max(1, len(prem) // bl) + boots = [] + for _ in range(400): + st = rng.integers(0, max(1, len(prem) - bl), nb) + boots.append(np.mean(np.concatenate([prem[i:i + bl] for i in st]))) + lo, hi = np.percentile(boots, [2.5, 97.5]) + print(f" {asset}: basis implicito medio {np.mean(x)*100:+6.2f}%/anno " + f"funding realizzato a scadenza {np.mean(y)*100:+6.2f}%/anno") + print(f" regressione realizzato ~ a + b*implicito: b = {b1:+.3f} a = {b0*100:+.2f}%" + f" (b~1 e a~0 = previsore non distorto => niente da incassare)") + print(f" PREMIO INCASSABILE = implicito - realizzato = {np.mean(prem)*100:+6.2f}%/anno" + f" IC95% a blocchi [{lo*100:+.2f}%, {hi*100:+.2f}%] (n={m.sum():,} ore)") + + # ========================================================================== # 3. STRATEGIE — posizioni in spazio CONTRATTO (il roll si paga da solo) # ========================================================================== @@ -671,6 +711,9 @@ def main() -> None: cv = curve_frame(a, panels[a], metas[a], funds[a], roll_dte=7, perp=perps[a]) curves[a] = cv describe_curve(a, cv) + print("\n --- Q1-bis: il basis e' un PREVISORE del funding che paghi sulla copertura? ---") + for a in ASSETS: + basis_vs_funding(a, curves[a]) # ---------------- STEP 3b: spread bid-ask (calibrazione DATATA) ---------------- print("\n[3b] SPREAD BID-ASK — calibrazione sul book VIVO di oggi (non e' storia)") @@ -724,6 +767,44 @@ def main() -> None: G = pd.DataFrame([{k: v for k, v in r.items() if k != "daily"} for r in rows]) dailies = {i: r["daily"] for i, r in enumerate(rows)} + print(f"\n distribuzione dello Sharpe FULL sulle {len(G)} celle: " + f"min {G.sharpe.min():+.2f} p25 {G.sharpe.quantile(.25):+.2f} " + f"mediana {G.sharpe.median():+.2f} p75 {G.sharpe.quantile(.75):+.2f} " + f"max {G.sharpe.max():+.2f} sd {G.sharpe.std():.2f}") + degen = G[(G.vol < 0.005) | (G.n < 400)] + if len(degen): + print(f" ATTENZIONE: {len(degen)} celle DEGENERI (vol<0.5%/anno o <400 giorni attivi):") + print(f" Sharpe di quelle celle: {sorted(np.round(degen.sharpe.values,2))[:10]}") + print(" gonfiano la varianza dei trial e quindi il null del deflated-Sharpe:") + print(" il DSR viene riportato ANCHE sulla sola sottogriglia non degenere.") + + print("\n MIGLIORE CELLA PER FAMIGLIA (Q2 riguarda il CALENDAR: va letto separato):") + for fam in FAM: + sub = G[G.family == fam] + i = sub.sharpe.idxmax() + r = G.loc[i] + print(f" [{fam}] {r.sig:12s} W={int(r.win):5d}h roll={int(r.roll):2d}g thr={r.thr:.1f} " + f"| Sh {r.sharpe:+6.2f} hold {r.holdout:+6.2f} DD {r.maxdd*100:5.2f}% " + f"CAGR {r.cagr*100:+6.2f}% vol {r.vol*100:5.2f}%") + ii = A.implausible_sharpe(dailies[i]) + print(f" implausible_sharpe={ii['implausible']} " + f"(barre in perdita {ii.get('loss_frac', float('nan'))*100:.1f}%) " + f"-> {'coda ASSENTE, rischio fuori dal campione' if ii['implausible'] else 'coda PRESENTE'}") + + print("\n LE CELLE CANONICHE (quelle che un lettore chiede per nome), roll=7g:") + for fam, sig, lab in [ + ("FvP", "carry", "vendi il datato quando e' in CONTANGO / compra perp (il carry classico)"), + ("FvP", "static_short", "SEMPRE corto il datato / lungo perp"), + ("FvP", "static_long", "SEMPRE lungo il datato / corto perp (il contrario)"), + ("CAL", "static_long", "calendar: lungo back / corto front"), + ("CAL", "static_short", "calendar: corto back / lungo front")]: + sub = G[(G.family == fam) & (G.sig == sig) & (G.roll == 7)] + if len(sub): + r = G.loc[sub.index[0]] + print(f" {fam}/{sig:12s} Sh {r.sharpe:+6.2f} hold {r.holdout:+6.2f} " + f"CAGR {r.cagr*100:+6.2f}% DD {r.maxdd*100:5.2f}% vol {r.vol*100:5.2f}%" + f" <- {lab}") + print("\n migliori 8 celle per Sharpe FULL (NON e' la selezione — solo panorama):") for i in G.sharpe.nlargest(8).index: r = G.loc[i] @@ -756,6 +837,11 @@ def main() -> None: dsr, sr0 = A.deflated_sharpe(bs["sharpe"], list(G.sharpe.values), bd) print(f" deflated-Sharpe : DSR {dsr:.3f} (null max atteso {sr0:+.2f} su {len(G)} trial) " f"-> {'PASS' if dsr >= 0.95 else 'FAIL'}") + nd = G[(G.vol >= 0.005) & (G.n >= 400)] + if len(nd) >= 5 and len(nd) < len(G): + d2, s2_ = A.deflated_sharpe(bs["sharpe"], list(nd.sharpe.values), bd) + print(f" sulla sola sottogriglia NON degenere ({len(nd)} trial): " + f"DSR {d2:.3f} (null max {s2_:+.2f}) -> {'PASS' if d2 >= 0.95 else 'FAIL'}") imp = A.implausible_sharpe(bd) print(f" implausible_sharpe: implausible={imp['implausible']} " @@ -769,9 +855,13 @@ def main() -> None: tp = A.tp01_baseline_daily() try: mg = A.marginal_vs_tp01(bd) - print(f" marginal_vs_tp01 : {mg.get('verdict')} corr {mg.get('corr', float('nan')):+.3f} " + print(f" marginal_vs_tp01 : {mg.get('marginal_verdict')} " + f"corr_full {mg.get('corr_full')} corr_hold {mg.get('corr_hold')} " f"robust_oos={mg.get('robust_oos')} insample_edge={mg.get('has_insample_edge')} " f"is_hedge={mg.get('is_hedge')} beats_noise={mg.get('beats_noise_null')}") + w25 = mg.get("w25") or (mg.get("blends", {}) or {}).get("w25", {}) + print(f" uplift blend w=25%: full {w25.get('uplift_full')} " + f"hold {w25.get('uplift_hold')}") except Exception as exc: # noqa: BLE001 mg = {} print(f" marginal_vs_tp01 : NON GIRATO ({type(exc).__name__}: {exc})") @@ -848,14 +938,21 @@ def main() -> None: # ---------------- STEP 8: eseguibilita' ---------------- print("\n[8] Q3 — ESEGUIBILITA' a $600-3.000") + print(" lotti minimi letti dal VENUE (non da una tabella cablata):") + px = {a: float(curves[a]["index"].dropna().iloc[-1]) for a in ASSETS} for a in ASSETS: - try: - sp = _api("/public/get_instrument", instrument_name=f"{a}-25DEC26")["result"] - print(f" {a}-25DEC26: min_trade_amount ${sp['min_trade_amount']:,.0f} di nozionale, " - f"contract_size ${sp['contract_size']:,.0f}, tick {sp['tick_size']}, " - f"taker {sp['taker_commission']*1e4:.1f} bps") - except Exception as exc: # noqa: BLE001 - print(f" {a}: specs non lette ({type(exc).__name__})") + for nm in (f"{a}-25DEC26", f"{a}_USDC-25DEC26"): + try: + sp = _api("/public/get_instrument", instrument_name=nm)["result"] + lin = sp.get("settlement_currency") == "USDC" + lot = (sp["min_trade_amount"] * px[a]) if lin else sp["min_trade_amount"] + print(f" {nm:22s} {'LINEARE' if lin else 'INVERSE':8s} " + f"min {sp['min_trade_amount']:<10.4g} = ${lot:>8,.2f} di nozionale " + f"tick {sp['tick_size']} taker {sp['taker_commission']*1e4:.1f} bps") + except Exception as exc: # noqa: BLE001 + print(f" {nm:22s} specs non lette ({type(exc).__name__})") + print(" (il libro live trada i LINEARI USDC; i trimestrali studiati qui sono gli INVERSE,") + print(" che hanno 7 anni di storia. Le due famiglie hanno lotti minimi DIVERSI.)") print(f" vol ANNUA del candidato per $1 di nozionale lordo/gamba: {bs['vol']*100:.2f}%") if bs["vol"] > 0: lev = 0.20 / bs["vol"] diff --git a/scripts/research/r0822_hl_exec.py b/scripts/research/r0822_hl_exec.py index 8eede86..f31601a 100644 --- a/scripts/research/r0822_hl_exec.py +++ b/scripts/research/r0822_hl_exec.py @@ -312,7 +312,7 @@ def metriche(net: np.ndarray, idx: pd.DatetimeIndex) -> dict: # ================================================================================================== # 4. XS01 — ricostruzione dei PESI (il sleeve espone solo i ritorni) + prova d'identita' # ================================================================================================== -def xs01_pesi(): +def xs01_pesi(phase: int = 0): from src.portfolio.sleeves import XS_CFG, XS_UNIVERSE, _xsec_returns cols = {} for sym in XS_UNIVERSE: @@ -334,7 +334,7 @@ def xs01_pesi(): w = np.zeros(A) hist: list[float] = [] for i in range(n): - if i >= mlb and i % H == 0: + if i >= mlb and i % H == phase: rLs = [px[i] / px[i - L] - 1.0 for L in lb] di = float(np.mean([r.std() for r in rLs])) thr = np.percentile(hist, dp) if (dp > 0 and len(hist) >= mh) else -np.inf @@ -361,9 +361,12 @@ def xs01_pesi(): rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25) scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0) # prova d'identita' col sleeve UFFICIALE (max|diff| deve essere 0.0) - off = _xsec_returns() - dif = float(np.abs(pd.Series(s.values * scale, index=C.index).reindex(off.index).values - - off.values).max()) + if phase == 0: + off = _xsec_returns() + dif = float(np.abs(pd.Series(s.values * scale, index=C.index).reindex(off.index).values + - off.values).max()) + else: + dif = float("nan") # posizione TENUTA durante la barra i = W[i-1]*scale[i] => posta a fine barra i: W[i]*scale[i+1] pos_scale = np.concatenate([scale[1:], scale[-1:]]) return W * pos_scale[:, None], dret, C, list(C.columns), dif @@ -541,6 +544,29 @@ def main() -> None: f"{hc:>12.0f}%{m10['pct_eseguite']:>8.0f}% ord " f"{m10['pct_nozionale']:>4.0f}% ntl") + # -------- fortuna di FASE: l'haircut e' un DELTA, e in questo progetto ogni delta misurato + # su una griglia ancorata eredita la fortuna dell'ancora (lezione 2026-07-26). XS01 ribilancia + # ogni H=10 giorni -> 10 fasi. Statistica giusta: MEDIANA DELLE DIFFERENZE APPAIATE. + print(f"\n [gate anchor] haircut per FASE del ciclo H={H} (pavimento $10, " + f"mediana delle differenze appaiate):") + print(f" {'fase':>6}{'Sh mod':>9}{'Sh $10':>9}{'haircut':>10} (cap XS01 $600)") + hb = [] + for ph in range(H): + Wp, dp_, Cp, colsp, _ = xs01_pesi(ph) + lp = np.array([float(meta.loc[x, "lot_usd"]) if x in meta.index else 0.0 for x in colsp]) + pp = np.array([float(meta.loc[x, "markPx"]) if x in meta.index else np.nan for x in colsp]) + s0 = metriche(sim_libro(Wp, dp_, None, 600.0, None, None, None, fee_xs_mod)["net"], Cp.index)["sharpe"] + s1 = metriche(sim_libro(Wp, dp_, None, 600.0, MIN_ORDER_HL, lp, pp, fee_xs_mod)["net"], Cp.index)["sharpe"] + hb.append(s0 - s1) + print(f" {ph:>6}{s0:>9.2f}{s1:>9.2f}{s0-s1:>+10.3f}") + hb = np.array(hb) + print(f" mediana appaiata {np.median(hb):+.3f} Sharpe banda [{hb.min():+.3f}, {hb.max():+.3f}]" + f" fasi con danno >0.10: {int((hb > 0.10).sum())}/{H}") + print(f" -> il pavimento HL non e' un costo per XS01 a NESSUNA fase, non solo alla canonica.") + print(f" (sottoprodotto: la colonna 'Sh mod' REPLICA il phase-luck del 2026-07-02 — la fase") + print(f" canonica 0 sta in alto e la MEDIANA delle fasi e' ben sotto: e' l'audit di XS01,") + print(f" ritrovato per caso da un'altra strada.)") + # ---------------------------------------------------------------- 4. XSR01 print(f"\n[4] XSR01 — soglia PUBBLICATA ~$5.000, gate 2026-10-23 con haircut a $5.000 <= 40%") from scripts.live.paper_xsr import FEE_LEG, MIN_ORDER, _book, _step, build_panel diff --git a/scripts/research/r0822_leverage_skeptic.py b/scripts/research/r0822_leverage_skeptic.py index 963730a..9337eb0 100644 --- a/scripts/research/r0822_leverage_skeptic.py +++ b/scripts/research/r0822_leverage_skeptic.py @@ -245,6 +245,32 @@ def main() -> dict: f"p99 {np.quantile(gap, 0.01):+.2%} peggiore {gap.min():+.2%}") print(f" giorni che CHIUDONO sul minimo (m == R): {(np.abs(gap) < 1e-12).mean():.1%}") print(" -> replica superata su 4 statistiche su 4: la macchineria e' quella del 25/07.") + print("\n E il MECCANISMO del 25/07 ('il tuffo e' ~3x piu' profondo nei giorni che") + print(" finiscono BENE'), che e' cio' che rende la lente accoppiata diversa da una") + print(" marginale — gap medio per decile della CHIUSURA del giorno:") + q = pd.qcut(pd.Series(D["R_live"]).rank(method="first"), 10, labels=False) + tab = pd.DataFrame({"R": D["R_live"], "gap": gap, "q": q.values}).groupby("q").agg( + R=("R", "mean"), gap=("gap", "mean"), chiude_sul_min=("gap", lambda x: float((x > -1e-12).mean()))) + print(f"\n {'decile':>8}{'chiusura media':>17}{'gap medio':>12}{'chiude sul minimo':>20}") + for i, row in tab.iterrows(): + eti = " <- peggiori" if i == 0 else (" <- migliori" if i == 9 else "") + print(f" {int(i)+1:>8}{row['R']:>17.2%}{row['gap']:>12.2%}{row['chiude_sul_min']:>20.0%}{eti}") + peggio5 = gap[D["R_live"] <= np.quantile(D["R_live"], 0.05)] + print("\n ⚠️ PRECISAZIONE a una lettura pubblicata: il 26,8% di giorni con m==R NON e'") + print(" «il giorno brutto che scende tutto il giorno e chiude sul minimo» — sta nei") + print(" decili CENTRALI (56-88%), che sono i giorni in cui il libro e' FLAT e quindi") + print(" non ha nessuna escursione da fare. Nel decile peggiore chiude sul minimo solo") + print(" il 4% delle volte. Il meccanismo del 25/07 (gap ~3x nei giorni buoni) e'") + print(" replicato; la spiegazione che lo accompagna, no.") + print(f"\n gap medio nel 5% di giorni PEGGIORI: {peggio5.mean():+.2%}" + f" (contro {gap[D['R_live'] >= np.quantile(D['R_live'], 0.95)].mean():+.2%} nel 5% migliori)") + print(" 📌 CONSEGUENZA CHE VALE PER LA SEZIONE (4) DELLO SCETTICATO (lo stress di coda):") + print(" un giorno di crash CHIUDE quasi sul proprio minimo, quindi la lente accoppiata") + print(" **non peggiora** lo scenario di stress fuori campione — il tuffo extra che") + print(" aggiungerebbe a un giorno -10% e' dell'ordine di mezzo punto. Cio' che regge") + print(" k* verso il basso resta quello che dice lo scetticato (drift stimato + coda") + print(" assente dal dataset): la lente NON e' un quarto argomento, ne' nell'un verso") + print(" ne' nell'altro.") print(f"\n (0b) ⚠️ IL GATE NON E' DIRETTAMENTE ESEGUIBILE — due lenti di SKH01, non una.") for nm, x in (("recon accoppiata (SKH canonico)", R_rec), ("book_series('hourly') (SKH live)", R_live)): diff --git a/scripts/research/r0822_prop_alloc.py b/scripts/research/r0822_prop_alloc.py index 603b451..80875ff 100644 --- a/scripts/research/r0822_prop_alloc.py +++ b/scripts/research/r0822_prop_alloc.py @@ -702,6 +702,41 @@ def main() -> None: print(" scoperta; se resta a drift 0, il vantaggio e' pura DECORRELAZIONE e non dipende da") print(" quell'ipotesi. Il numero, non la narrazione, dice quale delle due.") + hr("E-ter. IL BUCO CHE LA FINESTRA NON PUO' CHIUDERE — 2024-2026 non contiene un CRASH") + print(" L'ottimizzazione toglie peso a TP01 (Sharpe 0.79 su questa finestra). Ma TP01 e' lo") + print(" sleeve DIFENSIVO: il LOO del 26/07 misura il suo contributo hold-out NEGATIVO nel 99.1%") + print(" delle ancore e conclude 'uno sleeve difensivo si giudica sul SINISTRO, non sul premio'.") + print(" Su un conto funded la barriera morde PROPRIO nel sinistro. Qui la coppia crypto (che ha") + print(" storia dal 2019, quindi il 2022 dentro) e' ri-ottimizzata sulle due finestre:") + cry_full = pc.crypto_daily_tuples(1.0, 0.0).index + W2 = [{"TP01": i / 8, "SKH01": 1 - i / 8} for i in range(9)] + print(f"\n {'finestra':>26} {'giorni':>7} {'argmax TP/SKH':>15} {'leva':>6} {'J':>7} " + f"{'J del 75/25':>12} {'Sharpe argmax':>14}") + for st, lab in (("2019-03-14", "FULL 2019+ (col 2022)"), (START_WIN, "2024+ (senza crash)")): + pairs = {} + for w in W2: + c = pc.crypto_daily_tuples(w["TP01"], w["SKH01"]) + c = c[c.index >= pd.Timestamp(st, tz="UTC")] + pairs[wkey(w)] = (deluck(c["R"].values), np.minimum(c["m"].values, c["R"].values) - c["R"].values) + nh = len(next(iter(pairs.values()))[0]) + ev2 = boot_idx(nh, EV_H, N_PATHS, seed=8230) + fu2 = boot_idx(nh, FU_H, N_PATHS, seed=8231) + res = [] + for k, (R_, G_) in pairs.items(): + for lv in LEVS: + jj = (eval_sim(R_, G_, ev2, lv, "HYRO")["p_pass"] + * funded_sim(R_, G_, fu2, lv, "HYRO", 100_000.0)["p_alive"]) + res.append((k, lv, jj, R_.mean() * 365 / (R_.std() * np.sqrt(365)))) + bk, blv, bj, bsh = max(res, key=lambda x: x[2]) + j75 = max(x[2] for x in res if x[0] == "75/25/0") + print(f" {lab:>26} {nh:>7d} {bk.rsplit('/', 1)[0]:>15} {blv:>6.3f} {bj:>7.3f} " + f"{j75:>12.3f} {bsh:>14.2f}") + print("\n Se anche sulla finestra che CONTIENE il 2022 l'ottimo scarta TP01, la sua sotto-") + print(" pesatura non e' un artefatto di finestra. Se invece li' TP01 pesa, allora la") + print(" raccomandazione del 2024+ e' una scommessa sull'assenza di crash — e su una barriera") + print(" di rovina quella scommessa la paghi una volta sola. **Questa finestra NON puo' dirlo") + print(" per XS01**, che nasce nel 2024: il buco resta aperto per lui, per costruzione.") + # ---------------------------------------------------------------- F. VRP01 / GTAA01 tick("E. banda ancora") hr("F. E GLI SLEEVE 'INUTILI A $600'? — l'asimmetria del 25/07 §4 vale per XS01, non per tutti") diff --git a/scripts/research/r0822_slip_audit.py b/scripts/research/r0822_slip_audit.py index c51939c..02cf8c5 100644 --- a/scripts/research/r0822_slip_audit.py +++ b/scripts/research/r0822_slip_audit.py @@ -50,8 +50,13 @@ della barra 5m e il fill, che vale decine di bps. La statistica giusta e' NORMAL della barra: il nostro fill e' esso stesso un print della barra 5m che lo contiene, quindi q = (fill - low)/(high - low) per un BUY, q = 1 - quello per un SELL. Se i nostri ordini market avessero impatto, un BUY stamperebbe AL MASSIMO della barra -> q -> 1. -Senza impatto q e' un'estrazione ~uniforme -> E[q] = 0.5. sigma_q = 1/sqrt(12) = 0.289 e' ~20x -piu' piccolo del rumore in bps. +Senza impatto E[q] = 0.5, e il null NON si assume uniforme: si MISURA su ~18.000 barre vere. + +⚠️ ESITO SULLA MIA STESSA POTENZA: assumendo l'uniforme avevo dichiarato MDE_q = 0.19 di range +(~5 bps). Il null misurato e' 1.43x piu' largo (il close di una barra 5m sta spesso proprio sul +proprio estremo) -> MDE vera 0.27 = ~7.2 bps, contro 7.8 della via in bps. **La normalizzazione +non compra potenza utile.** Con n=18 nessuna via statistica risolve 1,5 bps: cio' che decide e' +l'argomento STRUTTURALE (tick, partecipazione al nastro, e l'esperimento naturale del fill #1). Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822_slip_audit.py (--no-net salta il fetch dello strumento lineare e lo dichiara non misurato) @@ -268,16 +273,24 @@ def main(): bad = (fills["fee"] / fills["filled"] * 1e4).median() print(f" [se avessi usato la convenzione INVERSE: {bad:,.0f} bps -> " f"{bad / FEE_TAKER_BPS_SIDE:,.0f}x il vero, e senza sollevare nulla]") - odd = fills[(fills["fee_bps"] - FEE_TAKER_BPS_SIDE).abs() > 0.05] - if len(odd): - print(f" ⚠️ {len(odd)}/{len(fills)} fill NON al tier {FEE_TAKER_BPS_SIDE:.2f}:") - for _, o in odd.iterrows(): - print(f" #{o['i']} {o['fill_ts']} {o['asset']} {o['side']} " - f"{o['filled']} @ {o['price']:.2f} = ${o['notional']:.2f} · " - f"fee {o['fee']:.8f} = {o['fee_bps']:.2f} bps") - print(f" -> nozionale ${odd['notional'].min():.2f}: sotto i ~$7 la fee sembra") - print(f" arrotondata AL RIALZO. E' un costo REALE che il modello non ha, e") - print(f" morde solo gli ordini minuscoli (dove il libro vive a $600).") + # Il nuovo schema fee Deribit e' entrato il 2026-08-01. La tabella `_CONTRACT`/fee_watch + # legge il tier dichiarato dal venue; qui lo si legge sul NASTRO, che e' la prova migliore. + CUT = pd.Timestamp("2026-08-01", tz="UTC") + pre, post = fills[fills["fill_ts"] < CUT], fills[fills["fill_ts"] >= CUT] + print(f" ripartito sul cambio di listino del 2026-08-01:") + for nm, g in (("PRIMA ", pre), ("DOPO ", post)): + if len(g): + u = sorted(float(x) for x in g["fee_bps"].round(2).unique()) + print(f" {nm} n={len(g):2d} fee/lato {u} " + f"(nozionale ${g['notional'].min():.0f}-${g['notional'].max():.0f})") + if len(pre) and len(post): + print(f" => il taglio {pre['fee_bps'].median():.2f} -> {post['fee_bps'].median():.2f} " + f"bps/lato e' VISIBILE sul nastro, {len(post)}/{len(post)} fill esatti al nuovo tier.") + print(f" Non e' un effetto taglia (i due fill a {pre['fee_bps'].median():.2f} bps sono da " + f"${pre['notional'].median():.0f}, sopra la mediana del campione).") + print(f" Conferma INDIPENDENTE di fee_watch (che legge il tier dichiarato, non i fill)") + print(f" e della regola pre-registrata del 26/07: taker <=5 bps/lato -> non si tocca") + print(f" nulla. Il listino si e' mosso NEL VERSO BUONO.") # ---------------------------------------------------------------- 2. potenza, PRIMA print("\n[2] POTENZA DICHIARATA — calcolata PRIMA di guardare lo slippage\n" + "-" * 96) @@ -396,6 +409,22 @@ def main(): if len(x): print(f" {a} {sd:4s} n={len(x):2d} media {x.mean():+7.2f} med {x.median():+7.2f}") + # La media di REF-B5 e' dominata da UN giorno: la si mostra con e senza, e si dice quale + # si cita. Non e' cherry-picking se si pubblicano entrambe e la regola e' dichiarata. + wild = R.nlargest(3, "bar_rng_bps") + day = wild["fill_ts"].dt.date.mode()[0] + keep = R[R["fill_ts"].dt.date != day] + print(f"\n ROBUSTEZZA: le 3 barre piu' larghe del campione ({wild['bar_rng_bps'].min():.0f}-" + f"{wild['bar_rng_bps'].max():.0f} bps di range) cadono tutte il {day}") + print(f" (ETH +14% in 6 ore). Sono {len(R) - len(keep)} fill su {len(R)} e da soli fanno") + print(f" la media: REF-B5 su tutto {ci_mean(R['slip_B5'])['mean']:+.2f} bps -> " + f"{ci_mean(keep['slip_B5'])['mean']:+.2f} bps senza quel giorno") + print(f" (IC95 [{ci_mean(keep['slip_B5'])['lo']:+.2f}, {ci_mean(keep['slip_B5'])['hi']:+.2f}], " + f"n={len(keep)}). La MEDIANA invece non si muove: " + f"{R['slip_B5'].median():+.2f} -> {keep['slip_B5'].median():+.2f}.") + print(f" => con n=18 la media in bps e' una statistica inservibile (un giorno la") + print(f" ribalta); si cita la mediana, e comunque decide l'argomento strutturale.") + # ---------------------------------------------------------------- 5. la statistica con potenza print("\n[5] LA STATISTICA CON POTENZA — q, posizione del fill nella barra 5m\n" + "-" * 96) q = R["q"].dropna() @@ -407,8 +436,9 @@ def main(): outside = (R["in_bar"] == False).sum() print(f" fill FUORI dal range della propria barra: {outside}/{dq['n']} " f"(un fill fuori sarebbe strumento o ora sbagliati)") - print(f" q=1.00 esatti (fill AL massimo = firma di impatto): {(q >= 0.999).sum()}/{dq['n']}" - f" · q=0.00 esatti: {(q <= 0.001).sum()}/{dq['n']}") + hi_obs, lo_obs = int((q >= 0.99).sum()), int((q <= 0.01).sum()) + print(f" fill sull'ESTREMO avverso (q>=0.99 = firma di impatto): {hi_obs}/{dq['n']}" + f" · sull'estremo favorevole (q<=0.01): {lo_obs}/{dq['n']}") rng_med = float(R["bar_rng_bps"].median()) print(f" range mediano di barra 5m: {rng_med:.1f} bps -> lo scostamento misurato vale") print(f" ~{(dq['mean'] - 0.5) * rng_med:+.2f} bps/lato " @@ -429,17 +459,48 @@ def main(): pval = float((null >= dq["mean"]).mean()) print(f"\n NULL EMPIRICO (non 'uniforme per assunzione'): posizione del close nel proprio") print(f" range, su {len(qc):,} barre 5m vere, segno randomizzato.") + sd_unif = (1 / np.sqrt(12)) / np.sqrt(dq["n"]) print(f" null: media {null.mean():.3f} · sd {null.std():.3f} " - f"(l'uniforme darebbe sd {(1 / np.sqrt(12)) / np.sqrt(dq['n']):.3f})") + f"(l'uniforme darebbe sd {sd_unif:.3f})") print(f" p(one-sided, impatto = q piu' alto del null) = {pval:.3f}" f" -> {'IMPATTO RILEVATO' if pval < 0.05 else 'nessun impatto rilevabile'}") + # ...e quanti estremi ci si ASPETTA sotto quel null (un conteggio nudo non e' un'evidenza) + p_hi = float(((qc >= 0.99).mean() + (qc <= 0.01).mean()) / 2) + print(f" attesi sotto il null: {p_hi * dq['n']:.1f} su {dq['n']} per lato " + f"(osservati {hi_obs} avversi / {lo_obs} favorevoli)") + print(f" -> l'unico q>=0.99 e' il fill #3 (ETH sell, q=0.996) su una barra da 7.5 bps di") + print(f" range: e' 0.03 bps di prezzo. Un estremo su una barra piatta non e' impatto.") + mde_q_true = 2.80 * float(null.std()) + print(f"\n ⚠️ CORREZIONE ALLA MIA STESSA POTENZA DICHIARATA. Il null vero e' " + f"{null.std() / sd_unif:.2f}x piu' largo") + print(f" dell'uniforme (il close di una barra 5m sta spesso PROPRIO sul suo estremo:") + print(f" sd per-osservazione {null.std() * np.sqrt(dq['n']):.3f} contro 0.289 dell'uniforme).") + print(f" MDE_q vera = {mde_q_true:.3f} di range = {mde_q_true * rng_med:.1f} bps, non " + f"{mde_q * rng_med:.1f}: la mia stima") + print(f" a priori era ottimista del {(mde_q_true / mde_q - 1) * 100:.0f}%. => la statistica") + print(f" normalizzata NON compra potenza utile ({mde_q_true * rng_med:.1f} bps contro " + f"{mde_bps:.1f} della via in bps):") + print(f" con n=18 NESSUNA via statistica risolve {MARGIN_BPS_SIDE:.1f} bps. Cio' che decide") + print(f" e' l'argomento strutturale qui sotto, non un intervallo di confidenza.") # -- argomento FISICO, indipendente dal campione: quanto siamo grandi e quanto vale un tick from src.live.deribit import _CONTRACT, INSTRUMENT print(f"\n ARGOMENTO STRUTTURALE (non dipende da n=18):") pr = R["partecip"].dropna() print(f" la nostra size sul volume della barra 5m che ci contiene: mediana " - f"{pr.median() * 100:.2f}% · max {pr.max() * 100:.2f}%") + f"{pr.median() * 100:.2f}% · p90 {pr.quantile(.9) * 100:.2f}% · max {pr.max() * 100:.2f}%") + top = R.dropna(subset=["partecip"]).nlargest(1, "partecip").iloc[0] + print(f"\n ESPERIMENTO NATURALE — il fill #{top['i']} ({top['fill_ts']:%Y-%m-%d %H:%M} " + f"{top['fill_ts']:%a}, {top['asset']} ${top['notional']:.0f}):") + print(f" e' stato il {top['partecip'] * 100:.1f}% del volume della propria barra 5m") + print(f" (weekend, nastro sottilissimo) e quella barra ha avuto un range di SOLI") + print(f" {top['bar_rng_bps']:.2f} bps, con il nostro print a meta' (q={top['q']:.2f}).") + print(f" Un ordine che prende un quinto del nastro e lascia il prezzo entro 1 bps e'") + print(f" la prova diretta che a questa taglia il libro NON cammina il book. Vale piu'") + print(f" dell'intero intervallo di confidenza: e' il caso peggiore del campione.") + print(f" ⚠️ ed e' un SABATO: la partecipazione peggiore cade nel weekend, dove il") + print(f" progetto ha misurato che TP01 fa il 38% del proprio gross. Se l'attrito") + print(f" mordera' a taglia maggiore, mordera' proprio dove sta il rendimento.") for a in sorted(R["asset"].unique()): tick = _CONTRACT[INSTRUMENT[a]]["tick"] px = float(R[R["asset"] == a]["price"].median()) @@ -447,8 +508,29 @@ def main(): print(f" spread di 5 tick costerebbe {2.5 * tick / px * 1e4:.3f} bps di mezzo-spread") print(f" => il pavimento fisico del costo d'esecuzione e' ~0.1 bps, DUE ordini di") print(f" grandezza sotto il margine di {MARGIN_BPS_SIDE:.1f} bps. Perche' lo slippage") - print(f" sfondi il margine il libro dovrebbe CAMMINARE il book — e a <1% del nastro") - print(f" con 0/18 fill al massimo di barra non c'e' traccia che lo faccia.") + print(f" sfondi il margine il libro dovrebbe CAMMINARE il book — e a <1% del nastro,") + print(f" con MENO estremi avversi di quanti il null ne preveda ({hi_obs} contro " + f"{p_hi * dq['n']:.1f}), non c'e'") + print(f" traccia che lo faccia.") + + # -- COSA CAMBIA CON IL CAPITALE: la partecipazione scala LINEARE, il conto no. + print(f"\n SCALA — l'unica parte di questa misura che ha una data di scadenza:") + eq = 636.0 # equity reale osservata (data/live/equity_seen.json) + print(f" la partecipazione scala lineare col capitale (equity oggi ~${eq:.0f}).") + print(f" {'capitale':>10} {'partecip. mediana':>19} {'p90':>9} {'max osservato':>15}") + for cap in (600, 5_000, 20_000, 100_000, 272_000): + k = cap / eq + mx = pr.max() * k * 100 + print(f" {'$' + format(cap, ','):>10} {pr.median() * k * 100:>18.2f}% " + f"{pr.quantile(.9) * k * 100:>8.2f}% " + + (f"{mx:>14.1f}%" if mx <= 100 else f"{'>tutta la barra':>15}")) + print(f" => a $600 l'impatto e' strutturalmente impossibile e questa misura lo conferma.") + print(f" Ma il caso peggiore osservato e' gia' {pr.max() * 100:.0f}% di una barra 5m: a $5k") + print(f" diventa {pr.max() * 5000 / eq * 100:.0f}%, a $20k {pr.max() * 20000 / eq * 100:.0f}%. " + f"**La misura di oggi NON si estrapola**") + print(f" al capitale del piano: va RIFATTA a ogni salto di taglia. E' la stessa") + print(f" forma del muro di eseguibilita' gia' noto (XS01 ~$20k, XSR01 ~$5k), ma") + print(f" sull'asse dell'ATTRITO invece che del min-order.") # ---------------------------------------------------------------- 6. conversione print("\n[6] CONVERSIONE IN SHARPE/CAGR — curva PUBBLICATA, non ricalcolata\n" + "-" * 96) diff --git a/scripts/research/r0822_volsize_skeptic.py b/scripts/research/r0822_volsize_skeptic.py new file mode 100644 index 0000000..49efa1b --- /dev/null +++ b/scripts/research/r0822_volsize_skeptic.py @@ -0,0 +1,578 @@ +#!/usr/bin/env python +"""r0822_volsize_skeptic.py — SCETTICO del filone VOL-SIZE (r0822_vol_size.py). + +BERSAGLIO. Un altro agente ha trovato che dare a SKH01 una SIZE PER-TRADE (cella congelata +`VTL tv20 w90`: size fissata all'ingresso = clip(0.20 / rv90(t-1), 0, 3)) vale **+0,070 di +Sharpe di libro a ISO-PESO**, positivo in 23/23 ancore e 8/8 anni. Ha dichiarato lui stesso che +il meccanismo non e' quello che sembra: il guadagno viene dalla FREQUENZA dei trade (+0,051 sul +solo filtro di attivita') e non dalla MAGNITUDINE (+0,003) -> non e' controllo del rischio, e' +un filtro di frequenza. E ha PRE-REGISTRATO senza eseguirlo il test che lo falsificherebbe. + +QUATTRO ATTACCHI, dichiarati prima di misurare, con l'esito che mi aspetto: + + A1 IL TEST PRE-REGISTRATO — famiglia IV (inverse-vol per-trade sulla vol dell'ASSET) applicata + a SKH01_V1. La ragione dichiarata della famiglia IV e' la GEOMETRIA DELLO STOP: su V2-DD lo + stop e' una percentuale FISSA (4% long / 2% short), quindi il rischio in unita' di sigma + varia col regime; su V1 lo stop e' in ATR, cioe' gia' scalato sulla vol -> l'effetto DEVE + essere MINORE. Previsione direzionale: |eff. su V1| < |eff. su V2-DD|, cella per cella. + Se e' UGUALE, la spiegazione e' sbagliata anche se il numero regge. + ATTESA MIA: che il test sia SENZA POTENZA, perche' sospetto che IV non faccia niente + nemmeno su V2-DD (l'agente ha promosso VTL, non IV). Un test pre-registrato su una + famiglia inerte non falsifica nulla. + + A2 LA CONTABILITA' — l'agente dichiara di aver trovato e riparato un look-ahead di + contabilita': "tutto il P&L di un trade e' contabilizzato il giorno di CHIUSURA, fino a + 3,83 giorni dopo l'ingresso", costo "+0,04 di Sharpe hold-out fantasma", e ne ha tratto + una REGOLA pubblicata. Il compito mi chiede di verificare la riparazione troncando i dati + alla barra d'ingresso. Lo faccio, ma prima verifico la PREMESSA: quale giorno porta davvero + il P&L. ATTESA MIA: la riparazione e' corretta e ridondante. + + A3 L'ATTACCO ALLA SPIEGAZIONE — se il guadagno e' un filtro di frequenza, deve essere + riproducibile SENZA ALCUN vol-target. Controllo piu' brutale possibile: size BINARIA 0/1 + sulla stessa soglia della regola congelata (L>=1 <=> rv90<=20%), piu' un filtro che scarta + i trade nei periodi di maggiore ATTIVITA' della gamba. Se un binario replica il +0,070, + la formula di sizing e' ORNAMENTO. Controllo di segno obbligatorio: il binario INVERSO + (tieni solo i trade in alta vol) deve DANNEGGIARE, o non sto misurando un filtro. + + A4 L'ASSE DELLA FINESTRA — l'agente dichiara che w e' un PICCO, non un plateau (w30 negativo, + w180 ~0, w90 ottimo). Un picco su un asse continuo con 23 ancore e' la firma classica di + un max-of-k. Misuro l'asse fitto (9 finestre) e rifaccio la scelta di w IN-SAMPLE-ONLY. + +DISCIPLINA. Non ricalcolo niente che esista: importo `r0822_vol_size` (extract / equity_daily / +leg_daily / size_iv / leverage_series / sizes_from_L / book) e replico BIT-EXACT contro +`sleeves._skyhook_returns()` prima di ogni delta. Il confronto e' sempre ISO-PESO (baseline +ri-scalato allo stesso peso effettivo di SKH), perche' "alzare SKH01" e' gia' stato misurato e +respinto il 26/07. Statistica = MEDIANA DELLE DIFFERENZE APPAIATE per ancora (`anchor_luck_delta`), +mai differenza delle mediane. + +USO: nice -n 19 timeout 900 uv run python scripts/research/r0822_volsize_skeptic.py [--every K] + --every 8 -> 3 ancore (pilota). default: tutte e 23. +""" +from __future__ import annotations + +import argparse +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 altlib as A # noqa: E402 +import r0702_anchor_skh01 as R # noqa: E402 +import r0822_vol_size as V # noqa: E402 (bersaglio: riusato, non riscritto) +from src.strategies.skyhook import SKH01_V1 # noqa: E402 + +ASSETS = V.ASSETS +OFFSETS_FULL = V.OFFSETS_FULL +FROZEN = dict(tv=0.20, w=90, cap=3.0, active_only=False) # la cella congelata del LEAD +sh, vol, maxdd, cagr = V.sh, V.vol, V.maxdd, V.cagr +hold, ins, book = V.hold, V.ins, V.book + + +# --------------------------------------------------------------------------- famiglie di size +def _keep_to_sizes(ex: dict, keep: pd.Series) -> np.ndarray: + return V.sizes_from_L(ex, keep.astype(float)) + + +def size_vtl(sp: dict): + def f(ex): + return V.sizes_from_L(ex, V.leverage_series(ex["base_daily"], **sp)) + return f + + +def size_bin(sp: dict, mode: str): + """Size BINARIA 0/1 dalla STESSA L della regola congelata. Nessuna magnitudine: o il trade + si fa a size 1, o non si fa. `ge1` usa la soglia della regola stessa (L>=1 <=> rv90<=tv); + `gemed` la mediana espandente causale di L; `lt1` e' l'INVERSO (controllo di segno).""" + def f(ex): + L = V.leverage_series(ex["base_daily"], **sp) + if mode == "ge1": + keep = pd.Series(L.values >= 1.0, index=L.index) + elif mode == "lt1": + keep = pd.Series(L.values < 1.0, index=L.index) + elif mode == "gemed": + med = L.expanding(min_periods=V.VT_WARM).median() + keep = pd.Series(np.where(med.notna().values, L.values >= med.values, True), + index=L.index) + else: + raise ValueError(mode) + return _keep_to_sizes(ex, keep) + return f + + +def size_bin_grad(sp: dict): + """BINARIO + GRADUAZIONE: fra i trade tenuti, la size continua di L. Isola quanto aggiunge + la FORMULA una volta che la SELEZIONE e' gia' stata fatta.""" + def f(ex): + L = V.leverage_series(ex["base_daily"], **sp) + g = pd.Series(np.where(L.values >= 1.0, L.values, 0.0), index=L.index) + return V.sizes_from_L(ex, g) + return f + + +def size_chan(mode: str, binary: bool, w: int = 90, cap: float = 3.0): + """I DUE CANALI dentro `rv` di una serie all'88% di zeri, isolati come nel bersaglio: + mode='mag' -> vol calcolata sulle sole barre ATTIVE (MAGNITUDINE degli esiti) + mode='freq' -> conteggio di barre attive nella finestra (FREQUENZA dei trade) + binary=False riproduce le celle continue del bersaglio (`VTL neutro attive` / `ACT`); + binary=True le riduce a 0/1 alla mediana ESPANDENTE causale della propria L.""" + def f(ex): + if mode == "mag": + Ls = V.leverage_series(ex["base_daily"], tv=None, w=w, cap=cap, active_only=True) + else: + Ls = V.activity_L(ex["base_daily"], w=w, cap=cap) + if not binary: + return V.sizes_from_L(ex, Ls) + med = Ls.expanding(min_periods=V.VT_WARM).median() + keep = pd.Series(np.where(med.notna().values, Ls.values >= med.values, True), + index=Ls.index) + return _keep_to_sizes(ex, keep) + return f + + +def size_dropact(w: int = 90, q: float = 0.75): + """Scarta i trade il cui giorno d'ingresso cade nei periodi in cui la gamba e' stata piu' + ATTIVA (conteggio di barre attive negli ultimi w giorni sopra il quantile ESPANDENTE causale + q). Nessun vol-target, nessuna magnitudine, nessun prezzo: solo la frequenza dei trade.""" + def f(ex): + base = ex["base_daily"] + cnt = (base != 0.0).astype(float).rolling(w, min_periods=max(5, w // 3)).sum().shift(1) + thr = cnt.expanding(min_periods=V.VT_WARM).quantile(q) + keep = pd.Series(np.where(thr.notna().values & cnt.notna().values, + cnt.values <= thr.values, True), index=base.index) + return _keep_to_sizes(ex, keep) + return f + + +def iv_family() -> dict: + d = {} + for p in (0.5, 1.0): + for w in (30, 90): + for cp in (2.0, 4.0): + d[f"IV p{p} w{w} cap{cp:.0f}"] = V.size_iv(p, w, cp) + return d + + +# --------------------------------------------------------------------------- motore +def legs(EX: dict, funcs: dict) -> dict: + """name -> serie giornaliera della gamba SKH 50/50, per un'ancora.""" + out = {"BASE": V.leg_daily(EX, {a: V.size_flat(EX[a]) for a in ASSETS})} + for nm, fn in funcs.items(): + out[nm] = V.leg_daily(EX, {a: fn(EX[a]) for a in ASSETS}) + return out + + +def iso_pair(TP: pd.Series, leg: pd.Series, leg_base: pd.Series): + """Libro della variante e libro di CONTROLLO ISO-PESO (baseline ri-scalato alla stessa vol + di gamba -> stesso peso effettivo di SKH). E' la convenzione del bersaglio, riusata.""" + vb = vol(leg_base) + m_s = vol(leg) / vb if vb > 0 else 1.0 + return book(TP, leg), book(TP, leg_base * m_s), m_s + + +def delta_table(BK: dict, CT: dict, names, offs, label: str, extra_hold=True) -> dict: + res = {} + print(f" {'cella':<26}{'ISO dShFULL':>12}{'pos/n':>8}{'banda':>20}" + + (f"{'ISO dShHOLD':>13}{'pos/n':>8}" if extra_hold else "") + f"{'wSKHeff':>9}") + for nm in names: + dF = A.anchor_luck_delta(lambda o, n=nm: BK[n][o], lambda o, n=nm: CT[n][o], offs, + metric=sh) + row = dict(dF=dF["median_paired"], nF=dF["n_positive"], lo=dF["lo"], hi=dF["hi"]) + s = (f" {nm:<26}{row['dF']:>+12.3f}{row['nF']:>5}/{len(offs):<3}" + f" [{row['lo']:+.3f},{row['hi']:+.3f}]".ljust(20 + 26 + 12 + 8)) + s = (f" {nm:<26}{row['dF']:>+12.3f}{row['nF']:>5}/{len(offs):<3}" + f"{'[%+.3f,%+.3f]' % (row['lo'], row['hi']):>20}") + if extra_hold: + dH = A.anchor_luck_delta(lambda o, n=nm: hold(BK[n][o]), + lambda o, n=nm: hold(CT[n][o]), offs, metric=sh) + row.update(dH=dH["median_paired"], nH=dH["n_positive"]) + s += f"{row['dH']:>+13.3f}{row['nH']:>5}/{len(offs):<3}" + we = float(np.median([0.25 * m for m in MSCALE[nm]])) if nm in MSCALE else float("nan") + row["weff"] = we + s += f"{we:>9.3f}" + print(s) + res[nm] = row + return res + + +MSCALE: dict = {} + + +def per_year(BK: dict, CT: dict, nm: str, offs) -> tuple: + yrs = sorted({int(y) for y in BK[nm][offs[0]].index.year}) + cells = [] + for y in yrs: + v = [sh(BK[nm][o][BK[nm][o].index.year == y]) - sh(CT[nm][o][CT[nm][o].index.year == y]) + for o in offs if (BK[nm][o].index.year == y).sum() > 20] + cells.append(float(np.median(v)) if v else float("nan")) + return yrs, cells + + +# =========================================================================== main +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--every", type=int, default=1) + args = ap.parse_args() + offs = OFFSETS_FULL[:: max(1, args.every)] + t0 = time.time() + L = 112 + + print("=" * L) + print(" r0822 VOL-SIZE — SCETTICO. Bersaglio: `VTL tv20 w90` = +0,070 ISO dShFULL (23/23)") + print("=" * L) + print(f" ancore: {len(offs)}/{len(OFFSETS_FULL)} della griglia a priori 02/07 {list(offs)}") + print(" lente: path CANONICO (identica nelle due braccia di ogni confronto). TP01 canonico.") + + TP = A.tp01_baseline_daily() + + # ------------------------------------------------------------------ celle + IV = iv_family() + FUN_V2 = dict(IV) + FUN_V2["VTL tv20 w90 (LEAD)"] = size_vtl(FROZEN) + FUN_V2["BIN L>=1 (0/1)"] = size_bin(FROZEN, "ge1") + FUN_V2["BIN L>=med (0/1)"] = size_bin(FROZEN, "gemed") + FUN_V2["BIN L<1 (INVERSO)"] = size_bin(FROZEN, "lt1") + FUN_V2["BIN+GRAD"] = size_bin_grad(FROZEN) + FUN_V2["DROP attivita' >p75"] = size_dropact(90, 0.75) + FUN_V2["DROP attivita' >p50"] = size_dropact(90, 0.50) + FUN_V2["canale MAGNITUDINE cont."] = size_chan("mag", False) + FUN_V2["canale FREQUENZA cont."] = size_chan("freq", False) + FUN_V2["canale MAGNIT. 0/1"] = size_chan("mag", True) + FUN_V2["canale FREQ. 0/1"] = size_chan("freq", True) + WGRID = (15, 20, 30, 45, 60, 90, 120, 180, 270) + for w in WGRID: + if w != 90: + FUN_V2[f"VTL tv20 w{w}"] = size_vtl(dict(FROZEN, w=w)) + FUN_V1 = dict(IV) + FUN_V1["VTL tv20 w90 (LEAD)"] = size_vtl(FROZEN) + FUN_V1["BIN L>=1 (0/1)"] = size_bin(FROZEN, "ge1") + + NTRIAL = len(FUN_V2) + len(FUN_V1) + 2 # +2 baseline + print(f"\n GRIGLIA DICHIARATA: {len(FUN_V2)} celle su V2-DD + {len(FUN_V1)} su V1 " + f"+ 2 baseline = {NTRIAL} valutazioni x {len(offs)} ancore. Nessuna cella scelta") + print(" dopo aver guardato: IV e' la famiglia PRE-REGISTRATA, VTL/w e' l'asse dichiarato") + print(" fragile dall'agente, BIN/DROP sono controlli miei dichiarati sopra.") + + # ------------------------------------------------------------------ ciclo ancore + BK2, CT2, BK1, CT1 = {}, {}, {}, {} + EX0 = None + print("\n calcolo per ancora ...", flush=True) + for o in offs: + EXV2 = {a: V.extract(a, o) for a in ASSETS} + EXV1 = {a: V.extract(a, o, SKH01_V1) for a in ASSETS} + if o == offs[0]: + EX0 = (EXV2, EXV1) + for tag, EXX, FUN, BKd, CTd in (("V2", EXV2, FUN_V2, BK2, CT2), + ("V1", EXV1, FUN_V1, BK1, CT1)): + lg = legs(EXX, FUN) + base = lg["BASE"] + for nm, s in lg.items(): + b, c, m_s = iso_pair(TP, s, base) + BKd.setdefault(nm, {})[o] = b + CTd.setdefault(nm, {})[o] = c + if tag == "V2": + MSCALE.setdefault(nm, []).append(m_s) + if o != offs[0]: + del EXV2, EXV1 + print(f" fatto in {time.time()-t0:.0f}s") + EXV2, EXV1 = EX0 + + # ------------------------------------------------------------------ §0 SANITY + print("\n" + "-" * L) + print(" 0. SANITY — replica BIT-EXACT prima di qualunque delta (se non e' 0, il problema") + print(" e' mio e va detto)") + print("-" * L) + from src.portfolio.sleeves import _skyhook_returns + mine = V.leg_daily(EXV2, {a: V.size_flat(EXV2[a]) for a in ASSETS}) + off = _skyhook_returns() + d0 = float(np.max(np.abs(mine.values - off.values))) if len(mine) == len(off) else float("nan") + print(f" gamba 50/50 off{offs[0]} vs sleeves._skyhook_returns(): max|dif| = {d0:.2e} " + f"({len(mine)} giorni)") + assert d0 < 1e-15, "replica NON bit-exact" + for a in ASSETS: + ref = R.run_asset(a, offs[0])[0] + R._CACHE.clear() + dd = float(np.max(np.abs(V.equity_daily(EXV2[a], V.size_flat(EXV2[a])).values - ref.values))) + print(f" {a} vs r0702_anchor_skh01.run_asset: max|dif| = {dd:.2e}") + assert dd < 1e-15 + + lead = "VTL tv20 w90 (LEAD)" + dLEAD = A.anchor_luck_delta(lambda o: BK2[lead][o], lambda o: CT2[lead][o], offs, metric=sh) + dLEADH = A.anchor_luck_delta(lambda o: hold(BK2[lead][o]), lambda o: hold(CT2[lead][o]), + offs, metric=sh) + print(f" RIPRODUZIONE DEL TITOLO: ISO dShFULL {dLEAD['median_paired']:+.3f} " + f"({dLEAD['n_positive']}/{len(offs)}) ISO dShHOLD {dLEADH['median_paired']:+.3f} " + f"({dLEADH['n_positive']}/{len(offs)}) [pubblicato +0,070 (23/23) / +0,101 (19/23)]") + med_F = float(np.median([sh(BK2["BASE"][o]) for o in offs])) + med_H = float(np.median([sh(hold(BK2["BASE"][o])) for o in offs])) + med_D = float(np.median([maxdd(BK2["BASE"][o]) for o in offs])) + print(f" libro BASE (V2-DD), mediana della banda: ShFULL {med_F:.3f} / ShHOLD {med_H:.3f} " + f"/ maxDD {med_D*100:.1f}% [pubblicato 1,626 / 1,037 / 10,4%]") + + # ------------------------------------------------------------------ §1 CONTABILITA' + print("\n" + "-" * L) + print(" 1. LA CONTABILITA' — verifica della PREMESSA prima della riparazione") + print(" (l'agente: 'tutto il P&L di un trade e' contabilizzato il giorno di CHIUSURA')") + print("-" * L) + tot_ent = tot_ex = tot_chk = 0 + for a in ASSETS: + ex = EXV2[a] + d_e = ex["idx"][ex["i_ent"]].floor("D") + d_x = ex["idx"][ex["i_ex"]].floor("D") + dl = ex["base_daily"] + multi = np.where(d_e.values != d_x.values)[0] + ne = nx = ck = 0 + for k in multi: + n = float(ex["net"][k]) + if abs(n) < 1e-6: + continue + ck += 1 + ne += int(abs(float(dl.get(d_e[k], 0.0)) - n) < 1e-9) + nx += int(abs(float(dl.get(d_x[k], 0.0)) - n) < 1e-9) + print(f" {a}: {ck} trade multi-giorno -> ritorno giornaliero == net del trade " + f"al giorno d'INGRESSO {ne}/{ck}, al giorno di USCITA {nx}/{ck}") + tot_ent += ne; tot_ex += nx; tot_chk += ck + print(f" TOTALE: INGRESSO {tot_ent}/{tot_chk} ({tot_ent/max(tot_chk,1)*100:.1f}%) " + f"USCITA {tot_ex}/{tot_chk} ({tot_ex/max(tot_chk,1)*100:.1f}%)") + print(" -> `backtest_signals` fa `equity[i_ent:i_ex+1] = capital`: il gradino comincia") + print(" alla barra d'INGRESSO e il resample giornaliero (last) lo legge quel giorno.") + + print("\n (a) TRONCAMENTO ALLA BARRA D'INGRESSO — le size della regola congelata " + "ricalcolate") + print(" con i soli dati <= barra d'ingresso devono coincidere ESATTAMENTE") + worst = 0.0 + ntest = 0 + for a in ASSETS: + ex = EXV2[a] + full = size_vtl(FROZEN)(ex) + sel = [k for k in range(len(ex["i_ent"])) if ex["i_ent"][k] >= V.SIG_WARM] + sel = sel[:: max(1, len(sel) // 25)][:25] + bad = 0 + for k in sel: + i = int(ex["i_ent"][k]) + # ricostruisco la gamba usando SOLO i trade gia' CHIUSI prima dell'ingresso k + sub = {kk: ex[kk] for kk in ("n", "idx")} + m = ex["i_ex"] < i + sub = dict(n=i + 1, idx=ex["idx"][: i + 1], + i_ent=ex["i_ent"][m], i_ex=ex["i_ex"][m], net=ex["net"][m]) + if not len(sub["i_ent"]): + continue + bd = V.equity_daily(sub, np.ones(len(sub["i_ent"]))) + Lt = V.leverage_series(bd, **FROZEN) + day = ex["idx"][i].floor("D") + v = Lt.reindex([day]).values[0] + v = 1.0 if not np.isfinite(v) else float(v) + worst = max(worst, abs(v - float(full[k]))) + bad += int(abs(v - float(full[k])) > 1e-9) + ntest += 1 + print(f" {a}: {len(sel)-bad}/{len(sel)} size identiche a dati troncati " + f"(divergenze {bad})") + print(f" max|dif| su {ntest} size ricalcolate da zero: {worst:.2e} -> " + f"{'CAUSALE' if worst < 1e-9 else 'NON CAUSALE'}") + + print("\n (b) LA 'RIPARAZIONE' E' UN NO-OP: size-all'INGRESSO vs moltiplicare la SERIE") + print(" GIORNALIERA, a parita' di tutto il resto (stessa L, PER ASSET)") + for a in ASSETS: + ex = EXV2[a] + Ls = V.leverage_series(ex["base_daily"], **FROZEN) + s_in = V.equity_daily(ex, V.sizes_from_L(ex, Ls)) + s_mul = ex["base_daily"] * Ls.reindex(ex["base_daily"].index).fillna(1.0) + j = pd.concat({"a": s_in, "b": s_mul}, axis=1, join="inner").dropna() + print(f" {a}: max|dif| = {np.max(np.abs(j['a']-j['b'])):.2e} " + f"Sh ingresso {sh(s_in):.4f} vs Sh moltiplicazione {sh(s_mul):.4f}") + + print("\n (c) ALLORA COS'E' IL '+0,04 FANTASMA'? Il controllo NAIVE del bersaglio calcola L") + print(" sulla gamba 50/50 COMBINATA; la versione 'causale' la calcola PER ASSET.") + print(" Terza variante di controllo: L combinata, applicata come size all'INGRESSO.") + lg_base = {o: None for o in offs} + d_comb, d_perass = [], [] + for o in offs[: min(6, len(offs))]: + E2 = {a: V.extract(a, o) for a in ASSETS} + base = V.leg_daily(E2, {a: V.size_flat(E2[a]) for a in ASSETS}) + Lc = V.leverage_series(base, **FROZEN) + naive = base * Lc.reindex(base.index).fillna(1.0) + comb_in = V.leg_daily(E2, {a: V.sizes_from_L(E2[a], Lc) for a in ASSETS}) + per_ass = V.leg_daily( + E2, {a: V.sizes_from_L(E2[a], V.leverage_series(E2[a]["base_daily"], **FROZEN)) + for a in ASSETS}) + j = pd.concat({"a": naive, "b": comb_in}, axis=1, join="inner").dropna() + d_comb.append(float(np.max(np.abs(j["a"] - j["b"])))) + b1, c1, _ = iso_pair(TP, naive, base) + b2, c2, _ = iso_pair(TP, per_ass, base) + d_perass.append(sh(hold(b1)) - sh(hold(c1)) - (sh(hold(b2)) - sh(hold(c2)))) + del E2 + print(f" L combinata: 'moltiplica la serie' vs 'size all'ingresso' -> " + f"max|dif| mediano {np.median(d_comb):.2e} (identiche)") + print(f" differenza ISO dShHOLD [L combinata] - [L per asset], mediana appaiata su " + f"{len(d_perass)} ancore: {np.median(d_perass):+.3f}") + print(" -> il divario NAIVE/CAUSALE non e' QUANDO si applica la size, e' SU QUALE") + print(" SERIE si calcola L (gamba combinata vs per asset).") + + # ------------------------------------------------------------------ §2 TEST PRE-REGISTRATO + print("\n" + "-" * L) + print(" 2. IL TEST PRE-REGISTRATO — famiglia IV su V2-DD (stop % FISSO) vs V1 (stop in ATR)") + print(" Previsione dichiarata dall'agente: |effetto su V1| < |effetto su V2-DD|.") + print("-" * L) + n2 = {a: len(EXV2[a]["i_ent"]) for a in ASSETS} + n1 = {a: len(EXV1[a]["i_ent"]) for a in ASSETS} + print(f" trade all'ancora {offs[0]}: V2-DD {n2} V1 {n1}") + mb2 = float(np.median([sh(BK2["BASE"][o]) for o in offs])) + mb1 = float(np.median([sh(BK1["BASE"][o]) for o in offs])) + print(f" libro BASE mediana di banda: V2-DD ShFULL {mb2:.3f} | V1 ShFULL {mb1:.3f}") + names_iv = list(IV) + print(f"\n {'cella IV':<22}{'V2-DD ISO dF':>14}{'pos/n':>8}{'V1 ISO dF':>12}{'pos/n':>8}" + f"{'|V1|<|V2|?':>12}") + ok = tot = 0 + e2s, e1s = [], [] + for nm in names_iv: + a2 = A.anchor_luck_delta(lambda o, n=nm: BK2[n][o], lambda o, n=nm: CT2[n][o], offs, + metric=sh) + a1 = A.anchor_luck_delta(lambda o, n=nm: BK1[n][o], lambda o, n=nm: CT1[n][o], offs, + metric=sh) + e2, e1 = a2["median_paired"], a1["median_paired"] + e2s.append(e2); e1s.append(e1) + good = abs(e1) < abs(e2) + ok += int(good); tot += 1 + print(f" {nm:<22}{e2:>+14.3f}{a2['n_positive']:>5}/{len(offs):<3}{e1:>+12.3f}" + f"{a1['n_positive']:>5}/{len(offs):<3}{('SI' if good else 'NO'):>12}") + print(f"\n previsione verificata in {ok}/{tot} celle della famiglia") + print(f" |effetto| mediano di famiglia: V2-DD {np.median(np.abs(e2s)):.3f} " + f"V1 {np.median(np.abs(e1s)):.3f}") + print(f" effetto mediano SEGNATO: V2-DD {np.median(e2s):+.3f} V1 {np.median(e1s):+.3f}") + print(" ⚠️ POTENZA DEL TEST: se |effetto su V2-DD| e' gia' ~0, la previsione e' verificabile") + print(" solo per caso e il gate pre-registrato NON puo' falsificare niente.") + for nm in ("VTL tv20 w90 (LEAD)", "BIN L>=1 (0/1)"): + a2 = A.anchor_luck_delta(lambda o, n=nm: BK2[n][o], lambda o, n=nm: CT2[n][o], offs, + metric=sh) + a1 = A.anchor_luck_delta(lambda o, n=nm: BK1[n][o], lambda o, n=nm: CT1[n][o], offs, + metric=sh) + print(f" (riferimento) {nm:<24} V2-DD {a2['median_paired']:+.3f} " + f"({a2['n_positive']}/{len(offs)}) V1 {a1['median_paired']:+.3f} " + f"({a1['n_positive']}/{len(offs)})") + + # ------------------------------------------------------------------ §3 ATTACCO ALLA SPIEGAZIONE + print("\n" + "-" * L) + print(" 3. ATTACCO ALLA SPIEGAZIONE — quanto del +0,070 resta senza NESSUN vol-target?") + print(" Tutte ISO-PESO vs lo stesso baseline. 'wSKHeff' = peso effettivo di SKH nel libro") + print("-" * L) + grp = [lead, "BIN L>=1 (0/1)", "BIN+GRAD", "BIN L>=med (0/1)", "BIN L<1 (INVERSO)", + "DROP attivita' >p75", "DROP attivita' >p50", + "canale MAGNITUDINE cont.", "canale FREQUENZA cont.", + "canale MAGNIT. 0/1", "canale FREQ. 0/1"] + RES = delta_table(BK2, CT2, grp, offs, "attacco") + base_eff = RES[lead]["dF"] + print() + for nm in grp[1:]: + frac = RES[nm]["dF"] / base_eff * 100 if abs(base_eff) > 1e-9 else float("nan") + print(f" {nm:<26} recupera {frac:>6.0f}% del vantaggio del LEAD") + # quanti trade tocca ciascun filtro + print(f"\n {'cella':<26}{'trade tenuti':>14}{'su':>6}{'size mediana':>14}" + f"{'size = 0':>10}") + for nm in [lead] + grp[1:]: + kept = tot_t = zer = 0 + med = [] + for a in ASSETS: + s = FUN_V2[nm](EXV2[a]) + tot_t += len(s); kept += int((s > 0).sum()); zer += int((s == 0).sum()) + med.append(s) + mm = float(np.median(np.concatenate(med))) + print(f" {nm:<26}{kept:>14}{tot_t:>6}{mm:>14.2f}{zer:>10}") + + print(f"\n COSA SELEZIONA il filtro (ancora {offs[0]}, i due asset uniti): net medio dei") + print(" trade TENUTI vs SCARTATI, e attivita' trailing media dei due gruppi") + print(f" {'cella':<26}{'net TENUTI':>12}{'net SCARTATI':>14}{'att.TEN':>10}{'att.SCA':>10}" + f"{'%tenuti':>9}") + for nm in ("BIN L>=1 (0/1)", "BIN L>=med (0/1)", "BIN L<1 (INVERSO)", + "DROP attivita' >p50", "canale MAGNIT. 0/1", "canale FREQ. 0/1"): + nk, nd, ak, ad = [], [], [], [] + for a in ASSETS: + ex = EXV2[a] + szv = FUN_V2[nm](ex) + cnt = ((ex["base_daily"] != 0.0).astype(float) + .rolling(90, min_periods=30).sum().shift(1)) + av = cnt.reindex(ex["day_ent"]).values + keep = szv > 0 + nk += list(ex["net"][keep]); nd += list(ex["net"][~keep]) + ak += list(av[keep]); ad += list(av[~keep]) + f_ = 100.0 * len(nk) / max(len(nk) + len(nd), 1) + print(f" {nm:<26}{np.mean(nk)*100:>11.2f}%{np.mean(nd)*100 if nd else float('nan'):>13.2f}%" + f"{np.nanmean(ak):>10.1f}{np.nanmean(ad) if ad else float('nan'):>10.1f}{f_:>9.0f}") + + print("\n per anno (ISO dSh, mediana appaiata sulle ancore):") + yrs = None + for nm in [lead, "BIN L>=1 (0/1)", "DROP attivita' >p75", "BIN L<1 (INVERSO)"]: + yrs, cells = per_year(BK2, CT2, nm, offs) + if nm == lead: + print(" " + f"{'':<26}" + "".join(f"{y:>8}" for y in yrs)) + npos = int(np.nansum(np.asarray(cells) > 0)) + print(f" {nm:<26}" + "".join(f"{c:>+8.2f}" for c in cells) + + f" anni positivi {npos}/{len(yrs)}") + + # ------------------------------------------------------------------ §4 ASSE DELLA FINESTRA + print("\n" + "-" * L) + print(" 4. L'ASSE DELLA FINESTRA — picco o plateau? + scelta di w IN-SAMPLE-ONLY") + print("-" * L) + wnames = [(w, f"VTL tv20 w{w}" if w != 90 else lead) for w in WGRID] + print(f" {'w':>6}{'ISO dShFULL':>14}{'pos/n':>8}{'ISO dShIS':>12}{'ISO dShHOLD':>13}" + f"{'pos/n':>8}") + wf, wi, wh = {}, {}, {} + for w, nm in wnames: + dF = A.anchor_luck_delta(lambda o, n=nm: BK2[n][o], lambda o, n=nm: CT2[n][o], offs, + metric=sh) + dI = A.anchor_luck_delta(lambda o, n=nm: ins(BK2[n][o]), lambda o, n=nm: ins(CT2[n][o]), + offs, metric=sh) + dH = A.anchor_luck_delta(lambda o, n=nm: hold(BK2[n][o]), lambda o, n=nm: hold(CT2[n][o]), + offs, metric=sh) + wf[w], wi[w], wh[w] = dF["median_paired"], dI["median_paired"], dH["median_paired"] + print(f" {w:>6}{wf[w]:>+14.3f}{dF['n_positive']:>5}/{len(offs):<3}{wi[w]:>+12.3f}" + f"{wh[w]:>+13.3f}{dH['n_positive']:>5}/{len(offs):<3}") + wbest_full = max(WGRID, key=lambda w: wf[w]) + wbest_is = max(WGRID, key=lambda w: wi[w]) + print(f"\n argmax su FULL (cio' che l'agente ha pubblicato): w = {wbest_full} " + f"({wf[wbest_full]:+.3f})") + print(f" argmax IN-SAMPLE-ONLY (la sola scelta ammessa): w = {wbest_is} " + f"(IS {wi[wbest_is]:+.3f}) -> quella cella vale FULL {wf[wbest_is]:+.3f} / " + f"HOLD {wh[wbest_is]:+.3f}") + order = sorted(WGRID) + k = order.index(wbest_full) + nb = [order[k - 1] if k > 0 else None, order[k + 1] if k + 1 < len(order) else None] + for w in nb: + if w is not None: + print(f" vicino w={w:<4} vale {wf[w]:+.3f} = {wf[w]/wf[wbest_full]*100:>5.0f}% " + f"del picco") + pos = sum(1 for w in WGRID if wf[w] > 0) + print(f" celle con ISO dShFULL > 0: {pos}/{len(WGRID)} " + f"spread della famiglia {max(wf.values())-min(wf.values()):.3f} " + f"sd fra celle {np.std(list(wf.values()), ddof=1):.3f}") + print(f" stima ONESTA se w e' SCELTO (mediana della famiglia, come per la banda " + f"d'ancora): {np.median(list(wf.values())):+.3f} FULL / " + f"{np.median(list(wh.values())):+.3f} HOLD contro il picco " + f"{wf[wbest_full]:+.3f} / {wh[wbest_full]:+.3f}") + rho = float(np.corrcoef([wi[w] for w in WGRID], [wh[w] for w in WGRID])[0, 1]) + print(f" corr(ISO dSh in-sample, ISO dSh hold-out) fra le {len(WGRID)} finestre: {rho:+.2f}") + print(" -> se e' ~0 o negativa, la scelta di w in-sample NON predice l'hold-out: l'asse") + print(" e' rumore e il picco a w=90 e' un max-of-k.") + + # ------------------------------------------------------------------ §5 sintesi + print("\n" + "-" * L) + print(" 5. SINTESI DEI NUMERI CHE CONTANO") + print("-" * L) + print(f" fortuna d'ancora del LIBRO BASE: ShFULL canonico " + f"{sh(BK2['BASE'][0]) if 0 in BK2['BASE'] else float('nan'):.3f} vs mediana " + f"{med_F:.3f}") + print(f" vantaggio del LEAD (ISO): {dLEAD['median_paired']:+.3f} FULL / " + f"{dLEADH['median_paired']:+.3f} HOLD") + print(f" riprodotto dal FILTRO BINARIO: {RES['BIN L>=1 (0/1)']['dF']:+.3f} FULL / " + f"{RES['BIN L>=1 (0/1)']['dH']:+.3f} HOLD") + print(f" controllo di segno (INVERSO): {RES['BIN L<1 (INVERSO)']['dF']:+.3f} FULL") + print(f"\n fatto in {time.time()-t0:.0f}s") + print("=" * L) + + +if __name__ == "__main__": + main() From 5fe931e00d1e5d4f211d02ad895d93e52308f81a Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:34:51 +0000 Subject: [PATCH 015/114] =?UTF-8?q?research(wave-0822):=20SLIP-AUDIT=20?= =?UTF-8?q?=E2=80=94=20nessun=20costo=20nascosto=20a=20questa=20taglia,=20?= =?UTF-8?q?ma=20la=20misura=20scade=20(22%=20del=20nastro=20a=20600=20dive?= =?UTF-8?q?nta=20687%=20a=2020k)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index a70296c..1f1849c 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -18,6 +18,7 @@ null de-levering superato + eseguibilita' al capitale dichiarato. | — | **XSR-REPRO** (integrita') | 🚨 **DIFETTO DI PRODUZIONE** | il numero 1.82 e' SPIEGATO e non era sbagliato (era su una **terza** lente, e su una barra non ancora chiusa) — ma cercandone la causa e' emerso che **`paper_xsr` registra ~41 minuti di mercato al giorno**, non un giorno. Tre gate pre-registrati leggono serie costruite cosi' | | 5 | SKEW | **SCARTATO** (Q1, Q2) + **LEAD** (Q3, gate 2027-02-22) | il prezzo muove lo skew (t 3,1-9,4 su 8/8 test), **non il contrario** (max |t| in avanti 2,35 contro 2,08 atteso dal rumore). Ma Q3 e' grosso: **il f=0,73 di VRP01 e' per il 42% STRUTTURA A TERMINE e solo per il 25% skew** | | — | **HL-EXEC** (audit di fatto) | **3 falsificazioni misurate** | il pavimento vero e' **$10 (non $5)** e il taker **4,50 bps (non 5,0)** — ma il *"XS01 serve ~$20k"* e' **refutato del tutto** (nessuna soglia da min-order), il *"XSR01 ~$5.000"* e' **conservativo di 1,7x** (vero ~$3.000), e lo **slippage "rischio #1" di XSR01 e' refutato** con margine **21x** | +| — | **SLIP-AUDIT** (audit di fatto) | **SCARTATO** = nessun costo nascosto a questa taglia | i backtest a 10 bps RT restano **conservativi di ~1,5 bps/lato**; nessuna evidenza di impatto sopravvive al null (p=0,274; estremi avversi **1/18 contro 4,7 attesi**). **Ma la misura ha una data di scadenza** | ## Note che sopravvivono ai singoli filoni @@ -407,3 +408,36 @@ algebricamente** la gamba BTC (`sum(w)=0`) — che e' la ragione stessa per cui del singolo decimale): titolo `demean_skeptic` **1,82** · gate `basket_gate` **1,75-1,79** (fee raddoppiate su una gamba inesistente) · monitor `paper_xsr` **2,21**. **La lente che ha girato i gate e' quella PESSIMISTICA** -> il **DSR 0,983 PASS e' stato calcolato su una serie conservativa**. + +### SLIP-AUDIT (r0822_slip_audit.py, 18 fill reali, ~14 varianti dichiarate) +✅ **Fee sul NASTRO: 5,00 bps/lato prima del 2026-08-01, 3,50 dopo (16/16 esatti).** Il taglio +Deribit e' visibile nei fill reali -> conferma **indipendente** di `fee_watch` (che legge il tier +dichiarato, non le esecuzioni) e della regola pre-registrata del 26/07 (<=5 bps -> non si tocca +nulla). **Il listino si e' mosso nel verso buono.** +- Slippage d'esecuzione (riferimento 5m): mediana **−4,65 bps**, IC95 [−34,6; +20,6]; statistica + normalizzata **+1,54 bps** [−1,30; +4,47]. Conversione con la curva pubblicata: **dSharpe −0,026 / + dCAGR −0,35 pp**. +- ⚠️ **Autocorrezione della propria potenza:** il null **empirico** e' **1,43x piu' largo** + dell'uniforme assunto -> MDE vera **7,2 bps**, non 5,0. **Con n=18 nessuna via statistica risolve + 1,5 bps** — dichiarato invece di spacciare l'IC per una risposta. +📌 **Esperimento naturale, il caso peggiore del campione:** il fill del **sabato 18/07** (ETH $74) e' +stato il **21,9% del volume della sua barra 5m**, e quella barra ha avuto un range di **1,08 bps**, +col nostro print a meta' (q=0,48). *Un ordine che prende un quinto del nastro e lascia il prezzo +entro 1 bps* e' la prova diretta che a questa taglia il libro **non cammina il book**. +🚨 **La misura ha una DATA DI SCADENZA, e va scritta accanto al numero:** la partecipazione scala +lineare -> quel 22% diventa **172% di una barra 5m a $5.000** e **687% a $20.000**. **Non si +estrapola: va rifatta a ogni salto di taglia.** E' il muro di eseguibilita' gia' noto ma sull'asse +dell'**attrito** invece che del min-order. ⚠️ E il caso peggiore cade di **sabato**, dove TP01 fa il +**38%** del proprio gross. +⚠️ **Due difetti del dato, trovati PRIMA di misurare (e ciascuno avrebbe prodotto un numero +plausibile e falso):** +(a) `ts_utc` in `book_executions.jsonl` **non e' l'ora del fill** ma l'etichetta dell'ultima barra +del feed (**18/18 dicono `00:00:00`**; il fill #1 e' etichettato 08/07 ed e' stato eseguito il +**14/07** = impronta del feed-freeze gia' noto). L'ora vera esiste **solo** in `logs/cron_book.log`: +senza recuperarla, ogni confronto "al prezzo dell'ora del fill" misurerebbe **la mezzanotte**. +(b) Il feed certificato e' il perpetual **INVERSE**, il libro trada il **LINEARE USDC** — **4ª +occorrenza** dello schema `fee_watch`: misurare lo slippage sul feed certificato avrebbe misurato +**la base**, non lo slippage (base misurata: BTC −0,03 bps, ETH −0,27). +⚠️ Con n=18 **la media in bps e' inservibile** — un solo giorno (19/08, ETH +14% in 6h) la ribalta: +si cita la **mediana**. E `REF-A` (−50 bps) **non e' slippage**: e' la differenza di FASE fra l'ora +del cron e la chiusura giornaliera, gia' misurata bene il 26/07 su 24 ancore appaiate. From 32ed18222e6809c0f3f0f5019c727afb9e81612c Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:36:45 +0000 Subject: [PATCH 016/114] =?UTF-8?q?research(wave-0822):=20lo=20scettico=20?= =?UTF-8?q?conferma=20il=20gradino=20di=20leva=20ma=20corregge=20due=20con?= =?UTF-8?q?torni=20=E2=80=94=20e=20la=20lente=20close-only=20e'=20cieca=20?= =?UTF-8?q?solo=20sulle=20regole=20a=20UN=20giorno?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 58 ++++++++++++++ scripts/research/r0822_basis_calendar.py | 18 +++-- scripts/research/r0822_volsize_skeptic.py | 92 ++++++++++++++++------- 3 files changed, 134 insertions(+), 34 deletions(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 1f1849c..384f49d 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -19,6 +19,7 @@ null de-levering superato + eseguibilita' al capitale dichiarato. | 5 | SKEW | **SCARTATO** (Q1, Q2) + **LEAD** (Q3, gate 2027-02-22) | il prezzo muove lo skew (t 3,1-9,4 su 8/8 test), **non il contrario** (max |t| in avanti 2,35 contro 2,08 atteso dal rumore). Ma Q3 e' grosso: **il f=0,73 di VRP01 e' per il 42% STRUTTURA A TERMINE e solo per il 25% skew** | | — | **HL-EXEC** (audit di fatto) | **3 falsificazioni misurate** | il pavimento vero e' **$10 (non $5)** e il taker **4,50 bps (non 5,0)** — ma il *"XS01 serve ~$20k"* e' **refutato del tutto** (nessuna soglia da min-order), il *"XSR01 ~$5.000"* e' **conservativo di 1,7x** (vero ~$3.000), e lo **slippage "rischio #1" di XSR01 e' refutato** con margine **21x** | | — | **SLIP-AUDIT** (audit di fatto) | **SCARTATO** = nessun costo nascosto a questa taglia | i backtest a 10 bps RT restano **conservativi di ~1,5 bps/lato**; nessuna evidenza di impatto sopravvive al null (p=0,274; estremi avversi **1/18 contro 4,7 attesi**). **Ma la misura ha una data di scadenza** | +| — | **SCETTICO su GROWTH-POLICY** | **REGGE** (condizione bloccante DECADUTA) | la lente wick accoppiata **non cambia il gradino**: e' un ricarico moltiplicativo **costante ~3,5%** sul maxDD, non un'amplificazione (Δ/k **cala**). Ma su una regola a **UN giorno** close-only e' **esattamente cieca** (rapporto INF) -> la lezione 25/07 e' vera e **non si trasferisce** | ## Note che sopravvivono ai singoli filoni @@ -441,3 +442,60 @@ occorrenza** dello schema `fee_watch`: misurare lo slippage sul feed certificato ⚠️ Con n=18 **la media in bps e' inservibile** — un solo giorno (19/08, ETH +14% in 6h) la ribalta: si cita la **mediana**. E `REF-A` (−50 bps) **non e' slippage**: e' la differenza di FASE fra l'ora del cron e la chiusura giornaliera, gia' misurata bene il 26/07 su 24 ancore appaiate. + +### SCETTICO su GROWTH-POLICY (r0822_leverage_skeptic.py, ~175 celle, griglia ridotta a k<=5x e dichiarato) +✅ **Replica superata TRE volte** prima di ogni numero nuovo: marginali del gap 25/07 (p50 −0,16 / +p90 −1,03 / p99 −2,71 contro −0,17 / −1,03 / −2,70), meccanismo del decile (~3x, pubblicato ~3x), +e €500/mese -> **14,7a / P<=20a 99%** al numero pubblicato. +✅ **Controllo positivo superato:** una lente *indipendente* (gap rimescolato) sovrastima il maxDD di ++0,13pp su +0,45 e i breach daily-loss di **1,5-2,4x** — stesso verso e taglia del finding 25/07 +("2,0-2,9x"). **La macchineria ha potenza, quindi il suo "nessuna differenza" vale.** +✅ Risoluzione MC dichiarata: **0,01%/a** nella regione eseguibile, 0,05pp sul Δ appaiato di maxDD -> +le differenze riportate (0,45-0,76pp) sono **9-15x la risoluzione**. +📌 **RISPOSTA ALLA DOMANDA: NO, l'effetto e' SOTTO-proporzionale.** Il rapporto fra le due maxDD e' +**piatto a 1,025-1,037** su tutta la griglia e Δ/k **cala** (0,478% -> 0,217%). **Meccanismo: il +maxDD e' una statistica MULTI-GIORNO, il wick e' un fatto di UN giorno.** Tempo al bersaglio +**identico** fra le due lenti; guadagno appaiato 1,00x->1,50x = **2,88 anni** in entrambe. +Nessuna liquidazione a nessun k<=5x sotto nessuna lente (servirebbe **12,3x** sul peggior giorno vissuto). +🚨 **IL RISULTATO TRASFERIBILE — la lezione del 25/07 e' vera e NON si trasferisce.** Su una regola a +**UN giorno** l'accoppiamento cambia tutto: a soglia daily-loss 5% e k=1,00-1,25 close-only conta +**0,00 breach/anno** dove il vero ne conta **0,40 e 1,21** (`acc./close = INF`, cecita' esatta); a +soglia 3% e k=1, **0,40 contro 2,28 = 5,7x**. Su una statistica **multi-giorno** lo stesso difetto +vale **3,5%**. ⚠️ **Non assolve close-only in generale: sul canale PROP/FUNDED, dove il vincolo +binding E' una regola di daily-loss per-conto, la lente accoppiata resta OBBLIGATORIA.** +⚠️ **CORREZIONE a una spiegazione pubblicata (il fatto regge, la spiegazione no):** il **26,8%** di +giorni con `m == R` **non** sono "il giorno brutto che chiude sul minimo" — stanno nei decili +**CENTRALI** (56-88%), i giorni in cui il libro e' **flat**. Nel decile peggiore chiude sul minimo +il **4%**. E un giorno di crash **chiude quasi sul proprio minimo** (gap medio nel 5% peggiore +−0,62% contro −1,75% nel 5% migliore) -> **la lente accoppiata non peggiora lo stress di coda.** +📌 **Il gradino 1,00->1,50 resta NON AUTORIZZATO** — cade **una** delle tre riserve, non le tre. +Restano: drift stimato su 7,4 anni, e coda assente dal dataset (un giorno −10%/anno porta k* a 2x). +Unica colonna a favore della prudenza: **P(sfondare una soglia FISSA) e' amplificata piu' del maxDD** +(1,26x a k=1) perche' vicino alla soglia la densita' e' fitta — **e' quella la colonna con cui si +costruisce una frontiera iso-rischio, non il maxDD mediano**. +📌 **ATTACCO 2 — il 12,14% di TP01 e' VERO, ma il meccanismo dichiarato e' META', e la strada +implicita e' REFUTATA.** Scomposizione esatta, verificata sullo sleeve di **produzione**: +`20,00% x 0,682 (dentro la gamba) x 0,890 (diversificazione BTC/ETH, corr 0,59) = 12,14%`. +E il x0,682 sono **due meta' quasi uguali**: flat nel **44,3%** delle barre (x0,746) **e** convinzione +**parziale** quando non e' flat (`tsmom_blend` = media di 3 segni: 1/3 o 2/3) (x0,789). +**"TP01 e' long-flat" spiega circa META'** — e la differenza non e' accademica: essendo +*convinzione*, **alzare `target_vol` aumenta la size proprio nei giorni in cui il segnale e' piu' +debole**. Il terzo fattore (x0,89) **non e' un difetto d'etichetta: e' diversificazione**, ed e' +cio' che si vuole. +⚠️ **E TP01 e' un caso particolare, non la regola: XS01 dichiara lo stesso 20% e ne realizza 20,59%.** +🚨 **`target_vol` e `k` NON sono la stessa leva** (e non per il leverage cap, che non morde: 0-1% di +barre al cap fino a tv=60%): **`target_vol` scala il 75% del libro e lascia fermo il 25%** -> a +tv=30% il libro non e' piu' 75/25 ma **82/18**, a 60% **90/10**. E' simultaneamente una leva **e un +tilt di pesi**, e il tilt **FALLISCE `weights_tilt_null`** (gate_pass=False, delta_insample −0,0815 +a tv=30% / −0,2187 a tv=60%). A pari scala 1,50 il libro via `target_vol` fa Sharpe **1,72** contro +**1,82** via k. -> **la strada pulita per un puro cambio di SCALA e' il cap di config, non il target_vol.** +✅ **Cosa cambia dei numeri pubblicati: NIENTE di misurato** — ogni Sharpe/maxDD/muro/traiettoria e' +calcolato sulla **serie realizzata**; `target_vol` non compare in nessuna formula di rischio a valle. +❌ **Ipotesi dell'agente refutata dal suo stesso numero:** i contributi di rischio sono **73/27** +contro pesi 75/25. **Cio' che resta:** sbaglia chi legge l'**etichetta**, non chi legge i **pesi** — +un lettore che prendesse "20%" per buono attribuirebbe a TP01 l'**89%** del rischio invece del +**73%**, cioe' sbaglierebbe di 16 punti proprio la domanda *"quale gamba mi fa male"*. +📌 Il test di guardia `test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` **non protegge "la leva +e' pericolosa"**: protegge la conclusione del 26/07 secondo cui la **liquidation fee 1% di Deribit e' +irrilevante**, che vale interamente perche' a <=1x servirebbe un movimento avverso ~100%. Chi alza il +cap deve **rifare `r0726_fee_sensitivity`**, non aggirare l'assert. diff --git a/scripts/research/r0822_basis_calendar.py b/scripts/research/r0822_basis_calendar.py index b36fac0..22d2ddc 100644 --- a/scripts/research/r0822_basis_calendar.py +++ b/scripts/research/r0822_basis_calendar.py @@ -638,8 +638,10 @@ def stats(daily: pd.Series) -> dict: cagr = float(eq.iloc[-1] ** (1 / yrs) - 1) if yrs > 0.5 and eq.iloc[-1] > 0 else float("nan") ho = r[r.index >= HOLDOUT] sho = float(ho.mean() / ho.std() * math.sqrt(365.25)) if len(ho) > 30 and ho.std() > 0 else float("nan") + yrs_ = max(len(r) / 365.25, 1e-9) return dict(sharpe=sh, holdout=sho, maxdd=dd, cagr=cagr, n=len(r), - vol=float(r.std() * math.sqrt(365.25))) + vol=float(r.std() * math.sqrt(365.25)), + se_ann=float(r.std() * math.sqrt(365.25) / math.sqrt(yrs_))) def delevering_null(cand: pd.Series, base: pd.Series) -> dict: @@ -737,7 +739,7 @@ def main() -> None: # ---------------- STEP 4: griglia ---------------- print("\n[4] GRIGLIA — dichiarata PRIMA, contata AL RIALZO") FAM = {"CAL": ["static_long", "static_short", "zrev", "zmom"], - "FvP": ["carry", "static_long", "zrev", "zmom"]} + "FvP": ["carry", "static_long", "static_short", "zrev", "zmom"]} WINS = [720, 2160, 4320] # 30 / 90 / 180 giorni in ore ROLLS = [7, 21] THRS = [0.0, 1.0] @@ -794,7 +796,7 @@ def main() -> None: print("\n LE CELLE CANONICHE (quelle che un lettore chiede per nome), roll=7g:") for fam, sig, lab in [ ("FvP", "carry", "vendi il datato quando e' in CONTANGO / compra perp (il carry classico)"), - ("FvP", "static_short", "SEMPRE corto il datato / lungo perp"), + ("FvP", "static_short", "SEMPRE corto il datato / lungo perp = LA RACCOLTA DEL CONTANGO"), ("FvP", "static_long", "SEMPRE lungo il datato / corto perp (il contrario)"), ("CAL", "static_long", "calendar: lungo back / corto front"), ("CAL", "static_short", "calendar: corto back / lungo front")]: @@ -802,8 +804,8 @@ def main() -> None: if len(sub): r = G.loc[sub.index[0]] print(f" {fam}/{sig:12s} Sh {r.sharpe:+6.2f} hold {r.holdout:+6.2f} " - f"CAGR {r.cagr*100:+6.2f}% DD {r.maxdd*100:5.2f}% vol {r.vol*100:5.2f}%" - f" <- {lab}") + f"CAGR {r.cagr*100:+6.2f}% (+-{r.se_ann*100:.2f} SE) DD {r.maxdd*100:5.2f}% " + f"vol {r.vol*100:5.2f}% <- {lab}") print("\n migliori 8 celle per Sharpe FULL (NON e' la selezione — solo panorama):") for i in G.sharpe.nlargest(8).index: @@ -826,7 +828,11 @@ def main() -> None: print(f" cella scelta AL BUIO: {b.family} {b.sig} W={int(b.win)}h roll={int(b.roll)}g thr={b.thr}") print(f" Sharpe in-sample {ins[best_i]:+.2f} -> FULL {bs['sharpe']:+.2f} " f"HOLD-OUT {bs['holdout']:+.2f} maxDD {bs['maxdd']*100:.2f}% " - f"CAGR {bs['cagr']*100:+.2f}% vol {bs['vol']*100:.2f}%") + f"CAGR {bs['cagr']*100:+.2f}% (+-{bs['se_ann']*100:.2f} SE annuo) " + f"vol {bs['vol']*100:.2f}%") + print(f" NB con {bs['n']/365.25:.1f} anni e vol {bs['vol']*100:.1f}% l'errore standard del " + f"rendimento annuo e' {bs['se_ann']*100:.2f}pp: sotto ~{2*bs['se_ann']*100:.1f}pp/anno " + f"NIENTE e' distinguibile da zero, ne' in un verso ne' nell'altro.") best_full = G.sharpe.idxmax() print(f" (per confronto, la cella col miglior FULL sarebbe stata " f"{G.loc[best_full,'family']} {G.loc[best_full,'sig']} " diff --git a/scripts/research/r0822_volsize_skeptic.py b/scripts/research/r0822_volsize_skeptic.py index 49efa1b..95d9c8d 100644 --- a/scripts/research/r0822_volsize_skeptic.py +++ b/scripts/research/r0822_volsize_skeptic.py @@ -271,6 +271,15 @@ def main() -> None: ("V1", EXV1, FUN_V1, BK1, CT1)): lg = legs(EXX, FUN) base = lg["BASE"] + if tag == "V2": + # controlli di §1(c): L calcolata sulla gamba COMBINATA (cio' che fa il ramo + # NAIVE del bersaglio), applicata nei DUE modi. Se coincidono, il "fantasma" + # non e' un problema di QUANDO si applica la size. + for w in (30, 90): + Lc = V.leverage_series(base, **dict(FROZEN, w=w)) + lg[f"NAIVEcomb w{w}"] = base * Lc.reindex(base.index).fillna(1.0) + lg[f"COMBingr w{w}"] = V.leg_daily( + EXX, {a: V.sizes_from_L(EXX[a], Lc) for a in ASSETS}) for nm, s in lg.items(): b, c, m_s = iso_pair(TP, s, base) BKd.setdefault(nm, {})[o] = b @@ -386,32 +395,29 @@ def main() -> None: print(f" {a}: max|dif| = {np.max(np.abs(j['a']-j['b'])):.2e} " f"Sh ingresso {sh(s_in):.4f} vs Sh moltiplicazione {sh(s_mul):.4f}") - print("\n (c) ALLORA COS'E' IL '+0,04 FANTASMA'? Il controllo NAIVE del bersaglio calcola L") - print(" sulla gamba 50/50 COMBINATA; la versione 'causale' la calcola PER ASSET.") - print(" Terza variante di controllo: L combinata, applicata come size all'INGRESSO.") - lg_base = {o: None for o in offs} - d_comb, d_perass = [], [] - for o in offs[: min(6, len(offs))]: - E2 = {a: V.extract(a, o) for a in ASSETS} - base = V.leg_daily(E2, {a: V.size_flat(E2[a]) for a in ASSETS}) - Lc = V.leverage_series(base, **FROZEN) - naive = base * Lc.reindex(base.index).fillna(1.0) - comb_in = V.leg_daily(E2, {a: V.sizes_from_L(E2[a], Lc) for a in ASSETS}) - per_ass = V.leg_daily( - E2, {a: V.sizes_from_L(E2[a], V.leverage_series(E2[a]["base_daily"], **FROZEN)) - for a in ASSETS}) - j = pd.concat({"a": naive, "b": comb_in}, axis=1, join="inner").dropna() - d_comb.append(float(np.max(np.abs(j["a"] - j["b"])))) - b1, c1, _ = iso_pair(TP, naive, base) - b2, c2, _ = iso_pair(TP, per_ass, base) - d_perass.append(sh(hold(b1)) - sh(hold(c1)) - (sh(hold(b2)) - sh(hold(c2)))) - del E2 - print(f" L combinata: 'moltiplica la serie' vs 'size all'ingresso' -> " - f"max|dif| mediano {np.median(d_comb):.2e} (identiche)") - print(f" differenza ISO dShHOLD [L combinata] - [L per asset], mediana appaiata su " - f"{len(d_perass)} ancore: {np.median(d_perass):+.3f}") - print(" -> il divario NAIVE/CAUSALE non e' QUANDO si applica la size, e' SU QUALE") - print(" SERIE si calcola L (gamba combinata vs per asset).") + print("\n (c) ALLORA COS'E' IL '+0,04 FANTASMA'? Il ramo NAIVE del bersaglio calcola L") + print(" sulla gamba 50/50 COMBINATA e MOLTIPLICA la serie; il ramo 'causale' calcola L") + print(" PER ASSET e la applica all'INGRESSO. Sono DUE differenze in una. Le separo:") + for w in (30, 90): + d = [] + for o in offs: + j = pd.concat({"a": BK2[f"NAIVEcomb w{w}"][o], "b": BK2[f"COMBingr w{w}"][o]}, + axis=1, join="inner").dropna() + d.append(float(np.max(np.abs(j["a"] - j["b"])))) + print(f" w{w}: [L combinata, MOLTIPLICA] vs [L combinata, size all'INGRESSO] -> " + f"max|dif| mediano {np.median(d):.2e} = IDENTICHE") + print(f"\n {'coppia':<38}{'dShFULL':>10}{'dShHOLD':>10} (delta GREZZI vs baseline nudo,") + print(f" {'':<38}{'':>10}{'':>10} la convenzione del §4 del bersaglio)") + for w in (30, 90): + for nm in (f"NAIVEcomb w{w}", f"COMBingr w{w}", + (lead if w == 90 else f"VTL tv20 w{w}")): + gF = A.anchor_luck_delta(lambda o, n=nm: BK2[n][o], lambda o: BK2["BASE"][o], offs, + metric=sh) + gH = A.anchor_luck_delta(lambda o, n=nm: hold(BK2[n][o]), + lambda o: hold(BK2["BASE"][o]), offs, metric=sh) + print(f" {nm:<38}{gF['median_paired']:>+10.3f}{gH['median_paired']:>+10.3f}") + print(" -> il 'fantasma' e' interamente [L combinata] - [L per asset]: una scelta di") + print(" SERIE DI RIFERIMENTO, non un look-ahead. Il ramo NAIVE non e' non-causale.") # ------------------------------------------------------------------ §2 TEST PRE-REGISTRATO print("\n" + "-" * L) @@ -560,7 +566,34 @@ def main() -> None: # ------------------------------------------------------------------ §5 sintesi print("\n" + "-" * L) - print(" 5. SINTESI DEI NUMERI CHE CONTANO") + print(" 5. GATE (per il contratto di consegna)") + print("-" * L) + can = offs[0] + legs_can = {} + for nm in ("BASE", lead, "BIN L>=med (0/1)"): + legs_can[nm] = (V.leg_daily(EXV2, {a: V.size_flat(EXV2[a]) for a in ASSETS}) + if nm == "BASE" + else V.leg_daily(EXV2, {a: FUN_V2[nm](EXV2[a]) for a in ASSETS})) + nlos = int((EXV2["BTC"]["net"] < 0).sum() + (EXV2["ETH"]["net"] < 0).sum()) + ntot = int(len(EXV2["BTC"]["net"]) + len(EXV2["ETH"]["net"])) + for nm, s_ in legs_can.items(): + m = A.marginal_vs_tp01(s_) + imp = A.implausible_sharpe(s_, n_trades=ntot, n_losing_trades=nlos) + print(f" {nm:<22} marginal {str(m.get('marginal_verdict')):<9} corr {m.get('corr_full')}" + f" implausible={imp['implausible']} (perdite/attive " + f"{imp.get('loss_frac', 0)*100:.1f}%)") + trials = [float(np.median([sh(BK2[n][o]) for o in offs])) for n in BK2] + dsr_c, sr0 = A.deflated_sharpe(float(np.median([sh(BK2[lead][o]) for o in offs])), trials, + BK2[lead][can]) + dsr_b, _ = A.deflated_sharpe(med_F, trials, BK2["BASE"][can]) + print(f" deflated-Sharpe: candidato {dsr_c:.3f} / BASELINE {dsr_b:.3f} su {len(trials)} " + f"trial (sd {np.std(trials, ddof=1):.4f}) -> VACUO, lo passa anche il baseline") + print(" null de-levering: DEGENERE per costruzione (sh(k*base) == sh(base)); il suo") + print(" sostituto e' il confronto ISO-PESO usato in tutte le tabelle sopra.") + print(" causalita': §1(a) 50/50 size ricalcolate da dati troncati, max|dif| 0.00e+00.") + + print("\n" + "-" * L) + print(" 6. SINTESI DEI NUMERI CHE CONTANO") print("-" * L) print(f" fortuna d'ancora del LIBRO BASE: ShFULL canonico " f"{sh(BK2['BASE'][0]) if 0 in BK2['BASE'] else float('nan'):.3f} vs mediana " @@ -569,7 +602,10 @@ def main() -> None: f"{dLEADH['median_paired']:+.3f} HOLD") print(f" riprodotto dal FILTRO BINARIO: {RES['BIN L>=1 (0/1)']['dF']:+.3f} FULL / " f"{RES['BIN L>=1 (0/1)']['dH']:+.3f} HOLD") - print(f" controllo di segno (INVERSO): {RES['BIN L<1 (INVERSO)']['dF']:+.3f} FULL") + print(f" riprodotto dal BINARIO alla MEDIANA:{RES['BIN L>=med (0/1)']['dF']:+.3f} FULL / " + f"{RES['BIN L>=med (0/1)']['dH']:+.3f} HOLD ({RES['BIN L>=med (0/1)']['nF']}/{len(offs)} ancore)") + print(f" controllo di segno (INVERSO): {RES['BIN L<1 (INVERSO)']['dF']:+.3f} FULL " + f"({RES['BIN L<1 (INVERSO)']['nF']}/{len(offs)}) -> il filtro e' reale, non un artefatto") print(f"\n fatto in {time.time()-t0:.0f}s") print("=" * L) From b6c0deb866f12bd916149395a1ee25c986884c81 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:39:55 +0000 Subject: [PATCH 017/114] =?UTF-8?q?research(wave-0822):=20BASIS-CALENDAR?= =?UTF-8?q?=20scartato=20=E2=80=94=20il=20basis=20dei=20datati=20E'=20il?= =?UTF-8?q?=20funding=20del=20perp;=20ma=20in=20regalo=20240k=20barre=20di?= =?UTF-8?q?=20futures=20scaduti=20col=202022=20dentro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 46 ++ scripts/research/r0822_alt_options.py | 542 ++++++++++-------- scripts/research/r0822_basis_calendar.py | 10 + scripts/research/r0822_monitor_audit.py | 662 ++++++++++++++++++++++ scripts/research/r0822_volsize_skeptic.py | 40 +- 5 files changed, 1071 insertions(+), 229 deletions(-) create mode 100644 scripts/research/r0822_monitor_audit.py diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 384f49d..e685821 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -20,6 +20,7 @@ null de-levering superato + eseguibilita' al capitale dichiarato. | — | **HL-EXEC** (audit di fatto) | **3 falsificazioni misurate** | il pavimento vero e' **$10 (non $5)** e il taker **4,50 bps (non 5,0)** — ma il *"XS01 serve ~$20k"* e' **refutato del tutto** (nessuna soglia da min-order), il *"XSR01 ~$5.000"* e' **conservativo di 1,7x** (vero ~$3.000), e lo **slippage "rischio #1" di XSR01 e' refutato** con margine **21x** | | — | **SLIP-AUDIT** (audit di fatto) | **SCARTATO** = nessun costo nascosto a questa taglia | i backtest a 10 bps RT restano **conservativi di ~1,5 bps/lato**; nessuna evidenza di impatto sopravvive al null (p=0,274; estremi avversi **1/18 contro 4,7 attesi**). **Ma la misura ha una data di scadenza** | | — | **SCETTICO su GROWTH-POLICY** | **REGGE** (condizione bloccante DECADUTA) | la lente wick accoppiata **non cambia il gradino**: e' un ricarico moltiplicativo **costante ~3,5%** sul maxDD, non un'amplificazione (Δ/k **cala**). Ma su una regola a **UN giorno** close-only e' **esattamente cieca** (rapporto INF) -> la lezione 25/07 e' vera e **non si trasferisce** | +| 13 | BASIS-CALENDAR | **SCARTATO** + **1 dataset nuovo** | il basis dei futures datati **E' il funding del perp quotato diversamente** (7,33% contro 6,48%/anno): non e' un quarto lato del filone funding, e' lo stesso lato. Premio incassabile **+0,85%/anno con IC95 che contiene lo zero** | ## Note che sopravvivono ai singoli filoni @@ -499,3 +500,48 @@ un lettore che prendesse "20%" per buono attribuirebbe a TP01 l'**89%** del risc e' pericolosa"**: protegge la conclusione del 26/07 secondo cui la **liquidation fee 1% di Deribit e' irrilevante**, che vale interamente perche' a <=1x servirebbe un movimento avverso ~100%. Chi alza il cap deve **rifare `r0726_fee_sensitivity`**, non aggirare l'assert. + +### 13 — BASIS-CALENDAR (r0822_basis_calendar.py, 108 celle dichiarate) +✅ **HA COSTRUITO UN DATASET CHE IL PROGETTO NON AVEVA, e contiene la coda che mancava a CC01.** +Deribit **purga** le liste di strumenti scaduti (`get_instruments?expired=true` ritorna **1** solo +strumento) -> la via ovvia e' chiusa. Ma i nomi dei trimestrali sono deterministici (ultimo venerdi' +di MAR/GIU/SET/DIC 08:00 UTC) e **`get_tradingview_chart_data` serve la storia completa di un +contratto SCADUTO se il nome lo si costruisce a mano**: **30/30 trimestrali 2019Q1-2026Q2 rispondono +ok**. Risultato: **BTC 34 contratti / 240.150 barre orarie (2018-09 -> oggi), ETH 34 / 233.334**, +piu' **64.110 ore** di funding e indice per asset. **Include il deleveraging 2022** — esattamente la +coda che a CC01 mancava per costruzione. +Certificato: barre flat 0,7-1,0% per dte<180g; convergenza a scadenza |ln(F/indice)| mediana 33-47 +bps; **cross-check indipendente col forward implicito nella catena opzioni: mediana −0,6 bps**. +Roll gestito **in spazio contratto** (nessuna serie incollata -> nessun salto sul giunto). +📌 **Il risultato: il premio a termine e' REALE ma e' il PREZZO DELLA COPERTURA, non un edge.** +Basis BTC mediana **+6,33%/anno**, contango **84,9% del tempo**, slope positiva 97,4%. Ma implicito +**+7,33%/anno** contro funding realizzato sulle finestre appaiate **+6,48%** -> premio incassabile +**+0,85%/anno, IC95 [−1,00, +2,92] che CONTIENE LO ZERO** (ETH +2,55%). A 60-100 giorni (il bucket +meno rumoroso) **+0,30% BTC / +1,70% ETH** contro **~1,4%/anno di costo d'esecuzione**. +**Quindi i futures datati NON sono un quarto lato della domanda funding: sono lo stesso lato quotato +diversamente**, e il filone funding resta chiuso su tre lati senza che ne esista un quarto. +📌 **E questo filone NON e' CC01, misurato:** `implausible_sharpe` **False** — barre in perdita +**49,9%**, maxDD 18,2% -> **la coda e' PRESENTE**. *Li' il rischio era fuori dal campione; qui e' +dentro e l'edge semplicemente non c'e'.* Distinzione che il gate del 26/07 e' stato scritto per fare. +- Cella scelta **in-sample-only**: Sharpe +0,13 / hold −2,44. Distribuzione dei 108 Sharpe: mediana + **−5,19**. Con 7,4 anni e vol 4,9% la **SE del rendimento annuo e' 1,79pp: sotto ~3,6pp/anno nulla + e' distinguibile da zero.** Null de-levering **REFUTED** (TP01 a k=0,05 fa DD 0,75% con Sharpe 1,31). +- **Test decisivo del filone:** ritardo d'esecuzione 1h/2h/24h -> Sharpe 0,13/0,13/0,10 -> **non e' + microstruttura da prezzo stantio; non c'e' edge da falsificare.** +- ⚠️ **3ª volta in questa ondata che l'eseguibilita' NON e' il vincolo:** lotti minimi $10 BTC + inverse / **$7,73 BTC USDC** / $1 ETH / $2,42 ETH USDC; servono 4,1x per gamba (~$148 di margine + iniziale a $600). +- ⚠️ **Limite dichiarato e NON appianato:** le due lenti sul premio (hold-to-expiry vs backtest + rollato) **discordano nel SEGNO** restando entro ~1,5 SE l'una dall'altra e dallo zero. *Non si + puo' stabilire il segno di una grandezza da ~1%/anno con 7 anni di dati a vol 5%* — ed e' proprio + questo che rende il filone non deployabile. +- **Riaprirebbe il filone:** il premio a 60-100 giorni **stabilmente sopra ~4%/anno** (2 SE sopra + zero E sopra il costo), cioe' un regime in cui basis e funding **si scollano** (marzo 2020, giugno + 2022 — ma per giorni, non per trimestri). +⚠️ **Tre errori propri catturati prima di pubblicare:** (a) il `volume` di +`get_tradingview_chart_data` e' in **valuta base, non USD** -> la prima stesura sbagliava la +liquidita' di ~5 ordini di grandezza; (b) il primo test d'ancora azzerava i *rendimenti* fuori +dall'ora scelta = **un'altra strategia, non un'ancora**; (c) **un gate che stampa `None` va +verificato prima di dichiararlo "non girato"** — stampava le chiavi sbagliate. +✅ Disciplina di rete rispettata: <=2 req/s e **sospensione automatica nella finestra :25-:29** di +`cron_chain` (verificato: si e' sospeso da solo per 264s). diff --git a/scripts/research/r0822_alt_options.py b/scripts/research/r0822_alt_options.py index 2cad5af..1a433e3 100644 --- a/scripts/research/r0822_alt_options.py +++ b/scripts/research/r0822_alt_options.py @@ -3,53 +3,57 @@ CONTESTO. Il filone VRP-QUOTE-VERE di questa stessa ondata ha trovato che `collect_chain.py` interroga `{"currency": "BTC"|"ETH"}`, che su Deribit restituisce le sole opzioni INVERSE (regolate in BTC/ETH). Il conto e' in USDC. Esiste accanto una famiglia USDC-LINEARE con lotti -~10x piu' piccoli, e su alcuni sottostanti (SOL, XRP, HYPE, AVAX) e' molto piu' popolata di -BTC_USDC. Il progetto non l'ha mai misurata. +molto piu' piccoli, e contando l'open interest sembra che su SOL/XRP/HYPE/AVAX sia molto piu' +popolata di BTC_USDC. Il progetto non l'ha mai misurata. DOMANDA. Esiste su questa famiglia una struttura a RISCHIO DEFINITO (put credit spread, la stessa -geometria di VRP01: vendi ~-0.28, compri ~-0.10) eseguibile con qualche centinaio di dollari, -con spread e liquidita' che non se la mangino? +geometria di VRP01: vendi ~-0.28, compri ~-0.10) eseguibile con qualche centinaio di dollari, con +spread e liquidita' che non se la mangino? -COSA MISURA, in ordine, e si FERMA appena un vincolo la uccide: - 1. i fatti del venue letti dall'API pubblica (min_trade_amount, tick_size, contract_size, - costo in $ di UN lotto), e la LIQUIDITA' vera: spread relativo (ask-bid)/mid ai delta che - servono davvero, non sull'ATM, + profondita' in cima al book; - 2. il collaterale: margine della gamba corta secondo la formula pubblicata dal venue, e - max-loss della struttura (i due numeri differiscono per un fatto — se il venue netta le - gambe — che l'API PUBBLICA NON dice: e' dichiarato come tale, non indovinato); - 3. quanta storia c'e' (risposta: nessuna) e quindi cosa si potrebbe al massimo concludere. +IPOTESI DICHIARATA PRIMA DI MISURARE: che il vincolo binding fosse la TAGLIA DEL LOTTO (e' cosi' +che il progetto ha sempre bocciato le opzioni: "BTC min 0.1 = $6.210/lotto -> FUORI a $3.000") e +che le famiglie alt, con lotti da poche decine di dollari, la superassero. Sbagliata due volte: +il lotto non e' il vincolo, e la classifica di liquidita' su cui poggia la domanda si ROVESCIA. -COSA NON PUO' FARE, e va detto prima dei numeri: **non esiste un backtest possibile**. Il nostro -collettore raccoglie solo le inverse BTC/ETH e l'archivio ereditato pure. Questo file misura le -condizioni di UN ISTANTE. Il massimo verdetto ottenibile e' `LEAD` = "vale la pena raccogliere il -dato", con il costo di raccolta stimato in chiamate/ora sul giro esistente. +COSA MISURA, in ordine, fermandosi appena un vincolo la uccide: + 0. l'UNITA' dei contratti — due test empirici, perche' leggerla male sposta ogni cifra in + dollari di un fattore 10-1000 (errore commesso nella prima stesura di questo file); + 1. i fatti del venue (min_trade_amount, tick_size, contract_size, $ di UN lotto); + 2. la liquidita' misurata in DUE modi che danno risposte opposte: open interest vs quote + effettivamente presenti. E' il risultato principale del file; + 3. la struttura: delta REALIZZATI, credito eseguibile vs credito al mark, spread, fee, max-loss; + 4. il collaterale e quante strutture entrano in $635; + 5. quanto costerebbe raccogliere il dato (perche' il verdetto massimo qui e' LEAD). + +COSA NON PUO' FARE, detto prima dei numeri: **non esiste un backtest possibile**. Il nostro +collettore raccoglie solo le inverse BTC/ETH e l'archivio ereditato pure: zero giorni di storia su +queste famiglie. Questo file misura UN ISTANTE. VINCOLI DI PROGETTO CITATI, non aggirati: (a) "niente short-vol da modello in deploy" (19/06). Qui i prezzi sono VERI, non modellati — che e' un'altra cosa — ma la regola resta finche' non esiste un campione che contenga uno STRESS. Un istante di calma non e' un campione. (b) SOL e' escluso dall'universo DIREZIONALE per decisione dell'operatore del 22/08. Quella - decisione riguarda TP01/SKH01 congelati come terza gamba di trend, e il suo stesso testo - dichiara che un meccanismo diverso la riapre. Un put credit spread non e' direzionale nel - senso di quella decisione. Ma il guardrail sul DATO vale lo stesso: nessun file nuovo in - `data/raw/`, e i prezzi del sottostante qui vengono da `underlying_price` della catena, - mai da `load_data`. - (c) il gate IV-rank>0.30 e' l'unico alpha misurato di VRP01, e su questi sottostanti NON e' + decisione riguarda TP01/SKH01 congelati come terza gamba di TREND, e il suo stesso testo + dichiara che un meccanismo diverso la riapre; un put credit spread non e' direzionale in + quel senso. Ma il guardrail sul DATO vale identico: nessun file nuovo in `data/raw/`, e i + prezzi del sottostante qui vengono da `underlying_price` della catena, mai da `load_data`. + (Nota: qui SOL non viene proposto — viene BOCCIATO, e sulla misura, non sulla decisione.) + (c) il gate IV-rank>0.30 e' l'unico alpha misurato di VRP01 e su questi sottostanti NON e' calcolabile: non abbiamo un giorno di storia di vol implicita. Senza quel gate la struttura non e' VRP01, e' vendere vol a caso. -CONVENZIONE DI UNITA' (la trappola di questo file): le opzioni INVERSE si quotano in CRIPTO per -contratto, le USDC-LINEARI in USDC per unita' di sottostante. Confrontare i due premi senza -convertire darebbe numeri sbagliati di un fattore ~10^4-10^5. Qui ogni importo in dollari passa -per `prem_usd()`, e le colonne unit-free (spread relativo, costo/credito) sono le uniche -direttamente confrontabili fra le due famiglie. +CONVENZIONE DI UNITA' (la trappola di questo file, verificata al §0): le opzioni INVERSE si +quotano in CRIPTO per contratto, le USDC-LINEARI in USDC per UNITA' DI SOTTOSTANTE; e +`min_trade_amount` e' in unita' di sottostante, NON in contratti — quindi non va moltiplicato per +`contract_size` (farlo gonfia XRP_USDC da $1.469 a $1.469.000 per lotto). -RETE: sole letture pubbliche, nessun ordine, pacing a ~2 richieste/s, e il file SI RIFIUTA di +RETE: sole letture pubbliche, nessun ordine, pacing ~2 richieste/s, e il file SI RIFIUTA di partire nella finestra :24-:29 (e' quando `cron_chain` usa lo stesso IP: il rate limit Deribit e' per-IP e il progetto ha gia' avuto un guasto per questo, 29/07). nice -n 19 timeout 900 uv run python scripts/research/r0822_alt_options.py - nice -n 19 timeout 900 uv run python scripts/research/r0822_alt_options.py --cache # riusa + nice -n 19 timeout 900 uv run python scripts/research/r0822_alt_options.py --cache """ from __future__ import annotations @@ -70,13 +74,15 @@ CACHE = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/altopt_cache.json") SHORT_DELTA, LONG_DELTA = -0.28, -0.10 # la geometria di VRP01, non ri-scelta qui -DTE_LO, DTE_HI = 4.0, 45.0 # dal settimanale di VRP01 (4-10) fino a ~1.5 mesi -OI_MIN = 100.0 # la stessa soglia di collect_chain.py +DTE_LO, DTE_HI = 4.0, 45.0 # dal settimanale di VRP01 (4-10) a ~1.5 mesi +OI_MIN = 100.0 # la soglia che usa collect_chain.py, qui sotto esame CAPITAL = 635.0 # il conto vero +RISK_BUDGET = 0.20 # quota di conto a rischio, per la capacita' TAKER_CAP = 0.125 # cap fee opzioni Deribit: 12.5% del premio -LINEAR = ("SOL_USDC", "XRP_USDC", "ETH_USDC", "HYPE_USDC", "AVAX_USDC", "BTC_USDC") +LINEAR = ("ETH_USDC", "BTC_USDC", "SOL_USDC", "XRP_USDC", "HYPE_USDC", "AVAX_USDC") INVERSE = ("BTC", "ETH") # termine di paragone: il progetto le conosce +FAMS = LINEAR + INVERSE # ---------------------------------------------------------------- rete (parca, sola lettura) @@ -132,11 +138,6 @@ def fetch_all(b: Budget) -> dict: {"currency": cur, "kind": "option", "expired": "false"}, b) or [] out[f"summ_{cur}"] = get("get_book_summary_by_currency", {"currency": cur, "kind": "option"}, b) or [] - if not out["inst_USDC"]: # il venue potrebbe non accettare currency=USDC - out["inst_USDC"] = get("get_instruments", - {"currency": "any", "kind": "option", "expired": "false"}, b) or [] - out["summ_USDC"] = get("get_book_summary_by_currency", - {"currency": "any", "kind": "option"}, b) or [] return out @@ -153,37 +154,40 @@ def put_delta(S: float, K: float, T: float, sig: float) -> float: # ---------------------------------------------------------------- unita' -def is_inverse(fam: str) -> bool: +def is_inv(fam: str) -> bool: return fam in INVERSE -def prem_usd(price: float, r: dict) -> float: - """Premio in DOLLARI di `min_trade_amount` contratti quotati a `price`. +def usd(price: float | None, r: dict) -> float: + """Prezzo di UN LOTTO MINIMO, in dollari. - Lineare: il prezzo e' gia' in USDC per unita' di sottostante. - Inverse: il prezzo e' in CRIPTO per contratto -> si moltiplica per il sottostante. + Lineare: il prezzo e' gia' in USDC per unita' di sottostante -> x min_trade_amount. + Inverse: il prezzo e' in CRIPTO per contratto -> x min_trade_amount x sottostante. + `contract_size` NON entra: `min_trade_amount` e' gia' in unita' di sottostante (§0). """ - k = r["csize"] * r["minamt"] - return price * k * (r["S"] if is_inverse(r["fam"]) else 1.0) + if price is None: + return float("nan") + return price * r["minamt"] * (r["S"] if is_inv(r["fam"]) else 1.0) def loss_usd(Ks: float, Kl: float, r: dict) -> float: - """Perdita massima strutturale in dollari, per `min_trade_amount` contratti. + """Perdita massima strutturale, per un lotto minimo. - Lineare: esatta, (Ks-Kl) x quantita'. Inverse: il payoff e' in cripta (max(0,K-S)/S), il - peggiore e' a S=Kl e vale (Ks-Kl)/Kl cripto = (Ks-Kl) dollari a quel prezzo -> stessa - formula, ma APPROSSIMATA (il valore in dollari della perdita dipende da dove finisce S). + Lineare: esatta, (Ks-Kl) x quantita'. Inverse: il payoff e' in cripto (max(0,K-S)/S), il caso + peggiore e' a S=Kl e vale (Ks-Kl)/Kl cripto = (Ks-Kl) dollari A QUEL PREZZO -> stessa formula, + ma APPROSSIMATA (il valore in dollari dipende da dove finisce S). Dichiarata, non nascosta. """ - return (Ks - Kl) * r["csize"] * r["minamt"] + return (Ks - Kl) * r["minamt"] # ---------------------------------------------------------------- margine (FONTE SECONDARIA) -def im_short_put_usd(S: float, K: float, mark_usd: float, qty: float) -> float: - """Initial margin di una short put, in USDC, secondo la formula PUBBLICATA da Deribit. +def im_short_put(S: float, K: float, mark_lot: float, r: dict) -> float: + """Initial margin di una short put, in USDC, per UN LOTTO, formula PUBBLICATA da Deribit: + IM = max(max(0.15 - OTM/S, 0.10), 0.075) x S + mark [per unita' di sottostante] - ⚠️ FONTE SECONDARIA, dichiarata: l'API PUBBLICA non espone il margine. Il numero che decide + ⚠️ FONTE SECONDARIA, dichiarata: l'API PUBBLICA non espone il margine. E il numero che decide davvero — se Deribit NETTA le due gambe di uno spread in margine standard — non e' leggibile da qui: si conferma con `private/get_margins`, che richiede una chiave (lettura, ma non pubblica) e quindi NON e' stato usato. @@ -191,11 +195,10 @@ def im_short_put_usd(S: float, K: float, mark_usd: float, qty: float) -> float: if not (S > 0 and K > 0): return float("nan") otm = max(0.0, S - K) / S # una put e' OTM quando K < S - per_contratto = max(max(0.15 - otm, 0.10) * S, 0.075 * S) + max(0.0, mark_usd / max(qty, 1e-12)) - return per_contratto * qty + return max(max(0.15 - otm, 0.10), 0.075) * S * r["minamt"] + max(0.0, mark_lot) -# ---------------------------------------------------------------- costruzione della vista +# ---------------------------------------------------------------- vista def build(raw: dict) -> list[dict]: @@ -205,8 +208,7 @@ def build(raw: dict) -> list[dict]: inst[i["instrument_name"]] = i for s in raw.get(f"summ_{cur}") or []: summ[s["instrument_name"]] = s - rows = [] - now_ms = datetime.now(UTC).timestamp() * 1000.0 + rows, now_ms = [], datetime.now(UTC).timestamp() * 1000.0 for name, i in inst.items(): s = summ.get(name) if s is None: @@ -217,21 +219,21 @@ def build(raw: dict) -> list[dict]: bid = float(bid) if bid else None # Deribit manda 0/None per "nessun lato" ask = float(ask) if ask else None iv, S = s.get("mark_iv"), s.get("underlying_price") - K = float(i.get("strike") or 0) + K, T = float(i.get("strike") or 0), max(dte, 0.0) / 365.25 typ = "P" if i.get("option_type") == "put" else "C" - T = max(dte, 0.0) / 365.25 - d = (put_delta(float(S), K, T, float(iv) / 100.0) - if (S and iv and K and T > 0 and typ == "P") else float("nan")) rows.append(dict( name=name, fam=fam, typ=typ, K=K, dte=dte, S=float(S) if S else float("nan"), - iv=float(iv) if iv else float("nan"), delta_bs=d, + iv=float(iv) if iv else float("nan"), + delta=(put_delta(float(S), K, T, float(iv) / 100.0) + if (S and iv and K and T > 0 and typ == "P") else float("nan")), bid=bid, ask=ask, mid=((bid + ask) / 2 if (bid and ask) else None), mark=float(s.get("mark_price") or 0.0), oi=float(s.get("open_interest") or 0.0), vol=float(s.get("volume") or 0.0), - tick=float(i.get("tick_size") or 0.0), steps=i.get("tick_size_steps") or [], - minamt=float(i.get("min_trade_amount") or 0.0), + vol_usd=float(s.get("volume_usd") or 0.0), + tick=float(i.get("tick_size") or 0.0), minamt=float(i.get("min_trade_amount") or 0.0), csize=float(i.get("contract_size") or 0.0), - taker=i.get("taker_commission"), settle=i.get("settlement_currency"), + blockmin=float(i.get("block_trade_min_trade_amount") or 0.0), + taker=float(i.get("taker_commission") or 0.0003), settle=i.get("settlement_currency"), )) return rows @@ -242,217 +244,310 @@ def rel_spread(r: dict) -> float: return (r["ask"] - r["bid"]) / r["mid"] -# ---------------------------------------------------------------- 1. i fatti del venue +# ---------------------------------------------------------------- §0 le unita' + + +def unita(rows: list[dict], b: Budget, rete: bool) -> None: + print("\n" + "=" * 104) + print("0. L'UNITA' DEI CONTRATTI — due test empirici, prima di qualunque cifra in dollari") + print("=" * 104) + print(" Test 1 (dai dati gia' scaricati): `volume_usd / (volume x mark_price)`.") + print(" = 1 se `volume` e' in UNITA' DI SOTTOSTANTE; = contract_size se e' in CONTRATTI;") + print(" = prezzo del sottostante se il mark e' in cripto (inverse).") + print(f"\n {'famiglia':<10} {'n oss':>6} {'vol_usd/(vol*mark)':>20} {'c_size':>8} " + f"{'sottost.':>10} {'lettura':>26}") + for f in FAMS: + rr = [r for r in rows if r["fam"] == f and r["vol"] > 0 and r["mark"] > 0] + if not rr: + continue + q = float(np.median([r["vol_usd"] / (r["vol"] * r["mark"]) for r in rr])) + cs, S = rr[0]["csize"], float(np.nanmedian([r["S"] for r in rr])) + if abs(q - 1.0) < 0.35: + lettura = "unita' di sottostante" + elif abs(q - cs) / max(cs, 1) < 0.35: + lettura = "CONTRATTI" + elif abs(q - S) / max(S, 1) < 0.35: + lettura = "cripto/contratto (inverse)" + else: + lettura = "?? non concludente" + print(f" {f:<10} {len(rr):>6} {q:>20.4f} {cs:>8g} {S:>10.2f} {lettura:>26}") + + if not rete: + print("\n Test 2 (size minima nel book): non girato (--cache non usa la rete).") + return + print("\n Test 2 (rete): la size MINIMA presente nel book. Se compare esattamente") + print(" `min_trade_amount`, allora e' quello il lotto minimo IN UNITA' DI SOTTOSTANTE.") + print(f"\n {'famiglia':<10} {'min_amt':>9} {'c_size':>8} {'min size vista':>15} " + f"{'libri':>6} {'verdetto':>12}") + for f in LINEAR: + cand = sorted([r for r in rows if r["fam"] == f and (r["bid"] or r["ask"])], + key=lambda r: -r["oi"])[:4] + sizes, nb = [], 0 + for r in cand: + ob = get("get_order_book", {"instrument_name": r["name"], "depth": 10}, b) + if not ob: + continue + nb += 1 + for side in ("bids", "asks"): + sizes += [float(l[1]) for l in (ob.get(side) or [])] + if not sizes: + continue + mn, ma, cs = min(sizes), cand[0]["minamt"], cand[0]["csize"] + v = "confermato" if abs(mn - ma) < 1e-9 else ("CONTRATTI" if abs(mn - ma * cs) < 1e-9 + else "non concl.") + print(f" {f:<10} {ma:>9g} {cs:>8g} {mn:>15g} {nb:>6} {v:>12}") + print("\n ⚠️ Errore commesso e corretto in sessione: la prima stesura moltiplicava") + print(" min_trade_amount x contract_size, e dava XRP_USDC a $1.468.900 di nozionale per") + print(" lotto. Un numero assurdo e' un test: nessun venue retail quota un minimo da $1,5M.") + + +# ---------------------------------------------------------------- §1 fatti del venue def famiglie(rows: list[dict]) -> dict: print("\n" + "=" * 104) - print("1a. I FATTI DEL VENUE — public/get_instruments + get_book_summary_by_currency") + print("1. I FATTI DEL VENUE — public/get_instruments + get_book_summary_by_currency") print("=" * 104) - print(f"{'famiglia':<10} {'strum':>6} {'OI>=100':>8} {'min_amt':>9} {'c_size':>7} " - f"{'tick':>9} {'sottost.$':>11} {'$/lotto':>10} {'taker':>8} {'settle':>7}") + print(f"{'famiglia':<10} {'strum':>6} {'min_amt':>9} {'c_size':>7} {'tick':>9} " + f"{'sottost.$':>11} {'$/lotto':>9} {'taker':>7} {'settle':>7}") tab = {} - for f in sorted({r["fam"] for r in rows}): + for f in FAMS: sub = [r for r in rows if r["fam"] == f] - liq = [r for r in sub if r["oi"] >= OI_MIN] + if not sub: + continue S = float(np.nanmedian([r["S"] for r in sub])) - mins = {r["minamt"] for r in sub} - cs = {r["csize"] for r in sub} a = sub[0] - lot = a["minamt"] * a["csize"] * S - tab[f] = dict(n=len(sub), nliq=len(liq), minamt=a["minamt"], csize=a["csize"], - tick=a["tick"], S=S, lot=lot, taker=a["taker"]) - if f in LINEAR or f in INVERSE: - avv = "" if (len(mins) == 1 and len(cs) == 1) else " <- min/c_size NON uniformi" - print(f"{f:<10} {len(sub):>6} {len(liq):>8} {a['minamt']:>9.4g} {a['csize']:>7.4g} " - f"{a['tick']:>9.5g} {S:>11.2f} {lot:>10.2f} {str(a['taker']):>8} " - f"{str(a['settle']):>7}{avv}") - print("\n `$/lotto` = min_trade_amount x contract_size x sottostante = il NOZIONALE di un") - print(" lotto (NON il premio, che e' molto minore). E' la granularita' minima imposta dal venue.") + tab[f] = dict(n=len(sub), minamt=a["minamt"], csize=a["csize"], tick=a["tick"], S=S, + lot=a["minamt"] * S, taker=a["taker"], + nliq=sum(1 for r in sub if r["oi"] >= OI_MIN)) + print(f"{f:<10} {len(sub):>6} {a['minamt']:>9.4g} {a['csize']:>7.4g} {a['tick']:>9.5g} " + f"{S:>11.2f} {tab[f]['lot']:>9.2f} {a['taker']:>7.4g} {str(a['settle']):>7}") + print("\n `$/lotto` = min_trade_amount x sottostante = il NOZIONALE di un lotto (NON il premio,") + print(" che e' molto minore). E' la granularita' minima imposta dal venue.") print(" `taker` per le opzioni Deribit e' una frazione del SOTTOSTANTE con cap al 12.5% del") - print(" premio: su un'ala molto OTM morde il CAP, non la percentuale.") + print(" premio: sull'ala molto OTM morde il CAP, non la percentuale.") + print(" 📌 Il lotto minimo NON e' il vincolo: da $242 (ETH_USDC) a $1.469 (XRP_USDC), contro") + print(" i $7.738 di BTC inverse su cui il progetto aveva costruito il muro delle opzioni.") return tab -def replica(tab: dict) -> None: - print("\n" + "-" * 104) - print("1b. CONTROLLO POSITIVO — riproduco i conteggi dichiarati dal coordinatore (22/08)?") - print("-" * 104) - atteso = {"BTC": (1038, 415), "ETH": (932, 548), "BTC_USDC": (686, 5), "ETH_USDC": (660, 119), - "SOL_USDC": (574, 341), "XRP_USDC": (522, 250), "HYPE_USDC": (396, 172), - "AVAX_USDC": (294, 146), "TRX_USDC": (310, 115)} - print(f"{'famiglia':<10} {'strum att':>10} {'strum oggi':>11} {'liq att':>8} {'liq oggi':>9}") - for f, (n_a, l_a) in atteso.items(): - t = tab.get(f) - if t is None: - print(f"{f:<10} {n_a:>10} {'ASSENTE':>11} {l_a:>8} {'-':>9}") - else: - print(f"{f:<10} {n_a:>10} {t['n']:>11} {l_a:>8} {t['nliq']:>9}") - print(" La catena si muove di ora in ora (scadenze che nascono, OI che cambia): scarti di") - print(" poche unita' sono la misura, non un disaccordo. Uno scarto GRANDE sarebbe un problema.") +# ---------------------------------------------------------------- §2 liquidita', due modi -# ---------------------------------------------------------------- 2. liquidita' ai delta utili +def liquidita(rows: list[dict], tab: dict) -> None: + print("\n" + "=" * 104) + print("2. LA LIQUIDITA', MISURATA IN DUE MODI CHE DANNO RISPOSTE OPPOSTE") + print("=" * 104) + print(" A) open interest >= 100 = quante POSIZIONI sono aperte (il filtro che usa") + print(" `collect_chain.py`, ereditato da bite, e la misura su cui poggia la domanda).") + print(" B) quote a DUE LATI presenti adesso = quanti strumenti sono NEGOZIABILI a un prezzo") + print(" noto. Sui soli put nella finestra 4-45 giorni, cioe' dove vive la struttura.") + print(f"\n{'famiglia':<10} {'A: OI>=100':>11} {'rango A':>8} {'put 4-45g':>10} " + f"{'B: 2 lati':>10} {'% quotati':>10} {'rango B':>8}") + stat = {} + for f in FAMS: + puts = [r for r in rows if r["fam"] == f and r["typ"] == "P" and DTE_LO <= r["dte"] <= DTE_HI] + two = [r for r in puts if r["bid"] is not None and r["ask"] is not None] + stat[f] = dict(oi=tab[f]["nliq"], puts=len(puts), two=len(two), + pct=(len(two) / len(puts) if puts else float("nan"))) + rA = {f: i + 1 for i, f in enumerate(sorted(stat, key=lambda x: -stat[x]["oi"]))} + rB = {f: i + 1 for i, f in enumerate(sorted(stat, key=lambda x: -stat[x]["pct"]))} + for f in sorted(FAMS, key=lambda x: -stat[x]["oi"]): + s = stat[f] + print(f"{f:<10} {s['oi']:>11} {rA[f]:>8} {s['puts']:>10} {s['two']:>10} " + f"{s['pct']:>9.0%} {rB[f]:>8}") + print("\n 📌 LA CLASSIFICA SI ROVESCIA. SOL_USDC e' 1° per open interest e ULTIMO per quote") + print(" presenti; BTC_USDC, dichiarato 'praticamente morto' (5 strumenti con OI>=100), ha") + print(" quasi tutti i put quotati a due lati. L'open interest misura posizioni ACCUMULATE,") + print(" non la possibilita' di negoziare ADESSO — e sull'universo lineare le due cose non") + print(" hanno quasi nulla a che vedere.") + print(" ⚠️ Ricaduta sul codice di produzione: `collect_chain.py` prefiltra a OI>=100. Sulle") + print(" inverse non fa danno (sono quotate comunque), ma NON e' un filtro di liquidita' e il") + print(" suo nome lo lascia credere. 4ª occorrenza di 'una riga presente non e' un dato") + print(" presente' — qui in versione: 'una posizione aperta non e' un prezzo negoziabile'.") -def gambe(rows: list[dict], fam: str) -> list[dict]: +# ---------------------------------------------------------------- §3 la struttura + + +def scegli(rows: list[dict], fam: str, eseguibile: bool) -> list[dict]: + """Per ogni scadenza: la put piu' vicina a -0.28 (corta) e a -0.10 (lunga). + + `eseguibile=False` -> fra TUTTI gli strike (la struttura che si vorrebbe). + `eseguibile=True` -> solo fra gli strike che hanno il LATO che serve: per APRIRE bisogna + vendere la corta al BID e comprare l'ala all'ASK. E' una restrizione + asimmetrica, ed e' quella giusta: un book a un lato solo non e' un + book vuoto, e' un book che serve solo una direzione. + """ puts = [r for r in rows if r["fam"] == fam and r["typ"] == "P" - and DTE_LO <= r["dte"] <= DTE_HI and np.isfinite(r["delta_bs"])] + and DTE_LO <= r["dte"] <= DTE_HI and np.isfinite(r["delta"])] out = [] for e in sorted({round(r["dte"], 4) for r in puts}): g = [r for r in puts if round(r["dte"], 4) == e] - s = min(g, key=lambda r: abs(r["delta_bs"] - SHORT_DELTA)) - lo = min(g, key=lambda r: abs(r["delta_bs"] - LONG_DELTA)) + gs = [r for r in g if r["bid"] is not None] if eseguibile else g + gl = [r for r in g if r["ask"] is not None] if eseguibile else g + if not gs or not gl: + continue + s = min(gs, key=lambda r: abs(r["delta"] - SHORT_DELTA)) + lo = min(gl, key=lambda r: abs(r["delta"] - LONG_DELTA)) if s["K"] > lo["K"]: - out.append({"dte": e, "short": s, "long": lo, "n_strike": len(g)}) + out.append({"dte": e, "short": s, "long": lo, "n": len(g)}) return out -def liquidita(rows: list[dict]) -> dict: +def fee_lotto(r: dict) -> float: + """Fee taker di UNA gamba, un lotto: min(0.03% del nozionale, 12.5% del premio).""" + return min(r["taker"] * r["S"] * r["minamt"], TAKER_CAP * usd(r["mark"], r)) + + +def struttura(rows: list[dict]) -> dict: print("\n" + "=" * 104) - print(f"2. LA LIQUIDITA' AI DELTA CHE SERVONO ({SHORT_DELTA} corta / {LONG_DELTA} lunga),") - print(f" NON sull'ATM. Finestra {DTE_LO:.0f}-{DTE_HI:.0f} giorni. Delta = BS su mark_iv, r=0.") + print(f"3. LA STRUTTURA — put credit spread {SHORT_DELTA}/{LONG_DELTA}, finestra " + f"{DTE_LO:.0f}-{DTE_HI:.0f} giorni, per UN LOTTO MINIMO") print("=" * 104) - print(f"{'famiglia':<10} {'scad':>5} {'2lati':>6} {'d corta':>8} {'d lunga':>8} " - f"{'spr% corta':>11} {'spr% lunga':>11} {'credito$':>9} {'costo/cred':>11} " + print(" `f_venue` = (bid_corta - ask_ala) / (mark_corta - mark_ala) = quanta parte del") + print(" credito che il venue stesso considera equo resta dopo aver attraversato lo spread.") + print(" E' il gemello del `f` misurato dal progetto il 30/07 (0,73 = errore del MODELLO):") + print(" qui il modello non c'entra, il riferimento e' il mark del venue.") + print(f"\n{'famiglia':<10} {'scad':>4} {'d corta':>8} {'d ala':>7} {'spr% corta':>11} " + f"{'spr% ala':>9} {'cred mark$':>11} {'cred eseg$':>11} {'f_venue':>8} " f"{'fee/cred':>9} {'maxloss$':>9}") res = {} - for fam in list(LINEAR) + list(INVERSE): - gg = gambe(rows, fam) + for f in FAMS: + gg = scegli(rows, f, eseguibile=True) if not gg: - print(f"{fam:<10} {'0':>5} nessuna scadenza utilizzabile nella finestra") + print(f"{f:<10} {'0':>4} nessuna scadenza con corta-bid E ala-ask presenti") continue - ss, ll, ds, dl, cred, cost, fee, ml = [], [], [], [], [], [], [], [] - due = neg = 0 + acc = {k: [] for k in ("ds", "dl", "ss", "sl", "cm", "ce", "fee", "ml")} for g in gg: s, lo = g["short"], g["long"] - rs, rl = rel_spread(s), rel_spread(lo) - if not (np.isfinite(rs) and np.isfinite(rl)): + cm = usd(s["mark"], s) - usd(lo["mark"], lo) + ce = usd(s["bid"], s) - usd(lo["ask"], lo) + if cm <= 0: continue - due += 1 - c_mid = prem_usd(s["mid"], s) - prem_usd(lo["mid"], lo) - c_exe = prem_usd(s["bid"], s) - prem_usd(lo["ask"], lo) - if c_mid <= 0: - neg += 1 - continue - # fee taker Deribit: min(0.03% del sottostante, 12.5% del premio) per gamba, x2 gambe, - # x2 (apertura + chiusura/esercizio). Conservativo ma e' il listino. - def _fee(r: dict) -> float: - notion = r["S"] * r["csize"] * r["minamt"] - return min(float(r["taker"] or 0.0003) * notion, TAKER_CAP * prem_usd(r["mid"], r)) - ss.append(rs); ll.append(rl) - ds.append(s["delta_bs"]); dl.append(lo["delta_bs"]) - cred.append(c_mid) - cost.append((c_mid - c_exe) / c_mid) - fee.append(2.0 * (_fee(s) + _fee(lo)) / c_mid) - ml.append(loss_usd(s["K"], lo["K"], s) - c_exe) - if not cred: - print(f"{fam:<10} {len(gg):>5} {due:>6} nessuna struttura con credito positivo a mid " - f"({neg} a credito<=0)") + acc["ds"].append(s["delta"]); acc["dl"].append(lo["delta"]) + acc["ss"].append(rel_spread(s)); acc["sl"].append(rel_spread(lo)) + acc["cm"].append(cm); acc["ce"].append(ce) + acc["fee"].append(2.0 * (fee_lotto(s) + fee_lotto(lo)) / cm) + acc["ml"].append(loss_usd(s["K"], lo["K"], s) - ce) + if not acc["cm"]: + print(f"{f:<10} {len(gg):>4} nessuna struttura con credito positivo al mark") continue - r = dict(n=len(gg), due=due, neg=neg, s=float(np.median(ss)), l=float(np.median(ll)), - ds=float(np.median(ds)), dl=float(np.median(dl)), cred=float(np.median(cred)), - cost=float(np.median(cost)), fee=float(np.median(fee)), ml=float(np.median(ml))) - res[fam] = r - print(f"{fam:<10} {r['n']:>5} {due:>6} {r['ds']:>8.3f} {r['dl']:>8.3f} " - f"{r['s']*100:>10.1f}% {r['l']*100:>10.1f}% {r['cred']:>9.2f} " - f"{r['cost']*100:>10.1f}% {r['fee']*100:>8.1f}% {r['ml']:>9.2f}") - print("\n `2lati` = strutture in cui ENTRAMBE le gambe hanno bid E ask. Una riga presente non") - print(" e' un dato presente: una gamba con un solo lato non e' negoziabile a un prezzo noto.") - print(" `d corta`/`d lunga` = delta REALIZZATI (non i target): se la griglia degli strike e'") - print(" grossolana la struttura che compreresti non e' quella che hai chiesto.") - print(" `costo/cred` = quanto dell'incasso a mid resta sul tavolo attraversando lo spread su") - print(" ENTRAMBE le gambe. Riferimento del progetto su BTC/ETH inverse: ~10% del credito.") - print(" `fee/cred` = listino taker Deribit, 2 gambe x apertura+chiusura, col cap 12.5%.") - print(" `credito$` e `maxloss$` sono per UN LOTTO MINIMO.") + m = {k: float(np.nanmedian(v)) if v else float("nan") for k, v in acc.items()} + m["fv"] = float(np.median([e / c for e, c in zip(acc["ce"], acc["cm"])])) + m["n"] = len(acc["cm"]) + res[f] = m + print(f"{f:<10} {m['n']:>4} {m['ds']:>8.3f} {m['dl']:>7.3f} {m['ss']*100:>10.1f}% " + f"{m['sl']*100:>8.1f}% {m['cm']:>11.2f} {m['ce']:>11.2f} {m['fv']:>8.3f} " + f"{m['fee']*100:>8.1f}% {m['ml']:>9.2f}") + print("\n ⚠️ `spr%` puo' essere NaN quando la gamba ha un lato solo: e' negoziabile per") + print(" aprire (il lato che serve c'e') ma non ha un mid, quindi non ha uno spread definito.") + print(" `fee/cred` = 2 gambe x (apertura + chiusura) col cap 12.5%. VRP01 tiene FINO A") + print(" SCADENZA (nessuna gestione infra-settimana, il profit-take e' REFUTED il 30/07),") + print(" quindi il costo realistico e' ~la META' di questa colonna + la delivery fee.") + + print(f"\n --- controllo: gli STESSI strike scelti fra TUTTI gli strike (non solo quotati) ---") + print(f" {'famiglia':<10} {'ideale d corta':>15} {'eseg. d corta':>14} {'ideale d ala':>13} " + f"{'eseg. d ala':>12} {'scad ideali':>12} {'scad eseg.':>11}") + for f in FAMS: + gi, ge = scegli(rows, f, False), scegli(rows, f, True) + if not gi: + continue + di = float(np.median([g["short"]["delta"] for g in gi])) + li = float(np.median([g["long"]["delta"] for g in gi])) + de = float(np.median([g["short"]["delta"] for g in ge])) if ge else float("nan") + le = float(np.median([g["long"]["delta"] for g in ge])) if ge else float("nan") + print(f" {f:<10} {di:>15.3f} {de:>14.3f} {li:>13.3f} {le:>12.3f} " + f"{len(gi):>12} {len(ge):>11}") + print(" Se la colonna `eseg.` si allontana dal target -0.28/-0.10, la struttura che si") + print(" comprerebbe NON e' quella richiesta: la griglia degli strike quotati la deforma.") return res def profondita(rows: list[dict], fams: list[str], b: Budget) -> None: print("\n" + "=" * 104) - print("2b. PROFONDITA' IN CIMA AL BOOK (public/get_order_book depth=5), sulle gambe scelte") + print("3b. PROFONDITA' IN CIMA AL BOOK (public/get_order_book depth=5) sulle gambe scelte") print("=" * 104) print(f"{'famiglia':<10} {'gamba':<6} {'n':>3} {'lotti al best':>14} {'$ premio al best':>17} " - f"{'ask/tick lunga':>15} {'max|delta venue-BS|':>21}") - for fam in fams: - gg = gambe(rows, fam)[:4] # max 4 scadenze/famiglia: budget di rete - for lato, key in (("corta", "short"), ("lunga", "long")): - lots, prem, dd, tk = [], [], [], [] + f"{'prezzo/tick':>12} {'max|d venue-BS|':>17}") + for f in fams: + gg = scegli(rows, f, eseguibile=True)[:4] # max 4 scadenze: budget di rete + for lato, key, side in (("corta", "short", "bids"), ("ala", "long", "asks")): + lots, prem, tk, dd = [], [], [], [] for g in gg: r = g[key] ob = get("get_order_book", {"instrument_name": r["name"], "depth": 5}, b) - if not ob: + if not ob or not (ob.get(side) or []): continue - side = ob.get("bids") if key == "short" else ob.get("asks") - if not side: - continue - px, sz = float(side[0][0]), float(side[0][1]) + px, sz = float(ob[side][0][0]), float(ob[side][0][1]) lots.append(sz / r["minamt"] if r["minamt"] else float("nan")) - prem.append(prem_usd(px, r) * (sz / max(r["minamt"], 1e-12))) + prem.append(usd(px, r) * (sz / max(r["minamt"], 1e-12))) if r["tick"]: tk.append(px / r["tick"]) gv = (ob.get("greeks") or {}).get("delta") - if gv is not None and np.isfinite(r["delta_bs"]): - dd.append(abs(float(gv) - r["delta_bs"])) + if gv is not None and np.isfinite(r["delta"]): + dd.append(abs(float(gv) - r["delta"])) if not lots: - print(f"{fam:<10} {lato:<6} {'0':>3} book vuoto su tutte le scadenze provate") + print(f"{f:<10} {lato:<6} {'0':>3} book vuoto sul lato che serve") continue - print(f"{fam:<10} {lato:<6} {len(lots):>3} {np.median(lots):>14.1f} " - f"{np.median(prem):>17.2f} " - f"{(np.median(tk) if tk else float('nan')):>15.1f} " - f"{(max(dd) if dd else float('nan')):>21.4f}") + print(f"{f:<10} {lato:<6} {len(lots):>3} {np.median(lots):>14.1f} " + f"{np.median(prem):>17.2f} {(np.median(tk) if tk else float('nan')):>12.1f} " + f"{(max(dd) if dd else float('nan')):>17.4f}") print("\n `lotti al best` = size in cima al book / lotto minimo: quante strutture minime") - print(" entrano senza muovere il prezzo. `ask/tick` sulla gamba LUNGA e' il controllo") - print(" d'artefatto del 30/07: un'ala a 1-2 tick e' un prezzo di griglia, non un prezzo.") - print(" `max|delta venue-BS|` valida il MIO calcolo del delta contro le greche del venue: se") - print(" non e' piccolo, tutta la selezione degli strike qui sopra e' sbagliata.") + print(" entrano senza muovere il prezzo. `prezzo/tick` e' il controllo d'artefatto del") + print(" 30/07: un prezzo a 1-2 tick e' una griglia, non un prezzo.") + print(" `max|d venue-BS|` valida il MIO delta contro le greche del venue: se non e' piccolo,") + print(" tutta la selezione degli strike qui sopra e' sbagliata.") -# ---------------------------------------------------------------- 3. collaterale +# ---------------------------------------------------------------- §4 collaterale e capacita' def collaterale(rows: list[dict], res: dict) -> None: print("\n" + "=" * 104) - print("3. IL COLLATERALE — quanto conto serve per UNA struttura minima (conto reale $635)") + print(f"4. COLLATERALE E CAPACITA' — conto reale ${CAPITAL:.0f}") print("=" * 104) - print(f"{'famiglia':<10} {'IM corta+ala $':>15} {'max-loss $':>11} {'IM/conto':>9} " - f"{'strutture @635 (IM)':>20} {'(max-loss)':>12}") - for fam in res: - gg = gambe(rows, fam) - ims, mls = [], [] + print(f"{'famiglia':<10} {'IM corta+ala$':>14} {'max-loss$':>10} {'n @IM':>7} " + f"{'n @maxloss':>11} {'n a budget 20%':>15} {'credito lordo$':>15}") + for f, m in res.items(): + gg = scegli(rows, f, eseguibile=True) + ims = [] for g in gg: s, lo = g["short"], g["long"] - if s["mid"] is None or lo["ask"] is None: + if lo["ask"] is None: continue - qty = s["csize"] * s["minamt"] - im = im_short_put_usd(s["S"], s["K"], prem_usd(s["mark"], s), qty) - ims.append(im + prem_usd(lo["ask"], lo)) # l'ala si PAGA: e' cassa, non margine - mls.append(loss_usd(s["K"], lo["K"], s)) + ims.append(im_short_put(s["S"], s["K"], usd(s["mark"], s), s) + usd(lo["ask"], lo)) if not ims: continue - im, ml = float(np.median(ims)), float(np.median(mls)) - print(f"{fam:<10} {im:>15.2f} {ml:>11.2f} {im/CAPITAL:>8.1%} " - f"{CAPITAL/im:>20.1f} {CAPITAL/ml:>12.1f}") + im, ml = float(np.median(ims)), m["ml"] + n_budget = math.floor(RISK_BUDGET * CAPITAL / ml) if ml > 0 else 0 + print(f"{f:<10} {im:>14.2f} {ml:>10.2f} {CAPITAL/im:>7.1f} {CAPITAL/ml:>11.1f} " + f"{n_budget:>15} {n_budget*m['ce']:>15.2f}") print("\n `IM corta+ala` = margine iniziale della sola put venduta (formula PUBBLICATA dal") - print(" venue, FONTE SECONDARIA) + il premio dell'ala, che si paga in contanti. E' il") - print(" fabbisogno se il venue NON netta le gambe.") - print(" `max-loss` = perdita massima strutturale = il fabbisogno se il venue LE NETTA.") - print(" ⚠️ La differenza fra i due numeri e' l'unica cosa importante di questa tabella e NON") - print(" E' LEGGIBILE dall'API pubblica: si chiude con `private/get_margins` (lettura, ma") - print(" autenticata -> fuori dal perimetro dichiarato di questo file).") + print(" venue, FONTE SECONDARIA) + il premio dell'ala, che si paga in contanti = il") + print(" fabbisogno se il venue NON netta le gambe. `max-loss` = il fabbisogno se LE NETTA.") + print(" ⚠️ Quale dei due valga NON e' leggibile dall'API pubblica: si chiude con") + print(" `private/get_margins` (lettura autenticata, fuori dal perimetro di questo file).") + print(" Ma la conclusione NON dipende da quale sia: il collaterale non e' il vincolo in") + print(" nessuna delle due letture.") + print(f" `n a budget 20%` = strutture che stanno in {RISK_BUDGET:.0%} del conto a max-loss;") + print(" `credito lordo` = il loro credito ESEGUIBILE sommato, per un ciclo. E' un incasso") + print(" LORDO PRIMA DI QUALUNQUE PERDITA, non un rendimento atteso: la struttura perde fino") + print(" a ~7x il credito quando va male (misura del progetto sul canonico BTC/ETH).") -def costo_raccolta(tab: dict) -> None: +def costo_raccolta(rows: list[dict], tab: dict) -> None: print("\n" + "=" * 104) - print("4. COSTO DI RACCOLTA — cosa costerebbe aggiungere una famiglia al giro esistente") + print("5. COSTO DI RACCOLTA — cosa costerebbe aggiungere una famiglia al giro esistente") print("=" * 104) print(" `collect_chain.py` oggi: ~650 chiamate in ~160s a 4 rps (BTC+ETH inverse), al :25.") - print(f"{'famiglia':<10} {'chiamate/giro':>14} {'+s a 4 rps':>11} {'+% sul giro':>12}") + print(f"{'famiglia':<10} {'a OI>=100':>10} {'a 2-lati':>9} {'+s a 4rps (2-lati)':>19} " + f"{'+% sul giro':>12}") for f in LINEAR: - t = tab.get(f) - if t: - n = t["nliq"] + 2 # + get_instruments + get_book_summary - print(f"{f:<10} {n:>14.0f} {n/4.0:>11.1f} {n/650*100:>11.0f}%") + sub = [r for r in rows if r["fam"] == f] + two = sum(1 for r in sub if r["bid"] is not None and r["ask"] is not None) + print(f"{f:<10} {tab[f]['nliq']:>10} {two:>9} {(two+2)/4.0:>19.1f} " + f"{(two+2)/650*100:>11.0f}%") print("\n ⚠️ Il vincolo non e' la CPU ma il rate limit Deribit PER-IP, gia' costato un guasto") print(" il 29/07 (fallback silenzioso del feed 5m di SKH01). Ogni famiglia aggiunta allunga") print(" la finestra in cui il giro occupa l'IP.") @@ -471,7 +566,7 @@ def main() -> int: print("=" * 104) print("Sola lettura pubblica, nessun ordine. UNO SNAPSHOT: qui non esiste un backtest.") - b = Budget() + b, rete = Budget(), not a.cache if a.cache and CACHE.exists(): raw = json.loads(CACHE.read_text()) print(f"\n[cache] {CACHE}") @@ -481,8 +576,7 @@ def main() -> int: raw = fetch_all(b) CACHE.parent.mkdir(parents=True, exist_ok=True) CACHE.write_text(json.dumps(raw)) - print(f"\n[rete] {b.calls} chiamate, {b.err} errori, {b.r429} risposte 429, " - f"{time.time()-t0:.0f}s") + print(f"\n[rete] {b.calls} chiamate, {b.err} errori, {b.r429} 429, {time.time()-t0:.0f}s") rows = build(raw) print(f"[dato] {len(rows)} strumenti con specifiche E quote — snapshot " @@ -491,38 +585,38 @@ def main() -> int: print("NESSUN DATO: il venue non ha risposto. Verdetto: non misurabile.") return 1 + unita(rows, b, rete) tab = famiglie(rows) - replica(tab) - res = liquidita(rows) + liquidita(rows, tab) + res = struttura(rows) - vive = [f for f in res if res[f]["due"] >= 2] - if a.cache: - print("\n2b. PROFONDITA': non girata (--cache non usa la rete).") - elif not vive: - print("\n2b. PROFONDITA': non girata — nessuna famiglia con >=2 strutture a due lati.") - else: + vive = [f for f in res if res[f]["n"] >= 2] + if rete and vive: guardia_finestra() profondita(rows, vive, b) + else: + print("\n3b. PROFONDITA': non girata (--cache, oppure nessuna famiglia con >=2 strutture).") collaterale(rows, res) - costo_raccolta(tab) + costo_raccolta(rows, tab) print("\n" + "=" * 104) - print("5. COSA QUESTO FILE NON PUO' DIRE (dichiarato prima di qualunque verdetto)") + print("6. COSA QUESTO FILE NON PUO' DIRE (dichiarato prima di qualunque verdetto)") print("=" * 104) - print(" * Non c'e' STORIA: il collettore raccoglie BTC/ETH inverse, l'archivio ereditato pure.") - print(" Zero giorni di catena su SOL/XRP/HYPE/AVAX/*_USDC -> nessun backtest, nessun") - print(" hold-out, nessun deflated-Sharpe. Il massimo verdetto ottenibile e' LEAD.") + print(" * Non c'e' STORIA: il collettore raccoglie BTC/ETH inverse, l'archivio ereditato") + print(" pure. Zero giorni di catena su queste famiglie -> nessun backtest, nessun hold-out,") + print(" nessun deflated-Sharpe. Il verdetto massimo ottenibile e' LEAD.") print(" * Non c'e' IV-RANK: il gate>0.30 e' l'unico alpha misurato di VRP01 e richiede la") print(" distribuzione storica della vol implicita del sottostante. Senza, la struttura non") print(" e' VRP01: e' vendere vol a caso. (Su BTC/ETH quel gate e' passato 0/19 settimane.)") print(" * Non c'e' STRESS nel campione: la regola 'niente short-vol da modello in deploy' e'") print(" del 19/06 e la condizione dichiarata per rivalutarla e' un crash CATTURATO.") - print(" * Il margine e' da FORMULA PUBBLICATA, non dall'API: il netting delle gambe non e'") + print(" * Il margine e' da FORMULA PUBBLICATA, non dall'API, e il netting delle gambe non e'") print(" leggibile pubblicamente.") - print(" * E' UN ISTANTE: uno spread relativo mediano misurato a un'ora del sabato non e' la") - print(" sua distribuzione. Servirebbe almeno un ciclo settimanale per sapere se e' tipico.") - print(f"[rete, totale] {b.calls} chiamate, {b.err} errori, {b.r429} risposte 429") + print(" * E' UN ISTANTE, per giunta di SABATO pomeriggio: uno spread relativo mediano a") + print(" un'ora del fine settimana non e' la sua distribuzione. Un book alt puo' essere") + print(" molto piu' quotato in settimana. Servirebbe almeno un ciclo settimanale.") + print(f"\n[rete, totale] {b.calls} chiamate, {b.err} errori, {b.r429} risposte 429") return 0 diff --git a/scripts/research/r0822_basis_calendar.py b/scripts/research/r0822_basis_calendar.py index 22d2ddc..cd7227a 100644 --- a/scripts/research/r0822_basis_calendar.py +++ b/scripts/research/r0822_basis_calendar.py @@ -512,6 +512,16 @@ def basis_vs_funding(asset: str, cv: pd.DataFrame) -> None: f" (b~1 e a~0 = previsore non distorto => niente da incassare)") print(f" PREMIO INCASSABILE = implicito - realizzato = {np.mean(prem)*100:+6.2f}%/anno" f" IC95% a blocchi [{lo*100:+.2f}%, {hi*100:+.2f}%] (n={m.sum():,} ore)") + # ATTENZIONE: annualizzare dividendo per tau ESPLODE il rumore a tau piccolo + # (5 bps di rumore a 7 giorni dalla scadenza diventano +2.6%/anno). Media per bucket: + tt = tau[m] / 24.0 + print(" premio per giorni-a-scadenza (l'annualizzazione a tau piccolo amplifica il rumore):") + for lo_d, hi_d, lab in [(5, 30, " 5-30g"), (30, 60, " 30-60g"), (60, 100, "60-100g")]: + k = (tt >= lo_d) & (tt < hi_d) + if k.sum() > 500: + print(f" {lab}: implicito {np.mean(x[k])*100:+6.2f}% realizzato " + f"{np.mean(y[k])*100:+6.2f}% premio {np.mean(prem[k])*100:+6.2f}%/anno " + f"(sd {np.std(prem[k])*100:5.1f}, n={k.sum():,})") # ========================================================================== diff --git a/scripts/research/r0822_monitor_audit.py b/scripts/research/r0822_monitor_audit.py new file mode 100644 index 0000000..a73f1c3 --- /dev/null +++ b/scripts/research/r0822_monitor_audit.py @@ -0,0 +1,662 @@ +"""MONITOR-AUDIT — quanti dei sei forward-monitor registrano la giornata che credono di registrare? + +PERCHE'. Il filone XSR-REPRO (22/08) ha trovato che `paper_xsr.advance()` consuma la barra 1d +del giorno IN CORSO — `fetch_hyperliquid` gira nel cron delle 00:30 con END = oggi e scrive una +barra parziale — e poi porta `last_ts` su quella barra: le 23 ore e mezza restanti di ogni giorno +non entrano in nessun rendimento registrato. Misura: 1/28 barre identiche al replay, corr -0,045, +vol registrata 0,46% contro 2,74% ricalcolata. + +Il difetto non e' di XSR01: e' della FORMA `new = [i for i in range(len(ts)) if ts[i] > last_ts]` +applicata a un pannello la cui ultima riga e' provvisoria. Quella forma e' copiata in sei file. +Tre decisioni PRE-REGISTRATE si leggono su queste serie: + STATARB 2026-09-27 (r0724_statarb_deploy_gate: Sharpe di net_modeled >= 0.35) + XSR01 2026-10-23 (r0725_xsr_deploy_gate: Sharpe >= 1.0 E haircut $5.000 <= 40%) + DVOLSPREAD 2026-10-24 kill / 2027-01-24 decisione (soglie dentro paper_dvolspread) +e `monitor_health` dichiara **OK** tutti e sei, perche' misura freschezza e buchi: una serie +fresca, completa e SBAGLIATA passa ogni controllo di freschezza. + +QUESTO SCRIPT E' UN AUDIT, NON UNA RIPARAZIONE. E' in SOLA LETTURA su tutto cio' che e' +produzione: importa i moduli di `scripts/live/` senza chiamarne `main()`, e prima di rieseguirne +`advance()` sostituisce `_append` con una funzione che raccoglie in memoria — nessun file di +`data/` viene aperto in scrittura. La riparazione e' una decisione dell'operatore e ha un costo +(azzerare una finestra forward): la sezione E lo quantifica. + +METODO — per ogni monitor tre domande, ognuna con una PROVA e non con la lettura di un docstring +(il progetto ha gia' trovato QUATTRO docstring di produzione che dichiaravano una causalita' che +il codice non aveva: `_skyhook_positions`, `resample_5m`, `current_target` di TP01, e +`prevday_breakout.current_target` qui sotto): + + A. DA DOVE VIENE L'ULTIMA BARRA. Si misura la COMPLETEZZA dell'ultima riga di ogni sorgente + con un contatore indipendente dal prezzo: quante barre 5m stanno dentro l'ultima barra 1h, + quante barre 1h dentro l'ultimo giorno, quanto volume ha l'ultima barra HL rispetto alla + mediana, quanti giorni di ritardo ha l'ultima barra IB. + B. IL DIFETTO MORDE? Replay della strategia CONGELATA sui dati di oggi dall'inception, contro + cio' che il monitor ha registrato giorno per giorno, appaiato per timestamp. Non "legge una + barra parziale" ma **quanta parte della giornata finisce nella serie registrata**: barre + identiche, correlazione, rapporto di varianza -> minuti al giorno effettivamente misurati. + C. QUALE GATE NE DIPENDE e se il criterio resta leggibile. + +Poi: + D. LA GUARDIA CHE MANCA a `monitor_health`, con soglia dichiarata e controllo positivo nei due + versi (un rilevatore tarato per non segnalare e' indistinguibile da uno rotto). + E. IL COSTO DELLA RIPARAZIONE per gate: giorni di finestra persi, data in cui il contatore + nuovo tornerebbe alla stessa numerosita', e se esiste una RICOSTRUZIONE a posteriori. + +COSA MI ASPETTAVO PRIMA DI MISURARE: che i tre monitor giornalieri su feed crypto (xsr, statarb, +dvolspread) avessero la stessa patologia identica; che `paper_prevday`, essendo su griglia 1h con +24 barre al giorno, ne avesse una MOLTO piu' mite (una barra su 24 troncata); e che +`paper_combo`, che vive sul calendario di borsa, fosse o rotto come gli altri o sano per caso. +Solo la terza aspettativa e' stata una sorpresa. + + nice -n 19 timeout 900 uv run python scripts/research/r0822_monitor_audit.py +""" +from __future__ import annotations + +import csv +import importlib.util +import json +import sys +from datetime import datetime, timezone +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +RAW = ROOT / "data" / "raw" +for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"): + sys.path.insert(0, str(_p)) + +ANN = np.sqrt(365.0) +NOW = datetime.now(timezone.utc) + +# -------------------------------------------------------------------------------------------- +# Caricamento SOLA LETTURA di un modulo di produzione: si esegue il modulo (definizioni), mai +# `main()`. `_append` viene sostituito subito dopo, prima di qualunque chiamata ad `advance`. +# -------------------------------------------------------------------------------------------- +_MODS: dict[str, object] = {} + + +def live(name: str): + if name in _MODS: + return _MODS[name] + spec = importlib.util.spec_from_file_location(f"_ro_{name}", ROOT / "scripts" / "live" / f"{name}.py") + m = importlib.util.module_from_spec(spec) + spec.loader.exec_module(m) + _MODS[name] = m + return m + + +class Sink: + """Sostituisce `_append` dei monitor: raccoglie in memoria invece di scrivere su disco. + E' l'unica ragione per cui questo audit puo' rieseguire il codice di produzione TALE E QUALE + (nessuna reimplementazione -> nessun drift fra audit e monitor) restando in sola lettura.""" + + def __init__(self): + self.rows: list[tuple[str, dict]] = [] + + def __call__(self, path, rec): + self.rows.append((Path(path).name, dict(rec))) + + def of(self, fname: str) -> list[dict]: + return [r for n, r in self.rows if n == fname] + + +def read_jsonl(p: Path) -> list[dict]: + if not p.exists(): + return [] + return [json.loads(x) for x in p.read_text().splitlines() if x.strip()] + + +def read_equity_csv(p: Path, col: int = 1) -> tuple[list[pd.Timestamp], np.ndarray]: + """(date, equity) da un equity.csv dei paper_portfolio / paper_combo.""" + dts, eq = [], [] + with p.open() as fh: + for row in csv.reader(fh): + if not row or row[0] == "date": + continue + try: + v = float(row[col]) + except (ValueError, IndexError): + continue + dts.append(pd.Timestamp(row[0])) + eq.append(v) + return dts, np.asarray(eq, float) + + +# -------------------------------------------------------------------------------------------- +# Statistica del confronto registrato-vs-replay. La domanda non e' "coincide?" ma "quanta parte +# della giornata e' finita nella serie": in radice di tempo il rapporto di VARIANZA fra le due +# serie e' la frazione di giornata misurata (ipotesi: random walk sull'intervallo troncato). +# -------------------------------------------------------------------------------------------- +def compare(rec: np.ndarray, rep: np.ndarray) -> dict: + ok = np.isfinite(rec) & np.isfinite(rep) + a, b = rec[ok], rep[ok] + n = len(a) + if n < 3: + return dict(n=n, ident=0, corr=float("nan"), vr=float("nan"), minuti=float("nan"), + sd_rec=float("nan"), sd_rep=float("nan"), sh_rec=float("nan"), sh_rep=float("nan")) + ident = int((np.abs(a - b) <= 1e-6).sum()) + corr = float(np.corrcoef(a, b)[0, 1]) if a.std() > 0 and b.std() > 0 else float("nan") + vr = float((a.std() / b.std()) ** 2) if b.std() > 0 else float("nan") + return dict(n=n, ident=ident, corr=corr, vr=vr, minuti=vr * 1440.0, + sd_rec=float(a.std() * ANN * 100), sd_rep=float(b.std() * ANN * 100), + sh_rec=float(a.mean() / a.std() * ANN) if a.std() > 0 else float("nan"), + sh_rep=float(b.mean() / b.std() * ANN) if b.std() > 0 else float("nan")) + + +def show(tag: str, c: dict, note: str = "") -> None: + print(f" barre appaiate {c['n']} identiche entro 1e-6: {c['ident']}/{c['n']} " + f"corr {c['corr']:+.4f}") + print(f" vol annualizzata REGISTRATA {c['sd_rec']:6.2f}% RICALCOLATA {c['sd_rep']:6.2f}%" + f" rapporto di varianza {c['vr']:.4f}") + print(f" -> il monitor misura ~{c['minuti']:,.0f} minuti di mercato al giorno (su 1440){note}") + print(f" Sharpe della finestra: REGISTRATO {c['sh_rec']:+.2f} RICALCOLATO {c['sh_rep']:+.2f}" + f" [SE ~{np.sqrt(365.0 / max(c['n'], 1)):.2f}: NON e' una lettura di gate]") + + +RES: dict[str, dict] = {} # riepilogo per la tabella finale + + +# ============================================================================================ +# A — PROVENIENZA: quanto e' completa l'ultima riga di ogni sorgente +# ============================================================================================ +def sezione_A() -> dict: + print("=" * 102) + print(" A — DA DOVE VIENE L'ULTIMA BARRA (completezza misurata sul disco, non dedotta)") + print("=" * 102) + src = {} + + print("\n A1. BTC/ETH — il feed 1h certificato (scritto da rebuild_history nel cron delle 00:30)") + for a in ("btc", "eth"): + d1 = pd.read_parquet(RAW / f"{a}_1h.parquet", columns=["timestamp", "volume"]) + t1 = pd.to_datetime(d1["timestamp"], unit="ms", utc=True) + d5 = pd.read_parquet(RAW / f"{a}_5m.parquet", columns=["timestamp"]) + t5 = pd.to_datetime(d5["timestamp"], unit="ms", utc=True) + last_h = t1.iloc[-1] + n5 = int(((t5 >= last_h) & (t5 < last_h + pd.Timedelta("1h"))).sum()) + n1d = int((t1 >= last_h.normalize()).sum()) + src[a] = dict(last=last_h, n5=n5, n1d=n1d, mtime=datetime.fromtimestamp( + (RAW / f"{a}_1h.parquet").stat().st_mtime, tz=timezone.utc)) + print(f" {a.upper()} ultima barra 1h {last_h} barre 5m dentro: {n5}/12 " + f"(~{n5 * 5} minuti su 60)") + print(f" barre 1h dentro l'ultimo giorno UTC: {n1d}/24 -> la barra 1d che ne " + f"esce dal resample copre ~{n5 * 5 + (n1d - 1) * 60} minuti su 1440") + print(f" parquet scritto alle {src[a]['mtime']:%H:%M:%S} UTC") + print(" `resample_tf` (src/strategies/trend_portfolio.py:195) NON scarta la barra in corso —") + print(" lo dichiara il suo stesso docstring — e `altlib.get(asset,'1d')` ci passa dentro.") + + print("\n A2. Hyperliquid 1d (scritto da fetch_hyperliquid nello stesso cron, END = oggi)") + pq = RAW / "hl_btc_1d.parquet" + d = pd.read_parquet(pq) + v = d["volume"].astype(float).values + tsl = pd.Timestamp(int(d["timestamp"].iloc[-1]), unit="ms", tz="UTC") + mt = pd.Timestamp(pq.stat().st_mtime, unit="s", tz="UTC") + q = float(v[-1] / np.median(v[-40:-1])) + orolog = (mt - tsl).total_seconds() / 60.0 + src["hl"] = dict(last=tsl, frac=q, min_orologio=orolog) + print(f" hl_btc_1d ultima barra {tsl} parquet scritto alle {mt:%H:%M:%S} UTC") + print(f" misura da OROLOGIO (diretta): la barra era aperta da {orolog:.0f} minuti su 1440 " + f"quando e' stata scritta") + print(f" misura da VOLUME (proxy): {v[-1]:,.0f} = {q * 100:.1f}% del volume mediano di " + f"un giorno pieno -> {q * 1440:,.0f} minuti") + print(" ⚠ i due stimatori NON coincidono (fattore ~2) e la discrepanza va detta invece che") + print(" mediata: il volume dei primi 30 minuti UTC non e' 1/48 di quello giornaliero") + print(" (stagionalita' intragiornaliera). Il numero da usare e' quello da OROLOGIO — e'") + print(" una misura diretta del tempo trascorso; il volume e' solo un controllo di segno.") + + print("\n A3. ETF/IB 1d (fetch_ib_equities, stesso cron ~00:35 UTC) e DVOL") + for sym in ("spy", "tlt"): + p = RAW / f"eq_{sym}_1d.parquet" + if not p.exists(): + continue + e = pd.read_parquet(p) + last = pd.Timestamp(int(e["timestamp"].iloc[-1]), unit="ms", tz="UTC") + lag = (pd.Timestamp(NOW).normalize() - last.normalize()).days + src[f"eq_{sym}"] = dict(last=last, lag=lag) + print(f" eq_{sym}_1d ultima barra {last.date()} = {lag} giorni fa " + f"(la borsa USA chiude alle 20:00 UTC: alle 00:35 l'ultima barra e' COMPLETA)") + for a in ("btc", "eth"): + p = RAW / f"dvol_{a}.parquet" + if p.exists(): + dv = pd.read_parquet(p) + tl = pd.Timestamp(int(dv["timestamp"].iloc[-1]), unit="ms", tz="UTC") + print(f" dvol_{a} ultima barra {tl}") + print("\n LETTURA: tre sorgenti su quattro consegnano un'ultima riga PROVVISORIA (crypto 1h,") + print(" crypto 1d, HL 1d); la quarta (IB) e' l'unica che consegna solo barre chiuse, e non per") + print(" merito del nostro codice ma perche' la borsa e' chiusa quando il cron gira.") + return src + + +# ============================================================================================ +# B — IL DIFETTO MORDE? replay contro registrato, monitor per monitor +# ============================================================================================ +def b_xsr(): + print("\n" + "=" * 102) + print(" B1 — paper_xsr (CONTROLLO: difetto gia' stabilito da r0822_xsr_repro, qui replicato)") + print("=" * 102) + px = live("paper_xsr") + st = json.loads((ROOT / "data" / "paper_xsr" / "state.json").read_text()) + rec = read_jsonl(ROOT / "data" / "paper_xsr" / "returns.jsonl") + sink = Sink(); px._append = sink + ts, dt, W, R, rb, syms = px.build_panel() + st0 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0, syms=st["syms"], + modeled=px._book(px.MODELED_CAPITAL, len(st["syms"])), + reals={n: px._book(c, len(st["syms"])) for c, n in px.REAL_BOOKS}) + px.advance(st0) + rep = {int(r["ts"]): float(r["net_modeled"]) for r in sink.of("returns.jsonl")} + pairs = [(float(r["net_modeled"]), rep[int(r["ts"])]) for r in rec if int(r["ts"]) in rep] + c = compare(np.array([p[0] for p in pairs]), np.array([p[1] for p in pairs])) + print(f"\n pannello: `build_panel` -> `load_hl` -> parquet HL 1d (ultima riga = giorno in corso)") + show("xsr", c) + RES["paper_xsr"] = dict(parziale="SI", cmp=c, gate="XSR01 2026-10-23", + src="hl_*_1d.parquet", cad_h=24.0) + + +def b_statarb(): + print("\n" + "=" * 102) + print(" B2 — paper_statarb (gate PRE-REGISTRATO 2026-09-27)") + print("=" * 102) + ps = live("paper_statarb") + st = json.loads((ROOT / "data" / "paper_statarb" / "state.json").read_text()) + rec = read_jsonl(ROOT / "data" / "paper_statarb" / "returns.jsonl") + j = ps.build_joint("1d") + ts, dt, pos, sr = ps._signal(j) + i0 = int(np.where(ts == st["start_ts"])[0][0]) + sink = Sink(); ps._append = sink + st0 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0, + pos_modeled=float(pos[i0]), pos_real=float(pos[i0]), + cap_modeled=ps.MODELED_CAPITAL, cap_real=ps.REAL_CAPITAL, + peak_modeled=ps.MODELED_CAPITAL, peak_real=ps.REAL_CAPITAL, + dd_modeled=0.0, dd_real=0.0, n_trades=0) + ps.advance(st0, j) + rep = {int(r["ts"]): float(r["net_modeled"]) for r in sink.of("returns.jsonl")} + pairs = [(float(r["net_modeled"]), rep[int(r["ts"])]) for r in rec if int(r["ts"]) in rep] + c = compare(np.array([p[0] for p in pairs]), np.array([p[1] for p in pairs])) + print("\n pannello: `build_joint('1d')` -> `altlib.get(...,'1d')` -> `resample_tf` " + "(ultima riga = 1 barra 1h su 24)") + show("statarb", c) + RES["paper_statarb"] = dict(parziale="SI", cmp=c, gate="STATARB 2026-09-27", + src="btc/eth 1h -> resample 1d", cad_h=24.0) + + +def b_dvolspread(): + print("\n" + "=" * 102) + print(" B3 — paper_dvolspread (kill PRE-REGISTRATO 2026-10-24, decisione 2027-01-24)") + print("=" * 102) + pv = live("paper_dvolspread") + st = json.loads((ROOT / "data" / "paper_dvolspread" / "state.json").read_text()) + rec = read_jsonl(ROOT / "data" / "paper_dvolspread" / "returns.jsonl") + P = pv._panel() + i0 = int(np.where(P["ts"] == st["start_ts"])[0][0]) + sink = Sink(); pv._append = sink + st0 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0, n_active=0, + n_flat_signal=0, n_flat_nodata=0, + wb_modeled=float(P["wb"][i0]), wb_real=float(P["wb"][i0]), + cap_modeled=pv.MODELED_CAPITAL, cap_real=pv.REAL_CAPITAL, + peak_modeled=pv.MODELED_CAPITAL, peak_real=pv.REAL_CAPITAL, + dd_modeled=0.0, dd_real=0.0, n_flips=0, frozen=pv.FROZEN) + pv.advance(st0, P) + rep = {int(r["ts"]): float(r["net_modeled"]) for r in sink.of("returns.jsonl")} + pairs = [(float(r["net_modeled"]), rep[int(r["ts"])]) for r in rec if int(r["ts"]) in rep] + c = compare(np.array([p[0] for p in pairs]), np.array([p[1] for p in pairs])) + print("\n pannello: `_panel` -> `ortholib.aligned` -> `altlib.get(...,'1d')` -> `resample_tf`") + show("dvolspread", c) + print("\n ⚠ La contabilita' a 3 stati (ATTIVE / flat-da-segnale / flat-SENZA-DATO) e il veto") + print(" d'integrita' all'80% funzionano e restano validi: contano se il DVOL c'era. Non") + print(" possono vedere QUANTO dura la barra su cui il DVOL c'era — sono ortogonali a questo") + print(" difetto, ed e' il motivo per cui l'unico monitor con una guardia dedicata e'") + print(" compromesso quanto quelli senza.") + RES["paper_dvolspread"] = dict(parziale="SI", cmp=c, gate="DVOLSPREAD 2026-10-24 / 2027-01-24", + src="btc/eth 1h -> resample 1d + dvol", cad_h=24.0) + + +def b_prevday(): + print("\n" + "=" * 102) + print(" B4 — paper_prevday (griglia ORARIA: la patologia puo' essere diversa — si misura)") + print("=" * 102) + pp = live("paper_prevday") + st = json.loads((ROOT / "data" / "paper_prevday" / "state.json").read_text()) + rec = read_jsonl(ROOT / "data" / "paper_prevday" / "returns.jsonl") + dfs = pp.build_bars() + S = st["start_ts"] + pos = {a: pp.pb.current_target(dfs[a][dfs[a]["timestamp"] <= S]) for a in pp.ASSETS} + sink = Sink(); pp._append = sink + st0 = dict(start_ts=S, last_ts=S, n_bars=0, pos_modeled=pos, pos_real=dict(pos), + cap_modeled=pp.MODELED_CAPITAL, cap_real=pp.REAL_CAPITAL, + peak_modeled=pp.MODELED_CAPITAL, peak_real=pp.REAL_CAPITAL, + dd_modeled=0.0, dd_real=0.0, n_trades=0) + pp.advance(st0, dfs) + rep = {int(r["ts"]): float(r["net_modeled"]) for r in sink.of("returns.jsonl")} + pairs = [(int(r["ts"]), float(r["net_modeled"]), rep[int(r["ts"])]) + for r in rec if int(r["ts"]) in rep] + A = np.array([p[1] for p in pairs]); B = np.array([p[2] for p in pairs]) + c = compare(A, B) + print("\n pannello: `build_bars` -> `harness.load(asset,'1h')` = il parquet 1h GREZZO, " + "nessun resample") + show("prevday", c, note=" <- 24 barre/giorno, quindi il conto e' sull'INTERA giornata") + + diff = np.abs(A - B) > 1e-6 + hh = pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True).hour + print(f"\n DOVE cade la divergenza: {int(diff.sum())} barre su {len(A)} " + f"({diff.mean() * 100:.1f}%)") + vc = pd.Series(hh[diff]).value_counts().sort_index() + print(" ora UTC delle barre divergenti: " + + ", ".join(f"{int(h):02d}:00 x{int(n)}" for h, n in vc.items())) + giorni = len(set(pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True).date)) + print(f" = ESATTAMENTE una barra per giorno su {giorni} giorni, e sempre la barra 00:00 —") + print(" cioe' quella che il cron delle 00:30 trova aperta da ~35 minuti.") + print(f"\n ⚠ LA MISURA CHE CONTA E' L'ALTRA META': {int((~diff).sum())} barre su {len(A)} " + "coincidono BIT A BIT con") + print(" il replay fatto oggi. Non e' un dettaglio contabile: e' la prova indipendente che il") + print(" feed BTC/ETH certificato NON riscrive le barre gia' chiuse (62 giorni di riscritture") + print(" notturne). Senza questa prova la ricostruzione della sezione E sarebbe un'ipotesi.") + RES["paper_prevday"] = dict(parziale="SI", cmp=c, gate="nessuno (lead in monitor)", + src="btc/eth 1h grezzo", cad_h=1.0) + + +def b_portfolio(): + print("\n" + "=" * 102) + print(" B5 — paper_portfolio (book di RICERCA a 5 sleeve: TP01+XS01+VRP01+SKH01+GTAA01)") + print("=" * 102) + ppf = live("paper_portfolio") + dts, eq = read_equity_csv(ROOT / "data" / "paper_portfolio" / "equity.csv") + _, r = ppf.portfolio_daily() + ridx = {pd.Timestamp(k): float(v) for k, v in r.items()} + A, B = [], [] + for i in range(1, len(dts)): + d = dts[i] + if d in ridx: + A.append(eq[i] / eq[i - 1] - 1.0) + B.append(ridx[d]) + c = compare(np.array(A), np.array(B)) + print("\n serie: `StrategyPortfolio(active_sleeves()).combined_daily()`; i rendimenti " + "registrati si\n ricavano da equity.csv (il monitor salva l'equity, non i ritorni)") + show("portfolio", c) + print("\n ⚠ ULTIMA RIGA ESCLUSA DAL GIUDIZIO? No, e' inclusa, e vale la pena dirlo: la barra di") + print(" OGGI coincide per forza (il replay legge lo stesso disco che il cron ha scritto alle") + print(" 00:30 e da allora nessuno lo ha riscritto). Un audit che girasse SOLO sull'ultima") + print(" barra concluderebbe 'tutto a posto'. La potenza sta nelle barre di IERI e prima.") + RES["paper_portfolio"] = dict(parziale="SI", cmp=c, gate="nessuno (dashboard/book di ricerca)", + src="5 sleeve, il piu' recente su 1h->1d e HL 1d", cad_h=24.0) + + +def b_combo(): + print("\n" + "=" * 102) + print(" B6 — paper_combo (calendario di BORSA: TP01 Deribit + GTAA IB)") + print("=" * 102) + pc = live("paper_combo") + dts, eq = read_equity_csv(ROOT / "data" / "paper_combo" / "equity.csv", col=1) + naked, _ = pc.both_daily() + ridx = {pd.Timestamp(k): float(v) for k, v in naked.items()} + A, B = [], [] + for i in range(1, len(dts)): + d = dts[i] + if d in ridx: + A.append(eq[i] / eq[i - 1] - 1.0) + B.append(ridx[d]) + c = compare(np.array(A), np.array(B)) + print("\n serie: `combo_daily()` = TP01 compoundato sulla griglia dei GIORNI DI BORSA + GTAA") + show("combo", c) + tp_last = pd.Timestamp(list(ridx.keys())[-1]) + print(f"\n PERCHE' QUESTO E' DIVERSO. La griglia e' `eq.index` (i giorni di borsa di IB),") + print(f" non il calendario crypto: l'ultima riga della serie e' {tp_last.date()}, mentre il") + print(f" feed crypto su disco arriva a oggi. Alle 00:35 UTC l'ultima barra IB e' quella di") + print(" IERI ed e' CHIUSA -> il monitor si ferma li', e il TP01 compoundato fra due giorni di") + print(" borsa consecutivi usa due chiusure crypto COMPLETE (il weekend finisce dentro la") + print(" barra del lunedi', che e' la ragione per cui il rapporto qui sopra non e' 2/7).") + print(" ⚠ E' un caso sano PER CASO, non per progetto: nessuna riga di codice chiede la barra") + print(" chiusa. Se domani GTAA01 venisse allineato al calendario crypto (o se si aggiungesse") + print(" una gamba 24/7 alla griglia), il monitor diventerebbe rotto come gli altri cinque") + print(" senza che nulla lo segnali.") + RES["paper_combo"] = dict(parziale="NO", cmp=c, gate="nessuno (paper cross-venue)", + src="TP01 1h->1d su griglia IB", cad_h=24.0) + + +# ============================================================================================ +# C — TABELLA DI SINTESI +# ============================================================================================ +GATE_INFO = { + "paper_xsr": ("XSR01 2026-10-23", "Sharpe>=1.0 E haircut $5.000<=40%"), + "paper_statarb": ("STATARB 2026-09-27", "Sharpe di net_modeled >= 0.35"), + "paper_dvolspread": ("DVOLSPREAD 24/10 kill", "Sharpe<-0.50 -> ritiro; 27/01 decisione piena"), + "paper_prevday": ("-", "lead senza data"), + "paper_portfolio": ("-", "dashboard"), + "paper_combo": ("-", "paper cross-venue"), +} + + +def sezione_C(): + print("\n" + "=" * 102) + print(" C — TABELLA: una riga per monitor") + print("=" * 102) + print(f"\n {'monitor':<19}{'parz.':>6}{'ident./tot':>13}{'corr':>9}{'min/g':>9}" + f"{'ricostr.':>10} gate dipendente") + for k, v in RES.items(): + c = v["cmp"] + ratio = "{}/{}".format(c["ident"], c["n"]) + print(f" {k:<19}{v['parziale']:>6}{ratio:>13}{c['corr']:>+9.3f}" + f"{c['minuti']:>9,.0f}{'SI':>10} {GATE_INFO[k][0]}") + print("\n 'ricostr.' = la serie CORRETTA sulla stessa finestra forward si puo' ricalcolare oggi") + print(" dai feed su disco: e' letteralmente la colonna 'replay' di questo script (sezione E).") + + +# ============================================================================================ +# D — LA GUARDIA CHE MANCA A monitor_health +# ============================================================================================ +def guardia_barra_chiusa(last_bar_ms: float, cadenza_h: float, scritta_a: float, + grazia_min: float = 5.0) -> bool: + """VERO se la barra consumata per ultima era GIA' CHIUSA quando e' stata registrata. + + `last_bar_ms` timestamp (open-labeled) dell'ultima barra registrata, in ms epoch; + `cadenza_h` passo nominale della serie (24 per i giornalieri, 1 per prevday); + `scritta_a` quando quella riga e' stata scritta (mtime del file di serie), in secondi epoch. + + E' O(1), non ricalcola nessuna strategia, e usa solo cio' che il monitor gia' scrive. + La `grazia` esiste per lo skew d'orologio, non per ammorbidire il giudizio: le violazioni + misurate valgono ore, non minuti.""" + chiusura = last_bar_ms / 1000.0 + cadenza_h * 3600.0 + return chiusura <= scritta_a + grazia_min * 60.0 + + +def _ultima_barra(dirname: str, series: str) -> tuple[float, float]: + p = ROOT / "data" / dirname / series + mt = p.stat().st_mtime + if series.endswith(".jsonl"): + rows = read_jsonl(p) + return float(rows[-1]["ts"]), mt + dts, _ = read_equity_csv(p) + return float(pd.Timestamp(dts[-1]).timestamp() * 1000), mt + + +def sezione_D(): + print("\n" + "=" * 102) + print(" D — LA GUARDIA CHE MANCA (proposta, NON implementata: tocca il cron di produzione)") + print("=" * 102) + print(""" + PERCHE' `monitor_health` NON LO VEDE. Misura due guasti — coda (eta' dell'ultima barra) e buchi + interni (copertura fra prima e ultima barra) — e questo difetto non e' ne' l'uno ne' l'altro: + la serie e' FRESCA (scritta stanotte) e COMPLETA (una barra per giorno, zero buchi). Entrambe + le misure guardano i TIMESTAMP e nessuna guarda cosa c'e' dentro la barra. Aggiungere una terza + misura della stessa famiglia non aiuterebbe: serve una misura che confronti la barra letta con + il momento in cui e' stata letta. + + DUE CANDIDATI, e non sono equivalenti.""") + + # ---- candidato 1: vol registrata vs vol ricalcolata -------------------------------------- + print("\n D1. CANDIDATO 'RAPPORTO DI VOLATILITA'' (vol registrata / vol ricalcolata).") + print(f" {'monitor':<19}{'vol reg.':>10}{'vol ric.':>10}{'rapporto':>10} verdetto a " + "soglia [0.70, 1.40]") + lo, hi = 0.70, 1.40 + miss = [] + for k, v in RES.items(): + c = v["cmp"] + rr = c["sd_rec"] / c["sd_rep"] if c["sd_rep"] else float("nan") + seg = "SEGNALA" if not (lo <= rr <= hi) else "tace" + if v["parziale"] == "SI" and seg == "tace": + miss.append(k) + print(f" {k:<19}{c['sd_rec']:>9.2f}%{c['sd_rep']:>9.2f}%{rr:>10.3f} {seg}") + print(f" -> MANCA {len(miss)} monitor rotti su {sum(1 for v in RES.values() if v['parziale'] == 'SI')}" + f": {', '.join(miss) if miss else '-'}") + print(" Motivo strutturale, non taratura: su una griglia oraria il difetto tocca 1 barra") + print(" su 24 e sposta la vol dell'1-2%, cioe' DENTRO il rumore di stima di una vol su") + print(" poche settimane. Abbassare la soglia per prenderlo genererebbe falsi allarmi ogni") + print(" volta che la vol di mercato cambia regime. E costa: richiede di RIESEGUIRE ogni") + print(" strategia a ogni giro (qui ~6 s per il solo book a 5 sleeve).") + print(" Resta utile per un guasto DIVERSO — il drift fra monitor e strategia — che la D2") + print(" non vede. Va aggiunta DOPO, non al posto della D2.") + + # ---- candidato 2: barra chiusa ------------------------------------------------------------ + print("\n D2. CANDIDATO 'BARRA CHIUSA' — RACCOMANDATO.") + print(" REGOLA: ts_ultima_barra + cadenza <= quando quella riga e' stata SCRITTA") + print(" (il 'quando' e' l'mtime del file di serie: `_append` scrive solo se c'e' una") + print(" barra nuova, quindi l'mtime E' l'ora dell'ultima registrazione).") + print(" Soglia: grazia 5 minuti per lo skew d'orologio. La soglia esatta e' irrilevante —") + print(" si vede sotto che le violazioni valgono 23,4 h e 0,4 h, non minuti.") + specs = [("paper_xsr", "returns.jsonl", 24.0), ("paper_statarb", "returns.jsonl", 24.0), + ("paper_dvolspread", "returns.jsonl", 24.0), ("paper_prevday", "returns.jsonl", 1.0), + ("paper_portfolio", "equity.csv", 24.0), ("paper_combo", "equity.csv", 24.0)] + print(f"\n {'monitor':<19}{'ultima barra':>22}{'scritta alle':>22}" + f"{'mancavano':>12} verdetto") + esiti = {} + for name, ser, cad in specs: + lb, mt = _ultima_barra(name, ser) + ok = guardia_barra_chiusa(lb, cad, mt) + manca = (lb / 1000.0 + cad * 3600.0 - mt) / 3600.0 + esiti[name] = ok + print(f" {name:<19}" + f"{str(pd.Timestamp(lb, unit='ms', tz='UTC'))[:19]:>22}" + f"{str(pd.Timestamp(mt, unit='s', tz='UTC'))[:19]:>22}" + f"{max(manca, 0):>11.1f}h {'OK' if ok else '*** LEGGE-IN-CORSO ***'}") + print("\n -> segnala 5 monitor su 6 e TACE sull'unico sano. Non e' un rilevatore tarato") + print(" per segnalare: `paper_combo` e' un controllo positivo REALE, con dati veri,") + print(" che la guardia distingue dagli altri cinque senza nessuna eccezione cablata.") + + # ---- controllo positivo sintetico, nei due versi ------------------------------------------ + print("\n D3. CONTROLLO POSITIVO SINTETICO (obbligatorio: una guardia che segnala tutto o") + print(" niente e' inutile quanto una rotta). Casi costruiti, nei DUE versi:") + base = pd.Timestamp("2026-08-21 00:00:00", tz="UTC") + casi = [ + ("sano: barra 1d di ieri, scritta stanotte alle 00:30", + base, 24.0, pd.Timestamp("2026-08-22 00:30", tz="UTC"), True), + ("ROTTO: barra 1d di OGGI, scritta oggi alle 00:30 (= i cinque monitor)", + pd.Timestamp("2026-08-22 00:00", tz="UTC"), 24.0, + pd.Timestamp("2026-08-22 00:30", tz="UTC"), False), + ("sano: barra 1h delle 23:00, scritta a mezzanotte e mezza", + pd.Timestamp("2026-08-21 23:00", tz="UTC"), 1.0, + pd.Timestamp("2026-08-22 00:30", tz="UTC"), True), + ("ROTTO: barra 1h delle 00:00, scritta alle 00:30 (= paper_prevday)", + pd.Timestamp("2026-08-22 00:00", tz="UTC"), 1.0, + pd.Timestamp("2026-08-22 00:30", tz="UTC"), False), + ("sano: monitor in ritardo di 3 giorni (fermo, ma non legge una barra aperta)", + pd.Timestamp("2026-08-18 00:00", tz="UTC"), 24.0, + pd.Timestamp("2026-08-22 00:30", tz="UTC"), True), + ("limite: barra chiusa 4 minuti DOPO la scrittura -> assorbita dalla grazia", + pd.Timestamp("2026-08-21 00:04", tz="UTC"), 24.0, + pd.Timestamp("2026-08-22 00:00", tz="UTC"), True), + ] + tutti = True + for desc, bar, cad, w, atteso in casi: + got = guardia_barra_chiusa(bar.timestamp() * 1000, cad, w.timestamp()) + tutti &= (got == atteso) + print(f" [{'ok ' if got == atteso else 'FAIL'}] atteso {'OK ' if atteso else 'ALLARME'}" + f" -> ottenuto {'OK ' if got else 'ALLARME'} {desc}") + print(f" -> controllo positivo {'SUPERATO' if tutti else '*** FALLITO ***'} " + "(la guardia distingue 'fermo' da 'legge una barra aperta': sono guasti diversi") + print(" e vanno riparati in modi diversi — uno e' il cron, l'altro e' `advance()`).") + print("\n D4. COSA NON COPRE, dichiarato. La guardia vede la barra CONSUMATA, non la barra") + print(" USATA PER IL SEGNALE: un monitor che si fermasse alla barra chiusa ma calcolasse") + print(" il target su un pannello che include quella in corso passerebbe. Per quello serve") + print(" la D1 (o un test di identita' contro il backtest), ed e' il motivo per cui la") + print(" raccomandazione e' 'prima la D2, poi la D1', non 'una delle due'.") + + +# ============================================================================================ +# E — COSTO DELLA RIPARAZIONE +# ============================================================================================ +def sezione_E(): + print("\n" + "=" * 102) + print(" E — LA DOMANDA OPERATIVA: cosa costa riparare `advance()`") + print("=" * 102) + oggi = pd.Timestamp(NOW).normalize().tz_localize(None) + righe = [ + ("paper_statarb", "STATARB", pd.Timestamp("2026-09-27"), "returns.jsonl"), + ("paper_xsr", "XSR01", pd.Timestamp("2026-10-23"), "returns.jsonl"), + ("paper_dvolspread", "DVOLSPREAD (kill)", pd.Timestamp("2026-10-24"), "returns.jsonl"), + ("paper_dvolspread", "DVOLSPREAD (decisione)", pd.Timestamp("2027-01-24"), "returns.jsonl"), + ] + print(f"\n {'monitor / gate':<34}{'barre oggi':>12}{'data gate':>13}{'se AZZERI':>12}" + f"{'ritardo':>10}") + for mon, nome, gate, ser in righe: + n = len(read_jsonl(ROOT / "data" / mon / ser)) + # con una barra al giorno, tornare a n barre richiede n giorni dal reset + rip = oggi + pd.Timedelta(days=n) + rit = (rip - gate).days + print(f" {f'{mon} / {nome}':<34}{n:>12}{str(gate.date()):>13}" + f"{str(rip.date()):>12}{rit:>+9}g") + print("\n Cosi' letta, la riparazione sposta i tre gate di 1-2 mesi. MA la colonna giusta e'") + print(" un'altra, e cambia la decisione:") + print(""" + ✅ LA FINESTRA FORWARD **NON VA PERSA**: e' RICOSTRUIBILE, per tutti e sei i monitor. + Le ore mancanti non sono mai state registrate, ma non e' li' che vivono: vivono nei feed + ARCHIVIATI su disco, che contengono le barre CHIUSE di ogni giorno della finestra. Il + "replay" della sezione B *e'* la serie corretta sulla stessa identica finestra + [inception -> oggi], calcolata con la config congelata e lo stesso codice di produzione. + Ricostruire non e' rifare un backtest su una finestra scelta dopo: inception, parametri e + universo sono congelati e datati PRIMA — cambia solo che ogni barra dura 24 ore invece di 35 + minuti. + + LE DUE CONDIZIONI, ed e' giusto chiedere che siano verificate e non assunte: + (1) il feed non riscrive le barre gia' chiuse. **MISURATO qui**: `paper_prevday` ha + 1426 barre su 1488 identiche BIT A BIT al replay dopo 62 giorni di riscritture + notturne del parquet BTC/ETH (sezione B4). Per Hyperliquid la stessa cosa e' misurata + in `r0822_xsr_repro` T2 (Sharpe 2024 e 2025 riprodotti al centesimo su 731 barre). + (2) la config e' congelata e verificabile: `paper_dvolspread` lo controlla da solo + (`frozen` nello stato, esce 1 se diverge); per gli altri il congelamento e' nel + sorgente ed e' in git. + + ⚠ COSA **NON** E' RICOSTRUIBILE, e va detto perche' e' l'eccezione che rende la regola utile: + una serie che dipende da uno stato non archiviato. Nel progetto ce n'e' una — la catena + opzioni (`data/raw/cb_chain`, un'ora persa e' persa per sempre: Deribit non serve book + storici) — e infatti `monitor_health` le da' una soglia d'eta' di 3 ore invece di 48. I sei + monitor di questo audit NON sono in quella categoria: leggono tutti feed che il cron + ri-scarica per intero ogni notte. + + QUINDI LA DECISIONE DELL'OPERATORE NON E' "riparare e perdere la finestra" ma: + (a) riparare `advance()` perche' si fermi all'ultima barra CHIUSA; + (b) RIGENERARE `returns.jsonl` / `equity.csv` dall'inception con lo stesso codice — cio' che + questo script ha gia' calcolato in memoria — invece di `--reset`; + (c) dichiarare nel diario che le serie sono state rigenerate e da quale commit, perche' una + serie forward rigenerata e' credibile solo se la data di inception e la config sono + verificabili in git (lo sono). + Il costo vero e' (c), non i giorni: nessuna delle tre date di gate si sposta. + + ⚠ UNA COSA CHE LA RIGENERAZIONE CAMBIA DAVVERO, e va scritta nel diario: i libri REAL saltano i + ribilanci sotto il min-order, e quella decisione dipende dal capitale corrente, quindi il + percorso REAL rigenerato non e' il percorso REAL registrato — non e' "lo stesso numero + calcolato meglio", e' un secondo percorso deterministico. Per XSR01 conta: il gate del 23/10 + legge net_REAL-$5000 per l'haircut.""") + + +def main() -> None: + print("\n" + "#" * 102) + print("# r0822_monitor_audit — SOLA LETTURA su scripts/live, src/live, data, config, cron") + print(f"# ora UTC {NOW:%Y-%m-%d %H:%M:%S} (il cron_daily di oggi ha girato alle 00:30)") + print("#" * 102 + "\n") + sezione_A() + b_xsr() + b_statarb() + b_dvolspread() + b_prevday() + b_portfolio() + b_combo() + sezione_C() + sezione_D() + sezione_E() + print("\n" + "=" * 102) + print(" FINE — nessun file di produzione e' stato modificato da questo script.") + print("=" * 102 + "\n") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_volsize_skeptic.py b/scripts/research/r0822_volsize_skeptic.py index 95d9c8d..324327d 100644 --- a/scripts/research/r0822_volsize_skeptic.py +++ b/scripts/research/r0822_volsize_skeptic.py @@ -511,7 +511,7 @@ def main() -> None: print("\n per anno (ISO dSh, mediana appaiata sulle ancore):") yrs = None - for nm in [lead, "BIN L>=1 (0/1)", "DROP attivita' >p75", "BIN L<1 (INVERSO)"]: + for nm in [lead, "BIN L>=med (0/1)", "canale FREQUENZA cont.", "BIN L<1 (INVERSO)"]: yrs, cells = per_year(BK2, CT2, nm, offs) if nm == lead: print(" " + f"{'':<26}" + "".join(f"{y:>8}" for y in yrs)) @@ -524,8 +524,9 @@ def main() -> None: print(" 4. L'ASSE DELLA FINESTRA — picco o plateau? + scelta di w IN-SAMPLE-ONLY") print("-" * L) wnames = [(w, f"VTL tv20 w{w}" if w != 90 else lead) for w in WGRID] + bd0 = EXV2["BTC"]["base_daily"] print(f" {'w':>6}{'ISO dShFULL':>14}{'pos/n':>8}{'ISO dShIS':>12}{'ISO dShHOLD':>13}" - f"{'pos/n':>8}") + f"{'pos/n':>8}{'barre att.':>12}") wf, wi, wh = {}, {}, {} for w, nm in wnames: dF = A.anchor_luck_delta(lambda o, n=nm: BK2[n][o], lambda o, n=nm: CT2[n][o], offs, @@ -535,8 +536,9 @@ def main() -> None: dH = A.anchor_luck_delta(lambda o, n=nm: hold(BK2[n][o]), lambda o, n=nm: hold(CT2[n][o]), offs, metric=sh) wf[w], wi[w], wh[w] = dF["median_paired"], dI["median_paired"], dH["median_paired"] + nact = float(np.median((bd0 != 0.0).astype(float).rolling(w).sum().dropna().values)) print(f" {w:>6}{wf[w]:>+14.3f}{dF['n_positive']:>5}/{len(offs):<3}{wi[w]:>+12.3f}" - f"{wh[w]:>+13.3f}{dH['n_positive']:>5}/{len(offs):<3}") + f"{wh[w]:>+13.3f}{dH['n_positive']:>5}/{len(offs):<3}{nact:>12.0f}") wbest_full = max(WGRID, key=lambda w: wf[w]) wbest_is = max(WGRID, key=lambda w: wi[w]) print(f"\n argmax su FULL (cio' che l'agente ha pubblicato): w = {wbest_full} " @@ -561,8 +563,14 @@ def main() -> None: f"{wf[wbest_full]:+.3f} / {wh[wbest_full]:+.3f}") rho = float(np.corrcoef([wi[w] for w in WGRID], [wh[w] for w in WGRID])[0, 1]) print(f" corr(ISO dSh in-sample, ISO dSh hold-out) fra le {len(WGRID)} finestre: {rho:+.2f}") - print(" -> se e' ~0 o negativa, la scelta di w in-sample NON predice l'hold-out: l'asse") - print(" e' rumore e il picco a w=90 e' un max-of-k.") + print(" LETTURA (attacco 4 FALLITO, e il motivo e' informativo): la scelta di w") + print(" in-sample-only cade sulla cella pubblicata, e la corr IS/HOLD e' POSITIVA -> l'asse") + print(" non e' rumore. E non e' nemmeno un PICCO: e' una SOGLIA. Sotto w=45 la finestra") + print(" contiene troppo poche barre ATTIVE per misurare qualsiasi cosa (colonna a destra) e") + print(" il segno si rovescia; da 45 a 270 e' un plateau, 5 celle su 5 positive con 19-23") + print(" ancore su 23. La descrizione 'picco, non plateau' del bersaglio e' PIU' SEVERA del") + print(" vero — ed e' proprio cio' che si aspetta da un FILTRO DI FREQUENZA: serve una") + print(" finestra abbastanza lunga da contare i trade.") # ------------------------------------------------------------------ §5 sintesi print("\n" + "-" * L) @@ -606,6 +614,28 @@ def main() -> None: f"{RES['BIN L>=med (0/1)']['dH']:+.3f} HOLD ({RES['BIN L>=med (0/1)']['nF']}/{len(offs)} ancore)") print(f" controllo di segno (INVERSO): {RES['BIN L<1 (INVERSO)']['dF']:+.3f} FULL " f"({RES['BIN L<1 (INVERSO)']['nF']}/{len(offs)}) -> il filtro e' reale, non un artefatto") + + print("\n" + "=" * L) + print(" VERDETTO DELLO SCETTICO") + print("=" * L) + print(" A1 test PRE-REGISTRATO (IV su V1): previsione |V1| < |V2-DD| VERIFICATA IN " + f"{ok}/{tot} CELLE.") + print(" L'effetto e' PIU' GRANDE dove lo stop e' gia' in ATR (V1 mediana " + f"{np.median(np.abs(e1s)):.3f} vs V2-DD {np.median(np.abs(e2s)):.3f}), e il test NON e'") + print(" senza potenza (su V2-DD la famiglia arriva a +0.067 con 23/23). -> la") + print(" SPIEGAZIONE 'lo stop a percentuale fissa lascia variare il rischio' e' REFUTATA.") + print(" A2 contabilita': la premessa del difetto e' FALSA (P&L al giorno d'INGRESSO, " + "99.3%);") + print(" la 'riparazione' e' un no-op bit-exact; il '+0,04 fantasma' e' [L combinata] -") + print(" [L per asset]. Nessun look-ahead c'era, e la variante scartata era la MIGLIORE.") + print(" A3 un filtro BINARIO 0/1 sulla mediana della stessa L riproduce il " + f"{RES['BIN L>=med (0/1)']['dF']/base_eff*100:.0f}% del") + print(" vantaggio senza alcun vol-target -> la FORMULA DI SIZING E' ORNAMENTO. Il canale") + print(" FREQUENZA da solo fa +0.051, la MAGNITUDINE +0.003 (replica esatta del bersaglio).") + print(" A4 l'asse w NON e' un picco ne' un max-of-k: e' una soglia sul numero di barre") + print(" attive nella finestra, con plateau 45-270. Attacco fallito.") + print(" --> il NUMERO regge (+0.070 riprodotto esattamente), la SPIEGAZIONE cade due volte,") + print(" e il gate pre-registrato per il 22/12/2026 e' GIA' STATO FALLITO oggi.") print(f"\n fatto in {time.time()-t0:.0f}s") print("=" * L) From 1750b5a93b6a6b927fc3fcf0972961b8f99905ee Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:43:33 +0000 Subject: [PATCH 018/114] =?UTF-8?q?research(wave-0822):=20lo=20scettico=20?= =?UTF-8?q?RITIRA=20il=20difetto=20di=20contabilita'=20(non=20esiste,=20bi?= =?UTF-8?q?t-exact)=20e=20chiude=20oggi=20il=20gate=20del=2022/12=20?= =?UTF-8?q?=E2=80=94=20il=20meccanismo=20e'=20un=20filtro=20di=20frequenza?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 46 +++++++++++- scripts/research/r0822_alt_options.py | 91 ++++++++++++++++++++++- scripts/research/r0822_monitor_audit.py | 55 ++++++++++++-- scripts/research/r0822_prop_alloc.py | 66 ++++++++++++++-- scripts/research/r0822_volsize_skeptic.py | 7 +- 5 files changed, 240 insertions(+), 25 deletions(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index e685821..e656596 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -14,7 +14,7 @@ null de-levering superato + eseguibilita' al capitale dichiarato. | 11 | VRP-QUOTE-VERE | **SCARTATO** + **1 difetto di RACCOLTA in produzione** | il campione contiene l'**astensione** della strategia (0/19 settimane passano IV-rank>0.30) e il titolo non sopravvive all'ora d'ingresso; ma soprattutto: **`collect_chain.py` raccoglie la famiglia di contratti che il conto NON puo' marginare** | | 7 | TERM-STRUCTURE | **SCARTATO** + 1 LEAD stretto (decisione 2027-02-01) | la pendenza e' un **termometro contemporaneo** (picco CCF a lag −1h/0h); il carry non paga il bid-ask (**18,6 giorni di roll-down per pagare UN round-trip**); come gate e' la **5ª occorrenza** della ridondanza col trend | | 6 | OI-PIN | **SCARTATO** | il max-pain batte uno strike casuale ma **non batte mai (0/24) la media a 7 giorni dello spot**, un livello che non usa NESSUN dato di opzioni: non e' pinning, e' reversione verso il centro recente con l'OI come stimatore rumoroso di quel centro | -| 8 | VOL-SIZE | **LEAD** (gate 2026-12-22) + 1 falsificazione | dare a SKH01 una size per-trade regge a **23/23 ancore**, 8/8 anni, null di permutazione e trasferimento su V1 — ma vale **+0,07 di Sharpe di libro, un terzo della fortuna d'ancora del libro stesso (+0,196)**. Il vol-target di **libro** e' invece falsificato: compra peso SKH gia' respinto e peggiora l'eseguibilita' | +| 8 | VOL-SIZE | **LEAD RIDIMENSIONATO** — gate 22/12 **gia' fallito oggi** | dare a SKH01 una size per-trade regge a **23/23 ancore**, 8/8 anni, null di permutazione e trasferimento su V1 — ma vale **+0,07 di Sharpe di libro, un terzo della fortuna d'ancora del libro stesso (+0,196)**. Il vol-target di **libro** e' invece falsificato: compra peso SKH gia' respinto e peggiora l'eseguibilita' | | — | **XSR-REPRO** (integrita') | 🚨 **DIFETTO DI PRODUZIONE** | il numero 1.82 e' SPIEGATO e non era sbagliato (era su una **terza** lente, e su una barra non ancora chiusa) — ma cercandone la causa e' emerso che **`paper_xsr` registra ~41 minuti di mercato al giorno**, non un giorno. Tre gate pre-registrati leggono serie costruite cosi' | | 5 | SKEW | **SCARTATO** (Q1, Q2) + **LEAD** (Q3, gate 2027-02-22) | il prezzo muove lo skew (t 3,1-9,4 su 8/8 test), **non il contrario** (max |t| in avanti 2,35 contro 2,08 atteso dal rumore). Ma Q3 e' grosso: **il f=0,73 di VRP01 e' per il 42% STRUTTURA A TERMINE e solo per il 25% skew** | | — | **HL-EXEC** (audit di fatto) | **3 falsificazioni misurate** | il pavimento vero e' **$10 (non $5)** e il taker **4,50 bps (non 5,0)** — ma il *"XS01 serve ~$20k"* e' **refutato del tutto** (nessuna soglia da min-order), il *"XSR01 ~$5.000"* e' **conservativo di 1,7x** (vero ~$3.000), e lo **slippage "rischio #1" di XSR01 e' refutato** con margine **21x** | @@ -252,13 +252,15 @@ recente, e l'OI e' solo uno stimatore rumoroso di quel centro. ### 8 — VOL-SIZE (r0822_vol_size.py, 28 trial + 23 ancore) ✅ Sanity **bit-exact**: ricomposizione trade-by-trade `max|diff| = 0.0` contro `run_asset` e contro `sleeves._skyhook_returns()`. -🚨 **DIFETTO DI METODO GENERALE, trovato dall'agente nella propria prima stesura e MISURATO invece +❌ **RITIRATO DALLO SCETTICO (vedi in fondo): il difetto qui sotto NON ESISTE su questo codice.** +Lasciato a verbale perche' la regola era stata pubblicata e va vista insieme alla sua smentita. +~~🚨 **DIFETTO DI METODO GENERALE, trovato dall'agente nella propria prima stesura e MISURATO invece che cancellato: su uno sleeve a equity a GRADINO, un overlay giornaliero e' NON CAUSALE.** Tutto il P&L di un trade SKH01 e' contabilizzato il giorno di **CHIUSURA**, fino a **3,83 giorni dopo l'ingresso**; moltiplicare la serie giornaliera per `L_t` (che usa solo dati <= t−1) **passa qualunque controllo di causalita' scritto sulla serie giornaliera** — il look-ahead sta nella CONTABILITA', non nella formula. Costo misurato: **+0,04 di Sharpe hold-out fantasma**. -**REGOLA: un overlay si applica alla data d'INGRESSO, non a quella in cui il P&L viene contabilizzato.** +**REGOLA: un overlay si applica alla data d'INGRESSO, non a quella in cui il P&L viene contabilizzato.**~~ **<- RITIRATA, vedi sotto** 🚨 **REGOLA 2 — il null del de-levering e' DEGENERE quando la variante e' una ri-scalatura** (`sh(k*base) === sh(base)`): il test che serve non e' sul DD ma **ISO-PESO sullo Sharpe**, altrimenti si sta solo misurando di quanto e' cambiato il peso di uno sleeve. Qui era decisivo: quasi ogni @@ -545,3 +547,41 @@ dall'ora scelta = **un'altra strategia, non un'ancora**; (c) **un gate che stamp verificato prima di dichiararlo "non girato"** — stampava le chiavi sbagliate. ✅ Disciplina di rete rispettata: <=2 req/s e **sospensione automatica nella finestra :25-:29** di `cron_chain` (verificato: si e' sospeso da solo per 264s). + +### SCETTICO su VOL-SIZE (r0822_volsize_skeptic.py, 43 valutazioni x 23 ancore) +✅ Replica **bit-exact** prima di ogni attacco: gamba 50/50 vs `sleeves._skyhook_returns()` +**max|diff| = 0,0**; e il titolo riprodotto **esattamente** (+0,070 ISO dShFULL 23/23, +0,101 HOLD). +🚨 **RITIRA UNA REGOLA PUBBLICATA POCHE ORE PRIMA — il difetto di contabilita' NON ESISTE.** +`backtest_signals` fa `equity[i_ent:i_ex+1] = capital`: il P&L e' contabilizzato al giorno +d'**INGRESSO** in **291/293 (99,3%)** dei trade multi-giorno, all'uscita in **4**. Troncando i dati +alla barra d'ingresso e ricalcolando le size da zero: **50/50 identiche, max|diff| = 0,0**. E la +"riparazione" e' un **no-op bit-exact** (max|diff| 6,7e-16, Sharpe identico a 4 decimali). +**Il "+0,04 fantasma" era interamente `[L sulla gamba COMBINATA] − [L PER ASSET]`** — due varianti +entrambe causali — **e la variante scartata come "non causale" era la MIGLIORE sull'hold-out +(+0,039)**. *Il costo di una regola derivata da un difetto inesistente e' gia' stato pagato.* +📌 **REGOLA CORRETTA (piu' debole e vera):** *"applicare un overlay alla data d'ingresso"* resta un +buon consiglio **generale**, ma su questo motore le due date **coincidono** — e va verificato con un +troncamento bit-exact **prima** di scartare un ramo, non dedotto dalla forma dell'equity. +🚨 **IL GATE PRE-REGISTRATO AL 22/12/2026 E' GIA' FALLITO OGGI, e nel modo piu' netto.** L'agente +aveva scritto: *"se l'effetto e' UGUALE su V1, la spiegazione e' sbagliata anche se il numero +regge"*. Misurato: |effetto| mediano di famiglia **V1 0,067 contro V2-DD 0,032** — l'inverse-vol +funziona **il DOPPIO dove lo stop e' gia' in ATR**, cioe' esattamente al contrario della previsione, +in **8 celle su 8** con **23/23 ancore positive**. E il test **ha potenza** (su V2-DD la famiglia +arriva a +0,067, quanto il LEAD). +📌 **La spiegazione avversaria e' CONFERMATA e la sua forma minima e' un binario.** `BIN L>=med` +(size **0/1** alla mediana espandente, nessuna magnitudine, 194/406 trade scartati) vale +**+0,080 FULL (20/23) = 115% del LEAD**. Controllo di segno superato: il binario **INVERSO** fa +**−0,159 (0/23)**. Canali: **FREQUENZA +0,051 (23/23)** contro **MAGNITUDINE +0,003 (17/23)**. +I trade tenuti hanno net medio **2,03% contro 0,36%** degli scartati. +✅ **Un attacco dello scettico e' FALLITO, e lo dichiara:** l'asse della finestra **non e' un picco** +— l'argmax in-sample-only cade su **w=90 = la cella pubblicata**, corr(IS, HOLD) fra le 9 finestre +**+0,63**, plateau da 45 a 270. Sotto w=45 la finestra contiene **1-3 barre attive** (colonna +misurata) e il segno si rovescia: non e' fragilita', e' campione vuoto. +**-> quindi anche la mia nota "picco, non plateau" nel filone 8 va letta come SUPERATA.** +Stima onesta se `w` e' scelto (mediana di famiglia): **+0,025 FULL**. +📌 **CONSEGUENZA:** il LEAD non ha piu' una condizione aperta da attendere — la sua gamba +esplicativa e' chiusa oggi. Se qualcuno vorra' portarlo avanti, **il candidato giusto non e' la +formula ma il binario**: meno gradi di liberta', **dimezza gli ordini** (migliora l'eseguibilita' a +$635), 115% sul FULL — ma paga in fragilita' d'ancora (20/23 contro 23/23, banda che include −0,061, +hold-out +0,072 contro +0,101). **Resta un terzo della fortuna d'ancora del libro stesso (+0,196): +niente qui giustifica di toccare pesi, cron o config.** diff --git a/scripts/research/r0822_alt_options.py b/scripts/research/r0822_alt_options.py index 1a433e3..7a6af36 100644 --- a/scripts/research/r0822_alt_options.py +++ b/scripts/research/r0822_alt_options.py @@ -356,9 +356,16 @@ def liquidita(rows: list[dict], tab: dict) -> None: s = stat[f] print(f"{f:<10} {s['oi']:>11} {rA[f]:>8} {s['puts']:>10} {s['two']:>10} " f"{s['pct']:>9.0%} {rB[f]:>8}") - print("\n 📌 LA CLASSIFICA SI ROVESCIA. SOL_USDC e' 1° per open interest e ULTIMO per quote") - print(" presenti; BTC_USDC, dichiarato 'praticamente morto' (5 strumenti con OI>=100), ha") - print(" quasi tutti i put quotati a due lati. L'open interest misura posizioni ACCUMULATE,") + # la correlazione di rango si CALCOLA, non si racconta: sulle sole famiglie lineari, + # cioe' quelle su cui la domanda del filone e' stata posta. + a = np.array([rA[f] for f in LINEAR], float) + c = np.array([rB[f] for f in LINEAR], float) + rho = float(np.corrcoef(a, c)[0, 1]) + print(f"\n 📌 LA CLASSIFICA SI ROVESCIA. Correlazione di rango fra i due criteri sulle sei") + print(f" famiglie lineari: {rho:+.2f}. SOL_USDC e' 1ª per open interest fra le lineari e") + print(" ULTIMA di tutte per quote presenti; BTC_USDC, dichiarato 'praticamente morto' (5") + print(" strumenti con OI>=100), ha quasi tutti i put quotati a due lati. L'open interest") + print(" misura posizioni ACCUMULATE,") print(" non la possibilita' di negoziare ADESSO — e sull'universo lineare le due cose non") print(" hanno quasi nulla a che vedere.") print(" ⚠️ Ricaduta sul codice di produzione: `collect_chain.py` prefiltra a OI>=100. Sulle") @@ -366,6 +373,28 @@ def liquidita(rows: list[dict], tab: dict) -> None: print(" suo nome lo lascia credere. 4ª occorrenza di 'una riga presente non e' un dato") print(" presente' — qui in versione: 'una posizione aperta non e' un prezzo negoziabile'.") + print("\n" + "-" * 104) + print("2b. E ALLORA DOV'E' LA LIQUIDITA' CHE SI VEDE NEL VOLUME? Nel mercato a BLOCCHI,") + print(" che ha un minimo d'ordine suo — e non e' raggiungibile con $635.") + print("-" * 104) + print(f"{'famiglia':<10} {'con volume 24h':>15} {'volume 24h $':>14} {'blocco min':>11} " + f"{'blocco min $':>13} {'x il conto':>11}") + for f in FAMS: + sub = [r for r in rows if r["fam"] == f] + if not sub: + continue + nv = sum(1 for r in sub if r["vol"] > 0) + vusd = sum(r["vol_usd"] for r in sub) + bm = sub[0]["blockmin"] + bm_usd = bm * tab[f]["S"] * (1.0 if not is_inv(f) else 1.0) + print(f"{f:<10} {nv:>15} {vusd:>14,.0f} {bm:>11g} {bm_usd:>13,.0f} " + f"{bm_usd/CAPITAL:>10.0f}x") + print("\n 📌 SOL_USDC ha molti piu' strumenti con volume nelle ultime 24h che quotati a due") + print(" lati: quelle opzioni si negoziano, ma via blocchi/RFQ. Il minimo di blocco e' un") + print(" fatto del venue, non un'opinione, e a $635 e' fuori portata di due ordini di") + print(" grandezza. **Non si conclude che il mercato alt non esista: si conclude che la parte") + print(" a cui questo conto ha accesso — il book continuo — e' quella sottile.**") + # ---------------------------------------------------------------- §3 la struttura @@ -461,9 +490,65 @@ def struttura(rows: list[dict]) -> dict: f"{len(gi):>12} {len(ge):>11}") print(" Se la colonna `eseg.` si allontana dal target -0.28/-0.10, la struttura che si") print(" comprerebbe NON e' quella richiesta: la griglia degli strike quotati la deforma.") + + print("\n --- CONTROLLO POSITIVO: questo snapshot e' di SABATO. E' atipico? ---") + print(" Non posso aspettare una settimana, ma posso puntare lo stesso strumento su una") + print(" grandezza che il progetto ha GIA' misurato su 19 settimane di giorni feriali:") + print(" attraversare lo spread sul canonico BTC/ETH inverse costa ~10% del credito (30/07).") + for f in INVERSE: + if f in res: + print(f" {f:<4} inverse: costo di attraversamento oggi = " + f"{(1-res[f]['fv'])*100:5.1f}% del credito (atteso ~10%)") + print(" Se coincide, il sabato non sta degradando i book che il progetto conosce, e la") + print(" lettura sulle famiglie nuove non e' un artefatto del giorno. **E' un controllo") + print(" debole** (una grandezza sola, un istante), non una prova di tipicita'.") + robustezza_dte(rows) return res +def robustezza_dte(rows: list[dict]) -> None: + """L'UNICO grado di liberta' di questo file, esposto invece che nascosto. + + VRP01 e' settimanale (tenor 7g, finestra 4-10). Qui la finestra e' stata allargata a 4-45 + per dare piu' scadenze alle famiglie alt, che ne hanno poche. Allargare una finestra e' + una scelta, e una scelta va misurata: se la conclusione dipendesse da essa, sarebbe la + conclusione a essere stata scelta. + """ + global DTE_HI + print("\n --- ROBUSTEZZA: la finestra DTE e' il mio unico grado di liberta' ---") + print(f" {'famiglia':<10} {'finestra':>9} {'scad':>5} {'f_venue':>8} {'spr% corta':>11} " + f"{'cred eseg$':>11} {'maxloss$':>9}") + orig = DTE_HI + try: + for hi in (10.0, 45.0): + DTE_HI = hi + for f in FAMS: + gg = scegli(rows, f, eseguibile=True) + fv, ss, ce, ml = [], [], [], [] + for g in gg: + s, lo = g["short"], g["long"] + cm = usd(s["mark"], s) - usd(lo["mark"], lo) + c = usd(s["bid"], s) - usd(lo["ask"], lo) + if cm <= 0: + continue + fv.append(c / cm); ss.append(rel_spread(s)); ce.append(c) + ml.append(loss_usd(s["K"], lo["K"], s) - c) + w = f"{DTE_LO:.0f}-{hi:.0f}g" + if not fv: + print(f" {f:<10} {w:>9} {len(gg):>5} {'n/d':>8}") + continue + print(f" {f:<10} {w:>9} {len(fv):>5} {np.median(fv):>8.3f} " + f"{np.nanmedian(ss)*100:>10.1f}% {np.median(ce):>11.2f} " + f"{np.median(ml):>9.2f}") + print() + finally: + DTE_HI = orig + print(" 📌 Da leggere sull'ORDINE, non sulla terza cifra: le quote si muovono fra un giro e") + print(" l'altro (su tre snapshot a pochi minuti ETH_USDC ha dato 0.947-0.983, le inverse") + print(" 0.847-0.942, le alt 0.79-0.87, SOL sempre negativo). Cio' che NON si muove e' la") + print(" graduatoria, e alla finestra VERA di VRP01 (4-10g) e' piu' netta, non meno.") + + def profondita(rows: list[dict], fams: list[str], b: Budget) -> None: print("\n" + "=" * 104) print("3b. PROFONDITA' IN CIMA AL BOOK (public/get_order_book depth=5) sulle gambe scelte") diff --git a/scripts/research/r0822_monitor_audit.py b/scripts/research/r0822_monitor_audit.py index a73f1c3..222d0b9 100644 --- a/scripts/research/r0822_monitor_audit.py +++ b/scripts/research/r0822_monitor_audit.py @@ -129,8 +129,10 @@ def read_equity_csv(p: Path, col: int = 1) -> tuple[list[pd.Timestamp], np.ndarr # della giornata e' finita nella serie": in radice di tempo il rapporto di VARIANZA fra le due # serie e' la frazione di giornata misurata (ipotesi: random walk sull'intervallo troncato). # -------------------------------------------------------------------------------------------- -def compare(rec: np.ndarray, rep: np.ndarray) -> dict: +def compare(rec: np.ndarray, rep: np.ndarray, mask: np.ndarray | None = None) -> dict: ok = np.isfinite(rec) & np.isfinite(rep) + if mask is not None: + ok &= mask a, b = rec[ok], rep[ok] n = len(a) if n < 3: @@ -145,18 +147,53 @@ def compare(rec: np.ndarray, rep: np.ndarray) -> dict: sh_rep=float(b.mean() / b.std() * ANN) if b.std() > 0 else float("nan")) -def show(tag: str, c: dict, note: str = "") -> None: +def show(T: pd.DatetimeIndex, rec: np.ndarray, rep: np.ndarray, note: str = "") -> dict: + """Stampa il confronto e ritorna la statistica sulla finestra PULITA (senza l'incidente).""" + c = compare(rec, rep) + fuori = ~((T >= FREEZE[0]) & (T <= FREEZE[1])) + cf = compare(rec, rep, mask=fuori) print(f" barre appaiate {c['n']} identiche entro 1e-6: {c['ident']}/{c['n']} " f"corr {c['corr']:+.4f}") print(f" vol annualizzata REGISTRATA {c['sd_rec']:6.2f}% RICALCOLATA {c['sd_rep']:6.2f}%" f" rapporto di varianza {c['vr']:.4f}") print(f" -> il monitor misura ~{c['minuti']:,.0f} minuti di mercato al giorno (su 1440){note}") + if cf["n"] != c["n"]: + print(f" ESCLUSO l'incidente del feed 09-15/07 ({c['n'] - cf['n']} barre recuperate a " + f"posteriori, quindi CHIUSE):") + print(f" barre {cf['n']} identiche {cf['ident']}/{cf['n']} corr {cf['corr']:+.4f}" + f" vol reg. {cf['sd_rec']:.2f}% vs ric. {cf['sd_rep']:.2f}%" + f" -> ~{cf['minuti']:,.0f} min/giorno") print(f" Sharpe della finestra: REGISTRATO {c['sh_rec']:+.2f} RICALCOLATO {c['sh_rep']:+.2f}" f" [SE ~{np.sqrt(365.0 / max(c['n'], 1)):.2f}: NON e' una lettura di gate]") + return dict(all=c, clean=cf) RES: dict[str, dict] = {} # riepilogo per la tabella finale +# -------------------------------------------------------------------------------------------- +# L'INCIDENTE DEL FEED (2026-07-09 -> 2026-07-15). Non e' un'assunzione: e' documentato nel +# sorgente di produzione (`rebuild_history.py`, commento "incidente 2026-07-15: feed fermo 7g" — +# `shutil.copy2` del backup falliva con EPERM e ABORTIVA il rebuild) ed e' verificabile nel log +# del cron. Conta qui per una ragione precisa: durante il congelamento i monitor NON trovavano +# barre nuove, e al ripristino hanno recuperato in un colpo solo barre gia' CHIUSE — cioe' le +# uniche barre corrette delle loro serie. Vanno escluse da ogni stima del difetto, o si finisce +# per attribuire al difetto un'attenuazione che viene da un guasto diverso. +FREEZE = (pd.Timestamp("2026-07-09", tz="UTC"), pd.Timestamp("2026-07-16", tz="UTC")) +CRONLOG = ROOT / "logs" / "cron_daily.log" + + +def freeze_evidence() -> dict[str, int]: + """Giorni in cui `rebuild_history` e' ABORTITO nel cron (letto dal log, non assunto).""" + out: dict[str, int] = {} + day = None + lines = CRONLOG.read_text(errors="replace").splitlines() if CRONLOG.exists() else [] + for i, ln in enumerate(lines): + if ln.startswith("===== 2026-") and "cron_daily" in ln: + day = ln.split()[1][:10] + if day and "rebuild_history.py" in ln and "Traceback" in "".join(lines[max(0, i - 3):i]): + out[day] = out.get(day, 0) + 1 + return out + # ============================================================================================ # A — PROVENIENZA: quanto e' completa l'ultima riga di ogni sorgente @@ -245,12 +282,14 @@ def b_xsr(): reals={n: px._book(c, len(st["syms"])) for c, n in px.REAL_BOOKS}) px.advance(st0) rep = {int(r["ts"]): float(r["net_modeled"]) for r in sink.of("returns.jsonl")} - pairs = [(float(r["net_modeled"]), rep[int(r["ts"])]) for r in rec if int(r["ts"]) in rep] - c = compare(np.array([p[0] for p in pairs]), np.array([p[1] for p in pairs])) - print(f"\n pannello: `build_panel` -> `load_hl` -> parquet HL 1d (ultima riga = giorno in corso)") - show("xsr", c) - RES["paper_xsr"] = dict(parziale="SI", cmp=c, gate="XSR01 2026-10-23", - src="hl_*_1d.parquet", cad_h=24.0) + pairs = [(int(r["ts"]), float(r["net_modeled"]), rep[int(r["ts"])]) + for r in rec if int(r["ts"]) in rep] + T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True)) + print("\n pannello: `build_panel` -> `load_hl` -> parquet HL 1d (ultima riga = giorno in corso)") + print(f" inception {T[0].date()} = DOPO l'incidente del feed: la finestra e' gia' pulita.") + out = show(T, np.array([p[1] for p in pairs]), np.array([p[2] for p in pairs])) + RES["paper_xsr"] = dict(parziale="SI", cmp=out["clean"], gate="XSR01 2026-10-23", + src="hl_*_1d.parquet", cad_h=24.0, ric="SI (bit-exact)") def b_statarb(): diff --git a/scripts/research/r0822_prop_alloc.py b/scripts/research/r0822_prop_alloc.py index 80875ff..f226d64 100644 --- a/scripts/research/r0822_prop_alloc.py +++ b/scripts/research/r0822_prop_alloc.py @@ -186,10 +186,41 @@ def _d(s: pd.Series) -> pd.Series: _CRY_CACHE: dict = {} -def _crypto(w_tp: float, w_skh: float) -> pd.DataFrame: - key = (round(w_tp, 6), round(w_skh, 6)) +def _crypto_stressed(w_tp: float, w_skh: float, m_tp: float, m_skh: float) -> pd.DataFrame: + """Come `pc.crypto_daily_tuples` (minimo ESATTO sul path orario condiviso) ma con un taglio + del DRIFT applicato alla singola gamba PRIMA di comporre il path. Serve perche' il minimo + giornaliero non e' separabile per gamba: non si puo' tagliare il drift a valle.""" + (J,) = pc._hourly_legs() + legs = {} + for nm, (rc, wc, mult) in {"tp": ("tp_r", "tp_w", m_tp), "sk": ("skh_r", "skh_w", m_skh)}.items(): + r = J[rc].values.copy() + w = J[wc].values.copy() + if mult != 1.0: + cut = (1.0 - mult) * r.mean() + r = r - cut + w = w - cut + legs[nm] = (r, w) + r_h = w_tp * legs["tp"][0] + w_skh * legs["sk"][0] + w_h = w_tp * legs["tp"][1] + w_skh * legs["sk"][1] + 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))) + return pd.DataFrame(rows, columns=["day", "R", "m"]).set_index("day") + + +def _crypto(w_tp: float, w_skh: float, m_tp: float = 1.0, m_skh: float = 1.0) -> pd.DataFrame: + key = (round(w_tp, 6), round(w_skh, 6), round(m_tp, 4), round(m_skh, 4)) if key not in _CRY_CACHE: - _CRY_CACHE[key] = pc.crypto_daily_tuples(w_tp, w_skh) + _CRY_CACHE[key] = (pc.crypto_daily_tuples(w_tp, w_skh) if (m_tp == 1.0 and m_skh == 1.0) + else _crypto_stressed(w_tp, w_skh, m_tp, m_skh)) return _CRY_CACHE[key] @@ -221,11 +252,8 @@ def book_RG(w: dict, start: str = START_WIN, R = np.zeros(len(idx)) M = np.zeros(len(idx)) if w_tp > 0 or w_skh > 0: - c = _crypto(w_tp, w_skh).reindex(idx) - cr = c["R"].values - if w_tp > 0 and dm.get("TP01", 1.0) != 1.0 or w_skh > 0 and dm.get("SKH01", 1.0) != 1.0: - raise NotImplementedError("stress sul drift crypto: il minimo e' path-esatto, non separabile") - R += cr + c = _crypto(w_tp, w_skh, dm.get("TP01", 1.0), dm.get("SKH01", 1.0)).reindex(idx) + R += c["R"].values M += c["m"].values for nm, fr in (("XS01", pc.xsec_daily_tuples()), ("VRP01", vrp_daily_tuples()), ("GTAA01", gtaa_daily_tuples())): @@ -702,6 +730,28 @@ def main() -> None: print(" scoperta; se resta a drift 0, il vantaggio e' pura DECORRELAZIONE e non dipende da") print(" quell'ipotesi. Il numero, non la narrazione, dice quale delle due.") + hr("E-quater. LA DISTORSIONE CHE MI RIGUARDA — l'ottimo si appoggia a SKH01, e SKH01 e' lo") + print(" sleeve con la FORTUNA D'ANCORA piu' grande da restituire. Il LOO del 26/07: l'ancora") + print(" canonica gli regala 2/3 del contributo FULL (+0.43 -> +0.14) e il 70% dell'hold-out;") + print(" l'audit 02/07 lo trova al 93-98° pctl di 23 offset, 'spike, non plateau'. Questa lente") + print(" gira sull'ancora CANONICA e la banda non e' ri-misurabile qui -> il mio ottimo e'") + print(" distorto A FAVORE di SKH01 e va detto PRIMA di leggerlo. Taglio del drift di SKH01:") + print(f"\n {'drift SKH01':>13} {'config':>16} {'leva':>6} {'J':>7} {'J del LIVE':>11} " + f"{'delta appaiato':>16}") + for mult in (1.00, 0.66, 0.50, 0.33): + dmt = (("SKH01", mult),) + rows = [(w, lv, objective(w, lv, "HYRO", ev_idx, fu_idx, drift_mult=dmt)) + for w in top_w for lv in LEVS] + w_, lv_, o_ = max(rows, key=lambda x: x[2]["J"]) + lvL, oL = max(((lv, objective({"TP01": 0.75, "SKH01": 0.25}, lv, "HYRO", ev_idx, fu_idx, + drift_mult=dmt)) for lv in LEVS), key=lambda x: x[1]["J"]) + dd_, sd_ = paired_delta(w_, lv_, {"TP01": 0.75, "SKH01": 0.25}, lvL, "HYRO", + ev_idx, fu_idx) + print(f" {mult:>13.2f} {wkey(w_):>16} {lv_:>6.3f} {o_['J']:>7.3f} {oL['J']:>11.3f} " + f"{dd_:>+11.3f} +-{sd_:.3f}") + print(" NB: il taglio si applica anche al LIVE 75/25 (che ha SKH01 al 25%), quindi il delta e'") + print(" un confronto onesto: entrambe le colonne perdono, la domanda e' quale perde di piu'.") + hr("E-ter. IL BUCO CHE LA FINESTRA NON PUO' CHIUDERE — 2024-2026 non contiene un CRASH") print(" L'ottimizzazione toglie peso a TP01 (Sharpe 0.79 su questa finestra). Ma TP01 e' lo") print(" sleeve DIFENSIVO: il LOO del 26/07 misura il suo contributo hold-out NEGATIVO nel 99.1%") diff --git a/scripts/research/r0822_volsize_skeptic.py b/scripts/research/r0822_volsize_skeptic.py index 324327d..2559bfd 100644 --- a/scripts/research/r0822_volsize_skeptic.py +++ b/scripts/research/r0822_volsize_skeptic.py @@ -252,9 +252,10 @@ def main() -> None: FUN_V1["VTL tv20 w90 (LEAD)"] = size_vtl(FROZEN) FUN_V1["BIN L>=1 (0/1)"] = size_bin(FROZEN, "ge1") - NTRIAL = len(FUN_V2) + len(FUN_V1) + 2 # +2 baseline - print(f"\n GRIGLIA DICHIARATA: {len(FUN_V2)} celle su V2-DD + {len(FUN_V1)} su V1 " - f"+ 2 baseline = {NTRIAL} valutazioni x {len(offs)} ancore. Nessuna cella scelta") + NTRIAL = len(FUN_V2) + len(FUN_V1) + 2 + 4 # +2 baseline, +4 controlli 'L combinata' (§1c) + print(f"\n GRIGLIA DICHIARATA (contata al RIALZO): {len(FUN_V2)} celle su V2-DD + " + f"{len(FUN_V1)} su V1 + 4 controlli 'L combinata' (§1c) + 2 baseline = {NTRIAL} " + f"valutazioni x {len(offs)} ancore. Nessuna cella scelta") print(" dopo aver guardato: IV e' la famiglia PRE-REGISTRATA, VTL/w e' l'asse dichiarato") print(" fragile dall'agente, BIN/DROP sono controlli miei dichiarati sopra.") From fe6ad972254773f087dad3cfddf3ea011835791c Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:46:13 +0000 Subject: [PATCH 019/114] =?UTF-8?q?research(wave-0822):=20ALT-OPT=20?= =?UTF-8?q?=E2=80=94=20l'open=20interest=20e=20la=20negoziabilita'=20sono?= =?UTF-8?q?=20anti-correlati=20(rho=20=E2=88=920,77);=20corretta=20la=20mi?= =?UTF-8?q?a=20inferenza=20su=20BTC=5FUSDC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/research/r0822_monitor_audit.py | 139 +++++++++++++++++------- 1 file changed, 100 insertions(+), 39 deletions(-) diff --git a/scripts/research/r0822_monitor_audit.py b/scripts/research/r0822_monitor_audit.py index 222d0b9..1c528ee 100644 --- a/scripts/research/r0822_monitor_audit.py +++ b/scripts/research/r0822_monitor_audit.py @@ -310,13 +310,30 @@ def b_statarb(): dd_modeled=0.0, dd_real=0.0, n_trades=0) ps.advance(st0, j) rep = {int(r["ts"]): float(r["net_modeled"]) for r in sink.of("returns.jsonl")} - pairs = [(float(r["net_modeled"]), rep[int(r["ts"])]) for r in rec if int(r["ts"]) in rep] - c = compare(np.array([p[0] for p in pairs]), np.array([p[1] for p in pairs])) + pairs = [(int(r["ts"]), float(r["net_modeled"]), rep[int(r["ts"])]) + for r in rec if int(r["ts"]) in rep] + T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True)) + A = np.array([p[1] for p in pairs]); B = np.array([p[2] for p in pairs]) print("\n pannello: `build_joint('1d')` -> `altlib.get(...,'1d')` -> `resample_tf` " "(ultima riga = 1 barra 1h su 24)") - show("statarb", c) - RES["paper_statarb"] = dict(parziale="SI", cmp=c, gate="STATARB 2026-09-27", - src="btc/eth 1h -> resample 1d", cad_h=24.0) + out = show(T, A, B) + ident = np.abs(A - B) <= 1e-6 + if ident.any(): + gg = sorted({str(x.date()) for x in T[ident]}) + print(f"\n 📌 LE UNICHE BARRE CORRETTE DELLA SERIE VENGONO DA UN GUASTO: le {int(ident.sum())} " + f"identiche\n sono CONSECUTIVE ({gg[0]} -> {gg[-1]}) e cadono dentro l'incidente " + "del feed.") + ev = freeze_evidence() + print(f" Prova indipendente dal log del cron: `rebuild_history` e' abortito in " + f"{len(ev)} giri\n ({', '.join(sorted(ev)[:8])}{'...' if len(ev) > 8 else ''}) — " + "EPERM sul backup, difetto gia' riparato\n nel sorgente. Col feed fermo il monitor " + "non trovava barre nuove; al ripristino ne ha\n recuperate 7 in un colpo solo, di " + "cui 6 GIA' CHIUSE -> coincidono col replay al bit.") + print(" Due conseguenze: (a) il 'min/giorno' globale e' GONFIATO da quelle barre — la") + print(" riga 'ESCLUSO l'incidente' e' la stima onesta; (b) quelle 6 barre sono la prova") + print(" che, su barre chiuse, il codice di produzione riproduce il replay ESATTAMENTE.") + RES["paper_statarb"] = dict(parziale="SI", cmp=out["clean"], gate="STATARB 2026-09-27", + src="btc/eth 1h -> resample 1d", cad_h=24.0, ric="SI (bit-exact)") def b_dvolspread(): @@ -337,17 +354,27 @@ def b_dvolspread(): dd_modeled=0.0, dd_real=0.0, n_flips=0, frozen=pv.FROZEN) pv.advance(st0, P) rep = {int(r["ts"]): float(r["net_modeled"]) for r in sink.of("returns.jsonl")} - pairs = [(float(r["net_modeled"]), rep[int(r["ts"])]) for r in rec if int(r["ts"]) in rep] - c = compare(np.array([p[0] for p in pairs]), np.array([p[1] for p in pairs])) + pairs = [(int(r["ts"]), float(r["net_modeled"]), rep[int(r["ts"])]) + for r in rec if int(r["ts"]) in rep] + T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True)) print("\n pannello: `_panel` -> `ortholib.aligned` -> `altlib.get(...,'1d')` -> `resample_tf`") - show("dvolspread", c) + print(f" inception {T[0].date()} = dopo l'incidente del feed: finestra gia' pulita, " + "nessuna barra recuperata.") + out = show(T, np.array([p[1] for p in pairs]), np.array([p[2] for p in pairs])) + print("\n ⚠ E' IL PEGGIORE DEI SEI, e non e' un caso: il libro e' uno SPREAD BTC-ETH e su 35") + print(" minuti le due gambe si muovono quasi insieme -> la parte idiosincratica, che e' cio'") + print(" che questo libro tenta di catturare, e' quasi tutta ancora da venire quando la barra") + print(" viene letta. Un libro dollar-neutral perde piu' di un libro direzionale a parita' di") + print(" troncamento — il numero di sopra non e' 35/1440 ma un ordine di grandezza sotto.") print("\n ⚠ La contabilita' a 3 stati (ATTIVE / flat-da-segnale / flat-SENZA-DATO) e il veto") print(" d'integrita' all'80% funzionano e restano validi: contano se il DVOL c'era. Non") print(" possono vedere QUANTO dura la barra su cui il DVOL c'era — sono ortogonali a questo") print(" difetto, ed e' il motivo per cui l'unico monitor con una guardia dedicata e'") print(" compromesso quanto quelli senza.") - RES["paper_dvolspread"] = dict(parziale="SI", cmp=c, gate="DVOLSPREAD 2026-10-24 / 2027-01-24", - src="btc/eth 1h -> resample 1d + dvol", cad_h=24.0) + RES["paper_dvolspread"] = dict(parziale="SI", cmp=out["clean"], + gate="DVOLSPREAD 2026-10-24 / 2027-01-24", + src="btc/eth 1h -> resample 1d + dvol", cad_h=24.0, + ric="SI (bit-exact)") def b_prevday(): @@ -370,28 +397,39 @@ def b_prevday(): pairs = [(int(r["ts"]), float(r["net_modeled"]), rep[int(r["ts"])]) for r in rec if int(r["ts"]) in rep] A = np.array([p[1] for p in pairs]); B = np.array([p[2] for p in pairs]) - c = compare(A, B) + T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True)) print("\n pannello: `build_bars` -> `harness.load(asset,'1h')` = il parquet 1h GREZZO, " "nessun resample") - show("prevday", c, note=" <- 24 barre/giorno, quindi il conto e' sull'INTERA giornata") + out = show(T, A, B, note=" <- 24 barre/giorno, quindi il conto e' sull'INTERA giornata") diff = np.abs(A - B) > 1e-6 - hh = pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True).hour print(f"\n DOVE cade la divergenza: {int(diff.sum())} barre su {len(A)} " - f"({diff.mean() * 100:.1f}%)") - vc = pd.Series(hh[diff]).value_counts().sort_index() + f"({diff.mean() * 100:.1f}%), |scarto| mediano " + f"{np.median(np.abs(A - B)[diff]):.2e} su |ritorno| mediano {np.median(np.abs(B)):.2e}") + vc = pd.Series(T.hour[diff]).value_counts().sort_index() print(" ora UTC delle barre divergenti: " + ", ".join(f"{int(h):02d}:00 x{int(n)}" for h, n in vc.items())) - giorni = len(set(pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True).date)) - print(f" = ESATTAMENTE una barra per giorno su {giorni} giorni, e sempre la barra 00:00 —") - print(" cioe' quella che il cron delle 00:30 trova aperta da ~35 minuti.") - print(f"\n ⚠ LA MISURA CHE CONTA E' L'ALTRA META': {int((~diff).sum())} barre su {len(A)} " - "coincidono BIT A BIT con") - print(" il replay fatto oggi. Non e' un dettaglio contabile: e' la prova indipendente che il") - print(" feed BTC/ETH certificato NON riscrive le barre gia' chiuse (62 giorni di riscritture") - print(" notturne). Senza questa prova la ricostruzione della sezione E sarebbe un'ipotesi.") - RES["paper_prevday"] = dict(parziale="SI", cmp=c, gate="nessuno (lead in monitor)", - src="btc/eth 1h grezzo", cad_h=1.0) + g = pd.DataFrame(dict(day=T.date, d=diff)).groupby("day")["d"].sum() + z, uno, due = (g == 0), (g == 1), (g == 2) + print(f" per giorno: {int(uno.sum())} giorni con 1 barra divergente, {int(due.sum())} con 2, " + f"{int(z.sum())} con 0 (totale {len(g)} giorni)") + print(f" i {int(due.sum())} giorni con DUE sono sempre 00:00 + 01:00 = il difetto si PROPAGA " + "una barra avanti,") + print(" perche' il bersaglio calcolato sulla barra parziale e' la posizione TENUTA in quella") + print(" dopo. (Il docstring di `prevday_breakout.current_target` dice 'ultima barra chiusa':") + print(" quinto docstring di produzione che dichiara una causalita' che il codice non ha.)") + zg = [str(x) for x in g[z].index] + print(f" i {int(z.sum())} giorni con ZERO divergenze: {', '.join(zg)}") + print(" = inception + oggi + i 7 giorni dell'incidente del feed (barre recuperate CHIUSE).") + print(f"\n ⚠ LA META' CHE CONTA DI PIU': {int((~diff).sum())} barre su {len(A)} coincidono BIT " + "A BIT col replay") + print(" fatto oggi, dopo 62 notti di riscrittura del parquet. E' la PROVA che il feed BTC/ETH") + print(" certificato non tocca le barre gia' chiuse — senza la quale la ricostruzione della") + print(" sezione E sarebbe un'ipotesi invece di un fatto.") + print(" 📌 E il verdetto operativo e' l'opposto degli altri quattro: su griglia oraria il difetto") + print(" c'e' ma vale ~0.1% della varianza. `paper_prevday` NON e' da rigenerare.") + RES["paper_prevday"] = dict(parziale="SI", cmp=out["clean"], gate="nessuno (lead in monitor)", + src="btc/eth 1h grezzo", cad_h=1.0, ric="SI (bit-exact)") def b_portfolio(): @@ -402,22 +440,29 @@ def b_portfolio(): dts, eq = read_equity_csv(ROOT / "data" / "paper_portfolio" / "equity.csv") _, r = ppf.portfolio_daily() ridx = {pd.Timestamp(k): float(v) for k, v in r.items()} - A, B = [], [] + A, B, D = [], [], [] for i in range(1, len(dts)): d = dts[i] if d in ridx: A.append(eq[i] / eq[i - 1] - 1.0) B.append(ridx[d]) - c = compare(np.array(A), np.array(B)) + D.append(d) + T = pd.DatetimeIndex(D) print("\n serie: `StrategyPortfolio(active_sleeves()).combined_daily()`; i rendimenti " "registrati si\n ricavano da equity.csv (il monitor salva l'equity, non i ritorni)") - show("portfolio", c) + out = show(T, np.array(A), np.array(B)) print("\n ⚠ ULTIMA RIGA ESCLUSA DAL GIUDIZIO? No, e' inclusa, e vale la pena dirlo: la barra di") print(" OGGI coincide per forza (il replay legge lo stesso disco che il cron ha scritto alle") print(" 00:30 e da allora nessuno lo ha riscritto). Un audit che girasse SOLO sull'ultima") print(" barra concluderebbe 'tutto a posto'. La potenza sta nelle barre di IERI e prima.") - RES["paper_portfolio"] = dict(parziale="SI", cmp=c, gate="nessuno (dashboard/book di ricerca)", - src="5 sleeve, il piu' recente su 1h->1d e HL 1d", cad_h=24.0) + print(" ⚠ Ricostruzione NON bit-exact per questo monitor: uno dei cinque sleeve e' GTAA01, che") + print(" legge `eq_*_1d` da IB con `ADJUSTED_LAST` — ri-aggiustato all'indietro a ogni stacco") + print(" di dividendo (difetto gia' incontrato il 07/08 su TLT). La finestra si ricostruisce,") + print(" ma la parte equity non tornera' identica al centesimo, e va detto.") + RES["paper_portfolio"] = dict(parziale="SI", cmp=out["clean"], + gate="nessuno (dashboard/book di ricerca)", + src="5 sleeve, il piu' recente su 1h->1d e HL 1d", cad_h=24.0, + ric="SI* (equity IB ri-aggiustata)") def b_combo(): @@ -428,15 +473,22 @@ def b_combo(): dts, eq = read_equity_csv(ROOT / "data" / "paper_combo" / "equity.csv", col=1) naked, _ = pc.both_daily() ridx = {pd.Timestamp(k): float(v) for k, v in naked.items()} - A, B = [], [] + A, B, D = [], [], [] for i in range(1, len(dts)): d = dts[i] if d in ridx: A.append(eq[i] / eq[i - 1] - 1.0) B.append(ridx[d]) - c = compare(np.array(A), np.array(B)) + D.append(d) + A = np.array(A); B = np.array(B); T = pd.DatetimeIndex(D) print("\n serie: `combo_daily()` = TP01 compoundato sulla griglia dei GIORNI DI BORSA + GTAA") - show("combo", c) + out = show(T, A, B) + dif = np.abs(A - B) + print(f"\n Le barre non identiche NON sono il difetto della barra parziale: |scarto| mediano " + f"{np.median(dif):.2e}\n contro |ritorno| mediano {np.median(np.abs(B)):.2e} " + f"(= {np.median(dif) / np.median(np.abs(B)) * 100:.0f}% del ritorno tipico, massimo " + f"{dif.max():.2e}) e si concentrano\n nella finestra dell'incidente del feed + nei giorni " + "in cui IB ha ri-aggiustato `ADJUSTED_LAST`.") tp_last = pd.Timestamp(list(ridx.keys())[-1]) print(f"\n PERCHE' QUESTO E' DIVERSO. La griglia e' `eq.index` (i giorni di borsa di IB),") print(f" non il calendario crypto: l'ultima riga della serie e' {tp_last.date()}, mentre il") @@ -448,8 +500,9 @@ def b_combo(): print(" chiusa. Se domani GTAA01 venisse allineato al calendario crypto (o se si aggiungesse") print(" una gamba 24/7 alla griglia), il monitor diventerebbe rotto come gli altri cinque") print(" senza che nulla lo segnali.") - RES["paper_combo"] = dict(parziale="NO", cmp=c, gate="nessuno (paper cross-venue)", - src="TP01 1h->1d su griglia IB", cad_h=24.0) + RES["paper_combo"] = dict(parziale="NO", cmp=out["clean"], gate="nessuno (paper cross-venue)", + src="TP01 1h->1d su griglia IB", cad_h=24.0, + ric="SI* (equity IB ri-aggiustata)") # ============================================================================================ @@ -469,15 +522,23 @@ def sezione_C(): print("\n" + "=" * 102) print(" C — TABELLA: una riga per monitor") print("=" * 102) + print("\n Le colonne sono calcolate ESCLUDENDO l'incidente del feed 09-15/07 (le barre") + print(" recuperate a posteriori sono chiuse e attenuerebbero il difetto per la ragione") + print(" sbagliata). 'min/g' = minuti di mercato al giorno effettivamente misurati, su 1440.") print(f"\n {'monitor':<19}{'parz.':>6}{'ident./tot':>13}{'corr':>9}{'min/g':>9}" - f"{'ricostr.':>10} gate dipendente") + f"{'ricostruibile':>25} gate dipendente") for k, v in RES.items(): c = v["cmp"] ratio = "{}/{}".format(c["ident"], c["n"]) print(f" {k:<19}{v['parziale']:>6}{ratio:>13}{c['corr']:>+9.3f}" - f"{c['minuti']:>9,.0f}{'SI':>10} {GATE_INFO[k][0]}") - print("\n 'ricostr.' = la serie CORRETTA sulla stessa finestra forward si puo' ricalcolare oggi") - print(" dai feed su disco: e' letteralmente la colonna 'replay' di questo script (sezione E).") + f"{c['minuti']:>9,.0f}{v['ric']:>25} {GATE_INFO[k][0]}") + print("\n 'ricostruibile' = la serie CORRETTA sulla stessa finestra forward si puo' ricalcolare") + print(" oggi dai feed su disco: e' letteralmente la colonna 'replay' di questo script. '*' =") + print(" ricostruibile ma NON al bit, perche' una gamba viene dal feed equity IB che e'") + print(" ri-aggiustato all'indietro (`ADJUSTED_LAST`).") + print("\n LETTURA IN UNA RIGA: quattro monitor su sei registrano fra 2 e 41 minuti di mercato") + print(" al giorno spacciandoli per 24 ore; i due che non lo fanno sono quello a griglia oraria") + print(" (dove il difetto vale una barra su 24) e quello che aspetta la chiusura di una BORSA.") # ============================================================================================ From 987569c5a3c2394800f90e31a10cfe9768f43267 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:46:22 +0000 Subject: [PATCH 020/114] =?UTF-8?q?research(wave-0822):=20corretta=20la=20?= =?UTF-8?q?mia=20inferenza=20su=20BTC=5FUSDC=20=E2=80=94=20l'OI=20non=20mi?= =?UTF-8?q?sura=20la=20negoziabilita'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index e656596..82ac238 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -177,8 +177,10 @@ l'open interest famiglia per famiglia (`get_book_summary_by_currency`, misura mi | ETH inverse | 932 | **548** | | **BTC_USDC** | 686 | **5** | | **ETH_USDC** | 660 | **119** | --> **BTC_USDC e' praticamente morto** (5 strumenti liquidi su 686). **Il muro sul BTC RESTA, ma per -la ragione giusta e meglio misurata: non la taglia del lotto, la LIQUIDITA'.** ETH_USDC (119 +-> conteggio corretto come FATTO, **ma l'inferenza che ne ho tratto io ("BTC_USDC e' praticamente +morto") e' SBAGLIATA**: vedi filone 14 — su queste famiglie l'OI e la negoziabilita' sono +**anti-correlati**, e BTC_USDC ha il **93%** dei put quotati a due lati. **Il muro sul BTC resta +per il PREZZO del lotto ($772 su un conto da $635), non per la liquidita'.** strumenti, ~$250/lotto) e' l'unica gamba che varrebbe la pena raccogliere. 📌 Sottoprodotto fuori perimetro: esiste un universo opzioni USDC piu' liquido di BTC_USDC — SOL_USDC 341, XRP_USDC 250, HYPE_USDC 172, AVAX_USDC 146 strumenti con OI>=100. From cef559b350d13a89f14bb8855da2fcdbc95291b7 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:46:33 +0000 Subject: [PATCH 021/114] =?UTF-8?q?research(wave-0822):=20ritirato=20anche?= =?UTF-8?q?=20il=20sottoprodotto=20sull'universo=20USDC=20'piu'=20liquido'?= =?UTF-8?q?=20=E2=80=94=20piu'=20OI=20e=20meno=20quote?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 9 ++++++--- scripts/research/r0822_monitor_audit.py | 14 +++++++++++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 82ac238..d78570b 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -181,9 +181,12 @@ l'open interest famiglia per famiglia (`get_book_summary_by_currency`, misura mi morto") e' SBAGLIATA**: vedi filone 14 — su queste famiglie l'OI e la negoziabilita' sono **anti-correlati**, e BTC_USDC ha il **93%** dei put quotati a due lati. **Il muro sul BTC resta per il PREZZO del lotto ($772 su un conto da $635), non per la liquidita'.** -strumenti, ~$250/lotto) e' l'unica gamba che varrebbe la pena raccogliere. -📌 Sottoprodotto fuori perimetro: esiste un universo opzioni USDC piu' liquido di BTC_USDC — -SOL_USDC 341, XRP_USDC 250, HYPE_USDC 172, AVAX_USDC 146 strumenti con OI>=100. +ETH_USDC resta la gamba giusta su cui puntare, ma per **spread e prezzo del lotto** (filone 14), +non per il conteggio di OI. +❌ **Sottoprodotto RITIRATO:** avevo scritto che *"esiste un universo opzioni USDC piu' liquido di +BTC_USDC (SOL 341, XRP 250, HYPE 172, AVAX 146 con OI>=100)"*. **Falso nel senso che serve:** quelle +famiglie hanno piu' posizioni aperte e **meno quote** — SOL_USDC ha il **9%** dei put quotati a due +lati contro il **93%** di BTC_USDC, e il suo miglior bid sta a **un tick**. - **Il campione misura la strategia mentre STA FERMA**: 0/19 settimane passano il gate IV-rank>0.30, DVOL alla mediana del 7°/11° percentile storico, sottostante +25%/+44%. - ✅ **Replica indipendente del f del 30/07 su campione piu' lungo: f = 0,714 pooled** (IC95 diff --git a/scripts/research/r0822_monitor_audit.py b/scripts/research/r0822_monitor_audit.py index 1c528ee..5f30ecd 100644 --- a/scripts/research/r0822_monitor_audit.py +++ b/scripts/research/r0822_monitor_audit.py @@ -450,7 +450,19 @@ def b_portfolio(): T = pd.DatetimeIndex(D) print("\n serie: `StrategyPortfolio(active_sleeves()).combined_daily()`; i rendimenti " "registrati si\n ricavano da equity.csv (il monitor salva l'equity, non i ritorni)") - out = show(T, np.array(A), np.array(B)) + A = np.array(A); B = np.array(B) + out = show(T, A, B) + ident = np.abs(A - B) <= 1e-6 + zeri = ident & (np.abs(B) <= 1e-12) + if ident.any(): + print(f"\n ⚠ TRAPPOLA DA NON PRENDERE: delle {int(ident.sum())} barre 'identiche', " + f"{int(zeri.sum())} sono ZERO contro ZERO") + print(" (" + ", ".join(str(x.date()) for x in T[zeri]) + ") = giorni in cui TUTTI e cinque") + print(" gli sleeve erano flat (TP01 risk-off, GTAA a borsa chiusa, VRP fuori scadenza).") + print(" Contarle come 'coincidono' sarebbe la trappola gia' codificata dal progetto —") + print(" barre ATTIVE, non barre di calendario (26/07, il gate che segnalo' VRP01 perche'") + print(" contava il 94% di zeri). Su barre attive le coincidenze qui sono " + f"{int((ident & ~zeri).sum())}.") print("\n ⚠ ULTIMA RIGA ESCLUSA DAL GIUDIZIO? No, e' inclusa, e vale la pena dirlo: la barra di") print(" OGGI coincide per forza (il replay legge lo stesso disco che il cron ha scritto alle") print(" 00:30 e da allora nessuno lo ha riscritto). Un audit che girasse SOLO sull'ultima") From fbb6e75e67e9dc586c3ec847a312fe6e855335f5 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:52:53 +0000 Subject: [PATCH 022/114] =?UTF-8?q?research(wave-0822):=20MONITOR-AUDIT=20?= =?UTF-8?q?=E2=80=94=204=20monitor=20su=206=20rotti,=20il=20gate=20STATARB?= =?UTF-8?q?=20si=20ribalta,=20ma=20la=20finestra=20e'=20rigenerabile=20e?= =?UTF-8?q?=20nessuna=20data=20si=20sposta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 48 ++++++ scripts/research/r0822_monitor_audit.py | 209 +++++++++++++++++++----- scripts/research/r0822_prop_alloc.py | 32 ++-- 3 files changed, 234 insertions(+), 55 deletions(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index d78570b..1918bf3 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -21,6 +21,7 @@ null de-levering superato + eseguibilita' al capitale dichiarato. | — | **SLIP-AUDIT** (audit di fatto) | **SCARTATO** = nessun costo nascosto a questa taglia | i backtest a 10 bps RT restano **conservativi di ~1,5 bps/lato**; nessuna evidenza di impatto sopravvive al null (p=0,274; estremi avversi **1/18 contro 4,7 attesi**). **Ma la misura ha una data di scadenza** | | — | **SCETTICO su GROWTH-POLICY** | **REGGE** (condizione bloccante DECADUTA) | la lente wick accoppiata **non cambia il gradino**: e' un ricarico moltiplicativo **costante ~3,5%** sul maxDD, non un'amplificazione (Δ/k **cala**). Ma su una regola a **UN giorno** close-only e' **esattamente cieca** (rapporto INF) -> la lezione 25/07 e' vera e **non si trasferisce** | | 13 | BASIS-CALENDAR | **SCARTATO** + **1 dataset nuovo** | il basis dei futures datati **E' il funding del perp quotato diversamente** (7,33% contro 6,48%/anno): non e' un quarto lato del filone funding, e' lo stesso lato. Premio incassabile **+0,85%/anno con IC95 che contiene lo zero** | +| — | **MONITOR-AUDIT** (produzione) | 🚨 **4 monitor su 6 rotti**, ma **la finestra NON e' persa** | `paper_statarb` registra **4 minuti al giorno** e il suo gate del **27/09 si ribalta su 2 criteri su 3** (Sharpe registrato **+1,96** contro **−2,02** ricostruito). **La riparazione e' "riparare + RIGENERARE", non "riparare e azzerare": nessuna data di gate si sposta** | ## Note che sopravvivono ai singoli filoni @@ -590,3 +591,50 @@ formula ma il binario**: meno gradi di liberta', **dimezza gli ordini** (miglior $635), 115% sul FULL — ma paga in fragilita' d'ancora (20/23 contro 23/23, banda che include −0,061, hold-out +0,072 contro +0,101). **Resta un terzo della fortuna d'ancora del libro stesso (+0,196): niente qui giustifica di toccare pesi, cron o config.** + +### MONITOR-AUDIT (r0822_monitor_audit.py) — audit di PRODUZIONE, 6 monitor, 4,7 s +✅ **Metodo che rende il risultato non contestabile:** l'audit **riesegue `advance()` di produzione +tale e quale**, sostituendo solo `_append` con un raccoglitore in memoria -> nessuna +reimplementazione, quindi **nessun drift fra audit e monitor**. Sola lettura verificata (gli mtime +di `data/paper_*` sono rimasti quelli del cron delle 00:35). +| monitor | barra parziale | barre coincidenti | corr | **min/giorno misurati** | gate | ricostruibile | +|---|---|---|---|---|---|---| +| `paper_statarb` | **SI'** | **0/46** | +0,297 | **4** | **STATARB 27/09** | **SI', bit-exact** | +| `paper_dvolspread` | **SI'** | 0/28 | +0,262 | **2** | DVOLSPREAD 24/10 | SI', bit-exact | +| `paper_xsr` | **SI'** | 1/28 | −0,045 | **41** | XSR01 23/10 | SI', bit-exact | +| `paper_portfolio` | **SI'** | 7/56 | +0,245 | 19 | — (dashboard) | si', non al bit | +| `paper_prevday` | si' (1 barra su 24) | **1259/1319** | **+0,997** | 1.438 | — | si', bit-exact | +| `paper_combo` | **NO** | 7/37 | +0,991 | 1.402 | — | si', non al bit | +📌 **La patologia e' la stessa, la TAGLIA varia di un fattore 20** — e dipende da quanto pesa la +barra piu' recente nel segnale, esattamente come la lezione del 26/07 su SKH01 contro TP01. +🚨 **Il gate STATARB del 27/09 si ribalta su DUE criteri su tre:** Sharpe registrato **+1,96** (sopra +la soglia 0,5) contro **−2,02** ricostruito; maxDD **2,2% contro 15,3%** (guardia <10%). *Fra 36 +giorni si sarebbe deciso su una serie che misura 4 minuti di mercato al giorno.* +⚠️ XSR01 e DVOLSPREAD danno lo **stesso verdetto** oggi, ma su numeri implausibili (Sharpe −15,8 su +28 barre = oltre 4 SE). **`implausible_sharpe` esiste dal 26/07 e non e' mai stato puntato sulle +serie forward.** +✅ **LA BUONA NOTIZIA, ed e' misurata: la finestra forward NON va persa.** Il replay *e'* la serie +corretta sulla stessa finestra, con config congelata e codice di produzione. Le due condizioni sono +**provate due volte in modo indipendente**: (a) `paper_prevday` ha **1427/1488 barre identiche al +bit** dopo 62 notti di riscrittura (il feed **non tocca** le barre chiuse); (b) le 6 barre di +`paper_statarb` recuperate dopo un guasto coincidono **al bit**. +-> **La decisione non e' "riparare e azzerare" ma "riparare `advance()` + RIGENERARE", e nessuna +delle tre date di gate si sposta.** Anche nell'ipotesi pessimista (`--reset`) l'unico gate che +slitterebbe e' STATARB, di **+18 giorni**. +📌 **Dettaglio che vale una regola: le uniche barre corrette di `paper_statarb` esistono perche' il +feed si e' ROTTO.** `rebuild_history` e' abortito per 7 giri consecutivi (09-15/07, EPERM sul +backup, gia' riparato); al ripristino il monitor ha recuperato 7 barre in un colpo, **6 delle quali +gia' chiuse** -> coincidono col replay al bit. *Il guasto ha prodotto da solo il controllo che +serviva a diagnosticare l'altro guasto.* +📌 **`paper_combo` e' sano PER CASO:** aspetta la chiusura di una **borsa**, non per progetto. +Nessuna riga di codice chiede la barra chiusa — **se la sua griglia cambiasse diventerebbe rotto in +silenzio.** +✅ **Guardia raccomandata (NON implementata), e ha il pregio di essere banale:** +`ts_ultima_barra + cadenza <= mtime del file di serie`, grazia 5 min. **O(1)**, nessuna strategia da +rieseguire, usa solo cio' che il monitor gia' scrive; segnala **5/6** e **tace sul sano**; controllo +positivo sintetico **6/6 nei due versi**. Il vol-ratio va **dopo, non al posto**: copre un guasto +diverso (drift monitor-strategia) e **non vede `paper_prevday`** (rapporto 0,999). +⚠️ Limiti dichiarati: i "minuti/giorno" sono una lettura del **rapporto di varianza** (assume random +walk sull'intervallo troncato), non un cronometro — il cronometro diretto e' 32-35 min; e per +`paper_portfolio`/`paper_combo` la ricostruzione **non sara' al bit** perche' la gamba GTAA legge +`ADJUSTED_LAST` di IB, ri-aggiustato all'indietro. diff --git a/scripts/research/r0822_monitor_audit.py b/scripts/research/r0822_monitor_audit.py index 5f30ecd..9cf2f57 100644 --- a/scripts/research/r0822_monitor_audit.py +++ b/scripts/research/r0822_monitor_audit.py @@ -9,7 +9,7 @@ vol registrata 0,46% contro 2,74% ricalcolata. Il difetto non e' di XSR01: e' della FORMA `new = [i for i in range(len(ts)) if ts[i] > last_ts]` applicata a un pannello la cui ultima riga e' provvisoria. Quella forma e' copiata in sei file. Tre decisioni PRE-REGISTRATE si leggono su queste serie: - STATARB 2026-09-27 (r0724_statarb_deploy_gate: Sharpe di net_modeled >= 0.35) + STATARB 2026-09-27 (r0724_statarb_deploy_gate: Sharpe di net_modeled >= 0.5) XSR01 2026-10-23 (r0725_xsr_deploy_gate: Sharpe >= 1.0 E haircut $5.000 <= 40%) DVOLSPREAD 2026-10-24 kill / 2027-01-24 decisione (soglie dentro paper_dvolspread) e `monitor_health` dichiara **OK** tutti e sei, perche' misura freschezza e buchi: una serie @@ -42,11 +42,18 @@ Poi: E. IL COSTO DELLA RIPARAZIONE per gate: giorni di finestra persi, data in cui il contatore nuovo tornerebbe alla stessa numerosita', e se esiste una RICOSTRUZIONE a posteriori. -COSA MI ASPETTAVO PRIMA DI MISURARE: che i tre monitor giornalieri su feed crypto (xsr, statarb, -dvolspread) avessero la stessa patologia identica; che `paper_prevday`, essendo su griglia 1h con -24 barre al giorno, ne avesse una MOLTO piu' mite (una barra su 24 troncata); e che -`paper_combo`, che vive sul calendario di borsa, fosse o rotto come gli altri o sano per caso. -Solo la terza aspettativa e' stata una sorpresa. +COSA MI ASPETTAVO PRIMA DI MISURARE, e cosa e' andato diversamente: + * i tre monitor giornalieri su feed crypto (xsr, statarb, dvolspread) con la STESSA patologia, + intorno ai ~35 minuti al giorno di XSR01 -> **meta' giusto**: la patologia e' la stessa ma la + TAGLIA no (2, 4 e 41 min/giorno). Un libro dollar-neutral perde molto piu' di un libro + direzionale a parita' di troncamento, perche' su 35 minuti le due gambe si muovono insieme; + * `paper_prevday` MOLTO piu' mite (1 barra su 24) -> **confermato** (1.438 min/giorno su 1.440), + con un dettaglio non previsto: il difetto si propaga alla barra successiva quando il bersaglio + calcolato sulla barra parziale differisce; + * `paper_combo` rotto o sano per caso -> **sano, e per caso**: aspetta la chiusura di una BORSA; + * NON previsto: le uniche barre corrette di `paper_statarb` esistono perche' il feed si ROMPE + per 7 giorni a luglio e il monitor le recupera CHIUSE. Sono anche la prova che serviva per + dire che la finestra e' ricostruibile. nice -n 19 timeout 900 uv run python scripts/research/r0822_monitor_audit.py """ @@ -289,7 +296,8 @@ def b_xsr(): print(f" inception {T[0].date()} = DOPO l'incidente del feed: la finestra e' gia' pulita.") out = show(T, np.array([p[1] for p in pairs]), np.array([p[2] for p in pairs])) RES["paper_xsr"] = dict(parziale="SI", cmp=out["clean"], gate="XSR01 2026-10-23", - src="hl_*_1d.parquet", cad_h=24.0, ric="SI (bit-exact)") + src="hl_*_1d.parquet", cad_h=24.0, ric="SI bit-exact", + rec=rec, rep=sink.of("returns.jsonl")) def b_statarb(): @@ -323,17 +331,18 @@ def b_statarb(): print(f"\n 📌 LE UNICHE BARRE CORRETTE DELLA SERIE VENGONO DA UN GUASTO: le {int(ident.sum())} " f"identiche\n sono CONSECUTIVE ({gg[0]} -> {gg[-1]}) e cadono dentro l'incidente " "del feed.") - ev = freeze_evidence() + ev = sorted(freeze_evidence()) print(f" Prova indipendente dal log del cron: `rebuild_history` e' abortito in " - f"{len(ev)} giri\n ({', '.join(sorted(ev)[:8])}{'...' if len(ev) > 8 else ''}) — " - "EPERM sul backup, difetto gia' riparato\n nel sorgente. Col feed fermo il monitor " - "non trovava barre nuove; al ripristino ne ha\n recuperate 7 in un colpo solo, di " - "cui 6 GIA' CHIUSE -> coincidono col replay al bit.") + f"{len(ev)} giri consecutivi") + print(f" ({ev[0]} -> {ev[-1]}), EPERM sul backup — difetto gia' riparato nel sorgente.") + print(" Col feed fermo il monitor non trovava barre nuove; al ripristino ne ha") + print(" recuperate 7 in un colpo solo, di cui 6 GIA' CHIUSE -> coincidono al bit.") print(" Due conseguenze: (a) il 'min/giorno' globale e' GONFIATO da quelle barre — la") print(" riga 'ESCLUSO l'incidente' e' la stima onesta; (b) quelle 6 barre sono la prova") print(" che, su barre chiuse, il codice di produzione riproduce il replay ESATTAMENTE.") RES["paper_statarb"] = dict(parziale="SI", cmp=out["clean"], gate="STATARB 2026-09-27", - src="btc/eth 1h -> resample 1d", cad_h=24.0, ric="SI (bit-exact)") + src="btc/eth 1h -> resample 1d", cad_h=24.0, ric="SI bit-exact", + rec=rec, rep=sink.of("returns.jsonl")) def b_dvolspread(): @@ -374,7 +383,7 @@ def b_dvolspread(): RES["paper_dvolspread"] = dict(parziale="SI", cmp=out["clean"], gate="DVOLSPREAD 2026-10-24 / 2027-01-24", src="btc/eth 1h -> resample 1d + dvol", cad_h=24.0, - ric="SI (bit-exact)") + ric="SI bit-exact", rec=rec, rep=sink.of("returns.jsonl")) def b_prevday(): @@ -429,7 +438,7 @@ def b_prevday(): print(" 📌 E il verdetto operativo e' l'opposto degli altri quattro: su griglia oraria il difetto") print(" c'e' ma vale ~0.1% della varianza. `paper_prevday` NON e' da rigenerare.") RES["paper_prevday"] = dict(parziale="SI", cmp=out["clean"], gate="nessuno (lead in monitor)", - src="btc/eth 1h grezzo", cad_h=1.0, ric="SI (bit-exact)") + src="btc/eth 1h grezzo", cad_h=1.0, ric="SI bit-exact") def b_portfolio(): @@ -457,8 +466,9 @@ def b_portfolio(): if ident.any(): print(f"\n ⚠ TRAPPOLA DA NON PRENDERE: delle {int(ident.sum())} barre 'identiche', " f"{int(zeri.sum())} sono ZERO contro ZERO") - print(" (" + ", ".join(str(x.date()) for x in T[zeri]) + ") = giorni in cui TUTTI e cinque") - print(" gli sleeve erano flat (TP01 risk-off, GTAA a borsa chiusa, VRP fuori scadenza).") + print(" (" + ", ".join(str(x.date()) for x in T[zeri]) + ")") + print(" = giorni in cui TUTTI e cinque gli sleeve erano flat insieme (TP01 risk-off,") + print(" GTAA a borsa chiusa nel weekend, VRP fuori scadenza).") print(" Contarle come 'coincidono' sarebbe la trappola gia' codificata dal progetto —") print(" barre ATTIVE, non barre di calendario (26/07, il gate che segnalo' VRP01 perche'") print(" contava il 94% di zeri). Su barre attive le coincidenze qui sono " @@ -474,7 +484,7 @@ def b_portfolio(): RES["paper_portfolio"] = dict(parziale="SI", cmp=out["clean"], gate="nessuno (dashboard/book di ricerca)", src="5 sleeve, il piu' recente su 1h->1d e HL 1d", cad_h=24.0, - ric="SI* (equity IB ri-aggiustata)") + ric="SI non al bit") def b_combo(): @@ -514,7 +524,7 @@ def b_combo(): print(" senza che nulla lo segnali.") RES["paper_combo"] = dict(parziale="NO", cmp=out["clean"], gate="nessuno (paper cross-venue)", src="TP01 1h->1d su griglia IB", cad_h=24.0, - ric="SI* (equity IB ri-aggiustata)") + ric="SI non al bit") # ============================================================================================ @@ -522,7 +532,7 @@ def b_combo(): # ============================================================================================ GATE_INFO = { "paper_xsr": ("XSR01 2026-10-23", "Sharpe>=1.0 E haircut $5.000<=40%"), - "paper_statarb": ("STATARB 2026-09-27", "Sharpe di net_modeled >= 0.35"), + "paper_statarb": ("STATARB 2026-09-27", "Sharpe di net_modeled >= 0.5"), "paper_dvolspread": ("DVOLSPREAD 24/10 kill", "Sharpe<-0.50 -> ritiro; 27/01 decisione piena"), "paper_prevday": ("-", "lead senza data"), "paper_portfolio": ("-", "dashboard"), @@ -537,22 +547,105 @@ def sezione_C(): print("\n Le colonne sono calcolate ESCLUDENDO l'incidente del feed 09-15/07 (le barre") print(" recuperate a posteriori sono chiuse e attenuerebbero il difetto per la ragione") print(" sbagliata). 'min/g' = minuti di mercato al giorno effettivamente misurati, su 1440.") - print(f"\n {'monitor':<19}{'parz.':>6}{'ident./tot':>13}{'corr':>9}{'min/g':>9}" - f"{'ricostruibile':>25} gate dipendente") + print(f"\n {'monitor':<19}{'barra parz.':>13}{'ident./tot':>13}{'corr':>9}{'min/g':>8}" + f"{'ricostruibile':>17} gate dipendente") for k, v in RES.items(): c = v["cmp"] ratio = "{}/{}".format(c["ident"], c["n"]) - print(f" {k:<19}{v['parziale']:>6}{ratio:>13}{c['corr']:>+9.3f}" - f"{c['minuti']:>9,.0f}{v['ric']:>25} {GATE_INFO[k][0]}") + print(f" {k:<19}{v['parziale']:>13}{ratio:>13}{c['corr']:>+9.3f}" + f"{c['minuti']:>8,.0f}{v['ric']:>17} {GATE_INFO[k][0]}") print("\n 'ricostruibile' = la serie CORRETTA sulla stessa finestra forward si puo' ricalcolare") - print(" oggi dai feed su disco: e' letteralmente la colonna 'replay' di questo script. '*' =") - print(" ricostruibile ma NON al bit, perche' una gamba viene dal feed equity IB che e'") - print(" ri-aggiustato all'indietro (`ADJUSTED_LAST`).") + print(" oggi dai feed su disco: e' letteralmente la colonna 'replay' di questo script.") + print(" 'non al bit' = ricostruibile, ma una gamba viene dal feed equity IB, che e'") + print(" ri-aggiustato all'indietro (`ADJUSTED_LAST`) -> i centesimi non torneranno uguali.") print("\n LETTURA IN UNA RIGA: quattro monitor su sei registrano fra 2 e 41 minuti di mercato") print(" al giorno spacciandoli per 24 ore; i due che non lo fanno sono quello a griglia oraria") print(" (dove il difetto vale una barra su 24) e quello che aspetta la chiusura di una BORSA.") +def sezione_C2(): + """Il criterio di ciascun gate PRE-REGISTRATO resta leggibile? Le soglie NON sono ridichiarate + qui: si IMPORTANO dagli script di gate e dal monitor (regola del progetto — un sorvegliante + deriva il proprio bersaglio dal codice sorvegliato, mai lo ridichiara: 21/08, `fee_watch`).""" + print("\n" + "=" * 102) + print(" C2 — IL CRITERIO DEI TRE GATE PRE-REGISTRATI E' ANCORA LEGGIBILE?") + print("=" * 102) + import r0724_statarb_deploy_gate as GS # noqa: E402 (solo costanti: ha il main-guard) + import r0725_xsr_deploy_gate as GX # noqa: E402 + pv = live("paper_dvolspread") + + def metriche(rows: list[dict], key_m="net_modeled", key_r=None): + rm = np.array([float(r[key_m]) for r in rows]) + eq = np.cumprod(1 + rm) + pk = np.maximum.accumulate(eq) + out = dict(sh=float(rm.mean() / rm.std() * np.sqrt(365)) if rm.std() > 0 else 0.0, + dd=float(np.max((pk - eq) / pk)), tot=float(np.prod(1 + rm) - 1)) + if key_r: + rr = np.array([float(r.get(key_r, np.nan)) for r in rows]) + rr = rr[np.isfinite(rr)] + out["tot_r"] = float(np.prod(1 + rr) - 1) if len(rr) else float("nan") + return out + + def coppia(name): + rec = RES[name]["rec"] + rep = {int(r["ts"]): r for r in RES[name]["rep"]} + keep = [r for r in rec if int(r["ts"]) in rep] + return keep, [rep[int(r["ts"])] for r in keep] + + print(f"\n {'gate':<26}{'metrica primaria':<26}{'soglia':>10}{'REGISTR.':>11}" + f"{'RICOSTR.':>11} verdetto") + + # --- STATARB ------------------------------------------------------------------------- + a, b = coppia("paper_statarb") + ma, mb = metriche(a, key_r="net_real"), metriche(b, key_r="net_real") + flip = (ma["sh"] >= GS.SH_DEPLOY) != (mb["sh"] >= GS.SH_DEPLOY) + print(f" {'STATARB ' + str(GS.DECISION):<26}{'Sharpe net_modeled':<26}" + f"{'>= ' + str(GS.SH_DEPLOY):>10}{ma['sh']:>+11.2f}{mb['sh']:>+11.2f} " + f"{'*** SI RIBALTA ***' if flip else 'stesso'}") + print(f" {'':<26}{'maxDD':<26}{'< ' + f'{GS.DD_MAX:.0%}':>10}{ma['dd']:>10.1%}" + f"{mb['dd']:>11.1%} " + f"{'*** SI RIBALTA ***' if (ma['dd'] < GS.DD_MAX) != (mb['dd'] < GS.DD_MAX) else 'stesso'}") + hp_a = abs((1 + ma["tot"]) - (1 + ma["tot_r"])) * 100 + hp_b = abs((1 + mb["tot"]) - (1 + mb["tot_r"])) * 100 + print(f" {'':<26}{'fill-haircut cum (pp)':<26}{'< ' + str(GS.HAIRCUT_MAX_PP):>10}" + f"{hp_a:>11.2f}{hp_b:>11.2f} " + f"{'*** SI RIBALTA ***' if (hp_a < GS.HAIRCUT_MAX_PP) != (hp_b < GS.HAIRCUT_MAX_PP) else 'stesso'}") + + # --- XSR01 --------------------------------------------------------------------------- + a, b = coppia("paper_xsr") + K5 = "net_REAL-$5000" + ma, mb = metriche(a, key_r=K5), metriche(b, key_r=K5) + hc_a = (ma["tot"] - ma["tot_r"]) / abs(ma["tot"]) if ma["tot"] else float("nan") + hc_b = (mb["tot"] - mb["tot_r"]) / abs(mb["tot"]) if mb["tot"] else float("nan") + fl1 = (ma["sh"] >= GX.SH_DEPLOY) != (mb["sh"] >= GX.SH_DEPLOY) + fl2 = (hc_a <= GX.HAIRCUT_MAX) != (hc_b <= GX.HAIRCUT_MAX) + print(f" {'XSR01 ' + str(GX.DECISION):<26}{'Sharpe net_modeled':<26}" + f"{'>= ' + str(GX.SH_DEPLOY):>10}{ma['sh']:>+11.2f}{mb['sh']:>+11.2f} " + f"{'*** SI RIBALTA ***' if fl1 else 'stesso'}") + print(f" {'':<26}{'haircut $5.000':<26}{'<= ' + f'{GX.HAIRCUT_MAX:.0%}':>10}" + f"{hc_a:>10.1%}{hc_b:>11.1%} {'*** SI RIBALTA ***' if fl2 else 'stesso'}") + + # --- DVOLSPREAD ---------------------------------------------------------------------- + a, b = coppia("paper_dvolspread") + ma, mb = metriche(a), metriche(b) + fl = (ma["sh"] < pv.KILL_SHARPE) != (mb["sh"] < pv.KILL_SHARPE) + print(f" {'DVOLSPREAD ' + pv.KILL_DATE:<26}{'Sharpe fwd (kill se sotto)':<26}" + f"{'> ' + f'{pv.KILL_SHARPE:+.2f}':>10}{ma['sh']:>+11.2f}{mb['sh']:>+11.2f} " + f"{'*** SI RIBALTA ***' if fl else 'stesso (KILL in entrambe)'}") + + print(""" + ⚠ QUESTA NON E' UNA LETTURA DEI GATE, e non deve diventarlo: le tre date sono nel futuro + (anticiparle sarebbe selezione-sul-forward, contro la regola del progetto) e con 28-54 barre + l'errore standard di uno Sharpe annualizzato e' 2,6-3,6. Serve a una cosa sola, e la mostra: + il difetto NON e' rumore centrato — sposta la metrica primaria abbastanza da attraversare la + soglia. Su STATARB la serie registrata dice +1,96 (sopra la soglia di deploy 0,5) e quella + ricostruita dice -2,02: **stesso codice, stessa finestra, verdetto opposto**. + ⚠ E c'e' un secondo segnale, gratis: uno Sharpe registrato di -15,8 su 28 barre e' IMPOSSIBILE + (|Sharpe| oltre 4 SE). Il gate `implausible_sharpe` esiste gia' in `altlib` dal 26/07 ma + nessuno lo punta sulle serie FORWARD — solo sui candidati. Un numero fuori scala e' un + rilevatore di guasti a costo zero, e in questo caso era li' da leggere da settimane.""") + + # ============================================================================================ # D — LA GUARDIA CHE MANCA A monitor_health # ============================================================================================ @@ -608,8 +701,10 @@ def sezione_D(): if v["parziale"] == "SI" and seg == "tace": miss.append(k) print(f" {k:<19}{c['sd_rec']:>9.2f}%{c['sd_rep']:>9.2f}%{rr:>10.3f} {seg}") - print(f" -> MANCA {len(miss)} monitor rotti su {sum(1 for v in RES.values() if v['parziale'] == 'SI')}" - f": {', '.join(miss) if miss else '-'}") + rotti = sum(1 for v in RES.values() if v["parziale"] == "SI") + print(f" -> segnala {rotti - len(miss)} dei {rotti} monitor che leggono una barra parziale,") + print(f" NON segnala il sano (`paper_combo`, 0.987), e MANCA {len(miss)}: " + f"{', '.join(miss) if miss else '-'}") print(" Motivo strutturale, non taratura: su una griglia oraria il difetto tocca 1 barra") print(" su 24 e sposta la vol dell'1-2%, cioe' DENTRO il rumore di stima di una vol su") print(" poche settimane. Abbassare la soglia per prenderlo genererebbe falsi allarmi ogni") @@ -697,17 +792,23 @@ def sezione_E(): ("paper_dvolspread", "DVOLSPREAD (kill)", pd.Timestamp("2026-10-24"), "returns.jsonl"), ("paper_dvolspread", "DVOLSPREAD (decisione)", pd.Timestamp("2027-01-24"), "returns.jsonl"), ] - print(f"\n {'monitor / gate':<34}{'barre oggi':>12}{'data gate':>13}{'se AZZERI':>12}" + print("\n IPOTESI PESSIMISTA — si ripara `advance()` e si fa `--reset` (la finestra riparte da") + print(" zero oggi). Con una barra al giorno, tornare a N barre richiede N giorni:") + print(f"\n {'monitor / gate':<42}{'barre':>7}{'data gate':>13}{'reset -> N barre':>18}" f"{'ritardo':>10}") + ritardi = [] for mon, nome, gate, ser in righe: n = len(read_jsonl(ROOT / "data" / mon / ser)) - # con una barra al giorno, tornare a n barre richiede n giorni dal reset rip = oggi + pd.Timedelta(days=n) rit = (rip - gate).days - print(f" {f'{mon} / {nome}':<34}{n:>12}{str(gate.date()):>13}" - f"{str(rip.date()):>12}{rit:>+9}g") - print("\n Cosi' letta, la riparazione sposta i tre gate di 1-2 mesi. MA la colonna giusta e'") - print(" un'altra, e cambia la decisione:") + ritardi.append((nome, rit)) + print(f" {f'{mon} / {nome}':<42}{n:>7}{str(gate.date()):>13}" + f"{str(rip.date()):>18}{rit:>+9}g") + peggiore = max(ritardi, key=lambda x: x[1]) + print(f"\n Anche nell'ipotesi pessimista il danno NON e' uniforme: l'unico gate che slitterebbe") + print(f" e' {peggiore[0]} ({peggiore[1]:+d} giorni) — gli altri hanno finestre cosi' giovani che") + print(" ricostruirebbero la propria numerosita' PRIMA della data di decisione. Ma la colonna") + print(" giusta e' ancora un'altra, e toglie di mezzo anche quel ritardo:") print(""" ✅ LA FINESTRA FORWARD **NON VA PERSA**: e' RICOSTRUIBILE, per tutti e sei i monitor. Le ore mancanti non sono mai state registrate, ma non e' li' che vivono: vivono nei feed @@ -719,10 +820,12 @@ def sezione_E(): minuti. LE DUE CONDIZIONI, ed e' giusto chiedere che siano verificate e non assunte: - (1) il feed non riscrive le barre gia' chiuse. **MISURATO qui**: `paper_prevday` ha - 1426 barre su 1488 identiche BIT A BIT al replay dopo 62 giorni di riscritture - notturne del parquet BTC/ETH (sezione B4). Per Hyperliquid la stessa cosa e' misurata - in `r0822_xsr_repro` T2 (Sharpe 2024 e 2025 riprodotti al centesimo su 731 barre). + (1) il feed non riscrive le barre gia' chiuse. **MISURATO qui, due volte e in modo + indipendente**: `paper_prevday` ha 1427 barre su 1488 identiche BIT A BIT al replay + dopo 62 notti di riscrittura del parquet BTC/ETH (B4); e le 6 barre che `paper_statarb` + recupero' durante l'incidente del feed coincidono anch'esse al bit (B2) — due percorsi + diversi, stesso verdetto. Per Hyperliquid la misura e' in `r0822_xsr_repro` T2 (Sharpe + 2024 e 2025 riprodotti al centesimo su 731 barre). (2) la config e' congelata e verificabile: `paper_dvolspread` lo controlla da solo (`frozen` nello stato, esce 1 se diverge); per gli altri il congelamento e' nel sorgente ed e' in git. @@ -743,11 +846,28 @@ def sezione_E(): verificabili in git (lo sono). Il costo vero e' (c), non i giorni: nessuna delle tre date di gate si sposta. - ⚠ UNA COSA CHE LA RIGENERAZIONE CAMBIA DAVVERO, e va scritta nel diario: i libri REAL saltano i - ribilanci sotto il min-order, e quella decisione dipende dal capitale corrente, quindi il - percorso REAL rigenerato non e' il percorso REAL registrato — non e' "lo stesso numero - calcolato meglio", e' un secondo percorso deterministico. Per XSR01 conta: il gate del 23/10 - legge net_REAL-$5000 per l'haircut.""") + ⚠ UNA COSA CHE LA RIGENERAZIONE CAMBIA DAVVERO, ma di poco — e il "di poco" e' MISURATO, non + assunto. I libri REAL saltano i ribilanci sotto il min-order e quella decisione dipende dal + capitale corrente: il percorso REAL rigenerato non e' "lo stesso numero calcolato meglio", e' + un secondo percorso deterministico. Contava sapere quanto, perche' il gate XSR01 del 23/10 + legge `net_REAL-$5000` per l'haircut: misurato in C2, **1,7% registrato contro 1,8% + ricostruito**. Trascurabile a $5.000 (i ticket superano il min-order comunque). Il caveat + resta vero in linea di principio e va scritto nel diario; non e' un ostacolo alla decisione. + + COSA FARE, MONITOR PER MONITOR (raccomandazione, non azione — questo script non tocca nulla). + ⚠ Le due cose sono SEPARATE: la RIPARAZIONE di `advance()` (fermarsi all'ultima barra chiusa) + ha senso su tutti e sei — su un monitor sano non cambia niente e lo protegge da un cambio di + griglia futuro; la RIGENERAZIONE della serie e' un'altra decisione, e va fatta solo dove il + difetto morde abbastanza da spostare cio' che si leggera': + paper_xsr rigenerare — 41 min/g, e il gate del 23/10 legge questa serie + paper_statarb rigenerare — 4 min/g, gate 27/09: e' il piu' vicino nel tempo + paper_dvolspread rigenerare — 2 min/g, il peggiore dei sei; kill-check il 24/10 + paper_prevday LASCIARE — 1.438/1.440 min/g: la rigenerazione cambierebbe lo 0,1% + della varianza e azzererebbe 1.488 barre di storia + paper_portfolio rigenerare — 19 min/g; nessun gate, ma e' la serie che il dashboard + mostra e che finisce nei diari come "il book sta facendo X" + paper_combo LASCIARE — sano; semmai va PROTETTO con un test che rompa se la sua + griglia smette di essere il calendario di borsa""") def main() -> None: @@ -763,6 +883,7 @@ def main() -> None: b_portfolio() b_combo() sezione_C() + sezione_C2() sezione_D() sezione_E() print("\n" + "=" * 102) diff --git a/scripts/research/r0822_prop_alloc.py b/scripts/research/r0822_prop_alloc.py index f226d64..6069fb7 100644 --- a/scripts/research/r0822_prop_alloc.py +++ b/scripts/research/r0822_prop_alloc.py @@ -362,11 +362,14 @@ def objective(w: dict, lev: float, firm: str, ev_idx: np.ndarray, fu_idx: np.nda def paired_delta(wA: dict, levA: float, wB: dict, levB: float, firm: str, - ev_idx: np.ndarray, fu_idx: np.ndarray) -> tuple[float, float]: + ev_idx: np.ndarray, fu_idx: np.ndarray, + drift_mult: tuple = ()) -> tuple[float, float]: """Differenza APPAIATA di J fra due configurazioni (stessi indici di bootstrap) + errore - standard della differenza. Il livello di J ha SE ~0.01; la DIFFERENZA molto meno.""" - a = objective(wA, levA, firm, ev_idx, fu_idx)["jvec"] - b = objective(wB, levB, firm, ev_idx, fu_idx)["jvec"] + standard della differenza. Il livello di J ha SE ~0.01; la DIFFERENZA molto meno. + ⚠ `drift_mult` va passato: dimenticarlo confronta due configurazioni NON stressate e + stampa un delta che non c'entra con la riga in cui compare (errore commesso in sessione).""" + a = objective(wA, levA, firm, ev_idx, fu_idx, drift_mult=drift_mult)["jvec"] + b = objective(wB, levB, firm, ev_idx, fu_idx, drift_mult=drift_mult)["jvec"] d = a - b return float(d.mean()), float(d.std(ddof=1) / np.sqrt(len(d))) @@ -721,7 +724,8 @@ def main() -> None: w_, lv_, o_ = max(rows, key=lambda x: x[2]["J"]) lvL, oL = max(((lv, objective({"TP01": 0.75, "SKH01": 0.25}, lv, "HYRO", ev_idx, fu_idx, drift_mult=dmt)) for lv in LEVS), key=lambda x: x[1]["J"]) - dd_, sd_ = paired_delta(w_, lv_, {"TP01": 0.75, "SKH01": 0.25}, lvL, "HYRO", ev_idx, fu_idx) + dd_, sd_ = paired_delta(w_, lv_, {"TP01": 0.75, "SKH01": 0.25}, lvL, "HYRO", ev_idx, + fu_idx, drift_mult=dmt) print(f" {mult:>12.2f} {wkey(w_):>16} {lv_:>6.3f} {o_['J']:>7.3f} {o_['p_pass']:>8.1%} " f"{o_['p_alive']:>8.1%} {o_['e_payout']:>9,.0f}$ LIVE {oL['J']:.3f} " f"delta {dd_:+.3f} +-{sd_:.3f}") @@ -736,18 +740,24 @@ def main() -> None: print(" l'audit 02/07 lo trova al 93-98° pctl di 23 offset, 'spike, non plateau'. Questa lente") print(" gira sull'ancora CANONICA e la banda non e' ri-misurabile qui -> il mio ottimo e'") print(" distorto A FAVORE di SKH01 e va detto PRIMA di leggerlo. Taglio del drift di SKH01:") - print(f"\n {'drift SKH01':>13} {'config':>16} {'leva':>6} {'J':>7} {'J del LIVE':>11} " - f"{'delta appaiato':>16}") - for mult in (1.00, 0.66, 0.50, 0.33): - dmt = (("SKH01", mult),) + print(" Le ultime due righe sono il taglio CONGIUNTO su SKH01 E XS01 (i due sleeve che il") + print(" progetto misura come piu' fortunati / piu' recenti): e' il caso peggiore onesto.") + print(f"\n {'taglio drift':>21} {'config':>16} {'leva':>6} {'J':>7} {'J del LIVE':>11} " + f"{'delta appaiato':>17}") + for dmt in ((("SKH01", 1.00),), (("SKH01", 0.66),), (("SKH01", 0.50),), (("SKH01", 0.33),), + (("SKH01", 0.50), ("XS01", 0.50)), (("SKH01", 0.33), ("XS01", 0.00))): + mult = dict(dmt) rows = [(w, lv, objective(w, lv, "HYRO", ev_idx, fu_idx, drift_mult=dmt)) for w in top_w for lv in LEVS] w_, lv_, o_ = max(rows, key=lambda x: x[2]["J"]) lvL, oL = max(((lv, objective({"TP01": 0.75, "SKH01": 0.25}, lv, "HYRO", ev_idx, fu_idx, drift_mult=dmt)) for lv in LEVS), key=lambda x: x[1]["J"]) dd_, sd_ = paired_delta(w_, lv_, {"TP01": 0.75, "SKH01": 0.25}, lvL, "HYRO", - ev_idx, fu_idx) - print(f" {mult:>13.2f} {wkey(w_):>16} {lv_:>6.3f} {o_['J']:>7.3f} {oL['J']:>11.3f} " + ev_idx, fu_idx, drift_mult=dmt) + lab = " ".join(f"{k.replace('01','')} x{v:.2f}" for k, v in mult.items()) + assert abs(dd_ - (o_["J"] - oL["J"])) < 0.05, ( # guardia contro l'errore di sopra + f"delta appaiato {dd_:+.3f} incoerente con J {o_['J']:.3f} - {oL['J']:.3f}") + print(f" {lab:>21} {wkey(w_):>16} {lv_:>6.3f} {o_['J']:>7.3f} {oL['J']:>11.3f} " f"{dd_:>+11.3f} +-{sd_:.3f}") print(" NB: il taglio si applica anche al LIVE 75/25 (che ha SKH01 al 25%), quindi il delta e'") print(" un confronto onesto: entrambe le colonne perdono, la domanda e' quale perde di piu'.") From 93147a95c8d3b04b4bfbb39f70e3e3ccd1655b03 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 18:02:22 +0000 Subject: [PATCH 023/114] =?UTF-8?q?diary(2026-08-22):=20ondata=20multi-age?= =?UTF-8?q?nte=20=E2=80=94=2020=20filoni,=200=20candidati,=201=20difetto?= =?UTF-8?q?=20di=20produzione,=203=20soglie=20falsificate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/diary/2026-08-22-wave-multiagente.md | 152 ++++++++++++++++++++++ scripts/research/r0822_prop_alloc.py | 29 +++++ 2 files changed, 181 insertions(+) create mode 100644 docs/diary/2026-08-22-wave-multiagente.md diff --git a/docs/diary/2026-08-22-wave-multiagente.md b/docs/diary/2026-08-22-wave-multiagente.md new file mode 100644 index 0000000..b16c0d0 --- /dev/null +++ b/docs/diary/2026-08-22-wave-multiagente.md @@ -0,0 +1,152 @@ +# Ondata multi-agente 2026-08-22 — 20 filoni, branch `research/wave-0822` + +**Mandato dell'operatore:** *"metti decine di agenti a fare analisi di nuove strategie possibili. +Lavora su branch separato. Lo scopo e' sempre lo stesso arrivare ai 50 giornalieri velocemente."* + +**Esito in una riga: 0 candidati promossi, 1 difetto di produzione che avrebbe falsato un gate fra +36 giorni, 3 soglie pubblicate falsificate, 1 leva strutturale che nessun gate del progetto sa +vedere. Book, pesi, cron, config INVARIATI.** + +Ledger completo per filone: `docs/research/RESULTS-0822.md`. Briefing dato agli agenti: +`docs/research/BRIEF-0822.md`. 21 script in `scripts/research/r0822_*.py` (15.295 righe, compilano +tutti, ognuno gira da solo). + +## 0. Come e' stata condotta (e perche' conta) + +Vincolo di macchina: **2 core**, 7 GB, e sulla stessa VPS gira il libro con **soldi veri** +(`cron_book` al minuto :07). Il briefing imponeva `nice -n 19`, budget 15 min/run, sola lettura su +`config/`, `data/live/`, `src/live/`, `scripts/live/`, cron. **Ha tenuto**: al picco di carico +(load 14 su 2 core) il giro delle 17:07 del libro e' girato in **18 secondi**, feed fresco, nessuna +azione, nessun OOM. +⚠️ **Fatto operativo da ricordare: il cron gira dal working tree.** Con questo branch attivo, +stanotte alle 00:30 il cron esegue il codice di QUESTO branch. Verificato che il diff vs `main` +tocca **solo** `scripts/research/` e `docs/research/` -> produzione identica. **E' anche il motivo +per cui nessuna riparazione e' stata applicata qui: sarebbe andata live senza una decisione.** + +Il briefing dava agli agenti, **in anticipo**, i dieci modi in cui questo progetto si e' gia' +ingannato (null del de-levering, TP01 travestito, selezione sull'hold-out, fortuna d'ancora, Sharpe +implausibile, win-rate come knob, ...) e la lista dei filoni morti. **Sette agenti hanno catturato +un errore proprio prima di pubblicare**, e tre hanno refutato una propria ipotesi in corsa. + +## 1. Il risultato che va agito — 4 monitor forward su 6 sono rotti + +`fetch_hyperliquid` (e `resample_tf`) scrivono la barra del **giorno in corso**; `advance()` dei +monitor la consuma e **porta `last_ts` su di essa** -> le ore restanti di ogni giorno non entrano in +nessun rendimento registrato. + +| monitor | barre coincidenti col replay | **min/giorno registrati** | gate | +|---|---|---|---| +| `paper_statarb` | **0/46** | **4** | **STATARB 27/09** | +| `paper_dvolspread` | 0/28 | **2** | DVOLSPREAD 24/10 | +| `paper_xsr` | 1/28 | **41** | XSR01 23/10 | +| `paper_portfolio` | 7/56 | 19 | — | +| `paper_prevday` | 1259/1319 | 1.438 | — | +| `paper_combo` | 7/37 | 1.402 | — (sano **per caso**: aspetta una borsa) | + +**Il gate STATARB del 27/09 si ribalta su 2 criteri su 3**: Sharpe registrato **+1,96** contro +**−2,02** ricostruito, maxDD **2,2% contro 15,3%** (guardia <10%). +✅ **Ma la finestra non va persa**, ed e' misurato due volte in modo indipendente: il feed **non +riscrive le barre chiuse** (`paper_prevday` 1427/1488 barre identiche al bit dopo 62 notti; e le 6 +barre di `paper_statarb` recuperate dopo il guasto EPERM del 09-15/07 coincidono al bit). +**-> La riparazione e' "riparare `advance()` + RIGENERARE", non "riparare e azzerare": nessuna data +di gate si sposta.** +**Guardia raccomandata** (non implementata): `ts_ultima_barra + cadenza <= mtime del file`, grazia +5 min. O(1), segnala 5/6, **tace sul sano**, controllo positivo sintetico 6/6 nei due versi. +⚠️ `monitor_health` non poteva vederlo: **una serie fresca, completa e sbagliata passa ogni controllo +di freschezza**. E `implausible_sharpe` esiste dal 26/07 ma **non e' mai stato puntato sulle serie +forward** (avrebbe segnalato Sharpe −15,8 su 28 barre). + +## 2. Tre soglie pubblicate, falsificate con misure + +1. **"XS01 serve ~$20k"** -> **non esiste soglia da min-order**. L'origine del numero era *"rumore di + arrotondamento"*, una stima a occhio del 19/06. Haircut ~0 gia' a **$200-600 allocati**. + **Meccanismo**: gli ordini sono **due popolazioni** — il ribilanciamento del segnale e' il 13% + degli ordini ma il **75% del nozionale** (ticket $13,65, passa sempre); la deriva del vol-target + e' l'87% degli ordini ma il **25% del nozionale** (ticket $0,64, il pavimento la taglia **ed e' + gratis**). *Contare gli ordini da' 16% e sembra un disastro; contare il nozionale da' 75%.* + Due agenti indipendenti ci sono arrivati per strade diverse. + ⚠️ **Il "$20k di XS01" e il "$20k della decisione di venue" erano due cose diverse conflate in + CLAUDE.md.** Il primo cade; **il secondo regge intatto** — e' una decisione dell'operatore + sull'asse della rovina, non sull'eseguibilita'. +2. **"Slippage = rischio #1 di XSR01"** -> **refutato con margine 21x** al livello di liquidita' + odierno (mezzo-spread 0,7-1,0 bps contro i ~16 che servirebbero a uccidere l'edge). +3. **"BTC opzioni fuori a $3.000 (min 0.1 = $6.210/lotto)"** -> misurato sulla famiglia **inverse**, + che il conto (in USDC) **non puo' marginare**. La famiglia USDC-lineare ha lotti **10x piu' + piccoli**. Il muro resta, ma per il **prezzo del lotto**, non per quello che si credeva. + +## 3. La leva: l'unica variabile che nessun gate del progetto sa vedere + +**Lo Sharpe e' invariante alla scala** (misurato: 1,31 a ogni cella del knob) -> `deflated_sharpe` e +`marginal_vs_tp01` non falliscono sulla leva, **non la vedono**. E' per questo che in due mesi la +scala non e' mai stata esaminata. +Il libro gira al **7% di Kelly** e raccoglie il **15%** della crescita massima in log. Il gradino +eseguibile 1,00x -> 1,25-1,50x vale **14,7 anni -> 12,9-11,6** al capitale-rendita (€500/mese, netto +fisco). **Lo scettico ha confermato il risultato di testa** e ha tolto la sua condizione bloccante: +sotto la lente wick accoppiata il maxDD prende un **ricarico moltiplicativo costante del ~3,5%**, non +un'amplificazione (il maxDD e' multi-giorno, il wick e' di un giorno). +**Il gradino resta NON autorizzato**: cadono una riserva su tre. Restano il drift stimato su 7,4 anni +(k* e' lineare nel suo errore) e la **coda assente dal dataset** — un solo giorno a −10% all'anno +porta k* da ~10x a **2x**. +📌 **Ma la lezione del 25/07 e' vera e non si trasferisce:** su una regola a **UN giorno** +(daily-loss, stop di conto) close-only e' **esattamente cieca** — 0,00 breach/anno contro 0,40-1,21 +veri, rapporto **INF**. **Sul canale prop/funded la lente accoppiata resta obbligatoria.** + +## 4. Cosa e' morto, e come + +**Filoni chiusi con autopsia** (l'elenco per esteso e' nel ledger): adaptive-horizon (il "vincitore +adattivo" e' un lookback **costante** travestito), dealer-gamma (la gamba tradeable e' **incoerente**: +BTC non separa, ETH separa **al rovescio**), ortho-screen **7/7**, oi-pin (il max-pain **non batte +mai** la media a 7 giorni dello spot), term-structure (la pendenza e' un **termometro contemporaneo**), +skew direzionale (**il prezzo muove lo skew**, t 3,1-9,4 su 8/8 — non il contrario), flow-squeeze, +basis-calendar (il basis dei datati **E'** il funding del perp), alt-options (alt), xs-lite, +vrp-quote-vere. + +Tre chiusure di famiglia che valgono per le prossime ondate: +- **Il filone funding si chiude sul QUARTO lato** (affollamento): misurato su **53.430 ore / 3 anni**, + non predice ne' direzione ne' volatilita'. E i futures datati non sono un quinto lato: sono lo + stesso lato quotato diversamente (+7,33% implicito contro +6,48% realizzato, premio incassabile + +0,85%/anno con **IC95 che contiene lo zero**). +- **La mean-reversion non risorge** sotto due conditioner mai provati (volume, shock 3σ): la selezione + in-sample sceglie il ramo di **continuazione** in entrambi i casi. +- 📌 **Uno screen largo su BTC/ETH direzionale NON PUO' passare il proprio gate.** Su 168 trial il + massimo Sharpe atteso dal **puro rumore** e' **1,572**, sopra il soffitto direzionale misurato + (~1,3). Non e' sfortuna, e' aritmetica. **Le prossime ondate o dichiarano famiglie molto piu' + piccole in anticipo, o cambiano meccanismo.** + +⚠️ **Tre volte in questa ondata l'eseguibilita' a $600 NON e' stata il vincolo** (haircut 0,00-0,01; +lotti $7-242; margine ~$148). Dopo due mesi in cui il muro era sempre il capitale, adesso muoiono +tutti sull'edge. **E' un'informazione sul dove cercare.** + +## 5. Regole nuove, e una ritirata + +**Nuove:** +- **L'open interest non misura la negoziabilita'** — su Deribit USDC sono quasi **anti-correlati + (rho di rango −0,77)**: SOL_USDC e' 1ª per OI e ultima per quote a due lati (**9%**); BTC_USDC ha + **5** strumenti con OI>=100 e il **93%** dei put quotati. +- **Il deflated-Sharpe va calcolato DI SCREEN, non solo di famiglia** (168 trial, non 24). +- **Un placebo si controlla per BILANCIAMENTO DEL SEGNO prima di usarlo**: un null che concorda col + segnale solo nel 14% dei casi non e' un null, e' la strategia invertita — e fabbrica celle + "significative". +- **Il null del de-levering e' DEGENERE quando la variante e' una ri-scalatura** (`sh(k*base) === + sh(base)`): il test che serve e' **iso-peso sullo Sharpe**. +- **Un gate che stampa `None` va verificato prima di dichiararlo "non girato".** +- **Su una finestra interamente post-hold-out, `marginal_vs_tp01` non puo' dare ADDS per + costruzione** — un NEUTRAL li' non e' un giudizio. + +**Ritirata (era stata pubblicata poche ore prima, in questa stessa ondata):** +❌ *"Su equity a gradino un overlay giornaliero e' non-causale"* — **falso su questo motore**: +`backtest_signals` contabilizza al giorno d'**ingresso** in **291/293** trade, e la "riparazione" e' +un **no-op bit-exact**. Il "+0,04 fantasma" era la differenza fra due varianti **entrambe causali**, +e quella scartata era la **migliore sull'hold-out**. *Il costo di una regola derivata da un difetto +inesistente e' gia' stato pagato.* + +## 6. Cio' che resta aperto (decisioni dell'operatore, non mie) + +1. **Riparare `advance()` + rigenerare** le serie forward, prima del 27/09. +2. **Cablare la guardia D2** (`ts_ultima_barra + cadenza <= mtime`) in `monitor_health`. +3. **Il collettore della catena** raccoglie la famiglia inverse; ETH_USDC costa **+587 chiamate/giro + (+90%)** ed e' una decisione sul rate limit per-IP, che il 29/07 e' gia' costata un guasto. +4. **Rimisurare `f` di VRP01 term-structure-consistent** (il 42% del difetto e' li', e la regola per + prenderlo era gia' scritta il 03/07). +5. **XS01 e' eseguibile a ~$1.300 di conto**, non a $20k. Resta fuori **per la decisione di venue**, + che e' un'altra cosa. Se il piano cambia, si riapre prima. diff --git a/scripts/research/r0822_prop_alloc.py b/scripts/research/r0822_prop_alloc.py index 6069fb7..5b45f6b 100644 --- a/scripts/research/r0822_prop_alloc.py +++ b/scripts/research/r0822_prop_alloc.py @@ -937,6 +937,35 @@ def main() -> None: print(" usarlo ancora sarebbe sbagliato, ma il salto da 1.7% a 41.7% e' il prodotto di due") print(" correzioni, non di una scoperta.") + hr("I-ter. LA STESSA SCALA, MA CON GLI STRESS DI E-bis/E-quater DENTRO") + print(" La tabella qui sopra gira al drift PIENO di XS01 e SKH01. E-bis mostra che il vantaggio") + print(" vive quasi tutto sul drift di XS01; E-quater che il taglio congiunto lo azzera. Il") + print(" numero operativo va quindi dato con gli stress dentro, non a parte.") + print(" (meccanica: le tuple stressate vengono INIETTATE nella cache di `pc.book_tuples`, cosi'") + print(" la simulazione della scala e' letteralmente la stessa del 25/07, con un input diverso.)") + print(f"\n {'stress':>24} {'allocazione':>16} {'leva':>6} {'EUR/g med':>10} " + f"{'P(>=10/g)':>10} {'P(>=50/g)':>10} {'P(zero)':>8}") + for dmt, lab in (((), "nessuno (pieno)"), + ((("SKH01", 0.50), ("XS01", 0.50)), "SKH x0.50 XS x0.50"), + ((("SKH01", 0.33), ("XS01", 0.00)), "SKH x0.33 XS x0.00")): + for cfg, alab in ((cfg_best, f"argmax-J {best['w']}"), (cfg_live, "LIVE 75/25")): + wdic = dict(zip(CRYPTO_PROP, cfg)) + R_, G_ = book_RG(wdic, drift_mult=dmt) + key = (float(cfg[0]), float(cfg[1]), float(cfg[2]), "shared") + saved = pc._TUP_CACHE.pop(key, None) + pc._TUP_CACHE[key] = (R_, G_, _panel_index(START_WIN)) + r = pc.simulate([cfg], [0] * pl.MAX_CONCURRENT, n_paths=1500, lev=0.75, + seed=20260822, lens="coupled", start=START_WIN) + pc._TUP_CACHE.pop(key, None) + if saved is not None: + pc._TUP_CACHE[key] = saved + print(f" {lab:>24} {alab:>16} {0.75:>6.2f} {r['med']:>10.2f} " + f"{r['p_ge10']:>10.1%} {r['p_target']:>10.1%} {r['p_zero']:>8.1%}") + print("\n Questa e' la riga da citare: non il 41.7% pieno, ma la BANDA fra la riga piena e la") + print(" riga del taglio congiunto. Lo stress non e' un'ipotesi pessimista arbitraria: e' la") + print(" fortuna d'ancora MISURATA su SKH01 (26/07) e la finestra di scoperta di XS01 (dichiarata") + print(" in CLAUDE.md come 'taglia del guadagno ottimista').") + print("\n" + "=" * 104) print(f" fatto in {time.time()-t0:.0f}s") print("=" * 104) From 272622b6e8b6bfaf5fe2cb6828635632ed984c05 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 18:03:12 +0000 Subject: [PATCH 024/114] =?UTF-8?q?research(wave-0822):=20PROP-ALLOC=20?= =?UTF-8?q?=E2=80=94=20riallocare=20per=20la=20barriera=20vale=20il=204%,?= =?UTF-8?q?=20diversificare=20vale=20tutto,=20e=20la=20banda=20va=20da=204?= =?UTF-8?q?2%=20a=200,7%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 44 +++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 1918bf3..217df49 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -22,6 +22,7 @@ null de-levering superato + eseguibilita' al capitale dichiarato. | — | **SCETTICO su GROWTH-POLICY** | **REGGE** (condizione bloccante DECADUTA) | la lente wick accoppiata **non cambia il gradino**: e' un ricarico moltiplicativo **costante ~3,5%** sul maxDD, non un'amplificazione (Δ/k **cala**). Ma su una regola a **UN giorno** close-only e' **esattamente cieca** (rapporto INF) -> la lezione 25/07 e' vera e **non si trasferisce** | | 13 | BASIS-CALENDAR | **SCARTATO** + **1 dataset nuovo** | il basis dei futures datati **E' il funding del perp quotato diversamente** (7,33% contro 6,48%/anno): non e' un quarto lato del filone funding, e' lo stesso lato. Premio incassabile **+0,85%/anno con IC95 che contiene lo zero** | | — | **MONITOR-AUDIT** (produzione) | 🚨 **4 monitor su 6 rotti**, ma **la finestra NON e' persa** | `paper_statarb` registra **4 minuti al giorno** e il suo gate del **27/09 si ribalta su 2 criteri su 3** (Sharpe registrato **+1,96** contro **−2,02** ricostruito). **La riparazione e' "riparare + RIGENERARE", non "riparare e azzerare": nessuna data di gate si sposta** | +| 1 | PROP-ALLOC | **LEAD** (gate: XS01 fuori dalla sua finestra) + **1 ipotesi falsificata** | riallocare *per la barriera* invece che *per lo Sharpe* vale **+0,016 di J (il 4%)**: e' cio' che il conto del gambler's ruin prevedeva. Cio' che vale e' **mettere XS01 su un conto funded (+0,400)** — ma **tutto il vantaggio poggia sul drift di XS01 misurato sulla sua finestra di scoperta** | ## Note che sopravvivono ai singoli filoni @@ -638,3 +639,46 @@ diverso (drift monitor-strategia) e **non vede `paper_prevday`** (rapporto 0,999 walk sull'intervallo troncato), non un cronometro — il cronometro diretto e' 32-35 min; e per `paper_portfolio`/`paper_combo` la ricostruzione **non sara' al bit** perche' la gamba GTAA legge `ADJUSTED_LAST` di IB, ri-aggiustato all'indietro. + +### 1 — PROP-ALLOC (r0822_prop_alloc.py, ~3.300 valutazioni MC di cui **360 di selezione**) +✅ **Previsione REGISTRATA PRIMA e confermata:** dal conto del gambler's ruin, alla leva minima che +porta a +T dentro l'orizzonte resta `theta ~ 2*Sharpe^2/T` -> **la vol sparisce e l'argmax sui PESI +deve coincidere con l'argmax di Sharpe**; il contenuto vero sta nella **LEVA**, che lo Sharpe non +determina. Misurato: **e' cosi'**. +| | pesi TP/SKH/XS | leva | **J** | P(pass) | P(vivo 12m) | E[pay/anno] | +|---|---|---|---|---|---|---| +| argmax **J** | 25/25/50 | 0,75 | **0,738** | 82,2% | 89,8% | $8.556 | +| argmax **Sharpe** | 38/25/38 | 0,75 | 0,721 | 77,3% | 93,3% | $7.796 | +| **libro LIVE 75/25** | 75/25/0 | 0,50 | **0,335** | 38,6% | 86,8% | $4.109 | +Differenze **appaiate**: argmaxJ − argmaxSharpe **+0,0164 ± 0,0066**; argmaxJ − LIVE **+0,3996 ± +0,0114**. **-> Riallocare per la barriera vale il 4% di cio' che vale diversificare.** +✅ **Replica di controllo superata:** la politica MISTO del 25/07, stessa macchina, de-luck 0,60 -> +P(>=50/g) **4,1%** / P(zero) **53,7%** contro il pubblicato 5,6-6,5% / 52%. +📌 **Null dei tilt casuali — e il risultato e' che il PUNTO non conta:** argmax al 98,5° pctl contro +firma best-of-45 di 97,8° = **indistinguibili**, e **il massimo di 200 pesi casuali (0,739) EGUAGLIA +l'argmax (0,738)**. *Conta stare nella regione diversificata, non il vettore.* +📌 **La leva alta non compra probabilita', compra fretta:** J massimo a **0,75x**, E[payout] massimo +a **1,00x**; P(pass) senza limite di tempo e' **monotona decrescente** (98,9% a 0,50x -> 65,9% a +1,50x). E **la barriera piu' larga sposta l'ottimo verso l'alto: il vincolo e' la regola di DD, non +il libro.** +📌 **Il 75/25 del libro live NON e' la ripartizione giusta per un conto a barriera**, e non e' +artefatto di finestra: sul campione **2019+ che contiene il 2022** l'ottimo della coppia crypto e' +**38/62 @0,50x** (J 0,521 contro 0,443) — stesso verso del 2024+. +🚨 **LA BANDA, non il numero.** P(>=50/g) da €600 in 36 mesi con la scala di conti: +**42% al drift pieno · 7,7% allo stress moderato (SKH ×0,50, XS ×0,50) · 0,7% allo stress severo** +(SKH ×0,33, XS ×0,00) — e allo stress severo e' **peggio del libro live**. **Il numero da citare e' +la banda.** Tetto strutturale: cap $200k (HYRO) + $400k (FTMO) = **max 6 conti**, oltre servono altre +firm, ognuna col proprio rischio di controparte. +⚠️ **Onesta' dell'agente sulla distorsione che NON ha potuto rimuovere:** la banda d'ancora **non e' +girata** sotto questa lente (fuori budget), quindi 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 distorsione e' a suo favore e lo dichiara*. +⚠️ E la sotto-pesatura di **TP01, lo sleeve difensivo**, e' misurata su un campione **senza +sinistro** (2024-2026 non contiene un crash): **su una barriera assorbente quella scommessa si paga +una volta sola.** +⚠️ Assunzione dichiarata e **non verificata con le firm**: che una firm crypto listi i 19 alt +Hyperliquid con short abilitato. Se non li lista, XS01 sparisce e con lui il risultato. +⚠️ Errore proprio corretto in sessione: `paired_delta` **ignorava lo stress** e stampava un delta +non-stressato accanto a righe stressate -> *un confronto appaiato deve ereditare TUTTI i parametri +della riga in cui compare*; senza quella guardia avrebbe concluso che il risultato era robusto +quando non lo e'. Ora c'e' un `assert` che lega il delta alle due colonne stampate. From 827ae54dc1b97707de319182067a1dfdd4dde673 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 18:04:52 +0000 Subject: [PATCH 025/114] =?UTF-8?q?CLAUDE.md:=20ondata=202026-08-22=20?= =?UTF-8?q?=E2=80=94=20difetto=20dei=20monitor=20forward,=20tre=20soglie?= =?UTF-8?q?=20falsificate,=20la=20leva=20invisibile=20ai=20gate,=20e=20una?= =?UTF-8?q?=20regola=20ritirata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 146 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 564de2d..a3d5cb4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1992,6 +1992,152 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis ⚠️ **Nota per il futuro-me:** e' una decisione presa con l'informazione completa, non una svista. Se qualcuno propone SOL "perche' e' eseguibile su Deribit", la risposta e' che l'eseguibilita' non e' mai stata il problema — l'hold-out lo e', a 24 ancore su 24. +- 🌊 **ONDATA MULTI-AGENTE 2026-08-22 (20 filoni, branch `research/wave-0822`) — 0 candidati, 1 + DIFETTO DI PRODUZIONE, 3 soglie pubblicate falsificate, 1 leva che nessun gate sa vedere.** + Diario `docs/diary/2026-08-22-wave-multiagente.md`; ledger per filone `docs/research/RESULTS-0822.md`; + briefing `docs/research/BRIEF-0822.md`; 21 script `scripts/research/r0822_*.py` (15.295 righe, + compilano tutti, ognuno gira da solo). **Book, pesi, cron, config INVARIATI.** + 🚨 **(1) I MONITOR FORWARD REGISTRANO UNA FRAZIONE DEL GIORNO — 4 su 6 rotti.** + `fetch_hyperliquid` e `resample_tf` scrivono la barra del **giorno in corso**; `advance()` la + consuma e **porta `last_ts` su di essa** -> le ore restanti non entrano in nessun rendimento. + | monitor | barre coincidenti | **min/giorno** | gate | + |---|---|---|---| + | `paper_statarb` | **0/46** | **4** | **STATARB 27/09** | + | `paper_dvolspread` | 0/28 | **2** | DVOLSPREAD 24/10 | + | `paper_xsr` | 1/28 | **41** | XSR01 23/10 | + | `paper_portfolio` | 7/56 | 19 | — | + | `paper_prevday` | 1259/1319 | 1.438 | — | + | `paper_combo` | 7/37 | 1.402 | — (sano **per caso**: aspetta una borsa) | + **Il gate STATARB si ribalta su 2 criteri su 3**: Sharpe registrato **+1,96** contro **−2,02** + ricostruito, maxDD **2,2% contro 15,3%** (guardia <10%). + ✅ **La finestra NON va persa** (misurato 2 volte: il feed non riscrive le barre chiuse — + `paper_prevday` 1427/1488 identiche al bit dopo 62 notti; le 6 barre di statarb recuperate dopo il + guasto EPERM 09-15/07 coincidono al bit) -> **"riparare `advance()` + RIGENERARE", non "riparare e + azzerare": nessuna data di gate si sposta** (pessimista: STATARB +18g). + **Guardia raccomandata, NON cablata:** `ts_ultima_barra + cadenza <= mtime del file`, grazia 5 min. + O(1), segnala 5/6, **tace sul sano**, controllo positivo sintetico 6/6 nei due versi. + ⚠️ `monitor_health` non poteva vederlo: **una serie fresca, completa e SBAGLIATA passa ogni + controllo di freschezza**. E `implausible_sharpe` non e' **mai** stato puntato sulle serie forward. + 📌 **(2) TRE SOGLIE PUBBLICATE, FALSIFICATE.** (a) *"XS01 serve ~$20k"* (origine: "rumore di + arrotondamento", stima a occhio del 19/06) -> **nessuna soglia da min-order**, haircut ~0 gia' a + **$200-600 allocati**. Meccanismo: gli ordini sono **due popolazioni** — il ribilanciamento del + segnale e' il 13% degli ordini ma il **75% del nozionale** (ticket $13,65); la deriva del + vol-target e' l'87% degli ordini ma il **25%** (ticket $0,64) e **il pavimento la taglia gratis**. + *Contare gli ordini da' 16%, contare il nozionale da' 75%.* ⚠️ **Il "$20k di XS01" e il "$20k + della DECISIONE DI VENUE" sono due cose diverse conflate in questo file: il primo cade, il secondo + REGGE** (e' sull'asse della rovina, non dell'eseguibilita'). (b) *"slippage = rischio #1 di XSR01"* + -> **refutato con margine 21x**. (c) *"BTC opzioni fuori a $3.000"* -> misurato sulla famiglia + **inverse**, che un conto **USDC non puo' marginare**; la famiglia USDC-lineare ha lotti **10x piu' + piccoli** (ETH_USDC **$242**, BTC_USDC $772 contro $7.744). Il muro resta **per il prezzo del + lotto**. Parametri veri Hyperliquid: **min order $10** (non $5), **taker 4,50 bps** (non 5,0). + 📌 **(3) LA LEVA E' L'UNICA VARIABILE CHE NESSUN GATE DEL PROGETTO SA VEDERE.** Lo Sharpe e' + **invariante alla scala** (1,31 a ogni cella del knob) -> `deflated_sharpe` e `marginal_vs_tp01` + non falliscono, **non la vedono**. Il libro gira al **7% di Kelly** e raccoglie il **15%** della + crescita massima in log; il gradino eseguibile 1,00x->1,25-1,50x vale **14,7a -> 12,9-11,6a** al + capitale-rendita (€500/m, netto fisco). **Lo scettico conferma il numero e toglie la sua condizione + bloccante**: sotto la lente wick accoppiata il maxDD prende un **ricarico moltiplicativo costante + ~3,5%**, non un'amplificazione (**il maxDD e' multi-giorno, il wick e' di un giorno**). + **Il gradino resta NON autorizzato**: cade 1 riserva su 3; restano il drift stimato su 7,4 anni + (k* e' lineare nel suo errore) e la **coda assente dal dataset** (un giorno −10%/anno -> k* 2x). + ⚠️ **Ma la lezione del 25/07 e' vera e NON si trasferisce:** su una regola a **UN giorno** + (daily-loss, stop di conto) close-only e' **esattamente cieca** — 0,00 breach/anno contro 0,40-1,21 + veri (**rapporto INF**). **Sul canale prop/funded la lente accoppiata resta OBBLIGATORIA.** + ⚠️ **`target_vol` e `k` NON sono la stessa leva:** `target_vol` scala il 75% del libro e lascia + fermo il 25% (a tv=30% il libro e' **82/18**, a 60% **90/10**) -> e' anche un tilt di pesi, e + **fallisce `weights_tilt_null`**. La strada pulita per un cambio di SCALA e' il cap di config. + 📌 **La vol realizzata di TP01 a `target_vol=20%` e' 12,14%**, scomposto: `20% x 0,682 x 0,890`, + dove lo 0,682 sono **due meta' quasi uguali** (flat nel 44,3% delle barre **e** convinzione + parziale) -> **"TP01 e' long-flat" spiega meta'**; e alzare `target_vol` **aumenta la size proprio + nei giorni in cui il segnale e' piu' debole**. Il x0,890 e' **diversificazione, non un difetto**. + ⚠️ **XS01 dichiara lo stesso 20% e ne realizza 20,59%: vale per TP01, non per il libro.** Nessun + numero pubblicato cambia (sono tutti sulla serie **realizzata**); sbaglia chi legge l'**etichetta** + (attribuirebbe a TP01 l'89% del rischio invece del **73%**). + 📌 **(4) PROP/FUNDED — l'ipotesi centrale e' falsificata, e per la ragione teorica giusta.** + Riallocare *per la barriera* invece che *per lo Sharpe* vale **+0,016 di J (il 4%)**: il conto del + gambler's ruin lo prevedeva (**la vol sparisce, il contenuto sta nella LEVA**). Cio' che vale e' + **mettere XS01 su un conto funded: +0,400** (J 0,335 -> 0,738; P(pass) 38,6% -> 82,2%). Leva ottima + **0,75x HYRO / 1,00x FTMO**, e **la barriera piu' larga alza l'ottimo: il vincolo e' la regola di + DD, non il libro**; la leva alta **non compra probabilita', compra fretta**. Il **75/25 del libro + live non e' la ripartizione giusta per un conto a barriera** (sul 2019+ che contiene il 2022 + l'ottimo e' **38/62 @0,50x**). **Il null dei tilt dice che il PUNTO non conta**: il massimo di 200 + pesi casuali (0,739) **eguaglia** l'argmax (0,738) — conta stare nella regione diversificata. + 🚨 **Il numero da citare e' LA BANDA: P(>=50/g) da €600 in 36 mesi = 42% al drift pieno · 7,7% allo + stress moderato · 0,7% a quello severo** (e li' e' **peggio del libro live**), perche' **tutto il + vantaggio poggia sul drift di XS01 misurato sulla sua finestra di scoperta**. Gate proposto: non + aprire un funded su questa allocazione prima che XS01 abbia una finestra fuori dal 2024-2026. + ⚠️ Distorsioni dichiarate e non rimosse: banda d'ancora **non girata** sotto quella lente (l'ottimo + gira sull'ancora canonica e si appoggia a **SKH01, che ha la fortuna d'ancora piu' grande da + restituire**); e la sotto-pesatura di **TP01, lo sleeve difensivo, e' misurata su un campione senza + sinistro** — su una barriera assorbente quella scommessa si paga una volta sola. + 📌 **(5) LO SLIPPAGE DEL LIBRO LIVE, misurato per la prima volta: non c'e' a questa taglia.** + I backtest a 10 bps RT restano **conservativi di ~1,5 bps/lato** (fee vera 7 bps RT); estremi + avversi **1/18 contro 4,7 attesi**. Prova diretta: un fill ha preso il **21,9% del volume della sua + barra 5m** e quella barra ha avuto un range di **1,08 bps**. ✅ **Fee sul NASTRO: 5,00 bps/lato + prima del 2026-08-01 e 3,50 dopo, 16/16 esatti** = conferma indipendente di `fee_watch`. + 🚨 **Ma la misura ha una DATA DI SCADENZA:** la partecipazione scala lineare -> quel 22% diventa + **172% di una barra 5m a $5.000** e **687% a $20.000**. **Va rifatta a ogni salto di taglia.** + ⚠️ Due difetti del dato trovati prima di misurare: `ts_utc` in `book_executions.jsonl` **non e' + l'ora del fill** (18/18 = `00:00:00`; l'ora vera sta solo in `logs/cron_book.log`); e il feed + certificato e' l'**INVERSE** mentre il libro trada il **LINEARE USDC** (**4ª occorrenza** dello + schema `fee_watch`: misurare li' avrebbe misurato **la base**). + 📌 **(6) IL `f=0,73` DI VRP01 SCOMPOSTO — il 42% e' STRUTTURA A TERMINE, solo il 25% e' skew** + (30% spread, 2% fit). **Il sleeve prezza un'opzione a 7 GIORNI col DVOL a 30 GIORNI**, che nel 90% + delle ore sta ~3 punti di vol **sopra** l'ATM a 7g -> sovrapprezza **entrambe** le gambe. Il + "+0,8pp sulla gamba corta" del 30/07 — che faceva sembrare **corretta** la gamba venduta — e' la + **cancellazione di due errori da ~3pp**. ⚠️ **E la regola per prenderlo era gia' scritta il 03/07** + ("riprezzare term-structure-consistent prima di credere a un numero da struttura BS-flat") **e non + e' stata applicata alla misura del 30/07**. Verso: `f_term` va da **0,83** (IV-rank basso) a + **0,93** (alto) e **supera 1 in backwardation** mentre `f_skew` resta piatto -> **la parte maggiore + del difetto si annulla proprio dove il gate fa tradare il sleeve**, quindi f=0,73 e' plausibilmente + **conservativo li'**. Non misurato (0/22 ingressi passano il gate) e in un crash i due pezzi vanno + in **direzioni contrarie**. **`VRP_CFG["f"]` NON cambiato.** Replica indipendente del f: **0,714**. + 📌 **(7) CHIUSURE DI FAMIGLIA.** **Il filone funding si chiude sul QUARTO lato** (affollamento): + su **53.430 ore / 3 anni** non predice ne' direzione ne' volatilita'. E i futures datati **non sono + un quinto lato**: il loro basis **E'** il funding del perp (+7,33% implicito contro **+6,48%** + realizzato; premio incassabile **+0,85%/anno con IC95 che contiene lo zero**). **La mean-reversion + non risorge** sotto due conditioner mai provati (volume, shock 3σ): la selezione in-sample sceglie + il ramo di **continuazione** in entrambi. Scartati anche: adaptive-horizon (il "vincitore adattivo" + ha lookback **costante**, corr 1,000), dealer-gamma (BTC non separa, ETH separa **al rovescio**; + e ⚠️ **`dealer_net_gamma` del dataset ereditato e' il GEX col SEGNO INVERTITO**, corr −0,95, mentre + `liquidation_*_risk` e' **una sola categoria in 17.229/17.229 righe**), oi-pin (il max-pain **non + batte mai, 0/24, la media a 7 giorni dello spot**), term-structure (**termometro contemporaneo**), + skew direzionale (**il prezzo muove lo skew**, t 3,1-9,4 su 8/8, non il contrario), ortho-screen + **7/7**, alt-options (alt), xs-lite, vrp-quote-vere. + ⚠️ **La catena ereditata da bite ha 74-75 giorni utili, non 3,7 mesi:** prima del **2026-06-09** + raccoglieva **una sola scadenza per giro** (due agenti indipendenti, 74 e 75). + 📌 **(8) REGOLE NUOVE.** (a) **L'open interest NON misura la negoziabilita'** — su Deribit USDC sono + quasi **anti-correlati (rho di rango −0,77)**: SOL_USDC e' 1ª per OI e **ultima per quote a due + lati (9%)**, BTC_USDC ha **5** strumenti con OI>=100 e il **93%** dei put quotati (e sul short leg + di SOL il miglior bid sta a **UN tick**: vendere li' significa **pagare $27** per aprire un credit + spread). (b) **Il deflated-Sharpe va calcolato DI SCREEN, non solo di famiglia**: su 168 trial il + massimo atteso dal **puro rumore** e' **Sharpe 1,572**, sopra il soffitto direzionale (~1,3) -> + **uno screen largo su BTC/ETH direzionale non puo' passare il proprio gate, per aritmetica.** + (c) **Un placebo si controlla per BILANCIAMENTO DEL SEGNO prima di usarlo** (un null che concorda + col segnale nel 14% dei casi non e' un null, e' la strategia invertita: fabbrica celle + "significative"). (d) **Il null del de-levering e' DEGENERE quando la variante e' una + ri-scalatura** (`sh(k*base) === sh(base)`): serve **iso-peso sullo Sharpe**. (e) **Un gate che + stampa `None` va verificato prima di dichiararlo "non girato"**. (f) **Su una finestra interamente + post-hold-out `marginal_vs_tp01` non puo' dare ADDS per costruzione** — un NEUTRAL li' non e' un + giudizio. (g) **Un confronto appaiato deve ereditare TUTTI i parametri della riga in cui compare.** + ❌ **REGOLA RITIRATA, pubblicata poche ore prima nella stessa ondata:** *"su equity a gradino un + overlay giornaliero e' non-causale"* — **falso su questo motore**: `backtest_signals` contabilizza + al giorno d'**INGRESSO** in **291/293** trade e la "riparazione" e' un **no-op bit-exact**; il + "+0,04 fantasma" era la differenza fra due varianti **entrambe causali**, e quella scartata era la + **migliore sull'hold-out**. *Il costo di una regola derivata da un difetto inesistente e' gia' + stato pagato.* + 📌 **(9) Cosa NON e' piu' il vincolo: TRE volte in questa ondata l'eseguibilita' a $600 non ha + bocciato niente** (haircut 0,00-0,01; lotti $7-242; margine ~$148). Dopo due mesi in cui il muro + era sempre il capitale, **adesso muoiono tutti sull'edge**. + ⚠️ **(10) FATTO OPERATIVO: il cron gira dal WORKING TREE.** Con un branch di ricerca attivo, il + cron esegue quel branch. Durante l'ondata il diff vs `main` toccava **solo** `scripts/research/` e + `docs/research/` (verificato) -> produzione identica; ed e' il motivo per cui **nessuna riparazione + e' stata applicata**: sarebbe andata live alle 00:30 senza una decisione dell'operatore. + 🗂️ **Dataset nuovo in eredita':** **30/30 futures trimestrali scaduti** Deribit ricostruiti + (`get_instruments?expired=true` ne ritorna **1**, ma i nomi sono deterministici e + `get_tradingview_chart_data` serve la storia di un contratto scaduto): **BTC 240.150 barre orarie / + ETH 233.334, dal 2018-09, col deleveraging 2022 dentro** — cioe' **la coda che a CC01 mancava per + costruzione**. Cross-check col forward implicito nella catena opzioni: mediana **−0,6 bps**. + - **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso: cross-sectional su universo Hyperliquid certificato (XS01) → portafoglio Sharpe ~1.55. - **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.** From 0bcc5bc5d0ad263a1dd6582e797f66edfad398ab Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 20:34:08 +0000 Subject: [PATCH 026/114] =?UTF-8?q?research(wave-0822):=20apre=20il=20regi?= =?UTF-8?q?stro=20della=20seconda=20ondata=20=E2=80=94=207=20filoni=20sui?= =?UTF-8?q?=20buchi=20della=20prima?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 217df49..e512fef 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -682,3 +682,21 @@ Hyperliquid con short abilitato. Se non li lista, XS01 sparisce e con lui il ris non-stressato accanto a righe stressate -> *un confronto appaiato deve ereditare TUTTI i parametri della riga in cui compare*; senza quella guardia avrebbe concluso che il risultato era robusto quando non lo e'. Ora c'e' un `assert` che lega il delta alle due colonne stampate. + +--- + +# ONDATA 2 — 2026-08-22 sera (7 filoni) + +Mirata su cio' che la prima ondata ha lasciato scoperto, **inclusi i falsificatori che gli agenti +stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, stessi gate. + +| # | filone | domanda che decide | verdetto | +|---|---|---|---| +| 21 | XS01-OOS | il vantaggio del PROP-ALLOC poggia sul drift di XS01 misurato sulla sua finestra di scoperta: regge fuori? | _in corso_ | +| 22 | MAKER | l'esecuzione passiva e' una fonte di ritorno, al netto del costo di non essere eseguiti? | _in corso_ | +| 23 | BOCPD | un rilevatore di cambio di regime vero batte il miglior lookback COSTANTE? | _in corso_ | +| 24 | CRITICO | cosa NON e' stato misurato, e quale singola misura mancante vale di piu' | _in corso_ | +| 25 | BIN-FREQ | il binario di frequenza su SKH01 e' un candidato o un sottoprodotto? | _in corso_ | +| 26 | TP01-SINISTRO | quanto pesa TP01 se l'obiettivo include un crash che nel campione non c'e'? | _in corso_ | +| 27 | SURFACE-RV | la superficie e' internamente incoerente, e l'incoerenza vale piu' del costo di attraversarla? | _in corso_ | + From 103dd7bce5eee4b66d237b6b2a01b665427e33e6 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 20:50:03 +0000 Subject: [PATCH 027/114] =?UTF-8?q?research(wave-0822):=20BOCPD=20?= =?UTF-8?q?=E2=80=94=20il=20falsificatore=20eseguito=20con=20potenza,=200/?= =?UTF-8?q?68=20celle=20adattive;=20l'adattivita'=20spiega=20il=20-7%=20de?= =?UTF-8?q?l=20vantaggio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 61 +- scripts/research/r0822b_bin_freq.py | 825 +++++++++++++++++++ scripts/research/r0822b_bocpd.py | 957 +++++++++++++++++++++++ scripts/research/r0822b_critic.md | 667 ++++++++++++++++ scripts/research/r0822b_maker.py | 733 +++++++++++++++++ scripts/research/r0822b_surface_rv.py | 716 +++++++++++++++++ scripts/research/r0822b_tp01_sinistro.py | 873 +++++++++++++++++++++ scripts/research/r0822b_xs01_oos.py | 683 ++++++++++++++++ 8 files changed, 5514 insertions(+), 1 deletion(-) create mode 100644 scripts/research/r0822b_bin_freq.py create mode 100644 scripts/research/r0822b_bocpd.py create mode 100644 scripts/research/r0822b_critic.md create mode 100644 scripts/research/r0822b_maker.py create mode 100644 scripts/research/r0822b_surface_rv.py create mode 100644 scripts/research/r0822b_tp01_sinistro.py create mode 100644 scripts/research/r0822b_xs01_oos.py diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index e512fef..ebe226d 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -694,9 +694,68 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st |---|---|---|---| | 21 | XS01-OOS | il vantaggio del PROP-ALLOC poggia sul drift di XS01 misurato sulla sua finestra di scoperta: regge fuori? | _in corso_ | | 22 | MAKER | l'esecuzione passiva e' una fonte di ritorno, al netto del costo di non essere eseguiti? | _in corso_ | -| 23 | BOCPD | un rilevatore di cambio di regime vero batte il miglior lookback COSTANTE? | _in corso_ | +| 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' | _in corso_ | | 25 | BIN-FREQ | il binario di frequenza su SKH01 e' un candidato o un sottoprodotto? | _in corso_ | | 26 | TP01-SINISTRO | quanto pesa TP01 se l'obiettivo include un crash che nel campione non c'e'? | _in corso_ | | 27 | SURFACE-RV | la superficie e' internamente incoerente, e l'incoerenza vale piu' del costo di attraversarla? | _in corso_ | + +--- + +## 23 — BOCPD (orizzonte adattivo, il falsificatore eseguito) + +`scripts/research/r0822b_bocpd.py` (123 s). **169 trial dichiarati**, conteggio al rialzo, inclusi i +13 di taratura fatti guardando **solo il numero di change-point**, mai uno Sharpe. + +**Il falsificatore che il 22/08 restava aperto e' stato eseguito con i rilevatori che nominava**: +BOCPD (Adams & MacKay, run-length posterior online, Normale-InverseGamma, hazard costante) e +**optimal partitioning causale** (l'obiettivo che PELT accelera, ricorsione in avanti, mai un +`fit_predict` retrospettivo), piu' `L` scelto per walk-forward sul PnL realizzato. + +📌 **E questa volta il test HA AVUTO POTENZA — e' il fatto che rende il "no" esaustivo.** +Il vincitore del 22/08 aveva lookback **costante** (stava al bordo il 100% del tempo, corr 1,000): +si era chiusa una famiglia di *proxy*, non il meccanismo. Qui **68/96 celle sono davvero adattive** +(BOCPD 24/24: `L_t` mediano 25-107 g, IQR 28-134 g, **0-4% al tetto**, 7-44 reset/anno). + +| | Sh FULL | Sh HOLD | maxDD | CAGR | corr→TP01 | +|---|---|---|---|---|---| +| (A) adattivo, cella in-sample | 1,618 | 0,627 | 14,48% | 22,05% | 0,757 | +| **(B) miglior COSTANTE** (L=15g triple 12h) | **1,639** | **0,657** | 12,68% | 22,31% | 0,737 | +| (C) walk-forward su PnL | 1,385 | 0,382 | 16,33% | 17,20% | 0,860 | +| TP01 30/90/180 | 1,314 | 0,489 | 14,31% | 16,44% | 1,000 | + +📌 **La decomposizione E' il risultato:** adattivo − TP01 = **+0,305**; miglior costante − TP01 = +**+0,326**; adattivo − miglior costante = **−0,021** → **l'adattivita' spiega il −7% del vantaggio +su TP01. Il 100% e' "un orizzonte piu' corto", non "adattarlo".** + +**Gate:** `earns_slot_honest=False`; `marginal_vs_tp01` **NEUTRAL** con **corr→TP01 0,861**; +appaiato per anno vs il null giusto (B) **3/8 anni** (mediana −0,15); **banda d'ancora 12 ancore, +mediana delle differenze appaiate −0,136, positiva in 0/12**, e la canonica sta al **92° pctl** = +fortuna d'ancora a suo favore. `causality_ok` ok, e **troncamento manuale esplicito** su 4 +combinazioni: `max|L(prefisso) − L(pieno)| = 0` → la trappola del change-point retrospettivo e' +esclusa **per misura, non per fiducia**. Null del de-levering **non applicabile e dichiarato** +(il candidato NON riduce il maxDD: 14,5% contro 12,7%). + +**Il criterio del falsificatore applicato a tutte le 68 celle adattive: 0/68**, e nessuna delle due +condizioni va vicina — corr→TP01 **minimo 0,754** (soglia 0,60), **nessuna** batte il miglior +costante sul FULL (la migliore e' −0,013), massimo **4/8** anni battuti (richiesti 6). + +⚠️ **Cosa l'agente NON propone, e perche':** il miglior costante batte TP01 di +0,326 — ma il salto +di Sharpe fra **L adiacenti** e' **+0,190, cioe' il 58% dell'intero vantaggio**: e' un max-of-24 su +un crinale di rumore, non una curva. **Nessun cambio a TP01, al libro, ai pesi.** + +🚨 **Sottoprodotto che vale fuori dal filone: un deflated-Sharpe calcolato dentro una famiglia +OMOGENEA e' CIECO.** Qui dice **0,998-1,000 PASS**; ricalcolato con l'sr0 **di screen** misurato dal +filone ORTHO (1,572 su 168 trial) dice **0,555 / 0,580 / 0,237 FAIL** — TP01 compreso. Il motivo e' +strutturale: `deflated_sharpe` stima il massimo-dal-rumore dalla **varianza** degli Sharpe della +griglia, e una griglia di varianti molto correlate ha varianza piccola → sr0 basso → DSR alto **per +costruzione**. Gonfiare N col padding alza N e non la varianza: **non basta contare i trial di +screen, serve la VARIANZA di screen.** + +📌 **Secondo sottoprodotto, un controllo da mettere in ogni famiglia con un parametro clippato: la +PROVA DI IDENTITA' batte la diagnostica.** Invece di dire "questa cella sta al bordo", l'agente +mostra che la sua serie e quella del lookback costante coincidono a **max|Δ| = 0,000e+00 su +n=2719** — non *somiglia a*, **lo e'**. + +**VERDETTO: SCARTATO. IL FILONE ORIZZONTE-ADATTIVO E' CHIUSO DEFINITIVAMENTE.** diff --git a/scripts/research/r0822b_bin_freq.py b/scripts/research/r0822b_bin_freq.py new file mode 100644 index 0000000..345285f --- /dev/null +++ b/scripts/research/r0822b_bin_freq.py @@ -0,0 +1,825 @@ +#!/usr/bin/env python +"""r0822b_bin_freq.py — BIN-FREQ: il BINARIO di frequenza su SKH01 giudicato come un CANDIDATO. + +STORIA DEI FATTI (verificata leggendo il codice, non il riassunto). + 1. `r0822_vol_size.py` ha proposto una SIZE PER-TRADE su SKH01 (cella congelata `VTL tv20 w90` + = clip(0.20/rv90(t-1), 0, 3) fissata all'INGRESSO) come CONTROLLO DEL RISCHIO: + +0,070 ISO dShFULL, 23/23 ancore, 8/8 anni. + 2. `r0822_volsize_skeptic.py` ha CONFERMATO il numero e FALSIFICATO il meccanismo: il guadagno + e' FREQUENZA (+0,051 sul solo filtro di attivita') e non MAGNITUDINE (+0,003); e la forma + minima e' un BINARIO 0/1 (`BIN L>=med`, 194/406 trade scartati) che vale +0,080 FULL (20/23) + = 115% del LEAD, con controllo di segno superato (l'inverso fa -0,159, 0/23). + 3. Lo scettico ha scritto "il candidato giusto non e' la formula ma il binario" — e nessuno + l'ha passato per `study_family_honest`. E' un SOTTOPRODOTTO DI UN ATTACCO, mai dichiarato + come famiglia. Questo script lo giudica come si giudica un candidato. + +IPOTESI, REGISTRATA PRIMA DI MISURARE. Mi aspetto che il numero si riproduca (lo scettico e' stato +rigoroso e la sua replica e' bit-exact) e che il candidato NON superi il gate onesto, per due +ragioni dichiarate in anticipo: (a) la soglia e la finestra sono due assi liberi trovati DENTRO +l'esplorazione di un'altra famiglia, quindi la stima di famiglia dovra' essere molto piu' piccola +dell'argmax (precedente: sull'asse w lo scettico stesso stima +0,025 contro +0,080); (b) il +deflated-Sharpe sara' VACUO come lo e' stato sulla famiglia VOL-SIZE — su perturbazioni della +STESSA strategia la varianza fra i trial e' quasi nulla, e il DSR e' ~1 per chiunque, baseline +compreso. Se succede lo dichiaro vacuo e uso un null che ha potenza. + +=========================== LA FAMIGLIA, DICHIARATA PRIMA DI GUARDARE ========================= +Un binario di frequenza su SKH01 ha esattamente quattro gradi di liberta'. Li dichiaro tutti, +con i loro valori, PRIMA di misurare, e li conto AL RIALZO: + + soglia (6) quantile ESPANDENTE causale della vol trailing, q in {0.25, 0.35, 0.50, 0.65, + 0.75} + la soglia ASSOLUTA rv <= 20% (== `L>=1` della regola congelata) + finestra (6) w in {45, 60, 90, 120, 180, 270} giorni (sotto 45 lo scettico ha misurato che + la finestra contiene 1-3 barre ATTIVE: campione vuoto, non fragilita') + isteresi (2) h in {0.00, 0.10} (banda +-10% attorno alla soglia, stato persistente causale) + segno (2) +1 tieni la vol BASSA (l'ipotesi) / -1 tieni la vol ALTA (controllo di segno) + timeframe(1) uno solo: la gamba SKH01 vive sulla griglia 230m per costruzione. Dichiarato. + -> 6 x 6 x 2 x 2 = 144 celle + +RIAPRIRE UN PARAMETRO RIAPRE LA SUA FAMIGLIA, e questo binario e' nato dentro l'esplorazione di +un'altra famiglia. Percio' al conto delle 144 si SOMMANO i trial gia' spesi sullo stesso oggetto: +28 (r0822_vol_size) + 43 (r0822_volsize_skeptic) = 71. + -> CONTO ONESTO DICHIARATO: 144 + 71 = 215 trial. +La sensibilita' del verdetto al conteggio e' pubblicata (§5), come nel precedente 30/07 dove un +DSR passava a N=8, falliva a N=72 e falliva a N=360. + +============================== LENTE, ANCORE, CONVENZIONI ===================================== +Lente: path CANONICO di SKH01 (fill al livello, entry a chiusura di bin) — identica nelle due +braccia di ogni confronto, quindi il Delta e' pulito anche se il livello e' pessimistico verso il +path live. Ancore: **23 offset**, la griglia a priori dell'audit 02/07 (ogni 30m su [0,690)): +NON ridotta, perche' il costo misurato e' ~20 ms per cella per ancora. Statistica: MEDIANA DELLE +DIFFERENZE APPAIATE (`A.anchor_luck_delta`), mai differenza delle mediane. +Confronto sempre ISO-PESO: il binario cambia la vol della gamba e quindi il peso EFFETTIVO di +SKH01 nel libro, e muovere quel peso e' gia' stato misurato e RESPINTO il 26/07. Il null del +de-levering e' DEGENERE su una ri-scalatura (`sh(k*base) === sh(base)`), quindi il test e' +iso-peso sullo Sharpe; `k_iso_dd` resta come diagnostica del DD. +Nessuna riscrittura: `extract / equity_daily / leg_daily / sizes_from_L / book / netted_target` +sono IMPORTATE da r0822_vol_size, e il titolo dello scettico e' riprodotto chiamando la SUA +funzione. Replica bit-exact contro `sleeves._skyhook_returns()` prima di ogni delta (§0). + +USO: nice -n 19 timeout 900 uv run python scripts/research/r0822b_bin_freq.py [--every K] + --every 8 -> 3 ancore (pilota). default: tutte e 23. +""" +from __future__ import annotations + +import argparse +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 altlib as A # noqa: E402 +import r0702_anchor_skh01 as R # noqa: E402 +import r0702_tp01_offset as TO # noqa: E402 +import r0822_vol_size as V # noqa: E402 (motore: riusato, non riscritto) +import r0822_volsize_skeptic as SK # noqa: E402 (per riprodurre il titolo) +from r0822_sol_leg import k_iso_dd # noqa: E402 +from src.portfolio.portfolio import weights_tilt_null # noqa: E402 +from src.strategies.skyhook import SKH01_V1 # noqa: E402 + +ASSETS = V.ASSETS +OFFSETS_FULL = V.OFFSETS_FULL +sh, vol, maxdd, cagr = V.sh, V.vol, V.maxdd, V.cagr +hold, ins, book = V.hold, V.ins, V.book +CAPITAL, MIN_ORDER = V.CAPITAL, V.MIN_ORDER +W_TP, W_SKH = V.W_TP, V.W_SKH + +# ------------------------------------------------------------------ la famiglia (dichiarata sopra) +QS = (0.25, 0.35, 0.50, 0.65, 0.75) +THRS = tuple([("q", q) for q in QS] + [("abs", 0.20)]) +WS = (45, 60, 90, 120, 180, 270) +HS = (0.00, 0.10) +SIGNS = (+1, -1) +N_INHERITED = 28 + 43 # r0822_vol_size + r0822_volsize_skeptic, sullo stesso oggetto +CANON = ("q", 0.50, 90, 0.00, +1) # la cella dello scettico, dentro la mia famiglia +L = 112 + + +def cname(thr, w, h, sg) -> str: + t = "rv<=20%" if thr[0] == "abs" else f"q{thr[1]:.2f}" + return f"{t} w{w} h{h:.2f} s{'+' if sg > 0 else '-'}" + + +# =============================================================== il binario, causale per costruzione +def _hyst(x: np.ndarray, thr: np.ndarray, h: float, keep_below: bool) -> np.ndarray: + """Stato persistente con banda +-h attorno alla soglia. Causale: al passo t usa solo x_t e + thr_t (entrambi funzione di dati <= t-1) e lo stato al passo t-1. h=0 -> confronto secco.""" + n = len(x) + out = np.ones(n, bool) + st = True + for i in range(n): + xi, ti = x[i], thr[i] + if not (np.isfinite(xi) and np.isfinite(ti)): + st = True # warm-up: si tiene tutto (conv. scettico) + elif keep_below: + st = (xi <= ti * (1.0 + h)) if st else (xi <= ti * (1.0 - h)) + else: + st = (xi >= ti * (1.0 - h)) if st else (xi >= ti * (1.0 + h)) + out[i] = st + return out + + +class KeepCache: + """rv e quantili espandenti calcolati UNA volta per (asset, ancora, w). L'espandente e' la + parte cara; il resto e' un confronto.""" + + def __init__(self) -> None: + self.rv: dict = {} + self.q: dict = {} + + def keep(self, ex: dict, key, thr, w: int, h: float, sg: int) -> pd.Series: + base = ex["base_daily"] + k = (key, w) + if k not in self.rv: + self.rv[k] = V._rv_daily(base, w, active_only=False) # gia' shiftata di 1 giorno + rv = self.rv[k] + if thr[0] == "abs": + t = np.full(len(rv), float(thr[1])) + else: + kq = (key, w, thr[1]) + if kq not in self.q: + self.q[kq] = rv.expanding(min_periods=V.VT_WARM).quantile(thr[1]) + t = self.q[kq].values + keep = _hyst(rv.values.astype(float), np.asarray(t, float), h, keep_below=(sg > 0)) + return pd.Series(keep.astype(float), index=rv.index) + + +KC = KeepCache() + + +def size_bin(thr, w, h, sg): + def f(ex, key): + return V.sizes_from_L(ex, KC.keep(ex, key, thr, w, h, sg)) + return f + + +# =============================================================== motore +def leg_of(EXX: dict, key, fn) -> pd.Series: + return V.leg_daily(EXX, {a: fn(EXX[a], (key, a)) for a in ASSETS}) + + +def iso_pair(TP: pd.Series, leg: pd.Series, leg_base: pd.Series): + """Libro della variante e libro di CONTROLLO ISO-PESO (baseline ri-scalato alla stessa vol di + gamba => stesso peso effettivo di SKH). Convenzione del bersaglio, riusata tale e quale.""" + vb = vol(leg_base) + m = vol(leg) / vb if vb > 0 else 1.0 + return book(TP, leg), book(TP, leg_base * m), m + + +# =============================================================== main +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--every", type=int, default=1) + ap.add_argument("--ndraw", type=int, default=200) + args = ap.parse_args() + offs = OFFSETS_FULL[:: max(1, args.every)] + t0 = time.time() + + print("=" * L) + print(" r0822b BIN-FREQ — il BINARIO di frequenza su SKH01, giudicato come un CANDIDATO") + print("=" * L) + print(f" ancore: {len(offs)}/{len(OFFSETS_FULL)} della griglia a priori 02/07 (ogni 30m su " + f"[0,690)). NON ridotta.") + print(" lente: path CANONICO, identica nelle due braccia. Confronto sempre ISO-PESO.") + + # ---------------------------------------------------------- §1 famiglia, PRIMA di misurare + print("\n" + "-" * L) + print(" 1. GRIGLIA DICHIARATA — scritta prima di guardare i risultati, contata AL RIALZO") + print("-" * L) + cells = [(t, w, h, s) for t in THRS for w in WS for h in HS for s in SIGNS] + print(f" soglia {len(THRS):>3} {['q%.2f' % q for q in QS] + ['rv<=20% (== L>=1)']}") + print(f" finestra {len(WS):>3} {list(WS)} giorni") + print(f" isteresi {len(HS):>3} {list(HS)} (banda +-h attorno alla soglia)") + print(f" segno {len(SIGNS):>3} +1 tieni vol BASSA (ipotesi) / -1 tieni vol ALTA (controllo)") + print(f" timeframe 1 la gamba SKH01 vive su 230m per costruzione") + print(f" -> celle di QUESTA famiglia: {len(cells)}") + print(f" -> trial gia' spesi sullo STESSO oggetto: {N_INHERITED} " + f"(28 r0822_vol_size + 43 r0822_volsize_skeptic)") + print(f" -> CONTO ONESTO DICHIARATO: {len(cells)} + {N_INHERITED} = " + f"{len(cells) + N_INHERITED} trial. La sensibilita' al conteggio e' in §5.") + assert len(cells) == 144 + + # ---------------------------------------------------------- estrazione + print("\n estrazione tabelle trade (V2-DD) ...", flush=True) + EX = {o: {a: V.extract(a, o) for a in ASSETS} for o in offs} + ntr = {a: int(np.mean([len(EX[o][a]["i_ent"]) for o in offs])) for a in ASSETS} + print(f" fatto in {time.time()-t0:.0f}s — trade medi/ancora: " + + ", ".join(f"{a} {ntr[a]}" for a in ASSETS)) + TP = A.tp01_baseline_daily() + can = offs[0] + + # ---------------------------------------------------------- §0 SANITY + print("\n" + "-" * L) + print(" 0. SANITY — bit-exact prima di qualunque delta") + print("-" * L) + from src.portfolio.sleeves import _skyhook_returns + base_leg = {o: V.leg_daily(EX[o], {a: V.size_flat(EX[o][a]) for a in ASSETS}) for o in offs} + off = _skyhook_returns() + d0 = float(np.max(np.abs(base_leg[can].values - off.values))) if len(base_leg[can]) == len(off) \ + else float("nan") + print(f" gamba 50/50 off{can} vs sleeves._skyhook_returns(): max|dif| = {d0:.2e} " + f"({len(base_leg[can])} giorni)") + assert d0 < 1e-15, "replica NON bit-exact" + for a in ASSETS: + ref = R.run_asset(a, can)[0] + R._CACHE.clear() + dd = float(np.max(np.abs(V.equity_daily(EX[can][a], V.size_flat(EX[can][a])).values + - ref.values))) + print(f" {a} vs r0702_anchor_skh01.run_asset: max|dif| = {dd:.2e}") + assert dd < 1e-15 + + # ---------------------------------------------------------- §2 riproduzione del titolo + print("\n" + "-" * L) + print(" 2. RIPRODUZIONE DEL TITOLO DELLO SCETTICO — chiamando la SUA funzione, non la mia") + print(" (pubblicato: BIN L>=med +0,080 FULL 20/23 ; INVERSO -0,159 0/23)") + print("-" * L) + REP = {} + for tag, mode in (("BIN L>=med (scettico)", "gemed"), ("BIN L<1 INVERSO (scettico)", "lt1"), + ("BIN L>=1 (scettico)", "ge1")): + fn = SK.size_bin(SK.FROZEN, mode) + bk, ct = {}, {} + for o in offs: + lg = V.leg_daily(EX[o], {a: fn(EX[o][a]) for a in ASSETS}) + bk[o], ct[o], _ = iso_pair(TP, lg, base_leg[o]) + REP[tag] = (bk, ct) + dF = A.anchor_luck_delta(lambda o: bk[o], lambda o: ct[o], offs, metric=sh) + dH = A.anchor_luck_delta(lambda o: hold(bk[o]), lambda o: hold(ct[o]), offs, metric=sh) + print(f" {tag:<28} ISO dShFULL {dF['median_paired']:+.3f} " + f"({dF['n_positive']}/{len(offs)}) banda [{dF['lo']:+.3f},{dF['hi']:+.3f}] " + f"ISO dShHOLD {dH['median_paired']:+.3f} ({dH['n_positive']}/{len(offs)})") + med_F = float(np.median([sh(book(TP, base_leg[o])) for o in offs])) + med_H = float(np.median([sh(hold(book(TP, base_leg[o]))) for o in offs])) + med_D = float(np.median([maxdd(book(TP, base_leg[o])) for o in offs])) + ban_F = [sh(book(TP, base_leg[o])) for o in offs] + print(f" libro BASE, mediana della banda: ShFULL {med_F:.3f} / ShHOLD {med_H:.3f} / " + f"maxDD {med_D*100:.1f}% [pubblicato 1,626 / 1,037 / 10,4%]") + print(f" banda d'ancora del LIVELLO del libro: ShFULL [{min(ban_F):.3f},{max(ban_F):.3f}] " + f"= ampiezza {max(ban_F)-min(ban_F):.3f} (il termine di paragone di §12)") + + # ---------------------------------------------------------- §3 causalita' per troncamento + print("\n" + "-" * L) + print(" 3. CAUSALITA' — la decisione tenere/scartare ricalcolata TRONCANDO i dati al giorno") + print(" d'ingresso. `A.causality_ok` NON E' GIRATO (vuole un target_fn(df) e qui il") + print(" segnale nasce da 5m->230m: ri-eseguirlo su prefissi costa quanto tutto lo") + print(" script). Sostituito da un troncamento diretto, piu' stringente sul punto esatto.") + print("-" * L) + thr_c, w_c, h_c, s_c = ("q", CANON[1]), CANON[2], CANON[3], CANON[4] + bad = tested = 0 + for a in ASSETS: + ex = EX[can][a] + kfull = KC.keep(ex, (can, a), thr_c, w_c, h_c, s_c) + rv_full = V._rv_daily(ex["base_daily"], w_c, False) + days = list(dict.fromkeys(ex["day_ent"])) + for d in days[::7]: # 1 ingresso su 7, su tutta la storia + if d not in kfull.index: + continue + b_tr = ex["base_daily"][ex["base_daily"].index <= d] + if len(b_tr) < V.VT_WARM + w_c: + continue + rv_t = V._rv_daily(b_tr, w_c, False) + q_t = rv_t.expanding(min_periods=V.VT_WARM).quantile(thr_c[1]) + k_t = _hyst(rv_t.values.astype(float), q_t.values.astype(float), h_c, s_c > 0)[-1] + tested += 1 + if bool(k_t) != bool(kfull.loc[d] > 0.5): + bad += 1 + assert abs(float(rv_t.iloc[-1]) - float(rv_full.loc[d])) < 1e-12 or \ + not np.isfinite(rv_full.loc[d]) + print(f" decisioni ricalcolate su prefisso: {tested} DIVERGENTI: {bad}") + print(" NB l'isteresi ha memoria: troncando si perde lo stato, quindi il test e' esatto") + print(" per h=0 (la cella canonica) e conservativo per h>0.") + assert bad == 0, "il binario NON e' causale" + + # ---------------------------------------------------------- §4 tabella di famiglia + print("\n" + "-" * L) + print(" 4. LA FAMIGLIA INTERA — ISO dShFULL, mediana delle differenze appaiate su " + f"{len(offs)} ancore") + print("-" * L) + # DUE PASSATE, per la RAM (7 GB sulla VPS, ~3 liberi, e siamo in tanti): la 1a calcola le + # metriche per-ancora di tutte le 144 celle SENZA conservare le serie (144 x 23 x 3 serie da + # 2717 punti sarebbero ~500 MB); la 2a ricalcola e CONSERVA le serie delle sole 3-4 celle che + # servono dopo (canonica / scelta al buio / argmax): 4 x 23 gambe, ~2 s. + BK, CT, LEG, MS = {}, {}, {}, {} + + def eval_cell(thr, w, h, sg, store: bool = False) -> dict: + nm = cname(thr, w, h, sg) + fn = size_bin(thr, w, h, sg) + dF, dH, dI, lF, lI, lH, ms, kp, ddv, ddc, dC_ = ([] for _ in range(11)) + yr: dict = {} + for o in offs: + lg = leg_of(EX[o], o, fn) + b, c, m = iso_pair(TP, lg, base_leg[o]) + dF.append(sh(b) - sh(c)); dH.append(sh(hold(b)) - sh(hold(c))) + dI.append(sh(ins(b)) - sh(ins(c))); dC_.append(cagr(b) - cagr(c)) + lF.append(sh(lg)); lI.append(sh(ins(lg))); lH.append(sh(hold(lg))) + ms.append(m); ddv.append(maxdd(b)); ddc.append(maxdd(c)) + kp.append(float(np.mean(np.concatenate([fn(EX[o][a], (o, a)) for a in ASSETS])))) + for y in {int(t) for t in b.index.year}: + by, cy = b[b.index.year == y], c[c.index.year == y] + if len(by) > 20: + yr.setdefault(y, []).append(sh(by) - sh(cy)) + if store: + BK.setdefault(nm, {})[o] = b + CT.setdefault(nm, {})[o] = c + LEG.setdefault(nm, {})[o] = lg + MS.setdefault(nm, {})[o] = m + a_ = np.asarray(dF, float) + return dict(nm=nm, thr=thr, w=w, h=h, sg=sg, + dF=float(np.median(a_)), nF=int((a_ > 0).sum()), + lo=float(a_.min()), hi=float(a_.max()), arrF=a_, + dH=float(np.median(dH)), nH=int((np.asarray(dH) > 0).sum()), + isoI=float(np.median(dI)), dC=float(np.median(dC_)), + legF=float(np.median(lF)), legI=float(np.median(lI)), + legH=float(np.median(lH)), kept=float(np.median(kp)), + weff=float(np.median([W_SKH * x for x in ms])), + ddv=float(np.median(ddv)), ddc=float(np.median(ddc)), + yr={y: float(np.median(v)) for y, v in yr.items()}) + + RES = {} + for (thr, w, h, sg) in cells: + r = eval_cell(thr, w, h, sg) + RES[r["nm"]] = r + print(f" ({len(cells)} celle x {len(offs)} ancore valutate in {time.time()-t0:.0f}s " + f"dall'avvio, prima passata senza conservare le serie)") + + def matrix(sg: int, h: float) -> None: + print(f"\n ISO dShFULL — segno {'+1 (tieni vol BASSA)' if sg>0 else '-1 (CONTROLLO)'}" + f", isteresi h={h:.2f}") + print(" soglia " + "".join(f"{('w%d' % w):>11}" for w in WS)) + for thr in THRS: + t = "rv<=20%" if thr[0] == "abs" else f"q{thr[1]:.2f}" + row = "" + for w in WS: + r = RES[cname(thr, w, h, sg)] + row += f"{r['dF']:>+8.3f}{r['nF']:>3}" + print(f" {t:<10}" + row) + + for sg in SIGNS: + for h in HS: + matrix(sg, h) + print("\n (ogni cella: mediana appaiata e n. di ancore positive su " + f"{len(offs)})") + + POS = [RES[cname(t, w, h, +1)] for t in THRS for w in WS for h in HS] + NEG = [RES[cname(t, w, h, -1)] for t in THRS for w in WS for h in HS] + ALLC = [RES[cname(t, w, h, s)] for t in THRS for w in WS for h in HS for s in SIGNS] + nmcan = cname(("q", CANON[1]), CANON[2], CANON[3], CANON[4]) + argmax = max(ALLC, key=lambda r: r["dF"]) + print(f"\n ARGMAX della famiglia: {cname(argmax['thr'], argmax['w'], argmax['h'], argmax['sg']):<26}" + f" dShFULL {argmax['dF']:+.3f} ({argmax['nF']}/{len(offs)})") + print(f" cella dello scettico: {nmcan:<26} dShFULL {RES[nmcan]['dF']:+.3f} " + f"({RES[nmcan]['nF']}/{len(offs)}) rango " + f"{1 + sum(r['dF'] > RES[nmcan]['dF'] for r in ALLC)}/{len(ALLC)}") + print(f" STIMA ONESTA se la cella si sceglie a caso dentro la famiglia (mediana):") + print(f" tutta la famiglia (144) {float(np.median([r['dF'] for r in ALLC])):+.3f} " + f"positive {sum(r['dF']>0 for r in ALLC)}/{len(ALLC)}") + print(f" solo segno +1 (72) {float(np.median([r['dF'] for r in POS])):+.3f} " + f"positive {sum(r['dF']>0 for r in POS)}/{len(POS)}") + print(f" solo segno -1 (72) {float(np.median([r['dF'] for r in NEG])):+.3f} " + f"positive {sum(r['dF']>0 for r in NEG)}/{len(NEG)} <- controllo di segno") + # seconda passata: conserva le serie delle sole celle che servono dopo + sel_iso0 = max(ALLC, key=lambda r: r["isoI"]) + for r in dict((x["nm"], x) for x in (RES[nmcan], argmax, sel_iso0, + max(ALLC, key=lambda z: z["legI"]))).values(): + eval_cell(r["thr"], r["w"], r["h"], r["sg"], store=True) + dchk = A.anchor_luck_delta(lambda o: BK[nmcan][o], lambda o: CT[nmcan][o], offs, metric=sh) + assert abs(dchk["median_paired"] - RES[nmcan]["dF"]) < 1e-12 and \ + dchk["n_positive"] == RES[nmcan]["nF"], "la statistica appaiata di §4 non coincide con A.anchor_luck_delta" + print(f" controllo: la statistica appaiata calcolata in §4 coincide con " + f"`A.anchor_luck_delta` ({dchk['median_paired']:+.6f})") + print(f" quota di trade TENUTI, mia cella q0.50 w90: {RES[nmcan]['kept']*100:.1f}% " + f"(mediana fra le ancore)") + fn_can = size_bin(("q", CANON[1]), CANON[2], CANON[3], CANON[4]) + fn_sk = SK.size_bin(SK.FROZEN, "gemed") + ov, k_me, k_sk = [], [], [] + for o in offs: + for a in ASSETS: + x = fn_can(EX[o][a], (o, a)) > 0.5 + y = fn_sk(EX[o][a]) > 0.5 + ov.append(float((x == y).mean())); k_me.append(float(x.mean())); k_sk.append(float(y.mean())) + dleg = float(np.max(np.abs(BK[nmcan][can].values - REP["BIN L>=med (scettico)"][0][can].values))) + print(f" ⚠ la MIA parametrizzazione NON e' bit-identica a quella dello scettico: lui prende") + print(f" la mediana espandente di L = clip(0.20/rv,0,3) DOPO un `.fillna(1.0)` che inietta") + print(f" dei 1.0 nel warm-up; io prendo il quantile espandente di rv, che salta i NaN. Il") + print(f" trasformo e' monotono, quindi le due decisioni coincidono nel " + f"{float(np.mean(ov))*100:.1f}% dei trade") + print(f" (tenuti {float(np.mean(k_me))*100:.1f}% io / {float(np.mean(k_sk))*100:.1f}% lui); " + f"max|dif| fra i due libri = {dleg:.2e}.") + + # ---------------------------------------------------------- §5 i tre componenti del gate + print("\n" + "-" * L) + print(" 5. `study_family_honest` — NON chiamabile direttamente (e' cablato sui candidati") + print(" DIREZIONALI: factory -> target_fn via candidate_daily, e qui il candidato e' un") + print(" overlay su uno sleeve). Come il 30/07 sul tenore VRP01, uso i suoi TRE") + print(" COMPONENTI REALI: selezione in-sample-only -> deflated_sharpe -> marginal_vs_tp01.") + print("-" * L) + print(" (a) SELEZIONE IN-SAMPLE-ONLY — due criteri, entrambi dichiarati, entrambi sulla") + print(" MEDIANA fra le ancore (l'ancora non e' un hold-out, e' una dimensione di luck)") + sel_leg = max(ALLC, key=lambda r: r["legI"]) # come `select_cell_insample` + sel_iso = max(ALLC, key=lambda r: r["isoI"]) # cio' su cui si decide davvero + hop_leg = max(ALLC, key=lambda r: r["legH"]) + hop_iso = max(ALLC, key=lambda r: r["dH"]) + for tag, r in (("Sharpe IS standalone della gamba (come select_cell_insample)", sel_leg), + ("ISO dShFULL IN-SAMPLE del libro (cio' che decide)", sel_iso)): + n = cname(r["thr"], r["w"], r["h"], r["sg"]) + print(f" scelta al buio su {tag}") + print(f" -> {n:<26} ISO dShFULL {r['dF']:+.3f} ({r['nF']}/{len(offs)}) " + f"ISO dShHOLD {r['dH']:+.3f} ({r['nH']}/{len(offs)}) banda " + f"[{r['lo']:+.3f},{r['hi']:+.3f}]") + for tag, r in (("Sharpe HOLD-OUT standalone", hop_leg), ("ISO dShHOLD del libro", hop_iso)): + print(f" controllo (scegliendo SULL'HOLD-OUT, cio' che NON si fa) su {tag}:") + print(f" -> {cname(r['thr'], r['w'], r['h'], r['sg']):<26} dShFULL {r['dF']:+.3f}" + f" dShHOLD {r['dH']:+.3f}") + CHOSEN = sel_iso + nmc = cname(CHOSEN["thr"], CHOSEN["w"], CHOSEN["h"], CHOSEN["sg"]) + print(f" CELLA SCELTA AL BUIO (criterio primario = ISO dShFULL in-sample): {nmc}") + + print("\n (b) DEFLATED SHARPE — sullo Sharpe FULL standalone della gamba (la grandezza che") + print(" `select_cell_insample` usa), mediana fra le ancore; `daily` della barra canonica") + print(" serve solo per T/asimmetria/curtosi. Sensibilita' al CONTEGGIO ottenuta") + print(" REPLICANDO la distribuzione dei trial osservati (tiling), non riempiendola con") + print(" la mediana: riempire con la mediana ABBASSA la varianza dei trial e fa SALIRE il") + print(" DSR all'aumentare di N, che e' il verso sbagliato. (Errore mio, catturato nel") + print(" pilota e corretto: il primo output dava DSR 0,966 -> 0,986 -> 0,998 da N=144 a") + print(" N=500, cioe' 'piu' trial = piu' significativo'.)") + all_sr = [r["legF"] for r in ALLC] + pos_sr = [r["legF"] for r in POS] + base_sr = float(np.median([sh(base_leg[o]) for o in offs])) + dly = LEG[nmc][can] + + def tile(v, N): + return (list(v) * (N // len(v) + 1))[:N] + + for lab, sr, d in (("cella scelta al buio", CHOSEN["legF"], dly), + ("BASELINE (size fissa)", base_sr, base_leg[can])): + for N, tagN in ((len(ALLC), f"{len(ALLC)} di QUESTA famiglia"), + (len(ALLC) + N_INHERITED, + f"{len(ALLC)+N_INHERITED} = conto onesto (+{N_INHERITED} ereditati)"), + (500, "500 (ipotetico, al rialzo)")): + dsr, sr0 = A.deflated_sharpe(sr, tile(all_sr, N), d) + print(f" {lab:<22} N={tagN:<44} DSR {dsr:.3f} max atteso sotto il null {sr0:+.3f}") + lo_n, hi_n = len(ALLC), 20000 + if A.deflated_sharpe(CHOSEN["legF"], tile(all_sr, hi_n), dly)[0] >= 0.95: + cross = f">{hi_n}" + elif A.deflated_sharpe(CHOSEN["legF"], tile(all_sr, lo_n), dly)[0] < 0.95: + cross = f"<{lo_n}" + else: + for _ in range(40): + mid = (lo_n + hi_n) // 2 + if A.deflated_sharpe(CHOSEN["legF"], tile(all_sr, mid), dly)[0] >= 0.95: + lo_n = mid + else: + hi_n = mid + cross = f"~{lo_n}" + print(f" -> IL VERDETTO SI RIBALTA A N {cross} trial: sotto quel conteggio il DSR passa,") + print(f" sopra fallisce. Il conto onesto dichiarato ({len(ALLC)+N_INHERITED}) sta dal lato") + print(f" del PASS. Precedente 30/07: un DSR passava a N=8, falliva a N=72 e a N=360; qui") + print(f" la stessa sensibilita' e' pubblicata invece di essere taciuta.") + dsp, sr0p = A.deflated_sharpe(CHOSEN["legF"], tile(pos_sr, len(POS) + N_INHERITED), dly) + print(f" sensibilita' alla COMPOSIZIONE della famiglia: escludendo il segno -1 (che e' un") + print(f" controllo e non un candidato) i trial diventano {len(POS)}+{N_INHERITED} e la loro") + print(f" dispersione cala -> DSR {dsp:.3f} (max atteso {sr0p:+.3f}). Includere il segno -1") + print(f" e' la scelta CONSERVATIVA e resta quella dichiarata.") + print(f" dispersione dei trial: sd(Sharpe FULL) = {float(np.std(all_sr, ddof=1)):.3f} sulle") + print(f" {len(ALLC)} celle, {float(np.std(pos_sr, ddof=1)):.3f} sulle {len(POS)} di segno +1.") + print(f" -> il DSR qui NON e' vacuo (il baseline lo fallisce dove il candidato lo passa):") + print(f" la famiglia contiene celle davvero diverse fra loro, al contrario della famiglia") + print(f" VOL-SIZE dove erano tutte perturbazioni della stessa size continua.") + + print("\n (c) MARGINAL vs TP01 — sulla gamba modificata e sulla gamba base") + for lab, s in (("BASE (size fissa)", base_leg[can]), (nmc, LEG[nmc][can]), + ("cella dello scettico", LEG[nmcan][can])): + m = A.marginal_vs_tp01(s) + b25 = m.get("blends", {}).get("w25", {}) + print(f" {lab:<26} {str(m.get('marginal_verdict')):<10} corr {m.get('corr_full')} " + f"uplift w25 full {b25.get('uplift_full')} / hold {b25.get('uplift_hold')} " + f"robust_oos={m.get('robust_oos')} insample_edge={m.get('has_insample_edge')} " + f"hedge={m.get('is_hedge')}") + imp = A.implausible_sharpe( + LEG[nmc][can], + n_trades=int(sum(len(EX[can][a]["net"]) for a in ASSETS)), + n_losing_trades=int(sum((EX[can][a]["net"] < 0).sum() for a in ASSETS))) + print(f" implausible_sharpe (gamba scelta): implausible={imp['implausible']} " + f"attive {imp.get('active_frac', 0)*100:.1f}% perdite/attive " + f"{imp.get('loss_frac', 0)*100:.1f}% Calmar {imp.get('calmar', 0):.1f}") + + # ---------------------------------------------------------- §6 null con potenza + print("\n" + "-" * L) + print(" 6. IL NULL CHE HA POTENZA — permutazione della SEQUENZA tenuti/scartati.") + print(" Stessa quota di trade tenuti, riassegnata a caso: separa 'conta QUALE trade si") + print(" scarta' (informazione) da 'conta scartarne meta'' (aritmetica). Una mediana del") + print(" null lontana da 0 direbbe che il null non e' un null (lezione 22/08 sul null") + print(f" invertito). {args.ndraw} estrazioni su TUTTE le {len(offs)} ancore, non su 3.") + print("-" * L) + rng = np.random.default_rng(20260822) + for lab, r in (("cella scelta al buio", CHOSEN), ("cella dello scettico", RES[nmcan]), + ("argmax di famiglia", argmax)): + nm = cname(r["thr"], r["w"], r["h"], r["sg"]) + real = float(np.median([sh(BK[nm][o]) - sh(CT[nm][o]) for o in offs])) + fn = size_bin(r["thr"], r["w"], r["h"], r["sg"]) + sz0 = {o: {a: fn(EX[o][a], (o, a)) for a in ASSETS} for o in offs} + nul = [] + for _ in range(args.ndraw): + v = [] + for o in offs: + szp = {a: rng.permutation(sz0[o][a]) for a in ASSETS} + lg = V.leg_daily(EX[o], szp) + b, c, _ = iso_pair(TP, lg, base_leg[o]) + v.append(sh(b) - sh(c)) + nul.append(float(np.median(v))) + nul = np.asarray(nul) + pc = float((nul < real).mean()) + print(f" {lab:<24} {nm:<24} reale {real:+.3f} null mediana {np.median(nul):+.3f} " + f"p90 {np.percentile(nul, 90):+.3f} max {nul.max():+.3f} -> p ~ {1-pc:.3f}") + print(f" (seme 20260822; {args.ndraw} estrazioni x {len(offs)} ancore)") + print("\n ⚠ LA MEDIANA DEL NULL NON E' ~0, E NON E' UN DIFETTO DEL NULL: E' UN FATTO.") + print(" Scartare a caso meta' dei trade DANNEGGIA, perche' lo Sharpe ANNUALIZZATO cresce") + print(" con la radice del numero di scommesse per anno: dimezzare i trade sullo stesso") + print(" arco di calendario costa un fattore ~sqrt(2) di Sharpe di gamba. Misurato:") + nmv = cname(CHOSEN["thr"], CHOSEN["w"], CHOSEN["h"], CHOSEN["sg"]) + fnv = size_bin(CHOSEN["thr"], CHOSEN["w"], CHOSEN["h"], CHOSEN["sg"]) + rng2 = np.random.default_rng(20260823) + shb = float(np.median([sh(base_leg[o]) for o in offs])) + shv = float(np.median([sh(LEG[nmv][o]) for o in offs])) + kp = float(np.median([np.mean(np.concatenate([fnv(EX[o][a], (o, a)) for a in ASSETS])) + for o in offs])) + shn = [] + for _ in range(min(40, args.ndraw)): + sz0 = {o: {a: fnv(EX[o][a], (o, a)) for a in ASSETS} for o in offs} + shn.append(float(np.median([sh(V.leg_daily(EX[o], {a: rng2.permutation(sz0[o][a]) + for a in ASSETS})) for o in offs]))) + print(f" Sharpe della GAMBA: baseline (tutti i trade) {shb:.3f} -> permutata " + f"{float(np.median(shn)):.3f} -> filtrata {shv:.3f}") + print(f" quota tenuta {kp*100:.0f}% -> attesa dalla sola perdita di scommesse " + f"{shb*np.sqrt(kp):.3f}; osservata sotto permutazione {float(np.median(shn)):.3f}") + print(" Conseguenza per la lettura: il null NON e' centrato su zero, quindi il p-value") + print(" risponde alla domanda 'la SELEZIONE batte una selezione casuale della stessa") + print(" taglia?' e non alla domanda 'il filtro batte il non filtrare?'. La seconda e' il") + print(" confronto con la BASE (§4), che e' quello su cui si decide. Il filtro deve") + print(" superare una penale meccanica prima di guadagnare qualcosa: e' un test SEVERO.") + + # ---------------------------------------------------------- §7 iso-peso / de-levering + print("\n" + "-" * L) + print(" 7. ISO-PESO e NULL DEL DE-LEVERING") + print("-" * L) + print(f" {'cella':<26}{'wSKH eff':>10}{'dShFULL RAW':>13}{'ISO':>9}{'maxDD var':>11}" + f"{'maxDD ctrl':>11}{'k_iso_dd':>10}") + for lab, r in (("BASE", None), ("cella dello scettico", RES[nmcan]), + ("cella scelta al buio", CHOSEN)): + if r is None: + print(f" {'BASE (size fissa)':<26}{W_SKH:>10.3f}{0.0:>+13.3f}{0.0:>+9.3f}" + f"{maxdd(book(TP, base_leg[can]))*100:>10.1f}%{'-':>11}{'-':>10}") + continue + nm = cname(r["thr"], r["w"], r["h"], r["sg"]) + raw = A.anchor_luck_delta(lambda o, n=nm: BK[n][o], + lambda o: book(TP, base_leg[o]), offs, metric=sh) + dv = float(np.median([maxdd(BK[nm][o]) for o in offs])) + dc = float(np.median([maxdd(CT[nm][o]) for o in offs])) + ks = (f"{k_iso_dd(CT[nm][can], BK[nm][can]):.3f}" if dv < dc + else "n/a") # n/a = non riduce il DD: nulla da negare + print(f" {nm:<26}{r['weff']:>10.3f}{raw['median_paired']:>+13.3f}{r['dF']:>+9.3f}" + f"{dv*100:>10.1f}%{dc*100:>10.1f}%{ks:>10}") + print(" Il null del de-levering e' DEGENERE su una ri-scalatura (sh(k*base) === sh(base)):") + print(" qui il test e' l'iso-peso sullo Sharpe. `k_iso_dd` e' riportato come diagnostica del") + print(" DD ed e' calcolato CONTRO il controllo iso-peso (non contro il baseline nudo).") + print(" NB il binario ABBASSA il peso effettivo di SKH01, non lo alza: la preoccupazione del") + print(" 26/07 ('alzare SKH01') non morde, ma l'iso-peso resta obbligatorio nell'altro verso.") + print(f"\n LIVELLI del libro (mediana fra le {len(offs)} ancore), netti fee:") + print(f" {'configurazione':<30}{'ShFULL':>9}{'ShHOLD':>9}{'ShIS':>8}{'maxDD':>9}{'CAGR':>9}" + f"{'vol':>8}") + lev = [("BASE (libro live 75/25)", {o: book(TP, base_leg[o]) for o in offs})] + for lab, r in (("BIN, peso eff. lasciato cadere", RES[nmcan]), + ("BIN, controllo ISO-PESO", None)): + if r is None: + lev.append((lab, {o: CT[nmcan][o] for o in offs})) + else: + lev.append((lab, {o: BK[nmcan][o] for o in offs})) + for lab, dd_ in lev: + f_ = float(np.median([sh(dd_[o]) for o in offs])) + h_ = float(np.median([sh(hold(dd_[o])) for o in offs])) + i_ = float(np.median([sh(ins(dd_[o])) for o in offs])) + d_ = float(np.median([maxdd(dd_[o]) for o in offs])) + c_ = float(np.median([cagr(dd_[o]) for o in offs])) + v_ = float(np.median([vol(dd_[o]) for o in offs])) + print(f" {lab:<30}{f_:>9.3f}{h_:>9.3f}{i_:>8.3f}{d_*100:>8.1f}%{c_*100:>8.1f}%" + f"{v_*100:>7.1f}%") + print(" ⚠ la DIFFERENZA di queste mediane NON e' la mediana delle differenze appaiate (§4):") + print(" qui le mediane cadono su ancore diverse. La tabella serve a dare i LIVELLI netti fee") + print(" richiesti dal contratto; il numero che decide resta quello appaiato.") + + # ---------------------------------------------------------- §8 weights_tilt_null + print("\n" + "-" * L) + print(" 8. `weights_tilt_null` — una modulazione binaria della size E' un cambio di pesi nel") + print(" tempo. Il gate confronta vettori STATICI: cattura la sola componente di SCALA") + print(" (limite dichiarato, come nel bersaglio). k_seen = tutti i trial dichiarati.") + print("-" * L) + cols = {"TP01": TP, "SKH01": base_leg[can]} + for lab, r in (("cella dello scettico", RES[nmcan]), ("cella scelta al buio", CHOSEN), + ("argmax di famiglia", argmax)): + nm = cname(r["thr"], r["w"], r["h"], r["sg"]) + m = MS[nm][can] + wp = {"TP01": W_TP, "SKH01": W_SKH * m} + g = weights_tilt_null(cols, {"TP01": W_TP, "SKH01": W_SKH}, wp, caps={"SKH01": 0.5}, + floor=0.05, n=300, k_seen=len(ALLC) + N_INHERITED) + print(f" {lab:<24} {nm:<24} peso equiv SKH {wp['SKH01']/sum(wp.values()):.3f} " + f"d_IS {g['delta_insample']:+.4f} d_HOLD {g['delta_hold']:+.4f} " + f"pctl {g['pctl_hold']:.1f} gate_pass={g['gate_pass']}") + print(" ⚠ LIMITE, e va detto perche' e' aggirabile: il binario ABBASSA il peso effettivo di") + print(" SKH01, e il gate misura proprio quella caduta. Nella forma di deploy naturale (si") + print(" ri-scala la gamba per riportare il peso effettivo a 0,25) il vettore proposto e'") + print(" IDENTICO a quello corrente -> d_IS = d_HOLD = 0 e il gate diventa INERTE, non") + print(" superato. Quindi `weights_tilt_null` NON puo' essere l'evidenza decisiva qui: e'") + print(" negativo nella forma nuda e vacuo nella forma compensata. Cio' che decide resta la") + print(" banda APPAIATA a iso-peso (§4) e la stima di famiglia.") + + # ---------------------------------------------------------- §9 eseguibilita' + print("\n" + "-" * L) + print(f" 9. ESEGUIBILITA' a ${CAPITAL:.0f} — il vantaggio DICHIARATO dal filone (dimezza gli") + print(" ordini). Target NETTATO 0.75*TP01 + 0.25*SKH sul grid 1h = cio' che il cron manda.") + print("-" * L) + print(f" {'configurazione':<26}{'Sh model':>10}{'Sh reale':>10}{'haircut':>9}" + f"{'ordini eseg':>13}{'sotto-min':>11}{'turnover/a':>12}") + EXEC = {} + for lab, r in (("BASE (size fissa)", None), ("cella dello scettico", RES[nmcan]), + ("cella scelta al buio", CHOSEN)): + nm = None if r is None else "SEL" + tm = tr = tu = 0.0 + no = ns = 0 + for a in ASSETS: + df1h = TO.get1h(a) + SZa = {} if r is None else { + "SEL": (lambda ex, rr=r, aa=a: size_bin(rr["thr"], rr["w"], rr["h"], + rr["sg"])(ex, (can, aa)))} + tgt = V.netted_target(a, EX[can][a], nm, SZa, {}, {}, {}, {}, TP, can, df1h) + ev = A.eval_weights_smallcap(df1h, tgt, capital=CAPITAL, min_order=MIN_ORDER) + tm += 0.5 * ev["modeled"]["sharpe"] + tr += 0.5 * ev["realistic"]["sharpe"] + tu += ev["executed_turnover_per_year"] + no += ev["n_executed_trades"] + dw = np.abs(np.diff(np.nan_to_num(tgt), prepend=0.0)) + ns += int(((dw > 1e-12) & (dw * CAPITAL < MIN_ORDER)).sum()) + EXEC[lab] = (tm, tr, no, ns, tu) + print(f" {lab:<26}{tm:>10.3f}{tr:>10.3f}{tm-tr:>9.3f}{no:>13}{ns:>11}{tu:>12.1f}") + print(" 'ordini eseg' e 'sotto-min' sono sui due asset sommati, su tutta la storia 1h.") + ntr_base = int(sum(len(EX[can][a]['i_ent']) for a in ASSETS)) + ntr_sel = int(sum((size_bin(CHOSEN['thr'], CHOSEN['w'], CHOSEN['h'], CHOSEN['sg']) + (EX[can][a], (can, a)) > 0).sum() for a in ASSETS)) + print(f" trade SKH eseguiti (off{can}): BASE {ntr_base} -> cella scelta {ntr_sel} " + f"({ntr_sel/ntr_base*100:.0f}%)") + print(" ⚠ IL RISPARMIO DI FEE E' GIA' DENTRO i numeri sopra: `extract` netta ogni trade di") + print(f" FEE_RT={V.FEE_RT:.4f} (10 bps RT) all'ingresso, quindi un trade scartato non paga.") + print(" Convertirlo di nuovo con la curva del 26/07 (-0,017 Sh/bps di libro, SKH 4x TP01)") + print(" sarebbe un DOPPIO CONTEGGIO. Cio' che NON e' dentro e' (a) il min-order a $635,") + print(" misurato qui sopra come haircut, e (b) il fatto che la fee reale e' 3,5 bps/lato =") + print(" 7 bps RT contro i 10 modellati -> i backtest sono CONSERVATIVI, in entrambe le braccia.") + + # ---------------------------------------------------------- §9b fee vs informazione + print("\n" + "-" * L) + print(" 9b. QUANTO DELL'EFFETTO E' INFORMAZIONE E QUANTO E' SOLO FEE RISPARMIATA.") + print(" Scartare meta' dei trade risparmia meta' delle fee: parte del +0,080 e' quella,") + print(" ed e' un guadagno vero ma NON e' un'ipotesi sulla vol. Lo stesso confronto,") + print(" identico in tutto, ricalcolato a fee diverse (le size si ricalcolano da zero,") + print(" perche' la vol trailing della gamba dipende anche dalle fee).") + print("-" * L) + print(f" {'lente':<40}{'cella scettico':>17}{'pos/n':>8}{'cella al buio':>17}{'pos/n':>8}") + FEEDEC = {} + keep_fee = V.FEE_RT + for fee, tag in ((keep_fee, f"MODELLATA {keep_fee*1e4:.0f} bps RT (i backtest)"), + (0.0007, "REALE 3,5 bps/lato = 7 bps RT"), + (0.0, "fee ZERO (informazione pura)")): + V.FEE_RT = fee + if fee == keep_fee: + EXf, basef = EX, base_leg + else: + EXf = {o: {a: V.extract(a, o) for a in ASSETS} for o in offs} + basef = {o: V.leg_daily(EXf[o], {a: V.size_flat(EXf[o][a]) for a in ASSETS}) + for o in offs} + row = "" + for r in (RES[nmcan], CHOSEN): + fn = size_bin(r["thr"], r["w"], r["h"], r["sg"]) + bk, ct = {}, {} + for o in offs: + lg = V.leg_daily(EXf[o], {a: fn(EXf[o][a], (tag, o, a)) for a in ASSETS}) + bk[o], ct[o], _ = iso_pair(TP, lg, basef[o]) + d = A.anchor_luck_delta(lambda o: bk[o], lambda o: ct[o], offs, metric=sh) + FEEDEC[(tag, cname(r["thr"], r["w"], r["h"], r["sg"]))] = d["median_paired"] + row += f"{d['median_paired']:>+17.3f}{d['n_positive']:>5}/{len(offs):<3}" + print(f" {tag:<40}{row}") + if fee != keep_fee: + del EXf, basef + V.FEE_RT = keep_fee + assert V.FEE_RT == 0.001 + f_mod = FEEDEC[(f"MODELLATA {keep_fee*1e4:.0f} bps RT (i backtest)", nmcan)] + f_zero = FEEDEC[("fee ZERO (informazione pura)", nmcan)] + print(f" -> a fee ZERO resta {f_zero:+.3f} di {f_mod:+.3f} = " + f"{100*f_zero/f_mod if f_mod else float('nan'):.0f}% dell'effetto: la parte restante") + print(" e' fee risparmiata. La fee vera oggi e' 3,5 bps/lato, meno di quella modellata,") + print(" quindi nel LIVE la componente-fee del vantaggio e' PIU' PICCOLA di quella misurata") + print(" sui backtest — il verso in cui il numero pubblicato e' ottimista per il deploy.") + + # ---------------------------------------------------------- §10 trasferimento su V1 + print("\n" + "-" * L) + print(" 10. TRASFERIMENTO su SKH01_V1 — seconda sequenza di trade, geometria d'uscita") + print(" DIVERSA (stop/target in ATR invece che a percentuale fissa). Non e' un hold-out") + print(" temporale, ma e' l'unico campione indipendente disponibile a questo costo.") + print("-" * L) + EX1 = {o: {a: V.extract(a, o, SKH01_V1) for a in ASSETS} for o in offs} + base1 = {o: V.leg_daily(EX1[o], {a: V.size_flat(EX1[o][a]) for a in ASSETS}) for o in offs} + print(f" {'cella':<26}{'ISO dShFULL':>13}{'pos/n':>8}{'ISO dShHOLD':>14}{'pos/n':>8}" + f"{'tenuti':>9}") + for lab, r in (("cella dello scettico", RES[nmcan]), ("cella scelta al buio", CHOSEN)): + fn = size_bin(r["thr"], r["w"], r["h"], r["sg"]) + b1, c1, kp = {}, {}, [] + for o in offs: + lg = V.leg_daily(EX1[o], {a: fn(EX1[o][a], ("V1", o, a)) for a in ASSETS}) + b1[o], c1[o], _ = iso_pair(TP, lg, base1[o]) + kp.append(float(np.mean(np.concatenate( + [fn(EX1[o][a], ("V1", o, a)) for a in ASSETS])))) + dF = A.anchor_luck_delta(lambda o: b1[o], lambda o: c1[o], offs, metric=sh) + dH = A.anchor_luck_delta(lambda o: hold(b1[o]), lambda o: hold(c1[o]), offs, metric=sh) + print(f" {cname(r['thr'], r['w'], r['h'], r['sg']):<26}{dF['median_paired']:>+13.3f}" + f"{dF['n_positive']:>5}/{len(offs):<3}{dH['median_paired']:>+14.3f}" + f"{dH['n_positive']:>5}/{len(offs):<3}{float(np.median(kp))*100:>8.0f}%") + + # ---------------------------------------------------------- §11 per anno + print("\n" + "-" * L) + print(" 11. SCOMPOSIZIONE PER ANNO (regola 22/08 dopo SOL: un contributo positivo si") + print(" scompone per ANNO prima di crederci — 24/24 ancore possono essere un anno solo)") + print("-" * L) + yrs = sorted({int(y) for y in BK[nmcan][can].index.year}) + print(f" {'cella':<26}" + "".join(f"{y:>8}" for y in yrs)) + for lab, r in (("cella dello scettico", RES[nmcan]), ("cella scelta al buio", CHOSEN)): + nm = cname(r["thr"], r["w"], r["h"], r["sg"]) + cc = [] + for y in yrs: + v = [sh(BK[nm][o][BK[nm][o].index.year == y]) - sh(CT[nm][o][CT[nm][o].index.year == y]) + for o in offs if (BK[nm][o].index.year == y).sum() > 20] + cc.append(float(np.median(v)) if v else float("nan")) + print(f" {nm:<26}" + "".join(f"{c:>+8.2f}" for c in cc) + + f" positivi {int(np.nansum(np.asarray(cc) > 0))}/{len(yrs)}") + + # ---------------------------------------------------------- §12 la domanda che decide + print("\n" + "-" * L) + print(" 12. LA DOMANDA CHE DECIDE — un effetto che vale un terzo della propria incertezza") + print(" merita di cambiare un sistema che gira con soldi veri?") + print("-" * L) + amp = max(ban_F) - min(ban_F) + nm = nmcan + dC = float(np.median([cagr(BK[nm][o]) - cagr(CT[nm][o]) for o in offs])) + cag_band = [cagr(book(TP, base_leg[o])) for o in offs] + print(f" (i) IL CONFRONTO GIUSTO NON E' QUELLO DELLA DOMANDA. La banda d'ancora del") + print(f" LIVELLO ({amp:.3f} di ampiezza, +-{amp/2:.3f}) e' incertezza sul livello, e") + print(f" si CANCELLA in un confronto APPAIATO: le due braccia girano sulla STESSA") + print(f" ancora. Cio' che va confrontato con l'effetto e' la dispersione della") + print(f" DIFFERENZA appaiata, che e' un ordine di grandezza piu' piccola.") + print(f" cella scettico: effetto {RES[nm]['dF']:+.3f}, banda appaiata " + f"[{RES[nm]['lo']:+.3f},{RES[nm]['hi']:+.3f}], " + f"{RES[nm]['nF']}/{len(offs)} ancore positive.") + print(f" (ii) MA la banda del livello torna a mordere DUE volte, e in modo decisivo:") + print(f" (a) la SCELTA della cella e' fatta guardando quella banda -> la stima onesta") + print(f" non e' l'argmax ma la MEDIANA DI FAMIGLIA " + f"({float(np.median([r['dF'] for r in POS])):+.3f} sul segno +1);") + print(f" (b) l'effetto e' misurato sugli STESSI 8 anni su cui SKH01 e' stato") + print(f" selezionato, e non esiste un forward capace di misurarlo (SE(Sharpe)") + print(f" ~1,4 su 6 mesi contro un effetto di 0,08).") + print(f" (iii) IL CRITERIO, dichiarato: si tocca il libro live solo se TUTTE E QUATTRO —") + print(f" (1) l'effetto sopravvive alla stima ONESTA di famiglia (mediana, non argmax)") + print(f" (2) `weights_tilt_null` passa (e' il gate codificato per i cambi di peso)") + print(f" (3) il gate marginale + un null CON POTENZA lo sostengono") + print(f" (4) l'effetto e' grande abbastanza da pagare un modo di fallire in piu'.") + print(f" (iv) ORDINI DI GRANDEZZA a ${CAPITAL:.0f} (regola 26/07: quando le leve hanno") + print(f" ordini di grandezza diversi, DIRLO):") + print(f" effetto della cella (ISO, mediana appaiata): dCAGR {dC*100:+.2f}%/anno") + print(f" = ${dC*CAPITAL:+.2f}/anno = EUR {dC*CAPITAL/365*0.86:+.4f}/giorno") + print(f" banda d'ancora del CAGR del libro: " + f"[{min(cag_band)*100:.1f}%, {max(cag_band)*100:.1f}%] " + f"= ${(max(cag_band)-min(cag_band))*CAPITAL:.0f}/anno di incertezza") + print(f" versare EUR 500/mese (il piano dell'operatore) = EUR 16,44/giorno di capitale") + print(f" nuovo, cioe' ~{16.44/max(1e-9, abs(dC*CAPITAL/365*0.86)):.0f}x l'effetto misurato qui.") + print(f" (v) IL CRITERIO APPLICATO, riga per riga:") + med_pos = float(np.median([r['dF'] for r in POS])) + c1 = med_pos > 0 + c3 = True + print(f" (1) stima ONESTA di famiglia (mediana segno +1) = {med_pos:+.3f} " + f"contro argmax {argmax['dF']:+.3f} e cella pubblicata {RES[nmcan]['dF']:+.3f}" + f" -> {'PASS' if c1 else 'FAIL'}") + print(f" (2) `weights_tilt_null`: gate_pass=False nella forma nuda, INERTE nella forma") + print(f" compensata -> il gate non lo sostiene in nessuna delle due -> FAIL/vacuo") + print(f" (3) marginale ADDS + null di permutazione con potenza (§6) -> vedi sopra") + print(f" (4) taglia contro il modo di fallire in piu': " + f"EUR {dC*CAPITAL/365*0.86:+.4f}/giorno a ${CAPITAL:.0f} -> FAIL") + print(f" Di cui, per giunta, solo il {100*f_zero/f_mod if f_mod else float('nan'):.0f}% " + f"e' informazione e il resto e' fee risparmiata (§9b),") + print(f" e la fee vera e' piu' bassa di quella modellata.") + + print("\n" + "=" * L) + print(f" fatto in {time.time()-t0:.0f}s") + print("=" * L) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822b_bocpd.py b/scripts/research/r0822b_bocpd.py new file mode 100644 index 0000000..0673a12 --- /dev/null +++ b/scripts/research/r0822b_bocpd.py @@ -0,0 +1,957 @@ +#!/usr/bin/env python +"""r0822b_bocpd.py — IL FALSIFICATORE NOMINATO E MAI ESEGUITO del filone ADAPTIVE-HORIZON. + +CONTESTO (perche' questo file esiste) +------------------------------------- +Il 22/08 `r0822_adaptive_horizon.py` ha scartato il TSMOM a orizzonte adattivo con una diagnosi +netta: il "vincitore adattivo" aveva L_t **incollato al bordo il 100% del tempo** = un TSMOM a +orizzonte COSTANTE di 20 giorni travestito (corr 1.000, dSh 0.00 in 8/8 anni e 0/12 ancore); +isolando le 52 celle davvero adattive -> NEUTRAL, corr->TP01 0.90 = TP01 travestito. +Ma quell'agente ha dichiarato **cosa lo smentirebbe** e non ha potuto eseguirlo: + + "Un rilevatore il cui L_t si muove DAVVERO e batte il miglior lookback COSTANTE (non TP01) in + >=6/8 anni appaiati con corr->TP01 < 0.6. La mia famiglia copre 3 rilevatori a soglia/finestra + fissa — un run-length posterior bayesiano (BOCPD/PELT vero) o un L scelto per walk-forward sul + PnL realizzato non sono stati provati." + +Questo file esegue **quel** test. Oggi il filone e' chiuso su UNA famiglia (3 proxy a soglia +fissa), non sul MECCANISMO. Una seconda chiusura, con rilevatori veri, rende il "no" definitivo. + +PERCHE' BOCPD E' STRUTTURALMENTE DIVERSO DAI 3 PROXY DEL PRIMO TENTATIVO +----------------------------------------------------------------------- +I 3 proxy (CUSUM a soglia k, variance-ratio, Hurst) producevano una STATISTICA che veniva poi +MAPPATA linearmente dentro [Lmin, Lmax]: la mappatura e' arbitraria, e infatti la statistica +saturava contro il clip (bordo 100%). Un run-length posterior NON ha bisogno di mappatura: +la lunghezza di run **e' gia' un lookback**, nelle stesse unita' (barre). "Quanto indietro posso +guardare" = "da quanto tempo non cambia il regime" e' un'identita', non una scelta di scala. + * BOCPD (Adams & MacKay 2007): posterior online su r_t con hazard costante e verosimiglianza + Normale-InverseGamma coniugata (predittiva Student-t). Causale PER COSTRUZIONE: e' un filtro + in avanti, ogni P(r_t | x_{1:t}) usa solo x_{1:t}. + * OP/PELT causale: PELT e' un'accelerazione (pruning) dell'**optimal partitioning** + F(t) = min_{s e' un asse] + blend 2 (single L | triple L/3,L,2L — con L=90 il triple E' TP01) + timeframe 2 (1d | 12h) [>=12h: sotto, costi+overfit dominano, 19/06] + = 2*3*2*2*2*2 = **96 trial** + B) controllo L COSTANTE 12 L x 2 blend x 2 TF = **48 trial** + C) WF-PnL (L da walk-forward) 3 finestre x 2 blend x 2 TF = **12 trial** + D) taratura della SCALA 8 penalita' OP + 5 hazard BOCPD = **13** (sezione 1-pre) + ---------------------------------------------------------------- TOTALE DICHIARATO **169** +⚠️ (D) e' una scansione fatta guardando SOLO il numero di change-point rilevati — una proprieta' +del RILEVATORE — e mai uno Sharpe: nessuna di quelle 13 configurazioni produce un rendimento. Si +dichiara lo stesso, al rialzo, perche' ha ristretto la griglia di (A). Il suo esito e' gia' un +risultato: a penalita' OP >= 5 l'eta' resta incollata al tetto della finestra il 44% del tempo, +cioe' **l'artefatto del 22/08 riprodotto su un rilevatore vero** — e la griglia (A) usa solo la +regione in cui il rilevatore rileva davvero (0.5/1.0/2.0 = 18-40 change-point l'anno). +Costanti FISSATE A PRIORI (gradi di liberta' non cercati, dichiarati perche' esistono): +Lmin=20g, Lmax=200g (contengono 30..180 del canonico e sono gli stessi del primo tentativo); +BOCPD su rendimenti log STANDARDIZZATI da vol rolling causale 30g (il bersaglio e' il DRIFT, non +la vol); prior NIG mu0=0, kappa0=1, alpha0=1, beta0=1; troncamento del posterior a Lmax barre; +costo OP = mean-shift gaussiano su dati standardizzati, penalita' beta = k*log(t) causale. +Il deflated-Sharpe e' riportato a N=169 (mia famiglia), N=281 (mia + le 112 del primo tentativo: +**stessa domanda**, i trial si sommano) e N=449 (di SCREEN — lezione 22/08 del filone ORTHO: +su BTC/ETH direzionale il massimo atteso dal rumore su 168 trial e' Sharpe 1.572, SOPRA il +soffitto misurato ~1.3, quindi il conteggio di screen e' quello che decide). + +ORDINE DI ESECUZIONE (imposto dalla missione, ed e' l'ordine giusto) +-------------------------------------------------------------------- +La diagnostica che ha smascherato il primo tentativo va PER PRIMA: quanto si muove davvero L_t? +Se sta al bordo, l'artefatto e' riprodotto e il filone si chiude di nuovo senza guardare i +rendimenti. Solo se L_t si muove ha senso misurare se il movimento PAGA. + +Runtime atteso ~6-10 min su 2 core. Nessun file toccato fuori da questo. +""" +from __future__ import annotations + +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd +from scipy.special import gammaln +from scipy.stats import norm + +ROOT = Path("/opt/docker/PythagorasGoal") +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +sys.path.insert(0, str(ROOT)) + +import altlib as A # noqa: E402 +from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402 + +ASSETS = A.CERTIFIED +HOLDOUT = A.HOLDOUT + +# ---- costanti FISSATE A PRIORI (dichiarate sopra, non cercate) ------------------------- +LMIN_D, LMAX_D = 20, 200 # banda dell'orizzonte, IDENTICA al primo tentativo +FIXED_H = (30, 90, 180) # == (L/3, L, 2L) con L=90 -> TP01 canonico +TGT_VOL, LEV, VOLWIN = 0.20, 2.0, 30 +NIG_MU0, NIG_KAP0, NIG_AL0, NIG_BE0 = 0.0, 1.0, 1.0, 1.0 +STD_WIN_D = 30 # finestra della standardizzazione causale dei rendimenti + +TFS = ("1d", "12h") +DETECTORS = ("bocpd", "op") +PARAMS = {"bocpd": (50, 100, 250), "op": (0.5, 1.0, 2.0)} +# TARATURA DELLA SCALA (sezione 1-pre): scansione ESPLORATIVA fatta guardando SOLO il numero di +# change-point rilevati — una proprieta' del RILEVATORE — e mai lo Sharpe. Un rilevatore che non +# rileva nulla non e' un rilevatore: a pen_k>=5 l'eta' resta incollata al tetto della finestra +# (44% del tempo) = ESATTAMENTE l'artefatto del 22/08, riprodotto qui su un rilevatore vero. +CAL_PEN = (0.02, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0) +CAL_LAM = (25, 50, 100, 250, 500) +STATS = {"bocpd": ("mean", "map"), "op": ("age", "segmed")} +MAPPINGS = ("direct", "inverse") +BLENDS = ("single", "triple") + +CONST_L = (5, 7, 10, 15, 20, 30, 45, 60, 90, 120, 150, 200) +WF_WIN_D = (180, 365, 730) + +N_DECLARED_A = len(DETECTORS) * 3 * 2 * len(MAPPINGS) * len(BLENDS) * len(TFS) # 96 +N_DECLARED_B = len(CONST_L) * len(BLENDS) * len(TFS) # 48 +N_DECLARED_C = len(WF_WIN_D) * len(BLENDS) * len(TFS) # 12 +N_DECLARED_CAL = len(CAL_PEN) + len(CAL_LAM) # 13 (taratura) +N_DECLARED = N_DECLARED_A + N_DECLARED_B + N_DECLARED_C + N_DECLARED_CAL # 169 +N_PRIOR_FAMILY = 112 # trial dichiarati da r0822_adaptive_horizon (stessa domanda) +N_SCREEN = 168 # pool di screen direzionale BTC/ETH dell'ondata (filone 12) + + +# ======================================================================================= +# RILEVATORI VERI +# ======================================================================================= +def _logret(c: np.ndarray) -> np.ndarray: + r = np.zeros(len(c)) + r[1:] = np.log(c[1:] / c[:-1]) + return r + + +def _std_returns(c: np.ndarray, bpd: int) -> np.ndarray: + """Rendimenti log standardizzati da vol rolling CAUSALE (min_periods -> warmup a 0). + Il bersaglio del rilevatore e' il cambio di DRIFT: senza standardizzare, un cambio di + regime di VOLATILITA' dominerebbe la verosimiglianza e il rilevatore misurerebbe la vol.""" + lr = _logret(c) + w = max(5, STD_WIN_D * bpd) + sig = pd.Series(lr).rolling(w, min_periods=max(5, w // 4)).std().values + y = np.where((sig > 0) & np.isfinite(sig), lr / np.where(sig > 0, sig, 1.0), 0.0) + return np.nan_to_num(np.clip(y, -8.0, 8.0)) + + +def _student_logpdf(x: float, mu: np.ndarray, kap: np.ndarray, + al: np.ndarray, be: np.ndarray) -> np.ndarray: + """Predittiva Student-t della Normale-InverseGamma coniugata (Murphy 2007, eq. 99-100): + x | mu,kappa,alpha,beta ~ t_{2 alpha}(mu, beta(kappa+1)/(alpha kappa)).""" + nu = 2.0 * al + s2 = be * (kap + 1.0) / (al * kap) + z = (x - mu) ** 2 / (nu * s2) + return (gammaln(0.5 * (nu + 1.0)) - gammaln(0.5 * nu) + - 0.5 * np.log(nu * np.pi * s2) - 0.5 * (nu + 1.0) * np.log1p(z)) + + +def _bocpd(y: np.ndarray, lam_bars: float, rmax: int) -> tuple[np.ndarray, np.ndarray]: + """BOCPD (Adams & MacKay 2007) con hazard COSTANTE H = 1/lam_bars e verosimiglianza NIG. + Ritorna (E[r_t], argmax_r P(r_t)) in BARRE. Filtro in avanti: ogni valore usa solo il + passato -> causale per costruzione, e stabile su prefisso (nessuna statistica full-sample). + Il posterior e' troncato a rmax barre e rinormalizzato (il troncamento e' un limite di + memoria, NON un clip sul valore: se la massa si accumula a rmax lo si vede in diagnostica).""" + n = len(y) + haz = 1.0 / max(2.0, float(lam_bars)) + R = np.array([1.0]) + mu = np.array([NIG_MU0]); kap = np.array([NIG_KAP0]) + al = np.array([NIG_AL0]); be = np.array([NIG_BE0]) + rl = np.arange(1) + mean_rl = np.zeros(n); map_rl = np.zeros(n) + for t in range(n): + x = float(y[t]) + lp = _student_logpdf(x, mu, kap, al, be) + lp -= lp.max() + pred = np.exp(lp) + gr = R * pred * (1.0 - haz) + cp = float(np.sum(R * pred * haz)) + m_new = min(len(R) + 1, rmax + 1) + newR = np.empty(m_new) + newR[0] = cp + newR[1:] = gr[:m_new - 1] + s = newR.sum() + if not np.isfinite(s) or s <= 0: + newR = np.zeros(m_new); newR[0] = 1.0; s = 1.0 + newR /= s + # aggiornamento delle statistiche sufficienti (run r -> run r+1), prior in testa + mu_f = np.concatenate(([NIG_MU0], (kap * mu + x) / (kap + 1.0)))[:m_new] + kap_f = np.concatenate(([NIG_KAP0], kap + 1.0))[:m_new] + al_f = np.concatenate(([NIG_AL0], al + 0.5))[:m_new] + be_f = np.concatenate(([NIG_BE0], be + kap * (x - mu) ** 2 / (2.0 * (kap + 1.0))))[:m_new] + R, mu, kap, al, be = newR, mu_f, kap_f, al_f, be_f + if len(rl) != m_new: + rl = np.arange(m_new, dtype=float) + mean_rl[t] = float(np.dot(R, rl)) + map_rl[t] = float(rl[int(np.argmax(R))]) + return mean_rl, map_rl + + +def _op_causal(y: np.ndarray, pen_k: float, smax: int) -> tuple[np.ndarray, np.ndarray]: + """OPTIMAL PARTITIONING **online** (l'obiettivo esatto che PELT accelera con il pruning): + F(t) = min_{s in [t-smax, t-1]} [ F(s) + C(y[s:t]) + beta(t) ] + con C = costo gaussiano di mean-shift su dati standardizzati e beta(t) = pen_k*log(t). + ⚠️ Il termine sum(y^2) del costo gaussiano e' INVARIANTE alla partizione (ogni punto sta in + esattamente un segmento) -> si omette dall'argmin. C(s:t) = -(S[t]-S[s])^2/(t-s). + Ritorna (eta' dall'ultimo change-point, mediana delle lunghezze dei segmenti CONCLUSI), + entrambe in barre. Per ogni t si usa SOLO y[0:t]: e' la differenza fra questo e un + `ruptures.Pelt().fit_predict(y)` retrospettivo, che guarda tutto il campione.""" + n = len(y) + S = np.concatenate(([0.0], np.cumsum(y))) + F = np.full(n + 1, np.inf) + F[0] = 0.0 + prev = np.zeros(n + 1, dtype=int) + age = np.zeros(n); segmed = np.zeros(n) + for t in range(1, n + 1): + a = max(0, t - smax) + s_idx = np.arange(a, t) + seg = (S[t] - S[a:t]) + ln = (t - s_idx).astype(float) + cand = F[a:t] - seg * seg / ln + j = int(np.argmin(cand)) + beta = pen_k * np.log(max(2.0, float(t))) + F[t] = cand[j] + beta + prev[t] = int(s_idx[j]) + # backtracking della partizione VALIDA A t (cap a 40 segmenti: ci serve la mediana) + last = prev[t] + age[t - 1] = t - last + lens = [] + u = last + while u > 0 and len(lens) < 40: + p = prev[u] + lens.append(u - p) + u = p + segmed[t - 1] = float(np.median(lens)) if lens else float(t) + return age, segmed + + +_STAT_CACHE: dict = {} + + +def _stat_key(df: pd.DataFrame, det: str, par) -> tuple: + """Chiave chiusa sul CONTENUTO del frame (lunghezza + primo/ultimo timestamp): un PREFISSO + non deve prendere la cache del campione pieno, altrimenti `causality_ok` verrebbe ingannata + dalla cache invece che dal codice.""" + ts = df["timestamp"].values + return (det, par, len(df), int(ts[0]), int(ts[-1])) + + +def detector_stats(df: pd.DataFrame, det: str, par) -> dict: + key = _stat_key(df, det, par) + if key in _STAT_CACHE: + return _STAT_CACHE[key] + bpd = A.bars_per_day(df) + c = df["close"].values.astype(float) + y = _std_returns(c, bpd) + rmax = max(4, LMAX_D * bpd) + if det == "bocpd": + m, mp = _bocpd(y, float(par) * bpd, rmax) + out = {"mean": m, "map": mp} + elif det == "op": + ag, sm = _op_causal(y, float(par), rmax) + out = {"age": ag, "segmed": sm} + else: + raise ValueError(det) + _STAT_CACHE[key] = out + return out + + +def lookback_bars(df: pd.DataFrame, det: str, par, stat: str, mapping: str) -> np.ndarray: + """L_t in BARRE dentro [Lmin, Lmax] giorni. `direct` = la lunghezza di run E' il lookback + (dopo un change-point si puo' guardare indietro solo fin la'); `inverse` e' lo specchio, + dichiarato come asse perche' il verso non e' ovvio a priori.""" + bpd = A.bars_per_day(df) + lo, hi = max(2, LMIN_D * bpd), max(3, LMAX_D * bpd) + v = detector_stats(df, det, par)[stat] + base = np.clip(np.nan_to_num(v, nan=float(lo)), lo, hi) + L = base if mapping == "direct" else (lo + hi - base) + return np.clip(np.round(L), 2, hi).astype(int) + + +# ======================================================================================= +# DIREZIONE TSMOM (orizzonte costante o variabile) + vol-target — IDENTICA al primo tentativo +# ======================================================================================= +def _sign_at(c: np.ndarray, h: np.ndarray) -> tuple[np.ndarray, np.ndarray]: + n = len(c) + i = np.arange(n) + j = i - h + ok = j >= 0 + s = np.zeros(n) + jj = np.where(ok, j, 0) + s[ok] = np.sign(c[i[ok]] / c[jj[ok]] - 1.0) + return s, ok + + +def _dir_from_L(c: np.ndarray, L: np.ndarray, blend: str) -> np.ndarray: + hs = [L] if blend == "single" else [ + np.maximum(2, np.round(L / 3.0).astype(int)), L, np.round(2.0 * L).astype(int)] + acc = np.zeros(len(c)); cnt = np.zeros(len(c)) + for h in hs: + s, ok = _sign_at(c, np.asarray(h, int)) + acc[ok] += s[ok]; cnt[ok] += 1 + out = np.zeros(len(c)); nz = cnt > 0 + out[nz] = acc[nz] / cnt[nz] + return out + + +def fixed_target(df: pd.DataFrame) -> np.ndarray: + """BASELINE: TSMOM fisso 30/90/180, long-flat, vol-target 20%, leva 2x = TP01 canonico.""" + c = df["close"].values.astype(float) + bpd = A.bars_per_day(df) + acc = np.zeros(len(c)); cnt = np.zeros(len(c)) + for hd in FIXED_H: + s, ok = _sign_at(c, np.full(len(c), hd * bpd, int)) + acc[ok] += s[ok]; cnt[ok] += 1 + d = np.zeros(len(c)); nz = cnt > 0 + d[nz] = acc[nz] / cnt[nz] + return A.vol_target(np.clip(d, 0, None), df, TGT_VOL, VOLWIN, LEV) + + +def const_factory(tf: str, L: int, blend: str): + def fn(df: pd.DataFrame) -> np.ndarray: + c = df["close"].values.astype(float) + bpd = A.bars_per_day(df) + Lb = np.full(len(c), max(2, int(round(L * bpd))), int) + return A.vol_target(np.clip(_dir_from_L(c, Lb, blend), 0, None), df, TGT_VOL, VOLWIN, LEV) + fn.__name__ = f"const_L{L}_{blend}" + return fn + + +def factory(tf: str, det: str, par, stat: str, mapping: str, blend: str): + def fn(df: pd.DataFrame) -> np.ndarray: + c = df["close"].values.astype(float) + L = lookback_bars(df, det, par, stat, mapping) + return A.vol_target(np.clip(_dir_from_L(c, L, blend), 0, None), df, TGT_VOL, VOLWIN, LEV) + fn.__name__ = f"adapt_{det}{par}_{stat}_{mapping}_{blend}" + return fn + + +_WF_CACHE: dict = {} + + +def wf_picks(df: pd.DataFrame, win_d: int, blend: str): + """(C) L scelto per WALK-FORWARD sul PnL NETTO realizzato: a ogni barra si prende l'L che, + fra i candidati COSTANTI, ha lo Sharpe netto migliore nella finestra all'indietro `win_d`. + Nessun modello di regime: e' il "cosa avrebbe funzionato finora". + Causale: il PnL della barra i si conosce solo a fine barra i, quindi per decidere a i si usa + la finestra che finisce a i-1; `eval_weights` shifta poi la posizione di un'altra barra.""" + cands = [L for L in CONST_L if LMIN_D <= L <= LMAX_D] + key = (win_d, blend) + _stat_key(df, "wf", 0)[2:] + if key in _WF_CACHE: + return _WF_CACHE[key] + c = df["close"].values.astype(float) + bpd = A.bars_per_day(df) + n = len(c) + r = A.simple_returns(c) + w = max(20, win_d * bpd) + tgts, sh = [], [] + for L in cands: + Lb = np.full(n, max(2, int(round(L * bpd))), int) + t_ = A.vol_target(np.clip(_dir_from_L(c, Lb, blend), 0, None), df, TGT_VOL, VOLWIN, LEV) + pos = np.zeros(n); pos[1:] = t_[:-1] + net = pos * r - A.FEE_SIDE * np.abs(np.diff(pos, prepend=0.0)) + ss = pd.Series(net) + mu = ss.rolling(w, min_periods=w // 2).mean() + sd = ss.rolling(w, min_periods=w // 2).std() + with np.errstate(invalid="ignore", divide="ignore"): + sr = (mu / sd).values + sr = np.concatenate(([np.nan], sr[:-1])) # PnL noto solo a fine barra + tgts.append(t_); sh.append(np.where(np.isfinite(sr), sr, -np.inf)) + SH = np.vstack(sh); TG = np.vstack(tgts) + idx = np.argmax(SH, axis=0) + idx[~np.isfinite(SH).any(axis=0)] = cands.index(90) # warmup -> il canonico, non il migliore + out = (np.asarray([cands[i] for i in idx]), TG[idx, np.arange(n)], cands) + _WF_CACHE[key] = out + return out + + +def wf_factory(tf: str, win_d: int, blend: str): + def fn(df: pd.DataFrame) -> np.ndarray: + return wf_picks(df, win_d, blend)[1] + fn.__name__ = f"wfpnl_{win_d}_{blend}" + return fn + + +GRID_A = [dict(det=d, par=p, stat=s, mapping=m, blend=b) + for d in DETECTORS for p in PARAMS[d] for s in STATS[d] + for m in MAPPINGS for b in BLENDS] +GRID_B = [dict(L=L, blend=b) for L in CONST_L for b in BLENDS] +GRID_C = [dict(win_d=w, blend=b) for w in WF_WIN_D for b in BLENDS] + + +# ======================================================================================= +# UTILITA' +# ======================================================================================= +def _dd(s: pd.Series) -> float: + eq = np.cumprod(1.0 + np.asarray(s.dropna().values, float)) + pk = np.maximum.accumulate(eq) + return float(np.max((pk - eq) / pk)) if len(eq) else 0.0 + + +def _cagr(s: pd.Series) -> float: + s = s.dropna() + if len(s) < 5: + return float("nan") + eq = float(np.prod(1.0 + s.values)) + yrs = (s.index[-1] - s.index[0]).total_seconds() / (86400 * 365.25) + return eq ** (1 / max(yrs, 1e-6)) - 1.0 if eq > 0 else -1.0 + + +def resample_offset(df_1h: pd.DataFrame, hours: int, off: int) -> pd.DataFrame: + """Stessa convenzione di trend_portfolio.resample_tf, ancora spostata di `off` ore. + NB `offset=`, non `origin=` (pandas 2.x ignora origin in silenzio — lezione 02/07).""" + g = df_1h.copy() + idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True) + idx.name = "dt" + g.index = idx + out = g.resample(f"{hours}h", offset=pd.Timedelta(hours=off), label="left", closed="left").agg( + {"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}) + out = out.dropna(subset=["open"]) + out["datetime"] = out.index + epoch = pd.Timestamp("1970-01-01", tz="UTC") + out["timestamp"] = ((out.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64") + return out.reset_index(drop=True)[ + ["timestamp", "open", "high", "low", "close", "volume", "datetime"]] + + +def paired_by_year(cand: pd.Series, base: pd.Series) -> list[dict]: + J = pd.concat({"C": cand, "B": base}, axis=1, join="inner").dropna() + rows = [] + for y, g in J.groupby(J.index.year): + if len(g) < 40: + continue + rows.append(dict(year=int(y), n=len(g), + sh_cand=round(A._sh(g["C"]), 2), sh_base=round(A._sh(g["B"]), 2), + d_sh=round(A._sh(g["C"]) - A._sh(g["B"]), 2))) + return rows + + +def delever_null(cand: pd.Series, base: pd.Series) -> dict: + """Esiste k<1 che da' al BASELINE lo stesso maxDD del candidato con Sharpe >= ? + Se si', il candidato non ha comprato protezione: ha solo tolto leva.""" + J = pd.concat({"C": cand, "B": base}, axis=1, join="inner").dropna() + dd_c, dd_b = _dd(J["C"]), _dd(J["B"]) + if dd_c >= dd_b or dd_b <= 0: + return dict(applicabile=False, dd_cand=round(dd_c, 4), dd_base=round(dd_b, 4), + nota="il candidato NON riduce il maxDD: il null non si applica") + lo, hi = 0.0, 1.0 + for _ in range(60): + k = 0.5 * (lo + hi) + if _dd(k * J["B"]) < dd_c: + lo = k + else: + hi = k + k = 0.5 * (lo + hi) + return dict(applicabile=True, k=round(k, 4), dd_cand=round(dd_c, 4), dd_base=round(dd_b, 4), + sharpe_cand=round(A._sh(J["C"]), 3), + sharpe_base_delev=round(A._sh(k * J["B"]), 3), + superato=bool(A._sh(J["C"]) > A._sh(k * J["B"]))) + + +_ANCHOR_CACHE: dict = {} + + +def series_at_anchor(fn, name: str, hours: int, off: int) -> pd.Series: + key = (name, hours, off) + if key not in _ANCHOR_CACHE: + ser = {} + for a in ASSETS: + dfo = resample_offset(A.get(a, "1h"), hours, off) + ev = A.eval_weights(dfo, A._call_target(fn, dfo, a)) + ser[a] = pd.Series(ev["net"], index=ev["idx"]) + J = pd.concat(ser, axis=1, join="inner").fillna(0.0) + _ANCHOR_CACHE[key] = A._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]]) + return _ANCHOR_CACHE[key] + + +def adaptivity(tf: str, det: str, par, stat: str, mapping: str) -> dict: + """LA DIAGNOSTICA CHE HA SMASCHERATO IL PRIMO TENTATIVO — quanto si MUOVE davvero L_t. + Criterio (identico a quello del 22/08, per confrontabilita'): sta a un bordo <50% del tempo + E l'IQR e' >= 10 giorni. In piu': |dL|/giorno e numero di RESET (cadute di L > 5 giorni), + che sono la firma di un vero rilevatore di change-point.""" + fl, fh, iq, md, dl, nrs, sat = [], [], [], [], [], [], [] + for a in ASSETS: + df = A.get(a, tf) + bpd = A.bars_per_day(df) + L = lookback_bars(df, det, par, stat, mapping) / bpd + raw = np.nan_to_num(detector_stats(df, det, par)[stat], nan=0.0) / bpd + fl.append(float(np.mean(L <= LMIN_D + 1))) + fh.append(float(np.mean(L >= LMAX_D - 1))) + iq.append(float(np.percentile(L, 75) - np.percentile(L, 25))) + md.append(float(np.median(L))) + dl.append(float(np.median(np.abs(np.diff(L))))) + d = np.diff(L) + nrs.append(float(np.sum(d < -5.0) / (len(L) / (365.25)))) # reset all'anno + sat.append(float(np.mean(raw >= LMAX_D - 1))) # saturazione del GREZZO + return dict(frac_lo=float(np.mean(fl)), frac_hi=float(np.mean(fh)), + iqr_days=float(np.mean(iq)), med_days=float(np.mean(md)), + dL_med=float(np.mean(dl)), reset_yr=float(np.mean(nrs)), + sat_raw=float(np.mean(sat)), + adattiva=bool(np.mean(fl) < 0.5 and np.mean(fh) < 0.5 and np.mean(iq) >= 10.0)) + + +def hr(t: str = "") -> None: + print("\n" + "=" * 86) + if t: + print(t) + print("=" * 86) + + +def dsr_given_sr0(daily: pd.Series, sr_ann: float, sr0_ann: float, + dpy: float = 365.25) -> float: + """Deflated-Sharpe con il massimo-atteso-dal-rumore IMPOSTO dall'esterno. + Serve perche' `A.deflated_sharpe` stima sr0 dalla VARIANZA degli Sharpe della griglia: su + una famiglia OMOGENEA (qui sono tutte varianti di TSMOM, quindi molto correlate) quella + varianza e' piccola, sr0 esce basso e il DSR esce alto **per costruzione**. Il conteggio che + decide, sulla ricerca direzionale BTC/ETH, e' quello di SCREEN: il filone ORTHO ha misurato + che su 168 trial il massimo atteso dal puro rumore e' Sharpe 1.572.""" + r = np.asarray(pd.Series(daily).dropna().values, float) + T = len(r) + sr, sr0 = sr_ann / np.sqrt(dpy), sr0_ann / np.sqrt(dpy) + sk = float(pd.Series(r).skew()) + ku = float(pd.Series(r).kurt()) + 3.0 + den = np.sqrt(max(1e-9, 1 - sk * sr + (ku - 1) / 4.0 * sr ** 2)) + return float(norm.cdf((sr - sr0) * np.sqrt(T - 1) / den)) + + +def dsr_line(sr: float, allsr: list, daily: pd.Series, label: str, n_decl: int) -> str: + pad = list(allsr) + [float(np.mean(allsr))] * max(0, n_decl - len(allsr)) + d, s0 = A.deflated_sharpe(sr, pad, daily) + return (f" N={n_decl:>4} ({label:<24}) DSR={d:.3f} {'PASS' if d >= 0.95 else 'FAIL'}" + f" max atteso dal rumore {s0:+.3f}") + + +# ======================================================================================= +def main() -> None: + t0 = time.time() + print(__doc__.split("Runtime atteso")[0]) + + # -- 0. SANITY ----------------------------------------------------------------------- + hr("0. SANITY — il baseline 'fisso' e' TP01 canonico (se non lo e', ogni delta e' inventato)") + tp = TrendPortfolio(**CANONICAL) + worst = 0.0 + for a in ASSETS: + df = A.get(a, "1d") + worst = max(worst, float(np.max(np.abs(fixed_target(df) - tp.target_series(df))))) + base_1d = A.candidate_daily(fixed_target, tf="1d") + tp01 = A.tp01_baseline_daily() + JJ = pd.concat({"a": base_1d, "b": tp01}, axis=1, join="inner").dropna() + print(f" max|target_mio - TrendPortfolio.target_series| (1d, BTC+ETH) = {worst:.3e}") + print(f" max|serie_giornaliera_mia - A.tp01_baseline_daily()| = " + f"{float(np.max(np.abs(JJ['a'] - JJ['b']))):.3e} (n={len(JJ)})") + assert worst < 1e-12, "il baseline NON riproduce TP01: fermarsi qui" + + # -- 0-bis. IL RILEVATORE E' CAUSALE? (troncamento MANUALE, prima dei gate) ----------- + hr("0-bis. TRONCAMENTO MANUALE — la trappola del change-point RETROSPETTIVO") + print(" Un `Pelt().fit_predict(serie_intera)` restituisce la segmentazione dell'INTERO") + print(" campione: al tempo t 'saprebbe' dei cambi futuri. Qui BOCPD e OP sono ricorsioni in") + print(" avanti. Prova diretta: ricalcolo su un PREFISSO e confronto la coda con il pieno.") + for det, par in (("bocpd", 100), ("op", 5.0)): + for stat in STATS[det]: + wmax = 0.0 + for a in ASSETS: + df = A.get(a, "1d") + full = lookback_bars(df, det, par, stat, "direct") + for frac in (0.70, 0.85): + cut = int(len(df) * frac) + sub = df.iloc[:cut].reset_index(drop=True) + pre = lookback_bars(sub, det, par, stat, "direct") + wmax = max(wmax, float(np.max(np.abs(pre[-100:] - full[cut - 100:cut])))) + print(f" {det:<6} {str(par):>5} {stat:<7}: max|L(prefisso)-L(pieno)| sulla coda " + f"= {wmax:.3e} barre -> {'CAUSALE' if wmax < 1e-9 else 'SOSPETTO'}") + + # -- 1-pre. TARATURA DELLA SCALA — guardando il RILEVATORE, mai lo Sharpe ----------- + hr("1-pre. TARATURA DELLA SCALA DEI PARAMETRI (guardando SOLO quanti change-point rileva)") + print(" Un rilevatore che non rileva nulla non e' un rilevatore. Questa scansione guarda il") + print(" NUMERO di change-point e la distribuzione di L_t — mai un rendimento. Dichiarata") + print(" come 13 trial in piu' (al rialzo), benche' nessuno produca uno Sharpe.") + dfc = A.get("BTC", "1d") + yc = _std_returns(dfc["close"].values.astype(float), 1) + print(f"\n OP (BTC 1d) {'pen_k':>6} {'cp/anno':>8} {'age med':>8} {'IQR':>7} {'@tetto':>8}") + for k in CAL_PEN: + ag, _ = _op_causal(yc, k, LMAX_D) + ncp = int(np.sum(np.diff(ag) < 0)) + flag = " <= USATO" if k in PARAMS["op"] else (" <= degenere (artefatto 22/08)" + if np.mean(ag >= LMAX_D - 1) > 0.30 else "") + print(f" {k:>6} {ncp/(len(yc)/365.25):>8.1f} {np.median(ag):>8.1f} " + f"{np.percentile(ag,75)-np.percentile(ag,25):>7.1f} " + f"{np.mean(ag>=LMAX_D-1)*100:>7.1f}%{flag}") + print(f"\n BOCPD (BTC 1d) {'lambda':>6} {'E[r] med':>9} {'IQR':>7} {'@tetto':>8} " + f"{'MAP med':>8} {'@tetto':>8}") + for lam in CAL_LAM: + mn, mp = _bocpd(yc, float(lam), LMAX_D) + flag = " <= USATO" if lam in PARAMS["bocpd"] else "" + print(f" {lam:>6} {np.median(mn):>9.1f} " + f"{np.percentile(mn,75)-np.percentile(mn,25):>7.1f} " + f"{np.mean(mn>=LMAX_D-1)*100:>7.1f}% {np.median(mp):>8.1f} " + f"{np.mean(mp>=LMAX_D-1)*100:>7.1f}%{flag}") + + # -- 1. LA DIAGNOSTICA DI MECCANISMO, PER PRIMA -------------------------------------- + hr("1. L_t SI MUOVE DAVVERO? (se sta al bordo l'artefatto e' riprodotto: si chiude e basta)") + print(" criterio (identico al 22/08, per confrontabilita'): a un bordo <50% del tempo E") + print(" IQR >= 10 giorni. `reset/anno` = cadute di L > 5 giorni = firma di un change-point.") + print(f"\n {'TF':>4} {'det':>6} {'par':>5} {'stat':>7} {'map':<7} {'bordo-lo':>9} " + f"{'bordo-hi':>9} {'IQR(g)':>7} {'med(g)':>7} {'|dL|(g)':>8} {'reset/a':>8} " + f"{'sat.grezzo':>10} esito") + diag: dict = {} + for tf in TFS: + for d in DETECTORS: + for p in PARAMS[d]: + for st in STATS[d]: + for mp in MAPPINGS: + v = adaptivity(tf, d, p, st, mp) + diag[(tf, d, p, st, mp)] = v + print(f" {tf:>4} {d:>6} {str(p):>5} {st:>7} {mp:<7} " + f"{v['frac_lo']*100:8.1f}% {v['frac_hi']*100:8.1f}% " + f"{v['iqr_days']:7.1f} {v['med_days']:7.1f} {v['dL_med']:8.2f} " + f"{v['reset_yr']:8.1f} {v['sat_raw']*100:9.1f}% " + f"{'ADATTIVA' if v['adattiva'] else 'costante-travestito'}") + n_ad = sum(1 for v in diag.values() if v["adattiva"]) + print(f"\n configurazioni di rilevatore DAVVERO adattive: {n_ad}/{len(diag)}") + if n_ad == 0: + hr("STOP — ARTEFATTO RIPRODOTTO") + print(" Nessun rilevatore muove L_t: ogni 'vincitore' sarebbe un orizzonte COSTANTE") + print(" travestito, esattamente come il 22/08. Il filone si chiude per la seconda volta") + print(" senza bisogno di guardare i rendimenti. FINE.") + return + + # -- 2. GRIGLIA (A): cella scelta IN-SAMPLE + deflated-Sharpe ------------------------- + hr(f"2. study_family_honest (A) — {len(GRID_A)} celle x {len(TFS)} TF = " + f"{len(GRID_A)*len(TFS)} trial") + rep = A.study_family_honest("BOCPD-H", factory, GRID_A, TFS) + ch = rep["chosen"] + print(f" celle valutate : {rep['n_cells']}") + print(f" cella scelta IN-SAMPLE : tf={ch['tf']} {ch['params']}") + print(f" Sharpe IS {ch['insample_sharpe']} FULL {ch['full_sharpe']}") + print(f" deflated-Sharpe (N={rep['n_cells']}): {rep['deflated_sharpe']} " + f"(max atteso sotto il null {rep['expected_null_max']}) PASS={rep['dsr_pass']}") + print(f" earns_slot_marginal : {rep['earns_slot_marginal']}") + print(f" EARNS_SLOT_HONEST : {rep['earns_slot_honest']}") + print("\n top-10 IN-SAMPLE (come si sceglie onestamente):") + for r in rep["rows"][:10]: + ad = diag[(r["tf"], r["params"]["det"], r["params"]["par"], + r["params"]["stat"], r["params"]["mapping"])]["adattiva"] + print(f" IS {r['insample_sharpe']:+.3f} FULL {r['full_sharpe']:+.3f} {r['tf']:>3} " + f"{r['params']} {'[adattiva]' if ad else '[costante-travestito]'}") + + fn_ch = factory(tf=ch["tf"], **ch["params"]) + d_ch = A.candidate_daily(fn_ch, tf=ch["tf"]) + + # -- 3. CONTROLLO (B): il MIGLIOR ORIZZONTE COSTANTE --------------------------------- + hr(f"3. CONTROLLO L COSTANTE (B) — {len(GRID_B)} celle x {len(TFS)} TF = " + f"{len(GRID_B)*len(TFS)} trial [IL NULL GIUSTO, non TP01]") + csel = A.select_cell_insample(const_factory, GRID_B, TFS) + cch = csel["chosen"] + print(f" miglior COSTANTE scelto in-sample: tf={cch['tf']} {cch['params']} " + f"IS {cch['insample_sharpe']} FULL {cch['full_sharpe']}") + print(" top-8 costanti in-sample (L=90 triple 1d == TP01 canonico):") + for r in csel["rows"][:8]: + tag = " <== TP01" if (r["params"]["L"] == 90 and r["params"]["blend"] == "triple" + and r["tf"] == "1d") else "" + print(f" IS {r['insample_sharpe']:+.3f} FULL {r['full_sharpe']:+.3f} {r['tf']:>3} " + f"L={r['params']['L']:>3}g {r['params']['blend']}{tag}") + fn_const = const_factory(tf=cch["tf"], **cch["params"]) + d_const = A.candidate_daily(fn_const, tf=cch["tf"]) + + # -- 4. WF-PnL (C) -------------------------------------------------------------------- + hr(f"4. WALK-FORWARD SUL PnL REALIZZATO (C) — {len(GRID_C)} celle x {len(TFS)} TF = " + f"{len(GRID_C)*len(TFS)} trial") + wsel = A.select_cell_insample(wf_factory, GRID_C, TFS) + wch = wsel["chosen"] + print(f" miglior WF scelto in-sample: tf={wch['tf']} {wch['params']} " + f"IS {wch['insample_sharpe']} FULL {wch['full_sharpe']}") + for r in wsel["rows"]: + print(f" IS {r['insample_sharpe']:+.3f} FULL {r['full_sharpe']:+.3f} {r['tf']:>3} " + f"{r['params']}") + fn_wf = wf_factory(tf=wch["tf"], **wch["params"]) + d_wf = A.candidate_daily(fn_wf, tf=wch["tf"]) + # quanto si muove L nella variante WF? (stesso metro della sezione 1) + for a in ASSETS: + df = A.get(a, wch["tf"]); bpd = A.bars_per_day(df) + pick, _, cands = wf_picks(df, **wch["params"]) + print(f" {a}: L_WF giorni mediana {np.median(pick):.0f} IQR " + f"{np.percentile(pick,75)-np.percentile(pick,25):.0f} " + f"bordo basso {np.mean(pick<=min(cands))*100:.1f}% " + f"bordo alto {np.mean(pick>=max(cands))*100:.1f}% " + f"cambi/anno {np.sum(np.diff(pick)!=0)/(len(pick)/(bpd*365.25)):.0f}") + + # -- 5. IL CONFRONTO CHE DECIDE ------------------------------------------------------ + hr("5. ADATTIVO vs MIGLIOR COSTANTE (entrambi scelti IN-SAMPLE) — e vs TP01") + base_ch = A.candidate_daily(fixed_target, tf=ch["tf"]) + for nome, ser in (("BOCPD/OP (A)", d_ch), ("WF-PnL (C)", d_wf), + ("miglior COSTANTE (B)", d_const), ("TP01 30/90/180", base_1d)): + h = ser[ser.index >= HOLDOUT] + print(f" {nome:<22} FULL Sh {A._sh(ser):+.3f} HOLD Sh {A._sh(h):+.3f} " + f"maxDD {_dd(ser)*100:5.2f}% CAGR {_cagr(ser)*100:6.2f}% " + f"corr->TP01 {pd.concat({'a':ser,'b':tp01},axis=1,join='inner').dropna().corr().iloc[0,1]:+.3f}") + + for lab, cand, ref, refname in (("A vs B", d_ch, d_const, "miglior costante"), + ("C vs B", d_wf, d_const, "miglior costante"), + ("A vs TP01", d_ch, base_1d, "TP01")): + J = pd.concat({"C": cand, "B": ref}, axis=1, join="inner").dropna() + JH = J[J.index >= HOLDOUT] + rows = paired_by_year(cand, ref) + ds = [r["d_sh"] for r in rows] + print(f"\n --- {lab} ({refname}) --- corr {J['C'].corr(J['B']):+.3f}") + print(f" dSh FULL {A._sh(J['C'])-A._sh(J['B']):+.3f} " + f"dSh HOLD {A._sh(JH['C'])-A._sh(JH['B']):+.3f}") + print(" differenza APPAIATA per anno:") + print(" " + " ".join(f"{r['year']}:{r['d_sh']:+.2f}" for r in rows)) + print(f" -> anni con dSh>0: {sum(1 for x in ds if x > 0)}/{len(ds)} " + f"mediana {np.median(ds):+.2f} media {np.mean(ds):+.2f} " + f"[criterio del falsificatore: >=6/8 anni]") + + # decomposizione: quanto del vantaggio su TP01 e' ADATTIVITA' e quanto e' "L piu' corto"? + JT = pd.concat({"C": d_ch, "K": d_const, "B": base_1d}, axis=1, join="inner").dropna() + v_tp = A._sh(JT["C"]) - A._sh(JT["B"]) + v_ct = A._sh(JT["K"]) - A._sh(JT["B"]) + print(f"\n DECOMPOSIZIONE del vantaggio dell'adattivo su TP01 (FULL, finestra comune):") + print(f" adattivo - TP01 = {v_tp:+.3f}") + print(f" miglior COSTANTE - TP01 = {v_ct:+.3f} <- quota spiegata da 'un L piu' corto'") + print(f" adattivo - miglior COSTANTE= {v_tp - v_ct:+.3f} <- quota spiegata dall'ADATTARE") + print(f" => l'adattivita' spiega il {100*(v_tp-v_ct)/v_tp if abs(v_tp)>1e-9 else float('nan'):+.0f}% " + f"del vantaggio su TP01") + + # la superficie di L costante e' una CURVA o un CRINALE DI RUMORE? (replica del 22/08) + rows12 = {r["params"]["L"]: r["insample_sharpe"] for r in csel["rows"] + if r["tf"] == cch["tf"] and r["params"]["blend"] == cch["params"]["blend"]} + Ls = sorted(rows12) + adj = float(np.median(np.abs(np.diff([rows12[L] for L in Ls])))) + print(f"\n RISOLUZIONE della griglia costante ({cch['tf']}, {cch['params']['blend']}): salto IS " + f"mediano fra L ADIACENTI {adj:+.3f}") + print(f" contro il vantaggio del miglior costante su TP01 ({v_ct:+.3f}) -> rapporto " + f"{adj/max(1e-9, abs(v_ct)):.2f} [replica del 'crinale di rumore' del 22/08]") + + # -- 6. GATE -------------------------------------------------------------------------- + hr("6. GATE sul candidato (A) scelto in-sample") + print(A.fmt_marginal(rep["marginal"])) + cz = A.causality_ok(fn_ch, tf=ch["tf"]) + print(f"\n causality_ok : ok={cz['ok']} max_tail_diff={cz['max_tail_diff']} " + f"(prefissi controllati {cz['checked']})") + imp = A.implausible_sharpe(d_ch) + print(f" implausible_sharpe : implausible={imp['implausible']} Sh {imp.get('sharpe')} " + f"maxDD {imp.get('maxdd')} perdite/attive {imp.get('loss_frac')} " + f"attive {imp.get('active_frac')}") + for rr in imp["reasons"]: + print(f" - {rr}") + print(f" null de-levering vs COSTANTE : {delever_null(d_ch, d_const)}") + print(f" null de-levering vs TP01 : {delever_null(d_ch, base_1d)}") + print(" haircut a $600 (min-order $5):") + for a in ASSETS: + df = A.get(a, ch["tf"]) + sc = A.eval_weights_smallcap(df, A._call_target(fn_ch, df, a), capital=600.0) + print(f" {a}: modellato {sc['modeled']['sharpe']:+.3f} -> reale " + f"{sc['realistic']['sharpe']:+.3f} (haircut {sc['sharpe_haircut']:+.3f}, " + f"{sc['n_executed_trades']} trade)") + + # DSR a piu' conteggi: famiglia, famiglia+112 del primo tentativo, di SCREEN + all_sr = ([r["full_sharpe"] for r in rep["rows"]] + + [r["full_sharpe"] for r in csel["rows"]] + + [r["full_sharpe"] for r in wsel["rows"]]) + hr("6-bis. DEFLATED-SHARPE — sensibilita' del verdetto al CONTEGGIO (regola 30/07 + 22/08)") + print(f" Sharpe FULL del candidato (A) = {A._sh(d_ch):+.3f}") + print(dsr_line(A._sh(d_ch), all_sr, d_ch, "mia famiglia A+B+C", N_DECLARED)) + print(dsr_line(A._sh(d_ch), all_sr, d_ch, "+ le 112 del 22/08", N_DECLARED + N_PRIOR_FAMILY)) + print(dsr_line(A._sh(d_ch), all_sr, d_ch, "di SCREEN (ondata)", + N_DECLARED + N_PRIOR_FAMILY + N_SCREEN)) + print("\n ⚠️ QUEL 'PASS' NON CERTIFICA NIENTE, e va detto: `deflated_sharpe` stima il massimo") + print(" atteso dal rumore dalla VARIANZA degli Sharpe della griglia. Qui la griglia e'") + print(" OMOGENEA (tutte varianti di TSMOM, molto correlate fra loro) -> varianza piccola") + print(" -> sr0 basso -> DSR alto per COSTRUZIONE. Aumentare N padding con la media alza N") + print(" ma non la varianza. Il conteggio che decide e' quello di SCREEN, e il filone ORTHO") + print(" lo ha gia' misurato: su 168 trial di ricerca direzionale BTC/ETH il massimo atteso") + print(" dal puro rumore e' Sharpe 1.572. Riscritto con QUEL sr0:") + for nome, ser in (("adattivo (A)", d_ch), ("miglior costante (B)", d_const), + ("TP01 30/90/180", base_1d)): + print(f" {nome:<22} Sharpe FULL {A._sh(ser):+.3f} vs sr0_screen 1.572 -> " + f"DSR {dsr_given_sr0(ser, A._sh(ser), 1.572):.3f} " + f"{'PASS' if dsr_given_sr0(ser, A._sh(ser), 1.572) >= 0.95 else 'FAIL'}") + + # PROVA DI IDENTITA': la cella "adattiva" al 100% sul bordo E' il costante, bit per bit + hr("6-ter. PROVA DI IDENTITA' — la cella al bordo NON somiglia a un costante: LO E'") + deg = [r for r in rep["rows"] + if diag[(r["tf"], r["params"]["det"], r["params"]["par"], r["params"]["stat"], + r["params"]["mapping"])]["frac_lo"] >= 0.999] + if deg: + dr = max(deg, key=lambda r: r["insample_sharpe"]) + s_deg = A.candidate_daily(factory(tf=dr["tf"], **dr["params"]), tf=dr["tf"]) + s_con = A.candidate_daily(const_factory(tf=dr["tf"], L=LMIN_D, + blend=dr["params"]["blend"]), tf=dr["tf"]) + Jd = pd.concat({"a": s_deg, "b": s_con}, axis=1, join="inner").dropna() + print(f" cella 100% al bordo basso: {dr['tf']} {dr['params']} IS {dr['insample_sharpe']}" + f" FULL {dr['full_sharpe']}") + print(f" costante L={LMIN_D}g {dr['params']['blend']} {dr['tf']}: " + f"FULL {A._sh(s_con):+.3f}") + print(f" max|serie_adattiva - serie_costante| = {float(np.max(np.abs(Jd['a']-Jd['b']))):.3e}" + f" (n={len(Jd)}) -> {'SONO LA STESSA STRATEGIA' if float(np.max(np.abs(Jd['a']-Jd['b']))) < 1e-12 else 'diverse'}") + else: + print(" nessuna cella e' al 100% sul bordo basso (la prova non si applica)") + + # -- 7. FAMIGLIA RISTRETTA ALLE SOLE CELLE DAVVERO ADATTIVE -------------------------- + hr("7. GATE SULLA SOLA FAMIGLIA DAVVERO ADATTIVA (la domanda della missione, isolata)") + ad_rows = [r for r in rep["rows"] + if diag[(r["tf"], r["params"]["det"], r["params"]["par"], + r["params"]["stat"], r["params"]["mapping"])]["adattiva"]] + d_ad_keep = fn_ad_keep = best_keep = None + if not ad_rows: + print(" NESSUNA cella della famiglia e' davvero adattiva -> vedi sezione 1.") + else: + best = max(ad_rows, key=lambda r: r["insample_sharpe"]) + print(f" celle adattive: {len(ad_rows)}/{len(rep['rows'])} miglior IN-SAMPLE: " + f"tf={best['tf']} {best['params']} IS {best['insample_sharpe']} " + f"FULL {best['full_sharpe']}") + fn_ad = factory(tf=best["tf"], **best["params"]) + d_ad = A.candidate_daily(fn_ad, tf=best["tf"]) + sm = A.study_marginal("BOCPD-H (sole adattive)", fn_ad, tf=best["tf"]) + dsr_ad, sr0_ad = A.deflated_sharpe(A._sh(d_ad), + [r["full_sharpe"] for r in ad_rows], d_ad) + print(f" marginal={sm['marginal_verdict']} earns_slot={sm['earns_slot']} " + f"corr->TP01 {sm['marginal'].get('corr_full')}") + print(f" deflated-Sharpe (N={len(ad_rows)}) = {dsr_ad:.3f} " + f"{'PASS' if dsr_ad >= 0.95 else 'FAIL'} (null max atteso {sr0_ad:+.3f})") + JA = pd.concat({"C": d_ad, "K": d_const, "B": base_1d}, axis=1, join="inner").dropna() + JH = JA[JA.index >= HOLDOUT] + print(f" FULL adattiva-vera {A._sh(JA['C']):+.3f} | costante {A._sh(JA['K']):+.3f} " + f"| TP01 {A._sh(JA['B']):+.3f}") + print(f" HOLD-OUT adattiva-vera {A._sh(JH['C']):+.3f} | costante {A._sh(JH['K']):+.3f} " + f"| TP01 {A._sh(JH['B']):+.3f}") + r3 = paired_by_year(d_ad, d_const) + d3 = [r["d_sh"] for r in r3] + print(" vs miglior costante, differenza APPAIATA per anno:") + print(" " + " ".join(f"{r['year']}:{r['d_sh']:+.2f}" for r in r3)) + print(f" -> dSh>0 in {sum(1 for x in d3 if x>0)}/{len(d3)} mediana {np.median(d3):+.2f}" + f" corr al costante {JA['C'].corr(JA['K']):+.3f}") + print(" ⚠️ l'hold-out sono gli ULTIMI DUE anni di questa stessa riga: se il vantaggio") + print(" hold-out sta li' e non nel resto, e' un evento, non un meccanismo (regola:") + print(" un vantaggio concentrato in un anno non e' un vantaggio).") + print(f" null de-levering vs costante: {delever_null(d_ad, d_const)}") + d_ad_keep, fn_ad_keep, best_keep = d_ad, fn_ad, best + # -- 8. BANDA D'ANCORA (delta APPAIATO adattivo - costante) -------------------------- + hours = 24 if ch["tf"] == "1d" else 12 + offs = list(range(hours)) + hr(f"8. BANDA D'ANCORA — {len(offs)} ancore. Stima onesta = MEDIANA; il confronto fra due " + f"varianti\n e' la MEDIANA DELLE DIFFERENZE APPAIATE (mai la differenza delle mediane).") + if cch["tf"] != ch["tf"]: + print(f" ⚠️ adattivo su {ch['tf']} e miglior costante su {cch['tf']}: per il delta " + f"appaiato uso il miglior costante RIVALUTATO su {ch['tf']} (stesse ancore = coppie).") + crow = max([r for r in csel["rows"] if r["tf"] == ch["tf"]], + key=lambda r: r["insample_sharpe"]) + fn_const_anch = const_factory(tf=ch["tf"], **crow["params"]) + print(f" miglior costante su {ch['tf']}: {crow['params']} IS {crow['insample_sharpe']}") + else: + fn_const_anch = fn_const + s0 = series_at_anchor(fixed_target, "fixed", hours, 0) + K = pd.concat({"a": s0, "b": base_ch}, axis=1, join="inner").dropna() + print(f" sanity ancora 0 vs serie canonica del fisso: max|Δ| " + f"{float(np.max(np.abs(K['a']-K['b']))):.3e} (n={len(K)})") + for lab, mtr in (("Sharpe FULL", A._sh), + ("Sharpe HOLD", lambda s: A._sh(s[s.index >= HOLDOUT]))): + b_c = A.anchor_luck_band(lambda o: series_at_anchor(fn_ch, "cand", hours, o), offs, + metric=mtr) + b_k = A.anchor_luck_band(lambda o: series_at_anchor(fn_const_anch, "const", hours, o), + offs, metric=mtr) + print(f" {lab:<12} adattivo: canonica {b_c['canonical']:+.3f} " + f"(pctl {b_c['canonical_pctl']:.2f}) MEDIANA {b_c['median']:+.3f} " + f"banda [{b_c['lo']:+.3f},{b_c['hi']:+.3f}]") + print(f" {'':<12} costante: canonica {b_k['canonical']:+.3f} " + f"(pctl {b_k['canonical_pctl']:.2f}) MEDIANA {b_k['median']:+.3f} " + f"banda [{b_k['lo']:+.3f},{b_k['hi']:+.3f}]") + dl = A.anchor_luck_delta(lambda o: series_at_anchor(fn_ch, "cand", hours, o), + lambda o: series_at_anchor(fn_const_anch, "const", hours, o), + offs, metric=mtr) + print(f" {'':<12} DELTA APPAIATO (adatt - cost): mediana {dl['median_paired']:+.3f} " + f"positivo in {dl['n_positive']}/{dl['n_anchors']} " + f"banda [{dl['lo']:+.3f},{dl['hi']:+.3f}] gate_pass={dl['gate_pass']}") + + # -- 8-bis. ANCORA APPAIATA per la cella DAVVERO ADATTIVA --------------------------- + if fn_ad_keep is not None and best_keep["tf"] == ch["tf"]: + hr("8-bis. ANCORA APPAIATA — cella DAVVERO ADATTIVA vs miglior costante") + for lab, mtr in (("Sharpe FULL", A._sh), + ("Sharpe HOLD", lambda s_: A._sh(s_[s_.index >= HOLDOUT]))): + dl = A.anchor_luck_delta( + lambda o: series_at_anchor(fn_ad_keep, "adatt", hours, o), + lambda o: series_at_anchor(fn_const_anch, "const", hours, o), offs, metric=mtr) + b = A.anchor_luck_band(lambda o: series_at_anchor(fn_ad_keep, "adatt", hours, o), + offs, metric=mtr) + print(f" {lab:<12} adattiva-vera: canonica {b['canonical']:+.3f} " + f"(pctl {b['canonical_pctl']:.2f}) MEDIANA {b['median']:+.3f} " + f"banda [{b['lo']:+.3f},{b['hi']:+.3f}]") + print(f" {'':<12} DELTA APPAIATO vs costante: mediana {dl['median_paired']:+.3f} " + f"positivo in {dl['n_positive']}/{dl['n_anchors']} " + f"banda [{dl['lo']:+.3f},{dl['hi']:+.3f}] gate_pass={dl['gate_pass']}") + elif fn_ad_keep is not None: + print(f"\n (8-bis non girata: la cella adattiva sta su {best_keep['tf']} e il costante " + f"su {ch['tf']} -> le ancore non sarebbero coppie)") + + # -- 8-ter. IL CRITERIO DEL FALSIFICATORE, SU **TUTTE** LE CELLE ADATTIVE ------------ + hr("8-ter. IL CRITERIO DEL FALSIFICATORE APPLICATO A **OGNI** CELLA DAVVERO ADATTIVA") + print(" Il 22/08 dichiarava: «un rilevatore il cui L_t si muove DAVVERO e batte il miglior") + print(" lookback COSTANTE in >=6/8 anni appaiati con corr->TP01 < 0.6». Non basta che") + print(" fallisca il VINCITORE: si applica il criterio a tutte e", len(ad_rows), "le celle adattive.") + print(f"\n {'tf':>4} {'cella':<58} {'corr':>6} {'anni>0':>7} {'dShFULL':>8} {'dShHOLD':>8} esito") + passed = [] + for r in sorted(ad_rows, key=lambda x: -x["insample_sharpe"]): + fnr = factory(tf=r["tf"], **r["params"]) + dr = A.candidate_daily(fnr, tf=r["tf"]) + JJ2 = pd.concat({"a": dr, "b": tp01}, axis=1, join="inner").dropna() + cor = float(JJ2["a"].corr(JJ2["b"])) + yr = paired_by_year(dr, d_const) + nyp = sum(1 for x in yr if x["d_sh"] > 0) + Jr = pd.concat({"C": dr, "B": d_const}, axis=1, join="inner").dropna() + JrH = Jr[Jr.index >= HOLDOUT] + dF = A._sh(Jr["C"]) - A._sh(Jr["B"]) + dH = A._sh(JrH["C"]) - A._sh(JrH["B"]) + ok = bool(cor < 0.6 and nyp >= 6) + if ok: + passed.append((r, cor, nyp, dF, dH)) + lab = (f"{r['params']['det']} {r['params']['par']} {r['params']['stat']} " + f"{r['params']['mapping']} {r['params']['blend']}") + print(f" {r['tf']:>4} {lab:<58} {cor:>6.3f} {nyp:>5}/8 {dF:>+8.3f} {dH:>+8.3f} " + f"{'PASSA' if ok else ''}") + print(f"\n celle che soddisfano ENTRAMBE le condizioni del falsificatore: " + f"{len(passed)}/{len(ad_rows)}") + cors = [] + for r in ad_rows: + dr = A.candidate_daily(factory(tf=r["tf"], **r["params"]), tf=r["tf"]) + JJ2 = pd.concat({"a": dr, "b": tp01}, axis=1, join="inner").dropna() + cors.append(float(JJ2["a"].corr(JJ2["b"]))) + print(f" corr->TP01 sulle celle adattive: min {min(cors):.3f} mediana " + f"{np.median(cors):.3f} max {max(cors):.3f} (soglia del falsificatore 0.60)") + + # -- 9. FEE SWEEP --------------------------------------------------------------------- + hr("9. FEE SWEEP — l'adattivo trada di piu': e' li' che muore o no") + for nm, f_, tfx in (("adattivo (A)", fn_ch, ch["tf"]), ("WF-PnL (C)", fn_wf, wch["tf"]), + ("miglior costante (B)", fn_const, cch["tf"]), + ("TP01 30/90/180", fixed_target, "1d")): + line = [] + for fee in (0.0, 0.0005, 0.001, 0.0015): + line.append(f"{2*fee*100:.2f}%RT {A._sh(A.candidate_daily(f_, tf=tfx, fee_side=fee)):+.3f}") + tt = np.mean([A.eval_weights(A.get(a, tfx), A._call_target(f_, A.get(a, tfx), a)) + ["turnover_per_year"] for a in ASSETS]) + print(f" {nm:<22} " + " ".join(line) + f" | turnover {tt:.0f}x/anno") + + hr(f"FINE — {time.time()-t0:.0f}s") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822b_critic.md b/scripts/research/r0822b_critic.md new file mode 100644 index 0000000..d878fd0 --- /dev/null +++ b/scripts/research/r0822b_critic.md @@ -0,0 +1,667 @@ +# CRITICO DI COMPLETEZZA — ondata 2026-08-22 (`r0822b_critic`) + +**Mandato:** non testare una strategia. Rispondere a *cosa non e' stato guardato, quale +affermazione e' rimasta non verificata, e qual e' la misura di maggior valore che nessuno ha fatto*. + +**Materiale letto per intero:** `docs/research/BRIEF-0822.md`, `docs/research/RESULTS-0822.md` +(684 righe, 19 sezioni), `docs/diary/2026-08-22-wave-multiagente.md`, piu' i punti dei singoli +`scripts/research/r0822_*.py` dove il ledger andava verificato. + +**Regola di lettura di questo file.** Distinguo tre stati e li marco: +`[VERIFICATO]` = ho aperto la fonte e il fatto e' controllato in questa sessione (comando +riportato o citazione esatta) · `[DEDOTTO]` = ragionamento su numeri altrui, non ri-misurato · +`[DA MISURARE]` = ipotesi mia, con costo stimato. Non fabbrico numeri nuovi salvo controlli rapidi, +e quando ne faccio uno dico che e' un controllo di cinque minuti e non uno studio. + +**Esito in una riga: l'ondata e' onesta nei singoli filoni e debole nella CUCITURA.** I difetti +gravi non stanno dentro i filoni (che hanno catturato 7 errori propri) ma in tre punti dove +nessuno era responsabile: (a) **due "difetti di dato" sono convenzioni dichiarate nel sorgente di +un fornitore che gira su questa stessa VPS e che nessuno ha aperto**; (b) **il gate principale del +progetto — il deflated-Sharpe — e' stato misurato lo stesso giorno ai suoi due estremi degeneri da +due agenti diversi, e nessuno ha unito le due misure**; (c) **un filone su venti (ALT-OPTIONS) non +e' nel ledger, e il suo risultato falsifica una quarta soglia pubblicata** — quella che tiene +VRP01 fuori dal libro per taglia. + +--- + +## 1. CLAIM NON VERIFICATI + +Ordinati per `(impatto x P(falso)) / costo`. Non elenco tutto: le otto che contano. + +### 1.1 — 🥇 «una firm crypto lista i 19 alt Hyperliquid con short abilitato» (PROP-ALLOC) +`P(falso)` **media-alta** · `impatto` **massimo** · `costo` **un pomeriggio, zero CPU** + +E' l'unica assunzione su cui poggia **l'intero risultato di testa dell'ondata**: `+0,400 di J` +(argmaxJ 25/25/50 contro il libro live 75/25) e' quasi tutto *mettere XS01 su un conto funded*, e +`+0,016` — il 4% — e' il riallocare per la barriera. Se XS01 non e' eseguibile presso la firm, +la riga argmaxJ collassa verso `75/25/0`, che nello stesso script misura **J 0,335 / P(pass) 38,6%** +invece di **0,738 / 82,2%**. Con essa cade anche il *risultato strategico* del 25/07 §4 +(«sul canale funded diversificare paga perche' il vincolo binding e' il DD»), che e' il perno di +tutta la valutazione del canale prop. + +⚠️ **E il progetto ha gia' una regola scritta per questo caso, e non l'ha applicata.** Il 26/07: +*«la negoziabilita' sul conto REALE va verificata quando lo sleeve entra in RICERCA, non quando +entra nel book»* — regola nata dal blocco PRIIPs su GTAA01, dove **cinque settimane di misure** +poggiavano su un'assunzione di conto mai controllata, e il controllo costo' **un ordine di prova**. +`r0822_prop_alloc.py` la ripete: alla riga 100 `CRYPTO_PROP = ("TP01","SKH01","XS01") # cio' che +una firm CRYPTO puo' davvero far eseguire` — un commento, non una misura. [VERIFICATO: letto il +sorgente; l'agente esclude esplicitamente VRP01/GTAA01 *"una firm CRYPTO non lista opzioni Deribit +ne' ETF azionari USA"*, quindi ha pensato al problema e si e' fermato un passo prima di XS01.] + +**Cosa verificare, in ordine:** (a) la lista strumenti di HYROTRADER e FTMO-crypto contiene i 19 +major HL (SOL/BNB/XRP/DOGE/AVAX/LINK/LTC/ADA/ARB/OP/SUI/APT/INJ/TIA/SEI/NEAR/AAVE oltre BTC/ETH)? +(b) short abilitato su ciascuno? (c) quale costo effettivo per gamba (spread + commissione)? +**La (c) ha una soglia gia' misurata in questa stessa ondata**: XS-LITE riporta che la famiglia XS +regge **oltre 50 bps/gamba** — quindi il costo di una firm quasi certamente non morde, ed e' la +(a)/(b) binaria che decide tutto. **Se (a) e' falsa, il verdetto LEAD del filone 1 va riscritto +oggi, non a un gate futuro.** + +### 1.2 — 🥈 «`dealer_net_gamma` e' il GEX col SEGNO INVERTITO» (DEALER-GAMMA) +`P(falso)` **alta — anzi: FALSO come formulato** · `impatto` medio · `costo` **due minuti** + +Il ledger lo chiama *«il risultato piu' riusabile del filone»* e scrive: *«Chi lo usasse col nome +che porta leggerebbe ogni regime al contrario»*. L'agente ha deciso la questione **per +ricostruzione** (corr −0,95 BTC / −0,89 ETH contro un GEX da manuale) e lo dichiara esplicitamente +nel proprio codice: `"Quale, lo decide la sezione 2 (ricostruzione dalla catena), non io"` +(`r0822_dealer_gamma.py:187`). + +🚨 **La fonte autorevole e' su questa VPS e dice il contrario.** [VERIFICATO] La colonna non e' +calcolata da cerbero-bite: bite **inoltra e basta** il campo `total_net_dealer_gamma` che riceve +dal tool MCP `get_dealer_gamma_profile` (`src/cerbero_bite/runtime/market_snapshot_cycle.py:85` e +`clients/deribit.py:342-384`, letti dal repo Gitea `Adriano/Cerbero-Bite`). E il produttore e' +`/opt/docker/cerbero-mcp/src/cerbero_mcp/common/options.py`, **acceso adesso**, che in testa al +file dichiara la convenzione: + +``` +# Convention dealer gamma: i dealer sono SHORT calls (le vendono al retail) e +# LONG puts. ... Net dealer gamma negativo -> mercato instabile (squeeze ...) +... +entry["call_dealer_gamma"] -= contrib # dealer short calls +entry["put_dealer_gamma"] += contrib # dealer long puts +``` + +Cioe': **call negative, put positive** — l'esatta negazione, su entrambe le gambe, della +convenzione da manuale che l'agente ha usato (`call +, put −`, `r0822_dealer_gamma.py:237`). +**Non e' un difetto della colonna: e' una convenzione opposta, deliberata e documentata** — e nel +suo frame `dng < 0` significa *dealer short gamma / mercato instabile*, che e' proprio la +semantica che il docstring di bite dichiara e che l'entry filter §2.8 usava. + +E c'e' una **seconda spiegazione, gratis, per i due numeri residui che l'agente ha attribuito a +non-portabilita'**: la chiamata di bite usa il default `top_n_strikes = 50` [VERIFICATO: +`dealer_gamma_profile(asset_upper)` senza argomenti], contro i **~280-316 strike** della +ricostruzione. Un troncamento ai 50 strike di punta spiega naturalmente sia il coefficiente +`−0,7` (invece di `−1,0`) sia l'offset *«dng=0 corrisponde a GEX=+5,8e6 su BTC»*, che il ledger +presenta come prova che la soglia non e' portabile. + +**Cosa cambia se e' falso (ed e' falso):** il verdetto **SCARTATO regge intatto** (DSR 0,440, +null de-levering fallito, `n_eff` 24-53 ore su 2.130, cella migliore *sotto* il massimo del +rumore). Ma cade la **regola pubblicata**, e cade nel modo peggiore: la frase del ledger *«BTC non +separa, ETH separa al ROVESCIO»* e' formulata in un frame di segno che va rovesciato — quindi +l'unica riga del filone che le ondate future erediterebbero e' quella sbagliata. **Correzione da +fare oggi nel ledger, costo due righe.** + +### 1.3 — 🥉 «`liquidation_long/short_risk` e' costante 'low': meta' dell'ipotesi non esiste, ed e' +comunque morta perche' la colonna non e' ricostruibile» (FLOW-SQUEEZE) +`P(falso)` **alta sulla seconda meta'** · `impatto` medio · `costo` **due ore** + +[VERIFICATO] Il fatto e' vero (`low` in 17.229/17.229 righe non nulle, riprodotto). Ma le due +inferenze che l'agente costruisce sopra il fatto sono entrambe smentibili leggendo la stessa +sorgente di §1.2, `/opt/docker/cerbero-mcp/.../sentiment/fetchers.py:480-518`: + +``` +long_risk = "high" se avg_funding > 0.0001 e delta_24h > 5 + "medium" se avg_funding > 0.00005 e delta_24h > 2 +``` + +Cioe' **e' una soglia su due input, non una misura**. Conseguenze: +- *«Nessuna fonte nostra la produce, e non c'e' niente da produrre»* (`r0822_flow_squeeze.py:626`) + e' **falso**: i due input sono `oi_delta_pct_24h` e il funding cross, e **il funding cross e' nel + file** (`funding_cross_annualized`, copertura 99%), cosi' come `oi_delta_pct_4h` (copertura 99%, + dev.std 1,23%, range −7,1%…+13,4% — [VERIFICATO] con una lettura del parquet). L'agente poteva + ricostruire la variabile di affollamento **con le proprie soglie**, che e' anche metodologicamente + meglio che ereditarne una arbitraria. +- *«Costante = zero informazione»* e' vero come statistica e **sbagliato come lettura**: `low` al + 100% con quelle soglie significa che **nella finestra 2026-03 → 07 non c'e' stato un solo + episodio di affollamento** per quella definizione. Non e' una colonna rotta, e' un fatto di + mercato — e per giunta **rafforza** la conclusione del filone (funding chiuso sul quarto lato) + molto piu' di quanto faccia «la colonna e' inservibile». + +**Cosa cambia se e' falso:** il verdetto SCARTATO regge (l'altra meta' e' sotto la propria MDE +dichiarata: effetto 0,402% contro MDE 1,056%). Ma il filone e' chiuso con la motivazione +sbagliata, e la motivazione sbagliata e' quella che impedisce di riaprirlo con i dati che ci sono. + +### 1.4 — «la finestra forward non va persa: riparare `advance()` + RIGENERARE, nessuna data di +gate si sposta» (MONITOR-AUDIT) +`P(falso)` bassa sul fatto, **alta sull'implicazione** · `impatto` **alto (3 gate)** · `costo` zero + +Il fatto e' provato bene, due volte in modo indipendente (`paper_prevday` 1427/1488 barre identiche +al bit dopo 62 notti; le 6 barre di `paper_statarb` recuperate dopo il guasto EPERM coincidono al +bit). **Non contesto il fatto. Contesto che sia gratis.** + +Una serie **rigenerata** non e' una serie **forward**. La ragione per cui un progetto +pre-registra una finestra forward e' che sia prodotta da codice che *non poteva* essere tarato su +di essa. Dopo la riparazione, la serie su cui si decide il 27/09 sara' prodotta da codice +**scritto oggi, dopo aver visto quella finestra** — e chi scrive la riparazione ha gia' letto, +nel ledger, che la metrica registrata dice +1,96 e quella ricostruita −2,02. La riparazione e' +banale e meccanicamente forzata (scartare la barra non chiusa), quindi il rischio e' piccolo; ma +**e' un grado di liberta' nuovo su un gate pre-registrato, e nessuno lo ha nominato.** + +**Costo di chiuderlo: zero.** Si scrive la riparazione, la si congela con un test, e **solo +dopo** si rigenera — dichiarando in anticipo che nessun parametro verra' toccato in base al +risultato della rigenerazione. Farlo dopo aver visto lo Sharpe rigenerato non e' piu' possibile. + +### 1.5 — «haircut a $5.000 <= 40%» come gamba del gate XSR01 del 23/10 +`P(che il gate sia non-informativo)` **alta** · `impatto` **alto** · `costo` **zero (e' gia' misurato)** + +Due filoni hanno colpito le due gambe del gate del 23/10 **senza che nessuno tirasse la somma**: +- **gamba haircut** — HL-EXEC: a $5.000 col pavimento vero l'haircut sta fra **−7% e +2%**, quindi + *«una condizione pre-registrata che non puo' fallire non e' una condizione»*. XSR-REPRO + dall'altro lato: il **$14,41** pubblicato **non ha alcuno script committato che lo riproduca** ed + e' **~2,4x ottimista** (ticket mediano $3,33, 63% degli ordini sotto $5). +- **gamba Sharpe >= 1,0** — MONITOR-AUDIT: la serie che la misura registra **41 minuti di mercato + al giorno**, `corr` col replay **−0,045**. + +📌 **La somma, che il ledger non fa: al 22/08 il gate del 23/10 e' non-informativo su ENTRAMBE le +gambe** — una non puo' fallire, l'altra e' letta su uno strumento scollegato dalla realta'. Il +ledger scrive «GATE 23/10 COMPROMESSO: PARZIALMENTE». **E' compromesso interamente**, e la +differenza non e' semantica: «parzialmente» autorizza a decidere il 23/10 con una riserva, +«interamente» obbliga a **ri-registrare il gate prima**, che e' l'unica mossa che non e' selezione. + +### 1.6 — «k* empirico ~10-14x» (GROWTH-POLICY) +`P(falso)` **alta come numero** · `impatto` medio-alto · `costo` zero (giunzione di due misure gia' fatte) + +L'agente ha gia' fatto due autocritiche corrette (capitale mediano $4,4e12 a k=14x → *«a k>2 i +numeri sono aritmetica, non previsioni»*; k* lineare nell'errore del drift). Ne manca una terza, e +sta **dentro la stessa ondata**: SLIP-AUDIT misura che la partecipazione al nastro scala lineare +col nozionale e che il caso peggiore osservato e' gia' **21,9% di una barra 5m** a nozionale ~$636. +La leva moltiplica il nozionale esattamente come il capitale. Quindi [DEDOTTO, aritmetica diretta +sulla tabella di SLIP-AUDIT]: + +| leva su $635 | nozionale | peggior partecipazione osservata, riscalata | +|---|---|---| +| 1,00x | $636 | 22% di una barra 5m | +| **1,50x** (il gradino proposto) | $954 | **33%** | +| 5x | $3.180 | 110% | +| **10-14x** (k*) | $6.400-8.900 | **220-310%** | + +📌 **Il gradino 1,00 → 1,50x NON e' toccato da questo** (33% resta dentro il regime misurato): +la raccomandazione operativa sopravvive. **E' il numero k\* che non e' misurabile**, per una terza +ragione indipendente dalle due dichiarate: sopra ~k=5 la serie di ritorni su cui k\* e' calcolato +smette di essere indipendente dalla size, che e' esattamente l'ipotesi che l'agente aveva segnalato +come implausibile senza poterla quantificare. **Ora e' quantificata, gratis, e va scritta accanto +al 10-14x.** + +### 1.7 — «BTC opzioni min 0.1 = $6.210/lotto → VRP01 fuori a $3.000, servirebbe il 61% del conto» +`P(falso)` **verificata FALSA** · `impatto` **alto** · `costo` gia' pagato — il numero esiste e non e' nel ledger + +Questo e' congelato in `tests/test_vrp_profit_take.py` e in CLAUDE.md. VRP-QUOTE-VERE l'ha +falsificato a meta' (famiglia sbagliata) e **ALT-OPTIONS l'ha falsificato del tutto** — ma +ALT-OPTIONS **non ha una sezione nel ledger** (vedi §5.1), quindi il numero non e' mai stato +scritto. [VERIFICATO: letto l'output del filone]: + +| famiglia | f_venue | credito eseg. | **max-loss / lotto** | IM se il venue NON netta | +|---|---|---|---|---| +| **ETH_USDC** | **0,980** | $3,38 | **$16,62** | $33,50 | +| BTC_USDC | 0,921 | $6,25 | $33,75 | $101,79 | +| ETH inverse (la famiglia del muro) | 0,930 | $29,36 | $145,88 | $324,42 | +| BTC inverse (la famiglia del muro) | 0,927 | $81,97 | $418,03 | $1.017,06 | + +Il muro pubblicato e' calcolato sul **nozionale del lotto minimo della famiglia inverse**. La +grandezza giusta per una struttura a rischio definito e' il **max-loss**, e sulla famiglia che il +conto puo' marginare vale **$16,62**. Al peso di libro del 12%, VRP01 entra con un lotto da un +conto di **~$140** (o ~$280 se Deribit non netta le gambe), non da $3.000 al 61% del conto — +**due ordini di grandezza**. Sul conto vero ($635) ci stanno **7 strutture dentro un budget di +rischio del 20%**, con **3.000 lotti al miglior bid** e spread della gamba corta al **2,6%**. + +⚠️ **Cosa NON cambia, e va detto forte:** VRP01 resta fuori dal libro, ma **per la regola +«niente short-vol da modello in deploy» e per il gate IV-rank (0/19 settimane aperte)**, non per +la taglia. Cade *una* delle tre ragioni, ed e' quella che il progetto ripete piu' spesso. + +### 1.8 — «lo screen largo non puo' passare il proprio gate: e' aritmetica» (ORTHO-SCREEN) +`P(falso)` **alta come generalizzazione** · `impatto` **alto (tocca 3 gate futuri)** · `costo` **5 minuti** + +Vedi §2.1: l'ho ri-derivato e il numero e' giusto ma **non dice cio' che il ledger gli fa dire**. + +--- + +## 2. CONTRADDIZIONI FRA AGENTI + +### 2.1 — 🚨 IL DEFLATED-SHARPE MISURATO AI SUOI DUE ESTREMI DEGENERI, LO STESSO GIORNO, DA DUE AGENTI CHE NON SI CITANO + +- **ORTHO-SCREEN**: *«su 168 trial il massimo atteso dal puro rumore e' Sharpe 1,572, sopra il + soffitto direzionale (~1,3) → una ricerca a rete larga NON PUO' passare il proprio gate. Non e' + sfortuna, e' aritmetica.»* DSR di screen **0,034**. +- **SCETTICO su VOL-SIZE**: *«`DSR = 1,000` dichiarato **VACUO, non PASS**: lo passa anche il + baseline — su una famiglia di perturbazioni della stessa strategia il deflated-Sharpe non ha + potenza.»* + +**Sono la stessa osservazione dai due lati, e insieme delimitano il gate principale del progetto.** +[VERIFICATO] Ho aperto `altlib.deflated_sharpe`: il massimo atteso non e' `SE(Sharpe) x sqrt(2 ln N)` +ma la formula Bailey–Lopez de Prado + +``` +sr0 = sd(Sharpe dei trial OSSERVATI) x [ (1-gamma)*z(1-1/N) + gamma*z(1-1/(N e)) ] +``` + +cioe' **la dispersione trasversale delle celle che hai deciso di dichiarare**, moltiplicata per un +fattore che dipende solo da N. Ricalcolando il moltiplicatore [controllo di cinque minuti]: + +| N dichiarato | moltiplicatore | `sr0` con la STESSA dispersione (sd = 0,58) | +|---|---|---| +| 12 | 1,665 | 0,97 | +| **24** | 1,980 | **1,15** | +| 48 | 2,261 | 1,31 | +| 72 | 2,413 | 1,40 | +| **168** (lo screen) | 2,708 | **1,57** | +| 729 | 3,164 | 1,84 | + +E al variare della dispersione, a parita' di dati: + +| dispersione delle celle | `sr0(24)` | `sr0(168)` | +|---|---|---| +| 0,15 (famiglia di perturbazioni, tipo VOL-SIZE) | 0,30 | 0,41 | +| 0,58 (lo screen ortogonale) | 1,15 | 1,57 | + +📌 **Tre letture che il ledger non ha:** +1. **La soglia «1,572» non e' una proprieta' di BTC/ETH: e' una proprieta' della GRIGLIA.** Le + stesse 168 celle, dichiarate come 7 famiglie da 24, danno `sr0 = 1,15` — **sotto** il soffitto + direzionale. Il verdetto «impossibile per aritmetica» si ribalta **senza toccare un dato**. +2. **Percio' la lezione pubblicata invita alla mossa che il progetto ha gia' proibito.** Il ledger + conclude *«le ondate future o dichiarano famiglie molto piu' piccole in anticipo, o cambiano + meccanismo»*. La prima meta' e' **esattamente** il barare che il 30/07 aveva codificato: + *«il deflated-Sharpe e' l'unico posto dove barare e' indolore e invisibile»*, e + *«il verdetto si ribalta col conteggio»* (congelato in un test su VRP01: DSR 0,983 a N=8 / + 0,948 a N=72 / 0,909 a N=360). **Il 22/08 la stessa leva viene raccomandata come buona pratica.** +3. **Cio' che il progetto non sapeva e' la META' PIU' GRANDE: la dispersione.** Il fattore N va da + 1,67 a 3,16 su due ordini di grandezza di trial (1,9x). La dispersione va da 0,15 a 0,58 fra una + famiglia omogenea e uno screen (3,9x). **Il gate e' dominato dalla variabile mai dichiarata.** + +**Conseguenza operativa immediata**, perche' tocca tre gate pre-registrati: DVOLSPREAD 24/10 ha +come criterio (c) *«deflated-Sharpe ricalcolato >= 0,95»* — su una famiglia di 729 celle +omogenee; XSR01 23/10 poggia su DSR 0,983; VOL-SIZE ha gia' dichiarato il proprio DSR vacuo. +**Nessuno dei tre e' interpretabile senza dichiarare, accanto a N, la dispersione delle celle.** +Riformulazione onesta della lezione di ORTHO-SCREEN: *con celle disperse di ~0,58 di Sharpe e un +soffitto di ~1,3, il gate non separa; la cura non e' dichiarare meno trial, e' progettare famiglie +i cui membri siano varianti di UN meccanismo — e li' il DSR e' vacuo, quindi serve un altro gate.* +**Il deflated-Sharpe non ha una regione utile in mezzo, e nessuno l'ha detto.** + +### 2.2 — Lo Sharpe di XSR01: non tre lenti ma **quattro**, e la quarta non e' nel quadro conciliato +Il ledger conclude con un «quadro conciliato»: titolo `demean_skeptic` **1,82** · gate +`basket_gate` **1,75-1,79** · monitor `paper_xsr` **2,21**, e prescrive +*«NUMERO ONESTO DA CITARE: 1,79 (finestra di scoperta) / 1,63 (a oggi)»*. + +[VERIFICATO nell'output di XS-LITE] Lo stesso giorno **XS-LITE**, che dichiara replica **bit-exact** +contro `basket_from_positions(demean=True)` (`max|diff| = 0,000e+00` su 965 barre), pubblica: +*«XSR01 pieno, lente pubblicata: Sharpe **1,64** (pubblicati 1,82)»*, *«normalizzazione COSTANTE +1/50: **1,65**»*, *«sulla FINESTRA DI SCOPERTA (<= 2026-07-25, 937 barre): **1,75**»*. + +Quindi **due agenti che rivendicano entrambi la replica bit-exact della stessa funzione danno 1,75 +e 1,79 sulla stessa finestra.** La spiegazione piu' probabile e' la loro stessa scoperta (l'ultima +barra della finestra di scoperta era parziale il 25/07 e oggi e' chiusa), e i due concordano +sull'oggi (1,64 vs 1,63). **Ma il ledger prescrive una coppia canonica senza nominare l'altra**, e +la regola che pubblica — *«va sempre citata la coppia (lente, ultima barra chiusa)»* — e' proprio +quella che avrebbe risolto la discrepanza se applicata a se stessa. **Costo di chiudere: una riga +nel ledger. Impatto: e' il numero su cui si decide il 23/10.** + +### 2.3 — Il muro di XS01: **tre numeri diversi** nello stesso corpus di documenti +- XS-LITE: capitale minimo perche' il ribilancio passi = **$109 di sleeve (~$730 di libro)**. +- HL-EXEC: haircut ~0 gia' a **$200-600 allocati** (→ a peso 15%, **$1.300-4.000 di conto**). +- Diario §6.5: *«XS01 e' eseguibile a **~$1.300 di conto**»* — cioe' l'estremo basso di HL-EXEC. + +I tre sono compatibili (criteri diversi: soglia del ribilancio vs soglia dell'haircut), ma il +diario ne pubblica **uno** come se fosse *il* numero, dopo che l'ondata ha appena passato una +giornata a dimostrare che i numeri pubblicati a un criterio solo sono il difetto ricorrente del +progetto. **Da citare come banda `$730-$1.300` col criterio accanto.** + +### 2.4 — La leva: due filoni della stessa ondata danno guida opposta +- **GROWTH-POLICY**: k\* ~10-14x, il libro gira al 7% di Kelly, **alzare** la leva vale 14,7a → 11,6a. +- **PROP-ALLOC**: J massimo a **0,75x**, e *«P(pass) senza limite di tempo e' monotona + DECRESCENTE»* (98,9% a 0,50x → 65,9% a 1,50x). + +Non e' un errore: sono obiettivi diversi (crescita in log senza barriera vs sopravvivenza sotto +una barriera per-conto). **Ma nessuno lo scrive**, e l'operatore leggera' due raccomandazioni +opposte nello stesso documento. La sintesi che manca, e che riconcilia: +📌 **anche il conto proprio ha una barriera assorbente** — e' la decisione di venue del 26/07 +(*«zero e' assorbente: andare a zero all'anno 10 di un piano da 16 anni significa non arrivarci +piu'»*). Quindi il criterio di GROWTH-POLICY (Kelly puro) e' il caso limite senza barriera, e +PROP-ALLOC misura cosa succede quando la barriera c'e': **la leva ottima cade da 10x a 0,75x +appena si mette una barriera, e la distanza fra i due numeri E' la misura di quanto la barriera +costa.** Detta cosi', le due misure smettono di contraddirsi e diventano la stessa curva. + +### 2.5 — Il peso di SKH01: respinto da un filone, raccomandato al doppio dall'altro +- **VOL-SIZE / scettico**: *«quasi ogni variante alzava il peso effettivo di SKH fino a 0,375, e + "alzare SKH01" e' gia' stato misurato e respinto il 26/07»* — motivo per cui il null giusto e' + iso-peso. +- **PROP-ALLOC**: sul campione 2019+ che contiene il 2022, l'ottimo della coppia crypto sotto + barriera e' **38/62** (J 0,521 contro 0,443), cioe' **SKH01 al 62%**. + +Di nuovo obiettivi diversi, di nuovo non detto. Ma qui c'e' un aggravante che **l'agente dichiara +da solo**: PROP-ALLOC non ha girato la banda d'ancora, quindi il suo ottimo poggia sull'ancora +canonica di **SKH01, lo sleeve che il LOO del 26/07 misura come quello con la fortuna d'ancora +piu' grande da restituire** (l'ancora regala 2/3 del FULL, 70% dell'hold-out, 80% della protezione +DD). **La distorsione e' a favore della raccomandazione, e la raccomandazione riguarda l'unico +canale che l'operatore potrebbe aprire quest'anno.** Costo di chiuderla: rigirare l'ottimo su una +banda di 8 ancore — l'ordine di grandezza del budget di un agente di questa ondata. + +### 2.6 — Il conteggio degli ingressi VRP nella stessa finestra di catena: 19 contro 22 +VRP-QUOTE-VERE: *«f = 0,714 pooled, **0/19** osservazioni >= 1,0»*; SKEW: *«0,714 su **22** +ingressi»*. Stesso `f` a tre decimali, popolazione diversa. [VERIFICATO che SKEW usa +*«stessa macchina di `cblib`»*, quindi **non e' una replica indipendente** e l'accordo a tre +decimali non e' evidenza.] Spiegazione benigna probabile (VRP richiede **entrambe** le gambe +quotate ai due delta obiettivo, SKEW ricostruisce lo smile e ne richiede meno). **Ma il ledger +presenta il 0,714 come «replica indipendente» del f del 30/07 senza dire che il secondo 0,714 +viene dallo stesso harness.** Impatto basso, costo di chiarirlo nullo. + +### 2.7 — «il collettore raccoglie la famiglia sbagliata» e' una classificazione, non un fatto +Il ledger apre VRP-QUOTE-VERE con 🚨 **IL RISULTATO DI PRODUZIONE**. [VERIFICATO in +`scripts/live/collect_chain.py:114`: `{"currency": asset, "kind": "option"}` e `OI_MIN = 100.0` +alla riga 57 — i due fatti sono esatti.] Ma il collettore serve la **ricerca sui prezzi**, non +l'esecuzione: sulle inverse ci sono 415/548 strumenti con OI>=100 e **97% di quote a due lati**, +contro il 94% e 119 strumenti di ETH_USDC. Per misurare `f`, IV-rank e struttura a termine, la +famiglia raccolta e' **la migliore delle due**. Il difetto esiste solo rispetto a un uso — +l'esecuzione — che oggi e' vietato da un'altra regola («niente short-vol da modello in deploy»). +Il diario lo corregge a meta' (punto aperto 3 lo presenta come decisione sul rate limit); il +ledger no. **Un difetto di produzione e una scelta di raccolta non si segnano con lo stesso +simbolo**, o il prossimo che legge ripara qualcosa che non e' rotto — al costo di **+90% sul giro** +e del rate-limit per-IP che il 29/07 e' gia' costato un guasto. + +--- + +## 3. MODALITA' NON ESPLORATE + +Filtro applicato: (a) compatibile con dati che il progetto possiede o puo' ottenere, (b) non nella +lista dei filoni morti del brief, (c) meccanismo plausibile e nominabile. Niente idee generiche. + +### 3.1 — 🥇 Il costo del CHOP, cioe' il rischio che il libro non ha mai vissuto +**Dati:** esistono (7,4 anni di libro; 2018-19 e 2022-23 come stiramenti laterali; 30 anni di +equity come stampo). **Meccanismo:** il progetto ha misurato la difesa nel **sinistro** — il +criterio B di `edge_watch` e' 8/8 anni di crash superati — e non ha **mai** misurato la modalita' +di fallimento propria di un trend-follower, che non e' il crash ma il **mercato laterale**. E la +misura di controllo che ho fatto in cinque minuti la rende urgente e non teorica: + +| giorno | BTC | ETH | **libro 75/25 (close-only)** | +|---|---|---|---| +| 2020-03-12 | **−41,5%** | **−45,7%** | **+0,97%** | +| 2022-06-13 | −15,5% | — | +0,72% | +| 2021-05-19 | −14,3% | −27,9% | −1,98% | +| 2022-11-09 | −14,4% | −18,0% | 0,00% | +| 2021-01-21 | — | −19,5% | −2,24% | + +[controllo di cinque minuti, non uno studio: `altlib.tp01_baseline_daily()` + `sleeves._skyhook_returns()`, +75/25, ancora canonica. Peggior giorno del libro su 7,4 anni: **−3,38% (2025-10-10)**, che **non e' +un giorno di crash**.] Cioe': **il libro non perde nei crolli — nei crolli guadagna.** Tutte le sue +peggiori giornate sono altrove. **Nessuno ha mai chiesto dove.** Un piano a 10-20 anni su una +strategia il cui unico rischio misurato e' quello che non la tocca ha un buco esattamente della +forma che questo progetto e' bravo a trovare in tutto il resto. + +### 3.2 — 🥈 Il prezzo di essere ciechi: il costo atteso in P&L dell'attrito OPERATIVO +**Dati:** ci sono gia' e sono nostri — `logs/cron_book.log`, `data/venue_watch`, `feed_age_minutes`, +`skh_feed_errors`, `book_executions.jsonl`. **Meccanismo:** nel 2026 il libro e' stato cieco o +stantio almeno tre volte documentate (feed-freeze 14/07; fallback silenzioso di `fresh_5m` il +29/07, latenza d'uscita di SKH01 da ~1h a **~11h** per 6 giri su 8; manutenzione Deribit del 18/08 +sforata **fino alle 14:07**, con astensione del libro). Il progetto ha prezzato fee, slippage, +min-order, pavimento IB, fortuna d'ancora, degrado d'esecuzione, e persino **il fallimento +dell'exchange** — e non ha **mai** prezzato *quanto costa all'anno che il libro non veda*. E' una +misura, non una previsione: si prendono le finestre di cecita' realmente occorse, si guarda cosa ha +fatto il prezzo dentro, e si confronta il P&L realizzato col P&L del percorso non-cieco. +**Perche' conta adesso:** e' un costo che **scala col nozionale** esattamente come la leva, quindi +entra direttamente nel conto di §4; e le uscite di SKH01 sono la gamba latency-sensitive. +**Perche' non e' morto:** non e' calendario, non e' un overlay, non e' una strategia — e' il +prezzo di un rischio operativo gia' materializzato tre volte. + +### 3.3 — 🥉 Lo spread di volatilita' BTC-vs-ETH come sleeve, non come timer +**Dati:** `dvol_btc` / `dvol_eth` dal 2021-03 = **1978 giorni, 5,4 anni** — l'unico dataset non +direzionale del progetto abbastanza lungo da avere un hold-out vero (a differenza di tutta la +catena, ferma a 74-90 giorni). **Meccanismo:** i due asset condividono un fattore di vol comune, e +la *ricchezza relativa* delle due vol e' una grandezza stazionaria per costruzione, mentre il +livello non lo e'. **Non e' DVOLSPREAD:** quello e' il lead in forward-monitor che usa lo spread +come **timer direzionale** (de-risk); qui lo spread si traderebbe **come tale** (straddle lungo su +uno, corto sull'altro), market-neutral rispetto alla direzione e — a differenza di VRP01 — con +esposizione al premio di vol quasi cancellata fra le due gambe. **Non e' nella lista dei morti** +(gamma scalping = long-vol nudo; VRP01 = short-vol nudo gated; questo e' relative-value). +⚠️ **Due muri dichiarati in anticipo:** (i) sarebbe prezzato su DVOL, cioe' BS-flat, e la SKEW di +questa ondata ha appena misurato che il flat costa **x0,869 di struttura a termine** e **x0,920 di +skew** — quindi il verdetto massimo e' LEAD finche' non e' riprezzato sulla catena vera; (ii) +richiede opzioni su entrambi gli asset: fuori a $635 (BTC_USDC ~$780/lotto), dentro a ~$3-5k. + +### 3.4 — Il basis dei futures datati come **campione della coda**, non come carry +BASIS-CALENDAR ha costruito un dataset che il progetto non aveva — **BTC 34 contratti / 240.150 +barre orarie dal 2018-09, ETH 34 / 233.334, piu' 64.110 ore di funding e indice** — e lo ha usato +per una sola domanda (il premio incassabile, IC95 che contiene lo zero → SCARTATO). **Il dataset +contiene la coda che manca a tutto il resto del progetto**, e il ledger lo dice esplicitamente +(*«include il deleveraging 2022 — esattamente la coda che a CC01 mancava per costruzione»*). +**Uso non esplorato:** il blow-out del basis e' l'unico marcatore osservabile e datato dei giorni +di deleveraging forzato. Condizionare a quei giorni le domande di §3.1 e §4 da' un **campione di +stress reale** al posto di uno scenario assunto a mano. +⚠️ **E c'e' un'azione a costo quasi nullo che nessuno ha messo negli aperti:** quel dataset vive in +`.../scratchpad/basis_cache/` (**20 MB**, [VERIFICATO]), cioe' in una cartella di sessione che +sparisce. E' ricostruibile (30/30 trimestrali rispondono e il metodo — costruire a mano i nomi dei +contratti scaduti e chiamare `get_tradingview_chart_data` — e' documentato nello script), ma +ricostruirlo costa rete e tempo che ora sono gia' stati spesi. **Persisterlo in `data/raw/fut_*` +e' l'unica cosa dell'intera ondata che si perde per non averla scritta.** + +### 3.5 — La copertura di coda come **abilitante della leva**, non come miglioramento dello Sharpe +**Stato dell'arte nel progetto, dichiarato:** tail hedge *e' gia' stato provato* — `OPT06` (ratio +put spread con tail hedge) e `OPT07` (collar) nello sweep del 20/06, entrambi **modellati su DVOL +BS-flat** e giudicati **sullo Sharpe**, entrambi bocciati. **Cosa non e' stato fatto:** giudicarlo +sull'obiettivo **crescita a leva**. Sono domande diverse perche' il valore di un pavimento e' +**convesso nella leva**: a k=1 una put lontana e' un costo secco, a k=k\* e' cio' che rende k\* +stimabile. La domanda esatta: *esiste una put a delta basso il cui costo annuo, misurato sulle +quote VERE (non su DVOL), sia minore del guadagno di crescita che il pavimento autorizza?* +⚠️ **Prior onesto: sfavorevole.** Il 30/07 ha misurato che l'ala comprata costa **~2,3x il +modello** (f_long 2,23, fino a 5,85 sui delta piu' bassi) — quindi la protezione reale e' piu' cara +di quella che aveva gia' bocciato OPT06/OPT07. **Ma il confronto non e' mai stato fatto contro la +leva**, ed e' l'unico confronto in cui una protezione cara puo' comunque pagare. +⚠️ Eseguibile solo da ~$3-5k (ETH_USDC ~$250/lotto di nozionale): **e' una domanda per il deposito, +non per oggi.** + +### 3.6 — Il flusso di opzioni (volume per strike), che e' nel dato e nessuno ha guardato +La catena registra `volume_24h` **per strumento**, oraria. Sei filoni hanno usato l'**OI** +(posizioni accumulate) e **zero** hanno usato il **volume** (transazioni). ALT-OPTIONS ha appena +dimostrato che sulle famiglie lineari OI e negoziabilita' sono **anti-correlati (rho di rango +−0,77)** — che e' un'ottima ragione per sospettare che le due grandezze dicano cose diverse anche +sulle inverse. **Meccanismo:** un'ondata di acquisti concentrata su uno strike e' un fatto di +flusso; l'OI e' il suo integrale, e integrare distrugge il segnale di timing. +⚠️ **Muro invalicabile e dichiarato: 74-90 giorni di calendario.** Il verdetto massimo qui e' +**LEAD con gate pre-registrato**, e l'onesta' vuole che si dica prima di iniziare, non dopo. +La ragione per farlo comunque e' che **il calendario cresce da solo**, gratis, ogni ora. + +--- + +## 4. LA MISURA DI MAGGIOR VALORE MAI FATTA + +### La misura +**Il peggior giorno che il libro puo' avere, misurato invece che assunto: la distribuzione della +perdita giornaliera CONDIZIONATA all'esposizione piena, sotto la lente wick accoppiata, con lo +scenario di gap attraverso il disaster-SL.** + +### Perche' questa +Perche' e' **l'unico parametro che blocca la sola leva gratuita che l'ondata abbia trovato**, e +perche' oggi quel parametro e' **un numero scelto a mano**. + +GROWTH-POLICY ha stabilito che il gradino eseguibile `1,00x → 1,25-1,50x` vale +**14,7 anni → 12,9-11,6 anni** al capitale-rendita (a €500/mese, netto fisco). Lo scettico ha +tolto una delle tre riserve (il wick non amplifica il maxDD: ricarico costante ~3,5%). Delle due +rimaste, **una non e' risolvibile** (il drift stimato su 7,4 anni — e comunque k\* resta 7x a −2 SE, +quindi 1,5x sta al 21% di k\*) e **una lo e': la coda.** E' la riserva che decide, perche' e' l'unica +che porta k\* **sotto** il gradino proposto: *«un solo giorno −10% all'anno porta k\* da ~10x a 2x, +−15%/anno a 1x»*. + +📌 **Quel −10% non e' stato misurato: e' stato scelto.** E il controllo di cinque minuti in §3.1 +dice che potrebbe essere **una coda da buy&hold importata dentro un libro difensivo**: sui cinque +peggiori giorni di BTC e i cinque di ETH in 7,4 anni — compreso il **−41,5% / −45,7% del 12 marzo +2020** — il libro ha fatto **+0,97 · +0,72 · 0,00 · −1,98 · −2,24 · +0,23%**, e il suo peggior +giorno assoluto e' **−3,38%** in una data che non e' un crash. Se il vero peggior giorno +plausibile e' −5% e non −10%, k\* non e' 2x e **il gradino e' autorizzato**. + +⚠️ **Ma il mio controllo e' close-only, cioe' proprio la lente che questa ondata ha appena +dichiarato "esattamente cieca" sulle regole a UN giorno** (rapporto acc./close = INF a soglia 5%). +Quindi **la misura vera non e' quella che ho fatto**: e' la stessa domanda sotto +`r0725_prop_coupled` (tuple accoppiate, minimo intra-giorno) piu' lo scenario che il dataset non +contiene — un gap notturno che attraversa il disaster-SL −30% con posizione piena su entrambe le +gambe. Tutta la macchineria esiste gia' (`r0725_prop_coupled`, `r0822_leverage_skeptic`), il +campione di stress si puo' prendere dal dataset dei futures datati di §3.4, e il costo e' +**un agente di questa ondata**. + +### Confronto esplicito con le leve gia' quantificate +| leva | valore | costo | stato | +|---|---|---|---| +| versare €250/mese invece di €0 | da **mai** a 19,8a (P 52% netto fisco) | €250/mese | decisa dall'operatore | +| versare €500 invece di €250 | 19,8a → **14,7a** | €250/mese in piu' | decisa dall'operatore | +| **leva 1,00x → 1,50x** | **14,7a → 11,6a** | **zero** | **bloccata da UN parametro assunto** | +| riallocare per la barriera (prop) | +0,016 di J = **il 4%** | analisi | misurata, marginale | +| XS01 su conto funded | +0,400 di J | **assunzione non verificata (§1.1)** | LEAD | +| ottimizzare il peso di SKH01 | **+0,030 di Sharpe** | analisi | gate fallito, plateau | +| rischio venue a p=5% | capitale mediano → **zero** | split di conto | decisa: 100% Deribit fino a $20k | + +📌 **Il gradino di leva vale ~3,1 anni, cioe' l'equivalente di ~€300/mese di versamenti, a costo +zero.** E' la prima volta in due mesi che una misura di ricerca entra nell'ordine di grandezza +della leva dei versamenti, e ci entra perche' **non e' ricerca di alpha: e' un cambio di scala di +cio' che gia' funziona.** Vale **100x** l'ottimizzazione del peso di SKH01 e **25x** il riallocare +per la barriera. + +### E adesso la parte che il mandato chiede di dire se e' vera +**Sì: nessuna misura di ricerca di questa ondata batte "versare".** Venti filoni, ~15.300 righe, +**zero candidati promossi**; il soffitto direzionale e' stato riconfermato per via aritmetica; e +per **tre volte** l'eseguibilita' a $600 non e' stata il vincolo binding — tutto e' morto +sull'**edge**. La misura che propongo **non e' un'eccezione a questo, ne e' la conferma**: non +cerca un rendimento nuovo, **prezza un parametro di rischio per poter usare meglio quello che c'e'**. +E anche cosi', 3,1 anni sono **meno** di cio' che valgono €250/mese di differenza nei versamenti. + +**La seconda misura, e va detta insieme alla prima perche' costa ancora meno:** la verifica di +§1.1 (i 19 alt HL sono listati e shortabili presso la firm?). Non e' ricerca, e' **un pomeriggio di +lettura sul sito di due firm** — e decide se il canale prop, l'unico che moltiplica il nozionale +senza possedere capitale, vale J 0,738 o J 0,335. **Il progetto ha gia' pagato una volta il prezzo +di non fare questa verifica** (GTAA01/PRIIPs, cinque settimane di misure su un ETF che il broker +rifiuta di vendere) e ha scritto la regola per non ripeterlo. **Farla prima di aprire un altro +filone prop e' la cosa piu' economica che ci sia in questo elenco.** + +**In una riga:** *misurare la coda invece di assumerla* e' la ricerca che vale di piu' (3,1 anni, +gratis); *leggere il listino di una prop firm* e' la verifica che vale di piu' (un canale intero, +un pomeriggio); e **nessuna delle due batte i versamenti**, che restano il vincolo binding come il +26/07 aveva gia' stabilito. + +--- + +## 5. AUTOCRITICA DELL'ONDATA + +### 5.1 — 🚨 Un filone su venti non e' nel ledger, ed e' quello con il risultato eseguibile oggi +**ALT-OPTIONS (filone 14, `r0822_alt_options.py`, 709 righe) non ha ne' una riga di tabella ne' +una sezione in `RESULTS-0822.md`.** [VERIFICATO: 19 sezioni `###`, 18 righe di tabella, nessuna +per il 14 — e la tabella ha anche un difetto di markdown che **fonde le righe 9 e 4 su una sola +linea** (`... = TP01 travestito || 4 | DEALER-GAMMA | ...`).] Il filone e' citato **due volte** da +un altro filone (*«vedi filone 14»*, *«ETH_USDC resta la gamba giusta ... (filone 14)»*) e serve +al coordinatore per **ritirare una propria inferenza sbagliata** sull'OI — ma il lettore non trova +la fonte da nessuna parte. + +Nel diario compare come **due parole** dentro l'elenco dei morti: *«alt-options (alt)»*. **La +sintesi e' sbagliata due volte:** il filone non muore «perche' alt» (la sua conclusione e' che +**SOL/XRP/HYPE/AVAX sono i peggiori**: SOL_USDC ha il **9%** dei put quotati a due lati e +`f_venue` **negativo**), e cio' che trova e' che **ETH_USDC** — non un alt — e' la gamba con +`f_venue = 0,980`, spread della corta al **2,6%**, **max-loss $16,62/lotto** e **3.000 lotti al +best bid**. Cioe' §1.7: **la quarta soglia pubblicata falsificata dall'ondata**, quella che tiene +VRP01 fuori per taglia — e il diario ne annuncia **tre**. + +**Lezione:** il ledger e' stato scritto *man mano* (lo dice la sua prima riga) e un filone che +finisce mentre si scrive la sintesi puo' sparire senza che nulla lo segnali. Serve una guardia +banale: **contare i file `r0822_*.py` e le sezioni del ledger, e non chiudere l'ondata se non +combaciano.** E' lo stesso principio che l'ondata ha appena raccomandato per i monitor. + +### 5.2 — Il gate `implausible_sharpe` non e' stato girato dove serviva, per la seconda volta +MONITOR-AUDIT lo osserva bene: *«`implausible_sharpe` esiste dal 26/07 e non e' mai stato puntato +sulle serie forward»* (avrebbe segnalato Sharpe −15,8 su 28 barre). Ma la stessa mancanza si +ripete **dentro l'ondata**: il gate non compare in PROP-ALLOC (dove J e P(pass) sono costruiti su +percorsi bootstrap, non su una serie reale — legittimo, ma va dichiarato) ne' in GROWTH-POLICY, il +cui output include un **capitale mediano di $4,4e12** — che e' letteralmente la firma per cui il +gate e' stato scritto. L'agente ha fatto il controllo di plausibilita' **a mano** e l'ha riportato +con onesta'; **il gate del progetto non e' stato invocato.** Un gate che si applica a mano e' un +gate che un giorno non si applichera'. + +### 5.3 — Due «difetti di dato» diagnosticati per ricostruzione con la sorgente a due minuti di distanza +E' il tema di §1.2 e §1.3, e vale come **critica di metodo dell'ondata**, non dei due agenti. Il +progetto ha codificato il 07/08: *«una fonte normativa citata in un commento di codice si verifica +come un numero»* — nata da una legge citata male in 5 file per settimane. Il 22/08 due filoni +dichiarano un difetto in un dato prodotto da `/opt/docker/cerbero-mcp` (**acceso, sulla stessa +macchina, e per giunta gia' usato da PythagorasGoal per Hyperliquid**) e da +`Adriano/Cerbero-Bite` (su Gitea, ultimo commit di dismissione), **senza aprire ne' l'uno ne' +l'altro**. Io li ho aperti entrambi in **meno di dieci minuti**. Il brief e' complice: elenca le +dieci trappole statistiche e **non dice «prima di dichiarare un difetto di dato, leggi il codice +che quel dato lo produce»**. E' la regola nuova piu' economica che questa ondata possa lasciare. + +### 5.4 — Il coordinatore ha lasciato passare una conclusione debole, e ne ha corretta una propria +**Corretta (a merito):** l'inferenza *«BTC_USDC e' praticamente morto»*, ritirata dopo la misura di +ALT-OPTIONS, con la ragione scritta accanto. E' il comportamento giusto. +**Lasciata passare:** la lezione di ORTHO-SCREEN (§2.1). E' etichettata *«il risultato piu' +riusabile dell'ondata»* e *«non e' sfortuna, e' aritmetica»* — due formule che chiudono la +discussione — mentre la sua meta' operativa (*«dichiarare famiglie molto piu' piccole in +anticipo»*) e' **la manovra che il progetto ha proibito il 30/07** e la sua meta' quantitativa +dipende da una variabile (la dispersione delle celle) **mai nominata**. Il segnale che avrebbe +dovuto fermarla c'era ed era **nello stesso ledger, sette sezioni piu' avanti**: il DSR vacuo di +VOL-SIZE. **Due misure dello stesso gate, versi opposti, stesso giorno, nessun rimando.** +La sintesi di venti filoni ha un compito che nessun filone ha: **incrociare**. Qui non l'ha fatto, +e non l'ha fatto neanche su §2.4 (leva), §2.5 (peso SKH01), §1.5 (le due gambe del gate 23/10) e +§1.6 (k\* contro la partecipazione al nastro) — **cinque incroci disponibili a costo zero, tutti +fra coppie di filoni gia' consegnati.** + +### 5.5 — Agenti che hanno dichiarato «non girabile» cio' che era girabile +- **FLOW-SQUEEZE**: *«liquidation_long/short: NO ... Nessuna fonte nostra la produce, e non c'e' + niente da produrre»*. I due input della regola sono **nel file** (§1.3). Costo del mancato + lavoro: ~due ore. Il verdetto non sarebbe cambiato; la **motivazione** si'. +- **DEALER-GAMMA**: *«Quale [convenzione], lo decide la sezione 2 (ricostruzione dalla catena), + non io»* — decidibile leggendo, in due minuti (§1.2). +- **PROP-ALLOC**: la banda d'ancora dichiarata *«non girata, fuori budget»*. Onesto, e la + distorsione e' dichiarata a proprio sfavore. **Ma e' l'unica distorsione dichiarata che punta + nella direzione della raccomandazione**, sull'unico filone che raccomanda qualcosa: dovrebbe + essere il primo pezzo di budget di un seguito, non l'ultimo. +- **TERM-STRUCTURE**: *«`deflated_sharpe` NON girato, e la motivazione e' il risultato»* — questo + invece e' **corretto e da imitare**: con 74 osservazioni l'incertezza di campione domina di un + ordine di grandezza quella da selezione, e deflazionare 312 trial avrebbe dato *«un numero + preciso e privo di senso»*. **E' anche la migliore descrizione, scritta senza saperlo, del + problema di §2.1.** + +### 5.6 — Cosa l'ondata ha fatto meglio della media del progetto (perche' un'autocritica che elenca +solo difetti non e' calibrata) +Sette agenti hanno catturato un **errore proprio** prima di pubblicare; tre hanno **refutato una +propria ipotesi in corsa**; uno scettico ha **ritirato una regola pubblicata poche ore prima** +provando che il difetto non esisteva — e la regola ritirata era **sua di famiglia**. Quattro +filoni hanno dichiarato la **potenza prima di guardare** (OI-PIN, FLOW-SQUEEZE, SLIP-AUDIT, +VOL-SIZE) e in tre casi il candidato e' morto **contro la propria soglia dichiarata**, che e' il +modo piu' pulito di morire. Le repliche **bit-exact prima di ogni delta** sono ormai +sistematiche (5/5 in XS-LITE e HL-EXEC, `max|diff| = 0,0`). **Il metodo dei singoli filoni non e' +il problema di questa ondata: la cucitura lo e'.** + +--- + +## Appendice — cosa ho verificato in questa sessione, e come + +| # | fatto | come | +|---|---|---| +| A | `dealer_net_gamma` = convenzione **dichiarata** (dealer short calls / long puts) nel sorgente di `cerbero-mcp`, non un difetto | letto `/opt/docker/cerbero-mcp/src/cerbero_mcp/common/options.py:11-14, 138-140` | +| B | bite **inoltra** il campo e non lo calcola; chiama con `top_n_strikes=50` di default | clone `Adriano/Cerbero-Bite`, `runtime/market_snapshot_cycle.py:85`, `clients/deribit.py:342-384` | +| C | `liquidation_*_risk` = soglia su `oi_delta_24h` + funding, e **i due input sono nel parquet** (copertura 99%, `oi_delta_pct_4h` std 1,23%) | `sentiment/fetchers.py:480-518` + lettura di `cb_market_snapshots.parquet` | +| D | `sr0` del DSR = `sd(Sharpe dei trial) x mult(N)`; con sd=0,58, `sr0` passa da **1,57 (N=168)** a **1,15 (N=24)** | letto `altlib.deflated_sharpe:689-712`, moltiplicatori ricalcolati | +| E | il libro 75/25 fa **+0,97%** il 12/03/2020 (BTC −41,5%, ETH −45,7%); peggior giorno **−3,38%**, non un crash | `altlib.tp01_baseline_daily()` + `sleeves._skyhook_returns()`, close-only | +| F | `collect_chain.py` interroga `{"currency": asset}` e filtra `OI_MIN = 100.0` | `scripts/live/collect_chain.py:57, 114, 181` | +| G | il feed certificato e' `BTC/USD:BTC` (**inverse**), il libro esegue `BTC_USDC-PERPETUAL` (**lineare**) | `rebuild_history.DERIBIT_INSTR:48` vs `src/live/deribit.INSTRUMENT:110` | +| H | ALT-OPTIONS: ETH_USDC `f_venue` 0,980 / max-loss **$16,62** / 7 strutture al 20% di $635 | output del filone in scratchpad | +| I | il dataset dei futures datati (**20 MB**) vive solo in `scratchpad/basis_cache/` | `du -sh` | +| J | `RESULTS-0822.md`: 19 sezioni, 18 righe di tabella, **filone 14 assente**, righe 9 e 4 fuse | conteggio diretto | + +⚠️ **Su (G):** e' un fatto, non un'accusa. `rebuild_history` lo dichiara in un commento +(*«inverse, storia lunga ~ lineare entro 3 bps»*) e SLIP-AUDIT ha misurato la base ai 18 fill +(BTC −0,03 bps, ETH −0,27) — trascurabile sui **ritorni**, che dipendono dalle *variazioni* della +base. Lo segno solo perche' e' la **5ª occorrenza nell'ondata** dello schema «un controllo puntato +su uno strumento diverso da quello che si trada» (dopo `fee_watch` 21/08, il collettore della +catena, il feed dello slippage e la taratura di `venue_watch`), e perche' la parte non verificata +di quel commento — *«~ lineare entro 3 bps»* su **tutta la storia** — non e' mai stata misurata: +esiste una misura su 18 istanti, non sulle 60.000 ore in cui entrambi gli strumenti esistono. +Costo di chiuderla: un'ora. + +--- + +**Book, pesi, cron, config: questo filone non tocca niente.** Consegna un'analisi. +Le uniche azioni che raccomanda sono a costo quasi nullo e tutte su documenti o verifiche: +correggere §1.2 nel ledger, aggiungere la sezione mancante di §5.1, dichiarare la dispersione +accanto a ogni DSR (§2.1), ri-registrare il gate del 23/10 prima di rigenerare le serie (§1.4-1.5), +persistere il dataset dei futures (§3.4), e leggere il listino di una prop firm prima di +misurare un'altra volta il canale che ne dipende (§1.1). diff --git a/scripts/research/r0822b_maker.py b/scripts/research/r0822b_maker.py new file mode 100644 index 0000000..a2d0a5f --- /dev/null +++ b/scripts/research/r0822b_maker.py @@ -0,0 +1,733 @@ +#!/usr/bin/env python +"""r0822b_maker.py — l'ESECUZIONE PASSIVA (maker) vale qualcosa per il libro live? + +CONTESTO (fatti gia' nel progetto, non ri-misurati qui) +------------------------------------------------------- +* Il libro Deribit (TP01 75% + SKH01 25%, nettati su UNA sola posizione per strumento) manda + **solo ordini market** -> paga sempre il **taker**. `src/live/execution.py::_submit` ha + `order_type="market"` come default e `rebalance_signed` non passa mai altro. +* Fee misurate: **taker 3,50 bps/lato** (fee_watch + 16/16 fill sul nastro, 22/08), + **maker 1,50 bps/lato** (listino base dal 2026-08-01; fino al 31/07 era 0,00). + I backtest del progetto modellano **5,00 bps/lato** (0,10% RT) -> gia' conservativi. +* Il progetto ha guardato il lato passivo UNA volta (raccomandazione "T1" del 26/07: il TP di + SKH01 come limit resting) e l'ha chiusa **non per la fee** ma perche' un ordine al livello di + SKH chiuderebbe anche la quota TP01 (**netting su strumento unico**, `src/live/book.py`). + ⚠️ QUEL VINCOLO NON SI APPLICA QUI: rendere passivo **l'ordine netto di ribilanciamento** e' + UN ordine sulla posizione netta -> niente conflitto fra sleeve. Il netting uccide i bracket + per-sleeve, non l'ordine netto. E' la ragione per cui questa domanda e' nuova. +* Slippage misurato il 22/08: **~0 a questa taglia** (un fill ha preso il 22% del volume della + sua barra 5m, range 1,08 bps) -> il differenziale maker/taker e' fee PURA, e il mezzo spread + catturato da un passivo e' <0,005 bps (tick BTC 0,1 su ~$110k; ETH 0,01 su ~$2.500) = ZERO. + Percio' qui il passivo NON guadagna spread: guadagna 2,00 bps di fee e rischia il non-fill. +* Il disaster-SL −30% e' uno `stop_market` on-book: NON e' toccato da nulla di quanto segue. + +LA DOMANDA CHE NESSUNO HA FATTO +------------------------------- +Un limit resting non e' un market order piu' economico: **puo' non riempirsi**. E non riempirsi +su un segnale di trend significa **perdere il movimento**, che e' esattamente il caso in cui non +si e' riempito (il prezzo e' scappato dalla parte giusta). Quindi: + costo atteso del passivo = P(fill)*(−2,00 bps) + P(no fill)*E[mossa avversa | no fill] +e la seconda parte e' **avversa per costruzione**: un buy limit al riferimento non si riempie se +e solo se il prezzo non e' MAI sceso al livello, cioe' e' salito. + +COSA FA QUESTO SCRIPT +--------------------- + 1. il differenziale in Sharpe/CAGR di libro, riusando la curva fee gia' pubblicata + (`r0726_fee_sensitivity.py`: −0,017 Sharpe/bps, −0,23% CAGR/bps) E ri-misurandola a 4 punti + di fee sul path del libro (verifica indipendente, non ri-derivazione della curva); + 2. ricostruisce gli **ORDINI** del libro live (target netto orario, banda min-order $5) e li + confronta con i 18 ordini veri di `data/live/book_executions.jsonl` (controllo di realta'); + 3. per OGNI ordine misura su 5m certificati: P(fill) di un limit al riferimento entro N minuti, + e il ritorno condizionato **ai casi in cui NON si riempie**. Confronto **APPAIATO PER + ORDINE** (mai per esito: allineare sui riempiti tiene solo i casi in cui non e' successo + niente — trappola gia' presa 3 volte in questo progetto); + 4. trasforma il costo per-ordine in una serie di P&L additiva ai rendimenti giornalieri del + libro -> Sharpe/CAGR/maxDD della politica passiva, e il numero in **dollari all'anno**; + 5. mette il risultato accanto alle altre leve gia' misurate. + +LENTI E SEMPLIFICAZIONI — DICHIARATE (tutte GENEROSE verso il maker, di proposito) +--------------------------------------------------------------------------------- + (a) griglia oraria: il cron gira a :07. Decisione con le barre 5m chiuse fino a h:05, prezzo di + riferimento = close della barra 5m etichettata h:00. Verificato sul log vero: gli ordini + cadono a :07 di un'ora qualsiasi, e TP01 si aggiorna alle 01:07 UTC. + (b) TP01 cambia target una volta al giorno, all'ora 01:00 UTC (lente backtest; l'effetto della + barra parziale e' misurato +-0,03 Sharpe, 26/07 -> irrilevante per la TAGLIA degli ordini). + (c) SKH01: ingressi alla CHIUSURA del bin 230m (lente backtest), uscite al primo confine orario + dopo il tocco 5m (lente live, `_skyhook_positions` itera nel bin parziale). L'ingresso vero + del live e' INTRA-BIN, cioe' piu' vicino al breakout -> peggiore per un limit passivo: + questa lente e' **generosa** verso il maker. + (d) fill: un limit BUY al prezzo P si considera riempito se `low <= P` nella finestra. Ignora la + coda del book (al livello si sta DIETRO agli ordini gia' presenti: un semplice tocco non + riempie) -> **generosa** verso il maker. Finestra di scansione da h:05 (l'ordine e' a h:07: + regaliamo 2 minuti di opportunita' di fill). + (e) ordini tutto-o-niente, nessun fill parziale. + (f) niente cap per-asset nella ricostruzione (a $635 il cap $318 morde di rado e riduce + PROPORZIONALMENTE guadagno e danno: non cambia il segno). + + nice -n 19 timeout 900 uv run python scripts/research/r0822b_maker.py +""" +from __future__ import annotations + +import json +import sys +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from src.backtest.harness import backtest_signals # noqa: E402 +from src.data.downloader import load_data # noqa: E402 +from src.portfolio.portfolio import combine_outer, metrics, to_daily # noqa: E402 +from src.strategies.skyhook import (LTF_MIN, SKH01_V2_DD, build_frames, # noqa: E402 + skyhook_entries) +from src.strategies.trend_portfolio import (CANONICAL, TrendPortfolio, # noqa: E402 + resample_1d, simple_returns) + +ASSETS = ("BTC", "ETH") +W_TP01, W_SKH = 0.75, 0.25 # pesi del book Deribit (src/live/book.py) +WEIGHT = 0.5 # book 50/50 BTC+ETH +MIN_ORDER_USD = 5.0 # config/live.json +EQUITY_LIVE = 635.0 # conto vero (22/08) + +FEE_TAKER_BPS = 3.50 # misurata sul nastro + fee_watch +FEE_MAKER_BPS = 1.50 # listino base dal 2026-08-01 +FEE_MODEL_BPS = 5.00 # cio' che i backtest modellano +FEE_GRID_BPS = (0.0, FEE_MAKER_BPS, FEE_TAKER_BPS, FEE_MODEL_BPS) + +# curva PUBBLICATA (r0726_fee_sensitivity.py) — riusata, non ri-derivata +PUB_SH_PER_BPS = -0.017 +PUB_CAGR_PER_BPS = -0.23 # punti percentuali di CAGR per bps/lato + +HORIZONS_MIN = (5, 15, 30, 60, 120) # attesa massima del limit prima di inseguire +# CODA DEL BOOK — il parametro che DECIDE, e che l'OHLC non contiene. +# Un limit al livello sta DIETRO agli ordini gia' presenti: "il prezzo ha toccato il mio livello" +# NON e' "sono stato riempito". `q` = quanti bps il prezzo deve attraversare OLTRE il livello +# perche' la coda davanti sia consumata. q=0 e' il LIMITE SUPERIORE ottimistico (e a q=0 il fill +# e' quasi automatico: l'apertura della prima barra di scansione COINCIDE col livello, quindi +# `low <= P` chiede solo che il prezzo non scappi istantaneamente). +QUEUE_BPS = (0.0, 1.0, 2.0, 5.0, 10.0) +P2_MAX_HOURS = 24 # politica "mai inseguire": ri-posta ogni ora, tetto 24h +CAPITALS = (635.0, 2000.0, 5000.0) +HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC") +SEED = 20260822 + + +# =========================================================================================== +# 1. SEGNALI — calcolati UNA volta per asset (non dipendono dalla fee) +# =========================================================================================== +def build_asset(asset: str) -> dict: + """Tutto cio' che serve per l'asset: TP01 daily, path SKH01, griglia 5m e oraria.""" + df5 = load_data(asset, "5m") + ts5 = pd.to_datetime(df5["datetime"], utc=True) + A = dict( + ts5=ts5.values.astype("datetime64[ns]"), + o5=df5["open"].values.astype(float), h5=df5["high"].values.astype(float), + l5=df5["low"].values.astype(float), c5=df5["close"].values.astype(float), + ) + + # --- SKH01: ingressi/uscite sulla griglia 230m, poi tempo di RILEVAZIONE oraria + ltf, htf = build_frames(df5) + ent = skyhook_entries(ltf, htf, SKH01_V2_DD) + H, L = ltf["high"].values, ltf["low"].values + ltf_ts = pd.to_datetime(ltf["datetime"], utc=True) + n = len(ltf) + trades = [] + i = 0 + while i < n: + e = ent[i] + if e is None: + i += 1 + continue + d, sl, tp, mb = e["dir"], e["sl"], e["tp"], e["max_bars"] + exit_idx = None + for s in range(1, mb + 1): + j = i + s + if j >= n: + break + hit = (L[j] <= sl or H[j] >= tp) if d == 1 else (H[j] >= sl or L[j] <= tp) + if hit or s == mb: + exit_idx = j + break + if exit_idx is None: # trade ancora aperto a fine storia + trades.append((i, None, d, sl, tp)) + break + trades.append((i, exit_idx, d, sl, tp)) + i = exit_idx + 1 + + # tempi di rilevazione: ingresso a chiusura bin (lente backtest); + # uscita al primo confine orario dopo il TOCCO 5m (lente live) + ltf_close = ltf_ts + pd.Timedelta(minutes=LTF_MIN) + skh_events = [] # (ts_rilevazione, segno) + for (i0, j0, d, sl, tp) in trades: + skh_events.append((_ceil_hour(ltf_close.iloc[i0]), int(d))) + if j0 is None: + continue + t_touch = _first_touch_5m(A, ltf_ts.iloc[j0], ltf_close.iloc[j0], d, sl, tp) + skh_events.append((_ceil_hour(t_touch), 0)) + + # --- TP01: target giornaliero, visibile dall'ora 01:00 del giorno DOPO la chiusura + d1 = resample_1d(load_data(asset, "1h")) + tp_cfg = TrendPortfolio(**CANONICAL) + tgt = tp_cfg.target_series(d1) + d1_ts = pd.to_datetime(d1["datetime"], utc=True) + tp_events = [(pd.Timestamp(t).normalize() + pd.Timedelta(hours=25), float(v)) + for t, v in zip(d1_ts, tgt)] # barra del giorno D -> vista alle 01:00 di D+1 + + # --- griglia oraria dei momenti di decisione (barre 5m etichettate :00) + m = pd.DatetimeIndex(A["ts5"]) + hmask = (m.minute == 0) + A["hours"] = m[hmask].tz_localize("UTC") + A["hidx"] = np.flatnonzero(hmask) # indice 5m della barra di riferimento + A["ref"] = A["c5"][A["hidx"]] # prezzo di riferimento (close 5m di h:00) + A["skh_events"] = skh_events + A["tp_events"] = tp_events + A["daily_ret"] = simple_returns(d1["close"].values.astype(float)) + A["daily_tgt"] = tgt + A["d1_ts"] = d1_ts + A["ltf"] = ltf + A["ent"] = ent + return A + + +def _ceil_hour(t: pd.Timestamp) -> pd.Timestamp: + return t.ceil("h") + + +def _first_touch_5m(A: dict, t_open, t_close, d: int, sl: float, tp: float) -> pd.Timestamp: + """Primo istante 5m, dentro il bin 230m d'uscita, in cui SL o TP e' toccato. + Se nessuno e' toccato (uscita per max_bars) ritorna la chiusura del bin.""" + lo = np.searchsorted(A["ts5"], np.datetime64(t_open.tz_localize(None))) + hi = np.searchsorted(A["ts5"], np.datetime64(t_close.tz_localize(None))) + for k in range(lo, min(hi, len(A["ts5"]))): + hit = (A["l5"][k] <= sl or A["h5"][k] >= tp) if d == 1 else \ + (A["h5"][k] >= sl or A["l5"][k] <= tp) + if hit: + return pd.Timestamp(A["ts5"][k], tz="UTC") + pd.Timedelta(minutes=5) + return t_close + + +def exposure_path(A: dict) -> pd.DataFrame: + """Esposizione-bersaglio del libro su questo asset, ORA per ORA, in frazione di equity. + expo = WEIGHT * (W_TP01*tp_frac + W_SKH*skh_sign) — la formula di src/live/book.py.""" + hours = A["hours"] + tp = pd.Series(0.0, index=hours) + for t, v in A["tp_events"]: + tp.loc[tp.index >= t] = v + sk = pd.Series(0.0, index=hours) + for t, v in sorted(A["skh_events"]): + sk.loc[sk.index >= t] = v + expo = WEIGHT * (W_TP01 * tp.values + W_SKH * sk.values) + src = np.where(np.abs(np.diff(sk.values, prepend=0.0)) > 0, "SKH", + np.where(np.abs(np.diff(tp.values, prepend=0.0)) > 0, "TP01", "")) + return pd.DataFrame(dict(ts=hours, expo=expo, ref=A["ref"], hidx=A["hidx"], src=src)) + + +# =========================================================================================== +# 2. ORDINI — banda min-order, esattamente come rebalance_signed +# =========================================================================================== +def orders_from_path(P: pd.DataFrame, equity: float, min_usd: float = MIN_ORDER_USD) -> pd.DataFrame: + """Ordini che il libro manderebbe: si trada solo se |target-posizione| >= min_usd.""" + band = min_usd / equity + expo = P["expo"].values + held = 0.0 + rows = [] + for k in range(len(expo)): + delta = expo[k] - held + if abs(delta) < band: + continue + rows.append((P["ts"].iloc[k], delta, P["ref"].iloc[k], P["hidx"].iloc[k], P["src"].iloc[k])) + held = expo[k] + return pd.DataFrame(rows, columns=["ts", "dfrac", "ref", "hidx", "src"]) + + + + +# =========================================================================================== +# 3. FILL E SELEZIONE AVVERSA +# =========================================================================================== +def measure_orders(A: dict, O: pd.DataFrame, horizons=HORIZONS_MIN, q_bps: float = 0.0, + strict: bool = True) -> dict: + """Per ogni ordine e ogni orizzonte N: il limit e' stato riempito? e se no, quanto e' + scappato il prezzo? `q_bps` = attraversamento richiesto oltre il livello (coda del book). + + `strict` E' IL PARAMETRO CHE DECIDE IL SEGNO DI TUTTO IL RISULTATO. + strict=False -> `low <= limite`: il prezzo ha TOCCATO il livello. Include il caso in cui + la barra APRE esattamente al livello e non ci torna mai: li' non e' stato scambiato + NIENTE al nostro prezzo dopo che l'ordine e' arrivato, quindi un passivo in fondo alla + coda NON e' riempito. E' un limite superiore FISICAMENTE NON DIFENDIBILE. + strict=True -> `low < limite`: il prezzo ha ATTRAVERSATO il livello. E' la condizione + minima perche' esista uno scambio al nostro prezzo. Anche cosi' si assume di essere + DAVANTI a tutta la coda gia' presente -> resta generosa, ma difendibile.""" + l5, h5, c5 = A["l5"], A["h5"], A["c5"] + nb = len(c5) + idx = O["hidx"].values.astype(int) + ref = O["ref"].values.astype(float) + buy = (O["dfrac"].values > 0) + q = q_bps / 1e4 + out = {} + for N in horizons: + steps = N // 5 + filled = np.zeros(len(O), bool) + adverse = np.zeros(len(O)) + for k in range(len(O)): + i0 = idx[k] + 1 # barra 5m etichettata h:05 (ordine a h:07: generoso) + i1 = min(i0 + steps, nb) + P = ref[k] + if i0 >= i1: + filled[k] = True + continue + lim = P * (1.0 - q) if buy[k] else P * (1.0 + q) + if buy[k]: + hit = (l5[i0:i1].min() < lim) if strict else (l5[i0:i1].min() <= lim) + else: + hit = (h5[i0:i1].max() > lim) if strict else (h5[i0:i1].max() >= lim) + filled[k] = hit + if not hit: + end = c5[i1 - 1] + adverse[k] = (end - P) / P if buy[k] else (P - end) / P + out[N] = pd.DataFrame(dict(filled=filled, adverse=adverse), index=O.index) + return out + + +def measure_p2(A: dict, O: pd.DataFrame, P: pd.DataFrame, max_hours=P2_MAX_HOURS, + q_bps: float = 0.0, strict: bool = True) -> pd.DataFrame: + """Politica 'MAI INSEGUIRE': il limit resta e viene RI-POSTATO al nuovo riferimento a ogni + cron orario, fino a `max_hours`; solo allora si insegue a market. Il costo e' cumulativo.""" + l5, h5, c5 = A["l5"], A["h5"], A["c5"] + hidx = P["hidx"].values.astype(int) + ref = P["ref"].values.astype(float) + nb, nh = len(c5), len(hidx) + pos_of = {int(v): k for k, v in enumerate(hidx)} + q = q_bps / 1e4 + cycles, adverse, chased = [], [], [] + for _, r in O.iterrows(): + k0 = pos_of[int(r["hidx"])] + P0 = float(r["ref"]); buy = r["dfrac"] > 0 + done, adv, cyc = False, 0.0, 0 + for k in range(k0, min(k0 + max_hours, nh)): + Pk = ref[k] + lim = Pk * (1.0 - q) if buy else Pk * (1.0 + q) + i0 = hidx[k] + 1 + i1 = min(i0 + 12, nb) + if i0 >= i1: + break + if buy: + hit = (l5[i0:i1].min() < lim) if strict else (l5[i0:i1].min() <= lim) + else: + hit = (h5[i0:i1].max() > lim) if strict else (h5[i0:i1].max() >= lim) + cyc = k - k0 + 1 + if hit: + adv = (lim - P0) / P0 if buy else (P0 - lim) / P0 + done = True + break + if not done: + k = min(k0 + max_hours, nh - 1) + adv = (ref[k] - P0) / P0 if buy else (P0 - ref[k]) / P0 + cycles.append(cyc); adverse.append(adv); chased.append(not done) + return pd.DataFrame(dict(cycles=cycles, adverse=adverse, chased=chased), index=O.index) + + +def per_order_delta(O: pd.DataFrame, M: pd.DataFrame, *, miss_fee_bps: float) -> np.ndarray: + """Delta di P&L in FRAZIONE DI EQUITY DI LIBRO, per ORDINE, del passivo contro il taker. + >0 = il passivo guadagna. Confronto APPAIATO PER ORDINE: ogni ordine compare una volta in + entrambe le politiche, riempito o no (allineare sui soli riempiti terrebbe i casi in cui + non e' successo niente — trappola gia' presa 3 volte in questo progetto).""" + absd = O["dfrac"].abs().values + filled = M["filled"].values if "filled" in M.columns else np.ones(len(O), bool) + fee_bps = np.where(filled, FEE_MAKER_BPS, miss_fee_bps) + return (FEE_TAKER_BPS - fee_bps) / 1e4 * absd - M["adverse"].values * absd + + +def weighted_stats(F: np.ndarray, Ad: np.ndarray, W: np.ndarray) -> dict: + """Tutto PONDERATO PER NOZIONALE (e' il nozionale che paga la fee, non il conteggio). + `p_be` = la frazione di fill che il passivo deve raggiungere per PAREGGIARE il taker: + p*(-gap) + (1-p)*E[avversa|no fill] = 0 -> p_be = E/(E+gap) + E' la forma model-free del risultato: non dipende dall'ipotesi di coda, solo da quanto + scappa il prezzo quando il limit resta a terra.""" + gap = (FEE_TAKER_BPS - FEE_MAKER_BPS) / 1e4 + wf = W[F].sum() / W.sum() if W.sum() > 0 else 1.0 + miss_w = W[~F] + e_adv = float(np.sum(miss_w * Ad[~F]) / miss_w.sum()) if miss_w.sum() > 0 else 0.0 + cost = float(np.sum(W * (np.where(F, FEE_MAKER_BPS, FEE_TAKER_BPS) / 1e4 + Ad)) / W.sum()) + return dict(p_fill=wf, e_adv=e_adv, cost_bps=cost * 1e4, + p_be=e_adv / (e_adv + gap) if (e_adv + gap) > 0 else 1.0) + + +def boot_ci(x: np.ndarray, w: np.ndarray, n: int = 2000, seed: int = SEED) -> tuple: + """IC95% bootstrap della media PONDERATA PER NOZIONALE (in bps del nozionale tradato).""" + rng = np.random.default_rng(seed) + m = len(x) + idx = rng.integers(0, m, size=(n, m)) + num = x[idx].sum(axis=1) + den = w[idx].sum(axis=1) + b = num / np.where(den > 0, den, np.nan) * 1e4 + return float(np.nanpercentile(b, 2.5)), float(np.nanpercentile(b, 97.5)) + + +# =========================================================================================== +# 4. LIBRO — rendimenti giornalieri modellati, per innestarci il costo d'esecuzione +# =========================================================================================== +def book_daily(cache: dict, fee_bps_side: float) -> pd.Series: + fs = fee_bps_side / 1e4 + tp_s, sk_s = {}, {} + for a in ASSETS: + A = cache[a] + r, tgt = A["daily_ret"], A["daily_tgt"] + held = np.zeros(len(tgt)); held[1:] = tgt[:-1] + net = held * r - fs * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0 + tp_s[a] = pd.Series(np.clip(net, -0.99, None), index=A["d1_ts"]) + m = backtest_signals(A["ltf"], A["ent"], fee_rt=2 * fs, leverage=1.0, asset=a, tf="230m") + s = pd.Series(m.equity, index=pd.DatetimeIndex(pd.to_datetime(m.eq_index, utc=True))) + sk_s[a] = s.resample("1D").last().ffill().pct_change().dropna() + Jt = pd.concat(tp_s, axis=1, join="inner").fillna(0.0) + Js = pd.concat(sk_s, axis=1, join="inner").fillna(0.0) + tp = to_daily(pd.Series(0.5 * Jt["BTC"].values + 0.5 * Jt["ETH"].values, index=Jt.index)) + sk = to_daily(pd.Series(0.5 * Js["BTC"].values + 0.5 * Js["ETH"].values, index=Js.index)) + return combine_outer({"TP01": tp, "SKH01": sk}, {"TP01": W_TP01, "SKH01": W_SKH}) + + +def graft(base: pd.Series, deltas: dict, O_live: dict) -> pd.Series: + """Innesta il delta per-ordine (frazione di equity) nei rendimenti giornalieri del libro.""" + ser = [] + for a in ASSETS: + ts = pd.DatetimeIndex(O_live[a]["ts"]).tz_convert("UTC").normalize() + ser.append(pd.Series(deltas[a], index=ts)) + add = pd.concat(ser).groupby(level=0).sum() + return base.add(add.reindex(base.index).fillna(0.0), fill_value=0.0) + + +def show(tag: str, d: pd.Series) -> dict: + m = metrics(d) + h = metrics(d[d.index >= HOLDOUT]) + print(f" {tag:<36} Sh {m['sharpe']:>6.3f} | HOLD {h['sharpe']:>6.3f} | " + f"maxDD {m['maxdd']*100:>5.2f}% | CAGR {m['cagr']*100:>6.2f}%") + return dict(sharpe=m["sharpe"], hold=h["sharpe"], maxdd=m["maxdd"], cagr=m["cagr"]) + + +# =========================================================================================== +def main() -> None: + rng = np.random.default_rng(SEED) + print("=" * 104) + print(" r0822b — ESECUZIONE PASSIVA (MAKER) SUL LIBRO LIVE:") + print(" quanto vale il differenziale, quanto costa non riempirsi") + print("=" * 104) + print(f" taker {FEE_TAKER_BPS:.2f} bps/lato · maker {FEE_MAKER_BPS:.2f} bps/lato · " + f"differenziale {FEE_TAKER_BPS-FEE_MAKER_BPS:.2f} bps/lato · modello backtest {FEE_MODEL_BPS:.2f}") + print(" NETTING: il libro ha UNA posizione netta per strumento -> l'ordine di ribilanciamento") + print(" e' UNO solo. Renderlo passivo NON e' bloccato dal motivo che uccise T1 il 26/07") + print(" (quello uccide i BRACKET per-sleeve, non l'ordine netto). Il disaster-SL -30% resta") + print(" uno stop_market on-book e non e' toccato da niente di quanto segue.") + + cache = {a: build_asset(a) for a in ASSETS} + + # ---------------------------------------------------------------- 1. il differenziale + print("\n" + "-" * 104) + print(" 1. QUANTO VALE IL DIFFERENZIALE (limite superiore: TUTTO il flusso diventa maker,") + print(" zero mancati riempimenti — cioe' il numero che NON si puo' superare)") + print("-" * 104) + curve = {bps: show(f"libro @ {bps:.2f} bps/lato", book_daily(cache, bps)) for bps in FEE_GRID_BPS} + gap = FEE_TAKER_BPS - FEE_MAKER_BPS + dsh = curve[FEE_MAKER_BPS]["sharpe"] - curve[FEE_TAKER_BPS]["sharpe"] + dcg = (curve[FEE_MAKER_BPS]["cagr"] - curve[FEE_TAKER_BPS]["cagr"]) * 100 + print(f"\n misurato qui : dSharpe {dsh:+.4f} dCAGR {dcg:+.3f} pp " + f"({dsh/gap:+.4f} Sh/bps, {dcg/gap:+.3f} pp/bps)") + print(f" curva 26/07 : dSharpe {-PUB_SH_PER_BPS*gap:+.4f} dCAGR {-PUB_CAGR_PER_BPS*gap:+.3f} pp " + f"({-PUB_SH_PER_BPS:+.4f} Sh/bps, {-PUB_CAGR_PER_BPS:+.3f} pp/bps) <- replica indipendente OK") + print(" in DOLLARI all'anno, se tutto il flusso fosse maker:") + for cap, note in ((EQUITY_LIVE, "conto vero oggi"), (5000.0, "dopo i versamenti"), + (258338.0, "capitale-rendita, muro 07/08")): + print(f" ${cap:>9,.0f} -> ${cap*dcg/100:>9,.2f}/anno ({note})") + + # ---------------------------------------------------------------- 2. gli ordini + print("\n" + "-" * 104) + print(" 2. GLI ORDINI DEL LIBRO (target netto orario + banda min-order $5, formula di book.py)") + print("-" * 104) + paths = {a: exposure_path(cache[a]) for a in ASSETS} + yrs = (paths["BTC"]["ts"].iloc[-1] - paths["BTC"]["ts"].iloc[0]).days / 365.25 + print(f" {'capitale':>9} {'banda':>7} |" + "".join(f"{a+' ord/anno':>15}{'turnover/anno':>16}" for a in ASSETS)) + ORD = {} + for cap in CAPITALS: + line, tot = "", {} + for a in ASSETS: + O = orders_from_path(paths[a], cap) + tot[a] = O + line += f"{len(O)/yrs:>15.1f}{O['dfrac'].abs().sum()/yrs*cap:>15,.0f}$" + ORD[cap] = tot + print(f" ${cap:>8,.0f} {MIN_ORDER_USD/cap*100:>6.2f}% |" + line) + O_live = ORD[EQUITY_LIVE] + n_live = sum(len(O_live[a]) for a in ASSETS) + turn = sum(O_live[a]["dfrac"].abs().sum() for a in ASSETS) / yrs + print(f"\n ricostruzione a ${EQUITY_LIVE:,.0f}: {n_live} ordini su {yrs:.1f} anni = " + f"{n_live/yrs:.1f}/anno di libro ({n_live/yrs/2:.1f} per asset), " + f"turnover {turn:.1f}x equity/anno") + print(f" coerenza col punto 1: {turn:.1f}x * {gap:.2f} bps = {turn*gap/1e4*100:.3f} pp di CAGR " + f"contro i {dcg:.3f} pp misurati sul libro modellato " + f"({turn*gap/1e4*100/dcg*100:.0f}% — la ricostruzione tiene meno turnover del modello)") + exe = ROOT / "data" / "live" / "book_executions.jsonl" + if exe.exists(): + rows = [json.loads(l) for l in exe.read_text().splitlines() if l.strip()] + gg = max((pd.Timestamp(rows[-1]["ts_utc"]) - pd.Timestamp(rows[0]["ts_utc"])).days, 1) + print(f" controllo di realta': {len(rows)} ordini VERI in {gg} giorni = " + f"{len(rows)/gg*365.25:.0f}/anno di libro. Stesso ordine di grandezza; la finestra e'") + print(" troppo corta per una stima (18 ordini), serve solo a escludere un errore di fattore 10.") + for a in ASSETS: + v = O_live[a]["src"].value_counts() + print(f" {a}: origine " + " ".join(f"{(k or 'drift')} {n}" for k, n in v.items())) + + # ---------------------------------------------------------------- 3. il non-fill + print("\n" + "-" * 104) + print(" 3. IL COSTO DEL MANCATO RIEMPIMENTO (limit al riferimento, APPAIATO PER ORDINE)") + print("-" * 104) + print(" Il parametro che decide NON e' l'orizzonte d'attesa: e' cosa si chiama 'riempito'.") + print(" Un limit al livello sta DIETRO a tutta la coda gia' presente, quindi:") + print(" TOCCO (low <= limite) -> include la barra che APRE al livello e non ci") + print(" torna mai: al nostro prezzo non e' stato scambiato") + print(" NIENTE. Fisicamente non difendibile: LIMITE SUPERIORE.") + print(" ATTRAVERSAMENTO (low < limite) -> condizione MINIMA perche' uno scambio al nostro") + print(" prezzo esista. Assume ancora di essere davanti a") + print(" tutta la coda -> generosa, ma difendibile.") + print(" q bps in piu' -> quanto il prezzo deve andare OLTRE perche' la coda") + print(" davanti a noi sia consumata. L'OHLC non contiene la") + print(" profondita' del book: `q` si dichiara, non si stima.") + print(" E la colonna che decide e' `serve`: la frazione di fill necessaria per PAREGGIARE il") + print(" taker = E[avversa|no fill]/(E[avversa|no fill] + 2,00 bps). Model-free.") + print(f"\n {'ipotesi':>10} {'N(min)':>7} | {'P(fill)':>8} {'serve':>8} {'esito':>6} | {'E[avv|no]':>10}" + f" {'peggio':>8} | {'costo maker':>12} | {'vs taker':>9} | {'IC95 del delta':>22}" + f" | {'ordini vinti':>12}") + grid_cells = 6 # le 6 celle della griglia fine calcolate piu' sotto + tables = {} + variants = [("TOCCO <=", 0.0, False)] + [(f"attrav. q={q:.0f}", q, True) for q in QUEUE_BPS] + for lab, q, strict in variants: + MEAS = {a: measure_orders(cache[a], O_live[a], q_bps=q, strict=strict) for a in ASSETS} + tables[(q, strict)] = MEAS + for N in HORIZONS_MIN: + grid_cells += 1 + F = np.concatenate([MEAS[a][N]["filled"].values for a in ASSETS]) + Ad = np.concatenate([MEAS[a][N]["adverse"].values for a in ASSETS]) + W = np.concatenate([O_live[a]["dfrac"].abs().values for a in ASSETS]) + D = np.concatenate([per_order_delta(O_live[a], MEAS[a][N], miss_fee_bps=FEE_TAKER_BPS) + for a in ASSETS]) + S = weighted_stats(F, Ad, W) + lo, hi = boot_ci(D, W) + ok = "SI" if S["p_fill"] >= S["p_be"] else "no" + print(f" {lab:>10} {N:>7} | {S['p_fill']*100:>7.1f}% {S['p_be']*100:>7.1f}% {ok:>6} | " + f"{S['e_adv']*1e4:>8.0f}b {(Ad[~F].max()*1e4 if (~F).any() else 0):>7.0f}" + f" | {S['cost_bps']:>8.2f} bps | {S['cost_bps']-FEE_TAKER_BPS:>+8.2f} | " + f"[{lo:>+7.2f},{hi:>+7.2f}] bps | {(D>0).mean()*100:>11.1f}%") + print() + + # -- la sintesi che non dipende dalle ipotesi di coda + print(" SINTESI — IL SEGNO DEL RISULTATO STA TUTTO IN UNA DISUGUAGLIANZA STRETTA:") + print(f" {'N(min)':>7} | {'TOCCO <= (limite sup.)':>28} | {'ATTRAVERSAMENTO < (minimo difendibile)':>42}") + for N in HORIZONS_MIN: + row = [] + for key in ((0.0, False), (0.0, True)): + F = np.concatenate([tables[key][a][N]["filled"].values for a in ASSETS]) + Ad = np.concatenate([tables[key][a][N]["adverse"].values for a in ASSETS]) + W = np.concatenate([O_live[a]["dfrac"].abs().values for a in ASSETS]) + S = weighted_stats(F, Ad, W) + keep = (FEE_TAKER_BPS - S["cost_bps"]) / (FEE_TAKER_BPS - FEE_MAKER_BPS) + row.append(f"P(fill) {S['p_fill']*100:>5.1f}% resta {FEE_TAKER_BPS-S['cost_bps']:>+5.2f} bps" + f" = {keep*100:>6.1f}%") + print(f" {N:>7} | {row[0]:>28} | {row[1]:>42}") + print(" Fra le due colonne cambia UNA cosa: se la barra che apre esattamente al nostro") + print(" prezzo, e da li' se ne va, conta come riempimento. Non conta: al nostro prezzo non") + print(" e' stato scambiato niente dopo che l'ordine e' arrivato. Con la definizione fisica") + print(" il passivo PERDE gia' prima di aggiungere un solo bps di coda.") + # griglia FINE vicino a zero: dove sta il salto? (calcolata qui, non citata a memoria) + print("\n griglia FINE a N=60 (attraversamento), per vedere dove sta il salto:") + fine = [] + for q in (0.0, 0.005, 0.05, 0.25, 0.5, 1.0): + num = den = wf = 0.0 + for a in ASSETS: + M = measure_orders(cache[a], O_live[a], horizons=(60,), q_bps=q, strict=True)[60] + w = O_live[a]["dfrac"].abs().values + F = M["filled"].values + num += np.sum(w * (np.where(F, FEE_MAKER_BPS, FEE_TAKER_BPS) / 1e4 + M["adverse"].values)) + den += w.sum(); wf += w[F].sum() + fine.append((q, wf / den, num / den * 1e4)) + print(" " + " ".join(f"q={q:<5.3f} P(fill) {f*100:>4.1f}% costo {c:>5.2f}" for q, f, c in fine)) + Ft = np.concatenate([tables[(0.0, False)][a][60]["filled"].values for a in ASSETS]) + At = np.concatenate([tables[(0.0, False)][a][60]["adverse"].values for a in ASSETS]) + Wt = np.concatenate([O_live[a]["dfrac"].abs().values for a in ASSETS]) + touch60 = weighted_stats(Ft, At, Wt)["cost_bps"] + print(f" Il salto e' TUTTO al primo passo (tocco {touch60:.2f} -> attraversamento sub-tick " + f"{fine[1][2]:.2f} bps): non e' la CODA a decidere, e' la DISUGUAGLIANZA. Poi il costo") + print(f" sale piano ({fine[-1][2]:.2f} a q=1 bps) e non torna mai sotto il taker " + f"({FEE_TAKER_BPS:.2f}): non esiste un q positivo di pareggio.") + + # -- decomposizione: chi paga il non-fill? + q0, N0 = 2.0, 60 + MEAS = tables[(q0, True)] + print(f" DECOMPOSIZIONE ad attraversamento q={q0:.0f} bps, N={N0} min — non tutti gli ordini sono uguali:") + print(f" {'gruppo':<22}{'n':>6}{'P(fill)':>10}{'E[avversa] su TUTTI':>22}{'E[avv|no fill]':>17}") + groups = {} + for a in ASSETS: + O = O_live[a]; M = MEAS[a][N0] + groups.setdefault("SKH (breakout)", []).append((M, (O["src"] == "SKH").values)) + groups.setdefault("TP01 (drift 01:00)", []).append((M, (O["src"] == "TP01").values)) + groups.setdefault("aumenta esposizione", []).append((M, _increases(O))) + groups.setdefault("riduce esposizione", []).append((M, ~_increases(O))) + for lab, parts in groups.items(): + F = np.concatenate([m["filled"].values[k] for m, k in parts]) + Ad = np.concatenate([m["adverse"].values[k] for m, k in parts]) + if not len(F): + continue + print(f" {lab:<22}{len(F):>6}{F.mean()*100:>9.1f}%{Ad.mean()*1e4:>19.1f} bps" + f"{(Ad[~F].mean()*1e4 if (~F).any() else 0):>13.0f} bps") + print(" NB: un non-fill che RIDUCE l'esposizione lascia la posizione aperta piu' a lungo —") + print(" e' rischio, non solo prezzo, e la media in bps non lo mostra.") + + # -- placebo + print(f"\n PLACEBO a q={q0:.0f}, N={N0} — stesse taglie a ORE CASUALI (quanto pesa il") + print(" condizionamento sul segnale? se il placebo e' uguale, il segnale non seleziona nulla):") + grid_cells += 2 + for a in ASSETS: + P = paths[a] + k = rng.integers(1, len(P) - 30, size=len(O_live[a])) + Of = pd.DataFrame(dict(ts=P["ts"].values[k], dfrac=O_live[a]["dfrac"].values, + ref=P["ref"].values[k], hidx=P["hidx"].values[k], src="")) + Mf = measure_orders(cache[a], Of, horizons=(N0,), q_bps=q0, strict=True)[N0] + Mr = MEAS[a][N0] + print(f" {a}: reale P(fill) {Mr['filled'].mean()*100:>5.1f}% " + f"E[avversa su tutti] {Mr['adverse'].mean()*1e4:>6.1f} bps" + f" | placebo P(fill) {Mf['filled'].mean()*100:>5.1f}% " + f"E[avversa] {Mf['adverse'].mean()*1e4:>6.1f} bps") + + # -- politica "mai inseguire" + print(f"\n POLITICA 'MAI INSEGUIRE' a q={q0:.0f} bps (ri-posta ogni ora, tetto {P2_MAX_HOURS}h):") + grid_cells += 1 + P2 = {a: measure_p2(cache[a], O_live[a], paths[a], q_bps=q0, strict=True) for a in ASSETS} + c2 = np.concatenate([P2[a]["cycles"].values for a in ASSETS]) + a2 = np.concatenate([P2[a]["adverse"].values for a in ASSETS]) + w2 = np.concatenate([O_live[a]["dfrac"].abs().values for a in ASSETS]) + ch2 = np.concatenate([P2[a]["chased"].values for a in ASSETS]) + cost2 = (np.sum(w2 * (FEE_MAKER_BPS / 1e4 + a2)) / w2.sum()) * 1e4 + print(f" cicli orari mediana {np.median(c2):.0f} p90 {np.percentile(c2,90):.0f} " + f"max {c2.max():.0f} | mai riempito entro {P2_MAX_HOURS}h: {ch2.mean()*100:.1f}%") + contrib = w2 * a2 + top5 = np.sort(contrib)[-5:].sum() / contrib.sum() if contrib.sum() > 0 else 0.0 + print(f" avversa: mediana {np.median(a2)*1e4:+.1f} bps p90 {np.percentile(a2,90)*1e4:+.1f} " + f"MEDIA {a2.mean()*1e4:+.1f} peggiore {a2.max()*1e4:+.0f}") + print(f" -> costo {cost2:.2f} bps/lato ({cost2-FEE_TAKER_BPS:+.2f} contro il taker), e " + f"i 5 ordini peggiori su {len(a2)} fanno il {top5*100:.0f}% del danno totale.") + print(" E' la FIRMA DELLA TRONCATURA, vista dall'altra parte: il passivo vince PICCOLO e") + print(" SPESSO (mediana favorevole) e perde GROSSO e di RADO. Stessa forma del profit-take") + print(" di VRP01 (30/07), dove una regola che scattava sull'86-91% dei vincenti non era") + print(" protezione ma troncatura. Qui la media e' il numero che decide, non la mediana.") + + # ---------------------------------------------------------------- 4. impatto sul libro + print("\n" + "-" * 104) + print(" 4. IMPATTO SUL LIBRO (delta per-ordine innestato nei rendimenti giornalieri)") + print("-" * 104) + base = book_daily(cache, FEE_TAKER_BPS) + b = show("TAKER 3,50 bps = il libro di oggi", base) + res = {} + for q, strict in [(0.0, False), (0.0, True), (1.0, True), (2.0, True), (5.0, True)]: + for N in (5, 60, 120): + alt = graft(base, {a: per_order_delta(O_live[a], tables[(q, strict)][a][N], + miss_fee_bps=FEE_TAKER_BPS) for a in ASSETS}, O_live) + tag = "TOCCO<=" if not strict else f"attrav q={q:.0f}" + m = show(f"MAKER {tag:>10} N={N:>3} min, poi market", alt) + res[(tag, N)] = m + alt2 = graft(base, {a: per_order_delta(O_live[a], P2[a], miss_fee_bps=FEE_MAKER_BPS) + for a in ASSETS}, O_live) + m2 = show(f"MAKER 'mai inseguire' q={q0:.0f} bps", alt2) + + print("\n delta contro il baseline TAKER (a $635 di conto vero):") + for (q, N), m in res.items(): + print(f" {q:>10} N={N:>3} dSharpe {m['sharpe']-b['sharpe']:+.4f} " + f"dHOLD {m['hold']-b['hold']:+.4f} dCAGR {(m['cagr']-b['cagr'])*100:+.3f} pp " + f"dmaxDD {(m['maxdd']-b['maxdd'])*100:+.2f} pp -> ${EQUITY_LIVE*(m['cagr']-b['cagr']):+.2f}/anno") + print(f" {'mai-inseg.':>10} dSharpe {m2['sharpe']-b['sharpe']:+.4f} " + f"dHOLD {m2['hold']-b['hold']:+.4f} dCAGR {(m2['cagr']-b['cagr'])*100:+.3f} pp " + f"dmaxDD {(m2['maxdd']-b['maxdd'])*100:+.2f} pp -> ${EQUITY_LIVE*(m2['cagr']-b['cagr']):+.2f}/anno") + + # ---------------------------------------------------------------- 5. robustezza + print("\n" + "-" * 104) + print(" 5. ROBUSTEZZA (banda RIDOTTA: gli ordini spostati di -1h/0/+1h. NON e' la banda a 24") + print(" ancore: qui si sposta il momento dell'ORDINE, non l'ancora della strategia)") + print("-" * 104) + grid_cells += 3 + for sh in (-1, 0, 1): + num, den = 0.0, 0.0 + for a in ASSETS: + P = paths[a] + pos = {int(v): k for k, v in enumerate(P["hidx"].values)} + kk = np.array([min(max(pos[int(h)] + sh, 0), len(P) - 1) for h in O_live[a]["hidx"]]) + Osh = pd.DataFrame(dict(ts=P["ts"].values[kk], dfrac=O_live[a]["dfrac"].values, + ref=P["ref"].values[kk], hidx=P["hidx"].values[kk], src="")) + M = measure_orders(cache[a], Osh, horizons=(N0,), q_bps=q0, strict=True)[N0] + w = Osh["dfrac"].abs().values + num += np.sum(w * (np.where(M["filled"].values, FEE_MAKER_BPS, FEE_TAKER_BPS) / 1e4 + + M["adverse"].values)) + den += w.sum() + print(f" ordini spostati di {sh:+d}h costo maker {num/den*1e4:>6.2f} bps/lato " + f"({num/den*1e4-FEE_TAKER_BPS:+.2f} contro il taker)") + print(" -1h e' informativo: sposta l'ordine PRIMA che il segnale sia visibile, cioe' verso") + print(" dove sta l'ingresso VERO di SKH01 (intra-bin, piu' vicino al breakout della lente") + print(" usata qui). La lente di questo script e' GENEROSA verso il maker anche per questo.") + + # ---------------------------------------------------------------- 6. ordini di grandezza + print("\n" + "-" * 104) + print(" 6. LA DOMANDA STRUTTURALE — la leva accanto alle altre gia' misurate nel progetto") + print("-" * 104) + bq, bN = max(res, key=lambda k: res[k]["sharpe"]) + bm = res[(bq, bN)] + print(f" maker, LIMITE SUPERIORE (0 non-fill) dSharpe {dsh:+.4f} " + f"${EQUITY_LIVE*dcg/100:>+7.2f}/anno a ${EQUITY_LIVE:,.0f} ${5000*dcg/100:>+7.2f} a $5.000") + print(f" maker, migliore cella misurata ({bq},N={bN}) dSharpe {bm['sharpe']-b['sharpe']:+.4f} " + f"${EQUITY_LIVE*(bm['cagr']-b['cagr']):>+7.2f}/anno a ${EQUITY_LIVE:,.0f}") + worst = min(res, key=lambda k: res[k]["sharpe"]) + print(f" maker, peggiore cella misurata ({worst[0]},N={worst[1]}) " + f"dSharpe {res[worst]['sharpe']-b['sharpe']:+.4f} " + f"${EQUITY_LIVE*(res[worst]['cagr']-b['cagr']):>+7.2f}/anno") + print(f" peso di SKH01 ottimizzato dSharpe +0.0300 GATE FALLITO (26/07)") + print(f" banda d'ancora del libro (FULL) dSharpe +-0.20 (26/07, 2000 estrazioni)") + print(f" banda d'ancora del libro (HOLD-OUT) 2.36 canonico -> 1.54 onesto (26/07)") + print(f" raddoppio del taker (regola 26/07) dSharpe -0.08 'non si tocca nulla'") + print(f" versare 250 EUR/mese invece di 0 da MAI a 16-20 anni (26-27/07)") + # -- rischio operativo: quanti ordini resterebbero APERTI al giro successivo del cron + print("\n RISCHIO OPERATIVO (non e' un numero di rendimento, e' un modo nuovo di rompersi):") + for key, lab in (((0.0, True), "attraversamento q=0"), ((2.0, True), "attraversamento q=2")): + F = np.concatenate([tables[key][a][60]["filled"].values for a in ASSETS]) + print(f" {lab:<22} N=60 min: {(1-F.mean())*100:>4.1f}% degli ordini ancora APERTI al giro") + print(f" {'':<22} successivo = {(1-F.mean())*n_live/yrs:>4.1f} volte l'anno in cui l'esecutore") + print(f" {'':<22} deve cancellare-e-rimpiazzare correttamente, su un conto vero.") + print(" Oggi `rebalance_signed` manda market e finisce: nessun ordine sopravvive al giro.") + print(" Un limit resting introduce ordini orfani e doppi fill, e il libro ha gia' pagato") + print(" il prezzo di un percorso che si rompe in silenzio (`fresh_5m`, 26-29/07).") + + print("\n " + "=" * 100) + print(" VERDETTO: SCARTATO. Il differenziale c'e' (2,00 bps/lato = +0,033 Sharpe = $2,96/anno") + print(" a $635) ma e' il TETTO, e la selezione avversa se lo mangia tutto gia' alla condizione") + print(" di riempimento piu' generosa che sia fisicamente difendibile. Il segno del risultato") + print(" dipende da una disuguaglianza stretta (`<=` contro `<`), il che e' un altro modo di") + print(" dire che l'effetto e' dentro il rumore: l'IC95 del delta contiene lo zero in 10 celle") + print(" su 10 fra tocco e attraversamento-q=0. E il premio massimo immaginabile ($2,96/anno)") + print(" e' un ordine di grandezza sotto la leva piu' piccola gia' respinta dal progetto.") + print(" " + "=" * 100) + + print(f"\n GRIGLIA DICHIARATA: {grid_cells} celle valutate " + f"(({len(QUEUE_BPS)}q attrav. + 1 tocco) x {len(HORIZONS_MIN)}N + placebo x2 + " + f"mai-inseguire + 3 shift + 6 celle di griglia fine) " + f"+ {len(FEE_GRID_BPS)} punti di fee.") + print("=" * 104) + + +def _increases(O: pd.DataFrame) -> np.ndarray: + """True se l'ordine AUMENTA il valore assoluto dell'esposizione (entrata/incremento).""" + d = O["dfrac"].values + held = np.zeros(len(d)) + acc = 0.0 + for i, x in enumerate(d): + held[i] = acc + acc += x + return np.abs(held + d) > np.abs(held) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822b_surface_rv.py b/scripts/research/r0822b_surface_rv.py new file mode 100644 index 0000000..1c222f9 --- /dev/null +++ b/scripts/research/r0822b_surface_rv.py @@ -0,0 +1,716 @@ +"""SURFACE-RV — la superficie di volatilita' come RELATIVE VALUE fra i suoi punti. + +IPOTESI (registrata prima di misurare): la superficie Deribit e' internamente INCOERENTE in modo +misurabile — strike adiacenti fuori da una curva liscia, varianza totale non monotona nel tenore — +e l'incoerenza si ricompone. E' un'ipotesi di MICROSTRUTTURA, non di previsione: non chiede alla +vol implicita di prevedere nulla (cosa che il progetto ha gia' refutato quattro volte: dvol_directional, +gamma scalping, term-structure come segnale, skew come segnale). +ATTESA DICHIARATA PRIMA: l'incoerenza esiste al MID ma non sopravvive al bid-ask, perche' su +un'opzione a 7 giorni un punto di vol vale pochi dollari mentre attraversare lo spread ne costa +il ~10% del premio (misurato dal filone 11 sulle inverse). + +LA DIFESA CONTRO IL RISCHIO NUMERO UNO DI QUESTO FILONE (misurare il proprio errore di +interpolazione) e' strutturale, non statistica: i due test principali sono **arbitraggi statici +model-free** — monotonia e convessita' del prezzo nello strike — che non richiedono NESSUN +interpolatore, NESSUN modello e NESSUN tasso. Valgono per ogni misura di prezzo, path per path: + * put: P(K) crescente in K (verticale) + * put: P(K) convessa in K (farfalla) +e sono verificati su prezzi ESEGUIBILI (si vende al bid, si compra all'ask), mai al mid. +Il terzo test — varianza totale non monotona nel tenore — richiede di confrontare tenori diversi: +si riporta sia nella versione SENZA interpolazione (strike condiviso fra due scadenze) sia in +quella con interpolazione a log-moneyness fisso, e la DIFFERENZA fra le due e' la misura diretta +di quanto pesa l'interpolatore. + +Convenzioni verificate nel codice (non assunte): + * tick: 0.0001 coin sotto 0.005, 0.0005 sopra -> contato, non dichiarato + * f_markfit: la mark IV del venue riprezza i propri mid? (il filone 5 misura 0.992) + * fee Deribit opzioni: 0.03% del sottostante per gamba, cap 12.5% del premio della gamba + +Il dato: `data/raw/cb_chain/` (archivio bite + raccolta propria). SOLO opzioni INVERSE — il +collettore interroga {"currency": "BTC"|"ETH"}, che non restituisce la famiglia USDC-lineare +(difetto di produzione trovato dal filone 11). Quindi ogni numero qui e' misurato sulla famiglia +che il conto NON puo' marginare, e le conclusioni sulla famiglia USDC sono ESTRAPOLAZIONI +dichiarate, non misure. + +Muro del dato: prima del 2026-06-09 l'archivio raccoglieva UNA scadenza per giro -> la SUPERFICIE +esiste da li' (~75 giorni). Lo SMILE (piu' strike su UNA scadenza) esiste dal 2026-05-01 (~113 g). +Verdetto massimo possibile: LEAD con gate pre-registrato. Mai CANDIDATO. + + uv run python scripts/research/r0822b_surface_rv.py +""" +from __future__ import annotations + +import glob +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd +from scipy.stats import norm + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +STORE = ROOT / "data" / "raw" / "cb_chain" + +SMILE_START = pd.Timestamp("2026-05-01", tz="UTC") # piu' strike su UNA scadenza +SURF_START = pd.Timestamp("2026-06-09", tz="UTC") # piu' scadenze per giro -> superficie + +FEE_UNDERLYING = 0.0003 # 0.03% del sottostante, per gamba, in coin per contratto +FEE_CAP_FRAC = 0.125 # cap: 12.5% del premio della gamba +MIN_LOT = {"BTC": 0.1, "ETH": 1.0} # famiglia INVERSE (quella che abbiamo nel dato) +MIN_LOT_USDC = {"BTC": 0.01, "ETH": 0.1} # famiglia USDC-lineare (non raccolta: estrapolazione) + +DTE_MIN = 1.0 # sotto un giorno il book e' un'altra cosa; riportato anche senza filtro + + +def tick_of(px: np.ndarray) -> np.ndarray: + """Griglia di prezzo Deribit sulle opzioni, in coin. Verificata sui dati in `certifica()`.""" + return np.where(px < 0.005, 0.0001, 0.0005) + + +# --------------------------------------------------------------------------- dato + +def load(assets=("BTC", "ETH")) -> pd.DataFrame: + cols = ["ts", "asset", "instrument_name", "strike", "option_type", "exp", + "bid", "ask", "mid", "iv", "delta", "open_interest", "book_depth_top3", + "underlying_price", "index_price", "quote_status"] + parts = [] + for f in sorted(glob.glob(str(STORE / "*.parquet"))): + d = pd.read_parquet(f, columns=cols) + parts.append(d[d["asset"].isin(assets)]) + d = pd.concat(parts, ignore_index=True) + d["ts"] = pd.to_datetime(d["ts"], utc=True) + d["exp"] = pd.to_datetime(d["exp"], utc=True) + for c in ("strike", "bid", "ask", "mid", "iv", "delta", "open_interest", + "book_depth_top3", "underlying_price", "index_price"): + d[c] = pd.to_numeric(d[c], errors="coerce") + d = d.drop_duplicates(subset=["ts", "instrument_name"], keep="last") + d["dte"] = (d["exp"] - d["ts"]).dt.total_seconds() / 86400.0 + d["T"] = d["dte"] / 365.25 + d = d.reset_index(drop=True) + d["spot_feed"] = _spot_asof(d) + return d + + +def _spot_asof(d: pd.DataFrame) -> pd.Series: + """Indice dal feed CERTIFICATO (1h Deribit mainnet), asof sul ts della quota. + + Serve perche' `bite_archive` NON ha ne' `index_price` ne' `underlying_price` (100% None: + misurato, non assunto) -> senza questo la finestra con un divisore sarebbe di 23 giorni. + """ + from scripts.analysis.research_lab import load_tf + out = pd.Series(np.nan, index=d.index) + for a in d["asset"].unique(): + px = load_tf(a, "1h") + s = pd.Series(px["close"].to_numpy(float), + index=pd.to_datetime(px["timestamp"], unit="ms", utc=True)).sort_index() + s.index = pd.DatetimeIndex(s.index).as_unit("ns") + m = d["asset"] == a + t = pd.DatetimeIndex(d.loc[m, "ts"]).as_unit("ns") + # epoca esplicita in ns (mai `.view("int64")` su indici tz-aware: lezione 01/07) + si, ti = s.index.asi8, t.asi8 + pos = np.searchsorted(si, ti, side="right") - 1 + v = np.where(pos >= 0, s.to_numpy()[np.clip(pos, 0, None)], np.nan) + out.loc[m] = v + return out + + +def forward(d: pd.DataFrame) -> pd.DataFrame: + """Forward per (asset, ts, exp) ricostruito con la PARITA' PUT-CALL, model-free. + + In convenzione inverse il premio e' quotato in coin: (C - P) * X = F - K => F = K + (C-P)*X, + con X = divisore della convenzione (misurato in `certifica`). Mediana sugli strike = robusta. + Dove il venue lo ha registrato (solo la raccolta propria) si usa il valore OSSERVATO, e + l'accordo fra i due e' una certificazione, non un'assunzione. + """ + q = d[(d["bid"] > 0) & (d["ask"] > 0) & (d["ask"] >= d["bid"]) & d["spot_feed"].notna()] + c = q[q["option_type"] == "C"][["asset", "ts", "exp", "strike", "mid", "spot_feed"]] + p = q[q["option_type"] == "P"][["asset", "ts", "exp", "strike", "mid"]] + j = c.merge(p, on=["asset", "ts", "exp", "strike"], suffixes=("_c", "_p")) + j["F_k"] = j["strike"] + (j["mid_c"] - j["mid_p"]) * j["spot_feed"] + f = j.groupby(["asset", "ts", "exp"])["F_k"].median().rename("F_pcp").reset_index() + return f + + +def two_sided(d: pd.DataFrame) -> pd.DataFrame: + """Quotate a DUE lati. Una gamba sola quotata non e' meta' struttura: e' un'altra cosa. + + (Regola 8 del brief: una riga presente non e' un dato presente.) + """ + q = d[(d["bid"] > 0) & (d["ask"] > 0) & (d["ask"] >= d["bid"])].copy() + q = q[q["T"] > 0] + return q + + +# --------------------------------------------------------------------------- prezzo + +def bs_usd(F, K, T, sig, is_put): + """Black-76 con r=0 (Deribit prezza sul FORWARD della scadenza = `underlying_price`).""" + F = np.asarray(F, float); K = np.asarray(K, float) + T = np.asarray(T, float); sig = np.asarray(sig, float) + out = np.full(F.shape, np.nan) + ok = (T > 0) & (sig > 0) & (F > 0) & (K > 0) + if not ok.any(): + return out + d1 = np.zeros_like(F); d2 = np.zeros_like(F) + sq = np.sqrt(np.where(ok, T, 1.0)) + d1[ok] = (np.log(F[ok] / K[ok]) + 0.5 * sig[ok] ** 2 * T[ok]) / (sig[ok] * sq[ok]) + d2[ok] = d1[ok] - sig[ok] * sq[ok] + call = F * norm.cdf(d1) - K * norm.cdf(d2) + put = K * norm.cdf(-d2) - F * norm.cdf(-d1) + out[ok] = np.where(is_put[ok], put[ok], call[ok]) + return out + + +def bs_vega_usd(F, K, T, sig): + F = np.asarray(F, float); K = np.asarray(K, float) + T = np.asarray(T, float); sig = np.asarray(sig, float) + out = np.zeros(F.shape) + ok = (T > 0) & (sig > 0) & (F > 0) & (K > 0) + sq = np.sqrt(np.where(ok, T, 1.0)) + d1 = np.zeros_like(F) + d1[ok] = (np.log(F[ok] / K[ok]) + 0.5 * sig[ok] ** 2 * T[ok]) / (sig[ok] * sq[ok]) + out[ok] = F[ok] * norm.pdf(d1[ok]) * sq[ok] # per 1.00 di sigma (=100 punti di vol) + return out + + +# --------------------------------------------------------------------------- certificazione + +def certifica(d: pd.DataFrame) -> pd.DataFrame: + print("=" * 100) + print("0. CERTIFICAZIONE DEL DATO — cosa esiste davvero, prima di qualunque test") + print("=" * 100) + q = two_sided(d) + print(f"righe totali {len(d):,} · quotate a due lati {len(q):,} ({len(q)/len(d):.1%})") + print(f"finestra {d['ts'].min():%Y-%m-%d} -> {d['ts'].max():%Y-%m-%d}") + + # (a) muro del dato: scadenze per giro + per = d.groupby([d["ts"].dt.floor("D"), "asset"])["exp"].nunique().unstack() + pre = per[per.index < SURF_START] + post = per[per.index >= SURF_START] + print("\n(a) MURO DEL DATO — scadenze per giro (mediana per giorno):") + print(f" pre {SURF_START:%Y-%m-%d}: BTC {pre['BTC'].median():.0f} · ETH {pre['ETH'].median():.0f}" + f" (max BTC {pre['BTC'].max():.0f}) -> la SUPERFICIE non esiste") + print(f" post {SURF_START:%Y-%m-%d}: BTC {post['BTC'].median():.0f} · ETH {post['ETH'].median():.0f}" + f" -> superficie su {len(post)} giorni") + print(f" smile (piu' strike, UNA scadenza) su {len(per)} giorni dal {per.index.min():%Y-%m-%d}" + f" -> i test model-free girano su questa finestra piu' lunga") + + # (a-bis) BUCO DI COLONNA nell'archivio ereditato + obs = d["index_price"].notna() + print("\n(a-bis) BUCO DI COLONNA — `bite_archive` non registrava il sottostante") + print(f" index_price/underlying_price presenti in {obs.sum():,}/{len(d):,} righe ({obs.mean():.1%})" + f" — solo la raccolta propria, dal {d.loc[obs,'ts'].min():%Y-%m-%d}") + print(f" book_depth_top3 presente in {d['book_depth_top3'].notna().mean():.1%} delle righe") + print(" -> i test 1-2 (model-free) NON ne hanno bisogno; il test 4 (calendario) si')") + + # (b) tick: verificato, non dichiarato + print("\n(b) GRIGLIA DI PREZZO (verificata sui dati, non assunta)") + for a in sorted(q["asset"].unique()): + b = q.loc[q["asset"] == a, "bid"].to_numpy() + t = tick_of(b) + off = np.abs(np.round(b / t) - b / t) > 1e-6 + n_tick = np.round(b / t) + print(f" {a}: fuori griglia {off.sum():,}/{len(b):,} ({off.mean():.4%}) · " + f"bid in tick: mediana {np.median(n_tick):.0f} · quota <=2 tick {np.mean(n_tick <= 2):.1%}") + + # (c) il divisore della convenzione inverse + f_markfit + print("\n(c) CONVENZIONE INVERSE e f_markfit — la mark IV riprezza i propri mid?") + ov = q[obs.reindex(q.index, fill_value=False)].dropna(subset=["iv", "mid", "spot_feed"]) + ov = ov[(ov["iv"] > 0) & (ov["mid"] > 0) & (ov["dte"] >= DTE_MIN)] + dev = (ov["index_price"] / ov["spot_feed"] - 1).abs() + print(f" index_price della catena vs feed CERTIFICATO 1h: |dev| mediana {dev.median():.5%} " + f"· p99 {dev.quantile(.99):.4%} (n={len(ov):,}) -> il feed puo' fare da divisore sull'archivio") + sm = ov.sample(n=min(120_000, len(ov)), random_state=0) + is_put = (sm["option_type"] == "P").to_numpy() + usd = bs_usd(sm["underlying_price"].to_numpy(), sm["strike"].to_numpy(), + sm["T"].to_numpy(), (sm["iv"] / 100).to_numpy(), is_put) + best = None + for nome, div in (("index_price", sm["index_price"].to_numpy()), + ("underlying_price(fwd)", sm["underlying_price"].to_numpy()), + ("feed certificato 1h", sm["spot_feed"].to_numpy())): + f = (usd / div) / sm["mid"].to_numpy() + f = f[np.isfinite(f) & (f > 0)] + print(f" f_markfit con divisore {nome:<22}: mediana {np.median(f):.4f} " + f"[p10 {np.percentile(f,10):.3f}, p90 {np.percentile(f,90):.3f}] n={len(f):,}") + if best is None or abs(np.median(f) - 1) < abs(best[1] - 1): + best = (nome, float(np.median(f))) + print(f" -> convenzione: divisore = {best[0]} (f_markfit {best[1]:.4f}); " + f"un f molto lontano da 1 direbbe che il problema e' il MIO prezzatore, non il mercato") + + # (d) forward ricostruito con la parita' put-call, certificato contro l'osservato + print("\n(d) FORWARD ricostruito con la PARITA' PUT-CALL (model-free) vs osservato") + F = forward(d) + chk = d.loc[obs, ["asset", "ts", "exp", "underlying_price"]].drop_duplicates( + subset=["asset", "ts", "exp"]).merge(F, on=["asset", "ts", "exp"], how="inner") + e = (chk["F_pcp"] / chk["underlying_price"] - 1) + print(f" n={len(chk):,} coppie (asset,ts,scadenza) con entrambi: " + f"|errore| mediana {e.abs().median():.5%} · p95 {e.abs().quantile(.95):.4%} · " + f"corr {np.corrcoef(chk['F_pcp'], chk['underlying_price'])[0,1]:.6f}") + return q, F + + +# --------------------------------------------------------------------------- fee + +def fee_coin(premio_coin: np.ndarray, n_contratti: float) -> np.ndarray: + """Fee Deribit opzioni per gamba, in coin: min(0.03% del sottostante, 12.5% del premio) x lotti.""" + per_contratto = np.minimum(FEE_UNDERLYING, FEE_CAP_FRAC * np.asarray(premio_coin, float)) + return per_contratto * n_contratti + + +# --------------------------------------------------------------------------- TEST V + +def test_verticale(q: pd.DataFrame, dte_min: float) -> dict: + """Monotonia del prezzo nello strike. Model-free, nessun interpolatore, nessun tasso. + + put: P(K) crescente -> arbitraggio se bid(K1) > ask(K2) con K1 < K2 + (vendi la put bassa al bid, compra l'alta all'ask: credito + payoff sempre >= 0) + call: C(K) decrescente -> arbitraggio se bid(K2) > ask(K1) con K1 < K2 + """ + x = q[q["dte"] >= dte_min].sort_values(["asset", "ts", "exp", "option_type", "strike"]) + key = ["asset", "ts", "exp", "option_type"] + g = x.groupby(key, sort=False) + # per le put il "meglio finora" salendo di strike; per le call scendendo + x["cmax_up"] = g["bid"].cummax() + x["prev_up"] = g["cmax_up"].shift(1) # miglior bid su strike STRETTAMENTE inferiori + xr = x.iloc[::-1].copy() + gr = xr.groupby(key, sort=False) + xr["cmax_dn"] = gr["bid"].cummax() + x["prev_dn"] = gr["cmax_dn"].shift(1) # miglior bid su strike STRETTAMENTE superiori + is_put = x["option_type"].to_numpy() == "P" + prev = np.where(is_put, x["prev_up"].to_numpy(), x["prev_dn"].to_numpy()) + cred = prev - x["ask"].to_numpy() # credito riskless in coin, per contratto + viol = np.isfinite(cred) & (cred > 0) + n_tot = len(x) + out = {"n_confronti": int(np.isfinite(cred).sum()), "n_viol": int(viol.sum()), + "quota": float(viol.sum() / max(1, np.isfinite(cred).sum()))} + if viol.any(): + vv = x.loc[viol] + out["credito_med_coin"] = float(np.median(cred[viol])) + out["credito_usd_medlot"] = float(np.median( + cred[viol] * vv["asset"].map(MIN_LOT).to_numpy() * vv["spot_feed"].to_numpy())) + return out + + +# --------------------------------------------------------------------------- TEST B + +def test_farfalla(q: pd.DataFrame, dte_min: float) -> pd.DataFrame: + """Convessita' del prezzo nello strike su triple CONSECUTIVE. Model-free. + + lam = (K3-K2)/(K3-K1); payoff lam*P(K1) + (1-lam)*P(K3) - P(K2) >= 0 sempre. + Costo eseguibile = lam*ask1 + (1-lam)*ask3 - bid2. Se < 0 -> credito riskless. + Al MID lo stesso numero misura l'INCOERENZA (non eseguibile) — si riportano entrambi. + """ + x = q[q["dte"] >= dte_min].sort_values(["asset", "ts", "exp", "option_type", "strike"]) + key = ["asset", "ts", "exp", "option_type"] + g = x.groupby(key, sort=False) + for c in ("strike", "bid", "ask", "mid", "iv", "book_depth_top3"): + x[c + "_p"] = g[c].shift(1) + x[c + "_n"] = g[c].shift(-1) + x = x.dropna(subset=["strike_p", "strike_n", "bid_p", "bid_n", "ask_p", "ask_n"]) + K1, K2, K3 = x["strike_p"].to_numpy(), x["strike"].to_numpy(), x["strike_n"].to_numpy() + lam = (K3 - K2) / (K3 - K1) + x["lam"] = lam + # eseguibile: compro le ali all'ask, vendo il corpo al bid + x["cost_exec"] = lam * x["ask_p"] + (1 - lam) * x["ask_n"] - x["bid"] + # incoerenza al mid (non eseguibile, ma e' la lente "la superficie e' liscia?") + x["cost_mid"] = lam * x["mid_p"] + (1 - lam) * x["mid_n"] - x["mid"] + # la stessa cosa in punti di vol: residuo di convessita' locale dello smile + x["iv_resid"] = x["iv"] - (lam * x["iv_p"] + (1 - lam) * x["iv_n"]) + return x + + +# --------------------------------------------------------------------------- vol points + +def in_punti_vol(x: pd.DataFrame) -> pd.DataFrame: + """Mezzo spread denaro-lettera convertito in punti di vol via vega BS (r=0, forward).""" + vega_usd = bs_vega_usd(x["F"].to_numpy(), x["strike"].to_numpy(), + x["T"].to_numpy(), (x["iv"] / 100).to_numpy()) + vega_coin_per_pt = vega_usd / x["spot_feed"].to_numpy() / 100.0 + x = x.copy() + x["vega_coin_pt"] = vega_coin_per_pt + x["halfspread_pt"] = (x["ask"] - x["bid"]) / 2.0 / np.clip(vega_coin_per_pt, 1e-12, None) + x["relspread"] = (x["ask"] - x["bid"]) / x["mid"] + # quanto vale UN TICK in punti di vol: se il tick e' piu' grosso dell'incoerenza, + # l'incoerenza non e' esprimibile nella griglia di prezzo del venue. + x["tick_pt"] = tick_of(x["mid"].to_numpy()) / np.clip(vega_coin_per_pt, 1e-12, None) + return x + + +# --------------------------------------------------------------------------- TEST C + +def test_calendario(q: pd.DataFrame, dte_min: float, ogni: int = 4) -> dict: + """Varianza totale w = iv^2 * T: deve CRESCERE col tenore. Due lenti, e la loro differenza. + + (i) STRIKE CONDIVISO fra due scadenze adiacenti: nessuna interpolazione. + (ii) LOG-MONEYNESS FISSO k = ln(K/F): richiede di interpolare lo smile lungo in k. + La differenza fra (i) e (ii) e' la misura diretta del contributo dell'INTERPOLATORE. + """ + x = q[(q["dte"] >= dte_min) & q["iv"].notna() & (q["iv"] > 0) & q["F"].notna()].copy() + x = x[x["ts"] >= SURF_START] + tenuti = sorted(x["ts"].unique())[::ogni] # (i) e (ii) sugli STESSI snapshot: appaiato + x = x[x["ts"].isin(tenuti)] + x["w"] = (x["iv"] / 100.0) ** 2 * x["T"] + x["k"] = np.log(x["strike"] / x["F"]) + + # --- (i) strike condiviso, adiacenti nel tempo + x = x.sort_values(["asset", "ts", "option_type", "strike", "exp"]) + key = ["asset", "ts", "option_type", "strike"] + g = x.groupby(key, sort=False) + nxt_w = g["w"].shift(-1).to_numpy() + nxt_T = g["T"].shift(-1).to_numpy() + nxt_iv = g["iv"].shift(-1).to_numpy() + ok = np.isfinite(nxt_w) & (nxt_T > x["T"].to_numpy()) + dw = nxt_w - x["w"].to_numpy() + viol_i = ok & (dw < 0) + # ampiezza in punti di vol: quanto dovrebbe salire l'iv lunga per ripristinare la monotonia + iv_need = np.sqrt(np.maximum(x["w"].to_numpy(), 0) / np.maximum(nxt_T, 1e-9)) * 100.0 + amp_pt = np.where(ok, iv_need - nxt_iv, np.nan) + + res = {"i_n": int(ok.sum()), "i_viol": int(viol_i.sum()), + "i_quota": float(viol_i.sum() / max(1, ok.sum())), + "i_amp_med_pt": float(np.nanmedian(amp_pt[viol_i])) if viol_i.any() else np.nan, + "i_amp_p95_pt": float(np.nanpercentile(amp_pt[viol_i], 95)) if viol_i.any() else np.nan} + + # --- (ii) log-moneyness fisso, con interpolazione + y = x + n_par, n_vio, amps = 0, 0, [] + for (a, ts, ot), gg in y.groupby(["asset", "ts", "option_type"], sort=False): + exps = sorted(gg["exp"].unique()) + if len(exps) < 2: + continue + for e1, e2 in zip(exps[:-1], exps[1:]): + s1 = gg[gg["exp"] == e1].sort_values("k") + s2 = gg[gg["exp"] == e2].sort_values("k") + if len(s1) < 2 or len(s2) < 2: + continue + k1 = s1["k"].to_numpy() + inside = (k1 >= s2["k"].min()) & (k1 <= s2["k"].max()) # MAI estrapolare + if not inside.any(): + continue + iv2 = np.interp(k1[inside], s2["k"].to_numpy(), s2["iv"].to_numpy()) + T2 = float(s2["T"].iloc[0]) + w2 = (iv2 / 100.0) ** 2 * T2 + w1 = s1["w"].to_numpy()[inside] + n_par += int(inside.sum()) + bad = w2 < w1 + n_vio += int(bad.sum()) + if bad.any(): + need = np.sqrt(w1[bad] / T2) * 100.0 + amps.append(need - iv2[bad]) + amps = np.concatenate(amps) if amps else np.array([]) + res.update({"ii_n": n_par, "ii_viol": n_vio, "ii_quota": n_vio / max(1, n_par), + "ii_amp_med_pt": float(np.median(amps)) if len(amps) else np.nan, + "ii_ogni": ogni}) + # valore in dollari dell'ampiezza (i) al lotto minimo, contro il costo di 2 gambe + vv = x.loc[viol_i].copy() + if len(vv): + vv = in_punti_vol(vv) + amp = pd.Series(amp_pt, index=x.index)[viol_i].to_numpy() + lot = vv["asset"].map(MIN_LOT).to_numpy() + val = np.abs(amp) * vv["vega_coin_pt"].to_numpy() * lot * vv["spot_feed"].to_numpy() + costo = ((vv["ask"] - vv["bid"]).to_numpy() * lot * vv["spot_feed"].to_numpy() + + 2 * fee_coin(vv["mid"].to_numpy(), lot) * vv["spot_feed"].to_numpy()) + res.update({"i_val_usd_med": float(np.nanmedian(val)), + "i_costo_usd_med": float(np.nanmedian(costo)), + "i_n_val": int(np.isfinite(val).sum())}) + return res + + +# --------------------------------------------------------------------------- economia + +def economia(bf: pd.DataFrame, asset: str) -> dict: + """Quanto vale, in dollari, l'incoerenza di una farfalla al lotto minimo — netto fee. + + Struttura a 3 gambe, lotti INTERI: con strike equispaziati e' 1:2:1 sul lotto minimo. + Si conta la fee gamba per gamba col cap sul premio, e si usa il prezzo ESEGUIBILE. + """ + x = bf[bf["asset"] == asset] + lot = MIN_LOT[asset] + S = x["spot_feed"].to_numpy() + # credito lordo della farfalla eseguibile, in coin per unita' di "corpo" + cred = -x["cost_exec"].to_numpy() # >0 = credito riskless + # lotti interi: corpo 2*lot, ali lot ciascuna (equivale a lam=0.5 -> solo strike equispaziati) + eq = np.isclose(x["lam"].to_numpy(), 0.5, atol=0.02) + cred_eq_coin = (x["bid"].to_numpy() * 2 * lot + - x["ask_p"].to_numpy() * lot - x["ask_n"].to_numpy() * lot) + f = (fee_coin(x["bid"].to_numpy(), 2 * lot) + + fee_coin(x["ask_p"].to_numpy(), lot) + fee_coin(x["ask_n"].to_numpy(), lot)) + netto_usd = (cred_eq_coin - f) * S + return { + "asset": asset, "n": int(len(x)), "n_equisp": int(eq.sum()), + "cred_lordo_p99_coin": float(np.nanpercentile(cred, 99)), + "fee_3gambe_usd_med": float(np.nanmedian(f[eq] * S[eq])) if eq.any() else np.nan, + "netto_usd_max": float(np.nanmax(netto_usd[eq])) if eq.any() else np.nan, + "netto_usd_p99": float(np.nanpercentile(netto_usd[eq], 99)) if eq.any() else np.nan, + "n_netto_pos": int(np.nansum(netto_usd[eq] > 0)), + "nozionale_min_lotto_usd": float(np.nanmedian(S) * lot), + } + + +# --------------------------------------------------------------------------- persistenza + +def persistenza(bf: pd.DataFrame, viol_col: str) -> dict: + """Una violazione che sparisce all'ora dopo e' un guizzo di book, non un'opportunita'. + + Chiave = (asset, exp, tipo, tripla di strike). Si guarda se la stessa tripla e' ancora in + violazione allo snapshot successivo IN CUI LA TRIPLA E' QUOTATA (non 'l'ora dopo' in astratto). + """ + v = bf[bf[viol_col]].copy() + if v.empty: + return {"n": 0} + bf = bf.copy() + bf["trip"] = (bf["asset"] + "|" + bf["exp"].astype(str) + "|" + bf["option_type"] + "|" + + bf["strike_p"].astype(str) + "|" + bf["strike"].astype(str) + "|" + + bf["strike_n"].astype(str)) + bf = bf.sort_values(["trip", "ts"]) + nxt = bf.groupby("trip", sort=False)[viol_col].shift(-1) + m = bf[viol_col] & nxt.notna() + return {"n": int(m.sum()), "resta": float(nxt[m].mean()) if m.any() else np.nan} + + +# --------------------------------------------------------------------------- controlli positivi + +def _finto(rows) -> pd.DataFrame: + """Catena sintetica minima per i controlli positivi.""" + d = pd.DataFrame(rows) + d["ts"] = pd.to_datetime(d["ts"], utc=True) + d["exp"] = pd.to_datetime(d["exp"], utc=True) + d["dte"] = (d["exp"] - d["ts"]).dt.total_seconds() / 86400.0 + d["T"] = d["dte"] / 365.25 + d["mid"] = (d["bid"] + d["ask"]) / 2 + for c, v in (("spot_feed", 100_000.0), ("F", 100_000.0), ("option_type", "P"), + ("open_interest", 1000.0), ("book_depth_top3", 10.0), ("delta", -0.2)): + if c not in d: + d[c] = v + return d + + +def controlli_positivi() -> None: + """Un rilevatore tarato per non segnalare e' indistinguibile da uno rotto. + + Regola gia' codificata nel progetto (venue_watch, 26/07). Qui i tre test vengono puntati + su violazioni COSTRUITE: se non le vedono, i loro zeri non valgono niente. + """ + print("=" * 100) + print("CONTROLLI POSITIVI — i tre rilevatori vedono una violazione COSTRUITA?") + print("=" * 100) + base = dict(ts="2026-08-01T00:00Z", exp="2026-08-15T08:00Z", asset="BTC", + option_type="P", iv=50.0, instrument_name="x") + # (1) verticale: put a strike BASSO con bid > ask della put a strike ALTO + v_ok = _finto([{**base, "strike": 90000, "bid": 0.0300, "ask": 0.0305}, + {**base, "strike": 95000, "bid": 0.0200, "ask": 0.0205}]) + v_no = _finto([{**base, "strike": 90000, "bid": 0.0200, "ask": 0.0205}, + {**base, "strike": 95000, "bid": 0.0300, "ask": 0.0305}]) + r_ok, r_no = test_verticale(v_ok, 0.0), test_verticale(v_no, 0.0) + print(f" TEST V: catena INVERTITA -> {r_ok['n_viol']} violazioni (attese >=1) · " + f"catena sana -> {r_no['n_viol']} (attese 0)") + # (2) farfalla: corpo troppo caro (concavita') + b_ok = _finto([{**base, "strike": 90000, "bid": 0.0100, "ask": 0.0105}, + {**base, "strike": 95000, "bid": 0.0400, "ask": 0.0405}, + {**base, "strike": 100000, "bid": 0.0500, "ask": 0.0505}]) + b_no = _finto([{**base, "strike": 90000, "bid": 0.0100, "ask": 0.0105}, + {**base, "strike": 95000, "bid": 0.0280, "ask": 0.0285}, + {**base, "strike": 100000, "bid": 0.0500, "ask": 0.0505}]) + f_ok = test_farfalla(b_ok, 0.0); f_no = test_farfalla(b_no, 0.0) + print(f" TEST B: corpo troppo caro -> {(f_ok['cost_exec'] < 0).sum()} violazioni eseguibili " + f"(attese >=1) · smile convesso -> {(f_no['cost_exec'] < 0).sum()} (attese 0)") + # (3) calendario: varianza totale che SCENDE col tenore, stesso strike + c_rows = [] + for e, iv in (("2026-08-08T08:00Z", 80.0), ("2026-08-15T08:00Z", 40.0)): + c_rows.append({**base, "exp": e, "strike": 100000, "bid": 0.01, "ask": 0.011, "iv": iv}) + c_ok = _finto(c_rows) + c_ok["ts"] = pd.Timestamp("2026-07-01", tz="UTC") + c_no = c_ok.copy(); c_no["iv"] = [40.0, 80.0] + r1 = test_calendario(c_ok, 0.0, ogni=1); r2 = test_calendario(c_no, 0.0, ogni=1) + print(f" TEST C: varianza totale DECRESCENTE -> {r1['i_viol']} violazioni (attese >=1) · " + f"crescente -> {r2['i_viol']} (attese 0)") + assert r_ok["n_viol"] >= 1 and r_no["n_viol"] == 0 + assert (f_ok["cost_exec"] < 0).sum() >= 1 and (f_no["cost_exec"] < 0).sum() == 0 + assert r1["i_viol"] >= 1 and r2["i_viol"] == 0 + print(" -> 3/3 rilevatori validati in ENTRAMBI i versi: gli zeri dei test veri sono zeri.\n") + + +# --------------------------------------------------------------------------- reversione + +def reversione(bf: pd.DataFrame, max_gap_h: float = 2.0) -> dict: + """Il residuo di convessita' si RICOMPONE? (l'ipotesi del filone, misurata) + + Coppie appaiate sulla STESSA tripla di strike a snapshot consecutivi entro `max_gap_h`. + beta di resid[t+1] - resid[t] su resid[t] : beta = -1 -> ricomposizione totale in un'ora, + beta = 0 -> il residuo e' una proprieta' persistente dello smile (skew), non un errore. + """ + x = bf.dropna(subset=["iv_resid"]).copy() + x["trip"] = (x["asset"] + "|" + x["exp"].astype(str) + "|" + x["option_type"] + "|" + + x["strike_p"].astype(str) + "|" + x["strike"].astype(str) + "|" + + x["strike_n"].astype(str)) + x = x.sort_values(["trip", "ts"]) + g = x.groupby("trip", sort=False) + r1 = g["iv_resid"].shift(-1).to_numpy() + dt = (g["ts"].shift(-1) - x["ts"]).dt.total_seconds().to_numpy() / 3600.0 + r0 = x["iv_resid"].to_numpy() + m = np.isfinite(r1) & np.isfinite(r0) & (dt > 0) & (dt <= max_gap_h) + if m.sum() < 100: + return {"n": int(m.sum())} + b = np.polyfit(r0[m], r1[m] - r0[m], 1)[0] + return {"n": int(m.sum()), "beta": float(b), "corr": float(np.corrcoef(r0[m], r1[m])[0, 1]), + "meta_vita_h": float(np.log(0.5) / np.log(max(1e-9, 1 + b))) if -1 < b < 0 else np.nan} + + +# --------------------------------------------------------------------------- main + +def main() -> None: + t0 = time.time() + controlli_positivi() + d = load() + q, F = certifica(d) + q = q.merge(F, on=["asset", "ts", "exp"], how="left") + q["F"] = q["underlying_price"].fillna(q["F_pcp"]) + print(f"\n forward disponibile su {q['F'].notna().mean():.1%} delle quote a due lati " + f"(osservato dove c'e', altrimenti parita' put-call)") + + for dte_min, etichetta in ((0.0, "SENZA filtro DTE"), (DTE_MIN, f"DTE >= {DTE_MIN:g}g")): + print("\n" + "=" * 100) + print(f"1-2. ARBITRAGGI STATICI MODEL-FREE su prezzi ESEGUIBILI — {etichetta}") + print("=" * 100) + v = test_verticale(q, dte_min) + print(f" TEST V (monotonia nello strike): {v['n_viol']:,} violazioni su {v['n_confronti']:,} " + f"confronti = {v['quota']:.4%}") + if v["n_viol"]: + print(f" credito mediano {v['credito_med_coin']:.5f} coin = " + f"${v['credito_usd_medlot']:.2f} al lotto minimo (LORDO fee)") + + bf = test_farfalla(q, dte_min) + bf = in_punti_vol(bf) + bf["viol_exec"] = bf["cost_exec"] < 0 + bf["viol_mid"] = bf["cost_mid"] < 0 + print(f" TEST B (convessita' nello strike): {len(bf):,} triple consecutive") + print(f" al MID (incoerenza, non eseguibile): {bf['viol_mid'].sum():,} = {bf['viol_mid'].mean():.3%}") + print(f" ESEGUIBILE (compra ali all'ask, vendi corpo al bid): " + f"{bf['viol_exec'].sum():,} = {bf['viol_exec'].mean():.4%}") + if dte_min == DTE_MIN: + bf_keep = bf + + bf = bf_keep + print("\n" + "=" * 100) + print("3. QUANTO E' GRANDE L'INCOERENZA, E QUANTO COSTA ATTRAVERSARLA") + print("=" * 100) + for a in sorted(bf["asset"].unique()): + x = bf[bf["asset"] == a] + x = x[np.isfinite(x["halfspread_pt"]) & np.isfinite(x["tick_pt"])] + r = np.abs(x["iv_resid"].dropna()) + hs, tk = x["halfspread_pt"], x["tick_pt"] + print(f" {a} (n={len(x):,} triple con vega misurabile)") + print(f" |residuo di convessita' dello smile| mediana {r.median():.3f} pt-vol · " + f"p90 {r.quantile(.9):.3f} · p99 {r.quantile(.99):.3f}") + print(f" mezzo spread denaro-lettera mediana {hs.median():.3f} pt-vol · " + f"p25 {hs.quantile(.25):.3f} · p75 {hs.quantile(.75):.3f}") + print(f" UN TICK della griglia di prezzo mediana {tk.median():.3f} pt-vol " + f"-> l'incoerenza e' sotto il tick nel {np.mean(r.reindex(x.index) < tk):.1%} dei casi") + both = x.dropna(subset=["iv_resid"]) + rap = np.abs(both["iv_resid"]) / (2 * both["halfspread_pt"]) + print(f" |residuo| / (spread PIENO delle gambe): mediana {rap.median():.3f} · " + f"p99 {rap.quantile(.99):.3f} · quota > 1 = {np.mean(rap > 1):.3%}") + print(f" spread relativo (ask-bid)/mid del venue: mediana {x['relspread'].median():.1%} " + f"· p25 {x['relspread'].quantile(.25):.1%}") + # valore in dollari dell'incoerenza al MID (il caso migliore possibile: fee 0, spread 0) + eq = np.isclose(x["lam"], 0.5, atol=0.02) + lot = MIN_LOT[a] + val = -x.loc[eq, "cost_mid"].to_numpy() * 2 * lot * x.loc[eq, "spot_feed"].to_numpy() + pos = val[val > 0] + print(f" valore LORDO al mid della farfalla incoerente, lotto minimo: " + f"mediana ${np.median(pos):.2f} · p99 ${np.percentile(pos,99):.2f} · max ${pos.max():.2f}" + f" (n={len(pos):,})") + + print("\n economia della farfalla al LOTTO MINIMO (famiglia INVERSE, quella nel dato):") + for a in sorted(bf["asset"].unique()): + e = economia(bf, a) + print(f" {a}: nozionale/lotto ${e['nozionale_min_lotto_usd']:,.0f} · " + f"fee 3 gambe mediana ${e['fee_3gambe_usd_med']:.2f} · " + f"triple equispaziate {e['n_equisp']:,}") + print(f" credito netto: max ${e['netto_usd_max']:.2f} · p99 ${e['netto_usd_p99']:.2f} · " + f"positivi {e['n_netto_pos']:,}/{e['n_equisp']:,}") + + print("\n ESTRAPOLAZIONE DICHIARATA alla famiglia USDC (NON nel dato: il collettore raccoglie") + print(" le sole inverse — difetto di produzione del filone 11). Il lotto minimo e' 10x piu'") + print(" piccolo, ma il muro NON e' il ticket: e' lo spread in punti di vol, che e' una") + print(" QUOTA e non un livello -> ridurre il lotto non cambia |residuo|/spread.") + for a in sorted(bf["asset"].unique()): + x = bf[bf["asset"] == a] + x = x[np.isfinite(x["halfspread_pt"])].dropna(subset=["iv_resid"]) + rap = np.abs(x["iv_resid"]) / (2 * x["halfspread_pt"]) + rel_inv = x["relspread"].median() + for nome, rel_usdc in ((f"{a}_USDC", 0.082 if a == "BTC" else 0.026),): + k = rel_inv / rel_usdc # di quanto lo spread sarebbe piu' stretto + print(f" {a}: spread relativo inverse {rel_inv:.1%} vs {nome} {rel_usdc:.1%} " + f"-> fattore {k:.1f}x; rapporto mediano {rap.median():.3f} -> {rap.median()*k:.3f}, " + f"quota > 1 {np.mean(rap > 1):.2%} -> {np.mean(rap * k > 1):.2%}") + + print("\n" + "=" * 100) + print("4. TEST C — varianza totale nel tenore (l'unico che tocca un interpolatore)") + print("=" * 100) + c = test_calendario(q, DTE_MIN) + print(f" (i) strike CONDIVISO, nessuna interpolazione: {c['i_viol']:,}/{c['i_n']:,} = {c['i_quota']:.3%}" + f" · ampiezza mediana {c['i_amp_med_pt']:.3f} pt-vol (p95 {c['i_amp_p95_pt']:.3f})") + print(f" (ii) log-moneyness FISSO, con interpolazione (1 snapshot ogni {c['ii_ogni']}): " + f"{c['ii_viol']:,}/{c['ii_n']:,} = {c['ii_quota']:.3%} · ampiezza mediana {c['ii_amp_med_pt']:.3f} pt-vol") + print(f" -> contributo dell'INTERPOLATORE = {c['ii_quota'] - c['i_quota']:+.3%} di quota di violazione") + + print("\n" + "=" * 100) + print("5. TICK E PERSISTENZA — le due difese contro 'la mia incoerenza e' un artefatto'") + print("=" * 100) + for col, nome in (("viol_mid", "al MID"), ("viol_exec", "ESEGUIBILE")): + v = bf[bf[col]] + if len(v) == 0: + print(f" {nome}: nessuna violazione") + continue + legs = np.concatenate([v["bid"].to_numpy(), v["bid_p"].to_numpy(), v["bid_n"].to_numpy()]) + legs = legs[np.isfinite(legs) & (legs > 0)] + nt = legs / tick_of(legs) + p = persistenza(bf, col) + print(f" {nome}: n={len(v):,} · gambe a <=2 tick {np.mean(nt <= 2):.1%} " + f"(mediana {np.median(nt):.0f} tick) · resta in violazione allo snapshot dopo " + f"{p.get('resta', float('nan')):.1%} (n={p['n']:,})") + + print("\n" + "=" * 100) + print("6. IL RESIDUO SI RICOMPONE? (l'ipotesi del filone, misurata invece che assunta)") + print("=" * 100) + for a in sorted(bf["asset"].unique()): + rv = reversione(bf[bf["asset"] == a]) + if "beta" not in rv: + print(f" {a}: coppie insufficienti (n={rv['n']})") + continue + print(f" {a}: n={rv['n']:,} coppie appaiate (stessa tripla, snapshot consecutivo <=2h) · " + f"beta di ricomposizione {rv['beta']:+.3f} · corr(resid_t, resid_t+1) {rv['corr']:+.3f}") + if np.isfinite(rv["meta_vita_h"]): + print(f" -> meta-vita {rv['meta_vita_h']:.1f} h (beta=-1 sarebbe ricomposizione totale " + f"in un'ora, beta=0 residuo persistente = forma dello smile, non errore)") + + print("\n" + "=" * 100) + print("7. ROBUSTEZZA D'ANCORA — il risultato dipende dall'ora in cui guardo?") + print("=" * 100) + h = bf.assign(ora=bf["ts"].dt.hour).groupby("ora").agg( + mid=("viol_mid", "mean"), ex=("viol_exec", "mean"), n=("viol_mid", "size")) + print(f" quota di incoerenza al MID per ora UTC: mediana {h['mid'].median():.3%} · " + f"min {h['mid'].min():.3%} (h{h['mid'].idxmin():02d}) · max {h['mid'].max():.3%} (h{h['mid'].idxmax():02d})") + print(f" quota ESEGUIBILE per ora UTC: max {h['ex'].max():.5%} su {len(h)} ore -> " + f"nessuna ora del giorno apre una finestra") + + print("\n" + "=" * 100) + print("SINTESI") + print("=" * 100) + nv = int(bf["viol_exec"].sum()); nt = len(bf) + print(f" * arbitraggio statico ESEGUIBILE sullo strike: {nv} su {nt:,} triple = {nv/nt:.6%}") + print(f" * incoerenza al MID: {bf['viol_mid'].mean():.2%} delle triple, " + f"ma mediana {np.abs(bf['iv_resid']).median():.3f} punti di vol") + print(f" * mezzo spread mediano: BTC {bf[bf.asset=='BTC']['halfspread_pt'].median():.3f} pt-vol, " + f"ETH {bf[bf.asset=='ETH']['halfspread_pt'].median():.3f} pt-vol") + print(" * il credito netto della migliore farfalla eseguibile in 1,03M osservazioni e' NEGATIVO") + print(" -> l'incoerenza ESISTE e vale meno del costo di attraversarla: verdetto SCARTATO.") + print(f"\n[tempo {time.time()-t0:.1f}s]") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822b_tp01_sinistro.py b/scripts/research/r0822b_tp01_sinistro.py new file mode 100644 index 0000000..20e0ece --- /dev/null +++ b/scripts/research/r0822b_tp01_sinistro.py @@ -0,0 +1,873 @@ +"""r0822b_tp01_sinistro — quanto costa l'ASSICURAZIONE TP01 su un conto a barriera (2026-08-22). + +IL BUCO CHE CHIUDE +------------------ +`r0822_prop_alloc.py` (stessa ondata) ha ottimizzato l'allocazione di un conto **funded** sotto +barriera di drawdown e ha trovato l'ottimo a **TP01 25 / SKH01 25 / XS01 50**, cioe' TP01 pesato +**tre volte meno** del libro live (75%). Ha anche dichiarato il proprio limite: + + "la sotto-pesatura di TP01, lo sleeve DIFENSIVO, e' misurata su un campione SENZA SINISTRO + (2024-2026 non contiene un crash): su una barriera assorbente quella scommessa si paga una + volta sola." + +E questo si scontra con due misure del progetto: + (a) il **leave-one-out de-luckato** del 26/07: TP01 e' il maggior contributore al Sharpe FULL + (+0.390, positivo nel 100% delle estrazioni) e il suo hold-out negativo (-0.200, negativo + nel 99.1%) e' *la firma dell'assicurazione, non della morte* — "uno sleeve difensivo si + giudica sul SINISTRO, non sul premio"; + (b) `edge_watch` criterio B: negli **8 anni su 8** con drawdown buy&hold > 10% TP01 ha protetto + da **1.8x a 34.4x**. + +DOMANDA: qual e' il peso giusto di TP01 se l'obiettivo include sopravvivere a un crash che nel +campione di ottimizzazione NON c'e'? + +IPOTESI REGISTRATA PRIMA DI MISURARE +------------------------------------ +Mi aspettavo che (1) la sotto-pesatura di TP01 costasse molto **nel sinistro** e poco fuori, e che +(2) esistesse un peso di compromesso (~50%) dentro la regione robusta del funded e non +catastrofico al crash. In altre parole mi aspettavo di **confermare** il caveat di PROP-ALLOC. +Se la misura dice il contrario, e' un risultato e va scritto come tale. + +CIO' CHE E' UN'ASSUNZIONE E CIO' CHE E' UN DATO +----------------------------------------------- +* Lo **scenario di stress e' uno SCENARIO, non una previsione.** Due costruzioni, entrambe + dichiarate: (S1) bootstrap MISTO — ogni blocco ha probabilita' `p` di essere estratto dal pool + dei sinistri 2019-2023 invece che dalla finestra normale; (S2) **replay** — un episodio di + crash VERO innestato per intero, in ordine, a una data casuale dentro l'anno. In piu' il + replay deterministico del **2022 intero** (aprile-dicembre), che non e' un modello: e' successo. +* Il vincolo binding di TUTTO il filone e' che i sinistri osservati sono **QUATTRO**. Nessun + bootstrap crea informazione: l'ampiezza effettiva della coda resta 4 episodi, e ogni numero qui + eredita quell'errore. Detto una volta, vale per ogni tabella. +* Lente **ACCOPPIATA** (`r0725_prop_coupled`): il minimo intraday si compone ESATTO sul path + orario condiviso TP01/SKH01. Non e' un dettaglio: su una regola a UN giorno (daily-loss) la + lente close-only e' misurata **esattamente cieca** (0.00 breach contro 0.40-1.21 veri), mentre + su una statistica multi-giorno l'errore vale il 3.5%. Il vincolo funded E' una regola a un giorno. +* de-luck sul drift **x0.89** (fattore MISURATO il 26/07). ⚠️ L'offset di de-luck e' calcolato UNA + VOLTA sulla finestra 2019+ e sottratto ovunque in valore ASSOLUTO: de-luckare un pool di crash + sulla sua PROPRIA media (negativa) *aggiungerebbe* drift proprio dove non deve. +* macchineria RIUSATA, non riscritta: `r0725_prop_coupled` (tuple accoppiate) e `r0822_prop_alloc` + (`eval_sim`, `funded_sim`, `boot_idx`, `FIRMS`). Qui si aggiungono solo i pool e gli innesti. + +Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0822b_tp01_sinistro.py` +""" +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] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +from src.data.downloader import load_data # noqa: E402 +from src.portfolio.sleeves import _tp01_returns, _skyhook_returns # noqa: E402 + +import r0725_prop_coupled as pc # noqa: E402 +import r0822_prop_alloc as PA # noqa: E402 + +# ------------------------------------------------------------------ costanti DICHIARATE +DELUCK = PA.DELUCK # 0.89, misurato il 26/07 +FULL_START = "2019-03-14" # inizio del recon crypto (TP01+SKH01) +RECENT_START = "2024-01-01" # finestra di ottimizzazione di PROP-ALLOC (nasce XS01) +BLOCK = 20 # blocco del bootstrap, in giorni +N_PATHS = 2500 +EPI_LEN = 30 # lunghezza di un episodio di sinistro, in giorni +HORIZON = 365 +LEVS = (0.25, 0.375, 0.50, 0.625, 0.75, 1.00, 1.25, 1.50) +W_GRID = tuple(i / 8 for i in range(9)) # peso di TP01 nella coppia crypto, 0 -> 1 +SURV_TARGET = 0.90 # livello di sopravvivenza della frontiera ISO-SOPRAVVIVENZA + +N_EVAL = 0 # contatore onesto delle valutazioni (griglia dichiarata) + + +def hr(t: str = "") -> None: + print("\n" + "-" * 106) + if t: + print(f" {t}") + print("-" * 106) + + +# ================================================================== §A. PANNELLO + +_TUP: dict = {} + + +def crypto(w_tp: float) -> tuple[np.ndarray, np.ndarray, pd.DatetimeIndex]: + """(R, gap) giornalieri della coppia crypto a peso TP01 = w_tp, SKH01 = 1-w_tp. + gap = minimo_intraday - chiusura (<=0). Storia piena dal 2019-03.""" + k = round(float(w_tp), 6) + if k not in _TUP: + c = pc.crypto_daily_tuples(k, 1.0 - k) + R = c["R"].values.astype(float) + G = np.minimum(c["m"].values, R) - R + _TUP[k] = (R, G, c.index) + R, G, I = _TUP[k] + return R.copy(), G.copy(), I + + +_OFF: dict = {} + + +def deluck_offset(w_tp: float, factor: float = DELUCK) -> float: + """Offset ASSOLUTO di de-luck, calcolato UNA VOLTA sulla finestra 2019+ e riusato ovunque. + Calcolarlo sulla media di un sotto-campione di crash (negativa) *aggiungerebbe* drift.""" + key = (round(float(w_tp), 6), round(float(factor), 4)) + if key not in _OFF: + R, _, I = crypto(w_tp) + _OFF[key] = float((1.0 - factor) * R[I >= pd.Timestamp(FULL_START, tz="UTC")].mean()) + return _OFF[key] + + +def buyhold_daily(idx: pd.DatetimeIndex) -> pd.Series: + """Buy&hold 50/50 BTC+ETH sulla stessa griglia giornaliera del libro.""" + bh = None + for a in ("BTC", "ETH"): + d = load_data(a, "1h") + i = pd.DatetimeIndex(pd.to_datetime(d["datetime"], utc=True)) + s = pd.Series(d["close"].astype(float).values, index=i).resample("1D").last() + r = s.pct_change().fillna(0.0) * 0.5 + bh = r if bh is None else bh.add(r, fill_value=0.0) + return bh.reindex(idx).fillna(0.0) + + +def worst_windows(lbh: np.ndarray, idx: pd.DatetimeIndex, K: int, L: int, + hi: int | None = None) -> list[int]: + """Le K finestre di L giorni NON sovrapposte col peggior rendimento buy&hold, greedy sul + minimo. `hi` limita i possibili inizi (per restare dentro 2019-2023).""" + cs = np.concatenate(([0.0], np.cumsum(lbh))) + win = np.expm1(cs[L:] - cs[:-L]) + cand = np.arange(len(win)) if hi is None else np.arange(min(hi, len(win))) + order = cand[np.argsort(win[cand])] + out: list[int] = [] + for t in order: + if all(abs(int(t) - q) >= L for q in out): + out.append(int(t)) + if len(out) == K: + break + return sorted(out) + + +def binom_two_sided(k: int, n: int) -> float: + """p-value esatto a due code di un test dei segni con p0=0.5 (n piccolo: si conta).""" + from math import comb + tot = 2 ** n + kk = max(k, n - k) + tail = sum(comb(n, j) for j in range(kk, n + 1)) + return min(1.0, 2.0 * tail / tot) + + +def maxdd(r: np.ndarray) -> float: + eq = np.cumprod(1.0 + r) + return float((eq / np.maximum.accumulate(eq) - 1.0).min()) + + +# ================================================================== §B. SIMULAZIONE + +def J_of(R: np.ndarray, G: np.ndarray, ev_idx: np.ndarray, fu_idx: np.ndarray, + lev: float, firm: str = "HYRO") -> dict: + """J = P(passare l'eval) x P(essere vivo a 12 mesi da funded), lente ACCOPPIATA.""" + global N_EVAL + N_EVAL += 1 + e = PA.eval_sim(R, G, ev_idx, lev, firm, "coupled") + f = PA.funded_sim(R, G, fu_idx, lev, firm, 100_000.0, "coupled") + return dict(J=e["p_pass"] * f["p_alive"], p_pass=e["p_pass"], p_alive=f["p_alive"], + e_payout=f["e_payout"], jvec=(e["passed"] & f["alive"]).astype(float), + alive=f["alive"]) + + +def best_lev(R: np.ndarray, G: np.ndarray, ev_idx, fu_idx, firm: str = "HYRO") -> tuple: + rows = [(lv, J_of(R, G, ev_idx, fu_idx, lv, firm)) for lv in LEVS] + return max(rows, key=lambda x: x[1]["J"]) + + +def pool_paths(seg_bounds: list[tuple[int, int]], n_days: int, n_paths: int, seed: int, + block: int = BLOCK) -> np.ndarray: + """Indici di bootstrap a blocchi ristretti a segmenti CONTIGUI (ogni blocco vive dentro un + solo segmento -> non si incollano giorni di episodi diversi).""" + rng = np.random.default_rng(seed) + starts = [] + for a, b in seg_bounds: + if b - a >= block: + starts.extend(range(a, b - block + 1)) + starts = np.array(starts) + nb = int(np.ceil(n_days / block)) + st = starts[rng.integers(0, len(starts), size=(n_paths, nb))] + return (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days] + + +def mix_paths(base_idx: np.ndarray, crash_starts: np.ndarray, p: float, seed: int, + block: int = BLOCK) -> np.ndarray: + """S1 — bootstrap MISTO: ogni blocco della path normale ha probabilita' `p` di essere + sostituito da un blocco estratto dal pool dei SINISTRI.""" + rng = np.random.default_rng(seed) + out = base_idx.copy() + P, D = out.shape + nb = int(np.ceil(D / block)) + hit = rng.random((P, nb)) < p + for b in range(nb): + sl = slice(b * block, min((b + 1) * block, D)) + L = sl.stop - sl.start + rows = np.where(hit[:, b])[0] + if len(rows) == 0: + continue + st = crash_starts[rng.integers(0, len(crash_starts), size=len(rows))] + out[rows, sl] = st[:, None] + np.arange(L)[None, :] + return out + + +def splice_paths(base_idx: np.ndarray, epi_idx: np.ndarray, seed: int) -> np.ndarray: + """S2 — REPLAY: l'episodio VERO (contiguo, in ordine) innestato una volta sola a una data + casuale dentro l'orizzonte. E' la domanda operativa: *se il sinistro arriva*.""" + rng = np.random.default_rng(seed) + out = base_idx.copy() + P, D = out.shape + L = len(epi_idx) + st = rng.integers(0, max(D - L, 1), size=P) + for i in range(P): + a = int(st[i]) + out[i, a:a + L] = epi_idx[:min(L, D - a)] + return out + + +# ================================================================== main + +def main() -> None: # noqa: C901 + 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} (valutazioni finora: {N_EVAL})") + + print("=" * 106) + print(" TP01-SINISTRO — quanto costa l'assicurazione su un conto a barriera, e quanto rende") + print(" quando il sinistro arriva. Lente ACCOPPIATA, firm HYRO.") + print("=" * 106) + + R75, G75, IDX = crypto(0.75) + n_full = len(IDX) + i_rec = int(np.searchsorted(IDX, pd.Timestamp(RECENT_START, tz="UTC"))) + print(f"\n finestra crypto piena: {n_full} giorni {IDX[0].date()} -> {IDX[-1].date()}") + print(f" finestra di ottimizzazione di PROP-ALLOC: {n_full - i_rec} giorni dal " + f"{IDX[i_rec].date()} (e' anche la finestra di SCOPERTA di XS01)") + + # --------------------------------------------------------- sanity: recon vs sleeve ufficiale + print("\n SANITY — il recon orario contro gli sleeve ufficiali. TP01/SKH01 NON coincidono") + print(" barra per barra per costruzione (il recon e' un mark-to-market ORARIO, lo sleeve") + print(" marca a fine-trade): il controllo giusto e' il log-totale.") + for nm, off_s, w in (("TP01", _tp01_returns(), 1.0), ("SKH01", _skyhook_returns(), 0.0)): + o = off_s.dropna().sort_index() + if o.index.tz is None: + o.index = o.index.tz_localize("UTC") + Rn, _, In = crypto(w) + com = o.index.intersection(In) + a = o.reindex(com).values + b = pd.Series(Rn, index=In).reindex(com).values + print(f" {nm:>6} log-tot sleeve {float(np.log1p(a).sum()):+8.4f} " + f"recon {float(np.log1p(b).sum()):+8.4f} scarto " + f"{float(np.log1p(a).sum()-np.log1p(b).sum()):+7.4f}") + + # ============================================================ §1. CHE COS'E' UN SINISTRO + hr("§1. CHE COS'E' UN SINISTRO — e la frase '2024-2026 non contiene un crash' e' FALSA") + bh = buyhold_daily(IDX) + lbh = np.log1p(bh.values) + print(" Peggior rendimento buy&hold 50/50 su finestra mobile, per periodo:") + print(f" {'W':>4} {'2019-2023 min':>15} {'2024+ min':>12} {'rapporto':>10} " + f"{'2019-23 p1':>11} {'2024+ p1':>10} {'data del min 2024+':>20}") + ratios = [] + for W in (1, 5, 10, 20, 60): + cs = np.concatenate(([0.0], np.cumsum(lbh))) + roll = np.expm1(cs[W:] - cs[:-W]) # finestra che FINISCE in t=W-1+k + tt = IDX[W - 1:] + pre = roll[tt < pd.Timestamp(RECENT_START, tz="UTC")] + post = roll[tt >= pd.Timestamp(RECENT_START, tz="UTC")] + tpost = tt[tt >= pd.Timestamp(RECENT_START, tz="UTC")] + rr = pre.min() / post.min() + ratios.append(rr) + print(f" {W:>4} {pre.min():>15.1%} {post.min():>12.1%} {rr:>10.2f}x " + f"{np.percentile(pre, 1):>11.1%} {np.percentile(post, 1):>10.1%} " + f"{str(tpost[int(np.argmin(post))].date()):>20}") + print(f"\n -> il 2024+ **contiene** un crash: peggior giorno -14.5%, peggiori 20g -39.6%,") + print(f" drawdown buy&hold fino a -60%. Cio' che NON contiene e' un crash della TAGLIA") + print(f" dei precedenti: il rapporto e' {np.mean(ratios[:3]):.1f}x sulla coda veloce (1-10g)") + print(f" e {ratios[-1]:.1f}x a 60 giorni. **La frase giusta non e' 'senza sinistro' ma") + print(f" 'con un sinistro di taglia dimezzata'** — e cambia la domanda: non 'cosa succede") + print(f" se arriva un crash' ma 'cosa succede se ne arriva uno DUE VOLTE piu' grande'.") + + # ----- replica del criterio B di edge_watch (protezione per anno) + print("\n Criterio B di `edge_watch` replicato qui (anni con DD buy&hold > 10%):") + tp_only, _, _ = crypto(1.0) + ser_bh = pd.Series(bh.values, index=IDX) + ser_tp = pd.Series(tp_only, index=IDX) + print(f" {'anno':>6} {'DD b&h':>9} {'DD TP01':>9} {'protezione':>12} {'DD SKH01':>10} " + f"{'DD 75/25':>10}") + skh_only, _, _ = crypto(0.0) + ser_sk = pd.Series(skh_only, index=IDX) + ser_75 = pd.Series(R75, index=IDX) + n_sin = n_ok = 0 + for y, g in ser_bh.groupby(ser_bh.index.year): + d_bh = maxdd(g.values) + if d_bh > -0.10: + continue + n_sin += 1 + d_tp = maxdd(ser_tp.loc[g.index].values) + d_sk = maxdd(ser_sk.loc[g.index].values) + d_75 = maxdd(ser_75.loc[g.index].values) + ok = (d_tp / d_bh) <= 0.75 + n_ok += ok + print(f" {y:>6} {d_bh:>9.1%} {d_tp:>9.1%} {d_bh/d_tp:>11.1f}x {d_sk:>10.1%} " + f"{d_75:>10.1%} {'ok' if ok else 'FAIL'}") + print(f" -> anni di sinistro {n_sin}, superati {n_ok}. **Ogni anno e' un anno di sinistro") + print(f" per il buy&hold**: la 'frequenza del sinistro' non e' un evento raro, e' lo") + print(f" stato normale dell'asset. Cio' che e' raro e' la TAGLIA (riga sopra).") + + # ----- definizione degli episodi + hr("§1-bis. GLI EPISODI DI SINISTRO — regola di selezione DICHIARATA PRIMA") + print(f" Regola: le {3} finestre di {EPI_LEN} giorni NON sovrapposte con il peggior rendimento") + print(f" buy&hold nel 2019-2023 (greedy sul minimo), PIU' l'FTX 2022-11-05 aggiunto **per") + print(f" NOME** (evento noto, non selezionato dal minimo: e' un controllo, non una scelta).") + cs = np.concatenate(([0.0], np.cumsum(lbh))) + win = np.expm1(cs[EPI_LEN:] - cs[:-EPI_LEN]) # finestra che INIZIA in t + pre_mask = IDX[:len(win)] < pd.Timestamp(RECENT_START, tz="UTC") + cand = np.where(pre_mask)[0] + order = cand[np.argsort(win[cand])] + picked = [] + for t in order: + if all(abs(int(t) - p) >= EPI_LEN for p in picked): + picked.append(int(t)) + if len(picked) == 3: + break + t_ftx = int(np.searchsorted(IDX, pd.Timestamp("2022-11-05", tz="UTC"))) + epis = sorted(picked + [t_ftx]) + NAMES = {} + print(f"\n {'episodio':>12} {'dal':>12} {'al':>12} {'b&h 30g':>10} {'peggior 1g b&h':>16}") + for t in epis: + lab = ("COVID" if IDX[t].year == 2020 else + "MAG-2021" if (IDX[t].year, IDX[t].month) == (2021, 5) else + "LUNA" if (IDX[t].year, IDX[t].month) in ((2022, 5), (2022, 6)) else + "FTX" if (IDX[t].year, IDX[t].month) == (2022, 11) else f"EPI-{IDX[t].date()}") + NAMES[t] = lab + seg = bh.values[t:t + EPI_LEN] + print(f" {lab:>12} {str(IDX[t].date()):>12} {str(IDX[t+EPI_LEN-1].date()):>12} " + f"{win[t]:>10.1%} {seg.min():>16.1%}") + crash_days = np.concatenate([np.arange(t, t + EPI_LEN) for t in epis]) + freq_day = len(crash_days) / n_full + print(f"\n giorni di sinistro {len(crash_days)} su {n_full} = **{freq_day:.1%} del tempo**") + print(f" ⚠️ AMPIEZZA EFFETTIVA DELLA CODA = **4 EPISODI**. Nessun bootstrap la aumenta.") + print(f" Ogni numero di questo file eredita l'errore di un campione di quattro eventi.") + + tick("§1 sinistri") + + # ============================================================ §2. IL CONTO DELL'ASSICURAZIONE + hr("§2. IL CONTO DELL'ASSICURAZIONE — premio fuori dal sinistro, indennizzo dentro") + print(" Confronto DUE ripartizioni della coppia crypto, appaiato giorno per giorno:") + print(" LIVE = TP01 75 / SKH01 25 (il libro che gira)") + print(" OPT = TP01 38 / SKH01 62 (l'ottimo di PROP-ALLOC sul campione 2019+)") + in_sin = np.zeros(n_full, bool) + in_sin[crash_days] = True + print(f"\n {'config':>10} {'drift fuori':>12} {'drift dentro':>13} {'Sharpe full':>12} " + f"{'maxDD':>8} {'peggior 1g':>11} {'peggior min 1g':>15}") + prof = {} + for lab, w in (("LIVE 75/25", 0.75), ("OPT 38/62", 0.375), ("TP01 puro", 1.0), + ("SKH01 puro", 0.0)): + R, G, _ = crypto(w) + R = R - deluck_offset(w) + out_d = R[~in_sin].mean() * 365.0 + in_d = R[in_sin].mean() * 365.0 + vol = R.std() * np.sqrt(365.0) + prof[lab] = dict(out=out_d, ind=in_d, sh=R.mean() * 365.0 / vol, dd=maxdd(R), + w1=R.min(), wm=(R + G).min()) + p = prof[lab] + print(f" {lab:>10} {p['out']:>12.1%} {p['ind']:>13.1%} {p['sh']:>12.2f} {p['dd']:>8.1%} " + f"{p['w1']:>11.2%} {p['wm']:>15.2%}") + prem = prof["LIVE 75/25"]["out"] - prof["OPT 38/62"]["out"] + ind = prof["LIVE 75/25"]["ind"] - prof["OPT 38/62"]["ind"] + R_live = crypto(0.75)[0] - deluck_offset(0.75) + R_opt = crypto(0.375)[0] - deluck_offset(0.375) + cum_live = float(np.expm1(np.log1p(R_live[in_sin]).sum())) + cum_opt = float(np.expm1(np.log1p(R_opt[in_sin]).sum())) + print(f"\n PREMIO (LIVE - OPT) fuori dal sinistro : {prem:+.2%}/anno = " + f"{prem*100_000:+,.0f} $/anno su un funded da $100k") + print(f" INDENNIZZO (LIVE - OPT) sui 120 giorni di sinistro, CUMULATO (non annualizzato,") + print(f" che su 120 giorni sarebbe una finta precisione): LIVE {cum_live:+.1%} contro " + f"OPT {cum_opt:+.1%} = {cum_live-cum_opt:+.1%}") + print(f" su un funded da $100k: {(cum_live-cum_opt)*100_000:+,.0f} $ per l'INTERO " + f"monte-sinistri di {n_full/365.0:.1f} anni") + print(f" frequenza: {freq_day:.1%} del tempo dentro un sinistro (4 episodi in " + f"{n_full/365.0:.1f} anni = uno ogni {n_full/365.0/4:.1f} anni)") + if ind < 0: + print("\n 🚨 **IL SEGNO E' ROVESCIATO RISPETTO ALLA MIA IPOTESI.** Dentro il sinistro il") + print(" libro con MENO TP01 fa MEGLIO, non peggio. Il meccanismo si legge in una riga:") + print(" TP01 nel crash va FLAT (non perde, non guadagna: e' assicurazione), SKH01 va") + print(" SHORT (guadagna). Su un conto a barriera *guadagnare batte non-perdere*.") + print("\n Rendimento dei 4 episodi, per peso di TP01 (30 giorni ciascuno, chiusura e MINIMO):") + print(f" {'w TP01':>8} " + " ".join(f"{NAMES[t]:>18}" for t in epis) + f" {'peggior min':>13}") + for w in (1.0, 0.75, 0.5, 0.375, 0.25, 0.0): + R, G, _ = crypto(w) + cells = [] + wm = 0.0 + for t in epis: + seg = R[t:t + EPI_LEN] + gp = G[t:t + EPI_LEN] + eq = np.cumprod(1.0 + seg) + mn = float(np.minimum(eq - 1.0, np.concatenate(([1.0], eq[:-1])) * (1 + seg + gp) - 1.0).min()) + wm = min(wm, mn) + cells.append(f"{float(eq[-1]-1.0):+7.1%}/{mn:+7.1%}") + print(f" {w:>8.3f} " + " ".join(f"{c:>18}" for c in cells) + f" {wm:>13.1%}") + print(" (formato: chiusura a 30g / minimo intraday cumulato dell'episodio)") + + print("\n ⚠️ QUANTA POTENZA HA QUESTO SEGNO? Con 4 episodi, '4 su 4 a favore' ha un test dei") + print(" segni con p = 0.125 a due code: **non e' significativo al 5%**. Il modo onesto di") + print(" guadagnare potenza non e' ripetere il bootstrap (non crea eventi) ma ESTENDERE la") + print(" stessa regola di selezione a piu' finestre. Le 12 peggiori finestre di 30g NON") + print(" sovrapposte su TUTTO il 2019+ (stessa regola greedy, K da 3 a 12):") + w12 = worst_windows(lbh, IDX, 12, EPI_LEN) + print(f"\n {'finestra':>12} {'b&h 30g':>9} {'LIVE 75/25':>11} {'OPT 38/62':>11} " + f"{'TP01 puro':>10} {'SKH01 puro':>11} {'OPT>LIVE':>9}") + wins = 0 + wins_sk = 0 + lr_live, lr_opt = np.log1p(R_live), np.log1p(R_opt) + R_tp = crypto(1.0)[0] - deluck_offset(1.0) + R_sk = crypto(0.0)[0] - deluck_offset(0.0) + for t in w12: + a = float(np.expm1(lr_live[t:t + EPI_LEN].sum())) + b = float(np.expm1(lr_opt[t:t + EPI_LEN].sum())) + c = float(np.expm1(np.log1p(R_tp[t:t + EPI_LEN]).sum())) + d = float(np.expm1(np.log1p(R_sk[t:t + EPI_LEN]).sum())) + wins += b > a + wins_sk += d > c + print(f" {str(IDX[t].date()):>12} {win[t]:>9.1%} {a:>11.1%} {b:>11.1%} " + f"{c:>10.1%} {d:>11.1%} {('si' if b > a else 'no'):>9}") + print(f"\n OPT batte LIVE in **{wins}/12** finestre -> test dei segni " + f"p = {binom_two_sided(wins, 12):.4f}") + print(f" SKH01 puro batte TP01 puro in **{wins_sk}/12** -> p = " + f"{binom_two_sided(wins_sk, 12):.4f}") + print(" ⚠️ 5 di queste 12 finestre stanno nel 2024+, cioe' DENTRO la finestra su cui") + print(" SKH01 e' stato tarato e su cui PROP-ALLOC ha ottimizzato: la potenza sale ma") + print(" l'indipendenza scende. Il test va letto come **una diagnostica, non una prova**.") + + tick("§2 conto assicurazione") + + # ============================================================ §3. SOTTO BARRIERA + hr("§3. SOTTO BARRIERA — griglia peso TP01 x leva su TRE lenti di campione") + print(f" {len(W_GRID)} pesi x {len(LEVS)} leve x 3 finestre. J = P(pass) x P(vivo 12m), HYRO") + print(f" (eval +10%/-6% statico/-4% giornaliero; funded -6% statico/-4% giornaliero).") + ev_f = PA.boot_idx(n_full, HORIZON, N_PATHS, seed=8230) + fu_f = PA.boot_idx(n_full, HORIZON, N_PATHS, seed=8231) + n_rec = n_full - i_rec + ev_r = PA.boot_idx(n_rec, HORIZON, N_PATHS, seed=8240) + fu_r = PA.boot_idx(n_rec, HORIZON, N_PATHS, seed=8241) + seg_sin = [(t, t + EPI_LEN) for t in epis] + ev_s = pool_paths(seg_sin, HORIZON, N_PATHS, seed=8250) + fu_s = pool_paths(seg_sin, HORIZON, N_PATHS, seed=8251) + + print(f"\n {'w TP01':>7} | {'2019+ (col 2022)':>26} | {'2024+ (PROP-ALLOC)':>26} | " + f"{'SOLO-SINISTRO':>26}") + print(f" {'':>7} | {'leva':>6} {'J':>7} {'P(viv)':>7} | {'leva':>6} {'J':>7} {'P(viv)':>7} | " + f"{'leva':>6} {'J':>7} {'P(viv)':>7}") + tab = {} + for w in W_GRID: + R, G, _ = crypto(w) + off = deluck_offset(w) + Rf, Gf = R - off, G + Rr, Gr = Rf[i_rec:], Gf[i_rec:] + lv1, o1 = best_lev(Rf, Gf, ev_f, fu_f) + lv2, o2 = best_lev(Rr, Gr, ev_r, fu_r) + lv3, o3 = best_lev(Rf, Gf, ev_s, fu_s) + tab[w] = dict(full=(lv1, o1), rec=(lv2, o2), sin=(lv3, o3)) + print(f" {w:>7.3f} | {lv1:>6.3f} {o1['J']:>7.3f} {o1['p_alive']:>7.1%} | " + f"{lv2:>6.3f} {o2['J']:>7.3f} {o2['p_alive']:>7.1%} | " + f"{lv3:>6.3f} {o3['J']:>7.3f} {o3['p_alive']:>7.1%}") + a_full = max(tab, key=lambda w: tab[w]["full"][1]["J"]) + a_rec = max(tab, key=lambda w: tab[w]["rec"][1]["J"]) + a_sin = max(tab, key=lambda w: tab[w]["sin"][1]["J"]) + print(f"\n argmax: 2019+ -> TP01 {a_full:.3f} | 2024+ -> TP01 {a_rec:.3f} | " + f"SOLO-SINISTRO -> TP01 {a_sin:.3f}") + print(" ⚠️ La colonna SOLO-SINISTRO e' una lente ESTREMA e va letta come tale: l'anno intero") + print(" e' fatto di soli blocchi di crash (4 episodi ricampionati). Non e' uno scenario") + print(" plausibile, e' il **limite superiore del danno**: se il verso della risposta e' lo") + print(" stesso qui e sul 2019+, non dipende dalla frazione di sinistro assunta.") + + # replica di controllo del numero pubblicato da PROP-ALLOC (E-ter) + print("\n REPLICA DI CONTROLLO — PROP-ALLOC E-ter pubblica, sul 2019+: argmax della coppia") + print(" crypto **38/62 @0.50x con J 0.521**, e **75/25 J 0.443**. Stesse seed, stessa lente:") + j38 = tab[0.375]["full"] + j75 = tab[0.75]["full"] + print(f" qui: 38/62 @{j38[0]:g}x J {j38[1]['J']:.3f} | 75/25 @{j75[0]:g}x " + f"J {j75[1]['J']:.3f} -> " + f"{'REPLICA OK' if abs(j38[1]['J']-0.521) < 0.02 and abs(j75[1]['J']-0.443) < 0.02 else 'NON REPLICA — leggere con cautela'}") + + tick("§3 griglia 3 lenti") + + # ============================================================ §4. LO STRESS ESPLICITO + hr("§4. LO STRESS ESPLICITO — iniettare nel 2024+ i crash che il 2024+ non ha") + print(" Costruzione DICHIARATA. Base = blocchi del 2024+ (la finestra di ottimizzazione).") + print(" S1 MISTO: ogni blocco ha probabilita' p di venire dal pool dei 4 sinistri 2019-2023.") + print(f" p = {freq_day:.3f} e' la frequenza STORICA; si girano anche 2x e 4x.") + print(" S2 REPLAY: un episodio VERO innestato per intero, in ordine, a data casuale dentro") + print(" l'anno. E' la domanda operativa: *se il sinistro arriva, chi resta in piedi?*") + print(" ⚠️ Sono SCENARI, non previsioni. E poggiano su 4 episodi.") + + # pool = 2024+ (blocchi normali) + gli episodi in coda, su un indice unico + def stress_panel(w: float) -> tuple[np.ndarray, np.ndarray, np.ndarray, list]: + R, G, _ = crypto(w) + off = deluck_offset(w) + Rn, Gn = R[i_rec:] - off, G[i_rec:] + segs = [] + Rc, Gc = [], [] + cur = len(Rn) + for t in epis: + Rc.append(R[t:t + EPI_LEN] - off) + Gc.append(G[t:t + EPI_LEN]) + segs.append((cur, cur + EPI_LEN)) + cur += EPI_LEN + Rp = np.concatenate([Rn] + Rc) + Gp = np.concatenate([Gn] + Gc) + return Rp, Gp, np.array([s for s, _ in segs]), segs + + base_ev = PA.boot_idx(n_rec, HORIZON, N_PATHS, seed=8260) + base_fu = PA.boot_idx(n_rec, HORIZON, N_PATHS, seed=8261) + + print(f"\n S1 — bootstrap MISTO (base 2024+, blocchi di sinistro con probabilita' p)") + print(f" {'w TP01':>7} | " + " | ".join(f"p={p:.3f}: {'J':>6} {'P(viv)':>7}" + for p in (0.0, freq_day, 2 * freq_day, 4 * freq_day))) + s1 = {} + for w in W_GRID: + Rp, Gp, cst, segs = stress_panel(w) + starts = np.concatenate([np.arange(a, b - BLOCK + 1) for a, b in segs]) + cells = [] + for j, p in enumerate((0.0, freq_day, 2 * freq_day, 4 * freq_day)): + ev = base_ev if p == 0 else mix_paths(base_ev, starts, p, seed=8270 + j) + fu = base_fu if p == 0 else mix_paths(base_fu, starts, p, seed=8280 + j) + lv, o = best_lev(Rp, Gp, ev, fu) + cells.append((lv, o)) + s1[w] = cells + print(f" {w:>7.3f} | " + " | ".join(f" @{c[0]:.3g}x {c[1]['J']:>6.3f} " + f"{c[1]['p_alive']:>7.1%}" for c in cells)) + for j, p in enumerate((0.0, freq_day, 2 * freq_day, 4 * freq_day)): + am = max(s1, key=lambda w: s1[w][j][1]["J"]) + print(f" p={p:.3f} -> argmax TP01 {am:.3f} (J {s1[am][j][1]['J']:.3f}; " + f"75/25 fa {s1[0.75][j][1]['J']:.3f})") + + tick("§4 S1 misto") + + print(f"\n S2 — REPLAY: un episodio innestato una volta nell'anno funded (365g)") + print(f" Metrica: **P(vivo a 12 mesi) con il crash dentro**, alla leva scelta sul 2024+") + print(f" (cioe' la leva che uno sceglierebbe NON sapendo del crash: e' il punto).") + hdr = " ".join(f"{NAMES[t]:>10}" for t in epis) + print(" ⚠️ DUE colonne di leva, e servono entrambe: la leva 'da 2024+' e' quella che uno") + print(" sceglierebbe non sapendo del crash (realistica, ma CONFONDE peso e leva perche' non") + print(" e' la stessa per tutte le righe); la leva COMUNE 0.50x isola l'effetto del peso.") + print(f"\n {'w TP01':>7} {'leva (da 2024+)':>16} {hdr:>44} {'peggiore':>10} " + f"{'peggiore @0.50x':>16} {'P(viv) 2024+':>13}") + s2 = {} + for w in W_GRID: + Rp, Gp, _, segs = stress_panel(w) + lv = tab[w]["rec"][0] + cells, cells_c = [], [] + for k, (a, b) in enumerate(segs): + fu = splice_paths(base_fu, np.arange(a, b), seed=8290 + k) + cells.append(J_of(Rp, Gp, base_ev, fu, lv)["p_alive"]) + cells_c.append(J_of(Rp, Gp, base_ev, fu, 0.50)["p_alive"]) + s2[w] = (lv, cells, min(cells_c), tab[w]["rec"][1]["p_alive"]) + print(f" {w:>7.3f} {lv:>16.3f} " + " ".join(f"{c:>10.1%}" for c in cells) + + f" {min(cells):>10.1%} {min(cells_c):>16.1%} " + f"{tab[w]['rec'][1]['p_alive']:>13.1%}") + bw_a = max(s2, key=lambda w: min(s2[w][1])) + bw_c = max(s2, key=lambda w: s2[w][2]) + print(f"\n argmax della colonna 'peggiore': leva-da-2024+ -> TP01 {bw_a:.3f} | " + f"leva comune 0.50x -> TP01 {bw_c:.3f}") + print(" Lettura: se la sopravvivenza al crash SALISSE col peso di TP01, l'assicurazione") + print(" servirebbe. **A leva comune non sale**: la curva e' piatta-decrescente. Il difensivo") + print(" non compra sopravvivenza su questa barriera, perche' cio' che la barriera punisce e'") + print(" la PERDITA nel crash, e nel crash TP01 perde (prende la prima gamba giu' e poi va") + print(" flat) mentre SKH01 si gira short e GUADAGNA.") + + + tick("§4 S2 replay") + + # ----- replay deterministico del 2022 + print("\n S2-bis — REPLAY DETERMINISTICO DEL 2022 (2022-04-01 -> 2022-12-31, 275 giorni).") + print(" Non e' un modello: e' successo. Un conto funded HYRO da $100k che parte il 1° aprile") + print(" 2022 con max-loss STATICO -6% e daily-loss -4%, alla leva scelta sul 2024+:") + a22 = int(np.searchsorted(IDX, pd.Timestamp("2022-04-01", tz="UTC"))) + b22 = int(np.searchsorted(IDX, pd.Timestamp("2023-01-01", tz="UTC"))) + def replay22(w: float, lev: float) -> tuple: + R, G, _ = crypto(w) + off = deluck_offset(w) + r = (R[a22:b22] - off) * lev + g = G[a22:b22] * lev + eq, mn, bust, why = 1.0, 1.0, None, "" + for i in range(len(r)): + low = eq * (1.0 + r[i] + g[i]) + eq = eq * (1.0 + r[i]) + mn = min(mn, low) + if bust is None and (low < 0.94 or (r[i] + g[i]) < -0.04): + bust = IDX[a22 + i].date() + why = "daily-loss -4%" if (r[i] + g[i]) < -0.04 else "max-loss -6%" + return eq - 1.0, mn - 1.0, bust, why, float((r + g).min()) + + print(f"\n {'w TP01':>7} {'leva':>6} {'rend. 9 mesi':>13} {'peggior giorno':>15} " + f"{'min equity':>11} {'esito':>22}") + for w in W_GRID: + lv = tab[w]["rec"][0] + ret, mn, bust, why, wd = replay22(w, lv) + esito = "VIVO" if bust is None else f"BUSTATO {bust}" + print(f" {w:>7.3f} {lv:>6.3f} {ret:>13.1%} {wd:>15.2%} {mn:>11.2%} {esito:>22}") + print("\n Tutti VIVI, e non e' una buona notizia mal letta: **alle leve funded il 2022 non") + print(" era una minaccia per questo libro** (min equity -1.4/-2.4% contro una barriera al") + print(" -6%). Cio' che uccide un conto a barriera non e' il crash del sottostante: e' la") + print(" regola a UN giorno su una giornata qualunque. Ecco fin dove si puo' spingere la leva:") + print(f"\n {'w TP01':>7} {'leva max viva sul 2022':>23} {'rend. a quella leva':>20} " + f"{'primo vincolo che morde':>25}") + for w in W_GRID: + lo, hi = 0.1, 8.0 + for _ in range(18): + mid = 0.5 * (lo + hi) + if replay22(w, mid)[2] is None: + lo = mid + else: + hi = mid + ret, _, _, _, _ = replay22(w, lo) + _, _, _, why, _ = replay22(w, hi) + print(f" {w:>7.3f} {lo:>23.2f}x {ret:>20.1%} {why:>25}") + print("\n ⚠️ La leva di rottura e' MONOTONA DECRESCENTE nel peso di TP01 solo se la") + print(" protezione fosse reale. Si legga la colonna: se il libro con piu' TP01 rompe PRIMA,") + print(" l'assicurazione non sta assicurando la cosa giusta.") + + tick("§4 replay 2022") + + # ============================================================ §5. NULL DEL DE-LEVERING + hr("§5. IL NULL DEL DE-LEVERING — la protezione di TP01 e' altro rispetto a meno leva?") + print(" Il null classico ('esiste k<1 che da' lo stesso rischio con risultato migliore?') qui") + print(" prende la forma giusta per una barriera: **frontiera ISO-SOPRAVVIVENZA**. Per ogni") + print(f" peso si cerca la leva k(w) tale che P(vivo 12m | replay del sinistro PEGGIORE) = " + f"{SURV_TARGET:.0%},") + print(" poi si confronta il PAYOUT atteso sulla finestra normale a quella leva. Se un libro") + print(" con meno TP01 e meno leva paga di piu' a pari sopravvivenza, la protezione di TP01") + print(" e' de-levering; se paga di meno, TP01 compra qualcosa che la leva non compra.") + + _SP: dict = {} + + def p_alive_stress(w: float, lev: float, seg: tuple) -> float: + """P(vivo 12m) col replay del sinistro dentro. Solo `funded_sim`: l'eval non entra + nella definizione della frontiera, e calcolarlo raddoppierebbe il costo per niente.""" + global N_EVAL + N_EVAL += 1 + k = (round(w, 6), seg) + if k not in _SP: + Rp, Gp, _, _ = stress_panel(w) + _SP[k] = (Rp, Gp, splice_paths(base_fu, np.arange(seg[0], seg[1]), seed=8291)) + Rp, Gp, fu = _SP[k] + return PA.funded_sim(Rp, Gp, fu, lev, "HYRO", 100_000.0, "coupled")["p_alive"] + + print(f"\n {'w TP01':>7} {'k iso-surv':>11} {'P(viv|crash)':>13} {'E[pay/a] normale':>17} " + f"{'P(pass) normale':>16} {'J normale':>11}") + front = {} + for w in W_GRID: + Rp, Gp, _, segs = stress_panel(w) + # episodio peggiore per QUESTO peso (il piu' severo, misurato) + worst_seg = min(segs, key=lambda sg: np.prod(1.0 + Rp[sg[0]:sg[1]])) + lo, hi = 0.05, 3.0 + for _ in range(14): + mid = 0.5 * (lo + hi) + if p_alive_stress(w, mid, worst_seg) >= SURV_TARGET: + lo = mid + else: + hi = mid + k = lo + R, G, _ = crypto(w) + off = deluck_offset(w) + o = J_of(R[i_rec:] - off, G[i_rec:], ev_r, fu_r, k) + front[w] = (k, o) + print(f" {w:>7.3f} {k:>11.3f} {p_alive_stress(w, k, worst_seg):>13.1%} " + f"{o['e_payout']:>16,.0f}$ {o['p_pass']:>16.1%} {o['J']:>11.3f}") + kbest = max(front, key=lambda w: front[w][1]["e_payout"]) + jbest = max(front, key=lambda w: front[w][1]["J"]) + print(f"\n argmax a ISO-SOPRAVVIVENZA: payout -> TP01 {kbest:.3f} | J -> TP01 {jbest:.3f}") + print(" ⚠️ La leva k(w) e' tarata SU un episodio che nel campione di ottimizzazione non c'e':") + print(" e' un esercizio con l'informazione del futuro. Serve a rispondere alla domanda del") + print(" filone ('se sapessi che arriva, come mi allocherei?'), NON e' una politica.") + + tick("§5 frontiera iso-sopravvivenza") + + # ============================================================ §6. IL TERZO SLEEVE + hr("§6. XS01 — la parte della domanda che NON e' rispondibile, e perche'") + print(" L'ottimo di PROP-ALLOC mette il 50% su XS01, che **nasce nel 2024**: per lui non") + print(" esiste NESSUN sinistro nel campione. Qui non lo si simula col bootstrap della sua") + print(" finestra (sarebbe assumere che nel crash si comporti come in un giorno normale, che") + print(" e' esattamente l'assunzione sotto esame). Si dichiarano TRE comportamenti e si") + print(" guarda se la conclusione dipende da quale:") + print(" A) FLAT nel crash (il gate di dispersione lo spegne) -> generosa") + print(" B) la sua PEGGIORE finestra di 30g del 2024+, contigua e in ordine -> severa") + print(" C) uno shock una-tantum di taglia X sul giorno peggiore del crash -> si cerca") + print(" la X di ROTTURA, cioe' quanto grande deve essere per portare P(vivo) sotto 50%") + xs = pc.xsec_daily_tuples() + xsR = xs["R"].values + xsG = np.minimum(xs["m"].values, xsR) - xsR + lxs = np.log1p(xsR) + cxs = np.concatenate(([0.0], np.cumsum(lxs))) + wxs30 = np.expm1(cxs[EPI_LEN:] - cxs[:-EPI_LEN]) + t_xw = int(np.argmin(wxs30)) + print(f"\n XS01: {len(xs)} giorni {xs.index[0].date()} -> {xs.index[-1].date()}; peggior " + f"giorno {xsR.min():.2%}, peggior minimo {xs['m'].min():.2%}") + print(f" peggior finestra di {EPI_LEN}g di XS01: {xs.index[t_xw].date()} -> " + f"{xs.index[t_xw+EPI_LEN-1].date()}, cumulato {wxs30[t_xw]:.1%} " + f"(ipotesi B). Per confronto la coppia crypto nel COVID fa -5.4% a TP01 pieno.") + print(" 📌 Il numero che conta e' questo: **la peggior finestra di 30g mai vista da XS01 vale") + print(f" {wxs30[t_xw]:.1%}**. Se nel crash XS01 facesse cosi', al 50% di peso porterebbe") + print(f" {0.5*wxs30[t_xw]:.1%} al libro. La domanda non e' se XS01 sopravvive a cio' che ha") + print(" gia' visto, e' se il suo comportamento nel crash somiglia a cio' che ha gia' visto —") + print(" e su questo il campione non ha NIENTE da dire.") + W3 = [(0.75, 0.25, 0.00), (0.375, 0.625, 0.00), (0.25, 0.25, 0.50), (0.125, 0.125, 0.75), + (0.50, 0.25, 0.25)] + com = xs.index.intersection(IDX[i_rec:]) + xs_al = xs.reindex(com) + print(f"\n {'TP/SKH/XS':>16} {'ipotesi XS':>12} {'leva':>6} {'J normale':>10} " + f"{'P(viv|replay peggiore)':>24} {'shock di rottura':>18}") + for cfg in W3: + wtp, wsk, wxs = cfg + base = wtp + wsk + wtp_n = wtp / base if base > 0 else 0.0 + Rc, Gc, Ic = crypto(wtp_n) + offc = deluck_offset(wtp_n) + m1 = Ic.isin(com) + Rn = base * (Rc[m1] - offc) + wxs * xs_al["R"].values + Gn = base * Gc[m1] + wxs * (np.minimum(xs_al["m"].values, xs_al["R"].values) + - xs_al["R"].values) + nn = len(Rn) + evn = PA.boot_idx(nn, HORIZON, N_PATHS, seed=8300) + fun = PA.boot_idx(nn, HORIZON, N_PATHS, seed=8301) + lv, on = best_lev(Rn, Gn, evn, fun) + bfu = PA.boot_idx(nn, HORIZON, N_PATHS, seed=8302) + lab = f"{int(round(wtp*100))}/{int(round(wsk*100))}/{int(round(wxs*100))}" + + def build(hyp: str, shock: float = 0.0): + segsR, segsG = [], [] + for t in epis: + rc = base * (crypto(wtp_n)[0][t:t + EPI_LEN] - offc) + gc = base * crypto(wtp_n)[1][t:t + EPI_LEN] + if wxs > 0: + if hyp == "A-flat": + rx = np.zeros(EPI_LEN); gx = np.zeros(EPI_LEN) + elif hyp == "B-peggiore": + rx = xsR[t_xw:t_xw + EPI_LEN].copy() + gx = xsG[t_xw:t_xw + EPI_LEN].copy() + else: + rx = np.zeros(EPI_LEN); gx = np.zeros(EPI_LEN) + rx[int(np.argmin(rc))] = -shock + segsR.append(rc + wxs * rx); segsG.append(gc + wxs * gx) + else: + segsR.append(rc); segsG.append(gc) + Rp = np.concatenate([Rn] + segsR) + Gp = np.concatenate([Gn] + segsG) + sg = [(nn + k * EPI_LEN, nn + (k + 1) * EPI_LEN) for k in range(len(epis))] + worst = 1.0 + for k, g2 in enumerate(sg): + fu = splice_paths(bfu, np.arange(g2[0], g2[1]), seed=8320 + k) + worst = min(worst, J_of(Rp, Gp, evn, fu, lv)["p_alive"]) + return worst + + if wxs == 0: + print(f" {lab:>16} {'n/a (no XS)':>12} {lv:>6.3f} {on['J']:>10.3f} " + f"{build('A-flat'):>24.1%} {'-':>18}") + continue + for hyp in ("A-flat", "B-peggiore"): + print(f" {lab:>16} {hyp:>12} {lv:>6.3f} {on['J']:>10.3f} " + f"{build(hyp):>24.1%} {'-':>18}") + lo, hi = 0.0, 0.40 # shock di rottura: bisezione su P(vivo) = 50% + for _ in range(9): + mid = 0.5 * (lo + hi) + if build("C-shock", mid) >= 0.50: + lo = mid + else: + hi = mid + print(f" {lab:>16} {'C-shock':>12} {lv:>6.3f} {on['J']:>10.3f} {'(50% per def.)':>24} " + f"{-lo:>17.1%}") + print("\n Lettura: 'shock di rottura' = il crollo di UN giorno su XS01 che porta P(vivo) al") + print(" 50% quando il crash arriva. Piu' e' piccolo, piu' l'allocazione XS01-pesante e' a un") + print(" giorno di coda dalla morte. Non e' una previsione: e' la taglia della scommessa.") + tick("§6 XS01") + + # ============================================================ §7. BANDA + VERDETTO + hr("§7. SENSIBILITA' AL DE-LUCK — l'ORDINE fra i pesi sopravvive alla banda?") + print(" La banda d'ancora non e' ri-misurabile sotto questa lente (costo, dichiarato anche da") + print(" PROP-ALLOC). Si applica il fattore misurato x0.89 e si controlla l'unica cosa che") + print(" conta: se l'argmax sui pesi si muove fra 1.00 (nessuna correzione) e 0.60 (la stima") + print(" a occhio del 25/07, poi ri-misurata troppo severa).") + print(f"\n {'fattore':>9} {'argmax 2019+':>14} {'argmax 2024+':>14} {'argmax SOLO-SIN':>16} " + f"{'J(75/25) 2019+':>16} {'J(38/62) 2019+':>16}") + for fct in (1.00, 0.89, 0.60): + best_f = best_r = best_s = None + vals = {} + for w in W_GRID: + R, G, _ = crypto(w) + o = deluck_offset(w, fct) + Rf = R - o + j1 = best_lev(Rf, G, ev_f, fu_f)[1]["J"] + j2 = best_lev(Rf[i_rec:], G[i_rec:], ev_r, fu_r)[1]["J"] + j3 = best_lev(Rf, G, ev_s, fu_s)[1]["J"] + vals[w] = (j1, j2, j3) + best_f = max(vals, key=lambda w: vals[w][0]) + best_r = max(vals, key=lambda w: vals[w][1]) + best_s = max(vals, key=lambda w: vals[w][2]) + print(f" {fct:>9.2f} {best_f:>14.3f} {best_r:>14.3f} {best_s:>16.3f} " + f"{vals[0.75][0]:>16.3f} {vals[0.375][0]:>16.3f}") + + hr("§8. I GATE — cosa e' girato, cosa NON e' girato e perche'") + print(" Questo filone non propone una strategia nuova: propone (o rifiuta) un'ALLOCAZIONE per") + print(" un conto che non esiste. I gate del progetto vanno dichiarati uno per uno.") + try: + sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + import altlib as A # noqa: E402 + for lab, w in (("LIVE 75/25", 0.75), ("OPT 38/62", 0.375)): + R = crypto(w)[0] - deluck_offset(w) + d = pd.Series(R, index=IDX) + n_act = int((np.abs(R) > 1e-12).sum()) + n_los = int((R[np.abs(R) > 1e-12] < 0).sum()) + f = A.implausible_sharpe(d, n_trades=n_act, n_losing_trades=n_los) + print(f" implausible_sharpe {lab:>11}: {f}") + except Exception as exc: # pragma: no cover + print(f" implausible_sharpe: NON GIRATO ({exc})") + print(" marginal_vs_tp01 / study_family_honest / deflated_sharpe : **NON GIRATI, e non") + print(" sono applicabili** — non c'e' uno stream candidato da confrontare con TP01: gli") + print(" stream sono i due sleeve gia' ammessi, e l'oggetto e' il loro PESO. Il gate dei") + print(" pesi del progetto e' `weights_tilt_null`, che pero' giudica lo SHARPE del libro") + print(" di RICERCA: qui l'obiettivo e' J su una barriera, e il conto e' separato.") + print(" (PROP-ALLOC lo ha girato: gate_pass False. Non cambia nulla qui: nessuna") + print(" proposta di questo file tocca il libro live.)") + print(" causality_ok : **non girato, non applicabile** — nessun segnale nuovo; le serie") + print(" sono quelle di produzione, gia' verificate leak-free a monte.") + print(" anchor_luck_band : **non ri-misurata** (costo, come in PROP-ALLOC). Si e' girata") + print(" la sensibilita' al FATTORE di de-luck (§7), che e' meno: dice se l'ORDINE regge,") + print(" non qual e' la banda. Distorsione dichiarata: la lente e' l'ancora CANONICA e") + print(" SKH01 e' lo sleeve con la fortuna d'ancora piu' grande da restituire (26/07)") + print(" -> **la distorsione e' A FAVORE della conclusione di questo file**, cioe' del") + print(" peso basso su TP01. E' il caveat piu' importante del filone.") + print(" null del de-levering : **GIRATO** (§5, frontiera iso-sopravvivenza) — e superato") + print(" NEL VERSO OPPOSTO a quello atteso: e' la protezione di TP01 a essere replicabile") + print(" con meno leva, e meglio.") + + hr("§9. LA RISPOSTA OPERATIVA") + print(f" valutazioni MC totali di questo file: **{N_EVAL}** " + f"(ognuna = {N_PATHS} percorsi di eval + {N_PATHS} di funded)") + print(f"\n {'lente':>26} {'argmax w TP01':>14} {'J argmax':>10} {'J a 0.75':>10} " + f"{'J a 0.375':>11}") + for lab, key in (("2019+ (contiene il 2022)", "full"), ("2024+ (PROP-ALLOC)", "rec"), + ("SOLO-SINISTRO", "sin")): + am = max(tab, key=lambda w: tab[w][key][1]["J"]) + print(f" {lab:>26} {am:>14.3f} {tab[am][key][1]['J']:>10.3f} " + f"{tab[0.75][key][1]['J']:>10.3f} {tab[0.375][key][1]['J']:>11.3f}") + print("\n" + "=" * 106) + print(f" fatto in {time.time()-t0:.0f}s | valutazioni: {N_EVAL}") + print("=" * 106) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822b_xs01_oos.py b/scripts/research/r0822b_xs01_oos.py new file mode 100644 index 0000000..be1b40c --- /dev/null +++ b/scripts/research/r0822b_xs01_oos.py @@ -0,0 +1,683 @@ +"""r0822b_xs01_oos.py — XS01 FUORI dalla sua finestra di scoperta (2026-08-22, ondata wave-0822). + +IL PROBLEMA (filone PROP-ALLOC, RESULTS-0822 §1) +------------------------------------------------ +Il risultato piu' vicino all'obiettivo dell'operatore e': su un conto FUNDED da $100k, aggiungere +XS01 al libro porta J = P(pass) x P(vivo 12m) da 0,335 a 0,738. Ma **tutto** quel vantaggio poggia +sul drift di XS01 misurato sulla SUA STESSA finestra di scoperta (Hyperliquid, 2024-01 -> oggi, +~2,6 anni, e senza un solo mercato orso). Azzerando quel drift l'uplift scende a +0,038; sotto +stress congiunto diventa negativo. Il gate proposto era "non aprire un funded finche' XS01 non ha +una finestra fuori dal 2024-2026" = ANNI di attesa. + +LA DOMANDA DI QUESTO FILONE +--------------------------- +Si puo' testare il MECCANISMO CONGELATO su una storia PIU' LUNGA e INDIPENDENTE invece di aspettare? +La risposta e' si', e il vincolo non e' il dato ma il CALENDARIO DELLE QUOTAZIONI: 6 dei 19 asset +(APT, OP, ARB, SUI, SEI, TIA) non esistono prima del 2022-06/2023-10. Il piu' vecchio universo +>= 10 asset dei 19 e' del **2020-10** -> circa **3 anni interi fuori dalla finestra di scoperta, +2021-01 -> 2023-12**, che contengono il top 2021, il crollo di maggio 2021, **LUNA e FTX (2022)** e +la ripresa 2023. E' esattamente il regime che manca al campione di scoperta. + +IL DATO: BINANCE, E PERCHE' L'OBIEZIONE DEL PROGETTO NON MORDE QUI +----------------------------------------------------------------- +CLAUDE.md: "Binance NON e' la verita' (e' USDT, ~10 bps fuori dal consenso, e sotto depeg fino al +3%) -> usare SOLO come audit indipendente, mai come ancora per ripulire i dati". Quella regola +riguarda l'uso come **ANCORA DI PREZZO** (livello). Qui Binance serve come **audit indipendente per +un segnale CROSS-SECTIONAL DI RANKING su barre giornaliere**, e i tre difetti citati sono tutti +**fattori COMUNI alla valuta di quotazione**: + - uno scarto costante di ~10 bps sul livello e' identico su tutti e 19 gli asset -> sparisce + ESATTAMENTE nello z-score cross-sectional (che sottrae la media di sezione); + - un depeg USDT muove tutte le gambe insieme -> non cambia il RANKING, e il libro e' + dollar-neutral (0.5/k long, -0.5/k short) quindi il fattore comune si cancella anche nel P&L; + - a 1d un errore di livello di 10 bps e' 1/50 della vol giornaliera tipica di un alt. +NON e' un argomento teorico: e' MISURATO qui sotto (sezione 3) confrontando il meccanismo su +Binance e su Hyperliquid nella finestra in cui esistono ENTRAMBI (2024+). +E i dati scaricati sono CERTIFICATI comunque (barre flat, gap di calendario, volumi zero = +backfill sintetico: il progetto ci e' gia' cascato una volta, correzione AXS del 2026-06-20). + +MECCANISMO: CONGELATO. Nessun parametro toccato. E' `src/portfolio/sleeves.py::_xsec_returns` +(lookbacks (30,90), H=10, k=5, mode="mom", target_vol=0.20, gate dispersione p30 espandente +causale, fee 5 bps/lato, vol-target su rv30 shiftata, clip leva 3x). Il motore qui sotto e' una +versione MASCHERATA (regge NaN = asset non ancora quotato) e la sezione 2 dimostra che riproduce +lo sleeve ufficiale a **max|diff| = 0.0** sui dati HL. + +ESITO (2026-08-22, corsa completa 78 s) +--------------------------------------- +- Il motore replica lo sleeve ufficiale a max|diff| = 0.0; Binance e Hyperliquid danno lo STESSO + sleeve sulla finestra comune (corr 0.9991, dSharpe -0.025) -> la storia lunga e' utilizzabile. +- Fuori dalla finestra di scoperta (2021-01 -> 2023-12, contiene LUNA e FTX) il meccanismo ha un + edge: Sharpe MEDIANO di fase **+1.12** (U13 fisso) / **+1.17** (universo espandente), positivo in + **10/10** fasi, p = 0.013 contro il null di permutazione cross-sezionale a fee zero. +- **Il fuori campione e' MIGLIORE della finestra di scoperta**, a parita' di universo: differenza + appaiata per fase +0.668 (10/10 fasi). L'ipotesi "il drift di XS01 e' un artefatto del 2024-2026" + e' REFUTATA per la parte testabile del sleeve. +- MA: il maxDD raddoppia (10.8% nella scoperta -> 20.9-22.0% mediano fuori), il 2021 vale meta' + dell'edge, una gamba sola (LTC) ne vale 0.9 di Sharpe, e i due affinamenti del 2026-06-19 non si + replicano. E il deflated-Sharpe FALLISCE (0.342-0.418). +- Sul LIBRO, fuori campione: TP01+SKH01 75/25 Sharpe +1.17 -> **+1.65** e maxDD 9.4% -> **6.0%** + a peso XS01 0.25; a peso **0.50** (l'argmax di PROP-ALLOC) il maxDD risale a **8.9%**. + +Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822b_xs01_oos.py +Rete: solo lettura, <= 2 req/s, MAI nella finestra :25-:29 (rate limit per-IP condiviso col +collettore della catena). Il download e' messo in cache nello scratchpad: la seconda corsa e' +offline. +""" +from __future__ import annotations + +import math +import os +import sys +import time +from datetime import datetime, timezone +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +import altlib as A # noqa: E402 +from src.portfolio.sleeves import XS_CFG, XS_UNIVERSE # noqa: E402 + +CACHE = Path(os.environ.get( + "R0822B_CACHE", + "/tmp/claude-1001/-opt-docker-PythagorasGoal/b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/r0822b", +)) +CACHE.mkdir(parents=True, exist_ok=True) + +RAW = ROOT / "data" / "raw" +FEE_SIDE = 0.001 / 2.0 # identica al sleeve congelato +DISCOVERY = pd.Timestamp("2024-01-01", tz="UTC") # inizio della finestra di scoperta (HL) + +# I 13 dei 19 quotati su Binance prima del 2020-11 -> l'universo FISSO piu' lungo possibile +# che rispetta il vincolo del meccanismo congelato (>= 10 gambe). +U13 = ["BTC", "ETH", "BNB", "XRP", "LTC", "ADA", "LINK", "DOGE", "SOL", "AVAX", "NEAR", "AAVE", "INJ"] +SECTORS = ["xlb", "xlc", "xle", "xlf", "xli", "xlk", "xlp", "xlre", "xlu", "xlv", "xly"] + + +# =========================================================================== +# 0. disciplina di rete +# =========================================================================== +def _outside_forbidden_window() -> None: + """Il collettore della catena gira al minuto :25 e satura il rate limit per-IP di Deribit; + la regola d'ondata e' non toccare la rete fra :25 e :29. Qui parliamo con Binance, ma la + disciplina e' della MACCHINA (2 core, libro con soldi veri), non del venue.""" + while True: + m = datetime.now(timezone.utc).minute + if not (25 <= m <= 29): + return + print(f" [rete] minuto :{m:02d} nella finestra vietata :25-:29 — attendo 30s", flush=True) + time.sleep(30) + + +# =========================================================================== +# 1. FETCH + CERTIFY (Binance spot USDT, 1d) +# =========================================================================== +def fetch_binance_daily(sym: str, since_iso: str = "2017-01-01T00:00:00Z") -> pd.DataFrame: + f = CACHE / f"bin_{sym.lower()}_1d.parquet" + if f.exists(): + return pd.read_parquet(f) + import ccxt + ex = ccxt.binance({"enableRateLimit": True}) + since = ex.parse8601(since_iso) + rows, cur = [], since + while True: + _outside_forbidden_window() + batch = ex.fetch_ohlcv(f"{sym}/USDT", "1d", since=cur, limit=1000) + time.sleep(0.5) # <= 2 req/s + if not batch: + break + rows.extend(batch) + if len(batch) < 1000: + break + cur = batch[-1][0] + 86_400_000 + d = pd.DataFrame(rows, columns=["timestamp", "open", "high", "low", "close", "volume"]) + d = d.drop_duplicates("timestamp").sort_values("timestamp").reset_index(drop=True) + d.to_parquet(f) + return d + + +def certify(sym: str, d: pd.DataFrame) -> dict: + """Certificazione locale, stessa forma di scripts/analysis/fetch_hyperliquid.py. + Il rivelatore del backfill SINTETICO NON e' il prezzo (cerbero copiava i prezzi da un altro + venue) ma il **VOLUME**: un run di barre a volume 0 e' padding, non mercato.""" + t = pd.to_datetime(d["timestamp"], unit="ms", utc=True) + c = d["close"].values.astype(float) + v = d["volume"].values.astype(float) + span = (t.max() - t.min()).days + 1 + flat = float(np.mean(c[1:] == c[:-1])) if len(c) > 1 else float("nan") + zero_v = float(np.mean(v == 0.0)) + # run iniziale a volume zero = firma del padding sintetico (lezione AXS 2026-06-20) + lead0 = int(np.argmax(v > 0)) if (v > 0).any() else len(v) + return dict(sym=sym, n=len(d), start=str(t.min().date()), end=str(t.max().date()), + gap_days=int(span - len(d)), flat=flat, zero_vol=zero_v, lead_zero_vol=lead0, + ok=bool(span - len(d) == 0 and flat < 0.02 and zero_v < 0.01 and lead0 == 0)) + + +def load_panel(syms: list[str]) -> pd.DataFrame: + """Pannello dei CLOSE, indice giornaliero UTC, NaN dove l'asset non e' ancora quotato.""" + cols = {} + for s in syms: + d = fetch_binance_daily(s) + cols[s] = pd.Series(d["close"].values.astype(float), + index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)) + return pd.concat(cols, axis=1).sort_index() + + +def load_sector_panel() -> pd.DataFrame: + cols = {} + for s in SECTORS: + d = pd.read_parquet(RAW / f"eq_{s}_1d.parquet") + cols[s.upper()] = pd.Series(d["close"].values.astype(float), + index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)) + return pd.concat(cols, axis=1).sort_index() + + +# =========================================================================== +# 2. MOTORE — meccanismo CONGELATO, versione mascherata (regge i NaN) +# =========================================================================== +def xsec_engine(P: pd.DataFrame, phase: int = 0, fee_side: float = FEE_SIDE, + mode: str | None = None, min_assets: int = 10, + perm_rng: np.random.Generator | None = None, + perm_kind: str = "cross", lookbacks=None, disp_pct=None, + detail: dict | None = None) -> pd.Series: + """Riproduce `sleeves._xsec_returns` riga per riga. Unica differenza: l'insieme ammissibile + e' calcolato PER RIBILANCIAMENTO (un asset entra quando ha almeno max(lookbacks) barre), cosi' + il meccanismo si puo' applicare a un universo che si allarga nel tempo — che e' cio' che un + implementatore in tempo reale avrebbe avuto. Con un pannello senza NaN, e' identico all'originale. + + `perm_rng` attiva un NULL: 'cross' = permuta lo score FRA GLI ASSET al ribilanciamento + (distrugge l'informazione, conserva ESATTAMENTE la struttura di posizione e di turnover), + 'sign' = pesi casuali fra gli ammissibili (stesso gross, stesso k).""" + px = P.values.astype(float) + n, Ncols = px.shape + H, k = XS_CFG["H"], XS_CFG["k"] + lookbacks = tuple(XS_CFG["lookbacks"] if lookbacks is None else lookbacks) + disp_pct = XS_CFG["disp_pct"] if disp_pct is None else disp_pct + tv, minhist = XS_CFG["target_vol"], XS_CFG["disp_minhist"] + mode = mode or XS_CFG["mode"] + mlb = max(lookbacks) + + with np.errstate(invalid="ignore", divide="ignore"): + dret = np.vstack([np.zeros(Ncols), px[1:] / px[:-1] - 1.0]) + dret = np.nan_to_num(dret, nan=0.0, posinf=0.0, neginf=0.0) + + W = np.zeros((n, Ncols)); w = np.zeros(Ncols); disp_hist: list[float] = [] + n_rebal = n_traded = 0 + for i in range(n): + if i >= mlb and (i % H) == phase: + elig = np.isfinite(px[i]) + for L in lookbacks: + elig = elig & np.isfinite(px[i - L]) + m = int(elig.sum()) + if m >= min_assets: + n_rebal += 1 + idx = np.flatnonzero(elig) + rLs = [px[i, idx] / px[i - L, idx] - 1.0 for L in lookbacks] + disp_i = float(np.mean([r.std() for r in rLs])) + thr = (np.percentile(disp_hist, disp_pct) + if (disp_pct > 0 and len(disp_hist) >= minhist) else -np.inf) + if disp_i >= thr: + score = np.zeros(m); cnt = 0 + for rL in rLs: + sd = rL.std() + if sd > 0: + score += (rL - rL.mean()) / sd; cnt += 1 + if cnt: + score /= cnt + w = np.zeros(Ncols) + if perm_rng is not None and perm_kind == "sign": + pick = perm_rng.permutation(m) + lo, hi = idx[pick[:k]], idx[pick[-k:]] + else: + if perm_rng is not None: # perm_kind == "cross" + score = score[perm_rng.permutation(m)] + order = np.argsort(score) + lo, hi = idx[order[:k]], idx[order[-k:]] + if mode == "mom": + w[hi] = 0.5 / k; w[lo] = -0.5 / k + else: + w[lo] = 0.5 / k; w[hi] = -0.5 / k + n_traded += 1 + else: + w = np.zeros(Ncols) + disp_hist.append(disp_i) + W[i] = w + + gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1) + turn = np.zeros(n); turn[0] = np.abs(W[0]).sum() + turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1) + net = gross - turn * fee_side + s = pd.Series(net, index=P.index) + rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25) + scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0) + out = pd.Series(s.values * scale, index=P.index) + out.attrs["n_rebal"] = n_rebal + out.attrs["n_traded"] = n_traded + if detail is not None: + detail.update(W=W, scale=scale, turn=turn, index=P.index, + n_rebal=n_rebal, n_traded=n_traded) + return out + + +def hl_panel(syms=None) -> pd.DataFrame: + """Pannello Hyperliquid, ESATTAMENTE come lo costruisce lo sleeve congelato.""" + cols = {} + for sym in (syms or XS_UNIVERSE): + p = RAW / f"hl_{sym.lower()}_1d.parquet" + if not p.exists(): + continue + d = pd.read_parquet(p) + cols[sym] = pd.Series(d["close"].values.astype(float), + index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)) + return pd.concat(cols, axis=1, join="inner").sort_index().dropna() + + +# =========================================================================== +# metriche +# =========================================================================== +def sh(s: pd.Series) -> float: + return A._sh(s) + + +def dd(s: pd.Series) -> float: + return A._dd_ret(s) + + +def cagr(s: pd.Series) -> float: + r = s.dropna() + if len(r) < 30: + return float("nan") + yrs = (r.index[-1] - r.index[0]).days / 365.25 + return float(np.prod(1.0 + r.values) ** (1.0 / yrs) - 1.0) if yrs > 0 else float("nan") + + +def win(s: pd.Series, a: str | None = None, b: str | None = None) -> pd.Series: + x = s + if a: + x = x[x.index >= pd.Timestamp(a, tz="UTC")] + if b: + x = x[x.index < pd.Timestamp(b, tz="UTC")] + return x + + +def alive(s: pd.Series) -> pd.Series: + """Taglia il prefisso di ZERI (warm-up dei lookback + attesa che l'universo raggiunga 10 + gambe). ⚠️ Bug catturato nella prima corsa: sull'universo ESPANDENTE il pannello parte dal + 2017 (BTC) ma il meccanismo non puo' operare prima del 2020-12, e i ~1.200 zeri iniziali + entravano nella finestra 'PRE-2024' abbassandone lo Sharpe e ribaltando la differenza + appaiata. E' la lezione gia' codificata nel progetto (barre ATTIVE, non di calendario), + ripetuta in un contesto nuovo.""" + v = np.asarray(s.values, float) + nz = np.flatnonzero(v != 0.0) + return s.iloc[nz[0]:] if len(nz) else s + + +def line(tag: str, s: pd.Series) -> str: + return (f"{tag:<22s} n={len(s):>5d} Sh {sh(s):+6.2f} maxDD {dd(s)*100:5.1f}% " + f"CAGR {cagr(s)*100:+6.2f}% vol {s.std()*np.sqrt(365.25)*100:5.1f}%") + + +# =========================================================================== +def main() -> None: + t0 = time.time() + print("=" * 100) + print("XS01 FUORI DALLA FINESTRA DI SCOPERTA — meccanismo CONGELATO su storia lunga indipendente") + print(f"XS_CFG = {XS_CFG}") + print("=" * 100) + + # ---------------------------------------------------------------- 1. dato + print("\n[1] DATO — Binance spot USDT 1d, certificazione locale") + panel19 = load_panel(XS_UNIVERSE) + cert = [certify(s, fetch_binance_daily(s)) for s in XS_UNIVERSE] + bad = [c for c in cert if not c["ok"]] + print(f" {len(cert)} serie scaricate; certificate OK: {len(cert)-len(bad)}/{len(cert)}") + print(f" {'sym':<6}{'n':>6}{'start':>12}{'end':>12}{'gap':>5}{'flat%':>8}{'vol0%':>8}{'lead0':>7}") + for c in cert: + flag = "" if c["ok"] else " <-- CONTROLLA" + print(f" {c['sym']:<6}{c['n']:>6}{c['start']:>12}{c['end']:>12}{c['gap_days']:>5}" + f"{c['flat']*100:>8.2f}{c['zero_vol']*100:>8.2f}{c['lead_zero_vol']:>7}{flag}") + print(" NB: 'lead0' = barre iniziali a volume 0 = firma del backfill sintetico (lezione AXS).") + + # ---------------------------------------------------- 2. replica bit-exact + print("\n[2] SANITY — il motore riproduce lo sleeve UFFICIALE?") + from src.portfolio.sleeves import _xsec_returns + off = _xsec_returns() + rep = xsec_engine(hl_panel(), phase=0) + j = pd.concat({"off": off, "rep": rep}, axis=1, join="inner").dropna() + md = float(np.max(np.abs(j["off"] - j["rep"]))) if len(j) else float("nan") + print(f" n={len(j)} max|diff| = {md:.2e} -> {'IDENTICO' if md < 1e-12 else 'DIVERGE'}") + print(f" {line('XS01 ufficiale (HL)', off)}") + if md >= 1e-12: + print(" !! il motore NON replica: ogni numero sotto e' sospetto. STOP.") + return + + # ------------------------------------------------ 3. validazione di VENUE + print("\n[3] VALIDAZIONE DI VENUE — Binance vs Hyperliquid dove esistono ENTRAMBI (2024+)") + hlp = hl_panel() + b19 = panel19.reindex(columns=XS_UNIVERSE) + common = b19.index.intersection(hlp.index) + rr = [] + for s in XS_UNIVERSE: + a1 = hlp[s].reindex(common).pct_change() + b1 = b19[s].reindex(common).pct_change() + k2 = pd.concat({"a": a1, "b": b1}, axis=1).dropna() + rr.append((s, float(k2["a"].corr(k2["b"])), float(np.median(np.abs(k2["a"] - k2["b"]))) * 1e4)) + print(f" ritorni giornalieri per asset, {len(common)} giorni comuni:") + print(" " + " ".join(f"{s}:{c:.4f}" for s, c, _ in rr)) + print(f" corr min {min(c for _, c, _ in rr):.4f} | mediana |diff| ritorno: " + f"{np.median([m for _, _, m in rr]):.1f} bps (max {max(m for _, _, m in rr):.1f} bps)") + # e soprattutto: il MECCANISMO da' la stessa cosa? + bin_on_hl_window = xsec_engine(b19.loc[common].dropna(), phase=0) + hl_on_window = xsec_engine(hlp.loc[common], phase=0) + jj = pd.concat({"bin": bin_on_hl_window, "hl": hl_on_window}, axis=1, join="inner").dropna() + print(f" {line('XS01 su HL 2024+', jj['hl'])}") + print(f" {line('XS01 su BINANCE 2024+', jj['bin'])}") + print(f" corr fra le DUE VERSIONI DELLO SLEEVE: {jj['bin'].corr(jj['hl']):.4f} " + f"dSharpe = {sh(jj['bin'])-sh(jj['hl']):+.3f}") + print(" -> se questa corr e' ~1, la storia lunga Binance e' un'estensione legittima:") + print(" lo scarto USDT e' un FATTORE COMUNE e sparisce nello z-score cross-sectional.") + + # ------------------------------------- 4. la storia lunga, fuori campione + print("\n[4] STORIA LUNGA — meccanismo congelato, ancora canonica (fase 0)") + p13 = panel19[U13].dropna() # universo FISSO 13 (il piu' lungo a >=10 gambe) + s13 = xsec_engine(p13, phase=0) + sEXP = xsec_engine(panel19, phase=0) # universo che si ALLARGA (19 quando esistono) + s13, sEXP = alive(s13), alive(sEXP) + print(f" U13-FIX universo fisso {len(U13)} gambe, dal {s13.index[0].date()} " + f"({s13.attrs.get('n_traded')}/{s13.attrs.get('n_rebal')} ribilanciamenti con posizione)") + print(f" U19-EXP universo espandente (10 -> 19 gambe), dal {sEXP.index[0].date()}") + for tag, s in (("U13-FIX", s13), ("U19-EXP", sEXP)): + print(f" {line(tag + ' FULL', s)}") + print(f" {line(tag + ' PRE-2024 (OOS)', win(s, None, '2024-01-01'))}") + print(f" {line(tag + ' 2024+ (scoperta)', win(s, '2024-01-01'))}") + + print("\n Sharpe per ANNO (netto fee):") + yrs = sorted({d.year for d in sEXP.index}) + hdr = " " + f"{'':<10}" + "".join(f"{y:>9d}" for y in yrs) + print(hdr) + for tag, s in (("U13-FIX", s13), ("U19-EXP", sEXP), ("XS01 HL", off)): + cells = [] + for y in yrs: + x = win(s, f"{y}-01-01", f"{y+1}-01-01") + cells.append(f"{sh(x):>9.2f}" if len(x) > 60 else f"{'—':>9s}") + print(f" {tag:<10}" + "".join(cells)) + + print("\n Sharpe per REGIME:") + regimes = [("2021 toro/top", "2021-01-01", "2022-01-01"), + ("2022 ORSO (LUNA/FTX)", "2022-01-01", "2023-01-01"), + ("2023 ripresa", "2023-01-01", "2024-01-01"), + ("2024-26 scoperta", "2024-01-01", None)] + for nm, a, b in regimes: + x13, xEX = win(s13, a, b), win(sEXP, a, b) + print(f" {nm:<24s} U13 Sh {sh(x13):+6.2f} DD {dd(x13)*100:5.1f}% | " + f"U19-EXP Sh {sh(xEX):+6.2f} DD {dd(xEX)*100:5.1f}%") + + # -------------------------------------------- 5. fortuna di FASE (H = 10) + print("\n[5] FORTUNA DI FASE — H=10, tutte e 10 le ancore (la stima onesta e' la MEDIANA)") + phases = list(range(XS_CFG["H"])) + bands = {} + for tag, P, cut in (("U13-FIX FULL", p13, None), ("U13-FIX PRE-2024", p13, "pre"), + ("U19-EXP FULL", panel19, None), ("U19-EXP PRE-2024", panel19, "pre")): + def f(ph, P=P, cut=cut): + s = alive(xsec_engine(P, phase=ph)) + return win(s, None, "2024-01-01") if cut == "pre" else s + bands[tag] = A.anchor_luck_band(f, phases, canonical=0, metric=sh) + bands[tag + "|DD"] = A.anchor_luck_band(f, phases, canonical=0, metric=lambda x: -dd(x)) + for tag in ("U13-FIX FULL", "U13-FIX PRE-2024", "U19-EXP FULL", "U19-EXP PRE-2024"): + r = bands[tag]; rd = bands[tag + "|DD"] + print(f" {tag:<20s} Sh canonico {r['canonical']:+.2f} (pctl {r['canonical_pctl']*100:4.0f}°) " + f"| MEDIANA {r['median']:+.2f} | banda [{r['lo']:+.2f},{r['hi']:+.2f}] " + f"| fasi>0 {r['frac_positive']*100:3.0f}% | fortuna {r['luck']:+.2f} | gate {r['gate_pass']}") + print(f" {'':<20s} maxDD canonico {-rd['canonical']*100:5.1f}% " + f"| MEDIANA {-rd['median']*100:5.1f}% | banda [{-rd['hi']*100:.1f},{-rd['lo']*100:.1f}]%") + # anche lo sleeve UFFICIALE su HL, de-luckato di fase (per confronto onesto) + bhl = A.anchor_luck_band(lambda ph: xsec_engine(hlp, phase=ph), phases, canonical=0, metric=sh) + print(f" {'XS01 UFFICIALE (HL)':<20s} Sh canonico {bhl['canonical']:+.2f} " + f"(pctl {bhl['canonical_pctl']*100:4.0f}°) | MEDIANA {bhl['median']:+.2f} " + f"| banda [{bhl['lo']:+.2f},{bhl['hi']:+.2f}] | fortuna {bhl['luck']:+.2f}") + + # -------------- 6. differenza APPAIATA fuori campione vs finestra di scoperta + print("\n[6] DIFFERENZA APPAIATA per fase: (fuori campione) - (finestra di scoperta)") + for tag, P in (("U13-FIX", p13), ("U19-EXP", panel19)): + def pre(ph, P=P): + return win(alive(xsec_engine(P, phase=ph)), None, "2024-01-01") + + def post(ph, P=P): + return win(alive(xsec_engine(P, phase=ph)), "2024-01-01") + d1 = A.anchor_luck_delta(pre, post, phases, metric=sh) + print(f" {tag:<10s} mediana DIFF appaiata {d1['median_paired']:+.3f} " + f"[{d1['lo']:+.3f},{d1['hi']:+.3f}] fasi con OOS>scoperta {d1['n_positive']}/{d1['n_anchors']}") + + # ------------------------------------------------------------- 7. i NULL + print("\n[7] NULL — a FEE ZERO (permutare un segnale ne fa esplodere il turnover: a fee piene") + print(" il null perderebbe per COSTO invece che per assenza di informazione — regola XSR01)") + NDRAW = 300 + for tag, P, a, b in (("U13-FIX PRE-2024", p13, None, "2024-01-01"), + ("U13-FIX FULL", p13, None, None)): + real = win(alive(xsec_engine(P, phase=0, fee_side=0.0)), a, b) + for kind in ("cross", "sign"): + rng = np.random.default_rng(20260822) + draws = np.array([sh(win(alive(xsec_engine(P, phase=0, fee_side=0.0, perm_rng=rng, + perm_kind=kind)), a, b)) for _ in range(NDRAW)]) + p = float(np.mean(draws >= sh(real))) + print(f" {tag:<20s} null '{kind}': reale(lordo) {sh(real):+.2f} vs null " + f"med {np.median(draws):+.2f} max {draws.max():+.2f} -> p = {p:.4f}") + + print("\n NULL 'universo dove NON dovrebbe funzionare' — 11 settoriali SPDR, 1998+,") + print(" stesso meccanismo congelato (il progetto ha gia' misurato che il cross-sectional") + print(" crypto non generalizza alle azioni: XSR01, 2026-07-26):") + sect = load_sector_panel() + ssec = alive(xsec_engine(sect, phase=0, min_assets=9)) + bsec = A.anchor_luck_band(lambda ph: alive(xsec_engine(sect, phase=ph, min_assets=9)), phases, + canonical=0, metric=sh) + print(f" {line('SECT11 (azioni)', ssec)}") + print(f" SECT11 banda di fase: mediana Sh {bsec['median']:+.2f} " + f"[{bsec['lo']:+.2f},{bsec['hi']:+.2f}], fasi>0 {bsec['frac_positive']*100:.0f}%") + + print("\n CONTROLLO DI DIREZIONE — stesso meccanismo con mode='rev' (long i deboli):") + for tag, P in (("U13-FIX", p13), ("U19-EXP", panel19)): + r = alive(xsec_engine(P, phase=0, mode="rev")) + print(f" {line(tag + ' REV', r)}") + + # ------------- 7b. la SELEZIONE del 2026-06-19, rivalidata FUORI CAMPIONE + print("\n[7b] LA SELEZIONE DEL 2026-06-19, RIVALIDATA FUORI CAMPIONE") + print(" XS01 non e' solo un meccanismo: e' un meccanismo AFFINATO due volte sui dati HL") + print(" 2024+ (blend di lookback [30,90] e gate di dispersione p30). Quelle DUE scelte sono") + print(" debito di multiple-testing contratto sulla finestra di scoperta. Qui si paga:") + print(" ogni variante e' valutata sul PRE-2024, a MEDIANA delle 10 fasi (mai l'ancora).") + variants = [("blend [30,90] + gate p30 (CANONICA)", dict(lookbacks=(30, 90), disp_pct=30)), + ("solo L=30 + gate p30", dict(lookbacks=(30,), disp_pct=30)), + ("solo L=90 + gate p30", dict(lookbacks=(90,), disp_pct=30)), + ("blend [30,90] SENZA gate", dict(lookbacks=(30, 90), disp_pct=0)), + ("blend [30,90] + gate p15", dict(lookbacks=(30, 90), disp_pct=15)), + ("blend [30,90] + gate p50", dict(lookbacks=(30, 90), disp_pct=50))] + sel_rows = [] + for nm, kw in variants: + b_pre = A.anchor_luck_band( + lambda ph, kw=kw: win(alive(xsec_engine(p13, phase=ph, **kw)), None, "2024-01-01"), + phases, canonical=0, metric=sh) + b_post = A.anchor_luck_band( + lambda ph, kw=kw: win(alive(xsec_engine(p13, phase=ph, **kw)), "2024-01-01"), + phases, canonical=0, metric=sh) + sel_rows.append((nm, b_pre["median"], b_post["median"], b_pre["per_offset"])) + print(f" {nm:<38s} PRE-2024 mediana Sh {b_pre['median']:+5.2f} " + f"[{b_pre['lo']:+.2f},{b_pre['hi']:+.2f}] | 2024+ {b_post['median']:+5.2f}") + rank_pre = 1 + sorted([r[1] for r in sel_rows], reverse=True).index(sel_rows[0][1]) + rank_post = 1 + sorted([r[2] for r in sel_rows], reverse=True).index(sel_rows[0][2]) + print(f" -> la configurazione CANONICA (scelta nel 2026-06 guardando SOLO il 2024+) e'") + print(f" {rank_pre}/{len(sel_rows)} sul PRE-2024 e {rank_post}/{len(sel_rows)} sul 2024+.") + print( " 📌 LETTURA: il NUCLEO regge (ogni variante e' positiva fuori campione, banda") + print( " [+0.76,+1.28]), ma i DUE AFFINAMENTI del 19/06 NON si replicano:") + print( " - il BLEND [30,90] fuori campione e' PEGGIO del solo L=30 (+1.12 vs +1.28);") + print( " - il GATE di dispersione p30 fuori campione vale ZERO (con e senza: +1.12);") + print( " e' dentro la finestra di scoperta che vale (+0.37 contro -0.03).") + print( " Cioe': cio' che sopravvive e' il meccanismo cross-sectional nudo; i due") + print( " affinamenti sono, per quanto qui misurabile, adattamento alla finestra in") + print( " cui furono scelti. NON e' un argomento per cambiarli (cambiarli guardando") + print( " QUESTO risultato sarebbe la stessa selezione, spostata di finestra): e'") + print( " un argomento per non attribuire loro il valore che il progetto gli attribuisce.") + + # --- concentrazione: un contributo positivo si scompone per ANNO e per GAMBA (lezione SOL) + print("\n Concentrazione (lezione SOL 22/08: '24/24 ancore positive erano UN ANNO SOLO'):") + pre13 = win(s13, None, "2024-01-01") + for y in (2021, 2022, 2023): + rest = pre13[pre13.index.year != y] + print(f" PRE-2024 togliendo il {y}: Sh {sh(rest):+5.2f} " + f"(intero pre-2024 {sh(pre13):+5.2f})") + loo = {} + for a_ in U13: + sub = panel19[[x for x in U13 if x != a_]].dropna() + bb = A.anchor_luck_band(lambda ph, sub=sub: win(alive(xsec_engine(sub, phase=ph, + min_assets=10)), + None, "2024-01-01"), + phases, canonical=0, metric=sh) + loo[a_] = bb["median"] + base_med = bands["U13-FIX PRE-2024"]["median"] + worst = min(loo, key=loo.get) + print(f" Leave-one-asset-out sul PRE-2024 (mediana di fase; base {base_med:+.2f}):") + print(" " + " ".join(f"-{k2}:{v:+.2f}" for k2, v in sorted(loo.items(), key=lambda t: t[1]))) + print(f" gamba piu' importante: {worst} (senza di lei {loo[worst]:+.2f}); " + f"minimo {min(loo.values()):+.2f}, tutte >0: {all(v > 0 for v in loo.values())}") + + # ---------------------------------------------------------- 8. i GATE + print("\n[8] GATE") + cand = s13.copy(); cand.index = cand.index.tz_convert("UTC") + mg = A.marginal_vs_tp01(cand) + print(f" marginal_vs_tp01 (U13-FIX, 2021+): {mg.get('marginal_verdict')} " + f"corr {mg.get('corr_full')} n={mg.get('n_days')}") + for kk in ("robust_oos", "has_insample_edge", "is_hedge", "beats_noise_null", + "insample_sharpe", "tp01_beta", "alpha_ann"): + if kk in mg: + print(f" {kk:<20s} {mg[kk]}") + if "blends" in mg: + for wname, bl in mg["blends"].items(): + print(f" blend {wname}: uplift_full {bl.get('uplift_full')} " + f"uplift_hold {bl.get('uplift_hold')}") + + mgE = A.marginal_vs_tp01(sEXP) + print(f" marginal_vs_tp01 (U19-EXP, 2020-12+): {mgE.get('marginal_verdict')} " + f"corr {mgE.get('corr_full')}") + + # deflated Sharpe: TUTTI i trial valutati, al RIALZO + all_sr = [] + for tag in ("U13-FIX FULL", "U13-FIX PRE-2024", "U19-EXP FULL", "U19-EXP PRE-2024"): + all_sr += list(bands[tag]["per_offset"].values()) + all_sr += list(bhl["per_offset"].values()) + list(bsec["per_offset"].values()) + for _, _, _, po in sel_rows: # 6 varianti x 10 fasi della sezione 7b + all_sr += list(po.values()) + all_sr += [float(v) for v in loo.values()] + cand_pool = list(all_sr) # SOLO configurazioni di XS01 + ctrl = [sh(alive(xsec_engine(p13, phase=0, mode="rev"))), + sh(alive(xsec_engine(panel19, phase=0, mode="rev"))), sh(jj["bin"]), sh(jj["hl"])] + for fs in (0.0005, 0.001, 0.002): + ctrl.append(sh(alive(xsec_engine(p13, phase=0, fee_side=fs)))) + dsr_val = bands["U13-FIX PRE-2024"]["median"] + med_ph = int(np.argsort(list(bands["U13-FIX PRE-2024"]["per_offset"].values()))[len(phases) // 2]) + med_series = win(alive(xsec_engine(p13, phase=med_ph)), None, "2024-01-01") + d1, s1 = A.deflated_sharpe(dsr_val, cand_pool, med_series) + d2, s2 = A.deflated_sharpe(dsr_val, cand_pool + ctrl, med_series) + print(f" deflated_sharpe — headline = MEDIANA di fase OOS {dsr_val:+.2f}") + print(f" pool A = SOLE configurazioni XS01 ({len(cand_pool)} trial): DSR {d1:.3f} " + f"(max dal rumore {s1:+.2f}) -> {'PASS' if d1 >= 0.95 else 'FAIL'}") + print(f" pool B = tutto cio' che ho valutato, controlli NEGATIVI inclusi " + f"({len(cand_pool)+len(ctrl)} trial): DSR {d2:.3f} (max dal rumore {s2:+.2f}) " + f"-> {'PASS' if d2 >= 0.95 else 'FAIL'}") + print( " ⚠️ Il pool B e' il conteggio 'al rialzo' che il progetto impone, ma qui e' anche") + print( " il piu' SEVERO in modo improprio: il DSR pesa la VARIANZA dei trial, e io ci") + print( " ho messo dentro controlli costruiti per essere negativi (REV -0.97, SECT11") + print( " -0.44). Si cita il piu' basso dei due; e si dice che entrambi penalizzano un") + print( " MEDIANA-di-fase come se fosse un MAX-di-k, che e' il caso per cui il DSR e'") + print( " costruito. Il debito di selezione vero di XS01 non e' stato contratto qui:") + print( " e' quello del 2026-06-19, e la sezione 7b lo misura direttamente.") + + imp = A.implausible_sharpe(s13) + print(f" implausible_sharpe: {imp['implausible']} Sh {imp.get('sharpe'):+.2f} " + f"maxDD {imp.get('maxdd', 0)*100:.1f}% perdite su barre attive " + f"{imp.get('loss_frac', float('nan'))*100:.1f}% (attive {imp.get('active_frac', 0)*100:.0f}%)" + f"{' — ' + '; '.join(imp['reasons']) if imp['reasons'] else ''}") + + # causalita': i prezzi DOPO la data di decisione non devono cambiare le posizioni prima + Pc = p13.copy() + cut = len(Pc) // 2 + rngc = np.random.default_rng(7) + Pd = Pc.copy() + Pd.iloc[cut:] = Pc.iloc[cut:].values * (1.0 + rngc.normal(0, 0.35, size=Pc.iloc[cut:].shape)) + a_ = xsec_engine(Pc, phase=0); b_ = xsec_engine(Pd, phase=0) + # confronto solo dove il vol-target non ha ancora "visto" i dati perturbati (rv30 shiftata) + lim = cut - 35 + cdiff = float(np.max(np.abs(a_.values[:lim] - b_.values[:lim]))) + print(f" causalita' (prezzi futuri randomizzati da i={cut}): max|diff| prima di i={lim} " + f"= {cdiff:.2e} -> {'LEAK-FREE' if cdiff < 1e-12 else 'PERDITA DI FUTURO'}") + + print(" null de-levering: NON PERTINENTE — il claim qui non e' 'meno drawdown' ma " + "'l'edge esiste fuori campione'. Il de-levering e' comunque implicito nel vol-target,") + print(" e le tabelle di fase riportano DD e Sharpe INSIEME (un k<1 abbassa entrambi).") + + print(f"\n sensibilita' al COSTO (il rischio #1 di uno sleeve a 13 gambe alt):") + for fs in (0.0005, 0.001, 0.002, 0.003): + x = alive(xsec_engine(p13, phase=0, fee_side=fs)) + print(f" {fs*1e4:5.0f} bps/lato: FULL Sh {sh(x):+5.2f} | " + f"PRE-2024 {sh(win(x, None, '2024-01-01')):+5.2f} | " + f"2024+ {sh(win(x, '2024-01-01')):+5.2f}") + + # ------------------------------------------------- 9. impatto sul LIBRO + print("\n[9] IMPATTO SUL LIBRO — TP01+SKH01 (75/25 live) con e senza XS01, su 2021+") + from src.portfolio.sleeves import _tp01_returns, _skyhook_returns + tp = A._to_daily(_tp01_returns()); sk = A._to_daily(_skyhook_returns()) + for xs_tag, xs in (("XS01 storia lunga (U13-FIX)", s13), ("XS01 ufficiale (HL 2024+)", off)): + J = pd.concat({"tp": tp, "sk": sk, "xs": A._to_daily(xs)}, axis=1, join="inner").dropna() + if len(J) < 200: + continue + base = 0.75 * J["tp"] + 0.25 * J["sk"] + for w in (0.15, 0.25, 0.50): + mix = (1 - w) * base + w * J["xs"] + print(f" [{xs_tag}] {J.index[0].date()}..{J.index[-1].date()} n={len(J)} " + f"w={w:.2f}: Sh {sh(base):+.2f} -> {sh(mix):+.2f} " + f"({sh(mix)-sh(base):+.2f}) maxDD {dd(base)*100:.1f}% -> {dd(mix)*100:.1f}% " + f"corr(XS,libro) {J['xs'].corr(base):+.3f}") + # e il numero che il PROP-ALLOC usa davvero: il libro 2021+ contiene il 2022 + pre = J[J.index < DISCOVERY] + if len(pre) > 200: + bpre = 0.75 * pre["tp"] + 0.25 * pre["sk"] + print(f" -> SOLO FUORI CAMPIONE ({pre.index[0].date()}..{pre.index[-1].date()}, " + f"n={len(pre)}), sweep del peso di XS01:") + for w in (0.15, 0.25, 0.33, 0.50): + mpre = (1 - w) * bpre + w * pre["xs"] + print(f" w={w:.2f}: libro Sh {sh(bpre):+.2f} -> {sh(mpre):+.2f} " + f"({sh(mpre)-sh(bpre):+.2f}) maxDD {dd(bpre)*100:.1f}% -> " + f"{dd(mpre)*100:.1f}% corr {pre['xs'].corr(bpre):+.3f}") + print(" ⚠️ PROP-ALLOC mette XS01 al 50%: su un conto a BARRIERA cio' che si") + print(" compra e' il maxDD, quindi la riga da guardare e' quella, non lo Sharpe.") + + # ---------------------------------------------------- 10. eseguibilita' + print("\n[10] ESEGUIBILITA' — la domanda del canale funded e' se il libro STA su $100k") + det: dict = {} + xsec_engine(p13, phase=0, detail=det) + W, sc = det["W"], det["scale"] + dW = np.abs(np.diff(W, axis=0)); rb = np.flatnonzero(dW.sum(axis=1) > 1e-12) + print(f" {len(U13)} gambe, {len(rb)} ribilanciamenti, gross medio " + f"{np.abs(W).sum(axis=1).mean():.2f}, leva media (vol-target) {np.mean(sc[sc > 0]):.2f}x") + print(f" {'capitale':>10} {'ticket mediano':>16} {'ticket p10':>12} {'<$5':>7} {'<$10':>7}") + for cap in (600, 5000, 20000, 100000): + parts = [dW[i][dW[i] > 1e-12] * sc[i + 1] * cap for i in rb if sc[i + 1] > 0] + tk = np.concatenate(parts) if parts else np.array([]) + if not tk.size: + print(" (nessun ticket calcolabile — controlla il motore)"); break + print(f" {cap:>10,d} {np.median(tk):>15.2f}$ {np.percentile(tk, 10):>11.2f}$ " + f"{(tk < 5).mean()*100:>6.1f}% {(tk < 10).mean()*100:>6.1f}%") + print(" ⚠️ Approssimazione DICHIARATA: il modello congelato applica il vol-target alla") + print(" SERIE dei rendimenti, non alle posizioni -> qui il ticket e' calcolato sui soli") + print(" delta di RIBILANCIAMENTO (ogni 10 giorni), ignorando il micro-turnover") + print(" giornaliero che una implementazione vera del vol-target genererebbe. E' un") + print(" LIMITE INFERIORE del numero di ordini, quindi ottimistico sul lato costi.") + print(" ⚠️ E NON rimette in discussione il ~$20k citato dal progetto: quella soglia e' sui") + print(" 19 alt Hyperliquid (min-order e liquidita' del venue), non su questi 13.") + + print(f"\n[fine] {time.time()-t0:.1f}s") + + +if __name__ == "__main__": + main() From 0f2e195674fe50c8233b7a087146587b34c83ded Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 20:51:47 +0000 Subject: [PATCH 028/114] =?UTF-8?q?research(wave-0822):=20MAKER=20scartato?= =?UTF-8?q?=20(il=20segno=20dipende=20da=20<=20contro=20<=3D)=20+=20CRITIC?= =?UTF-8?q?O=20=E2=80=94=20la=20cucitura=20dell'ondata,=20non=20i=20filoni?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 134 ++++++++++++++++++++++- scripts/research/r0822b_bin_freq.py | 9 +- scripts/research/r0822b_surface_rv.py | 58 +++++++++- scripts/research/r0822b_tp01_sinistro.py | 88 ++++++++++++--- 4 files changed, 271 insertions(+), 18 deletions(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index ebe226d..2590956 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -693,9 +693,9 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | # | filone | domanda che decide | verdetto | |---|---|---|---| | 21 | XS01-OOS | il vantaggio del PROP-ALLOC poggia sul drift di XS01 misurato sulla sua finestra di scoperta: regge fuori? | _in corso_ | -| 22 | MAKER | l'esecuzione passiva e' una fonte di ritorno, al netto del costo di non essere eseguiti? | _in corso_ | +| 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' | _in corso_ | +| 24 | CRITICO | cosa NON e' stato misurato, e quale singola misura mancante vale di piu' | **3 correzioni all'ondata**, 1 regola mia ritirata | | 25 | BIN-FREQ | il binario di frequenza su SKH01 e' un candidato o un sottoprodotto? | _in corso_ | | 26 | TP01-SINISTRO | quanto pesa TP01 se l'obiettivo include un crash che nel campione non c'e'? | _in corso_ | | 27 | SURFACE-RV | la superficie e' internamente incoerente, e l'incoerenza vale piu' del costo di attraversarla? | _in corso_ | @@ -759,3 +759,133 @@ mostra che la sua serie e quella del lookback costante coincidono a **max|Δ| = n=2719** — non *somiglia a*, **lo e'**. **VERDETTO: SCARTATO. IL FILONE ORIZZONTE-ADATTIVO E' CHIUSO DEFINITIVAMENTE.** + +--- + +## 22 — MAKER (esecuzione passiva come fonte di ritorno) + +`scripts/research/r0822b_maker.py` (19 s, sola lettura). **42 celle dichiarate**, nessuna riduzione. + +📌 **Osservazione strutturale che riapriva la domanda:** il netting su strumento unico — il motivo per +cui T1 resto' non implementata il 26/07 — **non si applica qui**. Quello uccide i *bracket per-sleeve* +(un limit al livello di SKH chiuderebbe quota TP01); l'ordine **netto** di ribilanciamento e' uno +solo, quindi passivizzarlo non ha conflitti fra sleeve. La domanda era davvero aperta. + +**(1) Il differenziale, limite superiore** (tutto maker, zero non-fill): dSharpe **+0,0333**, +dCAGR +0,466 pp → **$2,96/anno a $635**, $23,28 a $5.000, $1.203 al muro di $258k. +✅ Replica indipendente della curva del 26/07: **+0,0167 Sh/bps e +0,233 pp/bps** contro i pubblicati +0,017 / 0,23. + +📌 **(2) Il costo del non-fill, ed e' qui che muore: il parametro che decide non e' l'orizzonte, e' +cosa si chiama "riempito".** + +| N (min) | TOCCO `low <= P` (limite superiore) | ATTRAVERSAMENTO `low < P` (minimo difendibile) | +|---|---|---| +| 5 | fill 93,2% · resta **+0,31** bps (15,5%) | fill 86,2% · resta **−0,47** bps (**−23,6%**) | +| 60 | 98,6% · +0,34 (17,1%) | 95,9% · **−0,45** (−22,4%) | +| 120 | 98,9% · +0,67 (33,7%) | 96,9% · −0,38 (−19,0%) | + +**Fra le due colonne cambia una sola disuguaglianza stretta**: se la barra che apre esattamente al +nostro prezzo, e da li' se ne va, conta come fill. **Non conta** — a quel prezzo non e' stato +scambiato niente dopo l'arrivo dell'ordine, e un passivo sta *dietro* a tutta la coda. Griglia fine: +il salto e' **tutto al primo passo** (3,16 → 3,95 bps a penetrazione sub-tick) e **non esiste un q +positivo di pareggio**. Lente model-free: il fill *richiesto* per pareggiare e' **88-98%**, quello +ottenuto e' sotto in **25 celle su 25**. + +**Gate:** appaiato **per ORDINE** (ogni ordine compare una volta in entrambe le politiche, riempito o +no — allineare sui riempiti avrebbe tenuto solo i casi in cui non e' successo niente); **IC95 +bootstrap contiene lo zero in 10 celle su 10** al tocco e a q=0, interamente negativo da q≥1; +**placebo a ore casuali girato** → reale ≈ placebo e **incoerente fra i due asset** (BTC 6,8 vs 3,1 +bps; ETH 4,1 vs 4,7) ⇒ la selezione avversa **non** e' "il libro trada nei momenti brutti", e' +**strutturale all'esecuzione passiva** e non si aggira scegliendo momenti migliori. Null del +de-levering **non pertinente e dichiarato** (nessun claim di DD; il maxDD **cambia segno** fra le +lenti = non e' un effetto). + +📌 **La firma della troncatura, vista dall'altra parte.** Il passivo **vince sul 92-98% degli ordini e +perde sulla media**: mediana dell'avversa **−2,0 bps** (favorevole), media **+4,3**, peggiore **+948**, +e i **5 ordini peggiori su 1535 fanno il 42% del danno**. E' la forma speculare del profit-take di +VRP01 (30/07). **Una politica che vince quasi sempre e perde sulla media si giudica sulla media.** + +⚠️ **Rischio operativo quantificato:** a N=60 restano aperti al giro successivo il **4,6-7,8% degli +ordini = 9-15 volte l'anno** in cui l'esecutore deve cancellare-e-rimpiazzare su un conto vero. Oggi +`rebalance_signed` manda market e finisce: **nessun ordine sopravvive al giro** → si comprerebbe un +modo nuovo di rompersi (il libro ha gia' pagato quel prezzo con `fresh_5m`) per un beneficio il cui +IC95 contiene lo zero. + +⚠️ **Numero del brief non riprodotto:** l'agente ricostruisce **191 ordini/anno di libro** a $635 +contro i "35-44/anno per asset" del brief; il nastro vero da' 18 ordini in 45 giorni = **146/anno**. +La conclusione non dipende da questo (il punto 1 usa il turnover modellato, che replica la curva +pubblicata al terzo decimale), **ma il numero del brief va ri-verificato.** + +**VERDETTO: SCARTATO.** +0,033 Sharpe nel caso impossibile, contro ±0,20 di banda d'ancora del libro. +**Smentitore dichiarato:** una misura di **profondita' del book** sui perpetual USDC (che l'OHLC non +contiene e che il progetto non raccoglie — `cb_chain` e' opzioni). Se a taglie di $5-80 la coda al +miglior livello si consumasse senza che il prezzo debba attraversare, il segno si ribalta. **E' +misurabile in avanti**: registrare `book_depth` al momento di ogni ordine e poi il volume scambiato +al livello. + +--- + +## 24 — CRITICO (critico di completezza dell'ondata) + +`scripts/research/r0822b_critic.md` (48 KB, markdown — non uno script). Ha letto brief, registro, +diario e i sorgenti degli agenti. + +📌 **La tesi, e regge:** *il metodo dei singoli filoni non e' il problema dell'ondata, la CUCITURA lo +e'.* I difetti gravi stanno nei punti dove **nessun agente era responsabile**. + +🚨 **(a) `dealer_net_gamma` NON e' "il GEX col segno invertito" — la regola l'ho pubblicata io in +CLAUDE.md ed e' FALSA.** ✅ **Verificata da me aprendo la fonte**, che gira su questa stessa VPS: +`/opt/docker/cerbero-mcp/src/cerbero_mcp/common/options.py:11-14` **dichiara la convenzione** +(*"i dealer sono SHORT calls (le vendono al retail) e LONG puts"*) e `dealer_gamma_profile:138-140` +la implementa (`call_dealer_gamma -= contrib` / `put_dealer_gamma += contrib`). Non e' un segno +invertito per errore: e' un'**assunzione di posizionamento dichiarata**, diversa da quella del GEX +standard. Il default `top_n_strikes=50` spiega anche il coefficiente −0,7 e l'offset che il filone +DEALER-GAMMA attribuiva a "non-portabilita'". **Il verdetto SCARTATO regge; cade la regola**, che le +prossime ondate avrebbero ereditato al contrario. + +🚨 **(b) Il deflated-Sharpe e' stato misurato ai suoi DUE estremi degeneri lo stesso giorno, da due +agenti che non si citano — e tre gate pre-registrati ci poggiano sopra** (XSR01 23/10, DVOLSPREAD +24/10, 22/12). `altlib.deflated_sharpe` calcola `sr0 = sd(Sharpe dei trial) × mult(N)`: dipende dalla +**varianza** della griglia, non solo da N. Da un lato VOL-SIZE dichiara il proprio DSR **VACUO** +(1,000 anche per il baseline); dall'altro ORTHO-SCREEN pubblica *"sr0 = 1,572 ⇒ uno screen largo su +BTC/ETH non puo' passare il proprio gate per aritmetica"* — ma quell'1,572 implica sd≈0,58, e **le +stesse 168 celle dichiarate come 7 famiglie da 24 danno 1,15, sotto il soffitto ~1,3**: il verdetto +"impossibile per aritmetica" **si ribalta senza toccare un dato**, e la cura che suggerisce +(dichiarare famiglie piu' piccole) e' esattamente la manovra che il progetto ha proibito il 30/07. +✅ **Convergenza indipendente:** il filone BOCPD, che non ha parlato col critico, ha misurato lo +stesso difetto dall'altro capo (DSR 0,998 PASS dentro la famiglia omogenea → **0,555 FAIL** con +l'sr0 di screen). **Due agenti, due strade, stessa conclusione: il gate non ha una regione utile in +mezzo.** + +🚨 **(c) Il filone ALT-OPTIONS non e' nel registro** — ✅ verificato: **zero occorrenze**, ne' riga ne' +sezione, benche' due altri filoni lo citino e il mio brief per SURFACE-RV mandasse a leggerlo. Il +diario lo liquida come *"alt-options (alt)"*, ma la sua conclusione riguarda **ETH_USDC, che non e' +un alt**: `f_venue` 0,980 e **max-loss $16,62/lotto**. Se regge, e' la **quarta** soglia pubblicata +falsificata (il diario ne annuncia tre) e sposta VRP01 da *"servirebbe il 61% di un conto da $3.000"* +a *"entra in un conto da ~$140"*. ⚠️ **Cio' che NON cambia: la regola "niente short-vol da modello in +deploy" non era una regola di eseguibilita'** — VRP01 resta fuori dal libro per il `f` modellato e +per il gate IV-rank, non per la taglia del lotto. + +📌 **(d) La misura di maggior valore mai fatta, secondo il critico: il peggior giorno che il libro +puo' avere, MISURATO invece che assunto.** Il gradino di leva 1,00→1,50x vale **14,7a → 11,6a** al +capitale-rendita (~€300/mese di versamenti equivalenti, a costo zero) ed e' bloccato da **un unico +parametro scelto a mano** ("un giorno −10%/anno porta k* a 2x"). Controllo di cinque minuti: sui 10 +peggiori giorni di BTC/ETH in 7,4 anni — **incluso il −41,5%/−45,7% del 12 marzo 2020** — il libro ha +fatto **+0,97 · +0,72 · 0,00 · −1,98 · −2,24 · +0,23%**, e il suo peggior giorno assoluto (**−3,38%**) +**non e' un giorno di crash**. Il −10% e' plausibilmente una **coda da buy&hold importata in un libro +difensivo**. + +📌 **(e) La risposta onesta al mandato, che gli era stato chiesto di dare se vera: nessuna misura di +ricerca batte "versare".** 3,1 anni di ricerca valgono meno dei 5 anni che vale €250/mese in piu'. +E la verifica che costa meno di tutte **non e' ricerca**: leggere il listino di due prop firm. +L'intero risultato di testa dell'ondata (+0,400 di J) poggia sull'assunto — scritto in un **commento +a riga 100** — che una firm crypto listi i 19 alt Hyperliquid con short abilitato. **E' la ripetizione +esatta dell'errore GTAA01/PRIIPs**, per cui il progetto ha gia' scritto la regola: *la negoziabilita' +sul conto REALE va verificata quando lo sleeve entra in RICERCA.* + +**Altre contraddizioni fra agenti segnalate:** lo Sharpe di XSR01 compare con **quattro** lenti (non +tre); il muro di XS01 con **tre** numeri; la leva riceve guida **opposta** da GROWTH-POLICY e +PROP-ALLOC; il peso di SKH01 e' **respinto da un agente e raddoppiato da un altro**. Piu' 6 modalita' +non esplorate e 6 punti di autocritica — incluso che i **20 MB del dataset futures 2018-2026 (l'unico +con la coda 2022) vivono solo in uno scratchpad di sessione**. diff --git a/scripts/research/r0822b_bin_freq.py b/scripts/research/r0822b_bin_freq.py index 345285f..bef27eb 100644 --- a/scripts/research/r0822b_bin_freq.py +++ b/scripts/research/r0822b_bin_freq.py @@ -486,9 +486,16 @@ def main() -> None: print(f" e' la scelta CONSERVATIVA e resta quella dichiarata.") print(f" dispersione dei trial: sd(Sharpe FULL) = {float(np.std(all_sr, ddof=1)):.3f} sulle") print(f" {len(ALLC)} celle, {float(np.std(pos_sr, ddof=1)):.3f} sulle {len(POS)} di segno +1.") - print(f" -> il DSR qui NON e' vacuo (il baseline lo fallisce dove il candidato lo passa):") + print(f" -> il DSR qui NON e' vacuo (il baseline lo FALLISCE dove il candidato lo passa):") print(f" la famiglia contiene celle davvero diverse fra loro, al contrario della famiglia") print(f" VOL-SIZE dove erano tutte perturbazioni della stessa size continua.") + print(f" ❌ QUESTO REFUTA LA MIA IPOTESI (b), registrata nel docstring PRIMA di misurare") + print(f" ('il DSR sara' vacuo come su VOL-SIZE'). La ragione per cui mi sbagliavo e'") + print(f" strutturale e vale come regola: un DSR e' vacuo quando i trial si somigliano") + print(f" (var_tr -> 0 => sr0 -> 0 => DSR ~ 1 per chiunque). Una famiglia BINARIA contiene") + print(f" celle che scartano insiemi di trade MOLTO diversi — compreso il segno inverso —") + print(f" quindi la dispersione dei trial e' reale e il test riacquista potenza. Non e' il") + print(f" binario a essere piu' solido della formula: e' la FAMIGLIA a essere piu' larga.") print("\n (c) MARGINAL vs TP01 — sulla gamba modificata e sulla gamba base") for lab, s in (("BASE (size fissa)", base_leg[can]), (nmc, LEG[nmc][can]), diff --git a/scripts/research/r0822b_surface_rv.py b/scripts/research/r0822b_surface_rv.py index 1c222f9..d8cfe23 100644 --- a/scripts/research/r0822b_surface_rv.py +++ b/scripts/research/r0822b_surface_rv.py @@ -369,7 +369,9 @@ def test_calendario(q: pd.DataFrame, dte_min: float, ogni: int = 4) -> dict: iv_need = np.sqrt(np.maximum(x["w"].to_numpy(), 0) / np.maximum(nxt_T, 1e-9)) * 100.0 amp_pt = np.where(ok, iv_need - nxt_iv, np.nan) - res = {"i_n": int(ok.sum()), "i_viol": int(viol_i.sum()), + room = amp_pt[ok] # <0 = margine, >0 = violazione + res = {"room_med_pt": float(np.nanmedian(room)), "room_p5_pt": float(np.nanpercentile(room, 5)), + "i_n": int(ok.sum()), "i_viol": int(viol_i.sum()), "i_quota": float(viol_i.sum() / max(1, ok.sum())), "i_amp_med_pt": float(np.nanmedian(amp_pt[viol_i])) if viol_i.any() else np.nan, "i_amp_p95_pt": float(np.nanpercentile(amp_pt[viol_i], 95)) if viol_i.any() else np.nan} @@ -471,6 +473,37 @@ def persistenza(bf: pd.DataFrame, viol_col: str) -> dict: return {"n": int(m.sum()), "resta": float(nxt[m].mean()) if m.any() else np.nan} +def controfattuale_usdc(bf: pd.DataFrame, asset: str, rel_usdc: float) -> dict: + """SE la famiglia USDC quotasse la STESSA superficie col SUO spread, sarebbe eseguibile? + + Controfattuale DICHIARATO, non una misura: il collettore non raccoglie le USDC (difetto di + produzione del filone 11), quindi si tiene il mid osservato sulle inverse e si stringe lo + spread al valore relativo misurato dagli altri filoni sulla famiglia USDC, con il lotto + minimo USDC e la stessa formula di fee. Assunzione dichiarata: stessa griglia di tick + (una griglia piu' grossa peggiorerebbe il risultato, quindi questo e' un LIMITE SUPERIORE). + """ + x = bf[(bf["asset"] == asset)].dropna(subset=["mid", "mid_p", "mid_n"]).copy() + x = x[np.isclose(x["lam"], 0.5, atol=0.02)] + lot = MIN_LOT_USDC[asset] + out = {} + for nome, rel in (("inverse osservato", None), (f"{asset}_USDC controfattuale", rel_usdc)): + if rel is None: + b, a_p, a_n = x["bid"], x["ask_p"], x["ask_n"] + l = MIN_LOT[asset] + else: + b = x["mid"] * (1 - rel / 2) + a_p = x["mid_p"] * (1 + rel / 2) + a_n = x["mid_n"] * (1 + rel / 2) + l = lot + cred = (b.to_numpy() * 2 * l - a_p.to_numpy() * l - a_n.to_numpy() * l) + f = (fee_coin(b.to_numpy(), 2 * l) + fee_coin(a_p.to_numpy(), l) + fee_coin(a_n.to_numpy(), l)) + netto = (cred - f) * x["spot_feed"].to_numpy() + out[nome] = {"n": int(len(x)), "pos": int(np.nansum(netto > 0)), + "max": float(np.nanmax(netto)), "p999": float(np.nanpercentile(netto, 99.9)), + "lotto_usd": float(np.nanmedian(x["spot_feed"]) * l)} + return out + + # --------------------------------------------------------------------------- controlli positivi def _finto(rows) -> pd.DataFrame: @@ -657,6 +690,12 @@ def main() -> None: print(f" (ii) log-moneyness FISSO, con interpolazione (1 snapshot ogni {c['ii_ogni']}): " f"{c['ii_viol']:,}/{c['ii_n']:,} = {c['ii_quota']:.3%} · ampiezza mediana {c['ii_amp_med_pt']:.3f} pt-vol") print(f" -> contributo dell'INTERPOLATORE = {c['ii_quota'] - c['i_quota']:+.3%} di quota di violazione") + if "i_val_usd_med" in c: + print(f" valore mediano della violazione (i) al lotto minimo ${c['i_val_usd_med']:.2f} " + f"contro un costo di 2 gambe (spread + fee) di ${c['i_costo_usd_med']:.2f}") + print(f" margine di monotonia sull'intera superficie: la iv lunga sta {-c['room_med_pt']:.2f} pt-vol " + f"SOPRA il minimo che la non-arbitraggio richiede (p5 {-c['room_p5_pt']:.2f}) " + f"-> la superficie non e' nemmeno VICINA al bordo") print("\n" + "=" * 100) print("5. TICK E PERSISTENZA — le due difese contro 'la mia incoerenza e' un artefatto'") @@ -674,6 +713,23 @@ def main() -> None: f"(mediana {np.median(nt):.0f} tick) · resta in violazione allo snapshot dopo " f"{p.get('resta', float('nan')):.1%} (n={p['n']:,})") + print("\n CONTROFATTUALE (dichiarato, non misurato): la stessa superficie con lo spread USDC") + for a, rel in (("BTC", 0.082), ("ETH", 0.026)): + if a not in set(bf["asset"]): + continue + cf = controfattuale_usdc(bf, a, rel) + for nome, r in cf.items(): + print(f" {a} {nome:<28}: lotto ${r['lotto_usd']:,.0f} · farfalle a credito netto>0 " + f"{r['pos']:,}/{r['n']:,} · migliore ${r['max']:.2f} · p99.9 ${r['p999']:.2f}") + + print("\n QUANTO MANCA per essere eseguibile (near-miss, triple equispaziate):") + for a in sorted(bf["asset"].unique()): + x = bf[(bf["asset"] == a) & np.isclose(bf["lam"], 0.5, atol=0.02)] + c = x["cost_exec"].to_numpy() * 2 * MIN_LOT[a] * x["spot_feed"].to_numpy() # >0 = costo + print(f" {a}: costo della farfalla eseguibile — migliore ${np.nanmin(c):.2f} · " + f"p0.1 ${np.nanpercentile(c, 0.1):.2f} · mediana ${np.nanmedian(c):.2f} " + f"(negativo = credito riskless)") + print("\n" + "=" * 100) print("6. IL RESIDUO SI RICOMPONE? (l'ipotesi del filone, misurata invece che assunta)") print("=" * 100) diff --git a/scripts/research/r0822b_tp01_sinistro.py b/scripts/research/r0822b_tp01_sinistro.py index 20e0ece..2c158f4 100644 --- a/scripts/research/r0822b_tp01_sinistro.py +++ b/scripts/research/r0822b_tp01_sinistro.py @@ -444,13 +444,19 @@ def main() -> None: # noqa: C901 wins_sk += d > c print(f" {str(IDX[t].date()):>12} {win[t]:>9.1%} {a:>11.1%} {b:>11.1%} " f"{c:>10.1%} {d:>11.1%} {('si' if b > a else 'no'):>9}") + pre = [t for t in w12 if IDX[t] < pd.Timestamp(RECENT_START, tz="UTC")] + wins_pre = sum(1 for t in pre + if float(np.expm1(lr_opt[t:t + EPI_LEN].sum())) + > float(np.expm1(lr_live[t:t + EPI_LEN].sum()))) print(f"\n OPT batte LIVE in **{wins}/12** finestre -> test dei segni " f"p = {binom_two_sided(wins, 12):.4f}") print(f" SKH01 puro batte TP01 puro in **{wins_sk}/12** -> p = " f"{binom_two_sided(wins_sk, 12):.4f}") - print(" ⚠️ 5 di queste 12 finestre stanno nel 2024+, cioe' DENTRO la finestra su cui") - print(" SKH01 e' stato tarato e su cui PROP-ALLOC ha ottimizzato: la potenza sale ma") - print(" l'indipendenza scende. Il test va letto come **una diagnostica, non una prova**.") + print(f" ⚠️ {12-len(pre)} di queste 12 finestre stanno nel 2024+, cioe' DENTRO la finestra") + print(" su cui SKH01 e' stato tarato e su cui PROP-ALLOC ha ottimizzato: la potenza sale") + print(" ma l'indipendenza scende. **Sul solo sotto-campione pre-2024**, che e' quello") + print(f" indipendente: OPT batte LIVE in **{wins_pre}/{len(pre)}** -> p = " + f"{binom_two_sided(wins_pre, len(pre)):.4f}. E' questo il numero da citare.") tick("§2 conto assicurazione") @@ -583,11 +589,15 @@ def main() -> None: # noqa: C901 bw_c = max(s2, key=lambda w: s2[w][2]) print(f"\n argmax della colonna 'peggiore': leva-da-2024+ -> TP01 {bw_a:.3f} | " f"leva comune 0.50x -> TP01 {bw_c:.3f}") - print(" Lettura: se la sopravvivenza al crash SALISSE col peso di TP01, l'assicurazione") - print(" servirebbe. **A leva comune non sale**: la curva e' piatta-decrescente. Il difensivo") - print(" non compra sopravvivenza su questa barriera, perche' cio' che la barriera punisce e'") - print(" la PERDITA nel crash, e nel crash TP01 perde (prende la prima gamba giu' e poi va") - print(" flat) mentre SKH01 si gira short e GUADAGNA.") + print(" ⚠️ LE DUE COLONNE DICONO COSE DIVERSE, E LA SECONDA E' A CAMPANA: a leva COMUNE la") + print(f" sopravvivenza al crash SALE col peso di TP01 fino a {bw_c:.3f} (" + f"{s2[0.0][2]:.1%} a TP01=0 -> {s2[bw_c][2]:.1%}) e poi ricade.") + print(" **Quindi TP01 protegge davvero: a pari leva, il libro con piu' difensivo sopravvive") + print(" di piu' al crash.** Il punto e' che quella protezione si compra anche abbassando la") + print(" leva — ed e' esattamente cio' che misura §5. Alla leva che ciascun libro sceglierebbe") + print(" (prima colonna) il vantaggio sparisce, perche' il libro senza TP01 la sceglie piu'") + print(" bassa. E' la firma del **null del de-levering**, qui in veste di 'protezione dal") + print(" crash' invece che di 'meno drawdown'.") tick("§4 S2 replay") @@ -637,9 +647,16 @@ def main() -> None: # noqa: C901 ret, _, _, _, _ = replay22(w, lo) _, _, _, why, _ = replay22(w, hi) print(f" {w:>7.3f} {lo:>23.2f}x {ret:>20.1%} {why:>25}") - print("\n ⚠️ La leva di rottura e' MONOTONA DECRESCENTE nel peso di TP01 solo se la") - print(" protezione fosse reale. Si legga la colonna: se il libro con piu' TP01 rompe PRIMA,") - print(" l'assicurazione non sta assicurando la cosa giusta.") + print("\n 📌 La colonna 'leva max viva' CRESCE col peso di TP01 (fino a TP01=0.875; a TP01=1") + print(" ricade perche' li' a mordere non e' piu' il giorno singolo ma la perdita CUMULATA).") + print(" **L'assicurazione e' reale e si vede: piu' TP01 = piu' leva sopportabile nel 2022.**") + print(" E accanto c'e' il prezzo: alla PROPRIA leva massima il libro senza TP01 porta a casa") + print(" il +23.3% e quello a TP01 pieno il -5.9%. TP01 compra capacita' di leva, e la") + print(" capacita' di leva non si converte in rendimento perche' cio' che TP01 toglie e'") + print(" proprio il rendimento. E' il null del de-levering scritto in una riga sola.") + print(" ⚠️ Cambia anche il VINCOLO che morde: sotto TP01 0.50 e' la regola a UN GIORNO,") + print(" sopra e' il max-loss cumulato. Due libri diversi muoiono di due morti diverse, e una") + print(" sola delle due lenti (l'accoppiata) vede la prima.") tick("§4 replay 2022") @@ -725,8 +742,10 @@ def main() -> None: # noqa: C901 print(f" {0.5*wxs30[t_xw]:.1%} al libro. La domanda non e' se XS01 sopravvive a cio' che ha") print(" gia' visto, e' se il suo comportamento nel crash somiglia a cio' che ha gia' visto —") print(" e su questo il campione non ha NIENTE da dire.") - W3 = [(0.75, 0.25, 0.00), (0.375, 0.625, 0.00), (0.25, 0.25, 0.50), (0.125, 0.125, 0.75), - (0.50, 0.25, 0.25)] + # ⚠ le ultime tre righe hanno XS01 FISSO al 50%: senza quel controllo si confonderebbe + # "piu' TP01" con "meno XS01", che e' un'altra domanda. + W3 = [(0.75, 0.25, 0.00), (0.375, 0.625, 0.00), (0.50, 0.25, 0.25), + (0.125, 0.375, 0.50), (0.25, 0.25, 0.50), (0.375, 0.125, 0.50)] com = xs.index.intersection(IDX[i_rec:]) xs_al = xs.reindex(com) print(f"\n {'TP/SKH/XS':>16} {'ipotesi XS':>12} {'leva':>6} {'J normale':>10} " @@ -832,7 +851,9 @@ def main() -> None: # noqa: C901 n_act = int((np.abs(R) > 1e-12).sum()) n_los = int((R[np.abs(R) > 1e-12] < 0).sum()) f = A.implausible_sharpe(d, n_trades=n_act, n_losing_trades=n_los) - print(f" implausible_sharpe {lab:>11}: {f}") + print(f" implausible_sharpe {lab:>11}: implausible={f['implausible']} " + f"(Sharpe {f['sharpe']:.2f}, maxDD {f['maxdd']:.1%}, quota barre in perdita " + f"{f['loss_frac']:.1%}, Calmar {f['calmar']:.2f}) {f['reasons']}") except Exception as exc: # pragma: no cover print(f" implausible_sharpe: NON GIRATO ({exc})") print(" marginal_vs_tp01 / study_family_honest / deflated_sharpe : **NON GIRATI, e non") @@ -864,6 +885,45 @@ def main() -> None: # noqa: C901 am = max(tab, key=lambda w: tab[w][key][1]["J"]) print(f" {lab:>26} {am:>14.3f} {tab[am][key][1]['J']:>10.3f} " f"{tab[0.75][key][1]['J']:>10.3f} {tab[0.375][key][1]['J']:>11.3f}") + print("\n ESISTE UN PESO DI COMPROMESSO? Il criterio dichiarato: (a) dentro il 5% del") + print(" massimo di J sulla finestra 2024+ (la 'regione robusta' del funded) E (b) dentro il") + print(" 5% del massimo di J sulla finestra 2019+, che il sinistro ce l'ha dentro.") + jr_max = max(tab[w]["rec"][1]["J"] for w in W_GRID) + jf_max = max(tab[w]["full"][1]["J"] for w in W_GRID) + js_max = max(tab[w]["sin"][1]["J"] for w in W_GRID) + print(f"\n {'w TP01':>7} {'J 2024+':>9} {'entro 5%':>9} {'J 2019+':>9} {'entro 5%':>9} " + f"{'J solo-sin':>11} {'entro 5%':>9} {'compromesso':>13}") + ok_both = [] + for w in W_GRID: + a, b, c = (tab[w]["rec"][1]["J"], tab[w]["full"][1]["J"], tab[w]["sin"][1]["J"]) + oa, ob, oc = a >= 0.95 * jr_max, b >= 0.95 * jf_max, c >= 0.95 * js_max + if oa and ob: + ok_both.append(w) + print(f" {w:>7.3f} {a:>9.3f} {('si' if oa else 'no'):>9} {b:>9.3f} " + f"{('si' if ob else 'no'):>9} {c:>11.3f} {('si' if oc else 'no'):>9} " + f"{('SI' if (oa and ob) else '-'):>13}") + print(f"\n -> pesi che soddisfano (a) E (b): {ok_both if ok_both else 'NESSUNO'}") + print(f" il libro live 75/25 e' {'dentro' if 0.75 in ok_both else 'FUORI'} da questa regione.") + print("\n LA RISPOSTA, IN QUATTRO RIGHE") + print(" 1) La premessa del caveat e' sbagliata nella FORMA: il 2024+ un crash ce l'ha (b&h") + print(" -60% di drawdown, -39.6% in 20 giorni). Manca la TAGLIA, ~2x sulla coda veloce.") + print(" 2) Il compromesso ESISTE e non e' un compromesso: lo stesso peso (TP01 ~0.375) e'") + print(" l'argmax sulla finestra con il 2022 dentro E su quella senza. Il caveat di") + print(" PROP-ALLOC e' onesto come cautela e **falsificato come previsione**.") + print(" 3) TP01 protegge davvero (a pari leva sopravvive di piu' al crash, e alza la leva") + print(" sopportabile nel 2022 da 0.93x a 2.20x) ma la stessa protezione si compra piu' a") + print(" buon mercato ABBASSANDO LA LEVA: a iso-sopravvivenza il payout e' massimo a") + print(" TP01 0.125-0.250 e cala monotonamente da li'. **7ª occorrenza del null del") + print(" de-levering**, in una veste nuova: 'protezione dal crash' invece di 'meno DD'.") + print(" 4) Il rischio vero dell'ottimo di PROP-ALLOC NON e' TP01 sotto-pesato: e' XS01 al") + print(" 50% con un comportamento nel crash che il campione non contiene. Un solo giorno") + print(" a -8% su XS01 dimezza la sopravvivenza; con TP01 al 50% ne serve uno a -12.3%.") + print(" **Se TP01 vale qualcosa su un conto funded, vale come zavorra contro l'ignoto di") + print(" XS01, non come assicurazione contro il crash di BTC/ETH — che SKH01 fa meglio.**") + print("\n ⚠️ COSA MI SMENTIREBBE: un crash in cui SKH01 NON si gira short in tempo (gap") + print(" notturno oltre lo stop, o venue fermo). Il campione ne ha quattro e in tutti e") + print(" quattro SKH01 ha girato. Basterebbe un episodio in cui SKH01 perde quanto il") + print(" sottostante per rovesciare §2 e §5 — e con n=4 quell'episodio non e' improbabile.") print("\n" + "=" * 106) print(f" fatto in {time.time()-t0:.0f}s | valutazioni: {N_EVAL}") print("=" * 106) From 1ec928cf092bbd8adabed5b18857f2d631a54b92 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 20:52:40 +0000 Subject: [PATCH 029/114] =?UTF-8?q?research(wave-0822):=20XS01-OOS=20?= =?UTF-8?q?=E2=80=94=20l'edge=20esiste=20FUORI=20dalla=20finestra=20di=20s?= =?UTF-8?q?coperta=20ed=20e'=20piu'=20grande=20li';=20il=20gate=20del=20pr?= =?UTF-8?q?op-alloc=20si=20sostituisce,=20non=20si=20aspetta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 85 ++++++++++++++++++++++++++- scripts/research/r0822b_surface_rv.py | 41 ++++++++++--- 2 files changed, 116 insertions(+), 10 deletions(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 2590956..a402bd9 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -692,7 +692,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | # | filone | domanda che decide | verdetto | |---|---|---|---| -| 21 | XS01-OOS | il vantaggio del PROP-ALLOC poggia sul drift di XS01 misurato sulla sua finestra di scoperta: regge fuori? | _in corso_ | +| 21 | XS01-OOS | il vantaggio del PROP-ALLOC poggia sul drift di XS01 misurato sulla sua finestra di scoperta: regge fuori? | **LEAD** — regge, ed e' PIU' GRANDE fuori | | 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 | @@ -889,3 +889,86 @@ tre); il muro di XS01 con **tre** numeri; la leva riceve guida **opposta** da GR PROP-ALLOC; il peso di SKH01 e' **respinto da un agente e raddoppiato da un altro**. Piu' 6 modalita' non esplorate e 6 punti di autocritica — incluso che i **20 MB del dataset futures 2018-2026 (l'unico con la coda 2022) vivono solo in uno scratchpad di sessione**. + +--- + +## 21 — XS01-OOS (XS01 fuori dalla sua finestra di scoperta) — **il risultato di testa della serata** + +`scripts/research/r0822b_xs01_oos.py` (78-126 s, cache nello scratchpad, seconda corsa **offline**, +numeri identici su due corse). **140 trial dichiarati** (contati al rialzo: le 6 varianti di config +sono contate come trial anche se sono la *rivalidazione di una selezione passata*), piu' 1.200 +estrazioni di null che **non** sono trial. + +**L'idea:** il gate del PROP-ALLOC diceva *"non aprire un funded prima che XS01 abbia una finestra +fuori dal 2024-2026"* = un'attesa di anni. **Quella finestra esiste gia'** — gli stessi 19 ticker su +**Binance spot USDT 1d, 2021-01 → 2023-12**, che contiene **LUNA e FTX**. Meccanismo **congelato** +(blend [30,90], H=10, k=5, gate p30, vol-target 20%), nessuna griglia cercata. + +⚠️ **L'obiezione ovvia — "Binance non e' la verita'" — e' stata misurata, non aggirata:** quella +regola del progetto riguarda l'**ancoraggio di prezzo**, non un ranking cross-sezionale, e lo scarto +USDT e' un **fattore comune** che sparisce nello z-score. Verificato: corr dei ritorni giornalieri per +asset **≥0,9992** (min NEAR), |differenza| mediana **5,1 bps**, e soprattutto lo **stesso sleeve** +calcolato sui due venue da' **corr 0,9991, dSharpe −0,025**. Replica dello sleeve ufficiale +`max|diff| = 0,0`. Dato certificato: **0 gap, flat 0,00-0,71%, volumi zero 0,00%**, nessun run +iniziale a volume 0 (il rivelatore del backfill sintetico, lezione AXS). + +📌 **IL RISULTATO, e va nel verso opposto a quello temuto:** + +| | Sharpe (**mediana di fase**) | banda 10 fasi | fasi >0 | maxDD | CAGR | +|---|---|---|---|---|---| +| **fuori campione 2021-2023 (U13)** | **+1,12** | [+0,74, +1,54] | **10/10** | **22,0%** | +26,1% | +| finestra di scoperta 2024+ (U13) | +0,37 | — | — | 20,5% | +8,9% | +| **fuori campione (U19-EXP)** | **+1,17** | [+0,91, +1,78] | **10/10** | 20,9% | +30,0% | +| XS01 **ufficiale** (HL, 2024+) | +1,15 | [+0,46, +1,59] | — | **10,8%** | +29,0% | + +**Differenza appaiata per fase (fuori campione − scoperta): +0,668, positiva in 10/10 fasi.** +**Il fuori campione e' la finestra MIGLIORE, non peggiore.** Per anno: 2021 **+2,14** · 2022 +0,22 · +2023 +0,84 · 2024 −0,48 · 2025 +0,68 · 2026 +2,26. + +**Sul LIBRO, solo fuori campione** (TP01+SKH01 75/25 + XS01, n=1076, dati che PROP-ALLOC non aveva mai +visto): w=0 → Sh **+1,17** / DD 9,4% · w=0,25 → **+1,65 / 6,0%** · w=0,33 → +1,67 / 6,6% · +**w=0,50 (l'argmax di PROP-ALLOC) → +1,56 / 8,9%**. corr(XS01, libro) **−0,019**. + +**Gate:** `causality` leak-free (randomizzando i prezzi dopo la decisione `max|diff| = 0`); +`marginal_vs_tp01` **ADDS** su entrambi gli universi (corr +0,031 / −0,01, `is_hedge` **False**); +banda di fase `gate_pass=True` **4/4**, e la fortuna d'ancora e' **piccola** (+0,05) contro il **+0,18** +dello sleeve ufficiale; null di permutazione **a fee zero** p=0,013 (cross-sez.) e 0,017 (pesi +casuali); sopravvive a **30 bps/lato** (+1,01). **`deflated_sharpe` FAIL 0,342** — e l'agente dichiara +che il DSR penalizza una **mediana-di-fase** come se fosse un **max-di-k**, *"ma un gate fallito resta +fallito"*. Null del de-levering **non pertinente e dichiarato**. Haircut a $600: **non e' il vincolo** +(ticket mediano $76, **0,0%** sotto il min-order). + +🚨 **Null "universo dove NON dovrebbe funzionare"** (11 settoriali SPDR, 1998+, meccanismo congelato): +**−0,45, 0/10 fasi positive** → **terza conferma indipendente che il cross-sectional e' +crypto-specifico** (dopo XSR01-equity 26/07 e STATARB-EQ 25/07). + +🚨 **Il risultato che il progetto non si aspettava: i DUE AFFINAMENTI del 2026-06-19 NON si +replicano.** Fuori campione il **blend [30,90] e' PEGGIO del solo L=30** (+1,12 vs +1,28) e il **gate +di dispersione p30 vale ZERO** (+1,12 con e senza) — mentre *dentro* la finestra di scoperta il gate +vale eccome (+0,37 contro −0,03). **Cio' che sopravvive e' il meccanismo cross-sectional NUDO.** +⚠️ Non e' un argomento per cambiarli — cambiarli guardando *questo* risultato sarebbe la stessa +selezione spostata di finestra — **e' un argomento per non attribuire loro il valore che la memoria +del progetto gli attribuisce.** + +⚠️ **Concentrazione misurata prima di crederci** (lezione SOL, stesso giorno): togliendo il **2021** il +fuori campione scende **+1,17 → +0,57**; **leave-one-asset-out**: togliendo **LTC** la mediana di fase +crolla **+1,12 → +0,20** (tutte e 13 restano >0) → **ampiezza effettiva bassa**, stessa diagnosi di +STATARB-MULTI. + +**VERDETTO: LEAD.** Non promuove niente; **riscrive il gate del PROP-ALLOC.** + +📌 **`IL GATE DEL PROP-ALLOC SI PUO' CHIUDERE OGGI: PARZIALMENTE` — e non si chiude, si SOSTITUISCE.** +L'attesa di anni **non serve**. Ma tre cose che il gate non chiedeva sono ora misurate: +- **il rendimento sopravvive, il RISCHIO no**: maxDD standalone **10,8% → 20,9-22,0%** — e su un conto + funded il vincolo binding e' la **regola di DD**, quindi la correzione va nel verso sbagliato; +- **il peso 0,50 dell'argmax-J non sopravvive**: fuori campione a w=0,50 il maxDD di libro risale a + **8,9%** contro **6,0%** a w=0,25, cioe' restituisce quasi tutta la protezione che e' l'unica ragione + per metterlo su un conto a barriera → l'ottimo fuori campione e' **w ≈ 0,25-0,33**; +- **32% dell'universo non e' testabile** (ARB/OP/SUI/APT/SEI/TIA non quotati nel 2021-23) e su di esso + vive gran parte del numero di scoperta (**19 gambe 1,31 vs 13 gambe 0,54**, stessa finestra e stesso + venue) → *cio' che e' stato validato e' una versione piu' DEBOLE di quella che sta nel libro.* + +⚠️ **Il punto fragile che l'agente dichiara e non ha testato:** la validazione di venue gira **solo sul +2024+**, cioe' proprio il periodo **senza depeg USDT**, mentre il fuori campione contiene il **depeg di +maggio 2022**. Se i close Binance/USDT 2021-23 divergessero da un venue **USD** in modo *non comune fra +gli asset*, il ranking cambierebbe e il risultato con esso. diff --git a/scripts/research/r0822b_surface_rv.py b/scripts/research/r0822b_surface_rv.py index d8cfe23..45379f5 100644 --- a/scripts/research/r0822b_surface_rv.py +++ b/scripts/research/r0822b_surface_rv.py @@ -370,7 +370,7 @@ def test_calendario(q: pd.DataFrame, dte_min: float, ogni: int = 4) -> dict: amp_pt = np.where(ok, iv_need - nxt_iv, np.nan) room = amp_pt[ok] # <0 = margine, >0 = violazione - res = {"room_med_pt": float(np.nanmedian(room)), "room_p5_pt": float(np.nanpercentile(room, 5)), + res = {"room_med_pt": float(np.nanmedian(room)), "room_p5_pt": float(np.nanpercentile(room, 95)), "i_n": int(ok.sum()), "i_viol": int(viol_i.sum()), "i_quota": float(viol_i.sum() / max(1, ok.sum())), "i_amp_med_pt": float(np.nanmedian(amp_pt[viol_i])) if viol_i.any() else np.nan, @@ -473,7 +473,8 @@ def persistenza(bf: pd.DataFrame, viol_col: str) -> dict: return {"n": int(m.sum()), "resta": float(nxt[m].mean()) if m.any() else np.nan} -def controfattuale_usdc(bf: pd.DataFrame, asset: str, rel_usdc: float) -> dict: +def controfattuale_usdc(bf: pd.DataFrame, asset: str, rel_usdc: float, + solo_gia_strette: bool = False) -> dict: """SE la famiglia USDC quotasse la STESSA superficie col SUO spread, sarebbe eseguibile? Controfattuale DICHIARATO, non una misura: il collettore non raccoglie le USDC (difetto di @@ -484,6 +485,14 @@ def controfattuale_usdc(bf: pd.DataFrame, asset: str, rel_usdc: float) -> dict: """ x = bf[(bf["asset"] == asset)].dropna(subset=["mid", "mid_p", "mid_n"]).copy() x = x[np.isclose(x["lam"], 0.5, atol=0.02)] + if solo_gia_strette: + # il controfattuale ingenuo prende il MID di un mercato larghissimo e finge di poterlo + # quotare al 2,6%: e' proprio dove il mid e' meno informativo. Qui si tiene solo la parte + # di superficie GIA' quotata stretta quanto la famiglia USDC, dove il mid vale qualcosa. + rs = np.maximum.reduce([((x["ask"] - x["bid"]) / x["mid"]).to_numpy(), + ((x["ask_p"] - x["bid_p"]) / x["mid_p"]).to_numpy(), + ((x["ask_n"] - x["bid_n"]) / x["mid_n"]).to_numpy()]) + x = x[rs <= rel_usdc] lot = MIN_LOT_USDC[asset] out = {} for nome, rel in (("inverse osservato", None), (f"{asset}_USDC controfattuale", rel_usdc)): @@ -499,8 +508,10 @@ def controfattuale_usdc(bf: pd.DataFrame, asset: str, rel_usdc: float) -> dict: f = (fee_coin(b.to_numpy(), 2 * l) + fee_coin(a_p.to_numpy(), l) + fee_coin(a_n.to_numpy(), l)) netto = (cred - f) * x["spot_feed"].to_numpy() out[nome] = {"n": int(len(x)), "pos": int(np.nansum(netto > 0)), - "max": float(np.nanmax(netto)), "p999": float(np.nanpercentile(netto, 99.9)), - "lotto_usd": float(np.nanmedian(x["spot_feed"]) * l)} + "pos1": int(np.nansum(netto > 1.0)), "somma": float(np.nansum(netto[netto > 0])), + "max": float(np.nanmax(netto)) if len(x) else np.nan, + "p999": float(np.nanpercentile(netto, 99.9)) if len(x) else np.nan, + "lotto_usd": float(np.nanmedian(x["spot_feed"]) * l) if len(x) else np.nan} return out @@ -694,7 +705,7 @@ def main() -> None: print(f" valore mediano della violazione (i) al lotto minimo ${c['i_val_usd_med']:.2f} " f"contro un costo di 2 gambe (spread + fee) di ${c['i_costo_usd_med']:.2f}") print(f" margine di monotonia sull'intera superficie: la iv lunga sta {-c['room_med_pt']:.2f} pt-vol " - f"SOPRA il minimo che la non-arbitraggio richiede (p5 {-c['room_p5_pt']:.2f}) " + f"SOPRA il minimo che la non-arbitraggio richiede (5% piu' stretto: {-c['room_p5_pt']:.2f}) " f"-> la superficie non e' nemmeno VICINA al bordo") print("\n" + "=" * 100) @@ -717,10 +728,22 @@ def main() -> None: for a, rel in (("BTC", 0.082), ("ETH", 0.026)): if a not in set(bf["asset"]): continue - cf = controfattuale_usdc(bf, a, rel) - for nome, r in cf.items(): - print(f" {a} {nome:<28}: lotto ${r['lotto_usd']:,.0f} · farfalle a credito netto>0 " - f"{r['pos']:,}/{r['n']:,} · migliore ${r['max']:.2f} · p99.9 ${r['p999']:.2f}") + for strette, et in ((False, "tutta la superficie"), (True, "solo quote gia' strette")): + cf = controfattuale_usdc(bf, a, rel, solo_gia_strette=strette) + for nome, r in cf.items(): + if strette and "inverse" in nome: + continue + print(f" {a} {nome:<26} [{et:<24}]: lotto ${r['lotto_usd']:,.0f} · " + f"credito netto>0 {r['pos']:,}/{r['n']:,} · >$1 {r['pos1']:,} · " + f"migliore ${r['max']:.2f} · somma ${r['somma']:,.0f}") + # dove vive l'incoerenza: e' larga dove il mercato e' largo? + x = bf[(bf["asset"] == a)].dropna(subset=["iv_resid", "relspread"]) + x = x[np.isfinite(x["relspread"])] + dec = pd.qcut(x["relspread"], 5, labels=False, duplicates="drop") + t = x.assign(d=dec).groupby("d").agg(spread=("relspread", "median"), + resid=("iv_resid", lambda v: v.abs().median())) + print(f" |residuo| per quintile di spread relativo {a}: " + + " · ".join(f"{r.spread:.1%}->{r.resid:.3f}pt" for r in t.itertuples())) print("\n QUANTO MANCA per essere eseguibile (near-miss, triple equispaziate):") for a in sorted(bf["asset"].unique()): From 93ce12931a38a781245ffe8ad1c8c2155e74f3bf Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 20:54:29 +0000 Subject: [PATCH 030/114] =?UTF-8?q?CLAUDE.md:=20ritiro=20una=20regola=20mi?= =?UTF-8?q?a=20=E2=80=94=20dealer=5Fnet=5Fgamma=20non=20e'=20il=20GEX=20in?= =?UTF-8?q?vertito,=20e'=20una=20convenzione=20dichiarata=20nel=20sorgente?= =?UTF-8?q?=20su=20questa=20VPS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 23 ++++++++++++++----- scripts/research/r0822b_surface_rv.py | 33 +++++++++++++++++++++------ 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a3d5cb4..9eea10f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1993,7 +1993,7 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis Se qualcuno propone SOL "perche' e' eseguibile su Deribit", la risposta e' che l'eseguibilita' non e' mai stata il problema — l'hold-out lo e', a 24 ancore su 24. - 🌊 **ONDATA MULTI-AGENTE 2026-08-22 (20 filoni, branch `research/wave-0822`) — 0 candidati, 1 - DIFETTO DI PRODUZIONE, 3 soglie pubblicate falsificate, 1 leva che nessun gate sa vedere.** + DIFETTO DI PRODUZIONE, **4** soglie pubblicate falsificate, 1 leva che nessun gate sa vedere.** Diario `docs/diary/2026-08-22-wave-multiagente.md`; ledger per filone `docs/research/RESULTS-0822.md`; briefing `docs/research/BRIEF-0822.md`; 21 script `scripts/research/r0822_*.py` (15.295 righe, compilano tutti, ognuno gira da solo). **Book, pesi, cron, config INVARIATI.** @@ -2098,11 +2098,22 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis non risorge** sotto due conditioner mai provati (volume, shock 3σ): la selezione in-sample sceglie il ramo di **continuazione** in entrambi. Scartati anche: adaptive-horizon (il "vincitore adattivo" ha lookback **costante**, corr 1,000), dealer-gamma (BTC non separa, ETH separa **al rovescio**; - e ⚠️ **`dealer_net_gamma` del dataset ereditato e' il GEX col SEGNO INVERTITO**, corr −0,95, mentre - `liquidation_*_risk` e' **una sola categoria in 17.229/17.229 righe**), oi-pin (il max-pain **non - batte mai, 0/24, la media a 7 giorni dello spot**), term-structure (**termometro contemporaneo**), - skew direzionale (**il prezzo muove lo skew**, t 3,1-9,4 su 8/8, non il contrario), ortho-screen - **7/7**, alt-options (alt), xs-lite, vrp-quote-vere. + ⚠️ **e qui una REGOLA MIA, pubblicata la sera stessa e RITIRATA poche ore dopo:** avevo scritto che + `dealer_net_gamma` del dataset ereditato *"e' il GEX col SEGNO INVERTITO"* (corr −0,95). **FALSO, e + la fonte gira su questa stessa VPS:** `/opt/docker/cerbero-mcp/src/cerbero_mcp/common/options.py:11-14` + **dichiara la convenzione** — *"i dealer sono SHORT calls (le vendono al retail) e LONG puts"* — e + `dealer_gamma_profile:138-140` la implementa (`call_dealer_gamma -= contrib` / `put_dealer_gamma + += contrib`). Non e' un segno sbagliato: e' un'**assunzione di posizionamento dichiarata**, diversa + da quella del GEX standard, e il default `top_n_strikes=50` spiega anche il coefficiente −0,7 e + l'offset che avevo attribuito a "non-portabilita'". **Il verdetto SCARTATO regge; cade la regola** — + che le prossime ondate avrebbero ereditato al contrario. **REGOLA VERA: prima di dichiarare che un + campo di un dataset e' sbagliato, aprire il codice che lo produce** — qui erano due comandi, e la + correlazione a −0,95 con la ricostruzione "standard" era esattamente cio' che una convenzione + diversa **deve** produrre. `liquidation_*_risk` e' invece **una sola categoria in 17.229/17.229 + righe**, e quello e' confermato. + Scartati anche: oi-pin (il max-pain **non batte mai, 0/24, la media a 7 giorni dello spot**), + term-structure (**termometro contemporaneo**), skew direzionale (**il prezzo muove lo skew**, + t 3,1-9,4 su 8/8, non il contrario), ortho-screen **7/7**, alt-options, xs-lite, vrp-quote-vere. ⚠️ **La catena ereditata da bite ha 74-75 giorni utili, non 3,7 mesi:** prima del **2026-06-09** raccoglieva **una sola scadenza per giro** (due agenti indipendenti, 74 e 75). 📌 **(8) REGOLE NUOVE.** (a) **L'open interest NON misura la negoziabilita'** — su Deribit USDC sono diff --git a/scripts/research/r0822b_surface_rv.py b/scripts/research/r0822b_surface_rv.py index 45379f5..65a8206 100644 --- a/scripts/research/r0822b_surface_rv.py +++ b/scripts/research/r0822b_surface_rv.py @@ -777,17 +777,36 @@ def main() -> None: print(f" quota ESEGUIBILE per ora UTC: max {h['ex'].max():.5%} su {len(h)} ore -> " f"nessuna ora del giorno apre una finestra") + print("\n" + "=" * 100) + print("GRIGLIA DICHIARATA (contata al RIALZO)") + print("=" * 100) + print(" 3 test di arbitraggio (verticale · farfalla · calendario)") + print(" x 2 asset x 2 tipi (call/put) x 2 filtri DTE (0g e 1g) x 2 convenzioni di prezzo") + print(" (mid = incoerenza · bid/ask = eseguibile) = 48 celle") + print(" + calendario in 2 lenti (strike condiviso / log-moneyness interpolato) = 4") + print(" + controfattuale USDC x 2 asset x 2 sottocampioni = 4") + print(" + reversione x 2 asset · ancora oraria 24 ore x 2 metriche = 50") + print(" + 6 controlli positivi (3 rilevatori x 2 versi) = 6") + print(" TOTALE dichiarato ~112 valutazioni. NESSUNA e' una selezione: non c'e' una cella") + print(" scelta, c'e' un censimento. Il deflated-Sharpe non e' applicabile (nessuno stream).") + print("\n" + "=" * 100) print("SINTESI") print("=" * 100) nv = int(bf["viol_exec"].sum()); nt = len(bf) - print(f" * arbitraggio statico ESEGUIBILE sullo strike: {nv} su {nt:,} triple = {nv/nt:.6%}") - print(f" * incoerenza al MID: {bf['viol_mid'].mean():.2%} delle triple, " - f"ma mediana {np.abs(bf['iv_resid']).median():.3f} punti di vol") - print(f" * mezzo spread mediano: BTC {bf[bf.asset=='BTC']['halfspread_pt'].median():.3f} pt-vol, " - f"ETH {bf[bf.asset=='ETH']['halfspread_pt'].median():.3f} pt-vol") - print(" * il credito netto della migliore farfalla eseguibile in 1,03M osservazioni e' NEGATIVO") - print(" -> l'incoerenza ESISTE e vale meno del costo di attraversarla: verdetto SCARTATO.") + print(f" 1. La superficie e' ARBITRAGE-FREE sui prezzi eseguibili: monotonia 0/{1_111_585:,},") + print(f" convessita' {nv}/{nt:,} = {nv/nt:.6%} (e quell'una vale $0,00 e sparisce l'ora dopo),") + print(f" calendario 6/107.177 con margine mediano di 13 punti di vol dal bordo.") + print(f" 2. L'incoerenza al MID esiste ed e' grande ({bf['viol_mid'].mean():.1%} delle triple)") + print(f" ma vale {np.abs(bf['iv_resid']).median():.3f} punti di vol contro un mezzo spread di 0,86-1,21,") + print(f" ed e' SOTTO IL TICK del venue nel 95-97% dei casi: non e' esprimibile nella griglia.") + print(f" 3. MECCANISMO: |residuo| cresce in modo monotono col LARGO del mercato") + print(f" (BTC 0,080 pt a spread 2,6% -> 0,255 pt a spread 50%). L'incoerenza al mid E' la") + print(f" larghezza del mercato guardata attraverso il mid, non un errore di prezzo.") + print(f" 4. Il controfattuale USDC (lotto 10x piu' piccolo, spread 2,6%) sembra aprire 2.293") + print(f" farfalle a credito su ETH — ma restringendolo alla parte di superficie GIA' quotata") + print(f" stretta quanto le USDC diventa 0/13.287. Era un artefatto del controfattuale.") + print(f" -> l'incoerenza ESISTE e vale meno del costo di attraversarla: verdetto SCARTATO.") print(f"\n[tempo {time.time()-t0:.1f}s]") From 82ccff2553e8086adfacba768387b176d7bd7c33 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 21:07:19 +0000 Subject: [PATCH 031/114] =?UTF-8?q?research(wave-0822):=20TP01-SINISTRO=20?= =?UTF-8?q?=E2=80=94=20l'allarme=20e'=20falsificato=20nel=20verso;=20il=20?= =?UTF-8?q?rischio=20dell'ottimo=20funded=20e'=20XS01,=20non=20TP01?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/research/r0822b_bin_freq.py | 136 +++-- scripts/research/r0822b_depeg.py | 640 +++++++++++++++++++++++ scripts/research/r0822b_prop_recal.py | 637 ++++++++++++++++++++++ scripts/research/r0822b_surface_rv.py | 43 +- scripts/research/r0822b_tp01_sinistro.py | 41 +- 5 files changed, 1430 insertions(+), 67 deletions(-) create mode 100644 scripts/research/r0822b_depeg.py create mode 100644 scripts/research/r0822b_prop_recal.py diff --git a/scripts/research/r0822b_bin_freq.py b/scripts/research/r0822b_bin_freq.py index bef27eb..a33d18b 100644 --- a/scripts/research/r0822b_bin_freq.py +++ b/scripts/research/r0822b_bin_freq.py @@ -62,6 +62,7 @@ USO: nice -n 19 timeout 900 uv run python scripts/research/r0822b_bin_freq.py [ from __future__ import annotations import argparse +import gc import sys import time from pathlib import Path @@ -132,6 +133,10 @@ class KeepCache: self.rv: dict = {} self.q: dict = {} + def clear(self) -> None: + self.rv.clear() + self.q.clear() + def keep(self, ex: dict, key, thr, w: int, h: float, sg: int) -> pd.Series: base = ex["base_daily"] k = (key, w) @@ -152,6 +157,18 @@ class KeepCache: KC = KeepCache() +def rss_mb() -> float: + """RSS in MB, per non far morire il lavoro degli altri agenti (brief §8: 2 core, ~3 GB).""" + try: + with open("/proc/self/status") as f: + for ln in f: + if ln.startswith("VmRSS:"): + return float(ln.split()[1]) / 1024.0 + except Exception: + pass + return float("nan") + + def size_bin(thr, w, h, sg): def f(ex, key): return V.sizes_from_L(ex, KC.keep(ex, key, thr, w, h, sg)) @@ -175,7 +192,7 @@ def iso_pair(TP: pd.Series, leg: pd.Series, leg_base: pd.Series): def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--every", type=int, default=1) - ap.add_argument("--ndraw", type=int, default=200) + ap.add_argument("--ndraw", type=int, default=120) args = ap.parse_args() offs = OFFSETS_FULL[:: max(1, args.every)] t0 = time.time() @@ -208,8 +225,10 @@ def main() -> None: print("\n estrazione tabelle trade (V2-DD) ...", flush=True) EX = {o: {a: V.extract(a, o) for a in ASSETS} for o in offs} ntr = {a: int(np.mean([len(EX[o][a]["i_ent"]) for o in offs])) for a in ASSETS} + R.get5m.cache_clear() # ~90 MB di 5m non servono durante la §4 (la VPS ha 2 core + gc.collect() # e ~3 GB liberi condivisi); si ricaricano in §9b in ~3 s print(f" fatto in {time.time()-t0:.0f}s — trade medi/ancora: " + - ", ".join(f"{a} {ntr[a]}" for a in ASSETS)) + ", ".join(f"{a} {ntr[a]}" for a in ASSETS) + f" (RSS {rss_mb():.0f} MB)") TP = A.tp01_baseline_daily() can = offs[0] @@ -299,52 +318,71 @@ def main() -> None: print(" 4. LA FAMIGLIA INTERA — ISO dShFULL, mediana delle differenze appaiate su " f"{len(offs)} ancore") print("-" * L) - # DUE PASSATE, per la RAM (7 GB sulla VPS, ~3 liberi, e siamo in tanti): la 1a calcola le - # metriche per-ancora di tutte le 144 celle SENZA conservare le serie (144 x 23 x 3 serie da - # 2717 punti sarebbero ~500 MB); la 2a ricalcola e CONSERVA le serie delle sole 3-4 celle che - # servono dopo (canonica / scelta al buio / argmax): 4 x 23 gambe, ~2 s. + # RAM (brief §8: 7 GB totali, ~3 liberi, e siamo in tanti). Tre accorgimenti, tutti + # necessari — la prima stesura di questo script e' stata UCCISA a meta' della §4: + # (a) DUE PASSATE: la 1a calcola le metriche per-ancora di tutte le 144 celle SENZA + # conservare le serie (144 x 23 x 3 serie da 2717 punti = ~500 MB); la 2a ricalcola e + # conserva le serie delle sole 3-4 celle che servono dopo (~2 s); + # (b) ciclo ANCORA-ESTERNO con la cache di rv/quantili SVUOTATA a ogni ancora: viva una sola + # ancora alla volta (72 serie invece di 1.656); + # (c) RSS stampata, cosi' se qualcuno la rilancia sa cosa aspettarsi. BK, CT, LEG, MS = {}, {}, {}, {} + ACC: dict = {} - def eval_cell(thr, w, h, sg, store: bool = False) -> dict: + def _one(o, thr, w, h, sg, store: bool): + """UNA cella a UNA ancora. Le size si calcolano una volta sola e si riusano (la prima + stesura le ricalcolava per il conteggio dei tenuti); la scomposizione per anno e il maxDD + si fanno SOLO nella seconda passata, perche' servono a 3 celle e costavano il 60% del + tempo su tutte e 144.""" nm = cname(thr, w, h, sg) fn = size_bin(thr, w, h, sg) - dF, dH, dI, lF, lI, lH, ms, kp, ddv, ddc, dC_ = ([] for _ in range(11)) - yr: dict = {} - for o in offs: - lg = leg_of(EX[o], o, fn) - b, c, m = iso_pair(TP, lg, base_leg[o]) - dF.append(sh(b) - sh(c)); dH.append(sh(hold(b)) - sh(hold(c))) - dI.append(sh(ins(b)) - sh(ins(c))); dC_.append(cagr(b) - cagr(c)) - lF.append(sh(lg)); lI.append(sh(ins(lg))); lH.append(sh(hold(lg))) - ms.append(m); ddv.append(maxdd(b)); ddc.append(maxdd(c)) - kp.append(float(np.mean(np.concatenate([fn(EX[o][a], (o, a)) for a in ASSETS])))) + szs = {a: fn(EX[o][a], (o, a)) for a in ASSETS} + lg = V.leg_daily(EX[o], szs) + b, c, m = iso_pair(TP, lg, base_leg[o]) + d = ACC.setdefault(nm, dict(thr=thr, w=w, h=h, sg=sg, dF=[], dH=[], dI=[], dC=[], + lF=[], lI=[], lH=[], ms=[], kp=[], ddv=[], ddc=[], yr={})) + d["dF"].append(sh(b) - sh(c)); d["dH"].append(sh(hold(b)) - sh(hold(c))) + d["dI"].append(sh(ins(b)) - sh(ins(c))) + d["lF"].append(sh(lg)); d["lI"].append(sh(ins(lg))); d["lH"].append(sh(hold(lg))) + d["ms"].append(m) + d["kp"].append(float(np.mean(np.concatenate(list(szs.values()))))) + if store: + d["dC"].append(cagr(b) - cagr(c)) + d["ddv"].append(maxdd(b)); d["ddc"].append(maxdd(c)) for y in {int(t) for t in b.index.year}: by, cy = b[b.index.year == y], c[c.index.year == y] if len(by) > 20: - yr.setdefault(y, []).append(sh(by) - sh(cy)) - if store: - BK.setdefault(nm, {})[o] = b - CT.setdefault(nm, {})[o] = c - LEG.setdefault(nm, {})[o] = lg - MS.setdefault(nm, {})[o] = m - a_ = np.asarray(dF, float) - return dict(nm=nm, thr=thr, w=w, h=h, sg=sg, - dF=float(np.median(a_)), nF=int((a_ > 0).sum()), - lo=float(a_.min()), hi=float(a_.max()), arrF=a_, - dH=float(np.median(dH)), nH=int((np.asarray(dH) > 0).sum()), - isoI=float(np.median(dI)), dC=float(np.median(dC_)), - legF=float(np.median(lF)), legI=float(np.median(lI)), - legH=float(np.median(lH)), kept=float(np.median(kp)), - weff=float(np.median([W_SKH * x for x in ms])), - ddv=float(np.median(ddv)), ddc=float(np.median(ddc)), - yr={y: float(np.median(v)) for y, v in yr.items()}) + d["yr"].setdefault(y, []).append(sh(by) - sh(cy)) + BK.setdefault(nm, {})[o] = b + CT.setdefault(nm, {})[o] = c + LEG.setdefault(nm, {})[o] = lg + MS.setdefault(nm, {})[o] = m - RES = {} - for (thr, w, h, sg) in cells: - r = eval_cell(thr, w, h, sg) - RES[r["nm"]] = r + for o in offs: + KC.clear() + for (thr, w, h, sg) in cells: + _one(o, thr, w, h, sg, store=False) + KC.clear() + + def _med(v, dflt=float("nan")): + return float(np.median(v)) if len(v) else dflt + + def _fin(d: dict) -> dict: + a_ = np.asarray(d["dF"], float) + return dict(nm=cname(d["thr"], d["w"], d["h"], d["sg"]), thr=d["thr"], w=d["w"], + h=d["h"], sg=d["sg"], dF=float(np.median(a_)), nF=int((a_ > 0).sum()), + lo=float(a_.min()), hi=float(a_.max()), arrF=a_, + dH=float(np.median(d["dH"])), nH=int((np.asarray(d["dH"]) > 0).sum()), + isoI=float(np.median(d["dI"])), dC=_med(d["dC"]), + legF=float(np.median(d["lF"])), legI=float(np.median(d["lI"])), + legH=float(np.median(d["lH"])), kept=float(np.median(d["kp"])), + weff=float(np.median([W_SKH * x for x in d["ms"]])), + ddv=_med(d["ddv"]), ddc=_med(d["ddc"]), + yr={y: float(np.median(v)) for y, v in d["yr"].items()}) + + RES = {nm: _fin(d) for nm, d in ACC.items()} print(f" ({len(cells)} celle x {len(offs)} ancore valutate in {time.time()-t0:.0f}s " - f"dall'avvio, prima passata senza conservare le serie)") + f"dall'avvio, prima passata senza conservare le serie; RSS {rss_mb():.0f} MB)") def matrix(sg: int, h: float) -> None: print(f"\n ISO dShFULL — segno {'+1 (tieni vol BASSA)' if sg>0 else '-1 (CONTROLLO)'}" @@ -383,9 +421,19 @@ def main() -> None: f"positive {sum(r['dF']>0 for r in NEG)}/{len(NEG)} <- controllo di segno") # seconda passata: conserva le serie delle sole celle che servono dopo sel_iso0 = max(ALLC, key=lambda r: r["isoI"]) + ACC2 = ACC for r in dict((x["nm"], x) for x in (RES[nmcan], argmax, sel_iso0, max(ALLC, key=lambda z: z["legI"]))).values(): - eval_cell(r["thr"], r["w"], r["h"], r["sg"], store=True) + ACC = {} + for o in offs: + _one(o, r["thr"], r["w"], r["h"], r["sg"], store=True) + KC.clear() + RES[r["nm"]] = _fin(ACC[r["nm"]]) # ora con dC / maxDD / per-anno + ACC = ACC2 + POS = [RES[cname(t, w, h, +1)] for t in THRS for w in WS for h in HS] + NEG = [RES[cname(t, w, h, -1)] for t in THRS for w in WS for h in HS] + ALLC = POS + NEG + argmax = RES[argmax["nm"]] dchk = A.anchor_luck_delta(lambda o: BK[nmcan][o], lambda o: CT[nmcan][o], offs, metric=sh) assert abs(dchk["median_paired"] - RES[nmcan]["dF"]) < 1e-12 and \ dchk["n_positive"] == RES[nmcan]["nF"], "la statistica appaiata di §4 non coincide con A.anchor_luck_delta" @@ -718,8 +766,10 @@ def main() -> None: print(f" {tag:<40}{row}") if fee != keep_fee: del EXf, basef + gc.collect() V.FEE_RT = keep_fee assert V.FEE_RT == 0.001 + print(f" (RSS {rss_mb():.0f} MB)") f_mod = FEEDEC[(f"MODELLATA {keep_fee*1e4:.0f} bps RT (i backtest)", nmcan)] f_zero = FEEDEC[("fee ZERO (informazione pura)", nmcan)] print(f" -> a fee ZERO resta {f_zero:+.3f} di {f_mod:+.3f} = " @@ -735,6 +785,8 @@ def main() -> None: print(" temporale, ma e' l'unico campione indipendente disponibile a questo costo.") print("-" * L) EX1 = {o: {a: V.extract(a, o, SKH01_V1) for a in ASSETS} for o in offs} + R.get5m.cache_clear() # i 5m non servono piu': ~80 MB restituiti + gc.collect() base1 = {o: V.leg_daily(EX1[o], {a: V.size_flat(EX1[o][a]) for a in ASSETS}) for o in offs} print(f" {'cella':<26}{'ISO dShFULL':>13}{'pos/n':>8}{'ISO dShHOLD':>14}{'pos/n':>8}" f"{'tenuti':>9}") @@ -751,6 +803,10 @@ def main() -> None: print(f" {cname(r['thr'], r['w'], r['h'], r['sg']):<26}{dF['median_paired']:>+13.3f}" f"{dF['n_positive']:>5}/{len(offs):<3}{dH['median_paired']:>+14.3f}" f"{dH['n_positive']:>5}/{len(offs):<3}{float(np.median(kp))*100:>8.0f}%") + del EX1, base1 + KC.clear() + gc.collect() + print(f" (RSS {rss_mb():.0f} MB)") # ---------------------------------------------------------- §11 per anno print("\n" + "-" * L) diff --git a/scripts/research/r0822b_depeg.py b/scripts/research/r0822b_depeg.py new file mode 100644 index 0000000..0c4a413 --- /dev/null +++ b/scripts/research/r0822b_depeg.py @@ -0,0 +1,640 @@ +"""r0822b_depeg.py — IL FALSIFICATORE DI VENUE PER XS01-OOS (2026-08-22, ondata wave-0822). + +IL PUNTO FRAGILE, DICHIARATO DALL'AUTORE DEL RISULTATO +----------------------------------------------------- +`r0822b_xs01_oos.py` (RESULTS-0822 §21) ha misurato che il meccanismo congelato di XS01 ha un edge +FUORI dalla sua finestra di scoperta — 2021-2023 su **Binance spot USDT** — e che li' e' PIU' GRANDE +(mediana di fase +1,12 contro +0,37, differenza appaiata +0,668 in 10/10 fasi). Quel risultato +riscrive il gate del canale funded, quindi merita il suo falsificatore. + +Lo stesso autore lo ha nominato: + > "La validazione di venue Binance-vs-Hyperliquid gira SOLO sulla finestra comune 2024+, cioe' + > proprio il periodo SENZA depeg USDT. Il fuori campione contiene il depeg di maggio 2022. Se i + > close Binance/USDT 2021-2023 divergessero da un venue USD in modo NON COMUNE fra gli asset, il + > ranking cambierebbe e il risultato con esso. Test che lo deciderebbe: rifare il meccanismo sui + > close Coinbase USD per il sottoinsieme quotato li', 2021-2023, e chiedere corr fra i due sleeve + > >= 0.99 come sul 2024+." + +Questo script esegue esattamente quel test. + +PERCHE' LA DOMANDA NON E' OZIOSA (il meccanismo del danno, prima dei numeri) +--------------------------------------------------------------------------- +La regola del progetto — "Binance non e' la verita', e' USDT, ~10 bps fuori, e sotto depeg fino al +3% off" — e' stata **correttamente** giudicata non-binding da §21: uno z-score cross-sezionale +sottrae la media di sezione, quindi e' invariante a un fattore **COMUNE**. Ma un depeg non e' +garantito comune: nei giorni di stress la liquidita' si sposta in modo diverso per asset (i major +restano scambiati contro USDT, i minori no; i flussi di fuga passano per coppie diverse), e una +divergenza **IDIOSINCRATICA** cambia il RANKING — che e' esattamente cio' che la strategia legge. +Maggio 2022 (UST/LUNA, con USDT sceso a ~0,95) e' il caso peggiore possibile. +Percio' la divergenza va misurata in DUE pezzi, e sono due domande diverse: + (a) COMUNE = mediana per giorno dello scarto fra i venue -> quella che lo z-score annulla; + (b) IDIOSINCRATICA = scarto per asset DOPO aver tolto la mediana del giorno -> quella che uccide. +Riportare solo (a) darebbe la risposta rassicurante e sbagliata. + +IL VINCOLO DEL DATO, E COME E' STATO RISOLTO (dichiarato PRIMA di misurare) +-------------------------------------------------------------------------- +Su Coinbase USD, dei 13 asset di U13: + BNB -> quotato solo dal 2025-10 = FUORI (nessun venue USD ce l'ha nel 2021-23: e' l'asset + nativo del venue che stiamo mettendo in discussione) + XRP -> SOSPESO su Coinbase dal 2021-01-19 al 2023-07 (causa SEC) = buco proprio sulla finestra + NEAR/INJ -> quotati su Coinbase solo dal 2022-09 + AVAX -> dal 2021-09-30 (il vincolo che decide l'inizio della finestra) +XRP e' recuperato da **Bitstamp USD** (l'altra referenza USD del `venue_watch` del progetto, che +non ha mai sospeso la coppia). Restano fuori BNB, NEAR, INJ. +Ne escono DUE test, e il secondo copre cio' che il primo non copre: + + TEST 1 "U10-USD" = universo FISSO di 10 gambe (BTC ETH LTC LINK AAVE ADA DOGE SOL AVAX + XRP), + 2021-09-30 -> 2023-12-31, meccanismo **CONGELATO** (k=5, min_assets=10: + dieci gambe sono ESATTAMENTE il minimo, nessun parametro toccato). + Contiene il depeg di maggio 2022 e FTX. NON contiene il 2021. + TEST 2 "U13-IBRIDO" = i 13 asset e la finestra INTERA 2021-01 -> 2023-12, con il close USD + sostituito a quello USDT **dove esiste** (9-12 gambe su 13 a seconda della + data) e Binance dove non esiste. Non e' un confronto puro di venue, e' + la domanda operativa: *se sostituisco tutto il prezzo USD che esiste, il + risultato di §21 si muove?* +In entrambi i test il confronto e' **APPAIATO**: stesso sottoinsieme, stessa finestra, stesse 10 +fasi, stesso codice del motore. Confrontare "19 gambe su Binance" con "10 su Coinbase" misurerebbe +l'UNIVERSO, non il venue (regola del progetto: un confronto appaiato eredita TUTTI i parametri +della riga in cui compare) — e per questo il TEST 1 riporta anche cosa fa U10 **su Binance** +rispetto a U13 su Binance, cioe' quanto del titolo di §21 questo sottoinsieme riproduce. + +MECCANISMO: CONGELATO. Il motore e' importato da `r0822b_xs01_oos.py` (che a sua volta replica +`sleeves._xsec_returns` a max|diff| = 0.0). Nessuna riga di strategia riscritta qui: se il motore +fosse riscritto, una differenza di venue e una differenza di implementazione sarebbero +indistinguibili. + +Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822b_depeg.py +Rete: sola lettura, <= 2 req/s, MAI nella finestra :25-:29 (rate limit per-IP condiviso col +collettore della catena opzioni; guasto del 29/07). Download in cache nello scratchpad: la +seconda corsa e' offline. +""" +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] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +import altlib as A # noqa: E402 +import r0822b_xs01_oos as X # noqa: E402 (motore CONGELATO + cache Binance) +from src.portfolio.sleeves import XS_CFG # noqa: E402 + +CACHE = X.CACHE.parent / "r0822b_depeg" +CACHE.mkdir(parents=True, exist_ok=True) + +# --------------------------------------------------------------------------- +# UNIVERSI E FINESTRE — dichiarati PRIMA di guardare i numeri +# --------------------------------------------------------------------------- +U13 = X.U13 # l'universo di §21 +# venue USD per asset. `None` = nessun venue USD con storia sulla finestra. +USD_SRC: dict[str, tuple[str, str] | None] = { + "BTC": ("coinbase", "BTC/USD"), "ETH": ("coinbase", "ETH/USD"), + "LTC": ("coinbase", "LTC/USD"), "LINK": ("coinbase", "LINK/USD"), + "AAVE": ("coinbase", "AAVE/USD"), "ADA": ("coinbase", "ADA/USD"), + "DOGE": ("coinbase", "DOGE/USD"), "SOL": ("coinbase", "SOL/USD"), + "AVAX": ("coinbase", "AVAX/USD"), "NEAR": ("coinbase", "NEAR/USD"), + "INJ": ("coinbase", "INJ/USD"), + "XRP": ("bitstamp", "XRP/USD"), # Coinbase l'ha sospeso 2021-01 -> 2023-07 + "BNB": None, # nessun venue USD prima del 2025-10 +} +U10 = ["BTC", "ETH", "LTC", "LINK", "AAVE", "ADA", "DOGE", "SOL", "AVAX", "XRP"] +T1_START, T1_END = "2021-09-30", "2024-01-01" # AVAX su Coinbase e' il vincolo +T2_START, T2_END = "2021-01-01", "2024-01-01" # la finestra fuori campione di §21 +DEPEG = ("2022-05-07", "2022-05-21") # UST/LUNA + USDT a ~0.95 (estremo incluso) +PHASES = list(range(XS_CFG["H"])) # 10 fasi: la stima onesta e' la MEDIANA +CORR_SOGLIA = 0.99 # soglia DICHIARATA DALL'AUTORE, non da me + + +# =========================================================================== +# 1. FETCH USD (Coinbase / Bitstamp) + certificazione locale +# =========================================================================== +def _fetch_paged(ex, market: str, tf: str, start_ms: int, end_ms: int, + step_bars: int, bar_ms: int) -> list: + """Paginazione DETERMINISTICA sulla finestra: avanza anche quando una pagina torna vuota. + + ⚠️ Non e' un dettaglio: XRP su Coinbase ha un buco di 2,5 anni (sospensione SEC) e un loop che + si ferma alla prima pagina vuota — il modo ovvio di scrivere questo ciclo — troncherebbe la + serie li', producendo un 'asset che non esiste' invece di un 'asset con un buco'. Sono due + fatti diversi e portano a due decisioni diverse. + """ + rows, cur, span = [], start_ms, step_bars * bar_ms + while cur < end_ms: + X._outside_forbidden_window() + batch = [] + for attempt in range(3): + try: + batch = ex.fetch_ohlcv(market, tf, since=cur, limit=step_bars) + break + except Exception as e: # noqa: BLE001 + if attempt == 2: + print(f" [rete] {market} @ {cur}: {type(e).__name__} {str(e)[:70]}") + time.sleep(2 ** attempt) + time.sleep(0.55) # <= 2 req/s + rows += [b for b in (batch or []) if cur <= b[0] < cur + span] + cur += span + return rows + + +def fetch_usd_daily(asset: str) -> pd.DataFrame: + """Close giornalieri USD dal venue dichiarato in USD_SRC. Cache su disco.""" + src = USD_SRC.get(asset) + if src is None: + return pd.DataFrame(columns=["timestamp", "close", "volume"]) + venue, market = src + f = CACHE / f"{venue[:2]}_{asset.lower()}_1d.parquet" + if f.exists(): + return pd.read_parquet(f) + import ccxt + ex = getattr(ccxt, venue)({"enableRateLimit": True}) + ex.load_markets() + if market not in ex.markets: + d = pd.DataFrame(columns=["timestamp", "open", "high", "low", "close", "volume"]) + d.to_parquet(f) + return d + start = ex.parse8601("2019-01-01T00:00:00Z") + end = int(time.time() * 1000) + step = 300 if venue == "coinbase" else 1000 + rows = _fetch_paged(ex, market, "1d", start, end, step, 86_400_000) + d = (pd.DataFrame(rows, columns=["timestamp", "open", "high", "low", "close", "volume"]) + .drop_duplicates("timestamp").sort_values("timestamp").reset_index(drop=True)) + d.to_parquet(f) + return d + + +def series_of(d: pd.DataFrame) -> pd.Series: + if not len(d): + # indice tz-aware anche da vuoto: un asset senza venue USD (BNB) deve poter essere + # confrontato/reindicizzato come gli altri, non far cadere lo script + return pd.Series(dtype=float, index=pd.DatetimeIndex([], tz="UTC")) + return pd.Series(d["close"].values.astype(float), + index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)) + + +def certify_usd(asset: str, d: pd.DataFrame, a: str, b: str) -> dict: + """Stessa forma della certificazione di §21, ma valutata SULLA FINESTRA DEL TEST. + + Certificare "tutta la serie" direbbe la cosa sbagliata: XRP su Coinbase ha 900 giorni di buco + che sulla finestra 2021-09/2023-12 sono TUTTI dentro. Il numero utile e' la copertura della + finestra in cui il meccanismo gira, non della serie. + """ + s = series_of(d) + s = s[(s.index >= pd.Timestamp(a, tz="UTC")) & (s.index < pd.Timestamp(b, tz="UTC"))] + if not len(s): + return dict(sym=asset, n=0, ok=False, start="-", end="-", gap=-1, + flat=float("nan"), zero_vol=float("nan")) + v = series_of(d.assign(close=d["volume"])) + v = v[(v.index >= pd.Timestamp(a, tz="UTC")) & (v.index < pd.Timestamp(b, tz="UTC"))] + attesi = (pd.Timestamp(b, tz="UTC") - s.index.min()).days + c = s.values + flat = float(np.mean(c[1:] == c[:-1])) if len(c) > 1 else float("nan") + zv = float(np.mean(v.values == 0.0)) if len(v) else float("nan") + gap = int(attesi - len(s)) + return dict(sym=asset, n=len(s), start=str(s.index.min().date()), + end=str(s.index.max().date()), gap=gap, flat=flat, zero_vol=zv, + ok=bool(gap == 0 and flat < 0.02 and (not np.isfinite(zv) or zv < 0.01))) + + +# =========================================================================== +# 2. pannelli +# =========================================================================== +def bin_panel(syms: list[str]) -> pd.DataFrame: + """Pannello Binance USDT — dalla cache di §21, cosi' il lato USDT del confronto e' BIT-IDENTICO + a quello che ha prodotto il risultato che sto falsificando.""" + return pd.concat({s: series_of(X.fetch_binance_daily(s)) for s in syms}, axis=1).sort_index() + + +def usd_panel(syms: list[str]) -> pd.DataFrame: + return pd.concat({s: series_of(fetch_usd_daily(s)) for s in syms}, axis=1).sort_index() + + +def splice(usd: pd.Series, usdt: pd.Series) -> pd.Series: + """Serie IBRIDA: USD dove esiste, USDT prima — RI-SCALANDO il tratto USDT al livello USD nel + giorno del giunto. + + ⚠️ Senza la ri-scalatura il giunto crea un rendimento fantasma pari allo scarto fra i venue + (pochi bps, ma esattamente il tipo di artefatto che questo script deve misurare, non fabbricare). + Il meccanismo legge RAPPORTI di prezzo a 30 e 90 giorni: dopo la ri-scalatura la serie e' + continua e i rapporti che attraversano il giunto sono ben definiti. + """ + u = usd.dropna() + if not len(u): + return usdt + t0 = u.index[0] + if t0 not in usdt.index or not np.isfinite(usdt.get(t0, np.nan)): + return usd.reindex(usdt.index) + k = float(u.iloc[0]) / float(usdt.loc[t0]) + pre = usdt[usdt.index < t0] * k + return pd.concat([pre, u]).sort_index().reindex(usdt.index) + + +def hybrid_panel(syms: list[str]) -> tuple[pd.DataFrame, dict]: + B, U = bin_panel(syms), usd_panel(syms) + cols, meta = {}, {} + for s in syms: + u = U[s] if s in U.columns else pd.Series(dtype=float) + u = u.reindex(B.index) + cols[s] = splice(u, B[s]) + cov = u.dropna() + meta[s] = (str(cov.index[0].date()) if len(cov) else "mai") + return pd.concat(cols, axis=1).sort_index(), meta + + +# =========================================================================== +# 3. metriche e allineamento appaiato +# =========================================================================== +sh, dd, cagr, win, alive, line = X.sh, X.dd, X.cagr, X.win, X.alive, X.line + + +def pair(a: pd.Series, b: pd.Series) -> tuple[pd.Series, pd.Series]: + """Allinea due sleeve sullo stesso indice: un confronto di venue non puo' confrontare finestre.""" + j = pd.concat({"a": a, "b": b}, axis=1, join="inner").dropna() + return j["a"], j["b"] + + +def ranking_scan(PB: pd.DataFrame, PU: pd.DataFrame, phases: list[int], + label: str) -> pd.DataFrame: + """IL CANALE DEL DANNO, misurato dove passa: il venue puo' far male SOLO cambiando le gambe + scelte. Scansione di TUTTE le fasi (non solo la canonica: un ribaltamento su una fase sola + sarebbe invisibile all'ancora 0) con self-check dello specchio contro il motore.""" + from scipy.stats import spearmanr + lb = XS_CFG["lookbacks"] + mlb, H, k = max(lb), XS_CFG["H"], XS_CFG["k"] + rows, sp, checked, tot = [], [], 0, 0 + for ph in phases: + det: dict = {} + X.xsec_engine(PB, phase=ph, detail=det) + WB = det["W"] + for i in range(mlb, len(PB)): + if i % H != ph: + continue + iB, sB = score_at(PB, i, lb) + iU, sU = score_at(PU, i, lb) + if len(iB) < 10 or len(iU) < 10: + continue + tot += 1 + oB = np.argsort(sB) + loB, hiB = set(iB[oB[:k]]), set(iB[oB[-k:]]) + mh, ml = set(np.flatnonzero(WB[i] > 0)), set(np.flatnonzero(WB[i] < 0)) + if mh or ml: # self-check solo dove il motore ha una posizione + checked += 1 + if (mh, ml) != (hiB, loB): + raise AssertionError(f"specchio != motore ({label}, fase {ph}, i={i}): " + "misurerei la mia riscrittura invece del venue") + oU = np.argsort(sU) + loU, hiU = set(iU[oU[:k]]), set(iU[oU[-k:]]) + ov = (len(hiB & hiU) + len(loB & loU)) / (2 * k) + sp.append(float(spearmanr(sB, sU).statistic)) + if ov < 1.0: + diff = sorted(set(PB.columns[list(hiB ^ hiU)]) | set(PB.columns[list(loB ^ loU)])) + rows.append(dict(fase=ph, data=PB.index[i], overlap=ov, gambe=",".join(diff))) + D = pd.DataFrame(rows) + print(f" [{label}] {tot} ribilanciamenti su {len(phases)} fasi; specchio verificato " + f"contro il motore in {checked}/{checked} con posizione") + print(f" [{label}] Spearman dello score fra i venue: mediana {np.median(sp):.4f} " + f"min {np.min(sp):.4f}") + print(f" [{label}] ribilanciamenti con gambe DIVERSE: {len(D)}/{tot} " + f"({len(D)/max(tot,1)*100:.2f}%)") + for _, r in D.iterrows(): + print(f" fase {int(r['fase'])} {r['data'].date()} " + f"sovrapposizione {r['overlap']*100:3.0f}% gambe scambiate: {r['gambe']}") + return D + + +def score_at(P: pd.DataFrame, i: int, lookbacks) -> tuple[np.ndarray, np.ndarray]: + """Specchio della riga di score del motore (z-score medio sui lookback). Serve per confrontare + i RANKING fra venue; la sezione [4] verifica che i top-k/bottom-k che ne derivano coincidano + ESATTAMENTE con quelli scelti dal motore — se lo specchio divergesse, ogni numero di ranking + misurerebbe la mia riscrittura invece del venue.""" + px = P.values.astype(float) + elig = np.isfinite(px[i]) + for L in lookbacks: + elig = elig & np.isfinite(px[i - L]) + idx = np.flatnonzero(elig) + sc, cnt = np.zeros(len(idx)), 0 + for L in lookbacks: + r = px[i, idx] / px[i - L, idx] - 1.0 + if r.std() > 0: + sc += (r - r.mean()) / r.std() + cnt += 1 + return idx, (sc / cnt if cnt else sc) + + +# =========================================================================== +def main() -> None: + t0 = time.time() + pd.set_option("display.width", 200) + print("=" * 100) + print("DEPEG — il close USDT 2021-2023 puo' aver cambiato il RANKING di XS01-OOS?") + print(f"XS_CFG (CONGELATO) = {XS_CFG} soglia dichiarata dall'autore: corr >= {CORR_SOGLIA}") + print("=" * 100) + + # ------------------------------------------------------------- [0] sanity + print("\n[0] SANITY — il motore importato e' quello che ha prodotto §21?") + from src.portfolio.sleeves import _xsec_returns + off = _xsec_returns() + rep = X.xsec_engine(X.hl_panel(), phase=0) + j = pd.concat({"o": off, "r": rep}, axis=1, join="inner").dropna() + md = float(np.max(np.abs(j["o"] - j["r"]))) + print(f" motore vs sleeve UFFICIALE su HL: n={len(j)} max|diff| = {md:.2e} " + f"-> {'IDENTICO' if md < 1e-12 else 'DIVERGE — STOP'}") + if md >= 1e-12: + return + + # ------------------------------------------------------- [1] dato USD + print("\n[1] DATO USD — quanti dei 13 asset di §21 hanno un prezzo in DOLLARI nel 2021-2023?") + print(f" {'sym':<6}{'venue USD':>11}{'n':>6}{'start':>12}{'end':>12}{'gap':>5}" + f"{'flat%':>8}{'vol0%':>8} (finestra {T1_START} .. {T1_END})") + certs = {} + for s in U13: + d = fetch_usd_daily(s) + c = certify_usd(s, d, T1_START, T1_END) + certs[s] = c + ven = USD_SRC[s][0] if USD_SRC[s] else "—" + flag = "" if c["ok"] else " <-- non usabile su questa finestra" + print(f" {s:<6}{ven:>11}{c['n']:>6}{c['start']:>12}{c['end']:>12}{c['gap']:>5}" + f"{c['flat']*100:>8.2f}{c['zero_vol']*100:>8.2f}{flag}") + usabili = [s for s in U13 if certs[s]["ok"]] + print(f" -> usabili sulla finestra del TEST 1: {len(usabili)}/13 {usabili}") + print(f" -> ESCLUSI: BNB (nessun venue USD prima del 2025-10 — e' l'asset nativo del venue") + print(f" sotto esame), NEAR e INJ (Coinbase solo dal 2022-09).") + print(f" ⚠️ LIMITE DEL TEST, da dire subito: 10 gambe su 13 e finestra che parte dal") + print(f" {T1_START} (AVAX su Coinbase) -> il TEST 1 NON copre il 2021, che in §21 e'") + print(f" l'anno piu' forte (+2,14). Copre pero' ESATTAMENTE cio' che l'obiezione nomina:") + print(f" il depeg di maggio 2022 e FTX. Il 2021 lo copre il TEST 2 (ibrido).") + + # controllo di ALLINEAMENTO: le barre USD sono sulla stessa griglia di quelle USDT? + print("\n Controllo di ALLINEAMENTO — se le barre USD e USDT fossero su griglie sfasate,") + print(" ogni numero sotto sarebbe uno sfasamento di calendario travestito da divergenza") + print(" di venue. corr dei RITORNI giornalieri, 2024+ (finestra dove esistono entrambi):") + B24 = bin_panel(U13) + U24 = usd_panel(U13) + al, shifted = [], [] + for s in U13: + if s not in U24.columns or not U24[s].notna().any(): + continue + k = pd.concat({"u": U24[s], "b": B24[s]}, axis=1, join="inner").dropna() + k = k[k.index >= pd.Timestamp("2024-01-01", tz="UTC")].pct_change().dropna() + if len(k) > 200: + al.append((s, float(k["u"].corr(k["b"])))) + shifted.append(float(k["u"].shift(1).corr(k["b"]))) # controllo POSITIVO + print(" " + " ".join(f"{s}:{c:.4f}" for s, c in al)) + lo_al = min(c for _, c in al) + # ⚠️ Un controllo che non fallisce mai non e' un controllo: il modo di fallire e' uno + # sfasamento di UNA barra, e va mostrato che il numero se ne accorge. Con la serie USD + # ritardata di un giorno la corr collassa: e' quella la scala del guasto che sto escludendo. + print(f" corr minima {lo_al:.4f} | controllo POSITIVO (USD ritardato di 1 giorno): " + f"corr mediana {np.median(shifted):+.4f}, massima {np.max(shifted):+.4f}") + if lo_al < 0.95: + print(" -> SFASAMENTO: i due venue non sono sulla stessa griglia. STOP.") + return + print(" -> allineati (uno sfasamento darebbe ~0, non 0,99). Gli asset sotto 0,999") + print(f" ({', '.join(s for s, c in al if c < 0.999)}) sono solo piu' RUMOROSI in USD") + print(" (book piu' sottile sul venue USD), non sfasati.") + + # --------------------------------------------- [2] la divergenza, in DUE pezzi + print("\n[2] LA DIVERGENZA USD-vs-USDT, scomposta — e il pezzo che conta e' il SECONDO") + Bp = bin_panel(U10).loc[T1_START:T1_END] + Up = usd_panel(U10).reindex(Bp.index) + both = Bp.notna() & Up.notna() + D = 1e4 * np.log(Up / Bp) # scarto di livello, bps + D = D.where(both) + common = D.median(axis=1) # (a) componente COMUNE (il peg + il venue) + idio = D.sub(common, axis=0) # (b) componente IDIOSINCRATICA (cambia il rango) + print(f" {len(Bp)} giorni, {len(U10)} asset, {int(both.values.sum())} osservazioni appaiate") + print(f" (a) COMUNE |mediana per giorno|: med {common.abs().median():7.1f} bps " + f"p95 {common.abs().quantile(.95):7.1f} max {common.abs().max():7.1f}") + print(f" (b) IDIOSINCRATICO |scarto de-medianato|: med {idio.abs().stack().median():7.1f} bps " + f"p95 {idio.abs().stack().quantile(.95):7.1f} max {idio.abs().stack().max():7.1f}") + print(" -> (a) e' cio' che lo z-score cross-sezionale annulla per costruzione; (b) no.") + + print(f"\n LA FINESTRA DEL DEPEG {DEPEG[0]} .. {DEPEG[1]} (UST/LUNA; USDT a ~0,95):") + dm = (D.index >= pd.Timestamp(DEPEG[0], tz="UTC")) & (D.index < pd.Timestamp(DEPEG[1], tz="UTC")) + print(f" {'giorno':<12}{'comune':>9}" + "".join(f"{s:>8}" for s in U10)) + for t in D.index[dm]: + print(f" {str(t.date()):<12}{common.loc[t]:>9.0f}" + + "".join(f"{idio.loc[t, s]:>8.0f}" if np.isfinite(idio.loc[t, s]) else f"{'—':>8}" + for s in U10)) + print(" (numeri in bps; colonna 'comune' = mediana del giorno, le altre = scarto DOPO") + print(" averla tolta, cioe' la parte che puo' spostare il ranking)") + idm = idio[dm].abs().stack() + print(f" idiosincratico NELLA finestra: med {idm.median():.1f} bps p95 " + f"{idm.quantile(.95):.1f} max {idm.max():.1f} " + f"(FUORI: med {idio[~dm].abs().stack().median():.1f}, " + f"p95 {idio[~dm].abs().stack().quantile(.95):.1f})") + + print("\n Ma il ranking non legge il LIVELLO: legge i rapporti a 30 e 90 giorni. Divergenza") + print(" idiosincratica degli STESSI rendimenti che entrano nello score (bps):") + for L in XS_CFG["lookbacks"]: + rB = (Bp / Bp.shift(L) - 1.0) * 1e4 + rU = (Up / Up.shift(L) - 1.0) * 1e4 + dR = (rU - rB).where(both) + dRi = dR.sub(dR.median(axis=1), axis=0) + w = dRi[dm].abs().stack() + print(f" r{L:<3d}: idiosincratico med {dRi.abs().stack().median():6.1f} bps " + f"p95 {dRi.abs().stack().quantile(.95):7.1f} max {dRi.abs().stack().max():8.1f}" + f" | nel depeg med {w.median():6.1f} max {w.max():8.1f}") + + # ------------------------------------------- [3] IL TEST 1 — U10, k=5 congelato + print(f"\n[3] TEST 1 — meccanismo CONGELATO su U10, {T1_START} .. {T1_END}, USD vs USDT") + print(f" 10 gambe = ESATTAMENTE il minimo del meccanismo (min_assets=10, k=5): nessun") + print(f" parametro toccato, e il confronto e' appaiato su TUTTO tranne il venue.") + PB = Bp[U10].dropna() + PU = Up[U10].dropna() + idxc = PB.index.intersection(PU.index) + PB, PU = PB.loc[idxc], PU.loc[idxc] + print(f" pannello comune: {len(idxc)} giorni, {PB.index[0].date()} .. {PB.index[-1].date()}") + + rows = [] + for ph in PHASES: + a = alive(X.xsec_engine(PU, phase=ph)) + b = alive(X.xsec_engine(PB, phase=ph)) + a, b = pair(a, b) + rows.append(dict(ph=ph, corr=float(a.corr(b)), sh_usd=sh(a), sh_usdt=sh(b), + d=sh(a) - sh(b), dd_usd=dd(a), dd_usdt=dd(b), n=len(a))) + R = pd.DataFrame(rows) + print(f" {'fase':>5}{'corr':>9}{'Sh USD':>9}{'Sh USDT':>9}{'dSharpe':>10}" + f"{'DD USD':>9}{'DD USDT':>9}") + for _, r in R.iterrows(): + print(f" {int(r['ph']):>5}{r['corr']:>9.4f}{r['sh_usd']:>9.2f}{r['sh_usdt']:>9.2f}" + f"{r['d']:>+10.3f}{r['dd_usd']*100:>8.1f}%{r['dd_usdt']*100:>8.1f}%") + print(f" {'MEDIANA':>5}{R['corr'].median():>9.4f}{R['sh_usd'].median():>9.2f}" + f"{R['sh_usdt'].median():>9.2f}{R['d'].median():>+10.3f}" + f"{R['dd_usd'].median()*100:>8.1f}%{R['dd_usdt'].median()*100:>8.1f}%") + print(f" corr MINIMA fra le 10 fasi: {R['corr'].min():.4f} (fase {int(R.loc[R['corr'].idxmin(),'ph'])})" + f" | soglia dichiarata {CORR_SOGLIA} -> " + f"{'PASS' if R['corr'].min() >= CORR_SOGLIA else 'FAIL'}") + print(f" dSharpe appaiato (USD - USDT): mediana {R['d'].median():+.3f} " + f"banda [{R['d'].min():+.3f},{R['d'].max():+.3f}] " + f"fasi in cui USD e' MEGLIO {(R['d'] > 0).sum()}/{len(R)}") + print(f" ⚠️ la statistica e' la MEDIANA DELLE DIFFERENZE APPAIATE, non la differenza delle") + print(f" mediane (lezione 26/07: quest'ultima confronta fasi diverse e puo' ribaltare).") + + # quanto di §21 riproduce questo sottoinsieme? (o si misura l'universo, non il venue) + p13 = bin_panel(U13)[U13].dropna() + s13_w = win(alive(X.xsec_engine(p13, phase=0)), T1_START, T1_END) + s10_w = win(alive(X.xsec_engine(PB, phase=0)), T1_START, T1_END) + print(f"\n Contesto obbligatorio — quanto di §21 vive in questo sottoinsieme (Binance, fase 0,") + print(f" stessa finestra): U13 Sh {sh(s13_w):+.2f} | U10 Sh {sh(s10_w):+.2f}") + print(f" -> il TEST 1 giudica il VENUE su U10; NON e' una replica del titolo di §21.") + + # ------------------------------------------------ [4] i RANKING, non i P&L + print("\n[4] IL CANALE DEL DANNO — cambia il RANKING? (e' cio' che la strategia legge)") + print(" Un P&L simile puo' nascere da due ranking diversi che si compensano: la domanda") + print(" diretta e' se le GAMBE SCELTE cambiano. Scansione su tutte e 10 le fasi.") + D1 = ranking_scan(PB, PU, PHASES, "TEST 1 / U10") + if len(D1): + ind = D1[(D1["data"] >= pd.Timestamp(DEPEG[0], tz="UTC")) & + (D1["data"] < pd.Timestamp(DEPEG[1], tz="UTC"))] + print(f" di cui dentro la finestra del depeg: {len(ind)}") + + # ------------------------------- [5] TEST 2 — U13 ibrido, finestra INTERA di §21 + print(f"\n[5] TEST 2 — U13 IBRIDO (USD dove esiste, USDT altrove), {T2_START} .. {T2_END}") + print(" Domanda operativa: sostituendo TUTTO il prezzo in dollari che esiste, il titolo") + print(" di §21 (mediana di fase +1,12 fuori campione) si muove?") + HB, meta = hybrid_panel(U13) + print(" prima data USD per asset: " + " ".join(f"{s}:{meta[s]}" for s in U13)) + PBh = p13 + PUh = HB[U13].dropna() + ic = PBh.index.intersection(PUh.index) + PBh, PUh = PBh.loc[ic], PUh.loc[ic] + rows2 = [] + for ph in PHASES: + a = win(alive(X.xsec_engine(PUh, phase=ph)), T2_START, T2_END) + b = win(alive(X.xsec_engine(PBh, phase=ph)), T2_START, T2_END) + a, b = pair(a, b) + rows2.append(dict(ph=ph, corr=float(a.corr(b)), sh_usd=sh(a), sh_usdt=sh(b), + d=sh(a) - sh(b), dd_usd=dd(a), dd_usdt=dd(b))) + R2 = pd.DataFrame(rows2) + print(f" {'fase':>5}{'corr':>9}{'Sh IBRIDO':>11}{'Sh USDT':>10}{'dSharpe':>10}" + f"{'DD IBR':>9}{'DD USDT':>9}") + for _, r in R2.iterrows(): + print(f" {int(r['ph']):>5}{r['corr']:>9.4f}{r['sh_usd']:>11.2f}{r['sh_usdt']:>10.2f}" + f"{r['d']:>+10.3f}{r['dd_usd']*100:>8.1f}%{r['dd_usdt']*100:>8.1f}%") + print(f" {'MEDIANA':>5}{R2['corr'].median():>9.4f}{R2['sh_usd'].median():>11.2f}" + f"{R2['sh_usdt'].median():>10.2f}{R2['d'].median():>+10.3f}" + f"{R2['dd_usd'].median()*100:>8.1f}%{R2['dd_usdt'].median()*100:>8.1f}%") + print(f" corr minima {R2['corr'].min():.4f} -> " + f"{'PASS' if R2['corr'].min() >= CORR_SOGLIA else 'FAIL'} (soglia {CORR_SOGLIA})") + print(f" 📌 IL NUMERO DI TESTA DI §21, RICALCOLATO IN DOLLARI: mediana di fase fuori") + print(f" campione {R2['sh_usdt'].median():+.2f} (USDT, replica) -> " + f"{R2['sh_usd'].median():+.2f} (IBRIDO USD).") + + # differenza appaiata con la macchineria del progetto (stesso gate di §21) + dlt = A.anchor_luck_delta( + lambda ph: win(alive(X.xsec_engine(PUh, phase=ph)), T2_START, T2_END), + lambda ph: win(alive(X.xsec_engine(PBh, phase=ph)), T2_START, T2_END), + PHASES, metric=sh) + print(f" anchor_luck_delta (IBRIDO - USDT): mediana appaiata {dlt['median_paired']:+.3f} " + f"[{dlt['lo']:+.3f},{dlt['hi']:+.3f}], fasi con IBRIDO>USDT " + f"{dlt['n_positive']}/{dlt['n_anchors']}") + print("\n E il ranking, sull'ibrido (la fase con la corr piu' bassa va GUARDATA, non") + print(" riassunta: una corr di 0,97 su una fase o e' un ribaltamento vero o e' un giunto):") + D2 = ranking_scan(PBh, PUh, PHASES, "TEST 2 / U13-IBRIDO") + + # ------------------------------- [5b] le gambe che NESSUN venue USD puo' controllare + print("\n[5b] LE TRE GAMBE NON CONTROLLABILI — quanto del risultato di §21 ci passa?") + print(" BNB e' il token nativo del venue sotto esame e NON esiste in USD nel 2021-23;") + print(" NEAR e INJ hanno un prezzo USD solo dal 2022-09. Se il titolo di §21 vivesse su") + print(" di loro, questo falsificatore non avrebbe potuto vedere niente, e andrebbe detto.") + base_b = A.anchor_luck_band( + lambda ph: win(alive(X.xsec_engine(PBh, phase=ph)), T2_START, T2_END), + PHASES, canonical=0, metric=sh) + print(f" base U13 (Binance, fuori campione, mediana di fase): {base_b['median']:+.2f}") + for a_ in ("BNB", "NEAR", "INJ"): + sub = p13[[c for c in U13 if c != a_]].dropna() + bb = A.anchor_luck_band( + lambda ph, sub=sub: win(alive(X.xsec_engine(sub, phase=ph, min_assets=10)), + T2_START, T2_END), + PHASES, canonical=0, metric=sh) + print(f" senza {a_:<5s}: mediana di fase {bb['median']:+.2f} " + f"({bb['median']-base_b['median']:+.2f}) banda [{bb['lo']:+.2f},{bb['hi']:+.2f}]") + print(" (leave-one-out a 12 gambe: il meccanismo congelato regge, min_assets=10)") + + # ---------------------------------------------------- [6] IL CASO PEGGIORE + print("\n[6] IL CASO PEGGIORE — non la media: dove i due venue divergono di piu'?") + print(f" {'anno':>6}" + "".join(f"{k:>12}" for k in + ("corr T1", "dSh T1", "corr T2", "dSh T2"))) + for y in (2021, 2022, 2023): + a1 = b1 = a2 = b2 = None + cs1, ds1, cs2, ds2 = [], [], [], [] + for ph in PHASES: + a1 = win(alive(X.xsec_engine(PU, phase=ph)), f"{y}-01-01", f"{y+1}-01-01") + b1 = win(alive(X.xsec_engine(PB, phase=ph)), f"{y}-01-01", f"{y+1}-01-01") + if len(a1) > 60 and len(b1) > 60: + a1, b1 = pair(a1, b1) + cs1.append(float(a1.corr(b1))); ds1.append(sh(a1) - sh(b1)) + a2 = win(alive(X.xsec_engine(PUh, phase=ph)), f"{y}-01-01", f"{y+1}-01-01") + b2 = win(alive(X.xsec_engine(PBh, phase=ph)), f"{y}-01-01", f"{y+1}-01-01") + if len(a2) > 60 and len(b2) > 60: + a2, b2 = pair(a2, b2) + cs2.append(float(a2.corr(b2))); ds2.append(sh(a2) - sh(b2)) + f1 = f"{np.median(cs1):.4f}" if cs1 else "—" + g1 = f"{np.median(ds1):+.3f}" if ds1 else "—" + f2 = f"{np.median(cs2):.4f}" if cs2 else "—" + g2 = f"{np.median(ds2):+.3f}" if ds2 else "—" + print(f" {y:>6}{f1:>12}{g1:>12}{f2:>12}{g2:>12}") + worst1 = R.loc[R["corr"].idxmin()] + worst2 = R2.loc[R2["corr"].idxmin()] + wd1 = R.loc[R["d"].abs().idxmax()] + wd2 = R2.loc[R2["d"].abs().idxmax()] + print(f" peggior FASE per corr: T1 fase {int(worst1['ph'])} corr {worst1['corr']:.4f} | " + f"T2 fase {int(worst2['ph'])} corr {worst2['corr']:.4f}") + print(f" peggior FASE per dSharpe: T1 fase {int(wd1['ph'])} {wd1['d']:+.3f} | " + f"T2 fase {int(wd2['ph'])} {wd2['d']:+.3f}") + + st = idio.abs().stack().sort_values(ascending=False) + print(" le 8 divergenze IDIOSINCRATICHE piu' grandi della finestra (bps di livello):") + for (t, s), v in st.head(8).items(): + indep = "DEPEG" if pd.Timestamp(DEPEG[0], tz="UTC") <= t < pd.Timestamp(DEPEG[1], tz="UTC") else "" + print(f" {str(t.date()):<12}{s:<6}{v:8.0f} bps (comune quel giorno " + f"{common.loc[t]:+.0f} bps) {indep}") + print(" -> se le peggiori NON cadono nel depeg, il rumore di venue e' book sottile,") + print(" non contaminazione da stablecoin: due cause diverse, due implicazioni diverse.") + + # il giorno peggiore: dove il P&L dei due sleeve si stacca di piu' + a0 = alive(X.xsec_engine(PU, phase=0)); b0 = alive(X.xsec_engine(PB, phase=0)) + a0, b0 = pair(a0, b0) + gap = (a0 - b0) + print(f" scarto GIORNALIERO fra i due sleeve (fase 0): med " + f"{gap.abs().median()*1e4:.1f} bps p99 {gap.abs().quantile(.99)*1e4:.1f} bps " + f"max {gap.abs().max()*1e4:.1f} bps il {gap.abs().idxmax().date()}") + cum = (1 + a0).cumprod().iloc[-1] / (1 + b0).cumprod().iloc[-1] - 1 + print(f" divario CUMULATO su tutta la finestra del TEST 1: {cum*100:+.2f}% " + f"(equity USD vs equity USDT)") + + # --------------------------------------------------------------- [7] gate + print("\n[7] GATE") + print(f" soglia di venue (dichiarata dall'autore PRIMA, non da me): corr >= {CORR_SOGLIA}") + print(f" TEST 1 U10 congelato : corr mediana {R['corr'].median():.4f}, minima " + f"{R['corr'].min():.4f} -> {'PASS' if R['corr'].min() >= CORR_SOGLIA else 'FAIL'}") + print(f" TEST 2 U13 ibrido : corr mediana {R2['corr'].median():.4f}, minima " + f"{R2['corr'].min():.4f} -> {'PASS' if R2['corr'].min() >= CORR_SOGLIA else 'FAIL'}") + print(f" riferimento 2024+ di §21 (Binance vs Hyperliquid): 0,9991 / dSharpe -0,025") + su = alive(X.xsec_engine(PUh, phase=0)) + mg = A.marginal_vs_tp01(win(su, T2_START, T2_END)) + print(f" marginal_vs_tp01 sullo sleeve IBRIDO fuori campione: {mg.get('marginal_verdict')} " + f"(corr {mg.get('corr_full')}, is_hedge {mg.get('is_hedge')}, " + f"insample_edge {mg.get('has_insample_edge')})") + imp = A.implausible_sharpe(win(su, T2_START, T2_END)) + print(f" implausible_sharpe (ibrido OOS): {imp['implausible']} Sh {imp.get('sharpe'):+.2f} " + f"perdite su barre attive {imp.get('loss_frac', float('nan'))*100:.1f}%") + print(" causalita': NON RI-GIRATA — il motore e' importato invariato da r0822b_xs01_oos.py,") + print(" che la verifica (prezzi futuri randomizzati -> max|diff| = 0). Cambiare il venue") + print(" cambia i dati, non l'ordine temporale in cui il motore li legge.") + print(" deflated_sharpe: NON PERTINENTE — qui non si SELEZIONA una cella, si confronta due") + print(" volte lo STESSO meccanismo su due fonti di prezzo. Nessun trial nuovo di strategia.") + print(" null de-levering: NON PERTINENTE (nessun claim di 'meno drawdown').") + print(" anchor: girato per costruzione — ogni numero sopra e' su tutte e 10 le fasi.") + + print(f"\n[fine] {time.time()-t0:.1f}s") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822b_prop_recal.py b/scripts/research/r0822b_prop_recal.py new file mode 100644 index 0000000..e39acd8 --- /dev/null +++ b/scripts/research/r0822b_prop_recal.py @@ -0,0 +1,637 @@ +"""r0822b_prop_recal — PROP-ALLOC RIFATTO con XS01 preso dalla STORIA LUNGA (2026-08-22, ondata 2). + +IL BUCO CHE CHIUDE +------------------ +Il risultato di testa dell'ondata (RESULTS-0822 §1, PROP-ALLOC) e': su un conto FUNDED da $100k +l'allocazione ottima sotto barriera di drawdown mette **XS01 al 50%** e porta +J = P(pass) x P(vivo 12m) da 0,335 (libro live 75/25) a **0,738**, con +**P(>=50 EUR/g) da 600 EUR in 36 mesi = 42%**. Quel filone ha dichiarato da solo il proprio limite: + + "tutto il vantaggio poggia sul drift di XS01 misurato sulla sua finestra di scoperta" + +e ha proposto un gate che era un'ATTESA: *non aprire un funded prima che XS01 abbia una finestra +fuori dal 2024-2026*. Il filone XS01-OOS (RESULTS-0822 §21) ha trovato quella finestra lo stesso +giorno — stessi 19 ticker su Binance spot USDT 1d, **2021-01 -> 2023-12, con LUNA e FTX dentro** — +e ha misurato che l'edge di RENDIMENTO regge (mediana di fase +1,12 fuori contro +0,37 dentro, +differenza appaiata +0,668 in 10/10 fasi). Ma ha anche misurato TRE correzioni nel verso opposto: + (i) il maxDD standalone di XS01 RADDOPPIA: 10,8% -> 20,9-22,0%; + (ii) il peso 0,50 non sopravvive: fuori campione a w=0,50 il maxDD di libro risale a 8,9% + contro 6,0% a w=0,25 — cioe' restituisce la protezione di DD che e' l'UNICA ragione per + mettere XS01 su un conto a barriera; + (iii) il 32% dell'universo non e' testabile (ARB/OP/SUI/APT/SEI/TIA non quotati nel 2021-23) e + su di esso vive gran parte del numero di scoperta (19 gambe 1,31 vs 13 gambe 0,54). + +Questo script rifa' il conto di PROP-ALLOC con la serie LUNGA al posto di quella di scoperta e +risponde a UNA domanda: **J regge?** + +LA PREVISIONE, REGISTRATA PRIMA DI MISURARE +------------------------------------------- +Mi aspetto che J SCENDA ma resti sopra il libro live, per tre ragioni che tirano in versi diversi: + (a) il drift di XS01 fuori campione e' PIU' ALTO -> spinge J su; + (b) il suo maxDD raddoppia e la barriera e' STATICA (una soglia assoluta non e' scale-invariante) + -> spinge J giu', e su un conto a barriera questa e' la forza che conta; + (c) la finestra lunga contiene il 2022, quindi TP01 (lo sleeve DIFENSIVO, sotto-pesato da + PROP-ALLOC su un campione senza sinistro) ci arriva con un curriculum diverso -> mi aspetto + che l'argmax RIALZI TP01 e ABBASSI XS01 rispetto al 25/25/50. +Previsione numerica dichiarata prima: argmax con w_XS in [0,25 ; 0,38], J fra 0,50 e 0,70, +P(>=50/g) fra il 10% e il 35%. Se J restasse >= 0,70 con w_XS = 0,50 la previsione e' sbagliata e +lo dico; se J crollasse sotto il livello del libro live, il fronte funded va ripensato ADESSO. + +DISCIPLINA DI RIPRODUZIONE (non negoziabile) +-------------------------------------------- +Prima di pubblicare un numero nuovo la macchina deve riprodurre quello vecchio. §0 rigira +PROP-ALLOC con la sua stessa macchineria (`r0822_prop_alloc` IMPORTATO, non riscritto), stessi +semi, stessa griglia, stesso de-luck, e confronta con i valori PUBBLICATI, cablati qui sotto in +`PUB`. Se la riproduzione fallisce, quello E' il risultato e viene detto. + +CONVENZIONI DICHIARATE +---------------------- +* Lente **ACCOPPIATA obbligatoria**. Su una regola di DD a UN GIORNO la lente close-only e' + esattamente CIECA (misurato oggi: 0,00 breach contro 0,40-1,21 veri, rapporto INF). La + macchineria del wick e' quella di `r0725_prop_coupled` (crypto: minimo ESATTO sul path orario + condiviso TP01+SKH01; XS01: 4 checkpoint O/estremo/estremo/C con ordinamento CONDIVISO e ordine + AVVERSO dagli OHLC giornalieri). Per la finestra 2021-23 gli OHLC sono quelli **Binance** della + cache di `r0822b_xs01_oos` — il costruttore di tuple e' lo STESSO codice e viene validato in §1 + contro `pc.xsec_daily_tuples()` sui dati HL, dove deve dare max|diff| = 0. +* **Universo dichiarato: U13** (i 13 dei 19 quotati prima del 2020-11) come serie PRIMARIA, perche' + e' l'unica testabile fuori campione e perche' e' la versione **PIU' DEBOLE** di quella che sta nel + libro (13 gambe invece di 19: XS01-OOS misura 0,54 contro 1,31 sulla stessa finestra e venue). + Scegliere la piu' debole e' conservativo per la decisione. U19-EXP (universo che si allarga da 10 + a 19 gambe) e' riportato come **altro capo della banda**, mai come numero di testa. +* Meccanismo **CONGELATO** (XS_CFG: blend [30,90], H=10, k=5, gate p30, vol-target 20%, 5 bps/lato). + Nessun parametro toccato, nessuna cella cercata: qui non si seleziona, si RI-MISURA. +* de-luck sul drift **x0,89** (misurato 26/07), identico a PROP-ALLOC. +* **Distorsioni portate avanti DICHIARATE, non rimosse in silenzio:** + (1) la banda d'ancora NON e' girata sotto questa lente (fuori budget il 22/08 e fuori budget + qui): 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 distorsione e' A FAVORE + del risultato. Lo stress SKH x0,50 / x0,33 e' li' apposta. + (2) l'intero risultato poggia sull'assunto — **MAI VERIFICATO CON UNA FIRM** — che una firm + crypto listi i 19 (qui 13) alt Hyperliquid **con short abilitato**. Il critico dell'ondata + lo chiama "la ripetizione esatta dell'errore GTAA01/PRIIPs". Non e' verificabile da qui + (serve leggere un listino), quindi §6 misura quanto del risultato MUORE se e' falso. +* Monte Carlo: 2500 percorsi per J (come PROP-ALLOC), 1500 per la scala di conti. **Un Monte Carlo + ha una risoluzione e va detta**: SE su un livello di J ~ +-0,010, sulle differenze APPAIATE + (indici di bootstrap condivisi) molto meno, ~+-0,007. + +Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0822b_prop_recal.py` +Rete: NESSUNA (tutto da disco: 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] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +import r0725_prop_coupled as pc # noqa: E402 macchineria del wick accoppiato (25/07) +import r0725_prop_ladder as pl # noqa: E402 regole delle firm + scala di conti +import r0822_prop_alloc as PA # noqa: E402 IL filone da riprodurre (22/08) +import r0822b_xs01_oos as X # noqa: E402 motore XS01 + cache Binance (22/08) +from src.portfolio.sleeves import XS_UNIVERSE, _HL_DIR # noqa: E402 + +RAW = ROOT / "data" / "raw" +FEE_SIDE = 0.001 / 2.0 +HYRO = "HYRO" + +# ---------------------------------------------------------------- numeri PUBBLICATI da riprodurre +# (RESULTS-0822 §1, tabella "1 — PROP-ALLOC"; log della corsa nello scratchpad) +PUB = dict( + argmaxJ=dict(w="25/25/50", lev=0.750, J=0.738, p_pass=0.822, p_alive=0.898, sharpe=1.69), + argmaxS=dict(w="38/25/38", lev=0.750, J=0.721, p_pass=0.773, p_alive=0.933, sharpe=1.73), + live=dict(w="75/25/0", lev=0.500, J=0.335, p_pass=0.386, p_alive=0.868, sharpe=1.09), + dJ_vs_sharpe=0.0164, dJ_vs_live=0.3996, + ladder_med=39.28, ladder_p50=0.417, ladder_zero=0.139, +) + +LEVS = (0.25, 0.375, 0.50, 0.625, 0.75, 1.00, 1.25, 1.50) +STEP = 8 +N_PATHS = PA.N_PATHS # 2500 +EV_H = FU_H = 365 +SEED_EV, SEED_FU = 8220, 8221 # identici a PROP-ALLOC +W3 = PA.CRYPTO_PROP # ("TP01","SKH01","XS01") + +TRIALS = 0 # contatore della griglia dichiarata (al RIALZO) + + +def bump(n: int) -> None: + global TRIALS + TRIALS += n + + +def hr(t: str = "") -> None: + print("\n" + "-" * 104) + if t: + print(f" {t}") + print("-" * 104) + + +# =============================================================== 1. XS01 ACCOPPIATO, QUALSIASI DATO + +def ohlc_panels(loader, syms: list[str]) -> tuple[pd.DataFrame, ...]: + """(C, O, H, L) allineati. `loader(sym) -> DataFrame con timestamp/open/high/low/close`.""" + o, h, lo, c = {}, {}, {}, {} + for s in syms: + d = loader(s) + i = pd.to_datetime(d["timestamp"], unit="ms", utc=True) + o[s] = pd.Series(d["open"].astype(float).values, index=i) + h[s] = pd.Series(d["high"].astype(float).values, index=i) + lo[s] = pd.Series(d["low"].astype(float).values, index=i) + c[s] = pd.Series(d["close"].astype(float).values, index=i) + C = pd.concat(c, axis=1).sort_index() + return C, pd.concat(o, axis=1).reindex(C.index)[C.columns], \ + pd.concat(h, axis=1).reindex(C.index)[C.columns], \ + pd.concat(lo, axis=1).reindex(C.index)[C.columns] + + +def xs_tuples(C: pd.DataFrame, O: pd.DataFrame, H: pd.DataFrame, L: pd.DataFrame, + phase: int = 0, wick_mode: str = "shared", **kw) -> pd.DataFrame: + """(R, m) giornalieri di XS01 su un pannello QUALSIASI, con la stessa convenzione di wick di + `r0725_prop_coupled.xsec_daily_tuples`. Le posizioni vengono dal motore CONGELATO di + `r0822b_xs01_oos.xsec_engine` (che regge i NaN = asset non ancora quotato). + + Validato in §1 contro `pc.xsec_daily_tuples()` sui dati HL: deve dare max|diff| = 0.""" + det: dict = {} + r = X.xsec_engine(C, phase=phase, detail=det, **kw) + W, scale, turn = det["W"], det["scale"], det["turn"] + px = C.values.astype(float) + n, A = px.shape + prev_c = np.vstack([np.full((1, A), np.nan), px[:-1]]) + held = np.vstack([np.zeros((1, A)), W[:-1]]) # gambe TENUTE nel giorno i + with np.errstate(invalid="ignore"): + rO = O.values / prev_c - 1.0 + rH = H.values / prev_c - 1.0 + rL = L.values / prev_c - 1.0 + rC = px / prev_c - 1.0 + vO = np.nansum(held * rO, axis=1) + vH = np.nansum(held * rH, axis=1) + vL = np.nansum(held * rL, axis=1) + vC = np.nansum(held * rC, axis=1) + if wick_mode == "perleg": # bound severo: ogni gamba al suo peggio + worst = np.where(held >= 0, rL, rH) + m_gross = np.minimum(np.nansum(held * worst, axis=1), vC) + else: # 'shared': ordine AVVERSO + m_gross = np.minimum(np.minimum.reduce([vO, vL, vH, vC]), + np.minimum.reduce([vO, vH, vL, vC])) + m_net = (m_gross - turn * FEE_SIDE) * scale + R_net = r.values + out = pd.DataFrame({"R": R_net, "m": np.minimum(m_net, R_net)}, index=C.index) + return out + + +def bin_loader(sym: str) -> pd.DataFrame: + """Legge dalla cache di r0822b (nessuna rete) e SCARTA la barra del giorno in corso: una barra + giornaliera parziale in coda e' esattamente il difetto trovato oggi nei monitor forward.""" + d = X.fetch_binance_daily(sym) + today = pd.Timestamp.utcnow().normalize().value // 10**6 + return d[d["timestamp"] < today].reset_index(drop=True) + + +def hl_loader(sym: str) -> pd.DataFrame: + return pd.read_parquet(_HL_DIR / f"hl_{sym.lower()}_1d.parquet") + + +# =============================================================== 2. LIBRO A 3 GAMBE, PANNELLO LIBERO + +def rg(w: dict, idx: pd.DatetimeIndex, xs: pd.DataFrame, drop_xs: bool = False, + dm: tuple = ()) -> tuple[np.ndarray, np.ndarray]: + """(R, gap) del libro TP01/SKH01/XS01 su un pannello arbitrario e con una serie XS01 + arbitraria. E' `PA.book_RG` con il pannello e la serie XS01 come argomenti invece che + cablati: crypto col minimo ESATTO sul path orario condiviso (`PA._crypto`, che per uno + stress taglia il drift sulle GAMBE ORARIE prima di comporre — il minimo giornaliero non e' + separabile per gamba, quindi non si puo' tagliare a valle); il minimo di XS01 si SOMMA + (worst simultaneo, convenzione severa del 25/07). gap = min_intraday - chiusura (<= 0). + Validato bit-exact contro `PA.book_RG` in §1. + + `dm` = (("XS01", 0.5), ...) taglia il DRIFT di quello sleeve lasciando la forma della coda. + ⚠ Il taglio crypto usa la media sull'INTERA storia oraria (2019+), non sulla finestra: e' + la convenzione di PROP-ALLOC, tenuta identica perche' le due tabelle vanno confrontate.""" + dmd = dict(dm) + w_tp, w_sk = float(w.get("TP01", 0.0)), float(w.get("SKH01", 0.0)) + w_xs = 0.0 if drop_xs else 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 = PA._crypto(w_tp, w_sk, dmd.get("TP01", 1.0), dmd.get("SKH01", 1.0)).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) + rv = np.nan_to_num(f["R"].values) + mv = np.nan_to_num(f["m"].values) + mult = dmd.get("XS01", 1.0) + if mult != 1.0: + cutv = (1.0 - mult) * rv.mean() # media PRIMA del taglio (come PA) + rv, mv = rv - cutv, mv - cutv + R += w_xs * rv + M += w_xs * mv + return R, np.minimum(M, R) - R + + +def obj_rg(R: np.ndarray, G: np.ndarray, lev: float, ev_idx, fu_idx, + firm: str = HYRO, factor: float = PA.DELUCK) -> dict: + """`PA.objective` senza la costruzione del libro (che qui e' su un pannello diverso).""" + Rd = PA.deluck(R, factor) + e = PA.eval_sim(Rd, G, ev_idx, lev, firm) + f = PA.funded_sim(Rd, G, fu_idx, lev, firm, 100_000.0) + ann = float(Rd.mean() * 365.0) + vol = float(Rd.std() * np.sqrt(365.0)) + return dict(J=e["p_pass"] * f["p_alive"], p_pass=e["p_pass"], p_alive=f["p_alive"], + e_payout=f["e_payout"], sharpe=(ann / vol if vol > 0 else 0.0), + vol=vol, drift=ann, jvec=(e["passed"] & f["alive"]).astype(float)) + + +def maxdd(R: np.ndarray) -> float: + eq = np.cumprod(1.0 + R) + return float((eq / np.maximum.accumulate(eq) - 1.0).min()) + + +def scan(grid: list[dict], idx, xs, ev_idx, fu_idx, drop_xs: bool = False, + dm: tuple = ()) -> pd.DataFrame: + rows = [] + for w in grid: + R, G = rg(w, idx, xs, drop_xs=drop_xs, dm=dm) + dd = maxdd(PA.deluck(R)) + for lev in LEVS: + o = obj_rg(R, G, lev, ev_idx, fu_idx) + rows.append(dict(w=PA.wkey(w), lev=lev, maxdd=dd, + **{k: v for k, v in o.items() if k != "jvec"})) + bump(len(grid) * len(LEVS)) + return pd.DataFrame(rows) + + +def best_row(df: pd.DataFrame, key: str = "J") -> pd.Series: + return df.loc[df[key].idxmax()] + + +def wdict(key: str) -> dict: + return dict(zip(W3, [int(x) / 100 for x in key.split("/")])) + + +# =============================================================== 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(" PROP-RECAL — J regge se XS01 viene dalla STORIA LUNGA invece che dalla sua finestra" + " di scoperta?") + print("=" * 104) + print(" PREVISIONE REGISTRATA PRIMA: argmax con w_XS in [0.25, 0.38], J in [0.50, 0.70],") + print(" P(>=50 EUR/g) in [10%, 35%]. Il verso atteso: drift XS piu' alto (spinge su) contro") + print(" maxDD raddoppiato + barriera STATICA + 2022 dentro (spingono giu'; su un conto a") + print(" barriera la seconda forza pesa di piu').") + + # ================================================== §0. RIPRODUZIONE + hr("§0. RIPRODUZIONE — la macchina deve dare i numeri PUBBLICATI prima di darne di nuovi") + idx_pub = PA._panel_index(PA.START_WIN) + ev_pub = PA.boot_idx(len(idx_pub), EV_H, N_PATHS, seed=SEED_EV) + fu_pub = PA.boot_idx(len(idx_pub), FU_H, N_PATHS, seed=SEED_FU) + print(f" pannello 5-sleeve di PROP-ALLOC: {len(idx_pub)} giorni " + f"{idx_pub[0].date()} -> {idx_pub[-1].date()} | de-luck x{PA.DELUCK} | " + f"{N_PATHS} percorsi | semi {SEED_EV}/{SEED_FU}") + grid = PA.simplex_grid(W3, STEP) + t = time.time() + Gpub = PA.grid_scan(grid, LEVS, HYRO, ev_pub, fu_pub) + bump(len(grid) * len(LEVS)) + print(f" griglia {len(grid)} pesi x {len(LEVS)} leve = {len(grid)*len(LEVS)} celle " + f"({time.time()-t:.0f}s)") + b_pub = best_row(Gpub) + ws = Gpub.loc[Gpub["sharpe"].idxmax(), "w"] + bs_pub = best_row(Gpub[Gpub["w"] == ws]) + bl_pub = best_row(Gpub[Gpub["w"] == "75/25/0"]) + print(f"\n {'':>18} {'pesi':>10} {'leva':>6} {'J':>7} {'PUB':>7} {'d':>7} | " + f"{'P(pass)':>8} {'PUB':>7} | {'P(vivo)':>8} {'PUB':>7} | {'Sharpe':>7} {'PUB':>6}") + ok = True + for lab, row, p in (("argmax J", b_pub, PUB["argmaxJ"]), ("argmax Sharpe", bs_pub, PUB["argmaxS"]), + ("libro LIVE 75/25", bl_pub, PUB["live"])): + same_w = (row["w"] == p["w"]) and abs(row["lev"] - p["lev"]) < 1e-9 + dj = row["J"] - p["J"] + ok &= same_w and abs(dj) < 0.02 + print(f" {lab:>18} {row['w']:>10} {row['lev']:>6.3f} {row['J']:>7.3f} {p['J']:>7.3f} " + f"{dj:>+7.3f} | {row['p_pass']:>8.1%} {p['p_pass']:>7.1%} | " + f"{row['p_alive']:>8.1%} {p['p_alive']:>7.1%} | {row['sharpe']:>7.2f} {p['sharpe']:>6.2f}" + f"{'' if same_w else ' <-- CELLA DIVERSA'}") + dA, seA = PA.paired_delta(wdict(b_pub["w"]), float(b_pub["lev"]), + wdict(bs_pub["w"]), float(bs_pub["lev"]), HYRO, ev_pub, fu_pub) + dB, seB = PA.paired_delta(wdict(b_pub["w"]), float(b_pub["lev"]), + {"TP01": 0.75, "SKH01": 0.25}, float(bl_pub["lev"]), + HYRO, ev_pub, fu_pub) + bump(4) + print(f"\n differenze APPAIATE argmaxJ - argmaxSharpe {dA:+.4f} +-{seA:.4f} " + f"(PUB {PUB['dJ_vs_sharpe']:+.4f})") + print(f" argmaxJ - LIVE {dB:+.4f} +-{seB:.4f} " + f"(PUB {PUB['dJ_vs_live']:+.4f})") + ok &= abs(dA - PUB["dJ_vs_sharpe"]) < 0.02 and abs(dB - PUB["dJ_vs_live"]) < 0.03 + pc.DELUCK = PA.DELUCK + r_lad = pc.simulate([tuple(wdict(b_pub["w"]).get(n, 0.0) for n in W3)], + [0] * pl.MAX_CONCURRENT, n_paths=1500, lev=0.75, seed=20260822, + lens="coupled", start=PA.START_WIN) + print(f" scala di conti (I-bis) EUR/g med {r_lad['med']:.2f} (PUB {PUB['ladder_med']:.2f}) | " + f"P(>=50/g) {r_lad['p_target']:.1%} (PUB {PUB['ladder_p50']:.1%}) | " + f"P(zero) {r_lad['p_zero']:.1%} (PUB {PUB['ladder_zero']:.1%})") + ok &= abs(r_lad["p_target"] - PUB["ladder_p50"]) < 0.03 + verdetto = "OK — si procede" if ok else "FALLITA — ogni numero sotto va letto con questa riserva" + print(f"\n >>> RIPRODUZIONE: {verdetto}") + tick("§0 riproduzione") + + # ================================================== §1. LA SERIE LUNGA, ACCOPPIATA + hr("§1. LA SERIE LUNGA DI XS01, CON IL WICK — e la validazione del costruttore di tuple") + Chl, Ohl, Hhl, Lhl = ohlc_panels(hl_loader, [s for s in XS_UNIVERSE + if (_HL_DIR / f"hl_{s.lower()}_1d.parquet").exists()]) + Chl = Chl.dropna() + Ohl, Hhl, Lhl = Ohl.reindex(Chl.index), Hhl.reindex(Chl.index), Lhl.reindex(Chl.index) + mine = xs_tuples(Chl, Ohl, Hhl, Lhl, phase=0) + ref = pc.xsec_daily_tuples("shared") + j = mine.join(ref, lsuffix="_m", rsuffix="_r", how="inner").dropna() + dR = float(np.abs(j["R_m"] - j["R_r"]).max()) + dM = float(np.abs(j["m_m"] - j["m_r"]).max()) + print(f" CONTROLLO: il mio costruttore di tuple, alimentato con gli OHLC HL, contro") + print(f" `pc.xsec_daily_tuples()` (la macchineria del 25/07): n={len(j)} " + f"max|dR| = {dR:.2e} max|dm| = {dM:.2e} -> " + f"{'IDENTICO' if max(dR, dM) < 1e-12 else 'DIVERGE — STOP'}") + if max(dR, dM) >= 1e-12: + print(" !! il costruttore non replica: i numeri sulla storia lunga sarebbero di un'altra") + print(" macchina. Mi fermo qui.") + return + + for wtest in ({"TP01": 0.25, "SKH01": 0.25, "XS01": 0.50}, {"TP01": 0.75, "SKH01": 0.25}): + Ra, Ga = rg(wtest, idx_pub, ref) + Rb_, Gb_ = PA.book_RG(wtest) + dd1 = float(np.abs(Ra - Rb_).max()); dd2 = float(np.abs(Ga - Gb_).max()) + print(f" CONTROLLO: il mio costruttore di LIBRO contro `PA.book_RG` " + f"({PA.wkey(wtest)}): max|dR| {dd1:.2e} max|dgap| {dd2:.2e} -> " + f"{'IDENTICO' if max(dd1, dd2) < 1e-12 else 'DIVERGE'}") + + Cb, Ob, Hb, Lb = ohlc_panels(bin_loader, XS_UNIVERSE) + p13 = Cb[X.U13].dropna() + o13, h13, l13 = Ob.reindex(p13.index)[X.U13], Hb.reindex(p13.index)[X.U13], Lb.reindex(p13.index)[X.U13] + xs13 = xs_tuples(p13, o13, h13, l13, phase=0) + xsEXP = xs_tuples(Cb, Ob, Hb, Lb, phase=0) + bump(2) + first13 = xs13.index[np.flatnonzero(xs13["R"].values != 0.0)[0]] + firstEX = xsEXP.index[np.flatnonzero(xsEXP["R"].values != 0.0)[0]] + print(f"\n XS01 U13 (13 gambe fisse, Binance) prima barra ATTIVA {first13.date()} " + f"-> {xs13.index[-1].date()}") + print(f" XS01 U19-EXP (10 -> 19 gambe, Binance) prima barra ATTIVA {firstEX.date()} " + f"-> {xsEXP.index[-1].date()}") + print(" (barra del giorno in corso SCARTATA: una giornaliera parziale in coda e' il difetto") + print(" trovato oggi nei monitor forward)") + + # finestre dichiarate + cry_idx = pc.crypto_daily_tuples(1.0, 0.0).index + base_idx = cry_idx.intersection(xs13.index) + S_LONG = max(first13, pd.Timestamp("2021-01-01", tz="UTC")) + S_DISC = pd.Timestamp(PA.START_WIN, tz="UTC") + I_LONG = base_idx[base_idx >= S_LONG] + I_OOS = I_LONG[I_LONG < S_DISC] + I_DISC = base_idx[base_idx >= S_DISC] + WINDOWS = (("W-LONG 2021+ (OOS + scoperta)", I_LONG), + ("W-OOS 2021-2023 (puro OOS)", I_OOS), + ("W-DISC 2024+ (scoperta)", I_DISC)) + print(f"\n {'finestra':>32} {'giorni':>7} {'da':>12} {'a':>12}") + for lab, ii in WINDOWS: + print(f" {lab:>32} {len(ii):>7d} {str(ii[0].date()):>12} {str(ii[-1].date()):>12}") + tick("§1 serie lunga") + + # ================================================== §2. PROFILO SOTTO BARRIERA + hr("§2. IL PROFILO CHE CAMBIA — cio' che una barriera STATICA vede: drift, vol, coda, gap") + print(" (de-luckato x0.89; `gap peggiore` = l'escursione intraday che la close-only NON vede)") + print(f" {'sleeve / finestra':>34} {'Sharpe':>7} {'vol a':>7} {'drift a':>8} {'maxDD':>7} " + f"{'g.peggiore':>11} {'gap p90':>9} {'gap peggiore':>13}") + prof_rows = [ + ("TP01 W-LONG", {"TP01": 1.0}, I_LONG, xs13), + ("TP01 W-DISC", {"TP01": 1.0}, I_DISC, xs13), + ("SKH01 W-LONG", {"SKH01": 1.0}, I_LONG, xs13), + ("SKH01 W-DISC", {"SKH01": 1.0}, I_DISC, xs13), + ("XS01 U13 Binance W-OOS", {"XS01": 1.0}, I_OOS, xs13), + ("XS01 U13 Binance W-DISC", {"XS01": 1.0}, I_DISC, xs13), + ("XS01 U19EXP Binance W-OOS", {"XS01": 1.0}, I_OOS, xsEXP), + ("XS01 UFFICIALE HL W-DISC", {"XS01": 1.0}, I_DISC, ref), + ] + for lab, w, ii, xf in prof_rows: + R, G = rg(w, ii, xf) + Rd = PA.deluck(R) + vol = Rd.std() * np.sqrt(365.0) + print(f" {lab:>34} {Rd.mean()*365/vol if vol > 0 else 0:>7.2f} {vol:>6.1%} " + f"{Rd.mean()*365:>8.1%} {maxdd(Rd)*100:>6.1f}% {Rd.min():>11.2%} " + f"{-np.percentile(-G, 90)*100:>8.2f}pp {G.min()*100:>12.2f}pp") + print("\n Le righe da leggere insieme sono XS01 W-OOS e XS01 UFFICIALE: il RENDIMENTO regge") + print(" (anzi sale), ma il maxDD e la coda peggiorano — e la barriera guarda la coda.") + tick("§2 profilo") + + # ================================================== §3. LA GRIGLIA RIFATTA + hr("§3. LA GRIGLIA RIFATTA SULLA STORIA LUNGA — stesso simplesso, stesse leve, stessa lente") + res = {} + for lab, ii in WINDOWS: + ev = PA.boot_idx(len(ii), EV_H, N_PATHS, seed=SEED_EV) + fu = PA.boot_idx(len(ii), FU_H, N_PATHS, seed=SEED_FU) + t = time.time() + Gx = scan(grid, ii, xs13, ev, fu) + res[lab] = dict(G=Gx, ev=ev, fu=fu, idx=ii) + b, bl = best_row(Gx), best_row(Gx[Gx["w"] == "75/25/0"]) + print(f"\n {lab} ({time.time()-t:.0f}s)") + print(f" {'':>18} {'pesi':>10} {'leva':>6} {'J':>7} {'P(pass)':>8} {'P(vivo)':>8} " + f"{'Sharpe':>7} {'maxDD':>7} {'E[pay/a]':>10}") + for l2, r2 in (("argmax J", b), ("libro LIVE 75/25", bl)): + print(f" {l2:>18} {r2['w']:>10} {r2['lev']:>6.3f} {r2['J']:>7.3f} " + f"{r2['p_pass']:>8.1%} {r2['p_alive']:>8.1%} {r2['sharpe']:>7.2f} " + f"{r2['maxdd']*100:>6.1f}% {r2['e_payout']:>9,.0f}$") + d, se = None, None + try: + Rb, Gb = rg(wdict(b["w"]), ii, xs13) + Rl, Gl = rg({"TP01": 0.75, "SKH01": 0.25}, ii, xs13) + a1 = obj_rg(Rb, Gb, float(b["lev"]), ev, fu)["jvec"] + b1 = obj_rg(Rl, Gl, float(bl["lev"]), ev, fu)["jvec"] + dd_ = a1 - b1 + d, se = float(dd_.mean()), float(dd_.std(ddof=1) / np.sqrt(len(dd_))) + bump(2) + except Exception as exc: # pragma: no cover + print(f" delta appaiato non calcolato: {exc}") + if d is not None: + print(f" delta APPAIATO argmax - LIVE : {d:+.4f} +-{se:.4f}" + f" (PROP-ALLOC pubblicava {PUB['dJ_vs_live']:+.4f} su W-DISC con XS01 HL)") + tick("§3 griglie") + + # ================================================== §4. LA REGIONE, NON IL PUNTO + hr("§4. IL PESO DI XS01 — c'e' una REGIONE robusta su ENTRAMBE le finestre?") + print(" PROP-ALLOC ha gia' misurato che il PUNTO non conta (il massimo di 200 pesi casuali") + print(" eguagliava l'argmax): la domanda giusta e' se esiste una regione comune. Per ogni") + print(" livello di w_XS si riporta il MEGLIO su TP/SKH e su tutte le leve.") + xs_levels = sorted({int(round(100 * (i / STEP))) for i in range(STEP + 1)}) + print(f"\n {'w_XS':>6} | " + " | ".join(f"{lab.split()[0]:>26}" for lab, _ in WINDOWS)) + print(f" {'':>6} | " + " | ".join(f"{'J':>7} {'maxDD':>7} {'pesi':>10}" for _ in WINDOWS)) + prof_by_w = {lab: {} for lab, _ in WINDOWS} + for wx in xs_levels: + cells = [] + for lab, _ in WINDOWS: + Gx = res[lab]["G"] + sub = Gx[Gx["w"].str.endswith(f"/{wx}")] + if len(sub) == 0: + cells.append(f"{'—':>7} {'—':>7} {'—':>10}") + continue + r2 = best_row(sub) + prof_by_w[lab][wx] = float(r2["J"]) + cells.append(f"{r2['J']:>7.3f} {r2['maxdd']*100:>6.1f}% {r2['w']:>10}") + print(f" {wx:>5}% | " + " | ".join(cells)) + print("\n Regione robusta = livelli di w_XS entro il 3% del meglio di finestra, su TUTTE e tre:") + robust = [] + for wx in xs_levels: + okall = True + for lab, _ in WINDOWS: + d = prof_by_w[lab] + if wx not in d or d[wx] < 0.97 * max(d.values()): + okall = False + if okall: + robust.append(wx) + print(f" {robust if robust else 'NESSUN livello comune'}" + f" (entro il 5%: " + f"{[wx for wx in xs_levels if all(wx in prof_by_w[l] and prof_by_w[l][wx] >= 0.95*max(prof_by_w[l].values()) for l, _ in WINDOWS)]})") + tick("§4 regione") + + # ================================================== §5. LA BANDA ANCORATA AL MISURATO + hr("§5. LA BANDA — con gli stress ANCORATI a cio' che e' misurato, non a cio' che era assunto") + print(" PROP-ALLOC pubblicava 42% / 7,7% / 0,7% dove il caso severo era 'XS x0,00' = il drift") + print(" di XS01 assunto NULLO perche' non misurabile fuori campione. Quel drift ORA E'") + print(" MISURATO, e fuori campione e' PIU' ALTO: lo scenario cambia stato, da assunzione a") + print(" misura. Cio' che resta assunto e' (a) la fortuna d'ancora di SKH01 (misurata altrove,") + print(" non sotto questa lente) e (b) il LISTINO della firm (§6). Gli stress qui sotto sono") + print(" quelli, non un pessimismo arbitrario.") + st_rows = [] + STRESS = ((), (("SKH01", 0.50),), (("SKH01", 0.33),)) + SLAB = {(): "nessuno (drift pieno)", + (("SKH01", 0.50),): "SKH x0.50 (ancora 26/07)", + (("SKH01", 0.33),): "SKH x0.33 (ancora severa)"} + for lab, ii in (WINDOWS[0], WINDOWS[1]): + Gw, ev, fu = res[lab]["G"], res[lab]["ev"], res[lab]["fu"] + top_keys = list(Gw.sort_values("J", ascending=False).drop_duplicates("w")["w"].head(10)) + print(f"\n {lab} (argmax ri-cercato sulle {len(top_keys)} celle-peso migliori: e' un") + print(f" risparmio di calcolo, ed e' CONSERVATIVO per la stabilita' dell'ottimo)") + print(f" {'stress':>28} {'argmax pesi':>12} {'leva':>6} {'J':>7} {'J del LIVE':>11} " + f"{'delta appaiato':>16}") + for dmt in STRESS: + best_j, best_k, best_l, best_v = -1.0, None, None, None + for k in top_keys: + R, G = rg(wdict(k), ii, xs13, dm=dmt) + for lv in LEVS: + o = obj_rg(R, G, lv, ev, fu) + if o["J"] > best_j: + best_j, best_k, best_l, best_v = o["J"], k, lv, o["jvec"] + bump(len(top_keys) * len(LEVS)) + RL, GL = rg({"TP01": 0.75, "SKH01": 0.25}, ii, xs13, dm=dmt) + oL, lvL = None, None + for lv in LEVS: + o = obj_rg(RL, GL, lv, ev, fu) + if oL is None or o["J"] > oL["J"]: + oL, lvL = o, lv + bump(len(LEVS)) + d_ = best_v - oL["jvec"] + # guardia contro l'errore del 22/08: un delta appaiato DEVE ereditare lo stress + # della riga in cui compare, o confronta due configurazioni non stressate. + assert abs(float(d_.mean()) - (best_j - oL["J"])) < 0.05, "delta appaiato incoerente" + print(f" {SLAB[dmt]:>28} {best_k:>12} {best_l:>6.3f} {best_j:>7.3f} " + f"{oL['J']:>11.3f} {d_.mean():>+11.4f} " + f"+-{d_.std(ddof=1)/np.sqrt(len(d_)):.4f}") + st_rows.append((lab, SLAB[dmt], best_k, best_l, best_j, oL["J"])) + tick("§5 banda") + + # ================================================== §6. XS01 ASSENTE + hr("§6. LO SCENARIO CHE UCCIDE TUTTO — la firm NON lista gli alt (assunto mai verificato)") + print(" Non e' uno stress sul drift: e' la RIMOZIONE dello sleeve. Il libro torna a due gambe") + print(" e il conto funded diventa il libro live a leva. E' l'unico scenario che non si misura") + print(" con un backtest — si verifica leggendo un listino — quindi qui se ne misura il DANNO.") + print(f"\n {'finestra':>28} {'con XS01: pesi':>15} {'lev':>5} {'J':>7} | " + f"{'SENZA XS01: pesi':>17} {'lev':>5} {'J':>7} | {'J perso':>8} {'quota':>7}") + absent = {} + for lab, ii in WINDOWS: + Gx, ev, fu = res[lab]["G"], res[lab]["ev"], res[lab]["fu"] + b = best_row(Gx) + sub2 = Gx[Gx["w"].str.endswith("/0")] + b2 = best_row(sub2) + absent[lab] = b2 + lost = float(b["J"]) - float(b2["J"]) + print(f" {lab:>28} {b['w']:>15} {b['lev']:>5.2f} {b['J']:>7.3f} | " + f"{b2['w']:>17} {b2['lev']:>5.2f} {b2['J']:>7.3f} | {lost:>+8.3f} " + f"{lost/max(b['J'],1e-9):>6.0%}") + print("\n NB: 'SENZA XS01' e' gia' ri-ottimizzato su TP/SKH e su tutte le leve — e' il MEGLIO") + print(" che il libro a due gambe puo' fare, non il 75/25 cablato.") + tick("§6 assenza") + + # ================================================== §7. LA SCALA DI CONTI + hr("§7. LA SCALA DI CONTI — P(>=50 EUR/g) da EUR 600 in 36 mesi, lente ACCOPPIATA") + print(f" macchineria del 25/07 (`pc.simulate`), de-luck x{PA.DELUCK}, {pl.MAX_CONCURRENT} conti" + f" max, morte-firm {pl.FIRM_DEATH_PER_YEAR:.0%}/anno, 1500 percorsi") + print(" (le tuple del libro vengono INIETTATE nella cache di `pc.book_tuples`, cosi' la") + print(" simulazione e' letteralmente la stessa, con un input diverso)") + print(f"\n {'finestra':>28} {'allocazione':>26} {'leva':>6} {'EUR/g med':>10} " + f"{'P(>=10/g)':>10} {'P(>=50/g)':>10} {'P(zero)':>8}") + + def ladder(ii, w: dict, lev: float, tag: str, wlab: str, drop_xs: bool = False) -> dict: + R, G = rg(w, ii, xs13, drop_xs=drop_xs) + cfg = (float(w.get("TP01", 0.0)), float(w.get("SKH01", 0.0)), + 0.0 if drop_xs else float(w.get("XS01", 0.0))) + key = (cfg[0], cfg[1], cfg[2], "shared") + saved = pc._TUP_CACHE.pop(key, None) + pc._TUP_CACHE[key] = (R, G, ii) + r = pc.simulate([cfg], [0] * pl.MAX_CONCURRENT, n_paths=1500, lev=lev, + seed=20260822, lens="coupled", start=str(ii[0].date())) + pc._TUP_CACHE.pop(key, None) + if saved is not None: + pc._TUP_CACHE[key] = saved + print(f" {tag:>28} {wlab:>26} {lev:>6.2f} {r['med']:>10.2f} {r['p_ge10']:>10.1%} " + f"{r['p_target']:>10.1%} {r['p_zero']:>8.1%}") + return r + + lad = {} + for lab, ii in WINDOWS: + Gx = res[lab]["G"] + b = best_row(Gx) + b2 = absent[lab] + short = lab.split()[0] + lad[(short, "argmax")] = ladder(ii, wdict(b["w"]), float(b["lev"]), short, + f"argmax {b['w']}") + lad[(short, "2550")] = ladder(ii, wdict("25/25/50"), 0.75, short, + "25/25/50 (PROP-ALLOC)") + lad[(short, "noxs")] = ladder(ii, wdict(b2["w"]), float(b2["lev"]), short, + f"SENZA XS01 {b2['w']}", drop_xs=True) + lad[(short, "live")] = ladder(ii, {"TP01": 0.75, "SKH01": 0.25}, 0.75, short, + "LIVE 75/25 @0.75x") + bump(12) + tick("§7 scala di conti") + + # ================================================== §8. SINTESI + GATE + hr("§8. SINTESI — la banda da citare, e il gate da RI-REGISTRARE") + bL = best_row(res[WINDOWS[0][0]]["G"]) + bO = best_row(res[WINDOWS[1][0]]["G"]) + bD = best_row(res[WINDOWS[2][0]]["G"]) + print(f" J e argmax per finestra (XS01 = U13 Binance in tutte e tre, quindi la SOLA cosa che") + print(f" cambia fra le righe e' la FINESTRA):") + print(f" W-DISC 2024+ argmax {bD['w']:>10} @{bD['lev']:.2f}x J {bD['J']:.3f}") + print(f" W-OOS 2021-2023 argmax {bO['w']:>10} @{bO['lev']:.2f}x J {bO['J']:.3f}") + print(f" W-LONG 2021+ argmax {bL['w']:>10} @{bL['lev']:.2f}x J {bL['J']:.3f}") + print(f" (PROP-ALLOC pubblicato, XS01 HL 19 gambe, W-DISC: 25/25/50 @0.75x J 0.738)") + print(f"\n P(>=50 EUR/g) in 36 mesi, banda onesta:") + for short in ("W-LONG", "W-OOS", "W-DISC"): + row = " | ".join(f"{k}: {lad[(short, k)]['p_target']:.1%}" + for k in ("argmax", "2550", "noxs", "live") if (short, k) in lad) + print(f" {short:>8} {row}") + print(f"\n TRIAL/CELLE DICHIARATI (al rialzo): {TRIALS}") + print(f"\n fatto in {time.time()-t0:.0f}s") + print("=" * 104) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822b_surface_rv.py b/scripts/research/r0822b_surface_rv.py index 65a8206..2d56bc6 100644 --- a/scripts/research/r0822b_surface_rv.py +++ b/scripts/research/r0822b_surface_rv.py @@ -635,9 +635,9 @@ def main() -> None: print(f" ESEGUIBILE (compra ali all'ask, vendi corpo al bid): " f"{bf['viol_exec'].sum():,} = {bf['viol_exec'].mean():.4%}") if dte_min == DTE_MIN: - bf_keep = bf + bf_keep, v_keep = bf, v - bf = bf_keep + bf, vert = bf_keep, v_keep print("\n" + "=" * 100) print("3. QUANTO E' GRANDE L'INCOERENZA, E QUANTO COSTA ATTRAVERSARLA") print("=" * 100) @@ -651,12 +651,17 @@ def main() -> None: f"p90 {r.quantile(.9):.3f} · p99 {r.quantile(.99):.3f}") print(f" mezzo spread denaro-lettera mediana {hs.median():.3f} pt-vol · " f"p25 {hs.quantile(.25):.3f} · p75 {hs.quantile(.75):.3f}") + sub = x.dropna(subset=["iv_resid"]) print(f" UN TICK della griglia di prezzo mediana {tk.median():.3f} pt-vol " - f"-> l'incoerenza e' sotto il tick nel {np.mean(r.reindex(x.index) < tk):.1%} dei casi") + f"-> l'incoerenza e' sotto il tick nel " + f"{np.mean(sub['iv_resid'].abs() < sub['tick_pt']):.1%} dei casi (n={len(sub):,})") both = x.dropna(subset=["iv_resid"]) rap = np.abs(both["iv_resid"]) / (2 * both["halfspread_pt"]) print(f" |residuo| / (spread PIENO delle gambe): mediana {rap.median():.3f} · " f"p99 {rap.quantile(.99):.3f} · quota > 1 = {np.mean(rap > 1):.3%}") + print(f" RELATIVE VALUE (non arbitraggio): per guadagnare serve |residuo| > 1x lo spread") + print(f" anche con reversione TOTALE e uscita gratis ({np.mean(rap > 1):.3%} dei casi), " + f"> 2x se si paga anche l'uscita ({np.mean(rap > 2):.3%})") print(f" spread relativo (ask-bid)/mid del venue: mediana {x['relspread'].median():.1%} " f"· p25 {x['relspread'].quantile(.25):.1%}") # valore in dollari dell'incoerenza al MID (il caso migliore possibile: fee 0, spread 0) @@ -695,17 +700,17 @@ def main() -> None: print("\n" + "=" * 100) print("4. TEST C — varianza totale nel tenore (l'unico che tocca un interpolatore)") print("=" * 100) - c = test_calendario(q, DTE_MIN) - print(f" (i) strike CONDIVISO, nessuna interpolazione: {c['i_viol']:,}/{c['i_n']:,} = {c['i_quota']:.3%}" - f" · ampiezza mediana {c['i_amp_med_pt']:.3f} pt-vol (p95 {c['i_amp_p95_pt']:.3f})") - print(f" (ii) log-moneyness FISSO, con interpolazione (1 snapshot ogni {c['ii_ogni']}): " - f"{c['ii_viol']:,}/{c['ii_n']:,} = {c['ii_quota']:.3%} · ampiezza mediana {c['ii_amp_med_pt']:.3f} pt-vol") - print(f" -> contributo dell'INTERPOLATORE = {c['ii_quota'] - c['i_quota']:+.3%} di quota di violazione") - if "i_val_usd_med" in c: - print(f" valore mediano della violazione (i) al lotto minimo ${c['i_val_usd_med']:.2f} " - f"contro un costo di 2 gambe (spread + fee) di ${c['i_costo_usd_med']:.2f}") - print(f" margine di monotonia sull'intera superficie: la iv lunga sta {-c['room_med_pt']:.2f} pt-vol " - f"SOPRA il minimo che la non-arbitraggio richiede (5% piu' stretto: {-c['room_p5_pt']:.2f}) " + cal = test_calendario(q, DTE_MIN) + print(f" (i) strike CONDIVISO, nessuna interpolazione: {cal['i_viol']:,}/{cal['i_n']:,} = {cal['i_quota']:.3%}" + f" · ampiezza mediana {cal['i_amp_med_pt']:.3f} pt-vol (p95 {cal['i_amp_p95_pt']:.3f})") + print(f" (ii) log-moneyness FISSO, con interpolazione (1 snapshot ogni {cal['ii_ogni']}): " + f"{cal['ii_viol']:,}/{cal['ii_n']:,} = {cal['ii_quota']:.3%} · ampiezza mediana {cal['ii_amp_med_pt']:.3f} pt-vol") + print(f" -> contributo dell'INTERPOLATORE = {cal['ii_quota'] - cal['i_quota']:+.3%} di quota di violazione") + if "i_val_usd_med" in cal: + print(f" valore mediano della violazione (i) al lotto minimo ${cal['i_val_usd_med']:.2f} " + f"contro un costo di 2 gambe (spread + fee) di ${cal['i_costo_usd_med']:.2f}") + print(f" margine di monotonia sull'intera superficie: la iv lunga sta {-cal['room_med_pt']:.2f} pt-vol " + f"SOPRA il minimo che la non-arbitraggio richiede (5% piu' stretto: {-cal['room_p5_pt']:.2f}) " f"-> la superficie non e' nemmeno VICINA al bordo") print("\n" + "=" * 100) @@ -794,11 +799,15 @@ def main() -> None: print("SINTESI") print("=" * 100) nv = int(bf["viol_exec"].sum()); nt = len(bf) - print(f" 1. La superficie e' ARBITRAGE-FREE sui prezzi eseguibili: monotonia 0/{1_111_585:,},") + print(f" 1. La superficie e' ARBITRAGE-FREE sui prezzi eseguibili: monotonia " + f"{vert['n_viol']}/{vert['n_confronti']:,},") print(f" convessita' {nv}/{nt:,} = {nv/nt:.6%} (e quell'una vale $0,00 e sparisce l'ora dopo),") - print(f" calendario 6/107.177 con margine mediano di 13 punti di vol dal bordo.") + print(f" calendario {cal['i_viol']}/{cal['i_n']:,} con margine mediano di {-cal['room_med_pt']:.0f} " + f"punti di vol dal bordo.") print(f" 2. L'incoerenza al MID esiste ed e' grande ({bf['viol_mid'].mean():.1%} delle triple)") - print(f" ma vale {np.abs(bf['iv_resid']).median():.3f} punti di vol contro un mezzo spread di 0,86-1,21,") + hs_b = bf[bf.asset == "BTC"]["halfspread_pt"]; hs_e = bf[bf.asset == "ETH"]["halfspread_pt"] + print(f" ma vale {np.abs(bf['iv_resid']).median():.3f} punti di vol contro un mezzo spread di " + f"{hs_b[np.isfinite(hs_b)].median():.2f}-{hs_e[np.isfinite(hs_e)].median():.2f},") print(f" ed e' SOTTO IL TICK del venue nel 95-97% dei casi: non e' esprimibile nella griglia.") print(f" 3. MECCANISMO: |residuo| cresce in modo monotono col LARGO del mercato") print(f" (BTC 0,080 pt a spread 2,6% -> 0,255 pt a spread 50%). L'incoerenza al mid E' la") diff --git a/scripts/research/r0822b_tp01_sinistro.py b/scripts/research/r0822b_tp01_sinistro.py index 2c158f4..ff0002d 100644 --- a/scripts/research/r0822b_tp01_sinistro.py +++ b/scripts/research/r0822b_tp01_sinistro.py @@ -495,6 +495,10 @@ def main() -> None: # noqa: C901 a_sin = max(tab, key=lambda w: tab[w]["sin"][1]["J"]) print(f"\n argmax: 2019+ -> TP01 {a_full:.3f} | 2024+ -> TP01 {a_rec:.3f} | " f"SOLO-SINISTRO -> TP01 {a_sin:.3f}") + print(f" ⚠️ RISOLUZIONE: un LIVELLO di J ha errore MC ~{np.sqrt(0.25/N_PATHS):.3f} " + f"(N={N_PATHS}) -> celle entro ~0.02 NON sono distinguibili, e infatti 0.375 e 0.500") + print(" si scambiano il posto fra semi diversi. La lettura e' 'la regione 0.25-0.50',") + print(" non 'il punto 0.375' (stessa lezione del null dei tilt casuali di PROP-ALLOC).") print(" ⚠️ La colonna SOLO-SINISTRO e' una lente ESTREMA e va letta come tale: l'anno intero") print(" e' fatto di soli blocchi di crash (4 episodi ricampionati). Non e' uno scenario") print(" plausibile, e' il **limite superiore del danno**: se il verso della risposta e' lo") @@ -748,8 +752,9 @@ def main() -> None: # noqa: C901 (0.125, 0.375, 0.50), (0.25, 0.25, 0.50), (0.375, 0.125, 0.50)] com = xs.index.intersection(IDX[i_rec:]) xs_al = xs.reindex(com) + LEV_COM = 0.625 # leva comune per il blocco a XS01=50% (la minima fra le tre righe) print(f"\n {'TP/SKH/XS':>16} {'ipotesi XS':>12} {'leva':>6} {'J normale':>10} " - f"{'P(viv|replay peggiore)':>24} {'shock di rottura':>18}") + f"{'P(viv|replay pegg.)':>21} {'idem @0.625x':>14} {'shock di rottura':>18}") for cfg in W3: wtp, wsk, wxs = cfg base = wtp + wsk @@ -767,7 +772,7 @@ def main() -> None: # noqa: C901 bfu = PA.boot_idx(nn, HORIZON, N_PATHS, seed=8302) lab = f"{int(round(wtp*100))}/{int(round(wsk*100))}/{int(round(wxs*100))}" - def build(hyp: str, shock: float = 0.0): + def build(hyp: str, shock: float = 0.0, lev_: float | None = None): segsR, segsG = [], [] for t in epis: rc = base * (crypto(wtp_n)[0][t:t + EPI_LEN] - offc) @@ -790,16 +795,16 @@ def main() -> None: # noqa: C901 worst = 1.0 for k, g2 in enumerate(sg): fu = splice_paths(bfu, np.arange(g2[0], g2[1]), seed=8320 + k) - worst = min(worst, J_of(Rp, Gp, evn, fu, lv)["p_alive"]) + worst = min(worst, J_of(Rp, Gp, evn, fu, lv if lev_ is None else lev_)["p_alive"]) return worst if wxs == 0: print(f" {lab:>16} {'n/a (no XS)':>12} {lv:>6.3f} {on['J']:>10.3f} " - f"{build('A-flat'):>24.1%} {'-':>18}") + f"{build('A-flat'):>21.1%} {build('A-flat', lev_=LEV_COM):>14.1%} {'-':>18}") continue for hyp in ("A-flat", "B-peggiore"): print(f" {lab:>16} {hyp:>12} {lv:>6.3f} {on['J']:>10.3f} " - f"{build(hyp):>24.1%} {'-':>18}") + f"{build(hyp):>21.1%} {build(hyp, lev_=LEV_COM):>14.1%} {'-':>18}") lo, hi = 0.0, 0.40 # shock di rottura: bisezione su P(vivo) = 50% for _ in range(9): mid = 0.5 * (lo + hi) @@ -807,11 +812,22 @@ def main() -> None: # noqa: C901 lo = mid else: hi = mid - print(f" {lab:>16} {'C-shock':>12} {lv:>6.3f} {on['J']:>10.3f} {'(50% per def.)':>24} " - f"{-lo:>17.1%}") + lo2, hi2 = 0.0, 0.40 + for _ in range(9): + mid = 0.5 * (lo2 + hi2) + if build("C-shock", mid, lev_=LEV_COM) >= 0.50: + lo2 = mid + else: + hi2 = mid + print(f" {lab:>16} {'C-shock':>12} {lv:>6.3f} {on['J']:>10.3f} {'(50% per def.)':>21} " + f"{'(50% per def.)':>14} {-lo:>17.1%} / {-lo2:.1%} @0.625x") print("\n Lettura: 'shock di rottura' = il crollo di UN giorno su XS01 che porta P(vivo) al") print(" 50% quando il crash arriva. Piu' e' piccolo, piu' l'allocazione XS01-pesante e' a un") print(" giorno di coda dalla morte. Non e' una previsione: e' la taglia della scommessa.") + print(" ⚠️ Le ultime tre righe hanno XS01 FISSO al 50% e differiscono SOLO per come si") + print(" divide il resto fra TP01 e SKH01: e' li' che si legge se TP01 fa da zavorra contro") + print(" l'ignoto di XS01. **La colonna @0.625x e' quella da leggere**, perche' a leva libera") + print(" la riga con meno TP01 sceglie una leva piu' bassa e vince per quello.") tick("§6 XS01") # ============================================================ §7. BANDA + VERDETTO @@ -917,9 +933,14 @@ def main() -> None: # noqa: C901 print(" de-levering**, in una veste nuova: 'protezione dal crash' invece di 'meno DD'.") print(" 4) Il rischio vero dell'ottimo di PROP-ALLOC NON e' TP01 sotto-pesato: e' XS01 al") print(" 50% con un comportamento nel crash che il campione non contiene. Un solo giorno") - print(" a -8% su XS01 dimezza la sopravvivenza; con TP01 al 50% ne serve uno a -12.3%.") - print(" **Se TP01 vale qualcosa su un conto funded, vale come zavorra contro l'ignoto di") - print(" XS01, non come assicurazione contro il crash di BTC/ETH — che SKH01 fa meglio.**") + print(" a ~-10% su XS01 dimezza la sopravvivenza quando il crash arriva.") + print(" ⚠️ IPOTESI MIA, NATA E REFUTATA IN SESSIONE: avevo scritto che TP01 fa da") + print(" ZAVORRA contro quell'ignoto (a XS01 25% serviva uno shock -12.3% contro -8.0% a") + print(" XS01 50%). Il controllo a **XS01 FISSO al 50% e leva comune** lo smonta: lo shock") + print(" di rottura e' -10.1 / -10.2 / -10.2% muovendo TP01 da 12% a 38%, cioe' INVARIANTE,") + print(" e la sopravvivenza sotto l'ipotesi severa CALA (74.1 -> 72.6 -> 64.7%). Cio' che") + print(" protegge dall'ignoto di XS01 e' **meno XS01**, non piu' TP01: il primo numero") + print(" confrontava due pesi di XS01 diversi e attribuiva a TP01 il merito dell'altro.") print("\n ⚠️ COSA MI SMENTIREBBE: un crash in cui SKH01 NON si gira short in tempo (gap") print(" notturno oltre lo stop, o venue fermo). Il campione ne ha quattro e in tutti e") print(" quattro SKH01 ha girato. Basterebbe un episodio in cui SKH01 perde quanto il") From 409de063f6291c556b1fc37ccea8f1e9cea3f74a Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 21:08:02 +0000 Subject: [PATCH 032/114] =?UTF-8?q?research(wave-0822):=20registro=20?= =?UTF-8?q?=E2=80=94=20TP01-SINISTRO,=20l'allarme=20falsificato=20nel=20ve?= =?UTF-8?q?rso?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 61 ++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index a402bd9..cbb1d25 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -697,7 +697,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 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 | | 25 | BIN-FREQ | il binario di frequenza su SKH01 e' un candidato o un sottoprodotto? | _in corso_ | -| 26 | TP01-SINISTRO | quanto pesa TP01 se l'obiettivo include un crash che nel campione non c'e'? | _in corso_ | +| 26 | TP01-SINISTRO | quanto pesa TP01 se l'obiettivo include un crash che nel campione non c'e'? | **allarme SCARTATO** — il rischio e' XS01, non TP01 | | 27 | SURFACE-RV | la superficie e' internamente incoerente, e l'incoerenza vale piu' del costo di attraversarla? | _in corso_ | @@ -972,3 +972,62 @@ L'attesa di anni **non serve**. Ma tre cose che il gate non chiedeva sono ora mi 2024+**, cioe' proprio il periodo **senza depeg USDT**, mentre il fuori campione contiene il **depeg di maggio 2022**. Se i close Binance/USDT 2021-23 divergessero da un venue **USD** in modo *non comune fra gli asset*, il ranking cambierebbe e il risultato con esso. + +--- + +## 26 — TP01-SINISTRO (il peso di TP01 contro un crash che nel campione non c'e') + +`scripts/research/r0822b_tp01_sinistro.py` (~200 s). **1.784 valutazioni Monte Carlo** contate dal +codice, ognuna 2.500 percorsi eval + 2.500 funded, **lente accoppiata**, de-luck x0,89. +**Replica di controllo:** i numeri E-ter di PROP-ALLOC (argmax **38/62 @0,50x J 0,521**, live 75/25 +J 0,443) riprodotti **al millesimo**. + +**L'allarme da verificare** era il caveat che PROP-ALLOC aveva dichiarato contro se stesso: *"la +sotto-pesatura di TP01 e' misurata su un campione SENZA SINISTRO: su una barriera assorbente quella +scommessa si paga una volta sola"*. L'agente ha **registrato nel docstring, prima di misurare**, che +si aspettava di **confermarlo**. + +| lente | argmax w TP01 | J argmax | J a 0,75 (live) | +|---|---|---|---| +| **2019+ (contiene il 2022)** | **0,375** | 0,521 | 0,443 | +| 2024+ (la finestra di PROP-ALLOC) | **0,375** | 0,423 | 0,317 | +| SOLO-SINISTRO | **0,000** | 0,771 | 0,000 | + +**L'allarme e' falsificato nel VERSO, non nella cautela: TP01 ~0,375 e' l'argmax sia sulla finestra +che contiene il 2022 sia su quella che non lo contiene, e nel sinistro il libro con MENO TP01 fa +MEGLIO** — perche' TP01 va flat mentre **SKH01 si gira short e guadagna**. Conto dell'assicurazione: +premio **-4,01%/anno** fuori dal sinistro, indennizzo **-12,5%** cumulato sui 120 giorni di sinistro +(LIVE -2,8% contro OPT +9,7%). Test dei segni: OPT batte LIVE **12/12** sulle peggiori finestre 30g +(p=0,0005) e **8/8 sul solo sotto-campione pre-2024 indipendente** (p=0,0078). + +**Null del de-levering — 7a occorrenza, in veste nuova** ("protezione dal crash" invece di "meno +drawdown") **e decisivo**: a leva comune TP01 protegge davvero (sopravvivenza al crash 32,4% -> 76,4% +da TP01 0 a 0,625), ma la stessa protezione si compra abbassando la leva, e **a ISO-SOPRAVVIVENZA +meno TP01 paga di piu'** (payout $4.490 a TP01 0,125 -> **$2.119** a TP01 1,00, monotono). + +**Tre risultati collaterali che valgono piu' dell'allarme rimosso:** +1. **"2024-2026 non contiene un crash" e' FALSA nella forma**: il 2024+ ha buy&hold a **-60% di + drawdown** e -39,6% in 20 giorni. Manca la **taglia** (~2,3x sulla coda 1-10 giorni). La domanda + giusta non e' *"e se arriva un crash"* ma **"e se ne arriva uno due volte piu' grande"**. +2. **Alle leve funded il 2022 NON era una minaccia** (min equity -1,4/-2,4% contro barriera -6%): + cio' che uccide un conto a barriera e' la **regola a un giorno su una giornata qualunque**, non il + crash del sottostante. E il vincolo che morde **cambia col peso**: sotto TP01 0,50 e' il + daily-loss, sopra e' il max-loss cumulato. +3. **Il rischio vero dell'ottimo non e' TP01, e' XS01**: uno shock di **un giorno a ~-10%** dimezza + la sopravvivenza quando il crash arriva, e **XS01 non ha nessun sinistro nel campione**. + **Ipotesi dell'agente nata e refutata in sessione:** aveva scritto che TP01 fa da zavorra contro + quell'ignoto (-12,3% a XS01 25% contro -8,0% a XS01 50%); il controllo a **XS01 fisso e leva + comune** lo smonta - lo shock di rottura e' **-10,1/-10,2/-10,2%** muovendo TP01 da 12% a 38% + (**invariante**) e la sopravvivenza severa **cala**. *Protegge meno XS01, non piu' TP01: il primo + confronto attribuiva a TP01 il merito di un altro peso.* + +**Libro live NON toccato** (75/25 resta). Nota per la memoria: il 75/25 e' **fuori** dalla regione di +compromesso del funded (0,375-0,50) - coerente col fatto gia' scritto il 25/07, *canale funded e +libro proprietario vogliono libri diversi*. + +**Caveat principale, dichiarato:** `anchor_luck_band` **non ri-misurata** sotto questa lente; gira +sull'ancora canonica e si appoggia a **SKH01, lo sleeve con la fortuna d'ancora piu' grande da +restituire** -> **la distorsione e' A FAVORE della conclusione**. **Smentitore dichiarato:** un crash +in cui **SKH01 non si gira short in tempo** (gap notturno oltre lo stop, venue fermo). Il campione ne +ha **quattro** episodi e in tutti e quattro SKH01 ha girato: **l'ampiezza effettiva della coda e' 4, e +nessun bootstrap la aumenta.** From 1b5ddd1ba10a01fb3392c2acf4d24207c3cafd2a Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 21:08:36 +0000 Subject: [PATCH 033/114] =?UTF-8?q?research(wave-0822):=20SURFACE-RV=20?= =?UTF-8?q?=E2=80=94=20la=20superficie=20e'=20arbitrage-free=20sui=20prezz?= =?UTF-8?q?i=20eseguibili;=20l'incoerenza=20al=20mid=20E'=20la=20larghezza?= =?UTF-8?q?=20del=20mercato?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 65 ++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index cbb1d25..146a894 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -698,7 +698,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 24 | CRITICO | cosa NON e' stato misurato, e quale singola misura mancante vale di piu' | **3 correzioni all'ondata**, 1 regola mia ritirata | | 25 | BIN-FREQ | il binario di frequenza su SKH01 e' un candidato o un sottoprodotto? | _in corso_ | | 26 | TP01-SINISTRO | quanto pesa TP01 se l'obiettivo include un crash che nel campione non c'e'? | **allarme SCARTATO** — il rischio e' XS01, non TP01 | -| 27 | SURFACE-RV | la superficie e' internamente incoerente, e l'incoerenza vale piu' del costo di attraversarla? | _in corso_ | +| 27 | SURFACE-RV | la superficie e' internamente incoerente, e l'incoerenza vale piu' del costo di attraversarla? | **SCARTATO** — l'incoerenza al mid **e'** la larghezza del mercato | --- @@ -1031,3 +1031,66 @@ restituire** -> **la distorsione e' A FAVORE della conclusione**. **Smentitore d in cui **SKH01 non si gira short in tempo** (gap notturno oltre lo stop, venue fermo). Il campione ne ha **quattro** episodi e in tutti e quattro SKH01 ha girato: **l'ampiezza effettiva della coda e' 4, e nessun bootstrap la aumenta.** + +--- + +## 27 — SURFACE-RV (la superficie come relative value fra i suoi punti) + +`scripts/research/r0822b_surface_rv.py` (~85-130 s). **~112 valutazioni**, e **nessuna e' una +selezione**: e' un **censimento su 1,24 M quote a due lati**, non una ricerca di celle. + +L'ipotesi era di **microstruttura, non di previsione**: non chiede alla vol implicita di prevedere +niente, chiede se la superficie sia internamente incoerente e si ricomponga. E' l'unico angolo della +superficie che l'ondata non aveva coperto (livello, skew, struttura a termine e quote reali erano gia' +chiusi). + +| test **model-free** (nessun interpolatore) | esito | +|---|---| +| **V — monotonia P(K)** | **0 / 1.111.585** | +| **B — convessita' P(K)** | mid 6,213% · **eseguibile 1 / 1.031.256 = 0,0001%**, vale **$0,00**, sparita l'ora dopo | +| **C — varianza totale nel tenore** | strike condiviso 6/107.177 (0,006%) · con interpolazione 10/112.827 → **l'interpolatore contribuisce +0,003 pp** | + +**Ampiezza:** |residuo| mediano **0,125 pt-vol (BTC) / 0,200 (ETH)** contro **mezzo** spread +0,861 / 1,208 → rapporto su spread pieno **0,053 / 0,063**; **sotto il tick nel 96,5% / 95,3% dei +casi** — l'incoerenza **non e' esprimibile nella griglia di prezzo del venue**. Economia al lotto +minimo: fee 3 gambe $7,54 (BTC) / $2,11 (ETH), miglior credito netto su **782 k farfalle** +equispaziate **−$0,95 / −$0,33**, **positivi 0**. Calendario: violazione mediana **$0,09** contro +**$0,59** di costo, e la superficie sta **13,2 pt-vol** dal bordo di non-arbitraggio → vincolo +strutturalmente slack. + +📌 **IL MECCANISMO, che vale piu' del verdetto: |residuo| cresce MONOTONAMENTE col largo del +mercato** — BTC **0,080 pt** a spread 2,6% → **0,255 pt** a spread 50%. **L'incoerenza al mid *e'* la +larghezza del mercato guardata attraverso il mid**, non un errore di prezzo. + +⚠️ **E questo uccide l'unica riga non-zero, che era un artefatto del CONTROFATTUALE dell'agente +stesso:** prendendo tutta la superficie, lo spread USDC del 2,6% aprirebbe **2.293 farfalle a credito +su ETH** (miglior caso $123); **restringendo alla parte di superficie gia' quotata stretta quanto le +USDC → 0/13.287 (ETH) e 0/145.431 (BTC)**, miglior caso −$0,58 / −$0,82. *Prendeva il mid di un +mercato larghissimo e fingeva di poterlo quotare al 2,6%.* + +**Reversione MISURATA, non assunta:** β **−0,062/−0,056 per ora**, corr(t,t+1) **+0,94**, mezza-vita +**11-12 h** → il residuo e' **la forma dello smile**, non un errore che si ricompone. + +**Gate:** **controlli positivi 3/3 in entrambi i versi** (violazione costruita → rilevata; catena sana +→ zero) — *un rilevatore che trova ~0 e non e' validato e' indistinguibile da uno rotto*; causalita' +**strutturale** (ogni test e' contemporaneo dentro un solo snapshot); ancora oraria girata (incoerenza +mid 5,99-6,36% su 24 ore, eseguibile max 0,00222% → **nessuna ora apre una finestra**). +`marginal_vs_tp01`/`deflated_sharpe`/de-levering/`implausible_sharpe` **non girati, e il perche' e' il +risultato**: non esiste una serie di ritorni da valutare (1 opportunita' su 1,03 M, del valore di +$0,00). `f_markfit` **1,0010** — converge col **0,992** del filone SKEW, implementazione separata → +il difetto non e' il prezzatore. + +**VERDETTO: SCARTATO. Il lato relative-value della superficie e' chiuso.** +**Smentitore piu' forte, dichiarato:** quote **reali** della famiglia USDC (tutta l'estrapolazione e' +un controfattuale sui mid inverse) — osservazione decisiva: puntare il collettore anche su +`{"currency": "USDC"}` e rifare TEST B sul suo bid/ask. + +🚨 **SOTTOPRODOTTO — buco di colonna nell'archivio ereditato, mai registrato:** `bite_archive` ha +`index_price` e `underlying_price` **100% None (1.232.212/1.232.212)**. Il sottostante esiste **solo +dal 2026-07-30** (raccolta propria, 18,2% delle righe); `book_depth_top3` solo nel 67,2%. **Chiunque +calcoli moneyness o riprezzi sull'archivio pre-30/07 sta usando una colonna che non c'e'.** +✅ **Ed e' chiudibile:** il forward si ricostruisce con la **parita' put-call** dalla catena stessa — +verificato contro l'osservato su **6.578 coppie: |errore| mediano 0,023%, p95 0,147%, corr +1,000000** → rende la superficie utilizzabile su tutti i 75 giorni invece che 23. +✅ **Terza convergenza indipendente sul muro del dato** (75 giorni di superficie); e lo **smile** — +che i filoni SKEW/TERM non usavano — esiste invece su **113 giorni dal 2026-05-01**. From aaa32daff735ba1860b4e24226ebc373fbba68d9 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 21:08:57 +0000 Subject: [PATCH 034/114] CLAUDE.md: buco di colonna nell'archivio catena (underlying 100% None pre-30/07) e come chiuderlo con la parita' put-call --- CLAUDE.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 9eea10f..9792220 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2348,6 +2348,23 @@ df = load_data("BTC", "1h") # OK. load_data("SOL", ...) -> FileNotFoundError ( diverse, e `book_depth_top3` è **NULL su errore, mai 0**. ⚠️ Le righe ereditate da bite hanno `quote_status='unknown'`: bite non registrava il perché, e si dichiara l'ignoranza invece di inventare uno stato. + 🚨 **BUCO DI COLONNA nell'archivio ereditato, trovato il 2026-08-22 e mai registrato prima:** + `bite_archive` ha `index_price` e `underlying_price` **100% None (1.232.212/1.232.212)**. Il + sottostante esiste **solo dal 2026-07-30** (raccolta propria, 18,2% delle righe) e + `book_depth_top3` solo nel **67,2%**. **Chiunque calcoli moneyness o riprezzi sull'archivio + pre-30/07 sta usando una colonna che non c'è** — e il file si legge senza errori, quindi il + difetto è silenzioso. + ✅ **Ed è CHIUDIBILE senza dato nuovo:** il forward si ricostruisce con la **parità put-call** + dalla catena stessa — verificato contro l'osservato su **6.578 coppie: |errore| mediano 0,023%, + p95 0,147%, corr 1,000000** → rende la superficie utilizzabile su tutti i **75 giorni** invece + che 23. (Lo **smile**, che non richiede il forward, esiste invece su **113 giorni dal 2026-05-01**; + la **superficie completa** solo da **2026-06-09**, perché prima bite raccoglieva una sola scadenza + per giro — misurato da **tre** agenti indipendenti.) + ⚠️ La famiglia raccolta è quella **inverse** (`get_instruments?currency=BTC|ETH`): la superficie + **USDC-lineare non è nell'archivio**, e ogni conclusione su di essa nel progetto è oggi un + **controfattuale costruito sui mid inverse**, non una misura. Puntare il collettore anche su + `currency=USDC` costa **+587 chiamate/giro (+90%)** ed è una decisione sul rate-limit per-IP, che + ha già causato un guasto il 29/07. **NON assorbito, e perché:** il motore credit-spread ETH (il progetto ha già la regola "niente short-vol da modello in deploy"), la GUI, kill switch/dead-man/audit (PythagorasGoal ha `venue_watch`/`edge_watch`/`monitor_health`/`fee_watch`), `dvol_history` (`fetch_dvol.py` ha From 74539db8b8056d9e04d46be0e3b119ee64297b70 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 21:17:36 +0000 Subject: [PATCH 035/114] =?UTF-8?q?research(wave-0822):=20DEPEG=20?= =?UTF-8?q?=E2=80=94=20XS01-OOS=20sopravvive=20al=20falsificatore=20di=20v?= =?UTF-8?q?enue;=20il=20caso=20peggiore=20non=20e'=20il=20depeg=20ma=20il?= =?UTF-8?q?=202025-10-10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 63 +++++++++++ scripts/research/r0822b_depeg.py | 189 +++++++++++++++++++++++++++++-- 2 files changed, 243 insertions(+), 9 deletions(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 146a894..d2ec17c 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -693,6 +693,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | # | filone | domanda che decide | verdetto | |---|---|---|---| | 21 | XS01-OOS | il vantaggio del PROP-ALLOC poggia sul drift di XS01 misurato sulla sua finestra di scoperta: regge fuori? | **LEAD** — regge, ed e' PIU' GRANDE fuori | +| 28 | DEPEG | il falsificatore che l'autore di 21 ha nominato contro se stesso: il depeg USDT contamina il ranking? | **SOPRAVVIVE** — il venue sposta 0,01 di Sharpe | | 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 | @@ -1094,3 +1095,65 @@ verificato contro l'osservato su **6.578 coppie: |errore| mediano 0,023%, p95 0, 1,000000** → rende la superficie utilizzabile su tutti i 75 giorni invece che 23. ✅ **Terza convergenza indipendente sul muro del dato** (75 giorni di superficie); e lo **smile** — che i filoni SKEW/TERM non usavano — esiste invece su **113 giorni dal 2026-05-01**. + +--- + +## 28 — DEPEG (il falsificatore di XS01-OOS, eseguito) + +`scripts/research/r0822b_depeg.py` (811 righe, **18 s**, offline dalla cache, **due corse +byte-identiche**). **112 configurazioni**, e **nessuna e' una selezione**: e' un confronto **appaiato** +dello stesso meccanismo congelato su due fonti di prezzo. + +**Perche':** l'autore di §21 aveva dichiarato contro se stesso che la sua validazione di venue gira +**solo sul 2024+**, cioe' proprio il periodo **senza depeg USDT**, mentre il fuori campione contiene +il **maggio 2022**. La soglia — `corr fra i due sleeve >= 0,99` — era **dichiarata da lui, non +dall'esecutore del test**. + +**Il dato, e il primo limite:** su 13 asset **10 hanno un prezzo USD** che copre la finestra (9 +Coinbase + **XRP da Bitstamp**, perche' Coinbase l'aveva sospeso). **BNB non esiste in USD prima del +2025-10** — e' il token nativo del venue sotto esame. AVAX parte 2021-09-30 → il test **non copre il +2021**, ma copre **esattamente cio' che l'obiezione nomina: maggio 2022 e FTX**. + +📌 **Divergenza SCOMPOSTA nelle due componenti che sono domande diverse** (824 giorni, 8.240 +osservazioni appaiate): **(a) comune** med 3,2 bps / max 76,7 — quella che lo z-score annulla; +**(b) idiosincratica** med **2,1 bps** / p95 11,2 / max 116,5 — quella che cambia il ranking. +Nel depeg (07→20 maggio 2022) il comune arriva a **−39 bps** ma l'idiosincratico sale solo a med +**3,6** / max **33,8 bps** contro 2,0 / 10,9 fuori. + +**TEST 1** (meccanismo congelato, 10 asset, 10 fasi, 2021-09→2023-12): **corr mediana 0,9992, minima +0,9921 → PASS**; **dSharpe appaiato −0,000** [−0,056, +0,036]; divario di equity cumulato su 2,25 +anni **+0,07%**. **Il canale del danno, guardato direttamente:** 734 ribilanciamenti, **10 con gambe +diverse (1,36%)**, tutti a sovrapposizione 4/5, **ZERO dentro la finestra del depeg**; Spearman dello +score mediana **1,0000**. +**TEST 2** (finestra intera di §21): il numero di testa **+1,12 → +1,11**, delta appaiato −0,003. + +**VERDETTO: `IL RISULTATO XS01-OOS: SOPRAVVIVE`** — il venue sposta il titolo di §21 di **0,01 di +Sharpe** e **non cambia una sola gamba dentro il depeg**. +**Controllo positivo girato** (obbligatorio, o "corr 0,99" potrebbe essere uno sfasamento): USD +ritardato di 1 giorno → corr mediana **−0,011**. + +**Tre cose trovate che vanno registrate:** +1. 📌 **L'unica corr sotto soglia e' ATTRIBUITA, e non e' il prezzo.** Col **gate di dispersione + spento** su entrambi i venue la minima passa **0,9674 → 0,9977**. Il gate `disp >= percentile` e' + **binario**, quindi pochi bps cadono ai due lati → **4 intervalli da 10 giorni** in cui uno sleeve + e' pieno e l'altro **flat**, piu' una coda di ~30 giorni in cui la `rv30` del vol-target resta + diversa. **Una scansione di ranking non lo vede.** ✅ Converge con §21 (*"il gate p30 fuori campione + vale ZERO"*): **non porta edge e amplifica il rumore** — ⚠️ non e' una proposta di cambiarlo, + cambiarlo guardando questo numero sarebbe selezione. +2. 🚨 **Il caso peggiore dell'intero campione NON e' il depeg: e' il 2025-10-10** (cascata di + liquidazioni sulle coppie /USDT) — **INJ 1.981 bps**, ADA 315, LTC 201, AAVE 136 mentre BTC/ETH + stanno a 21-22. **Il meccanismo temuto dall'obiezione e' REALE**; semplicemente non e' accaduto nel + 2021-2023 ed e' caduto nella finestra di **scoperta**, non nel fuori campione. Nel depeg le + peggiori divergenze sono **AVAX subito dopo la quotazione = book sottile, non stablecoin**. +3. ⚠️ **BNB resta non controllabile** e toglierlo costa **−0,75** di mediana di fase (+1,12 → +0,37) + — ✅ **replica esatta del leave-one-out di §21 con implementazione separata**. Contesto onesto: + nella LOO di §21 **LTC (+0,20), BTC (+0,33) e XRP (+0,33) pesano di piu'**, e quelle tre **sono + coperte** dal controllo USD. + +**Smentitore residuo dichiarato:** la divergenza del 2025-10-10 e' di **chiusura**; un evento +**intra-giornaliero** della stessa natura sposterebbe il ranking **senza lasciare traccia sulle +chiusure**, e i close giornalieri non lo vedrebbero. + +⚠️ *Nota di attribuzione: il file risultava `M` in git perche' **io** (coordinatore) l'avevo incluso +in un commit mentre l'agente ci stava ancora lavorando — non un altro agente. Un `git add -A` durante +un'ondata cattura il lavoro in corso.* diff --git a/scripts/research/r0822b_depeg.py b/scripts/research/r0822b_depeg.py index 0c4a413..d9dd013 100644 --- a/scripts/research/r0822b_depeg.py +++ b/scripts/research/r0822b_depeg.py @@ -191,7 +191,12 @@ def certify_usd(asset: str, d: pd.DataFrame, a: str, b: str) -> dict: flat=float("nan"), zero_vol=float("nan")) v = series_of(d.assign(close=d["volume"])) v = v[(v.index >= pd.Timestamp(a, tz="UTC")) & (v.index < pd.Timestamp(b, tz="UTC"))] - attesi = (pd.Timestamp(b, tz="UTC") - s.index.min()).days + # ⚠️ la copertura si misura sulla finestra RICHIESTA, non sulla prima barra disponibile: + # contando dal proprio inizio, un asset quotato a meta' finestra esce con gap=0 e "OK", e il + # conteggio degli usabili contraddirebbe la riga sotto che lo esclude. E' lo stesso difetto + # che il progetto ha gia' incontrato due volte (serie TRONCATA che passa ogni controllo + # interno): un controllo che guarda solo DENTRO la serie non vede cio' che la serie non copre. + attesi = (pd.Timestamp(b, tz="UTC") - pd.Timestamp(a, tz="UTC")).days c = s.values flat = float(np.mean(c[1:] == c[:-1])) if len(c) > 1 else float("nan") zv = float(np.mean(v.values == 0.0)) if len(v) else float("nan") @@ -294,19 +299,83 @@ def ranking_scan(PB: pd.DataFrame, PU: pd.DataFrame, phases: list[int], if ov < 1.0: diff = sorted(set(PB.columns[list(hiB ^ hiU)]) | set(PB.columns[list(loB ^ loU)])) rows.append(dict(fase=ph, data=PB.index[i], overlap=ov, gambe=",".join(diff))) - D = pd.DataFrame(rows) + D = pd.DataFrame(rows, columns=["fase", "data", "overlap", "gambe"]) print(f" [{label}] {tot} ribilanciamenti su {len(phases)} fasi; specchio verificato " f"contro il motore in {checked}/{checked} con posizione") print(f" [{label}] Spearman dello score fra i venue: mediana {np.median(sp):.4f} " f"min {np.min(sp):.4f}") print(f" [{label}] ribilanciamenti con gambe DIVERSE: {len(D)}/{tot} " f"({len(D)/max(tot,1)*100:.2f}%)") - for _, r in D.iterrows(): - print(f" fase {int(r['fase'])} {r['data'].date()} " - f"sovrapposizione {r['overlap']*100:3.0f}% gambe scambiate: {r['gambe']}") + if len(D): + pre = D[D["data"] < pd.Timestamp(T2_END, tz="UTC")] + print(f" [{label}] di cui NELLA finestra fuori campione ({T2_START}..{T2_END}): " + f"{len(pre)} — elencati qui sotto; quelli dal 2024 in poi non sono la domanda") + for _, r in pre.iterrows(): + print(f" fase {int(r['fase'])} {r['data'].date()} " + f"sovrapposizione {r['overlap']*100:3.0f}% gambe scambiate: {r['gambe']}") return D +def gate_flips(PB: pd.DataFrame, PU: pd.DataFrame, ph: int) -> list[tuple]: + """Il SECONDO canale, che una scansione di ranking non vede: il gate di dispersione e' BINARIO + (`disp_i >= percentile espandente`) e quindi DISCONTINUO — due prezzi che differiscono di + pochi bps possono cadere ai due lati della soglia, e allora uno sleeve resta pienamente + investito per 10 giorni mentre l'altro sta FLAT. Non e' un cambio di ranking: e' un + interruttore. Ritorna gli intervalli in cui i due venue non sono d'accordo su acceso/spento.""" + dB: dict = {} + dU: dict = {} + X.xsec_engine(PB, phase=ph, detail=dB) + X.xsec_engine(PU, phase=ph, detail=dU) + gb = np.abs(dB["W"]).sum(axis=1) > 1e-9 + gu = np.abs(dU["W"]).sum(axis=1) > 1e-9 + idx = PB.index + flip = np.flatnonzero(gb != gu) + runs, i0 = [], None + for j, i in enumerate(flip): + if i0 is None: + i0 = i + if j + 1 == len(flip) or flip[j + 1] != i + 1: + runs.append((idx[i0], idx[i], int(i - i0 + 1), bool(gb[i0]))) + i0 = None + return runs + + +def autopsia(PB: pd.DataFrame, PU: pd.DataFrame, ph: int, D: pd.DataFrame, label: str) -> None: + """La fase con la corr piu' bassa NON si riassume, si guarda: una corr di 0,97 o e' un + ribaltamento di ranking (il guasto che sto cercando) o e' un singolo giorno di rumore su una + gamba sottile (che non e' un guasto). Sono conclusioni opposte a partire dallo stesso numero.""" + a, b = pair(alive(X.xsec_engine(PU, phase=ph)), alive(X.xsec_engine(PB, phase=ph))) + g = (a - b) + top = g.abs().sort_values(ascending=False).head(4) + dph = D[D["fase"] == ph] if len(D) else D + runs = gate_flips(PB, PU, ph) + print(f" autopsia della fase peggiore ({label}, fase {ph}): i 4 giorni di scarto maggiore") + for t, v in top.items(): + prev = dph[dph["data"] <= t]["data"] + near = (t - prev.iloc[-1]).days if len(prev) else None + inflip = [r for r in runs if r[0] <= t <= r[1]] + # ⚠️ la coda: dopo una discordanza del gate uno dei due sleeve ha 10 giorni di ZERI in + # piu' nella finestra rv30 del vol-target -> la LEVA dei due resta diversa per ~30 giorni + # dopo che la discordanza e' finita. Chiamare "rumore di prezzo" un giorno dentro quella + # coda attribuirebbe al venue un effetto che e' della soglia binaria. + tail = [r for r in runs if r[1] < t and (t - r[1]).days <= 30] + if inflip: + r = inflip[0] + tag = (f"GATE DISCORDE {r[0].date()}..{r[1].date()}: " + f"{'USDT investito, USD flat' if r[3] else 'USD investito, USDT flat'}") + elif tail: + tag = (f"coda del vol-target: gate discorde fino al {tail[-1][1].date()}, " + f"{(t - tail[-1][1]).days}g fa -> rv30 diversa, LEVA diversa") + elif near is not None and near <= XS_CFG["H"]: + tag = f"ultimo ribilanciamento con gambe diverse {near} giorni prima" + else: + tag = "stesse gambe, stesso gate: solo rumore di prezzo" + print(f" {t.date()} scarto {g.loc[t]*1e4:+8.0f} bps ({tag})") + print(f" scarto quadratico totale della fase: {float((g**2).sum())**0.5*1e4:.0f} bps; " + f"i 4 giorni sopra ne spiegano " + f"{float((top**2).sum())/float((g**2).sum())*100:.0f}%") + + def score_at(P: pd.DataFrame, i: int, lookbacks) -> tuple[np.ndarray, np.ndarray]: """Specchio della riga di score del motore (z-score medio sui lookback). Serve per confrontare i RANKING fra venue; la sezione [4] verifica che i top-k/bottom-k che ne derivano coincidano @@ -494,6 +563,35 @@ def main() -> None: (D1["data"] < pd.Timestamp(DEPEG[1], tz="UTC"))] print(f" di cui dentro la finestra del depeg: {len(ind)}") + # ---------------------- [4b] il SECONDO canale: il gate binario, e l'attribuzione + print("\n[4b] IL GATE DI DISPERSIONE E' UN INTERRUTTORE — e una scansione di ranking non lo") + print(" vede. `disp_i >= percentile espandente` e' una soglia: pochi bps di differenza di") + print(" prezzo possono cadere ai due lati, e per 10 giorni uno sleeve e' pieno e l'altro") + print(" e' FLAT. E' l'unico modo in cui una differenza di venue microscopica puo'") + print(" produrre una differenza di P&L macroscopica.") + for label_, Pb_, Pu_ in (("TEST 1 / U10", PB, PU),): + tot_fl = 0 + for ph in PHASES: + for a_, b_, nd, bfirst in gate_flips(Pb_, Pu_, ph): + tot_fl += 1 + print(f" [{label_}] fase {ph}: {a_.date()} .. {b_.date()} ({nd}g) — " + f"{'USDT investito / USD FLAT' if bfirst else 'USD investito / USDT FLAT'}") + if tot_fl == 0: + print(f" [{label_}] nessuna discordanza del gate su 10 fasi") + else: + print(f" [{label_}] {tot_fl} intervalli di discordanza su 10 fasi") + + print("\n ATTRIBUZIONE — stesso confronto con il gate SPENTO su ENTRAMBI i venue") + print(" (diagnostica, NON una proposta di cambiare il meccanismo: serve a separare") + print(" 'il prezzo e' diverso' da 'la soglia binaria amplifica una differenza minuscola'):") + cg = [] + for ph in PHASES: + a_, b_ = pair(alive(X.xsec_engine(PU, phase=ph, disp_pct=0)), + alive(X.xsec_engine(PB, phase=ph, disp_pct=0))) + cg.append(float(a_.corr(b_))) + print(f" [TEST 1] corr con gate SPENTO: mediana {np.median(cg):.4f} minima {np.min(cg):.4f}" + f" (col gate acceso: mediana {R['corr'].median():.4f}, minima {R['corr'].min():.4f})") + # ------------------------------- [5] TEST 2 — U13 ibrido, finestra INTERA di §21 print(f"\n[5] TEST 2 — U13 IBRIDO (USD dove esiste, USDT altrove), {T2_START} .. {T2_END}") print(" Domanda operativa: sostituendo TUTTO il prezzo in dollari che esiste, il titolo") @@ -537,6 +635,24 @@ def main() -> None: print("\n E il ranking, sull'ibrido (la fase con la corr piu' bassa va GUARDATA, non") print(" riassunta: una corr di 0,97 su una fase o e' un ribaltamento vero o e' un giunto):") D2 = ranking_scan(PBh, PUh, PHASES, "TEST 2 / U13-IBRIDO") + print("\n Discordanze del GATE binario sull'ibrido (finestra fuori campione):") + nfl = 0 + for ph in PHASES: + for a_, b_, nd, bfirst in gate_flips(PBh, PUh, ph): + if a_ >= pd.Timestamp(T2_END, tz="UTC"): + continue + nfl += 1 + print(f" fase {ph}: {a_.date()} .. {b_.date()} ({nd}g) — " + f"{'USDT investito / USD FLAT' if bfirst else 'USD investito / USDT FLAT'}") + print(f" totale {nfl} intervalli su 10 fasi (su ~204 ribilanciamenti per fase)") + cg2 = [] + for ph in PHASES: + a_, b_ = pair(win(alive(X.xsec_engine(PUh, phase=ph, disp_pct=0)), T2_START, T2_END), + win(alive(X.xsec_engine(PBh, phase=ph, disp_pct=0)), T2_START, T2_END)) + cg2.append(float(a_.corr(b_))) + print(f" ATTRIBUZIONE [TEST 2] corr con gate SPENTO: mediana {np.median(cg2):.4f} " + f"minima {np.min(cg2):.4f} (col gate acceso: mediana {R2['corr'].median():.4f}, " + f"minima {R2['corr'].min():.4f})") # ------------------------------- [5b] le gambe che NESSUN venue USD puo' controllare print("\n[5b] LE TRE GAMBE NON CONTROLLABILI — quanto del risultato di §21 ci passa?") @@ -588,15 +704,44 @@ def main() -> None: f"T2 fase {int(worst2['ph'])} corr {worst2['corr']:.4f}") print(f" peggior FASE per dSharpe: T1 fase {int(wd1['ph'])} {wd1['d']:+.3f} | " f"T2 fase {int(wd2['ph'])} {wd2['d']:+.3f}") + autopsia(PB, PU, int(worst1["ph"]), D1, "TEST 1") + autopsia(PBh, PUh, int(worst2["ph"]), D2, "TEST 2") st = idio.abs().stack().sort_values(ascending=False) - print(" le 8 divergenze IDIOSINCRATICHE piu' grandi della finestra (bps di livello):") + print(" le 8 divergenze IDIOSINCRATICHE piu' grandi della finestra del TEST 1 (bps):") for (t, s), v in st.head(8).items(): indep = "DEPEG" if pd.Timestamp(DEPEG[0], tz="UTC") <= t < pd.Timestamp(DEPEG[1], tz="UTC") else "" print(f" {str(t.date()):<12}{s:<6}{v:8.0f} bps (comune quel giorno " f"{common.loc[t]:+.0f} bps) {indep}") - print(" -> se le peggiori NON cadono nel depeg, il rumore di venue e' book sottile,") - print(" non contaminazione da stablecoin: due cause diverse, due implicazioni diverse.") + print(" -> le peggiori NON cadono nel depeg e sono quasi tutte AVAX nelle settimane subito") + print(" dopo la sua quotazione su Coinbase: book sottile, non contaminazione da") + print(" stablecoin. Due cause diverse e due implicazioni diverse.") + + # --------- il caso peggiore su TUTTO il campione, non solo sulla finestra del test + print("\n ⚠️ E ORA LA DOMANDA GIUSTA, allargata: dove divergono di piu' i due venue in") + print(" TUTTO il campione 2021-2026? (cercare il caso peggiore solo dentro la finestra") + print(" che si sta difendendo e' un modo di non trovarlo)") + have = [s for s in U13 if USD_SRC[s] is not None] + Bf = bin_panel(have) + Uf = usd_panel(have).reindex(Bf.index) + bo = Bf.notna() & Uf.notna() + Df = (1e4 * np.log(Uf / Bf)).where(bo) + idf = Df.sub(Df.median(axis=1), axis=0) + worst_day = idf.abs().max(axis=1).sort_values(ascending=False) + print(f" {'giorno':<12}{'|idio| max':>11}{'asset':>7}{'comune':>9} finestra") + for t in worst_day.head(6).index: + s = idf.loc[t].abs().idxmax() + w = "fuori campione" if t < pd.Timestamp(T2_END, tz="UTC") else "scoperta 2024+" + print(f" {str(t.date()):<12}{idf.loc[t, s]:>11.0f}{s:>7}{Df.loc[t].median():>9.0f} {w}") + tw = worst_day.index[0] + print(f" il giorno peggiore in assoluto e' il {tw.date()}: scarto per asset (bps)") + print(" " + " ".join(f"{s}:{idf.loc[tw, s]:.0f}" for s in have + if np.isfinite(idf.loc[tw, s]))) + print(" 📌 E' un fatto che va detto per intero: la divergenza idiosincratica PUO'") + print(" esplodere — qui di due ordini di grandezza — ma e' successo in una CASCATA DI") + print(" LIQUIDAZIONI sulle coppie /USDT, non in un depeg, e cade nella finestra di") + print(" SCOPERTA (2024+, dove lo sleeve gira su Hyperliquid), non nel fuori campione.") + print(" Il meccanismo temuto dall'obiezione e' REALE; non e' accaduto nel 2021-2023.") # il giorno peggiore: dove il P&L dei due sleeve si stacca di piu' a0 = alive(X.xsec_engine(PU, phase=0)); b0 = alive(X.xsec_engine(PB, phase=0)) @@ -615,7 +760,11 @@ def main() -> None: print(f" TEST 1 U10 congelato : corr mediana {R['corr'].median():.4f}, minima " f"{R['corr'].min():.4f} -> {'PASS' if R['corr'].min() >= CORR_SOGLIA else 'FAIL'}") print(f" TEST 2 U13 ibrido : corr mediana {R2['corr'].median():.4f}, minima " - f"{R2['corr'].min():.4f} -> {'PASS' if R2['corr'].min() >= CORR_SOGLIA else 'FAIL'}") + f"{R2['corr'].min():.4f} -> {'PASS' if R2['corr'].min() >= CORR_SOGLIA else 'FAIL'}" + f" (una fase su 10; col gate spento minima {np.min(cg2):.4f} = PASS)") + print(f" ⚠️ il TEST 2 e' MIO, non quello dichiarato: e' piu' severo per costruzione") + print(f" (le gambe cambiano venue in date diverse) e la sua unica fase sotto soglia") + print(f" e' attribuita al gate binario, non al prezzo.") print(f" riferimento 2024+ di §21 (Binance vs Hyperliquid): 0,9991 / dSharpe -0,025") su = alive(X.xsec_engine(PUh, phase=0)) mg = A.marginal_vs_tp01(win(su, T2_START, T2_END)) @@ -633,6 +782,28 @@ def main() -> None: print(" null de-levering: NON PERTINENTE (nessun claim di 'meno drawdown').") print(" anchor: girato per costruzione — ogni numero sopra e' su tutte e 10 le fasi.") + # ------------------------------------------------------------ [8] verdetto + print("\n[8] VERDETTO") + print(f" Il TEST DICHIARATO (meccanismo congelato, stesso sottoinsieme, stessa finestra,") + print(f" USD contro USDT) PASSA: corr mediana {R['corr'].median():.4f}, minima " + f"{R['corr'].min():.4f} >= {CORR_SOGLIA}, dSharpe appaiato {R['d'].median():+.3f}.") + print(f" Il titolo di §21 ricalcolato in dollari si muove di {R2['sh_usd'].median()-R2['sh_usdt'].median():+.2f} " + f"({R2['sh_usdt'].median():+.2f} -> {R2['sh_usd'].median():+.2f} di mediana di fase).") + print(f" Nella finestra del depeg la componente idiosincratica sale (med {idm.median():.1f} bps " + f"contro {idio[~dm].abs().stack().median():.1f}) ma resta due ordini di grandezza sotto") + print(f" la vol giornaliera di un alt, e ZERO ribilanciamenti cambiano gambe li' dentro.") + print(f" L'unica corr sotto soglia (TEST 2, fase 2: {R2['corr'].min():.4f}) e' attribuita:") + print(f" col gate di dispersione SPENTO la minima diventa {np.min(cg2):.4f} -> non e' il") + print(f" prezzo, e' la soglia BINARIA che amplifica pochi bps in 10 giorni di flat.") + print( " -> IL RISULTATO XS01-OOS SOPRAVVIVE al controllo di venue.") + print( " Cio' che NON e' stato controllato, e va citato insieme al verdetto: BNB non ha un") + print( " prezzo USD nel 2021-23 (e' il token nativo del venue sotto esame), e toglierlo") + print( " costa -0.75 di mediana di fase. Il test copre 10-12 gambe su 13, non 13.") + print(f" E il caso peggiore di divergenza dell'INTERO campione ({tw.date()}, " + f"{idf.loc[tw].abs().max():.0f} bps su una gamba)") + print( " dimostra che il meccanismo temuto e' reale: semplicemente non e' accaduto nella") + print( " finestra fuori campione. Non e' 'non puo' succedere', e' 'non e' successo li''.") + print(f"\n[fine] {time.time()-t0:.1f}s") From fd37add5186d52c4bf5371002b0872b42376c965 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 21:22:43 +0000 Subject: [PATCH 036/114] =?UTF-8?q?research(wave-0822):=20BIN-FREQ=20?= =?UTF-8?q?=E2=80=94=20direzione=20reale=20(0/72=20sul=20segno=20inverso),?= =?UTF-8?q?=20taglia=20=3D=20selezione;=20e=20due=20premesse=20del=20brief?= =?UTF-8?q?ing=20refutate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 74 ++++++++++++++++++++++++++++- scripts/research/r0822b_bin_freq.py | 66 ++++++++++++++++--------- 2 files changed, 117 insertions(+), 23 deletions(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index d2ec17c..2fa4a62 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -697,7 +697,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 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 | -| 25 | BIN-FREQ | il binario di frequenza su SKH01 e' un candidato o un sottoprodotto? | _in corso_ | +| 25 | BIN-FREQ | il binario di frequenza su SKH01 e' un candidato o un sottoprodotto? | **LEAD** — direzione reale, taglia = selezione | | 26 | TP01-SINISTRO | quanto pesa TP01 se l'obiettivo include un crash che nel campione non c'e'? | **allarme SCARTATO** — il rischio e' XS01, non TP01 | | 27 | SURFACE-RV | la superficie e' internamente incoerente, e l'incoerenza vale piu' del costo di attraversarla? | **SCARTATO** — l'incoerenza al mid **e'** la larghezza del mercato | @@ -1157,3 +1157,75 @@ chiusure**, e i close giornalieri non lo vedrebbero. ⚠️ *Nota di attribuzione: il file risultava `M` in git perche' **io** (coordinatore) l'avevo incluso in un commit mentre l'agente ci stava ancora lavorando — non un altro agente. Un `git add -A` durante un'ondata cattura il lavoro in corso.* + +--- + +## 25 — BIN-FREQ (il binario di frequenza su SKH01, giudicato come candidato) + +`scripts/research/r0822b_bin_freq.py` (~400 s, **23 ancore piene**, due esecuzioni indipendenti con +output **numericamente identici**). **144 celle** dichiarate nel docstring **prima di guardare**, piu' +i **71 trial gia' spesi sullo stesso oggetto** (28 VOL-SIZE + 43 scettico) → **conto onesto = 215**: +*riaprire un parametro riapre la sua famiglia.* + +**Repliche bit-exact** (`max|dif| = 0`) contro `sleeves._skyhook_returns()` **e** contro +`r0702_anchor_skh01.run_asset`; **titolo dello scettico riprodotto esattamente** chiamando la sua +funzione (`BIN L>=med` +0,080 in 20/23, inverso −0,159 in 0/23). + +| | ShFULL | ShHOLD | maxDD | CAGR | +|---|---|---|---|---| +| BASE (libro live 75/25) | 1,626 | 1,037 | 10,4% | 18,6% | +| BIN, peso effettivo lasciato cadere | 1,690 | 1,031 | 10,3% | 17,8% | +| **BIN, controllo ISO-PESO** | **1,589** | 0,956 | 10,0% | 16,8% | + +📌 **La statistica che decide non e' l'argmax, e' la MEDIANA DI FAMIGLIA.** Argmax +(`q0,50 w60 h0,10 s+`) ISO **+0,112 in 23/23**; **stima onesta di famiglia +0,007**, con **39/72 +celle positive = 54% ≈ monetina**. **L'argmax vale 16× la mediana → punta, non plateau.** +**Controllo di segno: 0/72 celle positive** sulla meta' inversa → la *direzione* e' solidissima. + +**Gate:** `marginal_vs_tp01` **ADDS**; selezione **in-sample-only** sceglie la stessa cella mentre +scegliendo **sull'hold-out** ne esce un'altra (−0,017) → **il gate ha potenza**; +**`weights_tilt_null` FAIL** (pctl 36-38) **e per giunta inerte** (il binario *abbassa* il peso +effettivo 0,250→0,19, e nella forma di deploy naturale il vettore proposto e' identico a quello +corrente); causalita' verificata con **troncamento diretto** su 52 decisioni, 0 divergenti; +**trasferisce a SKH01_V1** (geometria d'uscita diversa): +0,087 in 20-21/23. + +❌ **Il deflated-Sharpe NON e' vacuo — refuta l'ipotesi a-priori che l'agente aveva registrato:** +**0,976** (N=144) / **0,977** (N=215, conto onesto) / 0,948 (N=500), baseline 0,828/0,834. E il +**motivo e' riusabile**: il DSR e' vacuo quando i trial si somigliano (var→0 ⇒ sr0→0 ⇒ DSR≈1 *per +chiunque*); una famiglia **binaria** contiene celle che scartano insiemi di trade molto diversi, +quindi **riacquista potenza**. *Non e' il binario a essere piu' solido: e' la famiglia a essere piu' +larga.* ✅ Terza misura convergente della stessa giornata sul difetto del DSR (con BOCPD e CRITICO). + +⚠️ **Il null "con potenza" ha una mediana che NON e' zero, ed e' un fatto di misura:** scartare a caso +il 40% dei trade **danneggia**, perche' lo Sharpe annualizzato va con √(scommesse/anno) — misurato: +gamba 1,107 → **0,883** permutata contro **0,861** previsti da √0,60. Il null resta valido e **piu' +severo** (reale +0,112 vs massimo del null −0,031, p<0,01), **ma risponde a "batte una selezione +casuale?", non a "batte il non filtrare?"**. + +🚨 **Due premesse del MIO briefing misurate e REFUTATE:** +(i) *"il binario dimezza gli ordini → migliora l'eseguibilita' e riduce le fee"* — gli ordini +**nettati** calano del **17%**, non del 50%; i sotto-min-order **non si muovono** perche' sono di +TP01; e l'**haircut a $635 e' 0,001 in entrambe le braccia**: non c'era niente da guadagnare. +(ii) *"converti il risparmio di fee in Sharpe con la curva del 26/07"* — **a fee ZERO l'effetto resta +l'87-110%** → non e' risparmio di costo, e' **informazione sul rendimento lordo**, e convertirlo +sarebbe stata un'attribuzione sbagliata. + +**VERDETTO: LEAD** (non candidato, non scartato). La **direzione** e' reale e sopravvive a tutto; la +**taglia di una singola cella e' selezione**. +📌 **E la domanda che avevo posto — "+0,080 contro ±0,196 di banda d'ancora, merita?" — era il +confronto SBAGLIATO** (la banda del *livello* si cancella in un disegno appaiato), **ma la risposta +corretta e' PEGGIORE per il candidato**. Criterio dichiarato, 4 gambe: (1) mediana di famiglia ≥ meta' +della cella e >65% celle positive → **FAIL (6%, 54%)**; (2) `weights_tilt_null` → **FAIL/vacuo**; +(3) marginale + null con potenza → PASS; (4) taglia ≥ €0,10/giorno per pagare un modo di fallire in +piu' → **FAIL: €0,0113/giorno** ($4,79/anno a $635). +Due prove ausiliarie che chiudono il caso: **due implementazioni difendibili della stessa regola** +(d'accordo sul 97,6% dei trade) danno **+0,066 contro +0,080** — *il 21% dell'effetto sta nel come si +riempie il warm-up*; e **versare €500/mese vale ~1456× l'effetto misurato**. +**Libro, pesi, cron, config INVARIATI.** + +📌 **Riapertura pre-registrata: non una data, una SOGLIA DI CAPITALE** — cio' che fallisce e' il +rapporto fra un costo operativo **fisso** e un beneficio **proporzionale**, e nessun dato in piu' lo +cambia (con SE(Sharpe)≈1,4 su 6 mesi nessun forward misura 0,07). €0,10/giorno richiede **~$5.600** +alla stima della cella e **~$53.000** a quella onesta di famiglia → si rivaluta alla revisione dei +**$20k** gia' in calendario, **e solo se la cella e' giustificata da un MECCANISMO** invece che +dall'argmax di una griglia. diff --git a/scripts/research/r0822b_bin_freq.py b/scripts/research/r0822b_bin_freq.py index a33d18b..9130f92 100644 --- a/scripts/research/r0822b_bin_freq.py +++ b/scripts/research/r0822b_bin_freq.py @@ -56,8 +56,12 @@ Nessuna riscrittura: `extract / equity_daily / leg_daily / sizes_from_L / book / sono IMPORTATE da r0822_vol_size, e il titolo dello scettico e' riprodotto chiamando la SUA funzione. Replica bit-exact contro `sleeves._skyhook_returns()` prima di ogni delta (§0). -USO: nice -n 19 timeout 900 uv run python scripts/research/r0822b_bin_freq.py [--every K] - --every 8 -> 3 ancore (pilota). default: tutte e 23. +USO: nice -n 19 timeout 900 uv run python scripts/research/r0822b_bin_freq.py + [--every K] 1 ancora ogni K delle 23 (K=8 -> 3 ancore, pilota ~80 s) + [--ndraw N] estrazioni del null di permutazione (default 100) + Con i default gira in ~400 s e ~500 MB di RSS su questa VPS (2 core). Tutti i numeri + riportati qui vengono da questa invocazione, e due esecuzioni indipendenti hanno dato + output numericamente IDENTICI. """ from __future__ import annotations @@ -192,9 +196,12 @@ def iso_pair(TP: pd.Series, leg: pd.Series, leg_base: pd.Series): def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--every", type=int, default=1) - ap.add_argument("--ndraw", type=int, default=120) + ap.add_argument("--ndraw", type=int, default=100) args = ap.parse_args() offs = OFFSETS_FULL[:: max(1, args.every)] + if len(offs) < 3: + raise SystemExit(f"--every {args.every} lascia {len(offs)} ancore: " + "`A.anchor_luck_delta` ne vuole almeno 3 (usa --every <= 8).") t0 = time.time() print("=" * L) @@ -772,11 +779,18 @@ def main() -> None: print(f" (RSS {rss_mb():.0f} MB)") f_mod = FEEDEC[(f"MODELLATA {keep_fee*1e4:.0f} bps RT (i backtest)", nmcan)] f_zero = FEEDEC[("fee ZERO (informazione pura)", nmcan)] - print(f" -> a fee ZERO resta {f_zero:+.3f} di {f_mod:+.3f} = " - f"{100*f_zero/f_mod if f_mod else float('nan'):.0f}% dell'effetto: la parte restante") - print(" e' fee risparmiata. La fee vera oggi e' 3,5 bps/lato, meno di quella modellata,") - print(" quindi nel LIVE la componente-fee del vantaggio e' PIU' PICCOLA di quella misurata") - print(" sui backtest — il verso in cui il numero pubblicato e' ottimista per il deploy.") + quota_info = 100.0 * f_zero / f_mod if f_mod else float("nan") + print(f" -> a fee ZERO resta {f_zero:+.3f} di {f_mod:+.3f} = {quota_info:.0f}% dell'effetto.") + if quota_info >= 85.0: + print(" ❌ IPOTESI REFUTATA (era la mia, ed era anche quella del compito): il vantaggio") + print(" NON e' fee risparmiata. A fee zero l'effetto c'e' tutto, quindi scartare i") + print(" trade in alta vol e' INFORMAZIONE sul rendimento lordo, non un risparmio di") + print(" costo. Conseguenza pratica: NON si puo' replicare abbassando il turnover, e") + print(" NON va convertito con la curva fee del 26/07 (sarebbe doppio conteggio in una") + print(" direzione e attribuzione sbagliata nell'altra).") + else: + print(" Una parte e' fee risparmiata; la fee vera (3,5 bps/lato) e' piu' bassa di quella") + print(" modellata, quindi nel LIVE quella componente e' PIU' PICCOLA.") # ---------------------------------------------------------- §10 trasferimento su V1 print("\n" + "-" * L) @@ -863,21 +877,29 @@ def main() -> None: f"= ${(max(cag_band)-min(cag_band))*CAPITAL:.0f}/anno di incertezza") print(f" versare EUR 500/mese (il piano dell'operatore) = EUR 16,44/giorno di capitale") print(f" nuovo, cioe' ~{16.44/max(1e-9, abs(dC*CAPITAL/365*0.86)):.0f}x l'effetto misurato qui.") - print(f" (v) IL CRITERIO APPLICATO, riga per riga:") - med_pos = float(np.median([r['dF'] for r in POS])) - c1 = med_pos > 0 - c3 = True - print(f" (1) stima ONESTA di famiglia (mediana segno +1) = {med_pos:+.3f} " - f"contro argmax {argmax['dF']:+.3f} e cella pubblicata {RES[nmcan]['dF']:+.3f}" - f" -> {'PASS' if c1 else 'FAIL'}") + print(f" (v) IL CRITERIO APPLICATO, riga per riga. La forma quantitativa di (1) e'") + print(f" dichiarata qui: 'sopravvive alla stima di famiglia' = la MEDIANA della meta'") + print(f" d'ipotesi vale almeno la META' della cella selezionata (altrimenti non e' un") + print(f" plateau, e' una punta), E la quota di celle positive e' chiaramente sopra il") + print(f" 50% (altrimenti la famiglia e' una monetina). Soglie scelte per essere") + print(f" verificabili, non per far passare o bocciare un numero gia' visto.") + med_pos = float(np.median([r["dF"] for r in POS])) + fpos = sum(r["dF"] > 0 for r in POS) / len(POS) + c1 = (med_pos >= 0.5 * CHOSEN["dF"]) and (fpos >= 0.65) + eurday = dC * CAPITAL / 365 * 0.86 + c4 = abs(eurday) >= 0.10 + print(f" (1) mediana di famiglia (segno +1) {med_pos:+.3f} = " + f"{100*med_pos/CHOSEN['dF'] if CHOSEN['dF'] else float('nan'):.0f}% della cella scelta " + f"({CHOSEN['dF']:+.3f}); celle positive {fpos*100:.0f}% -> {'PASS' if c1 else 'FAIL'}") print(f" (2) `weights_tilt_null`: gate_pass=False nella forma nuda, INERTE nella forma") - print(f" compensata -> il gate non lo sostiene in nessuna delle due -> FAIL/vacuo") - print(f" (3) marginale ADDS + null di permutazione con potenza (§6) -> vedi sopra") - print(f" (4) taglia contro il modo di fallire in piu': " - f"EUR {dC*CAPITAL/365*0.86:+.4f}/giorno a ${CAPITAL:.0f} -> FAIL") - print(f" Di cui, per giunta, solo il {100*f_zero/f_mod if f_mod else float('nan'):.0f}% " - f"e' informazione e il resto e' fee risparmiata (§9b),") - print(f" e la fee vera e' piu' bassa di quella modellata.") + print(f" compensata -> non lo sostiene in nessuna delle due -> FAIL/vacuo") + print(f" (3) marginale ADDS su entrambe le celle + null di permutazione battuto a") + print(f" p<0.01 con la penale meccanica dentro (§6) -> PASS") + print(f" (4) taglia contro un modo di fallire in piu': EUR {eurday:+.4f}/giorno a " + f"${CAPITAL:.0f} -> {'PASS' if c4 else 'FAIL'} (soglia dichiarata: EUR 0,10/giorno)") + print(f" VERDETTO: {'CANDIDATO' if (c1 and c4) else 'LEAD / NESSUN CAMBIO'} — servono") + print(f" tutte e quattro; (1) e (4) falliscono, (2) non e' utilizzabile. Cio' che") + print(f" REGGE e va conservato e' il SEGNO: 0/72 celle positive sul controllo inverso.") print("\n" + "=" * L) print(f" fatto in {time.time()-t0:.0f}s") From e460caf667034d1e1300138706570b19cf445a0f Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 21:32:25 +0000 Subject: [PATCH 037/114] =?UTF-8?q?research(wave-0822):=20PROP-RECAL=20?= =?UTF-8?q?=E2=80=94=20il=2042%=20del=20numero=20funded=20stava=20in=206?= =?UTF-8?q?=20gambe=20che=20fuori=20campione=20non=20esistono;=20P(50/g)?= =?UTF-8?q?=2042%=20->=207,8%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 85 +++++++++ scripts/research/r0822b_prop_recal.py | 260 +++++++++++++++++++++++++- 2 files changed, 342 insertions(+), 3 deletions(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 2fa4a62..2aab3f3 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -694,6 +694,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st |---|---|---|---| | 21 | XS01-OOS | il vantaggio del PROP-ALLOC poggia sul drift di XS01 misurato sulla sua finestra di scoperta: regge fuori? | **LEAD** — regge, ed e' PIU' GRANDE fuori | | 28 | DEPEG | il falsificatore che l'autore di 21 ha nominato contro se stesso: il depeg USDT contamina il ranking? | **SOPRAVVIVE** — il venue sposta 0,01 di Sharpe | +| 29 | PROP-RECAL | J rifatto con XS01 dalla storia lunga: il canale funded si apre? | **LEAD ridimensionato ~5x** — 42% → **7,8%** | | 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 | @@ -1229,3 +1230,87 @@ cambia (con SE(Sharpe)≈1,4 su 6 mesi nessun forward misura 0,07). €0,10/gior alla stima della cella e **~$53.000** a quella onesta di famiglia → si rivaluta alla revisione dei **$20k** gia' in calendario, **e solo se la cella e' giustificata da un MECCANISMO** invece che dall'argmax di una griglia. + +--- + +## 29 — PROP-RECAL (J rifatto con XS01 dalla storia lunga) — **la decisione** + +`scripts/research/r0822b_prop_recal.py` (891 righe, 309 s, nessuna rete). **5.212 valutazioni MC** +contate al rialzo; risoluzione dichiarata **±0,010 sui livelli di J, ±0,007 sulle differenze +appaiate**. ✅ **Riproduzione ESATTA cifra per cifra** dei numeri di PROP-ALLOC (J 0,738/0,721/0,335; +P(pass) 82,2/77,3/38,6%; scala 39,28 €/g e 41,7%) piu' **tre controlli bit-exact a 0,0**. + +📌 **IL RISULTATO: J non regge alla taglia pubblicata, e il colpevole NON e' la finestra.** +Decomposizione a **un grado di liberta' per volta**, stesso pannello: + +| passo | J | Δ | +|---|---|---| +| XS01 = HL 19 gambe, finestra di scoperta (**il pubblicato**) | 0,739 | — | +| → Binance 19 gambe, **date allineate** (VENUE) | 0,732 | **−0,006** | +| → Binance **13 gambe** (UNIVERSO) | 0,426 | **−0,307** | +| → pannello lungo (FASE) | 0,459 | +0,034 | +| → **fuori campione 2021-2023** (FINESTRA) | 0,520 | **+0,061** | + +🚨 **Il 42% del numero pubblicato sta nelle 6 gambe che fuori campione non esistono** (ARB OP SUI APT +SEI TIA) — **e quelle 6 furono messe nell'universo nel 2026-06 guardando la liquidita' HL del 2024+, +cioe' dentro la finestra di scoperta.** La finestra fuori campione, da sola, **migliora**. +✅ Allargare l'universo man mano che le gambe nascono (10→19 espandente) da' **J identico a U13**: +i 0,307 **non si recuperano**. + +⚠️ **Errore di metodo catturato prima di pubblicare:** la prima stesura dava al passo venue **−0,254** +e avrebbe pubblicato *"Binance costa un quarto di J"*. Era **FASE, non venue** — `xsec_engine` +ribilancia sulla parita' dell'indice di pannello, e due pannelli che iniziano a date diverse +ribilanciano in **giorni di calendario diversi**. Allineati i pannelli, il venue vale **−0,006** = +✅ **replica indipendente della validazione di XS01-OOS, sulla funzione obiettivo invece che sullo +Sharpe.** + +**J onesto** (mediana delle 10 fasi, peso 38/38/25): + +| finestra | mediana J | banda p10-p90 | LIVE 75/25 | **delta appaiato** | +|---|---|---|---|---| +| **W-LONG 2021+** | **0,453** | [0,431 – 0,488] | 0,281 | **+0,175 (10/10 fasi)** | +| W-OOS 2021-23 | 0,504 | [0,455 – 0,608] | 0,364 | +0,146 (10/10) | +| W-DISC 2024+ | 0,442 | [0,404 – 0,464] | 0,331 | +0,112 (9/10) | + +XS01 standalone fuori campione: Sharpe 1,04, drift 22,7%, **maxDD −19,2%** contro −11,2% dello sleeve +ufficiale → **il rendimento regge, la coda no, e la barriera guarda la coda.** + +📌 **Il peso 0,50 non sopravvive; la REGIONE robusta si': w_XS ∈ [25%, 38%] e' entro il 3% +dell'ottimo su TUTTE E TRE le finestre**, discovery inclusa. A 50% J scende a 0,438/0,477/0,408 e il +maxDD di libro risale a −12,6%/−11,6%/−9,1% (contro −9,6%/−9,6%/−7,3% a 25%) = **replica +indipendente del punto (ii) di XS01-OOS**. + +🚨 **IL NUMERO OPERATIVO, con banda di fase: P(≥50 €/g) da €600 in 36 mesi = 7,8% [7,2–9,1%], con +P(zero) 25,5%** — contro il **41,7%** pubblicato stamattina. **6,6%** se la firm non lista gli alt; +**4,3%** per il libro live. ⚠️ J e P(≥50/g) hanno **argmax diversi**: si cita il centro della regione +robusta, non l'argmax di finestra. + +**Stress ora ancorati al MISURATO** (delta appaiato argmax−LIVE): drift pieno +0,193 · SKH ×0,50 ++0,124 · SKH ×0,33 +0,113 · **`XS ×0,00` +0,041** · SKH ×0,33 + XS ×0,00 **−0,024** (peggio del libro +live). Lo scenario severo **cambia stato**: non e' piu' *"non sappiamo se il drift esiste"* ma un +contro-fattuale a rendimento nullo, e dice che **XS01 come pura decorrelazione vale +0,04** — tutto il +resto e' il suo drift, che ora e' misurato per **13 gambe su 19**. + +**Gate:** lente **accoppiata obbligatoria** ovunque (close-only girata solo come controllo: **regala ++0,020 di J**); **banda di fase di XS01 girata — prima volta sotto la lente prop** (fortuna della fase +canonica solo +0,019, delta positivo **10/10**); null del de-levering **strutturale** (ogni +configurazione valutata a 8 leve). ⚠️ **Banda d'ancora di SKH01 (23 offset) NON girata**, surrogata +col taglio del drift — **e' la distorsione dichiarata due volte e mai rimossa**, ed e' diventata la +gamba (b) del gate. + +**VERDETTO: LEAD — ridimensionato di un fattore ~5 sul numero operativo, non falsificato.** L'edge +sopravvive alla storia lunga (+0,175 di J, **10/10 fasi**, col 2022 dentro); crollano il **peso 50%** +e il contributo delle **6 gambe non testabili**, non la finestra. + +📌 **`GATE PROP-01` — sostituisce l'attesa di anni, che e' CHIUSA:** +**(a) LISTINO — verifica, €0, BLOCCANTE prima di ogni spesa:** ≥10 delle 13 gambe negoziabili **con +short**; sotto 10 il meccanismo congelato **non ribilancia affatto**. FAIL → si decide sulla riga +senza XS01. *E' la stessa classe dell'errore GTAA01/PRIIPs.* +**(b) BANDA D'ANCORA DI SKH01 SOTTO LA LENTE PROP — misura, data 2026-10-31:** mediana delle +differenze appaiate sui 23 offset, **PASS se > +0,05** (l'agente dichiara **prima** di aspettarsi un +PASS; il surrogato dice +0,11/+0,12). +**(c) CAPITALE:** un'eval HYRO $100k costa **$579 su un conto di $635** — la prima eval consuma il +**91%** del conto. + +**`IL CANALE FUNDED: SI APRE A CONDIZIONE DI leggere il listino della firm` — e di citarlo come +P(≥50 €/g) 7,8% [7,2–9,1%] con P(zero) 25,5%, non 42%.** diff --git a/scripts/research/r0822b_prop_recal.py b/scripts/research/r0822b_prop_recal.py index e39acd8..dcef4bd 100644 --- a/scripts/research/r0822b_prop_recal.py +++ b/scripts/research/r0822b_prop_recal.py @@ -74,6 +74,32 @@ CONVENZIONI DICHIARATE ha una risoluzione e va detta**: SE su un livello di J ~ +-0,010, sulle differenze APPAIATE (indici di bootstrap condivisi) molto meno, ~+-0,007. +ESITO (2026-08-22, corsa completa 309 s, 5.212 celle dichiarate) +---------------------------------------------------------------- +* RIPRODUZIONE **ESATTA** dei numeri pubblicati: J 0,738 / 0,721 / 0,335, P(pass) 82,2 / 77,3 / + 38,6%, delta appaiati +0,0164 e +0,3996, scala di conti 39,28 EUR/g e P(>=50/g) 41,7%. Tre + controlli bit-exact in piu' (il mio costruttore di tuple = quello del 25/07; il mio costruttore + di libro = `PA.book_RG`, anche sotto stress): **max|diff| = 0,0** su tutti. +* **J NON regge alla taglia pubblicata, e il colpevole NON e' la finestra.** Decomposizione a un + grado di liberta' per volta, tutta sullo stesso pannello: venue (HL -> Binance, 19 gambe, + date allineate) **-0,006**; universo (19 -> 13 gambe) **-0,307**; fase **+0,034**; finestra + (2024+ -> 2021-2023) **+0,061**. Il 42% del numero pubblicato sta nelle **6 gambe che non + esistono fuori campione** (ARB OP SUI APT SEI TIA), non nel periodo. +* Numeri onesti sulla parte testabile (13 gambe), mediana delle 10 fasi di XS01, peso 38/38/25: + J **0,453** su W-LONG 2021+ (LIVE 0,281; delta appaiato **+0,175**, positivo 10/10 fasi), + **0,504** su W-OOS 2021-23, **0,442** su W-DISC. Cioe' ~0,45-0,50 invece di 0,738. +* **La previsione registrata prima e' confermata sui pesi e sbagliata (ottimista) sul livello:** + regione robusta **w_XS = 25-38%** su TUTTE e tre le finestre (prevista [0,25 ; 0,38]) — quindi + il **50% dell'argmax di PROP-ALLOC non sopravvive**; J 0,45-0,52 contro [0,50 ; 0,70] previsto; + P(>=50/g) 7,8% contro [10% ; 35%] previsto. +* **Il numero operativo: P(>=50 EUR/g) da EUR 600 in 36 mesi = 7,8% [banda di fase 7,2-9,1%], + P(zero) 25,5%** — contro il 41,7% pubblicato, e contro **6,6%** se la firm non lista gli alt. + Il libro live sulla stessa lente fa 4,3%. +* Lo scenario severo di PROP-ALLOC (`XS x0,00`) **cambia stato**: non e' piu' "non sappiamo se il + drift esiste" ma un contro-fattuale a rendimento nullo, e li' il delta scende a **+0,041** + (col taglio congiunto SKH x0,33 diventa **-0,024**, cioe' peggio del libro live). Tutto il + vantaggio vive sul drift di XS01 — che ora e' misurato, per 13 gambe su 19. + Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0822b_prop_recal.py` Rete: NESSUNA (tutto da disco: parquet certificati + cache Binance di r0822b_xs01_oos). """ @@ -190,7 +216,7 @@ def bin_loader(sym: str) -> pd.DataFrame: """Legge dalla cache di r0822b (nessuna rete) e SCARTA la barra del giorno in corso: una barra giornaliera parziale in coda e' esattamente il difetto trovato oggi nei monitor forward.""" d = X.fetch_binance_daily(sym) - today = pd.Timestamp.utcnow().normalize().value // 10**6 + today = pd.Timestamp.now("UTC").normalize().value // 10**6 return d[d["timestamp"] < today].reset_index(drop=True) @@ -466,6 +492,83 @@ def main() -> None: f" (PROP-ALLOC pubblicava {PUB['dJ_vs_live']:+.4f} su W-DISC con XS01 HL)") tick("§3 griglie") + # ================================================== §3-bis. DECOMPOSIZIONE + hr("§3-bis. DA DOVE VIENE IL SALTO 0.738 -> ~0.47 — un grado di liberta' alla volta") + print(" Fra il numero pubblicato e quello di §3 cambiano TRE cose insieme (venue, universo,") + print(" finestra). Sommarle e chiamarle 'l'effetto della storia lunga' sarebbe l'errore che") + print(" il progetto ha gia' pagato due volte. Qui si muove un grado di liberta' per volta,") + print(" sempre sul MIO pannello a 3 gambe (cosi' anche il pannello smette di essere una") + print(" variabile nascosta).") + ev_d, fu_d = res[WINDOWS[2][0]]["ev"], res[WINDOWS[2][0]]["fu"] + ev_o, fu_o = res[WINDOWS[1][0]]["ev"], res[WINDOWS[1][0]]["fu"] + ev_l, fu_l = res[WINDOWS[0][0]]["ev"], res[WINDOWS[0][0]]["fu"] + # ⚠ ERRORE CATTURATO IN SESSIONE, e vale piu' del numero che correggeva: `xsec_engine` + # ribilancia quando `i % H == phase`, dove `i` e' l'indice NEL PANNELLO. Due pannelli che + # cominciano a date diverse ribilanciano quindi in GIORNI DI CALENDARIO diversi: confrontarli + # misura la FASE, non il venue. Alla prima stesura il passo "venue" dava -0.254 di J (e + # avrei pubblicato "Binance costa un quarto di J") mentre XS01-OOS aveva misurato dSharpe + # -0.025 sullo stesso confronto fatto bene. Qui il pannello Binance e' RIALLINEATO alle + # stesse date di quello HL, cosi' la parita' dell'indice — e con essa la fase — coincide. + hl_idx = ref.index + C19 = Cb[list(XS_UNIVERSE)].reindex(hl_idx).dropna() + xs19_al = xs_tuples(C19, Ob.reindex(C19.index)[C19.columns], + Hb.reindex(C19.index)[C19.columns], + Lb.reindex(C19.index)[C19.columns], phase=0) + C13 = Cb[X.U13].reindex(C19.index).dropna() + xs13_al = xs_tuples(C13, Ob.reindex(C13.index)[X.U13], Hb.reindex(C13.index)[X.U13], + Lb.reindex(C13.index)[X.U13], phase=0) + bump(2) + steps = [("XS01 = HL 19 gambe W-DISC (= il pubblicato)", I_DISC, ref, ev_d, fu_d), + ("XS01 = BINANCE 19 g. W-DISC allineato (VENUE)", I_DISC, xs19_al, ev_d, fu_d), + ("XS01 = BINANCE 13 g. W-DISC allineato (UNIVERSO)", I_DISC, xs13_al, ev_d, fu_d), + ("XS01 = BINANCE 13 g. W-DISC pannello lungo (FASE)", I_DISC, xs13, ev_d, fu_d), + ("XS01 = BINANCE 13 g. W-OOS (FINESTRA)", I_OOS, xs13, ev_o, fu_o)] + print(f"\n {'passo':>50} {'argmax':>10} {'leva':>6} {'J':>7} {'dJ dal passo prima':>20}") + prev = None + dec = {} + for lab2, ii, xf, ev, fu in steps: + Gs = scan(grid, ii, xf, ev, fu) + b2 = best_row(Gs) + dec[lab2] = b2 + dj = "" if prev is None else f"{float(b2['J'])-prev:>+20.3f}" + print(f" {lab2:>50} {b2['w']:>10} {b2['lev']:>6.3f} {b2['J']:>7.3f} {dj}") + prev = float(b2["J"]) + print("\n Le righe sono ordinate dal piu' al meno controllabile. Quella che il progetto deve") + print(" guardare e' la piu' grande: se e' la FASE, il numero pubblicato non e' un numero") + print(" di strategia ma di calendario, ed e' il quarto modo d'ingannarsi dell'elenco del") + print(" brief. La finestra fuori campione, da sola, non peggiora nulla — conferma su J cio'") + print(" che XS01-OOS ha misurato sul rendimento.") + + print("\n E l'altro capo della banda: universo ESPANDENTE (10 -> 19 gambe man mano che") + print(" vengono quotate), che e' cio' che un implementatore in tempo reale avrebbe avuto:") + print(f" {'finestra':>34} {'argmax':>10} {'leva':>6} {'J':>7} {'maxDD':>7} " + f"{'J con U13':>10}") + for lab2, ii, ev, fu in (("W-OOS 2021-2023", I_OOS, ev_o, fu_o), + ("W-LONG 2021+", I_LONG, ev_l, fu_l)): + Gs = scan(grid, ii, xsEXP, ev, fu) + b2 = best_row(Gs) + j13 = float(best_row(res[[w for w in res if w.startswith(lab2.split()[0])][0]]["G"])["J"]) + print(f" {lab2:>34} {b2['w']:>10} {b2['lev']:>6.3f} {b2['J']:>7.3f} " + f"{b2['maxdd']*100:>6.1f}% {j13:>10.3f}") + + hr("§3-ter. CONTROLLO DI LENTE — quanto sarebbe cieca la close-only su QUESTI numeri") + print(" Su una regola di DD a UN GIORNO la close-only non scatta mai. Non e' un'opinione:") + print(" si vede rigirando le stesse celle con gap = 0.") + bL0 = best_row(res[WINDOWS[0][0]]["G"]) + Rc, Gc = rg(wdict(bL0["w"]), I_LONG, xs13) + o_cp = obj_rg(Rc, Gc, float(bL0["lev"]), ev_l, fu_l) + Rd_ = PA.deluck(Rc) + e_cl = PA.eval_sim(Rd_, Gc, ev_l, float(bL0["lev"]), HYRO, lens="close") + f_cl = PA.funded_sim(Rd_, Gc, fu_l, float(bL0["lev"]), HYRO, 100_000.0, lens="close") + bump(2) + print(f" W-LONG argmax {bL0['w']} @{bL0['lev']:.2f}x :" + f" ACCOPPIATA J {o_cp['J']:.3f} (P(pass) {o_cp['p_pass']:.1%}, P(vivo) {o_cp['p_alive']:.1%})") + print(f" {'':>44} CLOSE-ONLY J {e_cl['p_pass']*f_cl['p_alive']:.3f} " + f"(P(pass) {e_cl['p_pass']:.1%}, P(vivo) {f_cl['p_alive']:.1%})") + print(f" -> la close-only regala {e_cl['p_pass']*f_cl['p_alive']-o_cp['J']:+.3f} di J. Ogni") + print(" numero di questo file e' ACCOPPIATO; la close-only e' qui solo come controllo.") + tick("§3-bis/ter decomposizione") + # ================================================== §4. LA REGIONE, NON IL PUNTO hr("§4. IL PESO DI XS01 — c'e' una REGIONE robusta su ENTRAMBE le finestre?") print(" PROP-ALLOC ha gia' misurato che il PUNTO non conta (il massimo di 200 pesi casuali") @@ -502,6 +605,60 @@ def main() -> None: f"{[wx for wx in xs_levels if all(wx in prof_by_w[l] and prof_by_w[l][wx] >= 0.95*max(prof_by_w[l].values()) for l, _ in WINDOWS)]})") tick("§4 regione") + # ================================================== §4-bis. BANDA DI FASE + hr("§4-bis. LA BANDA DI FASE DI XS01 (H=10) SOTTO LA LENTE PROP — mai girata prima") + print(" XS01 ribilancia ogni 10 giorni: le fasi possibili sono 10 e il progetto misura che la") + print(" fase canonica e' fortunata (audit 02/07: DD al 15° pctl, FULL all'85°). PROP-ALLOC") + print(" gira sulla SOLA fase 0. Qui la si gira tutta, al peso della REGIONE ROBUSTA di §4.") + print(" La stima onesta e' la MEDIANA della banda; il confronto col LIVE e' la MEDIANA delle") + print(" DIFFERENZE APPAIATE (mai la differenza delle mediane).") + W_ROB = "38/38/25" + print(f"\n peso fisso {W_ROB} (centro della regione robusta), leva scelta per fase") + print(f" {'finestra':>28} {'J fase 0':>9} {'MEDIANA':>9} {'banda p10-p90':>16} " + f"{'fasi>LIVE':>10} {'delta appaiato mediano':>24}") + phase_med = {} + for lab, ii in WINDOWS: + ev, fu = res[lab]["ev"], res[lab]["fu"] + RL, GL = rg({"TP01": 0.75, "SKH01": 0.25}, ii, xs13) + jL, lvL = -1.0, None + vL = None + for lv in LEVS: + o = obj_rg(RL, GL, lv, ev, fu) + if o["J"] > jL: + jL, lvL, vL = o["J"], lv, o["jvec"] + js, deltas, bestph = [], [], {} + for ph in range(10): + xsp = xs_tuples(p13, o13, h13, l13, phase=ph) + Rp, Gp = rg(wdict(W_ROB), ii, xsp) + jb, vb = -1.0, None + for lv in LEVS: + o = obj_rg(Rp, Gp, lv, ev, fu) + if o["J"] > jb: + jb, vb = o["J"], o["jvec"] + js.append(jb) + deltas.append(float((vb - vL).mean())) + bestph[ph] = jb + bump(10 * len(LEVS) + len(LEVS)) + med = float(np.median(js)) + phase_med[lab] = dict(median=med, band=(float(np.percentile(js, 10)), + float(np.percentile(js, 90))), + ph0=js[0], jlive=jL, dmed=float(np.median(deltas)), + npos=int(sum(1 for d in deltas if d > 0)), + argmax_phase=int(np.argmax(js))) + print(f" {lab:>28} {js[0]:>9.3f} {med:>9.3f} " + f"[{np.percentile(js, 10):>6.3f},{np.percentile(js, 90):>6.3f}] " + f"{sum(1 for j2 in js if j2 > jL):>7}/10 " + f"{np.median(deltas):>+18.3f} ({sum(1 for d in deltas if d > 0)}/10 >0)") + print(f"\n J del libro LIVE 75/25 (non dipende dalla fase di XS01): " + f"{phase_med[WINDOWS[0][0]]['jlive']:.3f} W-LONG, " + f"{phase_med[WINDOWS[1][0]]['jlive']:.3f} W-OOS, " + f"{phase_med[WINDOWS[2][0]]['jlive']:.3f} W-DISC") + print(" ⚠ Questa e' la banda della fase di XS01 SOLTANTO. La banda d'ancora di SKH01 (23") + print(" offset, quella che il progetto misura come la piu' grande da restituire) resta") + print(" NON GIRATA sotto questa lente — costo del recon orario. §5 la surroga con un") + print(" taglio del drift, che e' una stima, non una misura.") + tick("§4-bis banda di fase") + # ================================================== §5. LA BANDA ANCORATA AL MISURATO hr("§5. LA BANDA — con gli stress ANCORATI a cio' che e' misurato, non a cio' che era assunto") print(" PROP-ALLOC pubblicava 42% / 7,7% / 0,7% dove il caso severo era 'XS x0,00' = il drift") @@ -511,10 +668,13 @@ def main() -> None: print(" non sotto questa lente) e (b) il LISTINO della firm (§6). Gli stress qui sotto sono") print(" quelli, non un pessimismo arbitrario.") st_rows = [] - STRESS = ((), (("SKH01", 0.50),), (("SKH01", 0.33),)) + STRESS = ((), (("SKH01", 0.50),), (("SKH01", 0.33),), + (("XS01", 0.00),), (("SKH01", 0.33), ("XS01", 0.00))) SLAB = {(): "nessuno (drift pieno)", (("SKH01", 0.50),): "SKH x0.50 (ancora 26/07)", - (("SKH01", 0.33),): "SKH x0.33 (ancora severa)"} + (("SKH01", 0.33),): "SKH x0.33 (ancora severa)", + (("XS01", 0.00),): "XS x0.00 [ORA SUPERATO]", + (("SKH01", 0.33), ("XS01", 0.00)): "SKH x0.33 + XS x0.00 [vecchio severo]"} for lab, ii in (WINDOWS[0], WINDOWS[1]): Gw, ev, fu = res[lab]["G"], res[lab]["ev"], res[lab]["fu"] top_keys = list(Gw.sort_values("J", ascending=False).drop_duplicates("w")["w"].head(10)) @@ -546,6 +706,11 @@ def main() -> None: f"{oL['J']:>11.3f} {d_.mean():>+11.4f} " f"+-{d_.std(ddof=1)/np.sqrt(len(d_)):.4f}") st_rows.append((lab, SLAB[dmt], best_k, best_l, best_j, oL["J"])) + print(" Le ultime due righe sono gli scenari di PROP-ALLOC. `XS x0.00` significava") + print(" 'il drift di XS01 potrebbe essere zero, non lo sappiamo': dopo XS01-OOS e") + print(" questo file NON e' piu' uno stress plausibile ma un contro-fattuale, perche'") + print(" quel drift e' MISURATO fuori campione ed e' positivo. Resta utile come") + print(" pavimento: dice quanto vale XS01 come pura DECORRELAZIONE, a rendimento nullo.") tick("§5 banda") # ================================================== §6. XS01 ASSENTE @@ -612,6 +777,43 @@ def main() -> None: bump(12) tick("§7 scala di conti") + hr("§7-bis. LA BANDA DI FASE DEL NUMERO OPERATIVO — P(>=50/g) alle 10 fasi di XS01") + print(" Il numero che l'operatore legge non puo' stare su una fase sola. Peso fisso") + print(f" {W_ROB} (regione robusta), finestra W-LONG, leva per fase = quella che massimizza J.") + ev_l2, fu_l2 = res[WINDOWS[0][0]]["ev"], res[WINDOWS[0][0]]["fu"] + pt, med_eur, zeros = [], [], [] + print(f"\n {'fase':>5} {'leva':>6} {'J':>7} {'EUR/g med':>10} {'P(>=10/g)':>10} " + f"{'P(>=50/g)':>10} {'P(zero)':>8}") + for ph in range(10): + xsp = xs_tuples(p13, o13, h13, l13, phase=ph) + Rp, Gp = rg(wdict(W_ROB), I_LONG, xsp) + jb, lvb = -1.0, None + for lv in LEVS: + o = obj_rg(Rp, Gp, lv, ev_l2, fu_l2) + if o["J"] > jb: + jb, lvb = o["J"], lv + cfg = tuple(wdict(W_ROB).get(n, 0.0) for n in W3) + key = (cfg[0], cfg[1], cfg[2], "shared") + saved = pc._TUP_CACHE.pop(key, None) + pc._TUP_CACHE[key] = (Rp, Gp, I_LONG) + r = pc.simulate([cfg], [0] * pl.MAX_CONCURRENT, n_paths=1500, lev=lvb, + 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"]); med_eur.append(r["med"]); zeros.append(r["p_zero"]) + print(f" {ph:>5} {lvb:>6.3f} {jb:>7.3f} {r['med']:>10.2f} {r['p_ge10']:>10.1%} " + f"{r['p_target']:>10.1%} {r['p_zero']:>8.1%}") + bump(10 * len(LEVS)) + print(f"\n MEDIANA su 10 fasi: EUR/g {np.median(med_eur):.2f} | " + f"P(>=50/g) {np.median(pt):.1%} [p10 {np.percentile(pt,10):.1%}, " + f"p90 {np.percentile(pt,90):.1%}] | P(zero) {np.median(zeros):.1%}") + print(f" Contro il PUBBLICATO di PROP-ALLOC: P(>=50/g) 41.7%, P(zero) 13.9% (fase 0, XS01 a") + print(f" 19 gambe, finestra di scoperta).") + ladder_band = (float(np.median(pt)), float(np.percentile(pt, 10)), float(np.percentile(pt, 90)), + float(np.median(zeros))) + tick("§7-bis banda di fase della scala") + # ================================================== §8. SINTESI + GATE hr("§8. SINTESI — la banda da citare, e il gate da RI-REGISTRARE") bL = best_row(res[WINDOWS[0][0]]["G"]) @@ -623,11 +825,63 @@ def main() -> None: print(f" W-OOS 2021-2023 argmax {bO['w']:>10} @{bO['lev']:.2f}x J {bO['J']:.3f}") print(f" W-LONG 2021+ argmax {bL['w']:>10} @{bL['lev']:.2f}x J {bL['J']:.3f}") print(f" (PROP-ALLOC pubblicato, XS01 HL 19 gambe, W-DISC: 25/25/50 @0.75x J 0.738)") + print(f"\n Gli stessi J, ma alla MEDIANA delle 10 fasi di XS01 (peso fisso 38/38/25):") + for lab, _ in WINDOWS: + pm = phase_med[lab] + print(f" {lab:>28} fase 0 {pm['ph0']:.3f} -> MEDIANA {pm['median']:.3f} " + f"[{pm['band'][0]:.3f},{pm['band'][1]:.3f}] LIVE {pm['jlive']:.3f} " + f"delta mediano {pm['dmed']:+.3f} ({pm['npos']}/10 >0)") print(f"\n P(>=50 EUR/g) in 36 mesi, banda onesta:") for short in ("W-LONG", "W-OOS", "W-DISC"): row = " | ".join(f"{k}: {lad[(short, k)]['p_target']:.1%}" for k in ("argmax", "2550", "noxs", "live") if (short, k) in lad) print(f" {short:>8} {row}") + print(f"\n P(>=50 EUR/g) — IL NUMERO DA CITARE (W-LONG, peso {W_ROB}, mediana di 10 fasi):") + print(f" {ladder_band[0]:.1%} [banda di fase {ladder_band[1]:.1%} - {ladder_band[2]:.1%}]" + f" P(zero) {ladder_band[3]:.1%}") + print(f" contro il 41.7% pubblicato (fase 0, XS01 a 19 gambe, finestra di scoperta)") + print(f" e contro il {lad[('W-LONG', 'noxs')]['p_target']:.1%} se la firm non lista gli alt.") + + hr("§8-bis. IL GATE DA RI-REGISTRARE — `GATE PROP-01`") + print(""" SOSTITUISCE il gate del 22/08 ("non aprire un funded prima che XS01 abbia una + finestra fuori dal 2024-2026"), che e' CHIUSO: la finestra esiste, e' stata misurata, e + NON e' lei il problema (+0.061 di J, cioe' migliora). Tre gambe, in ordine di costo + crescente, perche' la prima puo' chiudere la questione da sola. + + (a) LISTINO — VERIFICA, non misura. Costo EUR 0. BLOCCANTE, prima di qualunque spesa, + nessuna data perche' precede tutto. + Leggere il listino della firm e contare quante delle 13 gambe U13 (BTC ETH BNB XRP + LTC ADA LINK DOGE SOL AVAX NEAR AAVE INJ) sono negoziabili CON SHORT ABILITATO. + PASS se >= 10 (il meccanismo congelato ha `min_assets = 10`: sotto, non ribilancia). + FAIL -> il canale non si chiude, ma si valuta sulla riga SENZA XS01: J 0.38-0.40 + invece di 0.45-0.52, P(>=50/g) ~6.6% invece di ~10%, e soprattutto la + raccomandazione "riallocare per la barriera" DECADE (senza XS01 l'argmax e' 38/62 o + 50/50, cioe' un tilt su SKH01, lo sleeve che il progetto misura come il piu' fragile). + E' la stessa classe dell'errore GTAA01/PRIIPs: 5 settimane di misure su un'assunzione + mai verificata sul conto reale, e il controllo costo' un ordine di prova. + + (b) BANDA D'ANCORA DI SKH01 SOTTO LA LENTE PROP — MISURA. Data: 2026-10-31. + E' l'unica distorsione dichiarata due volte (PROP-ALLOC e questo file) e mai rimossa. + 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; banda di FASE di XS01, che ho potuto girare: +0.175, positiva in 10/10. + Mi aspetto un PASS e lo dichiaro prima. FAIL <= +0.05 -> il vantaggio era calendario. + + (c) CAPITALE DEL BIGLIETTO — condizione, non data. Un'eval HYRO $100k costa $579 su un + conto di $635. E' gia' dentro la simulazione della scala (e' gran parte del P(zero) + del 26-34%), ma va detto: la prima eval consuma il 91% del conto vivo. + + COSA FA FALLIRE L'INTERO RISULTATO + 1. il listino non ha 10 gambe shortabili -> (a); + 2. la banda d'ancora di SKH01 azzera il delta -> (b); + 3. si continua a citare 0.738. E' un numero a 19 GAMBE, e le 6 che lo separano dalle + 13 testabili valgono -0.307 di J MISURATI sulla stessa finestra e sullo stesso + venue = il 42% del totale. Quelle 6 (ARB OP SUI APT SEI TIA) non hanno storia + fuori campione E sono state incluse nell'universo nel 2026-06 guardando la + liquidita' HL del 2024+, cioe' dentro la finestra di scoperta.""") + print(f"\n TRIAL/CELLE DICHIARATI (al rialzo): {TRIALS}") print(f"\n fatto in {time.time()-t0:.0f}s") print("=" * 104) From 89af0554d59672d8b309b93a07ea079a76855fbb Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 21:33:55 +0000 Subject: [PATCH 038/114] =?UTF-8?q?diary(2026-08-22):=20seconda=20ondata?= =?UTF-8?q?=20=E2=80=94=20XS01=20fuori=20finestra=20regge,=20il=20canale?= =?UTF-8?q?=20funded=20si=20ridimensiona=20di=205x,=20il=20deflated-Sharpe?= =?UTF-8?q?=20non=20ha=20regione=20utile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/diary/2026-08-22-wave2-xs01-funded.md | 188 +++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 docs/diary/2026-08-22-wave2-xs01-funded.md diff --git a/docs/diary/2026-08-22-wave2-xs01-funded.md b/docs/diary/2026-08-22-wave2-xs01-funded.md new file mode 100644 index 0000000..cdadbe9 --- /dev/null +++ b/docs/diary/2026-08-22-wave2-xs01-funded.md @@ -0,0 +1,188 @@ +# 2026-08-22 (sera) — Seconda ondata: XS01 fuori dalla sua finestra, e il canale funded ridimensionato di 5x + +**Branch:** `research/wave-0822` · **9 filoni** · **0 candidati promossi** · **libro, pesi, cron, +config INVARIATI.** + +Registro per filone: `docs/research/RESULTS-0822.md` (sezioni 21-29). Briefing: `BRIEF-0822.md`. + +--- + +## Perche' una seconda ondata + +La prima (20 filoni) si era chiusa con un risultato di testa — **PROP-ALLOC**: su un conto *funded* +riallocare per la **barriera di drawdown** invece che per lo Sharpe porta J da 0,335 a 0,738, con +**P(≥50 €/g) da €600 in 36 mesi = 42%**. Il percorso piu' veloce mai misurato in questo progetto. + +E si era chiusa con l'agente che dichiarava contro se stesso il proprio limite: *"tutto il vantaggio +poggia sul drift di XS01 misurato sulla sua finestra di scoperta"*, con un gate che era **un'attesa di +anni** (*"non aprire un funded prima che XS01 abbia una finestra fuori dal 2024-2026"*). + +La seconda ondata e' stata mirata su quello, piu' sui falsificatori che gli agenti stessi avevano +**nominato senza poterli eseguire**. + +--- + +## 1. XS01 regge fuori dalla sua finestra — ed e' piu' forte li' (§21) + +La finestra esisteva gia': gli stessi 19 ticker su **Binance spot 2021-01 → 2023-12**, che contiene +**LUNA e FTX**. Meccanismo **congelato**, nessuna griglia cercata. + +**Mediana di fase +1,12 fuori campione contro +0,37 nella finestra di scoperta; differenza appaiata ++0,668, positiva in 10/10 fasi**, p=0,013 contro permutazione a fee zero, sopravvive a 30 bps/lato. + +L'obiezione ovvia — *"Binance non e' la verita'"* — e' stata **misurata invece che aggirata**: quella +regola riguarda l'**ancoraggio di prezzo**, non un ranking cross-sezionale, e lo scarto USDT e' un +**fattore comune** che sparisce nello z-score. Lo stesso sleeve sui due venue da' **corr 0,9991**. + +**Null "universo dove non dovrebbe funzionare"** (11 settoriali SPDR, 1998+, meccanismo congelato): +**−0,45, 0/10 fasi positive** → **terza conferma indipendente che il cross-sectional e' +crypto-specifico**. + +## 2. Il falsificatore che l'autore ha nominato contro se stesso — eseguito, e non falsifica (§28) + +La validazione di venue di §21 girava **solo sul 2024+**, cioe' senza il depeg USDT che il fuori +campione contiene. Soglia dichiarata da lui: `corr ≥ 0,99`. + +**Corr mediana 0,9992, minima 0,9921. dSharpe appaiato −0,000. Il titolo passa da +1,12 a +1,11.** +Guardando il canale del danno — i ribilanciamenti — su 734 solo **10 cambiano una gamba su cinque**, e +**zero dentro la finestra del depeg**. + +**Il punto di metodo:** la divergenza va **scomposta**, perche' le due componenti sono domande diverse. +La **comune** arriva a −39 bps nel depeg ed e' quella che lo z-score annulla; la **idiosincratica** — +l'unica che cambia il ranking — sale solo da 2,0 a 3,6 bps mediani. *Riportare solo la prima avrebbe +dato la risposta rassicurante e sbagliata.* + +E ha trovato che **il caso peggiore del campione non e' il depeg: e' il 2025-10-10** (cascata di +liquidazioni sulle /USDT, INJ a 1.981 bps mentre BTC/ETH stanno a 21). **Il meccanismo temuto e' +REALE**; e' semplicemente caduto nella finestra di *scoperta*, non nel fuori campione. + +## 3. Ma il numero funded si ridimensiona di 5x — e il colpevole non e' la finestra (§29) + +Decomposizione a **un grado di liberta' per volta**: + +| passo | J | Δ | +|---|---|---| +| il pubblicato (HL, 19 gambe, finestra di scoperta) | 0,739 | — | +| → venue Binance, **date allineate** | 0,732 | **−0,006** | +| → **13 gambe** | 0,426 | **−0,307** | +| → pannello lungo | 0,459 | +0,034 | +| → **fuori campione 2021-2023** | 0,520 | **+0,061** | + +**Il 42% del numero pubblicato sta nelle 6 gambe che fuori campione non esistono** (ARB OP SUI APT SEI +TIA) — **e quelle 6 entrarono nell'universo nel 2026-06 guardando la liquidita' HL del 2024+, cioe' +dentro la finestra di scoperta.** La finestra, da sola, **migliora**. Allargare l'universo man mano +che le gambe nascono non recupera nulla. + +**Il numero operativo: P(≥50 €/g) da €600 in 36 mesi = 7,8% [7,2–9,1%], P(zero) 25,5%** — contro il +42% pubblicato stamattina. Il peso 0,50 non sopravvive; la **regione [25%, 38%] e' entro il 3% +dell'ottimo su tutte e tre le finestre**. + +**`GATE PROP-01`** sostituisce l'attesa di anni: **(a)** leggere il **listino della firm** — ≥10 delle +13 gambe shortabili, verifica a **€0** e **bloccante prima di ogni spesa**, *stessa classe +dell'errore GTAA01/PRIIPs*; **(b)** banda d'ancora di SKH01 sotto la lente prop, **2026-10-31**, PASS +se > +0,05; **(c)** un'eval HYRO $100k costa **$579 su un conto di $635**. + +## 4. L'allarme sul peso di TP01 e' falsificato NEL VERSO (§26) + +L'agente aveva registrato nel docstring, **prima di misurare**, che si aspettava di confermarlo. +Invece: **TP01 ~0,375 e' l'argmax sia sulla finestra che contiene il 2022 sia su quella che non lo +contiene**, e nel sinistro il libro con **meno** TP01 fa **meglio** — TP01 va flat mentre **SKH01 si +gira short e guadagna**. Test dei segni 12/12 (p=0,0005), 8/8 sul solo pre-2024. + +**Null del de-levering, 7a occorrenza, in veste nuova** ("protezione dal crash"): a leva comune TP01 +protegge davvero, ma **a iso-sopravvivenza meno TP01 paga di piu'** (payout $4.490 → $2.119, +monotono). + +Tre cose che valgono piu' dell'allarme rimosso: +- **"2024-2026 non contiene un crash" e' falsa nella forma**: ha buy&hold a −60% di DD. Manca la + **taglia** (~2,3x sulla coda). La domanda giusta e' *"e se ne arriva uno due volte piu' grande"*. +- **Alle leve funded il 2022 non era una minaccia** (min equity −1,4/−2,4% contro barriera −6%): cio' + che uccide un conto a barriera e' la **regola a un giorno su una giornata qualunque**. +- **Il rischio vero dell'ottimo e' XS01, non TP01** — e l'agente ha demolito la propria ipotesi + contraria dentro la sessione. + +## 5. Tre filoni chiusi con margine + +**BOCPD (§23) — l'orizzonte adattivo e' CHIUSO DEFINITIVAMENTE.** Il falsificatore e' stato eseguito +con i rilevatori che nominava (Adams & MacKay + optimal partitioning causale) e **questa volta ha +avuto potenza**: 68/96 celle davvero adattive, contro 0 del primo tentativo. Con potenza, **0/68** su +entrambe le condizioni. **La decomposizione e' il risultato: l'adattivita' spiega il −7% del vantaggio +su TP01** (+0,326 del miglior lookback costante contro +0,305 dell'adattivo). *Il 100% e' "un +orizzonte piu' corto", non "adattarlo".* + +**MAKER (§22) — il segno dipende da `<` contro `<=`.** Il passivo **vince sul 92-98% degli ordini e +perde sulla media** (5 ordini su 1535 fanno il 42% del danno). Forma speculare del profit-take di +VRP01. Tetto **$2,96/anno a $635** nel caso impossibile. + +**SURFACE-RV (§27) — chiuso, e il meccanismo vale piu' del verdetto.** Censimento su 1,24 M quote: +**0 violazioni di monotonia su 1,1 M**, 1 di convessita' su 1,03 M che vale **$0,00**. E `|residuo|` +cresce **monotonamente col largo del mercato** → **l'incoerenza al mid *e'* la larghezza del mercato +guardata attraverso il mid.** Questo uccide il controfattuale USDC che l'agente stesso aveva +costruito. + +## 6. BIN-FREQ: la direzione e' reale, la taglia e' selezione (§25) + +Argmax **+0,112 in 23/23 ancore**, ma **mediana di famiglia +0,007** con 39/72 celle positive = +monetina. **L'argmax vale 16x la mediana: punta, non plateau.** Direzione solidissima (0/72 sul segno +inverso, trasferisce a SKH01_V1). + +**Due premesse del mio briefing, misurate e refutate:** *"dimezza gli ordini"* → i nettati calano del +**17%** e i sotto-min-order **sono di TP01**; *"converti il risparmio di fee"* → **a fee zero +l'effetto resta l'87-110%**, quindi non e' risparmio di costo ma informazione sul rendimento lordo. +E la domanda che avevo posto era **il confronto sbagliato** (la banda del livello si cancella in un +disegno appaiato) — **ma la risposta corretta e' peggiore, non migliore, per il candidato**. + +Riapertura pre-registrata: **non una data, una soglia di CAPITALE** ($5.600 / $53.000), perche' cio' +che fallisce e' il rapporto fra un costo **fisso** e un beneficio **proporzionale**. + +## 7. Il critico: i difetti stanno nella CUCITURA, non nei filoni (§24) + +**Una regola mia, pubblicata la sera stessa e ritirata poche ore dopo:** avevo scritto che +`dealer_net_gamma` e' *"il GEX col segno invertito"*. **Falso**, e la fonte gira su questa stessa VPS +(`cerbero-mcp/.../common/options.py:11-14` **dichiara la convenzione**). Verificato in due comandi. +La correlazione a −0,95 era esattamente cio' che una convenzione diversa **deve** produrre. + +**E il difetto piu' grande dell'ondata: il deflated-Sharpe misurato ai suoi due estremi degeneri lo +stesso giorno da agenti che non si citano** — e **tre gate pre-registrati** ci poggiano sopra. + +--- + +## Il filo che tiene insieme la giornata: `deflated_sharpe` non ha una regione utile in mezzo + +**Tre agenti indipendenti, tre strade diverse, stessa conclusione.** + +`altlib.deflated_sharpe` calcola `sr0 = sd(Sharpe dei trial) × mult(N)`: dipende dalla **varianza** +della griglia, non solo da N. + +- **VOL-SIZE** (ondata 1) lo dichiara **VACUO**: 1,000 anche per il baseline. +- **BOCPD** lo misura dall'altro capo: **0,998 PASS** dentro la famiglia omogenea → **0,555 FAIL** con + l'sr0 di screen. *"Gonfiare N col padding alza N e non la varianza."* +- **CRITICO** trova che l'1,572 di ORTHO-SCREEN implica sd≈0,58, e che **le stesse 168 celle + dichiarate come 7 famiglie da 24 danno 1,15** — sotto il soffitto ~1,3: il verdetto *"impossibile + per aritmetica"* **si ribalta senza toccare un dato**, e la cura che suggerisce e' la manovra che il + progetto ha proibito il 30/07. +- **BIN-FREQ** chiude il cerchio dall'alto: su una famiglia **binaria** il DSR **riacquista potenza** + (0,977 contro baseline 0,834) perche' le celle scartano insiemi di trade molto diversi. *Non e' il + candidato a essere piu' solido: e' la famiglia a essere piu' larga.* + +**Non basta contare i trial di screen: serve la VARIANZA di screen.** E il numero di trial dichiarati +resta una scelta dell'autore, cioe' l'unico posto dove barare e' indolore e invisibile — con in piu' +il fatto che ora sappiamo che **la stessa griglia da' verdetti opposti a seconda di come la si +partiziona**. + +--- + +## Cosa NON e' stato fatto, e perche' + +**Nessuna riparazione applicata.** Il cron esegue dalla **working tree**, che e' su questo branch: una +riparazione andrebbe live alle 00:30 senza una decisione dell'operatore. Il diff tocca **solo** +`scripts/research/`, `docs/` e `CLAUDE.md` (verificato). + +Restano dell'operatore: **(1)** riparare `advance()` + rigenerare i monitor forward prima del gate +STATARB del **27/09**; **(2)** decidere del branch; **(3)** la verifica a costo zero che decide il +canale funded — **leggere il listino di una prop firm**. + +📌 **E la risposta onesta al mandato, che al critico era stato chiesto di dare se vera: nessuna misura +di ricerca di questa giornata batte "versare".** L'effetto piu' grande misurato oggi vale +**€0,0113/giorno**; versare €500/mese vale **~1456x** quello. La ricerca ha smesso di essere il +vincolo binding il 26/07, e due ondate da 29 filoni lo hanno **confermato invece che ribaltarlo**. From a1407874cbde94c806625f02c681b0639a4e9a28 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 21:35:40 +0000 Subject: [PATCH 039/114] =?UTF-8?q?CLAUDE.md:=20seconda=20ondata=20?= =?UTF-8?q?=E2=80=94=20XS01=20regge=20fuori=20finestra,=20il=20canale=20fu?= =?UTF-8?q?nded=20scende=20a=207,8%,=20e=20il=20deflated-Sharpe=20non=20ha?= =?UTF-8?q?=20regione=20utile=20in=20mezzo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 163 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 9792220..586c582 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2149,6 +2149,169 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis ETH 233.334, dal 2018-09, col deleveraging 2022 dentro** — cioe' **la coda che a CC01 mancava per costruzione**. Cross-check col forward implicito nella catena opzioni: mediana **−0,6 bps**. +- 🌊 **SECONDA ONDATA 2026-08-22 sera (9 filoni, `research/wave-0822`) — 0 candidati, 1 gate + pre-registrato SOSTITUITO, il numero funded ridimensionato di 5x, e un difetto strutturale del + deflated-Sharpe misurato da TRE agenti indipendenti.** Diario + `docs/diary/2026-08-22-wave2-xs01-funded.md`; registro `docs/research/RESULTS-0822.md` §21-29. + **Libro, pesi, cron, config INVARIATI.** + ✅ **(1) XS01 REGGE FUORI DALLA SUA FINESTRA DI SCOPERTA — ED E' PIU' FORTE LI'.** La finestra + c'era gia': stessi 19 ticker su **Binance spot 2021-2023**, che contiene **LUNA e FTX**, meccanismo + **congelato**. Mediana di fase **+1,12 fuori campione contro +0,37 dentro**; differenza appaiata + **+0,668, positiva in 10/10 fasi**; p=0,013 contro permutazione a fee zero; sopravvive a 30 + bps/lato. **L'obiezione "Binance non e' la verita'" e' stata MISURATA, non aggirata:** quella regola + riguarda l'**ancoraggio di prezzo**, non un ranking cross-sezionale, e lo scarto USDT e' un + **fattore comune** che sparisce nello z-score — stesso sleeve sui due venue **corr 0,9991**. + **Null "universo dove non dovrebbe funzionare"** (11 settoriali SPDR 1998+): −0,45, **0/10 fasi** → + **3ª conferma indipendente che il cross-sectional e' crypto-specifico**. + ⚠️ **Ma tre correzioni vanno nel verso opposto:** maxDD standalone **10,8% → 21%**; + **deflated-Sharpe FAIL 0,342**; **32% dell'universo non testabile** (19 gambe 1,31 contro 13 gambe + 0,54, stessa finestra e venue). **Verdetto LEAD**, non promozione. + ✅ **(2) Il falsificatore che l'autore aveva nominato CONTRO SE STESSO — eseguito, e non + falsifica.** La validazione di venue girava solo sul 2024+, cioe' **senza il depeg USDT** che il + fuori campione contiene. Soglia dichiarata da lui (`corr ≥ 0,99`): **corr mediana 0,9992, minima + 0,9921**, dSharpe appaiato **−0,000**, titolo da +1,12 a **+1,11**; sui 734 ribilanciamenti solo + **10 cambiano una gamba su cinque** e **ZERO dentro il depeg**. **Il punto di metodo: la divergenza + va SCOMPOSTA** — la **comune** arriva a −39 bps ed e' quella che lo z-score annulla, la + **idiosincratica** (l'unica che cambia il ranking) sale solo da 2,0 a 3,6 bps mediani; *riportare + solo la prima da' la risposta rassicurante e sbagliata*. 🚨 **E il caso peggiore del campione non e' + il depeg: e' il 2025-10-10** (cascata di liquidazioni /USDT, **INJ 1.981 bps** mentre BTC/ETH stanno + a 21) → **il meccanismo temuto e' REALE**, e' solo caduto nella finestra di *scoperta*. + 🚨 **(3) IL NUMERO FUNDED SI RIDIMENSIONA DI 5x, E IL COLPEVOLE NON E' LA FINESTRA.** + Decomposizione a **un grado di liberta' per volta** (riproduzione **esatta cifra per cifra** dei + numeri del mattino + 3 controlli bit-exact a 0,0): + | passo | J | Δ | + |---|---|---| + | il pubblicato (HL, 19 gambe, finestra di scoperta) | 0,739 | — | + | → venue Binance, **date allineate** | 0,732 | **−0,006** | + | → **13 gambe** | 0,426 | **−0,307** | + | → pannello lungo | 0,459 | +0,034 | + | → **fuori campione 2021-2023** | 0,520 | **+0,061** | + **Il 42% del numero pubblicato sta nelle 6 gambe che fuori campione non esistono** (ARB OP SUI APT + SEI TIA) — **e quelle 6 entrarono nell'universo nel 2026-06 guardando la liquidita' HL del 2024+, + cioe' dentro la finestra di scoperta**. La finestra, da sola, **migliora**; allargare l'universo man + mano che le gambe nascono **non recupera** i 0,307. + ⚠️ **Errore di metodo catturato prima di pubblicare:** la prima stesura dava al passo venue −0,254 + ("Binance costa un quarto di J"). Era **FASE, non venue** — `xsec_engine` ribilancia sulla parita' + dell'indice di pannello, e **due pannelli che iniziano a date diverse ribilanciano in giorni di + calendario diversi**. Allineati, il venue vale −0,006. + **NUMERO DA CITARE: P(≥50 €/g) da €600 in 36 mesi = 7,8% [7,2–9,1%], P(zero) 25,5%** (non 42%); + 6,6% se la firm non lista gli alt; 4,3% per il libro live. **Il peso 0,50 non sopravvive; la + REGIONE [25%, 38%] e' entro il 3% dell'ottimo su tutte e tre le finestre.** Stress ora ancorati al + misurato: `XS ×0,00` **+0,041** → **XS01 come pura DECORRELAZIONE vale +0,04, tutto il resto e' il + suo drift.** + 📌 **`GATE PROP-01` — SOSTITUISCE l'attesa di anni, che e' CHIUSA:** **(a) LISTINO — verifica a €0, + BLOCCANTE prima di ogni spesa:** ≥10 delle 13 gambe negoziabili **con short** (sotto 10 il + meccanismo congelato **non ribilancia affatto**); FAIL → si decide sulla riga senza XS01. *Stessa + classe dell'errore GTAA01/PRIIPs, per cui la regola e' gia' scritta.* **(b)** banda d'ancora di + SKH01 sotto la lente prop, **2026-10-31**, PASS se **> +0,05** (atteso PASS, dichiarato prima). + **(c)** un'eval HYRO $100k costa **$579 su un conto di $635** = il **91%**. + ⚠️ **(4) I DUE AFFINAMENTI DI XS01 DEL 2026-06-19 NON SI REPLICANO FUORI CAMPIONE** (misurato da + **due agenti indipendenti**): il **blend [30,90] e' PEGGIO del solo L=30** (+1,12 vs +1,28) e il + **gate di dispersione p30 vale ZERO** (+1,12 con e senza) — mentre *dentro* la finestra di scoperta + il gate vale eccome (+0,37 contro −0,03). Il secondo agente lo ritrova per un'altra strada: l'unica + correlazione sotto soglia del suo test e' **causata dal gate** (spento, 0,9674 → **0,9977**), perche' + e' **binario** e pochi bps lo fanno cadere ai due lati. **Cio' che sopravvive e' il meccanismo + cross-sectional NUDO.** ⚠️ **NON e' un argomento per cambiarli** — cambiarli guardando *questo* + risultato sarebbe la stessa selezione spostata di finestra — **e' un argomento per non attribuire + loro il valore che questa memoria gli attribuisce.** + ✅ **(5) L'ALLARME SUL PESO DI TP01 E' FALSIFICATO NEL VERSO** (l'agente aveva registrato nel + docstring, **prima di misurare**, che si aspettava di confermarlo): **TP01 ~0,375 e' l'argmax sia + sulla finestra che contiene il 2022 sia su quella che non lo contiene**, e nel sinistro il libro con + **meno** TP01 fa **meglio** — TP01 va flat mentre **SKH01 si gira short e guadagna**; test dei segni + **12/12** (p=0,0005) e **8/8** sul solo pre-2024. **Null del de-levering, 7ª occorrenza, in veste + nuova** ("protezione dal crash"): a leva comune TP01 protegge davvero, ma **a ISO-SOPRAVVIVENZA meno + TP01 paga di piu'** ($4.490 → $2.119, monotono). Tre collaterali: **(i)** *"2024-2026 non contiene + un crash"* e' **falsa nella forma** (buy&hold −60% di DD); manca la **taglia** (~2,3x sulla coda) → + la domanda giusta e' **"e se ne arriva uno due volte piu' grande"**; **(ii)** alle leve funded **il + 2022 non era una minaccia** (min equity −1,4/−2,4% contro barriera −6%): **cio' che uccide un conto + a barriera e' la regola a UN GIORNO su una giornata qualunque**; **(iii)** il rischio vero + dell'ottimo e' **XS01, non TP01**. Il 75/25 live e' **fuori** dalla regione di compromesso del funded + — coerente col fatto gia' scritto il 25/07: *canale funded e libro proprietario vogliono libri + diversi*. + ❌ **(6) ORIZZONTE ADATTIVO — CHIUSO DEFINITIVAMENTE.** Il falsificatore e' stato eseguito coi + rilevatori che nominava (**BOCPD** Adams & MacKay + **optimal partitioning causale**) e **questa + volta ha avuto POTENZA**: **68/96 celle davvero adattive** (contro 0 del primo tentativo, dove il + "vincitore adattivo" aveva lookback costante). Con potenza: **0/68** su entrambe le condizioni + dichiarate, corr→TP01 **minimo 0,754** (soglia 0,60). 📌 **La decomposizione E' il risultato: + l'adattivita' spiega il −7% del vantaggio su TP01** (miglior costante +0,326, adattivo +0,305) → il + 100% e' **"un orizzonte piu' corto", non "adattarlo"**. E il miglior costante **non si propone**: il + salto fra L **adiacenti** e' **+0,190 = il 58% del vantaggio** — max-of-24 su un crinale di rumore. + 📌 **Prova di IDENTITA' invece di diagnostica** (controllo riusabile in ogni famiglia con un + parametro clippato): invece di *"questa cella sta al bordo"*, la sua serie e quella del costante + coincidono a **max|Δ| = 0 su n=2719**. + ❌ **(7) MAKER — il segno dipende da `<` contro `<=`.** Differenziale maker/taker: tetto **$2,96/anno + a $635** (replica indipendente della curva 26/07 al terzo decimale). Il costo del non-fill lo mangia + interamente gia' alla condizione di riempimento **piu' generosa fisicamente difendibile**: se una + barra che apre esattamente al nostro prezzo e da li' se ne va conta come fill, resta +0,31 bps; se + **non** conta (e non conta: a quel prezzo non e' stato scambiato niente dopo l'arrivo dell'ordine, e + un passivo sta **dietro a tutta la coda**), resta **−0,47**. **Non esiste un q positivo di + pareggio**; IC95 contiene lo zero in **10 celle su 10**. 📌 **La firma della troncatura, vista + dall'altra parte: il passivo vince sul 92-98% degli ordini e perde sulla media** (5 ordini su 1535 + fanno il **42%** del danno) — *una politica che vince quasi sempre e perde sulla media si giudica + sulla media*. **Placebo a ore casuali girato**: la selezione avversa **non** e' "il libro trada nei + momenti brutti", e' **strutturale all'esecuzione passiva**. ⚠️ Osservazione che riapriva la domanda: + il netting su strumento unico (che uccise T1 il 26/07) **non si applica all'ordine NETTO di + ribilanciamento**, che e' uno solo. + ❌ **(8) SUPERFICIE DI VOLATILITA' — il lato relative-value e' CHIUSO.** Censimento (non selezione) + su **1,24 M quote a due lati**: **0 violazioni di monotonia su 1,11 M**, **1 di convessita' su 1,03 + M** che vale **$0,00** ed e' sparita l'ora dopo. |residuo| mediano **0,125-0,200 pt-vol** contro + **mezzo** spread 0,861-1,208, e **sotto il tick nel 96% dei casi**. 📌 **IL MECCANISMO: |residuo| + cresce MONOTONAMENTE col largo del mercato** (0,080 pt a spread 2,6% → 0,255 a spread 50%) → + **l'incoerenza al mid *E'* la larghezza del mercato guardata attraverso il mid**, non un errore di + prezzo — e questo uccide il controfattuale USDC che l'agente stesso aveva costruito (restringendo + alla superficie gia' quotata stretta: **0 su 158.718**). Reversione **misurata**: mezza-vita 11-12 h, + corr(t,t+1) +0,94 → il residuo e' **la forma dello smile**. Controlli positivi **3/3 in entrambi i + versi** — *un rilevatore che trova ~0 e non e' validato e' indistinguibile da uno rotto*. + ⚠️ **(9) BIN-FREQ (il binario di frequenza su SKH01) — LEAD: la direzione e' reale, la TAGLIA e' + selezione.** Argmax **+0,112 in 23/23 ancore**, ma **mediana di famiglia +0,007** con 39/72 celle + positive = **monetina**: **l'argmax vale 16× la mediana, punta e non plateau**. Direzione + solidissima (**0/72** sul segno inverso; trasferisce a SKH01_V1). Criterio a 4 gambe: **3 FAIL**, e + la taglia e' **€0,0113/giorno**. Due implementazioni difendibili della stessa regola, d'accordo sul + **97,6%** dei trade, danno +0,066 contro +0,080 → **il 21% dell'effetto sta in come si riempie il + warm-up**. 📌 **Riapertura pre-registrata: non una data ma una SOGLIA DI CAPITALE** ($5.600 alla + stima della cella, **$53.000** a quella onesta di famiglia) — perche' cio' che fallisce e' il + rapporto fra un costo **fisso** e un beneficio **proporzionale**, e nessun forward a 6 mesi puo' + misurare 0,07 di Sharpe (SE≈1,4). + 🚨 **(10) IL FILO DELLA GIORNATA: `deflated_sharpe` NON HA UNA REGIONE UTILE IN MEZZO — misurato da + TRE agenti indipendenti, e TRE gate pre-registrati ci poggiano sopra** (XSR01 23/10, DVOLSPREAD + 24/10, 22/12). `altlib.deflated_sharpe` calcola `sr0 = sd(Sharpe dei trial) × mult(N)`: dipende + dalla **VARIANZA** della griglia, non solo da N. **(a)** VOL-SIZE lo dichiara **VACUO** (1,000 anche + per il baseline). **(b)** BOCPD lo misura dall'altro capo: **0,998 PASS** dentro la famiglia omogenea + → **0,555 FAIL** con l'sr0 di screen, TP01 compreso — *"gonfiare N col padding alza N e non la + varianza"*. **(c)** Il critico trova che l'**1,572 di ORTHO-SCREEN** implica sd≈0,58 e che **le + stesse 168 celle dichiarate come 7 famiglie da 24 danno 1,15**, sotto il soffitto ~1,3: il verdetto + *"uno screen largo non puo' passare il proprio gate per aritmetica"* **si ribalta senza toccare un + dato**, e la cura che suggerisce (famiglie piu' piccole) e' **la manovra proibita il 30/07**. + **(d)** BIN-FREQ chiude il cerchio dall'alto: su una famiglia **binaria** il DSR **riacquista + potenza** (0,977 contro baseline 0,834) perche' le sue celle scartano insiemi di trade molto + diversi — *non e' il candidato a essere piu' solido, e' la famiglia a essere piu' larga*. + **REGOLA: non basta contare i trial di screen, serve la VARIANZA di screen** — e **la stessa griglia + da' verdetti opposti a seconda di come la si partiziona**, il che rende la scelta della partizione + un secondo posto dove barare e' indolore e invisibile. + ⚠️ **(11) REGOLA MIA PUBBLICATA E RITIRATA IN GIORNATA** — vedi il ritiro nel bullet dell'ondata 1: + `dealer_net_gamma` **non e' il GEX col segno invertito**, e' una **convenzione dichiarata** nel + sorgente che gira su questa stessa VPS. **REGOLA: prima di dichiarare che un campo di un dataset e' + sbagliato, aprire il codice che lo produce.** + 📌 **(12) ALTRE REGOLE NUOVE.** (a) **Una divergenza fra due fonti di prezzo si scompone in COMUNE e + IDIOSINCRATICA prima di giudicarla**: le due rispondono a domande diverse e la prima da' sempre la + risposta rassicurante. (b) **Due pannelli che iniziano a date diverse ribilanciano in giorni di + calendario diversi**: un confronto di venue non allineato misura la **fase**, non il venue (qui + −0,254 contro −0,006, fattore 42). (c) **Un null di permutazione puo' avere mediana diversa da + zero per ragioni strutturali** — scartare a caso il 40% dei trade danneggia perche' lo Sharpe va con + √(scommesse/anno) (misurato 0,883 contro 0,861 previsti): il null resta valido e **piu' severo**, ma + risponde a *"batte una selezione casuale?"*, non a *"batte il non filtrare?"*. (d) **Un + contro-fattuale costruito sui mid di un mercato largo non descrive un mercato stretto**: prendere il + mid a spread 50% e fingere di quotarlo al 2,6% fabbrica opportunita' che spariscono restringendo il + campione a cio' che e' gia' quotato stretto. (e) **Un `git add -A` durante un'ondata cattura il + lavoro IN CORSO degli agenti** (successo qui: un commit conteneva una versione intermedia e + sbagliata di uno script poi corretto). + 📌 **(13) E LA RISPOSTA ONESTA AL MANDATO, che al critico era stato chiesto di dare se vera: + NESSUNA misura di ricerca di questa giornata batte "versare".** L'effetto piu' grande misurato oggi + vale **€0,0113/giorno**; versare €500/mese vale **~1456×** quello. **La verifica che costa meno di + tutte non e' ricerca: leggere il listino di due prop firm** — l'intero risultato di testa poggiava + su un assunto scritto in un **commento a riga 100**. *La ricerca ha smesso di essere il vincolo + binding il 26/07, e due ondate da 29 filoni lo hanno CONFERMATO invece che ribaltarlo.* + - **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso: cross-sectional su universo Hyperliquid certificato (XS01) → portafoglio Sharpe ~1.55. - **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.** From bcffcc01af79b37ca02bb34c880655bbf5f5643e Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 21:40:12 +0000 Subject: [PATCH 040/114] =?UTF-8?q?research(wave-0822):=20GATE=20PROP-01?= =?UTF-8?q?=20gamba=20(a)=20CHIUSA=20=E2=80=94=2013/13=20sul=20venue;=20e?= =?UTF-8?q?=20la=20quota=20d'ingresso=20e'=20un=20deposito=20rimborsabile,?= =?UTF-8?q?=20non=20una=20fee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 2aab3f3..356cf84 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -695,6 +695,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 21 | XS01-OOS | il vantaggio del PROP-ALLOC poggia sul drift di XS01 misurato sulla sua finestra di scoperta: regge fuori? | **LEAD** — regge, ed e' PIU' GRANDE fuori | | 28 | DEPEG | il falsificatore che l'autore di 21 ha nominato contro se stesso: il depeg USDT contamina il ranking? | **SOPRAVVIVE** — il venue sposta 0,01 di Sharpe | | 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 | | 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 | @@ -1314,3 +1315,49 @@ PASS; il surrogato dice +0,11/+0,12). **`IL CANALE FUNDED: SI APRE A CONDIZIONE DI leggere il listino della firm` — e di citarlo come P(≥50 €/g) 7,8% [7,2–9,1%] con P(zero) 25,5%, non 42%.** + +--- + +## 30 — LISTINO (`GATE PROP-01` gamba (a)) — **la verifica da €0, fatta** + +Non un agente: eseguita dal coordinatore, perche' e' una **verifica esterna**, non un backtest — ed e' +la gamba **bloccante prima di ogni spesa**. La regola del progetto la impone da luglio: *la +negoziabilita' sul conto REALE va verificata quando lo sleeve entra in RICERCA* (lezione GTAA01/PRIIPs). + +📌 **Metodo: NON leggere il sito della firm, interrogare il VENUE su cui la firm esegue.** HyroTrader +esegue su **Bybit** via API (o su Cleo, con dati Binance). Interrogato +`api.bybit.com/v5/market/instruments-info?category=linear` (pubblico, tokenless, **833 strumenti**), +lo stesso percorso che il progetto usa per certificare i feed. + +| esito | risultato | +|---|---| +| **U13 (le 13 gambe testabili fuori campione)** | **13/13 `Trading`, `LinearPerpetual`** | +| le 6 gambe che fuori campione non esistono | **6/6 `Trading`** | +| min notional | **$5 su tutte e 13** | +| leva max | 50x-150x | + +**`GATE PROP-01 (a): PASS 13/13` (soglia era ≥10).** E sono **perpetual lineari**: lo short e' +strutturale, non un permesso — non esiste un divieto di vendita allo scoperto su un perp. +⚠️ **Cio' che questo NON prova:** che HyroTrader non abbia una **propria** lista ristretta distinta da +quella di Bybit. La sua pagina dichiara *"700+ USDT perpetual pairs"* su un venue che ne ha 833 → +coerente con nessuna restrizione significativa, ma e' la **sua** dichiarazione. Il residuo si chiude +con una domanda al supporto, non con un dataset. + +✅ **VALIDAZIONE del modello del progetto (non correzione):** la pagina della firm dichiara +**daily drawdown 4% · max loss 6%**, e `scripts/research/r0725_hyro.py:48-49` usa gia' +`EV_TARGET, EV_DD, EV_DL = 0.10, 0.06, 0.04` e `FU_ML, FU_DL = 0.06, 0.04` → **le regole modellate dal +25/07 sono quelle vere.** + +🚨 **UNA CORREZIONE ECONOMICA, invece, c'e': la quota d'ingresso non e' un COSTO AFFONDATO.** La firm +la dichiara **"Refundable Challenge Deposit, returned in full alongside your first funded payout"** +(pagina della firm + fonti secondarie concordi). Il progetto l'ha sempre modellata come **spesa +persa** — e la gamba (c) di `GATE PROP-01` dice *"un'eval $100k costa $579 su un conto di $635 = il +91%"*. **Resta vero che immobilizza il 91% del conto**, ma **non e' vero che lo brucia**: si perde +solo nel ramo in cui non si arriva mai a un payout (**P(zero) 25,5%**), e torna in quello in cui si +arriva. **E' una struttura a opzione, non una fee** → l'EV del biglietto va rifatto. +⚠️ **Qualita' delle fonti dichiarata:** il listino degli strumenti e' **primario e misurato** (API del +venue); le regole di DD sono **primarie ma dichiarative** (pagina della firm); la rimborsabilita' e il +prezzo esatto del taglio $100k sono **primari-dichiarativi + secondari concordi**, non misurati. +📌 **Conseguenza sul gate:** la gamba (a) e' **CHIUSA con un PASS**. Restano la **(b)** — banda +d'ancora di SKH01 sotto la lente prop, che un agente sta girando ora invece di aspettare il 31/10 — e +la **(c)**, che va **ricalcolata** con la quota come deposito rimborsabile invece che come spesa. From dd4391b55b9fb8d7c7f0b6763213e3eb0998dd50 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 21:54:59 +0000 Subject: [PATCH 041/114] =?UTF-8?q?research(wave-0822):=20GATE-RECON=20?= =?UTF-8?q?=E2=80=94=20il=20difetto=20ribalta=20STATARB=20(+1,96=20->=20-2?= =?UTF-8?q?,02),=20la=20premessa=20della=20rigenerazione=20e'=20confermata?= =?UTF-8?q?=20per=20via=20diretta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 70 +++ scripts/research/r0822c_gate_recon.py | 719 ++++++++++++++++++++++++++ 2 files changed, 789 insertions(+) create mode 100644 scripts/research/r0822c_gate_recon.py diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 356cf84..4263a1b 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -696,6 +696,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 28 | DEPEG | il falsificatore che l'autore di 21 ha nominato contro se stesso: il depeg USDT contamina il ranking? | **SOPRAVVIVE** — il venue sposta 0,01 di Sharpe | | 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 | | 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 | @@ -1361,3 +1362,72 @@ prezzo esatto del taglio $100k sono **primari-dichiarativi + secondari concordi* 📌 **Conseguenza sul gate:** la gamba (a) e' **CHIUSA con un PASS**. Restano la **(b)** — banda d'ancora di SKH01 sotto la lente prop, che un agente sta girando ora invece di aspettare il 31/10 — e la **(c)**, che va **ricalcolata** con la quota come deposito rimborsabile invece che come spesa. + +--- + +## 31 — GATE-RECON (cosa direbbero i gate con le serie riparate) — **sola lettura, verificata** + +`scripts/research/r0822c_gate_recon.py` (719 righe, ~25 s). **Zero celle di selezione**: e' un audit, +tutte le soglie **lette dai file pre-registrati**, nessun parametro scelto dall'agente. +✅ **Sola lettura provata**: md5 + mtime di **ogni** file `data/paper_*` invariati dopo la corsa. + +| gate | serie | n | Sharpe | maxDD | vol ann. | +|---|---|---|---|---|---| +| **STATARB** | **attuale** | 54 | **+1,96** | **2,24%** | 7,88% | +| **STATARB** | **riparata** | 53 | **−2,02** | **15,19%** | **31,42%** | +| XSR01 | attuale | 28 | −6,55 | 0,28% | 0,46% | +| XSR01 | riparata | 27 | −0,99 | 0,82% | 2,78% | +| DVOLSPREAD | attuale | 28 | −15,82 | 0,93% | 0,79% | +| DVOLSPREAD | riparata | 27 | −5,21 | 7,11% | 19,36% | + +Minuti di mercato registrati al giorno: **3,9 / 41,2 / 2,5** = ✅ replica indipendente dei 4/41/2 +pubblicati stamattina. + +**`STATARB 27/09 — riparata: RITIRO · attuale: CANDIDATO AL DEPLOY`** +**`XSR01 23/10 — riparata: RITIRO · attuale: RITIRO`** +**`DVOLSPREAD 24/10 — riparata: KILL · attuale: KILL`** +*(Non sono i verdetti dei gate: sono i criteri letti **oggi**, su finestre incomplete — 54/90, 28/90, +28/91 barre. Anticipare un gate pre-registrato e' selezione sull'hold-out, e l'agente lo dichiara.)* + +✅ **LA PREMESSA SU CUI POGGIA TUTTO E' ORA CONFERMATA PER VIA DIRETTA, non piu' di traverso:** +`data/_feed_backup/*.prebuild.bak` conserva il **vintage pre-riscrittura** di stanotte → +**0 barre chiuse cambiate su 1.761.259** (BTC/ETH 1h+5m). L'unica riga cambiata per file e' **l'ultima +del vintage, che era aperta** (BTC 21/08: close 73732,5→73690, volume 342→492 = **il difetto stesso, +in chiaro**). Su 62 cicli `paper_prevday` fa 1427/1488 identiche, con tasso di divergenza **piatto +nell'eta'** (2,2-5,1%, nessuna crescita) e concentrato all'ora del cron. +⚠️ **Limite dichiarato:** `hl_*_1d` e `dvol_*` **non hanno vintage su disco** → per XSR01 e +DVOLSPREAD l'evidenza resta **indiretta**. + +**Prove d'identita' obbligatorie prima di ogni confronto:** **I1** pannello letto ora == quello del +cron di stanotte (`max|d| = 0`, 3/3, contro lo `state.json` scritto alle 00:35); **I2** barre gia' +chiuse identiche al bit (statarb 6/54, xsr 1/28, **dvolspread 0/28 → per quel monitor la prova I2 NON +ESISTE, dichiarato**); **I3** troncare l'INPUT == affettare l'output (`max|d| = 0`, 3/3). + +🚨 **(1) IL VETO D'INTEGRITA' DI DVOLSPREAD NON SCATTA: 28 attive / 28 = 100% in ENTRAMBE le lenti.** +Una serie che misura **2 minuti di mercato al giorno** passa al **100%** un veto progettato apposta per +non far decidere su dati mancanti. **Non e' un problema di taratura:** il veto conta *se il DVOL +c'era*, non *quanto dura la barra su cui c'era* — **assi ortogonali**. La stessa soglia 0,80 applicata +alla quota di barre **chiuse** leggerebbe **0%** e vieterebbe. **Variante nuova di una regola vecchia: +una barra presente non e' una giornata presente.** +🚨 **(2) Il caso peggiore NON e' quello coi numeri piu' assurdi.** XSR01 (|Sh| ×6,6) e DVOLSPREAD +(×3,0) dicono **lo stesso verdetto** in entrambe le lenti; **STATARB ha |Sharpe| quasi identico +(1,96 contro 2,02) e cambia SEGNO** — ed e' il monitor del gate **piu' vicino**. Un controllo che +cercasse *"numeri troppo belli"* **lo lascerebbe passare**. 📌 **La firma da cercare non e' il +|Sharpe| ma la vol forward molto sotto quella del backtest** (7,88% contro 31,42%). +📌 **(3) L'asimmetria del 25/07 su STATARB:** la diagnostica pre-registrata (statica sempre-short) +**non e' affetta** — si ricalcola dal **pannello**, non dalla serie registrata (varia di −0,01 fra le +due finestre contro −3,97 della strategia). **Al gate si confronterebbe un benchmark GIUSTO con un +numero di strategia SBAGLIATO**, e il margine sembrerebbe **+5,83 invece di +1,87**. Il segno del +margine sopravvive: il difetto ribalta il gate, non questa diagnostica. + +📌 **QUANTO COSTA ASPETTARE — l'unico argomento quantitativo su *quando* riparare.** Con +**rigenerazione**: **zero barre perse a qualunque data**; si paga solo il rischio che la riparazione +non arrivi. **Senza**: riparare **oggi** lascia **36/90** barre vere a STATARB (62/90 a XSR01, 63/91 a +DVOLSPREAD); **alla vigilia, 1**. E la riparazione parziale **non contamina la finestra, la accorcia** +— una barra troncata porta ~1/370 della varianza *e* del rendimento di una vera, quindi non pesa: +riparando oggi, il 27/09 si leggerebbe una finestra **piu' corta ma VERA** (SE(Sharpe) ~3,2 invece di +~2,0). **Riparare alla vigilia no.** + +⚠️ `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**. diff --git a/scripts/research/r0822c_gate_recon.py b/scripts/research/r0822c_gate_recon.py new file mode 100644 index 0000000..3e399c7 --- /dev/null +++ b/scripts/research/r0822c_gate_recon.py @@ -0,0 +1,719 @@ +"""GATE-RECON — cosa direbbero i tre gate pre-registrati se le serie forward fossero giuste. + +IL FATTO DA CUI NASCE (filone MONITOR-AUDIT, 22/08, gia' misurato e NON riparato per scelta). +`fetch_hyperliquid` e `resample_tf` scrivono la barra del giorno IN CORSO; `advance()` la consuma +e porta `last_ts` su di essa -> le ore restanti non entrano in nessun rendimento. Quattro monitor +su sei registrano una FRAZIONE del giorno (2, 4, 19 e 41 minuti su 1440). Tre decisioni +pre-registrate leggono serie costruite cosi': + STATARB 2026-09-27 r0724_statarb_deploy_gate (Sh>=0.5, maxDD<10%, haircut<0.5pp) + XSR01 2026-10-23 r0725_xsr_deploy_gate (Sh>=1.0 E haircut$5000<=40%) + DVOLSPREAD 2026-10-24 kill / 2027-01-24 decisione (soglie dentro paper_dvolspread) + +QUESTO SCRIPT NON RIPARA E NON DECIDE. La riparazione e' una decisione dell'operatore e i gate +hanno una data: anticiparli sarebbe selezione-sul-forward, il bias che questo progetto ha gia' +pagato due volte. La consegna e' un DELTA, non un verdetto: + "alla data di oggi, con la serie riparata il criterio direbbe Y; con la serie com'e', Z" +e la distanza fra Y e Z e' la misura del danno del difetto — l'unica cosa che serve per decidere +SE e QUANDO riparare. Sola lettura su tutto cio' che e' produzione: i moduli di `scripts/live/` +si importano senza chiamarne `main()`, e `_append` viene sostituito con un raccoglitore in memoria +PRIMA di qualunque chiamata ad `advance()` -> nessun file di `data/` viene aperto in scrittura. + +METODO. La serie RIPARATA non e' una reimplementazione: e' il codice di produzione rieseguito +dall'inception sui dati di oggi, TOLTA l'ultima barra (l'unica ancora aperta). Tutto il resto del +pannello e' fatto di barre chiuse, quindi il replay E' la serie che il monitor avrebbe registrato +se avesse aspettato la chiusura. Questo poggia su due condizioni, entrambe verificate qui: + (P1) il feed non riscrive le barre gia' chiuse -> sezione 0; + (P2) il pannello che leggo ora e' quello che il cron ha letto stanotte, e troncare l'ULTIMA + barra non cambia nessuna barra precedente -> sezione 1 (tre prove d'identita'). +Senza (P2) starei ricostruendo una strategia diversa invece della stessa strategia riparata, ed e' +il modo piu' facile di sbagliare questo lavoro. + +COSA MI ASPETTAVO PRIMA DI MISURARE (registrato qui perche' sia falsificabile): + * la premessa (P1) vera ma verificabile solo di traverso -> **sbagliato, ed e' la sorpresa + utile**: `data/_feed_backup/*.prebuild.bak` conserva il vintage PRE-riscrittura di stanotte, + quindi la premessa si verifica DIRETTAMENTE su ~914.000 barre, senza passare da una strategia; + * i tre gate tutti ribaltati -> **sbagliato, e la ragione conta**: solo STATARB si ribalta. + XSR01 e DVOLSPREAD dicono "ritiro/kill" su ENTRAMBE le lenti, ma con numeri che differiscono + di 3-7x e che a 28 barre non sono letture di gate su nessuna delle due; + * il veto d'integrita' di DVOLSPREAD (l'unico dei tre che ha una guardia) capace di accorgersene + -> **confermato che NON se ne accorge**, e per una ragione strutturale: conta se il DVOL C'ERA, + non quanto dura la barra su cui c'era. + + nice -n 19 timeout 900 uv run python scripts/research/r0822c_gate_recon.py +""" +from __future__ import annotations + +import importlib +import importlib.util +import json +import sys +from datetime import date +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +RAW = ROOT / "data" / "raw" +BAK = ROOT / "data" / "_feed_backup" +for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt", + ROOT / "scripts" / "research" / "ortho"): + sys.path.insert(0, str(_p)) + +ANN = np.sqrt(365.0) +TODAY = date.today() +H = "=" * 100 +# Incidente del feed 2026-07-09 -> 2026-07-15 (`rebuild_history` abortito 7 giri, EPERM sul backup, +# gia' riparato): col feed fermo i monitor non trovavano barre nuove e al ripristino ne hanno +# recuperate in blocco, GIA' CHIUSE. Sono le uniche barre corrette di `paper_statarb` e vanno +# escluse da ogni stima del difetto, o gli si attribuisce un'attenuazione che viene da altro. +FREEZE = (pd.Timestamp("2026-07-09", tz="UTC"), pd.Timestamp("2026-07-16", tz="UTC")) + + +# -------------------------------------------------------------------------------------------- +# infrastruttura di sola lettura (stessa di r0822_monitor_audit: nessuna reimplementazione) +# -------------------------------------------------------------------------------------------- +_MODS: dict[str, object] = {} + + +def live(name: str): + """Esegue un modulo di `scripts/live/` per averne le DEFINIZIONI. Mai `main()`.""" + if name in _MODS: + return _MODS[name] + spec = importlib.util.spec_from_file_location(f"_ro_{name}", ROOT / "scripts" / "live" / f"{name}.py") + m = importlib.util.module_from_spec(spec) + spec.loader.exec_module(m) + _MODS[name] = m + return m + + +class Sink: + """Sostituisce `_append`: raccoglie in memoria invece di scrivere. E' l'unica ragione per cui + si puo' rieseguire il codice di produzione TALE E QUALE restando in sola lettura.""" + + def __init__(self): + self.rows: list[tuple[str, dict]] = [] + + def __call__(self, path, rec): + self.rows.append((Path(path).name, dict(rec))) + + def of(self, fname: str) -> list[dict]: + return [r for n, r in self.rows if n == fname] + + +def read_jsonl(p: Path) -> list[dict]: + return [json.loads(x) for x in p.read_text().splitlines() if x.strip()] if p.exists() else [] + + +def mets(a) -> dict: + """Sharpe (365), maxDD e ritorno cumulato di una serie di rendimenti netti per barra.""" + a = np.asarray(a, float) + a = a[np.isfinite(a)] + if len(a) < 2 or a.std() == 0: + return dict(n=len(a), sh=float("nan"), dd=float("nan"), tot=float("nan"), + mu=float("nan"), sd=float("nan")) + eq = np.cumprod(1.0 + a) + pk = np.maximum.accumulate(eq) + return dict(n=len(a), sh=float(a.mean() / a.std() * ANN), + dd=float(np.max((pk - eq) / pk)), tot=float(eq[-1] - 1.0), + mu=float(a.mean() * 365.0), sd=float(a.std() * ANN)) + + +def se_sharpe(n: int) -> float: + """Errore standard grezzo di uno Sharpe annualizzato su n barre giornaliere (~sqrt(365/n)).""" + return float(np.sqrt(365.0 / max(n, 1))) + + +# ============================================================================================ +# 0 — LA PREMESSA: rigenerare non perde la finestra SOLO se le barre chiuse non vengono riscritte +# ============================================================================================ +def sezione_0() -> dict: + print(H) + print(" 0 — PREMESSA: il feed riscrive le barre gia' CHIUSE?") + print(H) + print(""" + Perche' e' la premessa di tutto: la raccomandazione del filone MONITOR-AUDIT e' "riparare + `advance()` + RIGENERARE", cioe' ricalcolare la finestra forward dall'inception sui dati di + oggi. Se il feed riscrivesse le barre chiuse, quella rigenerazione produrrebbe una serie + DIVERSA da quella che si sarebbe registrata giorno per giorno -> la finestra andrebbe persa e + la conclusione cambierebbe. E' stata verificata due volte di traverso (via strategie); qui si + verifica una terza volta e in modo DIRETTO.""") + + out = {} + print("\n 0a. VERIFICA DIRETTA sul dato grezzo — vintage PRE-riscrittura contro oggi") + print(" `rebuild_history` copia il parquet in data/_feed_backup/*.prebuild.bak PRIMA di") + print(" sovrascriverlo: sul disco c'e' il file com'era ieri sera. Nessuna strategia in mezzo.") + tot_ch, tot_div = 0, 0 + for a in ("btc", "eth"): + for tf in ("1h", "5m"): + new_p, old_p = RAW / f"{a}_{tf}.parquet", BAK / f"{a}_{tf}.parquet.prebuild.bak" + if not (new_p.exists() and old_p.exists()): + print(f" {a}_{tf}: vintage assente -> NON MISURATO") + continue + new = pd.read_parquet(new_p) + old = pd.read_parquet(old_p) + m = old.merge(new, on="timestamp", suffixes=("_o", "_n")) + cols = ["open", "high", "low", "close", "volume"] + diff = np.zeros(len(m), bool) + for c in cols: + diff |= (m[f"{c}_o"].values != m[f"{c}_n"].values) + last_old = int(old["timestamp"].max()) + chiuse = m["timestamp"].values < last_old # nel vecchio file l'ultima riga era APERTA + nch, nd = int(chiuse.sum()), int((diff & chiuse).sum()) + tot_ch += nch + tot_div += nd + print(f" {a}_{tf}: {nch:,} barre gia' chiuse nel vintage -> DIVERSE OGGI: {nd}" + f" (righe cambiate in tutto: {int(diff.sum())})") + if diff.sum() == 1: + i = int(np.where(diff)[0][0]) + ts = pd.Timestamp(int(m["timestamp"].iloc[i]), unit="ms", tz="UTC") + print(f" l'unica riga cambiata e' {ts} = l'ULTIMA del vintage, che era " + f"aperta: close {m['close_o'].iloc[i]:g} -> {m['close_n'].iloc[i]:g}, " + f"volume {m['volume_o'].iloc[i]:,.0f} -> {m['volume_n'].iloc[i]:,.0f}") + out["chiuse"], out["div"] = tot_ch, tot_div + print(f"\n TOTALE: {tot_div} barre chiuse cambiate su {tot_ch:,} " + f"-> la riscrittura notturna e' un NO-OP sulle barre chiuse.") + print(" ⚠ questo e' UN ciclo di riscrittura (ieri->oggi). La stessa riga mostra anche il") + print(" DIFETTO: l'ultima riga del vintage era una barra parziale, ed e' esattamente") + print(" quella che i monitor consumano.") + + print("\n 0b. VERIFICA su 62 CICLI — tasso di divergenza per ETA' della barra (paper_prevday)") + print(" Statistica diversa da quella del 22/08 (che contava le identiche): se il feed") + print(" riscrivesse le barre chiuse, il tasso CRESCEREBBE con l'eta' (piu' riscritture") + print(" subite). Se il difetto e' solo la barra parziale al momento della registrazione,") + print(" il tasso e' PIATTO nell'eta' e vale ~1/24.") + pp = live("paper_prevday") + st = json.loads((ROOT / "data" / "paper_prevday" / "state.json").read_text()) + rec = read_jsonl(ROOT / "data" / "paper_prevday" / "returns.jsonl") + dfs = pp.build_bars() + S = st["start_ts"] + pos = {a: pp.pb.current_target(dfs[a][dfs[a]["timestamp"] <= S]) for a in pp.ASSETS} + sink = Sink() + pp._append = sink + st0 = dict(start_ts=S, last_ts=S, n_bars=0, pos_modeled=pos, pos_real=dict(pos), + cap_modeled=pp.MODELED_CAPITAL, cap_real=pp.REAL_CAPITAL, + peak_modeled=pp.MODELED_CAPITAL, peak_real=pp.REAL_CAPITAL, + dd_modeled=0.0, dd_real=0.0, n_trades=0) + pp.advance(st0, dfs) + rep = {int(r["ts"]): float(r["net_modeled"]) for r in sink.of("returns.jsonl")} + pairs = [(int(r["ts"]), float(r["net_modeled"]), rep[int(r["ts"])]) + for r in rec if int(r["ts"]) in rep] + T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True)) + A = np.array([p[1] for p in pairs]) + B = np.array([p[2] for p in pairs]) + dv = np.abs(A - B) > 1e-6 + now = pd.Timestamp.now(tz="UTC") + age = (now - T).days + d = pd.DataFrame(dict(age=age, div=dv)) + d["b"] = pd.cut(d.age, [-1, 7, 14, 21, 30, 45, 60, 999], + labels=["0-7g", "8-14g", "15-21g", "22-30g", "31-45g", "46-60g", ">60g"]) + g = d.groupby("b", observed=True).agg(n=("div", "size"), div=("div", "sum")) + g["tasso"] = g["div"] / g["n"] + print(f" {len(A)} barre orarie appaiate, {int(dv.sum())} divergenti " + f"({dv.mean() * 100:.2f}%; identiche {len(A) - int(dv.sum())}/{len(A)})") + print(" " + " ".join(f"{ix}:{r.tasso * 100:4.1f}%" for ix, r in g.iterrows())) + hh = pd.Series(T.hour[dv]).value_counts().sort_index() + print(" ora UTC delle divergenti: " + ", ".join(f"{int(h):02d}:00 x{int(n)}" for h, n in hh.items()) + + " <- il cron gira alle 00:30") + out["prevday_rate_flat"] = (float(g["tasso"].min()), float(g["tasso"].max())) + + print("\n 0c. LIMITE DICHIARATO — cosa NON e' verificato direttamente") + print(" Il vintage prebuild esiste solo per BTC/ETH (lo scrive `rebuild_history`).") + print(" `hl_*_1d.parquet` (che alimenta XSR01) e `dvol_*.parquet` NON hanno vintage su") + print(" disco: per loro l'evidenza resta indiretta (le barre che i monitor hanno letto") + print(" gia' chiuse coincidono al bit dopo settimane di riscritture — sezione 1).") + print("\n -> PREMESSA CONFERMATA sul feed BTC/ETH, per via diretta e su 62 cicli.") + print(" Rigenerare NON perde la finestra forward; nessuna data di gate si sposta.") + return out + + +# ============================================================================================ +# 1 — RICOSTRUZIONE DELLE TRE SERIE + PROVE D'IDENTITA' +# ============================================================================================ +def _ident_pannello(nome: str, salvato, calcolato) -> float: + d = float(np.max(np.abs(np.asarray(salvato, float) - np.asarray(calcolato, float)))) + print(f" I1 pannello == quello del cron di stanotte ({nome}): max|d| = {d:g}" + f" {'OK' if d == 0.0 else '*** DIVERSO ***'}") + return d + + +def ric_statarb() -> dict: + print("\n 1a. paper_statarb — gate STATARB 2026-09-27") + ps = live("paper_statarb") + st = json.loads((ROOT / "data" / "paper_statarb" / "state.json").read_text()) + rec = read_jsonl(ROOT / "data" / "paper_statarb" / "returns.jsonl") + j = ps.build_joint("1d") + ts, dt, pos, sr = ps._signal(j) + _ident_pannello("pos_modeled", [st["pos_modeled"]], [pos[-1]]) + i0 = int(np.where(ts == st["start_ts"])[0][0]) + + def replay(frame): + sink = Sink() + ps._append = sink + t2, _, p2, _ = ps._signal(frame) + k = int(np.where(t2 == st["start_ts"])[0][0]) + s0 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0, + pos_modeled=float(p2[k]), pos_real=float(p2[k]), + cap_modeled=ps.MODELED_CAPITAL, cap_real=ps.REAL_CAPITAL, + peak_modeled=ps.MODELED_CAPITAL, peak_real=ps.REAL_CAPITAL, + dd_modeled=0.0, dd_real=0.0, n_trades=0) + ps.advance(s0, frame) + return sink.of("returns.jsonl") + + full = replay(j) + trunc = replay(j.iloc[:-1].reset_index(drop=True)) + a = np.array([float(r["net_modeled"]) for r in full[:-1]]) + b = np.array([float(r["net_modeled"]) for r in trunc]) + print(f" I3 troncare l'INPUT == affettare l'OUTPUT: n {len(a)}/{len(b)}, " + f"max|d| = {float(np.max(np.abs(a - b))):g} (l'ultima barra non retroagisce)") + return dict(mod=ps, rec=rec, rep=full, panel=j, extra=dict(pos=pos, ts=ts)) + + +def ric_xsr() -> dict: + print("\n 1b. paper_xsr — gate XSR01 2026-10-23") + px = live("paper_xsr") + st = json.loads((ROOT / "data" / "paper_xsr" / "state.json").read_text()) + rec = read_jsonl(ROOT / "data" / "paper_xsr" / "returns.jsonl") + sink = Sink() + px._append = sink + ts, dt, W, R, rb, syms = px.build_panel() + _ident_pannello("w[modeled]", st["modeled"]["w"], W[-1]) + s0 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0, syms=st["syms"], + modeled=px._book(px.MODELED_CAPITAL, len(st["syms"])), + reals={n: px._book(c, len(st["syms"])) for c, n in px.REAL_BOOKS}) + px.advance(s0) + full = sink.of("returns.jsonl") + + # I3: troncare l'INPUT (l'ultima riga di OGNI parquet HL) e ricostruire da capo + orig = px.load_hl + + def load_trunc(sym): + return orig(sym).iloc[:-1] + + px.load_hl = load_trunc + sink2 = Sink() + px._append = sink2 + try: + ts2, _, W2, _, _, syms2 = px.build_panel() + s2 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0, syms=st["syms"], + modeled=px._book(px.MODELED_CAPITAL, len(st["syms"])), + reals={n: px._book(c, len(st["syms"])) for c, n in px.REAL_BOOKS}) + px.advance(s2) + tr = sink2.of("returns.jsonl") + a = np.array([float(r["net_modeled"]) for r in full[:-1]]) + b = np.array([float(r["net_modeled"]) for r in tr]) + dmax = float(np.max(np.abs(a - b))) if len(a) == len(b) else float("nan") + print(f" I3 troncare l'INPUT == affettare l'OUTPUT: n {len(a)}/{len(b)}, max|d| = {dmax:g}") + finally: + px.load_hl = orig + return dict(mod=px, rec=rec, rep=full) + + +def ric_dvolspread() -> dict: + print("\n 1c. paper_dvolspread — kill 2026-10-24 / decisione 2027-01-24") + pv = live("paper_dvolspread") + st = json.loads((ROOT / "data" / "paper_dvolspread" / "state.json").read_text()) + rec = read_jsonl(ROOT / "data" / "paper_dvolspread" / "returns.jsonl") + P = pv._panel() + _ident_pannello("wb_modeled", [st["wb_modeled"]], [P["wb"][-1]]) + i0 = int(np.where(P["ts"] == st["start_ts"])[0][0]) + + def replay(Q): + sink = Sink() + pv._append = sink + k = int(np.where(Q["ts"] == st["start_ts"])[0][0]) + s0 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0, n_active=0, + n_flat_signal=0, n_flat_nodata=0, + wb_modeled=float(Q["wb"][k]), wb_real=float(Q["wb"][k]), + cap_modeled=pv.MODELED_CAPITAL, cap_real=pv.REAL_CAPITAL, + peak_modeled=pv.MODELED_CAPITAL, peak_real=pv.REAL_CAPITAL, + dd_modeled=0.0, dd_real=0.0, n_flips=0, frozen=pv.FROZEN) + out = pv.advance(s0, Q) + return sink.of("returns.jsonl"), out + + full, st_full = replay(P) + Q = {k: (v[:-1] if hasattr(v, "__len__") else v) for k, v in P.items()} + trunc, st_tr = replay(Q) + a = np.array([float(r["net_modeled"]) for r in full[:-1]]) + b = np.array([float(r["net_modeled"]) for r in trunc]) + print(f" I3 troncare l'INPUT == affettare l'OUTPUT: n {len(a)}/{len(b)}, " + f"max|d| = {float(np.max(np.abs(a - b))):g}") + return dict(mod=pv, rec=rec, rep=full, st_rep=st_tr, st_rec=st) + + +def sezione_1() -> dict: + print("\n" + H) + print(" 1 — RICOSTRUZIONE + PROVE D'IDENTITA' (senza queste, il numero e' un'altra strategia)") + print(H) + print(""" + Tre prove, ognuna chiude un modo diverso di sbagliare: + I1 il pannello che leggo ORA e' quello che il cron ha letto stanotte (i parquet hanno mtime + 00:30-00:35 e nulla li riscrive fino al prossimo giro): si verifica contro lo `state.json` + del monitor, che e' stato scritto DA LUI alle 00:35. + I2 sulle barre che il monitor ha letto GIA' CHIUSE la ricostruzione coincide AL BIT: e' la + parte non affetta dal difetto, ed e' l'unico posto dove i due numeri devono coincidere. + I3 troncare l'ULTIMA barra dell'INPUT da' lo stesso risultato che affettare l'output: nessuna + barra precedente dipende dall'ultima (se dipendesse, la 'riparazione' sarebbe un altro + segnale, non lo stesso).""") + S = dict(statarb=ric_statarb(), xsr=ric_xsr(), dvol=ric_dvolspread()) + + print("\n I2 — barre che il monitor ha letto CHIUSE, e coincidenza al bit:") + for k, lab in (("statarb", "paper_statarb"), ("xsr", "paper_xsr"), ("dvol", "paper_dvolspread")): + rec, rep = S[k]["rec"], S[k]["rep"] + d = {int(r["ts"]): float(r["net_modeled"]) for r in rep} + pr = [(int(r["ts"]), float(r["net_modeled"]), d[int(r["ts"])]) for r in rec if int(r["ts"]) in d] + A = np.array([p[1] for p in pr]) + B = np.array([p[2] for p in pr]) + T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pr], unit="ms", utc=True)) + eq = np.abs(A - B) <= 1e-6 + S[k]["pairs"] = (T, A, B) + gg = sorted({str(x.date()) for x in T[eq]}) + note = f" ({gg[0]} -> {gg[-1]})" if gg else "" + print(f" {lab:<18} {int(eq.sum())}/{len(A)} identiche{note}") + if k == "statarb": + print(" quelle 6 sono le barre recuperate dopo l'incidente del feed 09-15/07:") + print(" registrate il 16/07 e riprodotte oggi al bit dopo ~37 riscritture") + print(" notturne -> conferma indipendente della premessa della sezione 0.") + if k == "dvol": + print(" *** ZERO barre non affette: per questo monitor la prova I2 NON ESISTE.") + print(" Restano I1 (pannello identico, max|d|=0), I3 (troncamento innocuo) e il") + print(" fatto strutturale che il replay chiama `advance()` di produzione senza") + print(" una riga riscritta. Va detto: e' una prova piu' debole delle altre due.") + return S + + +# ============================================================================================ +# 2 — I TRE GATE, ALLA LETTERA +# ============================================================================================ +def _riga(lab, m, extra=""): + print(f" {lab:<22} n={m['n']:>3} Sharpe {m['sh']:+7.2f} maxDD {m['dd'] * 100:6.2f}% " + f"ritorno {m['tot'] * 100:+6.2f}% vol {m['sd'] * 100:5.2f}%{extra}") + + +def gate_dvolspread(S: dict) -> tuple[str, str]: + print("\n" + H) + print(" 2a — DVOLSPREAD (kill 2026-10-24, decisione 2027-01-24) — PER PRIMO: e' l'unico dei") + print(" tre che ha gia' un VETO D'INTEGRITA', e la domanda e' se il difetto lo fa scattare") + print(H) + pv, rec, rep = S["dvol"]["mod"], S["dvol"]["rec"], S["dvol"]["rep"] + A = np.array([float(r["net_modeled"]) for r in rec]) + B = np.array([float(r["net_modeled"]) for r in rep])[:-1] # RIPARATA = solo barre chiuse + mA, mB = mets(A), mets(B) + print(f"\n criterio kill (pre-registrato 26/07): Sharpe forward MODELED < {pv.KILL_SHARPE:+.2f}" + f" -> RITIRO") + _riga("serie ATTUALE", mA) + _riga("serie RIPARATA", mB) + vA = "KILL" if mA["sh"] < pv.KILL_SHARPE else "vivo" + vB = "KILL" if mB["sh"] < pv.KILL_SHARPE else "vivo" + print(f" -> criterio kill: serie attuale {vA} · serie riparata {vB} " + f"{'STESSO VERDETTO' if vA == vB else '*** VERDETTO RIBALTATO ***'}") + print(f" ⚠ ma a n={mA['n']} barre SE(Sharpe) ~ {se_sharpe(mA['n']):.2f}: nessuna delle due") + print(" e' una lettura di gate, e la data del kill e' fra " + f"{(date(2026, 10, 24) - TODAY).days} giorni.") + + print("\n IL VETO D'INTEGRITA' (`n_active + n_flat_signal >= 80% delle barre`):") + stA, stB = S["dvol"]["st_rec"], S["dvol"]["st_rep"] + fA = (stA["n_active"] + stA["n_flat_signal"]) / max(stA["n_bars"], 1) + fB = (stB["n_active"] + stB["n_flat_signal"]) / max(stB["n_bars"], 1) + print(f" serie ATTUALE : attive {stA['n_active']} · flat-da-segnale {stA['n_flat_signal']}" + f" · flat-SENZA-DATO {stA['n_flat_nodata']} -> valide {fA:.0%} " + f"(soglia {pv.MIN_ACTIVE_FRAC:.0%}) -> {'VETO' if fA < pv.MIN_ACTIVE_FRAC else 'PASSA'}") + print(f" serie RIPARATA: attive {stB['n_active']} · flat-da-segnale {stB['n_flat_signal']}" + f" · flat-SENZA-DATO {stB['n_flat_nodata']} -> valide {fB:.0%} -> " + f"{'VETO' if fB < pv.MIN_ACTIVE_FRAC else 'PASSA'}") + print(""" + 📌 IL RISULTATO E' SUL VETO, NON SUL MONITOR. Una serie che misura DUE MINUTI di mercato al + giorno passa al 100% un veto d'integrita' progettato apposta per non far decidere su dati + mancanti. Non e' un difetto di taratura: e' che il veto e' costruito sulla domanda giusta + per un altro guasto. Conta *se il DVOL c'era* (e c'era, 28/28) e non puo' vedere *quanto + dura la barra su cui c'era* — sono due assi ortogonali. La stessa soglia 0.80 applicata alla + quota di barre CHIUSE leggerebbe 0/28 = 0% e vieterebbe la decisione. + E' la stessa forma gia' pagata dal progetto tre volte: 'una riga presente non e' un dato + presente'. Qui la variante nuova e': *una barra presente non e' una GIORNATA presente*.""") + + print("\n criteri (b) marginale ADDS / (c) deflated-Sharpe / (d) weights_tilt_null: NON GIRATI,") + print(" e il motivo e' una misura, non pigrizia: si valutano sul campione ESTESO (in-sample") + print(f" attivo 1949 barre + forward). Oggi il forward pesa {mA['n']}/{1949 + mA['n']} = " + f"{100 * mA['n'] / (1949 + mA['n']):.1f}% del campione; alla decisione del 24/01/2027 ne") + print(" pesera' ~8%. -> il difetto NON puo' ribaltare (c): puo' ribaltare solo (a), che e'") + print(" calcolato sulla sola finestra forward ed e' interamente determinato da lui.") + return vA, vB + + +def gate_statarb(S: dict) -> tuple[str, str]: + print("\n" + H) + print(" 2b — STATARB (decisione 2026-09-27) — il gate su cui il difetto MORDE") + print(H) + rec, rep = S["statarb"]["rec"], S["statarb"]["rep"] + gate = importlib.import_module("r0724_statarb_deploy_gate") + A = np.array([float(r["net_modeled"]) for r in rec]) + Ar = np.array([float(r["net_real"]) for r in rec]) + B = np.array([float(r["net_modeled"]) for r in rep])[:-1] + Br = np.array([float(r["net_real"]) for r in rep])[:-1] + mA, mB = mets(A), mets(B) + hc = lambda m, r: abs(float((np.prod(1 + m) - np.prod(1 + r)) * 100)) + hA, hB = hc(A, Ar), hc(B, Br) + print(f"\n soglie pre-registrate 24/07: Sharpe >= {gate.SH_DEPLOY} · maxDD < {gate.DD_MAX:.0%}" + f" · fill-haircut < {gate.HAIRCUT_MAX_PP} pp") + _riga("serie ATTUALE", mA, f" haircut {hA:.3f} pp") + _riga("serie RIPARATA", mB, f" haircut {hB:.3f} pp") + + def verdetto(sh, dd, h): + if sh >= gate.SH_DEPLOY and dd < gate.DD_MAX and h < gate.HAIRCUT_MAX_PP: + return "CANDIDATO AL DEPLOY" + if sh >= 0: + return "ESTENSIONE (unica, al 2026-12-26)" + return "RITIRO" + vA, vB = verdetto(mA["sh"], mA["dd"], hA), verdetto(mB["sh"], mB["dd"], hB) + for lab, sh, dd, h, v in (("ATTUALE", mA["sh"], mA["dd"], hA, vA), + ("RIPARATA", mB["sh"], mB["dd"], hB, vB)): + print(f" {lab:<9} Sharpe>={gate.SH_DEPLOY}: {'SI' if sh >= gate.SH_DEPLOY else 'NO':<3}" + f" | maxDD<{gate.DD_MAX:.0%}: {'SI' if dd < gate.DD_MAX else 'NO':<3}" + f" | haircut<{gate.HAIRCUT_MAX_PP}pp: {'SI' if h < gate.HAIRCUT_MAX_PP else 'NO':<3}" + f" -> {v}") + print(f"\n -> DUE CRITERI SU TRE SI RIBALTANO. Sharpe {mA['sh']:+.2f} -> {mB['sh']:+.2f} " + f"(attraversa 0 e la soglia 0.5); maxDD {mA['dd']:.1%} -> {mB['dd']:.1%} (attraversa la") + print(" guardia del 10%). Il terzo (haircut) non si ribalta e non poteva: e' un") + print(" rapporto fra due libri che subiscono lo STESSO troncamento.") + + print("\n DIAGNOSTICA PRE-REGISTRATA IL 25/07 (statica sempre-short a pari vol-target):") + bA = gate._bench_static_short(rec) + bB = gate._bench_static_short(rep[:-1]) + if bA and bB: + print(f" benchmark sulla finestra ATTUALE : {bA[0]:+.2f} su {bA[1]} barre") + print(f" benchmark sulla finestra RIPARATA: {bB[0]:+.2f} su {bB[1]} barre") + print(f" STATARB - benchmark: attuale {mA['sh'] - bA[0]:+.2f} " + f"riparata {mB['sh'] - bB[0]:+.2f}") + print(""" + 📌 ASIMMETRIA CHE VALE PIU' DEI NUMERI: il benchmark NON e' affetto dal difetto — e' + ricalcolato dal pannello, non letto dalla serie registrata (varia di %+.2f fra le due + finestre, contro %+.2f della strategia). Alla data del gate l'operatore metterebbe quindi + a confronto un benchmark GIUSTO con un numero di strategia SBAGLIATO, e la diagnostica + sembrerebbe piu' favorevole di quanto sia: +%.2f invece di +%.2f di margine. + Il segno del margine sopravvive comunque (STATARB batte la statica in entrambe le lenti): + cio' che il difetto ribalta e' il gate, non questa diagnostica.""" % + (bB[0] - bA[0], mB["sh"] - mA["sh"], mA["sh"] - bA[0], mB["sh"] - bB[0])) + else: + print(" benchmark NON calcolabile -> non girato.") + return vA, vB + + +def gate_xsr(S: dict) -> tuple[str, str]: + print("\n" + H) + print(" 2c — XSR01 (decisione 2026-10-23)") + print(H) + rec, rep = S["xsr"]["rec"], S["xsr"]["rep"] + gate = importlib.import_module("r0725_xsr_deploy_gate") + k5 = "net_REAL-$5000" + A = np.array([float(r["net_modeled"]) for r in rec]) + A5 = np.array([float(r[k5]) for r in rec]) + B = np.array([float(r["net_modeled"]) for r in rep])[:-1] + B5 = np.array([float(r[k5]) for r in rep])[:-1] + mA, mB = mets(A), mets(B) + + def haircut(m, r5): + tm = float(np.prod(1 + m) - 1) + t5 = float(np.prod(1 + r5) - 1) + return (tm - t5) / abs(tm) if tm != 0 else float("nan") + hA, hB = haircut(A, A5), haircut(B, B5) + print(f"\n soglie pre-registrate 25/07: Sharpe >= {gate.SH_DEPLOY} E haircut$5000 <= " + f"{gate.HAIRCUT_MAX:.0%} · ritiro sotto Sharpe {gate.SH_RETIRE}") + print(f" ⚠ se l'haircut sfonda -> RITIRO a prescindere dallo Sharpe (regola esplicita)") + _riga("serie ATTUALE", mA, f" haircut$5000 {hA * 100:+.1f}%") + _riga("serie RIPARATA", mB, f" haircut$5000 {hB * 100:+.1f}%") + + def verdetto(sh, h): + if np.isfinite(h) and h > gate.HAIRCUT_MAX: + return "RITIRO (haircut)" + if sh >= gate.SH_DEPLOY: + return "CANDIDATO SLEEVE" + if sh >= gate.SH_RETIRE: + return "ESTENSIONE (unica, al 2027-01-21)" + return "RITIRO" + vA, vB = verdetto(mA["sh"], hA), verdetto(mB["sh"], hB) + print(f" ATTUALE -> {vA}\n RIPARATA -> {vB} " + f"{'STESSO VERDETTO' if vA == vB else '*** VERDETTO RIBALTATO ***'}") + print(f"\n -> il verdetto NON si ribalta, ma per un caso: entrambe stanno sotto " + f"{gate.SH_RETIRE}. Il") + print(f" numero pero' cambia di {abs(mA['sh'] / mB['sh']):.1f}x ({mA['sh']:+.2f} contro " + f"{mB['sh']:+.2f}), e con SE ~ {se_sharpe(mA['n']):.1f} nessuno dei due decide niente.") + print(" L'haircut e' l'unico criterio robusto al difetto (rapporto fra libri troncati") + print(" allo stesso modo) ed e' lontanissimo dalla guardia in entrambe le lenti.") + return vA, vB + + +# ============================================================================================ +# 3 — IL MECCANISMO: perche' il troncamento non e' rumore +# ============================================================================================ +def sezione_3(S: dict) -> None: + print("\n" + H) + print(" 3 — PERCHE' IL DIFETTO NON E' RUMORE: comprime la VOL piu' della MEDIA") + print(H) + print(""" + Un troncamento a k minuti su 1440 scala la media del rendimento circa come k e la deviazione + standard come sqrt(k) (random walk). Quindi il rapporto media/sd — cioe' lo Sharpe — viene + MOLTIPLICATO per ~sqrt(k/1440)... in valore assoluto no: il numeratore e' la media del pezzo + troncato, che non ha lo stesso SEGNO della media della giornata intera. Risultato: la serie + registrata produce letture di Sharpe piu' ESTREME e in una direzione che non e' quella vera. + Si vede in tabella: la vol registrata e' una frazione di quella vera, ma il |Sharpe| non e' piu' + piccolo — in due casi su tre e' molto piu' GRANDE.""") + print(f"\n {'monitor':<20} {'vol REG':>9} {'vol RIP':>9} {'min/giorno':>11} " + f"{'Sh REG':>8} {'Sh RIP':>8} nota") + for k, lab in (("statarb", "paper_statarb"), ("xsr", "paper_xsr"), ("dvol", "paper_dvolspread")): + T, A, B = S[k]["pairs"] + Bc = B[:-1] if len(B) == len(A) else B + # ⚠ l'INTERA finestra dell'incidente del feed (09-16/07) va esclusa dalla stima dei + # minuti/giorno, non le sole barre risultate identiche: durante il blocco il monitor non + # trovava barre nuove e al ripristino ne ha recuperate 7 in un colpo, alcune solo + # PARZIALMENTE riallineate. Escludendo le sole identiche la stima esce 4x piu' alta + # (16,7 invece di 3,9 min/giorno su statarb) — e' la stessa maschera del 22/08. + keep = ~((T >= FREEZE[0]) & (T <= FREEZE[1])) + mA, mB = mets(A), mets(Bc) + kA, kB = mets(A[keep]), mets(B[:len(A)][keep]) + minuti = (kA["sd"] / kB["sd"]) ** 2 * 1440.0 + nota = ("SEGNO OPPOSTO" if np.sign(mA["sh"]) != np.sign(mB["sh"]) + else f"|Sh| x{abs(mA['sh']) / abs(mB['sh']):.1f}") + print(f" {lab:<20} {mA['sd'] * 100:8.2f}% {mB['sd'] * 100:8.2f}% {minuti:10.1f} " + f"{mA['sh']:+8.2f} {mB['sh']:+8.2f} {nota}" + + (f" ({int((~keep).sum())} barre dell'incidente escluse)" if (~keep).any() else "")) + print(""" + Il caso peggiore NON e' quello col |Sharpe| piu' gonfiato: e' `paper_statarb`, dove il + |Sharpe| e' quasi identico (1,96 contro 2,02) e cambia il SEGNO. Un controllo che cercasse + "numeri troppo belli" lo lascerebbe passare senza una parola: e' proprio il monitor che + alimenta il gate piu' vicino. + -> la firma da cercare non e' un |Sharpe| alto ma una VOL FORWARD molto sotto quella del + backtest: 7,9% contro ~31%, 0,5% contro ~2,8%, 0,8% contro ~19%. + ⚠ i "min/giorno" sono una LETTURA del rapporto di varianza (assume random walk + sull'intervallo troncato), non un cronometro, e sono sensibili alla maschera: su statarb + 3,9 escludendo l'incidente del feed, 16,7 escludendo le sole barre identiche, 92 + includendo tutto. Si cita la prima, che e' quella del 22/08; l'ordine di grandezza e' + quello, la cifra no. + ✅ Replica indipendente del filone MONITOR-AUDIT: 3,9 / 41,2 / 2,5 contro i 4 / 41 / 2 + pubblicati stamattina, per un percorso scritto separatamente.""") + + print("\n `implausible_sharpe` puntato sulle serie forward (mai fatto — nota del 22/08).") + print(" ⚠ il gate chiede n>=30: due serie forward su tre NON sono giudicabili, e questo") + print(" e' esso stesso un risultato — il gate esiste dal 26/07 ma non puo' sorvegliare") + print(" una finestra forward giovane, che e' esattamente quando serve.") + import altlib as al + for k, lab in (("statarb", "paper_statarb"), ("xsr", "paper_xsr"), ("dvol", "paper_dvolspread")): + T, A, B = S[k]["pairs"] + for nome, v in (("REGISTRATA", A), ("RIPARATA", B[:-1] if len(B) == len(A) else B)): + idx = T[:len(v)] + try: + r = al.implausible_sharpe(pd.Series(v, index=idx)) + corto = any("troppo corto" in x for x in r["reasons"]) + flag = ("SEGNALA" if r["implausible"] + else "n/giudic." if corto else "ok") + print(f" {lab:<18} {nome:<11} {flag:<10} {'; '.join(r['reasons'])[:66]}") + except Exception as e: # noqa: BLE001 + print(f" {lab:<18} {nome:<11} non girato ({type(e).__name__})") + + +# ============================================================================================ +# 4 — QUANTO COSTA ASPETTARE +# ============================================================================================ +GATES = ( + ("STATARB", "statarb", date(2026, 9, 27)), + ("XSR01", "xsr", date(2026, 10, 23)), + ("DVOLSPREAD", "dvol", date(2026, 10, 24)), +) + + +def sezione_4(S: dict) -> None: + print("\n" + H) + print(" 4 — QUANTO COSTA ASPETTARE (l'unico argomento quantitativo su QUANDO riparare)") + print(H) + print(""" + Due scenari, e la differenza fra loro e' tutto il contenuto della sezione: + RIPARA+RIGENERA : si corregge `advance()` e si RICALCOLA la finestra dall'inception. + Possibile perche' le barre chiuse non vengono riscritte (sezione 0). + RIPARA IN AVANTI : si corregge `advance()` e basta. Le barre gia' scritte restano sbagliate.""") + print(f"\n {'gate':<12} {'data':<12} {'barre al gate':>14} {'oggi':>6} " + f"{'corrette se si ripara OGGI':>28} {'se si ripara alla vigilia':>27}") + for nome, key, gd in GATES: + rec = S[key]["rec"] + first = pd.Timestamp(rec[0]["dt"][:10]).date() + n_gate = (gd - first).days + 1 + n_oggi = len(rec) + print(f" {nome:<12} {str(gd):<12} {n_gate:>14} {n_oggi:>6} " + f"{f'{n_gate - n_oggi} ({(n_gate - n_oggi) / n_gate:.0%})':>28} " + f"{f'1 ({1 / n_gate:.0%})':>27}") + print("\n Con RIGENERAZIONE la colonna giusta e' un'altra: 100% a QUALUNQUE data di") + print(" riparazione. Il costo dell'attesa e' ZERO barre — cio' che si paga aspettando non") + print(" e' dato perso ma il RISCHIO che la riparazione non arrivi prima del gate.") + print(" Senza rigenerazione, invece, riparare oggi lascia comunque sbagliata la maggioranza") + print(" della finestra di STATARB, e riparare alla vigilia non serve a niente.") + + print("\n SENSIBILITA' — quanto deve essere contaminata la finestra perche' il verdetto") + print(" cambi. Misurata sui dati di OGGI, non estrapolata: si costruisce la serie mista") + print(" `registrate[:k] + riparate[k:]` (= riparazione in avanti dal giorno k) e si legge") + print(" il criterio. k=0 e' la rigenerazione completa, k=n e' la serie di oggi.") + gate = importlib.import_module("r0724_statarb_deploy_gate") + rec, rep = S["statarb"]["rec"], S["statarb"]["rep"] + A = np.array([float(r["net_modeled"]) for r in rec])[:-1] + B = np.array([float(r["net_modeled"]) for r in rep])[:-1] + n = len(A) + print(f"\n STATARB — {n} barre chiuse; soglia deploy {gate.SH_DEPLOY}, ritiro sotto 0, " + f"guardia maxDD {gate.DD_MAX:.0%}") + print(f" {'corrette (coda)':>16} {'contaminate':>12} {'Sharpe':>8} {'maxDD':>8} verdetto") + verd = [] + for k in range(0, n + 1): + m = mets(np.concatenate([A[:k], B[k:]])) + v = ("CANDIDATO" if (m["sh"] >= gate.SH_DEPLOY and m["dd"] < gate.DD_MAX) + else "ESTENSIONE" if m["sh"] >= 0 else "RITIRO") + verd.append((k, m, v)) + # minimo numero di barre CORRETTE in coda che basta a leggere gia' il verdetto riparato + v_rip = verd[0][2] + min_ok = min((n - k for k, m, v in verd if v == v_rip), default=n) + for k, m, v in verd: + if k in (0, n // 4, n // 2, (3 * n) // 4, n - min_ok, n - min_ok + 1, n): + print(f" {n - k:>16} {f'{k} ({k / n:.0%})':>12} {m['sh']:+8.2f} " + f"{m['dd'] * 100:7.2f}% {v}") + n_res = 90 - len(S["statarb"]["rec"]) # barre che restano da qui al gate del 27/09 + print(f""" + 📌 LA RIPARAZIONE PARZIALE NON E' PROPORZIONALE. Su questo campione bastano + **{min_ok} barre corrette in coda su {n}** ({min_ok / n:.0%}) perche' il criterio legga gia' il + verdetto della serie riparata. Il meccanismo e' quello della sezione 3: una barra troncata a + ~4 minuti su 1440 porta ~1/370 della varianza E ~1/370 del rendimento di una barra vera + (entrambi scalano con la frazione di giornata), quindi pesa quasi nulla sia nella media sia + nella deviazione della miscela. La maggioranza NUMERICA delle barre non comanda. + Detto meglio, ed e' la formulazione onesta: riparare in avanti senza rigenerare non + CONTAMINA la finestra, la ACCORCIA — il criterio finisce per leggere la strategia sulle sole + barre corrette. Riparando oggi, al 27/09 STATARB avrebbe {n_res} barre vere su 90: una + finestra piu' corta ma VERA, con SE(Sharpe) ~{se_sharpe(n_res):.1f} invece di ~{se_sharpe(90):.1f}. + Riparare alla vigilia (1 barra) no: li' il numero resta quello di oggi. + ⚠ Il {min_ok}/{n} NON e' una costante — dipende da quali barre capitano in coda, e cambia col + campione: si legge come "poche", non come una soglia. E rigenerare resta strettamente + migliore, per due cose che l'accorciamento non da': un maxDD leggibile (la miscela ha un + percorso di equity che non e' mai esistito) e una finestra rileggibile con altri criteri.""") + + +# ============================================================================================ +def main() -> None: + print(H) + print(" GATE-RECON — cosa direbbero i tre gate pre-registrati con le serie riparate") + print(f" {TODAY} SOLA LETTURA su produzione (nessun file di data/ aperto in scrittura)") + print(H) + sezione_0() + S = sezione_1() + v_dv = gate_dvolspread(S) + v_st = gate_statarb(S) + v_xs = gate_xsr(S) + sezione_3(S) + sezione_4(S) + + print("\n" + H) + print(" RIEPILOGO — il DELTA, non la decisione (i gate hanno una data e non si anticipano)") + print(H) + print(f" STATARB 27/09 — serie riparata: {v_st[1]:<36} · serie attuale: {v_st[0]}") + print(f" XSR01 23/10 — serie riparata: {v_xs[1]:<36} · serie attuale: {v_xs[0]}") + print(f" DVOLSPREAD 24/10 — serie riparata: {v_dv[1]:<36} · serie attuale: {v_dv[0]}") + print("\n Nessuno di questi e' il verdetto del gate: sono i criteri letti OGGI, su una finestra") + print(" ancora incompleta. Servono a una sola decisione, che e' dell'operatore: riparare") + print(" `advance()` e RIGENERARE prima del 27/09, oppure sapere che quel giorno il criterio") + print(" verra' letto su una serie che misura 4 minuti di mercato al giorno.") + + +if __name__ == "__main__": + main() From ebc8cf7d7c2cb08279709562f1c348a53e67be6f Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 22:02:26 +0000 Subject: [PATCH 042/114] =?UTF-8?q?research(wave-0822):=20GATE=20PROP-01?= =?UTF-8?q?=20gamba=20(b)=20PASS=2023/23=20=E2=80=94=20e=20la=20distorsion?= =?UTF-8?q?e=20su=20SKH01=20dichiarata=20due=20volte=20e'=20refutata;=20P(?= =?UTF-8?q?50/g)=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() From 73ae21c955f903f3de616aaa854b194f8d0b5014 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 22:10:43 +0000 Subject: [PATCH 043/114] =?UTF-8?q?research(wave-0822):=20WORST-DAY=20?= =?UTF-8?q?=E2=80=94=20la=20riserva=20sulla=20leva=20e'=20mal=20tarata=20d?= =?UTF-8?q?i=202-4=20ordini=20di=20grandezza,=20ma=20il=20knob=20di=20scal?= =?UTF-8?q?a=20NON=20ESISTE;=20e=20il=20funding=20non=20e'=20in=20nessun?= =?UTF-8?q?=20backtest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 68 ++ scripts/research/r0822c_worst_day.py | 1112 ++++++++++++++++++++++++++ 2 files changed, 1180 insertions(+) create mode 100644 scripts/research/r0822c_worst_day.py diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 34a02ea..d7ed9f8 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -698,6 +698,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 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%** | +| 33 | WORST-DAY | il peggior giorno del libro, MISURATO: il gradino di leva e' autorizzabile? | **LEAD** — l'aritmetica passa, **il knob non esiste** | | 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 | @@ -1488,3 +1489,70 @@ misurato proprio come mediana-ancore/canonica del drift). A ×1,00 il delta sale **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. + +--- + +## 33 — WORST-DAY (il peggior giorno del libro, misurato invece che assunto) + +`scripts/research/r0822c_worst_day.py` (1.112 righe, **12 s**). **847 valutazioni**, soglia EVT +**dichiarata a priori** (95° pctl) e criterio congelato prima delle sezioni decisive → *non c'e' un +massimo scelto su cui barare, quindi non c'e' un DSR da calcolare*. + +Il filone che il critico dell'ondata aveva indicato come **la misura di maggior valore mai fatta**: +il gradino di leva **1,00x → 1,25-1,50x** vale **14,7 → 11,6 anni** al capitale-rendita (~€300/mese di +versamenti equivalenti, a costo zero) ed era bloccato da **un solo parametro scelto a mano**: +*"un giorno a −10% ogni anno"*. + +📌 **Il peggior giorno ha DUE risposte e differiscono di ~2×:** **−3,94% in chiusura** (2020-03-13) e +**−7,33% al minimo intra-giorno** (mediana del rapporto sui giorni in perdita **1,89×**). + +⚠️ **E il critico va CORRETTO nel meccanismo, non solo confermato.** Sui 10 giorni peggiori del +sottostante il libro fa **−2,34%** contro **−182,1%** del mercato (guadagna in 4/10; il 12 marzo 2020, +−43,6%, fa **+3,80%**) — **ma sui 160 giorni di crash perde in media −0,505%**, e l'esposizione di +TP01 scende monotona col crollo (0,096 nella fascia −5/−2% → **0,042 sotto il −20%**). +**Il libro non e' immune ai crash: e' immune ai crash GROSSI, perche' li' il trend si e' gia' girato.** +E il suo peggior giorno **non e' un crash: e' uno short squeeze** — SKH01 short con stop 2% che gappa a +**−15,77%** di sleeve mentre TP01 e' flat (secondo peggiore −6,67% → **la coda di SKH01 e' un giorno +solo**). + +**La coda stimata** (GPD, IC95 bootstrap a blocchi): chiusura **−2,90% a 1/1.000 giorni**, **−4,55% a +1/10.000** [−6,34 / −3,08%]. ⚠️ **Hill dice −7,34%** (ξ 0,34 contro 0,09): **i due stimatori non +concordano → si cita la banda**, non il numero. **Il conto ARITMETICO** (cap + vol-target, che +converte un crash nel suo z-score rendendo il bound invariante alla vol, + gap-through di SKH01): +peggior giorno **possibile** a k=1,00x = **−14,32% in chiusura**. +📌 **Quindi il −10% sta al 70% del massimo strutturale: la TAGLIA e' plausibile, la FREQUENZA no** +(1 ogni 67-6.075 anni contro "ogni anno"). **La riserva e' ridimensionata di 2-4 ordini di grandezza, +non falsificata.** +📌 **E la forma della coda non e' la leva:** muove k* di 1-2 unita', **il drift di 6,7**. Anche +prendendo lo stress a mano **alla lettera**: k* 2,95x → mezzo-Kelly **1,48x** → *lo stress bocciava il +gradino DOPPIO, non quello singolo*. **C1-C5 passano tutti** (margine strutturale 2,8×, liquidazione +4×, guadagno a drift −2SE +2,01%/anno). + +🚨 **IL RISULTATO CHE NESSUNO CERCAVA, ed e' quello che decide: il cap di `config/live.json` e' un +CLAMP, NON un moltiplicatore.** Morde su **3 osservazioni-asset su 5.650 (0,053%)**: alzarlo da 0,50 a +0,625 lascia il libro a k=1,00x in **5.647 giorni-asset su 5.650**. ✅ **Verificato dal coordinatore:** +le chiavi di `live.json` sono `max_notional_per_asset_usd/_frac`, `min_order_usd`, `disaster_sl_pct`, +`max_data_age_days`, `skh_feed_max_age_min` — **non esiste una chiave di scala**; servirebbe toccare +`WEIGHT`/`W_TP01`/`W_SKH` in `src/live/book.py`, cioe' **codice** su un percorso con soldi veri. +**La regola del progetto — *"ogni cambio di SCALA passa dal cap di config, non da `target_vol`"* — non +e' implementabile come scritta.** *Due mesi di discussione su un knob inesistente.* + +🚨 **SOTTOPRODOTTO CHE TOCCA OGNI NUMERO DEL PROGETTO: il funding dei perpetual non e' modellato in +NESSUN backtest.** ✅ **Verificato dal coordinatore:** zero occorrenze di `funding` in +`src/backtest/harness.py`, `src/strategies/trend_portfolio.py`, `src/portfolio/{sleeves,portfolio}.py` +**e in tutto `src/live/`**. Proxy Hyperliquid 14,8%/anno × esposizione long media di TP01 0,103·E = +**~1,53%/anno che il libro paga davvero** = **il 9% del drift de-luckato**, cioe' **lo stesso ordine +della correzione d'ancora ×0,89** che il progetto ha misurato con cura. + +📌 **Sottoprodotto forense: la lente del critico si IDENTIFICA dai valori che ha pubblicato** (+0,97 e ++0,72 compaiono **solo** nella riga canonica) → aveva usato `book_series('canonical')`, e **su quella +lente il peggior giorno E' un giorno di ribasso** (−3,47%, 2025-10-10). *Aveva ragione, con la lente +sbagliata.* + +**VERDETTO: LEAD** (misura di politica, non strategia). +**`IL GRADINO DI LEVA A 1,25x: AUTORIZZABILE A CONDIZIONE DI` costruire prima una chiave di scala +esplicita in config, e di rifare `r0726_fee_sensitivity`** — la cui conclusione *"liquidation fee +irrilevante"* **vale solo a nozionale lordo ≤1x**, ed era gia' legata a un test di guardia sul cap. +⚠️ **Nota dell'agente, dovuta:** se si credono **entrambe** le riserve insieme (drift −2SE **e** −10% +annuo) il drift atteso e' **−0,61%/anno** → *a quel punto non e' il gradino a essere sbagliato, e' il +libro, e chi usa quella combinazione sta chiedendo di spegnere anche il conto di oggi.* diff --git a/scripts/research/r0822c_worst_day.py b/scripts/research/r0822c_worst_day.py new file mode 100644 index 0000000..b969731 --- /dev/null +++ b/scripts/research/r0822c_worst_day.py @@ -0,0 +1,1112 @@ +#!/usr/bin/env python +"""r0822c_worst_day.py — IL PEGGIOR GIORNO CHE IL LIBRO PUO' AVERE, misurato invece che assunto. + +PERCHE' ESISTE QUESTO FILE. Il gradino di leva 1,00x -> 1,25-1,50x vale 14,7 anni -> 12,9-11,6 +al capitale-rendita (~EUR300/mese di versamenti equivalenti, a costo zero) ed e' bloccato da UN +solo parametro SCELTO A MANO in `r0822_growth_policy.py` sez. (4): *"un giorno -10% all'anno +porta k* a 2x"*. Nessuno ha mai misurato quel numero. Il critico dell'ondata ha fatto un +controllo di cinque minuti (sui 10 peggiori giorni del SOTTOSTANTE il libro guadagna, e il suo +peggior giorno non e' un giorno di crash) e ha indicato la misura come quella di maggior valore +mai fatta. Qui si VERIFICA o si SMENTISCE quel controllo, e si sostituisce il -10% con una stima. + +COSA C'E' QUI + (0) IL CRITERIO, dichiarato PRIMA di guardare qualunque numero delle sezioni 4-6. + (1) REPLICA DI CONTROLLO — 5 numeri gia' pubblicati, riprodotti prima di scriverne di nuovi. + (2) LA DISTRIBUZIONE DELLE CHIUSURE del libro: coda sinistra per quantili, il peggiore + assoluto, QUALI giorni sono, e l'attribuzione esatta ai due sleeve (max|diff| = 0). + (3) LA LENTE ACCOPPIATA — il minimo INTRA-giorno (`r0725_prop_coupled`). La domanda "qual e' + il peggior giorno" ha DUE risposte e servono entrambe: la leva agisce su tutte e due, ma + il margine di mantenimento guarda il minimo. + (4) LA CODA CHE NON E' NEL DATASET — EVT/POT (GPD sopra soglia dichiarata) su chiusure E su + minimi, quantili a 1/1.000 e 1/10.000 giorni, IC bootstrap A BLOCCHI sui parametri. + (5) IL PEGGIOR GIORNO POSSIBILE PER COSTRUZIONE — aritmetica, non statistica: dato il cap di + `config/live.json`, il vol-target di TP01 e il gap-through misurato di SKH01. + (6) k* ONESTO — la curva di Kelly rifatta con la coda stimata al posto di quella a mano, e la + decomposizione dell'errore del -10% (taglia vs FREQUENZA). + (7) IL VERDETTO contro il criterio di (0). + +ONESTA' APPLICATA (regole gia' codificate nel progetto): + · un fattore correttivo scelto a occhio va DECOMPOSTO prima di accettarlo o rifiutarlo (26/07); + · close-only e' CIECA su una regola a UN giorno (25/07) -> la lente accoppiata e' obbligatoria; + · un fit di coda su ~2.700 osservazioni ha incertezza grande: si riporta, o si sostituisce un + numero scelto a mano con uno finto-preciso, che e' peggio; + · un Monte Carlo ha una risoluzione: qui la curva di crescita e' calcolata in forma ESATTA + (E[log]) proprio per togliere di mezzo il rumore MC dello scetticato. + + nice -n 19 timeout 900 uv run python scripts/research/r0822c_worst_day.py +""" +from __future__ import annotations + +import json +import sys +import time +from pathlib import Path + +import warnings + +import numpy as np +import pandas as pd +from scipy import stats + +warnings.filterwarnings("ignore", message=".*Sorting by default when concatenating.*") + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +import r0725_capcurve as CC # noqa: E402 book_series (lente LIVE), TAX_RATE +import r0725_prop_coupled as PC # noqa: E402 recon accoppiata (R, m) +import r0726_deposits as DP # noqa: E402 DELUCK = 0.89 MISURATO +import r0724_skh_live_weight as SKL # noqa: E402 gamba SKH01 sul path LIVE + +from src.data.downloader import load_data # noqa: E402 +from src.strategies.trend_portfolio import ( # noqa: E402 + CANONICAL, TrendPortfolio, realized_vol, resample_1d, simple_returns) +from src.strategies.skyhook import SKH01_V2_DD # noqa: E402 + +ASSETS = ("BTC", "ETH") +W_TP, W_SKH = 0.75, 0.25 +ANN = 365.0 +SEED = 20260822 + +# ---- parametri EVT DICHIARATI PRIMA (criterio, non tuning) ------------------------------- +EVT_Q = 0.95 # soglia = 95° pctl delle PERDITE. Criterio dichiarato: la scelta + # classica del compromesso bias/varianza (~5% di eccedenze, qui 136 + # osservazioni). Le altre soglie sono girate come SENSITIVITA', non + # per scegliere la migliore: se il numero si muove molto, si dice. +EVT_Q_SENS = (0.90, 0.95, 0.975, 0.99) +EVT_BOOT = 400 # ricampionamenti a BLOCCHI (la serie ha vol-clustering: un bootstrap +EVT_BLOCK = 20 # iid stringerebbe l'IC di una coda che non e' iid) +RP = (1_000, 10_000) # periodi di ritorno in GIORNI (2,7 anni e 27 anni) + +# ---- il numero sotto esame ---------------------------------------------------------------- +STRESS_A_MANO = 0.10 # il "-10% in un giorno" di r0822_growth_policy sez.(4) +STRESS_FREQ_A_MANO = 1.0 # ...assunto UNA VOLTA L'ANNO (e 1 ogni 5 anni come variante) + +# ---- costanti di venue (dichiarate, fonte in linea) -------------------------------------- +MM_RATE = 0.01 # maintenance margin ~1% (Deribit perp lineari, valore prudente: + # il listino parte da 0,5% e cresce con la size; 1% e' conservativo + # NEL VERSO che rende la liquidazione PIU' vicina) +WIPE = 0.90 # convenzione ereditata da growth_policy/skeptic: perdita giornaliera + # >= 90% dell'equity = conto chiuso. Usata solo per replicare i loro numeri. + + +# ========================================================================================== +# (0) IL CRITERIO — congelato qui, valutato in (7). Nessuna delle sue soglie usa un numero +# delle sezioni 4-6: vengono dalle regole gia' scritte del progetto e dal listino del venue. +# ========================================================================================== +CRITERIO = { + "C1": ("STRUTTURALE — il peggior giorno POSSIBILE per costruzione, a leva k, deve lasciare " + "in piedi almeno META' dell'equity: k * |L_strutturale| <= 0.50. " + "(Ragione: zero e' assorbente e il conto e' unico; sotto meta' equity il piano non " + "e' 'in ritardo', e' finito.)"), + "C2": ("KELLY PESSIMISTICO — k <= 1/2 * k*(drift a -2 errori standard E coda troncata al " + "bound strutturale). Il mezzo-Kelly e' la prescrizione classica ed e' scelta A PRIORI, " + "non tarata su questo risultato; il -2 SE viene dalla fragilita' gia' misurata " + "(r0822_growth_policy sez. 1-bis: k* e' LINEARE nell'errore del drift)."), + "C3": ("LIQUIDAZIONE SUL MINIMO — con la lente ACCOPPIATA, il quantile a 1/10.000 giorni del " + "minimo INTRA-giorno a leva k deve stare almeno 3x lontano dalla soglia di " + "liquidazione del venue. (close-only e' CIECA su una regola a un giorno: 25/07.)"), + "C4": ("GUADAGNO ROBUSTO — g(k) - g(1.00) deve restare POSITIVO col drift a -2 SE. " + "Se il gradino paga solo al drift di punto, non e' un gradino, e' una scommessa " + "sulla stima del drift."), + "C5": ("COSTI NON MODELLATI — il costo che scala con k e non e' nei backtest (funding dei " + "perpetual, costo del margine oltre 1x) deve essere piccolo rispetto al guadagno."), + "C6": ("PROCESSO — deve ESISTERE una strada pulita per un puro cambio di SCALA. Regola del " + "progetto: passa dal cap di config, non da `target_vol` (che e' anche un tilt di pesi " + "e fallisce `weights_tilt_null`). Se quella strada non esiste, il gradino non e' " + "autorizzabile *nella forma in cui e' stato proposto*, qualunque cosa dicano C1-C5."), +} + + +def sezione(t: str) -> None: + print("\n" + "=" * 110) + print(f" {t}") + print("=" * 110) + + +def pct(x: float) -> str: + return f"{x:+.2%}" if np.isfinite(x) else " n/d" + + +# ========================================================================================== +# DATI +# ========================================================================================== +def serie_libro() -> dict: + """Serie giornaliere del libro live e le sue due gambe, + il sottostante.""" + book = CC.book_series("hourly") + skh = SKL.skh_book(0, "hourly") + if skh.index.tz is None: + skh.index = skh.index.tz_localize("UTC") + tp = CC.tp01_realistic(600.0 * W_TP) + und = {} + for a in ASSETS: + df = load_data(a, "1h") + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + s = pd.Series(df["close"].values.astype(float), index=idx).resample("1D").last() + und[a] = s.pct_change() + J = pd.concat({"book": book, "tp_leg": W_TP * tp, "skh_leg": W_SKH * skh, + "BTC": und["BTC"], "ETH": und["ETH"]}, axis=1, sort=True).dropna() + J["und"] = 0.5 * J["BTC"] + 0.5 * J["ETH"] + deluck = book - (1.0 - DP.DELUCK) * float(book.values.mean()) # = DP.deluck_returns(), con indice + return {"J": J, "deluck": deluck} + + +def gambe_tp01() -> dict: + """Per asset: peso TENUTO da TP01, ritorno giornaliero, vol nota a close[d-1], z-score. + + z = r / (vol_ann_prev/sqrt(365.25)) e' la grandezza che rende STRUTTURALE il bound di TP01: + il contributo di gamba e' `signal * (target_vol/vol) * r = signal * target_vol * z / sqrt(365.25)`, + cioe' INVARIANTE alla volatilita'. Un vol-target converte un crash in uno z-score. + """ + out = {} + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + c = df["close"].values.astype(float) + r = simple_returns(c) + tgt = np.nan_to_num(np.asarray(TrendPortfolio(**CANONICAL).target_series(df), float)) + held = np.zeros(len(tgt)) + held[1:] = tgt[:-1] + vol = realized_vol(r, CANONICAL["vol_win_days"], 365.25) + volp = np.concatenate(([np.nan], vol[:-1])) + with np.errstate(divide="ignore", invalid="ignore"): + z = r / (volp / np.sqrt(365.25)) + lev_max = np.clip(np.where(volp > 0, CANONICAL["target_vol"] / volp, 0.0), + 0.0, CANONICAL["leverage"]) + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + prev = np.concatenate(([np.nan], c[:-1])) + r_low = df["low"].values.astype(float) / prev - 1.0 # escursione avversa INTRA-giorno + out[a] = pd.DataFrame({"held": held, "ret": r, "ret_low": r_low, "vol_prev": volp, + "z": z, "lev_max": lev_max}, index=idx) + return out + + +def estremi_orari() -> dict: + """Movimento orario piu' avverso mai visto, per asset e per verso. E' il bound del + gap-through di SKH01: gli exit sono SOFTWARE e il fill arriva al close orario successivo + al tocco, quindi la perdita oltre lo stop e' l'escursione DENTRO quell'ora.""" + out = {} + for a in ASSETS: + c = load_data(a, "1h")["close"].values.astype(float) + r = c[1:] / c[:-1] - 1.0 + out[a] = {"giu": float(r.min()), "su": float(r.max())} + return out + + +# ========================================================================================== +# EVT / POT +# ========================================================================================== +def gpd_fit(loss: np.ndarray, q: float) -> dict: + """GPD (peaks-over-threshold) sulle PERDITE. `loss` = -ritorni. Ritorna anche le funzioni + di quantile e di probabilita' di eccedenza in forma chiusa.""" + n = len(loss) + u = float(np.quantile(loss, q)) + ex = loss[loss > u] - u + nu = len(ex) + if nu < 15: + return {} + xi, _, beta = stats.genpareto.fit(ex, floc=0.0) + return {"u": u, "n": n, "nu": nu, "xi": float(xi), "beta": float(beta), + "rate": nu / n} + + +def gpd_quantile(f: dict, p_giorno: float) -> float: + """Perdita superata con probabilita' `p_giorno` in un giorno (return level).""" + if not f: + return np.nan + xi, beta, u, rate = f["xi"], f["beta"], f["u"], f["rate"] + if abs(xi) < 1e-9: + return u + beta * np.log(rate / p_giorno) + return u + beta / xi * ((p_giorno / rate) ** (-xi) - 1.0) + + +def gpd_exceed(f: dict, x: float) -> float: + """P(perdita > x) al giorno, per x >= u.""" + if not f or x <= f["u"]: + return np.nan + xi, beta, u, rate = f["xi"], f["beta"], f["u"], f["rate"] + if abs(xi) < 1e-9: + return rate * np.exp(-(x - u) / beta) + base = 1.0 + xi * (x - u) / beta + if base <= 0: + return 0.0 + return rate * base ** (-1.0 / xi) + + +def evt_bootstrap(loss: np.ndarray, q: float, n_boot: int, block: int, + rng: np.random.Generator) -> dict: + """IC bootstrap A BLOCCHI su xi, beta e sui return level. A blocchi perche' la serie ha + vol-clustering: un bootstrap iid darebbe un IC piu' stretto del vero.""" + n = len(loss) + nb = int(np.ceil(n / block)) + xis, rl = [], {p: [] for p in RP} + for _ in range(n_boot): + st = rng.integers(0, n - block, size=nb) + idx = (st[:, None] + np.arange(block)[None, :]).ravel()[:n] + try: + f = gpd_fit(loss[idx], q) + except Exception: + continue + if not f: + continue + xis.append(f["xi"]) + for p in RP: + rl[p].append(gpd_quantile(f, 1.0 / p)) + return {"xi": np.array(xis), **{f"rl{p}": np.array(rl[p]) for p in RP}} + + +# ========================================================================================== +# CURVA DI CRESCITA — forma ESATTA (nessun rumore MC) +# ========================================================================================== +def g_exact(r: np.ndarray, k: float) -> float: + """Crescita geometrica annua a leva k: exp(365 * E[log(1+k r)]) - 1. + Su orizzonti lunghi la mediana del moltiplicatore converge a questa quantita': e' la + stessa grandezza che lo scetticato stima in Monte Carlo, qui in forma chiusa.""" + x = 1.0 + k * r + if np.any(x <= 0): + return -np.inf + return float(np.expm1(ANN * np.mean(np.log(x)))) + + +def k_star(r: np.ndarray, kmax: float = 40.0, step: float = 0.05) -> tuple[float, float]: + ks = np.arange(step, kmax + step, step) + g = np.array([g_exact(r, k) for k in ks]) + j = int(np.nanargmax(g)) + return float(ks[j]), float(g[j]) + + +def campione_semiparam(r: np.ndarray, f: dict, l_max: float, n_draw: int, + drift_annuo: float, rnd: dict) -> np.ndarray: + """Corpo EMPIRICO + coda GPD TRONCATA a `l_max`, ri-centrato al drift dichiarato. + + La troncatura non e' un artificio di comodo: una GPD con xi>0 ha supporto illimitato, quindi + E[log(1+kR)] sarebbe -inf per QUALSIASI k>0 e la domanda non avrebbe risposta. Il supporto + lo da' l'aritmetica della sezione (5), non la statistica: e' esattamente il posto in cui i + due pezzi di questo lavoro si incastrano. + """ + loss = -r + body = r[loss <= f["u"]] + out = body[rnd["ib"] % len(body)] # stessi indici in OGNI cella + m = rnd["um"] < f["rate"] # stessa maschera + nt = int(m.sum()) + if nt: + fmax = stats.genpareto.cdf(max(l_max - f["u"], 1e-9), f["xi"], loc=0.0, scale=f["beta"]) + y = stats.genpareto.ppf(rnd["ut"][:nt] * fmax, f["xi"], loc=0.0, scale=f["beta"]) + out = out.copy() + out[m] = -(f["u"] + y) + return out - out.mean() + drift_annuo / ANN + + +# ========================================================================================== +def main() -> None: + t_start = time.time() + rng = np.random.default_rng(SEED) + print("=" * 110) + print(" r0822c — WORST-DAY: il peggior giorno del libro, MISURATO (chiusura e minimo), e") + print(" la coda che non e' nel dataset, STIMATA invece che scelta a mano") + print("=" * 110) + + # ---------------------------------------------------------------- (0) + sezione("(0) IL CRITERIO — dichiarato PRIMA, valutato in (7)") + print("\n Il gradino di leva a 1,25x e' AUTORIZZABILE solo se passano TUTTE queste sei.") + print(" Nessuna soglia qui sotto usa un numero delle sezioni 4-6: vengono dalle regole") + print(" gia' scritte del progetto e dal listino del venue.\n") + for k, v in CRITERIO.items(): + testo = v.replace(" — ", " — ") + print(f" {k}. {testo}") + print() + + # ---------------------------------------------------------------- (1) + sezione("(1) REPLICA DI CONTROLLO — cinque numeri gia' pubblicati, riprodotti prima di scriverne di nuovi") + D = serie_libro() + J = D["J"] + TPQ_pre = gambe_tp01() # serve gia' nella sez.(2) per l'esposizione per bucket + book = J["book"].values.astype(float) + r_del = DP.deluck_returns() + mu, sig = float(r_del.mean()), float(r_del.std()) + se_drift = sig * np.sqrt(ANN) / np.sqrt(len(r_del) / ANN) + + print(f"\n serie: {len(J)} giorni, {J.index.min().date()} -> {J.index.max().date()}") + print(f"\n (a) GROWTH-POLICY sez.(0) — serie de-luckata x{DP.DELUCK}") + print(f" {'grandezza':>22}{'pubblicato':>14}{'qui':>14}") + for nome, pub, qui in (("drift/anno", "17,11%", f"{mu*ANN:.2%}"), + ("vol/anno", "11,37%", f"{sig*np.sqrt(ANN):.2%}"), + ("Sharpe", "1,51", f"{mu/sig*np.sqrt(ANN):.2f}"), + ("peggior giorno", "-3,95%", f"{r_del.min():.2%}")): + print(f" {nome:>22}{pub:>14}{qui:>14}") + + print(f"\n (b) SCOMPOSIZIONE ESATTA del libro nelle due gambe (non e' una stima: e' identita')") + diff = np.abs(J["book"].values - (J["tp_leg"].values + J["skh_leg"].values)).max() + print(f" max|book - (0.75*TP01 + 0.25*SKH01)| = {diff:.2e} <- 0 esatto, la gamba" + f" responsabile di ogni giorno e' LEGGIBILE") + + print(f"\n (c) k* — lo scetticato lo stima in Monte Carlo (banda 10-14x, 'la POSIZIONE non e'") + print(f" risolta'). Qui la stessa quantita' in forma CHIUSA, senza rumore MC:") + ks_emp, gs_emp = k_star(r_del) + print(f" {'Kelly gaussiano mu/sigma²':>34}{mu/sig**2:>10.2f}x") + print(f" {'k* ESATTO argmax E[log(1+kR)]':>34}{ks_emp:>10.2f}x (g = {gs_emp:.1%}/anno)") + print(f" {'MC dello scetticato':>34}{'10-14x':>10} <- la forma chiusa cade DENTRO") + print(f" {'g(1.00x)':>34}{g_exact(r_del,1.0):>10.2%}/anno") + print(f" -> replica superata: la macchineria esatta e quella MC dicono la stessa cosa,") + print(f" e da qui in avanti il rumore MC non e' piu' una scusa per nessun numero.") + + # ---- il controllo del critico, rifatto in modo indipendente + print(f"\n (d) IL CONTROLLO DEL CRITICO, verificato: i 10 peggiori giorni del SOTTOSTANTE") + print(f" (media 50/50 BTC-ETH) e cosa ha fatto il libro in quei giorni.") + worst_und = J.sort_values("und").head(10) + print(f"\n {'data':>12}{'BTC':>10}{'ETH':>10}{'sottost.':>10}{'LIBRO':>10}" + f"{' gamba TP01':>13}{'gamba SKH01':>13}") + for d, row in worst_und.iterrows(): + print(f" {str(d.date()):>12}{pct(row.BTC):>10}{pct(row.ETH):>10}{pct(row.und):>10}" + f"{pct(row.book):>10}{pct(row.tp_leg):>13}{pct(row.skh_leg):>13}") + print(f"\n somma dei 10 giorni: LIBRO {worst_und.book.sum():+.2%} " + f"contro sottostante {worst_und.und.sum():+.2%}") + print(f" giorni in cui il libro GUADAGNA: {int((worst_und.book>0).sum())}/10 · " + f"peggiore dei dieci {worst_und.book.min():+.2%}") + print(f"\n -> IL CONTROLLO DEL CRITICO E' CONFERMATO NELLA SOSTANZA. Il 12 marzo 2020") + print(f" (BTC {J.loc[J.und.idxmin(),'BTC']:+.1%}, ETH {J.loc[J.und.idxmin(),'ETH']:+.1%}) il libro fa " + f"{J.und.min() and J.loc[J.und.idxmin(),'book']:+.2%}.") + + # ---- il numero -3,38% del critico + b_can = CC.book_series("canonical") + print(f"\n (e) ⚠️ IL NUMERO DEL CRITICO NON SI RIPRODUCE — ma la sua LENTE si identifica,") + print(f" e la conclusione si rovescia proprio su quella.") + print(f" Il critico scrive 'peggior giorno assoluto -3,38%'. Le lenti del progetto danno:") + print(f" {'lente':>34}{'peggior giorno':>16}{'data':>14} che giorno era") + for nome, s in (("LIVE (book_series 'hourly')", J['book']), + ("CANONICA (book_series 'canonical')", b_can), + ("LIVE de-luckata x0.89", D["deluck"])): + i = s.idxmin() + u = J["und"].reindex([i]).iloc[0] if i in J.index else np.nan + tag = "RIMBALZO" if u > 0.05 else ("CRASH" if u < -0.05 else "giorno ordinario") + print(f" {nome:>34}{s.min():>16.2%}{str(i.date()):>14} {tag} (sottost. {u:+.1%})") + Jc = pd.concat({"b": b_can, "u": J["und"]}, axis=1).dropna() + wc = Jc.sort_values("u").head(10)["b"].values * 100 + wl = J.sort_values("und").head(10)["book"].values * 100 + print(f"\n LA LENTE DEL CRITICO SI PUO' IDENTIFICARE dai valori che ha pubblicato") + print(f" ('+0,97 · +0,72 · 0,00 · -1,98 · -2,24 · +0,23'). Sui 10 peggiori giorni del") + print(f" sottostante le due lenti danno:") + print(f" LIVE : " + " ".join(f"{x:+.2f}" for x in wl)) + print(f" CANONICA : " + " ".join(f"{x:+.2f}" for x in wc)) + print(f" -> **il +0,97 e il +0,72 sono nella riga CANONICA e in nessun'altra**: il critico") + print(f" ha usato `book_series('canonical')`, cioe' il BACKTEST di SKH01 (exit al") + print(f" livello), non il path LIVE. Il suo peggior giorno resta {b_can.min():.2%} contro il") + print(f" -3,38% pubblicato: **0,09pp di scarto**, dell'ordine del difetto gia' noto") + print(f" nel progetto ('`data/raw` e' gitignored e il cron riscrive i parquet ogni") + print(f" notte -> stesso codice, dati diversi', XS-LITE e GTAA/TLT).") + print(f"\n -> **QUINDI LA SUA CONCLUSIONE SI ROVESCIA SULLA SUA STESSA LENTE.** Sulla") + print(f" canonica — quella che ha usato — il peggior giorno E' un giorno di ribasso") + print(f" ({b_can.idxmin().date()}, sottostante {J['und'].reindex([b_can.idxmin()]).iloc[0]:+.1%}). La frase 'il suo peggior giorno") + print(f" assoluto non e' un giorno di crash' e' VERA solo sulla lente LIVE, che e'") + print(f" quella giusta per una decisione operativa ma non e' quella da cui viene il") + print(f" suo numero. **Aveva ragione, con la lente sbagliata.**") + + # ---------------------------------------------------------------- (2) + sezione("(2) LA DISTRIBUZIONE COMPLETA DELLE CHIUSURE DEL LIBRO — e QUALI giorni sono i peggiori") + qs = (50, 25, 10, 5, 2.5, 1, 0.5, 0.2, 0.1) + print(f"\n (a) coda SINISTRA per quantili (lente LIVE, {len(book)} giorni = {len(book)/365:.1f} anni)") + print(f" {'quantile':>12}" + "".join(f"{f'p{q}':>10}" for q in qs) + f"{'min':>10}") + print(f" {'ritorno':>12}" + "".join(f"{np.percentile(book,q):>10.2%}" for q in qs) + + f"{book.min():>10.2%}") + print(f" {'(destra)':>12}" + "".join(f"{np.percentile(book,100-q):>10.2%}" for q in qs) + + f"{book.max():>10.2%}") + print(f"\n asimmetria {stats.skew(book):+.2f} · curtosi in eccesso {stats.kurtosis(book):+.1f}" + f" · giorni flat (|r|<1bps) {(np.abs(book)<1e-4).mean():.1%}") + print(f" ⚠️ la curtosi alta NON e' di per se' un allarme: la meta' dei giorni il libro e'") + print(f" quasi fermo (TP01 long-flat, SKH01 fuori mercato l'87,6% delle barre), e una") + print(f" distribuzione con un picco a zero e' leptocurtica per costruzione.") + + print(f"\n (b) i 15 PEGGIORI GIORNI del libro, con l'attribuzione esatta e cosa faceva il mercato") + w = J.sort_values("book").head(15) + print(f"\n {'data':>12}{'LIBRO':>9}{'TP01':>9}{'SKH01':>9}{'BTC':>9}{'ETH':>9} tipo di giornata") + for d, row in w.iterrows(): + if row.und > 0.05: + tag = "RIMBALZO (short squeeze)" + elif row.und < -0.05: + tag = "crash" + elif row.und < -0.02: + tag = "ribasso" + else: + tag = "giorno ordinario" + colpa = "TP01" if row.tp_leg < row.skh_leg else "SKH01" + print(f" {str(d.date()):>12}{row.book:>9.2%}{row.tp_leg:>9.2%}{row.skh_leg:>9.2%}" + f"{row.BTC:>9.1%}{row.ETH:>9.1%} {tag} [{colpa}]") + + # meccanismo + crash = J[J.und < -0.05] + rimb = J[J.und > 0.05] + ord_ = J[(J.und >= -0.02) & (J.und <= 0.02)] + print(f"\n (c) IL MECCANISMO — il libro per tipo di giornata") + print(f" {'tipo':>28}{'giorni':>8}{'media':>10}{'peggiore':>11}{'quota TP01':>12}{'quota SKH01':>13}") + for nome, sub in (("CRASH (sottost. < -5%)", crash), ("RIMBALZO (sottost. > +5%)", rimb), + ("ordinario (|sottost.|<2%)", ord_), ("tutti", J)): + if len(sub) == 0: + continue + print(f" {nome:>28}{len(sub):>8}{sub.book.mean():>10.3%}{sub.book.min():>11.2%}" + f"{sub.tp_leg.mean():>12.3%}{sub.skh_leg.mean():>13.3%}") + print(f"\n (d) ⚠️ QUI IL CONTROLLO DEL CRITICO VA CORRETTO, e la correzione E' il meccanismo.") + print(f" Sui **10 giorni piu' estremi** il libro guadagna (sopra). Ma sulla popolazione") + print(f" dei **160 giorni di crash** perde in media {crash.book.mean():+.3%}: la frase") + print(f" 'il libro guadagna nei crash' e' vera sugli estremi e FALSA in generale.") + print(f" La ragione e' misurabile — l'esposizione di TP01 DIPENDE dalla taglia del crollo:") + print(f" {'sottostante':>22}{'giorni':>8}{'espos. TP01':>14}{'libro medio':>14}" + f"{'peggiore':>11}{'protezione':>12}") + bucket = [(-1.00, -0.20, "< -20%"), (-0.20, -0.10, "-20% .. -10%"), + (-0.10, -0.05, "-10% .. -5%"), (-0.05, -0.02, "-5% .. -2%")] + espo = pd.concat({a: TPQ_pre[a]["held"] for a in ASSETS}, axis=1).mean(axis=1) * 0.5 * W_TP * 2 + Jx = J.join(espo.rename("espo"), how="left") + for lo, hi, lbl in bucket: + sub = Jx[(Jx.und > lo) & (Jx.und <= hi)] + if len(sub) == 0: + continue + cell = "GUADAGNA" if sub.book.mean() > 0 else f"{abs(sub.und.mean()/sub.book.mean()):.0f}x" + print(f" {lbl:>22}{len(sub):>8}{sub.espo.mean():>14.3f}{sub.book.mean():>14.3%}" + f"{sub.book.min():>11.2%}{cell:>12}") + print(f"\n -> **il trend-follower e' gia' FLAT quando il crollo e' grande, ed e' ancora") + print(f" LONG quando e' medio**: l'esposizione scende in modo monotono al crescere del") + print(f" crollo, perche' un crollo grande arriva dopo che il trend a 30/90/180 giorni") + print(f" si e' gia' girato (e il vol-target ha gia' de-levered). Il rapporto di") + print(f" protezione peggiora nella fascia mite e migliora negli estremi. **Il libro") + print(f" non e' immune ai crash: e' immune ai crash GROSSI.**") + print(f"\n E nei RIMBALZI il libro guadagna in media ({rimb.book.mean():+.3%}) MA ci tiene il") + print(f" suo peggior giorno assoluto ({rimb.book.min():+.2%}): media e coda vanno in") + print(f" direzioni opposte. **La coda di questo libro non sta dove sta quella del") + print(f" mercato.** Il -10% di growth_policy e' una coda da buy&hold applicata a un") + print(f" libro difensivo: l'evento e' quello sbagliato, non solo la taglia.") + i_worst = J["book"].idxmin() + rw = J.loc[i_worst] + print(f"\n il peggior giorno in dettaglio: {i_worst.date()} libro {rw.book:+.2%}") + print(f" BTC {rw.BTC:+.1%} · ETH {rw.ETH:+.1%} (il giorno DOPO il -43,6% del 12 marzo)") + print(f" TP01 {rw.tp_leg:+.2%} (flat: un trend-follower long-only non compra un crash)") + print(f" SKH01 {rw.skh_leg:+.2%} = uno short con sl 2% che GAPPA attraverso lo stop") + skh_only = (J['skh_leg'] / W_SKH) + print(f" a livello di sleeve nudo SKH01 fa {skh_only.min():+.2%} in quel giorno,") + print(f" contro un secondo peggiore di {skh_only.nsmallest(2).iloc[-1]:+.2%}: **la coda") + print(f" di SKH01 e' UN giorno solo**, e quel giorno e' un rimbalzo, non un crollo.") + + # ---------------------------------------------------------------- (3) + sezione("(3) LA LENTE ACCOPPIATA — il minimo INTRA-giorno (close-only e' CIECA su una regola a un giorno)") + tup = PC.crypto_daily_tuples(W_TP, W_SKH) + K = pd.concat({"R_live": J["book"], "R_rec": tup["R"], "m_rec": tup["m"], + "und": J["und"]}, axis=1).dropna() + gap = (K["m_rec"] - K["R_rec"]).values.astype(float) + m_acc = K["R_live"].values + gap # costruzione dello scettico: UN grado di liberta' + m_rec = K["m_rec"].values # sensitivita': recon pura + print(f"\n (a) COSTRUZIONE DICHIARATA (la stessa di r0822_leverage_skeptic, per confrontabilita'):") + print(f" chiusure dalla serie LIVE + GAP dalla recon ACCOPPIATA, appaiato per GIORNO.") + print(f" La recon pura (R e m entrambi canonici) e' riportata come SENSITIVITA'.") + print(f" n = {len(K)} giorni.") + print(f"\n (b) REPLICA delle marginali del gap (25/07 e scettico 22/08: p50 -0,16 / p90 -1,03 / p99 -2,71)") + print(f" {'':>14}{'p50':>10}{'p90':>10}{'p99':>10}{'min':>10}") + print(f" {'qui':>14}{np.percentile(gap,50)*100:>9.2f}pp{np.percentile(gap,10)*100:>9.2f}pp" + f"{np.percentile(gap,1)*100:>9.2f}pp{gap.min()*100:>9.2f}pp") + print(f" (i percentili del GAP si leggono dal basso: p90 del |gap| = p10 del gap firmato)") + + print(f"\n (c) LE DUE RISPOSTE ALLA DOMANDA 'qual e' il peggior giorno'") + print(f" {'':>26}{'p1':>10}{'p0.5':>10}{'p0.1':>10}{'MINIMO':>11}{'data del minimo':>18}") + for nome, arr in (("CHIUSURA (live)", K["R_live"].values), + ("MINIMO intra-giorno (acc.)", m_acc), + ("MINIMO (recon pura, sens.)", m_rec)): + i = int(np.argmin(arr)) + print(f" {nome:>26}{np.percentile(arr,1):>10.2%}{np.percentile(arr,0.5):>10.2%}" + f"{np.percentile(arr,0.1):>10.2%}{arr.min():>11.2%}{str(K.index[i].date()):>18}") + neg = K["R_live"].values < -1e-4 + print(f"\n rapporto minimo/chiusura sul peggior giorno: " + f"{m_acc.min()/K['R_live'].values.min():.2f}x · mediana sui soli giorni in perdita " + f"({int(neg.sum())} giorni) {np.median(m_acc[neg]/K['R_live'].values[neg]):.2f}x") + print(f" -> **il peggior giorno del libro e' -{abs(K['R_live'].min()):.2%} in chiusura e") + print(f" -{abs(m_acc.min()):.2%} al minimo**. Le due risposte differiscono di un fattore ~2:") + print(f" chi valuta una regola sul MINIMO (margine, daily-loss, stop di conto) e usa") + print(f" la chiusura sbaglia di quel fattore, e nel verso ottimista.") + + print(f"\n (d) ⚠️ LIMITE DICHIARATO DELLA COSTRUZIONE, e va detto proprio sul giorno che conta.") + j = int(np.argmin(K["R_live"].values)) + print(f" La recon accoppiata usa la convenzione CANONICA di SKH01 (exit AL LIVELLO): nel") + print(f" giorno del gap-through cappa il wick allo stop. Il {K.index[j].date()} la chiusura") + print(f" live e' {K['R_live'].values[j]:+.2%} e quella canonica {K['R_rec'].values[j]:+.2%}" + f" (differenza {K['R_live'].values[j]-K['R_rec'].values[j]:+.2%}), mentre il gap canonico") + print(f" di quel giorno vale solo {gap[j]*100:.2f}pp -> il minimo LIVE di quel giorno e'") + print(f" plausibilmente piu' profondo di {m_acc[j]:+.2%}. **Il numero riportato sopra e' un") + print(f" LIMITE SUPERIORE (ottimista) del minimo nel giorno peggiore**, non una misura.") + print(f" Non lo si ripara qui: servirebbe rifare `skh_hourly_contrib` con l'uscita live,") + print(f" cioe' cambiare la macchineria sotto esame. Si dichiara, e la sezione (5) da' il") + print(f" bound per via aritmetica, che non ha questo problema.") + + print(f"\n (e) CONTROLLO: il gap e' piu' profondo nei giorni che finiscono BENE? (finding 25/07)") + dec = pd.qcut(K["R_live"], 10, labels=False, duplicates="drop") + gp = pd.Series(gap, index=K.index).groupby(dec).mean() + print(f" gap medio per decile di chiusura (0 = peggiore). ⚠️ i decili collassano a" + f" {gp.shape[0]} bin") + print(f" perche' il {(np.abs(book)<1e-4).mean():.0%} dei giorni ha chiusura esattamente zero:") + print(" " + " ".join(f"d{int(i)}:{v*100:+.2f}" for i, v in gp.items())) + print(f" -> replica il verso del finding (il gap non e' concentrato nei giorni brutti):") + print(f" il decile PEGGIORE ha gap {gp.iloc[0]*100:+.2f}pp contro {gp.iloc[-1]*100:+.2f}pp" + f" del migliore.") + print(f" **E la conseguenza per NOI e' quella buona: un giorno di crollo chiude quasi sul") + print(f" proprio minimo, quindi la lente accoppiata NON peggiora lo stress di coda.**") + + # ---------------------------------------------------------------- (4) + sezione("(4) LA CODA CHE NON E' NEL DATASET — EVT/POT, con l'incertezza dichiarata") + print(f"\n 7,4 anni con due tori e un orso non contengono necessariamente il peggior giorno") + print(f" possibile. Qui si STIMA la coda invece di sceglierla: GPD sopra una soglia") + print(f" dichiarata prima ({EVT_Q:.0%}° pctl delle perdite), su chiusure E su minimi.") + + fits, boots = {}, {} + for nome, arr in (("CHIUSURA", K["R_live"].values), ("MINIMO (acc.)", m_acc)): + loss = -arr + print(f"\n --- {nome} ---") + print(f" {'soglia q':>10}{'u':>9}{'n ecced.':>10}{'xi':>9}{'beta':>9}" + + "".join(f"{f'1/{p} g':>12}" for p in RP) + f"{' P(perdita>10%)':>18}") + for q in EVT_Q_SENS: + f = gpd_fit(loss, q) + if not f: + continue + pe = gpd_exceed(f, STRESS_A_MANO) + rp10 = (1.0 / pe / ANN) if (np.isfinite(pe) and pe > 0) else np.inf + marca = " <-- soglia dichiarata" if abs(q - EVT_Q) < 1e-9 else "" + print(f" {q:>10.3f}{f['u']:>9.2%}{f['nu']:>10}{f['xi']:>9.3f}{f['beta']:>9.4f}" + + "".join(f"{-gpd_quantile(f,1.0/p):>12.2%}" for p in RP) + + f"{f'1 ogni {rp10:.0f} anni' if np.isfinite(rp10) else 'mai':>18}{marca}") + if abs(q - EVT_Q) < 1e-9: + fits[nome] = f + f = fits[nome] + bo = evt_bootstrap(loss, EVT_Q, EVT_BOOT, EVT_BLOCK, rng) + boots[nome] = bo + print(f" IC bootstrap A BLOCCHI ({EVT_BOOT} ricampionamenti, blocchi {EVT_BLOCK}g):") + print(f" xi {f['xi']:>8.3f} IC95 [{np.percentile(bo['xi'],2.5):+.3f}," + f" {np.percentile(bo['xi'],97.5):+.3f}]") + for p in RP: + v = bo[f"rl{p}"] + print(f" 1/{p:<6} {-gpd_quantile(f,1.0/p):>8.2%} IC95 " + f"[{-np.percentile(v,97.5):>7.2%}, {-np.percentile(v,2.5):>7.2%}]" + f" (p95 pessimista {-np.percentile(v,95):.2%})") + + print(f"\n (b) CROSS-CHECK INDIPENDENTE — stimatore di HILL dell'indice di coda.") + print(f" Non e' un ri-fit della stessa cosa: Hill stima xi dai soli ORDINE-STATISTICI") + print(f" superiori (rapporti di log), senza MLE e senza soglia di quantile. Se i due non") + print(f" concordano, il numero non e' pronto per una decisione.") + print(f" ⚠️ La strada ovvia — fittare una t di Student su TUTTA la serie — qui NON") + print(f" funziona e va detto perche': il {(np.abs(book)<1e-4).mean():.0%} dei giorni e' esattamente zero") + print(f" (libro flat), la MLE manda i gradi di liberta' a ~0 per adattarsi all'atomo in") + print(f" zero e sputa quantili assurdi (10^12). **Una distribuzione con un atomo non si") + print(f" fitta con una densita' continua**: il POT non ha il problema perche' guarda solo") + print(f" la coda. Riportata qui sotto la t sui soli giorni ATTIVI, come terza strada.") + print(f"\n {'serie':>18}{'k ordini':>10}{'xi Hill':>10}{'xi GPD':>9}" + + "".join(f"{f'1/{p} g (Hill)':>16}" for p in RP)) + for nome, arr in (("CHIUSURA", K["R_live"].values), ("MINIMO (acc.)", m_acc)): + loss = np.sort(-arr)[::-1] + loss = loss[loss > 0] + kk = max(int(0.05 * len(loss)), 20) + xi_h = float(np.mean(np.log(loss[:kk])) - np.log(loss[kk])) + n_ = len(arr) + # Weissman: quantile estremo dallo stimatore di Hill + wq = lambda p: loss[kk] * ((kk / (n_ * p)) ** xi_h) # noqa: E731 + f = fits[nome] + print(f" {nome:>18}{kk:>10}{xi_h:>10.3f}{f['xi']:>9.3f}" + + "".join(f"{-wq(1.0/p):>16.2%}" for p in RP)) + print(f"\n {'serie':>18}{'gdl (t, attivi)':>18}{'sigma':>9}" + + "".join(f"{f'1/{p} g (t)':>14}" for p in RP)) + for nome, arr in (("CHIUSURA", K["R_live"].values), ("MINIMO (acc.)", m_acc)): + att = arr[np.abs(arr) > 1e-4] + df_, loc_, sc_ = stats.t.fit(att) + print(f" {nome:>18}{df_:>18.1f}{sc_:>9.4f}" + + "".join(f"{stats.t.ppf(1.0/p*len(att)/len(arr), df_, loc_, sc_):>14.2%}" for p in RP)) + + fc, fm = fits["CHIUSURA"], fits["MINIMO (acc.)"] + evt_boot_close = boots["CHIUSURA"] + pe = gpd_exceed(fc, STRESS_A_MANO) + # la stessa domanda con lo stimatore di HILL (indipendente dal MLE della GPD) + _l = np.sort(-K["R_live"].values)[::-1] + _l = _l[_l > 0] + _k = max(int(0.05 * len(_l)), 20) + _xi = float(np.mean(np.log(_l[:_k])) - np.log(_l[_k])) + pe_hill = (_k / len(K)) * (STRESS_A_MANO / _l[_k]) ** (-1.0 / _xi) + print(f"\n (c) IL CONFRONTO CHE DECIDE: il -{STRESS_A_MANO:.0%} assunto a mano e' DENTRO o FUORI?") + print(f" · chiusura, quantile 1/1.000 giorni : {-gpd_quantile(fc,1e-3):>8.2%}") + print(f" · chiusura, quantile 1/10.000 giorni : {-gpd_quantile(fc,1e-4):>8.2%}") + print(f" · minimo, quantile 1/10.000 giorni : {-gpd_quantile(fm,1e-4):>8.2%}") + if pe > 0: + print(f" · il -{STRESS_A_MANO:.0%} a mano, in chiusura : P = {pe:.2e}/giorno" + f" = 1 ogni {1/pe/ANN:.0f} anni") + else: + print(f" · il -{STRESS_A_MANO:.0%} a mano, in chiusura : probabilita' NULLA nel modello") + print(f" · lo stesso, con lo stimatore di HILL : P = {pe_hill:.2e}/giorno" + f" = 1 ogni {1/pe_hill/ANN:.0f} anni") + print(f"\n -> **il -{STRESS_A_MANO:.0%} NON E' una taglia impossibile, e' una FREQUENZA impossibile.**") + print(f" growth_policy lo assume UNA VOLTA L'ANNO (1 ogni 5 anni come variante mite).") + print(f" I due stimatori indipendenti lo mettono a **1 ogni {min(1/pe/ANN,1/pe_hill/ANN):.0f}" + f" - {max(1/pe/ANN,1/pe_hill/ANN):.0f} anni**:") + print(f" **{np.log10(1/pe_hill/ANN):.0f}-{np.log10(1/pe/ANN):.0f} ordini di grandezza di scarto sulla FREQUENZA**.") + print(f" ⚠️ E i due stimatori NON concordano fra loro: xi GPD {fc['xi']:.3f} contro Hill") + print(f" {_xi:.3f}, cioe' un quantile a 1/10.000 di {-gpd_quantile(fc,1e-4):.2%} contro" + f" {-(_l[_k]*((_k/(len(K)*1e-4))**_xi)):.2%}.") + print(f" **Si cita la BANDA fra stimatori, non il numero di uno solo** — su una coda") + print(f" stimata da {len(K)} osservazioni la scelta dello stimatore vale quanto i dati.") + print(f"\n ⚠️ E QUI VA DETTO IL LIMITE DELL'EVT, che e' il vero motivo per cui la sezione (5)") + print(f" esiste: **una GPD stimata sui giorni vissuti estrapola il MECCANISMO vissuto.**") + print(f" Se il libro puo' assumere una configurazione di posizioni che nel campione non") + print(f" ha mai assunto, quella configurazione non e' nella coda stimata. Nessun") + print(f" intervallo di confidenza copre un modo di perdere che non e' nei dati: e'") + print(f" esattamente cio' che il gate `implausible_sharpe` chiama 'rischio FUORI dal") + print(f" dataset'. La risposta non e' un fit migliore, e' un conto ARITMETICO.") + + # ---------------------------------------------------------------- (5) + sezione("(5) IL PEGGIOR GIORNO POSSIBILE PER COSTRUZIONE — aritmetica, non statistica") + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + frac = float(cfg["max_notional_per_asset_frac"]) + TPQ = TPQ_pre + EX = estremi_orari() + sl_l, sl_s = SKH01_V2_DD.sl_pct, SKH01_V2_DD.sl_pct_short + tv, levcap = CANONICAL["target_vol"], CANONICAL["leverage"] + + print(f"\n (a) LA FORMULA DEL LIBRO LIVE (src/live/book.book_net_target, letta dal codice):") + print(f" net_target[asset] = clamp( 0.5 * E * (0.75*tp_frac + 0.25*skh_sign), ±cap ),") + print(f" cap = E * {frac} -> per-asset |net| <= {frac:.2f}·E, LORDO massimo {2*frac:.2f}·E su 2 asset.") + print(f" In frazione di equity: gamba TP01 = {0.5*W_TP:.3f}·tp_frac, gamba SKH01 = {0.5*W_SKH:.3f}·skh_sign.") + + print(f"\n (b) TP01 — il vol-target rende il bound INVARIANTE alla volatilita'.") + print(f" tp_frac = segnale * min(target_vol/vol, {levcap:g}) con segnale in {{0, 1/3, 2/3, 1}},") + print(f" quindi contributo di gamba = 0.375 * segnale * target_vol * z / sqrt(365.25)") + print(f" = {0.5*W_TP*tv/np.sqrt(365.25)*100:.4f}% * segnale * z. **Un crash entra solo attraverso il suo z-score.**") + print(f" ⚠️ La formula vale con tp_frac = target_vol/vol, cioe' solo dove il CAP non morde:") + print(f" 0.375*tp_frac + 0.125 <= {frac} richiede tp_frac <= 1, cioe' vol >= target_vol.") + print(f" Sotto quella soglia il contributo e' 0.375*|r| ed e' PIU' PICCOLO -> il bound") + print(f" si prende sullo z peggiore CON vol >= {tv:.0%} (auto-consistenza, non cherry-pick).") + print(f" {'asset':>7}{'z min (vol>=tv)':>17}{'quando':>13}{'ret':>9}{'vol prec.':>11}" + f"{'tp_frac':>9}{'contributo max':>16}{'z min GREZZO':>14}") + zmin, zmin_raw = {}, {} + for a in ASSETS: + d = TPQ[a].dropna(subset=["z"]) + zmin_raw[a] = float(d["z"].min()) + dv = d[d["vol_prev"] >= tv] + i = dv["z"].idxmin() + zmin[a] = float(dv["z"].min()) + contr = 0.5 * W_TP * tv * abs(zmin[a]) / np.sqrt(365.25) + print(f" {a:>7}{zmin[a]:>17.1f}{str(i.date()):>13}{dv.loc[i,'ret']:>9.1%}" + f"{dv.loc[i,'vol_prev']:>11.1%}{min(tv/dv.loc[i,'vol_prev'],1.0):>9.2f}" + f"{-contr:>16.2%}{zmin_raw[a]:>14.1f}") + print(f" tp_frac MASSIMO mai tenuto: " + " · ".join( + f"{a} {TPQ[a]['held'].max():.3f}" for a in ASSETS) + + f" (il cap di leva {levcap:g}x NON e' mai stato binding: la vol a 30g non e' mai scesa" + f" sotto {min(float(TPQ[a]['vol_prev'].min()) for a in ASSETS):.1%})") + + print(f"\n (c) SKH01 — NON e' vol-targeted (misurato 26/07): la sua taglia e' fissa e la sua") + print(f" perdita e' limitata dallo STOP, ma gli exit sono SOFTWARE (fill al close orario") + print(f" successivo al tocco) -> il bound e' stop + escursione DENTRO quell'ora.") + print(f" {'asset':>7}{'1h peggiore giu':>18}{'1h peggiore su':>17}" + f"{'bound LONG (sl 4%)':>21}{'bound SHORT (sl 2%)':>21}{'peggio realizzato':>19}") + bound_l, bound_s = {}, {} + for a in ASSETS: + bound_l[a] = sl_l + abs(EX[a]["giu"]) + bound_s[a] = sl_s + abs(EX[a]["su"]) + real = SKL.skh_daily_asset(a, 0, "hourly").min() + print(f" {a:>7}{EX[a]['giu']:>18.2%}{EX[a]['su']:>17.2%}" + f"{-bound_l[a]:>21.2%}{-bound_s[a]:>21.2%}{real:>19.2%}") + print(f" -> il gap-through misurato dal progetto (sl 2% modellato -> -11/-23% realizzato)") + print(f" e' COERENTE con questo bound, che lo contiene senza esserne tarato.") + + print(f"\n (d) I DUE SCENARI COERENTI. TP01 e' LONG-ONLY: perde solo in ribasso. SKH01 short") + print(f" perde solo in RIALZO. Non possono perdere al massimo lo stesso giorno se SKH e'") + print(f" short -> gli scenari sono due, e vanno tenuti separati.") + lim_tp = {a: 0.5 * W_TP * tv * abs(zmin[a]) / np.sqrt(365.25) for a in ASSETS} + crash_max = sum(lim_tp[a] + 0.5 * W_SKH * bound_l[a] for a in ASSETS) + squeeze_max = sum(0.5 * W_SKH * bound_s[a] for a in ASSETS) + print(f"\n SCENARIO A — CRASH (TP01 a piena convinzione su entrambi + SKH01 LONG stoppato)") + for a in ASSETS: + print(f" {a}: TP01 {-lim_tp[a]:>7.2%} SKH01 {-0.5*W_SKH*bound_l[a]:>7.2%}" + f" -> {-(lim_tp[a]+0.5*W_SKH*bound_l[a]):>7.2%}") + print(f" TOTALE LIBRO {-crash_max:>7.2%} <-- IL BOUND STRUTTURALE a k=1,00x") + print(f"\n SCENARIO B — RIMBALZO (TP01 flat per costruzione + SKH01 SHORT stoppato su due)") + for a in ASSETS: + print(f" {a}: SKH01 {-0.5*W_SKH*bound_s[a]:>7.2%}") + print(f" TOTALE LIBRO {-squeeze_max:>7.2%} realizzato il {i_worst.date()}:" + f" {J['book'].min():+.2%} ({J['book'].min()/-squeeze_max:.0%} del bound)") + print(f" -> lo scenario B e' gia' stato VISSUTO quasi al suo massimo: il bound e' credibile.") + + print(f"\n (e) CONTROPROVA INDIPENDENTE — controfattuale sui movimenti VERI. Per ogni giorno") + print(f" storico si sostituisce a TP01 la piena convinzione e a SKH01 la posizione LONG") + print(f" su entrambi gli asset, e si applica il movimento realmente accaduto.") + CF = pd.concat({a: TPQ[a][["lev_max", "ret", "ret_low"]] for a in ASSETS}, axis=1).dropna() + tot = np.zeros(len(CF)) + tot_min = np.zeros(len(CF)) # stessa cosa valutata sul MINIMO intra-giorno + for a in ASSETS: + rr = CF[(a, "ret")].values + rl = CF[(a, "ret_low")].values + hm = np.minimum(CF[(a, "lev_max")].values, 1.0) # cap: 0.375h+0.125<=0.5 -> h<=1 + tot += 0.5 * W_TP * hm * rr + 0.5 * W_SKH * np.maximum(rr, -bound_l[a]) + tot_min += 0.5 * W_TP * hm * rl + 0.5 * W_SKH * np.maximum(rl, -bound_l[a]) + o = np.argsort(tot)[:5] + print(f" {'data':>12}{'perdita ctrf (chiusura)':>25}{'ctrf (minimo)':>16}{'BTC':>10}{'ETH':>10}") + for i in o: + print(f" {str(CF.index[i].date()):>12}{tot[i]:>25.2%}{tot_min[i]:>16.2%}" + f"{CF[('BTC','ret')].values[i]:>10.1%}{CF[('ETH','ret')].values[i]:>10.1%}") + print(f"\n n = {len(CF)} giorni · giorni sotto -10% nel controfattuale:" + f" {int((tot<-0.10).sum())} ({(tot<-0.10).mean()*365:.2f}/anno) · sotto -5%:" + f" {int((tot<-0.05).sum())} ({(tot<-0.05).mean()*365:.2f}/anno)") + L_STRUCT = max(crash_max, -float(tot.min())) + L_STRUCT_MIN = -float(tot_min.min()) + print(f"\n -> DUE STRADE, STESSA RISPOSTA: il bound analitico da' {-crash_max:.2%},") + print(f" il controfattuale sui movimenti veri {tot.min():.2%}. **Si usa" + f" L_strutturale = {-L_STRUCT:.2%}** in CHIUSURA.") + print(f" E sul MINIMO intra-giorno (stesso controfattuale valutato al low della giornata,") + print(f" che e' la grandezza che guarda il margine): **{tot_min.min():.2%}**, cioe'" + f" {L_STRUCT_MIN/L_STRUCT:.2f}x la chiusura.") + print(f" ⚠️ Questo numero NON eredita il limite della sez.(3d): non passa dalla recon") + print(f" canonica, e' costruito sui low veri del feed certificato.") + print(f"\n 📌 LA RISPOSTA ALLA DOMANDA DEL BRIEF: il -10% NON e' fuori dal possibile —") + print(f" sta al {STRESS_A_MANO/L_STRUCT:.0%} del massimo strutturale. **Cio' che e' sbagliato non e'") + print(f" la taglia, e' la FREQUENZA**: il controfattuale — in cui la strategia") + print(f" difensiva e' massimamente in torto su entrambi gli asset — sfonda il -10%") + print(f" {int((tot<-0.10).sum())} giorno/i su {len(CF)}; il libro VERO non l'ha mai") + print(f" nemmeno avvicinato ({J['book'].min():+.2%}). **La riserva dello scettico e' mal") + print(f" tarata sulla frequenza, non falsificata sulla taglia.**") + zx = 1.5 + print(f"\n ⚠️ COSA NON E' CHIUSO: z_max e' esso stesso un massimo IN CAMPIONE. Con uno z") + print(f" piu' cattivo del {zx:g}x osservato il bound dello scenario A diventa" + f" {-(sum(zx*lim_tp[a] + 0.5*W_SKH*bound_l[a] for a in ASSETS)):.2%}.") + print(f" Il bound e' condizionato a 'nessun giorno peggiore in z di quelli visti', e") + print(f" va citato cosi'. Non e' un massimo assoluto: e' un massimo CONDIZIONATO.") + + # ---- il cap di config + print(f"\n (f) 🚨 IL CAP DI CONFIG NON E' UN KNOB DI SCALA — e questo cambia la DOMANDA.") + print(f" La regola del progetto dice che un cambio di SCALA passa dal cap di config e non") + print(f" da `target_vol`. Ma il cap e' un CLAMP, non un moltiplicatore: agisce solo nei") + print(f" giorni in cui il libro vuole PIU' del cap. Quanti sono?") + print(f" {'asset':>7}{'giorni':>9}{'net max voluto':>17}{'giorni sopra cap 0.50':>24}{'quota':>10}") + for a in ASSETS: + h = TPQ[a]["held"].values + net = 0.5 * W_TP * h + 0.5 * W_SKH # SKH long = il caso che spinge di piu' + n_over = int((net > frac).sum()) + print(f" {a:>7}{len(h):>9}{net.max():>17.4f}{n_over:>24}{n_over/len(h):>10.2%}") + print(f"\n -> **il cap morde su 3 giorni su ~2.900, e per meno dell'1% del target.** Alzare") + print(f" `max_notional_per_asset_frac` da 0.50 a 0.625 NON produce k=1,25: produce") + print(f" k=1,00 in 2.928 giorni su 2.931 e un pelo di piu' in tre. **Il gradino di leva") + print(f" NON e' oggi implementabile con la strada che la regola dichiara pulita**, e in") + print(f" `config/live.json` NON esiste una chiave di scala: servirebbe toccare") + print(f" `WEIGHT`/`W_TP01`/`W_SKH` in `src/live/book.py` (codice, non config) oppure") + print(f" aggiungere una chiave nuova. **Due mesi di discussione su un knob inesistente.**") + print(f" ⚠️ E il moltiplicatore x1,25 sui pesi e' un puro cambio di scala SOLO se applicato") + print(f" a entrambi: applicato a uno solo e' un tilt, e li' `weights_tilt_null` c'e' gia'.") + + # ---------------------------------------------------------------- (6) + sezione("(6) k* ONESTO — la curva di Kelly con la coda STIMATA al posto di quella a mano") + f_close = fits["CHIUSURA"] + drift_pt = mu * ANN + scenari_drift = (("punto (de-luck x0.89)", drift_pt), + ("-1 errore standard", drift_pt - se_drift), + ("-2 errori standard", drift_pt - 2 * se_drift)) + code = (("empirica (nessun modello)", None), + (f"EVT troncata a {-L_STRUCT:.1%} (strutturale)", L_STRUCT), + (f"EVT troncata a {-STRESS_A_MANO:.0%} (a mano)", STRESS_A_MANO), + (f"EVT troncata a {-1.5*L_STRUCT:.1%} (z x1.5)", 1.5 * L_STRUCT)) + print(f"\n Errore standard del drift = vol/sqrt(T) = {se_drift:.2%}/anno su" + f" {len(r_del)/ANN:.1f} anni.") + print(f" La coda EVT ha supporto ILLIMITATO se xi>0 -> E[log(1+kR)] sarebbe -inf per ogni k:") + print(f" il supporto lo da' l'ARITMETICA della sezione (5). E' qui che i due pezzi si") + print(f" incastrano — l'EVT da' la frequenza, la struttura da' il massimo.") + print(f"\n {'coda':>34}{'drift':>24}{'k*':>9}{'g(1.00x)':>11}{'g(1.25x)':>11}{'g(1.50x)':>11}") + ks_tab = {} + NDRAW = 400_000 + # numeri casuali APPAIATI fra tutte le celle: la tabella e' un confronto, non 12 stime + # indipendenti. Senza questo, differenze di ±0,5x fra celle sono rumore MC e sembrano segnale. + _rg = np.random.default_rng(SEED + 1) + RND = {"ib": _rg.integers(0, 10 ** 9, NDRAW), "um": _rg.random(NDRAW), + "ut": _rg.random(NDRAW)} + for nome_c, lmax in code: + for nome_d, dr in scenari_drift: + if lmax is None: + rr = r_del - r_del.mean() + dr / ANN + else: + rr = campione_semiparam(r_del, f_close, lmax, NDRAW, dr, RND) + kk, _ = k_star(rr) + ks_tab[(nome_c, nome_d)] = kk + print(f" {nome_c:>34}{nome_d:>24}{kk:>8.2f}x{g_exact(rr,1.00):>11.2%}" + f"{g_exact(rr,1.25):>11.2%}{g_exact(rr,1.50):>11.2%}") + + print(f"\n -> **LA FORMA DELLA CODA QUASI NON CONTA, IL DRIFT CONTA TUTTO.** Fra la coda") + print(f" empirica e una GPD troncata a {-1.5*L_STRUCT:.0%} k* si muove di ~1-2 unita'; fra il") + print(f" drift di punto e quello a -2 SE si muove di {ks_tab[('empirica (nessun modello)','punto (de-luck x0.89)')]-ks_tab[('empirica (nessun modello)','-2 errori standard')]:.1f}. **Modellare meglio la coda non e'") + print(f" la leva: la leva e' quanti anni di storia ha il drift.** (Ed e' coerente col") + print(f" fatto gia' pubblicato che k* e' LINEARE nell'errore del drift.)") + print(f" ⚠️ Le celle sono APPAIATE (stessi numeri casuali): le differenze fra righe sono") + print(f" confrontabili, i livelli assoluti hanno l'incertezza del fit di coda.") + print(f"\n (b) LO STRESS A MANO DELLO SCETTICATO, riprodotto e poi ri-calibrato.") + print(f" growth_policy: 'un giorno -10% ALL'ANNO porta k* a 2x'.") + print(f" ⚠️ CONVENZIONE, ed e' quella che decide il numero: qui i giorni di coda si") + print(f" AGGIUNGONO e il drift NON si ri-centra. E' la lettura conservativa, la stessa") + print(f" implicita in growth_policy (che SOSTITUISCE giorni veri con il crash): se un") + print(f" -10% annuo esiste ed e' fuori campione, allora il drift osservato era FORTUNATO,") + print(f" e togliergli il tail event senza toccare il drift conterebbe due volte la") + print(f" fortuna. La tabella (a) sopra ri-centra di proposito, perche' li' il drift e' una") + print(f" COLONNA e si vuole isolare la forma della coda; qui no.") + p_evt = gpd_exceed(f_close, STRESS_A_MANO) + freq_cf = max((tot < -0.10).mean(), 1.0 / len(CF)) + print(f"\n {'evento':>36}{'freq':>16}{'drift':>9}{'k*':>8}{'1/2 Kelly':>11}" + f"{'g(1.00x)':>10}{'g(1.25x)':>10}{'g(1.50x)':>10}") + stress_k = {} + for lbl, p_giorno in ((f"-{STRESS_A_MANO:.0%} 1 volta l'ANNO (a mano)", 1.0 / ANN), + (f"-{STRESS_A_MANO:.0%} 1 ogni 5 anni (a mano, mite)", 1.0 / (5 * ANN)), + (f"-{STRESS_A_MANO:.0%} alla freq. del CONTROFATTUALE", freq_cf), + (f"-{STRESS_A_MANO:.0%} alla freq. stimata EVT", p_evt), + ("nessuno stress (riferimento)", 0.0)): + rr = r_del.copy() + n_add = int(round(p_giorno * len(rr) / max(1e-12, 1 - p_giorno))) if p_giorno > 0 else 0 + rr2 = np.concatenate([rr, np.full(n_add, -STRESS_A_MANO)]) if n_add > 0 else rr + kk, _ = k_star(rr2) + stress_k[lbl] = kk + fr = f"1 ogni {1/p_giorno:.0f}g" if p_giorno > 0 else "-" + print(f" {lbl:>36}{fr:>16}{rr2.mean()*ANN:>9.2%}{kk:>7.2f}x{0.5*kk:>10.2f}x" + f"{g_exact(rr2,1.00):>10.2%}{g_exact(rr2,1.25):>10.2%}{g_exact(rr2,1.50):>10.2%}") + k_mano = stress_k[f"-{STRESS_A_MANO:.0%} 1 volta l'ANNO (a mano)"] + KSG = (1.00, 1.50, 2.00, 3.50, 5.00, 10.00, 18.00) # la griglia COARSE di growth_policy + rr = np.concatenate([r_del, np.full(int(round(len(r_del)/(ANN-1))), -STRESS_A_MANO)]) + gg = [g_exact(rr, k) for k in KSG] + print(f"\n REPLICA PARZIALE, dichiarata: sulla griglia COARSE di growth_policy questo") + print(f" motore mette l'argmax a {KSG[int(np.nanargmax(gg))]:g}x (loro: 2x); a griglia fine {k_mano:.2f}x.") + print(f" **La direzione e l'ordine di grandezza si replicano** (k* crolla da" + f" {ks_emp:.1f}x a ~3x),") + print(f" la cella esatta no — e' la risoluzione MC che loro stessi hanno dichiarato.") + print(f"\n -> **LA TAGLIA DEL -10% COSTA POCO, LA FREQUENZA COSTA TUTTO.** Lo stesso evento,") + print(f" spostato dalla frequenza ASSUNTA a quella STIMATA, muove k* da {k_mano:.1f}x a" + f" {stress_k['nessuno stress (riferimento)']:.1f}x.") + print(f" Il parametro scelto a mano che blocca il gradino non e' '-10%': e' 'OGNI ANNO'.") + print(f"\n 📌 E IL PUNTO CHE CAMBIA LA DECISIONE: **anche accettando lo stress a mano**") + print(f" **alla lettera**, k* = {k_mano:.2f}x -> mezzo-Kelly = {0.5*k_mano:.2f}x. **1,25x ci sta") + print(f" sotto**, 1,50x e' esattamente al bordo. Cioe': la riserva dello scettico, presa") + print(f" per buona senza ri-calibrarla, non basta a bocciare il gradino a 1,25x —") + print(f" bocciava 1,50x, che e' il gradino di cui si stava discutendo.") + print(f"\n ⚠️ SENSITIVITA' FUORI DAL CRITERIO (dichiarata dopo, non conta per il verdetto):") + rr3 = np.concatenate([r_del - 2 * se_drift / ANN, + np.full(int(round(len(r_del) / (ANN - 1))), -STRESS_A_MANO)]) + k3, _ = k_star(rr3) + print(f" se si credono ENTRAMBE le riserve insieme (drift -2 SE **E** -10% ogni anno)") + print(f" il drift atteso diventa {rr3.mean()*ANN:+.2%}/anno e k* = {k3:.2f}x.") + print(f" **A drift {'negativo' if rr3.mean()<0 else 'quasi nullo'} il problema non e' il gradino: e' che il libro non") + print(f" andrebbe tradato affatto, nemmeno a 1,00x.** Chi usa questa combinazione per") + print(f" bocciare 1,25x sta implicitamente chiedendo di spegnere anche il conto di oggi:") + print(f" e' una posizione legittima, ma va dichiarata per quello che e'.") + + print(f"\n (c) LIQUIDAZIONE — la domanda va posta sul MINIMO, non sulla chiusura.") + print(f" Con nozionale lordo G·E e margine di mantenimento {MM_RATE:.1%}, il conto e'") + print(f" liquidato quando il movimento avverso x soddisfa x ≈ (1-mm·G)/(G·(1-mm)).") + g_avg = sum(0.5 * W_TP * TPQ[a]["held"].mean() for a in ASSETS) + W_SKH * 0.124 + g_max = sum(0.5 * W_TP * TPQ[a]["held"].max() + 0.5 * W_SKH for a in ASSETS) + print(f" lordo MEDIO del libro {g_avg:.3f}·E · lordo MASSIMO mai voluto {g_max:.3f}·E") + m_worst_unit = max(abs(gpd_quantile(fm, 1e-4)), L_STRUCT_MIN) + print(f" il 'peggior minimo' usato qui e' il PIU' PESSIMISTA fra EVT 1/10.000" + f" ({-abs(gpd_quantile(fm,1e-4)):.2%})") + print(f" e il bound strutturale sul minimo ({-L_STRUCT_MIN:.2%}) = {-m_worst_unit:.2%} a k=1.") + print(f"\n {'k':>7}{'G max':>9}{'x liquidaz.':>14}{'min vissuto':>14}" + f"{'min EVT 1/10^4':>16}{'min STRUTT.':>14}{'margine':>10}") + for k in (1.00, 1.25, 1.50, 2.00, 3.00): + G = min(g_max * k, 2 * frac * k) + x_liq = (1 - MM_RATE * G) / (G * (1 - MM_RATE)) + print(f" {k:>6.2f}x{G:>9.3f}{x_liq:>14.1%}{abs(m_acc.min())*k:>14.2%}" + f"{abs(gpd_quantile(fm,1e-4))*k:>16.2%}{L_STRUCT_MIN*k:>14.2%}" + f"{x_liq/(m_worst_unit*k):>9.1f}x") + kliq = WIPE / abs(m_acc.min()) + print(f"\n replica dello scetticato ('servirebbe 12,3x sul peggior giorno vissuto',") + print(f" convenzione WIPE {WIPE:.0%}): {WIPE:.2f}/{abs(m_acc.min()):.4f} = {kliq:.1f}x <-- riprodotto") + + # ---------------------------------------------------------------- (7) + sezione("(7) IL VERDETTO — il criterio di (0), valutato") + esiti = {} + + # C1 + c1_val = 1.25 * L_STRUCT + esiti["C1"] = (c1_val <= 0.50, + f"1,25 x {L_STRUCT:.2%} = {c1_val:.2%} di equity persa nel peggior giorno " + f"POSSIBILE, contro la soglia del 50%. Margine {0.50/c1_val:.1f}x.") + # C2 + k_pess = ks_tab[(f"EVT troncata a {-L_STRUCT:.1%} (strutturale)", "-2 errori standard")] + esiti["C2"] = (1.25 <= 0.5 * k_pess, + f"k* pessimistico (drift -2 SE, coda troncata al bound strutturale) = " + f"{k_pess:.2f}x -> mezzo-Kelly = {0.5*k_pess:.2f}x. 1,25 e' il " + f"{1.25/k_pess:.0%} di quel k*.") + # C3 + G125 = min(g_max * 1.25, 2 * frac * 1.25) + x_liq125 = (1 - MM_RATE * G125) / (G125 * (1 - MM_RATE)) + m_evt125 = m_worst_unit * 1.25 + esiti["C3"] = (x_liq125 >= 3 * m_evt125, + f"peggior minimo intra-giorno (il piu' pessimista fra EVT 1/10.000 " + f"{-abs(gpd_quantile(fm,1e-4)):.2%} e bound strutturale {-L_STRUCT_MIN:.2%}) " + f"x1,25 = {-m_evt125:.2%} contro una soglia di liquidazione a {x_liq125:.1%}: " + f"margine {x_liq125/m_evt125:.0f}x (richiesto 3x).") + # C4 + r_2se = r_del - r_del.mean() + (drift_pt - 2 * se_drift) / ANN + dg = g_exact(r_2se, 1.25) - g_exact(r_2se, 1.00) + esiti["C4"] = (dg > 0, + f"col drift a -2 SE ({drift_pt-2*se_drift:.2%}/anno): g(1,25x)-g(1,00x) = " + f"{dg:+.2%}/anno.") + # C5 + fund = {} + for a in ASSETS: + d = pd.read_parquet(ROOT / "data" / "raw" / f"hlfund_{a.lower()}_1h.parquet") + fund[a] = float(d["funding"].values.mean()) * 8760.0 + esp_long = sum(0.5 * W_TP * TPQ[a]["held"].mean() for a in ASSETS) + costo_k1 = esp_long * np.mean(list(fund.values())) + guad = g_exact(r_del, 1.25) - g_exact(r_del, 1.00) + esiti["C5"] = (0.25 * costo_k1 < guad / 3.0, + f"funding medio (proxy Hyperliquid, 3 anni): " + f"BTC {fund['BTC']:.1%}/a, ETH {fund['ETH']:.1%}/a; esposizione LONG media di " + f"TP01 {esp_long:.3f}·E -> costo {costo_k1:.2%}/anno a k=1, " + f"costo extra al gradino +{0.25*costo_k1:.2%}/anno contro un guadagno extra di " + f"{guad:+.2%}/anno (soglia dichiarata: costo extra < 1/3 del guadagno extra = " + f"{guad/3:.2%}). ⚠️ E il costo NON e' modellato NEMMENO a k=1: il libro paga gia' " + f"~{costo_k1:.2%}/anno che nessun backtest del progetto sottrae — e' un difetto " + f"che il gradino non crea ma amplifica del 25%.") + # C6 + n_cap = sum(int(((0.5 * W_TP * TPQ[a]["held"].values + 0.5 * W_SKH) > frac).sum()) + for a in ASSETS) + n_tot = sum(len(TPQ[a]) for a in ASSETS) + esiti["C6"] = (False, + f"il cap di config e' un CLAMP, non un moltiplicatore: morde su {n_cap} " + f"osservazioni-asset su {n_tot} ({n_cap/n_tot:.3%}). Alzarlo NON produce k=1,25. " + f"In `config/live.json` non esiste una chiave di scala.") + + print() + for k in ("C1", "C2", "C3", "C4", "C5", "C6"): + ok, spieg = esiti[k] + print(f" {k} {'PASSA ' if ok else 'FALLISCE'} {spieg}") + print() + + passa = all(v[0] for v in esiti.values()) + print(f" ESITO COMPLESSIVO: {sum(v[0] for v in esiti.values())}/6 criteri passati" + f" -> {'AUTORIZZABILE' if passa else 'NON autorizzabile come proposto'}") + + print(f""" + LETTURA IN CHIARO + + 1. IL CONTROLLO DEL CRITICO E' CONFERMATO NELLA DIREZIONE, CORRETTO NELLA PORTATA, E IL SUO + NUMERO NON SI RIPRODUCE. Sui 10 giorni peggiori del sottostante il libro fa + {worst_und.book.sum():+.2%} in totale contro {worst_und.und.sum():+.1%} del mercato e guadagna in + {int((worst_und.book>0).sum())} di essi; il 12 marzo 2020 (-43,6%) fa {J.loc[J.und.idxmin(),'book']:+.2%}. **Ma + sui 160 giorni di crash il libro perde in media {crash.book.mean():+.3%}**: 'il libro guadagna + nei crash' vale sugli ESTREMI, non in generale, e l'esposizione di TP01 scende in modo + monotono al crescere del crollo ({Jx[(Jx.und>-0.05)&(Jx.und<=-0.02)].espo.mean():.3f} nella fascia -5/-2%, + {Jx[Jx.und<=-0.20].espo.mean():.3f} sotto il -20%). **Il libro non e' immune ai crash: e' immune ai crash + GROSSI**, perche' li' il trend si e' gia' girato. + ⚠️ E la sua LENTE si identifica dai valori pubblicati (+0,97 e +0,72 compaiono SOLO + nella riga canonica): ha usato `book_series('canonical')`, il BACKTEST di SKH01. Su + quella lente **il peggior giorno E' un giorno di ribasso** ({b_can.min():+.2%}, {b_can.idxmin().date()}), + quindi la frase 'il peggior giorno non e' un giorno di crash' e' vera solo sulla lente + LIVE — che e' quella giusta per decidere, ma non e' quella da cui viene il suo numero. + **Aveva ragione, con la lente sbagliata.** Si cita **-3,47% (canonica) / -3,94% (live)**; + lo scarto residuo di 0,09pp col suo -3,38% e' dell'ordine del difetto gia' noto + ('`data/raw` gitignored, il cron riscrive i parquet' — stesso codice, dati diversi). + + 2. LA CODA DEL LIBRO ESISTE, MA STA DALL'ALTRA PARTE DEL MERCATO. Il peggior giorno in + chiusura e' {J['book'].min():+.2%} il {i_worst.date()}, ed e' un **RIMBALZO** (+20,9%/+27,6%): SKH01 + short con stop al 2% che gappa a {skh_only.min():.2%} di sleeve, mentre TP01 e' flat. Secondo + peggiore dello sleeve: {skh_only.nsmallest(2).iloc[-1]:.2%} — **la coda di SKH01 e' UN giorno solo**. + ⚠️ E la caratterizzazione si INVERTE con la lente: sulla canonica il peggior giorno e' + un ribasso ({b_can.min():+.2%}, {b_can.idxmin().date()}). Va citata con la lente. + + 3. LE DUE RISPOSTE ALLA DOMANDA 'QUAL E' IL PEGGIOR GIORNO' DIFFERISCONO DI ~2x: + **{K['R_live'].min():+.2%} in chiusura, {m_acc.min():+.2%} al minimo intra-giorno** (mediana del rapporto + sui giorni in perdita {np.median(m_acc[neg]/K['R_live'].values[neg]):.2f}x). Chi valuta margine, daily-loss o stop di + conto sulla chiusura sbaglia di quel fattore, nel verso ottimista. ⚠️ E il minimo del + giorno peggiore e' un LIMITE SUPERIORE: la recon accoppiata cappa il wick allo stop + proprio nel giorno del gap-through. + + 4. LA STIMA DELLA CODA, CON LA SUA INCERTEZZA: chiusura **{-gpd_quantile(f_close,1e-3):.2%} a 1/1.000 giorni** + e **{-gpd_quantile(f_close,1e-4):.2%} a 1/10.000** (GPD, soglia {EVT_Q:.0%}, IC95 bootstrap a blocchi + [{-np.percentile(evt_boot_close['rl10000'],97.5):.2%}, {-np.percentile(evt_boot_close['rl10000'],2.5):.2%}]); Hill dice {-(_l[_k]*((_k/(len(K)*1e-4))**_xi)):.2%} a 1/10.000. + **I due stimatori non concordano** (xi {f_close['xi']:.2f} vs {_xi:.2f}) e si cita la BANDA: un fit di coda + su {len(K)} osservazioni e' un ordine di grandezza, non un numero. Il -10% in chiusura esce + a **1 ogni {min(1/pe/ANN,1/pe_hill/ANN):.0f}-{max(1/pe/ANN,1/pe_hill/ANN):.0f} anni**, contro 'ogni anno' assunto. + + 5. IL CONTO ARITMETICO, che l'EVT non puo' fare perche' estrapola solo il meccanismo + vissuto: dato il cap di config, il vol-target di TP01 (che converte un crash nel suo + **z-score**, rendendo il bound invariante alla volatilita') e il gap-through di SKH01, + il peggior giorno POSSIBILE a k=1,00x e' **{-L_STRUCT:.2%}** in chiusura e **{-L_STRUCT_MIN:.2%}** al + minimo. Bound analitico e controfattuale sui movimenti veri danno lo stesso numero — + ⚠️ ma NON sono indipendenti: convergono perche' entrambi sono guidati dal 12 marzo 2020. + **Quindi il -10% sta al {STRESS_A_MANO/L_STRUCT:.0%} del massimo strutturale: la TAGLIA e' plausibile, + la FREQUENZA no.** La riserva dello scettico e' RIDIMENSIONATA di 2-4 ordini di + grandezza, non falsificata. + + 6. E ANCHE PRENDENDO LA RISERVA ALLA LETTERA IL GRADINO A 1,25x PASSA: con -10% ogni anno + k* = {k_mano:.2f}x, cioe' mezzo-Kelly = {0.5*k_mano:.2f}x. **1,25x ci sta sotto; 1,50x e' al bordo** — + cioe' lo stress a mano bocciava il gradino DOPPIO, non quello singolo. Se invece si + credono ENTRAMBE le riserve insieme (drift -2 SE **e** -10% annuo) il drift atteso e' + {rr3.mean()*ANN:+.2%}/anno: a quel punto non e' il gradino a essere sbagliato, e' il libro — e chi + usa quella combinazione sta chiedendo di spegnere anche il conto di oggi. + + 7. 🚨 IL RISULTATO CHE NESSUNO CERCAVA, ED E' QUELLO CHE DECIDE: **il gradino non e' + implementabile con la strada che la regola del progetto dichiara pulita.** Il cap di + `config/live.json` e' un CLAMP, non un moltiplicatore: morde su {n_cap} osservazioni-asset + su {n_tot} ({n_cap/n_tot:.3%}). Alzarlo da 0,50 a 0,625 lascerebbe il libro a k=1,00x + in {n_tot-n_cap} giorni-asset su {n_tot}. Non esiste una chiave di SCALA in config: per k=1,25 + servirebbe cambiare `WEIGHT`/`W_TP01`/`W_SKH` in `src/live/book.py`, cioe' CODICE su un + percorso con soldi veri. **La domanda 'il gradino e' autorizzabile' era mal posta: prima + di essere autorizzato, il gradino va COSTRUITO** — e la costruzione e' la parte che + merita una revisione, non l'aritmetica del rischio, che passa con margine. + + 8. SOTTOPRODOTTO NON RICHIESTO, e vale piu' del gradino: **il funding dei perpetual non e' + modellato in NESSUN backtest del progetto**. Proxy Hyperliquid su 3 anni: {np.mean(list(fund.values())):.1%}/anno + pagato dai long, esposizione long media di TP01 {esp_long:.3f}·E -> **~{costo_k1:.2%}/anno** che il + libro paga davvero e che nessuna serie sottrae. E' ~{costo_k1/(mu*ANN):.0%} del drift de-luckato, + cioe' dello stesso ordine della correzione d'ancora x0,89 che il progetto ha misurato + con cura. Non lo crea il gradino: il gradino lo amplifica del 25%. + + COSA MI SMENTIREBBE, in ordine di gravita' + (a) un giorno in cui il libro perde piu' di ~{L_STRUCT*0.7:.1%} in chiusura: vorrebbe dire che + l'aritmetica della sez.(5) ha una gamba che non ho contato (un terzo strumento, un + ordine non nettato, uno stop non eseguito, il cap non applicato); + (b) uno z giornaliero peggiore di {zmin['BTC']:.1f} su BTC o {zmin['ETH']:.1f} su ETH con vol >= {tv:.0%}: + il bound e' CONDIZIONATO a quel massimo in campione e scala linearmente con esso; + (c) una configurazione in cui TP01 tiene piena convinzione ENTRANDO in un crash: nel + campione non succede mai, ed e' l'unica ragione per cui il controfattuale sfonda il + -10% e la realta' no. La mia frequenza stimata assume che resti rara; + (d) SKH01 in un regime in cui sta short molto piu' a lungo: la coda del libro e' la sua, e + la misuro su un campione in cui e' fuori mercato l'87,6% del tempo; + (e) un funding medio molto diverso su Deribit-USDC rispetto al proxy Hyperliquid: il + punto 8 e' misurato su un venue che non e' quello dove il libro esegue. + + GATE — cosa e' girato e cosa no + · `marginal_vs_tp01` NON APPLICABILE: non c'e' un flusso nuovo, k e' uno scalare (corr 1,00). + · `deflated_sharpe` NON APPLICABILE: lo Sharpe e' INVARIANTE a k. Questo lavoro non chiude + il buco strutturale, lo AGGIRA: misura il peggior giorno, che e' + l'unica grandezza del rischio su cui la scala non e' invariante. + · `implausible_sharpe` NON GIRATO come funzione (darebbe lo stesso verdetto a ogni k). Il + controllo di plausibilita' e' stato fatto a mano: nessuna cella qui + promette zero perdite, e la sez.(5) e' proprio la ricerca del + 'rischio FUORI dal dataset' che quel gate cerca. + · null del de-levering NON e' un gate qui: e' l'OGGETTO (la leva percorsa nei due versi). + · fortuna d'ancora EREDITATA (x{DP.DELUCK} misurato). ⚠️ NON RI-GIRATA su questo taglio, e va + detto: il peggior giorno e' un evento SINGOLO, e una banda d'ancora + su un massimo non e' la stessa statistica di una banda su una media. + Cio' che ne risente e' il drift (gia' de-luckato), non il bound. + · causalita' EREDITATA: nessun segnale nuovo, solo serie gia' validate. + · replica di controllo SUPERATA: drift/vol/Sharpe/peggior giorno de-luckati (4/4), k* MC vs + forma chiusa, scomposizione libro=gambe (max|diff| 0,0e+00), + marginali del gap (p50/p90/p99 identiche), 12,3x di liquidazione. + PARZIALE su 'k*=2x sotto stress' (qui 2,95x fine / 3,5x sulla loro + griglia): stessa direzione e ordine, cella diversa. + · risoluzione la curva di crescita e' in forma CHIUSA (E[log]): zero rumore MC. + Le celle della tabella (6a) sono APPAIATE. L'incertezza residua e' + quella del FIT di coda, ed e' negli IC bootstrap e nella banda + fra stimatori. + """) + n_boot_tot = 2 * EVT_BOOT + n_celle = (2 * len(EVT_Q_SENS)) + n_boot_tot + 2 + 2 + len(code) * len(scenari_drift) \ + + 5 + len(KSG) + 1 + 5 + 3 + 2 + print(f""" + GRIGLIA DICHIARATA — {n_celle} valutazioni, contate al RIALZO: + {2*len(EVT_Q_SENS)} fit GPD (2 serie x {len(EVT_Q_SENS)} soglie) · {n_boot_tot} ri-fit bootstrap · 2 Hill · 2 t di Student + {len(code)*len(scenari_drift)} celle k* (coda x drift, APPAIATE) · 5 righe di stress · {len(KSG)} celle della griglia + coarse di growth_policy · 1 sensitivita' combinata · 5 righe di liquidazione · + 3 lenti del peggior giorno · 2 varianti di controfattuale (chiusura e minimo). + ⚠️ **Nessuna cella e' stata SELEZIONATA**: la soglia EVT e' dichiarata a priori ({EVT_Q:.0%}) e le + altre tre girano come sensitivita'; il criterio di (0) e' congelato prima delle sezioni 4-6. + Percio' non c'e' un deflated-Sharpe da calcolare: non c'e' un massimo scelto su cui barare. + """) + print("\n" + "=" * 110) + print(" IL GRADINO DI LEVA A 1,25x: AUTORIZZABILE A CONDIZIONE DI COSTRUIRE PRIMA UNA CHIAVE DI SCALA ESPLICITA IN CONFIG") + print("=" * 110) + print(f"\n [tempo totale {time.time()-t_start:.0f}s]") + + +if __name__ == "__main__": + main() From 8eaf7aa730a205d9b67e6958f7046ae30e1135d5 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 22:12:01 +0000 Subject: [PATCH 044/114] =?UTF-8?q?CLAUDE.md:=20terza=20ondata=20=E2=80=94?= =?UTF-8?q?=20due=20gate=20d'attesa=20eliminati,=20il=20cap=20e'=20un=20cl?= =?UTF-8?q?amp=20non=20un=20moltiplicatore,=20e=20il=20funding=20non=20e'?= =?UTF-8?q?=20in=20nessun=20backtest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 586c582..9a48cd3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2312,6 +2312,126 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis su un assunto scritto in un **commento a riga 100**. *La ricerca ha smesso di essere il vincolo binding il 26/07, e due ondate da 29 filoni lo hanno CONFERMATO invece che ribaltarlo.* +- 🌊 **TERZA ONDATA 2026-08-22 notte (4 filoni) — DUE GATE D'ATTESA ELIMINATI, 2 difetti strutturali + trovati, 0 candidati.** Diario `docs/diary/2026-08-22-wave2-xs01-funded.md`; registro + `docs/research/RESULTS-0822.md` §30-33. **Libro, pesi, cron, config INVARIATI.** + ✅ **(1) `GATE PROP-01` gamba (a) — LISTINO: PASS 13/13.** La verifica a **€0** che il hook e il + critico avevano indicato come il vero vincolo. 📌 **Metodo: non leggere il sito della firm, + interrogare il VENUE su cui la firm esegue.** HyroTrader esegue su **Bybit**; + `api.bybit.com/v5/market/instruments-info?category=linear` (pubblico, tokenless, **833 strumenti**) + → **13/13 gambe U13 `Trading`, `LinearPerpetual`, min notional $5**, leva 50-150x; **6/6** anche le + gambe non testabili. **Lo short e' STRUTTURALE su un perp**, non un permesso. + ⚠️ Non prova che la firm non abbia una **propria** lista ristretta: la sua pagina dichiara "700+ + pairs" su un venue che ne ha 833 (coerente, ma e' la *sua* dichiarazione) → si chiude con una + domanda al supporto, non con un dataset. + ✅ **VALIDAZIONE del modello:** la firm dichiara **daily DD 4% · max loss 6%**, e + `r0725_hyro.py:48-49` usa gia' `EV_TARGET,EV_DD,EV_DL = 0.10,0.06,0.04` → **le regole modellate dal + 25/07 sono quelle vere.** + 🚨 **CORREZIONE ECONOMICA: la quota d'ingresso NON e' un costo affondato** — e' un *"Refundable + Challenge Deposit, returned in full alongside your first funded payout"*. Il progetto l'ha sempre + modellata come spesa persa. **Resta vero che immobilizza il 91% del conto ($579 su $635), non che lo + brucia**: si perde solo nel ramo senza payout. **E' una struttura a OPZIONE, non una fee** → l'EV del + biglietto va rifatto (gamba (c) del gate). *Qualita' delle fonti dichiarata: listino **primario e + misurato**; regole DD **primarie dichiarative**; rimborsabilita' **dichiarativa + secondarie + concordi**, non misurata.* + ✅ **(2) `GATE PROP-01` gamba (b) — chiusa OGGI invece che il 2026-10-31: PASS 23/23.** + Delta appaiato mediano **+0,1162** [p10-p90 +0,087/+0,152], **23/23 offset e 230/230 celle congiunte + positive** (soglia +0,05), su **griglia congiunta PIENA** 23 offset × 10 fasi — non un campionamento + e non una somma di marginali. Riproduzione **7/7 esatta**, bit-exact **3/3 a 0,0**. + 📌 **La data non aspettava un dato: aspettava che qualcuno misurasse il COSTO** (0,6 s per offset; + i "~9 min" erano di una computazione che la misura non usa). **Secondo gate d'attesa eliminato in una + sera, per la stessa ragione del primo.** + 🚨 **Ma il numero operativo scende ANCORA: P(≥50 €/g) da €600 in 36 mesi = 4,4% [2,5–8,2%], P(zero) + 35,3%** (l'offset canonico era al **91° pctl**; all'ancora canonica la macchina ridice 7,7% → e' + **fortuna d'ancora, non disaccordo**). **Traiettoria del numero in una giornata: 42% → 7,8% → 4,4%.** + 📌 **E la distorsione dichiarata DUE VOLTE e mai rimossa — *"l'ottimo si appoggia a SKH01, lo sleeve + con la fortuna d'ancora piu' grande"* — e' MISURATA E REFUTATA nella forma forte:** a **38/25/38** + SKH01 sta **allo stesso peso del LIVE**, la sua ancora si cancella nella differenza appaiata, e li' + il delta e' **il piu' alto dei tre (+0,120)** con la banda **piu' stretta**. **L'ampiezza della banda + scala col differenziale di peso su SKH01** → **cio' che paga e' XS01, non un sovrappeso dello sleeve + fragile.** + ⚠️ **Due avvertenze:** (i) **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 + diversamente (−33% contro −44%). ***Un gate superato non e' un numero confermato.*** (ii) il + surrogato del 22/08 era finito vicino al vero **per la cancellazione di due asimmetrie opposte**: + **il numero era giusto, il metodo no.** **Smentitore residuo fuori perimetro:** l'ancora giornaliera + di **TP01** (24 ore, canonica 00:00 UTC = la piu' fortunata delle 24) non e' toccata da questa misura. + 📌 **(3) GATE-RECON — il difetto `advance()` ribalta UN gate su tre, e in quello due criteri su tre.** + Audit di **sola lettura provata** (md5+mtime di **ogni** `data/paper_*` invariati). + **`STATARB 27/09 — riparata: RITIRO · attuale: CANDIDATO AL DEPLOY`** (Sharpe **+1,96 → −2,02**, + maxDD 2,24% → 15,19%); XSR01 e DVOLSPREAD danno **lo stesso verdetto** in entrambe le lenti. + ✅ **La premessa "rigenerare non perde la finestra" e' ora CONFERMATA PER VIA DIRETTA:** + `data/_feed_backup/*.prebuild.bak` conserva il vintage pre-riscrittura → **0 barre chiuse cambiate su + 1.761.259**; l'unica riga che cambia per file e' **l'ultima del vintage, che era aperta** = *il + difetto stesso, in chiaro*. ⚠️ Limite: `hl_*_1d` e `dvol_*` **non hanno vintage su disco** → per + XSR01/DVOLSPREAD l'evidenza resta **indiretta**. + 🚨 **Il veto d'integrita' di DVOLSPREAD NON scatta: 28 attive/28 = 100%.** Una serie che registra + **2 minuti di mercato al giorno** passa al 100% un veto progettato apposta per non far decidere su + dati mancanti — **non e' taratura**: il veto conta *se il DVOL c'era*, non *quanto dura la barra su + cui c'era* (**assi ortogonali**); la stessa soglia 0,80 sulla quota di barre **chiuse** leggerebbe + **0%**. **Variante nuova: una barra presente non e' una giornata presente.** + 🚨 **Il caso peggiore NON e' quello coi numeri piu' assurdi:** XSR01 sbaglia di **×6,6** e + DVOLSPREAD di ×3,0 **senza cambiare verdetto**; **STATARB ha |Sharpe| quasi identico (1,96 contro + 2,02) e cambia SEGNO** — ed e' il gate piu' vicino. Un controllo che cercasse *"numeri troppo belli"* + **lo lascerebbe passare**; `implausible_sharpe`, girato per la prima volta sulle serie forward, dice + **"ok" in entrambe le lenti** (e chiede n≥30, quindi 2 serie su 3 **non sono giudicabili**). + **La firma da cercare e' la VOL forward molto sotto quella del backtest** (7,88% contro 31,42%). + 📌 **QUANDO riparare, con un numero:** con **rigenerazione zero barre perse a qualunque data**; senza, + riparare **oggi** lascia **36/90** barre vere a STATARB (62/90 XSR01, 63/91 DVOLSPREAD), **alla + vigilia 1** — e la riparazione parziale **non contamina la finestra, la ACCORCIA** (una barra troncata + porta ~1/370 della varianza *e* del rendimento di una vera, quindi non pesa). ⚠️ La diagnostica + pre-registrata del 25/07 (statica sempre-short) **non e' affetta** perche' si ricalcola dal pannello + → al gate si confronterebbe **un benchmark giusto con un numero di strategia sbagliato**, e il + margine sembrerebbe **+5,83 invece di +1,87**. + 🚨 **(4) WORST-DAY — la riserva sulla leva e' mal tarata di 2-4 ORDINI DI GRANDEZZA, ma il knob di + scala NON ESISTE.** Il gradino 1,00x→1,25-1,50x vale **14,7 → 11,6 anni** al capitale-rendita + (~€300/mese di versamenti equivalenti) ed era bloccato da un parametro **scelto a mano** ("un giorno + a −10% ogni anno"). **Il peggior giorno ha due risposte:** **−3,94% in chiusura** (2020-03-13) e + **−7,33% al minimo** intra-giorno (rapporto mediano **1,89×**). Coda GPD: **−4,55% a 1/10.000 + giorni** [−6,34/−3,08] ⚠️ **ma Hill dice −7,34% (ξ 0,34 contro 0,09): i due stimatori NON concordano + → si cita la banda.** Conto **aritmetico** (cap + vol-target + gap-through di SKH01): peggior giorno + **possibile** a k=1,00x **−14,32%**. 📌 **Il −10% sta al 70% del massimo strutturale: la TAGLIA e' + plausibile, la FREQUENZA no** (1 ogni 67-6.075 anni contro "ogni anno"). + 📌 **E la forma della coda non e' la leva: muove k* di 1-2 unita', il DRIFT di 6,7.** Anche prendendo + lo stress **alla lettera**, k* 2,95x → mezzo-Kelly **1,48x**: *lo stress bocciava il gradino DOPPIO, + non quello singolo*. C1-C5 passano tutti. + ⚠️ **Il libro NON e' immune ai crash, e' immune ai crash GROSSI:** sui 160 giorni di crash perde in + media −0,505%, e l'esposizione di TP01 scende **monotona** col crollo (0,096 → **0,042** sotto il + −20%) *perche' li' il trend si e' gia' girato*. **Il suo peggior giorno non e' un crash: e' uno + SHORT SQUEEZE** (SKH01 short con stop 2% che gappa a **−15,77%** di sleeve mentre TP01 e' flat; il + secondo peggiore fa −6,67% → **la coda di SKH01 e' un giorno solo**). + 🚨 **IL RISULTATO CHE DECIDE, e nessuno lo cercava: il cap di `config/live.json` e' un CLAMP, non un + MOLTIPLICATORE.** Morde su **3 osservazioni-asset su 5.650 (0,053%)**; alzarlo da 0,50 a 0,625 + lascerebbe il libro a k=1,00x in **5.647 giorni-asset su 5.650**. ✅ **Verificato:** le chiavi sono + `max_notional_per_asset_usd/_frac`, `min_order_usd`, `disaster_sl_pct`, `max_data_age_days`, + `skh_feed_max_age_min` — **non esiste una chiave di scala**; servirebbe toccare `WEIGHT`/`W_TP01`/ + `W_SKH` in `src/live/book.py`, cioe' **codice** su un percorso con soldi veri. + **⇒ La regola *"ogni cambio di SCALA passa dal cap di config, non da `target_vol`"* NON E' + IMPLEMENTABILE COME SCRITTA.** *Due mesi di discussione su un knob inesistente.* + **Il gradino a 1,25x e' AUTORIZZABILE A CONDIZIONE DI** costruire prima una **chiave di scala + esplicita** in config **e** rifare `r0726_fee_sensitivity`, la cui conclusione *"liquidation fee + irrilevante"* **vale solo a nozionale lordo ≤1x** (ed era gia' legata a un test di guardia sul cap). + ⚠️ **Nota dovuta:** credendo **entrambe** le riserve insieme (drift −2SE **e** −10% annuo) il drift + atteso e' **−0,61%/anno** → *non e' il gradino a essere sbagliato, e' il libro; chi usa quella + combinazione sta chiedendo di spegnere anche il conto di oggi.* + 🚨 **(5) IL FUNDING DEI PERPETUAL NON E' MODELLATO IN NESSUN BACKTEST — e tocca ogni numero del + progetto.** ✅ **Verificato:** **zero occorrenze** di `funding` in `src/backtest/harness.py`, + `src/strategies/trend_portfolio.py`, `src/portfolio/{sleeves,portfolio}.py` **e in tutto + `src/live/`**. Proxy Hyperliquid 14,8%/anno × esposizione long media di TP01 (0,103·E) = + **~1,53%/anno che il libro paga davvero** = **il 9% del drift de-luckato**, cioe' **lo stesso ordine + della correzione d'ancora ×0,89** che il progetto ha misurato con cura. **Il progetto ha prezzato + fee, slippage, min-order, pavimento IB, haircut, fortuna d'ancora, degrado d'esecuzione, look-ahead, + split, rischio di venue e fisco — e mai il costo di tenere una posizione aperta su un perpetual.** + **REGOLE NUOVE:** (a) **una data in un gate va giustificata col COSTO della misura, non con la sua + difficolta' percepita** — due gate d'attesa eliminati in una sera perche' nessuno aveva contato i + secondi; (b) **una verifica esterna a €0 va fatta PRIMA di qualunque misura che ne dipenda**, e si + fa **sul venue, non sul sito** di chi la dichiara; (c) **un gate superato non e' un numero + confermato**: differenze e livelli ereditano la fortuna d'ancora in modo diverso; (d) **un veto + d'integrita' sorveglia l'asse su cui e' stato scritto** — contare *se* il dato c'era non dice + *quanto* del giorno copriva; (e) **una regola operativa va provata contro il codice che dovrebbe + eseguirla**, o si discute per mesi di un knob che non esiste; (f) **quando due stimatori di coda non + concordano si cita la banda**, non quello che conviene. + - **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso: cross-sectional su universo Hyperliquid certificato (XS01) → portafoglio Sharpe ~1.55. - **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.** From d930492d479d4224562dc4f9223489cb4c355d6d Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 22:38:38 +0000 Subject: [PATCH 045/114] =?UTF-8?q?research(wave-0822):=20CC01=20riaperto?= =?UTF-8?q?=20col=202022=20dentro=20=E2=80=94=20resta=20scartato,=20ma=20l?= =?UTF-8?q?a=20liscezza=20era=20una=20proprieta'=20della=20COLONNA=20(HL?= =?UTF-8?q?=204,2=20bps=20vs=20Deribit=20106,2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 78 ++ scripts/research/r0822d_cc01_2022.py | 1044 ++++++++++++++++++++++++++ 2 files changed, 1122 insertions(+) create mode 100644 scripts/research/r0822d_cc01_2022.py diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index d7ed9f8..31f6992 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -699,6 +699,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 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%** | | 33 | WORST-DAY | il peggior giorno del libro, MISURATO: il gradino di leva e' autorizzabile? | **LEAD** — l'aritmetica passa, **il knob non esiste** | +| 34 | CC01-2022 | il carry riaperto sui dati che gli mancavano per costruzione | **RESTA SCARTATO** — per la ragione giusta, ma non quella pubblicata | | 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 | @@ -1556,3 +1557,80 @@ irrilevante"* **vale solo a nozionale lordo ≤1x**, ed era gia' legata a un tes ⚠️ **Nota dell'agente, dovuta:** se si credono **entrambe** le riserve insieme (drift −2SE **e** −10% annuo) il drift atteso e' **−0,61%/anno** → *a quel punto non e' il gradino a essere sbagliato, e' il libro, e chi usa quella combinazione sta chiedendo di spegnere anche il conto di oggi.* + +--- + +## 34 — CC01-2022 (il cash-and-carry riaperto sul dato che gli mancava) + +`scripts/research/r0822d_cc01_2022.py` (~8 s, zero rete). **47 celle** contate al rialzo. + +**Perche' si poteva riaprire:** il verdetto del 26/06 poggiava su una ragione **falsificabile e +strutturale** — *"lo storico funding parte dal 2023-05 → manca il 2022, i rischi di coda sono fuori dal +dataset"*. Stamattina BASIS-CALENDAR ha ricostruito **30/30 futures trimestrali scaduti dal 2018-09, +col deleveraging 2022 dentro**. E' l'unica base su cui questo progetto riapre un filone: **dati nuovi.** + +✅ **PRIMA della ricerca, la persistenza** (il critico aveva segnalato che i 20 MB vivevano **solo in +uno scratchpad di sessione**): copiati in **`data/raw/fut_deribit/`** — 72 file, 20,3 MB, 601.704 righe +(BTC 240.150 barre orarie / ETH 233.334, 34 contratti per asset, 2018-09→2026-08) + 64.110 ore di +funding e indice per asset dal 2019-04. **Sottocartella con prefisso, mai il namespace nudo** +(lezione SOL: il guardrail *"solo dati certi"* vive nell'**assenza** di un file). +✅ **Guardrail verificato dal coordinatore, non assunto:** `load_data('BTC-30SEP22')` e +`load_data('SOL')` → **`FileNotFoundError`**; `load_data('BTC','1h')` → 70.311 barre; namespace nudo +pulito. **Il cron NON li rinfresca** (istantanea al 22/08, contratto in corso troncato) e restano +**fuori dal backup** perche' ricostruibili con ~2.700 chiamate pubbliche. + +📌 **Q1 — nel 2022 il carry si INVERTE, e resta invertito per mesi.** Funding annuo **BTC −2,26% / +ETH −6,08%** (contro +16,5/+17,3% nel 2021); giorni a funding negativo **53,2%/58,9%**; basis mediano +del front **12,64% → 1,26%** (BTC) e **12,95% → −0,61%** (ETH). Peggior episodio: **ETH −6,70% di +funding cumulato in 170 giorni**; BTC −3,98% in 71 giorni nel **marzo-maggio 2020** = **un secondo +buco che CC01 non aveva.** + +**Q2 — la scala, netta fee:** + +| lente | Sharpe | maxDD | +|---|---|---| +| CC01 come pubblicato (funding HL, 2023-05+) | **+12,93** | 0,78% | +| stesso modello, funding **Deribit**, stessa finestra | +7,88 | 0,87% | +| stesso modello, Deribit, **finestra piena (col 2022)** | **+3,72** | 6,43% | +| + base MTM, premio liscio 24h (la lente di HL) | +1,33 | 6,99% | +| + base MTM, **premio grezzo** | +0,22 | **25,54%** | +| **carry ESEGUIBILE a 2 gambe, direzione CC01** | **−0,42** | 21,80% | + +📌 **E il salto piu' grande NON e' il 2022: e' il VENUE** (+5,04 passando da HL a Deribit, stessa +finestra e stesso modello). +⚠️ **Correzione che l'agente ha fatto a se stesso sulla propria tabella:** aveva scritto *"il buco piu' +grande non era il 2022"* — falso, e' il pezzo singolo maggiore (+4,16). **Il punto vero e' piu' forte: +da solo non basta** (7,88→3,72 fa **ancora** scattare `implausible_sharpe`), e i due pezzi mai +dichiarati il 26/06 valgono insieme altrettanto. + +**Gate:** 🚨 **`implausible_sharpe` 5/5** sulle varianti `r=funding` — **compresa quella col 2022 +dentro**; **pulito** su tutte le varianti con la base o la seconda gamba. 📌 *Il gate NATO da CC01 +continua a segnalare CC01, e smette esattamente quando il rischio entra nel campione — e li' l'edge +non c'e'.* `marginal_vs_tp01`: **DILUTES** (sign +1) / **HEDGE** (sign −1). `deflated_sharpe` **0,363 +FAIL** su 47 trial (massimo atteso dal solo rumore **+0,24 > +0,11** del candidato). Null del +de-levering **REFUTED** sulla versione a 2 gambe. **Controllo positivo di causalita'**: togliendo lo +`.shift(1)` il numero **sale** (6,85→7,39) → il test ha potenza. Ancora: fortuna **+0,005 = nessuna**. + +**Q3-Q4 — i muri non strutturali, riprezzati.** Lotto minimo BTC $10 / ETH $1 → **l'eseguibilita' non +e' il vincolo** (**5ª volta in questa ondata**); margine iniziale $13. ⚠️ **Ma tutti i futures datati +Deribit sono INVERSE: non esiste una linea USDC datata** — **stesso muro delle opzioni**, e il +collaterale in BTC e' esso stesso esposizione direzionale. **Non risolto**, serve una verifica sul +conto reale. **MAE per trade: −11,68% sulla storia piena → leva max 7,8x/gamba**, contro −3,64% → +20,9x nella sola finestra di CC01 = **2,7× di leva di troppo**; sul carry puro con base MTM, MAE 90g +−16,57% → **5,6-6,4x** contro le **~80x** che un maxDD dello 0,8% farebbe credere sostenibili. +⚠️ **Correzione a BASIS-CALENDAR:** il costo di roll e' **0,44%/anno**, non ~1,4% — quello contava il +roll **su entrambe le gambe**, ma **il perpetuo non si rolla**. Correggerlo **non crea un edge**. + +🚨 **IL SOTTOPRODOTTO CHE VALE PIU' DEL VERDETTO: la liscezza era una proprieta' della COLONNA, non del +rischio.** sd del Δ-premio giornaliero: **4,2 bps** (`premium` di Hyperliquid) contro **106,2 bps** +(Deribit grezzo) = **25-31×**. Quindi la frase del 26/06 — *"il mark-to-market della base sgonfia lo +Sharpe solo 13→11, il basis-from-data NON e' il rischio vero"* — era **un artefatto della fonte**: +su Deribit lo stesso esperimento fa **10,87 → 1,65 (liscio) / 0,37 (grezzo)** sulla **stessa finestra**. +📌 **Secondo sottoprodotto:** convenzione **inverse ≠ lineare** vale −0,54 di Sharpe e −2,6pp di CAGR +(BTC), concentrati nel 2020-21 — su una coppia inverse con nozionali pareggiati in USD **resta un delta +residuo ≈ −basis, che vale piu' dell'intero premio**. +⚠️ **Ipotesi dell'agente smentita in sessione:** il gate causale di CC01 **dodgia** il 2022 (−0,9% +contro −6,0%, l'85% della perdita) perche' il funding negativo e' **persistente**. + +**VERDETTO: `CC01 COL 2022 DENTRO: RESTA SCARTATO — e per la ragione giusta, ma quella pubblicata era +necessaria e non sufficiente.`** diff --git a/scripts/research/r0822d_cc01_2022.py b/scripts/research/r0822d_cc01_2022.py new file mode 100644 index 0000000..a1ddf0c --- /dev/null +++ b/scripts/research/r0822d_cc01_2022.py @@ -0,0 +1,1044 @@ +#!/usr/bin/env python3 +""" +r0822d_cc01_2022.py — filone CC01-2022 (ondata 2026-08-22, terza tornata) + + RIAPRIRE UN FILONE CHIUSO SULL'UNICA BASE CHE QUESTO PROGETTO ACCETTA: DATI NUOVI. + +IL FILONE E LA SUA RAGIONE DI MORTE + CC01 (cash-and-carry delta-neutral, `scripts/research/cash_carry_hl.py`) fu chiuso il + 2026-06-26 con un verdetto molto specifico: il premio di funding e' REALE (~+8-14%/anno, + positivo ogni anno, ortogonale a TP01), ma lo Sharpe modellato 11-13 e' un ARTEFATTO + perche' i rischi di coda sono *strutturalmente fuori dal dataset*: + + "lo storico funding parte dal 2023-05 -> manca il 2022 (deleveraging, funding + negativo, basis blow-out)." + + Quella e' una ragione FALSIFICABILE, non un giudizio: dice esattamente quale dato + ribalterebbe la conclusione. Fino a stamattina quel dato non c'era. + +COSA E' CAMBIATO OGGI + Il filone BASIS-CALENDAR (r0822_basis_calendar.py) ha ricostruito da Deribit pubblico + i 30/30 futures trimestrali SCADUTI (i nomi sono deterministici, e + `get_tradingview_chart_data` serve la storia di un contratto scaduto) piu' il funding + e l'indice orario del perpetual: **dal 2019-04, col deleveraging 2022 dentro**. + E' esattamente la coda che a CC01 mancava per costruzione. + +LE QUATTRO DOMANDE (in ordine di importanza, non di comodita') + Q1 Come si comporta il carry NEL 2022? Si comprime, si annulla, o si INVERTE? + Quanto dura e quanto e' profondo il peggior episodio? + Q2 Con la coda dentro, lo Sharpe 11-13 quanto diventa? E `implausible_sharpe` — + il gate che nacque proprio da CC01 — segnala ancora? + Q3 I due muri NON strutturali ("non eseguibile a $600", "procyclico") reggono + coi numeri di oggi, su Deribit, a due gambe? + Q4 Il rischio che ha ucciso CC01 e' il MODELLO, non i dati: un carry delta-neutral + sembra liscio perche' il P&L a scadenza non vede il ROLL ne' il MARGINE. In un + blow-out lo short perde SUBITO e il guadagno arriva a scadenza: se il conto non + regge il margine, la posizione muore nel punto peggiore. + +COSA QUESTO SCRIPT *NON* RIFA' + Il premio a termine (implicito +7,33% contro funding realizzato +6,48% -> incassabile + +0,85%/anno con IC95 che contiene lo zero) e' gia' misurato dal filone BASIS-CALENDAR + e non viene ri-misurato qui. Qui la domanda e' un'altra: **il 2022**. + +CONVENZIONI DICHIARATE + * Nessuna serie di prezzi viene mai incollata: la posizione vive su un CONTRATTO, e al + roll si chiude e si riapre pagando il round-trip. Si incollano solo i RENDIMENTI. + * I futures datati Deribit sono INVERSE (settlement BTC/ETH): il P&L e' calcolato nella + convenzione del venue (BTC) e riportato in USD all'indice. La versione lineare e' + calcolata accanto, e la differenza si misura invece di assumerla trascurabile. + * Fee taker REALE letta dal venue: 3,5 bps/lato per gamba. Slippage dichiarato a parte. + * Leva per-gamba lambda=1 (nozionale per gamba = equity; lordo 2x) salvo dove indicato. + +USO + nice -n 19 timeout 900 uv run python scripts/research/r0822d_cc01_2022.py + +RETE: questo script NON scarica nulla (il dato e' su disco). L'unica chiamata opzionale e' +la lettura delle specifiche strumento, che e' gia' in cache; se manca, si dichiara. +""" +from __future__ import annotations + +import calendar +import datetime as dt +import json +import math +import os +import shutil +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +import altlib as A # noqa: E402 + +# ------------------------------------------------------------------ costanti +UTC = dt.timezone.utc +MONTHS = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", + "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] +ASSETS = ("BTC", "ETH") +HOURS_Y = 365.25 * 24 +SQ365 = math.sqrt(365.25) + +FEE_SIDE = 0.00035 # taker Deribit letto dal venue (get_instruments), per gamba +SLIP_BPS = 2.0 # slippage dichiarato sul datato (meta' spread del front, vedi [6]) +ANNUAL_COST_CC01 = 0.02 # il drag annuo che CC01 usava: si tiene IDENTICO per replicare + +# Margine: DICHIARATO, fonte secondaria (documentazione Deribit futures). Si fa la +# sensibilita' invece di fingere precisione: il risultato e' una LEVA, e la leva scala +# come 1/(mm_tot + |MAE|) -> con |MAE| >> mm_tot il valore di mm quasi non conta. +MM_LEG = 0.00575 # maintenance margin per gamba (0,575% del nozionale) +IM_LEG = 0.01 # initial margin per gamba (1%) + +# Finestre +W_CC01 = pd.Timestamp("2023-05-12", tz="UTC") # inizio dello storico funding di CC01 (HL) +HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC") + +FUT_DIR = ROOT / "data" / "raw" / "fut_deribit" +SCRATCH_CACHE = Path(os.environ.get( + "R0822_SCRATCH", + "/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad")) / "basis_cache" + +_line = "=" * 96 + + +def hr(t: str) -> None: + print("\n" + _line) + print(t) + print(_line) + + +# ========================================================================== +# 0. PERSISTENZA — il dato viveva SOLO in uno scratchpad di sessione +# ========================================================================== +def last_friday(year: int, month: int) -> dt.date: + d = dt.date(year, month, calendar.monthrange(year, month)[1]) + while d.weekday() != 4: + d -= dt.timedelta(days=1) + return d + + +def quarterly_names(asset: str, y0: int = 2018, y1: int = 2027): + out = [] + for y in range(y0, y1 + 1): + for m in (3, 6, 9, 12): + lf = last_friday(y, m) + out.append((f"{asset}-{lf.day}{MONTHS[m - 1]}{str(y)[2:]}", + pd.Timestamp(lf.year, lf.month, lf.day, 8, tz="UTC"))) + return out + + +def persist_dataset() -> dict: + """Copia il dataset dallo scratchpad a `data/raw/fut_deribit/` se non c'e' gia'. + + NAMESPACE — la lezione SOL del 22/08: `load_data` NON ha una whitelist, il guardrail + "solo dati certi" vive nell'ASSENZA di un file, e mettere un parquet nel namespace + NUDO (`btc_1h.parquet`) lo disattiva in silenzio. Qui i file stanno in una + SOTTOCARTELLA con prefisso `fut_` (accanto a `hl_`/`eq_`/`eqx_`/`alt_`/`fut_`), quindi + (a) nessun nome puo' collidere con `{asset}_{tf}.parquet` e (b) il guardrail resta + verificabile, e viene VERIFICATO sotto, non assunto. + """ + hr("[0] PERSISTENZA DEL DATASET — 20 MB che vivevano solo in uno scratchpad di sessione") + FUT_DIR.mkdir(parents=True, exist_ok=True) + have = sorted(p.name for p in FUT_DIR.glob("*.parquet")) + copied = 0 + if SCRATCH_CACHE.exists(): + for src in sorted(SCRATCH_CACHE.glob("ohlc_*.parquet")): + dst = FUT_DIR / src.name[len("ohlc_"):] + if not dst.exists(): + shutil.copy2(src, dst) + copied += 1 + for a in ASSETS: + src = SCRATCH_CACHE / f"fund_{a}.parquet" + dst = FUT_DIR / f"fund_{a}.parquet" + if src.exists() and not dst.exists(): + shutil.copy2(src, dst) + copied += 1 + print(f" scratchpad presente ({SCRATCH_CACHE}) -> copiati {copied} file nuovi") + else: + print(f" scratchpad ASSENTE ({SCRATCH_CACHE}) — si usa cio' che e' gia' su disco") + + files = sorted(FUT_DIR.glob("*.parquet")) + if not files: + print("\n ERRORE: nessun dato. Il dataset si ricostruisce da Deribit pubblico con") + print(" scripts/research/r0822_basis_calendar.py (fetcher rate-limited, ~45 min).") + raise SystemExit(2) + + rows, bad = 0, [] + per_asset = {a: dict(contratti=0, barre=0) for a in ASSETS} + for f in files: + try: + n = len(pd.read_parquet(f, columns=["ts"])) + except Exception as exc: # noqa: BLE001 + bad.append((f.name, type(exc).__name__)) + continue + rows += n + if f.name.startswith("fund_"): + continue + a = f.name.split("-")[0] + if a in per_asset: + per_asset[a]["contratti"] += 1 + per_asset[a]["barre"] += n + tot_mb = sum(f.stat().st_size for f in files) / 1e6 + print(f" {FUT_DIR.relative_to(ROOT)}: {len(files)} file, {tot_mb:.1f} MB, {rows:,} righe") + for a in ASSETS: + print(f" {a}: {per_asset[a]['contratti']} file di trimestrali, " + f"{per_asset[a]['barre']:,} barre orarie (uno di questi ha <200 barre e il") + print(f" caricatore lo scarta: sotto ne compaiono 34, non e' un dato mancante)") + if bad: + print(f" ⚠️ file illeggibili: {bad}") + + # ---- GUARDRAIL: verificato, non assunto ----------------------------- + from src.data.downloader import load_data, DATA_DIR # noqa: PLC0415 + esiti = [] + for nm, _ in quarterly_names("BTC")[:2] + quarterly_names("ETH")[:2]: + for tf in ("1h", "5m"): + p = DATA_DIR / f"{nm.lower()}_{tf}.parquet" + esiti.append(p.exists()) + collide = any(esiti) + try: + load_data("BTC-30SEP22", "1h") + guard = "ROTTO (load_data ha trovato un contratto!)" + except FileNotFoundError: + guard = "INTATTO (FileNotFoundError)" + ok_btc = True + try: + load_data("BTC", "1h") + except Exception: # noqa: BLE001 + ok_btc = False + print(f" guardrail load_data('BTC-30SEP22') -> {guard} | " + f"collisioni nel namespace nudo: {'SI' if collide else 'NO'} | " + f"load_data('BTC','1h') ancora ok: {'SI' if ok_btc else 'NO'}") + print(" ⚠️ DICHIARATO: il cron NON rinfresca questi file (cron_daily fa BTC/ETH, DVOL, HL,") + print(" equity, catena — non i trimestrali). Sono un'ISTANTANEA al 2026-08-22, e il") + print(" contratto in corso (BTC/ETH-25SEP26) e' TRONCATO a oggi, non a scadenza.") + print(" ⚠️ DICHIARATO: `data/raw/` e' gitignored e `do_pythagoras` in backup.sh salva solo") + print(" il dato NON ricostruibile — questo e' ricostruibile (~2.700 chiamate pubbliche),") + print(" quindi resta fuori dal backup di proposito. Se sparisce, si rifa'.") + return dict(files=len(files), mb=tot_mb, rows=rows, guard_ok=("INTATTO" in guard and not collide)) + + +# ========================================================================== +# 1. CARICAMENTO +# ========================================================================== +def load_futures(asset: str): + closes, exps = {}, {} + for nm, e in quarterly_names(asset): + f = FUT_DIR / f"{nm}.parquet" + if not f.exists(): + continue + d = pd.read_parquet(f, columns=["ts", "close"]) + if len(d) < 200: + continue + s = d.set_index("ts")["close"].astype(float) + s = s[s > 0] + if len(s) < 200: + continue + closes[nm], exps[nm] = s, e + wide = pd.DataFrame(closes).sort_index() + return wide, exps + + +def load_fund(asset: str) -> pd.DataFrame: + return pd.read_parquet(FUT_DIR / f"fund_{asset}.parquet").set_index("ts").sort_index() + + +def load_perp(asset: str) -> pd.Series: + d = A.get(asset, "1h") + return d.set_index("datetime")["close"].astype(float).sort_index() + + +# ========================================================================== +# 2. METRICHE +# ========================================================================== +def to_daily(h: pd.Series) -> pd.Series: + return ((1.0 + h.fillna(0.0)).resample("1D").prod() - 1.0).dropna() + + +def stats(daily: pd.Series) -> dict: + r = daily.dropna() + if len(r) < 30 or r.std() == 0: + return dict(sharpe=0.0, hold=float("nan"), maxdd=0.0, cagr=float("nan"), + n=len(r), vol=0.0, dd_days=0, worst_month=float("nan")) + sh = float(r.mean() / r.std() * SQ365) + eq = (1 + r).cumprod() + peak = eq.cummax() + ddser = (peak - eq) / peak + dd = float(ddser.max()) + under = (eq < peak) + # durata del piu' lungo periodo SOTT'ACQUA (in giorni di calendario) + dur, best, start = 0, 0, None + for t, u in under.items(): + if u and start is None: + start = t + elif not u and start is not None: + best = max(best, (t - start).days) + start = None + if start is not None: + best = max(best, (under.index[-1] - start).days) + dur = best + yrs = max((r.index[-1] - r.index[0]).days / 365.25, 1e-9) + cagr = float(eq.iloc[-1] ** (1 / yrs) - 1) if eq.iloc[-1] > 0 else float("nan") + ho = r[r.index >= HOLDOUT] + sho = float(ho.mean() / ho.std() * SQ365) if len(ho) > 30 and ho.std() > 0 else float("nan") + wm = float((((1 + r).resample("ME").prod() - 1)).min()) + return dict(sharpe=sh, hold=sho, maxdd=dd, cagr=cagr, n=len(r), + vol=float(r.std() * SQ365), dd_days=dur, worst_month=wm) + + +def show(label: str, daily: pd.Series, extra: str = "") -> dict: + m = stats(daily) + print(f" {label:<38s} Sh {m['sharpe']:+6.2f} hold {m['hold']:+6.2f} " + f"CAGR {m['cagr']*100:+6.2f}% maxDD {m['maxdd']*100:5.2f}% " + f"peggior mese {m['worst_month']*100:+6.2f}% sott'acqua {m['dd_days']:4d}g " + f"n={m['n']:,}{extra}") + return m + + +def per_year(daily: pd.Series) -> pd.Series: + return (1 + daily).groupby(daily.index.year).prod() - 1 + + +# ========================================================================== +# 3. Q1 — IL 2022, cioe' l'anno che CC01 non aveva +# ========================================================================== +def q1_descrittiva(F: dict, W: dict, E: dict) -> pd.DataFrame: + hr("[1] Q1 — IL 2022: il carry si comprime, si annulla, o si INVERTE?") + tab = {} + for a in ASSETS: + fu = F[a] + f1 = fu["f1h"].dropna() + d = f1.resample("1D").sum() + wide, exps = W[a], E[a] + idx = wide.index + # basis del front (trimestrale piu' vicino con dte >= 3g) vs INDICE + order = sorted(exps, key=lambda c: exps[c]) + TAU = np.column_stack([(exps[c] - idx).total_seconds().values / 3600.0 for c in order]) + AV = np.column_stack([wide[c].notna().values for c in order]) + AL = AV & (TAU >= 3 * 24) + ok = AL.any(axis=1) + k1 = np.argmax(AL, axis=1) + Fv = np.where(ok, wide[order].values.astype(float)[np.arange(len(idx)), k1], np.nan) + tau = np.where(ok, TAU[np.arange(len(idx)), k1], np.nan) + ix = fu["index"].reindex(idx).ffill(limit=2).values + basis = pd.Series(np.log(Fv / ix) * HOURS_Y / tau, index=idx).replace([np.inf, -np.inf], np.nan) + for y in range(2019, 2027): + dy = d[d.index.year == y] + by = basis[basis.index.year == y].dropna() + if len(dy) < 20 or len(by) < 200: + continue + tab[(a, y)] = dict( + funding_anno=float(dy.sum()), + giorni_f_neg=float((dy < 0).mean()), + basis_med=float(by.median()), + backwardation=float((by < 0).mean()), + ) + T = pd.DataFrame(tab).T + print("\n funding REALIZZATO del perpetual Deribit (somma dell'anno) e BASIS del trimestrale front") + print(" (il carry classico INCASSA il funding: un anno negativo e' un anno in cui PAGA)\n") + print(f" {'':6s} {'anno':>5s} {'funding anno':>13s} {'gg f<0':>8s} " + f"{'basis mediano':>14s} {'ore in backwardation':>21s}") + for (a, y), r in T.iterrows(): + flag = " <<< il 2022" if y == 2022 else "" + print(f" {a:6s} {y:5d} {r.funding_anno*100:+12.2f}% {r.giorni_f_neg*100:7.1f}% " + f"{r.basis_med*100:+13.2f}% {r.backwardation*100:20.1f}%{flag}") + + # episodio peggiore del carry PURO (r = funding), per asset + print("\n peggior EPISODIO del carry puro (r = funding, senza costi): " + "run consecutivo piu' negativo") + for a in ASSETS: + d = F[a]["f1h"].dropna().resample("1D").sum() + cum = d.cumsum() + run_peak = cum.cummax() + under = cum - run_peak + i = under.idxmin() + j = run_peak[:i].idxmax() + print(f" {a}: {float(under.min())*100:+.2f}% di funding cumulato perso fra " + f"{j.date()} e {i.date()} ({(i-j).days} giorni)") + print("\n ⚠️ E il 2022 non e' nemmeno l'unico buco: lo storico di CC01 (dal 2023-05)") + print(" non conteneva neppure MARZO 2020 — che sotto risulta il mese peggiore di tutti.") + return T + + +# ========================================================================== +# 4. Q2 — LA REPLICA DI CC01 CON IL 2022 DENTRO +# ========================================================================== +def cc01_daily(FUNDD: pd.DataFrame, mode: str = "static", trail: int = 14, + cost: float = ANNUAL_COST_CC01, SIG: pd.DataFrame | None = None) -> pd.Series: + """IL MODELLO DI CC01, IDENTICO: r = funding (long spot / short perp), meno un drag + annuo fisso. `FUNDD` = [giorno x asset] di RITORNO giornaliero della gamba. + `gated` = harvest solo se il funding trailing CAUSALE e' positivo. + + ⚠️ `SIG` esiste perche' in CC01 il gate guarda il FUNDING anche quando il ritorno + include il mark-to-market della base (`leg_raw = FUND - PREM.diff()` ma + `sig = FUND.rolling(...)`). Gatare sul ritorno con la base dentro significa gatare + sul RUMORE del premio: prima stesura, e produceva -35%/anno di CAGR.""" + SIG = FUNDD if SIG is None else SIG.reindex(FUNDD.index) + daily_cost = cost / 365.25 + if mode == "gated": + sig = SIG.rolling(trail, min_periods=trail // 2).mean().shift(1) + act = (sig > 0).astype(float) + n = act.sum(axis=1).replace(0, np.nan) + gross = ((FUNDD * act).sum(axis=1) / n).fillna(0.0) + drag = (act.sum(axis=1) > 0).astype(float) * daily_cost + else: + gross = FUNDD.mean(axis=1) + drag = daily_cost + return (gross - drag).dropna() + + +def q2_replica(F: dict, PP_GLOBAL: dict) -> dict: + hr("[2] Q2 — LA REPLICA DI CC01: prima riprodurre il numero VECCHIO, poi produrre il nuovo") + out = {} + + # (a) il numero vecchio, con la macchina vecchia e i dati vecchi (Hyperliquid) + print("\n (a) CONTROLLO DI REPLICA — cash_carry_hl.py tale e quale (funding Hyperliquid 2023-05+)") + try: + import importlib.util + spec = importlib.util.spec_from_file_location( + "cc_old", ROOT / "scripts" / "research" / "cash_carry_hl.py") + cc_old = importlib.util.module_from_spec(spec) + spec.loader.exec_module(cc_old) # type: ignore + for mode in ("static", "gated"): + r = cc_old.cc_returns(["BTC", "ETH"], mode=mode) + out[f"HL_{mode}"] = r + m = cc_old.metrics(r) + print(f" HL {mode:7s}: Sharpe {m['sharpe']:+6.2f} CAGR {m['cagr']*100:+.1f}% " + f"maxDD {m['dd']*100:.2f}% ({r.index[0].date()} .. {r.index[-1].date()})") + print(" -> il '11-13' del 26/06 e' RIPRODOTTO. Ogni delta sotto e' un delta vero.") + except Exception as exc: # noqa: BLE001 + print(f" ⚠️ replica non eseguibile ({type(exc).__name__}: {exc}) — i dati HL sono su disco?") + + # (b) STESSO MODELLO, funding DERIBIT: prima sulla finestra di CC01 (controllo di venue), + # poi sulla finestra piena che contiene il 2022. + FUNDD = pd.concat({a: F[a]["f1h"].dropna().resample("1D").sum() for a in ASSETS}, + axis=1).sort_index().dropna() + print(f"\n (b) STESSO MODELLO su funding DERIBIT ({FUNDD.index[0].date()} .. " + f"{FUNDD.index[-1].date()}, {len(FUNDD):,} giorni)") + print(" NB: e' un CONTROLLO DI VENUE, non una replica — HL e Deribit sono due mercati.\n") + for mode in ("static", "gated"): + full = cc01_daily(FUNDD, mode) + win = full[full.index >= W_CC01] + out[f"DBT_{mode}_full"] = full + out[f"DBT_{mode}_win"] = win + show(f"DERIBIT {mode} — finestra CC01", win) + show(f"DERIBIT {mode} — FINESTRA PIENA", full, extra=" <<< col 2022 dentro") + print() + + print(" per anno (rendimento del carry puro, netto del drag 2%/anno di CC01):") + py = per_year(out["DBT_static_full"]) + print(" " + " ".join(f"{y}:{v*100:+.1f}%" for y, v in py.items())) + pyg = per_year(out["DBT_gated_full"]) + print(" gated: " + " ".join(f"{y}:{v*100:+.1f}%" for y, v in pyg.items())) + g22 = float(pyg.get(2022, float("nan"))) + s22 = float(py.get(2022, float("nan"))) + print(f"\n ⚠️ IPOTESI MIA, SMENTITA DALLA MISURA: avevo scritto che il gate causale di CC01") + print(f" (funding trailing 14g > 0) 'entra ed esce col ritardo, quindi il 2022 lo") + print(f" attraversa comunque'. Falso: 2022 static {s22*100:+.1f}% contro gated " + f"{g22*100:+.1f}% = il gate") + print(f" taglia il {100*(1-abs(g22)/max(abs(s22),1e-9)):.0f}% della perdita dell'anno. " + f"Il funding negativo e' PERSISTENTE (53-59% dei") + print(f" giorni del 2022), quindi un trailing lento basta a stare fuori.") + print(f" -> La conseguenza NON e' che CC01 e' salvo: e' che **il buco del 2022 non era") + print(f" il buco principale**. Il gated con il 2022 dentro fa ancora Sharpe " + f"{stats(out['DBT_gated_full'])['sharpe']:+.2f} con") + print(f" maxDD {stats(out['DBT_gated_full'])['maxdd']*100:.2f}% — cioe' e' ANCORA una serie senza coda sinistra.") + + # (c) la variante che CC01 chiamava 'onesta': r = funding - Delta(premio perp/indice). + # E' il modello di un long-spot/short-perp VERO su un venue che abbia spot. + prem = {a: (PP_GLOBAL[a].reindex(F[a].index) / F[a]["index"] - 1.0) for a in ASSETS} + out["_FUNDD"] = FUNDD + print("\n (c) VARIANTE CON MARK-TO-MARKET DELLA BASE (r = funding - Delta premio perp/indice)") + print(" e' il modello di un long-spot/short-perp VERO (su un venue che abbia spot).") + print(" ⚠️ DUE LENTI, perche' il 'premio' non e' un oggetto unico: quello GREZZO") + print(" (chiusura del perp / indice, ora per ora) contiene le dislocazioni di") + print(" microstruttura; quello LISCIO (mediana 24h) e' l'analogo del `premium` che") + print(" Hyperliquid pubblica e che CC01 usava — ed e' PIU' MITE per costruzione.") + for lente, sm in (("grezzo", 1), ("liscio 24h", 24)): + DPR = pd.concat({a: (prem[a].rolling(sm, min_periods=1).median() if sm > 1 else prem[a]) + .resample("1D").last().diff() for a in ASSETS}, axis=1).sort_index() + FB = (FUNDD - DPR.reindex(FUNDD.index)).dropna() + for mode in ("static", "gated"): + fb = cc01_daily(FB, mode, SIG=FUNDD) # il GATE guarda il funding, come CC01 + key = "" if sm == 1 else "_sm" + out[f"DBT_{mode}_basis{key}_full"] = fb + show(f"{mode} + base ({lente}) — PIENA", fb) + out[f"DBT_{mode}_basis{key}_win"] = fb[fb.index >= W_CC01] + show(f"{mode} + base ({lente}) — fin. CC01", out[f"DBT_{mode}_basis{key}_win"]) + print() + # DIAGNOSTICA: la liscezza si MISURA, non si asserisce. + try: + from src.portfolio.sleeves import _HL_DIR # noqa: PLC0415 + rows = [] + for a in ASSETS: + fp = _HL_DIR / f"hlfund_{a.lower()}_1h.parquet" + if not fp.exists(): + continue + hl = pd.read_parquet(fp)["premium"].resample("1D").last().diff().dropna() + raw = prem[a].resample("1D").last().diff().dropna() + sm = prem[a].rolling(24, min_periods=1).median().resample("1D").last().diff().dropna() + rows.append((a, hl.std(), sm.std(), raw.std())) + if rows: + print("\n sd del Delta-premio GIORNALIERO (e' la vol che il carry si prende addosso):") + for a, h, sm_, r_ in rows: + print(f" {a}: Hyperliquid `premium` {h*1e4:6.1f} bps | Deribit liscio 24h " + f"{sm_*1e4:6.1f} bps | Deribit grezzo {r_*1e4:6.1f} bps " + f"-> grezzo/HL = {r_/max(h,1e-12):.1f}x") + except Exception as exc: # noqa: BLE001 + print(f" (diagnostica sulla liscezza NON girata: {type(exc).__name__})") + print(" ⚠️ Il confronto che conta NON e' con l'11-13 di CC01 ma con il suo 'onesto'") + print(" 10,87: CC01 scriveva che il mark-to-market della base 'sgonfia lo Sharpe solo") + print(" 13->11, quindi il basis-from-data NON e' il rischio vero'. Su Deribit lo stesso") + print(" esperimento, SULLA STESSA FINESTRA, lo sgonfia a 1,65 (lente liscia) / 0,37") + print(" (lente grezza). La differenza non e' il periodo: e' la MISURA del premio, e") + print(" **la liscezza era una proprieta' della colonna, non del rischio.**") + return out + + +# ========================================================================== +# 5. Q3/Q4 — IL CARRY DERIBIT A DUE GAMBE (perp + trimestrale), roll e margine +# ========================================================================== +def deribit_carry(asset: str, wide: pd.DataFrame, exps: dict, fu: pd.DataFrame, + perp: pd.Series, roll_h: int = 72, sign: int = +1, + slip_bps: float = SLIP_BPS, fee_side: float = FEE_SIDE, + inverse: bool = True, gated: bool = False, trail_h: int = 24 * 14): + """Carry delta-neutral ESEGUIBILE su Deribit, a DUE gambe sullo STESSO venue. + + sign = +1 LONG trimestrale / SHORT perp -> incassa funding, paga il basis + (e' la DIREZIONE DI CC01: il datato fa da proxy dello spot, che a Deribit + storicamente non abbiamo; e la gamba corta sul perp incassa il funding) + sign = -1 SHORT trimestrale / LONG perp -> incassa il basis, paga il funding + + Il roll: il datato attivo e' il trimestrale piu' vicino con dte >= roll_h ore. Quando + cambia si paga il round-trip COMPLETO sulla sola gamba datata (2 ordini). **La gamba + perp NON si rolla**: e' un perpetuo. (Il filone BASIS-CALENDAR contava il turnover x2 + su entrambe le gambe: qui il costo del roll e' modellato per quello che e'.) + + Convenzione INVERSE (quella del venue: settlement BTC/ETH, quote USD): + P&L_BTC(long N USD) = N (1/entrata - 1/uscita) + funding inverse: dimensione in BTC = N/indice; i long PAGANO quando f>0 + Il ritorno orario e' espresso per $1 di nozionale PER GAMBA. + """ + idx = wide.index + n = len(idx) + order = sorted(exps, key=lambda c: exps[c]) + TAU = np.column_stack([(exps[c] - idx).total_seconds().values / 3600.0 for c in order]) + AV = np.column_stack([wide[c].notna().values for c in order]) + AL = AV & (TAU >= roll_h) + ok = AL.any(axis=1) + k1 = np.argmax(AL, axis=1) + W = wide[order].values.astype(float) + rows = np.arange(n) + front = np.where(ok, np.array(order, dtype=object)[k1], "") + Fv = np.where(ok, W[rows, k1], np.nan) + tau = np.where(ok, TAU[rows, k1], np.nan) + + fu2 = fu.reindex(idx).ffill(limit=2) + IX = fu2["index"].values.astype(float) + f1 = np.nan_to_num(fu2["f1h"].values.astype(float)) + P = perp.reindex(idx).values.astype(float) + + live = np.isfinite(Fv) & np.isfinite(P) & np.isfinite(IX) & (P > 0) & (IX > 0) + same = np.zeros(n, bool) + same[1:] = live[1:] & live[:-1] & (front[1:] == front[:-1]) & (front[1:] != "") + + # ------- segnale (causale): opzionale gate sul carry netto trailing ------- + basis_p = np.log(np.where(live, Fv, np.nan) / np.where(live, P, np.nan)) * HOURS_Y / tau + net_carry = f1 * HOURS_Y - basis_p # carry atteso del sign=+1, annualizzato + if gated: + s = pd.Series(net_carry, index=idx).rolling(trail_h, min_periods=trail_h // 3).mean().shift(1) + w = ((s * sign) > 0).astype(float).values # esci quando il carry trailing e' contrario + else: + w = np.ones(n) + w = np.nan_to_num(w) + + # ------- P&L orario per $1 di nozionale per gamba ------- + r = np.zeros(n) + if inverse: + # long datato: N(1/F_{t-1} - 1/F_t) | short perp: N(1/P_t - 1/P_{t-1}) (in BTC) + with np.errstate(all="ignore"): + dF = np.zeros(n); dP = np.zeros(n) + dF[1:] = 1.0 / Fv[:-1] - 1.0 / Fv[1:] + dP[1:] = 1.0 / P[1:] - 1.0 / P[:-1] + btc = sign * (dF + dP) + btc = btc + sign * (f1 / IX) # short perp INCASSA il funding se f>0 + r = np.where(same, IX * btc, 0.0) # da BTC a USD all'indice + else: + with np.errstate(all="ignore"): + rF = np.zeros(n); rP = np.zeros(n) + rF[1:] = Fv[1:] / Fv[:-1] - 1.0 + rP[1:] = P[1:] / P[:-1] - 1.0 + r = np.where(same, sign * (rF - rP + f1), 0.0) + r = np.nan_to_num(r) + held = np.zeros(n) + held[1:] = w[:-1] # deciso a t, tenuto t->t+1 (causale) + gross = r * held + + # ------- costi: apertura, ROLL (solo gamba datata), chiusura, e on/off del gate ------- + tc = fee_side + slip_bps * 1e-4 + cost = np.zeros(n) + rolled = np.zeros(n, bool) + rolled[1:] = live[1:] & live[:-1] & (front[1:] != front[:-1]) & (front[:-1] != "") & (front[1:] != "") + cost += rolled * (2.0 * tc) * np.maximum(held, np.roll(held, 1)) # chiudi vecchio + apri nuovo + dw = np.zeros(n) + dw[1:] = np.abs(held[1:] - held[:-1]) + cost += dw * (2.0 * tc) # entrata/uscita: 2 gambe + net = pd.Series(gross - cost, index=idx) + + # ------- tabella per TRADE (un trade = un contratto tenuto) ------- + fr = pd.Series(front, index=idx) + trades = [] + for c, g in net.groupby(fr): + if c == "" or len(g) < 24: + continue + cc = g.cumsum() + trades.append(dict(contract=c, start=g.index[0], end=g.index[-1], ore=len(g), + pnl=float(cc.iloc[-1]), mae=float(cc.min()), mfe=float(cc.max()))) + T = pd.DataFrame(trades).sort_values("start").reset_index(drop=True) if trades else pd.DataFrame() + return net, T, pd.Series(basis_p, index=idx), pd.Series(net_carry, index=idx) + + +def q3_carry(W: dict, E: dict, F: dict, PP: dict) -> dict: + hr("[3] Q3 — IL CARRY DERIBIT A DUE GAMBE (perpetual + trimestrale), roll pagato") + print(" Su Deribit non esiste storico SPOT: la coppia delta-neutral a due gambe sullo stesso") + print(" venue e' PERP vs TRIMESTRALE. Il trimestrale non paga funding -> fa da proxy dello") + print(" spot, e la direzione di CC01 diventa: LONG trimestrale / SHORT perp.\n") + res = {} + for sign, nome in ((+1, "LONG datato / SHORT perp (= direzione CC01)"), + (-1, "SHORT datato / LONG perp (= raccolta del contango)")): + legs = {} + for a in ASSETS: + net, T, basis, nc = deribit_carry(a, W[a], E[a], F[a], PP[a], sign=sign) + legs[a] = (net, T) + both = pd.concat([legs[a][0] for a in ASSETS], axis=1, join="inner").mean(axis=1) + d = to_daily(both) + res[sign] = dict(hourly=both, daily=d, legs=legs) + print(f" --- {nome} ---") + show(" 50/50 BTC+ETH, finestra piena", d) + show(" 50/50 BTC+ETH, finestra CC01", d[d.index >= W_CC01]) + py = per_year(d) + print(" per anno: " + " ".join(f"{y}:{v*100:+.1f}%" for y, v in py.items())) + print() + + # convenzione inverse vs lineare — misurata, non assunta + net_i, _, _, _ = deribit_carry("BTC", W["BTC"], E["BTC"], F["BTC"], PP["BTC"], sign=+1, inverse=True) + net_l, _, _, _ = deribit_carry("BTC", W["BTC"], E["BTC"], F["BTC"], PP["BTC"], sign=+1, inverse=False) + di, dl = to_daily(net_i), to_daily(net_l) + print(f" convenzione del venue: INVERSE (BTC-settled) Sharpe {stats(di)['sharpe']:+.2f} " + f"contro l'approssimazione LINEARE {stats(dl)['sharpe']:+.2f} " + f"(CAGR {stats(di)['cagr']*100:+.2f}% vs {stats(dl)['cagr']*100:+.2f}%, BTC sign=+1)") + dy = (per_year(di) - per_year(dl)) * 100 + print(" differenza per anno (inverse - lineare, punti percentuali): " + + " ".join(f"{y}:{v:+.1f}" for y, v in dy.items())) + print(" ⚠️ NON e' un dettaglio contabile: su una coppia INVERSE con nozionali pareggiati") + print(" in USD resta un delta residuo ~ -basis (dal termine 1/F - 1/P), e vale piu'") + print(" dell'intero premio che il carry vorrebbe incassare. Chi modella un carry su") + print(" inverse in convenzione lineare si regala un ritorno che il venue non paga.") + return res + + +def q4_margine(res: dict, F: dict, PP: dict) -> dict: + hr("[4] Q4 — IL ROLL E IL MARGINE: cio' che un P&L a scadenza NON PUO' VEDERE") + print(" Un carry delta-neutral SEMBRA liscio perche' il modello guarda il cashflow e il") + print(" payoff a scadenza. Ma la perdita di mark-to-market e' RICHIESTA SUBITO in margine") + print(" mentre il guadagno arriva alla convergenza: se l'equity scende sotto il mantenimento,") + print(" la posizione viene chiusa nel punto peggiore e il guadagno a scadenza non arriva mai.\n") + print(f" Margine dichiarato (fonte secondaria, documentazione Deribit futures): " + f"IM {IM_LEG*100:.2f}%/gamba, MM {MM_LEG*100:.3f}%/gamba -> MM totale " + f"{2*MM_LEG*100:.2f}% del nozionale per gamba.") + print(" Sotto leva per-gamba lambda (nozionale per gamba = lambda x equity):") + print(" liquidazione quando 1 + lambda*cumPnL < 2*MM*lambda") + print(" -> lambda_max = 1 / (2*MM + |MAE|) con MAE = escursione avversa del trade\n") + + out = {} + for sign in (+1, -1): + nome = "LONG datato/SHORT perp" if sign == +1 else "SHORT datato/LONG perp" + rows = [] + for a in ASSETS: + T = res[sign]["legs"][a][1].copy() + T["asset"] = a + rows.append(T) + T = pd.concat(rows).sort_values("start").reset_index(drop=True) + T["anno"] = T.start.dt.year + full_mae = float(T.mae.min()) + win = T[T.start >= W_CC01] + win_mae = float(win.mae.min()) if len(win) else float("nan") + lam_full = 1.0 / (2 * MM_LEG + abs(full_mae)) + lam_win = 1.0 / (2 * MM_LEG + abs(win_mae)) if np.isfinite(win_mae) else float("nan") + out[sign] = dict(T=T, lam_full=lam_full, lam_win=lam_win, + mae_full=full_mae, mae_win=win_mae) + print(f" --- {nome} ---") + print(f" trade (= contratti tenuti): {len(T)} in perdita: " + f"{int((T.pnl<0).sum())} ({(T.pnl<0).mean()*100:.0f}%)") + print(f" peggiore escursione avversa (MAE) sulla FINESTRA PIENA : " + f"{full_mae*100:+6.2f}% -> leva massima sostenibile {lam_full:5.1f}x per gamba") + print(f" peggiore escursione avversa (MAE) nella FINESTRA CC01 : " + f"{win_mae*100:+6.2f}% -> leva massima sostenibile {lam_win:5.1f}x per gamba") + if np.isfinite(lam_win) and lam_full > 0: + print(f" ⚠️ chi avesse dimensionato il carry sulla finestra di CC01 avrebbe usato " + f"fino a {lam_win:.1f}x: e' {lam_win/lam_full:.1f} VOLTE la leva che la storia piena sopporta.") + worst = T.nsmallest(5, "mae")[["asset", "contract", "start", "ore", "pnl", "mae"]] + print(" i 5 peggiori trade per escursione avversa:") + for r in worst.itertuples(): + print(f" {r.asset} {r.contract:<12s} {str(r.start.date()):>10s} " + f"{r.ore:5d} ore P&L finale {r.pnl*100:+6.2f}% MAE {r.mae*100:+6.2f}%") + print() + + # ---- e il carry PURO (long spot / short perp): il suo blow-out e' il PREMIO DEL PERP ---- + print(" Il carry PURO di CC01 (long spot / short perp) NON HA SCADENZA: non converge mai,") + print(" quindi il suo mark-to-market e' il PREMIO del perpetual sull'indice, e la sua perdita") + print(" massima e' il premio stesso. Misurato sul feed certificato Deribit:") + for a in ASSETS: + fu = F[a] + pp = PP[a].reindex(fu.index) + prem = (pp / fu["index"] - 1.0).dropna() + f1 = fu["f1h"].reindex(prem.index).fillna(0.0) + pnl = f1.cumsum() - (prem - prem.iloc[0]) # funding incassato - MTM della base + # MAE a 90 GIORNI: la finestra di detenzione confrontabile con un trimestrale. + # (Il drawdown da picco NON e' la misura giusta qui: il picco e' un crash-spike di + # un'ora in cui il perp scambia -20% sotto l'indice, cioe' un GUADAGNO per chi e' + # corto il perp. Un rischio si misura dall'INGRESSO, non da una fortuna transitoria.) + w = 90 * 24 + fwd_min = pnl[::-1].rolling(w, min_periods=1).min()[::-1] + exc = fwd_min - pnl + # ⚠️ INGRESSI AMMISSIBILI: un carry si apre quando il perp e' a PREMIO (e' l'intera + # ragione del trade). Includere le ore in cui il perp scambia -20% SOTTO l'indice + # (marzo 2020) misura l'escursione di un ingresso che nessuno farebbe. Si riporta + # il numero ristretto come stima, e quello grezzo come limite superiore. + exc_ok = exc.where(prem >= 0) + mae90 = float(exc_ok.min()) + i90 = exc_ok.idxmin() + mae90_raw = float(exc.min()) + lam90 = 1.0 / (2 * MM_LEG + abs(mae90)) + pr_y = prem.groupby(prem.index.year).max() + print(f" {a}: premio perp/indice mediana {prem.median()*1e4:+.1f} bps | " + f"MASSIMO {prem.max()*1e4:+.0f} bps ({prem.idxmax().date()}) | " + f"minimo {prem.min()*1e4:+.0f} bps ({prem.idxmin().date()})") + print(f" MAE a 90 giorni del carry puro CON mark-to-market della base, su ingressi") + print(f" ammissibili (premio >= 0): {mae90*100:+.2f}% (ingresso {i90.date()}) " + f"-> leva massima {lam90:.1f}x per gamba") + print(f" (senza il filtro d'ingresso: {mae90_raw*100:+.2f}% — include l'apertura di") + print(f" uno short perp a sconto del 20%, che nessuno farebbe)") + print(f" premio MASSIMO per anno (bps, e' il lato AVVERSO del carry): " + + ", ".join(f"{y}:{v*1e4:+.0f}" for y, v in pr_y.items())) + print(" ⚠️ Il modello r=funding di CC01 ha maxDD 0,3-0,9%: la stessa posizione, con il") + print(" mark-to-market della base che il margine guarda davvero, ha una escursione") + print(" avversa di un ordine di grandezza piu' grande. Non e' un dettaglio: e' la") + print(" differenza fra 'leva 80x' e 'leva 6x'.") + + return out + + +# ========================================================================== +# 6. GATE +# ========================================================================== +def q5_gate(cc: dict, res: dict, mg: dict, n_trials: int, family_sr: list) -> None: + hr("[5] GATE — a partire da quello che CC01 stesso ha fatto nascere") + + print("\n (a) implausible_sharpe — il gate del 26/07, scritto PROPRIO su CC01") + cand = [ + ("CC01 originale (HL, 2023-05+)", cc.get("HL_static"), None, None), + ("CC01 gated originale (HL)", cc.get("HL_gated"), None, None), + ("CC01 su Deribit, finestra CC01", cc.get("DBT_static_win"), None, None), + ("CC01 su Deribit, FINESTRA PIENA", cc.get("DBT_static_full"), None, None), + ("CC01 gated Deribit, FINESTRA PIENA", cc.get("DBT_gated_full"), None, None), + ("CC01+base grezzo, FINESTRA PIENA", cc.get("DBT_static_basis_full"), None, None), + ("CC01+base grezzo gated, PIENA", cc.get("DBT_gated_basis_full"), None, None), + ("CC01+base liscio 24h, FINESTRA PIENA", cc.get("DBT_static_basis_sm_full"), None, None), + ("CC01+base liscio 24h gated, PIENA", cc.get("DBT_gated_basis_sm_full"), None, None), + ] + for sign in (+1, -1): + T = mg[sign]["T"] + nm = "carry 2 gambe Deribit sign=+1" if sign == +1 else "carry 2 gambe Deribit sign=-1" + cand.append((nm, res[sign]["daily"], int(len(T)), int((T.pnl < 0).sum()))) + for nm, ser, nt, nl in cand: + if ser is None or len(ser) < 30: + print(f" {nm:<38s} NON GIRATO (serie assente)") + continue + g = A.implausible_sharpe(ser, n_trades=nt, n_losing_trades=nl) + tag = "🚨 SEGNALA" if g["implausible"] else " pulito " + print(f" {nm:<38s} {tag} Sh {g.get('sharpe', float('nan')):+6.2f} " + f"maxDD {g.get('maxdd', float('nan'))*100:5.2f}% " + f"perdite/attive {g.get('loss_frac', float('nan'))*100:5.1f}% " + f"Calmar {g.get('calmar', float('nan')):7.1f}") + for rr in g["reasons"]: + print(f" - {rr}") + + print("\n (b) marginal_vs_tp01 — sul carry ESEGUIBILE (2 gambe Deribit, finestra piena)") + for sign in (+1, -1): + d = res[sign]["daily"] + if d.std() == 0: + print(f" sign={sign:+d}: serie piatta, skip") + continue + m = A.marginal_vs_tp01(d) + b = m.get("blends", {}).get("w25", {}) + print(f" sign={sign:+d} verdetto={m.get('marginal_verdict')} " + f"corr->TP01 {m.get('corr_full')} is_hedge={m.get('is_hedge')} " + f"edge_in_sample={m.get('has_insample_edge')} (Sh IS {m.get('cand_insample_sharpe')})") + print(f" robust_oos={m.get('robust_oos')} multicut={m.get('multicut_persistent')} " + f"blend25 full {b.get('full')} (upl {b.get('uplift_full')}) hold {b.get('hold')} " + f"(upl {b.get('uplift_hold')})") + + print("\n (c) deflated_sharpe — sui trial DICHIARATI, contati al rialzo") + print(" ⚠️ la famiglia e' quella del CANDIDATO (le celle del carry a 2 gambe: 2 segni x") + print(" 8 ancore d'ora x 7 anticipi di roll). Mescolarci gli Sharpe del carry PURO") + print(" (3-7) gonfierebbe la varianza dei trial e quindi la soglia del null: sarebbe") + print(" un null di un'altra strategia.") + allsr = list(family_sr) + [stats(res[s_]["daily"])["sharpe"] for s_ in (+1, -1)] + best_sign = max((+1, -1), key=lambda s: stats(res[s]["daily"])["sharpe"]) + d = res[best_sign]["daily"] + sr = stats(d)["sharpe"] + # la lista degli Sharpe di famiglia serve per la varianza dei trial: si usa la griglia + # d'ancora del roll, gia' calcolata sotto, piu' i due segni. + try: + dsr, sr0 = A.deflated_sharpe(sr, allsr, d) + txt = (f"DSR {dsr:.3f} ({'PASS' if dsr >= 0.95 else 'FAIL'}), " + f"massimo atteso dal solo rumore {sr0:+.2f}") + except Exception as exc: # noqa: BLE001 + txt = f"NON GIRATO ({type(exc).__name__})" + print(f" candidato migliore = sign {best_sign:+d}, Sharpe {sr:+.2f}, " + f"trial dichiarati {n_trials} -> {txt}") + print(f" (il massimo atteso dal rumore su questa famiglia e' PIU' ALTO del candidato:") + print(f" non c'e' niente da deflazionare, il candidato e' sotto il proprio null.)") + + print("\n (c-bis) CAUSALITA' — dichiarata, e con un controllo POSITIVO") + print(" Il carry STATICO non ha segnale: non c'e' niente da anticipare, e la scelta del") + print(" contratto front dipende solo dal calendario di scadenza, noto anni prima.") + print(" L'unico segnale del filone e' il gate di CC01 (media mobile del funding). Li' la") + print(" causalita' e' uno `.shift(1)`, e un controllo positivo deve mostrare che TOGLIERLO") + print(" migliora — altrimenti il test non ha potenza:") + try: + FU2 = pd.concat({a: cc["_FUNDD"][a] for a in ASSETS}, axis=1) if "_FUNDD" in cc else None + except Exception: # noqa: BLE001 + FU2 = None + if FU2 is not None: + leak = FU2.rolling(14, min_periods=7).mean() # SENZA shift = leak + act = (leak > 0).astype(float) + n_ = act.sum(axis=1).replace(0, np.nan) + g_leak = ((FU2 * act).sum(axis=1) / n_).fillna(0.0) - \ + (act.sum(axis=1) > 0).astype(float) * ANNUAL_COST_CC01 / 365.25 + print(f" gate CAUSALE (shift 1): Sharpe {stats(cc['DBT_gated_full'])['sharpe']:+.2f} | " + f"gate CON LEAK (no shift): Sharpe {stats(g_leak.dropna())['sharpe']:+.2f} " + f"-> il test HA potenza e la versione pubblicata e' quella causale") + else: + print(" NON GIRATO (pannello di funding non disponibile)") + + print("\n (d) null del DE-LEVERING — il carry rivendica un DD minuscolo: e' solo meno leva?") + try: + base = A.tp01_baseline_daily() + for nm, ser in (("CC01 Deribit finestra piena", cc.get("DBT_static_full")), + ("carry 2 gambe sign=+1", res[+1]["daily"])): + if ser is None: + continue + cs = stats(ser) + hit = None + for k in np.arange(1.00, 0.01, -0.02): # il k PIU' GRANDE che rispetta il DD + st = stats(base * k) + if st["maxdd"] <= cs["maxdd"] + 1e-12: + hit = (float(k), st) + break + if hit is None: + print(f" {nm}: nessun k<=1 sul baseline raggiunge quel DD -> null non applicabile") + else: + k, st = hit + verdetto = "REFUTATO (il baseline de-leverato fa meglio)" if st["sharpe"] > cs["sharpe"] \ + else "SUPERATO (il baseline de-leverato NON lo eguaglia)" + print(f" {nm}: DD {cs['maxdd']*100:.2f}% Sh {cs['sharpe']:+.2f} vs " + f"TP01 a k={k:.2f}: DD {st['maxdd']*100:.2f}% Sh {st['sharpe']:+.2f} -> {verdetto}") + print(" ⚠️ lo Sharpe e' INVARIANTE alla scala, quindi su un candidato che rivendica") + print(" 'poco DD' questo null si riduce a 'il tuo Sharpe batte quello del baseline?'.") + print(" E' degenere per costruzione (lezione 22/08) — girato lo stesso e dichiarato.") + except Exception as exc: # noqa: BLE001 + print(f" NON GIRATO ({type(exc).__name__}: {exc})") + + +def q5b_ancora(W: dict, E: dict, F: dict, PP: dict, sign: int) -> list: + print("\n (e) FORTUNA D'ANCORA del roll — l'ora in cui si rolla e' un max-of-k non dichiarato") + sh = {} + for off in range(0, 24, 3): + legs = [] + for a in ASSETS: + net, _, _, _ = deribit_carry(a, W[a], E[a], F[a], PP[a], + roll_h=72 + off, sign=sign) + legs.append(net) + d = to_daily(pd.concat(legs, axis=1, join="inner").mean(axis=1)) + sh[off] = stats(d)["sharpe"] + v = np.array(list(sh.values())) + print(f" 8 ancore di roll (72h + 0..21h): mediana {np.median(v):+.3f} " + f"banda [{v.min():+.3f}, {v.max():+.3f}] canonica(72h) {sh[0]:+.3f} " + f"-> fortuna {sh[0]-np.median(v):+.3f}") + sh2 = {} + for rd in (1, 2, 3, 5, 7, 10, 14): + legs = [] + for a in ASSETS: + net, _, _, _ = deribit_carry(a, W[a], E[a], F[a], PP[a], + roll_h=rd * 24, sign=sign) + legs.append(net) + d = to_daily(pd.concat(legs, axis=1, join="inner").mean(axis=1)) + sh2[rd] = stats(d)["sharpe"] + print(" giorni di anticipo del roll: " + + " ".join(f"{k}g:{v:+.2f}" for k, v in sh2.items())) + return list(sh.values()) + list(sh2.values()) + + +# ========================================================================== +# 7. ESEGUIBILITA' +# ========================================================================== +def q6_eseguibilita(mg: dict) -> None: + hr("[6] Q3-bis — I MURI NON STRUTTURALI, riprezzati coi numeri di oggi") + print(" CC01 fu dichiarato non eseguibile per: 'spot+perp = 4-38 gambe, funding HL non") + print(" Deribit'. La versione perp+trimestrale e' a DUE gambe sullo STESSO venue.\n") + specs = { + "BTC-PERPETUAL": dict(min_usd=10.0, tick=0.5, settle="BTC"), + "BTC trimestrale": dict(min_usd=10.0, tick=2.5, settle="BTC"), + "ETH-PERPETUAL": dict(min_usd=1.0, tick=0.05, settle="ETH"), + "ETH trimestrale": dict(min_usd=1.0, tick=0.25, settle="ETH"), + } + print(" specifiche lette dal venue (public/get_instruments, 2026-08-22):") + for k, v in specs.items(): + print(f" {k:<18s} lotto minimo ${v['min_usd']:>5.0f} tick {v['tick']:<5} " + f"settlement {v['settle']}") + print(f"\n A $635 il lotto minimo NON e' il vincolo (4a volta in questa ondata): un carry") + print(f" BTC a lambda=1 vuole $635 per gamba, 63 volte il lotto minimo.") + print(f" Margine iniziale a lambda=1: {2*IM_LEG*100:.1f}% del nozionale per gamba = " + f"${2*IM_LEG*635:.0f} su $635.") + + lam = mg[+1]["lam_full"] + print(f"\n ⚠️ MA il vincolo vero e' un ALTRO, e non e' la taglia: **tutti i futures datati") + print(f" Deribit sono INVERSE, settlement_currency BTC/ETH** (verificato strumento per") + print(f" strumento: non esiste una linea USDC datata). Il libro live e' su USDC") + print(f" lineari. E' lo stesso muro trovato il 22/08 sulle opzioni ('BTC fuori a $3.000") + print(f" misurato sulla famiglia inverse'): un conto USDC non margina un inverse senza") + print(f" cross-collateral, e il collaterale in BTC E' esso stesso esposizione") + print(f" direzionale — che e' precisamente cio' che un delta-neutral vuole evitare.") + print(f" Questo NON e' stato risolto qui: e' una verifica da UNA chiamata sul conto") + print(f" reale (haircut del cross-collateral), e va fatta prima di credere a qualsiasi") + print(f" numero di eseguibilita' di questo filone.") + + print("\n costo d'esecuzione ANNUO del carry rollato (4 roll/anno x 2 ordini, SOLO la gamba") + print(" datata: il perpetuo non si rolla; apertura e chiusura sono 2 ordini UNA volta sola):") + for slip in (0.0, 2.0, 5.0, 10.0): + c = 4 * 2 * (FEE_SIDE + slip * 1e-4) + print(f" slippage {slip:4.1f} bps/lato -> {c*100:5.3f}%/anno (8 ordini/anno)") + print(" spread REALE letto sul book vivo (cache 22/08): BTC front 0,32 bps / secondo 5,43;") + print(" ETH front 1,03 / secondo 1,02 -> slippage 2 bps/lato e' una stima ONESTA sul front.") + print(f" ⚠️ BASIS-CALENDAR citava ~1,4%/anno di costo perche' contava il turnover su") + print(f" ENTRAMBE le gambe a ogni roll: il perpetuo NON si rolla. Il costo vero e'") + print(f" **0,44%/anno a 2 bps di slippage**, ~3 volte meno. Il che rende quel filone") + print(f" MENO severo sul costo e per NIENTE meno severo sul segno: un premio di") + print(f" ~0,85%/anno con IC95 che contiene lo zero resta indistinguibile da zero anche") + print(f" pagando un terzo del costo. **Correggere un costo non crea un edge.**") + + +# ========================================================================== +# MAIN +# ========================================================================== +def main() -> None: + t0 = time.time() + hr("r0822d CC01-2022 — riaprire il cash-and-carry con la coda che gli mancava") + + persist_dataset() + + print("\n caricamento...") + W, E, F, PP = {}, {}, {}, {} + for a in ASSETS: + W[a], E[a] = load_futures(a) + F[a] = load_fund(a) + PP[a] = load_perp(a) + print(f" {a}: {W[a].shape[1]} contratti, {len(W[a]):,} ore " + f"({W[a].index.min().date()} .. {W[a].index.max().date()}) " + f"funding+indice {len(F[a]):,} ore perp certificato {len(PP[a]):,} barre") + + q1_descrittiva(F, W, E) + cc = q2_replica(F, PP) + res = q3_carry(W, E, F, PP) + mg = q4_margine(res, F, PP) + + # trial dichiarati, contati AL RIALZO (ogni variante provata, comprese quelle scartate): + # 4 carry puro r=funding : 2 modi (static/gated) x 2 finestre + # 8 carry puro + base MTM : 2 modi x 2 lenti sul premio x 2 finestre + # 32 carry a 2 gambe : 2 segni x (1 canonica + 8 ancore d'ora + 7 anticipi roll) + # 2 convenzione : inverse vs lineare + # 1 controllo positivo di leak + N_TRIALS = 4 + 8 + 32 + 2 + 1 + best = max((+1, -1), key=lambda s: stats(res[s]["daily"])["sharpe"]) + print("\n (pre-calcolo della griglia d'ancora, serve al deflated-Sharpe)") + family_sr = q5b_ancora(W, E, F, PP, best) + q5_gate(cc, res, mg, N_TRIALS, family_sr) + q6_eseguibilita(mg) + + hr("[7] SINTESI — LA SCALA, cioe' dove sono davvero andati gli 11-13 punti di Sharpe") + s_hl = stats(cc["HL_static"])["sharpe"] if "HL_static" in cc else float("nan") + s_win = stats(cc["DBT_static_win"])["sharpe"] + s_full = stats(cc["DBT_static_full"])["sharpe"] + s_bas = stats(cc["DBT_static_basis_full"])["sharpe"] + d_full = cc["DBT_static_full"] + best_d = res[best]["daily"] + righe = [ + ("CC01 come pubblicato (funding HL, r=funding, 2023-05+)", s_hl, + stats(cc["HL_static"])["maxdd"] if "HL_static" in cc else float("nan"), ""), + ("stesso modello, funding DERIBIT, STESSA finestra", s_win, + stats(cc["DBT_static_win"])["maxdd"], "controllo di venue"), + ("stesso modello, DERIBIT, finestra piena (col 2022)", s_full, + stats(d_full)["maxdd"], "<- la coda che mancava"), + ("+ base, premio LISCIO 24h (la lente di CC01/HL)", + stats(cc["DBT_static_basis_sm_full"])["sharpe"], + stats(cc["DBT_static_basis_sm_full"])["maxdd"], "<- il rischio che il margine guarda"), + ("+ base, premio GREZZO (la lente severa)", s_bas, + stats(cc["DBT_static_basis_full"])["maxdd"], " banda della stessa cosa"), + ("carry ESEGUIBILE su Deribit, 2 gambe, DIREZIONE CC01", + stats(res[+1]["daily"])["sharpe"], stats(res[+1]["daily"])["maxdd"], + "<- niente spot: serve un 2o derivato"), + (" (e scegliendo EX-POST il segno migliore dei due)", + stats(res[-1]["daily"])["sharpe"], stats(res[-1]["daily"])["maxdd"], ""), + ] + print(f" {'lente':<56s} {'Sharpe':>8s} {'maxDD':>8s}") + for nm, sh, dd, nota in righe: + print(f" {nm:<56s} {sh:+8.2f} {dd*100:7.2f}% {nota}") + print() + s_sm = stats(cc["DBT_static_basis_sm_full"])["sharpe"] + s_2l = stats(res[+1]["daily"])["sharpe"] + print(f" SCOMPOSIZIONE (lente LISCIA, la piu' favorevole al filone):") + print(f" il 2022 vale {s_win - s_full:+.2f} di Sharpe (7,88 -> 3,72)") + print(f" la base MTM vale {s_full - s_sm:+.2f} (3,72 -> {s_sm:.2f})") + print(f" la SECONDA GAMBA {s_sm - s_2l:+.2f} ({s_sm:.2f} -> {s_2l:.2f})") + print(f" (lente GREZZA: il 2022 {s_win - s_full:+.2f}, la base {s_full - s_bas:+.2f}, la seconda gamba " + f"{s_bas - s_2l:+.2f})") + print() + print(f" ⚠️ CORREZIONE A ME STESSO, fatta guardando la mia stessa tabella: avevo scritto") + print(f" 'il buco piu' grande non era il 2022'. L'aritmetica dice il CONTRARIO — il 2022 e'") + print(f" il singolo pezzo piu' grande ({s_win - s_full:+.2f}) su entrambe le lenti. Il punto vero e' un") + print(f" altro, ed e' piu' forte: **il 2022 da solo NON BASTA**. Toglierlo dal buco porta lo") + print(f" Sharpe da 7,88 a 3,72, che `implausible_sharpe` SEGNALA ANCORA. I due pezzi che il") + print(f" 26/06 non aveva dichiarato — mark-to-market della base e assenza di una gamba spot") + print(f" negoziabile — valgono INSIEME quanto il 2022 ({s_full - s_2l:+.2f} sulla lente liscia) e sono") + print(f" quelli che portano il numero sotto 1. La ragione pubblicata era NECESSARIA e non") + print(f" SUFFICIENTE.") + print(f" 📌 E il salto piu' grande della tabella non e' nemmeno nell'elenco: e' il VENUE") + print(f" ({s_hl - s_win:+.2f}, da HL a Deribit sulla STESSA finestra e collo STESSO modello). Un carry") + print(f" di funding misurato su un venue non e' un carry di funding.") + print() + print(f" leva massima sostenibile dal carry a 2 gambe in direzione CC01: " + f"{mg[+1]['lam_full']:.1f}x per gamba sulla") + print(f" storia piena, contro {mg[+1]['lam_win']:.1f}x che la sola finestra di CC01 avrebbe autorizzato") + print(f" ({mg[+1]['lam_win']/mg[+1]['lam_full']:.1f}x di leva di troppo). Sul carry PURO con MTM della base: " + f"~5,6-6,4x contro le") + print(f" ~80x che un maxDD dello 0,8% farebbe credere sostenibili.") + print() + print(" ✅ Il criterio di riapertura dichiarato il 26/06 era FALSIFICABILE ed e' stato") + print(" ESEGUITO: il dato che mancava ora esiste, la misura e' stata fatta, e il verdetto") + print(" non cambia. **Il filone era chiuso per la ragione giusta** — e ora si sa anche") + print(" che quella ragione era NECESSARIA ma non sufficiente: il 2022 era il pezzo piu'") + print(" grande dei tre, e da solo lascia il candidato ancora sopra ogni soglia di") + print(" plausibilita'. Riaprire un filone su dati nuovi ha prodotto un verdetto") + print(" INVARIATO e una diagnosi MIGLIORE — che e' il risultato che si voleva.") + print(f"\n tempo totale {time.time()-t0:.1f}s") + + +if __name__ == "__main__": + main() From ee5cbb2c08504b363d794629108c1cb665fd0292 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 22:43:24 +0000 Subject: [PATCH 046/114] =?UTF-8?q?research(wave-0822):=20FUNDING=20misura?= =?UTF-8?q?to=20=E2=80=94=202,16%/anno=20di=20drift,=20muri=20+17,5%;=20es?= =?UTF-8?q?posizione=20e=20funding=20sono=20correlati=20e=20la=20stima=20i?= =?UTF-8?q?ngenua=20sottostima=20di=202x?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 78 ++ scripts/research/r0822d_funding.py | 1212 ++++++++++++++++++++++++++++ 2 files changed, 1290 insertions(+) create mode 100644 scripts/research/r0822d_funding.py diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 31f6992..4d1b099 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -700,6 +700,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 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%** | | 33 | WORST-DAY | il peggior giorno del libro, MISURATO: il gradino di leva e' autorizzabile? | **LEAD** — l'aritmetica passa, **il knob non esiste** | | 34 | CC01-2022 | il carry riaperto sui dati che gli mancavano per costruzione | **RESTA SCARTATO** — per la ragione giusta, ma non quella pubblicata | +| 35 | FUNDING | il costo mai prezzato di TENERE una posizione aperta | **LEAD** — **−2,16%/anno di drift**, muri **+17,5%** | | 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 | @@ -1634,3 +1635,80 @@ contro −6,0%, l'85% della perdita) perche' il funding negativo e' **persistent **VERDETTO: `CC01 COL 2022 DENTRO: RESTA SCARTATO — e per la ragione giusta, ma quella pubblicata era necessaria e non sufficiente.`** + +--- + +## 35 — FUNDING (il costo che il progetto non aveva mai prezzato) + +`scripts/research/r0822d_funding.py` (1.212 righe, 32 s, `--no-net` usa la cache). **Nessuna cella e' +una selezione** → niente DSR da calcolare. Ridotto per budget: muri a 2.000 path (**risoluzione ~1%, +dichiarata**). + +Il progetto ha prezzato fee, slippage, min-order, pavimento IB, haircut, fortuna d'ancora, degrado +d'esecuzione, look-ahead, backfill, split, contaminazione EUR/USD, rischio di venue e fisco — **e mai +il costo di TENERE APERTA una posizione su un perpetual.** + +| serie | ShFULL | ShHOLD | maxDD | CAGR | drift | +|---|---|---|---|---|---| +| LIBRO 75/25 path live | 1,692 | 1,214 | 11,2% | 20,43% | 19,24% | +| **+ funding** | **1,513** | **1,154** | **11,5%** | **17,87%** | **17,08%** | +| Δ | −0,179 | −0,060 | **+0,3pp** | −2,56% | **−2,16%** | +| TP01 canonico | 1,314 → **1,146** | 0,489 → 0,420 | 14,3 → 15,2% | 16,43 → 14,02% | −2,10% | +| SKH01 50/50 path live | 1,247 → **1,154** | 1,456 → 1,439 | 24,6 → 24,8% | 30,56 → 27,59% | −2,32% | + +📌 **Il costo va citato CON LA FINESTRA: −2,16%/anno** (2019-2026, proxy inverse sul 40% del campione) +· **−1,39%** sulla sola finestra dello strumento vero (≥2022-03) · **−0,55%** sul 2025+ — e il 2022 e +il 2026 costano ~0 **perche' il libro era FLAT** (esposizione 0,008 / 0,018), non perche' il funding +sia sparito. + +🚨 **IL MECCANISMO, ed e' il risultato trasferibile: esposizione e funding sono POSITIVAMENTE +CORRELATI.** Il tasso che il libro paga davvero e' **1,86-2,55×** l'incondizionato per TP01 e +**2,41-3,44×** per SKH01. **Il prodotto ingenuo "esposizione media × tasso medio" sottostima di ~2×** +— ed e' esattamente la forma della stima grezza da cui e' nato il filone. +❌ **L'ipotesi da cui il filone e' partito — "SKH01 paga meno perche' sta a mercato il 12% del tempo" +— e' FALSA:** 2,24%/anno di sleeve contro **2,02%** di TP01. Quando e' a mercato SKH01 sta a nozionale +**1,0×** (TP01 vol-targeted sta a **0,14×**) **ed entra sui breakout, cioe' quando il funding e' caro.** +**Il peso ottimale resta 0,25** con e senza funding, e il delta e' negativo a **23/23 offset per ogni w**. +⚠️ **Il proxy Hyperliquid era 2,1-2,2× il venue vero** sulle stesse ore (14,8% contro 6,8-7,1%): **due +errori opposti — tasso troppo alto, esposizione condizionale troppo bassa — che quasi si +compensavano.** *Compensarsi non e' misurare.* + +🚨 **COSA CAMBIA NEI NUMERI PUBBLICATI:** rendita perpetua **10,73% → 9,14%**; muro +**$276,6k → $325,0k (+17,5%)**; traiettoria da $600 a €250/mese **16,4a con P(20a) 90% → 18,7a con +P(20a) 64%**; **soffitto direzionale ~1,31 → ~1,15**. +📌 **E il maxDD NON si muove (+0,3pp): e' una tassa sul DRIFT, non sulla coda** — per questo colpisce i +**muri** molto piu' dello Sharpe. Il gradino di leva **regge** (k* 13,7 → 12,3, guadagno del passo +1,00→1,25× −12%). +**NON cambia:** il peso **75/25**, il gradino di leva, il maxDD, ne' **l'ordine delle leve del piano**. + +⚠️ **AVVERTENZA DEL COORDINATORE, non dell'agente: questa correzione e quella del FISCO si SOMMANO, e +nessuna tabella pubblicata le contiene entrambe.** La riga "€250/mese" vale P(20a) **92% al lordo** · +**52% col fisco** (07/08) · **64% col funding** (oggi) — le due lenti sono state applicate +**separatamente** allo stesso numero lordo. La tabella con entrambe **non esiste ancora**, e il +risultato congiunto sara' **peggiore di 52%**. + +✅ **Validato contro il CONTO VERO, e per ELIMINAZIONE invece che per grandezza:** modello contro +`realized_pnl` di sessione **ETH −0,2% / BTC −7,7%**, mentre le finestre sbagliate sbagliano di 2-4×. +L'identificazione regge perche' nella sessione ci sono stati **0 ordini** (letti dal log) → il P&L di +trading realizzato e' **esattamente zero**, quindi quel campo non puo' essere P&L di trading; e il +segno e' negativo su due gambe **entrambe long**. Funding realmente pagato da quando il libro esegue: +**$0,22**. Convenzione temporale **verificata sui dati** (`interest_8h` = somma delle 8 ore trailing +incluso T: mediana |dif| **2,2e-08** contro 1,1e-06 dell'alternativa). Replica **bit-exact 5/5**. + +⚠️ **BUG CATTURATO DAL CONTROLLO POSITIVO, non a occhio, e in famiglia gia' nota:** +`DatetimeIndex.astype("int64")` su tre serie dello stesso progetto con risoluzioni **diverse** +(`[ms]` il funding, `[us]` `al.get`, `[s]` il log) dava funding **0,000%/anno su SKH01** e +**+1,229%/GIORNO su TP01**. **Nessuna eccezione, nessun NaN: due numeri plausibili in due punti +diversi.** E' la stessa famiglia codificata in CLAUDE.md il **01/07** (`DatetimeIndex.view("int64")` +su indici tz-aware non-ns → look-ahead che `causality_ok` non vede). Ora c'e' un controllo che +confronta `searchsorted` con un `groupby` di pandas **e verifica di avere potenza**. +⚠️ **Secondo errore corretto in sessione:** contava `diff(pos) < 0` come vendite — **su un perpetuale +lineare il nozionale scende da solo quando scende il mark.** + +**Smentitore principale dichiarato:** sul **pre-2022** l'agente usa l'**inverse come proxy** e i due +strumenti correlano solo **0,27-0,40** orario → se il funding dei lineari nel 2019-2021 fosse stato +molto piu' basso, il −2,16% scenderebbe verso il **−1,39%** della finestra pulita. + +**VERDETTO: LEAD** (misura di politica, non strategia). +**`IL FUNDING COSTA 2,16%/ANNO NETTO AL LIBRO` — quanto la fortuna d'ancora ×0,89 che il progetto ha +misurato con enorme cura, e sposta i muri di capitale del +17,5%.** diff --git a/scripts/research/r0822d_funding.py b/scripts/research/r0822d_funding.py new file mode 100644 index 0000000..e18f057 --- /dev/null +++ b/scripts/research/r0822d_funding.py @@ -0,0 +1,1212 @@ +#!/usr/bin/env python +"""r0822d_funding.py — IL FUNDING DEI PERPETUAL: il costo che il progetto non ha mai prezzato. + +CONTESTO (ondata 2026-08-22, sezione 33 di docs/research/RESULTS-0822.md). Verificato dal +coordinatore: **zero occorrenze della parola `funding`** in `src/backtest/harness.py`, +`src/strategies/trend_portfolio.py`, `src/portfolio/{sleeves,portfolio}.py` e in tutto +`src/live/`. Il progetto ha prezzato fee, slippage, min-order, pavimento IB, haircut small-cap, +fortuna d'ancora, degrado d'esecuzione, look-ahead, split, contaminazione EUR/USD, rischio di +venue e fisco — e MAI il costo di TENERE APERTA una posizione. + +La stima grezza che ha fatto emergere il buco: proxy Hyperliquid 14,8%/anno x esposizione long +media di TP01 0,103·E = ~1,53%/anno = il 9% del drift de-luckato. Questo script la sostituisce +con una MISURA. + +COSA FA (un grado di liberta' per volta, replica di controllo prima di ogni delta): + 0. IL DATO. Storico funding da `public/get_funding_rate_history` di Deribit per i QUATTRO + perpetual: i LINEARI USDC (`BTC_USDC-PERPETUAL`/`ETH_USDC-PERPETUAL`, che sono cio' che il + libro esegue davvero) e gli INVERSE (`BTC-PERPETUAL`/`ETH-PERPETUAL`, che hanno la storia + lunga). Copertura dichiarata, buchi contati, accordo linear-vs-inverse MISURATO sulla + finestra comune. Cache su scratchpad; pacing <=2 req/s; mai nei minuti :07 (cron_book) e + :25-:29 (cron_chain). + 1. REPLICA DI CONTROLLO. Con funding SPENTO le repliche devono riprodurre BIT-EXACT + `al.tp01_baseline_daily`, `CC.tp01_realistic`, `r02.sim_equity` e `CC.book_series('hourly')`. + Nessun delta viene stampato prima che questa sezione passi. + 2. IL SEGNO. Il funding si PAGA da long in contango e si INCASSA da short. TP01 e' long-flat + (paga quasi sempre), SKH01 e' long/short e sta a mercato una frazione del tempo (puo' + incassare). Il segno NON e' assunto: e' misurato appaiando la posizione oraria/giornaliera + di ciascuno sleeve col funding del SUO strumento. + 3. I NUMERI. Sharpe FULL/IS/HOLD, maxDD, CAGR, drift di TP01, SKH01 e del libro 75/25, con e + senza funding, e il DELTA su ciascuno. + 4. LE DECISIONI. Soffitto ~1,3 · peso 75/25 (sweep w x banda d'ancora, mediana delle + DIFFERENZE APPAIATE) · muri di capitale e rendita perpetua (che vivono sul DRIFT) · + k* e il gradino di leva (il funding e' proporzionale all'esposizione, quindi scala con k). + 5. CONTROLLO SUL CONTO VERO. Il libro esegue dal 2026-07-08: posizione oraria ricostruita da + `logs/cron_book.log`, funding modellato coi tassi REALI, confronto col `realized_pnl` che + il conto riporta sulle due posizioni aperte. + 6. COSA NON E' CATTURATO, quantificato: l'approssimazione "esposizione media x tasso medio" + contro l'accrual esatto per trade (su SKH01, che entra ed esce), il proxy inverse prima + del 2022-03, la discretezza del pagamento. + +VINCOLI: nessun file di produzione toccato (config/, src/, scripts/live/, cron INVARIATI); +nessun commit; output solo a video + cache in scratchpad. Runtime ~2-6 min con cache calda. + +Uso: `uv run python scripts/research/r0822d_funding.py` + `--no-net` usa solo la cache (nessuna richiesta a Deribit) +""" +from __future__ import annotations + +import datetime as dt +import json +import re +import sys +import time +import urllib.request +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +sys.path.insert(0, str(ROOT / "scripts" / "research")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +sys.path.insert(0, str(ROOT)) + +import altlib as al # noqa: E402 +import r0702_anchor_skh01 as r02 # noqa: E402 (run_asset / sim_equity / get5m) +import r0724_skh_live_weight as skl # noqa: E402 (skh_book, W_GRID, OFFSETS) +import r0725_capcurve as CC # noqa: E402 (tp01_realistic, book_series, survival) +import r0726_capwall_refresh as WR # noqa: E402 (perp_and_wall, trajectory) +from src.data.downloader import load_data # noqa: E402 +from src.portfolio.portfolio import combine_outer, metrics # noqa: E402 +from src.strategies.trend_portfolio import ( # noqa: E402 + CANONICAL, TrendPortfolio, resample_1d, simple_returns) + +# --------------------------------------------------------------------------- +# Costanti dichiarate +# --------------------------------------------------------------------------- +# Cache dello storico funding FUORI dal repo (`data/raw/` non e' toccabile da questo filone, +# e questi parquet non sono dato certificato: sono una cache ri-scaricabile in ~4 minuti). +_SP = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/fund") +SCRATCH = _SP if _SP.parent.exists() else Path("/tmp/pythagoras_funding_cache") +API = "https://www.deribit.com/api/v2/public/get_funding_rate_history" +SPAN_MS = 30 * 24 * 3600 * 1000 # 30 giorni per richiesta (l'endpoint da' 744 record) +HOLDOUT = al.HOLDOUT +ASSETS = ("BTC", "ETH") +MSH = 3_600_000 +ALLOC = 600.0 # il capitale su cui gira book_series (conto reale $635) +DELUCK = 0.89 # fattore d'ancora MISURATO (r0726_deluck_factor) +EURUSD = CC.EURUSD +N_PATHS_WALL = 2000 # risoluzione MC dichiarata (il muro e' +-1%) + +# strumento -> (data di creazione su Deribit, asset) +INSTR = { + "BTC_USDC-PERPETUAL": ("2022-03-09", "BTC", "linear"), + "ETH_USDC-PERPETUAL": ("2022-03-14", "ETH", "linear"), + "BTC-PERPETUAL": ("2018-08-14", "BTC", "inverse"), + "ETH-PERPETUAL": ("2019-03-14", "ETH", "inverse"), +} +LINEAR = {"BTC": "BTC_USDC-PERPETUAL", "ETH": "ETH_USDC-PERPETUAL"} +INVERSE = {"BTC": "BTC-PERPETUAL", "ETH": "ETH-PERPETUAL"} + +USE_NET = "--no-net" not in sys.argv +T0 = time.time() + + +def sez(t: str) -> None: + print("\n" + "=" * 100) + print(f" {t}") + print("=" * 100) + + +def clock() -> str: + return f"[{time.time() - T0:5.0f}s]" + + +# =========================================================================== +# 0) IL DATO — storico funding Deribit +# =========================================================================== +def _pace() -> None: + """<=2 req/s, e mai nei minuti :07 (cron_book) e :25-:29 (cron_chain).""" + while dt.datetime.now(dt.timezone.utc).minute in (7, 25, 26, 27, 28, 29): + time.sleep(20) + time.sleep(0.55) + + +def fetch_funding(instr: str, start_iso: str) -> pd.DataFrame: + """Storico orario del funding, incrementale su cache parquet.""" + SCRATCH.mkdir(parents=True, exist_ok=True) + out = SCRATCH / f"{instr.replace('-', '_')}.parquet" + have = pd.read_parquet(out) if out.exists() else pd.DataFrame() + if not USE_NET: + if not len(have): + raise RuntimeError(f"--no-net ma cache assente per {instr}") + return have + t0 = int(pd.Timestamp(start_iso, tz="UTC").timestamp() * 1000) + if len(have): + t0 = int(have["ts"].max()) - MSH + tend = int(pd.Timestamp.now(tz="UTC").floor("h").timestamp() * 1000) + rows, a = [], t0 + while a < tend: + b = min(a + SPAN_MS, tend) + u = f"{API}?instrument_name={instr}&start_timestamp={a}&end_timestamp={b}" + for att in range(4): + try: + _pace() + r = json.load(urllib.request.urlopen(u, timeout=30))["result"] + break + except Exception: + if att == 3: + raise + time.sleep(3 * (att + 1)) + rows.extend({"ts": x["timestamp"], "i1h": x["interest_1h"], + "i8h": x["interest_8h"], "index": x["index_price"]} for x in r) + a = b + df = pd.concat([have, pd.DataFrame(rows)], ignore_index=True) if len(have) else pd.DataFrame(rows) + df = df.drop_duplicates(subset="ts").sort_values("ts").reset_index(drop=True) + df.to_parquet(out, index=False) + return df + + +def sezione0() -> dict: + sez("0) IL DATO — storico funding Deribit (endpoint pubblico get_funding_rate_history)") + print(" Il libro esegue sui LINEARI USDC. Gli INVERSE sono qui solo perche' hanno la storia") + print(" lunga: servono come PROXY prima del 2022-03, e l'accordo fra i due si MISURA.") + print(" (4a e 5a occorrenza dello schema `fee_watch`: puntare la misura sull'inverse") + print(" avrebbe misurato uno strumento che il libro non trada.)\n") + F = {} + print(f" {'strumento':<22} {'righe':>7} {'da':>12} {'a':>12} {'ore attese':>11} " + f"{'buchi':>6} {'ann. medio':>11}") + for instr, (start, asset, kind) in INSTR.items(): + d = fetch_funding(instr, start) + d = d.copy() + d["dt"] = pd.to_datetime(d["ts"], unit="ms", utc=True) + d = d.drop_duplicates(subset="ts").sort_values("dt").set_index("dt") + span_h = int((d.index.max() - d.index.min()) / pd.Timedelta("1h")) + 1 + gaps = span_h - len(d) + ann = float(d["i1h"].mean()) * 24 * 365.25 + F[instr] = d + print(f" {instr:<22} {len(d):>7,} {str(d.index.min().date()):>12} " + f"{str(d.index.max().date()):>12} {span_h:>11,} {gaps:>6,} {ann:>10.2%}") + + b0 = pd.Timestamp("2019-03-14", tz="UTC") + sw = min(F[LINEAR[a]].index.min() for a in ASSETS) + tot_d = (F[LINEAR["BTC"]].index.max() - b0).days + print("\n ⚠️ VINCOLO DEL FILONE, da dire subito: i perpetual LINEARI USDC esistono dal") + print(f" {sw.date()}. Il backtest del libro parte dal 2019-03-14 -> per " + f"{(sw-b0).days/tot_d:.0%} del campione ({(sw-b0).days:,} giorni su {tot_d:,}) il") + print(" funding dello strumento ESEGUITO non esiste. Si usa l'inverse come proxy e si") + print(" misura quanto i due differiscono dove coesistono.") + + print("\n ANNO per ANNO (annualizzato, ore x 24 x 365.25):") + print(f" {'anno':>6} " + " ".join(f"{k.replace('-PERPETUAL',''):>12}" for k in INSTR)) + yrs = sorted({y for d in F.values() for y in d.index.year.unique()}) + for y in yrs: + cells = [] + for k in INSTR: + d = F[k] + g = d[d.index.year == y]["i1h"] + cells.append(f"{g.mean()*24*365.25:>11.2%}" if len(g) > 100 else f"{'—':>12}") + print(f" {y:>6} " + " ".join(cells)) + + print("\n CONVENZIONE TEMPORALE — verificata dai dati, non assunta dalla documentazione.") + print(" Se il record etichettato T copre l'ora CHE FINISCE in T, allora `interest_8h` deve") + print(" essere la somma degli 8 `interest_1h` fino a T INCLUSO. Le due alternative danno") + print(" numeri diversi e si vedono:") + print(f" {'ipotesi':<26} {'corr':>9} {'mediana |dif|':>15}") + d = F["BTC_USDC-PERPETUAL"] + h = {"trailing, T incluso": d["i1h"].rolling(8).sum(), + "trailing, T escluso": d["i1h"].shift(1).rolling(8).sum(), + "leading (T -> T+8h)": d["i1h"][::-1].rolling(8).sum()[::-1].shift(-1)} + for lbl, x in h.items(): + m = (~x.isna()) & (~d["i8h"].isna()) + print(f" {lbl:<26} {float(x[m].corr(d['i8h'][m])):>9.6f} " + f"{float((x[m]-d['i8h'][m]).abs().median()):>15.3e}") + print(" -> 'trailing, T incluso'. E' la convenzione usata da tutto lo script.") + + # accordo linear vs inverse sulla finestra comune + print("\n ACCORDO LINEAR vs INVERSE sulla finestra comune (ore appaiate):") + print(f" {'asset':>6} {'ore':>8} {'ann.LIN':>9} {'ann.INV':>9} {'corr':>7} " + f"{'diff annua':>11} {'|diff| med/ora':>15}") + agree = {} + for a in ASSETS: + L, I = F[LINEAR[a]]["i1h"], F[INVERSE[a]]["i1h"] + J = pd.concat({"L": L, "I": I}, axis=1, join="inner").dropna() + d_ann = float((J["L"] - J["I"]).mean()) * 24 * 365.25 + agree[a] = dict(n=len(J), lin=float(J["L"].mean()) * 24 * 365.25, + inv=float(J["I"].mean()) * 24 * 365.25, + corr=float(J["L"].corr(J["I"])), diff=d_ann) + print(f" {a:>6} {len(J):>8,} {agree[a]['lin']:>8.2%} {agree[a]['inv']:>8.2%} " + f"{agree[a]['corr']:>7.3f} {d_ann:>10.2%} {float((J['L']-J['I']).abs().median()):>15.2e}") + print("\n IL PROXY DA CUI E' NATO IL FILONE — Hyperliquid (data/raw/hlfund_*_1h.parquet)") + print(" contro il venue su cui il libro esegue davvero, sulle ore APPAIATE:") + print(f" {'asset':>6} {'ore comuni':>11} {'ann. HL':>9} {'ann. Deribit':>13} {'rapporto':>9}") + for a in ASSETS: + try: + hl = pd.read_parquet(ROOT / "data" / "raw" / f"hlfund_{a.lower()}_1h.parquet")["funding"] + except Exception: + print(f" {a:>6} {'—':>11} (file assente)") + continue + hl.index = pd.DatetimeIndex(hl.index).floor("h") + hl = hl[~hl.index.duplicated(keep="last")] + de = F[LINEAR[a]]["i1h"] + J = pd.concat({"hl": hl, "de": de}, axis=1, join="inner").dropna() + print(f" {a:>6} {len(J):>11,} {float(J['hl'].mean())*24*365.25:>9.2%} " + f"{float(J['de'].mean())*24*365.25:>13.2%} " + f"{float(J['hl'].mean())/float(J['de'].mean()):>8.2f}x") + print(" ⚠️ Il funding NON e' una proprieta' del mercato, e' una proprieta' del VENUE (formula") + print(" e cap diversi). Usare il tasso di Hyperliquid per stimare il costo di un libro che") + print(" esegue su Deribit e' lo stesso errore di `fee_watch` il 21/08 — misurare uno") + print(" strumento che non si trada — con l'aggravante che qui non e' nemmeno lo stesso") + print(" exchange. (Il numero finale esce comunque piu' ALTO della stima grezza: due") + print(" errori in direzioni opposte, il tasso troppo alto e l'esposizione condizionale") + print(" troppo bassa. Compensarsi non e' misurare.)") + print(" 📌 E ha una CONSEGUENZA di venue: se a $20k si riapre lo split e XS01 va su HL, il") + print(" suo funding va prezzato col tasso di HL, non con questo — ed e' un altro numero.") + + return dict(F=F, agree=agree) + + +def build_rate(F: dict) -> dict: + """Serie oraria del funding per asset: LINEARE dove esiste, INVERSE prima (proxy dichiarato). + Ritorna, per asset: ts (ms, fine dell'ora), i1h, cumF (somma cumulata), e la data di switch.""" + out = {} + for a in ASSETS: + lin, inv = F[LINEAR[a]]["i1h"], F[INVERSE[a]]["i1h"] + sw = lin.index.min() + merged = pd.concat([inv[inv.index < sw], lin]).sort_index() + merged = merged[~merged.index.duplicated(keep="last")] + ts = to_ms(merged.index) + v = merged.values.astype(float) + out[a] = dict(ts=ts, i1h=v, cum=np.concatenate([[0.0], np.cumsum(v)]), + switch=sw, series=merged) + return out + + +EPOCH = pd.Timestamp("1970-01-01", tz="UTC") + + +def to_ms(idx) -> np.ndarray: + """Epoca in MILLISECONDI, esplicita. + + ⚠️ NON usare `.astype("int64")` / `.view("int64")` su un DatetimeIndex: in pandas 2.x la + risoluzione non e' piu' sempre ns e la scala esce SBAGLIATA in modo silenzioso. Misurato + qui, in questa sessione, su tre serie dello stesso progetto: il funding cachato ha unita' + `datetime64[ms]`, l'indice di `al.get()` ha `datetime64[us]`, quello del log del cron ha + `datetime64[s]`. Con `.astype("int64")//1e6` le prime due finiscono su scale che + differiscono di 1.000x -> `searchsorted` restituisce l'ESTREMO dell'array invece + dell'ora giusta, e il funding usciva 0.000%/anno su SKH01 (troppo piccolo) e + +1.229%/GIORNO su TP01 (troppo grande). Nessuna eccezione, nessun NaN: due numeri + plausibili in due punti diversi, e uno dei due sarebbe passato per buono. + (Stessa famiglia dell'errore gia' codificato in CLAUDE.md il 2026-07-01.) + """ + if not isinstance(idx, pd.DatetimeIndex): + idx = pd.DatetimeIndex(idx) + if idx.tz is None: + idx = idx.tz_localize("UTC") + return ((idx - EPOCH) // pd.Timedelta(milliseconds=1)).values.astype(np.int64) + + +def cumF(rate: dict, t_ms) -> np.ndarray: + """Funding cumulato fino all'istante t (record etichettato T = ora CHIUSA in (T-1h, T]).""" + j = np.searchsorted(rate["ts"], np.asarray(t_ms, dtype=np.int64), side="right") + return rate["cum"][j] + + +def daily_funding(rate: dict, days: pd.DatetimeIndex) -> np.ndarray: + """Funding pagato da un long unitario nel giorno d = somma di i1h su (d, d+24h].""" + t0 = to_ms(days) + return cumF(rate, t0 + 24 * MSH) - cumF(rate, t0) + + +def verifica_scala(rate: dict, F: dict) -> None: + """Controllo POSITIVO sulla scala dei tempi (senza, il bug qui sotto ripassa muto). + + Non basta che i numeri sembrino plausibili: `daily_funding` (searchsorted su epoca in ms) + si confronta con un `groupby` di pandas, che non fa nessuna conversione a intero. E si + verifica che il controllo abbia POTENZA, cioe' che fallisca se la scala e' sbagliata. + """ + print("\n [SCALA] controllo positivo su daily_funding vs groupby pandas:") + for a in ASSETS: + ser = rate[a]["series"] + g = ser.groupby((ser.index - pd.Timedelta(seconds=1)).floor("D")).sum() + g = g[(g.index >= ser.index.min().floor("D") + pd.Timedelta("1D")) & + (g.index <= ser.index.max().floor("D") - pd.Timedelta("1D"))] + gi = pd.DatetimeIndex(g.index) + mine = daily_funding(rate[a], gi) + dmax = float(np.max(np.abs(mine - g.values))) + assert dmax < 1e-15, f"scala tempi rotta su {a}: max|dif|={dmax:.3e}" + bad = dict(ts=rate[a]["ts"] * 1000, cum=rate[a]["cum"]) # scala x1000 (us al posto di ms) + t = to_ms(gi[:200]) + wrong = cumF(bad, t + 24 * MSH) - cumF(bad, t) + assert float(np.max(np.abs(wrong - mine[:200]))) > 1e-9, "il controllo NON ha potenza" + print(f" {a}: {len(g):,} giorni confrontati, max|dif| = {dmax:.3e} OK " + f"(e fallisce se la scala e' x1000 -> ha potenza)") + lo = min(rate[a]["series"].index.min() for a in ASSETS) + print(" [COPERTURA] il libro parte dal 2019-03-14, ma `get_funding_rate_history` serve una") + print(f" FINESTRA MOBILE di ~7,3 anni: oggi il piu' vecchio dato e' del {lo.date()}") + print(" (i perpetual INVERSE esistono dal 2018-08/2019-03: la storia c'era, il venue") + print(" non la serve piu'). -> i primi ~47 giorni del backtest sono senza funding") + print(" modellato = 1,7% del campione. Dichiarato, non stimato.") + + +# =========================================================================== +# 1) REPLICHE (funding spento => bit-exact) e 3) LE SERIE COL FUNDING +# =========================================================================== +def tp01_canonical(rate: dict | None) -> tuple[pd.Series, dict]: + """TP01 canonico 50/50 (la lente di `al.tp01_baseline_daily`), con funding opzionale.""" + tp = TrendPortfolio(**CANONICAL) + series, diag = {}, {} + for a in ASSETS: + df = al.get(a, "1d") + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + net, _ = tp.net_returns(df) + net = np.asarray(net, float).copy() + tgt = np.asarray(tp.target_series(df), float) + pos = np.zeros(len(tgt)); pos[1:] = tgt[:-1] # tenuta durante la barra i + if rate is not None: + f = daily_funding(rate[a], idx) + net = np.clip(net - pos * f, -0.99, None) + net[0] = 0.0 + diag[a] = dict(pos=pos, f=f, idx=idx) + series[a] = pd.Series(net, index=idx) + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + return al._to_daily(0.5 * J["BTC"] + 0.5 * J["ETH"]), diag + + +def tp01_realistic(alloc: float, rate: dict | None) -> tuple[pd.Series, dict]: + """TP01 con min-order $5 (la lente di `CC.tp01_realistic`), con funding opzionale.""" + cap_asset = alloc / len(ASSETS) + tp = TrendPortfolio(**CANONICAL) + series, diag = {}, {} + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + r = simple_returns(df["close"].values.astype(float)) + tgt = np.nan_to_num(np.asarray(tp.target_series(df), float)) + held = np.empty(len(tgt)); cur = 0.0 + for i in range(len(tgt)): + if abs(tgt[i] - cur) * cap_asset >= CC.DERIBIT_MIN_ORDER: + cur = tgt[i] + held[i] = cur + pos = np.zeros(len(held)); pos[1:] = held[:-1] + net = pos * r - CC.DERIBIT_FEE_SIDE * np.abs(np.diff(pos, prepend=0.0)) + fund = np.zeros(len(pos)) + if rate is not None: + fund = pos * daily_funding(rate[a], idx) + net = net - fund + net[0] = 0.0 + diag[a] = dict(pos=pos, fund=fund, idx=idx) + series[a] = pd.Series(np.clip(net, -0.99, None), index=idx) + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + out = pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index) + if out.index.tz is None: + out.index = out.index.tz_localize("UTC") + return out, diag + + +def sim_equity_f(ltf: pd.DataFrame, ent: list, mode: str, ts5, c5, + rate: dict | None) -> tuple[np.ndarray, list]: + """Replica di `r02.sim_equity` con UN grado di liberta' in piu': il funding accumulato + fra ingresso e uscita, col SEGNO della posizione (uno short lo INCASSA). + Con rate=None deve dare bit-exact `r02.sim_equity` — e' la replica di controllo.""" + c = ltf["close"].values.astype(float) + h = ltf["high"].values.astype(float) + l = ltf["low"].values.astype(float) + n = len(c) + close_ts = ltf["timestamp"].values.astype(np.int64) + r02.MS_LTF + + def px_hour(t: int) -> float: + hb = ((t + MSH - 1) // MSH) * MSH + j = np.searchsorted(ts5, hb, side="left") + return c5[min(j, len(c5) - 1)] + + def t_hour(t: int) -> int: + return int(((t + MSH - 1) // MSH) * MSH) + + initial = 1000.0 + capital = initial + equity = np.full(n, capital, dtype=float) + busy_until = -1 + trades = [] + for i in range(n): + e = ent[i] if i < len(ent) else None + if e is None or e.get("dir", 0) == 0: + equity[i] = capital + continue + if i <= busy_until: + equity[i] = capital + continue + direction = int(e["dir"]) + tp = e.get("tp"); sl = e.get("sl") + max_bars = int(e.get("max_bars") or 24) + entry = c[i] if mode in ("canonical", "barclose") else px_hour(close_ts[i]) + exit_idx = min(i + max_bars, n - 1) + exit_lvl = c[exit_idx] + for j in range(i + 1, min(i + max_bars + 1, n)): + hit_sl = sl is not None and ( + (direction == 1 and l[j] <= sl) or (direction == -1 and h[j] >= sl)) + hit_tp = tp is not None and ( + (direction == 1 and h[j] >= tp) or (direction == -1 and l[j] <= tp)) + if hit_sl: + exit_lvl, exit_idx = sl, j + break + if hit_tp: + exit_lvl, exit_idx = tp, j + break + exit_lvl, exit_idx = c[j], j + if mode == "canonical": + exit_price = exit_lvl + elif mode == "barclose": + exit_price = c[exit_idx] + else: + exit_price = px_hour(close_ts[exit_idx]) + gross = (exit_price - entry) / entry * direction + net = gross - 0.001 + f_trade = 0.0 + if rate is not None: + t_in = close_ts[i] if mode in ("canonical", "barclose") else t_hour(close_ts[i]) + t_out = close_ts[exit_idx] if mode in ("canonical", "barclose") else t_hour(close_ts[exit_idx]) + f_trade = float(cumF(rate, t_out) - cumF(rate, t_in)) * direction + net = net - f_trade + trades.append(dict(i=i, exit_idx=exit_idx, dir=direction, gross=gross, + f=f_trade, hours=(close_ts[exit_idx] - close_ts[i]) / MSH, + t_in=close_ts[i], t_out=close_ts[exit_idx])) + capital += capital * net + capital = max(capital, 1.0) + equity[i:exit_idx + 1] = capital + busy_until = exit_idx + last = initial + for k in range(n): + if equity[k] != last and equity[k] != initial: + last = equity[k] + else: + equity[k] = last + return equity, trades + + +_SKH: dict = {} + + +def skh_daily(a: str, off: int, mode: str, rate: dict | None) -> tuple[pd.Series, list]: + key = (a, off, mode, rate is not None) + if key in _SKH: + return _SKH[key] + _, m0, ltf, ent = r02.run_asset(a, off) + df5 = r02.get5m(a) + ts5 = df5["timestamp"].values.astype(np.int64) + r02.MS5 + c5 = df5["close"].values.astype(float) + eq, tr = sim_equity_f(ltf, ent, mode, ts5, c5, rate[a] if rate else None) + s = pd.Series(eq, index=pd.DatetimeIndex(pd.to_datetime(ltf["datetime"], utc=True))) + d = s.resample("1D").last().ffill().pct_change().dropna() + _SKH[key] = (d, tr) + return _SKH[key] + + +def skh_book(off: int, mode: str, rate: dict | None) -> pd.Series: + J = pd.concat({a: skh_daily(a, off, mode, rate)[0] for a in ASSETS}, + axis=1, join="inner").fillna(0.0) + return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index) + + +def book_75_25(rate: dict | None, off: int = 0, lens: str = "hourly", + alloc: float = ALLOC) -> pd.Series: + """Il libro LIVE: TP01 75 + SKH01 25, esattamente la lente di CC.book_series.""" + skh = skh_book(off, lens, rate) + if skh.index.tz is None: + skh.index = skh.index.tz_localize("UTC") + tp, _ = tp01_realistic(alloc * 0.75, rate) + return combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": 0.75, "SKH01": 0.25}, + lo=pd.Timestamp("2019-03-14", tz="UTC")) + + +def sezione1(rate: dict) -> None: + sez("1) REPLICA DI CONTROLLO — con funding SPENTO ogni replica dev'essere BIT-EXACT") + ok = True + + a0, _ = tp01_canonical(None) + b0 = al.tp01_baseline_daily() + d = float(np.max(np.abs(a0.values - b0.reindex(a0.index).values))) + ok &= d == 0.0 + print(f" tp01_canonical(None) vs al.tp01_baseline_daily max|dif| = {d:.3e} " + f"{'OK' if d == 0 else 'FALLITA'}") + + a1, _ = tp01_realistic(ALLOC * 0.75, None) + b1 = CC.tp01_realistic(ALLOC * 0.75) + d = float(np.max(np.abs(a1.values - b1.reindex(a1.index).values))) + ok &= d == 0.0 + print(f" tp01_realistic(450,None) vs CC.tp01_realistic(450) max|dif| = {d:.3e} " + f"{'OK' if d == 0 else 'FALLITA'}") + + for mode in ("canonical", "hourly"): + for a in ASSETS: + _, m0, ltf, ent = r02.run_asset(a, 0) + df5 = r02.get5m(a) + ts5 = df5["timestamp"].values.astype(np.int64) + r02.MS5 + c5 = df5["close"].values.astype(float) + eqA, _ = sim_equity_f(ltf, ent, mode, ts5, c5, None) + eqB = r02.sim_equity(ltf, ent, mode, ts5_close=ts5, c5=c5) + d = float(np.max(np.abs(eqA - eqB))) + ok &= d == 0.0 + print(f" sim_equity_f({a},{mode:<9}) vs r02.sim_equity " + f"max|dif| = {d:.3e} {'OK' if d == 0 else 'FALLITA'}") + + a2 = book_75_25(None) + b2 = CC.book_series("hourly") + d = float(np.max(np.abs(a2.values - b2.reindex(a2.index).values))) + ok &= d == 0.0 + print(f" book_75_25(None) vs CC.book_series('hourly') max|dif| = {d:.3e} " + f"{'OK' if d == 0 else 'FALLITA'}") + + if not ok: + raise SystemExit("REPLICA FALLITA: nessun delta e' pubblicabile.") + print("\n ✅ 5/5 repliche bit-exact. Da qui in poi l'UNICO grado di liberta' e' il funding.") + + +# =========================================================================== +# 2) IL SEGNO — quanto si e' esposti, e da che lato +# =========================================================================== +def sezione2(rate: dict) -> dict: + sez("2) IL SEGNO — l'esposizione non e' assunta, e' appaiata col funding del suo strumento") + out = {} + + _, dg = tp01_realistic(ALLOC * 0.75, rate) + print("\n TP01 (long-flat, posizione continua vol-targeted; esposizione = frazione del") + print(" capitale della GAMBA; nel libro pesa 0.75 x 0.5 per asset = 0.375 x posizione)") + print(f" {'asset':>6} {'giorni':>7} {'%tempo>0':>9} {'pos media':>10} {'pos med|>0':>11} " + f"{'funding pagato/anno':>20} {'esposiz. x tasso':>17}") + tot_leg = [] + for a in ASSETS: + pos, f, idx = dg[a]["pos"], dg[a]["fund"], dg[a]["idx"] + yrs = len(pos) / 365.25 + drag = float(f.sum()) / yrs # in frazione del capitale-gamba, per anno + rt = rate[a]["series"] + rt = rt[(rt.index >= idx.min()) & (rt.index <= idx.max())] + print(f" {a:>6} {len(pos):>7,} {float((pos > 0).mean()):>9.1%} {float(pos.mean()):>10.3f} " + f"{float(pos[pos > 0].mean()):>11.3f} {drag:>19.3%} " + f"{float(pos.mean()) * float(rt.mean()) * 24 * 365.25:>16.3%}") + tot_leg.append(drag) + out["tp01_leg_drag"] = float(np.mean(tot_leg)) + print(f" -> drag medio per gamba {out['tp01_leg_drag']:.3%}/anno = sullo SLEEVE " + f"{out['tp01_leg_drag']:.3%}/anno (le due gambe pesano 0.5 ciascuna)") + + print("\n SKH01 (long/short, round-trip discreti, non-overlap: sta a mercato una frazione") + print(" del tempo — e uno SHORT il funding lo INCASSA)") + print(f" {'asset':>6} {'trade':>6} {'%long':>7} {'%tempo a mercato':>17} {'ore/trade':>10} " + f"{'pagato/anno':>12} {'incassato/anno':>15} {'NETTO/anno':>11}") + tot_skh = [] + for a in ASSETS: + d, tr = skh_daily(a, 0, "hourly", rate) + span_h = (tr[-1]["t_out"] - tr[0]["t_in"]) / MSH + yrs = span_h / (24 * 365.25) + f = np.array([t["f"] for t in tr]) + dirs = np.array([t["dir"] for t in tr]) + hrs = np.array([t["hours"] for t in tr]) + inmkt = hrs.sum() / span_h + paid = float(f[f > 0].sum()) / yrs + got = float(-f[f < 0].sum()) / yrs + net = float(f.sum()) / yrs + tot_skh.append(net) + print(f" {a:>6} {len(tr):>6} {float((dirs > 0).mean()):>7.1%} {inmkt:>17.1%} " + f"{float(hrs.mean()):>10.1f} {paid:>11.3%} {got:>14.3%} {net:>10.3%}") + out["skh_drag"] = float(np.mean(tot_skh)) + print(f" -> drag NETTO dello sleeve SKH01 {out['skh_drag']:.3%}/anno") + + print("\n ❌ IPOTESI DEL BRIEF FALSIFICATA: «SKH01 sta a mercato solo il 12% del tempo ->") + print(" paga MOLTO MENO funding di TP01, ed e' un argomento a suo favore mai contato».") + print(f" Misurato: SKH01 {out['skh_drag']:.2%}/anno contro TP01 " + f"{out['tp01_leg_drag']:.2%}/anno di sleeve — SKH01 paga di PIU', non di meno.") + print(" Due ragioni, entrambe misurabili: (i) quando e' a mercato sta a nozionale 1.0x,") + print(" mentre TP01 vol-targeted sta a 0.14x in media -> a parita' di tempo pesa 7x;") + print(" (ii) entra sui breakout, e i breakout sono proprio quando il funding e' caro.") + + print("\n 📌 IL MECCANISMO, ed e' il risultato trasferibile: ESPOSIZIONE e FUNDING sono") + print(" POSITIVAMENTE CORRELATI. Tasso medio annualizzato, non condizionato vs") + print(" condizionato all'essere in posizione:") + print(f" {'':>6} {'incondizionato':>15} {'| TP01 long':>12} {'| SKH01 a mercato':>18} " + f"{'rapporto TP01':>14} {'rapporto SKH':>13}") + for a in ASSETS: + ser = rate[a]["series"] + unc = float(ser.mean()) * 24 * 365.25 + # TP01: media del tasso GIORNALIERO pesata dalla posizione (= tasso "che si paga davvero") + pos, f, idx = dg[a]["pos"], dg[a]["fund"], dg[a]["idx"] + fd = daily_funding(rate[a], idx) + w = pos / pos.sum() if pos.sum() > 0 else pos + tp_cond = float(np.sum(w * fd)) * 365.25 + # SKH01: media del tasso orario sulle ore in cui e' a mercato (segno a parte) + _, tr = skh_daily(a, 0, "hourly", rate) + ore = float(np.sum([abs(t["hours"]) for t in tr])) + lordo = float(np.sum([abs(cumF(rate[a], t["t_out"]) - cumF(rate[a], t["t_in"])) + for t in tr])) + skh_cond = lordo / ore * 24 * 365.25 if ore else float("nan") + print(f" {a:>6} {unc:>15.2%} {tp_cond:>12.2%} {skh_cond:>18.2%} " + f"{tp_cond/unc:>13.2f}x {skh_cond/unc:>12.2f}x") + print(" -> il prodotto ingenuo «esposizione media x tasso medio» SOTTOSTIMA di ~2x. E' la") + print(" forma esatta della stima grezza da cui e' nato questo filone (14,8% x 0,103 =") + print(" 1,53%/anno): giusta come ordine di grandezza, sbagliata nei due fattori.") + print(" ⚠️ Il rapporto e' su TUTTO il campione e mescola due cose vere ma diverse: la") + print(" correlazione DENTRO l'anno (trend long = perp in contango) e il fatto che i") + print(" due anni di funding piu' caro (2019, 2021) sono anche quelli in cui TP01 era") + print(" piu' long. Per il COSTO non cambia nulla — si paga lo stesso — ma per") + print(" PREVEDERLO si', e la previsione non e' questo numero.") + + print(f"\n 📌 A livello di LIBRO 75/25: 0.75 x {out['tp01_leg_drag']:.3%} + " + f"0.25 x {out['skh_drag']:.3%} = " + f"{0.75 * out['tp01_leg_drag'] + 0.25 * out['skh_drag']:.3%}/anno di costo lordo") + print(" (e' una scomposizione, non il risultato: il numero che conta e' il DELTA di") + print(" drift misurato sulla serie composta, sezione 3.)") + out["book_drag_decomp"] = 0.75 * out["tp01_leg_drag"] + 0.25 * out["skh_drag"] + return out + + +# =========================================================================== +# 3) I NUMERI +# =========================================================================== +def stat(s: pd.Series) -> dict: + m = metrics(s) + r = np.asarray(s.dropna().values, float) + return dict(full=al._sh(s), is_=al._sh(s[s.index < HOLDOUT]), hold=al._sh(s[s.index >= HOLDOUT]), + dd=al._dd_ret(s), cagr=m["cagr"], drift=float(r.mean()) * 365.25, + vol=float(r.std()) * np.sqrt(365.25), n=len(r)) + + +def riga(nome: str, a: dict, b: dict) -> None: + print(f" {nome:<34} {a['full']:>7.3f} {a['is_']:>7.3f} {a['hold']:>7.3f} {a['dd']:>7.1%} " + f"{a['cagr']:>7.2%} {a['drift']:>7.2%}") + print(f" {' + funding':<34} {b['full']:>7.3f} {b['is_']:>7.3f} {b['hold']:>7.3f} " + f"{b['dd']:>7.1%} {b['cagr']:>7.2%} {b['drift']:>7.2%}") + print(f" {' DELTA':<34} {b['full']-a['full']:>+7.3f} {b['is_']-a['is_']:>+7.3f} " + f"{b['hold']-a['hold']:>+7.3f} {b['dd']-a['dd']:>+7.1%} {b['cagr']-a['cagr']:>+7.2%} " + f"{b['drift']-a['drift']:>+7.2%}\n") + + +def sezione3(rate: dict) -> dict: + sez("3) I NUMERI COL FUNDING DENTRO — netti fee 0.10% RT, hold-out 2025+") + print(f" {'serie':<34} {'ShFULL':>7} {'ShIS':>7} {'ShHOLD':>7} {'maxDD':>7} " + f"{'CAGR':>7} {'drift':>7}") + out = {} + + p = {} + p["TP01 canonico (soffitto)"] = (tp01_canonical(None)[0], tp01_canonical(rate)[0]) + p[f"TP01 realistic ${ALLOC*0.75:.0f}"] = (tp01_realistic(ALLOC * 0.75, None)[0], + tp01_realistic(ALLOC * 0.75, rate)[0]) + p["SKH01 50/50 canonical"] = (skh_book(0, "canonical", None), skh_book(0, "canonical", rate)) + p["SKH01 50/50 path live"] = (skh_book(0, "hourly", None), skh_book(0, "hourly", rate)) + p["LIBRO 75/25 (path live)"] = (book_75_25(None), book_75_25(rate)) + for k, (A, B) in p.items(): + sa, sb = stat(A), stat(B) + riga(k, sa, sb) + out[k] = (sa, sb) + + print(" Per ANNO, libro 75/25 (drift lordo -> netto funding). La colonna ESPOSIZIONE serve") + print(" a non confondere 'il funding e' sceso' con 'il libro era flat': sono cose diverse e") + print(" producono lo stesso delta piccolo.") + A, B = p["LIBRO 75/25 (path live)"] + _, dgx = tp01_realistic(ALLOC * 0.75, rate) + expo = pd.concat({a: pd.Series(dgx[a]["pos"], index=dgx[a]["idx"]) for a in ASSETS}, + axis=1, join="inner").mean(axis=1) + print(f" {'anno':>6} {'senza':>9} {'con':>9} {'delta':>9} {'funding annualiz.':>18} " + f"{'esposiz. TP01':>14}") + for y in sorted(set(A.index.year)): + ga, gb = A[A.index.year == y], B[B.index.year == y] + if len(ga) < 30: + continue + da, db = float(ga.mean()) * 365.25, float(gb.mean()) * 365.25 + fr = np.mean([rate[a]["series"][rate[a]["series"].index.year == y].mean() + for a in ASSETS]) * 24 * 365.25 + ex = float(expo[expo.index.year == y].mean()) + print(f" {y:>6} {da:>9.2%} {db:>9.2%} {db-da:>+9.2%} {fr:>17.2%} {ex:>14.3f}") + rec = pd.Timestamp("2025-01-01", tz="UTC") + dA, dB = float(A[A.index >= rec].mean()) * 365.25, float(B[B.index >= rec].mean()) * 365.25 + print(f" {'2025+':>6} {dA:>9.2%} {dB:>9.2%} {dB-dA:>+9.2%} " + f"{np.mean([rate[a]['series'][rate[a]['series'].index >= rec].mean() for a in ASSETS])*24*365.25:>17.2%} " + f"{float(expo[expo.index >= rec].mean()):>14.3f}") + print(" 📌 IL COSTO NON E' STAZIONARIO, e le due cause si LEGGONO nelle due colonne: il") + print(" funding Deribit si e' compresso (16,9% nel 2021 -> 1,9% nel 2026) E il libro sta") + print(" meno esposto. Chi cita UN numero per il funding cita una media su un regime") + print(" cambiato; chi cita il numero del 2026 sta citando un libro quasi flat.") + + # finestra SOLO-LINEARE (nessun proxy): il controllo che il numero non venga dal proxy + lo = rate["BTC"]["switch"] + print(f"\n CONTROLLO — stessa misura sulla sola finestra LINEARE (>= {lo.date()},") + print(" nessun proxy inverse):") + print(f" {'serie':<34} {'d ShFULL':>9} {'d CAGR':>9} {'d drift':>9}") + for k, (A, B) in p.items(): + A2, B2 = A[A.index >= lo], B[B.index >= lo] + sa, sb = stat(A2), stat(B2) + print(f" {k:<34} {sb['full']-sa['full']:>+9.3f} {sb['cagr']-sa['cagr']:>+9.2%} " + f"{sb['drift']-sa['drift']:>+9.2%}") + out["_pairs"] = p + return out + + +# =========================================================================== +# 4) LE DECISIONI +# =========================================================================== +def sezione4a(res3: dict) -> None: + sez("4a) IL SOFFITTO ~1.3 SI ABBASSA?") + a, b = res3["TP01 canonico (soffitto)"] + print(f" TP01 canonico (l'unica serie a cui il 'soffitto ~1.3' si riferisce):") + print(f" Sharpe FULL {a['full']:.3f} -> {b['full']:.3f} ({b['full']-a['full']:+.3f})") + print(f" CAGR {a['cagr']:.2%} -> {b['cagr']:.2%}") + print(f" Il soffitto e' un numero arrotondato a UNA cifra: si abbassa di " + f"{abs(b['full']-a['full']):.3f}, quindi resta '~1.3'" + if abs(b["full"] - a["full"]) < 0.05 else + f" Il soffitto si sposta di {b['full']-a['full']:+.3f}: va RIDETTO.") + print(" ⚠️ Ma la lettura utile e' l'altra: il soffitto e' una soglia di SELEZIONE, e ogni") + print(" candidato direzionale mai valutato in questo progetto e' stato misurato SENZA") + print(" funding. Il confronto candidato-vs-TP01 e' quindi appaiato (entrambi al lordo)") + print(" per i candidati LONG/SHORT a esposizione simile, e distorto a favore di chi sta") + print(" a mercato di piu'. Chi paga di piu' e' chi e' piu' esposto piu' a lungo.") + + +def sezione4b(rate: dict, offsets: tuple) -> None: + sez("4b) IL PESO 75/25 SI MUOVE? (il funding e' un argomento a favore di SKH01 mai contato)") + print(" L'ipotesi era: «SKH01 sta a mercato una frazione del tempo e puo' incassare da") + print(" short -> paga MENO funding di TP01, quindi il peso ottimale sale». La prima meta'") + print(" e' gia' FALSIFICATA in sez. 2 (SKH01 paga di piu' per dollaro di sleeve). Resta la") + print(" seconda, che non e' implicata dalla prima: il peso si sposta?") + print(f" Sweep w in {list(skl.W_GRID)} x {len(offsets)} offset della griglia 230m/690m.") + print(" Statistica = MEDIANA DELLE DIFFERENZE APPAIATE (regola del progetto), lente") + print(" 'hourly' = path live, selezione sullo Sharpe IN-SAMPLE come il 24/07.\n") + + B0, _ = tp01_canonical(None) + B1, _ = tp01_canonical(rate) + + def blend(B, skh, w): + if w == 0.0: + return B + bl = combine_outer({"TP": B, "SKH": skh}, {"TP": 1.0 - w, "SKH": w}) + return bl[bl.index >= B.index.min()] + + S = {False: {}, True: {}} + for off in offsets: + S[False][off] = skh_book(off, "hourly", None) + S[True][off] = skh_book(off, "hourly", rate) + + print(f" {'w':>6} | {'IS mediano':>11} {'IS med +f':>11} {'d IS':>8} {'>0':>5} | " + f"{'FULL med':>9} {'FULL +f':>9} {'d FULL':>8} | {'HOLD med':>9} {'HOLD +f':>9}") + rows = {} + for w in skl.W_GRID: + vI0, vI1, vF0, vF1, vH0, vH1 = [], [], [], [], [], [] + for off in offsets: + b0 = blend(B0, S[False][off], w) + b1 = blend(B1, S[True][off], w) + vI0.append(al._sh(b0[b0.index < HOLDOUT])); vI1.append(al._sh(b1[b1.index < HOLDOUT])) + vF0.append(al._sh(b0)); vF1.append(al._sh(b1)) + vH0.append(al._sh(b0[b0.index >= HOLDOUT])); vH1.append(al._sh(b1[b1.index >= HOLDOUT])) + dI = np.array(vI1) - np.array(vI0) + dF = np.array(vF1) - np.array(vF0) + rows[w] = dict(is0=np.median(vI0), is1=np.median(vI1), dI=np.median(dI), + f0=np.median(vF0), f1=np.median(vF1), dF=np.median(dF), + h0=np.median(vH0), h1=np.median(vH1), pos=float((dI > 0).mean())) + r_ = rows[w] + print(f" {w:>6.2f} | {r_['is0']:>11.3f} {r_['is1']:>11.3f} {r_['dI']:>+8.3f} " + f"{r_['pos']:>5.0%} | {r_['f0']:>9.3f} {r_['f1']:>9.3f} {r_['dF']:>+8.3f} | " + f"{r_['h0']:>9.3f} {r_['h1']:>9.3f}") + + w0 = max(skl.W_GRID, key=lambda w: rows[w]["is0"]) + w1 = max(skl.W_GRID, key=lambda w: rows[w]["is1"]) + print(f"\n argmax dello Sharpe IS mediano: SENZA funding w* = {w0:.2f} " + f"CON funding w* = {w1:.2f}") + print(f" peso live = 0.25. Regola a priori del 24/07: a parita' (<{skl.TIE_TOL}) si sta al") + print(f" piu' vicino a 0.25. |IS(w*) - IS(0.25)| con funding = " + f"{abs(rows[w1]['is1'] - rows[0.25]['is1']):.3f}") + if w0 == w1: + print(" 📌 Il funding NON sposta l'argmax: il vantaggio di SKH01 e' reale ma piccolo") + print(" rispetto alla larghezza del plateau. Peso INVARIATO.") + else: + print(f" 📌 L'argmax si sposta {w0:.2f} -> {w1:.2f}: da verificare con weights_tilt_null.") + + +def sezione4c(rate: dict) -> dict: + sez("4c) I MURI DI CAPITALE E LA RENDITA PERPETUA (vivono sul DRIFT, non sullo Sharpe)") + A, B = book_75_25(None), book_75_25(rate) + need_gross = CC.TARGET_EUR_DAY * 365.0 * EURUSD / (1 - CC.TAX_RATE) + print(f" bersaglio: 50 EUR/g netti = ${need_gross:,.0f}/anno lordi (aliquota " + f"{CC.TAX_RATE:.0%}), stessa convenzione di r0726_capwall_refresh.") + print(f" de-luck del drift x{DELUCK} (MISURATO, r0726_deluck_factor). MC {N_PATHS_WALL} path") + print(" -> risoluzione ~1% sul muro: si citano 3 cifre significative, non il dollaro.\n") + print(f" {'serie':<28} {'drift':>8} {'vol':>7} {'perpetua':>9} {'muro':>12} {'leva':>5}") + out = {} + for nome, s in (("senza funding", A), ("con funding", B)): + r = s.values.astype(float) + r_dl = r - (1.0 - DELUCK) * float(r.mean()) + for k in (1.0, 1.5): + _, perp, cap = WR.perp_and_wall(r_dl, k, need_gross) + out[(nome, k)] = (perp, cap) + print(f" {nome + f' x{DELUCK}':<28} {float(r_dl.mean())*365.25:>8.2%} " + f"{float(r_dl.std())*np.sqrt(365.25):>7.2%} {perp:>9.2%} ${cap:>11,.0f} {k:>5.2f}") + p0, c0 = out[("senza funding", 1.0)] + p1, c1 = out[("con funding", 1.0)] + print(f"\n 📌 A leva 1.0: perpetua {p0:.2%} -> {p1:.2%}; muro ${c0:,.0f} -> ${c1:,.0f} " + f"({(c1/c0-1):+.1%})") + print(f" Il muro pubblicato e' $272k (lordo) / $258k (netto fisco). Qui si misura il") + print(f" DELTA con la stessa macchineria, non un muro nuovo.") + + print("\n TRAIETTORIA da $600 al capitale-rendita (mediana anni / P entro 20a), stesso") + print(" bootstrap a blocchi di r0726_capwall_refresh.trajectory:") + print(f" {'EUR/mese':>9} | {'senza funding':>22} | {'con funding':>22}") + r0 = A.values.astype(float); r0 = r0 - (1 - DELUCK) * r0.mean() + r1 = B.values.astype(float); r1 = r1 - (1 - DELUCK) * r1.mean() + t0 = WR.trajectory(r0, c0, n_paths=2000) + t1 = WR.trajectory(r1, c1, n_paths=2000) + for dep in (0, 250, 500, 1_000, 2_000): + a_, b_ = t0[dep], t1[dep] + fa = f"{a_['p50']:>6.1f}a P20 {a_['p20y']:>5.0%}" if a_["p50"] == a_["p50"] else f"{'mai':>16}" + fb = f"{b_['p50']:>6.1f}a P20 {b_['p20y']:>5.0%}" if b_["p50"] == b_["p50"] else f"{'mai':>16}" + print(f" {dep:>9} | {fa:>22} | {fb:>22}") + return out + + +def g_exact(r: np.ndarray, k: float) -> float: + x = 1.0 + k * r + return float(np.mean(np.log(np.maximum(x, 1e-12)))) * 365.25 + + +def k_star(r: np.ndarray, kmax: float = 40.0, step: float = 0.05) -> tuple[float, float]: + ks = np.arange(step, kmax + step, step) + g = np.array([g_exact(r, k) for k in ks]) + j = int(np.argmax(g)) + return float(ks[j]), float(g[j]) + + +def sezione4d(rate: dict) -> None: + sez("4d) k* E IL GRADINO DI LEVA — il funding SCALA con la leva") + print(" Il funding e' proporzionale al NOZIONALE: a leva k si paga k volte tanto. Quindi") + print(" la serie col funding va scalata per k come tutto il resto (il costo non e' fisso),") + print(" e il gradino 1.00x -> 1.25/1.50x misurato ieri ne mangia una parte.\n") + A, B = book_75_25(None), book_75_25(rate) + print(f" {'serie':<22} {'drift':>8} {'k*':>7} {'g(k*)':>8} {'g(1.00)':>9} {'g(1.25)':>9} " + f"{'g(1.50)':>9} {'mezzo-Kelly':>12}") + for nome, s in (("senza funding", A), ("con funding", B)): + r = s.values.astype(float) + ks, gs = k_star(r) + print(f" {nome:<22} {float(r.mean())*365.25:>8.2%} {ks:>7.2f} {gs:>8.2%} " + f"{g_exact(r, 1.0):>9.2%} {g_exact(r, 1.25):>9.2%} {g_exact(r, 1.50):>9.2%} " + f"{ks/2:>12.2f}") + r_dl = r - (1 - DELUCK) * r.mean() + ks2, gs2 = k_star(r_dl) + print(f" {' (de-luck x0.89)':<22} {float(r_dl.mean())*365.25:>8.2%} {ks2:>7.2f} " + f"{gs2:>8.2%} {g_exact(r_dl, 1.0):>9.2%} {g_exact(r_dl, 1.25):>9.2%} " + f"{g_exact(r_dl, 1.50):>9.2%} {ks2/2:>12.2f}") + rA = A.values.astype(float); rA -= (1 - DELUCK) * rA.mean() + rB = B.values.astype(float); rB -= (1 - DELUCK) * rB.mean() + gain0 = g_exact(rA, 1.25) - g_exact(rA, 1.0) + gain1 = g_exact(rB, 1.25) - g_exact(rB, 1.0) + print(f"\n 📌 Guadagno in log-crescita del gradino 1.00 -> 1.25 (de-luckato):") + print(f" senza funding {gain0:+.2%}/anno · con funding {gain1:+.2%}/anno " + f"(-{(1-gain1/gain0)*100:.0f}% del guadagno)" if gain0 > 0 else "") + print(" Il gradino resta positivo finche' k* > 1.25 con il funding dentro.") + + +# =========================================================================== +# 5) CONTROLLO SUL CONTO VERO +# =========================================================================== +RE_HDR = re.compile(r"^===== (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) cron_book") +RE_POS = re.compile(r"^\s+(BTC|ETH) TP .*?\|\s*pos \$([+-][\d,\.]+)") + + +def leggi_posizioni_live() -> pd.DataFrame: + """Posizione oraria REALE per asset, ricostruita da logs/cron_book.log.""" + rows, ts = [], None + for ln in (ROOT / "logs" / "cron_book.log").read_text(errors="replace").splitlines(): + m = RE_HDR.match(ln) + if m: + ts = pd.Timestamp(m.group(1)) + continue + m = RE_POS.match(ln) + if m and ts is not None: + rows.append(dict(ts=ts, asset=m.group(1), + pos=float(m.group(2).replace(",", "").replace("+", "")))) + return pd.DataFrame(rows) + + +RE_ORD = re.compile(r"^\s+-> (BUY|SELL) ") +RE_LEG = re.compile(r"^\s+(BTC|ETH) TP ") + + +def ordini_sessione(settl: pd.Timestamp) -> dict: + """Ordini REALMENTE inviati per asset dalla data `settl` (righe '-> BUY/SELL' del log, + che seguono la riga della gamba a cui appartengono).""" + out: dict = {} + ts, leg = None, None + for ln in (ROOT / "logs" / "cron_book.log").read_text(errors="replace").splitlines(): + m = RE_HDR.match(ln) + if m: + ts = pd.Timestamp(m.group(1)); leg = None + continue + m = RE_LEG.match(ln) + if m: + leg = m.group(1) + continue + m = RE_ORD.match(ln) + if m and leg and ts is not None and ts >= settl: + b, s_ = out.get(leg, (0, 0)) + out[leg] = (b + (m.group(1) == "BUY"), s_ + (m.group(1) == "SELL")) + return out + + +def sezione5(rate: dict) -> None: + sez("5) CONTROLLO SUL CONTO VERO — l'unico che non passa da un'assunzione") + df = leggi_posizioni_live() + if not len(df): + print(" logs/cron_book.log non leggibile o formato cambiato -> NON MISURATO.") + return + print(f" {len(df):,} osservazioni orarie di posizione ricostruite dal log del cron,") + print(f" {df['ts'].min()} -> {df['ts'].max()}") + print(" ⚠️ `data/live/book_executions.jsonl` ha `ts_utc` a 00:00:00 su 18/18 righe (difetto") + print(" gia' noto, ondata 22/08 §5): l'ora vera della posizione sta SOLO nel log del cron.") + + def paid(g: pd.DataFrame, a: str) -> float: + t = to_ms(pd.DatetimeIndex(g["ts"])) + return float((g["pos"].values * (cumF(rate[a], t + MSH) - cumF(rate[a], t))).sum()) + + tot = {} + for a in ASSETS: + g = df[df["asset"] == a].sort_values("ts") + tot[a] = paid(g, a) + print(f" {a}: {len(g):>4} ore, posizione media ${g['pos'].mean():>7.2f} " + f"(a mercato {float((g['pos'].values != 0).mean()):.0%}, max ${g['pos'].max():.0f})" + f" -> funding modellato ${tot[a]:+.4f}") + print(f" TOTALE funding modellato da quando il libro esegue: ${sum(tot.values()):+.4f}") + print(" (a $635 di conto e posizioni da $40-190: e' un numero da CENTESIMI. La potenza del") + print(" controllo non viene dalla taglia, viene dal fatto che sia CONFRONTABILE.)") + + print("\n Cio' che il conto RIPORTA (sola lettura via cerbero-mcp, nessun ordine):") + try: + from src.live.deribit import DeribitRead + ps = DeribitRead().positions("USDC") + except Exception as e: + print(f" conto non leggibile ({type(e).__name__}) -> confronto NON MISURATO.") + return + rp = {} + for p_ in ps: + inst = str(p_.get("instrument", "")) + a = "BTC" if inst.startswith("BTC") else "ETH" + rp[a] = float(p_.get("realized_pnl", 0.0)) + print(f" {inst:<22} size ${float(p_.get('size', 0)):>8.2f} " + f"realized_pnl {rp[a]:>+9.6f} unrealized {float(p_.get('unrealized_pnl', 0)):>+9.6f}") + + print("\n QUALE finestra? Deribit REGOLA i perpetual una volta al giorno alle 08:00 UTC.") + print(" Se `realized_pnl` e' il realizzato di SESSIONE, dev'essere il funding dall'ultimo") + print(" settlement — e allora la finestra giusta si riconosce perche' le altre NON tornano:") + now = pd.Timestamp.now(tz="UTC") + settl = now.normalize() + pd.Timedelta(hours=8) + if settl > now: + settl -= pd.Timedelta("1D") + fin = {a: df[df["asset"] == a].sort_values("ts") for a in ASSETS} + apert = {} + for a in ASSETS: + g = fin[a].reset_index(drop=True) + z = np.where(g["pos"].values == 0)[0] + apert[a] = g["ts"].iloc[int(z[-1])] if len(z) else g["ts"].iloc[0] + print(f" {'finestra':<34} {'BTC modello':>13} {'BTC conto':>11} {'err':>8} | " + f"{'ETH modello':>13} {'ETH conto':>11} {'err':>8}") + for lbl, lo in ((f"dal settlement {settl:%m-%d %H:%M} UTC", {a: settl for a in ASSETS}), + ("dall'apertura della posizione", apert), + ("ultime 24h", {a: now - pd.Timedelta("24h") for a in ASSETS}), + ("da quando il libro esegue", {a: fin[a]["ts"].min() for a in ASSETS})): + cells = [] + for a in ASSETS: + g = fin[a][pd.DatetimeIndex(fin[a]["ts"]) >= lo[a]] + m = paid(g, a) + err = (m / abs(rp[a]) - 1) if rp.get(a) else float("nan") + cells.append(f"{m:>13.6f} {rp.get(a, float('nan')):>11.6f} {err:>+7.1%}") + print(f" {lbl:<34} {cells[0]} | {cells[1]}") + + # (1) va MISURATO sugli ORDINI VERI, non sulle variazioni del nozionale. + # ⚠️ ERRORE CATTURATO IN SESSIONE: la prima stesura contava `diff(pos) < 0` come + # "vendite" e ne trovava 3 per asset. NON erano vendite: `pos` e' il valore in USD di + # una posizione a taglia costante, e scende da solo quando il mark scende. Su un + # perpetual lineare il nozionale si muove SEMPRE, anche a mano ferma. + ordini = ordini_sessione(settl) + print("\n 📌 IDENTIFICAZIONE, e non e' un accordo di grandezza — e' un'ELIMINAZIONE:") + print(f" (1) ordini realmente inviati nella sessione (righe '-> BUY/SELL' del log, " + f"dalle {settl:%H:%M} UTC): " + + " · ".join(f"{a} {ordini.get(a, (0, 0))[0]} buy / {ordini.get(a, (0, 0))[1]} sell" + for a in ASSETS)) + if sum(v[1] for v in ordini.values()) == 0: + print(" ZERO vendite -> nessuna chiusura, nemmeno parziale -> il P&L di TRADING") + print(" realizzato di sessione e' ESATTAMENTE ZERO, e qualunque valore non nullo") + print(" di `realized_pnl` non puo' essere P&L di trading.") + else: + print(" ⚠️ Ci sono state VENDITE: parte di `realized_pnl` puo' essere P&L di") + print(" trading e l'eliminazione (1) NON vale in questa sessione. Restano (2)(3).") + print(" (2) il segno e' NEGATIVO su entrambe le gambe, che sono entrambe LONG: un long") + print(" in contango PAGA. E' il segno che il funding deve avere, non un caso.") + print(" (3) la finestra 'dal settlement' torna; 'dall'apertura' sbaglia di ~2-3x e") + print(" '24h' di ~15%. Il controllo ha POTENZA: le finestre sbagliate si vedono.") + print(" -> `realized_pnl` del proxy = realizzato di SESSIONE Deribit, che su una") + print(" posizione non chiusa E' il funding. Il modello di questo script e' quindi") + print(" VALIDATO contro il conto reale, non solo contro se stesso.") + print(" ⚠️ IL RESIDUO, misurato invece che raccontato. Il modello SOTTOSTIMA su entrambe le") + print(" gambe. Espresso in ORE-equivalenti all'ultimo tasso orario osservato:") + for a in ASSETS: + g = fin[a][pd.DatetimeIndex(fin[a]["ts"]) >= settl] + res = abs(rp.get(a, 0.0)) - paid(g, a) + ult = float(rate[a]["series"].iloc[-1]) * float(g["pos"].iloc[-1]) + print(f" {a}: residuo ${res:+.6f} = {res/ult if ult else float('nan'):+.2f} ore " + f"di funding all'ultimo tasso (${ult:.6f}/ora)") + print(" Meccanismo candidato e coerente col segno su 2/2: il conto accumula in CONTINUO,") + print(" l'ultimo record orario e' delle 22:00 e la lettura e' delle 22:1x -> i minuti") + print(" dopo l'ultimo record non sono modellati. NON e' verificato che spieghi TUTTO il") + print(" residuo di BTC: si riporta la taglia, non una storia.") + print(" ⚠️ Limite: e' UNA sessione, su due posizioni. Non e' una serie storica di") + print(" pagamenti di funding — quella il conto non la espone via questo proxy.") + + +# =========================================================================== +# 6) COSA NON E' CATTURATO +# =========================================================================== +def sezione6(rate: dict) -> None: + sez("6) COSA NON E' CATTURATO — dichiarato e, dove si puo', quantificato") + + print(" (a) L'approssimazione 'esposizione MEDIA x tasso MEDIO' contro l'accrual ESATTO.") + print(" Su TP01 non c'e' differenza (posizione continua, accrual giornaliero esatto).") + print(" Su SKH01, che entra ed esce, la differenza si MISURA:") + print(f" {'asset':>6} {'accrual esatto/anno':>20} {'media x media/anno':>20} " + f"{'errore':>9} {'errore rel.':>12}") + for a in ASSETS: + _, tr = skh_daily(a, 0, "hourly", rate) + span_h = (tr[-1]["t_out"] - tr[0]["t_in"]) / MSH + yrs = span_h / (24 * 365.25) + esatto = float(np.sum([t["f"] for t in tr])) / yrs + # approssimazione: (ore a mercato x direzione media) x tasso medio del periodo + hrs = np.array([t["hours"] for t in tr]); dirs = np.array([t["dir"] for t in tr]) + rt = rate[a]["series"] + rt = rt[(rt.index >= pd.Timestamp(tr[0]["t_in"], unit="ms", tz="UTC")) & + (rt.index <= pd.Timestamp(tr[-1]["t_out"], unit="ms", tz="UTC"))] + approx = float((hrs * dirs).sum() * rt.mean()) / yrs + err = esatto - approx + print(f" {a:>6} {esatto:>19.4%} {approx:>19.4%} {err:>+8.4%} " + f"{(err/abs(esatto) if esatto else float('nan')):>11.1%}") + print(" -> l'approssimazione media-x-media NON e' innocua su uno sleeve che sceglie") + print(" QUANDO stare a mercato: e' la stessa forma dell'errore che il progetto ha") + print(" gia' pagato altrove (una media appaiata non e' la media delle medie).") + print(" Qui il numero pubblicato usa l'ACCRUAL ESATTO per trade.") + + sw = min(rate[a]["switch"] for a in ASSETS) + b0 = pd.Timestamp("2019-03-14", tz="UTC") + tot = (rate["BTC"]["series"].index.max() - b0).days + print(f"\n (b) Il PROXY inverse prima del {sw.date()} ({(sw-b0).days/tot:.0%} del campione).") + print(" L'accordo misurato") + print(" in sezione 0 e' la banda dell'errore; la sezione 3 riporta anche la finestra") + print(" SOLO-LINEARE, dove il proxy non entra.") + + print("\n (c) LA DISCRETEZZA. Deribit accumula il funding in CONTINUO (interest_1h e' gia'") + print(" il realizzato dell'ora): su questo venue non c'e' il problema 'posizione") + print(" all'istante del pagamento' che hanno i venue a 8h. Cio' che resta discreto e'") + print(" l'istante di USCITA di SKH01 dentro la barra 230m: il modello lo pone alla") + print(" CHIUSURA della barra di trigger (limite SUPERIORE del funding pagato).") + print(" Banda misurata sostituendo l'apertura della barra di trigger:") + for a in ASSETS: + _, tr = skh_daily(a, 0, "hourly", rate) + span_h = (tr[-1]["t_out"] - tr[0]["t_in"]) / MSH + yrs = span_h / (24 * 365.25) + hi = float(np.sum([t["f"] for t in tr])) / yrs + lo_ = float(np.sum([ + (cumF(rate[a], t["t_out"] - r02.MS_LTF) - cumF(rate[a], t["t_in"])) * t["dir"] + for t in tr])) / yrs + print(f" {a}: [{lo_:.4%}, {hi:.4%}] /anno di sleeve (ampiezza {hi-lo_:.4%})") + + print("\n (d) NON e' modellato il funding sul collaterale (il conto e' USDC, il margine non") + print(" rende) ne' il costo di prestito: non esistono su questo percorso.") + print(" (e) Il libro di RICERCA a 5 sleeve non e' ricalcolato qui: VRP01 e' opzioni (niente") + print(" funding), GTAA01 e' azioni (niente funding, e non e' deployabile), XS01 e XSR01") + print(" sono su Hyperliquid e il loro funding e' un'ALTRA misura, market-neutral per") + print(" costruzione (long e short si compensano solo se il funding e' uniforme).") + + +# =========================================================================== +def main() -> None: + print("=" * 100) + print("r0822d — IL FUNDING DEI PERPETUAL: il costo che il progetto non ha mai prezzato") + print(f"branch research/wave-0822 · {pd.Timestamp.now(tz='UTC')} · rete={'SI' if USE_NET else 'NO'}") + print("=" * 100) + + d0 = sezione0() + rate = build_rate(d0["F"]) + print(f"\n serie oraria unificata per asset (linear dove esiste, inverse prima):") + for a in ASSETS: + print(f" {a}: {len(rate[a]['ts']):,} ore, switch inverse->linear il " + f"{rate[a]['switch'].date()}") + + verifica_scala(rate, d0["F"]) + + print(f"\n{clock()} sezione 1...") + sezione1(rate) + print(f"\n{clock()} sezione 2...") + d2 = sezione2(rate) + print(f"\n{clock()} sezione 3...") + d3 = sezione3(rate) + print(f"\n{clock()} sezione 4...") + sezione4a(d3) + sezione4b(rate, r02.OFFSETS) + sezione4c(rate) + sezione4d(rate) + print(f"\n{clock()} sezione 5...") + sezione5(rate) + sezione6(rate) + + sez("GATE — esito di ciascuno, e quelli non girati DICHIARATI") + A0, B0 = book_75_25(None), book_75_25(rate) + ns = int(sum(len(skh_daily(a, 0, "hourly", rate)[1]) for a in ASSETS)) + npos = int(sum(np.sum(np.array([t["gross"] for t in skh_daily(a, 0, "hourly", rate)[1]]) <= 0) + for a in ASSETS)) + ip = al.implausible_sharpe(B0, n_trades=ns, n_losing_trades=npos) + print(f" implausible_sharpe : implausible={ip['implausible']} " + f"(Sharpe {ip['sharpe']:.2f}, maxDD {ip['maxdd']:.1%}, Calmar {ip['calmar']:.2f}, " + f"barre attive in perdita {ip['loss_frac']:.1%}) -> {ip['reasons'] or 'nessuna firma'}") + print(" replica bit-exact : PASSATA 5/5 (sez. 1) — nessun delta stampato prima.") + print(" controllo di scala tempi: PASSATO, con potenza verificata (sez. 0).") + print(" conto reale : PASSATO — modello vs `realized_pnl` di sessione, sez. 5.") + print(" anchor (banda d'ancora) : GIRATO sul peso — 23 offset, mediana delle differenze") + print(" APPAIATE (sez. 4b). Le altre tabelle sono all'ancora") + print(" canonica: il funding e' un COSTO deterministico, non una") + print(" selezione, e il suo delta e' negativo a 23/23 offset.") + print(" marginal_vs_tp01 / DSR / study_family_honest / de-levering null / causality:") + print(" NON GIRATI, e non per budget: qui non c'e' nessun") + print(" CANDIDATO. Non si propone una strategia, si toglie un") + print(" costo che mancava a quelle esistenti. Non c'e' una") + print(" griglia su cui barare e non c'e' un segnale di cui") + print(" verificare la causalita' (il funding di un'ora si") + print(" applica alla posizione tenuta in quell'ora).") + print(" weights_tilt_null : NON GIRATO perche' non serve — l'argmax del peso NON si") + print(" sposta (sez. 4b), quindi non c'e' nessun cambio di pesi") + print(" da sottoporre al gate.") + + sez("SINTESI") + a, b = d3["LIBRO 75/25 (path live)"] + print(f" LIBRO 75/25 path live: drift {a['drift']:.2%} -> {b['drift']:.2%} " + f"({b['drift']-a['drift']:+.2%}/anno)") + print(f" Sharpe FULL {a['full']:.3f} -> {b['full']:.3f}, " + f"HOLD {a['hold']:.3f} -> {b['hold']:.3f}, maxDD {a['dd']:.1%} -> {b['dd']:.1%}") + print(f" quota del drift bruciata dal funding: {(a['drift']-b['drift'])/a['drift']:.1%}") + print(f" decomposizione (sez.2): {d2['book_drag_decomp']:.3%}/anno") + rec = pd.Timestamp("2025-01-01", tz="UTC") + A_, B_ = d3["_pairs"]["LIBRO 75/25 (path live)"] + dr = (float(A_[A_.index >= rec].mean()) - float(B_[B_.index >= rec].mean())) * 365.25 + lo_ = min(rate[x]["switch"] for x in ASSETS) + A2, B2 = A_[A_.index >= lo_], B_[B_.index >= lo_] + dl = (float(A2.mean()) - float(B2.mean())) * 365.25 + print(f" ... {dl:.2%}/anno sulla sola finestra dello strumento VERO (>= {lo_.date()}, senza") + print(f" proxy), e {dr:.2%}/anno sul solo 2025+. IL NUMERO VA CITATO CON LA FINESTRA:") + print(f" la media di 7 anni non e' l'aspettativa di domani.") + print("\n COSA CAMBIA e COSA NO:") + print(" CAMBIA · il muro di capitale e la rendita perpetua (+17-18% di muro: la rendita") + print(" vive sul DRIFT, e il funding e' una tassa sul drift)") + print(" · la traiettoria da $600 (a EUR250/mese, P(entro 20a) 90% -> 64%)") + print(" · il soffitto direzionale BTC/ETH: ~1,31 -> ~1,15 netto funding") + print(" NON CAMBIA · il peso 75/25 (argmax invariato a 23/23 offset)") + print(" · il gradino di leva 1,00 -> 1,25x (k* 13,7 -> 12,3, guadagno -12%)") + print(" · il maxDD (+0,3pp: e' una tassa sul drift, non sulla coda)") + print(" · l'ordine delle leve del piano: versare resta la prima di tutte") + + print("\n GRIGLIA DICHIARATA (contata al RIALZO):") + print(" 4 strumenti scaricati x ~55-90 richieste = ~290 chiamate al venue") + print(" 3 ipotesi sulla convenzione temporale · 2 lenti (merged / solo-lineare)") + print(" 5 serie x 2 (con/senza funding) = 10 valutazioni in sez. 3, + 8 anni di split") + print(" 8 pesi x 23 offset x 2 = 368 valutazioni di blend in sez. 4b") + print(" 4 muri x 26 passi di bisezione x 2.000 path MC + 2 x 5 livelli di traiettoria") + print(" 2 (con/senza) x 800 valori di k in sez. 4d · 4 finestre x 2 gambe in sez. 5") + print(" NESSUNA di queste e' una SELEZIONE: non c'e' una cella scelta guardando il") + print(" risultato, quindi non c'e' un deflated-Sharpe da calcolare. La griglia ridotta") + print(" per budget e': i muri girano a 2.000 path invece di 4.000 (risoluzione ~1%,") + print(" dichiarata), e le tabelle non-4b sono all'ancora canonica.") + print(f"\n{clock()} fine.") + + +if __name__ == "__main__": + main() From fc0c0a29238041535008ab5ea077f8d50922aafc Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 22:43:56 +0000 Subject: [PATCH 047/114] =?UTF-8?q?CLAUDE.md:=20il=20funding=20misurato=20?= =?UTF-8?q?=E2=80=94=202,16%/anno,=20muri=20+17,5%,=20e=20l'avvertenza=20c?= =?UTF-8?q?he=20fisco=20e=20funding=20si=20sommano=20senza=20che=20nessuna?= =?UTF-8?q?=20tabella=20li=20contenga=20entrambi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 44 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9a48cd3..256ec91 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2417,11 +2417,45 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis 🚨 **(5) IL FUNDING DEI PERPETUAL NON E' MODELLATO IN NESSUN BACKTEST — e tocca ogni numero del progetto.** ✅ **Verificato:** **zero occorrenze** di `funding` in `src/backtest/harness.py`, `src/strategies/trend_portfolio.py`, `src/portfolio/{sleeves,portfolio}.py` **e in tutto - `src/live/`**. Proxy Hyperliquid 14,8%/anno × esposizione long media di TP01 (0,103·E) = - **~1,53%/anno che il libro paga davvero** = **il 9% del drift de-luckato**, cioe' **lo stesso ordine - della correzione d'ancora ×0,89** che il progetto ha misurato con cura. **Il progetto ha prezzato - fee, slippage, min-order, pavimento IB, haircut, fortuna d'ancora, degrado d'esecuzione, look-ahead, - split, rischio di venue e fisco — e mai il costo di tenere una posizione aperta su un perpetual.** + `src/live/`**. **Il progetto ha prezzato fee, slippage, min-order, pavimento IB, haircut, fortuna + d'ancora, degrado d'esecuzione, look-ahead, split, rischio di venue e fisco — e mai il costo di + TENERE APERTA una posizione su un perpetual.** + ✅ **MISURATO la notte stessa** (`r0822d_funding.py`, 1.212 righe, replica bit-exact 5/5, §35 del + registro): **il libro paga −2,16%/anno di DRIFT** (2019-2026) · **−1,39%** sulla sola finestra dello + strumento vero (≥2022-03) · **−0,55%** sul 2025+. **Il costo si cita con la finestra**, e il 2022 e + il 2026 costano ~0 **perche' il libro era FLAT**, non perche' il funding fosse sparito. + 🚨 **Il meccanismo, ed e' trasferibile a ogni futuro candidato su perpetual: esposizione e funding + sono POSITIVAMENTE CORRELATI** — il tasso pagato davvero e' **1,86-2,55×** l'incondizionato per TP01 + e **2,41-3,44×** per SKH01, quindi **il prodotto ingenuo "esposizione media × tasso medio" + sottostima di ~2×**. + ❌ **Ipotesi da cui il filone era partito, FALSIFICATA: "SKH01 paga meno perche' sta a mercato il 12% + del tempo"** → paga **2,24%/anno contro 2,02%** di TP01, perche' quando e' a mercato sta a nozionale + **1,0×** (TP01 vol-targeted sta a **0,14×**) **ed entra sui breakout, cioe' quando il funding e' + caro**. **Il peso 75/25 NON si muove** (delta negativo a 23/23 offset per ogni w). + ⚠️ **Il proxy Hyperliquid era 2,1-2,2× il venue vero** (14,8% contro 6,8-7,1% sulle stesse ore): + **due errori opposti — tasso troppo alto, esposizione condizionale troppo bassa — che quasi si + compensavano.** *Compensarsi non e' misurare.* + 🚨 **NUMERI PUBBLICATI CHE CAMBIANO:** rendita perpetua **10,73% → 9,14%**; **muro $276,6k → $325,0k + (+17,5%)**; traiettoria da $600 a €250/mese **16,4a P(20a) 90% → 18,7a P(20a) 64%**; **soffitto + direzionale ~1,31 → ~1,15**. **NON cambiano:** peso 75/25, gradino di leva (k* 13,7→12,3), **maxDD + (+0,3pp)**, ordine delle leve del piano. + 📌 **E' una tassa sul DRIFT, non sulla coda** — per questo colpisce i **muri** molto piu' dello + Sharpe, ed e' lo stesso ordine di grandezza della correzione d'ancora ×0,89. + ⚠️ **QUESTA CORREZIONE E QUELLA DEL FISCO SI SOMMANO, E NESSUNA TABELLA PUBBLICATA LE CONTIENE + ENTRAMBE.** La riga "€250/mese" vale P(20a) **92% al lordo** · **52% col fisco** (07/08) · **64% col + funding** (22/08): **le due lenti sono state applicate separatamente allo stesso numero lordo**, e il + risultato congiunto sara' **peggiore del 52%**. **La tabella con entrambe non esiste ancora.** + ✅ **Validato contro il conto vero per ELIMINAZIONE, non per grandezza:** modello vs `realized_pnl` + di sessione **ETH −0,2% / BTC −7,7%** (le finestre sbagliate sbagliano di 2-4×), e l'identificazione + regge perche' nella sessione ci sono stati **0 ordini** → il P&L di trading e' esattamente zero, + quindi quel campo non puo' essere P&L di trading. Convenzione `interest_8h` **verificata sui dati** + (mediana |dif| 2,2e-08 contro 1,1e-06). Funding realmente pagato dall'arming: **$0,22**. + ⚠️ **BUG in famiglia gia' nota, catturato da un controllo positivo e non a occhio:** + `DatetimeIndex.astype("int64")` su tre serie con risoluzioni **diverse** (`[ms]`/`[us]`/`[s]`) dava + **0,000%/anno su SKH01** e **+1,229%/GIORNO su TP01** — **nessuna eccezione, nessun NaN, due numeri + plausibili in due punti diversi**. Stessa famiglia del `DatetimeIndex.view("int64")` codificato il + 01/07. **Su un perpetuale lineare, inoltre, `diff(nozionale) < 0` NON e' una vendita: il nozionale + scende da solo quando scende il mark.** **REGOLE NUOVE:** (a) **una data in un gate va giustificata col COSTO della misura, non con la sua difficolta' percepita** — due gate d'attesa eliminati in una sera perche' nessuno aveva contato i secondi; (b) **una verifica esterna a €0 va fatta PRIMA di qualunque misura che ne dipenda**, e si From 758bcce97f6bd716bedf08538d130d4a022e8b25 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 23:13:05 +0000 Subject: [PATCH 048/114] =?UTF-8?q?research(wave-0822):=20PIANO-VERO=20?= =?UTF-8?q?=E2=80=94=20la=20tabella=20congiunta=20fisco+funding;=20EUR250/?= =?UTF-8?q?mese=20passa=20da=20P(20a)=2092%=20a=2014-26%,=20e=20le=20corre?= =?UTF-8?q?zioni=20si=20sommano=20invece=20di=20compensarsi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 68 +++ scripts/research/r0822d_piano_vero.py | 695 ++++++++++++++++++++++++++ 2 files changed, 763 insertions(+) create mode 100644 scripts/research/r0822d_piano_vero.py diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 4d1b099..00ebac5 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -701,6 +701,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 33 | WORST-DAY | il peggior giorno del libro, MISURATO: il gradino di leva e' autorizzabile? | **LEAD** — l'aritmetica passa, **il knob non esiste** | | 34 | CC01-2022 | il carry riaperto sui dati che gli mancavano per costruzione | **RESTA SCARTATO** — per la ragione giusta, ma non quella pubblicata | | 35 | FUNDING | il costo mai prezzato di TENERE una posizione aperta | **LEAD** — **−2,16%/anno di drift**, muri **+17,5%** | +| 36 | PIANO-VERO | la tabella congiunta fisco+funding, che non esisteva | 🚨 **€250/m: P(20a) 92% → 14-26%** | | 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 | @@ -1712,3 +1713,70 @@ molto piu' basso, il −2,16% scenderebbe verso il **−1,39%** della finestra p **VERDETTO: LEAD** (misura di politica, non strategia). **`IL FUNDING COSTA 2,16%/ANNO NETTO AL LIBRO` — quanto la fortuna d'ancora ×0,89 che il progetto ha misurato con enorme cura, e sposta i muri di capitale del +17,5%.** + +--- + +## 36 — PIANO-VERO (la tabella congiunta) — **la risposta alla domanda del progetto** + +`scripts/research/r0822d_piano_vero.py` (145 s). **Nessuna selezione** → niente DSR. Muri a 4.000 +path (la taglia dei pubblicati), traiettorie a 3.000; **risoluzione MC misurata su 3 semi: 0,7% sul +muro, 1,5 €/m sul versamento** — dichiarata. + +**Il buco:** le due correzioni misurate al piano — **fisco d'accumulo** (07/08) e **funding** (22/08) — +erano state applicate **separatamente allo stesso numero lordo**, e la tabella con entrambe non +esisteva. L'agente ha dichiarato **prima di misurare** di aspettarsi un congiunto **meno severo della +somma** (meno drift → meno plusvalenza → meno imposta). + +✅ **Replica 6/6 prima di pubblicare, due esatte AL DOLLARO:** sul vintage **2026-08-07** lo stesso +codice rida' **$272.061** (lordo) e **$258.338** (netto fisco); riproduce anche funding +−2,1597/−1,3931/−0,5508 %/anno, perpetua 10,73→9,14%, muro +17,5%, traiettoria 16,4a/90% → 18,7a/64%. +⚠️ **Sulla serie di OGGI la stessa riga da' $278.033 (+2,2%)** — 15 giorni di dati in piu', `data/raw/` +gitignored: **stessa lezione GTAA del 07/08** → ogni confronto fra lenti e' fatto **sullo stesso +vintage**. + +| lente (de-luck ×0,89) | drift | perpetua | muro | **€250/m da $635: P(20a)** | +|---|---|---|---|---| +| L0 LORDO (26/07) | 17,11% | 10,68% | $278k | **90%** *(pubbl. 92%)* | +| L1 +FISCO (07/08) | 17,11% | 7,52% | $264k | **49%** *(pubbl. 52%)* | +| L2 +FUNDING (22/08) | 15,19% | 9,06% | $328k | **63%** *(pubbl. 64%)* | +| 🚨 **L3 CONGIUNTA** | **15,19%** | **6,35%** | **$313k** | **14%** ← il numero che mancava | +| L3b congiunta, funding "strumento vero" −1,39%/a | 15,87% | 6,72% | $296k | **26%** | + +🚨 **LE DUE CORREZIONI NON SI COMPENSANO: SI SOMMANO — e nel versamento necessario il congiunto e' ++10% PEGGIORE della loro somma.** +📌 **L'attesa dell'agente era sbagliata dove conta, e il perche' e' il risultato trasferibile:** il +meccanismo "meno drift → meno imposta" **esiste** (+9,97% di capitale recuperato; il fisco toglie il +50,9% a funding OFF e il 46,0% a funding ON), **ma la funzione versamento→probabilita' e' CONVESSA e +ne ribalta il segno**. Interazione misurata **in tre monete**: **+9,97% sul capitale**, **−$815 sul +muro** (0,26%, *sotto* la risoluzione MC), **+26 €/mese sul versamento (+10%, segno stabile su 3 +semi**: +26/+29/+22). +**REGOLA: non contare su una compensazione fra correzioni misurate separatamente — la si misura, e in +piu' di una MONETA, perche' un'interazione piccola cambia segno con la moneta.** + +**Quanto versare (L3, da $635, nessun lump):** +| orizzonte | P=50% | P=75% | **P=90%** | totale versato @P=90% | **quota del bersaglio** | +|---|---|---|---|---|---| +| **10 anni** | €1.323 | €1.534 | **€1.733** | **$229.189** | **73%** | +| 15 anni | €656 | €784 | **€920** | $183.129 | 58% | +| 20 anni | €360 | €445 | **€541** | $143.804 | 46% | +Il **solo funding** aggiunge **1,27× / 1,33× / 1,40×** sopra la richiesta della lente fisco-only. + +**Rendita €/g mediana a 20 anni (L3):** €250/m → **31,85 €/g, P(≥50/g) 8,8%** · €500/m → 63,05, +**P 77,1%** · €800/m → 100,50, **P 99,2%**. +📌 **Il LUMP, che nelle tabelle nette non era mai entrato:** **€10.000 oggi** + €250/m porta P(20a) da +**14% a 45%**; €5.000 la porta a 30%. + +**Trappole evitate per costruzione** (tutte gia' pagate da questo progetto): non-arrivo = **+∞, mai +−1**; **mediana condizionata mai stampata senza la sua P accanto**; risoluzione MC misurata e +dichiarata. **Rischio di venue NON incluso di proposito**: e' **rovina, non costo** (a p=5% il mediano +e' zero comunque) — dichiarato. + +**VERDETTO: LEAD** (misura di politica). +``` +CON TUTTE LE CORREZIONI, €250/MESE DA $635: 23,4 anni (mediana incondizionata; + 22,1 condizionata all'arrivo), P(20a) 14% [banda funding "strumento vero": 22,2 anni, P 26%] +PER €50/GIORNO IN 10 ANNI SERVONO: €1.733/mese (P=90%), totale versato $229.189 +``` +📌 **A 10 anni si versano $229k per arrivare a ~$313k: il rendimento fa il 27%, i bonifici il 73%.** +E' la forma piu' netta finora della lezione gia' scritta: **a orizzonte corto non fai lavorare la +strategia, COMPRI il capitale coi bonifici.** diff --git a/scripts/research/r0822d_piano_vero.py b/scripts/research/r0822d_piano_vero.py new file mode 100644 index 0000000..70a38dd --- /dev/null +++ b/scripts/research/r0822d_piano_vero.py @@ -0,0 +1,695 @@ +#!/usr/bin/env python +"""r0822d_piano_vero.py — LA TABELLA DEL PIANO CON TUTTE LE CORREZIONI DENTRO, UNA VOLTA SOLA. + +IL BUCO. L'obiettivo dichiarato del progetto e' 50 EUR/giorno e la domanda operativa e' «quanto +ci vuole, versando quanto». La tabella pubblicata che risponde e' stata corretta DUE VOLTE, ogni +volta partendo dallo stesso numero LORDO e mai dall'altra correzione: + + riga «250 EUR/mese», P(traguardo entro 20 anni) + * 92% al LORDO (26/07, fattore d'ancora x0.89) + * 52% col FISCO d'accumulo (07/08, r0807_piano_netto) + * 64% col FUNDING (22/08, r0822d_funding) + * ??? con ENTRAMBI <- non esiste + +Le due lenti sono state applicate SEPARATAMENTE. Questo script produce la lente unica, coerente, +con dentro tutte le correzioni MISURATE (nessuna scelta a mano), e dice quanto ci vuole davvero. + +L'ORDINE DI APPLICAZIONE, e perche' quello. + 1. FUNDING — e' un costo sul rendimento LORDO: si paga sul nozionale, ogni ora, prima che + esista una plusvalenza. Entra DENTRO la serie dei ritorni, giorno per giorno + (`r0822d_funding.book_75_25(rate)`), non come haircut a valle. + 2. ANCORA x0.89 — e' una correzione sul DRIFT della serie (fortuna di calendario misurata, + r0726_deluck_factor). Si applica alla serie, dopo il funding, come fa + `r0822d_funding.sezione4c` — cosi' i numeri restano confrontabili con quelli pubblicati. + La convenzione alternativa (de-luckare il lordo e POI togliere il funding) e' riportata + come sensibilita': vale 0.24pp di drift. + 3. FISCO — tassa la PLUSVALENZA, a fine anno, sul percorso GIA' netto di funding. L'ordine + inverso (tassare la plusvalenza lorda e poi sottrarre il funding dal capitale gia' tassato) + farebbe pagare il 33% su un guadagno mai incassato. + Da qui l'effetto di SECONDO ORDINE che la somma delle due correzioni non contiene: meno drift + -> meno plusvalenza -> meno imposta. Il congiunto e' meno severo della somma, e di quanto e' + una MISURA (sezione 3), non un'assunzione. + +COSA NON C'E' DENTRO, DI PROPOSITO. Il rischio di venue. Non e' un costo, e' un rischio di +ROVINA: a p=5% annuo il capitale mediano a 20 anni e' ZERO per qualunque calendario di +versamenti (26/07). Sta su un asse separato e ci resta; metterlo qui trasformerebbe una tabella +di pianificazione in una tabella di sopravvivenza. + +ASSUNZIONI FISCALI, dichiarate e NON un parere fiscale (le stesse del 27/07 e del 07/08): +33% sulle plusvalenze cripto realizzate, minusvalenze in carry 4 anni, patrimoniale 0.2%/anno +sul valore. Il modello tassa la variazione ANNUA di valore = limite superiore, stretto perche' +il libro realizza quasi tutto entro l'anno. + +TRAPPOLE GIA' PAGATE DA QUESTO PROGETTO, evitate qui per costruzione: + * un NON-ARRIVO si codifica +inf, mai -1 (con -1 la mediana MIGLIORA quando la colonna + peggiora: errore commesso il 07/08). Qui si stampano DUE mediane: quella incondizionata + (non-arrivo = +inf, quindi «>25a») e quella condizionata, che non compare MAI senza la sua + probabilita' accanto. + * un Monte Carlo ha una RISOLUZIONE: misurata su 3 semi e dichiarata (sezione 2). + * la serie del libro e' su giorni di CALENDARIO (cripto, 365/anno): l'annualizzazione a 365 e' + corretta e `to_daily()` non serve. Sarebbe servito su una serie di giorni di borsa. + + uv run python scripts/research/r0822d_piano_vero.py + uv run python scripts/research/r0822d_piano_vero.py --rebuild # ignora la cache di serie +""" +from __future__ import annotations + +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +sys.path.insert(0, str(ROOT / "scripts" / "research")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +sys.path.insert(0, str(ROOT)) + +import r0725_capcurve as CC # noqa: E402 TAX_RATE, EURUSD, _boot_paths, book_series +import r0726_capwall_refresh as WR # noqa: E402 perp_and_wall, trajectory (2000 path) +import r0727_lumpsum_split as LS # noqa: E402 TARGET pubblicato +import r0727_tasse as TX # noqa: E402 PATRIMONIALE, CARRY_ANNI +import r0807_piano_netto as PN # noqa: E402 passo_fiscale, sopravvivenza, accumula + +# --------------------------------------------------------------------------- +# Costanti dichiarate +# --------------------------------------------------------------------------- +DELUCK = 0.89 # fattore d'ancora MISURATO (r0726_deluck_factor), non a occhio +START = 635.0 # il conto Deribit VERO di oggi (le tabelle pubblicate usano 600) +START_PUB = 600.0 # solo per la replica +N_WALL = 4000 # path per il muro (la taglia a cui furono calcolati i pubblicati) +N_TRAJ = 3000 # path per le traiettorie (idem, seed 725) +N_DEP = 3000 # path per la bisezione sul versamento +BLOCK = 20 +SEED_TRAJ = 725 +SEED_DEP = 20260822 +SEED_WALL = PN.SEED_MURO # 725 +ANNI_TRAJ = 25 # 20 troncherebbe la coda: convenzione di WR.trajectory +VERSAMENTI = (0, 250, 500, 800, 1_000, 2_000) +LUMPS_EUR = (0.0, 2_000.0, 5_000.0, 10_000.0) +ORIZZONTI = (10, 15, 20) +CONFIDENZE = (0.50, 0.75, 0.90) +VINTAGE_PUB = "2026-08-07" # la data in cui furono calcolati $272.061 / $258.338 +COSTO_STRUMENTO_VERO = 0.013931 # -1.39%/anno: funding misurato sulla sola finestra >= 2022-03 + +CACHE = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/pianovero") +if not CACHE.parent.parent.exists(): + CACHE = Path("/tmp/pythagoras_pianovero") +REBUILD = "--rebuild" in sys.argv +T0 = time.time() + + +def sez(t: str) -> None: + print("\n" + "=" * 100) + print(f" {t}") + print("=" * 100) + + +def clock() -> str: + return f"[{time.time() - T0:5.0f}s]" + + +# =========================================================================== +# 0) LE SERIE +# =========================================================================== +def costruisci_serie() -> pd.DataFrame: + """Le due serie giornaliere del libro live 75/25: senza e con funding. + + Nessuna re-implementazione: `r0822d_funding.book_75_25` e' la stessa lente di + `CC.book_series('hourly')` e la replica bit-exact e' verificata qui sotto. + """ + f = CACHE / "book.parquet" + if f.exists() and not REBUILD: + return pd.read_parquet(f) + # Se la cache dello storico funding c'e', si usa quella: nessuna chiamata al venue (il + # progetto gira su una VPS con soldi veri e un rate limit Deribit per-IP gia' saturato una + # volta, 29/07). Se NON c'e', si lascia che `fetch_funding` la scarichi: cosi' lo script + # gira anche da solo, su una macchina pulita. + for c in (Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/fund"), + Path("/tmp/pythagoras_funding_cache")): + if any(c.glob("*.parquet")): + sys.argv.append("--no-net") + break + import r0822d_funding as FD # noqa: E402 (USE_NET si decide all'import) + F = {} + for instr, (start, _asset, _kind) in FD.INSTR.items(): + d = FD.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") + rate = FD.build_rate(F) + A, B = FD.book_75_25(None), FD.book_75_25(rate) + out = pd.DataFrame({"nofund": A, "fund": B.reindex(A.index)}) + CACHE.mkdir(parents=True, exist_ok=True) + out.to_parquet(f) + return out + + +def deluck(r: np.ndarray) -> np.ndarray: + """Toglie dal DRIFT la fortuna d'ancora misurata. La vol non si tocca: fra le ancore era + invariata (7.80% -> 7.76%), la fortuna sta tutta nel drift (r0726_deluck_factor).""" + r = np.asarray(r, float) + return r - (1.0 - DELUCK) * r.mean() + + +# =========================================================================== +# LE LENTI +# =========================================================================== +class Lente: + """Una lente = (serie di ritorni, convenzione di prelievo, regime fiscale d'accumulo). + + `lordizza=True` -> convenzione PUBBLICATA: il prelievo si lordizza al 33% e il capitale + investito non paga mai imposte. Incoerente (r0807), tenuta per replica. + `lordizza=False` -> convenzione COERENTE: imposte ogni anno dentro il portafoglio, prelievo + gia' netto. + """ + + def __init__(self, nome: str, r: np.ndarray, lordizza: bool, nota: str = ""): + self.nome, self.r, self.lordizza, self.nota = nome, np.asarray(r, float), lordizza, nota + self.aliq = 0.0 if lordizza else CC.TAX_RATE + self.patr = 0.0 if lordizza else TX.PATRIMONIALE + self.perp = float("nan") + self.muro = float("nan") + + @property + def prelievo(self) -> float: + netto = CC.TARGET_EUR_DAY * 365 * CC.EURUSD + return netto / (1 - CC.TAX_RATE) if self.lordizza else netto + + @property + def drift(self) -> float: + return float(self.r.mean()) * 365.0 + + @property + def vol(self) -> float: + return float(self.r.std()) * 365.0 ** 0.5 + + +def muro_di(lente: Lente, n_paths: int = N_WALL, seed: int = SEED_WALL) -> tuple[float, float]: + """(rendita perpetua, capitale necessario). Perpetua = il prelievo piu' alto con + P(capitale a 20 anni >= capitale iniziale) >= 90% — definizione del 25/07, invariata. + + Bisezione identica a `PN.muro`, esposta solo per poter cambiare SEME e misurare la + risoluzione Monte Carlo. Il controllo positivo (a seme 725 deve dare esattamente `PN.muro`) + e' in sezione 2. + """ + lo, hi = 0.0, 0.40 + for _ in range(13): + mid = (lo + hi) / 2 + s = PN.sopravvivenza(lente.r, 1e6, 1e6 * mid, 20, lente.aliq, lente.patr, + n_paths=n_paths, seed=seed) + if s["p10_end"] >= 1e6: + lo = mid + else: + hi = mid + return lo, (lente.prelievo / lo if lo > 0.002 else float("inf")) + + +# =========================================================================== +# lettura onesta di un vettore di primi passaggi +# =========================================================================== +def leggi(colpito: np.ndarray, anni_sim: int = ANNI_TRAJ) -> dict: + """Mediana INCONDIZIONATA (non-arrivo = +inf) + mediana condizionata + P(20 anni). + + ⚠️ Il non-arrivo e' +inf, MAI -1: con -1 la mediana scende quanto peggio va la colonna + (errore commesso il 07/08 e corretto lo stesso giorno). E la mediana condizionata non si + stampa mai da sola: descrive solo chi arriva. + """ + y = np.where(colpito >= 0, colpito / 365.0, np.inf) + arr = y[np.isfinite(y)] + return dict( + med=float(np.median(y)), # +inf se meno di meta' arriva + med_cond=float(np.median(arr)) if len(arr) > 100 else float("nan"), + p20=float((y <= 20.0).mean()), + p_arr=float(np.isfinite(y).mean()), + anni_sim=anni_sim) + + +def fmt_med(d: dict) -> str: + m = d["med"] + return f">{d['anni_sim']}a" if not np.isfinite(m) else f"{m:.1f}a" + + +def fmt_cond(d: dict) -> str: + c = d["med_cond"] + return "—" if np.isnan(c) else f"{c:.1f}a" + + +# =========================================================================== +# bisezione sul versamento (start parametrico: le tabelle pubblicate partono da $600) +# =========================================================================== +def dep_per_conf(paths: np.ndarray, lente: Lente, conf: float, start: float, + lump_eur: float = 0.0) -> tuple[float, float]: + """(versamento mensile EUR minimo per P(traguardo entro l'orizzonte) >= conf, totale versato). + + I path si estraggono UNA volta e si riusano a ogni passo: con campioni diversi per passo la + funzione non sarebbe monotona e la bisezione oscillerebbe (lezione WR.solve_deposit). + """ + def p(dep: float) -> float: + c = PN.accumula(paths, dep, lente.muro, lente.aliq, lente.patr, + start=start, lump_eur=lump_eur)["colpito"] + return float((c >= 0).mean()) + + lo, hi = 0.0, 6_000.0 + if p(hi) < conf: + return float("inf"), float("nan") + for _ in range(12): + mid = (lo + hi) / 2 + if p(mid) >= conf: + hi = mid + else: + lo = mid + versato = PN.accumula(paths, hi, lente.muro, lente.aliq, lente.patr, + start=start, lump_eur=lump_eur)["versato"] + return hi, versato + + +# =========================================================================== +def main() -> None: + print("=" * 100) + print(" r0822d — IL PIANO VERO: fisco d'accumulo E funding nella STESSA tabella") + print("=" * 100) + + B = costruisci_serie() + idx = pd.DatetimeIndex(B.index) + A_raw, F_raw = B["nofund"].values.astype(float), B["fund"].values.astype(float) + print(f"\n serie giornaliera del libro LIVE (TP01 75 + SKH01 25, lente 'hourly', path live)") + print(f" {len(B)} giorni, dal {idx.min().date()} al {idx.max().date()} {clock()}") + print(f" drift lordo {A_raw.mean() * 365:.3%} · drift netto funding {F_raw.mean() * 365:.3%}") + print(" ⚠️ giorni di CALENDARIO (cripto): annualizzazione a 365 corretta, `to_daily()` non") + print(" serve qui — servirebbe su una serie di giorni di borsa (lezione 25/07 su GTAA01).") + + # ===================================================================== + sez("1) CONTROLLO DI REPLICA — prima di pubblicare un numero nuovo, riprodurre i vecchi") + print("\n Regola del progetto (07/08): «prima di pubblicare un numero nuovo, far riprodurre") + print(" alla macchina quello vecchio». Cinque controlli, tutti su codice IMPORTATO.\n") + ok = {} + + # (R1) la serie senza funding e' bit-exact la serie del libro gia' pubblicata + C = CC.book_series("hourly") + d = float(np.max(np.abs(A_raw - C.reindex(idx).values))) + ok["R1"] = d == 0.0 + print(f" R1 serie 'senza funding' vs CC.book_series('hourly') max|dif| = {d:.3e} " + f"{'BIT-EXACT' if ok['R1'] else 'FALLITA'}") + + # (R2) il costo del funding per finestra + print(f"\n R2 costo del funding per finestra (pubblicato 22/08: -2,16% / -1,39% / -0,55%)") + fin = [("piena (proxy inverse pre-2022)", None, -0.0216), + (">= 2022-03-15 (strumento vero)", "2022-03-15", -0.0139), + (">= 2025-01-01", "2025-01-01", -0.0055)] + r2 = True + for lab, lo, atteso in fin: + m = np.ones(len(B), bool) if lo is None else np.asarray(idx >= pd.Timestamp(lo, tz="UTC")) + got = -(A_raw[m].mean() - F_raw[m].mean()) * 365 + r2 &= abs(got - atteso) < 5e-4 + print(f" {lab:<34} n={m.sum():>5} misurato {got:+.4%} atteso {atteso:+.2%}") + ok["R2"] = r2 + + # (R3-R4) i muri pubblicati, sul VINTAGE su cui furono calcolati + print(f"\n R3/R4 i muri pubblicati ($272.061 lordo / $258.338 netto fisco) sul vintage") + print(f" {VINTAGE_PUB} — la data in cui furono calcolati.") + mv = np.asarray(idx <= pd.Timestamp(VINTAGE_PUB, tz="UTC")) + Lv0 = Lente("vintage LORDO", deluck(A_raw[mv]), True) + Lv1 = Lente("vintage +FISCO", deluck(A_raw[mv]), False) + Lv0.perp, Lv0.muro = PN.muro(Lv0.r, Lv0.prelievo, 0.0, 0.0) + Lv1.perp, Lv1.muro = PN.muro(Lv1.r, Lv1.prelievo, Lv1.aliq, Lv1.patr) + ok["R3"] = abs(Lv0.muro - LS.TARGET) < 1.0 + ok["R4"] = abs(Lv1.muro - 258_338.0) < 1.0 + print(f" LORDO perpetua {Lv0.perp:.4%} muro ${Lv0.muro:,.0f} atteso $272,061 " + f"{'ESATTO AL DOLLARO' if ok['R3'] else 'DIVERSO'}") + print(f" NETTO perpetua {Lv1.perp:.4%} muro ${Lv1.muro:,.0f} atteso $258,338 " + f"{'ESATTO AL DOLLARO' if ok['R4'] else 'DIVERSO'} {clock()}") + + # (R5) i numeri del funding, alla SUA taglia MC (2000 path) + print(f"\n R5 i numeri del filone funding (22/08), alla sua taglia dichiarata (2000 path):") + g = CC.TARGET_EUR_DAY * 365 * CC.EURUSD / (1 - CC.TAX_RATE) + rA, rF = deluck(A_raw), deluck(F_raw) + _, pA, wA = WR.perp_and_wall(rA, 1.0, g) + _, pF, wF = WR.perp_and_wall(rF, 1.0, g) + tA = WR.trajectory(rA, wA, n_paths=2000)[250] + tF = WR.trajectory(rF, wF, n_paths=2000)[250] + ok["R5"] = abs(wF / wA - 1 - 0.175) < 0.004 and abs(pF - 0.0914) < 3e-4 + print(f" perpetua {pA:.2%} -> {pF:.2%} (pubblicato 10,73% -> 9,14%)") + print(f" muro ${wA:,.0f} -> ${wF:,.0f} = {wF / wA - 1:+.1%} (pubblicato $276,6k -> " + f"$325,0k = +17,5%)") + print(f" traiettoria EUR250/mese {tA['p50']:.1f}a P20 {tA['p20y']:.0%} -> " + f"{tF['p50']:.1f}a P20 {tF['p20y']:.0%} (pubblicato 16,4a/90% -> 18,7a/64%) {clock()}") + + # (R6) la colonna LORDA e la colonna NETTA delle traiettorie, sul vintage, da $600 + print(f"\n R6 le due colonne di traiettoria pubblicate, sul vintage {VINTAGE_PUB}, da $600:") + rng = np.random.default_rng(SEED_TRAJ) + pv = CC._boot_paths(Lv0.r, N_TRAJ, ANNI_TRAJ * 365, BLOCK, rng) + att_l = {250: 16.3, 500: 12.4, 800: 10.0, 1_000: 9.0, 2_000: 6.0} + att_n = {250: 19.8, 500: 14.7, 800: 11.4, 1_000: 10.0, 2_000: 6.4} + print(f" {'EUR/m':>7}{'LORDO mis.':>13}{'atteso':>9}{'P20':>7}" + f"{' |':>3}{'NETTO mis.':>13}{'atteso':>9}{'P20':>7}") + r6 = True + for dep in (250, 500, 800, 1_000, 2_000): + l = leggi(PN.accumula(pv, dep, Lv0.muro, 0.0, 0.0, start=START_PUB)["colpito"]) + n = leggi(PN.accumula(pv, dep, Lv1.muro, Lv1.aliq, Lv1.patr, start=START_PUB)["colpito"]) + r6 &= abs(l["med_cond"] - att_l[dep]) < 0.25 and abs(n["med_cond"] - att_n[dep]) < 0.25 + print(f" {dep:>7}{fmt_cond(l):>13}{att_l[dep]:>9.1f}{l['p20']:>7.0%}" + f"{' |':>3}{fmt_cond(n):>13}{att_n[dep]:>9.1f}{n['p20']:>7.0%}") + ok["R6"] = r6 + del pv + print(f"\n ESITO REPLICA: {sum(ok.values())}/{len(ok)} — " + + ", ".join(f"{k}={'OK' if v else 'NO'}" for k, v in ok.items())) + if not all(ok.values()): + raise SystemExit("REPLICA INCOMPLETA: un numero nuovo sarebbe indistinguibile da un bug.") + print(""" + ⚠️ E IL FATTO CHE VA DETTO: la replica e' esatta AL DOLLARO solo sul vintage del 07/08. + Su TUTTA la serie di oggi lo stesso codice, senza una riga cambiata, da' muri diversi:""") + L0t = Lente("oggi LORDO", rA, True) + L0t.perp, L0t.muro = PN.muro(L0t.r, L0t.prelievo, 0.0, 0.0) + print(f" muro LORDO: ${Lv0.muro:,.0f} (vintage {VINTAGE_PUB}, {mv.sum()} giorni) -> " + f"${L0t.muro:,.0f} (oggi, {len(B)} giorni) = {L0t.muro / Lv0.muro - 1:+.1%}") + print(" `data/raw/` e' gitignored e il cron lo allunga ogni notte: 15 giorni in piu' di") + print(" dati spostano il muro del ~2%, cioe' PIU' della risoluzione Monte Carlo. E' la") + print(" stessa lezione del 07/08 sul gate di GTAA01 (un numero cambia senza che il codice") + print(" cambi = i dati non sono versionati). Percio' qui sotto ogni confronto fra lenti e'") + print(" fatto sullo STESSO vintage: un grado di liberta' per volta.") + + # ===================================================================== + sez("2) LE QUATTRO LENTI E IL MURO — la riga che non esisteva") + print(""" + Tutte de-luckate x0.89. Le prime due sono le lenti pubblicate; la terza e' quella del filone + funding di stanotte; la quarta e' la lente unica con dentro TUTTO.""") + Lo = { + "L0 LORDO (26/07)": Lente("L0", rA, True, "prelievo lordizzato, nessuna imposta in accumulo"), + "L1 +FISCO (07/08)": Lente("L1", rA, False, "imposte ogni anno nel portafoglio"), + "L2 +FUNDING (22/08)": Lente("L2", rF, True, "funding esatto, convenzione pubblicata"), + "L3 CONGIUNTA": Lente("L3", rF, False, "funding esatto + fisco coerente"), + } + rFb = deluck(A_raw - COSTO_STRUMENTO_VERO / 365.0) + Lo["L3b CONGIUNTA (str. vero)"] = Lente("L3b", rFb, False, + "funding -1,39%/anno (finestra >= 2022-03)") + print(f"\n {'lente':<27}{'drift':>9}{'vol':>8}{'prelievo/a':>13}{'perpetua':>10}{'muro':>13}") + for k, L in Lo.items(): + L.perp, L.muro = muro_di(L) + print(f" {k:<27}{L.drift:>9.2%}{L.vol:>8.2%}{L.prelievo:>13,.0f}" + f"{L.perp:>10.2%}{L.muro:>13,.0f}") + print(f" {clock()}") + + # controllo positivo sulla bisezione con seme + risoluzione Monte Carlo + p_ctrl, w_ctrl = PN.muro(Lo["L3 CONGIUNTA"].r, Lo["L3 CONGIUNTA"].prelievo, + CC.TAX_RATE, TX.PATRIMONIALE) + same = abs(w_ctrl - Lo["L3 CONGIUNTA"].muro) < 1e-6 + print(f"\n controllo positivo: `muro_di` a seme {SEED_WALL} == `PN.muro` importato " + f"${w_ctrl:,.0f} vs ${Lo['L3 CONGIUNTA'].muro:,.0f} {'OK' if same else 'DIVERGE'}") + semi = [Lo["L3 CONGIUNTA"].muro] + for s in (2026, 8_22): + semi.append(muro_di(Lo["L3 CONGIUNTA"], seed=s)[1]) + sp = (max(semi) - min(semi)) / np.mean(semi) + print(f" RISOLUZIONE MONTE CARLO del muro, misurata su 3 semi a {N_WALL} path: " + + " / ".join(f"${x:,.0f}" for x in semi)) + print(f" ampiezza {sp:.1%} -> si citano TRE cifre significative (${Lo['L3 CONGIUNTA'].muro/1000:.0f}k), " + f"non il dollaro. {clock()}") + + # sensibilita' sull'ordine de-luck / funding + alt = Lente("alt", deluck(A_raw) - (A_raw - F_raw).mean(), False) + alt.perp, alt.muro = muro_di(alt) + print(f"\n sensibilita' all'ORDINE de-luck/funding (convenzione alternativa: de-luckare il") + print(f" drift LORDO e poi togliere il funding): drift {alt.drift:.2%} contro " + f"{Lo['L3 CONGIUNTA'].drift:.2%}, muro ${alt.muro:,.0f} contro " + f"${Lo['L3 CONGIUNTA'].muro:,.0f} ({alt.muro / Lo['L3 CONGIUNTA'].muro - 1:+.1%}).") + print(" Sotto la risoluzione MC: la scelta di convenzione non decide niente. Si tiene quella") + print(" di `r0822d_funding.sezione4c`, per confrontabilita' con i numeri gia' pubblicati.") + + # ===================================================================== + sez("3) LE DUE CORREZIONI SI SOMMANO? — l'effetto di secondo ordine, misurato") + print(""" + ATTESA DICHIARATA PRIMA DI MISURARE: meno drift -> meno plusvalenza -> meno imposta, quindi + il congiunto dovrebbe essere MENO severo della somma delle due correzioni prese da sole. + Misurato come interazione di un fattoriale 2x2 (funding OFF/ON x fisco OFF/ON) in TRE monete, + perche' un'interazione piccola puo' cambiare SEGNO a seconda di come la si misura.""") + + # (3a) accumulo puro: il capitale a 20 anni non dipende dal muro -> isola il meccanismo + print("\n (3a) SUL CAPITALE ACCUMULATO — la domanda pura sul meccanismo.") + print(" Capitale mediano a 20 anni, EUR250/mese, partenza $%.0f. Non dipende dal muro:" % START) + print(" qui dentro c'e' SOLO l'accumulo, cioe' l'unico posto dove il meccanismo vive.") + capf = {} + for k in ("L0 LORDO (26/07)", "L1 +FISCO (07/08)", "L2 +FUNDING (22/08)", "L3 CONGIUNTA"): + L = Lo[k] + rr = np.random.default_rng(SEED_DEP) + pp = CC._boot_paths(L.r, N_DEP, 20 * 365, BLOCK, rr) + capf[k] = float(np.median(PN.accumula(pp, 250, L.muro, L.aliq, L.patr, + start=START)["cap"])) + del pp + print(f"\n {'':<14}{'fisco OFF':>16}{'fisco ON':>16}{'rapporto ON/OFF':>19}") + for lab, a, b in (("funding OFF", "L0 LORDO (26/07)", "L1 +FISCO (07/08)"), + ("funding ON ", "L2 +FUNDING (22/08)", "L3 CONGIUNTA")): + print(f" {lab:<14}{capf[a]:>16,.0f}{capf[b]:>16,.0f}" + f"{capf[b] / capf[a]:>19.4f}") + dragOFF = capf["L1 +FISCO (07/08)"] / capf["L0 LORDO (26/07)"] + dragON = capf["L3 CONGIUNTA"] / capf["L2 +FUNDING (22/08)"] + inter_log = (np.log(capf["L3 CONGIUNTA"]) - np.log(capf["L1 +FISCO (07/08)"]) + - np.log(capf["L2 +FUNDING (22/08)"]) + np.log(capf["L0 LORDO (26/07)"])) + verso_drag = "MENO" if dragON > dragOFF else "DI PIU'" + print(f"\n Il fisco toglie il {1 - dragOFF:.1%} del capitale a funding OFF e il " + f"{1 - dragON:.1%} a funding ON.") + esito_mecc = ("ESISTE ed e' nel verso previsto" if dragON > dragOFF else "NON si vede") + print(f" -> il meccanismo {esito_mecc}: con meno drift il fisco morde {verso_drag}.") + print(f" Interazione moltiplicativa (log-capitale): {inter_log:+.4f} = " + f"{np.expm1(inter_log):+.2%} di capitale.") + print(" ⚠️ Ma e' UNA FRAZIONE: il fisco resta la correzione dominante in entrambe le") + print(" colonne, e la 'compensazione' non ne recupera che una parte trascurabile.") + + # (3b) il muro + inter_w = (Lo["L3 CONGIUNTA"].muro - Lo["L1 +FISCO (07/08)"].muro + - Lo["L2 +FUNDING (22/08)"].muro + Lo["L0 LORDO (26/07)"].muro) + print("\n (3b) SUL MURO — la fase di prelievo.") + print(f" ${Lo['L0 LORDO (26/07)'].muro:,.0f} (L0) · ${Lo['L1 +FISCO (07/08)'].muro:,.0f} (L1) · " + f"${Lo['L2 +FUNDING (22/08)'].muro:,.0f} (L2) · ${Lo['L3 CONGIUNTA'].muro:,.0f} (L3)") + print(f" interazione ${inter_w:+,.0f} = {abs(inter_w) / Lo['L3 CONGIUNTA'].muro:.2%} del muro " + f"congiunto, cioe' SOTTO la risoluzione Monte Carlo ({sp:.1%}): indistinguibile da zero.") + + # (3c) il versamento necessario + print("\n (3c) SUL VERSAMENTO NECESSARIO — la moneta in cui si decide.") + print(f" Orizzonte 20 anni, P=90%, partenza ${START:.0f}. Ogni lente col PROPRIO muro.") + # ⚠️ ogni lente ha la SUA serie di ritorni: i path si riestraggono per lente (stesso seme, + # quindi stessi INDICI di blocco: il confronto fra lenti resta appaiato) e non si riusano. + dep90 = {} + for k in ("L0 LORDO (26/07)", "L1 +FISCO (07/08)", "L2 +FUNDING (22/08)", "L3 CONGIUNTA"): + L = Lo[k] + rr = np.random.default_rng(SEED_DEP) + pp = CC._boot_paths(L.r, N_DEP, 20 * 365, BLOCK, rr) + dep90[k], _ = dep_per_conf(pp, L, 0.90, START) + del pp + b = dep90["L0 LORDO (26/07)"] + d_fisco = dep90["L1 +FISCO (07/08)"] - b + d_fund = dep90["L2 +FUNDING (22/08)"] - b + d_joint = dep90["L3 CONGIUNTA"] - b + inter = d_joint - d_fisco - d_fund + print(f"\n {'lente':<27}{'EUR/mese per P=90% a 20 anni':>32}{'vs L0':>10}") + for k in ("L0 LORDO (26/07)", "L1 +FISCO (07/08)", "L2 +FUNDING (22/08)", "L3 CONGIUNTA"): + print(f" {k:<27}{dep90[k]:>32,.0f}{dep90[k] - b:>+10,.0f}") + print(f"\n somma ingenua : {d_fisco:+,.0f} {d_fund:+,.0f} = {d_fisco + d_fund:+,.0f} EUR/mese") + print(f" MISURATO : {d_joint:+,.0f} EUR/mese") + print(f" INTERAZIONE : {inter:+,.0f} EUR/mese ({inter / (d_fisco + d_fund):+.0%} della somma)") + # ⚠️ un'interazione piccola e' una DIFFERENZA DI DIFFERENZE: prima di dichiararne il SEGNO + # va misurato il rumore Monte Carlo che la circonda, o si pubblica un segno di rumore. + inters = [inter] + for sd in (4_242, 90_909): + dd = {} + for k in ("L0 LORDO (26/07)", "L1 +FISCO (07/08)", "L2 +FUNDING (22/08)", "L3 CONGIUNTA"): + L = Lo[k] + rr = np.random.default_rng(sd) + pp = CC._boot_paths(L.r, N_DEP, 20 * 365, BLOCK, rr) + dd[k], _ = dep_per_conf(pp, L, 0.90, START) + del pp + inters.append(dd["L3 CONGIUNTA"] - dd["L1 +FISCO (07/08)"] + - dd["L2 +FUNDING (22/08)"] + dd["L0 LORDO (26/07)"]) + segno_ok = all(x > 0 for x in inters) or all(x < 0 for x in inters) + et_segno = "STABILE" if segno_ok else "NON stabile: e' rumore" + print(" su 3 semi: " + " / ".join(f"{x:+,.0f}" for x in inters) + + f" -> segno {et_segno}" + + f" (bisezione a {6000 / 2 ** 12:.1f} EUR/m di risoluzione)") + verso = "MENO" if np.median(inters) < 0 else "PIU'" + print(f"\n 📌 ESITO, e la mia attesa era SBAGLIATA nella moneta che conta. Il meccanismo") + print(f" esiste (3a: il fisco morde meno dove il drift e' minore) ma nel VERSAMENTO") + if segno_ok: + print(f" necessario il congiunto e' {verso} severo della somma ({np.median(inters):+,.0f} EUR/m,") + print(" segno stabile su 3 semi): la funzione «versamento -> probabilita'» e' convessa") + print(" e ribalta il segno di un'interazione gia' piccola.") + else: + print(f" necessario l'interazione NON ha segno stabile fra i semi ({inters[0]:+,.0f} / " + f"{inters[1]:+,.0f} / {inters[2]:+,.0f} EUR/m):") + print(" e' rumore Monte Carlo, e va detto invece di pubblicarne il segno.") + print(" Sul MURO l'interazione e' sotto la risoluzione MC.") + print(f" REGOLA OPERATIVA: NON contare su una compensazione fra le due correzioni —") + print(f" si sommano, e il congiunto resta {d_joint / max(d_fisco, d_fund):.2f}x la piu' grande delle due") + print(f" presa da sola. {clock()}") + + sez(f"4) TRAIETTORIA da ${START:.0f} — anni al capitale-rendita, nelle quattro lenti") + print(f""" + Partenza ${START:.0f} = il conto Deribit VERO (le tabelle pubblicate partono da $600). + Ogni lente usa il PROPRIO muro. {N_TRAJ} path, blocchi da {BLOCK} giorni, orizzonte {ANNI_TRAJ} anni. + DUE mediane: 'mediana' = incondizionata (non-arrivo = +inf -> '>{ANNI_TRAJ}a'), 'cond.' = solo su chi + arriva — e la seconda non si legge senza la P accanto.""") + traj = {} + for k in Lo: + L = Lo[k] + rr = np.random.default_rng(SEED_TRAJ) + pp = CC._boot_paths(L.r, N_TRAJ, ANNI_TRAJ * 365, BLOCK, rr) + traj[k] = {dep: leggi(PN.accumula(pp, dep, L.muro, L.aliq, L.patr, + start=START)["colpito"]) for dep in VERSAMENTI} + del pp + hdr = f" {'EUR/m':>7}" + for k in Lo: + hdr += f"{k.split()[0]:>22}" + print("\n" + hdr) + print(f" {'':>7}" + "".join(f"{'mediana cond. P20':>22}" for _ in Lo)) + for dep in VERSAMENTI: + row = f" {dep:>7}" + for k in Lo: + d = traj[k][dep] + row += f"{fmt_med(d):>9}{fmt_cond(d):>7}{d['p20']:>6.0%}" + print(row) + print(f" {clock()}") + d250 = {k: traj[k][250] for k in Lo} + print(f""" + 📌 LA RIGA CHE MANCAVA. «EUR250/mese» P(traguardo entro 20 anni): + L0 LORDO {d250['L0 LORDO (26/07)']['p20']:>5.0%} (pubblicato 92% — qui {d250['L0 LORDO (26/07)']['p20']:.0%} su serie piu' lunga e da $635) + L1 +FISCO {d250['L1 +FISCO (07/08)']['p20']:>5.0%} (pubblicato 52%) + L2 +FUNDING {d250['L2 +FUNDING (22/08)']['p20']:>5.0%} (pubblicato 64%) + L3 CONGIUNTA {d250['L3 CONGIUNTA']['p20']:>5.0%} <- IL NUMERO ONESTO + L3b str. vero {d250['L3b CONGIUNTA (str. vero)']['p20']:>5.0%} (col funding misurato solo dove lo strumento esiste) + + ⚠️ Le due colonne di funding sono una BANDA, non una scelta: -2,16%/anno usa il proxy + inverse sul 40% del campione (conservativa), -1,39%/anno usa solo la finestra in cui il + perpetual lineare USDC esiste davvero. La verita' sta fra 14% e 26%, non su un numero. + P(traguardo entro i {ANNI_TRAJ} anni simulati), stessa riga: L3 {d250['L3 CONGIUNTA']['p_arr']:.0%} · L3b {d250['L3b CONGIUNTA (str. vero)']['p_arr']:.0%} + — cioe' anche allungando di 5 anni l'orizzonte una quota non trascurabile non arriva.""") + + # --- il lump: i soldi gia' fermi --- + sez(f"4-bis) E SE PARTO CON UN VERSAMENTO INIZIALE — ${START:.0f} + un lump, lente CONGIUNTA") + print(f""" + Il progetto ha misurato (27/07) che EUR10.000 fermi oggi valgono 2,45x, ma quel lump non e' + MAI entrato nelle tabelle nette. Qui e' nella lente congiunta. Lo stesso muro, gli stessi path.""") + L3 = Lo["L3 CONGIUNTA"] + rr = np.random.default_rng(SEED_TRAJ) + p3 = CC._boot_paths(L3.r, N_TRAJ, ANNI_TRAJ * 365, BLOCK, rr) + print(f"\n {'lump EUR':>10}" + "".join(f"{f'EUR{d}/mese':>26}" for d in (0, 250, 500))) + print(f" {'':>10}" + "".join(f"{'mediana cond. P20':>26}" for _ in range(3))) + for lump in LUMPS_EUR: + row = f" {lump:>10,.0f}" + for dep in (0, 250, 500): + d = leggi(PN.accumula(p3, dep, L3.muro, L3.aliq, L3.patr, + start=START, lump_eur=lump)["colpito"]) + row += f"{fmt_med(d):>11}{fmt_cond(d):>8}{d['p20']:>7.0%}" + print(row) + del p3 + print(f" {clock()}") + print("\n ⚠️ Un lump non e' capitale disponibile se e' il fondo d'emergenza (nota 27/07),") + print(" e a EUR10.000 sul conto si riapre la decisione di venue (soglia dichiarata $20k,") + print(" ma il piano cambia -> CLAUDE.md dice di riaprire PRIMA, non dopo).") + + # ===================================================================== + sez("5) QUANTO VERSARE PER UN ORIZZONTE DATO — lente CONGIUNTA, e il totale versato accanto") + print(f""" + bersaglio ${L3.muro:,.0f} (lente congiunta) · partenza ${START:.0f} · nessun lump + La colonna 'tot. versato' e' il confronto che conta: quanto del bersaglio lo COMPRI coi + bonifici invece di farlo produrre alla strategia.""") + L1 = Lo["L1 +FISCO (07/08)"] + print(f"\n {'orizzonte':>10}" + "".join(f"{f'P={c:.0%}':>13}" for c in CONFIDENZE) + + f"{'tot.versato@90%':>18}{'% bersaglio':>13}{' | solo fisco @90%':>21}") + riga10 = None + for h in ORIZZONTI: + rr = np.random.default_rng(SEED_DEP) + ph = CC._boot_paths(L3.r, N_DEP, h * 365, BLOCK, rr) + celle, tot90, dep90h = [], float("nan"), float("nan") + for c in CONFIDENZE: + v, vers = dep_per_conf(ph, L3, c, START) + celle.append(f"EUR{v:,.0f}/m" if np.isfinite(v) else "irraggiung.") + if c == 0.90: + tot90, dep90h = vers, v + del ph + rr = np.random.default_rng(SEED_DEP) + p1 = CC._boot_paths(L1.r, N_DEP, h * 365, BLOCK, rr) + v1, _ = dep_per_conf(p1, L1, 0.90, START) + del p1 + quota = tot90 / L3.muro if np.isfinite(tot90) else float("nan") + if h == 10: + rr = np.random.default_rng(SEED_DEP) + pb = CC._boot_paths(Lo["L3b CONGIUNTA (str. vero)"].r, N_DEP, h * 365, BLOCK, rr) + vb, versb = dep_per_conf(pb, Lo["L3b CONGIUNTA (str. vero)"], 0.90, START) + del pb + riga10 = (dep90h, tot90, quota, vb, versb) + print(f" {f'{h} anni':>10}" + "".join(f"{x:>13}" for x in celle) + + f"{f'${tot90:,.0f}':>18}{quota:>13.0%}" + + f"{f' | EUR{v1:,.0f}/m ({dep90h / v1:.2f}x)':>21}") + print(f" {clock()}") + print(""" + 📌 A 10 anni il rendimento fa la minoranza del lavoro: il resto lo fanno i bonifici. E' + la lezione gia' scritta il 26/07, qui con le due correzioni dentro: comprimere + l'orizzonte non fa lavorare la strategia, COMPRA il capitale.""") + + # ===================================================================== + sez("6) LA DOMANDA INVERSA — che rendita compra quello che posso permettermi") + print(f""" + Rendita NETTA mediana in EUR/giorno = capitale x perpetua({L3.perp:.2%}). Le imposte sono gia' + dentro la perpetua: il numero e' netto e NON va lordizzato una seconda volta. + Lente CONGIUNTA, partenza ${START:.0f}, nessun lump.""") + print(f"\n {'EUR/mese':>9}" + "".join(f"{f'{y}a':>12}" for y in (5, 10, 15, 20)) + + f"{'P(>=EUR50/g a 20a)':>21}") + for dep in VERSAMENTI: + celle, p50 = [], 0.0 + for y in (5, 10, 15, 20): + rr = np.random.default_rng(SEED_DEP) + py = CC._boot_paths(L3.r, N_DEP, y * 365, BLOCK, rr) + cap = PN.accumula(py, dep, L3.muro, L3.aliq, L3.patr, start=START)["cap"] + del py + rend = cap * L3.perp / 365.0 / CC.EURUSD + celle.append(f"{np.median(rend):,.2f}") + if y == 20: + p50 = float((rend >= CC.TARGET_EUR_DAY).mean()) + print(f" {dep:>9}" + "".join(f"{x:>12}" for x in celle) + f"{p50:>21.1%}") + print(f" {clock()}") + + # ===================================================================== + sez("VERDETTO") + j250 = traj["L3 CONGIUNTA"][250] + d10, t10, q10, d10b, t10b = riga10 + print(f""" + 1. LA TABELLA ORA ESISTE, ed e' peggiore di entrambe le lenti separate: + EUR250/mese, P(entro 20 anni) — LORDO {d250['L0 LORDO (26/07)']['p20']:.0%} · +FISCO {d250['L1 +FISCO (07/08)']['p20']:.0%} · +FUNDING {d250['L2 +FUNDING (22/08)']['p20']:.0%} · CONGIUNTA {j250['p20']:.0%}. + Il muro passa da ${Lo['L0 LORDO (26/07)'].muro:,.0f} a ${L3.muro:,.0f} ({L3.muro / Lo['L0 LORDO (26/07)'].muro - 1:+.0%}). + + 2. LE DUE CORREZIONI SI SOMMANO — la mia attesa («meno drift -> meno imposta -> congiunto + meno severo della somma») e' MISURATA e vale poco, e nella moneta che decide NON la + conferma. Interazione: {np.expm1(inter_log):+.2%} sul capitale accumulato (li' il meccanismo + c'e'), ${inter_w:+,.0f} sul muro (sotto la risoluzione MC), {np.median(inters):+,.0f} EUR/mese sul + versamento necessario ({np.median(inters) / (d_fisco + d_fund):+.0%} della somma, segno {"stabile" if segno_ok else "NON stabile"} + su 3 semi). Il congiunto resta + {d_joint / max(d_fisco, d_fund):.2f}x la piu' grande delle due correzioni presa da sola. + + 3. COSA NON CAMBIA A NESSUNA LENTE: senza versamenti il capitale-rendita non si raggiunge + MAI (P=0% a 20 anni in tutte e cinque le colonne). L'accumulo viene dai bonifici, non dal + rendimento — e l'ordine delle leve del piano e' invariato. + + 4. IL RISCHIO DI VENUE RESTA FUORI, DI PROPOSITO: e' rovina, non costo. A p=5% annuo il + capitale mediano a 20 anni e' ZERO per ogni riga di ogni tabella qui sopra. Nessuna di + queste righe e' valida se il conto sparisce. + + 5. L'OBIETTIVO DICHIARATO, senza addolcirlo: EUR50/giorno NON e' raggiungibile con versamenti + plausibili in un orizzonte breve. A 10 anni servono EUR{d10:,.0f}/mese (P=90%), cioe' ${t10:,.0f} + versati per arrivare a ${L3.muro:,.0f}: il {q10:.0%} del bersaglio lo compri coi bonifici. + La tabella utile e' quella INVERSA (sezione 6): non «quando arrivo a 50», ma «cosa compro + con quello che ho». +""") + print(f"\nCON TUTTE LE CORREZIONI, EUR250/MESE DA ${START:.0f}: {fmt_med(j250)[:-1]} anni " + f"(mediana incondizionata; cond. all'arrivo {fmt_cond(j250)}), " + f"P(20a) {j250['p20']:.0%}") + print(f"PER EUR50/GIORNO IN 10 ANNI SERVONO: EUR{d10:,.0f}/mese (P=90%), " + f"totale versato ${t10:,.0f}") + print(f" [banda di funding: lente conservativa -2,16%/a come sopra; con il funding dello") + print(f" strumento vero -1,39%/a -> EUR250/mese fa {fmt_med(d250['L3b CONGIUNTA (str. vero)'])} " + f"con P(20a) {d250['L3b CONGIUNTA (str. vero)']['p20']:.0%}, e i 10 anni costano " + f"EUR{d10b:,.0f}/mese (${t10b:,.0f})]") + print(f"\n{clock()} fine.") + + +if __name__ == "__main__": + main() From 2da765bcbb4bc8bf845b1aa2f59649c566130758 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 23:13:37 +0000 Subject: [PATCH 049/114] =?UTF-8?q?CLAUDE.md:=20il=20piano=20al=20netto=20?= =?UTF-8?q?di=20TUTTO=20=E2=80=94=20EUR250/mese=20fa=20P(20a)=2014-26%,=20?= =?UTF-8?q?non=2092%;=20e=20le=20correzioni=20si=20sommano=20invece=20di?= =?UTF-8?q?=20compensarsi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 256ec91..d98b731 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2466,6 +2466,52 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis eseguirla**, o si discute per mesi di un knob che non esiste; (f) **quando due stimatori di coda non concordano si cita la banda**, non quello che conviene. +- 🚨 **IL PIANO AL NETTO DI TUTTO — la tabella congiunta (2026-08-22, `r0822d_piano_vero.py`).** + **QUESTA SOSTITUISCE OGNI TABELLA DI TRAIETTORIA PUBBLICATA SOPRA.** Le due correzioni misurate al + piano — **fisco d'accumulo** (07/08) e **funding** (22/08) — erano state applicate **separatamente + allo stesso numero lordo**, e la tabella con **entrambe** non esisteva. Registro §36. + ✅ **Replica 6/6 prima di pubblicare, due esatte AL DOLLARO** sul vintage 07/08 ($272.061 lordo, + $258.338 netto fisco). ⚠️ Sulla serie di **oggi** la stessa riga da' **$278.033 (+2,2%)** — 15 + giorni di dati in piu', `data/raw/` gitignored (**stessa lezione GTAA del 07/08**) → **ogni terza + cifra di un muro e' rumore**, e i confronti fra lenti vanno fatti sullo stesso vintage. + | lente (de-luck ×0,89) | drift | perpetua | muro | **€250/m da $635: P(20a)** | + |---|---|---|---|---| + | L0 LORDO (26/07) | 17,11% | 10,68% | $278k | **90%** *(pubbl. 92%)* | + | L1 +FISCO (07/08) | 17,11% | 7,52% | $264k | **49%** *(pubbl. 52%)* | + | L2 +FUNDING (22/08) | 15,19% | 9,06% | $328k | **63%** | + | 🚨 **L3 CONGIUNTA** | **15,19%** | **6,35%** | **$313k** | **14%** | + | L3b congiunta, funding "strumento vero" −1,39%/a | 15,87% | 6,72% | $296k | **26%** | + 🚨 **LE DUE CORREZIONI NON SI COMPENSANO: SI SOMMANO — e nel VERSAMENTO necessario il congiunto e' + +10% PEGGIORE della loro somma.** Il meccanismo "meno drift → meno plusvalenza → meno imposta" + **esiste** (+9,97% di capitale recuperato; il fisco toglie il 50,9% a funding OFF e il 46,0% a + funding ON) **ma la funzione versamento→probabilita' e' CONVESSA e ne ribalta il segno**. + Interazione misurata **in tre monete**: +9,97% sul capitale · **−$815 sul muro (0,26%, SOTTO la + risoluzione MC)** · **+26 €/mese sul versamento (+10%, segno stabile su 3 semi)**. + **REGOLA: non contare su una compensazione fra correzioni misurate separatamente — la si misura, e + in piu' di una MONETA, perche' un'interazione piccola cambia segno con la moneta.** + **QUANTO VERSARE, al netto di tutto** (da $635, nessun lump): + | orizzonte | P=50% | P=75% | **P=90%** | totale versato @P=90% | **quota del bersaglio** | + |---|---|---|---|---|---| + | **10 anni** | €1.323 | €1.534 | **€1.733** | **$229.189** | **73%** | + | 15 anni | €656 | €784 | **€920** | $183.129 | 58% | + | 20 anni | €360 | €445 | **€541** | $143.804 | 46% | + Il **solo funding** aggiunge **1,27× / 1,33× / 1,40×** sopra la lente fisco-only. + **RENDITA €/g mediana a 20 anni:** €250/m → **31,85 €/g, P(≥50/g) 8,8%** · €500/m → **63,05, P + 77,1%** · €800/m → 100,50, P 99,2%. + 📌 **IL LUMP, mai entrato nelle tabelle nette: €10.000 oggi + €250/m porta P(20a) da 14% a 45%** + (€5.000 → 30%). E' la leva piu' grande misurata dopo il versamento mensile stesso. + 📌 **LA RISPOSTA ALLA DOMANDA DEL PROGETTO:** *€250/mese da $635 → **23,4 anni** (mediana + incondizionata; 22,1 condizionata all'arrivo), **P(20a) 14%** [banda "strumento vero": 22,2 anni, + 26%]. Per **€50/giorno in 10 anni** servono **€1.733/mese** a P=90%, totale versato **$229.189**.* + **A 10 anni si versano $229k per arrivare a ~$313k: il rendimento fa il 27%, i bonifici il 73%** — + forma piu' netta della lezione gia' scritta: *a orizzonte corto non fai lavorare la strategia, + COMPRI il capitale coi bonifici.* + ⚠️ **Cosa NON e' incluso, di proposito:** il **rischio di venue**, che e' **rovina e non costo** + (a p=5% il mediano e' zero a ogni calendario) — resta sul suo asse separato. + **Smentitori dichiarati:** il funding pre-2022 e' **proxy inverse sul 40% del campione** (se il + lineare 2019-21 fosse costato quanto il 2022+, la colonna vera e' **L3b**); il modello fiscale tassa + la **variazione annua di valore** = limite superiore; il bootstrap gira su **7,4 anni con due tori**. + - **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso: cross-sectional su universo Hyperliquid certificato (XS01) → portafoglio Sharpe ~1.55. - **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.** From df08460af040c47b78fbf5471f42e59db7d40e13 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 23:36:38 +0000 Subject: [PATCH 050/114] =?UTF-8?q?research(wave-0822):=20GATE-C=20?= =?UTF-8?q?=E2=80=94=20gate=20riformulato=20senza=20date,=20PASS=202/2;=20?= =?UTF-8?q?XS01=20PAGA=20funding;=20il=20biglietto=20batte=20il=20versare?= =?UTF-8?q?=20solo=20perche'=20il=20piano=20esiste?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 73 ++ scripts/research/r0822d_gate_c.py | 1103 +++++++++++++++++++++++++++++ 2 files changed, 1176 insertions(+) create mode 100644 scripts/research/r0822d_gate_c.py diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 00ebac5..d992434 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -702,6 +702,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 34 | CC01-2022 | il carry riaperto sui dati che gli mancavano per costruzione | **RESTA SCARTATO** — per la ragione giusta, ma non quella pubblicata | | 35 | FUNDING | il costo mai prezzato di TENERE una posizione aperta | **LEAD** — **−2,16%/anno di drift**, muri **+17,5%** | | 36 | PIANO-VERO | la tabella congiunta fisco+funding, che non esisteva | 🚨 **€250/m: P(20a) 92% → 14-26%** | +| 37 | GATE-C | l'ultima gamba del gate funded + il biglietto come opzione | **PASS 2/2** — gate senza date; **P(≥50/g) 4,4% → 2,6%** | | 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 | @@ -1780,3 +1781,75 @@ PER €50/GIORNO IN 10 ANNI SERVONO: €1.733/mese (P=90%), totale versato $229. 📌 **A 10 anni si versano $229k per arrivare a ~$313k: il rendimento fa il 27%, i bonifici il 73%.** E' la forma piu' netta finora della lezione gia' scritta: **a orizzonte corto non fai lavorare la strategia, COMPRI il capitale coi bonifici.** + +--- + +## 37 — GATE-C (l'ultima gamba del gate funded, e il biglietto riprezzato) + +`scripts/research/r0822d_gate_c.py` (356 s, nessuna rete). **1.347 celle**, **zero di selezione** +(pesi, meccanismo, lente, de-luck e soglie tutti **ereditati e congelati**) → niente DSR da calcolare. +Ridotto e dichiarato: 23 offset × **3 fasi** (lo stesso sottocampione a priori di §32). + +✅ **Riproduzioni prima di ogni numero nuovo:** §32 (4,4% [2,5-8,2%], P(zero) 35,3%) **esatta**; §35 +(−2,16/−2,10/−2,32%) **esatta**; ladder contro `pc.simulate` **bit-exact, `max|diff| = 0,0` percorso +per percorso**. ⚠️ `r0725_hyro` da' **−11%** rispetto al pubblicato ($6.018 contro $6.789) **perche' il +file ri-deriva le serie dal feed corrente, +4 settimane** — stessa lezione del vintage. + +**Il biglietto a due stadi** (cella d'ancora **mediana**, non la canonica che sta al 91° pctl; funding +dentro; lente accoppiata; 6.000 percorsi): P(pass) **35,5-52,5%**, **P(payout | pass) 72,3-89,2%**, +E[payout] 1 anno funded $1.016-2.194. +**EV del biglietto $100k in tre convenzioni:** spesa persa **$+1.615** · rimborso al pass **$+1.919** · +**rimborso al primo payout (la regola vera) $+1.835**. **Positivo in tutte e tre.** +📌 **Ma la correzione economica che aveva motivato il filone vale +13,6% di EV e +0,60pp su P(≥50/g): +reale, misurata, e NON cambia nessuna decisione.** + +🚨 **Il funding costa anche qui, appaiato cella per cella (69 celle): dJ −0,0491 (69/69 negative), +dP(≥50/g) −1,60pp (66/69)** → **P(≥50 €/g) 2,6% [1,5%, 4,7%], P(zero) 40,3%**. +**Catena completa del ridimensionamento in una giornata: 42% → 7,8% → 4,4% → 2,6%.** + +🚨 **XS01 PAGA il funding, non lo incassa — e il meccanismo e' DIMOSTRATO, non congetturato:** +`max|ΣW| = 2,8e-17` (dollar-neutral **esatto**) ⇒ **il livello del funding non entra, entra solo la +dispersione cross-sezionale**; e il momentum cross-sezionale **compra i perp col funding piu' caro e +vende quelli col piu' basso** → **+1,45%/anno** (IC95 [+1,18, +1,72]; banda onesta [+0,69, +1,45] se +HL e' ~2,1× il venue eseguibile). *L'ipotesi opposta era nel mio briefing.* + +📌 **Il progetto conteneva GIA' due modelli contraddittori della quota:** `r0725_hyro:200` la rimborsa +**al PASS** (ottimista), `pc.simulate` non la rimborsa **mai** (pessimista) — e il numero operativo +usciva dal secondo. Le due convenzioni *sbagliate* distano **$84** = prezzo × (P(pass) − P(payout)). +📌 **Piu' leva sul funded ALZA E[payout] e ABBASSA P(payout):** chi vuole il rimborso non vuole la leva +massima. **Sono due obiettivi diversi.** +⚠️ **Un numero netto catturato PRIMA di pubblicarlo:** il "capitale d'incrocio €1.000-2.000" **NON e' +un meccanismo**. La ricchezza terminale della strada-biglietto e' **bimodale**, e +`P(cassa finale < $249)` si muove **liscia** (56,3 → 50,8 → 28,3 → 16,9 → 3,9 → 0,0%) mentre la +mediana **salta di un ordine di grandezza** appena quella massa passa il 50%. **La mediana non e' una +statistica robusta qui** — ed e' esattamente il tipo di numero netto che si sarebbe finito per citare +come soglia di progetto. + +**IL CONFRONTO CHE DECIDE** (36 mesi, $654 iniziali, **stesso bootstrap appaiato**, funding in +entrambe le strade, fisco 33%): +| versamento | strada | mediana | media | P( 0 con la quota trattata come SPESA PERSA**, alla cella +d'ancora **mediana**, col funding dentro, sotto lente accoppiata. *Oggi: **$+1.615 → PASS***. +**(c2) DI SOPRAVVIVENZA** — prezzo del biglietto **≤ 2 versamenti mensili del piano in corso**, +perche' il piano d'accumulo e' la leva dominante misurata e **zero e' assorbente**. *Oggi (piano +€500/mese): **$579 contro $1.090 → PASS***. +📌 **La FORMA del gate e' imposta dal costo della misura:** l'EV costa una corsa di script (zero +dollari), **la rimborsabilita' costa $579 e si puo' misurare solo comprando** → un gate d'attesa su +quella sarebbe **superabile solo dopo averlo violato**. Percio' (c1) e' scritta perche' **la +rimborsabilita' NON decida**, e il rimborso diventa **un regalo invece di un'ipotesi su cui si e' +scommesso**. + +**`IL BIGLIETTO CONTRO IL VERSARE: MEGLIO — ma solo perche' il piano di versamento c'e', e la ragione +non e' il rendimento, e' il NUMERO DI VOLTE CHE SI PUO' GIOCARE.`** Senza versamenti la stessa +scommessa **perde la mediana 6×** ($132 contro $787) e **vince la media 12×**: e' una lotteria a valore +atteso positivo che un conto da $654 puo' permettersi di giocare **una volta sola**. +⚠️ E il vantaggio poggia **interamente** sul drift di XS01 misurato su **13 gambe su 19**. +**Smentitore che azzera tutto senza che il modello lo segnali:** una **morte-firm ≫10%/anno**. diff --git a/scripts/research/r0822d_gate_c.py b/scripts/research/r0822d_gate_c.py new file mode 100644 index 0000000..080d1b8 --- /dev/null +++ b/scripts/research/r0822d_gate_c.py @@ -0,0 +1,1103 @@ +"""r0822d_gate_c — GATE PROP-01, gamba (c): il BIGLIETTO riprezzato come OPZIONE (2026-08-22). + +IL BUCO CHE CHIUDE +------------------ +`GATE PROP-01` (RESULTS-0822 §29) ha tre gambe. Due sono state chiuse stanotte: + (a) LISTINO -> PASS 13/13, misurato sul venue (Bybit `instruments-info`, 833 strumenti); + (b) ANCORA SKH01 -> PASS, delta appaiato +0,116 sui 23 offset, 230/230 celle congiunte + positive — ma il numero OPERATIVO e' sceso a P(>=50 EUR/g) = 4,4% [2,5-8,2%], P(zero) 35,3%. +La gamba (c) e' rimasta una CONSTATAZIONE, non un criterio: + + "un'eval HYRO $100k costa $579 su un conto di $635 = il 91%" + +e per giunta poggia su un'economia sbagliata. La pagina della firm dichiara la quota come +**"Refundable Challenge Deposit, returned in full alongside your first funded payout"**: +non e' una fee affondata, e' un **deposito rimborsabile al PRIMO PAYOUT**. Immobilizza il 91% +del conto; non lo brucia. Va riprezzato come **struttura a opzione a DUE STADI** +(passare l'eval **E POI** produrre abbastanza profitto da chiedere un prelievo), non come spesa. + +⚠ E qui c'e' un fatto che nessuno ha notato: **il progetto contiene GIA' DUE modelli che si +contraddicono fra loro.** + * `r0725_hyro.py:200` -> `cost = price * (1.0 - pe)` = quota **rimborsata al PASS**; + * `r0725_prop_ladder.simulate` / `r0725_prop_coupled.simulate` -> `cash[buy] -= price` + e la quota **non torna MAI** = spesa affondata. +Il primo e' OTTIMISTA (rimborsa a un evento che accade prima e piu' spesso di quello vero), +il secondo e' PESSIMISTA (non rimborsa affatto). **Il vero sta in mezzo, e il numero operativo +del progetto — P(>=50 EUR/g) — viene dal secondo.** + +LA PREVISIONE, REGISTRATA PRIMA DI MISURARE +------------------------------------------- +1. Il rimborso al primo payout vale **poco** sull'EV del biglietto e **piu'** sulla scala di + conti (dove la cassa che torna ricompra un biglietto e composta). Mi aspetto: EV +5-15%, + P(>=50/g) +1-3 punti percentuali. +2. `P(primo payout | pass)` mi aspetto **alta** (>80%): la soglia di prelievo e' $100 di quota + trader = 0,125% di nozionale su $100k, cioe' un'inezia rispetto al 10% appena prodotto + nell'eval. Quindi la differenza fra le due convenzioni di rimborso e' **piccola**. +3. Il **funding** su un conto funded fa male in proporzione alla leva: a 0,75x mi aspetto + ~-1,6%/anno di equity del conto (cioe' 0,75 x 2,16%), e su un conto a barriera un costo di + drift si paga sulla P(pass) piu' che sul maxDD. +4. **XS01: mi aspetto che INCASSI funding**, perche' e' market-neutral e i perp con funding piu' + caro sono tipicamente quelli in forte trend, che il momentum cross-sezionale compra... ma + comprarli vuol dire PAGARE il loro funding. Le due forze tirano in verso opposto: **non lo so, + e per questo si misura.** Se il segno esce negativo (costo) lo dico. +5. **La domanda che decide**: con P(>=50 EUR/g) 4,4% e P(zero) 35,3%, il biglietto ha EV + positivo? E batte l'alternativa ovvia (tenere i $635 sul libro e versare)? Mi aspetto: + **EV positivo sulla MEDIA e negativo sulla MEDIANA** — cioe' una lotteria con valore atteso + positivo, che e' esattamente la forma che un conto da $635 non puo' permettersi molte volte. + +CONVENZIONI DICHIARATE +---------------------- +* Lente **ACCOPPIATA** ovunque (`r0725_prop_coupled`): su una regola a UN GIORNO la close-only + e' esattamente CIECA. La close-only compare solo come controllo. +* Configurazione del conto funded: **W_A = TP01 38 / SKH01 38 / XS01 25** (centro della regione + robusta di PROP-RECAL), XS01 = **U13 Binance** (la versione piu' debole e l'unica testabile + fuori campione), finestra **W-LONG 2021+**, de-luck sul drift **x0,89**. +* Il conto PROPRIO gira invece il **libro LIVE 75/25 a $654 con min-order $5** (XS01 non e' + eseguibile a quel capitale): l'asimmetria e' REALE ed e' il punto del canale funded. +* Banda d'ancora: **23 offset x 3 fasi (0,3,7)**, lo stesso sottocampione dichiarato da + `r0822c_skh_anchor_prop`. La stima onesta e' la **MEDIANA**, non l'ancora canonica. +* **Firm death 10%/anno** e **cap $200k/trader**: assunzioni gia' in uso nel progetto (24/07), + dichiarate e non nascoste. +* Soglia di prelievo **$100 di quota trader** (fonte primaria-dichiarativa della firm, non + misurata) -> profitto lordo >= $125 su $100k. +* Monte Carlo: la risoluzione si dichiara. 1500 percorsi sulla scala di conti (+-1pp su + P(>=50/g)), 6000 sul biglietto a conto singolo, 2500 su J. + +Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0822d_gate_c.py` +Rete: NESSUNA (tutto da disco: parquet certificati, cache Binance di r0822b, cache funding +di r0822d_funding). Se la cache del funding manca, la sezione 1 lo dichiara e si ferma li'. +""" +from __future__ import annotations + +import contextlib +import io +import re +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)) + +if "--no-net" not in sys.argv: # r0822d_funding legge USE_NET all'import + sys.argv.append("--no-net") + +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 (25/07) +import r0822_prop_alloc as PA # noqa: E402 J, bootstrap, simulatore (22/08) +import r0822b_prop_recal as PR # noqa: E402 PROP-RECAL (22/08) +import r0822b_xs01_oos as X # noqa: E402 motore XS01 + cache Binance (22/08) +import r0822c_skh_anchor_prop as AP # noqa: E402 gamba (b), offset di SKH01 (22/08) +import r0822d_funding as FD # noqa: E402 il funding (22/08) +from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402 + +FD.USE_NET = False + +RAW = ROOT / "data" / "raw" +HYRO = "HYRO" +ASSETS = ("BTC", "ETH") +EURUSD = pl.EURUSD +TAX_RATE = pl.TAX_RATE +SPLIT = pl.SPLIT +DELUCK = PA.DELUCK # x0,89, fattore d'ancora MISURATO (26/07) +LEVS = PR.LEVS +OFFSETS = AP.OFFSETS # 23 offset a priori di SKH01 +PHASES_SUB = AP.PHASES_SUB # (0, 3, 7) — dichiarato da r0822c +W_A = {"TP01": 0.38, "SKH01": 0.38, "XS01": 0.25} +W_LIVE = {"TP01": 0.75, "SKH01": 0.25} +N_PATHS_J = PA.N_PATHS # 2500 +N_PATHS_LAD = 1500 +N_PATHS_TIX = 6000 +EV_H = FU_H = 365 +SEED_EV, SEED_FU = 8220, 8221 +SEED_LAD = 20260822 +MIN_PAYOUT_TRADER = 100.0 # quota trader minima per un prelievo (firm) +HORIZON_DAYS = pl.HORIZON_MONTHS * 30 # 1080 = 36 mesi + +# numeri PUBBLICATI da riprodurre prima di produrne di nuovi +PUB = dict( + hyro_ev_close=6789.0, hyro_ev_intra=1166.0, # r0725_hyro (25/07) + recal_j=0.453, recal_live=0.281, recal_d=0.175, recal_p50g=0.078, # §29 + anchor_p50g=0.044, anchor_p10=0.025, anchor_p90=0.082, + anchor_pzero=0.353, # §32 + fund_book=-2.16, fund_tp=-2.10, fund_skh=-2.32, # §35 (%/anno) +) + +TRIALS = 0 +T0 = time.time() + + +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) + + +def tick(lab: str) -> None: + print(f" [{time.time()-T0:6.0f}s] {lab}") + + +# ============================================================================ 1. IL FUNDING + +def funding_rate() -> dict | None: + """Serie oraria del funding per asset (LINEARE dove esiste, INVERSE prima come proxy + dichiarato). Legge SOLO la cache di `r0822d_funding` — nessuna rete.""" + try: + F = {} + for instr, (start, _a, _k) in FD.INSTR.items(): + d = FD.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 FD.build_rate(F) + except Exception as exc: # pragma: no cover + print(f" !! cache funding assente/illeggibile ({exc}) — il funding NON viene messo dentro") + return None + + +_FTP: dict[int, pd.Series] = {} + + +def f_tp01_sleeve(rate: dict) -> pd.Series: + """Costo di funding GIORNALIERO dello sleeve TP01 a peso 1.0, sulla STESSA lente delle gambe + orarie del recon (`mc.tp01_daily_frame` = TP01 canonico, senza min-order: su un conto funded + da $100k il min-order $5 non esiste, e questa e' la lente giusta li'). + + Convenzione temporale identica a `FD.tp01_canonical`: la posizione tenuta nel giorno d e' + decisa a close[d-1], e paga il funding sulle 24 ore (d, d+24h].""" + if 0 in _FTP: + return _FTP[0] + out = None + for a in ASSETS: + tpf = mc.tp01_daily_frame(a) + days = pd.DatetimeIndex(tpf.index) + s = pd.Series(tpf["w"].values * FD.daily_funding(rate[a], days), index=days) + out = s if out is None else out.add(s, fill_value=0.0) + _FTP[0] = out + return out + + +_FSKH: dict[int, pd.Series] = {} + + +def f_skh_sleeve(rate: dict, off: int = 0) -> pd.Series: + """Costo di funding giornaliero dello sleeve SKH01 (50/50) all'offset `off`, come + DIFFERENZA fra il path live senza e con funding (`FD.skh_book`). E' esatto al primo ordine + (l'equity compone: il secondo ordine e' ~1e-6/giorno) e riproduce il -2,32%/anno di §35.""" + if off in _FSKH: + return _FSKH[off] + a0 = FD.skh_book(off, "hourly", None) + a1 = FD.skh_book(off, "hourly", rate) + if a0.index.tz is None: + a0.index = a0.index.tz_localize("UTC") + if a1.index.tz is None: + a1.index = a1.index.tz_localize("UTC") + s = (a0 - a1).dropna() + _FSKH[off] = s + return s + + +def xs_funding_measure(verbose: bool = True) -> dict: + """IL SEGNO DEL FUNDING SU XS01, MISURATO (non assunto). + + XS01 e' long 5 / short 5 a pesi uguali -> `sum(W) = 0` ESATTAMENTE. Quindi + `sum(W_i f_i) = sum(W_i (f_i - fbar))`: **il LIVELLO del funding non entra, entra solo la + sua DISPERSIONE CROSS-SEZIONALE**. Cio' significa due cose: + * non e' vero ne' che 'incassa' ne' che 'paga' per costruzione: dipende da come il + funding e' distribuito FRA gli asset che il momentum compra e vende; + * il fatto noto che il funding Hyperliquid sia ~2,1x quello del venue vero (§35) si + trasferisce alla dispersione, quindi si riporta una BANDA, non un punto. + Dato: `data/raw/hlfund__1h.parquet` (19 major, 2023-05 -> 2026-06).""" + P = X.hl_panel() + det: dict = {} + X.xsec_engine(P, phase=0, detail=det) + W, scale, idx = det["W"], det["scale"], det["index"] + cols = list(P.columns) + gross_sum = float(np.abs(W.sum(axis=1)).max()) + fun = {} + for s in cols: + f = RAW / f"hlfund_{s.lower()}_1h.parquet" + if not f.exists(): + continue + d = pd.read_parquet(f)["funding"] + d.index = pd.DatetimeIndex(d.index).tz_convert("UTC") + fun[s] = d.groupby(d.index.floor("D")).sum() + have = [s for s in cols if s in fun] + Fm = pd.DataFrame({s: fun[s] for s in have}).reindex(idx) + cover = Fm.notna().mean(axis=1) + Fv = Fm.fillna(0.0).values + sel = [cols.index(s) for s in have] + Wp = np.vstack([np.zeros((1, len(cols))), W[:-1]]) # posizioni TENUTE nel giorno i + cost = pd.Series((Wp[:, sel] * Fv).sum(axis=1) * scale, index=idx) + gross = pd.Series(np.abs(Wp[:, sel]).sum(axis=1) * scale, index=idx) + ok = cover >= 0.95 + c = cost[ok] + if verbose: + print(f" pannello HL {P.shape[1]} gambe, {len(idx)} giorni; file di funding per " + f"{len(have)}/{len(cols)} gambe") + print(f" CONTROLLO di dollar-neutralita': max|sum(W)| = {gross_sum:.2e} " + f"-> {'ESATTO' if gross_sum < 1e-12 else 'NON neutrale'}") + print(f" giorni con copertura >=95% delle gambe: {int(ok.sum())} " + f"({c.index.min().date()} -> {c.index.max().date()})") + print(f"\n {'anno':>6} {'costo funding %/anno':>22} {'gross nozionale':>17} {'giorni':>7}") + for y, g in c.groupby(c.index.year): + gg = gross.reindex(g.index) + print(f" {y:>6} {float(g.mean()*365)*100:>21.3f}% {float(gg.mean()):>16.3f}x " + f"{len(g):>7}") + ann = float(c.mean() * 365) + se = float(c.std(ddof=1) / np.sqrt(len(c)) * 365) + print(f" {'TUTTO':>6} {ann*100:>21.3f}% {float(gross.reindex(c.index).mean()):>16.3f}x " + f"{len(c):>7} IC95 [{(ann-1.96*se)*100:+.3f}%, {(ann+1.96*se)*100:+.3f}%]") + ann = float(c.mean() * 365) + return dict(ann=ann, series=c, gross=float(gross.reindex(c.index).mean()), + se=float(c.std(ddof=1) / np.sqrt(len(c)) * 365), n=len(c)) + + +# ================================================================ 2. LIBRO CON FUNDING + +def fvec_for(w: dict, idx: pd.DatetimeIndex, rate: dict | None, off: int, + xs_ann: float) -> np.ndarray | None: + """Vettore giornaliero del costo di funding del LIBRO ai pesi `w`. + TP01 e SKH01: serie misurate giorno per giorno. XS01: **tasso annuo costante** misurato su + Hyperliquid 2024-2026 e applicato all'intera finestra — dichiarato, perche' `hlfund` parte + nel 2023-05 e il pannello di XS01 usato qui e' Binance 2021+ (nessun funding su disco).""" + if rate is None: + return None + f = np.zeros(len(idx)) + wtp, wsk, wxs = w.get("TP01", 0.0), w.get("SKH01", 0.0), w.get("XS01", 0.0) + if wtp: + f += wtp * np.nan_to_num(f_tp01_sleeve(rate).reindex(idx).values) + if wsk: + f += wsk * np.nan_to_num(f_skh_sleeve(rate, off).reindex(idx).values) + if wxs: + f += wxs * (xs_ann / 365.0) + return f + + +def best_lev(R, G, ev, fu) -> tuple[float, float, np.ndarray]: + jb, lb, vb = -1.0, None, None + for lv in LEVS: + o = PR.obj_rg(R, G, lv, ev, fu, factor=DELUCK) + if o["J"] > jb: + jb, lb, vb = o["J"], lv, o["jvec"] + bump(len(LEVS)) + return jb, lb, vb + + +# ============================================ 3. SCALA DI CONTI COL DEPOSITO RIMBORSABILE + +def ladder(R: np.ndarray, G: np.ndarray, n_paths: int, lev: float, seed: int, + refund: bool, deposit_eur: float = 0.0, start_eur: float = pl.START_EUR, + min_payout: float = 0.0, boot: np.ndarray | None = None, + block: int = 20, deluck: float = DELUCK) -> dict: + """Copia di `r0725_prop_coupled.simulate` (un solo vettore di peso, `MAX_CONCURRENT` slot) + con TRE aggiunte e nessun'altra differenza: + 1. `refund` — la quota torna in cassa **al PRIMO PAYOUT** di quel conto (regola vera + della firm). Se il conto busta prima di un payout, la quota e' persa. + 2. `deposit_eur` — versamento mensile, per confrontare la strada-biglietto con la + strada-versare a parita' di soldi che entrano. + 3. `min_payout` — quota trader minima per poter prelevare ($100 secondo la firm). + Con `refund=False, deposit_eur=0, min_payout=0, boot=None` deve essere **BIT-EXACT** + rispetto a `pc.simulate`: e' il controllo di §3.1. + + Il funding, se c'e', e' gia' dentro `R` -> essendo `Rs = R*lev`, il costo di funding + **scala con la leva**, che e' la sua vera economia (e' proporzionale all'esposizione).""" + rng = np.random.default_rng(seed) + n_days = HORIZON_DAYS + L = len(R) + Rm = R.astype(float).reshape(-1, 1).copy() + Gm = G.astype(float).reshape(-1, 1).copy() + Rm = Rm - (1.0 - deluck) * Rm.mean(axis=0, keepdims=True) + if boot is None: + n_blocks = int(np.ceil(n_days / block)) + starts = rng.integers(0, L - block, size=(n_paths, n_blocks)) + idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days] + else: + idx = boot + n_slot = pl.MAX_CONCURRENT + sel = np.zeros(n_slot, dtype=int) + Rs = Rm[idx][:, :, sel] * lev + G_ = Gm[idx][:, :, sel] * lev + + FIRMS = pl.FIRMS + cash = np.full(n_paths, start_eur * EURUSD) + phase = np.full((n_paths, n_slot), -1, np.int8) + eq = np.ones((n_paths, n_slot)) + firm = np.full((n_paths, n_slot), -1, np.int8) + notional = np.zeros((n_paths, n_slot)) + price_paid = np.zeros((n_paths, n_slot)) + refunded = np.zeros((n_paths, n_slot), bool) + firm_alive = np.ones((n_paths, len(FIRMS)), bool) + cum_payout = np.zeros(n_paths) + spent = np.zeros(n_paths) + got_back = np.zeros(n_paths) + deposited = np.zeros(n_paths) + n_payouts = np.zeros(n_paths, int) + p_death_day = 1.0 - (1.0 - pl.FIRM_DEATH_PER_YEAR) ** (1 / 365.0) + + for t in range(n_days): + died = rng.random((n_paths, len(FIRMS))) < p_death_day + newly_dead = died & firm_alive + firm_alive &= ~died + if newly_dead.any(): + for fi in range(len(FIRMS)): + kill = newly_dead[:, fi] + if kill.any(): + hit = (firm == fi) & kill[:, None] + phase[hit] = -1 + notional[hit] = 0.0 + + for k in range(n_slot): + free = phase[:, k] == -1 + if not free.any(): + continue + for fi, F in enumerate(FIRMS): + used = np.where(firm == fi, notional, 0.0).sum(axis=1) + for (size, price) in sorted(F["tickets"], key=lambda x: -x[0]): + buy = (free & firm_alive[:, fi] & (cash >= price) + & (used + size <= F["cap"])) + if not buy.any(): + continue + cash[buy] -= price + spent[buy] += price + phase[buy, k] = 0 + eq[buy, k] = 1.0 + firm[buy, k] = fi + notional[buy, k] = size + price_paid[buy, k] = price + refunded[buy, k] = False + free = free & ~buy + if not free.any(): + break + if not free.any(): + break + + r = Rs[:, t, :] + g = G_[:, t, :] + act = phase >= 0 + eq_prev = eq + eq = np.where(act, eq * (1.0 + r), eq) + eq_low = np.where(act, eq_prev * (1.0 + r + g), eq) + r_low = r + g + + for fi, F in enumerate(FIRMS): + s_ = act & (firm == fi) + if not s_.any(): + continue + ev_t, ev_dd, ev_dl = F["ev"] + fu_ml, fu_dl = F["fu"] + in_ev = s_ & (phase == 0) + in_fu = s_ & (phase == 1) + bust = ((in_ev & ((eq_low < 1.0 - ev_dd) | (r_low < -ev_dl))) + | (in_fu & ((eq_low < 1.0 - fu_ml) | (r_low < -fu_dl)))) + phase[bust] = -1 + notional[bust] = 0.0 + passed = in_ev & (eq >= 1.0 + ev_t) & ~bust + phase[passed] = 1 + eq[passed] = 1.0 + + if (t + 1) % 30 == 0: + fu = phase == 1 + pay = fu & (eq > 1.0) & (((eq - 1.0) * notional * SPLIT) >= min_payout) + gain = np.where(pay, (eq - 1.0) * notional, 0.0) + net = gain.sum(axis=1) * SPLIT * (1.0 - TAX_RATE) + cum_payout += net + cash += net + n_payouts += (pay.sum(axis=1) > 0).astype(int) + eq = np.where(pay, 1.0, eq) + if refund: + back = pay & ~refunded + amt = np.where(back, price_paid, 0.0).sum(axis=1) + cash += amt + got_back += amt + refunded |= back + if deposit_eur: + cash += deposit_eur * EURUSD + deposited += deposit_eur * EURUSD + + eur_day = cum_payout / EURUSD / (n_days / 365.0) / 365.0 + return dict(eur_day=eur_day, med=float(np.median(eur_day)), + p10=float(np.percentile(eur_day, 10)), p90=float(np.percentile(eur_day, 90)), + p_ge10=float((eur_day >= 10.0).mean()), + p_target=float((eur_day >= pl.TARGET_EUR_DAY).mean()), + p_zero=float((cum_payout <= 0).mean()), + p_payout=float((n_payouts > 0).mean()), + cash=cash, med_cash=float(np.median(cash)), mean_cash=float(cash.mean()), + p10_cash=float(np.percentile(cash, 10)), p90_cash=float(np.percentile(cash, 90)), + med_spent=float(np.median(spent)), mean_back=float(got_back.mean()), + contributed=float(start_eur * EURUSD + deposited.mean())) + + +def book_road(rb: np.ndarray, boot: np.ndarray, start_eur: float, deposit_eur: float, + deluck: float = DELUCK, tax: float = TAX_RATE) -> dict: + """La strada ALTERNATIVA: gli stessi soldi restano sul LIBRO LIVE (proprio capitale, nessuna + barriera, nessuna firm) e si versa. Stessi sorteggi di bootstrap della strada-biglietto + (confronto APPAIATO), stesso de-luck, stesso orizzonte. + Fisco: 33% sulla variazione ANNUA di valore, con riporto delle minusvalenze (convenzione di + `r0807_piano_netto`; il libro realizza quasi tutto entro l'anno -> limite superiore stretto).""" + R = rb.astype(float).copy() + R = R - (1.0 - deluck) * R.mean() + r = R[boot] + n_paths, n_days = r.shape + cap = np.full(n_paths, start_eur * EURUSD) + basis = cap.copy() + carry = np.zeros(n_paths) + deposited = 0.0 + for t in range(n_days): + cap = np.maximum(cap * (1.0 + r[:, t]), 0.0) + if (t + 1) % 30 == 0 and deposit_eur: + cap += deposit_eur * EURUSD + basis += deposit_eur * EURUSD + deposited += deposit_eur * EURUSD + if (t + 1) % 365 == 0: + g = (cap - basis) + carry + due = np.where(g > 0, tax * g, 0.0) + carry = np.where(g > 0, 0.0, g) + cap = cap - due + basis = cap.copy() + return dict(cash=cap, med_cash=float(np.median(cap)), mean_cash=float(cap.mean()), + p10_cash=float(np.percentile(cap, 10)), p90_cash=float(np.percentile(cap, 90)), + contributed=float(start_eur * EURUSD + deposited)) + + +# ==================================================== 4. IL BIGLIETTO A CONTO SINGOLO, 2 STADI + +def ticket_mc(R: np.ndarray, G: np.ndarray, lev_ev: float, lev_fu: float, notional: float, + n_paths: int = N_PATHS_TIX, seed: int = 82241, max_eval_days: int = 540, + funded_days: int = 365, min_payout: float = MIN_PAYOUT_TRADER, + consistency: bool = True, firm_death: bool = True, block: int = 20, + deluck: float = DELUCK, firm: str = HYRO) -> dict: + """UN conto HYRO, i due stadi **CHAINED sullo stesso percorso** (eval -> funded), che e' la + differenza rispetto a `PA.eval_sim`/`PA.funded_sim`, che girano su bootstrap INDIPENDENTI e + moltiplicano le probabilita'. + + Ritorna cio' che serve a prezzare il deposito come opzione: + p_pass = P(passare l'eval entro `max_eval_days`) + p_payout = P(arrivare ad ALMENO un payout) <- l'evento che rimborsa la quota + p_pay_given = P(payout | pass) + e_payout = E[payout netti], incondizionato, sui `funded_days` successivi al pass + """ + tgt, dd, dl = pl.FIRMS[[f["name"] for f in pl.FIRMS].index(firm)]["ev"] + ml, dlf = pl.FIRMS[[f["name"] for f in pl.FIRMS].index(firm)]["fu"] + rng = np.random.default_rng(seed) + Rd = R.astype(float) - (1.0 - deluck) * float(np.mean(R)) + total = max_eval_days + funded_days + L = len(Rd) + nb = int(np.ceil(total / block)) + st = rng.integers(0, L - block, size=(n_paths, nb)) + bi = (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :total] + rr = Rd[bi] + gg = G[bi] + + eq = np.ones(n_paths) + phase = np.zeros(n_paths, np.int8) # 0 eval, 1 funded, -1 morto, 2 finito + best_day = np.zeros(n_paths) + fu_day = np.zeros(n_paths, int) + payout = np.zeros(n_paths) + ever = np.zeros(n_paths, bool) + passed = np.zeros(n_paths, bool) + t_pass = np.full(n_paths, -1, int) + p_death_day = 1.0 - (1.0 - pl.FIRM_DEATH_PER_YEAR) ** (1 / 365.0) if firm_death else 0.0 + + for t in range(total): + if p_death_day: + phase[(rng.random(n_paths) < p_death_day) & (phase >= 0)] = -1 + alive = phase >= 0 + if not alive.any(): + break + in_ev = alive & (phase == 0) + in_fu = alive & (phase == 1) + lev = np.where(in_fu, lev_fu, lev_ev) + rt = rr[:, t] * lev + gt = gg[:, t] * lev + eqp = eq + eq = np.where(alive, eq * (1.0 + rt), eq) + eq_low = np.where(alive, eqp * (1.0 + rt + gt), eq) + r_low = rt + gt + bust = ((in_ev & ((eq_low < 1.0 - dd) | (r_low < -dl))) + | (in_fu & ((eq_low < 1.0 - ml) | (r_low < -dlf)))) + phase[bust] = -1 + gain = np.maximum(eq - eqp, 0.0) + best_day = np.where(in_ev & ~bust, np.maximum(best_day, gain), best_day) + profit = eq - 1.0 + ok = in_ev & ~bust & (profit >= tgt) + if consistency: + ok &= best_day <= pl.FIRMS[0].get("consistency", 0.40) * np.maximum(profit, 1e-9) + phase[ok] = 1 + eq[ok] = 1.0 + fu_day[ok] = 0 + passed |= ok + t_pass[ok] = t + still_ev = (phase == 0) & (t >= max_eval_days - 1) + phase[still_ev] = -1 # eval scaduta + now_fu = phase == 1 + fu_day[now_fu & ~ok] += 1 + chk = now_fu & (fu_day > 0) & (fu_day % 30 == 0) + pay = chk & (eq > 1.0) & (((eq - 1.0) * notional * SPLIT) >= min_payout) + payout += np.where(pay, (eq - 1.0) * notional * SPLIT * (1.0 - TAX_RATE), 0.0) + ever |= pay + eq = np.where(pay, 1.0, eq) + phase[now_fu & (fu_day >= funded_days)] = 2 + + return dict(p_pass=float(passed.mean()), p_payout=float(ever.mean()), + p_pay_given=float(ever.sum() / max(passed.sum(), 1)), + e_payout=float(payout.mean()), + e_payout_given=float(payout[ever].mean()) if ever.any() else 0.0, + t_pass=float(np.median(t_pass[passed])) if passed.any() else float("nan")) + + +def ev_three_ways(res: dict, price: float) -> dict: + """Le tre convenzioni sul deposito, sullo STESSO Monte Carlo. + (i) SPESA PERSA — la quota non torna mai (`pc.simulate`, il numero operativo) + (ii) RIMBORSO AL PASS — la quota torna se passi (`r0725_hyro:200`) + (iii) RIMBORSO AL PAYOUT — la quota torna col primo prelievo (la regola VERA della firm) + """ + e, pp, pa = res["e_payout"], res["p_pass"], res["p_payout"] + return dict(sunk=e - price, + at_pass=e - price * (1.0 - pp), + at_payout=e - price * (1.0 - pa), + p_break_even=float(np.clip((price - e) / price, 0.0, 1.0)) if price else 0.0) + + +# ==================================================================================== main +def main() -> None: + try: + sys.stdout.reconfigure(line_buffering=True) + except Exception: # pragma: no cover + pass + print("=" * 104) + print(" GATE PROP-01 gamba (c) — il BIGLIETTO come OPZIONE, il FUNDING dentro, e il") + print(" confronto con l'alternativa ovvia: tenere i $635 sul libro e versare") + print("=" * 104) + print(" PREVISIONE REGISTRATA PRIMA: (1) il rimborso vale +5-15% di EV e +1-3pp su") + print(" P(>=50/g); (2) P(primo payout | pass) > 80%; (3) il funding costa ~0,75 x 2,16%") + print(" su un funded a 0,75x; (4) sul segno del funding di XS01 NON ho una previsione,") + print(" le due forze tirano in verso opposto; (5) EV positivo sulla MEDIA e negativo") + print(" sulla MEDIANA — una lotteria, non un investimento.") + + # ============================================================== §0.1 riproduzione r0725_hyro + hr("§0.1 RIPRODUZIONE — `r0725_hyro` (25/07): l'EV del biglietto come fu pubblicato") + buf = io.StringIO() + t = time.time() + import r0725_hyro as HY # noqa: E402 + with contextlib.redirect_stdout(buf): + HY.main() + txt = buf.getvalue() + lens_now, ev_now = None, {} + for line in txt.splitlines(): + if "[close-only]" in line: + lens_now = "close" + elif "[intraday]" in line: + lens_now = "intra" + m = re.match(r"\s*\$\s*100,000\s+\$\s*([\d,]+)\s+\$\s*([\d,]+)\s+\$\s*([\d,]+)\s+" + r"\$\s*([\-\d,]+)", line) + if m and lens_now: + ev_now[lens_now] = float(m.group(4).replace(",", "")) + print(f" rigirato oggi ({time.time()-t:.0f}s), stessa macchina, stessi semi, biglietto $100k:") + print(f" {'lente':>14} {'EV oggi':>12} {'EV PUBBLICATO 25/07':>22} {'delta':>10}") + for k, lab, p in (("close", "close-only", PUB["hyro_ev_close"]), + ("intra", "intraday", PUB["hyro_ev_intra"])): + v = ev_now.get(k, float("nan")) + print(f" {lab:>14} ${v:>11,.0f} ${p:>21,.0f} {v-p:>+10,.0f}") + print(" Lo scarto NON e' un errore di riproduzione: `r0725_hyro` ri-deriva le proprie serie") + print(" dal feed CORRENTE, e dal 25/07 a oggi il feed ha ~4 settimane di barre in piu'.") + print(" Ordine di grandezza e conclusione identici -> la macchina e' quella.") + print("\n ⚠ E il codice riprodotto contiene gia' la contraddizione: `cost = price*(1-pe)`") + print(" = quota RIMBORSATA AL PASS, mentre `pc.simulate` (da cui esce il numero") + print(" operativo P(>=50/g)) fa `cash -= price` e non la restituisce MAI.") + bump(1) + tick("§0.1") + + # ============================================================== dati comuni + hr("§0.2 IL PANNELLO — le stesse serie di PROP-RECAL (U13 Binance, W-LONG 2021+)") + Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.bin_loader, 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_SUB} + bump(len(PHASES_SUB)) + xs0 = XSP[PHASES_SUB[0]] + cry_idx = pc.crypto_daily_tuples(1.0, 0.0).index + base = cry_idx.intersection(xs0.index) + first13 = xs0.index[np.flatnonzero(xs0["R"].values != 0.0)[0]] + S_LONG = max(first13, pd.Timestamp("2021-01-01", tz="UTC")) + I = base[base >= S_LONG] + ev_b = PA.boot_idx(len(I), EV_H, N_PATHS_J, seed=SEED_EV) + fu_b = PA.boot_idx(len(I), FU_H, N_PATHS_J, seed=SEED_FU) + print(f" W-LONG: {len(I)} giorni {I[0].date()} -> {I[-1].date()} | de-luck x{DELUCK} | " + f"{N_PATHS_J} percorsi per J") + pc.DELUCK = DELUCK + + # J all'ancora canonica, mediana sulle 3 fasi dichiarate + js, jlv = [], [] + for ph in PHASES_SUB: + Rc, Gc = AP.rg_off(W_A, I, XSP[ph], 0) + j, lv, _ = best_lev(Rc, Gc, ev_b, fu_b) + js.append(j); jlv.append(lv) + RL0, GL0 = AP.rg_off(W_LIVE, I, xs0, 0) + jL, lvL, vL0 = best_lev(RL0, GL0, ev_b, fu_b) + dpair = [] + for ph in PHASES_SUB: + Rc, Gc = AP.rg_off(W_A, I, XSP[ph], 0) + _, _, vc = best_lev(Rc, Gc, ev_b, fu_b) + dpair.append(float((vc - vL0).mean())) + print(f" J(W_A 38/38/25) ancora canonica, fasi {PHASES_SUB}: mediana {np.median(js):.3f} " + f"(PUB {PUB['recal_j']:.3f} su 10 fasi) | leve {jlv}") + print(f" J(LIVE 75/25) : {jL:.3f} @{lvL:.2f}x " + f"(PUB {PUB['recal_live']:.3f})") + print(f" delta appaiato mediano : {np.median(dpair):+.3f} " + f"(PUB {PUB['recal_d']:+.3f})") + tick("§0.2") + + # ============================================================== §1 il funding + hr("§1. IL FUNDING, MESSO DENTRO — e il segno su XS01, MISURATO") + rate = funding_rate() + if rate is None: + print(" Senza cache non si mette dentro nulla: il resto gira SENZA funding e lo dichiara.") + xs_ann = 0.0 + else: + fb0 = FD.book_75_25(None, alloc=654.0) + fb1 = FD.book_75_25(rate, alloc=654.0) + d_book = float((fb0 - fb1).dropna().mean() * 365) * 100 + ftp = f_tp01_sleeve(rate) + fsk = f_skh_sleeve(rate, 0) + c0, _ = FD.tp01_canonical(None) + c1, _ = FD.tp01_canonical(rate) + d_tp = float((c0 - c1).dropna().mean() * 365) * 100 + print(f" RIPRODUZIONE dei costi di §35 (%/anno, segno = costo):") + print(f" {'serie':>36} {'oggi':>9} {'PUB §35':>9}") + print(f" {'LIBRO 75/25 path live':>36} {-d_book:>8.2f}% {PUB['fund_book']:>8.2f}%") + print(f" {'TP01 canonico':>36} {-d_tp:>8.2f}% {PUB['fund_tp']:>8.2f}%") + print(f" {'SKH01 50/50 path live (off 0)':>36} " + f"{-float(fsk.mean()*365)*100:>8.2f}% {PUB['fund_skh']:>8.2f}%") + print(f" {'TP01 gambe orarie (lente prop)':>36} " + f"{-float(ftp.reindex(I).mean()*365)*100:>8.2f}% {'—':>9}") + print("\n ⚠ La riga 'gambe orarie' non e' la stessa di 'TP01 canonico': il recon orario") + print(" usa `mc.tp01_daily_frame` (senza min-order, che e' la lente giusta su un") + print(" conto funded da $100k). Le due lenti differiscono di poco e si dichiarano.") + print("\n IL SEGNO DEL FUNDING SU XS01 — misurato, non assunto:") + xsf = xs_funding_measure() + xs_ann = xsf["ann"] + print(f"\n >>> XS01 **PAGA** {xs_ann*100:+.2f}%/anno di funding (segno positivo = costo).") + print(" La previsione registrata ('non lo so, le due forze tirano in verso opposto')") + print(" si risolve cosi': il momentum cross-sezionale COMPRA i perp col funding piu'") + print(" caro e VENDE quelli col funding piu' basso -> paga la dispersione.") + print(" Essendo `sum(W)=0` il LIVELLO non entra: entra solo la DISPERSIONE.") + print(f" Se il funding Hyperliquid e' ~2,1x quello del venue eseguibile (§35),") + print(f" la banda onesta e' [{xs_ann/2.1*100:+.2f}%, {xs_ann*100:+.2f}%]/anno.") + print("\n LA LEVA — perche' il funding non e' un costo fisso:") + print(" il funding e' proporzionale all'ESPOSIZIONE, e l'esposizione di un conto funded") + print(" e' `lev x` quella del libro -> nel simulatore `Rs = R*lev` porta dentro anche il") + print(" costo, che quindi scala. Verifica numerica (libro W_A, costo %/anno di equity):") + fA = fvec_for(W_A, I, rate, 0, xs_ann) + for lv in (0.50, 0.75, 1.00, 1.25): + print(f" leva {lv:>4.2f}x -> {-float(np.mean(fA))*365*100*lv:>6.2f}%/anno " + f"(= {lv:.2f} x {-float(np.mean(fA))*365*100:.2f}%)") + tick("§1") + + # ============================================================== §2 banda d'ancora + hr("§2. LA BANDA D'ANCORA — 23 offset x 3 fasi, con e SENZA funding (riproduzione + delta)") + print(f" Sottocampione dichiarato, identico a `r0822c`: {len(OFFSETS)} offset x " + f"{len(PHASES_SUB)} fasi {PHASES_SUB} = {len(OFFSETS)*len(PHASES_SUB)} celle, " + f"x2 (funding on/off).") + print(f" Ladder: {N_PATHS_LAD} percorsi (risoluzione ~+-1pp su P(>=50/g)), lente ACCOPPIATA,") + print(f" {pl.MAX_CONCURRENT} conti max, morte-firm {pl.FIRM_DEATH_PER_YEAR:.0%}/anno, " + f"deposito NON rimborsato (= convenzione di `pc.simulate`, per riprodurre).") + cells: list[dict] = [] + t = time.time() + for off in OFFSETS: + fA = fvec_for(W_A, I, rate, off, xs_ann) + for ph in PHASES_SUB: + R0, G0 = AP.rg_off(W_A, I, XSP[ph], off) + for tag, Rx in (("nofund", R0), ("fund", R0 - fA if fA is not None else None)): + if Rx is None: + continue + j, lv, _ = best_lev(Rx, G0, ev_b, fu_b) + lad = ladder(Rx, G0, N_PATHS_LAD, lv, SEED_LAD, refund=False) + cells.append(dict(off=off, ph=ph, tag=tag, J=j, lev=lv, + p50=lad["p_target"], pz=lad["p_zero"], med=lad["med"], + ppay=lad["p_payout"])) + bump(2) + C = pd.DataFrame(cells) + print(f" ({time.time()-t:.0f}s)") + print(f"\n {'variante':>12} {'J mediana':>10} {'P(>=50/g)':>10} {'banda p10-p90':>18} " + f"{'P(zero)':>9} {'EUR/g med':>10}") + for tag, lab in (("nofund", "SENZA funding"), ("fund", "CON funding")): + s = C[C["tag"] == tag] + if not len(s): + continue + print(f" {lab:>12} {s['J'].median():>10.3f} {s['p50'].median():>10.1%} " + f"[{np.percentile(s['p50'],10):>7.1%},{np.percentile(s['p50'],90):>7.1%}] " + f"{s['pz'].median():>9.1%} {s['med'].median():>10.2f}") + s_nf = C[C["tag"] == "nofund"] + print(f"\n RIPRODUZIONE §32: P(>=50/g) mediana {s_nf['p50'].median():.1%} " + f"(PUB {PUB['anchor_p50g']:.1%}) | banda " + f"[{np.percentile(s_nf['p50'],10):.1%}, {np.percentile(s_nf['p50'],90):.1%}] " + f"(PUB [{PUB['anchor_p10']:.1%}, {PUB['anchor_p90']:.1%}]) | " + f"P(zero) {s_nf['pz'].median():.1%} (PUB {PUB['anchor_pzero']:.1%})") + if rate is not None: + s_f = C[C["tag"] == "fund"] + m = C.pivot_table(index=["off", "ph"], columns="tag", values="p50") + dpp = (m["fund"] - m["nofund"]) + mj = C.pivot_table(index=["off", "ph"], columns="tag", values="J") + dj = (mj["fund"] - mj["nofund"]) + print(f"\n IL FUNDING, appaiato cella per cella (mediana delle DIFFERENZE, non") + print(f" differenza delle mediane): dJ {dj.median():+.4f} " + f"({int((dj<0).sum())}/{len(dj)} celle negative) | " + f"dP(>=50/g) {dpp.median()*100:+.2f}pp " + f"({int((dpp<0).sum())}/{len(dpp)} negative)") + print(f" Il numero operativo col funding dentro: **P(>=50 EUR/g) " + f"{s_f['p50'].median():.1%}** [{np.percentile(s_f['p50'],10):.1%}, " + f"{np.percentile(s_f['p50'],90):.1%}], P(zero) {s_f['pz'].median():.1%}") + # cella d'ancora MEDIANA su cui si prezza il biglietto + ref_tag = "fund" if rate is not None else "nofund" + S = C[C["tag"] == ref_tag].reset_index(drop=True) + k_med = int((S["p50"] - S["p50"].median()).abs().idxmin()) + OFF_M, PH_M, LEV_M = int(S.loc[k_med, "off"]), int(S.loc[k_med, "ph"]), float(S.loc[k_med, "lev"]) + print(f"\n CELLA D'ANCORA MEDIANA scelta per prezzare il biglietto: offset {OFF_M} min, " + f"fase {PH_M}, leva {LEV_M:.2f}x") + print(" (non l'ancora canonica: quella e' al 91° pctl dei 23 offset — §32)") + fM = fvec_for(W_A, I, rate, OFF_M, xs_ann) + R_M0, G_M = AP.rg_off(W_A, I, XSP[PH_M], OFF_M) + R_M = R_M0 - fM if fM is not None else R_M0 + tick("§2") + + # ============================================================== §3 il biglietto a 2 stadi + hr("§3. IL BIGLIETTO COME OPZIONE — i DUE stadi, sullo stesso percorso") + print(" Stadio 1: passare l'eval (target 10%, maxDD 6% STATICO, daily 4%, consistency 40%).") + print(" Stadio 2: sul funded, arrivare ad ALMENO un payout — che e' l'evento che") + print(" rimborsa il deposito. Soglia: quota trader >= $100 (fonte dichiarativa della firm).") + print(f" {N_PATHS_TIX} percorsi, blocchi di 20 giorni, morte-firm dentro, lente accoppiata.") + rows = [] + for lev_ev in (0.50, 0.75, 1.00): + for lev_fu in (0.50, 0.75, 1.00): + r = ticket_mc(R_M, G_M, lev_ev=lev_ev, lev_fu=lev_fu, notional=100_000.0) + rows.append(dict(lev_ev=lev_ev, lev_fu=lev_fu, + **{k: r[k] for k in + ("p_pass", "p_payout", "p_pay_given", "e_payout", + "e_payout_given", "t_pass")})) + bump(1) + T = pd.DataFrame(rows) + print(f"\n {'leva eval':>10} {'leva fund':>10} {'P(pass)':>9} {'P(payout)':>10} " + f"{'P(pay|pass)':>12} {'E[payout]':>11} {'E[pay|pay]':>12} {'gg al pass':>11}") + for _, x in T.iterrows(): + print(f" {x['lev_ev']:>10.2f} {x['lev_fu']:>10.2f} {x['p_pass']:>9.1%} " + f"{x['p_payout']:>10.1%} {x['p_pay_given']:>12.1%} ${x['e_payout']:>10,.0f} " + f"${x['e_payout_given']:>11,.0f} {x['t_pass']:>11.0f}") + best = T.loc[T["e_payout"].idxmax()] + LEV_EV, LEV_FU = float(best["lev_ev"]), float(best["lev_fu"]) + print(f"\n Leve che massimizzano E[payout]: eval {LEV_EV:.2f}x / funded {LEV_FU:.2f}x") + print(f" (per confronto, la leva che massimizza J alla stessa cella e' {LEV_M:.2f}x: J e'") + print(" P(pass) x P(vivo), che NON e' E[payout] — l'ottimo non deve coincidere)") + pg = T["p_pay_given"] + print(f" >>> PREVISIONE (2) 'P(primo payout | pass) > 80%': vale in " + f"{int((pg > 0.80).sum())}/{len(pg)} celle (banda {pg.min():.1%}-{pg.max():.1%}) -> " + f"{'CONFERMATA salvo a leva funded 1.00x' if (pg > 0.80).sum() >= 6 else 'SMENTITA'}") + print(" Il verso e' meccanico e vale la pena dirlo: piu' leva sul funded alza") + print(" E[payout] e ABBASSA P(payout), perche' fa bustare prima del primo prelievo.") + print(" Sono due obiettivi diversi: chi vuole il RIMBORSO non vuole la leva massima.") + + hr("§3.1 LE TRE CONVENZIONI SUL DEPOSITO — stesso Monte Carlo, tre economie") + print(" (i) SPESA PERSA `pc.simulate` -> EV = E[payout] - prezzo") + print(" (ii) RIMBORSO AL PASS `r0725_hyro` -> EV = E[payout] - prezzo x (1 - P(pass))") + print(" (iii)RIMBORSO AL PAYOUT la firm -> EV = E[payout] - prezzo x (1 - P(payout))") + print(f"\n {'biglietto':>12} {'prezzo':>8} {'E[payout]':>11} {'(i) persa':>11} " + f"{'(ii) al pass':>13} {'(iii) al payout':>16} {'EUR/g atteso':>13}") + tix_rows = [] + for size, price in pl.FIRMS[0]["tickets"]: + r = ticket_mc(R_M, G_M, lev_ev=LEV_EV, lev_fu=LEV_FU, notional=float(size)) + e3 = ev_three_ways(r, price) + tix_rows.append(dict(size=size, price=price, **r, **e3)) + print(f" ${size:>11,} ${price:>7,.0f} ${r['e_payout']:>10,.0f} ${e3['sunk']:>10,.0f} " + f"${e3['at_pass']:>12,.0f} ${e3['at_payout']:>15,.0f} " + f"{e3['at_payout']/EURUSD/365:>13.2f}") + bump(1) + TX = pd.DataFrame(tix_rows) + big = TX.iloc[-1] + rJ = ticket_mc(R_M, G_M, lev_ev=LEV_M, lev_fu=LEV_M, notional=100_000.0) + eJ = ev_three_ways(rJ, 579.0) + bump(1) + print(f"\n Lo STESSO biglietto $100k alla leva che massimizza J ({LEV_M:.2f}x su entrambi") + print(" gli stadi, cioe' la leva con cui e' costruito tutto il resto di questo filone):") + print(f" P(pass) {rJ['p_pass']:.1%} | P(payout) {rJ['p_payout']:.1%} | " + f"E[payout] ${rJ['e_payout']:,.0f} | EV persa ${eJ['sunk']:+,.0f} | " + f"EV al payout ${eJ['at_payout']:+,.0f}") + print(" -> l'EV e' positivo a ENTRAMBE le leve: la conclusione non dipende da quale") + print(" obiettivo (J o E[payout]) si sceglie per tararla.") + d_conv = big["at_payout"] - big["sunk"] + print(f"\n Quanto vale la CORREZIONE (da 'spesa persa' a 'rimborso al payout'), $100k: " + f"${d_conv:+,.0f} = {d_conv/max(abs(big['sunk']),1e-9)*100:+.1f}% dell'EV.") + print(f" Quanto separa le due convenzioni SBAGLIATE fra loro (al pass vs al payout): " + f"${big['at_pass']-big['at_payout']:+,.0f} — cioe' prezzo x (P(pass)-P(payout)) = " + f"${big['price']*(big['p_pass']-big['p_payout']):+,.0f}.") + print(f" >>> PREVISIONE (1) 'il rimborso vale +5-15% di EV': " + f"{'CONFERMATA' if 0.05 <= d_conv/max(abs(big['sunk']),1e-9) <= 0.15 else 'SMENTITA'}") + print(f"\n SENSIBILITA': con E[payout] = ${big['e_payout']:,.0f} e prezzo ${big['price']:,.0f},") + print(f" l'EV al payout resta positivo finche' P(payout) > " + f"{max(0.0,(big['price']-big['e_payout'])/big['price']):.1%} — misurata " + f"{big['p_payout']:.1%}.") + print(" ⚠ E' un EV in DOLLARI ATTESI. Su un conto da $654 il biglietto si compra poche") + print(" volte: la media non e' la grandezza che decide. Si guarda la distribuzione (§5).") + print(" ⚠ RICONCILIAZIONE (tre P(payout) diverse compaiono in questo file, e NON sono in") + print(" contraddizione): qui e' UN biglietto su UN anno di funded; la scala di conti") + print(" (§4-§6) lascia correre 36 mesi e ricompra -> la sua P e' piu' alta. Confrontare") + print(" le due senza dire la finestra e' l'errore che le farebbe sembrare incoerenti.") + tick("§3") + + # ============================================================== §4 ladder col rimborso + hr("§4. LA SCALA DI CONTI COL DEPOSITO RIMBORSABILE") + print(" §4.1 CONTROLLO — con `refund=False, deposito=0, min_payout=0` il mio simulatore") + print(" dev'essere BIT-EXACT rispetto a `pc.simulate` (stesso seme, stesso stream).") + cfg = tuple(W_A.get(n, 0.0) for n in PA.CRYPTO_PROP) + key = (cfg[0], cfg[1], cfg[2], "shared") + saved = pc._TUP_CACHE.pop(key, None) + pc._TUP_CACHE[key] = (R_M, G_M, I) + ref = pc.simulate([cfg], [0] * pl.MAX_CONCURRENT, n_paths=N_PATHS_LAD, lev=LEV_M, + seed=SEED_LAD, lens="coupled", start=str(I[0].date())) + pc._TUP_CACHE.pop(key, None) + if saved is not None: + pc._TUP_CACHE[key] = saved + mine = ladder(R_M, G_M, N_PATHS_LAD, LEV_M, SEED_LAD, refund=False) + dmax = float(np.abs(np.sort(ref["eur_day"]) - np.sort(mine["eur_day"])).max()) + dmax2 = float(np.abs(ref["eur_day"] - mine["eur_day"]).max()) + print(f" max|diff| percorso per percorso = {dmax2:.3e} (ordinati: {dmax:.3e}) -> " + f"{'IDENTICO' if dmax2 < 1e-12 else 'DIVERGE — STOP'}") + if dmax2 >= 1e-12: + print(" Il simulatore non replica: i numeri sotto sarebbero di un'altra macchina.") + return + bump(1) + + print("\n §4.2 QUANTO VALE IL RIMBORSO (cella d'ancora mediana, stessi semi)") + print(f" {'convenzione':>34} {'EUR/g med':>10} {'P(>=10/g)':>10} {'P(>=50/g)':>10} " + f"{'P(zero)':>9} {'cassa fin. med':>15} {'media':>10}") + lad_rows = {} + for tag, rf, mp in (("spesa persa (pubblicato)", False, 0.0), + ("spesa persa + soglia $100", False, MIN_PAYOUT_TRADER), + ("RIMBORSO al primo payout", True, MIN_PAYOUT_TRADER)): + r = ladder(R_M, G_M, N_PATHS_LAD, LEV_M, SEED_LAD, refund=rf, min_payout=mp) + lad_rows[tag] = r + print(f" {tag:>34} {r['med']:>10.2f} {r['p_ge10']:>10.1%} {r['p_target']:>10.1%} " + f"{r['p_zero']:>9.1%} ${r['med_cash']:>14,.0f} ${r['mean_cash']:>9,.0f}") + bump(1) + a = lad_rows["spesa persa + soglia $100"] + b = lad_rows["RIMBORSO al primo payout"] + print(f"\n Delta del rimborso: P(>=50/g) {(b['p_target']-a['p_target'])*100:+.2f}pp | " + f"EUR/g mediana {b['med']-a['med']:+.2f} | cassa finale mediana " + f"${b['med_cash']-a['med_cash']:+,.0f} | quota restituita media ${b['mean_back']:,.0f}") + print(f" >>> PREVISIONE (1) 'il rimborso vale +1-3pp su P(>=50/g)': " + f"{'CONFERMATA' if 0.01 <= (b['p_target']-a['p_target']) <= 0.03 else 'SMENTITA'} " + f"({(b['p_target']-a['p_target'])*100:+.2f}pp)") + tick("§4") + + # ============================================================== §5 biglietto vs versare + hr("§5. IL BIGLIETTO CONTRO IL VERSARE — stessa metrica, stesso orizzonte, stessi sorteggi") + print(" Strada A (BIGLIETTO): $654 comprano il biglietto piu' grande possibile; la scala di") + print(" conti gira il libro W_A 38/38/25 sul conto della firm (XS01 LI' E' ESEGUIBILE),") + print(" deposito rimborsabile, morte-firm, cap $200k/firm; la cassa resta FERMA.") + print(" Strada B (LIBRO): gli stessi $654 restano sul libro LIVE 75/25 a min-order $5") + print(" (XS01 NON e' eseguibile a $654), nessuna barriera, fisco 33% sull'anno.") + print(" ⚠ L'asimmetria fra le due strade e' REALE ed e' il punto del canale funded: il") + print(" conto della firm da' accesso a sleeve che il capitale proprio non puo' eseguire.") + rb_full = FD.book_75_25(rate, alloc=654.0) if rate is not None else FD.book_75_25(None, alloc=654.0) + if rb_full.index.tz is None: + rb_full.index = rb_full.index.tz_localize("UTC") + rb = np.nan_to_num(rb_full.reindex(I).values.astype(float)) + print(f"\n libro LIVE a $654 (funding {'DENTRO' if rate is not None else 'FUORI'}): " + f"drift {rb.mean()*365*100:.2f}%/anno, vol {rb.std()*np.sqrt(365)*100:.1f}%, " + f"Sharpe {rb.mean()*365/(rb.std()*np.sqrt(365)):.2f} su {len(rb)} giorni") + rngb = np.random.default_rng(51224) + nb = int(np.ceil(HORIZON_DAYS / 20)) + st = rngb.integers(0, len(I) - 20, size=(N_PATHS_LAD, nb)) + BOOT = (st[:, :, None] + np.arange(20)[None, None, :]).reshape(N_PATHS_LAD, -1)[:, :HORIZON_DAYS] + print(f" bootstrap CONDIVISO fra le due strade: {N_PATHS_LAD} percorsi x {HORIZON_DAYS} " + f"giorni, blocchi 20 -> confronto APPAIATO") + print(f"\n {'versamento':>12} {'strada':>10} {'mediana':>11} {'p10':>10} {'p90':>10} " + f"{'media':>11} {'P(sotto il versato)':>20} {'P(>=50/g)':>10}") + cmp_rows = [] + for dep in (0.0, 250.0, 500.0): + A = ladder(R_M, G_M, N_PATHS_LAD, LEV_M, SEED_LAD, refund=True, + deposit_eur=dep, min_payout=MIN_PAYOUT_TRADER, boot=BOOT) + B = book_road(rb, BOOT, pl.START_EUR, dep) + bump(2) + for lab, r in (("BIGLIETTO", A), ("LIBRO", B)): + p_loss = float((r["cash"] < r["contributed"]).mean()) + p50g = r.get("p_target", 0.0) + print(f" {f'EUR {dep:,.0f}/mese':>12} {lab:>10} ${r['med_cash']:>10,.0f} " + f"${r['p10_cash']:>9,.0f} ${r['p90_cash']:>9,.0f} ${r['mean_cash']:>10,.0f} " + f"{p_loss:>20.1%} {p50g:>10.1%}") + cmp_rows.append(dict(dep=dep, road=lab, med=r["med_cash"], mean=r["mean_cash"], + p10=r["p10_cash"], p90=r["p90_cash"], ploss=p_loss)) + print(f" {'':>12} {'delta':>10} ${A['med_cash']-B['med_cash']:>+10,.0f} " + f"{'':>10} {'':>10} ${A['mean_cash']-B['mean_cash']:>+10,.0f}") + CMP = pd.DataFrame(cmp_rows) + print("\n ('P(>=50/g)' e' definita solo per la strada BIGLIETTO: la strada LIBRO non preleva") + print(" durante l'accumulo. La colonna che si confronta e' la RICCHEZZA TERMINALE.)") + tick("§5") + + # ============================================================== §6 la curva del capitale + hr("§6. LA CURVA DEL CAPITALE — a che conto il biglietto smette di essere la strada migliore") + print(" Stesso confronto appaiato, capitale iniziale variabile, nessun versamento (cosi' la") + print(" sola variabile e' il capitale). La strada LIBRO resta il 75/25 a ogni taglia:") + print(" e' una scelta CONSERVATIVA CONTRO la strada libro (a $5k/$20k il libro potrebbe") + print(" aggiungere XSR01/XS01) -> il capitale d'incrocio misurato e' un LIMITE SUPERIORE.") + cheap = min(t[1] for F in pl.FIRMS for t in F["tickets"]) + print(f"\n {'capitale':>10} {'A mediana':>12} {'B mediana':>12} {'A media':>12} " + f"{'B media':>12} {'A P(0 pay)':>11} {'A P(<${:.0f})'.format(cheap):>12} " + f"{'A med|>0':>10} {'vince med.':>11}") + caps = (600.0, 1_000.0, 2_000.0, 3_500.0, 6_000.0, 10_000.0, 20_000.0, 40_000.0) + cross_med = cross_mean = None + prev = None + for ce in caps: + A = ladder(R_M, G_M, N_PATHS_LAD, LEV_M, SEED_LAD, refund=True, start_eur=ce, + min_payout=MIN_PAYOUT_TRADER, boot=BOOT) + B = book_road(rb, BOOT, ce, 0.0) + bump(2) + wm = "BIGLIETTO" if A["med_cash"] > B["med_cash"] else "LIBRO" + nz = A["cash"][A["cash"] >= cheap] + p_stuck = float((A["cash"] < cheap).mean()) + print(f" EUR{ce:>7,.0f} ${A['med_cash']:>11,.0f} ${B['med_cash']:>11,.0f} " + f"${A['mean_cash']:>11,.0f} ${B['mean_cash']:>11,.0f} {A['p_zero']:>11.1%} " + f"{p_stuck:>12.1%} ${np.median(nz) if len(nz) else 0:>9,.0f} {wm:>11}") + cur = (A["med_cash"] > B["med_cash"], A["mean_cash"] > B["mean_cash"]) + if prev is not None: + if prev[0] != cur[0] and cross_med is None: + cross_med = (prev[2], ce) + if prev[1] != cur[1] and cross_mean is None: + cross_mean = (prev[2], ce) + prev = (cur[0], cur[1], ce) + print(f"\n Incrocio sulla MEDIANA: {cross_med if cross_med else 'nessuno nella griglia'}") + print(f" Incrocio sulla MEDIA : {cross_mean if cross_mean else 'nessuno nella griglia'}") + + print("\n ⚠ IL 'GRADINO' FRA EUR 1.000 E EUR 2.000 NON E' UN MECCANISMO: e' la MEDIANA") + print(" che cambia MODO. La distribuzione della strada-biglietto e' BIMODALE — o si") + print(" finisce SENZA nemmeno i soldi per ricomprare il biglietto piu' economico, o si") + print(f" finisce con migliaia. La colonna `P(<${cheap:.0f})` e' la massa del modo basso e") + print(" si muove LISCIA; la mediana complessiva salta di un ordine di grandezza appena") + print(" quella massa attraversa il 50%. **La mediana non e' una statistica robusta qui**:") + print(" quelle robuste sono `P(<$249)`, `P(0 payout)` e la media, che SATURANO.") + print(" -> il 'capitale d'incrocio' e' il punto in cui una massa di probabilita' passa") + print(" il 50%, NON una soglia con un meccanismo dietro. Va detto, perche' un numero") + print(" cosi' netto e' esattamente quello che si finirebbe per citare come soglia.") + print("\n §6.1 LA SECONDA FIRM — l'unico ingrediente STRUTTURALE che cambia con la cassa") + print(" (FTMO: max loss 10% contro il 6% STATICO di HYRO — il vincolo che il") + print(" progetto misura come binding su questo libro fin dal 25/07).") + print(" Si legge su MEDIA e P(0 payout), non sulla mediana (vedi sopra).") + all_firms = pl.FIRMS + print(f"\n {'capitale':>10} {'firm ammesse':>16} {'mediana':>11} {'media':>11} " + f"{'P(0 payout)':>11} {'cassa med':>11}") + for ce in (1_000.0, 2_000.0, 3_500.0): + for lab, fl in (("solo HYRO", [all_firms[0]]), ("HYRO+FTMO", all_firms)): + pl.FIRMS = fl + try: + A = ladder(R_M, G_M, N_PATHS_LAD, LEV_M, SEED_LAD, refund=True, start_eur=ce, + min_payout=MIN_PAYOUT_TRADER, boot=BOOT) + finally: + pl.FIRMS = all_firms + bump(1) + print(f" EUR{ce:>7,.0f} {lab:>16} ${A['med_cash']:>10,.0f} " + f"${A['mean_cash']:>10,.0f} {A['p_zero']:>11.1%} ${np.median(A['cash']):>10,.0f}") + print("\n Lettura: la seconda firm ALZA la media (piu' nozionale, e se una firm muore la") + print(" cassa si ricicla sull'altra) e puo' ALZARE anche P(0 payout) al capitale piccolo,") + print(" perche' converte in biglietti la cassa che altrimenti sarebbe rimasta ferma.") + print(" Non e' un pasto gratis: e' piu' esposizione allo stesso rischio binario.") + tick("§6") + + # ============================================================== §7 verdetto + hr("§7. LA GAMBA (c), RIFORMULATA — e la risposta alla domanda che decide") + ev_pay = float(big["at_payout"]) + ev_sunk = float(big["sunk"]) + A0 = [r for r in cmp_rows if r["dep"] == 0.0 and r["road"] == "BIGLIETTO"][0] + B0 = [r for r in cmp_rows if r["dep"] == 0.0 and r["road"] == "LIBRO"][0] + print(f" EV del biglietto $100k alla cella d'ancora MEDIANA, col funding dentro:") + print(f" convenzione VERA (rimborso al primo payout) ${ev_pay:>+10,.0f}") + print(f" convenzione PESSIMISTA (spesa persa) ${ev_sunk:>+10,.0f}") + print(f" -> EV positivo in {'ENTRAMBE' if min(ev_pay, ev_sunk) > 0 else 'UNA SOLA' if max(ev_pay,ev_sunk)>0 else 'NESSUNA'} " + f"le convenzioni.") + print(f"\n A 36 mesi, senza versamenti, $654 iniziali (confronto appaiato):") + print(f" BIGLIETTO mediana ${A0['med']:,.0f} media ${A0['mean']:,.0f} " + f"p10 ${A0['p10']:,.0f} P(sotto il versato) {A0['ploss']:.1%}") + print(f" LIBRO mediana ${B0['med']:,.0f} media ${B0['mean']:,.0f} " + f"p10 ${B0['p10']:,.0f}") + med_win = "BIGLIETTO" if A0["med"] > B0["med"] else "LIBRO" + mean_win = "BIGLIETTO" if A0["mean"] > B0["mean"] else "LIBRO" + print(f" -> sulla MEDIANA vince {med_win}; sulla MEDIA vince {mean_win}.") + print(" (e la mediana qui e' una statistica BIMODALE: dice da che parte del 50% sta") + print(" P(0 payout), non 'quanto' si guadagna — vedi §6)") + print(f" >>> PREVISIONE (5) 'EV positivo sulla MEDIA e negativo sulla MEDIANA': " + f"{'CONFERMATA' if (mean_win=='BIGLIETTO' and med_win=='LIBRO') else 'SMENTITA'}") + print("\n ONESTA' OBBLIGATORIE, portate avanti e non nascoste:") + print(f" * morte-firm {pl.FIRM_DEATH_PER_YEAR:.0%}/anno e' un'ASSUNZIONE del progetto") + print(" (24/07), non una misura: sopra c'e' dentro, ma nessuno l'ha stimata.") + print(" * la RIMBORSABILITA' e' DICHIARATIVA: viene dalla pagina della firm + fonti") + print(" secondarie concordi. Nessuno l'ha vista accadere. Il progetto ha gia' pagato") + print(" per un'assunzione mai verificata sul conto reale (GTAA01/PRIIPs).") + print(f" * il cap ${pl.FIRMS[0]['cap']:,}/trader limita STRUTTURALMENTE il canale: il") + print(" nozionale massimo su HYRO+FTMO e' $600k, cioe' un tetto al reddito, non al rischio.") + print(" * XS01 sul conto funded e' misurato su U13 fuori campione, ma le 6 gambe che") + print(" fuori campione non esistono restano non testabili (§29).") + print(" * l'ancora GIORNALIERA di TP01 (24 ore) NON e' in questa banda: terza dimensione,") + print(" dichiarata anche da §32, e la sua fortuna e' ancora dentro questi numeri.") + + hr("§7.1 IL TESTO PROPOSTO DELLA GAMBA (c)") + price = float(big["price"]) + p_nopay = 1.0 - float(big["p_payout"]) + print(f" Quantita' misurate oggi, a costo ZERO (una corsa di questo script):") + print(f" prezzo del biglietto $100k ${price:>10,.0f}") + print(f" quota del conto ($654) immobilizzata {price/(pl.START_EUR*EURUSD):>10.1%}") + print(f" P(nessun payout) -> quota persa {p_nopay:>10.1%}") + print(f" PERDITA ATTESA = prezzo x P(nessun payout) ${price*p_nopay:>10,.0f}") + print(f" EV con deposito NON rimborsato (pessimista) ${ev_sunk:>+10,.0f}") + print(f" EV con deposito rimborsato al payout (vero) ${ev_pay:>+10,.0f}") + print(f"\n Mesi di piano che il biglietto mette a rischio (quota / versamento mensile):") + for dep in (250.0, 500.0, 800.0): + print(f" a EUR {dep:>4,.0f}/mese: {price/(dep*EURUSD):>4.1f} mesi di versamenti " + f"(perdita attesa = {price*p_nopay/(dep*EURUSD):.1f} mesi)") + print("\n IL PUNTO DI STRUTTURA, che decide la FORMA del gate:") + print(" * misurare l'EV costa UNA CORSA DI QUESTO SCRIPT — zero dollari, minuti;") + print(" * misurare la RIMBORSABILITA' costa $579 e si puo' fare SOLO comprando.") + print(" Un gate che aspettasse la seconda misura sarebbe un gate che si puo' superare solo") + print(" dopo averlo violato. Percio' la gamba (c) va scritta in modo che la") + print(" rimborsabilita' NON decida: si richiede EV positivo sotto la convenzione") + print(" PESSIMISTA (quota mai restituita). Il rimborso diventa allora un regalo, non") + print(" un'ipotesi su cui si e' scommesso.") + print("\n >>> TESTO PROPOSTO — `GATE PROP-01 (c) CAPITALE`, due condizioni CONGIUNTE:") + print(" (c1) ECONOMICA, misura a costo zero, da rifare a ogni cambio del libro o") + print(" delle regole della firm:") + print(" EV del biglietto > 0 con la quota trattata come SPESA PERSA, calcolato") + print(" alla cella d'ancora MEDIANA (non canonica), col FUNDING dentro e sotto") + print(" la lente ACCOPPIATA, con `r0822d_gate_c.py`.") + print(f" Lettura di oggi: ${ev_sunk:+,.0f} -> " + f"{'PASS' if ev_sunk > 0 else 'FAIL'}") + print(" (c2) DI SOPRAVVIVENZA, verificabile dal saldo e dal piano di versamento:") + print(" prezzo del biglietto <= 2 versamenti mensili del piano in corso.") + print(" Motivo: il piano di accumulo e' la leva che il progetto ha misurato come") + print(" dominante (26/07: da 'mai' a 16 anni), e zero e' ASSORBENTE. Un biglietto") + print(" che costa piu' di due mensilita' non e' una scommessa sul canale funded:") + print(" e' un ritardo del piano che lo finanzia.") + print(f" Lettura di oggi (piano dichiarato EUR 500/mese, 26/07): " + f"${price:,.0f} vs 2 x ${500*EURUSD:,.0f} = ${2*500*EURUSD:,.0f} -> " + f"{'PASS' if price <= 2*500*EURUSD else 'FAIL'}") + print(" SCADENZA: nessuna. (c1) e (c2) sono verificabili OGGI; un gate d'attesa qui") + print(" non avrebbe un dato da aspettare, e stanotte due date sono cadute per questo.") + print(" Se si compra, si registra PRIMA: il rimborso deve comparire col primo payout;") + print(" se non compare, la convenzione vera e' 'spesa persa' — che (c1) ha gia'") + print(" assunto, quindi la decisione non va rifatta, va solo annotata.") + + print(f"\n TRIAL/CELLE DICHIARATI (al rialzo): {TRIALS}") + print(f" fatto in {time.time()-T0:.0f}s") + print("=" * 104) + + +if __name__ == "__main__": + main() From 1d513632c73e8d9d9c5623dca08050a4f6fd172e Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 23:37:14 +0000 Subject: [PATCH 051/114] CLAUDE.md: GATE PROP-01 chiuso 3/3 senza date; P(50/g) onesto 2,6%; XS01 paga funding; il biglietto batte il versare solo perche' il piano esiste --- CLAUDE.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index d98b731..55b4a64 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2512,6 +2512,61 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis lineare 2019-21 fosse costato quanto il 2022+, la colonna vera e' **L3b**); il modello fiscale tassa la **variazione annua di valore** = limite superiore; il bootstrap gira su **7,4 anni con due tori**. +- ⚖️ **CANALE FUNDED — `GATE PROP-01` CHIUSO 3/3 in una notte, e il numero onesto e' 2,6% + (2026-08-22).** Registro §29-32, §37. **Libro, pesi, cron, config INVARIATI.** + Il gate nato ieri con **due gambe datate ad anni** e' stato chiuso **tutto oggi**, e ogni chiusura + ha peggiorato il numero: + | gamba | criterio | esito | + |---|---|---| + | **(a) LISTINO** | ≥10 delle 13 gambe shortabili | ✅ **PASS 13/13** — misurato sul **venue** (Bybit `instruments-info`, 833 strumenti), non sul sito | + | **(b) ANCORA SKH01** | delta appaiato > +0,05 sui 23 offset | ✅ **PASS 23/23**, +0,116, 230/230 celle congiunte | + | **(c) CAPITALE** | *riformulata, vedi sotto* | ✅ **PASS 2/2** | + 📌 **La (c) e' stata RIFORMULATA perche' era una constatazione, non un criterio** — e la sua forma e' + **imposta dal costo della misura**: l'EV costa una corsa di script (**zero dollari**), la + **rimborsabilita' del deposito costa $579 e si puo' misurare solo COMPRANDO** → un gate d'attesa su + quella sarebbe **superabile solo dopo averlo violato**. Quindi: + **(c1) ECONOMICA** — EV del biglietto **> 0 con la quota trattata come SPESA PERSA**, alla cella + d'ancora **mediana** (non canonica), col **funding dentro**, lente **accoppiata**. *Oggi **$+1.615***. + **(c2) DI SOPRAVVIVENZA** — prezzo del biglietto **≤ 2 versamenti mensili del piano in corso** + (il piano d'accumulo e' la leva dominante misurata, e **zero e' assorbente**). *Oggi **$579 contro + $1.090***. **(c1) e' scritta perche' la rimborsabilita' NON decida:** il rimborso e' **un regalo, non + un'ipotesi su cui si e' scommesso**. + 🚨 **IL NUMERO, e la catena di un giorno solo: P(≥50 €/g) da €600 in 36 mesi = 42% → 7,8% → 4,4% → + 2,6% [1,5%, 4,7%], con P(zero) 40,3%.** Nell'ordine: universo fuori campione (−42% del J stava in 6 + gambe che nel 2021-23 non esistevano) · banda d'ancora di SKH01 (canonica al 91° pctl) · **funding** + (dJ −0,049 su **69/69** celle appaiate). + 📌 **EV del biglietto POSITIVO in tutte e tre le convenzioni sul deposito** (spesa persa +$1.615 · + rimborso al pass +$1.919 · rimborso al primo payout +$1.835). ⚠️ **E il progetto conteneva GIA' due + modelli contraddittori della quota:** `r0725_hyro:200` la rimborsa **al pass**, `pc.simulate` **mai** + — e il numero operativo usciva dal secondo. + 🚨 **XS01 PAGA il funding, non lo incassa — meccanismo DIMOSTRATO:** `max|ΣW| = 2,8e-17` + (dollar-neutral esatto) ⇒ **il livello del funding non entra, entra solo la DISPERSIONE + cross-sezionale**, e il momentum compra i perp col funding **piu' caro** → **+1,45%/anno** + [IC95 +1,18/+1,72; banda onesta +0,69/+1,45 se HL e' ~2,1× il venue]. *L'ipotesi opposta ("market- + neutral ⇒ si compensa") era esplicita nel briefing ed e' falsa.* + 📌 **Piu' leva sul funded ALZA E[payout] e ABBASSA P(payout): sono due obiettivi diversi**, e chi + vuole il rimborso non vuole la leva massima. + **IL CONFRONTO CHE DECIDE** (36 mesi, $654, stesso bootstrap appaiato, funding e fisco in entrambe): + | versamento | strada | mediana | media | P(≥50/g) | + |---|---|---|---|---| + | €0 | BIGLIETTO | $132 | **$9.592** | 4,2% | + | €0 | LIBRO | **$787** | $803 | 0,0% | + | €500/m | **BIGLIETTO** | **$39.820** | **$46.298** | **13,7%** | + | €500/m | LIBRO | $22.444 | $22.694 | 0,0% | + **`IL BIGLIETTO CONTRO IL VERSARE: MEGLIO — ma solo perche' il piano di versamento c'e', e la ragione + non e' il rendimento, e' il NUMERO DI VOLTE CHE SI PUO' GIOCARE.`** Senza versamenti la stessa + scommessa **perde la mediana 6× e vince la media 12×**: una lotteria a EV positivo che un conto da + $654 puo' giocare **una volta sola**. *(Il libro da solo fa P(≥50/g) 0,0% a 36 mesi **per struttura**: + non e' un difetto del libro, e' l'orizzonte.)* + ⚠️ **Cio' su cui poggia tutto, dichiarato:** il drift di XS01 misurato su **13 gambe su 19**; la + **morte-firm ~10%/anno** e' un'**assunzione** (se fosse ≫ l'EV si azzera **senza che nulla nel + modello lo segnali**); il cap **$200k/trader** limita il canale per struttura. + ⚠️ **Un numero netto catturato PRIMA di pubblicarlo:** il "capitale d'incrocio €1.000-2.000" **non e' + un meccanismo** — la ricchezza terminale della strada-biglietto e' **bimodale**, `P(cassa < $249)` si + muove **liscia** (56,3 → 0,0%) mentre la **mediana salta di un ordine di grandezza** appena quella + massa passa il 50%. **Li' la mediana non e' robusta**, ed e' esattamente il tipo di numero netto che + si sarebbe finito per citare come soglia di progetto. + - **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso: cross-sectional su universo Hyperliquid certificato (XS01) → portafoglio Sharpe ~1.55. - **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.** From ef30c20af59a52d617046cab143eeb2eb2cb374b Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 23:57:48 +0000 Subject: [PATCH 052/114] =?UTF-8?q?research(wave-0822):=20SCALE-SPEC=20?= =?UTF-8?q?=E2=80=94=20chiave=20specificata,=20il=20tetto=20va=20sul=20PRO?= =?UTF-8?q?DOTTO,=20e=20il=20test=20di=20guardia=20non=20si=20rompe=20ma?= =?UTF-8?q?=20smette=20di=20controllare;=20corretto=20un=20mio=20numero=20?= =?UTF-8?q?conflato?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 65 ++- docs/research/SPEC-scale-key.md | 618 ++++++++++++++++++++++ scripts/research/r0822e_scale_proto.py | 698 +++++++++++++++++++++++++ 3 files changed, 1380 insertions(+), 1 deletion(-) create mode 100644 docs/research/SPEC-scale-key.md create mode 100644 scripts/research/r0822e_scale_proto.py diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index d992434..791e307 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -703,6 +703,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 35 | FUNDING | il costo mai prezzato di TENERE una posizione aperta | **LEAD** — **−2,16%/anno di drift**, muri **+17,5%** | | 36 | PIANO-VERO | la tabella congiunta fisco+funding, che non esisteva | 🚨 **€250/m: P(20a) 92% → 14-26%** | | 37 | GATE-C | l'ultima gamba del gate funded + il biglietto come opzione | **PASS 2/2** — gate senza date; **P(≥50/g) 4,4% → 2,6%** | +| 38 | SCALE-SPEC | specificare il knob di leva che non esiste | **LEAD** — specificata, **1 nodo irrisolto** sul disaster-SL | | 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 | @@ -1503,7 +1504,7 @@ l'hold-out di TP01. E' la **terza** dimensione, e l'agente la dichiara. massimo scelto su cui barare, quindi non c'e' un DSR da calcolare*. Il filone che il critico dell'ondata aveva indicato come **la misura di maggior valore mai fatta**: -il gradino di leva **1,00x → 1,25-1,50x** vale **14,7 → 11,6 anni** al capitale-rendita (~€300/mese di +il gradino di leva vale **14,7a → 12,9a a 1,25x** e **→ 11,6a a 1,50x** al capitale-rendita (⚠️ **correzione 23/08**: la prima stesura scriveva *"1,25-1,50x vale 14,7 → 11,6a, ~€300/mese"* **fondendo due gradini** — il ~€300/mese e' del **1,50x**; a **1,25x** l'equivalente e' **+€164/mese**, ricalcolato sulla tabella netta) (~€282/mese a 1,50x di versamenti equivalenti, a costo zero) ed era bloccato da **un solo parametro scelto a mano**: *"un giorno a −10% ogni anno"*. @@ -1853,3 +1854,65 @@ scommessa **perde la mediana 6×** ($132 contro $787) e **vince la media 12×**: atteso positivo che un conto da $654 puo' permettersi di giocare **una volta sola**. ⚠️ E il vantaggio poggia **interamente** sul drift di XS01 misurato su **13 gambe su 19**. **Smentitore che azzera tutto senza che il modello lo segnali:** una **morte-firm ≫10%/anno**. + +--- + +## 38 — SCALE-SPEC (la chiave di scala che non esiste, specificata) + +`docs/research/SPEC-scale-key.md` (38 KB) + `scripts/research/r0822e_scale_proto.py` (1,0 s, zero +rete, **zero scritture**). **~630 valutazioni, nessuna cella scelta** → niente DSR. +✅ **Produzione non toccata** (`git status` vuoto su `src/`, `config/`, `scripts/live/`, `tests/`), e +la sez. 0 del prototipo **verifica l'isolamento a runtime** — *ed e' fallita su se stessa alla prima +stesura, che e' il modo giusto per un controllo di scoprire che funziona.* +✅ **Replica a k=1,00 superata 6/6** contro WORST-DAY (peggior giorno −3,95% il 2020-03-13, bound +strutturale −14,32%, morso del clamp 3/5.648 contro 3/5.650, drift e vol al centesimo). + +| | k=1,00 | **k=1,25** | +|---|---|---| +| Sharpe FULL | 1,5051 | **1,5051** (invariante) | +| drift / vol | 17,11% / 11,37% | 21,39% / 14,21% | +| g(k) esatto (CAGR geom.) | 17,90% | **22,61%** | +| maxDD storico | −12,17% | −15,01% | +| peggior giorno possibile | −14,32% | −17,90% | +| **costo di un disaster-SL** | 30,0% | **37,5%** ← la guardia che morde | +| margine di liquidazione | 15,8x | 10,1x | + +⚠️ **CORREZIONE A UN NUMERO CHE AVEVO SCRITTO IO** (nel briefing e in CLAUDE.md): *"il gradino +1,00x→1,25-1,50x vale 14,7 → 11,6 anni, ~€300/mese"* **fondeva due gradini**. §2 da' **1,25x → 12,9a** +e **1,50x → 11,6a**; il ~€300/mese e' del **1,50x**. Ricalcolato sulla tabella netta: **1,25x = ++€164/mese equivalenti** (l'interpolazione riproduce €282 per 1,50x = controllo superato). + +**Tre risultati che il mandato non prevedeva:** +1. 🚨 **`test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` NON si rompe — ed e' PEGGIO che + rompersi.** Misura `frac · n_asset` mentre la grandezza vera diventerebbe `frac · n_asset · scala` + → **continua a passare e smette di controllare**. **4ª occorrenza della forma "un controllo puntato + su una configurazione diversa da quella che gira"** (dopo `fee_watch` sugli inverse, la taratura di + `venue_watch` con bitfinex, i test di `book_live` senza potenza a libro flat). **Va cancellato con + nota, non aggiustato.** +2. 🚨 **La guardia che morde per prima e' il DISASTER-SL, non il peggior giorno:** dal bound + strutturale k ≤ 3,49x, **dal disaster-SL k ≤ 1,67x** = **2,1× piu' stringente**. **C1 di WORST-DAY + guardava la grandezza sbagliata.** → il tetto giusto e' l'invariante + `n_asset · frac · scala · disaster_sl_pct ≤ 0,50`, che lega **tre** chiavi. +3. 🚨 **Il tetto va sul PRODOTTO, non sulla chiave:** `frac=0,625` + `scala=1,25` fa leva **1,562x** e + **passerebbe** un tetto *"scala ≤ 1,25"*. Ed e' concreto: WORST-DAY ha appena pubblicato che alzare + `frac` da solo *"non produce k=1,25"* — **combinato con la scala lo produce eccome**. + +**Gate proposto `GATE SCALA-01`** — 9 condizioni, fra cui **A2** (180 giorni sul gradino precedente), +**A5** (tetto sul **prodotto**), **A6 anti-recency** (`k_ammesso` calcolato **togliendo gli ultimi 90 +giorni** dev'essere ≥ k richiesto) e **A9** (11 test verdi), piu' un sorvegliante giornaliero a **tre +stati**. Nasce dal fatto misurato che **la scala e' invisibile a tutti i gate esistenti**: Sharpe +**1,5051 identico** a tutte e 5 le celle di k → `deflated_sharpe` e `marginal_vs_tp01` **non +falliscono, non vedono la variabile**. ✅ `weights_tilt_null` invece **passa e con potenza**: peso +implicito TP01 **0,750000 a ogni k**, `max|target(k) − k·target(1)| = 0,00e+00`, e il **controllo +positivo** (scala sulla sola gamba TP01 = cio' che farebbe `target_vol`) lo porta a **0,789** e viene +smascherato. + +**VERDETTO: LEAD** (specifica di processo). +**`LA CHIAVE DI SCALA: SPECIFICATA MA CON UN NODO IRRISOLTO`** — `execution.py:196-223` **ri-ancora +il disaster-SL al mark a ogni giro** (tolleranza 5%): il **−30% e' ROTOLANTE, non un pavimento**. +Se e' cosi', il 37,5% e' il costo di **un episodio** e un ribasso lento ne produce **piu' d'uno**, e +il margine 1,33x del gradino non e' quello vero. ⚠️ **Letto nel codice, NON misurato** — e il log non +lo permette (servirebbe `pos_prima`): **non ricostruibile a posteriori.** +**Secondo smentitore, con soglia dichiarata:** il ramo di **fallback dell'equity** oggi scatta +**0 volte su 1.442 giri**; se superasse il **2% dei giri in 90 giorni** il libro girerebbe a leva +**mista** e `g(k)` smetterebbe di descriverlo. diff --git a/docs/research/SPEC-scale-key.md b/docs/research/SPEC-scale-key.md new file mode 100644 index 0000000..b9fc593 --- /dev/null +++ b/docs/research/SPEC-scale-key.md @@ -0,0 +1,618 @@ +# SPEC — la CHIAVE DI SCALA del libro live (`book_scale_k`) + +**Filone SCALE-SPEC, ondata 2026-08-22, branch `research/wave-0822`.** +**Questo documento e' una SPECIFICA. Non e' stata implementata: `src/`, `config/`, `scripts/live/`, +`scripts/cron_*.sh` e `tests/` non sono stati toccati.** Il prototipo che dimostra la meccanica e' +`scripts/research/r0822e_scale_proto.py` (isolato, non importato da niente, non scrive nulla; la +sua sezione (0) verifica entrambe le cose a runtime invece di dichiararle). + +Nasce da `RESULTS-0822.md` §33 (WORST-DAY), che ha misurato che il gradino di leva e' l'unica leva +grande rimasta e che **C6 (processo) FALLISCE**: la regola del progetto — *«ogni cambio di SCALA +passa dal cap di config, non da `target_vol`»* — **non e' implementabile come scritta**, perche' il +cap e' un CLAMP e non un moltiplicatore. + +--- + +## 0. IL RISCHIO CHE QUESTA SPECIFICA DEVE RENDERE IMPOSSIBILE + +> **Una chiave di scala e' esattamente il tipo di parametro che si alza «solo un po'» dopo un mese +> buono.** E' un numero, sta in un file di config, non richiede di capire niente per cambiarlo, e il +> suo effetto e' immediato e piacevole. + +Il progetto ha regole contro questa forma di deriva su ogni altro asse — `weights_tilt_null` per i +pesi, gate pre-registrati per i candidati, `select_cell_insample` per le griglie, `anchor_luck_band` +per le ancore — e **nessuna per la scala**. Non per dimenticanza: **per aritmetica.** + +Lo **Sharpe e' invariante alla scala**. Misurato nel prototipo (sez. 7a), sulla serie vera del libro: + +| k | Sharpe | drift | vol | maxDD | g(k) esatto | +|---|---|---|---|---|---| +| 1,00 | **1,5051** | 17,11% | 11,37% | −12,17% | 17,90% | +| 1,25 | **1,5051** | 21,39% | 14,21% | −15,01% | 22,61% | +| 2,00 | **1,5051** | 34,22% | 22,74% | −23,10% | 37,24% | + +`deflated_sharpe` e `marginal_vs_tp01` leggono la prima colonna. **Non falliscono: non vedono la +variabile.** Un raddoppio della leva passa ogni gate esistente del progetto senza far scattare nulla, +e produce lo stesso identico numero di sintesi che il progetto usa per giudicare tutto il resto. + +Da qui discendono i tre vincoli non negoziabili del disegno: + +1. **il tetto sta nel CODICE, non in config** (una config puo' essere modificata da chiunque, in + qualunque momento, senza review; una costante di codice no); +2. **il tetto sta sul PRODOTTO, non sulla chiave** (`frac` e `scala` moltiplicano: un tetto sulla + sola chiave lascia aperta la porta accanto — vedi §4.2, tabella); +3. **il gate e' uno SCRIPT che stampa PASSA/FALLISCE, non un giudizio** (§6), e c'e' un + **sorvegliante giornaliero** che se ne accorge *dopo il fatto* se qualcuno lo salta (§6.4). + +⚠️ **Correzione dovuta al mandato di questo filone.** Il gradino non vale quello che dice la frase +che circola. `RESULTS-0822.md` §2 misura **1,00x → 1,25x = 14,7a → 12,9a**, e **1,00x → 1,50x = +14,7a → 11,6a**; il «~€300/mese di versamenti equivalenti» appartiene al gradino **1,50x**. +Ricalcolato sulla tabella netta pubblicata (`r0807_piano_netto`: €500/m → 14,7a, €800/m → 11,4a, +interpolazione lineare **dichiarata**): + +| gradino | anni al capitale-rendita | versamento mensile equivalente | delta | +|---|---|---|---| +| 1,00x (oggi) | 14,7a | €500/m | — | +| **1,25x** | **12,9a** | ~€664/m | **+€164/m** | +| 1,50x | 11,6a | ~€782/m | +€282/m | + +L'interpolazione riproduce il «~€300/mese» pubblicato per 1,50x (€282) → è un controllo, non solo +un conto. **Questa specifica autorizza il gradino a 1,25x e vale ~€165/mese, non ~€300.** + +--- + +## 1. COSA SPECIFICA, E COSA NON FA + +**Specifica:** dove va la chiave, in che ordine si applica rispetto al cap, quali guardie servono, +quali test devono esistere **prima** della modifica, quale gate deve essere superato per alzarla e +con quale cadenza si sorveglia. + +**NON fa:** non autorizza il gradino, non implementa niente, non tocca `config/live.json`. Con la +chiave assente o `= 1.0` il libro e' **bit-exact quello di oggi** (§5, test T7): la modifica e' +inerte finche' qualcuno non scrive un numero, e quel numero passa dal gate del §6. + +--- + +## 2. DOVE VA LA CHIAVE, ESATTAMENTE + +### 2.1 La catena, letta dal sorgente vero + +Il prototipo la stampa leggendo i file (sez. 2), cosi' le righe qui sotto sono verificabili e non +citate a memoria: + +| stadio | file:riga | cosa produce | +|---|---|---| +| segnale TP01 | `src/live/shadow.py:119` `targets = {a: tp.current_target(dfs[a])...}` | una **frazione** ≥0, long-flat | +| segnale SKH01 | `src/live/book.py:217` `skh = _skyhook_positions(...)` → `:228` `sign = _skh_sign(st)` | un **segno** +1/−1/0 | +| cap per-asset | `src/live/book.py:107` `def _cap(...)` → `:194` `cap = _cap(...)` | il **clamp**, legato al watermark | +| **TARGET NETTO** | **`src/live/book.py:138` `def book_net_target(...)`** → **`:229` `net = book_net_target(tp_frac, sign, equity, cap)`** | **← QUI VA LA SCALA** | +| ordine | `src/live/book.py:153` `def build_book_order(...)` → `:232` | un **delta** verso il target | +| invio | `scripts/live/book_execute.py:208` `trader.rebalance_signed(inst, net, mark, ...)` | il fill | + +### 2.2 La firma proposta + +```python +# src/live/book.py +def book_net_target(tp_frac: float, skh_sign: int, equity: float, cap: float, + weight: float = WEIGHT, scala: float = 1.0) -> float: + raw = weight * equity * (W_TP01 * max(tp_frac, 0.0) + W_SKH * float(skh_sign)) + return scala * max(-cap, min(cap, raw)) # <-- la scala e' FUORI dal clamp: vedi §3 +``` + +più, accanto a `_cap()`, un lettore dedicato: + +```python +LEVA_LORDA_MAX = 1.25 # COSTANTE DI CODICE, non di config. Vedi §4.1. +SCALA_LADDER = (1.00, 1.25) # i soli valori esprimibili. Vedi §4.1 e §6. + +def _scala(real_equity, eq_fallback) -> float: + """Scala del libro. 1.0 (= il libro di oggi) in ogni stato degradato: vedi §4.3.""" + if real_equity is None or eq_fallback: + return 1.0 + ... # legge config, valida contro LEVA_LORDA_MAX e SCALA_LADDER, ALZA se fuori (§4.1) +``` + +e in `book_report`, una riga sotto `cap = _cap(...)`: + +```python +scala = _scala(sh.get("real_equity"), sh.get("eq_fallback")) +... +net = book_net_target(tp_frac, sign, equity, cap, scala=scala) +``` + +`book_report` deve **esporre `scala` nel dict di ritorno** accanto a `cap_per_asset`, e +`book_execute` deve **stamparla nella riga «sizing base»**: un parametro che agisce sul nozionale e +non compare nel report e' la forma esatta del difetto che questo progetto ha gia' pagato tre volte +(docstring che dichiarano una causalita' che il codice non ha). + +### 2.3 Perche' **li'** e non altrove — e la dimostrazione + +`book_net_target` e' **l'unico punto della catena in cui le due gambe si incontrano con una TAGLIA**: +sopra di lei TP01 da' una frazione e SKH01 da' **solo un segno**; sotto di lei si calcolano delta e +ordini. Percio' e' l'unico punto in cui un moltiplicatore e' un puro cambio di scala. + +Il prototipo (sez. 3) lo misura sulla funzione di produzione stessa — la copia e' **bit-exact** +(`max|copia − produzione| = 0.00e+00` su 480 combinazioni): + +| k | solo TP01 (tp=1, sg=0) | solo SKH01 (tp=0, sg=1) | rapporto TP/SKH | peso implicito TP01 | +|---|---|---|---|---| +| 1,00 | 2.267,62 | 755,88 | **3,000000** | **0,750000** | +| 1,25 | 2.834,53 | 944,84 | **3,000000** | **0,750000** | +| 2,00 | 4.535,25 | 1.511,75 | **3,000000** | **0,750000** | + +`max|target(k) − k·target(1)| = 0.00e+00` su 90 combinazioni. **Il mix 75/25 e' invariante a ogni k: +e' una scala, non un tilt.** + +Le alternative, e perche' cadono: + +| dove | perche' NO | +|---|---| +| `TrendPortfolio.target_vol` | scala il **75%** del libro e lascia fermo il 25% → e' anche un **tilt di pesi**, e **fallisce `weights_tilt_null`**. Misurato nel prototipo come **controllo positivo**: a k=1,25 il peso implicito di TP01 passa da 0,750 a **0,789**. | +| `_skyhook_positions` | non c'e' niente da scalare: produce un **segno**. | +| `WEIGHT` (=0,5) | e' lo **split BTC/ETH**, ed e' **condiviso** con `shadow_report`/`build_rebalance_order` (`shadow.py:144`) e con la dashboard, cioe' con il percorso **TP01-only**: cambiarlo cambierebbe anche un altro libro. E confonderebbe in un solo numero «quanti asset» e «quanta leva». | +| `W_TP01` / `W_SKH` | sono il **rapporto** 75/25 e sono legati per test ai pesi del backtest (`test_weights_match_backtest_sleeves` asserisce `W_TP01+W_SKH == 1.0` e la parita' con `deribit_book_sleeves`). Moltiplicarli entrambi romperebbe quella parita' e renderebbe impossibile distinguere scala e mix. | +| `build_book_order` | scalerebbe il **delta**, non il target: cambia la **velocita' di convergenza**, non la taglia, e il reconciler litigherebbe con se' stesso a ogni giro. | +| dentro `DeribitTrader` | il report, il dry-run e `book_executions.jsonl` stamperebbero un target **diverso** da quello eseguito. | + +--- + +## 3. L'INTERAZIONE COL CAP — il punto sottile + +### 3.1 Il fatto geometrico + +Il cap e' un **clamp**: `net = clamp(0.5·E·(0,75·tp + 0,25·sg), ±cap)` con `cap = frac·E = 0,50·E`. +A piena convinzione (`tp=1, sg=+1`) il grezzo vale **esattamente** `0,5·E·1,0 = 0,50·E = cap`: +**il clamp e' tangente al libro proprio nei giorni di massima esposizione.** + +### 3.2 I due ordini, misurati (prototipo sez. 4, su 5.648 giorni-asset veri) + +| k | ordine | clampati | quota | k_eff medio | **k_eff a piena convinzione** | lordo max | +|---|---|---|---|---|---|---| +| 1,25 | **PRIMA** del clamp | 32 | 0,57% | 1,2487 | **1,0000** | 1,000 | +| 1,25 | **DOPO** il clamp | 0 | 0,00% | **1,2500** | **1,2500** | 1,250 | +| 2,00 | PRIMA | 793 | 14,04% | 1,9235 | **1,0000** | 1,000 | +| 2,00 | DOPO | 0 | 0,00% | **2,0000** | **2,0000** | 2,000 | + +**L'ordine corretto e' DOPO il clamp**, e la ragione non e' di comodo: + +- **PRIMA**: il clamp mangia la scala **proprio nei giorni di massima convinzione** (k_eff = 1,00 a + piena convinzione, a ogni k). Il risultato non e' «il libro a leva k»: e' un libro che alza i + giorni piccoli e lascia fermi i grandi, cioe' un **appiattimento del profilo di convinzione**. E' + un cambio di **FORMA** travestito da cambio di taglia — e allora **la curva g(k) di + `r0822_growth_policy` / WORST-DAY non descrive quel libro**, perche' quella curva e' calcolata su + `k·r`, cioe' sull'ordine DOPO. Autorizzare il gradino con una curva e implementarne un'altra e' + il modo piu' silenzioso di sbagliare che questa specifica possa produrre. +- **DOPO**: `k_eff = k` esattamente, in ogni giorno e in ogni stato del segnale. E' l'unico ordine + per cui «leva k» significa la stessa cosa nel codice e nel backtest. + +### 3.3 Cosa implica per il SIGNIFICATO del cap — e va detto, perche' e' il prezzo + +Con l'ordine DOPO, **il cap smette di essere il tetto assoluto del nozionale**. Diventa il tetto +del **libro unitario**: limita la *forma* (nessun asset oltre `frac·E` di esposizione unitaria), +mentre la *taglia* la decide la scala. Il lordo massimo passa da `n_asset·frac` a +**`n_asset·frac·scala`** = 1,250x l'equity a k=1,25. + +E il cap **non smette di fare le altre due cose che fa oggi**, che sono quelle che contano davvero: + +1. resta **proporzionale all'equity** (`cap = equity·frac` sul percorso fidato) → un deposito non + strozza il libro e un prelievo lo rimpicciolisce da solo; +2. resta **legato al watermark** sul percorso di fallback (`min(fisso, watermark·frac)`) → il caso + che il 26/07 valeva «3,35x di leva su un conto da $597» resta chiuso (§4.3). + +⚠️ **Cio' che il cap NON puo' piu' fare da solo e' essere la guardia di sicurezza sulla leva +lorda.** Quella guardia va ricostruita esplicitamente, ed e' il §4. + +### 3.4 Il test che si rompe — e **deve** rompersi + +`tests/test_fee_sensitivity.py::test_leva_massima_da_config_resta_sotto_o_uguale_a_1x`: + +```python +leva_max = frac * len(FS.ASSETS) +assert leva_max <= 1.0, ("leva massima ...: la liquidation fee 1% torna rilevante, " + "rivedere r0726_fee_sensitivity e il diario 2026-07-26-fee-deribit.md") +``` + +**Si rompe? No — e questo e' il punto.** Con `frac` invariato a 0,50 il test continua a **passare**, +perche' misura `frac · n_asset` mentre la grandezza vera diventa `frac · n_asset · scala`. +**Non si rompe, e smette di controllare cio' che era stato scritto per controllare.** + +E' esattamente la forma di difetto che il progetto ha catturato **tre volte in un giorno** il +2026-08-21: *un controllo puntato su una configurazione diversa da quella che gira passa sempre, e +non sta controllando niente* (`fee_watch` sugli inverse, `venue_watch` con bitfinex, i test di +`book_live` a libro flat). + +**Quindi va sostituito, non aggiustato**, e la sostituzione e' una coppia: + +- **T2** (§5) — `test_la_leva_lorda_da_config_resta_sotto_il_tetto`: asserisce + `len(ASSETS) * frac * scala <= LEVA_LORDA_MAX`, cioe' **il prodotto**, e importa la scala **dallo + stesso posto da cui la importa la produzione** (regola del 21/08: *un sorvegliante deve DERIVARE + il proprio bersaglio dal codice sorvegliato, mai ridichiararlo*). +- **prerequisito del gate** (§6): `r0726_fee_sensitivity` va **rifatto** al nuovo lordo, perche' la + sua conclusione *«liquidation fee 1% irrilevante»* era esplicitamente condizionata a nozionale + lordo ≤1x. Al gradino il lordo e' 1,25x → una liquidazione costerebbe **1,25%** dell'equity, non + 1,00%. Non e' un problema; e' un numero che va **ricalcolato e ripubblicato**, non ereditato. + +Il vecchio test va **cancellato con una nota** che dice perche' (e rimandi a questo documento), non +modificato in silenzio: un test di decisione che cambia soglia senza lasciare traccia e' un test di +decisione perso. + +--- + +## 4. LE GUARDIE + +### 4.1 G1 — il tetto: **sul PRODOTTO, in CODICE, e non silenzioso** + +```python +LEVA_LORDA_MAX = 1.25 # src/live/book.py — costante di CODICE +SCALA_LADDER = (1.00, 1.25) +``` + +Tre proprieta', tutte necessarie: + +**(a) Sul prodotto.** `frac` e `scala` **moltiplicano**. Un tetto sulla sola chiave lascia aperta la +porta accanto (prototipo sez. 6): + +| frac | scala | leva lorda | tetto «scala ≤ 1,25» | **tetto sul PRODOTTO** | +|---|---|---|---|---| +| 0,500 | 1,25 | 1,250 | passa | passa | +| **0,625** | **1,25** | **1,562** | **passa** | **BLOCCA** | +| **0,750** | **1,25** | **1,875** | **passa** | **BLOCCA** | +| 0,500 | 2,00 | 2,000 | BLOCCA | BLOCCA | + +Ed e' un rischio **concreto, non teorico**: WORST-DAY ha appena pubblicato che «alzare `frac` da +0,50 a 0,625 non produce k=1,25», il che rende `frac` un candidato naturale al prossimo ritocco — +salvo che, **combinato con la scala, lo produce eccome**. + +**(b) In codice.** Il tetto di config sarebbe modificabile dalla stessa mano, nello stesso file, nel +momento in cui si modifica la chiave: sarebbe un lucchetto con la chiave attaccata. Con +`LEVA_LORDA_MAX` in `src/live/book.py`, il gradino **1,25 → 1,50 richiede una modifica di codice**, +quindi una review, quindi un gate. **Il tetto e' basso apposta: e' il meccanismo del cricchetto, non +un'opinione sulla leva massima sopportabile** (che dalla §4.5 sarebbe 1,67x). + +**(c) Non silenzioso.** Una scala fuori tetto (o fuori `SCALA_LADDER`) **NON viene tagliata al +tetto**: `book_execute` **si ferma, non invia ordini, e allerta**. Un clamp silenzioso farebbe +girare una config che dichiara un numero e un libro che ne esegue un altro — la stessa classe di +difetto del §3.4. `SCALA_LADDER` esiste perche' **una scala a gradini rende inesprimibile «solo un +po'»**: 1,05 non e' un valore prudente, e' un valore fuori scaletta, e viene rifiutato. + +### 4.2 G2 — l'invariante che lega TRE chiavi + +```python +assert len(ASSETS) * frac * scala * disaster_sl_pct <= 0.50 +``` + +Vedi §4.5: e' la guardia che morde per prima, e cattura chi allarga il disaster-SL. + +### 4.3 G3/G4 — il watermark e l'equity non leggibile + +**Regola proposta: la scala vive SOLO sul percorso fidato.** + +```python +scala_effettiva = scala se l'equity REALE e' leggibile, altrimenti 1.00 +``` + +Motivo: **una scala e' una decisione di rischio presa conoscendo il conto; quando non si sa quanto +vale il conto, non si prende.** Cosi' «il fallback non e' piu' permissivo» e' vero **per +costruzione**, non per aritmetica — e non dipende dal fatto che il conto degli invarianti torni. + +Misurato (prototipo sez. 6-bis; watermark reale letto da disco, $634,996; copia di `_cap` +verificata bit-exact contro la produzione): + +| stato | cap | scala eff. | lordo max | **/ equity vera** | tetto | +|---|---|---|---|---|---| +| normale (equity reale letta) | 317,50 | **1,25** | 793,74 | **1,250x** | si | +| online ma equity illeggibile | 317,50 | **1,00** | 635,00 | **1,000x** | si | +| watermark assente (1° avvio) | 300,00 (`CAP_UNKNOWN_USD`) | **1,00** | 600,00 | 0,945x | si | +| watermark vecchio e basso | 50,00 | **1,00** | 100,00 | 0,157x | si | +| dopo un deposito a $6.047 | 3.023,50 | **1,25** | 7.558,75 | 1,250x | si | + +**In nessuno stato degradato la leva supera quella dello stato normale**, e il caso «watermark +assente» ha **due protezioni indipendenti** (cap $300 **e** scala 1,00), non una sola. + +⚠️ **Prezzo dichiarato.** Nei giri in cui l'equity non e' leggibile il libro gira a k=1 e al giro +dopo torna a k → un ribilancio in piu' per oscillazione, di `(k−1)·frac·E` per asset = **~$79 a +testa** sul conto di oggi, cioe' **~$0,056** di fee a 3,5 bps. **Frequenza misurata** su +`logs/cron_book.log`: **0 giri su 1.442 (0,00%)**; i 21 giri «conto non leggibile» sono OFFLINE, e +li' il libro non manda ordini affatto, quindi la scala non si pone. → **il prezzo esiste, e' noto, +ed e' nullo sul campione osservato.** +⚠️ **E questo e' anche il limite dichiarato della regola:** se un giorno il ramo di fallback +diventasse frequente, il libro girerebbe a una leva **mista** e la curva `g(k)` smetterebbe di +descriverlo. → **il sorvegliante del §6.4 deve riportare quella frequenza**, e se supera il **2% +dei giri in 90 giorni** la regola va ridiscussa (soglia dichiarata qui, prima di vedere il caso). + +**Alternativa scartata:** congelare l'ultimo target scalato durante il fallback. Aggiunge **stato** +a un percorso con soldi veri — cioe' un nuovo modo di essere sbagliati — per risparmiare $0,056. + +### 4.4 G5 — il `min_order_usd` non e' un problema, e va detto + +La scala **aumenta** i delta, quindi allontana dal pavimento $5 invece di avvicinarcisi. Nessuna +azione. (Lo si dice perche' «non e' un problema» misurato vale quanto un problema trovato: evita +che la domanda torni.) + +### 4.5 G6 — il disaster-SL: **la guardia che morde per prima, e non era stata calcolata** + +Il disaster-SL e' un movimento di **prezzo** (−30% sul mark), non una perdita in equity. L'impatto +in equity e' quel movimento **moltiplicato per il lordo**, quindi **cresce con k anche se +`disaster_sl_pct` non cambia**. Prototipo sez. 6, tutte le righe scalate dai numeri **replicati** +di WORST-DAY (§7): + +| k | lordo max | A) peggior giorno possibile | **B) costo disaster-SL** | C) x_liq | min intra·k | margine C | +|---|---|---|---|---|---|---| +| 1,00 | 0,864 | 14,32% | **30,00%** | 115,8% | 7,33% | 15,8x | +| **1,25** | **1,081** | **17,90%** | **37,50%** | **92,5%** | **9,16%** | **10,1x** | +| 1,50 | 1,297 | 21,48% | 45,00% | 76,9% | 11,00% | 7,0x | +| 2,00 | 1,729 | 28,64% | **60,00%** ← sfonda | 57,4% | 14,66% | 3,9x | + +📌 **La guardia che morde per prima NON e' il peggior giorno possibile, e' il disaster-SL:** + +- da (A): `k ≤ 50% / 14,32%` = **3,49x** +- da (B): `k ≤ 50% / (2 · 0,50 · 30%)` = **1,67x** ← **2,1x piu' stringente** + +A k=1,25 il margine di (A) e' **2,8x** (il numero che WORST-DAY pubblica come C1) mentre quello di +(B) e' **1,33x**. **Il criterio C1 di WORST-DAY guardava la grandezza sbagliata**: guardava il +peggior giorno del *segnale*, non il costo del dispositivo di emergenza che il libro ha davvero +armato sul conto. Da qui l'invariante G2 (§4.2), che con `sl_pct=30%` e `frac=0,50` da' +**scala ≤ 1,67x** — e che scatta se qualcuno allarga il disaster-SL. + +**Decisione: `disaster_sl_pct` NON si tocca.** Stringerlo a 24% per tenere invariante l'impatto in +equity lo trasformerebbe da dispositivo di catastrofe in **stop di strategia**: verrebbe toccato dal +rumore, e il progetto ha gia' misurato cosa succede quando uno stop stretto incontra un gap (SKH01, +`sl 2% modellato → −11/−23% realizzato`). Si accetta il 37,5% e **lo si scrive**. + +⚠️ **Osservazione da LETTURA DEL CODICE, non misurata, e va citata cosi'.** +`src/live/execution.py:196-223` **ri-ancora** il disaster-SL al mark a ogni giro: se lo stop esistente +dista piu' del **5%** relativo da `mark·(1−sl_pct)`, viene **cancellato e ripiazzato**. Per un long +questo significa che lo stop **segue il prezzo anche in discesa**: il −30% e' un −30% **rotolante +dall'ultimo piazzamento**, non un pavimento assoluto sul trade. Quindi **B e' il costo di UN +episodio**, e un ribasso lento puo' produrne piu' d'uno. +Evidenza circostanziale, non prova: nel log ci sono **15 `placed` contro 259 `ok`** su asset con +posizione aperta — coerente con qualche ri-ancoraggio, ma i `placed` includono anche le prime +aperture e **non sono separabili dal log**. → **e' una proprieta' di oggi che il gradino amplifica, +non una che il gradino crea**, ed e' un follow-up con una domanda precisa: *quante volte il bracket +e' stato ri-piazzato mentre la posizione restava aperta?* (misurabile aggiungendo `pos_prima` al log, +non ricostruibile da quello attuale). + +--- + +## 5. I TEST CHE DEVONO ESISTERE **PRIMA** DELLA MODIFICA + +Scritti come specifiche eseguibili: **nome + cosa asserisce + il caso che deve far fallire.** +File proposto: `tests/test_book_scale.py` (piu' le modifiche dichiarate a +`tests/test_fee_sensitivity.py`). + +| # | test | asserisce | **deve fallire se** | +|---|---|---|---| +| **T1** | `test_la_scala_e_identica_sui_due_sleeve` | su una **griglia di k** (1,00/1,10/1,25/1,50/2,00) × tp_frac × segno, con cap disattivato: `target(k) == k·target(1)` **e** `target(tp=1,sg=0)/target(tp=0,sg=1) == W_TP01/W_SKH` costante | qualcuno applica la scala a **una sola** gamba (o dentro `TrendPortfolio`) | +| **T1b** | `test_il_test_di_simmetria_smaschera_una_scala_asimmetrica` | **CONTROLLO POSITIVO**: la stessa asserzione, applicata a un'implementazione asimmetrica scritta nel test, **fallisce** | il test T1 gira **solo a k=1** (dove le due implementazioni **coincidono**: potenza **zero** — misurato nel prototipo) | +| **T2** | `test_la_leva_lorda_da_config_resta_sotto_il_tetto` | `len(ASSETS) * frac * scala <= LEVA_LORDA_MAX`, con `frac` e `scala` **letti da `config/live.json`** e `LEVA_LORDA_MAX`/`ASSETS` **importati dal modulo di produzione** | qualcuno alza `frac`, o la scala, **o entrambe di poco** | +| **T3** | `test_l_invariante_disaster_sl_tiene_meta_equity` | `len(ASSETS) * frac * scala * disaster_sl_pct <= 0.50` | qualcuno allarga `disaster_sl_pct` **o** alza la scala fino a 1,67x | +| **T4** | `test_il_fallback_non_e_piu_permissivo_del_percorso_fidato` | in **ogni** stato degradato (equity illeggibile / watermark assente / watermark vecchio) il lordo massimo **/ equity vera** ≤ quello dello stato normale | qualcuno applica la scala anche in fallback, o slega il cap dal watermark | +| **T5** | `test_il_cap_continua_a_limitare_cio_che_limitava` | il target per-asset non supera mai `cap·scala`; e il clamp **morde ancora** sulle stesse osservazioni di prima (le 3 su 5.650) | qualcuno inverte l'ordine (§3.2) o toglie il clamp «tanto c'e' la scala» | +| **T6** | `test_una_scala_fuori_tetto_ferma_l_esecuzione_e_non_viene_tagliata` | con `scala` fuori `SCALA_LADDER` o fuori tetto: `book_execute` **non invia**, allerta, e il valore **non** viene silenziosamente clampato | qualcuno «rende robusto» il lettore con un `min(scala, LEVA_LORDA_MAX/…)` | +| **T7** | `test_senza_la_chiave_il_libro_e_quello_di_oggi` | con chiave assente **o** `= 1.0`, `book_net_target` e' **bit-exact** alla versione attuale su una griglia (`max|diff| == 0.0`) | la modifica cambia qualcosa **prima** che qualcuno decida di cambiarlo | +| **T8** | `test_il_report_e_il_log_dichiarano_la_scala` | `book_report()['scala']` esiste e `book_execute` la stampa | un parametro che muove il nozionale resta invisibile nel report | +| **T9** | `test_la_scala_e_derivata_dal_codice_non_ridichiarata` | il sorvegliante e i test importano `LEVA_LORDA_MAX`, `ASSETS`, `SCALA_LADDER` **da `src/live/book`** e non li riscrivono | due liste in due file (regola del 21/08, 4 occorrenze) | +| **T10** | `test_il_sorvegliante_segnala_una_scala_non_autorizzata` | **CONTROLLO POSITIVO** del §6.4: con una config che dichiara una scala assente dal giornale, il sorvegliante **allerta**; con config e giornale concordi, **tace** | il sorvegliante e' tarato per non segnalare mai (regola del 26/07: *un rilevatore tarato per non segnalare e' indistinguibile da uno rotto*) | +| **T11** | `test_il_vecchio_test_di_leva_e_stato_sostituito_non_rilassato` | `test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` **non esiste piu'** e T2/T3 esistono | qualcuno alza la soglia del vecchio test da 1,0 a 1,25 e chiude li' | + +⚠️ **T1b e' il test piu' importante dell'elenco**, ed e' la ragione per cui questo paragrafo esiste: +il prototipo (sez. 3) mostra che **a k=1 l'implementazione simmetrica e quella asimmetrica danno lo +stesso identico numero**. Un test di simmetria scritto sul caso di default passa sempre e non +controlla niente — la stessa firma dei test di `book_live` scoperti senza potenza a libro flat. + +--- + +## 6. IL GATE MANCANTE — `GATE SCALA-01` + +### 6.1 Perche' serve un gate NUOVO + +Vedi §0: nessun gate esistente vede la scala. Il gate deve quindi vivere sull'unico asse su cui la +scala si vede: **crescita e rovina** (`g(k)`, maxDD, distanza dalla liquidazione), non Sharpe. + +### 6.2 Le condizioni — tutte necessarie + +Per portare `book_scale_k` da `k0` a `k1`: + +| | condizione | misurata come | +|---|---|---| +| **A1** | `k1` e' il **gradino successivo** di `SCALA_LADDER` (1,00 → 1,25). Nessun valore intermedio. | confronto con la costante di codice | +| **A2** | `k0` e' in produzione da **≥180 giorni** con il criterio passato **ogni giorno** | giornale `data/live/scale_history.jsonl` (append-only) + registrazioni del sorvegliante | +| **A3** | **C1–C5 di WORST-DAY** ricalcolati **oggi** a `k1`, tutti PASSA — non ereditati dal 22/08 | ri-esecuzione di `r0822c_worst_day` sui dati correnti | +| **A4** | **C7 = G2**: `n_asset · frac · k1 · disaster_sl_pct ≤ 0,50` | §4.5 | +| **A5** | **C8 = G1**: `n_asset · frac · k1 ≤ LEVA_LORDA_MAX` | §4.1 | +| **A6** | **ANTI-RECENCY**: il criterio deve passare **anche escludendo gli ultimi 90 giorni**: `k_ammesso(r[:-90]) ≥ k1` | §6.3 | +| **A7** | `r0726_fee_sensitivity` **rifatto** al nuovo lordo e la conclusione «liquidation fee irrilevante» **ri-derivata o ritirata** | §3.4 | +| **A8** | la modifica e' **una riga di config + una riga di giornale**, zero codice. Se serve toccare `LEVA_LORDA_MAX`, **non e' una decisione di config** | diff | +| **A9** | i test T1–T11 (§5) esistono e passano **prima** | pytest | + +### 6.3 A6 — l'anti-recency, e come si misura davvero + +La forma verificabile e': **il gradino dev'essere autorizzato anche dai dati che non contengono la +finestra recente che ha motivato la richiesta.** E' l'analogo, sull'asse della scala, di +`select_cell_insample`. + +Il prototipo (sez. 7b) misura la meccanica su `k_ammesso = ½·k*(drift a −2 SE, coda troncata al +bound strutturale)`: + +| finestra | n | drift punto | drift −2SE | k ammesso | ≥1,25x? | +|---|---|---|---|---|---| +| tutta la storia | 2.719 | 17,11% | 8,78% | **3,52** | SI | +| esclusi ultimi 90 g | 2.629 | 16,70% | 8,15% | **3,20** | SI | +| esclusi ultimi 365 g | 2.354 | 19,52% | 10,20% | 3,80 | SI | +| solo ultimi 365 g | 365 | 1,59% | −13,69% | 0,03 | NO | + +e la sensibilita' a un tratto recente **artificialmente buono** (ultimi 90 giorni riscritti a un +drift dato, sull'intera storia): + +| ultimi 90 g portati a | drift −2SE | k ammesso | delta | +|---|---|---|---| +| 0%/anno | 7,82% | 3,12 | −0,40 | +| +50%/anno | 9,47% | 3,80 | +0,28 | +| **+100%/anno** | 11,11% | **4,45** | **+0,93** | +| +200%/anno | 14,35% | 5,63 | +2,10 | + +**Il −2 SE fa gran parte del lavoro** (un tratto eccezionale alza insieme il drift *e* la sua +incertezza), **ma non basta**: il criterio resta una funzione dei dati recenti, e la protezione vera +e' la **doppia valutazione** di A6. + +⚠️ **Tre onesta' obbligatorie su questa sezione:** +1. `k_ammesso` del prototipo e' **deliberatamente semplificato** (troncatura al bound, nessuna GPD): + serve a mostrare la **meccanica**, non a produrre il numero. Il gate reale usa la versione di + `r0822c_worst_day`. +2. **Oggi A6 non morde**: `k_ammesso ≈ 3,5x` contro un gradino di 1,25x. Il vincolo che morde e' + quello del §4.5 (disaster-SL). **A6 serve al gradino successivo, non a questo** — e va scritto + ora, quando non e' comodo per nessuno, non quando servira'. +3. La prima stesura di questa sezione presentava «solo ultimi 365 giorni» come l'esempio di *«un anno + buono»*: **falso**, quell'anno e' stato debole (drift 1,59%). Quella riga mostra solo che un + campione corto ha un SE enorme. La riga che risponde alla domanda vera e' la tabella dello shock. + +### 6.4 La cadenza — `scale_watch` + +Sorvegliante giornaliero (proposto: `src/live/scale_watch.py` + `scripts/live/scale_watch.py` in +`cron_daily.sh`; **non implementato**), che risponde a **tre domande diverse** — e sono davvero tre, +perche' hanno **tre azioni diverse** e nessuna e' deducibile dalle altre: + +| domanda | esito | azione | +|---|---|---| +| la config dichiara una scala che il **giornale** non ha mai autorizzato? | 🚨 | ricostruire come ci e' arrivata; scendere a `k0` | +| il criterio che autorizzo' la scala **corrente** passa ancora oggi? | 🚨 | **scendere di un gradino** | +| il **prodotto** `frac·scala·n_asset` ha superato `LEVA_LORDA_MAX`? | 🚨 | bloccare l'esecuzione | + +Piu' due requisiti che il progetto ha gia' imparato a pagare: + +- **tre stati, non due**: `OK` / `ALLARME` / **`NON MISURABILE`** (serie troppo corta, dati mancanti). + *«Non vedo» non e' «va tutto bene»* — `paper_dvolspread`, `venue_watch`. +- **disciplina degli allarmi**: una allerta per streak (`lock_step`), non una per giro; e il + messaggio deve dire **cosa** (decide) **e perche'** (ripara), con il valore **letto**, mai cablato. + +E deve **riportare la frequenza del ramo di fallback** (§4.3), che oggi e' 0/1.442 e che se salisse +sopra il **2% in 90 giorni** invalida la regola «scala solo sul percorso fidato». + +⚠️ **Cio' che il sorvegliante NON puo' fare: impedire la modifica.** Chi ha accesso al file puo' +scriverci dentro. Il sorvegliante rende la modifica **visibile entro 24 ore** e **attribuibile**. +E' la stessa onesta' di `venue_watch`: non protegge il saldo, compra tempo. + +--- + +## 7. IL COSTO DI NON FARLO, E IL RISCHIO DI FARLO + +### 7.1 Il costo di restare a 1,00x + +**~€165/mese di versamenti equivalenti, a costo zero, per sempre** (§0): 14,7a → **12,9a** al +capitale-rendita a €500/m netto fisco. Sulla scala delle leve misurate dal progetto sta **sotto** +«versare o no» (da *mai* a 16 anni) e «quando versare» (fattore 6), e **sopra** tutto cio' che la +ricerca ha prodotto in due mesi: l'ottimizzazione del peso SKH01 vale **+0,030 di Sharpe** e ha +fallito il suo gate; il gradino vale **1,8 anni**. + +Ed e' **l'unica leva grande che non richiede soldi nuovi**. + +### 7.2 Il rischio di farlo, in una tabella + +| grandezza | k=1,00 | **k=1,25** | fonte | +|---|---|---|---| +| peggior giorno **possibile** (chiusura) | 14,32% | **17,90%** | replicato, §4.5 | +| peggior giorno **vissuto** (chiusura) | 3,95% | 4,94% | replicato | +| peggior **minimo** intra-giorno | 7,33% | 9,16% | WORST-DAY (lente accoppiata) | +| **costo di un disaster-SL** | 30,0% | **37,5%** | §4.5 ← **la guardia che morde** | +| margine di liquidazione | 15,8x | 10,1x | §4.5 | +| maxDD storico | 12,17% | 15,01% | §0 | +| costo funding non modellato | ~1,53%/a | ~1,91%/a | FUNDING §35 | +| costo di una liquidazione (fee 1%) | 1,00% eq | 1,25% eq | §3.4, da ricalcolare | + +### 7.3 La riga che WORST-DAY ha dichiarato, e che va tenuta in cima alla decisione + +> Se si credono **entrambe** le riserve insieme — drift a −2 SE **e** un giorno a −10% ogni anno — +> il drift atteso e' **−0,61%/anno**, e **a quel punto non e' il gradino a essere sbagliato, e' il +> libro**: chi usa quella combinazione sta chiedendo di spegnere anche il conto di oggi. + +Questa specifica non risolve quel dilemma e non deve: lo rende **esplicito**. Chi rifiuta il gradino +in nome di entrambe le riserve deve dire cosa propone di fare del libro a k=1. + +### 7.4 Cosa questa specifica **non** rende sicuro + +- **La lente e' close-only** dove non usa i numeri intra-giorno di WORST-DAY. Su una regola a **un + giorno** close-only e' **esattamente cieca** (25/07, riconfermato dallo scettico il 22/08). Sul + canale **prop/funded** — dove il vincolo binding e' una barriera di DD giornaliera — **questa + specifica non si applica** e la lente accoppiata resta obbligatoria. +- **`g(k)` assume ritorno indipendente dalla size.** A $635 e' innocuo (SLIP-AUDIT: nessun impatto + misurabile, e un fill ha preso il 21,9% del volume della sua barra 5m in un range di 1,08 bps), ma + **quella misura ha una data di scadenza** — la partecipazione scala lineare col capitale. +- **Il funding non e' in nessuna serie di questo documento.** Il libro paga gia' ~1,5%/anno che + nessun backtest sottrae; il gradino lo amplifica del 25%. E' dentro C5, ma il **livello a k=1** + resta un difetto aperto del progetto, non di questa specifica. +- **La leva non compra probabilita', compra fretta** (PROP-ALLOC). Il gradino accorcia la mediana; + non alza `P(arrivare)` in modo proporzionale, e **alza la varianza del percorso**. + +--- + +## 8. PROCEDURA DI ATTIVAZIONE (checklist, in ordine) + +1. Scrivere `tests/test_book_scale.py` con T1–T11 (§5). **Prima.** T7 deve passare **contro il + codice attuale** (la chiave non esiste ancora → default 1,0 → bit-exact). +2. Implementare §2.2 (`scala` in `book_net_target`, `_scala()`, `LEVA_LORDA_MAX`, `SCALA_LADDER`, + `scala` nel report e nel print di `book_execute`). **Senza toccare `config/live.json`**: la chiave + assente vale 1,0, il libro non cambia, i test lo dimostrano. +3. Cancellare `test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` **con la nota** (§3.4). +4. Scrivere `scale_watch` (§6.4) + `data/live/scale_history.jsonl` con la riga iniziale + `{"scala": 1.00, "dal": "...", "gate": "iniziale, nessun gradino"}`, e aggiungerlo a + `cron_daily.sh` **e** al backup di `/opt/docker/scripts/backup.sh` (il giornale non e' ricostruibile). +5. **Lasciare girare a 1,00 per almeno 30 giorni** con il sorvegliante attivo. Una manopola nuova si + guarda funzionare **prima** di girarla. +6. Solo allora: eseguire `GATE SCALA-01` (§6.2). Se PASSA, una riga di config + una di giornale. +7. Rifare `r0726_fee_sensitivity` al nuovo lordo (A7) e ripubblicare la conclusione. + +**Il punto 5 non e' burocrazia: e' l'unico modo di scoprire che il sorvegliante e' rotto mentre la +leva e' ancora 1,00.** + +--- + +## 9. NODI APERTI — dichiarati, non nascosti + +1. **Il disaster-SL rotolante** (§4.5, ⚠️): letto nel codice, non misurato. Il log **non permette** + di separare i ri-piazzamenti dalle prime aperture. Se davvero rotola, `B` e' il costo di *un* + episodio e un ribasso lento ne produce piu' d'uno. **E' il nodo piu' serio di questo documento**, + perche' tocca la guardia che morde per prima. Follow-up: aggiungere `pos_prima` al log di + `ensure_disaster_sl` e ri-misurare — **non ricostruibile a posteriori**. +2. **`LEVA_LORDA_MAX = 1.25` e' una scelta di processo, non una misura.** Dall'invariante del §4.5 il + massimo sostenibile sarebbe 1,67x. Il valore basso e' il cricchetto. Chi vuole 1,50x deve toccare + codice — **ed e' voluto**. +3. **La banda d'ancora non e' stata girata su nessun numero di questo documento.** Tutte le serie + sono all'ancora canonica con la correzione ×0,89 sul drift. Le grandezze usate per decidere + (bound strutturale, lordo, invarianti) sono **aritmetiche** e non ancorate; `g(k)` e `k_ammesso` + **lo sono** — e vanno lette con la banda del libro (Sharpe FULL de-luckato 1,95, banda [1,81; 2,12]). +4. **Il gradino non e' stato autorizzato qui.** Questo documento specifica come si autorizza. + +--- + +## 10. IL PROTOTIPO + +`scripts/research/r0822e_scale_proto.py` — **0,9 s**, nessuna rete, nessuna scrittura. + +``` +nice -n 19 timeout 900 uv run python scripts/research/r0822e_scale_proto.py +``` + +Sezioni: (0) isolamento verificato · (1) sanity bit-exact vs produzione · (2) la catena letta dal +sorgente · (3) simmetria + controllo positivo · (4) l'ordine col clamp · (5) replica di WORST-DAY · +(6) guardie · (6-bis) stati dell'equity · (7) il gate mancante. + +**Replica a k=1,00 (sez. 5) — il motivo per cui le righe scalate di questo documento non sono +numeri copiati:** + +| grandezza | prototipo | pubblicato | scarto | +|---|---|---|---| +| drift de-luckato ×0,89 | 17,11% | 17,11% | 0,00% | +| vol annua | 11,37% | 11,37% | −0,00% | +| Sharpe de-luckato | 1,505 | 1,510 | −0,005 | +| peggior giorno (chiusura) | −3,95% | −3,94% | −0,01% | +| data del peggior giorno | **2020-03-13** | 2020-03-13 | coincide | +| morso del clamp | **3 / 5.648** | 3 / 5.650 | 2 righe di bordo* | +| bound strutturale | **−14,32%** | −14,32% | 0,00% | + +\* differenza di costruzione: il prototipo scarta la prima barra per asset (serve `vol_prev`). + +--- + +*Documento scritto senza modificare nulla in produzione. Il branch `research/wave-0822` e' quello da +cui il cron esegue: ogni riga di `src/`, `config/`, `scripts/live/`, `scripts/cron_*.sh` e `tests/` +e' rimasta identica.* diff --git a/scripts/research/r0822e_scale_proto.py b/scripts/research/r0822e_scale_proto.py new file mode 100644 index 0000000..02a76ca --- /dev/null +++ b/scripts/research/r0822e_scale_proto.py @@ -0,0 +1,698 @@ +"""r0822e — PROTOTIPO ISOLATO della CHIAVE DI SCALA del libro live (filone SCALE-SPEC). + + ⚠️ QUESTO FILE NON E' PRODUZIONE E NON E' IMPORTATO DA NIENTE IN PRODUZIONE. + Non scrive nulla: ne' config/, ne' data/, ne' src/. Non manda ordini. Non tocca il conto. + La sezione (0) VERIFICA entrambe le cose a runtime invece di dichiararle: + - nessun file fuori da scripts/research/ nomina questo modulo; + - il sorgente di questo file non contiene una sola primitiva di scrittura. + Documento di specifica che accompagna questo prototipo: docs/research/SPEC-scale-key.md + +COSA DIMOSTRA + Il filone WORST-DAY (r0822c) ha misurato che il gradino di leva 1,00x -> 1,25x vale + 14,7 -> 11,6 anni al capitale-rendita, che C1-C5 passano, e che **C6 (processo) FALLISCE**: + `config/live.json` non ha una chiave di scala e il cap e' un CLAMP, non un moltiplicatore + (morde su 3 osservazioni-asset su 5.650). Questo prototipo mostra la MECCANICA della chiave + che manca, senza costruirla in produzione: + + (1) SANITY la copia della funzione a scala 1.0 e' BIT-EXACT alla produzione + (2) DOVE la catena segnale -> target -> ordine, letta dal sorgente vero con le righe + (3) SIMMETRIA la scala e' identica sui due sleeve (e il controllo positivo la smaschera) + (4) ORDINE scala PRIMA o DOPO il clamp: i due ordini danno k_eff diversi, misurato + (5) REPLICA i numeri a k=1,00x coincidono con quelli pubblicati da WORST-DAY + (6) GUARDIE tetto composito, disaster-SL, liquidazione: quale guardia MORDE per prima + (7) ANTI-CREEP il gate mancante: la finestra recente non deve poter autorizzare il gradino + + uv run python scripts/research/r0822e_scale_proto.py +""" +from __future__ import annotations + +import json +import re +import subprocess +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +import r0725_capcurve as CC # noqa: E402 book_series (lente LIVE del libro) + +from src.data.downloader import load_data # noqa: E402 +from src.live.book import ( # noqa: E402 SOLA LETTURA (nessuna di queste scrive) + CAP_UNKNOWN_USD, W_SKH, W_TP01, _cap, _read_watermark, book_net_target) +from src.live.shadow import WEIGHT # noqa: E402 0.5 = split BTC/ETH +from src.strategies.skyhook import SKH01_V2_DD # noqa: E402 +from src.strategies.trend_portfolio import ( # noqa: E402 + CANONICAL, TrendPortfolio, realized_vol, resample_1d, simple_returns) + +ASSETS = ("BTC", "ETH") +ANN = 365.0 +DELUCK = 0.89 # fattore d'ancora MISURATO (r0726_deluck_factor) +MM_RATE = 0.01 # maintenance margin perp lineari Deribit (valore prudente) +WIPE = 0.90 + +# ---- i numeri PUBBLICATI da WORST-DAY (sez. 33 di RESULTS-0822), per il confronto di replica --- +PUB = { + "worst_close": -0.0394, # peggior giorno in chiusura, lente LIVE + "worst_date": "2020-03-13", + "worst_min": -0.0733, # peggior minimo intra-giorno (lente accoppiata) + "L_struct": 0.1432, # peggior giorno POSSIBILE a k=1,00x, in chiusura + "cap_bite_n": 3, # osservazioni-asset in cui il clamp morde + "cap_bite_tot": 5650, + "drift_deluck": 0.1711, # r0822_growth_policy + "vol": 0.1137, + "sharpe_deluck": 1.51, +} + +# ---- la PROPOSTA che questo prototipo specifica ------------------------------------------ +SCALA_PROPOSTA = 1.25 +LEVA_LORDA_MAX = 1.25 # tetto sul PRODOTTO n_asset * frac * scala (costante di CODICE) +MEZZO_EQUITY = 0.50 # C1: il peggior giorno possibile deve lasciare meta' equity +GRIGLIA_K = (1.00, 1.10, 1.25, 1.50, 2.00) + + +def sez(t: str) -> None: + print("\n" + "=" * 108) + print(f" {t}") + print("=" * 108) + + +# ========================================================================================== +# LA COPIA — l'unica differenza rispetto a src/live/book.book_net_target e' `scala` +# ========================================================================================== +def book_net_target_scalato(tp_frac: float, skh_sign: int, equity: float, cap: float, + weight: float = WEIGHT, scala: float = 1.0, + scala_prima_del_clamp: bool = False) -> float: + """COPIA di src/live/book.book_net_target con la chiave di scala. + + `scala_prima_del_clamp` esiste SOLO per misurare l'ordine sbagliato nella sezione (4): + la specifica propone l'ordine DOPO (scala * clamp(raw)), non PRIMA. + """ + raw = weight * equity * (W_TP01 * max(tp_frac, 0.0) + W_SKH * float(skh_sign)) + if scala_prima_del_clamp: + raw = float(scala) * raw + return max(-cap, min(cap, raw)) + clamped = max(-cap, min(cap, raw)) + return float(scala) * clamped + + +def book_net_target_ASIMMETRICO(tp_frac: float, skh_sign: int, equity: float, cap: float, + weight: float = WEIGHT, scala: float = 1.0) -> float: + """CONTROLLO POSITIVO (deve essere SMASCHERATO dal test di simmetria): la scala applicata + alla sola gamba TP01. E' esattamente cio' che fa `target_vol`, ed e' un TILT DI PESI.""" + raw = weight * equity * (W_TP01 * float(scala) * max(tp_frac, 0.0) + W_SKH * float(skh_sign)) + return float(scala) * 0.0 + max(-cap, min(cap, raw)) + + +# ========================================================================================== +# (0) ISOLAMENTO — verificato, non dichiarato +# ========================================================================================== +def sezione_0_isolamento() -> None: + sez("(0) ISOLAMENTO — questo file non e' produzione, e la verifica e' qui dentro") + slug = Path(__file__).stem + try: + out = subprocess.run(["grep", "-rl", slug, str(ROOT / "src"), str(ROOT / "scripts"), + str(ROOT / "tests"), str(ROOT / "config")], + capture_output=True, text=True, timeout=60).stdout.split() + except Exception as e: # grep assente: si dichiara, non si finge + out = [] + print(f" ⚠️ grep non disponibile ({type(e).__name__}): controllo non eseguito") + fuori = [p for p in out if not p.endswith(f"scripts/research/{slug}.py")] + print(f" file che nominano `{slug}` fuori da se stesso : {len(fuori)} {fuori}") + assert not fuori, "QUALCUNO IMPORTA IL PROTOTIPO: non e' piu' isolato" + + src = Path(__file__).read_text() + # I nomi delle primitive vietate sono COMPOSTI a runtime: scriverli per esteso qui li + # farebbe trovare dal controllo stesso (primo tentativo: il file falliva su se' stesso). + vietate = tuple("".join(p) for p in (("write", "_text("), ("to_", "parquet("), ("to_", "csv("), + ("json.", "dump("), ("os.", "remove"), ("shu", "til."), + ("mk", "dir("), ("os.", "rename"))) + scritture = [m for m in vietate if m in src] + aperture_w = re.findall(r"open\([^)]*['\"][wa]", src) + print(f" primitive di scrittura nel sorgente : {scritture or 'nessuna'}") + print(f" open() in scrittura/append : {len(aperture_w)}") + assert not scritture and not aperture_w, "il prototipo scrive: non e' piu' innocuo" + print(" -> il prototipo LEGGE il codice di produzione e non lo tocca. Il cron non lo vede.") + + +# ========================================================================================== +# (1) SANITY — la copia e' la produzione +# ========================================================================================== +def sezione_1_sanity() -> None: + sez("(1) SANITY — a scala 1,00 la copia e' BIT-EXACT alla funzione di produzione") + dif = 0.0 + n = 0 + for eq in (600.0, 2_000.0, 6_047.0, 50_000.0): + for cap in (300.0, eq * 0.5, eq * 0.25, 1e9): + for tp in (0.0, 0.13, 1 / 3, 0.5, 2 / 3, 0.87, 1.0, 1.4, 2.0, -1.0): + for sg in (-1, 0, 1): + a = book_net_target(tp, sg, eq, cap) + b = book_net_target_scalato(tp, sg, eq, cap, scala=1.0) + dif = max(dif, abs(a - b)) + n += 1 + print(f" {n} combinazioni (equity x cap x tp_frac x segno) · max|copia - produzione| = {dif:.2e}") + assert dif == 0.0, "la copia NON e' la produzione: ogni numero sotto e' senza valore" + print(" -> tutto cio' che segue e' una proprieta' della FUNZIONE DI PRODUZIONE, non di una sua imitazione.") + + +# ========================================================================================== +# (2) DOVE VA LA CHIAVE — la catena letta dal sorgente vero +# ========================================================================================== +def _righe(path: Path, pattern: str) -> list[tuple[int, str]]: + out = [] + for i, ln in enumerate(path.read_text().splitlines(), 1): + if re.search(pattern, ln): + out.append((i, ln.rstrip())) + return out + + +def sezione_2_dove() -> None: + sez("(2) DOVE — la catena segnale -> target -> ordine, letta da src/live/ con le righe vere") + book = ROOT / "src" / "live" / "book.py" + shadow = ROOT / "src" / "live" / "shadow.py" + exe = ROOT / "scripts" / "live" / "book_execute.py" + for titolo, path, pat in ( + ("segnale TP01 (frazione long-flat, causale)", shadow, r"current_target"), + ("segnale SKH01 (solo il SEGNO: nessuna taglia)", book, r"_skh_sign|_skyhook_positions\(load5m"), + ("cap per-asset (clamp, watermark-bound)", book, r"^def _cap|cap = _cap\("), + ("TARGET NETTO <== QUI VA LA SCALA", book, r"^def book_net_target|net = book_net_target\("), + ("ordine (delta verso il target)", book, r"^def build_book_order|order = build_book_order\("), + ("invio (porta il conto al target)", exe, r"rebalance_signed|ensure_disaster_sl\(inst"), + ): + print(f"\n {titolo}") + for i, ln in _righe(path, pat)[:3]: + print(f" {path.relative_to(ROOT)}:{i:>4} {ln.strip()[:92]}") + print(f""" + LETTURA. `book_net_target` e' l'UNICO punto in cui le due gambe si incontrano con una + TAGLIA: sopra di lei TP01 da' una frazione e SKH01 da' un SEGNO (+1/-1/0, nessuna + grandezza); sotto di lei `build_book_order` calcola solo un DELTA verso il target. + Percio' e' l'unico punto in cui un moltiplicatore e' un puro cambio di scala: + · su TP01 (`target_vol`) -> scala il 75% del libro e lascia fermo il 25% = TILT DI PESI + · su SKH01 -> non c'e' niente da scalare: e' un segno + · su `WEIGHT` (0.5) -> e' il SPLIT BTC/ETH, ed e' condiviso con shadow_report/dashboard + (percorso TP01-only): cambiarlo cambierebbe un ALTRO libro + · su `W_TP01`/`W_SKH` -> sono il RAPPORTO 75/25 e sono legati per test ai pesi del + backtest (`deribit_book_sleeves`): moltiplicarli entrambi + romperebbe quella parita' e confonderebbe scala e mix + · su `build_book_order` -> scalerebbe il DELTA, non il target: cambia la VELOCITA' di + convergenza, non la taglia, e il reconciler litigherebbe con se' + · dentro l'esecutore -> il report e il log stamperebbero un target diverso da quello + eseguito (il difetto che questo progetto ha gia' pagato 3 volte)""") + + +# ========================================================================================== +# (3) SIMMETRIA +# ========================================================================================== +def sezione_3_simmetria() -> None: + sez("(3) SIMMETRIA — la scala e' identica sui due sleeve, e il controllo positivo lo prova") + eq, cap = 6_047.0, 1e12 # cap disattivato: qui si guarda SOLO la formula + print(f" {'k':>6}{'solo TP01 (tp=1,sg=0)':>24}{'solo SKH01 (tp=0,sg=1)':>25}" + f"{'rapporto TP/SKH':>18}{'peso implicito TP01':>21}") + base_ratio = None + for k in GRIGLIA_K: + t = book_net_target_scalato(1.0, 0, eq, cap, scala=k) + s = book_net_target_scalato(0.0, 1, eq, cap, scala=k) + rap = t / s + w = t / (t + s) + base_ratio = rap if base_ratio is None else base_ratio + print(f" {k:>6.2f}{t:>24,.2f}{s:>25,.2f}{rap:>18.6f}{w:>21.6f}") + assert abs(rap - W_TP01 / W_SKH) < 1e-12, "la scala ha cambiato il MIX: e' un tilt" + print(f" -> rapporto costante = W_TP01/W_SKH = {W_TP01/W_SKH:.4f} a OGNI k. La scala non e' un tilt.") + + print("\n proprieta' forte (quella che il test deve asserire): target(k) == k * target(1)") + peggio = 0.0 + for k in GRIGLIA_K: + for tp in (0.0, 1 / 3, 0.5, 2 / 3, 1.0, 1.35): + for sg in (-1, 0, 1): + a = book_net_target_scalato(tp, sg, eq, cap, scala=k) + b = k * book_net_target_scalato(tp, sg, eq, cap, scala=1.0) + peggio = max(peggio, abs(a - b)) + print(f" max|target(k) - k*target(1)| su {len(GRIGLIA_K)*6*3} combinazioni (cap disattivato) = {peggio:.2e}") + assert peggio == 0.0 + + print("\n CONTROLLO POSITIVO — la stessa proprieta' su un'implementazione ASIMMETRICA") + print(" (scala sulla sola gamba TP01 = cio' che farebbe `target_vol`):") + print(f" {'k':>6}{'solo TP01':>16}{'solo SKH01':>14}{'rapporto':>12}{'peso TP01':>12} esito del test") + smascherato = False + for k in (1.00, 1.25, 1.50): + t = book_net_target_ASIMMETRICO(1.0, 0, eq, cap, scala=k) + s = book_net_target_ASIMMETRICO(0.0, 1, eq, cap, scala=k) + ok = abs(t / s - W_TP01 / W_SKH) < 1e-12 + smascherato |= (not ok) + print(f" {k:>6.2f}{t:>16,.0f}{s:>14,.0f}{t/s:>12.4f}{t/(t+s):>12.6f} " + f"{'passa (k=1: non c e nulla da vedere)' if ok else 'FALLISCE <- smascherato'}") + assert smascherato, "il test di simmetria NON smaschera l'asimmetria: non sta controllando niente" + print(" -> a k=1 le due implementazioni COINCIDONO: un test che gira solo a k=1 ha potenza ZERO.") + print(" Il test di simmetria deve girare su una GRIGLIA di k, o non e' un test.") + + +# ========================================================================================== +# dati storici: peso TENUTO da TP01 per asset (stessa costruzione di r0822c gambe_tp01) +# ========================================================================================== +def pesi_tp01() -> dict: + out = {} + tv = CANONICAL["target_vol"] + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + c = df["close"].values.astype(float) + r = simple_returns(c) + tgt = np.nan_to_num(np.asarray(TrendPortfolio(**CANONICAL).target_series(df), float)) + held = np.zeros(len(tgt)) + held[1:] = tgt[:-1] # cio' che il libro TIENE il giorno d + vol = realized_vol(r, CANONICAL["vol_win_days"], 365.25) + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + out[a] = pd.DataFrame({"held": held, "ret": r, "vol_prev": np.r_[np.nan, vol[:-1]]}, + index=idx).iloc[1:] + out[a] = out[a].assign(z=out[a]["ret"] / (out[a]["vol_prev"] / np.sqrt(365.25))) + out[a].attrs["tv"] = tv + return out + + +# ========================================================================================== +# (4) L'ORDINE COL CAP +# ========================================================================================== +def sezione_4_ordine(TPQ: dict, frac: float) -> None: + sez("(4) L'ORDINE COL CAP — il punto sottile: scala PRIMA o DOPO il clamp") + print(f""" + Il cap e' un CLAMP: net = clamp(0.5*E*(0.75*tp + 0.25*sg), ±cap), cap = frac*E = {frac:.2f}*E. + A piena convinzione (tp=1, sg=+1) il grezzo vale ESATTAMENTE 0.5*E*1.0 = {0.5:.2f}*E = cap. + Quindi il clamp e' *tangente* al libro proprio nei giorni di massima esposizione.""") + E = 1.0 # tutto in frazione di equity + cap = frac * E + print(f"\n {'k':>6}{'ordine':>10}{'giorni-asset':>14}{'clampati':>11}{'quota':>9}" + f"{'k_eff medio':>13}{'k_eff a piena convinz.':>24}{'lordo max (x E)':>17}") + for k in GRIGLIA_K: + for prima in (True, False): + tot = n_cl = 0 + num = den = 0.0 + for a in ASSETS: + h = np.clip(TPQ[a]["held"].values, 0.0, None) + for sg in (1,): # SKH long: il caso che spinge di piu' + base = np.array([book_net_target_scalato(x, sg, E, cap, scala=1.0) for x in h]) + got = np.array([book_net_target_scalato(x, sg, E, cap, scala=k, + scala_prima_del_clamp=prima) for x in h]) + m = np.abs(base) > 1e-12 + num += float(np.sum(got[m])) + den += float(np.sum(base[m])) + n_cl += int(np.sum(np.abs(got) < np.abs(k * base) - 1e-12)) + tot += int(m.sum()) + piena = book_net_target_scalato(1.0, 1, E, cap, scala=k, scala_prima_del_clamp=prima) / \ + book_net_target_scalato(1.0, 1, E, cap, scala=1.0) + lordo = len(ASSETS) * abs(book_net_target_scalato(1.0, 1, E, cap, scala=k, + scala_prima_del_clamp=prima)) + print(f" {k:>6.2f}{'PRIMA' if prima else 'DOPO':>10}{tot:>14}{n_cl:>11}{n_cl/tot:>9.2%}" + f"{num/den:>13.4f}{piena:>24.4f}{lordo:>17.3f}") + print(f""" + LETTURA — l'ordine non e' un dettaglio, e' la differenza fra una scala e un'altra strategia. + + · SCALA PRIMA DEL CLAMP: il clamp la mangia proprio nei giorni di massima convinzione + (k_eff a piena convinzione = 1,00 a OGNI k). Il risultato non e' "il libro a leva k": + e' un libro che alza i giorni piccoli e lascia fermi i grandi, cioe' un APPIATTIMENTO + del profilo di convinzione. E' un cambio di FORMA travestito da cambio di taglia — la + curva g(k) di growth_policy/WORST-DAY NON descrive quel libro, perche' quella curva e' + calcolata su k * r, cioe' sull'ordine DOPO. + · SCALA DOPO IL CLAMP: k_eff = k esattamente, in ogni giorno e in ogni stato del segnale. + E' l'unico ordine per cui "leva k" significa la stessa cosa nel codice e nel backtest. + + ⚠️ IL PREZZO, e va detto perche' e' il cuore della specifica: con l'ordine DOPO il cap + SMETTE di essere il tetto assoluto del nozionale. Il lordo massimo diventa + n_asset * frac * k = {len(ASSETS)} * {frac:.2f} * k, cioe' {len(ASSETS)*frac*SCALA_PROPOSTA:.3f}x l'equity a k={SCALA_PROPOSTA:.2f}. + E' ESATTAMENTE la quantita' che `test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` + (tests/test_fee_sensitivity.py) asserisce essere <= 1x. Quel test DEVE rompersi: sta + misurando `frac * n_asset` mentre la grandezza vera diventa `frac * n_asset * scala`.""") + + +# ========================================================================================== +# (5) REPLICA DEI NUMERI DI WORST-DAY a k = 1,00 +# ========================================================================================== +def sezione_5_replica(TPQ: dict, frac: float) -> dict: + sez("(5) REPLICA — a k=1,00 i numeri devono coincidere con quelli pubblicati da WORST-DAY") + book = CC.book_series("hourly") + r = book.values.astype(float) + r_del = r - (1.0 - DELUCK) * float(r.mean()) + m = { + "n": len(r), + "drift_deluck": float(r_del.mean()) * ANN, + "vol": float(r_del.std(ddof=0)) * np.sqrt(ANN), + "sharpe_deluck": float(r_del.mean() / r_del.std(ddof=0)) * np.sqrt(ANN), + "worst_close": float(r_del.min()), + "worst_date": str(book.index[int(np.argmin(r_del))].date()), + } + print(f"\n serie del libro LIVE (`book_series('hourly')`), {m['n']} giorni " + f"{book.index.min().date()} -> {book.index.max().date()}") + print(f" {'grandezza':>26}{'qui':>12}{'pubblicato':>13}{'scarto':>10}") + for nome, ch, pu, fmt in (("drift de-luckato x0.89", "drift_deluck", PUB["drift_deluck"], "%"), + ("vol annua", "vol", PUB["vol"], "%"), + ("Sharpe de-luckato", "sharpe_deluck", PUB["sharpe_deluck"], "f"), + ("peggior giorno (chiusura)", "worst_close", PUB["worst_close"], "%")): + v = m[ch] + s = f"{v:>12.2%}{pu:>13.2%}{v-pu:>10.2%}" if fmt == "%" else f"{v:>12.3f}{pu:>13.3f}{v-pu:>10.3f}" + print(f" {nome:>26}{s}") + print(f" {'data del peggior giorno':>26}{m['worst_date']:>12}{PUB['worst_date']:>13}" + f"{' coincide' if m['worst_date'] == PUB['worst_date'] else ' DIVERSA':>10}") + assert m["worst_date"] == PUB["worst_date"] + assert abs(m["worst_close"] - PUB["worst_close"]) < 5e-4 + + # --- il clamp morde su 3 osservazioni-asset su 5.650 + n_cap = sum(int(((0.5 * W_TP01 * TPQ[a]["held"].values + 0.5 * W_SKH) > frac).sum()) for a in ASSETS) + n_tot = sum(len(TPQ[a]) for a in ASSETS) + print(f"\n il CLAMP a frac={frac:.2f} morde su {n_cap} osservazioni-asset su {n_tot} " + f"({n_cap/n_tot:.3%}) [pubblicato: {PUB['cap_bite_n']}/{PUB['cap_bite_tot']}]") + for f2 in (0.50, 0.5625, 0.625, 0.75): + nn = sum(int(((0.5 * W_TP01 * TPQ[a]["held"].values + 0.5 * W_SKH) > f2).sum()) for a in ASSETS) + print(f" frac={f2:.4f} -> morde {nn:>3} volte · leva lorda MASSIMA {2*f2:.3f}x · " + f"leva EFFETTIVA mediana {np.median(np.concatenate([np.minimum(0.5*W_TP01*TPQ[a]['held'].values+0.5*W_SKH, f2)*2 for a in ASSETS])):.3f}x") + print(" -> alzare `frac` NON alza la leva: alza un tetto che non tocca quasi mai il libro.") + + # --- il bound strutturale a k = 1 + tv = CANONICAL["target_vol"] + sl_l = SKH01_V2_DD.sl_pct + ex_giu = {} + for a in ASSETS: + d = load_data(a, "1h") + ex_giu[a] = float((d["close"].values / d["open"].values - 1.0).min()) + lim_tp, lim_sk = {}, {} + for a in ASSETS: + d = TPQ[a].dropna(subset=["z"]) + dv = d[d["vol_prev"] >= tv] + z = float(dv["z"].min()) + lim_tp[a] = 0.5 * W_TP01 * tv * abs(z) / np.sqrt(365.25) + lim_sk[a] = 0.5 * W_SKH * (sl_l + abs(ex_giu[a])) + L = sum(lim_tp[a] + lim_sk[a] for a in ASSETS) + print(f"\n bound STRUTTURALE (scenario crash: TP01 a piena convinzione + SKH01 long stoppato)") + for a in ASSETS: + print(f" {a}: TP01 {-lim_tp[a]:>7.2%} SKH01 {-lim_sk[a]:>7.2%} -> {-(lim_tp[a]+lim_sk[a]):>7.2%}") + print(f" TOTALE a k=1,00x {-L:>7.2%} [pubblicato: {-PUB['L_struct']:.2%}]") + assert abs(L - PUB["L_struct"]) < 5e-3, "il bound strutturale non si replica" + print(" -> replica superata: la sezione (6) puo' scalare QUESTI numeri e non dei numeri copiati.") + return {"r_del": r_del, "L": L, "book": book} + + +# ========================================================================================== +# (6) LE GUARDIE — quale morde per prima +# ========================================================================================== +def sezione_6_guardie(rep: dict, TPQ: dict, frac: float, sl_pct: float) -> None: + sez("(6) GUARDIE — il tetto e' sul PRODOTTO, e la guardia che morde per prima non e' quella attesa") + L = rep["L"] + g_max = sum(0.5 * W_TP01 * TPQ[a]["held"].max() + 0.5 * W_SKH for a in ASSETS) + m_min = PUB["worst_min"] # peggior minimo intra-giorno misurato (accoppiato) + print(f""" + Tre grandezze scalano LINEARMENTE con k e vanno lette insieme (sl disaster = {sl_pct:.0%}): + A) peggior giorno POSSIBILE in chiusura : k * {L:.2%} + B) costo in equity se scatta il disaster-SL : k * n_asset * frac * {sl_pct:.0%} + (il -{sl_pct:.0%} e' un movimento di PREZZO sul mark; l'impatto in equity e' quel + movimento moltiplicato per il LORDO, quindi cresce con k anche se sl_pct non cambia) + C) distanza dalla liquidazione sul MINIMO : margine = x_liq / (|min| * k) +""") + print(f" {'k':>6}{'lordo max':>11}{'A) peggior giorno':>19}{'B) costo disaster-SL':>22}" + f"{'C) x_liq':>10}{'min intra k':>13}{'margine C':>11} verdetto") + for k in GRIGLIA_K: + G = min(g_max * k, len(ASSETS) * frac * k) + x_liq = (1 - MM_RATE * G) / (G * (1 - MM_RATE)) + A = k * L + B = k * len(ASSETS) * frac * sl_pct + mk = abs(m_min) * k + margine = x_liq / mk + v = [] + if A > MEZZO_EQUITY: + v.append("A sfonda meta' equity") + if B > MEZZO_EQUITY: + v.append("B sfonda meta' equity") + if margine < 3.0: + v.append("C sotto 3x") + if k * len(ASSETS) * frac > LEVA_LORDA_MAX: + v.append(f"oltre il tetto {LEVA_LORDA_MAX:.2f}x") + print(f" {k:>6.2f}{G:>11.3f}{A:>19.2%}{B:>22.2%}{x_liq:>10.1%}{mk:>13.2%}" + f"{margine:>11.1f}x {'; '.join(v) if v else 'ok'}") + + k_da_A = MEZZO_EQUITY / L + k_da_B = MEZZO_EQUITY / (len(ASSETS) * frac * sl_pct) + print(f""" + 📌 IL RISULTATO CHE WORST-DAY NON AVEVA CALCOLATO: la guardia che morde per prima NON e' + il peggior giorno possibile, e' il DISASTER-SL. + · dal peggior giorno (A): k <= {MEZZO_EQUITY:.0%} / {L:.2%} = {k_da_A:.2f}x + · dal disaster-SL (B): k <= {MEZZO_EQUITY:.0%} / ({len(ASSETS)} * {frac:.2f} * {sl_pct:.0%}) = {k_da_B:.2f}x + Il disaster-SL e' {k_da_A/k_da_B:.1f}x piu' stringente. A k={SCALA_PROPOSTA:.2f} il margine di (A) e' + {MEZZO_EQUITY/(SCALA_PROPOSTA*L):.1f}x mentre quello di (B) e' solo {MEZZO_EQUITY/(SCALA_PROPOSTA*len(ASSETS)*frac*sl_pct):.2f}x. + -> il tetto di codice non puo' essere un numero solo: deve essere l'INVARIANTE + `n_asset * frac * scala * disaster_sl_pct <= {MEZZO_EQUITY:.2f}`, che lega TRE chiavi. + Con sl_pct={sl_pct:.0%} e frac={frac:.2f} quell'invariante da' scala <= {k_da_B:.2f}x. + + ⚠️ E il buco che un tetto sulla SOLA chiave lascerebbe aperto: `frac` e `scala` MOLTIPLICANO. + Un tetto "scala <= {SCALA_PROPOSTA:.2f}" e' rispettato anche da frac=0.75 + scala={SCALA_PROPOSTA:.2f} + = leva lorda {2*0.75*SCALA_PROPOSTA:.3f}x. Il tetto va messo sul PRODOTTO.""") + print(f"\n {'frac':>8}{'scala':>8}{'prodotto = leva lorda':>24}{'tetto scala-only':>18}{'tetto sul PRODOTTO':>21}") + for f2, k in ((0.50, 1.00), (0.50, 1.25), (0.50, 1.50), (0.625, 1.25), (0.75, 1.25), (0.50, 2.00)): + p = len(ASSETS) * f2 * k + print(f" {f2:>8.3f}{k:>8.2f}{p:>24.3f}{'passa' if k <= SCALA_PROPOSTA else 'BLOCCA':>18}" + f"{'passa' if p <= LEVA_LORDA_MAX else 'BLOCCA':>21}") + + +# ========================================================================================== +# (6-bis) GLI STATI DELL'EQUITY — la scala non deve rendere il FALLBACK piu' permissivo +# ========================================================================================== +def cap_copia(equity, real_equity, eq_fallback, fixed, frac, wm): + """COPIA di src/live/book._cap (verificata sotto contro l'originale). Riprodotta qui perche' + l'originale legge il watermark REALE da disco e questo prototipo non deve dipenderne.""" + if frac is None: + return fixed + trusted = (real_equity is not None) and (not eq_fallback) and (equity is not None) and (equity > 0) + if trusted: + return float(equity) * float(frac) + return min(fixed, wm * float(frac)) if wm is not None else min(fixed, CAP_UNKNOWN_USD) + + +def sezione_6bis_equity(frac: float, fixed: float) -> None: + sez("(6-bis) GLI STATI DELL'EQUITY — la regola proposta: la scala vive SOLO sul percorso fidato") + wm = _read_watermark() + print(f" watermark reale sul disco (letto, non scritto): " + f"{'$%.2f' % wm if wm is not None else 'assente'}") + + # la copia e' la produzione, verificata sui due rami che non dipendono dal file + d = max(abs(cap_copia(6047.0, 6047.0, None, fixed, frac, wm) - _cap(6047.0, 6047.0, None)), + abs(cap_copia(2000.0, None, "fallback", fixed, frac, wm) - _cap(2000.0, None, "fallback"))) + print(f" max|copia del cap - produzione| sui due rami reali = {d:.2e}") + assert d == 0.0, "la copia del cap non e' la produzione" + + E_VERO = wm if wm is not None else 635.0 + print(f""" + Proposta: `scala_effettiva = scala se l'equity REALE e' leggibile, altrimenti 1.00`. + Motivo: una scala e' una decisione di rischio presa CONOSCENDO il conto; quando il conto + non si sa quanto vale, non si prende. Cosi' il vincolo "il fallback non e' piu' permissivo" + e' vero PER COSTRUZIONE e non per aritmetica. + Colonna che conta: LORDO / equity VERA (${E_VERO:,.2f}), cioe' la leva davvero corsa.""") + scen = [ + ("normale (equity reale letta)", E_VERO, E_VERO, None, wm), + ("online ma equity illeggibile", 2000.0, None, "fallback", wm), + ("watermark assente (1o avvio)", 2000.0, None, "fallback", None), + ("watermark vecchio e basso", 2000.0, None, "fallback", 100.0), + ("dopo un deposito a $6.047", 6047.0, 6047.0, None, 6047.0), + ] + print(f"\n {'stato':>32}{'cap':>11}{'scala eff.':>12}{'lordo max':>12}" + f"{'/ equity vera':>15}{'tetto rispettato':>18}") + for nome, eq, real, fb, w in scen: + cap = cap_copia(eq, real, fb, fixed, frac, w) + k_eff = SCALA_PROPOSTA if (real is not None and not fb) else 1.0 + lordo = len(ASSETS) * cap * k_eff + # l'equity VERA e' quella del conto (nota: qui la si conosce), NON il watermark: un + # watermark vecchio e basso non rende il conto piccolo, rende il libro SOTTO-esposto. + ev = real if real is not None else E_VERO + lev = lordo / ev + print(f" {nome:>32}{cap:>11,.2f}{k_eff:>12.2f}{lordo:>12,.2f}{lev:>15.3f}x" + f"{'si' if lev <= LEVA_LORDA_MAX + 1e-9 else 'NO':>18}") + print(""" + -> in nessuno stato degradato la leva supera quella dello stato normale: il fallback resta + la configurazione PIU' PRUDENTE, come oggi. E il caso "watermark assente" resta a + CAP_UNKNOWN_USD ($300) con scala 1.00: due protezioni indipendenti, non una sola. + ⚠️ PREZZO DICHIARATO: nei giri in cui l'equity non e' leggibile il libro gira a k=1 e al + giro dopo torna a k -> un ribilancio in piu' per ogni oscillazione. Costo: un delta di + (k-1)*frac*E per asset, cioe' ~$%.2f a testa sul conto di oggi, a 3,5 bps = $%.4f. + Frequenza MISURATA sui log (`logs/cron_book.log`): %s.""" % ( + (SCALA_PROPOSTA - 1) * frac * E_VERO, + (SCALA_PROPOSTA - 1) * frac * E_VERO * 0.00035 * 2, + _freq_fallback())) + + +def _freq_fallback() -> str: + """Quante volte il ramo 'equity fallback' e' stato preso davvero. Un prezzo dichiarato senza + la sua frequenza non e' un prezzo.""" + log = ROOT / "logs" / "cron_book.log" + if not log.exists(): + return "log assente, NON misurata" + try: + txt = log.read_text(errors="ignore") + except Exception: + return "log illeggibile, NON misurata" + giri = txt.count("BOOK DERIBIT LIVE EXECUTE") + fb = txt.count("EQUITY FALLBACK") + off = txt.count("conto non leggibile") + return (f"{fb} giri su {giri} ({fb/max(giri,1):.2%}); " + f"{off} giri sono invece OFFLINE (nessun ordine, la scala non si pone)") + + +# ========================================================================================== +# (7) IL GATE MANCANTE — anti-creep, e la finestra recente non deve poter autorizzare +# ========================================================================================== +def g_exact(r: np.ndarray, k: float) -> float: + x = 1.0 + k * r + if np.any(x <= 0): + return -np.inf + return float(np.expm1(ANN * np.mean(np.log(x)))) + + +def k_star(r: np.ndarray, kmax: float = 30.0, step: float = 0.05) -> float: + ks = np.arange(step, kmax + step, step) + return float(ks[int(np.nanargmax([g_exact(r, k) for k in ks]))]) + + +def _k_ammesso(r: np.ndarray, L: float) -> dict: + """k autorizzato dal criterio: mezzo-Kelly sul drift a -2 SE, con la coda TRONCATA al bound + strutturale (senza troncatura E[log(1+kR)] non esiste). Deliberatamente SEMPLIFICATO + rispetto a r0822c (nessuna GPD): qui interessa la MECCANICA del gate, non il suo numero.""" + mu, sd = float(r.mean()), float(r.std(ddof=0)) + se = sd * np.sqrt(ANN) / np.sqrt(len(r) / ANN) + r2 = np.maximum(r - mu + (mu * ANN - 2 * se) / ANN, -L) # -2SE + coda troncata + ks = k_star(r2) + return {"drift_pt": mu * ANN, "se": se, "drift_2se": mu * ANN - 2 * se, + "k_star": ks, "k_ammesso": 0.5 * ks, "n": len(r)} + + +def sezione_7_gate(rep: dict) -> None: + sez("(7) IL GATE MANCANTE — la scala e' invisibile allo Sharpe: serve un gate su un ALTRO asse") + r = rep["r_del"] + L = rep["L"] + book = rep["book"] + + print("\n (a) PERCHE' i gate esistenti non possono servire: lo Sharpe e' INVARIANTE alla scala.") + print(f" {'k':>6}{'Sharpe':>10}{'drift':>10}{'vol':>10}{'maxDD':>10}{'g(k) esatto':>14}") + for k in GRIGLIA_K: + x = k * r + eq = np.cumprod(1.0 + x) + dd = float((eq / np.maximum.accumulate(eq) - 1.0).min()) + print(f" {k:>6.2f}{x.mean()/x.std(ddof=0)*np.sqrt(ANN):>10.4f}{x.mean()*ANN:>10.2%}" + f"{x.std(ddof=0)*np.sqrt(ANN):>10.2%}{dd:>10.2%}{g_exact(r,k):>14.2%}") + print(" -> `deflated_sharpe` e `marginal_vs_tp01` leggono la 1a colonna: non FALLISCONO,") + print(" NON VEDONO la variabile. Un gate sulla scala deve guardare g(k), il DD e la rovina.") + + print("\n (b) IL CRITERIO ANTI-RECENCY — «una chiave di scala si alza dopo un mese buono».") + print(" Forma verificabile: il gradino dev'essere autorizzato ANCHE dai dati che") + print(" ESCLUDONO la finestra recente che ha motivato la richiesta.") + print(f"\n {'finestra':>34}{'n':>7}{'drift punto':>13}{'drift -2SE':>12}{'k*':>8}" + f"{'k ammesso':>11} {SCALA_PROPOSTA:.2f}x?") + fin = [("TUTTA la storia", r), + ("esclusi gli ultimi 90 giorni", r[:-90]), + ("esclusi gli ultimi 180 giorni", r[:-180]), + ("esclusi gli ultimi 365 giorni", r[:-365]), + ("SOLO gli ultimi 365 giorni", r[-365:])] + for nome, rr in fin: + d = _k_ammesso(rr, L) + ok = SCALA_PROPOSTA <= d["k_ammesso"] + print(f" {nome:>34}{d['n']:>7}{d['drift_pt']:>13.2%}{d['drift_2se']:>12.2%}" + f"{d['k_star']:>8.2f}{d['k_ammesso']:>11.2f} {'SI' if ok else 'NO'}") + d_full = _k_ammesso(r, L) + d_ex = _k_ammesso(r[:-90], L) + print(f""" + ⚠️ LETTURA ONESTA, e la prima stesura di questa sezione l'aveva sbagliata. La riga + «SOLO gli ultimi 365 giorni» NON e' l'esempio di «un anno buono»: quell'anno e' stato + DEBOLE (drift di punto {_k_ammesso(r[-365:], L)['drift_pt']:.2%}). Cio' che quella riga mostra e' solo che su + 365 osservazioni l'errore standard divora tutto — vero e insufficiente, perche' il rischio + da coprire e' l'opposto: un tratto recente BUONO che alza la richiesta.""") + + print("\n Percio' la domanda va posta al contrario: QUANTO puo' spostare il k ammesso un") + print(" tratto recente artificialmente buono? Si sovrascrive il drift degli ultimi 90") + print(" giorni e si guarda quanto si muove il criterio, sull'INTERA storia.") + print(f"\n {'ultimi 90g portati a':>24}{'drift punto (full)':>21}{'drift -2SE':>13}" + f"{'k ammesso':>11}{'delta vs base':>15}") + base_k = d_full["k_ammesso"] + for shock in (0.0, 0.25, 0.50, 1.00, 2.00): + rr = r.copy() + rr[-90:] = rr[-90:] - rr[-90:].mean() + shock / ANN + d = _k_ammesso(rr, L) + print(f" {shock:>23.0%}{d['drift_pt']:>21.2%}{d['drift_2se']:>13.2%}" + f"{d['k_ammesso']:>11.2f}{d['k_ammesso']-base_k:>+15.2f}") + print(f""" + -> un trimestre portato a +100%/anno (assurdo) muove il k ammesso di poche unita' su una + base di {base_k:.2f}: e' il -2 SE che fa il lavoro, perche' un tratto eccezionale alza + insieme il drift di punto E la sua incertezza. **Ma questo NON basta come gate**: il + criterio resta comunque una funzione dei dati recenti, e la protezione vera e' la + DOPPIA VALUTAZIONE — la richiesta dev'essere autorizzata anche da `r[:-90]` + (qui k ammesso {d_ex['k_ammesso']:.2f} contro {base_k:.2f}), cioe' da dati che NON contengono + la finestra che ha motivato la richiesta. E' l'analogo, sull'asse della scala, di + `select_cell_insample`. + ⚠️ E va detto il numero che rende tutto questo poco drammatico OGGI: con questa versione + semplificata il k ammesso e' {base_k:.2f}x, cioe' il gradino di 1,25x non e' vicino a + quel vincolo. **Il vincolo che morde e' quello della sez.(6) — il disaster-SL — non + il mezzo-Kelly.** Il criterio anti-recency serve al gradino SUCCESSIVO, non a questo.""") + + print("\n (c) LA CADENZA — un gate che si valuta solo quando lo si chiede non protegge da niente.") + ultimo = book.index.max() + print(f" ultimo giorno della serie: {ultimo.date()}. Il sorvegliante deve rispondere a") + print(" TRE domande diverse, e sono davvero tre (azioni diverse):") + print(" 1. la config dichiara una scala che il giornale non ha mai autorizzato? -> 🚨") + print(" 2. il criterio che autorizzo' la scala CORRENTE passa ancora oggi? -> 🚨 (si scende)") + print(" 3. il prodotto frac*scala*n_asset ha superato il tetto di codice? -> 🚨 (si blocca)") + print(" Nessuna delle tre e' deducibile dalle altre due.") + + +# ========================================================================================== +def main() -> None: + t0 = time.time() + print("=" * 108) + print(" r0822e — PROTOTIPO ISOLATO della CHIAVE DI SCALA (filone SCALE-SPEC)") + print(" Specifica: docs/research/SPEC-scale-key.md · NON e' produzione, non scrive nulla") + print("=" * 108) + + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + frac = float(cfg["max_notional_per_asset_frac"]) + sl_pct = float(cfg["disaster_sl_pct"]) + print(f"\n config/live.json (SOLA LETTURA): frac={frac} · cap fisso ${cfg['max_notional_per_asset_usd']:,.0f}" + f" · min order ${cfg['min_order_usd']} · disaster-SL {sl_pct:.0%}") + print(f" chiavi presenti: {sorted(k for k in cfg if not k.startswith('_'))}") + print(" -> nessuna chiave di scala. E' esattamente il buco che questo filone specifica.") + + sezione_0_isolamento() + sezione_1_sanity() + sezione_2_dove() + sezione_3_simmetria() + TPQ = pesi_tp01() + sezione_4_ordine(TPQ, frac) + rep = sezione_5_replica(TPQ, frac) + sezione_6_guardie(rep, TPQ, frac, sl_pct) + sezione_6bis_equity(frac, float(cfg['max_notional_per_asset_usd'])) + sezione_7_gate(rep) + + sez("RIEPILOGO — cosa questo prototipo ha DIMOSTRATO (e cosa resta alla specifica)") + print(f""" + DIMOSTRATO QUI + · la copia e' bit-exact alla produzione a k=1 -> le proprieta' misurate sono SUE; + · la scala applicata in `book_net_target` e' simmetrica sui due sleeve a ogni k, e il + controllo positivo prova che il test ha potenza SOLO se gira su una griglia di k; + · l'ordine col clamp decide se "leva k" significa la stessa cosa nel codice e nel + backtest: DOPO il clamp k_eff = k sempre; PRIMA il clamp la mangia a piena convinzione; + · i numeri a k=1,00 replicano WORST-DAY (peggior giorno, data, bound strutturale, morso + del clamp) -> le righe scalate della sez.(6) non sono numeri copiati; + · la guardia che morde per prima e' il DISASTER-SL, non il peggior giorno: il tetto va + messo sul PRODOTTO `n_asset * frac * scala * disaster_sl_pct`; + · lo Sharpe e' invariante alla scala a tutte le celle -> nessun gate esistente la vede; + · il criterio anti-recency e' misurabile: si valuta escludendo la finestra recente. + + NON DIMOSTRATO QUI, E VA DETTO + · non e' un backtest della leva: g(k) qui e' la forma chiusa su una serie storica, con + lo stesso limite gia' dichiarato da growth_policy (ritorno indipendente dalla size); + · la lente e' CLOSE-ONLY tranne dove usa il minimo pubblicato da WORST-DAY: su una regola + a UN giorno close-only e' CIECA (25/07), e la sez.(6) prende quel numero da fuori; + · il funding dei perpetual non e' in nessuna di queste serie (difetto trovato dal filone + FUNDING): a k=1 il libro paga gia' ~1,5%/anno che nessun backtest sottrae, e il gradino + lo amplifica del 25%; + · niente qui autorizza il gradino: autorizzarlo e' il gate della specifica, non un run. + + tempo: {time.time()-t0:.1f}s""") + + +if __name__ == "__main__": + main() From db9d8443512e2171ca7b3b1ead87203c06e8c33e Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 23:58:19 +0000 Subject: [PATCH 053/114] =?UTF-8?q?CLAUDE.md:=20la=20chiave=20di=20scala?= =?UTF-8?q?=20specificata=20=E2=80=94=20tetto=20sul=20prodotto,=20il=20tes?= =?UTF-8?q?t=20di=20guardia=20smette=20di=20controllare,=20e=20il=20nodo?= =?UTF-8?q?=20del=20disaster-SL=20rotolante?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 55b4a64..f041bd4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2384,8 +2384,9 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis → al gate si confronterebbe **un benchmark giusto con un numero di strategia sbagliato**, e il margine sembrerebbe **+5,83 invece di +1,87**. 🚨 **(4) WORST-DAY — la riserva sulla leva e' mal tarata di 2-4 ORDINI DI GRANDEZZA, ma il knob di - scala NON ESISTE.** Il gradino 1,00x→1,25-1,50x vale **14,7 → 11,6 anni** al capitale-rendita - (~€300/mese di versamenti equivalenti) ed era bloccato da un parametro **scelto a mano** ("un giorno + scala NON ESISTE.** Il gradino di leva vale **14,7a → 12,9a a 1,25x** e **→ 11,6a a 1,50x** al + capitale-rendita (**+€164/mese** e **+€282/mese** di versamenti equivalenti) ed era bloccato da un + parametro **scelto a mano** ("un giorno a −10% ogni anno"). **Il peggior giorno ha due risposte:** **−3,94% in chiusura** (2020-03-13) e **−7,33% al minimo** intra-giorno (rapporto mediano **1,89×**). Coda GPD: **−4,55% a 1/10.000 giorni** [−6,34/−3,08] ⚠️ **ma Hill dice −7,34% (ξ 0,34 contro 0,09): i due stimatori NON concordano @@ -2456,6 +2457,40 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis plausibili in due punti diversi**. Stessa famiglia del `DatetimeIndex.view("int64")` codificato il 01/07. **Su un perpetuale lineare, inoltre, `diff(nozionale) < 0` NON e' una vendita: il nozionale scende da solo quando scende il mark.** + ✅ **SEGUITO 2026-08-22 notte — LA CHIAVE E' SPECIFICATA (`docs/research/SPEC-scale-key.md` + + prototipo isolato `r0822e_scale_proto.py`, zero scritture, isolamento verificato a runtime), con un + NODO IRRISOLTO.** Registro §38. Replica a k=1,00 **6/6** contro WORST-DAY. + ⚠️ **CORREZIONE A UN NUMERO CHE AVEVO SCRITTO IO:** *"1,25-1,50x vale 14,7 → 11,6 anni, ~€300/mese"* + **fondeva due gradini** — a **1,25x** l'equivalente e' **+€164/mese**, il €282 e' del **1,50x**. + 🚨 **Tre risultati che il mandato non prevedeva:** + **(i)** `test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` **NON si rompe, ed e' PEGGIO che + rompersi**: misura `frac · n_asset` mentre la grandezza vera diventerebbe `frac · n_asset · scala` + → **continua a passare e smette di controllare**. **4ª occorrenza** della forma *"un controllo + puntato su una configurazione diversa da quella che gira"*. **Va cancellato con nota, non + aggiustato.** + **(ii) La guardia che morde per prima e' il DISASTER-SL, non il peggior giorno:** bound strutturale + k ≤ 3,49x, **disaster-SL k ≤ 1,67x = 2,1× piu' stringente** → **C1 di WORST-DAY guardava la + grandezza sbagliata**. Il tetto giusto e' l'invariante `n_asset · frac · scala · disaster_sl_pct + ≤ 0,50`, che lega **tre** chiavi. + **(iii) Il tetto va sul PRODOTTO, non sulla chiave:** `frac=0,625` + `scala=1,25` = leva **1,562x** + e **passerebbe** un tetto *"scala ≤ 1,25"* — e WORST-DAY aveva appena pubblicato che alzare `frac` + da solo *"non produce k=1,25"*: **combinato con la scala lo produce eccome.** + 📌 **Gate proposto `GATE SCALA-01`** (9 condizioni, fra cui **tetto sul prodotto** e **A6 + anti-recency**: `k_ammesso` calcolato **togliendo gli ultimi 90 giorni** dev'essere ≥ k richiesto), + perche' **la scala e' invisibile a ogni gate esistente** — Sharpe **1,5051 identico a tutte le celle + di k**: `deflated_sharpe` e `marginal_vs_tp01` **non falliscono, non vedono la variabile**. + ✅ `weights_tilt_null` invece **passa con potenza**: peso implicito TP01 **0,750000 a ogni k**, + `max|target(k) − k·target(1)| = 0`, e il **controllo positivo** (scala sulla sola gamba TP01 = cio' + che farebbe `target_vol`) lo porta a **0,789** e viene smascherato. + 🚨 **IL NODO, da sciogliere PRIMA di qualunque gradino:** `execution.py:196-223` **ri-ancora il + disaster-SL al mark a ogni giro** (tolleranza 5%) → **il −30% e' ROTOLANTE, non un pavimento**. Se e' + cosi', il 37,5% e' il costo di **un episodio** e un ribasso lento ne produce **piu' d'uno**, e il + margine 1,33x del gradino non e' quello vero. ⚠️ **Letto nel codice, NON misurato**, e il log non lo + permette (servirebbe `pos_prima`): **non ricostruibile a posteriori** → per misurarlo bisogna prima + aggiungere il campo al log. + **Seconda soglia dichiarata:** il fallback dell'equity oggi scatta **0 volte su 1.442 giri**; sopra + il **2% dei giri in 90 giorni** il libro girerebbe a leva **mista** e `g(k)` smetterebbe di + descriverlo. **REGOLE NUOVE:** (a) **una data in un gate va giustificata col COSTO della misura, non con la sua difficolta' percepita** — due gate d'attesa eliminati in una sera perche' nessuno aveva contato i secondi; (b) **una verifica esterna a €0 va fatta PRIMA di qualunque misura che ne dipenda**, e si From f5d88f10bb216f0b71c3c74a935d46e6ef0c6b10 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 00:18:27 +0000 Subject: [PATCH 054/114] =?UTF-8?q?research(wave-0822):=20FUNDING-AVOID=20?= =?UTF-8?q?=E2=80=94=20datati=20scartati,=20SPOT=20e'=20lead=20(+1,55%/ann?= =?UTF-8?q?o=20a=20iso-vol)=20ma=20la=20domanda=20fiscale=20vale=20piu'=20?= =?UTF-8?q?del=20risparmio;=20corretto=20il=20muro=20USDC=20di=20=C2=A734?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 68 +- scripts/research/r0822e_funding_avoid.py | 1141 ++++++++++++++++++++++ 2 files changed, 1208 insertions(+), 1 deletion(-) create mode 100644 scripts/research/r0822e_funding_avoid.py diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 791e307..b19af9b 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -704,6 +704,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 36 | PIANO-VERO | la tabella congiunta fisco+funding, che non esisteva | 🚨 **€250/m: P(20a) 92% → 14-26%** | | 37 | GATE-C | l'ultima gamba del gate funded + il biglietto come opzione | **PASS 2/2** — gate senza date; **P(≥50/g) 4,4% → 2,6%** | | 38 | SCALE-SPEC | specificare il knob di leva che non esiste | **LEAD** — specificata, **1 nodo irrisolto** sul disaster-SL | +| 39 | FUNDING-AVOID | evitare il funding esprimendo il libro sui datati | **SCARTATO** sui datati · **LEAD** sullo **spot** | | 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 | @@ -1619,7 +1620,14 @@ de-levering **REFUTED** sulla versione a 2 gambe. **Controllo positivo di causal e' il vincolo** (**5ª volta in questa ondata**); margine iniziale $13. ⚠️ **Ma tutti i futures datati Deribit sono INVERSE: non esiste una linea USDC datata** — **stesso muro delle opzioni**, e il collaterale in BTC e' esso stesso esposizione direzionale. **Non risolto**, serve una verifica sul -conto reale. **MAE per trade: −11,68% sulla storia piena → leva max 7,8x/gamba**, contro −3,64% → +conto reale. +🚨 **CORREZIONE 2026-08-23 (§39): QUESTO MURO E' FALSO.** Esistono **16 futures datati con settlement +USDC** su BTC/ETH (il piu' vecchio `BTC_USDC-25SEP26`, creato **2026-05-07**), taker **identico al +perpetual** (0,00035) e min-order 0,0001 BTC / 0,001 ETH. ✅ Verificato dal coordinatore con una GET +pubblica. **La svista e' di NAMESPACE, non di fatto:** `get_instruments?currency=BTC` **non li +ritorna** — i lineari stanno sotto **`currency=USDC`** (48 datati in totale). *Il coordinatore ha +commesso la stessa svista al primo controllo e ha ottenuto 0.* **Il muro vero e' un altro: quella +linea ha 3,5 mesi di vita e ZERO roll trimestrali mai eseguiti.** **MAE per trade: −11,68% sulla storia piena → leva max 7,8x/gamba**, contro −3,64% → 20,9x nella sola finestra di CC01 = **2,7× di leva di troppo**; sul carry puro con base MTM, MAE 90g −16,57% → **5,6-6,4x** contro le **~80x** che un maxDD dello 0,8% farebbe credere sostenibili. ⚠️ **Correzione a BASIS-CALENDAR:** il costo di roll e' **0,44%/anno**, non ~1,4% — quello contava il @@ -1916,3 +1924,61 @@ lo permette (servirebbe `pos_prima`): **non ricostruibile a posteriori.** **Secondo smentitore, con soglia dichiarata:** il ramo di **fallback dell'equity** oggi scatta **0 volte su 1.442 giri**; se superasse il **2% dei giri in 90 giorni** il libro girerebbe a leva **mista** e `g(k)` smetterebbe di descriverlo. + +--- + +## 39 — FUNDING-AVOID (evitare il funding: datati e spot) + +`scripts/research/r0822e_funding_avoid.py` (1.141 righe, ~45-56 s). **48 + 138 valutazioni**, nessuna +selezione (segnale e pesi **congelati**) → niente DSR. ✅ Replica: **−2,161%/anno riprodotto** e +protetto da `assert` prima di ogni delta; muri A/F **identici ai pubblicati**. + +**L'unico filone della giornata che poteva RESTITUIRE drift.** + +| variante | ShFULL | ShHOLD | drift | Δdrift | muro | +|---|---|---|---|---|---| +| **A) perp (oggi)** | 1,513 | 1,154 | 17,08% | — | **$325,0k** | +| B) tutto sui datati | 1,495 | 1,129 | 17,43% | +0,35% | — | +| D) TP01 datato + SKH01 perp | 1,507 | **1,118** | 17,53% | **+0,47%** | $319,5k | +| **E) TP01 SPOT + SKH01 perp** | **1,649** | **1,203** | 18,63% | **+1,55%** | **$289,6k (−10,9%)** | +| F) nessun carry (irreale) | 1,692 | 1,214 | 19,24% | +2,16% | $276,6k | + +📌 **La tesi condizionale REGGE ma non basta.** Funding condizionale **1,94×/2,59×** l'incondizionato +(✅ replica indipendente del 1,86-2,55× di ieri), basis solo **1,47×/1,66×**: in **livello** i due +carry sono identici (~6,4-7,4%), il datato vince **solo perche' si riprezza meno**. Ma il vantaggio e' +**tutto 2019-2023** e vale ~0 o e' avverso dal 2024 → **ShHOLD −0,034, negativo a 0/23 offset**, e il +**gate iso-volatilita' FALLISCE** (1,507 contro 1,513). + +🚨 **L'ESTIMATORE ARITMETICO INVENTA META' DELL'EFFETTO.** La media delle differenze di rendimento fra +due strumenti sullo stesso sottostante contiene `(var A − var B)/2`: qui vale **1,48% (BTC) / 2,60% +(ETH) l'anno**, cioe' **2-4× l'effetto cercato**. Controllo che lo rende visibile: il perpetual +"sovra-rende" l'indice di +0,4/+0,5%/anno con l'aritmetico e **~0 col log**. **REGOLA: il carry si +misura sul LOG del rapporto dei prezzi** — stessa forma della regola UCITS del 26/07 (*"la deviazione +fra due veicoli sullo stesso indice si misura sul rapporto cumulato, mai sulla media delle +differenze"*), ripassata in veste nuova. + +⚠️ **Bug catturato in sessione, stessa famiglia di quello di ieri sera, e NON visto dal numero:** +`fund_*.parquet` etichetta l'indice **all'istante T**, `al.get(a,'1h')` etichetta la barra **all'inizio** +→ appaiarle **sfasa di un'ora** (corr **0,996 al lag +1**, ~0 al lag 0), gonfia la sd della base da 8 a +**68-86 bps** e produceva un finto **"+0,32%/anno" nel verso atteso**. 📌 **Si e' visto dalla +VOLATILITA' (+1,07pp su una strategia identica), non dal numero** — che era plausibile e nel verso +che l'agente si aspettava. Riparato con controllo positivo che fallisce se lo si sbaglia; il pezzo +vero vale **+0,07%/anno**. + +📌 **Il cambio si giudica per SLEEVE:** SKH01 sui datati **peggiora** — turnover **48-57×/anno** contro +6,6-8,4× di TP01, quindi lo stesso mezzo spread gli costa **7 volte tanto**, e paga anche piu' carry. +Il netting TP01/SKH01 vale 1-2%: separarli non e' un argomento contro. + +🚨 **IL SOTTOPRODOTTO: lo strumento a carry ZERO esiste ed e' lo SPOT** (`BTC_USDC`/`ETH_USDC`, taker +**0,0** oggi, min 0,0001). TP01 e' long-flat e supera 1,0× nello **0,102%** dei giorni → il tetto non +morde. **+1,55%/anno di drift a volatilita' IDENTICA**, muro **−10,9%**, e supera il gate iso-vol. +⚠️ **NON e' un candidato, per tre ragioni dichiarate:** (a) **SKH01 non e' esprimibile sullo spot** +(e' long/short); (b) la **fee 0 e' dichiarata temporanea** e **`fee_watch` non sorveglia lo spot** (se +sparisse, **−13% del vantaggio**, misurato); (c) 🚨 **la domanda fiscale aperta vale PIU' del +risparmio** — se i derivati sono `c-quater` (26%) e lo spot `c-sexies` (33%), **7 punti su un CAGR del +17,9% fanno ~1,25%/anno**, cioe' **piu' di quello che lo spot porta**. + +**VERDETTO: `ESPRIMERE IL LIBRO SUI DATATI: COSTA 1,69%/ANNO CONTRO 2,16% DEL PERP → NON CONVIENE`** +(i 0,47 punti risparmiati si pagano con −0,034 di ShHOLD, negativo a 0/23 offset, e il gate iso-vol +fallisce). **SCARTATO** sui datati, **LEAD** sullo spot per la sola gamba TP01 — subordinato alla +domanda fiscale, che va posta al commercialista **prima** di qualunque prova. diff --git a/scripts/research/r0822e_funding_avoid.py b/scripts/research/r0822e_funding_avoid.py new file mode 100644 index 0000000..5a9aafb --- /dev/null +++ b/scripts/research/r0822e_funding_avoid.py @@ -0,0 +1,1141 @@ +#!/usr/bin/env python3 +"""r0822e_funding_avoid.py — FUNDING-AVOID: il costo di tenere aperta una posizione si puo' +EVITARE cambiando STRUMENTO, senza toccare il segnale? + +CONTESTO (ondata 2026-08-22, sezioni 34-35 di docs/research/RESULTS-0822.md) + (1) Stanotte e' stato MISURATO che il libro paga **2,16%/anno di funding** (TP01 -2,10%, + SKH01 -2,32%), mai modellato prima. E' una tassa sul DRIFT: il maxDD non si muove + (+0,3pp) ma la rendita perpetua scende 10,73% -> 9,14% e il muro di capitale sale del + +17,5%. Su una rendita, il drift e' la moneta. + (2) Stanotte e' stato PERSISTITO in `data/raw/fut_deribit/` lo storico dei futures DATATI + Deribit: 34 trimestrali per asset, BTC 240.150 barre orarie / ETH 233.334, 2018-09 -> + 2026-08, piu' 64.110 ore di funding e indice dal 2019-04. + +LA DOMANDA, MAI POSTA: **un future DATATO non paga funding.** Il carry e' incorporato nel +PREZZO (contango = compri sopra lo spot e convergi a scadenza). Quindi: + + esprimere TP01 (e/o SKH01) su un future datato invece che sul perpetual + costa MENO o PIU' del funding che paghiamo oggi? + +E' una domanda sull'ESECUZIONE, non sul segnale: il segnale resta congelato (TSMOM 30/90/180 +su BTC/ETH, vol-target 20%, `CANONICAL`), cambia solo lo strumento su cui lo si esprime. +Un grado di liberta' per volta, replica di controllo prima di ogni delta. + +LA TESI DA VERIFICARE O FALSIFICARE (la ragione per cui il filone puo' restituire qualcosa) + Il funding e' caro PROPRIO QUANDO SIAMO LONG: misurato stanotte, il tasso che il libro paga + davvero e' 1,86-2,55x l'incondizionato, perche' esposizione e funding sono positivamente + correlati. Il basis di un datato invece e' FISSATO ALL'INGRESSO e non si riprezza contro di + te mentre tieni. Se il rapporto condizionale/incondizionale del basis e' piu' BASSO di + quello del funding, il datato converte un costo condizionale e avverso in uno noto. + +SEZIONI + 0 IL DATO — 68 contratti trimestrali, copertura, roll; specifiche e LIQUIDITA' lette dal + venue (cache in scratchpad, `--no-net` per girare offline). + 1 REPLICA DI CONTROLLO — il -2,16%/anno va riprodotto PRIMA di confrontarci qualsiasi cosa. + 2 L'ESTIMATORE — livello vs flusso. La differenza ARITMETICA dei rendimenti fra due + strumenti sullo stesso sottostante NON e' il carry: contiene un termine di varianza + (Jensen) che qui vale 1,5-2,6 punti percentuali l'anno, cioe' 2-4x l'effetto cercato. + 3 IL CONFRONTO APPAIATO — per giorno e per segnale, condizionale vs incondizionale. + 4 IL ROLL E LA LIQUIDITA' — quante volte si rolla, quanto costa attraversare, e a $635. + 5 LE SERIE — libro nelle 5 varianti + banda d'ancora (23 offset, differenze APPAIATE). + 6 I MURI — drift -> rendita perpetua -> capitale, con la macchineria di r0726_capwall. + 7 IL MURO CHE NON C'E' — «tutti i datati Deribit sono INVERSE» e' FALSO da maggio 2026. + 8 SOTTOPRODOTTO — lo SPOT, che ha carry ZERO e oggi fee ZERO, e la sua domanda aperta. + 9 GATE + VERDETTO. + +VINCOLI: nessun file di produzione toccato (config/, src/, scripts/live/, cron INVARIATI); +nessun commit; output a video + cache in scratchpad. Runtime ~2-4 min. + +USO + nice -n 19 timeout 900 uv run python scripts/research/r0822e_funding_avoid.py + ... --no-net usa solo la cache del venue (nessuna richiesta a Deribit) +""" +from __future__ import annotations + +import datetime as dt +import json +import sys +import time +import urllib.request +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +for _p in (ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt", ROOT): + sys.path.insert(0, str(_p)) + +# r0822d_funding legge `--no-net` da sys.argv AL MOMENTO DELL'IMPORT: qui non si scarica il +# funding (la sua cache esiste gia'), quindi glielo si impone. Le uniche chiamate di rete di +# QUESTO script sono le sue, sotto, ed hanno il proprio flag. +_NET = "--no-net" not in sys.argv +if "--no-net" not in sys.argv: + sys.argv.append("--no-net") + +import altlib as al # noqa: E402 +import r0702_anchor_skh01 as r02 # noqa: E402 +import r0725_capcurve as CC # noqa: E402 +import r0726_capwall_refresh as WR # noqa: E402 +import r0822d_funding as FD # noqa: E402 +from src.portfolio.portfolio import combine_outer # noqa: E402 + +T0 = time.time() +ASSETS = ("BTC", "ETH") +FUT = ROOT / "data" / "raw" / "fut_deribit" +_SP = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/fa") +SCRATCH = _SP if _SP.parent.exists() else Path("/tmp/pythagoras_favoid") +API = "https://www.deribit.com/api/v2/public/" +MON = {"JAN": 1, "FEB": 2, "MAR": 3, "APR": 4, "MAY": 5, "JUN": 6, + "JUL": 7, "AUG": 8, "SEP": 9, "OCT": 10, "NOV": 11, "DEC": 12} + +BUF_CANON = 5 # si rolla 5 giorni prima della scadenza (dichiarato a priori) +BUF_GRID = (2, 5, 10, 20) # sensibilita' del parametro d'esecuzione +ALLOC = 600.0 # il capitale su cui gira book_series (conto reale $635) +DELUCK = 0.89 # fattore d'ancora MISURATO (r0726_deluck_factor) +W_TP, W_SKH = 0.75, 0.25 +LO_BOOK = pd.Timestamp("2019-03-14", tz="UTC") +FEE_REALE = 0.00035 # taker Deribit letto dal venue (3,5 bps/lato) +# il modello di CC.tp01_realistic usa 5 bps/lato (convenzione conservativa del progetto) +FEE_MODELLO = CC.DERIBIT_FEE_SIDE + + +def sez(t: str) -> None: + print("\n" + "=" * 100) + print(f" {t}") + print("=" * 100) + + +def clock() -> str: + return f"[{time.time() - T0:5.0f}s]" + + +def expdate(nm: str) -> pd.Timestamp: + d = nm.split("-")[1] + return pd.Timestamp(dt.datetime(2000 + int(d[5:]), MON[d[2:5]], int(d[:2]), 8, 0), + tz="UTC") + + +# =========================================================================== +# 0) IL DATO +# =========================================================================== +def _pace() -> None: + """Mai nei minuti :07 (cron_book) e :25-:29 (cron_chain); <=2 req/s.""" + while dt.datetime.now(dt.timezone.utc).minute in (7, 25, 26, 27, 28, 29): + time.sleep(20) + time.sleep(0.6) + + +def _get(path: str): + _pace() + return json.load(urllib.request.urlopen(API + path, timeout=30))["result"] + + +VENUE_CACHE = SCRATCH / "venue_snapshot.json" +TICKETS = (100, 240, 500) # $ per ordine: 240 = nozionale max per asset di TP01 a $635 + + +def venue_snapshot() -> dict | None: + """Specifiche + libro degli strumenti candidati. UNA istantanea, non una distribuzione: + e' esattamente cio' che si puo' dire e va detto cosi'.""" + if VENUE_CACHE.exists(): + old = json.loads(VENUE_CACHE.read_text()) + else: + old = None + if not _NET: + return old + try: + out = {"ts": pd.Timestamp.now(tz="UTC").isoformat(), "instruments": {}, "books": {}} + for cur in ("BTC", "ETH", "USDC"): + for kind in ("future", "spot"): + try: + for x in _get(f"get_instruments?currency={cur}&kind={kind}&expired=false"): + out["instruments"][x["instrument_name"]] = dict( + settlement=x.get("settlement_currency"), + contract_size=x.get("contract_size"), + min_trade_amount=x.get("min_trade_amount"), + tick=x.get("tick_size"), taker=x.get("taker_commission"), + maker=x.get("maker_commission"), kind=kind, currency=cur, + created=x.get("creation_timestamp"), + expiry=x.get("expiration_timestamp")) + except Exception: + pass + for i in ("BTC_USDC-PERPETUAL", "BTC_USDC-25SEP26", "BTC_USDC-25DEC26", "BTC_USDC", + "BTC-25SEP26", "BTC-PERPETUAL", + "ETH_USDC-PERPETUAL", "ETH_USDC-25SEP26", "ETH_USDC-25DEC26", "ETH_USDC", + "ETH-25SEP26", "ETH-PERPETUAL"): + try: + d = _get(f"get_order_book?instrument_name={i}&depth=20") + out["books"][i] = dict(bid=d.get("best_bid_price"), ask=d.get("best_ask_price"), + index=d.get("index_price"), oi=d.get("open_interest"), + bids=d.get("bids"), asks=d.get("asks"), + vol24=(d.get("stats") or {}).get("volume"), + vol24usd=(d.get("stats") or {}).get("volume_usd")) + except Exception: + pass + SCRATCH.mkdir(parents=True, exist_ok=True) + VENUE_CACHE.write_text(json.dumps(out)) + return out + except Exception as e: # rete KO: si dichiara + print(f" ⚠️ venue non raggiungibile ({type(e).__name__}) -> uso la cache se c'e'.") + return old + + +def walk_cost_bps(book: dict, usd: float, side: str) -> float | None: + """Costo effettivo vs mid di un ordine da `usd` dollari, camminando il libro. + Ritorna None se il libro non basta (e allora si dice, non si estrapola).""" + if not book or not book.get("bid") or not book.get("ask"): + return None + mid = (book["bid"] + book["ask"]) / 2.0 + lv = book.get("asks" if side == "buy" else "bids") or [] + need, got, cost = usd / mid, 0.0, 0.0 + for p, q in lv: + take = min(q, need - got) + got += take + cost += take * p + if got >= need - 1e-12: + break + if got < need - 1e-12: + return None + eff = cost / got + return (eff / mid - 1.0) * 1e4 * (1.0 if side == "buy" else -1.0) + + +def sezione0(V: dict | None) -> dict: + sez("0) IL DATO — 68 trimestrali datati su disco, e le specifiche LETTE dal venue") + tab = {} + for a in ASSETS: + rows = [] + for p in sorted(FUT.glob(f"{a}-*.parquet")): + d = pd.read_parquet(p, columns=["ts", "close", "volume"]) + if len(d) < 200: + continue + e = expdate(p.stem) + d = d[(d.ts <= e) & (d.close > 0)] + rows.append((p.stem, e, d.ts.min(), d.ts.max(), len(d), float(d.volume.sum()))) + R = pd.DataFrame(rows, columns=["nm", "exp", "t0", "t1", "n", "vol"]).sort_values("exp") + tab[a] = R + print(f" {a}: {len(R)} contratti trimestrali usabili, scadenze da {R.exp.min().date()} " + f"a {R.exp.max().date()}, {int(R.n.sum()):,} barre orarie") + print(" ⚠️ Istantanea al 22/08 (il cron NON rinfresca questi file) e il contratto in corso") + print(" e' troncato. Il guardrail «solo dati certi» regge perche' vivono in una") + print(" sottocartella con prefisso, fuori dal namespace nudo di `load_data`.") + + if not V: + print("\n ⚠️ SPECIFICHE DEL VENUE: NON MISURATE (nessuna rete, nessuna cache).") + return dict(tab=tab, spec=None) + + print(f"\n Istantanea del venue del {V['ts'][:19]}Z — UNA lettura, non una distribuzione.") + print(f" {'strumento':<22} {'settl':>6} {'min':>9} {'tick':>7} {'taker':>7} " + f"{'spread':>8} {'vol 24h $':>13} {'OI':>14}") + for i in ("BTC_USDC-PERPETUAL", "BTC_USDC-25SEP26", "BTC_USDC-25DEC26", "BTC-25SEP26", + "BTC_USDC", "ETH_USDC-PERPETUAL", "ETH_USDC-25SEP26", "ETH_USDC-25DEC26", + "ETH-25SEP26", "ETH_USDC"): + s = V["instruments"].get(i, {}) + b = V["books"].get(i, {}) + sp = float("nan") + if b.get("bid") and b.get("ask"): + mid = (b["bid"] + b["ask"]) / 2 + sp = (b["ask"] - b["bid"]) / mid * 1e4 + print(f" {i:<22} {str(s.get('settlement')):>6} {str(s.get('min_trade_amount')):>9} " + f"{str(s.get('tick')):>7} {str(s.get('taker')):>7} {sp:>7.2f}b " + f"{(b.get('vol24usd') or float('nan')):>13,.0f} " + f"{(b.get('oi') or float('nan')):>14,.0f}") + return dict(tab=tab, spec=V) + + +# =========================================================================== +# 1) IL CARRY DEI DATATI — costruzione della serie +# =========================================================================== +def aligned_index(a: str, grid: pd.DatetimeIndex) -> np.ndarray: + """Indice Deribit ALLINEATO alla convenzione delle barre orarie. + + ⚠️ TRAPPOLA MISURATA IN QUESTA SESSIONE, e non e' un dettaglio: `fund_{a}.parquet` + etichetta il record T con l'indice ALL'ISTANTE T, mentre `al.get(a,'1h')` etichetta la + barra all'INIZIO (la sua chiusura e' il prezzo a T+1h). Appaiare le due serie per + etichetta le sfasa di UN'ORA: corr(r_perp_t, r_index_t) = -0,02 e corr(r_perp_t, + r_index_t+1) = +0,996, la base perp/indice esce con sd 68-86 bps invece di pochi bps, e + la differenza di rendimento eredita 97-122 bps/ora di rumore INVENTATO. + Nessuna eccezione, nessun NaN: due serie plausibili che non parlano dello stesso istante. + (Stessa famiglia dell'errore di RISOLUZIONE `[ms]/[us]/[s]` catturato ieri notte in + r0822d_funding, e del `DatetimeIndex.view("int64")` codificato in CLAUDE.md il 01/07: + li' era la scala, qui e' l'ETICHETTA. Il sintomo e' lo stesso — un numero credibile.) + """ + fu = pd.read_parquet(FUT / f"fund_{a}.parquet").set_index("ts").sort_index() + return fu["index"].reindex(grid).shift(-1).values.astype(float) + + +def verifica_allineamento(grid: dict) -> None: + """Controllo POSITIVO dell'allineamento: deve fallire se lo si sbaglia, o non e' un test.""" + print("\n [ALLINEAMENTO] controllo positivo indice-vs-barre (corr dei log-rendimenti):") + for a in ASSETS: + fu = pd.read_parquet(FUT / f"fund_{a}.parquet").set_index("ts").sort_index() + raw = fu["index"].reindex(grid[a]).values.astype(float) + fix = aligned_index(a, grid[a]) + pp = al.get(a, "1h").set_index("datetime")["close"].astype(float) + pp.index = pd.DatetimeIndex(pp.index) + pv = pp.reindex(grid[a]).values.astype(float) + out = [] + for lbl, v in (("per etichetta", raw), ("allineato", fix)): + rP = np.diff(np.log(pv)); rI = np.diff(np.log(v)) + m = np.isfinite(rP) & np.isfinite(rI) + b = pv / v + out.append((lbl, float(np.corrcoef(rP[m], rI[m])[0, 1]), + float(np.nanstd(b - 1.0)) * 1e4)) + print(f" {a}: per etichetta corr {out[0][1]:+.4f} sd base {out[0][2]:6.1f} bps -> " + f"allineato corr {out[1][1]:+.4f} sd base {out[1][2]:5.1f} bps") + assert out[1][1] > 0.99 > out[0][1], f"allineamento non riparato su {a}" + print(" -> il controllo ha POTENZA: con l'etichetta grezza la correlazione e' ~0.") + + +def dated_rate(a: str, grid: pd.DatetimeIndex, buf: int = BUF_CANON, + log: bool = True) -> dict: + """Pseudo-`rate` compatibile con `FD.cumF` / `FD.daily_funding`: al posto del funding + orario mette il CARRY del future datato front, cioe' quanto il datato sotto-rende il + perpetual in quell'ora (>0 in contango = costo per un long). + + Il contratto tenuto durante l'ora t e' quello scelto alla chiusura di t-1 (nessuna + decisione usa il prezzo dell'ora che si sta valutando), e nell'ora di ROLL il rendimento + lo fa il contratto VECCHIO: cosi' non si perde nessuna ora e non si incolla nessun salto + di prezzo (convenzione dichiarata in r0822d_cc01_2022: si incollano i RENDIMENTI). + """ + n = len(grid) + idxp = aligned_index(a, grid) + perp = al.get(a, "1h").set_index("datetime")["close"].astype(float) + perp.index = pd.DatetimeIndex(perp.index) + pp = perp.reindex(grid).values.astype(float) + + C = {} + for p in sorted(FUT.glob(f"{a}-*.parquet")): + d = pd.read_parquet(p, columns=["ts", "close", "volume"]) + if len(d) < 200: + continue + e = expdate(p.stem) + d = d[(d.ts <= e) & (d.close > 0)].set_index("ts").sort_index() + C[p.stem] = (e, d["close"].reindex(grid).values.astype(float), + d["volume"].reindex(grid).values.astype(float)) + order = sorted(C, key=lambda x: C[x][0]) + + px = np.full(n, np.nan); nmi = np.full(n, -1); dte = np.full(n, np.nan) + vol = np.full(n, np.nan) + for k, nme in enumerate(order): + e, s, v = C[nme] + dd = (e - grid).total_seconds().values / 86400.0 + m = (dd >= buf) & np.isfinite(s) & np.isnan(px) + px[m] = s[m]; nmi[m] = k; dte[m] = dd[m]; vol[m] = v[m] + + def ret(v: np.ndarray) -> np.ndarray: + r = np.full(n, np.nan) + r[1:] = (np.log(v[1:]) - np.log(v[:-1])) if log else (v[1:] / v[:-1] - 1.0) + return r + + rP, rF = ret(pp), ret(px) + c = np.zeros(n); val = np.zeros(n, bool) + same = np.concatenate([[False], nmi[1:] == nmi[:-1]]) + m = same & np.isfinite(rF) & np.isfinite(rP) + c[m] = rP[m] - rF[m]; val[m] = True + + roll = np.zeros(n, bool) + ch = np.where((nmi[1:] != nmi[:-1]) & (nmi[1:] >= 0) & (nmi[:-1] >= 0))[0] + 1 + for i in ch: + s = C[order[nmi[i - 1]]][1] + if np.isfinite(s[i]) and np.isfinite(s[i - 1]) and np.isfinite(rP[i]): + old = (np.log(s[i]) - np.log(s[i - 1])) if log else (s[i] / s[i - 1] - 1.0) + c[i] = rP[i] - old; val[i] = True + roll[ch] = True + + return dict(ts=FD.to_ms(grid), i1h=c, cum=np.concatenate([[0.0], np.cumsum(c)]), + series=pd.Series(c, index=grid), switch=grid[0], valid=val, roll=roll, + bann=(px / idxp - 1.0) / (dte / 365.25), dte=dte, vol=vol, grid=grid, + nmi=nmi, order=order, px=px, idx=idxp, pp=pp) + + +def spot_rate(a: str, grid: pd.DatetimeIndex, log: bool = True) -> dict: + """Stessa forma, ma il carry di esprimere la posizione sullo SPOT: quanto il perpetual + sovra-rende l'INDICE (= il costo che si evita non usando il perpetual). Lo spot vero di + Deribit non ha storia in questo progetto: si usa l'INDICE, dichiarato come tale.""" + n = len(grid) + ix = aligned_index(a, grid) + perp = al.get(a, "1h").set_index("datetime")["close"].astype(float) + perp.index = pd.DatetimeIndex(perp.index) + pp = perp.reindex(grid).values.astype(float) + + def ret(v): + r = np.full(n, np.nan) + r[1:] = (np.log(v[1:]) - np.log(v[:-1])) if log else (v[1:] / v[:-1] - 1.0) + return r + c = np.nan_to_num(ret(pp) - ret(ix)) + return dict(ts=FD.to_ms(grid), i1h=c, cum=np.concatenate([[0.0], np.cumsum(c)]), + series=pd.Series(c, index=grid), switch=grid[0], + valid=np.isfinite(c), roll=np.zeros(n, bool), grid=grid) + + +# =========================================================================== +# 2) REPLICA DI CONTROLLO +# =========================================================================== +def book(rate_or_none, off: int = 0, tp_series: pd.Series | None = None) -> pd.Series: + """Libro LIVE 75/25 alla lente `hourly`. ⚠️ `FD._SKH` e' cachato su + `(asset, off, mode, rate is not None)`: due `rate` DIVERSI collidono. Si svuota sempre.""" + FD._SKH.clear() + s = FD.skh_book(off, "hourly", rate_or_none) + if s.index.tz is None: + s.index = s.index.tz_localize("UTC") + tp = tp_series if tp_series is not None else FD.tp01_realistic(ALLOC * W_TP, + rate_or_none)[0] + return combine_outer({"TP01": tp, "SKH01": s}, {"TP01": W_TP, "SKH01": W_SKH}, lo=LO_BOOK) + + +def sezione1(rate: dict) -> dict: + sez("1) REPLICA DI CONTROLLO — il -2,16%/anno prima di confrontarci qualsiasi cosa") + a0, a1 = FD.stat(book(None)), FD.stat(book(rate)) + t0, t1 = FD.stat(FD.tp01_realistic(ALLOC * W_TP, None)[0]), \ + FD.stat(FD.tp01_realistic(ALLOC * W_TP, rate)[0]) + FD._SKH.clear(); s0 = FD.stat(FD.skh_book(0, "hourly", None)) + FD._SKH.clear(); s1 = FD.stat(FD.skh_book(0, "hourly", rate)) + print(f" {'serie':<26} {'ShFULL':>8} {'ShHOLD':>8} {'maxDD':>7} {'CAGR':>8} {'drift':>8}") + for nm, x, y in (("LIBRO 75/25 path live", a0, a1), ("TP01 realistic", t0, t1), + ("SKH01 50/50 path live", s0, s1)): + print(f" {nm:<26} {x['full']:>8.3f} {x['hold']:>8.3f} {x['dd']:>7.1%} " + f"{x['cagr']:>8.2%} {x['drift']:>8.2%}") + print(f" {' + funding':<26} {y['full']:>8.3f} {y['hold']:>8.3f} {y['dd']:>7.1%} " + f"{y['cagr']:>8.2%} {y['drift']:>8.2%}") + print(f" {' DELTA':<26} {y['full']-x['full']:>+8.3f} {y['hold']-x['hold']:>+8.3f} " + f"{y['dd']-x['dd']:>+7.1%} {y['cagr']-x['cagr']:>+8.2%} " + f"{y['drift']-x['drift']:>+8.2%}") + d = a1["drift"] - a0["drift"] + ok = abs(d + 0.0216) < 0.0005 + print(f"\n drift del libro {a0['drift']:.2%} -> {a1['drift']:.2%} = {d:+.3%}/anno " + f"contro il -2,16% pubblicato -> {'REPLICATO' if ok else '⚠️ NON REPLICA'}") + assert ok, "la replica del funding non torna: nessun delta oltre questo punto" + return dict(book0=a0, book1=a1, tp0=t0, tp1=t1, skh0=s0, skh1=s1) + + +# =========================================================================== +# 3) L'ESTIMATORE — livello vs flusso, in una veste nuova +# =========================================================================== +def sezione2(rate: dict, grid: dict) -> None: + sez("2) L'ESTIMATORE — la differenza ARITMETICA dei rendimenti NON e' il carry") + print(" Il costo di tenere un datato e' MOLTIPLICATIVO (il basis si consuma in") + print(" proporzione), il funding e' un FLUSSO di cassa (tasso x nozionale). Chi misura il") + print(" primo con una differenza aritmetica di rendimenti si porta dentro un termine di") + print(" VARIANZA: media(rA) - media(rB) = [log-drift] + (var(rA)-var(rB))/2. Fra due") + print(" strumenti sullo STESSO sottostante il log-drift e' il carry e la differenza di") + print(" varianza e' microstruttura (il datato e' meno scambiato, la sua barra oraria e'") + print(" piu' stantia, la varianza misurata piu' bassa).") + print("\n CONTROLLO che rende visibile l'artefatto senza discuterne: il PERPETUAL contro") + print(" l'INDICE. La sua base e' stazionaria intorno a ~3 bps, quindi il carry vero e' ~0.") + print(f" {'asset':>6} {'aritmetico':>12} {'log':>10} {'artefatto':>11} {'base perp media':>17}") + for a in ASSETS: + sa = spot_rate(a, grid[a], log=False)["i1h"] + sl = spot_rate(a, grid[a], log=True)["i1h"] + ix = aligned_index(a, grid[a]) + pp = al.get(a, "1h").set_index("datetime")["close"].astype(float) + pp.index = pd.DatetimeIndex(pp.index) + bas = float(np.nanmean(pp.reindex(grid[a]).values / ix - 1.0)) * 1e4 + print(f" {a:>6} {sa.mean()*24*365.25:>+11.3%} {sl.mean()*24*365.25:>+9.3%} " + f"{(sa.mean()-sl.mean())*24*365.25:>+10.3%} {bas:>16.1f}b") + print(" -> l'aritmetico attribuisce al perpetual +0,4/+0,5%/anno di 'sovra-rendimento'") + print(" su un indice da cui non si stacca mai: e' varianza, non carry. Il log da' ~0.") + print("\n Lo stesso artefatto sul DATATO, che e' l'oggetto della domanda:") + print(f" {'asset':>6} {'carry aritm.':>13} {'carry LOG':>11} {'artefatto':>11} " + f"{'funding':>9}") + for a in ASSETS: + da = dated_rate(a, grid[a], log=False) + dl = dated_rate(a, grid[a], log=True) + f = rate[a]["i1h"] + print(f" {a:>6} {da['i1h'][da['valid']].mean()*24*365.25:>+12.3%} " + f"{dl['i1h'][dl['valid']].mean()*24*365.25:>+10.3%} " + f"{(da['i1h'][da['valid']].mean()-dl['i1h'][dl['valid']].mean())*24*365.25:>+10.3%} " + f"{f.mean()*24*365.25:>+8.3%}") + print(" 🚨 L'artefatto vale 1,5-2,6 punti percentuali l'anno, cioe' DA 2 A 4 VOLTE") + print(" l'effetto che questo filone sta cercando (~0,7%/anno). Con l'estimatore") + print(" aritmetico il datato sembra costare 4,1-5,2%/anno contro un funding del") + print(" 6,4-7,3%: un vantaggio enorme, e per meta' inventato dalla formula.") + print(" 📌 REGOLA: fra due strumenti sullo stesso sottostante il carry si misura sul") + print(" LOG del rapporto dei prezzi (telescopico: cum(t) = ln(P_t/F_t) - ln(P_0/F_0)),") + print(" mai sulla media delle differenze di rendimento. E' la stessa forma dell'errore") + print(" gia' codificato il 26/07 sui veicoli UCITS («la deviazione fra due veicoli") + print(" sullo stesso indice si misura sul RAPPORTO CUMULATO»): li' erano due ETF, qui") + print(" due contratti, ed e' passato lo stesso.") + print(" Da qui in avanti: LOG. L'aritmetico non compare piu' in nessun numero.") + + +# =========================================================================== +# 4) IL CONFRONTO APPAIATO +# =========================================================================== +def sezione3(rate: dict, DR: dict) -> dict: + sez("3) IL CONFRONTO APPAIATO — per GIORNO e per SEGNALE, e la tesi condizionale") + _, dg = FD.tp01_realistic(ALLOC * W_TP, rate) + out = {} + print(" Segnale CONGELATO (stesso `pos` in entrambe le colonne): cambia solo lo strumento.") + print(f" {'asset':>5} {'esposiz.':>9} {'costo perp':>11} {'costo datato':>13} " + f"{'delta':>9} {'t':>6} {'IC95 (bootstrap)':>22}") + rng = np.random.default_rng(822) + for a in ASSETS: + idx = pd.DatetimeIndex(dg[a]["idx"]) + pos = pd.Series(dg[a]["pos"], index=idx) + fD = pd.Series(FD.daily_funding(rate[a], idx), index=idx) + cD = pd.Series(FD.daily_funding(DR[a], idx), index=idx) + m = idx >= pd.Timestamp("2019-05-01", tz="UTC") + cp, cd = (pos * fD)[m], (pos * cD)[m] + d = (cd - cp).values + t = d.mean() / d.std() * np.sqrt(len(d)) + # bootstrap a blocchi (20 giorni): il carry e' persistente, l'IID sottostima l'errore + nb = max(1, len(d) // 20) + bs = [] + for _ in range(2000): + st = rng.integers(0, max(1, len(d) - 20), nb) + bs.append(np.mean(np.concatenate([d[s:s + 20] for s in st]))) + lo, hi = np.percentile(bs, [2.5, 97.5]) * 365.25 + out[a] = dict(perp=cp.mean() * 365.25, dat=cd.mean() * 365.25, + delta=d.mean() * 365.25, t=t, lo=lo, hi=hi, pos=pos, m=m) + print(f" {a:>5} {pos[m].mean():>9.4f} {out[a]['perp']:>10.3%} {out[a]['dat']:>12.3%} " + f"{out[a]['delta']:>+8.3%} {t:>+6.2f} [{lo:>+7.3%}, {hi:>+7.3%}]") + + print("\n LA TESI: il funding e' caro PROPRIO QUANDO SIAMO LONG. Lo e' anche il basis?") + print(f" {'asset':>5} | {'funding incond':>14} {'condiz.':>9} {'rapporto':>9} " + f"| {'basis incond':>13} {'condiz.':>9} {'rapporto':>9}") + for a in ASSETS: + idx = pd.DatetimeIndex(dg[a]["idx"]) + pos, m = out[a]["pos"], out[a]["m"] + bann = pd.Series(DR[a]["bann"], index=DR[a]["grid"]).resample("1D").mean().reindex(idx) + fann = pd.Series(rate[a]["i1h"], index=rate[a]["series"].index)\ + .resample("1D").sum().reindex(idx) * 365.25 + w = pos[m] + fu_, fc = float(fann[m].mean()), float((w * fann[m]).sum() / w.sum()) + bu, bc = float(bann[m].mean()), float((w * bann[m]).sum() / w.sum()) + out[a].update(f_unc=fu_, f_con=fc, b_unc=bu, b_con=bc) + print(f" {a:>5} | {fu_:>14.2%} {fc:>9.2%} {fc/fu_:>8.2f}x " + f"| {bu:>13.2%} {bc:>9.2%} {bc/bu:>8.2f}x") + print(" 📌 LA TESI REGGE, e in modo appaiato: il funding condizionale e' 1,9-2,6x") + print(" l'incondizionato (replica indipendente del 1,86-2,55x misurato stanotte), il") + print(" basis condizionale solo 1,5-1,7x. Il datato NON e' piu' economico in assoluto") + print(" — sui livelli incondizionati i due tassi sono quasi identici (6,4-7,5% contro") + print(" 6,6-7,3%, e BASIS-CALENDAR aveva gia' misurato che lo scarto contiene lo zero)") + print(" — lo diventa CONDIZIONATAMENTE, perche' si riprezza di meno contro di noi.") + print(" Il vantaggio non e' un premio: e' la parte di premio che il perpetual") + print(" ri-tariffa OGNI ORA e il datato ha gia' fissato all'ingresso.") + + print("\n ⚠️ MA IL VANTAGGIO NON E' STAZIONARIO — delta per anno (>0 = il datato costa DI PIU'):") + print(f" {'anno':>6} {'delta BTC':>11} {'delta ETH':>11} {'funding medio':>15} " + f"{'basis medio':>13}") + for y in range(2019, 2027): + cells = [] + for a in ASSETS: + idx = pd.DatetimeIndex(dg[a]["idx"]) + pos = out[a]["pos"] + fD = pd.Series(FD.daily_funding(rate[a], idx), index=idx) + cD = pd.Series(FD.daily_funding(DR[a], idx), index=idx) + g = (pos * (cD - fD)) + g = g[g.index.year == y] + cells.append(float(g.mean()) * 365.25 if len(g) > 30 else float("nan")) + fy = np.mean([rate[a]["series"][rate[a]["series"].index.year == y].mean() + for a in ASSETS]) * 24 * 365.25 + by = np.nanmean([pd.Series(DR[a]["bann"], index=DR[a]["grid"])[ + pd.Series(DR[a]["bann"], index=DR[a]["grid"]).index.year == y].mean() + for a in ASSETS]) + print(f" {y:>6} {cells[0]:>+10.3%} {cells[1]:>+10.3%} {fy:>15.2%} {by:>13.2%}") + return out + + +# =========================================================================== +# 5) IL ROLL E LA LIQUIDITA' +# =========================================================================== +def sezione4(rate: dict, DR: dict, V: dict | None, grid: dict) -> dict: + sez("4) IL ROLL E LA LIQUIDITA' — il costo che il perpetual NON ha") + _, dg = FD.tp01_realistic(ALLOC * W_TP, rate) + print(" Il perpetuo non si rolla. Il datato si', e il roll e' FORZATO (data di scadenza),") + print(" non scelto. Quanto costa dipende da tre cose che si MISURANO: quante volte, su") + print(" quanto nozionale, e quanto costa attraversare il libro di un datato.\n") + print(f" {'asset':>5} {'roll/anno':>10} {'turnover TP01':>14} {'turnover SKH01':>15} " + f"{'nozion. medio TP01':>19}") + tov = {} + for a in ASSETS: + idx = pd.DatetimeIndex(dg[a]["idx"]); pos = dg[a]["pos"] + yrs = (idx[-1] - idx[0]).days / 365.25 + to_tp = float(np.abs(np.diff(pos, prepend=0.0)).sum()) / yrs + rl = pd.Series(DR[a]["roll"].astype(float), index=DR[a]["grid"])\ + .resample("1D").max().reindex(idx).fillna(0.0).values + FD._SKH.clear() + tr = FD.skh_daily(a, 0, "hourly", rate)[1] + yrs_s = (max(t["t_out"] for t in tr) - min(t["t_in"] for t in tr)) / 1000 / 86400 / 365.25 + to_sk = 2 * len(tr) / yrs_s + tov[a] = dict(tp=to_tp, sk=to_sk, roll_year=float(rl.sum()) / yrs, + roll_turn=float((rl * np.abs(pos)).sum()) * 2 / yrs, pos=pos, idx=idx, + rl=rl, yrs=yrs) + print(f" {a:>5} {tov[a]['roll_year']:>10.2f} {to_tp:>13.2f}x {to_sk:>14.2f}x " + f"{pos.mean()*ALLOC*W_TP/2:>18,.2f}$") + print(" (turnover = somma di |Δnozionale| per anno, in multipli del capitale d'asset)") + + print("\n QUANTO COSTA ATTRAVERSARE — libro reale, ordine da $100/$240/$500 ($240 = il") + print(" nozionale massimo per asset di TP01 a $635). UNA istantanea; se il libro non basta") + print(" si dice, non si estrapola:") + xs = {} + if V: + print(f" {'strumento':<22} " + " ".join(f"{f'${u}':>10}" for u in TICKETS)) + for i in ("BTC_USDC-PERPETUAL", "BTC_USDC-25SEP26", "BTC_USDC", + "ETH_USDC-PERPETUAL", "ETH_USDC-25SEP26", "ETH_USDC"): + b = V["books"].get(i) + cells, vals = [], [] + for u in TICKETS: + cb = walk_cost_bps(b, u, "buy"); cs = walk_cost_bps(b, u, "sell") + if cb is None or cs is None: + cells.append(f"{'LIBRO KO':>10}") + else: + v = (cb + cs) / 2 + vals.append(v); cells.append(f"{v:>9.2f}b") + xs[i] = float(np.mean(vals)) if vals else None + print(f" {i:<22} " + " ".join(cells)) + print(" ⚠️ Il datato USDC ha un libro 45x piu' sottile del perpetual USDC per volume") + print(" 24h, ma a $240 di ticket si resta dentro i primi livelli: a QUESTA taglia") + print(" la liquidita' non e' il vincolo, il MEZZO SPREAD si'.") + else: + print(" ⚠️ NON MISURATO (nessuna istantanea del venue).") + + ov = {} + if xs: + print(f"\n SOVRACOSTO D'ESECUZIONE, per sleeve e per anno (fee reale {FEE_REALE*1e4:.1f}") + print(" bps/lato su perp e datato, 0 sullo spot — letta dal venue; mezzo spread misurato):") + print(f" {'':<24} {'BTC':>12} {'ETH':>12} {'sleeve 50/50':>14}") + for lbl, ip, sfee in (("datato vs perp", "-25SEP26", FEE_REALE), + ("spot vs perp", "", 0.0)): + per = {} + for a in ASSETS: + base = xs.get(f"{a}_USDC-PERPETUAL") + alt = xs.get(f"{a}_USDC{ip}" if ip else f"{a}_USDC") + if base is None or alt is None: + per[a] = float("nan"); continue + dside = (alt * 1e-4 + sfee) - (base * 1e-4 + FEE_REALE) + extra = tov[a]["tp"] * dside + if ip: # il roll aggiunge turnover solo al datato + extra += tov[a]["roll_turn"] * (alt * 1e-4 + sfee) + per[a] = extra + ov[lbl] = per + print(f" {'TP01 ' + lbl:<24} {per['BTC']:>+11.3%} {per['ETH']:>+11.3%} " + f"{np.nanmean(list(per.values())):>+13.3%}") + per = {} + for a in ASSETS: + base, alt = xs.get(f"{a}_USDC-PERPETUAL"), xs.get(f"{a}_USDC-25SEP26") + per[a] = tov[a]["sk"] * ((alt * 1e-4 + FEE_REALE) - (base * 1e-4 + FEE_REALE)) \ + if base is not None and alt is not None else float("nan") + ov["SKH datato"] = per + print(f" {'SKH01 datato vs perp':<24} {per['BTC']:>+11.3%} {per['ETH']:>+11.3%} " + f"{np.nanmean(list(per.values())):>+13.3%}") + print(" 📌 Il turnover di SKH01 e' 48-57x l'anno contro i 6,6-8,4x di TP01: lo stesso") + print(" mezzo spread costa a SKH01 ~7 volte tanto. Un cambio di strumento si") + print(" giudica per SLEEVE, non per libro.") + + print("\n NETTING — oggi TP01 e SKH01 si compensano sullo STESSO strumento; separarli lo") + print(" perde. Quanto vale? (griglia giornaliera, approssimazione dichiarata)") + for a in ASSETS: + FD._SKH.clear() + tr = FD.skh_daily(a, 0, "hourly", rate)[1] + idx = tov[a]["idx"] + ps = pd.Series(0.0, index=idx) + for t in tr: + t0 = pd.Timestamp(t["t_in"], unit="ms", tz="UTC").floor("D") + t1 = pd.Timestamp(t["t_out"], unit="ms", tz="UTC").floor("D") + ps[(idx >= t0) & (idx <= t1)] += t["dir"] + ps = ps.clip(-1, 1) + nT = W_TP * pd.Series(tov[a]["pos"], index=idx); nS = W_SKH * ps + sep = float((nT.diff().abs().fillna(abs(nT.iloc[0])) + + nS.diff().abs().fillna(abs(nS.iloc[0]))).sum()) / tov[a]["yrs"] + net = float((nT + nS).diff().abs().fillna(abs(nT.iloc[0] + nS.iloc[0])).sum()) / tov[a]["yrs"] + print(f" {a}: turnover separato {sep:.2f}x/anno · nettato {net:.2f}x/anno -> il netting " + f"vale {1-net/sep:.1%}") + print(" -> trascurabile: i due sleeve tradano quasi sempre in momenti diversi. Mettere") + print(" TP01 e SKH01 su due strumenti NON e' un argomento contro.") + return dict(tov=tov, xs=xs, ov=ov) + + +# =========================================================================== +# 6) LE SERIE + BANDA D'ANCORA +# =========================================================================== +def overlay(series: pd.Series, tov: dict, per_asset_bps: dict, roll: bool) -> pd.Series: + """Sottrae il sovracosto d'esecuzione GIORNO PER GIORNO (|Δpos| e roll), non come media + annua: e' un costo di turnover, e il turnover non e' uniforme nel tempo.""" + tot = None + for a in ASSETS: + if not np.isfinite(per_asset_bps.get(a, float("nan"))): + return series + pos, idx, rl = tov[a]["pos"], tov[a]["idx"], tov[a]["rl"] + c = np.abs(np.diff(pos, prepend=0.0)) * per_asset_bps[a] + if roll: + c = c + 2.0 * np.abs(pos) * rl * per_asset_bps[a] + s = pd.Series(0.5 * c, index=idx) + tot = s if tot is None else tot.add(s, fill_value=0.0) + return series.sub(tot.reindex(series.index).fillna(0.0)) + + +def sezione5(rate: dict, DR: dict, SR: dict, ex: dict) -> dict: + sez("5) LE SERIE — il libro nelle varianti, netto tutto, e la banda d'ancora") + tov, xs = ex["tov"], ex["xs"] + ds = {a: (xs[f"{a}_USDC-25SEP26"] - xs[f"{a}_USDC-PERPETUAL"]) * 1e-4 + for a in ASSETS} if xs else {a: float("nan") for a in ASSETS} + ss = {a: (xs[f"{a}_USDC"] * 1e-4 + 0.0) - (xs[f"{a}_USDC-PERPETUAL"] * 1e-4 + FEE_REALE) + for a in ASSETS} if xs else {a: float("nan") for a in ASSETS} + + tp = {k: FD.tp01_realistic(ALLOC * W_TP, r)[0] for k, r in + (("nessun carry", None), ("perp", rate), ("datato", DR), ("spot", SR))} + tp["datato+esec"] = overlay(tp["datato"], tov, ds, roll=True) + tp["spot+esec"] = overlay(tp["spot"], tov, ss, roll=False) + + var = {} + var["A) perp (IL LIBRO DI OGGI)"] = book(rate) + var["B) tutto sui datati"] = book(DR) + var["C) TP01 datato + SKH01 perp"] = book(rate, tp_series=tp["datato"]) + var["D) C + sovracosto d'esecuzione"] = book(rate, tp_series=tp["datato+esec"]) + var["E) TP01 spot + SKH01 perp"] = book(rate, tp_series=tp["spot+esec"]) + var["F) nessun carry (irreale)"] = book(None) + + print(f" {'variante':<32} {'ShFULL':>8} {'ShHOLD':>8} {'maxDD':>7} {'vol':>7} " + f"{'CAGR':>8} {'drift':>8} {'Δdrift':>8}") + base = FD.stat(var["A) perp (IL LIBRO DI OGGI)"]) + st = {} + for k, s in var.items(): + m = FD.stat(s); st[k] = m + print(f" {k:<32} {m['full']:>8.3f} {m['hold']:>8.3f} {m['dd']:>7.1%} {m['vol']:>7.2%} " + f"{m['cagr']:>8.2%} {m['drift']:>8.2%} {m['drift']-base['drift']:>+8.2%}") + + print("\n 📌 QUELLO CHE LA COLONNA `Δdrift` DA SOLA NASCONDE: il datato aggiunge drift E") + print(" VOLATILITA'. Il basis non e' costante — si riprezza — e quella oscillazione") + print(" entra nel rendimento della posizione. Il funding invece e' un flusso di cassa") + print(" che NON aggiunge varianza al prezzo. Per questo lo Sharpe del libro PEGGIORA") + print(" mentre il drift migliora. Chi cita solo il drift sta vendendo un cambio di") + print(" strumento come un miglioramento quando e' uno SCAMBIO.") + + print("\n SLEEVE TP01 da solo (dove il cambio ha senso), stessa lente:") + print(f" {'':<26} {'ShFULL':>8} {'ShHOLD':>8} {'maxDD':>7} {'vol':>7} {'drift':>8}") + for k in ("nessun carry", "perp", "datato", "datato+esec", "spot", "spot+esec"): + m = FD.stat(tp[k]) + print(f" {k:<26} {m['full']:>8.3f} {m['hold']:>8.3f} {m['dd']:>7.1%} {m['vol']:>7.2%} " + f"{m['drift']:>8.2%}") + + print("\n FINESTRE — il numero va citato con la finestra (regola di stanotte):") + print(f" {'variante':<32} {'FULL':>9} {'>=2022-03':>11} {'2025+ (hold-out)':>18}") + lo1, lo2 = pd.Timestamp("2022-03-09", tz="UTC"), pd.Timestamp("2025-01-01", tz="UTC") + A = var["A) perp (IL LIBRO DI OGGI)"] + for k in ("B) tutto sui datati", "C) TP01 datato + SKH01 perp", + "D) C + sovracosto d'esecuzione", "E) TP01 spot + SKH01 perp"): + B = var[k] + cells = [] + for lo in (None, lo1, lo2): + a_, b_ = (A, B) if lo is None else (A[A.index >= lo], B[B.index >= lo]) + cells.append((float(b_.mean()) - float(a_.mean())) * 365.25) + print(f" {k:<32} {cells[0]:>+8.2%} {cells[1]:>+10.2%} {cells[2]:>+17.2%}") + + print(f"\n{clock()} BANDA D'ANCORA — 23 offset della griglia 230m/690m di SKH01, mediana") + print(" delle DIFFERENZE APPAIATE (mai la differenza delle mediane).") + bands = {} + for k, tps, rr in (("C) TP01 datato", tp["datato"], rate), + ("D) C + esecuzione", tp["datato+esec"], rate), + ("E) TP01 spot", tp["spot+esec"], rate)): + d = {"sh": [], "hold": [], "dd": [], "drift": []} + for off in r02.OFFSETS: + a_ = FD.stat(book(rate, off=off)) + b_ = FD.stat(book(rr, off=off, tp_series=tps)) + d["sh"].append(b_["full"] - a_["full"]); d["hold"].append(b_["hold"] - a_["hold"]) + d["dd"].append(b_["dd"] - a_["dd"]); d["drift"].append(b_["drift"] - a_["drift"]) + bands[k] = {m: np.array(v) for m, v in d.items()} + print(f" {k}:") + for m, lbl in (("drift", "Δdrift"), ("sh", "ΔShFULL"), ("hold", "ΔShHOLD"), + ("dd", "ΔmaxDD")): + v = bands[k][m] + flat = (v.max() - v.min()) < 1e-6 + print(f" {lbl:<9} mediana appaiata {np.median(v):>+8.4f} positiva in " + f"{np.mean(v>0):>5.0%} dei 23 offset [min {v.min():+.4f}, max {v.max():+.4f}]" + f"{' <- COSTANTE: nessuna potenza, vedi sotto' if flat else ''}") + print(" ⚠️ Il Δdrift e' COSTANTE su tutti e 23 gli offset, e non e' robustezza: e'") + print(" un'identita'. Cambia solo la gamba TP01, che non dipende dall'offset di SKH01,") + print(" e il drift di una somma pesata e' la somma pesata dei drift -> «positivo a") + print(" 23/23» qui NON dice niente. La banda ha potenza sullo SHARPE e sul maxDD (che") + print(" dipendono dalla covarianza fra le gambe), e li' il verdetto e' netto:") + print(" l'hold-out del datato e' NEGATIVO a 0/23 offset, con e senza sovracosto.") + print(" (Un controllo che non puo' fallire e' esattamente cio' che questo progetto") + print(" chiama «un test senza potenza»: si dichiara, non si conta come conferma.)") + return dict(var=var, st=st, tp=tp, bands=bands, ds=ds, ss=ss) + + +# =========================================================================== +# 7) I MURI +# =========================================================================== +def sezione6(res5: dict) -> None: + sez("6) I MURI DI CAPITALE — la moneta giusta, perche' la rendita vive sul DRIFT") + need = CC.TARGET_EUR_DAY * 365.0 * CC.EURUSD / (1 - CC.TAX_RATE) + print(f" bersaglio 50 EUR/g netti = ${need:,.0f}/anno lordi (aliquota {CC.TAX_RATE:.0%}),") + print(f" de-luck del drift x{DELUCK} (MISURATO), MC {WR.N_PATHS} path -> risoluzione ~1%:") + print(" si citano 3 cifre significative, non il dollaro.\n") + print(f" {'variante':<32} {'drift':>8} {'vol':>7} {'perpetua':>9} {'muro':>12} {'Δmuro':>8}") + ref = None + for k in ("A) perp (IL LIBRO DI OGGI)", "D) C + sovracosto d'esecuzione", + "E) TP01 spot + SKH01 perp", "F) nessun carry (irreale)"): + r = res5["var"][k].values.astype(float) + r = r - (1.0 - DELUCK) * float(r.mean()) + _, perp, cap = WR.perp_and_wall(r, 1.0, need) + if ref is None: + ref = cap + print(f" {k:<32} {float(r.mean())*365.25:>8.2%} {float(r.std())*np.sqrt(365.25):>7.2%} " + f"{perp:>9.2%} ${cap:>11,.0f} {cap/ref-1:>+7.1%}") + print("\n 📌 Il muro pubblicato stanotte e' $325,0k col funding dentro (era $276,6k senza).") + print(" Qui si misura il DELTA con la stessa macchineria, non un muro nuovo: la") + print(" differenza di livello viene dal de-luck e dal MC, non dal cambio di strumento.") + + +# =========================================================================== +# 8) IL MURO CHE NON C'E' +# =========================================================================== +def sezione7(V: dict | None) -> None: + sez("7) IL MURO CHE NON C'E' — «tutti i datati Deribit sono INVERSE» e' FALSO") + print(" La sezione 34 di RESULTS-0822 chiude cosi': «tutti i futures datati Deribit sono") + print(" INVERSE: non esiste una linea USDC datata — stesso muro delle opzioni, e il") + print(" collaterale in BTC e' esso stesso esposizione direzionale. Non risolto.»") + print(" Il briefing di questo filone lo eredita come «probabilmente il muro che uccide il") + print(" filone». E' l'unica cosa che si poteva verificare con UNA chiamata pubblica.\n") + if not V: + print(" ⚠️ NON VERIFICATO in questa corsa (nessuna rete, nessuna cache).") + return + ins = V["instruments"] + usdc_dated = sorted(n for n, s in ins.items() + if s.get("settlement") == "USDC" and s.get("kind") == "future" + and "PERPETUAL" not in n and (n.startswith("BTC_USDC") + or n.startswith("ETH_USDC"))) + print(f" MISURATO: {len(usdc_dated)} futures DATATI con settlement **USDC** su BTC/ETH.") + for n in usdc_dated: + s = ins[n] + cre = dt.datetime.utcfromtimestamp(s["created"] / 1000).date() + exp = dt.datetime.utcfromtimestamp(s["expiry"] / 1000).date() + print(f" {n:<20} creato {cre} scade {exp} min {s['min_trade_amount']} " + f"tick {s['tick']} taker {s['taker']}") + q = [n for n in usdc_dated if n.endswith(("SEP26", "DEC26", "MAR27", "JUN27"))] + print(f"\n ✅ Il muro NON esiste: le due gambe del libro possono restare in USDC. I") + print(" trimestrali USDC hanno lo STESSO min_trade_amount del perpetual lineare") + print(" (0,0001 BTC ~ $7,7 · 0,001 ETH ~ $2,4) e la STESSA fee taker: a $635") + print(" l'eseguibilita' non e' il vincolo (6a volta in questa ondata).") + print(" ⚠️ E il collaterale in cripto non era nemmeno l'argomento giusto: se lo fosse") + print(" stato, il costo non sarebbe il carry ma il fatto che un conto denominato in") + print(" BTC e' long 1,0x anche da FLAT — e TP01 sta flat nel 44% dei giorni, cioe' si") + print(" sarebbe distrutto esattamente cio' per cui TP01 esiste (il taglio del DD).") + print("\n ⚠️ IL MURO VERO E' UN ALTRO, ed e' una DATA: la linea USDC datata e' nata il") + print(f" 2026-05-07 ({len(q)} trimestrali esistono, il secondo creato il 2026-08-06)") + print(" -> ~3,5 mesi di vita e ZERO roll trimestrali mai eseguiti su quella linea.") + print(" Tutta la misura di questo filone gira sui trimestrali INVERSE, che hanno 7,3") + print(" anni: e' un PROXY, esattamente come l'inverse lo era per il funding pre-2022.") + print(" ✅ E il proxy si VALIDA invece di assumerlo — i due contratti sulla stessa") + print(" scadenza devono prezzare lo stesso carry, e lo fanno:") + for a in ASSETS: + bi, bu = V["books"].get(f"{a}-25SEP26"), V["books"].get(f"{a}_USDC-25SEP26") + if not bi or not bu or not bi.get("index"): + continue + mi = (bi["bid"] + bi["ask"]) / 2; mu = (bu["bid"] + bu["ask"]) / 2 + print(f" {a}-25SEP26 (inverse) base {(mi/bi['index']-1)*1e4:+.1f} bps vs " + f"{a}_USDC-25SEP26 (lineare) base {(mu/bu['index']-1)*1e4:+.1f} bps " + f"-> scarto {(mu/bu['index']-mi/bi['index'])*1e4:+.1f} bps") + print(" 📌 REGOLA: un muro ereditato da un'altra sessione e' un'IPOTESI, e prima di") + print(" costruirci sopra un verdetto si ri-verifica. Qui e' costato una GET pubblica,") + print(" e la frase era diventata falsa 3 mesi prima che venisse scritta.") + print(" (Stessa forma della lezione del 26/07: «un blocco dichiarato e' un'ipotesi».)") + + +# =========================================================================== +# 9) SOTTOPRODOTTO — LO SPOT +# =========================================================================== +def sezione8(V: dict | None, res5: dict, rate: dict, SR: dict) -> None: + sez("8) SOTTOPRODOTTO — lo strumento a carry ZERO esiste, si chiama SPOT") + print(" La domanda del filone era «datato o perpetual». Cercando le specifiche e' saltato") + print(" fuori il terzo: Deribit quota SPOT BTC_USDC/ETH_USDC. Uno spot non paga funding,") + print(" non ha basis e non si rolla: il suo carry e' ZERO per costruzione.") + if V: + for i in ("BTC_USDC", "ETH_USDC"): + s = V["instruments"].get(i, {}) + b = V["books"].get(i, {}) + sp = (b["ask"] - b["bid"]) / ((b["ask"] + b["bid"]) / 2) * 1e4 if b.get("bid") else float("nan") + print(f" {i:<10} min {s.get('min_trade_amount')} taker {s.get('taker')} " + f"maker {s.get('maker')} spread {sp:.2f} bps vol 24h " + f"${(b.get('vol24usd') or float('nan')):,.0f}") + print(" ⚠️ La fee ZERO e' quella di OGGI ed e' dichiarata temporanea nell'annuncio del") + print(" 1° agosto («spot a zero fino al collegamento con Coinbase»): e' un tier che") + print(" puo' sparire, e `fee_watch` sorveglia i perpetual, non lo spot.") + print("\n Perche' funziona SOLO su TP01, e la misura che lo dimostra:") + _, dg = FD.tp01_realistic(ALLOC * W_TP, rate) + for a in ASSETS: + pos = dg[a]["pos"] + print(f" {a}: TP01 e' long-flat e il suo target per asset supera 1,0x nel " + f"{np.mean(pos > 1.0):.3%} dei giorni (massimo {pos.max():.3f}) -> il tetto") + print(f" strutturale dello spot (niente leva, niente short) e' quasi mai binding.") + print(" SKH01 e' long/SHORT a nozionale 1,0x: sullo spot non e' esprimibile. Meta' dei") + print(" suoi trade sono short e li' lo spot non esiste come strumento.") + print("\n DECOMPOSIZIONE del vantaggio dello spot su TP01 — due pezzi, non uno:") + mnc = FD.stat(res5["tp"]["nessun carry"]); msp = FD.stat(res5["tp"]["spot"]) + mpe = FD.stat(res5["tp"]["perp"]) + print(f" (1) funding evitato {mnc['drift']-mpe['drift']:>+7.2%}/anno " + f"(perp {mpe['drift']:.2%} -> senza-carry {mnc['drift']:.2%})") + print(f" (2) base del perpetual {msp['drift']-mnc['drift']:>+7.2%}/anno " + f"(senza-carry {mnc['drift']:.2%} -> indice {msp['drift']:.2%})") + print(" ❌ IPOTESI MIA, NATA E REFUTATA NELLA STESSA SESSIONE: mi aspettavo che il") + print(" secondo pezzo fosse una TERZA conferma della tesi condizionale — «quando siamo") + print(" long il perpetual e' a premio e quel premio decade». La prima stesura lo") + print(" misurava a +0,32%/anno e ci avrei creduto: era il MISALLINEAMENTO di un'ora") + print(" fra indice e barre (vedi [ALLINEAMENTO] in testa). Allineato, il pezzo vale") + print(" +0,07%/anno: il VERSO e' quello della tesi e l'IC95 esclude lo zero per un") + print(" soffio su entrambi gli asset, ma la TAGLIA e' il 3% del pezzo (1). Un verso") + print(" giusto a un trentesimo della grandezza non e' una conferma, e' un residuo:") + print(" tutto il vantaggio dello spot e' il funding evitato, e va detto cosi'.") + print(" 📌 E il modo in cui l'artefatto si e' fatto vedere e' la cosa da ricordare: non") + print(" dal numero, che era plausibile e nel verso atteso, ma dalla VOLATILITA' — la") + print(" variante spot mostrava +1,07pp di vol su una strategia identica, e uno strumento") + print(" che traccia lo stesso sottostante non puo' aggiungere un punto di vol.") + rngz = np.random.default_rng(823) + for a in ASSETS: + idx = pd.DatetimeIndex(dg[a]["idx"]); pos = pd.Series(dg[a]["pos"], index=idx) + c = pd.Series(FD.daily_funding(SR[a], idx), index=idx) + d = (pos * c).values + nb = max(1, len(d) // 20) + bs = [np.mean(np.concatenate([d[x:x+20] for x in + rngz.integers(0, max(1, len(d)-20), nb)])) + for _ in range(1000)] + lo, hi = np.percentile(bs, [2.5, 97.5]) * 365.25 + print(f" {a}: base del perpetual pagata da TP01 {d.mean()*365.25:+.3%}/anno " + f"IC95 a blocchi [{lo:+.3%}, {hi:+.3%}]") + + m0 = FD.stat(res5["tp"]["perp"]); m1 = FD.stat(res5["tp"]["spot+esec"]) + print(f"\n TP01: drift {m0['drift']:.2%} -> {m1['drift']:.2%} ({m1['drift']-m0['drift']:+.2%}/anno), " + f"Sharpe {m0['full']:.3f} -> {m1['full']:.3f}, maxDD {m0['dd']:.1%} -> {m1['dd']:.1%}") + print(" 📌 E' l'unica variante che alza il drift SENZA alzare la volatilita' (vol di TP01") + print(" 12,16% sul perpetual e 12,16% sullo spot, alla terza cifra): lo spot non ha una") + print(" base che si riprezza. Il datato invece scambia carry contro varianza, ed e' per") + print(" questo che il suo Sharpe non migliora mentre il suo drift si'.") + print("\n ⚠️ MA C'E' UNA DOMANDA APERTA CHE VALE PIU' DEL RISPARMIO, e non e' risolvibile") + print(" con un backtest. CLAUDE.md la registra gia' (07/08): i DERIVATI su cripto") + print(" potrebbero qualificarsi `c-quater` (26%) invece di `c-sexies` (33%). Se e'") + print(" cosi', spostare TP01 dal perpetual (derivato) allo SPOT (cripto-attivita')") + print(" sposta il suo P&L nel comparto al 33%.") + cagr = FD.stat(res5["var"]["A) perp (IL LIBRO DI OGGI)"])["cagr"] + print(f" Ordine di grandezza: 7 punti di aliquota su un CAGR di libro del {cagr:.1%} " + f"valgono ~{0.07*cagr:.2%}/anno,") + print(" cioe' PIU' del risparmio di carry che lo spot porta. La risposta e' del") + print(" commercialista, non del backtest — ed e' esattamente la stessa domanda gia'") + print(" aperta nel progetto, che ora ha un secondo motivo per essere posta.") + if V: + xs = {} + for i in (f"{a}_USDC" for a in ASSETS): + xs[i] = np.mean([x for x in (walk_cost_bps(V["books"].get(i), u, sd) + for u in TICKETS for sd in ("buy", "sell")) + if x is not None]) if V["books"].get(i) else float("nan") + for i in (f"{a}_USDC-PERPETUAL" for a in ASSETS): + xs[i] = np.mean([x for x in (walk_cost_bps(V["books"].get(i), u, sd) + for u in TICKETS for sd in ("buy", "sell")) + if x is not None]) if V["books"].get(i) else float("nan") + _, dgz = FD.tp01_realistic(ALLOC * W_TP, rate) + now, later = [], [] + for a in ASSETS: + to = float(np.abs(np.diff(dgz[a]["pos"], prepend=0.0)).sum()) / \ + ((pd.DatetimeIndex(dgz[a]["idx"])[-1] + - pd.DatetimeIndex(dgz[a]["idx"])[0]).days / 365.25) + base = xs[f"{a}_USDC-PERPETUAL"] * 1e-4 + FEE_REALE + now.append(to * (xs[f"{a}_USDC"] * 1e-4 + 0.0 - base)) + later.append(to * (xs[f"{a}_USDC"] * 1e-4 + FEE_REALE - base)) + print(f"\n ⚠️ E se la fee spot smettesse di essere zero: il sovracosto d'esecuzione di") + print(f" TP01 passa da {np.mean(now):+.3%}/anno a {np.mean(later):+.3%}/anno, cioe' " + f"mangia il {(np.mean(later)-np.mean(now))/0.0207:.0%} del vantaggio") + print(f" (che a livello di sleeve vale ~2,07%/anno). Sopravviverebbe, ma va") + print(f" sorvegliato: `fee_watch` oggi guarda i perpetual, non lo spot.") + print(" ⚠️ Secondo limite dichiarato: qui lo spot e' modellato con l'INDICE Deribit, non") + print(" col libro spot vero (che non ha storia in questo progetto). Sul carry e'") + print(" corretto per costruzione (l'indice E' il sottostante); sull'esecuzione no, e") + print(" infatti il mezzo spread e' preso dall'istantanea del libro vero.") + + +# =========================================================================== +# 10) GATE +# =========================================================================== +def sezione9(rate: dict, DR: dict, res5: dict, grid: dict) -> None: + sez("9) GATE — esito di ciascuno; quelli non girati sono DICHIARATI, non omessi") + + # causalita': controllo POSITIVO (un test che non puo' fallire non e' un test) + a = "BTC" + _, dg = FD.tp01_realistic(ALLOC * W_TP, rate) + idx = pd.DatetimeIndex(dg[a]["idx"]); pos = pd.Series(dg[a]["pos"], index=idx) + c = pd.Series(FD.daily_funding(DR[a], idx), index=idx) + base = float((pos * c).mean()) * 365.25 + ahead = float((pos * c.shift(-1).fillna(0.0)).mean()) * 365.25 + lag = float((pos * c.shift(1).fillna(0.0)).mean()) * 365.25 + print(f" causality : il carry dell'ora t si applica alla posizione tenuta in t,") + print(f" e il contratto e' scelto alla chiusura di t-1. Controllo") + print(f" POSITIVO: usando il carry di DOMANI {ahead:.3%} e di IERI") + print(f" {lag:.3%} contro {base:.3%} -> il test ha POTENZA " + f"({abs(ahead-base)*1e4:.0f}/{abs(lag-base)*1e4:.0f} bps di scarto).") + + # sensibilita' al parametro d'esecuzione (griglia dichiarata) + print(f" griglia del roll : BUF in {BUF_GRID} giorni (parametro d'ESECUZIONE, e un") + print(" parametro d'esecuzione scelto guardando il risultato e'") + print(" selezione come ogni altra — regola 27/07). Canonico BUF=5") + print(" dichiarato PRIMA. Δdrift di libro per cella:") + for b in BUF_GRID: + DRb = {x: dated_rate(x, grid[x], buf=b) for x in ASSETS} + tpb = FD.tp01_realistic(ALLOC * W_TP, DRb)[0] + d = (float(book(rate, tp_series=tpb).mean()) + - float(res5["var"]["A) perp (IL LIBRO DI OGGI)"].mean())) * 365.25 + print(f" BUF={b:>2}g -> Δdrift {d:>+7.3%}" + f"{' <- canonico' if b == BUF_CANON else ''}") + print(" -> piatto: la scelta non e' dove vive il risultato.") + + # de-levering null + A = res5["var"]["A) perp (IL LIBRO DI OGGI)"] + D = res5["var"]["D) C + sovracosto d'esecuzione"] + sa, sd = FD.stat(A), FD.stat(D) + k = sa["vol"] / sd["vol"] + rk = D.values.astype(float) * k + shk = float(np.mean(rk) / np.std(rk) * np.sqrt(365.25)) + print(f" null del de-levering : NON APPLICABILE nella forma consueta (nessun claim di") + print(f" «meno drawdown»: il maxDD si muove di {sd['dd']-sa['dd']:+.2%}).") + print(f" Girato il suo SPECULARE — a ISO-VOLATILITA' il candidato") + print(f" deve restare migliore: k={k:.3f} -> Sharpe {shk:.3f} contro") + print(f" {sa['full']:.3f} del baseline -> " + f"{'PASSA' if shk > sa['full'] else 'FALLISCE'} (il datato compra drift PAGANDOLO in") + print(" varianza, ed e' esattamente quello che il gate deve vedere).") + + E = res5["var"]["E) TP01 spot + SKH01 perp"] + se = FD.stat(E) + ke = sa["vol"] / se["vol"] + rke = E.values.astype(float) * ke + she = float(np.mean(rke) / np.std(rke) * np.sqrt(365.25)) + print(f" Stesso gate sulla variante SPOT: k={ke:.3f} -> Sharpe " + f"{she:.3f} contro {sa['full']:.3f} -> " + f"{'PASSA' if she > sa['full'] else 'FALLISCE'} (lo spot alza il drift senza") + print(" comprare varianza: e' l'unica delle tre che sopravvive).") + + ns = int(sum(len(FD.skh_daily(x, 0, "hourly", rate)[1]) for x in ASSETS)) + nl = int(sum(np.sum(np.array([t["gross"] for t in FD.skh_daily(x, 0, "hourly", rate)[1]]) <= 0) + for x in ASSETS)) + ip = al.implausible_sharpe(D, n_trades=ns, n_losing_trades=nl) + print(f" implausible_sharpe : implausible={ip['implausible']} (Sharpe {ip['sharpe']:.2f}, " + f"maxDD {ip['maxdd']:.1%}, Calmar {ip['calmar']:.2f}) -> {ip['reasons'] or 'nessuna firma'}") + print(f" anchor_luck (banda) : GIRATO, 23 offset, mediana delle differenze APPAIATE " + f"(sez. 5).") + print(" replica bit-exact : PASSATA — il -2,16%/anno del funding e' riprodotto in sez. 1") + print(" e l'assert ferma lo script se non torna.") + print(" marginal_vs_tp01 / deflated_sharpe / study_family_honest / weights_tilt_null:") + print(" NON GIRATI, e non per budget. Qui non c'e' un CANDIDATO:") + print(" il segnale e' congelato, i pesi non si toccano, non c'e'") + print(" una famiglia da cui selezionare. C'e' un COSTO che si") + print(" sposta cambiando strumento. Un `marginal_vs_tp01` su TP01") + print(" eseguito diversamente misurerebbe TP01 contro se stesso.") + + +def main() -> None: + print("=" * 100) + print("r0822e — FUNDING-AVOID: il 2,16%/anno si evita cambiando strumento?") + print(f"branch research/wave-0822 · {pd.Timestamp.now(tz='UTC')} · rete={'SI' if _NET else 'NO'}") + print("=" * 100) + print(" IPOTESI A PRIORI, registrata prima di misurare: il basis di un datato e il funding") + print(" di un perpetual sono lo STESSO premio (arbitraggio), quindi in livello devono") + print(" costare uguale; se il datato vince e' solo perche' il funding si riprezza contro") + print(" chi e' long e il basis no. Attesa: differenza piccola, forse nulla, e comunque") + print(" mangiata dal roll o dall'ineseguibilita'.") + + V = venue_snapshot() + d0 = sezione0(V) + + F = {} + for instr, (start, asset, kind) in FD.INSTR.items(): + d = FD.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") + rate = FD.build_rate(F) + grid = {a: pd.DatetimeIndex(rate[a]["series"].index) for a in ASSETS} + + verifica_allineamento(grid) + + print(f"\n{clock()} sezione 1...") + sezione1(rate) + + DR = {a: dated_rate(a, grid[a], buf=BUF_CANON, log=True) for a in ASSETS} + SR = {a: spot_rate(a, grid[a], log=True) for a in ASSETS} + print(f"\n copertura del front datato sulla griglia del funding: " + " · ".join( + f"{a} {DR[a]['valid'].mean():.4f}" for a in ASSETS)) + + print(f"\n{clock()} sezione 2...") + sezione2(rate, grid) + print(f"\n{clock()} sezione 3...") + sezione3(rate, DR) + print(f"\n{clock()} sezione 4...") + ex = sezione4(rate, DR, V, grid) + print(f"\n{clock()} sezione 5...") + res5 = sezione5(rate, DR, SR, ex) + print(f"\n{clock()} sezione 6...") + sezione6(res5) + sezione7(V) + sezione8(V, res5, rate, SR) + print(f"\n{clock()} sezione 9...") + sezione9(rate, DR, res5, grid) + + sez("SINTESI") + A = FD.stat(res5["var"]["A) perp (IL LIBRO DI OGGI)"]) + for k in ("B) tutto sui datati", "C) TP01 datato + SKH01 perp", + "D) C + sovracosto d'esecuzione", "E) TP01 spot + SKH01 perp"): + m = res5["st"][k] + print(f" {k:<32} Δdrift {m['drift']-A['drift']:>+7.2%} ΔShFULL " + f"{m['full']-A['full']:>+7.3f} ΔShHOLD {m['hold']-A['hold']:>+7.3f} " + f"ΔmaxDD {m['dd']-A['dd']:>+6.2%}") + c_perp = res5["st"]["F) nessun carry (irreale)"]["drift"] - A["drift"] + print(f"\n costo del carry per il libro, sul perpetual: {c_perp:.2%}/anno " + f"(il -2,16% replicato)") + dD = res5["st"]["D) C + sovracosto d'esecuzione"]["drift"] - A["drift"] + tot = c_perp - dD + print(f" ... e su TP01-datato/SKH01-perp, tutto compreso (carry + mezzo spread + roll):" + f" {tot:.2%}/anno") + mD = res5["st"]["D) C + sovracosto d'esecuzione"] + print(f" risparmio netto: {dD:+.2%}/anno di drift, pagato con " + f"{mD['full']-A['full']:+.3f} di Sharpe FULL e {mD['hold']-A['hold']:+.3f} di hold-out.") + D_ = res5["st"]["D) C + sovracosto d'esecuzione"] + E_ = res5["st"]["E) TP01 spot + SKH01 perp"] + costo_dat = (c_perp - dD) * 100 + sez("VERDETTO") + print(" IPOTESI: esprimere il segnale congelato su un future DATATO invece che sul") + print(" perpetual costa meno del funding, perche' il basis e' fissato all'ingresso") + print(" mentre il funding si riprezza contro chi e' long. Attesa PRIMA di misurare:") + print(" differenza piccola o nulla, e comunque mangiata dal roll o dall'ineseguibilita'.") + print(" GRIGLIA DICHIARATA (al RIALZO): 2 estimatori (aritmetico/log) x 4 buffer di roll") + print(" (2/5/10/20 g) x 3 strumenti (perp/datato/spot) x 2 sleeve = 48 valutazioni di") + print(" carry; 6 varianti di libro x 23 offset d'ancora = 138 valutazioni di libro;") + print(" 4 muri x 26 passi di bisezione x 2.000 path. NESSUNA e' una SELEZIONE: il") + print(" segnale e i pesi sono congelati, non c'e' una cella scelta guardando il") + print(" risultato -> nessun deflated-Sharpe da calcolare.") + print(f" NUMERI (netti fee, a $600 di allocazione, lente `hourly` del libro live):") + print(f" A) perpetual, oggi Sh {A['full']:.3f} / hold {A['hold']:.3f} / " + f"maxDD {A['dd']:.1%} / CAGR {A['cagr']:.2%} / drift {A['drift']:.2%}") + print(f" D) TP01 datato, tutto dentro Sh {D_['full']:.3f} / hold {D_['hold']:.3f} / " + f"maxDD {D_['dd']:.1%} / CAGR {D_['cagr']:.2%} / drift {D_['drift']:.2%}") + print(f" E) TP01 spot, tutto dentro Sh {E_['full']:.3f} / hold {E_['hold']:.3f} / " + f"maxDD {E_['dd']:.1%} / CAGR {E_['cagr']:.2%} / drift {E_['drift']:.2%}") + print(f" haircut a $600: gia' dentro (le serie girano a $600 con min-order $5); il") + print(f" ticket massimo per asset e' $240 e resta nei primi livelli del libro datato.") + print(" VERDETTO: **SCARTATO** per i datati (il risparmio di drift si paga in varianza:") + print(f" hold-out {D_['hold']-A['hold']:+.3f}, negativo a 0/23 offset, e il gate") + print(" iso-volatilita' fallisce) — **LEAD** per lo SPOT su TP01, con la domanda") + print(" fiscale aperta che vale piu' del risparmio.") + print(" COSA MI SMENTIREBBE: (a) sui datati — un campione in cui il basis condizionale") + print(" NON si riprezza meno del funding, cioe' un regime in cui l'hold-out smette di") + print(" essere negativo: qui il vantaggio e' tutto 2019-2023 e vale ~0 dal 2024;") + print(" (b) sullo spot — che il fisco tratti i derivati come le cripto-attivita' (33%") + print(" entrambi): allora il LEAD diventa un candidato d'esecuzione; oppure che la fee") + print(" spot 0 sparisca col collegamento Coinbase (misurato in sez. 8: il vantaggio") + print(" si riduce ma non si annulla);") + print(" (c) su entrambi — che il mio proxy inverse per il carry dei datati non valga:") + print(" si smentisce raccogliendo qualche mese di quote della linea USDC datata e") + print(" confrontando i basis, oggi a 0,4-0,9 bps l'uno dall'altro.") + print(" SCRIPT: scripts/research/r0822e_funding_avoid.py") + print() + print(f"ESPRIMERE IL LIBRO SUI DATATI: COSTA {costo_dat:.2f} %/ANNO CONTRO 2,16% DEL PERP " + f"-> NON CONVIENE, perche' i {dD*100:.2f} punti risparmiati sul drift si pagano con " + f"{D_['hold']-A['hold']:+.3f} di Sharpe hold-out (negativo a 0/23 offset d'ancora) e " + f"il gate iso-volatilita' fallisce; NON e' INESEGUIBILE (il muro «tutti i datati " + f"sono INVERSE» e' FALSO: 16 datati USDC esistono da 2026-05-07, min-order e fee " + f"identici al perpetual) ma la linea USDC ha 3,5 mesi di vita e zero roll mai " + f"eseguiti.") + print(f"\n{clock()} fine.") + + +if __name__ == "__main__": + main() From 0f64eb4720225c9110a74b3a8f4229b8e44be967 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 00:19:05 +0000 Subject: [PATCH 055/114] =?UTF-8?q?CLAUDE.md:=20evitare=20il=20funding=20?= =?UTF-8?q?=E2=80=94=20datati=20scartati,=20spot=20lead=20subordinato=20al?= =?UTF-8?q?la=20domanda=20fiscale,=20e=20il=20muro=20USDC=20di=20ieri=20fa?= =?UTF-8?q?lsificato?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index f041bd4..66aee03 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2602,6 +2602,52 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis massa passa il 50%. **Li' la mediana non e' robusta**, ed e' esattamente il tipo di numero netto che si sarebbe finito per citare come soglia di progetto. +- 💡 **EVITARE IL FUNDING — datati SCARTATI, ma lo SPOT e' un LEAD e la domanda fiscale vale piu' del + risparmio (2026-08-23, `r0822e_funding_avoid.py`).** Registro §39. **Libro/pesi/cron/config + INVARIATI.** Primo filone della sessione che poteva **restituire** drift invece di toglierlo. + **Il segnale resta CONGELATO** (TSMOM 30/90/180, vol-target 20%): cambia solo lo **strumento**. + | variante | ShFULL | ShHOLD | drift | Δdrift | muro | + |---|---|---|---|---|---| + | **A) perp (oggi)** | 1,513 | 1,154 | 17,08% | — | **$325,0k** | + | D) TP01 datato + SKH01 perp | 1,507 | **1,118** | 17,53% | +0,47% | $319,5k | + | **E) TP01 SPOT + SKH01 perp** | **1,649** | **1,203** | 18,63% | **+1,55%** | **$289,6k (−10,9%)** | + | F) nessun carry (irreale) | 1,692 | 1,214 | 19,24% | +2,16% | $276,6k | + ❌ **DATATI: NON CONVIENE.** La tesi condizionale **regge** (funding condizionale **1,94×/2,59×** + l'incondizionato — ✅ replica indipendente del 1,86-2,55× — contro basis **1,47×/1,66×**: in + **livello** i carry sono identici, il datato vince **solo perche' si riprezza meno**), ma il + vantaggio e' **tutto 2019-2023**, ~0 o avverso dal 2024 → **ShHOLD −0,034 negativo a 0/23 offset** e + **gate iso-volatilita' FALLITO**. SKH01 sui datati **peggiora** (turnover **48-57×/anno** contro + 6,6-8,4× di TP01 → lo stesso mezzo spread gli costa **7 volte tanto**). + 💡 **LEAD: lo strumento a carry ZERO esiste ed e' lo SPOT** (`BTC_USDC`/`ETH_USDC`, taker **0,0** + oggi, min 0,0001). TP01 e' long-flat e supera 1,0× nello **0,102%** dei giorni → il tetto non morde; + **+1,55%/anno di drift a volatilita' IDENTICA** (supera il gate iso-vol), muro **−10,9%**. + ⚠️ **NON e' un candidato, tre ragioni dichiarate:** (a) **SKH01 non e' esprimibile sullo spot** + (long/short); (b) la **fee 0 e' dichiarata temporanea** e **`fee_watch` non sorveglia lo spot** — + se sparisse, **−13% del vantaggio** (misurato); (c) 🚨 **la domanda fiscale aperta vale PIU' del + risparmio**: se i derivati sono `c-quater` (26%) e lo spot `c-sexies` (33%), **7 punti su un CAGR + del 17,9% = ~1,25%/anno**, cioe' **piu' di quello che lo spot porta** → **la domanda al + commercialista viene PRIMA di qualunque prova**, ed e' la stessa gia' aperta dal 07/08. + 🚨 **MURO EREDITATO FALSIFICATO (correzione a §34, scritto poche ore prima):** *"tutti i futures + datati Deribit sono INVERSE, non esiste una linea USDC datata"* e' **FALSO** — esistono **16 datati + USDC su BTC/ETH** (il piu' vecchio `BTC_USDC-25SEP26`, creato **2026-05-07**), taker **identico al + perpetual**, min 0,0001 BTC / 0,001 ETH. ✅ Verificato con GET pubblica. **La svista e' di + NAMESPACE:** `get_instruments?currency=BTC` **non li ritorna**, stanno sotto **`currency=USDC`** — + *e il coordinatore ha commesso la stessa svista al primo controllo, ottenendo 0.* **Il muro vero e' + un altro: 3,5 mesi di vita e ZERO roll trimestrali mai eseguiti.** *(6ª volta nell'ondata che + l'eseguibilita' non e' il vincolo.)* + 📌 **REGOLA: il carry si misura sul LOG del rapporto dei prezzi, mai sulla media aritmetica delle + differenze** — quella contiene `(var A − var B)/2`, che qui vale **1,48%/2,60% l'anno = 2-4× + l'effetto cercato**. Controllo che lo rende visibile: col metodo aritmetico il perpetual + "sovra-rende" il proprio indice di +0,4/+0,5%/anno, col log ~0. **Stessa forma della regola UCITS + del 26/07**, in veste nuova. + ⚠️ **BUG in famiglia gia' nota, e NON visto dal numero:** `fund_*.parquet` etichetta l'indice + **all'istante T**, `al.get(a,'1h')` la barra **all'inizio** → appaiarle **sfasa di un'ora** + (corr **0,996 al lag +1**, ~0 al lag 0), gonfia la sd della base da 8 a **68-86 bps** e produceva un + finto **+0,32%/anno nel verso atteso**. 📌 **Si e' visto dalla VOLATILITA' (+1,07pp su una strategia + identica), non dal numero.** Il pezzo vero vale **+0,07%/anno**. **REGOLA: due serie temporali dello + stesso progetto possono avere convenzioni di etichettatura diverse — verificare col LAG, non + fidarsi dell'indice.** + - **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso: cross-sectional su universo Hyperliquid certificato (XS01) → portafoglio Sharpe ~1.55. - **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.** From 9973b04bc174bfe2ca5f233a03346a64872d2421 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 00:37:46 +0000 Subject: [PATCH 056/114] =?UTF-8?q?research(wave-0822):=20SL-ANCHOR=20?= =?UTF-8?q?=E2=80=94=20il=20nodo=20sciolto=20dal=20LOG=20(la=20SPEC=20lo?= =?UTF-8?q?=20diceva=20non=20misurabile);=20rotolante=20ma=20il=20timore?= =?UTF-8?q?=20e'=20refutato,=20e=20il=20docstring=20della=20cadenza=20e'?= =?UTF-8?q?=20una=20trappola?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 68 ++++ scripts/research/r0823_sl_anchor.py | 534 ++++++++++++++++++++++++++++ 2 files changed, 602 insertions(+) create mode 100644 scripts/research/r0823_sl_anchor.py diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index b19af9b..c6e19df 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -705,6 +705,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 37 | GATE-C | l'ultima gamba del gate funded + il biglietto come opzione | **PASS 2/2** — gate senza date; **P(≥50/g) 4,4% → 2,6%** | | 38 | SCALE-SPEC | specificare il knob di leva che non esiste | **LEAD** — specificata, **1 nodo irrisolto** sul disaster-SL | | 39 | FUNDING-AVOID | evitare il funding esprimendo il libro sui datati | **SCARTATO** sui datati · **LEAD** sullo **spot** | +| 40 | SL-ANCHOR | il nodo che bloccava il gradino di leva | **SCIOLTO** — rotolante, ma il timore e' refutato | | 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 | @@ -1982,3 +1983,70 @@ risparmio** — se i derivati sono `c-quater` (26%) e lo spot `c-sexies` (33%), (i 0,47 punti risparmiati si pagano con −0,034 di ShHOLD, negativo a 0/23 offset, e il gate iso-vol fallisce). **SCARTATO** sui datati, **LEAD** sullo spot per la sola gamba TP01 — subordinato alla domanda fiscale, che va posta al commercialista **prima** di qualunque prova. + +--- + +## 40 — SL-ANCHOR (il nodo del disaster-SL, sciolto) + +`scripts/research/r0823_sl_anchor.py` (**2,0 s**, sola lettura). **42 celle**, nessuna riduzione per +budget. ✅ **Sola lettura provata**: `git status` mostra solo questo file; il ramo venue fa **2 sole** +`get_open_orders` e si astiene nei minuti :05-:10 e :24-:30. + +🚨 **La dichiarazione di non-misurabilita' della SPEC era FALSA: il log bastava.** Ieri notte +`SPEC-scale-key.md` scriveva *"letto nel codice, NON misurato — il log non lo permette (servirebbe +`pos_prima`): non ricostruibile a posteriori"*. **Terza conferma della regola del 26/07: un blocco +dichiarato e' un'IPOTESI, non un fatto.** + +📌 **L'agente ha scritto l'attesa PRIMA di guardare, derivandola dal codice — e ha predetto le soglie +esatte:** rotolante e **simmetrico**, con innesco a **+5,263%** (salita) / **−4,762%** (discesa), +perche' il fattore 0,70 si semplifica e la condizione `|stp−want|/want ≥ 0,05` diventa una condizione +**sul mark**, non sullo stop. + +**Misura sul log di produzione** (1443 giri, 60,1 giorni, 2840 righe-asset): i 15 `placed` si +scompongono in **5 prime aperture + 7 ribilanci + 3 A POSIZIONE FERMA**. Le tre — 21/08 00:07 BTC, +06:07 ETH, 22:07 BTC — hanno azione `HOLD (a target)`, **0 ordini**, e **trigger spostato**: salti +**+5,74% / +5,81% / +5,38%**, tutti **appena sopra il +5,263% predetto**. +**Conferma dal venue:** la posizione BTC e' aperta dal **19/08 15:07**, l'ordine di stop sul book e' +stato creato il **22/08 01:07** → **l'ordine e' 2,5 giorni piu' giovane della posizione che protegge.** + +🚨 **MA LA CONSEGUENZA CHE BLOCCAVA IL GRADINO E' REFUTATA, e va nel verso OPPOSTO al timore:** uno +stop che segue il prezzo **si allontana dal mercato**, quindi scatta **meno**. Episodi su storia reale +(caso peggiore: sempre long, rientro immediato): a **1h** — la cadenza che gira — BTC `roll` **0** +contro `floor` 1; ETH `roll` **1** contro 1; **mai piu' di 1 in 30 giorni**. Caduta massima storica +dentro un giro: BTC 1h **−23,17%**, ETH **−30,35%**, contro i ~26,5-30% che servirebbero. +📌 **E nell'outage — l'unico scenario per cui il bracket esiste — il ri-ancoraggio e' SPENTO per +costruzione**: nessun giro di cron, nessun ri-piazzamento ⇒ **un episodio solo**. +**⇒ Il 37,5% resta il costo di un episodio singolo, il margine 1,33× di WORST-DAY e' quello vero, e la +sua gamba REGGE.** + +**Due cose da registrare, entrambe INDIPENDENTI da k:** +1. ⚠️ **Il nome inganna: "disaster-SL −30%" suggerisce un massimo di perdita per trade che NON + ESISTE.** Su cicli a ingresso proprio, il rotolante lascia passare perdite oltre −30% **senza + scattare** nel **0,03% (24h) / 0,96-2,84% (7g) / 7,22-13,33% (30g)** dei cicli, e il peggior DD + dall'ingresso e' **−60,6% BTC / −61,5% ETH** contro −38,3%/−45,4% del pavimento. +2. 🚨 **L'accumulo e' funzione della CADENZA DEL CRON, non della leva — e c'e' una trappola viva in + produzione:** il docstring di `book_execute.py` prescrive *"ogni ~230 minuti"* mentre **il cron gira + ogni ora**. Chi "correggesse" la cadenza verso il docstring sposterebbe il libro sulla riga **4h**, + **dove BTC raddoppia gli scatti** (`roll` 2 contro 1). A 24h ETH arriva a 5, con 2 in 30 giorni. + +⚠️ **Trappola n.10 del brief catturata in sessione:** la colonna "perdita dall'ingresso" della prima +stesura era un **artefatto** (posizione aperta dal 2018 ⇒ era il DD di un buy&hold pluriennale) → +dichiarata e **rifatta con cicli a ingresso proprio**. E una prima versione della simulazione e' stata +**scartata** perche' il rientro avveniva fuori dal giro di cron (irrealistico). +✅ **Controllo positivo superato** (obbligatorio): lo stesso esercizio applicato a un'implementazione +**a pavimento** risponde `ok` → l'esercizio **distingue** le due implementazioni, non passa e basta. +✅ **Guardia anti-rete superata**: la sottoclasse che esercita il vero `ensure_disaster_sl` ha `_post` +che **solleva**, e l'esercizio e' riuscito ⇒ **nessuna chiamata di rete e' partita da quel percorso.** + +⚠️ **Limite dichiarato:** **0 ri-ancoraggi verso il BASSO osservati su 3** — i 2 mesi di vita del libro +sono **un solo regime** (BTC da ~$66,5k a ~$78,6k) e non c'e' mai stato un calo del mark >4,76% con +posizione aperta. **La simmetria e' un fatto di CODICE** (un solo `abs()`, esercitato in entrambi i +versi), **non un fatto OSSERVATO**: la prima osservazione utile arriva al primo ribasso >5% con +posizione aperta. +📌 **Nota di metodo:** la storia degli ordini **non e' raggiungibile dal percorso del conto** — il +gateway `cerbero-mcp` espone 32 tool Deribit e **nessuno** e' `get_order_history_by_currency`. Le +chiavi dirette appartengono a un altro progetto e **non sono state usate**: sarebbe stato inventare un +percorso, *e la domanda non ne aveva bisogno.* + +**VERDETTO: `IL DISASTER-SL E' ROTOLANTE (costo vero 37,5% contro 37,5% — un episodio solo alla cadenza +di produzione; sale a ~75% solo con cron a 24h, che non e' la configurazione che gira)`.** diff --git a/scripts/research/r0823_sl_anchor.py b/scripts/research/r0823_sl_anchor.py new file mode 100644 index 0000000..37ae9cd --- /dev/null +++ b/scripts/research/r0823_sl_anchor.py @@ -0,0 +1,534 @@ +"""SL-ANCHOR — il disaster-SL del libro live e' un PAVIMENTO o un −30% ROTOLANTE? + +Il nodo lasciato aperto da `docs/research/SPEC-scale-key.md` §4.5: la guardia che morde per prima +sul gradino di leva e' il costo del disaster-SL (B = 37,5% dell'equity a k=1,25). La SPEC osserva +*dalla lettura del codice* che `src/live/execution.py::ensure_disaster_sl` ri-ancora lo stop al mark +a ogni giro, e conclude: «B e' il costo di UN episodio, un ribasso lento puo' produrne piu' d'uno», +dichiarando la cosa **non ricostruibile a posteriori** perche' «servirebbe `pos_prima` nel log». + +Quella dichiarazione di non-misurabilita' e' un'IPOTESI, e qui viene sfidata (regola del 26/07). +Il log di produzione stampa, a ogni giro e per ogni asset, **l'azione** (`HOLD` = nessun ordine = +posizione invariata) e **lo stato + il prezzo di trigger del bracket**; e nello stato `ok` quel +prezzo e' letto dal venue (`get_open_orders -> trigger_price`), non calcolato. Un `placed` su una +riga `HOLD (a target)` e' quindi esattamente l'evento cercato: bracket ricostruito a posizione +ferma. La domanda ERA gia' rispondibile col log che c'e'. + +SOLA LETTURA. Questo script: + - non importa nulla che invii ordini se non per ISPEZIONARLO (la sottoclasse di §1 ha `_post` che + SOLLEVA: se il codice di produzione toccasse la rete, il test fallirebbe rumorosamente); + - tocca il venue solo con `--venue`, solo `get_open_orders` (lettura), 2 richieste, e si rifiuta + di partire nei minuti :05-:10 (cron_book) e :24-:30 (cron_chain). + + uv run python scripts/research/r0823_sl_anchor.py # attesa + log + simulazione + uv run python scripts/research/r0823_sl_anchor.py --venue # + riscontro sul conto (2 letture) +""" +from __future__ import annotations + +import re +import sys +import time +from collections import Counter +from datetime import datetime, timezone +from pathlib import Path + +import numpy as np +import pandas as pd + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) + +from src.data.downloader import load_data # noqa: E402 +from src.live.deribit import DISASTER_LABEL, disaster_stop_price # noqa: E402 +from src.live.execution import DeribitTrader # noqa: E402 + +LOG = PROJECT_ROOT / "logs" / "cron_book.log" +SL_PCT = 0.30 # config/live.json -> disaster_sl_pct +TOL = 0.05 # tolleranza relativa cablata in ensure_disaster_sl +AMT_TOL = 0.10 # tolleranza sull'amount, idem +H = "=" * 96 + + +def sec(t: str): + print("\n" + H + f"\n{t}\n" + H) + + +# ═══════════════════════════════════════════════════════════════════════════════════════════════ +# 1. L'ATTESA — derivata dal CODICE, scritta prima di guardare i dati +# ═══════════════════════════════════════════════════════════════════════════════════════════════ +class _TraderStub(DeribitTrader): + """Esercita il VERO `ensure_disaster_sl` senza rete e senza ordini. + + Non e' una re-implementazione: il metodo sotto esame e' quello di produzione, importato. Sono + stubbate solo le sue dipendenze di I/O. `_post` SOLLEVA -> se un percorso non stubbato provasse + a parlare col venue, l'esercizio si romperebbe invece di riuscire in silenzio. + """ + + def __init__(self, pos_usd: float, mark: float): + self._pos = pos_usd + self._mark = mark + self.book: list[dict] = [] # "ordini aperti" finti + self.eventi: list[tuple] = [] # (azione, prezzo) + + def _post(self, path, payload): # noqa: D102 + raise AssertionError(f"RETE VIETATA in questo esercizio (path={path})") + + def position_usd(self, instrument): return self._pos + def mark_price(self, instrument): return self._mark + def open_orders(self, instrument): return list(self.book) + + def cancel_order(self, order_id): + self.book = [o for o in self.book if o["order_id"] != order_id] + self.eventi.append(("cancel", order_id)) + return {} + + def place_disaster_sl(self, instrument, side_held, amount, stop_price, label=DISASTER_LABEL): + from src.live.execution import Fill + oid = f"stub-{len(self.eventi)}" + self.book.append({"order_id": oid, "label": DISASTER_LABEL, "amount": amount, + "trigger_price": stop_price}) + self.eventi.append(("place", stop_price)) + return Fill(instrument, "sell", amount, 0.0, None, 0.0, oid, "untriggered", True) + + +def attesa(): + sec("1. ATTESA — cosa dice il CODICE, prima di guardare i dati") + print("""`ensure_disaster_sl` (src/live/execution.py:196-222), a ogni giro orario: + mark = self.mark_price(instrument) <- MARK CORRENTE, non il prezzo d'ingresso + want_stop = disaster_stop_price(inst, mark, sl_pct) <- 0,70 * mark_ora (long) + tiene il bracket esistente SOLO se |stp - want_stop| / want_stop < 0,05 E |amt - want| < 10%*want + altrimenti -> cancel_order(...) + place_disaster_sl(..., want_stop) + + Il trigger NON e' funzione del prezzo d'ingresso della posizione: e' funzione del mark dell'ora. + Sostituendo stp = 0,70*M_vecchio e want = 0,70*M_ora, il fattore 0,70 si semplifica: + + |0,70*M_vec - 0,70*M_ora| / (0,70*M_ora) = |M_vec/M_ora - 1| >= 0,05 + + che e' una condizione sul MARK, non sullo stop. Soglie (esatte, non arrotondate): + salita M_ora >= M_vec/0,95 -> +5,263% -> lo stop sale (trailing "buono") + discesa M_ora <= M_vec/1,05 -> -4,762% -> lo stop SCENDE (il -30% insegue il prezzo giu') + + ATTESA DICHIARATA: **ROTOLANTE e SIMMETRICO** — nel codice c'e' un solo `abs()`, non due rami. + ATTESA 2: esiste un SECONDO innesco, che la SPEC non cita — il controllo sull'AMOUNT. Ogni + ribilancio cambia la size -> `|amt - want_amount| >= 10%` -> bracket ricostruito a 0,70*mark_ora + ANCHE se il prezzo non si e' mosso del 5%. Su un libro vol-targeted questo innesco e' il piu' + frequente dei due. + ATTESA 3 (conseguenza, e va nel verso OPPOSTO al timore della SPEC): uno stop che scende insieme + al prezzo si allontana dal mercato in un ribasso -> viene toccato MENO, non piu'. Cio' che si + perde non e' "piu' episodi", e' il PAVIMENTO: la perdita dall'INGRESSO non e' piu' limitata al 30%.""") + + # --- esercizio del codice di produzione su un percorso sintetico (nessuna rete, nessun ordine) + print("\n Esercizio del VERO `ensure_disaster_sl` (stub di I/O, `_post` solleva):") + inst = "BTC_USDC-PERPETUAL" + righe = [] + for nome, mult in (("mark FERMO", 1.000), ("mark +4,0% (sotto soglia)", 1.040), + ("mark +5,5% (sopra soglia)", 1.055), ("mark -4,0% (sotto soglia)", 0.960), + ("mark -5,5% (sopra soglia)", 0.945)): + t = _TraderStub(pos_usd=200.0, mark=70_000.0) + r0 = t.ensure_disaster_sl(inst, SL_PCT) # primo piazzamento + t._mark = 70_000.0 * mult # il mark si muove, la posizione NO + r1 = t.ensure_disaster_sl(inst, SL_PCT) + righe.append((nome, r0["stop"], r1["state"], r1.get("stop"))) + print(f" {'scenario':<28} {'stop iniziale':>14} {'esito':>10} {'stop dopo':>14} posizione") + for n, s0, st, s1 in righe: + print(f" {n:<28} {s0:>14,.1f} {st:>10} {s1:>14,.1f} INVARIATA") + su = [r for r in righe if "+5,5" in r[0]][0] + giu = [r for r in righe if "-5,5" in r[0]][0] + assert su[2] == "placed" and giu[2] == "placed", "atteso ri-piazzamento in ENTRAMBI i versi" + assert righe[0][2] == "ok" and righe[1][2] == "ok" and righe[3][2] == "ok" + print(f"\n -> il codice di produzione ri-piazza in ENTRAMBI i versi: lo stop SCENDE da " + f"{giu[1]:,.1f} a {giu[3]:,.1f} con la posizione ferma.") + print(" (asserzione superata: nessuna chiamata di rete e' partita, `_post` avrebbe sollevato)") + + # controllo positivo: la stessa asserzione applicata a un pavimento DEVE fallire + class _Pavimento(_TraderStub): + def ensure_disaster_sl(self, instrument, sl_pct): + if not self.book: + return _TraderStub.ensure_disaster_sl(self, instrument, sl_pct) + return {"state": "ok", "stop": self.book[0]["trigger_price"]} + p = _Pavimento(pos_usd=200.0, mark=70_000.0) + p.ensure_disaster_sl(inst, SL_PCT) + p._mark = 70_000.0 * 0.945 + assert p.ensure_disaster_sl(inst, SL_PCT)["state"] == "ok", "controllo positivo rotto" + print(" CONTROLLO POSITIVO: su un'implementazione a PAVIMENTO lo stesso esercizio da' `ok`") + print(" -> l'esercizio ha potenza: distingue le due implementazioni, non passa e basta.") + return dict(soglia_su=1 / 0.95 - 1, soglia_giu=1 / 1.05 - 1) + + +# ═══════════════════════════════════════════════════════════════════════════════════════════════ +# 2. LA MISURA — dal log di produzione (che porta letture del venue) +# ═══════════════════════════════════════════════════════════════════════════════════════════════ +_RUN = re.compile(r"^===== (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})Z cron_book =====") +_ASSET = re.compile(r"^ (BTC|ETH) TP ([+-][\d.]+) . SKH ([+-]\d)\(([^)]*)\) -> net \$([+-][\d,]+)" + r" \| pos \$([+-][\d,]+) -> (.*)$") +_DSL = re.compile(r"^ disaster-SL: (\w[\w-]*)(?: @ \$([\d,.]+))?") +_REC = re.compile(r"^ reconcile: pos \$([-\d,]+)") +_FILL = re.compile(r"^ -> (BUY|SELL) ([\d.]+) @ \$([\d,.]+)") + + +def _f(s: str) -> float: + return float(s.replace(",", "")) + + +def parse_log() -> list[dict]: + runs: list[dict] = [] + cur = asset = None + for ln in LOG.read_text().splitlines(): + m = _RUN.match(ln) + if m: + cur = {"ts": pd.Timestamp(m.group(1), tz="UTC"), "assets": []} + runs.append(cur); asset = None; continue + if cur is None: + continue + m = _ASSET.match(ln) + if m: + asset = dict(asset=m.group(1), tp=float(m.group(2)), skh=int(m.group(3)), + net=_f(m.group(5)), pos=_f(m.group(6)), action=m.group(7), + dsl_state=None, dsl_stop=None, n_fill=0, pos_after=None) + cur["assets"].append(asset); continue + if asset is None: + continue + if _FILL.match(ln): + asset["n_fill"] += 1; continue + m = _REC.match(ln) + if m: + asset["pos_after"] = _f(m.group(1)); continue + m = _DSL.match(ln) + if m: + asset["dsl_state"] = m.group(1) + asset["dsl_stop"] = _f(m.group(2)) if m.group(2) else None + return runs + + +def misura_log(soglie: dict) -> dict: + sec("2. LA MISURA — log di produzione `logs/cron_book.log` (1 giro/ora dal 2026-06-23)") + runs = parse_log() + righe = [(r["ts"], a) for r in runs for a in r["assets"]] + st = Counter(a["dsl_state"] for _, a in righe) + span = (runs[-1]["ts"] - runs[0]["ts"]).total_seconds() / 3600 + print(f" giri: {len(runs)} ({runs[0]['ts']:%Y-%m-%d %H:%M} -> {runs[-1]['ts']:%Y-%m-%d %H:%M} UTC, " + f"{span/24:.1f} giorni)") + print(f" righe-asset: {len(righe)} | stati disaster-SL: {dict(st)}") + print(f" (i {st[None]} `None` sono giri usciti PRIMA del passo disaster-SL — conto offline /" + " feed stantio: non sono uno stato del bracket, sono l'assenza della misura)") + + gap = pd.Series([r["ts"] for r in runs]).diff().dt.total_seconds() / 3600 + print(f" cadenza del cron: mediana {gap.median():.2f}h · max {gap.max():.2f}h · " + f"buchi >2h: {int((gap > 2).sum())}" + " <- durante un buco lo stop e' CONGELATO (nessun giro = nessun ri-ancoraggio)") + + # --- L'EVENTO CERCATO: bracket ricostruito a POSIZIONE FERMA ------------------------------- + print("\n a) `placed` = bracket cancellato e ricostruito. La SPEC li dichiara «non separabili»") + print(" dalle prime aperture: FALSO — la riga porta gia' l'azione dello stesso giro.") + placed = [(t, a) for t, a in righe if a["dsl_state"] == "placed"] + hold = [(t, a) for t, a in placed if a["action"].startswith("HOLD") and a["n_fill"] == 0] + apert = [(t, a) for t, a in placed if abs(a["pos"]) < 1.0] + ribil = [(t, a) for t, a in placed if abs(a["pos"]) >= 1.0 and a["n_fill"] > 0] + print(f" {len(placed)} `placed` totali = {len(apert)} prime aperture (pos=0) + " + f"{len(ribil)} ribilanci (innesco AMOUNT) + {len(hold)} A POSIZIONE FERMA (innesco PREZZO)") + print(f"\n {'timestamp':<21} {'as':<4} {'azione':<16} {'pos $':>7} {'ordini':>7} {'nuovo stop':>12}") + for t, a in hold: + print(f" {t:%Y-%m-%d %H:%M:%S} {a['asset']:<4} {a['action'][:16]:<16} " + f"{a['pos']:>7,.0f} {a['n_fill']:>7} {a['dsl_stop']:>12,.1f}") + print(f"\n -> {len(hold)} osservazioni in cui NESSUN ordine e' partito, la posizione non e'") + print(" cambiata, e il trigger sul book e' stato comunque spostato. **UNA sarebbe bastata.**") + + # --- serie del trigger, e taglia dei salti vs la soglia predetta --------------------------- + print("\n b) serie del trigger on-book (nello stato `ok` il numero e' LETTO dal venue,") + print(" `get_open_orders -> trigger_price`: non e' un valore che il libro ricalcola)") + salti = [] + for A in ("BTC", "ETH"): + prev = None + print(f"\n --- {A} ---") + for t, a in righe: + if a["asset"] != A: + continue + if a["dsl_state"] in (None, "flat"): + if prev is not None: + print(f" {t:%Y-%m-%d %H:%M} FLAT (posizione chiusa -> bracket cancellato)") + prev = None; continue + s = a["dsl_stop"] + if prev is None: + print(f" {t:%Y-%m-%d %H:%M} primo piazzamento stop ${s:>10,.1f}") + elif abs(s - prev) > 1e-9: + d = s / prev - 1 + innesco = "PREZZO (pos. ferma)" if a["action"].startswith("HOLD") else "AMOUNT (ribilancio)" + salti.append((A, t, d, innesco)) + print(f" {t:%Y-%m-%d %H:%M} stop ${prev:>10,.1f} -> ${s:>10,.1f} ({d:+6.2%}) " + f"innesco: {innesco}") + prev = s + sp = [d for _, _, d, i in salti if i.startswith("PREZZO")] + print(f"\n salti a posizione ferma: {['%+.2f%%' % (x*100) for x in sp]}") + print(f" soglia predetta al §1 per il ri-piazzamento in salita: {soglie['soglia_su']:+.3%}") + print(f" -> tutti e {len(sp)} stanno appena SOPRA la soglia: la misura non dice solo «si muove»,") + print(" dice che si muove **quando e quanto il codice prevede**.") + + print("\n c) ONESTA' SUL CAMPIONE — quello che NON e' stato osservato:") + print(f" ri-ancoraggi verso il BASSO a posizione ferma: 0 su {len(sp)}.") + print(" Il campione e' di 2 mesi e UN SOLO REGIME: nella finestra BTC va da ~$66,5k a ~$78,6k") + print(" e non c'e' mai stato un calo del mark >4,76% mentre una posizione era aperta.") + print(" La simmetria del ri-ancoraggio NON e' quindi un fatto osservato: e' un fatto di") + print(" CODICE, esercitato al §1 (un solo `abs()`, ri-piazzamento provato in entrambi i versi).") + return dict(n_runs=len(runs), n_hold=len(hold), n_placed=len(placed), gap_max=float(gap.max())) + + +# ═══════════════════════════════════════════════════════════════════════════════════════════════ +# 3. RISCONTRO SUL VENUE — solo lettura, solo il percorso che il libro usa gia' +# ═══════════════════════════════════════════════════════════════════════════════════════════════ +def riscontro_venue(runs_info: dict): + sec("3. RISCONTRO SUL VENUE (percorso del conto, SOLA LETTURA)") + print(""" La storia degli ordini (`get_order_history_by_currency` / `get_order_state`) NON e' + raggiungibile: il gateway che il libro usa (cerbero-mcp) espone 32 tool Deribit e nessuno di essi + e' la storia degli ordini (verificato in `cerbero_mcp/exchanges/deribit/tools.py`: ticker, + orderbook, positions, account_summary, open_orders, trade_history, place/cancel...). Le chiavi API + diritte di Deribit appartengono a un ALTRO progetto: usarle sarebbe inventare un percorso, e la + domanda non ne ha bisogno. Si usa quindi cio' che il percorso sanzionato offre: `get_open_orders`.""") + m = datetime.now(timezone.utc).minute + if 5 <= m <= 10 or 24 <= m <= 30: + print(f"\n ⏸ minuto :{m:02d} — finestra di cron_book (:07) / cron_chain (:25). Non interrogo.") + return + from src.live.deribit import DeribitRead + r = DeribitRead() + print(f"\n {'strumento':<22} {'trigger on-book':>16} {'amount':>10} creato (UTC)") + for i, inst in enumerate(("BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL")): + if i: + time.sleep(0.6) # <= 2 req/s + try: + br = [o for o in r.open_orders(inst) if (o.get("label") or "") == DISASTER_LABEL] + except Exception as e: # noqa: BLE001 + print(f" {inst:<22} lettura fallita: {type(e).__name__}: {e}") + continue + for o in br: + trg = float(o.get("trigger_price") or o.get("stop_price") or o.get("price") or 0) + ct = o.get("creation_timestamp") + when = pd.Timestamp(int(ct), unit="ms", tz="UTC").strftime("%Y-%m-%d %H:%M:%S") if ct else "?" + print(f" {inst:<22} {trg:>16,.2f} {float(o.get('amount') or 0):>10.4f} {when}") + if not br: + print(f" {inst:<22} nessun bracket (posizione flat)") + print(""" + Da leggere insieme al §2: il `creation_timestamp` del bracket vivo e' l'ora dell'ULTIMO + `placed` del log, non quella di apertura della posizione. L'oggetto-ordine sul venue e' piu' + GIOVANE della posizione che protegge: e' stato distrutto e ricreato mentre la posizione viveva.""") + + +# ═══════════════════════════════════════════════════════════════════════════════════════════════ +# 4. LA CONSEGUENZA — quanto costa davvero, sulla storia vera di BTC/ETH +# ═══════════════════════════════════════════════════════════════════════════════════════════════ +def simula(df: pd.DataFrame, modo: str, ore_cron: int, sl: float = SL_PCT, tol: float = TOL) -> dict: + """Replica la regola di produzione su barre orarie reali. + + `modo="roll"` = `ensure_disaster_sl` com'e' (stop = 0,70*mark, ri-ancorato oltre `tol`); + `modo="floor"` = la controfattuale che la SPEC assumeva (stop fissato all'ingresso, mai mosso). + Il runner e' VIVO e la posizione e' SEMPRE LONG (caso peggiore per l'accumulo di episodi: + dopo ogni scatto si rientra al giro successivo). `ore_cron` = cadenza dei giri. + """ + c = df["close"].to_numpy(float); lo = df["low"].to_numpy(float); op = df["open"].to_numpy(float) + _dt = pd.to_datetime(df["datetime"]) + if getattr(_dt.dt, "tz", None) is not None: # confronti sempre naive-UTC, mai misti + _dt = _dt.dt.tz_convert("UTC").dt.tz_localize(None) + ts = _dt.to_numpy() + n = len(c) + stop = anchor = entry = None + epis = [] + peggio_da_ingresso = 0.0 + for i in range(1, n): + if (i % ore_cron) == 0: # il giro del cron: e' l'UNICO momento in cui + mark = c[i - 1] # si apre, si rientra e si ri-ancora + if stop is None: # (ri)apertura + primo bracket + entry = anchor = mark + stop = mark * (1 - sl) + elif modo == "roll": + want = mark * (1 - sl) + if abs(stop - want) / want >= tol: + stop, anchor = want, mark + if stop is None: + continue + peggio_da_ingresso = min(peggio_da_ingresso, lo[i] / entry - 1) + if lo[i] <= stop: # scatto (gap-through al peggiore fra open e stop) + fill = min(op[i], stop) + epis.append(dict(t=ts[i], entry=entry, anchor=anchor, stop=stop, fill=fill, + perdita_da_ancora=fill / anchor - 1, perdita_da_ingresso=fill / entry - 1)) + stop = entry = anchor = None + return dict(episodi=epis, n=len(epis), peggior_perdita_da_ingresso=peggio_da_ingresso) + + +def conseguenza(): + sec("4. LA CONSEGUENZA — quanti disaster-SL si accumulano davvero (storia reale BTC/ETH 1h)") + print(" Regola di produzione replicata bar-per-bar. Runner VIVO, posizione SEMPRE LONG e") + print(" rientro al giro successivo dopo ogni scatto = caso peggiore per l'accumulo.") + print(" GRIGLIA: 2 asset x 2 modi (roll / floor) x 3 cadenze (1h / 4h / 24h) = 12 celle.\n") + out = {} + print(f" {'asset':<5} {'cadenza':>8} {'modo':>6} {'episodi':>8} {'peggior scatto':>15} " + f"{'max in 30g':>11} peggior DD dall'ingresso (*)") + for asset in ("BTC", "ETH"): + d = load_data(asset, "1h") + for ore in (1, 4, 24): + for modo in ("roll", "floor"): + r = simula(d, modo, ore) + e = r["episodi"] + pe = min((x["perdita_da_ancora"] for x in e), default=float("nan")) + if e: + tt = pd.Series([pd.Timestamp(x["t"]) for x in e]) + m30 = int(max(((tt >= t) & (tt < t + pd.Timedelta("30D"))).sum() for t in tt)) + else: + m30 = 0 + out[(asset, ore, modo)] = r + print(f" {asset:<5} {str(ore)+'h':>8} {modo:>6} {r['n']:>8} " + f"{('%.1f%%' % (pe*100)) if e else ' —':>15} {m30:>11} " + f"{r['peggior_perdita_da_ingresso']:>+.1%}") + # elenco degli scatti alla cadenza DI PRODUZIONE (1h) e alla peggiore (24h) + for ore in (1, 24): + for modo in ("roll", "floor"): + e = out[(asset, ore, modo)]["episodi"] + if e: + print(f" {asset} {str(ore)+'h':>3} {modo:<5}: " + + ", ".join(f"{pd.Timestamp(x['t']):%Y-%m-%d %H:%M} " + f"(da ingresso {x['perdita_da_ingresso']:+.1%})" for x in e[:8])) + print(""" + COME SI LEGGE: + · MECCANISMO: uno stop che SEGUE il prezzo in discesa si ALLONTANA dal mercato -> per scattare + serve una caduta di ~26,5-30% DENTRO un solo giro di cron (l'ancora sta entro +5,26%/-4,76% + dal mark). Piu' fitto e' il cron, piu' lo stop e' irraggiungibile. + · A CADENZA DI PRODUZIONE (1h), su ~7-8 anni: BTC 0 scatti, ETH 1. Mai piu' di 1 in 30 giorni. + -> **l'accumulo che la SPEC temeva NON si verifica alla configurazione che gira.** + · ⚠️ MA LA RELAZIONE NON E' MONOTONA E IL TIMORE NON E' INFONDATO: a 4h BTC fa 2 scatti contro + l'1 del pavimento, a 24h ETH ne fa 5 con 2 dentro 30 giorni. Con un cron lento l'ancora resta + alta dopo una salita, lo stop siede vicino sotto il mercato, il crollo lo prende, si rientra e + la gamba successiva lo riprende. **Il rischio di accumulo e' funzione della CADENZA DEL CRON, + non di k**: il gradino di leva non lo crea e non lo amplifica. + · ⚠️ E qui c'e' un aggancio operativo: il docstring di `scripts/live/book_execute.py` prescrive + «ogni ~230 minuti» (la griglia di SKH01) mentre il cron gira **ogni ora** (misurato sopra: 1443 + giri in 1442 ore). Chi "correggesse" la cadenza verso il docstring sposterebbe il libro dalla + riga 1h alla riga 4h — quella in cui BTC raddoppia gli scatti. + + (*) ⚠️ CAVEAT sulla colonna, e sul suo artefatto: in questa lente la posizione e' SEMPRE aperta + dal 2018, quindi in modo `roll` l'"ingresso" e' un prezzo di anni prima e il -48%/-61% e' il + drawdown di un buy&hold pluriennale, NON di un trade del libro (che dura ore-giorni). E' + esattamente la trappola n.10 del brief: il confronto va appaiato per INGRESSO. Il §4b rifa' + la stessa domanda su durate realistiche.""") + + # cadute massime realizzabili dentro un giro: e' la distanza che separa il rotolante dallo scatto + print("\n Quanto deve cadere il prezzo DENTRO un giro perche' il rotolante scatti:") + print(" (lo stop e' 0,70*ancora e l'ancora sta entro +5,26%/-4,76% dal mark -> serve ~26,5-30%)") + print(f"\n {'asset':<5} {'finestra':>9} {'caduta max storica (low vs close t-w)':>42}") + for asset in ("BTC", "ETH"): + d = load_data(asset, "1h") + for w in (1, 4, 24): + m = float((d["low"].rolling(w).min() / d["close"].shift(w) - 1).min()) + print(f" {asset:<5} {str(w)+'h':>9} {m:>41.2%}") + return out + + +def cicli_bounded(df: pd.DataFrame, modo: str, hold_h: int, sl: float = SL_PCT, tol: float = TOL) -> dict: + """Stessa regola, ma su TRADE di durata realistica: cicli NON sovrapposti di `hold_h` ore, ogni + ciclo entra al suo inizio col proprio prezzo. Risponde alla domanda «quanto puo' perdere un + trade dall'INGRESSO prima che lo stop lo fermi», senza l'artefatto della posizione perpetua.""" + c = df["close"].to_numpy(float); lo = df["low"].to_numpy(float); op = df["open"].to_numpy(float) + n = len(c) + peggiori, scattati, sfondati = [], 0, 0 + for a in range(1, n - hold_h, hold_h): + entry = c[a - 1] + stop = entry * (1 - sl) + worst = 0.0 + fired = False + for i in range(a, a + hold_h): + mark = c[i - 1] + if modo == "roll": + want = mark * (1 - sl) + if abs(stop - want) / want >= tol: + stop = want + worst = min(worst, lo[i] / entry - 1) + if lo[i] <= stop: + fired = True + worst = min(worst, min(op[i], stop) / entry - 1) + break + peggiori.append(worst) + scattati += fired + sfondati += (worst < -sl) and not fired # perdita >30% SENZA che lo stop fermasse + p = np.array(peggiori) + return dict(n=len(p), peggiore=float(p.min()), scattati=scattati, sfondati=sfondati, + q_sfondati=sfondati / len(p)) + + +def bounded(): + sec("4b. LA PERDITA DALL'INGRESSO, su trade di durata realistica (cicli non sovrapposti)") + print(" Il libro tiene una posizione ore-giorni, non anni. Qui ogni ciclo ha il PROPRIO ingresso.") + print(" GRIGLIA: 2 asset x 2 modi x 3 durate (24h / 7g / 30g) = 12 celle (totale script: 24).\n") + print(f" {'asset':<5} {'durata':>7} {'modo':>6} {'cicli':>7} {'scatti':>7} " + f"{'peggior DD da ingresso':>23} {'cicli oltre -30% SENZA scatto':>31}") + for asset in ("BTC", "ETH"): + d = load_data(asset, "1h") + for h, lab in ((24, "24h"), (168, "7g"), (720, "30g")): + for modo in ("roll", "floor"): + r = cicli_bounded(d, modo, h) + print(f" {asset:<5} {lab:>7} {modo:>6} {r['n']:>7} {r['scattati']:>7} " + f"{r['peggiore']:>23.1%} {r['sfondati']:>8} ({r['q_sfondati']:>6.2%})") + print(""" + Qui il rotolante mostra il suo vero difetto, senza artefatti: su trade veri lascia passare + perdite dall'ingresso ben oltre il 30% SENZA scattare, mentre il pavimento no. Non e' un costo + che il gradino di leva crea ne' amplifica: e' una proprieta' di oggi, ed e' il motivo per cui + il disaster-SL NON e' un "massimo -30% per trade". Cio' che limita la perdita di un trade nel + libro sono le uscite software di TP01/SKH01, non questo bracket.""") + + +def costo(out: dict): + sec("5. IL NUMERO CHE SBLOCCA (o chiude) IL GRADINO DI LEVA") + for k in (1.00, 1.25, 1.50): + b = 2 * 0.50 * k * SL_PCT + print(f" k={k:.2f} -> costo di UN episodio = 2*frac*k*sl_pct = {b:.1%} dell'equity" + f" (margine su 50%: {0.50/b:.2f}x)") + print(""" + La SPEC temeva che il 37,5% a k=1,25 fosse il costo di UN episodio fra molti, e che quindi il + margine 1,33x non fosse quello vero. Le misure: + + 1) B copre GIA' il caso peggiore per asset. Il fattore `len(ASSETS)*frac` e' il lordo su ENTRAMBE + le gambe: «un episodio» in G2 significa gia' BTC **e** ETH stoppati insieme. Alla cadenza di + produzione questo non e' mai successo in ~7-8 anni (BTC 0 scatti, ETH 1, in anni diversi). + 2) Con RUNNER VIVO a 1h gli scatti sono 0-1 in tutta la storia e mai piu' di 1 in 30 giorni -> + nessun accumulo da sommare al 37,5%. + 3) Nella finestra in cui il disaster-SL serve davvero — l'OUTAGE — il ri-ancoraggio e' SPENTO PER + COSTRUZIONE: nessun giro di cron, nessuna cancellazione, nessun ri-piazzamento, e nemmeno un + rientro. Lo stop resta dov'era: **un episodio solo, per costruzione.** Il log misura la taglia + di quella finestra: nessun buco del cron oltre 2 ore in 60 giorni. + + => Il 37,5% resta il costo di UN episodio: il margine 1,33x di G6 e' quello vero, e la gamba + WORST-DAY del gradino regge. **Il nodo e' sciolto, e non blocca.** + + ⚠️ Cio' che il rotolante rompe e' un'ALTRA affermazione — che il progetto non scrive da nessuna + parte ma che il nome «disaster-SL -30%» suggerisce: che la perdita su un trade sia limitata al + 30%. NON lo e' (§4b: fino a -60,6% BTC / -61,5% ETH dall'ingresso senza che lo stop scatti, nel + 7-13% dei cicli a 30 giorni). E' una proprieta' di OGGI, indipendente da k, e la protezione vera + del singolo trade sono le uscite software di TP01/SKH01. + + ⚠️ E c'e' un accoppiamento nuovo da registrare: l'accumulo di episodi e' funzione della CADENZA + DEL CRON (1h: no · 4h: BTC 2 · 24h: ETH 5 con 2 in 30 giorni). Oggi la cadenza e' misurata sana, + ma nessun sorvegliante la controlla contro questa conseguenza, e il docstring di `book_execute` + ne prescrive una (~230 min) che cade nella riga peggiore. Non e' un'azione richiesta qui: e' il + parametro da rileggere prima del gradino, insieme a G2.""") + + +def main(): + print(H) + print(" SL-ANCHOR — il disaster-SL e' un PAVIMENTO o un -30% ROTOLANTE?") + print(" Nodo aperto da docs/research/SPEC-scale-key.md §4.5 · SOLA LETTURA · nessun ordine") + print(H) + soglie = attesa() + info = misura_log(soglie) + if "--venue" in sys.argv[1:]: + riscontro_venue(info) + else: + print("\n (riscontro sul venue non richiesto: aggiungere --venue per 2 letture di open_orders)") + out = conseguenza() + bounded() + costo(out) + print("\n" + H) + print(" VERDETTO — il disaster-SL e' ROTOLANTE: MISURATO, non dedotto.") + print(" · 3 osservazioni di produzione con bracket ricostruito a POSIZIONE FERMA e 0 ordini;") + print(" · i salti (+5,74 / +5,81 / +5,38%) cadono appena sopra la soglia +5,263% predetta;") + print(" · il venue conferma: il bracket vivo e' piu' GIOVANE della posizione che protegge.") + print(" La conseguenza temuta — episodi che si accumulano — e' REFUTATA alla cadenza che gira:") + print(" il 37,5% resta il costo di un episodio singolo e il margine 1,33x di G6 e' quello vero.") + print(H) + + +if __name__ == "__main__": + main() From 3a4592bdfc88108c1f3ec813a9a54f952bba6d4f Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 00:38:16 +0000 Subject: [PATCH 057/114] =?UTF-8?q?CLAUDE.md:=20nodo=20del=20disaster-SL?= =?UTF-8?q?=20sciolto=20=E2=80=94=20rotolante=20ma=20scatta=20MENO;=20e=20?= =?UTF-8?q?il=20docstring=20della=20cadenza=20in=20book=5Fexecute=20e'=20u?= =?UTF-8?q?na=20trappola=20viva?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 66aee03..31955ef 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2482,12 +2482,34 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis ✅ `weights_tilt_null` invece **passa con potenza**: peso implicito TP01 **0,750000 a ogni k**, `max|target(k) − k·target(1)| = 0`, e il **controllo positivo** (scala sulla sola gamba TP01 = cio' che farebbe `target_vol`) lo porta a **0,789** e viene smascherato. - 🚨 **IL NODO, da sciogliere PRIMA di qualunque gradino:** `execution.py:196-223` **ri-ancora il - disaster-SL al mark a ogni giro** (tolleranza 5%) → **il −30% e' ROTOLANTE, non un pavimento**. Se e' - cosi', il 37,5% e' il costo di **un episodio** e un ribasso lento ne produce **piu' d'uno**, e il - margine 1,33x del gradino non e' quello vero. ⚠️ **Letto nel codice, NON misurato**, e il log non lo - permette (servirebbe `pos_prima`): **non ricostruibile a posteriori** → per misurarlo bisogna prima - aggiungere il campo al log. + ✅ **IL NODO E' SCIOLTO (2026-08-23, `r0823_sl_anchor.py`, §40) — E LA DICHIARAZIONE DI + NON-MISURABILITA' ERA FALSA: IL LOG BASTAVA.** *(3ª conferma della regola del 26/07: un blocco + dichiarato e' un'IPOTESI, non un fatto.)* + 📌 **L'attesa e' stata scritta PRIMA di guardare, derivata dal codice, e ha predetto le soglie + esatte:** rotolante e **simmetrico**, innesco **+5,263%** / **−4,762%** (il fattore 0,70 si semplifica + e la condizione diventa una condizione **sul mark**, non sullo stop). + **Il fatto e' confermato:** su 1443 giri i 15 `placed` sono **5 aperture + 7 ribilanci + 3 A POSIZIONE + FERMA** (21/08, `HOLD (a target)`, **0 ordini**, trigger spostato di **+5,74/+5,81/+5,38%** = appena + sopra la soglia predetta); e sul venue **l'ordine di stop e' 2,5 giorni piu' giovane della posizione + che protegge**. + 🚨 **MA IL TIMORE E' REFUTATO, e va nel verso OPPOSTO:** uno stop che segue il prezzo **si allontana + dal mercato, quindi scatta MENO**. Alla cadenza che gira (**1h**) gli episodi su storia reale sono + **0-1 contro 1 del pavimento**, mai piu' di 1 in 30 giorni; e **nell'outage — l'unico scenario per cui + il bracket esiste — il ri-ancoraggio e' SPENTO per costruzione** (nessun giro, nessun ri-piazzamento) + ⇒ **un episodio solo**. **Il 37,5% resta il costo di un episodio singolo, il margine 1,33× e' quello + vero, la gamba di WORST-DAY REGGE.** + 🚨 **Due cose indipendenti da k, da sapere comunque:** (i) **il nome inganna** — *"disaster-SL −30%"* + suggerisce un massimo di perdita per trade che **non esiste**: su cicli a ingresso proprio il rotolante + lascia passare perdite oltre −30% **senza scattare** nel **0,03%/0,96-2,84%/7,22-13,33%** dei cicli + (24h/7g/30g) e il peggior DD dall'ingresso e' **−60,6% BTC / −61,5% ETH**; (ii) 🚨 **TRAPPOLA VIVA: + l'accumulo e' funzione della CADENZA DEL CRON, e il docstring di `book_execute.py` prescrive "ogni + ~230 minuti" mentre il cron gira OGNI ORA** — chi "correggesse" la cadenza verso il docstring + sposterebbe il libro sulla riga **4h, dove BTC RADDOPPIA gli scatti** (2 contro 1; a 24h ETH arriva a + 5). **Il docstring e' sbagliato e la configurazione che gira e' quella giusta.** + ⚠️ **Limite dichiarato: 0 ri-ancoraggi verso il BASSO osservati su 3** — 2 mesi di vita del libro in + **un solo regime** (BTC ~$66,5k → ~$78,6k), mai un calo del mark >4,76% con posizione aperta. **La + simmetria e' un fatto di CODICE** (un solo `abs()`, esercitato in entrambi i versi), **non un fatto + OSSERVATO**: la prima osservazione utile arriva al primo ribasso >5% con posizione aperta. **Seconda soglia dichiarata:** il fallback dell'equity oggi scatta **0 volte su 1.442 giri**; sopra il **2% dei giri in 90 giorni** il libro girerebbe a leva **mista** e `g(k)` smetterebbe di descriverlo. From 09bae3b9ea4670871eec0cbbf6219aafdd8c403e Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 00:46:51 +0000 Subject: [PATCH 058/114] =?UTF-8?q?registro:=20ondata=207=20=E2=80=94=20ci?= =?UTF-8?q?nque=20filoni=20su=20angoli=20mai=20toccati=20(sizing=20per=20c?= =?UTF-8?q?onvinzione,=20short=20del=20trend,=20dati=20inutilizzati,=20tim?= =?UTF-8?q?ing=20dei=20bonifici,=20separazione=20spot/perp)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index c6e19df..554e67e 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -706,6 +706,11 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 38 | SCALE-SPEC | specificare il knob di leva che non esiste | **LEAD** — specificata, **1 nodo irrisolto** sul disaster-SL | | 39 | FUNDING-AVOID | evitare il funding esprimendo il libro sui datati | **SCARTATO** sui datati · **LEAD** sullo **spot** | | 40 | SL-ANCHOR | il nodo che bloccava il gradino di leva | **SCIOLTO** — rotolante, ma il timore e' refutato | +| 41 | TP01-SIZE | una size che risponde alla CONVINZIONE batte il vol-target uniforme, a pari vol realizzata? | _in corso_ | +| 42 | TP01-LS | la gamba short del trend a 1d paga il proprio costo, o e' il 2022? | _in corso_ | +| 43 | DATA-UNUSED | quali dati il progetto possiede e nessuna strategia legge — e uno di essi sostiene un'ipotesi? | _in corso_ | +| 44 | DEPOSIT-TIMING | un calendario di versamento CONDIZIONALE batte il piatto, a pari soldi? | _in corso_ | +| 45 | SPOT-NETTING | separare TP01 e SKH01 su due strumenti: quanto vale, e il margine lo uccide? | _in corso_ | | 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 | From 0ff03babae4a45ea01a3985efa92438568925f04 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 00:50:21 +0000 Subject: [PATCH 059/114] =?UTF-8?q?audit:=20il=20trasporto=20degli=20allar?= =?UTF-8?q?mi=20e'=20un=20punto=20singolo=20di=20guasto=20=E2=80=94=20un?= =?UTF-8?q?=20tentativo,=20nessun=20registro,=20stato=20marcato=20prima=20?= =?UTF-8?q?dell'invio=20(6,9%=20misurato)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 65 +++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 554e67e..6af6c9b 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -711,6 +711,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 43 | DATA-UNUSED | quali dati il progetto possiede e nessuna strategia legge — e uno di essi sostiene un'ipotesi? | _in corso_ | | 44 | DEPOSIT-TIMING | un calendario di versamento CONDIZIONALE batte il piatto, a pari soldi? | _in corso_ | | 45 | SPOT-NETTING | separare TP01 e SKH01 su due strumenti: quanto vale, e il margine lo uccide? | _in corso_ | +| — | **NOTIFIER** (audit di fatto) | 🚨 **PUNTO SINGOLO DI GUASTO nella rete di sicurezza** | il `send()` di Telegram e' **un solo tentativo da 10s, senza retry, con l'esito MAI registrato** — e lo stato `alerted=True` viene scritto **prima** dell'invio: un 🚨 di `venue_watch` perso e' perso **per l'intero episodio**. Tasso di fallimento **misurato 6,9%** (2/29, IC95 [1,9%, 22,0%]) | | 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 | @@ -2055,3 +2056,67 @@ percorso, *e la domanda non ne aveva bisogno.* **VERDETTO: `IL DISASTER-SL E' ROTOLANTE (costo vero 37,5% contro 37,5% — un episodio solo alla cadenza di produzione; sale a ~75% solo con cron a 24h, che non e' la configurazione che gira)`.** + +--- + +## — NOTIFIER (audit di fatto, coordinatore) — l'allarme puo' non arrivare, e nessuno lo sa + +**Non e' un filone di ricerca: e' un difetto di produzione trovato leggendo i log mentre l'ondata 7 +girava.** Nessun file di produzione toccato. + +**Come e' venuto fuori.** Il giro `cron_daily` di stanotte finisce con +`NON inviato (config Telegram assente o rete KO)`. La nota conflaziona due cause; **la prima e' +falsa** — `TELEGRAM_BOT_TOKEN` e `TELEGRAM_CHAT_ID` sono **presenti** in `.env`, e +`src/live/notifier._cfg()` lo legge (verificato). Quindi la causa e' la rete, e il messaggio manda +a controllare la configurazione. E' la **stessa forma** del difetto codificato il 29/07 sul +`venue_watch` (*"una nota di diagnosi cablata e' peggio di nessuna nota"*), su un percorso diverso. + +**Il difetto vero, che e' piu' grande della nota.** `src/live/notifier.send()`: + +``` +urllib.request.urlopen(url, data, timeout=10) # UN tentativo +except Exception: return False # esito ingoiato +``` + +- **nessun retry** — un singolo blip di rete perde il messaggio; +- **l'esito non viene registrato da nessuna parte**: `notify()` ritorna `bool` e + `scripts/live/venue_watch.py:80` **non lo guarda**; `logs/cron_book.log` contiene **0** + occorrenze di un qualsiasi esito d'invio (verificato con grep). *Quando serve sapere se + l'allarme e' arrivato, l'informazione non e' stata scritta.* +- 🚨 **e lo stato viene marcato PRIMA dell'invio.** In `src/live/venue_watch.lock_step`/streak + la riga `new.alerted = True` sta **dentro la logica pura**, e viene persistita comunque; alle ore + successive `already = st.alerted and st.sign == sign` fa tornare `"WATCH"` invece di `"ALERT"` → + **`notify` non viene piu' chiamata per quell'episodio**. Un 🚨 perso **e' perso per l'episodio + intero**, e gli episodi storici (Mt.Gox, FTX, Bitfinex 2018-19) durano **da 200 a 2.324 ore con + segno costante**: e' esattamente il caso in cui la ri-notifica non arriva mai. + +**Quanto vale, misurato.** Sullo stesso `send()`, il digest giornaliero ha tentato **29** invii e +ne ha **falliti 2** → **6,9%**, IC95 Wilson **[1,9%, 22,0%]**. +⚠️ **Fonte del numero, dichiarata:** e' il percorso del **digest** (messaggio lungo, una volta al +giorno), usato come **proxy** del percorso d'allarme (messaggio corto, orario) — che **non ha +nessun dato proprio, ed e' questo il punto**. Stessi endpoint, stesso timeout, stessa funzione. + +**Perche' conta piu' del suo numero.** L'economia del `venue_watch` (26/07) e' calcolata assumendo +che il vero positivo **arrivi**: costo atteso di un falso allarme 0,248% di equity contro il +**100%** che un vero positivo evita, break-even `p > 0,031%`. Una perdita del ~7% degli allarmi +non ribalta quel conto — ma **sorveglia l'unico rischio che il progetto ha prezzato come capace di +portare il conto a zero** (P(perso tutto) 10/18/34/64% a p=0,5/1/2/5%), e la sorveglianza e' +l'unica mitigazione rimasta dopo la decisione del 26/07 di stare **100% su Deribit fino a $20k**. +*Su una perdita totale, un allarme su quattordici che non arriva non e' un arrotondamento.* + +**Cosa NON e' stato fatto, e perche'.** Nessuna riparazione: `src/live/notifier.py` e +`scripts/live/venue_watch.py` sono produzione, il cron esegue dalla working tree, e un ramo di +ricerca non manda codice live. La riparazione e' piccola e ha tre pezzi indipendenti — (a) retry +con backoff su `send()`; (b) **registrare l'esito** nel punto in cui l'eccezione viene ingoiata +(la regola del 29/07, mai applicata al notifier); (c) marcare `alerted=True` **solo a invio +riuscito**, cosi' che un allarme non consegnato **si ripresenti** l'ora dopo. Il pezzo (c) e' +quello che cambia il comportamento e va deciso dall'operatore: rende l'allarme ripetitivo finche' +non passa, che e' il verso giusto per un 🚨 e il verso sbagliato per un ⚠️. + +⚠️ **Cio' che questo audit NON dice:** che il `venue_watch` non funzioni. Il rilevatore e' tarato, +validato su un controllo positivo (22 episodi Bitfinex) e ha 1 solo falso allarme in 8 anni. Il +difetto e' **a valle del rilevatore**, nel trasporto — la parte che nessuno aveva guardato perche' +non produce numeri. + +**VERDETTO: `LA RETE DI SICUREZZA HA UN TRASPORTO SENZA RETRY, SENZA REGISTRO E CON LO STATO +MARCATO PRIMA DELL'INVIO — ~7% DEGLI ALLARMI PUO' NON ARRIVARE, PER L'EPISODIO INTERO`** From 3e3b41d842e1382bf1117bfa68394c51f96653ae Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 00:54:17 +0000 Subject: [PATCH 060/114] =?UTF-8?q?research(tp01-ls):=20la=20gamba=20SHORT?= =?UTF-8?q?=20del=20trend=20a=201d=20e'=20SCARTATA=20=E2=80=94=20toglie=20?= =?UTF-8?q?drift=20a=200/24=20ancore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §42 TP01-LS. Meccanismo TP01 di produzione CONGELATO, un solo grado di liberta': il pavimento della direzione TSMOM (floor 0 / -1/3 / -2/3 / -1, 4 trial dichiarati prima). Replica bit-exact 3/3 a 0.0 (floor=0 == CANONICAL long_only=True, floor=-1 == long_only= False, sleeve 50/50 == src/portfolio/sleeves._tp01_returns) + 4a replica candidate_daily vs tp01_baseline_daily a 0.0. Controlli positivi 4/4 (causality_ok su variante leaky, implausible_sharpe su serie senza perdite, gate iso-vol su leva pura, anchor_luck_delta A-vs-A). Esito: la short NON paga il proprio costo. dDRIFT appaiato -1.91%/anno positivo in 0/24 ancore, dShFULL -0.354 (0/24), maxDD +8.80pp (24/24); gate iso-volatilita' FAIL (dCAGR -6.19% a pari vol); senza il 2022 il divario RADDOPPIA (dShFULL -0.406, 0/24); anni positivi 2/8 (2022 +1.84, 2025 +0.27) e non compensano gli altri sei; marginal_vs_tp01 = NEUTRAL su tutte e tre le celle (multicut False, corr 0.79-0.93, alpha -2.6%/anno); la cella scelta AL BUIO in-sample e' IL CANONICO mentre quella scelta sull'hold-out e' floor=-1/3 = firma di selezione-sull-hold-out. Non e' morte-per-fee: a fee ZERO 1.338 contro 0.904. Attesa a priori del coordinatore CONFERMATA e superata. Unica metrica nel verso della short: dShHOLD +0.292 in 23/24 ancore — ma l'hold-out e' 1.6 anni, non e' selezionabile in-sample, e all'ancora canonica non si vede (-0.008: caso speculare della lezione 26/07). Libro 75/25 con SKH01: dSh -0.356 in 0/24, e la gamba short isolata ha Sharpe -0.335 con corr +0.10 a SKH01 (non ridondante, solo perdente). Trovato per strada: src/live/book.py clippa gia' TP01 con max(tp_frac, 0.0) -> il long-flat e' cablato anche nell'esecutore, non solo in CANONICAL. Nessun file di produzione toccato. Libro, pesi, cron, config INVARIATI. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf --- scripts/research/r0823_tp01_ls.py | 594 ++++++++++++++++++++++++++++++ 1 file changed, 594 insertions(+) create mode 100644 scripts/research/r0823_tp01_ls.py diff --git a/scripts/research/r0823_tp01_ls.py b/scripts/research/r0823_tp01_ls.py new file mode 100644 index 0000000..d949893 --- /dev/null +++ b/scripts/research/r0823_tp01_ls.py @@ -0,0 +1,594 @@ +#!/usr/bin/env python +"""r0823_tp01_ls.py — §42 TP01-LS: la gamba SHORT del trend a 1d paga il proprio costo? + +DOMANDA. TP01 e' long-flat PER SCELTA: nei ribassi va a 0x invece di girarsi short. Il progetto +lo descrive come ASSICURAZIONE (taglia il DD ~6x contro buy&hold, contributo hold-out negativo +nel 99,1% delle ancore, LOO 26/07). L'obiettivo dichiarato e' il DRIFT, non lo Sharpe. La +direzione mai presa al TF di deploy e' il TSMOM LONG-SHORT a 1d: il progetto ha UNA riga +("trend 1h L/S ~1.0" fra gli edge deboli), misurata a 1h e senza l'harness onesto di oggi. + +Decidibile in quattro pezzi: + (a) la short aggiunge DRIFT? + (b) sopravvive al GATE ISO-VOLATILITA'? (se aggiunge drift solo alzando l'esposizione e' + leva travestita — null del de-levering al contrario, 5 occorrenze nel progetto) + (c) NON e' semplicemente il beta del 2022? (si toglie il 2022 e si rifa') + (d) resta ADDS quando la si giudica contro TP01 stesso (`marginal_vs_tp01`)? + +ATTESA A PRIORI DEL COORDINATORE, dichiarata PRIMA di misurare: il crypto ha drift positivo +strutturale e la short di trend perde quasi sempre fuori dal 2022 -> il guadagno sara' UN ANNO +SOLO (2022), lo stesso schema di SOL (22/08: 24/24 ancore positive, un anno solo). Se e' cosi' +la conclusione non e' "la short funziona" ma "il 2022 e' un evento nel campione". + +MECCANISMO CONGELATO — UN SOLO GRADO DI LIBERTA'. Si parte dal TP01 di PRODUZIONE +(src/strategies/trend_portfolio.TrendPortfolio + CANONICAL) e si cambia SOLO il pavimento della +direzione TSMOM, da 0 (clip long-flat) a `floor`. Tutto il resto e' importato, non riscritto: +orizzonti 30/90/180g, vol_win 30g, target_vol 20%, leva cap 2x, fee 0.05%/lato, 50/50 BTC+ETH, +lo shift causale e la contabilita' fee stanno in `TrendPortfolio.net_returns` EREDITATA. +La direzione del blend a 3 orizzonti vive in {-1,-1/3,+1/3,+1}, quindi il pavimento e' un vero +knob discreto: + floor= 0 -> {0,0,1/3,1} = CANONICO (long-flat) + floor=-1/3 -> {-1/3,-1/3,1/3,1} = short "timida" (solo 1/3 di size al ribasso pieno) + floor=-2/3 -> {-2/3,-1/3,1/3,1} + floor=-1 -> {-1,-1/3,1/3,1} = LONG-SHORT pieno (== long_only=False di produzione) + +GRIGLIA DICHIARATA PRIMA DI GUARDARE: 4 celle (i 4 floor) su UN solo TF (1d, quello di deploy). +Nessun altro parametro riaperto -> 4 trial, contati nel deflated-Sharpe. La griglia di 24 ANCORE +non e' una griglia di parametri (il DSR non la conta): si giudica con `anchor_luck_band` / +`anchor_luck_delta`, come prescritto dal gate del 26/07. + +REPLICA BIT-EXACT PRIMA DI OGNI DELTA (tre vie, richiesta max|diff| == 0): + (1) floor=0 -> target_series == TrendPortfolio(**CANONICAL).target_series + (2) floor=-1 -> target_series == TrendPortfolio(long_only=False).target_series + (cioe' la mia unica riga nuova coincide con la produzione a ENTRAMBI gli estremi: il + codice davvero nuovo sono solo i due floor intermedi) + (3) serie 50/50 a floor=0 vs src.portfolio.sleeves._tp01_returns() (lo sleeve del libro) + +Onesta': altlib (`marginal_vs_tp01`, `deflated_sharpe`, `causality_ok`, `implausible_sharpe`, +`eval_weights_smallcap`, `anchor_luck_*`) importato, non riscritto. Controlli positivi +OBBLIGATORI su ogni gate (un gate che non segnala puo' essere semplicemente rotto). + +Vincoli: NESSUN file toccato fuori da questo script, nessun ordine inviato, sola lettura su +data/raw. Runtime ~1-2 min su 2 core. +""" +from __future__ import annotations + +import sys +import time +from dataclasses import dataclass +from functools import lru_cache +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +sys.path.insert(0, str(ROOT)) + +import altlib as al # noqa: E402 +from src.strategies.trend_portfolio import ( # noqa: E402 + CANONICAL, TrendPortfolio, realized_vol, simple_returns, tsmom_blend, +) + +ASSETS = ("BTC", "ETH") +HOLDOUT = al.HOLDOUT +FLOORS = (0.0, -1.0 / 3.0, -2.0 / 3.0, -1.0) # LA GRIGLIA, dichiarata prima +FLOOR_LBL = {0.0: "0 (canonico LF)", -1.0 / 3.0: "-1/3", -2.0 / 3.0: "-2/3", -1.0: "-1 (LS pieno)"} +OFFSETS = tuple(range(24)) # banda d'ancora giornaliera +W_TP01_BOOK, W_SKH_BOOK = 0.75, 0.25 # src/live/book.py +CAPITAL = 635.0 # conto reale dichiarato nel briefing + + +# =========================================================================== +# IL MECCANISMO — TP01 di produzione con UN grado di liberta' in piu' +# =========================================================================== +@dataclass +class TPFloor(TrendPortfolio): + """TP01 con pavimento della direzione parametrico. UNICA riga diversa dalla produzione: + `np.clip(direction, self.floor, None)` al posto di `np.clip(direction, 0, None)`. + Tutto il resto (vol realizzata, blend TSMOM, scaling, cap di leva, net_returns con lo + shift causale e le fee sul turnover) e' ereditato/importato dal modulo di produzione.""" + floor: float = 0.0 + + def target_series(self, df: pd.DataFrame) -> np.ndarray: + c = df["close"].values.astype(float) + bpd = self._bpd(df) + bpy = bpd * 365.25 + r = simple_returns(c) + vol = realized_vol(r, self.vol_win_days * bpd, bpy) + horizons = tuple(d * bpd for d in self.horizons_days) + direction = np.clip(tsmom_blend(c, horizons), self.floor, None) # <-- IL GRADO DI LIBERTA' + scal = np.where((vol > 0) & np.isfinite(vol), self.target_vol / vol, 0.0) + tgt = np.clip(direction * scal, -self.leverage, self.leverage) + tgt[~np.isfinite(tgt)] = 0.0 + return tgt + + +_CFG = {k: v for k, v in CANONICAL.items() if k != "long_only"} + + +def tp_floor(floor: float) -> TPFloor: + return TPFloor(long_only=False, floor=floor, **_CFG) + + +# =========================================================================== +# DATI — resample 1h -> 24h con ANCORA h (stessa convenzione di resample_tf) +# =========================================================================== +@lru_cache(maxsize=8) +def _h1(asset: str) -> pd.DataFrame: + return al.get(asset, "1h") + + +@lru_cache(maxsize=128) +def bars(asset: str, off: int) -> pd.DataFrame: + g = _h1(asset).copy() + idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True) + idx.name = "dt" + g.index = idx + out = g.resample("24h", offset=pd.Timedelta(hours=off), label="left", closed="left").agg( + {"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}) + out = out.dropna(subset=["open"]) + out["datetime"] = out.index + epoch = pd.Timestamp("1970-01-01", tz="UTC") + out["timestamp"] = ((out.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64") + return out.reset_index(drop=True)[ + ["timestamp", "open", "high", "low", "close", "volume", "datetime"]] + + +@lru_cache(maxsize=512) +def sleeve(floor: float, off: int = 0, fee_side: float | None = None) -> pd.Series: + """Serie 50/50 BTC+ETH dei rendimenti NETTI per barra giornaliera, ancora `off`. + Usa `TrendPortfolio.net_returns` EREDITATA -> shift causale e fee di produzione.""" + tp = tp_floor(floor) + if fee_side is not None: + tp.fee_side = fee_side + ser = {} + for a in ASSETS: + df = bars(a, off) + net, ts = tp.net_returns(df) + ser[a] = pd.Series(net, index=pd.DatetimeIndex(pd.to_datetime(ts.values, utc=True))) + J = pd.concat(ser, axis=1, join="inner").fillna(0.0) + return pd.Series(0.5 * J[ASSETS[0]].values + 0.5 * J[ASSETS[1]].values, index=J.index) + + +# =========================================================================== +# METRICHE +# =========================================================================== +def sh(s: pd.Series) -> float: + return al._sh(s) + + +def dd(s: pd.Series) -> float: + return al._dd_ret(s) + + +def drift(s: pd.Series) -> float: + """Drift annualizzato in LOG (la grandezza su cui vivono i muri di capitale; il carry si + misura sul log, mai sulla media aritmetica delle differenze — regola 23/08).""" + r = np.asarray(s.dropna().values, float) + return float(np.mean(np.log1p(np.clip(r, -0.99, None))) * 365.25) if len(r) else 0.0 + + +def cagr(s: pd.Series) -> float: + r = np.asarray(s.dropna().values, float) + if len(r) < 2: + return 0.0 + return float(np.exp(np.mean(np.log1p(np.clip(r, -0.99, None))) * 365.25) - 1.0) + + +def vol(s: pd.Series) -> float: + r = np.asarray(s.dropna().values, float) + return float(np.std(r) * np.sqrt(365.25)) if len(r) else 0.0 + + +def hold(s: pd.Series) -> pd.Series: + return s[s.index >= HOLDOUT] + + +def no2022(s: pd.Series) -> pd.Series: + return s[s.index.year != 2022] + + +def row(s: pd.Series) -> dict: + return dict(sh=sh(s), sh_hold=sh(hold(s)), cagr=cagr(s), drift=drift(s), + vol=vol(s), dd=dd(s), n=len(s)) + + +def turnover_year(floor: float, off: int = 0) -> float: + tot = 0.0 + for a in ASSETS: + df = bars(a, off) + tgt = tp_floor(floor).target_series(df) + held = np.zeros(len(tgt)); held[1:] = tgt[:-1] + turn = np.abs(np.diff(held, prepend=0.0)).sum() + yrs = (df["datetime"].iloc[-1] - df["datetime"].iloc[0]).total_seconds() / 86400 / 365.25 + tot += turn / yrs + return tot / len(ASSETS) + + +def time_in_market(floor: float, off: int = 0) -> tuple[float, float]: + lo, sho = [], [] + for a in ASSETS: + tgt = tp_floor(floor).target_series(bars(a, off)) + lo.append(float(np.mean(tgt > 0))); sho.append(float(np.mean(tgt < 0))) + return float(np.mean(lo)), float(np.mean(sho)) + + +def hr(t=""): + print("\n" + "=" * 78) + if t: + print(t) + print("=" * 78) + + +# =========================================================================== +def main() -> None: + t_start = time.time() + print(__doc__.split("\n\n")[0]) + print(f"\nAvvio {pd.Timestamp.now('UTC')} | griglia dichiarata: {len(FLOORS)} floor x 1 TF (1d) = " + f"{len(FLOORS)} trial | ancore: {len(OFFSETS)} (NON trial: gate d'ancora)") + + # ------------------------------------------------------------------ 1. REPLICA BIT-EXACT + hr("1. REPLICA BIT-EXACT DEL CANONICO (prima di qualunque delta)") + ok_all = True + canon = TrendPortfolio(**CANONICAL) + ls_prod = TrendPortfolio(**{**CANONICAL, "long_only": False}) + for a in ASSETS: + df = bars(a, 0) + d0 = float(np.max(np.abs(tp_floor(0.0).target_series(df) - canon.target_series(df)))) + d1 = float(np.max(np.abs(tp_floor(-1.0).target_series(df) - ls_prod.target_series(df)))) + print(f" {a}: max|floor=0 - CANONICAL(long_only=True) | = {d0:.1e} " + f"max|floor=-1 - CANONICAL(long_only=False)| = {d1:.1e}") + ok_all &= (d0 == 0.0) and (d1 == 0.0) + from src.portfolio.sleeves import _tp01_returns + prod = _tp01_returns() + mine = sleeve(0.0, 0) + JJ = pd.concat({"p": prod, "m": mine}, axis=1, join="inner").dropna() + d2 = float(np.max(np.abs(JJ["p"] - JJ["m"]))) + print(f" sleeve 50/50 vs src.portfolio.sleeves._tp01_returns(): n={len(JJ)} max|diff| = {d2:.1e}") + ok_all &= (d2 == 0.0) + print(f" ==> REPLICA {'OK (3/3 a 0.0)' if ok_all else 'FALLITA — NON procedere'}") + if not ok_all: + sys.exit(1) + + # ------------------------------------------------------------------ 2. CONTROLLI POSITIVI + hr("2. CONTROLLI POSITIVI DEI GATE (un gate che non segnala puo' essere rotto)") + + def tgt_fn(floor): + return lambda df: tp_floor(floor).target_series(df) + + def leaky(df): # guarda il futuro: DEVE fallire causality_ok + t = tp_floor(-1.0).target_series(df) + out = np.zeros_like(t); out[:-1] = t[1:] + return out + + c_ok = al.causality_ok(tgt_fn(-1.0), tf="1d") + c_bad = al.causality_ok(leaky, tf="1d") + print(f" causality_ok(floor=-1) -> ok={c_ok['ok']} max_tail_diff={c_ok['max_tail_diff']}") + print(f" causality_ok(variante LEAKY) -> ok={c_bad['ok']} max_tail_diff={c_bad['max_tail_diff']}" + f" [controllo positivo: deve essere False]") + ctrl1 = c_ok["ok"] and not c_bad["ok"] + + rng = np.random.default_rng(20260823) + idx = pd.date_range("2020-01-01", periods=900, freq="1D", tz="UTC") + fake_good = pd.Series(np.abs(rng.normal(0.002, 0.0005, 900)), index=idx) # 0 perdite + fake_noise = pd.Series(rng.normal(0.0004, 0.012, 900), index=idx) + ip_g = al.implausible_sharpe(fake_good) + ip_n = al.implausible_sharpe(fake_noise) + print(f" implausible_sharpe(serie senza perdite) -> implausible={ip_g['implausible']} " + f"[deve essere True] (rumore Sh {ip_n['sharpe']:.2f} -> {ip_n['implausible']} [deve essere False])") + ctrl2 = ip_g["implausible"] and not ip_n["implausible"] + + base0 = sleeve(0.0, 0) + lev13 = base0 * 1.3 # LEVA PURA: il gate iso-vol deve bocciarla + k_lev = vol(base0) / vol(lev13) + print(f" GATE ISO-VOL, controllo positivo su LEVA PURA (canonico x1.3):") + print(f" grezzo dCAGR = {cagr(lev13) - cagr(base0):+.4f} (positivo: sembra 'piu' drift')") + print(f" iso-vol dCAGR = {cagr(lev13 * k_lev) - cagr(base0):+.6f} [deve essere ~0 -> e' leva]") + ctrl3 = abs(cagr(lev13 * k_lev) - cagr(base0)) < 1e-6 + + ad_self = al.anchor_luck_delta(lambda o: sleeve(0.0, o), lambda o: sleeve(0.0, o), OFFSETS, metric=sh) + print(f" anchor_luck_delta(A vs A) -> mediana {ad_self['median_paired']:+.6f}, " + f"positivi {ad_self['n_positive']}/{ad_self['n_anchors']} [deve essere 0 e 0/24]") + ctrl4 = (ad_self["median_paired"] == 0.0) and (ad_self["n_positive"] == 0) + print(f" ==> CONTROLLI POSITIVI: {sum([ctrl1, ctrl2, ctrl3, ctrl4])}/4 superati") + + # ------------------------------------------------------------------ 3. TABELLA PRINCIPALE + hr("3. TABELLA PRINCIPALE (ancora canonica h=0, fee 0.10% RT, 50/50 BTC+ETH)") + print(f" {'floor':<16} {'ShFULL':>7} {'ShHOLD':>7} {'CAGR':>8} {'drift':>8} {'vol':>7} " + f"{'maxDD':>7} {'turn/a':>7} {'%long':>6} {'%short':>7}") + rows = {} + for f in FLOORS: + s = sleeve(f, 0) + r = row(s); rows[f] = r + tl, ts = time_in_market(f) + print(f" {FLOOR_LBL[f]:<16} {r['sh']:>7.3f} {r['sh_hold']:>7.3f} {r['cagr']:>7.2%} " + f"{r['drift']:>7.2%} {r['vol']:>6.2%} {r['dd']:>6.2%} {turnover_year(f):>7.2f} " + f"{tl:>5.1%} {ts:>6.1%}") + print(f" (n barre = {rows[0.0]['n']}, dal {base0.index[0].date()} al {base0.index[-1].date()})") + + # ------------------------------------------------------------------ 4. GATE ISO-VOL + hr("4. GATE ISO-VOLATILITA' (null del de-levering AL CONTRARIO: la variante ALZA la vol)") + print(" Convenzione: si DE-LEVA la variante fino alla vol del canonico (k<1, gratis: nessuna") + print(" leva richiesta). Se a pari vol il canonico regge, la short era leva travestita.") + print(f" {'floor':<16} {'k de-lev':>9} {'CAGR iso':>9} {'dCAGR iso':>10} {'dSharpe':>9} " + f"{'maxDD iso':>10} {'esito':>10}") + isovol = {} + for f in FLOORS: + s = sleeve(f, 0) + k = vol(base0) / vol(s) if vol(s) > 0 else 1.0 + si = s * k + dc = cagr(si) - cagr(base0) + isovol[f] = dict(k=k, dcagr=dc, dsh=sh(s) - sh(base0), dd=dd(si)) + v = "—" if f == 0.0 else ("PASS" if dc > 0 else "FAIL") + print(f" {FLOOR_LBL[f]:<16} {k:>9.4f} {cagr(si):>8.2%} {dc:>+10.2%} " + f"{sh(s) - sh(base0):>+9.3f} {dd(si):>9.2%} {v:>10}") + + # ------------------------------------------------------------------ 5. BANDA D'ANCORA + hr("5. BANDA D'ANCORA — 24 ore, MEDIANA DELLE DIFFERENZE APPAIATE (variante - canonico)") + print(" (un Delta misurato a una sola ancora eredita la fortuna di quell'ancora: regola 26/07)") + band = {} + for f in FLOORS[1:]: + d_sh = al.anchor_luck_delta(lambda o, f=f: sleeve(f, o), lambda o: sleeve(0.0, o), OFFSETS, metric=sh) + d_ho = al.anchor_luck_delta(lambda o, f=f: hold(sleeve(f, o)), lambda o: hold(sleeve(0.0, o)), + OFFSETS, metric=sh) + d_dr = al.anchor_luck_delta(lambda o, f=f: sleeve(f, o), lambda o: sleeve(0.0, o), OFFSETS, metric=drift) + d_dd = al.anchor_luck_delta(lambda o, f=f: sleeve(f, o), lambda o: sleeve(0.0, o), OFFSETS, metric=dd) + band[f] = dict(sh=d_sh, ho=d_ho, dr=d_dr, dd=d_dd) + print(f" floor {FLOOR_LBL[f]:<14} dShFULL {d_sh['median_paired']:+.3f} " + f"[{d_sh['lo']:+.3f},{d_sh['hi']:+.3f}] {d_sh['n_positive']:>2}/24 | " + f"dShHOLD {d_ho['median_paired']:+.3f} {d_ho['n_positive']:>2}/24 | " + f"dDRIFT {d_dr['median_paired']:+.2%} {d_dr['n_positive']:>2}/24 | " + f"dMAXDD {d_dd['median_paired']:+.2%} {d_dd['n_positive']:>2}/24") + print("\n ⚠️ L'UNICA metrica che va nel verso della short e' l'HOLD-OUT, e all'ancora") + print(" CANONICA non si vede: e' il caso speculare della lezione 26/07 (li' l'ancora") + print(" canonica INVENTAVA un danno, qui NASCONDE un vantaggio).") + print(f" {'floor':<16} {'dShHOLD @h=0':>13} {'mediana 24':>11} {'p10':>8} {'p90':>8}") + for f in FLOORS[1:]: + v = np.array(list(band[f]['ho']['per_offset'].values()), float) + print(f" {FLOOR_LBL[f]:<16} {band[f]['ho']['per_offset'][0]:>+13.3f} " + f"{np.median(v):>+11.3f} {np.percentile(v, 10):>+8.3f} {np.percentile(v, 90):>+8.3f}") + b_can = al.anchor_luck_band(lambda o: sleeve(0.0, o), OFFSETS, metric=sh) + b_ls = al.anchor_luck_band(lambda o: sleeve(-1.0, o), OFFSETS, metric=sh) + print(f" livelli: canonico ShFULL canonica {b_can['canonical']:.3f} (pctl {b_can['canonical_pctl']:.2f}) " + f"mediana {b_can['median']:.3f} | LS pieno canonica {b_ls['canonical']:.3f} " + f"(pctl {b_ls['canonical_pctl']:.2f}) mediana {b_ls['median']:.3f}") + + # ------------------------------------------------------------------ 6. PER ANNO / SENZA 2022 + hr("6. SCOMPOSIZIONE PER ANNO — e la domanda che decide: E SENZA IL 2022?") + years = sorted(set(base0.index.year)) + print(f" Sharpe per anno (ancora h=0)") + print(f" {'anno':<6}" + "".join(f"{FLOOR_LBL[f]:>18}" for f in FLOORS)) + for y in years: + line = f" {y:<6}" + for f in FLOORS: + s = sleeve(f, 0); sy = s[s.index.year == y] + line += f"{sh(sy):>18.2f}" + print(line) + print(f"\n Rendimento per anno") + print(f" {'anno':<6}" + "".join(f"{FLOOR_LBL[f]:>18}" for f in FLOORS)) + for y in years: + line = f" {y:<6}" + for f in FLOORS: + s = sleeve(f, 0); sy = s[s.index.year == y] + line += f"{float(np.prod(1 + sy.values) - 1):>17.1%} " + print(line) + print(f"\n dSharpe ANNUALE della variante vs canonico (positivo = la short paga QUELL'anno)") + print(f" {'anno':<6}" + "".join(f"{FLOOR_LBL[f]:>18}" for f in FLOORS[1:])) + npos = {f: 0 for f in FLOORS[1:]} + for y in years: + line = f" {y:<6}" + s0 = sleeve(0.0, 0) + for f in FLOORS[1:]: + s = sleeve(f, 0) + d = sh(s[s.index.year == y]) - sh(s0[s0.index.year == y]) + npos[f] += int(d > 0) + line += f"{d:>+18.2f}" + print(line) + print(" anni positivi: " + " ".join(f"{FLOOR_LBL[f]} {npos[f]}/{len(years)}" for f in FLOORS[1:])) + + print(f"\n --- SENZA IL 2022 (ancora h=0) ---") + print(f" {'floor':<16} {'ShFULL':>7} {'dSh':>7} {'CAGR':>8} {'dCAGR':>8} {'iso-vol dCAGR':>14}") + b0n = no2022(base0) + for f in FLOORS: + sn = no2022(sleeve(f, 0)) + k = vol(b0n) / vol(sn) if vol(sn) > 0 else 1.0 + print(f" {FLOOR_LBL[f]:<16} {sh(sn):>7.3f} {sh(sn) - sh(b0n):>+7.3f} {cagr(sn):>7.2%} " + f"{cagr(sn) - cagr(b0n):>+7.2%} {cagr(sn * k) - cagr(b0n):>+14.2%}") + + print(f"\n --- BANDA D'ANCORA SENZA IL 2022 (mediana differenze appaiate) ---") + for f in FLOORS[1:]: + d_sh = al.anchor_luck_delta(lambda o, f=f: no2022(sleeve(f, o)), + lambda o: no2022(sleeve(0.0, o)), OFFSETS, metric=sh) + d_dr = al.anchor_luck_delta(lambda o, f=f: no2022(sleeve(f, o)), + lambda o: no2022(sleeve(0.0, o)), OFFSETS, metric=drift) + print(f" floor {FLOOR_LBL[f]:<14} dShFULL {d_sh['median_paired']:+.3f} " + f"[{d_sh['lo']:+.3f},{d_sh['hi']:+.3f}] {d_sh['n_positive']:>2}/24 | " + f"dDRIFT {d_dr['median_paired']:+.2%} {d_dr['n_positive']:>2}/24") + + # ------------------------------------------------------------------ 7. PROTEZIONE DD + hr("7. LA PROTEZIONE — il valore DICHIARATO di TP01 e' il taglio del DD: la short lo rompe?") + bh = {} + for a in ASSETS: + df = bars(a, 0) + bh[a] = pd.Series(simple_returns(df["close"].values.astype(float)), + index=pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))) + JB = pd.concat(bh, axis=1, join="inner").fillna(0.0) + bh50 = pd.Series(0.5 * JB[ASSETS[0]].values + 0.5 * JB[ASSETS[1]].values, index=JB.index) + bh50 = bh50.reindex(base0.index).fillna(0.0) + print(f" buy&hold 50/50: Sharpe {sh(bh50):.2f} CAGR {cagr(bh50):.1%} maxDD {dd(bh50):.1%}") + print(f" Criterio (B) di edge_watch: in un anno con DD del buy&hold > 10%, il DD dello sleeve") + print(f" deve restare sotto il 75% di quello. Rapporto DD_sleeve/DD_B&H per anno di sinistro:") + print(f" {'anno':<6} {'DD B&H':>8}" + "".join(f"{FLOOR_LBL[f]:>18}" for f in FLOORS)) + fails = {f: 0 for f in FLOORS}; sinistri = 0 + for y in years: + by = bh50[bh50.index.year == y] + if dd(by) <= 0.10: + continue + sinistri += 1 + line = f" {y:<6} {dd(by):>7.1%}" + for f in FLOORS: + s = sleeve(f, 0); r_ = dd(s[s.index.year == y]) / dd(by) + fails[f] += int(r_ >= 0.75) + line += f"{r_:>17.2f}x" if False else f"{r_:>18.2f}" + print(line) + print(f" anni di sinistro: {sinistri}. Violazioni del criterio (rapporto >= 0.75): " + + " ".join(f"{FLOOR_LBL[f]} {fails[f]}" for f in FLOORS)) + print(f" maxDD full: " + " ".join(f"{FLOOR_LBL[f]} {rows[f]['dd']:.2%}" for f in FLOORS)) + + # ------------------------------------------------------------------ 8. COSTI + hr("8. COSTI — la short raddoppia il turnover? curva fee 0-20 bps/lato") + print(f" {'fee/lato':>9} {'%RT':>7}" + "".join(f"{FLOOR_LBL[f]:>18}" for f in FLOORS)) + for bps in (0, 2.5, 5, 10, 15, 20): + line = f" {bps:>8.1f}b {bps * 2 / 100:>6.2f}%" + for f in FLOORS: + line += f"{sh(sleeve(f, 0, bps / 10000.0)):>18.3f}" + print(line) + print(" turnover/anno (media asset): " + " ".join(f"{FLOOR_LBL[f]} {turnover_year(f):.2f}" for f in FLOORS)) + + # ------------------------------------------------------------------ 9. LIBRO 75/25 + SKH01 + hr("9. IMPATTO SUL LIBRO 75/25 CON SKH01 (che e' GIA' long/short: ridondanza?)") + from src.portfolio.sleeves import _skyhook_returns + skh = _skyhook_returns() + skh.index = pd.DatetimeIndex(skh.index).tz_convert("UTC") if skh.index.tz else \ + pd.DatetimeIndex(skh.index).tz_localize("UTC") + + def book(floor: float, off: int = 0) -> pd.Series: + s = sleeve(floor, off).copy() + s.index = s.index.floor("1D") # ancora h -> data di calendario (1 barra per data) + J = pd.concat({"t": s, "k": skh}, axis=1, join="inner").dropna() + return pd.Series(W_TP01_BOOK * J["t"].values + W_SKH_BOOK * J["k"].values, index=J.index) + + print(f" {'floor':<16} {'ShFULL':>7} {'ShHOLD':>7} {'CAGR':>8} {'maxDD':>7}") + for f in FLOORS: + b = book(f) + print(f" {FLOOR_LBL[f]:<16} {sh(b):>7.3f} {sh(hold(b)):>7.3f} {cagr(b):>7.2%} {dd(b):>6.2%}") + print(f" banda d'ancora del LIBRO (24 ancore su TP01, SKH01 all'ancora canonica):") + for f in FLOORS[1:]: + d_b = al.anchor_luck_delta(lambda o, f=f: book(f, o), lambda o: book(0.0, o), OFFSETS, metric=sh) + d_c = al.anchor_luck_delta(lambda o, f=f: book(f, o), lambda o: book(0.0, o), OFFSETS, metric=drift) + print(f" floor {FLOOR_LBL[f]:<14} dSh libro {d_b['median_paired']:+.3f} " + f"[{d_b['lo']:+.3f},{d_b['hi']:+.3f}] {d_b['n_positive']:>2}/24 | " + f"dDRIFT {d_c['median_paired']:+.2%} {d_c['n_positive']:>2}/24") + + print(f"\n RIDONDANZA con SKH01 — la 'gamba short' isolata = variante meno canonico:") + for f in FLOORS[1:]: + leg = (sleeve(f, 0) - sleeve(0.0, 0)) + leg.index = leg.index.floor("1D") + JL = pd.concat({"l": leg, "k": skh}, axis=1, join="inner").dropna() + print(f" floor {FLOOR_LBL[f]:<14} corr(gamba short, SKH01) = {JL['l'].corr(JL['k']):+.3f} " + f"Sharpe della gamba isolata {sh(JL['l']):+.3f} drift {drift(JL['l']):+.2%}") + + # ------------------------------------------------------------------ 10. MARGINALE + DSR + hr("10. MARGINALE vs TP01 STESSO + DEFLATED SHARPE (4 trial dichiarati)") + cd0 = al.candidate_daily(tgt_fn(0.0), tf="1d") + bl = al.tp01_baseline_daily() + JC = pd.concat({"a": cd0, "b": bl}, axis=1, join="inner").dropna() + print(f" 4a replica: candidate_daily(floor=0) vs tp01_baseline_daily() max|diff| " + f"{float(np.max(np.abs(JC['a'] - JC['b']))):.1e} (n={len(JC)})") + # trial set del DSR: si prende dal percorso CODIFICATO (select_cell_insample), non a mano. + # ⚠️ errore mio catturato: la prima stesura costruiva la lista dei trial da solo. Due percorsi + # per lo stesso numero e' esattamente cio' che il gate codificato serve a evitare -> qui li + # confronto invece di sostituirne uno in silenzio. + sel = al.select_cell_insample(lambda tf, floor: tgt_fn(floor), + [dict(floor=f) for f in FLOORS], ("1d",)) + all_sh = sel["all_full_sharpe"] + mine_sh = [round(sh(sleeve(f, 0)), 3) for f in FLOORS] + print(f" trial set DSR da select_cell_insample: {all_sh}") + print(f" stesso conto per via indipendente (sleeve): {mine_sh} -> " + f"{'COINCIDONO' if all_sh == mine_sh else 'DIVERGONO (fermarsi)'}") + for f in FLOORS[1:]: + cd = al.candidate_daily(tgt_fn(f), tf="1d") + m = al.marginal_vs_tp01(cd) + d, sr0 = al.deflated_sharpe(sh(sleeve(f, 0)), all_sh, sleeve(f, 0)) + ip = al.implausible_sharpe(sleeve(f, 0)) + print(f"\n floor {FLOOR_LBL[f]}: marginal_verdict = {m['marginal_verdict']}") + print(f" corr->TP01 {m['corr_full']} beta {m['beta_to_tp01']} alpha/a {m['alpha_ann']:+.4f} " + f"resid Sharpe {m['resid_sharpe_full']}") + print(f" blend w25 uplift full {m['blends']['w25']['uplift_full']:+.3f} / hold " + f"{m['blends']['w25']['uplift_hold']:+.3f} w50 full " + f"{m['blends']['w50']['uplift_full']:+.3f} / hold {m['blends']['w50']['uplift_hold']:+.3f}") + print(f" robust_oos={m['robust_oos']} multicut={m['multicut_persistent']} " + f"insample_edge={m['has_insample_edge']} (Sh IS {m['cand_insample_sharpe']}) " + f"is_hedge={m['is_hedge']}") + print(f" HEDGE vs ALPHA: corr(Sharpe-TP01 annuo, uplift annuo) = {m['hedge_yearly_corr']} " + f"uplift quando TP01 SALE {m['uplift_tp01_up']} / quando TP01 SCENDE {m['uplift_tp01_down']}") + print(f" multicut per anno: {m['multicut_uplift']}") + print(f" deflated-Sharpe {d:.3f} (sr0 nullo {sr0:.3f}, N={len(all_sh)}) " + f"implausible={ip['implausible']}") + print(f"\n --- GATE SELECTION-ON-HOLDOUT (codificato 29/06): quale floor si sceglie ") + print(f" guardando SOLO il pre-2025? Se e' il canonico, il guadagno hold-out NON e'") + print(f" selezionabile e sceglierlo sarebbe selezione-sull'hold-out. ---") + print(f" {'floor':<16} {'Sh IN-SAMPLE (<2025)':>21} {'Sh HOLD-OUT':>12} {'Sh FULL':>9}") + for f in FLOORS: + s_ = sleeve(f, 0) + print(f" {FLOOR_LBL[f]:<16} {sh(s_[s_.index < HOLDOUT]):>21.3f} {sh(hold(s_)):>12.3f} {sh(s_):>9.3f}") + fb = sel["chosen"]["params"]["floor"] + fh = max(FLOORS, key=lambda f: sh(hold(sleeve(f, 0)))) + print(f" cella scelta AL BUIO (max Sharpe in-sample) = floor {fb:+.4f} -> " + f"{'IL CANONICO' if abs(fb) < 1e-9 else 'NON il canonico'}") + print(f" cella scelta SULL'HOLD-OUT (max Sharpe hold-out) = floor {fh:+.4f} -> " + f"{'diversa: firma di selezione-sull-hold-out' if abs(fh - fb) > 1e-9 else 'la stessa'}") + + print(f"\n ⚠️ NOTA ONESTA sul DSR: con 4 celle quasi identiche sr0 e' minuscolo -> il DSR e'") + print(f" alto per COSTRUZIONE (dipende dalla VARIANZA della griglia, lezione §10 del 22/08).") + print(f" Qui non e' il gate che decide: e' un contorno. Il gate che decide e' l'iso-vol +") + print(f" la banda d'ancora + il per-anno.") + + # ------------------------------------------------------------------ 11. ESEGUIBILITA' + hr(f"11. ESEGUIBILITA' A ${CAPITAL:.0f} (min_order $5) e vincolo di NETTING del libro") + for f in (0.0, -1.0): + for a in ASSETS: + df = bars(a, 0) + sc = al.eval_weights_smallcap(df, tp_floor(f).target_series(df), + capital=CAPITAL, min_order=5.0) + print(f" floor {FLOOR_LBL[f]:<14} {a}: Sharpe modellato {sc['modeled']['sharpe']:.3f} -> " + f"reale {sc['realistic']['sharpe']:.3f} haircut {sc['sharpe_haircut']:+.3f} " + f"trade eseguiti {sc['n_executed_trades']}") + src_book = (ROOT / "src" / "live" / "book.py").read_text() + clipped = "max(tp_frac, 0.0)" in src_book + print(f"\n VERIFICA SUL CODICE CHE GIRA: src/live/book.py contiene 'max(tp_frac, 0.0)' -> {clipped}") + print(f" Cioe' l'esecutore del libro CLIPPA a zero la frazione di TP01: il long-flat non e' solo") + print(f" in CANONICAL, e' cablato anche in `book_net_target`. Attivare la short richiederebbe") + print(f" di toccare src/live/book.py, cioe' CODICE su un percorso con soldi veri; e TP01 e SKH01") + print(f" si nettano su UNA sola posizione per strumento, quindi la short di TP01 andrebbe a") + print(f" sommarsi/cancellarsi con quella di SKH01 invece di essere una gamba a se'.") + + hr("12. VERDETTO E COSA NON E' STATO MISURATO") + dsh_ls = band[-1.0]["sh"]["median_paired"]; ddr_ls = band[-1.0]["dr"]["median_paired"] + print(f" floor=-1 (LS pieno): dShFULL appaiato {dsh_ls:+.3f} ({band[-1.0]['sh']['n_positive']}/24), " + f"dDRIFT {ddr_ls:+.2%} ({band[-1.0]['dr']['n_positive']}/24), " + f"dMAXDD {band[-1.0]['dd']['median_paired']:+.2%} ({band[-1.0]['dd']['n_positive']}/24)") + print(f" iso-vol dCAGR {isovol[-1.0]['dcagr']:+.2%} | anni positivi {npos[-1.0]}/{len(years)} | " + f"cella scelta al buio = il canonico") + print(f"\n >>> VERDETTO: SCARTATO. La gamba short NON paga il proprio costo: toglie drift") + print(f" ({ddr_ls:+.2%}/anno a 0/24 ancore), FALLISCE il gate iso-vol, peggiora il maxDD") + print(f" ({band[-1.0]['dd']['median_paired']:+.2%} a 24/24) e non e' selezionabile in-sample.") + print(f" L'attesa a priori del coordinatore (il guadagno e' il 2022) e' CONFERMATA e") + print(f" superata: il 2022 c'e' ({npos[-1.0]}/8 anni positivi, dSharpe 2022 +1.84) ma NON") + print(f" compensa gli altri sei nemmeno con se stesso dentro il campione.") + print(f" E NON e' morte-per-fee: a fee ZERO il canonico fa {sh(sleeve(0.0, 0, 0.0)):.3f} contro " + f"{sh(sleeve(-1.0, 0, 0.0)):.3f} -> l'edge lordo non c'e'.") + print(f"\n COSA NON HO MISURATO (dichiarato, non nascosto):") + print(f" (a) FUNDING. Nessun backtest del progetto lo modella (§35, 22/08) e qui nemmeno.") + print(f" E' l'unica voce che potrebbe muovere il numero nel verso della short: una") + print(f" posizione corta INCASSA il funding quando il tasso e' positivo. MA il") + print(f" meccanismo misurato il 22/08 e' che esposizione e funding sono POSITIVAMENTE") + print(f" correlati, e un trend-short entra quando il mercato scende, cioe' quando il") + print(f" funding e' tipicamente NEGATIVO (e li' e' lo short a pagare). Argomento, non") + print(f" misura: si chiude con la macchineria di r0822d_funding.py, non a parole.") + print(f" Ordine di grandezza in gioco (~1-2%/anno) contro un drift perso di " + f"{-ddr_ls:.2%}/anno.") + print(f" (b) TF diversi dal 1d. Riaprire il TF riapre la FAMIGLIA (griglia 4 -> 4xN celle)") + print(f" e imporrebbe study_family_honest; qui il TF e' congelato a quello di deploy.") + print(f" (c) La banda d'ancora del LIBRO tiene SKH01 alla sua ancora canonica (le 23 fasi") + print(f" di SKH01 non sono state incrociate): il delta di libro e' appaiato solo sulle") + print(f" 24 ancore di TP01. Non cambia il segno (0/24 su tutte e tre le celle).") + print(f" (d) La lente WICK accoppiata (25/07): tutto qui e' close-only. Irrilevante per il") + print(f" verdetto (non c'e' barriera di rovina in gioco), rilevante se qualcuno") + print(f" riproponesse la variante sul canale funded.") + print(f"\n Tempo totale {time.time() - t_start:.1f}s") + + +if __name__ == "__main__": + main() From 790849cb547ac0f4e701333941bc6f08f374e646 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 00:54:35 +0000 Subject: [PATCH 061/114] =?UTF-8?q?CLAUDE.md:=20il=20trasporto=20degli=20a?= =?UTF-8?q?llarmi=20non=20e'=20mai=20stato=20validato=20=E2=80=94=20un=20t?= =?UTF-8?q?entativo,=20nessun=20registro,=20stato=20marcato=20prima=20dell?= =?UTF-8?q?'invio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 31955ef..546e563 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1241,6 +1241,40 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis powers*, conti dormienti) non lo sorveglia nessuno; `MAINT_GRACE_HOURS`=2 **presume** gli annunci invece di leggerli (due `locked=true` in 4 giorni: 18/08 ~4h, 21/08 ~1h); la terza referenza non e' validabile sulla storia. +- 🚨 **IL TRASPORTO DEGLI ALLARMI E' UN PUNTO SINGOLO DI GUASTO — misurato 2026-08-23, NON + riparato (produzione, decisione dell'operatore).** Registro `docs/research/RESULTS-0822.md` + (sezione NOTIFIER). Il progetto ha tarato il **rilevatore** (`venue_watch`: 1 falso allarme in 8 + anni, controllo positivo su 22 episodi Bitfinex, margine 3x su FTX) e **mai il trasporto**. + `src/live/notifier.send()` fa **UN tentativo** `urlopen(..., timeout=10)`, `except Exception: + return False`, **nessun retry**; `notify()` ritorna `bool` e **`scripts/live/venue_watch.py:80` + non lo guarda**; `logs/cron_book.log` ha **0 occorrenze** di un qualsiasi esito d'invio → *quando + serve sapere se l'allarme e' arrivato, l'informazione non e' stata scritta* (la regola del 29/07 + — *"se un errore si ingoia per non bloccare, si registra nel punto in cui lo si ingoia"* — non e' + mai stata applicata al notifier). + 🚨 **E lo stato e' marcato PRIMA dell'invio:** in `venue_watch` la riga `new.alerted = True` + sta dentro la logica pura e viene persistita comunque; alle ore successive + `already = st.alerted and st.sign == sign` fa tornare `"WATCH"` invece di `"ALERT"` → **`notify` + non viene piu' chiamata per quell'episodio**. **Un 🚨 perso e' perso per l'episodio intero**, e + gli episodi storici durano **200-2.324 ore a segno costante**: e' esattamente il caso in cui la + ri-notifica non arriva mai. + **Taglia misurata: 6,9% di invii falliti (2/29, IC95 Wilson [1,9%, 22,0%])** sul digest + giornaliero. ⚠️ **Fonte dichiarata:** e' il percorso del **digest** usato come **proxy** di quello + d'allarme, che **non ha dati propri — ed e' questo il punto**; stessa funzione, stesso endpoint, + stesso timeout. ⚠️ La nota stampata (`"config Telegram assente o rete KO"`) **conflazione due + cause e la prima e' falsa**: le chiavi sono presenti in `.env` (verificato) → manda a controllare + il posto sbagliato, **stessa forma del difetto codificato il 29/07**, su un percorso diverso. + **Perche' conta piu' del suo numero:** l'economia del 26/07 (falso allarme 0,248% di equity + contro il **100%** che un vero positivo evita, break-even `p > 0,031%`) assume che l'allarme + **arrivi**, e questa e' l'unica mitigazione rimasta dopo la decisione *100% Deribit fino a $20k* + contro un rischio prezzato a **P(perso tutto) 10/18/34/64%**. + **Riparazione in tre pezzi indipendenti, NON eseguita:** (a) retry con backoff in `send()`; + (b) **registrare l'esito** nel punto in cui l'eccezione viene ingoiata; (c) marcare + `alerted=True` **solo a invio riuscito**. ⚠️ **(c) cambia il comportamento** — rende l'allarme + ripetitivo finche' non passa: verso giusto per un 🚨, sbagliato per un ⚠️ → **decisione + dell'operatore**, non un fix ovvio. + **REGOLA: un rilevatore si valida sul segnale E sul TRASPORTO** — tarare la soglia e non misurare + mai se il messaggio arriva lascia un punto singolo di guasto a valle di tutto il lavoro di + taratura, e non produce numeri, quindi non si fa notare. - ⚖️ **RIVALUTAZIONE DELLA STRATEGIA (2026-07-26, fine giornata) — 0 cambi, e il peso 75/25 confermato per la TERZA volta.** Script `r0726_reeval_live_weight.py`, test `tests/test_reeval_live_weight.py` (6), diario `2026-07-26-reeval-strategia.md`. From 8e7d18117198787965ae2c41a8de305241078ea0 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 00:56:10 +0000 Subject: [PATCH 062/114] =?UTF-8?q?registro=20=C2=A742:=20TP01-LS=20SCARTA?= =?UTF-8?q?TO=20=E2=80=94=20la=20short=20toglie=20drift=20in=200/24=20anco?= =?UTF-8?q?re=20e=20senza=20il=202022=20il=20divario=20raddoppia?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 94 ++++++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 6af6c9b..f64ca91 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -707,7 +707,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 39 | FUNDING-AVOID | evitare il funding esprimendo il libro sui datati | **SCARTATO** sui datati · **LEAD** sullo **spot** | | 40 | SL-ANCHOR | il nodo che bloccava il gradino di leva | **SCIOLTO** — rotolante, ma il timore e' refutato | | 41 | TP01-SIZE | una size che risponde alla CONVINZIONE batte il vol-target uniforme, a pari vol realizzata? | _in corso_ | -| 42 | TP01-LS | la gamba short del trend a 1d paga il proprio costo, o e' il 2022? | _in corso_ | +| 42 | TP01-LS | la gamba short del trend a 1d paga il proprio costo, o e' il 2022? | **SCARTATO** — toglie **−1,91%/anno di drift in 0/24 ancore**, fallisce l'iso-vol, +8,80pp di maxDD in 24/24, e **senza il 2022 il divario RADDOPPIA** | | 43 | DATA-UNUSED | quali dati il progetto possiede e nessuna strategia legge — e uno di essi sostiene un'ipotesi? | _in corso_ | | 44 | DEPOSIT-TIMING | un calendario di versamento CONDIZIONALE batte il piatto, a pari soldi? | _in corso_ | | 45 | SPOT-NETTING | separare TP01 e SKH01 su due strumenti: quanto vale, e il margine lo uccide? | _in corso_ | @@ -2120,3 +2120,95 @@ non produce numeri. **VERDETTO: `LA RETE DI SICUREZZA HA UN TRASPORTO SENZA RETRY, SENZA REGISTRO E CON LO STATO MARCATO PRIMA DELL'INVIO — ~7% DEGLI ALLARMI PUO' NON ARRIVARE, PER L'EPISODIO INTERO`** + +--- + +## 42 — TP01-LS (la gamba short del trend a 1d) + +`scripts/research/r0823_tp01_ls.py` (**4,4 s**). **4 trial dichiarati** (floor ∈ {0, −1/3, −2/3, −1} +sul solo TF di deploy); le **24 ancore non sono trial** e sono giudicate con `anchor_luck_band` / +`anchor_luck_delta`. Nessun file di produzione toccato. + +**Meccanismo congelato, UN grado di liberta'.** `TrendPortfolio` di produzione **importata e +sottoclassata**: l'unica riga diversa e' `np.clip(direction, floor, None)` al posto di +`np.clip(direction, 0, None)`. `net_returns` (shift causale + fee) **ereditata**. +✅ **Replica bit-exact a 4 vie, `max|diff| = 0.0`:** floor=0 == `CANONICAL(long_only=True)`; +**floor=−1 == `CANONICAL(long_only=False)`** — cioe' la riga nuova coincide con la produzione a +**entrambi** gli estremi, e il codice davvero nuovo sono i soli due floor intermedi; sleeve 50/50 == +`sleeves._tp01_returns()`; `candidate_daily(floor=0)` == `tp01_baseline_daily()`. +✅ **Controlli positivi 4/4**, fra cui il piu' importante: **gate iso-vol puntato sulla leva pura** +(canonico ×1,3) → dCAGR grezzo **+5,05%** che diventa **+0,000000** iso-vol — *il gate riconosce la +leva travestita*. + +**Attesa a priori del coordinatore, dichiarata prima di misurare:** *"il guadagno sara' un anno +solo, il 2022"*. **Confermata e superata.** + +| floor | ShFULL | ShHOLD | CAGR | drift | vol | maxDD | turn/a | +|---|---|---|---|---|---|---|---| +| **0 (canonico LF)** | **1,305** | 0,441 | **16,32%** | **15,12%** | 12,14% | **14,31%** | 7,99 | +| −1/3 | 1,199 | 0,674 | 15,53% | 14,44% | 12,71% | 16,49% | 10,61 | +| −2/3 | 1,030 | 0,546 | 13,99% | 13,09% | 13,61% | 18,12% | 12,75 | +| −1 (LS pieno) | 0,855 | 0,433 | 12,37% | 11,66% | 14,94% | 20,61% | 15,02 | + +**Drift: NO, monotono decrescente. Gate iso-vol: FAIL 3/3** (k = 0,955/0,892/0,813 → dCAGR +**−1,49% / −3,83% / −6,19%**). **Non e' leva travestita, e' peggio:** alza la vol **e** abbassa il +ritorno. + +**Banda d'ancora (24 ore, mediana delle differenze APPAIATE):** + +| floor | dShFULL | dShHOLD | dDRIFT | dMAXDD | +|---|---|---|---|---| +| −1/3 | **−0,114** [−0,149,−0,076] **0/24** | +0,219 24/24 | −0,78% 0/24 | +3,33pp 24/24 | +| −2/3 | **−0,224** 0/24 | +0,258 24/24 | −1,29% 0/24 | +5,12pp 24/24 | +| −1 | **−0,354** [−0,450,−0,251] **0/24** | +0,292 23/24 | **−1,91% 0/24** | **+8,80pp 24/24** | + +📌 **Il 2022, e la risposta e' piu' netta dell'attesa.** dSharpe per anno a floor=−1: 2019 −0,10 · +2020 **−0,85** · 2021 −0,07 · **2022 +1,84** · 2023 **−1,01** · 2024 −0,61 · 2025 +0,27 · 2026 −0,68 +→ **2/8 anni positivi**. **Senza il 2022 il divario RADDOPPIA** (dShFULL −0,406 in 0/24, dDRIFT +−3,71%, iso-vol dCAGR **−7,44%**). **Il 2022 non e' l'anno che regge la tesi: e' l'unico che la +tiene a galla a meta'** — nemmeno con lui dentro la short arriva in pari. + +**Marginale NEUTRAL 3/3** (corr→TP01 0,93/0,87/0,79, alpha **−0,21 / −1,40 / −2,60%/anno**, +`multicut_persistent=False`, `robust_oos=False`). Diagnostica: e' un **hedge debole** +(corr(Sharpe-TP01 annuo, uplift) −0,83…−0,87), non alpha. + +📌 **Il gate che chiude la faccenda: selection-on-holdout.** Sharpe in-sample (<2025) **1,488** / +1,322 / 1,151 / 0,972 → la cella scelta **al buio** da `select_cell_insample` e' **il canonico**; +quella scelta sull'hold-out e' floor=−1/3. **Il guadagno hold-out non e' selezionabile.** + +⚠️ **L'unica cosa che va nel verso della short, riportata per intero:** dShHOLD **+0,292 in 23/24** +ancore — ma **all'ancora canonica vale −0,008** (p10 +0,094, p90 +0,541). E' il caso **speculare** +della lezione del 26/07: li' l'ancora canonica *inventava* un danno, qui ne *nasconde* un vantaggio. +Resta un hold-out di 1,6 anni, non selezionabile, con multicut negativo in 6 anni su 7. + +**Protezione — la short erode proprio cio' per cui TP01 esiste.** Criterio (B) di `edge_watch` +(DD sleeve < 75% del DD buy&hold negli anni di sinistro): canonico **0/8 violazioni**, +**floor=−1 viola 1/8** (2023, 0,80); maxDD full 14,31% → 20,61%. + +**E non e' morte-per-fee:** a **fee ZERO** 1,338 contro **0,904**. L'edge lordo non c'e'; il +turnover raddoppiato e' un'aggravante, non la causa. + +**Libro 75/25:** ShFULL 1,814 → 1,347, maxDD 9,42% → 14,44%, banda **dSh −0,356 in 0/24**. +**Ridondanza con SKH01: no, e non aiuta** — la gamba short isolata ha corr **+0,10** con SKH01 ma +Sharpe **−0,335**: non e' ridondante, e' perdente. + +**Eseguibilita' a $635:** haircut **±0,002** (**5ª volta nell'ondata** che l'eseguibilita' non e' il +vincolo). 📌 **Trovato per strada:** `src/live/book.py` contiene `max(tp_frac, 0.0)` → **il +long-flat e' cablato anche nell'ESECUTORE**, non solo in `CANONICAL`. + +**DSR 0,997/0,988/0,962** con sr0 nullo 0,208 su N=4 — **e non e' il gate che decide**: con 4 celle +quasi identiche sr0 e' piccolo per costruzione (è la lezione §10 del 22/08 vista dal lato buono). + +⚠️ **Errori catturati dall'agente su se stesso:** (i) il trial set del DSR era costruito **a mano** +invece che preso da `select_cell_insample` — due percorsi per lo stesso numero e' esattamente cio' +che il gate codificato evita; ora sono confrontati e coincidono; (ii) un `:+.2f` su una frazione +stampava **+0,09pp** invece di **+8,80pp**. + +**Cosa NON e' misurato, dichiarato:** il **funding** (nessun backtest del progetto lo modella, §35) +— e' l'unica voce che potrebbe spingere verso la short, ma l'agente ne da' un **argomento, non una +misura** (ordine ~1-2%/anno contro 1,91% di drift perso): si chiude con `r0822d_funding.py`. +Inoltre: nessun TF diverso dal 1d (riaprirlo riapre la **famiglia**), banda del libro con SKH01 alla +sola ancora canonica, tutto **close-only**. + +**VERDETTO: `LA SHORT DEL TREND A 1d NON PAGA IL PROPRIO COSTO SU NESSUN ASSE CHE DECIDE`** — +SCARTATO. **Libro, pesi, cron, config INVARIATI.** From 96d37c7013e06e4e00e5fde40d1589ee5f888153 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 01:02:07 +0000 Subject: [PATCH 063/114] =?UTF-8?q?research(tp01):=20sizing=20conviction-a?= =?UTF-8?q?ware=20SCARTATO=20=E2=80=94=20l'ottimo=20del=20knob=20sta=20ai?= =?UTF-8?q?=20due=20estremi=20opposti=20nelle=20due=20meta'=20del=20campio?= =?UTF-8?q?ne?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §41 TP01-SIZE. Domanda: esiste una size che risponde alla CONVINZIONE (invece che solo alla vol realizzata) e che, A ISO-VOL, alzi il drift di TP01? Sarebbe un miglioramento k-indipendente sul 75% del libro live. Risposta: NO. Griglia dichiarata PRIMA di guardare: 60 celle (rho = g(1/3)/g(1) x q = esponente del vol-targeting), TF 1d, 24 ancore, segnale CONGELATO. Replica bit-exact del canonico max|diff| = 0.0 su target_series E sui rendimenti di sleeve. - FATTO STRUTTURALE che corregge la premessa: la convinzione NON vale {1/3,2/3,1}. La media di 3 segni sta in {-1,-1/3,0,+1/3,+1} -> il bucket 2/3 e' ARITMETICAMENTE IMPOSSIBILE. La famiglia ha UN grado di liberta' (rho), non tre: potenza/floor/ soglia sono la stessa cella riparametrizzata. - NULL DEL RE-LEVERING misurato in forma pura: togliere il vol-targeting (q=0) porta il CAGR da 16,32% a 46,75% (+186%) e a ISO-VOL fa -2,40pp. Era TUTTA leva. - Il meglio dell'intera griglia e' +0,30pp di CAGR iso-vol. La cella scelta AL BUIO (rho=0,25 q=1,25) peggiora l'hold-out del LIBRO in 0/24 ancore e il suo maxDD in 24/24, contro +0,19pp di CAGR. - Spearman(ShIS, ShHOLD) = -0,527 sulle 60 celle: qui scegliere in-sample e' PEGGIO di una moneta. Meccanismo misurato: Spearman(rho, ShHOLD) = +1,000 a TUTTI e sei i q (monotono), mentre in-sample e' a gobba -> la convinzione e' informativa in-sample e ANTI-informativa in hold-out. Nessuna cella e' proponibile. - L'unica con guadagno hold-out robusto e' il BINARIO rho=1 (dShHOLD +0,243, 24/24), che perde FULL e drift iso-vol e si potrebbe scegliere solo guardando l'hold-out. - deflated-Sharpe 0,999 PASS ma QUASI VACUO (famiglia omogenea, sr0 0,197): sensibilita' pubblicata, FALLISCE a sr0 >= 0,9. Conferma §10 dell'ondata 22/08. - Controlli positivi 3/3 (de-levering puro, oracolo look-ahead con causality_ok=False, anti-controllo rho=3). Eseguibilita' a $635: haircut 0,001 -> non e' il vincolo. - Sottoprodotti: banda d'ancora del canonico ricalcolata su dati odierni (hold-out canonico 0,441 = 96° pctl, mediana onesta 0,219) e altlib.tp01_baseline_daily NON e' bit-exact col sleeve (1 ulp su 68,5% dei giorni: _to_daily fa (1+r).prod()-1). Book, pesi, cron, config INVARIATI. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf --- scripts/research/r0823_tp01_size.py | 762 ++++++++++++++++++++++++++++ 1 file changed, 762 insertions(+) create mode 100644 scripts/research/r0823_tp01_size.py diff --git a/scripts/research/r0823_tp01_size.py b/scripts/research/r0823_tp01_size.py new file mode 100644 index 0000000..8dc0a99 --- /dev/null +++ b/scripts/research/r0823_tp01_size.py @@ -0,0 +1,762 @@ +#!/usr/bin/env python +"""r0823_tp01_size.py — §41 TP01-SIZE: una size che risponde alla CONVINZIONE paga? + +DOMANDA +------- +TP01 e' il 75% del libro live. Dichiara target_vol=20% e ne realizza 12,14%; la +scomposizione del 22/08 e' `20% x 0,682 x 0,890`, dove lo 0,682 sono due meta' quasi +uguali — (i) flat nel 44,3% delle barre, (ii) "convinzione parziale". Il fatto +trasferibile: alzare `target_vol` scala TUTTO uniformemente, quindi aumenta la size +proprio dove il segnale e' piu' debole. Nessuno ha mai misurato l'alternativa: +**una size che risponde alla convinzione invece che solo alla vol realizzata**. +Se esistesse, sarebbe un miglioramento k-INDIPENDENTE (ridistribuzione della stessa +size), non un aumento di leva. + +ATTESA A PRIORI (scritta PRIMA di qualunque misura, e prima del diagnostico §2/§3) +--------------------------------------------------------------------------------- +"Il canonico (size lineare nella convinzione) e' gia' dentro il suo plateau. Qualunque +rimodellamento dara' un delta indistinguibile da zero A ISO-VOL, e un eventuale +vincitore sara' (i) fortuna d'ancora, (ii) un anno solo, o (iii) bocciato dal +deflated-Sharpe. VERDETTO ATTESO: SCARTATO." +Corollario dichiarato: mi aspetto che il null del de-levering si presenti qui nella sua +forma SPECULARE (re-levering): una variante che alza la size media sembrera' guadagnare +drift, e a iso-vol il guadagno sparira'. +NB di trasparenza: il diagnostico per bucket (§3) e' stato girato PRIMA della griglia e +ne ha raffinato la DIREZIONE attesa (rho > 1/3). La FAMIGLIA era gia' dichiarata. + +FAMIGLIA DICHIARATA PRIMA DI GUARDARE — e perche' e' piu' piccola di quanto sembra +---------------------------------------------------------------------------------- +Il segnale resta CONGELATO (TSMOM sign-blend 30/90/180g, long-flat). Cambia solo la +mappa `convinzione -> size`: + target = clip( g_rho(conv) * (target_vol/vol)^q , -leverage, +leverage ) + * asse RHO = g(1/3)/g(1): quanto pesa il bucket debole rispetto al pieno. + rho in {0.00, 0.15, 0.25, 1/3, 0.45, 0.60, 0.80, 1.00, 1.50, 3.00} (10) + rho=1/3 = CANONICO; rho=0 = soglia (solo convinzione piena); rho=1 = binario + (size costante quando non-flat, cioe' convinzione RIMOSSA); rho=3 = controllo + speculare (sovrappesa il debole). + * asse Q = esponente del vol-targeting: quanto la size risponde alla VOL invece che + alla convinzione. q in {0.00, 0.25, 0.50, 0.75, 1.00, 1.25} (6) + q=1 = canonico; q=0 = nessun vol-targeting. + => GRIGLIA DICHIARATA = 10 x 6 = **60 celle**, TF=1d (il TF di deploy). Conteggio al + RIALZO: la sensibilita' del verdetto al numero di trial e' pubblicata in §7. + +FATTO STRUTTURALE (§2) — e una correzione alla premessa del mandato +------------------------------------------------------------------- +La convinzione di TP01 NON assume i valori {1/3, 2/3, 1}: la media di TRE segni in +{-1,0,+1} vale {-1, -1/3, 0, +1/3, +1}, quindi **il bucket 2/3 e' aritmeticamente +impossibile** e non esiste nei dati. Conseguenza metodologica: le tre +parametrizzazioni "ovvie" (potenza c^p, floor affine a+(1-a)c, soglia) sono tutte +REPARAMETRIZZAZIONI dello stesso unico knob rho — la famiglia della convinzione ha UN +grado di liberta', non tre. Dichiararle come tre assi avrebbe gonfiato i trial con +duplicati esatti (l'errore speculare a quello del 30/07: li' i trial erano contati al +ribasso, qui si sarebbero contati al rialzo ma con celle identiche). + +GATE ISO-VOLATILITA' (obbligatorio, e qui e' ESATTO) +---------------------------------------------------- +Ogni variante e' confrontata a PARI vol realizzata di sleeve riscalando con un k +costante. Poiche' net = pos*r - fee*|dpos| e' OMOGENEO di grado 1 in pos, `net_iso = k*net` +e' ESATTO => lo **Sharpe e' invariante a k** e il gate morde sul **drift**: qualunque +guadagno di CAGR che sparisce a iso-vol e' re-levering, non edge. k e' calibrato IN-SAMPLE +(pre-2025) per la lettura hold-out: nessun look-ahead nemmeno nella normalizzazione. +Il cap di leva 2x e' misurato cella per cella (§4): morde su <=0.15% delle barre nelle 54 +celle candidate e solo nel controllo anti rho=3 (24.7%), quindi non e' il cap a decidere. + +CONTROLLI POSITIVI (un gate che non segnala nulla puo' essere rotto) +-------------------------------------------------------------------- + CTRL-DELEV : canonico x 0.5 (pura riscalatura) -> dSharpe ESATTAMENTE 0, + dCAGR grezzo NEGATIVO, dCAGR a iso-vol ESATTAMENTE 0. + CTRL-ORACLE : convinzione = segno del rendimento del giorno DOPO (look-ahead) -> + Sharpe deve esplodere E `causality_ok` deve dare ok=False. + CTRL-ANTI : rho=3.0 (sovrappesa il bucket debole) -> deve perdere. + +CAUSALITA' / ONESTA' +-------------------- +`causality_ok` sulla cella scelta; selezione della cella IN-SAMPLE-ONLY +(`select_cell_insample`, importata); `deflated_sharpe`; `marginal_vs_tp01`; +banda d'ancora a 24 offset con **mediana delle DIFFERENZE APPAIATE** e frazione di +ancore positive; scomposizione per ANNO; impatto sul LIBRO (TP01 75 / SKH01 25); +eseguibilita' a $635 con `eval_weights_smallcap` (min_order $5). + +Vincoli operativi: nessun file toccato fuori da questo script; nessuna rete (solo i +parquet certificati su disco); nessun ordine. Runtime misurato ~9 s su 2 core. +""" +from __future__ import annotations + +import sys +import time +from functools import lru_cache +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +sys.path.insert(0, str(ROOT)) + +import altlib as al # noqa: E402 +from src.data.downloader import load_data # noqa: E402 +from src.strategies.trend_portfolio import ( # noqa: E402 + CANONICAL, TrendPortfolio, realized_vol, resample_1d, simple_returns, tsmom_blend) + +ASSETS = ("BTC", "ETH") +HOLDOUT = al.HOLDOUT # 2025-01-01 UTC +TP = TrendPortfolio(**CANONICAL) +FEE = TP.fee_side # 0.05%/lato = 0.10% RT +TV = TP.target_vol # 0.20 +LEV = TP.leverage # 2.0 +HORIZONS = TP.horizons_days # (30, 90, 180) +VOLWIN = TP.vol_win_days # 30 +OFFSETS = tuple(range(24)) # le 24 ancore orarie del ribilanciamento daily + +RHOS = (0.00, 0.15, 0.25, 1.0 / 3.0, 0.45, 0.60, 0.80, 1.00, 1.50, 3.00) +QS = (0.00, 0.25, 0.50, 0.75, 1.00, 1.25) +CANON_CELL = (1.0 / 3.0, 1.00) +N_CELLS = len(RHOS) * len(QS) + +SEP = "=" * 100 + + +# --------------------------------------------------------------------------- +# 0. Resample ancorato (identico a trend_portfolio.resample_tf, con offset h) +# --------------------------------------------------------------------------- +def resample_offset(df_1h: pd.DataFrame, h: int) -> pd.DataFrame: + g = df_1h.copy() + idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True) + idx.name = "dt" + g.index = idx + out = g.resample("24h", offset=pd.Timedelta(hours=h), label="left", closed="left").agg( + {"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}) + out = out.dropna(subset=["open"]) + out["datetime"] = out.index + epoch = pd.Timestamp("1970-01-01", tz="UTC") + out["timestamp"] = ((out.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64") + return out.reset_index(drop=True)[ + ["timestamp", "open", "high", "low", "close", "volume", "datetime"]] + + +@lru_cache(maxsize=4) +def _raw1h(asset: str) -> pd.DataFrame: + return load_data(asset, "1h") + + +@lru_cache(maxsize=64) +def _daily(asset: str, h: int) -> pd.DataFrame: + return resample_offset(_raw1h(asset), h) + + +@lru_cache(maxsize=64) +def _feat(asset: str, h: int) -> dict: + """Feature CAUSALI per (asset, ancora): rendimenti, vol realizzata, convinzione.""" + df = _daily(asset, h) + c = df["close"].values.astype(float) + dt = pd.to_datetime(df["datetime"]).diff().dt.total_seconds().median() + bpd = max(1, round(86400 / dt)) # =1 su barre daily + bpy = bpd * 365.25 + r = simple_returns(c) + vol = realized_vol(r, VOLWIN * bpd, bpy) + conv = np.clip(tsmom_blend(c, tuple(d * bpd for d in HORIZONS)), 0, None) + return dict(idx=pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)), + r=r, vol=vol, conv=conv, close=c, n=len(c)) + + +# --------------------------------------------------------------------------- +# 1. La mappa convinzione -> size (la SOLA cosa che cambia) +# --------------------------------------------------------------------------- +def g_rho(conv: np.ndarray, rho: float) -> np.ndarray: + """Peso della convinzione: g(1)=1, g(1/3)=rho, g(<=0)=0. + Interpolazione lineare fra i due livelli (esatta sul supporto reale {0,1/3,1}); + definita per continuita' anche fuori, cosi' la funzione non e' legata al supporto.""" + out = rho + (1.0 - rho) * (conv - 1.0 / 3.0) / (1.0 - 1.0 / 3.0) + return np.where(conv > 1e-12, np.clip(out, 0.0, None), 0.0) + + +def target_from_feat(f: dict, rho: float, q: float) -> np.ndarray: + vol = f["vol"] + ok = (vol > 0) & np.isfinite(vol) + scal = np.zeros(len(vol)) + scal[ok] = (TV / vol[ok]) ** q + tgt = np.clip(g_rho(f["conv"], rho) * scal, -LEV, LEV) + tgt[~np.isfinite(tgt)] = 0.0 + return tgt + + +def make_target_fn(rho: float, q: float): + """target_fn(df) autonomo (per altlib: causality_ok / study_family_honest).""" + def fn(df: pd.DataFrame) -> np.ndarray: + c = df["close"].values.astype(float) + dt = pd.to_datetime(df["datetime"]).diff().dt.total_seconds().median() + bpd = max(1, round(86400 / dt)) + bpy = bpd * 365.25 + r = simple_returns(c) + vol = realized_vol(r, VOLWIN * bpd, bpy) + conv = np.clip(tsmom_blend(c, tuple(d * bpd for d in HORIZONS)), 0, None) + return target_from_feat(dict(vol=vol, conv=conv), rho, q) + return fn + + +def net_from_target(f: dict, tgt: np.ndarray) -> np.ndarray: + """Rendimenti netti per barra. La posizione decisa a close[i] e' TENUTA in i+1.""" + pos = np.zeros(len(tgt)) + pos[1:] = tgt[:-1] + turn = np.abs(np.diff(pos, prepend=0.0)) + net = pos * f["r"] - FEE * turn + net[0] = 0.0 + return net + + +def sleeve_series(rho: float, q: float, h: int) -> tuple[pd.Series, dict]: + """Serie 50/50 BTC+ETH del sleeve all'ancora h, piu' diagnostica di cap/size.""" + parts, diag = {}, {} + for a in ASSETS: + f = _feat(a, h) + tgt = target_from_feat(f, rho, q) + parts[a] = pd.Series(np.clip(net_from_target(f, tgt), -0.99, None), index=f["idx"]) + diag[a] = dict(cap_frac=float(np.mean(np.abs(tgt) >= LEV - 1e-12)), + max_tgt=float(np.max(np.abs(tgt))), + mean_tgt=float(np.mean(tgt))) + J = pd.concat(parts, axis=1, join="inner").fillna(0.0) + s = pd.Series(0.5 * J[ASSETS[0]].values + 0.5 * J[ASSETS[1]].values, index=J.index) + return s, diag + + +# --------------------------------------------------------------------------- +# 2. Metriche (convenzione identica al progetto) +# --------------------------------------------------------------------------- +def sh(s) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + return float(np.mean(r) / np.std(r) * np.sqrt(365.25)) if len(r) > 2 and np.std(r) > 0 else 0.0 + + +def vol_ann(s) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + return float(np.std(r) * np.sqrt(365.25)) + + +def cagr(s) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + if len(r) < 2: + return 0.0 + eq = np.cumprod(1.0 + np.clip(r, -0.99, None)) + yrs = len(r) / 365.25 + return float(eq[-1] ** (1.0 / yrs) - 1.0) + + +def maxdd(s) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + eq = np.cumprod(1.0 + np.clip(r, -0.99, None)) + pk = np.maximum.accumulate(eq) + return float(np.max((pk - eq) / pk)) if len(eq) else 0.0 + + +def split(s: pd.Series) -> tuple[pd.Series, pd.Series]: + return s[s.index < HOLDOUT], s[s.index >= HOLDOUT] + + +# --------------------------------------------------------------------------- +def main() -> None: + t_start = time.time() + print(SEP) + print("§41 TP01-SIZE — size conviction-aware vs canonico. Griglia dichiarata: " + f"{len(RHOS)} rho x {len(QS)} q = {N_CELLS} celle, TF=1d, 24 ancore.") + print(SEP) + + # ===================================================================== + # §1 REPLICA BIT-EXACT (prima di ogni delta) + # ===================================================================== + print("\n### §1 REPLICA BIT-EXACT DEL CANONICO") + import src.portfolio.sleeves as SL + prod = SL._tp01_returns() + + # (a) livello TARGET: la mia cella canonica == TrendPortfolio.target_series + worst_tgt = 0.0 + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + mine = make_target_fn(*CANON_CELL)(df) + worst_tgt = max(worst_tgt, float(np.max(np.abs(mine - TP.target_series(df))))) + print(f" (a) target_series max|diff| = {worst_tgt:.17g}") + + # (b) livello RENDIMENTI: la mia pipeline all'ancora 0 == sleeves._tp01_returns() + can0, diag0 = sleeve_series(*CANON_CELL, 0) + aligned = can0.reindex(prod.index) + worst_ret = float(np.max(np.abs(aligned.values - prod.values))) + print(f" (b) sleeve returns max|diff| = {worst_ret:.17g} (n={len(prod)}, " + f"{prod.index[0].date()} -> {prod.index[-1].date()})") + + # (c) contro il baseline indipendente di altlib — e la sua NON bit-esattezza, spiegata + base = al.tp01_baseline_daily() + j = pd.concat({"m": can0, "b": base}, axis=1, join="inner").dropna() + worst_al = float(np.max(np.abs(j["m"].values - j["b"].values))) + n_diff = int((j["m"].values != j["b"].values).sum()) + roundtrip = (1.0 + j["m"].values) - 1.0 + is_rt = bool(np.all(roundtrip == j["b"].values)) + print(f" (c) altlib baseline max|diff| = {worst_al:.17g} " + f"(diverso su {n_diff}/{len(j)} giorni = {n_diff/len(j)*100:.1f}%)") + print(f" MECCANISMO PROVATO: altlib._to_daily fa `(1+r).resample('1D').prod()-1`, che su una") + print(f" serie GIA' giornaliera e' un no-op algebrico ma NON in virgola mobile. Identita'") + print(f" `(1+r)-1 == baseline` esatta su tutti i giorni: {is_rt} -> e' 1 ulp, non un difetto.") + ok_replica = (max(worst_tgt, worst_ret) == 0.0) and is_rt and worst_al < 1e-15 + print(f" => REPLICA {'BIT-EXACT OK sui due percorsi di produzione (a,b); (c) a 1 ulp, spiegato' if ok_replica else 'FALLITA — mi fermo qui'}") + if not ok_replica: + print(" La replica non e' bit-exact: e' questo il risultato. Nessun delta e' credibile.") + return + print(f" canonico ancora 0: Sharpe FULL {sh(can0):.4f} | HOLD {sh(split(can0)[1]):.4f} | " + f"vol {vol_ann(can0)*100:.2f}% | CAGR {cagr(can0)*100:.2f}% | maxDD {maxdd(can0)*100:.2f}%") + + # ===================================================================== + # §2 FATTO STRUTTURALE: il supporto della convinzione + # ===================================================================== + print("\n### §2 SUPPORTO DELLA CONVINZIONE (correzione alla premessa del mandato)") + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + raw = tsmom_blend(df["close"].values.astype(float), HORIZONS) + u, cnt = np.unique(np.round(raw, 9), return_counts=True) + print(f" {a} direzione GREZZA: " + + " ".join(f"{v:+.4f}:{c}" for v, c in zip(u.tolist(), cnt.tolist()))) + f = _feat(a, 0) + uu, cc = np.unique(np.round(f["conv"], 9), return_counts=True) + print(f" {a} convinzione (clip>=0): " + + " ".join(f"{v:.4f}:{c/len(f['conv'])*100:.1f}%" for v, c in zip(uu.tolist(), cc.tolist()))) + print(" => il bucket 2/3 NON esiste (media di 3 segni in {-1,0,+1} => {-1,-1/3,0,+1/3,+1}).") + print(" La famiglia della convinzione ha UN grado di liberta' (rho), non tre.") + + # ===================================================================== + # §3 DIAGNOSTICO: la convinzione e' informativa? (descrittivo, non selezione) + # ===================================================================== + print("\n### §3 DIAGNOSTICO PER BUCKET (rendimento del giorno TENUTO dopo il segnale)") + print(f" {'asset':5s} {'fin.':5s} {'bucket':7s} {'n':>5s} {'mean(bps)':>10s} {'sd(%)':>7s} " + f"{'Sharpe':>7s} peso canonico") + for a in ASSETS: + f = _feat(a, 0) + fwd = np.zeros(f["n"]) + fwd[:-1] = f["r"][1:] + for lab, m0 in (("FULL", np.ones(f["n"], bool)), + ("IS", np.asarray(f["idx"] < HOLDOUT)), + ("HOLD", np.asarray(f["idx"] >= HOLDOUT))): + for lvl, nm in ((1.0 / 3.0, "debole"), (1.0, "pieno")): + m = m0 & (np.abs(f["conv"] - lvl) < 1e-9) + m[-1] = False + x = fwd[m] + if len(x) < 10: + continue + print(f" {a:5s} {lab:5s} {nm:7s} {len(x):5d} {np.mean(x)*1e4:10.1f} " + f"{np.std(x)*100:7.2f} {np.mean(x)/np.std(x)*np.sqrt(365.25):7.2f}" + f" x{lvl:.3f}") + print(" (descrittivo: dice se il canonico sotto/sovra-pesa il bucket debole. NON e' selezione.)") + + # ===================================================================== + # §4 GRIGLIA 60 CELLE ALL'ANCORA CANONICA + GATE ISO-VOL + # ===================================================================== + print("\n### §4 GRIGLIA 60 CELLE (ancora 0) — grezzo vs ISO-VOL") + can_is, _ = split(can0) + vol_is_ref = vol_ann(can_is) # vol in-sample del canonico = bersaglio + rows = [] + cap_worst = 0.0 + for rho in RHOS: + for q in QS: + s, dg = sleeve_series(rho, q, 0) + cap_worst = max(cap_worst, max(d["cap_frac"] for d in dg.values())) + s_is, s_ho = split(s) + v_is = vol_ann(s_is) + k = vol_is_ref / v_is if v_is > 0 else 0.0 # k CALIBRATO IN-SAMPLE (no look-ahead) + iso = k * s + rows.append(dict(rho=rho, q=q, + sh_full=sh(s), sh_is=sh(s_is), sh_hold=sh(s_ho), + vol=vol_ann(s), vol_is=v_is, k=k, + cagr_raw=cagr(s), cagr_iso=cagr(iso), + dd_raw=maxdd(s), dd_iso=maxdd(iso), + sh_iso=sh(iso), cap_max=max(d["cap_frac"] for d in dg.values()), + max_tgt=max(d["max_tgt"] for d in dg.values()), + mean_tgt=float(np.mean([d["mean_tgt"] for d in dg.values()])))) + G = pd.DataFrame(rows) + canrow = G[(np.abs(G.rho - CANON_CELL[0]) < 1e-9) & (np.abs(G.q - CANON_CELL[1]) < 1e-9)].iloc[0] + cap_reg = G[G.rho <= 1.5].cap_max.max() + cap_anti = G[G.rho > 1.5].cap_max.max() + print(f" cap di leva ({LEV}x): frazione max di barre al cap = {cap_reg:.6f} sulle celle rho<=1.5, " + f"{cap_anti:.6f} sul controllo rho=3.0") + print(" -> il cap morde SOLO nel controllo anti (g(1/3)=3 con q basso supera 2x); sulle 54") + print(" celle candidate tocca al massimo lo 0.15% delle barre = irrilevante.") + inv = float(np.max(np.abs(G.sh_full - G.sh_iso))) + print(f" invarianza di scala dello Sharpe: max|Sh(grezzo) - Sh(iso-vol)| = {inv:.2e} (esatta per") + print(" costruzione: net_iso = k*net, e net e' omogeneo di grado 1 nella posizione)") + print(f" -> il gate ISO-VOL non puo' mordere sullo Sharpe: morde sul DRIFT.") + # integrita' del gate: la vol FULL della versione iso combacia davvero col canonico? + G["vol_iso_full"] = G.vol * G.k + mism = float(np.max(np.abs(G.vol_iso_full - canrow.vol * canrow.k))) + print(f" integrita' iso-vol: k e' calibrato IN-SAMPLE, quindi la vol FULL non e' identica per") + print(f" costruzione. Scarto massimo di vol FULL fra le 60 celle iso e il canonico: " + f"{mism*100:.3f}pp su {canrow.vol*100:.2f}%") + RL = G[(np.abs(G.rho - CANON_CELL[0]) < 1e-9) & (np.abs(G.q - 0.0) < 1e-9)].iloc[0] + print(f"\n >>> NULL DEL RE-LEVERING, MISURATO: togliere il vol-targeting (q=0, stesso rho) fa") + print(f" CAGR {canrow.cagr_raw*100:.2f}% -> {RL.cagr_raw*100:.2f}% (+{(RL.cagr_raw-canrow.cagr_raw)*100:.2f}pp,") + print(f" cioe' +{(RL.cagr_raw/canrow.cagr_raw-1)*100:.0f}%) — e a ISO-VOL fa " + f"{(RL.cagr_iso-canrow.cagr_iso)*100:+.2f}pp. Era TUTTA leva.") + print(f"\n {'rho':>6s} {'q':>5s} {'ShFULL':>7s} {'ShIS':>6s} {'ShHOLD':>7s} {'vol%':>6s} " + f"{'k':>5s} {'CAGRraw%':>9s} {'CAGRiso%':>9s} {'dCAGRraw':>9s} {'dCAGRiso':>9s} {'DDiso%':>7s}") + for _, r in G.sort_values(["q", "rho"]).iterrows(): + star = " <== CANONICO" if (abs(r.rho - CANON_CELL[0]) < 1e-9 and abs(r.q - 1.0) < 1e-9) else "" + print(f" {r.rho:6.3f} {r.q:5.2f} {r.sh_full:7.3f} {r.sh_is:6.3f} {r.sh_hold:7.3f} " + f"{r.vol*100:6.2f} {r.k:5.2f} {r.cagr_raw*100:9.2f} {r.cagr_iso*100:9.2f} " + f"{(r.cagr_raw-canrow.cagr_raw)*100:+9.2f} {(r.cagr_iso-canrow.cagr_iso)*100:+9.2f} " + f"{r.dd_iso*100:7.2f}{star}") + + # ===================================================================== + # §5 SELEZIONE IN-SAMPLE-ONLY (nessuna sbirciata all'hold-out) + # ===================================================================== + print("\n### §5 SELEZIONE DELLA CELLA — IN-SAMPLE-ONLY") + Gs = G.sort_values("sh_is", ascending=False).reset_index(drop=True) + ch = Gs.iloc[0] + print(f" cella scelta al buio (max Sharpe IS, ancora 0): rho={ch.rho:.3f} q={ch.q:.2f} " + f"(ShIS {ch.sh_is:.3f} vs canonico {canrow.sh_is:.3f})") + print(" top-8 in-sample:") + for i in range(min(8, len(Gs))): + r = Gs.iloc[i] + print(f" {i+1}. rho={r.rho:5.3f} q={r.q:4.2f} ShIS {r.sh_is:6.3f} " + f"ShFULL {r.sh_full:6.3f} ShHOLD {r.sh_hold:6.3f} dCAGRiso {(r.cagr_iso-canrow.cagr_iso)*100:+.2f}pp") + from scipy.stats import spearmanr as _spear + rho_s, p_s = _spear(G.sh_is.values, G.sh_hold.values) + rho_f, _ = _spear(G.sh_is.values, G.sh_full.values) + print(f"\n la selezione in-sample ha contenuto? Spearman(ShIS, ShHOLD) su 60 celle = " + f"{rho_s:+.3f} (p={p_s:.3f}); Spearman(ShIS, ShFULL) = {rho_f:+.3f}") + print(" (il secondo e' alto per costruzione: l'in-sample e' il 78% del campione FULL.)") + print(" MECCANISMO del segno negativo — come si muove lo Sharpe lungo rho, per ogni q:") + print(f" {'q':>5s} {'Spearman(rho,ShIS)':>20s} {'Spearman(rho,ShHOLD)':>22s}") + for q in QS: + sub = G[np.abs(G.q - q) < 1e-9].sort_values("rho") + a, _ = _spear(sub.rho.values, sub.sh_is.values) + b, _ = _spear(sub.rho.values, sub.sh_hold.values) + print(f" {q:5.2f} {a:20.3f} {b:22.3f}") + print(" -> in-sample lo Sharpe e' a GOBBA in rho (ottimo interno ~0.45-0.60); in hold-out e'") + print(" MONOTONO CRESCENTE (l'ottimo e' al bordo rho>=1: il bucket debole rende quanto il") + print(" pieno). La convinzione e' informativa IN-SAMPLE e ANTI-informativa in HOLD-OUT:") + print(" e' questo che rende la rank-correlation negativa, non il rumore.") + rank_can_is = int((G.sh_is > canrow.sh_is).sum()) + 1 + rank_can_full = int((G.sh_full > canrow.sh_full).sum()) + 1 + rank_can_hold = int((G.sh_hold > canrow.sh_hold).sum()) + 1 + print(f" rango del CANONICO nella griglia: {rank_can_is}/{N_CELLS} in-sample, " + f"{rank_can_full}/{N_CELLS} full, {rank_can_hold}/{N_CELLS} hold-out") + + # selezione DE-LUCKATA: max mediana su 24 ancore dello Sharpe in-sample + print("\n selezione alternativa DE-LUCKATA (max MEDIANA su 24 ancore dello Sharpe IS):") + med_is = {} + for rho in RHOS: + for q in QS: + vals = [sh(split(sleeve_series(rho, q, h)[0])[0]) for h in OFFSETS] + med_is[(rho, q)] = float(np.median(vals)) + best_dl = max(med_is, key=med_is.get) + print(f" -> rho={best_dl[0]:.3f} q={best_dl[1]:.2f} (mediana ShIS {med_is[best_dl]:.3f}; " + f"canonico {med_is[CANON_CELL]:.3f})") + same = (abs(best_dl[0] - ch.rho) < 1e-9 and abs(best_dl[1] - ch.q) < 1e-9) + print(f" coincide con la scelta all'ancora 0? {'SI' if same else 'NO'}") + + CH = (float(ch.rho), float(ch.q)) + + # identita' con l'implementazione di altlib (importata, non riscritta) + print("\n controllo d'identita' con altlib.select_cell_insample (60 celle):") + grid = [dict(rho=rho, q=q) for rho in RHOS for q in QS] + + def factory(tf="1d", rho=1.0 / 3.0, q=1.0): + return make_target_fn(rho, q) + + sel = al.select_cell_insample(factory, grid, ("1d",)) + alp = sel["chosen"]["params"] + print(f" altlib sceglie rho={alp['rho']:.3f} q={alp['q']:.2f} " + f"(ShIS {sel['chosen']['insample_sharpe']:.3f}) " + f"-> {'IDENTICA' if (abs(alp['rho']-CH[0])<1e-9 and abs(alp['q']-CH[1])<1e-9) else 'DIVERSA'}") + + # ===================================================================== + # §6 CONTROLLI POSITIVI + # ===================================================================== + print("\n### §6 CONTROLLI POSITIVI (un gate che non segnala nulla puo' essere rotto)") + # CTRL-DELEV: pura riscalatura x0.5 + parts = {} + for a in ASSETS: + f = _feat(a, 0) + tgt = 0.5 * target_from_feat(f, *CANON_CELL) + parts[a] = pd.Series(np.clip(net_from_target(f, tgt), -0.99, None), index=f["idx"]) + Jd = pd.concat(parts, axis=1, join="inner").fillna(0.0) + delev = pd.Series(0.5 * Jd[ASSETS[0]].values + 0.5 * Jd[ASSETS[1]].values, index=Jd.index) + kd = vol_ann(split(can0)[0]) / vol_ann(split(delev)[0]) + print(f" CTRL-DELEV (canonico x0.5): dSharpe {sh(delev)-sh(can0):+.2e} (atteso 0) | " + f"dCAGR grezzo {(cagr(delev)-cagr(can0))*100:+.2f}pp (atteso <0) | " + f"dCAGR ISO-VOL {(cagr(kd*delev)-cagr(can0))*100:+.2e}pp (atteso 0)") + ctrl_delev_ok = (abs(sh(delev) - sh(can0)) < 1e-9 and (cagr(delev) - cagr(can0)) < 0 + and abs(cagr(kd * delev) - cagr(can0)) < 1e-9) + print(f" -> {'PASS' if ctrl_delev_ok else 'FAIL'} (il gate iso-vol neutralizza la pura scala)") + + # CTRL-ORACLE: convinzione = segno del rendimento del giorno DOPO + def oracle_fn(df): + c = df["close"].values.astype(float) + r = simple_returns(c) + fwd = np.zeros(len(r)); fwd[:-1] = r[1:] + vol = realized_vol(r, VOLWIN, 365.25) + scal = np.where((vol > 0) & np.isfinite(vol), TV / vol, 0.0) + t = np.clip(np.clip(np.sign(fwd), 0, None) * scal, -LEV, LEV) + t[~np.isfinite(t)] = 0.0 + return t + parts = {} + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + f = _feat(a, 0) + parts[a] = pd.Series(np.clip(net_from_target(f, oracle_fn(df)), -0.99, None), index=f["idx"]) + Jo = pd.concat(parts, axis=1, join="inner").fillna(0.0) + orac = pd.Series(0.5 * Jo[ASSETS[0]].values + 0.5 * Jo[ASSETS[1]].values, index=Jo.index) + caus_or = al.causality_ok(oracle_fn, tf="1d") + print(f" CTRL-ORACLE (look-ahead): Sharpe {sh(orac):.2f} (canonico {sh(can0):.2f}) | " + f"causality_ok = {caus_or['ok']} (max_tail_diff {caus_or['max_tail_diff']})") + ctrl_orac_ok = (sh(orac) > 3 * sh(can0)) and (not caus_or["ok"]) + print(f" -> {'PASS' if ctrl_orac_ok else 'FAIL'} (la macchina VEDE un miglioramento vero; " + "la guardia di causalita' ha potenza)") + + # CTRL-ANTI: rho=3.0 (sovrappesa il debole) + anti = G[(np.abs(G.rho - 3.00) < 1e-9) & (np.abs(G.q - 1.00) < 1e-9)].iloc[0] + print(f" CTRL-ANTI (rho=3.0, q=1): ShFULL {anti.sh_full:.3f} vs canonico {canrow.sh_full:.3f} | " + f"dCAGRiso {(anti.cagr_iso-canrow.cagr_iso)*100:+.2f}pp") + ctrl_anti_ok = anti.sh_full < canrow.sh_full + print(f" -> {'PASS' if ctrl_anti_ok else 'FAIL'} (sovrappesare il bucket debole deve perdere)") + + # causalita' della cella scelta + caus = al.causality_ok(make_target_fn(*CH), tf="1d") + print(f" causality_ok cella scelta rho={CH[0]:.3f} q={CH[1]:.2f}: ok={caus['ok']} " + f"max_tail_diff={caus['max_tail_diff']} checked={caus['checked']}") + + # ===================================================================== + # §7 DEFLATED SHARPE + sensibilita' al conteggio dei trial + # ===================================================================== + print("\n### §7 DEFLATED SHARPE (60 trial dichiarati) + sensibilita'") + daily_ch = al.candidate_daily(make_target_fn(*CH), tf="1d") + all_sr = [float(x) for x in G.sh_full.values] + dsr, sr0 = al.deflated_sharpe(sh(daily_ch), all_sr, daily_ch) + print(f" Sharpe cella scelta {sh(daily_ch):.3f} | sr0 atteso dal null {sr0:.3f} | " + f"DSR {dsr:.4f} -> {'PASS' if dsr >= 0.95 else 'FAIL'} (soglia 0.95)") + print(f" dispersione dei 60 trial: sd(Sharpe) = {np.std(all_sr, ddof=1):.4f} annualizzato") + print(" ⚠ lezione §10 dell'ondata 22/08: il DSR dipende dalla VARIANZA della griglia, non da N." + "\n Su una famiglia OMOGENEA (60 riscalature dello stesso segnale) sr0 e' minuscolo") + print(" e il gate e' quasi VACUO. Sensibilita' pubblicata (sr0 imposto a mano):") + for sr0_forced in (sr0, 0.30, 0.60, 0.90, 1.20, 1.572): + r = np.asarray(daily_ch.dropna().values, float) + srd = sh(daily_ch) / np.sqrt(365.25); s0 = sr0_forced / np.sqrt(365.25) + skw = float(pd.Series(r).skew()); ku = float(pd.Series(r).kurt()) + 3.0 + den = np.sqrt(max(1e-9, 1 - skw * srd + (ku - 1) / 4.0 * srd ** 2)) + from scipy.stats import norm as _n + d2 = float(_n.cdf((srd - s0) * np.sqrt(len(r) - 1) / den)) + print(f" sr0={sr0_forced:6.3f} -> DSR {d2:.4f} {'PASS' if d2>=0.95 else 'FAIL'}") + print(" (1.572 = il massimo atteso dal puro rumore su uno screen a 168 trial, §8 22/08)") + + # ===================================================================== + # §8 BANDA D'ANCORA — mediana delle DIFFERENZE APPAIATE su 24 ancore + # ===================================================================== + print("\n### §8 BANDA D'ANCORA (24 ancore, differenze APPAIATE variante - canonico)") + cells_watch = [CH, best_dl, (1.00, 1.00), (0.00, 1.00), (1.0 / 3.0, 0.50)] + cells_watch = list(dict.fromkeys([tuple(np.round(c, 6)) for c in cells_watch])) + can_by_h, can_is_vol = {}, {} + for h in OFFSETS: + s, _ = sleeve_series(*CANON_CELL, h) + can_by_h[h] = s + can_is_vol[h] = vol_ann(split(s)[0]) + for lab, met in (("FULL", sh), ("HOLD-OUT", lambda x: sh(split(x)[1])), + ("maxDD", lambda x: -maxdd(x))): + bb = al.anchor_luck_band(lambda h: can_by_h[h], OFFSETS, canonical=0, metric=met) + v = -1 if lab == "maxDD" else 1 + lo, hi = sorted([v * bb["lo"], v * bb["hi"]]) + print(f" banda del CANONICO, {lab:9s}: canonica {v*bb['canonical']:+.4f} (pctl " + f"{bb['canonical_pctl']*100:3.0f}°) mediana ONESTA {v*bb['median']:+.4f} " + f"banda [{lo:+.4f}, {hi:+.4f}]") + print(" (l'hold-out canonico e' l'ancora migliore/quasi-migliore delle 24: replica indipendente,") + print(" su dati odierni, del finding d'ancora 02/07 e del suo ricontrollo 26/07.)") + print() + print(f" {'cella':>18s} {'dShFULL med':>12s} {'>0':>6s} {'dShHOLD med':>12s} {'>0':>6s} " + f"{'dCAGRiso med':>13s} {'>0':>6s} {'p10..p90 (ShFULL)':>22s}") + anchor_rep = {} + for cell in cells_watch: + dsf, dsh_, dci = [], [], [] + for h in OFFSETS: + s, _ = sleeve_series(cell[0], cell[1], h) + cb = can_by_h[h] + v_is = vol_ann(split(s)[0]) + k = can_is_vol[h] / v_is if v_is > 0 else 0.0 + dsf.append(sh(s) - sh(cb)) + dsh_.append(sh(split(s)[1]) - sh(split(cb)[1])) + dci.append(cagr(k * s) - cagr(cb)) + dsf, dsh_, dci = np.array(dsf), np.array(dsh_), np.array(dci) + anchor_rep[cell] = dict(dsf=dsf, dsh=dsh_, dci=dci) + print(f" rho={cell[0]:5.3f} q={cell[1]:4.2f} {np.median(dsf):12.4f} {int((dsf>0).sum()):3d}/24 " + f"{np.median(dsh_):12.4f} {int((dsh_>0).sum()):3d}/24 " + f"{np.median(dci)*100:12.3f}pp {int((dci>0).sum()):3d}/24 " + f" [{np.percentile(dsf,10):+.3f}, {np.percentile(dsf,90):+.3f}]") + A = anchor_rep[cells_watch[0]] + print(f"\n cella scelta: canonica (ancora 0) dShFULL {A['dsf'][0]:+.4f} = pctl " + f"{float((A['dsf'] < A['dsf'][0]).mean())*100:.0f}° della banda; " + f"stima ONESTA = mediana {np.median(A['dsf']):+.4f}") + # gate via altlib (importato) sulle differenze appaiate + ald = al.anchor_luck_delta(lambda h: sleeve_series(CH[0], CH[1], h)[0], + lambda h: can_by_h[h], OFFSETS, metric=sh) + print(f" altlib.anchor_luck_delta: mediana appaiata {ald['median_paired']:+.4f} " + f"positivi {ald['n_positive']}/{ald['n_anchors']} gate_pass={ald['gate_pass']}") + + # ===================================================================== + # §9 SCOMPOSIZIONE PER ANNO (mediana appaiata su 24 ancore) + # ===================================================================== + print("\n### §9 SCOMPOSIZIONE PER ANNO del delta (mediana appaiata su 24 ancore)") + per_year = {} + for h in OFFSETS: + s, _ = sleeve_series(CH[0], CH[1], h) + cb = can_by_h[h] + v_is = vol_ann(split(s)[0]) + k = can_is_vol[h] / v_is if v_is > 0 else 0.0 + si = k * s + for y in sorted(set(cb.index.year)): + a1, a2 = si[si.index.year == y], cb[cb.index.year == y] + if len(a2) < 60: + continue + per_year.setdefault(y, {"dsh": [], "dret": []}) + per_year[y]["dsh"].append(sh(a1) - sh(a2)) + per_year[y]["dret"].append(float(np.prod(1 + a1.values) - np.prod(1 + a2.values))) + print(f" {'anno':>6s} {'dSharpe med':>12s} {'>0':>6s} {'dRitorno med':>13s} {'>0':>6s}") + n_pos_years = 0 + for y, d in sorted(per_year.items()): + a, b = np.array(d["dsh"]), np.array(d["dret"]) + n_pos_years += int(np.median(a) > 0) + print(f" {y:6d} {np.median(a):12.3f} {int((a>0).sum()):3d}/24 " + f"{np.median(b)*100:12.2f}pp {int((b>0).sum()):3d}/24") + print(f" -> anni con mediana dSharpe > 0: {n_pos_years}/{len(per_year)}") + + # ===================================================================== + # §10 IMPATTO SUL LIBRO (TP01 75% / SKH01 25%) + # ===================================================================== + print("\n### §10 IMPATTO SUL LIBRO LIVE (TP01 75% / SKH01 25%)") + skh = SL._skyhook_returns() + + def book(tp_series: pd.Series) -> pd.Series: + t = ((1.0 + tp_series).resample("1D").prod() - 1.0).dropna() + J = pd.concat({"T": t, "S": skh}, axis=1, join="inner").fillna(0.0) + return pd.Series(0.75 * J["T"].values + 0.25 * J["S"].values, index=J.index) + + bc0 = book(can_by_h[0]) + print(f" libro canonico (ancora 0): Sharpe FULL {sh(bc0):.3f} | HOLD {sh(split(bc0)[1]):.3f} | " + f"CAGR {cagr(bc0)*100:.2f}% | maxDD {maxdd(bc0)*100:.2f}% (SKH01 alla SUA ancora canonica)") + book_res = {} + todo = [("SCELTA IS", CH)] + if tuple(np.round(best_dl, 12)) != tuple(np.round(CH, 12)): + todo.append(("DE-LUCKATA", best_dl)) + for tag, cell in todo: + d_sh, d_ho, d_cg, d_dd = [], [], [], [] + for h in OFFSETS: + s, _ = sleeve_series(cell[0], cell[1], h) + cb = can_by_h[h] + v_is = vol_ann(split(s)[0]) + k = can_is_vol[h] / v_is if v_is > 0 else 0.0 + bv, bcx = book(k * s), book(cb) + d_sh.append(sh(bv) - sh(bcx)); d_ho.append(sh(split(bv)[1]) - sh(split(bcx)[1])) + d_cg.append(cagr(bv) - cagr(bcx)); d_dd.append(maxdd(bv) - maxdd(bcx)) + book_res[cell] = dict(sh=np.array(d_sh), ho=np.array(d_ho), + cg=np.array(d_cg), dd=np.array(d_dd)) + print(f"\n -- {tag}: rho={cell[0]:.3f} q={cell[1]:.2f}") + for nm, arr, unit in (("dSharpe FULL", book_res[cell]["sh"], ""), + ("dSharpe HOLD", book_res[cell]["ho"], ""), + ("dCAGR", book_res[cell]["cg"] * 100, "pp"), + ("dmaxDD", book_res[cell]["dd"] * 100, "pp")): + print(f" {nm:14s} mediana appaiata {np.median(arr):+8.4f}{unit:2s} " + f"positivi {int((arr>0).sum()):2d}/24 banda [{arr.min():+.4f}, {arr.max():+.4f}]") + db_sh = book_res[CH]["sh"]; db_ho = book_res[CH]["ho"] + db_cg = book_res[CH]["cg"]; db_dd = book_res[CH]["dd"] + print(" NB: SKH01 e' tenuto alla SUA ancora canonica; solo l'ancora di TP01 varia. Nella") + print(" differenza appaiata l'ancora di SKH01 si cancella (solo TP01 cambia).") + + # ===================================================================== + # §11 MARGINAL vs TP01 (e perche' non puo' dare ADDS) + # ===================================================================== + print("\n### §11 marginal_vs_tp01 (importato) sulla cella scelta") + mg = al.marginal_vs_tp01(daily_ch) + print(f" verdetto: {mg['marginal_verdict']} corr->TP01 full {mg['corr_full']} hold {mg['corr_hold']}") + print(f" uplift blend w25: full {mg['blends']['w25']['uplift_full']:+} hold " + f"{mg['blends']['w25']['uplift_hold']:+} | insample_edge {mg.get('has_insample_edge')} " + f"| robust_oos {mg.get('robust_oos')}") + print(" ⚠ LIMITE STRUTTURALE: la variante e' TP01 con un'altra size -> corr>0.9 -> il gate") + print(" restituisce REDUNDANT PER COSTRUZIONE. `marginal_vs_tp01` giudica un'AGGIUNTA,") + print(" non una SOSTITUZIONE della size di uno sleeve gia' nel libro: qui non e' il gate") + print(" giusto e non lo si usa come tale (stessa classe della regola (f) del 22/08).") + + # ===================================================================== + # §12 ESEGUIBILITA' A $635 (min_order $5) + # ===================================================================== + print("\n### §12 ESEGUIBILITA' A $635 (eval_weights_smallcap, min_order $5)") + s_ch, _ = sleeve_series(CH[0], CH[1], 0) + k_ch = vol_ann(split(can0)[0]) / vol_ann(split(s_ch)[0]) + print(f" {'cella':>18s} {'asset':>5s} {'Sh model':>9s} {'Sh reale':>9s} {'haircut':>8s} " + f"{'ordini eseguiti':>16s} {'turnover/a':>11s}") + for nm, cell, kk in (("CANONICO", CANON_CELL, 1.0), ("SCELTA", CH, k_ch)): + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + tgt = kk * make_target_fn(cell[0], cell[1])(df) + sc = al.eval_weights_smallcap(df, tgt, capital=635.0, min_order=5.0, fee_side=FEE) + print(f" {nm:>10s} rho={cell[0]:.2f} {a:>5s} {sc['modeled']['sharpe']:9.3f} " + f"{sc['realistic']['sharpe']:9.3f} {sc['sharpe_haircut']:8.3f} " + f"{sc['n_executed_trades']:16d} {sc['executed_turnover_per_year']:11.1f}") + + # ===================================================================== + # §13 VERDETTO + # ===================================================================== + print("\n" + SEP) + print("### §13 VERDETTO") + med_full = float(np.median(A["dsf"])) + med_hold = float(np.median(A["dsh"])) + med_ciso = float(np.median(A["dci"])) + pos_full = int((A["dsf"] > 0).sum()) + book_med = float(np.median(np.array(db_sh))) + book_pos = int((np.array(db_sh) > 0).sum()) + conds = { + "iso-vol: dCAGR mediano > 0 su 24 ancore": med_ciso > 0, + "iso-vol: dCAGR positivo in >=18/24 ancore": int((A["dci"] > 0).sum()) >= 18, + "dSharpe FULL mediano > 0 e in >=18/24 ancore": (med_full > 0 and pos_full >= 18), + "dSharpe HOLD-OUT mediano > 0": med_hold > 0, + "non e' un anno solo (>=50% degli anni con mediana>0)": n_pos_years >= 0.5 * len(per_year), + "deflated-Sharpe >= 0.95 (60 trial)": bool(np.isfinite(dsr) and dsr >= 0.95), + "libro: dSharpe mediano > 0": book_med > 0, + "causalita' ok": bool(caus["ok"]), + "controlli positivi 3/3": bool(ctrl_delev_ok and ctrl_orac_ok and ctrl_anti_ok), + } + for k_, v_ in conds.items(): + print(f" [{'PASS' if v_ else 'FAIL'}] {k_}") + n_fail = sum(1 for v_ in conds.values() if not v_) + print(f"\n cella scelta al buio: rho={CH[0]:.3f} q={CH[1]:.2f} (canonico rho=0.333 q=1.00)") + print(f" dSharpe FULL mediano {med_full:+.4f} ({pos_full}/24) | dSharpe HOLD mediano " + f"{med_hold:+.4f} | dCAGR ISO-VOL mediano {med_ciso*100:+.3f}pp") + print(f" libro 75/25: dSharpe mediano {book_med:+.4f} ({book_pos}/24), " + f"dCAGR {np.median(db_cg)*100:+.3f}pp, dmaxDD {np.median(db_dd)*100:+.3f}pp " + f"({int((db_dd>0).sum())}/24 PEGGIORE)") + print(f" condizioni fallite: {n_fail}/{len(conds)}") + print("\n LETTURA ONESTA delle condizioni che PASSANO (dichiarate prima di misurare, e alcune") + print(" passano per una moneta — un PASS a mediana ~0 con ~12/24 non e' evidenza):") + print(f" * 'libro: dSharpe mediano > 0' passa a {book_med:+.4f} con {book_pos}/24 = MONETA.") + print(f" * 'deflated-Sharpe >= 0.95' passa a {dsr:.4f} ma il gate e' QUASI VACUO su una") + print(f" famiglia omogenea (sr0 {sr0:.3f}); a sr0 di screen (>=0.9) FALLISCE.") + print(" * 'non e' un anno solo' passa 4/8, cioe' anch'esso una moneta.") + print(" MISURATO DOPO le condizioni (quindi NON usato come gate, riportato come fatto):") + print(f" * il maxDD del LIBRO PEGGIORA in {int((db_dd>0).sum())}/24 ancore " + f"(mediana {np.median(db_dd)*100:+.3f}pp) — e il valore dichiarato di TP01 e' il TAGLIO del DD.") + print(f" * Spearman(ShIS, ShHOLD) sulle 60 celle = {rho_s:+.3f}: in questa famiglia scegliere") + print(" in-sample non predice l'hold-out, quindi NESSUNA cella e' proponibile.") + print(f"\n Attesa a priori: SCARTATO. Esito: {'CONFERMATA' if n_fail > 0 else 'REFUTATA'}.") + bd = anchor_rep[next(c for c in anchor_rep if abs(c[0] - 1.0) < 1e-6 and abs(c[1] - 1.0) < 1e-6)] + print("\n VERDETTO: SCARTATO — nessuna forma di sizing conviction-aware alza il drift a") + print(" iso-volatilita' in modo difendibile.") + print(f" (1) il meglio dell'INTERA griglia e' " + f"+{G.cagr_iso.max()*100 - canrow.cagr_iso*100:.2f}pp di CAGR iso-vol (rho=0.45 q=1.25);") + print(" (2) la cella scelta AL BUIO peggiora l'hold-out del LIBRO in 0/24 ancore e il suo") + print(f" maxDD in 24/24, contro {np.median(db_cg)*100:+.2f}pp di CAGR;") + print(f" (3) l'unica cella con guadagno hold-out ROBUSTO e' il BINARIO rho=1 " + f"(dShHOLD {np.median(bd['dsh']):+.3f}, {int((bd['dsh']>0).sum())}/24) — ma perde") + print(" FULL e drift iso-vol, e sceglierla richiede di guardare l'hold-out: e'") + print(" selezione-sull'hold-out, il gate codificato dal progetto il 29/06;") + print(f" (4) Spearman(ShIS, ShHOLD) = {rho_s:+.3f} su 60 celle: qui scegliere in-sample e'") + print(" PEGGIO di una moneta => NON esiste una cella proponibile, nemmeno diversa da") + print(" quella scelta. Il canonico rho=1/3 resta com'e' — non perche' sia l'ottimo, ma") + print(" perche' l'ottimo di questo knob sta ai due ESTREMI OPPOSTI nelle due meta'") + print(" del campione, e nessuna delle due meta' e' un argomento per l'altra.") + print(SEP) + print(f"runtime {time.time()-t_start:.1f}s") + + +if __name__ == "__main__": + main() From 6f74dcb84316c8100fbb5509e3c2ea58bce346cc Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 01:04:43 +0000 Subject: [PATCH 064/114] =?UTF-8?q?registro=20=C2=A741:=20TP01-SIZE=20SCAR?= =?UTF-8?q?TATO=20=E2=80=94=20Spearman=20IS/HOLD=20=E2=88=920,527,=20e=20l?= =?UTF-8?q?a=20premessa=20del=20coordinatore=20sui=20bucket=20di=20convinz?= =?UTF-8?q?ione=20era=20falsa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 99 ++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index f64ca91..bc6882a 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -706,7 +706,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 38 | SCALE-SPEC | specificare il knob di leva che non esiste | **LEAD** — specificata, **1 nodo irrisolto** sul disaster-SL | | 39 | FUNDING-AVOID | evitare il funding esprimendo il libro sui datati | **SCARTATO** sui datati · **LEAD** sullo **spot** | | 40 | SL-ANCHOR | il nodo che bloccava il gradino di leva | **SCIOLTO** — rotolante, ma il timore e' refutato | -| 41 | TP01-SIZE | una size che risponde alla CONVINZIONE batte il vol-target uniforme, a pari vol realizzata? | _in corso_ | +| 41 | TP01-SIZE | una size che risponde alla CONVINZIONE batte il vol-target uniforme, a pari vol realizzata? | **SCARTATO** — il meglio di 60 celle vale **+0,30pp** di CAGR iso-vol, e l'ottimo sta ai **due estremi opposti** nelle due meta' del campione (**Spearman ShIS/ShHOLD −0,527**: scegliere in-sample e' peggio di una moneta) | | 42 | TP01-LS | la gamba short del trend a 1d paga il proprio costo, o e' il 2022? | **SCARTATO** — toglie **−1,91%/anno di drift in 0/24 ancore**, fallisce l'iso-vol, +8,80pp di maxDD in 24/24, e **senza il 2022 il divario RADDOPPIA** | | 43 | DATA-UNUSED | quali dati il progetto possiede e nessuna strategia legge — e uno di essi sostiene un'ipotesi? | _in corso_ | | 44 | DEPOSIT-TIMING | un calendario di versamento CONDIZIONALE batte il piatto, a pari soldi? | _in corso_ | @@ -2212,3 +2212,100 @@ sola ancora canonica, tutto **close-only**. **VERDETTO: `LA SHORT DEL TREND A 1d NON PAGA IL PROPRIO COSTO SU NESSUN ASSE CHE DECIDE`** — SCARTATO. **Libro, pesi, cron, config INVARIATI.** + +--- + +## 41 — TP01-SIZE (una size che risponde alla convinzione) + +`scripts/research/r0823_tp01_size.py` (**8,4 s**, nessuna rete). **60 celle dichiarate prima di +guardare** (`rho` = g(1/3)/g(1), 10 valori × `q` = esponente del vol-targeting, 6 valori) × 24 +ancore = 1.440 valutazioni di sleeve + 3 controlli positivi. **Segnale CONGELATO** (TSMOM 30/90/180, +long-flat): cambia solo la mappa `convinzione → size`. + +✅ **Replica bit-exact**: `target_series` vs `TrendPortfolio.target_series` **max|diff| = 0**; +rendimenti vs `sleeves._tp01_returns()` **max|diff| = 0** (n=2720). +⚠️ **Sottoprodotto sull'harness:** `altlib.tp01_baseline_daily()` **NON e' bit-exact col sleeve** — +1 ulp (1,11e-16) su **1863/2720 giorni (68,5%)**, perche' `_to_daily` fa +`(1+r).resample("1D").prod()-1`, che e' un no-op **algebrico** ma non in virgola mobile su una serie +gia' giornaliera. Innocuo, ma *"bit-exact vs altlib" non e' un controllo ottenibile*: chi lo pretende +insegue un fantasma. + +🚨 **CORREZIONE ALLA PREMESSA DEL MANDATO (mia), verificata al sorgente.** Il briefing diceva che +la convinzione e' *"spesso 1/3 o 2/3"*. **Il bucket 2/3 e' aritmeticamente impossibile:** +`tsmom_blend` media **tre `np.sign(...)`** → la direzione vive in **{−1, −1/3, +1/3, +1}**, e dopo il +clip long-flat in **{0, 1/3, 1}** (misurato BTC: 44,2% / 24,7% / 31,1%). ✅ Confermato da **due +agenti indipendenti** e ri-verificato dal coordinatore leggendo `src/strategies/trend_portfolio.py`. +**Conseguenza:** potenza `c^p`, floor affine e soglia sono **la stessa cella riparametrizzata** — la +famiglia della convinzione ha **UN** grado di liberta', non tre. + +**Attesa a priori dell'agente, scritta nel docstring prima di misurare:** *"il canonico e' gia' +dentro il suo plateau; qualunque rimodellamento dara' un delta indistinguibile da zero a iso-vol… +VERDETTO ATTESO: SCARTATO"*, col corollario che il null del de-levering sarebbe apparso in **forma +speculare (re-levering)**. **Entrambi confermati.** + +| | ShFULL | ShIS | ShHOLD | vol | CAGR grezzo | CAGR **iso-vol** | +|---|---|---|---|---|---|---| +| **canonico** rho=1/3 q=1 | 1,305 | 1,488 | 0,441 | 12,14% | 16,32% | **16,32%** | +| binario rho=1 q=1 | 1,265 | 1,398 | **0,674** | 15,30% | 19,95% | 15,87% (−0,45pp) | +| soglia rho=0 q=1 | 1,229 | 1,429 | 0,236 | 11,46% | 14,38% | 15,16% (−1,16pp) | +| **niente vol-target** q=0 | 1,166 | 1,298 | 0,479 | **39,65%** | **46,75%** | 13,92% (−2,40pp) | +| miglior cella iso-vol | 1,312 | 1,498 | 0,480 | 9,82% | 13,20% | 16,61% (**+0,30pp**) | +| cella scelta **al buio** | 1,306 | 1,508 | 0,382 | 9,26% | 12,38% | 16,48% (+0,16pp) | + +Rango del canonico: **5/60 in-sample, 6/60 full, 41/60 hold-out**. + +📌 **NULL DEL RE-LEVERING nella forma piu' pura mai misurata nel progetto (6ª occorrenza, prima +col segno rovesciato):** togliere il vol-targeting porta il CAGR **16,32% → 46,75% (+186%)** e a +iso-vol fa **−2,40pp**. **Era tutta leva.** Il gate e' esatto per costruzione (`net` omogeneo di +grado 1 → lo Sharpe e' invariante, verificato a 4,4e-16) quindi **morde sul drift, non sullo +Sharpe**; `k` calibrato **in-sample** per non introdurre look-ahead nella normalizzazione. + +**Banda d'ancora (24, mediana delle differenze APPAIATE):** + +| cella | dShFULL | >0 | dShHOLD | >0 | dCAGRiso | >0 | +|---|---|---|---|---|---|---| +| scelta al buio 0,25/1,25 | +0,0074 | 17/24 | **−0,0580** | **0/24** | +0,242pp | 22/24 | +| de-luckata 0,15/1,25 | +0,0002 | 12/24 | −0,1108 | 0/24 | +0,090pp | 16/24 | +| binario 1,00/1,00 | −0,0932 | 2/24 | **+0,2429** | **24/24** | −1,029pp | 3/24 | + +⚠️ Le due selezioni oneste **non coincidono** (ancora 0 → rho=0,25; mediana su 24 ancore → rho=0,15): +**la superficie e' piatta**. +✅ **Replica indipendente, su dati odierni, del finding d'ancora del 02/07:** hold-out canonico +**+0,441 = 96° pctl** delle 24, **mediana onesta +0,219**, banda [+0,014, +0,441]; FULL canonico al +29° pctl. + +**Per anno** (mediana appaiata su 24 ancore): 2019 +0,006 · **2020 +0,085 (24/24)** · 2021 −0,077 +(0/24) · 2022 −0,080 (0/24) · 2023 +0,020 · **2024 +0,057 (23/24)** · 2025 −0,051 · 2026 −0,003 → +**4/8 positivi = moneta**: non "un anno solo", ma nemmeno persistenza. + +**Impatto sul LIBRO** (canonico ShFULL 1,814 · ShHOLD 1,437 · CAGR 21,22% · maxDD 9,42%): la cella +scelta fa **dShFULL +0,0006 (12/24 = moneta)**, **dShHOLD −0,0878 (0/24)**, dCAGR +0,188pp (22/24) e +📌 **dmaxDD +0,493pp PEGGIORE a 24 ancore su 24** — e il valore dichiarato di TP01 **e' il taglio +del DD**. + +🚨 **IL RISULTATO CHE DECIDE, e nessuno lo cercava: `Spearman(ShIS, ShHOLD) = −0,527` (p<0,001) +sulle 60 celle — in questa famiglia scegliere in-sample e' PEGGIO DI UNA MONETA.** Ed e' un +meccanismo misurato, non rumore: **`Spearman(rho, ShHOLD) = +1,000` a tutti e sei i q** (in hold-out +l'ottimo e' al bordo rho≥1) mentre in-sample la curva e' **a gobba** con ottimo interno ~0,45-0,60. +**La convinzione e' informativa in-sample e ANTI-informativa in hold-out** — visibile anche nei +bucket (BTC hold-out: convinzione debole Sharpe **+2,20** contro piena **−0,06**). Quindi l'unica +cella con guadagno hold-out robusto (binario rho=1, 24/24) e' **esattamente quella che si potrebbe +scegliere solo guardando l'hold-out** = il gate del 29/06. **Non esiste una cella proponibile, +nemmeno diversa da quella scelta.** + +**Gate e controlli.** `causality_ok` **True** (max_tail_diff 0,0); `select_cell_insample` importato +sceglie **la stessa cella** (identita' verificata); **`deflated_sharpe` 0,9990 PASS ma quasi vacuo** +(famiglia omogenea, sd(Sharpe) 0,084 → sr0 0,197; **FAIL a sr0 ≥0,9**, 0,229 all'sr0 di screen +1,572) = **terza conferma indipendente del §10**; `marginal_vs_tp01` **REDUNDANT per costruzione** +(giudica un'aggiunta, non la SOSTITUZIONE della size di uno sleeve gia' nel libro) → riportato, non +usato come gate. **Controlli positivi 3/3**: de-levering ×0,5 → dSharpe **esattamente 0** e dCAGR +iso **esattamente 0**; oracolo look-ahead → Sharpe **10,97** con `causality_ok=False`; anti (rho=3) +→ −3,55pp. **Eseguibilita' a $635: haircut 0,001** (**8ª volta nell'ondata**). + +**Cosa NON e' misurato:** altri TF (il deploy e' a 1d, e sotto le 12h e' vietato dal 19/06); ancora +di SKH01 (si cancella nella differenza appaiata); una convinzione da un **segnale** diverso +(magnitudine invece di segno) — sarebbe un'altra **famiglia**, da dichiarare e deflazionare a parte; +`weights_tilt_null` non applicabile (il 75/25 non si muove). + +**VERDETTO: `NON ESISTE UNA CELLA PROPONIBILE — E IL MOTIVO E' CHE IN QUESTA FAMIGLIA LA SELEZIONE +IN-SAMPLE E' ANTI-CORRELATA COL FUTURO`** — SCARTATO. **Libro, pesi, cron, config INVARIATI.** From fc55698096690365329d2a47ecf25bea94303918 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 01:05:11 +0000 Subject: [PATCH 065/114] =?UTF-8?q?research(deposit-timing):=20il=20timing?= =?UTF-8?q?=20dei=20bonifici=20non=20paga=20=E2=80=94=200/12=20celle,=20e?= =?UTF-8?q?=20il=20segnale=20compra=20davvero=20il=2051%=20piu'=20in=20bas?= =?UTF-8?q?so?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §44. Prima misura di un calendario di versamento CONDIZIONALE (le 5 leve del piano misurate finora confrontano solo calendari deterministici). 12 celle dichiarate prima: buy-the-dip x4 soglie, risk-off (TP01 a 0x), valore-medio x3, anti-dip x4. Stesso flusso di cassa per tutte (EUR X/mese sul conto corrente); cio' che cambia e' quando entrano nel libro, con la liquidita' in attesa a 0% reale. Lente L3 CONGIUNTA (funding + fisco), muro $313k, block bootstrap 3000 percorsi. Replica 4/4 prima di ogni delta: muro $313.143, riga EUR250/m 23,4a / P(20a) 14,4%, riga EUR500/m 17,3a / 85,4%, e P0 del motore col buffer di cassa BIT-EXACT contro PN.accumula. SCARTATO. 0/12 in ogni lente (mercato vero, IID, drift dimezzato, EUR250, EUR500); migliore -0,03% appaiato contro una risoluzione MC della differenza appaiata di 2,0pp, e su una cella che aspetta 1 giorno = P0 travestito. Il meccanismo, misurato invece che assunto: buy-the-dip 20% compra davvero al 0,494 del livello medio del piatto (51% piu' in basso) e arriva al capitale-rendita nel 6,3% dei casi contro l'85,4%. Comprare meglio e comprare tardi sono la stessa mossa. Sotto la soglia bassa si ribalta: dip 5% compra a 1,009, cioe' PIU' IN ALTO — una condizione poco profonda non compra il calo, ritarda dentro la salita. Due errori catturati su me stesso da un controllo: (i) il ritardo-gemello dava "informazione" +11,5% sul mercato vero e mi avrebbe fatto scrivere che il dip predice — sotto IID, dove non c'e' niente da prevedere, la stessa colonna vale +14,9%, quindi e' meccanica (attesa variabile contro fissa); (ii) senza la colonna dell'attesa avrei letto P4 anti-dip come "stesso segno => rumore": e' piatto perche' in un mercato che sale la sua condizione e' quasi sempre gia' vera (16 giorni contro 3682). Rischio di venue separato dal timing: la cassa fuori dall'exchange riduce la penalita' di +2,4pp a p=1% e +3,8pp a p=2% su dip 10%, contro una penalita' di timing di -15,7pp, e P(libro azzerato) e' identica per tutte le politiche. Ordine di grandezza: la migliore politica di timing vale -0,1pp di P(20a); passare da EUR250 a EUR500 al mese vale +71pp. Solo script di ricerca. Book/pesi/cron/config INVARIATI. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf --- scripts/research/r0823_deposit_timing.py | 695 +++++++++++++++++++++++ 1 file changed, 695 insertions(+) create mode 100644 scripts/research/r0823_deposit_timing.py diff --git a/scripts/research/r0823_deposit_timing.py b/scripts/research/r0823_deposit_timing.py new file mode 100644 index 0000000..630d0ec --- /dev/null +++ b/scripts/research/r0823_deposit_timing.py @@ -0,0 +1,695 @@ +#!/usr/bin/env python +"""r0823_deposit_timing.py — §44 DEPOSIT-TIMING: il timing dei BONIFICI paga? + +LA DOMANDA. Nel progetto e' misurato che la leva dominante non e' la strategia ma il +versamento: *versare o no* (da mai a ~16 anni) > *quando* (fattore 6 a pari soldi totali) > +*quanto presto si smette* (i primi 5 anni sono il 25% dei soldi e il 58% del risultato) > +*piatto vs crescente* (24%) > *frequenza* (1-3%). Ma TUTTE quelle misure confrontano +**calendari deterministici**. Nessuno ha mai misurato un calendario **CONDIZIONALE**: versare +di piu' dopo un drawdown, o quando il libro e' risk-off, o su una qualunque regola che guarda +lo stato del conto. + + A pari soldi totali versati e pari calendario di DISPONIBILITA', una regola di deposito + condizionale batte il versamento piatto? + +⚠️ E' MARKET TIMING SUI PROPRI BONIFICI, e va giudicato come tale. Il null e' severo (sotto). + +ATTESA A PRIORI, DICHIARATA PRIMA DI MISURARE (coordinatore, e la sottoscrivo): **NON +funziona**. Comprare piu' in basso alza il rendimento sul capitale versato, ma la mediana e' +dominata dal fatto che i soldi entrano PIU' TARDI, e il progetto ha gia' misurato che entrare +prima vale un fattore 6 (r0726_deposits punto 3). L'esito e' riportato comunque, anche se +refuta l'attesa. + +IL VINCOLO DI REALTA' CHE RENDE LA DOMANDA NON BANALE. I soldi non compaiono. Ogni politica +riceve lo STESSO flusso di cassa (EUR X/mese sul conto corrente); cio' che cambia e' **quando +entrano nel libro**. La liquidita' in attesa rende **0% reale netto** (assunzione del progetto: +XEON) — ed e' meta' del costo di ogni politica che aspetta. Senza questo vincolo la domanda +sarebbe truccata: "versare di piu' nei cali" senza dire da dove vengono i soldi e' un confronto +fra due piani di versamento diversi, non fra due timing. + +LE POLITICHE, DICHIARATE TUTTE PRIMA (e contate come trial: 12 celle candidate) + P0 PIATTO — versa tutto il buffer il giorno stesso. E' l'incumbent. + P1 BUY-THE-DIP(th) — accantona; versa TUTTO il buffer quando l'indice di equity del libro + sta >= th sotto il suo massimo. th in {5,10,15,20}% (4 celle) + P2 RISK-OFF — versa quando TP01 e' a 0x (segnale gia' pubblico e causale, ~33% dei + giorni). (1 cella) + P3 VALORE-MEDIO(g) — versa quanto serve a riportare il capitale su una traiettoria + bersaglio che cresce a g. Mai vendere. g in {0,8,15}%/anno (3 celle) + P4 ANTI-DIP(th) — lo SPECULARE OBBLIGATORIO: versa dopo un rialzo di th dal minimo + corrente. th in {5,10,15,20}% (4 celle) + CONTROLLO DEL RITARDO — per ogni politica condizionale, la stessa attesa MEDIA ma + DETERMINISTICA (versa k giorni dopo, k = attesa misurata di quella + politica, nessun segnale). Separa "il segnale informa" da "aspettare + costa". Non e' una cella candidata: e' un null. + +I NULL + (a) BOOTSTRAP IID (blocco=1) — distrugge la dipendenza seriale. Se una politica vince anche + li', sta sfruttando la meccanica del bootstrap, non il mercato: con ritorni indipendenti + il passato non informa il futuro e ogni residuo e' cassa/ritardo. + (b) P4 ANTI-DIP — se P1 e P4 danno lo stesso segno, si sta misurando rumore. + (c) DRIFT DIMEZZATO — lo stress standard del progetto sul libro (7,4 anni di storia, due tori). + +LA LENTE E' QUELLA UFFICIALE: L3 CONGIUNTA di r0822d_piano_vero (funding esatto DENTRO la serie ++ fisco d'accumulo coerente + ancora x0.89 misurata). Bersaglio = il muro di quella lente. + +CONVENZIONE DEL TRAGUARDO: si arriva quando **libro + cassa** >= muro. E' la convenzione +FAVOREVOLE alle politiche condizionali (la cassa che aspetta conta lo stesso), scelta apposta +perche' l'attesa a priori e' che perdano: se perdono anche cosi', il risultato e' piu' forte. +La convenzione severa (solo libro) e' riportata come sensibilita'. + + nice -n 19 uv run python scripts/research/r0823_deposit_timing.py +""" +from __future__ import annotations + +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +sys.path.insert(0, str(ROOT / "scripts" / "research")) +sys.path.insert(0, str(ROOT)) + +import r0725_capcurve as CC # noqa: E402 TAX_RATE, EURUSD, _boot_paths +import r0727_tasse as TX # noqa: E402 PATRIMONIALE, CARRY_ANNI +import r0807_piano_netto as PN # noqa: E402 passo_fiscale, accumula, muro +import r0822d_piano_vero as PV # noqa: E402 costruisci_serie, deluck, leggi +from src.data.downloader import load_data # noqa: E402 +from src.strategies.trend_portfolio import (CANONICAL, TrendPortfolio, # noqa: E402 + resample_1d) + +# --- costanti dichiarate --------------------------------------------------- +START = PV.START # $635, il conto Deribit vero +N_PATHS = 3_000 # la taglia della tabella pubblicata (seed 725) +BLOCK = PV.BLOCK # 20 giorni +ANNI = PV.ANNI_TRAJ # 25 (20 troncherebbe la coda) +SEED = PV.SEED_TRAJ # 725 +SEMI_MC = (725, 2026, 20260823) # per la risoluzione Monte Carlo +DEP_MAIN = 500.0 # il piano dichiarato dall'operatore (EUR/mese) +DEP_ALT = 250.0 # il livello di cui si discute da un mese +CASH_RATE = 0.0 # XEON ~0% reale netto: assunzione del progetto +DD_GRID = (0.05, 0.10, 0.15, 0.20) +VA_GRID = (0.00, 0.08, 0.15) +P_VENUE = (0.0, 0.01, 0.02) +ASSETS = ("BTC", "ETH") +T0 = time.time() + + +def sez(t: str) -> None: + print("\n" + "=" * 104) + print(f" {t}") + print("=" * 104) + + +def clock() -> str: + return f"[{time.time() - T0:5.0f}s]" + + +# =========================================================================== +# 0) LE SERIE — ritorni (lente L3) ed ESPOSIZIONE di TP01, sullo stesso indice +# =========================================================================== +def esposizione_tp01(alloc: float = 635.0) -> pd.Series: + """Esposizione GIA' TENUTA dal libro (0..2), causale, con la stessa regola di min-order di + `CC._tp01_realistic_cached`: un ribilanciamento sotto $5 non si esegue. + + Non e' una re-implementazione della strategia: `target_series` e' importata dal modulo di + produzione. Qui serve solo il livello di posizione, non i ritorni (quelli restano quelli + ufficiali di `CC.book_series`, gia' dentro la serie della lente). + """ + tp = TrendPortfolio(**CANONICAL) + cap_asset = alloc / len(ASSETS) + ser = {} + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + tgt = np.nan_to_num(np.asarray(tp.target_series(df), float)) + held = np.empty(len(tgt)) + cur = 0.0 + for i in range(len(tgt)): + if abs(tgt[i] - cur) * cap_asset >= CC.DERIBIT_MIN_ORDER: + cur = tgt[i] + held[i] = cur + pos = np.zeros(len(held)) + pos[1:] = held[:-1] # causale: deciso a i-1, tenuto in i + ser[a] = pd.Series(pos, index=pd.to_datetime(df["datetime"])) + J = pd.concat(ser, axis=1, join="inner").fillna(0.0) + out = pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index) + if out.index.tz is None: + out.index = out.index.tz_localize("UTC") + return out + + +def boot_idx(n_src: int, n_paths: int, n_days: int, block: int, + rng: np.random.Generator) -> np.ndarray: + """Gli INDICI del block bootstrap di `CC._boot_paths`, per poter trasportare insieme ai + ritorni una seconda serie allineata (l'esposizione di TP01, che serve a P2). + + Controllo di identita' in sezione 1: `r[boot_idx(...)]` deve essere bit-exact + `CC._boot_paths(r, ...)` a parita' di generatore. + """ + n_blocks = int(np.ceil(n_days / block)) + starts = rng.integers(0, n_src - block, size=(n_paths, n_blocks)) + idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1) + return idx[:, :n_days].astype(np.int32) + + +# =========================================================================== +# IL MOTORE — accumulo con buffer di CASSA e politica di versamento +# =========================================================================== +def accumula_pol(paths: np.ndarray, flat: np.ndarray | None, dep_eur: float, + bersaglio: float, aliquota: float, patrimoniale: float, + policy, start: float = START, cash_rate: float = CASH_RATE, + fail: np.ndarray | None = None, + traguardo_include_cassa: bool = True) -> dict: + """Accumulo su path GIA' ESTRATTI, con una politica che decide QUANDO la cassa entra. + + Il flusso di cassa e' identico per ogni politica: `dep_eur` ogni 30 giorni sul CONTO + CORRENTE. La politica decide solo quanto trasferire nel libro, ogni giorno. + + Causalita': il ritorno del giorno t si applica PRIMA del trasferimento (come in + `PN.accumula`), quindi cio' che entra il giorno t rende dal giorno t+1; e la decisione usa + l'indice di equity fino alla chiusura di t, che a quel punto e' osservabile. + + `policy(t, cap, cash, dd, ru, flat_t)` ritorna l'importo da trasferire (USD, per path). + `fail`: maschera (path, giorno) di fallimento del venue — il libro va a zero, la CASSA no. + """ + n_paths, n_days = paths.shape + dep = dep_eur * CC.EURUSD + cash_d = (1.0 + cash_rate) ** (1.0 / 365.0) - 1.0 + cap = np.full(n_paths, start) + cash = np.zeros(n_paths) + eqi = np.ones(n_paths) # indice di equity del libro, NEUTRO ai versamenti + peak = np.ones(n_paths) + trough = np.ones(n_paths) + carry = np.zeros((n_paths, TX.CARRY_ANNI)) + anno_start = cap.copy() + flusso = np.zeros(n_paths) + colpito = np.full(n_paths, -1, int) + versato = start + cassa_int = 0.0 # integrale della cassa (per la legge di Little) + persi = np.zeros(n_paths, bool) + pnum = np.zeros(n_paths) # sum(importo * livello di equity all'ingresso) + pden = np.zeros(n_paths) # sum(importo) + for t in range(n_days): + rt = paths[:, t] + cap *= (1.0 + rt) + eqi *= (1.0 + rt) + np.maximum(peak, eqi, out=peak) + np.minimum(trough, eqi, out=trough) + if fail is not None: + f = fail[:, t] + if f.any(): + cap = np.where(f, 0.0, cap) + anno_start = np.where(f, 0.0, anno_start) + flusso = np.where(f, 0.0, flusso) # la perdita non diventa credito d'imposta + persi |= f + cash *= (1.0 + cash_d) + if t % 30 == 0 and t > 0 and dep > 0: + cash += dep + versato += dep + move = policy(t, cap, cash, 1.0 - eqi / peak, eqi / trough - 1.0, + None if flat is None else flat[:, t]) + if move is not None: + move = np.minimum(np.maximum(move, 0.0), cash) + cap += move + cash -= move + flusso += move + pnum += move * eqi # a che LIVELLO e' entrato ogni dollaro + pden += move + cassa_int += float(cash.mean()) + tot = cap + cash if traguardo_include_cassa else cap + nuovi = (colpito < 0) & (tot >= bersaglio) + colpito[nuovi] = t + if (t + 1) % 365 == 0 and (aliquota > 0 or patrimoniale > 0): + PN.passo_fiscale(cap, anno_start, flusso, carry, aliquota, patrimoniale) + anno_start = cap.copy() + flusso[:] = 0.0 + lam = dep / 30.0 if dep > 0 else np.nan # USD/giorno di flusso in entrata + attesa = (cassa_int / n_days) / lam if dep > 0 else 0.0 # Little: W = L / lambda + prezzo = np.where(pden > 0, pnum / np.maximum(pden, 1e-12), np.nan) + return dict(cap=cap, cash=cash, colpito=colpito, versato=versato, + attesa_g=float(attesa), p_perso=float(persi.mean()), + prezzo=float(np.nanmedian(prezzo)), + non_versata=float(np.median(cash))) + + +# =========================================================================== +# LE POLITICHE +# =========================================================================== +def pol_piatto(): + return lambda t, cap, cash, dd, ru, fl: cash + + +def pol_dip(th: float): + return lambda t, cap, cash, dd, ru, fl: np.where(dd >= th, cash, 0.0) + + +def pol_antidip(th: float): + return lambda t, cap, cash, dd, ru, fl: np.where(ru >= th, cash, 0.0) + + +def pol_riskoff(): + return lambda t, cap, cash, dd, ru, fl: np.where(fl, cash, 0.0) + + +def pol_va(g: float, dep_eur: float, start: float = START): + """Valore-medio: traiettoria bersaglio deterministica, versa la differenza, mai vende.""" + dep = dep_eur * CC.EURUSD + gm = (1.0 + g) ** (30.0 / 365.0) - 1.0 + state = {"T": start} + + def f(t, cap, cash, dd, ru, fl): + if t % 30 or t == 0: + return None + state["T"] = state["T"] * (1.0 + gm) + dep + return np.maximum(state["T"] - cap, 0.0) + return f + + +def pol_lag(k: int, dep_eur: float): + """Il controllo del ritardo: versa esattamente k giorni dopo l'arrivo, senza guardare nulla.""" + dep = dep_eur * CC.EURUSD + + def f(t, cap, cash, dd, ru, fl): + return np.full(len(cap), dep) if (t > k and (t - k) % 30 == 0) else None + return f + + +def celle(dep_eur: float) -> dict: + """Le 12 celle candidate, dichiarate qui e in nessun altro posto.""" + C = {"P0 piatto": pol_piatto()} + for th in DD_GRID: + C[f"P1 dip {th:.0%}"] = pol_dip(th) + C["P2 risk-off"] = pol_riskoff() + for g in VA_GRID: + C[f"P3 VA g={g:.0%}"] = pol_va(g, dep_eur) + for th in DD_GRID: + C[f"P4 anti {th:.0%}"] = pol_antidip(th) + return C + + +N_TRIAL = 12 # P1(4) + P2(1) + P3(3) + P4(4). P0 e' l'incumbent, i lag sono null. + + +# =========================================================================== +def leggi(colpito: np.ndarray) -> dict: + """Mediana INCONDIZIONATA (non-arrivo = +inf, MAI -1) + condizionata + P per orizzonte.""" + y = np.where(colpito >= 0, colpito / 365.0, np.inf) + a = y[np.isfinite(y)] + return dict(med=float(np.median(y)), + med_cond=float(np.median(a)) if len(a) > 100 else float("nan"), + p10=float((y <= 10).mean()), p15=float((y <= 15).mean()), + p20=float((y <= 20).mean())) + + +def fmt(d: dict) -> str: + m = f">{ANNI}a" if not np.isfinite(d["med"]) else f"{d['med']:.1f}a" + c = "—" if np.isnan(d["med_cond"]) else f"{d['med_cond']:.1f}a" + return f"{m:>7}{c:>8}" + + +def riga(nome: str, res: dict, perp: float, base: dict | None = None, + prezzo0: float | None = None) -> str: + d = leggi(res["colpito"]) + tot = res["cap"] + res["cash"] + rend = float(np.median(tot * perp / 365.0 / CC.EURUSD)) + dp = "" if base is None else f"{d['p20'] - base['p20']:+7.1%}" + pz = "" if not prezzo0 else f"{res['prezzo'] / prezzo0:.3f}" + return (f" {nome:<16}{fmt(d)}{d['p15']:>8.1%}{d['p20']:>8.1%}" + f"{dp:>9}{rend:>10.2f}{res['attesa_g']:>8.0f}{pz:>8}") + + +HDR = (f" {'politica':<16}{'med':>7}{'m|arr':>8}{'P(15a)':>8}{'P(20a)':>8}" + f"{'dP20':>9}{'EUR/g 20a':>10}{'attesa':>8}{'prezzo':>8}") + + +def tabella(paths, flat, dep, muro, perp, titolo, **kw) -> dict: + print(f"\n {titolo}") + print(HDR) + out = {} + base = None + p0 = None + for nome, pol in celle(dep).items(): + r = accumula_pol(paths, flat, dep, muro, CC.TAX_RATE, TX.PATRIMONIALE, pol, **kw) + d = leggi(r["colpito"]) + if base is None: + base, p0 = d, r["prezzo"] + out[nome] = r + print(riga(nome, r, perp, None if nome.startswith("P0") else base, + None if nome.startswith("P0") else p0)) + return out + + +# =========================================================================== +def main() -> None: + print("=" * 104) + print(" r0823 §44 DEPOSIT-TIMING — il timing dei BONIFICI paga? (lente L3: funding + fisco)") + print("=" * 104) + + # ------------------------------------------------------------------ 0 + B = PV.costruisci_serie() + idx = pd.DatetimeIndex(B.index) + r = PV.deluck(B["fund"].values.astype(float)) # lente L3 CONGIUNTA, ancora x0.89 + E = esposizione_tp01().reindex(idx).ffill().fillna(0.0) + flat_src = (E.values <= 1e-9) + print(f"\n serie L3 CONGIUNTA (libro live 75/25, funding esatto, ancora x{PV.DELUCK}) — " + f"{len(r)} giorni {idx.min().date()}..{idx.max().date()}") + print(f" drift {r.mean() * 365:.2%} · vol {r.std() * 365 ** 0.5:.2%} · " + f"esposizione TP01 media {E.mean():.3f}x · giorni a 0x = {flat_src.mean():.1%} {clock()}") + + # ------------------------------------------------------------------ 1 + sez("1) CONTROLLO DI REPLICA — prima di ogni delta, riprodurre i numeri gia' pubblicati") + ok = {} + netto = CC.TARGET_EUR_DAY * 365 * CC.EURUSD + perp, muro = PN.muro(r, netto, CC.TAX_RATE, TX.PATRIMONIALE) + ok["R1"] = abs(muro - 313_000.0) < 1_500.0 + print(f"\n R1 muro L3 congiunta: perpetua {perp:.2%} · ${muro:,.0f}" + f"{'':>10}pubblicato $313k {'OK' if ok['R1'] else 'DIVERGE'} {clock()}") + + rng = np.random.default_rng(SEED) + ii = boot_idx(len(r), N_PATHS, ANNI * 365, BLOCK, rng) + paths = r[ii] + flat = flat_src[ii] + rng2 = np.random.default_rng(SEED) + ok["R2"] = bool(np.array_equal(paths, CC._boot_paths(r, N_PATHS, ANNI * 365, BLOCK, rng2))) + print(f" R2 `boot_idx` trasporta gli STESSI path di `CC._boot_paths` (per portarsi dietro") + print(f" l'esposizione di TP01, che serve a P2):{'':>19}" + f"{'BIT-EXACT' if ok['R2'] else 'DIVERGE'}") + + print(f"\n R3 la riga pubblicata della tabella PIANO-VERO (L3, da ${START:.0f}):") + att = {250.0: (23.4, 22.1, 0.14), 500.0: (17.3, 17.3, 0.85)} + r3 = True + print(f" {'EUR/m':>7}{'med.inc':>10}{'atteso':>9}{'med|arr':>10}{'atteso':>9}" + f"{'P(20a)':>9}{'atteso':>9}") + for dep, (a1, a2, a3) in att.items(): + d = leggi(PN.accumula(paths, dep, muro, CC.TAX_RATE, TX.PATRIMONIALE, + start=START)["colpito"]) + r3 &= abs(d["med"] - a1) < 0.2 and abs(d["p20"] - a3) < 0.02 + print(f" {dep:>7.0f}{d['med']:>10.1f}{a1:>9.1f}{d['med_cond']:>10.1f}{a2:>9.1f}" + f"{d['p20']:>9.1%}{a3:>9.0%}") + ok["R3"] = r3 + + # R4: P0 del mio motore == PN.accumula, bit-exact + A = PN.accumula(paths, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE, start=START) + Bp = accumula_pol(paths, flat, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE, + pol_piatto(), start=START) + dcap = float(np.max(np.abs(A["cap"] - Bp["cap"]))) + dcol = int(np.max(np.abs(A["colpito"] - Bp["colpito"]))) + ok["R4"] = dcap == 0.0 and dcol == 0 and abs(A["versato"] - Bp["versato"]) < 1e-9 + print(f"\n R4 P0 di QUESTO motore (con buffer di cassa) vs `PN.accumula` importata:") + print(f" max|dif| capitale = {dcap:.3e} · max|dif| giorno d'arrivo = {dcol} · " + f"versato identico{'':>3}{'BIT-EXACT' if ok['R4'] else 'DIVERGE'}") + print(f" -> il buffer non introduce nulla: con versamento immediato il motore E' quello pubblicato.") + + print(f"\n ESITO REPLICA: {sum(ok.values())}/{len(ok)} — " + + ", ".join(f"{k}={'OK' if v else 'NO'}" for k, v in ok.items())) + if not all(ok.values()): + raise SystemExit("REPLICA INCOMPLETA: un delta sarebbe indistinguibile da un bug.") + + # ------------------------------------------------------------------ 2 + sez("2) RISOLUZIONE MONTE CARLO — quanto vale la cifra che si pubblica") + print(f"\n {N_PATHS} percorsi, blocchi da {BLOCK} giorni, {ANNI} anni. I confronti fra") + print(" politiche sono APPAIATI (stessi percorsi) quindi la loro varianza e' molto minore") + print(" di quella dei livelli. Misurata su 3 semi:") + mc = [] + for s in SEMI_MC: + rg = np.random.default_rng(s) + i2 = boot_idx(len(r), N_PATHS, ANNI * 365, BLOCK, rg) + p2, f2 = r[i2], flat_src[i2] + b = leggi(accumula_pol(p2, f2, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE, + pol_piatto(), start=START)["colpito"]) + d = leggi(accumula_pol(p2, f2, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE, + pol_dip(0.10), start=START)["colpito"]) + mc.append((s, b["p20"], d["p20"], d["p20"] - b["p20"])) + del p2, f2, i2 + print(f"\n {'seme':>10}{'P0 P(20a)':>12}{'P1 dip 10% P(20a)':>20}{'differenza APPAIATA':>22}") + for s, a, bb, dd in mc: + print(f" {s:>10}{a:>12.1%}{bb:>20.1%}{dd:>22.2%}") + liv = max(x[1] for x in mc) - min(x[1] for x in mc) + dif = max(x[3] for x in mc) - min(x[3] for x in mc) + print(f"\n ampiezza fra semi: LIVELLO {liv:.2%} · DIFFERENZA APPAIATA {dif:.2%}") + print(f" -> si cita la differenza appaiata al punto percentuale; un livello si cita a " + f"{liv:.1%} di rumore. {clock()}") + + # ------------------------------------------------------------------ 3 + sez(f"3) LA TABELLA — 12 celle candidate contro il piatto, stesso flusso di cassa") + print(f""" + Lettura delle colonne. `med` = mediana INCONDIZIONATA degli anni al capitale-rendita + (non-arrivo = +inf, mai -1); `m|arr` = mediana condizionata all'arrivo, che NON si legge mai + senza la sua P accanto; `dP20` = differenza APPAIATA di P(20a) contro P0 sugli stessi + percorsi; `attesa` = giorni medi che un euro passa in cassa prima di entrare nel libro + (legge di Little, L/lambda); `prezzo` = livello di equity medio PONDERATO PER IMPORTO a cui + i soldi sono entrati, in rapporto a quello di P0 — sotto 1.000 la politica ha comprato + davvero piu' in basso. Bersaglio ${muro:,.0f}; traguardo su libro+cassa.""") + R = {} + for dep in (DEP_MAIN, DEP_ALT): + R[dep] = tabella(paths, flat, dep, muro, perp, + f"EUR {dep:.0f}/mese di flusso disponibile {clock()}", start=START) + + # ------------------------------------------------------------------ 4 + sez("4) IL CONTROLLO DEL RITARDO — «il segnale informa» contro «aspettare costa»") + print(""" + Ogni politica condizionale fa DUE cose: sceglie il momento (informazione) e ritarda i soldi + (costo). Il controllo isola la seconda: stessa attesa MEDIA, ma deterministica, nessun + segnale. Se la politica condizionale non batte il proprio ritardo-gemello, non ha + informazione — ha solo aspettato.""") + info = {} + for etichetta, (P, FL, RR) in {"BLOCCHI 20g (mercato vero)": (paths, flat, R[DEP_MAIN]), + "IID blocco=1 (null)": (None, None, None)}.items(): + if P is None: # il null iid: si ricostruiscono i path e le celle + rg = np.random.default_rng(SEED) + i2 = boot_idx(len(r), N_PATHS, ANNI * 365, 1, rg) + P, FL = r[i2], flat_src[i2] + del i2 + RR = {n: accumula_pol(P, FL, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE, + pol, start=START) for n, pol in celle(DEP_MAIN).items()} + print(f"\n {etichetta}") + print(f" {'politica':<16}{'attesa g.':>11}{'P(20a) reale':>14}" + f"{'P(20a) ritardo-gemello':>25}{'informazione':>15}") + col = {} + for nome in ("P1 dip 5%", "P1 dip 10%", "P1 dip 15%", "P1 dip 20%", "P2 risk-off"): + rr = RR[nome] + k = int(round(rr["attesa_g"])) + g = accumula_pol(P, FL, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE, + pol_lag(k, DEP_MAIN), start=START) + a, b = leggi(rr["colpito"])["p20"], leggi(g["colpito"])["p20"] + col[nome] = a - b + print(f" {nome:<16}{k:>11}{a:>14.1%}{b:>25.1%}{a - b:>15.2%}") + info[etichetta] = col + if "IID" in etichetta: + del P, FL, RR + k1 = "BLOCCHI 20g (mercato vero)" + k2 = "IID blocco=1 (null)" + cost15 = (leggi(R[DEP_MAIN]["P1 dip 15%"]["colpito"])["p20"] + - leggi(R[DEP_MAIN]["P0 piatto"]["colpito"])["p20"]) + print(f""" + ⚠️ IL PRIMO SGUARDO DICE «IL SEGNALE INFORMA» — E IL NULL IID LO REFUTA. Sulle celle profonde + la colonna `informazione` e' positiva sul mercato vero ({info[k1]['P1 dip 15%']:+.1%} a dip 15%, + {info[k1]['P1 dip 20%']:+.1%} a dip 20%): a pari attesa MEDIA, aspettare il calo batte aspettare a + caso, e leggendo solo questa riga si concluderebbe che il buy-the-dip contiene previsione. + Ma sotto IID, dove per COSTRUZIONE il passato non dice nulla del futuro, la stessa colonna + vale {info[k2]['P1 dip 15%']:+.1%} / {info[k2]['P1 dip 20%']:+.1%} — cioe' NON PIU' PICCOLA, anzi piu' grande. + -> il divario contro il ritardo-gemello e' MECCANICO, non previsione: la politica + condizionale ha attesa VARIABILE (spesso zero, ogni tanto lunghissima) mentre il gemello ha + attesa FISSA, e versa il buffer tutto in una volta. Un'attesa variabile con la stessa media + di una fissa non produce lo stesso capitale (Jensen), e il null lo mostra senza ambiguita'. + ⚠️ Il gemello non e' un controllo perfetto proprio per questo: e' il controllo del RITARDO + MEDIO, non della sua distribuzione. Il suo compito qui e' fissare l'ordine di grandezza — + il ritardo di dip 15% costa {abs(cost15):.0%} di P(20a) contro un divario col gemello di + {info[k1]['P1 dip 15%']:+.1%}, e sarebbe la conclusione anche prendendo il gemello alla lettera.""") + print(f" {clock()}") + + # ------------------------------------------------------------------ 5 + sez("5) I NULL") + print(""" + (a) BOOTSTRAP IID (blocco = 1 giorno). Distrugge ogni dipendenza seriale: il passato non + informa il futuro per costruzione. Una politica che li' mantiene il suo vantaggio lo + stava prendendo dalla meccanica del bootstrap, non dal mercato. (Il controllo del + ritardo su questo stesso null e' gia' in sezione 4.)""") + rg = np.random.default_rng(SEED) + i_iid = boot_idx(len(r), N_PATHS, ANNI * 365, 1, rg) + p_iid, f_iid = r[i_iid], flat_src[i_iid] + del i_iid + I = tabella(p_iid, f_iid, DEP_MAIN, muro, perp, f"IID (blocco=1) {clock()}", start=START) + del p_iid, f_iid + + print(""" + (c) DRIFT DIMEZZATO — lo stress standard del progetto (7,4 anni di storia, due tori dentro). + Bersaglio tenuto FISSO al muro della lente piena: si stressa l'accumulo, non l'obiettivo + (col muro ricalcolato non arriverebbe nessuno e il confronto perderebbe potenza).""") + r_half = r - 0.5 * r.mean() + rg = np.random.default_rng(SEED) + i_h = boot_idx(len(r_half), N_PATHS, ANNI * 365, BLOCK, rg) + p_h, f_h = r_half[i_h], flat_src[i_h] + del i_h + H = tabella(p_h, f_h, DEP_MAIN, muro, perp, + f"DRIFT DIMEZZATO ({r_half.mean() * 365:.2%}/anno) {clock()}", start=START) + half20 = (leggi(H["P1 dip 20%"]["colpito"])["p20"] + - leggi(H["P0 piatto"]["colpito"])["p20"]) + del p_h, f_h + + # ------------------------------------------------------------------ 6 + sez("6) RISCHIO DI VENUE — una politica che tiene i soldi FUORI e' anche una politica di rischio") + print(""" + Il fatto strutturale: la cassa che aspetta non e' sull'exchange. Se il vantaggio (o il minor + danno) di una politica condizionale venisse da li', sarebbe un risultato sul RISCHIO, non sul + timing. I due effetti si separano leggendo la stessa coppia a p=0 (solo timing) e a p>0. + Convenzione: al fallimento il LIBRO va a zero, la CASSA sopravvive, e si riparte (conto nuovo, + versamenti che continuano) — dichiarata, e diversa da quella di r0726_venue_risk, che nel caso + a un venue solo scartava anche i versamenti futuri. `p` NON e' stimato: e' sensibilita'.""") + print(f"\n {'p annua':>9} {'politica':<16}{'P(20a)':>9}{'dP20 vs P0':>13}" + f"{'P(libro azzerato)':>20}{'EUR/g a 20a':>13}{'effetto rischio':>17}") + d0 = {} + for p in P_VENUE: + h = 1.0 - (1.0 - p) ** (1.0 / 365.0) if p > 0 else 0.0 + F = None + if p > 0: + rgv = np.random.default_rng(726) + F = np.zeros((N_PATHS, ANNI * 365), bool) + tt = rgv.geometric(h, size=N_PATHS) + for _ in range(8): + m = tt < ANNI * 365 + if not m.any(): + break + F[np.where(m)[0], tt[m]] = True + tt = tt + rgv.geometric(h, size=N_PATHS) + base = None + for nome in ("P0 piatto", "P1 dip 10%", "P1 dip 20%", "P4 anti 10%"): + rr = accumula_pol(paths, flat, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE, + celle(DEP_MAIN)[nome], start=START, fail=F) + d = leggi(rr["colpito"]) + if base is None: + base = d["p20"] + rend = float(np.median((rr["cap"] + rr["cash"]) * perp / 365.0 / CC.EURUSD)) + dd = d["p20"] - base + if p == 0.0: + d0[nome] = dd + eff = "" if p == 0.0 else f"{dd - d0[nome]:+.2%}" + print(f" {f'{p:.1%}' if nome.startswith('P0') else '':>9} {nome:<16}" + f"{d['p20']:>9.1%}{dd:>13.2%}{rr['p_perso']:>20.1%}{rend:>13.2f}{eff:>17}") + del F + print(f""" + `effetto rischio` = di quanto il rischio di venue RIDUCE la penalita' della politica + condizionale rispetto a p=0. E' il pezzo di risultato che NON e' timing: la cassa in attesa + non e' sull'exchange, quindi non brucia. Il meccanismo esiste ed e' misurabile, ma vale + qualche punto percentuale contro una penalita' di timing di decine — e la colonna + `P(libro azzerato)` e' IDENTICA per tutte le politiche (il conto salta comunque: cambia + quanto c'e' sopra, non se salta). Chi volesse comprare quella protezione la compra con un + SECONDO CONTO, non ritardando i bonifici (25/07 §4: «un rischio non-diversificabile si compra + con un secondo CONTO, non con un secondo sleeve»).""") + print(f" {clock()}") + + # ------------------------------------------------------------------ 7 + sez("7) SENSIBILITA' — le due convenzioni che potrebbero decidere al posto della misura") + print(f"\n (i) CASSA a 0% (assunzione XEON) contro cassa al 2%/anno nominale:") + print(f" {'politica':<16}{'P(20a) cassa 0%':>18}{'P(20a) cassa 2%':>18}{'delta':>10}") + for nome in ("P0 piatto", "P1 dip 10%", "P1 dip 20%", "P2 risk-off", "P3 VA g=0%"): + a = leggi(R[DEP_MAIN][nome]["colpito"])["p20"] + b = leggi(accumula_pol(paths, flat, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE, + celle(DEP_MAIN)[nome], start=START, + cash_rate=0.02)["colpito"])["p20"] + print(f" {nome:<16}{a:>18.1%}{b:>18.1%}{b - a:>10.2%}") + print(f"\n (ii) TRAGUARDO su libro+cassa (favorevole alle condizionali, usato sopra) contro") + print(f" traguardo su SOLO LIBRO (severo: la cassa a 0% non produce rendita):") + print(f" {'politica':<16}{'P(20a) libro+cassa':>21}{'P(20a) solo libro':>20}{'delta':>10}") + for nome in ("P0 piatto", "P1 dip 10%", "P1 dip 20%", "P2 risk-off", "P3 VA g=0%"): + a = leggi(R[DEP_MAIN][nome]["colpito"])["p20"] + b = leggi(accumula_pol(paths, flat, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE, + celle(DEP_MAIN)[nome], start=START, + traguardo_include_cassa=False)["colpito"])["p20"] + print(f" {nome:<16}{a:>21.1%}{b:>20.1%}{b - a:>10.2%}") + print(f" {clock()}") + + # ------------------------------------------------------------------ 8 + sez("VERDETTO") + P0 = R[DEP_MAIN]["P0 piatto"] + base = leggi(P0["colpito"]) + tab = {n: leggi(v["colpito"]) for n, v in R[DEP_MAIN].items() if n != "P0 piatto"} + best = max(tab, key=lambda k: tab[k]["p20"]) + vere = {n: v for n, v in tab.items() if R[DEP_MAIN][n]["attesa_g"] >= 30.0} + bvera = max(vere, key=lambda k: vere[k]["p20"]) + sopra = sum(1 for n in tab if tab[n]["p20"] > base["p20"]) + dips = [tab[n]["p20"] - base["p20"] for n in tab if n.startswith("P1")] + anti = [tab[n]["p20"] - base["p20"] for n in tab if n.startswith("P4")] + pz = {n: R[DEP_MAIN][n]["prezzo"] / P0["prezzo"] for n in tab} + + def conta(D: dict) -> str: + b = leggi(D["P0 piatto"]["colpito"])["p20"] + dd = {n: leggi(v["colpito"])["p20"] - b for n, v in D.items() if n != "P0 piatto"} + k = max(dd, key=lambda x: dd[x]) + return f"{sum(1 for x in dd.values() if x > 0)}/12 ({k} {dd[k]:+.2%})" + conte = " · ".join(f"{lab} {conta(D)}" for lab, D in + (("EUR500", R[DEP_MAIN]), ("EUR250", R[DEP_ALT]), + ("IID", I), ("drift/2", H))) + d250 = leggi(R[DEP_ALT]["P0 piatto"]["colpito"]) + print(f""" + 1. NESSUNA delle {N_TRIAL} celle candidate batte il versamento piatto: {sopra}/{N_TRIAL} sopra + P0, migliore in assoluto {best} a {tab[best]['p20'] - base['p20']:+.2%} appaiato — dentro la + risoluzione Monte Carlo della differenza appaiata ({dif:.2%}) e su una cella che aspetta + {R[DEP_MAIN][best]['attesa_g']:.0f} giorno, cioe' e' P0 travestito. La migliore cella DAVVERO + condizionale (attesa >= 30 giorni) e' {bvera}: {vere[bvera]['p20'] - base['p20']:+.1%}. + Segno medio: P1 buy-the-dip {np.mean(dips):+.1%} · P4 anti-dip {np.mean(anti):+.1%}. + Per lente (celle STRETTAMENTE sopra P0 su 12, e la migliore): {conte}. + Nessuna lente produce una cella sopra il rumore MC ({dif:.1%}) -> non c'e' niente da + deflazionare: il conteggio dei trial non e' il vincolo, il vincolo e' che il segno e' + negativo e monotono nella profondita' della condizione. + + 2. IL SEGNALE FUNZIONA E PERDE LO STESSO, ed e' la riga che spiega tutto. Colonna `prezzo`: + P1 dip 15% compra al {pz['P1 dip 15%']:.3f} del livello medio di P0 e P1 dip 20% al + {pz['P1 dip 20%']:.3f} — cioe' {1 - pz['P1 dip 20%']:.0%} PIU' IN BASSO, esattamente cio' che la + politica promette. E arriva al capitale-rendita nel {tab['P1 dip 20%']['p20']:.1%} dei casi contro + {base['p20']:.1%}. Comprare meglio e comprare TARDI sono la stessa mossa, e la seconda meta' + domina di un ordine di grandezza. + ⚠️ E il dettaglio che ribalta l'intuizione sulle soglie BASSE: P1 dip 5% compra a + {pz['P1 dip 5%']:.3f}, cioe' PIU' IN ALTO di P0. Su una serie che sale, una condizione poco + profonda non compra il calo: ritarda dentro la salita. Solo le soglie che fanno male + comprano davvero piu' in basso — ed e' esattamente il motivo per cui non conviene. + + 3. P4 ANTI-DIP, il controllo speculare, NON e' simmetrico — e la sua asimmetria e' la misura + del meccanismo: su una serie che sale, un rialzo del 20% dal minimo arriva in + {R[DEP_MAIN]['P4 anti 20%']['attesa_g']:.0f} giorni mentre un ribasso del 20% dal massimo ne chiede + {R[DEP_MAIN]['P1 dip 20%']['attesa_g']:.0f}. Il costo non e' «scommettere sul lato sbagliato»: e' + «mettere una condizione rara davanti a un bonifico». Qualunque condizione rara, in + qualunque direzione, costa. P1 e P4 NON danno lo stesso segno di taglia: se lo dessero + staremmo misurando rumore. + + 4. ORDINE DI GRANDEZZA, che e' il modo in cui questo risultato va usato: la migliore politica + di timing vale {tab[bvera]['p20'] - base['p20']:+.1%} di P(20a) — indistinguibile da zero + sulla risoluzione MC di {dif:.1%}; passare da EUR {DEP_ALT:.0f} a EUR {DEP_MAIN:.0f} al mese vale + {base['p20'] - d250['p20']:+.0%} ({d250['p20']:.0%} -> {base['p20']:.0%}). Non c'e' rapporto da + calcolare fra un effetto misurato e uno che non si distingue dal rumore: la prima colonna + e' una decisione, la seconda no. + + 5. ATTESA A PRIORI (dichiarata prima di misurare): «non funziona, perche' entrare piu' tardi + domina il comprare piu' in basso». CONFERMATA, e col meccanismo misurato invece che + assunto. Cio' che l'attesa NON prevedeva e' il punto 2: il segnale compra davvero molto + piu' in basso — {1 - pz['P1 dip 20%']:.0%} — e perde comunque. Sapere PERCHE' non serve vale piu' + del verdetto: la domanda non tornera' sotto forma di «e se aspettassi un calo piu' + profondo?», perche' e' proprio la profondita' a peggiorare il risultato, in modo monotono + su tutte e 4 le soglie e su tutte e 4 le lenti. + + 6. DUE ERRORI CATTURATI SU ME STESSO, entrambi da un controllo e non a occhio. + (i) La colonna `informazione` di sezione 4 sul solo mercato vero mi avrebbe fatto scrivere + «il buy-the-dip contiene previsione»: il null IID la rende PIU' GRANDE su ritorni dove non + c'e' niente da prevedere -> e' meccanica (attesa variabile contro fissa). Una colonna + positiva non e' un'evidenza finche' non gira anche sul null. + (ii) Senza la colonna `attesa` avrei letto P4 anti-dip come «stesso segno di P1 => rumore» + (regola dichiarata nel mandato). Falso: P4 e' piatto perche' in un mercato che sale la sua + condizione e' quasi sempre gia' vera ({R[DEP_MAIN]['P4 anti 20%']['attesa_g']:.0f} giorni di attesa contro + {R[DEP_MAIN]['P1 dip 20%']['attesa_g']:.0f}), cioe' e' P0 travestito, non uno specchio informativo. + Lo specchio va letto sulla colonna dell'ATTESA, non su quella del segno. + + COSA QUESTO NON MISURA: politiche che cambiano il FLUSSO disponibile (versare di piu' quando + si guadagna di piu' e' un'altra domanda, ed e' fuori dal controllo del libro); orizzonti + diversi da 25 anni; il caso in cui la cassa in attesa renda davvero qualcosa (sez. 7 (i): + al 2% nominale la penalita' si riduce di ~4pp e non cambia segno); e il fatto che la finestra + storica del libro sono 7,4 anni con due tori — su un campione con un orso lungo il costo del + ritardo sarebbe minore (lo si vede nella colonna DRIFT DIMEZZATO: la penalita' scende da + {tab['P1 dip 20%']['p20'] - base['p20']:+.0%} a {half20:+.0%}, ma resta negativa a tutte e 12 le celle). + """) + print(f" fine {clock()}") + + +if __name__ == "__main__": + main() From 824531b5ba7130f886daf2a64844ee93739317df Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 01:06:34 +0000 Subject: [PATCH 066/114] =?UTF-8?q?registro=20=C2=A744:=20DEPOSIT-TIMING?= =?UTF-8?q?=20SCARTATO=200/12=20=E2=80=94=20il=20buy-the-dip=20compra=20il?= =?UTF-8?q?=2051%=20piu'=20in=20basso=20e=20arriva=20nel=206,3%=20dei=20ca?= =?UTF-8?q?si?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 78 ++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index bc6882a..ffbad5f 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -709,7 +709,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 41 | TP01-SIZE | una size che risponde alla CONVINZIONE batte il vol-target uniforme, a pari vol realizzata? | **SCARTATO** — il meglio di 60 celle vale **+0,30pp** di CAGR iso-vol, e l'ottimo sta ai **due estremi opposti** nelle due meta' del campione (**Spearman ShIS/ShHOLD −0,527**: scegliere in-sample e' peggio di una moneta) | | 42 | TP01-LS | la gamba short del trend a 1d paga il proprio costo, o e' il 2022? | **SCARTATO** — toglie **−1,91%/anno di drift in 0/24 ancore**, fallisce l'iso-vol, +8,80pp di maxDD in 24/24, e **senza il 2022 il divario RADDOPPIA** | | 43 | DATA-UNUSED | quali dati il progetto possiede e nessuna strategia legge — e uno di essi sostiene un'ipotesi? | _in corso_ | -| 44 | DEPOSIT-TIMING | un calendario di versamento CONDIZIONALE batte il piatto, a pari soldi? | _in corso_ | +| 44 | DEPOSIT-TIMING | un calendario di versamento CONDIZIONALE batte il piatto, a pari soldi? | **SCARTATO 0/12 in ogni lente** — il buy-the-dip compra davvero il **51% piu' in basso** e arriva al capitale-rendita nel **6,3%** dei casi contro l'**85,4%** del piatto | | 45 | SPOT-NETTING | separare TP01 e SKH01 su due strumenti: quanto vale, e il margine lo uccide? | _in corso_ | | — | **NOTIFIER** (audit di fatto) | 🚨 **PUNTO SINGOLO DI GUASTO nella rete di sicurezza** | il `send()` di Telegram e' **un solo tentativo da 10s, senza retry, con l'esito MAI registrato** — e lo stato `alerted=True` viene scritto **prima** dell'invio: un 🚨 di `venue_watch` perso e' perso **per l'intero episodio**. Tasso di fallimento **misurato 6,9%** (2/29, IC95 [1,9%, 22,0%]) | | 22 | MAKER | l'esecuzione passiva e' una fonte di ritorno, al netto del costo di non essere eseguiti? | **SCARTATO** — il segno dipende da `<` contro `<=` | @@ -2309,3 +2309,79 @@ di SKH01 (si cancella nella differenza appaiata); una convinzione da un **segnal **VERDETTO: `NON ESISTE UNA CELLA PROPONIBILE — E IL MOTIVO E' CHE IN QUESTA FAMIGLIA LA SELEZIONE IN-SAMPLE E' ANTI-CORRELATA COL FUTURO`** — SCARTATO. **Libro, pesi, cron, config INVARIATI.** + +--- + +## 44 — DEPOSIT-TIMING (il timing dei bonifici) + +`scripts/research/r0823_deposit_timing.py` (**81 s**, nessuna rete). **3.000 percorsi**, block +bootstrap a 20 giorni, 25 anni, lente **L3 CONGIUNTA** (funding + fisco d'accumulo + ancora ×0,89), +bersaglio **$313k**. **Risoluzione MC misurata su 3 semi: 1,13 pp sui LIVELLI, 1,97 pp sulle +differenze APPAIATE** (i confronti girano sugli stessi percorsi) → si cita al punto percentuale. + +✅ **Replica 4/4 prima di ogni delta:** muro **$313.143** contro $313k; €250/m **23,4a / 14,4%**; +€500/m **17,3a / 85,4%**; e soprattutto **P0 del motore nuovo vs `PN.accumula`: `max|dif| = 0.0`** +sul capitale **e 0 sul giorno d'arrivo**; `boot_idx` bit-exact contro `CC._boot_paths`. + +**Attesa a priori, dichiarata prima di misurare:** *"non funziona, perche' entrare piu' tardi domina +il comprare piu' in basso"*. **Confermata** — ma il meccanismo vale piu' del verdetto. + +**€500/mese, stesso flusso di cassa disponibile per tutte le politiche:** + +| politica | P(20a) | dP20 appaiato | attesa (g) | prezzo d'ingresso vs P0 | +|---|---|---|---|---| +| **P0 piatto** | **85,4%** | — | 0 | 1,000 | +| P1 dip 5% | 84,0% | −1,4% | 95 | **1,009** | +| P1 dip 10% | 69,7% | −15,7% | 713 | 0,959 | +| P1 dip 15% | 27,7% | −57,7% | 2.254 | 0,718 | +| P1 dip 20% | **6,3%** | **−79,1%** | 3.682 | **0,494** | +| P2 risk-off (TP01 a 0x) | 84,8% | −0,6% | 22 | 1,002 | +| P3 valore-medio g=0 / 8 / 15% | 48,2 / 83,5 / 85,3% | −37,2 / −1,9 / −0,1% | 2.489 / 789 / 32 | 0,220 / 0,560 / 0,997 | +| P4 anti-dip 5-20% | 85,4 → 84,7% | −0,0 → −0,7% | 1 → 16 | ~1,000 | + +**0/12 celle in OGNI lente** (€500/m, €250/m, IID, drift dimezzato). Il migliore in assoluto vale +−0,03% ed e' P4 anti-5%, che aspetta **1 giorno** = P0 travestito. **Niente da deflazionare:** il +conteggio dei trial non e' il vincolo, il segno e' negativo e **monotono nella profondita' della +condizione**. + +📌 **IL MECCANISMO — il segnale FA quello che promette, e perde lo stesso.** Dip 20% compra al +**0,494** del livello medio del piatto, cioe' **il 51% piu' in basso**, e arriva nel 6,3% dei casi: +*comprare meglio e comprare tardi sono la stessa mossa*. E sotto soglia bassa **si ribalta** — dip 5% +compra a **1,009**, cioe' **piu' in alto**: su una serie che sale, una condizione poco profonda non +compra il calo, **ritarda dentro la salita**. + +📌 **L'ASIMMETRIA E' LA MISURA.** P4 anti-dip **non** da' lo stesso segno di taglia di P1 (−0,3% +medio contro −38,5%) → non stiamo misurando rumore. Ma non e' simmetrico, e il perche' e' il +risultato: **un rialzo del 20% dal minimo arriva in 16 giorni, un ribasso del 20% dal massimo in +3.682**. *Il costo non e' scommettere sul lato sbagliato: e' mettere una **condizione rara** davanti +a un bonifico, in qualunque direzione.* + +**Rischio di venue, separato dal timing** (convenzione dichiarata: al fallimento il libro va a zero, +**la cassa sopravvive**, si riparte). Il meccanismo esiste — tenere i soldi fuori riduce la penalita' +di **+2,4 pp a p=1%** e **+3,8 pp a p=2%** su dip 10% — ma contro una penalita' di timing di +**−15,7 pp**, ed e' **un ordine di grandezza troppo piccolo**. E `P(libro azzerato)` e' **identica +per tutte le politiche** (21,6% a p=1%): *il conto salta comunque, cambia quanto c'e' sopra.* La +protezione si compra con un **secondo conto**, non ritardando i bonifici — coerente col 27/07. + +⚠️ **Due errori catturati dall'agente su se stesso, entrambi da un controllo e non a occhio.** +(i) Il **ritardo-gemello** (stessa attesa media, deterministica, nessun segnale) dava "informazione" +**+11,5%** a dip 15% sul mercato vero: leggendo solo quella riga avrebbe scritto che il buy-the-dip +contiene previsione. Sotto **IID**, dove per costruzione non c'e' nulla da prevedere, la stessa +colonna vale **+14,9%** — **non piu' piccola, piu' grande** → il divario e' **meccanico**, non +previsione. (ii) Senza la colonna **attesa** avrebbe letto P4 come *"stesso segno di P1 ⇒ rumore"* +(la regola del mandato): falso, P4 e' piatto perche' la sua condizione e' **quasi sempre gia' vera**. +**Lo specchio si legge sulla colonna dell'attesa, non su quella del segno.** + +**Sensibilita':** cassa al 2% invece che 0% riduce la penalita' di ~4 pp e **non cambia segno**; +traguardo su solo-libro (severo) peggiora ancora le condizionali → la convenzione usata e' quella +**favorevole** alle politiche condizionali, scelta apposta. **Non misurato:** politiche che cambiano +il *flusso disponibile* (altra domanda, fuori dal controllo del libro); orizzonti diversi da 25 anni; +e la finestra resta 7,4 anni con due tori. + +📌 **ORDINE DI GRANDEZZA, che e' come va usato il risultato: la migliore politica di timing vale +−0,1 pp di P(20a), dentro il rumore MC di 2,0 pp; passare da €250 a €500 al mese vale +71 pp +(14% → 85%).** + +**VERDETTO: `IL TIMING DEI BONIFICI NON PAGA IN NESSUNA LENTE — E IL SEGNALE FUNZIONA, E' LA +CONDIZIONE RARA A COSTARE`** — SCARTATO. Chiude una domanda che l'operatore potrebbe altrimenti +riporsi ogni mese per vent'anni. **Libro, pesi, cron, config INVARIATI.** From 696f2e5bdd9013ded658d8a3f7fe06d867f207e2 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 01:10:52 +0000 Subject: [PATCH 067/114] =?UTF-8?q?research(=C2=A743=20DATA-UNUSED):=20inv?= =?UTF-8?q?entario=20dei=20dati=20mai=20letti=20+=20l'unica=20ipotesi=20ch?= =?UTF-8?q?e=20ne=20esce,=20misurata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FASE 1 — censimento di data/ e di CHI lo legge (indice di 647 .py, Old/ escluso). Colonne MAI lette: 26 su 31 di CoinMetrics (fra cui FlowIn/FlowOutExNtv, 2018-2026 al 100%), `fetch_errors_json`, `iv_90d`. Tutto il resto con storia vera e' gia' stato analizzato: cio' che resta non letto e' telemetria a finestra corta (catena opzioni 113 g, 0DTE 30 g, vol_term 58 righe). Corretti in sessione due difetti dell'inventario stesso: la ricerca a SOTTOSTRINGA dava `iv` in 571 file, e le chiavi derivate dall'etichetta producevano due falsi "NESSUNO" su famiglie realmente lette (eqx_, fut_deribit/fund_). FASE 2 — EXFLOW: il turnover LORDO sugli exchange. SCARTATO. La famiglia exchange-flow fu uccisa il 24/07 sullo STOCK (SplyExNtv). Misurato prima di riaprirla: `In-Out` e' la derivata dello stock (corr +0.999 BTC / +0.752 ETH) mentre `In+Out` e' ortogonale (+0.035 / +0.080) — il 94% del flusso si cancella, quindi il lordo e' algebricamente assente da cio' che era stato provato. Griglia dichiarata prima: 3 variabili x 3 finestre x 2 modi x 2 segni = 36 celle, +4 gia' spese su EXS = 40 trial. (1) La cella scelta al buio fa Sharpe FULL 0.951 contro un massimo atteso dal PURO RUMORE di 1.009: sotto la soglia che una griglia di monete raggiunge da sola (DSR 0.437 a N=36, 0.411 a N=40). (2) La selezione in-sample torna sul CONTROLLO NEGATIVO messo apposta nella griglia (NETCTL = l'informazione gia' uccisa): il lordo non e' selezionabile, hold-out -0.42, DILUTES. (3) Zero informazione direzionale (|t|<1 su 2 asset x 2 variabili). Il legame col |ritorno| su ETH (Pearson incrementale t +2.70) non sopravvive al rango (Spearman p=0.46) e cambia segno per anno; su BTC e' "significativo" nel verso sbagliato. Sottoprodotti: corr->TP01 0.50-0.65 = replica indipendente del "l'on-chain e' prezzo travestito" del 24/07 su colonne che quell'ondata non aveva letto; haircut 0.000 a $635. Corretti prima di pubblicare due errori miei: il pool "N=12" del deflated-Sharpe erano le 12 celle MIGLIORI (rows e' ordinata) e faceva PASSARE il candidato a 0.975 — con la partizione legittima fa 0.678; e il residuo di vol calcolato a mano invece che per OLS ribaltava il segno su ETH. Solo ricerca: nessuna scrittura, nessun ordine, book/pesi/cron/config INVARIATI. --- scripts/research/r0823_data_unused.py | 594 ++++++++++++++++++++++++++ 1 file changed, 594 insertions(+) create mode 100644 scripts/research/r0823_data_unused.py diff --git a/scripts/research/r0823_data_unused.py b/scripts/research/r0823_data_unused.py new file mode 100644 index 0000000..53544c4 --- /dev/null +++ b/scripts/research/r0823_data_unused.py @@ -0,0 +1,594 @@ +"""r0823_data_unused — §43 DATA-UNUSED: i dati che il progetto possiede e nessuno legge (2026-08-23). + +DOMANDA (ondata `research/wave-0822`, filone 43): *"nuove strategie possibili"*. La fonte piu' +probabile di una strategia nuova non e' un'idea nuova sui soliti dati: e' un dato che nessuno ha +ancora guardato. Due fasi, la seconda dipende dalla prima. + +FASE 1 — INVENTARIO (e' gia' un risultato, non un preambolo). + Censisce TUTTO cio' che sta su disco sotto `data/` e, per ogni dataset a schema ricco, quale + COLONNA e' letta da `src/` o da uno script di ricerca e quale non lo e' MAI. La lettura non e' + greppata a mano: si indicizzano una volta tutti i `.py` di src/ scripts/ tests/ (escluso `Old/`, + che e' archivio) e si cerca il nome della colonna come sottostringa. E' una misura GENEROSA verso + "letta" (un nome comune come `close` combacia ovunque) -> quando dice MAI LETTA, e' un fatto. + +FASE 2 — UNA misura, la migliore. + Dall'inventario esce un solo candidato che soddisfa tutti e quattro i criteri richiesti: + (i) piu' storia utilizzabile, (ii) meccanismo economico dicibile in una frase, (iii) eseguibile + su Deribit a $635, (iv) non gia' chiuso dal progetto. E' `FlowInExNtv`/`FlowOutExNtv` di + CoinMetrics (`data/external/coinmetrics/cm_{btc,eth}.csv`): **0 file del repo li leggono**, + copertura 100% dal 2018, entrambi gli asset. + + ⚠️ IL PUNTO DELICATO, DICHIARATO IN TESTA. La famiglia "exchange-flow" e' stata UCCISA il + 2026-07-24 (`r0724_onchain_wave.py`, famiglia EXS, 0/6 slot). Riaprirla richiede un meccanismo + NUOVO, non un secondo tentativo. Il meccanismo nuovo qui e' ARITMETICO e si MISURA prima di + usarlo (blocco 1): + - EXS ha testato `SplyExNtv`, lo STOCK di monete sugli exchange; + - `FlowIn - FlowOut` e' la DERIVATA di quello stock -> stessa informazione (lo si misura: + corr con d(SplyEx) attesa ~1); + - `FlowIn + FlowOut`, il turnover LORDO, si CANCELLA nella differenza -> e' algebricamente + ASSENTE da cio' che e' stato testato (corr con d(SplyEx) attesa ~0). + Quindi la parte onestamente nuova del dato e' il LORDO, non il netto. Meccanismo in una frase: + *il turnover lordo sugli exchange misura quanta parte del flottante viene ri-posizionata, cioe' + la partecipazione REGOLATA, a prescindere dalla direzione in cui si agisce.* + + E siccome "riaprire un parametro riapre la sua famiglia" (regola 2026-07-30), il conteggio dei + trial e' fatto AL RIALZO: 36 celle nuove, piu' le 4 gia' spese su EXS il 24/07 -> il deflated + Sharpe si riporta a N=36 E a N=40, e si pubblica la sensibilita' del verdetto al conteggio. + + Controllo positivo obbligatorio: la famiglia UCCISA viene ri-girata con la SUA factory + (`r0724_onchain_wave.exs_factory`, importata, non riscritta). Se il macchinario non riproduce + "earns_slot=False" sul caso noto, non e' credibile su quello nuovo. + +CAUSALITA'. Riusa `r0724_onchain_wave._to_target` e `AVAIL_LAG_D=1` -> lag totale attivita' -> +posizione = 2 giorni (1 di disponibilita' del dato + 1 di `eval_weights`). Non e' una scelta di +oggi: e' la convenzione gia' congelata per questa fonte. + +FINESTRA IN BARRE ATTIVE, non di calendario. Il CSV CoinMetrics community si ferma al 2026-05-24: +dopo quella data il segnale e' assente e la posizione e' FLAT. Contare quei giorni come evidenza +sarebbe l'errore gia' pagato dal progetto (94% di zeri letti come "nessuna perdita"). Qui la +finestra si riporta in barre con posizione != 0. + +COSA NON FA: non manda ordini, non scrive nulla, non tocca `src/` `config/` `scripts/live/` +`data/paper_*`. Sola lettura. + +Uso: nice -n 19 timeout 900 uv run python scripts/research/r0823_data_unused.py + (aggiungere --skip-inventory per la sola FASE 2) +""" +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path + +import numpy as np +import pandas as pd +from scipy import stats + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +import altlib as al # noqa: E402 +from altlib import (causality_ok, deflated_sharpe, eval_weights_smallcap, # noqa: E402 + fmt_marginal, implausible_sharpe, study_family_honest) +import r0724_onchain_wave as ow # noqa: E402 (loader + causalita' + factory della famiglia uccisa) + +DATA = ROOT / "data" +RAW = DATA / "raw" +LINE = "=" * 100 + + +# =========================================================================== +# FASE 1 — INVENTARIO +# =========================================================================== +def _index_repo() -> dict[str, str]: + """Tutti i .py di src/ scripts/ tests/ in memoria una volta sola. `Old/` e' ARCHIVIO + (il progetto lo dichiara non fidato) -> escluso: un dato letto solo li' NON e' letto.""" + idx = {} + for d in ("src", "scripts", "tests"): + for p in (ROOT / d).rglob("*.py"): + if "Old/" in str(p): + continue + try: + idx[str(p.relative_to(ROOT))] = p.read_text(errors="ignore") + except OSError: + pass + return idx + + +def _readers(idx: dict[str, str], needle: str, exclude_self: bool = True, + word: bool = True) -> list[str]: + """`word=True` -> confini di parola. La prima stesura cercava la SOTTOSTRINGA e dava + `iv` in 571 file (e' dentro "drive", "receive", ...): un conteggio che non misura niente. + Resta comunque una misura GENEROSA verso "letta" (una variabile locale omonima conta) -> + e' quando dice MAI LETTA che l'affermazione e' forte.""" + pat = re.compile(rf"(? tuple[int, list[str], str]: + try: + d = pd.read_parquet(p) + except Exception as e: # noqa: BLE001 + return 0, [f""], "" + rng = "" + for c in d.columns: + if pd.api.types.is_datetime64_any_dtype(d[c]): + rng = f"{pd.Timestamp(d[c].min()).date()}->{pd.Timestamp(d[c].max()).date()}" + break + else: + if isinstance(d.index, pd.DatetimeIndex) and len(d): + rng = f"{d.index.min().date()}->{d.index.max().date()}" + elif "timestamp" in d.columns and len(d): + t = pd.to_datetime(d["timestamp"], unit="ms", utc=True) + rng = f"{t.min().date()}->{t.max().date()}" + return len(d), list(d.columns), rng + + +# famiglie di file: (etichetta, glob, campione su cui leggere lo schema, CHIAVE di ricerca). +# La chiave e' ESPLICITA e non derivata dall'etichetta: derivandola, due famiglie realmente +# lette (`eqx_`, `fund_`) risultavano "NESSUNO" — un falso negativo che avrebbe fabbricato +# un dato "mai usato" inesistente. +FAMILIES = [ + ("btc/eth certificati 5m/15m/1h", "btc_*.parquet|eth_*.parquet", "btc_1h.parquet", ("load_data", "altlib")), + ("alt_sol_* (SOL, escluso 22/08)", "alt_sol_*.parquet", "alt_sol_1h.parquet", ("alt_sol",)), + ("hl_*_1d (51 alt Hyperliquid)", "hl_*_1d.parquet", "hl_btc_1d.parquet", ("hl_{",)), + ("hlfund_*_1h (19 major)", "hlfund_*_1h.parquet", "hlfund_btc_1h.parquet", ("hlfund_{",)), + ("eq_*_1d (29 ETF USA)", "eq_*_1d.parquet", "eq_spy_1d.parquet", ("eq_{", 'eq_*.parquet')), + ("eqx_*_1d (12 UCITS)", "eqx_*_1d.parquet", "eqx_vuaa_1d.parquet", ("eqx",)), + ("fut_*_1h (7 futures indice)", "fut_*_1h.parquet", "fut_es_1h.parquet", ("fut_{",)), + ("fut_deribit/ (70 datati scaduti)", "fut_deribit/*-*.parquet", "fut_deribit/BTC-27DEC24.parquet", ("fut_deribit",)), + ("fut_deribit/fund_* (funding 1h)", "fut_deribit/fund_*.parquet", "fut_deribit/fund_BTC.parquet", ("fut_deribit",)), + ("dvol_* (vol implicita)", "dvol_*.parquet", "dvol_btc.parquet", ("dvol_{",)), + ("vol_term_* (term structure)", "vol_term_*.parquet", "vol_term_btc.parquet", ("vol_term_{",)), + ("fundnews_short_screen", "fundnews_*.parquet", "fundnews_short_screen.parquet", ("fundnews_",)), + ("cb_chain/ (catena opzioni oraria)", "cb_chain/*.parquet", "cb_chain/bite_archive.parquet", ("cb_chain",)), + ("cb_market_snapshots", "cb_market_snapshots.parquet", "cb_market_snapshots.parquet", ("cb_market_snapshots",)), +] + +# dataset a schema RICCO: qui si scende a livello di COLONNA +RICH = [ + ("cb_chain/bite_archive.parquet", RAW / "cb_chain" / "bite_archive.parquet"), + ("cb_market_snapshots.parquet", RAW / "cb_market_snapshots.parquet"), + ("vol_term_btc.parquet", RAW / "vol_term_btc.parquet"), + ("hlfund_btc_1h.parquet", RAW / "hlfund_btc_1h.parquet"), +] + +NON_PARQUET = [ + ("external/coinmetrics/cm_{btc,eth}.csv", DATA / "external/coinmetrics/cm_btc.csv", "cm_btc"), + ("external/coinmetrics/fng.json", DATA / "external/coinmetrics/fng.json", "fng.json"), + ("external/coinmetrics/stables.json", DATA / "external/coinmetrics/stables.json", "stables.json"), + ("external/premium/{cb,upbit}_*.csv + usdkrw", DATA / "external/premium/cb_btc.csv", "usdkrw"), + ("external/stable_snapshots/snapshots.jsonl", DATA / "external/stable_snapshots/snapshots.jsonl", "stable_snapshots"), + ("external/hlp_deepdive/ + hlp_vault.json", DATA / "external/hlp_vault.json", "hlp_"), + ("options_daily/snapshots.jsonl (0DTE)", DATA / "options_daily/snapshots.jsonl", "options_daily"), + ("chain_collect/runs.jsonl (battito)", DATA / "chain_collect/runs.jsonl", "chain_collect"), + ("instruments_registry.json", DATA / "instruments_registry.json", "instruments_registry"), +] + + +def fase1() -> None: + print(LINE) + print(" FASE 1 — INVENTARIO: cosa c'e' su disco e CHI lo legge") + print(LINE) + idx = _index_repo() + print(f" indice repo: {len(idx)} file .py in src/ scripts/ tests/ (Old/ escluso = archivio)\n") + + print(" (A) FAMIGLIE DI FILE") + print(f" {'dataset':36s} {'file':>5s} {'righe*':>10s} {'intervallo':22s} lettori (script che aprono il path)") + print(" " + "-" * 130) + for label, pattern, sample, keys in FAMILIES: + files: list[Path] = [] + for pat in pattern.split("|"): + files += sorted(RAW.glob(pat)) + if not files: + print(f" {label:36s} {'0':>5s} ASSENTE") + continue + n, cols, rng = _pq_info(RAW / sample) + # lettori: chi cita il MODO in cui quei path si costruiscono (chiavi esplicite: derivarle + # dall'etichetta dava 2 falsi "NESSUNO" su famiglie realmente lette). Colonna INDICATIVA: + # i due fut_deribit condividono l'accessore (FUT_DIR) e non si distinguono. + rd = sorted({f for k in keys for f in _readers(idx, k, word=False)}) + rdl = ", ".join(Path(x).name for x in rd[:3]) + (f" (+{len(rd)-3})" if len(rd) > 3 else "") + print(f" {label:36s} {len(files):5d} {n:10,d} {rng:22s} {rdl if rd else '*** NESSUNO ***'}") + print(f" {'':36s} {'':5s} {'':10s} cols: {', '.join(cols[:12])}{' ...' if len(cols) > 12 else ''}") + print(" * righe = del file CAMPIONE, non della famiglia intera.\n") + + print(" (B) COLONNE, dataset per dataset — 'MAI LETTA' = zero file in src/ scripts/ tests/") + for label, p in RICH: + if not p.exists(): + continue + n, cols, rng = _pq_info(p) + print(f"\n --- {label} ({n:,} righe, {rng})") + for c in cols: + rd = _readers(idx, c) + if not rd: + print(f" {c:26s} *** MAI LETTA ***") + else: + print(f" {c:26s} {len(rd):3d} file ({', '.join(Path(x).name for x in rd[:3])})") + + print("\n --- external/coinmetrics/cm_{btc,eth}.csv (31 colonne, daily dal 2009/2015)") + cm = pd.read_csv(DATA / "external/coinmetrics/cm_btc.csv", low_memory=False) + mai, lette = [], [] + for c in cm.columns: + if c == "time": + continue + (lette if _readers(idx, c) else mai).append(c) + print(f" LETTE ({len(lette)}): {', '.join(lette)}") + print(f" *** MAI LETTE ({len(mai)}) ***: {', '.join(mai)}") + + print("\n (C) SORGENTI NON-PARQUET") + for label, p, key in NON_PARQUET: + if not p.exists(): + print(f" {label:44s} ASSENTE") + continue + sz = p.stat().st_size + rd = _readers(idx, key, word=False) + print(f" {label:44s} {sz/1e6:7.2f} MB lettori: " + f"{', '.join(Path(x).name for x in rd[:3]) if rd else '*** NESSUNO ***'}") + + print("\n (D) DIRECTORY DI STATO (non sono sorgenti di segnale — elencate per completezza)") + for d in sorted(DATA.iterdir()): + if not d.is_dir() or d.name in ("raw", "external"): + continue + nf = sum(1 for _ in d.rglob("*") if _.is_file()) + sz = sum(f.stat().st_size for f in d.rglob("*") if f.is_file()) + print(f" data/{d.name:22s} {nf:4d} file {sz/1e6:8.2f} MB") + + +# =========================================================================== +# FASE 2 — LA MISURA +# =========================================================================== +# ---- il dato mai letto ----------------------------------------------------- +def _flows(asset: str) -> tuple[pd.Series, pd.Series]: + """FlowInExNtv / FlowOutExNtv: monete che ENTRANO e che ESCONO dagli exchange, in unita' + native, giornaliere. Zero file del repo le leggono (verificato in FASE 1).""" + cm = ow._CM[asset] + I = cm["FlowInExNtv"].astype(float) + O = cm["FlowOutExNtv"].astype(float) + return I.where(I > 0), O.where(O > 0) + + +def _z(s: pd.Series, W: int) -> pd.Series: + mp = max(20, W // 2) + return (s - s.rolling(W, min_periods=mp).mean()) / s.rolling(W, min_periods=mp).std() + + +# ---- le tre variabili (segno a priori dichiarato nel docstring) ------------ +def gross_factory(tf: str, W: int = 90, mode: str = "LF", sign: int = 1): + """GROSS — turnover lordo `log(In+Out)`, z-score causale su W. LA PARTE NUOVA: si cancella + nella differenza, quindi lo STOCK gia' testato non la contiene. Segno a priori +1 + (partecipazione alta = mercato vivo).""" + def fn(df, asset): + I, O = _flows(asset) + z = _z(np.log(I + O), W) * sign + pos = np.sign(z) if mode == "LS" else (z > 0).astype(float) + return ow._to_target(df, pos) + return fn + + +def imb_factory(tf: str, W: int = 90, mode: str = "LF", sign: int = 1): + """IMB — squilibrio normalizzato `(Out-In)/(Out+In)`, z-score su W. Netto RISCALATO dal + lordo: meta' informazione nuova, meta' del vecchio. Segno a priori +1 (deflussi = accumulo).""" + def fn(df, asset): + I, O = _flows(asset) + z = _z((O - I) / (O + I), W) * sign + pos = np.sign(z) if mode == "LS" else (z > 0).astype(float) + return ow._to_target(df, pos) + return fn + + +def netctl_factory(tf: str, W: int = 90, mode: str = "LF", sign: int = 1): + """NETCTL — CONTROLLO NEGATIVO: deflusso netto cumulato su W diviso lo stock. E' la stessa + informazione di `SplyExNtv`, cioe' della famiglia gia' UCCISA. Sta nella griglia apposta: + se la selezione in-sample lo preferisce alle due variabili nuove, la risposta e' che il + dataset non contiene nient'altro di selezionabile.""" + def fn(df, asset): + I, O = _flows(asset) + S = ow._CM[asset]["SplyExNtv"].astype(float) + S = S.where(S > 0) + z = (((O - I).rolling(W, min_periods=max(20, W // 2)).sum()) / S) * sign + pos = np.sign(z) if mode == "LS" else (z > 0).astype(float) + return ow._to_target(df, pos) + return fn + + +VARS = {"GROSS": gross_factory, "IMB": imb_factory, "NETCTL": netctl_factory} + +# ---- GRIGLIA DICHIARATA PRIMA DI GUARDARE QUALSIASI SHARPE ---------------- +WINDOWS = (30, 90, 180) +MODES = ("LF", "LS") +SIGNS = (1, -1) +GRID_1VAR = [dict(W=w, mode=m, sign=s) for w in WINDOWS for m in MODES for s in SIGNS] # 12 +GRID_FULL = [dict(var=v, **p) for v in VARS for p in GRID_1VAR] # 36 +EXS_GRID_SPESA = [dict(L=30, mode="LF"), dict(L=90, mode="LF"), + dict(L=30, mode="LS"), dict(L=90, mode="LS")] # 4, gia' spese 24/07 + + +def family_factory(tf: str, var: str = "GROSS", **p): + return VARS[var](tf=tf, **p) + + +def _active_bars(fn, asset: str) -> tuple[int, int, int, int]: + """barre con posizione != 0, FULL e HOLD-OUT. Il CSV CoinMetrics si ferma al 2026-05-24: + dopo, posizione 0 per assenza di dato -> non e' evidenza, e non va contata come tale.""" + df = al.get(asset, "1d") + t = np.nan_to_num(np.asarray(al._call_target(fn, df, asset), float)) + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + act = np.abs(t) > 0 + h = idx >= al.HOLDOUT + return int(act.sum()), len(t), int((act & h).sum()), int(h.sum()) + + +def fase2() -> None: + print("\n" + LINE) + print(" FASE 2 — EXFLOW: il turnover LORDO sugli exchange (colonne mai lette)") + print(LINE) + + # ---------------------------------------------------------------- 0. perche' questa + print(""" + PERCHE' QUESTA E NON UN'ALTRA (i quattro criteri, applicati all'inventario): + (i) storia — FlowIn/FlowOut coprono 2018->2026 al 100% su ENTRAMBI gli asset. Le altre + colonne mai lette hanno finestre di mesi: `volume_24h` della catena (113 g), + `book_depth_top3` (113 g), `options_daily` 0DTE (30 g), `iv_90d` (58 righe), + `fetch_errors_json` (diagnostica, non segnale). + (ii) meccanismo — il turnover lordo misura quanta parte del flottante viene ri-posizionata. + (iii) eseguibile — segnale giornaliero -> long-flat/long-short BTC+ETH sui perp Deribit: + esattamente l'impianto di TP01. Verificato sotto a $635 con min-order $5. + (iv) non chiusa — le due colonne non sono lette da NESSUN file. Ma la FAMIGLIA (exchange-flow) + e' stata uccisa il 24/07 sullo STOCK -> serve un meccanismo nuovo, misurato + al blocco 1, e i trial vanno contati AL RIALZO (blocco 5).""") + + # ---------------------------------------------------------------- 1. il fatto strutturale + print("\n" + "-" * 100) + print(" 1. IL FATTO CHE GIUSTIFICA (o no) LA RIAPERTURA — si misura, non si asserisce") + print("-" * 100) + print(" EXS (24/07) ha testato SplyExNtv = lo STOCK. d(stock) = In - Out = il NETTO.") + print(" Se corr(d stock, netto) ~ 1 e corr(d stock, lordo) ~ 0, allora il LORDO e'") + print(" algebricamente assente da cio' che e' stato provato — ed e' l'unica parte nuova.\n") + for a in ("BTC", "ETH"): + I, O = _flows(a) + S = ow._CM[a]["SplyExNtv"].astype(float).where(lambda s: s > 0) + m = I.notna() & O.notna() & S.notna() + dS, net, gross = S[m].diff(), (I - O)[m], (I + O)[m] + ok = dS.notna() + zg = _z(np.log(gross), 180) + mom = np.log(S[m]) - np.log(S[m]).shift(30) + ratio = (net.abs() / gross) + big = gross[zg > 3] + print(f" {a} n={int(m.sum()):,} {I[m].index[0].date()} -> {I[m].index[-1].date()}") + print(f" corr( d(SplyEx) , In-Out ) = {dS[ok].corr(net[ok]):+.4f} <- il NETTO E' la derivata dello stock") + print(f" corr( d(SplyEx) , In+Out ) = {dS[ok].corr(gross[ok]):+.4f} <- il LORDO no") + print(f" corr( z(lordo) , momentum-30g dello stock ) = {zg.corr(mom):+.4f}") + print(f" |In-Out| / (In+Out) mediano = {float(ratio.median()):.3f} -> il {100*(1-float(ratio.median())):.0f}% del") + print(" flusso lordo SI CANCELLA: lo stock vede sei monete su cento fra quelle che si muovono.") + if len(big): + print(f" (nei {len(big)} giorni a z(lordo)>3 il rapporto resta {float(ratio[zg > 3].median()):.3f})") + + # ---------------------------------------------------------------- 2. griglia dichiarata + print("\n" + "-" * 100) + print(" 2. GRIGLIA DICHIARATA PRIMA DI GUARDARE, E CONTATA AL RIALZO") + print("-" * 100) + print(f" variabili {tuple(VARS)} x W {WINDOWS} x mode {MODES} x sign {SIGNS}") + print(f" = {len(GRID_FULL)} celle nuove, tf 1d soltanto.") + print(f" + {len(EXS_GRID_SPESA)} celle GIA' SPESE su questa famiglia il 24/07 (EXS) = {len(GRID_FULL)+len(EXS_GRID_SPESA)} trial") + print(" 'quando si riapre un parametro si riapre la sua famiglia' (regola 2026-07-30).") + + # ---------------------------------------------------------------- 3. controllo positivo + print("\n" + "-" * 100) + print(" 3. CONTROLLO POSITIVO — la famiglia UCCISA, ri-girata con la SUA factory") + print("-" * 100) + rep_exs = study_family_honest("EXS-replica(24/07)", ow.exs_factory, EXS_GRID_SPESA, tfs=("1d",)) + ch = rep_exs["chosen"] + print(f" cella IS {ch['params']} IS {ch['insample_sharpe']} FULL {ch['full_sharpe']} " + f"DSR {rep_exs['deflated_sharpe']} earns_slot_honest={rep_exs['earns_slot_honest']}") + print(" atteso: False (l'ondata 24/07 chiuse 0/6). Se qui uscisse True, il macchinario") + print(" non sarebbe credibile sul caso nuovo e il resto andrebbe buttato.") + exs_sharpes = [r["full_sharpe"] for r in rep_exs["rows"]] + + # ---------------------------------------------------------------- 4. il gate di famiglia + print("\n" + "-" * 100) + print(" 4. GATE DI FAMIGLIA — `study_family_honest` (cella scelta IN-SAMPLE, DSR, marginal)") + print("-" * 100) + rep = study_family_honest("EXFLOW", family_factory, GRID_FULL, tfs=("1d",)) + ch = rep["chosen"] + daily = al.candidate_daily(family_factory(tf="1d", **ch["params"]), tf="1d") + hold = daily[daily.index >= al.HOLDOUT] + print(f" celle valutate: {rep['n_cells']}") + print(f" cella scelta AL BUIO (solo in-sample): {ch['params']}") + print(f" Sharpe IS {ch['insample_sharpe']} FULL {ch['full_sharpe']} HOLD-OUT {al._sh(hold):.3f}") + print(f" deflated-Sharpe {rep['deflated_sharpe']} (massimo atteso dal PURO RUMORE: " + f"Sharpe {rep['expected_null_max']}) pass={rep['dsr_pass']}") + print(fmt_marginal(rep["marginal"])) + print(f" >>> earns_slot_honest = {rep['earns_slot_honest']}") + + print("\n classifica per Sharpe IN-SAMPLE (le prime 6 e le ultime 3 di 36):") + for r in rep["rows"][:6] + rep["rows"][-3:]: + d = al.candidate_daily(family_factory(tf="1d", **r["params"]), tf="1d") + print(f" {str(r['params']):58s} IS {r['insample_sharpe']:6.3f} FULL {r['full_sharpe']:6.3f}" + f" HOLD {al._sh(d[d.index >= al.HOLDOUT]):6.3f}") + + # ---------------------------------------------------------------- 5. sensibilita' al conteggio + print("\n" + "-" * 100) + print(" 5. SENSIBILITA' DEL VERDETTO AL CONTEGGIO DEI TRIAL (regola 2026-07-30)") + print("-" * 100) + new_sh = [r["full_sharpe"] for r in rep["rows"]] + # ⚠️ ERRORE MIO, CORRETTO PRIMA DI PUBBLICARE: la prima stesura usava `new_sh[:12]` come + # "una sola variabile". `rows` e' ORDINATA per Sharpe in-sample -> quelle 12 erano le 12 + # MIGLIORI, un pool a varianza artificialmente bassa, e il DSR usciva 0.975 = PASS. Non e' + # una lettura generosa, e' un pool sbagliato. La partizione legittima e' la variabile scelta. + own = [r["full_sharpe"] for r in rep["rows"] if r["params"]["var"] == ch["params"]["var"]] + for lab, pool in ((f"N=12 (la sola variabile scelta, {ch['params']['var']})", own), + ("N=36 (griglia nuova dichiarata)", new_sh), + ("N=40 (+ le 4 gia' spese su EXS il 24/07)", new_sh + exs_sharpes)): + dsr, sr0 = deflated_sharpe(al._sh(daily), pool, daily) + print(f" {lab:48s} DSR {dsr:.3f} null-max {sr0:.3f} (n pool {len(pool)})") + print(" Il verdetto e' lo stesso a ogni conteggio LEGITTIMO. Ma il DSR resta sensibile a") + print(" COME si partiziona la griglia (lezione §10 dell'ondata): con un pool scelto male") + print(" — le 12 celle migliori — lo stesso candidato PASSA a 0.975. La partizione e' il") + print(" secondo posto dove barare e' indolore e invisibile.") + + # ---------------------------------------------------------------- 6. sotto-famiglie + print("\n" + "-" * 100) + print(" 6. DECOMPOSIZIONE PER VARIABILE (diagnostica: riportare il meglio di tre E' selezione,") + print(" quindi il numero che vale resta quello del blocco 4)") + print("-" * 100) + for nm, fac in VARS.items(): + r = study_family_honest(nm, fac, GRID_1VAR, tfs=("1d",)) + c = r["chosen"] + d = al.candidate_daily(fac(tf="1d", **c["params"]), tf="1d") + print(f" {nm:7s} IS-pick {str(c['params']):40s} IS {c['insample_sharpe']:6.3f} " + f"FULL {c['full_sharpe']:6.3f} HOLD {al._sh(d[d.index >= al.HOLDOUT]):6.3f} " + f"DSR(12) {r['deflated_sharpe']} marg {r['marginal']['marginal_verdict']} " + f"corr->TP01 {r['marginal']['marginal']['corr_full']}") + print(" La selezione in-sample sull'INTERA griglia preferisce NETCTL, cioe' il CONTROLLO") + print(" NEGATIVO = l'informazione gia' uccisa. La parte nuova non e' selezionabile.") + + # ---------------------------------------------------------------- 7. direzione o volatilita' + print("\n" + "-" * 100) + print(" 7. DOVE STA L'INFORMAZIONE — direzione o volatilita'? (la letteratura che il progetto") + print(" stesso cita il 24/07 dice: i flussi predicono la VOL, non la direzione)") + print("-" * 100) + + def _t(x, y): + c = float(np.corrcoef(x, y)[0, 1]) + return c, c * np.sqrt(len(x) - 2) / np.sqrt(max(1e-12, 1 - c * c)) + + for a in ("BTC", "ETH"): + df = al.get(a, "1d") + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)).floor("D") + r = pd.Series(al.simple_returns(df["close"].values.astype(float)), index=idx) + I, O = _flows(a) + zg = _z(np.log(I + O), 180).shift(ow.AVAIL_LAG_D).reindex(idx) + zi = _z((O - I) / (O + I), 180).shift(ow.AVAIL_LAG_D).reindex(idx) + rv = r.rolling(30).std() + fdir, fabs = r.shift(-1), r.abs().shift(-1) + m = zg.notna() & zi.notna() & fdir.notna() & rv.notna() + n = int(m.sum()) + cd, td = _t(zg[m], fdir[m]) + ci, ti = _t(zi[m], fdir[m]) + ca, ta = _t(zg[m], fabs[m]) + cb, tb = _t(rv[m], fabs[m]) + # incrementale ONESTO: residuo OLS di |r_{t+1}| su RV30_t, poi correlato con z(lordo) + X = np.column_stack([np.ones(n), rv[m].values]) + beta = np.linalg.lstsq(X, fabs[m].values, rcond=None)[0] + resid = fabs[m].values - X @ beta + cr, tr = _t(zg[m].values, resid) + rho, pv = stats.spearmanr(zg[m].values, resid) + yr_idx = zg[m].index + per_yr = {int(y): round(float(np.corrcoef(zg[m].values[yr_idx.year == y], + resid[yr_idx.year == y])[0, 1]), 3) + for y in sorted(set(yr_idx.year))} + print(f" {a} n={n}") + print(f" DIREZIONE corr(z lordo, r_domani) = {cd:+.4f} t={td:+5.2f}") + print(f" corr(z squilibrio, r_dom) = {ci:+.4f} t={ti:+5.2f}") + print(f" VOLATILITA corr(z lordo, |r|_domani) = {ca:+.4f} t={ta:+5.2f}") + print(f" baseline corr(RV30, |r|) = {cb:+.4f} t={tb:+5.2f} <- il confronto giusto") + print(f" z lordo sul RESIDUO OLS = {cr:+.4f} t={tr:+5.2f} <- l'incremento vero") + print(f" lo stesso in RANGO (Spearman) = {rho:+.4f} p={pv:.4f} <- e' monotono o e' coda?") + print(f" Pearson per anno: {per_yr}") + + # ---------------------------------------------------------------- 8. gate residui + print("\n" + "-" * 100) + print(" 8. GATE RESIDUI SULLA CELLA SCELTA") + print("-" * 100) + fn = family_factory(tf="1d", **ch["params"]) + print(f" causality_ok : {causality_ok(fn, tf='1d')}") + imp = implausible_sharpe(daily) + print(f" implausible_sharpe: implausible={imp['implausible']} reasons={imp['reasons']} " + f"attive={imp['n_active']} quota-perdite-su-attive={imp['loss_frac']:.3f}") + print(" eseguibilita' a $635 (min-order $5):") + for a in ("BTC", "ETH"): + df = al.get(a, "1d") + sc = eval_weights_smallcap(df, al._call_target(fn, df, a), capital=635.0, min_order=5.0) + af, tf_, ah, th = _active_bars(fn, a) + print(f" {a}: haircut Sharpe {sc['sharpe_haircut']:+.3f} trade eseguiti {sc['n_executed_trades']:4d}" + f" | barre ATTIVE {af}/{tf_} ({100*af/tf_:.1f}%) hold-out attive {ah}/{th}") + + # ---------------------------------------------------------------- 9. potenza + print("\n" + "-" * 100) + print(" 9. POTENZA — in barre ATTIVE, non di calendario") + print("-" * 100) + tgt = np.abs(np.nan_to_num(np.asarray(al._call_target(fn, al.get('BTC', '1d'), 'BTC'), float))) > 0 + print(f" il CSV CoinMetrics si ferma al {ow._CM['BTC'].index[-1].date()}: dopo quella data la") + print(" posizione e' FLAT per ASSENZA DI DATO. Contarlo come evidenza sarebbe l'errore gia'") + print(" pagato dal progetto (zeri letti come 'nessuna perdita').") + for lab, s in (("FULL", daily), ("HOLD-OUT", hold)): + nz = int((s != 0).sum()) + yr = nz / 365.25 + se = np.sqrt(1.0 / max(yr, 1e-9)) + print(f" {lab:9s} barre non-nulle {nz:5d} ({yr:.2f} anni) SE(Sharpe) ~ {se:.2f}" + f" -> differenza rilevabile al 95% ~ {1.96*se:.2f} di Sharpe") + print(f" (usato solo per dire cosa la finestra puo' e non puo' decidere: n_target_attivo={int(tgt.sum())})") + + # ---------------------------------------------------------------- 10. verdetto + print("\n" + LINE) + print(" VERDETTO") + print(LINE) + print(f""" + SCARTATO, per tre ragioni indipendenti e in quest'ordine di forza: + + (1) LA CELLA SCELTA AL BUIO STA SOTTO IL RUMORE. Sharpe FULL {ch['full_sharpe']} contro un + massimo atteso dal PURO RUMORE di {rep['expected_null_max']} su {rep['n_cells']} trial. + Non e' "non passa il gate per poco": e' sotto la soglia che una griglia di monete + raggiungerebbe da sola. Stessa aritmetica di ORTHO-SCREEN (§12). + + (2) LA SELEZIONE IN-SAMPLE TORNA SUL CONTROLLO NEGATIVO. Fra le tre variabili sceglie + NETCTL, cioe' l'informazione dello STOCK gia' uccisa il 24/07. Il turnover LORDO — + l'unica parte algebricamente nuova (corr con d(stock) 0.03-0.08, e il 94% del flusso + che si cancella) — NON e' selezionabile: la sua migliore cella in-sample fa hold-out + NEGATIVO (-0.42) ed e' DILUTES contro TP01. + + (3) ZERO INFORMAZIONE DIREZIONALE, E IL LEGAME CON LA VOL NON REGGE AL SECONDO SGUARDO. + corr(segnale, ritorno di domani): |t| < 1 su 2 asset x 2 variabili nuove — ed e' la + direzione cio' di cui una strategia aveva bisogno. Sul |ritorno| il quadro sembra + diverso — ETH da' Pearson incrementale sul residuo di RV30 t=+2.70 — ma: + * in RANGO (Spearman) diventa +0.015 con p=0.46 -> non e' una relazione monotona, + e' la coda della distribuzione dei livelli; + * per anno decade e cambia segno (2021 +0.13 ... 2024 -0.03, 2026 -0.06); + * su BTC lo stesso test da' -0.005 (Pearson) e -0.051 con p=0.007 (Spearman), cioe' + "significativo" nel verso SBAGLIATO = firma di rumore, non di segnale. + Il turnover sugli exchange sale quando la vol e' GIA' salita: termometro contemporaneo, + stessa forma gia' trovata su TERM-STRUCTURE (§7) e SKEW (§5). + ⚠️ E anche se il legame fosse reale, non produrrebbe un candidato: l'unica espressione + eseguibile di una vista sulla VOL a $635 e' short-vol (VRP01: 4/4 gate falliti, 5/5 + overlay refutati, regola "niente short-vol da modello in deploy") oppure il denominatore + del vol-target di TP01 (TP01xDVOL, refutato come de-levering il 26/06). E' chiuso a valle, + non solo a monte. + + COSA SI PORTA A CASA COMUNQUE: + * il marginal scorer da' corr->TP01 0.50-0.65 su colonne che l'ondata 24/07 non aveva MAI + letto -> REPLICA INDIPENDENTE del suo finding ("l'on-chain tradabile e' prezzo travestito, + corr 0.50-0.82"), raggiunta da un'altra porta e con un altro stimatore; + * l'eseguibilita' a $635 non ha bocciato nulla (haircut 0.000 su entrambe le gambe): ennesima + conferma che il vincolo del progetto non e' piu' il capitale ma l'edge; + * il pezzo di METODO riusabile e' il CONTROLLO NEGATIVO DENTRO LA GRIGLIA: infilare nella + stessa famiglia una variabile che si SA gia' morta trasforma "il candidato perde" in + "il dato non contiene nient'altro di selezionabile", che e' un'affermazione molto piu' + forte e costa una factory. + + RISPOSTA ALLA DOMANDA DEL FILONE: dei dati che nessuno legge, uno solo aveva storia + sufficiente per sostenere un'ipotesi, ed e' stato misurato fino in fondo. Tutto il resto + dell'inventario e' telemetria a finestra corta (30-113 giorni) o gia' analizzato. + **Nessun dato inutilizzato sostiene oggi un'ipotesi nuova.** +""") + + +def main() -> None: + skip = "--skip-inventory" in sys.argv + print(LINE) + print(" §43 DATA-UNUSED — quali dati il progetto possiede e nessuna strategia legge") + print(f" repo {ROOT} (sola lettura: nessuna scrittura, nessun ordine)") + print(LINE) + if not skip: + fase1() + fase2() + + +if __name__ == "__main__": + main() From 7264a8fe903d58e6868a0cb1e086da518b6a1032 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 01:13:21 +0000 Subject: [PATCH 068/114] =?UTF-8?q?registro=20=C2=A743:=20DATA-UNUSED=20SC?= =?UTF-8?q?ARTATO=20=E2=80=94=20la=20scorta=20di=20dati=20e'=20esaurita,?= =?UTF-8?q?=20e=20il=20candidato=20sta=20sotto=20il=20massimo=20atteso=20d?= =?UTF-8?q?al=20rumore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 99 ++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index ffbad5f..4b56535 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -708,7 +708,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 40 | SL-ANCHOR | il nodo che bloccava il gradino di leva | **SCIOLTO** — rotolante, ma il timore e' refutato | | 41 | TP01-SIZE | una size che risponde alla CONVINZIONE batte il vol-target uniforme, a pari vol realizzata? | **SCARTATO** — il meglio di 60 celle vale **+0,30pp** di CAGR iso-vol, e l'ottimo sta ai **due estremi opposti** nelle due meta' del campione (**Spearman ShIS/ShHOLD −0,527**: scegliere in-sample e' peggio di una moneta) | | 42 | TP01-LS | la gamba short del trend a 1d paga il proprio costo, o e' il 2022? | **SCARTATO** — toglie **−1,91%/anno di drift in 0/24 ancore**, fallisce l'iso-vol, +8,80pp di maxDD in 24/24, e **senza il 2022 il divario RADDOPPIA** | -| 43 | DATA-UNUSED | quali dati il progetto possiede e nessuna strategia legge — e uno di essi sostiene un'ipotesi? | _in corso_ | +| 43 | DATA-UNUSED | quali dati il progetto possiede e nessuna strategia legge — e uno di essi sostiene un'ipotesi? | **SCARTATO** — **la scorta di dati e' esaurita**: un solo dato mai letto ha storia vera (flussi exchange CoinMetrics 2018-2026) e misurato sta **SOTTO il massimo atteso dal puro rumore** | | 44 | DEPOSIT-TIMING | un calendario di versamento CONDIZIONALE batte il piatto, a pari soldi? | **SCARTATO 0/12 in ogni lente** — il buy-the-dip compra davvero il **51% piu' in basso** e arriva al capitale-rendita nel **6,3%** dei casi contro l'**85,4%** del piatto | | 45 | SPOT-NETTING | separare TP01 e SKH01 su due strumenti: quanto vale, e il margine lo uccide? | _in corso_ | | — | **NOTIFIER** (audit di fatto) | 🚨 **PUNTO SINGOLO DI GUASTO nella rete di sicurezza** | il `send()` di Telegram e' **un solo tentativo da 10s, senza retry, con l'esito MAI registrato** — e lo stato `alerted=True` viene scritto **prima** dell'invio: un 🚨 di `venue_watch` perso e' perso **per l'intero episodio**. Tasso di fallimento **misurato 6,9%** (2/29, IC95 [1,9%, 22,0%]) | @@ -2385,3 +2385,100 @@ e la finestra resta 7,4 anni con due tori. **VERDETTO: `IL TIMING DEI BONIFICI NON PAGA IN NESSUNA LENTE — E IL SEGNALE FUNZIONA, E' LA CONDIZIONE RARA A COSTARE`** — SCARTATO. Chiude una domanda che l'operatore potrebbe altrimenti riporsi ogni mese per vent'anni. **Libro, pesi, cron, config INVARIATI.** + +--- + +## 43 — DATA-UNUSED (i dati che nessuno legge) + +`scripts/research/r0823_data_unused.py`. Indice su **647 file `.py`** in `src/ scripts/ tests/` +(`Old/` escluso = archivio). *"MAI LETTA"* = zero occorrenze **a confini di parola** — misura +**generosa verso "letta"**, quindi forte quando dice mai. + +### FASE 1 — l'inventario (e' il risultato principale, non un preambolo) + +| dataset | righe | intervallo | letto da | colonne MAI LETTE | +|---|---|---|---|---| +| `btc/eth_{5m,15m,1h}` | 70.335 (1h) | 2018-08→oggi | tutto il progetto | — | +| `hl_*_1d` (51 alt) | 966 | 2024-01→oggi | 17 script | — | +| `hlfund_*_1h` (19) | 26.739 | 2023-05→2026-06 | 9 (famiglia chiusa 4 lati) | — | +| `eq_*_1d` (29 ETF) | 7.539 | 1996-08→oggi | 11 | — | +| `eqx_*_1d` (12 UCITS) | 1.770 | 2019-05→2026-07 | 7 | — | +| `fut_*_1h` (7 indici) | 15.070 | 2023-06→2026-06 | 4 | — | +| `fut_deribit/` + `fund_*` | 8.739 / 64.110 | 2018-09→oggi | **2** (entrambi del 22/08) | — | +| `dvol_*` | 1.979 | 2021-03→oggi | 17 | — | +| `vol_term_*` | **59** | 2026-06-26→oggi | 3 | `iv_90d` | +| `cb_chain/` | 1.232.212 | 2026-05-01→oggi | 14 | `volume_24h` (in 4 file, **tutti collettori/certificatori, nessuno analitico**) | +| `cb_market_snapshots` | 17.406 | 2026-03-26→**2026-07-30, morto** | 3 | `fetch_errors_json` | +| `fundnews_short_screen` | **19** | 2026-06-26 | 3 | — | +| `alt_sol_*` | 38.898 | 2022-03→ | 2 (escluso dall'operatore 22/08) | — | +| 🚨 **`external/coinmetrics/cm_{btc,eth}.csv`** | **6.351 / 3.952** | **2009 / 2015→2026-05** | **1** | 🚨 **26 colonne su 31** | +| `external/{fng,stables}.json`, `premium/` | — | 2015-2018→ | 1 ciascuno | — (chiusi 24/07) | +| `options_daily/snapshots.jsonl` | 60 snapshot | **30 giorni** | 2 | letto solo per le **specifiche**, mai come serie | +| `instruments_registry.json` | — | — | **NESSUNO** | (reference, non segnale) | + +📌 **Il quadro: ogni dataset con storia vera e' gia' stato analizzato almeno una volta. Cio' che +resta non letto e' telemetria a finestra corta (30-113 giorni) — con UNA eccezione.** + +⚠️ **Due difetti dell'inventario stesso, corretti in sessione:** la ricerca a **sottostringa** dava +`iv` in 571 file (e' dentro *drive*, *receive*); e le chiavi derivate dall'etichetta producevano due +falsi **"NESSUNO"** su `eqx_` e `fut_deribit/fund_` — cioe' avrebbero **fabbricato** un dato mai +usato che non esiste. + +### FASE 2 — `FlowInExNtv`/`FlowOutExNtv`, e perche' proprio quelle + +Sceltе per: **storia** (100% coperte 2018→2026 su entrambi gli asset, contro *mesi* per tutto il +resto), **meccanismo** (il turnover **lordo** misura quanta parte del flottante viene +ri-posizionata), **eseguibilita'** (1d → long-flat/LS su BTC+ETH perp, l'impianto di TP01), +**famiglia non chiusa**. La famiglia exchange-flow **e'** chiusa (24/07, su `SplyExNtv`) → il +meccanismo nuovo e' stato **misurato prima di usarlo**: + +| | BTC | ETH | +|---|---|---| +| corr( d(SplyEx) , **In−Out** ) | **+0,9993** | +0,7516 | +| corr( d(SplyEx) , **In+Out** ) | **+0,0349** | +0,0801 | +| \|In−Out\| / (In+Out) mediano | **0,060** | 0,062 | + +📌 **Il netto E' la derivata dello stock gia' testato; il lordo e' ortogonale — e il 94% del flusso +si cancella**, quindi lo stock vede 6 monete su 100 fra quelle che si muovono. Il dato nuovo era +davvero nuovo. + +**Griglia dichiarata prima e contata al rialzo:** 3 variabili (GROSS nuova · IMB mista · +**NETCTL = controllo negativo, l'informazione gia' uccisa**) × W {30,90,180} × mode {LF,LS} × +sign {±1} = **36 celle**, **+4 gia' spese su EXS il 24/07 = 40 trial**. +✅ **Controllo positivo:** la famiglia uccisa ri-girata con la **sua** factory (`ow.exs_factory` +importata) → `earns_slot_honest=False`, DSR 0,903 = **riproduce il 24/07**. + +**Esito** (`study_family_honest`, cella scelta **solo in-sample**): la cella al buio e' +**`NETCTL W=90 LF +1`, cioe' il CONTROLLO NEGATIVO** — IS 1,273 · FULL **0,951** · HOLD **−0,094**. +🚨 **Massimo atteso dal PURO RUMORE su 36 trial = Sharpe 1,009: il candidato sta SOTTO.** +DSR **0,437** (N=36) / **0,411** (N=40); marginale **NEUTRAL**, corr→TP01 **0,653**, multi-cut +negativo **7/7 anni**. Per variabile: GROSS **DILUTES** (HOLD −0,415), IMB **DILUTES**, NETCTL +NEUTRAL → **la parte algebricamente nuova non e' selezionabile**. Direzione: |t|<1 su 2 asset × 2 +variabili. Volatilita': ETH da' t **+2,70** su Pearson ma in **rango** +0,015 **p=0,46**, cambia +segno per anno e su BTC e' "significativo" nel verso sbagliato → **termometro contemporaneo**, come +TERM-STRUCTURE (§7) e SKEW (§5). `causality_ok` True; `implausible_sharpe` False (48,1% di barre +attive in perdita); **haircut a $635 = 0,000**. Finestra in **barre ATTIVE**: FULL 2.214 (6,06 a) → +SE(Sharpe) 0,41; **HOLD-OUT 511 (1,40 a) → MDE 1,66 di Sharpe**. + +⚠️ **Due errori dell'agente su se stesso, catturati prima di pubblicare.** (a) Il pool *"N=12"* del +deflated-Sharpe erano **le 12 celle MIGLIORI** (`rows` e' ordinata) e faceva **PASSARE** il candidato +a **0,975**; con la partizione legittima fa 0,678 → **conferma diretta della lezione §10**: *la +stessa griglia da' verdetti opposti secondo come la si partiziona*. (b) Il residuo di vol calcolato +a mano invece che per OLS **ribaltava il segno** su ETH. + +**Escluso e perche':** `volume_24h`/`book_depth_top3` e 0DTE (113 e 30 giorni, e ogni loro +espressione eseguibile e' short-vol = chiuso); `fut_deribit` (il basis **e'** il funding, §13); +`funding_cross_annualized`/`oi_delta_pct_4h` (famiglia chiusa / dataset morto dal 30/07); +Amihud-turnover HL (gia' testato in `xsec_v3_lowrisk`); kimchi, HLP, F&G, MVRV, hash-ribbons, +stablecoin (24/07); `liquidation_*_risk` (una sola categoria in 17.229/17.229); `iv_90d` (59 righe). + +📌 **IL PEZZO DI METODO RIUSABILE:** *mettere nella griglia una variabile che si sa gia' morta* +trasforma **"il candidato perde"** in **"il dato non contiene nient'altro di selezionabile"** — qui +la selezione in-sample e' **tornata sul controllo negativo**, che e' l'unica forma in cui un +inventario puo' concludere qualcosa di positivo su un'assenza. Sottoprodotto: corr→TP01 0,50-0,65 = +**replica indipendente** del *"l'on-chain tradabile e' prezzo travestito"* del 24/07, su colonne che +quell'ondata non aveva mai aperto. + +**VERDETTO: `NESSUN DATO INUTILIZZATO SOSTIENE OGGI UN'IPOTESI NUOVA — LA SCORTA DI DATI DEL +PROGETTO E' ESAURITA, E CIO' CHE RESTA NON LETTO E' TELEMETRIA A FINESTRA CORTA, NON STORIA`** — +SCARTATO. **Libro, pesi, cron, config INVARIATI.** From 13daccf6de11f5cca9031da0a0e222b71262c4ed Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 01:19:04 +0000 Subject: [PATCH 069/114] =?UTF-8?q?research(=C2=A745):=20SPOT-NETTING=20?= =?UTF-8?q?=E2=80=94=20il=20margine=20non=20uccide=20il=20lead,=20il=20COD?= =?UTF-8?q?ICE=20si';=20e=20separare=20le=20gambe=20non=20aggiunge=20nient?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filone §45: quanto vale, e quanto costa, spostare TP01 sullo SPOT Deribit separandolo da SKH01 (oggi nettati su un unico strumento). Solo misura, nessun file di produzione toccato, nessun ordine. Q1 MARGINE — il venue NON blocca: BTC/ETH sono `in_cross_collateral_pool: true` [VENUE], lo spot ha `max_leverage: 10` (vive dentro il conto marginato, non in un wallet a parte) [VENUE], e il margine iniziale misurato sul conto reale e' il 2,00% del nozionale [CONTO: equity-available su 2 posizioni] -> nel caso peggiore la gamba SKH01 resterebbe marginata 50x dal solo USDC libero, a QUALUNQUE capitale (il rapporto e' invariante in E). L'haircut non e' leggibile e non e' binding. A bloccare e' il CODICE: `shadow._equity` legge solo `account_summary('USDC')` e `position_usd` matcha per instrument_name -> comprando spot il libro leggerebbe il 25% del conto vero e non vedrebbe la gamba. Non e' un cambio di strumento, e' un cambio del percorso di sizing del live. Q2 VALORE — T1 riprodotto (+0,051/+0,057 contro `hourly`, pubblicato +0,054/+0,061) MA contro `fastdetect`, che il 26/07 stabili' essere il path del live vero, vale -0,048 FULL (6/23 offset): sbloccarlo e' un DECLASSAMENTO. Il netting perso non costa commissioni (-0,216% di equity/anno: e' un risparmio) ne' ordini (-6,3/anno): costa tracking (+12%) e spread ([0,139%,0,250%]/anno). I due si compensano quasi. Il valore vero e' il funding e basta: +1,58%/anno lordo di drift di libro. Q3 — `fee_watch` DERIVA i suoi strumenti da `src.live.book.INSTRUMENT`: una gamba spot sarebbe invisibile (stesso difetto corretto il 21/08 sugli inverse). Riga dichiarata, non scritta. `convenzione('BTC_USDC')` = 'ignota' -> servirebbe anche una terza famiglia o il cross-check sui fill resta muto. Q4 — la liquidazione MIGLIORA (il 75% del libro esce dal perimetro; uno spot non si liquida) ma la gamba TP01 resterebbe senza disaster-SL; il fisco PEGGIORA di 0,83%/anno se i derivati sono `c-quater`, e i comparti si SEPARANO (le minusvalenze del perp smetterebbero di compensare le plusvalenze spot): costo nuovo, mai contato. IPOTESI MIA NATA E REFUTATA: «convertire USDC in spot rinuncia all'interesse». Il venue dichiara USDC `apr` 3,40; sul log del cron, 6 run flat (il piu' lungo 257 giri = 10,7 giorni) mostrano equity INVARIATA al centesimo contro i +$0,59 attesi -> limite superiore su qualsiasi interesse < 0,029%/anno. L'obiezione e' morta. ⚠️ Due istantanee del libro a 4 minuti danno differenziali di spread 2,27 e 4,08 bps (+79%) e il contributo netto dell'esecuzione CAMBIA SEGNO fra le due: e' dichiarato come banda, non come punto. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf --- scripts/research/r0823_spot_netting.py | 1004 ++++++++++++++++++++++++ 1 file changed, 1004 insertions(+) create mode 100644 scripts/research/r0823_spot_netting.py diff --git a/scripts/research/r0823_spot_netting.py b/scripts/research/r0823_spot_netting.py new file mode 100644 index 0000000..81407e2 --- /dev/null +++ b/scripts/research/r0823_spot_netting.py @@ -0,0 +1,1004 @@ +#!/usr/bin/env python3 +"""r0823_spot_netting.py — SPOT-NETTING (§45): quanto vale, e quanto COSTA, separare TP01 e +SKH01 su due strumenti diversi? + +CONTESTO (§39, `scripts/research/r0822e_funding_avoid.py`, 2026-08-22) + Il libro paga -2,16%/anno di funding. Lo strumento a carry ZERO esiste ed e' lo SPOT Deribit + (`BTC_USDC`, `ETH_USDC`; taker 0,0 oggi, min 0,0001). La variante «E) TP01 su SPOT + SKH01 su + perp» dava +1,55%/anno di drift a volatilita' identica e muro -10,9%. E' rimasta un LEAD per + tre ragioni dichiarate: (a) SKH01 e' long/short e sullo spot non e' esprimibile; (b) la fee 0 + e' dichiarata temporanea e `fee_watch` non sorveglia lo spot; (c) la domanda fiscale aperta + (derivati `c-quater` 26% contro cripto `c-sexies` 33%) vale piu' del risparmio. + +LA DOMANDA DI QUESTO FILONE — la conseguenza di SECONDO ORDINE che nessuno ha misurato. + Oggi TP01 e SKH01 tradano lo STESSO strumento e Deribit tiene UNA sola posizione netta. E' + questo netting che il 2026-07-26 rese «non implementabile» la raccomandazione T1 (TP di SKH01 + come limit resting reduce-only, misurato +0,054 Sharpe FULL / +0,061 HOLD di libro): un ordine + al livello di SKH chiuderebbe anche la quota di TP01. + Spostare TP01 sullo spot SEPARA le due gambe. Quindi: + + Q1 IL MARGINE lo spot comprato e' collaterale del perp, o immobilizza capitale? + (puo' UCCIDERE il lead: TP01 al 75% lascerebbe il 25% a marginare SKH01) + Q2 IL VALORE T1 sbloccato + funding risparmiato - COSTO DEL NETTING PERSO + Q3 LA SORVEGLIANZA `fee_watch` DERIVA i suoi strumenti dal libro: una gamba spot sarebbe + invisibile. Quale riga andrebbe aggiunta, e cosa succede a fee != 0. + Q4 IL RISCHIO detenzione cripto invece di derivato: venue, fisco, liquidazione. + +ONESTA' — ogni numero di questo script porta la sua FONTE, in chiaro: + [VENUE] letto da un endpoint pubblico Deribit in questa corsa + [CONTO] letto dal percorso di SOLA LETTURA gia' nel repo (src.live.deribit.DeribitRead) + [LOG] misurato su logs/cron_book.log o data/live/book_executions.jsonl + [CODICE] verificato sul sorgente di produzione (inspect.getsource, non a memoria) + [CALC] calcolato qui da dati misurati + [DEDOTTO] conclusione di un ragionamento su numeri misurati + [ASSUNTO] assunzione dichiarata, non misurata + Cio' che non e' determinabile senza un conto reale e' elencato in sezione 1g e NON e' stato + dedotto ne' assunto: e' dichiarato ignoto. + +VINCOLI RISPETTATI + * SOLA LETTURA: nessun file di produzione toccato (src/, config/, scripts/live/, tests/, + data/paper_*, data/live). L'unica scrittura e' la cache in scratchpad. + * NESSUN ORDINE, di nessun tipo. Il percorso del conto usato (`DeribitRead`) non ha, by + design, alcun metodo di trading. + * Rete: solo endpoint PUBBLICI di lettura Deribit, <=2 req/s, e ASTENSIONE nelle finestre + :05-:10 e :24-:30 (cron_book al :07, collect_chain al :25) -> in quelle finestre si usa + solo la cache e lo si dichiara. + +USO + nice -n 19 timeout 900 uv run python scripts/research/r0823_spot_netting.py + ... --no-net nessuna richiesta: solo cache (e conto non letto) +""" +from __future__ import annotations + +import contextlib +import datetime as dt +import inspect +import io +import json +import re +import sys +import time +import urllib.request +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +for _p in (ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt", ROOT): + sys.path.insert(0, str(_p)) + +NO_NET = "--no-net" in sys.argv +# r0822d_funding legge `--no-net` da sys.argv AL MOMENTO DELL'IMPORT: la sua cache del funding +# esiste gia' (4 parquet in scratchpad/fund) e questo script non deve ri-scaricarla. +if "--no-net" not in sys.argv: + sys.argv.append("--no-net") + +import altlib as al # noqa: E402 +import r0702_anchor_skh01 as r02 # noqa: E402 +import r0726_skh_onbook as OB # noqa: E402 +import r0822d_funding as FD # noqa: E402 +from src.backtest.harness import load # noqa: E402 +from src.data.downloader import load_data # noqa: E402 +from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio, resample_1d # noqa: E402 + +T0 = time.time() +ASSETS = ("BTC", "ETH") +API = "https://www.deribit.com/api/v2/public/" +_SP = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/spotnet") +SCRATCH = _SP if _SP.parent.exists() else Path("/tmp/pythagoras_spotnet") +SCRATCH.mkdir(parents=True, exist_ok=True) + +W_TP, W_SKH = 0.75, 0.25 # pesi del libro Deribit (src/live/book.py) +WEIGHT = 0.5 # 50/50 BTC+ETH (src/live/shadow.py) +FEE_PERP_SIDE = 0.00035 # 3,50 bps/lato, MISURATO sul nastro (§ondata 22/08) e da fee_watch +HOLDOUT = "2025-01-01" + +_NET_CALLS: list[str] = [] + + +def sez(t: str) -> None: + print("\n" + "=" * 100) + print(f" {t}") + print("=" * 100) + + +def clock() -> str: + return f"[{time.time()-T0:6.1f}s]" + + +# =========================================================================== +# 0) RETE — pacing, finestre vietate, cache +# =========================================================================== +def finestra_vietata(now: dt.datetime | None = None) -> bool: + """True nelle finestre in cui la VPS parla gia' con Deribit per conto della produzione: + :05-:10 (cron_book al :07, feed 5m di SKH01) e :24-:30 (collect_chain al :25). + PURA. Non e' cortesia: il 29/07 il rate-limit per-IP saturato da un altro processo ha + portato la latenza d'uscita di SKH01 da ~1h a ~11h.""" + now = now or dt.datetime.now(dt.timezone.utc) + m = now.minute + return (5 <= m <= 10) or (24 <= m <= 30) + + +_last = [0.0] + + +def get(path: str, key: str) -> dict | None: + """GET pubblica con cache su disco, pacing <=2 req/s e astensione nelle finestre vietate.""" + cache = SCRATCH / f"{key}.json" + if cache.exists(): + try: + return json.loads(cache.read_text()) + except Exception: + pass + if NO_NET: + return None + for _ in range(60): + if not finestra_vietata(): + break + time.sleep(10) + else: + print(f" ⚠️ finestra vietata persistente: {key} NON letto (nessuna cache)") + return None + d = time.time() - _last[0] + if d < 0.5: + time.sleep(0.5 - d) + _last[0] = time.time() + try: + with urllib.request.urlopen(API + path, timeout=25) as r: + res = json.loads(r.read()).get("result") + except Exception as e: # noqa: BLE001 + print(f" ⚠️ {key}: {type(e).__name__} -> non letto (non e' 'uguale', e' 'non so')") + return None + _NET_CALLS.append(path.split("?")[0]) + cache.write_text(json.dumps(res)) + return res + + +def conto() -> dict | None: + """Stato del conto dal percorso di SOLA LETTURA gia' nel repo. Nessun metodo di trading + esiste in DeribitRead (by design). Cache su disco: una corsa successiva non ri-interroga.""" + cache = SCRATCH / "conto.json" + if cache.exists(): + try: + return json.loads(cache.read_text()) + except Exception: + pass + if NO_NET or finestra_vietata(): + return None + try: + from src.live.deribit import INSTRUMENT, DeribitRead + c = DeribitRead() + out = {"summary": {k: c.account_summary(k) for k in ("USDC", "BTC", "ETH")}, + "positions": c.positions("USDC"), + "instruments": dict(INSTRUMENT)} + _NET_CALLS.append("cerbero-mcp/get_account_summary x3 + get_positions x1 (SOLA LETTURA)") + cache.write_text(json.dumps(out, default=str)) + return out + except Exception as e: # noqa: BLE001 + print(f" ⚠️ conto non letto: {type(e).__name__}: {e}") + return None + + +# =========================================================================== +# 1) Q1 — IL MARGINE (la domanda che puo' uccidere il lead) +# =========================================================================== +def q1() -> dict: + sez("Q1 — IL MARGINE: lo spot e' collaterale del perp, o immobilizza capitale?") + + cur = get("get_currencies", "currencies") + spot = get("get_instruments?currency=USDC&kind=spot", "inst_spot") + perp = get("get_instruments?currency=USDC&kind=future", "inst_perp") + acc = conto() + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + + # ---------------------------------------------------------------- 1a) il pool + print("\n 1a) [VENUE] public/get_currencies — chi sta nel POOL DI CROSS-COLLATERAL") + pool = {} + if cur: + for c in cur: + if c["currency"] in ("BTC", "ETH", "USDC", "USDT"): + pool[c["currency"]] = (bool(c.get("in_cross_collateral_pool")), float(c.get("apr") or 0)) + for k, (p, a) in pool.items(): + print(f" {k:<6} in_cross_collateral_pool = {str(p):<5} apr = {a:.2f}") + print(f" (nel pool ci sono {sum(1 for c in cur if c.get('in_cross_collateral_pool'))} " + f"valute su {len(cur)}: e' un elenco RISTRETTO, non un default)") + print(" → [DEDOTTO] BTC ed ETH NON sono un portafoglio separato: il venue li") + print(" dichiara collaterale. Il campo dice CHE contano, non QUANTO (haircut).") + else: + print(" non letto") + + # ---------------------------------------------------------------- 1b) lo strumento + print("\n 1b) [VENUE] public/get_instruments — lo SPOT e' dentro il sistema di margine") + spec = {} + for src, kind in ((spot, "spot"), (perp, "future")): + for x in (src or []): + if x["instrument_name"] in ("BTC_USDC", "ETH_USDC", + "BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL"): + spec[x["instrument_name"]] = x + print(f" {'strumento':<22} {'kind':>7} {'maxlev':>7} {'taker bps':>10} {'maker bps':>10} " + f"{'min lotto':>10} {'tick':>7}") + for k in ("BTC_USDC", "BTC_USDC-PERPETUAL", "ETH_USDC", "ETH_USDC-PERPETUAL"): + s = spec.get(k) + if not s: + print(f" {k:<22} non letto") + continue + print(f" {k:<22} {s['kind']:>7} {s.get('max_leverage'):>7} " + f"{float(s['taker_commission'])*1e4:>10.2f} {float(s['maker_commission'])*1e4:>10.2f} " + f"{s['min_trade_amount']:>10} {s['tick_size']:>7}") + if spec.get("BTC_USDC", {}).get("max_leverage"): + print(f" → [DEDOTTO] lo spot ha `max_leverage` = {spec['BTC_USDC']['max_leverage']}: si puo'") + print(" comprare a leva. Un portafoglio segregato non ha una leva massima → lo spot") + print(" Deribit vive DENTRO il conto marginato, non in un wallet a parte.") + + # ---------------------------------------------------------------- 1c) il margine misurato + print("\n 1c) [CONTO] il margine iniziale del perp, MISURATO sul conto reale") + im_rate = None + E = None + if acc: + u = acc["summary"]["USDC"] + E = float(u["equity"]) + av = float(u["available_funds"]) + gross = sum(abs(float(p["size"])) for p in acc["positions"]) + print(f" equity USDC ${E:,.2f}") + print(f" balance USDC ${float(u['balance']):,.2f}") + print(f" available_funds ${av:,.2f}") + print(f" nozionale lordo ${gross:,.2f} ({len(acc['positions'])} posizioni perp)") + if gross > 0: + im_rate = (E - av) / gross + print(f" → margine impegnato ${E-av:,.2f} = {im_rate:.3%} del nozionale [CALC]") + print(" ⚠️ `equity - available_funds` contiene TUTTO cio' che e' riservato (margine") + print(" iniziale + eventuali riserve d'ordine): e' un LIMITE SUPERIORE dell'IM,") + print(" quindi l'aritmetica di 1d che ne segue e' conservativa.") + for p in acc["positions"]: + print(f" {p['instrument']:<22} {p['direction']:>5} ${float(p['size']):>8,.2f} " + f"leverage={p.get('leverage')}") + else: + print(" conto non letto (finestra vietata, --no-net o cache assente)") + + # ---------------------------------------------------------------- 1d) l'aritmetica + print("\n 1d) [CALC] L'ARITMETICA CHE DECIDE — quanto margine servirebbe davvero a SKH01") + Eu = E if E else 635.73 + cap = min(float(cfg["max_notional_per_asset_usd"]), Eu * float(cfg["max_notional_per_asset_frac"])) + tpmax = {} + tp = TrendPortfolio(**CANONICAL) + for a in ASSETS: + t = np.nan_to_num(np.asarray(tp.target_series(resample_1d(load_data(a, "1h"))), float)) + tpmax[a] = (float(t.max()), float((t > 1.0).mean())) + print(f" equity assunta ${Eu:,.2f} [CONTO] · cap/asset ${cap:,.0f} [CODICE config/live.json]") + print(f" target massimo storico di TP01: BTC {tpmax['BTC'][0]:.3f} · ETH {tpmax['ETH'][0]:.3f} " + f"(> 1,0x nel {np.mean([tpmax[a][1] for a in ASSETS]):.3%} dei giorni)") + tp_gross = sum(min(cap, WEIGHT * Eu * W_TP * min(tpmax[a][0], 2.0)) for a in ASSETS) + tp_gross_1x = sum(min(cap, WEIGHT * Eu * W_TP * 1.0) for a in ASSETS) + skh_gross = sum(WEIGHT * Eu * W_SKH for a in ASSETS) + imr = im_rate if im_rate else 0.02 + need = skh_gross * imr + free = Eu - tp_gross_1x + print(f"\n CASO PEGGIORE (TP01 al massimo, SKH01 a mercato su entrambi gli asset):") + print(f" spot comprato da TP01 ${tp_gross_1x:>8,.2f} ({tp_gross_1x/Eu:.0%} del conto)") + print(f" USDC che resta libero ${free:>8,.2f}") + print(f" nozionale perp di SKH01 ${skh_gross:>8,.2f}") + print(f" margine che richiede ${need:>8,.2f} (a {imr:.2%} misurato in 1c)") + print(f" → copertura {free/need:>8,.1f}x") + print(" 📌 [DEDOTTO] Il rapporto NON dipende dal capitale: entrambi i termini sono") + print(" proporzionali a E. Se lo spot valesse ZERO come collaterale, la gamba SKH01") + print(f" resterebbe marginata {free/need:.0f} volte. **Il margine non uccide il lead**,") + print(" e non lo ucciderebbe a nessuna taglia finche' il libro sta a lordo <= 1x") + print(" con lo split 75/25. L'haircut, che non e' leggibile, non e' binding.") + print(f" ⚠️ Perche' il lordo resta <= 1x: cap = equity x {cfg['max_notional_per_asset_frac']}") + print(f" su {len(ASSETS)} asset (guardia `test_leva_massima_da_config_resta_sotto_o_uguale_a_1x`).") + print(" Se un giorno esistesse la chiave di SCALA (SPEC-scale-key, §38), questa") + print(" copertura si dividerebbe per k — a k=1,25x resterebbe comunque ~40x.") + + # ---------------------------------------------------------------- 1e) il codice + print("\n 1e) [CODICE] Cio' che il VENUE permette non e' cio' che il LIBRO sa fare") + from src.live import shadow as SH + src_eq = inspect.getsource(SH._equity) + solo_usdc = 'account_summary("USDC")' in src_eq and "if eq > 1:" in src_eq + from src.live.deribit import DeribitRead as DR + src_pos = inspect.getsource(DR.position_usd) + per_instr = 'instrument_name") == instrument' in src_pos + print(f" src/live/shadow.py::_equity legge SOLO account_summary('USDC') -> {solo_usdc}") + print(" e ripiega sulle valute-moneta SOLO se quell'equity e' <= 1 (`if eq > 1: return`).") + print(f" src/live/deribit.py::position_usd matcha per instrument_name -> {per_instr}") + print(" e cerca fra `get_positions(currency=settle)`: uno spot COMPRATO e' un SALDO,") + print(" non una posizione con quel nome.") + print(" → [DEDOTTO] CONSEGUENZA MISURABILE, e non e' piccola:") + print(f" comprando ${tp_gross_1x:,.0f} di spot, l'equity che il libro LEGGE passerebbe da") + print(f" ${Eu:,.0f} a ${free:,.0f} ({free/Eu:.0%}) → il sizing di TUTTO il libro si") + print(f" ridurrebbe al {free/Eu:.0%}, e la gamba spot non comparirebbe fra le posizioni") + print(" → il riconciliatore la ricomprerebbe. **Non e' un cambio di strumento: e' un") + print(" cambio del percorso di sizing del libro live.**") + print(" ⚠️ Questo NON dice che il venue sbagli: dice che `_equity` e' scritta per un conto") + print(" mono-valuta. Il rimedio (sommare i saldi moneta x mark) e' poche righe — ma sono") + print(" righe sul percorso con soldi veri, e non e' questo filone a scriverle.") + + # ---------------------------------------------------------------- 1f) il rendimento dell'USDC + print("\n 1f) IPOTESI MIA, NATA E REFUTATA QUI: «convertire USDC in spot rinuncia a un interesse»") + apr = pool.get("USDC", (None, None))[1] + print(f" [VENUE] get_currencies dichiara USDC `apr` = {apr} (USDT 0,0 · BTC 0,0 · ETH 0,0)") + print(" Se quel tasso fosse accreditato a questo conto, mettere il 75% del capitale in") + print(f" spot BTC/ETH (apr 0,0) costerebbe fino a {(apr or 0)*0.75:.2f}%/anno = meta' del") + print(" vantaggio di §39. Va MISURATO, non assunto.") + runs = flat_runs() + if runs: + print(f"\n [LOG] logs/cron_book.log — periodi in cui il conto e' stato FLAT (nessuna") + print(" posizione: equity = balance, e l'unica cosa che potrebbe muoverla e' l'interesse)") + print(f" {'da':>20} {'a':>20} {'giri':>6} {'giorni':>7} {'equity':>18} {'atteso a apr':>13}") + for r0, r1, n, days, e0, e1 in runs[:6]: + att = e0 * (apr or 0) / 100 * days / 365.25 + print(f" {r0:>20} {r1:>20} {n:>6} {days:>7.1f} " + f"${e0:>8.2f} -> ${e1:>7.2f} {att:>+13.3f}") + best = max(runs, key=lambda r: r[3]) + att = best[4] * (apr or 0) / 100 * best[3] / 365.25 + print(f"\n ❌ REFUTATA: sul run piu' lungo ({best[3]:.1f} giorni) l'equity si e' mossa di") + print(f" ${best[5]-best[4]:+.2f} contro i ${att:+.2f} che {apr:.2f}%/anno avrebbe prodotto.") + bound = 0.005 / best[4] * 365.25 / best[3] * 100 + print(f" La stampa e' a 2 decimali → limite superiore su QUALSIASI interesse:") + print(f" < {bound:.3f}%/anno. **L'USDC di questo conto non rende nulla.**") + print(" (Un accredito MENSILE e' escluso: un run attraversa il 31/07-01/08 con d=0,00.)") + print(" 📌 Il campo `apr` e' letto dal venue ma la sua SEMANTICA non e' documentata") + print(" nella risposta: sui token a rendimento (STETH 2,24 · BUIDL 3,49 · USDE 4,00)") + print(" descrive il rendimento dell'ASSET. Su USDC descrive qualcosa che questo") + print(" conto non riceve. → l'obiezione del rendimento perso e' MORTA, e il lead") + print(" non deve pagarla.") + + # ---------------------------------------------------------------- 1g) l'ignoto + print("\n 1g) COSA NON E' DETERMINABILE senza un conto reale (dichiarato, non dedotto)") + print(" (i) L'HAIRCUT del collaterale BTC/ETH. Nessun endpoint pubblico lo espone") + print(" (`public/get_collateral_info` non esiste: -32601 Method not found).") + print(" → non binding per 1d, ma resta ignoto.") + print(" (ii) Se `account_summary('USDC').equity` includa il valore del saldo BTC/ETH.") + print(" Il conto tiene 7,8e-05 ETH (~$0,19): troppo piccolo per discriminare a 2") + print(" decimali. Il wrapper cerbero-mcp espone 8 campi e NON quelli che") + print(" risolverebbero la domanda (`cross_collateral_enabled`, `spot_reserve`,") + print(" `initial_margin`, `maintenance_margin`).") + print(" (iii) Se `get_positions` restituisca lo spot come posizione. Il wrapper non") + print(" passa `kind` e oggi non c'e' spot da vedere.") + print(" → Le tre si risolvono con UN acquisto di prova da $7,73 (lotto minimo BTC_USDC)") + print(" leggendo equity/available/positions prima e dopo. **E' un ORDINE: fuori dal") + print(" perimetro di questo filone, e' una decisione dell'operatore.**") + return dict(E=Eu, cap=cap, im=imr, free=free, need=need, spec=spec, apr=apr, + tp_gross=tp_gross_1x, skh_gross=skh_gross) + + +def flat_runs() -> list[tuple]: + """[LOG] Run consecutivi di giri orari con conto FLAT: (t0, t1, n giri, giorni, eq0, eq1).""" + p = ROOT / "logs" / "cron_book.log" + if not p.exists(): + return [] + blocks = re.split(r"===== (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) cron_book =====", p.read_text()) + rows = [] + for i in range(1, len(blocks), 2): + m = re.search(r"conto reale\s*:\s*\$([\d.,]+)", blocks[i + 1]) + pos = [float(x.replace(",", "")) for x in re.findall(r"pos \$([+-][\d,.]+)", blocks[i + 1])] + if m and pos and all(abs(x) < 1 for x in pos): + rows.append((blocks[i], float(m.group(1).replace(",", "")))) + out, cur, prev = [], [], None + for ts, eq in rows: + t = dt.datetime.strptime(ts, "%Y-%m-%dT%H:%M:%SZ") + if prev and (t - prev).total_seconds() <= 3900: + cur.append((ts, eq)) + else: + if len(cur) > 24: + out.append(cur) + cur = [(ts, eq)] + prev = t + if len(cur) > 24: + out.append(cur) + res = [] + for run in out: + t0 = dt.datetime.strptime(run[0][0], "%Y-%m-%dT%H:%M:%SZ") + t1 = dt.datetime.strptime(run[-1][0], "%Y-%m-%dT%H:%M:%SZ") + res.append((run[0][0], run[-1][0], len(run), (t1 - t0).total_seconds() / 86400, + run[0][1], run[-1][1])) + res.sort(key=lambda r: -r[3]) + return res + + +# =========================================================================== +# 2) Q2 — IL VALORE DELLA SEPARAZIONE +# =========================================================================== +def q2a() -> dict: + """T1 sbloccato — RIPRODOTTO, non citato a memoria.""" + sez("Q2 (i) — T1 SBLOCCATO: il numero pubblicato riprodotto, e la lente giusta") + print(" Il 26/07 T1 valeva +0,054 Sharpe FULL / +0,061 HOLD di libro (mediana APPAIATA sui") + print(" 23 offset, `onbook_tp` contro `hourly`) ed era bloccato dal netting. Qui si RIFA' il") + print(" conto con la macchineria originale (`r0726_skh_onbook`), e si aggiunge la lente che") + print(" il 26/07 stesso ha stabilito essere quella del live vero: `fastdetect` (rilevazione") + print(" INTRA-BARRA + uscita a mercato entro ~1h), non `hourly`.\n") + modes = ("hourly", "fastdetect", "onbook_tp", "onbook", "canonical") + A = {m: [] for m in modes} + for off in r02.OFFSETS: + for m in modes: + s, _ = OB.skh_series(off, m) + A[m].append(OB.sh3(OB.book(s, W_SKH))) + A = {m: np.array(v) for m, v in A.items()} + print(f" {'modalita':<44} {'FULL med':>9} {'HOLD med':>9} {'DD med':>8}") + for m in modes: + med = np.median(A[m], axis=0) + print(f" {OB.LABEL[m]:<44} {med[0]:>9.3f} {med[2]:>9.3f} {med[3]:>7.1%}") + print("\n differenze APPAIATE per-offset (mediana, e in quanti dei 23 offset e' positiva):") + out = {} + for base in ("hourly", "fastdetect"): + for m in ("onbook_tp", "onbook"): + d0 = A[m][:, 0] - A[base][:, 0] + d2 = A[m][:, 2] - A[base][:, 2] + out[(m, base)] = (float(np.median(d0)), int((d0 > 0).sum()), + float(np.median(d2)), int((d2 > 0).sum())) + print(f" {m:>10} - {base:<11} dFULL {np.median(d0):>+7.3f} ({int((d0>0).sum()):>2}/23)" + f" dHOLD {np.median(d2):>+7.3f} ({int((d2>0).sum()):>2}/23)") + dfl = A["fastdetect"][:, 0] - A["hourly"][:, 0] + print(f"\n [CALC] fastdetect - hourly (= quanto la lente `hourly` SOTTOSTIMA il live):") + print(f" dFULL {np.median(dfl):>+.3f} in {int((dfl>0).sum())}/23 offset " + f"(il 26/07 pubblico' +0,081)") + a, b = out[("onbook_tp", "hourly")], out[("onbook_tp", "fastdetect")] + print(f"\n 📌 IL PUNTO: il numero pubblicato (+{a[0]:.3f} FULL / +{a[2]:.3f} HOLD contro `hourly`)") + print(f" e' RIPRODOTTO. Ma contro il path che il live gia' percorre (`fastdetect`) il TP") + print(f" a limite vale {b[0]:+.3f} FULL ({b[1]}/23) e {b[2]:+.3f} HOLD ({b[3]}/23).") + if b[0] <= 0: + print(" → **Sbloccare T1 non compra niente: lo comprerebbe a un path PEGGIORE di quello") + print(" che gira.** E' esattamente la conclusione del 26/07 («il fix richiesto sarebbe") + print(" un DECLASSAMENTO»), qui ri-misurata sulla banda appaiata invece che a off 0.") + else: + print(" → il TP a limite aggiungerebbe ancora qualcosa sopra il live: taglia sopra.") + return out + + +def q2b() -> dict: + """Il funding risparmiato dalla sola gamba TP01.""" + sez("Q2 (ii) — IL FUNDING RISPARMIATO dalla gamba TP01 (riprodotto da §35/§39)") + with contextlib.redirect_stdout(io.StringIO()): + F = FD.sezione0()["F"] + rate = FD.build_rate(F) + alloc = 600.0 + s_perp, dg = FD.tp01_realistic(alloc * W_TP, rate) + s_spot, _ = FD.tp01_realistic(alloc * W_TP, None) + a, b = FD.stat(s_spot), FD.stat(s_perp) + pagato = float(np.mean([dg[x]["fund"].mean() for x in ASSETS])) * 365.25 + print(f" {'':<34} {'ShFULL':>8} {'ShHOLD':>8} {'drift':>8} {'vol':>8} {'maxDD':>8}") + print(f" {'TP01 su PERP (oggi)':<34} {b['full']:>8.3f} {b['hold']:>8.3f} {b['drift']:>8.2%} " + f"{b['vol']:>8.2%} {b['dd']:>8.1%}") + print(f" {'TP01 senza carry (= SPOT)':<34} {a['full']:>8.3f} {a['hold']:>8.3f} {a['drift']:>8.2%} " + f"{a['vol']:>8.2%} {a['dd']:>8.1%}") + print(f" {'DELTA':<34} {a['full']-b['full']:>+8.3f} {a['hold']-b['hold']:>+8.3f} " + f"{a['drift']-b['drift']:>+8.2%} {a['vol']-b['vol']:>+8.2%} {a['dd']-b['dd']:>+8.1%}") + print(f"\n funding pagato dalla gamba TP01: {pagato:+.2%}/anno di sleeve [CALC su dati VENUE]") + print(f" a peso di libro (x{W_TP}): {(a['drift']-b['drift'])*W_TP:+.2%}/anno di drift di LIBRO") + print(f" → §39 pubblico' +1,55%/anno sulla variante E: riprodotto " + f"({(a['drift']-b['drift'])*W_TP:+.2%} + il residuo di base del perpetual, +0,07%).") + print(" 📌 La vol NON cambia (terza cifra): e' l'unica variante che alza il drift senza") + print(" alzare il rischio. E' questo, e non lo Sharpe, il motivo per cui i MURI si muovono.") + return dict(d_drift_book=(a["drift"] - b["drift"]) * W_TP, pagato=pagato, + tp_drift_spot=a["drift"]) + + +# --------------------------------------------------------------------------- il netting +def skh_pos_path(a: str, off: int = 0) -> tuple[pd.DatetimeIndex, np.ndarray, dict]: + """Segno di SKH01 su griglia 230m, replicando ESATTAMENTE `backtest_signals` (stessa + regola d'uscita, stessa priorita' SL, stesso non-overlap). Validato contro il suo + `n_trades` e la sua `time_in_market`: se diverge, il controllo lo dice.""" + daily, m, ltf, ent = r02.run_asset(a, off) + c = ltf["close"].values.astype(float) + h = ltf["high"].values.astype(float) + lo = ltf["low"].values.astype(float) + n = len(c) + pos = np.zeros(n) + busy, ntr, bars = -1, 0, 0 + for i in range(n): + e = ent[i] if i < len(ent) else None + if e is None or e.get("dir", 0) == 0 or i <= busy: + continue + d = int(e["dir"]); tp = e.get("tp"); sl = e.get("sl"); mb = int(e.get("max_bars") or 24) + ex = min(i + mb, n - 1) + for j in range(i + 1, min(i + mb + 1, n)): + if sl is not None and ((d == 1 and lo[j] <= sl) or (d == -1 and h[j] >= sl)): + ex = j; break + if tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and lo[j] <= tp)): + ex = j; break + ex = j + pos[i + 1:ex + 1] = d + busy, ntr, bars = ex, ntr + 1, bars + (ex - i) + idx = pd.to_datetime(ltf["timestamp"].values + r02.MS_LTF, unit="ms", utc=True) + chk = dict(ntr=ntr, ntr_ref=int(m.n_trades), bars=bars, + bars_ref=int(round(m.time_in_market * n))) + return idx, pos, chk + + +def walk_bps(levels: list, usd: float, mid: float | None) -> float | None: + """Costo in bps di attraversare `usd` dollari sul lato dato del libro, rispetto al mid. + None se il libro visibile non basta. PURA.""" + if not levels: + return None + mid = mid or levels[0][0] + resto, speso, preso = usd, 0.0, 0.0 + for px, am in levels: + cap = px * am + q = min(resto, cap) + speso += q + preso += q / px + resto -= q + if resto <= 1e-9: + break + if resto > 1e-9 or preso <= 0: + return None + return (speso / preso / mid - 1) * 1e4 + + +def q2c(Q1: dict) -> dict: + """Il COSTO del netting perso — simulato sul percorso di produzione, cadenza oraria.""" + sez("Q2 (iii) — IL COSTO DEL NETTING PERSO (il lato che §39 stimava «1-2%»)") + E = Q1["E"]; cap = Q1["cap"]; imr = Q1["im"] + minu = 5.0 + print(f" Simulazione della RICONCILIAZIONE ORARIA (quella che gira), 2 percorsi a parita' di") + print(f" segnale: NETTA (oggi, un ordine per asset) contro SEPARATA (TP01 spot + SKH01 perp).") + print(f" E = ${E:,.2f} costante [ASSUNTO] · cap/asset ${cap:,.0f} · min ordine ${minu:.0f} " + f"[CODICE config]") + print(" Il nozionale in USD si muove col prezzo anche a segnale fermo: e' la prima causa di") + print(" ordini nel live, ed e' modellata (pos *= px[t]/px[t-1]).\n") + + tp = TrendPortfolio(**CANONICAL) + res = {} + for a in ASSETS: + h1 = load(a, "1h") + idx1 = pd.DatetimeIndex(pd.to_datetime(h1["datetime"], utc=True)) + px = h1["close"].values.astype(float) + d1 = resample_1d(load_data(a, "1h")) + tgt = np.nan_to_num(np.asarray(tp.target_series(d1), float)) + di = pd.DatetimeIndex(pd.to_datetime(d1["datetime"], utc=True)) + # il target deciso sulla barra i e' noto alla sua CHIUSURA -> vale da i+1 (causale) + tps = pd.Series(tgt, index=di).shift(1).reindex(idx1, method="ffill").fillna(0.0).values + si, sp, chk = skh_pos_path(a, 0) + sks = pd.Series(sp, index=si).reindex(idx1, method="ffill").fillna(0.0).values + ok = (chk["ntr"] == chk["ntr_ref"]) and abs(chk["bars"] - chk["bars_ref"]) <= 1 + print(f" [CONTROLLO] {a}: percorso SKH01 ricostruito = harness? trade " + f"{chk['ntr']} vs {chk['ntr_ref']}, barre in mercato {chk['bars']} vs " + f"{chk['bars_ref']} -> {'OK' if ok else 'DIVERGE'}") + assert ok, f"ricostruzione SKH01 divergente su {a}" + res[a] = dict(px=px, tp=tps, sk=sks, idx=idx1) + + def simula(a: str, sep: bool) -> dict: + d = res[a]; px, tps, sks = d["px"], d["tp"], d["sk"] + n = len(px) + p_net = p_tp = p_sk = 0.0 + ordini = ord_live = ord_spot = 0 + turn_perp = turn_spot = gross_sum = track = 0.0 + # finestra del libro LIVE (arming del cron_book): serve come CONTROLLO POSITIVO del + # conteggio ordini contro le 18 esecuzioni realmente avvenute. + t_live = pd.Timestamp("2026-06-23", tz="UTC") + anni_live = (d["idx"][-1] - t_live).days / 365.25 + for t in range(1, n): + g = px[t] / px[t - 1] + p_net *= g; p_tp *= g; p_sk *= g + raw_tp = WEIGHT * E * W_TP * max(tps[t], 0.0) + raw_sk = WEIGHT * E * W_SKH * sks[t] + if sep: + # cap: si conserva il tetto per-asset sul LORDO delle due gambe + lordo = abs(raw_tp) + abs(raw_sk) + k = min(1.0, cap / lordo) if lordo > 0 else 1.0 + t_tp, t_sk = raw_tp * k, raw_sk * k + if abs(t_tp - p_tp) >= minu: + turn_spot += abs(t_tp - p_tp); p_tp = t_tp; ordini += 1; ord_spot += 1 + ord_live += int(d["idx"][t] >= t_live) + if abs(t_sk - p_sk) >= minu: + turn_perp += abs(t_sk - p_sk); p_sk = t_sk; ordini += 1 + ord_live += int(d["idx"][t] >= t_live) + gross_sum += abs(p_tp) + abs(p_sk) + track += abs((p_tp + p_sk) - (t_tp + t_sk)) + else: + t_net = max(-cap, min(cap, raw_tp + raw_sk)) + if abs(t_net - p_net) >= minu: + turn_perp += abs(t_net - p_net); p_net = t_net; ordini += 1 + ord_live += int(d["idx"][t] >= t_live) + gross_sum += abs(p_net) + track += abs(p_net - t_net) + anni = (d["idx"][-1] - d["idx"][0]).days / 365.25 + return dict(ordini=ordini / anni, spot=turn_spot / anni, perp=turn_perp / anni, + gross=gross_sum / (n - 1), anni=anni, track=track / (n - 1), + ord_live=ord_live, anni_live=anni_live, ord_spot=ord_spot / anni) + + print() + tot = {} + for sep in (False, True): + agg = dict(ordini=0.0, spot=0.0, perp=0.0, gross=0.0, track=0.0, ord_live=0.0, + ord_spot=0.0) + for a in ASSETS: + r = simula(a, sep) + for k in agg: + agg[k] += r[k] + r0 = simula("BTC", sep) + agg["anni"] = r0["anni"] + agg["ord_live"] = agg["ord_live"] / r0["anni_live"] + tot["SEPARATA" if sep else "NETTA"] = agg + print(f" {'percorso':<12} {'ordini/anno':>12} {'turnover perp':>14} {'turnover spot':>14} " + f"{'lordo medio':>12}") + for k, v in tot.items(): + print(f" {k:<12} {v['ordini']:>12.1f} {v['perp']:>13,.0f}$ {v['spot']:>13,.0f}$ " + f"{v['gross']:>11,.2f}$") + dn = tot["SEPARATA"]; nn = tot["NETTA"] + print(f" {'DELTA':<12} {dn['ordini']-nn['ordini']:>+12.1f} " + f"{dn['perp']-nn['perp']:>+13,.0f}$ {dn['spot']-nn['spot']:>+13,.0f}$ " + f"{dn['gross']-nn['gross']:>+11,.2f}$") + print(f"\n su {nn['anni']:.1f} anni di storia (2019-03 -> oggi), a capitale COSTANTE ${E:,.0f}") + print(f"\n ⚠️ MENO ORDINI NON E' MEGLIO — il controllo giusto non e' il conteggio ma il") + print(f" TRACKING (5ª occorrenza della lezione «banda in valuta assoluta», 27/07):") + print(f" scarto medio |posizione tenuta - target| : NETTA ${nn['track']:.2f} · " + f"SEPARATA ${dn['track']:.2f}") + print(f" La separata fa {nn['ordini']-dn['ordini']:.1f} ordini/anno in MENO perche' la banda da $5 e'") + print(f" applicata a due target piu' PICCOLI invece che alla loro somma: filtra di piu'.") + print(f" Lo scarto {'peggiora' if dn['track'] > nn['track'] else 'migliora'} di " + f"${dn['track']-nn['track']:+.2f} ({(dn['track']-nn['track'])/max(nn['track'],1e-9):+.0%}).") + print(f"\n [CONTROLLO POSITIVO] il conteggio ordini regge sul campione dove esiste la verita'?") + print(f" simulato sulla finestra del libro live (dal 2026-06-23): " + f"NETTA {nn['ord_live']:.0f} ordini/anno") + nex = len([x for x in (ROOT / "data" / "live" / "book_executions.jsonl") + .read_text().splitlines() if x.strip()]) + gg = (pd.Timestamp.now(tz="UTC") - pd.Timestamp("2026-06-23", tz="UTC")).days + print(f" osservato: {nex} esecuzioni in {gg} giorni = {nex*365.25/gg:.0f}/anno [LOG]") + print(f" → la simulazione e' nello stesso ordine di grandezza dell'osservato; la media su") + print(f" 8 anni ({nn['ordini']:.0f}/anno) e' piu' alta perche' il 2026 e' un anno in cui il libro") + print(f" e' stato flat l'85% del tempo. **Il DELTA fra i due percorsi, non il livello,") + print(f" e' la grandezza su cui si decide.**") + + fee_net = nn["perp"] * FEE_PERP_SIDE + fee_sep_oggi = dn["perp"] * FEE_PERP_SIDE + dn["spot"] * 0.0 + fee_sep_dopo = (dn["perp"] + dn["spot"]) * FEE_PERP_SIDE + print(f"\n [CALC] COMMISSIONI (perp {FEE_PERP_SIDE*1e4:.2f} bps/lato [VENUE/nastro], " + f"spot {Q1['spec'].get('BTC_USDC',{}).get('taker_commission',0)*1e4 if Q1['spec'] else 0:.2f} bps [VENUE]):") + print(f" NETTA (oggi) ${fee_net:>7.2f}/anno = {fee_net/E:>6.3%} di equity") + print(f" SEPARATA, spot a fee ZERO ${fee_sep_oggi:>7.2f}/anno = {fee_sep_oggi/E:>6.3%}" + f" ({(fee_sep_oggi-fee_net)/E:+.3%})") + print(f" SEPARATA, spot al taker del perp ${fee_sep_dopo:>7.2f}/anno = {fee_sep_dopo/E:>6.3%}" + f" ({(fee_sep_dopo-fee_net)/E:+.3%})") + print(" 📌 [DEDOTTO] Il netting NON e' un risparmio di commissioni: e' il contrario. Oggi la") + print(" separazione fa PAGARE MENO, perche' i tre quarti del turnover passano su uno") + print(" strumento a fee zero. Il netting risparmia ORDINI, non dollari.") + + im_net = nn["gross"] * imr + im_sep = dn["gross"] * imr + print(f"\n [CALC] MARGINE IMMOBILIZZATO (a {imr:.2%} misurato in 1c):") + print(f" NETTA lordo medio ${nn['gross']:.2f} -> IM ${im_net:.2f} = {im_net/E:.3%} di equity") + print(f" SEPARATA lordo medio ${dn['gross']:.2f} -> il perp e' solo la gamba SKH01") + print(f" ⚠️ Il «1-2%» stimato da §39 come risparmio del netting su margine e commissioni:") + print(f" misurato, il margine in gioco vale {im_net/E:.3%} di equity e le commissioni") + print(f" {fee_net/E:.3%}/anno. **La stima era alta di un ordine di grandezza.**") + + STRUM = ("BTC_USDC", "BTC_USDC-PERPETUAL", "ETH_USDC", "ETH_USDC-PERPETUAL") + books = {k: get(f"get_order_book?instrument_name={k}&depth=20", f"ob20_{k}") for k in STRUM} + hs = {} + for k, b in books.items(): + if b and b.get("best_bid_price") and b.get("best_ask_price"): + mid = (b["best_bid_price"] + b["best_ask_price"]) / 2 + hs[k] = (b["best_ask_price"] - b["best_bid_price"]) / 2 / mid * 1e4 + # TUTTE le istantanee del libro presenti in cache (ogni corsa ne lascia una): il + # differenziale di spread e' l'input piu' rumoroso di questa sezione e va mostrato + # come BANDA, non come punto. + snaps = [] + for pref in sorted({f.name.rsplit("_", 3)[0] for f in SCRATCH.glob("ob*_*_USDC*.json")}): + bb = {} + for k in STRUM: + f = SCRATCH / f"{pref}_{k}.json" + if f.exists(): + try: + bb[k] = json.loads(f.read_text()) + except Exception: + pass + if len(bb) == 4 and all(bb[k].get("best_bid_price") for k in STRUM): + h = {k: (bb[k]["best_ask_price"] - bb[k]["best_bid_price"]) / + (bb[k]["best_ask_price"] + bb[k]["best_bid_price"]) * 1e4 for k in STRUM} + d = np.mean([h["BTC_USDC"] - h["BTC_USDC-PERPETUAL"], + h["ETH_USDC"] - h["ETH_USDC-PERPETUAL"]]) + ts = pd.Timestamp(bb["BTC_USDC"].get("timestamp", 0), unit="ms", tz="UTC") + snaps.append((pref, ts, float(d))) + if hs: + print("\n [VENUE] ⚠️ LA FEE NON E' TUTTO IL COSTO: mezzo spread al top del book, ORA") + for k in ("BTC_USDC", "BTC_USDC-PERPETUAL", "ETH_USDC", "ETH_USDC-PERPETUAL"): + if k in hs: + b = books[k] + print(f" {k:<22} {hs[k]:>6.2f} bps bid {b['best_bid_price']:>10,.2f} " + f"x{b.get('best_bid_amount')} ask {b['best_ask_price']:>10,.2f} " + f"x{b.get('best_ask_amount')}") + d_hs = np.mean([hs.get("BTC_USDC", np.nan) - hs.get("BTC_USDC-PERPETUAL", np.nan), + hs.get("ETH_USDC", np.nan) - hs.get("ETH_USDC-PERPETUAL", np.nan)]) + extra = dn["spot"] * d_hs * 1e-4 + print(f" → lo spot e' {d_hs:+.2f} bps piu' largo del perp (istantanea, 1 momento solo)") + print(f" su ${dn['spot']:,.0f}/anno di turnover deviato = ${extra:+.2f}/anno = " + f"{extra/E:+.3%} di equity") + print(f" Sommato al risparmio di fee ({(fee_sep_oggi-fee_net)/E:+.3%}): " + f"{(fee_sep_oggi-fee_net+extra)/E:+.3%}/anno.") + print("\n ⚠️ E QUANTO VALGA QUELLA ISTANTANEA LO DICONO LE ISTANTANEE STESSE. Questo") + print(" script ne lascia una in cache a ogni corsa; quelle presenti:") + for pref, ts, d in snaps: + print(f" {str(ts)[:19]} UTC differenziale spot-perp {d:>+6.2f} bps " + f"-> sovracosto {d*1e-4*dn['spot']/E:>+7.3%}/anno") + if len(snaps) >= 2: + lo, hi = min(x[2] for x in snaps), max(x[2] for x in snaps) + print(f" → due letture a pochi minuti di distanza danno {lo:.2f} e {hi:.2f} bps " + f"({(hi-lo)/max(lo,1e-9):+.0%}).") + print(f" BANDA del sovracosto d'esecuzione: " + f"[{lo*1e-4*dn['spot']/E:+.3%}, {hi*1e-4*dn['spot']/E:+.3%}]/anno, contro un") + print(f" risparmio di fee di {(fee_sep_oggi-fee_net)/E:+.3%} -> il netto d'esecuzione") + n_lo = (fee_sep_oggi - fee_net) / E + lo * 1e-4 * dn["spot"] / E + n_hi = (fee_sep_oggi - fee_net) / E + hi * 1e-4 * dn["spot"] / E + print(f" CAMBIA SEGNO fra le due letture: [{n_lo:+.3%}, {n_hi:+.3%}]/anno.") + print(f" **Non e' risolvibile con un'istantanea**, e non serve che lo sia:") + print(f" l'ampiezza della banda ({n_hi-n_lo:.3%}) e' il {(n_hi-n_lo)/0.0158:.0%} del lead.") + print(f" Il numero onesto oggi e' «l'esecuzione non decide»; se un giorno") + print(f" dovesse decidere, si raccoglie il libro su piu' ore.") + else: + print(" (una sola istantanea in cache: rilanciare lo script a ore diverse per") + print(" avere la banda — un punto solo non e' una misura di spread)") + print(" ⚠️ Il book si allarga di notte e nei crash. §39 fece la stessa cosa e la") + print(" dichiaro'. Va misurata su piu' ore prima di metterla in un muro.") + print("\n [VENUE] ⚠️ E IL MEZZO SPREAD NON BASTA: il MIGLIOR LIVELLO dello spot e'") + print(" SOTTILE. Profondita' cumulata (20 livelli) e costo di attraversare un") + print(" ticket, misurati camminando il libro:") + tick = dn["spot"] / max(dn["ord_spot"], 1e-9) + print(f" ticket medio della gamba spot a ${E:,.0f} di equity: ~${tick:.0f} " + f"({dn['ord_spot']:.0f} ordini spot/anno) -> scala col capitale") + print(f" {'strumento':<22} {'top ask $':>10} {'20 liv. $':>11} " + + "".join(f"{'$'+f'{tick*m:,.0f}':>10}" for m in (1, 20 / 0.636, 80 / 0.636))) + prof = {} + for k in ("BTC_USDC", "BTC_USDC-PERPETUAL", "ETH_USDC", "ETH_USDC-PERPETUAL"): + b = books.get(k) + if not b or not b.get("asks"): + continue + top = b["asks"][0][0] * b["asks"][0][1] + cum = sum(px * am for px, am in b["asks"]) + prof[k] = cum + costi = [] + for m in (1, 20 / 0.636, 80 / 0.636): + costi.append(walk_bps(b["asks"], tick * m, + (b["best_bid_price"] + b["best_ask_price"]) / 2)) + print(f" {k:<22} {top:>10,.0f} {cum:>11,.0f} " + + "".join((f"{c:>9.1f}b" if c is not None else f"{'FUORI':>10}") for c in costi)) + print(" (le tre colonne sono il costo in bps di attraversare il ticket medio a") + print(f" ${E:,.0f} / $20.000 / $80.000 di equity; 'FUORI' = il libro visibile non basta)") + cb = walk_bps(books["BTC_USDC"]["asks"], tick, None) if books.get("BTC_USDC") else None + print(" 📌 [DEDOTTO] A questa taglia il ticket e' cosi' piccolo che cammina un") + print(" livello solo: il costo vero e' il MEZZO SPREAD, ed e' quello contato") + print(" sopra. La profondita' diventa il vincolo molto dopo i $20k — ma va") + print(" RI-MISURATA a ogni salto di taglia, come lo slippage del libro (§ondata).") + sp_cost = extra + + opp = 0.0 + tots = 0 + for a in ASSETS: + d = res[a] + m = (d["tp"] > 0) & (d["sk"] < 0) + opp += int(m.sum()); tots += len(m) + print(f"\n [CALC] Quando il netting serve DAVVERO (segni opposti: TP01 long e SKH01 short):") + print(f" {opp/tots:.2%} delle ore di storia. Li' la netta tiene {'' }un nozionale piu' piccolo") + print(f" della somma delle gambe; nel resto del tempo netta e separata tengono lo STESSO") + print(f" lordo e il netting risparmia solo l'ordine.") + return dict(net=nn, sep=dn, fee_net=fee_net, fee_sep_oggi=fee_sep_oggi, + fee_sep_dopo=fee_sep_dopo, opp=opp / tots, im_net=im_net, + spread=locals().get("sp_cost", 0.0), E=E, + spread_banda=[x[2] * 1e-4 * dn["spot"] for x in snaps]) + + +def q2d() -> None: + """Il percorso LIVE reale, per contro-prova.""" + sez("Q2 (iii-bis) — CONTRO-PROVA SUL PATH LIVE REALE (2 mesi, 1443 giri orari)") + p = ROOT / "logs" / "cron_book.log" + blocks = re.split(r"===== (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) cron_book =====", p.read_text()) + pat = re.compile(r"(BTC|ETH) TP ([+-][\d.]+) · SKH ([+-]\d)\((flat|LONG|SHORT)") + stato = {"flat": 0, "LONG": 0, "SHORT": 0} + giri = 0 + both = 0 + for i in range(1, len(blocks), 2): + rows = pat.findall(blocks[i + 1]) + if rows: + giri += 1 + for a, t, s, st in rows: + stato[st if st in stato else "flat"] += 1 + if float(t) > 0 and int(s) < 0: + both += 1 + ex = [json.loads(x) for x in (ROOT / "data" / "live" / "book_executions.jsonl") + .read_text().splitlines() if x.strip()] + print(f" [LOG] {giri} giri con segnale leggibile · stati di SKH01 (conteggio ASSET-ORA, " + f"2 asset per giro):") + print(f" flat {stato['flat']} · LONG {stato['LONG']} · SHORT {stato['SHORT']} " + f"(totale {sum(stato.values())} = 2 x {giri})") + print(f" [LOG] asset-ora con TP01 long E SKH01 short (il caso in cui il netting conta): {both}") + print(f" [LOG] {len(ex)} esecuzioni reali; fee totale pagata ${sum(e['fee'] for e in ex):.4f}") + print(f" su nozionale ${sum(abs(e['filled']*e['price']) for e in ex):,.2f} = " + f"{sum(e['fee'] for e in ex)/sum(abs(e['filled']*e['price']) for e in ex)*1e4:.2f} bps/lato") + print(" 📌 In 2 mesi di vita del libro SKH01 non e' MAI stato short: il campione live NON") + print(" puo' misurare il costo del netting, e va detto invece di estrapolarlo. La misura") + print(" che decide e' quella su 7,4 anni di sezione precedente.") + print(" ⚠️ `ts_utc` in book_executions.jsonl NON e' l'ora del fill (18/18 a 00:00:00): l'ora") + print(" vera sta solo in logs/cron_book.log. Qui non serve (si contano fee e nozionale).") + + +# =========================================================================== +# 3) Q3 — LA SORVEGLIANZA MANCANTE +# =========================================================================== +def q3(Q1: dict, Q2b: dict, Q2c: dict) -> None: + sez("Q3 — LA SORVEGLIANZA: `fee_watch` non vedrebbe una gamba spot") + import importlib.util + _spec = importlib.util.spec_from_file_location( + "fee_watch_ro", ROOT / "scripts" / "live" / "fee_watch.py") + FW = importlib.util.module_from_spec(_spec) + _spec.loader.exec_module(FW) # import puro: il main e' sotto __main__, non gira + src = inspect.getsource(FW) + derivato = "INSTRUMENTS = tuple(BOOK_INSTRUMENT[a] for a in sorted(BOOK_INSTRUMENT))" in src + print(f" [CODICE] scripts/live/fee_watch.py DERIVA i suoi strumenti dal libro -> {derivato}") + print(f" INSTRUMENTS = {FW.INSTRUMENTS}") + print(" → una gamba su BTC_USDC/ETH_USDC non comparirebbe: il sorvegliante continuerebbe a") + print(" dire OK guardando due strumenti su cui passerebbe solo un quarto del turnover.") + print(" (E' ESATTAMENTE il difetto corretto il 21/08, in cui sorvegliava gli inverse.)") + print("\n LA RIGA — non scritta, dichiarata (questo filone non tocca scripts/live/):") + print(" in `src/live/book.py`, accanto a INSTRUMENT:") + print(' SPOT_INSTRUMENT = {"BTC": "BTC_USDC", "ETH": "ETH_USDC"}') + print(" in `scripts/live/fee_watch.py`:") + print(" from src.live.book import SPOT_INSTRUMENT as BOOK_SPOT") + print(" INSTRUMENTS = (tuple(BOOK_INSTRUMENT[a] for a in sorted(BOOK_INSTRUMENT))") + print(" + tuple(BOOK_SPOT[a] for a in sorted(BOOK_SPOT)))") + print(f" ⚠️ NON basta: `convenzione('BTC_USDC')` = '{FW.convenzione('BTC_USDC')}' -> il") + print(" cross-check sui fill reali tornerebbe None (= «non misurata», che e' onesto ma") + print(" cieco). Servirebbe una terza famiglia 'spot' (amount in BASE, fee in USDC).") + print(" Senza, si sorveglia il TIER ma non cio' che si paga davvero.") + + sp = Q1["spec"].get("BTC_USDC") + if sp: + print(f"\n [VENUE] listino spot OGGI: taker {float(sp['taker_commission'])*1e4:.2f} bps · " + f"maker {float(sp['maker_commission'])*1e4:.2f} bps · " + f"min lotto {sp['min_trade_amount']} BTC") + print(" ⚠️ La fee 0 e' dichiarata TEMPORANEA nell'annuncio del 1° agosto («spot a zero fino") + print(" al collegamento con Coinbase»). Fonte: articolo Insights, SECONDARIA.") + costo = (Q2c["fee_sep_dopo"] - Q2c["fee_sep_oggi"]) / Q1["E"] + print(f" Se lo spot passasse al taker dei lineari (3,50 bps) il lead perderebbe " + f"{costo:.3%}/anno") + print(f" ({costo/Q2b['d_drift_book']:.0%} del guadagno di {Q2b['d_drift_book']:.2%}): " + f"**sopravviverebbe**. La ragione per cui") + print(" sorvegliarlo non e' che lo ucciderebbe, e' che oggi nessuno se ne accorgerebbe —") + print(" e un costo che nessuno vede non entra in nessun muro.") + + +# =========================================================================== +# 4) Q4 — IL RISCHIO CHE SI AGGIUNGE +# =========================================================================== +def q4(Q1: dict, Q2b: dict, Q2c: dict) -> None: + sez("Q4 — IL RISCHIO: detenere cripto non e' tenere un derivato") + E = Q1["E"] + print(" (i) VENUE — invariato, e non e' una buona notizia: il rischio era GIA' totale") + print(" (decisione dell'operatore 26/07: 100% Deribit fino a $20k). Lo spot non lo") + print(" aumenta, ma cambia COSA c'e' sul conto: oggi margine USDC, domani cripto.") + print(" In un fallimento la differenza e' di procedura concorsuale, non di quantita'.") + print(" [ASSUNTO] nessun numero: `p` non e' stimato, e' un parametro dell'operatore.") + + print("\n (ii) FISCO — e' qui che sta il numero grosso, ed e' lo stesso del 30/07 e di §39.") + tp_share = Q2b["tp_drift_spot"] * W_TP + print(f" La gamba TP01 porta ~{tp_share:.1%}/anno di drift al libro [CALC da Q2ii].") + print(f" Se i derivati fossero `c-quater` (26%) e lo spot `c-sexies` (33%), spostare") + print(f" TP01 sullo spot sposterebbe quel P&L nel comparto piu' caro: 7 punti su") + print(f" {tp_share:.1%} = {0.07*tp_share:.2%}/anno.") + print(f" Confronto diretto col guadagno misurato in Q2(ii): {Q2b['d_drift_book']:+.2%}/anno.") + if 0.07 * tp_share > Q2b["d_drift_book"] * 0.5: + print(" → **la domanda fiscale vale una frazione maggioritaria del guadagno**: si") + print(" risponde prima di provare, e la risposta non e' un backtest.") + print(" ⚠️ Comparti SEPARATI: se i derivati sono `c-quater`, le minusvalenze del perp") + print(" (SKH01) NON compenserebbero le plusvalenze spot (TP01). Oggi si compensano") + print(" perche' stanno nello stesso comparto. Questo e' un costo NUOVO della") + print(" separazione, mai considerato prima. [DEDOTTO, da verificare col commercialista]") + + print("\n (iii) LIQUIDAZIONE — e qui la separazione MIGLIORA, ed e' misurabile.") + print(f" Oggi il 100% del lordo del libro (medio ${Q2c['net']['gross']:.2f}) e' su un perp") + print(" marginato: liquidabile. Separando, la gamba TP01 diventa un ACQUISTO: uno spot") + print(" non ha prezzo di liquidazione, qualunque cosa faccia il prezzo.") + print(f" → esce dal perimetro di liquidazione il {W_TP:.0%} del libro per peso " + f"(gamba TP01),") + print(f" e col lordo misurato in Q2(iii) restano ~${Q2c['sep']['gross']*W_SKH/(W_TP+W_SKH):.0f}" + f" di nozionale liquidabile invece di ${Q2c['net']['gross']:.0f}.") + print(" ⚠️ CONTROPARTITA: il disaster-SL -30% on-book oggi protegge la posizione NETTA.") + print(" Separando, potrebbe stare solo sul perp (SKH01). La gamba spot resterebbe") + print(" senza stop — il che e' coerente (non puo' essere liquidata) ma significa che") + print(" in un blackout prolungato TP01 resta long al 75% del conto senza protezione.") + print(" Oggi il disaster-SL, sulla netta, copre anche quella quota.") + print(" 📌 [DEDOTTO] Il rischio non sparisce: cambia forma. Da «liquidazione forzata") + print(" a -30%» a «esposizione lunga non protetta». Sono due code diverse, e la") + print(" seconda e' quella che TP01 accetta per costruzione (long-flat, non a leva).") + + print("\n (iv) HAIRCUT — leggibile? NO (1g). Binding? NO (1d): la copertura del margine di") + print(f" SKH01 col solo USDC libero e' {Q1['free']/Q1['need']:.0f}x e non dipende dal capitale.") + print(" → e' l'unico caso di questo filone in cui «non lo so» e «non conta» coincidono,") + print(" e va detto in quest'ordine: prima non lo so, poi perche' non conta.") + + +# =========================================================================== +# 5) VERDETTO +# =========================================================================== +def verdetto(Q1: dict, Q2a: dict, Q2b: dict, Q2c: dict) -> None: + sez("VERDETTO") + t1_live = Q2a[("onbook_tp", "fastdetect")] + print(f" Q1 MARGINE il venue NON uccide il lead: BTC/ETH sono nel pool di") + print(f" cross-collateral [VENUE], lo spot ha max_leverage 10 [VENUE], e la") + print(f" gamba SKH01 resterebbe marginata {Q1['free']/Q1['need']:.0f}x dal solo USDC") + print(f" libero [CALC], a QUALUNQUE capitale. Uccide il lead il CODICE:") + print(f" `_equity` legge solo USDC e leggerebbe {Q1['free']/Q1['E']:.0%} del conto vero.") + bnd = Q2c["spread_banda"] or [Q2c["spread"]] + print(f" Q2 VALORE il funding risparmiato e' reale ({Q2b['d_drift_book']:+.2%}/anno di drift") + print(f" di libro, riprodotto). T1 sbloccato vale {t1_live[0]:+.3f} Sharpe FULL") + print(f" contro il path che il live gia' percorre: **zero o negativo**.") + print(f" Il netting perso NON costa commissioni ({(Q2c['fee_sep_oggi']-Q2c['fee_net'])/Q1['E']:+.3%}: e' un") + print(f" RISPARMIO) ne' ordini ({Q2c['sep']['ordini']-Q2c['net']['ordini']:+.1f}/anno): costa TRACKING (+12%) e") + print(f" SPREAD ([{min(bnd)/Q1['E']:.3%},{max(bnd)/Q1['E']:.3%}]/anno su 2 istantanee: lo spot e'") + print(f" 2-4 bps piu' largo del perp, che e' un mercato a UN TICK). I due") + print(f" si compensano quasi: l'esecuzione NON decide questo lead.") + print(f" Q3 SORVEGLIANZA una gamba spot sarebbe INVISIBILE a `fee_watch`, che deriva i suoi") + print(f" strumenti dal libro. Riga dichiarata; serve anche una convenzione") + print(f" 'spot' o il cross-check sui fill resta muto.") + print(f" Q4 RISCHIO la liquidazione MIGLIORA (il 75% del libro esce dal perimetro), il") + print(f" fisco PEGGIORA e vale {0.07*Q2b['tp_drift_spot']*W_TP:.2%}/anno se i derivati sono") + print(f" `c-quater` — piu' della meta' del guadagno.") + E = Q1["E"] + lordo = Q2b["d_drift_book"] + print("\n 📌 IL NUMERO DA CITARE — il lead non vale +1,55%/anno, vale meno, e si scrive") + print(" con la sua colonna d'esecuzione (che §39 aveva stimato e qui e' misurata):") + print(f" funding risparmiato dalla gamba TP01 {lordo:>+8.2%}/anno di drift di libro") + e_lo = (Q2c["fee_sep_oggi"] - Q2c["fee_net"]) / E + min(bnd) / E + e_hi = (Q2c["fee_sep_oggi"] - Q2c["fee_net"]) / E + max(bnd) / E + print(f" esecuzione (fee zero MENO spread), contributo netto " + f"[{-e_hi:+.3%},{-e_lo:+.3%}]/anno") + print(f" (fee {-(Q2c['fee_sep_oggi']-Q2c['fee_net'])/E:+.3%} certo · spread " + f"{-max(bnd)/E:+.3%}..{-min(bnd)/E:+.3%} da {len(bnd)} istantanee del libro)") + print(f" ------------------------------------------------------") + print(f" NETTO OGGI [{lordo-e_hi:+.2%},{lordo-e_lo:+.2%}]/anno") + print(f" NETTO se lo spot torna a 3,50 bps " + f"[{lordo-e_hi-(Q2c['fee_sep_dopo']-Q2c['fee_sep_oggi'])/E:+.2%}," + f"{lordo-e_lo-(Q2c['fee_sep_dopo']-Q2c['fee_sep_oggi'])/E:+.2%}]/anno") + print(f" imposta in piu' se i derivati sono c-quater " + f"{-0.07*Q2b['tp_drift_spot']*W_TP:>+7.2%}/anno ← non misurabile qui") + print(f" NETTO nello scenario fiscale sfavorevole " + f"{lordo-e_hi-(Q2c['fee_sep_dopo']-Q2c['fee_sep_oggi'])/E-0.07*Q2b['tp_drift_spot']*W_TP:>+8.2%}/anno" + f" (estremo conservativo)") + print(" ⚠️ Che il netto di oggi cada su +1,55% — lo stesso numero di §39 — e' una") + print(" COINCIDENZA di due decomposizioni diverse, non una conferma: §39 pubblico'") + print(" +1,55% come delta di drift della variante E (lordo dello spread, che stimava") + print(" a parte); qui il LORDO e' +1,58% (riprodotto entro 0,03 punti) e il netto ci") + print(" ricasca sopra dopo aver sottratto uno spread MISURATO. Due strade, un numero.") + print("\n 📌 IL LEAD REGGE, MA IL SUO VALORE NON E' QUELLO CHE SEMBRAVA.") + print(f" Il valore vero e' il FUNDING, e basta: {Q2b['d_drift_book']:+.2%}/anno lordo.") + print(" La separazione delle gambe — la ragione per cui questo filone e' stato aperto —") + print(" NON aggiunge: T1 e' un declassamento del path che gira, il netting non risparmia") + print(" commissioni, il margine non era il vincolo. E introduce due costi nuovi che") + print(" nessuno aveva contato: la SEPARAZIONE DEI COMPARTI FISCALI (le minusvalenze del") + print(" perp smetterebbero di compensare le plusvalenze spot) e la gamba TP01 senza") + print(" disaster-SL.") + print(" → Il prossimo passo NON e' un backtest: e' (1) la domanda al commercialista e") + print(" (2) un acquisto di prova da $7,73 che risolve le tre incognite di 1g. Entrambi") + print(" stanno fuori dal perimetro di questo filone.") + + +def main() -> None: + print("=" * 100) + print(" r0823_spot_netting — §45 SPOT-NETTING: quanto vale, e quanto costa, separare le gambe") + print("=" * 100) + print(f" {clock()} avvio · finestra vietata ora: {finestra_vietata()} · --no-net: {NO_NET}") + Q1 = q1() + print(f"\n{clock()} Q1 fatta") + A = q2a() + print(f"\n{clock()} Q2(i) fatta") + B = q2b() + print(f"\n{clock()} Q2(ii) fatta") + C = q2c(Q1) + q2d() + print(f"\n{clock()} Q2(iii) fatta") + q3(Q1, B, C) + q4(Q1, B, C) + verdetto(Q1, A, B, C) + sez("PROVA DI SOLA LETTURA") + print(f" chiamate di rete di questa corsa ({len(_NET_CALLS)}):") + for c in _NET_CALLS: + print(f" - {c}") + if not _NET_CALLS: + print(" - nessuna (tutto da cache)") + print(f" scritture: SOLO {SCRATCH} (cache). Nessun file del repo aperto in scrittura.") + print(f" ordini inviati: 0 — `DeribitRead` non ha, by design, alcun metodo di trading.") + print(f"\n{clock()} fine") + + +if __name__ == "__main__": + main() From 279c70222ba2ef6b9277f8689d130e61f9bbc6c9 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 01:20:19 +0000 Subject: [PATCH 070/114] =?UTF-8?q?research(=C2=A745):=20il=20percorso=20-?= =?UTF-8?q?-no-net=20non=20deve=20crollare=20=E2=80=94=20`apr`=20assente?= =?UTF-8?q?=20si=20DICHIARA,=20non=20si=20formatta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trovato dallo smoke test a cache fredda: senza get_currencies la sezione 1f cadeva su TypeError formattando None. Un'analisi che non puo' girare offline non e' riproducibile. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf --- scripts/research/r0823_spot_netting.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/research/r0823_spot_netting.py b/scripts/research/r0823_spot_netting.py index 81407e2..befd6eb 100644 --- a/scripts/research/r0823_spot_netting.py +++ b/scripts/research/r0823_spot_netting.py @@ -317,6 +317,11 @@ def q1() -> dict: # ---------------------------------------------------------------- 1f) il rendimento dell'USDC print("\n 1f) IPOTESI MIA, NATA E REFUTATA QUI: «convertire USDC in spot rinuncia a un interesse»") apr = pool.get("USDC", (None, None))[1] + if apr is None: + print(" [VENUE] get_currencies NON letto in questa corsa -> sezione non eseguibile.") + print(" «non l'ho guardato» non e' «e' zero»: si dichiara, non si assume.") + return dict(E=Eu, cap=cap, im=imr, free=free, need=need, spec=spec, apr=None, + tp_gross=tp_gross_1x, skh_gross=skh_gross) print(f" [VENUE] get_currencies dichiara USDC `apr` = {apr} (USDT 0,0 · BTC 0,0 · ETH 0,0)") print(" Se quel tasso fosse accreditato a questo conto, mettere il 75% del capitale in") print(f" spot BTC/ETH (apr 0,0) costerebbe fino a {(apr or 0)*0.75:.2f}%/anno = meta' del") From 33c24b14b27c39bbe71d1ad7e8f334e1defbff64 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 01:22:54 +0000 Subject: [PATCH 071/114] =?UTF-8?q?registro=20=C2=A745:=20SPOT-NETTING=20?= =?UTF-8?q?=E2=80=94=20il=20lead=20regge=20ma=20vale=20solo=20il=20funding?= =?UTF-8?q?;=20T1=20sbloccato=20e'=20un=20declassamento=20e=20il=20netting?= =?UTF-8?q?=20perso=20risparmia=20commissioni?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 115 +++++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 4b56535..3a0b168 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -710,7 +710,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 42 | TP01-LS | la gamba short del trend a 1d paga il proprio costo, o e' il 2022? | **SCARTATO** — toglie **−1,91%/anno di drift in 0/24 ancore**, fallisce l'iso-vol, +8,80pp di maxDD in 24/24, e **senza il 2022 il divario RADDOPPIA** | | 43 | DATA-UNUSED | quali dati il progetto possiede e nessuna strategia legge — e uno di essi sostiene un'ipotesi? | **SCARTATO** — **la scorta di dati e' esaurita**: un solo dato mai letto ha storia vera (flussi exchange CoinMetrics 2018-2026) e misurato sta **SOTTO il massimo atteso dal puro rumore** | | 44 | DEPOSIT-TIMING | un calendario di versamento CONDIZIONALE batte il piatto, a pari soldi? | **SCARTATO 0/12 in ogni lente** — il buy-the-dip compra davvero il **51% piu' in basso** e arriva al capitale-rendita nel **6,3%** dei casi contro l'**85,4%** del piatto | -| 45 | SPOT-NETTING | separare TP01 e SKH01 su due strumenti: quanto vale, e il margine lo uccide? | _in corso_ | +| 45 | SPOT-NETTING | separare TP01 e SKH01 su due strumenti: quanto vale, e il margine lo uccide? | **LEAD REGGE, SEPARAZIONE SENZA VALORE** — il margine non e' il vincolo (copertura **50x**, invariante al capitale), ma **T1 sbloccato e' un DECLASSAMENTO** (−0,048 contro il path che gira) e il netting perso **risparmia** commissioni: il lead vale **il funding e basta**, [+1,55%, +1,66%]/anno; a bloccarlo e' **il codice di sizing**, non Deribit | | — | **NOTIFIER** (audit di fatto) | 🚨 **PUNTO SINGOLO DI GUASTO nella rete di sicurezza** | il `send()` di Telegram e' **un solo tentativo da 10s, senza retry, con l'esito MAI registrato** — e lo stato `alerted=True` viene scritto **prima** dell'invio: un 🚨 di `venue_watch` perso e' perso **per l'intero episodio**. Tasso di fallimento **misurato 6,9%** (2/29, IC95 [1,9%, 22,0%]) | | 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 | @@ -2482,3 +2482,116 @@ quell'ondata non aveva mai aperto. **VERDETTO: `NESSUN DATO INUTILIZZATO SOSTIENE OGGI UN'IPOTESI NUOVA — LA SCORTA DI DATI DEL PROGETTO E' ESAURITA, E CIO' CHE RESTA NON LETTO E' TELEMETRIA A FINESTRA CORTA, NON STORIA`** — SCARTATO. **Libro, pesi, cron, config INVARIATI.** + +--- + +## 45 — SPOT-NETTING (separare le gambe su due strumenti) + +`scripts/research/r0823_spot_netting.py`. **Sola lettura provata:** `git status` mostra solo questo +file, `git status -- src/ config/ scripts/live/ scripts/cron_*.sh tests/ data/` **vuoto**; ~17 GET +pubbliche + ~13 letture del conto via `DeribitRead` (**nessun metodo di trading esiste in quella +classe, by design**); **ordini inviati: 0**; finestre :05-:10 e :24-:30 rispettate da una guardia +nello script. + +### Q1 — il margine non uccide il lead; **il codice si'** + +**[VENUE]** `get_currencies`: BTC ed ETH hanno `in_cross_collateral_pool: true` (**11 valute su 51** +→ elenco ristretto, non un default). `get_instruments?kind=spot`: `BTC_USDC`/`ETH_USDC` hanno +**`max_leverage: 10`**, taker **0,00 bps**, min lotto 0,0001 → **[DEDOTTO]** lo spot vive dentro il +conto marginato, non in un portafoglio segregato. +**[CONTO]** equity USDC **$636,01**, nozionale lordo $327,52, **margine impegnato $6,55 = 2,000% del +nozionale** (limite superiore: contiene eventuali riserve d'ordine → l'aritmetica che segue e' +conservativa). +**[CALC] caso peggiore** (TP01 al massimo su entrambi gli asset): spot $477, USDC libero **$159**, +nozionale perp di SKH01 $159, margine richiesto $3,18 → **copertura 50,0x**. Entrambi i termini sono +proporzionali a E → **il rapporto non dipende dal capitale**. Anche con lo spot valutato **zero** +come collaterale, SKH01 resta marginato 50 volte. **L'haircut — non leggibile — non e' binding.** + +🚨 **[CODICE, verificato con `inspect.getsource`]** `src/live/shadow.py::_equity` legge **solo** +`account_summary("USDC")` e ripiega sulle valute-moneta solo se quell'equity e' ≤1; `position_usd` +matcha per `instrument_name`. → comprando $477 di spot il libro **leggerebbe $159 invece di $636 +(25%)** e non vedrebbe la gamba, che il riconciliatore **ricomprerebbe**. **Non e' un cambio di +strumento: e' un cambio del percorso di SIZING del live.** + +❌ **Ipotesi dell'agente, nata e refutata nello stesso filone.** `get_currencies` dichiara USDC +`apr` **3,40** (USDT/BTC/ETH 0,0): se accreditato, tenere il 75% in spot costerebbe fino a +**2,55%/anno = meta' del vantaggio**. **[LOG]** 6 run col conto **flat** (il piu' lungo **257 giri +orari = 10,7 giorni**): equity **invariata al centesimo** contro i +$0,59 attesi → limite superiore +su qualunque interesse **< 0,029%/anno**, e un accredito mensile e' escluso (un run attraversa il +31/07-01/08 con Δ=0,00). **L'obiezione e' morta e il lead non deve pagarla.** + +### Q2 — il valore della separazione: **nullo** + +🚨 **(i) T1 sbloccato e' un DECLASSAMENTO.** Con la macchineria originale (`r0726_skh_onbook`, 23 +offset, differenze appaiate): `onbook_tp − hourly` = **+0,051 FULL (19/23) / +0,057 HOLD** → il +pubblicato (+0,054/+0,061) e' **riprodotto**. Ma il 26/07 stesso stabili' che il live fa rilevazione +**intra-barra**, e `fastdetect − hourly` = **+0,082 in 23/23** (pubblicato +0,081). Contro il path +che il live **gia' percorre**: **`onbook_tp − fastdetect` = −0,048 FULL (6/23)**. *Sbloccare T1 +comprerebbe un path peggiore di quello che gira* — conferma indipendente della conclusione del 26/07, +raggiunta per una strada diversa. + +**(ii) Funding risparmiato** [riprodotto da §35/§39]: TP01 su perp drift 13,66% → senza carry 15,77% +(**+2,11%/anno di sleeve**, vol invariata 12,17→12,22%); a peso di libro ×0,75 → **+1,58%/anno**. + +📌 **(iii) Il costo del netting perso: §39 lo stimava "1-2%", ed e' ALTO DI UN ORDINE DI +GRANDEZZA.** Simulazione della riconciliazione **oraria** su 8 anni, percorso SKH01 **validato +contro l'harness** (trade 194/194 BTC, 213/213 ETH): + +| percorso | ordini/anno | turnover perp | turnover spot | lordo medio | +|---|---|---|---|---| +| NETTA | 289,1 | $12.412 | $0 | $84,52 | +| SEPARATA | 282,8 | $8.493 | $3.893 | $85,52 | + +**Commissioni: NETTA $4,34/anno (0,683% di equity) contro SEPARATA $2,97 — un RISPARMIO di +0,216%.** **Il netting non risparmia commissioni.** Ordini −6,3/anno, ma ⚠️ **meno ordini non e' +meglio**: il tracking peggiora del **+12%** ($1,61 → $1,81 di scarto medio dal target) — **5ª +occorrenza** della lezione *"banda in valuta assoluta"*. Margine: irrilevante (IM 0,266% di equity). +**Segni opposti — cioe' le ore in cui il netting serve davvero — 0,61% della storia.** +**[VENUE] Il costo vero e' lo SPREAD:** il perp e' un mercato a **un tick** (0,01-0,02 bps di mezzo +spread), lo spot no (1,5-3,1 bps) → sovracosto **[0,139%, 0,250%]/anno**. ⚠️ **Due istantanee a 4 +minuti danno 2,27 e 4,08 bps (+79%) e il contributo netto dell'esecuzione CAMBIA SEGNO fra le due** +→ si cita come banda **[−0,034%, +0,077%]/anno**, ampiezza = 7% del lead: **l'esecuzione non decide +questo lead**. ✅ **Controllo positivo:** ordini simulati sulla finestra live **132/anno** contro +**114/anno osservati**. ⚠️ **[LOG] In 2 mesi SKH01 non e' MAI stato short** → il campione live **non +puo'** misurare il netting, e l'agente lo dichiara invece di estrapolarlo. Fee reale sul nastro +**3,70 bps/lato**. + +**Il numero da citare:** lordo **+1,58%** → **netto oggi [+1,55%, +1,66%]** → con spot a 3,50 bps +[+1,33%, +1,44%] → **nello scenario fiscale sfavorevole +0,50%**. + +### Q3 — la sorveglianza + +**[CODICE]** `fee_watch` deriva `INSTRUMENTS` da `BOOK_INSTRUMENT` → una gamba spot **non +comparirebbe**: e' esattamente il difetto corretto il 21/08. Riga **dichiarata e non scritta**: +`SPOT_INSTRUMENT` in `src/live/book.py` + concatenazione in `fee_watch`. ⚠️ **Non basta:** +`convenzione('BTC_USDC')` ritorna **`'ignota'`** → il cross-check sui fill tornerebbe `None` +(onesto ma cieco): servirebbe una **terza famiglia 'spot'**. Se la fee tornasse a 3,50 bps il lead +perderebbe 0,216%/anno = 14% del guadagno → **sopravviverebbe**; *la ragione per sorvegliarlo non e' +che lo ucciderebbe, e' che oggi nessuno se ne accorgerebbe*. + +### Q4 — il rischio + +**Venue:** invariato (era gia' totale). **Fisco:** 7 punti di aliquota sulla gamba TP01 = **0,83%/anno += oltre meta' del guadagno** — e 🚨 **un costo nuovo mai contato: se i derivati sono `c-quater` i +comparti si SEPARANO**, quindi le minusvalenze del perp (SKH01) smetterebbero di compensare le +plusvalenze spot (TP01) **[DEDOTTO, per il commercialista]**. **Liquidazione — qui la separazione +MIGLIORA:** lo spot non si liquida, esce dal perimetro il 75% del libro ($85 → ~$21 di nozionale +liquidabile); **contropartita:** il disaster-SL oggi protegge la posizione **netta**, separando +resterebbe solo sul perp e in un blackout **TP01 resterebbe long al 75% del conto senza stop**. +**Haircut:** non leggibile e **non binding** — *unico caso in cui "non lo so" e "non conta" +coincidono, e va detto in quest'ordine*. + +### Non determinabile senza un conto reale +(i) l'haircut del collaterale (`get_collateral_info` → *Method not found*); (ii) se +`account_summary('USDC').equity` **includa** i saldi BTC/ETH (il conto ne tiene $0,19: troppo poco +per discriminare); (iii) se `get_positions` restituisca lo spot. **Tutte e tre si risolvono con un +acquisto di prova da $7,73** leggendo equity/available/positions prima e dopo — **e' un ordine, +quindi decisione dell'operatore.** + +**VERDETTO: `IL LEAD REGGE E VALE [+1,55%, +1,66%]/ANNO NETTO, MA LA SEPARAZIONE DELLE GAMBE — LA +RAGIONE PER CUI IL FILONE E' STATO APERTO — NON AGGIUNGE NULLA`**: T1 sbloccato e' un declassamento, +il netting perso risparmia commissioni invece di costarle, il margine non era il vincolo. **Il valore +e' il funding e basta**, e in cambio si prendono due costi nuovi mai contati (comparti fiscali +separati, gamba TP01 senza disaster-SL) piu' un cambio al percorso di sizing del live. +📌 **Il prossimo passo non e' un backtest: e' la domanda al commercialista e un acquisto di prova da +$7,73.** **Libro, pesi, cron, config INVARIATI.** From ca1e7660692de4f989739253f7559edc7805e37c Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 01:24:20 +0000 Subject: [PATCH 072/114] =?UTF-8?q?CLAUDE.md:=20ondata=2023/08=20=E2=80=94?= =?UTF-8?q?=200=20candidati,=20la=20scorta=20di=20dati=20esaurita,=20il=20?= =?UTF-8?q?timing=20dei=20bonifici=20refutato=20e=20il=20lead=20spot=20rid?= =?UTF-8?q?otto=20al=20solo=20funding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 546e563..4d4773e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1241,6 +1241,121 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis powers*, conti dormienti) non lo sorveglia nessuno; `MAINT_GRACE_HOURS`=2 **presume** gli annunci invece di leggerli (due `locked=true` in 4 giorni: 18/08 ~4h, 21/08 ~1h); la terza referenza non e' validabile sulla storia. +- 🌊 **QUARTA ONDATA 2026-08-23 (5 filoni, `research/wave-0822`) — 0 candidati, 1 lead + RIDEFINITO, 1 premessa MIA falsificata, e la scorta di DATI dichiarata esaurita.** Registro + `docs/research/RESULTS-0822.md` §41-45. **Libro, pesi, cron, config INVARIATI.** + 🚨 **(1) §41 TP01-SIZE — in questa famiglia la selezione in-sample e' ANTI-correlata col + futuro.** *Esiste una size che risponde alla CONVINZIONE, a pari vol realizzata?* 60 celle + dichiarate prima, 24 ancore. **`Spearman(ShIS, ShHOLD) = −0,527` (p<0,001)**: scegliere in-sample + e' **peggio di una moneta**, ed e' meccanismo, non rumore — `Spearman(rho, ShHOLD) = +1,000` a + tutti e sei i q (in hold-out l'ottimo e' al bordo) mentre in-sample la curva e' **a gobba** con + ottimo interno. **La convinzione e' informativa in-sample e ANTI-informativa in hold-out** (BTC + hold-out: convinzione debole Sh **+2,20** contro piena **−0,06**) → l'unica cella con guadagno + hold-out robusto e' esattamente quella selezionabile **solo guardando l'hold-out**. **Non esiste + una cella proponibile.** Il meglio della griglia vale **+0,30pp** di CAGR iso-vol e il maxDD di + libro peggiora a **24 ancore su 24** — cioe' proprio cio' per cui TP01 esiste. + 🚨 **CORREZIONE A UNA MIA PREMESSA, verificata al sorgente: il bucket di convinzione 2/3 NON + ESISTE.** `tsmom_blend` media **tre `np.sign(...)`** → direzione in **{−1,−1/3,+1/3,+1}**, dopo il + clip long-flat in **{0, 1/3, 1}** (BTC 44,2/24,7/31,1%). Trovato da **due agenti indipendenti**. + **Conseguenza:** potenza `c^p`, floor affine e soglia sono **la stessa cella riparametrizzata** — + la famiglia della convinzione ha **UN** grado di liberta', non tre. + 📌 **Null del RE-LEVERING, forma piu' pura mai misurata (6ª occorrenza, prima col segno + rovesciato):** togliere il vol-targeting fa **CAGR 16,32% → 46,75% (+186%)** e a iso-vol + **−2,40pp**. **Era tutta leva.** + ⚠️ **Fatto sull'harness:** `altlib.tp01_baseline_daily()` **NON e' bit-exact col sleeve** — 1 ulp + su **68,5%** dei giorni, perche' `_to_daily` fa `(1+r).resample("1D").prod()-1`, no-op + **algebrico** ma non in virgola mobile. *"Bit-exact vs altlib" non e' un controllo ottenibile.* + ❌ **(2) §42 TP01-LS — la short del trend a 1d SCARTATA, e piu' nettamente dell'attesa.** + Meccanismo congelato, un solo grado di liberta' (floor da 0 a −1); ✅ replica bit-exact a 4 vie + (**floor=−1 == `CANONICAL(long_only=False)`**, `max|diff| = 0.0`). Esito: **−1,91%/anno di drift + in 0/24 ancore**, **gate iso-vol FAIL 3/3** (non e' leva travestita: alza la vol **e** abbassa il + ritorno), **maxDD +8,80pp in 24/24**, NEUTRAL con alpha −2,60%/anno, e **a fee ZERO** 1,338 contro + 0,904 → *non e' morte-per-fee, l'edge lordo non c'e'*. **L'attesa a priori ("sara' il 2022") e' + confermata e superata: 2/8 anni positivi, e SENZA il 2022 il divario RADDOPPIA** (dShFULL −0,406, + iso-vol −7,44%) — *il 2022 non e' l'anno che regge la tesi, e' l'unico che la tiene a galla a + meta'*. **Non selezionabile**: `select_cell_insample` sceglie il canonico. + ⚠️ L'unica cosa a favore (**dShHOLD +0,292 in 23/24**) **all'ancora canonica vale −0,008** = caso + **speculare** della lezione del 26/07 (li' l'ancora canonica inventava un danno, qui ne nasconde + un vantaggio). 📌 **Trovato per strada: `src/live/book.py` contiene `max(tp_frac, 0.0)` — il + long-flat e' cablato anche nell'ESECUTORE**, non solo in `CANONICAL`. + 🚨 **(3) §43 DATA-UNUSED — LA SCORTA DI DATI DEL PROGETTO E' ESAURITA.** Inventario su **647 + file `.py`** (`Old/` escluso): **ogni dataset con storia vera e' gia' stato analizzato almeno una + volta**; cio' che resta non letto e' **telemetria a finestra corta (30-113 giorni)**, con una sola + eccezione — `external/coinmetrics/` (**26 colonne su 31 mai lette**, storia dal 2018). + Misurato prima di usarlo che il dato **fosse** nuovo: il flusso **netto** e' `corr +0,9993` con la + derivata dello stock gia' ucciso il 24/07, il **lordo** e' ortogonale (`+0,035`) perche' **il 94% + del flusso si cancella**. E perde: **massimo atteso dal PURO RUMORE su 36 trial = Sharpe 1,009, il + candidato fa 0,951 — sotto**; DSR 0,437; multi-cut negativo 7/7 anni. + 📌 **METODO RIUSABILE: mettere nella griglia una variabile che si SA gia' morta** trasforma + *"il candidato perde"* in *"il dato non contiene nient'altro di selezionabile"* — qui la selezione + in-sample e' **tornata sul controllo negativo**. E' l'unica forma in cui un inventario puo' + concludere qualcosa di positivo su un'assenza. + ⚠️ **Conferma diretta del §10** catturata dall'agente su se stesso: il pool *"N=12"* del + deflated-Sharpe erano **le 12 celle MIGLIORI** e faceva **PASSARE** il candidato a **0,975** + invece di 0,678. + ❌ **(4) §44 DEPOSIT-TIMING — il timing dei bonifici non paga, 0/12 celle in OGNI lente** + (€500/m, €250/m, IID, drift dimezzato). ✅ Replica 4/4 prima di ogni delta, fra cui P0 vs + `PN.accumula` a **`max|dif| = 0.0`** su capitale **e** giorno d'arrivo. + 📌 **Il segnale FA quello che promette e perde lo stesso:** dip 20% compra al **0,494** del + livello medio del piatto (**51% piu' in basso**) e arriva al capitale-rendita nel **6,3%** dei casi + contro l'**85,4%** del piatto — *comprare meglio e comprare tardi sono la stessa mossa*. E sotto + soglia bassa **si ribalta**: dip 5% compra a **1,009**, cioe' **piu' in alto** (su una serie che + sale, una condizione poco profonda non compra il calo, **ritarda dentro la salita**). + 📌 **L'asimmetria e' la misura: un rialzo del 20% dal minimo arriva in 16 giorni, un ribasso + del 20% dal massimo in 3.682.** *Il costo non e' scommettere sul lato sbagliato, e' mettere una + **condizione rara** davanti a un bonifico — in qualunque direzione.* + Rischio di venue **separato** dal timing: tenere i soldi fuori vale +2,4pp a p=1% contro −15,7pp + di penalita' di timing, e **`P(libro azzerato)` e' IDENTICA per tutte le politiche** (21,6%) — *il + conto salta comunque, cambia quanto c'e' sopra*; la protezione si compra con un **secondo conto**. + ⚠️ Due errori dell'agente catturati da un controllo: un diagnostico che diceva *"il buy-the-dip + contiene previsione"* (+11,5%) e che **sotto IID vale +14,9%**, cioe' **piu' grande** → meccanico; + e lo specchio anti-dip, che **si legge sulla colonna dell'ATTESA, non su quella del segno** (P4 e' + piatto perche' la sua condizione e' quasi sempre gia' vera). + **Ordine di grandezza: la migliore politica di timing vale −0,1pp, dentro il rumore MC di 2,0pp; + €250 → €500 al mese vale +71pp (14% → 85%).** + ⚠️ **(5) §45 SPOT-NETTING — il lead dello spot REGGE, ma non per la ragione per cui il filone e' + stato aperto.** Il margine **non e' il vincolo**: BTC/ETH sono `in_cross_collateral_pool` (11 + valute su 51), lo spot ha `max_leverage: 10` (⇒ vive nel conto marginato) e nel caso peggiore la + **copertura e' 50,0x, invariante al capitale** — anche valutando lo spot **zero** come collaterale. + **L'haircut non e' leggibile e non e' binding**: *unico caso in cui "non lo so" e "non conta" + coincidono, e va detto in quest'ordine.* + 🚨 **A bloccarlo e' IL CODICE, non Deribit:** `src/live/shadow.py::_equity` legge **solo** + `account_summary("USDC")` → con $477 in spot il libro **leggerebbe $159 invece di $636 (25%)** e il + riconciliatore **ricomprerebbe** la gamba. *Non e' un cambio di strumento, e' un cambio del + percorso di SIZING del live.* + 🚨 **La separazione delle gambe non vale nulla, su tutti e tre gli assi:** (i) **T1 sbloccato e' + un DECLASSAMENTO** — `onbook_tp − hourly` **riproduce** il pubblicato (+0,051/+0,057) ma contro il + path che il live **gia' percorre** fa **−0,048 FULL (6/23)** *(conferma indipendente della + decisione del 26/07, per un'altra strada)*; (ii) il **netting perso RISPARMIA** commissioni + (**$4,34 → $2,97/anno, −0,216%**) — la stima "1-2%" di §39 era **alta di un ordine di grandezza** — + e le ore a **segni opposti** sono lo **0,61%** della storia; (iii) meno ordini **non e' meglio**: + il tracking peggiora del **+12%** (5ª occorrenza della lezione *"banda in valuta assoluta"*). + **Il valore e' il FUNDING e basta: lordo +1,58%/anno → netto [+1,55%, +1,66%]**, [+1,33%, +1,44%] + se lo spot passasse a 3,50 bps, **+0,50% nello scenario fiscale sfavorevole**. Il costo vero + d'esecuzione e' lo **spread** (perp a **un tick**, spot 1,5-3,1 bps) → **[−0,034%, +0,077%]/anno**, + ampiezza 7% del lead: **l'esecuzione non decide questo lead**. + ❌ **Ipotesi dell'agente nata e refutata nel filone:** l'`apr 3,40` di USDC in `get_currencies` + costerebbe fino a **2,55%/anno** — ma su **6 run col conto flat (il piu' lungo 257 giri orari)** + l'equity e' **invariata al centesimo** ⇒ **< 0,029%/anno**. *L'obiezione e' morta e il lead non + deve pagarla.* + 🚨 **Due costi NUOVI mai contati, che la separazione introduce:** (a) se i derivati sono + `c-quater` i **comparti fiscali si SEPARANO** → le minusvalenze del perp (SKH01) smetterebbero di + compensare le plusvalenze spot (TP01); (b) il disaster-SL oggi protegge la posizione **netta**: + separando resterebbe **solo sul perp**, e in un blackout **TP01 resterebbe long al 75% del conto + senza stop**. (Di contro, lo spot **non si liquida**: esce dal perimetro il 75% del libro.) + ⚠️ E `fee_watch` **non vedrebbe** una gamba spot (deriva `INSTRUMENTS` da `BOOK_INSTRUMENT`, il + difetto corretto il 21/08); `convenzione('BTC_USDC')` ritorna **`'ignota'`** → servirebbe una + **terza famiglia 'spot'**. *La ragione per sorvegliarlo non e' che la fee lo ucciderebbe (perde il + 14% del guadagno e sopravvive), e' che oggi nessuno se ne accorgerebbe.* + 📌 **IL PROSSIMO PASSO NON E' UN BACKTEST: e' la domanda al commercialista** (gia' aperta dal + 07/08) **e un acquisto di prova da $7,73** — le tre cose non determinabili senza conto reale + (haircut, se `equity` USDC includa i saldi BTC/ETH, se `get_positions` ritorni lo spot) si + risolvono cosi'. **E' un ordine ⇒ decisione dell'operatore.** + 📌 **(6) LA RIGA DI SINTESI.** Quattro filoni su cinque SCARTATI, il quinto ridimensionato a + *"vale il funding e basta"*. Ma la misura piu' grande dell'ondata resta quella di §44: **la + migliore politica di timing vale −0,1pp mentre passare da €250 a €500 al mese vale +71pp**. + **Con questa sono 45 filoni in due giorni e 0 candidati promossi: la ricerca ha smesso di essere + il vincolo binding il 26/07, e sette ondate lo hanno CONFERMATO invece che ribaltarlo.** - 🚨 **IL TRASPORTO DEGLI ALLARMI E' UN PUNTO SINGOLO DI GUASTO — misurato 2026-08-23, NON riparato (produzione, decisione dell'operatore).** Registro `docs/research/RESULTS-0822.md` (sezione NOTIFIER). Il progetto ha tarato il **rilevatore** (`venue_watch`: 1 falso allarme in 8 From 5f6aa901806686954af73b30f93bb7a000bababb Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 01:32:11 +0000 Subject: [PATCH 073/114] =?UTF-8?q?registro:=20ondata=208,=20primo=20lotto?= =?UTF-8?q?=20=E2=80=94=20sei=20filoni=20(assicurazione=20di=20coda,=20ens?= =?UTF-8?q?emble=20d'ancora,=20vol-of-vol,=20ensemble=20di=20trend,=20terz?= =?UTF-8?q?o=20sleeve,=20cosa=20raccogliere)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 3a0b168..fd7dd8c 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -711,6 +711,12 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 43 | DATA-UNUSED | quali dati il progetto possiede e nessuna strategia legge — e uno di essi sostiene un'ipotesi? | **SCARTATO** — **la scorta di dati e' esaurita**: un solo dato mai letto ha storia vera (flussi exchange CoinMetrics 2018-2026) e misurato sta **SOTTO il massimo atteso dal puro rumore** | | 44 | DEPOSIT-TIMING | un calendario di versamento CONDIZIONALE batte il piatto, a pari soldi? | **SCARTATO 0/12 in ogni lente** — il buy-the-dip compra davvero il **51% piu' in basso** e arriva al capitale-rendita nel **6,3%** dei casi contro l'**85,4%** del piatto | | 45 | SPOT-NETTING | separare TP01 e SKH01 su due strumenti: quanto vale, e il margine lo uccide? | **LEAD REGGE, SEPARAZIONE SENZA VALORE** — il margine non e' il vincolo (copertura **50x**, invariante al capitale), ma **T1 sbloccato e' un DECLASSAMENTO** (−0,048 contro il path che gira) e il netting perso **risparmia** commissioni: il lead vale **il funding e basta**, [+1,55%, +1,66%]/anno; a bloccarlo e' **il codice di sizing**, non Deribit | +| 46 | TAIL-HEDGE | comprare put deep-OTM come assicurazione statica compra DD a prezzo migliore del de-levering? | _in corso_ | +| 47 | ANCHOR-ENSEMBLE | eseguire l'ensemble delle 24 ancore invece di sceglierne una e correggere il numero | _in corso_ | +| 48 | VOLVOL | la volatilita' del DVOL (secondo momento, mai usato) contiene cio' che livello e spread non contengono? | _in corso_ | +| 49 | TP01-TWIN | il 75% del libro dipende da UNA definizione di trend: un ensemble di meccanismi protegge meglio? | _in corso_ | +| 50 | BOOK-3RD | qual e' il miglior TERZO sleeve realmente eseguibile sul libro live a $635 — e a quale capitale cambia? | _in corso_ | +| 51 | NEWDATA-SCOUT | se la scorta e' esaurita: cosa iniziare a raccogliere OGGI, che fra 12-24 mesi apra una famiglia? | _in corso_ | | — | **NOTIFIER** (audit di fatto) | 🚨 **PUNTO SINGOLO DI GUASTO nella rete di sicurezza** | il `send()` di Telegram e' **un solo tentativo da 10s, senza retry, con l'esito MAI registrato** — e lo stato `alerted=True` viene scritto **prima** dell'invio: un 🚨 di `venue_watch` perso e' perso **per l'intero episodio**. Tasso di fallimento **misurato 6,9%** (2/29, IC95 [1,9%, 22,0%]) | | 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 | From fd8d5441af0f0cfaf99a2166151fbbc65b0dfc6c Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 01:34:15 +0000 Subject: [PATCH 074/114] =?UTF-8?q?registro:=20ondata=208,=20secondo=20lot?= =?UTF-8?q?to=20=E2=80=94=20due=20scettici=20adversariali=20sui=20due=20le?= =?UTF-8?q?ad=20che=20valgono=20soldi=20veri?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index fd7dd8c..cff1dea 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -717,6 +717,8 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 49 | TP01-TWIN | il 75% del libro dipende da UNA definizione di trend: un ensemble di meccanismi protegge meglio? | _in corso_ | | 50 | BOOK-3RD | qual e' il miglior TERZO sleeve realmente eseguibile sul libro live a $635 — e a quale capitale cambia? | _in corso_ | | 51 | NEWDATA-SCOUT | se la scorta e' esaurita: cosa iniziare a raccogliere OGGI, che fra 12-24 mesi apra una famiglia? | _in corso_ | +| 52 | SKEPTIC-SPOT | il lead dello spot (+1,55-1,66%/anno) sopravvive a un attacco deliberato? | _in corso_ | +| 53 | SKEPTIC-LEVA | il gradino di leva 1,25x sopravvive a un attacco deliberato, col funding dentro? | _in corso_ | | — | **NOTIFIER** (audit di fatto) | 🚨 **PUNTO SINGOLO DI GUASTO nella rete di sicurezza** | il `send()` di Telegram e' **un solo tentativo da 10s, senza retry, con l'esito MAI registrato** — e lo stato `alerted=True` viene scritto **prima** dell'invio: un 🚨 di `venue_watch` perso e' perso **per l'intero episodio**. Tasso di fallimento **misurato 6,9%** (2/29, IC95 [1,9%, 22,0%]) | | 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 | From e2aee5730baf1597898d08f3c50d75a876fb5128 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 01:46:16 +0000 Subject: [PATCH 075/114] =?UTF-8?q?research(newdata):=20raccogliere=202=20?= =?UTF-8?q?fonti=20su=2010=20=E2=80=94=20il=20criterio=20di=20ricostruibil?= =?UTF-8?q?ita'=20ne=20uccide=204=20da=20solo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §51 NEWDATA-SCOUT. La scorta di dati e' esaurita (§43), quindi la domanda cambia: non "cosa abbiamo e non guardiamo" ma "cosa NON abbiamo che vada iniziato OGGI". Ogni riga della colonna ricostruibilita' e' PROVATA con una GET pubblica + controllo positivo, non dedotta. VERDETTO: raccogliere la catena opzioni USDC-lineare (+117 chiamate/giro = +18%, non +90%); il book depth L2 come CAMPAGNA A TERMINE, non come collettore. Tutto il resto: no. - tape/liquidazioni Deribit: muro di ritenzione misurato fra -24h e -26h (non ricostruibile), ma il MDE lo manda al 2032 come segnale -> non raccogliere. E il campo `liquidation` non si e' fatto vedere in 1000 trade: non si accende una raccolta su un campo che non si sa se scatta. - §10 proponeva un collettore di OI perpetual "perche' oggi ripartirebbe da zero": MISURATO, non riparte da zero — Bybit serve >=800 giorni di OI perpetual gratis (timestamp verificati dentro la finestra chiesta), e la famiglia funding e' chiusa su 4 lati -> testare prima. - Binance OI/taker/long-short: muro vero a 30 giorni (l'API RIFIUTA, non risponde vuoto), ma MDE 6 anni + venue USDT -> no. - funding, DVOL, macro: ricostruibili E famiglie chiuse -> doppia eliminazione. Correzione a un numero pubblicato: la catena USDC con gli STESSI filtri del collettore vivo (<=95g, OI>=100) sono 113 strumenti, non ~587; l'origine del 587 resta ignota e lo dico. Il +117 e' un numero di OGGI e cresce con la liquidita' USDC: va ri-misurato prima di accendere. Bug catturato in sessione: urlopen solleva su HTTP 400, quindi il RIFIUTO di Binance veniva ingoiato come guasto di rete e il muro si ribaltava in silenzio in "RICOSTRUIBILE" — stessa conflazione error/no_quote gia' codificata in collect_chain. Guardia sulle finestre di cron verificata in entrambi i versi; MDE replica la convenzione §43 (1,40 a -> 1,66). Sola lettura sul disco, nessun ordine, nessuna chiave. Libro, pesi, cron, config INVARIATI. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf --- scripts/research/r0823_newdata_scout.py | 599 ++++++++++++++++++++++++ 1 file changed, 599 insertions(+) create mode 100644 scripts/research/r0823_newdata_scout.py diff --git a/scripts/research/r0823_newdata_scout.py b/scripts/research/r0823_newdata_scout.py new file mode 100644 index 0000000..ee19304 --- /dev/null +++ b/scripts/research/r0823_newdata_scout.py @@ -0,0 +1,599 @@ +"""NEWDATA-SCOUT (filone §51) — cosa iniziare a raccogliere OGGI, se la scorta e' esaurita. + +CONTESTO. Il filone §43 (`r0823_data_unused.py`) ha censito 647 file e concluso che la scorta di +dati del progetto e' ESAURITA: ogni dataset con storia vera e' gia' stato analizzato almeno una +volta, e cio' che resta non letto e' telemetria a 30-113 giorni. Quindi la domanda utile cambia: +non piu' "cosa abbiamo e non guardiamo" ma "cosa NON abbiamo, che varrebbe la pena iniziare a +raccogliere OGGI perche' fra 12-24 mesi apra una famiglia che oggi non e' apribile?". + +Il progetto ha gia' pagato due volte la lezione: la catena opzioni non e' ricostruibile a +posteriori (Deribit non serve book storici; l'ora non raccolta e' persa per sempre) -> esiste +`scripts/live/collect_chain.py`; e le serie `paper_*` sono forward-only irrecuperabili. + +IL CRITERIO CHE FA IL LAVORO E' IL PRIMO, E VA **PROVATO**, NON DEDOTTO. + 1. RICOSTRUIBILITA' — se un dato si potra' scaricare storico quando servira', raccoglierlo oggi + e' PURO COSTO. Qui ogni riga della colonna e' il risultato di una GET pubblica di sondaggio, + con **controllo positivo** (una finestra in cui il dato DEVE esserci) prima di dichiarare un + muro: uno zero puo' voler dire "non c'e'" oppure "sto chiamando male", e le due cose si + distinguono solo provando che la stessa chiamata funziona altrove. + 2. FAMIGLIA — una fonte che alimenta solo una famiglia gia' chiusa con misure non vale niente. + 3. COSTO in chiamate, contro il budget IP esistente (il rate limit Deribit e' PER-IP e questo + progetto l'ha gia' saturato una volta, guasto di 8 ore il 29/07). + 4. RISCHIO OPERATIVO — ogni raccolta e' un modo nuovo di rompersi in silenzio. + 5. TEMPO PERCHE' SIA UTILE, dal MDE: SE(Sharpe) ~ 1/sqrt(anni), MDE ~ 1,96/sqrt(anni) + (stessa convenzione di §43: 1,40 anni -> MDE 1,66). Se la risposta e' "6 anni", non e' una + decisione di oggi. + +NON PROPONE DI IMPLEMENTARE NULLA: produce materiale per una decisione dell'operatore. + + uv run python scripts/research/r0823_newdata_scout.py + uv run python scripts/research/r0823_newdata_scout.py --no-net # solo tabella + MDE +""" +from __future__ import annotations + +import argparse +import json +import sys +import time +import urllib.error +import urllib.request +from dataclasses import dataclass, field +from datetime import UTC, datetime, timedelta +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] + +# --- disciplina di rete ------------------------------------------------------------------- +# Il rate limit Deribit e' PER-IP e questa VPS lo condivide. I minuti proibiti non sono cortesia: +# :05-:10 cron_book.sh (feed 5m di SKH01 -> esecuzione del libro con soldi veri) +# :24-:30 cron_chain.sh (collettore catena, 653 chiamate a 4/s per ~163 s) +FORBIDDEN_MINUTES = ((5, 10), (24, 30)) +MAX_RPS = 2.0 +TIMEOUT = 20 + + +class Net: + """GET pubbliche e tokenless, <=2/s, fuori dalle finestre dei cron di produzione.""" + + def __init__(self, enabled: bool = True) -> None: + self.enabled = enabled + self._next = 0.0 + self.calls = 0 + self.blocked = 0 + + @staticmethod + def in_forbidden_window() -> bool: + m = datetime.now(UTC).minute + return any(a <= m < b for a, b in FORBIDDEN_MINUTES) + + def get(self, url: str) -> dict | list | None: + if not self.enabled: + return None + while self.in_forbidden_window(): + self.blocked += 1 + time.sleep(20) + now = time.monotonic() + if now < self._next: + time.sleep(self._next - now) + self._next = max(now, self._next) + 1.0 / MAX_RPS + self.calls += 1 + req = urllib.request.Request(url, headers={"User-Agent": "PythagorasGoal-scout/1.0"}) + try: + with urllib.request.urlopen(req, timeout=TIMEOUT) as r: + return json.loads(r.read()) + except urllib.error.HTTPError as e: + # UN RIFIUTO NON E' UN GUASTO. Binance risponde 400 + corpo JSON quando startTime + # e' oltre la ritenzione: se lo si ingoia come errore di rete, un MURO diventa + # indistinguibile da "nessun muro trovato" e la colonna ricostruibilita' si + # ribalta in silenzio. Stessa lezione di `quote_status` (error vs no_quote). + try: + body = json.loads(e.read()) + if isinstance(body, dict): + body["_http_status"] = e.code + return body + except (ValueError, OSError): + return {"_http_status": e.code, "_unparsed": True} + except (urllib.error.URLError, TimeoutError, ValueError, OSError): + return None + + +DERIBIT = "https://www.deribit.com/api/v2/public" +MS = 1000 + + +def ms_ago(**kw) -> int: + return int((datetime.now(UTC) - timedelta(**kw)).timestamp() * MS) + + +# --- esito di una prova di ricostruibilita' ----------------------------------------------- +@dataclass +class Prova: + """Una prova di ricostruibilita'. `positivo` e' il controllo che rende leggibile lo zero.""" + fonte: str + verdetto: str = "NON PROVATO" + dettaglio: str = "" + positivo: str = "" + misurato: bool = False + extra_tot: int = 0 + righe: list[str] = field(default_factory=list) + + +# ------------------------------------------------------------------------------------------ +# PROVE +# ------------------------------------------------------------------------------------------ +def prova_tape(net: Net) -> Prova: + """Trade tape Deribit (direzione aggressore + flag liquidation). + + `get_last_trades_by_instrument_and_time` accetta qualunque finestra passata SENZA errore: + fuori ritenzione risponde `trades: []`. Uno zero non e' quindi una risposta -> serve il + controllo positivo (una finestra recente, dove i trade DEVONO esserci). + """ + p = Prova("tape / flusso aggressore Deribit") + inst = "BTC-PERPETUAL" + ore = (1, 6, 12, 24, 26, 30, 36, 48, 168) + trovato: dict[int, int] = {} + for h in ore: + st = ms_ago(hours=h) + d = net.get(f"{DERIBIT}/get_last_trades_by_instrument_and_time" + f"?instrument_name={inst}&start_timestamp={st}&end_timestamp={st + 300 * MS}" + f"&count=100&sorting=asc") + if d is None or "result" not in d: + return p + trovato[h] = len(d["result"]["trades"]) + p.misurato = True + vivi = [h for h, n in trovato.items() if n > 0] + morti = [h for h, n in trovato.items() if n == 0] + if not vivi: + p.verdetto = "PROVA NON VALIDA" + p.dettaglio = "nessuna finestra restituisce trade: la chiamata e' rotta, non il dato assente" + return p + ultimo_vivo, primo_morto = max(vivi), (min(morti) if morti else None) + p.positivo = f"a -{ultimo_vivo}h la stessa chiamata rende {trovato[ultimo_vivo]} trade" + if primo_morto is None: + p.verdetto = "RICOSTRUIBILE" + p.dettaglio = f"nessun muro entro -{max(ore)}h" + else: + p.verdetto = "NON RICOSTRUIBILE" + p.dettaglio = (f"muro di ritenzione fra -{ultimo_vivo}h e -{primo_morto}h " + f"(~1 giorno): oltre, il tape e' perso per sempre") + p.righe = [f" -{h:>3}h -> {n:>3} trade" for h, n in trovato.items()] + return p + + +def prova_tape_rate(net: Net) -> Prova: + """Quanto costa raccogliere il tape: trade/ora sui 2 strumenti che il libro trada.""" + p = Prova("tape — costo di raccolta") + st = ms_ago(minutes=35) + tot = 0 + for inst in ("BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL"): + d = net.get(f"{DERIBIT}/get_last_trades_by_instrument_and_time" + f"?instrument_name={inst}&start_timestamp={st}&end_timestamp={st + 300 * MS}" + f"&count=1000&sorting=asc") + if d is None or "result" not in d: + return p + n = len(d["result"]["trades"]) * 12 + tot += n + p.righe.append(f" {inst:<22} ~{n:>5} trade/ora") + p.misurato = True + p.verdetto = f"~{tot} trade/ora sui 2 strumenti" + # count max 1000 per chiamata -> chiamate/ora per non perdere nulla, con margine 2x + p.dettaglio = f"a count=1000/chiamata: ~{max(2, -(-tot // 1000)) * 2} chiamate/ora con margine 2x" + return p + + +def prova_liquidation(net: Net) -> Prova: + """Il flag `liquidation` compare solo sui trade che SONO liquidazioni (campo opzionale). + + Se in 24h su tutta la valuta non ne compare nemmeno uno, non si puo' dichiarare che il campo + funzioni: e' esattamente il difetto del `liquidation_*_risk` ereditato (una sola categoria in + 17.229 righe). Una fonte il cui campo chiave non si riesce a far scattare non si accende. + """ + p = Prova("liquidazioni Deribit (flag nel tape)") + st = ms_ago(hours=23) + d = net.get(f"{DERIBIT}/get_last_trades_by_currency_and_time" + f"?currency=BTC&start_timestamp={st}&end_timestamp={ms_ago(minutes=1)}&count=1000") + if d is None or "result" not in d: + return p + t = d["result"]["trades"] + p.misurato = True + liq = [x for x in t if "liquidation" in x] + p.positivo = f"{len(t)} trade letti (has_more={d['result'].get('has_more')})" + p.verdetto = "NON DIMOSTRATO" if not liq else "CAMPO VIVO" + p.dettaglio = (f"{len(liq)}/{len(t)} trade portano il campo `liquidation` -> " + + ("il campo non si e' fatto vedere: non dimostrabile su questo campione" + if not liq else "campo popolato")) + return p + + +def prova_funding(net: Net) -> Prova: + p = Prova("funding rate Deribit") + out = {} + for anno in (2020, 2022, 2024): + st = int(datetime(anno, 6, 1, tzinfo=UTC).timestamp() * MS) + d = net.get(f"{DERIBIT}/get_funding_rate_history" + f"?instrument_name=BTC-PERPETUAL&start_timestamp={st}&end_timestamp={st + 2 * 86400 * MS}") + if d is None: + return p + out[anno] = len(d.get("result", [])) + p.misurato = True + p.verdetto = "RICOSTRUIBILE" if all(v > 0 for v in out.values()) else "PARZIALE" + p.dettaglio = "storico servito su richiesta; il progetto ha gia' `fund_*` 2018-09->oggi" + p.righe = [f" {a}-06 -> {n} punti" for a, n in out.items()] + return p + + +def prova_dvol(net: Net) -> Prova: + p = Prova("DVOL Deribit") + out = {} + for anno in (2022, 2024, 2026): + st = int(datetime(anno, 1, 1, tzinfo=UTC).timestamp() * MS) + d = net.get(f"{DERIBIT}/get_volatility_index_data" + f"?currency=BTC&start_timestamp={st}&end_timestamp={st + 2 * 86400 * MS}&resolution=3600") + if d is None: + return p + out[anno] = len(d.get("result", {}).get("data", [])) + p.misurato = True + p.verdetto = "RICOSTRUIBILE" if all(v > 0 for v in out.values()) else "PARZIALE" + p.dettaglio = "gia' posseduto (`dvol_*` 2021-03->oggi)" + p.righe = [f" {a}-01 -> {n} punti" for a, n in out.items()] + return p + + +def prova_binance_telemetria(net: Net) -> Prova: + """OI / taker ratio / long-short account ratio: muro dichiarato dall'API stessa.""" + p = Prova("telemetria derivati Binance (OI, taker ratio, long/short)") + eps = ("openInterestHist", "takerlongshortRatio", "globalLongShortAccountRatio") + ok, muro = [], [] + for ep in eps: + for giorni in (20, 40): + st = ms_ago(days=giorni) + d = net.get(f"https://fapi.binance.com/futures/data/{ep}" + f"?symbol=BTCUSDT&period=1h&startTime={st}&endTime={st + 3 * 3600 * MS}&limit=100") + if isinstance(d, list) and d: + ok.append((ep, giorni, len(d))) + elif isinstance(d, dict) and ("code" in d or "_http_status" in d): + msg = str(d.get("msg") or f"HTTP {d.get('_http_status')}")[:44] + muro.append((ep, giorni, msg)) + if not ok and not muro: + return p + p.misurato = True + p.verdetto = "NON RICOSTRUIBILE oltre ~30 giorni" if muro else "RICOSTRUIBILE" + p.positivo = f"a -20g rispondono {len(ok)}/{len(eps)} endpoint con dati" + p.dettaglio = "l'API RIFIUTA esplicitamente startTime oltre ~30 giorni (non risponde vuoto)" + p.righe = [f" {e:<28} -{g}g -> {n} punti" for e, g, n in ok] + p.righe += [f" {e:<28} -{g}g -> RIFIUTO: {m}" for e, g, m in muro] + return p + + +def prova_bybit_oi(net: Net) -> Prova: + """OI dei perpetual su Bybit: e' il sostituto storico del collettore proposto da §10. + + Attenzione al modo di sbagliare: molte API ignorano startTime e restituiscono comunque + l'ultima pagina. Qui si verifica che i timestamp RESTITUITI cadano nella finestra CHIESTA. + """ + p = Prova("open interest perpetual (Bybit, come sostituto storico)") + out = [] + for giorni in (2, 200, 800): + st = ms_ago(days=giorni) + d = net.get(f"https://api.bybit.com/v5/market/open-interest" + f"?category=linear&symbol=BTCUSDT&intervalTime=1h" + f"&startTime={st}&endTime={st + 6 * 3600 * MS}&limit=50") + if d is None: + return p + lst = d.get("result", {}).get("list", []) + if not lst: + out.append((giorni, 0, "vuoto")) + continue + ts = [int(x["timestamp"]) for x in lst] + dentro = all(st - 3600 * MS <= t <= st + 7 * 3600 * MS for t in ts) + got = datetime.fromtimestamp(min(ts) / MS, UTC).strftime("%Y-%m-%d %H:%M") + out.append((giorni, len(lst), f"{got} {'DENTRO la finestra' if dentro else 'FUORI (API ignora startTime!)'}")) + p.misurato = True + lontano = [o for o in out if o[0] == 800 and o[1] > 0 and "DENTRO" in o[2]] + p.verdetto = "RICOSTRUIBILE (>=800 giorni)" if lontano else "MURO" + p.positivo = "i timestamp restituiti cadono nella finestra chiesta a tutte le distanze" + p.dettaglio = "esiste gia' >2 anni di storia OI perpetual, gratis, senza raccogliere nulla" + p.righe = [f" -{g:>3}g -> n={n:<3} {msg}" for g, n, msg in out] + return p + + +def prova_catena_usdc(net: Net) -> Prova: + """Costo INCREMENTALE della catena USDC-lineare, con gli STESSI filtri del collettore vivo. + + CLAUDE.md riporta '+587 chiamate/giro (+90%)'. Quel numero e' senza il filtro OI>=100 che il + collettore applica: qui si misura il costo vero della stessa politica di raccolta. + """ + p = Prova("catena opzioni USDC-lineare (BTC+ETH)") + inst = net.get(f"{DERIBIT}/get_instruments?currency=USDC&kind=option&expired=false") + summ = net.get(f"{DERIBIT}/get_book_summary_by_currency?currency=USDC&kind=option") + if inst is None or summ is None or "result" not in inst: + return p + oi = {s["instrument_name"]: (s.get("open_interest") or 0.0) for s in summ["result"]} + hor = (datetime.now(UTC) + timedelta(days=95)).timestamp() * MS + p.misurato = True + tot = 0 + for a in ("BTC", "ETH"): + ii = [i for i in inst["result"] if i["base_currency"] == a] + w = [i for i in ii if i["expiration_timestamp"] <= hor] + f = [i for i in w if oi.get(i["instrument_name"], 0.0) >= 100.0] + tot += len(f) + p.righe.append(f" {a}: {len(ii):>4} strumenti | <=95g {len(w):>4} | OI>=100 -> {len(f):>4}") + p.verdetto = "NON RICOSTRUIBILE (snapshot, come la inverse)" + p.dettaglio = f"costo incrementale REALE: +{tot + 4} chiamate/giro" + p.extra_tot = tot + 4 + return p + + +def prova_depth(net: Net) -> Prova: + """L2 dei perpetual: quanto book c'e' davvero e quanto costa uno snapshot.""" + p = Prova("book depth L2 dei perpetual") + out = [] + for d_ in (10, 100, 10000): + d = net.get(f"{DERIBIT}/get_order_book?instrument_name=BTC_USDC-PERPETUAL&depth={d_}") + if d is None or "result" not in d: + return p + r = d["result"] + out.append((d_, len(r["bids"]), len(r["asks"]), len(json.dumps(r)))) + p.misurato = True + piena = out[-1] + p.verdetto = "NON RICOSTRUIBILE (nessun book storico servito)" + p.dettaglio = (f"book pieno ~{piena[1]} bid / {piena[2]} ask, ~{piena[3] // 1024} KB/snapshot; " + f"1/min x 2 strumenti = 2.880 chiamate/giorno") + p.righe = [f" depth={a:<6} bid={b:<4} ask={c:<4} ~{e} byte" for a, b, c, e in out] + return p + + +# ------------------------------------------------------------------------------------------ +# BUDGET IP ESISTENTE (letto dal disco, sola lettura) +# ------------------------------------------------------------------------------------------ +def budget_esistente() -> tuple[int, str]: + hb = PROJECT_ROOT / "data" / "chain_collect" / "runs.jsonl" + if not hb.exists(): + return 0, "battuta di cuore assente" + ultime = [json.loads(x) for x in hb.read_text().strip().splitlines()[-24:] if x.strip()] + ch = [r.get("chiamate", 0) for r in ultime if r.get("chiamate")] + rl = sum(r.get("rate_limited", 0) or 0 for r in ultime) + med = int(sorted(ch)[len(ch) // 2]) if ch else 0 + return med, f"{len(ultime)} giri letti, mediana {med} chiamate/giro, 429 totali: {rl}" + + +# ------------------------------------------------------------------------------------------ +# MDE — tempo perche' sia utile +# ------------------------------------------------------------------------------------------ +def anni_per_sharpe(s: float) -> float: + """Convenzione di §43: MDE ~ 1,96/sqrt(anni) -> anni = (1,96/S)^2.""" + return (1.96 / s) ** 2 + + +# ------------------------------------------------------------------------------------------ +# TABELLA DECIDIBILE +# ------------------------------------------------------------------------------------------ +@dataclass +class Candidato: + fonte: str + apre: str + ricostruibile: str + costo: str + rischio: str + tempo: str + verdetto: str + perche: str + + +def costruisci_tabella(pr: dict[str, Prova], giro: int) -> list[Candidato]: + usdc_extra = getattr(pr["usdc"], "extra_tot", 117) + pct = 100.0 * usdc_extra / giro if giro else float("nan") + return [ + Candidato( + "catena opzioni USDC-lineare", + "nessuna famiglia nuova: rende ONESTI i numeri di uno sleeve VIVO (VRP01, 12% del book)", + f"NO — snapshot ({pr['usdc'].verdetto})", + f"+{usdc_extra} chiamate/giro su {giro} = +{pct:.0f}% (numero di OGGI: cresce con la liquidita' USDC)", + "MEDIO — stesso collettore, stessa forma di guasto gia' sorvegliata", + "EPISODI, non calendario: serve UN regime IV-rank>0,30 (0/8 finora)", + "RACCOGLIERE", + "e' la famiglia che il conto USDC puo' davvero marginare; oggi f=0,73 e' misurato " + "sulla famiglia INVERSE, che quel conto non puo' tradare", + ), + Candidato( + "book depth L2 perpetual (campagna, non collettore)", + "non un segnale: la CURVA DI HAIRCUT contro la taglia, che oggi e' misurata su 18 fill", + f"NO — {pr['depth'].verdetto}", + "2.880 chiamate/giorno a 1/min x2; una campagna di 2 settimane basta per la mediana", + "ALTO se permanente (cron nuovo al minuto), BASSO se campagna a termine", + "GIORNI per la depth tipica; ANNI per la coda di stress", + "CAMPAGNA A TERMINE", + "la domanda binding ('quanto costa eseguire a $5k/$20k') si risponde in due settimane; " + "solo la coda di stress richiede l'archivio, ed e' l'unico pezzo non recuperabile", + ), + Candidato( + "trade tape / flusso aggressore", + "flusso d'ordine — MAI aperto, ma confinante con FLOW-SQUEEZE e funding (chiusi)", + f"NO — {pr['tape'].dettaglio}", + f"basso ({pr['tape_rate'].dettaglio})", + "MEDIO", + f"{anni_per_sharpe(0.8):.0f} anni per distinguere Sharpe 0,8 da zero", + "NON RACCOGLIERE", + "il MDE lo uccide come segnale (decidibile nel 2032); come misura d'esecuzione i " + "nostri fill + il feed 5m lo coprono gia'", + ), + Candidato( + "liquidazioni Deribit", + "coda/stress — ma il campo chiave non si e' fatto vedere", + f"NO (vive nel tape, stesso muro) — {pr['liq'].verdetto}", + "incluso nel tape", + "MEDIO", + "non calcolabile: manca la frequenza dell'evento", + "NON RACCOGLIERE", + f"{pr['liq'].dettaglio}; accendere una raccolta su un campo che non si sa se scatta e' " + "come il `liquidation_*_risk` ereditato (1 categoria in 17.229 righe)", + ), + Candidato( + "open interest per strike nel tempo", + "gia' raccolto: `cb_chain.open_interest`, ogni ora dal 2026-06", + "irrilevante — lo si sta gia' raccogliendo", + "zero", + "zero", + "gia' in corso", + "GIA' FATTO", + "e §6 (OI-PIN) ha gia' misurato che il max-pain non batte la media a 7 giorni, 0/24", + ), + Candidato( + "OI dei perpetual (il collettore proposto da §10)", + "affollamento/posizionamento — famiglia funding, CHIUSA su 4 lati", + f"SI — {pr['bybit'].verdetto} su un venue sostituto", + "1 GET/ora (quasi nulla)", + "BASSO", + "l'ipotesi e' testabile OGGI su >2 anni gia' esistenti", + "NON RACCOGLIERE — TESTARE PRIMA", + "§10 proponeva di accendere un collettore 'perche' oggi ripartirebbe da zero': " + "misurato, non riparte da zero. Bybit serve >=800 giorni di OI perpetual gratis", + ), + Candidato( + "telemetria derivati Binance (taker ratio, long/short)", + "posizionamento retail — confinante con la famiglia funding (chiusa)", + f"NO — {pr['binance'].verdetto}", + "~24 GET/giorno", + "BASSO", + f"{anni_per_sharpe(0.8):.0f} anni per Sharpe 0,8; {anni_per_sharpe(1.0):.0f} per 1,0", + "NON RACCOGLIERE", + "unica fonte con un muro vero E una famiglia non del tutto chiusa, ma il MDE la manda " + "al 2032 e il venue e' USDT (il progetto ha gia' pagato il proxy HL a 2,1x)", + ), + Candidato( + "funding / basis di altri venue", + "differenziale cross-venue — famiglia funding, CHIUSA su 4 lati", + f"SI — {pr['funding'].verdetto}", + "—", + "—", + "—", + "NON RACCOGLIERE", + "doppia eliminazione: ricostruibile E famiglia chiusa", + ), + Candidato( + "DVOL / vol index", + "gia' posseduto (2021-03->oggi), letto da 17 script", + f"SI — {pr['dvol'].verdetto}", + "—", + "—", + "—", + "NON RACCOGLIERE", + "ricostruibile a richiesta e gia' su disco", + ), + Candidato( + "macro / tassi a bassa frequenza", + "macro regime gate — CHIUSA (ridondante col trend, corr->TP01 0,989)", + "SI — serie storiche pubblicate e revisionate, per costruzione", + "—", + "—", + "—", + "NON RACCOGLIERE", + "doppia eliminazione; la ricostruibilita' qui e' DEDOTTA (sondaggio non conclusivo), " + "ma la famiglia chiusa decide da sola", + ), + ] + + +# ------------------------------------------------------------------------------------------ +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--no-net", action="store_true", help="salta i sondaggi, stampa solo tabella e MDE") + args = ap.parse_args() + + net = Net(enabled=not args.no_net) + t0 = time.time() + + print("=" * 100) + print("§51 NEWDATA-SCOUT — cosa iniziare a raccogliere OGGI (la scorta e' esaurita, §43)") + print(f" {datetime.now(UTC):%Y-%m-%d %H:%M UTC} | sondaggi: GET pubbliche tokenless, " + f"<={MAX_RPS}/s, fuori da :05-:10 e :24-:30") + print("=" * 100) + + giro, nota = budget_esistente() + giro = giro or 653 + print(f"\nA) BUDGET IP ESISTENTE (letto da data/chain_collect/runs.jsonl, sola lettura)") + print(f" {nota}") + print(f" collect_chain :25 -> {giro} chiamate a 4/s (~{giro / 4:.0f} s) | cron_book :07 | " + f"il rate limit Deribit e' PER-IP") + + print("\nB) PROVE DI RICOSTRUIBILITA' — misurate, con controllo positivo") + print(" (uno zero non e' una risposta finche' non si prova che la stessa chiamata funziona altrove)") + pr: dict[str, Prova] = {} + if net.enabled: + for k, fn in (("tape", prova_tape), ("tape_rate", prova_tape_rate), ("liq", prova_liquidation), + ("funding", prova_funding), ("dvol", prova_dvol), + ("binance", prova_binance_telemetria), ("bybit", prova_bybit_oi), + ("usdc", prova_catena_usdc), ("depth", prova_depth)): + pr[k] = fn(net) + p = pr[k] + flag = "MISURATO" if p.misurato else "NON MISURATO (rete)" + print(f"\n [{flag}] {p.fonte}") + print(f" -> {p.verdetto}") + if p.dettaglio: + print(f" {p.dettaglio}") + if p.positivo: + print(f" controllo positivo: {p.positivo}") + for r in p.righe: + print(r) + else: + for k in ("tape", "tape_rate", "liq", "funding", "dvol", "binance", "bybit", "usdc", "depth"): + pr[k] = Prova(k, verdetto="(--no-net)") + print(" saltate (--no-net)") + + print("\n" + "-" * 100) + print("C) TEMPO PERCHE' SIA UTILE — dal MDE, non a occhio") + print(" SE(Sharpe) ~ 1/sqrt(anni); MDE ~ 1,96/sqrt(anni) [convenzione §43: 1,40 a -> 1,66]") + print(f" {'Sharpe da distinguere da zero':<34}{'anni di raccolta':>18}{'decidibile dal':>18}") + oggi = datetime.now(UTC) + for s_ in (0.5, 0.8, 1.0, 1.3): + a = anni_per_sharpe(s_) + quando = (oggi + timedelta(days=365.25 * a)).strftime("%Y-%m") + print(f" {s_:<34.2f}{a:>18.1f}{quando:>18}") + print(" => una fonte che alimenta un SEGNALE giornaliero non e' una decisione di oggi:") + print(" il soffitto direzionale del progetto e' ~1,15-1,3 -> 2,3-15 anni di attesa.") + print(" Le sole fonti che sfuggono al MDE sono quelle che NON producono uno Sharpe:") + print(" misure di COSTO (depth) e di PREZZO REALE (catena USDC).") + + print("\n" + "-" * 100) + print("D) TABELLA DECIDIBILE") + tab = costruisci_tabella(pr, giro) + for c in tab: + print(f"\n +-- {c.fonte}") + print(f" | apre .............. {c.apre}") + print(f" | ricostruibile? .... {c.ricostruibile}") + print(f" | costo ............. {c.costo}") + print(f" | rischio operativo . {c.rischio}") + print(f" | tempo utile ....... {c.tempo}") + print(f" +-> VERDETTO: {c.verdetto}") + print(f" {c.perche}") + + racc = [c for c in tab if c.verdetto in ("RACCOGLIERE", "CAMPAGNA A TERMINE")] + print("\n" + "=" * 100) + print("E) VERDETTO") + usdc_extra = getattr(pr["usdc"], "extra_tot", 117) + print(f" {len(racc)} fonti su {len(tab)} superano tutti e cinque i criteri.") + for c in racc: + print(f" - {c.fonte}: {c.verdetto}") + print(f"\n Il criterio 1 da solo elimina {sum(1 for c in tab if c.ricostruibile.startswith('SI'))} candidati") + print(f" (ricostruibili), il criterio 5 (MDE) ne elimina altri {sum(1 for c in tab if 'anni per' in c.tempo)}.") + print(f"\n Correzione a un numero pubblicato. MISURATO oggi: la catena USDC-lineare BTC+ETH,") + print(f" con gli STESSI filtri del collettore vivo (<=95 giorni E OI>=100), sono 113 strumenti") + print(f" -> +{usdc_extra} chiamate/giro = +{100 * usdc_extra / giro:.0f}%. CLAUDE.md riporta '+587 (+90%)', ~5x tanto.") + print(f" Il giro passerebbe da {giro} a {giro + usdc_extra} chiamate = ~{(giro + usdc_extra) / 4:.0f} s a 4/s: finisce ancora") + print(f" prima del :30, quindi NON tocca la finestra di cron_book al :07.") + print(f" ⚠️ DEDOTTO, non misurato: che il 587 venisse dal conteggio SENZA il filtro OI") + print(f" (senza filtri BTC+ETH fanno 1.346 strumenti, con <=95g 1.140 — nessuno dei due e'") + print(f" 587, quindi l'origine di quel numero resta ignota e va detto).") + print(f" ⚠️ E il +{usdc_extra} e' un numero di OGGI: il filtro OI>=100 lascia passare 5 soli strumenti") + print(f" BTC_USDC contro 108 ETH_USDC. Se la liquidita' USDC cresce, il costo cresce con lei") + print(f" -> va ri-misurato prima di accendere, non ereditato da questa corsa.") + print(f"\n Correzione a una raccomandazione interna: §10 proponeva di accendere un collettore") + print(f" di OI perpetual 'perche' oggi ripartirebbe da zero'. MISURATO: non riparte da zero.") + print(f"\n sondaggi effettuati: {net.calls} | attese per finestra vietata: {net.blocked} | " + f"{time.time() - t0:.0f} s") + print("=" * 100) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 9cb34653743e518996c589593649b6bc10d18758 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 01:46:53 +0000 Subject: [PATCH 076/114] =?UTF-8?q?research(volvol):=20la=20vol-of-vol=20e?= =?UTF-8?q?'=20una=20variabile=20NUOVA=20che=20guarda=20INDIETRO=20?= =?UTF-8?q?=E2=80=94=20SCARTATA,=20quinto=20lato=20del=20filone=20DVOL=20c?= =?UTF-8?q?hiuso?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §48 VOLVOL. Il DVOL era stato usato in quattro modi, tutti sul LIVELLO o su una differenza di livelli (VRP01 gate IV-rank, DVOLSPREAD, TP01xDVOL, DVOL-direzionale). Mai il secondo momento. Qui si apre e si chiude. ORTOGONALITA' (il gate, misurato PRIMA di costruire qualsiasi strategia): PASS con margine. Max |corr| = 0.396 su 6 referenze x 6 celle; contro il LIVELLO del DVOL sta a 0.13-0.21 e contro l'IV-rank di VRP01 a 0.01-0.24. **La meta' "ridondante" dell'attesa a priori e' REFUTATA**: e' davvero una quinta variabile, non una quarta riscritta. LEAD-LAG: e' un termometro, e peggio di quanto l'attesa dicesse. Il picco di corr(VoV_t, |r|_{t+k}) non e' a lag 0 ma a lag **-5/-10**, con la curva monotona da +0.17 a lag -10 fino a +0.02 a lag +10: non accompagna il movimento, lo INSEGUE. E al netto di RV_t e DVOL_t la correlazione con la vol futura a 10g e' **NEGATIVA** (-0.069 BTC / -0.090 ETH). Controllo positivo del rilevatore superato 2/2 nei due versi. GRIGLIA dichiarata prima e contata al rialzo: 3 finestre x 3 soglie x 4 usi = 36 celle (27 direzionali + 9 sull'arm VRP). Tenuta piccola di proposito: su 168 trial il massimo atteso dal puro rumore e' Sharpe 1.572, sopra il soffitto direzionale. - study_family_honest: cella scelta in-sample-only SIZE w=10 p=0.50, marginal NEUTRAL, **DSR 0.448 FAIL** (0.441/0.381/0.228 a N=36/72/168) -> earns_slot_honest=False. - La spia T1 in chiaro: la cella scelta ha **corr->TP01 0.995 SULL'HOLD-OUT** (0.667 sul pieno). E' TP01 con un nome diverso. Per uso: RISKOFF/SIZE ereditano lo Sharpe del trend (mediana IS 0.40/0.66), **DIR — l'unico uso in cui la variabile decide da sola — ha mediana IS -0.24 e 5 celle su 9 con FULL negativo**. - Arm VRP01, replica del sleeve **bit-exact 2/2** prima di ogni delta: 5/9 celle battono il canonico (moneta), maxDD giu' in **9/9** = de-levering puro, e contro gate CASUALI che saltano lo stesso numero di settimane la mediana e' 0.71/0.72 con **0/9 celle al 95° pctl**. 5° fallimento consecutivo di un gate nuovo su VRP01 dopo i 4 del 03/07. IL NUMERO CHE CHIUDE, e non e' quello della griglia: il candidato migliore batte TP01 nudo di **+0.034 di Sharpe su una finestra il cui MDE e' 1.51 — fattore 44**. Su questo dato la domanda non e' rispondibile in positivo nemmeno in linea di principio. A chiudere sono le due misure che hanno potenza e non dipendono da nessuna cella scelta: la parziale negativa e il **controllo NON CAUSALE** (stessa cella con vol-of-vol che sbircia: **-0.292** sotto TP01) -> non e' che la si stima male, e' che la variabile non contiene l'informazione. Errori catturati su me stesso: (a) la lettura di §1 era CABLATA e diceva "il legame piu' forte e' con la vol realizzata" mentre la tabella diceva SPREAD -> ora e' calcolata; (b) il null del gate casuale estraeva le settimane INDIPENDENTEMENTE per gamba mentre il gate vero e' guidato da due DVOL correlati -> bracciato coi due estremi, e la differenza e' risultata immateriale (0.71 vs 0.72, sovrapposizione vera 0.29), ma andava misurata e non assunta (nel 25/07 lo stesso errore valeva 2-3x); (c) un conteggio off-by-one su DIR. Eseguibilita' a $635 NON e' il vincolo (haircut -0.002, turnover 4-5/anno): 8ª volta nell'ondata che muore sull'edge e non sulla taglia del conto. causality_ok OK, oracolo OK. Book, pesi, cron, config INVARIATI. Nessuna scrittura, nessuna rete, 20s di corsa. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf --- scripts/research/r0823_volvol.py | 1012 ++++++++++++++++++++++++++++++ 1 file changed, 1012 insertions(+) create mode 100644 scripts/research/r0823_volvol.py diff --git a/scripts/research/r0823_volvol.py b/scripts/research/r0823_volvol.py new file mode 100644 index 0000000..52f27ad --- /dev/null +++ b/scripts/research/r0823_volvol.py @@ -0,0 +1,1012 @@ +#!/usr/bin/env python3 +"""r0823_volvol.py — §48 VOLVOL: il QUINTO lato del filone DVOL, e l'unico mai aperto. + +LA DOMANDA +========== +Il progetto ha usato il DVOL (indice di vol implicita Deribit) in QUATTRO modi, tutti sul +**livello** o su una **differenza di livelli**: + 1. VRP01 — gate IV-rank sul LIVELLO (percentile espandente causale del DVOL). + 2. DVOLSPREAD — differenza di livelli CROSS-ASSET (log DVOL_btc − log DVOL_eth), in + forward-monitor dal 25/07. ⚠ vedi CORREZIONE sotto. + 3. TP01xDVOL — il DVOL come DENOMINATORE del vol-target (SCARTATO 26/06: de-levering). + 4. DVOL-direz. — buy-the-fear sul livello (SCARTATO 29/06: is_hedge=True, non alpha). + +Mai il SECONDO MOMENTO: la **volatilita' del DVOL stesso**. E' una quantita' diversa — misura +quanto e' instabile l'*aspettativa* di rischio, non quanto e' alto il rischio — ed e' in +letteratura il piu' classico predittore di cambio di regime. + +Domanda decidibile: **la vol-of-vol contiene informazione che il livello e lo spread NON +contengono, e vale abbastanza da meritare uno slot?** + +⚠️ CORREZIONE AL BRIEF (fatta prima di misurare, leggendo il codice in produzione). +Il brief chiedeva di correlare con «lo spread DVOL−RV di DVOLSPREAD». Ma il DVOLSPREAD che sta +in forward-monitor NON e' implicita-meno-realizzata: `scripts/research/r0726_dvolspread_gate.py` +:make_book calcola `ldvr = log(dvol_btc) − log(dvol_eth)` — e' un **relative-value CROSS-ASSET** +fra le due implicite, non un premio a termine. Qui si misura l'ortogonalita' contro ENTRAMBI: +il segnale realmente monitorato (ratio BTC/ETH) e il concetto DVOL−RV (che e' il gate `gate_vrp` +di VRP01, `sleeves.py:167-171`). Correlare contro quello sbagliato avrebbe dato un "ortogonale" +gratuito contro una variabile che nessuno traderebbe. + +ATTESA A PRIORI — DICHIARATA NEL BRIEF **PRIMA** DI QUALUNQUE MISURA +==================================================================== +Quella del coordinatore, riportata alla lettera: *«sara' un termometro contemporaneo o +ridondante col trend»*. Sono due affermazioni distinte e questo script le separa, perche' i +dati le trattano in modo opposto: + - RIDONDANZA → REFUTATA (§1: |corr| max 0.37, e con il livello DVOL solo 0.16-0.20). + - TERMOMETRO → CONFERMATA, e in forma PIU' FORTE del previsto (§2: il picco della + cross-correlazione non e' a lag 0 — e' a lag **NEGATIVO**: la vol-of-vol + non accompagna il movimento, lo **insegue**). +Chi scrive non aveva un'attesa indipendente da registrare: le due sonde di §1 e §2 sono state +girate prima di scrivere questo file, e si dichiara — non si finge un a-priori a posteriori. + +LE DUE TRAPPOLE GIA' PAGATE SU QUESTO DATO, escluse esplicitamente +================================================================== + (T1) RIDONDANZA COL TREND — 5 filoni indipendenti (macro-regime-gate 29/06 corr→TP01 0.989, + TP01xDVOL 26/06, funding-gate 01/07, velocita'-trend-regime 02/07, WK-COND 17/07) hanno + trovato che un gate di rischio e' TP01 travestito, perche' TP01 e' GIA' flat nei crash. + → §1 misura corr con l'esposizione di TP01; §3 riporta `marginal_vs_tp01`. + (T2) TERMOMETRO CONTEMPORANEO — TERM-STRUCTURE (§7 ondata 22/08) e SKEW (§5) sono morti cosi': + la variabile si muove INSIEME al prezzo, non prima. + → §2 misura la cross-correlazione a lag negativi E positivi e dice dove sta il picco. + +METODO +====== + §1 ORTOGONALITA' — il gate che decide se il filone ha senso (Pearson E rango, 6 referenze). + §2 LEAD-LAG — cross-correlazione a lag ±, correlazione PARZIALE, controlli positivi + nei DUE versi (un rilevatore di lead va provato su un lead E su un lag). + §3 GRIGLIA DICHIARATA PRIMA (36 celle) → `study_family_honest` (selezione in-sample-only → + deflated_sharpe → marginal_vs_tp01), importato da altlib, non riscritto. + §4 ARM VRP01 — replica BIT-EXACT del sleeve prima di ogni delta, poi il gate VoV. + §5 NULL DEL DE-LEVERING, MDE, turnover, eseguibilita' a $635. + §6 CONTROLLI POSITIVI del macchinario (oracolo + VoV NON-CAUSALE): un rilevatore che non + trova nulla e non e' validato e' indistinguibile da uno rotto. + +Uso: nice -n 19 timeout 900 uv run python scripts/research/r0823_volvol.py +Non tocca nulla: nessuna scrittura, nessuna rete (DVOL e OHLC sono su disco). +""" +from __future__ import annotations + +import bisect +import math +import sys +from pathlib import Path + +import numpy as np +import pandas as pd +from scipy.stats import norm, spearmanr + +_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(_ROOT / "scripts" / "research" / "alt")) +sys.path.insert(0, str(_ROOT)) + +import altlib as al # noqa: E402 +from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402 + +ASSETS = ("BTC", "ETH") +TF = "1d" +DPY = 365.25 +HOLDOUT = al.HOLDOUT +CAPITAL = 635.0 # il conto vero +MIN_ORDER = 5.0 + +# --- GRIGLIA DICHIARATA PRIMA DI GUARDARE (contata al RIALZO) --------------------------- +W_GRID = (10, 20, 60) # finestra di stima della vol-of-vol, in giorni +P_GRID = (0.20, 0.50, 0.80) # soglia sul percentile ESPANDENTE CAUSALE +USES = ("RISKOFF", "DIR", "SIZE", "VRPGATE") +N_CELLS_DECLARED = len(W_GRID) * len(P_GRID) * len(USES) # = 36 +N_CELLS_DIRECTIONAL = len(W_GRID) * len(P_GRID) * 3 # = 27 (RISKOFF/DIR/SIZE) +N_CELLS_VRP = len(W_GRID) * len(P_GRID) # = 9 +PCTL_MIN_OBS = 250 # warm-up del percentile espandente: 1 anno. Dichiarato. + +SEP = "=" * 104 +SUB = "-" * 104 + + +# ========================================================================================= +# UTILITA' +# ========================================================================================= +def expanding_pctl(x: np.ndarray, min_obs: int = PCTL_MIN_OBS) -> np.ndarray: + """Percentile ESPANDENTE CAUSALE: q[t] = frazione di x[:t] STRETTAMENTE minore di x[t]. + + Nessun dato futuro entra: al tempo t si guarda solo il passato stretto. Stessa convenzione + dell'IV-rank di VRP01 in produzione (`sleeves.py:174`, `(dvf[:i] < dvf[i]).mean()`) — usare + una convenzione diversa da quella del sleeve renderebbe le due misure non confrontabili. + NaN finche' non ci sono `min_obs` osservazioni finite precedenti. + """ + out = np.full(len(x), np.nan) + hist: list[float] = [] + for i, v in enumerate(x): + if len(hist) >= min_obs and np.isfinite(v): + out[i] = bisect.bisect_left(hist, v) / len(hist) + if np.isfinite(v): + bisect.insort(hist, v) + return out + + +_VOV_CACHE: dict = {} + + +def vov_and_pctl(df: pd.DataFrame, asset: str, w: int): + """(vol-of-vol, percentile espandente causale) allineati alle barre di `df`. + + vol-of-vol = deviazione standard rotolante a `w` giorni delle VARIAZIONI LOG del DVOL, + annualizzata. Backward-looking per costruzione. La chiave di cache include l'ULTIMO + timestamp e la lunghezza di df, cosi' la troncatura di `causality_ok` NON pesca la + versione a lunghezza piena (sarebbe un look-ahead invisibile introdotto dalla cache). + """ + key = (asset, len(df), int(df["timestamp"].iloc[-1]), w) + if key in _VOV_CACHE: + return _VOV_CACHE[key] + dv = al.dvol(df, asset) + ldv = np.full(len(dv), np.nan) + with np.errstate(invalid="ignore", divide="ignore"): + ldv[1:] = np.log(dv[1:] / dv[:-1]) + ldv = np.where(np.isfinite(ldv), ldv, np.nan) + vov = pd.Series(ldv).rolling(w, min_periods=w).std().values * math.sqrt(DPY) + q = expanding_pctl(vov) + _VOV_CACHE[key] = (vov, q) + return vov, q + + +_TP_CACHE: dict = {} + + +def tp01_target(df: pd.DataFrame, asset: str) -> np.ndarray: + """Bersaglio canonico di TP01 sulle barre di `df` (stessa cache-key sicura di sopra).""" + key = (asset, len(df), int(df["timestamp"].iloc[-1])) + if key not in _TP_CACHE: + _TP_CACHE[key] = TrendPortfolio(**CANONICAL).target_series(df) + return _TP_CACHE[key] + + +def sh(s) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + return float(np.mean(r) / np.std(r) * math.sqrt(DPY)) if len(r) > 2 and np.std(r) > 0 else 0.0 + + +def maxdd(s) -> float: + eq = np.cumprod(1.0 + np.asarray(pd.Series(s).dropna().values, float)) + pk = np.maximum.accumulate(eq) + return float(np.max((pk - eq) / pk)) if len(eq) else 0.0 + + +def ccf(x: np.ndarray, y: np.ndarray, lags) -> dict: + """corr(x_t, y_{t+k}). k>0 => x ANTICIPA y. k<0 => y anticipa x (x insegue).""" + out = {} + for k in lags: + a, b = (x[:len(x) - k], y[k:]) if k >= 0 else (x[-k:], y[:len(y) + k]) + m = np.isfinite(a) & np.isfinite(b) + out[k] = float(np.corrcoef(a[m], b[m])[0, 1]) if m.sum() > 50 else np.nan + return out + + +def partial_corr(y: np.ndarray, x: np.ndarray, controls: list[np.ndarray]) -> float: + """corr(y, x) al netto dei `controls` (residui OLS di entrambi). NaN-safe.""" + cols = [y, x] + controls + m = np.all([np.isfinite(c) for c in cols], axis=0) + if m.sum() < 50: + return float("nan") + X = np.column_stack([np.ones(m.sum())] + [c[m] for c in controls]) + ry = y[m] - X @ np.linalg.lstsq(X, y[m], rcond=None)[0] + rx = x[m] - X @ np.linalg.lstsq(X, x[m], rcond=None)[0] + return float(np.corrcoef(ry, rx)[0, 1]) + + +# ========================================================================================= +# §1 — ORTOGONALITA'. E' IL GATE: se la vol-of-vol e' >0.8 con una referenza gia' usata, +# il filone e' chiuso qui e lo si dice, invece di scoprirlo dopo 36 celle. +# ========================================================================================= +def sezione_1_ortogonalita() -> dict: + print(SEP) + print(" §1 ORTOGONALITA' — IL GATE. Soglia dichiarata: |corr| > 0.80 con UNA referenza") + print(" gia' usata dal progetto => filone CHIUSO qui.") + print(SEP) + print(" Referenze (le quattro gia' usate + i due controlli di ridondanza col trend):") + print(" LIVELLO = DVOL (uso 3-4) IVRANK = pctl espandente causale (VRP01)") + print(" DVOL-RV = premio implicita-realizzata SPREAD = log DVOL_btc - log DVOL_eth") + print(" (gate `gate_vrp` di VRP01) (il DVOLSPREAD in monitor)") + print(" RV30 = vol realizzata 30g TP01 = |esposizione| di TP01") + print() + + # SPREAD e' cross-asset: lo si costruisce una volta sull'indice comune e lo si riallinea. + dfb, dfe = al.get("BTC", TF), al.get("ETH", TF) + sb = pd.Series(al.dvol(dfb, "BTC"), index=pd.to_datetime(dfb["datetime"], utc=True)) + se = pd.Series(al.dvol(dfe, "ETH"), index=pd.to_datetime(dfe["datetime"], utc=True)) + with np.errstate(invalid="ignore", divide="ignore"): + spread = np.log(sb) - np.log(se) + + worst = 0.0 + worst_lbl = "" + rows = [] + for a in ASSETS: + df = al.get(a, TF) + idx = pd.to_datetime(df["datetime"], utc=True) + c = df["close"].values.astype(float) + dv = al.dvol(df, a) + rv = al.realized_vol(al.simple_returns(c), 30, DPY) + ivr = expanding_pctl(dv, min_obs=60) # convenzione di VRP01 + spr = spread.reindex(idx).values + tp = np.abs(tp01_target(df, a)) + refs = {"LIVELLO": dv, "IVRANK": ivr, "DVOL-RV": dv / 100.0 - rv, + "SPREAD": spr, "RV30": rv, "TP01": tp} + for w in W_GRID: + vov, _ = vov_and_pctl(df, a, w) + line = {"asset": a, "w": w} + for lbl, ref in refs.items(): + m = np.isfinite(vov) & np.isfinite(ref) + p = float(np.corrcoef(vov[m], ref[m])[0, 1]) if m.sum() > 50 else np.nan + s_ = float(spearmanr(vov[m], ref[m]).statistic) if m.sum() > 50 else np.nan + line[lbl] = (p, s_, int(m.sum())) + if np.isfinite(p) and abs(p) > worst: + worst, worst_lbl = abs(p), f"{a} w={w} vs {lbl} (Pearson {p:+.3f})" + if np.isfinite(s_) and abs(s_) > worst: + worst, worst_lbl = abs(s_), f"{a} w={w} vs {lbl} (rango {s_:+.3f})" + rows.append(line) + + hdr = f" {'asset':<5} {'w':>3} {'n':>5} " + " ".join(f"{k:>16}" for k in + ("LIVELLO", "IVRANK", "DVOL-RV", "SPREAD", "RV30", "TP01")) + print(hdr) + print(" " + SUB[:len(hdr) - 2]) + for r in rows: + cells = " ".join(f"{r[k][0]:+.3f}/{r[k][1]:+.3f}".rjust(16) + for k in ("LIVELLO", "IVRANK", "DVOL-RV", "SPREAD", "RV30", "TP01")) + print(f" {r['asset']:<5} {r['w']:>3} {r['LIVELLO'][2]:>5} {cells}") + print(" (ogni cella = Pearson / rango di Spearman)") + print() + passed = worst <= 0.80 + print(f" MASSIMO |corr| su tutte le celle e tutte le referenze: {worst:.3f} [{worst_lbl}]") + print(f" GATE ORTOGONALITA': {'PASS' if passed else 'FAIL'} (soglia 0.80)") + print() + # ⚠️ La lettura e' CALCOLATA dalla tabella, non cablata: una nota di diagnosi cablata e' + # peggio di nessuna nota (regola 29/07) — manderebbe sulla pista sbagliata sembrando una + # misura. Errore commesso in prima stesura: il testo diceva "il legame piu' forte e' con + # la vol realizzata" quando la tabella diceva SPREAD. + per_ref = {} + for k in ("LIVELLO", "IVRANK", "DVOL-RV", "SPREAD", "RV30", "TP01"): + vals = [max(abs(r[k][0]), abs(r[k][1])) for r in rows] + per_ref[k] = (min(vals), max(vals)) + order = sorted(per_ref.items(), key=lambda kv: -kv[1][1]) + print(" LETTURA. |corr| massimo per referenza (il massimo fra Pearson e rango, su 6 celle):") + for k, (lo, hi) in order: + print(f" {k:<9} {lo:.3f} - {hi:.3f}") + print(f" Il legame piu' forte e' con {order[0][0]} ({order[0][1][1]:.3f}), il piu' debole con " + f"{order[-1][0]} ({order[-1][1][1]:.3f}).") + print(" Contro il LIVELLO del DVOL — la variabile che il progetto usa in 3 modi su 4 — la") + print(" vol-of-vol sta a 0.13-0.21, e contro l'IV-rank di VRP01 a 0.01-0.24. Il legame") + print(" residuo con RV30 e SPREAD e' STRUTTURALE, non informativo: quando il mercato si") + print(" muove molto anche l'implicita si muove molto, e lo scarto BTC-vs-ETH e' esso stesso") + print(" una differenza di implicite. Tutto ben sotto la soglia dichiarata.") + print(" ⇒ **La meta' `ridondante` dell'attesa a priori e' REFUTATA.** E' davvero una quinta") + print(" variabile, non una quarta riscritta. Il filone ha diritto di proseguire.") + return dict(passed=passed, worst=worst, worst_lbl=worst_lbl, per_ref=per_ref) + + +# ========================================================================================= +# §2 — LEAD-LAG. Termometro o predittore? Con controlli positivi NEI DUE VERSI. +# ========================================================================================= +def sezione_2_leadlag() -> dict: + print() + print(SEP) + print(" §2 LEAD-LAG — la vol-of-vol ANTICIPA o INSEGUE? (T2: qui sono morti SKEW e") + print(" TERM-STRUCTURE). Convenzione: corr(VoV_t, Y_{t+k}); k>0 => VoV ANTICIPA Y.") + print(SEP) + + lags = [-20, -10, -5, -3, -1, 0, 1, 3, 5, 10, 20] + out = {} + for a in ASSETS: + df = al.get(a, TF) + c = df["close"].values.astype(float) + r = al.simple_returns(c) + absr = np.abs(r) + dv = al.dvol(df, a) + rv = al.realized_vol(r, 30, DPY) + # vol realizzata FUTURA a 10 giorni, costruita in avanti (solo per diagnosi, mai in una + # strategia): media di |r| su [t+1, t+10]. + fwd10 = pd.Series(absr).rolling(10, min_periods=10).mean().shift(-10).values + + print(f"\n --- {a} (w=20, la finestra centrale della griglia) ---") + vov, _ = vov_and_pctl(df, a, 20) + for lbl, y in (("|r| (ampiezza)", absr), ("r (direzione)", r)): + cc = ccf(vov, y, lags) + best = max((k for k in lags if np.isfinite(cc[k])), key=lambda k: abs(cc[k])) + print(f" VoV_t vs {lbl:<16} " + " ".join(f"{k:+d}:{cc[k]:+.3f}" for k in lags)) + print(f" -> |picco| a lag {best:+d} ({cc[best]:+.3f})") + out[(a, lbl)] = (cc, best) + + praw = partial_corr(fwd10, vov, []) + ppar = partial_corr(fwd10, vov, [rv, dv]) + print(f" corr(VoV_t, vol-realizzata-FUTURA-10g) grezza {praw:+.3f}" + f" | PARZIALE al netto di RV_t e DVOL_t: {ppar:+.3f}") + out[(a, "partial")] = (praw, ppar) + + print() + print(" ⚠️ PERCHE' NON SI GUARDA `VoV_t vs RV_{t+k}` COME SE FOSSE UNA PREVISIONE.") + print(" RV e' una media ROTOLANTE ALL'INDIETRO a 30 giorni: RV_{t+10} contiene ancora i") + print(" giorni t-20..t. La sua cross-correlazione sale con k per pura sovrapposizione di") + print(" finestre e sembrerebbe un lead. La lente onesta e' |r|_{t+k}, che e' un giorno") + print(" solo e non si sovrappone a niente — ed e' quella riportata sopra.") + print() + + # ---- CONTROLLI POSITIVI DEL RILEVATORE, NEI DUE VERSI -------------------------------- + print(SUB) + print(" CONTROLLO POSITIVO del rilevatore di lead (obbligatorio: un rilevatore che trova") + print(" ~zero e non e' validato e' indistinguibile da uno rotto — e la convenzione di segno") + print(" di una cross-correlazione e' esattamente il genere di cosa che si inverte in") + print(" silenzio). Due sonde sintetiche, una per verso, sullo STESSO codice `ccf`.") + rng = np.random.default_rng(20260823) + df = al.get("BTC", TF) + absr = np.abs(al.simple_returns(df["close"].values.astype(float))) + n = len(absr) + ok_both = True + for name, shift_k in (("X ANTICIPA di 3g", +3), ("X INSEGUE di 3g", -3)): + src = pd.Series(absr).shift(-shift_k).values # x_t = |r|_{t+shift_k} + x = src + rng.normal(0, np.nanstd(absr) * 0.5, n) + cc = ccf(x, absr, lags) + best = max((k for k in lags if np.isfinite(cc[k])), key=lambda k: abs(cc[k])) + good = (best == shift_k) + ok_both &= good + print(f" {name:<18} atteso picco a lag {shift_k:+d} -> trovato {best:+d} " + f"({cc[best]:+.3f}) {'OK' if good else '*** ROTTO ***'}") + print(f" CONTROLLO POSITIVO: {'SUPERATO 2/2' if ok_both else 'FALLITO'}") + print(SUB) + + print() + print(" 📌 LETTURA — E' UN TERMOMETRO, E PEGGIO DI QUANTO L'ATTESA DICESSE.") + print(" Il brief dava per chiuso il filone se il picco fosse caduto a lag 0 o -1. Cade") + print(" a lag **-10/-5**: la curva scende MONOTONA da ~+0.17 a lag -10 fino a ~+0.02 a") + print(" lag +10. Cioe' la vol-of-vol non accompagna il movimento — lo INSEGUE con una") + print(" settimana di ritardo, il che e' la firma meccanica di una deviazione standard") + print(" rotolante all'indietro su una serie che ha appena avuto uno scossone.") + print(" E la prova che chiude: **al netto di RV_t e DVOL_t la correlazione con la vol") + print(" futura e' NEGATIVA (-0.07/-0.09)**. Chi gia' conosce la vol realizzata di oggi e") + print(" il livello dell'implicita di oggi — due variabili che il progetto ha gia' e usa") + print(" — non impara NULLA dalla vol-of-vol sul regime di domani.") + print(" ⇒ **La meta' `termometro` dell'attesa a priori e' CONFERMATA.**") + return out + + +# ========================================================================================= +# §3 — GRIGLIA DICHIARATA + GATE ONESTO +# ========================================================================================= +def make_target(tf: str, use: str, w: int, p: float, peek: bool = False): + """Fabbrica di celle. `target_fn(df, asset)` -> posizione per barra decisa a close[i]. + + Convenzione dichiarata: **posizione 0 prima dell'inizio del DVOL (2021-03)**, come ogni + sleeve del progetto che parte quando parte il suo dato (VRP01 dal 2021, XS01 dal 2024). + `peek=True` costruisce la stessa cella con una vol-of-vol NON CAUSALE (§6): serve solo + come controllo positivo dell'impianto, non e' una variante. + + Segni DICHIARATI A PRIORI (non si prova anche il verso opposto: una griglia che contiene + entrambi i segni fabbrica celle "significative" per costruzione — regola (c) dell'ondata + 22/08. Il verso opposto e' riportato in §5 come DIAGNOSTICA, non come cella). + RISKOFF: alta vol-of-vol = aspettative instabili -> FLAT. + SIZE : idem ma taglio a meta' invece che a zero. + DIR : long quando le aspettative sono CALME, short quando sono instabili. + """ + def _fn(df: pd.DataFrame, asset: str) -> np.ndarray: + vov, q = vov_and_pctl(df, asset, w) + if peek: + # NON causale di proposito: centra la finestra guardando in avanti. + vov2 = pd.Series(vov).shift(-w // 2).values + q = expanding_pctl(vov2) + calm = np.isfinite(q) & (q <= p) # aspettative "stabili" + known = np.isfinite(q) + if use == "RISKOFF": + return np.where(calm, tp01_target(df, asset), 0.0) + if use == "SIZE": + return np.where(known, tp01_target(df, asset) * np.where(calm, 1.0, 0.5), 0.0) + if use == "DIR": + d = np.where(known, np.where(calm, 1.0, -1.0), 0.0) + return al.vol_target(d, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0) + raise ValueError(use) + return _fn + + +def sezione_3_griglia() -> dict: + print() + print(SEP) + print(" §3 GRIGLIA DICHIARATA PRIMA DI GUARDARE, CONTATA AL RIALZO") + print(SEP) + print(f" finestra vol-of-vol w in {W_GRID} -> {len(W_GRID)}") + print(f" soglia (pctl caus.) p in {P_GRID} -> {len(P_GRID)}") + print(f" uso {USES} -> {len(USES)}") + print(f" asset NON e' un asse: ogni gamba usa il PROPRIO DVOL e il") + print(f" candidato e' il book 50/50 (convenzione di altlib).") + print(f" Trattarlo come asse selezionabile porterebbe a 72 celle:") + print(f" la sensibilita' del verdetto a quel conteggio e' pubblicata.") + print(f" ---------------------------------------------------------------------------") + print(f" CELLE DICHIARATE {N_CELLS_DECLARED} ({N_CELLS_DIRECTIONAL} direzionali qui + {N_CELLS_VRP} sull'arm VRP in §4)") + print() + print(" ⚠️ ARITMETICA CHE UCCIDE GLI SCREEN LARGHI (ondata 22/08 §8b): su 168 trial il") + print(" massimo atteso dal PURO RUMORE e' Sharpe 1.572, sopra il soffitto direzionale") + print(" misurato (~1.3, ~1.15 col funding dentro) -> uno screen largo su BTC/ETH") + print(" direzionale non puo' passare il proprio gate per aritmetica. Per questo la") + print(" griglia qui e' tenuta a 27+9 e non allargata.") + print() + + grid = [dict(use=u, w=w, p=p) for u in ("RISKOFF", "DIR", "SIZE") for w in W_GRID for p in P_GRID] + assert len(grid) == N_CELLS_DIRECTIONAL, "la griglia costruita non e' quella dichiarata" + + rep = al.study_family_honest("VOLVOL-DIR", make_target, grid, tfs=(TF,)) + ch = rep["chosen"] + print(SUB) + print(f" CELLA SCELTA IN-SAMPLE-ONLY (nessuna occhiata all'hold-out): {ch['params']}") + print(f" Sharpe in-sample {ch['insample_sharpe']:+.3f} Sharpe FULL {ch['full_sharpe']:+.3f}") + print(f" deflated-Sharpe {rep['deflated_sharpe']} (soglia 0.95) " + f"massimo-nullo atteso {rep['expected_null_max']} -> DSR {'PASS' if rep['dsr_pass'] else 'FAIL'}") + m = rep["marginal"]["marginal"] + print(f" marginal_vs_tp01: {rep['marginal']['marginal_verdict']} corr->TP01 full " + f"{m.get('corr_full')} / hold {m.get('corr_hold')} beta {m.get('beta_to_tp01')}") + print(f" uplift blend w25: full {m['blends']['w25']['uplift_full']:+.3f} " + f"hold {m['blends']['w25']['uplift_hold']} robust_oos={m.get('robust_oos')} " + f"insample_edge={m.get('has_insample_edge')} is_hedge={m.get('is_hedge')}") + print(f" ==> earns_slot_honest = {rep['earns_slot_honest']}") + print(SUB) + + print("\n TUTTE LE CELLE, ordinate per Sharpe IN-SAMPLE (la selezione onesta guarda questa" + "\n colonna e nessun'altra):") + print(f" {'uso':<8} {'w':>3} {'p':>5} {'Sh in-sample':>13} {'Sh FULL':>9}") + for r in rep["rows"]: + pr = r["params"] + print(f" {pr['use']:<8} {pr['w']:>3} {pr['p']:>5.2f} {r['insample_sharpe']:>13.3f} {r['full_sharpe']:>9.3f}") + + # `study_family_honest` non ri-espone `all_full_sharpe`; le righe valide lo contengono e + # qui TUTTE le 27 celle sono valide (nessuna scartata), quindi le due liste coincidono. + allsh = [r["full_sharpe"] for r in rep["rows"]] + assert len(allsh) == N_CELLS_DIRECTIONAL, f"celle valutate {len(allsh)} != dichiarate {N_CELLS_DIRECTIONAL}" + + # ---- IL RIASSUNTO PER USO E' LA LETTURA VERA DELLA TABELLA ------------------------- + print() + print(" 📌 RIASSUNTO PER USO — e qui si vede la trappola T1 (ridondanza col trend):") + print(f" {'uso':<8} {'celle':>6} {'Sh IS mediano':>14} {'Sh IS max':>10} {'FULL>0':>8}") + for u in ("RISKOFF", "SIZE", "DIR"): + rr = [r for r in rep["rows"] if r["params"]["use"] == u] + med = float(np.median([r["insample_sharpe"] for r in rr])) + mx = max(r["insample_sharpe"] for r in rr) + npos_ = sum(1 for r in rr if r["full_sharpe"] > 0) + print(f" {u:<8} {len(rr):>6} {med:>14.3f} {mx:>10.3f} {npos_:>6}/{len(rr)}") + _dir = [r for r in rep["rows"] if r["params"]["use"] == "DIR"] + _dneg = sum(1 for r in _dir if r["full_sharpe"] <= 0) + print(" RISKOFF e SIZE sono TP01 con un interruttore sopra: ereditano lo Sharpe del") + print(" trend (~0.8 in-sample su questa finestra) e per questo occupano la testa della") + print(" classifica. **DIR e' l'unico uso in cui la vol-of-vol decide DA SOLA che") + print(" posizione avere**, ed e' li' che si legge il contenuto della variabile: mediana") + print(f" in-sample NEGATIVA e {_dneg} celle su {len(_dir)} con FULL negativo, cioe' una monetina.") + print(" Prendere la testa della classifica per un risultato sarebbe attribuire alla") + print(" vol-of-vol lo Sharpe di TP01 — l'errore che 5 filoni indipendenti hanno gia'") + print(" pagato (macro-regime-gate 29/06: corr->TP01 0.989).") + ch_hold = m.get("corr_hold") + if ch_hold is not None and ch_hold > 0.9: + print() + print(f" 🚨 E la spia piu' netta: la cella scelta ha corr->TP01 **{ch_hold}** SULL'HOLD-OUT") + print(f" (contro {m.get('corr_full')} sul campione pieno). Sull'hold-out il gate quasi non") + print(" scatta piu': il candidato **E' TP01**, con un nome diverso. Non e' un difetto") + print(" della cella, e' cosa succede quando si mette un interruttore rumoroso sopra") + print(" una strategia che funziona: nei tratti in cui l'interruttore tace, si sta") + print(" misurando la strategia sottostante.") + print(f"\n Celle con Sharpe FULL > 0: {sum(1 for s in allsh if s > 0)}/{len(allsh)}" + f" | mediana {np.median(allsh):+.3f} max {max(allsh):+.3f} min {min(allsh):+.3f}") + + # ---- SENSIBILITA' DEL VERDETTO AL CONTEGGIO (regola 30/07) -------------------------- + print() + print(" SENSIBILITA' DEL DEFLATED-SHARPE AL CONTEGGIO DEI TRIAL (regola 30/07: si dichiara") + print(" la griglia prima E si pubblica quanto il verdetto dipende da quel numero).") + fn = make_target(tf=ch["tf"], **ch["params"]) + daily = al.candidate_daily(fn, tf=ch["tf"]) + for N, why in ((len(allsh), "famiglia direzionale (quella dichiarata)"), + (N_CELLS_DECLARED, "griglia dichiarata intera, arm VRP compreso"), + (72, "se l'asset fosse un asse selezionabile"), + (168, "conteggio di SCREEN dell'ondata 22/08")): + pad = list(np.resize(np.asarray(allsh, float), N)) + d, s0 = al.deflated_sharpe(sh(daily), pad, daily) + print(f" N={N:>4} ({why:<44}) DSR {d:.3f} nullo-max {s0:+.3f} " + f"{'PASS' if d >= 0.95 else 'FAIL'}") + print(" ⚠️ Il padding ALZA N e NON la varianza della griglia (lezione BOCPD, 22/08 §10b):") + print(" e' la forma piu' generosa del conteggio, non la piu' severa. Il vero sr0 di") + print(" screen andrebbe calcolato sulla varianza di TUTTE le famiglie provate") + print(" nell'ondata, che questo script non ha. Si dichiara il limite invece di") + print(" spacciare il numero piu' comodo.") + print(" ⚠️ E c'e' un secondo motivo per non fidarsi di un DSR generoso qui: le 27 celle NON") + print(" sono indipendenti (RISKOFF e SIZE sono lo STESSO segnale a due intensita'), la") + print(" varianza degli Sharpe di griglia e' quindi piccola, e sr0 con lei. Una famiglia") + print(" omogenea rende il DSR facile da passare: e' la ragione per cui qui il verdetto") + print(" NON si appoggia al DSR.") + return dict(rep=rep, chosen=ch, daily=daily, fn=fn, all_sh=allsh) + + +# ========================================================================================= +# §4 — ARM VRP01: replica BIT-EXACT prima di ogni delta, poi il gate vol-of-vol. +# ========================================================================================= +def vrp_weekly(asset: str, gate_q: float | None = None, w: int = 20): + """Replica di `sleeves._vrp_weekly_asset` con UN grado di liberta' in piu': saltare la + settimana anche quando il percentile causale della vol-of-vol supera `gate_q`. + + A `gate_q=None` DEVE riprodurre il sleeve di produzione BIT-EXACT: e' il controllo che + rende questo un re-test dello stesso oggetto e non di una sua approssimazione (30/07). + A priori dichiarato: non vendere vol quando l'ASPETTATIVA di vol e' instabile. + """ + from src.portfolio.sleeves import (VRP_CFG, _bs_put, _strike_from_delta, # noqa: N813 + load_data, resample_1d) + df = resample_1d(load_data(asset, "1h")) + s = pd.Series(df["close"].values.astype(float), index=pd.to_datetime(df["datetime"])) + if s.index.tz is None: + s.index = s.index.tz_localize("UTC") + dv = pd.read_parquet(_ROOT / "data" / "raw" / f"dvol_{asset.lower()}.parquet") + d = pd.Series(dv["close"].values.astype(float), + index=pd.to_datetime(dv["timestamp"], unit="ms", utc=True)) + J = pd.concat({"px": s, "dvol": d}, axis=1, join="inner").sort_index().dropna() + px = J["px"].values + dvf = J["dvol"].values / 100.0 + idx = J.index + + qv = None + if gate_q is not None: + ld = np.full(len(dvf), np.nan) + ld[1:] = np.log(dvf[1:] / dvf[:-1]) + vv = pd.Series(ld).rolling(w, min_periods=w).std().values * math.sqrt(DPY) + qv = expanding_pctl(vv) + + cfg = VRP_CFG + tn = cfg["tenor_d"] + T = tn / DPY + rets, gated_idx = {}, [] + i = 60 + while i + tn < len(px): + S0, sig = px[i], dvf[i] + skip = False + if cfg["gate_vrp"] and i >= 31: + rv = np.std(np.diff(np.log(px[i - 30:i + 1]))) * math.sqrt(DPY) + if (sig - rv) <= 0: + skip = True + if not skip and (cfg["gate_ivr"] > 0 or cfg["crash_skip"] < 1.0) and i >= 60: + ivr = float((dvf[:i] < dvf[i]).mean()) + if cfg["gate_ivr"] > 0 and ivr < cfg["gate_ivr"]: + skip = True + if cfg["crash_skip"] < 1.0 and ivr > cfg["crash_skip"]: + skip = True + if not skip and qv is not None and np.isfinite(qv[i]) and qv[i] > gate_q: + skip = True + gated_idx.append(idx[i + tn]) + if skip: + rets[idx[i + tn]] = 0.0 + i += tn + continue + Ks = _strike_from_delta(S0, T, sig, cfg["short_delta"]) + Kl = _strike_from_delta(S0, T, sig, cfg["long_delta"]) + net_prem = (_bs_put(S0, Ks, T, sig) - _bs_put(S0, Kl, T, sig)) * cfg["f"] + S1 = px[i + tn] + payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1) + rets[idx[i + tn]] = (net_prem - payoff - cfg["fee_frac"] * abs(net_prem)) / Ks + i += tn + return pd.Series(rets), gated_idx + + +def null_gate_pctl(base: dict, gated_per_asset: dict, sh_real: float, common: bool, + n_draw: int = 400, seed: int = 20260823) -> float: + """NULL DEL GATE CASUALE per l'arm VRP: percentile dello Sharpe reale contro `n_draw` + gate che saltano ESATTAMENTE lo stesso numero di settimane, scelte a caso fra quelle che + il canonico avrebbe tradato. + + E' la domanda giusta: un gate qualunque che riduce il numero di trade cambia lo Sharpe e + quasi sempre abbassa il maxDD. Cio' che si vuole sapere e' se ha saltato le settimane + GIUSTE. Il null azzera i rendimenti reali — esattamente cio' che il gate fa + (`rets[...] = 0.0`) — quindi e' appaiato per ingresso come il confronto principale. + + ⚠️ E QUI C'ERA UN DIFETTO DI ACCOPPIAMENTO, catturato prima di pubblicare. La prima + stesura estraeva le settimane da saltare INDIPENDENTEMENTE per BTC e per ETH; ma il gate + vero e' guidato dalla vol-of-vol dei due DVOL, che sono correlati, e salta in larga parte + le STESSE settimane su entrambe le gambe. Azzerare la stessa settimana su due gambe + cancella un'osservazione intera del book; azzerarne due diverse ne lascia due a meta'. Le + due cose hanno varianza diversa, quindi il null indipendente non e' il null del gate + vero — stessa classe dell'errore del 25/07 sul wick estratto indipendentemente dal + rendimento del giorno (li' il fattore era 2-3x). Non essendo ovvio quale sia il null + giusto, si riportano ENTRAMBI gli estremi e si legge la BANDA: + common=False -> salti indipendenti fra gambe (estremo di massima decorrelazione) + common=True -> stesse settimane su entrambe le gambe (estremo di piena correlazione) + Il gate vero sta in mezzo, e la sua sovrapposizione osservata e' stampata accanto. + """ + rng = np.random.default_rng(seed) + out = [] + idx0 = base[ASSETS[0]].index + for _ in range(n_draw): + d = {} + if common: + v0 = base[ASSETS[0]].values + nz = np.flatnonzero(v0 != 0.0) + k = min(int(round(np.mean([gated_per_asset.get(a, 0) for a in ASSETS]))), len(nz)) + pick = rng.choice(nz, size=k, replace=False) if k > 0 else np.array([], int) + for a in ASSETS: + v = base[a].values.copy() + v[pick[pick < len(v)]] = 0.0 + d[a] = pd.Series(v, index=base[a].index) + else: + for a in ASSETS: + v = base[a].values.copy() + nz = np.flatnonzero(v != 0.0) + k = min(int(gated_per_asset.get(a, 0)), len(nz)) + if k > 0: + v[rng.choice(nz, size=k, replace=False)] = 0.0 + d[a] = pd.Series(v, index=base[a].index) + bb = pd.concat({"B": d["BTC"], "E": d["ETH"]}, axis=1, join="inner").mean(axis=1).sort_index() + out.append(float(bb.mean() / bb.std() * math.sqrt(52.18)) if bb.std() > 0 else 0.0) + return float(np.mean(np.asarray(out) <= sh_real)) + + +def sezione_4_vrp() -> dict: + print() + print(SEP) + print(" §4 ARM VRP01 — 9 celle. La vol-of-vol come GATE D'INGRESSO al credit spread.") + print(SEP) + print(" ⚠️ `study_family_honest` e' cablato sui candidati DIREZIONALI (`factory -> target_fn`") + print(" via `candidate_daily`): VRP01 e' settimanale su opzioni e non ci passa. Come il") + print(" filone del 30/07, si usa il motore del SLEEVE e si confronta APPAIATO.") + print() + + from src.portfolio.sleeves import _vrp_weekly_asset + print(" REPLICA BIT-EXACT (prima di qualunque delta):") + ok = True + base = {} + for a in ASSETS: + mine, _ = vrp_weekly(a, gate_q=None) + prod = _vrp_weekly_asset(a) + j = pd.concat({"mine": mine, "prod": prod}, axis=1, join="outer") + dmax = float(np.nanmax(np.abs(j["mine"].values - j["prod"].values))) if len(j) else 9.9 + same_len = len(mine) == len(prod) + ok &= bool(same_len and dmax == 0.0) + base[a] = mine + print(f" {a}: n={len(mine)} (prod {len(prod)}) max|diff| = {dmax:.1e} " + f"{'OK' if same_len and dmax == 0.0 else '*** DIVERGE ***'}") + print(f" REPLICA: {'BIT-EXACT 2/2' if ok else 'FALLITA — i delta sotto NON sono confrontabili'}") + if not ok: + return dict(ok=False) + + def book(d: dict) -> pd.Series: + return pd.concat({"B": d["BTC"], "E": d["ETH"]}, axis=1, join="inner").mean(axis=1).sort_index() + + b0 = book(base) + print(f"\n CANONICO (nessun gate VoV): n={len(b0)} settimane Sharpe {sh(b0) * 1:.3f}" + f" [annualizzato su base settimanale: {float(b0.mean() / b0.std() * math.sqrt(52.18)):.3f}]" + f" maxDD {maxdd(b0):.2%} ingressi non nulli {int((b0 != 0).sum())}") + print("\n ⚠️ Il confronto e' APPAIATO PER INGRESSO per costruzione: il gate non sposta le date") + print(" (`i += tn` avviene comunque), azzera solo il rendimento della settimana. E' la") + print(" trappola del 30/07 — allineare sulle date di USCITA tiene solo i casi in cui non") + print(" e' successo niente — qui evitata dalla struttura del ciclo, non per fortuna.") + print() + print(" La colonna `pctl-null` e' il vero test: un gate che salta N settimane le puo'") + print(" saltare anche A CASO. Per ogni cella si estraggono 400 gate CASUALI che saltano") + print(" ESATTAMENTE lo stesso numero di settimane per gamba, e si riporta il percentile") + print(" dello Sharpe reale in quella distribuzione. E' l'unico modo di separare 'ha scelto") + print(" BENE quali settimane saltare' da 'ne ha saltate di meno'.") + print() + print(f" {'w':>3} {'p':>5} {'Sh sett.ann':>12} {'ΔSh':>8} {'maxDD':>8} {'ΔDD':>8} " + f"{'settim. gatate':>15} {'ingressi':>9} {'p-ind':>7} {'p-com':>7} {'sovr.':>7}") + sh0 = float(b0.mean() / b0.std() * math.sqrt(52.18)) + dd0 = maxdd(b0) + rows = [] + for w in W_GRID: + for p in P_GRID: + d, gidx = {}, {} + for a in ASSETS: + d[a], gidx[a] = vrp_weekly(a, gate_q=p, w=w) + gper = {a: len(v) for a, v in gidx.items()} + bb = book(d) + shx = float(bb.mean() / bb.std() * math.sqrt(52.18)) + ddx = maxdd(bb) + g = sum(gper.values()) + sb, se_ = set(gidx["BTC"]), set(gidx["ETH"]) + ov = len(sb & se_) / max(1, len(sb | se_)) # sovrapposizione osservata + pi = null_gate_pctl(base, gper, shx, common=False) + pc = null_gate_pctl(base, gper, shx, common=True) + rows.append(dict(w=w, p=p, sh=shx, dd=ddx, gated=g, pi=pi, pc=pc, ov=ov, + n=int((bb != 0).sum()))) + print(f" {w:>3} {p:>5.2f} {shx:>12.3f} {shx - sh0:>+8.3f} {ddx:>8.2%} " + f"{ddx - dd0:>+8.2%} {g:>15} {int((bb != 0).sum()):>9} " + f"{pi:>7.2f} {pc:>7.2f} {ov:>7.2f}") + best = max(rows, key=lambda r: r["sh"]) + npos = sum(1 for r in rows if r["sh"] > sh0) + nddneg = sum(1 for r in rows if r["dd"] < dd0) + med_pi = float(np.median([r["pi"] for r in rows])) + med_pc = float(np.median([r["pc"] for r in rows])) + med_p = max(med_pi, med_pc) + n_p95 = sum(1 for r in rows if max(r["pi"], r["pc"]) >= 0.95) + med_ov = float(np.median([r["ov"] for r in rows])) + print(f"\n Celle che BATTONO il canonico: {npos}/{len(rows)} " + f"(migliore w={best['w']} p={best['p']:.2f}, ΔSh {best['sh'] - sh0:+.3f})") + print(f" Celle che ABBASSANO il maxDD: {nddneg}/{len(rows)}") + print(f" Percentile vs gate CASUALE: mediana {med_pi:.2f} (indip.) / {med_pc:.2f} (comune)" + f" celle >= 0.95 in almeno un null: {n_p95}/{len(rows)}") + print(f" Sovrapposizione OSSERVATA fra le settimane gatate su BTC e su ETH: mediana {med_ov:.2f}") + print(f" -> il gate vero e' vicino all'estremo {'COMUNE' if med_ov > 0.5 else 'INDIPENDENTE'};") + print(" e' quella la colonna da leggere, l'altra e' il bracket.") + print() + print(" 📌 LETTURA. Il maxDD scende in " + f"{nddneg}/{len(rows)}" + " celle, ma non e' protezione: e' il") + print(" NULL DEL DE-LEVERING nella sua forma piu' pura — si fanno meno trade, quindi si") + print(" rischia di meno. La prova sta nelle colonne `p-ind`/`p-com`: contro gate CASUALI") + print(" che saltano lo STESSO numero di settimane, la mediana e' " + + f"{med_pi:.2f}/{med_pc:.2f}" + " (a segnale") + print(f" nullo si attende 0.50) e {'nessuna cella arriva' if n_p95 == 0 else f'{n_p95} celle arrivano'} al 95° percentile in nessuno dei due null.") + print(" Cioe' **saltare quelle settimane li' non e' meglio che saltarne altrettante a") + print(" caso**. La mediana sopra 0.50 e' un accenno, non un'evidenza: con 9 celle") + print(" fortemente correlate fra loro (stesso segnale a 3 finestre e 3 soglie) la") + print(" mediana di famiglia non ha la potenza di una singola cella al 95°.") + print(f" ✅ Il dubbio sull'accoppiamento del null e' stato MISURATO immateriale: i due") + print(f" estremi danno {med_pi:.2f} e {med_pc:.2f} (bracket largo {abs(med_pi - med_pc):.2f}) e la sovrapposizione") + print(f" vera e' {med_ov:.2f}. Andava comunque misurato invece che assunto: nel 25/07 lo") + print(" stesso genere di indipendenza sbagliata valeva un fattore 2-3x sui breach.") + print(" ⚠️ Il massimo di 9 celle NON e' una scoperta: e' un massimo di 9 celle. La lettura") + print(" e' la FRAZIONE che batte il canonico, che a segnale nullo vale ~1/2 — e infatti") + print(f" e' {npos}/{len(rows)}.") + print(" ⇒ 5° fallimento consecutivo di un gate nuovo su VRP01 dopo i 4 del 03/07: l'alpha") + print(" di quel sleeve resta il binario IV-rank>0.30, e nient'altro.") + return dict(ok=True, sh0=sh0, dd0=dd0, rows=rows, npos=npos, n=len(rows), + med_pctl=med_p, med_pi=med_pi, med_pc=med_pc, n_p95=n_p95, n_ddneg=nddneg) + + +# ========================================================================================= +# §5 — NULL DEL DE-LEVERING, MDE, TURNOVER, ESEGUIBILITA' +# ========================================================================================= +def sezione_5_realismo(g3: dict) -> dict: + print() + print(SEP) + print(" §5 NULL DEL DE-LEVERING, MDE, TURNOVER, ESEGUIBILITA' A $635") + print(SEP) + ch, fn, daily = g3["chosen"], g3["fn"], g3["daily"] + B = al.tp01_baseline_daily() + J = pd.concat({"B": B, "C": daily}, axis=1, join="inner").dropna() + act = J[J["C"] != 0.0] + + # ---- NULL DEL DE-LEVERING ------------------------------------------------------------ + print(" (a) NULL DEL DE-LEVERING — 7 occorrenze nel progetto (VRP-DD, TP01xDVOL, MAT01,") + print(" azioni intere UCITS, SOL, protezione-dal-crash, ...). E' il PRIMO test da fare") + print(" su ogni claim 'meno drawdown'.") + print(" ⚠️ Nella sua forma ingenua qui e' DEGENERE: RISKOFF e SIZE sono ri-scalature") + print(" TEMPORALI di TP01, e lo Sharpe e' invariante alla scala -> `sh(k*TP01)` non") + print(" distingue nulla (regola (d) dell'ondata 22/08). La forma non degenere e':") + print(" **il gate ha aggiunto TIMING, cioe' Sharpe, o solo tolto esposizione?**") + w_ = J[J.index >= J[J["C"] != 0].index.min()] # finestra in cui il candidato esiste + shB, shC = sh(w_["B"]), sh(w_["C"]) + ddB, ddC = maxdd(w_["B"]), maxdd(w_["C"]) + k = ddC / ddB if ddB > 0 else float("nan") + print(f" finestra comune attiva: {w_.index.min().date()} -> {w_.index.max().date()} ({len(w_)} g)") + print(f" TP01 nudo Sharpe {shB:+.3f} maxDD {ddB:.2%}") + print(f" candidato Sharpe {shC:+.3f} maxDD {ddC:.2%}") + print(f" TP01 a leva k={k:.3f} (stesso maxDD del candidato): Sharpe {shB:+.3f} (invariante)") + n_act_w = int((w_["C"] != 0).sum()) + se_w = math.sqrt(DPY / max(1, n_act_w)) + mde_w = (norm.ppf(0.95) + norm.ppf(0.80)) * se_w + d_sh = shC - shB + if shC <= shB: + verdict_dl = "REFUTED (e' de-levering)" + elif abs(d_sh) < mde_w: + verdict_dl = f"NON DECIDIBILE (ΔSh {d_sh:+.3f} contro MDE {mde_w:.2f} = {mde_w / max(1e-9, abs(d_sh)):.0f}x)" + else: + verdict_dl = f"superato (ΔSh {d_sh:+.3f}, sopra l'MDE {mde_w:.2f})" + print(f" => NULL DEL DE-LEVERING: {verdict_dl}") + print() + print(" ⚠️ IL SEGNO DA SOLO NON E' UNA RISPOSTA. Il candidato batte TP01 di" + f" {d_sh:+.3f} di") + print(f" Sharpe su {n_act_w} barre attive, dove il minimo effetto rilevabile all'80%") + print(f" di potenza e' {mde_w:.2f} — cioe' **{mde_w / max(1e-9, abs(d_sh)):.0f} volte piu' grande del vantaggio misurato**.") + print(" Scrivere 'ha aggiunto timing' senza questo confronto sarebbe leggere il") + print(" segno di un numero che non ha potenza per avere un segno. Il maxDD piu'") + print(" basso (8.9% contro 12.3%) e' invece esattamente cio' che si ottiene") + print(" togliendo esposizione: k=" f"{k:.3f}" " di leva costante da' lo STESSO maxDD") + print(" allo STESSO Sharpe, senza bisogno di alcuna vol-of-vol.") + + # ---- diagnostica del segno opposto --------------------------------------------------- + print() + print(" (b) DIAGNOSTICA DEL SEGNO OPPOSTO (non e' una cella: e' il controllo di") + print(" bilanciamento del 22/08 §8c). Se il verso contrario funzionasse MEGLIO, la") + print(" variabile sarebbe rumore con un segno appiccicato sopra.") + inv = {"RISKOFF": "flat quando CALMA", "SIZE": "meta' size quando CALMA", "DIR": "short quando CALMA"} + + def _inv(df, asset): + vov, q = vov_and_pctl(df, asset, ch["params"]["w"]) + calm = np.isfinite(q) & (q <= ch["params"]["p"]) + known = np.isfinite(q) + u = ch["params"]["use"] + if u == "RISKOFF": + return np.where(known & ~calm, tp01_target(df, asset), 0.0) + if u == "SIZE": + return np.where(known, tp01_target(df, asset) * np.where(calm, 0.5, 1.0), 0.0) + d = np.where(known, np.where(calm, -1.0, 1.0), 0.0) + return al.vol_target(d, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0) + + dinv = al.candidate_daily(_inv, tf=TF) + Ji = pd.concat({"B": B, "C": dinv}, axis=1, join="inner").dropna() + wi = Ji[Ji.index >= Ji[Ji["C"] != 0].index.min()] + print(f" a-priori ({ch['params']['use']}, {inv[ch['params']['use']].replace('CALMA','INSTABILE')}):" + f" Sharpe {shC:+.3f}") + print(f" opposto ({inv[ch['params']['use']]}): Sharpe {sh(wi['C']):+.3f}") + print(f" => il verso dichiarato a priori {'REGGE' if shC >= sh(wi['C']) else 'e BATTUTO dal suo opposto (firma di rumore)'}") + + # ---- MDE su barre ATTIVE ------------------------------------------------------------- + print() + print(" (c) FINESTRA IN BARRE ATTIVE e MDE (minimo effetto rilevabile).") + H = J[J.index >= HOLDOUT] + Ha = H[H["C"] != 0.0] + n_act_full, n_act_hold = int((J["C"] != 0).sum()), int(len(Ha)) + print(f" DVOL parte 2021-03: prima il candidato e' FLAT per costruzione.") + print(f" barre di calendario: FULL {len(J)} HOLD-OUT {len(H)}") + print(f" barre ATTIVE : FULL {n_act_full} HOLD-OUT {n_act_hold}" + f" ({n_act_hold / max(1, len(H)):.0%} del calendario di hold-out)") + for lbl, n_a in (("FULL", n_act_full), ("HOLD-OUT", n_act_hold)): + if n_a < 10: + print(f" {lbl}: campione insufficiente") + continue + se = math.sqrt(DPY / n_a) + mde = (norm.ppf(0.95) + norm.ppf(0.80)) * se + print(f" {lbl:<9} anni attivi {n_a / DPY:5.2f} SE(Sharpe) ~ {se:.2f} " + f"MDE(80% potenza, 5% un lato) = {mde:.2f}") + print(" ⇒ Sull'hold-out il minimo effetto rilevabile e' dell'ordine di UNO-DUE punti di") + print(" Sharpe: un candidato a 0.3-0.8 non e' MISURABILE li', ne' in positivo ne' in") + print(" negativo. Va detto, perche' rende non-informativo qualunque numero di hold-out") + print(" si fosse voluto citare come conferma.") + + # ---- turnover + eseguibilita' -------------------------------------------------------- + print() + print(" (d) TURNOVER ed ESEGUIBILITA' a $635 (min_order $5), per gamba.") + print(f" {'asset':<6} {'tempo a mkt':>12} {'turnover/a':>11} {'Sh modellato':>13} " + f"{'Sh reale':>9} {'haircut':>8} {'trade eseguiti':>15}") + for a in ASSETS: + df = al.get(a, TF) + tgt = fn(df, a) + ev = al.eval_weights(df, tgt) + sc = al.eval_weights_smallcap(df, tgt, capital=CAPITAL, min_order=MIN_ORDER) + print(f" {a:<6} {ev['time_in_market']:>12.3f} {ev['turnover_per_year']:>11.1f} " + f"{sc['modeled']['sharpe']:>13.3f} {sc['realistic']['sharpe']:>9.3f} " + f"{sc['sharpe_haircut']:>+8.3f} {sc['n_executed_trades']:>15}") + print(" ⚠️ Un gate BINARIO su una variabile rumorosa oscilla e paga fee su nulla: il") + print(" turnover qui e' la spia, e va letto accanto al tempo a mercato.") + + # ---- guardia di causalita' ----------------------------------------------------------- + print() + print(" (e) GUARDIA DI CAUSALITA' (`altlib.causality_ok`): il bersaglio ricalcolato su un") + print(" prefisso troncato deve coincidere con quello a lunghezza piena.") + co = al.causality_ok(fn, tf=TF) + print(f" ok={co['ok']} max|diff| sulla coda = {co['max_tail_diff']} (prefissi controllati {co['checked']})") + return dict(shB=shB, shC=shC, ddB=ddB, ddC=ddC, dl=verdict_dl, + n_act_hold=n_act_hold, causal=co["ok"], inv_sh=sh(wi["C"])) + + +# ========================================================================================= +# §6 — CONTROLLI POSITIVI DELL'IMPIANTO +# ========================================================================================= +def sezione_6_controlli(g3: dict) -> dict: + print() + print(SEP) + print(" §6 CONTROLLI POSITIVI DELL'IMPIANTO — un 'non ho trovato niente' vale solo se il") + print(" macchinario e' capace di trovare qualcosa.") + print(SEP) + B = al.tp01_baseline_daily() + + # (i) ORACOLO: gate che conosce il domani. Deve essere enorme. + def oracle(df, asset): + c = df["close"].values.astype(float) + fwd = np.zeros(len(c)) + fwd[:-1] = c[1:] / c[:-1] - 1.0 + return np.where(fwd > 0, tp01_target(df, asset), 0.0) + + do = al.candidate_daily(oracle, tf=TF) + print(f" (i) ORACOLO (flat quando domani scende — NON causale, di proposito):") + print(f" Sharpe {sh(do):+.3f} contro TP01 nudo {sh(B):+.3f}") + ok_i = sh(do) > sh(B) + 1.0 + print(f" => l'impianto RISKOFF sa esprimere un gate che aiuta: {'OK' if ok_i else '*** ROTTO ***'}") + + # (ii) VOL-OF-VOL NON CAUSALE: se nemmeno conoscendola in anticipo aiuta, il difetto e' + # nella VARIABILE, non nella stima. + print() + print(" (ii) LA STESSA CELLA CON UNA VOL-OF-VOL NON CAUSALE (finestra centrata: si conosce") + print(" in anticipo). E' il controllo che separa 'la stima e' rumorosa' da 'la") + print(" variabile non contiene niente'.") + ch = g3["chosen"]["params"] + dp = al.candidate_daily(make_target(tf=TF, peek=True, **ch), tf=TF) + Jc = pd.concat({"B": B, "C": g3["daily"], "P": dp}, axis=1, join="inner").dropna() + Jc = Jc[Jc.index >= Jc[Jc["C"] != 0].index.min()] + print(f" cella {ch}") + print(f" TP01 nudo Sharpe {sh(Jc['B']):+.3f}") + print(f" VoV causale Sharpe {sh(Jc['C']):+.3f}") + print(f" VoV CHE SBIRCIA Sharpe {sh(Jc['P']):+.3f}") + gain = sh(Jc["P"]) - sh(Jc["B"]) + print(f" guadagno del sbirciare sopra TP01: {gain:+.3f}") + if gain <= 0.10: + print(" 📌 **Nemmeno conoscendo la vol-of-vol in anticipo il gate aiuta.** Il") + print(" problema non e' che la si stima male: e' che la variabile non contiene") + print(" l'informazione che si stava cercando. Coerente con la parziale") + print(" NEGATIVA di §2, per una strada indipendente.") + else: + print(" ⚠️ Sbirciando il gate aiuterebbe: allora il limite e' nella STIMA causale,") + print(" non nella variabile. Da riportare come tale.") + return dict(oracle_ok=ok_i, peek_gain=gain) + + +# ========================================================================================= +def main() -> None: + print(SEP) + print(" §48 VOLVOL — la VOLATILITA' DEL DVOL: il quinto lato del filone DVOL") + print(" " + __doc__.strip().splitlines()[0]) + print(SEP) + print(" ATTESA A PRIORI (del coordinatore, dichiarata nel brief PRIMA di ogni misura):") + print(" «sara' un termometro contemporaneo o ridondante col trend»") + print(" Sono due affermazioni: questo script le separa e le giudica una per una.") + print() + + r1 = sezione_1_ortogonalita() + if not r1["passed"]: + print("\n GATE ORTOGONALITA' FALLITO -> filone chiuso qui, come dichiarato.") + return + r2 = sezione_2_leadlag() + g3 = sezione_3_griglia() + r4 = sezione_4_vrp() + r5 = sezione_5_realismo(g3) + r6 = sezione_6_controlli(g3) + + # --------------------------------------------------------------------------------- + print() + print(SEP) + print(" VERDETTO") + print(SEP) + rep = g3["rep"] + ch = g3["chosen"]["params"] + print(f" ortogonalita' (il gate) : PASS — max |corr| {r1['worst']:.3f} contro le 6 referenze") + print(f" lead-lag : picco a lag NEGATIVO; parziale su vol futura NEGATIVA") + print(f" griglia dichiarata : {N_CELLS_DECLARED} celle ({N_CELLS_DIRECTIONAL} direzionali + {N_CELLS_VRP} VRP)") + print(f" cella scelta in-sample-only : {ch}") + print(f" earns_slot_honest : {rep['earns_slot_honest']} " + f"(marginal {rep['marginal']['marginal_verdict']}, DSR {rep['deflated_sharpe']})") + print(f" null del de-levering : {r5['dl']}") + print(f" segno opposto (diagnostica) : a-priori {r5['shC']:+.3f} vs opposto {r5['inv_sh']:+.3f} -> il verso regge") + print(f" arm VRP01 : {r4.get('npos')}/{r4.get('n')} celle battono il canonico (moneta); " + f"maxDD giu' in {r4.get('n_ddneg')}/{r4.get('n')} = de-levering;") + print(f" vs gate CASUALE mediana {r4.get('med_pi'):.2f}/{r4.get('med_pc'):.2f}, " + f"{r4.get('n_p95')}/{r4.get('n')} celle al 95° pctl") + print(f" MDE (barre attive) : FULL 985 barre -> MDE 1.51 ; HOLD-OUT {r5['n_act_hold']} barre -> MDE 2.73") + print(f" causalita' : {'OK' if r5['causal'] else 'VIOLATA'}") + print(f" controlli positivi : oracolo {'OK' if r6['oracle_ok'] else 'ROTTO'}, " + f"guadagno dello sbirciare {r6['peek_gain']:+.3f}") + print() + print(" *** SCARTATO ***") + print() + print(" La vol-of-vol E' una variabile nuova — l'ortogonalita' lo dimostra e refuta meta'") + print(" dell'attesa a priori. Ma e' una variabile nuova che guarda INDIETRO: il picco della") + print(" cross-correlazione con l'ampiezza dei movimenti sta a lag negativo, e al netto della") + print(" vol realizzata e del livello dell'implicita — due cose che il progetto gia' ha — la") + print(" sua correlazione con la vol FUTURA e' negativa. Il controllo non causale chiude il") + print(" cerchio da una strada indipendente: nemmeno conoscendola in anticipo il gate aiuta.") + print() + print(" 📌 E LA LETTURA CHE VALE PIU' DEL VERDETTO: il candidato migliore della griglia") + print(" batte TP01 nudo di +0.034 di Sharpe su una finestra il cui minimo effetto") + print(" rilevabile e' 1.51 — un fattore 44. **Su questo dato, con questo orizzonte, la") + print(" domanda non e' rispondibile in positivo nemmeno in linea di principio**: non c'e'") + print(" campione per distinguere un overlay che vale mezzo punto di Sharpe da uno che non") + print(" vale niente. Cio' che chiude il filone non e' il numero della griglia (che non ha") + print(" potenza) ma le DUE misure che ce l'hanno: la parziale negativa di §2 e il") + print(" controllo non causale di §6, che non dipendono da nessuna cella scelta.") + print() + print(" CON QUESTO IL FILONE DVOL E' CHIUSO SU CINQUE LATI: livello (VRP01, tenuto),") + print(" differenza cross-asset (DVOLSPREAD, in monitor), denominatore del vol-target") + print(" (de-levering), direzionale (hedge), SECONDO MOMENTO (questo).") + print() + print(" COSA NON E' STATO MISURATO, e va detto:") + print(" - la vol-of-vol a frequenza INTRA-GIORNALIERA: il DVOL su disco e' giornaliero, e") + print(" un secondo momento e' proprio il posto dove la frequenza puo' contare. Non e'") + print(" un buco che si chiude con questi dati.") + print(" - la vol-of-vol come variabile di PREZZO delle opzioni (vol-of-vol implicita dalla") + print(" curvatura dello smile) invece che stimata sullo storico dell'indice: sarebbe una") + print(" variabile forward-looking e diversa da questa. Il censimento della superficie") + print(" (22/08 §8) ha pero' misurato che il residuo di smile E' la larghezza del mercato") + print(" guardata dal mid, quindi la strada e' stretta.") + print(" - il regime dopo il 2025: l'hold-out ha ~0.83 anni ATTIVI e un MDE di 2.73 punti di") + print(" Sharpe. Nessun numero di hold-out di questo filone e' informativo, in nessun verso") + print(" — e per questo qui non se ne cita nessuno come conferma.") + print(" - la vol-of-vol come ingresso di un modello MULTI-variabile (insieme a RV e livello)") + print(" invece che come gate binario: la parziale di §2 dice che l'incremento condizionato") + print(" e' negativo, quindi l'aspettativa e' che non cambi nulla, ma un modello congiunto") + print(" non e' stato provato e non si finge che lo sia.") + print(" - i TRE usi provati non esauriscono gli usi possibili: sono quelli dichiarati nel") + print(" brief. Un uso non provato non e' un uso refutato.") + print(SEP) + + +if __name__ == "__main__": + main() From 4e1dae5c09edbbce0e608d69264520b7593ba08b Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 01:48:56 +0000 Subject: [PATCH 077/114] =?UTF-8?q?registro=20=C2=A751=20e=20=C2=A748:=20c?= =?UTF-8?q?osa=20raccogliere=20(2=20su=2010,=20e=20il=20+90%=20era=20+18%)?= =?UTF-8?q?=20e=20il=20quinto=20lato=20del=20DVOL=20chiuso?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 125 +++++++++++++++++++++++++++++++++- 1 file changed, 123 insertions(+), 2 deletions(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index cff1dea..ef80f9b 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -713,10 +713,10 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 45 | SPOT-NETTING | separare TP01 e SKH01 su due strumenti: quanto vale, e il margine lo uccide? | **LEAD REGGE, SEPARAZIONE SENZA VALORE** — il margine non e' il vincolo (copertura **50x**, invariante al capitale), ma **T1 sbloccato e' un DECLASSAMENTO** (−0,048 contro il path che gira) e il netting perso **risparmia** commissioni: il lead vale **il funding e basta**, [+1,55%, +1,66%]/anno; a bloccarlo e' **il codice di sizing**, non Deribit | | 46 | TAIL-HEDGE | comprare put deep-OTM come assicurazione statica compra DD a prezzo migliore del de-levering? | _in corso_ | | 47 | ANCHOR-ENSEMBLE | eseguire l'ensemble delle 24 ancore invece di sceglierne una e correggere il numero | _in corso_ | -| 48 | VOLVOL | la volatilita' del DVOL (secondo momento, mai usato) contiene cio' che livello e spread non contengono? | _in corso_ | +| 48 | VOLVOL | la volatilita' del DVOL (secondo momento, mai usato) contiene cio' che livello e spread non contengono? | **SCARTATO** — e' davvero una variabile NUOVA (ortogonalita' PASS, max \|corr\| 0,396) ma **guarda INDIETRO**: picco a lag **−5/−10** e parziale con la vol futura **NEGATIVA**. Quinto lato del filone DVOL chiuso | | 49 | TP01-TWIN | il 75% del libro dipende da UNA definizione di trend: un ensemble di meccanismi protegge meglio? | _in corso_ | | 50 | BOOK-3RD | qual e' il miglior TERZO sleeve realmente eseguibile sul libro live a $635 — e a quale capitale cambia? | _in corso_ | -| 51 | NEWDATA-SCOUT | se la scorta e' esaurita: cosa iniziare a raccogliere OGGI, che fra 12-24 mesi apra una famiglia? | _in corso_ | +| 51 | NEWDATA-SCOUT | se la scorta e' esaurita: cosa iniziare a raccogliere OGGI, che fra 12-24 mesi apra una famiglia? | **RACCOGLIERE 2 FONTI SU 10** — catena USDC (**+18%, non +90%**) e una **campagna a termine** di book depth; il criterio di ricostruibilita' ne uccide 4 da solo e **refuta una raccomandazione del 22/08** | | 52 | SKEPTIC-SPOT | il lead dello spot (+1,55-1,66%/anno) sopravvive a un attacco deliberato? | _in corso_ | | 53 | SKEPTIC-LEVA | il gradino di leva 1,25x sopravvive a un attacco deliberato, col funding dentro? | _in corso_ | | — | **NOTIFIER** (audit di fatto) | 🚨 **PUNTO SINGOLO DI GUASTO nella rete di sicurezza** | il `send()` di Telegram e' **un solo tentativo da 10s, senza retry, con l'esito MAI registrato** — e lo stato `alerted=True` viene scritto **prima** dell'invio: un 🚨 di `venue_watch` perso e' perso **per l'intero episodio**. Tasso di fallimento **misurato 6,9%** (2/29, IC95 [1,9%, 22,0%]) | @@ -2603,3 +2603,124 @@ e' il funding e basta**, e in cambio si prendono due costi nuovi mai contati (co separati, gamba TP01 senza disaster-SL) piu' un cambio al percorso di sizing del live. 📌 **Il prossimo passo non e' un backtest: e' la domanda al commercialista e un acquisto di prova da $7,73.** **Libro, pesi, cron, config INVARIATI.** + +--- + +## 51 — NEWDATA-SCOUT (cosa iniziare a raccogliere) + +`scripts/research/r0823_newdata_scout.py`. Conseguenza diretta di §43: se la scorta e' esaurita, la +domanda utile non e' piu' *"cosa abbiamo e non guardiamo"* ma *"cosa NON abbiamo, che varrebbe la +pena iniziare a raccogliere oggi"* — perche' **una catena opzioni non e' ricostruibile a posteriori** +e un'ora non raccolta e' persa per sempre. + +| fonte | apre | ricostruibile? | costo | tempo utile | verdetto | +|---|---|---|---|---|---| +| **catena opzioni USDC-lineare** | nessuna famiglia nuova: **rende onesti i numeri di uno sleeve VIVO** (VRP01, 12%) | **NO** | **+117 chiam./giro = +18%** | episodi (1 regime IV-rank>0,30) | **RACCOGLIERE** | +| **book depth L2 perpetual** | la curva haircut-vs-taglia (oggi **18 fill**) | **NO** | 2.880/g a 1/min×2 | giorni per la mediana, anni per la coda | **CAMPAGNA A TERMINE** | +| trade tape / flusso aggressore | flusso d'ordine (mai aperto) | **NO** — muro **−24h/−26h** | ~6/ora | **6,0 anni** | NON RACCOGLIERE | +| liquidazioni Deribit | coda/stress | NO (vive nel tape) | — | non calcolabile | NON RACCOGLIERE | +| OI per strike nel tempo | — | irrilevante | zero | in corso | **GIA' FATTO** | +| **OI perpetual** (il collettore raccomandato da §10) | affollamento — famiglia **CHIUSA 4 lati** | 🚨 **SI', >=800 giorni** | 1/ora | testabile **oggi** | **NON RACCOGLIERE — TESTARE PRIMA** | +| telemetria Binance (taker, long/short) | posizionamento retail | **NO** — muro 30g | ~24/g | 6,0 anni | NON RACCOGLIERE | +| funding/basis altri venue · DVOL · macro | famiglie chiuse / gia' posseduto | SI' | — | — | NON RACCOGLIERE | + +📌 **Il criterio 1 (ricostruibilita') ne uccide 4 da solo, ed e' stato applicato PROVANDO +l'endpoint, non deducendolo** — ogni verdetto e' una GET con **controllo positivo**, perche' *uno +zero non e' una risposta finche' non si prova che la stessa chiamata funziona altrove*: +tape Deribit **−24h → 100 trade, −26h → 0** (muro verificato coi timestamp restituiti); Binance +**−20g risponde, −40g rifiuta esplicitamente** (`parameter 'startTime' is invalid`); funding e DVOL +serviti storici su richiesta. +🚨 **E ribalta una raccomandazione del 22/08:** §10 proponeva di accendere un collettore di OI +perpetual *"perche' oggi ripartirebbe da zero"*. **Non riparte da zero:** Bybit risponde a −800 +giorni coi timestamp dentro la finestra (2024-06-14). **L'ipotesi e' testabile oggi a costo zero**, e +solo se sopravvive vale pagare la specificita' di venue. + +🚨 **Correzione a un numero che avevo pubblicato io (+90%).** Con **gli stessi filtri del +collettore vivo** (≤95 giorni, OI≥100) la catena USDC BTC+ETH e' di **113 strumenti**, non ~587 → +**+18%, non +90%**. ✅ **Verificato indipendentemente dal coordinatore sul venue**: inverse +(BTC 270 + ETH 379) = **649**, USDC = **113** → **+17,4%**. L'origine del 587 era il conteggio +**grezzo** senza il filtro OI (1.140 strumenti USDC ≤95g, cioe' +81% — vicino al 90% pubblicato): +**il filtro che il collettore applica davvero taglia il 90% della famiglia USDC**, ed e' la replica +esatta di §8 (*OI e negoziabilita' sono anti-correlati su Deribit USDC*; BTC_USDC ha **5** +strumenti sopra soglia). Il giro passerebbe da **652 chiamate / 163 s** a **769 / 192 s**, che +finisce prima del :30 e non tocca `cron_book` al :07. ⚠️ E' un numero **di oggi**: cresce con la +liquidita' USDC e va ri-misurato prima di accendere. + +**Tempo perche' sia utile, dal MDE** (`≈ 1,96/√anni`): Sharpe 0,8 → **6,0 anni**; 1,0 → 3,8; 1,3 → +2,3. **Il soffitto direzionale del progetto e' ~1,15-1,3 ⇒ qualunque fonte che alimenti un SEGNALE +non e' una decisione di oggi.** 📌 **Le due sopravvissute sfuggono al MDE perche' non producono uno +Sharpe: sono misure di COSTO (depth) e di PREZZO REALE (catena USDC).** + +⚠️ **Onesta' dell'agente su cio' che le due fonti NON fanno:** la catena USDC **non apre una famiglia +chiusa** — vale perche' il `f=0,73` di VRP01 e' misurato sulla famiglia **inverse, che un conto USDC +non puo' marginare**: ripara il numero di uno sleeve vivo, non apre alpha. Il depth: la domanda +binding (*quanto costa eseguire a $5k/$20k*) si risponde in **due settimane**; solo la coda di stress +richiede l'archivio — da cui **"campagna", non "collettore"**. + +⚠️ **Bug catturato in sessione:** `urlopen` **solleva** su HTTP 400, quindi il rifiuto di Binance +veniva ingoiato come guasto di rete e il muro **si ribaltava in silenzio in "RICOSTRUIBILE"** — la +stessa conflazione *errore / assenza-di-dato* gia' codificata in `collect_chain` con `quote_status`. + +**VERDETTO: `RACCOGLIERE DUE FONTI SU DIECI, E NESSUNA DELLE DUE PER FARNE UN SEGNALE`** — catena +USDC (+18%) e una campagna a termine di book depth. **Nessuna azione presa: e' materiale per una +decisione dell'operatore.** + +--- + +## 48 — VOLVOL (il secondo momento del DVOL) + +`scripts/research/r0823_volvol.py` (**20 s**, nessuna rete, nessuna scrittura). **36 celle** +dichiarate prima (3 finestre × 3 soglie × 4 usi), tenute poche di proposito data l'aritmetica dei +168 trial. + +✅ **L'ortogonalita' — che era il gate — la PASSA**, e questa e' meta' dell'attesa a priori +**refutata**: misurata prima di costruire qualsiasi strategia, Pearson **e** rango, 6 celle × 6 +referenze, **max |corr| = 0,396** (soglia 0,80): contro il livello DVOL 0,13-0,21, contro l'IV-rank +di VRP01 0,01-0,24, contro RV30 0,09-0,38, contro |esposizione TP01| 0,03-0,30. **E' una quinta +variabile, non una quarta riscritta.** + +⚠️ **Correzione a una mia riga del briefing, fatta leggendo il codice in produzione:** il +**DVOLSPREAD in forward-monitor NON e' implicita−realizzata** — `r0726_dvolspread_gate.make_book` +calcola **`log(dvol_btc) − log(dvol_eth)`**, un relative-value **cross-asset** (✅ verificato dal +coordinatore al sorgente, riga 67). L'agente ha misurato contro **entrambe**: *correlare contro +quella sbagliata avrebbe dato un "ortogonale" gratuito contro una variabile che nessuno trada.* + +📌 **DOVE MUORE: guarda INDIETRO.** `corr(VoV_t, |r|_{t+k})` su BTC: **−10: +0,164 · −5: +0,169 +· −1: +0,132 · 0: +0,113 · +1: +0,057 · +10: +0,018** — picco a lag **−5/−10**, curva monotona +decrescente. **Non e' il termometro contemporaneo** di TERM-STRUCTURE e SKEW: **insegue con una +settimana di ritardo**. Correlazione **parziale** con la vol realizzata futura a 10g, al netto di +`RV_t` e `DVOL_t`: **−0,069 (BTC) / −0,090 (ETH)**. ✅ Controllo positivo del rilevatore **2/2 nei +due versi** (lead sintetico → +3, lag sintetico → −3). + +**Gate:** cella in-sample-only SIZE w=10 p=0,50, marginal **NEUTRAL**, **DSR 0,448 FAIL** +(0,441/0,381/0,228 a N=36/72/168) → `earns_slot_honest=False`. **La trappola del trend in chiaro:** +la cella scelta ha **corr→TP01 0,995 sull'hold-out**; e per uso, RISKOFF/SIZE ereditano lo Sharpe +del trend (mediana IS 0,40/0,66) mentre **DIR — l'unico uso in cui la variabile decide da sola — ha +mediana IS −0,24 e 5/9 celle con FULL negativo**. +**Arm VRP01** (replica del sleeve **bit-exact 2/2** prima di ogni delta): 5/9 battono il canonico = +moneta, maxDD giu' in **9/9 = de-levering**, e contro gate **casuali** che saltano lo stesso numero +di settimane la mediana e' 0,71/0,72 con **0/9 al 95° pctl** → **quinto fallimento consecutivo di un +gate nuovo su VRP01**. + +📌 **IL NUMERO CHE DECIDE, e non e' un gate.** Barre **attive**: FULL 985 → **MDE 1,51**; +hold-out 303 → **MDE 2,73**. Il candidato migliore batte TP01 nudo di **+0,034 di Sharpe — un +fattore 44 sotto l'MDE**: *su questo dato la domanda non e' rispondibile in positivo nemmeno in +linea di principio*. A chiudere sono le due misure che **hanno** potenza e **non dipendono da nessuna +cella scelta**: la parziale negativa, e il **controllo NON CAUSALE** — la stessa cella con la +vol-of-vol che **sbircia** fa **−0,292 sotto TP01**. *Non e' che la si stima male: la variabile non +contiene l'informazione.* + +⚠️ **Errori dell'agente su se stesso:** (i) la lettura dell'ortogonalita' era **cablata** e diceva +*"il legame piu' forte e' con la vol realizzata"* mentre la tabella diceva SPREAD → ora e' calcolata +dalla tabella; (ii) il null del gate casuale estraeva le settimane **indipendentemente per gamba** +mentre il gate vero e' guidato da due DVOL correlati — differenza risultata **immateriale** (0,71 +contro 0,72) *ma andava misurata, non assunta* (nel 25/07 lo stesso errore valeva 2-3×); +(iii) un off-by-one nel conteggio. + +**Non misurato, dichiarato:** vol-of-vol **intra-giornaliera** (il DVOL su disco e' 1d, e un secondo +momento e' proprio dove la frequenza puo' contare); vol-of-vol **implicita dalla curvatura dello +smile**; un modello **congiunto** RV+livello+VoV invece di un gate binario. *"Un uso non provato non +e' un uso refutato."* + +**VERDETTO: `E' UNA VARIABILE NUOVA E GUARDA INDIETRO`** — SCARTATO, **quinto lato del filone DVOL +chiuso**. **Libro, pesi, cron, config INVARIATI.** From 7d4f0c328ec44c1d3ce20363e7c77c7a24cf2b0e Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 01:49:29 +0000 Subject: [PATCH 078/114] =?UTF-8?q?research(=C2=A749=20TP01-TWIN):=20un=20?= =?UTF-8?q?ENSEMBLE=20di=205=20meccanismi=20di=20trend=20NON=20protegge=20?= =?UTF-8?q?piu'=20di=20TP01=20=E2=80=94=20REFUTED?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Domanda: TP01 e' il 75% del libro live e dipende da UNA definizione di trend (TSMOM 30/90/180). Sostituirlo con un ensemble di meccanismi diversi ma tutti long-flat (Donchian, incrocio EWMA, canale di Keltner, Kaufman/KAMA) migliora la PROTEZIONE — che e' il suo compito — a pari drift e a pari costo? NON e' la domanda `marginal_vs_tp01` (secondo sleeve): e' la STESSA quota di libro espressa da piu' meccanismi, quindi `weights_tilt_null` non si applica (il vettore dei pesi e' identico nei due bracci). Esito: REFUTED. Libro, pesi, cron, config INVARIATI. Entrambe le attese a priori, scritte prima di misurare, sono REFUTATE: (A1) "correlano 0,85-0,95, l'ensemble e' TP01 con piu' fee" -> 0,795 sui rendimenti e 0,608 sulle POSIZIONI; KEL/KAU stanno a 0,51-0,53 da TSMOM. Meccanismi genuinamente diversi: il filone non si chiude sulla matrice, va misurato fino in fondo. (A2) "meno DD sara' de-levering" -> k_isovol 0,990: NON C'E' NIENTE DA DE-LEVERE. Vol identica (12,1 vs 12,0%), esposizione media PIU' ALTA (0,152 vs 0,141), tempo a mercato 70% vs 55%. Il null non e' "superato", e' SENZA POTENZA, e va detto cosi'. Muore invece sulla DISTRIBUZIONE della protezione (criterio (B) di edge_watch importato dal sorgente di produzione) e sul costo al libro: - per anno x 24 ancore: l'ensemble protegge PEGGIO in 2022, 2024, 2025 e 2026 a 0/24 ancore (degradazione UNANIME, non fortuna d'ancora) e meglio in 2019-2021 e 2023. I quattro anni in cui perde sono i quattro PIU' RECENTI. Nel 2022 (DD buy&hold 68%, il sinistro maggiore) il rapporto passa 0,04 -> 0,13 = 3,2x peggio. - 73/192 celle ancora-x-anno a favore dell'ensemble (TSMOM meglio nel 62%); rapporto MEDIO 0,18 -> 0,21 (peggiora, 0/24 ancore favorevoli). - costo sul LIBRO 75/25: Sharpe hold-out delta appaiato -0,127, favorevole 3/24. - 0/30 delle 31 composizioni possibili batte TSMOM da solo su protezione E hold-out insieme: il compromesso non e' di questo ensemble, e' della famiglia. - e il criterio che si voleva migliorare NON E' BINDING: TSMOM ha rapporto peggiore 0,635 contro la soglia 0,75 in 192 celle su 192. ERRORE MIO catturato in sessione e riportato nello script: la prima stesura decideva sul CASO PEGGIORE (max degli 8 rapporti), che migliora 0,56 -> 0,49, e avrebbe stampato "PROMOSSO A LEAD". Il massimo di 8 numeri non e' una statistica: si era mosso perche' era migliorato UN anno (il 2023, che deteneva il massimo) mentre 4 su 8 peggioravano. Secondo errore corretto: il tempo a mercato era calcolato sulla serie 50/50 (non-zero se lo e' UNO dei due asset) e dava 66,8/79,4% contro i 55,4/70,3% di eval_weights nella stessa pagina. Onesta' verso l'ensemble: l'ancora canonica h=0 e' la MIGLIORE delle 24 per lo Sharpe hold-out di TSMOM (100 pctl, replica indipendente della fortuna d'ancora del 02/07 e 26/07), quindi il divario a h=0 (-0,37) e' gonfiato: il numero da citare e' la mediana appaiata, -0,067. Fatto trasferibile: mediare meccanismi di trend NON diversifica il rischio di trend. Cinque segnali guidati dallo stesso prezzo divergono solo ai BORDI del trend — cioe' nei ribassi — e la media li tiene mezzi-lunghi mentre il singolo e' gia' flat. Piu' meccanismi = ingresso e uscita piu' morbidi, non piu' assicurazione. Cio' che NON si conclude: che la monocultura sia sicura — servirebbe un regime in cui il TSMOM fallisce, e in 7,4 anni non c'e'. Controlli: replica bit-exact vs sleeves._tp01_returns (max|diff| = 0,0); criterio (B) 8/8 su TP01 come pubblicato e 0/8 su buy&hold; stimatore di correlazione validato su corr(TP01,SKH01) = +0,095 contro +0,09 pubblicato; il null del de-levering DEVE scattare e scatta su un de-levering vero (target_vol 10%); causality_ok max_tail_diff 0,0 su 5/5 + ensemble. DSR di famiglia 0,999 PASS ma dichiarato NON decisivo (famiglia omogenea -> sr0 piccolo per costruzione); trial contati al rialzo 22 + 31 composizioni = 53. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf --- scripts/research/r0823_tp01_twin.py | 902 ++++++++++++++++++++++++++++ 1 file changed, 902 insertions(+) create mode 100644 scripts/research/r0823_tp01_twin.py diff --git a/scripts/research/r0823_tp01_twin.py b/scripts/research/r0823_tp01_twin.py new file mode 100644 index 0000000..3969b51 --- /dev/null +++ b/scripts/research/r0823_tp01_twin.py @@ -0,0 +1,902 @@ +#!/usr/bin/env python +"""r0823_tp01_twin.py — §49 TP01-TWIN: TP01 e' UN SOLO meccanismo sul 75% del libro. + +LA DOMANDA (decidibile, non retorica) +===================================== +TP01 e' il 75% del libro live. Il progetto ne ha misurato il valore con precisione: NON genera +ritorno (contributo hold-out negativo nel 99,1% delle ancore, LOO 26/07) ma TAGLIA IL DRAWDOWN +~6x contro il buy&hold, e supera il criterio di protezione di `edge_watch` 8/8 anni di sinistro. +E' un'assicurazione e si giudica sul sinistro. + +Ma la sua direzione e' UNA definizione di trend: media di tre `sign(close/close[-h]-1)` a +30/90/180 giorni. Tutto il 75% del libro dipende da quella. + + Sostituire TP01 con un ENSEMBLE di meccanismi di trend DIVERSI ma tutti long-flat migliora + la PROTEZIONE (che e' il suo compito), a pari drift e a pari costo? + +NB la formulazione: NON "un secondo sleeve che si aggiunge" (quella e' la domanda +`marginal_vs_tp01`, gia' fatta decine di volte e sempre REDUNDANT per i trend), ma LA STESSA +QUOTA DI LIBRO espressa da piu' meccanismi. E' robustezza, non alpha. Percio' `weights_tilt_null` +NON si applica: il peso 75/25 non si muove, cambia solo cosa c'e' dentro il 75%. + +ATTESA A PRIORI — SCRITTA PRIMA DI MISURARE (riportata anche se refutata; lo e') +=============================================================================== +(A1) "I meccanismi di trend a 1d sullo stesso asset correlano 0,85-0,95, quindi l'ensemble sara' + TP01 con piu' fee." Corollario: la matrice di correlazione (§2) puo' chiudere il filone da + sola. -> REFUTATA: corr media 0,795 sui rendimenti e 0,608 sulle POSIZIONI; KEL e KAU + stanno a 0,51-0,53 di posizione da TSMOM. Non sono lo stesso oggetto. +(A2) "Se l'ensemble avra' meno DD sara' per DE-LEVERING (7a occorrenza del null)." + -> REFUTATA: la vol realizzata e' IDENTICA (12,1% vs 12,0%), l'esposizione media e il tempo + a mercato dell'ensemble sono PIU' ALTI. Non c'e' niente da de-levere: il null non e' + applicabile, e va detto cosi' invece di dire "non refuta" (che suggerirebbe che l'ensemble + abbia superato un test che in realta' non aveva potenza). + +MECCANISMI — DICHIARATI TUTTI PRIMA, E CONTATI COME TRIAL (§8) +============================================================== +Tutti LONG-FLAT, tutti vol-targeted 20% (cap 2x, finestra 30g), tutti a 1d, tutti sullo stesso +50/50 BTC+ETH, tutti causali (decidono a close[i], tengono in i+1), tutti a fee 0,10% RT. + TSMOM — il riferimento: media dei sign(c/c[-h]-1), h in 30/90/180 (= TP01 CANONICAL) + DON — Donchian breakout long-flat: entra sopra il massimo dei precedenti N, esce sotto il + minimo dei precedenti N/2. NB: SKH01 e' un Donchian ma L/S e dual-TF 230m/690m — + NON e' lo stesso oggetto e qui non viene ricreato. + EWMA — incrocio di medie esponenziali, blend di 3 coppie (stessa forma multi-orizzonte di TSMOM) + KEL — canale di Keltner: entra sopra ema+k*atr, esce sotto la ema (mid) + KAU — Kaufman: KAMA (efficiency-ratio adattiva), lunga se la KAMA sale su `slope` barre +Indicatori presi da `src/strategies/indicators.py` (ema, atr) e da `altlib` (donchian, vol_target, +eval_weights, eval_weights_smallcap, causality_ok, study_family_honest): importati, non riscritti. + +ENSEMBLE (due varianti, dichiarate a priori) + ENS-DIR — media delle 5 DIREZIONI, poi UN solo vol-target. E' esattamente la struttura di TP01 + (che gia' media 3 direzioni prima di scalare): "stessa quota, piu' meccanismi". + ENS-POS — media delle 5 POSIZIONI gia' vol-targeted (differisce solo dove morde il cap 2x). +Il costo si conta sulla posizione NETTATA (il libro ha UNA posizione per strumento su Deribit): +la fee e' su |delta(posizione media)|. Si riporta anche il caso non-nettato come limite superiore. + +COSA DECIDE (in ordine) + §2 correlazione fra meccanismi + §4 PROTEZIONE anno per anno, criterio (B) di `scripts/live/edge_watch.py` importato dal + sorgente di produzione: negli anni con DD buy&hold > 10%, il DD dello sleeve deve restare + sotto il 75% di quello. + ⚠️ ERRORE MIO, CATTURATO IN SESSIONE E RIPORTATO: la prima stesura decideva sul CASO + PEGGIORE (il massimo degli 8 rapporti), che migliora 0,56 -> 0,49, e avrebbe stampato + "ENSEMBLE PROMOSSO A LEAD". Ma il massimo di 8 numeri non e' una statistica: si e' mosso + perche' e' migliorato UN anno (2023), mentre 4 anni su 8 PEGGIORANO — compreso il 2022, che + e' l'unico sinistro vero del campione (0,04 -> 0,13, tre volte peggio). §4-bis misura la + DISTRIBUZIONE (test dei segni per anno, e 24x8 celle appaiate sulle ancore) invece del massimo. + §5 GATE ISO-VOLATILITA' + NULL DEL DE-LEVERING (7a occorrenza). NB: lo Sharpe e' invariante + alla scala, quindi il null "iso-DD, confronta lo Sharpe" e' DEGENERE (regola dell'ondata + 22/08): la forma non degenere e' iso-VOL sulla PROTEZIONE + confronto diretto di Sharpe. + §5-bis LA COMPOSIZIONE DELL'ENSEMBLE E' ESSA STESSA UN PARAMETRO LIBERO: 31 sottoinsiemi non + vuoti, di cui ne e' stato dichiarato UNO. Si misura dove cade il dichiarato nella + distribuzione dei 31 — se sta in alto, un ensemble "migliore" e' selezione, non robustezza. + §6 banda d'ancora 24 ore, MEDIANA DELLE DIFFERENZE APPAIATE (mai la differenza delle mediane) + §7 costi: curva fee 0-20 bps RT + `eval_weights_smallcap` a $635 + §8 deflated-Sharpe di FAMIGLIA (`study_family_honest`) — con l'avvertenza che su una famiglia + OMOGENEA sr0 e' piccolo per costruzione e il DSR passa facile: NON e' il gate che decide qui + §9 impatto sul libro 75/25 con SKH01, ANCHE in banda d'ancora appaiata (la gamba SKH01 e' + identica nei due bracci, quindi la sua fortuna d'ancora si CANCELLA nella differenza) + +VINCOLI: script di sola lettura. Non tocca src/, config/, scripts/live/, tests/, data/. +Runtime ~1 min su 2 CPU. +""" +from __future__ import annotations + +import importlib.util +import itertools +import sys +import time +import warnings +from functools import lru_cache +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +sys.path.insert(0, str(ROOT)) + +import altlib as al # noqa: E402 +from src.data.downloader import load_data # noqa: E402 +from src.portfolio.portfolio import DAYS_PER_YEAR, combine_outer, to_daily # noqa: E402 +from src.portfolio.sleeves import _tp01_returns, skyhook_sleeve # noqa: E402 +from src.strategies.indicators import atr as ind_atr # noqa: E402 +from src.strategies.indicators import ema as ind_ema # noqa: E402 +from src.strategies.trend_portfolio import (CANONICAL, resample_tf, # noqa: E402 + simple_returns, tsmom_blend) + +warnings.filterwarnings("ignore", category=RuntimeWarning) + +ASSETS = ("BTC", "ETH") +FEE_SIDE = CANONICAL["fee_side"] # 0.0005 = 0.10% RT +TARGET_VOL = CANONICAL["target_vol"] # 0.20 +LEV_CAP = CANONICAL["leverage"] # 2.0 +VOL_WIN = CANONICAL["vol_win_days"] # 30 +HOLDOUT = al.HOLDOUT +ANCHORS = tuple(range(24)) +CAPITALE = 635.0 # conto reale dichiarato nel progetto + +# --- criterio (B) importato DAL SORGENTE DI PRODUZIONE (non riscritto) ------ +_spec = importlib.util.spec_from_file_location("_edge_watch", ROOT / "scripts" / "live" / "edge_watch.py") +_ew = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_ew) +SINISTRO_DD, PROTECT_MAX = _ew.SINISTRO_DD, _ew.PROTECT_MAX + + +# =========================================================================== +# 0. DATI E MECCANISMI +# =========================================================================== +@lru_cache(maxsize=8) +def raw1h(asset: str) -> pd.DataFrame: + return load_data(asset, "1h") + + +def resample_offset(df_1h: pd.DataFrame, h: int) -> pd.DataFrame: + """Barre giornaliere ancorate alle h:00 UTC. h=0 usa direttamente resample_1d + (stessa convenzione label/closed='left' di trend_portfolio.resample_tf).""" + if h == 0: + return resample_tf(df_1h, "1D") + g = df_1h.copy() + idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True) + idx.name = "dt" + g.index = idx + out = g.resample("24h", offset=pd.Timedelta(hours=h), label="left", closed="left").agg( + {"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}) + out = out.dropna(subset=["open"]) + out["datetime"] = out.index + epoch = pd.Timestamp("1970-01-01", tz="UTC") + out["timestamp"] = ((out.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64") + return out.reset_index(drop=True)[["timestamp", "open", "high", "low", "close", "volume", "datetime"]] + + +@lru_cache(maxsize=64) +def bars(asset: str, h: int) -> pd.DataFrame: + return resample_offset(raw1h(asset), h) + + +# --- direzioni: array in [0,1], NaN durante il proprio warm-up ------------- +def dir_tsmom(df, horizons=(30, 90, 180)): + c = df["close"].values.astype(float) + d = np.clip(tsmom_blend(c, tuple(horizons)), 0.0, None) # LONG-FLAT + d[:min(horizons)] = np.nan # nan_to_num -> 0 = esattamente tsmom_blend + return d + + +def dir_don(df, win=50): + ex = max(2, win // 2) + hi, _ = al.donchian(df, win) + _, lo = al.donchian(df, ex) + c = df["close"].values.astype(float) + out = np.full(len(c), np.nan) + st = 0.0 + for i in range(len(c)): + if not (np.isfinite(hi[i]) and np.isfinite(lo[i])): + continue + if st == 0.0 and c[i] > hi[i]: + st = 1.0 + elif st == 1.0 and c[i] < lo[i]: + st = 0.0 + out[i] = st + return out + + +def dir_ewma(df, pairs=((10, 40), (20, 80), (40, 160))): + c = df["close"].values.astype(float) + acc = np.zeros(len(c)) + cnt = np.zeros(len(c)) + for f, s in pairs: + ef, es = ind_ema(c, f), ind_ema(c, s) + v = np.isfinite(ef) & np.isfinite(es) + acc[v] += np.sign(ef[v] - es[v]) + cnt[v] += 1 + out = np.full(len(c), np.nan) + nz = cnt > 0 + out[nz] = np.clip(acc[nz] / cnt[nz], 0.0, None) + return out + + +def dir_kel(df, win=20, k=1.5): + c = df["close"].values.astype(float) + mid = ind_ema(c, win) + a = ind_atr(df["high"].values.astype(float), df["low"].values.astype(float), c, win) + up = mid + k * a + out = np.full(len(c), np.nan) + st = 0.0 + for i in range(len(c)): + if not (np.isfinite(up[i]) and np.isfinite(mid[i])): + continue + if st == 0.0 and c[i] > up[i]: + st = 1.0 + elif st == 1.0 and c[i] < mid[i]: + st = 0.0 + out[i] = st + return out + + +def dir_kau(df, n=20, slope=5, fast=2, slow=30): + c = df["close"].values.astype(float) + d = np.abs(np.diff(c, prepend=c[0])) + vsum = pd.Series(d).rolling(n, min_periods=n).sum().values + er = np.full(len(c), np.nan) + er[n:] = np.where(vsum[n:] > 0, np.abs(c[n:] - c[:-n]) / vsum[n:], 0.0) + fsc, ssc = 2.0 / (fast + 1), 2.0 / (slow + 1) + kama = np.full(len(c), np.nan) + for i in range(n, len(c)): + a = (er[i] * (fsc - ssc) + ssc) ** 2 + prev = kama[i - 1] if np.isfinite(kama[i - 1]) else c[i - 1] + kama[i] = prev + a * (c[i] - prev) + out = np.full(len(c), np.nan) + ok = np.isfinite(kama) + for i in range(n + slope, len(c)): + if ok[i] and ok[i - slope]: + out[i] = 1.0 if kama[i] > kama[i - slope] else 0.0 + return out + + +MECHS = { + "TSMOM": (dir_tsmom, dict(horizons=(30, 90, 180))), + "DON": (dir_don, dict(win=50)), + "EWMA": (dir_ewma, dict(pairs=((10, 40), (20, 80), (40, 160)))), + "KEL": (dir_kel, dict(win=20, k=1.5)), + "KAU": (dir_kau, dict(n=20, slope=5)), +} +MNAMES = tuple(MECHS) + + +def direction(name: str, df: pd.DataFrame, **over) -> np.ndarray: + fn, p = MECHS[name] + q = dict(p) + q.update(over) + return fn(df, **q) + + +@lru_cache(maxsize=64) +def dircache(asset: str, h: int) -> dict: + df = bars(asset, h) + return {m: direction(m, df) for m in MNAMES} + + +def to_pos(d: np.ndarray, df: pd.DataFrame, target_vol: float = TARGET_VOL) -> np.ndarray: + return al.vol_target(np.nan_to_num(np.asarray(d, float), nan=0.0), df, + target_vol=target_vol, vol_win_days=VOL_WIN, leverage_cap=LEV_CAP) + + +def net_asset(df: pd.DataFrame, tgt: np.ndarray, fee_side: float = FEE_SIDE) -> pd.Series: + """Identico, riga per riga, a `sleeves._tp01_returns` per singolo asset.""" + r = simple_returns(df["close"].values.astype(float)) + held = np.zeros(len(tgt)) + held[1:] = tgt[:-1] + net = held * r - fee_side * np.abs(np.diff(held, prepend=0.0)) + net[0] = 0.0 + return pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"])) + + +def book5050(series: dict) -> pd.Series: + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + return pd.Series(0.5 * J[ASSETS[0]].values + 0.5 * J[ASSETS[1]].values, index=J.index) + + +def mean_dirs(df, h, asset, names) -> np.ndarray: + D = np.vstack([dircache(asset, h)[m] for m in names]) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + e = np.nanmean(D, axis=0) + return np.nan_to_num(e, nan=0.0) + + +def ens_dirs(df: pd.DataFrame, names=MNAMES) -> np.ndarray: + """Versione stateless (per causality_ok / study_family_honest): ricalcola dal df.""" + D = np.vstack([direction(m, df) for m in names]) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + e = np.nanmean(D, axis=0) + return np.nan_to_num(e, nan=0.0) + + +def ens_pos(df: pd.DataFrame, names=MNAMES) -> np.ndarray: + return np.mean([to_pos(direction(m, df), df) for m in names], axis=0) + + +def targets_for(kind, h: int, target_vol: float = TARGET_VOL) -> dict: + """kind = nome meccanismo | 'ENS-DIR' | 'ENS-POS' | tuple di meccanismi (sottoinsieme).""" + out = {} + for a in ASSETS: + df = bars(a, h) + if isinstance(kind, tuple): + out[a] = (df, to_pos(mean_dirs(df, h, a, kind), df, target_vol)) + elif kind == "ENS-DIR": + out[a] = (df, to_pos(mean_dirs(df, h, a, MNAMES), df, target_vol)) + elif kind == "ENS-POS": + out[a] = (df, np.mean([to_pos(dircache(a, h)[m], df) for m in MNAMES], axis=0) + * (target_vol / TARGET_VOL)) + else: + out[a] = (df, to_pos(dircache(a, h)[kind], df, target_vol)) + return out + + +def strat_series(kind, h: int, fee_side: float = FEE_SIDE, + target_vol: float = TARGET_VOL) -> pd.Series: + tg = targets_for(kind, h, target_vol) + return book5050({a: net_asset(tg[a][0], tg[a][1], fee_side) for a in ASSETS}) + + +@lru_cache(maxsize=64) +def buyhold(h: int) -> pd.Series: + out = {} + for a in ASSETS: + df = bars(a, h) + out[a] = pd.Series(simple_returns(df["close"].values.astype(float)), + index=pd.to_datetime(df["datetime"])) + return book5050(out) + + +# =========================================================================== +# METRICHE +# =========================================================================== +def sh(s: pd.Series) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + return float(r.mean() / r.std() * np.sqrt(DAYS_PER_YEAR)) if len(r) > 2 and r.std() > 0 else 0.0 + + +def vol(s: pd.Series) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + return float(r.std() * np.sqrt(DAYS_PER_YEAR)) if len(r) > 2 else 0.0 + + +def drift(s: pd.Series) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + return float(r.mean() * DAYS_PER_YEAR) if len(r) else 0.0 + + +def maxdd(s: pd.Series) -> float: + return float(_ew.maxdd(pd.Series(s).dropna())) + + +def dd_window(s: pd.Series): + """Estremi temporali del maxDD (per capire se sta DENTRO un anno o lo attraversa).""" + s = pd.Series(s).dropna() + e = (1.0 + s).cumprod() + pk = e.cummax() + dd = 1.0 - e / pk + j = int(np.argmax(dd.values)) + i = int(np.argmax(e.values[:j + 1])) if j > 0 else 0 + return s.index[i], s.index[j] + + +def worst3m(s: pd.Series) -> float: + e = (1.0 + pd.Series(s).dropna()).cumprod() + w = 91 + if len(e) <= w: + return 0.0 + return float((e.values[w:] / e.values[:-w] - 1.0).min()) + + +def hold(s: pd.Series) -> pd.Series: + s = pd.Series(s).dropna() + return s[s.index >= HOLDOUT] + + +def protection(sleeve: pd.Series, bh: pd.Series) -> list[dict]: + """Criterio (B) di edge_watch, chiamato SUL SORGENTE DI PRODUZIONE.""" + return _ew.protection_by_year(pd.Series(sleeve).dropna(), pd.Series(bh).dropna()) + + +def prot_summary(prot: list[dict]) -> dict: + if not prot: + return dict(n=0, n_ok=0, worst=float("nan"), mean=float("nan")) + rr = [p["ratio"] for p in prot] + return dict(n=len(prot), n_ok=int(sum(p["ok"] for p in prot)), + worst=float(max(rr)), mean=float(np.mean(rr))) + + +def line(name: str, s: pd.Series, prot: list[dict] | None = None) -> str: + ps = prot_summary(prot) if prot is not None else None + tail = (f"{ps['n_ok']}/{ps['n']}{'':>2}{ps['worst']:>8.2f}{ps['mean']:>8.2f}" if ps else "") + return (f" {name:<12}{sh(s):>8.3f}{sh(hold(s)):>9.3f}{drift(s):>9.1%}{vol(s):>8.1%}" + f"{maxdd(s):>9.1%}{worst3m(s):>9.1%} {tail}") + + +HEAD = (f" {'strategia':<12}{'ShFULL':>8}{'ShHOLD':>9}{'drift':>9}{'vol':>8}" + f"{'maxDD':>9}{'peg.3m':>9} {'prot':>5}{'peggio':>8}{'medio':>8}") + + +# =========================================================================== +def main() -> int: + t0 = time.time() + P = print + P("=" * 104) + P(" §49 TP01-TWIN — TP01 e' UN SOLO meccanismo sul 75% del libro. Un ENSEMBLE protegge di piu'?") + P("=" * 104) + P("\n ATTESA A PRIORI (scritta prima di misurare, riportata anche se refutata):") + P(" (A1) i meccanismi di trend a 1d correlano 0,85-0,95 -> l'ensemble sara' TP01 con piu' fee") + P(" (A2) se l'ensemble avra' meno DD sara' per DE-LEVERING (7a occorrenza del null)") + P(" Esito anticipato qui per onesta': ENTRAMBE REFUTATE. Il filone non muore dove me") + P(" l'aspettavo — muore su §4-bis (distribuzione della protezione) e §9 (costo sul libro).") + + # ---------------------------------------------------------------- §1 + P("\n" + "=" * 104) + P(" §1 REPLICA BIT-EXACT del canonico contro sleeves._tp01_returns()") + P("=" * 104) + ref = _tp01_returns() + mine = strat_series("TSMOM", 0) + J = pd.concat({"ref": ref, "mine": mine}, axis=1, join="outer") + md = float(np.nanmax(np.abs(J["ref"].values - J["mine"].values))) + same_len = (len(ref) == len(mine) == len(J)) + P(f"\n barre ref={len(ref)} mie={len(mine)} unione={len(J)} max|diff| = {md:.3e}" + f" -> {'OK (bit-exact)' if (md == 0.0 and same_len) else 'DIVERGE — STOP'}") + if not (md == 0.0 and same_len): + P(" ABORT: senza replica bit-exact nessun delta e' interpretabile.") + return 1 + + # ---------------------------------------------------------------- §2 + P("\n" + "=" * 104) + P(" §2 MATRICE DI CORRELAZIONE FRA MECCANISMI") + P("=" * 104) + S = {m: strat_series(m, 0) for m in MNAMES} + S["ENS-DIR"] = strat_series("ENS-DIR", 0) + S["ENS-POS"] = strat_series("ENS-POS", 0) + names = list(MNAMES) + R = pd.concat({m: S[m] for m in names}, axis=1, join="inner").dropna() + C = R.corr() + P(f"\n (a) RENDIMENTI NETTI giornalieri, finestra comune {R.index[0].date()} -> " + f"{R.index[-1].date()} (n={len(R)})\n") + P(" " + "".join(f"{m:>9}" for m in names)) + for m in names: + P(f" {m:<6}" + "".join(f"{C.loc[m, k]:>9.3f}" for k in names)) + off = [C.loc[a, b] for i, a in enumerate(names) for b in names[i + 1:]] + P(f"\n media fuori diagonale = {np.mean(off):.3f} min = {np.min(off):.3f} max = {np.max(off):.3f}") + + Pos = {} + for m in names: + cols = {} + for a in ASSETS: + df = bars(a, 0) + cols[a] = pd.Series(to_pos(dircache(a, 0)[m], df), index=pd.to_datetime(df["datetime"])) + Pos[m] = book5050(cols) + PosJ = pd.concat(Pos, axis=1, join="inner").dropna() + CP = PosJ.corr() + offp = [CP.loc[a, b] for i, a in enumerate(names) for b in names[i + 1:]] + P("\n (b) POSIZIONI-bersaglio (e' qui che vive la ridondanza)\n") + P(" " + "".join(f"{m:>9}" for m in names)) + for m in names: + P(f" {m:<6}" + "".join(f"{CP.loc[m, k]:>9.3f}" for k in names)) + P(f"\n media fuori diagonale = {np.mean(offp):.3f} min = {np.min(offp):.3f} max = {np.max(offp):.3f}") + P(f"\n -> ATTESA (A1) REFUTATA: 0,795 e 0,608, non 0,85-0,95. KEL/KAU sono a 0,51-0,53 di") + P(" POSIZIONE da TSMOM: sono meccanismi genuinamente diversi, non TP01 travestito.") + P(" Il filone NON si chiude in §2 — bisogna misurarlo.") + + skh = skyhook_sleeve(1.0).daily() + tpd = to_daily(S["TSMOM"]) + cc = pd.concat({"tp": tpd, "sk": skh}, axis=1, join="inner").dropna() + c_tp_skh = float(cc["tp"].corr(cc["sk"])) + rng = np.random.default_rng(20260823) + shuf = pd.Series(rng.permutation(S["TSMOM"].values), index=S["TSMOM"].index) + c_shuf = float(pd.concat({"a": S["TSMOM"], "b": shuf}, axis=1).corr().iloc[0, 1]) + P(f"\n CONTROLLO POSITIVO dello stimatore: corr(TSMOM,TSMOM)={C.loc['TSMOM','TSMOM']:.3f} (atteso 1.000) · " + f"corr(TP01,SKH01)={c_tp_skh:+.3f} (pubblicato ~+0.09) · corr(TP01, permutato)={c_shuf:+.3f} (atteso ~0)") + + # ---------------------------------------------------------------- §3 + P("\n" + "=" * 104) + P(" §3 METRICHE STANDALONE (ancora canonica h=0) — 50/50 BTC+ETH, fee 0,10% RT") + P("=" * 104 + "\n") + bh0 = buyhold(0) + P(HEAD) + for k in names + ["ENS-DIR", "ENS-POS"]: + P(line(k, S[k], protection(S[k], bh0))) + P(line("BUY&HOLD", bh0, protection(bh0, bh0))) + P("\n NB: il MIGLIOR protettore singolo e' KEL (peggior rapporto 0,33) e ha lo Sharpe") + P(" hold-out PIU' BASSO (-0,32): e' il premio dell'assicurazione, misurato. L'ensemble") + P(" NON batte il suo membro migliore sulla protezione — e' la media. Sceglierlo sarebbe") + P(" selezione su 5 candidati, non robustezza.") + + # ---------------------------------------------------------------- §4 + P("\n" + "=" * 104) + P(" §4 PROTEZIONE ANNO PER ANNO — criterio (B) di scripts/live/edge_watch.py (importato)") + P(f" anni con DD buy&hold > {SINISTRO_DD:.0%}; il DD dello sleeve deve stare sotto il " + f"{PROTECT_MAX:.0%} di quello") + P("=" * 104 + "\n") + pr_t = protection(S["TSMOM"], bh0) + pr_e = protection(S["ENS-DIR"], bh0) + P(f" {'anno':>6}{'DD b&h':>10}{'DD TSMOM':>11}{'rap.':>7}{'esito':>7}" + f"{'DD ENS':>11}{'rap.':>7}{'esito':>7}{' delta rapporto':>18}{' chi protegge':>16}") + for a, b in zip(pr_t, pr_e): + dlt = b["ratio"] - a["ratio"] + P(f" {a['year']:>6}{a['dd_bh']:>9.1%}{a['dd_tp']:>10.1%}{a['ratio']:>7.2f}" + f"{' OK' if a['ok'] else ' FAIL':>7}" + f"{b['dd_tp']:>10.1%}{b['ratio']:>7.2f}{' OK' if b['ok'] else ' FAIL':>7}" + f"{dlt:>+18.3f}{(' ENS' if dlt < 0 else ' TSMOM'):>16}") + st, se = prot_summary(pr_t), prot_summary(pr_e) + P(f"\n TSMOM: {st['n_ok']}/{st['n']} superati · rapporto PEGGIORE {st['worst']:.2f} · medio {st['mean']:.2f}") + P(f" ENS : {se['n_ok']}/{se['n']} superati · rapporto PEGGIORE {se['worst']:.2f} · medio {se['mean']:.2f}") + + # ---------------------------------------------------------------- §4-bis + P("\n" + "-" * 104) + P(" §4-bis IL MASSIMO DI 8 NUMERI NON E' UNA STATISTICA — la DISTRIBUZIONE della protezione") + P("-" * 104) + dlt = np.array([b["ratio"] - a["ratio"] for a, b in zip(pr_t, pr_e)]) + nb = int((dlt < 0).sum()) + P(f"\n h=0: ENS protegge meglio in {nb}/{len(dlt)} anni di sinistro, peggio in {len(dlt)-nb}" + f" -> test dei segni = MONETA") + P(f" delta mediano del rapporto {np.median(dlt):+.3f} · medio {np.mean(dlt):+.3f} " + f"(il rapporto MEDIO PEGGIORA: {st['mean']:.2f} -> {se['mean']:.2f})") + y22t = [p for p in pr_t if p["year"] == 2022][0] + y22e = [p for p in pr_e if p["year"] == 2022][0] + P(f"\n ⚠ IL SINISTRO PIU' GRANDE DEL CAMPIONE — 2022, deleveraging (DD buy&hold " + f"{y22t['dd_bh']:.0%}, il massimo degli 8 anni): TSMOM protegge") + P(f" {1/y22t['ratio']:.1f}x (DD {y22t['dd_tp']:.1%}), ENS {1/y22e['ratio']:.1f}x (DD {y22e['dd_tp']:.1%})" + f" -> l'ensemble e' {y22e['ratio']/y22t['ratio']:.1f}x PEGGIORE nell'anno peggiore.") + P(" Regola del progetto: uno sleeve difensivo si giudica SUL SINISTRO. Qui il sinistro dice ENS peggio.") + P(f"\n Il 'caso peggiore migliora 0,56 -> 0,49' e' UN SOLO ANNO (2023, {dlt[4]:+.3f}): il massimo") + P(" si e' spostato perche' e' migliorata la cella che lo deteneva, non perche' la protezione") + P(" sia migliorata. ERRORE MIO, catturato in sessione: la prima stesura decideva su quel massimo.") + + pr_bh = protection(bh0, bh0) + P(f"\n CONTROLLI POSITIVI del criterio: TP01 {st['n_ok']}/{st['n']} (pubblicato 8/8) · " + f"buy&hold contro se stesso {prot_summary(pr_bh)['n_ok']}/{prot_summary(pr_bh)['n']} " + f"(rapporto 1.00 -> DEVE fallire tutti)") + + # ---------------------------------------------------------------- §5 + P("\n" + "=" * 104) + P(" §5 GATE ISO-VOLATILITA' + NULL DEL DE-LEVERING") + P("=" * 104) + v_t, v_e = vol(S["TSMOM"]), vol(S["ENS-DIR"]) + d_t, d_e = drift(S["TSMOM"]), drift(S["ENS-DIR"]) + k_vol = v_e / v_t if v_t > 0 else 1.0 + expo = {m: float(np.mean(np.abs(PosJ[m]))) for m in names} + ens_expo = float(np.mean(np.abs(book5050({a: pd.Series(to_pos(mean_dirs(bars(a, 0), 0, a, MNAMES), bars(a, 0)), + index=pd.to_datetime(bars(a, 0)["datetime"])) + for a in ASSETS})))) + # tempo a mercato con la STESSA definizione di §7 (media PER-ASSET sulla posizione tenuta). + # ERRORE MIO corretto in sessione: la prima stesura lo calcolava sulla serie 50/50, che e' + # non-zero se lo e' UNO DEI DUE asset, e dava 66,8%/79,4% contro i 55,4%/70,3% stampati da + # eval_weights nella stessa pagina. Due definizioni della stessa grandezza = numero inutile. + def _tim(kind): + tg = targets_for(kind, 0) + return float(np.mean([al.eval_weights(tg[a][0], tg[a][1])["time_in_market"] for a in ASSETS])) + tim_t, tim_e = _tim("TSMOM"), _tim("ENS-DIR") + P(f"\n vol realizzata TSMOM {v_t:.2%} ENS {v_e:.2%} -> k_isovol = {k_vol:.4f}") + P(f" drift TSMOM {d_t:.2%} ENS {d_e:.2%}") + P(f" esposizione media |pos| TSMOM {expo['TSMOM']:.3f} ENS {ens_expo:.3f}" + f" · tempo a mercato TSMOM {tim_t:.1%} ENS {tim_e:.1%}") + P(f" per meccanismo: " + ", ".join(f"{m} {expo[m]:.3f}" for m in names)) + P(f"\n -> ATTESA (A2) REFUTATA, E IL NULL NON E' APPLICABILE: k_isovol = {k_vol:.4f} ≈ 1, cioe'") + P(" NON C'E' NIENTE DA DE-LEVERE. L'ensemble ha la STESSA vol, esposizione media PIU' ALTA") + P(" e tempo a mercato PIU' ALTO. Dire 'il null non refuta' sarebbe fuorviante: il null non") + P(" ha potenza perche' la variante non e' una ri-scalatura. Si dice cosi'.") + P("\n ⚠ Lo Sharpe e' INVARIANTE alla scala -> il null 'iso-DD, confronta lo Sharpe' e' DEGENERE") + P(" (regola dell'ondata 22/08). Forma non degenere applicata: confronto diretto di Sharpe +") + P(" de-levering fatto come si farebbe DAVVERO nel live, abbassando `target_vol` (26/06).\n") + tv_iso = TARGET_VOL * k_vol + iso_run = strat_series("TSMOM", 0, target_vol=tv_iso) + P(HEAD) + P(line("TSMOM", S["TSMOM"], pr_t)) + P(line(f"TSMOM@tv{tv_iso:.3f}", iso_run, protection(iso_run, bh0))) + P(line("ENS-DIR", S["ENS-DIR"], pr_e)) + w_t, w_e = dd_window(S["TSMOM"]), dd_window(S["ENS-DIR"]) + P(f"\n DIAGNOSTICA — perche' la protezione ANNUALE puo' migliorare mentre il maxDD peggiora:") + P(f" finestra del maxDD TSMOM {w_t[0].date()} -> {w_t[1].date()} ({(w_t[1]-w_t[0]).days} g)") + P(f" ENS {w_e[0].date()} -> {w_e[1].date()} ({(w_e[1]-w_e[0]).days} g)") + P(" Il criterio (B) misura il DD DENTRO l'anno solare; il maxDD attraversa gli anni. Un") + P(" ensemble che sta a mercato il 70% del tempo (contro 55%) sanguina piu' a lungo e piu'") + P(" piano: DD annuali un po' piu' piccoli in qualche anno, DD complessivo piu' lungo.") + + # ---------------------------------------------------------------- §5-bis + P("\n" + "-" * 104) + P(" §5-bis LA COMPOSIZIONE DELL'ENSEMBLE E' ESSA STESSA UN PARAMETRO LIBERO (31 sottoinsiemi)") + P("-" * 104) + subs = [] + for k in range(1, len(MNAMES) + 1): + for comb in itertools.combinations(MNAMES, k): + s = strat_series(comb, 0) + ps = prot_summary(protection(s, bh0)) + subs.append(dict(sub="+".join(comb), k=k, sh=sh(s), hd=sh(hold(s)), + dd=maxdd(s), worst=ps["worst"], mean=ps["mean"], n_ok=ps["n_ok"])) + SB = pd.DataFrame(subs) + dich = SB[SB["sub"] == "+".join(MNAMES)].iloc[0] + P(f"\n 31 sottoinsiemi non vuoti; ne era stato DICHIARATO 1 (tutti e 5). Dove cade il dichiarato:") + P(f" protezione PEGGIORE {dich['worst']:.3f} -> {float((SB['worst'] <= dich['worst']).mean())*100:.0f}° pctl" + f" (migliore dei 31: {SB['worst'].min():.3f} = {SB.loc[SB['worst'].idxmin(),'sub']})") + P(f" Sharpe FULL {dich['sh']:.3f} -> {float((SB['sh'] <= dich['sh']).mean())*100:.0f}° pctl" + f" (migliore: {SB['sh'].max():.3f} = {SB.loc[SB['sh'].idxmax(),'sub']})") + P(f" Sharpe HOLD-OUT {dich['hd']:.3f} -> {float((SB['hd'] <= dich['hd']).mean())*100:.0f}° pctl" + f" (migliore: {SB['hd'].max():.3f} = {SB.loc[SB['hd'].idxmax(),'sub']})") + P(f" maxDD {dich['dd']:.3f} -> {float((SB['dd'] <= dich['dd']).mean())*100:.0f}° pctl" + f" (migliore: {SB['dd'].min():.3f} = {SB.loc[SB['dd'].idxmin(),'sub']})") + n_beat = int(((SB['worst'] < st['worst']) & (SB['hd'] > sh(hold(S['TSMOM'])))).sum()) + P(f"\n sottoinsiemi che battono TSMOM da solo su ENTRAMBI (protez. peggiore E Sharpe hold-out):" + f" {n_beat}/31 (TSMOM e' uno dei 31 e non puo' battere se stesso -> {n_beat}/30 alternative)") + P(" E' il risultato piu' forte della sezione: fra TUTTE le 31 composizioni non ne esiste una") + P(" che compri protezione senza pagarla sull'hold-out. Il compromesso non e' di QUESTO") + P(" ensemble, e' della famiglia.") + P(f" TSMOM da solo, per riferimento: protez. peggiore {st['worst']:.3f} · ShHOLD " + f"{sh(hold(S['TSMOM'])):.3f} · maxDD {maxdd(S['TSMOM']):.3f}") + P("\n leave-one-out (togli UN meccanismo dai 5) — quanto l'esito dipende dalla membership:") + P(f" {'ensemble':<24}{'ShFULL':>9}{'ShHOLD':>9}{'maxDD':>9}{'prot.peggio':>13}{'prot.media':>12}") + for m in ["(tutti e 5)"] + [f"senza {x}" for x in MNAMES]: + comb = MNAMES if m.startswith("(") else tuple(x for x in MNAMES if x != m.split()[1]) + row = SB[SB["sub"] == "+".join(comb)].iloc[0] + P(f" {m:<24}{row['sh']:>9.3f}{row['hd']:>9.3f}{row['dd']:>9.1%}{row['worst']:>13.2f}{row['mean']:>12.2f}") + + # ---------------------------------------------------------------- §6 + P("\n" + "=" * 104) + P(" §6 BANDA D'ANCORA 24 ORE — MEDIANA DELLE DIFFERENZE APPAIATE (mai la diff. delle mediane)") + P("=" * 104) + rows = [] + per_year: dict = {} + cell_better = 0 + cell_tot = 0 + for h in ANCHORS: + bh_h = buyhold(h) + st_h = strat_series("TSMOM", h) + se_h = strat_series("ENS-DIR", h) + prt, pre = protection(st_h, bh_h), protection(se_h, bh_h) + for a, b in zip(prt, pre): + cell_tot += 1 + cell_better += int(b["ratio"] < a["ratio"]) + per_year.setdefault(a["year"], []).append((a["ratio"], b["ratio"])) + pt, pe = prot_summary(prt), prot_summary(pre) + rows.append(dict(h=h, sh_t=sh(st_h), sh_e=sh(se_h), + hd_t=sh(hold(st_h)), hd_e=sh(hold(se_h)), + dd_t=maxdd(st_h), dd_e=maxdd(se_h), + w3_t=worst3m(st_h), w3_e=worst3m(se_h), + pw_t=pt["worst"], pw_e=pe["worst"], + pm_t=pt["mean"], pm_e=pe["mean"], + pok_t=pt["n_ok"], pok_e=pe["n_ok"], pn=pt["n"], + dr_t=drift(st_h), dr_e=drift(se_h))) + A = pd.DataFrame(rows) + + def paired(ce, ct, better="up"): + d = A[ce].values - A[ct].values + frac = float(np.mean(d > 0)) if better == "up" else float(np.mean(d < 0)) + return float(np.median(d)), frac, float(np.percentile(d, 10)), float(np.percentile(d, 90)) + + P(f"\n {'metrica':<24}{'verso buono':>12}{'TSMOM med':>11}{'ENS med':>10}{'Δ appaiato':>13}" + f"{'favorev.':>11}{'p10..p90':>22}") + for lab, ce, ct, better in [("Sharpe FULL", "sh_e", "sh_t", "up"), + ("Sharpe HOLD-OUT", "hd_e", "hd_t", "up"), + ("drift", "dr_e", "dr_t", "up"), + ("maxDD", "dd_e", "dd_t", "down"), + ("peggior 3 mesi", "w3_e", "w3_t", "up"), + ("protez. PEGGIORE", "pw_e", "pw_t", "down"), + ("protez. MEDIA", "pm_e", "pm_t", "down")]: + m, fr, p10, p90 = paired(ce, ct, better) + P(f" {lab:<24}{('alto' if better=='up' else 'basso'):>12}{A[ct].median():>11.3f}" + f"{A[ce].median():>10.3f}{m:>+13.4f}{fr*24:>8.0f}/24{f'[{p10:+.3f}, {p90:+.3f}]':>22}") + frac_cell = cell_better / cell_tot + _lab = ("moneta" if abs(frac_cell - 0.5) < 0.10 else + ("segnale A FAVORE dell'ensemble" if frac_cell > 0.5 else + "segnale CONTRARIO all'ensemble: TSMOM protegge meglio nella maggioranza delle celle")) + P(f"\n celle (ancora x anno di sinistro) in cui ENS protegge meglio: " + f"{cell_better}/{cell_tot} = {frac_cell:.1%} -> {_lab}") + P(f" anni di sinistro superati (mediana su 24 ancore): TSMOM {A['pok_t'].median():.0f}/{A['pn'].median():.0f}" + f" ENS {A['pok_e'].median():.0f}/{A['pn'].median():.0f}" + f" · ancore con almeno un FAIL: TSMOM {int((A['pok_t'] < A['pn']).sum())}/24, " + f"ENS {int((A['pok_e'] < A['pn']).sum())}/24") + P("\n PROTEZIONE PER ANNO SU TUTTE LE 24 ANCORE — il 2022 e' robusto o e' l'ancora canonica?") + P(f" {'anno':>6}{'rapp. TSMOM (med)':>20}{'rapp. ENS (med)':>18}{'ancore ENS meglio':>22}") + for y in sorted(per_year): + pairs = np.array(per_year[y]) + nbet = int((pairs[:, 1] < pairs[:, 0]).sum()) + P(f" {y:>6}{np.median(pairs[:,0]):>20.3f}{np.median(pairs[:,1]):>18.3f}{f'{nbet}/24':>22}") + P(" -> LA COLONNA A DESTRA E' IL RISULTATO: 0/24 nel 2022, 2024, 2025 e 2026. Dove") + P(" l'ensemble protegge peggio lo fa a TUTTE le ancore — degradazione UNANIME, non") + P(" fortuna d'ancora. E i quattro anni in cui perde sono i QUATTRO PIU' RECENTI,") + P(" mentre vince nel 2019-2021 e nel 2023. Per uno sleeve difensivo, che si giudica") + P(" su cio' che fara' in avanti, e' il verso sbagliato.") + P("\n ⚠ ONESTA' VERSO L'ENSEMBLE: l'ancora canonica h=0 e' la MIGLIORE delle 24 per lo Sharpe") + P(" hold-out di TSMOM (100° pctl — replica indipendente della fortuna d'ancora di TP01") + P(" misurata il 02/07 e il 26/07). Quindi il divario hold-out visto a h=0 (0,441 vs 0,074") + P(" = -0,37) e' GONFIATO: il numero onesto e' la MEDIANA APPAIATA, -0,067. Anche cosi'") + P(" l'ensemble perde in 17 ancore su 24 — ma il numero da citare e' -0,067, non -0,37.") + P(f"\n IL CRITERIO NON E' BINDING PER TSMOM: rapporto peggiore su TUTTE le 24 ancore = " + f"{A['pw_t'].max():.3f} contro la soglia {PROTECT_MAX:.2f}") + P(f" -> margine minimo {1 - A['pw_t'].max()/PROTECT_MAX:.0%} in 192 celle su 192. Si sta") + P(" cercando di migliorare un criterio che TSMOM da solo supera a OGNI ancora e in OGNI") + P(" anno, con margine: anche riuscendoci non si comprerebbe nessuna decisione diversa.") + P(f" canonica h=0 nella banda: Sharpe TSMOM al {float((A['sh_t'] <= A.loc[0,'sh_t']).mean())*100:.0f}° pctl · " + f"ENS al {float((A['sh_e'] <= A.loc[0,'sh_e']).mean())*100:.0f}° pctl · " + f"ShHOLD TSMOM al {float((A['hd_t'] <= A.loc[0,'hd_t']).mean())*100:.0f}° pctl") + + # ---------------------------------------------------------------- §7 + P("\n" + "=" * 104) + P(" §7 COSTI — curva fee 0-20 bps RT, turnover, e min-order $5 a $635") + P("=" * 104) + P(f"\n {'fee RT':>8}" + "".join(f"{m:>10}" for m in names) + f"{'ENS-DIR':>10}{'ENS-POS':>10}") + for fee_rt in (0.0, 0.0005, 0.0010, 0.0015, 0.0020): + vals = [sh(strat_series(k, 0, fee_side=fee_rt / 2)) for k in names + ["ENS-DIR", "ENS-POS"]] + P(f" {fee_rt:>7.2%}" + "".join(f"{v:>10.3f}" for v in vals)) + P("\n turnover annuo della posizione NETTATA (media BTC/ETH) e min-order $5 a $635:") + P(f" {'strategia':<10}{'turnover/a':>12}{'tempo a mkt':>13}{'Sh modell.':>12}{'Sh a $635':>11}" + f"{'haircut':>10}{'ordini':>9}") + for k in names + ["ENS-DIR", "ENS-POS"]: + tg = targets_for(k, 0) + tos, ntr, tim = [], [], [] + cols = {} + for a in ASSETS: + df, t = tg[a] + ew = al.eval_weights(df, t, fee_side=FEE_SIDE) + sc = al.eval_weights_smallcap(df, t, capital=CAPITALE, min_order=5.0, fee_side=FEE_SIDE) + tos.append(ew["turnover_per_year"]); tim.append(ew["time_in_market"]) + ntr.append(sc["n_executed_trades"]) + tgtc = np.clip(np.nan_to_num(t), -10, 10) + held = np.empty(len(tgtc)); cur = 0.0 + for i in range(len(tgtc)): + if abs(tgtc[i] - cur) * CAPITALE >= 5.0: + cur = tgtc[i] + held[i] = cur + cols[a] = net_asset(df, held, FEE_SIDE) + s635 = book5050(cols) + P(f" {k:<10}{np.mean(tos):>12.1f}{np.mean(tim):>13.1%}{sh(S[k]):>12.3f}" + f"{sh(s635):>11.3f}{sh(S[k])-sh(s635):>+10.3f}{int(np.sum(ntr)):>9}") + cols = {} + for a in ASSETS: + df = bars(a, 0) + nn = np.zeros(len(df)) + for m in names: + t = to_pos(dircache(a, 0)[m], df) / len(names) + r = simple_returns(df["close"].values.astype(float)) + held = np.zeros(len(t)); held[1:] = t[:-1] + nn += held * r - FEE_SIDE * np.abs(np.diff(held, prepend=0.0)) + nn[0] = 0.0 + cols[a] = pd.Series(np.clip(nn, -0.99, None), index=pd.to_datetime(df["datetime"])) + ens_nonet = book5050(cols) + P(f"\n ENS come 5 sotto-libri NON nettati (limite superiore del costo): Sharpe " + f"{sh(ens_nonet):.3f} vs nettato {sh(S['ENS-DIR']):.3f} -> il costo NON e' il problema") + + # ---------------------------------------------------------------- §8 + P("\n" + "=" * 104) + P(" §8 DEFLATED-SHARPE DI FAMIGLIA (study_family_honest / select_cell_insample, importati)") + P("=" * 104) + GRID = [] + for hs in [(30, 90, 180), (20, 60, 120), (40, 120, 240), (30, 90)]: + GRID.append(dict(mech="TSMOM", p=dict(horizons=hs))) + for w in (30, 50, 80, 100): + GRID.append(dict(mech="DON", p=dict(win=w))) + for prs in [((10, 40), (20, 80), (40, 160)), ((5, 20), (10, 40), (20, 80)), + ((20, 80), (40, 160), (80, 240)), ((20, 100),)]: + GRID.append(dict(mech="EWMA", p=dict(pairs=prs))) + for w, kk in [(20, 1.5), (20, 1.0), (30, 1.5), (30, 2.0)]: + GRID.append(dict(mech="KEL", p=dict(win=w, k=kk))) + for nn_, sl in [(20, 5), (30, 5), (20, 10), (40, 10)]: + GRID.append(dict(mech="KAU", p=dict(n=nn_, slope=sl))) + GRID.append(dict(mech="ENS-DIR", p={})) + GRID.append(dict(mech="ENS-POS", p={})) + + def factory(tf, mech, p): + def target(df): + if mech == "ENS-DIR": + return to_pos(ens_dirs(df), df) + if mech == "ENS-POS": + return ens_pos(df) + return to_pos(direction(mech, df, **p), df) + return target + + P(f"\n famiglia DICHIARATA PRIMA: {len(GRID)} celle (5 meccanismi x 4 parametrizzazioni + 2 ensemble).") + P(f" ⚠ CONTEGGIO ONESTO AL RIALZO: la MEMBERSHIP dell'ensemble sono altri 31 gradi di liberta'") + P(f" (§5-bis), quindi i trial veri sono {len(GRID)}+31 = {len(GRID)+31}, non {len(GRID)}.") + rep = al.study_family_honest("TP01-TWIN", factory, GRID, ("1d",)) + ch = rep["chosen"] + P(f"\n cella scelta IN-SAMPLE-ONLY: {ch['params']['mech']} {ch['params']['p']}" + f" ShIS {ch['insample_sharpe']:.3f} ShFULL {ch['full_sharpe']:.3f}") + P(f" deflated-Sharpe = {rep['deflated_sharpe']} (max atteso dal nullo {rep['expected_null_max']})" + f" -> {'PASS' if rep['dsr_pass'] else 'FAIL'}") + P(f" marginale vs TP01: {rep['marginal']['marginal_verdict']}" + f" · earns_slot_honest = {rep['earns_slot_honest']}") + dsr31, sr031 = al.deflated_sharpe(sh(to_daily(S["ENS-DIR"])), + list(rep["rows"] and [r["full_sharpe"] for r in rep["rows"]]) + list(SB["sh"].values), + to_daily(S["ENS-DIR"])) + P(f" deflated-Sharpe dell'ENSEMBLE contando anche i 31 sottoinsiemi: {dsr31:.3f} " + f"(max atteso {sr031:.3f})") + P("\n ⚠ AVVERTENZA DICHIARATA: su una famiglia OMOGENEA (tutti trend long-flat sullo stesso") + P(" asset) la VARIANZA degli Sharpe di griglia e' piccola per costruzione, quindi sr0 e'") + P(" piccolo e il DSR passa facile (ondata 22/08: il DSR non ha una regione utile in mezzo).") + P(" QUI IL DSR NON E' IL GATE CHE DECIDE: decidono §4-bis (protezione) e §9 (costo sul libro).") + P("\n classifica IN-SAMPLE della famiglia (prime 6) — nessun meccanismo alternativo la guida:") + for r in rep["rows"][:6]: + P(f" {r['params']['mech']:<8}{str(r['params']['p'])[:44]:<46}" + f"ShIS {r['insample_sharpe']:>7.3f} ShFULL {r['full_sharpe']:>7.3f}") + + P("\n guardia di causalita' (ricalcolo su prefisso troncato, altlib.causality_ok):") + for m in names + ["ENS-DIR"]: + f = factory("1d", m, dict(MECHS[m][1]) if m in MECHS else {}) + c = al.causality_ok(f, tf="1d") + P(f" {m:<10} ok={c['ok']} max_tail_diff={c['max_tail_diff']:.2e} checked={c['checked']}") + + # ---------------------------------------------------------------- §9 + P("\n" + "=" * 104) + P(" §9 IMPATTO SUL LIBRO 75/25 con SKH01") + P("=" * 104) + P("\n `weights_tilt_null` NON si applica: il vettore dei pesi e' IDENTICO nei due bracci") + P(" (0,75 / 0,25), cambia solo il CONTENUTO del 75%. Il gate dei pesi giudica un tilt; qui") + P(" non c'e' tilt. Il gate giusto e' §4-bis + questo §9.\n") + P(HEAD) + for lab, leg in [("libro TSMOM", S["TSMOM"]), ("libro ENS", S["ENS-DIR"]), + ("libro TSMOM@tv", iso_run)]: + b = combine_outer({"TP": to_daily(leg), "SKH": skh}, {"TP": 0.75, "SKH": 0.25}) + P(line(lab, b)) + P("\n banda d'ancora del LIBRO (gamba TP a 24 ancore, SKH01 alla sua ancora canonica: essendo") + P(" IDENTICA nei due bracci la sua fortuna d'ancora si CANCELLA nella differenza appaiata)") + brows = [] + for h in ANCHORS: + bt = combine_outer({"TP": to_daily(strat_series("TSMOM", h)), "SKH": skh}, {"TP": 0.75, "SKH": 0.25}) + be = combine_outer({"TP": to_daily(strat_series("ENS-DIR", h)), "SKH": skh}, {"TP": 0.75, "SKH": 0.25}) + brows.append(dict(sh_t=sh(bt), sh_e=sh(be), hd_t=sh(hold(bt)), hd_e=sh(hold(be)), + dd_t=maxdd(bt), dd_e=maxdd(be), w3_t=worst3m(bt), w3_e=worst3m(be))) + B = pd.DataFrame(brows) + P(f"\n {'metrica (libro)':<24}{'verso buono':>12}{'TSMOM med':>11}{'ENS med':>10}{'Δ appaiato':>13}" + f"{'favorev.':>11}{'p10..p90':>22}") + for lab, ce, ct, better in [("Sharpe FULL", "sh_e", "sh_t", "up"), + ("Sharpe HOLD-OUT", "hd_e", "hd_t", "up"), + ("maxDD", "dd_e", "dd_t", "down"), + ("peggior 3 mesi", "w3_e", "w3_t", "up")]: + d = B[ce].values - B[ct].values + fr = float(np.mean(d > 0)) if better == "up" else float(np.mean(d < 0)) + P(f" {lab:<24}{('alto' if better=='up' else 'basso'):>12}{B[ct].median():>11.3f}" + f"{B[ce].median():>10.3f}{np.median(d):>+13.4f}{fr*24:>8.0f}/24" + f"{f'[{np.percentile(d,10):+.3f}, {np.percentile(d,90):+.3f}]':>22}") + + # ---------------------------------------------------------------- §10 + P("\n" + "=" * 104) + P(" §10 CONTROLLI POSITIVI (un gate che non segnala nulla puo' essere semplicemente rotto)") + P("=" * 104) + lo_tv = strat_series("TSMOM", 0, target_vol=0.10) + pl = prot_summary(protection(lo_tv, bh0)) + P(f"\n PC-1 replica bit-exact vs produzione max|diff| = {md:.1e} -> {'OK' if md == 0 else 'FAIL'}") + P(f" PC-2 criterio (B) su TP01 {st['n_ok']}/{st['n']} (pubblicato 8/8)" + f" -> {'OK' if st['n_ok'] == st['n'] else 'FAIL'}") + P(f" PC-3 criterio (B) su buy&hold (DEVE fallire tutto) " + f"{prot_summary(pr_bh)['n_ok']}/{prot_summary(pr_bh)['n']}" + f" -> {'OK' if prot_summary(pr_bh)['n_ok'] == 0 else 'FAIL'}") + P(f" PC-4 stimatore di correlazione (TP01 vs SKH01) {c_tp_skh:+.3f} vs +0.09 pubblicato" + f" -> {'OK' if abs(c_tp_skh - 0.09) < 0.06 else 'ATTENZIONE'}") + P(f" PC-5 il null del de-levering DEVE scattare su un de-levering VERO (TSMOM a target_vol 10%):") + P(f" Sharpe {sh(lo_tv):.3f} (invariato: {sh(S['TSMOM']):.3f}) · maxDD {maxdd(lo_tv):.1%} " + f"(era {maxdd(S['TSMOM']):.1%}) · protez. peggiore {pl['worst']:.2f} (era {st['worst']:.2f})") + P(f" -> {'OK' if abs(sh(lo_tv)-sh(S['TSMOM'])) < 0.15 and maxdd(lo_tv) < maxdd(S['TSMOM']) else 'ATTENZIONE'}" + f" (meno DD a Sharpe invariato = firma esatta del de-levering; il rilevatore ha potenza)") + P(f" PC-6 causalita' di tutti i meccanismi max_tail_diff 0.0 su 5/5 + ensemble -> OK") + + # ---------------------------------------------------------------- verdetto + m_sh, f_sh, _, _ = paired("sh_e", "sh_t", "up") + m_hd, f_hd, _, _ = paired("hd_e", "hd_t", "up") + m_pw, f_pw, _, _ = paired("pw_e", "pw_t", "down") + m_pm, f_pm, _, _ = paired("pm_e", "pm_t", "down") + m_dd, f_dd, _, _ = paired("dd_e", "dd_t", "down") + d_bhd = B["hd_e"].values - B["hd_t"].values + P("\n" + "=" * 104) + P(" VERDETTO") + P("=" * 104) + P(f"\n (1) i meccanismi NON sono ridondanti: corr 0,795 (rendimenti) / {np.mean(offp):.3f} (posizioni)") + P(f" -> l'attesa a priori (A1) e' REFUTATA, il filone e' stato misurato fino in fondo") + P(f" (2) NON e' de-levering: k_isovol {k_vol:.3f}, esposizione ENS {ens_expo:.3f} > TSMOM " + f"{expo['TSMOM']:.3f}, tempo a mercato {tim_e:.0%} > {tim_t:.0%} -> (A2) REFUTATA, null senza potenza") + P(f" (3) PROTEZIONE — il compito dello sleeve: peggior rapporto {st['worst']:.2f} -> {se['worst']:.2f}, MA") + P(f" rapporto MEDIO {st['mean']:.2f} -> {se['mean']:.2f} (peggiora), {nb}/8 anni meglio = moneta,") + P(f" {cell_better}/{cell_tot} celle a favore di ENS = {cell_better/cell_tot:.0%} " + f"(TSMOM meglio nel {1-cell_better/cell_tot:.0%}), e nel 2022 " + f"(il sinistro piu' grande) ENS e' {y22e['ratio']/y22t['ratio']:.1f}x PEGGIORE") + P(f" (4) COSTO: Sharpe hold-out dello SLEEVE Δ appaiato {m_hd:+.3f} (favorevole {f_hd*24:.0f}/24) · " + f"maxDD Δ {m_dd:+.4f} ({f_dd*24:.0f}/24)") + P(f" COSTO SUL LIBRO: Sharpe hold-out Δ appaiato {np.median(d_bhd):+.3f}, " + f"favorevole {float(np.mean(d_bhd>0))*24:.0f}/24") + P(f" (5) il criterio che si voleva migliorare NON E' BINDING: TSMOM ha rapporto peggiore " + f"{A['pw_t'].max():.2f} su TUTTE le 24 ancore contro la soglia {PROTECT_MAX:.2f}") + P(f" (6) la MEMBERSHIP e' un parametro libero: il dichiarato e' al " + f"{float((SB['worst'] <= dich['worst']).mean())*100:.0f}° pctl di 31 sottoinsiemi sulla protezione") + ok = (m_pm < 0 and f_pm >= 0.75 and cell_better / cell_tot > 0.60 + and np.median(d_bhd) > -0.05 and f_dd >= 0.60) + P(f"\n ==> {'ENSEMBLE PROMOSSO A LEAD' if ok else 'REFUTED — TP01 resta un solo meccanismo, nessun cambio al libro'}") + P("\n CIO' CHE NON SI CONCLUDE: che avere un solo meccanismo sia sicuro. Il rischio di") + P(" monocultura resta reale e NON e' stato misurato qui (servirebbe un regime in cui il TSMOM") + P(" fallisce, e in 7,4 anni non c'e'). Si conclude che QUESTO ensemble non compra protezione:") + P(" compra TEMPO A MERCATO (55% -> 70%) a pari vol, e lo paga nei crash e sull'hold-out.") + P("\n IL FATTO TRASFERIBILE: mediare meccanismi di trend NON diversifica il rischio di trend.") + P(" Cinque segnali che salgono e scendono con lo stesso prezzo si accendono in disaccordo solo") + P(" ai BORDI del trend — cioe' proprio nei ribassi — e la media li tiene mezzi-lunghi mentre il") + P(" singolo e' gia' flat. Piu' meccanismi = ingresso/uscita piu' morbidi, non piu' assicurazione.") + P(f"\n runtime {time.time()-t0:.0f}s") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From af19892c20012d6cdc5e4e05aa1bfd09d1fbb433 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 01:49:37 +0000 Subject: [PATCH 079/114] =?UTF-8?q?CLAUDE.md:=20il=20costo=20della=20caten?= =?UTF-8?q?a=20USDC=20era=20+90%=20e=20sono=20+18%=20=E2=80=94=20il=20587?= =?UTF-8?q?=20non=20contava=20il=20filtro=20OI=20che=20il=20collettore=20a?= =?UTF-8?q?pplica?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4d4773e..1e67af9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3033,8 +3033,16 @@ df = load_data("BTC", "1h") # OK. load_data("SOL", ...) -> FileNotFoundError ( ⚠️ La famiglia raccolta è quella **inverse** (`get_instruments?currency=BTC|ETH`): la superficie **USDC-lineare non è nell'archivio**, e ogni conclusione su di essa nel progetto è oggi un **controfattuale costruito sui mid inverse**, non una misura. Puntare il collettore anche su - `currency=USDC` costa **+587 chiamate/giro (+90%)** ed è una decisione sul rate-limit per-IP, che - ha già causato un guasto il 29/07. + `currency=USDC` costa ~~**+587 chiamate/giro (+90%)**~~ 🚨 **CORRETTO 2026-08-23 (§51): sono + +117 chiamate = +18%.** Il 587 era il conteggio **grezzo** (1.140 strumenti USDC ≤ 95g = +81%) + **senza il filtro OI≥100 che il collettore applica davvero**, e quel filtro taglia il 90% della + famiglia USDC: con gli stessi filtri del collettore vivo sono **113 strumenti** contro **649** + inverse (✅ verificato al venue da due percorsi indipendenti; replica esatta di §8 — su Deribit + USDC l'OI e la negoziabilità sono anti-correlati). Il giro passerebbe da 652 chiamate/163 s a + **769/192 s**, dentro la finestra del :25 e senza toccare `cron_book` al :07. Resta una decisione + sul rate-limit per-IP, che ha già causato un guasto il 29/07 — ma **di un ordine di grandezza + più piccola di come era stata scritta**, ed è un numero **di oggi** (cresce con la liquidità + USDC, va ri-misurato prima di accendere). **NON assorbito, e perché:** il motore credit-spread ETH (il progetto ha già la regola "niente short-vol da modello in deploy"), la GUI, kill switch/dead-man/audit (PythagorasGoal ha `venue_watch`/`edge_watch`/`monitor_health`/`fee_watch`), `dvol_history` (`fetch_dvol.py` ha From c7ec178499d85ed7b37533c0bf161e4c80487052 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 01:51:01 +0000 Subject: [PATCH 080/114] =?UTF-8?q?registro=20=C2=A749:=20TP01-TWIN=20REFU?= =?UTF-8?q?TED=20=E2=80=94=20l'ensemble=20protegge=20peggio=20nei=204=20an?= =?UTF-8?q?ni=20recenti=20a=200/24=20ancore,=20e=20i=20meccanismi=20non=20?= =?UTF-8?q?erano=20ridondanti?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 83 ++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index ef80f9b..54ece7d 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -714,7 +714,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 46 | TAIL-HEDGE | comprare put deep-OTM come assicurazione statica compra DD a prezzo migliore del de-levering? | _in corso_ | | 47 | ANCHOR-ENSEMBLE | eseguire l'ensemble delle 24 ancore invece di sceglierne una e correggere il numero | _in corso_ | | 48 | VOLVOL | la volatilita' del DVOL (secondo momento, mai usato) contiene cio' che livello e spread non contengono? | **SCARTATO** — e' davvero una variabile NUOVA (ortogonalita' PASS, max \|corr\| 0,396) ma **guarda INDIETRO**: picco a lag **−5/−10** e parziale con la vol futura **NEGATIVA**. Quinto lato del filone DVOL chiuso | -| 49 | TP01-TWIN | il 75% del libro dipende da UNA definizione di trend: un ensemble di meccanismi protegge meglio? | _in corso_ | +| 49 | TP01-TWIN | il 75% del libro dipende da UNA definizione di trend: un ensemble di meccanismi protegge meglio? | **REFUTED** — protegge **PEGGIO** nei 4 anni piu' recenti a **0/24 ancore** (2022: **3,2x peggio**) e costa **−0,127** di Sharpe hold-out al libro. E i meccanismi **non erano ridondanti**: corr di posizione 0,61 | | 50 | BOOK-3RD | qual e' il miglior TERZO sleeve realmente eseguibile sul libro live a $635 — e a quale capitale cambia? | _in corso_ | | 51 | NEWDATA-SCOUT | se la scorta e' esaurita: cosa iniziare a raccogliere OGGI, che fra 12-24 mesi apra una famiglia? | **RACCOGLIERE 2 FONTI SU 10** — catena USDC (**+18%, non +90%**) e una **campagna a termine** di book depth; il criterio di ricostruibilita' ne uccide 4 da solo e **refuta una raccomandazione del 22/08** | | 52 | SKEPTIC-SPOT | il lead dello spot (+1,55-1,66%/anno) sopravvive a un attacco deliberato? | _in corso_ | @@ -2724,3 +2724,84 @@ e' un uso refutato."* **VERDETTO: `E' UNA VARIABILE NUOVA E GUARDA INDIETRO`** — SCARTATO, **quinto lato del filone DVOL chiuso**. **Libro, pesi, cron, config INVARIATI.** + +--- + +## 49 — TP01-TWIN (il 75% del libro dipende da una sola definizione di trend) + +`scripts/research/r0823_tp01_twin.py` (**8 s**). ✅ Replica bit-exact vs `sleeves._tp01_returns()`: +**max|diff| = 0.0**. **53 trial** contati al rialzo (22 celle + 31 composizioni). + +❌ **L'ATTESA A PRIORI DEL COORDINATORE E' REFUTATA, ed e' il motivo per cui il filone valeva.** +Avevo scritto *"i meccanismi di trend a 1d correlano 0,85-0,95, l'ensemble sara' TP01 con piu' fee"*. +Misurato — rendimenti netti / **posizioni-bersaglio** (dove vive davvero la ridondanza): + +| | TSMOM | DON | EWMA | KEL | KAU | +|---|---|---|---|---|---| +| **TSMOM** | 1.000 | 0.852 / **0.700** | 0.929 / **0.859** | 0.714 / **0.510** | 0.767 / **0.527** | +| DON | | 1.000 | 0.892 / 0.775 | 0.696 / 0.464 | 0.759 / 0.517 | +| EWMA | | | 1.000 | 0.696 / 0.440 | 0.791 / 0.528 | +| KEL | | | | 1.000 | 0.855 / 0.758 | + +Media fuori diagonale **0,795 (rendimenti) / 0,608 (posizioni)**. Keltner e Kaufman stanno a +**0,51-0,53** di posizione da TSMOM: **meccanismi genuinamente diversi, non TP01 travestito** → il +filone **non si chiudeva sulla matrice** ed e' stato misurato fino in fondo. + +🚨 **LA PROTEZIONE — il criterio (B) di `edge_watch.py`, importato dal sorgente di produzione.** +La colonna che decide non e' l'ancora canonica ma la frazione di ancore: + +| anno | DD b&h | rapp. TSMOM | rapp. ENS | ancore in cui ENS e' meglio | +|---|---|---|---|---| +| 2019 | 56,4% | 0,170 | 0,172 | 14/24 | +| 2020 | 59,2% | 0,124 | 0,114 | 18/24 | +| 2021 | 52,0% | 0,130 | 0,121 | 18/24 | +| **2022** | **68,4%** | **0,039** | **0,129** | **0/24** | +| 2023 | 22,0% | 0,531 | 0,486 | 23/24 | +| **2024** | 35,6% | 0,222 | 0,345 | **0/24** | +| **2025** | 45,1% | 0,166 | 0,209 | **0/24** | +| **2026** | 46,6% | 0,025 | 0,108 | **0/24** | + +**Dove l'ensemble perde, perde a TUTTE le ancore** — degradazione unanime, non fortuna d'ancora — e +i quattro anni in cui perde sono i **quattro piu' recenti**. Nel **2022, il sinistro maggiore, e' +3,2x peggiore**. Rapporto **medio** 0,18 → 0,21 (**0/24** favorevoli); celle ancora×anno a favore +**73/192 = 38%**. + +⚠️ **Errore dell'agente catturato in sessione, e del tipo che avrebbe cambiato il verdetto:** la +prima stesura decideva sul **caso peggiore** (max degli 8 rapporti), che migliora 0,56 → 0,49, e +avrebbe stampato **"PROMOSSO A LEAD"**. *Il massimo di 8 numeri non e' una statistica*: si era mosso +perche' era migliorato **un** anno (il 2023, che deteneva il massimo) mentre **4 su 8** peggioravano. + +📌 **Il null del de-levering non e' "superato": e' SENZA POTENZA, e va detto cosi'.** +`k_isovol = 0,9896 ≈ 1` — **non c'e' niente da de-levere**: vol 12,14% contro 12,02%, esposizione +media ENS **0,152 > 0,141**, tempo a mercato **70% > 55%**. ✅ E il rilevatore **ha** potenza: su un +de-levering vero (`target_vol=10%`) segnala Sharpe invariato 1,305 e maxDD 14,3% → 7,3%. +Aggiunto un gate su 31 sottoinsiemi: **0/30 composizioni alternative** batte TSMOM da solo su +protezione **E** hold-out insieme. + +**Banda d'ancora (24, differenze APPAIATE):** Sharpe FULL +0,0177 (14/24) · **Sharpe HOLD-OUT +−0,0671 (7/24)** · maxDD +0,0164 (**2/24**) · peggior 3 mesi **+0,0113 (24/24)** · protezione media +**+0,0305 (0/24)**. **Sul libro 75/25** (la gamba SKH e' identica nei due bracci → la sua ancora si +cancella): **Sharpe hold-out Δ −0,127, favorevole 3/24**; FULL +0,0001; maxDD −0,004; peggior 3 mesi ++0,014 (24/24). +✅ **Onesta' verso l'ensemble:** l'ancora canonica h=0 e' la **migliore delle 24** per l'hold-out di +TSMOM (**100° pctl** — replica indipendente della fortuna d'ancora del 02/07 e 26/07), quindi il +divario a h=0 (−0,37) e' **gonfiato**: il numero da citare e' **−0,067**. + +**I costi non sono il problema:** turnover **5,3/anno contro 8,0** di TSMOM (la nettatura cancella +turnover), a 0,20% RT 1,252 contro 1,274, haircut a $635 **−0,002**. `study_family_honest`: cella +in-sample-only = **TSMOM**, marginale **REDUNDANT**, `earns_slot_honest=False` — con DSR 0,999 +**dichiarato non decisivo** (famiglia omogenea ⇒ sr0 piccolo per costruzione). + +📌 **E il criterio che si voleva migliorare NON E' BINDING:** TSMOM ha rapporto peggiore **0,635 +contro la soglia 0,75 in 192 celle su 192**. `weights_tilt_null` non si applica (il vettore dei pesi +e' identico, cambia il contenuto del 75%) ed e' detto esplicitamente nello script. + +📌 **IL FATTO TRASFERIBILE: mediare meccanismi di trend NON diversifica il rischio di trend.** +Cinque segnali guidati dallo **stesso prezzo** divergono solo ai **bordi** del trend — cioe' nei +ribassi — e la media li tiene **mezzi-lunghi mentre il singolo e' gia' flat**. Piu' meccanismi = +ingresso/uscita piu' morbidi (peggior 3 mesi migliore **24/24**), **non** piu' assicurazione. +⚠️ **Cio' che NON si conclude:** che la monocultura sia sicura. Servirebbe un regime in cui il TSMOM +fallisce, e in 7,4 anni non c'e'. + +**VERDETTO: `MEDIARE MECCANISMI DI TREND NON DIVERSIFICA IL RISCHIO DI TREND — PROTEGGE PEGGIO +PROPRIO NEI RIBASSI`** — REFUTED. **Libro, pesi, cron, config INVARIATI.** From 5700280f652fe34f13b7c68dbc012f2068f72639 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 01:57:56 +0000 Subject: [PATCH 081/114] =?UTF-8?q?research(tail-hedge):=20REFUTATO=20?= =?UTF-8?q?=E2=80=94=20la=20copertura=20di=20coda=20ALZA=20il=20maxDD=20de?= =?UTF-8?q?l=20libro,=200/162=20celle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §46. Prima misura del progetto sull'acquisto di put deep-OTM come assicurazione statica sul libro (TP01 75 / SKH01 25), senza delta-hedge e senza pretendere alpha. Motore prezzi RIUSATO da VRP01 (_bs_put/_strike_from_delta su DVOL reale). Griglia dichiarata prima: 3 delta x 3 tenori x 3 budget x 2 roll = 54 celle per lente di f (162 in tutto). VERDETTO: il null del de-levering non arriva nemmeno a essere il gate che decide — la premessa cade prima. Il maxDD SCENDE in 0/54 celle a OGNI lente di f (compresa f=1.00, cioe' regalando alla copertura il prezzo di modello): 9,42% -> 9,53-10,37%. Meccanismo: il bleed del premio cade DENTRO i drawdown, che su questo libro sono lunghi e poco profondi, mentre il payoff cade su singoli giorni di crollo che NON sono il fondo del drawdown. Beta del libro al sottostante +0,076 (il 19/05/2021 il mercato ha fatto -21,1% e il libro -2,0%): la put protegge una perdita gia' ridotta di ~10x dalla strategia stessa, quindi coprirla costerebbe ~1/beta volte il budget. ATTESA A PRIORI, riportata anche dove sbagliata: (c) REFUTATA nella forma scritta — 15/20 dei peggiori giorni SONO crolli, e lo short-squeeze che avevo in mente sta fuori finestra; (b) e (a) confermate, e (b) e' il meccanismo operativo. f MISURATO OGGI sulle quote vere (420 put con bid E ask, catena USDC): 1,89 / 1,27 / 1,06 a delta -0,05 / -0,10 / -0,15 — piu' mite del 2,23-5,85 del 30/07, e il verdetto non cambia. Struttura del modello che vale come risultato: f si paga solo sulla parte di valore che converge a INTRINSECO, quindi un roll anticipato non lo paga (sensitivita' con f asimmetrico riportata). ESEGUIBILITA': corregge un muro ereditato. Comprare un'opzione costa il PREMIO, non il NOZIONALE — i "lotti da migliaia di dollari" del 22/08 sono il collaterale per VENDERE. Il lotto minimo BTC_USDC (0,01) costa $1,79-7,93; la copertura diventa comprabile a ogni roll da ~$9,2k (30g, delta -0,05) a ~$38,8k (7g, delta -0,15). Secondo muro, STRUTTURALE: il tick da 5 USDC — piu' la copertura e' deep-OTM (cioe' economica) piu' il tick domina. CANALE FUNDED: sull'unica regola binding (max-loss 6% statico) la copertura va nel verso sbagliato — alza il maxDD e quindi abbassa la leva ammessa (k 0,629 -> 0,594-0,620). DUE ERRORI MIEI CATTURATI PRIMA DI PUBBLICARE, entrambi dal controllo e non a occhio: (1) il controllo positivo "payoff gratis" FALLIVA perche' non accreditavo il valore dell'asset regalato, mentre il MTM successivo ne addebitava il decadimento -> il free lunch costava quanto il premio. Un controllo positivo rotto dichiara guasto l'apparato. (2) la prova di causalita' dava 1,28e-04 ("DIVERGE"): confrontavo fino al taglio, ma il prefisso si ferma un roll prima -> differiva per ASSENZA di un roll, non per look-ahead. Finestra corretta: 0,00e+00 esatto. (3) e un errore di CRITERIO: contavo "vittoria" anche dove il maxDD PEGGIORA, dove il null del de-levering e' degenere (k=1) e un Δdrift>0 risponde a una domanda di alpha, non a questa. Col criterio corretto le "3/54 vittorie" a f=1 diventano 0/54. Controlli 3/3 OK (free lunch e f=0,1 riconosciuti, f=10 rifiutato). Causalita' esatta. Lente wick accoppiata riusata da r0725_prop_coupled, copertura 100% del pannello. Distorsioni dichiarate: finestra 2021-03+ (il DVOL non esiste prima, quindi marzo 2020 e' FUORI CAMPIONE); spread misurato sui soli strumenti con bid E ask = pavimento che favorisce la copertura; 3-5 breach in 5,4 anni non distinguono le varianti. Libro, pesi, cron, config INVARIATI. Nessun ordine. --- scripts/research/r0823_tail_hedge.py | 881 +++++++++++++++++++++++++++ 1 file changed, 881 insertions(+) create mode 100644 scripts/research/r0823_tail_hedge.py diff --git a/scripts/research/r0823_tail_hedge.py b/scripts/research/r0823_tail_hedge.py new file mode 100644 index 0000000..9cd00e8 --- /dev/null +++ b/scripts/research/r0823_tail_hedge.py @@ -0,0 +1,881 @@ +"""r0823_tail_hedge — §46 TAIL-HEDGE: comprare put deep-OTM come ASSICURAZIONE STATICA sul libro. + +LA DOMANDA (decidibile, e NON e' una domanda di alpha) + Il progetto ha gia' testato la VENDITA di volatilita' (VRP01, gated su IV-rank), il GAMMA + SCALPING (long-vol delta-hedgiato, SCARTATO: perde ogni anno) e le strutture Albimarini. + Non ha mai testato la cosa piu' semplice: **comprare put deep-OTM e tenerle**, senza + delta-hedge, senza pretendere alpha. E' un riduttore di DRAWDOWN, e il drawdown e' il vincolo + binding su due assi gia' misurati: (a) il canale funded (daily-loss 4%, max-loss 6%), (b) la + rendita perpetua, che vive sul DD (il libro rende quanto un ETF S&P e serve 2,5x meno capitale + solo perche' perde meno). + Domanda: **comprare protezione di coda compra riduzione di DD a un prezzo migliore di quello + che si ottiene semplicemente DE-LEVERANDO?** + +=========================== ATTESA A PRIORI (scritta PRIMA di misurare) =========================== +Mi aspetto **REFUTATO al f realistico**, e forse marginale a f=1. Tre ragioni, in ordine di forza: + (c) **Si sta comprando assicurazione contro il rischio sbagliato.** Il 22/08 ha misurato che il + peggior giorno del libro NON e' un crash: e' uno SHORT SQUEEZE di SKH01 (short con stop 2% + che gappa a -15,77% di sleeve mentre TP01 e' flat). Una PUT non protegge da un rialzo. + (b) **Il libro e' GIA' difensivo dove la put paga.** L'esposizione di TP01 scende MONOTONA col + crollo (0,096 -> 0,042 sotto il -20%, misurato 22/08) perche' li' il trend si e' gia' girato: + la put paga proprio quando il libro non ha piu' esposizione da proteggere. + (a) **Comprare vol e' pagare il VRP**, cioe' l'esatto specchio di cio' che VRP01 incassa e che il + gamma scalping ha gia' pagato perdendo ogni anno, ogni variante, ogni frequenza. +Previsione quantitativa dichiarata: la riduzione di DD ci sara' (la put paga davvero nei crolli), +ma il de-levering la comprera' a meno; e il costo salira' ~linearmente col budget mentre il +beneficio saturera'. +================================================================================================== + +IL GATE CHE DECIDE — NULL DEL DE-LEVERING (6 occorrenze nel progetto: VRP-DD, TP01xDVOL, MAT01, +azioni intere UCITS, SOL, TP01-SIZE). Si trova il `k` costante per cui `k*libro` ha lo STESSO +maxDD della variante coperta, e si confronta Sharpe e DRIFT a quel punto. + ⚠️ Corollario 22/08: quando la variante e' una pura ri-scalatura il null e' DEGENERE + (`sh(k*base) === sh(base)`, identita' aritmetica). Qui NON lo e' **sul drift**: il drift + geometrico di `k*base` cambia con k, mentre lo Sharpe no. Quindi il confronto ha DUE gambe: + * Sharpe: Sharpe(coperto) vs Sharpe(base) [ = Sharpe(k*base), identicamente ] + * Drift: drift(coperto) vs drift(k*base) [ questa e' la gamba informativa ] + La non-linearita' della put e' cio' che rende la PRIMA gamba non-degenere (la copertura puo' + davvero alzare lo Sharpe, una ri-scalatura no). + +COSA C'E' DENTRO + 1) Motore prezzi RIUSATO (`src.portfolio.sleeves._bs_put` / `_strike_from_delta`, gli stessi di + VRP01) su DVOL reale. Premio **MODELLATO**, non quotato: dichiarato come tale. + 2) f MISURATO OGGI sulle quote vere della catena Deribit (BTC_USDC + inverse), sulle ali che + questa strategia comprerebbe — non assunto dal 30/07. + 3) Griglia dichiarata prima: delta {-0.05,-0.10,-0.15} x tenore {7,14,30} x budget + {0.5%,1%,2%}/anno x roll {scadenza, meta' vita} = **54 celle** per lente di f. + 4) Lente di CODA (non media): maxDD, 5 peggiori mesi, peggior giorno, breach 4%/6% con la + **lente wick accoppiata** (riusata da r0725_prop_coupled, non riscritta). + 5) Eseguibilita' misurata DAL VENUE (min_trade_amount + tick), non assunta. + 6) Controlli positivo e negativo obbligatori. + +CONVENZIONI DICHIARATE + * Libro = TP01 75 / SKH01 25 (il book live Deribit). Il costo si sottrae al DRIFT DEL LIBRO e il + beneficio si misura SUL LIBRO, non sullo sleeve. + * Copertura 50/50 BTC/ETH (l'esposizione del libro e' 50/50). + * **Mark-to-market GIORNALIERO** della put (BS a DVOL e spot correnti). E' essenziale: una + copertura marcata solo a scadenza sarebbe misurata male proprio sul DD, che e' l'unica cosa + che questo filone valuta. + * `f` applicato SIMMETRICAMENTE (si compra E si marca a f x modello). Conseguenza strutturale + che il modello cattura ed e' un risultato in se': **f si paga solo sulla parte di valore che + converge a INTRINSECO (la scadenza); un roll anticipato non lo paga.** Sensitivita' con f + asimmetrico (compri caro, vendi a modello) riportata a parte. + * Spread bid-ask MISURATO dalla catena vera per bucket di tenore, pagato a ogni compra e a ogni + vendita: senza, il roll a meta' vita vincerebbe per finzione. + * Fee opzioni Deribit: taker 0,03% del sottostante con cap 12,5% del premio; consegna ITM + 0,015% con lo stesso cap. + * Finestra: **2021-03-24 -> oggi** (il DVOL non esiste prima). Contiene il 2022. NON contiene + il crollo di marzo 2020, che e' il sinistro piu' grande della storia del libro: dichiarato. + +Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0823_tail_hedge.py` +""" +from __future__ import annotations + +import json +import math +import sys +import time +import urllib.request +from functools import lru_cache +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +from src.data.downloader import load_data # noqa: E402 +from src.strategies.trend_portfolio import resample_1d # noqa: E402 +# MOTORE PREZZI RIUSATO — gli stessi di VRP01, non una reimplementazione. +from src.portfolio.sleeves import ( # noqa: E402 + _bs_put, _strike_from_delta, _tp01_returns, _skyhook_returns, VRP_CFG, +) +import altlib as al # noqa: E402 +from scipy.stats import norm as _norm # noqa: E402 + +ASSETS = ("BTC", "ETH") +W_TP, W_SKH = 0.75, 0.25 # book live Deribit (sleeves.deribit_book_sleeves) +DPY = 365.25 + +# ---- costi opzioni Deribit (listino pubblico) ------------------------------------------------- +OPT_TAKER_UNDERLYING = 0.0003 # 0,03% del sottostante per gamba +OPT_FEE_CAP_FRAC = 0.125 # cap 12,5% del premio dell'opzione +OPT_DELIVERY_UNDERLYING = 0.00015 # 0,015% consegna ITM + +# ---- GRIGLIA DICHIARATA PRIMA DI GUARDARE ----------------------------------------------------- +G_DELTA = (-0.05, -0.10, -0.15) +G_TENOR = (7, 14, 30) +G_BUDGET = (0.005, 0.010, 0.020) # quota di equity spesa in premio, ANNUA +G_ROLL = ("expiry", "half") +N_CELLS = len(G_DELTA) * len(G_TENOR) * len(G_BUDGET) * len(G_ROLL) # = 54 + +# ---- lenti di f: 1.0 (modello nudo) + i due misurati il 30/07 + quello misurato OGGI ----------- +F_LENSES_STATIC = (1.0, 2.23, 5.85) + +DERIBIT = "https://www.deribit.com/api/v2/public/" + + +# ==================================================================== 0. UTIL METRICHE + +def _sh(x) -> float: + r = np.asarray(pd.Series(x).dropna().values, float) + return float(np.mean(r) / np.std(r) * math.sqrt(DPY)) if len(r) > 2 and np.std(r) > 0 else 0.0 + + +def _dd(x) -> float: + r = np.asarray(pd.Series(x).dropna().values, float) + eq = np.cumprod(1.0 + r) + pk = np.maximum.accumulate(eq) + return float(np.max((pk - eq) / pk)) if len(eq) else 0.0 + + +def _drift(x) -> float: + """Drift GEOMETRICO annualizzato (CAGR). E' la grandezza su cui vive la rendita perpetua, + ed e' l'unica delle due che CAMBIA quando si de-leva (lo Sharpe no: identita' aritmetica).""" + r = np.asarray(pd.Series(x).dropna().values, float) + if len(r) < 2: + return 0.0 + tot = float(np.prod(1.0 + r)) + if tot <= 0: + return -1.0 + return float(tot ** (DPY / len(r)) - 1.0) + + +def _dd_of_k(r: np.ndarray, k: float) -> float: + return _dd(k * r) + + +def k_for_same_drift(base: np.ndarray, target_drift: float) -> float: + """Il de-levering a PARI DRIFT. Serve sull'asse funded: se la copertura NON riduce il maxDD + (che e' cio' che questo filone misura), il confronto a pari DD e' degenere -> la domanda + onesta diventa 'a parita' di rendimento atteso, chi sfora meno la regola giornaliera?'. + drift(k*r) e' crescente in k su un libro a drift positivo.""" + lo, hi = 1e-4, 1.0 + if _drift(hi * base) <= target_drift: + return 1.0 + for _ in range(60): + mid = 0.5 * (lo + hi) + if _drift(mid * base) > target_drift: + hi = mid + else: + lo = mid + return 0.5 * (lo + hi) + + +def k_for_same_dd(base: np.ndarray, target_dd: float) -> float: + """Il `k` del NULL DEL DE-LEVERING: la scala costante che porta il maxDD del libro NUDO + esattamente al maxDD della variante coperta. maxDD(k*r) e' monotono crescente in k + (compounding), quindi bisezione.""" + lo, hi = 1e-4, 1.0 + if _dd_of_k(base, hi) <= target_dd: + return 1.0 # la copertura non riduce il DD: k=1 + for _ in range(60): + mid = 0.5 * (lo + hi) + if _dd_of_k(base, mid) > target_dd: + hi = mid + else: + lo = mid + return 0.5 * (lo + hi) + + +# ==================================================================== 1. DATI + +@lru_cache(maxsize=1) +def daily_panel() -> pd.DataFrame: + """Pannello giornaliero allineato: rendimenti del libro + prezzi/low/DVOL per asset. + Un solo indice, cosi' nessuna domanda successiva puo' disallinearsi.""" + tp = _tp01_returns() + skh = _skyhook_returns() + J = pd.concat({"tp": tp, "skh": skh}, axis=1, join="inner").fillna(0.0) + book = W_TP * J["tp"] + W_SKH * J["skh"] + + cols = {"book": book, "tp": J["tp"], "skh": J["skh"]} + for a in ASSETS: + d = resample_1d(load_data(a, "1h")) + idx = pd.DatetimeIndex(pd.to_datetime(d["datetime"], utc=True)) + for c in ("close", "low", "high"): + assert c in d.columns, f"resample_1d senza colonna {c}" + cols[f"px_{a}"] = pd.Series(d["close"].values.astype(float), index=idx) + cols[f"lo_{a}"] = pd.Series(d["low"].values.astype(float), index=idx) + dv = pd.read_parquet(ROOT / "data" / "raw" / f"dvol_{a.lower()}.parquet") + cols[f"iv_{a}"] = pd.Series(dv["close"].values.astype(float) / 100.0, + index=pd.to_datetime(dv["timestamp"], unit="ms", utc=True)) + P = pd.concat(cols, axis=1, join="inner").sort_index().dropna() + return P + + +# ==================================================================== 2. IL MOTORE DELLA COPERTURA + +def _entry_fee(prem_paid: float, S: float) -> float: + return min(OPT_TAKER_UNDERLYING * S, OPT_FEE_CAP_FRAC * prem_paid) if prem_paid > 0 else 0.0 + + +def hedge_leg(px: np.ndarray, lo: np.ndarray, iv: np.ndarray, *, + delta: float, tenor: int, budget_ann: float, roll: str, + f: float, spread: float, f_sell: float | None = None, + free: bool = False) -> tuple[np.ndarray, np.ndarray, dict]: + """Serie GIORNALIERE (ret, wick_extra) del contributo di UNA gamba di copertura, per 1 unita' + di equity allocata a quella gamba. + + ret[i] = variazione giornaliera del valore MTM della posizione (frazione di equity) + wick_extra[i] = guadagno AGGIUNTIVO della copertura al minimo intra-giornaliero, ottenuto + RIPREZZANDO la put al LOW del giorno (cattura la convessita', non solo il delta) + + CAUSALE per costruzione: strike, premio e quantita' a `t` usano solo px[t]/iv[t]; il payoff + usa prezzi successivi e non retroagisce mai sulla decisione. + + `free=True` -> CONTROLLO POSITIVO: il payoff c'e' ma il premio non viene addebitato. + """ + n = len(px) + T_full = tenor / DPY + hold = tenor if roll == "expiry" else max(1, tenor // 2) + n_rolls_year = DPY / hold + b_roll = budget_ann / n_rolls_year # spesa per roll, in frazione di equity + + ret = np.zeros(n) + wick = np.zeros(n) + diag = dict(n_rolls=0, n_itm=0, prem_spent=0.0, payoff_recv=0.0, fees=0.0, spread_cost=0.0) + + i = 60 # warm-up (coerente con VRP01) + while i + hold < n: + S0, sig = float(px[i]), float(iv[i]) + if not (S0 > 0 and sig > 0): + i += hold + continue + K = _strike_from_delta(S0, T_full, sig, delta) + p_model = _bs_put(S0, K, T_full, sig) + if p_model <= 0: + i += hold + continue + p_ask = f * p_model * (1.0 + spread) # si COMPRA al lato caro + fee0 = _entry_fee(p_ask, S0) + cost_unit = p_ask + fee0 + q = b_roll / cost_unit # unita' di sottostante coperte + # giorno d'ingresso: il valore MTM e' q*f*p_model (il mid), quindi si perde subito + # fee + mezzo spread. Se `free`, il premio non si paga affatto. + v_prev = q * f * p_model + # ⚠️ `free` = CONTROLLO POSITIVO (payoff regalato). La prima stesura metteva 0.0 qui e + # lasciava v_prev>0: cosi' il MTM successivo addebitava il decadimento di un valore mai + # accreditato -> il "free lunch" costava quanto il premio e il controllo FALLIVA. Il + # regalo va ACCREDITATO: l'equity sale di v_prev il giorno in cui ricevi l'asset. + ret[i] += (v_prev if free else (v_prev - b_roll)) + diag["n_rolls"] += 1 + diag["prem_spent"] += 0.0 if free else b_roll + diag["fees"] += q * fee0 + diag["spread_cost"] += q * f * p_model * spread + + for s in range(1, hold + 1): + j = i + s + rem = (tenor - s) / DPY + Sj, ivj = float(px[j]), float(iv[j]) + if s == tenor: # SCADENZA: valore = intrinseco + intr = max(0.0, K - Sj) + v = q * intr + if intr > 0: + dfee = min(OPT_DELIVERY_UNDERLYING * Sj, OPT_FEE_CAP_FRAC * intr) * q + v -= dfee + diag["fees"] += dfee + diag["n_itm"] += 1 + diag["payoff_recv"] += v + vw = v # a scadenza niente wick residuo + else: + fs = f if f_sell is None else f_sell + v_mid = q * f * _bs_put(Sj, K, rem, ivj) + v = v_mid + vw = q * f * _bs_put(float(lo[j]), K, rem, ivj) + if s == hold: # VENDITA anticipata: mezzo spread + v = q * fs * _bs_put(Sj, K, rem, ivj) * (1.0 - spread) + sfee = min(OPT_TAKER_UNDERLYING * Sj, OPT_FEE_CAP_FRAC * max(v, 0.0)) + v -= sfee + diag["fees"] += sfee + diag["spread_cost"] += v_mid * spread + diag["payoff_recv"] += v + wick[j] += vw - v_mid # EXTRA al minimo, contro il MID di chiusura + ret[j] += v - v_prev + v_prev = v + i += hold + + return ret, np.maximum(wick, 0.0), diag + + +@lru_cache(maxsize=None) +def hedge_series(delta: float, tenor: int, budget: float, roll: str, f: float, + spread: float, f_sell: float | None = None, + free: bool = False) -> tuple: + """Copertura del LIBRO: 50/50 BTC+ETH (il libro e' 50/50). Ritorna (ret, wick) giornalieri + come frazione dell'equity del libro.""" + P = daily_panel() + ret = np.zeros(len(P)) + wick = np.zeros(len(P)) + diags = {} + for a in ASSETS: + r, w, d = hedge_leg(P[f"px_{a}"].values, P[f"lo_{a}"].values, P[f"iv_{a}"].values, + delta=delta, tenor=tenor, budget_ann=budget * 0.5, roll=roll, + f=f, spread=spread, f_sell=f_sell, free=free) + ret += r + wick += w + diags[a] = d + return ret, wick, diags + + +# ==================================================================== 3. f e SPREAD DAL VENUE + +def _get(path: str, timeout: int = 25): + with urllib.request.urlopen(DERIBIT + path, timeout=timeout) as r: + return json.load(r)["result"] + + +def measure_f_from_venue() -> dict: + """f = quota MID di mercato / prezzo BS a DVOL piatto, MISURATO OGGI sulle ali che questa + strategia comprerebbe. Non e' un'assunzione ereditata dal 30/07: e' una misura su questa + catena, su questi strike, con questo motore. + + Si misura anche il MEZZO SPREAD relativo per bucket di tenore, che serve al roll anticipato. + Solo strumenti con bid E ask veri (un ask solitario al tick non e' un prezzo). + """ + out = dict(ok=False, note=None, per_delta={}, spread={}, n=0) + try: + idx = {a: float(_get(f"get_index_price?index_name={a.lower()}_usdc")["index_price"]) + for a in ASSETS} + dvol = {a: float(_get(f"get_volatility_index_data?currency={a}&start_timestamp=" + f"{int((time.time()-86400)*1000)}&end_timestamp={int(time.time()*1000)}" + f"&resolution=3600")["data"][-1][4]) / 100.0 + for a in ASSETS} + time.sleep(0.6) + bs = _get("get_book_summary_by_currency?currency=USDC&kind=option") + except Exception as e: # rete assente: si dichiara, non si finge + out["note"] = f"venue non raggiungibile ({type(e).__name__}: {e})" + return out + + now = pd.Timestamp.now(tz="UTC") + rows = [] + for r in bs: + nm = r["instrument_name"] + if not nm.endswith("-P"): + continue + base = nm.split("_USDC-")[0] if "_USDC-" in nm else None + if base not in ASSETS: + continue + try: + _, exp, ks, _ = nm.split("-") + K = float(ks.replace("d", ".")) + texp = pd.to_datetime(exp, format="%d%b%y", utc=True) + pd.Timedelta(hours=8) + except Exception: + continue + bid, ask = r.get("bid_price"), r.get("ask_price") + if not bid or not ask or bid <= 0 or ask <= bid: + continue # niente bid vero -> non e' un prezzo + T = (texp - now).total_seconds() / (365.25 * 86400) + if T <= 1 / DPY or T > 45 / DPY: + continue + S, sig = idx[base], dvol[base] + mid = 0.5 * (bid + ask) + half_spread = 0.5 * (ask - bid) / mid + model = _bs_put(S, K, T, sig) + if model <= 1e-9: + continue + # delta BS della put allo stesso vol piatto (per assegnare l'osservazione al suo bucket) + d1 = (math.log(S / K) + 0.5 * sig ** 2 * T) / (sig * math.sqrt(T)) + dlt = -float(_norm.cdf(-d1)) + rows.append(dict(asset=base, K=K, T=T, days=T * DPY, delta=dlt, + mid=mid, model=model, f=mid / model, hs=half_spread)) + if not rows: + out["note"] = "nessuno strumento con bid E ask veri nella finestra <=45g" + return out + D = pd.DataFrame(rows) + out["n"] = int(len(D)) + for dtgt in G_DELTA: + sel = D[(D.delta >= dtgt - 0.035) & (D.delta <= dtgt + 0.035)] + if len(sel) >= 3: + out["per_delta"][dtgt] = dict(n=int(len(sel)), f_med=float(sel.f.median()), + f_lo=float(sel.f.quantile(0.10)), + f_hi=float(sel.f.quantile(0.90)), + hs_med=float(sel.hs.median())) + for lab, (a, b) in dict(t7=(1, 10), t14=(10, 21), t30=(21, 45)).items(): + sel = D[(D.days >= a) & (D.days < b)] + if len(sel) >= 3: + out["spread"][lab] = dict(n=int(len(sel)), hs_med=float(sel.hs.median()), + hs_p90=float(sel.hs.quantile(0.90))) + out["ok"] = bool(out["per_delta"] or out["spread"]) + return out + + +def venue_min_ticket() -> dict: + """Eseguibilita' MISURATA dal venue: min_trade_amount + tick, e il costo in DOLLARI del + lotto minimo per una put deep-OTM vera. ⚠️ Comprare un'opzione costa il PREMIO, non il + NOZIONALE: il muro dei 'lotti da migliaia di dollari' del 22/08 e' il collaterale per + VENDERE, non il costo per COMPRARE. Qui si misura la cosa giusta.""" + out = dict(ok=False, note=None, fam={}) + try: + for cur, pref in (("USDC", "{a}_USDC-"), ("BTC", "BTC-"), ("ETH", "ETH-")): + inst = _get(f"get_instruments?currency={cur}&kind=option&expired=false") + time.sleep(0.6) + for a in ASSETS: + p = pref.format(a=a) + sel = [i for i in inst if i["instrument_name"].startswith(p)] + if not sel: + continue + fam = "USDC-lineare" if cur == "USDC" else "inverse" + out["fam"][(a, fam)] = dict(min_amt=float(sel[0]["min_trade_amount"]), + tick=float(sel[0]["tick_size"]), + settle=sel[0]["settlement_currency"], + n=len(sel)) + out["ok"] = bool(out["fam"]) + except Exception as e: + out["note"] = f"venue non raggiungibile ({type(e).__name__}: {e})" + return out + + +# ==================================================================== 4. LENTE WICK ACCOPPIATA + +@lru_cache(maxsize=1) +def coupled_book_tuples() -> pd.DataFrame: + """(R, m) giornaliere del libro 75/25 sotto la LENTE WICK ACCOPPIATA — RIUSATA da + r0725_prop_coupled (importata, non riscritta). Se il recon non e' disponibile si dichiara e + si ricade su close-only, DICENDO che close-only e' CIECA su una regola a UN giorno + (misurato 22/08: 0,00 breach/anno contro 0,40-1,21 veri, rapporto INF).""" + try: + import r0725_prop_coupled as pc + return pc.crypto_daily_tuples(W_TP, W_SKH) + except Exception as e: # dichiarato, mai silenzioso + print(f" ⚠️ lente accoppiata NON disponibile ({type(e).__name__}: {e}) -> close-only") + return pd.DataFrame() + + +# ==================================================================== 5. VALUTAZIONE DI UNA CELLA + +def evaluate(book: np.ndarray, hret: np.ndarray) -> dict: + hedged = book + hret + dd_h = _dd(hedged) + k = k_for_same_dd(book, dd_h) + base_k = k * book + dd_b = _dd(book) + dd_reduced = bool(dd_h < dd_b - 1e-12) + dr_h, dr_k = _drift(hedged), _drift(base_k) + return dict( + sh_base=_sh(book), sh_hedged=_sh(hedged), sh_delev=_sh(base_k), + dd_base=dd_b, dd_hedged=dd_h, dd_delev=_dd(base_k), + dr_base=_drift(book), dr_hedged=dr_h, dr_delev=dr_k, + k=k, hedged=hedged, dd_reduced=dd_reduced, + # VINCE solo se (i) il DD scende davvero E (ii) a quel DD il drift batte il de-levering. + # Senza (i) il null e' DEGENERE (k=1, dr_delev==dr_base) e un Δdrift>0 direbbe solo + # "la put ha guadagnato in questa finestra", che e' una domanda di ALPHA — non questa. + wins=bool(dd_reduced and dr_h > dr_k), + ) + + +def worst_months(r: pd.Series, n: int = 5) -> list: + m = (1.0 + r).resample("ME").prod() - 1.0 + return [(str(i.date())[:7], float(v)) for i, v in m.nsmallest(n).items()] + + +# ==================================================================== MAIN + +def main() -> None: + t0 = time.time() + print("=" * 100) + print("§46 TAIL-HEDGE — put deep-OTM come ASSICURAZIONE STATICA sul libro TP01 75 / SKH01 25") + print("=" * 100) + print(f"GRIGLIA DICHIARATA PRIMA DI GUARDARE: {len(G_DELTA)} delta x {len(G_TENOR)} tenori x " + f"{len(G_BUDGET)} budget x {len(G_ROLL)} roll = {N_CELLS} celle per lente di f.") + print("ATTESA A PRIORI: REFUTATO al f realistico. Ragione piu' forte (c): il peggior giorno del") + print(" libro NON e' un crash ma uno short-squeeze di SKH01 -> una PUT non protegge da un rialzo.") + + P = daily_panel() + book = P["book"].values + idx = P.index + bs_book = pd.Series(book, index=idx) + print(f"\nFinestra (comune libro+DVOL): {idx[0].date()} -> {idx[-1].date()} " + f"({len(P)} giorni, {len(P)/DPY:.1f} anni)") + print(f"LIBRO NUDO su questa finestra: Sharpe {_sh(book):.3f} | maxDD {_dd(book)*100:.2f}% | " + f"drift {_drift(book)*100:.2f}%/a") + print("⚠️ La finestra NON contiene marzo 2020 (il DVOL parte 2021-03): il sinistro piu' grande") + print(" della storia del libro e' FUORI CAMPIONE. La copertura non e' testata li'.") + + # ---------------------------------------------------------------- §A. f e spread dal venue + print("\n" + "=" * 100) + print("§A. f E SPREAD — MISURATI OGGI SULLE QUOTE VERE (non assunti dal 30/07)") + print("=" * 100) + fv = measure_f_from_venue() + if fv["ok"]: + print(f" {fv['n']} put con bid E ask veri, tenore <=45g, catena USDC-lineare.") + print(f" {'delta':>7} {'n':>4} {'f mediano':>10} {'f p10':>8} {'f p90':>8} {'mezzo-spread':>13}") + for d, v in sorted(fv["per_delta"].items(), reverse=True): + print(f" {d:>7.2f} {v['n']:>4} {v['f_med']:>10.2f} {v['f_lo']:>8.2f} " + f"{v['f_hi']:>8.2f} {v['hs_med']*100:>12.1f}%") + print(f" mezzo-spread per tenore: " + " ".join( + f"{k} {v['hs_med']*100:.1f}% (n={v['n']})" for k, v in fv["spread"].items())) + else: + print(f" NON MISURATO: {fv['note']}") + F_MEAS = {d: v["f_med"] for d, v in fv.get("per_delta", {}).items()} + if F_MEAS: + print(" -> f USATO nella lente 'misurato oggi': " + + " ".join(f"δ{d:.2f}={v:.2f}" for d, v in sorted(F_MEAS.items(), reverse=True))) + SPREAD = {} + for lab, ten in (("t7", 7), ("t14", 14), ("t30", 30)): + SPREAD[ten] = fv.get("spread", {}).get(lab, {}).get("hs_med", 0.10) + print(f" -> mezzo-spread usato per tenore: " + + " ".join(f"{t}g {SPREAD[t]*100:.1f}%" for t in G_TENOR) + + ("" if fv["ok"] else " (DEFAULT 10%: venue non letto)")) + print(" ⚠️ DISTORSIONE DICHIARATA, e va nel verso che FAVORISCE la copertura: lo spread e' il") + print(" mediano dei soli strumenti con bid E ask veri. Le ali piu' profonde e piu' corte") + print(" spesso NON hanno bid (solo un ask al tick) e cadono fuori dal campione: quelle") + print(" sarebbero le piu' care, e per un tail-hedge sono proprio quelle che si comprano.") + print(" Quindi il costo qui e' un PAVIMENTO, e il verdetto REFUTATO ne esce rafforzato.") + + # ---------------------------------------------------------------- §B. il null del de-levering + print("\n" + "=" * 100) + print("§B. NULL DEL DE-LEVERING — IL GATE CHE DECIDE (in cima, come si deve)") + print("=" * 100) + print(" Per ogni cella: k = scala che porta il libro NUDO allo STESSO maxDD della coperta.") + print(" Sharpe(k*base) == Sharpe(base) per identita' aritmetica -> la gamba informativa e' il") + print(" DRIFT. La copertura VINCE solo se drift(coperta) > drift(k*base) [e/o Sharpe sale].") + + lenses = [("f=1.00 (modello nudo)", 1.0)] + for f in F_LENSES_STATIC[1:]: + lenses.append((f"f={f:.2f} (misurato 30/07)", f)) + if F_MEAS: + lenses.append(("f MISURATO oggi per-delta", None)) + + all_rows = [] + for lab, fval in lenses: + print(f"\n ---- lente {lab} " + "-" * (78 - len(lab))) + print(f" {'delta':>6} {'ten':>4} {'budget':>7} {'roll':>7} | {'ShCop':>7} {'ShBase':>7} | " + f"{'DDcop':>7} {'DDbase':>7} | {'k':>6} {'DrCop':>8} {'DrDelev':>8} {'Δdrift':>8} | esito") + best = None + for dlt in G_DELTA: + f_use = fval if fval is not None else F_MEAS.get(dlt, F_LENSES_STATIC[1]) + for ten in G_TENOR: + for bud in G_BUDGET: + for roll in G_ROLL: + hr, hw, dg = hedge_series(dlt, ten, bud, roll, f_use, SPREAD[ten]) + ev = evaluate(book, hr) + gap = ev["dr_hedged"] - ev["dr_delev"] + win = ev["wins"] + row = dict(lens=lab, f=f_use, delta=dlt, tenor=ten, budget=bud, roll=roll, + **{k: v for k, v in ev.items() if k != "hedged"}, gap=gap, + hret=hr, hwick=hw, diag=dg) + all_rows.append(row) + if best is None or gap > best["gap"]: + best = row + if bud == 0.010 and roll == "expiry": # riga stampata: budget centrale + print(f" {dlt:>6.2f} {ten:>4} {bud*100:>6.1f}% {roll:>7} | " + f"{ev['sh_hedged']:>7.3f} {ev['sh_base']:>7.3f} | " + f"{ev['dd_hedged']*100:>6.2f}% {ev['dd_base']*100:>6.2f}% | " + f"{ev['k']:>6.3f} {ev['dr_hedged']*100:>7.2f}% " + f"{ev['dr_delev']*100:>7.2f}% {gap*100:>+7.2f}% | " + f"{'COPERTURA' if win else ('de-levering' if ev['dd_reduced'] else 'DD PEGGIORE')}") + sub = [r for r in all_rows if r["lens"] == lab] + nwin = sum(1 for r in sub if r["wins"]) + nred = sum(1 for r in sub if r["dd_reduced"]) + cand = [r for r in sub if r["dd_reduced"]] + best = max(cand or sub, key=lambda r: (r["gap"] if cand else -r["dd_hedged"])) + print(f" >> {lab}: il maxDD SCENDE in {nred}/{len(sub)} celle; fra quelle la copertura " + f"batte il de-levering in {nwin}/{len(sub)}.") + head = ("migliore fra le celle che RIDUCONO il DD" if cand else + "NESSUNA cella riduce il DD -> si riporta quella col DD MINORE (k=1, confronto " + "degenere)") + print(f" {head}: delta {best['delta']:.2f} / " + f"{best['tenor']}g / {best['budget']*100:.1f}% / {best['roll']} -> " + f"maxDD {best['dd_base']*100:.2f}% -> {best['dd_hedged']*100:.2f}% (k {best['k']:.3f}), " + f"Δdrift vs de-levering {best['gap']*100:+.2f} pp/a, " + f"ΔSharpe {best['sh_hedged']-best['sh_base']:+.3f}") + + # ---------------------------------------------------------------- §C. controlli + print("\n" + "=" * 100) + print("§C. CONTROLLI — un rilevatore che non segnala nulla puo' essere semplicemente rotto") + print("=" * 100) + ctrl = [] + # (+1) copertura GRATIS (payoff senza premio): DEVE ridurre il DD e battere il de-levering + hr, _, _ = hedge_series(-0.10, 30, 0.020, "expiry", 1.0, 0.0, free=True) + ev = evaluate(book, hr); ctrl.append(("(+) payoff SENZA premio (free lunch)", ev, True)) + # (+2) premio 1/10: stesso budget -> 10x le put. Speculare al negativo, DEVE funzionare + hr, _, _ = hedge_series(-0.10, 30, 0.020, "expiry", 0.1, 0.0) + ev = evaluate(book, hr); ctrl.append(("(+) premio 1/10 (f=0.1, 10x le put)", ev, True)) + # (-) premio 10x: DEVE essere refutata + hr, _, _ = hedge_series(-0.10, 30, 0.020, "expiry", 10.0, 0.0) + ev = evaluate(book, hr); ctrl.append(("(-) premio 10x (f=10)", ev, False)) + # (=) put ATM f=1, spread 0: NON e' un controllo (non deve funzionare per forza) ma e' il + # riferimento piu' caro possibile della famiglia: si stampa come misura, non come test. + hr, _, _ = hedge_series(-0.50, 30, 0.020, "expiry", 1.0, 0.0) + ev = evaluate(book, hr); ctrl.append(("(=) put ATM(δ-0.50) f=1 — misura, non controllo", ev, None)) + for nm, ev, must in ctrl: + gap = ev["dr_hedged"] - ev["dr_delev"] + got = "RICONOSCIUTA" if ev["wins"] else "refutata" + tag = "" if must is None else (" ✓" if ev["wins"] == must else " ✗ CONTROLLO FALLITO") + print(f" {nm:<48} DD {ev['dd_base']*100:5.2f}%->{ev['dd_hedged']*100:5.2f}% k {ev['k']:.3f} " + f"Δdrift {gap*100:+7.2f}pp ShΔ {ev['sh_hedged']-ev['sh_base']:+.3f} -> {got}{tag}") + ok = all(ev["wins"] == must for _, ev, must in ctrl if must is not None) + print(" APPARATO: " + ("controlli 3/3 OK — il rilevatore riconosce cio' che DEVE riconoscere " + "e rifiuta cio' che DEVE rifiutare" + if ok else + "\u26a0\ufe0f UN CONTROLLO E' FALLITO: i numeri sopra NON sono credibili")) + + # ---------------------------------------------------------------- §D. causalita' + print("\n" + "=" * 100) + print("§D. CAUSALITA' — prova esplicita, non un gate generico") + print("=" * 100) + print(" `altlib.causality_ok` vuole un target_fn(df)->posizioni: NON si applica a un overlay") + print(" di opzioni (non e' una posizione continua sul sottostante). Al suo posto la prova") + print(" diretta e piu' forte: si ricalcola la copertura su un PREFISSO del pannello e si") + print(" verifica che coincida con la coda della serie completa. Un leak divergerebbe.") + hr_full, _, _ = hedge_series(-0.10, 14, 0.010, "expiry", 1.0, SPREAD[14]) + cut = int(len(P) * 0.85) + Pf = daily_panel() + sub = Pf.iloc[:cut] + hr_cut, _, _ = hedge_leg(sub["px_BTC"].values, sub["lo_BTC"].values, sub["iv_BTC"].values, + delta=-0.10, tenor=14, budget_ann=0.010 * 0.5, roll="expiry", + f=1.0, spread=SPREAD[14]) + hr_full_btc, _, _ = hedge_leg(Pf["px_BTC"].values, Pf["lo_BTC"].values, Pf["iv_BTC"].values, + delta=-0.10, tenor=14, budget_ann=0.010 * 0.5, roll="expiry", + f=1.0, spread=SPREAD[14]) + # ⚠️ ERRORE MIO, catturato dalla prima corsa (dmax 1.28e-04 = "DIVERGE"): confrontavo fino a + # `cut`, ma il prefisso si ferma un roll prima (`while i + hold < n`), quindi l'ultimo ciclo + # non e' CALCOLATO nel troncato mentre lo e' nel completo. La differenza era per ASSENZA di + # un roll, non per look-ahead. Il confronto onesto finisce prima dell'ultimo roll possibile. + hold = 14 + stop = cut - 2 * hold + tail = 200 + dmax = float(np.max(np.abs(hr_cut[stop - tail:stop] - hr_full_btc[stop - tail:stop]))) + dmax_naive = float(np.max(np.abs(hr_cut[cut - tail:cut] - hr_full_btc[cut - tail:cut]))) + print(f" max|differenza| su [{stop-tail}:{stop}] (cut a {cut}/{len(P)}, ultimo roll escluso): " + f"{dmax:.2e} -> {'CAUSALE' if dmax < 1e-12 else '⚠️ DIVERGE'}") + print(f" (fino a `cut` fa {dmax_naive:.2e}: NON e' un leak, e' il roll che il prefisso non ha") + print(f" ancora eseguito — la finestra sbagliata dichiarava un difetto inesistente.)") + + # ---------------------------------------------------------------- §E. lente di coda + print("\n" + "=" * 100) + print("§E. LENTE DI CODA — maxDD, peggiori mesi, peggior giorno, breach 4%/6%") + print("=" * 100) + CP = coupled_book_tuples() + if not CP.empty: + CP = CP[(CP.index >= idx[0]) & (CP.index <= idx[-1])] + cov = len(CP.index.intersection(idx)) / len(idx) + print(f" lente ACCOPPIATA disponibile: {len(CP)} giorni, copertura {cov*100:.1f}% del pannello") + # scelta della cella per la lente di coda: la MIGLIORE al f misurato (o 2.23), budget 1% + f_pick = F_MEAS.get(-0.10, F_LENSES_STATIC[1]) + cells_tail = [("NUDO", None)] + for dlt in G_DELTA: + cells_tail.append((f"δ{dlt:.2f} 30g 1%/a expiry", (dlt, 30, 0.010, "expiry", + F_MEAS.get(dlt, f_pick)))) + cells_tail.append(("δ-0.10 30g 2%/a expiry", (-0.10, 30, 0.020, "expiry", f_pick))) + cells_tail.append(("δ-0.10 30g 2%/a f=1 (irreale)", (-0.10, 30, 0.020, "expiry", 1.0))) + + def _breach(series_close: np.ndarray, wick_extra: np.ndarray) -> tuple: + s_ = pd.Series(series_close, index=idx) + n_yr = len(P) / DPY + br_c = float((s_ <= -0.04).sum()) / n_yr + if CP.empty: + return float("nan"), br_c + m = CP["m"].reindex(idx).values + n_ev = int(np.nansum((m + np.nan_to_num(wick_extra)) <= -0.04)) + _breach.last_n = n_ev + return n_ev / n_yr, br_c + + print(f"\n {'variante':<32} {'maxDD':>7} {'pegg.giorno':>12} {'breach 4% /a':>14} " + f"{'(close-only)':>13} {'drift':>8} {'Sharpe':>7}") + for nm, spec in cells_tail: + if spec is None: + hr = np.zeros(len(P)); hw = np.zeros(len(P)) + else: + dlt, ten, bud, roll, fu = spec + hr, hw, _ = hedge_series(dlt, ten, bud, roll, fu, SPREAD[ten]) + hed = book + hr + s = pd.Series(hed, index=idx) + worst_day = float(s.min()) + br_coup, br_close = _breach(hed, hr + hw) # il minimo del libro + la copertura + n_ev = getattr(_breach, "last_n", -1) + print(f" {nm:<32} {_dd(hed)*100:>6.2f}% {worst_day*100:>11.2f}% {br_coup:>8.2f}({n_ev:>2}) " + f"{br_close:>12.2f} {_drift(hed)*100:>7.2f}% {_sh(hed):>7.3f}") + if spec is not None: + # ⚠️ il confronto a pari DD e' DEGENERE qui (il DD non scende, quindi k=1). Sull'asse + # funded la domanda onesta e' l'altra: a PARI DRIFT, chi sfora meno la regola a un + # giorno? Cioe' basta rimpicciolire la posizione per comprare gli stessi breach in meno. + kk = k_for_same_drift(book, _drift(hed)) + brk, brkc = _breach(kk * book, np.zeros(len(P)) if CP.empty + else (kk - 1.0) * np.nan_to_num(CP['m'].reindex(idx).values)) + lab_ = " ^ de-levering k=%.3f, PARI DRIFT" % kk + print(f" {lab_:<32} {_dd(kk*book)*100:>6.2f}% " + f"{float(np.min(kk*book))*100:>11.2f}% {brk:>8.2f}({getattr(_breach,'last_n',-1):>2}) " + f"{brkc:>12.2f} " + f"{_drift(kk*book)*100:>7.2f}% {_sh(kk*book):>7.3f}") + print(" ⚠️ close-only e' CIECA su una regola a UN giorno (22/08: 0,00 contro 0,40-1,21 veri).") + print(" La colonna che decide e' quella ACCOPPIATA. Fra parentesi il CONTEGGIO di eventi:") + print(" 3-5 breach in 5,4 anni NON distinguono le varianti fra loro (un evento in piu' o in") + print(" meno sposta il tasso del 20-30%) -> si legge il SEGNO, non la taglia. Il fatto") + print(" robusto e' che il de-levering a PARI DRIFT compra lo stesso ordine di grandezza di") + print(" riduzione dei breach senza toccare il maxDD nel verso sbagliato.") + print("\n REGOLA MAX-LOSS 6% (statica dal saldo iniziale, HyroTrader — e' la regola BINDING") + print(" del canale funded, gia' modellata il 25/07 come EV_DD=0.06):") + k6 = k_for_same_dd(book, 0.06) + print(f" libro NUDO maxDD {_dd(book)*100:5.2f}% -> SFORA (serve k={k6:.3f} per " + f"stare a 6%, drift {_drift(k6*book)*100:.2f}%/a)") + for nm, spec in cells_tail[1:]: + dlt, ten, bud, roll, fu = spec + hr2, _, _ = hedge_series(dlt, ten, bud, roll, fu, SPREAD[ten]) + hd = book + hr2 + k6h = k_for_same_dd(hd, 0.06) + print(f" {nm:<20} maxDD {_dd(hd)*100:5.2f}% -> SFORA (serve k={k6h:.3f}, " + f"drift {_drift(k6h*hd)*100:.2f}%/a)") + print(" >> Sull'UNICA regola che decide un conto funded la copertura va nel verso SBAGLIATO:") + print(" alza il maxDD e quindi ABBASSA la leva ammessa. Il beneficio sui breach a un") + print(" giorno (0,92 -> 0,55/anno) e' reale ma si compra piu' a buon mercato de-levando,") + print(" che sulla regola a 6 punti e' l'unica leva che funziona.") + + print("\n 5 PEGGIORI MESI (nudo vs coperto δ-0.10 30g 1%/a, f misurato):") + hr, _, _ = hedge_series(-0.10, 30, 0.010, "expiry", F_MEAS.get(-0.10, f_pick), SPREAD[30]) + wm_b = worst_months(bs_book) + wm_h = worst_months(pd.Series(book + hr, index=idx)) + mh = dict(wm_h) + hs_m = (1.0 + pd.Series(book + hr, index=idx)).resample("ME").prod() - 1.0 + hs_m.index = [str(i.date())[:7] for i in hs_m.index] + for mth, v in wm_b: + hv = float(hs_m.get(mth, float("nan"))) + print(f" {mth} nudo {v*100:>7.2f}% coperto {hv*100:>7.2f}% " + f"delta {(hv - v)*100:>+6.2f}pp") + + # ---------------------------------------------------------------- §F. il rischio sbagliato + print("\n" + "=" * 100) + print("§F. L'ATTESA (c) MISURATA — la put protegge dal rischio che il libro ha davvero?") + print("=" * 100) + und = 0.5 * (P["px_BTC"].pct_change() + P["px_ETH"].pct_change()).fillna(0.0).values + ordr = np.argsort(book)[:20] + n_crash = int((und[ordr] <= -0.05).sum()) + print(f" Sui 20 PEGGIORI giorni del libro: {n_crash}/20 sono giorni di crollo del sottostante") + print(f" (BTC/ETH medio <= -5%). Negli altri {20-n_crash} il libro perde mentre il mercato NON") + print(f" crolla -> una put non puo' proteggere per costruzione.") + print(f" {'giorno':>12} {'libro':>8} {'BTC/ETH':>9} {'TP01':>8} {'SKH01':>8}") + for j in ordr[:8]: + print(f" {str(idx[j].date()):>12} {book[j]*100:>7.2f}% {und[j]*100:>8.2f}% " + f"{P['tp'].values[j]*100:>7.2f}% {P['skh'].values[j]*100:>7.2f}%") + beta = float(np.polyfit(und, book, 1)[0]) + corr = float(np.corrcoef(book, und)[0, 1]) + print(f" corr(libro, sottostante) = {corr:+.3f} | beta del libro al sottostante = {beta:+.3f}") + print() + print(" --- VERDETTO SULL'ATTESA A PRIORI (si riporta anche dove e' REFUTATA) ---") + if n_crash >= 12: + print(f" (c) REFUTATA nella forma in cui l'ho scritta: {n_crash}/20 dei peggiori giorni del") + print( " libro SONO giorni di crollo. Non e' vero che si compra assicurazione contro il") + print( " rischio sbagliato in senso di DIREZIONE. ⚠️ E il caso che avevo in mente (lo") + print( " short-squeeze di SKH01) sta FUORI FINESTRA: il DVOL parte 2021-03.") + else: + print(f" (c) CONFERMATA: solo {n_crash}/20 dei peggiori giorni sono crolli.") + print(f" (b) CONFERMATA, ed e' il meccanismo operativo: beta del libro al sottostante {beta:+.3f}.") + print( " Il 2021-05-19 il sottostante ha fatto -21,1% e il libro -2,0%: la strategia aveva") + print( " GIA' ridotto la perdita di ~10x. La put paga in proporzione al crollo del") + print( " SOTTOSTANTE, ma protegge una perdita gia' ridotta di un ordine di grandezza ->") + print( " per coprire un DD del libro serve un nozionale di put ~1/beta volte l'equity, e") + print( " quel nozionale costa ~1/beta volte il budget.") + print( " (a) CONFERMATA: vedi il decadimento del premio nel §B (il costo cresce col budget e") + print( " col f mentre il beneficio satura).") + + # ---------------------------------------------------------------- §G. eseguibilita' + print("\n" + "=" * 100) + print("§G. ESEGUIBILITA' — misurata DAL VENUE. Comprare un'opzione costa il PREMIO, non il") + print(" NOZIONALE: il muro dei 'lotti da migliaia di dollari' e' il collaterale per VENDERE.") + print("=" * 100) + vm = venue_min_ticket() + if vm["ok"]: + print(f" {'asset':>6} {'famiglia':>14} {'min_trade_amount':>18} {'tick':>10} {'settle':>8}") + for (a, fam), v in sorted(vm["fam"].items()): + print(f" {a:>6} {fam:>14} {v['min_amt']:>18} {v['tick']:>10} {v['settle']:>8}") + else: + print(f" NON MISURATO: {vm['note']}") + # costo del lotto minimo con i premi modellati di fine campione + print(f"\n Costo del LOTTO MINIMO e capitale a cui la copertura diventa comprabile a ogni roll:") + print(f" {'delta':>6} {'ten':>4} {'premio/unita':>13} {'lotto min':>10} " + f"{'$ lotto':>9} {'budget 1%/roll a $635':>22} {'capitale minimo':>16}") + for dlt in G_DELTA: + for ten in (7, 30): + S = float(P["px_BTC"].values[-1]); sig = float(P["iv_BTC"].values[-1]) + T = ten / DPY + K = _strike_from_delta(S, T, sig, dlt) + pm = _bs_put(S, K, T, sig) + fu = F_MEAS.get(dlt, F_LENSES_STATIC[1]) + price = fu * pm + minamt = vm["fam"].get(("BTC", "USDC-lineare"), {}).get("min_amt", 0.01) + lot = minamt * price + per_roll_635 = 0.010 * (ten / DPY) * 0.5 * 635.0 # 1%/a, meta' su BTC + cap_min = lot / (0.010 * (ten / DPY) * 0.5) + print(f" {dlt:>6.2f} {ten:>4} {price:>13.2f} {minamt:>10} {lot:>9.2f} " + f"{per_roll_635:>21.2f} {cap_min:>15,.0f}") + print(" ⚠️ E c'e' un secondo muro, STRUTTURALE e non di capitale: il TICK. Su BTC_USDC il tick") + print(" e' 5 USDC -> una put che vale meno di $5 NON e' quotabile sotto $5. Piu' e' deep-OTM") + print(" (cioe' piu' e' economica la copertura) piu' il tick domina il prezzo.") + # eval_weights_smallcap sulla gamba LINEARE, come riferimento del pavimento + try: + from src.strategies.trend_portfolio import TrendPortfolio, CANONICAL + d1 = resample_1d(load_data("BTC", "1h")) + tpm = TrendPortfolio(**CANONICAL) + sc = al.eval_weights_smallcap(d1, tpm.target_series(d1), capital=635.0, min_order=5.0) + print(f"\n Riferimento (gamba LINEARE dello stesso libro): TP01/BTC a $635 haircut Sharpe " + f"{sc['sharpe_haircut']:+.3f} -> il pavimento $5 non morde sul perpetual.") + print(" La differenza NON e' il livello del costo ma la sua FORMA (regola 25/07): sul") + print(" perpetual il minimo e' in DOLLARI di nozionale, sull'opzione e' in UNITA' di") + print(" sottostante MOLTIPLICATE per un premio che tende a zero -> due muri diversi.") + except Exception as e: + print(f"\n (riferimento smallcap non calcolato: {type(e).__name__}: {e})") + + # ---------------------------------------------------------------- §H. gate residui + print("\n" + "=" * 100) + print("§H. GATE RESIDUI") + print("=" * 100) + # cella migliore in assoluto al f misurato/realistico + real = [r for r in all_rows if r["lens"] != "f=1.00 (modello nudo)"] + pool = real if real else all_rows + bestr = max(pool, key=lambda r: r["gap"]) + hedged_best = pd.Series(book + bestr["hret"], index=idx) + imp = al.implausible_sharpe(hedged_best) + print(f" implausible_sharpe(libro coperto, cella migliore al f realistico): " + f"implausible={imp['implausible']} Sharpe {imp.get('sharpe', float('nan')):.2f} " + f"maxDD {imp.get('maxdd', float('nan'))*100:.2f}%") + for r in imp["reasons"]: + print(f" - {r}") + sr_all = [r["sh_hedged"] for r in pool] + dsr, nullmax = al.deflated_sharpe(bestr["sh_hedged"], sr_all, hedged_best) + print(f" deflated_sharpe della cella MIGLIORE su {len(pool)} trial: DSR {dsr:.3f} " + f"(max atteso dal nullo {nullmax:.2f}) -> {'PASS' if dsr >= 0.95 else 'FAIL'}") + print(" ⚠️ Il DSR qui e' informativo ma NON e' il gate che decide: la domanda non e' 'questa") + print(" cella ha alpha' (non ne ha per costruzione) ma 'batte il de-levering' — ed e' il §B.") + + # sensitivita': f asimmetrico (compri caro, vendi a modello) + print("\n SENSITIVITA' f ASIMMETRICO (si compra a f, si vende a modello — caso pessimo per il") + print(" roll anticipato, che nel modello base non paga f perche' non arriva a intrinseco):") + fu = F_MEAS.get(-0.10, F_LENSES_STATIC[1]) + for roll in G_ROLL: + hs, _, _ = hedge_series(-0.10, 30, 0.010, roll, fu, SPREAD[30]) + ha, _, _ = hedge_series(-0.10, 30, 0.010, roll, fu, SPREAD[30], f_sell=1.0) + es, ea = evaluate(book, hs), evaluate(book, ha) + print(f" roll={roll:<7} f simmetrico Δdrift {(es['dr_hedged']-es['dr_delev'])*100:+6.2f}pp" + f" f asimmetrico Δdrift {(ea['dr_hedged']-ea['dr_delev'])*100:+6.2f}pp") + + # ---------------------------------------------------------------- §I. verdetto + print("\n" + "=" * 100) + print("§I. VERDETTO") + print("=" * 100) + for lab, _ in lenses: + sub = [r for r in all_rows if r["lens"] == lab] + nw = sum(1 for r in sub if r["wins"]) + nred = sum(1 for r in sub if r["dd_reduced"]) + cand = [r for r in sub if r["dd_reduced"]] or sub + bb = max(cand, key=lambda r: r["gap"]) + print(f" {lab:<32}: DD scende {nred:>2}/{len(sub)} | batte il de-levering {nw:>2}/{len(sub)}" + f" | miglior Δdrift {bb['gap']*100:+6.2f} pp/a | ΔSharpe {bb['sh_hedged']-bb['sh_base']:+.3f}") + nw_real = sum(1 for r in pool if r["wins"]) + nred_real = sum(1 for r in pool if r["dd_reduced"]) + verdict = "REFUTATO" if nw_real == 0 else ("PARZIALE" if nw_real < len(pool) * 0.5 else "REGGE") + print(f"\n >>> {verdict}: al f realistico il maxDD scende in {nred_real}/{len(pool)} celle e la") + print(f" copertura batte il de-levering in {nw_real}/{len(pool)}.") + print( " ⚠️ Il fatto che il maxDD SALGA nella maggior parte delle celle e' il risultato piu'") + print( " forte del filone: prima ancora del prezzo, la premessa 'la copertura riduce il") + print( " drawdown' e' FALSA su questo libro — il bleed del premio cade DENTRO i drawdown") + print( " (che sono lunghi e poco profondi) mentre il payoff cade su singoli giorni di") + print( " crollo che NON sono il fondo del drawdown.") + print(f"\n Tempo: {time.time()-t0:.0f}s") + + +if __name__ == "__main__": + main() From d84471e6f7be07ee4a39c6aadee7e7e1ec461cbe Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 01:59:43 +0000 Subject: [PATCH 082/114] =?UTF-8?q?registro=20=C2=A746:=20TAIL-HEDGE=20REF?= =?UTF-8?q?UTATO=20=E2=80=94=20il=20maxDD=20sale=20in=20162/162,=20e=20com?= =?UTF-8?q?prare=20un'opzione=20costa=20il=20premio=20non=20il=20nozionale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 73 ++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 54ece7d..6967882 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -711,7 +711,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 43 | DATA-UNUSED | quali dati il progetto possiede e nessuna strategia legge — e uno di essi sostiene un'ipotesi? | **SCARTATO** — **la scorta di dati e' esaurita**: un solo dato mai letto ha storia vera (flussi exchange CoinMetrics 2018-2026) e misurato sta **SOTTO il massimo atteso dal puro rumore** | | 44 | DEPOSIT-TIMING | un calendario di versamento CONDIZIONALE batte il piatto, a pari soldi? | **SCARTATO 0/12 in ogni lente** — il buy-the-dip compra davvero il **51% piu' in basso** e arriva al capitale-rendita nel **6,3%** dei casi contro l'**85,4%** del piatto | | 45 | SPOT-NETTING | separare TP01 e SKH01 su due strumenti: quanto vale, e il margine lo uccide? | **LEAD REGGE, SEPARAZIONE SENZA VALORE** — il margine non e' il vincolo (copertura **50x**, invariante al capitale), ma **T1 sbloccato e' un DECLASSAMENTO** (−0,048 contro il path che gira) e il netting perso **risparmia** commissioni: il lead vale **il funding e basta**, [+1,55%, +1,66%]/anno; a bloccarlo e' **il codice di sizing**, non Deribit | -| 46 | TAIL-HEDGE | comprare put deep-OTM come assicurazione statica compra DD a prezzo migliore del de-levering? | _in corso_ | +| 46 | TAIL-HEDGE | comprare put deep-OTM come assicurazione statica compra DD a prezzo migliore del de-levering? | **REFUTATO** — la premessa cade **prima del prezzo**: il maxDD **SALE in 162/162 celle**, a ogni lente di f (f=1,00 regalato compreso). Meccanismo: **beta del libro al sottostante +0,076** | | 47 | ANCHOR-ENSEMBLE | eseguire l'ensemble delle 24 ancore invece di sceglierne una e correggere il numero | _in corso_ | | 48 | VOLVOL | la volatilita' del DVOL (secondo momento, mai usato) contiene cio' che livello e spread non contengono? | **SCARTATO** — e' davvero una variabile NUOVA (ortogonalita' PASS, max \|corr\| 0,396) ma **guarda INDIETRO**: picco a lag **−5/−10** e parziale con la vol futura **NEGATIVA**. Quinto lato del filone DVOL chiuso | | 49 | TP01-TWIN | il 75% del libro dipende da UNA definizione di trend: un ensemble di meccanismi protegge meglio? | **REFUTED** — protegge **PEGGIO** nei 4 anni piu' recenti a **0/24 ancore** (2022: **3,2x peggio**) e costa **−0,127** di Sharpe hold-out al libro. E i meccanismi **non erano ridondanti**: corr di posizione 0,61 | @@ -2805,3 +2805,74 @@ fallisce, e in 7,4 anni non c'e'. **VERDETTO: `MEDIARE MECCANISMI DI TREND NON DIVERSIFICA IL RISCHIO DI TREND — PROTEGGE PEGGIO PROPRIO NEI RIBASSI`** — REFUTED. **Libro, pesi, cron, config INVARIATI.** + +--- + +## 46 — TAIL-HEDGE (put deep-OTM come assicurazione statica) + +`scripts/research/r0823_tail_hedge.py`. **Libro, pesi, cron, config INVARIATI. Nessun ordine.** + +🚨 **Il null del de-levering non arriva nemmeno a essere il gate che decide: la premessa cade +prima.** + +| lente di f | maxDD scende | batte il de-levering | miglior Δdrift | ΔSharpe | +|---|---|---|---|---| +| **f=1,00** (modello nudo, **regalato**) | **0/54** | 0/54 | +0,07 pp/a | −0,108 | +| f=2,23 (misura 30/07) | **0/54** | 0/54 | −0,00 pp/a | −0,004 | +| f=5,85 (misura 30/07) | **0/54** | 0/54 | +0,02 pp/a | −0,032 | +| **f misurato oggi** (1,89/1,27/1,06) | **0/54** | 0/54 | −0,01 pp/a | −0,004 | + +Libro nudo (2021-03→oggi, finestra DVOL): Sharpe 1,356 / maxDD **9,42%** / drift 13,75%. Coperto: +maxDD **9,53-10,37%**. **Non serve trovare il `k`**: il claim *"meno drawdown"* e' falso al primo +passo, e dove il DD **peggiora** il null e' degenere (k=1) — un Δdrift>0 li' risponde a una domanda +di *alpha*, non a questa. + +📌 **IL MECCANISMO, ed e' il risultato trasferibile: il beta del libro al sottostante e' +0,076.** +Il 19/05/2021 il mercato ha fatto **−21,1%** e il libro **−2,0%**. *La put protegge una perdita gia' +ridotta ~10x dalla strategia stessa*, quindi coprirla costerebbe ~1/beta volte il budget. E il bleed +del premio cade **dentro** i drawdown — che su questo libro sono **lunghi e poco profondi** — mentre +il payoff cade su singoli giorni di crollo **che non sono il fondo del drawdown**: sui 5 peggiori +mesi la copertura peggiora **tutti e cinque**. +❌ **Meta' dell'attesa a priori dell'agente refutata:** 15/20 dei peggiori giorni del libro **sono** +crolli (lo short-squeeze di SKH01 sta fuori finestra). Regge l'altra meta', ed e' il beta. + +**f misurato oggi sulle quote vere** (420 put con bid **E** ask, catena USDC): δ−0,05 → **1,89** · +δ−0,10 → **1,27** · δ−0,15 → **1,06** — piu' mite del 2,23-5,85 del 30/07, **e il verdetto non +cambia**. 📌 Struttura del modello che vale come risultato a se': **f si paga solo sulla parte di +valore che converge a intrinseco**, quindi un roll anticipato non lo paga (asimmetria misurata: +expiry −0,27pp invariato, half −0,19 → −0,39pp). + +🚨 **CORREGGE UN MURO PUBBLICATO: comprare un'opzione costa il PREMIO, non il NOZIONALE.** I +*"lotti da migliaia di dollari"* del 22/08 sono il **collaterale per VENDERE**. Lotto minimo +BTC_USDC = **$1,79-7,93** di premio; la copertura e' comprabile a ogni roll da **~$9,2k** (30g +δ−0,05) a **~$38,8k** (7g δ−0,15). ✅ **Verificato al venue dal coordinatore:** `min_trade_amount` +**0,01** su tutte le 343 put BTC_USDC, e il premio del lotto minimo di una put a 5 giorni va da +**$0,00** a qualche dollaro. +📌 **Il secondo muro e' strutturale e non di capitale: il TICK da 5 USDC.** ✅ Verificato +(`tick_size = 5.0` su tutte). **Piu' la copertura e' deep-OTM — cioe' economica — piu' il tick +domina**: una put marcata 0,82 USDC non e' quotabile sotto 5. E' un pavimento di prezzo del **6x** +sulle ali piu' lontane, e nessun budget lo aggira. + +**Canale funded:** sulla regola binding (max-loss 6% **statico**) la copertura va **nel verso +sbagliato** — alza il maxDD, quindi **abbassa** la leva ammessa (k 0,629 → 0,594-0,620). Il +beneficio sui breach a un giorno (0,92 → 0,55/anno) e' reale, ma il de-levering **a pari drift** lo +compra allo stesso ordine di grandezza **senza toccare il maxDD**. + +⚠️ **Tre errori dell'agente, tutti catturati dai controlli e non a occhio.** (i) Il controllo +positivo *"payoff gratis"* **falliva**: non accreditava il valore dell'asset regalato mentre il MTM +ne addebitava il decadimento → il free lunch costava quanto il premio. **Un controllo positivo rotto +dichiara guasto l'apparato.** (ii) La causalita' dava 1,28e-04 ("DIVERGE") perche' il confronto +arrivava fino al taglio mentre il prefisso si ferma **un roll prima**: differiva per **assenza di un +roll**, non per look-ahead → finestra corretta, **0,00e+00 esatto**. (iii) Errore di **criterio**: +contava "vittoria" anche dove il maxDD peggiora — col criterio corretto le *"3/54 vittorie a f=1"* +diventano **0/54**. Controlli finali **3/3** (free-lunch e f=0,1 riconosciuti, f=10 rifiutato). + +⚠️ **Non misurato, dichiarato:** **marzo 2020 e' FUORI CAMPIONE** (il DVOL parte 2021-03) — *il +sinistro piu' grande della storia del libro non e' testato, ed e' l'unico scenario in cui la +conclusione potrebbe muoversi*. Lo spread viene dai soli strumenti con bid **E** ask veri: e' un +**pavimento che FAVORISCE la copertura** (le ali piu' profonde spesso hanno solo un ask al tick). I +3-5 breach in 5,4 anni **non distinguono le varianti fra loro**: si legge il segno, non la taglia. +Nessuna copertura **dinamica** (gated su regime) e' stata provata: la domanda era la statica. + +**VERDETTO: `LA PUT PROTEGGE UNA PERDITA GIA' RIDOTTA 10x DALLA STRATEGIA — IL maxDD SALE IN 162 +CELLE SU 162`** — REFUTATO. From c4514fc2c7e2dccf5d10fae9b9a24f3730529972 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 02:00:33 +0000 Subject: [PATCH 083/114] =?UTF-8?q?CLAUDE.md:=20il=20muro=20delle=20opzion?= =?UTF-8?q?i=20valeva=20per=20VENDERE=20=E2=80=94=20comprare=20costa=20il?= =?UTF-8?q?=20premio,=20e=20il=20muro=20vero=20e'=20il=20tick=20da=205=20U?= =?UTF-8?q?SDC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1e67af9..6045433 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2177,7 +2177,16 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis REGGE** (e' sull'asse della rovina, non dell'eseguibilita'). (b) *"slippage = rischio #1 di XSR01"* -> **refutato con margine 21x**. (c) *"BTC opzioni fuori a $3.000"* -> misurato sulla famiglia **inverse**, che un conto **USDC non puo' marginare**; la famiglia USDC-lineare ha lotti **10x piu' - piccoli** (ETH_USDC **$242**, BTC_USDC $772 contro $7.744). Il muro resta **per il prezzo del + piccoli** (ETH_USDC **$242**, BTC_USDC $772 contro $7.744). ~~Il muro resta **per il prezzo + del lotto**.~~ 🚨 **CORRETTO 2026-08-23 (§46): quelle cifre sono il COLLATERALE PER VENDERE. + COMPRARE un'opzione costa il PREMIO, non il nozionale** — `min_trade_amount` **0,01** su tutte le + 343 put BTC_USDC (✅ verificato al venue) e il premio del lotto minimo di una put deep-OTM va da + **$0,00 a pochi dollari** ⇒ **sul lato LONG il muro di capitale non esiste** (e la regola *niente + short-vol da modello* non ha mai avuto bisogno di un muro di taglia per reggere). + 📌 **Il muro vero sul lato long e' un altro, ed e' STRUTTURALE: il tick da 5,00 USDC** (✅ + verificato, `tick_size = 5.0` su tutte) — **piu' l'opzione e' deep-OTM, cioe' economica, piu' il + tick domina**: una put marcata 0,82 USDC non e' quotabile sotto 5, cioe' un pavimento di prezzo + del **6x** sulle ali lontane, che nessun capitale aggira. lotto**. Parametri veri Hyperliquid: **min order $10** (non $5), **taker 4,50 bps** (non 5,0). 📌 **(3) LA LEVA E' L'UNICA VARIABILE CHE NESSUN GATE DEL PROGETTO SA VEDERE.** Lo Sharpe e' **invariante alla scala** (1,31 a ogni cella del knob) -> `deflated_sharpe` e `marginal_vs_tp01` From 89ab19aed580730fd8a18f0861b9a9649d24a242 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 02:00:43 +0000 Subject: [PATCH 084/114] CLAUDE.md: rimosso il frammento di riga lasciato dalla correzione precedente --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6045433..132bb56 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2187,7 +2187,7 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis verificato, `tick_size = 5.0` su tutte) — **piu' l'opzione e' deep-OTM, cioe' economica, piu' il tick domina**: una put marcata 0,82 USDC non e' quotabile sotto 5, cioe' un pavimento di prezzo del **6x** sulle ali lontane, che nessun capitale aggira. - lotto**. Parametri veri Hyperliquid: **min order $10** (non $5), **taker 4,50 bps** (non 5,0). + Parametri veri Hyperliquid: **min order $10** (non $5), **taker 4,50 bps** (non 5,0). 📌 **(3) LA LEVA E' L'UNICA VARIABILE CHE NESSUN GATE DEL PROGETTO SA VEDERE.** Lo Sharpe e' **invariante alla scala** (1,31 a ogni cella del knob) -> `deflated_sharpe` e `marginal_vs_tp01` non falliscono, **non la vedono**. Il libro gira al **7% di Kelly** e raccoglie il **15%** della From 58395a594e4bf7e77f0ac098aa4d6201b8045fd9 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 02:03:55 +0000 Subject: [PATCH 085/114] =?UTF-8?q?research(=C2=A747):=20l'ensemble=20d'an?= =?UTF-8?q?cora=20e'=20ESEGUIBILE=20a=20ogni=20capitale=20=E2=80=94=20ma?= =?UTF-8?q?=20su=20TP01=20vale=20+0,014=20di=20Sharpe,=20e=20il=20segnale?= =?UTF-8?q?=20sta=20su=20SKH01?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VERDETTO: ESEGUIBILE MA INUTILE SU TP01 — nessun cambio al libro. La domanda mai chiusa: invece di de-luckare il NUMERO, si puo' de-luckare l'ESECUZIONE (N tranche, N ore, 1/N di size)? Tre risposte: (i) batte l'ancora MEDIANA: si', di +0,014 di Sharpe FULL. E l'algebra dice che non puo' fare di piu': la posizione dell'ensemble e' la MEDIA delle posizioni, il lordo orario e' lineare -> lordo_ens == media dei lordi (max|dif| 1.4e-17, verificato). Tutto il guadagno e' vol (-1,1%); il drift si muove di +0,013% = solo fee-netting. Cio' che compra davvero non e' nella colonna Sharpe: sd(ShFULL) 0,061 -> 0, sd(ShHOLD) 0,112 -> 0. (ii) batte la CANONICA che gira oggi: NO sull'hold-out (-0,223), SI su FULL (+0,024) e IS (+0,071). I due numeri non sono due stime della stessa cosa: +0,44 e' un'estrazione gia' avvenuta (98 pctl di 24), +0,21 e' cio' che si ottiene senza estrarre. La stima onesta del futuro e' +0,21. (iii) eseguibile da quale capitale: da TUTTI, gia' a $635. PREMESSA DEL 02/07 REFUTATA, CONCLUSIONE INTATTA. Il 02/07 boccio' il tranching perche' "i delta per-ancora (~$1-2) sono sotto il min-order $5". Ma `book.build_book_order` banda la posizione NETTA e manda UN ordine per asset per giro: un delta per-ancora non e' mai un ordine. Il tranching non moltiplica gli ordini per K, e la degenerazione non avviene (retention 0,88-1,06 a $635; a K=24 il path eseguito segue l'ideale MEGLIO che a K=1, corr 0,9986 vs 0,9954). Cio' che lo boccia e' la taglia dell'effetto, non l'esecuzione — e la ragione conta, perche' quella vecchia cadrebbe al primo esecutore che mandasse un ordine per tranche. FUNDING: canale chiuso per ALGEBRA, non per piccolezza. funding = pos*f e' lineare in pos -> funding_ens == media esatta (max|dif| = 0). Il rapporto condizionale 2,25x non si attenua (2,25 -> 2,26 da K=1 a K=24): la correlazione esposizione-funding vive alla scala del regime, non dell'ora. DOVE STA IL SEGNALE: SKH01, l'unica ancora a cui il 02/07 non porto' mai questa domanda. Sulla lente del path che gira, mediana delle 23 fasi ShFULL 0,974 -> ensemble 1,286 (+0,312), maxDD 23,3% -> 16,6%: 20x TP01, perche' i suoi trade sono DISCRETI (spostare la griglia cambia QUALI trade esistono). Dentro il libro pesa il 25% e li' non si distingue da zero. 23 e' PRIMO -> sulla griglia 30m non esiste sotto-ensemble simmetrico. Contro-intuitivo misurato: tranciando entrambi gli sleeve gli ORDINI salgono 6,5x (197 -> 1283/anno) ma le FEE SCENDONO ($5,76 -> $5,29/anno) — su un venue a fee proporzionale si paga il nozionale, e mediare 23 fasi trasforma un +-1,0x che sbatte in una posizione frazionaria. Su un venue a pavimento fisso il segno si ribalterebbe. BARRIERA (canale funded): [A7] refutata sulla regola misurabile e con un meccanismo — il lato binding non e' la barriera (-6%, P(breach) 0-7%) ma il BERSAGLIO (+10%): meno vol allontana dal traguardo quanto dalla barriera, e l'ensemble sta al 31 pctl su P(pass). MA la regola che il 22/08 ha misurato uccidere e' la daily-loss a UN giorno, che close-only non puo' vedere (cieca, non conservativa). Sul p1 giornaliero — cio' che quella regola legge — l'ensemble batte il 66% delle configurazioni. Dichiarato come indizio. COSTO VERO, e non e' negli ordini: 23 ancore su 24 di TP01 richiedono barre di oggi, cioe' `fresh_5m` — il path che il 26/07 ricade in silenzio sul certificato e che il 29/07 ha fallito 6 giri su 8. E' l'unica delle tre obiezioni del 02/07 che sopravvive intatta. Repliche prima di ogni delta: h=0 == tp01_baseline_daily; guardie di causalita' su tutte le ancore; `banded()` == `r07.smallcap_net` bit-exact (0.0e+00, stessi ordini); K=4 == EW di 4 book (2.2e-16 sul daily); offset 0 == `sleeves._skyhook_returns()` (0.0e+00). I livelli del 02/07 sono derivati col dato: dichiarato, non nascosto. Due errori miei catturati e congelati in commento: (a) confrontavo `turnover_per_year`, che eval_weights ARROTONDA, accanto a una disuguaglianza stretta -> sembrava violata; (b) i percentili di coda usavano un solo segno per tre colonne di cui due sono ritorni e una una frequenza -> stampavo 34 dove il valore vero e' 66, cioe' "peggio della mediana" per un numero migliore della mediana. E il criterio N_max SATURA (6/6 celle a ogni capitale): un gate che passa sempre non misura niente, e lo dice il codice. Book, pesi, ancore, cron, config INVARIATI. Nessuna proposta. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf --- scripts/research/r0823_anchor_ensemble.py | 1230 +++++++++++++++++++++ 1 file changed, 1230 insertions(+) create mode 100644 scripts/research/r0823_anchor_ensemble.py diff --git a/scripts/research/r0823_anchor_ensemble.py b/scripts/research/r0823_anchor_ensemble.py new file mode 100644 index 0000000..02e12c8 --- /dev/null +++ b/scripts/research/r0823_anchor_ensemble.py @@ -0,0 +1,1230 @@ +#!/usr/bin/env python +"""r0823_anchor_ensemble.py — §47 ANCHOR-ENSEMBLE: si puo' ESEGUIRE l'ensemble d'ancora? + +LA DOMANDA. Il progetto ha misurato con enorme cura la FORTUNA D'ANCORA (TP01 gira sull'ancora +daily 00:00 UTC, la migliore delle 24; SKH01 sull'offset 0 di 23; XS01 fase 0 di 10; VRP01 fase +0 di 7) e ha sempre risposto DE-LUCKANDO IL NUMERO — mai cambiando il modo di eseguire. Qui si +chiede l'altra cosa: invece di scegliere un'ancora e poi correggere la stima, si puo' dividere +lo sleeve in N tranche, ognuna ribilanciata a un'ora diversa e ognuna a 1/N della size? + +PRECEDENTE DA SUPERARE (non ignorare). Il 2026-07-02 (`r0702_tp01_offset.py`, diario +`2026-07-02-timing-crt-wave.md`) misuro' il tranching K=2/K=4 e concluse: *"sola riduzione della +varianza della STIMA (dSharpe n.s., dDD ~0.5pt), NO deploy a $600 — a $225/asset di quota TP01 +i delta per-ancora (~$1-2) sono sotto il min-order $5 -> degenererebbe comunque in K=1; serve +feed intraday fuori path certificato. Rivalutare a >=5-10k."* +Tre cose sono cambiate: (a) il FUNDING e' misurato (-2,16%/anno, 22/08) e il tranching cambia il +profilo orario d'esposizione; (b) la banda d'ancora del LIBRO e' misurata sullo spazio congiunto +(26/07); (c) il 22/08 ha misurato che il pavimento min-order non e' il vincolo che si credeva. + +E c'e' un quarto fatto, di CODICE, che nessuno aveva messo accanto alla conclusione del 02/07 — +si veda §4: `src/live/book.build_book_order` applica `abs(delta) < min_usd -> None` sulla +posizione **NETTA**, e manda **un solo ordine per asset per giro**. Il "delta per-ancora" non e' +mai un ordine. La premessa del 02/07 descrive un'esecuzione che il libro non fa. + +COSA MISURA, nelle tre parti chieste: + (i) l'ensemble batte l'ancora MEDIANA? di quanto su Sharpe E su DRIFT, e con che + compressione di dispersione fra realizzazioni; + (ii) l'ensemble batte l'ancora CANONICA che gira oggi? (attesa: NO in-sample) — e quale dei + due numeri e' la stima onesta del FUTURO; + (iii) a quale CAPITALE l'ensemble diventa eseguibile senza degenerare? curva N_max(capitale) + con il pavimento VERO del venue, per $635 / $2k / $5k / $20k / $50k. +piu': il costo (ordini/anno, fee/anno), il FUNDING (l'unico canale per cui l'ensemble potrebbe +GUADAGNARE invece di ridurre varianza), SKH01 separatamente e insieme a TP01, la replica del +02/07 PRIMA di ogni delta, e il valore della riduzione di varianza SOTTO UNA BARRIERA ASSORBENTE +(canale funded), che e' la domanda che resta se la risposta e' "non aggiunge ritorno". + +RIUSO (non riscrittura): `altlib` (eval_weights/_sh/_to_daily/_dd_ret/HOLDOUT), +`r0702_tp01_offset` (resample_offset/hourly_target/smallcap_net — la replica e' like-for-like), +`r0702_anchor_skh01` (run_asset/resample_off/OFFSETS), `r0822d_funding` (fetch/build_rate/cumF — +il funding NON viene rimodellato), `src.live.deribit._CONTRACT` (il pavimento si DERIVA dal +codice sorvegliato, non si ridichiara — regola `fee_watch` 21/08), +`src.portfolio.portfolio.combine_outer`. + +VINCOLI: nessun file toccato fuori da questo script; nessuna rete (cache funding gia' su disco); +niente commit di dati. Runtime ~5-8 min su 2 core. + +Uso: `nice -n 19 uv run python scripts/research/r0823_anchor_ensemble.py` +""" +from __future__ import annotations + +import sys +import time +from functools import lru_cache +from itertools import product +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +for _p in ("scripts/research", "scripts/research/alt", ""): + sys.path.insert(0, str(ROOT / _p)) + +import altlib as al # noqa: E402 +import r0702_anchor_skh01 as r02 # noqa: E402 +import r0702_tp01_offset as r07 # noqa: E402 +import r0822d_funding as fd # noqa: E402 +from src.live.deribit import _CONTRACT # noqa: E402 (il pavimento si DERIVA, non si ridichiara) +from src.portfolio.portfolio import combine_outer # noqa: E402 + +fd.USE_NET = False # cache su disco: questo filone non tocca la rete + +ASSETS = ("BTC", "ETH") +HOURS = tuple(range(24)) +HOLDOUT = al.HOLDOUT +FEE = al.FEE_SIDE # 0.05%/lato = 0.10% RT +MSH = 3_600_000 + +# Il libro live: `src/live/book.py` W_TP01=0.75, W_SKH=0.25, WEIGHT=0.5 (50/50 BTC+ETH). +W_TP, W_SKH, W_ASSET = 0.75, 0.25, 0.5 +MIN_ORDER_USD = 5.0 # config/live.json min_order_usd (letto sotto) +CAPITALI = (635.0, 2000.0, 5000.0, 20000.0, 50000.0) + +# famiglie di tranching a priori, simmetriche, zero tuning (stessa logica del 02/07) +FAM_TP = {1: 24, 2: 12, 3: 8, 4: 6, 6: 4, 8: 3, 12: 2, 24: 1} # K -> n. rotazioni + +T0 = time.time() + + +def sez(t: str) -> None: + print("\n" + "=" * 100) + print(f" {t}") + print("=" * 100) + + +def clock() -> str: + return f"[{time.time() - T0:5.0f}s]" + + +def fam_rotations(K: int) -> list[tuple]: + """Le TUTTE rotazioni della famiglia K (nessuna selezione): (s, s+24/K, s+2*24/K, ...).""" + step = 24 // K + return [tuple((s + step * j) % 24 for j in range(K)) for s in range(step)] + + +# =========================================================================== +# §0 — ATTESE A PRIORI (scritte e stampate PRIMA di qualunque misura) +# =========================================================================== +ATTESE = [ + ("A1", "ALGEBRA: la posizione dell'ensemble e' la MEDIA delle posizioni delle tranche, e il " + "lordo orario e' lineare nella posizione -> lordo_ens == media dei lordi, ESATTAMENTE " + "(max|dif| = 0). Il turnover invece e' SUB-additivo (|media dei delta| <= media dei " + "|delta|) -> turnover_ens <= turnover MEDIO delle ancore. Quindi l'ensemble domina " + "debolmente l'ancora MEDIA su drift E su vol: non e' 'solo' riduzione di varianza, ma " + "il pezzo di drift e' solo fee-netting, cioe' briciole."), + ("A2", "(i) l'ensemble batte l'ancora MEDIANA su Sharpe. Quasi per costruzione: stesso drift, " + "meno vol. Attesa dSharpe FULL fra +0,03 e +0,15; dDRIFT ~ 0 (<0,3pp)."), + ("A3", "(ii) l'ensemble NON batte l'ancora CANONICA sull'hold-out. La canonica sta al ~92-96° " + "pctl delle 24: una media non batte il massimo. Attesa: dSharpe HOLD NEGATIVO. " + "E la stima onesta del futuro e' comunque l'ensemble, non la canonica."), + ("A4", "FUNDING: l'ensemble NON puo' guadagnare un centesimo. Il funding pagato in un'ora e' " + "pos*f, ESATTAMENTE lineare in pos -> funding_ens == media dei funding delle ancore, " + "max|dif| = 0. Il rapporto condizionale mean(pos*f)/(mean(pos)*mean(f)) — misurato " + "1,86-2,55x il 22/08 — e' esso stesso una media e NON si attenua: la correlazione " + "esposizione-funding vive alla scala del REGIME, non dell'ora del giorno. " + "Il canale sperato e' chiuso per ALGEBRA, non per piccolezza dell'effetto."), + ("A5", "(iii) ESEGUIBILITA': gli ordini NON sono K volte tanti. `build_book_order` banda la " + "posizione NETTA e manda un ordine per asset per giro -> il numero di ordini e' " + "limitato dalla BANDA, non da K. La premessa del 02/07 ('i delta per-ancora sono sotto " + "il min-order') descrive un'esecuzione che il libro non fa. MA la conclusione del 02/07 " + "sopravvivera' comunque, per un'altra strada: a $635 la banda mangia gia' K=1 (il " + "ribilancio MEDIANO di TP01 e' sotto $5), quindi non c'e' K da degradare. " + "Attesa: N_max($635) = 1, e N_max cresce con il capitale."), + ("A6", "SKH01: peggiore rapporto costo/beneficio (4x piu' fee-sensibile, 22/08) e " + "690/30 = 23 e' PRIMO -> sulla griglia a 30m dichiarata il 02/07 non esiste alcun " + "sotto-ensemble simmetrico (o 1 o 23). Le famiglie simmetriche esistono solo sulla " + "griglia 5m (138 = 2*3*23 -> K in {2,3,6,23})."), + ("A7", "BARRIERA: la riduzione di varianza vale poco su un conto proprio (dove il nemico e' il " + "drift) e MOLTO su un conto a barriera assorbente. Attesa: P(pass) dell'ensemble >= " + "mediana delle P(pass) per ancora, e soprattutto SPARISCE la lotteria (la banda di " + "P(pass) fra ancore si comprime). E il conto funded e' a $100k, dove la banda del " + "punto (iii) NON morde: il posto dove l'ensemble e' inutile e quello dove e' " + "ineseguibile sono lo STESSO posto, e sono entrambi il conto da $635."), +] + + +def sezione0() -> None: + sez("§0 — ATTESE A PRIORI (dichiarate PRIMA di misurare; il verdetto su ciascuna e' in §9)") + for k, t in ATTESE: + print(f" [{k}] {t}\n") + print(" Criterio N_max DICHIARATO ORA, prima di guardare i numeri (§4):") + print(" l'ensemble serve a UNA cosa: comprimere la dispersione fra realizzazioni. Quindi") + print(" retention(K,C) = [sd_band(1,C) - sd_band(K,C)] / [sd_ideale(1) - sd_ideale(K)]") + print(" con sd = deviazione standard dello Sharpe FULL fra TUTTE le rotazioni della") + print(" famiglia K (nessuna selezione), banda = pavimento vero del venue a capitale C.") + print(" N_max(C) = il massimo K in {2,3,4,6,8,12} con retention >= 0.50.") + print(" K=24 ha UNA sola rotazione -> riportato a parte, fuori dal criterio.") + + +# =========================================================================== +# PRIMITIVE — target orari, ensemble, valutazione ideale e BANDATA +# =========================================================================== +@lru_cache(maxsize=8) +def px(asset: str) -> np.ndarray: + return r07.get1h(asset)["close"].values.astype(float) + + +@lru_cache(maxsize=8) +def ret(asset: str) -> np.ndarray: + return al.simple_returns(px(asset)) + + +@lru_cache(maxsize=8) +def idx1h(asset: str) -> pd.DatetimeIndex: + return pd.DatetimeIndex(pd.to_datetime(r07.get1h(asset)["datetime"], utc=True)) + + +@lru_cache(maxsize=256) +def tgt(asset: str, hs: tuple) -> np.ndarray: + """Target dell'ensemble sulle ancore `hs`: media dei target orari delle tranche. + `r07.hourly_target` e' RIUSATA (merge_asof backward su epoca ms esplicita).""" + return np.mean([np.asarray(r07.hourly_target(asset, h)) for h in hs], axis=0) + + +def eval_ideal(asset: str, hs: tuple) -> dict: + """Valutazione IDEALE (nessun pavimento): riusa al.eval_weights, poi decompone.""" + df = r07.get1h(asset) + ev = al.eval_weights(df, tgt(asset, hs), fee_side=FEE) + t = tgt(asset, hs) + pos = np.zeros(len(t)); pos[1:] = t[:-1] + gross = pos * ret(asset) + turn = np.abs(np.diff(pos, prepend=0.0)) + net = gross - FEE * turn; net[0] = 0.0 + assert np.max(np.abs(net - ev["net"])) < 1e-15, "decomposizione != eval_weights" + return dict(pos=pos, gross=gross, turn=turn, net=net, idx=ev["idx"], + turn_y=ev["turnover_per_year"]) + + +@lru_cache(maxsize=256) +def book_ideal(hs: tuple) -> dict: + """Book 50/50 BTC+ETH IDEALE sull'orario: serie oraria + daily + turnover.""" + E = {a: eval_ideal(a, hs) for a in ASSETS} + S = {a: pd.Series(E[a]["net"], index=E[a]["idx"]) for a in ASSETS} + G = {a: pd.Series(E[a]["gross"], index=E[a]["idx"]) for a in ASSETS} + P = {a: pd.Series(E[a]["pos"], index=E[a]["idx"]) for a in ASSETS} + J = pd.concat(S, axis=1, join="inner").fillna(0.0) + JG = pd.concat(G, axis=1, join="inner").fillna(0.0) + JP = pd.concat(P, axis=1, join="inner").fillna(0.0) + h_net = 0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]] + h_gross = 0.5 * JG[ASSETS[0]] + 0.5 * JG[ASSETS[1]] + return dict(h_net=h_net, h_gross=h_gross, pos=JP, daily=al._to_daily(h_net), + turn_y=sum(0.5 * E[a]["turn_y"] for a in ASSETS)) + + +def banded(prices: np.ndarray, target: np.ndarray, capital: float, + min_usd: float = MIN_ORDER_USD, step: float | None = None) -> dict: + """Path ESEGUITO sotto il pavimento del venue. + + Modella `src/live/book.build_book_order` + `deribit.notional_to_amount`: + - `abs(delta) < min_usd` -> nessun ordine (banda sul NOZIONALE, non sulla frazione); + - se `step` e' dato (contract step del perp lineare, in base-coin) il delta eseguito viene + quantizzato allo step e CLAMPATO al minimo -> la posizione atterra su una griglia. + `target[i]` e' la frazione di `capital` desiderata alla barra i (stessa convenzione di + `r07.smallcap_net`, contro cui questa funzione e' validata bit-exact in §1). + + Ottimizzazione: la condizione puo' scattare solo dove il target CAMBIA (fra due cambi il + target e' costante, quindi se non e' scattata non scattera'). Si itera sui punti di cambio. + """ + n = len(target) + chg = np.empty(n, dtype=bool) + chg[0] = True + chg[1:] = target[1:] != target[:-1] + ci = np.flatnonzero(chg) + vals = np.empty(len(ci)) + cur = 0.0 + n_ord = 0 + notion = 0.0 + for k in range(len(ci)): + i = ci[k] + d = (target[i] - cur) * capital + if abs(d) >= min_usd: + if step is None: + cur = target[i] + notion += abs(d) + else: + u = abs(d) / prices[i] + q = np.round(u / step) * step + if q < step: + q = step + ex = np.sign(d) * q * prices[i] + cur = cur + ex / capital + notion += abs(ex) + n_ord += 1 + vals[k] = cur + held = np.repeat(vals, np.diff(np.append(ci, n))) + return dict(held=held, n_ord=n_ord, notional=notion, n_chg=len(ci)) + + +def eval_banded(asset: str, hs: tuple, capital: float, step: float | None = None) -> dict: + """Book di UN asset sotto banda. `capital` = capitale ALLOCATO A QUELL'ASSET.""" + b = banded(px(asset), tgt(asset, hs), capital, MIN_ORDER_USD, step) + held = b["held"] + pos = np.zeros(len(held)); pos[1:] = held[:-1] + turn = np.abs(np.diff(pos, prepend=0.0)) + net = pos * ret(asset) - FEE * turn + net[0] = 0.0 + return dict(net=net, pos=pos, turn=turn, n_ord=b["n_ord"], n_chg=b["n_chg"], + notional=b["notional"], idx=idx1h(asset)) + + +def book_banded(hs: tuple, capital: float, step_by_asset: dict | None = None) -> dict: + """Book 50/50 sotto banda. `capital` = capitale TOTALE del book TP01 (meta' per asset).""" + E = {} + for a in ASSETS: + st = step_by_asset.get(a) if step_by_asset else None + E[a] = eval_banded(a, hs, capital * 0.5, st) + S = {a: pd.Series(E[a]["net"], index=E[a]["idx"]) for a in ASSETS} + J = pd.concat(S, axis=1, join="inner").fillna(0.0) + h = 0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]] + yrs = (h.index[-1] - h.index[0]) / pd.Timedelta("365.25D") + return dict(daily=al._to_daily(h), n_ord=sum(E[a]["n_ord"] for a in ASSETS), + ord_y=sum(E[a]["n_ord"] for a in ASSETS) / yrs, + fee_y=sum(FEE * E[a]["turn"].sum() for a in ASSETS) / 2 / yrs, + notional_y=sum(E[a]["notional"] for a in ASSETS) / yrs, E=E) + + +def drift(s: pd.Series) -> float: + """Drift annuo geometrico (CAGR) di una serie di ritorni giornalieri.""" + v = np.asarray(s.dropna().values, float) + eq = float(np.prod(1.0 + v)) + yrs = len(v) / 365.25 + return eq ** (1 / yrs) - 1 if eq > 0 and yrs > 0 else -1.0 + + +def vol(s: pd.Series) -> float: + return float(np.std(np.asarray(s.dropna().values, float)) * np.sqrt(365.25)) + + +def m3(s: pd.Series) -> tuple: + return (al._sh(s), al._sh(s[s.index < HOLDOUT]), al._sh(s[s.index >= HOLDOUT])) + + +def pctl_of(v: np.ndarray, x: float) -> float: + return float((v < x).mean() + 0.5 * (v == x).mean()) * 100 + + +# =========================================================================== +# §1 — REPLICA. Nessun delta prima che questa sezione passi. +# =========================================================================== +def sezione1() -> dict: + sez("§1 — REPLICA DI CONTROLLO (il 02/07 PRIMA di ogni delta) + pavimento vero del venue") + + r07.sanity_h0() + r07.causality_guards() # prefix-recompute daily + troncamento 1h, per TUTTE le ancore + + # (a) la funzione `banded` di questo script contro `r07.smallcap_net` (codice del 02/07) + print("\n (a) `banded()` di questo script vs `r07.smallcap_net` del 02/07 (bit-exact):") + for a in ASSETS: + df = r07.get1h(a) + t = 0.5 * tgt(a, (0,)) + ref, ntr = r07.smallcap_net(df, t, 600.0) + b = banded(px(a), t, 600.0, 5.0, None) + held = b["held"] + pos = np.zeros(len(held)); pos[1:] = held[:-1] + net = pos * ret(a) - FEE * np.abs(np.diff(pos, prepend=0.0)); net[0] = 0.0 + d = float(np.max(np.abs(net - ref.values))) + assert d == 0.0, f"banded != smallcap_net su {a}: {d:.3e}" + assert b["n_ord"] == ntr, f"n_ordini {b['n_ord']} != {ntr}" + print(f" {a}: max|dif| = {d:.1e} ordini {b['n_ord']} == {ntr} OK " + f"(punti di cambio del target: {b['n_chg']:,} -> la compressione e' esatta)") + + # (b) K=4 == EW di 4 book ancorati (il claim "diff 1.4e-17" del 02/07) + K4 = (0, 6, 12, 18) + ens = book_ideal(K4)["daily"] + ew = sum(book_ideal((h,))["h_net"] for h in K4) / 4.0 + ew_d = al._to_daily(ew) + J = pd.concat({"e": ens, "w": ew_d}, axis=1, join="inner") + d = float(np.max(np.abs(J["e"] - J["w"]))) + print(f"\n (b) K=4 == EW di 4 book ancorati (claim 02/07 'diff 1.4e-17'):") + print(f" sull'ORARIO la posizione e' identica per costruzione; sul DAILY il compounding") + print(f" intra-giorno non e' lineare -> max|dif| = {d:.2e} su {len(J):,} giorni.") + + # (c) i numeri del 02/07, con la deriva del dato dichiarata + singles = {h: book_ideal((h,))["daily"] for h in HOURS} + shf = np.array([al._sh(singles[h]) for h in HOURS]) + shi = np.array([al._sh(singles[h][singles[h].index < HOLDOUT]) for h in HOURS]) + shh = np.array([al._sh(singles[h][singles[h].index >= HOLDOUT]) for h in HOURS]) + dd = np.array([al._dd_ret(singles[h]) for h in HOURS]) + e4 = book_ideal(K4)["daily"] + rot4 = [book_ideal(hs)["daily"] for hs in fam_rotations(4)] + h4 = np.array([al._sh(s[s.index >= HOLDOUT]) for s in rot4]) + print(f"\n (c) i numeri pubblicati il 02/07, ri-misurati oggi (il dato e' cresciuto di ~7") + print(f" settimane e `data/raw/` non e' versionato — la deriva si dichiara, non si nasconde):") + print(f" {'grandezza':<44} {'02/07':>16} {'oggi':>16}") + print(f" {'HOLD mediano fra le 24 ancore':<44} {'0.04':>16} {np.median(shh):>16.3f}") + print(f" {'HOLD banda fra le 24 ancore':<44} {'[-0.13,+0.30]':>16} " + f"{'[%+.2f,%+.2f]' % (shh.min(), shh.max()):>16}") + print(f" {'HOLD canonico h=0':<44} {'0.31 (98 pctl)':>16} " + f"{'%.3f (%.0f pctl)' % (shh[0], pctl_of(shh, shh[0])):>16}") + print(f" {'IS h=0 -> K=4':<44} {'1.49 -> 1.56':>16} " + f"{'%.2f -> %.2f' % (shi[0], al._sh(e4[e4.index < HOLDOUT])):>16}") + print(f" {'HOLD banda a K=4 (6 rotazioni)':<44} {'[-0.01,+0.13]':>16} " + f"{'[%+.2f,%+.2f]' % (h4.min(), h4.max()):>16}") + print(f" {'maxDD mediano fra le singole':<44} {'12.6%':>16} {np.median(dd):>15.1%}") + print(f" {'turnover/anno del book':<44} {'~8.3':>16} " + f"{book_ideal(K4)['turn_y']:>16.1f}") + pk = float((h4[:, None] <= shh[0]).mean()) + print(f" {'P(K=4 <= h=0) sull hold-out':<44} {'0.18 n.s.':>16} {pk:>16.2f}") + print(" -> la STRUTTURA replica (K=4 == EW, turnover invariato, banda che si comprime);") + print(" i LIVELLI si sono mossi col dato. E' la deriva attesa, non un disaccordo.") + + # (d) il pavimento VERO del venue, derivato dal codice di produzione + print("\n (d) IL PAVIMENTO, derivato da `src/live/book` + `src/live/deribit` (non ridichiarato):") + import json as _json + cfg = _json.loads((ROOT / "config" / "live.json").read_text()) + mo = float(cfg["min_order_usd"]) + assert mo == MIN_ORDER_USD, f"config min_order_usd={mo} != {MIN_ORDER_USD} dichiarato qui" + steps = {} + for a in ASSETS: + inst = f"{a}_USDC-PERPETUAL" + steps[a] = float(_CONTRACT[inst]["step"]) + p_now = px(a)[-1] + print(f" {inst:<22} step {steps[a]:g} {a} -> a ${p_now:,.0f} il lotto vale " + f"${steps[a]*p_now:6.2f} (banda config ${mo:.0f})") + print(f" -> pavimento effettivo per ordine = max($5, quantizzazione al lotto).") + print(f" Su BTC il LOTTO e' piu' grosso della banda: e' lui a comandare.") + print(f" ⚠️ `build_book_order` banda `abs(delta)` sulla posizione NETTA e manda UN ordine") + print(f" per asset per giro. Questo e' il fatto che riapre la domanda del 02/07 (§4).") + print(f" {clock()}") + return dict(singles=singles, shf=shf, shi=shi, shh=shh, dd=dd, steps=steps) + + +# =========================================================================== +# §2 — (i) l'ensemble contro l'ancora MEDIANA +# =========================================================================== +def sezione2(S1: dict) -> dict: + sez("§2 — (i) L'ENSEMBLE CONTRO L'ANCORA MEDIANA (e l'algebra che lo governa)") + + # (a) il teorema A1, verificato numericamente sull'ORARIO + print(" (a) [A1] verifica ALGEBRICA sull'orario (non un'approssimazione: un'identita'):") + for a in ASSETS: + E24 = eval_ideal(a, HOURS) + g_mean = np.mean([eval_ideal(a, (h,))["gross"] for h in HOURS], axis=0) + t_mean = np.mean([eval_ideal(a, (h,))["turn"] for h in HOURS], axis=0) + dg = float(np.max(np.abs(E24["gross"] - g_mean))) + te, tm = float(E24["turn"].sum()), float(t_mean.sum()) + print(f" {a}: max|lordo_ens - media(lordi)| = {dg:.2e} -> IDENTITA'") + print(f" turnover TOTALE ensemble {te:.4f} vs MEDIA delle ancore {tm:.4f} " + f"-> sub-additivita' {'OK' if te <= tm + 1e-9 else 'VIOLATA'} " + f"({te/tm-1:+.3%})") + # ⚠️ ERRORE CATTURATO IN SESSIONE: la prima stesura confrontava `turnover_per_year`, che + # `al.eval_weights` ARROTONDA a 1 decimale -> stampava "9.40 vs 9.38" accanto a una + # disuguaglianza che il conto esatto rispetta. Una quantita' arrotondata non si mette + # accanto a una disuguaglianza stretta: sembra violata proprio dove e' piu' stretta. + print(" => l'ensemble NON e' una scommessa diversa: e' la STESSA scommessa, spalmata.") + print(" Il drift non puo' cambiare se non per il fee-netting; la vol si', perche' la") + print(" media di N serie correlate ma non identiche ha meno varianza di ciascuna.") + + # (b) la tabella delle famiglie + print("\n (b) FAMIGLIE (tutte le rotazioni, nessuna selezione; book 50/50 ideale):") + print(f" {'K':>3} {'rot':>4} {'ShFULL med':>11} {'sd':>6} {'ShHOLD med':>11} {'sd':>6} " + f"{'drift med':>10} {'vol med':>8} {'maxDD med':>10} {'turn/y':>7}") + fam = {} + for K, nrot in FAM_TP.items(): + rots = fam_rotations(K) + assert len(rots) == nrot + D = [book_ideal(hs) for hs in rots] + f = np.array([al._sh(d["daily"]) for d in D]) + h = np.array([al._sh(d["daily"][d["daily"].index >= HOLDOUT]) for d in D]) + dr = np.array([drift(d["daily"]) for d in D]) + vo = np.array([vol(d["daily"]) for d in D]) + ddv = np.array([al._dd_ret(d["daily"]) for d in D]) + ty = np.array([d["turn_y"] for d in D]) + fam[K] = dict(f=f, h=h, dr=dr, vo=vo, dd=ddv, ty=ty, rots=rots) + print(f" {K:>3} {nrot:>4} {np.median(f):>11.3f} {f.std():>6.3f} " + f"{np.median(h):>11.3f} {h.std():>6.3f} {np.median(dr):>9.2%} " + f"{np.median(vo):>7.2%} {np.median(ddv):>9.1%} {np.mean(ty):>7.1f}") + + # (c) il confronto che risponde a (i): ensemble pieno vs ancora mediana + e24 = book_ideal(HOURS)["daily"] + med_h = int(np.argsort(S1["shf"])[len(S1["shf"]) // 2]) + med = S1["singles"][med_h] + print(f"\n (c) RISPOSTA A (i) — ensemble K=24 contro l'ancora MEDIANA (h={med_h}, la mediana") + print(f" per Sharpe FULL fra le 24) e contro la MEDIA delle 24:") + print(f" {'':<26} {'ShFULL':>8} {'ShIS':>8} {'ShHOLD':>8} {'drift':>8} {'vol':>8} {'maxDD':>8}") + for lbl, s in (("ancora mediana", med), ("ensemble K=24", e24)): + a, b, c = m3(s) + print(f" {lbl:<26} {a:>8.3f} {b:>8.3f} {c:>8.3f} {drift(s):>7.2%} " + f"{vol(s):>7.2%} {al._dd_ret(s):>7.1%}") + da, db, dc = m3(e24) + ma, mb, mc = m3(med) + print(f" {'DELTA (ens - mediana)':<26} {da-ma:>+8.3f} {db-mb:>+8.3f} {dc-mc:>+8.3f} " + f"{drift(e24)-drift(med):>+7.2%} {vol(e24)-vol(med):>+7.2%} " + f"{al._dd_ret(e24)-al._dd_ret(med):>+7.1%}") + f1 = fam[1] + print(f"\n Scomposizione del guadagno (ensemble vs MEDIA delle 24, che e' il confronto") + print(f" algebricamente pulito): drift {drift(e24):.2%} vs media ancore " + f"{np.mean(f1['dr']):.2%} (delta {drift(e24)-np.mean(f1['dr']):+.2%} = fee-netting);") + print(f" vol {vol(e24):.2%} vs media ancore {np.mean(f1['vo']):.2%} " + f"({vol(e24)/np.mean(f1['vo'])-1:+.1%}) <- QUI sta tutto il guadagno di Sharpe.") + print(f"\n COMPRESSIONE DELLA DISPERSIONE (e' questo che l'ensemble compra):") + print(f" {'K':>3} {'sd(ShFULL)':>11} {'sd(ShHOLD)':>11} {'banda ShHOLD':>18} " + f"{'sd/sd(K=1)':>11}") + for K in FAM_TP: + d = fam[K] + print(f" {K:>3} {d['f'].std():>11.3f} {d['h'].std():>11.3f} " + f"{'[%+.2f,%+.2f]' % (d['h'].min(), d['h'].max()):>18} " + f"{(d['h'].std()/fam[1]['h'].std() if fam[1]['h'].std() else 0):>11.2f}") + print(f" (a K=24 la dispersione e' ZERO per definizione: c'e' una sola realizzazione —") + print(f" ed e' esattamente il punto: l'ensemble non riduce l'incertezza, la ELIMINA.)") + print(f" {clock()}") + return dict(fam=fam, e24=e24, med_h=med_h) + + +# =========================================================================== +# §3 — (ii) l'ensemble contro l'ancora CANONICA che gira oggi +# =========================================================================== +def sezione3(S1: dict, S2: dict) -> None: + sez("§3 — (ii) L'ENSEMBLE CONTRO L'ANCORA CANONICA (h=0 UTC, quella che gira con soldi veri)") + can = S1["singles"][0] + e24 = S2["e24"] + print(f" {'':<26} {'ShFULL':>8} {'ShIS':>8} {'ShHOLD':>8} {'drift':>8} {'vol':>8} {'maxDD':>8}") + for lbl, s in (("CANONICA h=0 (oggi)", can), ("ensemble K=24", e24)): + a, b, c = m3(s) + print(f" {lbl:<26} {a:>8.3f} {b:>8.3f} {c:>8.3f} {drift(s):>7.2%} " + f"{vol(s):>7.2%} {al._dd_ret(s):>7.1%}") + ca, cb, cc = m3(can); ea, eb, ec = m3(e24) + print(f" {'DELTA (ens - canonica)':<26} {ea-ca:>+8.3f} {eb-cb:>+8.3f} {ec-cc:>+8.3f} " + f"{drift(e24)-drift(can):>+7.2%} {vol(e24)-vol(can):>+7.2%} " + f"{al._dd_ret(e24)-al._dd_ret(can):>+7.1%}") + print(f"\n Percentile della CANONICA fra le 24 ancore: FULL {pctl_of(S1['shf'], S1['shf'][0]):.0f}°" + f" IS {pctl_of(S1['shi'], S1['shi'][0]):.0f}°" + f" HOLD {pctl_of(S1['shh'], S1['shh'][0]):.0f}°" + f" maxDD {pctl_of(S1['dd'], S1['dd'][0]):.0f}°") + print(f" Percentile dell'ENSEMBLE fra le 24: FULL {pctl_of(S1['shf'], ea):.0f}°" + f" IS {pctl_of(S1['shi'], eb):.0f}° HOLD {pctl_of(S1['shh'], ec):.0f}°") + print("\n 📌 LA LETTURA, ed e' il punto di tutto il filone:") + print(" i due numeri NON sono due stime della stessa cosa. Il numero della canonica e' il") + print(" risultato di UNA estrazione gia' avvenuta e gia' nota; quello dell'ensemble e' cio'") + print(" che si ottiene SENZA estrarre. Chi confronta 'canonica vs ensemble' in-sample sta") + print(" confrontando il massimo osservato con la media, e la media perde sempre.") + print(" La domanda decidibile e' un'altra: DOMANI, quale dei due si otterra'?") + print(f" - tenendo la canonica: una estrazione dalla banda [{S1['shh'].min():+.2f}," + f"{S1['shh'].max():+.2f}] di hold-out, con mediana {np.median(S1['shh']):+.2f}" + f" — il {pctl_of(S1['shh'], S1['shh'][0]):.0f}° pctl di ieri non si eredita;") + print(f" - tenendo l'ensemble: {ec:+.2f}, senza estrazione.") + print(" Cioe': l'ensemble non compra ritorno, compra il fatto di sapere gia' cosa avrai.") + print(" E' la stessa cosa che il 26/07 disse del limit-TP di SKH01: *converte una lotteria") + print(" in certezza piu' che aggiungere ritorno*.") + print(f" {clock()}") + + +# =========================================================================== +# §4 — (iii) ESEGUIBILITA': la curva N_max(capitale) +# =========================================================================== +def sezione4(S1: dict, S2: dict) -> dict: + sez("§4 — (iii) A QUALE CAPITALE L'ENSEMBLE DIVENTA ESEGUIBILE (pavimento vero del venue)") + + print(" (a) LA TAGLIA DEGLI ORDINI. Il fatto strutturale prima dei numeri:") + print(" il tranching NON moltiplica gli ordini per K. `build_book_order` banda la") + print(" posizione NETTA e ne manda UNO per asset per giro: K cambia la FORMA del") + print(" sentiero di target, non il numero di ordini. Il numero di ordini lo decide la BANDA.") + print(f"\n {'K':>3} {'cambi di target/anno':>21} {'|dNozionale| mediano a $635':>28} " + f"{'p75':>10} {'quota sopra $5':>15}") + capass = 635.0 * 0.5 + for K in (1, 2, 4, 12, 24): + hs = fam_rotations(K)[0] + n_chg = 0; alld = [] + for a in ASSETS: + t = tgt(a, hs) + d = np.abs(np.diff(t, prepend=0.0)) * capass + d = d[d > 1e-12] + n_chg += len(d); alld.append(d) + D = np.concatenate(alld) + yrs = (idx1h("BTC")[-1] - idx1h("BTC")[0]) / pd.Timedelta("365.25D") + print(f" {K:>3} {n_chg/yrs:>21,.0f} {np.median(D):>27.2f}$ {np.percentile(D,75):>9.2f}$ " + f"{float((D >= MIN_ORDER_USD).mean()):>14.1%}") + print(f"\n ⚠️ IL FATTO CHE DECIDE, e non e' su K: a $635 la quota TP01 di UN asset e'") + print(f" ${capass:.0f}, l'esposizione media di TP01 e' ~{np.mean(tgt('BTC',(0,))):.3f}x ->") + print(f" il nozionale TENUTO e' ~${capass*np.mean(tgt('BTC',(0,))):.0f}. La banda da $5 e'") + print(f" il {5/(capass*np.mean(tgt('BTC',(0,)))):.0%} della posizione, e il lotto BTC " + f"(${S1['steps']['BTC']*px('BTC')[-1]:.1f}) e' il " + f"{S1['steps']['BTC']*px('BTC')[-1]/(capass*np.mean(tgt('BTC',(0,)))):.0%}.") + print(f" Gia' K=1 esegue solo una minoranza dei propri ribilanci. Non c'e' K da degradare.") + + # (b) curva bandata + print("\n (b) IL PATH ESEGUITO, per capitale (banda $5 + quantizzazione al lotto del venue):") + print(f" {'cap':>8} {'K':>3} {'ShFULL':>8} {'haircut':>8} {'ordini/anno':>12} " + f"{'fee $/anno':>11} {'corr(eseguito,ideale)':>22}") + band = {} + for C in CAPITALI: + for K in (1, 2, 4, 24): + hs = fam_rotations(K)[0] + bb = book_banded(hs, C, S1["steps"]) + ii = book_ideal(hs)["daily"] + cr = [] + for a in ASSETS: + cr.append(float(np.corrcoef(bb["E"][a]["pos"], tgt(a, hs))[0, 1])) + band[(C, K)] = dict(sh=al._sh(bb["daily"]), hc=al._sh(ii) - al._sh(bb["daily"]), + ord_y=bb["ord_y"], fee_y=bb["fee_y"] * C, corr=float(np.mean(cr)), + daily=bb["daily"]) + b = band[(C, K)] + print(f" {C:>8,.0f} {K:>3} {b['sh']:>8.3f} {b['hc']:>+8.3f} {b['ord_y']:>12.0f} " + f"{b['fee_y']:>11.2f} {b['corr']:>22.4f}") + print(" (haircut = Sharpe ideale - Sharpe eseguito; fee $/anno = fee proporzionale sul") + print(" nozionale realmente scambiato, NON un costo per ordine — su Deribit non esiste)") + + # (c) il criterio N_max dichiarato in §0 + print("\n (c) N_max(capitale) — criterio DICHIARATO IN §0, applicato senza ritocchi:") + ideal_sd = {K: S2["fam"][K]["f"].std() for K in FAM_TP} + passed: dict = {} + print(f" {'cap':>8} " + " ".join(f"{'K='+str(K):>9}" for K in (2, 3, 4, 6, 8, 12)) + + f" {'N_max':>7}") + nmax = {} + for C in CAPITALI: + sd_b = {} + for K in (1, 2, 3, 4, 6, 8, 12): + v = [] + for hs in fam_rotations(K): + bb = book_banded(hs, C, S1["steps"]) + v.append(al._sh(bb["daily"])) + sd_b[K] = float(np.std(v)) + cells, best, npass = [], 1, 0 + for K in (2, 3, 4, 6, 8, 12): + den = ideal_sd[1] - ideal_sd[K] + ret_ = (sd_b[1] - sd_b[K]) / den if den > 1e-12 else 0.0 + cells.append(f"{ret_:>9.2f}") + if ret_ >= 0.50: + best = K + npass += 1 + nmax[C] = best + passed[C] = npass + print(f" {C:>8,.0f} " + " ".join(cells) + f" {best:>7}") + print(" (retention = quota della compressione IDEALE di dispersione che il path ESEGUITO") + print(" conserva; 1.00 = la banda non toglie niente, <=0 = il tranching e' invisibile)") + + print("\n ⚠️ IL CRITERIO SATURA, e va detto prima di leggere la colonna N_max.") + print(f" Celle che passano la soglia 0.50: " + + ", ".join(f"${c:,.0f} -> {passed[c]}/6" for c in CAPITALI) + ".") + print(" Un gate che un candidato qualsiasi passa 6 volte su 6 a OGNI capitale non sta") + print(" misurando niente (regola 07/08: un gate si valida contando quante volte lo passa") + print(" un candidato a caso). Il criterio era scritto per catturare un degrado che NON") + print(" ESISTE: la banda del venue non degrada il tranching nemmeno a $635.") + print(" -> [A5] REFUTATA nella sua seconda meta'. La risposta a (iii) non e' un capitale:") + print(" e' che l'eseguibilita' NON e' il vincolo. (7a volta nell'ondata.)") + + print("\n (d) IL VINCOLO OPERATIVO VERO, che non e' il pavimento ma il FEED.") + print(" TP01 all'ancora h decide sulla barra 24h che CHIUDE alle h:00 di oggi: per h>0") + print(" servono barre di oggi. Il feed certificato si ricostruisce una volta al giorno") + print(" (~00:30 UTC) -> 23 ancore su 24 richiedono il feed 5m FRESCO, cioe' `fresh_5m`,") + print(" lo stesso che il 26/07 e' risultato ricadere IN SILENZIO sul certificato e che") + print(" il 29/07 ha fallito 6 giri su 8 dopo un riavvio. Oggi quel guasto tocca il solo") + print(" SKH01 ed e' sorvegliato (`skh_feed_max_age_min`=30). Un ensemble d'ancora ci") + print(" metterebbe sotto anche TP01, che oggi non dipende da quel path.") + print(" Non e' un argomento definitivo (l'allerta esiste gia'), ma e' il costo VERO del") + print(" tranching su questo libro: non ordini, non fee — una dipendenza di feed in piu'.") + print(" E' l'obiezione (3) del 02/07, l'unica delle sue tre che sopravvive intatta.") + print(f" {clock()}") + return dict(band=band, nmax=nmax, passed=passed) + + +# =========================================================================== +# §5 — IL FUNDING: l'unico canale per cui l'ensemble potrebbe GUADAGNARE +# =========================================================================== +def sezione5(S1: dict) -> dict: + sez("§5 — IL FUNDING (l'unico canale di GUADAGNO possibile) — e perche' e' chiuso per algebra") + F = {} + for instr, (start, asset, kind) in fd.INSTR.items(): + d = fd.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") + rate = fd.build_rate(F) + print(f" Tassi da `r0822d_funding` (cache su disco, nessuna rete). Copertura: " + f"{rate['BTC']['series'].index.min().date()} -> {rate['BTC']['series'].index.max().date()}") + + def f_hourly(a: str) -> np.ndarray: + """Tasso orario allineato all'indice 1h: il record etichettato T copre (T-1h, T].""" + ii = idx1h(a) + t_end = fd.to_ms(ii) + MSH + c = fd.cumF(rate[a], t_end) - fd.cumF(rate[a], fd.to_ms(ii)) + return c + + # (a) IL TEOREMA A4 + print("\n (a) [A4] il funding pagato in un'ora e' pos*f, LINEARE in pos:") + for a in ASSETS: + fh = f_hourly(a) + p24 = eval_ideal(a, HOURS)["pos"] + pm = np.mean([eval_ideal(a, (h,))["pos"] for h in HOURS], axis=0) + d = float(np.max(np.abs(p24 * fh - pm * fh))) + print(f" {a}: max|funding_ens - media(funding delle 24 ancore)| = {d:.2e} -> IDENTITA'") + print(" => l'ensemble NON puo' pagare meno funding della MEDIA delle ancore. Mai.") + print(" Il canale di guadagno sperato e' chiuso ALGEBRICAMENTE, non 'per piccolezza'.") + + # (b) il rapporto condizionale: si attenua mediando sulle 24 ore? + print("\n (b) e il rapporto CONDIZIONALE (22/08: esposizione e funding sono correlati, il") + print(" tasso pagato davvero e' 1,86-2,55x l'incondizionato per TP01) si attenua?") + print(f" {'K':>3} " + f"{'drag %/anno':>13} {'rapporto cond.':>16} " + f"{'corr(pos,f) oraria':>20}") + out = {} + cov = {a: (idx1h(a) >= rate[a]["series"].index.min()) for a in ASSETS} + for K in (1, 2, 4, 12, 24): + hs = fam_rotations(K)[0] + dr, rat, cc = [], [], [] + for a in ASSETS: + fh = f_hourly(a)[cov[a]] + p = eval_ideal(a, hs)["pos"][cov[a]] + paid = float(np.mean(p * fh)) * 24 * 365.25 + base = float(np.mean(p)) * float(np.mean(fh)) * 24 * 365.25 + dr.append(paid); rat.append(paid / base if base else np.nan) + cc.append(float(np.corrcoef(p, fh)[0, 1])) + out[K] = dict(drag=float(np.mean(dr)), ratio=float(np.mean(rat))) + print(f" {K:>3} {np.mean(dr):>12.3%} {np.mean(rat):>15.2f}x {np.mean(cc):>20.4f}") + print(" -> il rapporto e' ESSO STESSO una media di rapporti: mediare sull'ora del giorno") + print(" non tocca una correlazione che vive alla scala del REGIME (settimane/mesi).") + + # (c) replica del numero del 22/08 per una strada diversa + tp_noF, _ = fd.tp01_canonical(None) + tp_F, _ = fd.tp01_canonical(rate) + d22 = drift(tp_noF) - drift(tp_F) + p_h = eval_ideal("BTC", (0,))["pos"][cov["BTC"]] + print(f"\n (c) REPLICA del numero del 22/08 per una strada DIVERSA (griglia oraria invece che") + print(f" giornaliera, ancora canonica): drag di TP01 = {out[1]['drag']:.2%}/anno") + print(f" contro {d22:.2%}/anno rifacendo il conto con `fd.tp01_canonical` (griglia daily,") + print(f" lente del 22/08, che pubblico' 2,02%). Due path indipendenti, stesso ordine.") + + # (d) e sul BANDATO (dove la linearita' si rompe)? + print("\n (d) e sul path ESEGUITO (dove la banda rompe la linearita')?") + print(f" {'cap':>8} {'K':>3} {'drag %/anno':>13} {'vs ideale':>11}") + for C in (635.0, 5000.0, 50000.0): + for K in (1, 24): + hs = fam_rotations(K)[0] + dr = [] + for a in ASSETS: + e = eval_banded(a, hs, C * 0.5, S1["steps"][a]) + fh = f_hourly(a)[cov[a]] + dr.append(float(np.mean(e["pos"][cov[a]] * fh)) * 24 * 365.25) + print(f" {C:>8,.0f} {K:>3} {np.mean(dr):>12.3%} " + f"{np.mean(dr)-out[K]['drag']:>+10.3%}") + print(" -> la banda cambia il funding di briciole, e nel verso di chi tiene la posizione") + print(" piu' a lungo: non e' un canale, e' un residuo.") + print(f" {clock()}") + return dict(rate=rate, out=out, f_hourly=f_hourly, cov=cov) + + +# =========================================================================== +# §6 — SKH01: la stessa domanda su una griglia diversa, e piu' cara +# =========================================================================== +def skh_families() -> dict: + """Famiglie simmetriche per SKH01. Periodo congiunto 690m; griglia 5m -> 138 passi. + 138 = 2*3*23 -> K in {2,3,6,23} (e 46/69/138, fuori portata). Sulla griglia a 30m + dichiarata il 02/07 ci sono 23 offset e 23 e' PRIMO: o 1 o 23, niente in mezzo.""" + fams = {1: [(o,) for o in r02.OFFSETS]} + for K, shifts in ((2, range(0, 345, 30)), (3, range(0, 230, 30)), (6, range(0, 115, 30))): + cell = 690 // K + fams[K] = [tuple((s + cell * j) % 690 for j in range(K)) for s in shifts] + fams[23] = [tuple(r02.OFFSETS)] + return fams + + +_SKHD: dict = {} + + +def skh_daily(asset: str, off: int, lens: str) -> pd.Series: + """Serie daily di SKH01 a fase `off`. lens='canonical' (backtest) | 'hourly' (path del cron). + La cache pesante di `r02` viene SVUOTATA subito: 80+ offset x 2 asset di `ltf` non stanno + in memoria su questa macchina (2 core, ~3 GB liberi).""" + key = (asset, off, lens) + if key in _SKHD: + return _SKHD[key] + daily, m, ltf, ent = r02.run_asset(asset, off) + if lens == "canonical": + s = daily + else: + df5 = r02.get5m(asset) + ts5 = df5["timestamp"].values.astype(np.int64) + r02.MS5 + c5 = df5["close"].values.astype(float) + eq = r02.sim_equity(ltf, ent, "hourly", ts5, c5) + ss = pd.Series(eq, index=pd.DatetimeIndex(pd.to_datetime(ltf["datetime"], utc=True))) + s = ss.resample("1D").last().ffill().pct_change().dropna() + _SKHD[key] = s + r02._CACHE.pop((asset, off), None) # evict: tiene solo il daily + return s + + +def skh_book(offs: tuple, lens: str) -> pd.Series: + """Book SKH01 50/50 BTC+ETH, K tranche = media dei ritorni giornalieri (stessa convenzione + con cui il progetto combina BTC/ETH e gli sleeve: ribilanciamento a peso costante).""" + per = [] + for off in offs: + J = pd.concat({a: skh_daily(a, off, lens) for a in ASSETS}, axis=1, join="inner").fillna(0.0) + per.append(pd.Series(0.5 * J[ASSETS[0]].values + 0.5 * J[ASSETS[1]].values, index=J.index)) + JJ = pd.concat({i: s for i, s in enumerate(per)}, axis=1, join="inner").fillna(0.0) + return pd.Series(JJ.mean(axis=1).values, index=JJ.index) + + +def sezione6() -> dict: + sez("§6 — SKH01: la stessa domanda su una griglia diversa (e 23 e' un numero primo)") + from src.portfolio.sleeves import _skyhook_returns + ref = _skyhook_returns() + mine = skh_book((0,), "canonical") + d = float(np.max(np.abs(mine.values - ref.reindex(mine.index).values))) + print(f" [SANITY] offset 0 == `sleeves._skyhook_returns()`: max|dif| = {d:.2e} su " + f"{len(mine):,} giorni") + assert d < 1e-12 + + fams = skh_families() + print(f"\n [A6] 690/30 = 23, PRIMO -> sulla griglia a 30m del 02/07 non esiste sotto-ensemble") + print(f" simmetrico: o K=1 o K=23. Le famiglie K in {{2,3,6}} esistono solo sulla griglia") + print(f" 5m (138 = 2*3*23) e usano offset che il 02/07 non aveva mai valutato.") + print(f" ⚠️ K=3 = {{0,230,460}}: 230 e 460 sono multipli esatti del bin LTF -> la griglia") + print(f" d'ESECUZIONE 230m e' la STESSA, cambia solo la fase del segnale 690m. Non e' un") + print(f" tranching d'esecuzione: e' un ensemble di SEGNALE. Va detto, non nascosto.") + + res = {} + for lens in ("canonical", "hourly"): + print(f"\n LENTE '{lens}'" + (" (backtest, fill al livello)" if lens == "canonical" + else " (path del cron orario: fill al close 5m del prossimo multiplo orario)")) + print(f" {'K':>3} {'rot':>4} {'ShFULL med':>11} {'sd':>6} {'ShHOLD med':>11} {'sd':>6} " + f"{'drift med':>10} {'maxDD med':>10}") + R = {} + for K in (1, 2, 3, 6, 23): + S = [skh_book(o, lens) for o in fams[K]] + f = np.array([al._sh(s) for s in S]) + h = np.array([al._sh(s[s.index >= HOLDOUT]) for s in S]) + dr = np.array([drift(s) for s in S]) + dd = np.array([al._dd_ret(s) for s in S]) + R[K] = dict(f=f, h=h, dr=dr, dd=dd, S=S) + print(f" {K:>3} {len(S):>4} {np.median(f):>11.3f} {f.std():>6.3f} " + f"{np.median(h):>11.3f} {h.std():>6.3f} {np.median(dr):>9.2%} " + f"{np.median(dd):>9.1%}") + res[lens] = R + can = R[1]["f"][0] + print(f" canonico off=0: FULL {can:.3f} ({pctl_of(R[1]['f'], can):.0f}° pctl), " + f"HOLD {R[1]['h'][0]:.3f} ({pctl_of(R[1]['h'], R[1]['h'][0]):.0f}° pctl); " + f"ensemble K=23: FULL {R[23]['f'][0]:.3f}, HOLD {R[23]['h'][0]:.3f}") + print(f" {clock()}") + return res + + +# =========================================================================== +# §7 — I DUE SLEEVE INSIEME: il libro, e il netting che nessuno aveva contato +# =========================================================================== +def skh_pos_hourly(asset: str, off: int, ii: pd.DatetimeIndex) -> np.ndarray: + """Posizione oraria (+1/-1/0) di SKH01 a fase `off`, dalle boundary dei trade. + Riusa `fd.sim_equity_f` (che ritorna i trade con t_in/t_out in ms) — non riscrive il motore.""" + daily, m, ltf, ent = r02.run_asset(asset, off) + df5 = r02.get5m(asset) + ts5 = df5["timestamp"].values.astype(np.int64) + r02.MS5 + c5 = df5["close"].values.astype(float) + _, trades = fd.sim_equity_f(ltf, ent, "canonical", ts5, c5, None) + r02._CACHE.pop((asset, off), None) + t0 = fd.to_ms(ii) + p = np.zeros(len(ii)) + for tr in trades: + a = np.searchsorted(t0, tr["t_in"], side="left") + b = np.searchsorted(t0, tr["t_out"], side="right") + p[a:b] = tr["dir"] + return p + + +def sezione7(S1: dict, S4: dict, S6: dict) -> dict: + sez("§7 — I DUE SLEEVE INSIEME: il libro live, il NETTING, e dove il tranching vale davvero") + print(" Il libro esegue TP01 e SKH01 come UNA sola posizione netta per strumento (26/07, T1).") + print(" Quindi la banda da $5 non si applica al ribilancio di TP01 ma al NETTO — e i salti") + print(" grossi di SKH01 (che entra a 1,0x) trascinano dentro i micro-aggiustamenti di TP01,") + print(" gratis. Qui si incrociano le DUE griglie di tranching: TP01 (24 ancore orarie) e") + print(" SKH01 (23 fasi della griglia 230m/690m), che sono spazi di luck INDIPENDENTI.") + print(" ⚠️ LENTE: qui SKH01 e' mark-to-market ORARIO (posizione dai confini dei trade), non") + print(" compounding per trade come lo sleeve ufficiale -> i LIVELLI non sono confrontabili") + print(" coi numeri pubblicati. Si usano solo DIFFERENZE fra configurazioni, a lente identica.") + + ii = {a: idx1h(a) for a in ASSETS} + print(f" {clock()} costruzione delle 23 posizioni orarie di SKH01 (x2 asset)...") + SP = {a: {o: skh_pos_hourly(a, o, ii[a]) for o in r02.OFFSETS} for a in ASSETS} + sk = {1: {a: SP[a][0] for a in ASSETS}, + 23: {a: np.mean([SP[a][o] for o in r02.OFFSETS], axis=0) for a in ASSETS}} + + def run(ktp: int, ksk: int, C: float) -> dict: + hs = fam_rotations(ktp)[0] + nor, corr, nets, notion = 0, [], {}, 0.0 + for a in ASSETS: + tnet = W_ASSET * (W_TP * tgt(a, hs) + W_SKH * sk[ksk][a]) + b = banded(px(a), tnet, C, MIN_ORDER_USD, S1["steps"][a]) + held = b["held"] + pos = np.zeros(len(held)); pos[1:] = held[:-1] + net = pos * ret(a) - FEE * np.abs(np.diff(pos, prepend=0.0)); net[0] = 0.0 + nor += b["n_ord"]; notion += b["notional"] + corr.append(float(np.corrcoef(pos, tnet)[0, 1])) + nets[a] = pd.Series(net, index=ii[a]) + J = pd.concat(nets, axis=1, join="inner").fillna(0.0) + dly = al._to_daily(J[ASSETS[0]] + J[ASSETS[1]]) + yrs = (ii["BTC"][-1] - ii["BTC"][0]) / pd.Timedelta("365.25D") + return dict(ord_y=nor / yrs, corr=float(np.mean(corr)), sh=al._sh(dly), + hold=al._sh(dly[dly.index >= HOLDOUT]), dd=al._dd_ret(dly), + dly=dly, fee_y=FEE * notion / yrs) + + print(f"\n {'cap':>8} {'K_TP':>5} {'K_SKH':>6} {'ordini/anno':>12} {'fee $/anno':>11} " + f"{'fedelta':>9} {'ShFULL':>8} {'ShHOLD':>8} {'maxDD':>7}") + out = {} + for C in (635.0, 5000.0, 100000.0): + for ktp, ksk in ((1, 1), (24, 1), (1, 23), (24, 23)): + r = run(ktp, ksk, C) + out[(C, ktp, ksk)] = r + tag = " <- il libro di oggi" if (ktp, ksk) == (1, 1) else "" + print(f" {C:>8,.0f} {ktp:>5} {ksk:>6} {r['ord_y']:>12.0f} {r['fee_y']:>11.2f} " + f"{r['corr']:>9.4f} {r['sh']:>8.3f} {r['hold']:>8.3f} {r['dd']:>7.1%}{tag}") + print(" (fedelta' = corr fra la posizione ESEGUITA e il target ideale della stessa config)") + + print(f"\n 📌 LA SCOMPOSIZIONE CHE RISPONDE ALLA DOMANDA 'fallo sul LIBRO':") + add = {} + for C in (635.0, 5000.0, 100000.0): + d_tp = out[(C, 24, 1)]["sh"] - out[(C, 1, 1)]["sh"] + d_sk = out[(C, 1, 23)]["sh"] - out[(C, 1, 1)]["sh"] + d_both = out[(C, 24, 23)]["sh"] - out[(C, 1, 1)]["sh"] + add[C] = (d_tp, d_sk, d_both) + print(f" cap ${C:>8,.0f}: tranciare SOLO TP01 {d_tp:>+7.3f} · SOLO SKH01 {d_sk:>+7.3f} · " + f"ENTRAMBI {d_both:>+7.3f} (somma dei due singoli {d_tp+d_sk:>+7.3f}, " + f"scarto dall'additivita' {d_both-d_tp-d_sk:>+.3f})") + ok_add = all(abs(add[C][2] - add[C][0] - add[C][1]) < 0.01 for C in add) + print(f" -> additivita' dei due tranching: {'REGGE' if ok_add else 'NON regge a ogni capitale'}" + f" (scarto max {max(abs(add[C][2]-add[C][0]-add[C][1]) for C in add):.3f}).") + # ⚠️ ERRORE MIO, catturato rileggendo l'output: la prima stesura stampava "i due effetti sono + # additivi" come TESTO FISSO. A $635 non lo sono (i singoli sommano a un numero di segno + # opposto al congiunto). Ora l'affermazione la calcola il codice. Una conclusione scritta a + # mano accanto a una tabella e' una conclusione che la tabella non controlla. + print(f" ⚠️ E la TAGLIA e' rumore su questa lente: {abs(add[635.0][2]):.3f} a $635 e " + f"{abs(add[100000.0][2]):.3f} a $100k, con i segni dei singoli che cambiano fra i due") + print(f" capitali. Il numero grosso su SKH01 (§6) e' STANDALONE; dentro il libro pesa") + print(f" il 25% ed e' misurato su una lente diversa -> qui si legge 'non si distingue") + print(f" da zero', non 'il libro guadagna'.") + + print(f"\n 🚨 IL PREZZO VERO DEL TRANCHING SI VEDE SULL'HOLD-OUT, non sul FULL:") + for C in (635.0, 100000.0): + print(f" cap ${C:>8,.0f}: ShHOLD {out[(C,1,1)]['hold']:.3f} (oggi) -> " + f"{out[(C,24,1)]['hold']:.3f} (TP tranciato) -> {out[(C,1,23)]['hold']:.3f} " + f"(SKH tranciato) -> {out[(C,24,23)]['hold']:.3f} (entrambi)") + print(f" Non e' un difetto del tranching: e' la fortuna d'ancora che si restituisce.") + print(f" ENTRAMBE le ancore canoniche stanno all'estremo alto del loro hold-out (TP01 al") + print(f" 98° pctl di 24, SKH01 all'89° di 23) e il libro di oggi le somma. Un ensemble") + print(f" non puo' che perdere contro la somma di due massimi — ed e' esattamente perche'") + print(f" quel numero non e' l'aspettativa di domani.") + print(f" E il maxDD va nel verso opposto: {out[(635.,1,1)]['dd']:.1%} -> " + f"{out[(635.,24,23)]['dd']:.1%}. Rischio giu', hold-out fortunato giu'.") + + o1, o2 = out[(635., 1, 1)], out[(635., 24, 23)] + print(f"\n IL COSTO, a $635 — e sorprende: gli ORDINI salgono {o1['ord_y']:.0f} -> " + f"{o2['ord_y']:.0f}/anno ({o2['ord_y']/o1['ord_y']:.1f}x, " + f"{o2['ord_y']/365.25:.1f}/giorno su 24 giri orari),") + print(f" ma le FEE {'SCENDONO' if o2['fee_y'] < o1['fee_y'] else 'salgono'} " + f"${o1['fee_y']:.2f} -> ${o2['fee_y']:.2f}/anno " + f"({o1['fee_y']/635:.2%} -> {o2['fee_y']/635:.2%} di capitale).") + print(f" Non e' una contraddizione ed e' il fatto piu' istruttivo di questa sezione: su") + print(f" Deribit la fee e' PROPORZIONALE, quindi si paga il NOZIONALE, non gli ordini —") + print(f" e mediare 23 fasi di uno sleeve a trade discreti trasforma un +-1,0x che sbatte") + print(f" da un estremo all'altro in una posizione frazionaria che si muove poco. Piu'") + print(f" ordini, meno merce. (Su un venue a pavimento FISSO — IB, 25/07 — il segno si") + print(f" ribalterebbe: la stessa mossa costerebbe {o2['ord_y']/o1['ord_y']:.1f}x.)") + print(f" {clock()}") + return out + + +# =========================================================================== +# §8 — QUANTO VALE RIDURRE LA VARIANZA SOTTO UNA BARRIERA ASSORBENTE +# =========================================================================== +def barrier_pass(r: np.ndarray, k: float, up: float, dn: float, + starts: np.ndarray, maxlen: int = 365) -> tuple: + """P(pass) / P(breach) di una regola a barriera: dal saldo iniziale, +up = pass, + -dn = breach (max-loss STATICO, la regola vera: 25/07). Path close-only.""" + L = np.concatenate([[0.0], np.cumsum(np.log1p(np.clip(k * r, -0.99, None)))]) + a, b = np.log1p(up), np.log1p(-dn) + np_, nb, nu = 0, 0, 0 + for i in starts: + d = L[i + 1:i + 1 + maxlen] - L[i] + if len(d) < 5: + continue + hu = np.flatnonzero(d >= a) + hd = np.flatnonzero(d <= b) + ju = hu[0] if len(hu) else 10 ** 9 + jd = hd[0] if len(hd) else 10 ** 9 + if ju == jd == 10 ** 9: + nu += 1 + elif ju < jd: + np_ += 1 + else: + nb += 1 + tot = np_ + nb + nu + return (np_ / tot, nb / tot, nu / tot) if tot else (0.0, 0.0, 0.0) + + +def sezione8(S1: dict, S2: dict, S6: dict) -> dict: + sez("§8 — [A7] QUANTO VALE RIDURRE LA VARIANZA SOTTO UNA BARRIERA (il canale funded)") + print(" Su un conto proprio il nemico e' il drift e la varianza si sopporta. Su un conto") + print(" funded il vincolo binding e' la regola di DRAWDOWN (25/07 §4): li' la varianza E'") + print(" il nemico — e l'ensemble compra esattamente quello. Questa e' l'attesa [A7]; segue") + print(" la misura, che la refuta sulla meta' che si puo' misurare.") + print(" Regole HyroTrader modellate (`r0725_hyro`: EV_TARGET/EV_DD/EV_DL = 0.10/0.06/0.04):") + print(" target +10%, max-loss STATICO -6% dal saldo iniziale.") + print(" ⚠️ LIMITE DICHIARATO, ed e' quello che conta: la regola di DAILY-LOSS (-4%) NON e'") + print(" valutabile qui. Su una regola a UN giorno la lente close-only e' CIECA (22/08:") + print(" 0,00 breach/anno contro 0,40-1,21 veri, rapporto INF) e servirebbe la lente wick") + print(" ACCOPPIATA (`r0725_prop_coupled`), fuori portata di questo script. Il max-loss e'") + print(" multi-giorno: li' close-only e' ottimista di un ricarico ~3,5%, non cieca.") + + skhL = S6["canonical"] + skh_off = skhL[1]["S"] + skh_ens = skhL[23]["S"][0] + tp_h = S1["singles"] + tp_ens = S2["e24"] + + def book(tp: pd.Series, skh: pd.Series) -> pd.Series: + return combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": W_TP, "SKH01": W_SKH}, + lo=pd.Timestamp("2019-03-14", tz="UTC")) + + rng = np.random.default_rng(20260823) + pairs = [(int(h), int(o)) for h, o in zip(rng.integers(0, 24, 150), rng.integers(0, 23, 150))] + print(f"\n Spazio congiunto 24 ancore TP01 x 23 fasi SKH01 = 552 configurazioni; 150 estrazioni") + print(f" uniformi indipendenti (seme dichiarato) + l'ensemble congiunto + la canonica (0,0).") + print(f" {'leva':>5} {'':<24} {'P(pass)':>9} {'P(breach)':>10} {'P(aperto)':>10} {'maxDD':>8}") + res = {} + BK = {} + for (h, o) in pairs: + BK[(h, o)] = book(tp_h[h], skh_off[o]) + BE = book(tp_ens, skh_ens) + BC = book(tp_h[0], skh_off[0]) + for k in (0.50, 0.75, 1.00): + rows = [] + for (h, o) in pairs: + s_ = BK[(h, o)]; r = s_.values.astype(float) + st = np.arange(0, len(r) - 380, 15) + rows.append(barrier_pass(r, k, 0.10, 0.06, st) + (al._dd_ret(s_),)) + A = np.array(rows) + rr = BE.values.astype(float); st = np.arange(0, len(rr) - 380, 15) + pe = barrier_pass(rr, k, 0.10, 0.06, st) + (al._dd_ret(BE),) + rc = BC.values.astype(float); stc = np.arange(0, len(rc) - 380, 15) + pc = barrier_pass(rc, k, 0.10, 0.06, stc) + (al._dd_ret(BC),) + res[k] = dict(A=A, ens=pe, can=pc) + print(f" {k:>5.2f} {'CANONICA (0,0) — oggi':<24} {pc[0]:>9.1%} {pc[1]:>10.1%} " + f"{pc[2]:>10.1%} {pc[3]:>8.1%}") + print(f" {'':>5} {'mediana 150 estrazioni':<24} {np.median(A[:,0]):>9.1%} " + f"{np.median(A[:,1]):>10.1%} {np.median(A[:,2]):>10.1%} {np.median(A[:,3]):>8.1%}") + print(f" {'':>5} {'banda p10-p90':<24} " + f"{'[%.0f%%,%.0f%%]' % (np.percentile(A[:,0],10)*100, np.percentile(A[:,0],90)*100):>9} " + f"{'[%.1f%%,%.1f%%]' % (np.percentile(A[:,1],10)*100, np.percentile(A[:,1],90)*100):>10} " + f"{'':>10} " + f"{'[%.1f,%.1f]' % (np.percentile(A[:,3],10)*100, np.percentile(A[:,3],90)*100):>8}") + print(f" {'':>5} {'ENSEMBLE congiunto':<24} {pe[0]:>9.1%} {pe[1]:>10.1%} " + f"{pe[2]:>10.1%} {pe[3]:>8.1%} (pctl fra le estrazioni: " + f"{pctl_of(A[:,0], pe[0]):.0f}° su P(pass), {pctl_of(-A[:,1], -pe[1]):.0f}° su " + f"P(breach), {pctl_of(-A[:,3], -pe[3]):.0f}° su maxDD)") + print() + + print(" 🚨 [A7] REFUTATA sulla meta' misurabile, E CON UN MECCANISMO — non e' rumore.") + print(" Su queste regole il lato BINDING non e' la barriera, e' il BERSAGLIO: P(breach)") + print(f" sta fra {res[0.50]['ens'][1]:.1%} e {res[1.00]['ens'][1]:.1%} mentre P(pass) sta") + print(f" fra {res[0.50]['ens'][0]:.0%} e {res[1.00]['ens'][0]:.0%}. Se il vincolo e'") + print(" ARRIVARE a +10% e non evitare -6%, ridurre la varianza RALLENTA: e' la stessa") + print(" meno-vol che tiene lontani dalla barriera a tenere lontani dal traguardo.") + print(" L'ensemble infatti sta al 30-50° pctl su P(pass) — sotto la mediana — pur avendo") + print(" un maxDD MIGLIORE della mediana. Le due cose non sono in contraddizione: sono la") + print(" stessa cosa vista dai due lati.") + print(" ⚠️ E la regola che il progetto ha MISURATO essere quella che uccide — la") + print(" daily-loss a UN giorno (22/08) — e' esattamente quella che questa lente non puo'") + print(" vedere. Quindi: A7 non e' refutata, e' refutata SULLA REGOLA SBAGLIATA. Dirlo e'") + print(" il risultato; concludere 'l'ensemble non serve sul funded' NON lo sarebbe.") + + # --- proxy POTENTE per la regola a un giorno: la coda sinistra giornaliera ----------- + print("\n (b) L'UNICA cosa con potenza che si puo' dire sulla regola a un giorno: la CODA") + print(" SINISTRA giornaliera. La daily-loss legge il MINIMO del giorno; il close-only ne") + print(" e' un limite inferiore (rapporto mediano minimo/chiusura misurato 1,89x il 22/08).") + print(" Non e' una stima del breach — e' il confronto FRA configurazioni sulla grandezza") + print(" che quella regola legge, a lente identica.") + W = np.array([[float(np.min(BK[p].values)), float(np.percentile(BK[p].values, 1)), + float(np.mean(BK[p].values < -0.02))] for p in pairs]) + we = [float(np.min(BE.values)), float(np.percentile(BE.values, 1)), + float(np.mean(BE.values < -0.02))] + wc = [float(np.min(BC.values)), float(np.percentile(BC.values, 1)), + float(np.mean(BC.values < -0.02))] + print(f"\n {'':<24} {'peggior giorno':>15} {'p1 giornaliero':>15} {'giorni < -2%':>14}") + print(f" {'CANONICA (0,0) — oggi':<24} {wc[0]:>14.2%} {wc[1]:>14.2%} {wc[2]:>13.2%}") + print(f" {'mediana 150 estrazioni':<24} {np.median(W[:,0]):>14.2%} " + f"{np.median(W[:,1]):>14.2%} {np.median(W[:,2]):>13.2%}") + print(f" {'banda p10-p90':<24} " + f"{'[%.2f%%,%.2f%%]' % (np.percentile(W[:,0],10)*100, np.percentile(W[:,0],90)*100):>15} " + f"{'[%.2f%%,%.2f%%]' % (np.percentile(W[:,1],10)*100, np.percentile(W[:,1],90)*100):>15} " + f"{'[%.2f%%,%.2f%%]' % (np.percentile(W[:,2],10)*100, np.percentile(W[:,2],90)*100):>14}") + q = (pctl_of(W[:, 0], we[0]), pctl_of(W[:, 1], we[1]), pctl_of(-W[:, 2], -we[2])) + print(f" {'ENSEMBLE congiunto':<24} {we[0]:>14.2%} {we[1]:>14.2%} {we[2]:>13.2%}" + f" (pctl: {q[0]:.0f}° / {q[1]:.0f}° / {q[2]:.0f}° — " + f"= quota di configurazioni che l'ensemble BATTE)") + # ⚠️ ERRORE MIO, catturato rileggendo l'output: la prima stesura calcolava questi tre + # percentili con `pctl_of(-W, -we)` per tutte e tre le colonne. Sulle prime due (che sono + # RITORNI, negativi, dove "piu' alto = meglio") quel segno da' il COMPLEMENTO: stampava 34° + # dove il valore vero e' 66°, cioe' diceva "peggio della mediana" di un numero che e' + # meglio della mediana. La terza colonna (una FREQUENZA, dove "piu' basso = meglio") aveva + # bisogno del segno invertito ed era l'unica giusta. Tre colonne, due convenzioni. + print(f" -> l'ensemble batte la mediana su TUTTE E TRE le colonne, ma di POCO " + f"({q[0]:.0f}°/{q[1]:.0f}°/{q[2]:.0f}° pctl): comprime la coda, non la annulla.") + print(" E la banda p10-p90 del p1 e' strettissima -> su questa grandezza le 552") + print(" configurazioni si somigliano molto piu' che sullo Sharpe.") + print(" Resta un INDIZIO, non una misura del breach: la conferma richiede la lente") + print(" accoppiata, ed e' un follow-up con un costo noto, non un ostacolo.") + print("\n 📌 E il fatto che chiude il cerchio sull'eseguibilita': il conto funded e' da") + print(" $100.000, dove il pavimento del venue e' invisibile (§7 lo misura). Il tranching") + print(" e' ineseguibile in nessun posto — e utile solo dove la coda conta.") + print(f" {clock()}") + return dict(res=res, tail=dict(W=W, ens=we, can=wc)) + + +# =========================================================================== +# §9 — VERDETTO +# =========================================================================== +def sezione9(S1, S2, S4, S5, S6, S7, S8) -> None: + sez("§9 — VERDETTO SULLE ATTESE A PRIORI (derivato dai numeri, non scritto a mano)") + can = S1["singles"][0]; e24 = S2["e24"] + ca, cb, cc = m3(can); ea, eb, ec = m3(e24) + med = S1["singles"][S2["med_h"]] + ma, mb, mc = m3(med) + f1 = S2["fam"][1] + d_med = ea - ma + # A1: identita' del lordo, ri-verificata qui (cached, costo ~0) + id_max = max(float(np.max(np.abs(eval_ideal(a, HOURS)["gross"] - + np.mean([eval_ideal(a, (h,))["gross"] for h in HOURS], axis=0)))) + for a in ASSETS) + a1 = "CONFERMATA" if id_max < 1e-15 else "REFUTATA" + a2 = ("CONFERMATA" if 0.03 <= d_med <= 0.15 else + ("DIREZIONE OK, TAGLIA REFUTATA" if d_med > 0 else "REFUTATA")) + a3 = "CONFERMATA" if ec < cc else "REFUTATA" + r1, r24 = S5["out"][1]["ratio"], S5["out"][24]["ratio"] + a4 = "CONFERMATA" if abs(r24 - r1) < 0.05 else "REFUTATA" + nm = S4["nmax"]; pas = S4["passed"] + a5 = "REFUTATA" if (nm[635.0] > 1 or min(pas.values()) == 6) else "CONFERMATA" + a6 = "CONFERMATA (aritmetica: 23 e' primo)" + p_ens = pctl_of(S8["res"][1.00]["A"][:, 0], S8["res"][1.00]["ens"][0]) + # convenzione: il p1 e' un RITORNO (negativo), quindi "piu' alto = meglio" e il percentile + # si calcola SENZA invertire il segno. Invertirlo da' il complemento (34° invece di 66°) — + # errore commesso nella prima stesura di §8 e corretto li': va corretto anche qui, o le due + # sezioni stampano due numeri diversi per la stessa grandezza. + p_tail = pctl_of(S8["tail"]["W"][:, 1], S8["tail"]["ens"][1]) + a7 = "REFUTATA sulla regola misurabile" if p_ens < 50 else "CONFERMATA" + ver = [ + ("A1", a1, f"identita' del lordo verificata a max|dif| = {id_max:.1e}; turnover " + f"sub-additivo sul conto esatto."), + ("A2", a2, f"dSharpe FULL vs ancora mediana {d_med:+.3f} (previsto +0,03..+0,15): " + f"il SEGNO e' giusto, la TAGLIA e' {0.09/max(d_med,1e-9):.0f}x piu' piccola del " + f"centro della mia banda. Avevo sopravvalutato quanto sono decorrelate fra loro " + f"24 ancore della STESSA strategia."), + ("A3", a3, f"dSharpe HOLD vs canonica {ec-cc:+.3f}; la canonica sta al " + f"{pctl_of(S1['shh'], S1['shh'][0]):.0f}° pctl delle 24 sull'hold-out e al " + f"{pctl_of(S1['shi'], S1['shi'][0]):.0f}° in-sample: SFORTUNATA in-sample e FORTUNATA " + f"sull'hold-out, esattamente la struttura che il 02/07 aveva descritto."), + ("A4", a4, f"funding dell'ensemble ESATTAMENTE la media (max|dif| = 0) e rapporto " + f"condizionale immobile ({r1:.2f}x a K=1 -> {r24:.2f}x a K=24)."), + ("A5", a5, f"il pavimento NON degrada il tranching: retention {min(pas.values())}/6 celle " + f"sopra soglia a OGNI capitale, N_max($635) = {nm[635.0]} invece di 1. " + f"Il criterio SATURA -> non e' un N_max, e' un 'il vincolo non e' questo'."), + ("A6", a6, "e le famiglie simmetriche di SKH01 vivono solo sulla griglia 5m; K=3 e' un " + "ensemble di SEGNALE, non d'esecuzione."), + ("A7", a7, f"l'ensemble sta al {p_ens:.0f}° pctl su P(pass) a leva 1,0x — SOTTO la mediana — " + f"perche' su queste regole il lato binding e' il BERSAGLIO (+10%), non la " + f"barriera. Ma sul p1 giornaliero, che e' cio' che la regola killer legge, sta " + f"al {p_tail:.0f}° pctl (batte quella quota di configurazioni). La refutazione e' " + f"sulla regola che si puo' misurare, non su quella che uccide: dichiarato."), + ] + for k, v, t in ver: + print(f" [{k}] {v}") + print(f" {t}\n") + + sez("§9b — LE TRE RISPOSTE") + print(f" (i) L'ensemble batte l'ancora MEDIANA? SI su TP01, ma di BRICIOLE: " + f"dShFULL {d_med:+.3f}, dShHOLD {ec-mc:+.3f}, dDrift {drift(e24)-drift(med):+.2%}.") + print(f" Scomposto contro la MEDIA delle 24 (il confronto algebricamente pulito): il") + print(f" drift si muove di {drift(e24)-np.mean(f1['dr']):+.3%} (solo fee-netting, e la") + print(f" linearita' dice che non puo' fare altro) e la vol di " + f"{vol(e24)/np.mean(f1['vo'])-1:+.1%}. Tutto il guadagno e' vol.") + print(f" Cio' che l'ensemble compra DAVVERO non e' nella colonna Sharpe: e' la") + print(f" dispersione, sd(ShFULL) {f1['f'].std():.3f} -> 0 e sd(ShHOLD) " + f"{f1['h'].std():.3f} -> 0.") + skhc, skhh = S6["canonical"], S6["hourly"] + print(f"\n ⚠️ E su SKH01 la risposta e' DIVERSA di un ordine di grandezza: sulla lente") + print(f" del path che gira (hourly) la mediana delle 23 fasi fa ShFULL " + f"{np.median(skhh[1]['f']):.3f} e l'ensemble K=23 fa {skhh[23]['f'][0]:.3f} " + f"({skhh[23]['f'][0]-np.median(skhh[1]['f']):+.3f}),") + print(f" con maxDD mediano {np.median(skhh[1]['dd']):.1%} -> {skhh[23]['dd'][0]:.1%}. " + f"Su TP01 il tranching e' rumore; su SKH01 e' il grosso.") + print(f" Meccanismo: la dispersione d'ancora di SKH01 e' 3x quella di TP01 " + f"(sd {skhh[1]['f'].std():.3f} vs {f1['f'].std():.3f}) perche' i suoi trade sono") + print(f" DISCRETI — spostare la griglia cambia QUALI trade esistono, non di quanto sono") + print(f" grossi. Il tranching vale quanto la lumpiness che elimina.") + print(f" ⚠️ MA quel numero e' STANDALONE. Dentro il libro SKH01 pesa il 25% e il §7 lo") + print(f" misura su un'altra lente: li' il tranching congiunto vale " + f"{S7[(635.0,24,23)]['sh']-S7[(635.0,1,1)]['sh']:+.3f} a $635 e " + f"{S7[(100000.0,24,23)]['sh']-S7[(100000.0,1,1)]['sh']:+.3f} a $100k, coi segni dei") + print(f" singoli che cambiano fra i due capitali = non distinguibile da zero.") + print(f" La lettura onesta: il tranching di SKH01 e' l'unico posto dove c'e' un segnale,") + print(f" e non e' abbastanza grande da vedersi attraverso un peso del 25%.") + + print(f"\n (ii) L'ensemble batte la CANONICA che gira oggi? NO sull'hold-out " + f"({ec-cc:+.3f}), SI su FULL ({ea-ca:+.3f}) e su IS ({eb-cb:+.3f}).") + print(f" E i due numeri NON sono due stime della stessa cosa. Il {cc:+.2f} della canonica") + print(f" e' una estrazione GIA' AVVENUTA (98° pctl di 24); il {ec:+.2f} dell'ensemble e'") + print(f" cio' che si ottiene SENZA estrarre.") + print(f" 📌 LA STIMA ONESTA DEL FUTURO E' L'ENSEMBLE: {ec:+.2f}, non {cc:+.2f}.") + print(f" Tenere la canonica non conserva il {cc:+.2f} — ri-estrae dalla banda " + f"[{S1['shh'].min():+.2f},{S1['shh'].max():+.2f}], mediana {np.median(S1['shh']):+.2f}.") + print(f" (E questi numeri riproducono la memoria del progetto: canonico +0,44 al " + f"{pctl_of(S1['shh'], S1['shh'][0]):.0f}° pctl, mediana +{np.median(S1['shh']):.2f}.)") + + print(f"\n (iii) A quale capitale diventa eseguibile senza degenerare? A TUTTI, gia' a $635 —") + print(f" e questo REFUTA la mia attesa e la premessa (non la conclusione) del 02/07.") + print(f" A $635 il path eseguito a K=24 segue il target ideale con corr " + f"{S4['band'][(635.0,24)]['corr']:.4f} contro {S4['band'][(635.0,1)]['corr']:.4f} di K=1:") + print(f" il tranching e' seguito MEGLIO, non peggio, perche' un target che si muove a") + print(f" passi piccoli e frequenti entra in una banda fissa meglio di uno che salta.") + print(f" Ordini: {S4['band'][(635.0,1)]['ord_y']:.0f}/anno -> " + f"{S4['band'][(635.0,24)]['ord_y']:.0f}/anno, fee " + f"${S4['band'][(635.0,1)]['fee_y']:.2f} -> ${S4['band'][(635.0,24)]['fee_y']:.2f}/anno.") + + sez("§9c — IL RISULTATO CHE NON ERA NEL MANDATO, e cosa NON cambia") + print(" 📌 LA PREMESSA DEL 02/07 E' SBAGLIATA; LA SUA CONCLUSIONE REGGE PER UN'ALTRA RAGIONE.") + print(" Il 02/07 boccio' il tranching cosi': *'a $225/asset di quota TP01 i delta") + print(" per-ancora (~$1-2) sono sotto il min-order $5 -> degenererebbe comunque in K=1'*.") + print(" Ma `src/live/book.build_book_order` non manda mai un ordine PER ANCORA: banda") + print(" `abs(delta)` sulla posizione NETTA e ne manda UNO per asset per giro. Un 'delta") + print(" per-ancora' non e' un ordine, e' un pezzo di aritmetica dentro il target. Il") + print(" tranching non moltiplica gli ordini per K, e infatti la degenerazione NON avviene") + print(" (misurata: retention ~0,9-1,0 a $635).") + print(" Cio' che boccia il tranching di TP01 non e' l'esecuzione, e' che NON C'E' NIENTE") + print(" DA PRENDERE: +0,014 di Sharpe. La decisione del 02/07 resta, la sua ragione no.") + print(" E la ragione conta: quella del 02/07 sarebbe caduta al primo esecutore che mandasse") + print(" un ordine per tranche; questa non cade mai, perche' e' sulla taglia dell'effetto.") + print("\n 🚨 E IL FILONE HA TROVATO IL SUO CONTRARIO: l'unica ancora che il 02/07 NON aveva") + print(" esaminato — la griglia 230m/690m di SKH01 — e' quella dove il tranching vale.") + print(" Il 02/07 raccomando' l'audit su SKH01 (fatto il 02/07-bis) ma non ci porto' MAI") + print(" la domanda del tranching, che era nata su TP01.") + print("\n ⚠️ COSA COSTA DAVVERO, e non e' negli ordini: il feed. 23 ancore su 24 di TP01") + print(" richiedono barre di oggi, cioe' `fresh_5m` — lo stesso path che il 26/07 e'") + print(" risultato ricadere IN SILENZIO sul certificato e che il 29/07 ha fallito 6 giri") + print(" su 8. Oggi TP01 non dipende da quel path; con l'ensemble ci dipenderebbe.") + print(" E' l'unica delle tre obiezioni del 02/07 che sopravvive intatta.") + print(" (SKH01 quel feed lo usa GIA': il suo tranching non aggiunge dipendenze nuove —") + print(" il che rende l'asimmetria fra i due sleeve ancora piu' netta.)") + print("\n NIENTE CAMBIA nel libro live: pesi, ancore, cron, config INVARIATI. Il tranching non") + print(" e' un cambio di pesi (`weights_tilt_null` non si applica) ma e' un cambio di") + print(" ESECUZIONE: vale l'asticella del 26/07 (T1), il beneficio si confronta col path che") + print(" gira davvero. Non e' proposto nulla: SKH01 non e' passato per `study_family_honest`") + print(" su questa griglia, il guadagno e' misurato sugli STESSI 7 anni su cui SKH01 e' stato") + print(" selezionato, e la lente 'hourly' e' gia' nota (26/07) per sottostimare il path live.") + print(" Cio' che questo filone stabilisce e' DOVE guardare, non cosa fare.") + print(f"\n {clock()} fine.") + + +def main() -> None: + print("=" * 100) + print(" r0823_anchor_ensemble.py — §47 ANCHOR-ENSEMBLE") + print(f" fee {2*FEE:.2%} RT · HOLD-OUT >= {HOLDOUT.date()} · min_order ${MIN_ORDER_USD:.0f} · " + f"libro TP01 {W_TP:.0%} / SKH01 {W_SKH:.0%}") + print("=" * 100) + sezione0() + S1 = sezione1() + S2 = sezione2(S1) + sezione3(S1, S2) + S4 = sezione4(S1, S2) + S5 = sezione5(S1) + S6 = sezione6() + S7 = sezione7(S1, S4, S6) + S8 = sezione8(S1, S2, S6) + sezione9(S1, S2, S4, S5, S6, S7, S8) + + +if __name__ == "__main__": + main() From e7ee0738a347276a78ba3e28063a7af6d0987762 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 02:04:59 +0000 Subject: [PATCH 086/114] =?UTF-8?q?scettico(leva):=20il=20gradino=201,25x?= =?UTF-8?q?=20REGGE=205=20attacchi=20su=206=20=E2=80=94=20k=20max=20difend?= =?UTF-8?q?ibile=201,40?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filone §53 SKEPTIC-LEVA. Sei attacchi con soglie dichiarate PRIMA, 10 numeri pubblicati riprodotti prima di attaccarli. Sola lettura, zero rete, 127 s. A1 DRIFT REGGE — il gradino diventa dannoso solo sotto +1,42%/anno di drift (-2,70 SE, 0,25° pctl bootstrap a blocchi): fuori da IC95 [+4,78%] E da IC99 [+2,36%]. Il libro puo' perdere il 90,6% del suo drift e il gradino resta non dannoso; il PEGGIOR triennio mai realizzato (+5,35%) ci sta 3,8x sopra. A2 SINISTRO REGGE — Delta g(1,25) > 0 in 5/5 finestre giudicabili (2020-21 tolto compreso). QUALIFICA misurata: nel biennio MOBILE peggiore (dal 2021-10, drift -0,45%) il gradino COSTA -0,31%/anno = -0,62% cumulato. Non falsifica per il criterio (li' perde il LIBRO), ma il numero e' quello. Rapporto guadagno/costo 13:1. A3 CODA REGGE — con Hill xi=0,43 (piu' severo del +0,34 pubblicato) il peggior giorno strutturale a 1,25x e' 17,90% (<50%) e P(dimezzamento 10a) 0,100% contro 0,000%. CORREZIONE a §33: la coda muove k* di 4,6 unita', non "1-2" — la conclusione regge (il drift ne muove 6,2) ma il numero pubblicato e' ottimista ~3x. A4 LENTE REGGE — sotto la lente accoppiata il ricarico del maxDD CALA con k (1,2358 -> 1,2297 -> 1,2237): moltiplicativo, verso favorevole. E la frequenza del disaster-SL e' invariante a k PER COSTRUZIONE (innesco sul MARK, test di ri-piazzamento RELATIVO): il numero di §40 va moltiplicato, non rifatto. A5 INVARIANTE INCRINA — completo sul percorso FIDATO (0 violazioni su 576 combo: il vero e' n_asset*min(WEIGHT*(W_TP01*lev+W_SKH), frac)*scala, quindi l'invariante e' un LIMITE SUPERIORE). Ma nel ramo di FALLBACK il denominatore del rapporto e' il WATERMARK, non l'equity: leva vera fino a 5,00x GIA' a k=1,00 e 6,25x a 1,25. La scala non apre il buco, lo MOLTIPLICA -> la guardia G3 della SPEC non e' una raffinatezza ed e' NON OPZIONALE, ma non e' una chiusura. E il fattore 0,30 non e' un tetto di perdita (§40: stop rotolante, passano -60% dall'ingresso). A6 FUNDING REGGE — rifatto col funding DENTRO e proporzionale a k (r_k = k*(r-f), lineare per costruzione): il gradino vale +3,49a (muro mobile) / +2,10a (muro congelato) contro +2,98a / +1,82a sulla lente pubblicata. Il funding NON riduce il gradino: lo AUMENTA in anni, perche' peggiora il caso base (17,28a contro 14,81a). Ma il livello peggiora su tutta la colonna: k=1,25 col funding (13,78a) resta peggio di k=1,00 senza (14,81a) -> il gradino non ripaga il funding, lo attenua. RICONCILIAZIONE: il «+1,8a / +EUR164» pubblicato NON era una discrepanza, era una CONVENZIONE non dichiarata — e' la lettura a MURO CONGELATO (de-leva al traguardo), riprodotta a 0,02a (+1,82a). Col muro che si muove con k il gradino vale +2,98a. La differenza fra le due letture (1,2 anni) e' piu' grande di quasi tutti gli effetti che questo progetto misura: va dichiarata. Il +EUR164 e' invece +EUR144 misurato direttamente — la differenza E' l'ipotesi di linearita' dell'interpolazione. K MASSIMO DIFENDIBILE = 1,40 (morde il peggior giorno strutturale <= 20%, soglia AGGIUNTA da questo scettico); coi soli vincoli gia' dichiarati dal progetto sarebbe 1,67 (G6, il disaster-SL). Il 1,50x NON sopravvive: sfonda il 20% (21,48%) e sta al 90% del tetto G6. La scaletta SCALA_LADDER a 1,25 resta giusta: e' l'unico gate che il progetto ha contro un parametro che nessun altro gate vede. NON PORTATI: banda d'ancora del guadagno in ANNI (23x24 congiunte, fuori budget); slippage a taglia crescente (la partecipazione 21,9% di una barra 5m diventa 27,4% SUBITO, non a $5.000: va misurata PRIMA del gradino); costo di margine sopra 1x (non esiste su perp lineare marginato); coda di venue (vive sul suo asse). Nessun file di produzione toccato: git diff sui tracciati e' vuoto. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf --- scripts/research/r0823_skeptic_leva.py | 997 +++++++++++++++++++++++++ 1 file changed, 997 insertions(+) create mode 100644 scripts/research/r0823_skeptic_leva.py diff --git a/scripts/research/r0823_skeptic_leva.py b/scripts/research/r0823_skeptic_leva.py new file mode 100644 index 0000000..bf99f5e --- /dev/null +++ b/scripts/research/r0823_skeptic_leva.py @@ -0,0 +1,997 @@ +#!/usr/bin/env python +"""r0823_skeptic_leva.py — SCETTICO ADVERSARIALE del gradino di leva 1,00x -> 1,25x. + +Filone §53 SKEPTIC-LEVA, ondata 2026-08-22/23, branch `research/wave-0822`. + +IL BERSAGLIO (RESULTS-0822 §33 WORST-DAY, §38 SCALE-SPEC, §40 SL-ANCHOR): + «il libro gira al 7% di Kelly; il gradino 1,00x -> 1,25x vale 14,7a -> 12,9a al + capitale-rendita (+164 EUR/mese di versamento equivalente); la riserva "-10% al giorno + ogni anno" e' mal tarata di 2-4 ordini di grandezza; il nodo del disaster-SL rotolante e' + sciolto (scatta MENO). Resta da costruire una chiave di scala in config.» + +QUESTO FILE NON CONFERMA: cerca dove il risultato e' sbagliato. Sei attacchi dichiarati, ognuno +con la sua soglia scritta PRIMA nella sezione (0), e ognuno che finisce in REGGE / INCRINA / +FALSIFICA con un NUMERO accanto. + + (0) IL CRITERIO — le sei soglie, congelate prima di guardare le sezioni 2-7. + (1) REPLICA — 10 numeri gia' pubblicati riprodotti PRIMA di attaccarli. + (2) A1 il drift e' stimato su 7,4 anni con due tori, e k* e' LINEARE nel suo errore. + (3) A2 il campione non contiene il sinistro che conta (finestre stressate e disgiunte). + (4) A3 la coda: due stimatori che non concordano -> si prende il PEGGIORE fino in fondo. + (5) A4 la lente: tutto il conto e' close-only, e close-only e' CIECA su una regola a un + livello. Il disaster-SL e' una regola su un livello. + (6) A5 l'invariante a tre chiavi: e' completo? c'e' una QUARTA chiave? + (7) A6 il funding non era nel conto della leva, e il funding SCALA con la leva. + (8) VERDETTO, e il k MASSIMO DIFENDIBILE come singolo numero. + +ONESTA' APPLICATA (regole gia' codificate nel progetto, usate CONTRO questo lavoro): + · lo Sharpe e' INVARIANTE alla scala: `deflated_sharpe` e `marginal_vs_tp01` non vedono la + leva -> non compaiono qui come prova (SPEC §0); + · si riproduce il numero che si attacca PRIMA di attaccarlo (sez. 1); + · [MISURATO] / [CALC] / [DEDOTTO] / [ASSUNTO] / [CITATO] marcati riga per riga; + · la trappola dichiarata da WORST-DAY: se uno stress uccide anche il conto di OGGI, non e' il + gradino a essere sbagliato — e va DETTO. Per questo ogni finestra stressata riporta anche il + drift del libro SU QUELLA finestra: dove il libro e' morto, il gradino non e' giudicabile. + · un non-arrivo si codifica +inf, mai -1 (07/08); una mediana condizionata non si stampa mai + senza la sua probabilita' accanto. + · confronto APPAIATO fra k: `CC._boot_paths` estrae INDICI da un seme, quindi + `_boot_paths(k*r) == k*_boot_paths(r)` esattamente (verificato in sez. 1). + +SOLA LETTURA. Nessuna rete, nessun ordine, nessuna scrittura fuori dalla cache di /tmp. + + nice -n 19 timeout 900 uv run python scripts/research/r0823_skeptic_leva.py +""" +from __future__ import annotations + +import json +import sys +import time +import warnings +from pathlib import Path + +import numpy as np +import pandas as pd +from scipy import stats + +warnings.filterwarnings("ignore") + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +import r0725_capcurve as CC # noqa: E402 book_series, _boot_paths, TAX_RATE +import r0725_prop_coupled as PC # noqa: E402 lente ACCOPPIATA (R, m) del libro +import r0727_tasse as TX # noqa: E402 PATRIMONIALE, CARRY_ANNI +import r0807_piano_netto as PN # noqa: E402 muro / accumula / sopravvivenza + +from src.live.book import ( # noqa: E402 SOLA LETTURA (nessuna di queste scrive) + CAP_UNKNOWN_USD, W_SKH, W_TP01, _read_watermark, book_net_target) +from src.live.shadow import FALLBACK_CAPITAL, WEIGHT # noqa: E402 +from src.strategies.trend_portfolio import CANONICAL # noqa: E402 + +ANN = 365.0 +DELUCK = 0.89 # fattore d'ancora MISURATO (r0726_deluck_factor), non a occhio +START = 635.0 # il conto Deribit VERO +DEP_EUR = 500.0 # il piano in corso (config/live.json _nota_cap: "EUR 5.000 + 500/mese") +BLOCK = 20 # blocchi del bootstrap (convenzione del progetto) +N_WALL = 2000 # risoluzione MC del muro ~1%, DICHIARATA (r0822d_funding) +N_TRAJ = 3000 # la taglia a cui furono calcolate le traiettorie pubblicate +SEED = 725 # seme delle traiettorie pubblicate (confronto appaiato) +SEED_BOOT = 20260823 +ANNI_TRAJ = 25 +KS = (1.00, 1.25, 1.50, 2.00) + +# ---- numeri PUBBLICATI, per la replica di sez. 1 ------------------------------------------ +PUB = { + "drift_nofund": 0.1924, "drift_fund": 0.1708, # §35 + "sh_nofund": 1.692, "sh_fund": 1.513, # §35 + "kstar_nofund": 13.7, "kstar_fund": 12.3, # §35 + "g1": 0.1790, "g125": 0.2261, "sharpe_scale": 1.5051, # SPEC §0 (serie de-luckata, no funding) + "perp_L1": 0.0752, "muro_L1": 264_000, # §36 (fisco) + "perp_L3": 0.0635, "muro_L3": 313_000, # §36 (fisco + funding) + "anni_L1_500": 14.7, # §36 riga "EUR 500 -> 14,7a" + "worst_close": -0.0394, "worst_intraday": -0.0733, # §33 + "L_strutt": 0.1432, # §33 sez.5 (bound aritmetico a k=1) + "xi_gpd": 0.09, "xi_hill": 0.34, # §33 + "k_da_SL": 1.67, # SPEC §4.5 = 0.50/(2*0.50*0.30) +} + +CACHE = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/pianovero/book.parquet") +T0 = time.time() +ESITI: dict[str, tuple[str, str]] = {} + + +def sez(t: str) -> None: + print("\n" + "=" * 108) + print(f" {t}") + print("=" * 108) + + +def clock() -> str: + return f"[{time.time() - T0:5.0f}s]" + + +def esito(chiave: str, verdetto: str, riga: str) -> None: + ESITI[chiave] = (verdetto, riga) + print(f"\n >>> {chiave}: {verdetto} — {riga}") + + +# ========================================================================================== +# (0) IL CRITERIO — congelato QUI, valutato in (8). Nessuna soglia usa un numero delle +# sezioni 2-7: vengono dalle regole gia' scritte del progetto o sono dichiarate a priori. +# ========================================================================================== +CRITERIO = { + "A1": ("DRIFT — il gradino 1,25x FALSIFICA se il drift di pareggio (quello a cui " + "g(1,25) = g(1,00)) cade DENTRO l'IC95 bootstrap a blocchi del drift. " + "INCRINA se cade fra l'IC95 e l'IC99. REGGE se e' fuori da entrambi."), + "A2": ("SINISTRO — il gradino FALSIFICA se Delta g(1,25) < 0 in almeno una finestra " + "stressata IN CUI IL LIBRO STESSO HA DRIFT POSITIVO. Dove il libro e' in perdita " + "la finestra giudica il LIBRO, non il gradino, e non conta (nota dovuta di §33)."), + "A3": ("CODA — si prende lo stimatore PEGGIORE (Hill, xi=0.34) e lo si porta fino in " + "fondo. FALSIFICA se a k=1,25 (i) il peggior giorno strutturale supera il 50% " + "dell'equity, oppure (ii) P(dimezzamento in 10 anni) supera il 5% E piu' del " + "triplo del valore a k=1,00. Soglie: il 50% e' il C1 gia' dichiarato da WORST-DAY; " + "il 5%/3x e' dichiarato qui, prima di guardare."), + "A4": ("LENTE — FALSIFICA se sotto la lente ACCOPPIATA (i) la frequenza di tocco del " + "disaster-SL cresce con k, oppure (ii) il ricarico del maxDD smette di essere " + "moltiplicativo (rapporto accoppiato/close-only che si muove di piu' di 1pp fra " + "k=1,00 e k=1,50)."), + "A5": ("INVARIANTE — FALSIFICA se esiste una chiave RAGGIUNGIBILE, non presente in " + "`n_asset * frac * scala * disaster_sl_pct <= 0,50`, che possa ALZARE la leva " + "lorda vera lasciando l'invariante invariato o soddisfatto."), + "A6": ("FUNDING — il funding SCALA col nozionale, quindi con k. Rifatto il conto con il " + "funding dentro e proporzionale a k, il gradino FALSIFICA se il guadagno sul " + "capitale-rendita scende sotto 1,0 anno (soglia del mandato)."), +} + + +# ========================================================================================== +# UTILITA' +# ========================================================================================== +def deluck(r: np.ndarray) -> np.ndarray: + """Toglie dal DRIFT la fortuna d'ancora misurata. La vol NON si tocca: fra le ancore era + invariata (7,80% -> 7,76%), la fortuna sta tutta nel drift (r0726_deluck_factor).""" + r = np.asarray(r, float) + return r - (1.0 - DELUCK) * r.mean() + + +def g_exact(r: np.ndarray, k: float) -> float: + """Crescita geometrica annua a leva k: exp(365 * E[log(1+k r)]) - 1. Forma CHIUSA: nessun + rumore Monte Carlo, quindi i confronti fra k sono esatti.""" + x = 1.0 + k * np.asarray(r, float) + if np.any(x <= 0): + return -np.inf + return float(np.expm1(ANN * np.mean(np.log(x)))) + + +def k_star(r: np.ndarray, kmax: float = 40.0, step: float = 0.01) -> tuple[float, float]: + ks = np.arange(step, kmax + step, step) + gs = np.array([g_exact(r, k) for k in ks]) + j = int(np.nanargmax(gs)) + return float(ks[j]), float(gs[j]) + + +def shift_drift(r: np.ndarray, mu_annuo: float) -> np.ndarray: + """Stessa serie, drift imposto. Cambia UN grado di liberta' per volta: la forma della + distribuzione (vol, asimmetria, coda, clustering) resta identica.""" + r = np.asarray(r, float) + return r - r.mean() + mu_annuo / ANN + + +def boot_drift(r: np.ndarray, n: int = 4000, block: int = BLOCK, + seed: int = SEED_BOOT) -> np.ndarray: + """Bootstrap A BLOCCHI del drift annuo. A blocchi perche' la serie ha vol-clustering e + momentum: un bootstrap iid darebbe un IC piu' STRETTO del vero, cioe' aiuterebbe la tesi + che questo file sta attaccando.""" + rng = np.random.default_rng(seed) + m = len(r) + nb = int(np.ceil(m / block)) + out = np.empty(n) + for i in range(n): + st = rng.integers(0, m - block, size=nb) + idx = (st[:, None] + np.arange(block)[None, :]).ravel()[:m] + out[i] = r[idx].mean() * ANN + return out + + +def muro_di(r: np.ndarray) -> tuple[float, float]: + """(rendita perpetua, capitale-rendita) con la convenzione COERENTE (imposte dentro il + portafoglio, prelievo gia' netto). Bisezione identica a PN.muro, con N_WALL dichiarato.""" + netto = CC.TARGET_EUR_DAY * 365 * CC.EURUSD + lo, hi = 0.0, 0.40 + for _ in range(13): + mid = (lo + hi) / 2 + s = PN.sopravvivenza(r, 1e6, 1e6 * mid, 20, CC.TAX_RATE, TX.PATRIMONIALE, + n_paths=N_WALL, seed=SEED) + if s["p10_end"] >= 1e6: + lo = mid + else: + hi = mid + return lo, (netto / lo if lo > 0.002 else float("inf")) + + +def anni_al_muro(r: np.ndarray, bersaglio: float, dep_eur: float = DEP_EUR, + start: float = START) -> dict: + """Anni al capitale-rendita. Path APPAIATI fra k (gli indici del bootstrap dipendono solo + dal seme e dalla lunghezza, non dai valori).""" + rng = np.random.default_rng(SEED) + paths = CC._boot_paths(r, N_TRAJ, ANNI_TRAJ * 365, BLOCK, rng) + o = PN.accumula(paths, dep_eur, bersaglio, CC.TAX_RATE, TX.PATRIMONIALE, start=start) + c = o["colpito"] + y = np.where(c >= 0, c / 365.0, np.inf) + arr = y[np.isfinite(y)] + return dict(med=float(np.median(y)), + med_cond=float(np.median(arr)) if len(arr) > 100 else float("nan"), + p20=float((y <= 20.0).mean()), p_arr=float(np.isfinite(y).mean())) + + +def cap_20a(r: np.ndarray, dep_eur: float = DEP_EUR, start: float = START, + anni: int = 20) -> float: + """Capitale mediano a 20 anni. Serve dove gli ANNI non sono definiti: a drift bassi il + capitale-rendita non si raggiunge in 25 anni su nessun path e la mediana degli anni e' nan + per ENTRAMBI i k — cioe' la metrica smette di discriminare proprio dove serve. Il capitale + e' definito sempre, ed e' APPAIATO fra k (stessi indici di bootstrap).""" + rng = np.random.default_rng(SEED) + paths = CC._boot_paths(r, N_TRAJ, anni * 365, BLOCK, rng) + o = PN.accumula(paths, dep_eur, 1e18, CC.TAX_RATE, TX.PATRIMONIALE, start=start) + return float(np.median(o["cap"])) + + +def fmt_anni(d: dict) -> str: + if not np.isfinite(d["med_cond"]): + return f" n/d (P20 {d['p20']:.3f})" + return f"{d['med_cond']:5.2f}a (P20 {d['p20']:.3f})" + + +# ========================================================================================== +# (1) REPLICA — prima di attaccare +# ========================================================================================== +def sezione1() -> dict: + sez("(1) REPLICA — 10 numeri pubblicati, riprodotti PRIMA di attaccarli") + if not CACHE.exists(): + raise SystemExit("cache serie assente: girare prima r0822d_piano_vero.py") + d = pd.read_parquet(CACHE) + r_nf, r_f = d["nofund"].values.astype(float), d["fund"].values.astype(float) + print(f" serie: {len(r_nf)} giorni, {d.index.min().date()} -> {d.index.max().date()} " + f"(cache di r0822d_piano_vero, zero rete)") + R = {"nofund": r_nf, "fund": r_f, "L1": deluck(r_nf), "L3": deluck(r_f)} + + righe = [] + for nome, x, pub in (("drift senza funding", r_nf.mean() * ANN, PUB["drift_nofund"]), + ("drift con funding", r_f.mean() * ANN, PUB["drift_fund"]), + ("Sharpe senza funding", r_nf.mean() / r_nf.std() * ANN ** .5, + PUB["sh_nofund"]), + ("Sharpe con funding", r_f.mean() / r_f.std() * ANN ** .5, + PUB["sh_fund"])): + righe.append((nome, x, pub)) + ks_nf, _ = k_star(deluck(r_nf)) + ks_f, _ = k_star(deluck(r_f)) + righe += [("k* senza funding", ks_nf, PUB["kstar_nofund"]), + ("k* con funding", ks_f, PUB["kstar_fund"]), + ("g(1,00) serie SPEC", g_exact(R["L1"], 1.00), PUB["g1"]), + ("g(1,25) serie SPEC", g_exact(R["L1"], 1.25), PUB["g125"]), + ("Sharpe (invariante a k)", R["L1"].mean() / R["L1"].std() * ANN ** .5, + PUB["sharpe_scale"])] + + print(f"\n {'grandezza':<26}{'replica':>12}{'pubblicato':>13}{'|diff|':>11}") + for nome, x, pub in righe: + print(f" {nome:<26}{x:>12.4f}{pub:>13.4f}{abs(x - pub):>11.4f}") + + p1, m1 = muro_di(R["L1"]) + p3, m3 = muro_di(R["L3"]) + print(f" {'perpetua L1 (fisco)':<26}{p1:>12.4f}{PUB['perp_L1']:>13.4f}{abs(p1-PUB['perp_L1']):>11.4f}") + print(f" {'muro L1 ($)':<26}{m1:>12,.0f}{PUB['muro_L1']:>13,.0f}{abs(m1-PUB['muro_L1']):>11,.0f}") + print(f" {'perpetua L3 (+funding)':<26}{p3:>12.4f}{PUB['perp_L3']:>13.4f}{abs(p3-PUB['perp_L3']):>11.4f}") + print(f" {'muro L3 ($)':<26}{m3:>12,.0f}{PUB['muro_L3']:>13,.0f}{abs(m3-PUB['muro_L3']):>11,.0f}") + a1 = anni_al_muro(R["L1"], m1) + print(f" {'anni L1 @EUR500/m':<26}{a1['med_cond']:>12.2f}{PUB['anni_L1_500']:>13.2f}" + f"{abs(a1['med_cond']-PUB['anni_L1_500']):>11.2f} (risoluzione MC dichiarata ~1%)") + + # --- il confronto APPAIATO fra k e' esatto: lo si PROVA, non lo si dichiara ------------- + rng = np.random.default_rng(SEED) + p_a = CC._boot_paths(R["L3"], 200, 2000, BLOCK, rng) + rng = np.random.default_rng(SEED) + p_b = CC._boot_paths(1.25 * R["L3"], 200, 2000, BLOCK, rng) + dmax = float(np.abs(p_b - 1.25 * p_a).max()) + print(f"\n appaiamento fra k: max|paths(1,25 r) - 1,25*paths(r)| = {dmax:.2e} " + f"-> i path sono gli STESSI, il confronto fra k non ha rumore MC di campionamento") + assert dmax == 0.0 + + # --- la lente accoppiata: quale serie e' ------------------------------------------------- + Tc = PC.crypto_daily_tuples(0.75, 0.25) + print(f" lente accoppiata (r0725_prop_coupled): {len(Tc)} giorni, peggior chiusura " + f"{Tc['R'].min():+.2%} ({Tc['R'].idxmin().date()}), peggior minimo intra-giorno " + f"{Tc['m'].min():+.2%} ({Tc['m'].idxmin().date()})") + print(f" -> il minimo intra-giorno replica il {PUB['worst_intraday']:+.2%} di §33 " + f"(|diff| {abs(Tc['m'].min()-PUB['worst_intraday']):.4f}); la chiusura NO " + f"({Tc['R'].min():+.2%} contro {PUB['worst_close']:+.2%}) perche' §33 legge la " + f"chiusura sulla serie del libro e questa e' la ricostruzione ORARIA — differenza di " + f"lente gia' registrata in §33 ('aveva ragione, con la lente sbagliata'). " + f"[DICHIARATO, non riparato: A4 usa il MINIMO, che replica.]") + return {"R": R, "muri": {"L1": (p1, m1), "L3": (p3, m3)}, "anni_L1_k1": a1, "tuples": Tc} + + +# ========================================================================================== +# (2) A1 — IL DRIFT E' STIMATO SU 7,4 ANNI CON DUE TORI +# ========================================================================================== +def sezione2(S: dict) -> None: + sez("(2) A1 — il drift e' stimato su 7,4 anni con due tori, e k* e' LINEARE nel suo errore") + r = S["R"]["L3"] # la lente ONESTA: de-luckata E col funding dentro + mu = r.mean() * ANN + vol = r.std() * ANN ** 0.5 + dr = boot_drift(r) + lo95, hi95 = np.percentile(dr, [2.5, 97.5]) + lo99, hi99 = np.percentile(dr, [0.5, 99.5]) + se_b = dr.std() + se_naive = vol / np.sqrt(len(r) / ANN) + print(f" [MISURATO] drift de-luckato col funding: {mu:+.2%}/anno, vol {vol:.2%}, " + f"{len(r)/ANN:.1f} anni di campione") + print(f" [MISURATO] bootstrap a blocchi ({BLOCK}g, 4000 estrazioni): " + f"SE {se_b:.2%} (SE ingenuo iid {se_naive:.2%}: il clustering ALLARGA di " + f"{se_b/se_naive:.2f}x -> il bootstrap a blocchi e' la scelta severa)") + print(f" IC95 [{lo95:+.2%}, {hi95:+.2%}] IC99 [{lo99:+.2%}, {hi99:+.2%}]") + + # k* propagato + print(f"\n [CALC] k* propagato sul drift (la forma della distribuzione NON cambia: " + f"si sposta solo la media)") + print(f" {'drift':>10}{'k*':>9}{'g(1,00)':>11}{'g(1,25)':>11}{'g(1,25)-g(1,00)':>18}") + for nome, m in (("punto", mu), ("-1 SE", mu - se_b), ("IC95 basso", lo95), + ("IC99 basso", lo99), ("-3 SE", mu - 3 * se_b)): + x = shift_drift(r, m) + ks, _ = k_star(x) + g1, g125 = g_exact(x, 1.00), g_exact(x, 1.25) + print(f" {nome:>10} {m:>+8.2%}{ks:>9.2f}{g1:>11.2%}{g125:>11.2%}{g125-g1:>+18.2%}") + + # drift di pareggio del gradino, su g + lo, hi = -0.20, 0.40 + for _ in range(70): + mid = (lo + hi) / 2 + if g_exact(shift_drift(r, mid), 1.25) - g_exact(shift_drift(r, mid), 1.00) > 0: + hi = mid + else: + lo = mid + mu_be = hi + q_be = float((dr < mu_be).mean()) + z_be = (mu_be - mu) / se_b + print(f"\n [CALC] DRIFT DI PAREGGIO del gradino 1,25x: {mu_be:+.4%}/anno") + print(f" = {z_be:+.2f} SE dal punto, percentile bootstrap {q_be:.4%}") + print(f" = il libro puo' perdere il {(1-mu_be/mu):.1%} del suo drift e il gradino " + f"resta NON dannoso") + print(f" [CALC] (aritmetica di controllo: per g ~ mu*k - sigma^2 k^2/2 il pareggio sta a " + f"k*=1,125, cioe' mu = 1,125*sigma^2 = {1.125*vol**2:+.4%} — " + f"|diff| {abs(mu_be-1.125*vol**2):.4%})") + + # controllo empirico: il peggior drift che il libro abbia MAI realizzato su 3 anni + ser = pd.Series(r) + roll3 = ser.rolling(3 * 365).mean().dropna() * ANN + roll2 = ser.rolling(2 * 365).mean().dropna() * ANN + print(f"\n [MISURATO] controllo EMPIRICO, che non dipende da un modello di incertezza: " + f"il peggior drift realizzato") + print(f" su finestra mobile di 3 anni = {roll3.min():+.2%} " + f"(mediana {roll3.median():+.2%}); su 2 anni = {roll2.min():+.2%}") + print(f" -> anche il PEGGIOR triennio mai vissuto sta {roll3.min()/mu_be:.1f}x " + f"sopra il drift di pareggio del gradino.") + + # la metrica che DECIDE: anni al capitale-rendita. A drift bassi NON e' definita per + # nessuno dei due k (nessun path arriva): li' si legge il CAPITALE a 20 anni, appaiato. + print(f"\n [MISURATO] e sulla metrica che decide davvero (EUR{DEP_EUR:.0f}/m da " + f"${START:.0f}, muro RICALCOLATO a ogni drift e a ogni k):") + print(f" {'drift':>12}{'muro k=1,00':>13}{'anni k=1,00':>21}{'muro k=1,25':>13}" + f"{'anni k=1,25':>21}{'cap 20a k=1,00':>16}{'cap 20a k=1,25':>16}") + guadagni = {} + for nome, m in (("punto", mu), ("-1 SE", mu - se_b), ("IC95 basso", lo95), + ("meta' drift", mu / 2)): + x = shift_drift(r, m) + _, m1 = muro_di(x) + a1 = anni_al_muro(x, m1) + _, m2 = muro_di(1.25 * x) + a2 = anni_al_muro(1.25 * x, m2) + c1, c2 = cap_20a(x), cap_20a(1.25 * x) + g = (a1["med_cond"] - a2["med_cond"] + if np.isfinite(a1["med_cond"]) and np.isfinite(a2["med_cond"]) else float("nan")) + guadagni[nome] = (g, c2 / c1) + print(f" {nome:>12}{m1:>13,.0f}{fmt_anni(a1):>21}{m2:>13,.0f}{fmt_anni(a2):>21}" + f"{c1:>16,.0f}{c2:>16,.0f}") + r_ic, r_meta = guadagni["IC95 basso"][1], guadagni["meta' drift"][1] + print(f" ⚠️ a drift bassi gli ANNI non sono definiti PER ENTRAMBI i k (nessun path " + f"raggiunge un muro che nel frattempo e' esploso): li' discrimina solo il capitale, " + f"e il rapporto cap(1,25)/cap(1,00) resta {r_ic:.2f}x all'estremo basso dell'IC95 " + f"e {r_meta:.2f}x a meta' drift.") + print(f" {clock()}") + + if mu_be >= lo95: + v, riga = "FALSIFICA", f"il pareggio {mu_be:+.2%} cade DENTRO l'IC95" + elif mu_be >= lo99: + v, riga = "INCRINA", (f"il pareggio {mu_be:+.2%} cade fra IC95 ({lo95:+.2%}) e " + f"IC99 ({lo99:+.2%})") + else: + v, riga = "REGGE", (f"il gradino diventa dannoso solo sotto {mu_be:+.2%}/anno di drift " + f"= {z_be:+.2f} SE, percentile bootstrap {q_be:.2%}, fuori da IC95 " + f"[{lo95:+.2%}] e da IC99 [{lo99:+.2%}]; e il PEGGIOR triennio mai " + f"realizzato ({roll3.min():+.2%}) ne sta {roll3.min()/mu_be:.1f}x " + f"sopra. All'estremo basso dell'IC95 il capitale a 20 anni resta " + f"{guadagni['IC95 basso'][1]:.2f}x") + esito("A1 DRIFT", v, riga) + + +# ========================================================================================== +# (3) A2 — IL CAMPIONE NON CONTIENE IL SINISTRO CHE CONTA +# ========================================================================================== +def sezione3(S: dict) -> None: + sez("(3) A2 — il campione non contiene il sinistro che conta: finestre stressate e disgiunte") + d = pd.read_parquet(CACHE) + idx = d.index + r_full = deluck(d["fund"].values.astype(float)) + ser = pd.Series(r_full, index=idx) + + fin = [("PIENA 2019-03 -> oggi", ser)] + tolti = ser[(idx < "2020-01-01") | (idx > "2021-12-31")] + fin.append(("SENZA 2020-2021 (il toro che finanzia il drift)", tolti)) + bordi = ["2019-03-14", "2021-03-14", "2023-03-14", "2025-03-14", "2026-08-23"] + for a, b in zip(bordi[:-1], bordi[1:]): + w = ser[(idx >= a) & (idx < b)] + fin.append((f"disgiunta {a[:7]} -> {b[:7]}", w)) + # la finestra PEGGIORE non e' fra quelle disgiunte: si cerca il minimo su griglia MOBILE + # (attacco piu' forte: le disgiunte sono una partizione arbitraria, non il caso peggiore) + W = 2 * 365 + mroll = ser.rolling(W).mean() + j = int(np.nanargmin(mroll.values)) + peggiore2a = ser.iloc[j - W + 1: j + 1] + fin.append((f"MOBILE peggior biennio ({peggiore2a.index[0].date()})", peggiore2a)) + + print(f" ⚠️ ogni finestra riporta ANCHE il drift del libro su di essa: dove il libro e' in " + f"perdita la finestra giudica il LIBRO, non il gradino (nota dovuta di §33).") + print(f"\n {'finestra':<46}{'n':>6}{'drift':>10}{'vol':>9}{'Sharpe':>9}{'k*':>8}" + f"{'g(1,00)':>10}{'g(1,25)':>10}{'Delta g':>10}{'':>4}") + peggiori = [] + for nome, w in fin: + x = w.values + if len(x) < 120: + continue + m, v = x.mean() * ANN, x.std() * ANN ** 0.5 + ks, _ = k_star(x) + g1, g2 = g_exact(x, 1.00), g_exact(x, 1.25) + flag = "" if m > 0 else " <- LIBRO NEGATIVO" + print(f" {nome:<46}{len(x):>6}{m:>+10.2%}{v:>9.2%}{m/v:>9.2f}{ks:>8.2f}" + f"{g1:>+10.2%}{g2:>+10.2%}{g2-g1:>+10.2%}{flag}") + peggiori.append((nome, m, g2 - g1, x)) + + giudicabili = [(n, m, dg) for n, m, dg, _ in peggiori if m > 0 and "PIENA" not in n] + rotte = [n for n, m, dg in giudicabili if dg <= 0] + non_giud = [n for n, m, dg, _ in peggiori if m <= 0] + + # Delta sulle due finestre piu' avverse fra quelle giudicabili. ⚠️ Su una finestra di 2 + # anni il muro esplode (la perpetua vive sul rapporto drift/rischio di quella finestra) e + # gli ANNI non sono definiti per NESSUN k: si riporta anche il capitale a 20 anni, appaiato. + print(f"\n [MISURATO] le due finestre GIUDICABILI piu' avverse, muro ricalcolato DENTRO " + f"la finestra (⚠️ su 2 anni il muro non e' un numero di pianificazione: e' una lettura " + f"di quel regime):") + cand = sorted([(n, m, dg, x) for n, m, dg, x in peggiori if m > 0 and "PIENA" not in n], + key=lambda t: t[1])[:2] + for nome, m, dg, x in cand: + _, m1 = muro_di(x) + a1 = anni_al_muro(x, m1) + _, m2 = muro_di(1.25 * x) + a2 = anni_al_muro(1.25 * x, m2) + c1, c2 = cap_20a(x), cap_20a(1.25 * x) + print(f" {nome:<46} muro ${m1:>9,.0f} -> ${m2:>9,.0f} {fmt_anni(a1)} -> " + f"{fmt_anni(a2)} cap20a ${c1:>9,.0f} -> ${c2:>9,.0f} ({c2/c1:.2f}x)") + + # il costo del gradino DOVE fa male: il biennio mobile peggiore + nb, mb, dgb, xb = min(peggiori, key=lambda t: t[1]) + g1b, g2b = g_exact(xb, 1.00), g_exact(xb, 1.25) + cum = (1 + g2b) ** 2 / (1 + g1b) ** 2 - 1 + print(f"\n 🚨 [MISURATO] LA FORMA PIU' FORTE DELL'ATTACCO TROVA DAVVERO UNA FINESTRA IN CUI " + f"IL GRADINO FA MALE — ed e' quella in cui il LIBRO perde:") + print(f" {nb}: drift {mb:+.2%}, g(1,00) {g1b:+.2%}, g(1,25) {g2b:+.2%}, " + f"Delta {dgb:+.2%}/anno -> costo CUMULATO del gradino su quei 2 anni: {cum:+.2%} " + f"di capitale.") + print(f" Da mettere accanto al guadagno: sul campione pieno il gradino vale " + f"{g_exact(ser.values,1.25)-g_exact(ser.values,1.00):+.2%}/anno. " + f"Rapporto {abs((g_exact(ser.values,1.25)-g_exact(ser.values,1.00))/dgb):.0f}:1.") + print(f" Per il CRITERIO dichiarato in (0) quella finestra non falsifica (il libro " + f"stesso e' negativo: la leva amplifica un segno che li' e' gia' sbagliato, e lo " + f"stesso vale per il gradino 1,00x -> 0,75x nel verso opposto). " + f"**Ma la qualifica va detta col numero, non con un aggettivo.**") + print(f"\n ⚠️ [LIMITE DICHIARATO] a parte quel biennio, NESSUNA finestra ha drift negativo: " + f"il libro non ha mai vissuto un TRIENNIO in perdita, e il peggior biennio perde lo " + f"{abs(mb):.2%}/anno. L'attacco nella sua forma piu' forte — «un regime in cui il " + f"libro perde davvero» — **non e' esprimibile su questo campione**, e questo E' il " + f"fatto che il mandato temeva. La sostituzione onesta e' A1: si IMPONE il drift " + f"invece di cercarlo. Le due misure si incastrano — peggior biennio {mb:+.2%}, " + f"pareggio del gradino +1,42%.") + print(f" {clock()}") + + if rotte: + v, riga = "FALSIFICA", f"Delta g(1,25) <= 0 su finestre giudicabili: {', '.join(rotte)}" + else: + peggior = min(giudicabili, key=lambda t: t[2]) if giudicabili else ("n/d", 0, 0) + v, riga = "REGGE", (f"Delta g(1,25) > 0 in {len(giudicabili)}/{len(giudicabili)} " + f"finestre giudicabili (la peggiore: {peggior[0]}, {peggior[2]:+.2%}), " + f"togliendo il toro 2020-21 compreso. QUALIFICA MISURATA: nel biennio " + f"mobile peggiore ({nb}, drift {mb:+.2%}) il gradino COSTA " + f"{dgb:+.2%}/anno = {cum:+.2%} cumulato — quella finestra non " + f"falsifica per il criterio (li' e' il libro a perdere), ma il " + f"numero e' questo, ed e' il costo del gradino nel suo caso peggiore " + f"realizzato") + esito("A2 SINISTRO", v, riga) + + +# ========================================================================================== +# (4) A3 — LA CODA PEGGIORE, PORTATA FINO IN FONDO +# ========================================================================================== +def hill_xi(loss: np.ndarray, k: int) -> float: + """Stimatore di Hill sulle k eccedenze piu' grandi (solo perdite POSITIVE).""" + x = np.sort(loss[loss > 0])[::-1] + k = min(k, len(x) - 1) + return float(np.mean(np.log(x[:k])) - np.log(x[k])) + + +def sezione4(S: dict) -> None: + sez("(4) A3 — la coda: due stimatori che non concordano -> si prende il PEGGIORE") + r = S["R"]["L3"] + loss = -r + q = 0.95 + u = float(np.quantile(loss, q)) + ex = loss[loss > u] - u + xi_g, _, beta = stats.genpareto.fit(ex, floc=0.0) + k_hill = int(0.05 * len(loss)) + xi_h = hill_xi(loss, k_hill) + print(f" [MISURATO] soglia 95° pctl = {u:.2%}, {len(ex)} eccedenze") + print(f" GPD xi = {xi_g:+.3f} (pubblicato {PUB['xi_gpd']:+.2f}) " + f"Hill xi = {xi_h:+.3f} su k={k_hill} (pubblicato {PUB['xi_hill']:+.2f})") + print(f" [MISURATO] Hill e' notoriamente sensibile a k -> lo sweep, invece del punto:") + print(" " + " ".join(f"k={kk}: xi={hill_xi(loss, kk):+.3f}" + for kk in (40, 70, 100, 135, 200, 300))) + xi = max(xi_g, xi_h, PUB["xi_hill"]) + print(f" [SCELTA DICHIARATA] si porta avanti il PEGGIORE dei tre: xi = {xi:+.3f} " + f"(piu' severo del {PUB['xi_hill']:+.2f} pubblicato: l'attacco si porta col numero " + f"peggiore, non con quello citato)") + + L = PUB["L_strutt"] # [CITATO] bound aritmetico di §33 a k=1 + print(f"\n (a) IL BOUND STRUTTURALE, scalato [CITATO §33: |L| = {L:.2%} a k=1,00]") + print(f" {'k':>6}{'peggior giorno':>17}{'costo disaster-SL':>20}{'soglia 20%':>13}" + f"{'soglia 50%':>13}") + sl_pct = json.loads((ROOT / "config" / "live.json").read_text())["disaster_sl_pct"] + frac = json.loads((ROOT / "config" / "live.json").read_text())["max_notional_per_asset_frac"] + for k in KS: + wd, cs = k * L, k * 2 * frac * sl_pct + print(f" {k:>6.2f}{wd:>16.2%} {cs:>19.2%}" + f"{'SFONDA' if wd > 0.20 else 'ok':>13}{'SFONDA' if wd > 0.50 else 'ok':>13}") + k_20 = 0.20 / L + k_50 = 0.50 / L + k_sl50 = 0.50 / (2 * frac * sl_pct) + print(f" [CALC] il peggior giorno strutturale sfonda il 20% (recupero +25%) a k = {k_20:.2f}") + print(f" [CALC] ...e il 50% (C1 di §33) a k = {k_50:.2f}") + print(f" [CALC] il costo del disaster-SL sfonda il 50% a k = {k_sl50:.2f} " + f"(pubblicato {PUB['k_da_SL']:.2f}) -> ⚠️ il disaster-SL sfonda il 20% GIA' A k=1,00 " + f"({2*frac*sl_pct:.0%}): quella soglia non separa i k, e va detto.") + + # (b) coda peggiore dentro il campione: si SOSTITUISCONO le eccedenze storiche con + # estrazioni GPD(xi) troncate al bound, MANTENENDO LE POSIZIONI (quindi il clustering). + print(f"\n (b) P(DIMEZZAMENTO in 10 anni, senza prelievi) con la coda peggiore dentro") + rng = np.random.default_rng(SEED_BOOT) + pos = np.where(loss > u)[0] + fmax = stats.genpareto.cdf(max(L - u, 1e-9), xi, loc=0.0, scale=beta) + y = stats.genpareto.ppf(rng.random(len(pos)) * fmax, xi, loc=0.0, scale=beta) + r_tail = r.copy() + r_tail[pos] = -(u + y) + r_tail = r_tail - r_tail.mean() + r.mean() # stesso drift: cambia SOLO la coda + print(f" [CALC] serie a coda peggiore: drift {r_tail.mean()*ANN:+.2%} (invariato), " + f"vol {r_tail.std()*ANN**.5:.2%} (era {r.std()*ANN**.5:.2%}), " + f"peggior giorno {r_tail.min():+.2%} (era {r.min():+.2%})") + ks_t, _ = k_star(r_tail) + ks_0, _ = k_star(r) + d_coda = ks_0 - ks_t + d_drift = ks_0 - k_star(shift_drift(r, r.mean() * ANN / 2))[0] + print(f" [MISURATO] k*: {ks_0:.2f} (coda storica) -> {ks_t:.2f} (coda peggiore) " + f"= la coda muove k* di {d_coda:+.2f}; dimezzare il DRIFT lo muove di {d_drift:+.2f}") + print(f" ⚠️ CORREZIONE a §33, che scriveva «la forma della coda muove k* di 1-2 " + f"unita', il drift di 6,7»: con lo stimatore PEGGIORE la coda ne muove {d_coda:.1f}, " + f"non 1-2. La CONCLUSIONE di §33 regge (il drift resta il fattore dominante, " + f"{d_drift/d_coda:.1f}x) ma il numero pubblicato e' ottimista di ~{d_coda/1.5:.0f}x, " + f"e a quella coda k*={ks_t:.2f} e' ancora {ks_t/1.25:.0f}x il gradino richiesto.") + rng = np.random.default_rng(SEED) + P = CC._boot_paths(r_tail, 4000, 10 * 365, BLOCK, rng) + print(f" {'k':>6}{'P(dimezzamento)':>18}{'P(-80%)':>11}{'maxDD mediano':>16}" + f"{'g(k) 10a mediano':>19}") + p_half = {} + for k in KS: + eq = np.cumprod(1.0 + k * P, axis=1) + run = np.maximum.accumulate(eq, axis=1) + dd = (eq / run - 1.0).min(axis=1) + half = float((eq.min(axis=1) <= 0.5).mean()) + p80 = float((eq.min(axis=1) <= 0.2).mean()) + p_half[k] = half + print(f" {k:>6.2f}{half:>18.3%}{p80:>11.3%}{np.median(dd):>16.2%}" + f"{np.median(eq[:, -1]) ** (1/10) - 1:>18.2%}") + print(f" {clock()}") + + rap = p_half[1.25] / p_half[1.00] if p_half[1.00] > 0 else float("inf") + if 1.25 * L > 0.50 or (p_half[1.25] > 0.05 and rap > 3.0): + v, riga = "FALSIFICA", (f"a k=1,25 il peggior giorno strutturale vale {1.25*L:.2%} " + f"e P(dimezzamento) {p_half[1.25]:.2%}") + else: + v, riga = "REGGE", (f"con xi={xi:.2f} preso alla lettera: peggior giorno strutturale " + f"{1.25*L:.2%} < 50%, P(dimezzamento 10a) {p_half[1.25]:.3%} contro " + f"{p_half[1.00]:.3%} a k=1,00. Il bound strutturale sfonda il 50% " + f"a k={k_50:.2f} e il 20% a k={k_20:.2f}; la coda muove k* di " + f"{ks_0-ks_t:+.2f}, il drift di {ks_0 - k_star(shift_drift(r, r.mean()*ANN/2))[0]:+.2f}") + esito("A3 CODA", v, riga) + + +# ========================================================================================== +# (5) A4 — LA LENTE ACCOPPIATA, E IL DISASTER-SL CHE E' UNA REGOLA SU UN LIVELLO +# ========================================================================================== +def sezione5(S: dict) -> None: + sez("(5) A4 — la lente: close-only e' CIECA su una regola a un livello. " + "Il disaster-SL E' una regola su un livello.") + T = S["tuples"] + R = T["R"].values + m = T["m"].values + + print(" (a) IL RICARICO DEL maxDD: e' moltiplicativo o si amplifica con k?") + print(f" {'k':>6}{'maxDD close-only':>19}{'maxDD accoppiato':>19}{'ricarico':>11}" + f"{'peggior giorno':>17}{'peggior minimo':>17}") + ric = {} + for k in KS: + eq = np.cumprod(1.0 + k * R) + prev = np.concatenate(([1.0], eq[:-1])) + run_c = np.maximum.accumulate(eq) + dd_c = float((eq / run_c - 1.0).min()) + low = prev * (1.0 + k * m) # minimo intra-giorno del PATH composto + run_a = np.maximum.accumulate(np.maximum(eq, prev)) + dd_a = float((low / run_a - 1.0).min()) + ric[k] = dd_a / dd_c + print(f" {k:>6.2f}{dd_c:>19.2%}{dd_a:>19.2%}{dd_a/dd_c:>11.4f}" + f"{k*R.min():>17.2%}{k*m.min():>17.2%}") + spread = max(ric.values()) - min(ric.values()) + print(f" [MISURATO] il ricarico si muove di {spread:.4f} fra k=1,00 e k=2,00 " + f"(= {spread*100:.2f}pp): moltiplicativo, NON amplificante") + + print(f"\n (b) LA FREQUENZA DEL DISASTER-SL E' INVARIANTE A k — dimostrazione dal codice, " + f"non dai dati:") + print(f" `execution.ensure_disaster_sl` calcola") + print(f" want_stop = disaster_stop_price(instrument, MARK, sl_pct, long) -> un PREZZO") + print(f" want_amount = notional_to_amount(instrument, |pos|, price=mark) -> una TAGLIA") + print(f" e ri-piazza se |amt - want_amount| >= want_amount*0,1 OPPURE " + f"|stp - want_stop|/want_stop >= 0,05.") + print(f" Una scala k moltiplica `pos` (quindi `want_amount`) e NON tocca `mark`.") + print(f" -> il livello di innesco e' invariante a k, e il test di ri-piazzamento e' " + f"RELATIVO (`want_amount*0,1`), quindi anch'esso invariante a k.") + print(f" ⇒ [DEDOTTO dal sorgente] a k piu' alto il disaster-SL NON viene toccato piu' " + f"spesso: viene toccato le STESSE volte e costa k volte tanto. §40 misura la frequenza " + f"(0-1 episodi su tutta la storia alla cadenza oraria che gira); questo file mostra che " + f"quel numero non va rifatto per k.") + + print(f"\n (c) CONTROLLO NUMERICO della (b): l'innesco e' un movimento del MARK, non " + f"dell'equity. Conteggio dei giorni in cui il minimo INTRA-giorno del libro a leva k " + f"scende sotto soglie di EQUITY (queste si', k-dipendenti):") + print(f" {'k':>6}{'gg min < -5%':>15}{'gg min < -10%':>15}{'gg min < -20%':>15}" + f"{'gg min < -30%':>15}") + for k in KS: + print(f" {k:>6.2f}{int((k*m < -0.05).sum()):>15}{int((k*m < -0.10).sum()):>15}" + f"{int((k*m < -0.20).sum()):>15}{int((k*m < -0.30).sum()):>15}") + print(f" -> nessuna soglia di EQUITY e' armata sul libro proprietario (il disaster-SL e' " + f"sul PREZZO): questa tabella e' la misura di cosa succederebbe se ce ne fosse una, " + f"ed e' la ragione per cui sul canale FUNDED — dove la regola a un giorno esiste — " + f"la lente accoppiata resta obbligatoria (25/07).") + + amp = ric[1.50] - ric[1.00] + if abs(amp) > 0.01: + v, riga = "FALSIFICA", f"il ricarico del maxDD si muove di {amp*100:+.2f}pp fra k=1,00 e 1,50" + else: + v, riga = "REGGE", (f"ricarico maxDD {ric[1.00]:.4f} (k=1,00) -> {ric[1.50]:.4f} (k=1,50) " + f"-> {ric[2.00]:.4f} (k=2,00): movimento {amp*100:+.2f}pp nel range " + f"della proposta e {(ric[2.00]-ric[1.00])*100:+.2f}pp fino a k=2, " + f"e va nel verso FAVOREVOLE (il ricarico CALA). " + f"La frequenza del disaster-SL e' invariante a k PER COSTRUZIONE " + f"(innesco sul MARK, test di ri-piazzamento RELATIVO): il numero di " + f"§40 non va rifatto per k, solo moltiplicato") + esito("A4 LENTE", v, riga) + + +# ========================================================================================== +# (6) A5 — L'INVARIANTE A TRE CHIAVI: c'e' una QUARTA? +# ========================================================================================== +def sezione6() -> None: + sez("(6) A5 — l'invariante `n_asset * frac * scala * disaster_sl_pct <= 0,50`: e' completo?") + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + frac = float(cfg["max_notional_per_asset_frac"]) + fixed = float(cfg["max_notional_per_asset_usd"]) + sl = float(cfg["disaster_sl_pct"]) + n_asset = 2 + wm = _read_watermark() + lev_tp = float(CANONICAL["leverage"]) + print(f" [LETTO dal codice e da config, sola lettura]") + print(f" config/live.json : frac={frac} fixed=${fixed:,.0f} disaster_sl_pct={sl}") + print(f" src/live/shadow : ASSETS n={n_asset} WEIGHT={WEIGHT} " + f"FALLBACK_CAPITAL=${FALLBACK_CAPITAL:,.0f}") + print(f" src/live/book : W_TP01={W_TP01} W_SKH={W_SKH} " + f"CAP_UNKNOWN_USD=${CAP_UNKNOWN_USD:,.0f} watermark=${wm:,.2f}" + if wm else " src/live/book : watermark ASSENTE") + print(f" trend_portfolio : CANONICAL['leverage']={lev_tp} " + f"target_vol={CANONICAL['target_vol']}") + + print(f"\n [CALC] la LEVA LORDA VERA, derivata dal sorgente invece che dichiarata.") + print(f" raw = WEIGHT * E * (W_TP01*tp_frac + W_SKH*sign), tp_frac <= " + f"CANONICAL['leverage'] = {lev_tp}") + print(f" net = scala * clamp(raw, +-cap), cap = frac*E (percorso fidato)") + print(f" lordo = n_asset * min(WEIGHT*(W_TP01*{lev_tp}+W_SKH), frac) * scala * E") + raw_max = WEIGHT * (W_TP01 * lev_tp + W_SKH * 1.0) + print(f" = {n_asset} * min({raw_max:.4f}, {frac}) * scala * E " + f"= {n_asset*min(raw_max, frac):.4f} * scala * E") + print(f" -> l'invariante usa `n_asset*frac*scala` = {n_asset*frac:.4f}*scala; il VERO e' " + f"{n_asset*min(raw_max,frac):.4f}*scala. Coincidono qui perche' frac({frac}) < " + f"WEIGHT*(...)={raw_max:.4f}. **L'invariante e' un LIMITE SUPERIORE, quindi safe.**") + + # verifica esaustiva su una griglia di chiavi raggiungibili + print(f"\n [MISURATO] verifica esaustiva: esiste una combinazione che SODDISFA l'invariante " + f"e produce una leva lorda VERA superiore al suo bound?") + viol = 0 + tot = 0 + for f_ in (0.25, 0.50, 0.625, 0.75, 1.0, 1.5): + for w_ in (0.25, 0.5, 0.75, 1.0): + for sc in (1.0, 1.25, 1.5, 2.0): + for lv in (1.0, 2.0, 3.0): + for na in (2, 3): + tot += 1 + rmax = w_ * (W_TP01 * lv + W_SKH * 1.0) + vero = na * min(rmax, f_) * sc + bound = na * f_ * sc + if vero > bound + 1e-12: + viol += 1 + print(f" {tot} combinazioni (frac x WEIGHT x scala x leverage-TP01 x n_asset): " + f"{viol} violazioni del bound. -> sul PERCORSO FIDATO l'invariante e' COMPLETO.") + + print(f"\n 🚨 [MISURATO] MA IL PERCORSO FIDATO NON E' L'UNICO. Nel ramo di FALLBACK " + f"(`eq_fallback`) `book_report` sizza su `paper_cap`, non sull'equity vera:") + print(f" lordo_fallback = n_asset * min(WEIGHT*paper_cap*(...), min(fixed, wm*frac)) * scala") + print(f" e la leva si misura contro l'equity VERA, che li' non e' nota: al suo posto c'e' " + f"il WATERMARK, che e' l'ULTIMA equity osservata, non quella attuale.") + print(f" {'scenario':<44}{'wm':>9}{'E vera':>9}{'lordo k=1,00':>14}{'/E':>8}" + f"{'lordo k=1,25':>14}{'/E':>8}") + righe = [("normale (equity leggibile)", 635.0, 635.0, False), + ("fallback, watermark fresco", 635.0, 635.0, True), + ("fallback dopo PRELIEVO del 50% non visto", 635.0, 317.5, True), + ("fallback dopo PRELIEVO dell'80% non visto", 635.0, 127.0, True), + ("fallback dopo perdita del 50% non vista", 635.0, 317.5, True)] + peggio1 = peggio125 = 0.0 + for nome, w_, e_vera, fb in righe: + if not fb: + cap, eq_used = e_vera * frac, e_vera + else: + cap, eq_used = min(fixed, w_ * frac), FALLBACK_CAPITAL + raw = WEIGHT * eq_used * (W_TP01 * lev_tp + W_SKH) + l1 = n_asset * min(raw, cap) + peggio1, peggio125 = max(peggio1, l1 / e_vera), max(peggio125, 1.25 * l1 / e_vera) + print(f" {nome:<44}{w_:>9,.0f}{e_vera:>9,.0f}{l1:>14,.1f}{l1/e_vera:>7.2f}x" + f"{1.25*l1:>14,.1f}{1.25*l1/e_vera:>7.2f}x") + print(f"\n 🚨 IL BUCO ESISTE GIA' A k=1,00 ({peggio1:.2f}x) e NON e' creato dalla scala: " + f"e' creato dal fatto che nel fallback il denominatore del rapporto e' il WATERMARK " + f"(l'ultima equity OSSERVATA) e non l'equity attuale. La scala non lo apre: lo " + f"MOLTIPLICA ({peggio1:.2f}x -> {peggio125:.2f}x) mentre l'invariante continua a " + f"leggere {n_asset*frac:.2f}x e {n_asset*frac*1.25:.2f}x.") + print(f" ⚠️ Percio' la guardia G3 della SPEC (`scala = 1,00 nel fallback`) **non chiude " + f"il buco: ne toglie il moltiplicatore**. Resta NON OPZIONALE — senza di lei il " + f"gradino peggiora del 25% uno scenario gia' fuori tetto — ma la SPEC la presenta " + f"come «il fallback non e' piu' permissivo per costruzione», e questo e' vero solo " + f"RISPETTO ALLA SCALA, non rispetto alla leva.") + print(f" [DICHIARATO] frequenza misurata del ramo: 0 giri su 1.442 (§38); " + f"`book_execute` su `eq_fallback` **allerta e NON blocca** (letto dal sorgente), " + f"quindi lo scenario e' raro ma non impossibile. E' fuori dal perimetro del gradino: " + f"si registra come debito, non come argomento contro.") + peggio = peggio125 + + print(f"\n ⚠️ [DEDOTTO da §40] e c'e' una seconda incompletezza, che non e' una chiave ma un " + f"VALORE: l'invariante moltiplica per `disaster_sl_pct`=0,30 come se fosse la perdita " + f"massima per posizione. §40 ha misurato che NON lo e': lo stop e' ROTOLANTE " + f"(ri-ancorato al mark a ogni giro), quindi una discesa lenta lo trascina giu' e " + f"passano perdite fino a -60,6%/-61,5% dall'ingresso senza che scatti. " + f"Il termine 0,30 e' il costo di UN episodio a movimento singolo, non un tetto: " + f"l'invariante e' safe come tetto sulla LEVA e ottimista come tetto sulla PERDITA. " + f"(k-indipendente: sposta la scala di tutta la colonna, non il confronto fra k.)") + + esito("A5 INVARIANTE", "INCRINA", + f"completo sul percorso FIDATO (0 violazioni su {tot} combinazioni), ma nel ramo di " + f"FALLBACK il denominatore e' il watermark e non l'equity: leva vera fino a " + f"{peggio1:.2f}x gia' a k=1,00 e {peggio125:.2f}x a k=1,25 contro " + f"{n_asset*frac*1.25:.2f}x letti. La scala non apre il buco, lo moltiplica -> G3 e' " + f"NON OPZIONALE ma non e' una chiusura. E il fattore 0,30 non e' un tetto di perdita " + f"(§40: stop rotolante, passano -60% dall'ingresso)") + + +# ========================================================================================== +# (7) A6 — IL FUNDING SCALA CON LA LEVA +# ========================================================================================== +def sezione7(S: dict) -> None: + sez("(7) A6 — il funding non era nel conto della leva, E IL FUNDING SCALA CON LA LEVA") + print(f" [DEDOTTO] il funding e' `tasso x NOZIONALE`: a leva k il nozionale e' k volte, " + f"quindi il costo e' k volte. Percio' la serie giusta e' k*(r_lordo - f), cioe' " + f"k * (serie CON funding) — non k*r_lordo - f.") + cf = (S["R"]["L1"].mean() - S["R"]["L3"].mean()) * ANN + print(f" [MISURATO] verifica sul dato: costo del funding a k=1,00 = {cf:+.2%}/anno; " + f"a k=1,25 = {cf*1.25:+.2%}/anno (esattamente 1,25x: lineare per costruzione).") + print(f" ⚠️ {cf:.2%} e non il {-PUB['drift_fund']+PUB['drift_nofund']:.2%} " + f"pubblicato da §35: qui la serie e' DE-LUCKATA (x0,89), e il de-luck agisce sul " + f"drift, quindi anche sulla sua componente di funding. Stesso numero, lente diversa.") + + print(f"\n ⚠️ UNA SCELTA DI MODELLO CHE VALE PIU' DEL FUNDING, e §33 non la dichiara: " + f"il MURO si muove con k?") + print(f" Se si gira a leva k anche IN PRELIEVO, la rendita perpetua sale e il muro " + f"SCENDE -> il gradino paga due volte (accumuli prima E ti serve meno).") + print(f" Se si de-leva al traguardo, il muro resta quello di k=1,00 -> paga una volta.") + print(f" Si riportano ENTRAMBE. La seconda e' la lettura CONSERVATIVA ed e' quella " + f"compatibile col +1,8a / +EUR164 pubblicati.") + print(f"\n {'lente':<20}{'k':>5}{'drift':>8}{'g(k)':>8}{'perp.':>7}{'muro(k)':>10}" + f"{'anni muro(k)':>20}{'anni muro(1,00)':>20}{'guad.(k)':>10}{'guad.(fisso)':>13}") + out = {} + for nome, key in (("L1 fisco (pubbl.)", "L1"), ("L3 fisco+FUNDING", "L3")): + base = base_f = None + muro1 = None + for k in (1.00, 1.25, 1.50): + r = k * S["R"][key] + p, mu_ = muro_di(r) + if muro1 is None: + muro1 = mu_ + a = anni_al_muro(r, mu_) # muro che si muove con k + af = anni_al_muro(r, muro1) # muro CONGELATO a quello di k=1,00 + if base is None: + base, base_f = a["med_cond"], af["med_cond"] + gua, gua_f = base - a["med_cond"], base_f - af["med_cond"] + out[(key, k)] = (p, mu_, a, gua, af, gua_f) + print(f" {nome:<20}{k:>5.2f}{r.mean()*ANN:>8.2%}{g_exact(r,k=1.0):>8.2%}" + f"{p:>7.2%}{mu_:>10,.0f}{fmt_anni(a):>20}{fmt_anni(af):>20}" + f"{gua:>+9.2f}a{gua_f:>+12.2f}a") + print(f" {clock()}") + + g_L1, g_L3 = out[("L1", 1.25)][3], out[("L3", 1.25)][3] + gf_L1, gf_L3 = out[("L1", 1.25)][5], out[("L3", 1.25)][5] + print(f"\n 📌 [MISURATO] il gradino 1,25x vale {g_L1:+.2f}a (L1, muro mobile) / " + f"{gf_L1:+.2f}a (L1, muro congelato) e {g_L3:+.2f}a / {gf_L3:+.2f}a sulla lente L3 " + f"col funding dentro.") + print(f" Il numero PUBBLICATO (+1,8a) sta sotto entrambe le letture della lente su cui " + f"e' stato pubblicato: era una INTERPOLAZIONE della tabella dei versamenti, non una " + f"misura diretta (lo dichiara la SPEC). La misura diretta dice di piu', non di meno.") + print(f" Il funding NON riduce il gradino: lo AUMENTA in anni assoluti, perche' " + f"peggiora il caso base ({out[('L3',1.00)][2]['med_cond']:.2f}a contro " + f"{out[('L1',1.00)][2]['med_cond']:.2f}a) e la leva ne recupera una parte.") + print(f" ⚠️ Ma il livello e' peggiore su TUTTA la colonna: col funding dentro, k=1,25 " + f"({out[('L3',1.25)][2]['med_cond']:.2f}a) e' ancora PEGGIO di k=1,00 senza funding " + f"({out[('L1',1.00)][2]['med_cond']:.2f}a). " + f"**Il gradino non ripaga il funding: lo attenua.**") + + # versamento mensile equivalente, ricalcolato sulla lente onesta + print(f"\n [CALC] versamento mensile equivalente del gradino (bisezione sul deposito che, " + f"a k=1,00, raggiunge il traguardo nello stesso tempo che k=1,25 raggiunge il suo):") + for key, nome in (("L1", "L1 fisco"), ("L3", "L3 fisco+FUNDING")): + r1 = S["R"][key] + _, m1 = muro_di(r1) + for lettura, idx_a in (("muro MOBILE ", 2), ("muro CONGELATO", 4)): + tgt = out[(key, 1.25)][idx_a]["med_cond"] + lo, hi = DEP_EUR, 4_000.0 + for _ in range(14): + mid = (lo + hi) / 2 + a = anni_al_muro(r1, m1, dep_eur=mid) + if a["med_cond"] <= tgt: + hi = mid + else: + lo = mid + marca = "" + if idx_a == 4 and key == "L1": + interp = 500 + 300 * (14.7 - 12.9) / (14.7 - 11.4) + marca = (f" <- contro +EUR{interp-500:.0f} pubblicati: la differenza E' " + f"l'ipotesi di LINEARITA' dell'interpolazione, dichiarata dalla SPEC") + print(f" {nome:<20}{lettura} EUR{DEP_EUR:>6.0f}/m a k=1,25 == " + f"EUR{hi:>7.0f}/m a k=1,00 -> +EUR{hi-DEP_EUR:.0f}/mese{marca}") + print(f" {clock()}") + + if min(g_L3, gf_L3) < 1.0: + v, riga = "FALSIFICA", (f"col funding dentro il gradino vale {g_L3:+.2f} anni, sotto " + f"la soglia di 1,0 anno del mandato") + else: + v, riga = ("REGGE", f"col funding dentro e proporzionale a k il gradino 1,25x vale " + f"{g_L3:+.2f}a (muro mobile) / {gf_L3:+.2f}a (muro congelato) " + f"contro {g_L1:+.2f}a / {gf_L1:+.2f}a sulla lente pubblicata: il " + f"funding AUMENTA il gradino in anni assoluti (peggiora il caso " + f"base da {out[('L1',1.00)][2]['med_cond']:.2f}a a " + f"{out[('L3',1.00)][2]['med_cond']:.2f}a) — ma il LIVELLO peggiora " + f"su tutta la colonna: k=1,25 col funding ({out[('L3',1.25)][2]['med_cond']:.2f}a) " + f"resta peggio di k=1,00 senza ({out[('L1',1.00)][2]['med_cond']:.2f}a). " + f"Il gradino non ripaga il funding, lo attenua") + esito("A6 FUNDING", v, riga) + return out + + +# ========================================================================================== +# (8) VERDETTO +# ========================================================================================== +def sezione8(S: dict) -> None: + sez("(8) VERDETTO — e il k MASSIMO DIFENDIBILE come singolo numero") + for kk, (v, riga) in ESITI.items(): + print(f" {kk:<16} {v:<11} {riga}") + + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + frac, sl = float(cfg["max_notional_per_asset_frac"]), float(cfg["disaster_sl_pct"]) + L = PUB["L_strutt"] + r = S["R"]["L3"] + dr = boot_drift(r) + lo95 = float(np.percentile(dr, 2.5)) + ks_ic, _ = k_star(shift_drift(r, lo95)) + + vincoli = [ + ("PROGETTO disaster-SL <= 50% equity (G6 SPEC, invariante 3 chiavi)", + 0.50 / (2 * frac * sl), "P"), + ("PROGETTO peggior giorno strutturale <= 50% (C1 §33)", 0.50 / L, "P"), + ("PROGETTO mezzo-Kelly al limite BASSO dell'IC95 del drift (C2 §33)", ks_ic / 2, "P"), + ("SCETTICO peggior giorno strutturale <= 20% (recupero +25%)", 0.20 / L, "S"), + ("PROCESSO scaletta di codice `SCALA_LADDER` della SPEC", 1.25, "L"), + ] + print(f"\n {'vincolo':<66}{'k massimo':>12}") + for nome, k, _ in sorted(vincoli, key=lambda t: t[1]): + print(f" {nome:<66}{k:>12.2f}") + k_prog = min(k for _, k, t in vincoli if t == "P") + k_risk = min(k for _, k, t in vincoli if t in ("P", "S")) + bind_r = [n for n, k, t in vincoli if t in ("P", "S") and abs(k - k_risk) < 1e-9][0] + print(f"\n K MASSIMO DIFENDIBILE = {k_risk:.2f} (vincolo che morde: {bind_r})") + print(f" · coi soli vincoli GIA' DICHIARATI dal progetto sarebbe {k_prog:.2f} " + f"(morde il disaster-SL, non il peggior giorno: G6 della SPEC);") + print(f" · la soglia del 20% e' AGGIUNTA da questo scettico e va difesa: oltre quella " + f"perdita un recupero richiede +25%, e su un conto unico con zero assorbente il " + f"tempo di recupero e' il costo vero. Non e' una regola del progetto: e' una proposta.") + print(f" · e la scaletta della SPEC ferma comunque l'espressione a 1,25 — che e' un " + f"cricchetto di PROCESSO, non un limite di rischio, e resta la scelta giusta perche' " + f"e' l'unico gate che il progetto ha contro un parametro che nessun altro gate vede.") + print(f"\n ⇒ IL GRADINO 1,25x SOPRAVVIVE ALL'ATTACCO. Il 1,50x NO: sfonda la soglia del " + f"20% ({1.50*L:.2%}) e sta a {1.50/(0.50/(2*frac*sl)):.0%} del tetto G6.") + + print(f"\n 📌 RICONCILIAZIONE DEL NUMERO PUBBLICATO — e non era una discrepanza, era una " + f"CONVENZIONE non dichiarata:") + print(f" §33 pubblica «14,7a -> 12,9a» = +1,8a. Questo file misura +2,98a con il muro " + f"che si muove con k e **+1,82a con il muro congelato a quello di k=1,00**.") + print(f" Il numero pubblicato E' la lettura a muro congelato (de-leva al traguardo), " + f"riprodotta a 0,02a. Non e' sbagliato: e' il piu' conservativo dei due, e §33 non " + f"dichiara quale sta usando. **Va dichiarato**, perche' la differenza fra le due " + f"letture (1,2 anni) e' piu' grande di quasi tutti gli effetti che questo progetto " + f"misura.") + print(f"\n ATTACCHI NON PORTATI, e perche':") + print(f" · il costo di MARGINE sopra 1x non e' stato prezzato: su un perpetuale lineare " + f"marginato in USDC non c'e' un prestito, il carry E' il funding (gia' dentro). " + f"Se Deribit introducesse un costo di margine, il conto va rifatto.") + print(f" · la banda d'ANCORA del gradino: il de-luck ×0,89 e' applicato al drift, ma il " + f"guadagno in ANNI e' un Delta su griglia ancorata e quindi eredita la fortuna " + f"d'ancora come ogni Delta (lezione 26/07). Servirebbero le 23x24 ancore congiunte: " + f"fuori dal budget di 2 CPU. Il segno non e' in dubbio (positivo a ogni drift " + f"dell'IC95), la TAGLIA in anni si', ed e' la stessa distorsione che ha gia' " + f"ridimensionato il numero funded da 7,8% a 4,4%.") + print(f" · lo slippage a taglia crescente: §5 dell'ondata ha misurato che un fill ha " + f"preso il 21,9% del volume della sua barra 5m a $635, e che la partecipazione scala " + f"LINEARE. Una scala 1,25x la porta a 27,4% subito, non a $5.000. Non l'ho misurata " + f"e va misurata prima del gradino, non dopo.") + print(f" · la coda ASSENTE dal dataset per costruzione (un evento tipo LUNA/FTX sul " + f"VENUE, non sul prezzo): nessuno stimatore di coda la contiene, e il rischio di " + f"venue vive sul suo asse separato (26/07). Li' la leva moltiplica la perdita 1,25x " + f"in uno scenario in cui la perdita e' gia' del 100%: non cambia nulla, ed e' " + f"esattamente per questo che non e' un argomento a favore.") + print(f"\n {clock()}") + + +def main() -> None: + print(__doc__.split("ONESTA' APPLICATA")[0]) + sez("(0) IL CRITERIO — congelato PRIMA di guardare le sezioni 2-7") + for k, v in CRITERIO.items(): + print(f" {k}: {v}") + S = sezione1() + sezione2(S) + sezione3(S) + sezione4(S) + sezione5(S) + sezione6() + sezione7(S) + sezione8(S) + print(f"\n{clock()} fine.") + + +if __name__ == "__main__": + main() From a4c9fca77ac1336c54ad0d5d7ea4ce84ca186cbc Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 02:06:01 +0000 Subject: [PATCH 087/114] =?UTF-8?q?registro=20=C2=A747:=20ANCHOR-ENSEMBLE?= =?UTF-8?q?=20=E2=80=94=20su=20TP01=20il=20drift=20non=20puo'=20cambiare?= =?UTF-8?q?=20per=20algebra,=20il=20segnale=20sta=20su=20SKH01,=20e=20la?= =?UTF-8?q?=20premessa=20del=2002/07=20era=20sbagliata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 93 ++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 6967882..bfcbc6a 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -712,7 +712,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 44 | DEPOSIT-TIMING | un calendario di versamento CONDIZIONALE batte il piatto, a pari soldi? | **SCARTATO 0/12 in ogni lente** — il buy-the-dip compra davvero il **51% piu' in basso** e arriva al capitale-rendita nel **6,3%** dei casi contro l'**85,4%** del piatto | | 45 | SPOT-NETTING | separare TP01 e SKH01 su due strumenti: quanto vale, e il margine lo uccide? | **LEAD REGGE, SEPARAZIONE SENZA VALORE** — il margine non e' il vincolo (copertura **50x**, invariante al capitale), ma **T1 sbloccato e' un DECLASSAMENTO** (−0,048 contro il path che gira) e il netting perso **risparmia** commissioni: il lead vale **il funding e basta**, [+1,55%, +1,66%]/anno; a bloccarlo e' **il codice di sizing**, non Deribit | | 46 | TAIL-HEDGE | comprare put deep-OTM come assicurazione statica compra DD a prezzo migliore del de-levering? | **REFUTATO** — la premessa cade **prima del prezzo**: il maxDD **SALE in 162/162 celle**, a ogni lente di f (f=1,00 regalato compreso). Meccanismo: **beta del libro al sottostante +0,076** | -| 47 | ANCHOR-ENSEMBLE | eseguire l'ensemble delle 24 ancore invece di sceglierne una e correggere il numero | _in corso_ | +| 47 | ANCHOR-ENSEMBLE | eseguire l'ensemble delle 24 ancore invece di sceglierne una e correggere il numero | **ESEGUIBILE MA INUTILE SU TP01** — il drift **non puo'** cambiare (algebra: `lordo_ens == media(lordi)`, 1,4e-17); **il segnale sta su SKH01** (+0,312 di Sharpe mediano, maxDD 23,3%→16,6%), ma dentro il libro **non si distingue da zero**. E la premessa del 02/07 era sbagliata | | 48 | VOLVOL | la volatilita' del DVOL (secondo momento, mai usato) contiene cio' che livello e spread non contengono? | **SCARTATO** — e' davvero una variabile NUOVA (ortogonalita' PASS, max \|corr\| 0,396) ma **guarda INDIETRO**: picco a lag **−5/−10** e parziale con la vol futura **NEGATIVA**. Quinto lato del filone DVOL chiuso | | 49 | TP01-TWIN | il 75% del libro dipende da UNA definizione di trend: un ensemble di meccanismi protegge meglio? | **REFUTED** — protegge **PEGGIO** nei 4 anni piu' recenti a **0/24 ancore** (2022: **3,2x peggio**) e costa **−0,127** di Sharpe hold-out al libro. E i meccanismi **non erano ridondanti**: corr di posizione 0,61 | | 50 | BOOK-3RD | qual e' il miglior TERZO sleeve realmente eseguibile sul libro live a $635 — e a quale capitale cambia? | _in corso_ | @@ -2876,3 +2876,94 @@ Nessuna copertura **dinamica** (gated su regime) e' stata provata: la domanda er **VERDETTO: `LA PUT PROTEGGE UNA PERDITA GIA' RIDOTTA 10x DALLA STRATEGIA — IL maxDD SALE IN 162 CELLE SU 162`** — REFUTATO. + +--- + +## 47 — ANCHOR-ENSEMBLE (de-luckare l'ESECUZIONE invece del numero) + +`scripts/research/r0823_anchor_ensemble.py` (**127 s**). Attese a priori **A1-A7 scritte e stampate +prima** di misurare: **3 confermate, 3 refutate, 1 mezza**. **Libro, pesi, ancore, cron, config +INVARIATI.** + +**(i) Batte l'ancora MEDIANA: si', di +0,014 di Sharpe FULL — e l'algebra dice che non puo' fare di +piu'.** La posizione dell'ensemble e' la **media** delle posizioni e il lordo orario e' lineare in +essa ⇒ **`lordo_ens == media(lordi)`, verificato a max|dif| 1,4e-17**. Il drift non puo' cambiare se +non per fee-netting (**+0,013%**): **tutto il guadagno e' vol (−1,1%)**. +📌 **Cio' che l'ensemble compra davvero non sta nella colonna Sharpe: `sd(ShFULL) 0,061 → 0` e +`sd(ShHOLD) 0,112 → 0`. Non riduce l'incertezza, la ELIMINA.** +⚠️ **A2 refutata sulla taglia:** l'agente prevedeva +0,03…+0,15 ed e' **7x piu' piccolo** — aveva +sopravvalutato quanto sono decorrelate 24 ancore della **stessa** strategia. + +**(ii) Batte la CANONICA: NO sull'hold-out (−0,223), SI' su FULL (+0,024) e in-sample (+0,071).** +La canonica sta al **98° pctl di 24 sull'hold-out e al 15° in-sample**. +📌 **E i due numeri non sono due stime della stessa cosa: +0,44 e' un'estrazione GIA' AVVENUTA, ++0,21 e' cio' che si ottiene senza estrarre.** Tenere la canonica non conserva il +0,44: **ri-estrae** +da [+0,02, +0,44]. **La stima onesta del futuro e' +0,21.** + +**(iii) Eseguibile da quale capitale: da TUTTI, gia' a $635 — attesa REFUTATA.** A $635 il path +eseguito a K=24 segue l'ideale **meglio** che a K=1 (**corr 0,9986 contro 0,9954**), perche' *un +target che si muove a passi piccoli e frequenti entra in una banda fissa meglio di uno che salta*. +Ordini 119 → 729/anno, fee **$2,55 → $2,56**. ⚠️ Il criterio `N_max` dichiarato in anticipo +**satura (6/6 celle a ogni capitale)**: *un gate che passa sempre non misura niente* → la risposta +non e' un capitale, e' che **l'eseguibilita' non e' il vincolo (7ª volta nell'ondata)**. + +✅ **Replica del 02/07 prima di ogni delta:** `h=0 == tp01_baseline_daily`; **`banded()` == +`r07.smallcap_net` bit-exact (0,0e+00, stessi ordini)**; **K=4 == EW di 4 book** (2,2e-16); **offset +0 == `sleeves._skyhook_returns()`** (0,0e+00). La **struttura** replica; i **livelli** sono +ri-derivati col dato di oggi (`data/raw/` non versionato): HOLD mediano 0,04 → **0,220**, banda +[−0,13,+0,30] → **[+0,02,+0,44]**, canonico 0,31 → **0,436 (98° pctl in entrambi)** — **coincidono +con la memoria corrente del progetto** (canonico +0,44 al 96-98°, mediana +0,22). + +📌 **Il funding: canale chiuso per ALGEBRA, non per piccolezza.** `funding = pos·f` e' lineare in +`pos` ⇒ **`funding_ens == media esatta delle 24 ancore, max|dif| = 0`**. E il rapporto condizionale +misurato il 22/08 **non si attenua: 2,25x → 2,26x** da K=1 a K=24 — *e' esso stesso una media, e la +correlazione esposizione-funding vive alla scala del REGIME, non dell'ora del giorno*. (Replica +indipendente del drag su griglia oraria: **2,14%** contro il 2,02% pubblicato.) + +📌 **DOVE STA IL SEGNALE: SKH01 — l'ancora a cui il 02/07 non porto' mai questa domanda.** Sulla +lente del path che gira: mediana delle 23 fasi ShFULL **0,974 → 1,286 (+0,312)**, maxDD mediano +**23,3% → 16,6%**. **Venti volte TP01**, e il meccanismo e' chiaro: i suoi trade sono **discreti** — +spostare la griglia cambia **quali trade esistono**, non quanto sono grossi (sd d'ancora **0,194 +contro 0,061**). **23 e' primo** ⇒ sulla griglia a 30m non esiste sotto-ensemble simmetrico. +⚠️ **Ma dentro il libro SKH01 pesa il 25% e li' non si distingue da zero** (+0,005 a $635, +0,022 a +$100k, coi segni dei singoli che cambiano fra i due capitali). + +📌 **Contro-intuitivo misurato:** tranciando entrambi gli sleeve gli **ordini salgono 6,5x** +(197 → 1283/anno) ma le **fee SCENDONO** ($5,76 → $5,29). Su un venue a fee **proporzionale** si +paga il *nozionale*, e mediare 23 fasi trasforma un ±1,0x che sbatte da un estremo all'altro in una +posizione frazionaria. **Su un venue a pavimento fisso il segno si ribalterebbe** (regola del 25/07, +riconfermata da un terzo angolo). + +**Barriera (canale funded) — attesa refutata *sulla regola sbagliata*.** Il lato binding non e' la +barriera (−6%, P(breach) 0-7%) ma il **bersaglio** (+10%, P(pass) 40-82%): *meno vol allontana dal +traguardo quanto dalla barriera*, e l'ensemble sta al **31° pctl su P(pass)** pur avendo maxDD +migliore della mediana. Ma la regola che il 22/08 ha misurato **uccidere** e' la **daily-loss a un +giorno**, che close-only **non puo' vedere**: sul **p1 giornaliero** l'ensemble batte il **66%** di +150 configurazioni. **Indizio, non misura** — la conferma richiede la lente accoppiata. + +⚠️ **Il costo vero, e non e' negli ordini: 23 ancore su 24 di TP01 richiedono le barre di oggi**, +cioe' **`fresh_5m`** — il path che il 26/07 e' risultato ricadere **in silenzio** sul certificato e +che il 29/07 ha fallito **6 giri su 8**. Oggi TP01 non ci dipende. **E' l'unica delle tre obiezioni +del 02/07 che sopravvive intatta** (SKH01 quel feed lo usa gia', il che rende l'asimmetria fra i due +sleeve ancora piu' netta). + +🚨 **IL RISULTATO CHE NON ERA NEL MANDATO: la premessa del 02/07 era sbagliata.** Quel filone +boccio' il tranching perche' *"i delta per-ancora (~$1-2) sono sotto il min-order $5 → degenererebbe +in K=1"*. Ma **`src/live/book.build_book_order` banda `abs(delta)` sulla posizione NETTA e manda un +ordine per asset per giro**: *un delta per-ancora non e' mai un ordine*. **Stessa decisione, ragione +diversa — e la ragione conta**, perche' quella vecchia cadrebbe al primo esecutore che mandasse un +ordine per tranche, questa no (e' sulla **taglia dell'effetto**). + +⚠️ **Due errori dell'agente, catturati e congelati in commento:** (a) confrontava +`turnover_per_year`, che `eval_weights` **arrotonda**, accanto a una disuguaglianza stretta → +stampava "9,40 contro 9,38" facendo sembrare violata una sub-additivita' che il conto esatto +rispetta; (b) i percentili di coda usavano **un solo segno per tre colonne** di cui due sono ritorni +e una una frequenza → stampava **34° dove il valore vero e' 66°**, cioe' *"peggio della mediana"* +per un numero migliore della mediana. E la prima stesura affermava *"i due effetti sono additivi"* +come **testo fisso**, refutato dai suoi stessi numeri: ora l'affermazione la **calcola il codice**. + +**VERDETTO: `SU TP01 L'ENSEMBLE NON PUO' CAMBIARE IL DRIFT PER ALGEBRA — ELIMINA L'INCERTEZZA, NON +LA MIGLIORA; IL SEGNALE STA SU SKH01 E DENTRO IL LIBRO NON SI DISTINGUE DA ZERO`**. +**Nessuna proposta**: SKH01 non e' passato per `study_family_honest` su questa griglia, il guadagno +e' misurato sugli stessi 7 anni su cui e' stato selezionato, e la lente `hourly` e' gia' nota (26/07) +per sottostimare il path live. **Il filone stabilisce dove guardare, non cosa fare.** From 8c2fa6e46c18dddc8efe34a9288330ed73fcf905 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 02:07:24 +0000 Subject: [PATCH 088/114] =?UTF-8?q?registro=20=C2=A753:=20SKEPTIC-LEVA=20r?= =?UTF-8?q?egge=205/6=20=E2=80=94=20k=20max=201,40,=20il=201,50x=20cade,?= =?UTF-8?q?=20e=20nel=20ramo=20eq=5Ffallback=20la=20scala=20moltiplica=20u?= =?UTF-8?q?n=20buco=20esistente?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 104 +++++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index bfcbc6a..2438d2a 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -718,7 +718,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 50 | BOOK-3RD | qual e' il miglior TERZO sleeve realmente eseguibile sul libro live a $635 — e a quale capitale cambia? | _in corso_ | | 51 | NEWDATA-SCOUT | se la scorta e' esaurita: cosa iniziare a raccogliere OGGI, che fra 12-24 mesi apra una famiglia? | **RACCOGLIERE 2 FONTI SU 10** — catena USDC (**+18%, non +90%**) e una **campagna a termine** di book depth; il criterio di ricostruibilita' ne uccide 4 da solo e **refuta una raccomandazione del 22/08** | | 52 | SKEPTIC-SPOT | il lead dello spot (+1,55-1,66%/anno) sopravvive a un attacco deliberato? | _in corso_ | -| 53 | SKEPTIC-LEVA | il gradino di leva 1,25x sopravvive a un attacco deliberato, col funding dentro? | _in corso_ | +| 53 | SKEPTIC-LEVA | il gradino di leva 1,25x sopravvive a un attacco deliberato, col funding dentro? | **REGGE 5/6, INCRINA 1/6** — il **1,25x sopravvive**, il **1,50x NO**; k massimo difendibile **1,40**. L'incrinatura e' il ramo `eq_fallback`, dove **la scala non apre il buco: lo MOLTIPLICA** | | — | **NOTIFIER** (audit di fatto) | 🚨 **PUNTO SINGOLO DI GUASTO nella rete di sicurezza** | il `send()` di Telegram e' **un solo tentativo da 10s, senza retry, con l'esito MAI registrato** — e lo stato `alerted=True` viene scritto **prima** dell'invio: un 🚨 di `venue_watch` perso e' perso **per l'intero episodio**. Tasso di fallimento **misurato 6,9%** (2/29, IC95 [1,9%, 22,0%]) | | 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 | @@ -2967,3 +2967,105 @@ LA MIGLIORA; IL SEGNALE STA SU SKH01 E DENTRO IL LIBRO NON SI DISTINGUE DA ZERO` **Nessuna proposta**: SKH01 non e' passato per `study_family_honest` su questa griglia, il guadagno e' misurato sugli stessi 7 anni su cui e' stato selezionato, e la lente `hourly` e' gia' nota (26/07) per sottostimare il path live. **Il filone stabilisce dove guardare, non cosa fare.** + +--- + +## 53 — SKEPTIC-LEVA (attacco deliberato al gradino di leva) + +`scripts/research/r0823_skeptic_leva.py` (**127 s**, sola lettura, zero rete). **Le soglie dei sei +attacchi sono congelate nella sezione (0), scritte PRIMA di guardare le sezioni 2-7.** + +✅ **Replica prima di attaccare — 14 numeri, tutti riprodotti:** drift 19,23/17,07% · Sharpe +1,691/1,512 · **k\* 13,69 e 12,31** (§35 dice 13,7 e 12,3) · g(1,00) 17,90% e g(1,25) 22,61% +**esatti** · perpetua 7,55/6,38% · muri $263,3k/$311,7k · anni 14,81 contro 14,7 pubblicati. +E l'appaiamento fra k e' **provato**: `max|paths(1,25·r) − 1,25·paths(r)| = 0` ⇒ **i confronti fra k +non hanno rumore Monte Carlo**. + +**A1 DRIFT — REGGE, ed e' l'attacco che il mandato riteneva principale.** Bootstrap a blocchi (20 g, +4000 estrazioni): SE **5,09%** (**1,23x l'ingenuo iid** — il clustering allarga, ed e' la scelta +severa), IC95 **[+4,78%, +24,97%]**. Il **drift di pareggio del gradino e' +1,42%/anno = −2,70 SE = +0,25° percentile**: fuori da IC95 **e** da IC99. **Il libro puo' perdere il 90,6% del suo drift e il +gradino resta non dannoso.** Controllo che non dipende dal modello: il **peggior triennio mai +realizzato** (+5,35%) sta **3,8x sopra** la soglia. + +**A2 SINISTRO — REGGE, con una qualifica misurata.** Δg(1,25)>0 in **5/5** finestre giudicabili, +**2020-21 tolto compreso** (+2,77%). Ma cercando il **biennio MOBILE peggiore** (non la partizione +arbitraria) ne esce uno vero: da **2021-10, drift −0,45%**, dove il gradino costa **−0,31%/anno**. +Rapporto guadagno/costo **13:1**. 🚨 **A parte quel biennio nessuna finestra ha drift negativo** → +*l'attacco nella forma piu' forte non e' esprimibile su questo campione*, ed e' esattamente cio' che +il mandato temeva: la sostituzione onesta e' A1, che **impone** il drift invece di cercarlo. + +**A3 CODA — REGGE, e corregge §33.** Hill e' k-sensibile (sweep ξ 0,289→0,672): preso il +**peggiore, ξ=0,431**, piu' severo del 0,34 pubblicato. Peggior giorno strutturale a 1,25x +**17,90% < 50%**; P(dimezzamento su 10 anni) **0,100%**. ⚠️ **Correzione:** §33 scriveva *"la coda +muove k\* di 1-2 unita'"* → **ne muove 4,6** (12,31 → 7,71). La conclusione regge (il drift ne muove +6,2) ma **il numero pubblicato era ottimista ~3x**, e a quella coda k\* resta comunque **6x il +gradino**. + +**A4 LENTE — REGGE.** Sotto la lente accoppiata (minimo intra-giorno −7,32% **replicato**) il +ricarico del maxDD **cala** con k: 1,2358 → 1,2297 (1,50x) → 1,2237 (2,00x). E la frequenza del +disaster-SL e' **invariante a k per costruzione**: `want_stop` e' un **prezzo** ancorato al mark, +`want_amount` una taglia, e il test di ri-piazzamento e' **relativo** (`want_amount*0,1`) → **il +numero di §40 va moltiplicato, non rifatto**. + +🚨 **A5 INVARIANTE — L'UNICO CHE INCRINA, e sta dove nessuno guardava.** Sul **percorso fidato** +l'invariante e' **completo**: la leva vera e' `n_asset·min(WEIGHT·(W_TP01·lev+W_SKH), frac)·scala`, +un **limite superiore** — 0 violazioni su 576 combinazioni. **Ma il percorso fidato non e' l'unico:** +nel ramo **`eq_fallback`** il denominatore del rapporto e' il **watermark** (l'*ultima equity +osservata*), non l'equity. Con un prelievo dell'80% non ancora visto la leva vera e' **5,00x gia' a +k=1,00** e **6,25x a k=1,25**, mentre l'invariante legge 1,00x/1,25x. +📌 **La scala non apre il buco: lo MOLTIPLICA.** ⇒ la guardia **G3 della SPEC non e' opzionale**, +e **la SPEC la presenta come una chiusura mentre non lo e'**. Frequenza del ramo **0/1.442 giri**, e +li' `book_execute` **allerta e non blocca**. Seconda incompletezza: `disaster_sl_pct`=0,30 e' +trattato come tetto di perdita e §40 ha misurato che **non lo e'** (stop rotolante: passano −60% +dall'ingresso). + +**A6 FUNDING — REGGE, e nel verso OPPOSTO all'ipotesi del mandato.** Il funding scala con k per +costruzione (`r_k = k·(r−f)`, verificato 1,92% → 2,40%). Il gradino vale **+3,49a** (muro mobile) / +**+2,10a** (muro congelato) **col funding dentro**, contro +2,98a / +1,82a senza: **il funding non +riduce il gradino, lo AUMENTA**, perche' peggiora il **caso base** (17,28a contro 14,81a). +⚠️ **Ma il livello peggiora su tutta la colonna:** k=1,25 col funding (13,78a) resta **peggio** di +k=1,00 senza (14,81a) → **il gradino non ripaga il funding, lo attenua.** + +| lente | k | drift | perpetua | muro | anni (muro mobile) | anni (muro congelato) | +|---|---|---|---|---|---|---| +| L1 fisco | 1,00 | 17,11% | 7,55% | $263.347 | 14,81a | 14,81a | +| L1 fisco | **1,25** | 21,39% | 9,20% | $216.241 | **11,83a (+2,98)** | **13,00a (+1,82)** | +| L3 +FUNDING | 1,00 | 15,19% | 6,38% | $311.705 | 17,28a | 17,28a | +| L3 +FUNDING | **1,25** | 18,99% | 7,79% | $255.422 | **13,78a (+3,49)** | **15,17a (+2,10)** | + +🚨 **RICONCILIAZIONE: il "+1,8 anni / +€164" pubblicato non era una discrepanza, era una +CONVENZIONE NON DICHIARATA.** E' la lettura a **muro congelato** (de-leva al traguardo), riprodotta +a **0,02a**. §33 **non dichiara quale delle due letture usa**, e la differenza fra le due (**1,2 +anni**) e' **piu' grande di quasi tutti gli effetti che questo progetto misura**. E il versamento +equivalente misurato direttamente e' **+€144/mese, non €164** — la differenza *e'* l'ipotesi di +linearita' dell'interpolazione. + +**K MASSIMO DIFENDIBILE: 1,40.** + +| vincolo | k max | +|---|---| +| PROCESSO (scaletta `SCALA_LADDER`) | **1,25** | +| **SCETTICO** peggior giorno strutturale ≤20% (recupero +25%) | **1,40** | +| PROGETTO disaster-SL ≤50% equity (G6) | 1,67 | +| PROGETTO mezzo-Kelly al limite basso IC95 | 1,92 | +| PROGETTO peggior giorno ≤50% (C1 §33) | 3,49 | + +Coi **soli vincoli gia' dichiarati dal progetto** sarebbe **1,67**, e a mordere e' **G6 (il +disaster-SL), non il peggior giorno**. La soglia del 20% e' **aggiunta dall'agente ed e' una +proposta, non una regola**. 🚨 **Il 1,50x NON sopravvive**: sfonda il 20% (21,48%) e sta al 90% del +tetto G6. **La scaletta a 1,25 resta giusta: e' l'unico gate che il progetto ha contro un parametro +che nessun altro gate vede.** + +⚠️ **Attacchi NON portati, dichiarati:** (a) **banda d'ancora del guadagno in anni** — e' un Δ su +griglia ancorata, servivano 23×24 ancore congiunte, fuori dal budget di 2 CPU; **il segno non e' in +dubbio** (positivo a ogni drift dell'IC95), **la taglia in anni si'**, ed e' la stessa distorsione +che ha gia' ridimensionato il numero funded da 7,8% a 4,4%; (b) **slippage a taglia crescente** — +§5 ha misurato un fill al **21,9% del volume della sua barra 5m** a $635 con partecipazione lineare +⇒ **1,25x lo porta al 27,4% SUBITO, non a $5.000**: *va misurato prima del gradino*; (c) costo di +margine sopra 1x — **non esiste** su un perp lineare marginato in USDC (il carry *e'* il funding, +gia' dentro); (d) coda di venue — vive sul suo asse separato. + +**VERDETTO: `IL GRADINO 1,25x SOPRAVVIVE A CINQUE ATTACCHI SU SEI; IL 1,50x NO; k MASSIMO +DIFENDIBILE 1,40`** — e l'unica incrinatura non e' nel gradino ma nella **guardia che lo +accompagnerebbe**. From f4551090f5dc6acad159b8bb1807a6342decca2c Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 02:07:52 +0000 Subject: [PATCH 089/114] CLAUDE.md: il guadagno in anni del gradino di leva dipende da una convenzione mai dichiarata (muro mobile vs congelato, 1,2 anni di differenza) --- CLAUDE.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 132bb56..3f75888 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2543,7 +2543,8 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis margine sembrerebbe **+5,83 invece di +1,87**. 🚨 **(4) WORST-DAY — la riserva sulla leva e' mal tarata di 2-4 ORDINI DI GRANDEZZA, ma il knob di scala NON ESISTE.** Il gradino di leva vale **14,7a → 12,9a a 1,25x** e **→ 11,6a a 1,50x** al - capitale-rendita (**+€164/mese** e **+€282/mese** di versamenti equivalenti) ed era bloccato da un + capitale-rendita (**+€164/mese** e **+€282/mese** di versamenti equivalenti 🚨 **il primo e' + +€144 misurato direttamente — vedi il ritocco del 23/08 sotto**) ed era bloccato da un parametro **scelto a mano** ("un giorno a −10% ogni anno"). **Il peggior giorno ha due risposte:** **−3,94% in chiusura** (2020-03-13) e **−7,33% al minimo** intra-giorno (rapporto mediano **1,89×**). Coda GPD: **−4,55% a 1/10.000 @@ -2620,6 +2621,17 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis NODO IRRISOLTO.** Registro §38. Replica a k=1,00 **6/6** contro WORST-DAY. ⚠️ **CORREZIONE A UN NUMERO CHE AVEVO SCRITTO IO:** *"1,25-1,50x vale 14,7 → 11,6 anni, ~€300/mese"* **fondeva due gradini** — a **1,25x** l'equivalente e' **+€164/mese**, il €282 e' del **1,50x**. + 🚨 **RITOCCATO 2026-08-23 (§53), e il ritocco riguarda la CONVENZIONE piu' che il numero:** + il "+1,8 anni / +€164" e' la lettura a **muro CONGELATO** (si de-leva al traguardo) e §33 **non + dichiarava quale delle due letture stesse usando**. A **muro MOBILE** lo stesso gradino vale + **+2,98 anni**; con il **funding dentro**, +2,10a (congelato) / **+3,49a** (mobile). + **La differenza fra le due letture e' 1,2 anni — piu' grande di quasi tutti gli effetti che + questo progetto misura** ⇒ da qui in avanti un guadagno in anni si cita con la sua convenzione. + Il versamento equivalente **misurato direttamente** e' **+€144/mese**, non €164: la differenza + *e'* l'ipotesi di linearita' dell'interpolazione. + 📌 **E il funding va nel verso opposto all'atteso: non riduce il gradino, lo AUMENTA** (peggiora + il caso base) — **ma il livello peggiora su tutta la colonna**: k=1,25 col funding (13,78a) resta + **peggio** di k=1,00 senza (14,81a). *Il gradino non ripaga il funding, lo attenua.* 🚨 **Tre risultati che il mandato non prevedeva:** **(i)** `test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` **NON si rompe, ed e' PEGGIO che rompersi**: misura `frac · n_asset` mentre la grandezza vera diventerebbe `frac · n_asset · scala` From caa7cc40203c9030731ee357dc836548066f8b0d Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 02:16:18 +0000 Subject: [PATCH 090/114] =?UTF-8?q?research(book-3rd):=20nessun=20terzo=20?= =?UTF-8?q?sleeve=20ammissibile=20oggi,=20e=20per=20sette=20candidati=20ci?= =?UTF-8?q?=20sono=20sette=20muri=20diversi=20=E2=80=94=20nessuno=20dei=20?= =?UTF-8?q?quali=20e'=20il=20capitale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §50 BOOK-3RD. Domanda: con ~$635 su Deribit, qual e' il MIGLIOR terzo sleeve realmente eseguibile del LIBRO LIVE a 2 sleeve (`deribit_book_sleeves`, TP01 75 / SKH01 25) — e se la risposta e' "nessuno", a quale capitale cambia? Un solo script, nessun file di produzione toccato, nessun ordine. REPLICA DI CONTROLLO PRIMA DI OGNI DELTA: ShFULL 1,813 / ShHOLD 1,437 / maxDD 9,42% riprodotti al terzo decimale; replica ancorata (h=0, off=0) bit-exact contro lo sleeve di produzione (max|dif| = 0,0), idem XS01, VRP01 e STATARB (0,746 contro il motore originale). IL VINCOLO MAI MESSO IN TABELLA — IL NETTING. Letto dal sorgente: `book_net_target` somma i due sleeve in UN numero per asset e `build_book_order` manda UN ordine, quindi un terzo sleeve DIREZIONALE su BTC/ETH non paga un min-order proprio (misurato: 163 -> 344 ordini/anno a $635, ma il turnover sale solo del 14%; e per disuguaglianza triangolare le fee modellate per-sleeve sono un LIMITE SUPERIORE). Il rovescio: non puo' avere un proprio stop. Un terzo sleeve su ALTRI strumenti richiede una riga in `_CONTRACT` = codice su un percorso con soldi veri. LA MISURA (1000 estrazioni congiunte TP01 x24 · SKH01 x23 · candidato, mediana delle DIFFERENZE APPAIATE, funding dentro, peso 15%): PREVDAY +0,192 ShFULL / +0,363 ShHOLD / +1,31pp drift — eseguibile, netta, ADDS, e SENZA gate pre-registrato: e' il piu' forte ed e' il meno disciplinato. STATARB +0,159 / +0,081 / -0,12pp — gate aperto 27/09; NEUTRAL, robust_oos FALSE. VRP01 +0,121 / +0,067 / +0,19pp — fermo per REGOLA, non per lotto (sotto). XS01(19) +0,110 / +0,378 / +0,79pp — NON eseguibile: Deribit non quota 5 delle 19 gambe. DVOLSPREAD +0,073 / +0,062 / -1,12pp — gate aperto 24/10; il maxDD PEGGIORA sopra il 15%. XS01-D(11) -0,024 / -0,128 / -0,73pp — l'unica versione eseguibile oggi PEGGIORA il libro. DUE FATTI DI VENUE, LETTI DALL'API E NON DALLA MEMORIA: (a) il muro del LOTTO di VRP01 era misurato sulla famiglia INVERSE, che un conto USDC non puo' marginare: sulla USDC-lineare il lotto ETH e' $242 (non $1.832) e il BTC $772 (non $6.210) -> 1 lotto ETH = peso 12% gia' da ~$2.000. Cade il lotto, NON la regola "niente short-vol da modello in deploy". 5a occorrenza dello schema `fee_watch`. (b) Deribit quota 14 dei 19 major di XS01, ma 3 sono listati fra il 15 e il 21 agosto (APT non ha nemmeno una quota). Sulle 11 con >=1 anno di listino il meccanismo collassa, e il null dei sottoinsiemi (100 estrazioni da 11 fra le 19) separa le due cause: il sottoinsieme MEDIANO fa gia' 0,548 contro 1,265 (62% della caduta = AMPIEZZA, non riparabile col capitale) e quello di Deribit sta al 9° percentile (il resto = QUALI gambe mancano). Aspettare 2-3 listing non basta. `weights_tilt_null`: 25/28 PASS — e il numero da leggere non e' quello ma `frac_random_beat_hold`, che arriva a 0,94: dove vale cosi', "questo candidato migliora l'hold-out" e' un claim GENERICO. Il gate ha potenza (fallisce su XS01-D a 10/15/20%), ma resta necessario e non sufficiente. LA SCALA: non esiste una soglia di capitale che ammette un terzo sleeve. Il capitale sposta solo VRP01 (~$2.000 gamba ETH, ~$6.440 gamba BTC), che e' fermo per una regola. Le due date che contano sono un GATE (27/09, gratis) e la DECISIONE DI VENUE ($20k), che e' cio' che riapre XS01 sulle 19 gambe. ONESTA': attesa a priori dichiarata prima di misurare, esito A1 confermata, A2 meta' giusta, A3 e A4 REFUTATE, A5 confermata al rovescio. Caveat pubblicato: sono 7 candidati x 4 pesi = 28 configurazioni sullo stesso hold-out, nessuna passata per un deflated-Sharpe DI SCREEN — il modo giusto di usare la tabella e' scegliere un candidato per una ragione dichiarata PRIMA. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf --- scripts/research/r0823_book_3rd.py | 1262 ++++++++++++++++++++++++++++ 1 file changed, 1262 insertions(+) create mode 100644 scripts/research/r0823_book_3rd.py diff --git a/scripts/research/r0823_book_3rd.py b/scripts/research/r0823_book_3rd.py new file mode 100644 index 0000000..8eea1ad --- /dev/null +++ b/scripts/research/r0823_book_3rd.py @@ -0,0 +1,1262 @@ +#!/usr/bin/env python +"""r0823_book_3rd.py — §50 BOOK-3RD: qual e' il MIGLIOR TERZO SLEEVE del LIBRO LIVE, e vale la pena? + +IL PROBLEMA (e perche' non e' una domanda gia' fatta). Il progetto ha DUE oggetti che vengono +continuamente confusi: + * il BOOK DI RICERCA a 5 sleeve (TP01 33 / XS01 15 / VRP01 12 / SKH01 20 / GTAA01 20) — + `sleeves.active_sleeves()` — su cui e' calcolato quasi ogni numero pubblicato; + * il LIBRO LIVE a 2 sleeve (`sleeves.deribit_book_sleeves()`, TP01 75 / SKH01 25) — l'unica + cosa che gira con soldi veri, e che non ha MAI avuto un terzo sleeve. +Il libro a 5 non e' deployabile: GTAA01 e' bloccato dal PRIIPs (verificato sul conto reale il +26/07) e vuole $3k; XS01 e' dichiarato STAT-MODE per taglia; VRP01 cade sotto la regola +"niente short-vol da modello in deploy" e a $3.000 sarebbe ETH-only con 0 lotti al peso di book. + +DOMANDA DECIDIBILE: oggi, con ~$635 su Deribit, quale e' il miglior terzo sleeve REALMENTE +eseguibile — e vale la pena? Se la risposta e' "nessuno": a quale capitale cambia, e chi entra? + +COSA FA + 0. ATTESA A PRIORI, dichiarata PRIMA di misurare (e poi confrontata con l'esito). + 1. CENSIMENTO dello stato REALE dei candidati — non quello dichiarato in memoria: + strumenti richiesti, disponibilita' su Deribit (GET pubblica), LOTTO MINIMO letto dal + venue (non stimato), data di quotazione, gate pre-registrato in corso. + 2. IL VINCOLO DIMENTICATO — il NETTING. Su Deribit c'e' UNA sola posizione netta per + strumento, e TP01+SKH01 gia' si nettano su BTC/ETH_USDC-PERPETUAL. Un terzo sleeve + DIREZIONALE sugli stessi strumenti si netta con loro (zero costo di strumento, zero + separazione); uno su strumenti diversi no (serve una riga in `_CONTRACT`, cioe' CODICE su + un percorso con soldi veri). Verificato per introspezione sul sorgente di produzione. + 3. REPLICA DI CONTROLLO: riprodurre i numeri pubblicati del libro live (ShFULL ~1,81 / + ShHOLD ~1,44 / maxDD ~9,4%) PRIMA di stampare qualunque delta. Due lenti, dichiarate. + 4. SERIE ANCORATE, ognuna con sanity bit-exact contro lo sleeve/segnale di produzione. + 5. LA MISURA: dSharpe / d drift / d maxDD del libro live aggiungendo ogni candidato a pesi + {5,10,15,20}%, col FUNDING dentro, su ESTRAZIONI CONGIUNTE dello spazio d'ancora + (mediana delle DIFFERENZE APPAIATE — mai la differenza delle mediane). + 6. `weights_tilt_null` — il gate del progetto per ogni cambio di pesi. + 7. ESEGUIBILITA' a $635 / $2k / $5k / $20k: ticket per gamba, min-order, haircut. + 8. LA SCALA: a quale capitale entra il primo candidato, e quale. + +ONESTA'. Nessuna promozione. L'output e' una TABELLA DECIDIBILE. I gate pre-registrati NON si +anticipano: per i candidati sotto gate si dice cosa succederebbe SE passassero, non si decide +al loro posto (anticipare un gate e' selezione-sull'hold-out, regola scritta del progetto). + +VINCOLI RISPETTATI: nessun file di produzione toccato (src/, config/, scripts/live/, +scripts/cron_*.sh, tests/, data/paper_*, data/live); NESSUN ordine; rete = sole GET pubbliche +Deribit <=2 req/s, con astensione nei minuti :05-:10 e :24-:30 (cron_book al :07, +collect_chain al :25). Cache in scratchpad, fuori dal repo. + +USO + nice -n 19 timeout 900 uv run python scripts/research/r0823_book_3rd.py + ... --no-net solo cache (nessuna richiesta di rete) + ... --draws N estrazioni d'ancora (default 200) + ... --force ignora la cache delle serie +""" +from __future__ import annotations + +import argparse +import datetime as dt +import inspect +import json +import pickle +import sys +import time +import urllib.request +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +for _p in (str(ROOT), str(ROOT / "scripts" / "research"), + str(ROOT / "scripts" / "research" / "alt"), + str(ROOT / "scripts" / "research" / "ortho")): + 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.portfolio import sleeves as SL # noqa: E402 +from src.live import book as LB # noqa: E402 +from src.live import deribit as DB # noqa: E402 +from src.strategies.trend_portfolio import (CANONICAL, TrendPortfolio, # noqa: E402 + simple_returns) +from src.strategies.prevday_breakout import target as prevday_target # noqa: E402 + +# --- macchineria di ricerca gia' validata (riusata, non riscritta) --------------------------- +import altlib as al # noqa: E402 +import ortholib as ol # noqa: E402 +import orthogonal_signals as OS # noqa: E402 (STATARB-RESID: segnale ESATTO) +import r0702_tp01_offset as A_TP # noqa: E402 (daily_off: ancora oraria di TP01) +import r0703_vrpimp_anchor as A_VRP # noqa: E402 (fasi settimanali di VRP01) +import r0822d_funding as FU # noqa: E402 (funding: fetch/rate/cumF/skh_daily) +from r0726_dvolspread_gate import make_book as dvs_make_book # noqa: E402 + +# ============================================================================================= +# COSTANTI DICHIARATE +# ============================================================================================= +SCRATCH = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/b3rd") +if not SCRATCH.parent.exists(): + SCRATCH = Path("/tmp/pythagoras_book3rd") +CACHE = SCRATCH / "series_v3.pkl" + +CAPITAL_TODAY = 635.0 # il conto reale (CLAUDE.md 2026-08) +CAP_LADDER = (635.0, 2000.0, 5000.0, 20000.0) +MIN_ORDER = 5.0 # config/live.json -> min_order_usd +FEE_SIDE = al.FEE_SIDE # 0.05%/lato (0.10% RT), 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") +WEIGHTS_TESTED = (0.05, 0.10, 0.15, 0.20) +TP_HOURS = tuple(range(24)) # ancora giornaliera di TP01 +SKH_OFFSETS = tuple(range(0, 690, 30)) # 23 offset della griglia 230m/690m +XS_PHASES = tuple(range(10)) # ciclo H=10 di XS01 +VRP_PHASES = tuple(range(7)) # ciclo settimanale di VRP01 +SEED = 20260823 +# XS01 paga funding perche' il momentum compra i perp col funding piu' caro: +1,45%/anno +# MISURATO nell'ondata 2026-08-22 §37 (banda onesta +0,69/+1,45 se HL e' ~2,1x il venue vero). +# Qui e' applicato come DRAG DICHIARATO, non modellato per gamba: gli alt su Deribit sono +# quotati da giorni (vedi sezione 1) e non ne esiste storico di funding. +XS_FUNDING_DRAG_YR = 0.0145 + +API = "https://www.deribit.com/api/v2/public" +CACHE_FRESH_S = 6 * 3600 +T0 = time.time() +_ARGS = None + + +def sez(t: str) -> None: + print("\n" + "=" * 104) + print(f" {t}") + print("=" * 104) + + +def sub(t: str) -> None: + print("\n" + "-" * 104) + print(f" {t}") + print("-" * 104) + + +def clock() -> str: + return f"[{time.time() - T0:5.0f}s]" + + +# ============================================================================================= +# RETE — sole GET pubbliche, pacing, astensione nelle finestre del cron +# ============================================================================================= +def _pace() -> None: + while dt.datetime.now(dt.timezone.utc).minute in (5, 6, 7, 8, 9, 10, 24, 25, 26, 27, 28, 29, 30): + time.sleep(15) + time.sleep(0.55) + + +def get_json(path: str, cache_name: str) -> dict | None: + """GET pubblica con cache su scratchpad. Con --no-net usa solo la cache.""" + SCRATCH.mkdir(parents=True, exist_ok=True) + fp = SCRATCH / cache_name + if _ARGS and _ARGS.no_net: + return json.loads(fp.read_text()) if fp.exists() else None + # cache fresca -> NESSUNA richiesta, e quindi nessuna attesa nelle finestre del cron. + # (`_pace` blocca 15s per volta nei minuti :05-:10 e :24-:30: farlo quando non si sta + # per chiamare nessuno e' solo tempo perso.) + if fp.exists() and (time.time() - fp.stat().st_mtime) < CACHE_FRESH_S: + return json.loads(fp.read_text()) + try: + _pace() + with urllib.request.urlopen(f"{API}/{path}", timeout=30) as r: + d = json.loads(r.read()) + fp.write_text(json.dumps(d)) + return d + except Exception as e: # noqa: BLE001 + print(f" [rete KO su {path}: {type(e).__name__}] -> cache") + return json.loads(fp.read_text()) if fp.exists() else None + + +# ============================================================================================= +# 0) ATTESA A PRIORI — dichiarata PRIMA di misurare +# ============================================================================================= +ATTESA = """ + Registrata prima di guardare qualunque numero di questo script: + + (A1) NESSUN candidato e' deployabile oggi, e il muro NON sara' il lotto minimo del venue. + Nell'ondata del 22/08 l'eseguibilita' a $600 non ha bocciato niente per TRE volte di + fila; mi aspetto che il muro sia (a) i gate pre-registrati non scaduti, (b) + `weights_tilt_null`, (c) per XS01/XSR01 il fatto che il venue e' Hyperliquid, non Deribit. + (A2) Il candidato col dSharpe piu' alto sara' XS01 (LOO 26/07: +0,125 di FULL nel libro a 5, + corr ~0 a tutto), ma `weights_tilt_null` FALLIRA' per la stessa ragione strutturale per + cui fallisce su XSR01: storia dal 2024, quindi quasi nessun in-sample. + (A3) I due candidati "eseguibili a $600" gia' etichettati tali (STATARB-RESID, DVOLSPREAD) + daranno un uplift piccolo (Sharpe standalone 0,75 e 0,61 contro 1,81 del libro): mi + aspetto |dSharpe| < 0,10 e il segno incerto sulla banda d'ancora. + (A4) La scala: il primo a entrare sara' XS01 fra $2k e $5k, NON ai $20k della memoria — ma + per una ragione di VENUE, non di taglia (serve che Deribit quoti abbastanza gambe). + (A5) Se qualcosa mi sorprende, sara' un candidato dato per eseguibile che NON lo e' (e' gia' + successo due volte: GTAA01/PRIIPs e VRP01/lotto minimo). +""" + + +# ============================================================================================= +# 1) CENSIMENTO — lo stato REALE dei candidati +# ============================================================================================= +# I 19 major di XS01, letti dalla PRODUZIONE (non riscritti: se cambiano li', cambiano qui). +XS_UNIVERSE = list(SL.XS_UNIVERSE) + +# gate pre-registrati: (nome, data, cosa decide). Letti da CLAUDE.md / docstring dei monitor. +GATES = { + "STATARB-RESID": ("2026-09-27", "decisione di deploy (gate `r0724_statarb_deploy_gate`)"), + "XSR01": ("2026-10-23", "deploy solo se Sharpe>=1,0 E haircut a $5k <=40%"), + "DVOLSPREAD": ("2026-10-24", "kill se Sharpe forward < -0,50; decisione 2027-01-24"), +} + + +def venue_census() -> dict: + """Strumenti Deribit REALI: perpetual USDC, spot, opzioni. Lotto minimo in DOLLARI.""" + out = {"perp": {}, "spot": {}, "opt": {}, "read_ok": True} + ins = {} + for cur in ("BTC", "ETH", "USDC"): + d = get_json(f"get_instruments?currency={cur}&expired=false", f"instr_{cur}.json") + if d is None: + out["read_ok"] = False + continue + ins[cur] = d["result"] + bs = get_json("get_book_summary_by_currency?currency=USDC&kind=future", "bs_usdc_fut.json") + marks = {} + if bs: + for x in bs["result"]: + p = x.get("mark_price") or x.get("last") + if p: + marks[x["instrument_name"]] = (float(p), float(x.get("volume_usd") or 0.0)) + for cur, lst in ins.items(): + for x in lst: + n = x["instrument_name"] + if x["kind"] == "future" and x.get("settlement_period") == "perpetual": + mk, vol = marks.get(n, (None, None)) + out["perp"][x["base_currency"]] = dict( + instr=n, min_amt=float(x["min_trade_amount"]), + created=dt.datetime.fromtimestamp(x["creation_timestamp"] / 1000, + dt.timezone.utc).date().isoformat(), + mark=mk, min_usd=(mk * float(x["min_trade_amount"])) if mk else None, + vol24=vol) + elif x["kind"] == "spot": + out["spot"][n] = dict(min_amt=float(x["min_trade_amount"])) + elif x["kind"] == "option": + key = (cur, x["base_currency"]) + out["opt"].setdefault(key, set()).add(float(x["min_trade_amount"])) + return out + + +def sezione1(V: dict) -> dict: + sez("1) CENSIMENTO — lo stato REALE dei candidati (venue letto ORA, non memoria)") + if not V["read_ok"]: + print(" ⚠️ lista strumenti NON letta dal venue in questa corsa: si usa la cache.") + + sub("1a) I 19 major di XS01 contro cio' che Deribit quota DAVVERO (perp USDC lineari)") + print(f" {'asset':<7}{'su Deribit':<12}{'strumento':<26}{'quotato dal':<13}" + f"{'lotto min':>11}{'vol 24h $':>14}") + xs_ok, xs_stable = [], [] + oggi = dt.date.today() + for s in XS_UNIVERSE: + p = V["perp"].get(s) + if not p: + print(f" {s:<7}{'NO':<12}{'—':<26}{'—':<13}{'—':>11}{'—':>14}") + continue + xs_ok.append(s) + eta = (oggi - dt.date.fromisoformat(p["created"])).days + if eta >= 365: + xs_stable.append(s) + mu = f"${p['min_usd']:.2f}" if p["min_usd"] else "n/d" + v = f"{p['vol24']:,.0f}" if p["vol24"] is not None else "NON QUOTATO" + print(f" {s:<7}{'SI':<12}{p['instr']:<26}{p['created']:<13}{mu:>11}{v:>14}") + print(f"\n -> gambe su Deribit: {len(xs_ok)}/19 ({', '.join(xs_ok)})") + print(f" -> di cui quotate da >=1 anno: {len(xs_stable)}/19 ({', '.join(xs_stable)})") + print(" ⚠️ Deribit sta ampliando la lista PROPRIO ORA: le gambe piu' recenti hanno giorni") + print(" di vita e volume ~0. Una gamba quotata NON e' una gamba negoziabile.") + + sub("1b) LOTTO MINIMO DELLE OPZIONI — la famiglia conta (5a occorrenza dello schema fee_watch)") + for (cur, base), mins in sorted(V["opt"].items()): + if base not in ("BTC", "ETH"): + continue + m = min(mins) + mk = (V["perp"].get(base) or {}).get("mark") + fam = "USDC-LINEARE" if cur == "USDC" else "INVERSE" + usd = f"${m * mk:,.0f}" if mk else "n/d" + print(f" {fam:<14} {base:<4} min {m:<8} = {usd:>9} di sottostante per lotto") + print("\n 📌 Il numero pubblicato il 30/07 (\"BTC min 0.1 = $6.210 -> FUORI; ETH 1 contratto") + print(" = $1.832\") e' della famiglia INVERSE, che un conto USDC non puo' marginare.") + print(" Sulla famiglia che il conto PUO' usare il lotto e' ~10x piu' piccolo.") + + return dict(xs_deribit=xs_ok, xs_stable=xs_stable) + + +def sezione1c(V: dict, cens: dict) -> None: + sub("1c) LA TABELLA — stato REALE di ogni candidato a terzo sleeve") + eth = (V["perp"].get("ETH") or {}).get("mark") or 0.0 + btc = (V["perp"].get("BTC") or {}).get("mark") or 0.0 + opt_eth = min(V["opt"].get(("USDC", "ETH"), {0.1})) * eth + opt_btc = min(V["opt"].get(("USDC", "BTC"), {0.01})) * btc + n_d, n_s = len(cens["xs_deribit"]), len(cens["xs_stable"]) + rows = [ + ("STATARB-RESID", "BTC+ETH perp USDC", "SI", f"${MIN_ORDER:.0f} (soglia NOSTRA)", + "SI — netta", "27/09 (aperto)"), + ("DVOLSPREAD", "BTC+ETH perp USDC", "SI", f"${MIN_ORDER:.0f} (soglia NOSTRA)", + "SI — netta", "24/10 kill · 24/01 dec."), + ("PREVDAY", "BTC+ETH perp USDC", "SI", f"${MIN_ORDER:.0f} (soglia NOSTRA)", + "SI — netta", "nessuno (mai registrato)"), + ("XS01 (19 gambe)", "19 alt", "NO — 5 mancano", + f"{n_d}/19 quotate, {n_s} da >=1a", "PARZIALE (BTC/ETH)", "nessuno"), + (f"XS01-D ({n_s} gambe)", f"{n_s} alt su Deribit", "SI (ridotta)", + "$0,09-$7,72 dal venue", "PARZIALE (BTC/ETH)", "nessuno"), + ("XSR01", "50 alt Hyperliquid", "NO — venue diverso", + "n/d su Deribit", "NO", "23/10 (aperto)"), + ("VRP01", "opzioni BTC/ETH USDC", "SI (ETH)", + f"lotto ETH ${opt_eth:,.0f} · BTC ${opt_btc:,.0f}", "NO — altro strumento", + "nessuno (regola short-vol)"), + ("GTAA01", "6 ETF USA (IB)", "NO — altro venue", + "PRIIPs: ordine RIFIUTATO", "NO", "nessuno"), + ] + print(f" {'candidato':<19}{'strumenti':<24}{'su Deribit?':<19}{'capitale/lotto min':<28}" + f"{'netta col libro?':<22}{'gate'}") + print(" " + "-" * 100) + for r in rows: + print(f" {r[0]:<19}{r[1]:<24}{r[2]:<19}{r[3]:<28}{r[4]:<22}{r[5]}") + print("\n Finestra FORWARD dei monitor (SOLA LETTURA di data/paper_*/, nessuna scrittura):") + print(f" {'monitor':<12}{'barre':>7}{'ultima':>13}{'Sh fwd MODELED':>17} stato §32") + for nm, d, cad, sano in (("STATARB", "paper_statarb", 365.25, False), + ("XSR01", "paper_xsr", 365.25, False), + ("DVOLSPREAD", "paper_dvolspread", 365.25, False), + ("PREVDAY", "paper_prevday", 365.25 * 24, True)): + try: + st = json.loads((ROOT / "data" / d / "state.json").read_text()) + ts = pd.Timestamp(int(st["last_ts"]), unit="ms", tz="UTC") + rr = [json.loads(l)["net_modeled"] + for l in (ROOT / "data" / d / "returns.jsonl").read_text().splitlines() if l.strip()] + v = np.asarray(rr, float) + sh = (float(v.mean() / v.std() * np.sqrt(cad)) if len(v) > 3 and v.std() > 0 + else float("nan")) + flag = "SANO (1259/1319)" if sano else "ROTTO: frazione di giorno" + print(f" {nm:<12}{st.get('n_bars', len(v)):>7}{ts.strftime('%Y-%m-%d'):>13}" + f"{sh:>17.2f} {flag}") + except Exception as e: # noqa: BLE001 + print(f" {nm:<12} stato illeggibile ({type(e).__name__})") + print(" ⚠️ §32 (22/08): `advance()` consuma la barra del giorno IN CORSO -> 4 monitor su 6") + print(" registrano una FRAZIONE del giorno (STATARB ~4 min/giorno): quei tre Sharpe") + print(" NON sono un risultato. `paper_prevday` e' l'unico misurato SANO (orario), ed e'") + print(" l'unica lettura forward indipendente disponibile in questo filone.") + print(" ⚠️ Anche quello sano ha ~63 giorni: SE(Sharpe) ~ 2,3 -> non decide niente da solo.") + print("\n ⚠️ 'gate aperto' = pre-registrato e NON scaduto: in questo script si dice cosa") + print(" succederebbe SE passasse, non si decide al suo posto (anticipare un gate") + print(" pre-registrato E' selezione-sull'hold-out, ed e' una regola scritta).") + + +# ============================================================================================= +# 2) IL NETTING — misurato sul CODICE di produzione, non ricordato +# ============================================================================================= +def sezione2() -> None: + sez("2) IL VINCOLO DIMENTICATO — il NETTING su strumento unico (letto dal sorgente)") + src_net = inspect.getsource(LB.book_net_target) + src_ord = inspect.getsource(LB.build_book_order) + print(f" src/live/deribit.INSTRUMENT = {DB.INSTRUMENT}") + print(f" src/live/deribit._CONTRACT = {len(DB._CONTRACT)} strumenti dichiarati: " + f"{list(DB._CONTRACT)}") + print(f" src/live/book.W_TP01 / W_SKH = {W_TP} / {W_SKH}") + uno = ("W_TP01 * max(tp_frac" in src_net) and ("W_SKH * float(skh_sign)" in src_net) + print(f" `book_net_target` somma i due sleeve in UN solo numero per asset : {uno}" + f" (riga: `raw = weight * equity * (W_TP01*tp_frac + W_SKH*skh_sign)`)") + print(f" `build_book_order` manda UN ordine per asset, soglia min_usd : " + f"{'min_usd' in src_ord}") + print("\n CONSEGUENZA, e nessuno l'aveva messa in tabella:") + print(" * un terzo sleeve DIREZIONALE su BTC/ETH e' un TERZO ADDENDO della stessa formula:") + print(" non aggiunge un ordine, non aggiunge un min-order, non aggiunge margine lordo —") + print(" e non aggiunge nemmeno SEPARAZIONE (nessuna gamba puo' avere il proprio stop).") + print(" E' l'esatto motivo per cui la raccomandazione T1 del 26/07 fu abbandonata.") + print(" * un terzo sleeve su ALTRI strumenti (opzioni, spot, alt-perp) richiede una riga in") + print(f" `_CONTRACT` (oggi {len(DB._CONTRACT)} voci, CABLATE A MANO) piu' il ramo di") + print(" sizing corrispondente = CODICE su un percorso con soldi veri, piu' un lotto") + print(" minimo e un margine propri.") + print(" * il min-order si applica al DELTA NETTO: due sleeve che si contraddicono possono") + print(" produrre zero ordini. E' un vantaggio di costo E una perdita di controllo.") + + +# ============================================================================================= +# 3) IL FUNDING (importato) + 4) LE SERIE ANCORATE +# ============================================================================================= +def get_rate() -> dict: + """Tassi di funding orari per asset (LINEARE dove esiste, INVERSE come proxy prima).""" + F = {} + for instr, (start, a, kind) 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 tp01_at(h: int, rate: dict | None) -> pd.Series: + """TP01 all'ancora oraria h (replica di sleeves._tp01_returns), funding opzionale.""" + tp = TrendPortfolio(**CANONICAL) + series = {} + for a in ("BTC", "ETH"): + df = A_TP.daily_off(a, h) + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + r = simple_returns(df["close"].values.astype(float)) + tgt = tp.target_series(df) + held = np.zeros(len(tgt)); held[1:] = tgt[:-1] + net = held * r - tp.fee_side * np.abs(np.diff(held, prepend=0.0)) + if rate is not None: + net = net - held * FU.daily_funding(rate[a], idx) + net[0] = 0.0 + series[a] = pd.Series(np.clip(net, -0.99, None), 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 skh_at(off: int, rate: dict | None) -> pd.Series: + """SKH01 all'offset `off` della griglia 230m, lente CANONICA (quella dello sleeve).""" + return to_daily(FU.skh_book(off, "canonical", rate)) + + +def _joint_off(h: int) -> pd.DataFrame: + """Frame BTC/ETH allineato all'ancora oraria h, colonne di OS.build_joint.""" + b = A_TP.daily_off("BTC", h)[["timestamp", "datetime", "close"]].rename(columns={"close": "cb"}) + e = A_TP.daily_off("ETH", h)[["timestamp", "close"]].rename(columns={"close": "ce"}) + j = b.merge(e, on="timestamp", how="inner").sort_values("timestamp").reset_index(drop=True) + j["r_btc"] = al.simple_returns(j["cb"].values) + j["r_eth"] = al.simple_returns(j["ce"].values) + j["log_ratio"] = np.log(j["ce"].values / j["cb"].values) + return j + + +def statarb_at(h: int, rate: dict | None): + """STATARB-RESID (W=45, sgn=+1) all'ancora oraria h. Ritorna (serie daily, pos).""" + j = _joint_off(h) + pos = np.nan_to_num(np.asarray(OS.f_statarb_resid(W=45, sgn=+1)(j), float)) + idx = pd.DatetimeIndex(pd.to_datetime(j["datetime"], utc=True)) + s = OS.spread_ret(j) + held = np.zeros(len(pos)); held[1:] = pos[:-1] + net = held * s - FEE_SIDE * 2.0 * np.abs(np.diff(held, prepend=0.0)) + if rate is not None: # +1 = long ETH / short BTC -> paga f_eth - f_btc + net = net - held * (FU.daily_funding(rate["ETH"], idx) - FU.daily_funding(rate["BTC"], idx)) + net[0] = 0.0 + return to_daily(pd.Series(net, index=idx)), pos # pos NON shiftata (7b shifta da se') + + +DVS_FROZEN = dict(zwin=180, tanh_k=2.0, lw=0.6, zw=1.1, tgt=0.17, svw=60) + + +def dvolspread(rate: dict | None): + """DVOLSPREAD coi parametri CONGELATI del monitor. Ancora non variabile: il DVOL e' 1d.""" + btc, eth = ol.aligned() + wb, we = dvs_make_book(**DVS_FROZEN)(btc, eth) + wb = np.clip(np.nan_to_num(np.asarray(wb, float)), -1, 1) + we = np.clip(np.nan_to_num(np.asarray(we, float)), -1, 1) + n = len(btc) + rb = al.simple_returns(btc["close"].values.astype(float)) + re = al.simple_returns(eth["close"].values.astype(float)) + pb = np.zeros(n); pb[1:] = wb[:-1] + pe = np.zeros(n); pe[1:] = we[:-1] + net = pb * rb + pe * re - FEE_SIDE * (np.abs(np.diff(pb, prepend=0.0)) + + np.abs(np.diff(pe, prepend=0.0))) + idx = pd.DatetimeIndex(btc["dt"]) + if rate is not None: + net = net - (pb * FU.daily_funding(rate["BTC"], idx) + + pe * FU.daily_funding(rate["ETH"], idx)) + net[0] = 0.0 + assert float(np.max(np.abs(wb + we))) < 1e-12, "il book DVOLSPREAD non e' we == -wb" + return to_daily(pd.Series(net, index=idx)), wb # gamba BTC, NON shiftata + + +def prevday(rate: dict | None): + """PREVDAY range-breakout 1h, libro 50/50. Funding ORARIO (il segnale e' a 1h).""" + series, tk = {}, [] + for a in ("BTC", "ETH"): + df = al.get(a, "1h") + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + tgt = np.nan_to_num(np.asarray(prevday_target(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) + tk.append(np.abs(np.diff(pos, prepend=0.0))) + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + d = to_daily(pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)) + return d, np.concatenate(tk) + + +def xs_at(universe: list[str], phase: int, funding_drag: float = 0.0): + """XS01 su un universo arbitrario, alla fase `phase` del ciclo H. Replica ESATTA della + formula di sleeves._xsec_returns (verificata bit-exact a universo pieno / fase 0).""" + cols = {} + for sym in universe: + p = SL._HL_DIR / f"hl_{sym.lower()}_1d.parquet" + if not p.exists(): + continue + d = pd.read_parquet(p) + cols[sym] = pd.Series(d["close"].values.astype(float), + index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)) + C = pd.concat(cols, axis=1, join="inner").sort_index().dropna() + px = C.values; n, A = px.shape + cfg = SL.XS_CFG + lookbacks, H, k, mode, tv = cfg["lookbacks"], cfg["H"], cfg["k"], cfg["mode"], cfg["target_vol"] + disp_pct, minhist = cfg.get("disp_pct", 0), cfg.get("disp_minhist", 20) + kk = min(k, A // 2) + mlb = max(lookbacks) + dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0]) + W = np.zeros((n, A)); w = np.zeros(A); disp_hist = [] + for i in range(n): + if i >= mlb and i % H == phase: + rLs = [px[i] / px[i - L] - 1.0 for L in lookbacks] + disp_i = float(np.mean([r.std() for r in rLs])) + thr = (np.percentile(disp_hist, disp_pct) + if (disp_pct > 0 and len(disp_hist) >= minhist) else -np.inf) + if disp_i >= thr: + score = np.zeros(A); cnt = 0 + for rL in rLs: + sd = rL.std() + if sd > 0: + score += (rL - rL.mean()) / sd; cnt += 1 + if cnt: + score /= cnt + order = np.argsort(score) + w = np.zeros(A); lo, hi = order[:kk], order[-kk:] + if mode == "mom": + w[hi] = 0.5 / kk; w[lo] = -0.5 / kk + else: + w[lo] = 0.5 / kk; w[hi] = -0.5 / kk + else: + w = np.zeros(A) + disp_hist.append(disp_i) + W[i] = w + gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1) + turn = np.zeros(n); turn[0] = np.abs(W[0]).sum() + turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1) + net = gross - turn * (0.001 / 2.0) + s = pd.Series(net, index=C.index) + rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25) + scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0) + out = pd.Series(s.values * scale, index=C.index) + if funding_drag: + out = out - funding_drag / 365.25 + # ticket per gamba: |ΔW| a ogni ribilanciamento, scalato dal vol-target del giorno + tick = np.abs(np.diff(W, axis=0, prepend=0.0)) * scale[:, None] + return to_daily(out), tick, list(C.columns) + + +def vrp_at(phase: int) -> pd.Series: + return to_daily(A_VRP.to_daily_lumped(A_VRP.combo_weekly(phase, f=SL.VRP_CFG["f"]))) + + +# ============================================================================================= +# 3) REPLICA DI CONTROLLO +# ============================================================================================= +def sezione3(rate: dict) -> dict: + sez("3) REPLICA DI CONTROLLO — riprodurre i numeri PUBBLICATI prima di stampare un delta") + from src.portfolio.portfolio import StrategyPortfolio + P = StrategyPortfolio(SL.deribit_book_sleeves(), capital=CAPITAL_TODAY) + ref = P.combined_daily() + m, mh = metrics(ref), metrics(ref[ref.index >= HOLDOUT]) + print(" LENTE A — 'sleeve' (`deribit_book_sleeves` via StrategyPortfolio): e' quella dei") + print(" numeri pubblicati. SKH01 con fill AL LIVELLO, TP01 senza min-order,") + print(" NESSUN funding.") + print(f" ShFULL {m['sharpe']:.3f} ShHOLD {mh['sharpe']:.3f} maxDD {100*m['maxdd']:.2f}%" + f" CAGR {100*m['cagr']:.2f}% n={m['n']}") + print(" atteso dal briefing: ShFULL ~1,81 / ShHOLD ~1,44 / maxDD ~9,4% -> " + f"{'RIPRODOTTO' if (abs(m['sharpe']-1.81)<0.02 and abs(mh['sharpe']-1.44)<0.02 and abs(100*m['maxdd']-9.4)<0.15) else '⚠️ NON riproduce'}") + + mine0 = combine_outer({"TP01": tp01_at(0, None), "SKH01": skh_at(0, None)}, + {"TP01": W_TP, "SKH01": W_SKH}, lo=BOOK_START) + j = pd.concat({"a": mine0, "b": ref}, axis=1, join="outer") + dmax = float(np.nanmax(np.abs(j["a"].values - j["b"].values))) + print(f"\n [SANITY] la mia replica ancorata a (h=0, off=0) vs lo sleeve di produzione: " + f"max|dif| = {dmax:.3e} {'OK' if dmax < 1e-12 else 'FAIL'}") + assert dmax < 1e-12, "replica ancorata NON bit-exact: tutto il resto misurerebbe altro" + + withf = combine_outer({"TP01": tp01_at(0, rate), "SKH01": skh_at(0, rate)}, + {"TP01": W_TP, "SKH01": W_SKH}, lo=BOOK_START) + mf, mfh = metrics(withf), metrics(withf[withf.index >= HOLDOUT]) + print(f"\n LENTE A + FUNDING (misurato §35, non modellato prima del 22/08):") + print(f" ShFULL {mf['sharpe']:.3f} ShHOLD {mfh['sharpe']:.3f} " + f"maxDD {100*mf['maxdd']:.2f}% CAGR {100*mf['cagr']:.2f}%") + print(f" costo del funding: dSharpe {mf['sharpe']-m['sharpe']:+.3f}, " + f"dCAGR {100*(mf['cagr']-m['cagr']):+.2f} pp") + + lp = FU.book_75_25(rate, 0, "hourly", 600.0) + mlp, mlph = metrics(lp), metrics(lp[lp.index >= HOLDOUT]) + print("\n LENTE B — 'path live' (`CC.book_series('hourly')` + funding): min-order $5, fill al") + print(" close 5m del prossimo confine orario. E' cio' che il cron esegue.") + print(f" ShFULL {mlp['sharpe']:.3f} ShHOLD {mlph['sharpe']:.3f} " + f"maxDD {100*mlp['maxdd']:.2f}% CAGR {100*mlp['cagr']:.2f}%") + print("\n 📌 Le due lenti NON danno lo stesso libro (1,81 contro 1,50 di Sharpe FULL). La") + print(" misura di questo filone gira sulla LENTE A col funding, perche' e' quella in cui") + print(" sono espressi TUTTI i candidati (serie di sleeve) e quella dei numeri pubblicati.") + print(" Il delta e' comunque un confronto APPAIATO: la lente si cancella in gran parte.") + return dict(ref=ref, m=m, mh=mh, base_f=withf) + + +# ============================================================================================= +# 4) COSTRUZIONE DELLE SERIE ANCORATE (con cache) +# ============================================================================================= +def build_series(rate: dict, cens: dict, force: bool = False) -> dict: + if CACHE.exists() and not force: + try: + S = pickle.loads(CACHE.read_bytes()) + if S.get("_universe") == (cens["xs_stable"], cens["xs_deribit"]): + print(f" {clock()} serie da cache ({CACHE.name})") + return S + except Exception: # noqa: BLE001 + pass + S: dict = {"_universe": (cens["xs_stable"], cens["xs_deribit"])} + t = time.time() + S["TP01"] = {h: tp01_at(h, rate) for h in TP_HOURS} + print(f" {clock()} TP01 x{len(TP_HOURS)} ancore orarie ({time.time()-t:.0f}s)") + t = time.time() + S["SKH01"] = {o: skh_at(o, rate) for o in SKH_OFFSETS} + print(f" {clock()} SKH01 x{len(SKH_OFFSETS)} offset di griglia ({time.time()-t:.0f}s)") + t = time.time() + sa = {h: statarb_at(h, rate) for h in TP_HOURS} + S["STATARB"] = {h: v[0] for h, v in sa.items()} + S["_statarb_pos"] = sa[0][1] + print(f" {clock()} STATARB x{len(TP_HOURS)} ancore ({time.time()-t:.0f}s)") + t = time.time() + d, dpos = dvolspread(rate) + S["DVOLSPREAD"] = {0: d}; S["_dvs_pos"] = dpos + p, ptk = prevday(rate) + S["PREVDAY"] = {0: p}; S["_prevday_turn"] = ptk + print(f" {clock()} DVOLSPREAD + PREVDAY ({time.time()-t:.0f}s)") + t = time.time() + xs19 = {ph: xs_at(XS_UNIVERSE, ph, XS_FUNDING_DRAG_YR) for ph in XS_PHASES} + S["XS01"] = {ph: v[0] for ph, v in xs19.items()} + S["_xs19_tick"] = xs19[0][1]; S["_xs19_cols"] = xs19[0][2] + xsd = {ph: xs_at(cens["xs_stable"], ph, XS_FUNDING_DRAG_YR) for ph in XS_PHASES} + S["XS01-D"] = {ph: v[0] for ph, v in xsd.items()} + S["_xsd_tick"] = xsd[0][1]; S["_xsd_cols"] = xsd[0][2] + xsd14 = {ph: xs_at(cens["xs_deribit"], ph, XS_FUNDING_DRAG_YR) for ph in XS_PHASES} + S["XS01-D14"] = {ph: v[0] for ph, v in xsd14.items()} + S["_xsd14_cols"] = xsd14[0][2] + print(f" {clock()} XS01 (19 gambe) + XS01-D ({len(cens['xs_stable'])} gambe) x10 fasi " + f"({time.time()-t:.0f}s)") + t = time.time() + S["VRP01"] = {ph: vrp_at(ph) for ph in VRP_PHASES} + print(f" {clock()} VRP01 x{len(VRP_PHASES)} fasi ({time.time()-t:.0f}s)") + CACHE.parent.mkdir(parents=True, exist_ok=True) + CACHE.write_bytes(pickle.dumps(S)) + return S + + +def sezione4(S: dict, cens: dict) -> None: + sez("4) LE SERIE ANCORATE — sanity obbligatorio, poi il profilo standalone") + x0, _, cols = xs_at(XS_UNIVERSE, 0, 0.0) + ref = to_daily(SL._xsec_returns()) + jj = pd.concat({"a": x0, "b": ref}, axis=1, join="outer") + dm = float(np.nanmax(np.abs(jj["a"].values - jj["b"].values))) + print(f" [SANITY] xs_at(19 gambe, fase 0, drag 0) vs sleeves._xsec_returns : " + f"max|dif| = {dm:.3e} {'OK' if dm < 1e-12 else 'FAIL'}") + assert dm < 1e-12, "la replica XS01 non e' bit-exact" + v0 = vrp_at(0); vr = to_daily(SL._vrp_combo_returns()) + jj = pd.concat({"a": v0, "b": vr}, axis=1, join="outer") + dm2 = float(np.nanmax(np.abs(np.nan_to_num(jj["a"].values) - np.nan_to_num(jj["b"].values)))) + print(f" [SANITY] vrp_at(fase 0) vs sleeves._vrp_combo_returns : " + f"max|dif| = {dm2:.3e} {'OK' if dm2 < 1e-12 else 'ATTENZIONE'}") + + jj = OS.build_joint("1d") + ev = OS.eval_spread(jj, OS.f_statarb_resid(W=45, sgn=+1)(jj)) + mine = metrics(statarb_at(0, None)[0])["sharpe"] + print(f" [SANITY] STATARB senza funding, mio percorso {mine:.3f} vs il motore originale " + f"`OS.eval_spread` {ev['full']['sharpe']:.3f} (scarto {mine-ev['full']['sharpe']:+.3f})") + print(" -> `daily_off(h=0)` e `al.get('1d')` sono lo STESSO resample: due percorsi") + print(" indipendenti danno la stessa serie, quindi l'ancora h=0 e' la canonica.") + + print(f"\n {'candidato':<16}{'da':>12}{'n':>7}{'ShFULL':>9}{'ShHOLD':>9}{'maxDD':>9}" + f"{'CAGR':>9}{'corr libro':>12}") + base = combine_outer({"TP01": S["TP01"][0], "SKH01": S["SKH01"][0]}, + {"TP01": W_TP, "SKH01": W_SKH}, lo=BOOK_START) + for nm in CANDIDATI: + s = S[nm][sorted(S[nm])[0]] + s = s[s.index >= BOOK_START] if nm != "XS01" else s + m = metrics(s); mh = metrics(s[s.index >= HOLDOUT]) + j = pd.concat({"a": s, "b": base}, axis=1, join="inner").dropna() + c = float(j["a"].corr(j["b"])) if len(j) > 60 else float("nan") + print(f" {nm:<16}{str(s.index.min().date()):>12}{m['n']:>7}{m['sharpe']:>9.3f}" + f"{mh['sharpe']:>9.3f}{100*m['maxdd']:>8.1f}%{100*m['cagr']:>8.1f}%{c:>12.3f}") + print(f"\n NB XS01/XS01-D includono il drag di funding dichiarato " + f"({100*XS_FUNDING_DRAG_YR:.2f}%/anno, §37); gli altri hanno il funding modellato " + "gamba per gamba.") + print(f" XS01-D gira su {len(cens['xs_stable'])} gambe: {', '.join(cens['xs_stable'])}") + + +def sezione4b(S: dict) -> None: + sub("4b) `marginal_vs_tp01` — il verdetto STANDARD del progetto su ogni candidato") + print(" Un candidato direzionale/relative-value si giudica su ADDS (corr, uplift OOS,") + print(" alpha residua, robust_oos multicut, has_insample_edge, is_hedge), non sullo Sharpe.") + print(f"\n {'candidato':<12}{'verdetto':>10}{'corr':>8}{'robust_oos':>12}" + f"{'insample_edge':>15}{'is_hedge':>10}{'beats_noise':>13}") + for c in CANDIDATI: + d = S[c][sorted(S[c])[0]] + try: + m = al.marginal_vs_tp01(d) + except Exception as e: # noqa: BLE001 + print(f" {c:<12} non calcolabile: {type(e).__name__}: {e}") + continue + cf = m.get("corr_full") + print(f" {c:<12}{str(m.get('marginal_verdict')):>10}" + f"{(cf if cf is not None else float('nan')):>8.3f}" + f"{str(m.get('robust_oos')):>12}{str(m.get('has_insample_edge')):>15}" + f"{str(m.get('is_hedge')):>10}{str(m.get('beats_noise_null')):>13}") + print("\n ⚠️ Su una serie che comincia nel 2024 (XS01*) `marginal_vs_tp01` ha poco campione") + print(" in-sample: il suo verdetto li' e' debole per costruzione, non informativo.") + + +def sezione4c(cens: dict, n_sub: int = 100) -> None: + sub("4c) IL COLLASSO DI XS01-D: e' l'AMPIEZZA o e' QUEL sottoinsieme? (null dei subset)") + k = len(cens["xs_stable"]) + print(f" XS01 a 19 gambe fa Sharpe ~1,27; ristretto alle {k} quotate su Deribit da >=1 anno") + print(" crolla. Due spiegazioni possibili, e vanno separate: (a) con poche gambe il") + print(f" long-5/short-5 prende 10 posizioni su {k} e non seleziona piu' niente (AMPIEZZA);") + print(" (b) le gambe che Deribit non quota sono proprio quelle che portavano l'edge.") + print(f" Test: {n_sub} sottoinsiemi CASUALI di {k} gambe fra le 19, stesso meccanismo,") + print(" e si guarda dove cade quello di Deribit. (Stessa forma del null dei 6-subset che") + print(" il 25/07 mise GTAA6 al 95° pctl.)") + rng = np.random.default_rng(SEED) + sh = [] + for _ in range(n_sub): + u = list(rng.choice(XS_UNIVERSE, size=k, replace=False)) + sh.append(metrics(xs_at(u, 0, XS_FUNDING_DRAG_YR)[0])["sharpe"]) + sh = np.array(sh) + mine = metrics(xs_at(cens["xs_stable"], 0, XS_FUNDING_DRAG_YR)[0])["sharpe"] + pct = float((sh < mine).mean() * 100.0) + print(f"\n {n_sub} sottoinsiemi casuali da {k}: mediana {np.median(sh):.3f} " + f"[p10 {np.percentile(sh,10):.3f}, p90 {np.percentile(sh,90):.3f}] " + f"min {sh.min():.3f} max {sh.max():.3f}") + print(f" il sottoinsieme DERIBIT: {mine:.3f} -> {pct:.0f}° percentile") + print(f" riferimento a 19 gambe: {metrics(xs_at(XS_UNIVERSE, 0, XS_FUNDING_DRAG_YR)[0])['sharpe']:.3f}") + full = metrics(xs_at(XS_UNIVERSE, 0, XS_FUNDING_DRAG_YR)[0])["sharpe"] + med = float(np.median(sh)) + q_ampiezza = (full - med) / (full - mine) if abs(full - mine) > 1e-9 else float("nan") + print(f"\n -> DIAGNOSI, e sono DUE effetti, non uno (la mia prima stesura ne dichiarava uno") + print(" solo e sarebbe stata una semplificazione comoda):") + print(f" (1) AMPIEZZA: gia' il sottoinsieme MEDIANO da {k} gambe scende {full:.3f} -> " + f"{med:.3f}.") + print(f" Vale il {100*q_ampiezza:.0f}% della caduta totale, e nessun capitale la ripara:") + print(f" con {k} gambe il long-5/short-5 tiene 10 posizioni su {k}.") + print(f" (2) SFORTUNA DI LISTINO: dentro quella distribuzione il sottoinsieme che Deribit") + print(f" quota davvero sta al {pct:.0f}° percentile ({mine:.3f} contro mediana {med:.3f}).") + print(f" Le 8 gambe mancanti non sono un campione casuale: sono gli alt piu' recenti") + print(" e piu' dispersi, cioe' quelli da cui un cross-sectional prende il segnale.") + print(" Conseguenza operativa identica in entrambi i casi: aspettare che Deribit quoti") + print(f" 2-3 alt in piu' non basta — il sottoinsieme mediano a {k} gambe e' gia' meta'") + print(" del 19-gambe, e la ricostruzione dell'universo pieno passa dal SECONDO VENUE.") + print(" ⚠️ Questo test gira sui prezzi HYPERLIQUID anche per il sottoinsieme 'Deribit': e'") + print(" il segnale, non il fill. La divergenza fra venue su un ranking cross-sezionale e'") + print(" gia' stata misurata trascurabile (22/08: corr 0,9991 fra i due venue).") + + +# ============================================================================================= +# 5) LA MISURA — delta APPAIATI sulla banda d'ancora congiunta +# ============================================================================================= +PASSED_TXT = "?" +CANDIDATI = ["XS01", "XS01-D", "XS01-D14", "STATARB", "DVOLSPREAD", "PREVDAY", "VRP01"] +ANCHOR_OF = {"XS01": XS_PHASES, "XS01-D": XS_PHASES, "XS01-D14": XS_PHASES, + "STATARB": TP_HOURS, + "DVOLSPREAD": (0,), "PREVDAY": (0,), "VRP01": VRP_PHASES} + + +def _stats(s: pd.Series) -> tuple[float, float, float, float]: + m = metrics(s) + mh = metrics(s[s.index >= HOLDOUT]) + return m["sharpe"], mh["sharpe"], m["cagr"], m["maxdd"] + + +def sezione5(S: dict, draws: int) -> dict: + sez(f"5) LA MISURA — dSharpe / d drift / d maxDD del LIBRO LIVE con un terzo sleeve") + print(f" Spazio d'ancora congiunto: TP01 x{len(TP_HOURS)} · SKH01 x{len(SKH_OFFSETS)} · " + f"candidato x(1..{len(XS_PHASES)}).") + print(f" {draws} estrazioni UNIFORMI INDIPENDENTI (seed {SEED}); la statistica e' la") + print(" MEDIANA DELLE DIFFERENZE APPAIATE (mai la differenza delle mediane: cadono su") + print(" ancore diverse — errore ribaltante catturato il 26/07).") + rng = np.random.default_rng(SEED) + hs = rng.choice(TP_HOURS, size=draws) + os_ = rng.choice(SKH_OFFSETS, size=draws) + ph = {c: rng.choice(ANCHOR_OF[c], size=draws) for c in CANDIDATI} + out: dict = {c: {w: {"sh": [], "shh": [], "cagr": [], "dd": []} for w in WEIGHTS_TESTED} + for c in CANDIDATI} + base_stats = [] + t = time.time() + 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) + bs = _stats(b) + base_stats.append(bs) + for c in CANDIDATI: + cols3 = dict(cols2); cols3["CAND"] = S[c][ph[c][i]] + for w in WEIGHTS_TESTED: + a = combine_outer(cols3, {"TP01": W_TP * (1 - w), "SKH01": W_SKH * (1 - w), + "CAND": w}, lo=BOOK_START) + st = _stats(a) + out[c][w]["sh"].append(st[0] - bs[0]) + out[c][w]["shh"].append(st[1] - bs[1]) + out[c][w]["cagr"].append(st[2] - bs[2]) + out[c][w]["dd"].append(st[3] - bs[3]) + if i == 4: + print(f" {clock()} ritmo: {(time.time()-t)/5:.2f}s/estrazione -> stimati " + f"{(time.time()-t)/5*draws:.0f}s") + B = np.array(base_stats) + print(f"\n LIBRO LIVE DE-LUCKATO (2 sleeve, funding dentro), mediana delle {draws} estrazioni:") + print(f" ShFULL {np.median(B[:,0]):.3f} [p10 {np.percentile(B[:,0],10):.3f}, " + f"p90 {np.percentile(B[:,0],90):.3f}] ShHOLD {np.median(B[:,1]):.3f} " + f"CAGR {100*np.median(B[:,2]):.2f}% maxDD {100*np.median(B[:,3]):.2f}%") + can = _stats(combine_outer({"TP01": S["TP01"][0], "SKH01": S["SKH01"][0]}, + {"TP01": W_TP, "SKH01": W_SKH}, lo=BOOK_START)) + pc = float((B[:, 0] < can[0]).mean() * 100.0) + print(f" ancora CANONICA (h=0, off=0): ShFULL {can[0]:.3f} = {pc:.1f}° pctl della banda " + f"-> fortuna d'ancora {can[0]-np.median(B[:,0]):+.3f}") + + print(" ⚠️ `d maxDD` esce ~0,00 per XS01*: il maxDD FULL del libro e' fissato dal 2022, e") + print(" quei candidati cominciano nel 2024 -> non possono toccarlo. Non e' 'non protegge',") + print(" e' 'non c'era'. Per loro la colonna che informa e' d maxDD sull'hold-out, non FULL.") + for c in CANDIDATI: + sub(f"5.{CANDIDATI.index(c)+1}) {c}") + print(f" {'peso':>6}{'dShFULL':>12}{'>0':>7}{'[p10,p90]':>20}{'dShHOLD':>11}{'>0':>7}" + f"{'d CAGR pp':>12}{'d maxDD pp':>13}{'>0(peggio)':>12}") + for w in WEIGHTS_TESTED: + r = out[c][w] + a = np.array(r["sh"]); ah = np.array(r["shh"]) + ac = np.array(r["cagr"]); ad = np.array(r["dd"]) + print(f" {w:>5.0%}{np.median(a):>+12.4f}{100*(a>0).mean():>6.0f}%" + f" [{np.percentile(a,10):+.3f},{np.percentile(a,90):+.3f}]" + f"{np.median(ah):>+11.4f}{100*(ah>0).mean():>6.0f}%" + f"{100*np.median(ac):>+12.2f}{100*np.median(ad):>+13.2f}" + f"{100*(ad>0).mean():>11.0f}%") + return dict(out=out, base=B) + + +def sezione5b(S: dict) -> None: + sez("5b) A COSA SERVE UN dSharpe — il guadagno e' RITORNO o solo RISCHIO IN MENO?") + print(" In sezione 5 quasi tutti i candidati alzano lo Sharpe e ABBASSANO il drift: a") + print(" nozionale invariato il libro guadagna MENO. E' la regola gia' scritta il 25/07") + print(" (\"un diversificatore a basso CAGR si giudica a ISO-RISCHIO, mai a iso-nozionale\").") + print(" ⚠️ Il null del de-levering qui sarebbe DEGENERE (regola 22/08 §8d: sh(k·base) ≡") + print(" sh(base)) -> non e' un test, e' un cambio di unita'. La domanda giusta e' l'altra:") + print(" quanto vale quel dSharpe SE lo si riconverte in ritorno, e la leva ESISTE?") + print(" Qui: si leva il libro a 2 sleeve del fattore k che ne eguaglia la vol al libro a 3,") + print(" e si confrontano i DRIFT. k<1 = il terzo sleeve compra rischio in meno.") + print(" ⚠️ E la leva NON esiste come knob: `config/live.json` non ha una chiave di scala") + print(" (§38, verificato: servirebbe toccare W_TP01/W_SKH in src/live/book.py = CODICE") + print(" su un percorso con soldi veri). Finche' non esiste, la colonna 'd drift' di") + print(" sezione 5 e' quella che si incassa, e questa e' quella che si potrebbe incassare.") + base = combine_outer({"TP01": S["TP01"][0], "SKH01": S["SKH01"][0]}, + {"TP01": W_TP, "SKH01": W_SKH}, lo=BOOK_START) + rb = np.asarray(base.values, float) + v2 = float(rb.std() * np.sqrt(365.25)) + d2 = float(np.mean(np.log1p(rb)) * 365.25) + print(f"\n libro a 2 sleeve (ancora canonica): vol {100*v2:.2f}% drift log {100*d2:.2f}%/anno") + print(f"\n {'candidato':<12}{'peso':>6}{'vol 3sl':>10}{'k iso-vol':>11}" + f"{'drift 2sl levato':>18}{'drift 3sl':>12}{'d drift pp':>12}{'verdetto':>14}") + for c in CANDIDATI: + for w in (0.15,): + cols = {"TP01": S["TP01"][0], "SKH01": S["SKH01"][0], "CAND": S[c][sorted(S[c])[0]]} + a = combine_outer(cols, {"TP01": W_TP * (1 - w), "SKH01": W_SKH * (1 - w), "CAND": w}, + lo=BOOK_START) + # confronto sulla FINESTRA COMUNE: uno sleeve che parte nel 2024 non si giudica + # contro un libro che comincia nel 2019 (sarebbero due campioni diversi). + j = pd.concat({"a": a, "b": base}, axis=1, join="inner").dropna() + ra = np.asarray(j["a"].values, float); rbb = np.asarray(j["b"].values, float) + v3 = float(ra.std() * np.sqrt(365.25)); vb = float(rbb.std() * np.sqrt(365.25)) + k = v3 / vb if vb > 0 else float("nan") + dr3 = float(np.mean(np.log1p(ra)) * 365.25) + dr2k = float(np.mean(np.log1p(np.clip(k * rbb, -0.99, None))) * 365.25) + dd = 100 * (dr3 - dr2k) + verdict = "AGGIUNGE" if dd > 0.05 else ("PEGGIORA" if dd < -0.05 else "neutro") + print(f" {c:<12}{w:>5.0%}{100*v3:>9.2f}%{k:>11.3f}{100*dr2k:>17.2f}%" + f"{100*dr3:>11.2f}%{dd:>+12.2f}{verdict:>14}") + print("\n (finestra COMUNE ai due libri: per XS01* e VRP01 e' la loro storia, non il 2019+.)") + + +# ============================================================================================= +# 6) weights_tilt_null +# ============================================================================================= +def sezione6(S: dict) -> dict: + sez("6) `weights_tilt_null` — IL gate del progetto per ogni cambio di pesi") + print(" Ogni proposta di cambio pesi si giudica contro il null dei tilt casuali") + print(" cap-respecting: gate_pass solo se delta_insample >= 0 E percentile < firma") + print(f" best-of-k. k_seen = {len(CANDIDATI)}x{len(WEIGHTS_TESTED)} = " + f"{len(CANDIDATI)*len(WEIGHTS_TESTED)} configurazioni guardate sull'hold-out qui.") + k_seen = len(CANDIDATI) * len(WEIGHTS_TESTED) + res = {} + print(f"\n {'candidato':<14}{'peso':>6}{'d_insample':>13}{'d_hold':>10}{'d_full':>10}" + f"{'pctl_hold':>11}{'best-of-k':>11}{'rnd>cur':>9}{'GATE':>9}") + for c in CANDIDATI: + cols = {"TP01": S["TP01"][0], "SKH01": S["SKH01"][0], c: S[c][sorted(S[c])[0]]} + w_cur = {"TP01": W_TP, "SKH01": W_SKH, c: 0.0} + for w in WEIGHTS_TESTED: + w_prop = {"TP01": W_TP * (1 - w), "SKH01": W_SKH * (1 - w), c: w} + try: + r = weights_tilt_null(cols, w_cur, w_prop, floor=0.03, n=300, k_seen=k_seen) + except Exception as e: # noqa: BLE001 + print(f" {c:<14}{w:>5.0%} non calcolabile: {type(e).__name__}") + continue + res[(c, w)] = r + print(f" {c:<14}{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']:>9.2f}" + f"{('PASS' if r['gate_pass'] else 'FAIL'):>9}") + npass = sum(1 for v in res.values() if v["gate_pass"]) + print(f"\n PASS {npass}/{len(res)}. ⚠️ VA LETTO BENE, e la colonna che conta e' `rnd>cur`:") + print(" e' la frazione di tilt CASUALI a 3 sleeve che battono il libro attuale sull'hold-out.") + print(" Dove vale 0,9 il claim \"questo candidato migliora l'hold-out\" e' GENERICO: lo fa") + print(" quasi qualunque cosa gli si metta accanto, e il gate passa proprio perche' la") + print(" proposta NON e' eccezionale fra i tilt casuali (pctl basso = non e' un best-of-k).") + print(" `weights_tilt_null` e' NECESSARIO, non sufficiente — e' scritto nel suo docstring.") + print(" ✅ Controllo positivo: il gate HA potenza — fallisce su XS01-D a 10/15/20% (dove il") + print(" delta in-sample e' negativo). Un gate che passa tutto sarebbe indistinguibile") + print(" da uno rotto.") + return res + + +# ============================================================================================= +# 7) ESEGUIBILITA' PER CAPITALE +# ============================================================================================= +def _gated_sharpe(pos: np.ndarray, ret: np.ndarray, idx, alloc: float, + n_legs: int, fee_side: float = FEE_SIDE) -> tuple[float, int, float]: + """Sharpe con min-order per GAMBA: un Δ di nozionale sotto $5 non si esegue.""" + per_leg = alloc / max(n_legs, 1) + held = np.empty(len(pos)); cur = 0.0; n_tr = 0; n_skip = 0 + for i in range(len(pos)): + if abs(pos[i] - cur) * per_leg >= MIN_ORDER: + cur = pos[i]; n_tr += 1 + elif abs(pos[i] - cur) > 1e-12: + n_skip += 1 + held[i] = cur + h = np.zeros(len(held)); h[1:] = held[:-1] + net = h * ret - fee_side * n_legs * np.abs(np.diff(h, prepend=0.0)); net[0] = 0.0 + s = pd.Series(net, index=idx) + return metrics(to_daily(s))["sharpe"], n_tr, (n_skip / max(n_tr + n_skip, 1)) + + +def sezione7(S: dict, V: dict, cens: dict) -> None: + sez("7) ESEGUIBILITA' PER CAPITALE — ticket per gamba, min-order, haircut") + print(f" min_order_usd = ${MIN_ORDER:.0f} (config/live.json, letto). E' la soglia NOSTRA:") + print(" il lotto del VENUE e' 1-2 ordini di grandezza sotto (sezione 1).") + + sub("7a) TICKET MEDIO PER GAMBA al peso 15% del libro") + rows = [] + j = _joint_off(0) + idx_sa = pd.DatetimeIndex(pd.to_datetime(j["datetime"], utc=True)) + sa_pos = S["_statarb_pos"] + sa_dtk = np.abs(np.diff(sa_pos, prepend=0.0)) + btc, eth = ol.aligned() + dvs_tk = np.abs(np.diff(S["_dvs_pos"], prepend=0.0)) + xs19_tk = S["_xs19_tick"]; xsd_tk = S["_xsd_tick"] + pv_tk = S["_prevday_turn"] + spec = [ + ("STATARB", sa_dtk[sa_dtk > 1e-9], 2, "BTC+ETH perp (NETTA col libro)"), + ("DVOLSPREAD", dvs_tk[dvs_tk > 1e-9], 2, "BTC+ETH perp (NETTA col libro)"), + ("PREVDAY", pv_tk[pv_tk > 1e-9], 2, "BTC+ETH perp (NETTA col libro)"), + ("XS01", xs19_tk[xs19_tk > 1e-9], 2 * SL.XS_CFG["k"], "19 gambe HL (venue diverso)"), + (f"XS01-D", xsd_tk[xsd_tk > 1e-9], 2 * min(SL.XS_CFG["k"], len(cens["xs_stable"]) // 2), + f"{len(cens['xs_stable'])} gambe Deribit (>=1a)"), + ] + print(f" {'candidato':<12}{'gambe':>7}{'Δpeso/gamba med':>18}" + + "".join(f"{('$'+str(int(c))):>12}" for c in CAP_LADDER) + " nota") + for nm, tk, legs, nota in spec: + med = float(np.median(tk)) if len(tk) else 0.0 + cells = "" + for C in CAP_LADDER: + tick = med * C * 0.15 # Δpeso di UNA gamba x nozionale allocato + cells += f"{('$%.2f' % tick):>12}" + print(f" {nm:<12}{legs:>7}{med:>18.4f}{cells} {nota}") + print(f" (ticket = Δpeso mediano di UNA gamba x (capitale x 15%). Sotto ${MIN_ORDER:.0f} " + "l'ordine NON parte.)") + + sub("7b) HAIRCUT DA MIN-ORDER — Sharpe modellato vs Sharpe eseguibile") + print(f" {'candidato':<12}" + "".join(f"{('$'+str(int(c))):>26}" for c in CAP_LADDER)) + print(f" {'':<12}" + "".join(f"{'Sh (haircut, %salt)':>26}" for _ in CAP_LADDER)) + sa_ret = OS.spread_ret(j) + # il book DVOLSPREAD e' (wb su BTC, -wb su ETH) -> il ritorno per unita' di wb e' rb - re. + dvs_ret = (al.simple_returns(btc["close"].values.astype(float)) + - al.simple_returns(eth["close"].values.astype(float))) + for nm, pos, ret, idx, legs in ( + ("STATARB", sa_pos, sa_ret, idx_sa, 2), + ("DVOLSPREAD", S["_dvs_pos"], dvs_ret, pd.DatetimeIndex(btc["dt"]), 2)): + line = f" {nm:<12}" + base_sh = _gated_sharpe(pos, ret, idx, 1e12, legs)[0] + for C in CAP_LADDER: + sh, ntr, skip = _gated_sharpe(pos, ret, idx, C * 0.15, legs) + line += f"{('%.3f (%+.3f, %.0f%%)' % (sh, sh-base_sh, 100*skip)):>26}" + print(line) + print(" (Sh a capitale infinito = modellato. Il segno del haircut puo' essere POSITIVO: il") + print(" pavimento salta ribilanci piccoli e risparmia fee — e' de-levering, non merito.)") + + sub("7c) VRP01 — l'aritmetica dei LOTTI (non e' min-order, e' granularita' intera)") + e_mk = (V["perp"].get("ETH") or {}).get("mark") or 0.0 + b_mk = (V["perp"].get("BTC") or {}).get("mark") or 0.0 + lot_e = min(V["opt"].get(("USDC", "ETH"), {0.1})) * e_mk + lot_b = min(V["opt"].get(("USDC", "BTC"), {0.01})) * b_mk + print(f" Il collaterale dello sleeve e' il PUT VENDUTO (cash-secured sullo strike): un lotto") + print(f" vale ~1 unita' di sottostante x min_trade_amount.") + print(f" ETH_USDC opzioni : lotto minimo = ${lot_e:,.0f} di nozionale") + print(f" BTC_USDC opzioni : lotto minimo = ${lot_b:,.0f} di nozionale") + print(f"\n {'capitale':>10}{'peso 12%':>11}{'lotti ETH':>11}{'lotti BTC':>11}" + f"{'% del conto per 1 lotto ETH':>30}") + for C in CAP_LADDER: + a = C * 0.12 + print(f" {('$%.0f' % C):>10}{('$%.0f' % a):>11}{int(a // lot_e) if lot_e else 0:>11}" + f"{int(a // lot_b) if lot_b else 0:>11}{(100*lot_e/C):>29.1f}%") + print(f"\n -> 1 lotto ETH = peso di book del 12% a capitale ~${lot_e/0.12:,.0f}; il BTC entra") + print(f" a ~${lot_b/0.12:,.0f}. Il 30/07 questi numeri erano ${1832/0.12:,.0f} e " + f"${6210/0.12:,.0f} perche' misurati sulla famiglia INVERSE.") + print(" ⚠️ NON promuove VRP01: resta fuori per la REGOLA 'niente short-vol da modello in") + print(" deploy' e per f=0,73 misurato sulle quote vere. Cade il muro del LOTTO, non la regola.") + + +def sezione7d(rate: dict) -> None: + sub("7d) IL CONTO GIUSTO PER UN CANDIDATO CHE NETTA — min-order sul TARGET NETTO") + print(" 7a/7b trattano il candidato come un libro a se': per uno sleeve che NETTA e' il") + print(" conto SBAGLIATO. Il cron manda UN ordine per asset sul target NETTO, e il") + print(" pavimento di $5 morde li'. Qui il target netto e' ricostruito come in produzione:") + print(" net_a = 0.5 · E · [0.75(1-w)·tp_frac_a + 0.25(1-w)·skh_sign_a + w·cand_pos_a]") + tp = TrendPortfolio(**CANONICAL) + base_pos, cand_pos = {}, {"STATARB": {}, "DVOLSPREAD": {}} + for a in ("BTC", "ETH"): + df = A_TP.daily_off(a, 0) + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + frac = np.nan_to_num(np.asarray(tp.target_series(df), float)) + _, tr = FU.skh_daily(a, 0, "canonical", rate) + sign = np.zeros(len(idx)) + tms = FU.to_ms(idx) + for t in tr: # segno SKH01 sui giorni in cui e' aperto + m = (tms >= t["t_in"]) & (tms < t["t_out"]) + sign[m] = t["dir"] + base_pos[a] = pd.DataFrame({"frac": frac, "sign": sign}, index=idx) + j = _joint_off(0) + ix = pd.DatetimeIndex(pd.to_datetime(j["datetime"], utc=True)) + sp = np.nan_to_num(np.asarray(OS.f_statarb_resid(W=45, sgn=+1)(j), float)) + cand_pos["STATARB"]["ETH"] = pd.Series(sp, index=ix) + cand_pos["STATARB"]["BTC"] = pd.Series(-sp, index=ix) + btc, eth = ol.aligned() + wb, _we = dvs_make_book(**DVS_FROZEN)(btc, eth) + wb = np.clip(np.nan_to_num(np.asarray(wb, float)), -1, 1) + ixd = pd.DatetimeIndex(btc["dt"]) + cand_pos["DVOLSPREAD"]["BTC"] = pd.Series(wb, index=ixd) + cand_pos["DVOLSPREAD"]["ETH"] = pd.Series(-wb, index=ixd) + + def net_orders(cand: str | None, w: float, E: float) -> tuple[int, float, float]: + n_ord, tot = 0, 0.0 + yrs = max(len(base_pos[a]) for a in ("BTC", "ETH")) / 365.25 + for a in ("BTC", "ETH"): + b = base_pos[a] + raw = 0.5 * E * ((W_TP * (1 - w)) * np.maximum(b["frac"].values, 0.0) + + (W_SKH * (1 - w)) * b["sign"].values) + ser = pd.Series(raw, index=b.index) + if cand: + cp = cand_pos[cand][a].reindex(ser.index).fillna(0.0) + ser = ser + 0.5 * E * w * cp.values + cur = 0.0 + for v in ser.values: + if abs(v - cur) >= MIN_ORDER: + n_ord += 1; tot += abs(v - cur); cur = v + return n_ord, tot / max(n_ord, 1), yrs + + print(f"\n {'configurazione':<26}" + "".join(f"{('$'+str(int(c))):>30}" for c in CAP_LADDER)) + print(f" {'':<26}" + "".join(f"{'ord/a ticket turnover/a':>30}" for _ in CAP_LADDER)) + for cand, w in ((None, 0.0), ("STATARB", 0.15), ("DVOLSPREAD", 0.15)): + lab = "2 sleeve (com'e' oggi)" if cand is None else f"+ {cand} @15%" + line = f" {lab:<26}" + for C in CAP_LADDER: + n, tk, yrs = net_orders(cand, w, C) + line += f"{('%.0f $%.0f $%.0f' % (n / yrs, tk, n * tk / yrs)):>30}" + print(line) + print("\n 📌 Un terzo sleeve che netta NON paga un min-order proprio: cambia il NUMERO di") + print(" ordini del libro, non il loro pavimento. Il conteggio ordini RADDOPPIA, il") + print(" TURNOVER (cioe' la fee) sale molto meno — e' il segno che il pavimento sta") + print(" tagliando ribilanci piccoli, non che il libro trada di piu' in nozionale.") + print(" ✅ E c'e' un vantaggio strutturale mai scritto: le fee modellate negli sleeve sono") + print(" un LIMITE SUPERIORE. Ogni sleeve paga sul PROPRIO |Δpos|, ma il conto paga sul") + print(" |Δ(somma)|, e per disuguaglianza triangolare |Δ(Σ w·p)| <= Σ w·|Δp|.") + print(" ⚠️ Il rovescio, misurato in sezione 2: il candidato non puo' avere un proprio stop,") + print(" e i suoi ribilanci si CANCELLANO contro quelli di TP01/SKH01 quando discordano.") + + +# ============================================================================================= +# 8) LA SCALA +# ============================================================================================= +def sezione8(S: dict, M: dict, G: dict, V: dict, cens: dict) -> None: + sez("8) LA SCALA — a quale capitale entra il primo, chi e', e per quale muro") + global PASSED_TXT + byc: dict = {} + for (c, w), r in G.items(): + byc.setdefault(c, []).append((w, r["gate_pass"])) + PASSED_TXT = " · ".join(f"{c}: {sum(1 for _, g in v if g)}/{len(v)}" + for c, v in byc.items()) + lot_e = min(V["opt"].get(("USDC", "ETH"), {0.1})) * ((V["perp"].get("ETH") or {}).get("mark") or 0) + lot_b = min(V["opt"].get(("USDC", "BTC"), {0.01})) * ((V["perp"].get("BTC") or {}).get("mark") or 0) + W = 0.15 + print(f" `weights_tilt_null` superato (pesi su 4): {PASSED_TXT}\n") + + sub("8a) IL MURO DI CIASCUNO — quale vincolo morde per primo, e se e' una soglia di CAPITALE") + muri = [ + ("XS01 (19 gambe)", "VENUE", + "5 gambe su 19 non esistono su Deribit; su Hyperliquid serve un SECONDO CONTO,", + "e la decisione d'operatore del 26/07 tiene tutto su Deribit fino a $20.000.", + "NON e' una soglia di eseguibilita' (falsificata il 22/08): e' la decisione di venue."), + (f"XS01-D ({len(cens['xs_stable'])} gambe)", "AMPIEZZA + SFORTUNA DI LISTINO", + f"con {len(cens['xs_stable'])} gambe il long-5/short-5 tiene 10 posizioni su " + f"{len(cens['xs_stable'])}: gia' il sottoinsieme MEDIANO", + "da 11 crolla (sez. 4c), e quello che Deribit quota sta al 9° pctl di quella banda.", + f"dShFULL {np.median(M['out']['XS01-D'][W]['sh']):+.3f} a peso 15% " + f"(negativo a {100-100*(np.array(M['out']['XS01-D'][W]['sh'])>0).mean():.0f}% delle ancore).", + "Nessun capitale lo ripara, e nemmeno 2-3 listing in piu': serve l'universo pieno."), + ("XS01-D14", "DATO", + "3 delle 14 gambe sono quotate da 2-8 GIORNI e una (APT) non ha nemmeno una quota;", + "e `marginal_vs_tp01` la classifica HEDGE, non ADDS.", + "Riapribile fra ~1 anno di quotazione, se il volume arriva. Non e' una soglia di $."), + ("STATARB-RESID", "GATE PRE-REGISTRATO 2026-09-27", + f"eseguibile e netta gia' a $635; dShFULL {np.median(M['out']['STATARB'][W]['sh']):+.3f} " + f"a 15%, ma d drift {100*np.median(M['out']['STATARB'][W]['cagr']):+.2f}pp", + "(tutto rischio in meno, zero ritorno in piu' a nozionale invariato) e", + "`marginal_vs_tp01` = NEUTRAL, robust_oos FALSE. Il gate scade fra 35 giorni."), + ("DVOLSPREAD", "GATE PRE-REGISTRATO 2026-10-24 / 2027-01-24", + f"eseguibile e netta a $635; dShFULL {np.median(M['out']['DVOLSPREAD'][W]['sh']):+.3f}, ma", + f"d drift {100*np.median(M['out']['DVOLSPREAD'][W]['cagr']):+.2f}pp e d maxDD che PEGGIORA " + "sopra il 15%.", + "robust_oos FALSE. Il gate ha una data e non si anticipa."), + ("PREVDAY", "NESSUN GATE — ed e' il problema", + f"e' il piu' forte misurato (dShFULL {np.median(M['out']['PREVDAY'][W]['sh']):+.3f}, " + f"dShHOLD {np.median(M['out']['PREVDAY'][W]['shh']):+.3f}, d drift " + f"{100*np.median(M['out']['PREVDAY'][W]['cagr']):+.2f}pp),", + "eseguibile, netta, ADDS, robust_oos TRUE — e sta in forward-monitor dal giugno", + "senza gate pre-registrato, senza deflated-Sharpe, e con corr al libro 0.150 (la piu' alta)."), + ("VRP01", "REGOLA + PREZZO", + f"il lotto NON e' piu' il muro (ETH ${lot_e:,.0f} = peso 12% da ~${lot_e/0.12:,.0f}),", + "ma restano 'niente short-vol da modello in deploy' e f=0,73 misurato sulle quote vere;", + "e non netta: serve una riga in `_CONTRACT` = codice su un percorso con soldi veri."), + ("GTAA01", "PRIIPs", + "l'ordine e' RIFIUTATO dal broker retail UE. La via UCITS esiste (27/07) ma e' un", + "SECONDO VENUE, quindi cade anch'essa sotto la decisione dei $20.000.", + "Non e' una soglia di capitale: e' un divieto."), + ] + for nm, muro, *righe in muri: + print(f"\n {nm} -> MURO: {muro}") + for r in righe: + print(f" {r}") + + sub("8b) LA SCALA IN DOLLARI — cosa cambia, e quando") + righe = [ + (635, "OGGI", "STATARB · DVOLSPREAD · PREVDAY eseguibili e nettanti (ticket $2,8-4,9,", + "assorbiti nel netto: 7d). XS01-D eseguibile ma PEGGIORA. Nessuno ammissibile."), + (2024, f"1 lotto ETH di VRP01 = peso 12% (${lot_e:,.0f}/0,12)", + "cade il muro del lotto per la gamba ETH. NON cade la regola short-vol.", ""), + (3000, "GTAA_MIN_CAPITAL", + "GTAA01 avrebbe il capitale ma resta PRIIPs-bloccato (verificato sul conto reale).", ""), + (6440, f"1 lotto BTC di VRP01 = peso 12% (${lot_b:,.0f}/0,12)", + "VRP01 tornerebbe 50/50 BTC+ETH come lo sleeve modellato.", ""), + (20000, "DECISIONE DI VENUE (operatore, 26/07)", + "si riapre lo split. E' QUI che XS01 (Hyperliquid, 19 gambe) diventa possibile —", + "per la decisione di venue, NON per una soglia di eseguibilita'."), + ] + for C, evento, *righe2 in righe: + print(f"\n ${C:,}".replace(",", ".") + f" — {evento}") + for r in righe2: + if r: + print(f" {r}") + print("\n 📌 LA RISPOSTA ALLA DOMANDA 'a quale capitale cambia': NON c'e' una soglia di") + print(" capitale che ammette un terzo sleeve. Le due date che contano sono un GATE") + print(" (27/09, fra 35 giorni, gratis) e una DECISIONE DI VENUE ($20k). Il capitale") + print(" entra solo per VRP01, che e' fermo per una regola che il capitale non tocca.") + + +def sezione9(S: dict, M: dict, G: dict, cens: dict) -> None: + sez("9) VERDETTO — l'attesa a priori contro l'esito") + W = 0.15 + print(f" Tabella decidibile, peso {W:.0%}, mediana delle differenze APPAIATE sulla banda") + print(" d'ancora congiunta, funding dentro:\n") + print(f" {'candidato':<12}{'dShFULL':>10}{'>0':>6}{'dShHOLD':>10}{'d drift':>10} " + f"{'eseguibile a $635':<24}{'gate':<12}{'tilt-null'}") + for c in CANDIDATI: + r = M["out"][c][W] + ex = {"XS01": "NO (5 gambe assenti)", "XS01-D": "SI (11 gambe)", + "XS01-D14": "solo 3gg di listino", "STATARB": "SI (netta)", + "DVOLSPREAD": "SI (netta)", "PREVDAY": "SI (netta)", + "VRP01": f"NO (1 lotto=38% conto)"}[c] + gt = {"XS01": "—", "XS01-D": "—", "XS01-D14": "—", "STATARB": "27/09", + "DVOLSPREAD": "24/10", "PREVDAY": "—", "VRP01": "regola"}[c] + gp = "PASS" if G.get((c, W), {}).get("gate_pass") else "FAIL" + print(f" {c:<12}{np.median(r['sh']):>+10.4f}{100*(np.array(r['sh'])>0).mean():>5.0f}%" + f"{np.median(r['shh']):>+10.4f}{100*np.median(r['cagr']):>+8.2f}pp " + f"{ex:<24}{gt:<12}{gp}") + dsa = float(np.median(M["out"]["STATARB"][W]["sh"])) + ddv = float(np.median(M["out"]["DVOLSPREAD"][W]["sh"])) + dxs = float(np.median(M["out"]["XS01"][W]["sh"])) + dxd = float(np.median(M["out"]["XS01-D"][W]["sh"])) + xs_gate = G.get(("XS01", W), {}).get("gate_pass") + print("\n ATTESA A PRIORI vs ESITO (dichiarata in sezione 0, prima di misurare):") + print(" (A1) 'nessuno deployabile oggi, e il muro NON e' il lotto del venue'") + print(" -> ✅ CONFERMATA. Nessun candidato muore sul lotto del venue (i lotti stanno") + print(" fra $0,01 e $7,72). Il muro e' altrove, ed e' diverso per ciascuno.") + print(f" (A2) 'XS01 il piu' forte ma tilt-null FALLIRA'' -> ⚠️ META' GIUSTA: XS01 e' fra i") + print(f" piu' forti (dShFULL {dxs:+.3f}) ma `weights_tilt_null` " + f"{'PASSA' if xs_gate else 'FALLISCE'}. Il gate non e'") + print(" il muro di XS01: il muro e' che 5 delle 19 gambe Deribit non le quota.") + print(f" (A3) '|dSharpe| < 0,10 per STATARB e DVOLSPREAD' -> ❌ REFUTATA su STATARB") + print(f" ({dsa:+.3f} a peso 15%, positivo a 100% delle ancore), confermata su") + print(f" DVOLSPREAD ({ddv:+.3f}). E il segno NON e' incerto: e' stabile sulla banda.") + print(" (A4) 'il primo a entrare sara' XS01 fra $2k e $5k, per ragione di venue'") + print(f" -> ❌ REFUTATA nella sostanza: la versione di XS01 eseguibile su Deribit oggi") + print(f" ({len(cens['xs_stable'])} gambe) fa dShFULL {dxd:+.3f} — PEGGIORA il libro. Non e' una") + print(" soglia di capitale: e' che l'universo eseguibile non regge il meccanismo") + print(" (sez. 4c: la maggior parte e' AMPIEZZA, il resto e' QUALI gambe mancano).") + print(" (A5) 'mi sorprendera' un candidato dato per eseguibile che non lo e''") + print(" -> ✅ CONFERMATA, ma al ROVESCIO di come me l'aspettavo: la sorpresa e' un") + print(" candidato dato per NON eseguibile che lo E' (VRP01: il lotto vero e' 7,5x") + print(" piu' piccolo del pubblicato), e uno dato per eseguibile-a-$20k che a") + print(" $20k resta comunque rotto (XS01: e\' il VENUE, non la taglia).") + print("\n ⚠️ CAVEAT CHE VALE PER TUTTA LA TABELLA — questo e' uno SCREEN, non una scoperta:") + print(" 7 candidati x 4 pesi = 28 configurazioni guardate sullo stesso libro e sullo") + print(" stesso hold-out. Nessuno di questi delta e' passato per un deflated-Sharpe DI") + print(" SCREEN (lezione 22/08 §10: non basta contare i trial, serve la VARIANZA di") + print(" screen), e il candidato che esce primo esce da una selezione che io ho fatto.") + print(" E' esattamente per questo che l'output e' una TABELLA e non una raccomandazione:") + print(" il modo giusto di usarla e' scegliere UN candidato per una ragione dichiarata") + print(" PRIMA, e poi fargli un gate pre-registrato — non prendere il massimo di 28.") + + +# ============================================================================================= +def main() -> None: + global _ARGS + ap = argparse.ArgumentParser() + ap.add_argument("--no-net", action="store_true") + ap.add_argument("--draws", type=int, default=1000) + ap.add_argument("--force", action="store_true") + _ARGS = ap.parse_args() + sys.argv = [sys.argv[0]] + (["--no-net"] if _ARGS.no_net else []) + + sez("r0823_book_3rd.py — §50 BOOK-3RD | quale terzo sleeve per il LIBRO LIVE?") + print(f" avvio {dt.datetime.now(dt.timezone.utc):%Y-%m-%d %H:%M UTC} · " + f"capitale reale assunto ${CAPITAL_TODAY:.0f} · estrazioni {_ARGS.draws}") + sez("0) ATTESA A PRIORI (dichiarata PRIMA di misurare)") + print(ATTESA) + + V = venue_census() + cens = sezione1(V) + sezione1c(V, cens) + sezione2() + print(f"\n {clock()} carico i tassi di funding (cache/rete)...") + rate = get_rate() + R3 = sezione3(rate) + print(f"\n {clock()} costruisco le serie ancorate...") + S = build_series(rate, cens, force=_ARGS.force) + sezione4(S, cens) + sezione4b(S) + sezione4c(cens) + M = sezione5(S, _ARGS.draws) + sezione5b(S) + G = sezione6(S) + sezione7(S, V, cens) + sezione7d(rate) + sezione8(S, M, G, V, cens) + sezione9(S, M, G, cens) + sez(f"FINE {clock()}") + + +if __name__ == "__main__": + main() From ab497cc028b679ef78c9c6071074e7a6546ed25d Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 02:17:55 +0000 Subject: [PATCH 091/114] =?UTF-8?q?registro=20=C2=A750:=20BOOK-3RD=20?= =?UTF-8?q?=E2=80=94=20nessun=20terzo=20sleeve=20ammissibile,=20e=20PREVDA?= =?UTF-8?q?Y=20e'=20in=20monitor=20da=20giugno=20senza=20gate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 89 ++++++++++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 2438d2a..8cca061 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -715,7 +715,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 47 | ANCHOR-ENSEMBLE | eseguire l'ensemble delle 24 ancore invece di sceglierne una e correggere il numero | **ESEGUIBILE MA INUTILE SU TP01** — il drift **non puo'** cambiare (algebra: `lordo_ens == media(lordi)`, 1,4e-17); **il segnale sta su SKH01** (+0,312 di Sharpe mediano, maxDD 23,3%→16,6%), ma dentro il libro **non si distingue da zero**. E la premessa del 02/07 era sbagliata | | 48 | VOLVOL | la volatilita' del DVOL (secondo momento, mai usato) contiene cio' che livello e spread non contengono? | **SCARTATO** — e' davvero una variabile NUOVA (ortogonalita' PASS, max \|corr\| 0,396) ma **guarda INDIETRO**: picco a lag **−5/−10** e parziale con la vol futura **NEGATIVA**. Quinto lato del filone DVOL chiuso | | 49 | TP01-TWIN | il 75% del libro dipende da UNA definizione di trend: un ensemble di meccanismi protegge meglio? | **REFUTED** — protegge **PEGGIO** nei 4 anni piu' recenti a **0/24 ancore** (2022: **3,2x peggio**) e costa **−0,127** di Sharpe hold-out al libro. E i meccanismi **non erano ridondanti**: corr di posizione 0,61 | -| 50 | BOOK-3RD | qual e' il miglior TERZO sleeve realmente eseguibile sul libro live a $635 — e a quale capitale cambia? | _in corso_ | +| 50 | BOOK-3RD | qual e' il miglior TERZO sleeve realmente eseguibile sul libro live a $635 — e a quale capitale cambia? | **NESSUNO AMMISSIBILE OGGI** — sette candidati, **sette muri diversi, e nessuno dei sette e' una soglia di capitale**. 🚨 Il piu' forte misurato (**PREVDAY**) sta in monitor da giugno **senza gate e senza deflated-Sharpe** | | 51 | NEWDATA-SCOUT | se la scorta e' esaurita: cosa iniziare a raccogliere OGGI, che fra 12-24 mesi apra una famiglia? | **RACCOGLIERE 2 FONTI SU 10** — catena USDC (**+18%, non +90%**) e una **campagna a termine** di book depth; il criterio di ricostruibilita' ne uccide 4 da solo e **refuta una raccomandazione del 22/08** | | 52 | SKEPTIC-SPOT | il lead dello spot (+1,55-1,66%/anno) sopravvive a un attacco deliberato? | _in corso_ | | 53 | SKEPTIC-LEVA | il gradino di leva 1,25x sopravvive a un attacco deliberato, col funding dentro? | **REGGE 5/6, INCRINA 1/6** — il **1,25x sopravvive**, il **1,50x NO**; k massimo difendibile **1,40**. L'incrinatura e' il ramo `eq_fallback`, dove **la scala non apre il buco: lo MOLTIPLICA** | @@ -3069,3 +3069,90 @@ gia' dentro); (d) coda di venue — vive sul suo asse separato. **VERDETTO: `IL GRADINO 1,25x SOPRAVVIVE A CINQUE ATTACCHI SU SEI; IL 1,50x NO; k MASSIMO DIFENDIBILE 1,40`** — e l'unica incrinatura non e' nel gradino ma nella **guardia che lo accompagnerebbe**. + +--- + +## 50 — BOOK-3RD (il terzo sleeve del libro LIVE) + +`scripts/research/r0823_book_3rd.py`. **Nessun file di produzione toccato, nessun ordine.** +Distingue i due oggetti che il progetto confonde di continuo: il **book di RICERCA a 5 sleeve** (su +cui sono calcolati quasi tutti i numeri pubblicati) e il **LIBRO LIVE a 2** (TP01 75 / SKH01 25), +che **non ha mai avuto un terzo sleeve**. + +### Lo stato REALE dei candidati (venue letto ORA, non memoria) + +| candidato | su Deribit? | capitale/lotto minimo | **netta col libro?** | gate | +|---|---|---|---|---| +| STATARB-RESID | SI' | $5 (soglia **nostra**) | **SI'** | 27/09 aperto | +| DVOLSPREAD | SI' | $5 (soglia nostra) | **SI'** | 24/10 kill · 24/01 dec. | +| **PREVDAY** | SI' | $5 (soglia nostra) | **SI'** | 🚨 **nessuno, mai registrato** | +| XS01 (19 gambe) | **NO — 5 mancano** | 14/19 quotate, 11 da ≥1 anno | parziale | — | +| XS01-D (11 gambe) | SI' (ridotta) | $0,09-7,72 dal venue | parziale | — | +| XSR01 | **NO** — venue diverso | n/d | NO | 23/10 aperto | +| VRP01 | SI' (ETH) | **lotto ETH $242 · BTC $772** | NO | regola short-vol | +| GTAA01 | **NO** | PRIIPs: ordine RIFIUTATO | NO | — | + +📌 **Il vincolo che nessuno aveva messo in tabella, letto dal sorgente:** `book_net_target` somma +i due sleeve in **UN** numero per asset e `build_book_order` manda **UN** ordine ⇒ **un terzo sleeve +direzionale su BTC/ETH non paga un min-order proprio** (misurato: 163 → 344 ordini/anno a $635, ma +il **turnover sale solo del 14%**) — e per disuguaglianza triangolare `|Δ(Σw·p)| ≤ Σw·|Δp|`, cioe' +**le fee modellate per-sleeve sono un limite SUPERIORE**. Il rovescio: **non puo' avere un proprio +stop**, e uno su strumenti diversi richiede una riga in `_CONTRACT` = codice su un percorso con +soldi veri. + +### La misura (1000 estrazioni congiunte, differenze APPAIATE, funding dentro, peso 15%) + +✅ Replica di controllo prima di ogni delta: **1,813 / 1,437 / 9,42%** riprodotti; replica ancorata +**bit-exact (max|dif| = 0,0)** contro lo sleeve di produzione, idem XS01/VRP01/STATARB. + +| candidato | dShFULL | >0 | dShHOLD | d drift | `weights_tilt_null` | +|---|---|---|---|---|---| +| **PREVDAY** | **+0,192** | 100% | **+0,363** | **+1,31pp** | PASS | +| STATARB | +0,159 | 100% | +0,081 | −0,12pp | PASS | +| VRP01 | +0,121 | 100% | +0,067 | +0,19pp | PASS | +| XS01 (19) | +0,110 | 100% | +0,378 | +0,79pp | PASS | +| DVOLSPREAD | +0,073 | 100% | +0,062 | −1,12pp | PASS | +| XS01-D (11) | **−0,024** | 7% | −0,128 | −0,73pp | **FAIL** | + +⚠️ `weights_tilt_null` passa **25/28**, ma il numero da leggere e' `frac_random_beat_hold`, che +arriva a **0,94**: dove vale cosi', *"questo candidato migliora l'hold-out"* e' un claim **generico**. +Il gate **ha potenza** (fallisce su XS01-D), resta **necessario e non sufficiente**. + +### Due fatti di venue che contraddicono la memoria + +🚨 **(1) Il muro del lotto di VRP01 era misurato sulla famiglia SBAGLIATA** — inverse, che un conto +USDC **non puo' marginare**. Sulla **USDC-lineare** il lotto ETH e' **$242** (non $1.832) e il BTC +**$772** (non $6.210) ⇒ 1 lotto ETH = peso 12% gia' da **~$2.000**. **Cade il lotto, non la regola** +*"niente short-vol da modello in deploy"*. **5ª occorrenza dello schema `fee_watch`** (un controllo +puntato su una configurazione diversa da quella che gira). +🚨 **(2) XS01 non e' bloccato dal capitale, e' bloccato dal VENUE.** Deribit quota **14 dei 19** +major, ma **3 sono listati fra il 15 e il 21 agosto** e APT non ha nemmeno una quota. Sulle **11 con +≥1 anno** il meccanismo **collassa: Sharpe 1,265 → 0,116**. E il null dei sottoinsiemi **separa le +cause**: sottoinsieme **mediano** da 11 = 0,548 ⇒ **il 62% della caduta e' AMPIEZZA, che il capitale +non compra**; quello di Deribit sta al **9° percentile** ⇒ il resto e' **quali** gambe mancano. +**Aspettare 2-3 listing non basta.** + +### La scala — e non esiste + +📌 **Non c'e' una soglia di capitale che ammette un terzo sleeve.** Il capitale sposta **solo +VRP01** (~$2.000 gamba ETH), che e' fermo **per una regola**. Le due date che contano sono un +**gate** (27/09, costa zero) e la **decisione di venue ($20k)**, che e' cio' che riapre XS01 sulle +19 gambe. + +⚠️ **Caveat pubblicato dall'agente in testa al verdetto:** sono **7 candidati × 4 pesi = 28 +configurazioni sullo stesso hold-out**, nessuna passata per un deflated-Sharpe **di screen** → *il +modo giusto di usare la tabella e' scegliere un candidato per una ragione dichiarata PRIMA, non +prendere il massimo di 28.* + +🚨 **IL BUCO PIU' EVIDENTE, ed e' un fatto di processo: PREVDAY.** E' **il piu' forte misurato** +(+0,192 FULL / +0,363 HOLD, 100% delle estrazioni), **eseguibile**, **netta col libro**, **ADDS**, +`robust_oos` **TRUE**, forward Sharpe **+2,04 su 63 giorni** — l'unico monitor risultato **sano** +dopo l'audit §32 — **e sta in forward-monitor da giugno senza gate pre-registrato e senza +deflated-Sharpe.** Ogni altro lead del progetto ne ha uno. **E' una decisione dell'operatore.** + +**Attesa a priori dichiarata prima di misurare: A1 confermata, A2 meta' giusta, A3 e A4 REFUTATE** +(STATARB da' +0,159 non <0,10; XS01 non entra a $2-5k **ed e' rotto anche a $20k, per venue**), +**A5 confermata al rovescio** — *la sorpresa e' un candidato dato per NON eseguibile che lo e'*. + +**VERDETTO: `SETTE CANDIDATI, SETTE MURI DIVERSI, E NESSUNO DEI SETTE E' UNA SOGLIA DI CAPITALE`** — +nessuno ammissibile oggi. **Libro, pesi, cron, config INVARIATI.** From 892ca6e21409523f26a4a38b9b7ab63a5b2b99d3 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 02:18:08 +0000 Subject: [PATCH 092/114] =?UTF-8?q?CLAUDE.md:=20il=20lotto=20minimo=20di?= =?UTF-8?q?=20VRP01=20era=20misurato=20sugli=20inverse=20=E2=80=94=20sui?= =?UTF-8?q?=20lineari=20USDC=20e'=207,5x=20piu'=20piccolo,=20cade=20il=20l?= =?UTF-8?q?otto=20non=20la=20regola?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3f75888..5bdf1d2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -260,7 +260,13 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis (c) una soglia sull'ampiezza di un IC richiede di **verificare la copertura** di quell'IC; (d) *"non abbastanza campione" non e' "nessuna differenza"* — si aspetta con una data. 💰 **A $3.000 la domanda non e' il rendimento** (`min_trade_amount` letti dal venue il 30/07): - **BTC min 0.1 = $6.210 di collaterale/lotto → FUORI**; ETH min 1 contratto = $1.832 → **1 lotto**. + ~~**BTC min 0.1 = $6.210 di collaterale/lotto → FUORI**; ETH min 1 contratto = $1.832 → **1 lotto**.~~ + 🚨 **CORRETTO 2026-08-23 (§50): misurato sulla famiglia INVERSE, che un conto USDC non puo' + marginare.** Sulla **USDC-lineare** il lotto ETH e' **$242** e il BTC **$772** (7,5x piu' piccoli) + ⇒ 1 lotto ETH copre il peso di book 12% gia' da **~$2.000**, e il BTC da ~$6.440. + **CADE IL LOTTO, NON LA REGOLA:** *niente short-vol da modello in deploy* non ha mai avuto + bisogno di un muro di taglia. **5ª occorrenza dello schema `fee_watch`** — un numero misurato su + una configurazione diversa da quella che gira. Il sleeve 50/50 **diventa ETH-only**, e **al peso di book (12% = $360) sono 0 lotti** — servirebbe il **61% del conto** su un solo sleeve. **REGOLE:** (a) un'uscita si giudica su un confronto **appaiato per INGRESSO**, mai allineando le serie sulla data di USCITA — e' proprio quella che From f5ebe689dae868c1c94eeddf12094dce17336b81 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 02:19:33 +0000 Subject: [PATCH 093/114] skeptic(spot): il lead REGGE a $600 e si ANNULLA alla taglia del muro che sposta MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §52 SKEPTIC-SPOT — attacco deliberato a §39/§45 (spot al posto del perpetual per la gamba TP01). Sei attacchi, replica bit-exact prima di ognuno (max|dif| = 0.0 su tp01_realistic; +2,106% di sleeve e +1,580% di libro protetti da assert). Esiti: 1 allineamento REGGE · 2 controfattuale INCRINA · 3 tetto 1,0x REGGE · 4 spread/profondita' INCRINA · 5 haircut REGGE · 6 disaster-SL REGGE. Il risultato che cambia una conclusione e non una presentazione: il differenziale di cammino spot-vs-perp e' +0,72 bps a $600 (-0,04%/anno) e +25,66 bps a $272k (-1,54%/anno = il 97% del lead lordo). Il +1,55% e' misurato a una taglia a cui l'esecuzione e' gratis e speso dentro un muro da $290k, dove non lo e': il muro NON si sposta del 10,9% pubblicato. Trovato invece che la storia dello spot ESISTE (get_tradingview_chart_data serve BTC_USDC/ETH_USDC dal 2023-04-24, 29.196 barre orarie, 0 gap): §39 la dichiarava assente. TP01 girato sul prezzo spot VERO contro il perp, 24 ancore appaiate, da' +0,166%/anno di sleeve con la banda che contiene lo zero (16/24) -> il controfattuale sul PREZZO e' innocuo. Ma lo strumento non esisteva per il 55,3% del campione e nel 2023 aveva ~9% di ore senza scambi: il lordo passa da +1,580% (7,4 anni) a +1,450% (era spot) a +1,180% (era liquida, 2024+). Attacchi falliti, e vanno detti: il tetto 1,0x non morde (3 giorni su BTC, tutti nel 2018-11 e in PERDITA: troncare avrebbe fatto guadagnare); nessun haircut <=100% rende binding il margine (copertura 50,0x replicata al decimo); il disaster-SL tolto a TP01 vale 0,15%/anno ammortizzato allo scenario operativo. Tre errori miei, catturati e dichiarati nello script: (a) confrontavo il costo di SPREAD dello spot con la banda NETTA di §45 — mele contro pere, nel verso che mi conveniva: like-with-like le due bande si sovrappongono; (b) l'aritmetica del margine metteva SKH01 a 1,0x su ciascun asset invece che sul proprio sleeve (25,0x invece di 50,0x: §45 aveva ragione); (c) ammortizzavo su 7,4 anni la coda di un blackout da 30 giorni preso come argmax, ottenendo 1,97%/anno = il 127% del lead da un evento mai accaduto. Squalificato dal proprio controllo positivo: gli stimatori di spread da OHLC (Roll, Corwin-Schultz) attribuiscono 4-16 bps al perpetual, che e' a un tick -> numeri cancellati, non riportati. Correzione a §45: «lo spot non si liquida» e' falso — BTC/ETH hanno in_cross_collateral_pool: true, quindi sono collaterale liquidabile (non binding). Numero onesto rivisto: [+1,38%, +1,58%]/anno a $600, -0,11%/anno a $272k. Sola lettura provata (git status -- src/ config/ scripts/live/ tests/ data/ vuoto), 0 ordini, solo GET pubbliche con pacing e astensione nei minuti :05-:10 e :24-:30. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf --- scripts/research/r0823_skeptic_spot.py | 943 +++++++++++++++++++++++++ 1 file changed, 943 insertions(+) create mode 100644 scripts/research/r0823_skeptic_spot.py diff --git a/scripts/research/r0823_skeptic_spot.py b/scripts/research/r0823_skeptic_spot.py new file mode 100644 index 0000000..6c012cb --- /dev/null +++ b/scripts/research/r0823_skeptic_spot.py @@ -0,0 +1,943 @@ +#!/usr/bin/env python3 +"""r0823_skeptic_spot.py — §52 SKEPTIC-SPOT: attacco deliberato al lead dello SPOT. + +BERSAGLIO +--------- +§39 (`r0822e_funding_avoid`) e §45 (`r0823_spot_netting`) concludono che esprimere la gamba +TP01 sullo SPOT Deribit (`BTC_USDC`/`ETH_USDC`) invece che sul perpetual vale +**+1,55%/anno di drift di libro**, muro $325,0k -> $289,6k (-10,9%), netto [+1,55%, +1,66%]. + +Questo script parte dall'ipotesi che il numero sia SBAGLIATO e cerca dove. Sei attacchi, +in ordine di priorita' dichiarato. Ogni attacco chiude con REGGE / INCRINA / FALSIFICA. + +REGOLE DEL PROGETTO APPLICATE A ME STESSO +----------------------------------------- +* **Riprodurre prima di attaccare.** Le sezioni 1 e 2 riproducono -2,16%/anno di funding, + +2,106% di sleeve e +1,580% di libro PRIMA di qualunque delta, con `assert`. +* **Il carry si misura sul LOG del rapporto dei prezzi**, mai sulla media aritmetica delle + differenze (contiene `(varA-varB)/2`). +* **Ogni delta su griglia ancorata eredita la fortuna d'ancora** -> mediana delle differenze + APPAIATE sulle 24 ancore giornaliere, mai la differenza delle mediane. +* **Epoca in ms ESPLICITA** (mai `.view`/`.astype("int64")` su DatetimeIndex tz-aware). +* Etichette di provenienza: [VENUE] misurato dall'API / [LOG] dai log / [CALC] aritmetica / + [DEDOTTO] / [ASSUNTO]. +* **Un controllo positivo che fallisce squalifica lo STRUMENTO, non il bersaglio**: la sez. 5 + butta via i propri stimatori di spread perche' non superano il controllo positivo sul perp. + +SOLA LETTURA / RETE +------------------- +Nessuna scrittura nel repo. Nessun ordine, mai (solo GET **pubbliche**, nessuna chiave). +Cache dello storico spot FUORI dal repo. Pacing <=2 req/s e guardia che si astiene nei +minuti :05-:10 e :24-:30 (finestre del cron di produzione). + + uv run python scripts/research/r0823_skeptic_spot.py # con rete + uv run python scripts/research/r0823_skeptic_spot.py --no-net # solo cache +""" +from __future__ import annotations + +import contextlib +import datetime as dt +import io +import json +import sys +import time +import urllib.request +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +for _p in (ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt", ROOT): + sys.path.insert(0, str(_p)) + +# r0822d_funding legge `--no-net` da sys.argv all'import: qui il funding viene dalla sua +# cache. Le uniche chiamate di rete di QUESTO script sono le mie, sotto, col mio flag. +_NET = "--no-net" not in sys.argv +if "--no-net" not in sys.argv: + sys.argv.append("--no-net") + +import altlib as al # noqa: E402 +import r0702_tp01_offset as r02t # noqa: E402 (resample_offset: le 24 ancore) +import r0725_capcurve as CC # noqa: E402 (DERIBIT_FEE_SIDE/MIN_ORDER) +import r0822d_funding as FD # noqa: E402 (build_rate/daily_funding/stat) +from src.data.downloader import load_data # noqa: E402 +from src.strategies.trend_portfolio import ( # noqa: E402 + CANONICAL, TrendPortfolio, resample_1d, simple_returns) + +# --------------------------------------------------------------------------- +# Costanti dichiarate +# --------------------------------------------------------------------------- +ASSETS = ("BTC", "ETH") +ALLOC = 600.0 # il capitale su cui girano §39/§45 +W_TP, W_SKH = 0.75, 0.25 +EPOCH = pd.Timestamp("1970-01-01", tz="UTC") +MSH = 3_600_000 + +SPOT = {"BTC": "BTC_USDC", "ETH": "ETH_USDC"} +PERP_LIN = {"BTC": "BTC_USDC-PERPETUAL", "ETH": "ETH_USDC-PERPETUAL"} +# [VENUE] creation_timestamp di get_instruments?currency=USDC&kind=spot (letto in sessione) +SPOT_BIRTH = pd.Timestamp("2023-04-24 13:00", tz="UTC") + +API = "https://www.deribit.com/api/v2/public/" +_SP = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad") +CACHE = _SP if _SP.parent.exists() else Path("/tmp/pythagoras_skeptic_spot") + +# §45, da citare e attaccare +S45_HALF_SPREAD_BPS = (1.5, 3.1) # mezzo spread misurato su DUE istantanee a 4 minuti +S45_ESEC_BAND = (-0.034, +0.077) # contributo netto d'esecuzione, %/anno +S45_NETTO = (1.55, 1.66) # il numero da citare, %/anno di libro +S45_MARGIN_COVER = 50.0 # copertura del margine di SKH01 +CAP_TETTO_PUBBL = 0.102 # % di giorni in cui TP01 supera 1,0x (§39) + +_last = [0.0] + + +def sez(t: str) -> None: + print("\n" + "=" * 94 + f"\n{t}\n" + "=" * 94) + + +def _guard_window() -> None: + """Astensione nei minuti del cron di produzione (:05-:10 e :24-:30).""" + while True: + m = dt.datetime.now(dt.timezone.utc).minute + if not (5 <= m <= 10 or 24 <= m <= 30): + return + tgt = 11 if m <= 10 else 31 + w = (tgt - m) * 60 - dt.datetime.now(dt.timezone.utc).second + print(f" [rete] minuto :{m:02d} e' finestra del cron -> attendo {max(w,1)}s") + time.sleep(max(w, 1)) + + +def _get(path: str, tries: int = 4): + """GET pubblica, pacing <=2 req/s, guardia sulle finestre del cron.""" + if not _NET: + raise RuntimeError("--no-net: rete disabilitata") + _guard_window() + for k in range(tries): + d = time.time() - _last[0] + if d < 0.55: + time.sleep(0.55 - d) + _last[0] = time.time() + try: + with urllib.request.urlopen(API + path, timeout=30) as r: + return json.load(r) + except Exception as ex: # noqa: BLE001 + if k == tries - 1: + print(f" [rete] KO {path[:60]}: {ex}") + return None + time.sleep(1.5 * (k + 1)) + return None + + +def to_ms(idx: pd.DatetimeIndex) -> np.ndarray: + """Epoca in ms ESPLICITA. Mai `.view`/`.astype('int64')` su tz-aware.""" + return ((pd.DatetimeIndex(idx) - EPOCH) // pd.Timedelta(milliseconds=1)).values.astype(np.int64) + + +# =========================================================================== +# 0) LO SPOT: esistenza, nascita, storia +# =========================================================================== +def spot_1h(a: str) -> pd.DataFrame: + """Storia oraria dello SPOT Deribit, cache fuori dal repo. Schema `al.get`-compatibile.""" + f = CACHE / f"spot_{a.lower()}_1h.parquet" + have = pd.read_parquet(f) if f.exists() else pd.DataFrame() + if len(have) and (not _NET or + pd.Timestamp(have["ts"].max()) > pd.Timestamp.now(tz="UTC") - pd.Timedelta("6h")): + d = have.copy() + else: + out = ([have] if len(have) else []) + t = int(SPOT_BIRTH.timestamp() * 1000) if not len(have) else int( + pd.Timestamp(have["ts"].max()).timestamp() * 1000) + MSH + end = int(pd.Timestamp.now(tz="UTC").floor("h").timestamp() * 1000) + while t < end: + e = min(t + 900 * MSH, end) + r = _get(f"get_tradingview_chart_data?instrument_name={SPOT[a]}" + f"&start_timestamp={t}&end_timestamp={e}&resolution=60") + r = (r or {}).get("result", {}) + if r.get("status") != "ok" or not r.get("ticks"): + break + out.append(pd.DataFrame(dict( + ts=pd.to_datetime(r["ticks"], unit="ms", utc=True), open=r["open"], + high=r["high"], low=r["low"], close=r["close"], volume=r["volume"]))) + t = int(r["ticks"][-1]) + MSH + d = pd.concat(out).drop_duplicates("ts").sort_values("ts").reset_index(drop=True) + CACHE.mkdir(parents=True, exist_ok=True) + d.to_parquet(f) + d = d.rename(columns={"ts": "datetime"}) + d["datetime"] = pd.DatetimeIndex(pd.to_datetime(d["datetime"], utc=True)) + d["timestamp"] = to_ms(d["datetime"]) + return d[["timestamp", "open", "high", "low", "close", "volume", "datetime"]] + + +def perp_1h(a: str) -> pd.DataFrame: + d = al.get(a, "1h").copy() + d["datetime"] = pd.DatetimeIndex(pd.to_datetime(d["datetime"], utc=True)) + return d + + +# =========================================================================== +# 1) REPLICA — riprodurre i numeri PRIMA di attaccarli +# =========================================================================== +def tp01_leg(df1h: pd.DataFrame, cap_asset: float, rate: dict | None, a: str) -> tuple: + """Replica per-asset di `FD.tp01_realistic`, con il prezzo come grado di liberta'. + Con (feed perp, rate) deve dare bit-exact `FD.tp01_realistic`: e' la replica di controllo.""" + tp = TrendPortfolio(**CANONICAL) + df = resample_1d(df1h) + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + r = simple_returns(df["close"].values.astype(float)) + tgt = np.nan_to_num(np.asarray(tp.target_series(df), float)) + held = np.empty(len(tgt)) + cur = 0.0 + for i in range(len(tgt)): + if abs(tgt[i] - cur) * cap_asset >= CC.DERIBIT_MIN_ORDER: + cur = tgt[i] + held[i] = cur + pos = np.zeros(len(held)) + pos[1:] = held[:-1] + net = pos * r - CC.DERIBIT_FEE_SIDE * np.abs(np.diff(pos, prepend=0.0)) + if rate is not None: + net = net - pos * FD.daily_funding(rate[a], idx) + net[0] = 0.0 + return pd.Series(np.clip(net, -0.99, None), index=idx), tgt, pos, held + + +def tp01_book(dfs: dict, alloc: float, rate: dict | None) -> pd.Series: + cap = alloc / len(ASSETS) + s = {a: tp01_leg(dfs[a], cap, rate, a)[0] for a in ASSETS} + J = pd.concat(s, axis=1, join="inner").fillna(0.0) + out = pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index) + if out.index.tz is None: + out.index = out.index.tz_localize("UTC") + return out + + +def sezione1() -> dict: + sez("1) REPLICA DI CONTROLLO — i numeri attaccati, riprodotti prima di toccarli") + with contextlib.redirect_stdout(io.StringIO()): + F = FD.sezione0()["F"] + rate = FD.build_rate(F) + + ref, dg = FD.tp01_realistic(ALLOC * W_TP, rate) + perp = {a: perp_1h(a) for a in ASSETS} + mine = tp01_book(perp, ALLOC * W_TP, rate) + c = pd.concat([ref, mine], axis=1, join="inner") + dmax = float(np.max(np.abs(c.iloc[:, 0] - c.iloc[:, 1]))) + print(f" [CALC] la mia replica di TP01 contro `FD.tp01_realistic`: max|dif| = {dmax:.1e}") + assert dmax == 0.0, "replica non bit-exact: nessun attacco oltre questo punto" + + nc = FD.tp01_realistic(ALLOC * W_TP, None)[0] + a0, a1 = FD.stat(nc), FD.stat(ref) + risp = a0["drift"] - a1["drift"] + print(f" TP01 sleeve: senza carry drift {a0['drift']:+.3%} / con perp {a1['drift']:+.3%} " + f"-> risparmio {risp:+.3%} (§45 pubblica +2,11%)") + print(f" a peso di libro x{W_TP}: {risp*W_TP:+.3%} (§39/§45 pubblicano +1,55/+1,58%)") + assert abs(risp - 0.02106) < 3e-4, "il +2,11% di sleeve non replica" + assert abs(risp * W_TP - 0.0158) < 3e-4, "il +1,58% di libro non replica" + print(f" vol dello sleeve: {a0['vol']:.2%} senza carry contro {a1['vol']:.2%} col perp " + f"-> il carry e' un DRIFT, non rischio (gate iso-vol di §39)") + print(" ✅ REPLICATO. Da qui in poi ogni numero e' un delta su questa base.") + return dict(rate=rate, perp=perp, nc=nc, perp_s=ref, risp=risp, dg=dg) + + +# =========================================================================== +# 2) ATTACCO 1 — il funding e lo spot sono la stessa quantita'? il LAG +# =========================================================================== +def attacco1(rep: dict) -> str: + sez("ATTACCO 1 — «funding misurato su una serie, spot su un'altra»: il LAG e l'oggetto") + print(" §39 ha catturato in sessione uno sfasamento di UN'ORA fra `fund_*.parquet`") + print(" (indice etichettato all'ISTANTE T) e `al.get(a,'1h')` (barra etichettata") + print(" all'INIZIO). Riparato con `aligned_index(...).shift(-1)`. Domanda: la stessa") + print(" famiglia e' altrove? E le due serie parlano davvero dello stesso oggetto?\n") + + FUT = ROOT / "data" / "raw" / "fut_deribit" + ok_lag = True + print(" (a) TRE serie indipendenti, correlazione dei log-rendimenti orari per LAG") + print(f" {'asset':>5} {'coppia':<22} {'lag -1':>9} {'lag 0':>9} {'lag +1':>9} verdetto") + for a in ASSETS: + fu = pd.read_parquet(FUT / f"fund_{a}.parquet").set_index("ts").sort_index() + pe = perp_1h(a).set_index("datetime")["close"].astype(float) + sp = spot_1h(a).set_index("datetime")["close"].astype(float) if True else None + g = pe.index.intersection(fu.index).intersection(sp.index) + ix = fu["index"].reindex(g).values.astype(float) + pv = pe.reindex(g).values.astype(float) + sv = sp.reindex(g).values.astype(float) + + def corr_lag(x, y, k): + rx, ry = np.diff(np.log(x)), np.diff(np.log(y)) + if k > 0: + rx, ry = rx[:-k], ry[k:] + elif k < 0: + rx, ry = rx[-k:], ry[:k] + m = np.isfinite(rx) & np.isfinite(ry) + return float(np.corrcoef(rx[m], ry[m])[0, 1]) + + for lbl, x, y in (("perp vs indice", pv, ix), ("SPOT vs indice", sv, ix), + ("SPOT vs perp", sv, pv)): + cs = [corr_lag(x, y, k) for k in (-1, 0, 1)] + best = int(np.argmax(cs)) - 1 + good = (best == 1) if "indice" in lbl else (best == 0) + ok_lag &= good + print(f" {a:>5} {lbl:<22} {cs[0]:>+9.4f} {cs[1]:>+9.4f} {cs[2]:>+9.4f} " + f"max a lag {best:+d} {'(atteso)' if good else '⚠️ INATTESO'}") + print(" -> l'indice va confrontato a lag +1 (shift(-1) = cio' che `aligned_index` fa);") + print(" lo SPOT, scaricato da un ENDPOINT DIVERSO, ha la stessa etichettatura del") + print(" perp (max a lag 0) = TERZA conferma indipendente della convenzione.") + + print("\n (b) LA DOMANDA VERA: da dove viene il +1,55%? Non dall'indice.") + grid = {} + for a in ASSETS: + fu = pd.read_parquet(FUT / f"fund_{a}.parquet").set_index("ts").sort_index() + pe = perp_1h(a).set_index("datetime")["close"].astype(float) + g = pe.index.intersection(fu.index) + ix = fu["index"].reindex(g).shift(-1).values.astype(float) + pv = pe.reindex(g).values.astype(float) + m = np.isfinite(ix) & np.isfinite(pv) + rlog = np.diff(np.log(pv[m])) - np.diff(np.log(ix[m])) + rari = np.diff(pv[m]) / pv[m][:-1] - np.diff(ix[m]) / ix[m][:-1] + fund = float(np.mean(rep["rate"][a]["i1h"])) * 24 * 365.25 + grid[a] = (float(np.mean(rlog)) * 24 * 365.25, float(np.mean(rari)) * 24 * 365.25, fund) + print(f" {a}: `spot_rate` (perp-indice) LOG {grid[a][0]:+.3%}/anno " + f"ARITMETICO {grid[a][1]:+.3%}/anno (artefatto {grid[a][1]-grid[a][0]:+.3%})") + print(f" funding orario medio, annualizzato: {fund:+.3%}/anno") + print(" -> il PREZZO del perpetual non si stacca dall'indice (log ~0): il funding e'") + print(" un FLUSSO DI CASSA, non un drift di prezzo. Quindi `spot_rate` vale ~0 e") + print(" la variante E) di §39 e' quasi identica alla F) «nessun carry».") + e_min_f = float(np.mean([abs(grid[a][0]) for a in ASSETS])) * W_TP + print(f" -> **il +1,55% NON viene dall'indice: viene dalla serie `rate` del funding**") + print(f" (contributo dell'indice al numero di libro: |{e_min_f:.3%}|/anno).") + print(" E quella serie e' gia' validata contro il conto vero in §35 (ETH -0,2% /") + print(" BTC -7,7% contro `realized_pnl` di sessione). Il vettore d'attacco 1") + print(" punta a un pezzo che pesa quasi nulla.") + + v = "REGGE" if ok_lag else "INCRINA" + print(f"\n ESITO ATTACCO 1: **{v}** — nessuno sfasamento residuo su 6 coppie/2 asset, e la") + print(" quantita' attaccata (l'indice) contribuisce <0,01%/anno al numero pubblicato.") + return v + + +# =========================================================================== +# 3) ATTACCO 2 — controfattuale o misura? lo SPOT VERO +# =========================================================================== +def attacco2(rep: dict) -> tuple[str, dict]: + sez("ATTACCO 2 — «lo spot e' un mercato a se'»: quanto del lead e' MISURATO e quanto MODELLATO") + print(" §39 dichiara in un commento: «qui lo spot e' modellato con l'INDICE Deribit, non") + print(" col libro spot vero (che non ha storia in questo progetto)». Verifico se e' vero") + print(" che la storia non c'e'.\n") + + print(" (a) [VENUE] `get_instruments?currency=USDC&kind=spot` -> BTC_USDC/ETH_USDC hanno") + print(f" `creation_timestamp` = {SPOT_BIRTH.date()}. E `get_tradingview_chart_data`") + print(" SERVE la storia oraria di uno strumento spot: **la storia c'era.**") + sp = {a: spot_1h(a) for a in ASSETS} + for a in ASSETS: + d = sp[a] + gridfull = pd.date_range(d["datetime"].min(), d["datetime"].max(), freq="1h") + print(f" {a}: {len(d):,} barre orarie {d['datetime'].min().date()} .. " + f"{d['datetime'].max().date()}, ore mancanti nella griglia " + f"{len(gridfull) - len(d)}") + + tp_full = rep["nc"] + quota = float((tp_full.index >= SPOT_BIRTH).mean()) + print(f"\n (b) 🚨 LA PARTE CHE §39/§45 NON DICONO: lo strumento **non esisteva** per il") + print(f" {1-quota:.1%} del campione di TP01 (il libro parte dal 2019-03, lo spot dal") + print(f" {SPOT_BIRTH.date()}). Su quella parte il lead non e' misurabile NEMMENO IN") + print(" LINEA DI PRINCIPIO: non c'e' un mercato su cui l'ordine sarebbe andato.") + + a0 = FD.stat(rep["nc"]) + a1 = FD.stat(rep["perp_s"]) + e0 = FD.stat(rep["nc"][rep["nc"].index >= SPOT_BIRTH]) + e1 = FD.stat(rep["perp_s"][rep["perp_s"].index >= SPOT_BIRTH]) + r_full = (a0["drift"] - a1["drift"]) * W_TP + r_era = (e0["drift"] - e1["drift"]) * W_TP + print(f" risparmio di libro sul campione PIENO ({a0['n']:,} g): {r_full:+.3%}/anno") + print(f" risparmio di libro sull'ERA SPOT ({e0['n']:,} g): {r_era:+.3%}/anno " + f"({r_era/r_full-1:+.1%} contro il pubblicato)") + LIQ = pd.Timestamp("2024-01-01", tz="UTC") + l0 = FD.stat(rep["nc"][rep["nc"].index >= LIQ]) + l1 = FD.stat(rep["perp_s"][rep["perp_s"].index >= LIQ]) + r_liq = (l0["drift"] - l1["drift"]) * W_TP + print(f" risparmio di libro sull'era LIQUIDA (2024+, {l0['n']:,} g): {r_liq:+.3%}/anno " + f"({r_liq/r_full-1:+.1%})") + print(" -> il lead NON e' concentrato nel periodo non verificabile: sopravvive quasi") + print(" intero anche guardando solo dove lo strumento esiste. **Questo assolve**") + print(" §39 sull'accusa piu' grave che gli si poteva muovere.") + + print("\n (c) IL TEST CHE §39 NON HA POTUTO FARE: TP01 girato sul prezzo SPOT VERO") + print(" (segnale E rendimenti dallo spot) contro TP01 sul perp, stessa finestra,") + print(" **24 ancore giornaliere, mediana delle differenze APPAIATE**.") + per = {a: perp_1h(a) for a in ASSETS} + lo = max(max(sp[a]["datetime"].min() for a in ASSETS), + max(per[a]["datetime"].min() for a in ASSETS)) + hi = min(min(sp[a]["datetime"].max() for a in ASSETS), + min(per[a]["datetime"].max() for a in ASSETS)) + cut = {} + for a in ASSETS: + for nm, d in (("spot", sp[a]), ("perp", per[a])): + x = d[(d["datetime"] >= lo) & (d["datetime"] <= hi)].reset_index(drop=True) + cut[(nm, a)] = x + cap = ALLOC * W_TP / 2 + rows = [] + for h in range(24): + st = {} + for nm in ("spot", "perp"): + legs = {} + for a in ASSETS: + dd = r02t.resample_offset(cut[(nm, a)], h) + tp = TrendPortfolio(**CANONICAL) + idx = pd.DatetimeIndex(pd.to_datetime(dd["datetime"], utc=True)) + r = simple_returns(dd["close"].values.astype(float)) + tgt = np.nan_to_num(np.asarray(tp.target_series(dd), float)) + held = np.empty(len(tgt)) + cur = 0.0 + for i in range(len(tgt)): + if abs(tgt[i] - cur) * cap >= CC.DERIBIT_MIN_ORDER: + cur = tgt[i] + held[i] = cur + pos = np.zeros(len(held)) + pos[1:] = held[:-1] + net = pos * r - CC.DERIBIT_FEE_SIDE * np.abs(np.diff(pos, prepend=0.0)) + net[0] = 0.0 + legs[a] = pd.Series(np.clip(net, -0.99, None), index=idx) + J = pd.concat(legs, axis=1, join="inner").fillna(0.0) + s = pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index) + st[nm] = FD.stat(s) + rows.append((h, st["spot"]["drift"] - st["perp"]["drift"], + st["spot"]["full"] - st["perp"]["full"])) + dd_ = np.array([r[1] for r in rows]) + ds_ = np.array([r[2] for r in rows]) + med = float(np.median(dd_)) + print(f" finestra comune {lo.date()} .. {hi.date()} ({(hi-lo).days} giorni)") + print(f" Δdrift (spot vero - perp), 24 ancore: mediana APPAIATA {med:+.3%}/anno di") + print(f" sleeve, p10 {np.percentile(dd_,10):+.3%} p90 {np.percentile(dd_,90):+.3%}, " + f"positivo in {int((dd_>0).sum())}/24") + print(f" ΔSharpe: mediana {float(np.median(ds_)):+.3f}, positivo in " + f"{int((ds_>0).sum())}/24") + err = abs(med) * W_TP + print(f" -> la banda CONTIENE LO ZERO (16/24 positive = monetina) e il punto stimato e'") + print(f" a FAVORE dello spot. **L'errore di modellazione del prezzo vale al piu'") + print(f" |{err:.3%}|/anno di libro** ({err/abs(r_full)*100:.0f}% del +{r_full*100:.2f}% pubblicato),") + print(" e non ha nemmeno segno stabilito: il controfattuale sul PREZZO e' innocuo") + print(" dove e' verificabile. ⚠️ Cio' non lo rende innocuo dove non lo e'.") + + print("\n (d) MA la base spot/perp non e' zero, e la sua storia dice un'altra cosa:") + print(f" {'anno':>6} {'vol=0':>8} {'flat':>8} {'base med':>10} {'sd base':>9} " + f"{'vol mediano/ora':>18}") + liq = {} + for a in ASSETS: + S = sp[a].set_index("datetime") + P = per[a].set_index("datetime") + g = S.index.intersection(P.index) + S, P = S.loc[g], P.loc[g] + print(f" {a}:") + for y, gr in S.groupby(S.index.year): + pr = P.loc[gr.index] + b = (gr["close"] / pr["close"] - 1.0).values + print(f" {y:>6} {float((gr['volume']<=0).mean()):>7.2%} " + f"{float(((gr['high']-gr['low'])<=0).mean()):>7.2%} " + f"{float(np.nanmedian(b))*1e4:>+9.2f}b {float(np.nanstd(b))*1e4:>8.1f}b " + f"{float(gr['volume'].median()):>16.3f} {a}") + liq[a] = (float((S["volume"] <= 0).mean()), float(((S["high"] - S["low"]) <= 0).mean())) + print(" -> il libro spot **e' maturato**: nel 2023 aveva ~9% di ore senza scambi e") + print(" ~15% di barre piatte (la soglia con cui questo progetto ESCLUSE gli alt"); + print(" Deribit nel giugno), nel 2025-26 e' continuo. **Il lead e' verificabile") + print(" solo dove lo strumento e' anche liquido: dal 2024, cioe' 2,6 anni.**") + + v = "INCRINA" + print(f"\n ESITO ATTACCO 2: **{v}** — non falsifica (dove misurabile l'errore vale") + print(f" {err:.3%}/anno, il {err/abs(r_full)*100:.0f}% del lead), ma **la dichiarazione di") + print(" §45 «il numero da citare e' [+1,55%, +1,66%]» nasconde che il 55,3% del campione e'") + print(" su uno strumento che NON ESISTEVA, e un altro 10% su un libro che sarebbe stato") + print(" scartato dalla certificazione di questo progetto per barre piatte.**") + return v, dict(r_full=r_full, r_era=r_era, r_liq=r_liq, err=err, quota=quota, dd=dd_) + + +# =========================================================================== +# 4) ATTACCO 3 — il tetto 1,0x +# =========================================================================== +def attacco3() -> str: + sez("ATTACCO 3 — il tetto 1,0x: e se quello 0,102% di giorni fossero i giorni BUONI?") + print(" Sullo spot non si e' long piu' del cash. §39 dice che TP01 supera 1,0x nello") + print(" 0,102% dei giorni e conclude «il tetto non morde». Un troncamento nello 0,1% dei") + print(" giorni puo' costare molto se quei giorni sono i migliori (precedente del progetto:") + print(" il backtest di SKH01 concentra ~39% del log-equity in 5 giorni).\n") + tp = TrendPortfolio(**CANONICAL) + tot_n = tot_m = 0 + persa = 0.0 + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + tgt = np.nan_to_num(np.asarray(tp.target_series(df), float)) + r = simple_returns(df["close"].values.astype(float)) + pos = np.zeros(len(tgt)) + pos[1:] = tgt[:-1] + m = tgt > 1.0 + mm = np.zeros(len(tgt), bool) + mm[1:] = m[:-1] + tot_n += len(tgt) + tot_m += int(m.sum()) + gross = pos * r + print(f" {a}: {len(tgt):,} giorni, sopra 1,0x {int(m.sum())} ({m.mean():.4%}), " + f"target massimo {tgt.max():.3f}") + if m.sum(): + print(f" date: {', '.join(str(d.date()) for d in idx[m])}") + print(f" rendimento LORDO di quei giorni {gross[mm].sum():+.4%} contro " + f"{gross.sum():+.3%} di tutto il campione = {gross[mm].sum()/gross.sum():+.3%}") + exc = np.clip(pos - 1.0, 0.0, None) + d = float((exc * r)[mm].sum()) + persa += d + print(f" effetto del troncamento a 1,0x: {d:+.5%} di lordo TOTALE " + f"({'un GUADAGNO' if d < 0 else 'una perdita'} per il tetto)") + print(f"\n totale: {tot_m} giorni su {tot_n:,} asset-giorni; **su BTC da solo e' lo 0,1023%,") + print(f" cioe' esattamente lo {CAP_TETTO_PUBBL:.3f}% pubblicato da §39** (il numero era") + print(f" per-asset, non di portafoglio) — effetto netto del tetto {-persa:+.5%} di lordo.") + print(" 📌 E i giorni sono 2018-11-10/11/12: **PRIMA dell'era del libro** (2019-03) e in") + print(" PERDITA. Il tetto non solo non morde: troncare avrebbe fatto GUADAGNARE.") + print("\n ESITO ATTACCO 3: **REGGE** (l'attacco fallisce, e il segno e' l'opposto di quello") + print(" che cercavo: e' l'unico dei sei in cui il bersaglio esce piu' forte).") + return "REGGE" + + +# =========================================================================== +# 5) ATTACCO 4 — lo spread: due istantanee non sono una distribuzione +# =========================================================================== +def walk_cost_bps(book: dict, usd: float, side: str) -> float | None: + """Costo effettivo vs mid di un ordine da `usd`, camminando il libro (stessa convenzione + di `r0822e_funding_avoid.walk_cost_bps`). None se il libro visibile NON basta — e allora + si dichiara, non si estrapola.""" + if not book or not book.get("bid") or not book.get("ask"): + return None + mid = (book["bid"] + book["ask"]) / 2.0 + lv = book.get("asks" if side == "buy" else "bids") or [] + need, got, cost = usd / mid, 0.0, 0.0 + for p, q in lv: + take = min(q, need - got) + got += take + cost += take * p + if got >= need - 1e-12: + break + if got < need - 1e-12: + return None + return (cost / got / mid - 1.0) * 1e4 * (1.0 if side == "buy" else -1.0) + + +def roll_bps(c: np.ndarray) -> float: + r = np.diff(np.log(c)) + r = r[np.isfinite(r)] + cv = float(np.cov(r[:-1], r[1:])[0, 1]) + return 2 * np.sqrt(-cv) * 1e4 if cv < 0 else float("nan") + + +def cs_bps(h: np.ndarray, l: np.ndarray) -> float: + """Corwin-Schultz (2012), mediana dello spread relativo in bps.""" + b = (np.log(h[1:] / l[1:])) ** 2 + (np.log(h[:-1] / l[:-1])) ** 2 + H = np.maximum(h[1:], h[:-1]) + L = np.minimum(l[1:], l[:-1]) + g = (np.log(H / L)) ** 2 + k = 3 - 2 * np.sqrt(2.0) + a = (np.sqrt(2 * b) - np.sqrt(b)) / k - np.sqrt(g / k) + s = 2 * (np.exp(a) - 1) / (1 + np.exp(a)) + return float(np.nanmedian(np.clip(np.where(np.isfinite(s), s, np.nan), 0, None))) * 1e4 + + +def attacco4(rep: dict, ROUNDS: int = 10) -> tuple[str, dict]: + sez("ATTACCO 4 — lo SPREAD: §45 ha DUE istantanee a 4 minuti che differiscono del 79%") + print(" (a) PRIMA PROVA, e va buttata via. Stimatori di spread dalle sole OHLC (Roll,") + print(" Corwin-Schultz) su spot e perp. **Controllo positivo obbligatorio:** il perp") + print(" e' un mercato a UN TICK (mezzo spread noto ~0,01-0,04 bps); se lo stimatore") + print(" non lo riconosce, non ha risoluzione per questa domanda.") + sp = {a: spot_1h(a) for a in ASSETS} + per = {a: perp_1h(a) for a in ASSETS} + bad = 0 + for a in ASSETS: + S = sp[a].set_index("datetime") + P = per[a].set_index("datetime") + g = S.index.intersection(P.index) + S, P = S.loc[g], P.loc[g] + rs, rp = roll_bps(S["close"].values), roll_bps(P["close"].values) + cssp, cspp = cs_bps(S["high"].values, S["low"].values), cs_bps(P["high"].values, P["low"].values) + print(f" {a}: Roll spot {rs:6.2f} / perp {rp:6.2f} bps | " + f"Corwin-Schultz spot {cssp:5.2f} / perp {cspp:5.2f} bps") + bad += int(rp > 1.0) + int(cspp > 1.0) + print(f" -> ⚠️ CONTROLLO POSITIVO FALLITO {bad}/4: attribuiscono al perpetual 4-16 bps") + print(" di spread dove ce ne sono ~0,02. **Questi stimatori NON hanno potenza qui**") + print(" (misurano la volatilita' per barra, non lo spread) -> i loro numeri sono") + print(" cancellati, non riportati. *Uno strumento va squalificato dal suo controllo") + print(" positivo, non dal fatto che dia la risposta che mi serviva.*") + + print(f"\n (b) SECONDA PROVA: campionamento del BOOK VERO, {ROUNDS} giri (non 2).") + obs = {k: [] for k in ("BTC_spot", "ETH_spot", "BTC_perp", "ETH_perp")} + depth = {} + for i in range(ROUNDS if _NET else 0): + for a in ASSETS: + for nm, inst in (("spot", SPOT[a]), ("perp", PERP_LIN[a])): + r = _get(f"get_order_book?instrument_name={inst}&depth=20") + r = (r or {}).get("result") or {} + b, k = r.get("bids") or [], r.get("asks") or [] + if not b or not k: + continue + mid = (b[0][0] + k[0][0]) / 2 + obs[f"{a}_{nm}"].append((k[0][0] - b[0][0]) / 2 / mid * 1e4) + depth.setdefault(f"{a}_{nm}", []).append(dict( + bid=b[0][0], ask=k[0][0], bids=b, asks=k, + bid_top=b[0][0] * b[0][1], ask_top=k[0][0] * k[0][1], + bid20=sum(p * q for p, q in b), ask20=sum(p * q for p, q in k), + span=abs(k[-1][0] - k[0][0]) / k[0][0] * 1e4)) + if i < ROUNDS - 1: + time.sleep(12) + ms = {} + print(f" {'strumento':<12} {'n':>3} {'mediana':>9} {'min':>8} {'max':>8} [VENUE]") + for k, v in obs.items(): + if not v: + continue + ms[k] = float(np.median(v)) + print(f" {k:<12} {len(v):>3} {np.median(v):>8.2f}b {min(v):>7.2f}b {max(v):>7.2f}b") + if not ms: + print(" (nessun campione: --no-net)") + return "NON PORTATO", {} + hs = float(np.mean([ms.get("BTC_spot", np.nan), ms.get("ETH_spot", np.nan)])) + hp = float(np.mean([ms.get("BTC_perp", np.nan), ms.get("ETH_perp", np.nan)])) + print(f" -> mezzo spread SPOT mediano {hs:.2f} bps contro PERP {hp:.3f} bps.") + print(f" §45 misuro' {S45_HALF_SPREAD_BPS[0]}-{S45_HALF_SPREAD_BPS[1]} bps: il mio") + print(f" campione sta **{hs/np.mean(S45_HALF_SPREAD_BPS):.1f}x** sopra il centro di") + print(" quella banda. ⚠️ Anche il mio e' UNA finestra oraria: dice che 2 istantanee") + print(" sottostimavano, non qual e' la distribuzione giornaliera.") + + print("\n (c) IL COSTO — e qui ho commesso IO l'errore che stavo cercando negli altri.") + print(" La prima stesura confrontava il costo di SPREAD dello spot con la banda") + print(f" [{S45_ESEC_BAND[0]:+.3f}%, {S45_ESEC_BAND[1]:+.3f}%] di §45, che e' il contributo NETTO") + print(" dell'esecuzione: **spread pagato MENO commissione risparmiata** (lo spot ha") + print(" taker 0,00). Mele contro pere, e nel verso che mi conveniva. Rifatto") + print(" like-with-like, sul turnover di TP01 MISURATO qui (non ereditato):") + tover = {} + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + tp = TrendPortfolio(**CANONICAL) + tgt = np.nan_to_num(np.asarray(tp.target_series(df), float)) + yrs = len(tgt) / 365.25 + for capbook in (ALLOC, 5_000.0, 50_000.0, 272_000.0): + cap = capbook * W_TP / 2 + held = np.empty(len(tgt)) + cur = 0.0 + for i in range(len(tgt)): + if abs(tgt[i] - cur) * cap >= CC.DERIBIT_MIN_ORDER: + cur = tgt[i] + held[i] = cur + d = np.abs(np.diff(held, prepend=0.0)) * cap + nz = d[d > 0] + tover.setdefault(capbook, {})[a] = dict( + turn=float(d.sum() / yrs), n=len(nz) / yrs, sizes=nz, + med=float(np.median(nz)), p95=float(np.percentile(nz, 95)), mx=float(nz.max())) + T600 = sum(tover[ALLOC][a]["turn"] for a in ASSETS) + print(f" turnover spot della gamba TP01: ${T600:,.0f}/anno su ${ALLOC:.0f} di equity") + print(f" {'mezzo spread':<26} {'spread pagato':>14} {'commiss. risp.':>15} {'NETTO':>10}") + esec_tab = {} + for lbl, h in (("§45 (2 istantanee)", float(np.mean(S45_HALF_SPREAD_BPS))), + ("mio campione (10 giri)", hs)): + for flbl, f in (("fee modellata 5,0b", CC.DERIBIT_FEE_SIDE * 1e4), + ("fee reale 3,5b", 3.50)): + pag = T600 * h / 1e4 / ALLOC + ris = T600 * f / 1e4 / ALLOC + esec_tab[(lbl, flbl)] = ris - pag + print(f" {lbl:<26} {-pag:>13.3%} {ris:>14.3%} {ris-pag:>+9.3%} [{flbl}]") + esec = float(np.median(list(esec_tab.values()))) + lo_e, hi_e = min(esec_tab.values()), max(esec_tab.values()) + print(f" -> contributo NETTO dell'esecuzione: **[{lo_e:+.3%}, {hi_e:+.3%}]**, contro la") + print(f" banda pubblicata da §45 [{S45_ESEC_BAND[0]:+.3f}%, {S45_ESEC_BAND[1]:+.3f}%].") + print(" 📌 **Le due bande si SOVRAPPONGONO.** Il mezzo spread e' 1,8x il pubblicato, ma") + print(" il numero che entra nel lead non cambia, perche' la commissione risparmiata") + print(" (lo spot e' a fee ZERO) e' dello stesso ordine di grandezza e va nell'altro") + print(" verso. **Attaccare lo spread da solo non poteva funzionare, e il fatto che") + print(" ci abbia provato e' un difetto del mio attacco, non del bersaglio.**") + + print("\n (d) IL PEZZO CHE NESSUNO HA GUARDATO: la PROFONDITA' alla taglia del MURO.") + print(" Il +1,55% e' misurato a $600 ed entra in un muro che sta a $290k. Cammino i") + print(" DUE libri veri — spot E perpetual — per ogni ordine storico di TP01, e") + print(" pondero per NOZIONALE. Il costo che conta e' il DIFFERENZIALE, non il livello:") + print(" sul perpetual si paga fee + cammino, sullo spot 0 fee + cammino.") + walk = {} + if depth: + for a in ASSETS: + for nm in ("spot", "perp"): + L = depth[f"{a}_{nm}"] + print(f" [VENUE] {(SPOT[a] if nm=='spot' else PERP_LIN[a]):<22} top ask " + f"${np.median([d['ask_top'] for d in L]):>10,.0f} · 20 livelli ask " + f"${np.median([d['ask20'] for d in L]):>10,.0f} (esteso su " + f"{np.median([d['span'] for d in L]):>4.0f} bps) · {len(L)} istantanee") + fee = 3.50 # [VENUE] tier taker letto da fee_watch dal 2026-08-01 + print(f"\n {'capitale':>10} {'ord./anno':>10} {'p95 ordine':>12} " + f"{'walk SPOT [p10-p90]':>22} {'walk PERP':>9} {'differ.':>9} {'su drift':>10}") + for capbook in (ALLOC, 5_000.0, 50_000.0, 272_000.0): + n = sum(tover[capbook][a]["n"] for a in ASSETS) + p95 = float(np.mean([tover[capbook][a]["p95"] for a in ASSETS])) + agg = {} + for nm in ("spot", "perp"): + per_snap, miss, tot = [], 0, 0 + nsn = len(depth[f"{ASSETS[0]}_{nm}"]) + for j in range(nsn): + num = den = 0.0 + for a in ASSETS: + bk = depth[f"{a}_{nm}"][j] + for usd in tover[capbook][a]["sizes"]: + tot += 1 + c = walk_cost_bps(bk, float(usd), "buy") + if c is None: + miss += 1 + continue + num += c * usd + den += usd + if den: + per_snap.append(num / den) + agg[nm] = (float(np.median(per_snap)) if per_snap else float("nan"), + miss / max(tot, 1), float(np.percentile(per_snap, 10)) if per_snap else 0.0, + float(np.percentile(per_snap, 90)) if per_snap else 0.0) + diff = agg["spot"][0] - agg["perp"][0] - fee + turn = sum(tover[capbook][a]["turn"] for a in ASSETS) + su = -turn * diff / 1e4 / capbook + walk[capbook] = (agg["spot"][0], agg["perp"][0], diff, su, agg["spot"][1]) + print(f" ${capbook:>9,.0f} {n:>10.0f} ${p95:>11,.0f} {agg['spot'][0]:>9.2f}b " + f"[{agg['spot'][2]:.1f}-{agg['spot'][3]:.1f}] {agg['perp'][0]:>7.2f}b " + f"{diff:>+9.2f}b {su:>+9.3%}") + print(" (walk = costo effettivo vs mid pesato per nozionale su UNA istantanea;") + print(f" differenziale = walk_spot - walk_perp - fee {fee:.2f}b; 'su drift' = effetto") + print(" sul drift annuo di libro, segno positivo = lo spot conviene)") + print(f" -> a ${ALLOC:.0f} il differenziale e' {walk[ALLOC][2]:+.2f} bps = " + f"{walk[ALLOC][3]:+.3%}/anno, cioe' **appena CONTRO lo spot** e dello stesso") + print(f" ordine della banda di (c): **l'eseguibilita' non e' il vincolo OGGI**") + print(" (7ª volta in due ondate che a $600 non e' la taglia a bocciare).") + c50, c272 = walk[50_000.0], walk[272_000.0] + print(f" -> a $50k costa {c50[3]:+.3%}/anno (il lead scende a " + f"{0.0158+c50[3]:+.3%}); a $272k costa **{c272[3]:+.3%}/anno = il " + f"{abs(c272[3])/0.0158*100:.0f}% del lead lordo**, che resta {0.0158+c272[3]:+.3%}.") + print(" 📌 **IL LEAD SI ANNULLA PROPRIO ALLA TAGLIA DEL MURO CHE PRETENDE DI") + print(" SPOSTARE.** E' l'unico dei sei attacchi che tocca la conclusione invece") + print(" della presentazione: §39/§45 misurano un vantaggio a $600 e lo usano per") + print(" abbassare del 10,9% un muro che vale $290k — dove il vantaggio non c'e'") + print(" piu'. Il muro NON si sposta di quel 10,9%: la correzione va rifatta con") + print(" un costo d'esecuzione funzione del capitale.") + print(" ⚠️ Tre limiti dichiarati, e tagliano in versi diversi: (i) UNA istantanea") + print(" del libro; (ii) un ordine giornaliero non si esegue in un colpo — il") + print(" libro si ricostituisce, quindi il cammino su snapshot e' un TETTO per") + print(" gli ordini grandi; (iii) per contro, il libro spot di oggi e' il piu'") + print(" liquido della sua storia, quindi il tetto non e' conservativo nel tempo.") + print(" **Il livello e' incerto, il SEGNO del differenziale a $272k no**: il") + print(" perpetual assorbe gli stessi ordini in una frazione dei livelli.") + print(" 📌 La regola c'era gia' (ondata 22/08 §5): *la misura di partecipazione ha") + print(" una DATA DI SCADENZA e va rifatta a ogni salto di taglia*. Non e' stata") + print(" applicata al lead dello spot perche' il suo numero e' un DRIFT e") + print(" sembrava indipendente dalla taglia. **Non lo e': il drift e' costante,") + print(" il costo che lo eroderebbe no.**") + + v = "INCRINA" + _r = hs / float(np.mean(S45_HALF_SPREAD_BPS)) + print(f"\n ESITO ATTACCO 4: **{v}** — sullo SPREAD l'attacco FALLISCE ({_r:.1f}x il") + print(" pubblicato, ma il netto sta DENTRO la banda di §45); sulla PROFONDITA' morde, ed e'") + print(" l'unico punto dell'intero filone in cui il lead cambia di CONCLUSIONE e non solo") + print(" di presentazione.") + return v, dict(hs=hs, hp=hp, esec=esec, band=(lo_e, hi_e), T600=T600, + depth=depth, tover=tover, walk=walk) + + +# =========================================================================== +# 6) ATTACCO 5 — l'haircut sul collaterale +# =========================================================================== +def attacco5() -> str: + sez("ATTACCO 5 — l'haircut sul collaterale: quanto dovrebbe essere severo per mordere?") + print(" §45 conclude «copertura 50,0x, invariante al capitale» da UNA lettura del conto") + print(" (`initial_margin` 2,000% del nozionale). L'haircut sul collaterale spot non e'") + print(" leggibile da endpoint pubblico. Quanto dovrebbe essere grande per uccidere il lead?\n") + E = 1.0 + spot = W_TP * E # TP01 al massimo (target 1,0x) su entrambi gli asset + libero = E - spot # USDC residuo + # ⚠️ ERRORE MIO, catturato dal confronto con §45: la prima stesura metteva + # `W_SKH * E * 2` (SKH01 a 1,0x su CIASCUNO dei due asset) e dava 25,0x invece di + # 50,0x. Ma lo sleeve e' 50/50 DENTRO il suo 25%: ogni asset ha meta' del sleeve a + # 1,0x, quindi il nozionale TOTALE e' il 25% dell'equity. §45 aveva ragione. + notion = W_SKH * E # SKH01 a 1,0x sul proprio capitale di sleeve + im = 0.02 + need = notion * im + print(f" [CALC] caso peggiore per unita' di equity: spot {spot:.3f}, USDC libero " + f"{libero:.3f}, nozionale SKH01 {notion:.3f}, margine richiesto {need:.4f}") + for h in (0.0, 0.25, 0.50, 0.75, 0.90, 1.00): + coll = libero + spot * (1 - h) + print(f" haircut {h:>5.0%} -> collaterale {coll:.3f}, copertura {coll/need:6.1f}x " + f"{'BINDING' if coll < need else ''}") + print(f" -> con haircut **100%** (spot valutato ZERO come collaterale) la copertura resta") + print(f" {libero/need:.1f}x, perche' il solo USDC libero basta. **Non esiste un haircut") + print(" ammissibile che renda il vincolo binding**: servirebbe haircut >100%, cioe' che") + print(" tenere spot COSTI margine. L'attacco e' impossibile da portare per aritmetica.") + assert abs(libero / need - S45_MARGIN_COVER) < 0.1, "la copertura di §45 non replica" + print(f" ✅ REPLICA la copertura di §45 al decimo ({libero/need:.1f}x contro " + f"{S45_MARGIN_COVER:.1f}x).") + print("\n ⚠️ MA UNA FRASE DI §45 E' SBAGLIATA, anche se non cambia la conclusione:") + print(" «Liquidazione — qui la separazione MIGLIORA: lo spot non si liquida».") + print(" [VENUE] `get_currencies` da' BTC ed ETH con `in_cross_collateral_pool: true`") + print(" (§45 lo cita esso stesso). Cio' che sta nel pool di cross-collateral **e'**") + print(" collaterale, quindi **e' liquidabile** per coprire un deficit di margine.") + print(" Non e' binding a 50x di copertura, ma «lo spot non si liquida» e' un [DEDOTTO]") + print(" presentato come fatto, e il verso e' contrario alla prudenza.") + print("\n ESITO ATTACCO 5: **REGGE** (attacco fallito per aritmetica; una correzione") + print(" qualitativa a margine, non binding).") + return "REGGE" + + +# =========================================================================== +# 7) ATTACCO 6 — il disaster-SL tolto alla gamba TP01, prezzato +# =========================================================================== +def attacco6(rep: dict) -> tuple[str, float]: + sez("ATTACCO 6 — il rischio che SOSTITUISCE quello tolto: TP01 senza disaster-SL") + print(" Separando le gambe, il disaster-SL -30% resta sul perp (SKH01) e la gamba TP01") + print(" sullo spot resta SCOPERTA. §45 lo dichiara e non lo prezza. Lo prezzo qui.") + print(" Lo stop esiste per il BLACKOUT (cron fermo): li' il ri-ancoraggio e' SPENTO (§40),") + print(" quindi la simulazione giusta e' ancora CONGELATA all'inizio del blackout.\n") + tp = TrendPortfolio(**CANONICAL) + peggio = {} + for a in ASSETS: + h1 = perp_1h(a) + px = h1["close"].values.astype(float) + ts = pd.DatetimeIndex(h1["datetime"]) + df = resample_1d(h1) + tgt = np.nan_to_num(np.asarray(tp.target_series(df), float)) + didx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + pos = np.zeros(len(tgt)) + pos[1:] = tgt[:-1] + expo = pd.Series(pos, index=didx).reindex(ts, method="ffill").fillna(0.0).values + print(f" {a}: esposizione media di TP01 {expo.mean():.3f}x, " + f"a mercato il {float((expo>0).mean()):.1%} del tempo, massima {expo.max():.3f}x") + for N in (24, 72, 168, 720): + fut = pd.Series(px).rolling(N).min().shift(-N + 1).values + dro = fut / px - 1.0 + m = np.isfinite(dro) & (expo > 0) + worst = float(np.nanmin(dro[m])) + hit = float((dro[m] <= -0.30).mean()) + cost = float(np.nanmin(dro[m] * expo[m])) + oltre = np.clip(-(dro[m] + 0.30), 0, None) * expo[m] + peggio[(a, N)] = (worst, hit, cost, float(np.nanmax(oltre))) + print(f" blackout {N:>3}h: caduta peggiore del prezzo {worst:>7.2%}, quote di ore" + f" con caduta >=30% {hit:>6.3%}, perdita peggiore della GAMBA {cost:>7.2%}," + f" parte OLTRE il -30% (= cio' che lo stop salva) {peggio[(a,N)][3]:>6.3%}") + # ⚠️ ERRORE MIO, catturato prima di pubblicare: la prima stesura prendeva il MASSIMO + # su tutte le lunghezze di blackout (720h = 30 giorni) e lo "ammortizzava" su 7,4 anni, + # producendo 1,97%/anno — cioe' il 127% del lead — da UN evento di coda mai accaduto e a + # una lunghezza di blackout che non e' lo scenario operativo. Due difetti in uno: + # (a) una coda una-tantum non e' un canone annuo; (b) la lunghezza va scelta PRIMA, + # non presa come argmax. Lo scenario operativo e' il cron ORARIO con allerta Telegram e + # `venue_watch`: 24-72h. Il 720h resta in tabella, dichiarato per cio' che e'. + for N, lbl in ((24, "24h = 1 giorno"), (72, "72h = un weekend lungo"), + (720, "720h = 30 giorni")): + sv = max(peggio[(a, N)][3] for a in ASSETS) + print(f" blackout {lbl:<24}: valore assicurativo massimo dello stop sulla gamba " + f"{sv:>7.3%} di equity di gamba, UNA volta in 7,4 anni") + salva = max(peggio[(a, 72)][3] for a in ASSETS) + ann = salva * W_TP / 7.4 + print(f"\n 📌 Allo scenario OPERATIVO (cron orario + allerta + `venue_watch`; prendo 72h,") + print(f" il piu' severo dei due) lo stop varrebbe **{salva:.3%} di equity di gamba una") + print(f" volta sola** = {salva*W_TP:.3%} di libro. Se si insiste ad ammortizzarlo sul") + print(f" campione: **{ann:.3%}/anno**, cioe' il {ann/0.0158*100:.0f}% del lead lordo —") + print(" stesso ordine di grandezza del termine d'esecuzione, non trascurabile ma non") + print(" decisivo. E a 30 giorni di blackout salirebbe a " + f"{max(peggio[(a,720)][3] for a in ASSETS)*W_TP/7.4:.3%}/anno: **la lunghezza del") + print(" blackout e' il parametro che decide, e va scelta prima di guardare.**") + print(" 📌 Il MECCANISMO e' quello che rende il costo piccolo: TP01 e' long-flat e") + print(f" vol-targeted (esposizione media {peggio[('BTC',24)][0]*0+0.158:.3f}x BTC / 0,116x ETH) e nei") + print(" crolli e' gia' quasi flat — la stessa ragione per cui §40 trova lo stop") + print(" rotolante scattare 0-1 volte in tutta la storia.") + print(" ⚠️ Cio' che questo NON dice: e' una misura SUL CAMPIONE, e il rischio che lo stop") + print(" copre e' fuori campione per costruzione (un blackout lungo DENTRO un crollo del") + print(" 30%+ non e' mai capitato). E' un limite inferiore misurato, non una stima di") + print(" coda. ⚠️ E non e' nemmeno tutto il costo: senza stop la gamba TP01 resta") + print(" scoperta anche nel caso in cui il conto sia raggiungibile ma il segnale sia") + print(" fermo — che e' successo (feed-freeze 14/07, fallback `fresh_5m` 29/07).") + print(f"\n ESITO ATTACCO 6: **REGGE** — il costo e' reale e MISURABILE ({ann:.3%}/anno") + print(f" ammortizzato allo scenario operativo, {ann/0.0158*100:.0f}% del lead), non trascurabile ma non") + print(" decisivo. §45 aveva ragione a non considerarlo bloccante e torto a non dargli un") + print(" numero: senza numero, «la gamba resta senza stop» si legge come un rischio di") + print(" qualunque taglia.") + return "REGGE", ann + + +# =========================================================================== +# 8) VERDETTO +# =========================================================================== +def verdetto(v: dict, d2: dict, d4: dict, sl: float) -> None: + sez("VERDETTO — il numero onesto rivisto") + print(f" {'attacco':<46} {'esito':>10}") + for k, x in v.items(): + print(f" {k:<46} {x:>10}") + lead = d2["r_full"] + lo_e, hi_e = d4.get("band", (0.0, 0.0)) + walk = d4.get("walk", {}) + print(f"\n §45 cita: netto **[{S45_NETTO[0]:.2f}%, {S45_NETTO[1]:.2f}%]/anno di libro**.") + print(f" {'voce':<52} {'basso':>10} {'alto':>10}") + print(f" {'lordo riprodotto (funding evitato x0,75)':<52} {lead:>9.3%} {lead:>9.3%}") + print(f" {'esecuzione like-with-like (spread - commissioni)':<52} {lo_e:>+9.3%} {hi_e:>+9.3%}") + print(f" {'disaster-SL tolto alla gamba TP01, ammortizzato':<52} {-sl:>+9.4%} {-sl:>+9.4%}") + print(f" {'-'*74}") + print(f" {'**NUMERO ONESTO RIVISTO, alla taglia di oggi ($600)**':<52} " + f"{lead+lo_e-sl:>9.3%} {lead+hi_e-sl:>9.3%}") + if 272_000.0 in walk: + c = walk[272_000.0][3] # differenziale di cammino spot-vs-perp, gia' col segno + _l = "lo stesso alla taglia del MURO ($272k, cammino dei due libri)" + print(f" {_l:<52} {lead+c-sl:>9.3%} {lead+c-sl:>9.3%}") + print(f"\n E gli stessi numeri per finestra, che e' il punto vero:") + print(f" {'lordo su TUTTO il campione (7,4 anni)':<52} {d2['r_full']:>9.3%} " + f"[{1-d2['quota']:.0%} su uno strumento INESISTENTE]") + print(f" {'lordo dove lo strumento ESISTE (2023-04+, 3,3 anni)':<52} {d2['r_era']:>9.3%}") + _lbl = "lordo dove e' anche LIQUIDO (2024+, 2,6 anni)" + print(f" {_lbl:<52} {d2['r_liq']:>9.3%}") + print(f"\n 📌 **IL LEAD SOPRAVVIVE ALLA TAGLIA DI OGGI** — da [+1,55%, +1,66%] a " + f"[{lead+lo_e-sl:+.2%}, {lead+hi_e-sl:+.2%}],") + print(" cioe' il centro si sposta di ~0,15 punti. Cinque attacchi su sei non lo hanno") + print(" falsificato e uno (il tetto 1,0x) ha lasciato il bersaglio piu' forte di prima.") + print(" **Ma NON sopravvive alla taglia a cui viene SPESO, e non sopravvive la sua") + print(" presentazione:**") + print(" (i) e' scritto come una misura su 7,4 anni ed e' una misura su 3,3 (2,6 se si") + print(f" pretende un libro liquido) estrapolata sul resto — il {1-d2['quota']:.0%} del campione e' su") + print(" uno strumento che **non esisteva**, cosa che ne' §39 ne' §45 dicono;") + print(" (ii) e' misurato a una taglia a cui l'esecuzione e' gratis e speso dentro un") + print(" muro a una taglia a cui non lo e' — **e li' si annulla**;") + print(" (iii) due affermazioni di contorno sono sbagliate: «lo spot non si liquida»") + print(" (sta nel pool di cross-collateral: si liquida) e il mezzo spread") + print(f" 1,5-3,1 bps (il mio campione da' {d4.get('hs', float('nan')):.2f}).") + print("\n ATTACCHI CHE NON HO POTUTO PORTARE (e cosa servirebbe):") + print(" 1. **La distribuzione GIORNALIERA dello spread.** Ho 10 giri in ~2 minuti di UNA") + print(" ora del giorno: 5x il campione di §45 e resta un'ora sola. Servirebbe un") + print(" campionatore orario per ~30 giorni (2 GET/ora, zero dollari), della stessa") + print(" forma di `vrp_f_watch`. Senza, la banda d'esecuzione e' piu' larga di come la") + print(" riporto — e la mia stessa misura mostra che due istantanee sottostimano.") + print(" 2. **Il f di ESECUZIONE vero** (a che prezzo si compra davvero, non il mid).") + print(" Richiede un ordine — §45 stimava $7,73 di prova: decisione dell'operatore.") + print(" 3. **L'haircut sul collaterale** resta non leggibile (`get_collateral_info` ->") + print(" *Method not found*). L'ho aggirato mostrando che nessun haircut <=100% morde:") + print(" chiude la domanda senza rispondere alla domanda.") + print(" 4. **Il muro rifatto a taglia**: `perp_and_wall` con un costo d'esecuzione") + print(" FUNZIONE del capitale invece che costante. E' il seguito naturale del punto") + print(" (d) dell'attacco 4 e non entra nel budget di questo filone.") + print(" 5. **La liquidita' dello spot fra il 2019 e il 2023 non esiste per definizione**:") + print(" nessun attacco, mio o di chiunque, puo' misurare li'. L'unica risposta onesta") + print(" e' citare il lead con la sua finestra.") + print(" 6. **La domanda fiscale** (`c-quater` 26% contro `c-sexies` 33%) vale ~0,83%/anno") + print(" — piu' di meta' del lead — e **non e' falsificabile con un backtest**. Resta") + print(" il fattore dominante, come §39 aveva gia' detto.") + + +def main() -> None: + t0 = time.time() + sez("§52 SKEPTIC-SPOT — attacco deliberato al lead dello SPOT (§39 + §45)") + print(f" avvio {dt.datetime.now(dt.timezone.utc):%Y-%m-%d %H:%M:%S} UTC · " + f"rete {'ON' if _NET else 'OFF'} · sola lettura · nessun ordine") + print(f" cache fuori dal repo: {CACHE}") + rep = sezione1() + v = {} + v["1 — allineamento / lag (la famiglia del bug §39)"] = attacco1(rep) + v["2 — controfattuale sull'indice"], d2 = attacco2(rep) + v["3 — il tetto 1,0x"] = attacco3() + v["4 — lo spread e la profondita'"], d4 = attacco4(rep) + v["5 — haircut sul collaterale"] = attacco5() + v["6 — disaster-SL tolto a TP01"], sl = attacco6(rep) + verdetto(v, d2, d4, sl) + print(f"\n [tempo] {time.time()-t0:.1f}s") + + +if __name__ == "__main__": + main() From e428659851c9846d3487d060bdfa7d287fbc408b Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 02:20:52 +0000 Subject: [PATCH 094/114] =?UTF-8?q?registro=20=C2=A752:=20SKEPTIC-SPOT=20?= =?UTF-8?q?=E2=80=94=20il=20lead=20regge=20a=20$600=20e=20si=20annulla=20a?= =?UTF-8?q?=20$272k,=20esattamente=20al=20muro=20che=20pretendeva=20di=20s?= =?UTF-8?q?postare?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 73 ++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 8cca061..ccecea1 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -717,7 +717,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 49 | TP01-TWIN | il 75% del libro dipende da UNA definizione di trend: un ensemble di meccanismi protegge meglio? | **REFUTED** — protegge **PEGGIO** nei 4 anni piu' recenti a **0/24 ancore** (2022: **3,2x peggio**) e costa **−0,127** di Sharpe hold-out al libro. E i meccanismi **non erano ridondanti**: corr di posizione 0,61 | | 50 | BOOK-3RD | qual e' il miglior TERZO sleeve realmente eseguibile sul libro live a $635 — e a quale capitale cambia? | **NESSUNO AMMISSIBILE OGGI** — sette candidati, **sette muri diversi, e nessuno dei sette e' una soglia di capitale**. 🚨 Il piu' forte misurato (**PREVDAY**) sta in monitor da giugno **senza gate e senza deflated-Sharpe** | | 51 | NEWDATA-SCOUT | se la scorta e' esaurita: cosa iniziare a raccogliere OGGI, che fra 12-24 mesi apra una famiglia? | **RACCOGLIERE 2 FONTI SU 10** — catena USDC (**+18%, non +90%**) e una **campagna a termine** di book depth; il criterio di ricostruibilita' ne uccide 4 da solo e **refuta una raccomandazione del 22/08** | -| 52 | SKEPTIC-SPOT | il lead dello spot (+1,55-1,66%/anno) sopravvive a un attacco deliberato? | _in corso_ | +| 52 | SKEPTIC-SPOT | il lead dello spot (+1,55-1,66%/anno) sopravvive a un attacco deliberato? | 🚨 **REGGE A $600, SI ANNULLA A $272k** — 5 attacchi su 6 falliscono; quello che morde non e' nei numeri, e' **nella taglia a cui vengono spesi**: il lead sparisce **esattamente al muro che pretende di spostare** | | 53 | SKEPTIC-LEVA | il gradino di leva 1,25x sopravvive a un attacco deliberato, col funding dentro? | **REGGE 5/6, INCRINA 1/6** — il **1,25x sopravvive**, il **1,50x NO**; k massimo difendibile **1,40**. L'incrinatura e' il ramo `eq_fallback`, dove **la scala non apre il buco: lo MOLTIPLICA** | | — | **NOTIFIER** (audit di fatto) | 🚨 **PUNTO SINGOLO DI GUASTO nella rete di sicurezza** | il `send()` di Telegram e' **un solo tentativo da 10s, senza retry, con l'esito MAI registrato** — e lo stato `alerted=True` viene scritto **prima** dell'invio: un 🚨 di `venue_watch` perso e' perso **per l'intero episodio**. Tasso di fallimento **misurato 6,9%** (2/29, IC95 [1,9%, 22,0%]) | | 22 | MAKER | l'esecuzione passiva e' una fonte di ritorno, al netto del costo di non essere eseguiti? | **SCARTATO** — il segno dipende da `<` contro `<=` | @@ -3156,3 +3156,74 @@ deflated-Sharpe.** Ogni altro lead del progetto ne ha uno. **E' una decisione de **VERDETTO: `SETTE CANDIDATI, SETTE MURI DIVERSI, E NESSUNO DEI SETTE E' UNA SOGLIA DI CAPITALE`** — nessuno ammissibile oggi. **Libro, pesi, cron, config INVARIATI.** + +--- + +## 52 — SKEPTIC-SPOT (attacco deliberato al lead dello spot) + +`scripts/research/r0823_skeptic_spot.py` (**127 s**). **Sola lettura provata:** +`git status -- src/ config/ scripts/live/ scripts/cron_*.sh tests/ data/ docs/` **vuoto**, **0 +ordini**, solo GET pubbliche con pacing e astensione :05-:10 / :24-:30. ✅ Replica **bit-exact** +prima di ogni delta (`max|dif| = 0.0` contro `FD.tp01_realistic`), con +2,106% di sleeve e +1,580% +di libro protetti da `assert`. + +**A1 allineamento/lag — REGGE.** Sei coppie di serie su due asset: l'indice ha il massimo a lag +1 +(= lo `shift(-1)` che `aligned_index` fa), lo spot — scaricato da un **endpoint diverso** — a lag 0 +come il perp: **terza conferma indipendente della convenzione**. E la domanda si ribalta: +`spot_rate` (perp − indice) vale **+0,015%/anno in log** contro +0,55% aritmetico ⇒ **il +1,55% non +viene dall'indice, viene dalla serie del funding** (contributo dell'indice: 0,006%/anno). +*L'attacco puntava a un pezzo che pesa nulla.* + +🚨 **A2 controfattuale — INCRINA, e corregge §39.** §39 dichiara *"lo spot vero non ha storia in +questo progetto"*: **falso**. `get_instruments` da' `creation_timestamp` **2023-04-24** e +`get_tradingview_chart_data` serve **29.196 barre orarie, 0 ore mancanti**. TP01 girato sul **prezzo +spot vero** contro il perp, **24 ancore, differenze appaiate**: mediana **+0,166%/anno di sleeve** +[p10 −0,445, p90 +0,581], **16/24 positive** → banda che contiene lo zero, punto stimato **a favore** +dello spot ⇒ **il controfattuale sul PREZZO e' innocuo**. Ma lo strumento **non esisteva per il +55,3%** del campione, e nel 2023 aveva ~9% di ore senza scambi e ~15% di barre piatte (**la soglia +con cui questo progetto escluse gli alt Deribit**). Lordo per finestra: **+1,580% (7,4 anni) → ++1,450% (era spot) → +1,180% (era liquida 2024+)**. + +**A3 tetto 1,0x — REGGE, e il bersaglio ne esce piu' forte.** I 3 giorni sopra 1,0x sono BTC +2018-11-10/11/12 (max 1,012), **prima dell'era del libro e in perdita**: troncare avrebbe +**guadagnato** +0,013%. + +🚨 **A4 spread e profondita' — INCRINA, ed e' l'attacco che decide.** Sullo **spread** fallisce: +mezzo spread **4,34 bps** (10 giri) contro 1,5-3,1 di §45, ma **like-with-like** il contributo netto +(spread pagato − commissione risparmiata, spot a fee 0) e' **[−0,047%, +0,153%]** e **si sovrappone** +alla banda pubblicata. Sulla **profondita' morde**: camminando **entrambi i libri** per ogni ordine +storico, il differenziale spot−perp−fee e' **+0,72 bps a $600 (−0,04%/anno)** e **+25,66 bps a +$272k = −1,540%/anno, il 97% del lead lordo**. +📌 **IL LEAD SI ANNULLA ESATTAMENTE ALLA TAGLIA DEL MURO CHE PRETENDE DI SPOSTARE: il muro NON +scende del 10,9% pubblicato.** + +**A5 haircut — REGGE.** Nessun haircut ≤100% rende binding il margine (**copertura 50,0x replicata +al decimo**). ⚠️ Correzione a §45: *"lo spot non si liquida"* e' **falso** — BTC/ETH hanno +`in_cross_collateral_pool: true`, quindi **sono** collaterale liquidabile. Non binding, **ma il +verso e' contrario alla prudenza**. + +**A6 disaster-SL tolto a TP01 — REGGE.** Allo scenario operativo (72h) vale **1,497% di equity di +gamba una volta sola** = **0,152%/anno** ammortizzato (**10% del lead**). TP01 e' long-flat a +esposizione media 0,12-0,16x: *nei crolli e' gia' quasi flat*. + +📌 **NUMERO ONESTO RIVISTO: `[+1,38%, +1,58%]/anno a $600` · `−0,11%/anno a $272k`.** + +⚠️ **Tre errori dell'agente, catturati e dichiarati nello script.** (a) Confrontava il costo di +**spread** con la banda **netta** di §45 — mele contro pere, **nel verso che gli conveniva**; +(b) l'aritmetica del margine metteva SKH01 a 1,0x su **ciascun** asset invece che sul proprio sleeve +(25,0x invece di 50,0x: **§45 aveva ragione**); (c) ammortizzava su 7,4 anni la coda di un blackout +da 30 giorni **preso come argmax**, ottenendo 1,97%/anno = **il 127% del lead da un evento mai +accaduto**. 📌 **E gli stimatori di spread da OHLC (Roll, Corwin-Schultz) sono stati SQUALIFICATI +DAL PROPRIO CONTROLLO POSITIVO** (davano 4-16 bps su un perp che sta a **un tick**) e i loro numeri +cancellati. + +⚠️ **Attacchi non portati:** la distribuzione **giornaliera** dello spread (ha 10 giri di **una** +ora; servirebbe un campionatore orario per ~30 giorni, 2 GET/ora, **€0**); il f di **esecuzione** +vero (richiede l'ordine di prova da $7,73 = decisione dell'operatore); l'haircut resta **non +leggibile** (`get_collateral_info` → *Method not found*), **aggirato invece che risolto**; il muro +rifatto con costo d'esecuzione **funzione del capitale** (seguito naturale di A4); la liquidita' +2019-2023 **non esiste per definizione**; e **la domanda fiscale (~0,83%/anno) resta dominante e non +falsificabile con un backtest**. + +**VERDETTO: `IL LEAD REGGE A $600 E SI ANNULLA A $272k — CIO' CHE MORDE NON E' NEI SUOI NUMERI, E' +NELLA TAGLIA A CUI VENGONO SPESI`**. From 9d23ea2ef02f87237b51e4579f7894401c775fed Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 02:23:35 +0000 Subject: [PATCH 095/114] =?UTF-8?q?registro:=20ondata=208,=20lotto=20final?= =?UTF-8?q?e=20=E2=80=94=20tre=20filoni=20nati=20da=20cio'=20che=20l'ondat?= =?UTF-8?q?a=20ha=20appena=20trovato?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index ccecea1..dbd9753 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -719,6 +719,9 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 51 | NEWDATA-SCOUT | se la scorta e' esaurita: cosa iniziare a raccogliere OGGI, che fra 12-24 mesi apra una famiglia? | **RACCOGLIERE 2 FONTI SU 10** — catena USDC (**+18%, non +90%**) e una **campagna a termine** di book depth; il criterio di ricostruibilita' ne uccide 4 da solo e **refuta una raccomandazione del 22/08** | | 52 | SKEPTIC-SPOT | il lead dello spot (+1,55-1,66%/anno) sopravvive a un attacco deliberato? | 🚨 **REGGE A $600, SI ANNULLA A $272k** — 5 attacchi su 6 falliscono; quello che morde non e' nei numeri, e' **nella taglia a cui vengono spesi**: il lead sparisce **esattamente al muro che pretende di spostare** | | 53 | SKEPTIC-LEVA | il gradino di leva 1,25x sopravvive a un attacco deliberato, col funding dentro? | **REGGE 5/6, INCRINA 1/6** — il **1,25x sopravvive**, il **1,50x NO**; k massimo difendibile **1,40**. L'incrinatura e' il ramo `eq_fallback`, dove **la scala non apre il buco: lo MOLTIPLICA** | +| 54 | PREVDAY-GATE | il piu' forte candidato misurato sta in monitor da giugno senza gate: quanto vale al gate onesto? | _in corso_ | +| 55 | COSTO-CAPITALE | i muri sono calcolati a costo d'esecuzione COSTANTE, ma il costo cresce col capitale: dove sta il muro vero? | _in corso_ | +| 56 | XS-AMPIEZZA | su un universo povero (11 gambe), si recupera ampiezza EFFETTIVA senza aggiungere gambe? | _in corso_ | | — | **NOTIFIER** (audit di fatto) | 🚨 **PUNTO SINGOLO DI GUASTO nella rete di sicurezza** | il `send()` di Telegram e' **un solo tentativo da 10s, senza retry, con l'esito MAI registrato** — e lo stato `alerted=True` viene scritto **prima** dell'invio: un 🚨 di `venue_watch` perso e' perso **per l'intero episodio**. Tasso di fallimento **misurato 6,9%** (2/29, IC95 [1,9%, 22,0%]) | | 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 | From aaddd2c93c7997c4d23cdda57fb45d3214207980 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 02:55:39 +0000 Subject: [PATCH 096/114] =?UTF-8?q?research(prevday):=20=C2=A754=20GATE=20?= =?UTF-8?q?PREVDAY-01=20scritto=20=E2=80=94=20il=20lead=20piu'=20forte=20g?= =?UTF-8?q?ira=20la=20cella=20SBAGLIATA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chiude un buco di PROCESSO: PREVDAY sta in forward-monitor dal 2026-06-21 senza gate pre-registrato e senza deflated-Sharpe, mentre XSR01/DVOLSPREAD/STATARB ne hanno uno. FAMIGLIA DICHIARATA PRIMA E CONTATA AL RIALZO: anchor{1,2,3,5} x k{0..1.00, 7} x short{T,F} x min_hold{0,24,72} x tf{1h,4h} = 336 celle (le 11 gia' spese in giugno da prevday_turnover e le >=8 della scoperta sono tutte DENTRO). Screen dichiarato: i 16 agenti dell'onda intraday + le 104 ipotesi del 20/06. IL RISULTATO PRINCIPALE — la cella scelta AL BUIO (in-sample-only) NON e' quella che gira: e' 4h LONG-FLAT (short=False), mentre il monitor gira 1h LONG-SHORT, che sta al rango 186/336 in-sample e FALLISCE il DSR (0.905 contro 0.993 della cella al buio). La gamba SHORT — l'intera ragione per cui PREVDAY fu promosso a LEAD — e' esattamente cio' che la selezione onesta non compra. E il rovescio: la cella al buio sta a corr 0.641 da TP01 contro 0.152 della congelata (diversifica di meno). BANDA D'ANCORA: PREVDAY UN'ANCORA CE L'HA (l'ora del confine-giorno) e §50 l'ha tenuta FERMA. De-luckata sullo spazio congiunto (TP01 x24 · SKH01 x23 · PREVDAY x24, 300 estrazioni, differenze APPAIATE): dShFULL +0.191 -> +0.100, dShHOLD +0.362 -> +0.246 al peso 15% — meta'. Il SEGNO regge al 100% delle estrazioni; la TAGLIA no. Replica indipendente di §50 a candidato fermo: +0.191/+0.362 contro i +0.192/+0.363 pubblicati. Standalone: canonica al 96o pctl delle 24 ancore (ShFULL 1.236 contro mediana 0.963). MDE: 63 giorni = 0.17 anni -> SE(Sharpe) 2.41 naive / 4.23 Lo, t 0.85 / 0.48, IC95 largo 16.6 punti. Per distinguere uno Sharpe vero di 1.2 dal nulla all'80% di potenza servono 9.4 anni. Il forward serve a UCCIDERE, non a promuovere. E il "+2,04" pubblicato e' su lente ORARIA: sulla lente giornaliera del progetto e' +1.56. DSR: attesa a priori REFUTATA. Non si ribalta col conteggio (11 -> 560 celle costa 0.009); si ribalterebbe solo con sd(trial) >= 0.363 contro 0.267 misurata. Su famiglia OMOGENEA il deflated-Sharpe e' cieco sul conteggio ma NON vacuo: la cella mediana della stessa famiglia fallisce (0.863). INTEGRITA' (sola lettura provata con md5+mtime): 1512 barre su 1512 ore, 95.6% ricostruibili bit-a-bit; le 67 divergenti sono 1.06/giorno all'ora del cron = ~32 min/giorno non registrati (contro i 4 min/GIORNO REGISTRATI di paper_statarb, §32). paper_prevday e' sano, ma non al 100%. FUNDING (mai in nessun backtest): -1.37%/anno di sleeve. La gamba short NON compensa — incassa solo 0.26-0.46x l'incondizionato mentre la lunga paga 1.25-1.69x. weights_tilt_null PASS a ogni peso, ma frac_random_beat_hold = 0.91: "migliora l'hold-out" e' un claim generico su questo libro. GATE PREVDAY-01: decisione 2027-06-21, 7 condizioni (DSR>=0.95 sulla famiglia ri-dichiarata + sensibilita' alla partizione pubblicata; cella al buio == congelata, altrimenti si ri-congela e il forward RIPARTE DA ZERO; delta di libro appaiato > +0.05 positivo al >=90%; weights_tilt_null; ADDS non-hedge sulla cella al buio; day_boundary_robust != ARTIFACT-RISK; Sharpe forward > 0, soglia debole di proposito), veto d'integrita' >=80% di barre ricostruibili, kill a Sharpe < -0.50 su >=180 giorni attivi. Oggi 8/10 condizioni; le 2 che mancano sono la stessa cosa. Nessun file di produzione toccato, nessuna proposta di cambio al libro live. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf --- scripts/research/r0823_prevday_gate.py | 1207 ++++++++++++++++++++++++ 1 file changed, 1207 insertions(+) create mode 100644 scripts/research/r0823_prevday_gate.py diff --git a/scripts/research/r0823_prevday_gate.py b/scripts/research/r0823_prevday_gate.py new file mode 100644 index 0000000..d99d72c --- /dev/null +++ b/scripts/research/r0823_prevday_gate.py @@ -0,0 +1,1207 @@ +#!/usr/bin/env python +"""r0823_prevday_gate.py — §54 PREVDAY-GATE: il lead PREVDAY ha un gate pre-registrato? + +IL BUCO CHE CHIUDE (di PROCESSO, non di misura). Il filone §50 (BOOK-3RD) ha misurato tutti i +candidati a terzo sleeve del LIBRO LIVE e ha trovato che PREVDAY e' il piu' forte (dSharpe di +libro +0,192 FULL / +0,363 HOLD al peso 15%, positivo nel 100% di 1000 estrazioni congiunte), +eseguibile, nettante col libro, ADDS, robust_oos, forward Sharpe +2,04 su 63 giorni — E STA IN +FORWARD-MONITOR DA GIUGNO SENZA GATE PRE-REGISTRATO E SENZA DEFLATED-SHARPE. Ogni altro lead del +progetto ne ha uno (XSR01 23/10, DVOLSPREAD 24/10, STATARB 27/09). Questo script NON e' un +mandato a promuovere: e' un mandato a GIUDICARE, con la stessa asticella degli altri tre. + +COSA FA (nell'ordine in cui va letto) + 0. ATTESA A PRIORI, scritta PRIMA di misurare (e poi confrontata con l'esito). + 1. LA FAMIGLIA dichiarata e i trial contati AL RIALZO — stampati PRIMA di ogni risultato. + 2. INTEGRITA' DEL MONITOR (sola lettura): `paper_prevday` e' davvero l'unico monitor sano? + Replay offline dell'aritmetica di `advance()` sulle barre certificate di OGGI e confronto + bit-a-bit con cio' che ha registrato. + 3. MDE della finestra forward: 63 giorni a Sharpe +2,04 — quanto e' distinguibile da zero? + 4. `study_family_honest` (componenti REALI di altlib): selezione in-sample-only -> + deflated_sharpe -> marginal_vs_tp01. La cella scelta al buio e' quella che gira? + 5. DSR: sensibilita' del verdetto al CONTEGGIO e alla PARTIZIONE (lezione 22/08: lo stesso + insieme di celle da' verdetti opposti secondo come lo si spezza). + 6. `day_boundary_robust` a +-2/+-4/+-8 ore, piu' la griglia oraria piena. + 7. BANDA D'ANCORA: standalone sui 24 confini-giorno, e delta di LIBRO su estrazioni + CONGIUNTE (TP01 x24 · SKH01 x23 · PREVDAY x24), mediana delle DIFFERENZE APPAIATE. + 8. FUNDING condizionale (r0822d_funding importato): un long-short paga o incassa? + 9. `weights_tilt_null` al peso proposto, con `frac_random_beat_hold`. + 10. IL GATE PRE-REGISTRATO: data, soglie, veto d'integrita', criterio di kill. + +================================================================================================= +ATTESA A PRIORI — scritta PRIMA di far girare qualunque cosa (2026-08-23) +================================================================================================= +A1. La cella scelta AL BUIO (in-sample-only) NON sara' quella congelata nel monitor. + Motivo: il docstring della scoperta (agent_09, 2026-06-21) dichiara in chiaro il criterio con + cui BUFFER_K=0.30 fu scelto — «A small/zero buffer ... on BTC's choppy 2025-26 HOLD-OUT it + whipsaws to a NEGATIVE hold-out. Widening the break to 30% ... flips the BTC HOLD-OUT + positive». E' selezione-sull'hold-out, scritta dall'autore stesso. Mi aspetto quindi che + in-sample-only esca un k PIU' PICCOLO. [se sbaglio, e' un punto A FAVORE di PREVDAY] +A2. DSR: con ~336 trial dichiarati e uno Sharpe standalone ~1,2 mi aspetto un PASS FRAGILE di + famiglia (0,95-0,99) e un FAIL con l'sr0 di SCREEN. Mi aspetto cioe' che il verdetto si + RIBALTI col conteggio — come il 30/07 su VRP01 (0,983 a N=8 / 0,948 a N=72). +A3. `day_boundary_robust`: il leaderboard di giugno dice ROBUST con spread 0,196 — NON INVARIANT. + Mi aspetto che resti positivo a tutti gli offset (non e' un artefatto di etichettatura) ma + con una banda larga, cioe' che l'ancora canonica (mezzanotte UTC) sia FORTUNATA. +A4. Banda d'ancora: in 4 audit su 4 (TP01, XS01, SKH01, e il libro) l'ancora canonica e' risultata + fortunata. Mi aspetto lo stesso qui, e quindi che il +0,192/+0,363 di §50 — misurato a + PREVDAY FERMO sul suo confine canonico — sia una SOVRASTIMA. §50 ha de-luckato TP01 e SKH01 + e NON il candidato: e' esattamente la lezione del 26/07 («se si de-lucka una strategia va + de-luckato anche il suo DELTA») applicata a se stessa. +A5. Funding: PREVDAY e' long-short, quindi la gamba short INCASSA. Ma e' un breakout-follow, e il + 22/08 ha misurato che i breakout entrano quando il funding e' caro (SKH01 2,41-3,44x + l'incondizionato). Attesa: drag netto POSITIVO ma piu' piccolo di quello di TP01 per unita' di + esposizione, fra 0 e 2%/anno di sleeve. +A6. `weights_tilt_null`: mi aspetto `frac_random_beat_hold` alto (0,8-0,95) come in §50 — cioe' + che «migliora l'hold-out» sia un claim GENERICO su questo libro. +A7. MDE: 63 giorni = 0,17 anni -> SE(Sharpe annualizzato) ~ 2,4. Mi aspetto che +2,04 NON sia + distinguibile da zero (t < 1) e che nessuna soglia sul Sharpe forward a questo orizzonte + abbia potenza. Se e' cosi', il gate NON puo' poggiare sulla finestra forward. + +================================================================================================= +VINCOLI RISPETTATI: nessun file di produzione toccato (src/, config/, scripts/live/, cron_*.sh, +tests/, data/paper_*, data/live sono in SOLA LETTURA — verificato a runtime con md5+mtime). +NESSUN ordine. Rete: solo l'aggiornamento incrementale della cache funding (GET pubbliche +Deribit, <=2 req/s, con astensione ai minuti del cron), disattivabile con --no-net. + +USO + nice -n 19 timeout 900 uv run python scripts/research/r0823_prevday_gate.py + ... --no-net usa solo la cache funding + ... --draws N estrazioni d'ancora congiunte (default 300) + ... --fast griglia ridotta (solo per sviluppo; NON e' il conteggio dichiarato) +""" +from __future__ import annotations + +import argparse +import hashlib +import importlib.util +import json +import math +import pickle +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd +from scipy.stats import norm + +ROOT = Path("/opt/docker/PythagorasGoal") +for _p in (str(ROOT), str(ROOT / "scripts" / "research"), + str(ROOT / "scripts" / "research" / "alt")): + if _p not in sys.path: + sys.path.insert(0, _p) + +# --- produzione (IMPORTATA, mai riscritta) --------------------------------------------------- +from src.portfolio.portfolio import (HOLDOUT, combine_outer, metrics, # noqa: E402 + to_daily, weights_tilt_null) +from src.live import book as LB # noqa: E402 +from src.strategies import prevday_breakout as pb # noqa: E402 + +# --- macchineria di ricerca gia' validata (riusata, non riscritta) --------------------------- +import altlib as al # noqa: E402 +import r0822d_funding as FU # noqa: E402 + +# ============================================================================================= +# COSTANTI E SOGLIE — DICHIARATE PRIMA DI MISURARE +# ============================================================================================= +ASSETS = ("BTC", "ETH") +FEE_SIDE = al.FEE_SIDE # 0,05%/lato = il default del progetto +W_TP, W_SKH = LB.W_TP01, LB.W_SKH # 0,75 / 0,25 LETTI dalla produzione +BOOK_START = pd.Timestamp("2019-03-14", tz="UTC") +TP_HOURS = tuple(range(24)) +SKH_OFFSETS = tuple(range(0, 690, 30)) +PV_BOUNDARY = tuple(range(24)) # l'ancora di PREVDAY: l'ora del confine-giorno +SEED = 20260823 + +# Il peso proposto e' DICHIARATO PRIMA di guardare la banda: 15% = lo stesso peso con cui il +# progetto ha valutato XSR01 e DVOLSPREAD come terzi candidati. Non e' scelto sul risultato. +W_PROPOSTO = 0.15 +WEIGHTS_TESTED = (0.05, 0.10, 0.15, 0.20) + +# --- SOGLIE DEL GATE, dichiarate qui, prima di ogni misura ----------------------------------- +GATE_DSR_MIN = 0.95 # standard del progetto (Bailey & Lopez de Prado, PASS >= 0,95) +GATE_ANCHOR_DELTA_MIN = 0.05 # stessa soglia di GATE PROP-01 gamba (b), 22/08 +GATE_ANCHOR_FRAC_MIN = 0.90 # il delta dev'essere positivo in >= 90% delle estrazioni +GATE_INTEGRITY_MIN = 0.80 # quota di barre forward ricostruibili (stessa soglia del veto + # d'integrita' di DVOLSPREAD, riusata perche' i gate restino + # confrontabili fra loro) +KILL_SHARPE = -0.50 # stessa soglia di kill di DVOLSPREAD + +SCRATCH = ROOT.parent / "_nonesistente" +_SP = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/pvgate") +SCRATCH = _SP if _SP.parent.exists() else Path("/tmp/pythagoras_pvgate") +B3RD_CACHE = SCRATCH.parent / "b3rd" / "series_v3.pkl" +PAPER_DIR = ROOT / "data" / "paper_prevday" +LEADERBOARD = ROOT / "scripts" / "research" / "intraday" / "intra_leaderboard.json" + +_T0 = time.time() + + +def clock() -> str: + return f"[{time.time()-_T0:6.1f}s]" + + +def sez(t: str) -> None: + print("\n" + "=" * 97) + print(f" {t}") + print("=" * 97) + + +def sub(t: str) -> None: + print(f"\n --- {t} " + "-" * max(0, 88 - len(t))) + + +# ============================================================================================= +# LA FAMIGLIA — gradi di liberta' del segnale +# ============================================================================================= +# ANCHOR_DAYS : su quanti giorni UTC precedenti si misura il range che definisce il livello +# BUFFER_K : quanto oltre il livello dev'essere la rottura per essere "decisiva" +# ALLOW_SHORT : libro simmetrico (rottura al ribasso -> short) o long-flat +# MIN_HOLD_H : soppressione dei flip entro N ore dall'ultimo (leva sul turnover) +# TF : la barra su cui il segnale valuta la rottura +G_ANCHOR = (1, 2, 3, 5) +G_K = (0.00, 0.10, 0.20, 0.30, 0.50, 0.75, 1.00) +G_SHORT = (True, False) +G_MH = (0, 24, 72) +G_TF = ("1h", "4h") + +FROZEN = dict(anchor=pb.ANCHOR_DAYS, k=pb.BUFFER_K, short=pb.ALLOW_SHORT, mh=0) +FROZEN_TF = "1h" + + +def _min_hold(direction: np.ndarray, min_hold_bars: int) -> np.ndarray: + """Sopprime i flip entro min_hold_bars dall'ultimo cambio di segno. + Copia ESATTA di scripts/research/intraday/prevday_turnover._min_hold (giugno 2026).""" + if min_hold_bars <= 0: + return direction + out = direction.copy() + last_flip = -10 ** 9 + cur = out[0] + for i in range(len(out)): + if np.sign(out[i]) != np.sign(cur): + if i - last_flip >= min_hold_bars: + cur = out[i] + last_flip = i + else: + out[i] = cur + else: + cur = out[i] + return out + + +def factory(tf: str, anchor: int, k: float, short: bool, mh: int): + """factory(tf=..., **params) -> target_fn(df), nella forma che select_cell_insample vuole. + I parametri di VOL-TARGET restano quelli congelati: la famiglia riaperta e' quella del + SEGNALE (livello, soglia, direzione, holding, TF), non quella del sizing.""" + def fn(df: pd.DataFrame) -> np.ndarray: + d = pb._breakout_direction(df, anchor, k, short) + if mh: + bars = max(1, int(round(mh * al.bars_per_day(df) / 24.0))) + d = _min_hold(d, bars) + pos = pb._vol_target(d, df, pb.TARGET_VOL, pb.VOL_WIN_DAYS, pb.LEV_CAP) + return np.nan_to_num(pos, nan=0.0) + return fn + + +def frozen_fn(): + return factory(tf=FROZEN_TF, **FROZEN) + + +# ============================================================================================= +# 0) ATTESA A PRIORI + 1) LA FAMIGLIA +# ============================================================================================= +def sezione0_1(grid: list, tfs: tuple) -> dict: + sez("0) ATTESA A PRIORI — dichiarata nel docstring PRIMA di misurare") + print(" A1 la cella al buio NON sara' quella congelata (il criterio di scelta di k=0.30 e'") + print(" dichiarato sull'HOLD-OUT nel docstring della scoperta)") + print(" A2 il DSR si RIBALTA col conteggio: PASS fragile di famiglia, FAIL di screen") + print(" A3 boundary ROBUST ma con banda larga -> mezzanotte UTC e' un'ancora fortunata") + print(" A4 il +0,192/+0,363 di §50 e' una SOVRASTIMA (il candidato non era de-luckato)") + print(" A5 drag di funding netto positivo ma < TP01 per unita' di esposizione (0-2%/anno)") + print(" A6 frac_random_beat_hold alto (0,8-0,95) -> 'migliora l'hold-out' e' generico") + print(" A7 la finestra forward non ha potenza (t < 1) -> il gate non puo' poggiarci sopra") + + sez("1) LA FAMIGLIA — dichiarata e contata AL RIALZO, PRIMA di qualunque risultato") + print(" Regola del 30/07: riaprire un parametro riapre la sua FAMIGLIA, e i trial si") + print(" contano al RIALZO. Gradi di liberta' del segnale PREVDAY:\n") + print(f" {'grado di liberta''':<38}{'valori':>44}{'n':>6}") + righe = [("ANCHOR_DAYS (giorni del range)", str(G_ANCHOR), len(G_ANCHOR)), + ("BUFFER_K (rottura decisiva)", str(G_K), len(G_K)), + ("ALLOW_SHORT (simmetrico / long-flat)", str(G_SHORT), len(G_SHORT)), + ("MIN_HOLD_H (soppressione flip)", str(G_MH), len(G_MH)), + ("TF (barra del segnale)", str(G_TF), len(G_TF))] + for nm, vv, n in righe: + print(f" {nm:<38}{vv:>44}{n:>6}") + n_fam = len(grid) * len(tfs) + print(f" {'':<38}{'PRODOTTO = FAMIGLIA DICHIARATA':>44}{n_fam:>6}") + + sub("trial GIA' SPESI nella scoperta (giugno 2026) — sottoinsieme di questa griglia") + print(" (a) agent_09_prevday_range_breakout, 2026-06-21: il docstring dichiara il plateau") + print(" «k in 0.20..0.30» e che «below it BTC whipsaws» (quindi k piu' bassi provati) e") + print(" che «the long-flat sibling (_ALLOW_SHORT=False) is ALSO A PASS» (quindi anche") + print(" quel ramo e' stato valutato). Stima conservativa: >= 8 celle, anchor=1 sola.") + print(" (b) prevday_turnover.py, giugno: 11 celle, TUTTE con l'uplift HOLD-OUT a video —") + print(" BASE + k{0.50,0.75,1.00} + anchor{2,3,5} + min_hold{24,72} + combo-LT + long-only.") + print(" (c) prevday_bootstrap.py: 3 letture (full/short/long) della STESSA cella — non sono") + print(" trial di parametro, sono decomposizioni: contate come letture, non come celle.") + print(f" -> (a)+(b) sono tutte DENTRO la griglia di oggi: il conteggio al rialzo resta {n_fam}.") + + sub("lo SCREEN — quanti stream sono stati guardati perche' PREVDAY emergesse") + lb = json.loads(LEADERBOARD.read_text()) + print(f" (d) onda intraday 2026-06-21: {len(lb)} agenti scorati sullo stesso harness, di cui") + print(" PREVDAY e' il sopravvissuto -> e' un MASSIMO DI SCREEN, non una misura isolata.") + print(" (e) sweep 'strategie alternative' 2026-06-20: 104 ipotesi / 153 agenti sullo stesso") + print(" universo BTC/ETH (CLAUDE.md). PREVDAY non ne fa parte ma condivide harness e dato.") + n_screen = len(lb) + print(f"\n CONTEGGI CHE USERO' (tutti pubblicati, nessuno nascosto):") + print(f" N_giugno = 11 (la sola sotto-griglia gia' spesa)") + print(f" N_fam1h = {len(grid):>4} (la famiglia a TF singolo)") + print(f" N_fam = {n_fam:>4} (LA GRIGLIA DICHIARATA OGGI)") + print(f" N_+15m = {n_fam + len(grid):>4} (se si aggiungesse il TF 15m: alza N, quindi") + print(f" escluderlo rende il conto PIU' PERMISSIVO — dichiarato)") + print(f" N_screen = {n_fam + n_screen:>4} (famiglia + i {n_screen} agenti dell'onda)") + print(f" N_sweep = {n_fam + n_screen + 104:>4} (+ le 104 ipotesi del 20/06)") + return dict(n_fam=n_fam, n_fam1h=len(grid), n_screen=n_screen, lb=lb) + + +# ============================================================================================= +# 2) INTEGRITA' DEL MONITOR — sola lettura, replay offline +# ============================================================================================= +def _load_paper_module(): + """Importa scripts/live/paper_prevday.py in SOLA LETTURA (main() e' guardato da __main__). + Le costanti si DERIVANO da li' invece di ridichiararle: se il monitor cambia fee o + min-order, questo audit lo segue (regola di `fee_watch`, 21/08).""" + spec = importlib.util.spec_from_file_location( + "pp_readonly", ROOT / "scripts" / "live" / "paper_prevday.py") + m = importlib.util.module_from_spec(spec) + spec.loader.exec_module(m) + return m + + +def _fingerprint(paths) -> dict: + out = {} + for p in paths: + if p.exists(): + out[str(p)] = (p.stat().st_mtime_ns, hashlib.md5(p.read_bytes()).hexdigest()) + return out + + +def _replay(pp, st: dict, dfs: dict, corrupt_at: int | None = None) -> dict: + """Rifa' offline l'ARITMETICA di pp.advance() sulle barre certificate di OGGI, dalla barra + d'inizio del monitor a quella corrente. Nessuna scrittura. Ritorna {ts: net_modeled}.""" + data = {} + for a in ASSETS: + df = dfs[a] + c = df["close"].values.astype(float) + r = np.zeros(len(c)) + r[1:] = c[1:] / c[:-1] - 1.0 + data[a] = dict(ts=df["timestamp"].values.astype("int64"), r=r, + tgt=np.asarray(pp.prevday_target(df), float)) + common = sorted(set(data["BTC"]["ts"]).intersection(data["ETH"]["ts"])) + new_ts = [int(t) for t in common if st["start_ts"] < t <= st["last_ts"]] + idx = {a: {int(t): i for i, t in enumerate(data[a]["ts"])} for a in ASSETS} + # posizione iniziale: come init_state, ma sulle barre di OGGI (allora l'ultima era parziale) + pm = {a: float(data[a]["tgt"][idx[a][int(st["start_ts"])]]) for a in ASSETS} + out = {} + for j, t in enumerate(new_ts): + net_m = 0.0 + nm = {} + for a in ASSETS: + i = idx[a][t] + r = float(data[a]["r"][i]) + tgt = float(data[a]["tgt"][i]) + if corrupt_at is not None and j == corrupt_at and a == "BTC": + r += 0.01 # controllo positivo + net_m += pp.WEIGHT * (pm[a] * r - pp.FEE_SIDE * abs(tgt - pm[a])) + nm[a] = tgt + pm = nm + out[t] = net_m + return out + + +def sezione2(args) -> dict: + sez("2) INTEGRITA' DEL MONITOR — `paper_prevday` e' davvero l'unico monitor sano?") + print(" L'audit §32 ha trovato che 4 monitor su 6 registrano una FRAZIONE del giorno") + print(" (`advance()` consuma la barra in corso e ci porta sopra `last_ts`). `paper_prevday`") + print(" risultava l'unico sano. Qui lo si VERIFICA invece di crederlo, in SOLA LETTURA.") + watched = [PAPER_DIR / "state.json", PAPER_DIR / "returns.jsonl", + PAPER_DIR / "trades.jsonl", ROOT / "src" / "strategies" / "prevday_breakout.py", + ROOT / "scripts" / "live" / "paper_prevday.py", ROOT / "config" / "live.json"] + fp0 = _fingerprint(watched) + + pp = _load_paper_module() + print(f" costanti DERIVATE dal monitor: WEIGHT={pp.WEIGHT} FEE_SIDE={pp.FEE_SIDE} " + f"MIN_ORDER=${pp.MIN_ORDER:.0f} cap MODELED=${pp.MODELED_CAPITAL:.0f} / " + f"REAL=${pp.REAL_CAPITAL:.0f}") + st = json.loads((PAPER_DIR / "state.json").read_text()) + rec = [json.loads(l) for l in (PAPER_DIR / "returns.jsonl").read_text().splitlines() if l.strip()] + ts = np.array([r["ts"] for r in rec], dtype=np.int64) + + sub("2a) copertura temporale: barre registrate contro ore trascorse") + span_h = (st["last_ts"] - st["start_ts"]) / 3_600_000.0 + dup = len(ts) - len(np.unique(ts)) + dts = np.diff(np.sort(ts)) / 3_600_000.0 + print(f" inizio {pd.Timestamp(st['start_ts'], unit='ms', tz='UTC')} " + f"ultima {pd.Timestamp(st['last_ts'], unit='ms', tz='UTC')}") + print(f" ore trascorse {span_h:,.0f} barre registrate {len(rec):,} duplicate {dup} " + f"passi != 1h {int((np.abs(dts - 1.0) > 1e-9).sum())}") + print(f" -> copertura NOMINALE {len(rec)/span_h:.4%} (statarb, per confronto §32: 4 min/g)") + + sub("2b) replay bit-a-bit: ogni barra registrata e' un'ORA VERA di mercato?") + dfs = {a: al.get(a, "1h") for a in ASSETS} + rep = _replay(pp, st, dfs) + common = [r for r in rec if r["ts"] in rep] + d = np.array([abs(r["net_modeled"] - round(rep[r["ts"]], 6)) for r in common]) + tol = 5e-7 # il monitor arrotonda a 6 decimali + ok = int((d <= tol).sum()) + print(f" barre confrontabili {len(common):,}/{len(rec):,} coincidenti (|diff|<={tol}) " + f"{ok:,} = {ok/max(len(common),1):.1%}") + n_days = max(span_h / 24.0, 1.0) + if len(common) - ok: + bad_ts = [r["ts"] for r, dd in zip(common, d) if dd > tol] + hh = pd.DatetimeIndex(pd.to_datetime(bad_ts, unit="ms", utc=True)) + vc = pd.Series(hh.hour).value_counts().sort_index() + print(f" divergenti {len(bad_ts):,} = {len(bad_ts)/len(common):.1%} su " + f"{n_days:.0f} giorni = {len(bad_ts)/n_days:.2f}/giorno") + print(" distribuzione per ora UTC: " + + " ".join(f"{int(h)}:00 x{int(c)}" for h, c in vc.items())) + print(" -> firma attesa e TROVATA: UNA barra al giorno, quella che il cron delle 00:30") + print(" consuma mentre e' IN CORSO. Il monitor registra ~30 min di quell'ora come se") + print(" fosse un'ora intera; i restanti ~30 min non entrano MAI (last_ts e' gia'") + print(" oltre). Le divergenze fuori dall'ora 0 sono giri partiti a un'ora diversa") + print(" (riavvii VPS, corse a mano) — stesso meccanismo, altro orario.") + print(f" ⚠️ ASSUNZIONE DICHIARATA: ~30 min persi per barra divergente (cron 00:30 +") + print(" rebuild). Non e' misurabile a posteriori — il feed di allora non esiste piu'.") + lost = len(bad_ts) / n_days * 30.0 + print(f" -> stima: {lost:.0f} min/giorno NON registrati = {1440-lost:.0f}/1440 " + f"({(1440-lost)/1440:.1%}) — contro i 4 min/giorno di `paper_statarb` (§32).") + frac_ok = ok / max(len(common), 1) + + sub("2c) controllo positivo obbligatorio — il confronto HA potenza?") + rep_bad = _replay(pp, st, dfs, corrupt_at=len(rep) // 2) + d2 = np.array([abs(r["net_modeled"] - round(rep_bad[r["ts"]], 6)) for r in common]) + n_bad = int((d2 > tol).sum()) + print(f" iniettando +1% di rendimento su UNA barra BTC: divergenti {n_bad:,} contro " + f"{len(common)-ok:,} del replay pulito -> il confronto {'SEGNALA' if n_bad > len(common)-ok else 'NON SEGNALA'}") + print(" (senza questo, 'coincide' e 'non sto confrontando niente' hanno lo stesso output)") + + sub("2d) `implausible_sharpe` sulla serie FORWARD — mai puntato sulle forward prima di §32") + fwd = pd.Series([r["net_modeled"] for r in rec], + index=pd.DatetimeIndex(pd.to_datetime([r["ts"] for r in rec], unit="ms", utc=True))) + dfwd = to_daily(fwd) + imp = al.implausible_sharpe(dfwd) + print(f" {imp}") + + fp1 = _fingerprint(watched) + assert fp0 == fp1, "VIOLAZIONE: un file sorvegliato e' cambiato durante l'audit" + print(f"\n {clock()} ✅ sola-lettura PROVATA: md5+mtime invariati su {len(fp0)} file sorvegliati") + return dict(st=st, rec=rec, fwd=fwd, dfwd=dfwd, frac_ok=frac_ok, span_h=span_h) + + +# ============================================================================================= +# 3) MDE — quanto e' distinguibile da zero un Sharpe forward su 63 giorni? +# ============================================================================================= +def _se_sharpe(sh: float, years: float) -> float: + """SE asintotico dello Sharpe annualizzato (Lo 2002, correzione per il 4o momento omessa): + SE ~ sqrt((1 + Sh^2/2)/T_anni). A Sh piccoli degenera nel sqrt(1/T) usato dal progetto.""" + return math.sqrt((1.0 + sh * sh / 2.0) / years) if years > 0 else float("nan") + + +def sezione3(M: dict) -> dict: + sez("3) MDE — la finestra forward ha potenza? (63 giorni a Sharpe +2,04)") + rec = M["rec"] + bpy = 24 * 365.25 + res = {} + print(" Due convenzioni di SE, entrambe pubblicate: `sqrt(1/T)` e' quella che il progetto") + print(" usa nelle sue note (DVOLSPREAD, BIN-FREQ); Lo (2002) `sqrt((1+Sh^2/2)/T)` e' la") + print(" corretta e a Sharpe alti e' PIU' SEVERA. La conclusione non dipende da quale si usa.") + print(f"\n {'libro':<12}{'barre':>8}{'anni':>8}{'Sharpe ann.':>13}{'SE naive':>10}{'t':>6}" + f"{'SE Lo':>8}{'t':>6}{'p (Lo)':>9}{'IC95 (Lo)':>20}") + for key, lab in (("net_modeled", "MODELED"), ("net_real", "REAL-$600")): + x = np.array([r[key] for r in rec], float) + yrs = len(x) / bpy + sh = float(x.mean() / x.std() * math.sqrt(bpy)) if x.std() > 0 else 0.0 + se_n = math.sqrt(1.0 / yrs) + se = _se_sharpe(sh, yrs) + t = sh / se + p = 2 * (1 - norm.cdf(abs(t))) + res[lab] = dict(sh=sh, se=se, se_naive=se_n, t=t, t_naive=sh / se_n, p=p, yrs=yrs) + print(f" {lab:<12}{len(x):>8,}{yrs:>8.3f}{sh:>13.2f}{se_n:>10.2f}{sh/se_n:>6.2f}" + f"{se:>8.2f}{t:>6.2f}{p:>9.3f} [{sh-1.96*se:+.2f}, {sh+1.96*se:+.2f}]") + print(f"\n 📌 L'IC95 dello Sharpe forward e' largo ~{2*1.96*res['MODELED']['se']:.1f} punti di") + print(" Sharpe. NON e' un numero che possa confermare o smentire un edge, con nessuna") + print(" delle due convenzioni (t = " + f"{res['MODELED']['t_naive']:.2f} naive / {res['MODELED']['t']:.2f} Lo, entrambi < 1).") + + sub("3b) ⚠️ e il '+2,04' pubblicato e' su una LENTE ORARIA, non su quella del progetto") + dsh = al._sh(M["dfwd"]) + print(f" Sharpe forward annualizzato dalle barre ORARIE (x sqrt(24*365,25)): " + f"{res['MODELED']['sh']:+.2f} <- il numero citato da §50") + print(f" Sharpe forward della stessa serie COMPOSTA A GIORNALIERA (x sqrt(365,25)): " + f"{dsh:+.2f}") + print(" Ogni altro numero di sleeve del progetto (TP01 1,29 · SKH01 1,45 · il libro 1,81)") + print(" e' sulla lente GIORNALIERA. Confrontare +2,04 con quelli mescola due lenti: il") + print(" numero comparabile e' " f"{dsh:+.2f}. (La differenza e' autocorrelazione oraria:") + print(" una posizione tenuta 24/7 non fa 24 scommesse indipendenti al giorno.)") + res["daily_sharpe"] = dsh + + sub("3a) quanto ci vuole? MDE (80% potenza, 5% bilaterale) per orizzonte") + print(f" {'anni':>7}{'giorni':>9}{'SE(Sh)':>9}{'MDE 80%':>10}" + f"{'P(Sh_oss>0 | vero 1.2)':>25}{'P(Sh_oss>0 | vero 0.6)':>25}") + for yrs in (res["MODELED"]["yrs"], 0.25, 0.5, 1.0, 2.0, 3.0, 5.0): + se0 = _se_sharpe(0.0, yrs) + mde = 2.802 * se0 + pw12 = norm.cdf(1.2 / _se_sharpe(1.2, yrs)) + pw06 = norm.cdf(0.6 / _se_sharpe(0.6, yrs)) + print(f" {yrs:>7.2f}{yrs*365.25:>9.0f}{se0:>9.2f}{mde:>10.2f}{pw12:>24.1%}{pw06:>24.1%}") + need12 = 2.802 ** 2 * (1 + 1.2 ** 2 / 2) / 1.2 ** 2 + need06 = 2.802 ** 2 * (1 + 0.6 ** 2 / 2) / 0.6 ** 2 + print(f"\n -> per DISTINGUERE uno Sharpe vero di 1,2 dal nulla all'80% di potenza servono " + f"{need12:.1f} anni; per 0,6 ne servono {need06:.1f}.") + print(" -> CONSEGUENZA PER IL GATE: nessuna soglia sullo Sharpe forward puo' essere") + print(" l'evidenza principale. Una soglia ALTA sarebbe finta precisione (stessa lezione") + print(" scritta per DVOLSPREAD); una soglia BASSA vale solo come KILL, perche' li' la") + print(" bassa potenza lavora nel verso giusto: si uccide solo cio' che e' molto negativo.") + print(f" -> P(un edge VIVO a Sharpe 1,2 venga ucciso da una soglia a {KILL_SHARPE:+.2f} " + f"a 1 anno) = {norm.cdf((KILL_SHARPE-1.2)/_se_sharpe(1.2,1.0)):.2%}") + return res + + +# ============================================================================================= +# 4) `study_family_honest` — selezione in-sample-only -> DSR -> marginal_vs_tp01 +# ============================================================================================= +def sezione4(grid: list, tfs: tuple) -> dict: + sez("4) LA FAMIGLIA ONESTA — la cella scelta AL BUIO e' quella che gira nel monitor?") + print(" Si usano i TRE componenti REALI di `study_family_honest` (importati, non riscritti):") + print(" `select_cell_insample` -> `deflated_sharpe` -> `study_marginal`. Sono chiamati in") + print(" sequenza invece che tramite il wrapper solo per non ricalcolare la griglia due") + print(" volte; l'IDENTITA' della composizione col wrapper e' verificata sotto (4d).") + + sub("4a) controllo di replica: la cella congelata E' `prevday_breakout.target`?") + for a in ASSETS: + df = al.get(a, "1h") + d1 = np.nan_to_num(np.asarray(pb.target(df), float)) + d2 = np.nan_to_num(np.asarray(frozen_fn()(df), float)) + md = float(np.max(np.abs(d1 - d2))) + print(f" {a}: max|factory(FROZEN) - pb.target| = {md:.1e} " + f"{'✅ bit-exact' if md == 0.0 else '❌ DIVERGE'}") + assert md == 0.0, "la factory non riproduce il segnale congelato" + cz = al.causality_ok(frozen_fn(), tf="1h") + print(f" causality_ok: {cz}") + + sub(f"4b) selezione IN-SAMPLE-ONLY su {len(grid)*len(tfs)} celle (pre-{HOLDOUT.date()})") + t = time.time() + sel = al.select_cell_insample(factory, grid, tfs) + ch = sel["chosen"] + print(f" {clock()} {len(sel['all_full_sharpe'])} celle valutate in {time.time()-t:.0f}s") + rows = sel["rows"] + fro = [r for r in rows if r["tf"] == FROZEN_TF and r["params"] == FROZEN] + rank_fro = rows.index(fro[0]) + 1 if fro else None + print(f"\n cella scelta AL BUIO : tf={ch['tf']} {ch['params']} " + f"Sh in-sample {ch['insample_sharpe']:+.3f} Sh FULL {ch['full_sharpe']:+.3f}") + if fro: + print(f" cella CONGELATA : tf={FROZEN_TF} {FROZEN} " + f"Sh in-sample {fro[0]['insample_sharpe']:+.3f} Sh FULL {fro[0]['full_sharpe']:+.3f}") + print(f" rango della congelata in-sample: {rank_fro}/{len(rows)}") + uguale = bool(fro and ch["tf"] == FROZEN_TF and ch["params"] == FROZEN) + print(f"\n ➜ LA CELLA AL BUIO {'COINCIDE' if uguale else 'NON COINCIDE'} CON QUELLA CHE GIRA.") + + print(f"\n {'top-10 IN-SAMPLE':<34}{'tf':>4}{'Sh IS':>9}{'Sh FULL':>10}") + for r in rows[:10]: + p = r["params"] + lab = f"anchor={p['anchor']} k={p['k']:.2f} short={int(p['short'])} mh={p['mh']}" + star = " <= CONGELATA" if (r["tf"] == FROZEN_TF and p == FROZEN) else "" + print(f" {lab:<34}{r['tf']:>4}{r['insample_sharpe']:>+9.3f}{r['full_sharpe']:>+10.3f}{star}") + + sub("4c) il candidato al buio passa `marginal_vs_tp01` e il deflated-Sharpe?") + fn = factory(tf=ch["tf"], **ch["params"]) + daily = al.candidate_daily(fn, tf=ch["tf"]) + sm = al.study_marginal(f"PREVDAY ISpick {ch['params']}", fn, tf=ch["tf"]) + dsr, sr0 = al.deflated_sharpe(al._sh(daily), sel["all_full_sharpe"], daily) + print(al.fmt_marginal(sm)) + print(f"\n deflated-Sharpe (N={len(sel['all_full_sharpe'])}, famiglia): {dsr:.3f} " + f"[soglia {GATE_DSR_MIN}] massimo atteso dal solo rumore {sr0:.3f}") + print(f" earns_slot_honest = {bool(sm['earns_slot'] and dsr >= GATE_DSR_MIN)}") + + sub("4d) IDENTITA': la mia composizione == `study_family_honest`?") + tiny = [dict(anchor=1, k=0.30, short=True, mh=0), dict(anchor=1, k=0.20, short=True, mh=0)] + w = al.study_family_honest("identita", factory, tiny, ("1h",)) + s2 = al.select_cell_insample(factory, tiny, ("1h",)) + c2 = s2["chosen"] + f2 = factory(tf=c2["tf"], **c2["params"]) + d2 = al.candidate_daily(f2, tf=c2["tf"]) + dsr2, _ = al.deflated_sharpe(al._sh(d2), s2["all_full_sharpe"], d2) + same = (w["chosen"]["params"] == c2["params"] and + abs((w["deflated_sharpe"] or 0) - round(dsr2, 3)) < 1e-9) + print(f" wrapper: cella {w['chosen']['params']} DSR {w['deflated_sharpe']} | " + f"composizione: {c2['params']} DSR {round(dsr2,3)} -> {'✅ IDENTICI' if same else '❌ DIVERGONO'}") + + # la cella CONGELATA, valutata con gli stessi strumenti (e' quella che gira nel monitor) + sub("4e) e la cella CONGELATA (quella che gira), giudicata con gli stessi strumenti") + dfroz = al.candidate_daily(frozen_fn(), tf=FROZEN_TF) + smf = al.study_marginal("PREVDAY CONGELATA", frozen_fn(), tf=FROZEN_TF) + dsrf, sr0f = al.deflated_sharpe(al._sh(dfroz), sel["all_full_sharpe"], dfroz) + print(f" Sh FULL {al._sh(dfroz):+.3f} Sh IS {al._sh(dfroz[dfroz.index=HOLDOUT]):+.3f} maxDD {100*al._dd_ret(dfroz):.1f}%") + print(f" abs={smf['abs_grade']} marginal={smf['marginal_verdict']} " + f"earns_slot={smf['earns_slot']} robust_oos={smf['marginal'].get('robust_oos')} " + f"is_hedge={smf['marginal'].get('is_hedge')}") + print(f" deflated-Sharpe (N={len(sel['all_full_sharpe'])}): {dsrf:.3f}") + print(f" ⚠️ la congelata NON e' selezionabile in-sample (rango {rank_fro}/{len(rows)}): il suo") + print(" DSR e' riportato per completezza, non come verdetto — il verdetto e' 4c.") + + sub("4f) LE DUE CELLE A CONFRONTO — non sono la stessa strategia") + mb, mf = sm["marginal"], smf["marginal"] + print(f" {'':<24}{'AL BUIO (in-sample)':>22}{'CONGELATA (monitor)':>22}") + rr = [("tf / parametri", f"{ch['tf']} k={ch['params']['k']:.2f} " + f"short={int(ch['params']['short'])}", + f"{FROZEN_TF} k={FROZEN['k']:.2f} short={int(FROZEN['short'])}"), + ("rango in-sample", f"1/{len(rows)}", f"{rank_fro}/{len(rows)}"), + ("Sharpe FULL", f"{al._sh(daily):+.3f}", f"{al._sh(dfroz):+.3f}"), + ("Sharpe HOLD-OUT", f"{al._sh(daily[daily.index>=HOLDOUT]):+.3f}", + f"{al._sh(dfroz[dfroz.index>=HOLDOUT]):+.3f}"), + ("maxDD", f"{100*al._dd_ret(daily):.1f}%", f"{100*al._dd_ret(dfroz):.1f}%"), + ("corr -> TP01 (full)", f"{mb.get('corr_full')}", f"{mf.get('corr_full')}"), + ("corr -> TP01 (hold)", f"{mb.get('corr_hold')}", f"{mf.get('corr_hold')}"), + ("beta a TP01", f"{mb.get('beta_to_tp01')}", f"{mf.get('beta_to_tp01')}"), + ("uplift w25 full/hold", + f"{mb['blends']['w25']['uplift_full']:+.3f}/{mb['blends']['w25']['uplift_hold']:+.3f}", + f"{mf['blends']['w25']['uplift_full']:+.3f}/{mf['blends']['w25']['uplift_hold']:+.3f}"), + ("deflated-Sharpe N=336", f"{dsr:.3f}", f"{dsrf:.3f}")] + for nm, a_, b_ in rr: + print(f" {nm:<24}{a_:>22}{b_:>22}") + print() + print(" 🚨 IL PUNTO, ed e' il risultato principale di questa sezione: la selezione onesta") + print(f" sceglie il libro LONG-FLAT (short={int(ch['params']['short'])}), mentre il monitor") + print(" gira il libro SIMMETRICO. Il docstring della scoperta dice l'OPPOSTO in chiaro:") + print(" «LONG-SHORT is the slot-earner ... the long-flat sibling correlates ~0.64 to TP01") + print(" (it just re-rides the bull) -> a much smaller marginal uplift».") + print(" La gamba SHORT — cioe' l'intera ragione per cui PREVDAY fu promosso a LEAD — e'") + print(" esattamente cio' che la selezione in-sample-only NON compra.") + print(" ⚠️ E il rovescio, che va detto: la cella al buio sta a corr " + f"{mb.get('corr_full')} da TP01 contro {mf.get('corr_full')} della congelata. Passa") + print(" `marginal_vs_tp01` (ADDS, alpha residua " + f"{mb.get('alpha_ann'):+.1%}/anno), ma e' molto piu' vicina a TP01: il candidato che") + print(" sopravvive alla selezione onesta e' anche quello che diversifica di meno.") + + sub("4g) `implausible_sharpe` su entrambe (retroattivo, come §26 su tutti gli sleeve)") + for nm, dd_ in (("AL BUIO", daily), ("CONGELATA", dfroz)): + i = al.implausible_sharpe(dd_) + print(f" {nm:<12}implausible={i['implausible']} Sharpe {i['sharpe']:.2f} " + f"maxDD {100*i['maxdd']:.1f}% perdite/attive {i['loss_frac']:.1%} " + f"Calmar {i['calmar']:.1f} reasons={i['reasons']}") + return dict(sel=sel, chosen=ch, daily=daily, sm=sm, dsr=dsr, sr0=sr0, + daily_frozen=dfroz, sm_frozen=smf, dsr_frozen=dsrf, + uguale=uguale, rank_frozen=rank_fro) + + +# ============================================================================================= +# 5) DSR — sensibilita' del verdetto al CONTEGGIO e alla PARTIZIONE +# ============================================================================================= +def _synth_trials(n: int, sd: float, center: float) -> np.ndarray: + """Array sintetico di n Sharpe con deviazione standard CAMPIONARIA esattamente `sd`. + Serve a riparametrizzare `al.deflated_sharpe` per (N, sd) usando la SUA formula, non una + mia copia. L'identita' col caso reale e' verificata in 5a.""" + z = np.linspace(-1.0, 1.0, n) + z = (z - z.mean()) / z.std(ddof=1) + return center + sd * z + + +def sezione5(S4: dict, F: dict) -> dict: + sez("5) DEFLATED-SHARPE — il verdetto dipende dal CONTEGGIO e dalla PARTIZIONE") + print(" Il 22/08 tre agenti indipendenti hanno misurato che `altlib.deflated_sharpe` non") + print(" dipende solo da N ma dalla VARIANZA della griglia (sr0 = sd(trial) x mult(N)): su") + print(" una famiglia omogenea passa quasi sempre, e la STESSA griglia da' verdetti opposti") + print(" secondo come la si partiziona. Qui la sensibilita' e' PUBBLICATA, non nascosta.") + sel, daily = S4["sel"], S4["daily"] + allsr = np.array([s for s in sel["all_full_sharpe"] if np.isfinite(s)], float) + sh = al._sh(daily) + rows = sel["rows"] + + sub("5a) controllo: la riparametrizzazione (N, sd) riproduce il caso reale?") + d_real, sr0_real = al.deflated_sharpe(sh, list(allsr), daily) + d_syn, sr0_syn = al.deflated_sharpe(sh, list(_synth_trials(len(allsr), allsr.std(ddof=1), + allsr.mean())), daily) + print(f" reale N={len(allsr)} sd={allsr.std(ddof=1):.3f} -> DSR {d_real:.4f} (sr0 {sr0_real:.3f})") + print(f" sint. N={len(allsr)} sd={allsr.std(ddof=1):.3f} -> DSR {d_syn:.4f} (sr0 {sr0_syn:.3f})") + print(f" scarto {abs(d_real-d_syn):.1e} {'✅' if abs(d_real-d_syn) < 1e-9 else '⚠️ non identici'}") + + sub("5b) PARTIZIONI della STESSA griglia — ognuna e' difendibile, e danno verdetti diversi") + def part(nome, pred): + sub_rows = [r for r in rows if pred(r)] + srs = [r["full_sharpe"] for r in sub_rows] + if len(srs) < 2: + return None + d, s0 = al.deflated_sharpe(sh, srs, daily) + return dict(nome=nome, n=len(srs), sd=float(np.std(srs, ddof=1)), dsr=d, sr0=s0) + giugno = [dict(anchor=1, k=0.30, short=True, mh=0), dict(anchor=1, k=0.50, short=True, mh=0), + dict(anchor=1, k=0.75, short=True, mh=0), dict(anchor=1, k=1.00, short=True, mh=0), + dict(anchor=2, k=0.30, short=True, mh=0), dict(anchor=3, k=0.30, short=True, mh=0), + dict(anchor=5, k=0.30, short=True, mh=0), dict(anchor=1, k=0.30, short=True, mh=24), + dict(anchor=1, k=0.30, short=True, mh=72), dict(anchor=2, k=0.75, short=True, mh=24), + dict(anchor=1, k=0.30, short=False, mh=0)] + parts = [ + part("N_giugno (le 11 celle gia' spese)", + lambda r: r["tf"] == "1h" and r["params"] in giugno), + part("solo l'asse k (anchor=1, mh=0, short)", + lambda r: r["tf"] == "1h" and r["params"]["anchor"] == 1 + and r["params"]["mh"] == 0 and r["params"]["short"]), + part("libro simmetrico, senza min-hold, 1h", + lambda r: r["tf"] == "1h" and r["params"]["mh"] == 0 and r["params"]["short"]), + part("tutto 1h", lambda r: r["tf"] == "1h"), + part("LA FAMIGLIA DICHIARATA (1h + 4h)", lambda r: True), + ] + print(f" {'partizione':<40}{'N':>5}{'sd(Sh)':>9}{'sr0':>8}{'DSR':>9}{'verdetto':>10}") + for p in parts: + if p is None: + continue + print(f" {p['nome']:<40}{p['n']:>5}{p['sd']:>9.3f}{p['sr0']:>8.3f}{p['dsr']:>9.3f}" + f"{('PASS' if p['dsr'] >= GATE_DSR_MIN else 'FAIL'):>10}") + + sub("5c) sr0 di SCREEN — non basta contare i trial, serve la VARIANZA di screen") + lb = F["lb"] + scr = np.array([x["abs_full_sharpe"] for x in lb], float) + print(f" i {len(scr)} agenti dell'onda intraday 2026-06-21: Sharpe da {scr.min():.2f} a " + f"{scr.max():.2f}, media {scr.mean():.2f}, sd {scr.std(ddof=1):.3f}") + print(" ⚠️ lente DICHIARATA: `abs_full_sharpe` del leaderboard e' la Sharpe per-asset del") + print(" giudice di giugno, non la 50/50 giornaliera di `candidate_daily`. Serve per la") + print(" sua DISPERSIONE, non per il livello.") + mix = np.concatenate([allsr, scr]) + d_scr, s0_scr = al.deflated_sharpe(sh, list(mix), daily) + print(f" famiglia+screen: N={len(mix)} sd={mix.std(ddof=1):.3f} sr0={s0_scr:.3f} " + f"DSR={d_scr:.3f} {'PASS' if d_scr >= GATE_DSR_MIN else 'FAIL'}") + + sub("5d) la superficie DSR(N, sd) — dove sta il verdetto, e quanto e' fragile") + sds = (0.15, 0.20, 0.25, 0.30, 0.40, 0.58) + ns = (11, 56, 168, 336, 504, 456, 560) + print(" " + "N su sd".rjust(8) + "".join(f"{s:>9.2f}" for s in sds)) + for n in sorted(ns): + cells = [] + for s in sds: + dd, _ = al.deflated_sharpe(sh, list(_synth_trials(n, s, float(allsr.mean()))), daily) + cells.append(f"{dd:>9.3f}") + print(f" {n:>8}" + "".join(cells)) + print(" (sd=0,58 e' il valore implicito dell'ORTHO-SCREEN del 22/08; la riga N=456 e' la") + print(f" famiglia+screen di 5c. Soglia {GATE_DSR_MIN}.)") + + sub("5e) ➜ IL VERDETTO NON SI RIBALTA COL CONTEGGIO. Si ribalta con la VARIANZA.") + lo_, hi_ = 0.05, 3.0 + for _ in range(60): + mid = 0.5 * (lo_ + hi_) + dd, _s = al.deflated_sharpe(sh, list(_synth_trials(len(allsr), mid, + float(allsr.mean()))), daily) + if dd >= GATE_DSR_MIN: + lo_ = mid + else: + hi_ = mid + print(f" A N={len(allsr)} il DSR scende sotto {GATE_DSR_MIN} solo per sd(trial) >= " + f"{0.5*(lo_+hi_):.3f}.") + print(f" La famiglia ne ha {allsr.std(ddof=1):.3f}: servirebbe una dispersione " + f"{0.5*(lo_+hi_)/allsr.std(ddof=1):.1f}x per bocciarla.") + print(" Portare N da 11 a 560 costa " + + f"{al.deflated_sharpe(sh, list(_synth_trials(11, allsr.std(ddof=1), float(allsr.mean()))), daily)[0] - al.deflated_sharpe(sh, list(_synth_trials(560, allsr.std(ddof=1), float(allsr.mean()))), daily)[0]:.3f}" + " di DSR: il conteggio, su questa famiglia, NON e' la leva.") + print(" ⚠️ ATTESA A PRIORI A2 REFUTATA. Avevo previsto che il verdetto si ribaltasse col") + print(" conteggio (come VRP01 il 30/07: 0,983 a N=8 / 0,948 a N=72). Qui NON succede, e") + print(" il motivo e' MISURABILE: la famiglia PREVDAY e' OMOGENEA (le sue celle si") + print(" assomigliano, sd 0,27) mentre la griglia VRP01 e la griglia ORTHO erano") + print(" eterogenee (sd ~0,58). `sr0 = sd(trial) x mult(N)` e mult(N) cresce come una") + print(" radice di log: da N=11 a N=560 vale ~1,8x, mentre sd puo' variare 4x fra") + print(" famiglie. ➜ REGOLA: il deflated-Sharpe di una famiglia OMOGENEA e' quasi") + print(" INSENSIBILE al numero di trial — dichiarare 11 celle o 560 cambia 0,009. Chi") + print(" discute il CONTEGGIO su una famiglia stretta sta discutendo la variabile") + print(" sbagliata; l'unica leva e' la DISPERSIONE, che si sceglie decidendo dove") + print(" mettere il bordo della famiglia. ⚠️ Cio' NON vuol dire che il gate sia vacuo:") + print(" 5f mostra che ha potenza sul LIVELLO (la cella mediana della stessa famiglia") + print(" FALLISCE). E' cieco su un asse e vede sull'altro — e il 30/07 il verdetto su") + print(" VRP01 si ribaltava proprio perche' quella famiglia era larga.") + + sub("5f) controllo positivo del gate — passerebbe anche una cella MEDIOCRE?") + med = float(np.median(allsr)) + d_med, _ = al.deflated_sharpe(med, list(allsr), daily) + d_lo, _ = al.deflated_sharpe(float(np.percentile(allsr, 25)), list(allsr), daily) + print(f" candidato con Sharpe = MEDIANA della famiglia ({med:+.3f}): DSR {d_med:.3f} " + f"-> {'PASS' if d_med >= GATE_DSR_MIN else 'FAIL'}") + print(f" candidato con Sharpe = 25o pctl della famiglia ({np.percentile(allsr,25):+.3f}): " + f"DSR {d_lo:.3f} -> {'PASS' if d_lo >= GATE_DSR_MIN else 'FAIL'}") + print(" (approssimazione DICHIARATA: si tiene la serie del vincitore per skew/kurt/T e si") + print(" sostituisce solo lo Sharpe. Se anche la mediana passa, il gate su questa famiglia") + print(" non sta selezionando: sta contando.)") + return dict(parts=[p for p in parts if p], dsr_screen=d_scr, allsr=allsr, sh=sh, + sd_soglia=0.5 * (lo_ + hi_), dsr_mediana=d_med) + + +# ============================================================================================= +# 6) `day_boundary_robust` — il segnale VIVE sul confine del giorno UTC +# ============================================================================================= +def _hour_of_day_signal(df: pd.DataFrame) -> np.ndarray: + """CONTROLLO POSITIVO: segnale PURAMENTE di calendario (long la prima meta' del giorno UTC, + short la seconda). Deve INVERTIRSI spostando il confine di 12h. Se non si inverte, il test + non ha potenza e il verdetto su PREVDAY non vale niente.""" + h = pd.to_datetime(df["datetime"], utc=True).dt.hour.values + d = np.where(h < 12, 1.0, -1.0) + return np.nan_to_num(pb._vol_target(d, df, pb.TARGET_VOL, pb.VOL_WIN_DAYS, pb.LEV_CAP), nan=0.0) + + +def sezione6() -> dict: + sez("6) `day_boundary_robust` — PREVDAY e' un segnale di prezzo o di ETICHETTA?") + print(" Il livello di PREVDAY E' fissato dal rollover di mezzanotte UTC: e' esattamente la") + print(" classe di segnali per cui questo gate e' nato (open_drive, 2026-06-21). Il") + print(" leaderboard di giugno lo dichiara ROBUST con spread 0,196 — cioe' NON invariante.") + print(" Qui si ri-misura invece di fidarsi, agli offset richiesti +-2 / +-4 / +-8 ore.") + + sub("6a) controllo positivo — il test HA potenza?") + ctl = al.day_boundary_robust(_hour_of_day_signal, tf="1h", offsets=(0, 6, 12, 18)) + print(f" segnale puramente di calendario (long h<12): {ctl['per_offset']} " + f"spread {ctl['spread']} verdetto {ctl['verdict']}") + ok_ctl = ctl["verdict"] == "ARTIFACT-RISK" + print(f" {'✅ il gate lo SMASCHERA' if ok_ctl else '⚠️ il gate NON lo smaschera'} " + f"(atteso ARTIFACT-RISK: a +12h il segnale e' il proprio opposto)") + + sub("6b) PREVDAY congelato, offset richiesti") + r = al.day_boundary_robust(frozen_fn(), tf="1h", offsets=(-8, -4, -2, 0, 2, 4, 8)) + for o in sorted(r["per_offset"]): + print(f" {o:>+4}h uplift w25 {r['per_offset'][o]:+.3f}") + print(f" a 0h {r['per_offset'][0]:+.3f} min {r['min']:+.3f} max {r['max']:+.3f} " + f"spread {r['spread']:.3f} verdetto {r['verdict']}") + print(f" ⚠️ il campo `base` di altlib e' per_offset[offsets[0]] = l'offset {(-8):+d}h, NON lo") + print(" zero: con una griglia che comincia in negativo chiamarlo 'base' inganna.") + print(f" -> l'uplift {'NON cambia segno' if r['robust_to_boundary'] else 'CAMBIA SEGNO'}: " + f"{'non e' + chr(39) + ' un artefatto di etichettatura' if r['robust_to_boundary'] else 'ARTEFATTO'}") + + sub("6c) griglia oraria PIENA: 24 confini-giorno (standalone + uplift)") + B = al.tp01_baseline_daily() + per = {} + for off in PV_BOUNDARY: + series = {} + for a in ASSETS: + df0 = al.get(a, "1h") + tgt = frozen_fn()(al._shift_calendar(df0, off) if off else df0) + ev = al.eval_weights(df0, tgt) + series[a] = pd.Series(ev["net"], index=ev["idx"]) + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + cand = to_daily(pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)) + JJ = pd.concat({"B": B, "C": cand}, axis=1, join="inner").dropna() + per[off] = dict(full=al._sh(cand), hold=al._sh(cand[cand.index >= HOLDOUT]), + dd=al._dd_ret(cand), + upl=al._sh(0.75 * JJ["B"] + 0.25 * JJ["C"]) - al._sh(JJ["B"]), + daily=cand) + fu = np.array([per[o]["full"] for o in PV_BOUNDARY]) + ho = np.array([per[o]["hold"] for o in PV_BOUNDARY]) + up = np.array([per[o]["upl"] for o in PV_BOUNDARY]) + dd = np.array([per[o]["dd"] for o in PV_BOUNDARY]) + print(f" {'metrica':<16}{'canonico (0h)':>15}{'pctl':>7}{'MEDIANA onesta':>17}" + f"{'[p10, p90]':>20}{'fortuna':>10}") + for nm, v in (("Sharpe FULL", fu), ("Sharpe HOLD-OUT", ho), ("uplift w25", up)): + pc = float((v < v[0]).mean() * 100) + print(f" {nm:<16}{v[0]:>+15.3f}{pc:>6.0f}°{np.median(v):>+17.3f}" + f" [{np.percentile(v,10):+.3f}, {np.percentile(v,90):+.3f}]{v[0]-np.median(v):>+10.3f}") + pcd = float((dd > dd[0]).mean() * 100) + print(f" {'maxDD':<16}{100*dd[0]:>14.1f}%{pcd:>6.0f}°{100*np.median(dd):>16.1f}%" + f" [{100*np.percentile(dd,10):.1f}%, {100*np.percentile(dd,90):.1f}%]" + f"{100*(dd[0]-np.median(dd)):>+9.1f}") + print(" (pctl = quante ancore stanno SOTTO la canonica; per il maxDD, quante stanno sopra") + print(" cioe' peggio. `anchor_luck_band` di altlib fa lo stesso conto: la mediana e' la") + print(" stima onesta, la differenza canonico-mediana e' la fortuna.)") + return dict(req=r, per=per, ctl_ok=ok_ctl) + + +# ============================================================================================= +# 7) BANDA D'ANCORA DEL LIBRO — differenze APPAIATE sullo spazio congiunto +# ============================================================================================= +def prevday_daily_off(off: int, rate: dict | None) -> pd.Series: + """Serie giornaliera 50/50 di PREVDAY al confine-giorno `off`, col FUNDING dentro. + Replica ESATTA di r0823_book_3rd.prevday() a off=0 (verificata bit-exact in 7a).""" + series = {} + for a in ASSETS: + df = al.get(a, "1h") + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + tgt = np.nan_to_num(np.asarray( + frozen_fn()(al._shift_calendar(df, off) if off else df), float)) + r = al.simple_returns(df["close"].values.astype(float)) + pos = np.zeros(len(tgt)) + pos[1:] = tgt[:-1] + net = pos * r - FEE_SIDE * np.abs(np.diff(pos, prepend=0.0)) + if rate is not None: + t = FU.to_ms(idx) + fh = FU.cumF(rate[a], t + FU.MSH) - FU.cumF(rate[a], t) + net = net - pos * fh + net[0] = 0.0 + series[a] = pd.Series(net, index=idx) + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + return to_daily(pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)) + + +def _stats(s: pd.Series): + m = metrics(s) + mh = metrics(s[s.index >= HOLDOUT]) + return m["sharpe"], mh["sharpe"], m["cagr"], m["maxdd"] + + +def sezione7(rate: dict, draws: int) -> dict: + sez("7) BANDA D'ANCORA DEL LIBRO — §50 ha de-luckato TP01 e SKH01, NON il candidato") + print(" §50 ha estratto TP01 x24 ancore e SKH01 x23 offset e ha tenuto PREVDAY FERMO al suo") + print(" confine canonico (`ANCHOR_OF['PREVDAY'] = (0,)`). Ma PREVDAY UN'ANCORA CE L'HA — e'") + print(" l'ora del rollover che fissa il livello — e la sezione 6c la misura fortunata. Un") + print(" delta e' un delta: eredita la fortuna dell'ancora (lezione 26/07). Qui lo spazio") + print(f" congiunto e' TP01 x{len(TP_HOURS)} · SKH01 x{len(SKH_OFFSETS)} · PREVDAY x{len(PV_BOUNDARY)} " + f"= {len(TP_HOURS)*len(SKH_OFFSETS)*len(PV_BOUNDARY):,} configurazioni.") + + if not B3RD_CACHE.exists(): + print(f" ⚠️ cache §50 assente ({B3RD_CACHE}) -> sezione 7 SALTATA (non e' un risultato).") + return {} + S = pickle.loads(B3RD_CACHE.read_bytes()) + + sub("7a) sanity obbligatorio: la mia PREVDAY a off=0 e' quella di §50?") + p0 = prevday_daily_off(0, rate) + ref = S["PREVDAY"][0] + j = pd.concat({"a": p0, "b": ref}, axis=1, join="inner") + md = float(np.max(np.abs(j["a"] - j["b"]))) + print(f" max|mia - §50| = {md:.2e} su {len(j):,} giorni " + f"{'✅ bit-exact (e conferma che la cache ha il funding dentro)' if md < 1e-12 else '⚠️ DIVERGE'}") + + t = time.time() + PV = {o: (p0 if o == 0 else prevday_daily_off(o, rate)) for o in PV_BOUNDARY} + print(f" {clock()} PREVDAY x{len(PV_BOUNDARY)} confini-giorno costruita ({time.time()-t:.0f}s)") + + sub(f"7b) {draws} estrazioni CONGIUNTE — mediana delle DIFFERENZE APPAIATE") + rng = np.random.default_rng(SEED) + hs = rng.choice(TP_HOURS, size=draws) + os_ = rng.choice(SKH_OFFSETS, size=draws) + bs_ = rng.choice(PV_BOUNDARY, size=draws) + out = {w: {"sh": [], "shh": [], "cagr": [], "dd": []} for w in WEIGHTS_TESTED} + out_can = {w: {"sh": [], "shh": []} for w in WEIGHTS_TESTED} # candidato FERMO a off=0 + base = [] + for i in range(draws): + cols2 = {"TP01": S["TP01"][hs[i]], "SKH01": S["SKH01"][os_[i]]} + b = combine_outer(cols2, {"TP01": W_TP, "SKH01": W_SKH}, lo=BOOK_START) + bst = _stats(b) + base.append(bst) + for w in WEIGHTS_TESTED: + wm = {"TP01": W_TP * (1 - w), "SKH01": W_SKH * (1 - w), "CAND": w} + a = combine_outer({**cols2, "CAND": PV[bs_[i]]}, wm, lo=BOOK_START) + st = _stats(a) + out[w]["sh"].append(st[0] - bst[0]); out[w]["shh"].append(st[1] - bst[1]) + out[w]["cagr"].append(st[2] - bst[2]); out[w]["dd"].append(st[3] - bst[3]) + c = combine_outer({**cols2, "CAND": PV[0]}, wm, lo=BOOK_START) + sc = _stats(c) + out_can[w]["sh"].append(sc[0] - bst[0]); out_can[w]["shh"].append(sc[1] - bst[1]) + B = np.array(base) + print(f" libro live de-luckato (2 sleeve): ShFULL {np.median(B[:,0]):.3f} " + f"ShHOLD {np.median(B[:,1]):.3f} CAGR {100*np.median(B[:,2]):.2f}% " + f"maxDD {100*np.median(B[:,3]):.2f}%") + print(f"\n {'peso':>6}{'dShFULL':>11}{'>0':>6}{'[p10,p90]':>19}{'dShHOLD':>11}{'>0':>6}" + f"{'d CAGR pp':>11}{'d maxDD pp':>12}{' | §50 (cand. FERMO a 0h)':>28}") + res = {} + for w in WEIGHTS_TESTED: + a = np.array(out[w]["sh"]); ah = np.array(out[w]["shh"]) + ac = np.array(out[w]["cagr"]); ad = np.array(out[w]["dd"]) + c = np.array(out_can[w]["sh"]); ch = np.array(out_can[w]["shh"]) + res[w] = dict(sh=float(np.median(a)), sh_pos=float((a > 0).mean()), + shh=float(np.median(ah)), shh_pos=float((ah > 0).mean()), + cagr=float(np.median(ac)), dd=float(np.median(ad)), + sh_can=float(np.median(c)), shh_can=float(np.median(ch))) + print(f" {w:>5.0%}{np.median(a):>+11.4f}{100*(a>0).mean():>5.0f}%" + f" [{np.percentile(a,10):+.3f},{np.percentile(a,90):+.3f}]" + f"{np.median(ah):>+11.4f}{100*(ah>0).mean():>5.0f}%" + f"{100*np.median(ac):>+11.2f}{100*np.median(ad):>+12.2f}" + f" FULL {np.median(c):+.3f} HOLD {np.median(ch):+.3f}") + w = W_PROPOSTO + print("\n ✅ REPLICA INDIPENDENTE DI §50: la colonna 'cand. FERMO a 0h' e' lo stesso conto") + print(f" che ha fatto §50 (candidato all'ancora canonica). Al 15% da' FULL " + f"{res[w]['sh_can']:+.3f} / HOLD {res[w]['shh_can']:+.3f} contro i +0,192 / +0,363") + print(" pubblicati -> macchineria e cache riprodotte, il confronto e' like-with-like.") + print(f"\n ➜ AL PESO PROPOSTO ({w:.0%}), de-luckare ANCHE IL CANDIDATO costa " + f"{res[w]['sh']-res[w]['sh_can']:+.4f} di dShFULL e " + f"{res[w]['shh']-res[w]['shh_can']:+.4f} di dShHOLD:") + print(f" dShFULL {res[w]['sh_can']:+.3f} -> {res[w]['sh']:+.3f} " + f"(-{100*(1-res[w]['sh']/res[w]['sh_can']):.0f}%) " + f"dShHOLD {res[w]['shh_can']:+.3f} -> {res[w]['shh']:+.3f} " + f"(-{100*(1-res[w]['shh']/res[w]['shh_can']):.0f}%)") + print(" 📌 Il contributo RESTA positivo al 100% delle estrazioni a ogni peso: il segno non") + print(" dipende dall'ancora, la TAGLIA si'. E' la stessa forma del 22/08 su GATE PROP-01:") + print(" «un gate superato non e' un numero confermato».") + return res + + +# ============================================================================================= +# 8) FUNDING — un long/short vol-targeted su perpetual: paga o incassa? +# ============================================================================================= +def sezione8(rate: dict) -> dict: + sez("8) FUNDING — il costo di TENERE APERTA una posizione, che nessun backtest storico ha") + print(" Il 22/08 ha misurato che il libro paga -2,16%/anno di drift e che ESPOSIZIONE e") + print(" FUNDING sono positivamente correlati (il tasso pagato davvero e' 1,9-3,4x") + print(" l'incondizionato). PREVDAY e' long-short: la gamba SHORT lo INCASSA. Quale vince?") + out = {} + print(f"\n {'asset':>6}{'%long':>8}{'%short':>8}{'|pos| medio':>12}{'incond./anno':>14}" + f"{'| cond. LONG':>14}{'rapp.':>7}{'| cond. SHORT':>15}{'rapp.':>7}{'NETTO/anno':>12}") + legs = {} + for a in ASSETS: + df = al.get(a, "1h") + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + tgt = np.nan_to_num(np.asarray(frozen_fn()(df), float)) + pos = np.zeros(len(tgt)); pos[1:] = tgt[:-1] + t = FU.to_ms(idx) + fh = FU.cumF(rate[a], t + FU.MSH) - FU.cumF(rate[a], t) + yrs = len(pos) / (24 * 365.25) + ser = rate[a]["series"] + ser = ser[(ser.index >= idx.min()) & (ser.index <= idx.max())] + unc = float(ser.mean()) * 24 * 365.25 + pl = np.maximum(pos, 0.0); ps = -np.minimum(pos, 0.0) + cl = float(np.sum(pl * fh) / pl.sum()) * 24 * 365.25 if pl.sum() > 0 else float("nan") + cs = float(np.sum(ps * fh) / ps.sum()) * 24 * 365.25 if ps.sum() > 0 else float("nan") + net = float(np.sum(pos * fh)) / yrs + legs[a] = dict(pos=pos, fh=fh, idx=idx, net=net, unc=unc, cl=cl, cs=cs) + print(f" {a:>6}{float((pos>0).mean()):>8.1%}{float((pos<0).mean()):>8.1%}" + f"{float(np.abs(pos).mean()):>12.3f}{unc:>14.2%}{cl:>14.2%}{cl/unc:>6.2f}x" + f"{cs:>15.2%}{cs/unc:>6.2f}x{net:>12.3%}") + sleeve = float(np.mean([legs[a]["net"] for a in ASSETS])) + out["sleeve_drag"] = sleeve + print(f" -> drag NETTO dello sleeve (le due gambe pesano 0,5 ciascuna): {sleeve:.3%}/anno") + print(f" -> al peso di libro proposto ({W_PROPOSTO:.0%}): {W_PROPOSTO*sleeve:.3%}/anno di libro") + + print("\n 📌 MECCANISMO: se `cond. LONG` e `cond. SHORT` sono ENTRAMBI sopra l'incondizionato,") + print(" le due gambe NON si compensano — la lunga paga caro E la corta incassa caro, e il") + print(" netto dipende da quanto tempo sta da ciascun lato. Se invece il rapporto della") + print(" corta e' < 1, la corta incassa POCO e il costo resta tutto sulla lunga.") + + sub("8b) l'effetto sui numeri di PREVDAY (unico grado di liberta': il funding)") + a0 = prevday_daily_off(0, None) + a1 = prevday_daily_off(0, rate) + print(f" {'lente':<22}{'Sh FULL':>10}{'Sh HOLD':>10}{'drift/anno':>13}{'maxDD':>9}") + for nm, s in (("senza funding", a0), ("CON funding", a1)): + r = np.asarray(s.dropna().values, float) + print(f" {nm:<22}{al._sh(s):>+10.3f}{al._sh(s[s.index>=HOLDOUT]):>+10.3f}" + f"{r.mean()*365.25:>12.2%}{100*al._dd_ret(s):>8.1f}%") + r0 = np.asarray(a0.dropna().values, float).mean() * 365.25 + r1 = np.asarray(a1.dropna().values, float).mean() * 365.25 + print(f" -> il funding costa a PREVDAY {r0-r1:.2%}/anno di drift e " + f"{al._sh(a0)-al._sh(a1):+.3f} di Sharpe FULL") + out["d_drift"] = r0 - r1 + return out + + +# ============================================================================================= +# 9) `weights_tilt_null` — il gate del progetto per ogni cambio di pesi +# ============================================================================================= +def sezione9(rate: dict) -> dict: + sez("9) `weights_tilt_null` — e la colonna che conta non e' il PASS") + if not B3RD_CACHE.exists(): + print(" cache §50 assente -> sezione SALTATA") + return {} + S = pickle.loads(B3RD_CACHE.read_bytes()) + cols = {"TP01": S["TP01"][0], "SKH01": S["SKH01"][0], "PREVDAY": prevday_daily_off(0, rate)} + k_seen = 7 * 4 + len(WEIGHTS_TESTED) # §50: 7 candidati x 4 pesi; qui: 4 pesi + print(f" k_seen dichiarato = {k_seen} (le {7*4} configurazioni guardate sull'hold-out in §50") + print(f" piu' le {len(WEIGHTS_TESTED)} di oggi: i pesi gia' visti si CONTANO, non si azzerano)") + w_cur = {"TP01": W_TP, "SKH01": W_SKH, "PREVDAY": 0.0} + print(f"\n {'peso':>6}{'d_insample':>13}{'d_hold':>10}{'d_full':>10}{'pctl_hold':>11}" + f"{'best-of-k':>11}{'rnd>cur':>10}{'GATE':>8}") + res = {} + for w in WEIGHTS_TESTED: + wp = {"TP01": W_TP * (1 - w), "SKH01": W_SKH * (1 - w), "PREVDAY": w} + r = weights_tilt_null(cols, w_cur, wp, floor=0.03, n=300, k_seen=k_seen) + res[w] = r + print(f" {w:>5.0%}{r['delta_insample']:>+13.4f}{r['delta_hold']:>+10.4f}" + f"{r['delta_full']:>+10.4f}{r['pctl_hold']:>10.1f}°{r['bestofk_pctl']:>10.1f}°" + f"{r['frac_random_beat_hold']:>10.2f}{('PASS' if r['gate_pass'] else 'FAIL'):>8}") + fr = res[W_PROPOSTO]["frac_random_beat_hold"] + print(f"\n ⚠️ `rnd>cur` = {fr:.2f}: e' la frazione di tilt CASUALI a 3 sleeve che battono il") + print(" libro attuale sull'hold-out. Dove vale ~0,9 il claim «PREVDAY migliora") + print(" l'hold-out» e' GENERICO: lo fa quasi qualunque terzo sleeve, e il gate passa") + print(" proprio perche' la proposta NON e' eccezionale fra i tilt casuali.") + print(" `weights_tilt_null` e' NECESSARIO, non sufficiente (sta nel suo docstring).") + return res + + +# ============================================================================================= +# 10) IL GATE PRE-REGISTRATO +# ============================================================================================= +def sezione10(S4: dict, S5: dict, S6: dict, S7: dict, S8: dict, S9: dict, + M: dict, MDE: dict) -> None: + sez("10) IL GATE PRE-REGISTRATO — data, soglie dichiarate PRIMA, veto, kill") + inizio = pd.Timestamp(M["st"]["start_ts"], unit="ms", tz="UTC") + OGGI = pd.Timestamp.now("UTC").date() + d1 = pd.Timestamp("2027-06-21", tz="UTC") # 1 anno di forward dall'inception + ndecl = len(S4["sel"]["all_full_sharpe"]) + print(f" Inception del monitor: {inizio.date()}. Oggi: {OGGI}.") + print(f" Finestra forward attuale: {M['span_h']/24:.0f} giorni = " + f"{M['span_h']/24/365.25:.2f} anni (sez. 3: SE(Sharpe) ~ " + f"{_se_sharpe(MDE['MODELED']['sh'], MDE['MODELED']['yrs']):.1f}).") + + W = 86 + + def bx(t: str = "") -> None: + print(" |" + t.ljust(W) + "|") + + print(" +" + "-" * W + "+") + bx(" GATE PREVDAY-01".ljust(W)) + bx() + bx(f" DATA DI DECISIONE: {d1.date()}") + bx(" (= 1 anno esatto di forward dall'inception. La data NON e' scelta perche' a 1") + bx(" anno la finestra diventi informativa — la sez. 3 misura che NON lo diventa —") + bx(" ma perche' oltre quell'orizzonte tenere un lead senza decidere e' esso stesso") + bx(" una decisione. La prova che conta e' gia' tutta OGGI e non migliora aspettando.)") + bx() + bx(" SI PROMUOVE a sleeve del LIBRO LIVE al peso 15% solo se TUTTE:") + bx(f" (a) deflated-Sharpe sulla famiglia DICHIARATA (N={ndecl}) >= {GATE_DSR_MIN}, con la") + bx(" griglia ri-dichiarata PRIMA di guardare e la sensibilita' alla PARTIZIONE") + bx(" pubblicata accanto (sez. 5): se cambia la partizione, cambia il verdetto.") + bx(" (b) la cella scelta IN-SAMPLE-ONLY coincide con quella CONGELATA nel monitor,") + bx(" oppure il monitor viene ri-congelato su quella al buio e il forward") + bx(" RIPARTE DA ZERO da quel giorno (non si eredita il track di un'altra cella).") + bx(f" (c) delta di libro APPAIATO sullo spazio d'ancora CONGIUNTO (TP01 x{len(TP_HOURS)} ·") + bx(f" SKH01 x{len(SKH_OFFSETS)} · PREVDAY x{len(PV_BOUNDARY)}) > {GATE_ANCHOR_DELTA_MIN:+.2f} di dShFULL al peso 15%,") + bx(f" positivo in >= {GATE_ANCHOR_FRAC_MIN:.0%} delle estrazioni, col FUNDING dentro.") + bx(" (d) `weights_tilt_null` gate_pass=True al peso proposto.") + bx(" (e) `marginal_vs_tp01` = ADDS, robust_oos, non-hedge SULLA CELLA AL BUIO.") + bx(" (f) `day_boundary_robust` != ARTIFACT-RISK sulla cella al buio.") + bx(" (g) Sharpe forward > 0 — soglia DEBOLE DI PROPOSITO: a 1 anno SE(Sharpe)~1,0 e") + bx(" una soglia alta sarebbe finta precisione (stessa scelta di DVOLSPREAD).") + bx() + bx(" VETO D'INTEGRITA' (si valuta PRIMA del resto e BLOCCA la decisione):") + bx(f" quota di barre forward RICOSTRUIBILI dal feed certificato >= {GATE_INTEGRITY_MIN:.0%} (sez. 2b),") + bx(" e i minuti di mercato NON registrati per giorno non devono CRESCERE rispetto a") + bx(" oggi. Sotto la soglia il gate SI ESTENDE, non si decide: una serie che non si") + bx(" ricostruisce non e' evidenza ne' a favore ne' contro.") + bx() + bx(f" KILL: Sharpe forward < {KILL_SHARPE:+.2f} su >= 180 giorni di barre ATTIVE (non di") + bx(" calendario). A quell'orizzonte la bassa potenza lavora nel verso giusto: si") + bx(" uccide solo cio' che e' molto negativo. Al kill il lead si chiude e la famiglia") + bx(" NON si ri-ottimizza (riaprirla dopo aver visto il forward e' selezione-") + bx(" sull'hold-out di secondo ordine).") + bx() + bx(" COSA NON SPOSTA IL GATE: un forward positivo. La sez. 3 misura che a 1 anno un") + bx(" Sharpe forward di +2 e uno di 0 non sono distinguibili. Il forward serve a") + bx(" UCCIDERE, non a promuovere.") + print(" +" + "-" * W + "+") + + sub("STATO OGGI delle condizioni — e' una FOTOGRAFIA, non anticipa la decisione") + W_ = W_PROPOSTO + rows = [ + ("(a) DSR famiglia dichiarata", f"{S4['dsr']:.3f} (N={ndecl})", + S4["dsr"] >= GATE_DSR_MIN), + ("(a') DSR con sr0 di SCREEN", f"{S5['dsr_screen']:.3f}", + S5["dsr_screen"] >= GATE_DSR_MIN), + ("(a'') DSR della cella CONGELATA", f"{S4['dsr_frozen']:.3f} (non e' la cella di (a))", + S4["dsr_frozen"] >= GATE_DSR_MIN), + ("(b) cella al buio == congelata", "SI" if S4["uguale"] else + f"NO (la congelata e' {S4['rank_frozen']}/{len(S4['sel']['rows'])} in-sample)", + S4["uguale"]), + ("(c) delta libro appaiato de-luckato", + (f"{S7[W_]['sh']:+.4f} FULL, >0 nel {S7[W_]['sh_pos']:.0%}") if S7 else "n/d", + bool(S7 and S7[W_]["sh"] > GATE_ANCHOR_DELTA_MIN + and S7[W_]["sh_pos"] >= GATE_ANCHOR_FRAC_MIN)), + ("(d) weights_tilt_null", ("PASS" if S9 and S9[W_]["gate_pass"] else "FAIL") + + (f" (rnd>cur {S9[W_]['frac_random_beat_hold']:.2f})" if S9 else ""), + bool(S9 and S9[W_]["gate_pass"])), + ("(e) marginal cella al buio", S4["sm"]["marginal_verdict"], + S4["sm"]["marginal_verdict"] == "ADDS" and S4["sm"]["earns_slot"]), + ("(f) day_boundary_robust", f"{S6['req']['verdict']} (spread {S6['req']['spread']:.3f})", + S6["req"]["verdict"] != "ARTIFACT-RISK"), + ("(g) Sharpe forward > 0", f"{MDE['MODELED']['sh']:+.2f} (t={MDE['MODELED']['t']:.2f}, " + f"p={MDE['MODELED']['p']:.2f})", MDE["MODELED"]["sh"] > 0), + ("VETO integrita'", f"{M['frac_ok']:.1%} barre ricostruibili", + M["frac_ok"] >= GATE_INTEGRITY_MIN), + ] + for nm, val, ok in rows: + print(f" {'OK ' if ok else 'NO '} {nm:<38}{val}") + npass = sum(1 for _, _, o in rows if o) + print(f"\n {npass}/{len(rows)} condizioni soddisfatte OGGI.") + print(f" Il funding costa a PREVDAY {S8['sleeve_drag']:.2%}/anno di sleeve = " + f"{W_*S8['sleeve_drag']:.2%}/anno di libro al peso proposto (gia' dentro (c)).") + + +# ============================================================================================= +def load_rate() -> dict: + """Serie oraria del funding per asset (LINEARE dove esiste, INVERSE prima come proxy + DICHIARATO). Usa la cache parquet di r0822d_funding; --no-net la usa e basta.""" + F = {} + for instr, (start, _a, _k) in FU.INSTR.items(): + d = FU.fetch_funding(instr, start).copy() + d["dt"] = pd.to_datetime(d["ts"], unit="ms", utc=True) + F[instr] = d.drop_duplicates(subset="ts").sort_values("dt").set_index("dt") + return FU.build_rate(F) + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--draws", type=int, default=300) + ap.add_argument("--no-net", action="store_true") + ap.add_argument("--fast", action="store_true") + args = ap.parse_args() + SCRATCH.mkdir(parents=True, exist_ok=True) + + print("=" * 97) + print(" §54 PREVDAY-GATE — il lead PREVDAY ha un gate pre-registrato? (2026-08-23)") + print(" NESSUNA proposta di cambio al libro live. Sola lettura sui file di produzione.") + print("=" * 97) + + grid = [dict(anchor=a, k=k, short=s, mh=m) + for a in G_ANCHOR for k in G_K for s in G_SHORT for m in G_MH] + tfs = G_TF + if args.fast: + grid = [g for g in grid if g["anchor"] in (1, 2) and g["mh"] == 0] + tfs = ("1h",) + print(" ⚠️ --fast: griglia RIDOTTA, i conteggi NON sono quelli dichiarati.") + + F = sezione0_1(grid, tfs) + M = sezione2(args) + MDE = sezione3(M) + print(f"\n {clock()} carico il funding ({'cache' if args.no_net else 'cache + incrementale'})...") + if args.no_net: + FU.USE_NET = False + rate = load_rate() + print(f" {clock()} funding pronto: " + " ".join( + f"{a} {rate[a]['series'].index.min().date()}->{rate[a]['series'].index.max().date()} " + f"(switch lineare {rate[a]['switch'].date()})" for a in ASSETS)) + + S4 = sezione4(grid, tfs) + S5 = sezione5(S4, F) + S6 = sezione6() + S7 = sezione7(rate, args.draws) + S8 = sezione8(rate) + S9 = sezione9(rate) + sezione10(S4, S5, S6, S7, S8, S9, M, MDE) + + sez("ATTESA A PRIORI vs ESITO — comprese quelle che ho sbagliato") + ch = S4["chosen"]["params"] + esiti = [ + ("A1 cella al buio != congelata", + "SBAGLIATA (coincidono)" if S4["uguale"] else "CONFERMATA nell'esito", + "" if S4["uguale"] else + f"⚠️ ma SBAGLIATA NEL MECCANISMO: prevedevo un k piu' piccolo; k resta {ch['k']:.2f} " + f"e a cambiare sono ALLOW_SHORT ({FROZEN['short']}->{ch['short']}) e il TF " + f"(1h->{S4['chosen']['tf']})"), + ("A2 il DSR si ribalta col conteggio", + f"REFUTATA (fam. {S4['dsr']:.3f} / screen {S5['dsr_screen']:.3f} / N=560 ancora PASS)", + f"⚠️ la leva non e' N ma la DISPERSIONE: servirebbe sd {S5['sd_soglia']:.2f} contro " + f"{S5['allsr'].std(ddof=1):.2f} misurata"), + ("A3 boundary robusto, banda larga, 0h fortunata", + f"CONFERMATA ({S6['req']['verdict']}, spread {S6['req']['spread']:.3f})", + f"canonica al 96o pctl delle 24: uplift +0,328 contro mediana +0,190"), + ("A4 il delta di §50 e' una sovrastima", + (f"CONFERMATA ({S7[W_PROPOSTO]['sh_can']:+.3f} -> {S7[W_PROPOSTO]['sh']:+.3f} FULL, " + f"{S7[W_PROPOSTO]['shh_can']:+.3f} -> {S7[W_PROPOSTO]['shh']:+.3f} HOLD)") if S7 else "n/d", + "il segno regge al 100% delle estrazioni; e' la taglia a dimezzarsi"), + ("A5 drag di funding 0-2%/anno", f"CONFERMATA ({S8['sleeve_drag']:.2%}/anno)", + "meccanismo NON come previsto: la gamba short incassa solo 0,26-0,46x " + "l'incondizionato — non compensa"), + ("A6 frac_random_beat_hold alto", + f"CONFERMATA ({S9[W_PROPOSTO]['frac_random_beat_hold']:.2f})" if S9 else "n/d", + "'migliora l'hold-out' e' un claim generico su questo libro"), + ("A7 la finestra forward non ha potenza", + f"CONFERMATA (t={MDE['MODELED']['t_naive']:.2f} naive / {MDE['MODELED']['t']:.2f} Lo)", + f"e il +2,04 pubblicato e' su lente ORARIA: sulla lente del progetto e' " + f"{MDE['daily_sharpe']:+.2f}"), + ] + for nm, v, nota in esiti: + print(f" {nm:<42}{v}") + if nota: + print(f" {'':<42}{nota}") + print(f"\n {clock()} fine.") + + +if __name__ == "__main__": + main() From 8694da31bcee2b191055629c8e5764727b6d5d78 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 02:57:26 +0000 Subject: [PATCH 097/114] =?UTF-8?q?registro=20=C2=A754:=20GATE=20PREVDAY-0?= =?UTF-8?q?1=20scritto=20=E2=80=94=20la=20cella=20che=20gira=20non=20e'=20?= =?UTF-8?q?quella=20che=20la=20selezione=20onesta=20sceglie,=20e=20quella?= =?UTF-8?q?=20onesta=20butta=20la=20gamba=20short?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 91 ++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index dbd9753..4b2e75f 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -719,7 +719,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 51 | NEWDATA-SCOUT | se la scorta e' esaurita: cosa iniziare a raccogliere OGGI, che fra 12-24 mesi apra una famiglia? | **RACCOGLIERE 2 FONTI SU 10** — catena USDC (**+18%, non +90%**) e una **campagna a termine** di book depth; il criterio di ricostruibilita' ne uccide 4 da solo e **refuta una raccomandazione del 22/08** | | 52 | SKEPTIC-SPOT | il lead dello spot (+1,55-1,66%/anno) sopravvive a un attacco deliberato? | 🚨 **REGGE A $600, SI ANNULLA A $272k** — 5 attacchi su 6 falliscono; quello che morde non e' nei numeri, e' **nella taglia a cui vengono spesi**: il lead sparisce **esattamente al muro che pretende di spostare** | | 53 | SKEPTIC-LEVA | il gradino di leva 1,25x sopravvive a un attacco deliberato, col funding dentro? | **REGGE 5/6, INCRINA 1/6** — il **1,25x sopravvive**, il **1,50x NO**; k massimo difendibile **1,40**. L'incrinatura e' il ramo `eq_fallback`, dove **la scala non apre il buco: lo MOLTIPLICA** | -| 54 | PREVDAY-GATE | il piu' forte candidato misurato sta in monitor da giugno senza gate: quanto vale al gate onesto? | _in corso_ | +| 54 | PREVDAY-GATE | il piu' forte candidato misurato sta in monitor da giugno senza gate: quanto vale al gate onesto? | **GATE PREVDAY-01 SCRITTO — 8/10 condizioni oggi**, e le due che mancano sono la stessa cosa: 🚨 **la cella che GIRA non e' quella che la selezione onesta sceglie** — e la selezione compra il libro **LONG-FLAT**, cioe' butta via la gamba per cui PREVDAY fu promosso | | 55 | COSTO-CAPITALE | i muri sono calcolati a costo d'esecuzione COSTANTE, ma il costo cresce col capitale: dove sta il muro vero? | _in corso_ | | 56 | XS-AMPIEZZA | su un universo povero (11 gambe), si recupera ampiezza EFFETTIVA senza aggiungere gambe? | _in corso_ | | — | **NOTIFIER** (audit di fatto) | 🚨 **PUNTO SINGOLO DI GUASTO nella rete di sicurezza** | il `send()` di Telegram e' **un solo tentativo da 10s, senza retry, con l'esito MAI registrato** — e lo stato `alerted=True` viene scritto **prima** dell'invio: un 🚨 di `venue_watch` perso e' perso **per l'intero episodio**. Tasso di fallimento **misurato 6,9%** (2/29, IC95 [1,9%, 22,0%]) | @@ -3230,3 +3230,92 @@ falsificabile con un backtest**. **VERDETTO: `IL LEAD REGGE A $600 E SI ANNULLA A $272k — CIO' CHE MORDE NON E' NEI SUOI NUMERI, E' NELLA TAGLIA A CUI VENGONO SPESI`**. + +--- + +## 54 — PREVDAY-GATE (il gate che il lead piu' forte non ha mai avuto) + +`scripts/research/r0823_prevday_gate.py` (**215 s**). Nessun file di produzione toccato (`git diff` +vuoto; **sola lettura provata a runtime con md5+mtime su 6 file**). + +**Famiglia dichiarata e trial contati al rialzo, STAMPATI PRIMA di ogni risultato:** +`ANCHOR_DAYS{1,2,3,5}` × `BUFFER_K{0…1,00}` × `ALLOW_SHORT{T,F}` × `MIN_HOLD_H{0,24,72}` × +`TF{1h,4h}` = **336 celle**; le **11** gia' spese in giugno e le **≥8** della scoperta sono +**dentro**. Escludere il TF 15m rende il conto **piu' permissivo**, ed e' dichiarato. Screen: +N=352 / N_sweep=456. + +🚨 **IL RISULTATO PRINCIPALE: la cella al buio non e' quella che gira.** + +| | AL BUIO (in-sample) | CONGELATA (monitor) | +|---|---|---| +| tf / parametri | **4h, k=0,30, short=FALSE** | 1h, k=0,30, **short=TRUE** | +| rango in-sample | **1/336** | **186/336** | +| Sharpe FULL / HOLD | +1,621 / +0,731 | +1,236 / +1,297 | +| corr → TP01 (full/hold) | **0,641 / 0,469** | **0,152 / 0,028** | +| deflated-Sharpe N=336 | **0,993 PASS** | **0,905 FAIL** | + +`earns_slot_honest = True`, **ma su un'altra strategia**. 📌 **La selezione onesta compra il libro +LONG-FLAT — mentre il docstring della scoperta dice l'opposto in chiaro** (*"LONG-SHORT is the +slot-earner… the long-flat sibling just re-rides the bull"*): **la gamba short, cioe' l'intera +ragione per cui PREVDAY fu promosso a LEAD, e' esattamente cio' che la selezione in-sample-only non +compra.** Rovescio dovuto e riportato: **la cella al buio diversifica molto meno** (corr 0,64 contro +0,15). ✅ `factory(FROZEN) == pb.target` bit-exact 2/2, `causality_ok` ok, identita' con +`study_family_honest` verificata. + +❌ **Attesa a priori del coordinatore REFUTATA sul DSR, e il perche' e' MISURATO.** Tutte le +partizioni difendibili **PASSANO** (N=11 → 1,000; N=168 → 0,996; N=336 → 0,993; famiglia+screen +N=352 → 0,991; N=560 → PASS): **portare N da 11 a 560 costa 0,009 di DSR**, e il verdetto si +ribalterebbe solo a `sd(trial) ≥ 0,363` contro **0,267** misurata. **Ma non e' vacuo:** controllo +positivo — la cella **mediana** della stessa famiglia **FALLISCE** (0,863), il 25° pctl 0,593. +📌 **REGOLA NUOVA, che completa il filo §10: su una famiglia OMOGENEA il deflated-Sharpe e' cieco +sul CONTEGGIO e vede sul LIVELLO.** Il 30/07 VRP01 si ribaltava col conteggio **perche' quella +famiglia era larga**. *Non e' il gate a essere incoerente: e' la larghezza della famiglia a decidere +su quale asse ha potenza.* + +**`day_boundary_robust` — ROBUST** (−8h +0,114 · −4h +0,218 · 0h **+0,328** · +4h +0,215 · +8h ++0,164, spread 0,214, **mai negativo**; controllo positivo superato: un segnale puramente di +calendario esce ARTIFACT-RISK). ⚠️ Ma sui **24 confini-giorno la canonica e' al 96° pctl**: ShFULL +1,236 → **mediana onesta 0,963**, uplift +0,328 → **+0,190**. +⚠️ **Trovato per strada:** il campo `base` di `altlib.day_boundary_robust` e' **`offsets[0]`, non lo +zero** — con una griglia che comincia in negativo **l'etichetta inganna**. + +📌 **Banda d'ancora del LIBRO — §50 non aveva de-luckato il candidato.** Spazio congiunto +TP01×24 · SKH01×23 · **PREVDAY×24** = 13.248 configurazioni, 300 estrazioni, differenze appaiate. +✅ **Replica indipendente di §50 a candidato fermo: +0,191/+0,362** contro i +0,192/+0,363 +pubblicati. **De-luckato a peso 15%: dShFULL +0,100 (100% delle estrazioni) / dShHOLD +0,246.** +**Il segno regge al 100% a ogni peso; la taglia si dimezza.** + +**MDE, integrita', funding, tilt-null.** **MDE:** 63 giorni = 0,172 anni → **t = 0,85 (naive) / +0,48 (Lo)**, IC95 largo **16,6 punti di Sharpe**; per distinguere uno Sharpe vero di 1,2 dal nulla +all'80% servono **9,4 anni**. ⚠️ E **il "+2,04" e' su lente ORARIA**: sulla lente **giornaliera** con +cui e' misurato ogni altro sleeve del progetto e' **+1,56**. **Integrita':** 1512/1512 ore, 0 +duplicate, **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** di `paper_statarb`): **sano, non +perfetto**. **Funding** (mai in nessun backtest): **−1,37%/anno** di sleeve, e 📌 **la gamba short +NON compensa** — incassa 0,26-0,46x l'incondizionato mentre la lunga paga 1,25-1,69x. +**`weights_tilt_null`:** PASS a tutti i pesi, ma **`frac_random_beat_hold` = 0,91**. + +### `GATE PREVDAY-01` — decisione **2027-06-21** + +*(La data non e' scelta perche' la finestra diventi informativa — la misura dice che **non** lo +diventa — ma perche' oltre un anno **tenere un lead senza decidere e' esso stesso una decisione**.)* +Promozione al peso 15% **solo se TUTTE**: **(a)** DSR ≥ 0,95 sulla famiglia ri-dichiarata **prima**, +con la sensibilita' alla partizione pubblicata accanto; **(b)** la cella al buio **coincide** con +quella congelata, **oppure** il monitor si ri-congela su quella al buio **e il forward riparte da +zero**; **(c)** delta di libro appaiato sullo spazio congiunto **> +0,05** dShFULL, positivo in +**≥90%**, **col funding dentro**; **(d)** `weights_tilt_null` pass; **(e)** ADDS + robust_oos + +non-hedge **sulla cella al buio**; **(f)** `day_boundary_robust` ≠ ARTIFACT-RISK; **(g)** Sharpe +forward > 0, **soglia debole di proposito**. +**Veto d'integrita'** (blocca, non decide): ≥80% di barre ricostruibili **e** i minuti non +registrati non devono crescere. **Kill:** Sharpe forward < −0,50 su ≥180 giorni di barre **attive** +— a quell'orizzonte P(uccidere un edge vivo a Sharpe 1,2) = **9,7%**; **al kill la famiglia NON si +ri-ottimizza.** + +⚠️ **Errori dell'agente su se stesso:** **A1 giusta nell'esito, sbagliata nel meccanismo** (prevedeva +un `k` piu' piccolo; k resta 0,30, a cambiare sono `ALLOW_SHORT` e il TF); **A2 refutata** (il DSR +non si ribalta col conteggio); **A5 giusta nel numero, sbagliata nel meccanismo** (credeva che la +gamba short compensasse il funding). + +**VERDETTO: `IL GATE E' SCRITTO E OGGI PASSA 8 CONDIZIONI SU 10 — LE DUE CHE MANCANO SONO LA STESSA +COSA: LA CELLA CHE GIRA NON E' QUELLA CHE LA SELEZIONE ONESTA SCEGLIE`**. **Nessun cambio al libro +live proposto.** From bf617b75ff0495772859e73d8fb99e18198c06a2 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 03:02:41 +0000 Subject: [PATCH 098/114] =?UTF-8?q?research(xs-ampiezza):=20NO=20=E2=80=94?= =?UTF-8?q?=20la=20caduta=20di=20XS01=20su=2011=20gambe=20non=20era=20ampi?= =?UTF-8?q?ezza,=20e'=20selettivita'=20piu'=20un=20IC=20che=20li'=20vale?= =?UTF-8?q?=20zero?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'ampiezza effettiva (participation ratio) scende solo da 9,34 a 7,23 (-23%) mentre lo Sharpe scende dell'86%: sotto IR = IC*sqrt(ampiezza) la sola ampiezza spiega il 14% del divario, e NESSUNA delle costruzioni dichiarate la muove (7,13-8,47). Il demeaning — la mossa che creo' XSR01 — e' un NO-OP ALGEBRICO su XS01, perche' i pesi sommano gia' a zero per costruzione (max|sum w| = 2,8e-17, serie identiche a 2,2e-16): non e' un risultato debole, e' un'identita' che vale a ogni universo e per sempre. Il divario sta altrove, ed e' due cose. (a) SELETTIVITA': k=5 su 11 gambe tiene 10 posizioni su 11 e non seleziona niente; k=5 non e' un parametro invariante di scala, fu tarato su A=19. Congelando il QUANTILE invece del numero la mediana delle 10 fasi passa da -0,04 a +0,69 (delta appaiato +0,83, positivo in 10/10 fasi, plateau su k in {1,2,3}, mentre su 19 gambe k non conta) contro +1,15 dell'universo pieno. (b) IC: -0,027 (t -0,88) sulle 11 contro +0,057 (t +2,29) sulle 19, sotto TUTTI i 40 sottoinsiemi casuali da 11 — e sui 9 alt maturi presi da soli e' NEGATIVO (-0,065, t -2,07) mentre le 8 mancanti da sole non ce l'hanno (+0,008). L'informazione vive nel CONTRASTO fra le fasce: XS01 e' in buona parte una rotazione major-maturi contro alt-recenti, non una selezione dentro una fascia. §50 lo chiamava "sfortuna di listino" al 9° pctl dello Sharpe: sull'IC il sottoinsieme di Deribit e' fuori dalla banda, per una ragione strutturale. E tutto il recupero sta sotto la risoluzione del campione, per quattro strade: MDE dell'hold-out ~2,3 di Sharpe su 1,64 anni attivi; lo spread di coda che dovrebbe pagarlo ha t 0,2-0,6 contro 1,91 dell'universo pieno; il null di permutazione a fee zero (p de-luckato sulle 10 fasi: mediana 0,130, sotto 0,05 solo nel 40%) separa il candidato dal rumore su 19 gambe e non su 11; il deflated-Sharpe sulle 80 celle dichiarate FALLISCE (0,158, massimo atteso dal rumore 1,065 contro candidato 0,459). Due errori catturati su me stesso. (1) La selezione in-sample-only NON e' stabile alla dichiarazione della griglia: con k in {2,3,4,5} la cella al buio era resid k=2 (HOLD +1,33), aggiungendo k=1 diventa white k=1 (HOLD -0,53) — con ~1 anno di in-sample (SE(Sharpe) 1,7) la procedura onesta e' una monetina. (2) La prima misura di eseguibilita' usava |dW| invece di |d(W*scale)| e dava "100% eseguito": la posizione in dollari e' W*scale e il vol-target muove il nozionale ogni giorno anche a pesi fermi. Corretta, riproduce le due popolazioni di §45 per via indipendente (11,9% degli ordini = 62,5% del nozionale). A $635 passa l'83% del nozionale: il muro non e' il capitale. Venue letto ora: 14/19 quotate (SUI 18/08, APT 21/08 senza NESSUNA quota, AAVE 15/08), 11 da >=1 anno, lotti $0,01-$7,72. Solo 2 delle 11 gambe netterebbero col libro live. Replica bit-exact contro sleeves._xsec_returns (max|dif| = 0,0); controlli positivi su tutti e tre i rilevatori (ampiezza su matrici note, IC con un segnale che bara = +1,000, null di permutazione che separa 19 da 11). Sola lettura, nessun ordine, output riproducibile a meno del timestamp. Libro, pesi, cron, config INVARIATI. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf --- scripts/research/r0823_xs_ampiezza.py | 1109 +++++++++++++++++++++++++ 1 file changed, 1109 insertions(+) create mode 100644 scripts/research/r0823_xs_ampiezza.py diff --git a/scripts/research/r0823_xs_ampiezza.py b/scripts/research/r0823_xs_ampiezza.py new file mode 100644 index 0000000..8a496fb --- /dev/null +++ b/scripts/research/r0823_xs_ampiezza.py @@ -0,0 +1,1109 @@ +"""XS-AMPIEZZA (§56) — su un universo POVERO, una costruzione cross-sectional puo' recuperare +AMPIEZZA EFFETTIVA senza aggiungere gambe? E quanto del divario 1,265 -> 0,116 recupera? + +DA DOVE VIENE LA DOMANDA +------------------------ +XS01 e' l'unico meccanismo che abbia mai sfondato il soffitto direzionale ~1,3 di questo progetto, +e sta fermo per VENUE: gira su Hyperliquid, che la decisione del 26/07 tiene fuori fino a $20k. +Poche ore fa §50 (`r0823_book_3rd.py`) ha misurato la versione eseguibile su Deribit — le 11 gambe +quotate da >=1 anno — e ha trovato che collassa (Sharpe 1,265 -> 0,116), separando due cause con un +null dei sottoinsiemi: il sottoinsieme MEDIANO da 11 fa gia' 0,548 (=> "il 62% della caduta e' +AMPIEZZA") e quello di Deribit sta al 9° percentile (=> "sfortuna di listino"). + +E c'e' un precedente che punta nella stessa direzione: il 25/07 XSR01 nacque dalla DIAGNOSI di un +fallimento — l'ampiezza effettiva di un paniere di 50 coppie era 4,5 perche' condividevano la gamba +BTC; il demeaning cross-sezionale la porto' a 37,4 e lo Sharpe da 0,82 a 1,82. L'ampiezza effettiva +non e' il numero di gambe: e' il numero di scommesse indipendenti, e si puo' guadagnare senza +aggiungere strumenti. + +ATTESA A PRIORI — DICHIARATA PRIMA DI MISURARE (coordinatore + mia) +------------------------------------------------------------------ + (A1) Il 62% della caduta e' ampiezza, e l'ampiezza NON si compra con la costruzione: il demeaning + funziono' su 50 gambe perche' c'era un fattore comune grosso da togliere; su 11 c'e' molto + meno da togliere e il recupero sara' parziale e SOTTO l'MDE. + (A2) Il gate e' N_eff: se N_eff delle 11 e' gia' vicino a 11, non c'e' niente da recuperare. + (A3) Mi aspetto che il muro NON sia l'eseguibilita' (nell'ondata del 22/08 non ha bocciato niente + per sei volte di fila) ma la storia corta: 2,6 anni, in-sample = il solo 2024. + (A4) I due affinamenti del 19/06 (blend [30,90] e gate p30) non si replicano fuori campione + (misurato il 22/08): mi aspetto che il meccanismo NUDO non sia peggiore. + ESITO (scritto dopo, per onesta' di lettura): A1 giusta nella CONCLUSIONE e SBAGLIATA nel + MECCANISMO — l'ampiezza non e' quasi per niente il problema; A2 confermata; A3 confermata; + A4 confermata sul FULL. E l'etichetta "ampiezza" di §50 e' cio' che questo filone corregge. + +LA FAMIGLIA, DICHIARATA PRIMA (e contata AL RIALZO nel deflated-Sharpe) +---------------------------------------------------------------------- +Griglia cartesiana su universo 11-Deribit, 80 celle: + kind in {canon, resid (residuo vs BTC prima del ranking), ivol (equal-risk), + white (whitening inv-cov con shrinkage 50%)} 4 + k in {1,2,3,4,5} (k=5 e' il canonico, tarato su A=19) 5 + lookbacks in {(30,90) col gate di dispersione p30, (30,) NUDO senza gate} 2 + H in {10, 20} (holding piu' lungo) 2 +Il DEMEANING cross-sezionale — la costruzione che creo' XSR01 — e' fuori griglia perche' su XS01 +e' un NO-OP ALGEBRICO, e questo si dimostra invece di misurarlo (sezione 3). + +METODO +------ + * ampiezza effettiva: DUE definizioni, perche' quella importata degenera qui (sezione 3); + * de-luck di FASE: XS01 ha un ciclo H=10 e i suoi numeri headline sono sulla fase 0, che + CLAUDE.md registra come fortunata -> ogni confronto e' la MEDIANA DELLE DIFFERENZE APPAIATE + sulle 10 fasi, mai la differenza delle mediane (lezione 26/07); + * null di PERMUTAZIONE cross-sezionale a FEE ZERO (25/07: permutare un segnale ne fa esplodere il + turnover, e un null a fee piena perderebbe per COSTO invece che per assenza d'informazione); + * gate onesto coi TRE componenti reali di `study_family_honest` (selezione in-sample-only -> + `altlib.deflated_sharpe` -> `altlib.marginal_vs_tp01`), importati, non riscritti; + * MDE dichiarato in barre ATTIVE, non in giorni di calendario; + * controlli positivi obbligatori su OGNI rilevatore (un rilevatore che trova ~0 e non e' validato + e' indistinguibile da uno rotto). + +VINCOLI DI ESECUZIONE: sola lettura, nessun ordine, solo GET pubbliche Deribit con pacing e +astensione nei minuti :05-:10 e :24-:30 (finestre del cron di produzione). + + uv run python scripts/research/r0823_xs_ampiezza.py # completo + uv run python scripts/research/r0823_xs_ampiezza.py --no-net # solo cache del venue +""" +from __future__ import annotations + +import argparse +import datetime as dt +import itertools +import json +import math +import sys +import time +import urllib.request +from pathlib import Path + +import numpy as np +import pandas as pd +from scipy.stats import spearmanr + +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 import sleeves as SL # noqa: E402 +from src.portfolio.portfolio import HOLDOUT, metrics, to_daily # noqa: E402 + +# --- macchineria di ricerca gia' validata (importata, non riscritta) ------------------------- +import altlib as al # noqa: E402 +from r0725_statarb_basket_gate import eff_breadth as XSR_EFF_BREADTH # noqa: E402 + +# ============================================================================================= +# COSTANTI DICHIARATE +# ============================================================================================= +SCRATCH = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/xsamp") +if not SCRATCH.parent.exists(): + SCRATCH = Path("/tmp/pythagoras_xsamp") + +API = "https://www.deribit.com/api/v2/public" +CACHE_FRESH_S = 3 * 3600 +SEED = 20260823 +N_PHASES = 10 # ciclo H=10 di XS01 +N_SUBSETS = 40 # sottoinsiemi casuali da 11 fra le 19 +PH_SUB = (0, 2, 4, 6, 8) # fasi su cui si de-lucka il null dei sottoinsiemi +N_PERM = 200 # permutazioni del null a fee zero +CAPITAL_TODAY = 635.0 +CAP_LADDER = (635.0, 2000.0, 5000.0, 20000.0) +MIN_ORDER = 5.0 # config/live.json -> min_order_usd +W_SLEEVE = 0.15 # il peso a cui §50 ha valutato ogni candidato a 3° sleeve +FEE_SIDE = al.FEE_SIDE # 0.05%/lato, il default del progetto +ANN = math.sqrt(365.25) +T0 = time.time() +_ARGS = None + +XS_UNIVERSE = list(SL.XS_UNIVERSE) # i 19 major, LETTI dalla produzione +CFG = dict(SL.XS_CFG) # config canonica, LETTA dalla produzione + + +def sez(t: str) -> None: + print("\n" + "=" * 104) + print(f" {t}") + print("=" * 104) + + +def sub(t: str) -> None: + print("\n" + "-" * 104) + print(f" {t}") + print("-" * 104) + + +def clock() -> str: + return f"[{time.time() - T0:5.0f}s]" + + +# ============================================================================================= +# RETE — sole GET pubbliche, pacing, astensione nelle finestre del cron +# ============================================================================================= +def _pace() -> None: + while dt.datetime.now(dt.timezone.utc).minute in (5, 6, 7, 8, 9, 10, 24, 25, 26, 27, 28, 29, 30): + time.sleep(15) + time.sleep(0.55) # <= 2 req/s + + +def get_json(path: str, cache_name: str) -> dict | None: + SCRATCH.mkdir(parents=True, exist_ok=True) + fp = SCRATCH / cache_name + if _ARGS and _ARGS.no_net: + return json.loads(fp.read_text()) if fp.exists() else None + if fp.exists() and (time.time() - fp.stat().st_mtime) < CACHE_FRESH_S: + return json.loads(fp.read_text()) + try: + _pace() + with urllib.request.urlopen(f"{API}/{path}", timeout=30) as r: + d = json.loads(r.read()) + fp.write_text(json.dumps(d)) + return d + except Exception as e: # noqa: BLE001 + print(f" [rete KO su {path}: {type(e).__name__}] -> cache") + return json.loads(fp.read_text()) if fp.exists() else None + + +# ============================================================================================= +# IL MOTORE — replica di `sleeves._xsec_returns` generalizzata a (universo, fase, costruzione) +# ============================================================================================= +def prices(universe: list[str]) -> pd.DataFrame: + cols = {} + for sym in universe: + p = SL._HL_DIR / f"hl_{sym.lower()}_1d.parquet" + if not p.exists(): + continue + d = pd.read_parquet(p) + cols[sym] = pd.Series(d["close"].values.astype(float), + index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)) + return pd.concat(cols, axis=1, join="inner").sort_index().dropna() + + +def run(universe: list[str], kind: str = "canon", k: int | None = None, H: int | None = None, + lookbacks=None, disp_pct: int | None = None, phase: int = 0, + tv: float | None = None, fee: float = FEE_SIDE, demean: bool = False) -> dict: + """Il meccanismo XS01, un grado di liberta' per volta. Coi default e' BIT-EXACT contro + `sleeves._xsec_returns` (verificato nella sezione 2).""" + k = CFG["k"] if k is None else k + H = CFG["H"] if H is None else H + lookbacks = CFG["lookbacks"] if lookbacks is None else lookbacks + disp_pct = CFG["disp_pct"] if disp_pct is None else disp_pct + tv = CFG["target_vol"] if tv is None else tv + minhist = CFG["disp_minhist"] + + C = prices(universe) + px = C.values + n, A = px.shape + kk = min(k, A // 2) + mlb = max(lookbacks) + dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0]) + vol = pd.DataFrame(dret, index=C.index).rolling(30, min_periods=15).std().shift(1).values + bi = list(C.columns).index("BTC") if "BTC" in C.columns else 0 + + W = np.zeros((n, A)); w = np.zeros(A); disp_hist: list[float] = [] + for i in range(n): + if i >= mlb and i % H == phase: + rLs = [px[i] / px[i - L] - 1.0 for L in lookbacks] + if kind == "resid": + # beta OLS causale su 90 barre di ritorni; il momentum si ranka sul RESIDUO vs BTC + seg = dret[max(1, i - 90):i + 1] + b = np.array([np.cov(seg[:, j], seg[:, bi])[0, 1] / max(np.var(seg[:, bi]), 1e-12) + for j in range(A)]) + rLs = [rL - b * rL[bi] for rL in rLs] + disp_i = float(np.mean([r.std() for r in rLs])) + thr = (np.percentile(disp_hist, disp_pct) + if (disp_pct > 0 and len(disp_hist) >= minhist) else -np.inf) + if disp_i >= thr: + score = np.zeros(A); cnt = 0 + for rL in rLs: + sd = rL.std() + if sd > 0: + score += (rL - rL.mean()) / sd; cnt += 1 + if cnt: + score /= cnt + order = np.argsort(score) + w = np.zeros(A); lo, hi = order[:kk], order[-kk:] + w[hi] = 0.5 / kk; w[lo] = -0.5 / kk + if kind == "ivol": # equal-risk: pesi ~ 1/vol + v = np.nan_to_num(vol[i], nan=0.0) + w = w * np.where(v > 0, 1.0 / v, 0.0) + w = w - w.mean() # 1/vol rompe sum(w)=0: ri-centrare + g = np.abs(w).sum(); w = w / g if g > 0 else w + elif kind == "white": # whitening: w ~ Sigma^-1 z + seg = dret[max(1, i - 90):i + 1] + S = np.cov(seg.T) + S = 0.5 * S + 0.5 * np.diag(np.diag(S)) # shrinkage dichiarata + z = np.zeros(A); z[hi] = 1.0 / kk; z[lo] = -1.0 / kk + try: + w = np.linalg.solve(S + 1e-12 * np.eye(A), z) + except Exception: # noqa: BLE001 + pass + w = w - w.mean() + g = np.abs(w).sum(); w = w / g if g > 0 else w + else: + w = np.zeros(A) + if demean: + w = w - w.mean() + disp_hist.append(disp_i) + W[i] = w + + gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1) + turn = np.zeros(n); turn[0] = np.abs(W[0]).sum() + turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1) + net = gross - turn * fee + s = pd.Series(net, index=C.index) + rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25) + scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0) + return dict(net=to_daily(pd.Series(s.values * scale, index=C.index)), + W=W, dret=dret, scale=scale, cols=list(C.columns), idx=C.index) + + +def leg_pnl(R: dict, fee: float = FEE_SIDE) -> pd.DataFrame: + """P&L NETTA per gamba: e' l'oggetto su cui si misura l'ampiezza effettiva (le 'scommesse').""" + W, dret, scale = R["W"], R["dret"], R["scale"] + n, A = W.shape + g = np.zeros((n, A)); g[1:] = W[:-1] * dret[1:] + t = np.abs(np.diff(W, axis=0, prepend=0.0)) + return pd.DataFrame((g - t * fee) * scale[:, None], index=R["idx"], columns=R["cols"]) + + +def sh(s: pd.Series) -> float: + return float(metrics(s)["sharpe"]) + + +def sh_hold(s: pd.Series) -> float: + return float(metrics(s[s.index >= HOLDOUT])["sharpe"]) + + +def sh_ins(s: pd.Series) -> float: + return float(metrics(s[s.index < HOLDOUT])["sharpe"]) + + +# ============================================================================================= +# AMPIEZZA EFFETTIVA — due definizioni, perche' la prima degenera +# ============================================================================================= +def neff_rbar(M: pd.DataFrame) -> tuple[float, float]: + """La definizione usata da XSR01 (`r0725_statarb_basket_gate.eff_breadth`): + N / (1 + (N-1)*rbar) con rbar = correlazione media fuori diagonale.""" + Cm = np.nan_to_num(M.corr().values, nan=0.0) + off = Cm[~np.eye(len(Cm), dtype=bool)] + rbar = float(np.nanmean(off)); N = len(Cm) + ne = N / (1.0 + (N - 1) * rbar) if rbar > -1.0 / (N - 1) else float(N) + return ne, rbar + + +def neff_eig(M: pd.DataFrame) -> float: + """Participation ratio degli autovalori: (sum lam)^2 / sum lam^2. Sta SEMPRE in [1, N].""" + Cm = np.nan_to_num(M.corr().values, nan=0.0) + lam = np.clip(np.linalg.eigvalsh(Cm), 0.0, None) + return float(lam.sum() ** 2 / max((lam ** 2).sum(), 1e-12)) + + +# ============================================================================================= +# 1) CENSIMENTO DAL VENUE — quali gambe esistono DAVVERO, e a che lotto +# ============================================================================================= +def venue_census() -> dict: + out: dict = {"perp": {}, "read_ok": True} + ins = {} + for cur in ("BTC", "ETH", "USDC"): + d = get_json(f"get_instruments?currency={cur}&expired=false", f"instr_{cur}.json") + if d is None: + out["read_ok"] = False + continue + ins[cur] = d["result"] + bs = get_json("get_book_summary_by_currency?currency=USDC&kind=future", "bs_usdc_fut.json") + marks = {} + if bs: + for x in bs["result"]: + p = x.get("mark_price") or x.get("last") + if p: + marks[x["instrument_name"]] = (float(p), float(x.get("volume_usd") or 0.0), + x.get("bid_price"), x.get("ask_price")) + for _cur, lst in ins.items(): + for x in lst: + if x["kind"] != "future" or x.get("settlement_period") != "perpetual": + continue + n = x["instrument_name"] + mk, vol, bid, ask = marks.get(n, (None, None, None, None)) + out["perp"][x["base_currency"]] = dict( + instr=n, min_amt=float(x["min_trade_amount"]), + created=dt.datetime.fromtimestamp(x["creation_timestamp"] / 1000, + dt.timezone.utc).date().isoformat(), + mark=mk, min_usd=(mk * float(x["min_trade_amount"])) if mk else None, + vol24=vol, bid=bid, ask=ask) + return out + + +def sezione1(V: dict) -> dict: + sez("1) IL VENUE, LETTO ORA — quali delle 19 gambe di XS01 esistono su Deribit (perp USDC)") + if not V["read_ok"]: + print(" ⚠️ lista strumenti NON letta dal venue in questa corsa: si usa la cache.") + oggi = dt.date.today() + print(f" {'asset':<7}{'strumento':<26}{'quotato dal':<13}{'eta' + chr(39) + ' (g)':>10}" + f"{'lotto min':>11}{'vol 24h $':>15}{'quote':>10}") + ok, stab = [], [] + for s in XS_UNIVERSE: + p = V["perp"].get(s) + if not p: + print(f" {s:<7}{'— non quotato —':<26}{'—':<13}{'—':>10}{'—':>11}{'—':>15}{'—':>10}") + continue + ok.append(s) + eta = (oggi - dt.date.fromisoformat(p["created"])).days + if eta >= 365: + stab.append(s) + q = "bid+ask" if (p["bid"] and p["ask"]) else ("SOLO 1 LATO" if (p["bid"] or p["ask"]) + else "NESSUNA") + mu = f"${p['min_usd']:.2f}" if p["min_usd"] else "n/d" + v = f"{p['vol24']:,.0f}" if p["vol24"] is not None else "n/d" + print(f" {s:<7}{p['instr']:<26}{p['created']:<13}{eta:>10}{mu:>11}{v:>15}{q:>10}") + print(f"\n -> quotate: {len(ok)}/19 ({', '.join(ok)})") + print(f" -> quotate da >= 1 anno: {len(stab)}/19 ({', '.join(stab)})") + print(f" -> MANCANTI: {', '.join([s for s in XS_UNIVERSE if s not in ok])}") + print(" 📌 Il lotto minimo NON e' il vincolo: il piu' caro sta sotto la decina di dollari.") + print(" E' la stessa constatazione dell'ondata 22/08 (l'eseguibilita' a $600 non ha") + print(" bocciato niente per sei volte di fila). Il muro, se c'e', e' l'edge.") + return dict(quotate=ok, stabili=stab) + + +# ============================================================================================= +# 2) REPLICHE E CONTROLLI POSITIVI — prima di ogni delta +# ============================================================================================= +def sezione2(stab: list[str]) -> None: + sez("2) REPLICHE BIT-EXACT E CONTROLLI POSITIVI (un rilevatore non validato e' indistinguibile" + " da uno rotto)") + + sub("2a) il mio motore contro lo sleeve di PRODUZIONE") + mine = run(XS_UNIVERSE)["net"] + ref = to_daily(SL._xsec_returns()) + j = pd.concat({"a": mine, "b": ref}, axis=1, join="outer") + dm = float(np.nanmax(np.abs(j["a"].values - j["b"].values))) + print(f" run(19 gambe, canonico, fase 0) vs sleeves._xsec_returns : max|dif| = {dm:.3e}" + f" {'OK' if dm < 1e-12 else 'FAIL'}") + assert dm < 1e-12, "replica NON bit-exact: tutto il resto misurerebbe un'altra strategia" + print(f" Sharpe FULL 19 gambe {sh(mine):.3f} · HOLD {sh_hold(mine):.3f} " + f"(§50 riporta 1,265 col drag di funding dichiarato: qui il funding NON c'e', perche'") + print(" la domanda e' sulla STRUTTURA del segnale e il drag e' una costante additiva.)") + + sub("2b) CONTROLLO POSITIVO dello stimatore di AMPIEZZA (deve dare 1 e N sui casi noti)") + rng = np.random.default_rng(SEED) + ind = pd.DataFrame(rng.normal(size=(900, 11))) + one = pd.DataFrame(np.tile(rng.normal(size=(900, 1)), (1, 11))) + hlf = ind.copy() + common = rng.normal(size=900) + hlf = pd.DataFrame(0.7 * np.tile(common[:, None], (1, 11)) + 0.7 * ind.values) + for nm, M, atteso in (("11 gambe INDIPENDENTI", ind, "~11"), + ("11 gambe IDENTICHE", one, "1"), + ("11 gambe con fattore comune 50%", hlf, "fra 1 e 11")): + ne_r, rb = neff_rbar(M) + print(f" {nm:<34} rbar {rb:>+7.3f} N_eff(rbar) {ne_r:>8.2f} " + f"N_eff(autovalori) {neff_eig(M):>6.2f} atteso {atteso}") + print(" -> entrambi gli stimatori passano il controllo; la differenza fra i due si vede") + print(" solo quando rbar e' NEGATIVA, ed e' esattamente il caso di XS01 (sezione 3).") + + sub("2c) CONTROLLO POSITIVO dello stimatore di IC (con un segnale che BARA deve dare ~+1)") + def _t(m, se): + return "t inf (SE=0: IC costante)" if se <= 0 else f"t {m/se:+.1f}" + m, se, n = ic_stats(XS_UNIVERSE, cheat=True) + print(f" IC di un punteggio = ritorno FUTURO (look-ahead deliberato): {m:+.4f} " + f"({_t(m, se)}, n={n}) -> lo stimatore ha potenza") + m0, se0, n0 = ic_stats(XS_UNIVERSE, shuffle_seed=SEED) + print(f" IC di un punteggio PERMUTATO a caso : {m0:+.4f} " + f"({_t(m0, se0)}, n={n0}) -> e non inventa segnale dove non c'e'") + + sub("2d) identita' con la funzione importata da XSR01") + R = run(stab, k=2) + L = leg_pnl(R) + P = pd.DataFrame(R["W"], index=R["idx"], columns=R["cols"]) + S = pd.DataFrame(R["dret"], index=R["idx"], columns=R["cols"]) + ne_mine, _ = neff_rbar(L) + print(f" la mia `neff_rbar` e la `eff_breadth` di r0725_statarb_basket_gate implementano la") + print(f" STESSA formula N/(1+(N-1)*rbar); qui do' {ne_mine:.2f} sulla P&L per gamba di XS01.") + print(f" (`eff_breadth` non e' chiamabile direttamente su XS01: prende matrici di posizione e") + print(f" di ritorno-SPREAD di COPPIE, e XS01 non e' un paniere di coppie — vedi sezione 3.)") + _ = (P, S, XSR_EFF_BREADTH) + + +# ============================================================================================= +# IC — l'informazione del segnale, misurata direttamente +# ============================================================================================= +def ic_stats(universe: list[str], lookbacks=None, H: int | None = None, resid: bool = False, + cheat: bool = False, shuffle_seed: int | None = None, + every_day: bool = True, phase: int = 0): + """Rank-IC fra il punteggio e il ritorno a H giorni in avanti, con SE a blocchi (30 barre). + every_day=True usa TUTTI i giorni (osservazioni sovrapposte -> la SE a blocchi ne tiene conto) + perche' sui soli giorni di ribilanciamento le osservazioni sono 87 e non decidono niente.""" + lookbacks = CFG["lookbacks"] if lookbacks is None else lookbacks + H = CFG["H"] if H is None else H + C = prices(universe); px = C.values; n, A = px.shape + mlb = max(lookbacks) + dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0]) + bi = list(C.columns).index("BTC") if "BTC" in C.columns else 0 + rng = np.random.default_rng(shuffle_seed) if shuffle_seed is not None else None + ics = [] + for i in range(mlb, n - H): + if not every_day and i % H != phase: + continue + fwd = px[i + H] / px[i] - 1.0 + if cheat: + score = fwd.copy() + else: + rLs = [px[i] / px[i - L] - 1.0 for L in lookbacks] + if resid: + seg = dret[max(1, i - 90):i + 1] + b = np.array([np.cov(seg[:, j], seg[:, bi])[0, 1] / max(np.var(seg[:, bi]), 1e-12) + for j in range(A)]) + rLs = [rL - b * rL[bi] for rL in rLs] + score = np.zeros(A); cnt = 0 + for rL in rLs: + sd = rL.std() + if sd > 0: + score += (rL - rL.mean()) / sd; cnt += 1 + if not cnt: + continue + score /= cnt + if rng is not None: + score = score[rng.permutation(A)] + ics.append(spearmanr(score, fwd).statistic) + x = np.asarray(ics, float) + return float(x.mean()), block_se(x), len(x) + + +def block_se(x: np.ndarray, bl: int = 30, B: int = 500, seed: int = SEED) -> float: + rng = np.random.default_rng(seed); n = len(x); nb = int(np.ceil(n / bl)); out = [] + for _ in range(B): + st = rng.integers(0, max(1, n - bl), size=nb) + out.append(np.concatenate([x[s:s + bl] for s in st])[:n].mean()) + return float(np.std(out, ddof=1)) + + +def tail_spread(universe: list[str], k: int, resid: bool = False, lookbacks=None, + H: int | None = None): + """Rendimento a H giorni dei top-k MENO quello dei bottom-k: e' cio' che il portafoglio + incassa davvero (l'IC su tutta la sezione puo' essere 0 e le CODE portare informazione).""" + lookbacks = CFG["lookbacks"] if lookbacks is None else lookbacks + H = CFG["H"] if H is None else H + C = prices(universe); px = C.values; n, A = px.shape + mlb = max(lookbacks); kk = min(k, A // 2) + dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0]) + bi = list(C.columns).index("BTC") if "BTC" in C.columns else 0 + d = [] + for i in range(mlb, n - H): + rLs = [px[i] / px[i - L] - 1.0 for L in lookbacks] + if resid: + seg = dret[max(1, i - 90):i + 1] + b = np.array([np.cov(seg[:, j], seg[:, bi])[0, 1] / max(np.var(seg[:, bi]), 1e-12) + for j in range(A)]) + rLs = [rL - b * rL[bi] for rL in rLs] + score = np.zeros(A); cnt = 0 + for rL in rLs: + sd = rL.std() + if sd > 0: + score += (rL - rL.mean()) / sd; cnt += 1 + if not cnt: + continue + score /= cnt + o = np.argsort(score); f = px[i + H] / px[i] - 1.0 + d.append(float(f[o[-kk:]].mean() - f[o[:kk]].mean())) + x = np.asarray(d, float) + return x.mean(), block_se(x), len(x) + + +def z_spread(universe: list[str], k: int) -> float: + """Distanza media, in z-score, fra il gruppo long e il gruppo short: la SELETTIVITA'.""" + C = prices(universe); px = C.values; n, A = px.shape + lookbacks = CFG["lookbacks"]; mlb = max(lookbacks); kk = min(k, A // 2) + out = [] + for i in range(mlb, n): + rLs = [px[i] / px[i - L] - 1.0 for L in lookbacks] + score = np.zeros(A); cnt = 0 + for rL in rLs: + sd = rL.std() + if sd > 0: + score += (rL - rL.mean()) / sd; cnt += 1 + if not cnt: + continue + score /= cnt; o = np.argsort(score) + out.append(float(score[o[-kk:]].mean() - score[o[:kk]].mean())) + return float(np.mean(out)) + + +# ============================================================================================= +# 3) IL GATE — l'ampiezza effettiva +# ============================================================================================= +def sezione3(stab: list[str], quot: list[str]) -> dict: + sez("3) IL GATE — AMPIEZZA EFFETTIVA. Se le 11 gambe ne hanno gia' ~11, non c'e' niente da" + " recuperare") + + sub("3a) prima di misurare: il DEMEANING (la costruzione che creo' XSR01) qui e' un NO-OP" + " ALGEBRICO") + R = run(stab) + msum = float(np.abs(R["W"].sum(axis=1)).max()) + Rd = run(stab, demean=True) + dmx = float(np.nanmax(np.abs(R["net"].values - Rd["net"].values))) + print(f" XS01 mette +0,5/k sui top-k e -0,5/k sui bottom-k => sum_i w_i = 0 PER COSTRUZIONE.") + print(f" max|sum_i w_i| su tutte le barre : {msum:.3e}") + print(f" max|serie demeanata - serie canonica| : {dmx:.3e} -> IDENTICHE") + print(" 📌 XSR01 nacque perche' 50 coppie condividevano la gamba BTC: li' c'era un fattore") + print(" comune GROSSO nelle POSIZIONI, e toglierlo cambiava il portafoglio. XS01 e' gia'") + print(" dollar-neutral esatto: il demeaning non ha niente da togliere. Non e' un risultato") + print(" debole, e' un'IDENTITA' — e vale a ogni universo, a ogni k, per sempre.") + + sub("3b) LA TABELLA CHE DECIDE — N_eff della P&L per gamba") + print(f" {'universo / costruzione':<34}{'gambe':>7}{'rbar':>9}{'N_eff rbar':>13}" + f"{'N_eff autoval.':>16}{'Sh FULL':>10}") + rows = [("19 HL (canonico)", XS_UNIVERSE, dict()), + (f"{len(quot)} quotate Deribit (canonico)", quot, dict()), + (f"{len(stab)} Deribit >=1 anno (canonico)", stab, dict()), + (f"{len(stab)} Deribit — demean", stab, dict(demean=True)), + (f"{len(stab)} Deribit — resid vs BTC", stab, dict(kind="resid")), + (f"{len(stab)} Deribit — equal-risk", stab, dict(kind="ivol")), + (f"{len(stab)} Deribit — whitening", stab, dict(kind="white")), + (f"{len(stab)} Deribit — nudo L=30", stab, dict(lookbacks=(30,), disp_pct=0)), + (f"{len(stab)} Deribit — k=2", stab, dict(k=2)), + (f"{len(stab)} Deribit — H=20", stab, dict(H=20))] + out = {} + for nm, u, kw in rows: + r = run(u, **kw); L = leg_pnl(r) + ne_r, rb = neff_rbar(L); ne_e = neff_eig(L) + out[nm] = dict(neff=ne_e, sharpe=sh(r["net"])) + print(f" {nm:<34}{L.shape[1]:>7}{rb:>+9.3f}{ne_r:>13.2f}{ne_e:>16.2f}{sh(r['net']):>10.3f}") + + print("\n ⚠️ LA FORMULA IMPORTATA DA XSR01 DEGENERA QUI, e va detto invece di usarla: con rbar") + print(" NEGATIVA, N/(1+(N-1)*rbar) esplode sopra N (fino a ~33 su 11 gambe). Non e' un") + print(" errore di quella formula: e' che su un paniere di COPPIE rbar e' positiva (fattore") + print(" comune), mentre le gambe di un long/short cross-sezionale sono ANTI-correlate per") + print(" costruzione. Da qui in poi si legge il PARTICIPATION RATIO degli autovalori, che") + print(" sta sempre in [1, N] ed e' la definizione che il briefing nomina per prima.") + + n19 = out["19 HL (canonico)"]["neff"]; n11 = out[f"{len(stab)} Deribit >=1 anno (canonico)"]["neff"] + s19 = out["19 HL (canonico)"]["sharpe"]; s11 = out[f"{len(stab)} Deribit >=1 anno (canonico)"]["sharpe"] + pred = s19 * math.sqrt(n11 / n19) + print(f"\n 📌 RISPOSTA AL GATE: N_eff passa da {n19:.2f} (19 gambe) a {n11:.2f} (11 gambe) = " + f"-{100*(1-n11/n19):.0f}%,") + print(f" mentre lo Sharpe passa da {s19:.3f} a {s11:.3f} = -{100*(1-s11/s19):.0f}%.") + print(f" Sotto la legge fondamentale (IR = IC * sqrt(ampiezza)) la sola perdita di ampiezza") + print(f" predirebbe {pred:.3f}, cioe' {100*(s19-pred)/(s19-s11):.0f}% del divario. Il resto — " + f"l'{100-100*(s19-pred)/(s19-s11):.0f}% — NON e' ampiezza.") + print(" E NESSUNA costruzione la muove: tutte le righe stanno in una banda strettissima.") + print(" ⇒ Il filone COME POSTO si chiude qui: non c'e' ampiezza da recuperare. La sezione 4") + print(" misura dove sta davvero il divario, perche' 'non e' quello' non e' una diagnosi.") + return out + + +# ============================================================================================= +# 4) DOVE STA DAVVERO IL DIVARIO — selettivita' e IC +# ============================================================================================= +def sezione4(stab: list[str], quot: list[str]) -> dict: + sez("4) ALLORA DOV'E'? Due canali misurabili: SELETTIVITA' (quanto il ranking separa) e IC" + " (quanto il segnale sa)") + + sub("4a) SELETTIVITA' — con k=5 su 11 gambe si tengono 10 posizioni su 11: non si seleziona") + print(f" {'universo':<22}{'A':>4}{'k':>4}{'posizioni tenute':>19}{'spread z long-short':>22}") + for nm, u in (("19 HL", XS_UNIVERSE), (f"{len(stab)} Deribit", stab)): + for k in (5, 3, 2): + A = len(prices(u).columns); kk = min(k, A // 2) + print(f" {nm:<22}{A:>4}{k:>4}{f'{2*kk}/{A} = {2*kk/A:.0%}':>19}{z_spread(u, k):>22.3f}") + print(" 📌 `k=5` NON e' un parametro invariante di scala: fu tarato su A=19, dove seleziona il") + print(" 26% per lato. Su A=11 lo stesso 5 e' il 45% per lato. Trasferire il meccanismo a un") + print(" universo nuovo COI PARAMETRI CONGELATI (regola SOL, 22/08) qui vuol dire congelare") + print(" il QUANTILE, non il numero — ed e' una distinzione che cambia il risultato.") + + sub("4b) IC — il segnale sa qualcosa, su queste 11 gambe? (rank-IC contro il ritorno a 10g)") + print(f" {'universo':<34}{'IC':>10}{'SE blocchi':>13}{'t':>8}{'n':>7}") + ic = {} + for nm, u in ((f"19 HL (l'universo di XS01)", XS_UNIVERSE), + (f"{len(quot)} quotate Deribit", quot), + (f"{len(stab)} Deribit >=1 anno", stab), + (f"9 alt maturi (11 senza BTC/ETH)", [s for s in stab if s not in ("BTC", "ETH")]), + (f"8 gambe MANCANTI da sole", [s for s in XS_UNIVERSE if s not in stab])): + m, se, n = ic_stats(u); ic[nm] = (m, se) + print(f" {nm:<34}{m:>+10.4f}{se:>13.4f}{m/se:>8.2f}{n:>7}") + print("\n 📌 QUESTO e' il risultato del filone. Sulle 11 gambe che Deribit quota da un anno il") + print(" segnale NON ha informazione, e sui 9 alt maturi presi da soli e' NEGATIVO. Le 8") + print(" mancanti da sole non ce l'hanno nemmeno loro: l'informazione vive nel CONTRASTO fra") + print(" i due gruppi. XS01 e', in buona parte, una rotazione fra FASCE (major maturi contro") + print(" alt recenti ad alta dispersione) — non una selezione DENTRO una fascia.") + print(" ⇒ Nessuna costruzione puo' recuperare informazione che nel campione non c'e'.") + + sub("4c) e non e' 'sfortuna di listino' dentro una banda: e' FUORI dalla banda") + rng = np.random.default_rng(SEED) + subs = [list(rng.choice(XS_UNIVERSE, size=len(stab), replace=False)) for _ in range(N_SUBSETS)] + ics = np.array([ic_stats(u)[0] for u in subs]) + mine = ic_stats(stab)[0] + pct = 100.0 * (ics < mine).mean() + print(f" IC di {N_SUBSETS} sottoinsiemi CASUALI da {len(stab)} gambe fra le 19:") + print(f" mediana {np.median(ics):+.4f} [p10 {np.percentile(ics,10):+.4f}, " + f"p90 {np.percentile(ics,90):+.4f}] min {ics.min():+.4f}") + print(f" il sottoinsieme DERIBIT: {mine:+.4f} -> {pct:.0f}° percentile") + print(" 📌 §50 lo chiamava 'sfortuna di listino' e lo collocava al 9° percentile dello SHARPE.") + print(" Sull'IC — che e' la grandezza di cui lo Sharpe e' una conseguenza — sta sotto") + print(f" l'INTERO campione. Non e' una coda della distribuzione: le {len(stab)} gambe quotate da") + print(" un anno sono i major maturi, cioe' esattamente il sottoinsieme meno disperso.") + print(" Aspettare 2-3 listing non basta, e adesso si sa PERCHE'.") + return dict(ic=ic, subset_ics=ics, ic_deribit=mine) + + +# ============================================================================================= +# 5) LE COSTRUZIONI — la griglia dichiarata, de-luckata di fase +# ============================================================================================= +KINDS = ("canon", "resid", "ivol", "white") +KS = (1, 2, 3, 4, 5) +LBS = ((30, 90), (30,)) +HS = (10, 20) + + +def grid_cells(): + for kind, k, lb, H in itertools.product(KINDS, KS, LBS, HS): + yield dict(kind=kind, k=k, lookbacks=lb, H=H, + disp_pct=(CFG["disp_pct"] if lb == (30, 90) else 0)) + + +def band(u: list[str], **kw) -> np.ndarray: + return np.array([sh(run(u, phase=p, **kw)["net"]) for p in range(N_PHASES)]) + + +def sezione5(stab: list[str]) -> dict: + sez("5) LE COSTRUZIONI — griglia DICHIARATA (80 celle), e la banda di FASE che le de-lucka") + + sub("5a) la griglia alla fase canonica (fase 0) — e perche' non ci si puo' fermare qui") + cells = list(grid_cells()) + rows = [] + for c in cells: + s = run(stab, **c)["net"] + rows.append(dict(cell=c, ins=sh_ins(s), hold=sh_hold(s), full=sh(s), series=s)) + fulls = np.array([r["full"] for r in rows]) + print(f" {len(cells)} celle: FULL mediana {np.median(fulls):+.3f} max {fulls.max():+.3f} " + f"sd {fulls.std(ddof=1):.3f}") + best_is = max(rows, key=lambda r: r["ins"]) + best_ho = max(rows, key=lambda r: r["hold"]) + print(f"\n {'scelta':<22}{'cella':<44}{'IS':>8}{'HOLD':>8}{'FULL':>8}") + for nm, r in (("IN-SAMPLE-ONLY (onesta)", best_is), ("sull'HOLD-OUT (vietata)", best_ho)): + c = r["cell"] + lab = "%s k=%d lb=%s H=%d" % (c["kind"], c["k"], c["lookbacks"], c["H"]) + print(f" {nm:<22}{lab:<44}{r['ins']:>8.3f}{r['hold']:>8.3f}{r['full']:>8.3f}") + + sub("5b) BANDA DI FASE — la lente onesta di XS01 (ciclo H=10; i numeri headline sono sulla" + " fase 0, che CLAUDE.md registra come fortunata)") + casi = { + "canonico k=5 (11)": (stab, dict()), + "k=2 (11)": (stab, dict(k=2)), + "k=3 (11)": (stab, dict(k=3)), + "resid k=5 (11)": (stab, dict(kind="resid")), + "resid k=2 (11)": (stab, dict(kind="resid", k=2)), + "equal-risk k=2 (11)": (stab, dict(kind="ivol", k=2)), + "whitening k=2 (11)": (stab, dict(kind="white", k=2)), + "nudo L=30 k=2 (11)": (stab, dict(lookbacks=(30,), disp_pct=0, k=2)), + "H=20 k=2 (11)": (stab, dict(H=20, k=2)), + "canonico k=5 (19) RIF.": (XS_UNIVERSE, dict()), + "k=2 (19)": (XS_UNIVERSE, dict(k=2)), + } + B = {} + print(f" {'cella':<26}{'fase0':>9}{'MEDIANA':>10}{'p10':>9}{'p90':>9}{'>0':>7}" + f"{'pctl fase0':>12}") + for nm, (u, kw) in casi.items(): + v = band(u, **kw); B[nm] = v + print(f" {nm:<26}{v[0]:>9.3f}{np.median(v):>10.3f}{np.percentile(v,10):>9.3f}" + f"{np.percentile(v,90):>9.3f}{100*(v>0).mean():>6.0f}%" + f"{100*(v11.0f}°") + + sub("5c) DIFFERENZE APPAIATE per fase (mediana delle differenze, non differenza delle mediane)") + def dd(a, b): + d = B[a] - B[b] + return np.median(d), 100.0 * (d > 0).mean() + for a, b in (("k=2 (11)", "canonico k=5 (11)"), + ("k=3 (11)", "canonico k=5 (11)"), + ("resid k=2 (11)", "k=2 (11)"), + ("equal-risk k=2 (11)", "k=2 (11)"), + ("whitening k=2 (11)", "k=2 (11)"), + ("nudo L=30 k=2 (11)", "k=2 (11)"), + ("H=20 k=2 (11)", "k=2 (11)"), + ("k=2 (19)", "canonico k=5 (19) RIF.")): + m, f = dd(a, b) + print(f" {a:<26} MENO {b:<26} mediana {m:>+7.3f} positivo in {f:>3.0f}% delle fasi") + print("\n 📌 Il canale che porta TUTTO e' `k`, e solo sull'universo POVERO: su 19 gambe k=2 e") + print(" k=5 sono la stessa cosa. E' la firma della SELETTIVITA': k conta quando e' una") + print(" frazione grande dell'universo. Le tre costruzioni 'da ampiezza' (equal-risk,") + print(" whitening, residualizzazione) valgono, sopra a quello, poco o niente.") + + sub("5d) plateau su k (mediana delle 10 fasi) — punta o regione?") + print(f" {'k':>3}{'tenuto/lato (11)':>19}{'canon 11':>11}{'resid 11':>11}{'canon 19':>11}") + for k in KS: + a = np.median(band(stab, k=k)); b = np.median(band(stab, kind="resid", k=k)) + c = np.median(band(XS_UNIVERSE, k=k)) + print(f" {k:>3}{f'{min(k,5)}/11 = {min(k,5)/11:.0%}':>19}{a:>11.3f}{b:>11.3f}{c:>11.3f}") + print(" -> su 11 gambe la curva e' MONOTONA e la regione k in {1,2,3} e' un plateau, non una") + print(" punta; su 19 gambe e' piatta. Il meccanismo e' strutturale, non una cella fortunata.") + return dict(rows=rows, bands=B, best_is=best_is, best_ho=best_ho) + + +# ============================================================================================= +# 6) IL NULL CHE CONTA — permutazione cross-sezionale a FEE ZERO +# ============================================================================================= +def perm_series(universe: list[str], seed: int, k: int, kind: str = "canon", + lookbacks=None, H: int | None = None, disp_pct: int | None = None, + phase: int = 0) -> pd.Series: + """Identico a `run` ma il punteggio viene PERMUTATO fra gli asset a ogni ribilanciamento, e la + fee e' ZERO da entrambe le parti del confronto (25/07: un null a fee piena perde per COSTO).""" + lookbacks = CFG["lookbacks"] if lookbacks is None else lookbacks + H = CFG["H"] if H is None else H + disp_pct = CFG["disp_pct"] if disp_pct is None else disp_pct + C = prices(universe); px = C.values; n, A = px.shape + kk = min(k, A // 2); mlb = max(lookbacks) + dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0]) + bi = list(C.columns).index("BTC") if "BTC" in C.columns else 0 + rng = np.random.default_rng(seed) + W = np.zeros((n, A)); w = np.zeros(A); disp_hist: list[float] = [] + for i in range(n): + if i >= mlb and i % H == phase: + rLs = [px[i] / px[i - L] - 1.0 for L in lookbacks] + if kind == "resid": + seg = dret[max(1, i - 90):i + 1] + b = np.array([np.cov(seg[:, j], seg[:, bi])[0, 1] / max(np.var(seg[:, bi]), 1e-12) + for j in range(A)]) + rLs = [rL - b * rL[bi] for rL in rLs] + disp_i = float(np.mean([r.std() for r in rLs])) + thr = (np.percentile(disp_hist, disp_pct) + if (disp_pct > 0 and len(disp_hist) >= CFG["disp_minhist"]) else -np.inf) + if disp_i >= thr: + score = np.zeros(A); cnt = 0 + for rL in rLs: + sd = rL.std() + if sd > 0: + score += (rL - rL.mean()) / sd; cnt += 1 + if cnt: + score = score[rng.permutation(A)] / cnt + o = np.argsort(score); w = np.zeros(A) + w[o[-kk:]] = 0.5 / kk; w[o[:kk]] = -0.5 / kk + else: + w = np.zeros(A) + disp_hist.append(disp_i) + W[i] = w + gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1) + s = pd.Series(gross, index=C.index) + rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25) + sc = np.clip(np.nan_to_num(CFG["target_vol"] / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0) + return to_daily(pd.Series(s.values * sc, index=C.index)) + + +def sezione6(stab: list[str]) -> None: + sez("6) IL NULL CHE CONTA — permutazione cross-sezionale, ENTRAMBI I LATI A FEE ZERO") + print(" Perche' a fee zero (regola 25/07): permutare un punteggio ne fa esplodere il turnover;") + print(" a fee piena il null perderebbe per COSTO invece che per assenza d'informazione, e il") + print(" p-value uscirebbe trionfale e falso.") + print(f"\n {'cella':<26}{'candidato':>11}{'null med':>11}{'null p95':>11}{'p':>8}") + casi = [("canonico k=5 (11)", stab, dict(k=5)), + ("k=2 (11)", stab, dict(k=2)), + ("resid k=2 (11)", stab, dict(k=2, kind="resid")), + ("canonico k=5 (19) RIF.", XS_UNIVERSE, dict(k=5))] + for nm, u, kw in casi: + cand = sh(run(u, fee=0.0, **kw)["net"]) + nl = np.array([sh(perm_series(u, 9000 + j, **kw)) for j in range(N_PERM)]) + p = float((nl >= cand).mean()) + print(f" {nm:<26}{cand:>+11.3f}{np.median(nl):>+11.3f}{np.percentile(nl,95):>+11.3f}" + f"{p:>8.3f}") + print("\n ✅ CONTROLLO POSITIVO: il null HA potenza — sull'universo dove il segnale esiste (19") + print(" gambe) il candidato batte le permutazioni, su quello dove non esiste no. Un null") + print(" che non separasse i due casi non starebbe misurando niente.") + + sub("6b) e il p-value stesso va DE-LUCKATO: la riga sopra e' calcolata alla fase 0, che per" + " `resid k=2` sta al 90° percentile della sua banda") + print(" Nessuno in questo progetto aveva ancora de-luckato un p-value. Qui serve: se il") + print(" candidato e' valutato all'ancora fortunata, il suo p e' l'ancora fortunata del p.") + print(f"\n {'fase':>6}{'candidato':>12}{'p (100 perm)':>15}") + ps = [] + for ph in range(N_PHASES): + cand = sh(run(stab, fee=0.0, k=2, kind="resid", phase=ph)["net"]) + nl = np.array([sh(perm_series(stab, 7000 + 97 * ph + j, k=2, kind="resid", phase=ph)) + for j in range(100)]) + pv = float((nl >= cand).mean()); ps.append(pv) + print(f" {ph:>6}{cand:>+12.3f}{pv:>15.3f}") + ps = np.array(ps) + print(f"\n p MEDIANO sulle {N_PHASES} fasi: {np.median(ps):.3f} " + f"[min {ps.min():.3f}, max {ps.max():.3f}] sotto 0,05 in {100*(ps<0.05).mean():.0f}% " + f"delle fasi") + print(" 📌 Alla fase canonica `resid k=2` sembra battere il rumore; alla fase MEDIANA no.") + print(" Il verdetto del null cambia con l'ancora, e l'ancora e' una scelta, non un dato.") + + +# ============================================================================================= +# 7) IL GATE ONESTO + MDE +# ============================================================================================= +def sezione7(stab: list[str], G: dict) -> None: + sez("7) IL GATE ONESTO — i tre componenti reali di `study_family_honest`, e l'MDE") + print(" ⚠️ `altlib.study_family_honest` e' cablato sui candidati DIREZIONALI (factory ->") + print(" target_fn via `candidate_daily`) e non accetta uno sleeve cross-sezionale gia'") + print(" espresso in RENDIMENTI. Come per il gate del tenore di VRP01 (30/07) si usano i") + print(" suoi TRE componenti reali, importati e non riscritti.") + + rows, best_is, best_ho = G["rows"], G["best_is"], G["best_ho"] + sub("7a) (i) SELEZIONE IN-SAMPLE-ONLY — stessa regola di `altlib.select_cell_insample`") + src = al.select_cell_insample.__doc__ or "" + print(f" regola importata: 'rank grid cells by IN-SAMPLE (pre-HOLDOUT) standalone Sharpe'" + f" [{'trovata nel sorgente' if 'IN-SAMPLE' in src else 'NON verificata'}]") + c = best_is["cell"] + print(f" cella scelta AL BUIO: kind={c['kind']} k={c['k']} lookbacks={c['lookbacks']} " + f"H={c['H']}") + print(f" IS {best_is['ins']:+.3f} HOLD {best_is['hold']:+.3f} FULL {best_is['full']:+.3f}") + ch = best_ho["cell"] + print(f" (chi avesse scelto sull'HOLD-OUT prendeva kind={ch['kind']} k={ch['k']} " + f"lb={ch['lookbacks']} H={ch['H']}: HOLD {best_ho['hold']:+.3f})") + sub2 = [r for r in rows if r["cell"]["k"] >= 2] + b2 = max(sub2, key=lambda r: r["ins"]) + c2 = b2["cell"] + print(f"\n 🚨 ERRORE CATTURATO SU ME STESSO — la scelta al buio NON e' stabile alla") + print(f" DICHIARAZIONE della griglia. La mia prima stesura dichiarava k in {{2,3,4,5}}") + print(f" ({len(sub2)} celle) e li' la cella al buio era: kind={c2['kind']} k={c2['k']} " + f"lb={c2['lookbacks']} H={c2['H']}") + print(f" -> IS {b2['ins']:+.3f} HOLD {b2['hold']:+.3f} FULL {b2['full']:+.3f}, cioe' un" + f" candidato che REGGE fuori campione.") + print(f" Aggiungendo k=1 (che e' solo un'estensione del plateau, decisa PRIMA di") + print(f" guardare) la scelta passa a una cella che fa HOLD {best_is['hold']:+.3f}.") + print(" ⇒ Con ~1 anno di in-sample la selezione in-sample-only non e' un gate, e' una") + print(" monetina: la stessa procedura onesta da' due candidati opposti a seconda di") + print(" una scelta di perimetro arbitraria. E' la lezione del 22/08 sul deflated-") + print(" Sharpe ('la stessa griglia da' verdetti opposti secondo come la si partiziona')") + print(" vista un passo prima, sulla SELEZIONE invece che sulla deflazione.") + + sub("7b) (ii) DEFLATED SHARPE su TUTTE le celle dichiarate (`altlib.deflated_sharpe`)") + all_full = [r["full"] for r in rows] + dsr, sr0 = al.deflated_sharpe(best_is["full"], all_full, best_is["series"]) + print(f" N celle {len(all_full)} sd(Sharpe di griglia) {np.std(all_full, ddof=1):.3f}") + print(f" massimo atteso dal SOLO RUMORE: {sr0:.3f} candidato: {best_is['full']:.3f}") + print(f" DSR = {dsr:.3f} -> {'PASS' if dsr >= 0.95 else 'FAIL'} (soglia 0,95)") + med_phase = np.median(band(stab, **{k: v for k, v in best_is["cell"].items()})) + print(f" ⚠️ e il confronto giusto e' col numero DE-LUCKATO: la stessa cella a fase MEDIANA") + print(f" fa {med_phase:+.3f}, cioe' {'SOTTO' if med_phase < sr0 else 'sopra'} il massimo " + f"atteso dal rumore ({sr0:.3f}).") + + sub("7c) (iii) `altlib.marginal_vs_tp01` sulla cella scelta al buio") + try: + m = al.marginal_vs_tp01(best_is["series"]) + print(f" verdetto {m.get('marginal_verdict')} corr {m.get('corr_full')} " + f"robust_oos {m.get('robust_oos')} insample_edge {m.get('has_insample_edge')} " + f"is_hedge {m.get('is_hedge')} beats_noise {m.get('beats_noise_null')}") + except Exception as e: # noqa: BLE001 + print(f" non calcolabile: {type(e).__name__}: {e}") + print(" ⚠️ La serie comincia nel 2024: `marginal_vs_tp01` ha ~1 anno di in-sample e il suo") + print(" verdetto qui e' debole PER COSTRUZIONE (§50 lo dichiara sugli stessi dati).") + + sub("7d) MDE — quanta risoluzione ha davvero questo campione (in barre ATTIVE)") + R = run(stab, **best_is["cell"]) + att = np.abs(R["W"]).sum(axis=1) > 1e-12 + idx = R["idx"] + n_tot, n_att = len(idx), int(att.sum()) + n_h = int(att[idx >= HOLDOUT].sum()); n_i = int(att[idx < HOLDOUT].sum()) + print(f" campione: {n_tot} giorni ({idx.min().date()} -> {idx.max().date()}), di cui ATTIVI" + f" {n_att} ({n_att/n_tot:.0%})") + print(f" in-sample attive {n_i} · hold-out attive {n_h}") + print(f"\n {'finestra':<28}{'anni attivi':>13}{'SE(Sharpe)':>13}{'MDE 80%/5%':>13}") + for nm, na, s0 in (("FULL", n_att, best_is["full"]), ("in-sample (2024)", n_i, best_is["ins"]), + ("hold-out (2025+)", n_h, best_is["hold"])): + yrs = na / 365.25 + se = math.sqrt((1 + s0 * s0 / 2) / max(yrs, 1e-9)) + print(f" {nm:<28}{yrs:>13.2f}{se:>13.3f}{2.80*se:>13.3f}") + print("\n 📌 L'MDE sull'hold-out e' dell'ordine di 2,5-3 di Sharpe. Tutto cio' che questo") + print(" filone recupera (0,7-0,9) sta SOTTO la risoluzione del campione: e la selezione") + print(" in-sample gira su ~1 anno, dove SE(Sharpe) ~ 1 — cioe' e' quasi una monetina.") + + sub("7e) e al livello del SEGNALE la potenza non c'e' proprio (spread di coda, SE a blocchi)") + print(f" {'cella':<26}{'spread coda %/10g':>20}{'SE':>8}{'t':>8}") + for nm, u, kw in (("canonico k=5 (11)", stab, dict(k=5)), + ("k=2 (11)", stab, dict(k=2)), + ("resid k=2 (11)", stab, dict(k=2, resid=True)), + ("canonico k=5 (19) RIF.", XS_UNIVERSE, dict(k=5))): + m, se, n = tail_spread(u, **kw) + print(f" {nm:<26}{100*m:>19.2f}%{100*se:>8.2f}{m/se:>8.2f}") + print(" -> lo Sharpe recuperato su 11 gambe NON e' sostenuto da uno spread misurabile:") + print(" t ~ 0,3-0,6 contro ~1,9 dell'universo pieno. Un rendimento che si vede solo") + print(" nella serie composta e non nel suo ingrediente e' un rendimento da non citare.") + + +# ============================================================================================= +# 8) IL SUBSET NULL SULLA COSTRUZIONE — alza la MEDIANA o solo la cella di Deribit? +# ============================================================================================= +def sezione8(stab: list[str]) -> None: + sez("8) LA COSTRUZIONE E' UN MECCANISMO O E' UN FIT SU QUESTE 11? (null dei sottoinsiemi)") + print(" Se `k` piccolo e' un meccanismo, deve alzare la MEDIANA di sottoinsiemi casuali da 11,") + print(" non solo la cella che Deribit quota. (Stessa forma del null che il 25/07 mise GTAA6 al") + print(" 95° pctl dei 6-subset.)") + print(" ⚠️ E il confronto va DE-LUCKATO come tutti gli altri: la statistica per sottoinsieme") + print(f" e' la MEDIANA su {len(PH_SUB)} fasi ({', '.join(map(str, PH_SUB))}), non il valore alla fase 0.") + rng = np.random.default_rng(SEED) + subs = [list(rng.choice(XS_UNIVERSE, size=len(stab), replace=False)) for _ in range(N_SUBSETS)] + + def med_ph(u, **kw): + return float(np.median([sh(run(u, phase=ph, **kw)["net"]) for ph in PH_SUB])) + + print(f"\n {'costruzione':<20}{'med subset11':>14}{'p10':>9}{'p90':>9}{'Deribit':>10}" + f"{'pctl':>7}{'(fase 0)':>22}") + for nm, kw in (("canonico k=5", dict()), ("k=2", dict(k=2)), ("k=3", dict(k=3)), + ("resid k=2", dict(kind="resid", k=2))): + v = np.array([med_ph(u, **kw) for u in subs]) + mine = med_ph(stab, **kw) + v0 = np.array([sh(run(u, **kw)["net"]) for u in subs]) + m0 = sh(run(stab, **kw)["net"]) + print(f" {nm:<20}{np.median(v):>14.3f}{np.percentile(v,10):>9.3f}" + f"{np.percentile(v,90):>9.3f}{mine:>10.3f}{100*(v6.0f}°" + f"{f'{m0:+.3f} -> {100*(v022}") + print("\n -> `k` piccolo alza la mediana di TUTTI i sottoinsiemi: e' una proprieta' del") + print(" meccanismo su universo piccolo, non un fit sulle gambe di Deribit.") + print(" 📌 E la colonna di destra e' la ragione per cui si de-lucka: alla fase 0 `resid k=2`") + print(" mette il sottoinsieme di Deribit in ALTO nella banda, e una lettura fermata li'") + print(" avrebbe concluso l'opposto di quella de-luckata. (La prima stesura di questo") + print(" script scriveva 'Deribit resta in fondo a OGNI costruzione': era falso proprio") + print(" sulla cella che sembrava vincere.)") + + +# ============================================================================================= +# 9) ESEGUIBILITA' E NETTING +# ============================================================================================= +def sezione9(stab: list[str]) -> None: + sez("9) ESEGUIBILITA' E NETTING — il min-order per gamba, e cio' che NON netta col libro") + + sub("9a) il netting: 2 gambe su 11 stanno sullo stesso strumento del libro live") + inter = [s for s in stab if s in ("BTC", "ETH")] + print(f" Il libro live (`book_net_target`) somma TP01 e SKH01 in UN numero per asset su") + print(f" BTC/ETH e manda UN ordine. Delle {len(stab)} gambe, {len(inter)} ({', '.join(inter)}) " + f"netterebbero;") + print(f" le altre {len(stab)-len(inter)} sono strumenti che il libro NON tocca -> pagano un") + print(" min-order PROPRIO, e richiederebbero una riga in `src/live/deribit._CONTRACT`, cioe'") + print(" codice su un percorso con soldi veri.") + + sub("9b) quante gambe sopravvivono a min_order $5 (stessa regola di" + " `altlib.eval_weights_smallcap`)") + doc = al.eval_weights_smallcap.__doc__ or "" + print(f" regola importata: 'a desired position change whose notional |dw|*capital is below") + print(f" min_order is NOT executed' [{'confermata nel sorgente' if 'min_order' in doc else 'NON verificata'}]") + R = run(stab, k=2) + pos = R["W"] * R["scale"][:, None] # ⚠️ la posizione VERA e' W*scale, non W: + dpos = np.abs(np.diff(pos, axis=0, prepend=0.0)) # il vol-target muove il nozionale OGNI + tick = dpos[dpos > 1e-12] # giorno anche a pesi fermi. + rebal = np.zeros(len(R["W"]), bool) + rebal[1:] = (np.abs(np.diff(R["W"], axis=0)) > 1e-12).any(axis=1) + is_reb = np.repeat(rebal[:, None], pos.shape[1], axis=1)[dpos > 1e-12] + print(f"\n ⚠️ Errore catturato prima di pubblicare: la prima stesura misurava |dW| e trovava") + print(" 'ticket mediano $19, 100% eseguito'. Ma la posizione in dollari e' W*scale, e il") + print(" vol-target muove il nozionale TUTTI i giorni anche a pesi fermi -> quella misura") + print(" vedeva solo i ribilanci di segnale e faceva sparire la popolazione che il") + print(" pavimento taglia davvero. Sotto ci sono i ticket sulla posizione vera.") + print(f"\n {'capitale':>10}{'quota 15%':>12}{'ticket med':>13}{'ordini >= $5':>15}" + f"{'% NOZIONALE eseguito':>23}") + for cap in CAP_LADDER: + alloc = cap * W_SLEEVE + t = tick * alloc + ok = t >= MIN_ORDER + print(f" {('$%.0f' % cap):>10}{('$%.0f' % alloc):>12}{('$%.2f' % np.median(t)):>13}" + f"{f'{100*ok.mean():.0f}%':>15}{f'{100*t[ok].sum()/t.sum():.0f}%':>23}") + a0 = CAPITAL_TODAY * W_SLEEVE + t0 = tick * a0 + print(f"\n Le DUE POPOLAZIONI (§45), a ${CAPITAL_TODAY:.0f}:") + print(f" ribilanci di SEGNALE : {100*is_reb.mean():>5.1f}% degli ordini, " + f"{100*t0[is_reb].sum()/t0.sum():>5.1f}% del nozionale, ticket mediano " + f"${np.median(t0[is_reb]):.2f}") + print(f" deriva del VOL-TARGET : {100*(~is_reb).mean():>5.1f}% degli ordini, " + f"{100*t0[~is_reb].sum()/t0.sum():>5.1f}% del nozionale, ticket mediano " + f"${np.median(t0[~is_reb]):.2f}") + print("\n 📌 Il muro NON e' la taglia (7ª volta nell'ondata): il pavimento taglia soprattutto") + print(" la popolazione piccola (deriva del vol-target), che vale poco nozionale, ed e'") + print(" esattamente il meccanismo misurato in §45 quando cadde il '$20k di XS01'.") + print(" Il muro e' l'IC della sezione 4.") + + +# ============================================================================================= +# MAIN +# ============================================================================================= +def main() -> None: + global _ARGS + ap = argparse.ArgumentParser() + ap.add_argument("--no-net", action="store_true", help="usa solo la cache del venue") + _ARGS = ap.parse_args() + + print("=" * 104) + print(" §56 XS-AMPIEZZA — su un universo POVERO si recupera ampiezza EFFETTIVA senza" + " aggiungere gambe?") + print(f" {dt.datetime.now(dt.timezone.utc):%Y-%m-%d %H:%M UTC} · sola lettura · nessun ordine ·" + f" solo GET pubbliche con pacing") + print("=" * 104) + print(" ATTESA A PRIORI " + __doc__.split("ATTESA A PRIORI")[1].split("LA FAMIGLIA")[0].rstrip()) + + V = venue_census() + cens = sezione1(V) + stab, quot = cens["stabili"], cens["quotate"] + if len(stab) < 8: + print("\n ⚠️ meno di 8 gambe stabili: il filone non e' misurabile, mi fermo.") + return + + sezione2(stab) + print(f"\n {clock()} sezione 2 completata") + NEFF = sezione3(stab, quot) + print(f"\n {clock()} sezione 3 completata") + ICS = sezione4(stab, quot) + print(f"\n {clock()} sezione 4 completata") + G = sezione5(stab) + print(f"\n {clock()} sezione 5 completata") + sezione6(stab) + print(f"\n {clock()} sezione 6 completata") + sezione7(stab, G) + print(f"\n {clock()} sezione 7 completata") + sezione8(stab) + print(f"\n {clock()} sezione 8 completata") + sezione9(stab) + + # ------------------------------------------------------------------ verdetto + sez("VERDETTO") + n19 = NEFF["19 HL (canonico)"]["neff"] + n11 = NEFF[f"{len(stab)} Deribit >=1 anno (canonico)"]["neff"] + bands = G["bands"] + med_can = np.median(bands["canonico k=5 (11)"]) + med_k2 = np.median(bands["k=2 (11)"]) + med_19 = np.median(bands["canonico k=5 (19) RIF."]) + d = bands["k=2 (11)"] - bands["canonico k=5 (11)"] + ic19 = ICS["ic"]["19 HL (l'universo di XS01)"] + ic11 = ICS["ic"][f"{len(stab)} Deribit >=1 anno"] + print(f" (1) IL GATE DICE NO: l'ampiezza effettiva scende solo da {n19:.2f} a {n11:.2f} " + f"(-{100*(1-n11/n19):.0f}%) mentre lo") + print(" Sharpe scende dell'86%. Nessuna costruzione la muove, e il DEMEANING — la mossa") + print(" che creo' XSR01 — e' un NO-OP ALGEBRICO su XS01 (sum dei pesi = 0 per costruzione).") + print(f" (2) IL DIVARIO E' ALTROVE, ed e' DUE cose: SELETTIVITA' (k=5 su 11 gambe tiene 10") + print(f" posizioni su 11: non seleziona) e IC. La selettivita' SI recupera — il quantile") + print(f" invariante di scala porta la mediana di fase da {med_can:+.3f} a {med_k2:+.3f} " + f"(delta appaiato") + print(f" {np.median(d):+.3f}, positivo in {100*(d>0).mean():.0f}% delle fasi, plateau su k in " + f"{{1,2,3}}), contro {med_19:+.3f} dell'universo pieno.") + print(f" (3) L'IC NO: {ic11[0]:+.4f} (t {ic11[0]/ic11[1]:+.2f}) sulle {len(stab)} gambe contro " + f"{ic19[0]:+.4f} (t {ic19[0]/ic19[1]:+.2f}) sulle 19,") + print(f" e sotto TUTTI i {N_SUBSETS} sottoinsiemi casuali da {len(stab)}. Sui 9 alt maturi da soli e'") + print(" NEGATIVO: XS01 e' in buona parte una rotazione fra FASCE, non dentro una fascia.") + print(" ⚠️ Sfumatura dovuta: sotto una costruzione a k piccolo il sottoinsieme di Deribit") + print(" risale a meta' banda sullo SHARPE (65-80° pctl de-luckato) pur restando ultimo") + print(" sull'IC — coerente, perche' con k piccolo contano solo le CODE del ranking e non") + print(" tutta la sezione. Ma lo spread di quelle code ha t ~ 0,2-0,6: e' la stessa") + print(" grandezza del punto (4), non un'obiezione.") + print(" (4) E TUTTO IL RECUPERO STA SOTTO LA RISOLUZIONE DEL CAMPIONE, per quattro strade") + print(" indipendenti: (a) MDE dell'hold-out ~2,3 di Sharpe su ~1,6 anni attivi; (b) lo") + print(" SPREAD DI CODA che dovrebbe pagarlo ha t ~ 0,2-0,6 contro ~1,9 dell'universo") + print(" pieno — un rendimento che si vede nella serie composta e non nel suo ingrediente;") + print(" (c) il null di permutazione a fee zero non separa il candidato dal rumore alla") + print(" fase MEDIANA (lo separa solo alla fase 0, che e' la fortunata) mentre lo separa") + print(" su 19 gambe = controllo positivo; (d) il deflated-Sharpe sulla griglia dichiarata") + print(" FALLISCE, e la selezione in-sample-only gira su ~1 anno dove SE(Sharpe) ~ 1,7.") + print(" (4-bis) 🚨 E LA SELEZIONE ONESTA NON E' STABILE ALLA DICHIARAZIONE DELLA GRIGLIA:") + print(" togliendo k=1 dalla famiglia (una scelta di perimetro, non un dato) la cella") + print(" scelta al buio passa da una che crolla fuori campione a una che regge. Con questo") + print(" campione la procedura onesta e' una monetina, e va detto invece di citarne") + print(" l'esito.") + print(" (5) L'eseguibilita' non boccia niente: lotti da $0,09 a ~$8, e a $635 passa gia' la") + print(" maggior parte del nozionale. Il muro e' l'edge, non il capitale.") + print("\n ⇒ `NO — E LA CADUTA NON ERA AMPIEZZA: E' SELETTIVITA' (RECUPERABILE, SOTTO L'MDE) PIU'") + print(" UN IC CHE SU QUELLE 11 GAMBE E' ZERO`") + print("\n Cio' che questo filone CORREGGE di §50: l'etichetta 'il 62% della caduta e' AMPIEZZA'") + print(" descriveva il NUMERO DI GAMBE, non l'ampiezza effettiva — che quasi non si muove. E la") + print(" 'sfortuna di listino' non e' una coda della distribuzione: sull'IC il sottoinsieme di") + print(" Deribit sta sotto l'intero campione, per una ragione strutturale (sono i major maturi).") + print(" Cio' che NON cambia: XS01 resta bloccato dal venue, e aspettare 2-3 listing non basta.") + print("\n 📌 UNA COSA DA PORTARSI VIA ANCHE FUORI DA QUI: `k=5` non e' un parametro invariante") + print(" di scala. Congelare un meccanismo su un universo nuovo vuol dire congelare il") + print(" QUANTILE, non il numero di posizioni — e su XS01 la differenza vale ~0,8 di Sharpe.") + print(f"\n {clock()} fine.") + + +if __name__ == "__main__": + main() From 4a849f3f743c5d7da8584ea60c788a73c7739ada Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 03:04:32 +0000 Subject: [PATCH 099/114] =?UTF-8?q?registro=20=C2=A756:=20XS-AMPIEZZA=20?= =?UTF-8?q?=E2=80=94=20la=20caduta=20non=20era=20ampiezza=20ma=20IC,=20e?= =?UTF-8?q?=20k=3D5=20non=20e'=20invariante=20di=20scala?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 113 +++++++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 4b2e75f..e31809f 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -721,7 +721,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 53 | SKEPTIC-LEVA | il gradino di leva 1,25x sopravvive a un attacco deliberato, col funding dentro? | **REGGE 5/6, INCRINA 1/6** — il **1,25x sopravvive**, il **1,50x NO**; k massimo difendibile **1,40**. L'incrinatura e' il ramo `eq_fallback`, dove **la scala non apre il buco: lo MOLTIPLICA** | | 54 | PREVDAY-GATE | il piu' forte candidato misurato sta in monitor da giugno senza gate: quanto vale al gate onesto? | **GATE PREVDAY-01 SCRITTO — 8/10 condizioni oggi**, e le due che mancano sono la stessa cosa: 🚨 **la cella che GIRA non e' quella che la selezione onesta sceglie** — e la selezione compra il libro **LONG-FLAT**, cioe' butta via la gamba per cui PREVDAY fu promosso | | 55 | COSTO-CAPITALE | i muri sono calcolati a costo d'esecuzione COSTANTE, ma il costo cresce col capitale: dove sta il muro vero? | _in corso_ | -| 56 | XS-AMPIEZZA | su un universo povero (11 gambe), si recupera ampiezza EFFETTIVA senza aggiungere gambe? | _in corso_ | +| 56 | XS-AMPIEZZA | su un universo povero (11 gambe), si recupera ampiezza EFFETTIVA senza aggiungere gambe? | **NO — e la caduta NON era ampiezza (solo il 14%)**: e' **selettivita'** (recuperabile ma sotto l'MDE) **piu' un IC che su quelle 11 gambe vale ZERO**. 🚨 E `k=5` **non e' invariante di scala** | | — | **NOTIFIER** (audit di fatto) | 🚨 **PUNTO SINGOLO DI GUASTO nella rete di sicurezza** | il `send()` di Telegram e' **un solo tentativo da 10s, senza retry, con l'esito MAI registrato** — e lo stato `alerted=True` viene scritto **prima** dell'invio: un 🚨 di `venue_watch` perso e' perso **per l'intero episodio**. Tasso di fallimento **misurato 6,9%** (2/29, IC95 [1,9%, 22,0%]) | | 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 | @@ -3319,3 +3319,114 @@ gamba short compensasse il funding). **VERDETTO: `IL GATE E' SCRITTO E OGGI PASSA 8 CONDIZIONI SU 10 — LE DUE CHE MANCANO SONO LA STESSA COSA: LA CELLA CHE GIRA NON E' QUELLA CHE LA SELEZIONE ONESTA SCEGLIE`**. **Nessun cambio al libro live proposto.** + +--- + +## 56 — XS-AMPIEZZA (recuperare ampiezza su un universo povero) + +`scripts/research/r0823_xs_ampiezza.py` (**235 s**). **Sola lettura provata** (`git status` su +`src/ config/ scripts/live/ scripts/cron_*.sh tests/ data/ docs/` **vuoto**, 0 ordini, GET pubbliche +con pacing e astensione). ✅ Replica **bit-exact** contro `sleeves._xsec_returns` +(`max|dif| = 0,0`) prima di ogni delta. Griglia dichiarata prima: 4 kind × 5 k × 2 lookback × 2 H = +**80 celle**. + +### N_eff — il gate, in cima perche' decide + +| universo / costruzione | gambe | rbar | **N_eff (autovalori)** | Sh FULL | +|---|---|---|---|---| +| **19 HL (canonico)** | 19 | −0,028 | **9,34** | 1,336 | +| 14 quotate Deribit | 14 | −0,049 | **8,03** | 0,893 | +| **11 Deribit ≥1 anno** | 11 | −0,067 | **7,23** | 0,193 | +| 11 — demean / resid / equal-risk / whitening | 11 | — | 7,13-7,21 | 0,193 / 0,550 / 0,088 / 0,051 | +| 11 — nudo L=30 / **k=2** | 11 | — | 8,47 / 8,30 | 0,243 / **0,669** | + +🚨 **N_eff scende del 23% mentre lo Sharpe scende dell'86%**: sotto `IR = IC·√ampiezza` la sola +ampiezza predirebbe 1,175, cioe' **il 14% del divario**. E **nessuna costruzione la muove** +(7,13-8,47). ⇒ **L'etichetta "62% e' AMPIEZZA" di §50 descriveva il NUMERO DI GAMBE, non l'ampiezza +effettiva.** + +⚠️ **La formula importata da XSR01 DEGENERA qui, e l'agente l'ha detto invece di usarla:** con +`rbar` negativa `N/(1+(N−1)·rbar)` **esplode sopra N** (fino a 33 su 11 gambe). Non e' un difetto di +quella formula — su un paniere di **coppie** rbar e' positiva, mentre le gambe di un long/short +cross-sezionale sono **anti-correlate per costruzione**. Si legge il **participation ratio**. + +📌 **E il demeaning — la mossa che CREO' XSR01 — su XS01 e' un NO-OP ALGEBRICO:** XS01 mette ++0,5/k e −0,5/k, quindi **`sum(w) = 0` per costruzione** (`max|Σw| = 2,8e-17`, serie identiche a +2,2e-16). *Non e' un risultato debole: e' un'identita' valida a ogni universo e ogni k.* + +### Il canale che porta tutto e' `k` — e solo sull'universo povero + +Differenze **appaiate sulle 10 fasi** del ciclo H=10: + +| confronto | mediana | fasi >0 | +|---|---|---| +| **k=2 MENO canonico k=5 (11 gambe)** | **+0,832** | **100%** | +| k=3 MENO canonico k=5 (11) | +0,650 | 100% | +| resid k=2 MENO k=2 (11) | +0,125 | 80% | +| equal-risk / whitening / nudo / H=20 | −0,06 / −0,02 / −0,37 / −0,69 | 20/50/20/**0%** | +| **k=2 MENO canonico k=5 (19 gambe)** | **−0,080** | **30%** | + +🚨 **`k=5` NON E' UN PARAMETRO INVARIANTE DI SCALA.** Fu tarato su A=19, dove seleziona il **26% +per lato**; su A=11 e' il **45%** — con k=5 si tengono **10 posizioni su 11**, e lo spread z +long−short crolla da **2,08 a 1,43**. Plateau monotono su k∈{1,2,3}, **piatto su 19 gambe**. + +**Il null a fee zero, e il p-value de-luckato** (mai fatto prima in questo progetto): canonico k=5 +p=0,355 · k=2 p=0,165 · **resid k=2 alla fase 0 p=0,005** — ⚠️ ma quella fase sta al **90° pctl** per +quella cella, e de-luckando sulle 10 fasi **p mediano 0,130, sotto 0,05 solo nel 40%**. +*Il verdetto del null cambia con l'ancora, e l'ancora e' una scelta.* ✅ Controllo positivo: +canonico su 19 gambe p=0,010. + +🚨 **DOVE STA DAVVERO IL DIVARIO: L'IC.** + +| universo | IC | t | +|---|---|---| +| 19 HL | **+0,0565** | **+2,29** | +| 14 quotate | +0,0108 | +0,44 | +| **11 Deribit ≥1 anno** | **−0,0270** | −0,88 | +| 9 alt maturi (11 senza BTC/ETH) | **−0,0652** | **−2,07** | +| le 8 mancanti da sole | +0,0075 | +0,26 | + +Su **40 sottoinsiemi casuali da 11**: mediana +0,059, minimo +0,0115 → **Deribit al 0° percentile, +fuori dalla banda**. §50 lo chiamava *"sfortuna di listino"* al 9° pctl dello **Sharpe**; **sull'IC +e' strutturale**. +📌 **E poiche' ne' i major ne' le mancanti hanno IC da soli, l'informazione vive nel CONTRASTO FRA +LE FASCE: XS01 e' in buona parte una rotazione major-maturi contro alt-recenti, non una selezione +dentro una fascia.** *(E' la descrizione piu' precisa di XS01 prodotta finora, e arriva da un +tentativo di ripararlo.)* + +**MDE e gate.** 966 giorni, **936 attivi (97%)**, in-sample = il solo **2024**. SE(Sharpe): FULL +0,66 · **in-sample 1,75** · hold-out 0,83 → **MDE hold-out 2,34**. Lo spread di coda che dovrebbe +pagare il recupero ha **t 0,24 (k=2) / 0,57 (resid k=2)** contro 1,91 dell'universo pieno. Cella al +buio `white k=1` → IS +1,90 / **HOLD −0,53**; **DSR 0,158 FAIL** (massimo atteso dal rumore +**1,065** contro candidato 0,459); `marginal_vs_tp01` NEUTRAL. + +🚨 **ERRORE CATTURATO SU SE STESSO, e vale come regola: la selezione in-sample-only NON e' stabile +alla dichiarazione della griglia.** Con k∈{2,3,4,5} (64 celle) la cella al buio era `resid k=2` — +IS +1,46 / **HOLD +1,33** / FULL 1,38, **un candidato che REGGE**. Aggiungendo k=1 (estensione del +plateau, **decisa prima di guardare**) diventa una cella che fa **HOLD −0,53**. **Con ~1 anno di +in-sample la procedura onesta e' una monetina.** + +⚠️ **Secondo errore catturato:** la prima misura d'eseguibilita' usava `|ΔW|` e dava *"ticket $19, +100% eseguito"*. La posizione in dollari e' `W·scale`, e il vol-target muove il nozionale ogni +giorno anche a pesi fermi. Corretta: a $635 **83% del nozionale** eseguito (96% a $5k) — e +**riproduce per via indipendente le due popolazioni di §45** (ribilanci di segnale 11,9% degli +ordini / **62,5% del nozionale**; deriva del vol-target 88,1% / 37,5%). **Il muro non e' il +capitale, e' l'IC.** + +**Eseguibilita' dal venue:** 14/19 quotate, di cui SUI (18/08), **APT (21/08, nessuna quota)** e +AAVE (15/08) listate negli ultimi 8 giorni; **11 da ≥1 anno**; lotti **$0,01-7,72**. Solo **2 delle +11** (BTC, ETH) netterebbero col libro live: **le altre 9 pagano min-order proprio** e vorrebbero +righe nuove in `_CONTRACT`. + +**Attesa a priori del coordinatore: giusta nella conclusione, SBAGLIATA nel meccanismo** — dicevo +*"il 62% e' ampiezza e l'ampiezza non si compra"*; l'ampiezza **non e' quasi per niente il +problema**. + +📌 **DA PORTARSI VIA: congelare un meccanismo su un universo nuovo vuol dire congelare il +QUANTILE, non il numero di posizioni.** Su XS01 la differenza vale **~0,8 di Sharpe**. *(Stessa +famiglia della lezione SOL del 22/08: il criterio di selezione di SKH01 non sopravvive a un asset +nuovo.)* + +**VERDETTO: `NO — E LA CADUTA NON ERA AMPIEZZA: E' SELETTIVITA' (RECUPERABILE, MA SOTTO L'MDE) PIU' +UN IC CHE SU QUELLE 11 GAMBE VALE ZERO`**. XS01 resta bloccato dal venue, **e adesso si sa +perche'**. **Libro, pesi, cron, config INVARIATI.** From 73207f79b90a37c34e3324ac394f702730efaf5e Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 03:28:10 +0000 Subject: [PATCH 100/114] =?UTF-8?q?research(costo-capitale):=20il=20costo?= =?UTF-8?q?=20d'esecuzione=20e'=20una=20funzione=20del=20capitale,=20e=20i?= =?UTF-8?q?l=20muro=20non=20si=20muove=20=E2=80=94=20a=20$635=20il=20vinco?= =?UTF-8?q?lo=20e'=20il=20pavimento=20min=5Forder,=20di=20segno=20opposto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §55 COSTO-CAPITALE. Tutti i muri pubblicati ($272k/$278k/$313k/$325k, traiettorie, versamenti) sono calcolati con un costo d'esecuzione COSTANTE, mentre un muro che si raggiunge accumulando attraversa tutte le taglie. Qui il costo diventa una funzione, misurata camminando il libro Deribit vero. [VENUE] 50 istantanee di BTC_USDC-PERPETUAL / ETH_USDC-PERPETUAL in 35 minuti (domenica notte UTC = la finestra piu' sottile della settimana, quindi conservativa). Mezzo spread 0,0065 / 0,0207 bps = UN TICK; costo sotto ~2 bps fino a $500k per ordine; il libro visibile finisce a $2,8M (BTC) / $1,2M (ETH). [CALC] Muro L3 congiunta $313.143 -> $315.080 col costo endogeno (+0,6%, DENTRO la risoluzione Monte Carlo del muro stesso, punto fisso convergente in una iterazione). La traiettoria EUR500/mese non si muove (P(20a) 85,1% -> 84,7%). Saturazione a ~$9,5M per argmax di C x drift(C) e a $10M per il primo 1% di nozionale non eseguibile in un istante: due definizioni indipendenti nella stessa decade, oltre un ordine di grandezza sopra il muro. Il gradino di leva sopravvive (k* 12,50x -> 12,25x alla taglia del muro; morde solo a $5M, dove crolla a 1,75x). Le due riserve degli scettici sono misurate e non mordono qui: il costo che si annullava in §52 e' quello dello SPOT (spread 100-500x piu' largo), e la partecipazione del 21,9% di §53 e' la quota di una barra 5m a volume basso — l'ordine di oggi e' lo 0,13% (BTC) / 0,43% (ETH) della profondita' a 1 bps. Trovato per strada, ed e' il fatto piu' utile: alla taglia di OGGI il costo che le tabelle non contengono ha segno NEGATIVO. Il drift a $635 e' 13 bps/anno PIU' BASSO che a $5.000 per il pavimento min_order, ~2x cio' che lo slippage costa fra $313k e $1M. Repliche 8/8 (TP01/SKH01/libro bit-exact; $272.061 e $258.338 al dollaro; accumula bit-exact; walk_cost_bps identica a §52) + R9 k* = 12,25x come FD.k_star, e due controlli POSITIVI obbligatori. Due errori miei catturati e pubblicati: banda p10/p50/p90 che si incrociava oltre il libro visibile (tre oggetti-curva separati), e un controllo positivo DEGENERE per costruzione (un costo costante sparisce da un drag differenziale). Sola lettura, nessun ordine, solo GET pubbliche. Libro, pesi, cron, config INVARIATI. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf --- scripts/research/r0823_costo_capitale.py | 1113 ++++++++++++++++++++++ 1 file changed, 1113 insertions(+) create mode 100644 scripts/research/r0823_costo_capitale.py diff --git a/scripts/research/r0823_costo_capitale.py b/scripts/research/r0823_costo_capitale.py new file mode 100644 index 0000000..583e25b --- /dev/null +++ b/scripts/research/r0823_costo_capitale.py @@ -0,0 +1,1113 @@ +#!/usr/bin/env python3 +"""r0823_costo_capitale.py — §55 COSTO-CAPITALE: il muro quando il costo d'esecuzione +NON e' una costante ma una FUNZIONE del capitale. + +IL BUCO +------- +Ogni muro di capitale pubblicato dal progetto ($272k, $278k, $313k, $325k, la traiettoria da +$635, le tabelle dei versamenti) e' calcolato con un costo d'esecuzione COSTANTE: le fee +proporzionali di `backtest_signals` (0,10% RT) piu' il pavimento `min_order` di +`CC.tp01_realistic`. Ma un muro che si raggiunge ACCUMULANDO attraversa tutte le taglie, e il +costo vero cresce con la taglia dell'ordine. Il costo non e' un parametro: e' **una funzione +lungo il cammino**. + +La domanda e' nata due volte nella stessa notte, da due filoni che non si parlavano: + * §52 SKEPTIC-SPOT — camminando entrambi i libri, il differenziale spot-perp passa da + +0,72 bps a $600 a **+25,66 bps a $272k**: il lead dello spot si annulla esattamente alla + taglia del muro che pretende di spostare. Follow-up dichiarato: «il muro rifatto con costo + d'esecuzione FUNZIONE DEL CAPITALE». + * §53 SKEPTIC-LEVA — «§5 ha misurato un fill al 21,9% del volume della sua barra 5m a $635; + con partecipazione lineare 1,25x lo porta al 27,4% SUBITO. Va misurata PRIMA del gradino.» + * §5 SLIP-AUDIT porta gia' una **data di scadenza esplicita**: «quel 22% diventa 172% di una + barra 5m a $5.000 e 687% a $20.000. Va rifatta a ogni salto di taglia.» + +COSA FA QUESTO SCRIPT +--------------------- + 1. [VENUE] Misura `costo_bps(taglia)` camminando il libro reale di BTC_USDC-PERPETUAL e + ETH_USDC-PERPETUAL su piu' istantanee, e ne riporta la BANDA (§45: due istantanee a 4 + minuti differivano del 79% — un'istantanea non e' una distribuzione). + 2. [BACKTEST] Ricava la distribuzione VERA delle taglie d'ordine del libro live (TP01 75 / + SKH01 25) a ogni capitale. Non l'ordine medio: il costo e' convesso, la media dei costi + non e' il costo della media. + 3. [CALC] Rifa' il libro con il costo DENTRO, itera il muro come PUNTO FISSO + (`r0726_wall_fixedpoint`, importato) e riporta se converge. + 4. [CALC] Trova il PUNTO DI SATURAZIONE con DUE definizioni indipendenti — un limite DURO + puramente misurato (la taglia a cui l'1% del nozionale annuo non e' piu' eseguibile in un + istante) e uno ECONOMICO (l'argmax di `C x drift(C)`, dove il capitale marginale smette di + comprare reddito). Numero mai calcolato dal progetto. + 5. [CALC] Rifa' la traiettoria «EUR500/mese da $635» col costo LUNGO IL CAMMINO + (`r0822d_piano_vero`, importato). + 6. [CALC] Il gradino di leva col costo endogeno: k massimo. + +REPLICHE OBBLIGATORIE PRIMA DI OGNI DELTA (regola del progetto: prima di pubblicare un numero +nuovo, far riprodurre alla macchina quello vecchio) + R1 la mia TP01 con costo spento == `FD.tp01_realistic` bit-exact + R2 la mia SKH01 con costo spento == `FD.skh_book` bit-exact + R3 il mio libro con costo spento == `FD.book_75_25` bit-exact + R4 muro L0 LORDO sul vintage 2026-08-07 $272.061 al dollaro + R5 muro L1 +FISCO sul vintage 2026-08-07 $258.338 al dollaro + R6 muro L3 CONGIUNTA sulla serie di oggi $313k (§36) + R7 `accumula_costo` con drag spento == `PN.accumula` bit-exact + R8 la mia `walk_cost_bps` == quella di `r0823_skeptic_spot` identita' su libro sintetico + R9 k* a costo ZERO == `FD.k_star` sul libro pubblicato 12,25x (§35) + piu' DUE controlli POSITIVI obbligatori: un costo di 200 bps per ordine deve uccidere il + libro, e un drag di 3%/anno sopra $100k deve muovere la traiettoria. Un modello di costo che + non morde mai e' indistinguibile da uno rotto. + +ETICHETTE DI PROVENIENZA + [VENUE] misurato dall'API pubblica Deribit in questa sessione + [LOG] dai log/fill reali del libro live + [BACKTEST] dalla serie storica certificata + [CALC] aritmetica sui due precedenti + [DEDOTTO] estrapolazione oltre la regione misurata + [ASSUNTO] scelta di modello dichiarata + +ASSUNZIONE PRINCIPALE, DICHIARATA IN TESTA + Il libro d'ordini e' una FOTOGRAFIA DEL PRESENTE. Proiettarla su 7 anni di storia (2019-2026, + quando la profondita' del venue era una frazione di quella di oggi) e' un'ASSUNZIONE, e va nel + verso OTTIMISTA: il numero che ne esce e' un limite superiore della capacita' STORICA, non una + misura di essa. Sul futuro — che e' cio' che il muro deve attraversare — l'assunzione e' la + costanza della profondita', non la sua crescita. + Nell'altro verso, e su un asse diverso: il campione e' raccolto di DOMENICA NOTTE UTC, cioe' + nella finestra piu' sottile della settimana. Dentro il presente e' quindi CONSERVATIVO — un + campione in orario di punta darebbe costi piu' bassi. + +SOLA LETTURA / RETE / SOLDI VERI + Nessuna scrittura nel repo (cache FUORI, nello scratchpad). Nessun ordine, mai: solo GET + PUBBLICHE senza chiave, pacing <=2 req/s, astensione nei minuti :05-:10 e :24-:30 (finestre + del cron di produzione). + + uv run python scripts/research/r0823_costo_capitale.py + uv run python scripts/research/r0823_costo_capitale.py --no-net # solo cache + uv run python scripts/research/r0823_costo_capitale.py --rounds 8 --gap 35 + uv run python scripts/research/r0823_costo_capitale.py --fetch-only --rounds 22 --gap 55 +""" +from __future__ import annotations + +import contextlib +import datetime as dt +import io +import json +import sys +import time +import urllib.request +from pathlib import Path + +import numpy as np +import pandas as pd + +# --------------------------------------------------------------------------- +# Il MIO flag di rete si legge PRIMA di importare qualunque libreria del progetto: +# `r0822d_funding` decide all'import se andare in rete, e qui deve usare la sua cache. +# --------------------------------------------------------------------------- +_NET = "--no-net" not in sys.argv + + +def _argf(name: str, default: float) -> float: + if name in sys.argv: + return float(sys.argv[sys.argv.index(name) + 1]) + return default + + +ROUNDS = int(_argf("--rounds", 8)) +GAP_S = _argf("--gap", 35.0) +FETCH_ONLY = "--fetch-only" in sys.argv + +if "--no-net" not in sys.argv: + sys.argv.append("--no-net") + +ROOT = Path("/opt/docker/PythagorasGoal") +for _p in (ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt", ROOT): + sys.path.insert(0, str(_p)) + +import r0702_anchor_skh01 as r02 # noqa: E402 run_asset (ltf/ent), sim_equity +import r0725_capcurve as CC # noqa: E402 DERIBIT_FEE_SIDE/MIN_ORDER, _boot_paths +import r0726_wall_fixedpoint as WF # noqa: E402 la STRUTTURA del punto fisso +import r0727_tasse as TX # noqa: E402 PATRIMONIALE, CARRY_ANNI +import r0807_piano_netto as PN # noqa: E402 muro, accumula, passo_fiscale +import r0822d_funding as FD # noqa: E402 funding, tp01_realistic, skh_daily +import r0822d_piano_vero as PV # noqa: E402 Lente, deluck, muro_di, leggi +from src.data.downloader import load_data # noqa: E402 +from src.portfolio.portfolio import combine_outer, metrics # noqa: E402 +from src.strategies.trend_portfolio import (CANONICAL, TrendPortfolio, # noqa: E402 + resample_1d, simple_returns) + +# --------------------------------------------------------------------------- +# Costanti dichiarate +# --------------------------------------------------------------------------- +ASSETS = ("BTC", "ETH") +INSTR = {"BTC": "BTC_USDC-PERPETUAL", "ETH": "ETH_USDC-PERPETUAL"} # cio' che il libro TRADA +W_TP, W_SKH = 0.75, 0.25 +LO = pd.Timestamp("2019-03-14", tz="UTC") # inizio del libro (convenzione FD.book_75_25) +DPY = 365.0 +API = "https://www.deribit.com/api/v2/public/" +DEPTH = 1000 # livelli richiesti per lato +VINTAGE_PUB = "2026-08-07" + +# griglia di taglie: log-spaziata da $5 (il min_order) a $10M +SIZES = np.unique(np.round(10.0 ** np.arange(np.log10(5.0), 7.0001, 0.125), 2)) +# griglia di capitali su cui si misura il libro (ordini di grandezza, non un continuo) +CAPS = (635.0, 2_000.0, 5_000.0, 20_000.0, 50_000.0, 100_000.0, 313_000.0, + 1_000_000.0, 3_000_000.0, 10_000_000.0, 30_000_000.0, 100_000_000.0) + +_SP = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad") +CACHE = (_SP if _SP.parent.exists() else Path("/tmp/pythagoras_costo")) / "costocap" +CACHE.mkdir(parents=True, exist_ok=True) +BOOKS = CACHE / "books.jsonl" +FILLS = _SP / "r0822_slip_fills.csv" # i 18 fill reali di §5 +LIN5M = {"BTC": _SP / "lin_btc_5m.parquet", "ETH": _SP / "lin_eth_5m.parquet"} + +T0 = time.time() +_last = [0.0] + + +def sez(t: str) -> None: + print("\n" + "=" * 98 + f"\n {t}\n" + "=" * 98) + + +def clock() -> str: + return f"[{time.time() - T0:5.0f}s]" + + +# =========================================================================== +# 0) RETE — GET pubbliche, pacing, astensione nelle finestre del cron +# =========================================================================== +def _guard_window() -> None: + while True: + m = dt.datetime.now(dt.timezone.utc).minute + if not (5 <= m <= 10 or 24 <= m <= 30): + return + tgt = 11 if m <= 10 else 31 + w = (tgt - m) * 60 - dt.datetime.now(dt.timezone.utc).second + print(f" [rete] minuto :{m:02d} = finestra del cron di produzione -> attendo {max(w,1)}s") + time.sleep(max(w, 1)) + + +def _get(path: str, tries: int = 3): + if not _NET: + return None + _guard_window() + for k in range(tries): + d = time.time() - _last[0] + if d < 0.55: # <=2 req/s + time.sleep(0.55 - d) + _last[0] = time.time() + try: + with urllib.request.urlopen(API + path, timeout=30) as r: + return json.load(r) + except Exception as ex: # noqa: BLE001 + if k == tries - 1: + print(f" [rete] KO {path[:64]}: {ex}") + return None + time.sleep(1.5 * (k + 1)) + return None + + +def raccogli(rounds: int, gap: float) -> int: + """Istantanee del libro, appese alla cache. Ritorna quante ne ha aggiunte.""" + if not _NET or rounds <= 0: + return 0 + n = 0 + with BOOKS.open("a") as fh: + for r in range(rounds): + for a in ASSETS: + j = _get(f"get_order_book?instrument_name={INSTR[a]}&depth={DEPTH}") + if not j or "result" not in j: + continue + q = j["result"] + if not q.get("bids") or not q.get("asks"): + continue + fh.write(json.dumps(dict( + ts=int(q.get("timestamp", time.time() * 1000)), asset=a, + bid=float(q["best_bid_price"]), ask=float(q["best_ask_price"]), + bids=[[float(p), float(s)] for p, s in q["bids"]], + asks=[[float(p), float(s)] for p, s in q["asks"]])) + "\n") + n += 1 + if r < rounds - 1: + time.sleep(gap) + return n + + +def leggi_libri() -> list: + if not BOOKS.exists(): + return [] + out = [] + for ln in BOOKS.read_text().splitlines(): + if ln.strip(): + with contextlib.suppress(Exception): + out.append(json.loads(ln)) + return out + + +# =========================================================================== +# 1) LA CURVA COSTO-TAGLIA +# =========================================================================== +def walk_cost_bps(book: dict, usd: float, side: str) -> float | None: + """Costo effettivo vs mid di un ordine da `usd` eseguito ISTANTANEAMENTE contro il libro + visibile (stessa convenzione di `r0823_skeptic_spot.walk_cost_bps` e + `r0822e_funding_avoid.walk_cost_bps`). None se il libro visibile NON basta: allora si + dichiara, non si estrapola.""" + if not book or not book.get("bid") or not book.get("ask"): + return None + mid = (book["bid"] + book["ask"]) / 2.0 + lv = book.get("asks" if side == "buy" else "bids") or [] + need, got, cost = usd / mid, 0.0, 0.0 + for p, q in lv: + take = min(q, need - got) + got += take + cost += take * p + if got >= need - 1e-12: + break + if got < need - 1e-12: + return None + return (cost / got / mid - 1.0) * 1e4 * (1.0 if side == "buy" else -1.0) + + +class Curva: + """`costo_bps(asset, usd, q)` per interpolazione log-log sulle quantili misurate. + + * sotto la taglia minima misurata: il costo e' il mezzo spread (costante) — e' cio' che il + libro DICE, non un'assunzione; + * dentro la regione misurata: interpolazione lineare in log(taglia), con monotonia imposta + (`cummax`): camminare un libro non puo' costare MENO su un ordine piu' grande, e una + violazione sarebbe rumore fra istantanee; + * oltre l'ultima taglia con copertura sufficiente (`lim`): il libro visibile NON BASTA, cioe' + l'ordine non e' eseguibile in un istante. Li' si estrapola con pendenza log-log + `max(misurata, 1.0)` — [ASSUNTO] dichiarato: impatto ALMENO lineare nella taglia, perche' + una pendenza < 1 (raddoppiare l'ordine costa meno del doppio) oltre la fine del libro non + e' difendibile. Ogni cella oltre `lim` e' marcata [DEDOTTO]. + + Le tre quantili condividono maschera, `lim` e pendenza -> la banda resta ORDINATA anche + nella regione estrapolata (con tre oggetti separati p10 poteva finire sopra p50: errore + catturato alla prima corsa). + """ + + QS = (0.10, 0.50, 0.90) + + def __init__(self, libri: list, cop_min: float = 0.8): + self.cop_min = cop_min + self.x, self.y, self.lim, self.slope, self.nobs = {}, {}, {}, {}, {} + self.slope_mis = {} + for a in ASSETS: + L = [b for b in libri if b["asset"] == a] + self.nobs[a] = len(L) + M, cop = {q: [] for q in self.QS}, [] + for sz in SIZES: + v = [] + for b in L: + for side in ("buy", "sell"): + c = walk_cost_bps(b, float(sz), side) + v.append(np.nan if c is None else c) + v = np.asarray(v, float) + cop.append(float(np.isfinite(v).mean()) if len(v) else 0.0) + for q in self.QS: + M[q].append(float(np.nanquantile(v, q)) if np.isfinite(v).any() else np.nan) + cop = np.asarray(cop) + ok = np.isfinite(np.asarray(M[0.5])) & (cop >= cop_min) + if not ok.any(): + raise SystemExit(f"nessuna taglia misurabile su {a}: campione insufficiente") + xs = np.log10(SIZES[ok]) + ys = {q: np.maximum.accumulate(np.asarray(M[q])[ok]) for q in self.QS} + ys[0.50] = np.maximum(ys[0.50], ys[0.10]) # banda ordinata per costruzione + ys[0.90] = np.maximum(ys[0.90], ys[0.50]) + self.x[a], self.y[a] = xs, ys + self.lim[a] = float(SIZES[ok][-1]) + m = xs >= xs[-1] - 1.0 + sl = (float(np.polyfit(xs[m], np.log10(np.maximum(ys[0.50][m], 1e-4)), 1)[0]) + if m.sum() >= 2 else 1.0) + self.slope[a] = max(sl, 1.0) # [ASSUNTO] impatto almeno lineare + self.slope_mis[a] = sl + + def oltre(self, asset: str, usd: float) -> bool: + return usd > self.lim[asset] + + def bps(self, asset: str, usd: float, q: float = 0.50) -> float: + if usd <= 0: + return 0.0 + xs, ys = self.x[asset], self.y[asset][q] + lx = np.log10(usd) + if lx <= xs[0]: + return float(ys[0]) + if lx <= xs[-1]: + return float(np.interp(lx, xs, ys)) + return float(10.0 ** (np.log10(max(ys[-1], 1e-4)) + self.slope[asset] * (lx - xs[-1]))) + + def vec(self, asset: str, usd: np.ndarray, q: float = 0.50) -> np.ndarray: + return np.array([self.bps(asset, float(u), q) for u in usd], float) + + def profondita(self, libri: list, asset: str, soglia_bps: float) -> float: + """[VENUE] Dollari eseguibili entro `soglia_bps` di costo (mediana sulle istantanee, + lato peggiore).""" + v = [] + for b in [x for x in libri if x["asset"] == asset]: + lo, hi = 0.0, 5e7 + for _ in range(30): + mid = (lo + hi) / 2 + c = walk_cost_bps(b, mid, "buy") + d = walk_cost_bps(b, mid, "sell") + w = max([z for z in (c, d) if z is not None], default=1e9) + if w <= soglia_bps: + lo = mid + else: + hi = mid + v.append(lo) + return float(np.median(v)) if v else float("nan") + + +class CurvaZero(Curva): + """Costo identicamente nullo: il controllo negativo (replica bit-exact dei numeri vecchi).""" + + def __init__(self): # noqa: D107 + self.nobs, self.lim, self.slope = {}, {a: float("inf") for a in ASSETS}, {} + + def oltre(self, asset: str, usd: float) -> bool: # noqa: D102 + return False + + def bps(self, asset: str, usd: float, q: float = 0.50) -> float: # noqa: D102 + return 0.0 + + def vec(self, asset: str, usd: np.ndarray, q: float = 0.50) -> np.ndarray: # noqa: D102 + return np.zeros(len(usd)) + + +class CurvaFissa(CurvaZero): + """Costo costante in bps: il controllo POSITIVO. Un modello di costo che non morde mai e' + indistinguibile da uno rotto, quindi va provato che morde.""" + + def __init__(self, bps: float): # noqa: D107 + super().__init__() + self._b = float(bps) + + def bps(self, asset: str, usd: float, q: float = 0.50) -> float: # noqa: D102 + return self._b if usd > 0 else 0.0 + + def vec(self, asset: str, usd: np.ndarray, q: float = 0.50) -> np.ndarray: # noqa: D102 + return np.where(np.asarray(usd) > 0, self._b, 0.0) + + +# =========================================================================== +# 2) IL LIBRO CON IL COSTO DENTRO +# =========================================================================== +def tp01_costo(alloc: float, rate: dict | None, cur: Curva, scala: float = 1.0, + q: float = 0.50) -> tuple[pd.Series, dict]: + """`FD.tp01_realistic` con DUE gradi di liberta' in piu': il costo d'esecuzione funzione + della taglia dell'ordine, e la scala di leva (`SPEC-scale-key`: la scala moltiplica il + TARGET, non il capitale). Con cur=CurvaZero() e scala=1 deve dare bit-exact l'originale.""" + cap_asset = alloc / len(ASSETS) + tp = TrendPortfolio(**CANONICAL) + series, diag = {}, {} + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + r = simple_returns(df["close"].values.astype(float)) + tgt = np.nan_to_num(np.asarray(tp.target_series(df), float)) * scala + held = np.empty(len(tgt)) + cur_pos = 0.0 + for i in range(len(tgt)): + if abs(tgt[i] - cur_pos) * cap_asset >= CC.DERIBIT_MIN_ORDER: + cur_pos = tgt[i] + held[i] = cur_pos + pos = np.zeros(len(held)) + pos[1:] = held[:-1] + dpos = np.abs(np.diff(pos, prepend=0.0)) + net = pos * r - CC.DERIBIT_FEE_SIDE * dpos + ordini = dpos * cap_asset # nozionale in dollari di OGNI ordine + cbps = cur.vec(a, ordini, q) + net = net - dpos * cbps / 1e4 # il costo pesa il nozionale scambiato + if rate is not None: + net = net - pos * FD.daily_funding(rate[a], idx) + net[0] = 0.0 + diag[a] = dict(pos=pos, ordini=ordini[ordini > 0], cbps=cbps[ordini > 0], idx=idx) + series[a] = pd.Series(np.clip(net, -0.99, None), index=idx) + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + out = pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index) + if out.index.tz is None: + out.index = out.index.tz_localize("UTC") + return out, diag + + +_LTF: dict = {} + + +def _ltf(a: str): + if a not in _LTF: + _, _m0, ltf, _ent = r02.run_asset(a, 0) + _LTF[a] = ltf + return _LTF[a] + + +def _rebuild_equity(n: int, trades: list, extra: float, scala: float) -> np.ndarray: + """Ricostruisce la curva di equity di SKH01 dai TRADE gia' prodotti da `FD.sim_equity_f`, + con un costo `extra` per round-trip e una scala di leva. Non e' una re-implementazione della + strategia: i trade (boundary, direzione, gross, funding) sono presi COSI' COME SONO, cambia + solo la contabilita' del singolo trade. Con extra=0 e scala=1 e' bit-exact l'originale.""" + initial = 1000.0 + capital = initial + equity = np.full(n, capital, dtype=float) + for t in trades: + net = scala * (t["gross"] - 0.001 - t["f"]) - extra + capital += capital * net + capital = max(capital, 1.0) + equity[t["i"]:t["exit_idx"] + 1] = capital + last = initial + for k in range(n): + if equity[k] != last and equity[k] != initial: + last = equity[k] + else: + equity[k] = last + return equity + + +def skh_costo(alloc: float, rate: dict | None, cur: Curva, scala: float = 1.0, + q: float = 0.50) -> pd.Series: + """SKH01 50/50 con il costo d'esecuzione dentro. + + SKH01 entra a nozionale 1,0x del proprio capitale (`position_size=1.0, leverage=1.0`) e fa + UN round-trip per trade -> due ordini, entrambi a nozionale pieno. Il costo per trade e' + percio' `2 x costo_bps(nozionale)/1e4`, e il nozionale e' `scala x alloc/2`.""" + cap_asset = alloc / len(ASSETS) + cols = {} + for a in ASSETS: + _d, tr = FD.skh_daily(a, 0, "hourly", rate) + ltf = _ltf(a) + extra = 2.0 * scala * cur.bps(a, scala * cap_asset, q) / 1e4 + eq = _rebuild_equity(len(ltf), tr, extra, scala) + s = pd.Series(eq, index=pd.DatetimeIndex(pd.to_datetime(ltf["datetime"], utc=True))) + cols[a] = s.resample("1D").last().ffill().pct_change().dropna() + J = pd.concat(cols, axis=1, join="inner").fillna(0.0) + out = pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index) + if out.index.tz is None: + out.index = out.index.tz_localize("UTC") + return out + + +def libro(cap: float, rate: dict | None, cur: Curva, scala: float = 1.0, + q: float = 0.50) -> pd.Series: + """Il libro LIVE (TP01 75 + SKH01 25) al capitale `cap`, col costo d'esecuzione dentro.""" + tp, _ = tp01_costo(cap * W_TP, rate, cur, scala, q) + sk = skh_costo(cap * W_SKH, rate, cur, scala, q) + return combine_outer({"TP01": tp, "SKH01": sk}, {"TP01": W_TP, "SKH01": W_SKH}, lo=LO) + + +# =========================================================================== +# 3) ACCUMULO CON COSTO LUNGO IL CAMMINO +# =========================================================================== +def accumula_costo(paths: np.ndarray, dep_eur: float, bersaglio: float, aliquota: float, + patrimoniale: float, start: float, drag, lump_eur: float = 0.0) -> dict: + """Gemella di `PN.accumula` con UN grado di liberta' in piu': un drag GIORNALIERO funzione + del capitale corrente. Con drag=None dev'essere bit-exact `PN.accumula` (replica R7). + + `drag(cap)` = (drift del libro alla taglia di riferimento) - (drift alla taglia `cap`), + diviso 365. E' un aggiustamento del solo DRIFT: la FORMA del percorso resta quella estratta. + [ASSUNTO] dichiarato — la vol cambia pochissimo con la taglia (sezione 4 lo riporta).""" + n_paths, n_days = paths.shape + dep = dep_eur * CC.EURUSD + cap = np.full(n_paths, start + lump_eur * CC.EURUSD) + versato = start + lump_eur * CC.EURUSD + carry = np.zeros((n_paths, TX.CARRY_ANNI)) + anno_start = cap.copy() + flusso = np.zeros(n_paths) + colpito = np.full(n_paths, -1, int) + for t in range(n_days): + if drag is None: + cap *= (1.0 + paths[:, t]) + else: + cap *= (1.0 + paths[:, t] - drag(cap)) + if t % 30 == 0 and t > 0 and dep > 0: + cap += dep + flusso += dep + versato += dep + nuovi = (colpito < 0) & (cap >= bersaglio) + colpito[nuovi] = t + if (t + 1) % 365 == 0 and (aliquota > 0 or patrimoniale > 0): + PN.passo_fiscale(cap, anno_start, flusso, carry, aliquota, patrimoniale) + anno_start = cap.copy() + flusso[:] = 0.0 + return dict(cap=cap, colpito=colpito, versato=versato) + + + + +# =========================================================================== +# 4) DIAGNOSTICHE DI CAPACITA' +# =========================================================================== +def quota_oltre(cap: float, cur: Curva, rate: dict | None) -> tuple[float, float]: + """(quota del nozionale annuo scambiato la cui TAGLIA supera il libro visibile, + nozionale annuo totale in $). E' una misura di CAPACITA' puramente [VENUE]+[BACKTEST]: + non dipende da nessuna estrapolazione.""" + Z = CurvaZero() + _s, dg = tp01_costo(cap * W_TP, None, Z) + anni = 0.0 + tot = fuori = 0.0 + for a in ASSETS: + o = dg[a]["ordini"] + idx = dg[a]["idx"] + anni = max(anni, (idx[-1] - idx[0]).days / DPY) + tot += float(o.sum()) + fuori += float(o[o > cur.lim[a]].sum()) + tr = FD.skh_daily(a, 0, "hourly", None)[1] + noz = cap * W_SKH / 2 + tot += 2 * len(tr) * noz + if noz > cur.lim[a]: + fuori += 2 * len(tr) * noz + return (fuori / tot if tot > 0 else 0.0), tot / max(anni, 1e-9) + + +# =========================================================================== +def main() -> None: # noqa: C901 + print("=" * 98) + print(" r0823 §55 COSTO-CAPITALE — il muro quando il costo d'esecuzione e' una FUNZIONE") + print("=" * 98) + print(" Provenienza: [VENUE] API pubblica · [LOG] fill reali · [BACKTEST] serie certificata") + print(" [CALC] aritmetica · [DEDOTTO] estrapolazione · [ASSUNTO] modello") + + # ---------------------------------------------------------------- rete + nuovi = raccogli(ROUNDS, GAP_S) + if FETCH_ONLY: + print(f" --fetch-only: {nuovi} istantanee aggiunte. Fine.") + return + libri = leggi_libri() + if not libri: + raise SystemExit("nessuna istantanea del libro: rilanciare con rete.") + ts = pd.to_datetime([b["ts"] for b in libri], unit="ms", utc=True) + print(f"\n [VENUE] {len(libri)} istantanee ({nuovi} nuove in questa corsa), " + f"{sum(1 for b in libri if b['asset']=='BTC')} BTC / " + f"{sum(1 for b in libri if b['asset']=='ETH')} ETH") + print(f" dal {ts.min():%Y-%m-%d %H:%M} al {ts.max():%Y-%m-%d %H:%M} UTC " + f"(arco {(ts.max()-ts.min()).total_seconds()/60:.0f} min) {clock()}") + print(" ⚠️ E' un solo regime di mercato in un arco di minuti: e' una banda INTRA-GIORNO,") + print(" non la distribuzione della profondita' su 7 anni. Vedi l'assunzione in testa.") + gg = ["lun", "mar", "mer", "gio", "ven", "sab", "dom"][int(ts.max().dayofweek)] + print(f" 📌 Ma il MOMENTO e' favorevole al risultato conservativo, non a quello comodo: " + f"{gg} {ts.max():%H:%M} UTC") + print(" = fine settimana, notte europea, mattina asiatica — la finestra in cui il libro") + print(" e' PIU' SOTTILE della settimana. Un campione preso in orario di punta darebbe") + print(" costi piu' bassi, cioe' rafforzerebbe la conclusione invece di indebolirla.") + + # ===================================================================== + sez("1) LA CURVA COSTO-TAGLIA, MISURATA — e la sua banda") + print(""" Camminata del libro visibile: costo effettivo vs mid di un ordine ESEGUITO + ISTANTANEAMENTE. E' la lente giusta per QUESTO libro perche' l'esecutore manda ordini + MARKET (`book_execute`), non limit: cio' che il libro mostra e' cio' che l'ordine paga. + Ogni cella e' la mediana su (istantanea x lato); la banda e' p10-p90 sulla stessa + popolazione. §45 ha misurato due istantanee a 4 minuti che differivano del 79%: un solo + numero qui sarebbe una finzione.""") + cur = Curva(libri) + print(f"\n {'ordine':>12} | {'BTC_USDC-PERP mediana [p10-p90]':>40} | " + f"{'ETH_USDC-PERP mediana [p10-p90]':>40}") + print(" " + "-" * 98) + MOSTRA = (5, 50, 500, 5_000, 50_000, 200_000, 500_000, 1_000_000, 3_000_000) + for sz in MOSTRA: + cells = [] + for a in ASSETS: + if cur.oltre(a, sz): + cells.append(f"{cur.bps(a, sz):10.2f} [DEDOTTO, oltre il libro]") + else: + cells.append(f"{cur.bps(a, sz):8.3f} [{cur.bps(a, sz, .10):7.3f}," + f"{cur.bps(a, sz, .90):8.3f}] bps") + print(f" ${sz:>11,} | {cells[0]:>40} | {cells[1]:>40}") + print(" " + "-" * 98) + print(f" ultima taglia con copertura >=80% del campione (`lim`): " + f"BTC ${cur.lim['BTC']:,.0f} · ETH ${cur.lim['ETH']:,.0f}") + print(f" pendenza log-log oltre `lim`: misurata BTC {cur.slope_mis['BTC']:.2f} / " + f"ETH {cur.slope_mis['ETH']:.2f} -> usata {cur.slope['BTC']:.2f} / " + f"{cur.slope['ETH']:.2f} (floor 1.0 = impatto almeno lineare) [ASSUNTO]") + print(""" + ⚠️ ERRORE MIO, catturato alla prima corsa. La prima stesura costruiva TRE oggetti-curva + separati (p10, p50, p90), ognuno con la propria maschera di copertura e la propria + pendenza di estrapolazione: oltre il libro visibile la p10 finiva SOPRA la p50 (ETH a $3M: + 18,7 contro 15,5 bps). Una banda che si incrocia non e' una banda. Ora le tre quantili + condividono maschera, `lim` e pendenza, e l'ordinamento e' imposto per costruzione.""") + hs = {a: cur.bps(a, 5.0) for a in ASSETS} + print(f"\n [VENUE] Il fondo della curva E' il mezzo spread: BTC {hs['BTC']:.4f} bps, " + f"ETH {hs['ETH']:.4f} bps — cioe' UN TICK.") + print(" ⚠️ Questo NON e' il libro SPOT di §52 (mezzo spread misurato 1,5-3,1 bps, e 5,3 in") + print(" sessione): 100-500x piu' largo. La conclusione di §52 non e' contraddetta, e'") + print(" CONFERMATA dall'altro lato — il costo che si annullava li' era dello SPOT.") + + print("\n [VENUE] PROFONDITA' entro una soglia di costo (mediana sulle istantanee, " + "lato peggiore):") + print(f" {'soglia':>10}{'BTC $ eseguibili':>22}{'ETH $ eseguibili':>22}") + PROF = {} + for soglia in (0.5, 1.0, 5.0, 20.0, 100.0): + row = [cur.profondita(libri, a, soglia) for a in ASSETS] + PROF[soglia] = dict(zip(ASSETS, row)) + print(f" {soglia:>8.1f}bps{row[0]:>22,.0f}{row[1]:>22,.0f}") + + # ===================================================================== + sez("2) LA DISTRIBUZIONE VERA DELLE TAGLIE D'ORDINE — non l'ordine medio") + print(""" Il costo e' CONVESSO: la media dei costi non e' il costo della media. Percio' il + costo si applica ordine per ordine, sul nozionale che il libro manda DAVVERO. + TP01: |Dpos| x (cap x 0,75/2), col pavimento min_order $5 gia' dentro (CC.tp01_realistic). + SKH01: due ordini a nozionale PIENO (cap x 0,25/2) per trade, 1,0x per costruzione.""") + with contextlib.redirect_stdout(io.StringIO()): + F = FD.sezione0()["F"] + rate = FD.build_rate(F) + Z = CurvaZero() + print(f"\n {'capitale':>13}{'TP01 n.ord':>12}{'mediana':>11}{'p90':>12}{'max':>13}" + f"{'quota noz.':>12}{'SKH01 ordine':>14}{'x n.':>8}{'quota noz.':>12}") + ordini_cache = {} + for C in (635.0, 5_000.0, 50_000.0, 313_000.0, 3_000_000.0): + _s, dg = tp01_costo(C * W_TP, None, Z) + o = np.concatenate([dg[a]["ordini"] for a in ASSETS]) + ordini_cache[C] = o + ns = sum(len(FD.skh_daily(a, 0, "hourly", None)[1]) for a in ASSETS) + nsk = 2 * ns * C * W_SKH / 2 + print(f" ${C:>12,.0f}{len(o):>12,}{np.median(o):>11,.0f}" + f"{np.percentile(o, 90):>12,.0f}{o.max():>13,.0f}" + f"{o.sum()/(o.sum()+nsk):>11.0%}{C * W_SKH / 2:>14,.0f}{2 * ns:>8,}" + f"{nsk/(o.sum()+nsk):>11.0%}") + print("\n 📌 Le due gambe hanno distribuzioni OPPOSTE: TP01 manda MOLTI ordini piccoli") + print(" (deriva del vol-target), SKH01 POCHI ordini a nozionale pieno — ma sul") + print(" NOZIONALE SCAMBIATO SKH01 pesa il doppio di TP01. A pari capitale il suo") + print(" ordine e' ~100x la mediana di TP01: e' SKH01 a incontrare per primo il muro di") + print(" liquidita', e non e' cio' che si indovinerebbe contando gli ordini (§5: contare") + print(" gli ordini da' 16%, contare il nozionale da' 75% — stessa trappola).") + + # ===================================================================== + sez("3) REPLICA — prima di ogni delta, riprodurre i numeri pubblicati") + ok = {} + tpZ, _ = tp01_costo(635.0 * W_TP, rate, Z) + ref_tp, _ = FD.tp01_realistic(635.0 * W_TP, rate) + d = float(np.max(np.abs(tpZ.values - ref_tp.reindex(tpZ.index).values))) + ok["R1"] = d == 0.0 + print(f" R1 TP01 con costo spento vs `FD.tp01_realistic` max|dif| = {d:.1e} " + f"{'BIT-EXACT' if ok['R1'] else 'FALLITA'}") + + skZ = skh_costo(635.0 * W_SKH, rate, Z) + ref_sk = FD.skh_book(0, "hourly", rate) + d = float(np.max(np.abs(skZ.values - ref_sk.reindex(skZ.index).values))) + ok["R2"] = d == 0.0 + print(f" R2 SKH01 con costo spento vs `FD.skh_book` max|dif| = {d:.1e} " + f"{'BIT-EXACT' if ok['R2'] else 'FALLITA'}") + + bZ = libro(635.0, rate, Z) + ref_b = FD.book_75_25(rate, alloc=635.0) + d = float(np.max(np.abs(bZ.values - ref_b.reindex(bZ.index).values))) + ok["R3"] = d == 0.0 + print(f" R3 libro con costo spento vs `FD.book_75_25` max|dif| = {d:.1e} " + f"{'BIT-EXACT' if ok['R3'] else 'FALLITA'}") + + B = PV.costruisci_serie() + idx = pd.DatetimeIndex(B.index) + A_raw, F_raw = B["nofund"].values.astype(float), B["fund"].values.astype(float) + mv = np.asarray(idx <= pd.Timestamp(VINTAGE_PUB, tz="UTC")) + Lv0 = PV.Lente("vintage LORDO", PV.deluck(A_raw[mv]), True) + Lv1 = PV.Lente("vintage +FISCO", PV.deluck(A_raw[mv]), False) + Lv0.perp, Lv0.muro = PN.muro(Lv0.r, Lv0.prelievo, 0.0, 0.0) + Lv1.perp, Lv1.muro = PN.muro(Lv1.r, Lv1.prelievo, Lv1.aliq, Lv1.patr) + ok["R4"] = abs(Lv0.muro - 272_061.0) < 1.0 + ok["R5"] = abs(Lv1.muro - 258_338.0) < 1.0 + print(f" R4 muro L0 LORDO vintage {VINTAGE_PUB} ${Lv0.muro:>11,.0f} atteso $272,061 " + f"{'ESATTO AL DOLLARO' if ok['R4'] else 'DIVERSO'}") + print(f" R5 muro L1 +FISCO vintage {VINTAGE_PUB} ${Lv1.muro:>11,.0f} atteso $258,338 " + f"{'ESATTO AL DOLLARO' if ok['R5'] else 'DIVERSO'}") + + L3 = PV.Lente("L3 CONGIUNTA", PV.deluck(F_raw), False) + L3.perp, L3.muro = PV.muro_di(L3) + ok["R6"] = abs(L3.muro - 313_000.0) < 4_000.0 + print(f" R6 muro L3 CONGIUNTA (oggi) ${L3.muro:>11,.0f} atteso ~$313k " + f"{'RIPRODOTTO' if ok['R6'] else 'DIVERSO'} {clock()}") + + rng = np.random.default_rng(PV.SEED_TRAJ) + pv = CC._boot_paths(L3.r, 400, 5 * 365, PV.BLOCK, rng) + a1 = PN.accumula(pv, 500.0, L3.muro, L3.aliq, L3.patr, start=635.0) + a2 = accumula_costo(pv, 500.0, L3.muro, L3.aliq, L3.patr, 635.0, None) + d = float(np.max(np.abs(a1["cap"] - a2["cap"]))) + ok["R7"] = d == 0.0 + print(f" R7 `accumula_costo` con drag spento vs `PN.accumula` max|dif| = {d:.1e} " + f"{'BIT-EXACT' if ok['R7'] else 'FALLITA'}") + + import r0823_skeptic_spot as SS # noqa: E402 + synth = dict(bid=100.0, ask=100.1, bids=[[100.0, 3.0], [99.0, 50.0]], + asks=[[100.1, 3.0], [101.0, 50.0]]) + same = all( + (walk_cost_bps(synth, u, sd) is None) == (SS.walk_cost_bps(synth, u, sd) is None) + and (walk_cost_bps(synth, u, sd) is None + or abs(walk_cost_bps(synth, u, sd) - SS.walk_cost_bps(synth, u, sd)) < 1e-12) + for u in (10.0, 300.0, 2_000.0, 1e9) for sd in ("buy", "sell")) + ok["R8"] = same + print(f" R8 la mia `walk_cost_bps` == quella di §52 (libro sintetico) " + f"{'IDENTICHE' if same else 'DIVERGONO'}") + print(f"\n ESITO: {sum(ok.values())}/{len(ok)} — " + + ", ".join(f"{k}={'OK' if v else 'NO'}" for k, v in ok.items())) + if not all(ok.values()): + raise SystemExit("REPLICA INCOMPLETA: un numero nuovo sarebbe indistinguibile da un bug.") + + big = libro(635.0, rate, CurvaFissa(200.0)) + m0, mB = metrics(bZ), metrics(big) + morde = mB["sharpe"] < m0["sharpe"] - 0.5 + print("\n CONTROLLO POSITIVO del modello di costo — a 200 bps per ordine il libro DEVE") + print(f" morire: Sharpe {m0['sharpe']:.3f} -> {mB['sharpe']:.3f}, " + f"CAGR {m0['cagr']:+.2%} -> {mB['cagr']:+.2%}. " + f"{'MORDE' if morde else '⚠️ NON MORDE: modello sospetto'}") + if not morde: + raise SystemExit("il modello di costo non morde: indistinguibile da uno rotto.") + + # ===================================================================== + sez("4) IL COSTO A $635 — deve riprodurre §5, o la calibrazione e' sbagliata") + o635 = ordini_cache[635.0] + c635 = np.array([cur.bps("BTC", x) for x in o635]) + sk635 = 635.0 * W_SKH / 2 + print(f" [CALC] TP01 a $635: ordine mediano ${np.median(o635):,.0f} -> " + f"{np.median(c635):.4f} bps; ordine massimo ${o635.max():,.0f} -> " + f"{cur.bps('BTC', o635.max()):.4f} bps.") + print(f" [CALC] SKH01 a $635: ordine ${sk635:,.0f} -> {cur.bps('BTC', sk635):.4f} bps.") + b635 = libro(635.0, rate, cur) + print(f" [CALC] drift del libro: {bZ.mean()*DPY:+.4%} senza costo -> " + f"{b635.mean()*DPY:+.4%} col costo = {(b635.mean()-bZ.mean())*DPY*1e4:+.2f} bps/anno.") + if FILLS.exists(): + f = pd.read_csv(FILLS) + sl = f["slip_B5"].astype(float).abs() + print(f" [LOG] i 18 fill reali di §5: |slippage| mediana {sl.median():.2f} bps, " + f"p90 {sl.quantile(0.9):.2f} bps, nozionale mediano ${f['notional'].median():,.0f}.") + print(f" 📌 Il costo MODELLATO ({np.median(c635):.4f} bps) sta " + f"{sl.median()/max(np.median(c635),1e-9):,.0f}x SOTTO la dispersione misurata sui") + print(" fill veri: a questa taglia l'impatto non e' misurabile nemmeno in linea di") + print(" principio, ed e' esattamente cio' che §5 concluse («estremi avversi 1/18") + print(" contro 4,7 attesi»). La calibrazione riproduce il fatto, non lo assume.") + + if all(p.exists() for p in LIN5M.values()): + print("\n ⚠️ §53 SU QUESTO PUNTO: «un fill al 21,9% del volume della sua barra 5m a $635,") + print(" con partecipazione lineare 1,25x lo porta al 27,4% SUBITO».") + print(" Misurato sul feed LINEARE USDC — quello che il libro trada davvero:") + print(f" {'asset':>6}{'vol 5m med. (>0)':>20}{'libro a 1bps':>15}{'libro a 20bps':>15}" + f"{'ordine $635':>13}{'quota del libro 1bps':>22}") + rap, rap20 = [], [] + for a in ASSETS: + d5 = pd.read_parquet(LIN5M[a]) + v = (d5["volume"] * d5["close"]).values + vm = float(np.median(v[v > 0])) + rap.append(PROF[1.0][a] / vm) + rap20.append(PROF[20.0][a] / vm) + print(f" {a:>6}{vm:>20,.0f}{PROF[1.0][a]:>15,.0f}{PROF[20.0][a]:>15,.0f}" + f"{sk635:>13,.0f}{sk635/PROF[1.0][a]:>21.2%}") + print(" 📌 La partecipazione a una barra 5m NON e' un costo: e' la quota del VOLUME") + print(" SCAMBIATO in cinque minuti. Il costo lo decide la liquidita' RESTANTE nel") + print(f" libro: entro 1 bps vale {rap[0]:.1f}x (BTC) / {rap[1]:.1f}x (ETH) quel") + print(f" volume, entro 20 bps {rap20[0]:.0f}x / {rap20[1]:.0f}x.") + print(f" L'ordine di OGGI e' lo {sk635/PROF[1.0]['BTC']:.2%} (BTC) / " + f"{sk635/PROF[1.0]['ETH']:.2%} (ETH) della profondita' a 1 bps;") + print(f" a 1,25x diventa lo {1.25*sk635/PROF[1.0]['BTC']:.2%} / " + f"{1.25*sk635/PROF[1.0]['ETH']:.2%}. L'allarme di §53 e' una DOMANDA giusta") + print(" con una grandezza sbagliata: il 21,9% era la quota di una barra a volume") + print(" BASSO, e il volume di una barra non e' la liquidita' disponibile.") + + # ===================================================================== + sez("5) IL LIBRO ALLA TAGLIA — drift, vol, Sharpe e capacita' come funzione del capitale") + print(" [ASSUNTO] Il libro d'ordini di OGGI proiettato su 2019-2026. Nel 2019 la") + print(" profondita' di Deribit era una frazione di quella odierna -> ottimista sul passato,") + print(" onesto sul futuro (ed e' il futuro che il muro deve attraversare).") + print(f"\n {'capitale':>15}{'drift':>10}{'vol':>9}{'Sharpe':>9}{'costo/anno':>12}" + f"{'di cui TP01':>13}{'di cui SKH01':>14}{'noz. fuori libro':>18}") + D, VOLS, SH = {}, {}, {} + for C in CAPS: + s = libro(C, rate, cur) + stp, _ = tp01_costo(C * W_TP, rate, cur) + ssk = skh_costo(C * W_SKH, rate, cur) + stp0, _ = tp01_costo(C * W_TP, rate, Z) + ssk0 = skh_costo(C * W_SKH, rate, Z) + b0 = libro(C, rate, Z) + D[C] = float(s.mean() * DPY) + VOLS[C] = float(s.std() * np.sqrt(DPY)) + SH[C] = metrics(s)["sharpe"] + ct = float((stp0.mean() - stp.mean()) * DPY) + cs = float((ssk0.mean() - ssk.mean()) * DPY) + tot = float((b0.mean() - s.mean()) * DPY) + fq, _noz = quota_oltre(C, cur, rate) + print(f" ${C:>14,.0f}{D[C]:>10.2%}{VOLS[C]:>9.2%}{SH[C]:>9.3f}{tot:>12.3%}" + f"{ct:>13.3%}{cs:>14.3%}{fq:>17.1%}") + print(f" {clock()}") + print("\n 📌 La vol e' invariata su tutta la colonna finche' il costo non esplode: il costo") + print(" d'esecuzione e' una tassa sul DRIFT, non sul rischio — stessa forma del funding") + print(" (§35). E' cio' che autorizza il modello a un grado di liberta' della sezione 8.") + print(f" 📌 TROVATO PER STRADA: il drift a $635 ({D[635.0]:.2%}) e' PIU' BASSO che a $5.000") + print(f" ({D[5000.0]:.2%}) e il costo d'esecuzione non c'entra (0,00x%). E' il PAVIMENTO") + print(" `min_order` $5: a $635 un ribilanciamento su tre non si esegue, e la posizione") + print(" insegue il target peggio. Il capitale piccolo ha un suo costo, di segno opposto") + print(" a quello dello slippage — e nessuna tabella pubblicata lo distingue dall'altro.") + + # ===================================================================== + sez("6) IL MURO COME PUNTO FISSO, COL COSTO DENTRO") + print(""" Struttura da `r0726_wall_fixedpoint` (importato): serve capitale C per girare il + libro che determina il muro C, quindi C_{n+1} = muro(libro(C_n)). Con un costo FUNZIONE di C + l'iterazione ha un contenuto nuovo: il libro non cambia solo di composizione, cambia di + RENDIMENTO. Lente L3 CONGIUNTA (funding + fisco d'accumulo), de-luck x0,89.""") + inc = WF.included_at(L3.muro) + print("\n [struttura] `WF.included_at` alla taglia del muro direbbe " + + ", ".join(f"{k} {v:.0%}" for k, v in inc.items())) + print(" ⚠️ ma quella e' la composizione DEPLOYABLE del 26/07: GTAA01 e' fuori dallo stesso") + print(" 26/07 (PRIIPs) e la soglia $20k di XS01 e' caduta (§5-bis). Il libro che GIRA e") + print(" resta TP01+SKH01, ed e' quello su cui tutti i muri pubblicati sono calcolati:") + print(" qui cambia il COSTO alla taglia, non la composizione.") + print(f"\n {'iter':>5}{'ipotesi':>15}{'drift':>9}{'Sharpe':>9}{'perpetua':>10}{'-> muro':>15}") + Cn = L3.muro + for i in range(6): + s = libro(Cn, rate, cur) + L = PV.Lente("Ln", PV.deluck(s.values.astype(float)), False) + L.perp, L.muro = PV.muro_di(L) + print(f" {i:>5}{'$'+format(Cn,',.0f'):>15}{L.drift:>9.2%}" + f"{metrics(s)['sharpe']:>9.3f}{L.perp:>10.2%}{'$'+format(L.muro,',.0f'):>15}") + conv = abs(L.muro - Cn) / max(Cn, 1) < 0.02 + Cn = L.muro + if conv: + print(f"\n CONVERGE a ${Cn:,.0f} (variazione < 2%) {clock()}") + break + else: + print(f"\n non converge in 6 iterazioni, ultimo valore ${Cn:,.0f}") + muro_endo = Cn + Lf = PV.Lente("Lf", PV.deluck(libro(muro_endo, rate, cur).values.astype(float)), False) + Lf.perp, Lf.muro = PV.muro_di(Lf) + print(f"\n {'lente':<56}{'perpetua':>11}{'muro':>14}") + print(f" {'L3 CONGIUNTA, costo COSTANTE (pubblicato §36)':<56}{L3.perp:>11.2%}" + f"{'$'+format(L3.muro,',.0f'):>14}") + print(f" {'L3 + costo ENDOGENO alla taglia (punto fisso)':<56}{Lf.perp:>11.2%}" + f"{'$'+format(Lf.muro,',.0f'):>14}") + print(f"\n >>> il muro si sposta di {Lf.muro/L3.muro-1:+.1%} rispetto al numero pubblicato") + print(" ⚠️ La bisezione del muro e' un Monte Carlo a 4.000 percorsi: la sua risoluzione e'") + print(" ~1% (§36). Uno spostamento di questa taglia va letto come NULLO, non come +0,5%.") + + # ===================================================================== + sez("7) IL PUNTO DI SATURAZIONE — dove il costo marginale mangia il rendimento marginale") + print(""" Definizione: il profitto annuo in DOLLARI e' P(C) = C x drift(C). Il capitale + marginale smette di comprare reddito all'argmax di P (dP/dC = 0). E' il vero «quanto puo' + crescere questo libro», e il progetto non l'aveva mai calcolato. + ⚠️ La camminata assume l'ordine INTERO eseguito nell'istante contro il libro VISIBILE: e' + cio' che fa l'esecutore di OGGI (ordini market, nessuno slicing). Il numero e' percio' una + proprieta' dell'ESECUTORE, non del mercato — affettare l'ordine lo sposterebbe in alto.""") + print("\n (a) LIMITE DURO DI CAPACITA' — puramente [VENUE]+[BACKTEST], nessuna estrapolazione:") + print(f" {'capitale':>15}{'quota del nozionale annuo NON eseguibile in un istante':>58}") + Chard = None + for C in 10.0 ** np.arange(6.0, 8.01, 0.25): + fq, _ = quota_oltre(float(C), cur, rate) + if Chard is None and fq >= 0.01: + Chard = float(C) + print(f" ${C:>14,.0f}{fq:>57.1%}") + print(f" -> prima taglia con >=1% del nozionale fuori dal libro visibile: " + f"${Chard:,.0f}" if Chard else " -> mai, entro $100M") + print("\n (b) SATURAZIONE ECONOMICA — argmax di C x drift(C). Le celle oltre `lim` sono") + print(" [DEDOTTO] (pendenza log-log floor 1.0): la parte destra della curva dipende da") + print(" quell'assunzione, la SINISTRA no.") + grid = 10.0 ** np.arange(np.log10(3e5), 8.01, 0.05) + d0 = float(bZ.mean() * DPY) + for nome, qq in (("mediana", 0.50), ("p10 (libro migliore)", 0.10), ("p90 (peggiore)", 0.90)): + P, dd = [], [] + for C in grid: + s = libro(float(C), rate, cur, q=qq) + d = float(s.mean() * DPY) + dd.append(d) + P.append(C * d) + P, dd = np.asarray(P), np.asarray(dd) + j = int(np.argmax(P)) + c10 = grid[int(np.argmax(dd < 0.9 * d0))] if (dd < 0.9 * d0).any() else float("nan") + c50 = grid[int(np.argmax(dd < 0.5 * d0))] if (dd < 0.5 * d0).any() else float("nan") + fq, _ = quota_oltre(float(grid[j]), cur, rate) + print(f"\n curva {nome:<21} C* = ${grid[j]:>13,.0f} P(C*) = " + f"${P[j]:>11,.0f}/anno drift li' {dd[j]:.2%}") + print(f" {'':<27} costo = 10% del drift a ${c10:>13,.0f} 50% a ${c50:>13,.0f}") + print(f" {'':<27} a C* il {fq:.0%} del nozionale e' gia' fuori dal libro visibile") + print(f"\n {clock()}") + print(f" 📌 Il muro sta a ${Lf.muro:,.0f}. La saturazione e' PIU' DI UN ORDINE DI GRANDEZZA") + print(" sopra: il vincolo del piano non e' la capacita' del libro.") + print(" 📌 E le due definizioni CONCORDANO — argmax economico e primo 1% di nozionale non") + print(" eseguibile cadono nella stessa decade. Non e' un ottimo liscio: la saturazione") + print(" E' la fine del libro visibile.") + o_big = 1.25e6 + print(f"\n [DEDOTTO] E la cura e' nota e costa poco: un ordine da ${o_big:,.0f} costa " + f"{cur.bps('BTC', o_big):.1f} bps in un") + print(f" istante, ma spezzato in 10 tranche da ${o_big/10:,.0f} ne costa " + f"{cur.bps('BTC', o_big/10):.1f} l'una — {cur.bps('BTC', o_big)/max(cur.bps('BTC', o_big/10),1e-9):.0f}x") + print(" meno, SE il libro si ricostituisce fra una tranche e l'altra ([ASSUNTO]: non e'") + print(" misurato qui). Cioe' la capacita' di questo libro e' limitata dall'ESECUTORE, e la") + print(" riparazione e' una riga di codice — che pero' serve solo a ~$10M.") + + print("\n (c) CHI SATURA PER PRIMO (costo annuo per gamba, in punti di drift di LIBRO):") + print(f" {'capitale':>15}{'TP01':>10}{'SKH01':>10}{'ordine SKH':>15}" + f"{'bps su quello':>15}{'':>4}") + for C in (1e6, 3e6, 1e7, 3e7, 1e8): + stp, _ = tp01_costo(C * W_TP, rate, cur) + stp0, _ = tp01_costo(C * W_TP, rate, Z) + ssk, ssk0 = skh_costo(C * W_SKH, rate, cur), skh_costo(C * W_SKH, rate, Z) + o = C * W_SKH / 2 + flag = " [DED]" if cur.oltre("ETH", o) else "" + print(f" ${C:>14,.0f}{float((stp0.mean()-stp.mean())*DPY):>10.2%}" + f"{float((ssk0.mean()-ssk.mean())*DPY):>10.2%}{o:>15,.0f}" + f"{cur.bps('BTC', o):>15,.1f}{flag:>6}") + print(" 📌 SKH01 satura PRIMA di TP01 (ordine 100x piu' grande) ma TP01 lo SUPERA") + print(" subito dopo, perche' la sua CODA di ordini (max ~0,38 x capitale) e' ancora") + print(" piu' grande: due meccanismi diversi che mordono a due decadi diverse.") + + # ===================================================================== + sez("8) LA TRAIETTORIA COL COSTO LUNGO IL CAMMINO") + print(""" Attesa a priori del coordinatore, dichiarata prima di misurare: «poco, perche' il + grosso del tempo si passa a capitale piccolo dove il costo e' basso — ma il numero conta, + perche' se il muro sale mentre il cammino rallenta i due effetti si SOMMANO». + Modello: i percorsi si estraggono dal libro a $635 (quello di oggi) e ogni giorno si sottrae + `drift($635) - drift(cap_t)`, interpolato in log(capitale) sulla colonna della sezione 5.""") + caps_v = np.array(sorted(D)) + dr_v = np.array([D[c] for c in caps_v]) + + def mk_drag(dmap: dict): + cv = np.array(sorted(dmap)) + dv = np.array([dmap[c] for c in cv]) + + def _f(cap: np.ndarray) -> np.ndarray: + d = np.interp(np.log10(np.maximum(cap, 1.0)), np.log10(cv), dv) + return (dmap[635.0] - d) / DPY + return _f + + drag = mk_drag(D) + print("\n ⚠️ Il drag e' `D($635) - D(cap)` e contiene DUE effetti di segno opposto: il") + print(" pavimento min_order (che penalizza il capitale PICCOLO) e lo slippage (che") + print(" penalizza il GRANDE). Lungo il cammino cambia segno:") + print(f" {'capitale':>14}{'drag (in punti di drift annuo)':>36}") + for c in (2_000.0, 20_000.0, 100_000.0, 315_000.0, 1_000_000.0, 3_000_000.0): + print(f" ${c:>13,.0f}{drag(np.array([c]))[0]*DPY:>35.3%}") + print(" 📌 Per quasi tutto il tragitto il costo d'esecuzione e' NEGATIVO: uscire dal") + print(" pavimento min_order vale piu' di quanto costi lo slippage fino a ~$300k.") + base = libro(635.0, rate, cur) + Lb = PV.Lente("Lb", PV.deluck(base.values.astype(float)), False) + rng = np.random.default_rng(PV.SEED_TRAJ) + paths = CC._boot_paths(Lb.r, PV.N_TRAJ, PV.ANNI_TRAJ * 365, PV.BLOCK, rng) + print(f"\n {'EUR/mese':>9}{' costo COSTANTE (bersaglio $'+format(L3.muro,',.0f')+')':>40}" + f"{' costo ENDOGENO (bersaglio $'+format(Lf.muro,',.0f')+')':>40}") + print(f" {'':>9}{'mediana':>22}{'P(20a)':>18}{'mediana':>22}{'P(20a)':>18}") + for dep in (250.0, 500.0, 800.0): + a = PV.leggi(PN.accumula(paths, dep, L3.muro, Lb.aliq, Lb.patr, start=635.0)["colpito"]) + b = PV.leggi(accumula_costo(paths, dep, Lf.muro, Lb.aliq, Lb.patr, 635.0, + drag)["colpito"]) + print(f" {dep:>9,.0f}{PV.fmt_cond(a):>22}{a['p20']:>17.1%}" + f"{PV.fmt_cond(b):>22}{b['p20']:>17.1%}") + print(f" {clock()}") + print("\n Scomposizione a EUR500/mese (un grado di liberta' per volta):") + a0 = PV.leggi(PN.accumula(paths, 500.0, L3.muro, Lb.aliq, Lb.patr, start=635.0)["colpito"]) + a1 = PV.leggi(PN.accumula(paths, 500.0, Lf.muro, Lb.aliq, Lb.patr, start=635.0)["colpito"]) + a2 = PV.leggi(accumula_costo(paths, 500.0, Lf.muro, Lb.aliq, Lb.patr, 635.0, + drag)["colpito"]) + print(f" (i) base pubblicata {PV.fmt_cond(a0):>7} " + f"P(20a) {a0['p20']:.1%}") + print(f" (ii) solo il BERSAGLIO piu' alto {PV.fmt_cond(a1):>7} " + f"P(20a) {a1['p20']:.1%}") + print(f" (iii) + il CAMMINO piu' lento {PV.fmt_cond(a2):>7} " + f"P(20a) {a2['p20']:.1%}") + # controllo positivo del drag — e un errore MIO, catturato dal controllo stesso + def drag_finto(cap: np.ndarray) -> np.ndarray: + return np.where(cap > 100_000.0, 0.03 / DPY, 0.0) + + ab = PV.leggi(accumula_costo(paths, 500.0, Lf.muro, Lb.aliq, Lb.patr, 635.0, + drag_finto)["colpito"]) + mosso = abs(ab["p20"] - a2["p20"]) > 0.02 + print("\n CONTROLLO POSITIVO del drag — con un drag finto di 3%/anno sopra $100k la") + print(f" traiettoria DEVE muoversi: {PV.fmt_cond(ab):>6} P(20a) {ab['p20']:.1%} " + f"{'SI MUOVE' if mosso else '⚠️ NON SI MUOVE: drag sospetto'}") + if not mosso: + raise SystemExit("il drag non morde nemmeno a 3%/anno: macchina sospetta.") + Dfissa = {C: float(libro(C, rate, CurvaFissa(20.0)).mean() * DPY) for C in CAPS} + df20 = mk_drag(Dfissa) + print(f""" + ⚠️ ERRORE MIO, catturato da questo stesso controllo. La prima stesura usava come controllo + positivo un COSTO COSTANTE di 20 bps per ordine — e non muoveva niente: il drag di questa + sezione e' una DIFFERENZA, `D($635) - D(cap)`, e un costo che non dipende dalla taglia + abbassa i due termini della stessa quantita' e sparisce nella sottrazione. Misurato: + drag a $1M con costo costante 20 bps = {df20(np.array([1e6]))[0]*DPY:+.4%}/anno contro + {drag(np.array([1e6]))[0]*DPY:+.4%}/anno col costo vero. E' la stessa forma del null + degenere di §5-bis («il null del de-levering e' degenere quando la variante e' una + ri-scalatura»): un controllo puo' fallire perche' e' costruito male, non perche' la + macchina e' rotta — e distinguere le due cose e' il controllo.""") + + # ===================================================================== + sez("9) IL GRADINO DI LEVA COL COSTO ENDOGENO") + print(""" §53 lo lascio' aperto proprio qui: a 1,25x il nozionale sale SUBITO, quindi il + costo si paga SUBITO. La scala moltiplica il TARGET (SPEC-scale-key, §38), quindi moltiplica + anche la taglia di OGNI ordine: `costo_bps(k x nozionale)`, non `k x costo_bps(nozionale)`.""") + print(""" + ⚠️ CONVENZIONE, e non e' un dettaglio. Il `k*` pubblicato dal progetto (§35: 13,7 senza + de-luck, 12,25 con) e' calcolato da `FD.k_star` come argmax di `mean(log(1 + k*r))` sulla + serie di libro. RICOSTRUIRE il libro a scala k da' un numero DIVERSO e piu' alto, perche' i + due sleeve hanno clamp interni (`np.clip(net,-0.99)` in TP01, `max(capital,1.0)` in SKH01) + che a leva alta impediscono la rovina che il calcolo pubblicato invece conta. Per restare + comparabile, qui il costo endogeno entra come UN SOLO grado di liberta' sulla serie + pubblicata: `g(k) = g_exact(deluck(r_libro) - drag(k)/k, k)`, dove `drag(k)` e' il costo + d'esecuzione MISURATO alla taglia d'ordine k x nozionale.""") + KS = (1.0, 1.25, 1.5, 2.0, 3.0, 5.0) + KGRID = np.arange(0.5, 30.01, 0.25) + kstar_ctrl = None + KSTAR = {} + for cap_test, etichetta in ((635.0, "a $635 (il conto di oggi)"), + (float(Lf.muro), "alla taglia del muro"), + (5_000_000.0, "a $5M (dove il costo comincia a mordere)")): + print(f"\n --- {etichetta} ---") + r_base = PV.deluck(libro(cap_test, rate, Z).values.astype(float)) + + def gk(k: float) -> tuple[float, float]: + sk = libro(cap_test, rate, cur, scala=k) + s0k = libro(cap_test, rate, Z, scala=k) + dr = float(s0k.mean() - sk.mean()) # drag giornaliero alla scala k + return FD.g_exact(r_base - dr / k, k), dr * DPY + + print(f" {'k':>6}{'g(k) senza costo':>19}{'g(k) col costo':>17}{'costo/anno':>13}" + f"{'ordine SKH':>14}") + for k in KS: + g1, ca = gk(k) + g0 = FD.g_exact(r_base, k) + print(f" {k:>6.2f}{g0:>18.2%}{g1:>17.2%}{ca:>13.3%}" + f"{cap_test * W_SKH / 2 * k:>14,.0f}") + g0v = np.array([FD.g_exact(r_base, float(k)) for k in KGRID]) + g1v = np.array([gk(float(k))[0] for k in KGRID]) + k0 = float(KGRID[int(np.argmax(g0v))]) + k1 = float(KGRID[int(np.argmax(g1v))]) + KSTAR[etichetta] = (k0, k1) + if kstar_ctrl is None: + kstar_ctrl = k0 + print(f" k* con costo ZERO {k0:.2f}x -> k* con costo ENDOGENO {k1:.2f}x" + f" (mezzo-Kelly {k1/2:.2f}x) perdita {k0-k1:.2f}x") + kref = FD.k_star(PV.deluck(FD.book_75_25(rate).values.astype(float)))[0] + print(f"\n R9 controllo di replica: k* a costo ZERO su $635 = {kstar_ctrl:.2f}x contro " + f"{kref:.2f}x") + print(f" di `FD.k_star` sul libro pubblicato (alloc $600) " + f"{'RIPRODOTTO' if abs(kstar_ctrl - kref) <= 0.5 else '⚠️ DIVERGE'}") + print(f" {clock()}") + print("\n 📌 Il gradino 1,00x -> 1,25x costa, in costo d'esecuzione aggiuntivo, meno di un") + print(" punto base l'anno al conto di oggi. §53 chiedeva di misurarlo PRIMA: misurato,") + print(" non morde. Cio' che resta a bloccare il gradino sono le riserve gia' scritte") + print(" (drift stimato su 7,4 anni, coda assente dal dataset, chiave di scala assente") + print(" dalla config), NON il costo d'esecuzione.") + + # ===================================================================== + sez("VERDETTO") + kz, ke = KSTAR["alla taglia del muro"] + k5 = KSTAR["a $5M (dove il costo comincia a mordere)"][1] + print(f""" + [VENUE] La curva costo-taglia del perpetual USDC e' PIATTA al fondo (mezzo spread + {hs['BTC']:.4f}/{hs['ETH']:.4f} bps = UN TICK) e resta sotto ~2 bps fino a mezzo milione di + dollari per ordine — misurata di domenica notte UTC, cioe' nella finestra piu' sottile della + settimana. Il libro live manda ordini di ${sk635:,.0f} oggi e di ${Lf.muro*W_SKH/2:,.0f} alla + taglia del muro: entrambi dentro la regione piatta. + + [CALC] Il muro passa da ${L3.muro:,.0f} (costo costante, §36) a ${Lf.muro:,.0f} col costo + endogeno = {Lf.muro/L3.muro-1:+.1%}, cioe' DENTRO la risoluzione Monte Carlo del muro stesso. + La traiettoria EUR500/mese non si muove (P(20a) {a0['p20']:.1%} -> {a2['p20']:.1%}). La + saturazione economica sta a ~$1x10^7 e il primo 1% di nozionale non eseguibile in un istante + a ${Chard:,.0f}: due definizioni indipendenti che cadono nella stessa decade, oltre un ordine + di grandezza sopra il muro. + + [CALC] Il gradino di leva sopravvive: k* passa da {kz:.2f}x a {ke:.2f}x alla taglia del muro + ({ke-kz:+.2f}x), e a $635 il costo aggiuntivo di 1,25x sta sotto il punto base l'anno. Il + costo endogeno diventa vincolante sulla leva solo a $5M, dove k* crolla a {k5:.2f}x. + + Il rischio che i due scettici avevano nominato e' REALE ma vive su un ALTRO STRUMENTO e a + un'ALTRA TAGLIA: e' lo SPOT (§52, spread 100-500x piu' largo) e sono i milioni, non le + centinaia di migliaia. Sul perpetual, a questo capitale, il costo d'esecuzione NON e' il + vincolo. + + E il fatto piu' utile e' quello trovato per strada: alla taglia di OGGI il costo d'esecuzione + che le tabelle pubblicate non contengono ha segno NEGATIVO. Non e' lo slippage a essere + sottostimato: e' il PAVIMENTO `min_order`, che a $635 costa {(D[5000.0]-D[635.0])*1e4:.0f} bps + di drift l'anno — circa {(D[5000.0]-D[635.0])/max(D[313000.0]-D[1000000.0],1e-9):.0f}x cio' + che lo slippage costa passando da $313k a $1M, e in direzione opposta. +""") + + +if __name__ == "__main__": + main() From c97f221c62588af63b7e0dc1a1afa4303ca42e86 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 03:29:41 +0000 Subject: [PATCH 101/114] =?UTF-8?q?registro=20=C2=A755:=20COSTO-CAPITALE?= =?UTF-8?q?=20=E2=80=94=20il=20costo=20endogeno=20non=20morde,=20e=20alla?= =?UTF-8?q?=20taglia=20di=20oggi=20il=20costo=20mancante=20ha=20segno=20ne?= =?UTF-8?q?gativo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 72 ++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index e31809f..b4fc52d 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -720,7 +720,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 52 | SKEPTIC-SPOT | il lead dello spot (+1,55-1,66%/anno) sopravvive a un attacco deliberato? | 🚨 **REGGE A $600, SI ANNULLA A $272k** — 5 attacchi su 6 falliscono; quello che morde non e' nei numeri, e' **nella taglia a cui vengono spesi**: il lead sparisce **esattamente al muro che pretende di spostare** | | 53 | SKEPTIC-LEVA | il gradino di leva 1,25x sopravvive a un attacco deliberato, col funding dentro? | **REGGE 5/6, INCRINA 1/6** — il **1,25x sopravvive**, il **1,50x NO**; k massimo difendibile **1,40**. L'incrinatura e' il ramo `eq_fallback`, dove **la scala non apre il buco: lo MOLTIPLICA** | | 54 | PREVDAY-GATE | il piu' forte candidato misurato sta in monitor da giugno senza gate: quanto vale al gate onesto? | **GATE PREVDAY-01 SCRITTO — 8/10 condizioni oggi**, e le due che mancano sono la stessa cosa: 🚨 **la cella che GIRA non e' quella che la selezione onesta sceglie** — e la selezione compra il libro **LONG-FLAT**, cioe' butta via la gamba per cui PREVDAY fu promosso | -| 55 | COSTO-CAPITALE | i muri sono calcolati a costo d'esecuzione COSTANTE, ma il costo cresce col capitale: dove sta il muro vero? | _in corso_ | +| 55 | COSTO-CAPITALE | i muri sono calcolati a costo d'esecuzione COSTANTE, ma il costo cresce col capitale: dove sta il muro vero? | **NON MORDE** — il muro si sposta di **+0,6%** (dentro la risoluzione MC), la saturazione sta **30x sopra** ($9,5-10M), e 📌 **alla taglia di oggi il costo mancante ha segno NEGATIVO** | | 56 | XS-AMPIEZZA | su un universo povero (11 gambe), si recupera ampiezza EFFETTIVA senza aggiungere gambe? | **NO — e la caduta NON era ampiezza (solo il 14%)**: e' **selettivita'** (recuperabile ma sotto l'MDE) **piu' un IC che su quelle 11 gambe vale ZERO**. 🚨 E `k=5` **non e' invariante di scala** | | — | **NOTIFIER** (audit di fatto) | 🚨 **PUNTO SINGOLO DI GUASTO nella rete di sicurezza** | il `send()` di Telegram e' **un solo tentativo da 10s, senza retry, con l'esito MAI registrato** — e lo stato `alerted=True` viene scritto **prima** dell'invio: un 🚨 di `venue_watch` perso e' perso **per l'intero episodio**. Tasso di fallimento **misurato 6,9%** (2/29, IC95 [1,9%, 22,0%]) | | 22 | MAKER | l'esecuzione passiva e' una fonte di ritorno, al netto del costo di non essere eseguiti? | **SCARTATO** — il segno dipende da `<` contro `<=` | @@ -3430,3 +3430,73 @@ nuovo.)* **VERDETTO: `NO — E LA CADUTA NON ERA AMPIEZZA: E' SELETTIVITA' (RECUPERABILE, MA SOTTO L'MDE) PIU' UN IC CHE SU QUELLE 11 GAMBE VALE ZERO`**. XS01 resta bloccato dal venue, **e adesso si sa perche'**. **Libro, pesi, cron, config INVARIATI.** + +--- + +## 55 — COSTO-CAPITALE (il costo d'esecuzione come funzione del capitale) + +`scripts/research/r0823_costo_capitale.py` (**144 s**, sola lettura, nessun ordine). Nato da un +follow-up che **due agenti hanno dichiarato indipendentemente** la stessa notte (§52 e §53). + +### La curva costo-taglia, misurata + +**[VENUE]** 50 istantanee (25+25) in 35 minuti, **camminate su entrambi i lati** — non una +fotografia. Mediana [p10-p90]: + +| ordine | BTC_USDC-PERP | ETH_USDC-PERP | +|---|---|---| +| $5 | **0,006** [0,006-0,356] bps | **0,021** bps | +| $500 | 0,006 | 0,021 | +| $5.000 | 0,315 | 0,373 | +| $50.000 | 0,714 | 1,284 | +| $500.000 | **2,198** | **2,207** | +| $1.000.000 | 14,03 | 5,22 | + +Fine del libro visibile: **BTC $2,81M · ETH $1,19M**. **Il fondo della curva e' il mezzo spread = UN +TICK.** ⚠️ Il campione e' di **domenica notte UTC**, la finestra piu' sottile della settimana: +**conservativo dentro il presente, ottimista proiettato su 2019-2026** — dichiarato in testa allo +script. + +✅ **Repliche 8/8 + R9**: TP01/SKH01/libro a costo spento **bit-exact**; muri **$272.061** e +**$258.338 al dollaro** sul vintage 07/08, **$313.143** oggi; `accumula_costo` bit-exact contro +`PN.accumula`; `walk_cost_bps` identica a quella di §52; **R9: k\* a costo zero = 12,25x = +`FD.k_star`**. Due controlli positivi obbligatori superati. + +**Il muro col costo endogeno: $313.143 → $315.080 = +0,6%**, convergente in una iterazione. +📌 **Va letto come NULLO**: la bisezione e' un Monte Carlo a 4.000 percorsi con risoluzione ~1%. +Con la curva p90 resta +1,4%. + +**Il punto di saturazione — due definizioni indipendenti, stessa decade:** **duro** (primo 1% di +nozionale annuo non eseguibile in un istante) **$10,0M**; **economico** (argmax di `C x drift(C)`) +**$9,49M** [$8,46-10,64M], profitto $1,28M/anno. 📌 **Non e' un ottimo liscio: la saturazione E' la +fine del libro visibile.** E **la cura e' dell'esecutore, non del mercato** — $1,25M costano 41,8 bps +in un istante e **1,1 bps a tranche da $125k**; serve solo a ~$10M. **SKH01 satura prima** (ordine +~100x la mediana di TP01, e il 69% del nozionale annuo), **TP01 lo supera subito dopo** per la coda +dei suoi ordini. + +**Traiettoria €500/mese: 17,3a / P(20a) 85,1% → 17,3a / 84,7%.** L'attesa del coordinatore era +giusta, **ma per un motivo in piu' di quello previsto: il drag CAMBIA SEGNO** (−0,118%/anno a $2k, ++0,118% a $1M). **Gradino di leva: k\* 12,50x → 12,25x** alla taglia del muro; **a $635 il costo +aggiuntivo di 1,25x sta sotto il punto base l'anno**; morde solo a $5M. + +**Le due obiezioni degli scettici, misurate.** **§52 non e' contraddetta, e' confermata dall'altro +lato:** il costo che si annullava era dello **SPOT** (spread **100-500x** piu' largo del perpetual). +**§53 aveva la domanda giusta e la grandezza sbagliata:** il 21,9% era la quota di una barra 5m **a +volume basso**; l'ordine di oggi e' lo **0,13% (BTC) / 0,43% (ETH)** della profondita' a 1 bps, e a +1,25x diventa 0,16% / 0,54%. **La riserva di §53 e' misurata e non regge.** + +🚨 **IL FATTO TROVATO PER STRADA, ed e' il piu' utile: alla taglia di oggi il costo che le tabelle +pubblicate non contengono ha segno NEGATIVO.** Il drift a **$635 e' 13 bps/anno PIU' BASSO** che a +$5.000 — e non per lo slippage: e' il **pavimento `min_order` $5**. Vale **~2x** quello che lo +slippage costa passando da $313k a $1M, **in direzione opposta**. *Il conto piu' preciso non muove +il muro: sposta il segno dell'errore, e alla taglia di oggi lo sposta a favore.* + +⚠️ **Errori dell'agente, catturati e pubblicati nello script:** (i) la banda p10/p50/p90 era +costruita con **tre oggetti-curva separati** → oltre il libro visibile la p10 finiva **sopra** la +p50: *una banda che si incrocia non e' una banda*; (ii) il controllo positivo del drag era +**degenere per costruzione** (costo costante di 20 bps, che sparisce da un drag **differenziale**) — +stessa forma del null de-levering degenere di §5-bis: *un controllo puo' fallire perche' e' +costruito male, non perche' la macchina e' rotta.* + +**VERDETTO: `IL COSTO ENDOGENO NON MORDE — IL MURO SI SPOSTA DI +0,6% (DENTRO LA RISOLUZIONE MC) E +LA SATURAZIONE STA 30x SOPRA`**. **Libro, pesi, cron, config INVARIATI.** From 8c57274bacd5872d5ed893a82d4c25124adfb685 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 03:36:22 +0000 Subject: [PATCH 102/114] =?UTF-8?q?registro=20=C2=A754=20addendum:=20serie?= =?UTF-8?q?=20PREVDAY=20intatta=201512/1512=20ma=20MDE=204,72=20sulla=20fi?= =?UTF-8?q?nestra=20forward=20=E2=80=94=20il=20gate=20non=20puo'=20appoggi?= =?UTF-8?q?arsi=20al=20numero=20forward=20(e=20un=20mio=20meccanismo=20era?= =?UTF-8?q?=20falso)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index b4fc52d..328a072 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -3320,6 +3320,27 @@ gamba short compensasse il funding). COSA: LA CELLA CHE GIRA NON E' QUELLA CHE LA SELEZIONE ONESTA SCEGLIE`**. **Nessun cambio al libro live proposto.** + +### Addendum del coordinatore — la finestra forward di PREVDAY non puo' decidere niente + +Misura mia, sulla serie che il gate userebbe (`data/paper_prevday/returns.jsonl`, **sola lettura**). +✅ **La serie e' INTATTA**: **1512 barre orarie su 1512 attese** dal 2026-06-21 01:00 al 2026-08-23 +00:00, zero buchi — **e' il monitor sano**, quello che il difetto `advance()` non tocca (cadenza +oraria, la barra si chiude ogni ora). Questo la distingue da STATARB/XSR01/DVOLSPREAD. + +**Ma la finestra e' lunga 0,172 anni**, e li' l'**MDE(95%) e' 4,72 di Sharpe**: lo **+2,11** +osservato **non e' distinguibile da zero**. Il campione di scommesse e' **26 ENTRY** (14 BTC / +12 ETH, 151 trade/anno). ⚠️ **Conseguenza per `GATE PREVDAY-01`: qualunque cosa il gate decida, non +puo' appoggiarsi al numero forward** — le 8/10 condizioni superate stanno in piedi sul backtest e +sulla struttura, non sui 63 giorni di monitor. + +⚠️ **Errore mio, catturato prima di pubblicarlo:** avevo formulato il meccanismo come *"la +precisione apparente viene dal numero di MARK, non dal numero di SCOMMESSE"* — **falso**. La +deviazione standard dello Sharpe annualizzato vale ~`1/sqrt(anni)` **indipendentemente dalla +frequenza di campionamento**: infatti la stima al livello barra e quella al livello trade danno lo +**stesso** 4,72. Campionare piu' fitto non gonfia la precisione, e la formula lo sapeva gia'. +**Il fatto vero e' piu' semplice: 63 giorni sono 63 giorni.** + --- ## 56 — XS-AMPIEZZA (recuperare ampiezza su un universo povero) From 37c83c11c9b828dd832e4c0625191bc79a040b90 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 04:06:28 +0000 Subject: [PATCH 103/114] =?UTF-8?q?critico=20di=20chiusura:=20il=20muro=20?= =?UTF-8?q?$313k=20e'=20una=20mediana=20con=20banda=20[$187k,=20$1,14M],?= =?UTF-8?q?=20e=20=C2=ABN/N=20ancore=C2=BB=20vale=20~2=20osservazioni?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ultimo agente dell'ondata 2026-08-22/23. Sola lettura, nessun file di produzione toccato. Attese a priori dichiarate nel docstring: A1/A4/A5/A6 confermate, A2 e A3 REFUTATE. REPLICHE (prima di ogni accusa, tutte riuscite): - libro 75/25 path live: drift 19,23%/17,07%, Sharpe 1,692/1,513, funding -2,1597%/anno - i quattro muri L0/L1/L2/L3 riprodotti al dollaro: $278.033 / $264.373 / $327.617 / $313.143, e il muro E' esattamente `prelievo / perpetua` - §42 dShFULL(floor=-1) all'ancora 0 = -0,4502 (pubblicato -0,450) - TP01 canonico ShFULL 1,305; XS01 fase 0 == sleeve di produzione a max|diff| 0,0 - funding ri-misurato da implementazione indipendente: TP01 2,138%/anno, rapporto condizionale 1,93x (BTC) / 2,58x (ETH) contro il pubblicato 1,86-2,55x TROVATO: 1. Il muro $313k non ha mai avuto una banda. Propagando SOLO la SE del drift (5,151%/anno, block bootstrap; §53 la misura 5,09 su altra lente) va da $204k (+1 SE) a $707k (-1 SE), p10-p90 [$187k, $1,14M], e a -2 SE il traguardo NON esiste a nessun capitale. La riga «EUR500/mese -> P(20a) 85%» diventa ~0% a -1 SE. L'ondata ha pubblicato la risoluzione Monte Carlo del muro (0,7%) e mai quella del suo input, che e' 100x piu' grande. 2. «positivo in N/N ancore/fasi» non e' N osservazioni: N_eff misurato 1,6-2,1 (24 ancore TP01, corr 0,63) e 1,2-1,5 (10 fasi XS01, corr 0,79), con controllo positivo 24,5 / 1,00. La componente comune NON si cancella nella differenza appaiata (corr 0,59) -> A3 refutata. E la banda d'ancora non e' un IC: per la stessa grandezza l'IC95 bootstrap e' 4,3x piu' largo e contiene lo zero. I verdetti reggono, la precisione dei numeri no. 3. Tre baseline diverse sotto lo stesso nome «libro 75/25»: 1,68 (hourly, tutti i muri), 1,80 (canonical, §41/42/49/50/54), 1,63 (mediana d'ancora, §8/25) — spread 0,12 di Sharpe e 1,7pp di maxDD, piu' grande di quasi tutti gli effetti misurati. 4. «Soffitto direzionale ~1,15» contro 1,639 (§23) e 1,621 (§54) nella stessa ondata: seconda refutazione indipendente dell'argomento aritmetico di §12. 5. «EUR X/mese» versa ogni 30 GIORNI (12,17 versamenti/anno, +0,8-1,4%); e il contatore `versato` non si ferma al traguardo -> a 10 anni i bonifici fanno il 61%, non il 73%. 6. MDE: la catena opzioni misurata da me da' 77 giorni di superficie (registro 74-75, ok) = MDE 4,3 di Sharpe -> ogni SCARTATO di §4/§5/§7/§11 che poggia su uno Sharpe e' un non-risultato su quell'asse. §21 XS01-OOS, pilastro del canale funded, ha effetto +1,12 contro un MDE di 1,13. 7. Errore mio catturato in sessione: la prima stesura de-luckava una serie gia' de-luckata (0,89^2) e stampava un finto +31,5% sul muro; il valore vero della scelta di modello e' +4,0%. RISPOSTA AL MANDATO: no. La somma di TUTTI i lead positivi dell'ondata, se fossero autorizzati e additivi (non lo sono), vale +0,036 EUR/giorno a $635. EUR250 -> EUR500 al mese porta P(20a) dal 14% all'85%. Il valore dell'ondata e' difensivo ed e' reale; il mandato «arrivare ai 50 giornalieri velocemente» ha risposta negativa. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf --- scripts/research/r0823_critico_chiusura.py | 821 +++++++++++++++++++++ 1 file changed, 821 insertions(+) create mode 100644 scripts/research/r0823_critico_chiusura.py diff --git a/scripts/research/r0823_critico_chiusura.py b/scripts/research/r0823_critico_chiusura.py new file mode 100644 index 0000000..f5a9c44 --- /dev/null +++ b/scripts/research/r0823_critico_chiusura.py @@ -0,0 +1,821 @@ +"""r0823 — CRITICO DI CHIUSURA dell'ondata 2026-08-22/23 (55 filoni, `research/wave-0822`). + +Non cerca strategie: attacca cio' che l'ondata ha CONCLUSO, coordinatore incluso. +Sola lettura. Nessun ordine, nessuna rete, nessun file di produzione toccato. + +============================================================================================= +ATTESE A PRIORI — scritte PRIMA di misurare (un critico che ha sempre ragione non sta criticando) +============================================================================================= +A1 Le repliche dei numeri di testa RIUSCIRANNO (muri, funding, dShFULL della short di TP01): + l'ondata ha fatto molte repliche interne e i suoi script sono ripetibili. + -> attesa: CONFERMATA. Se fallisse, sarebbe il risultato piu' grosso del filone. +A2 Il muro di $313k sara' molto sensibile al drift, ma ci aspettiamo un fattore ~1,5x fra + +1SE e -1SE, cioe' una banda dell'ordine di [$230k, $430k]. + -> attesa: DA MISURARE. (esito: REFUTATA — misurato [$204k, $707k] a +/-1 SE, e a -2 SE + il traguardo NON esiste: la mia stima era ottimista di ~2x sull'ampiezza) +A3 Le 24 ancore / 10 fasi porteranno meno informazione di quanta ne sembri, ma su una + DIFFERENZA APPAIATA la componente comune si cancellera' e il numero effettivo di + osservazioni indipendenti sara' >= 5. + -> attesa: DA MISURARE. (esito: REFUTATA — non si cancella quasi nulla) +A4 La maggior parte degli SCARTATI dell'ondata sara' correttamente etichettata come + "non misurabile" quando lo e': l'ondata cita gli MDE spesso. + -> attesa: CONFERMATA in larga parte, con eccezioni nominate. +A5 L'aritmetica pubblicata (muri = prelievo/perpetua, tetti di leva) tornera'. + -> attesa: CONFERMATA. +A6 Nessuna misura dell'ondata battera' "versare": ce lo aspettiamo perche' l'ondata stessa + lo ha scritto due volte. Il valore e' nel METTERLE IN FILA con la stessa unita'. + -> attesa: CONFERMATA. + +METODO + * ogni accusa e' MISURATA, non argomentata; + * ogni rilevatore ha un controllo positivo (un rilevatore che non trova nulla e' + indistinguibile da uno rotto); + * le repliche vengono PRIMA di ogni numero nuovo. + +USO: uv run python scripts/research/r0823_critico_chiusura.py [--fast] +""" +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] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +from src.data.downloader import load_data # noqa: E402 +from src.strategies.trend_portfolio import ( # noqa: E402 + CANONICAL, TrendPortfolio, simple_returns) +from src.portfolio.sleeves import XS_CFG, XS_UNIVERSE, _HL_DIR, _xsec_returns # noqa: E402 + +import r0725_capcurve as CC # noqa: E402 EURUSD, TARGET_EUR_DAY, TAX_RATE, _boot_paths +import r0807_piano_netto as PN # noqa: E402 sopravvivenza / accumula +import r0727_tasse as TX # noqa: E402 PATRIMONIALE + +FAST = "--fast" in sys.argv +N_WALL = 2500 if FAST else 4000 +N_TRAJ = 2000 if FAST else 3000 +BLOCK = 20 +DELUCK = 0.89 +SEED = 725 +T0 = time.time() + +# La serie giornaliera del libro live 75/25, con e senza funding, prodotta da +# r0822d_piano_vero.costruisci_serie(). Se la cache non c'e' si rigenera (richiede rete). +CACHE = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/pianovero/book.parquet") + + +def sez(t: str) -> None: + print("\n" + "=" * 102) + print(f" {t} [{time.time() - T0:5.0f}s]") + print("=" * 102) + + +def sh(r: np.ndarray) -> float: + r = np.asarray(r, float) + return float(r.mean() / r.std() * np.sqrt(365.25)) + + +def deluck(r: np.ndarray) -> np.ndarray: + """Toglie dal DRIFT la fortuna d'ancora misurata (r0726_deluck_factor). La vol resta.""" + r = np.asarray(r, float) + return r - (1.0 - DELUCK) * r.mean() + + +def boot_stat(x: np.ndarray, fn, n: int = 3000, block: int = BLOCK, seed: int = 7): + """Block bootstrap di una statistica scalare su una serie (o su una matrice n x k).""" + rng = np.random.default_rng(seed) + x = np.asarray(x, float) + N = x.shape[0] + nb = int(np.ceil(N / block)) + out = np.empty(n) + for i in range(n): + st = rng.integers(0, N - block, size=nb) + ix = (st[:, None] + np.arange(block)[None, :]).reshape(-1)[:N] + out[i] = fn(x[ix]) + return out + + +def n_eff(M: pd.DataFrame) -> tuple[float, float, float]: + """(corr media fuori diagonale, N_eff da rbar, participation ratio degli autovalori).""" + C = M.corr().values + k = C.shape[0] + iu = np.triu_indices(k, 1) + rbar = float(C[iu].mean()) + ev = np.linalg.eigvalsh(C) + pr = float(ev.sum() ** 2 / (ev ** 2).sum()) + return rbar, k / (1.0 + (k - 1) * rbar), pr + + +# ============================================================================================= +# 1) REPLICHE — i controlli positivi della MIA macchineria, prima di qualunque accusa +# ============================================================================================= +def resample_off(df: pd.DataFrame, h: int) -> pd.DataFrame: + """Daily con ancora all'ora h. Stessa convenzione di r0702_tp01_offset.resample_offset.""" + d = df.copy() + d["datetime"] = pd.to_datetime(d["datetime"]) + return (d.set_index("datetime") + .resample("24h", offset=pd.Timedelta(hours=h), label="left", closed="left") + .agg({"open": "first", "high": "max", "low": "min", + "close": "last", "volume": "sum"}) + .dropna().reset_index()) + + +_RAW: dict[str, pd.DataFrame] = {} + + +def tp01_at(h: int, long_only: bool = True) -> pd.Series: + """Serie giornaliera netta di TP01 50/50 all'ancora h (long-flat o long-short).""" + if not _RAW: + for a in ("BTC", "ETH"): + _RAW[a] = load_data(a, "1h") + tp = TrendPortfolio(**{**CANONICAL, "long_only": long_only}) + out = {} + for a in ("BTC", "ETH"): + df = resample_off(_RAW[a], h) + r = simple_returns(df["close"].values.astype(float)) + tgt = tp.target_series(df) + held = np.zeros(len(tgt)) + held[1:] = tgt[:-1] + net = held * r - tp.fee_side * np.abs(np.diff(held, prepend=0.0)) + net[0] = 0.0 + out[a] = pd.Series(np.clip(net, -0.99, None), + index=pd.to_datetime(df["datetime"]).dt.normalize()) + J = pd.concat(out, axis=1, join="inner").fillna(0.0) + return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index) + + +def xs01_phase(phase: int) -> pd.Series: + """XS01 col meccanismo CONGELATO, ribilanciato alla fase `phase` del ciclo H=10.""" + cols = {} + for sym in XS_UNIVERSE: + p = _HL_DIR / f"hl_{sym.lower()}_1d.parquet" + if p.exists(): + d = pd.read_parquet(p) + cols[sym] = pd.Series(d["close"].values.astype(float), + index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)) + C = pd.concat(cols, axis=1, join="inner").sort_index().dropna() + px = C.values + n, A = px.shape + lbs, H, k, tv = XS_CFG["lookbacks"], XS_CFG["H"], XS_CFG["k"], XS_CFG["target_vol"] + dp, mh, mlb = XS_CFG["disp_pct"], XS_CFG["disp_minhist"], max(XS_CFG["lookbacks"]) + dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0]) + W = np.zeros((n, A)) + w = np.zeros(A) + hist: list[float] = [] + for i in range(n): + if i >= mlb and i % H == phase: + rLs = [px[i] / px[i - L] - 1.0 for L in lbs] + disp = float(np.mean([r.std() for r in rLs])) + thr = np.percentile(hist, dp) if (dp > 0 and len(hist) >= mh) else -np.inf + if disp >= thr: + score = np.zeros(A) + cnt = 0 + for rL in rLs: + sd = rL.std() + if sd > 0: + score += (rL - rL.mean()) / sd + cnt += 1 + if cnt: + score /= cnt + o = np.argsort(score) + w = np.zeros(A) + w[o[-k:]] = 0.5 / k + w[o[:k]] = -0.5 / k + else: + w = np.zeros(A) + hist.append(disp) + W[i] = w + gross = np.zeros(n) + gross[1:] = np.sum(W[:-1] * dret[1:], axis=1) + turn = np.zeros(n) + turn[0] = np.abs(W[0]).sum() + turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1) + s = pd.Series(gross - turn * (0.001 / 2.0), index=C.index) + rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25) + scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0) + return pd.Series(s.values * scale, index=C.index) + + +def perpetua(r: np.ndarray, aliq: float, patr: float, + n_paths: int = N_WALL, seed: int = SEED) -> float: + """Prelievo annuo massimo (frazione) con P(capitale a 20a >= iniziale) >= 90%. + Stessa definizione e stessa bisezione di r0807_piano_netto.muro / r0822d_piano_vero.muro_di.""" + lo, hi = 0.0, 0.40 + for _ in range(13): + mid = (lo + hi) / 2 + s = PN.sopravvivenza(r, 1e6, 1e6 * mid, 20, aliq, patr, n_paths=n_paths, seed=seed) + if s["p10_end"] >= 1e6: + lo = mid + else: + hi = mid + return lo + + +PREL_NET = CC.TARGET_EUR_DAY * 365 * CC.EURUSD # 50 EUR/g netti, in $/anno +PREL_LORDO = PREL_NET / (1 - CC.TAX_RATE) + + +def sezione_repliche(B: pd.DataFrame) -> dict: + sez("1) REPLICHE — la mia macchineria riproduce l'ondata? (prima di ogni accusa)") + out = {} + A, F = B["nofund"].values, B["fund"].values + print(f" serie del libro live 75/25, {len(B)} giorni, {B.index[0].date()} -> {B.index[-1].date()}") + print(f" {'':22}{'drift':>10}{'vol':>10}{'Sharpe':>10} pubblicato (§35)") + print(f" {'senza funding':22}{A.mean()*365:9.2%}{A.std()*365**.5:10.2%}{sh(A):10.3f}" + f" 19,24% / 11,37% / 1,692") + print(f" {'con funding':22}{F.mean()*365:9.2%}{F.std()*365**.5:10.2%}{sh(F):10.3f}" + f" 17,08% / 11,29% / 1,513") + dfund = (F.mean() - A.mean()) * 365 + print(f" costo del funding {dfund:+.4%}/anno pubblicato -2,1597% " + f"{'OK' if abs(dfund + 0.021597) < 2e-4 else 'DIVERGE'}") + out["fund_ok"] = abs(dfund + 0.021597) < 2e-4 + + L0, L1, L3 = deluck(A), deluck(A), deluck(F) + rows = [("L0 LORDO (26/07)", L0, 0.0, 0.0, PREL_LORDO, 0.1068, 278_000), + ("L1 +FISCO (07/08)", L1, CC.TAX_RATE, TX.PATRIMONIALE, PREL_NET, 0.0752, 264_000), + ("L2 +FUNDING (22/08)", L3, 0.0, 0.0, PREL_LORDO, 0.0906, 328_000), + ("L3 CONGIUNTA (§36)", L3, CC.TAX_RATE, TX.PATRIMONIALE, PREL_NET, 0.0635, 313_000)] + print(f"\n {'lente':<22}{'drift':>9}{'perpetua':>10}{'muro':>13}{'pubbl.':>11}{'scarto':>9}") + for nome, r, al, pa, prel, p_pub, m_pub in rows: + p = perpetua(r, al, pa) + muro = prel / p + print(f" {nome:<22}{r.mean()*365:8.2%}{p:10.2%}{muro:13,.0f}{m_pub:11,.0f}" + f"{muro/m_pub-1:+9.1%}") + out[nome] = dict(perp=p, muro=muro, r=r, aliq=al, patr=pa, prel=prel) + out["_raw_nofund"] = A + print(" ✅ le quattro lenti si riproducono; il muro E' esattamente `prelievo / perpetua`.") + + # replica di §42 (la short del trend) con implementazione indipendente + d0 = sh(tp01_at(0, long_only=False).values) - sh(tp01_at(0, long_only=True).values) + print(f"\n §42 dShFULL(floor=-1) all'ancora 0: {d0:+.4f} pubblicato -0,450 (estremo p10)") + print(f" §41/§35 TP01 canonico ShFULL: {sh(tp01_at(0).values):.3f} pubblicato 1,305-1,314") + # replica di XS01 + x = _xsec_returns() + p0 = xs01_phase(0) + print(f" XS01 fase 0 == sleeve di produzione: max|diff| = " + f"{float(np.abs(p0.values - x.values).max()):.3e}") + return out + + +# ============================================================================================= +# 2) LA BANDA CHE NON E' STATA PUBBLICATA +# ============================================================================================= +def sezione_muro(L: dict) -> dict: + sez("2) IL MURO E' UNA FUNZIONE ESPLOSIVA DI UN DRIFT STIMATO SU 7,4 ANNI") + l3 = L["L3 CONGIUNTA (§36)"] + r = l3["r"] + d = boot_stat(r, lambda x: x.mean() * 365, n=4000, seed=11) + SE = float(d.std()) + print(f" drift L3 {r.mean()*365:.3%}/anno · SE block-bootstrap (blocchi 20g) {SE:.3%}" + f" · IC95 [{np.percentile(d,2.5):.2%}, {np.percentile(d,97.5):.2%}]") + print(f" (§53 misura SE 5,09% su un'altra lente: ✅ concorda)") + print("\n Muro e traiettoria propagando SOLO l'incertezza del drift (vol e forma invariate):") + print(f" {'':>10}{'drift':>9}{'perpetua':>10}{'muro':>14}{'x muro pubbl.':>15}") + band = {} + for lab, k in (("+2 SE", 2.0), ("+1 SE", 1.0), ("p90", 1.2816), ("PUNTO", 0.0), + ("p10", -1.2816), ("-1 SE", -1.0), ("-2 SE", -2.0)): + rr = r + k * SE / 365.0 + p = perpetua(rr, l3["aliq"], l3["patr"]) + muro = PREL_NET / p if p > 0.002 else float("inf") + band[lab] = (rr, p, muro) + m = f"${muro:,.0f}" if np.isfinite(muro) else "IRRAGGIUNGIBILE" + x = f"{muro/l3['muro']:.2f}x" if np.isfinite(muro) else "—" + print(f" {lab:>10}{rr.mean()*365:8.2%}{p:10.2%}{m:>14}{x:>15}") + lo = band["p10"][2] + hi = band["p90"][2] + print(f"\n 🚨 BANDA p10-p90 DEL MURO: [${hi:,.0f}, " + f"{('$%s' % f'{lo:,.0f}') if np.isfinite(lo) else 'IRRAGGIUNGIBILE'}]" + f" contro il punto pubblicato ${l3['muro']:,.0f}") + print(" Il muro e' `prelievo / perpetua` e la perpetua va a ZERO ben prima del drift:") + print(" e' un 1/x su una quantita' che si annulla, quindi l'errore NON e' simmetrico.") + print(" ⚠️ La SE del block-bootstrap e' una LIMITE INFERIORE dell'incertezza vera") + print(" (misura la variabilita' dentro gli stessi 7,4 anni, non il cambio di regime).") + + print("\n Traiettorie sotto la stessa banda (start $635, 25 anni, block bootstrap):") + print(f" {'drift':>8}{'muro':>13}" + "".join(f"{f'EUR{d}/m':>18}" for d in (250, 500, 1000))) + for lab in ("+1 SE", "PUNTO", "-1 SE"): + rr, p, muro = band[lab] + if not np.isfinite(muro): + continue + rng = np.random.default_rng(SEED) + paths = CC._boot_paths(rr, N_TRAJ, 25 * 365, BLOCK, rng) + cells = [] + for dep in (250, 500, 1000): + a = PN.accumula(paths, dep, muro, l3["aliq"], l3["patr"], start=635.0) + y = np.where(a["colpito"] >= 0, a["colpito"] / 365.0, np.inf) + med = np.median(y) + cells.append(f"{('>25a' if not np.isfinite(med) else f'{med:.1f}a')} P{float((y<=20).mean()):.0%}") + print(f" {rr.mean()*365:7.2%}{muro:13,.0f}" + "".join(f"{c:>18}" for c in cells)) + print("\n 📌 «EUR500/mese -> P(20a) 85%» e' il PUNTO. A -1 SE di drift la stessa riga e' ~0%.") + print(" L'ondata ha misurato la risoluzione MONTE CARLO del muro (0,7%) e mai quella") + print(" del suo INPUT. Le due differiscono di due ordini di grandezza.") + return band + + +# ============================================================================================= +# 3) «N/N ANCORE» NON E' N OSSERVAZIONI +# ============================================================================================= +def sezione_ancore() -> None: + sez("3) «0/24 ANCORE» E «10/10 FASI»: quante osservazioni indipendenti sono davvero?") + + # -- controllo positivo del rilevatore (obbligatorio) ------------------------------------- + rng = np.random.default_rng(1) + indip = pd.DataFrame(rng.normal(size=(2000, 24))) + uguali = pd.DataFrame(np.repeat(rng.normal(size=(2000, 1)), 24, axis=1) + + rng.normal(scale=1e-6, size=(2000, 24))) + for nome, M in (("24 serie INDIPENDENTI", indip), ("24 serie IDENTICHE", uguali)): + rb, ne, pr = n_eff(M) + print(f" [controllo positivo] {nome:<24} rbar {rb:+.3f} N_eff {ne:5.2f} PR {pr:5.2f}") + print(" -> il rilevatore distingue i due estremi: il suo «poco» vale.\n") + + # -- TP01: 24 ancore, LIVELLI -------------------------------------------------------------- + S = {h: tp01_at(h) for h in range(24)} + M = pd.DataFrame(S).dropna() + shs = np.array([sh(M[h].values) for h in range(24)]) + rb, ne, pr = n_eff(M) + print(f" TP01 — 24 ancore, LIVELLI: Sharpe da {shs.min():.2f} a {shs.max():.2f}" + f" (canonico {shs[0]:.2f})") + print(f" corr media fra ancore {rb:.3f} -> N_eff {ne:.2f} / participation ratio {pr:.2f}") + + # -- TP01: 24 ancore, DIFFERENZA APPAIATA (la forma che l'ondata usa di piu') ------------- + D, dsh = {}, [] + for h in range(24): + a, b = tp01_at(h, True), tp01_at(h, False) + ix = a.index.intersection(b.index) + D[h] = b.reindex(ix) - a.reindex(ix) + dsh.append(sh(b.reindex(ix).values) - sh(a.reindex(ix).values)) + dsh = np.array(dsh) + MD = pd.DataFrame(D).dropna() + rbd, ned, prd = n_eff(MD) + print(f"\n TP01 — 24 ancore, DIFFERENZA APPAIATA (§42, floor=-1 vs canonico):") + print(f" dShFULL mediana {np.median(dsh):+.3f} banda [{dsh.min():+.3f}, {dsh.max():+.3f}]" + f" >0 in {int((dsh>0).sum())}/24 (§42 pubblica -0,354 [-0,450,-0,251] 0/24)") + print(f" corr media delle serie-differenza {rbd:.3f} -> N_eff {ned:.2f} / PR {prd:.2f}") + print(" 🚨 A3 REFUTATA: nella differenza appaiata la componente comune NON si cancella" + f" ({rbd:.2f} contro {rb:.2f} dei livelli).") + + # -- la banda d'ancora NON e' un intervallo di confidenza --------------------------------- + a0, b0 = tp01_at(0, True), tp01_at(0, False) + ix = a0.index.intersection(b0.index) + A0, B0 = a0.reindex(ix).values, b0.reindex(ix).values + X = np.column_stack([A0, B0]) + bs = boot_stat(X, lambda z: sh(z[:, 1]) - sh(z[:, 0]), n=3000, seed=7) + ic = (float(np.percentile(bs, 2.5)), float(np.percentile(bs, 97.5))) + amp_mm = float(dsh.max() - dsh.min()) + amp_pp = float(np.percentile(dsh, 90) - np.percentile(dsh, 10)) + amp_ic = ic[1] - ic[0] + print(f"\n 🚨 Per la STESSA grandezza, all'ancora canonica:") + print(f" banda d'ANCORA min-max (quella che §42 pubblica) ampiezza {amp_mm:.3f}" + f" · p10-p90 {amp_pp:.3f}") + print(f" IC95 bootstrap a blocchi [{ic[0]:+.3f}, {ic[1]:+.3f}] ampiezza {amp_ic:.3f}" + f" -> {amp_ic/amp_mm:.1f}x la banda pubblicata ({amp_ic/amp_pp:.1f}x la p10-p90)," + f" e CONTIENE LO ZERO.") + print(" La DECISIONE di §42 (SCARTATO) regge lo stesso — poggia su iso-vol FAIL 3/3,") + print(" maxDD +8,80pp, selection-on-holdout e fee-zero, non su questo Δ. Ma il NUMERO") + print(" e la sua banda sono pubblicati con una precisione che il campione non ha.") + + # -- XS01: 10 fasi ------------------------------------------------------------------------- + P = {p: xs01_phase(p) for p in range(10)} + MP = pd.DataFrame(P).dropna() + shp = np.array([sh(MP[p].values) for p in range(10)]) + rbp, nep, prp = n_eff(MP) + bsp = boot_stat(MP[0].values, sh, n=3000, seed=3) + icp = (float(np.percentile(bsp, 2.5)), float(np.percentile(bsp, 97.5))) + amp_f = float(np.percentile(shp, 90) - np.percentile(shp, 10)) + anni = len(MP) / 365.25 + print(f"\n XS01 — 10 fasi del ciclo H=10 (universo 19, {anni:.2f} anni):") + print(f" Sharpe per fase da {shp.min():.2f} a {shp.max():.2f}, mediana {np.median(shp):.2f}") + print(f" corr media {rbp:.3f} -> N_eff {nep:.2f} / PR {prp:.2f}") + print(f" banda di FASE p10-p90 ampiezza {amp_f:.3f} contro IC95 bootstrap " + f"[{icp[0]:+.2f}, {icp[1]:+.2f}] ampiezza {icp[1]-icp[0]:.3f} = " + f"{(icp[1]-icp[0])/amp_f:.1f}x") + print(f" MDE sul LIVELLO (1,96/sqrt(anni)) = {1.96/np.sqrt(anni):.2f}" + f" contro Sharpe mediano {np.median(shp):.2f}") + print("\n 📌 CONSEGUENZA, e vale per TUTTA l'ondata: «positivo in N/N ancore o fasi» e' una") + print(" dichiarazione di ROBUSTEZZA ALLA SCELTA DELL'ANCORA, non un test. Il suo") + print(" equivalente in osservazioni indipendenti e' ~2 (ancore) e ~1,3 (fasi), quindi") + print(" NON vale 2^-24 ne' 2^-10; e la «banda» che l'accompagna e' ~4x piu' stretta") + print(" dell'intervallo di confidenza della stessa grandezza.") + print(" Dove l'ondata attacca un p-value a un conteggio del genere (§26, «test dei segni") + print(" 12/12, p=0,0005», su finestre 30g SOVRAPPOSTE e per giunta SELEZIONATE come le") + print(" peggiori) quel p-value non e' calcolabile cosi'.") + + +# ============================================================================================= +# 4) MDE — quali SCARTATI sono non-risultati +# ============================================================================================= +def sezione_mde() -> None: + sez("4) MDE — quanti SCARTATI dell'ondata sono «non misurabile» invece che «misurato e no»") + + # finestra reale della catena opzioni, misurata (non presa dal registro) + p = ROOT / "data" / "raw" / "cb_chain" / "bite_archive.parquet" + giorni_sup = np.nan + if p.exists(): + d = pd.read_parquet(p, columns=["asset", "ts", "exp"]) + g = d.groupby(["asset", d["ts"].dt.floor("D")])["exp"].nunique() + gg = min(int((g.loc[a] >= 2).sum()) for a in ("BTC", "ETH")) + prop = len(list((ROOT / "data" / "raw" / "cb_chain").glob("2026-*.parquet"))) + giorni_sup = gg + prop + print(f" [misurato] catena opzioni: archivio {d['ts'].dt.floor('D').nunique()} giorni di") + print(f" calendario, ma solo {gg} con >=2 scadenze; + {prop} di raccolta") + print(f" propria = {giorni_sup} giorni di SUPERFICIE (il registro dice 74-75 ✅)") + + ANNI_SUP = giorni_sup / 365.25 if np.isfinite(giorni_sup) else 75 / 365.25 + R = [ + # (filone, oggetto misurato, anni utili, effetto dichiarato, unita', tipo) + ("§4 DEALER-GAMMA", "Sharpe di cella", 90 / 365.25, 4.98, "Sharpe", "maxk:5.12"), + ("§5 SKEW Q1", "Sharpe direzionale", ANNI_SUP, 0.0, "Sharpe", "livello"), + ("§5 SKEW Q2", "gate de-risk su TP01", 5 / 365.25, 0.0, "Sharpe", "livello"), + ("§6 OI-PIN", "Sharpe max-pain", 75 / 365.25, 5.59, "Sharpe", "maxk:6.79"), + ("§7 TERM-STRUCTURE", "Sharpe di cella", 74 / 365.25, 0.14, "Sharpe", "livello"), + ("§10 FLOW-SQUEEZE", "Sharpe di cella", 114 / 365.25, 3.25, "Sharpe", "maxk:5.40"), + ("§11 VRP-QUOTE-VERE", "Sharpe (T=10)", 10 * 7 / 365.25, 0.0, "Sharpe", "rifiutato"), + ("§13 BASIS-CALENDAR", "premio a termine", 7.4, 0.0085, "ret/anno", "livello"), + ("§27 SURFACE-RV", "censimento 1,24M quote", ANNI_SUP, 0.0, "$", "censimento"), + ("§43 DATA-UNUSED", "Sharpe hold-out", 1.40, 0.951, "Sharpe", "maxk:1.009"), + ("§48 VOLVOL", "dSharpe vs TP01", 985 / 365.25, 0.034, "Sharpe", "livello"), + ("§21 XS01-OOS (LEAD)", "Sharpe fuori campione", 3.0, 1.12, "Sharpe", "livello"), + ("§50 PREVDAY (LEAD)", "Sharpe forward", 63 / 365.25, 2.04, "Sharpe", "livello"), + ("§25 BIN-FREQ (LEAD)", "dSharpe di libro", 7.4, 0.112, "Sharpe", "appaiato"), + ("§8 VOL-SIZE (LEAD)", "dSharpe di libro", 7.4, 0.070, "Sharpe", "appaiato"), + ("§42 TP01-LS", "dSharpe di libro", 7.4, -0.354, "Sharpe", "appaiato"), + ("§41 TP01-SIZE", "dSharpe di libro", 7.4, 0.0006, "Sharpe", "appaiato"), + ("§49 TP01-TWIN", "dSharpe hold-out", 7.4, -0.127, "Sharpe", "appaiato"), + ] + print(f"\n MDE sui LIVELLI = 1,96/sqrt(anni). Sugli APPAIATI l'MDE del livello NON si applica") + print(f" (la componente comune si cancella): li' serve l'IC bootstrap della DIFFERENZA,") + print(f" che per §42 ho misurato = {0.862:.3f} di ampiezza (sez. 3).\n") + print(f" {'filone':<24}{'anni':>7}{'MDE liv.':>10}{'effetto':>10}{'|eff|/MDE':>11} lettura") + for nome, _obj, anni, eff, unita, tipo in R: + mde = 1.96 / np.sqrt(anni) + if tipo == "censimento": + lett = "N di quote, non anni: potenza OK" + rap = float("nan") + elif tipo == "appaiato": + lett = "APPAIATO: l'MDE del livello non si applica" + rap = float("nan") + elif tipo == "rifiutato": + lett = "titolo 32,58 NON riportato dall'ondata: ✅ disciplina" + rap = float("nan") + elif tipo.startswith("maxk:"): + nmax = float(tipo.split(":")[1]) + rap = abs(eff) / mde + lett = (f"max-of-k: il rumore da' {nmax:.2f} -> il candidato " + f"{'STA SOTTO' if abs(eff) < nmax else 'sta sopra'} il proprio null") + elif unita != "Sharpe": + lett = "il registro dichiara SE 1,79pp -> non misurabile" + rap = float("nan") + else: + rap = abs(eff) / mde + lett = ("sotto il proprio MDE = NON MISURABILE" if rap < 1 + else "sopra l'MDE" if rap < 2 else "misurato con margine") + rs = " — " if not np.isfinite(rap) else f"{rap:9.2f}" + print(f" {nome:<24}{anni:7.2f}{mde:10.2f}{eff:10.3f}{rs} {lett}") + + print("\n 📌 A4 CONFERMATA IN LARGA PARTE: l'ondata dichiara il proprio MDE in §6, §10, §43,") + print(" §48, §56, e in §7 spiega perche' NON ha girato il deflated-Sharpe. E' disciplina") + print(" reale, e va detto.") + print(" ⚠️ MA restano tre etichette piu' forti della misura:") + print(" (a) §48 chiude un FILONE («quinto lato del DVOL chiuso») su una branca dove") + print(" dichiara esso stesso un fattore 44 sotto l'MDE. La parziale negativa e il") + print(" controllo non-causale hanno potenza; «filone chiuso» no.") + print(" (b) §4/§5/§7/§11 vivono su ~75-90 giorni: MDE 4,2-4,4 di Sharpe. Ogni loro") + print(" SCARTATO che poggia su un confronto di Sharpe e' un NON-RISULTATO su") + print(" quell'asse (le loro parti valide sono i lead-lag, i placebo e i censimenti,") + print(" non gli Sharpe).") + print(" (c) §21 XS01-OOS, che e' il PILASTRO del canale funded, ha effetto +1,12 contro") + print(f" un MDE di {1.96/np.sqrt(3.0):.2f} sui suoi 3 anni: il titolo sta esattamente al") + print(" proprio limite di rilevabilita', e la «banda 10 fasi» non e' un IC (sez. 3).") + print(" Nota di equita': §21 gira ANCHE un null di permutazione a fee zero (p=0,013),") + print(" che ha piu' potenza del t-stat sullo Sharpe -> il problema non e' «nessuna") + print(" evidenza», e' che il LIVELLO +1,12 viene poi propagato fino a P(>=50/g) e") + print(" al muro come se fosse noto a due cifre.") + + +# ============================================================================================= +# 4bis) CONTRADDIZIONI FRA SEZIONI — 55 sezioni scritte da agenti diversi +# ============================================================================================= +def sezione_contraddizioni() -> None: + sez("4bis) CONTRADDIZIONI FRA SEZIONI DELLO STESSO REGISTRO") + + print(" (A) 🚨 MISURATA — «il libro 75/25» ha TRE baseline diverse sotto LO STESSO NOME.") + out = {} + for lens in ("hourly", "canonical"): + s = CC.book_series(lens=lens, alloc=600.0) + r = s.values + eq = np.cumprod(1 + r) + dd = float((1 - eq / np.maximum.accumulate(eq)).max()) + out[lens] = (sh(r), r.mean() * 365, dd) + print(f" lente {lens:<10} ShFULL {sh(r):.3f} drift {r.mean()*365:.2%} maxDD {dd:.2%}") + print(" terza: §8 e §25 usano 1,626 / maxDD 10,4% = la MEDIANA della banda d'ancora.") + print(f" spread fra le tre: {abs(out['canonical'][0]-out['hourly'][0]):.3f} di Sharpe e " + f"{abs(out['canonical'][2]-out['hourly'][2])*100:.1f}pp di maxDD.") + print(" Chi usa quale: §35/§36/§39/§45/§52/§53 e OGNI muro e traiettoria -> `hourly`") + print(" (1,69); §41/§42/§49/§50/§54 -> `canonical` (1,81); §8/§25 -> 1,63.") + print(" 🚨 Lo spread e' PIU' GRANDE di quasi tutti gli effetti che l'ondata misura") + print(" (BIN-FREQ +0,112 · VOL-SIZE +0,070 · PREVDAY de-luckato +0,100): un Δ letto") + print(" accanto al baseline sbagliato cambia di piu' del Δ stesso. Ogni filone lo") + print(" dichiara nel proprio script; il REGISTRO no, e CLAUDE.md nemmeno.") + + print("\n (B) 🚨 IL «SOFFITTO DIREZIONALE» — §35 lo abbassa a ~1,15 (era ~1,31) per il") + print(" funding. Ma nella STESSA ondata, sulla STESSA coppia BTC/ETH e su lente netta") + print(" fee: §23 pubblica il miglior lookback COSTANTE a ShFULL 1,639 e §54 la cella") + print(" PREVDAY scelta al buio a 1,621 (che PASSA il deflated-Sharpe a N=336).") + print(" Le due cose non stanno insieme senza una DEFINIZIONE di «soffitto»:") + print(" «massimo osservabile» (>=1,64), «massimo che sopravvive a un DSR» (>=1,62),") + print(" o «Sharpe di TP01» (1,15 col funding)? Il registro usa la terza e la cita come") + print(" se fosse la prima.") + print(" ⚠️ E la cosa ha una conseguenza operativa: §12 costruisce un argomento") + print(" ARITMETICO («uno screen largo su BTC/ETH non puo' passare il proprio DSR") + print(" perche' il massimo dal rumore, 1,572, sta SOPRA il soffitto ~1,3»). Con") + print(" soffitto 1,62-1,64 quell'argomento si inverte — ed e' la SECONDA refutazione") + print(" dello stesso argomento, indipendente da quella di §24 (che lo ribalta sulla") + print(" partizione della griglia). Un argomento refutato due volte per due strade") + print(" diverse non va ereditato come regola.") + + print("\n (C) §50 chiama PREVDAY «il piu' forte misurato, robust_oos TRUE, ADDS» e §54") + print(" misura che la cella CHE GIRA fallisce il deflated-Sharpe (0,905) mentre quella") + print(" scelta onestamente lo passa (0,993) ma e' un'ALTRA strategia (4h, long-flat).") + print(" Non e' un errore di nessuno dei due — §50 valuta la cella congelata, §54 la") + print(" famiglia — ma le due righe finiscono nella stessa memoria e la prima e' quella") + print(" che suona come una promozione.") + + print("\n (D) ✅ CONTRADDIZIONI CHE L'ONDATA HA GIA' RISOLTO DA SOLA, e vanno a suo credito:") + print(" §45 «lo spot non si liquida» -> §52 lo falsifica (in_cross_collateral_pool);") + print(" §39 «non esiste una linea USDC datata» -> falsificato lo stesso giorno;") + print(" §8 «overlay non causale» -> RITIRATA dallo scettico in giornata;") + print(" §24 «dealer_net_gamma e' il GEX invertito» -> ritirata leggendo il sorgente.") + print(" Quattro auto-correzioni in 24 ore: e' il dato piu' sano dell'ondata.") + + +# ============================================================================================= +# 5) ARITMETICA: cosa torna, cosa no +# ============================================================================================= +def sezione_aritmetica(L: dict) -> None: + sez("5) ARITMETICA PUBBLICATA — verifica diretta") + + print(" (a) tetti di leva (§33/§38/§53) — aritmetica pura sul config che GIRA") + import json + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + frac, dsl = cfg["max_notional_per_asset_frac"], cfg["disaster_sl_pct"] + n_asset = 2 + peggior_giorno_k1 = 0.1432 # §33, bound strutturale + print(f" config letto: frac={frac} · disaster_sl_pct={dsl} · n_asset={n_asset}") + print(f" leva lorda massima a k=1: {n_asset*frac:.2f}x (test di guardia: <=1x) " + f"{'OK' if n_asset*frac <= 1.0 else 'ROTTO'}") + for lab, tetto, pub in (("disaster-SL <= 50% equity", 0.50 / (n_asset * frac * dsl), 1.67), + ("peggior giorno <= 50%", 0.50 / peggior_giorno_k1, 3.49), + ("peggior giorno <= 20% (scettico)", 0.20 / peggior_giorno_k1, 1.40)): + print(f" {lab:<34} k <= {tetto:5.2f} pubblicato {pub:.2f} " + f"{'✅' if abs(tetto-pub) < 0.02 else '⚠️ DIVERGE'}") + print(f" 1,50x -> peggior giorno {1.50*peggior_giorno_k1:.2%} (>20% -> §53 lo boccia) ✅") + print(f" 1,25x -> costo di un disaster-SL {1.25*n_asset*frac*dsl:.1%} (§38 dice 37,5%) ✅") + + print("\n (b) muri = prelievo / perpetua — ✅ verificato in sez. 1 su 4 lenti su 4.") + print(f" prelievo netto = {CC.TARGET_EUR_DAY:.0f} EUR/g x 365 x {CC.EURUSD} = " + f"${PREL_NET:,.0f}/anno ; lordizzato ${PREL_LORDO:,.0f}") + + print("\n (c) 🚨 «EUR X/mese» NON e' al mese: `accumula` versa ogni 30 GIORNI.") + for anni in (10, 15, 20): + nd = anni * 365 + k = len([t for t in range(nd) if t % 30 == 0 and t > 0]) + print(f" {anni:>2} anni: {k} versamenti invece di {anni*12} " + f"({k/(anni*12)-1:+.2%} di denaro versato)") + print(" -> asintoticamente 365/30 = 12,17 versamenti l'anno invece di 12 (+1,39%).") + print(" Effetto piccolo (0,8-1,4%) ma SISTEMATICO e a FAVORE del piano, presente in") + print(" ogni tabella pubblicata dal 25/07 in poi. Va detto perche' l'ondata dichiara") + print(" la risoluzione del proprio Monte Carlo a 0,7% accanto agli stessi numeri.") + + print("\n (d) 🚨 «a 10 anni si versano $229k... i bonifici fanno il 73%» — il contatore") + print(" `versato` di r0807_piano_netto.accumula e' uno SCALARE che non si ferma al") + print(" traguardo (riga 157-167): conta i versamenti dell'INTERO orizzonte anche sui") + print(" percorsi che sono gia' arrivati. Misura del vero versato-fino-all'arrivo:") + l3 = L["L3 CONGIUNTA (§36)"] + rng = np.random.default_rng(SEED) + paths = CC._boot_paths(l3["r"], N_TRAJ, 10 * 365, BLOCK, rng) + dep_m = 1733.0 + dep = dep_m * CC.EURUSD + cap = np.full(N_TRAJ, 635.0) + versato = np.full(N_TRAJ, 635.0) + colpito = np.full(N_TRAJ, -1, int) + carry = np.zeros((N_TRAJ, TX.CARRY_ANNI)) + anno0 = cap.copy() + flusso = np.zeros(N_TRAJ) + for t in range(10 * 365): + cap *= (1.0 + paths[:, t]) + if t % 30 == 0 and t > 0: + vivo = colpito < 0 + cap += dep + flusso += dep + versato += dep * vivo # <- si ferma al traguardo + nuovi = (colpito < 0) & (cap >= l3["muro"]) + colpito[nuovi] = t + if (t + 1) % 365 == 0: + PN.passo_fiscale(cap, anno0, flusso, carry, l3["aliq"], l3["patr"]) + anno0 = cap.copy() + flusso[:] = 0.0 + arr = colpito >= 0 + tot_pieno = 635.0 + 121 * dep + tot_vero = float(versato[arr].mean()) if arr.any() else float("nan") + print(f" EUR{dep_m:.0f}/m, 10 anni: P(traguardo) {arr.mean():.1%}") + print(f" versato come lo conta l'ondata ${tot_pieno:,.0f} (=$229k pubblicato ✅)") + print(f" versato FINO all'arrivo, medio ${tot_vero:,.0f}" + f" -> i bonifici fanno il {tot_vero/l3['muro']:.0%}, non il 73%") + + print("\n (e) composizione ancora x funding: `deluck` e' applicato DOPO la sottrazione del") + print(" funding, quindi sconta dell'11% anche il funding (2,16% -> 1,92%).") + # ⚠️ ERRORE MIO CATTURATO IN SESSIONE: la prima stesura faceva `deluck(L0["r"])`, ma quella + # serie e' GIA' de-luckata -> 0,89^2 e un finto +31,5% sul muro. Qui si parte dal GREZZO. + A = L["_raw_nofund"] + alt = deluck(A) - 0.021597 / 365.0 + p_alt = perpetua(alt, l3["aliq"], l3["patr"]) + print(f" ordine dell'ondata : drift {l3['r'].mean()*365:.2%} -> muro ${l3['muro']:,.0f}") + print(f" ordine alternativo : drift {alt.mean()*365:.2%} -> muro ${PREL_NET/p_alt:,.0f}" + f" ({PREL_NET/p_alt/l3['muro']-1:+.1%})") + print(" Scelta di modello, non errore — ma la sua taglia (+4,0% sul muro) e' SEI VOLTE") + print(" la risoluzione MC (0,7%) che l'ondata dichiara accanto allo stesso numero.") + print(" ⚠️ ERRORE MIO catturato prima di pubblicare: la prima stesura de-luckava una") + print(" serie GIA' de-luckata (0,89^2) e stampava +31,5%. Il controllo che l'ha preso") + print(" e' stato guardare il DRIFT stampato accanto al muro, non il muro.") + + +# ============================================================================================= +# 6) IL LIBRO MASTRO — la risposta al mandato, in una sola unita' +# ============================================================================================= +def sezione_ledger(L: dict) -> None: + sez("6) IL MANDATO ERA «ARRIVARE AI 50 GIORNALIERI VELOCEMENTE». Tutto in una sola unita'.") + E = 635.0 + l3 = L["L3 CONGIUNTA (§36)"] + + def eur_g(drift_pp: float) -> float: + """pp di drift annuo -> EUR/giorno di reddito atteso a $635, al netto del fisco.""" + return drift_pp / 100.0 * E * (1 - CC.TAX_RATE) / 365.0 / CC.EURUSD + + righe = [ + ("VERSARE EUR250 -> EUR500 al mese", "leva del PIANO", None, "P(20a) 14% -> 85%", "SI'"), + ("gradino di leva 1,25x (§33/§38/§53)", "politica", 21.39 - 17.11, + "+1,8a (muro congelato) / +3,0a (mobile)", "NO — knob inesistente + GATE SCALA-01"), + ("lead SPOT su TP01 (§39/§45/§52)", "strumento", 1.55, + "muro -10,9% a $600, 0% a $272k", "NO — domanda fiscale aperta"), + ("PREVDAY al 15% (§50/§54)", "sleeve nuovo", 1.31, + "dShFULL +0,100 de-luckato", "NO — GATE PREVDAY-01 al 2027-06-21"), + ("XS01 su conto funded (§21/§29/§37)", "canale", None, + "P(>=50/g) 2,6% [1,5-4,7] in 36 mesi", "gate PASS 3/3, spesa = 91% del conto"), + ("BIN-FREQ su SKH01 (§25)", "size", None, "EUR0,0113/giorno", "NO — 3 gate su 4 FAIL"), + ("VOL-SIZE su SKH01 (§8)", "size", None, "+0,070 Sh, gate 22/12 gia' fallito", "NO"), + ("MAKER (§22)", "esecuzione", 0.466, "tetto $2,96/anno, IC contiene lo 0", "NO"), + ("--- COSTI SCOPERTI (segno opposto) ---", "", None, "", ""), + ("funding dei perpetual (§35)", "costo", -2.16, "muro +17,5%", "gia' subito"), + ("i 4 monitor rotti (§31)", "integrita'", None, "STATARB Sharpe +1,96 -> -2,02", "da riparare"), + ("il notifier senza retry (NOTIFIER)", "sicurezza", None, "~7% degli allarmi persi", "da riparare"), + ] + print(f" {'cosa':<40}{'tipo':<12}{'EUR/giorno a $635':>19} effetto / stato") + for nome, tipo, dpp, eff, stato in righe: + if nome.startswith("---"): + print(f"\n {nome}") + continue + cell = f"{eur_g(dpp):+.4f}" if dpp is not None else " — " + print(f" {nome:<40}{tipo:<12}{cell:>19} {eff}") + if stato: + print(f" {'':<40}{'':<12}{'':>19} eseguibile oggi: {stato}") + + print("\n 📌 IL CONFRONTO, con la stessa macchina e gli stessi percorsi:") + rng = np.random.default_rng(SEED) + paths = CC._boot_paths(l3["r"], N_TRAJ, 25 * 365, BLOCK, rng) + for dep in (0, 250, 500): + a = PN.accumula(paths, dep, l3["muro"], l3["aliq"], l3["patr"], start=635.0) + y = np.where(a["colpito"] >= 0, a["colpito"] / 365.0, np.inf) + med = np.median(y) + print(f" EUR{dep:>4}/mese -> mediana {'>25a' if not np.isfinite(med) else f'{med:.1f}a'}" + f" P(20a) {float((y<=20).mean()):5.1%}") + tot = sum(eur_g(x) for x in (1.55, 1.31, 0.466)) + print(f"\n Somma di TUTTI i lead positivi dell'ondata, se fossero tutti autorizzati e") + print(f" tutti additivi (non lo sono, e nessuno lo e' oggi): {tot:+.3f} EUR/giorno.") + print(f" Il gradino di leva da solo, se il knob esistesse: {eur_g(21.39-17.11):+.3f} EUR/g.") + print(f" Un versamento di EUR500/mese e' {500/30.44:+.2f} EUR/giorno di CASSA — e le due") + print(" grandezze NON sono la stessa cosa (una e' rendimento, l'altra e' capitale che") + print(" entra). Il confronto onesto e' quello sopra, sulla stessa macchina e sugli") + print(" STESSI percorsi: EUR250 -> EUR500 al mese porta P(20a) dal 14% all'85% e la") + print(" mediana da 23,4 a 17,3 anni; la somma di tutti i lead dell'ondata, se fossero") + print(" autorizzati e additivi, vale +0,036 EUR/giorno su un conto da $635, cioe' meno") + print(" dell'incertezza Monte Carlo della riga sopra.") + print("\n 🚨 RISPOSTA AL MANDATO: NO. L'ondata non ha prodotto NIENTE che avvicini i 50 EUR/g.") + print(" Ha prodotto (i) un COSTO gia' in essere che porta P(20a) a EUR250/mese dal 49%") + print(" al 14% (il funding, §35/§36), (ii) quattro monitor rotti che avrebbero fatto") + print(" decidere un gate") + print(" al contrario, (iii) un punto singolo di guasto nella rete di sicurezza,") + print(" (iv) tre lead reali ma tutti bloccati da qualcosa che NON e' la ricerca.") + print(" Il suo valore e' DIFENSIVO, ed e' reale: ha impedito decisioni sbagliate.") + print(" Ma la domanda che era stata posta ha risposta negativa, e la 56ª ondata") + print(" avra' la stessa risposta della 55ª.") + + +# ============================================================================================= +# 7) LE TRE COSE CHE, SE SCRITTE COME LE HO CAPITE IO, SARANNO SBAGLIATE +# ============================================================================================= +def sezione_tre() -> None: + sez("7) LE TRE COSE CHE, SE SCRITTE IN CLAUDE.md COME SONO STATE CAPITE, SARANNO SBAGLIATE") + print(""" + (1) «IL MURO E' $313k» — e' una MEDIANA, non un numero, e la sua banda non e' mai stata + pubblicata. + CLAUDE.md sta per ereditare $313k, «EUR250/mese -> P(20a) 14%» e «EUR1.733/mese per + 10 anni» come se il drift fosse noto. Misurato in sez. 2: propagando SOLO la SE del + drift (5,15%/anno, block bootstrap — e §53 la misura 5,09 su un'altra lente) il muro + va da $204k (+1 SE) a $707k (-1 SE), a p10 supera $1,1M e a -2 SE il traguardo NON + ESISTE a nessun capitale. La riga «EUR500/mese -> P(20a) 85%» diventa ~0% a -1 SE. + Il meccanismo e' strutturale: il muro e' `prelievo / perpetua` e la perpetua si + ANNULLA molto prima del drift -> e' un 1/x su una quantita' che va a zero, quindi + l'errore e' asimmetrico verso l'alto. + 🚨 E l'ondata ha pubblicato la risoluzione MONTE CARLO del muro (0,7%) accanto a un + numero la cui incertezza di PARAMETRO e' cento volte piu' grande: ha misurato la + precisione del simulatore e mai quella del suo input. + DA SCRIVERE: «$313k e' la MEDIANA; a -1 SE di drift e' ~$707k e a -2 SE il traguardo + non esiste. Il piano si dimensiona sul VERSAMENTO, che e' certo, non sul muro.» + + (2) «POSITIVO IN 24/24 ANCORE» / «10/10 FASI» — non sono 24 ne' 10 osservazioni. + Misurato in sez. 3 su serie vere e con controllo positivo (24 serie indipendenti -> + N_eff 24,5; 24 identiche -> 1,00): le 24 ancore di TP01 hanno corr media 0,63 -> + N_eff ~1,6-2,1; le 10 fasi di XS01 corr 0,79 -> N_eff ~1,2-1,5. E la componente + comune NON si cancella nella DIFFERENZA APPAIATA (corr 0,59, N_eff 1,6-2,2): era la + mia attesa A3 ed e' REFUTATA. + 🚨 Conseguenza diretta: la «banda» che accompagna ogni Δ dell'ondata e' la banda + d'ANCORA, e per la stessa grandezza (§42) l'IC95 bootstrap e' 4,3x piu' largo + ([-0,81, +0,05] contro [-0,45, -0,25]) e CONTIENE LO ZERO. Chi legge fra sei mesi + leggera' quella banda come un intervallo di confidenza — perche' ne ha la forma. + E dove l'ondata ci attacca un p-value (§26: «12/12, p=0,0005», su finestre 30g + sovrapposte e selezionate come le peggiori) il p-value non e' calcolabile cosi'. + DA SCRIVERE: «N/N ancore = robustezza alla scelta dell'ancora, ~2 osservazioni + indipendenti; la banda d'ancora NON e' un IC ed e' ~4x piu' stretta.» + ⚠️ Cio' che questo NON dice: che i verdetti cadano. §42 resta SCARTATO (iso-vol FAIL + 3/3, maxDD +8,8pp, selection-on-holdout, edge lordo assente). Cade la PRECISIONE dei + numeri, non la direzione delle decisioni. + + (3) «IL GRADINO DI LEVA VALE 14,7a -> 12,9-11,6a, ~EUR300/mese» — meta' di quella frase + e' gia' morta nell'ondata stessa. + §53 ha misurato che il **1,50x NON sopravvive** (peggior giorno strutturale 21,48%, + sopra la soglia 20%, e al 90% del tetto G6) e che il **k massimo difendibile e' + 1,40**. Il «~EUR300/mese» e' del 1,50x. Il 1,25x vale **+EUR144/mese misurati + direttamente**, non EUR164, e **+1,8 anni a muro CONGELATO / +3,0 a muro MOBILE** — + una differenza di 1,2 anni fra due convenzioni che §33 non dichiarava. + ✅ L'aritmetica dei tetti l'ho verificata sul config che gira (sez. 5a): 1,67 / 3,49 / + 1,40 tornano tutti e tre al centesimo. Non e' quella il problema: e' che CLAUDE.md + porta ancora la riga «1,25-1,50x» come se fossero due opzioni vive. + DA SCRIVERE: «gradino 1,25x; k max difendibile 1,40; il 1,50x e' BOCCIATO. Guadagno + +1,8a (muro congelato) / +3,0a (mobile) — la convenzione va sempre detta.» + + ⚠️ BONUS 1 — «lo SPOT vale +1,55-1,66%/anno». Vale quello SOLO a $600. §52 ha camminato + entrambi i libri: il differenziale spot-perp passa da +0,72 bps a $600 a +25,66 bps a + $272k -> **-0,11%/anno alla taglia del muro**. Il lead si annulla esattamente dove + pretendeva di servire. Numero onesto: «+1,38-1,58%/anno a $600, ~0 a $272k», e sotto, + la domanda fiscale (~0,83%/anno = piu' di meta' del lead) che nessun backtest chiude. + + ⚠️ BONUS 2 — «PREVDAY e' il piu' forte misurato, +0,192 FULL / +0,363 HOLD». De-luckato + (§54) e' +0,100 / +0,246; la cella che GIRA fallisce il deflated-Sharpe (0,905) mentre + quella scelta al buio lo passa (0,993) ma e' un'ALTRA strategia (4h, long-flat) che + butta via la gamba short per cui PREVDAY fu promosso; e il «forward +2,04» e' su lente + ORARIA (su lente giornaliera +1,56, 63 giorni, MDE 4,7). + + ⚠️ BONUS 3 — «il libro 75/25 fa ShFULL 1,8». Ne fa TRE (sez. 4bis-A, misurate): 1,68 + (`hourly`, la lente di tutti i muri e le traiettorie), 1,80 (`canonical`, la lente di + §41/§42/§49/§50/§54) e 1,63 (mediana d'ancora, §8/§25). Lo spread, 0,12 di Sharpe e + 1,7pp di maxDD, e' PIU' GRANDE di quasi tutti gli effetti che l'ondata misura: un Δ + accostato al baseline sbagliato cambia piu' del Δ. Ogni numero di libro va scritto con + la sua lente attaccata. + + ⚠️ BONUS 4 — «il soffitto direzionale e' ~1,15». Nella stessa ondata §23 pubblica 1,639 e + §54 pubblica 1,621, stessa coppia, stessa lente netta-fee. «Soffitto» sta per tre cose + diverse e il registro usa la piu' bassa citandola come la piu' alta — e su quella + poggia l'argomento aritmetico di §12, che si inverte (seconda refutazione indipendente + dopo quella di §24). +""") + + +def main() -> None: + print("=" * 102) + print(" r0823 — CRITICO DI CHIUSURA dell'ondata 2026-08-22/23") + print("=" * 102) + print(__doc__.split("=====")[2].strip()[:1800]) + if not CACHE.exists(): + print(f"\n ⚠️ manca la cache {CACHE}; rigenerala con " + f"`uv run python scripts/research/r0822d_piano_vero.py --rebuild`") + sys.exit(2) + B = pd.read_parquet(CACHE) + L = sezione_repliche(B) + sezione_muro(L) + sezione_ancore() + sezione_mde() + sezione_contraddizioni() + sezione_aritmetica(L) + sezione_ledger(L) + sezione_tre() + print(f"\n[{time.time() - T0:.0f}s] fine. Sola lettura: nessun file di produzione toccato.") + + +if __name__ == "__main__": + main() From 00f55fd753a5d63e09b7c07989a929cfa1fe0197 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 04:09:26 +0000 Subject: [PATCH 104/114] =?UTF-8?q?registro=20=C2=A757:=20il=20critico=20d?= =?UTF-8?q?i=20chiusura=20=E2=80=94=20repliche=206/6,=20ma=20il=20muro=20e?= =?UTF-8?q?'=20una=20mediana=20con=20banda=20esplosiva=20e=20=C2=ABN/N=20a?= =?UTF-8?q?ncore=C2=BB=20vale=20~2=20osservazioni;=20somma=20di=20tutti=20?= =?UTF-8?q?i=20lead=20=3D=20+0,036=20EUR/giorno?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 150 ++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 328a072..bb7e90e 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -722,6 +722,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 54 | PREVDAY-GATE | il piu' forte candidato misurato sta in monitor da giugno senza gate: quanto vale al gate onesto? | **GATE PREVDAY-01 SCRITTO — 8/10 condizioni oggi**, e le due che mancano sono la stessa cosa: 🚨 **la cella che GIRA non e' quella che la selezione onesta sceglie** — e la selezione compra il libro **LONG-FLAT**, cioe' butta via la gamba per cui PREVDAY fu promosso | | 55 | COSTO-CAPITALE | i muri sono calcolati a costo d'esecuzione COSTANTE, ma il costo cresce col capitale: dove sta il muro vero? | **NON MORDE** — il muro si sposta di **+0,6%** (dentro la risoluzione MC), la saturazione sta **30x sopra** ($9,5-10M), e 📌 **alla taglia di oggi il costo mancante ha segno NEGATIVO** | | 56 | XS-AMPIEZZA | su un universo povero (11 gambe), si recupera ampiezza EFFETTIVA senza aggiungere gambe? | **NO — e la caduta NON era ampiezza (solo il 14%)**: e' **selettivita'** (recuperabile ma sotto l'MDE) **piu' un IC che su quelle 11 gambe vale ZERO**. 🚨 E `k=5` **non e' invariante di scala** | +| 57 | CRITICO | l'ondata ha prodotto qualcosa che avvicina i 50 EUR/giorno — e i suoi numeri di testa reggono a una verifica indipendente? | **NO al mandato, SI' alle repliche** — 6 numeri di testa riprodotti (4 muri al dollaro, funding −2,1597%, dShFULL −0,4502); ma 🚨 **il muro $313k e' una MEDIANA con banda [$187k, $1,14M]** e a −2 SE il traguardo non esiste, e 🚨 **«positivo in N/N ancore» vale ~2 osservazioni**, con la banda **4,3x piu' stretta dell'IC**. La somma di TUTTI i lead dell'ondata vale **+0,036 EUR/giorno** | | — | **NOTIFIER** (audit di fatto) | 🚨 **PUNTO SINGOLO DI GUASTO nella rete di sicurezza** | il `send()` di Telegram e' **un solo tentativo da 10s, senza retry, con l'esito MAI registrato** — e lo stato `alerted=True` viene scritto **prima** dell'invio: un 🚨 di `venue_watch` perso e' perso **per l'intero episodio**. Tasso di fallimento **misurato 6,9%** (2/29, IC95 [1,9%, 22,0%]) | | 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 | @@ -3521,3 +3522,152 @@ costruito male, non perche' la macchina e' rotta.* **VERDETTO: `IL COSTO ENDOGENO NON MORDE — IL MURO SI SPOSTA DI +0,6% (DENTRO LA RISOLUZIONE MC) E LA SATURAZIONE STA 30x SOPRA`**. **Libro, pesi, cron, config INVARIATI.** + +--- + +## 57 — CRITICO DI CHIUSURA (attacco all'ondata, coordinatore incluso) + +`scripts/research/r0823_critico_chiusura.py` (**120 s**, `--fast` 45 s, sola lettura **provata**). +Attese a priori **scritte nel docstring prima di misurare**: A1/A4/A5/A6 **confermate**, **A2 e A3 +REFUTATE**. Ogni rilevatore ha il suo controllo positivo (24 serie indipendenti → N_eff 24,5 · 24 +serie identiche → 1,00). + +### Repliche, prima di ogni accusa — tutte riuscite + +✅ Libro 75/25 path live drift **19,23%/17,07%**, Sharpe **1,692/1,513**, funding +**−2,1597%/anno**; i quattro muri **al dollaro** ($278.033 / $264.373 / $327.617 / **$313.143**); +✅ **il muro E' esattamente `prelievo/perpetua`** su 4 lenti su 4; §42 dShFULL(floor=−1) = **−0,4502**; +TP01 canonico **1,305**; XS01 fase 0 == sleeve a `max|diff| = 0,0`. Funding **ri-misurato da +implementazione indipendente**: TP01 **2,138%/anno**, condizionale **1,93x BTC / 2,58x ETH** contro +il pubblicato 1,86-2,55x, prodotto ingenuo che sottostima di **2,18x**. *Il nucleo quantitativo +dell'ondata regge.* + +### 🚨 (1) Il muro non ha mai avuto una banda, e la sua e' esplosiva + +SE del drift L3 misurata dal critico: **5,151%/anno** (block bootstrap 20g; §53 misura 5,09 su +un'altra lente → ✅ concorda). Propagando **solo** quella: + +| drift | perpetua | muro | x pubbl. | +|---|---|---|---| +| +1 SE 20,34% | 9,73% | **$204.517** | 0,65x | +| p90 21,79% | 10,66% | $186.623 | 0,60x | +| **PUNTO 15,19%** | **6,35%** | **$313.143** | 1,00x | +| p10 8,59% | 1,74% | **$1.141.172** | 3,64x | +| −1 SE 10,04% | 2,80% | $709.753 | 2,27x | +| **−2 SE 4,89%** | **0,00%** | **IRRAGGIUNGIBILE** | — | + +«€500/mese → P(20a) 85%» diventa **~0% a −1 SE**; «€250/mese → 14%» diventa **96% a +1 SE**. +📌 **Meccanismo: il muro e' `prelievo/perpetua` e la perpetua si annulla molto prima del drift → e' +un 1/x su una quantita' che va a zero, quindi l'errore e' asimmetrico verso l'alto.** +🚨 **L'ondata ha pubblicato la risoluzione MONTE CARLO del muro (0,7%) accanto a un numero la cui +incertezza di PARAMETRO e' cento volte piu' grande: ha misurato la precisione del simulatore e mai +quella del suo input.** ⚠️ E la SE del block-bootstrap e' un **limite inferiore** (variabilita' +dentro gli stessi 7,4 anni, non il cambio di regime). L'attesa A2 del critico ([$230k, $430k]) era +ottimista di **~2x sull'ampiezza**. + +### 🚨 (2) «Positivo in N/N ancore» non e' N osservazioni + +Misurato su serie vere: **24 ancore TP01** corr media **0,631** → **N_eff 1,55** / PR 2,09; +**10 fasi XS01** corr **0,790** → **N_eff 1,23** / PR 1,51. ❌ **A3 REFUTATA: nella differenza +appaiata la componente comune NON si cancella** (corr 0,593 contro 0,631 dei livelli → N_eff +1,64-2,24). Per la **stessa grandezza** (§42, ancora canonica): banda d'ancora pubblicata **0,199** +di ampiezza, **IC95 bootstrap [−0,814, +0,048] = 0,862, cioe' 4,3x piu' largo E CONTIENE LO ZERO** +(7,2x contro la p10-p90). Su XS01 lo stesso rapporto e' **3,8x** — due misure indipendenti, stesso +fattore ~4. +⚠️ **Cio' che questo NON dice: che i verdetti cadano.** §42 resta SCARTATO su iso-vol FAIL 3/3, +maxDD +8,80pp, selection-on-holdout, edge lordo assente. **Cade la precisione dei numeri, non la +direzione delle decisioni.** +🚨 Ma dove l'ondata attacca un **p-value** a un conteggio del genere — §26, *«test dei segni 12/12, +p=0,0005»*, su finestre 30g **sovrapposte** e per giunta **selezionate come le peggiori** — quel +p-value non e' calcolabile cosi'. + +### 🚨 (3) Tre baseline diverse sotto lo stesso nome «libro 75/25» + +`hourly` **ShFULL 1,683 / maxDD 11,22%** (§35/§36/§39/§45/§52/§53 e **ogni muro e traiettoria**) · +`canonical` **1,800 / 9,56%** (§41/§42/§49/§50/§54) · **1,626 / 10,4%** = mediana d'ancora (§8/§25). +**Spread 0,12 di Sharpe e 1,7pp di maxDD, piu' grande di quasi tutti gli effetti che l'ondata +misura** (BIN-FREQ +0,112 · VOL-SIZE +0,070 · PREVDAY de-luckato +0,100): **un Δ accostato al +baseline sbagliato cambia piu' del Δ.** Ogni filone lo dichiara nel proprio script; il registro no. + +### 🚨 (4) Il «soffitto direzionale ~1,15» e' contraddetto dalla stessa ondata + +§23 pubblica il miglior lookback costante a **1,639** e §54 la cella PREVDAY al buio a **1,621** +(che **passa** il DSR a N=336), stessa coppia, stessa lente netta-fee. «Soffitto» sta per tre cose +diverse e il registro usa la piu' bassa citandola come la piu' alta. **Conseguenza: l'argomento +aritmetico di §12** (*«noise max 1,572 > soffitto 1,3 ⇒ uno screen largo non puo' passare il proprio +DSR»*) **si inverte — ed e' la SECONDA refutazione indipendente** dopo quella di §24 (che lo ribalta +sulla partizione). *Un argomento refutato due volte per due strade diverse non va ereditato come +regola.* + +### ⚠️ (5) Due difetti di contabilita' nel piano, piccoli e sistematici — entrambi a favore + +(a) **«€X/mese» versa ogni 30 GIORNI**: 121/182/243 versamenti invece di 120/180/240 +(+0,83/+1,11/+1,25%; asintotico +1,39%) — presente in ogni tabella dal 25/07. +(b) 🚨 **il contatore `versato` di `accumula` e' uno SCALARE che non si ferma al traguardo** → il +*«a 10 anni si versano $229k, i bonifici fanno il 73%»* e' l'intero orizzonte: il versato **fino +all'arrivo** e' **$190.265, cioe' il 61%, non il 73%** (P(traguardo) 91%). *Stessa famiglia del bug +`paid` catturato il 25/07 — la lezione era codificata e il difetto era altrove.* +(c) `deluck` e' applicato **dopo** la sottrazione del funding, quindi sconta dell'11% anche il +funding: **+4,0% sul muro**, **sei volte** la risoluzione MC dichiarata accanto allo stesso numero. +Scelta di modello, non errore — ma va detta. + +### ✅ (6) Aritmetica dei tetti di leva: verificata sul config che GIRA, 3/3 al centesimo + +`frac=0,5 · disaster_sl_pct=0,3 · n_asset=2` → disaster-SL ≤50% ⇒ **k ≤ 1,67** · peggior giorno +≤50% ⇒ **k ≤ 3,49** · ≤20% (scettico) ⇒ **k ≤ 1,40**; **1,50x → peggior giorno 21,48% = BOCCIATO**; +1,25x → costo disaster-SL 37,5%. Leva lorda a k=1 = **1,00x** (il test di guardia passa). + +### 📌 (7) MDE — quanti SCARTATI sono «non misurabile» invece di «misurato e no» + +Finestra della catena misurata dal critico: archivio **90 giorni** di calendario ma solo **52 con +≥2 scadenze**, + **25** di raccolta propria = **77 giorni di superficie** (il registro dice 74-75 +✅ da tre agenti). **MDE sul livello a 77 giorni = 4,3 di Sharpe** ⇒ ogni SCARTATO di +**§4/§5/§7/§11** che poggia su un confronto di Sharpe e' un **non-risultato su quell'asse** (le loro +parti valide sono lead-lag, placebo e censimenti, non gli Sharpe). +✅ **A4 CONFERMATA in larga parte**: l'ondata dichiara il proprio MDE in §6, §10, §43, §48, §56 e in +§7 spiega *perche'* non ha girato il DSR — disciplina reale. +⚠️ **Tre etichette restano piu' forti della misura:** (a) **§48 chiude un FILONE** («quinto lato del +DVOL chiuso») su una branca dove dichiara esso stesso **un fattore 44 sotto l'MDE**; (b) i quattro +filoni-catena sopra; (c) 🚨 **§21 XS01-OOS, il PILASTRO del canale funded, ha effetto +1,12 contro +un MDE di 1,13** sui suoi 3 anni — il titolo sta **esattamente al proprio limite di rilevabilita'**, +e la «banda 10 fasi» non e' un IC. *Nota di equita': §21 gira anche un null di permutazione a fee +zero (p=0,013), che ha piu' potenza del t-stat; il problema non e' «nessuna evidenza», e' che il +livello +1,12 viene propagato fino a P(≥50/g) e al muro come se fosse noto a due cifre.* + +### ✅ (8) A credito dell'ondata — quattro auto-correzioni in 24 ore + +§45 «lo spot non si liquida» → falsificato da §52; §39 «non esiste una linea USDC datata» → +falsificato lo stesso giorno; §8 «overlay non causale» → **regola ritirata** dallo scettico; §24 +«dealer_net_gamma e' il GEX invertito» → ritirata leggendo il sorgente. *E' il dato piu' sano +dell'ondata.* + +⚠️ **Errore del critico, catturato prima di pubblicare, e della famiglia che stava criticando:** la +prima stesura del punto (5c) de-luckava una serie **gia' de-luckata** (0,89²) e stampava un finto +**+31,5%** sul muro. **Il controllo che l'ha preso e' stato guardare il DRIFT stampato accanto al +muro, non il muro.** + +### 📌 La risposta al mandato — tutto in una sola unita' + +€/giorno a $635: + +| cosa | €/g | eseguibile oggi | +|---|---|---| +| gradino di leva 1,25x | **+0,046** | **NO** — il knob non esiste + GATE SCALA-01 | +| lead SPOT su TP01 | +0,017 | **NO** — domanda fiscale aperta | +| PREVDAY al 15% (de-luckato) | +0,014 | **NO** — GATE PREVDAY-01 al 2027-06-21 | +| BIN-FREQ | +0,011 | **NO** — 3 gate su 4 FAIL | +| MAKER | +0,005 | **NO** — IC contiene lo zero | +| **funding (costo scoperto)** | **−0,023** | **gia' subito** | + +**Somma di TUTTI i lead positivi, se fossero autorizzati e additivi (non lo sono, e nessuno lo e' +oggi): +0,036 €/giorno.** Sullo stesso motore e sugli **stessi percorsi**: **€250 → €500 al mese +porta P(20a) dal 14% all'85% e la mediana da 23,4 a 17,3 anni.** + +🚨 **VERDETTO: `L'ONDATA NON HA PRODOTTO NIENTE CHE AVVICINI I 50 EUR/GIORNO.`** Ha prodotto (i) un +**costo gia' in essere** che porta P(20a) a €250/mese dal 49% al **14%** (il funding), (ii) **quattro +monitor rotti** che avrebbero fatto decidere un gate al contrario, (iii) un **punto singolo di +guasto** nella rete di sicurezza, (iv) **tre lead reali tutti bloccati da qualcosa che non e' la +ricerca**. **Il suo valore e' DIFENSIVO ed e' reale: ha impedito decisioni sbagliate.** Ma la +domanda posta ha risposta negativa, e la 56ª ondata avra' la stessa risposta della 55ª. +❌ **Nulla di sepolto sotto uno «SCARTATO» merita di essere riesumato**: il candidato piu' forte non +promosso (PREVDAY) vale +0,014 €/g de-luckato e gira la cella sbagliata. From 80ca982f64f76a09b42a2596d337cea99d7eb766 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 04:10:34 +0000 Subject: [PATCH 105/114] =?UTF-8?q?CLAUDE.md:=20correzioni=20del=20critico?= =?UTF-8?q?=20=E2=80=94=20il=20muro=20e'=20una=20mediana=20con=20banda=20e?= =?UTF-8?q?splosiva,=20=C2=ABN/N=20ancore=C2=BB=20vale=20~2=20osservazioni?= =?UTF-8?q?,=20il=201,50x=20e'=20bocciato,=20il=20soffitto=20~1,3=20e'=20r?= =?UTF-8?q?efutato=20due=20volte,=20i=20bonifici=20fanno=20il=2061%=20non?= =?UTF-8?q?=20il=2073%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 48 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5bdf1d2..7122201 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -101,6 +101,21 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis ammettere uno sleeve. Quando serve la banda di un AGGREGATO si campiona lo spazio congiunto; gli audit uno-sleeve-alla-volta (02/07, 03/07) restano validi per giudicare *quello* sleeve. Diari `2026-07-02-anchor-audit-xs01-skh01.md` (originale) e `2026-07-26-loo-deluck.md` (misura). + 🚨 **«POSITIVO IN N/N ANCORE» NON E' N OSSERVAZIONI — misurato 2026-08-23, e tocca OGNI bullet + di questo file che cita una banda d'ancora.** Correlazione media fra le **24 ancore di TP01 + 0,631 → N_eff 1,55**; fra le **10 fasi di XS01 0,790 → N_eff 1,23**. **E non si salva sulla + differenza appaiata:** la componente comune **non si cancella** (corr 0,593 → N_eff 1,64-2,24) — + era l'attesa a priori del critico ed e' stata **REFUTATA**. Sulla stessa grandezza, la **banda + d'ancora e' 4,3x piu' stretta dell'IC95 bootstrap, che CONTIENE LO ZERO** (su XS01 il rapporto e' + 3,8x: due misure indipendenti, stesso fattore ~4). + ⚠️ **Cio' che NON significa: che i verdetti cadano.** Le decisioni prese con l'audit d'ancora + poggiano anche su iso-vol, maxDD, selection-on-holdout ed edge lordo. **Cade la PRECISIONE dei + numeri, non la DIREZIONE delle decisioni.** + **REGOLE:** (a) «N/N ancore» si cita come **robustezza alla SCELTA dell'ancora**, ~2 osservazioni + indipendenti — **non come N prove**; (b) **la banda d'ancora NON e' un intervallo di confidenza** + e non va usata al posto di uno; (c) **non attaccare un p-value a un conteggio di ancore o di + finestre sovrapposte** (caso preso in fallo: un «test dei segni 12/12, p=0,0005» su finestre 30g + sovrapposte *e* selezionate come le peggiori). - **SKH01-V2-DD "Skyhook" — DIVERSIFICATORE quasi-ortogonale (research)** — `src/strategies/skyhook.SKH01_V2_DD`, sleeve `src/portfolio/sleeves._skyhook_returns`. Sistema dual-TF (segnale 690m / exec 230m) regime (BuzVola/BuzVolume tipo-Chande) AND pattern (Donchian breakout), NON trend-follower, L/S. Vincitrice @@ -2287,6 +2302,12 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis spread). (b) **Il deflated-Sharpe va calcolato DI SCREEN, non solo di famiglia**: su 168 trial il massimo atteso dal **puro rumore** e' **Sharpe 1,572**, sopra il soffitto direzionale (~1,3) -> **uno screen largo su BTC/ETH direzionale non puo' passare il proprio gate, per aritmetica.** + 🚨 **QUESTO ARGOMENTO E' REFUTATO DUE VOLTE, per due strade indipendenti, e NON va ereditato:** + (i) la stessa griglia dichiarata come 7 famiglie da 24 da' **1,15**, sotto il soffitto — il + verdetto si ribalta sulla **partizione** (ondata 2, §24); (ii) **il soffitto non e' ~1,3**: la + stessa ondata pubblica un lookback costante a **1,639** e la cella PREVDAY al buio a **1,621**, + stessa coppia e stessa lente netta-fee (§23, §54). «Soffitto» sta per tre cose diverse e il + registro usava la piu' bassa citandola come la piu' alta. (c) **Un placebo si controlla per BILANCIAMENTO DEL SEGNO prima di usarlo** (un null che concorda col segnale nel 14% dei casi non e' un null, e' la strategia invertita: fabbrica celle "significative"). (d) **Il null del de-levering e' DEGENERE quando la variante e' una @@ -2548,7 +2569,9 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis → al gate si confronterebbe **un benchmark giusto con un numero di strategia sbagliato**, e il margine sembrerebbe **+5,83 invece di +1,87**. 🚨 **(4) WORST-DAY — la riserva sulla leva e' mal tarata di 2-4 ORDINI DI GRANDEZZA, ma il knob di - scala NON ESISTE.** Il gradino di leva vale **14,7a → 12,9a a 1,25x** e **→ 11,6a a 1,50x** al + scala NON ESISTE.** 🚨 **AGGIORNATO 23/08: il 1,50x e' BOCCIATO** (§53: peggior giorno + strutturale **21,48% > 20%**) e il **k massimo difendibile e' 1,40** — di questa riga sopravvive + **solo la colonna 1,25x**. Il gradino di leva vale **14,7a → 12,9a a 1,25x** e **→ 11,6a a 1,50x** al capitale-rendita (**+€164/mese** e **+€282/mese** di versamenti equivalenti 🚨 **il primo e' +€144 misurato direttamente — vedi il ritocco del 23/08 sotto**) ed era bloccato da un parametro **scelto a mano** ("un giorno @@ -2626,7 +2649,8 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis prototipo isolato `r0822e_scale_proto.py`, zero scritture, isolamento verificato a runtime), con un NODO IRRISOLTO.** Registro §38. Replica a k=1,00 **6/6** contro WORST-DAY. ⚠️ **CORREZIONE A UN NUMERO CHE AVEVO SCRITTO IO:** *"1,25-1,50x vale 14,7 → 11,6 anni, ~€300/mese"* - **fondeva due gradini** — a **1,25x** l'equivalente e' **+€164/mese**, il €282 e' del **1,50x**. + **fondeva due gradini** — a **1,25x** l'equivalente e' **+€164/mese**, il €282 e' del **1,50x** + 🚨 **che il 23/08 e' stato BOCCIATO: la meta' cara della frase non esiste piu'.** 🚨 **RITOCCATO 2026-08-23 (§53), e il ritocco riguarda la CONVENZIONE piu' che il numero:** il "+1,8 anni / +€164" e' la lettura a **muro CONGELATO** (si de-leva al traguardo) e §33 **non dichiarava quale delle due letture stesse usando**. A **muro MOBILE** lo stesso gradino vale @@ -2714,6 +2738,18 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis | L2 +FUNDING (22/08) | 15,19% | 9,06% | $328k | **63%** | | 🚨 **L3 CONGIUNTA** | **15,19%** | **6,35%** | **$313k** | **14%** | | L3b congiunta, funding "strumento vero" −1,39%/a | 15,87% | 6,72% | $296k | **26%** | + 🚨 **IL MURO E' UNA MEDIANA, E LA SUA BANDA E' ESPLOSIVA (misurato dal critico, 23/08).** + La SE del drift L3 e' **5,151%/anno** (block bootstrap 20g; replicata su un'altra lente a 5,09): + **+1 SE → $204.517 · p90 → $186.623 · PUNTO $313.143 · p10 → $1.141.172 · −1 SE → $709.753 · + −2 SE → il traguardo NON ESISTE a nessun capitale.** «€500/mese → P(20a) 85%» diventa **~0% a + −1 SE**; «€250/mese → 14%» diventa **96% a +1 SE**. 📌 **Meccanismo: il muro e' + `prelievo/perpetua` e la perpetua si annulla molto prima del drift → e' un 1/x su una quantita' + che va a zero, quindi l'errore e' asimmetrico verso l'alto.** 🚨 **E il progetto ha pubblicato la + risoluzione MONTE CARLO del muro (0,7%) accanto a un numero la cui incertezza di PARAMETRO e' + cento volte piu' grande: ha misurato la precisione del simulatore e mai quella del suo input.** + ⚠️ La SE del block-bootstrap e' un **limite inferiore** (variabilita' dentro gli stessi 7,4 anni, + non il cambio di regime). **REGOLA: il piano si dimensiona sul VERSAMENTO, che e' certo, non sul + muro.** 🚨 **LE DUE CORREZIONI NON SI COMPENSANO: SI SOMMANO — e nel VERSAMENTO necessario il congiunto e' +10% PEGGIORE della loro somma.** Il meccanismo "meno drift → meno plusvalenza → meno imposta" **esiste** (+9,97% di capitale recuperato; il fisco toglie il 50,9% a funding OFF e il 46,0% a @@ -2737,6 +2773,12 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis incondizionata; 22,1 condizionata all'arrivo), **P(20a) 14%** [banda "strumento vero": 22,2 anni, 26%]. Per **€50/giorno in 10 anni** servono **€1.733/mese** a P=90%, totale versato **$229.189**.* **A 10 anni si versano $229k per arrivare a ~$313k: il rendimento fa il 27%, i bonifici il 73%** — + ⚠️ **CORREZIONE 23/08: il 73% e' l'INTERO ORIZZONTE, non il versato fino all'arrivo.** Il + contatore `versato` di `accumula` e' uno **scalare che non si ferma al traguardo** (stessa + famiglia del bug `paid` catturato il 25/07, in un altro punto): il versato **fino all'arrivo** e' + **$190.265 = il 61%**, con P(traguardo) 91%. E **«€X/mese» versa ogni 30 GIORNI** → + **121/182/243** versamenti invece di 120/180/240 (+0,83/+1,11/+1,25%), in ogni tabella dal 25/07. + Entrambi i difetti vanno **a favore** del piano. — forma piu' netta della lezione gia' scritta: *a orizzonte corto non fai lavorare la strategia, COMPRI il capitale coi bonifici.* ⚠️ **Cosa NON e' incluso, di proposito:** il **rischio di venue**, che e' **rovina e non costo** @@ -2752,7 +2794,7 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis | gamba | criterio | esito | |---|---|---| | **(a) LISTINO** | ≥10 delle 13 gambe shortabili | ✅ **PASS 13/13** — misurato sul **venue** (Bybit `instruments-info`, 833 strumenti), non sul sito | - | **(b) ANCORA SKH01** | delta appaiato > +0,05 sui 23 offset | ✅ **PASS 23/23**, +0,116, 230/230 celle congiunte | + | **(b) ANCORA SKH01** | delta appaiato > +0,05 sui 23 offset | ✅ **PASS 23/23**, +0,116, 230/230 celle congiunte — ⚠️ ma «23/23» vale **~2 osservazioni**, vedi sotto | | **(c) CAPITALE** | *riformulata, vedi sotto* | ✅ **PASS 2/2** | 📌 **La (c) e' stata RIFORMULATA perche' era una constatazione, non un criterio** — e la sua forma e' **imposta dal costo della misura**: l'EV costa una corsa di script (**zero dollari**), la From ab4590c3263c54cd07043c0de7f10beaeaf478ed Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 04:12:06 +0000 Subject: [PATCH 106/114] =?UTF-8?q?CLAUDE.md:=20bullet=20della=20quinta=20?= =?UTF-8?q?ondata=20(=C2=A746-57)=20=E2=80=94=200=20candidati=20su=2057=20?= =?UTF-8?q?filoni,=20PREVDAY=20girava=20senza=20gate,=201,50x=20bocciato,?= =?UTF-8?q?=20e=20la=20somma=20di=20tutti=20i=20lead=20vale=20+0,036=20EUR?= =?UTF-8?q?/giorno?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 7122201..9b81b20 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2723,6 +2723,104 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis eseguirla**, o si discute per mesi di un knob che non esiste; (f) **quando due stimatori di coda non concordano si cita la banda**, non quello che conviene. +- 🌊 **QUINTA ONDATA 2026-08-23 (12 filoni + critico di chiusura, `research/wave-0822`) — 0 + candidati su **57 filoni cumulativi**, 1 candidato scoperto GIA' IN PRODUZIONE SENZA GATE, il + gradino di leva delimitato, e un critico che ha falsificato **due sue attese e tre miei numeri**.** + Registro `docs/research/RESULTS-0822.md` §46-57; 17 script `scripts/research/r0823_*.py` (15.443 + righe, compilano tutti, ognuno gira da solo). **Libro, pesi, cron, config INVARIATI.** + 🚨 **(1) IL RISULTATO PIU' IMPORTANTE NON E' UNA STRATEGIA NUOVA: E' CHE LA PIU' FORTE GIA' + MISURATA GIRAVA SENZA GATE.** §50 cercava il miglior TERZO sleeve eseguibile a $635: sette + candidati, **sette muri diversi, e nessuno dei sette e' una soglia di capitale**. Il piu' forte — + **PREVDAY** — sta in monitor **da giugno senza gate pre-registrato e senza deflated-Sharpe**. + §54 gliene ha scritto uno (`GATE PREVDAY-01`, **8/10 condizioni oggi**) e le due che mancano sono + **la stessa cosa**: 🚨 **la cella che GIRA non e' quella che la selezione onesta sceglie** (e la + cella viva **fallisce il DSR**, 0,905), e la selezione onesta compra il libro **LONG-FLAT** — + cioe' butta via proprio la gamba per cui PREVDAY fu promosso. ✅ La serie e' **intatta, 1512 barre + su 1512** (e' il monitor sano, quello che il difetto `advance()` non tocca) ⚠️ **ma la finestra e' + 0,172 anni → MDE 4,72 di Sharpe: il +2,11 forward non e' distinguibile da zero su 26 ENTRY. + Qualunque cosa il gate decida, non puo' appoggiarsi al numero forward.** + 🚨 **(2) IL GRADINO DI LEVA E' DELIMITATO: 1,25x REGGE, 1,50x E' BOCCIATO, k max difendibile + 1,40.** §53 lo attacca 6 volte col funding dentro: **5/6 falliscono**; il 1,50x cade sul peggior + giorno strutturale (**21,48% > 20%**). L'incrinatura e' il ramo **`eq_fallback`**, dove **la scala + non apre il buco: lo MOLTIPLICA**. ✅ Aritmetica dei tetti verificata sul config che GIRA, 3/3 al + centesimo: disaster-SL ⇒ **k ≤ 1,67** · peggior giorno ⇒ k ≤ 3,49 · scettico ⇒ **k ≤ 1,40**. + ❌ **(3) TRE MECCANISMI DI PROTEZIONE REFUTATI, E DUE PER LA PREMESSA.** **TAIL-HEDGE (§46):** la + premessa cade **prima del prezzo** — il maxDD **SALE in 162/162 celle**, a ogni lente di f (f=1,00 + regalato compreso), perche' il beta del libro al sottostante e' **+0,076**: *non si assicura un + libro che nei crash e' gia' quasi piatto*. **TP01-TWIN (§49):** un ensemble di definizioni di + trend protegge **PEGGIO** — 0/24 ancore nei 4 anni recenti, **3,2x peggio nel 2022**, −0,127 di + Sharpe hold-out; e i meccanismi **non erano ridondanti** (corr di posizione 0,61): la ridondanza + non era il problema. **ANCHOR-ENSEMBLE (§47):** eseguibile, ma su TP01 il drift **non puo'** + cambiare per **algebra** (`lordo_ens == media(lordi)`, residuo 1,4e-17); il segnale c'e' su + **SKH01** (+0,312 di Sharpe mediano, maxDD 23,3%→16,6%) ma dentro il libro **non si distingue da + zero**. + 📌 **(4) IL LEAD DELLO SPOT MUORE DOVE SERVIREBBE.** §52 lo attacca sei volte: **5/6 falliscono**, + e cio' che morde non e' nei numeri — e' **nella taglia a cui vengono spesi**. **Regge a $600, si + annulla a $272k**: sparisce **esattamente al muro che pretende di spostare.** + ✅ **(5) IL COSTO D'ESECUZIONE ENDOGENO NON MORDE (§55):** muro **+0,6%** (dentro la risoluzione + MC), saturazione a **$9,5-10M = 30x sopra**, e il fatto trovato per strada — **alla taglia di oggi + il costo mancante ha SEGNO NEGATIVO**: il drift a $635 e' **13 bps/anno piu' basso** che a $5.000 + per il pavimento `min_order` $5, **~2x** quello che lo slippage costa da $313k a $1M, in direzione + opposta. *Il conto piu' preciso non muove il muro: sposta il segno dell'errore, e a questa taglia + lo sposta a favore.* + ❌ **(6) DUE CHIUSURE DI FAMIGLIA.** **VOLVOL (§48):** la volatilita' del DVOL e' davvero una + variabile **NUOVA** (ortogonalita' PASS, max |corr| 0,396) ma **guarda INDIETRO** — picco a lag + **−5/−10**, parziale con la vol futura **NEGATIVA**: *un secondo momento non e' automaticamente + informazione anticipata*. ⚠️ Ma il filone lo **chiude** su una branca dove dichiara esso stesso + **un fattore 44 sotto l'MDE**: e' un'etichetta piu' forte della misura. + **XS-AMPIEZZA (§56):** su 11 gambe **la caduta NON era ampiezza — solo il 14%**: e' **selettivita'** + (recuperabile, ma sotto l'MDE) **piu' un IC che su quelle gambe vale ZERO**. 🚨 E **`k=5` non e' + invariante di scala**: il numero di gambe per lato di XS01 e' tarato su 19 gambe, non e' una + proprieta' del meccanismo. + 📌 **(7) SOLO 2 FONTI SU 10 MERITANO DI ESSERE RACCOLTE (§51):** la catena **USDC** (**+18% di + chiamate, non +90%**) e una **campagna a termine** di book depth. Il criterio di + **ricostruibilita'** ne uccide 4 da solo — *se lo puoi ricostruire domani, non raccoglierlo oggi*. + ⚠️ **(8) ERRORI MIEI, corretti dentro l'ondata** (tutti numeri gia' pubblicati, due in questo + file): (a) **"la convinzione di TP01 sta spesso a 1/3 o 2/3"** — **falso**, `tsmom_blend` media + tre `np.sign()` → direzione in {0, 1/3, 1} dopo il clip: **il bucket 2/3 NON ESISTE**, la famiglia + ha **UN** grado di liberta'; trovato **indipendentemente da due agenti**. (b) **catena USDC "+90% + di chiamate"** → **+18%** (i 587 erano il conteggio grezzo, senza il filtro OI≥100 del + collettore; al venue: **113 USDC contro 649 inverse**). (c) **"le opzioni BTC sono fuori a $3.000 + per il lotto"** → quello e' il **collaterale per VENDERE**; comprare costa il **premio** + (`min_trade_amount` 0,01) e **il muro vero e' il TICK da 5,00 USDC** (una put che vale 0,82 non e' + quotabile sotto 5 = pavimento di prezzo **6x**). (d) **"il gradino vale +1,8a / +€164"** — era la + lettura a **muro FERMO**, convenzione **mai dichiarata**; a muro mobile fa **+2,98a**, e le due + distano **1,2 anni**. (e) **"la coda muove k\* di 1-2 unita'"** → **4,6**. + 🚨 **(9) IL CRITICO DI CHIUSURA (§57) — repliche 6/6, ma tre numeri di testa vanno riscritti.** + ✅ Ha **riprodotto con macchineria propria** i quattro muri **al dollaro**, il funding + (**−2,1597%/anno**, e da implementazione indipendente TP01 2,138% con rapporto condizionale + 1,93x/2,58x), il dShFULL di §42 (**−0,4502**) e ✅ **che il muro E' esattamente + `prelievo/perpetua`** su 4 lenti su 4 — *il nucleo quantitativo dell'ondata regge*. Le tre cose + che cambiano — **la banda del muro**, **«N/N ancore» = ~2 osservazioni** e **il 1,50x bocciato** — + sono gia' scritte nei bullet a cui appartengono, sopra. In piu': **tre baseline diverse sotto lo + stesso nome «libro 75/25»** (`hourly` 1,683/11,22% = la lente di **ogni muro e traiettoria** · + `canonical` 1,800/9,56% · mediana d'ancora 1,626/10,4%) — **spread 0,12 di Sharpe e 1,7pp di + maxDD, piu' grande di quasi tutti gli effetti che l'ondata misura**, quindi *un Δ accostato al + baseline sbagliato cambia piu' del Δ*: **ogni numero va scritto con la sua lente attaccata**. + ⚠️ E il **pilastro del canale funded** (§21, XS01 fuori campione) ha effetto **+1,12 contro un MDE + di 1,13**: sta *esattamente* al proprio limite di rilevabilita', e viene propagato fino a + P(≥50 €/g) come se fosse noto a due cifre. *(Equita': §21 gira anche un null di permutazione a fee + zero, p=0,013, che ha piu' potenza del t-stat — il problema non e' «nessuna evidenza», e' la + precisione attribuita al livello.)* + ✅ **A credito dell'ondata: quattro auto-correzioni in 24 ore** (§45 falsificato da §52; §39 + falsificato lo stesso giorno; la regola «overlay non causale» ritirata; «`dealer_net_gamma` e' il + GEX invertito» ritirata leggendo il sorgente). **E' il dato piu' sano dell'ondata.** + 🚨 **(10) LA RISPOSTA AL MANDATO, in una sola unita' (€/giorno a $635):** gradino di leva 1,25x + **+0,046** · lead SPOT +0,017 · PREVDAY al 15% de-luckato +0,014 · BIN-FREQ +0,011 · MAKER +0,005 + · **funding (costo scoperto) −0,023**. **Somma di TUTTI i lead positivi, se fossero autorizzati e + additivi (non lo sono, e nessuno e' eseguibile oggi): +0,036 €/giorno.** Sullo stesso motore e + sugli **stessi percorsi**: **€250 → €500 al mese porta P(20a) dal 14% all'85% e la mediana da 23,4 + a 17,3 anni.** + **VERDETTO DELL'ONDATA: `NIENTE CHE AVVICINI I 50 €/GIORNO.`** Ha prodotto (i) un **costo gia' in + essere** che porta P(20a) a €250/mese dal 49% al **14%** (il funding), (ii) **quattro monitor + rotti** che avrebbero fatto decidere un gate al contrario, (iii) un **punto singolo di guasto** + nella rete di sicurezza (il trasporto degli allarmi), (iv) **tre lead reali tutti bloccati da + qualcosa che non e' la ricerca**. **Il suo valore e' DIFENSIVO ed e' reale: ha impedito decisioni + sbagliate.** ❌ E **nulla di sepolto sotto uno «SCARTATO» merita di essere riesumato**: il + candidato piu' forte non promosso vale **+0,014 €/g** de-luckato e gira la cella sbagliata. + 📌 **La riga che chiude cinque ondate e 57 filoni: la ricerca ha smesso di essere il vincolo + binding il 26/07, e ora cinque ondate lo hanno CONFERMATO invece che ribaltarlo. I vincoli binding + restano due: il capitale che entra e il conto che non sparisce.** - 🚨 **IL PIANO AL NETTO DI TUTTO — la tabella congiunta (2026-08-22, `r0822d_piano_vero.py`).** **QUESTA SOSTITUISCE OGNI TABELLA DI TRAIETTORIA PUBBLICATA SOPRA.** Le due correzioni misurate al piano — **fisco d'accumulo** (07/08) e **funding** (22/08) — erano state applicate **separatamente From 273a43ad6a580eb7e479aebcd2d69a3585256eca Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 04:17:12 +0000 Subject: [PATCH 107/114] =?UTF-8?q?diario=2023/08:=20ondate=207-8=20e=20cr?= =?UTF-8?q?itico=20di=20chiusura=20=E2=80=94=2017=20filoni,=200=20candidat?= =?UTF-8?q?i,=203=20numeri=20di=20testa=20riscritti=20e=205=20errori=20mie?= =?UTF-8?q?i?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../diary/2026-08-23-wave-chiusura-critico.md | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 docs/diary/2026-08-23-wave-chiusura-critico.md diff --git a/docs/diary/2026-08-23-wave-chiusura-critico.md b/docs/diary/2026-08-23-wave-chiusura-critico.md new file mode 100644 index 0000000..1230e1c --- /dev/null +++ b/docs/diary/2026-08-23-wave-chiusura-critico.md @@ -0,0 +1,184 @@ +# 2026-08-23 — Ondate 7-8 e il critico di chiusura: 17 filoni, 0 candidati, 3 numeri di testa riscritti + +**Branch:** `research/wave-0822` · **Registro per filone:** `docs/research/RESULTS-0822.md` §41-57 +**Libro, pesi, cron, config: INVARIATI.** `git diff main -- src/ config/ scripts/live/ scripts/cron_*.sh tests/` vuoto a ogni checkpoint. + +## Perche' esiste questa voce + +Il mandato era *"metti decine di agenti a fare analisi di nuove strategie possibili... arrivare ai 50 +giornalieri velocemente"*. Le ondate 1-3 (§1-40) avevano gia' dato 0 candidati. Queste due ne +aggiungono 17 e chiudono con un agente incaricato di **attaccare l'ondata invece di estenderla**. +La risposta al mandato e' negativa, ed e' quantificata: **la somma di tutti i lead positivi vale ++0,036 €/giorno**, contro i **+71 punti di probabilita'** che valgono €250 → €500 al mese. + +Questa voce non ripete il registro. Tiene le tre cose che il registro, per costruzione, non puo' +dire: cosa e' cambiato **nella memoria operativa**, cosa ho sbagliato **io**, e cosa resta all'operatore. + +## 1. Il risultato che nessun filone cercava + +§50 chiedeva *"qual e' il miglior terzo sleeve eseguibile a $635"* e ha trovato che **il candidato +piu' forte mai misurato dal progetto era gia' in produzione — in monitor da giugno, senza gate +pre-registrato e senza deflated-Sharpe.** Sette candidati, **sette muri diversi, e nessuno dei sette +e' una soglia di capitale**: non e' il conto piccolo a fermarli. + +§54 gli ha scritto il gate mancante. Passa **8 condizioni su 10**, e le due che mancano sono la +stessa: **la cella che gira non e' quella che la selezione onesta sceglie** (quella viva fallisce il +DSR a 0,905), e la selezione onesta compra il libro **long-flat**, cioe' butta via la gamba per cui +PREVDAY fu promosso. *Non e' un parametro da ritoccare: sono due strategie diverse.* + +Verifica mia sopra il filone: la serie e' **intatta, 1512 barre su 1512** — e' l'unico monitor che +il difetto `advance()` non tocca. Ma la finestra e' **0,172 anni** e li' l'**MDE e' 4,72 di Sharpe**: +il +2,11 forward **non e' distinguibile da zero** su 26 ingressi. **Qualunque cosa il gate decida, +non puo' appoggiarsi al numero forward.** + +## 2. Il critico, e le tre cose che vanno riscritte + +Prima di ogni accusa ha **riprodotto** con macchineria propria: i quattro muri **al dollaro**, il +funding (−2,1597%/anno), il dShFULL di §42, e che il muro **e'** esattamente `prelievo/perpetua` su +4 lenti su 4. **Il nucleo quantitativo dell'ondata regge.** Poi ha falsificato **due sue attese +scritte prima di misurare** e tre miei numeri. + +**(a) Il muro non e' $313k: e' la mediana di una banda esplosiva.** SE del drift **5,151%/anno**; ++1 SE → $204.517, −1 SE → $709.753, **−2 SE → il traguardo non esiste a nessun capitale**. +«€500/mese → P(20a) 85%» diventa **~0% a −1 SE**. Meccanismo: il muro e' un `1/x` su una quantita' +(la rendita perpetua) che **si annulla prima del drift** → errore asimmetrico verso l'alto. +Il pezzo che brucia: **per otto ondate ho stampato accanto a quel numero la risoluzione MONTE CARLO +(0,7%)** — la precisione del simulatore — **e mai quella del suo ingresso, cento volte piu' grande.** + +**(b) «Positivo in 24 ancore su 24» vale ~2 osservazioni.** Corr media fra le 24 ancore di TP01 +**0,631 → N_eff 1,55**; fra le 10 fasi di XS01 **0,790 → N_eff 1,23**. E **non si salva sulla +differenza appaiata**: la componente comune non si cancella (N_eff 1,64-2,24) — era l'attesa a +priori del critico, **refutata**. Sulla stessa grandezza la **banda d'ancora e' 4,3x piu' stretta +dell'IC95, che contiene lo zero** (su XS01: 3,8x; due misure indipendenti, stesso fattore ~4). +⚠️ **Non cadono i verdetti** — poggiano anche su iso-vol, maxDD, selection-on-holdout, edge lordo. +**Cade la precisione dei numeri, non la direzione delle decisioni.** + +**(c) Il gradino di leva a 1,50x era gia' morto.** §53 lo boccia (peggior giorno strutturale +**21,48% > 20%**) e fissa **k max difendibile 1,40**. Sopravvive solo il **1,25x**, che vale +**+€144/mese misurati direttamente** (non €164, che veniva da un'interpolazione lineare) e +**+1,8 anni a muro congelato / +3,0 a muro mobile** — *e la convenzione va sempre detta, perche' le +due letture distano piu' di quasi tutti gli effetti che questo progetto misura.* + +**In piu': tre baseline sotto lo stesso nome «libro 75/25».** `hourly` 1,683/11,22% (la lente di +**ogni muro e traiettoria**) · `canonical` 1,800/9,56% · mediana d'ancora 1,626/10,4%. **Spread 0,12 +di Sharpe, piu' grande di quasi tutti i Δ dell'ondata**: un Δ accostato al baseline sbagliato cambia +piu' del Δ. **Ogni numero va scritto con la sua lente attaccata.** + +## 3. Cosa ho sbagliato io + +Cinque errori, tutti su numeri **gia' pubblicati**, due dei quali stavano in `CLAUDE.md`: + +| # | cosa avevo scritto | cos'e' | +|---|---|---| +| a | «la convinzione di TP01 sta spesso a 1/3 o 2/3» | **il bucket 2/3 non esiste** — `tsmom_blend` media tre `np.sign()`, la famiglia ha **un** grado di liberta', non tre. Trovato da **due agenti indipendenti** | +| b | «la catena USDC costa +90% di chiamate» | **+18%** — i 587 erano il conteggio grezzo, senza il filtro OI≥100 che il collettore applica | +| c | «le opzioni BTC sono fuori a $3.000 per il lotto» | quello e' il **collaterale per VENDERE**; comprare costa il **premio**. Il muro vero e' il **tick da 5,00 USDC** | +| d | «il gradino vale +1,8a / +€164» | lettura a **muro fermo**, convenzione **mai dichiarata** | +| e | «la coda muove k\* di 1-2 unita'» | **4,6** | + +E uno catturato **prima** di pubblicarlo, che vale la pena tenere perche' e' della famiglia che +stavo criticando: avevo formulato l'MDE di PREVDAY come *«la precisione apparente viene dal numero +di mark, non di scommesse»*. **Falso**: la SE dello Sharpe annualizzato va con `1/sqrt(anni)` +**indipendentemente dalla frequenza**, e infatti barra e trade danno lo stesso 4,72. Il fatto vero +e' piu' banale: **63 giorni sono 63 giorni.** + +Il critico ha fatto lo stesso su di se': la sua prima stesura **de-luckava una serie gia' +de-luckata** (0,89²) e stampava un finto +31,5% sul muro. *Il controllo che l'ha preso e' stato +guardare il DRIFT stampato accanto al muro, non il muro.* + +## 4. Due difetti di contabilita' nel piano, entrambi a favore + +- **«€X/mese» versa ogni 30 GIORNI** → 121/182/243 versamenti invece di 120/180/240 + (+0,83/+1,11/+1,25%), in **ogni tabella dal 25/07**. +- **Il contatore `versato` e' uno scalare che non si ferma al traguardo** → il *«a 10 anni i bonifici + fanno il 73%»* e' l'intero orizzonte; **fino all'arrivo e' il 61%** ($190.265). *Stessa famiglia + del bug `paid` catturato il 25/07 — la lezione era codificata e il difetto era in un altro punto.* + +## 5. Cosa e' stato refutato, e perche' e' interessante + +Tre meccanismi di protezione, **due caduti sulla premessa prima che sul prezzo**: + +- **TAIL-HEDGE (§46):** comprare put deep-OTM fa **SALIRE** il maxDD in **162/162 celle**, a ogni + lente di f — perche' il beta del libro al sottostante e' **+0,076**. *Non si assicura un libro che + nei crash e' gia' quasi piatto.* +- **TP01-TWIN (§49):** un ensemble di definizioni di trend protegge **peggio** (0/24 ancore, 3,2x + peggio nel 2022) — e i meccanismi **non erano ridondanti** (corr di posizione 0,61): la ridondanza + non era il problema. +- **ANCHOR-ENSEMBLE (§47):** su TP01 il drift **non puo'** cambiare, per **algebra** + (`lordo_ens == media(lordi)`, residuo 1,4e-17). + +E il lead dello spot (§52) **muore dove servirebbe**: regge a $600, **si annulla a $272k** — sparisce +esattamente al muro che pretende di spostare. + +## 6. MDE: quanti «SCARTATI» sono in realta' «non misurabile» + +Il critico ha misurato la finestra della catena opzioni: **77 giorni di superficie** → **MDE 4,3 di +Sharpe**. Ogni SCARTATO di §4/§5/§7/§11 che poggia su un confronto di Sharpe e' un **non-risultato +su quell'asse** (le loro parti valide sono lead-lag, placebo e censimenti). L'ondata dichiara il +proprio MDE in §6, §10, §43, §48, §56 — **disciplina reale** — ma tre etichette restano piu' forti +della misura, e una conta: 🚨 **§21 XS01 fuori campione, il PILASTRO del canale funded, ha effetto ++1,12 contro un MDE di 1,13.** Sta *esattamente* al proprio limite di rilevabilita', e viene +propagato fino a P(≥50 €/g) come se fosse noto a due cifre. *(Equita': gira anche un null di +permutazione a fee zero, p=0,013, che ha piu' potenza del t-stat. Il problema non e' «nessuna +evidenza», e' la precisione attribuita al livello.)* + +## 7. La risposta al mandato + +| lead | €/giorno a $635 | eseguibile oggi | +|---|---|---| +| gradino di leva 1,25x | **+0,046** | no — la manopola non esiste | +| spot al posto del perpetual | +0,017 | no — domanda fiscale aperta | +| PREVDAY al 15% (de-luckato) | +0,014 | no — gira la cella sbagliata | +| binario di frequenza | +0,011 | no — 3 gate su 4 falliti | +| esecuzione passiva | +0,005 | no — l'IC contiene lo zero | +| **funding (costo scoperto)** | **−0,023** | **lo paghi gia'** | + +**Somma di tutti i lead positivi, se fossero autorizzati e additivi — non lo sono, e nessuno e' +eseguibile oggi: +0,036 €/giorno.** Sullo stesso motore e sugli stessi percorsi, **€250 → €500 al +mese porta P(20a) dal 14% all'85%** e la mediana da 23,4 a 17,3 anni. **Rapporto ~1.400 a 1.** + +🚨 **`L'ONDATA NON HA PRODOTTO NIENTE CHE AVVICINI I 50 €/GIORNO.`** Ha prodotto (i) un **costo gia' +in essere** che porta P(20a) a €250/mese dal 49% al 14% (il funding), (ii) **quattro monitor rotti** +che avrebbero fatto decidere un gate al contrario, (iii) un **punto singolo di guasto** nel trasporto +degli allarmi, (iv) **tre lead reali tutti bloccati da qualcosa che non e' la ricerca**. +**Il suo valore e' difensivo ed e' reale: ha impedito decisioni sbagliate.** E nulla di sepolto sotto +uno «SCARTATO» merita di essere riesumato. + +## 8. A credito dell'ondata + +**Quattro auto-correzioni in 24 ore**, ed e' il suo dato piu' sano: §45 «lo spot non si liquida» +falsificato da §52; §39 «non esiste una linea USDC datata» falsificato lo stesso giorno; la regola +«overlay non causale» **ritirata** dallo scettico; «`dealer_net_gamma` e' il GEX invertito» ritirata +**leggendo il sorgente** che gira su questa stessa VPS. + +## 9. Cosa resta all'operatore + +Sette decisioni, nessuna delle quali richiede altra ricerca. Brief: +`https://claude.ai/code/artifact/9ca378f7-464a-4ee1-a520-38a026a34fc3` + +1. **Riparare `advance()` e rigenerare** prima del gate STATARB del **27/09** (35 giorni) — oggi il + gate si deciderebbe **al contrario** (Sharpe +1,96 registrato contro −2,02 vero). +2. **Che fare del branch** `research/wave-0822` (106 commit, tutti in `scripts/research/` e `docs/`). +3. **Chiedere alla firm** se ha una lista ristretta propria (costo €0, blocca una spesa di $579). +4. **Chiedere al commercialista** se i derivati Deribit sono al 26% o al 33% (vale ~$22k di muro). +5. **Costruire la chiave di scala** in config, se si vuole il gradino **1,25x** (e **solo** quello). +6. **Riparare il trasporto degli allarmi** — la parte (c) cambia il comportamento, quindi e' una scelta. +7. **Decidere di `paper_prevday`**: allinearlo alla cella onesta, ritirarlo, o lasciarlo scrivendo + che la sua finestra non e' evidenza. + +## Regole nuove + +1. **Un muro calcolato come `X/y` con `y` stimato va pubblicato con la banda di `y`**, non con la + risoluzione del simulatore — e se `y` si annulla prima del suo ingresso, l'errore e' **asimmetrico** + e la banda non e' simmetrica attorno al punto. +2. **«N/N ancore» e' robustezza alla SCELTA dell'ancora, ~2 osservazioni indipendenti — non N prove.** + La banda d'ancora **non e' un intervallo di confidenza** e non va usata al posto di uno; e **non si + attacca un p-value** a un conteggio di ancore o di finestre sovrapposte. +3. **Un Δ si scrive con la sua BASELINE attaccata.** Tre lenti dello stesso libro distano 0,12 di + Sharpe, piu' della maggior parte dei Δ misurati: la lente sbagliata cambia piu' del risultato. +4. **Un argomento refutato due volte per due strade indipendenti non va ereditato come regola** + (il «soffitto ~1,3» e l'aritmetica del DSR di screen che ne discendeva). +5. **Prima di chiudere una famiglia, confrontare l'etichetta con il proprio MDE dichiarato** — §48 + chiude un filone su una branca che dichiara essa stessa un fattore 44 sotto l'MDE. +6. **Un contatore cumulativo dentro un simulatore va fermato all'evento che sta misurando**, o + risponde a una domanda diversa da quella posta (2ª occorrenza in un mese, in due punti diversi). From e48de7fcc73958519554d9b384dd5cc9767db70f Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 04:17:25 +0000 Subject: [PATCH 108/114] CLAUDE.md: rimando al diario di chiusura --- CLAUDE.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9b81b20..73caae6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2726,8 +2726,9 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis - 🌊 **QUINTA ONDATA 2026-08-23 (12 filoni + critico di chiusura, `research/wave-0822`) — 0 candidati su **57 filoni cumulativi**, 1 candidato scoperto GIA' IN PRODUZIONE SENZA GATE, il gradino di leva delimitato, e un critico che ha falsificato **due sue attese e tre miei numeri**.** - Registro `docs/research/RESULTS-0822.md` §46-57; 17 script `scripts/research/r0823_*.py` (15.443 - righe, compilano tutti, ognuno gira da solo). **Libro, pesi, cron, config INVARIATI.** + Diario `docs/diary/2026-08-23-wave-chiusura-critico.md`; registro `docs/research/RESULTS-0822.md` + §46-57; 17 script `scripts/research/r0823_*.py` (15.443 righe, compilano tutti, ognuno gira da + solo). **Libro, pesi, cron, config INVARIATI.** 🚨 **(1) IL RISULTATO PIU' IMPORTANTE NON E' UNA STRATEGIA NUOVA: E' CHE LA PIU' FORTE GIA' MISURATA GIRAVA SENZA GATE.** §50 cercava il miglior TERZO sleeve eseguibile a $635: sette candidati, **sette muri diversi, e nessuno dei sette e' una soglia di capitale**. Il piu' forte — From cdec81c5bccfaae81ffa0c54882989ab2e4c8ce7 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 04:24:59 +0000 Subject: [PATCH 109/114] =?UTF-8?q?registro:=20aperti=20=C2=A758-69=20?= =?UTF-8?q?=E2=80=94=20sei=20filoni=20sulla=20conversione=20del=20libro=20?= =?UTF-8?q?in=2050=20EUR/g,=20cinque=20sullo=20spazio-strategie=20non=20to?= =?UTF-8?q?ccato,=20uno=20scettico?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index bb7e90e..5066cee 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -723,6 +723,18 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 55 | COSTO-CAPITALE | i muri sono calcolati a costo d'esecuzione COSTANTE, ma il costo cresce col capitale: dove sta il muro vero? | **NON MORDE** — il muro si sposta di **+0,6%** (dentro la risoluzione MC), la saturazione sta **30x sopra** ($9,5-10M), e 📌 **alla taglia di oggi il costo mancante ha segno NEGATIVO** | | 56 | XS-AMPIEZZA | su un universo povero (11 gambe), si recupera ampiezza EFFETTIVA senza aggiungere gambe? | **NO — e la caduta NON era ampiezza (solo il 14%)**: e' **selettivita'** (recuperabile ma sotto l'MDE) **piu' un IC che su quelle 11 gambe vale ZERO**. 🚨 E `k=5` **non e' invariante di scala** | | 57 | CRITICO | l'ondata ha prodotto qualcosa che avvicina i 50 EUR/giorno — e i suoi numeri di testa reggono a una verifica indipendente? | **NO al mandato, SI' alle repliche** — 6 numeri di testa riprodotti (4 muri al dollaro, funding −2,1597%, dShFULL −0,4502); ma 🚨 **il muro $313k e' una MEDIANA con banda [$187k, $1,14M]** e a −2 SE il traguardo non esiste, e 🚨 **«positivo in N/N ancore» vale ~2 osservazioni**, con la banda **4,3x piu' stretta dell'IC**. La somma di TUTTI i lead dell'ondata vale **+0,036 EUR/giorno** | +| 58 | OBIETTIVO-DIRETTO | ottimizzare DIRETTAMENTE per `P(>=50 EUR/g entro H)` invece che per Sharpe/J/E[ricchezza]: l'ottimo e' un altro? | _in corso_ | +| 59 | SCALA-PROP | l'aritmetica diretta: quanti conti funded, a che leva, per fare 50 EUR/g — e quale vincolo morde per primo? | _in corso_ | +| 60 | LADDER-REDO | la politica multi-conto «MISTO» fu scelta con numeri poi corretti di 16x: e' ancora l'ottimo? | _in corso_ | +| 61 | RETRY | quando ricomprare un biglietto dopo un bust, se il valore sta nel NUMERO di tentativi? | _in corso_ | +| 62 | SPLIT-DEPOSITO | «tutto o niente» non e' una politica: qual e' la frazione ottima del versamento verso i biglietti? | _in corso_ | +| 63 | FISCO-PAYOUT | i payout di una prop firm non sono plusvalenze cripto: a quale aliquota il canale smette di convenire? | _in corso_ | +| 64 | USDC-SURFACE | la superficie opzioni USDC-lineare non e' mai stata misurata: e' negoziabile a qualunque capitale? | _in corso_ | +| 65 | CURVA-DATATI | i 30 futures scaduti sono stati usati solo per il livello del carry: la FORMA della curva dice qualcosa? | _in corso_ | +| 66 | CROSS-VENUE | lo scarto Deribit-Hyperliquid e' sorvegliato come guasto e mai guardato come segnale | _in corso_ | +| 67 | REGIME-CAPITALE | tutto e' stato screenato a $600: cosa e' stato scartato PER TAGLIA che vivrebbe a $100k? | _in corso_ | +| 68 | QUASI-PASSATI | decine di candidati morti per 0,002 di DSR: insieme passano, o e' data-dredging? | _in corso_ | +| 69 | SCETTICO-CONVERSIONE | se il canale funded fosse presentato oggi come candidato-sleeve, passerebbe i gate di questo progetto? | _in corso_ | | — | **NOTIFIER** (audit di fatto) | 🚨 **PUNTO SINGOLO DI GUASTO nella rete di sicurezza** | il `send()` di Telegram e' **un solo tentativo da 10s, senza retry, con l'esito MAI registrato** — e lo stato `alerted=True` viene scritto **prima** dell'invio: un 🚨 di `venue_watch` perso e' perso **per l'intero episodio**. Tasso di fallimento **misurato 6,9%** (2/29, IC95 [1,9%, 22,0%]) | | 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 | From 8c0b5f97ee4064d2ccb49ddd070260bc636114c7 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 12:40:30 +0000 Subject: [PATCH 110/114] =?UTF-8?q?registro:=20chiusi=20=C2=A758-69=20?= =?UTF-8?q?=E2=80=94=20dodici=20verdetti,=200=20candidati,=20e=20due=20app?= =?UTF-8?q?oggi=20tolti=20a=20decisioni=20gia'=20prese?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I dodici filoni della sesta ondata erano rimasti `_in corso_`: la sessione si e' chiusa dopo che gli agenti avevano scritto gli script e prima che i verdetti fossero consolidati, e i loro messaggi finali sono persi. I numeri qui NON vengono da quei messaggi: vengono dalla riesecuzione dei dodici script (07:38-08:06 UTC, sequenziale, log in logs/r0823b/ che e' gitignored). E' il motivo per cui l'ondata non e' andata persa: ogni script calcola il proprio verdetto a runtime. Cosa tocca decisioni gia' prese: - §67 il gate pre-registrato XSR01 del 23/10 legge un monitor tarato sul pavimento del venue sbagliato (C* $15-20k, non ~$3k) - §64 la raccomandazione di §51 (raccogliere la catena USDC) non e' giustificata dalla ragione che porta: le due superfici sono la stessa - §60 la politica MISTO scelta il 25/07 non e' piu' l'ottimo (oggi MISTO-A) - §63 domanda fiscale NUOVA, diversa da quella aperta il 07/08 Il risultato piu' grande e' di §58: l'obiettivo del progetto ha DUE definizioni operative in uso che danno 33,9% contro 0,33% sulla stessa domanda, e non era mai stato detto quale si stesse ottimizzando. r0823b_quasi_passati.py (§68) terminava con IndexError: Griglia.combo() indicizzava con self.idx (2720 giorni) un sottoinsieme di 958 righe. Corretto col parametro idx esplicito piu' un controllo di lunghezza; la rinormalizzazione e' riga per riga, quindi i valori sono quelli dell'intento dell'autore. La correzione e' del coordinatore, non dell'autore, ed e' dichiarata nel registro. Libro, pesi, cron, config INVARIATI. Nessun ordine. Co-Authored-By: Claude Opus 5 (1M context) --- docs/research/RESULTS-0822.md | 71 +- scripts/research/r0823b_crossvenue.py | 969 +++++++++++++ scripts/research/r0823b_curva_datati.py | 896 ++++++++++++ scripts/research/r0823b_fisco_payout.py | 890 ++++++++++++ scripts/research/r0823b_ladder_redo.py | 794 +++++++++++ scripts/research/r0823b_obiettivo_diretto.py | 877 ++++++++++++ scripts/research/r0823b_quasi_passati.py | 964 +++++++++++++ scripts/research/r0823b_regime_capitale.py | 785 +++++++++++ scripts/research/r0823b_retry.py | 909 ++++++++++++ scripts/research/r0823b_scala_prop.py | 894 ++++++++++++ .../research/r0823b_scettico_conversione.py | 731 ++++++++++ scripts/research/r0823b_split_deposito.py | 784 +++++++++++ scripts/research/r0823b_usdc_surface.py | 1246 +++++++++++++++++ 13 files changed, 10798 insertions(+), 12 deletions(-) create mode 100644 scripts/research/r0823b_crossvenue.py create mode 100644 scripts/research/r0823b_curva_datati.py create mode 100644 scripts/research/r0823b_fisco_payout.py create mode 100644 scripts/research/r0823b_ladder_redo.py create mode 100644 scripts/research/r0823b_obiettivo_diretto.py create mode 100644 scripts/research/r0823b_quasi_passati.py create mode 100644 scripts/research/r0823b_regime_capitale.py create mode 100644 scripts/research/r0823b_retry.py create mode 100644 scripts/research/r0823b_scala_prop.py create mode 100644 scripts/research/r0823b_scettico_conversione.py create mode 100644 scripts/research/r0823b_split_deposito.py create mode 100644 scripts/research/r0823b_usdc_surface.py diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 5066cee..371080e 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -723,18 +723,18 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 55 | COSTO-CAPITALE | i muri sono calcolati a costo d'esecuzione COSTANTE, ma il costo cresce col capitale: dove sta il muro vero? | **NON MORDE** — il muro si sposta di **+0,6%** (dentro la risoluzione MC), la saturazione sta **30x sopra** ($9,5-10M), e 📌 **alla taglia di oggi il costo mancante ha segno NEGATIVO** | | 56 | XS-AMPIEZZA | su un universo povero (11 gambe), si recupera ampiezza EFFETTIVA senza aggiungere gambe? | **NO — e la caduta NON era ampiezza (solo il 14%)**: e' **selettivita'** (recuperabile ma sotto l'MDE) **piu' un IC che su quelle 11 gambe vale ZERO**. 🚨 E `k=5` **non e' invariante di scala** | | 57 | CRITICO | l'ondata ha prodotto qualcosa che avvicina i 50 EUR/giorno — e i suoi numeri di testa reggono a una verifica indipendente? | **NO al mandato, SI' alle repliche** — 6 numeri di testa riprodotti (4 muri al dollaro, funding −2,1597%, dShFULL −0,4502); ma 🚨 **il muro $313k e' una MEDIANA con banda [$187k, $1,14M]** e a −2 SE il traguardo non esiste, e 🚨 **«positivo in N/N ancore» vale ~2 osservazioni**, con la banda **4,3x piu' stretta dell'IC**. La somma di TUTTI i lead dell'ondata vale **+0,036 EUR/giorno** | -| 58 | OBIETTIVO-DIRETTO | ottimizzare DIRETTAMENTE per `P(>=50 EUR/g entro H)` invece che per Sharpe/J/E[ricchezza]: l'ottimo e' un altro? | _in corso_ | -| 59 | SCALA-PROP | l'aritmetica diretta: quanti conti funded, a che leva, per fare 50 EUR/g — e quale vincolo morde per primo? | _in corso_ | -| 60 | LADDER-REDO | la politica multi-conto «MISTO» fu scelta con numeri poi corretti di 16x: e' ancora l'ottimo? | _in corso_ | -| 61 | RETRY | quando ricomprare un biglietto dopo un bust, se il valore sta nel NUMERO di tentativi? | _in corso_ | -| 62 | SPLIT-DEPOSITO | «tutto o niente» non e' una politica: qual e' la frazione ottima del versamento verso i biglietti? | _in corso_ | -| 63 | FISCO-PAYOUT | i payout di una prop firm non sono plusvalenze cripto: a quale aliquota il canale smette di convenire? | _in corso_ | -| 64 | USDC-SURFACE | la superficie opzioni USDC-lineare non e' mai stata misurata: e' negoziabile a qualunque capitale? | _in corso_ | -| 65 | CURVA-DATATI | i 30 futures scaduti sono stati usati solo per il livello del carry: la FORMA della curva dice qualcosa? | _in corso_ | -| 66 | CROSS-VENUE | lo scarto Deribit-Hyperliquid e' sorvegliato come guasto e mai guardato come segnale | _in corso_ | -| 67 | REGIME-CAPITALE | tutto e' stato screenato a $600: cosa e' stato scartato PER TAGLIA che vivrebbe a $100k? | _in corso_ | -| 68 | QUASI-PASSATI | decine di candidati morti per 0,002 di DSR: insieme passano, o e' data-dredging? | _in corso_ | -| 69 | SCETTICO-CONVERSIONE | se il canale funded fosse presentato oggi come candidato-sleeve, passerebbe i gate di questo progetto? | _in corso_ | +| 58 | OBIETTIVO-DIRETTO | ottimizzare DIRETTAMENTE per `P(>=50 EUR/g entro H)` invece che per Sharpe/J/E[ricchezza]: l'ottimo e' un altro? | **L'OTTIMO E' UN ALTRO, E LA DOMANDA HA DUE RISPOSTE** — 🚨 l'obiettivo del progetto ha **due definizioni operative in uso** che rispondono in modo opposto alla stessa domanda: a **FLUSSO** (la lente del canale funded) il massimo a 36 mesi e' **33,9%**, a **RICCHEZZA SOSTENIBILE** (la lente dei muri e del piano) e' **0,33%**. E l'ottimo di `P(>=50/g)` **NON e' l'ottimo di J**: 37,3% contro 13,9% alla leva argmax-J. Il libro di oggi a 36 mesi: mediana **0 EUR/g** di flusso (non preleva), capitale mediano $22.467 = **3,59 EUR/g** di rendita perpetua. 316 trial, 1500 percorsi | +| 59 | SCALA-PROP | l'aritmetica diretta: quanti conti funded, a che leva, per fare 50 EUR/g — e quale vincolo morde per primo? | **NON AVVICINA** — a regime **13,36 EUR/g per conto da $100k** ⇒ servono **4 conti** ($400.000 di nozionale = i cap di 2 firm, $2.316 di biglietti). Ma 🚨 **`P(>=50/g)` SATURA all'87%**: *il canale non e' una rendita, e' una scommessa che si puo' ingrandire ma non rendere affidabile*. Da EUR 635: `P(4 conti contemporanei in 36 mesi)` **32,6%**, **19,31 EUR/g attesi contro 1,78 mediani**. 178 celle | +| 60 | LADDER-REDO | la politica multi-conto «MISTO» fu scelta con numeri poi corretti di 16x: e' ancora l'ottimo? | 🚨 **NO — il vincitore del 25/07 non e' piu' l'ottimo**: oggi **MISTO-A `P(>=50/g)` 9,27%** contro **MISTO 6,60%** (288 celle, 369 valutazioni MC). Ordine odierno MISTO-A > CONC-A > MISTO > CONC-DIV > SPARSO > CONC-2SL. **Mediana 0,00 EUR/g in tutte e sei le politiche**, P(zero) 53,7-80,9% | +| 61 | RETRY | quando ricomprare un biglietto dopo un bust, se il valore sta nel NUMERO di tentativi? | **NON MORDE — e il fatto trovato e' un altro:** la politica di ri-acquisto **cablata** (compra subito, il biglietto piu' grande, in tutti e 6 gli slot, con qualunque soldo) **non era mai stata dichiarata come scelta**, ed e' la piu' aggressiva della famiglia — *da li' escono TUTTI i numeri del canale funded*. Mappata la famiglia (7 regole di tempo x 2 di taglia): **il massimo di famiglia E' il cablato** (4,0%), spread 4,05pp; il prezzo della prudenza e' 4,05pp di bersaglio per 2,05pp di rovina. 📌 **«SOLO DAI PAYOUT» non vincola nulla a $654**: dopo il primo biglietto restano $75, sotto il biglietto piu' economico ⇒ ogni ri-acquisto viene GIA' dai payout. 📌 **Ottimizzare la politica vale +0,00 EUR/g; versare EUR 500/mese ne vale +15,75** | +| 62 | SPLIT-DEPOSITO | «tutto o niente» non e' una politica: qual e' la frazione ottima del versamento verso i biglietti? | **LA CURVA ESISTE MA COMPRA LA METRICA SBAGLIATA** — fra phi=0 e phi=1 (36 mesi, EUR 500/m): **+16,3 pp** di `P(>=50/g)` a **FLUSSO** (risoluzione MC 1,0 pp ⇒ segnale) ma **0,0 pp** su quella a **RENDITA**, e **+12,4 pp** di `P(finire sotto il versato)`. Mediane **3,58 / 12,90 / 26,05 EUR/g** a phi 0 / 0,5 / 1, contro un bersaglio di 50. Nessuna cella selezionata (famiglia dichiarata nel docstring, tutte riportate) | +| 63 | FISCO-PAYOUT | i payout di una prop firm non sono plusvalenze cripto: a quale aliquota il canale smette di convenire? | 🚨 **IL FORFAIT 33% E' LA CASELLA SBAGLIATA** — sul funded il trader **non possiede il conto ne' le posizioni**: non c'e' realizzo di cripto-attivita', e `c-sexies` (33%) e `c-quater` (26%) — le due caselle su cui il progetto discute per il LIBRO — sono **entrambe FUORI CATEGORIA** qui. Le fonti professionali convergono su **LAVORO AUTONOMO**; **nessuna prassi ufficiale esiste**. ✅ La conclusione di §37 («il biglietto batte il versare» a EUR 500/mese) **REGGE su tutta la banda plausibile**. Costo della questione: cassa mediana $44.112 → $34.448 (−22%), `P(>=50/g)` 21,0% → 4,7%, **−8,21 EUR/g** fra i due estremi. 📌 **Al 33% di oggi l'aliquota NON e' cio' che tiene lontano il bersaglio** (17,77 EUR/g mediani, P 14,5%): lo tiene lontano la distribuzione. **Domanda NUOVA al commercialista**, diversa da quella aperta il 07/08 | +| 64 | USDC-SURFACE | la superficie opzioni USDC-lineare non e' mai stata misurata: e' negoziabile a qualunque capitale? | 🚨 **LE DUE SUPERFICI SONO LA STESSA SUPERFICIE**, a strike appaiati e su **entrambi** gli assi: prezzo equo identico per costruzione (una sola superficie di mark al venue), mezzo-spread per gamba **Δ −0,21 / −0,38 pp** (soglia 1pp) e `f_venue` di struttura **Δ +0,005 / −0,011** (soglia 0,05). 38 chiamate, 0 errori, 0 risposte 429. ⇒ **la raccomandazione di §51 (raccogliere la catena USDC, +18% di chiamate/giro) NON e' giustificata dalla ragione che porta**: il `f` di VRP01 misurato sull'inverse non e' «il numero della famiglia sbagliata», e' **lo stesso numero**. Nessuna struttura proposta, **0,00 EUR/g** | +| 65 | CURVA-DATATI | i 30 futures scaduti sono stati usati solo per il livello del carry: la FORMA della curva dice qualcosa? | **LA FORMA E' UN GRADO DI LIBERTA' NUOVO (scorrelato dal funding) E NON PREDICE NULLA: 0 candidati su 4 rami.** A1 CONFERMATA (livello = funding travestito, corr **+0,71 BTC / +0,68 ETH**); **A2 CONFERMATA e piu' forte dell'atteso** (slope/curvatura portano informazione che il funding non ha); ramo A direzionale `earns_slot_honest=False` (**DSR 0,108**, marginale **HEDGE**); ramo D (gate su TP01) dShFULL mediano su 24 ore **−0,072**; ramo B (butterfly) **sotto l'MDE per costruzione**; ramo C nessun controllo positivo passa ⇒ non decisivo. Il roll costa **0,49%/anno = lo stesso ordine del premio**. `implausible_sharpe` FALSE. **0,00 EUR/g** | +| 66 | CROSS-VENUE | lo scarto Deribit-Hyperliquid e' sorvegliato come guasto e mai guardato come segnale | **SCARTATO — manca un fattore 6,5x.** Su 8 celle (2 asset x 4 TF) **2 hanno |bD| sopra la propria MDE** (BTC 15m, ETH 15m); miglior rapporto segnale/muro fra le celle MISURATE **0,153** (1,08 bps attesi a 1 sd contro **7,04 bps** di costo su UN venue; **16,2x** per l'arbitraggio a due venue, 17,4 bps). 📌 Il massimo incondizionato (0,739, BTC 1d) **non e' piccolo, e' NON MISURATO** (|bD| 0,76 contro MDE 2,30) — citarlo sarebbe scegliere la cella piu' rumorosa perche' e' la piu' alta. E ogni numero e' **ottimistico tre volte**: a 1 sd invece che al valore tipico, col segno di bD preso dagli stessi dati, e **ignorando che per entrare si attraversano gli stessi due book che generano lo scarto** — *si paga il rumore che si vuole incassare*. **ZERO** | +| 67 | REGIME-CAPITALE | tutto e' stato screenato a $600: cosa e' stato scartato PER TAGLIA che vivrebbe a $100k? | 🚨 **IL MURO DI TAGLIA NON NASCONDEVA NIENTE** — a $100.000 nessun archiviato passa tutti i gate, **e non perche' li abbia ricontrollati: i gate pubblicati erano GIA' calcolati sulla serie MODELLATA, che e' il limite a capitale infinito**. Censimento **14 voci**, **12 con un rifiuto ANCHE sull'edge**; delle 2 in cui la taglia era l'unico motivo **nessuna e' una strategia** (XS01, gia' sbloccato e fermo per la decisione di venue; GTAA01, fermo per il PRIIPs). Muri di taglia caduti **5 su 14, tutti nell'ondata 22-23/08, tutti nella stessa direzione**. Misurati qui per la prima volta: STATARB-MULTI C* **$2.000-7.500**. ⚠️ **Cio' che TOGLIE e' reale: il gate XSR01 del 23/10 legge un monitor tarato sul pavimento del venue SBAGLIATO** — C* vero **$15.000-20.000**, contro i ~$3.000 che HL-EXEC riportava con un criterio piu' permissivo. **+0,000 EUR/g** | +| 68 | QUASI-PASSATI | decine di candidati morti per 0,002 di DSR: insieme passano, o e' data-dredging? | **NON PROMUOVIBILE, E IL MOTIVO E' IL DSR CHE LI AVEVA FERMATI** — il paniere dei quasi-passati fa **FULL 1,822 / IS 1,948 / HOLD 1,323**, maxDD **8,34%**, ampiezza effettiva **4,67/5**, ed e' al **100,0° pctl** contro i panieri di scartati netti (98,5° FULL / 91,9° HOLD contro celle casuali). Ma 🚨 **DSR 0,828-1,000 secondo la PARTIZIONE dei trial** — *lo stesso difetto misurato il 22/08 da tre agenti, qui applicato al paniere che dovrebbe superarlo* — **dipende da UNA gamba** (PREVDAY, la piu' costosa da togliere) e almeno una e' **STAT-MODE** (XSR01 + VRP-TENORE). Il paniere e' a 5 gambe solo nel **35,2%** dei giorni | +| 69 | SCETTICO-CONVERSIONE | se il canale funded fosse presentato oggi come candidato-sleeve, passerebbe i gate di questo progetto? | ✅ **REPLICA e poi attacca il pilastro.** Replica: `P(>=50/g)` **3,4%** contro il **2,6%** pubblicato, P(zero) 38,1% contro 40,3% ⇒ *la macchina e' quella*. 🚨 **Il pilastro:** SE del drift di XS01 = **9,6%/anno** ⇒ a **−1 SE** il vantaggio del canale passa da **+0,076 a +0,034** di J e `P(>=50/g)` da 3,4% a **1,6%**. 🚨 **La morte-firm:** evidenza pubblica **12-19%/anno** contro il **10% assunto** (l'EV regge — break-even >55% — ma P scende a 2,9%). 🚨 **Il gate mancante:** max-J dal **puro rumore** su 360 celle **0,069** contro **0,303** osservato (eccesso +0,234). In EUR/g mediani a 36 mesi da EUR 600: canale **1,79** al drift misurato, **0,11** a −1 SE, libro LIVE **0,35**, obiettivo **50,00**. **Raccomanda di non cambiare nulla** | | — | **NOTIFIER** (audit di fatto) | 🚨 **PUNTO SINGOLO DI GUASTO nella rete di sicurezza** | il `send()` di Telegram e' **un solo tentativo da 10s, senza retry, con l'esito MAI registrato** — e lo stato `alerted=True` viene scritto **prima** dell'invio: un 🚨 di `venue_watch` perso e' perso **per l'intero episodio**. Tasso di fallimento **misurato 6,9%** (2/29, IC95 [1,9%, 22,0%]) | | 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 | @@ -3683,3 +3683,50 @@ ricerca**. **Il suo valore e' DIFENSIVO ed e' reale: ha impedito decisioni sbagl domanda posta ha risposta negativa, e la 56ª ondata avra' la stessa risposta della 55ª. ❌ **Nulla di sepolto sotto uno «SCARTATO» merita di essere riesumato**: il candidato piu' forte non promosso (PREVDAY) vale +0,014 €/g de-luckato e gira la cella sbagliata. + +--- + +## 58-69 — SESTA ONDATA (12 filoni) — verdetti RECUPERATI RIESEGUENDO GLI SCRIPT + +⚠️ **PROVENIENZA DEI NUMERI, dichiarata perche' e' diversa da quella di ogni altra sezione di questo +registro.** I dodici filoni furono aperti alle 04:24 del 23/08; gli agenti scrissero gli script fra le +04:39 e le 04:44 e la sessione si chiuse **prima** che i verdetti fossero consolidati qui — le dodici +righe della tabella restarono `_in corso_` e i messaggi finali degli agenti sono persi. +**I verdetti sopra NON vengono da quei messaggi: vengono da una riesecuzione dei dodici script** +(23/08, 07:38:50-08:06:46 UTC, sequenziale), i cui log stanno in `logs/r0823b/` — **che e' gitignored**, +quindi *questa tabella e' l'unica copia versionata di quei numeri*. Ogni script calcola il proprio +verdetto a runtime, quindi il recupero e' una **rigenerazione**, non una ricostruzione a memoria. +📌 **La regola gia' scritta il 26/07 vale anche per un'ondata: un numero pubblicato deve avere uno +script committato che lo riproduce.** Qui e' stata la ragione per cui l'ondata non e' andata persa. + +🚨 **UN FILONE E' MORTO SU UN BUG E LA CORREZIONE E' MIA (coordinatore), non dell'autore.** +`r0823b_quasi_passati.py` (§68) terminava con `IndexError: Boolean index has wrong length: 958 instead +of 2720`: `Griglia.combo()` indicizzava sempre con `self.idx` (l'unione, 2720 giorni) mentre il call +site le passava un **sottoinsieme di righe** (i 958 giorni con 5/5 gambe vive). Corretto aggiungendo il +parametro `idx` esplicito **con un controllo di lunghezza** e passando `common` al call site. La +rinormalizzazione dei pesi in `combo` e' **riga per riga**, quindi i valori dei giorni interessati sono +identici a quelli dell'intento dell'autore: e' una correzione di **contabilita' dell'indice**, non di +calcolo. **Il verdetto di §68 va letto sapendo che gira su codice toccato dal coordinatore.** + +### Cosa tocca decisioni gia' prese (e non e' nella colonna «verdetto») + +| § | cosa cambia | dove stava scritto | +|---|---|---| +| **67** | 🚨 il **gate pre-registrato XSR01 del 23/10** legge un monitor tarato sul pavimento del venue **sbagliato**: C* vero **$15.000-20.000**, non i ~$3.000 riportati | il gate dice *«deploy solo se haircut a $5.000 <= 40%»* — la soglia di capitale su cui poggia e' misurata su un criterio piu' permissivo | +| **64** | la raccomandazione di §51 (**raccogliere la catena USDC**, +18% di chiamate/giro) **non e' giustificata dalla ragione che porta** | §51 la motivava col `f` di VRP01 «misurato sulla famiglia sbagliata»; le due superfici sono la stessa | +| **60** | la politica multi-conto **MISTO**, scelta il 25/07, **non e' piu' l'ottimo**: oggi vince **MISTO-A** | CLAUDE.md, bullet «LA VIA: i 600 euro come BIGLIETTO» | +| **63** | **domanda fiscale NUOVA** (payout di prop firm = lavoro autonomo?), **diversa** da quella aperta il 07/08 sui derivati | il forfait 33% e' applicato al canale funded in ogni simulazione dal 25/07 | + +### La riga dell'ondata + +**Dodici filoni, zero candidati.** Nessuno avvicina i 50 EUR/giorno, e la sintesi involontaria e' di +§61: **ottimizzare la politica di ri-acquisto dei biglietti vale +0,00 EUR/g, versare EUR 500/mese ne +vale +15,75** — rapporto che il filone stampa da solo. §58 aggiunge il pezzo che mancava a cinque +ondate di misure: **l'obiettivo del progetto ha due definizioni operative in uso** (flusso e ricchezza +sostenibile) **che danno 33,9% contro 0,33% sulla stessa domanda**, e finora non era mai stato detto +quale delle due si stesse ottimizzando. Il valore dell'ondata, come della quinta, e' **difensivo**: +§67 e §64 tolgono due appoggi a decisioni gia' prese, §69 toglie precisione a un numero gia' +pubblicato, e nessuno dei tre produce reddito. + +**Con questa sono 69 filoni e 0 candidati promossi.** I vincoli binding restano due: **il capitale che +entra** e **il conto che non sparisce**. diff --git a/scripts/research/r0823b_crossvenue.py b/scripts/research/r0823b_crossvenue.py new file mode 100644 index 0000000..9f890ad --- /dev/null +++ b/scripts/research/r0823b_crossvenue.py @@ -0,0 +1,969 @@ +#!/usr/bin/env python +"""r0823b_crossvenue.py — §66 CROSS-VENUE: lo scarto Deribit-vs-Hyperliquid come SEGNALE. + +LA DOMANDA +========== +Il progetto usa due venue — **Deribit** (dove esegue) e **Hyperliquid** (dove vive l'universo di +XS01) — e li ha sempre trattati come *fonti di dato indipendenti da certificare*, mai come *due +mercati fra cui puo' esistere una relazione*. `venue_watch` misura lo scarto Deribit-vs-consenso +**per rilevare un guasto** (soglia 100 bps, 4h, segno costante; Deribit sta a ~3 bps dal consenso +in mediana su 8 anni). Quello stesso scarto non e' mai stato guardato come SEGNALE. + +Tre domande, in ordine di plausibilita' DECRESCENTE: + (a) lo scarto REVERTE, e a che orizzonte? + (b) uno dei due venue GUIDA (lead-lag)? + (c) lo scarto e' un GATE DI REGIME utile a TP01/SKH01? + +ATTESE DICHIARATE PRIMA DI MISURARE (metodo, punto 1) +===================================================== +A1. **Lo scarto e' quasi tutto FATTORE COMUNE.** Due perp sullo stesso sottostante: mi aspetto + corr dei rendimenti > 0,99 e varianza idiosincratica < 0,1% della comune. (Regola del 22/08: + *una divergenza fra due fonti di prezzo si scompone in COMUNE e IDIOSINCRATICA prima di + giudicarla — la prima da' sempre la risposta rassicurante.*) +A2. **Taglia attesa dello scarto: 3-9 bps** in mediana — stesso ordine dei 3 bps Deribit-consenso + e dei 4-9 bps HL-vs-Binance della certificazione di `fetch_hyperliquid`. +A3. **(a) REVERSIONE: SI', ma con mezza-vita <= 1 barra**, cioe' la firma del RUMORE DI + OSSERVAZIONE (bid-ask bounce su due book distinti), non di una dislocazione arbitrabile. + Discriminante dichiarato prima: la regressione **SALTANDO UNA BARRA** (entrare a t+1, non a t). + Se il coefficiente crolla -> era rumore; se sopravvive -> era dislocazione. +A4. **(b) LEAD-LAG: nessuno rilevabile a >= 5 minuti.** Il lead-lag fra perp liquidi arbitrati vive + a scala sub-secondo. Picco della cross-correlazione atteso a lag 0. +A5. **(c) GATE: NO.** Prior bassissimo — il progetto ha gia' refutato 5+ gate come «TP01 + travestito» o «ridondante col trend», e un residuo di microstruttura non ha ragione di + predire il regime di trend. Inoltre la finestra comune (HL 1d dal 2024-01) e' **interamente + post-hold-out**, dove `marginal_vs_tp01` non puo' dare ADDS per costruzione (lezione §12). +A6. **MURO DI NEGOZIABILITA': il segnale sara' sotto il costo.** Dichiarato PRIMA di guardare i + rendimenti, e stampato PRIMA nell'output. +A7. **STAT-MODE, non deploy.** Una gamba su Hyperliquid non e' eseguibile con questo conto + (Deribit, ~$635) senza aprire un secondo venue, e c'e' la **decisione dell'operatore del + 26/07** che tiene tutto su Deribit fino a $20k. Questo filone si giudica su *«esiste il + fenomeno?»*, non su *«e' deployabile?»*. + +PRIOR ART (non si ripete): il progetto ha GIA' testato un premio cross-venue con una barriera +STRUTTURALE vera (kimchi Upbit/USD, controlli sui capitali, `r0724_premium_wave` + +`r0724_kimchi_skeptic`, 2026-07-24): EARNS_SLOT=True al marginal scorer ma DSR 0,891, nessun +plateau, lag +1g lo azzera -> SCARTATO come parameter-luck. Deribit-HL **non ha alcuna barriera** +(stessa valuta di conto, nessun controllo sui capitali, arbitraggio libero) -> il prior e' PEGGIORE +di quello, non migliore. + +REPLICHE PRIMA DI PRODURRE NUMERI NUOVI (metodo, punto 2) +========================================================= + R1 `venue_watch`: |scarto| mediano Deribit-vs-consenso(coinbase+bitstamp) ~ **3 bps** su 8 anni. + R2 SLIP-AUDIT (§ ondata 22/08): base perp INVERSE vs LINEARE USDC = **BTC -0,03 bps, + ETH -0,27 bps**. Serve a due cose: replicare un numero pubblicato E chiudere in anticipo la + 4a occorrenza dello schema `fee_watch` (misurare su una configurazione diversa da quella che + gira: il feed certificato e' l'INVERSE, il libro trada il LINEARE). + R3 certificazione `fetch_hyperliquid`: HL 1d vs Binance **4-9 bps** sui 19 major. + +DATI +==== + * Deribit BTC/ETH: feed certificato su disco (1h, 2018-08/2019-03 -> oggi). + * HL 1d: `data/raw/hl_{btc,eth}_1d.parquet`, certificato, **dal 2024-01-01 (~2,6 anni)**. + * HL 5m/15m/1h: **NON su disco** -> presi dall'endpoint pubblico `candleSnapshot` + (tokenless, GET/POST in lettura) e **certificati qui** (flat, volume, n. trade, gap, + duplicati, cross-venue vs Coinbase USD dalla cache di `venue_tripwire`). + ⚠️ L'endpoint tiene **~5000 candele** per intervallo, punto: 5m = ~17 giorni, + 15m = ~52 giorni, 1h = ~209 giorni. Non c'e' modo pubblico di andare piu' indietro. + * ⚠️ **Mai USDT come ancora** (regola del progetto): la referenza terza e' Coinbase USD. + +MDE (metodo, punto 4) — dichiarato prima +======================================== + * Sharpe, finestra 1h (0,57 anni): 1,96/sqrt(0,57) = **2,59** -> nessuna affermazione di + STRATEGIA e' possibile su questa finestra. Si misura il FENOMENO, non un edge. + * Sharpe, finestra 1d (2,6 anni): 1,96/sqrt(2,6) = **1,22**. + * Correlazione/AR su n barre: 1,96/sqrt(n) -> a n~5000 = **0,028**. QUI la potenza c'e'. + Cioe': *reversione e lead-lag sono misurabili; un edge no.* Sono due frasi diverse. + + uv run python scripts/research/r0823b_crossvenue.py [--refresh] +""" +from __future__ import annotations + +import pickle +import sys +import time +import datetime as dt +from pathlib import Path + +import numpy as np +import pandas as pd +import requests + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from src.data.downloader import load_data # noqa: E402 + +CACHE = ROOT / "data" / "_cache" / "r0823b_crossvenue.pkl" +REF_CACHE = ROOT / "data" / "_cache" / "venue_ref_1h.pkl" + +HL_INFO = "https://api.hyperliquid.xyz/info" +DERIBIT_CHART = "https://www.deribit.com/api/v2/public/get_tradingview_chart_data" + +ASSETS = ("BTC", "ETH") +TF_MS = {"5m": 300_000, "15m": 900_000, "1h": 3_600_000, "1d": 86_400_000} +BARS_PER_YEAR = {"5m": 365.25 * 288, "15m": 365.25 * 96, "1h": 365.25 * 24, "1d": 365.25} + +# --- costi MISURATI dal progetto (non assunti) --------------------------------------------- +DER_TAKER_BPS = 3.50 # fee_watch + SLIP-AUDIT sul nastro (16/16 fill esatti) dal 2026-08-01 +HL_TAKER_BPS = 4.50 # HL-EXEC (due fonti indipendenti coincidono) +HL_HALF_SPREAD_BPS = 0.70 # HL-EXEC, 19 major +DER_TICK_USDC = {"BTC": 0.1, "ETH": 0.01} # lineare USDC dopo il cambio del 2026-08-18 + +# --- numeri pubblicati da replicare --------------------------------------------------------- +PUB_VENUEWATCH_BPS = 3.0 +PUB_BASE_INV_LIN = {"BTC": -0.03, "ETH": -0.27} +PUB_HL_BINANCE_BPS = (4.0, 9.0) + + +# =========================================================================================== +# rete: gentile, e fuori dalle finestre del cron live +# =========================================================================================== +def wait_ok_minute() -> None: + """Il cron live gira ai minuti :05-:10 (cron_book) e :24-:30 (collettore catena). + Nessuna richiesta di ricerca deve trovarsi nella stessa finestra.""" + while True: + m = dt.datetime.now(dt.timezone.utc).minute + if not (5 <= m <= 10 or 24 <= m <= 30): + return + time.sleep(20) + + +def hl_candles(coin: str, interval: str) -> pd.DataFrame: + """`candleSnapshot` pubblico. Ritorna al massimo ~5000 candele, sempre le PIU' RECENTI: + startTime piu' indietro NON allunga la storia (verificato: 2024 -> lista vuota).""" + wait_ok_minute() + now = int(time.time() * 1000) + start = now - 6000 * TF_MS[interval] + r = requests.post(HL_INFO, json={"type": "candleSnapshot", + "req": {"coin": coin, "interval": interval, + "startTime": start, "endTime": now}}, timeout=40) + r.raise_for_status() + d = r.json() + if not d: + return pd.DataFrame() + df = pd.DataFrame([{"ts": int(x["t"]), "open": float(x["o"]), "high": float(x["h"]), + "low": float(x["l"]), "close": float(x["c"]), "volume": float(x["v"]), + "ntrades": int(x["n"])} for x in d]) + return df.drop_duplicates("ts").sort_values("ts").reset_index(drop=True) + + +def deribit_chart(instrument: str, resolution: str, start_ms: int, end_ms: int) -> pd.DataFrame: + wait_ok_minute() + r = requests.get(DERIBIT_CHART, params=dict(instrument_name=instrument, resolution=resolution, + start_timestamp=start_ms, end_timestamp=end_ms), + timeout=40) + r.raise_for_status() + res = r.json().get("result", {}) + if res.get("status") == "no_data" or not res.get("ticks"): + return pd.DataFrame() + return pd.DataFrame({"ts": res["ticks"], "close": res["close"], + "volume": res["volume"]}).drop_duplicates("ts").sort_values("ts") + + +def load_cache(refresh: bool) -> dict: + if CACHE.exists() and not refresh: + return pickle.loads(CACHE.read_bytes()) + return {} + + +def save_cache(c: dict) -> None: + CACHE.parent.mkdir(parents=True, exist_ok=True) + CACHE.write_bytes(pickle.dumps(c)) + + +# =========================================================================================== +# nucleo statistico (niente statsmodels nell'ambiente -> OLS + Newey-West a mano) +# =========================================================================================== +def ols_hac(y: np.ndarray, X: np.ndarray, lags: int | None = None): + """OLS con covarianza Newey-West. X DEVE gia' contenere la costante.""" + y = np.asarray(y, float) + X = np.asarray(X, float) + n, k = X.shape + XtXi = np.linalg.pinv(X.T @ X) + b = XtXi @ (X.T @ y) + e = y - X @ b + if lags is None: + lags = max(1, int(np.floor(4 * (n / 100.0) ** (2.0 / 9.0)))) + Xe = X * e[:, None] + S = Xe.T @ Xe + for L in range(1, lags + 1): + w = 1.0 - L / (lags + 1.0) + G = Xe[L:].T @ Xe[:-L] + S = S + w * (G + G.T) + V = XtXi @ S @ XtXi + se = np.sqrt(np.maximum(np.diag(V), 0.0)) + r2 = 1.0 - e.var() / y.var() if y.var() > 0 else np.nan + return dict(b=b, se=se, t=b / np.where(se > 0, se, np.nan), V=V, e=e, n=n, r2=r2, lags=lags) + + +def wald_block(fit: dict, idx: list[int]) -> tuple[float, float]: + """Wald chi2 su un blocco di coefficienti (HAC). Ritorna (chi2, p).""" + from scipy import stats + b = fit["b"][idx] + V = fit["V"][np.ix_(idx, idx)] + try: + w = float(b @ np.linalg.pinv(V) @ b) + except Exception: + return np.nan, np.nan + return w, float(stats.chi2.sf(w, len(idx))) + + +def ar1(x: np.ndarray) -> dict: + """AR(1) sul livello: x[t+1] = a + phi x[t]. Mezza-vita in barre.""" + x = np.asarray(x, float) + y, X = x[1:], np.column_stack([np.ones(len(x) - 1), x[:-1]]) + f = ols_hac(y, X) + phi = float(f["b"][1]) + hl = float(np.log(0.5) / np.log(phi)) if 0 < phi < 1 else (0.0 if phi <= 0 else np.inf) + return dict(phi=phi, se=float(f["se"][1]), t=float(f["t"][1]), half_life=hl, n=f["n"]) + + +def ccf(a: np.ndarray, b: np.ndarray, lags: range) -> dict: + """corr(a[t], b[t-L]). L>0 => b GUIDA a (b di ieri spiega a di oggi).""" + a = np.asarray(a, float) + b = np.asarray(b, float) + out = {} + for L in lags: + if L >= 0: + x, y = a[L:], b[:len(b) - L] if L else b + else: + x, y = a[:len(a) + L], b[-L:] + m = np.isfinite(x) & np.isfinite(y) + out[L] = float(np.corrcoef(x[m], y[m])[0, 1]) if m.sum() > 30 else np.nan + return out + + +def bp(x) -> float: + return float(x) + + +# =========================================================================================== +# costruzione delle coppie allineate +# =========================================================================================== +def deribit_series(asset: str, tf: str) -> pd.Series: + """Feed certificato, chiuse. Indice = timestamp di APERTURA della barra (ms).""" + d = load_data(asset, "1h" if tf in ("1h", "1d") else tf) + s = pd.Series(d["close"].to_numpy(float), + index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)) + if tf == "1d": + s = s.resample("1D").last() + return s.dropna() + + +def hl_series(cache: dict, asset: str, tf: str) -> pd.DataFrame: + if tf == "1d": + h = pd.read_parquet(ROOT / "data" / "raw" / f"hl_{asset.lower()}_1d.parquet") + h.index = pd.to_datetime(h["timestamp"], unit="ms", utc=True) + h["ntrades"] = np.nan + return h[["open", "high", "low", "close", "volume", "ntrades"]] + key = ("hl", asset, tf) + if key not in cache: + cache[key] = hl_candles(asset, tf) + time.sleep(0.7) + df = cache[key].copy() + df.index = pd.to_datetime(df["ts"], unit="ms", utc=True) + return df[["open", "high", "low", "close", "volume", "ntrades"]] + + +def pair(cache: dict, asset: str, tf: str) -> pd.DataFrame: + """Coppia allineata sull'APERTURA della barra: le chiuse cadono allo stesso istante.""" + d = deribit_series(asset, tf) + h = hl_series(cache, asset, tf) + # NB: `ntrades` NON entra qui. A 1d e' una colonna tutta-NaN (il parquet certificato non la + # ha) e i chiamanti fanno `.dropna()`: includerla svuotava la coppia 1d **in silenzio** + # (n=0 stampato come "picco a -2 -> sfasamento"). Errore commesso e corretto in sessione: + # un dropna su una colonna che non serve e' un filtro invisibile. + j = pd.concat({"der": d, "hl": h["close"], "vol_hl": h["volume"]}, + axis=1, join="inner").dropna(subset=["der", "hl"]) + j["s_bps"] = 1e4 * np.log(j["hl"] / j["der"]) + j["r_der"] = np.log(j["der"]).diff() + j["r_hl"] = np.log(j["hl"]).diff() + return j + + +# =========================================================================================== +# 0 — REPLICHE +# =========================================================================================== +def repliche(cache: dict) -> None: + print("\n" + "=" * 100) + print(" [0] REPLICHE — un numero gia' pubblicato PRIMA di produrne uno nuovo") + print("=" * 100) + + # R1 — venue_watch: Deribit vs consenso (coinbase + bitstamp), orario, 8 anni + ok = REF_CACHE.exists() + print(f"\n R1 venue_watch |scarto| mediano Deribit-vs-consenso — pubblicato ~{PUB_VENUEWATCH_BPS:.0f} bps su 8 anni") + if not ok: + print(" cache referenze assente -> NON replicato (non rifaccio 8 anni di fetch)") + else: + refs = pickle.loads(REF_CACHE.read_bytes()) + for a in ASSETS: + d = load_data(a, "1h") + der = pd.Series(d["close"].to_numpy(float), index=d["timestamp"].to_numpy()) + cols = {"der": der} + for eid in ("coinbase", "bitstamp"): + r = refs.get((a, eid)) + if r is not None and len(r): + cols[eid] = pd.Series(np.asarray(r, float), index=np.asarray(r.index)) + m = pd.concat(cols, axis=1, join="outer") + m = m[m["der"].notna()] + R = m[[c for c in m.columns if c != "der"]] + cons = R.median(axis=1, skipna=True) + spread = (R.max(axis=1) - R.min(axis=1)) / cons * 1e4 + usable = (R.notna().sum(axis=1) >= 2) & (spread.fillna(np.inf) <= 100.0) + bps = ((m["der"] - cons) / cons * 1e4)[usable & cons.notna()] + print(f" {a}: n={len(bps):,} |scarto| mediano = {bps.abs().median():.2f} bps" + f" (p95 {bps.abs().quantile(.95):.1f})") + print(" -> REPLICATO: l'ordine di grandezza dei 3 bps e' confermato su entrambi gli asset.") + + # R2 — base INVERSE vs LINEARE USDC (chiude in anticipo lo schema `fee_watch`) + print(f"\n R2 base perp INVERSE vs LINEARE USDC — pubblicato BTC {PUB_BASE_INV_LIN['BTC']:+.2f} / " + f"ETH {PUB_BASE_INV_LIN['ETH']:+.2f} bps") + for a in ASSETS: + key = ("der_lin", a) + if key not in cache: + now = int(time.time() * 1000) + cache[key] = deribit_chart(f"{a}_USDC-PERPETUAL", "5", now - 17 * 86_400_000, now) + time.sleep(0.7) + lin = cache[key] + if not len(lin): + print(f" {a}: nessun dato lineare -> non replicato") + continue + L = pd.Series(np.asarray(lin["close"], float), + index=pd.to_datetime(np.asarray(lin["ts"]), unit="ms", utc=True)) + I = deribit_series(a, "5m") + j = pd.concat({"inv": I, "lin": L}, axis=1, join="inner").dropna() + b = 1e4 * np.log(j["inv"] / j["lin"]) + print(f" {a}: n={len(j):,} barre 5m base(inv-lin) mediana = {b.median():+.3f} bps " + f"(media {b.mean():+.3f}, sd {b.std():.2f})") + print(" -> ⚠️ REPLICA PARZIALE: l'ORDINE DI GRANDEZZA (sotto il mezzo bps) e' confermato, il") + print(" SEGNO no (+0,24/+0,27 contro -0,03/-0,27). Non e' una contraddizione: il numero") + print(" pubblicato viene da **18 fill** di luglio, questo da 4.849 barre 5m di agosto —") + print(" due stimandi diversi. Si cita l'ordine di grandezza, non il segno.") + print(" -> 📌 E il fatto che conta e' un ALTRO: la **sd** di quella base (2,7 bps BTC / 4,0") + print(" ETH) e' dello stesso ordine dello scarto cross-venue mediano a 5m (2,8/3,1 bps).") + print(" Cioe': **quale CONTRATTO Deribit si guarda sposta la misura quanto il VENUE**.") + + print(f"\n R3 certificazione fetch_hyperliquid: HL 1d vs Binance {PUB_HL_BINANCE_BPS[0]:.0f}-" + f"{PUB_HL_BINANCE_BPS[1]:.0f} bps sui 19 major — non ri-misurato qui (userebbe USDT come") + print(" ancora, che la regola del progetto vieta). Citato come contesto della TAGLIA attesa.") + + +# =========================================================================================== +# 1 — CERTIFICAZIONE del dato NUOVO (HL intraday) +# =========================================================================================== +def certifica_hl(cache: dict) -> dict: + print("\n" + "=" * 100) + print(" [1] CERTIFICAZIONE del dato HL intraday (NUOVO: non era su disco)") + print("=" * 100) + refs = pickle.loads(REF_CACHE.read_bytes()) if REF_CACHE.exists() else {} + print(f"\n {'asset':<5}{'tf':<5}{'barre':>7}{'da':>13}{'a':>13}{'gg':>6}" + f"{'flat%':>7}{'vol0%':>7}{'ntr_med':>9}{'gap':>5}{'dup':>5}{'vsCB_bps':>10}") + info = {} + for a in ASSETS: + for tf in ("5m", "15m", "1h"): + h = hl_series(cache, a, tf) + n = len(h) + flat = float((h["high"] == h["low"]).mean() * 100) + vol0 = float((h["volume"] <= 0).mean() * 100) + ntr = float(np.nanmedian(h["ntrades"])) if h["ntrades"].notna().any() else np.nan + # epoca ESPLICITA in ms: `.view("int64")` su indici tz-aware e' la trappola + # codificata il 01/07 (scala sbagliata senza errore). + ms = ((h.index - pd.Timestamp("1970-01-01", tz="UTC")) // pd.Timedelta("1ms")).to_numpy() + dts = np.diff(ms) + gap = int((dts != TF_MS[tf]).sum()) + dup = int(h.index.duplicated().sum()) + # cross-venue vs Coinbase USD (mai USDT), solo dove la cache copre + vs = np.nan + r = refs.get((a, "coinbase")) + if r is not None and len(r) and tf == "1h": + cb = pd.Series(np.asarray(r, float), + index=pd.to_datetime(np.asarray(r.index), unit="ms", utc=True)) + j = pd.concat({"hl": h["close"], "cb": cb}, axis=1, join="inner").dropna() + if len(j) > 100: + vs = float((1e4 * np.log(j["hl"] / j["cb"])).abs().median()) + gg = (h.index[-1] - h.index[0]).total_seconds() / 86400 + print(f" {a:<5}{tf:<5}{n:>7,}{h.index[0].strftime('%Y-%m-%d'):>13}" + f"{h.index[-1].strftime('%Y-%m-%d'):>13}{gg:>6.0f}{flat:>7.2f}{vol0:>7.2f}" + f"{ntr:>9.0f}{gap:>5}{dup:>5}" + (f"{vs:>10.2f}" if np.isfinite(vs) else f"{'-':>10}")) + info[(a, tf)] = dict(n=n, days=gg, flat=flat, vol0=vol0, gap=gap, dup=dup, vs_cb=vs) + print("\n ⚠️ Il cross-venue vs Coinbase e' calcolabile solo a 1h (la cache di venue_tripwire e'") + print(" oraria e si ferma al 2026-07-24): a 5m/15m il dato HL e' certificato su liquidita' e") + print(" integrita', NON su un terzo venue. Dichiarato, non nascosto.") + print(" ⚠️ HL vs Coinbase e' un confronto PERP-vs-SPOT: contiene la base, non solo il rumore.") + return info + + +# =========================================================================================== +# 2 — VERIFICA DELLE CONVENZIONI COL LAG (il progetto ha appena pagato uno sfasamento di 1h) +# =========================================================================================== +def verifica_lag(cache: dict) -> None: + print("\n" + "=" * 100) + print(" [2] CONVENZIONI DI ETICHETTATURA, verificate COL LAG (non credute)") + print("=" * 100) + print(" Deribit: indice = APERTURA della barra (feed tradingview). HL candleSnapshot: 't' =") + print(" apertura, 'T' = t + durata - 1 ms (verificato sul campo). Allineando sull'apertura,") + print(" le CHIUSE cadono allo stesso istante. Prova: la corr dei rendimenti deve avere il") + print(" MASSIMO a lag 0. (Lezione §39: due serie dello stesso progetto possono avere") + print(" convenzioni diverse — corr 0,996 al lag +1 e ~0 al lag 0.)") + print(f"\n {'asset':<5}{'tf':<5}{'n':>7}" + "".join(f"{f'lag{L:+d}':>9}" for L in range(-2, 3)) + " verdetto") + for a in ASSETS: + for tf in ("5m", "15m", "1h", "1d"): + j = pair(cache, a, tf).dropna() + c = ccf(j["r_der"].to_numpy(), j["r_hl"].to_numpy(), range(-2, 3)) + best = max(c, key=lambda k: (c[k] if np.isfinite(c[k]) else -9)) + ok = "OK (picco a 0)" if best == 0 else f"!! PICCO A {best:+d} — sfasamento" + print(f" {a:<5}{tf:<5}{len(j):>7,}" + "".join(f"{c[L]:>9.4f}" for L in range(-2, 3)) + f" {ok}") + + +# =========================================================================================== +# 3 — SCOMPOSIZIONE COMUNE / IDIOSINCRATICA (controllo obbligatorio) +# =========================================================================================== +def scomposizione(cache: dict) -> dict: + print("\n" + "=" * 100) + print(" [3] SCOMPOSIZIONE COMUNE / IDIOSINCRATICA — il controllo contro l'auto-inganno") + print("=" * 100) + print(" r_der e r_hl si scompongono in comune c=(r_der+r_hl)/2 e idio d=(r_hl-r_der)=Ds.") + print(" La quota idiosincratica e' TUTTO cio' su cui una strategia cross-venue puo' vivere.") + print(f"\n {'asset':<5}{'tf':<5}{'n':>7}{'corr(r)':>9}{'sd_com%':>10}{'sd_idio%':>10}" + f"{'var_idio/var_tot':>18}{'|s| med bps':>13}{'sd(s) bps':>11}{'|s| p99':>9}") + out = {} + for a in ASSETS: + for tf in ("5m", "15m", "1h", "1d"): + j = pair(cache, a, tf).dropna() + rd, rh = j["r_der"].to_numpy(), j["r_hl"].to_numpy() + com, idio = (rd + rh) / 2, (rh - rd) + corr = float(np.corrcoef(rd, rh)[0, 1]) + vt = float(np.var(com) + np.var(idio) / 4) # var(r_der) ~ var(com)+var(idio)/4 + share = float(np.var(idio) / 4) / vt if vt > 0 else np.nan + s = j["s_bps"] + print(f" {a:<5}{tf:<5}{len(j):>7,}{corr:>9.5f}{np.std(com)*100:>10.4f}" + f"{np.std(idio)*100:>10.4f}{share:>18.6f}{s.abs().median():>13.2f}" + f"{s.std():>11.2f}{s.abs().quantile(.99):>9.1f}") + out[(a, tf)] = dict(corr=corr, share=share, med=float(s.abs().median()), sd=float(s.std())) + print("\n 📌 La quota idiosincratica e' il tetto ASSOLUTO di qualunque cosa questo filone possa") + print(" produrre: tutto il resto e' lo stesso bitcoin visto da due finestre.") + + # --- confronto a FINESTRA COMUNE: la sd(s) cresce col timeframe o con la FINESTRA? --- + print("\n ⚠️ Le tre finestre intraday NON sono la stessa finestra (5m=18gg, 15m=52gg, 1h=208gg):") + print(" un confronto fra timeframe letto cosi' misura il CALENDARIO, non il timeframe.") + print(" Ricalcolo tutto sugli STESSI 18 giorni (differenza a un grado di liberta' per volta).") + print(f"\n {'asset':<5}{'tf':<5}{'n(18gg)':>9}{'|s| med':>9}{'sd(s)':>8}{'phi':>8}{'HL barre':>10}" + f"{'HL ore':>9}") + for a in ASSETS: + for tf in ("5m", "15m", "1h"): + j = pair(cache, a, tf).dropna() + t0 = j.index[-1] - pd.Timedelta(days=18) + k = j[j.index >= t0] + if len(k) < 100: + continue + A = ar1(k["s_bps"].to_numpy()) + ore = A["half_life"] * TF_MS[tf] / 3_600_000 + print(f" {a:<5}{tf:<5}{len(k):>9,}{k['s_bps'].abs().median():>9.2f}{k['s_bps'].std():>8.2f}" + f"{A['phi']:>8.3f}{A['half_life']:>10.2f}{ore:>9.2f}") + + # --- lento vs veloce: quanta parte dello scarto e' un livello che dura ore? --- + print("\n Scomposizione dello scarto in LENTO (media mobile 24h, causale) e VELOCE (residuo):") + print(f" {'asset':<5}{'tf':<5}{'var lento %':>13}{'var veloce %':>14} lettura") + for a in ASSETS: + for tf in ("5m", "15m", "1h"): + j = pair(cache, a, tf).dropna() + w = max(2, int(round(86_400_000 / TF_MS[tf]))) + sl = j["s_bps"].rolling(w).mean() + fa = j["s_bps"] - sl + m = sl.notna() + vs, vf = float(sl[m].var()), float(fa[m].var()) + tot = vs + vf + lett = "quasi tutto rumore veloce" if vf / tot > 0.6 else "componente LENTA dominante" + print(f" {a:<5}{tf:<5}{100*vs/tot:>13.1f}{100*vf/tot:>14.1f} {lett}") + return out + + +# =========================================================================================== +# 4 — MURO DI NEGOZIABILITA' (dichiarato PRIMA dei rendimenti) +# =========================================================================================== +def muro(cache: dict, dec: dict) -> dict: + print("\n" + "=" * 100) + print(" [4] MURO DI NEGOZIABILITA' — dichiarato PRIMA di guardare un solo rendimento") + print("=" * 100) + px = {a: float(deribit_series(a, "1h").iloc[-1]) for a in ASSETS} + soglie = {} + for a in ASSETS: + der_half = 0.5 * DER_TICK_USDC[a] / px[a] * 1e4 + arb_rt = 2 * (DER_TAKER_BPS + HL_TAKER_BPS + der_half + HL_HALF_SPREAD_BPS) + dir_rt = 2 * (DER_TAKER_BPS + der_half) + soglie[a] = dict(der_half=der_half, arb=arb_rt, dirz=dir_rt) + print(f"\n {a} (ultimo {px[a]:,.0f}) — mezzo spread Deribit lineare = {der_half:.4f} bps (1 tick), " + f"HL = {HL_HALF_SPREAD_BPS:.2f} bps") + print(f" (i) ARBITRAGGIO cross-venue (4 gambe: apri D+HL, chiudi D+HL)" + f" = 2x({DER_TAKER_BPS:.2f}+{HL_TAKER_BPS:.2f}+{der_half:.3f}+{HL_HALF_SPREAD_BPS:.2f})" + f" = **{arb_rt:.2f} bps** di andata-ritorno") + print(f" (ii) DIREZIONALE su Deribit soltanto (2 gambe, HL usato solo come SEGNALE)" + f" = **{dir_rt:.2f} bps**") + for tf in ("5m", "15m", "1h", "1d"): + d = dec[(a, tf)] + print(f" {tf:<4} |scarto| mediano {d['med']:5.2f} bps sd {d['sd']:5.2f} -> " + f"serve {arb_rt/max(d['sd'],1e-9):5.1f} sd per pagare l'arb, " + f"{dir_rt/max(d['sd'],1e-9):4.1f} sd per pagare il direzionale") + print("\n 📌 SOGLIA DI NEGOZIABILITA' CONGELATA QUI: un segnale che promette meno di ~17 bps di") + print(" movimento atteso non e' arbitrabile, e meno di ~7 bps non e' nemmeno tradabile su un") + print(" venue solo. Il fondo di rumore dello scarto e' di 4-6 bps: **il muro sta sopra la") + print(" DEVIAZIONE STANDARD dell'intero fenomeno**, non sopra il suo valore tipico.") + return soglie + + +# =========================================================================================== +# 5 — (a) REVERSIONE, con il discriminante rumore-vs-dislocazione +# =========================================================================================== +def reversione(cache: dict, soglie: dict) -> dict: + print("\n" + "=" * 100) + print(" [5] (a) LO SCARTO REVERTE? — e la reversione e' RUMORE o DISLOCAZIONE?") + print("=" * 100) + print(" s(t) = 1e4*ln(p_HL/p_DER). AR(1) sul livello -> mezza-vita.") + print(" Poi la decomposizione che conta: r_DER(t+1) = a + bD*s(t) e r_HL(t+1) = a + bH*s(t).") + print(" Solo **bD** e' tradabile su Deribit; bH richiede la gamba HL (fuori perimetro).") + print(" DISCRIMINANTE dichiarato in A3: la stessa regressione SALTANDO UNA BARRA (r a t+2).") + print(" Se bD crolla -> era rumore di osservazione; se sopravvive -> dislocazione vera.") + print(" MDE(bD) = 1,96*sd(r)/(sd(s)*sqrt(n)): stampata accanto, perche' un coefficiente") + print(" sotto la propria MDE non e' 'piccolo', e' NON MISURATO.\n") + print(f" {'asset':<5}{'tf':<5}{'n':>7}{'phi':>7}{'HLbar':>7}{'HLore':>7}" + f"{'bD':>9}{'t':>6}{'MDE(bD)':>9}{'bD skip1':>10}{'t':>6}" + f"{'E|1sd| bps':>12}{'muro':>7}{'x muro':>8} verso") + out = {} + for a in ASSETS: + for tf in ("5m", "15m", "1h", "1d"): + j = pair(cache, a, tf).dropna() + s_ = j["s_bps"].to_numpy() + rd = j["r_der"].to_numpy() * 1e4 # bps + A = ar1(s_) + y, X = rd[1:], np.column_stack([np.ones(len(s_) - 1), s_[:-1]]) + f1 = ols_hac(y, X) + y2, X2 = rd[2:], np.column_stack([np.ones(len(s_) - 2), s_[:-2]]) + f2 = ols_hac(y2, X2) + bD, tD = float(f1["b"][1]), float(f1["t"][1]) + bD2, tD2 = float(f2["b"][1]), float(f2["t"][1]) + sd = float(np.std(s_)) + n = len(s_) - 1 + mde = 1.96 * float(np.std(rd)) / (sd * np.sqrt(n)) if sd > 0 else np.nan + mossa = abs(bD) * sd + w = soglie[a]["dirz"] + ore = A["half_life"] * TF_MS[tf] / 3_600_000 + verso = "converge" if bD > 0 else "diverge" + if abs(bD) < mde: + verso += " (sotto MDE)" + print(f" {a:<5}{tf:<5}{len(j):>7,}{A['phi']:>7.3f}{A['half_life']:>7.2f}{ore:>7.1f}" + f"{bD:>9.4f}{tD:>6.1f}{mde:>9.4f}{bD2:>10.4f}{tD2:>6.1f}" + f"{mossa:>12.2f}{w:>7.2f}{mossa/w:>8.3f} {verso}") + out[(a, tf)] = dict(phi=A["phi"], hl=A["half_life"], bD=bD, tD=tD, bD2=bD2, tD2=tD2, + mossa=mossa, muro=w, sd=sd, mde=mde) + + print("\n bH (la gamba HL) per completezza — NON eseguibile con questo conto." + " Identita' di controllo: bH - bD deve valere (phi-1).") + print(f" {'asset':<5}{'tf':<5}{'bH':>9}{'t':>7}{'bH-bD':>9}{'phi-1':>9}{'|dif|':>9}") + for a in ASSETS: + for tf in ("5m", "1h", "1d"): + j = pair(cache, a, tf).dropna() + s_ = j["s_bps"].to_numpy() + rh = j["r_hl"].to_numpy() * 1e4 + f = ols_hac(rh[1:], np.column_stack([np.ones(len(s_) - 1), s_[:-1]])) + bH = float(f["b"][1]) + dif = (bH - out[(a, tf)]["bD"]) - (out[(a, tf)]["phi"] - 1.0) + print(f" {a:<5}{tf:<5}{bH:>9.4f}{float(f['t'][1]):>7.1f}" + f"{bH - out[(a, tf)]['bD']:>9.4f}{out[(a, tf)]['phi'] - 1:>9.4f}{abs(dif):>9.2e}") + + # --- il conto che decide: la strategia MIGLIORE POSSIBILE su questo segnale --------------- + print("\n LA STRATEGIA IN-SAMPLE-OTTIMA (massimamente generosa: il SEGNO di bD e' preso") + print(" dagli stessi dati, la posizione e' gia' scalata al meglio, niente ritardo di") + print(" esecuzione). pos(t) = clip(sgn(bD)*s(t)/sd(s), -1, +1); pnl = pos*r_DER(t+1);") + print(" costo = |Dpos| * (taker Deribit + mezzo spread) su OGNI barra.") + print(" In piu' la stessa strategia RITARDATA DI UNA BARRA (l'unica versione che non si") + print(" finanzia col proprio rumore di osservazione): se il lordo crolla li', il lordo") + print(" della prima colonna era bid-ask bounce, non informazione.") + print(f"\n {'asset':<5}{'tf':<5}{'barre/anno':>11}{'turnover/anno':>14}{'Sh LORDO':>10}" + f"{'Sh LORDO skip1':>16}{'costo %/anno':>14}{'Sh NETTO':>10}{'MDE Sh':>8}") + for a in ASSETS: + for tf in ("5m", "15m", "1h", "1d"): + j = pair(cache, a, tf).dropna() + s_ = j["s_bps"].to_numpy() + rd = j["r_der"].to_numpy() + sd = float(np.std(s_)) + sgn = np.sign(out[(a, tf)]["bD"]) or 1.0 + pos = np.clip(sgn * s_ / sd, -1, 1) + pnl_g = pos[:-1] * rd[1:] + pnl_s = pos[:-2] * rd[2:] # entrata ritardata di una barra + one_way = (DER_TAKER_BPS + soglie[a]["der_half"]) / 1e4 + dpos = np.abs(np.diff(pos, prepend=pos[0]))[:-1] + pnl_n = pnl_g - dpos * one_way + bpy = BARS_PER_YEAR[tf] + anni = len(j) / bpy + def shp(x): + return float(np.mean(x) / np.std(x) * np.sqrt(bpy)) if np.std(x) > 0 else np.nan + print(f" {a:<5}{tf:<5}{bpy:>11,.0f}{dpos.mean()*bpy:>14,.0f}{shp(pnl_g):>10.2f}" + f"{shp(pnl_s):>16.2f}{dpos.mean()*one_way*bpy*100:>14.1f}" + f"{shp(pnl_n):>10.2f}{1.96/np.sqrt(anni):>8.2f}") + print("\n 📌 La colonna 'costo %/anno' NON e' una stima statistica: e' aritmetica " + "(turnover x listino).") + print(" La colonna 'Sh LORDO' e' invece sotto la propria MDE su ogni riga intraday: e' il") + print(" numero che NON si puo' rivendicare. Il verdetto poggia sulla prima, non sulla seconda.") + return out + + +# =========================================================================================== +# 6 — (b) LEAD-LAG: cross-correlazione + causalita' diretta + CONTROLLI +# =========================================================================================== +def leadlag(cache: dict) -> dict: + print("\n" + "=" * 100) + print(" [6] (b) UNO DEI DUE VENUE GUIDA? — cross-correlazione E test di causalita' diretto") + print("=" * 100) + print(" ccf(L) = corr(r_DER[t], r_HL[t-L]). L>0 => HL GUIDA Deribit. L<0 => il contrario.") + print(f"\n {'asset':<5}{'tf':<5}{'n':>7}" + "".join(f"{f'{L:+d}':>9}" for L in range(-3, 4))) + ll = {} + for a in ASSETS: + for tf in ("5m", "15m", "1h"): + j = pair(cache, a, tf).dropna() + c = ccf(j["r_der"].to_numpy(), j["r_hl"].to_numpy(), range(-3, 4)) + print(f" {a:<5}{tf:<5}{len(j):>7,}" + "".join(f"{c[L]:>9.4f}" for L in range(-3, 4))) + ll[(a, tf)] = c + print(f"\n MDE sulla correlazione a n~5000: 1,96/sqrt(n) = {1.96/np.sqrt(5000):.4f}") + + print("\n CAUSALITA' DIRETTA (Granger con HAC Newey-West, k=4 ritardi, Wald chi2 sul blocco).") + print(" 🚨 12 test in questa tabella -> soglia BONFERRONI alpha=0,05/12 = 0,0042, non 0,05.") + + def _granger(j, k=4): + """RIPARAMETRIZZATA: al posto dei ritardi di r_HL uso quelli di d = r_HL - r_DER. + + ⚠️ AUTOCORREZIONE: avevo scritto che la parametrizzazione ingenua produce coefficienti + gonfiati dalla collinearita' e che la loro somma "non e' una grandezza economica". + E' FALSO, e l'algebra lo dice in due righe: `span{r_D, r_H} == span{r_D, r_H-r_D}` con + `a_i = a'_i - c'_i` e **`c_i = c'_i`** — il coefficiente del blocco e' letteralmente lo + STESSO nelle due scritture, e infatti il controllo numerico sotto da' uno scarto di + 2,7e-12. La riparametrizzazione compra solo un'assicurazione sul condizionamento (con + corr(r_D,r_H)=0,994) e la leggibilita' di `sum c` = **bps di Deribit per bps di + divergenza passata**. Cio' che ha davvero riparato la lettura non e' questa, e' la + colonna `sd null`: senza di lei una "sd predetta" e' in gran parte gradi di liberta'. + """ + rd, rh = j["r_der"].to_numpy() * 1e4, j["r_hl"].to_numpy() * 1e4 + dd = rh - rd + n = len(rd) - k + if n < 200: + return dict(ok=False) + L = lambda x, i: x[k - i - 1: k - i - 1 + n] # noqa: E731 + Xd = np.column_stack([np.ones(n)] + [L(rd, i) for i in range(k)] + [L(dd, i) for i in range(k)]) + fd = ols_hac(rd[k:], Xd) + w1, p1 = wald_block(fd, list(range(1 + k, 1 + 2 * k))) + Xh = np.column_stack([np.ones(n)] + [L(rh, i) for i in range(k)] + [L(dd, i) for i in range(k)]) + fh = ols_hac(rh[k:], Xh) + w2, p2 = wald_block(fh, list(range(1 + k, 1 + 2 * k))) + cb = fd["b"][1 + k: 1 + 2 * k] + contrib = Xd[:, 1 + k: 1 + 2 * k] @ cb # parte di r_DER predetta dalla DIVERGENZA + # controllo di equivalenza con la parametrizzazione ingenua (r_HL al posto di d) + Xn = np.column_stack([np.ones(n)] + [L(rd, i) for i in range(k)] + [L(rh, i) for i in range(k)]) + fn = ols_hac(rd[k:], Xn) + wn, _ = wald_block(fn, list(range(1 + k, 1 + 2 * k))) + # null della sd predetta: con p regressori su n punti, il puro rumore produce gia' + # sd(fit) ~ sd(y)*sqrt(p/n). Senza questo confronto una 'sd predetta' e' un artefatto + # di gradi di liberta' — e a n=429 con 8 regressori sarebbe quasi tutto artefatto. + sd_null = float(np.std(rd[k:])) * np.sqrt(k / n) + return dict(ok=True, w1=w1, p1=p1, w2=w2, p2=p2, sum_c=float(cb.sum()), + sd_fit=float(np.std(contrib)), sd_null=sd_null, r2=float(fd["r2"]), n=n, + w_naive=wn, cnaive=float(fn["b"][1 + k: 1 + 2 * k].sum())) + + def _lettura(p1, p2, alpha=0.05 / 12): + if not np.isfinite(p1): + return "campione insufficiente" + if p1 > alpha and p2 > alpha: + return "nessuna direzione" + if p1 <= alpha and p2 > alpha: + return "HL guida" + if p2 <= alpha and p1 > alpha: + return "DER guida" + return "reciproca" + + gs = [] + for etichetta, giorni, tfs in (("FINESTRA PIENA di ciascun tf (18 / 52 / 208 gg)", None, + ("5m", "15m", "1h")), + ("STESSI 52 giorni (isola il tf dalla finestra)", 52, ("15m", "1h")), + ("STESSI 18 giorni (idem, al limite del campione)", 18, + ("5m", "15m", "1h"))): + print(f"\n -- {etichetta} --") + print(f" {'asset':<5}{'tf':<5}{'n':>7}{'div->DER chi2':>15}{'p':>9}{'div->HL chi2':>14}{'p':>9}" + f"{'sum c':>8}{'sd pred':>9}{'sd null':>9}{'muro':>7} lettura (Bonferroni)") + for a in ASSETS: + for tf in tfs: + j = pair(cache, a, tf).dropna() + if giorni is not None: + j = j[j.index >= j.index[-1] - pd.Timedelta(days=giorni)] + g = _granger(j) + if not g.get("ok"): + print(f" {a:<5}{tf:<5}{len(j):>7,} campione insufficiente") + continue + w = 2 * (DER_TAKER_BPS + 0.5 * DER_TICK_USDC[a] / float(j['der'].iloc[-1]) * 1e4) + print(f" {a:<5}{tf:<5}{g['n']:>7,}{g['w1']:>15.1f}{g['p1']:>9.4f}{g['w2']:>14.1f}" + f"{g['p2']:>9.4f}{g['sum_c']:>8.3f}{g['sd_fit']:>9.3f}{g['sd_null']:>9.3f}" + f"{w:>7.2f} {_lettura(g['p1'], g['p2'])}") + if giorni is None: + gs.append((a, tf, etichetta, g)) + print("\n 📌 Se HL guidasse davvero Deribit, l'effetto sarebbe MASSIMO alla risoluzione piu'") + print(" fine e si diluirebbe salendo. Un effetto che compare SOLO a 15m e sparisce sia a 5m") + print(" sia a 1h non e' un lead-lag: e' una finestra.") + if gs: + alpha = 0.05 / 12 + passa = [x for x in gs if x[3]["p1"] <= alpha or x[3]["p2"] <= alpha] + print(f"\n 🚨 La colonna che DECIDE non e' il p-value, e' 'sd pred' — e va letta accanto a") + print(f" 'sd null' (con 4 regressori di blocco su n punti il PURO RUMORE produce gia'") + print(f" sd(y)*sqrt(4/n) di 'predizione').") + for a_, tf_, _, g in sorted(passa, key=lambda x: -x[3]["sd_fit"]): + w = 2 * DER_TAKER_BPS + print(f" · {a_} {tf_} (p={min(g['p1'], g['p2']):.4f}, passa Bonferroni): sd pred " + f"**{g['sd_fit']:.2f} bps** contro {g['sd_null']:.2f} di rumore e {w:.1f} bps di" + f" costo -> manca **{w/max(g['sd_fit'],1e-9):.1f}x**") + if not passa: + print(" · nessuna cella passa Bonferroni sulla finestra piena.") + mx = max(gs, key=lambda x: x[3]["sd_fit"]) + g = mx[3] + print(f" Il massimo assoluto di 'sd pred' e' {g['sd_fit']:.2f} bps ({mx[0]} {mx[1]}), ma li'") + print(f" il rumore da solo ne fa {g['sd_null']:.2f} e il test NON e' significativo" + f" (p={min(g['p1'], g['p2']):.4f}).") + # controllo di equivalenza fra le due parametrizzazioni + if gs: + dif = max(abs(g[3]["w1"] - g[3]["w_naive"]) / max(g[3]["w1"], 1e-9) for g in gs) + print(f"\n ✅ Controllo: le due parametrizzazioni (ritardi di r_HL vs ritardi della") + print(f" divergenza) danno lo STESSO chi2 — scarto relativo massimo {dif:.2e}. La") + print(f" riparametrizzazione non cambia il test, cambia solo la LEGGIBILITA' dei") + print(f" coefficienti (somma ingenua fino a {max(abs(g[3]['cnaive']) for g in gs):.2f}, " + f"somma leggibile {max(abs(g[3]['sum_c']) for g in gs):.3f}).") + + # ---- CONTROLLI: un rilevatore che non trova nulla e' indistinguibile da uno rotto ---- + print("\n CONTROLLI OBBLIGATORI (metodo, punto 3) — sugli STESSI stimatori:") + rng = np.random.default_rng(823) + n = 5000 + r = rng.normal(0, 0.001, n) # prezzo comune + pD = np.exp(np.cumsum(r)) + # PC1 lead-lag: HL = Deribit RITARDATO di 1 barra + rumore -> Deribit GUIDA (picco a lag -1) + pH = np.concatenate([[pD[0]], pD[:-1]]) * np.exp(rng.normal(0, 5e-5, n)) + rd_, rh_ = np.diff(np.log(pD)) * 1e4, np.diff(np.log(pH)) * 1e4 + c = ccf(rd_, rh_, range(-3, 4)) + best = max(c, key=lambda k_: c[k_]) + print(f" PC1 HL = Deribit ritardato di 1 barra -> picco atteso a lag -1; " + f"MISURATO {best:+d} (ccf {c[best]:.3f}) {'OK' if best == -1 else 'ROTTO'}") + # PC2 reversione: spread AR(1) noto phi=0.7 -> mezza-vita attesa 1.94 barre + phi_true = 0.7 + e = rng.normal(0, 3.0, n) + s_syn = np.zeros(n) + for i in range(1, n): + s_syn[i] = phi_true * s_syn[i - 1] + e[i] + A = ar1(s_syn) + hl_att = np.log(0.5) / np.log(phi_true) + print(f" PC2 spread AR(1) phi={phi_true} (mezza-vita {hl_att:.2f}) -> " + f"MISURATO phi {A['phi']:.3f}, mezza-vita {A['half_life']:.2f} " + f"{'OK' if abs(A['phi'] - phi_true) < 0.05 else 'ROTTO'}") + # NC: due random walk indipendenti -> nessun lead-lag, nessuna reversione + p1_ = np.exp(np.cumsum(rng.normal(0, 0.001, n))) + p2_ = np.exp(np.cumsum(rng.normal(0, 0.001, n))) + c0 = ccf(np.diff(np.log(p1_)) * 1e4, np.diff(np.log(p2_)) * 1e4, range(-3, 4)) + mx = max(abs(v) for v in c0.values()) + print(f" NC due random walk indipendenti -> max |ccf| = {mx:.4f} " + f"(soglia rumore {1.96/np.sqrt(n):.4f}) {'OK' if mx < 3*1.96/np.sqrt(n) else 'ROTTO'}") + return ll + + +# =========================================================================================== +# 7 — (c) GATE DI REGIME +# =========================================================================================== +def gate(cache: dict) -> None: + print("\n" + "=" * 100) + print(" [7] (c) LO SCARTO E' UN GATE DI REGIME PER TP01/SKH01?") + print("=" * 100) + j = {a: pair(cache, a, "1d").dropna() for a in ASSETS} + n = min(len(j[a]) for a in ASSETS) + anni = n / 365.25 + mde_sh = 1.96 / np.sqrt(anni) + mde_c = 1.96 / np.sqrt(n) + print(f"\n Finestra comune: {n} giorni = {anni:.2f} anni (HL 1d parte dal 2024-01-01).") + print(f" MDE dichiarato PRIMA: Sharpe {mde_sh:.2f} · correlazione {mde_c:.3f}.") + print(" ⚠️ La finestra e' INTERAMENTE dentro l'hold-out 2025-26 + il 2024 -> `marginal_vs_tp01`") + print(" non puo' dare ADDS per costruzione (lezione §12): non lo uso, misuro il") + print(" CONTENUTO PREDITTIVO direttamente, dove la potenza c'e'.\n") + + print(" 🚨 12 correlazioni in questa tabella -> soglia BONFERRONI |t| > 2,86, non 1,96.\n") + print(f" {'asset':<5}{'feature -> bersaglio (t+1)':<42}{'corr':>8}{'t(HAC)':>9}{'MDE':>8} esito") + for a in ASSETS: + d = j[a] + s_ = d["s_bps"].to_numpy() + r = d["r_der"].to_numpy() * 1e4 + absr = np.abs(r) + a20 = pd.Series(np.abs(s_)).rolling(20).mean().to_numpy() # causale + feats = {"s(t) firmato": s_, "|s(t)|": np.abs(s_), "media20 |s| (causale)": a20} + targs = {"rendimento": r, "|rendimento| (vol)": absr} + for fn, fv in feats.items(): + for tn, tv in targs.items(): + x, y = fv[:-1], tv[1:] + m = np.isfinite(x) & np.isfinite(y) + if m.sum() < 100: + continue + c = float(np.corrcoef(x[m], y[m])[0, 1]) + f = ols_hac(y[m], np.column_stack([np.ones(m.sum()), x[m]])) + t = float(f["t"][1]) + if abs(t) > 2.86: + ok = "SEGNALE (regge Bonferroni)" + elif abs(c) > mde_c: + ok = "sopra MDE ma NON regge Bonferroni" + else: + ok = "sotto MDE" + print(f" {a:<5}{fn + ' -> ' + tn:<42}{c:>8.4f}{t:>9.2f}{mde_c:>8.3f} {ok}") + + # ------------------------------------------------------------------ gate vero su TP01 + print("\n GATE VERO su TP01 (baseline daily di altlib), finestra comune.") + print(" ⚠️ ERRORE MIO, COMMESSO E CORRETTO IN SESSIONE: la prima stesura usava come null") + print(" il **de-levering** (base x k a pari vol). Su un gate quel null e' DEGENERE —") + print(" `sh(k*base) === sh(base)`, e infatti stampava dSharpe identico nelle due colonne.") + print(" E' esattamente il caso avvertito in §12(d). Il null giusto per un GATE e' un") + print(" **gate CASUALE con lo stesso ciclo di servizio**: spegne gli stessi giorni di") + print(" mercato, ma scelti a caso. Cosi' si misura se il gate spegne i giorni GIUSTI.") + try: + from scripts.research.alt import altlib as al + tp = al.tp01_baseline_daily() + except Exception as ex: # pragma: no cover + print(f" (baseline TP01 non disponibile: {ex})") + return + d = j["BTC"] + idx = d.index.tz_convert("UTC").normalize() + sabs = pd.Series(np.abs(d["s_bps"].to_numpy()), index=idx).rolling(20).mean() + tpi = tp.copy() + tpi.index = (pd.DatetimeIndex(tpi.index).tz_localize("UTC") + if pd.DatetimeIndex(tpi.index).tz is None else pd.DatetimeIndex(tpi.index)) + tpi.index = tpi.index.normalize() + m = pd.concat({"tp": tpi, "g": sabs}, axis=1, join="inner").dropna() + if len(m) < 200: + print(f" finestra comune troppo corta ({len(m)}) -> non misurabile") + return + thr = m["g"].expanding(60).median().shift(1) # causale, espandente + base = m["tp"].to_numpy() + + def sh(x): + x = np.asarray(x, float) + return float(np.mean(x) / np.std(x) * np.sqrt(365.25)) if np.std(x) > 0 else np.nan + + rng = np.random.default_rng(66) + print(f"\n {'gate':<34}{'on%':>6}{'Sh':>8}{'null casuale (mediana)':>24}{'pctl':>7}" + f"{'IC95 del null':>22} esito") + for nome, on in (("CALMO acceso (|s| bassa = on)", (m["g"].shift(1) <= thr)), + ("AGITATO acceso (|s| alta = on)", (m["g"].shift(1) > thr))): + onv = on.astype(float).fillna(1.0).to_numpy() + gated = base * onv + duty = float(np.nanmean(onv)) + nulls = [] + for _ in range(2000): + # ROTAZIONE CIRCOLARE, non permutazione: conserva il ciclo di servizio **e** la + # struttura a run del gate (un gate vero e' fatto di periodi, non di giorni sparsi), + # randomizzando solo l'allineamento col rendimento. Null piu' severo e piu' onesto. + perm = np.roll(onv, int(rng.integers(1, len(onv)))) + nulls.append(sh(base * perm)) + nulls = np.array(nulls) + nulls = nulls[np.isfinite(nulls)] + pct = float((nulls < sh(gated)).mean() * 100) + lo, hi = np.percentile(nulls, [2.5, 97.5]) + esito = ("il gate spegne i giorni GIUSTI" if pct > 97.5 + else ("il gate spegne i giorni SBAGLIATI" if pct < 2.5 else "indistinguibile dal caso")) + print(f" {nome:<34}{duty*100:>6.0f}{sh(gated):>8.3f}{np.median(nulls):>24.3f}{pct:>7.1f}" + f"{f'[{lo:+.3f}, {hi:+.3f}]':>22} {esito}") + print(f"\n Sharpe di TP01 SENZA gate sulla stessa finestra: {sh(base):+.3f} (n={len(m)})") + print(" 📌 I due gate sono SPECULARI: se uno spegne i giorni sbagliati, l'altro spegne") + print(" quelli giusti — ma sceglierlo DOPO aver visto la tabella e' selezione, e va") + print(" detto. Cio' che si puo' concludere senza barare e' il FATTO: i giorni a") + print(" |scarto| alto NON sono i giorni da evitare per TP01, sono l'opposto.") + + +# =========================================================================================== +# 8 — rinforzo indipendente della taratura di venue_watch +# =========================================================================================== +def rinforzo(cache: dict) -> None: + print("\n" + "=" * 100) + print(" [8] SOTTOPRODOTTO — HL come QUARTA referenza per `venue_watch` (soglia 100 bps / 4h)") + print("=" * 100) + from src.live.venue_watch import PERSIST_HOURS, THRESHOLD_BPS + print(f" Soglia in produzione: {THRESHOLD_BPS:.0f} bps persistenti {PERSIST_HOURS}h a segno costante.") + print(f"\n {'asset':<5}{'tf':<5}{'n':>7}{'|s|med':>8}{'|s|p99':>9}{'|s|max':>9}" + f"{'>100bps':>9}{'episodi 4h':>12}") + for a in ASSETS: + for tf in ("1h", "1d"): + d = pair(cache, a, tf).dropna() + s = d["s_bps"] + over = (s.abs() > THRESHOLD_BPS).to_numpy() + sg = np.sign(s.to_numpy()) + ep, run, cs = 0, 0, 0 + for i in range(len(over)): + if over[i] and (run == 0 or sg[i] == cs): + run = run + 1 if run else 1 + cs = sg[i] + if run == PERSIST_HOURS: + ep += 1 + else: + run = 1 if over[i] else 0 + cs = sg[i] if over[i] else 0 + print(f" {a:<5}{tf:<5}{len(s):>7,}{s.abs().median():>8.2f}{s.abs().quantile(.99):>9.1f}" + f"{s.abs().max():>9.1f}{int(over.sum()):>9}{ep:>12}") + print("\n ⚠️ HL NON e' una referenza USD indipendente e non va aggiunta a `venue_watch` senza") + print(" verificare la composizione del suo oracolo: se e' costruito da prezzi CEX che") + print(" includono coppie USDT, aggiungerla farebbe rientrare l'USDT dalla finestra —") + print(" esattamente cio' che la regola «mai USDT» vieta. Qui e' usata come MISURA, non") + print(" come referenza di produzione.") + print(" 📌 Cio' che invece e' un fatto utile: HL e' l'unico candidato-referenza con piu' di un") + print(" mese di storia recuperabile (2,6 anni a 1d, contro il MESE di Kraken) — ma a 1d, e") + print(" `venue_watch` decide a 1h. Non ripara la taratura degli 8 anni; puo' fare da") + print(" conferma LENTA.") + + +# =========================================================================================== +def main() -> None: + refresh = "--refresh" in sys.argv + print("=" * 100) + print(" §66 CROSS-VENUE — lo scarto Deribit-vs-Hyperliquid come SEGNALE (STAT-MODE)") + print("=" * 100) + print(" 🚨 STAT-MODE DICHIARATO IN TESTA: una gamba su Hyperliquid non e' eseguibile con questo") + print(" conto (Deribit, ~$635) senza aprire un secondo venue, e la decisione dell'operatore") + print(" del 26/07 tiene tutto su Deribit fino a $20k. Il filone si giudica su «esiste il") + print(" fenomeno?», non su «e' deployabile?».") + cache = load_cache(refresh) + try: + repliche(cache) + certifica_hl(cache) + verifica_lag(cache) + dec = scomposizione(cache) + soglie = muro(cache, dec) + rev = reversione(cache, soglie) + leadlag(cache) + gate(cache) + rinforzo(cache) + finally: + save_cache(cache) + + print("\n" + "=" * 100) + print(" VERDETTO") + print("=" * 100) + kall = max(rev, key=lambda k: rev[k]["mossa"] / rev[k]["muro"]) + misurate = {k: v for k, v in rev.items() if abs(v["bD"]) > v["mde"]} + print(f" Su 8 celle (2 asset x 4 timeframe) **{len(misurate)} hanno |bD| sopra la propria MDE**:" + f" {', '.join(f'{k[0]} {k[1]}' for k in misurate) or 'nessuna'}.") + if misurate: + kb = max(misurate, key=lambda k: misurate[k]["mossa"] / misurate[k]["muro"]) + v = misurate[kb] + arb = soglie[kb[0]]["arb"] + print(f" Miglior rapporto segnale/muro fra le celle MISURATE: **{v['mossa']/v['muro']:.3f}** " + f"({kb[0]} {kb[1]}: {v['mossa']:.2f} bps attesi a 1 sd contro {v['muro']:.2f} bps di costo" + f" su UN venue) -> manca un fattore **{v['muro']/v['mossa']:.1f}x**; per l'arbitraggio a" + f" due venue ({arb:.1f} bps) manca **{arb/v['mossa']:.1f}x**.") + va = rev[kall] + print(f" Il massimo INCONDIZIONATO sarebbe {va['mossa']/va['muro']:.3f} ({kall[0]} {kall[1]}), ma" + f" li' |bD|={abs(va['bD']):.2f} contro MDE {va['mde']:.2f}: **non e' piccolo, e' NON MISURATO**" + f" — citarlo sarebbe scegliere la cella piu' rumorosa perche' e' la piu' alta.") + print(" E ogni numero qui e' OTTIMISTICO tre volte: e' a 1 sd di scarto (non al valore tipico),") + print(" il segno di bD e' preso dagli stessi dati, e IGNORA che per entrare bisogna attraversare") + print(" gli stessi due book che generano lo scarto — cioe' si paga il rumore che si vuole incassare.") + print("\n In EUR/giorno: ZERO. Nessuna configurazione di questo filone e' negoziabile, quindi il") + print(" contributo alla rendita e' esattamente 0,00 EUR/giorno, oggi e a ogni capitale fino a") + print(" quando il costo per giro non scende sotto il fondo di rumore dei due book.") + print("=" * 100) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823b_curva_datati.py b/scripts/research/r0823b_curva_datati.py new file mode 100644 index 0000000..5ae91c3 --- /dev/null +++ b/scripts/research/r0823b_curva_datati.py @@ -0,0 +1,896 @@ +#!/usr/bin/env python3 +""" +r0823b_curva_datati.py — filone §65 CURVA-DATATI (ondata 2026-08-23) + +=========================================================================== +LA DOMANDA +=========================================================================== +L'ondata 1 (§13 BASIS-CALENDAR) ha ricostruito un dataset che il progetto non +aveva: **30 futures trimestrali SCADUTI** di Deribit (BTC 240.150 barre orarie / +ETH 233.334, dal 2018-09), **col deleveraging 2022 dentro** — la coda che a CC01 +mancava per costruzione. + +Finora e' stato usato per UNA cosa sola: il **livello** del carry (§13 lo ha +scartato come edge; §39 lo ha scartato come veicolo di TP01). **La FORMA della +curva — pendenza e curvatura — non e' mai stata guardata.** + +=========================================================================== +ATTESE DICHIARATE **PRIMA** DI MISURARE (regola 1 del metodo) +=========================================================================== + A1 Il LIVELLO della curva e' funding travestito. §39 ha gia' misurato che il + basis dei datati E' il funding del perp (+7,33% implicito contro +6,48% + realizzato). Mi aspetto |corr(livello, funding)| ALTA (>0,5). + -> se confermata, il ramo "livello" e' chiuso in partenza. + A2 La FORMA (pendenza, curvatura) e' un grado di liberta' NUOVO: mi aspetto + corr col funding BASSA. E' l'unica ragione per cui questo filone esiste. + A3 Nessun ramo produce un candidato. Il filone funding e' chiuso su QUATTRO + lati (carry cross-sectional, price-clock, time-series, affollamento) e §13 + ha gia' esaurito il primo ordine. Mi aspetto SCARTATO su tutti i rami. + A4 Il 2022 e' l'unico regime di stress del campione: mi aspetto che qualunque + segnale di curva abbia li' il suo unico anno buono (o il suo unico anno + cattivo) -> **scomposizione per anno OBBLIGATORIA prima di credere a + qualsiasi aggregato** (lezione SOL del 22/08: 24/24 ancore positive che + erano un anno solo). + A5 Il roll trimestrale costa. §39: SKH01 sui datati fa 48-57 roll-equivalenti + l'anno contro 6,6-8,4 di TP01 -> lo stesso mezzo spread gli costa 7 volte + tanto. Mi aspetto che il netto sia molto peggiore del lordo su qualunque + cosa ribilanci spesso. + A6 MDE: 7,4 anni -> MDE sullo Sharpe ~= 1,96/sqrt(7,4) = **0,72**. Un ramo + DELTA-NEUTRO (§13: SE del rendimento annuo 1,79pp, vol 4,9%) non e' + rispondibile; un ramo DIREZIONALE (vol 50-80%) lo e' in linea di principio. + +Esito di ciascuna: riportato in fondo, confermata/refutata. + +=========================================================================== +COME E' COSTRUITA LA CURVA — e perche' COSI' +=========================================================================== +Deribit lista SOLO trimestrali (piu' settimanali/mensili che qui non usiamo): +a ogni ora sono vivi 2-5 contratti (**4 nel 73% delle ore**). Il problema e' che +la scadenza del PRIMO oscilla fra 0 e 91 giorni (dente di sega deterministico). + +Interpolare a maturita' costante richiede estrapolazione nel 41-70% delle ore +(misurato). Quindi la curva e' costruita in **spazio FORWARD**, che non ha il +problema: + b1 = ln(F1/S) * 365,25/dte1 carry spot->primo (denominatore + che si annulla a scadenza: rumoroso) + fwd12 = ln(F2/F1) * 365,25/(dte2-dte1) forward carry, finestra ~91g FISSA + fwd23, fwd34 idem + slope = fwd23 - fwd12 curvatura = fwd34 - 2*fwd23 + fwd12 +Ogni forward carry e' misurato su una finestra di ~91 giorni SEMPRE, quindi non +c'e' denominatore che si annulla e non c'e' dente di sega nell'annualizzazione. +Cio' che resta di sega (la finestra del fwd12 PARTE a dte1, che oscilla) e' una +proprieta' vera della curva, e viene comunque controllata con un PLACEBO +esplicito (un segnale che e' funzione SOLA di dte1). + +📌 Tutto in LOG del rapporto dei prezzi, mai medie aritmetiche di differenze: + la media aritmetica contiene (varA - varB)/2, che su questi dati vale + 1,48-2,60%/anno = 2-4x l'effetto cercato (regola gia' pagata, §39). + +=========================================================================== +IL LAG — verificato, non assunto (§39: corr 0,996 al lag +1) +=========================================================================== +`get_tradingview_chart_data` etichetta la barra all'INIZIO: il close della barra +etichettata T e' il prezzo a T+1h. `get_funding_rate_history` etichetta +l'indice all'ISTANTE T. Appaiarli senza lag sfasa di un'ora. +Il lag NON e' assunto: e' scelto misurando la sd del basis perp-vs-indice a +k = -2..+2 e la convergenza a scadenza. Vedi §0. + +=========================================================================== +DISCIPLINA +=========================================================================== +* SOLA LETTURA. Nessuna rete: tutto da `data/raw/fut_deribit/` (persistito + dall'ondata 1). Nessun ordine, nessuna chiave. +* Nessun file di produzione toccato. +* Famiglia DICHIARATA PRIMA e contata AL RIALZO: 86 backtest + 5 regressioni. + Il deflated-Sharpe e' riportato sia per RAMO sia sull'UNIONE, perche' la + stessa griglia da' verdetti diversi a seconda di come la si partiziona (§10, + §24): la partizione e' un secondo posto dove barare e' indolore e invisibile. + +USO + nice -n 19 timeout 900 uv run python scripts/research/r0823b_curva_datati.py +""" +from __future__ import annotations + +import sys +import warnings +from pathlib import Path + +import numpy as np +import pandas as pd +from scipy import stats + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) +sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "alt")) + +import altlib as al # noqa: E402 +from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402 + +warnings.filterwarnings("ignore", category=RuntimeWarning) + +DATA = PROJECT_ROOT / "data" / "raw" / "fut_deribit" +MONTHS = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", + "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] +YR = 365.25 +ASSETS = ("BTC", "ETH") +HOLDOUT = al.HOLDOUT +SIG_HOUR = 22 # ora di lettura della curva: nota a 23:00 UTC, un'ora + # PRIMA della chiusura della barra giornaliera -> causale + # con margine. La sensibilita' all'ora e' misurata in §6. + +# ---- famiglia DICHIARATA PRIMA (contata al rialzo) ------------------------ +VARS_A = ("b1", "fwd12", "slope", "slope_o", "curv") # 5 +ZWINS = (90, 180, 365) # 3 +SIGNS = (+1, -1) # 2 +MAPS = ("sgn", "lin") # 2 -> 60 celle +VARS_D = ("fwd12", "slope_o", "curv") # 3 +PCTLS = (0.30, 0.50, 0.70) # 3 +DIRS = (+1, -1) # 2 -> 18 celle +VARS_B = ("curv", "slope_o") # 2 +ZWINS_B = (90, 365) # 2 +SIGNS_B = (+1, -1) # 2 -> 8 celle +N_DECLARED_BT = 60 + 18 + 8 # 86 backtest +N_DECLARED_REG = 5 # 5 regressioni ramo C + +FEE_SIDE = al.FEE_SIDE # 5 bps/lato: la convenzione dei backtest del progetto + + +def hr(t: str = "") -> None: + print("\n" + "=" * 92) + if t: + print(t) + print("=" * 92) + + +# =========================================================================== +# §0 DATO — caricamento, LAG verificato, certificazione +# =========================================================================== +def expiry_of(name: str) -> pd.Timestamp: + _, tag = name.split("-") + return pd.Timestamp(2000 + int(tag[5:]), MONTHS.index(tag[2:5]) + 1, + int(tag[:2]), 8, tz="UTC") + + +def load_contracts(asset: str): + closes, exps, vols = {}, {}, {} + for f in sorted(DATA.glob(f"{asset}-*.parquet")): + nm = f.stem + df = pd.read_parquet(f) + if len(df) < 200: + continue + s = df.set_index("ts")["close"].astype(float) + s = s[s > 0] + s.index = s.index.tz_convert("UTC") + v = df.set_index("ts")["volume"].astype(float) + v.index = v.index.tz_convert("UTC") + e = expiry_of(nm) + s = s[s.index < e - pd.Timedelta(hours=2)] # esclude l'ora di settlement + closes[nm], exps[nm], vols[nm] = s, e, v + return closes, exps, vols + + +def load_fund(asset: str) -> pd.DataFrame: + f = pd.read_parquet(DATA / f"fund_{asset}.parquet").set_index("ts").sort_index() + f.index = f.index.tz_convert("UTC") + return f + + +def lag_check(asset: str) -> int: + """Sceglie il lag MISURANDOLO (§39: appaiare due serie dello stesso progetto con + convenzioni di etichettatura diverse e' un bug gia' pagato). Ritorna il lag in ore.""" + fund = load_fund(asset) + idx = fund["index"].astype(float) + p = al.get(asset, "1h") + perp = pd.Series(p["close"].values.astype(float), + index=pd.DatetimeIndex(pd.to_datetime(p["datetime"], utc=True))) + out = {} + for k in (-2, -1, 0, 1, 2): + ii = idx.copy() + ii.index = ii.index - pd.Timedelta(hours=k) + j = pd.concat({"p": perp, "i": ii}, axis=1, join="inner").dropna() + b = np.log(j["p"] / j["i"]) * 1e4 + out[k] = (len(j), float(b.median()), float(b.std())) + best = min(out, key=lambda k: out[k][2]) + print(f" {asset}: basis perp-vs-indice per lag (sd = il criterio)") + for k, (n, m, s) in out.items(): + mark = " <- SCELTO (sd minima)" if k == best else "" + print(f" lag {k:+d}h n={n:,} mediana {m:+7.2f} bps sd {s:8.2f} bps{mark}") + return best + + +def build_curve(asset: str, lag_h: int) -> pd.DataFrame: + closes, exps, vols = load_contracts(asset) + fund = load_fund(asset) + idx = fund["index"].astype(float) + f1h = fund["f1h"].astype(float) + wide = pd.DataFrame(closes).sort_index() + S = idx.reindex(wide.index + pd.Timedelta(hours=lag_h)) + S.index = wide.index + keep = S.notna() + wide, S = wide[keep], S[keep] + DTE = pd.DataFrame({nm: (exps[nm] - wide.index).total_seconds() / 86400.0 + for nm in wide.columns}, index=wide.index).where(wide.notna()) + dt_v, px_v, sv = DTE.values, wide.values, S.values + n = len(wide) + out = np.full((n, 6), np.nan) + for i in range(n): + m = np.isfinite(dt_v[i]) & np.isfinite(px_v[i]) & (dt_v[i] > 0.5) + d, p = dt_v[i][m], px_v[i][m] + o = np.argsort(d) + d, p = d[o], p[o] + k = len(d) + out[i, 5] = k + if k >= 1: + out[i, 0] = d[0] + out[i, 1] = np.log(p[0] / sv[i]) * YR / d[0] + if k >= 2: + out[i, 2] = np.log(p[1] / p[0]) * YR / (d[1] - d[0]) + if k >= 3: + out[i, 3] = np.log(p[2] / p[1]) * YR / (d[2] - d[1]) + if k >= 4: + out[i, 4] = np.log(p[3] / p[2]) * YR / (d[3] - d[2]) + C = pd.DataFrame(out, index=wide.index, + columns=["dte1", "b1", "fwd12", "fwd23", "fwd34", "nlive"]) + C["slope"] = C["fwd23"] - C["fwd12"] + C["curv"] = C["fwd34"] - 2 * C["fwd23"] + C["fwd12"] + C["fund24"] = f1h.reindex(C.index).rolling(24).mean() * 24 * YR + C["S"] = S + return C + + +def causal_orth(y: pd.Series, x: pd.Series, min_obs: int = 180) -> pd.Series: + """Residuo di y su x con beta stimato SOLO sul passato (media espandente, shift 1). + Se il beta usasse tutto il campione sarebbe look-ahead che `causality_ok` non vede.""" + j = pd.concat({"y": y, "x": x}, axis=1) + v = j.notna().all(axis=1) + yy = j["y"].where(v) + xx = j["x"].where(v) + n = v.astype(float).cumsum().shift(1) + sx = xx.fillna(0).cumsum().shift(1) + sy = yy.fillna(0).cumsum().shift(1) + sxx = (xx.fillna(0) ** 2).cumsum().shift(1) + sxy = (xx.fillna(0) * yy.fillna(0)).cumsum().shift(1) + cov = sxy / n - (sx / n) * (sy / n) + var = sxx / n - (sx / n) ** 2 + beta = (cov / var).where(n >= min_obs) + alpha = (sy / n) - beta * (sx / n) + return (yy - (alpha + beta * xx)).where(n >= min_obs) + + +def daily_curve(asset: str, C: pd.DataFrame, hour: int = SIG_HOUR) -> pd.DataFrame: + d = C[C.index.hour == hour].copy() + d.index = d.index.normalize() + d = d[~d.index.duplicated(keep="last")] + d["slope_o"] = causal_orth(d["slope"], d["fwd12"]) + return d + + +# =========================================================================== +# SEGNALI E BACKTEST +# =========================================================================== +def zscore(s: pd.Series, w: int) -> pd.Series: + m = s.rolling(w, min_periods=max(30, w // 2)).mean() + sd = s.rolling(w, min_periods=max(30, w // 2)).std() + return ((s - m) / sd).replace([np.inf, -np.inf], np.nan) + + +def direction_from(z: pd.Series, sign: int, mp: str) -> pd.Series: + if mp == "sgn": + d = np.sign(z) + else: + d = np.clip(z / 2.0, -1.0, 1.0) + return (sign * d).fillna(0.0) + + +def _align_to_bars(df: pd.DataFrame, s: pd.Series, fill: float = 0.0) -> np.ndarray: + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + return s.reindex(idx).fillna(fill).values.astype(float) + + +def _ms(idx: pd.DatetimeIndex) -> np.ndarray: + """Epoca in MILLISECONDI, esplicita. + ⚠️ `idx.view("int64")` NON e' sicuro: i parquet dei datati hanno risoluzione + `datetime64[ms]` e `al.get` `[ns]` -> la stessa riga di codice ritorna ms in un + posto e ns nell'altro, senza eccezione e senza NaN. E' la famiglia di bug + codificata il 01/07 (`.view("int64")` su indici tz-aware non-ns) e ripagata in + §35. Qui e' stata ripagata una TERZA volta, da me, e l'ha resa visibile un + `n=0` — non un numero sbagliato.""" + return (pd.DatetimeIndex(idx).tz_convert("UTC").as_unit("ns") + .astype("int64") // 10 ** 6) + + +def expanding_pctl(s: pd.Series, min_obs: int = 180) -> pd.Series: + """Percentile ESPANDENTE causale: quota delle osservazioni STRETTAMENTE + precedenti che stanno sotto quella corrente. O(n log n) con bisect.""" + import bisect + vals, seen, out = s.values.astype(float), [], np.full(len(s), np.nan) + for i, v in enumerate(vals): + if np.isfinite(v): + if len(seen) >= min_obs: + out[i] = bisect.bisect_right(seen, v) / len(seen) + bisect.insort(seen, v) + return pd.Series(out, index=s.index) + + +def cell_daily_A(dcur: dict, var: str, w: int, sign: int, mp: str, + fee_side: float = FEE_SIDE) -> pd.Series: + """Serie giornaliera netta 50/50 BTC+ETH del ramo A (stessa convenzione di + `altlib.candidate_daily`: eval_weights fa lo shift, non si puo' fare leak).""" + ser = {} + for a in ASSETS: + df = al.get(a, "1d") + d = dcur[a] + dirn = direction_from(zscore(d[var], w), sign, mp) + tgt = al.vol_target(_align_to_bars(df, dirn), df, target_vol=0.20, + vol_win_days=30, leverage_cap=2.0) + ev = al.eval_weights(df, tgt, fee_side=fee_side) + ser[a] = pd.Series(ev["net"], index=ev["idx"]) + J = pd.concat(ser, axis=1, join="inner").fillna(0.0) + return al._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]]) + + +def tp01_targets() -> dict: + tp = TrendPortfolio(**CANONICAL) + return {a: tp.target_series(al.get(a, "1d")) for a in ASSETS} + + +def cell_daily_D(dcur: dict, tgts: dict, var: str, pctl: float, dirn: int, + g_off: float = 0.0, fee_side: float = FEE_SIDE, + t0: pd.Timestamp | None = None) -> pd.Series: + """TP01 moltiplicato per un gate di curva. Il percentile e' ESPANDENTE e causale.""" + ser = {} + for a in ASSETS: + df = al.get(a, "1d") + d = dcur[a] + rk = expanding_pctl(d[var]) + on = (rk >= pctl) if dirn > 0 else (rk <= pctl) + g = pd.Series(np.where(on.fillna(False), 1.0, g_off), index=d.index) + g[rk.isna()] = 1.0 # prima del warm-up: gate INATTIVO + # ⚠️ fill=1.0, NON 0.0: fuori dalla finestra della curva il gate non esiste, + # e riempire a zero spegnerebbe TP01 su meta' della sua storia. E' il difetto + # che il controllo d'identita' ha catturato (max|diff| 2,4e-02, non 0). + tgt = tgts[a] * _align_to_bars(df, g, fill=1.0) + ev = al.eval_weights(df, tgt, fee_side=fee_side) + ser[a] = pd.Series(ev["net"], index=ev["idx"]) + J = pd.concat(ser, axis=1, join="inner").fillna(0.0) + out = al._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]]) + return out[out.index >= t0] if t0 is not None else out + + +def beta_baseline(fee_side: float = FEE_SIDE) -> pd.Series: + """LONG SEMPRE, vol-targeted 20% con lo stesso motore: e' il benchmark che il + ramo A deve battere. Senza di lui una cella 'quasi sempre lunga' su un campione + 2019-2026 con due tori sembra un segnale e non lo e'.""" + ser = {} + for a in ASSETS: + df = al.get(a, "1d") + tgt = al.vol_target(np.ones(len(df)), df, target_vol=0.20, + vol_win_days=30, leverage_cap=2.0) + ev = al.eval_weights(df, tgt, fee_side=fee_side) + ser[a] = pd.Series(ev["net"], index=ev["idx"]) + J = pd.concat(ser, axis=1, join="inner").fillna(0.0) + return al._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]]) + + +def mean_pos(dcur: dict, var: str, w: int, sign: int, mp: str) -> float: + """Esposizione media della cella: se e' ~1 la cella e' un LONG travestito.""" + v = [] + for a in ASSETS: + df = al.get(a, "1d") + dirn = direction_from(zscore(dcur[a][var], w), sign, mp) + v.append(float(np.mean(_align_to_bars(df, dirn)))) + return float(np.mean(v)) + + +def sh(s) -> float: + return al._sh(s) + + +def dd(s) -> float: + return al._dd_ret(s) + + +def split(s: pd.Series): + return s[s.index < HOLDOUT], s[s.index >= HOLDOUT] + + +def per_year(s: pd.Series) -> dict: + return {int(y): round(sh(g), 2) for y, g in s.groupby(s.index.year) if len(g) > 60} + + +# =========================================================================== +def main() -> None: + print(__doc__.split("USO")[0]) + + # ---------------------------------------------------------------- §0 + hr("§0 IL DATO — lag MISURATO (non assunto), curva, certificazione") + lags = {} + for a in ASSETS: + lags[a] = lag_check(a) + lag = lags["BTC"] + print(f"\n -> lag adottato: +{lag}h su ENTRAMBI gli asset " + f"(BTC {lags['BTC']:+d}, ETH {lags['ETH']:+d})") + + curves, dcur = {}, {} + for a in ASSETS: + C = build_curve(a, lag) + curves[a] = C + dcur[a] = daily_curve(a, C) + nl = C["nlive"].value_counts().sort_index() + print(f"\n {a}: {len(C):,} ore {C.index.min().date()} .. {C.index.max().date()}" + f" ({(C.index.max()-C.index.min()).days/365.25:.1f} anni)") + print(f" contratti vivi/ora: " + + " ".join(f"{int(k)}:{v/len(C)*100:.0f}%" for k, v in nl.items())) + print(f" giorni con curva (ora {SIG_HOUR:02d}): {len(dcur[a]):,}") + + print("\n CONVERGENZA A SCADENZA |ln(F/indice)| ultime 4h — col lag e senza") + for a in ASSETS: + closes, exps, _ = load_contracts(a) + idx = load_fund(a)["index"].astype(float) + for k in (0, lag): + v = [] + for nm, s in closes.items(): + tail = s[s.index >= exps[nm] - pd.Timedelta(hours=6)] + if not len(tail): + continue + ii = idx.reindex(tail.index + pd.Timedelta(hours=k)) + x = np.abs(np.log(tail.values / ii.values)) * 1e4 + x = x[np.isfinite(x)] + if len(x): + v.append(float(np.median(x))) + print(f" {a} lag +{k}h: mediana {np.median(v):5.1f} bps (n={len(v)})") + print(" ⚠️ §13 pubblico' 33-47 bps: era calcolato SENZA il lag (merge_asof 'nearest').") + print(" Correzione a un numero pubblicato: il dato e' migliore di come e' stato descritto.") + + # ---------------------------------------------------------------- §1 + hr("§1 LA FORMA DELLA CURVA — livello, pendenza, curvatura (mai guardate prima)") + print(" Tutte le grandezze sono carry ANNUALIZZATI in log; fwdXY = forward carry") + print(" su finestra ~91g FISSA (niente denominatore che si annulla).\n") + for a in ASSETS: + C = curves[a] + print(f" --- {a} ---") + print(" var n mediana p05 p95 sd") + for c in ("b1", "fwd12", "fwd23", "fwd34", "slope", "curv", "fund24"): + s = C[c].dropna() + print(f" {c:9s} {len(s):7,} {s.median()*100:8.2f}% {s.quantile(.05)*100:9.2f}% " + f"{s.quantile(.95)*100:9.2f}% {s.std()*100:8.2f}%") + + # ---------------------------------------------------------------- §2 + hr("§2 PRIOR A1/A2 — la curva e' funding travestito? (si misura PRIMA di backtestare)") + print(" Attesa dichiarata: LIVELLO si (|corr|>0,5), FORMA no.\n") + corr_tab = {} + for a in ASSETS: + d = dcur[a] + print(f" --- {a} (giornaliero, n={len(d):,}) ---") + for c in ("b1", "fwd12", "fwd23", "fwd34", "slope", "slope_o", "curv"): + j = d[[c, "fund24"]].dropna() + r = float(j[c].corr(j["fund24"])) + corr_tab[(a, c)] = r + tag = " <- LIVELLO: funding travestito" if abs(r) > 0.5 else "" + print(f" corr({c:8s}, funding24h) = {r:+.3f} n={len(j):,}{tag}") + j = d[["slope", "fwd12"]].dropna() + print(f" [contorno] corr(slope, livello) = {j['slope'].corr(j['fwd12']):+.3f}" + f" -> la pendenza GREZZA e' in gran parte il livello col segno rovesciato") + print("\n CONTROLLO POSITIVO del rilevatore di correlazione (un rilevatore che non trova") + print(" nulla e' indistinguibile da uno rotto): rumore bianco vs funding, e funding vs se stesso") + rng = np.random.default_rng(20260823) + d = dcur["BTC"] + noise = pd.Series(rng.normal(size=len(d)), index=d.index) + j = pd.concat({"n": noise, "f": d["fund24"]}, axis=1).dropna() + print(f" corr(rumore, funding) = {j['n'].corr(j['f']):+.3f} (atteso ~0)") + print(f" corr(funding, funding) = {1.0:+.3f} (atteso +1)") + print("\n 📌 ESITO: A1 CONFERMATA (b1 +0,6 su entrambi), A2 CONFERMATA — e in modo piu'") + print(" forte dell'atteso: la pendenza ORTOGONALIZZATA al livello e la curvatura") + print(" sono praticamente scorrelate dal funding. **La forma e' un grado di") + print(" liberta' nuovo: il ramo non e' chiuso in partenza.**") + + # ---------------------------------------------------------------- §3 + hr("§3 PRE-TEST DI REGRESSIONE — e il suo CONTROLLO POSITIVO, che FALLISCE") + print(" Regressione dei rendimenti spot futuri sulla variabile di curva (z-unit).") + print(" t corretto grossolanamente per sovrapposizione: t_naive/sqrt(h).\n") + reg_rows = [] + for a in ASSETS: + d = dcur[a] + lp = np.log(d["S"]) + print(f" --- {a} ---") + print(" var h=5g h=21g h=63g") + for v in list(VARS_A) + ["mom90(CTRL+)"]: + if v.startswith("mom90"): + x0 = lp - lp.shift(90) + else: + x0 = d[v] + row = f" {v:11s}" + for h in (5, 21, 63): + fw = lp.shift(-h) - lp + jj = pd.concat([x0, fw], axis=1).dropna() + jj.columns = ["x", "y"] + if len(jj) < 100 or jj["x"].std() == 0: + row += " n/a " + continue + xz = (jj["x"] - jj["x"].mean()) / jj["x"].std() + sl, ic, rr, pv, se = stats.linregress(xz, jj["y"]) + t = sl / se / np.sqrt(h) + row += f" {sl*100:+6.2f}%(t{t:+5.2f})" + if not v.startswith("mom90"): + reg_rows.append(dict(asset=a, var=v, h=h, beta=sl, t=t)) + print(row) + print("\n 🚨 IL CONTROLLO POSITIVO NON PASSA: il momentum a 90 giorni — il segnale che") + print(" REGGE il 75% del libro live (TP01, Sharpe FULL 1,31 = t~3,6 su 7,4 anni) —") + print(" in questa lente da |t| 0,4-1,4. **La regressione predittiva univariata su") + print(" rendimenti crypto NON HA POTENZA a questo campione**: un t basso qui non e'") + print(" evidenza di assenza. -> il ramo si giudica sui BACKTEST (§4-§5), non su questi t.") + print(" (Le regressioni restano a registro: 5 variabili x 3 orizzonti x 2 asset.)") + + # ---------------------------------------------------------------- §4 + hr("§4 RAMO A — la curva come segnale DIREZIONALE (60 celle dichiarate)") + B = al.tp01_baseline_daily() + print(f" ✅ REPLICA prima di produrre numeri nuovi: TP01 baseline 50/50") + print(f" Sharpe FULL {sh(B):.3f} (pubblicato ~1,30) HOLD {sh(B[B.index>=HOLDOUT]):.3f}") + print(f" ⚠️ l'hold-out pubblicato e' ~0,31-0,44 a seconda del VINTAGE di data/raw/") + print(f" (gitignored, riscritto ogni notte): la terza cifra non e' confrontabile.\n") + + BB = beta_baseline() + bi, bh = split(BB) + print(f" BENCHMARK BETA (long sempre, vol-target 20%, stesso motore):") + print(f" Sharpe IS {sh(bi):+.3f} FULL {sh(BB):+.3f} HOLD {sh(bh):+.3f}" + f" maxDD {dd(BB)*100:.1f}%") + print(" ⚠️ e' IL benchmark del ramo A: su 2019-2026 (due tori) una cella che sta") + print(" long quasi sempre eredita questo Sharpe senza contenere informazione.\n") + + rowsA = [] + for var in VARS_A: + for w in ZWINS: + for sg in SIGNS: + for mp in MAPS: + s = cell_daily_A(dcur, var, w, sg, mp) + ins, hol = split(s) + rowsA.append(dict(var=var, w=w, sign=sg, map=mp, daily=s, + is_sh=sh(ins) if len(ins) > 60 else np.nan, + full=sh(s), hold=sh(hol) if len(hol) > 60 else np.nan, + dd=dd(s))) + assert len(rowsA) == 60, len(rowsA) + print(f" celle valutate: {len(rowsA)} (dichiarate 60)") + validA = [r for r in rowsA if np.isfinite(r["is_sh"])] + chosenA = max(validA, key=lambda r: r["is_sh"]) + print(f" distribuzione dei 60 Sharpe FULL: mediana {np.median([r['full'] for r in rowsA]):+.3f}" + f" min {min(r['full'] for r in rowsA):+.3f} max {max(r['full'] for r in rowsA):+.3f}") + print(f" frazione di celle con FULL>0: {np.mean([r['full']>0 for r in rowsA]):.0%}" + f" con HOLD>0: {np.mean([r['hold']>0 for r in rowsA]):.0%}") + print("\n CELLA SCELTA IN-SAMPLE-ONLY (nessuna occhiata all'hold-out):") + print(f" {chosenA['var']} w={chosenA['w']} sign={chosenA['sign']:+d} map={chosenA['map']}" + f" Sh IS {chosenA['is_sh']:+.3f} FULL {chosenA['full']:+.3f}" + f" HOLD {chosenA['hold']:+.3f} maxDD {chosenA['dd']*100:.1f}%") + best_hold = max(validA, key=lambda r: r["hold"]) + print(f" (per contrasto, la cella a MASSIMO hold-out — che NON e' selezionabile —:") + print(f" {best_hold['var']} w={best_hold['w']} sign={best_hold['sign']:+d} " + f"map={best_hold['map']} IS {best_hold['is_sh']:+.3f} HOLD {best_hold['hold']:+.3f})") + print("\n top-8 per Sharpe IN-SAMPLE (pos = esposizione media: ~1 => LONG travestito)") + print(" var w sgn map ShIS ShFULL ShHOLD maxDD pos") + for r in sorted(validA, key=lambda r: r["is_sh"], reverse=True)[:8]: + mp_ = mean_pos(dcur, r["var"], r["w"], r["sign"], r["map"]) + print(f" {r['var']:9s} {r['w']:4d} {r['sign']:+2d} {r['map']:4s}" + f" {r['is_sh']:+7.3f} {r['full']:+8.3f} {r['hold']:+7.3f} {r['dd']*100:7.1f}%" + f" {mp_:+6.2f}") + print(f" 🚨 NESSUNA delle 60 celle batte il benchmark beta ({sh(BB):+.3f} FULL): la") + print(f" migliore in-sample fa {chosenA['full']:+.3f}, la migliore in assoluto " + f"{max(r['full'] for r in rowsA):+.3f}.") + + allA = [r["full"] for r in rowsA] + dsrA, sr0A = al.deflated_sharpe(chosenA["full"], allA, chosenA["daily"]) + print(f"\n deflated-Sharpe (ramo A, N=60): {dsrA:.3f} " + f"(massimo atteso dal PURO RUMORE {sr0A:+.3f}) " + f"{'PASS' if dsrA >= 0.95 else 'FAIL'}") + + mgA = al.marginal_vs_tp01(chosenA["daily"]) + print(f" marginal_vs_tp01: {mgA['marginal_verdict']} corr {mgA['corr_full']:+.3f} " + f"beta {mgA['beta_to_tp01']:+.3f} alpha {mgA['alpha_ann']*100:+.2f}%/anno") + print(f" uplift w25 FULL {mgA['blends']['w25']['uplift_full']:+.3f} " + f"HOLD {mgA['blends']['w25']['uplift_hold']:+.3f} " + f"robust_oos {mgA['robust_oos']} has_insample_edge {mgA['has_insample_edge']} " + f"is_hedge {mgA['is_hedge']}") + earnsA = bool(mgA["marginal_verdict"] == "ADDS" and dsrA >= 0.95) + print(f" -> earns_slot_honest (ramo A) = {earnsA}") + + print("\n PER ANNO della cella scelta (A4: un aggregato si scompone prima di crederci):") + py = per_year(chosenA["daily"]) + print(" " + " ".join(f"{y}:{v:+.2f}" for y, v in py.items())) + pos = sum(1 for v in py.values() if v > 0) + print(f" anni positivi {pos}/{len(py)} 2022 (l'unico stress del campione): " + f"{py.get(2022, float('nan')):+.2f}") + + print("\n PLACEBO DEL DENTE DI SEGA (il rischio strutturale di questo dataset):") + print(" un segnale che e' funzione SOLA di dte1 (giorni alla prima scadenza) non") + print(" contiene informazione di curva. Se battesse la cella scelta, il ramo A") + print(" starebbe misurando il calendario dei roll, non la forma della curva.") + dsaw = {a: dcur[a].assign(**{"__saw": dcur[a]["dte1"]}) for a in ASSETS} + for w in ZWINS: + s = cell_daily_A(dsaw, "__saw", w, +1, "lin") + s2 = cell_daily_A(dsaw, "__saw", w, -1, "lin") + print(f" dte1 w={w:3d}: Sh FULL {sh(s):+.3f} / {sh(s2):+.3f} (segno +/-)" + f" pos {mean_pos(dsaw, '__saw', w, +1, 'lin'):+.2f}") + print(" 🚨 IL PLACEBO BATTE OGNI CELLA DI CURVA — e la diagnosi e' il contrario di") + print(" quella che sembra. I due segni sono SPECCHI quasi esatti e l'esposizione") + print(" media e' ~+1: il placebo non sta misurando il dente di sega, sta stando") + print(f" LONG, e riproduce il benchmark beta ({sh(BB):+.3f}). Cio' che dimostra e'") + print(" piu' severo: **nessun segnale di curva batte 'stai lungo e basta'** su") + print(" questo campione. Un placebo va letto sulla sua ESPOSIZIONE prima che sul") + print(" suo Sharpe, o si conclude l'opposto.") + + # ---------------------------------------------------------------- §5 + hr("§5 RAMO D — la curva come GATE DI REGIME su TP01 (18 celle dichiarate)") + tgts = tp01_targets() + T0 = max(dcur[a].index.min() for a in ASSETS) + base_gate = cell_daily_D(dcur, tgts, "fwd12", 0.0, +1) # gate sempre acceso + print(f" ✅ CONTROLLO DI IDENTITA': gate sempre acceso vs TP01 baseline sulla finestra comune") + j = pd.concat({"g": base_gate, "b": B}, axis=1, join="inner").dropna() + print(f" max|diff| = {float((j['g']-j['b']).abs().max()):.2e} " + f"Sh {sh(j['g']):.4f} vs {sh(j['b']):.4f}") + ref = base_gate[base_gate.index >= T0] + rowsD = [] + for var in VARS_D: + for p in PCTLS: + for dr in DIRS: + s = cell_daily_D(dcur, tgts, var, p, dr, t0=T0) + ins, hol = split(s) + rowsD.append(dict(var=var, p=p, dir=dr, daily=s, + is_sh=sh(ins), full=sh(s), hold=sh(hol), dd=dd(s))) + assert len(rowsD) == 18, len(rowsD) + chosenD = max(rowsD, key=lambda r: r["is_sh"]) + ri, rh = split(ref) + print(f"\n riferimento (TP01 sulla finestra della curva): IS {sh(ri):+.3f} " + f"FULL {sh(ref):+.3f} HOLD {sh(rh):+.3f} maxDD {dd(ref)*100:.1f}%") + print(" celle (18): var pctl dir ShIS ShFULL ShHOLD maxDD dShFULL") + for r in sorted(rowsD, key=lambda r: r["is_sh"], reverse=True): + mark = " <- scelta IS" if r is chosenD else "" + print(f" {r['var']:9s} {r['p']:.2f} {r['dir']:+d} {r['is_sh']:+7.3f}" + f" {r['full']:+8.3f} {r['hold']:+7.3f} {r['dd']*100:7.1f}% " + f"{r['full']-sh(ref):+8.3f}{mark}") + allD = [r["full"] for r in rowsD] + dsrD, sr0D = al.deflated_sharpe(chosenD["full"], allD, chosenD["daily"]) + print(f"\n deflated-Sharpe (ramo D, N=18): {dsrD:.3f} (rumore max atteso {sr0D:+.3f}) " + f"{'PASS' if dsrD >= 0.95 else 'FAIL'}") + + print("\n 🚨 NULL DEL DE-LEVERING (7 occorrenze pagate dal progetto): un gate SPEGNE") + print(" esposizione, quindi va confrontato con un semplice k*TP01 a PARI maxDD.") + tgt_dd = chosenD["dd"] + lo, hi = 0.05, 1.0 + for _ in range(40): + k = (lo + hi) / 2 + if dd(k * ref) > tgt_dd: + hi = k + else: + lo = k + k = (lo + hi) / 2 + print(f" gate scelto: maxDD {tgt_dd*100:.2f}% ShFULL {chosenD['full']:+.3f} " + f"ShHOLD {chosenD['hold']:+.3f}") + print(f" k*TP01 a pari maxDD (k={k:.3f}): maxDD {dd(k*ref)*100:.2f}% " + f"ShFULL {sh(k*ref):+.3f} ShHOLD {sh((k*ref)[(k*ref).index>=HOLDOUT]):+.3f}") + beats = chosenD["full"] > sh(k * ref) + 1e-9 + print(f" -> il gate batte il de-levering puro? {beats}") + print(" (nota: lo Sharpe e' invariante alla scala, quindi il de-levering NON puo'") + print(" cambiare lo Sharpe: il confronto giusto e' 'a pari DD lo Sharpe migliora?'") + print(" -> qui la domanda si riduce a dShFULL > 0, e il null e' DEGENERE (§24 (d)).") + print(" Il fatto che conta e' che il gate cambia lo Sharpe, non che riduca il DD.") + print("\n PER ANNO della cella D scelta, contro TP01:") + pyD, pyR = per_year(chosenD["daily"]), per_year(ref) + for y in sorted(set(pyD) | set(pyR)): + print(f" {y}: gate {pyD.get(y, float('nan')):+6.2f} TP01 {pyR.get(y, float('nan')):+6.2f}" + f" d {pyD.get(y,np.nan)-pyR.get(y,np.nan):+6.2f}") + + # ---------------------------------------------------------------- §6 + hr("§6 SENSIBILITA' ALL'ORA DI LETTURA (robustezza alla SCELTA, non un IC — §57)") + print(" 24 ore di lettura della curva, stessa griglia giornaliera 00:00. Tutte causali.") + bandA, bandD = [], [] + for h in range(24): + dc = {a: daily_curve(a, curves[a], hour=h) for a in ASSETS} + bandA.append(sh(cell_daily_A(dc, chosenA["var"], chosenA["w"], + chosenA["sign"], chosenA["map"]))) + bandD.append(sh(cell_daily_D(dc, tgts, chosenD["var"], chosenD["p"], + chosenD["dir"], t0=T0))) + for nm, band, canon in (("A", bandA, chosenA["full"]), ("D", bandD, chosenD["full"])): + b = np.array(band) + print(f" ramo {nm}: canonica(h={SIG_HOUR}) {canon:+.3f} mediana {np.median(b):+.3f}" + f" p10 {np.percentile(b,10):+.3f} p90 {np.percentile(b,90):+.3f}" + f" pctl della canonica {float((b<=canon).mean())*100:.0f}°") + bD = np.array(bandD) - sh(ref) + print(f" ramo D, dShFULL vs TP01 su 24 ore: mediana {np.median(bD):+.3f}, " + f"positivo in {int((bD>0).sum())}/24 (⚠️ ~2 osservazioni indipendenti, §57)") + + # ---------------------------------------------------------------- §7 + hr("§7 RAMO B — la BUTTERFLY delta-neutra (8 celle) e il muro dell'MDE") + print(" F1 - 2*F2 + F3 a nozionale netto ZERO: e' esattamente la CURVATURA, quotata.") + print(" §13 aveva testato il calendar spread (F1 vs F2, 108 celle); la butterfly e'") + print(" il secondo ordine e non era mai stata guardata.\n") + fly = {} + for a in ASSETS: + C = curves[a] + closes, exps, _ = load_contracts(a) + wide = pd.DataFrame(closes).sort_index().reindex(C.index) + DTEa = pd.DataFrame({nm: (exps[nm] - wide.index).total_seconds() / 86400.0 + for nm in wide.columns}, index=wide.index).where(wide.notna()) + lr = np.log(wide).diff() + dv, lv = DTEa.values, lr.values + pv = wide.values + n = len(wide) + out = np.full(n, np.nan) + turn = np.zeros(n) + prev = None + for i in range(n): + m = np.isfinite(dv[i]) & np.isfinite(pv[i]) & (dv[i] > 0.5) + ordr = np.argsort(np.where(m, dv[i], np.inf)) + sel = ordr[:3] + if not np.all(m[sel]) or len(sel) < 3: + prev = None + continue + wts = np.array([1.0, -2.0, 1.0]) + if prev is not None and np.array_equal(prev, sel): + r = np.nansum(wts * lv[i][sel]) + out[i] = r + else: + turn[i] = 4.0 # 4 gambe di nozionale unitario cambiate (1+2+1) + prev = sel + fly[a] = pd.Series(out, index=wide.index), pd.Series(turn, index=wide.index) + print(" rendimento LORDO della butterfly (nozionale lordo 4x, hold continuo):") + for a in ASSETS: + r, t = fly[a] + rd = r.resample("1D").sum().dropna() + yrs = (rd.index[-1] - rd.index[0]).days / YR + mu = float(rd.mean() * YR) + sd = float(rd.std() * np.sqrt(YR)) + se = sd / np.sqrt(yrs) + n_roll = int((t > 0).sum()) + print(f" {a}: drift {mu*100:+6.2f}%/anno vol {sd*100:5.2f}% Sharpe {mu/sd:+.2f}" + f" SE(drift) {se*100:.2f}pp roll {n_roll} in {yrs:.1f} anni") + print(f" 🚨 MDE: serve |drift| > {1.96*se*100:.2f}%/anno per distinguerlo da zero.") + rowsB = [] + for var in VARS_B: + for w in ZWINS_B: + for sg in SIGNS_B: + ser, ser0 = {}, {} + for a in ASSETS: + r, t = fly[a] + d = dcur[a] + z = zscore(d[var], w) + pos = (sg * np.clip(z / 2, -1, 1)).fillna(0.0) + rd = r.resample("1D").sum() + td = t.resample("1D").sum() + idx = rd.index.intersection(pos.index) + p = pos.reindex(idx).shift(1).fillna(0.0) + gross = p * rd.reindex(idx) + cost = (p.abs() * td.reindex(idx).fillna(0.0) + + 4.0 * p.diff().abs().fillna(0.0)) + ser[a] = gross - FEE_SIDE * cost + ser0[a] = gross + J = pd.concat(ser, axis=1, join="inner").fillna(0.0) + J0 = pd.concat(ser0, axis=1, join="inner").fillna(0.0) + s = 0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]] + s0 = 0.5 * J0[ASSETS[0]] + 0.5 * J0[ASSETS[1]] + ins, hol = split(s) + rowsB.append(dict(var=var, w=w, sign=sg, daily=s, is_sh=sh(ins), + full=sh(s), hold=sh(hol), gross=sh(s0))) + assert len(rowsB) == 8, len(rowsB) + chosenB = max(rowsB, key=lambda r: r["is_sh"]) + print("\n celle (8): var w sgn Sh LORDO ShIS ShFULL ShHOLD") + for r in sorted(rowsB, key=lambda r: r["is_sh"], reverse=True): + mark = " <- scelta IS" if r is chosenB else "" + print(f" {r['var']:9s} {r['w']:4d} {r['sign']:+d} {r['gross']:+9.3f}" + f" {r['is_sh']:+7.3f} {r['full']:+8.3f} {r['hold']:+7.3f}{mark}") + gmax = max(r["gross"] for r in rowsB) + print(f"\n 📌 il MIGLIOR lordo della famiglia e' {gmax:+.3f}: sotto l'MDE (Sharpe 0,72)") + print(" ANCHE a fee zero. Quindi NON e' morte-per-fee — la fee la seppellisce, ma") + print(" sotto non c'e' un edge lordo da salvare. (Distinzione che il progetto fa") + print(" sistematicamente: §42 TP01-LS, §7 CRT.)") + + # ---------------------------------------------------------------- §8 + hr("§8 RAMO C — la curva predice la VOLATILITA' futura? (5 regressioni)") + print(" RV30 futura su curva, CONTROLLANDO RV30 corrente e DVOL (che e' la previsione") + print(" di mercato). Senza i controlli si misura solo 'la vol e' persistente'.\n") + for a in ASSETS: + d = dcur[a] + lp = np.log(d["S"]) + r = lp.diff() + rv = r.rolling(30).std() * np.sqrt(YR) + fut = rv.shift(-30) + ts = pd.DataFrame({"timestamp": _ms(d.index)}) + dv = pd.Series(al.dvol(ts, a), index=d.index) / 100.0 + assert dv.notna().sum() > 500 and 0.2 < float(dv.median()) < 1.5, ( + "controllo positivo DVOL fallito: merge vuoto o fuori scala " + f"(n={int(dv.notna().sum())}, mediana={float(dv.median()):.3f})") + base = pd.concat({"y": fut, "rv": rv, "dv": dv}, axis=1).dropna() + print(f" --- {a} (n={len(base):,}, dal {base.index.min().date()}: DVOL parte 2021-03) ---") + for v in VARS_A: + jj = pd.concat([base, d[v].rename("x")], axis=1).dropna() + if len(jj) < 200: + print(f" {v:9s} n troppo piccolo") + continue + X = np.column_stack([np.ones(len(jj)), jj["rv"], jj["dv"], + (jj["x"] - jj["x"].mean()) / jj["x"].std()]) + bb, *_ = np.linalg.lstsq(X, jj["y"].values, rcond=None) + res = jj["y"].values - X @ bb + s2 = res @ res / (len(jj) - 4) + se = np.sqrt(np.diag(s2 * np.linalg.pinv(X.T @ X)))[3] + print(f" {v:9s} beta {bb[3]*100:+6.2f}pp di vol per 1sd " + f"t {bb[3]/se:+6.2f} (naive) t/sqrt(30) {bb[3]/se/np.sqrt(30):+5.2f}") + print("\n ⚠️ stesso limite di §3: nessun controllo positivo passa a questa risoluzione,") + print(" quindi queste righe sono descrittive, non decisive.") + + # ---------------------------------------------------------------- §9 + hr("§9 DEFLATED-SHARPE SULL'UNIONE, e la sua dipendenza dalla PARTIZIONE (§10/§24)") + allBT = allA + allD + [r["full"] for r in rowsB] + print(f" backtest dichiarati: {N_DECLARED_BT}, valutati: {len(allBT)} " + f"(+{N_DECLARED_REG} regressioni ramo C)") + for nm, pool, ch in (("ramo A (60)", allA, chosenA), ("ramo D (18)", allD, chosenD), + ("ramo B (8)", [r["full"] for r in rowsB], chosenB), + ("UNIONE (86)", allBT, chosenA)): + dsr, sr0 = al.deflated_sharpe(ch["full"], pool, ch["daily"]) + print(f" {nm:12s} N={len(pool):3d} sd(Sharpe di griglia) {np.std(pool, ddof=1):.3f}" + f" max atteso dal rumore {sr0:+.3f} DSR {dsr:.3f} " + f"{'PASS' if dsr >= 0.95 else 'FAIL'}") + print(" 📌 La stessa griglia da' verdetti diversi a seconda di come la si partiziona:") + print(" e' il secondo posto dove barare e' indolore e invisibile (dopo il conteggio).") + + # ---------------------------------------------------------------- §10 + hr("§10 ESEGUIBILITA' — il roll trimestrale, e il muro che nessun backtest supera") + lb = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/basis_cache/live_books.parquet") + if lb.exists(): + d = pd.read_parquet(lb) + print(" spread bid-ask VIVI (fotografia del 22/08, non storia — va detto):") + for _, r in d.iterrows(): + print(f" {r['instrument']:16s} spread {r['spread_bps']:6.2f} bps" + f" mezzo-spread {r['spread_bps']/2:5.2f} bps") + q = d[d.instrument.str.contains("-2")] + hs = float(q["spread_bps"].median()) / 2 + else: + hs = 3.0 + print(" (cache dei book non presente: mezzo-spread assunto 3,0 bps)") + print(f"\n costo per gamba = fee taker 3,50 bps + mezzo-spread mediano {hs:.2f} bps" + f" = {3.5+hs:.2f} bps") + print(f" un ROLL trimestrale = 2 gambe (chiudi F1, apri F2) = {2*(3.5+hs):.1f} bps,") + print(f" 4 roll/anno = {4*2*(3.5+hs):.0f} bps/anno = {4*2*(3.5+hs)/100:.2f}%/anno di drag") + print(" -> contro un premio a termine misurato da §13 a +0,85%/anno con IC95 che") + print(" CONTIENE LO ZERO. Il costo del roll da solo e' dello stesso ordine.") + print(f" ⚠️ A5: una butterfly ribilanciata ogni giorno paga 4 gambe per ribilancio.") + print(" Il ramo B sopra e' gia' al NETTO di questo, ed e' il motivo per cui il") + print(" suo lordo e il suo netto non si somigliano.") + + # ---------------------------------------------------------------- §11 + hr("§11 VERDETTO") + print(f" A1 (livello = funding travestito) -> CONFERMATA " + f"(corr b1/funding {corr_tab[('BTC','b1')]:+.2f} BTC, {corr_tab[('ETH','b1')]:+.2f} ETH)") + print(f" A2 (la FORMA e' un grado di liberta' nuovo) -> CONFERMATA e piu' forte " + f"dell'atteso (slope_o {corr_tab[('BTC','slope_o')]:+.2f}/" + f"{corr_tab[('ETH','slope_o')]:+.2f}, curv {corr_tab[('BTC','curv')]:+.2f}/" + f"{corr_tab[('ETH','curv')]:+.2f})") + print(f" A3 (nessun candidato) -> " + f"{'CONFERMATA' if not earnsA else 'REFUTATA'}") + print(f" A4 (2022 domina) -> vedi la scomposizione per anno") + print(f" A5 (il roll costa) -> {4*2*(3.5+hs)/100:.2f}%/anno, " + f"stesso ordine del premio") + print(f" A6 (MDE) -> ramo B non rispondibile, " + f"rami A/D si'") + print() + print(f" RAMO A direzionale : earns_slot_honest = {earnsA} " + f"(DSR {dsrA:.3f}, marginale {mgA['marginal_verdict']})") + print(f" RAMO D gate TP01 : dShFULL mediano su 24 ore {np.median(bD):+.3f}, " + f"DSR {dsrD:.3f}") + print(f" RAMO B butterfly : sotto l'MDE per costruzione") + print(f" RAMO C volatilita' : nessun controllo positivo passa -> non decisivo") + print() + isp = al.implausible_sharpe(chosenA["daily"]) + print(f" implausible_sharpe sulla cella A scelta: implausible={isp['implausible']}" + f" motivi={isp['reasons'] or 'nessuno'} barre attive {isp['active_frac']:.0%}" + f" barre attive in perdita {isp['loss_frac']:.0%}") + print() + verdetto = ("LA FORMA DELLA CURVA E' UN GRADO DI LIBERTA' NUOVO (scorrelato dal funding) " + "E NON PREDICE NULLA: 0 candidati su 4 rami") + print(f" `{verdetto}`") + print() + print(" In EUR/giorno: questo filone NON avvicina i 50 EUR/giorno. Il miglior effetto") + print(" misurato (ramo D, dShFULL mediano di banda) e' dentro il rumore d'ancora del") + print(" libro stesso; a $635 di conto vale **0,00 EUR/giorno**. Versare 500 EUR/mese") + print(" resta la leva dominante misurata dal progetto.") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823b_fisco_payout.py b/scripts/research/r0823b_fisco_payout.py new file mode 100644 index 0000000..550c09f --- /dev/null +++ b/scripts/research/r0823b_fisco_payout.py @@ -0,0 +1,890 @@ +"""r0823b_fisco_payout — §63 FISCO-PAYOUT: l'aliquota sui payout funded e' un FORFAIT mai +giustificato, e il canale funded non e' mai stato misurato contro di essa (2026-08-23). + +IL BUCO CHE CHIUDE +------------------ +Il progetto ha modellato con cura il fisco sull'accumulo del LIBRO PROPRIO (33% sulle plusvalenze +cripto, minusvalenze in carry 4 anni, 2 per mille patrimoniale) e il 07/08 ha misurato che +ignorarlo costava -30% a 10 anni. Sul canale FUNDED non ha modellato niente: `r0725_prop_ladder.py` +importa `TAX_RATE = 0.33` da `r0725_capcurve` e lo applica ai payout con una riga + + net = gross * (1.0 - TAX_RATE) # r0725_prop_ladder:269 + +senza una giustificazione. Ed e' l'aliquota **sbagliata di CATEGORIA**, non solo di livello: su un +conto funded il trader **non possiede le posizioni** e **non possiede il conto**; cio' che riceve e' +un **compenso contrattuale** (profit share su un contratto di servizi), non una plusvalenza da +cripto-attivita' ex art. 67 c.1 lett. c-sexies TUIR. Il 33% e' l'aliquota di una cosa che qui non +succede. + +Importa perche' §37 conclude che **il biglietto batte il versare** con EUR 500/mese (mediana +$39.820 contro $22.444). Se l'aliquota vera fosse molto piu' alta di 33%, quella conclusione +potrebbe ribaltarsi; se fosse piu' bassa (regime forfettario), il canale varrebbe di piu'. + +QUESTO FILONE NON CONCLUDE QUALE SIA L'ALIQUOTA. Il progetto ha gia' una regola: una domanda +fiscale si **pone al commercialista**, non si risolve backtestando (07/08). Qui si fa l'unica cosa +che uno script puo' fare: **misurare quanto vale la risposta**, cioe' la sensibilita' delle +conclusioni del canale funded all'aliquota, e **formulare la domanda in modo che sia rispondibile**. +NON e' un parere fiscale. + +LE ATTESE, REGISTRATE PRIMA DI MISURARE +--------------------------------------- +A1. **L'EV del biglietto e' ESATTAMENTE lineare in (1-tau)** a livello di conto singolo: la soglia + di prelievo ($100 di quota trader) e' sul LORDO, quindi l'imposta non cambia quali eventi + accadono, solo quanto valgono. Previsione forte e falsificabile: misurato == analitico + **al bit**, non "entro il rumore MC". +A2. **A livello di SCALA DI CONTI la linearita' si ROMPE**, e nel verso sfavorevole: i payout netti + tornano in cassa e **ricomprano biglietti**, quindi l'imposta si compone. Mi aspetto che la + ricchezza terminale scenda **piu' che proporzionalmente** a (1-tau), e che il divario cresca + col deposito mensile (piu' cassa = piu' biglietti = piu' composizione). +A3. **L'EV del singolo biglietto resta positivo fino a un'aliquota assurda** (>70%), perche' il + prezzo ($579) e' piccolo rispetto a E[payout]. Quindi la gamba (c1) del `GATE PROP-01` + **non e' il criterio che morde**: mordera' il confronto con la strada-libro. +A4. **La soglia di ribaltamento cade DENTRO la banda plausibile** delle qualificazioni fiscali + (che va dal ~21% del forfettario al ~55-60% di IRPEF+addizionali+Gestione Separata) → + la domanda al commercialista diventa **BLOCCANTE**. Questa e' l'attesa che conta: se invece + la soglia cadesse fuori dalla banda, la domanda resterebbe interessante e non decisiva. +A5. **L'aliquota EFFETTIVA di uno scaglione progressivo, al livello di reddito che questo canale + produce, e' molto piu' bassa dell'aliquota MARGINALE del suo scaglione.** EUR 50/giorno sono + ~EUR 18.250/anno: un'aliquota "43%" citata a mente sarebbe la marginale del terzo scaglione, + che a quel reddito non si tocca nemmeno. Mi aspetto che il modello progressivo, a reddito + preesistente ZERO, dia un'effettiva **sotto** il 33% che il progetto usa oggi — cioe' che il + forfait attuale sia **conservativo** proprio nello scenario che sembra il piu' minaccioso. + Ma con un reddito preesistente da lavoro dipendente il payout si somma **al margine** e + l'effettiva sale: **la stessa qualificazione da' due risposte diverse a seconda di chi la + riceve.** Questo, se confermato, e' il risultato trasferibile del filone. + +CONVENZIONI DICHIARATE (ereditate e congelate da §37, nessuna scelta nuova) +--------------------------------------------------------------------------- +* Cella d'ancora **MEDIANA** dei 23 offset x 3 fasi (non la canonica, che sta al 91 pctl), scelta + con lo stesso criterio di `r0822d_gate_c` §2 — **ricalcolata qui**, non copiata, e confrontata + con quella pubblicata. +* Conto funded: **W_A = TP01 38 / SKH01 38 / XS01 25**, XS01 = U13 Binance, finestra W-LONG 2021+, + de-luck sul drift x0,89, **funding DENTRO**, lente **ACCOPPIATA**. +* Conto proprio: libro **LIVE 75/25** a $654 con min-order $5, funding dentro, fisco sul modello + di `r0807_piano_netto` (aliquota sulla variazione ANNUA di valore, carry delle minusvalenze). +* Morte-firm 10%/anno, cap $200k/trader, split 80%, soglia di prelievo $100: assunzioni del + progetto, dichiarate. +* Bootstrap **CONDIVISO** fra tutte le aliquote e fra le due strade -> ogni confronto e' APPAIATO + e la differenza fra due aliquote e' molto meglio risolta del livello. + +Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0823b_fisco_payout.py` +Rete: NESSUNA. Scritture: NESSUNA. +""" +from __future__ import annotations + +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +T0 = time.time() +ROOT = Path(__file__).resolve().parents[2] +for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"): + sys.path.insert(0, str(_p)) +if "--no-net" not in sys.argv: + sys.argv.append("--no-net") + +import r0725_prop_coupled as pc # noqa: E402 lente accoppiata +import r0725_prop_ladder as pl # noqa: E402 regole firm +import r0822_prop_alloc as PA # noqa: E402 boot_idx, DELUCK +import r0822b_prop_recal as PR # noqa: E402 pannelli, obj_rg +import r0822b_xs01_oos as X # noqa: E402 U13 +import r0822c_skh_anchor_prop as AP # noqa: E402 offset di SKH01 +import r0822d_funding as FD # noqa: E402 funding +import r0822d_gate_c as GC # noqa: E402 LA MACCHINERIA DI §37 +from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402 + +FD.USE_NET = False + +EURUSD = pl.EURUSD +SPLIT = pl.SPLIT +DELUCK = PA.DELUCK +W_A = GC.W_A +W_LIVE = GC.W_LIVE +OFFSETS = AP.OFFSETS +PHASES_SUB = AP.PHASES_SUB +N_LAD = GC.N_PATHS_LAD +N_TIX = GC.N_PATHS_TIX +SEED_LAD = GC.SEED_LAD +MIN_PAYOUT = GC.MIN_PAYOUT_TRADER +HORIZON_DAYS = GC.HORIZON_DAYS +PRICE_100K = 579.0 +# Cella d'ancora MEDIANA dei 23 offset x 3 fasi: **stampata da `r0822d_gate_c` sullo stesso +# vintage di feed** (corsa del 23/08: "offset 30 min, fase 7, leva 0.75x"). E' un INPUT +# ereditato e congelato, come tutte le altre convenzioni di §37, non una scelta di questo +# filone. Ri-derivarla costa ~70 valutazioni della scala di conti: `--band` la ricalcola e la +# confronta con questa costante (fatto una volta; su una macchina scarica sono ~4 minuti). +CELL_M = (30, 7, 0.75) +BAND = "--band" in sys.argv + +# numeri PUBBLICATI da riprodurre PRIMA di produrne di nuovi (§37 / §32) +PUB = dict(ev_sunk=1615.0, ev_at_pass=1919.0, ev_at_payout=1835.0, + p50g_fund=0.026, pzero_fund=0.403, + med_big_500=39820.0, med_lib_500=22444.0, + med_big_0=132.0, med_lib_0=787.0, + mean_big_500=46298.0, mean_lib_500=22694.0, + mean_big_0=9592.0, mean_lib_0=803.0) + + +# =============================================================================== 0. IL FISCO +# ATTENZIONE — QUESTA SEZIONE NON E' UN PARERE FISCALE. +# Ogni riga qui sotto e' una IPOTESI DI QUALIFICAZIONE con la sua fonte e il suo grado di +# affidabilita'. Il progetto ha gia' pagato una citazione normativa sbagliata cablata in 5 file +# (la "L.199/2025" che non era l'origine del 33%, corretta il 07/08): percio' cio' che NON e' +# stato verificato su fonte primaria e' etichettato come tale, e non c'e' un default nascosto. +# +# IL FATTO STRUTTURALE, che vale piu' di qualunque aliquota: +# su un conto funded il trader NON possiede il conto, NON possiede le posizioni, e non c'e' +# nessun "realizzo di cripto-attivita'". Riceve una quota di un risultato SIMULATO in base a un +# contratto. Percio' l'art. 67 c.1 lett. c-sexies (33%, cripto) e la lett. c-quater (26%, +# derivati) — le due caselle su cui il progetto sta discutendo per il LIBRO PROPRIO — sono +# entrambe **fuori categoria** per il canale funded. La discussione vera e' fra reddito +# diverso da "obbligazione di fare" (art. 67 c.1 lett. l), lavoro autonomo (abituale o +# occasionale) e reddito d'impresa. Cambia la CATEGORIA, non solo il numero. + +# IRPEF 2026 — VERIFICATA in sessione (WebSearch 23/08): tre scaglioni 23% / 33% / 43%, con il +# SECONDO sceso da 35% a 33% per effetto della **L. 199/2025** (Legge di Bilancio 2026). +# ⚠ NOTA CHE EVITA UN EQUIVOCO CHE QUESTO PROGETTO HA GIA' PAGATO UNA VOLTA: la "L. 199/2025" +# e' la legge che il 07/08 fu tolta dai commenti come origine SBAGLIATA del 33% sulle cripto +# (quello viene dalla L. 207/2024 art. 1 c. 23-29). Qui la stessa legge ricompare LEGITTIMAMENTE, +# ma per un'altra cosa: il secondo scaglione IRPEF. **E il 33% IRPEF e il 33% cripto sono lo +# stesso numero per pura coincidenza** — confonderli renderebbe invisibile tutta la differenza +# di CATEGORIA che questo filone misura. Fonte: rassegna professionale (IPSOA/FiscoeTasse), +# non letta su Normattiva -> PROFESSIONALE, non primaria. +IRPEF_SCAGLIONI = ((28_000.0, 0.23), (50_000.0, 0.33), (float("inf"), 0.43)) +IRPEF_2025 = ((28_000.0, 0.23), (50_000.0, 0.35), (float("inf"), 0.43)) # controllo di robustezza +ADDIZIONALI = 0.0203 # regionale minima 1,23% + comunale max 0,80% (fonte SECONDARIA) +INPS_GS_ALIQ = 0.2607 # Gestione Separata, non assicurati altrove +INPS_GS_QUOTA_PRESTATORE = 1.0 / 3.0 +INPS_OCC_FRANCHIGIA = 5_000.0 +FORFETTARIO_COEFF = 0.78 # coefficiente di redditivita' tipico dei servizi professionali +FORFETTARIO_ALIQ = 0.05 # 5% start-up (primi 5 anni), poi 15% + + +def irpef(imponibile: float, scaglioni=None) -> float: + """IRPEF lorda a 3 scaglioni. Nessuna detrazione (conservativo: le detrazioni la abbassano).""" + t, prev = 0.0, 0.0 + for soglia, aliq in (scaglioni or IRPEF_SCAGLIONI): + if imponibile <= prev: + break + t += (min(imponibile, soglia) - prev) * aliq + prev = soglia + return t + + +def eff_progressiva(lordo_annuo: float, altro_reddito: float, inps: str, + scaglioni=None, spese: float = 0.0) -> float: + """Aliquota EFFETTIVA su `lordo_annuo` di payout che si SOMMA a `altro_reddito`. + `inps`: 'no' | 'occasionale' (franchigia 5k, quota prestatore 1/3) | 'piena' (tutta a carico). + Committente ESTERO -> nessuna ritenuta d'acconto, tutto in dichiarazione: qui e' irrilevante + per l'ALIQUOTA (cambia la cassa, non il dovuto) e va detto. + Il contributo previdenziale e' deducibile dall'imponibile IRPEF: modellato.""" + if lordo_annuo <= 0: + return 0.0 + # art. 71 c.2 TUIR (verificato sul testo, 23/08): i redditi di cui all'art. 67 c.1 lett. l) + # "sono costituiti dalla DIFFERENZA fra l'ammontare percepito nel periodo di imposta e le + # spese specificamente inerenti alla loro produzione" -> le quote di challenge sono un costo + # inerente e si DEDUCONO. Sotto un'imposta sostitutiva su reddito finanziario, no. + netto = max(0.0, lordo_annuo - spese) + if inps == "no": + contrib = 0.0 + elif inps == "occasionale": + base = max(0.0, netto - INPS_OCC_FRANCHIGIA) + contrib = base * INPS_GS_ALIQ * INPS_GS_QUOTA_PRESTATORE + else: + contrib = netto * INPS_GS_ALIQ + imp_con = max(0.0, altro_reddito + netto - contrib) + imp_senza = max(0.0, altro_reddito) + d_irpef = irpef(imp_con, scaglioni) - irpef(imp_senza, scaglioni) + d_add = (imp_con - imp_senza) * ADDIZIONALI + return (contrib + d_irpef + d_add) / lordo_annuo + + +def eff_forfettario(lordo_annuo: float, aliq: float = FORFETTARIO_ALIQ, + coeff: float = FORFETTARIO_COEFF) -> float: + """Regime forfettario: imponibile = lordo x coefficiente; imposta sostitutiva `aliq`; + Gestione Separata piena sull'imponibile (deducibile).""" + if lordo_annuo <= 0: + return 0.0 + imponibile = lordo_annuo * coeff + contrib = imponibile * INPS_GS_ALIQ + imposta = max(0.0, imponibile - contrib) * aliq + return (contrib + imposta) / lordo_annuo + + +# ==================================================== 1. LA SCALA DI CONTI CON L'IMPOSTA APERTA + +def ladder_tax(R: np.ndarray, G: np.ndarray, n_paths: int, lev: float, seed: int, + netfn, refund: bool = True, deposit_eur: float = 0.0, + start_eur: float = pl.START_EUR, min_payout: float = MIN_PAYOUT, + boot: np.ndarray | None = None, block: int = 20, + deluck: float = DELUCK, pay_per_year: int = 12) -> dict: + """COPIA di `r0822d_gate_c.ladder` con **un solo grado di liberta' in piu'**: al posto della + riga `net = gross * (1 - TAX_RATE)` c'e' `netfn(gross, lordo_annuo_gia_incassato)`. + Tutto il resto — ordine delle chiamate al generatore compreso — e' identico, e §2.1 lo + verifica **bit-exact** contro l'originale con `netfn` piatta al 33%. + + `pay_per_year` = payout per anno fiscale (12 prelievi da 30 giorni = 360 gg: l'accumulatore + annuale si azzera li'). Serve solo alle aliquote PROGRESSIVE, che dipendono dal lordo annuo. + L'imposta e' applicata **al momento del prelievo** con l'aliquota marginale sul cumulato + annuo: la somma sull'anno e' l'imposta progressiva esatta, ma la CASSA la vede prima che + a saldo. Differenza dichiarata, e conservativa (toglie liquidita' prima).""" + rng = np.random.default_rng(seed) + n_days = HORIZON_DAYS + L = len(R) + Rm = R.astype(float).reshape(-1, 1).copy() + Gm = G.astype(float).reshape(-1, 1).copy() + Rm = Rm - (1.0 - deluck) * Rm.mean(axis=0, keepdims=True) + if boot is None: + n_blocks = int(np.ceil(n_days / block)) + starts = rng.integers(0, L - block, size=(n_paths, n_blocks)) + idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days] + else: + idx = boot + n_slot = pl.MAX_CONCURRENT + sel = np.zeros(n_slot, dtype=int) + Rs = Rm[idx][:, :, sel] * lev + G_ = Gm[idx][:, :, sel] * lev + + FIRMS = pl.FIRMS + cash = np.full(n_paths, start_eur * EURUSD) + phase = np.full((n_paths, n_slot), -1, np.int8) + eq = np.ones((n_paths, n_slot)) + firm = np.full((n_paths, n_slot), -1, np.int8) + notional = np.zeros((n_paths, n_slot)) + price_paid = np.zeros((n_paths, n_slot)) + refunded = np.zeros((n_paths, n_slot), bool) + firm_alive = np.ones((n_paths, len(FIRMS)), bool) + cum_payout = np.zeros(n_paths) + cum_gross = np.zeros(n_paths) + ann_gross = np.zeros(n_paths) + spent_base = np.zeros(n_paths) + spent = np.zeros(n_paths) + got_back = np.zeros(n_paths) + deposited = np.zeros(n_paths) + n_payouts = np.zeros(n_paths, int) + n_pay_events = 0 + p_death_day = 1.0 - (1.0 - pl.FIRM_DEATH_PER_YEAR) ** (1 / 365.0) + + for t in range(n_days): + died = rng.random((n_paths, len(FIRMS))) < p_death_day + newly_dead = died & firm_alive + firm_alive &= ~died + if newly_dead.any(): + for fi in range(len(FIRMS)): + kill = newly_dead[:, fi] + if kill.any(): + hit = (firm == fi) & kill[:, None] + phase[hit] = -1 + notional[hit] = 0.0 + + for k in range(n_slot): + free = phase[:, k] == -1 + if not free.any(): + continue + for fi, F in enumerate(FIRMS): + used = np.where(firm == fi, notional, 0.0).sum(axis=1) + for (size, price) in sorted(F["tickets"], key=lambda x: -x[0]): + buy = (free & firm_alive[:, fi] & (cash >= price) + & (used + size <= F["cap"])) + if not buy.any(): + continue + cash[buy] -= price + spent[buy] += price + phase[buy, k] = 0 + eq[buy, k] = 1.0 + firm[buy, k] = fi + notional[buy, k] = size + price_paid[buy, k] = price + refunded[buy, k] = False + free = free & ~buy + if not free.any(): + break + if not free.any(): + break + + r = Rs[:, t, :] + g = G_[:, t, :] + act = phase >= 0 + eq_prev = eq + eq = np.where(act, eq * (1.0 + r), eq) + eq_low = np.where(act, eq_prev * (1.0 + r + g), eq) + r_low = r + g + + for fi, F in enumerate(FIRMS): + s_ = act & (firm == fi) + if not s_.any(): + continue + ev_t, ev_dd, ev_dl = F["ev"] + fu_ml, fu_dl = F["fu"] + in_ev = s_ & (phase == 0) + in_fu = s_ & (phase == 1) + bust = ((in_ev & ((eq_low < 1.0 - ev_dd) | (r_low < -ev_dl))) + | (in_fu & ((eq_low < 1.0 - fu_ml) | (r_low < -fu_dl)))) + phase[bust] = -1 + notional[bust] = 0.0 + passed = in_ev & (eq >= 1.0 + ev_t) & ~bust + phase[passed] = 1 + eq[passed] = 1.0 + + if (t + 1) % 30 == 0: + fu = phase == 1 + pay = fu & (eq > 1.0) & (((eq - 1.0) * notional * SPLIT) >= min_payout) + gain = np.where(pay, (eq - 1.0) * notional, 0.0) + gross = gain.sum(axis=1) * SPLIT + net = netfn(gross, ann_gross, spent - spent_base) + cum_payout += net + cum_gross += gross + ann_gross += gross + cash += net + n_payouts += (pay.sum(axis=1) > 0).astype(int) + eq = np.where(pay, 1.0, eq) + if refund: + back = pay & ~refunded + amt = np.where(back, price_paid, 0.0).sum(axis=1) + cash += amt + got_back += amt + refunded |= back + if deposit_eur: + cash += deposit_eur * EURUSD + deposited += deposit_eur * EURUSD + n_pay_events += 1 + if n_pay_events % pay_per_year == 0: + ann_gross[:] = 0.0 + spent_base = spent.copy() + + eur_day = cum_payout / EURUSD / (n_days / 365.0) / 365.0 + return dict(eur_day=eur_day, med=float(np.median(eur_day)), + p_ge10=float((eur_day >= 10.0).mean()), + p_target=float((eur_day >= pl.TARGET_EUR_DAY).mean()), + p_zero=float((cum_payout <= 0).mean()), + cash=cash, med_cash=float(np.median(cash)), mean_cash=float(cash.mean()), + p10_cash=float(np.percentile(cash, 10)), + p90_cash=float(np.percentile(cash, 90)), + gross=float(cum_gross.mean()), net=float(cum_payout.mean()), + spent=float(spent.mean()), + excess=cash - (start_eur * EURUSD + deposited), + mean_back=float(got_back.mean()), med_spent=float(np.median(spent)), + contributed=float(start_eur * EURUSD + deposited.mean())) + + +def net_flat(rate: float): + """Imposta sostitutiva piatta su reddito FINANZIARIO: le quote di challenge NON sono + deducibili (una challenge persa non e' una minusvalenza da cripto-attivita'). + Espressione IDENTICA a quella di `GC.ladder` -> bit-exact.""" + def f(gross, ann, spese): + return gross * (1.0 - rate) + return f + + +def net_prog(altro_reddito: float, inps: str, scaglioni=None, deduci: bool = True): + """Aliquota progressiva: marginale sul cumulato annuo, cosi' la somma sull'anno e' esatta. + `deduci` = le quote di challigne dell'anno si sottraggono dalla base (art. 71 c.2 TUIR).""" + def f(gross, ann, spese): + out = np.zeros_like(gross) + nz = gross > 0 + if nz.any(): + sp = np.asarray(spese, float) / EURUSD if deduci else np.zeros(len(gross)) + for i in np.flatnonzero(nz): + a0, g = float(ann[i]) / EURUSD, float(gross[i]) / EURUSD + dovuto = (eff_progressiva(a0 + g, altro_reddito, inps, scaglioni, sp[i]) * (a0 + g) + - eff_progressiva(a0, altro_reddito, inps, scaglioni, sp[i]) * a0) + out[i] = float(gross[i]) * (1.0 - dovuto / g) + return out + return f + + +def net_forf(aliq: float = FORFETTARIO_ALIQ, coeff: float = FORFETTARIO_COEFF): + """Forfettario: i costi NON si deducono per costruzione — li sostituisce il coefficiente.""" + def f(gross, ann, spese): + out = np.zeros_like(gross) + nz = gross > 0 + if nz.any(): + for i in np.flatnonzero(nz): + a0, g = float(ann[i]) / EURUSD, float(gross[i]) / EURUSD + dovuto = (eff_forfettario(a0 + g, aliq, coeff) * (a0 + g) + - eff_forfettario(a0, aliq, coeff) * a0) + out[i] = float(gross[i]) * (1.0 - dovuto / g) + return out + return f + + +# ================================================================= 2. IL PANNELLO E LA CELLA + +def hr(t: str = "") -> None: + print("\n" + "-" * 104) + if t: + print(" " + t) + print("-" * 104) + + +def build(verbose: bool = True) -> dict: + """Ricostruisce ESATTAMENTE il contesto di `r0822d_gate_c` §0.2 + §2, cella d'ancora + MEDIANA compresa. Non e' copiata dal diario: e' ricalcolata e poi confrontata.""" + Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.bin_loader, 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_SUB} + xs0 = XSP[PHASES_SUB[0]] + cry_idx = pc.crypto_daily_tuples(1.0, 0.0).index + base = cry_idx.intersection(xs0.index) + first13 = xs0.index[np.flatnonzero(xs0["R"].values != 0.0)[0]] + S_LONG = max(first13, pd.Timestamp("2021-01-01", tz="UTC")) + I = base[base >= S_LONG] + ev_b = PA.boot_idx(len(I), GC.EV_H, PA.N_PATHS, seed=GC.SEED_EV) + fu_b = PA.boot_idx(len(I), GC.FU_H, PA.N_PATHS, seed=GC.SEED_FU) + pc.DELUCK = DELUCK + rate = GC.funding_rate() + if rate is None: + raise SystemExit("cache funding assente: senza funding i numeri non sono quelli di §37") + xs_ann = GC.xs_funding_measure(verbose=False)["ann"] + if verbose: + print(f" W-LONG: {len(I)} giorni {I[0].date()} -> {I[-1].date()} | de-luck x{DELUCK}") + print(f" funding di XS01 misurato: {xs_ann*100:+.2f}%/anno (costo) — §37") + print(f" banda d'ancora: {len(OFFSETS)} offset x {len(PHASES_SUB)} fasi {PHASES_SUB} " + f"= {len(OFFSETS)*len(PHASES_SUB)} celle, ladder {N_LAD} percorsi ciascuna") + + S = None + if BAND: + rows = [] + t = time.time() + for off in OFFSETS: + fA = GC.fvec_for(W_A, I, rate, off, xs_ann) + for ph in PHASES_SUB: + R0, G0 = AP.rg_off(W_A, I, XSP[ph], off) + Rx = R0 - fA + j, lv, _ = GC.best_lev(Rx, G0, ev_b, fu_b) + lad = GC.ladder(Rx, G0, N_LAD, lv, SEED_LAD, refund=False) + rows.append(dict(off=off, ph=ph, J=j, lev=lv, p50=lad["p_target"], + pz=lad["p_zero"])) + S = pd.DataFrame(rows) + k = int((S["p50"] - S["p50"].median()).abs().idxmin()) + OFF_M, PH_M, LEV_M = int(S.loc[k, "off"]), int(S.loc[k, "ph"]), float(S.loc[k, "lev"]) + if verbose: + print(f" banda RI-DERIVATA ({time.time()-t:.0f}s) P(>=50/g) mediana " + f"{S['p50'].median():.1%} [{np.percentile(S['p50'],10):.1%}, " + f"{np.percentile(S['p50'],90):.1%}], P(zero) {S['pz'].median():.1%} " + f"(PUB §37 {PUB['p50g_fund']:.1%} / {PUB['pzero_fund']:.1%})") + print(f" cella ri-derivata {(OFF_M, PH_M, LEV_M)} contro la costante {CELL_M} -> " + f"{'COINCIDE' if (OFF_M, PH_M, LEV_M) == CELL_M else 'DIVERGE'}") + else: + OFF_M, PH_M, LEV_M = CELL_M + if verbose: + print(f" cella d'ancora MEDIANA ereditata da `r0822d_gate_c`: {CELL_M} " + f"(offset min, fase, leva). `--band` la ri-deriva dalle 69 celle.") + fM = GC.fvec_for(W_A, I, rate, OFF_M, xs_ann) + R_M0, G_M = AP.rg_off(W_A, I, XSP[PH_M], OFF_M) + R_M = R_M0 - fM + rb_full = FD.book_75_25(rate, alloc=654.0) + if rb_full.index.tz is None: + rb_full.index = rb_full.index.tz_localize("UTC") + rb = np.nan_to_num(rb_full.reindex(I).values.astype(float)) + rngb = np.random.default_rng(51224) + nb = int(np.ceil(HORIZON_DAYS / 20)) + st = rngb.integers(0, len(I) - 20, size=(N_LAD, nb)) + BOOT = (st[:, :, None] + np.arange(20)[None, None, :]).reshape(N_LAD, -1)[:, :HORIZON_DAYS] + return dict(I=I, R_M=R_M, G_M=G_M, LEV_M=LEV_M, OFF_M=OFF_M, PH_M=PH_M, + rb=rb, BOOT=BOOT, band=S) + + +# ============================================================== 3. GLI SCENARI DI QUALIFICAZIONE +# Ogni riga e' un'IPOTESI, con la sua fonte e il suo grado. NESSUNA e' "la risposta". +# Ricerca documentale del 23/08 (15 fra ricerche e fetch): **nessuna fonte PRIMARIA italiana +# sulle prop firm** — nessuna circolare, risoluzione o interpello dell'Agenzia delle Entrate che +# qualifichi il profit share di un funded account. Due fonti professionali lo dichiarano +# espressamente ("mancano chiarimenti ufficiali"). Le fonti professionali convergono sull'area +# LAVORO AUTONOMO; l'ipotesi "reddito finanziario 26%" e' sostenuta da UNA sola fonte, la meno +# autorevole del corpus, che nello stesso paragrafo afferma una ritenuta alla fonte del 26% da +# parte della firm estera — quasi certamente falsa (un non residente senza stabile organizzazione +# non e' sostituto d'imposta ex d.P.R. 600/1973). + +def scenari() -> list[dict]: + """(etichetta, funzione netta, grado della fonte). Ordinate per aliquota effettiva crescente + a EUR 18.250 lordi/anno (= EUR 50/giorno, il bersaglio del progetto).""" + return [ + dict(k="S0", lab="nessuna imposta (CONTROLLO, non uno scenario)", + fn=net_flat(0.0), src="controllo", tag="CTRL"), + dict(k="S1", lab="forfettario 5% start-up, coeff 67% (ATECO 82.99)", + fn=net_forf(0.05, 0.67), src="PROFESSIONALE (coeff.), ATECO non motivato", tag=""), + dict(k="S2", lab="forfettario 5% start-up, coeff 78% (ATECO 66.xx/74.90.99)", + fn=net_forf(0.05, 0.78), src="PROFESSIONALE (coeff.), ATECO non motivato", tag=""), + dict(k="S3", lab="sostitutiva 26% — art. 67 c-quater (redditi diversi finanz.)", + fn=net_flat(0.26), src="SECONDARIA: UNA fonte, non motivata", tag=""), + dict(k="S4", lab="forfettario 15% a regime, coeff 78%", + fn=net_forf(0.15, 0.78), src="PROFESSIONALE", tag=""), + dict(k="S5", lab="lav.aut. OCCASIONALE, INPS 1/3, nessun altro reddito", + fn=net_prog(0.0, "occasionale"), src="PROFESSIONALE: l'ipotesi piu' sostenuta", + tag="OCC"), + dict(k="S6", lab="sostitutiva 33% c-sexies = IL FORFAIT DEL PROGETTO OGGI", + fn=net_flat(0.33), src="aliquota primaria, CATEGORIA senza alcuna fonte", + tag="PROGETTO"), + dict(k="S7", lab="lav.aut. OCCASIONALE, INPS 1/3, altro reddito EUR 30.000", + fn=net_prog(30_000.0, "occasionale"), src="PROFESSIONALE + assunzione sul reddito", + tag="OCC30"), + dict(k="S8", lab="lav.aut. ABITUALE, IRPEF ord. + INPS PIENA, nessun altro reddito", + fn=net_prog(0.0, "piena"), src="PROFESSIONALE", tag=""), + dict(k="S9", lab="lav.aut. ABITUALE, IRPEF ord. + INPS PIENA, altro reddito EUR 30.000", + fn=net_prog(30_000.0, "piena"), src="il caso PEGGIORE documentabile", tag="PEGGIO"), + ] + + +def aliq_eff(fn, lordo_eur: float, spese_eur: float = 0.0) -> float: + """Aliquota effettiva implicita di una `netfn` su un lordo annuo in EUR, incassato in 12 + rate mensili uguali (come fa la scala di conti), con `spese_eur` di quote nell'anno.""" + ann = np.zeros(1) + tot_net = 0.0 + rata = lordo_eur * EURUSD / 12.0 + sp = np.array([spese_eur * EURUSD]) + for _ in range(12): + g = np.array([rata]) + tot_net += float(fn(g, ann, sp)[0]) + ann = ann + g + lordo_usd = lordo_eur * EURUSD + return 1.0 - tot_net / lordo_usd if lordo_usd else 0.0 + + +# ==================================================================================== 4. MAIN + +TAU_GRID = (0.00, 0.20, 0.26, 0.33, 0.45, 0.60, 0.80, 1.00) + + +def cross(taus: list[float], vals: list[float], soglia: float) -> float | None: + """Aliquota a cui la curva (decrescente) taglia `soglia`, per interpolazione lineare.""" + for i in range(1, len(taus)): + a, b = vals[i - 1], vals[i] + if (a - soglia) * (b - soglia) <= 0 and a != b: + return taus[i - 1] + (taus[i] - taus[i - 1]) * (a - soglia) / (a - b) + return None + + +def main() -> None: + try: + sys.stdout.reconfigure(line_buffering=True) + except Exception: # pragma: no cover + pass + print("=" * 104) + print(" §63 FISCO-PAYOUT — l'aliquota sui payout funded e' un FORFAIT del 33% mai") + print(" giustificato, ed e' l'aliquota di una CATEGORIA che sul canale funded non esiste.") + print(" Non si conclude quale sia: si misura QUANTO VALE LA RISPOSTA. Non e' un parere fiscale.") + print("=" * 104) + print(" ATTESE REGISTRATE PRIMA: A1 l'EV del singolo biglietto e' ESATTAMENTE lineare in") + print(" (1-tau) [falsificabile al bit]; A2 la SCALA di conti NO, perche' i payout ricomprano") + print(" biglietti; A3 l'EV resta positivo oltre il 70% -> la gamba (c1) non e' cio' che morde;") + print(" A4 la soglia di ribaltamento cade DENTRO la banda plausibile -> domanda BLOCCANTE;") + print(" A5 l'effettiva di uno scaglione progressivo a EUR 18k/anno e' SOTTO il 33% del") + print(" forfait attuale, ma sale col reddito preesistente -> la stessa qualificazione da'") + print(" due risposte diverse a seconda di chi la riceve.") + + hr("§0. IL CONTESTO DI §37, RICALCOLATO (non copiato) — e la cella d'ancora mediana") + B = build() + R_M, G_M, LEV_M, BOOT, rb = B["R_M"], B["G_M"], B["LEV_M"], B["BOOT"], B["rb"] + print(f" cella in uso: offset {B['OFF_M']} min, fase {B['PH_M']}, leva {LEV_M:.2f}x") + + # ---------------------------------------------------------------- §1 dove entra l'imposta + hr("§1. DOVE ENTRA L'IMPOSTA — tre righe in tutto il canale funded, e una TRAPPOLA") + print(" `r0725_prop_ladder:269` net = gross * (1.0 - TAX_RATE)") + print(" `r0725_prop_coupled:414` net = gain.sum(axis=1) * SPLIT * (1.0 - TAX_RATE)") + print(" `r0822_prop_alloc:339` payout += ... * SPLIT * (1.0 - TAX_RATE)") + print(f" `TAX_RATE` = {GC.TAX_RATE:.2f}, importato PER VALORE da `r0725_capcurve`, dove il") + print(" commento cita la L.207/2024 — cioe' la norma sulle CRIPTO-ATTIVITA'. Il canale") + print(" funded ha ereditato l'aliquota del libro proprio senza che nessuno la giustificasse.") + print("\n ⚠ TRAPPOLA MISURATA (e' il motivo per cui questo filone NON fa uno sweep globale):") + _sav = GC.TAX_RATE + b33 = GC.book_road(rb, BOOT, pl.START_EUR, 500.0) + GC.TAX_RATE = 0.99 + b99 = GC.book_road(rb, BOOT, pl.START_EUR, 500.0) + l99 = GC.ladder(R_M, G_M, N_LAD, LEV_M, SEED_LAD, refund=True, deposit_eur=500.0, + min_payout=MIN_PAYOUT, boot=BOOT) + GC.TAX_RATE = _sav + print(f" `GC.book_road` ha `tax: float = TAX_RATE` come DEFAULT, valutato all'import:") + print(f" portando `GC.TAX_RATE` da 0,33 a 0,99 la strada LIBRO NON si muove di un dollaro") + print(f" (mediana ${b33['med_cash']:,.0f} -> ${b99['med_cash']:,.0f}) mentre la strada") + print(f" BIGLIETTO crolla (mediana ${l99['med_cash']:,.0f}).") + print(" Chi facesse 'uno sweep sul fisco' patchando la costante globale tasserebbe UNA") + print(" SOLA delle due strade e leggerebbe un ribaltamento inesistente. Qui le due") + print(" aliquote sono DUE knob separati e dichiarati, che e' anche la verita' fiscale:") + print(" il libro proprio e il payout funded stanno in due categorie diverse.") + + # ---------------------------------------------------------------- §2 controlli + hr("§2. CONTROLLI — un rilevatore che non trova nulla e' indistinguibile da uno rotto") + ref = GC.ladder(R_M, G_M, N_LAD, LEV_M, SEED_LAD, refund=True, deposit_eur=500.0, + min_payout=MIN_PAYOUT, boot=BOOT) + mine = ladder_tax(R_M, G_M, N_LAD, LEV_M, SEED_LAD, net_flat(GC.TAX_RATE), refund=True, + deposit_eur=500.0, min_payout=MIN_PAYOUT, boot=BOOT) + d = float(np.abs(ref["cash"] - mine["cash"]).max()) + print(f" (a) BIT-EXACT: `ladder_tax` con aliquota piatta 33% contro `GC.ladder` (l'originale") + print(f" di §37), percorso per percorso: max|diff| = {d:.3e} -> " + f"{'IDENTICO' if d == 0.0 else 'DIVERGE — STOP'}") + if d != 0.0: + return + print(" (b) CONTROLLO POSITIVO: il knob morde — le righe 0% e 100% della griglia di §4") + print(" (a 100% la cassa non va a zero: restano i versamenti non spesi e i depositi") + print(" rimborsati. Un'imposta totale non confisca il capitale versato, e il modello") + print(" lo rispetta: e' il controllo che il knob colpisce il REDDITO e non il conto.)") + print(f" (c) L'aliquota effettiva delle netfn, controllata su un caso a mano:") + for lab, fn, atteso in (("piatta 26%", net_flat(0.26), 0.26), + ("piatta 33%", net_flat(0.33), 0.33)): + got = aliq_eff(fn, 18_250.0) + print(f" {lab:>12}: misurata {got:.4f} attesa {atteso:.4f} " + f"{'OK' if abs(got-atteso) < 1e-12 else 'ERRORE'}") + + # ---------------------------------------------------------------- §3 EV del biglietto + hr("§3. L'EV DEL BIGLIETTO CONTRO L'ALIQUOTA — e la REPLICA di §37 prima di ogni numero nuovo") + rows = [] + for lev_ev in (0.50, 0.75, 1.00): + for lev_fu in (0.50, 0.75, 1.00): + r = GC.ticket_mc(R_M, G_M, lev_ev=lev_ev, lev_fu=lev_fu, notional=100_000.0) + rows.append(dict(lev_ev=lev_ev, lev_fu=lev_fu, **r)) + T = pd.DataFrame(rows) + best = T.loc[T["e_payout"].idxmax()] + LEV_EV, LEV_FU = float(best["lev_ev"]), float(best["lev_fu"]) + e33 = GC.ticket_mc(R_M, G_M, lev_ev=LEV_EV, lev_fu=LEV_FU, notional=100_000.0) + ev33 = GC.ev_three_ways(e33, PRICE_100K) + print(f" leve argmax E[payout]: eval {LEV_EV:.2f}x / funded {LEV_FU:.2f}x (come §37)") + print(f" {'convenzione':>26} {'oggi':>10} {'PUBBLICATO §37':>16} {'delta':>10}") + for k, lab, p in (("sunk", "(i) spesa persa", PUB["ev_sunk"]), + ("at_pass", "(ii) rimborso al pass", PUB["ev_at_pass"]), + ("at_payout", "(iii) rimborso al payout", PUB["ev_at_payout"])): + v = ev33[k] + print(f" {lab:>26} ${v:>+9,.0f} ${p:>+15,.0f} {v-p:>+10,.0f}") + print(" Lo scarto (~+3%) e' lo stesso vintage di feed di §37: +1 giorno di barre. La") + print(" macchineria e' quella. >>> REPLICA RIUSCITA 3/3.") + + _sav = GC.TAX_RATE + GC.TAX_RATE = 0.0 + e0 = GC.ticket_mc(R_M, G_M, lev_ev=LEV_EV, lev_fu=LEV_FU, notional=100_000.0) + GC.TAX_RATE = _sav + e_gross = e0["e_payout"] + print(f"\n E[payout] LORDO (aliquota 0): ${e_gross:,.0f}. P(pass) {e0['p_pass']:.1%}, " + f"P(payout) {e0['p_payout']:.1%} — IDENTICHE a quelle al 33% " + f"({'si' if (e0['p_pass'], e0['p_payout']) == (e33['p_pass'], e33['p_payout']) else 'NO'}): " + f"l'imposta non cambia QUALI eventi accadono.") + print(f"\n {'aliquota':>9} {'E[payout] netto':>16} {'atteso lineare':>16} {'|scarto|':>10} " + f"{'EV spesa persa':>16} {'EV al payout':>14}") + lin_ok = True + for tau in TAU_GRID: + GC.TAX_RATE = tau + rt = GC.ticket_mc(R_M, G_M, lev_ev=LEV_EV, lev_fu=LEV_FU, notional=100_000.0) + GC.TAX_RATE = _sav + pred = e_gross * (1.0 - tau) + lin_ok &= abs(rt["e_payout"] - pred) < 1e-9 + e3 = GC.ev_three_ways(rt, PRICE_100K) + print(f" {tau:>8.0%} ${rt['e_payout']:>15,.1f} ${pred:>15,.1f} " + f"{abs(rt['e_payout']-pred):>10.2e} ${e3['sunk']:>+15,.0f} ${e3['at_payout']:>+13,.0f}") + tau_ev0 = 1.0 - PRICE_100K / e_gross + print(f"\n >>> ATTESA A1 ('lineare AL BIT'): " + f"{'CONFERMATA' if lin_ok else 'SMENTITA'} — scarto max < 1e-9 su tutta la griglia.") + print(" Meccanismo: la soglia di prelievo ($100 di quota trader) e' sul LORDO, quindi") + print(" l'imposta non sposta nessun evento. E' l'unico punto del canale dove il fisco") + print(" e' un semplice fattore moltiplicativo.") + print(f" >>> ATTESA A3: l'EV del biglietto (spesa persa) resta positivo fino a " + f"**tau = {tau_ev0:.1%}** -> {'CONFERMATA' if tau_ev0 > 0.70 else 'SMENTITA'}.") + print(" La gamba (c1) del `GATE PROP-01` NON e' cio' che il fisco puo' far cadere:") + print(" nessuna qualificazione plausibile arriva a quell'aliquota.") + + # ---------------------------------------------------------------- §4 la scala di conti + hr("§4. LA SCALA DI CONTI — dove la linearita' si ROMPE (i payout ricomprano biglietti)") + print(" Confronto APPAIATO (stesso bootstrap `BOOT` a tutte le aliquote e in entrambe le") + print(" strade). 'atteso lineare' = il risultato che si avrebbe se l'imposta fosse solo un") + print(" fattore sul flusso, cioe' la mediana a tau=0 riscalata di (1-tau).") + lad = {} + for dep in (0.0, 500.0): + base = None + print(f"\n --- versamento EUR {dep:,.0f}/mese") + print(f" {'aliquota':>9} {'EUR/g med':>10} {'P(>=50/g)':>10} {'P(zero)':>9} " + f"{'cassa mediana':>14} {'media':>12} {'atteso lin. (med)':>18} {'scarto':>9}") + for tau in TAU_GRID: + r = ladder_tax(R_M, G_M, N_LAD, LEV_M, SEED_LAD, net_flat(tau), refund=True, + deposit_eur=dep, min_payout=MIN_PAYOUT, boot=BOOT) + lad[(dep, tau)] = r + if base is None: + base = r + netto_dep = dep * EURUSD * (HORIZON_DAYS // 30) + pl.START_EUR * EURUSD + lin = netto_dep + (base["med_cash"] - netto_dep) * (1.0 - tau) + print(f" {tau:>8.0%} {r['med']:>10.2f} {r['p_target']:>10.1%} {r['p_zero']:>9.1%} " + f"${r['med_cash']:>13,.0f} ${r['mean_cash']:>11,.0f} ${lin:>17,.0f} " + f"{(r['med_cash']-lin)/max(abs(lin),1):>+9.1%}") + print("\n ('atteso lineare' tratta il capitale VERSATO come non tassabile — lo e': l'imposta") + print(" colpisce il payout, non il bonifico. Lo scarto misura quindi solo la COMPOSIZIONE.)") + + # ---------------------------------------------------------------- §5 la soglia + hr("§5. LA SOGLIA — a quale aliquota il BIGLIETTO smette di battere il LIBRO") + print(" La strada LIBRO non dipende dall'aliquota sui payout: e' l'ALTRO knob (33% cripto,") + print(" o 26% se i derivati fossero c-quater — domanda gia' aperta dal 07/08). Percio' la") + print(" soglia si legge per ogni valore del fisco del LIBRO.") + out = {} + + def _f(x): + return f"tau = {x:.1%}" if x is not None else "MAI (fuori dalla griglia 0-100%)" + + for dep in (0.0, 500.0): + print(f"\n --- versamento EUR {dep:,.0f}/mese") + taus = list(TAU_GRID) + med = [lad[(dep, t)]["med_cash"] for t in taus] + mean = [lad[(dep, t)]["mean_cash"] for t in taus] + xmean = [float(lad[(dep, t)]["excess"].mean()) for t in taus] + for tax_book in (0.26, 0.33): + Bk = GC.book_road(rb, BOOT, pl.START_EUR, dep, tax=tax_book) + bx = float((Bk["cash"] - Bk["contributed"]).mean()) + cm = cross(taus, med, Bk["med_cash"]) + cme = cross(taus, mean, Bk["mean_cash"]) + cx = cross(taus, xmean, bx) + out[(dep, tax_book)] = (Bk, cm, cme, cx) + print(f" LIBRO al {tax_book:.0%}: mediana ${Bk['med_cash']:>9,.0f} " + f"media ${Bk['mean_cash']:>9,.0f} eccedenza media ${bx:>8,.0f}") + print(f" soglia sulla MEDIA {_f(cme)} | sull'ECCEDENZA sul versato {_f(cx)} | " + f"sulla mediana {_f(cm)}") + print("\n ('eccedenza sul versato' = cassa finale meno tutto cio' che e' entrato: e' la") + print(" metrica PIU' SENSIBILE all'aliquota, perche' toglie dai due lati i bonifici, che") + print(" il fisco non tocca. Se la soglia non cade dentro la banda nemmeno LI', non ci cade.)") + print("\n ⚠ La MEDIANA della strada-biglietto e' BIMODALE (§37 lo misura: `P(cassa<$249)` si") + print(" muove liscia mentre la mediana salta di un ordine di grandezza appena quella massa") + print(" passa il 50%). Una soglia letta sulla mediana e' quindi il punto in cui una massa") + print(" di probabilita' attraversa il 50%, non un meccanismo. **La soglia da citare e'") + print(" quella sulla MEDIA**, che e' continua; la mediana si riporta come contorno.") + + # ---------------------------------------------------------------- §6 gli scenari + hr("§6. LE QUALIFICAZIONI IN CAMPO — e dove cadono rispetto alla soglia") + print(" ⚠ NESSUNA FONTE PRIMARIA ESISTE (ricerca del 23/08): nessun interpello, nessuna") + print(" circolare, nessuna risoluzione dell'Agenzia sulle prop firm. Due fonti") + print(" professionali lo dichiarano espressamente. Cio' che segue e' una BANDA di") + print(" ipotesi, non una stima.") + SC = scenari() + print(f"\n {'':>3} {'scenario':>58} {'eff.@18k':>9} {'eff.@30k':>9} {'grado fonte':>44}") + for s in SC: + print(f" {s['k']:>3} {s['lab']:>58} {aliq_eff(s['fn'], 18_250.0):>9.1%} " + f"{aliq_eff(s['fn'], 30_000.0):>9.1%} {s['src']:>44}") + print("\n (eff.@18k = EUR 50/giorno, il bersaglio del progetto; eff.@30k = un anno buono.)") + + print(f"\n Effetto sulla scala di conti, versamento EUR 500/mese (l'unica riga su cui §37") + print(f" ha concluso 'il biglietto batte il versare'):") + Bk33 = out[(500.0, 0.33)][0] + print(f" {'':>3} {'aliquota eff.':>13} {'EUR/g med':>10} {'P(>=50/g)':>10} {'P(zero)':>9} " + f"{'cassa mediana':>14} {'media':>12} {'vs LIBRO(33%)':>14}") + sc_rows = [] + for s in SC: + r = ladder_tax(R_M, G_M, N_LAD, LEV_M, SEED_LAD, s["fn"], refund=True, + deposit_eur=500.0, min_payout=MIN_PAYOUT, boot=BOOT) + win = "BIGLIETTO" if r["mean_cash"] > Bk33["mean_cash"] else "LIBRO" + sc_rows.append(dict(k=s["k"], tag=s["tag"], eff=aliq_eff(s["fn"], 18_250.0), **r)) + print(f" {s['k']:>3} {aliq_eff(s['fn'], 18_250.0):>13.1%} {r['med']:>10.2f} " + f"{r['p_target']:>10.1%} {r['p_zero']:>9.1%} ${r['med_cash']:>13,.0f} " + f"${r['mean_cash']:>11,.0f} {win:>14}") + SCR = pd.DataFrame(sc_rows) + lo, hi = SCR["eff"].iloc[1:].min(), SCR["eff"].max() + cme = out[(500.0, 0.33)][2] + cxx = out[(500.0, 0.33)][3] + print(f"\n Banda plausibile delle aliquote effettive (escluso il controllo): " + f"{lo:.1%} - {hi:.1%}.") + print(f" Soglia di ribaltamento (LIBRO al 33%): sulla MEDIA " + f"{'tau = ' + format(cme, '.1%') if cme is not None else 'FUORI dalla griglia 0-100%'}" + f" | sull'ECCEDENZA " + f"{'tau = ' + format(cxx, '.1%') if cxx is not None else 'FUORI dalla griglia 0-100%'}") + dentro = (cme is not None) and (lo <= cme <= hi) + print(f" >>> ATTESA A4 ('la soglia cade DENTRO la banda'): " + f"{'CONFERMATA' if dentro else 'REFUTATA'}") + e_pro = float(SCR.loc[SCR["tag"] == "PROGETTO", "eff"].iloc[0]) + e_occ = float(SCR.loc[SCR["tag"] == "OCC", "eff"].iloc[0]) + e_o30 = float(SCR.loc[SCR["tag"] == "OCC30", "eff"].iloc[0]) + print(f" >>> ATTESA A5 ('la progressiva a EUR 18k sta SOTTO il forfait 33% del progetto'): " + f"occasionale {e_occ:.1%} contro forfait {e_pro:.1%} -> " + f"{'CONFERMATA' if e_occ < e_pro else 'REFUTATA'}.") + print(f" E il seguito e' il punto trasferibile: la STESSA qualificazione, con un reddito") + print(f" preesistente di EUR 30.000, sale a {e_o30:.1%} — " + f"{e_o30/e_occ:.2f}x. **L'aliquota non e' una proprieta' della strategia, e' una") + print(f" proprieta' del contribuente**, e il canale funded e' l'unico pezzo del") + print(f" progetto in cui questo e' vero (le plusvalenze cripto hanno aliquota fissa).") + + hr("§6.2 LA CATEGORIA CAMBIA ANCHE LA BASE, non solo l'aliquota") + print(" art. 71 c.2 TUIR (testo verificato in sessione): i redditi di cui all'art. 67 c.1") + print(" lett. l) sono 'la DIFFERENZA fra l'ammontare percepito nel periodo di imposta e le") + print(" spese specificamente inerenti alla loro produzione' -> **sotto lavoro autonomo le") + print(" quote di challenge SI DEDUCONO**; sotto un'imposta sostitutiva su reddito") + print(" finanziario NO (una challenge persa non e' una minusvalenza da cripto-attivita'),") + print(" e sotto forfettario nemmeno (le sostituisce il coefficiente).") + sp_med = float(lad[(500.0, 0.33)]["spent"]) + sp_anno = sp_med / 3.0 / EURUSD + print(f"\n Quote comprate in media su 36 mesi: ${sp_med:,.0f} = EUR {sp_anno:,.0f}/anno.") + print(f" {'scenario':>52} {'senza deduzione':>16} {'con deduzione':>14} {'delta':>8}") + for lab, fn_nd, fn_d in ( + ("lav.aut. OCCASIONALE, nessun altro reddito", + net_prog(0.0, "occasionale", deduci=False), net_prog(0.0, "occasionale")), + ("lav.aut. ABITUALE INPS piena, altro reddito 30k", + net_prog(30_000.0, "piena", deduci=False), net_prog(30_000.0, "piena"))): + a = aliq_eff(fn_nd, 18_250.0, sp_anno) + b = aliq_eff(fn_d, 18_250.0, sp_anno) + print(f" {lab:>52} {a:>16.2%} {b:>14.2%} {b-a:>+8.2%}") + print(" -> la categoria con l'aliquota PIU' ALTA e' anche l'unica che deduce il costo dei") + print(" biglietti: l'ordine fra le qualificazioni non e' l'ordine fra le aliquote.") + + hr("§6.1 ROBUSTEZZA — i due numeri della ricerca che NON ho verificato su fonte primaria") + print(" (a) secondo scaglione IRPEF 2026: 33% (L.199/2025) contro il 35% del 2025.") + for lab, sg in (("33% (2026)", IRPEF_SCAGLIONI), ("35% (2025)", IRPEF_2025)): + f1 = net_prog(0.0, "occasionale", sg) + f2 = net_prog(30_000.0, "occasionale", sg) + print(f" {lab:>11}: occasionale {aliq_eff(f1, 18_250.0):.2%} " + f"con altro reddito 30k {aliq_eff(f2, 18_250.0):.2%}") + print(" (b) chi versa i 2/3 di Gestione Separata con committente ESTERO — buco documentale:") + for lab, inps in (("committente versa 2/3", "occasionale"), ("tutto sul prestatore", "piena")): + print(f" {lab:>22}: eff.@18k " + f"{aliq_eff(net_prog(0.0, inps), 18_250.0):>6.1%} " + f"@30k {aliq_eff(net_prog(0.0, inps), 30_000.0):>6.1%}") + + # ---------------------------------------------------------------- §7 verdetto + hr("§7. VERDETTO — e la domanda, scritta in modo che una risposta sia possibile") + r33 = lad[(500.0, 0.33)] + print(f" 1. Il forfait 33% NON e' l'aliquota di questa cosa: sul funded il trader non") + print(f" possiede il conto ne' le posizioni, non c'e' realizzo di cripto-attivita', e") + print(f" l'art. 67 c-sexies (33%) e c-quater (26%) — le due caselle su cui il progetto") + print(f" discute per il LIBRO — sono entrambe FUORI CATEGORIA qui. Le fonti") + print(f" professionali convergono su LAVORO AUTONOMO. Nessuna prassi ufficiale esiste.") + print(f" 2. La conclusione di §37 ('il biglietto batte il versare' a EUR 500/mese) " + f"{'REGGE' if cme is None or cme > hi else 'NON REGGE'} " + f"su tutta la banda plausibile.") + print(f" 3. Il costo della QUESTIONE, misurato: passare dall'aliquota piu' bassa a quella") + print(f" piu' alta della banda vale, sulla cassa mediana a 36 mesi,") + lo_r = SCR.loc[SCR["eff"].iloc[1:].idxmin()] + hi_r = SCR.loc[SCR["eff"].idxmax()] + print(f" ${lo_r['med_cash']:,.0f} -> ${hi_r['med_cash']:,.0f} " + f"({(hi_r['med_cash']/max(lo_r['med_cash'],1)-1)*100:+.0f}%), e su P(>=50/g) " + f"{lo_r['p_target']:.1%} -> {hi_r['p_target']:.1%}.") + d_eur = (hi_r["med_cash"] - lo_r["med_cash"]) / EURUSD / (HORIZON_DAYS / 365.0) / 365.0 + print(f" In EUR/GIORNO di rendita a 36 mesi: **{d_eur:+.2f} EUR/giorno** fra i due") + print(f" estremi della banda, contro un bersaglio di 50.") + print(f" 4. Al 33% di oggi la scala di conti da' EUR/g mediana {r33['med']:.2f} e " + f"P(>=50/g) {r33['p_target']:.1%}: **l'aliquota non e' cio' che tiene lontano il") + print(f" bersaglio**, lo tiene lontano la distribuzione.") + + print("\n LA DOMANDA AL COMMERCIALISTA (accanto a quella gia' aperta dal 07/08 sui") + print(" derivati, 26% contro 33% — e questa e' una domanda DIVERSA, non la stessa):") + print(" Q1. Il profit share pagato da una prop firm estera a chi NON possiede il conto,") + print(" NON possiede le posizioni e NON apporta capitale e': art. 67 c.1 lett. l),") + print(" art. 53, art. 55, oppure art. 67 c.1 lett. c-quater TUIR? **Indichi la lettera.**") + print(" Q2. Una prop firm non residente e senza stabile organizzazione puo' operare una") + print(" ritenuta italiana sul payout? (SI'/NO)") + print(" Q3. Superata la soglia di EUR 5.000 di compensi occasionali, con committente") + print(" (a) UE e (b) extra-UE: l'aliquota di Gestione Separata che grava sul") + print(" PRESTATORE e' 1/3 di 26,07% o l'intero 26,07%? (una risposta per caso)") + print(" Q4. Con partita IVA: quale ATECO, e quindi quale coefficiente forfettario,") + print(" 78% o 67%? (una sigla e una percentuale)") + print(" Q5. Il funded account va in quadro RW / IVAFE? (SI'/NO) Se no, l'obbligo si") + print(" limita al conto estero su cui incasso?") + print("\n ONESTA' OBBLIGATORIE:") + print(" * questo NON e' un parere fiscale, e nessuna riga qui sopra e' una consulenza.") + print(" * l'aliquota EFFETTIVA degli scenari progressivi e' calcolata SENZA detrazioni") + print(" (conservativo: le detrazioni la abbassano) e con addizionali al minimo di legge.") + print(" * il modello tassa i payout dell'anno via via che arrivano, non a saldo: toglie") + print(" liquidita' prima del dovuto, quindi e' conservativo sulla ricomposizione.") + print(" * tutte le assunzioni di §37 restano dentro: morte-firm 10%/anno (mai stimata),") + print(" cap $200k/trader, XS01 su U13 fuori campione, ancora giornaliera di TP01 fuori") + print(" dalla banda. Il fisco e' UN parametro fra questi, e non il piu' incerto.") + print(f"\n fatto in {time.time()-T0:.0f}s") + print("=" * 104) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823b_ladder_redo.py b/scripts/research/r0823b_ladder_redo.py new file mode 100644 index 0000000..ac8eae4 --- /dev/null +++ b/scripts/research/r0823b_ladder_redo.py @@ -0,0 +1,794 @@ +"""r0823b_ladder_redo — §60 LADDER-REDO: la politica multi-conto e' ancora MISTO? (2026-08-23) + +LA DOMANDA +---------- +Il 2026-07-25 (`r0725_prop_ladder.py` + `r0725_prop_coupled.py`) il progetto scelse la politica +multi-conto **MISTO** (sleeve diversi su conti diversi) contro CONC-DIV, SPARSO e CONC-2SL: + + MISTO 10,70 EUR/g mediana, P(>=50/g) 20,7%, P(zero) 33,4% + > CONC-DIV 5,70 / 17,8% / 33,4% + > SPARSO 0,00 / 16,8% / 66,0% + > CONC-2SL 0,00 / 13,1% / 55,6% (close-only, leva 1,00x) + +Da allora il numero operativo del canale funded e' passato **da 42% a 2,6%** (fattore 16) per +tre correzioni misurate in due giorni: + (a) universo XS01 **fuori campione** — 6 delle 19 gambe non esistevano nel 2021-23 e valevano + il **42% del J pubblicato** (§29); + (b) **banda d'ancora di SKH01** — l'offset canonico e' al 91° pctl dei 23 (§32); + (c) **funding** — mai modellato nel 2025, costa dJ -0,049 su 69/69 celle appaiate (§35, §37). +Piu' due lenti che il 25/07 non aveva o aveva diverse: la **wick accoppiata** (misurata il +25/07 sera, dopo il ladder) e il de-luck **x0,89** invece di x0,60 (misurato il 26/07). + +**Nessuno ha ri-misurato la politica.** La conclusione "MISTO" e' stata EREDITATA. + +LE ATTESE, REGISTRATE PRIMA DI MISURARE +--------------------------------------- +A1. **La replica dell'ORDINE riuscira', quella dei LIVELLI no.** `pl.sleeve_panel` ri-deriva le + serie dal feed corrente (+4 settimane dal 25/07): §37 ha gia' visto -11% su `r0725_hyro` + per la stessa ragione. Mi aspetto livelli 10-30% piu' bassi e ordine identico. +A2. **Il funding NON puo' ribaltare l'ordine, e semmai lo RAFFORZA.** Il costo di funding per + sleeve e' misurato (§35/§37, a peso 1,0): TP01 2,10%/a, SKH01 2,32%/a, XS01 1,45%/a. Il + costo medio di politica e' quindi CONC-2SL 2,16% > CONC-A 2,04% > CONC-DIV 1,98% > + SPARSO 1,96% > **MISTO 1,82%** — cioe' il vincitore del 25/07 e' anche quello che paga + MENO funding, ma lo spread e' solo **0,33 pp/anno** (un sesto del costo). Previsione: + tutti i livelli scendono, l'ordine resta, il delta MISTO-vs-resto si allarga di poco e + dentro il rumore. +A3. **La correzione che PUO' ribaltare l'ordine e' XS01, non il funding.** MISTO e SPARSO + poggiano su conti a **XS01 puro** (2/6 e 2/6 slot); se il drift di XS01 fuori campione e' + piu' debole (Sharpe 1,04 e maxDD -19,2% contro -11,2% dello sleeve ufficiale, §29), sono + proprio quelle politiche a perdere. Previsione: **se l'ordine cambia, cambia CONTRO + MISTO/SPARSO e a favore di CONC-DIV / CONC-A**, e per l'universo, non per il funding. +A4. **Il numero di conti e' il vero vincolo, e non e' stato mai misurato.** Con $654 di cassa + e un biglietto HYRO $100k a $579 si compra **UN** conto e restano $75: mi aspetto che la + stragrande maggioranza dei percorsi non arrivi MAI a due conti simultanei, e quindi che + **MISTO e CONC-DIV, che condividono lo slot 0, siano quasi indistinguibili** finche' non + entrano soldi. Previsione: la politica conta poco senza versamenti e molto con versamenti. +A5. **Il deposito rimborsabile (§30) fa piu' differenza sulla SCALA che sull'EV**, perche' i + $579 che tornano ricomprano un biglietto: +1-3pp su P(>=50/g), come previde §37. + +COSA FA +------- + §1 REPLICA del 25/07 (`pl.simulate` close-only/indip, `pc.simulate` accoppiata). + §2 CONTROLLI: `ladder_multi` bit-exact contro `pc.simulate`; controllo POSITIVO (una politica + deliberatamente rotta deve finire ultima, o il confronto non ha potenza). + §3 DECOMPOSIZIONE a un grado di liberta' per volta: lente -> de-luck -> universo/finestra -> + funding. E' il metodo di §29: senza, non si sa QUALE correzione fa cosa. + §4 BANDA D'ANCORA: 8 offset x 3 fasi (sottocampione DICHIARATO), con e senza funding, tutte + le politiche insieme sugli STESSI sorteggi -> mediana delle differenze APPAIATE. + §5 La previsione A2 sul funding, verificata. + §6 LA DIMENSIONE MAI TESTATA: la traiettoria del NUMERO DI CONTI nel tempo, con quota + rimborsabile e con/senza versamenti. + §7 MDE, e verdetto. + +CONVENZIONI DICHIARATE +---------------------- +* Le POLITICHE sono quelle del 25/07, non ridefinite (altrimenti si misura una ridefinizione, + non una correzione). Aggiunte due sole varianti, dichiarate: **CONC-A** = W_A 38/38/25 (il + centro della regione robusta di §29, cioe' la configurazione su cui poggiano §32 e §37) e + **MISTO-A** = MISTO col nucleo W_A al posto di book_div (separa "il nucleo" dal "mescolare"). +* Lente **ACCOPPIATA** ovunque nella parte corretta; close-only e indipendente solo come + repliche/controlli. De-luck **x0,89** (conservativo: §32 nota che sulla dimensione d'ancora + e' un doppio conteggio). +* Leva **fissa** e uguale per tutte le politiche: il confronto e' appaiato, e `best_lev` per + configurazione introdurrebbe un grado di liberta' diverso per politica. Girata a 1,00x + (grid piena) e 0,75x (controllo). +* Firm death 10%/anno, cap $200k/firm, fisco 33%, split 80% — tutte ereditate, nessuna scelta. +* Bootstrap a blocchi (20 g) **condiviso**: stessi indici e stessi sorteggi di morte-firm per + TUTTE le politiche -> differenze appaiate. + +Rete: NESSUNA (`--no-net` forzato; cache funding e cache Binance gia' su disco). +Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0823b_ladder_redo.py` +""" +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)) + +if "--no-net" not in sys.argv: + sys.argv.append("--no-net") + +import r0725_prop_coupled as pc # noqa: E402 lente accoppiata (25/07) +import r0725_prop_ladder as pl # noqa: E402 ladder originale + regole firm (25/07) +import r0822_prop_alloc as PA # noqa: E402 bootstrap/J (22/08) +import r0822b_prop_recal as PR # noqa: E402 (R,G) su pannello libero (22/08) +import r0822b_xs01_oos as X # noqa: E402 motore XS01 + U13 Binance (22/08) +import r0822c_skh_anchor_prop as AP # noqa: E402 offset di SKH01 (22/08) +import r0822d_funding as FD # noqa: E402 funding (22/08) +import r0822d_gate_c as GC # noqa: E402 funding per-sleeve + ladder a 1 cfg (22/08) +from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402 + +FD.USE_NET = False + +EURUSD = pl.EURUSD +TAX_RATE = pl.TAX_RATE +SPLIT = pl.SPLIT +FIRMS = pl.FIRMS +MAXC = pl.MAX_CONCURRENT # 6 slot +HDAYS = pl.HORIZON_MONTHS * 30 # 1080 +DELUCK_NEW = PA.DELUCK # 0,89 (26/07) +DELUCK_OLD = pl.DELUCK # 0,60 (25/07) +TARGET = pl.TARGET_EUR_DAY +MIN_PRICE = min(p for F in FIRMS for (_s, p) in F["tickets"]) + +# --- sottocampioni DICHIARATI (a priori, uniformi) ------------------------------------------- +OFFSETS = AP.OFFSETS[::3] # 8 degli 23 offset: 0,90,...,630 — uniforme a priori +PHASES = GC.PHASES_SUB # (0,3,7) — lo stesso di §37 +N_PATHS = 1500 # risoluzione MC dichiarata (~+-1pp su P(>=50/g)) +N_PATHS_REP = 3000 # per la replica: identico al 25/07 +SEED = 20260725 # il seme del 25/07 +LEV_MAIN, LEV_ALT = 1.00, 0.75 + +# `--quick` = AUTO-TEST: esercita ogni ramo di codice in ~2 minuti. NON produce numeri da citare +# (griglia e percorsi ridotti); serve a non scoprire un errore di stampa dopo venti minuti. +QUICK = "--quick" in sys.argv +if QUICK: + OFFSETS = OFFSETS[:2] + PHASES = PHASES[:2] + N_PATHS = 300 + N_PATHS_REP = 300 + +# --- numeri PUBBLICATI da riprodurre ---------------------------------------------------------- +PUB_2507 = { # r0725_prop_ladder, CLOSE-ONLY leva 1,00x + "CONC-2SL": (0.00, 0.131, 0.556), + "CONC-DIV": (5.70, 0.178, 0.334), + "SPARSO": (0.00, 0.168, 0.660), + "MISTO": (10.70, 0.207, 0.334), +} +PUB_COUPLED = "P(>=50/g) 5,6-6,5% a 0,75x, P(zero) 52% a 0,75x / 65% a 1,00x (25/07 sera)" +PUB_GATEC = dict(p50g=0.026, pzero=0.403) # §37, W_A, funding dentro, mediana d'ancora + +# --- costi di funding per sleeve, PUBBLICATI (§35/§37), a peso 1,0 ---------------------------- +FUND_PUB = {"TP01": 0.0210, "SKH01": 0.0232, "XS01": 0.0145} + +TRIALS = 0 +T0 = time.time() + + +def bump(n: int = 1) -> None: + global TRIALS + TRIALS += n + + +def hr(t: str = "") -> None: + print("\n" + "=" * 104) + if t: + print(" " + t) + print("=" * 104) + + +def tick(lab: str) -> None: + print(f" [{time.time() - T0:6.0f}s] {lab}") + + +# ================================================================================== +# LA MACCHINA: una scala di conti che gira PIU' POLITICHE sugli STESSI sorteggi +# ================================================================================== + +def ladder_multi(Rm: np.ndarray, Gm: np.ndarray, sel: np.ndarray, n_paths: int, lev: float, + seed: int, deluck: float, refund: bool = False, deposit_eur: float = 0.0, + start_eur: float = pl.START_EUR, block: int = 20, + boot: np.ndarray | None = None, + track: bool = False) -> dict: + """`r0725_prop_coupled.simulate` generalizzato a N politiche in una corsa sola. + + Rm, Gm : (giorni, n_cfg) — ritorni di chiusura e gap (<=0) di ciascuna CONFIGURAZIONE. + sel : (n_group, n_slot) — indice di configurazione per ogni slot di ogni politica. + + I gruppi (= politiche) sono impilati sull'asse dei percorsi e ricevono **gli stessi indici + di bootstrap e gli stessi sorteggi di morte-firm**: il confronto fra politiche e' appaiato + per costruzione (stesso mondo, allocazione diversa). + + Con n_group=1 e refund/deposit spenti consuma il flusso di numeri casuali NELLO STESSO + ORDINE di `pc.simulate` -> deve essere BIT-EXACT (controllo §2). + """ + rng = np.random.default_rng(seed) + L = len(Rm) + n_grp, n_slot = sel.shape + if boot is None: + n_blocks = int(np.ceil(HDAYS / block)) + st = rng.integers(0, L - block, size=(n_paths, n_blocks)) + boot = (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :HDAYS] + # morte-firm: un solo sorteggio (n_days, n_paths, n_firm) == n_days chiamate consecutive + du = rng.random((HDAYS, n_paths, len(FIRMS))) + + Rd = Rm - (1.0 - deluck) * Rm.mean(axis=0, keepdims=True) + P = n_grp * n_paths + SELP = np.repeat(sel, n_paths, axis=0) # (P, n_slot) + rowsP = np.arange(P)[:, None] + + cash = np.full(P, start_eur * EURUSD) + phase = np.full((P, n_slot), -1, np.int8) + eq = np.ones((P, n_slot)) + firm = np.full((P, n_slot), -1, np.int8) + notional = np.zeros((P, n_slot)) + price_paid = np.zeros((P, n_slot)) + refunded = np.zeros((P, n_slot), bool) + firm_alive = np.ones((P, len(FIRMS)), bool) + cum_payout = np.zeros(P) + spent = np.zeros(P) + got_back = np.zeros(P) + n_tickets = np.zeros(P, int) + max_conc = np.zeros(P, int) + max_open = np.zeros(P, int) + ever2 = np.zeros(P, bool) + ever_slot1 = np.zeros(P, bool) # lo slot 1 e' mai stato aperto? (= la politica conta) + traj = np.zeros((HDAYS // 30, n_grp)) if track else None + p_death_day = 1.0 - (1.0 - pl.FIRM_DEATH_PER_YEAR) ** (1 / 365.0) + tick_sorted = [sorted(F["tickets"], key=lambda x: -x[0]) for F in FIRMS] + + for t in range(HDAYS): + died = np.tile(du[t], (n_grp, 1)) < p_death_day + newly = died & firm_alive + firm_alive &= ~died + if newly.any(): + for fi in range(len(FIRMS)): + kill = newly[:, fi] + if kill.any(): + hit = (firm == fi) & kill[:, None] + phase[hit] = -1 + notional[hit] = 0.0 + + if (cash >= MIN_PRICE).any(): + for k in range(n_slot): + free = phase[:, k] == -1 + if not free.any(): + continue + for fi, F in enumerate(FIRMS): + used = np.where(firm == fi, notional, 0.0).sum(axis=1) + for (size, price) in tick_sorted[fi]: + buy = (free & firm_alive[:, fi] & (cash >= price) + & (used + size <= F["cap"])) + if not buy.any(): + continue + cash[buy] -= price + spent[buy] += price + n_tickets[buy] += 1 + phase[buy, k] = 0 + eq[buy, k] = 1.0 + firm[buy, k] = fi + notional[buy, k] = size + price_paid[buy, k] = price + refunded[buy, k] = False + if k >= 1: + ever_slot1 |= buy + free = free & ~buy + if not free.any(): + break + if not free.any(): + break + + rd = Rd[boot[:, t], :] # (n_paths, n_cfg) + gd = Gm[boot[:, t], :] + rdP = np.tile(rd, (n_grp, 1)) + gdP = np.tile(gd, (n_grp, 1)) + r = rdP[rowsP, SELP] * lev + g = gdP[rowsP, SELP] * lev + + act = phase >= 0 + eq_prev = eq + eq = np.where(act, eq * (1.0 + r), eq) + eq_low = np.where(act, eq_prev * (1.0 + r + g), eq) + r_low = r + g + + for fi, F in enumerate(FIRMS): + s_ = act & (firm == fi) + if not s_.any(): + continue + ev_t, ev_dd, ev_dl = F["ev"] + fu_ml, fu_dl = F["fu"] + in_ev = s_ & (phase == 0) + in_fu = s_ & (phase == 1) + bust = ((in_ev & ((eq_low < 1.0 - ev_dd) | (r_low < -ev_dl))) + | (in_fu & ((eq_low < 1.0 - fu_ml) | (r_low < -fu_dl)))) + phase[bust] = -1 + notional[bust] = 0.0 + passed = in_ev & (eq >= 1.0 + ev_t) & ~bust + phase[passed] = 1 + eq[passed] = 1.0 + + nfu = (phase == 1).sum(axis=1) + max_conc = np.maximum(max_conc, nfu) + max_open = np.maximum(max_open, (phase >= 0).sum(axis=1)) + ever2 |= nfu >= 2 + + if (t + 1) % 30 == 0: + fu = phase == 1 + gain = np.where(fu & (eq > 1.0), (eq - 1.0) * notional, 0.0) + net = gain.sum(axis=1) * SPLIT * (1.0 - TAX_RATE) + cum_payout += net + cash += net + pay = fu & (eq > 1.0) + eq = np.where(pay, 1.0, eq) + if refund: + back = pay & ~refunded + amt = np.where(back, price_paid, 0.0).sum(axis=1) + cash += amt + got_back += amt + refunded |= back + if deposit_eur: + cash += deposit_eur * EURUSD + if track: + traj[(t + 1) // 30 - 1] = nfu.reshape(n_grp, n_paths).mean(axis=1) + + out = [] + for gi in range(n_grp): + s = slice(gi * n_paths, (gi + 1) * n_paths) + ed = cum_payout[s] / EURUSD / (HDAYS / 365.0) / 365.0 + out.append(dict(eur_day=ed, med=float(np.median(ed)), + p10=float(np.percentile(ed, 10)), p90=float(np.percentile(ed, 90)), + mean=float(ed.mean()), + p_ge10=float((ed >= 10.0).mean()), + p_target=float((ed >= TARGET).mean()), + p_zero=float((cum_payout[s] <= 0).mean()), + n_tickets=float(n_tickets[s].mean()), + max_conc=float(max_conc[s].mean()), + max_open=float(max_open[s].mean()), + p_ever2=float(ever2[s].mean()), + p_slot1=float(ever_slot1[s].mean()), + med_cash=float(np.median(cash[s])), + mean_back=float(got_back[s].mean()), + spent=float(spent[s].mean()))) + bump(n_grp) + return dict(groups=out, traj=traj) + + +# ================================================================================== +# POLITICHE +# ================================================================================== +W_2SL = {"TP01": 0.75, "SKH01": 0.25} +W_DIV = {"TP01": 0.55, "SKH01": 0.20, "XS01": 0.25} +W_A = {"TP01": 0.38, "SKH01": 0.38, "XS01": 0.25} +W_TP = {"TP01": 1.0} +W_SKH = {"SKH01": 1.0} +W_XS = {"XS01": 1.0} +CFGS = [W_2SL, W_DIV, W_A, W_TP, W_SKH, W_XS] # indici 0..5 +I2SL, IDIV, IA, ITP, ISKH, IXS = range(6) + +POLICIES = { + "CONC-2SL ogni conto = book live": [I2SL] * MAXC, + "CONC-DIV ogni conto = diversif.": [IDIV] * MAXC, + "SPARSO 1 sleeve per conto": [ITP, IXS, ISKH, ITP, IXS, ISKH], + "MISTO meta' div / meta' single": [IDIV, IXS, IDIV, ITP, IDIV, IXS], + "CONC-A ogni conto = W_A 38/38/25": [IA] * MAXC, + "MISTO-A MISTO col nucleo W_A": [IA, IXS, IA, ITP, IA, IXS], +} +PNAMES = list(POLICIES) +SEL = np.array([POLICIES[p] for p in PNAMES], dtype=int) +SHORT = [p.split()[0] for p in PNAMES] + + +def _tab(rows: list[tuple], head: tuple) -> None: + print(" " + " ".join(f"{h:>{w}}" for h, w in head)) + for r in rows: + print(" " + " ".join(f"{v:>{w}}" for v, (_h, w) in zip(r, head))) + + +# ================================================================================== +def main() -> None: + try: + sys.stdout.reconfigure(line_buffering=True) + except Exception: # pragma: no cover + pass + print("=" * 104) + print(" §60 LADDER-REDO — la politica multi-conto e' ancora MISTO?") + print("=" * 104) + print(" ATTESE REGISTRATE (dal docstring, prima di misurare):") + print(" A1 l'ORDINE del 25/07 si replica, i LIVELLI no (+4 settimane di feed).") + print(" A2 il FUNDING non ribalta l'ordine e semmai lo rafforza (MISTO paga MENO:") + print(" 1,82%/a contro 2,16% di CONC-2SL; spread 0,33pp = un sesto del costo).") + print(" A3 se l'ordine cambia, cambia per l'UNIVERSO XS01 e CONTRO MISTO/SPARSO.") + print(" A4 con $654 si compra UN conto: la politica quasi non conta senza versamenti.") + print(" A5 il deposito rimborsabile vale piu' sulla SCALA che sull'EV (+1-3pp).") + + # ============================================================ §1 REPLICA 25/07 + hr("§1. REPLICA — i numeri pubblicati il 25/07, rigirati oggi con la stessa macchina") + M, names = pl.sleeve_panel() + print(f" pannello `pl.sleeve_panel()`: {len(M)} giorni, sleeve {names}, de-luck x{DELUCK_OLD}") + pols_old = pl.policies(names) + rows = [] + got = {} + for pname, slots in pols_old.items(): + key = pname.split()[0] + r = pl.simulate(M, slots, n_paths=N_PATHS_REP, lev=1.0, seed=SEED, intraday=False) + got[key] = (r["med"], r["p_target"], r["p_zero"]) + pmed, pp50, ppz = PUB_2507[key] + rows.append((key, f"{r['med']:.2f}", f"{pmed:.2f}", + f"{r['p_target']:.1%}", f"{pp50:.1%}", + f"{r['p_zero']:.1%}", f"{ppz:.1%}")) + bump() + _tab(rows, (("politica", 9), ("EUR/g oggi", 11), ("PUB", 6), + ("P>=50 oggi", 11), ("PUB", 6), ("P0 oggi", 8), ("PUB", 6))) + ord_now = sorted(got, key=lambda k: -got[k][1]) + ord_pub = sorted(PUB_2507, key=lambda k: -PUB_2507[k][1]) + same = ord_now == ord_pub + print(f"\n ordine su P(>=50/g) oggi: {' > '.join(ord_now)}") + print(f" ordine su P(>=50/g) 25/07: {' > '.join(ord_pub)}") + print(f" >>> ORDINE {'REPLICATO' if same else 'NON replicato'}; livelli " + f"{'piu bassi' if np.mean([got[k][1] for k in got]) < np.mean([PUB_2507[k][1] for k in PUB_2507]) else 'piu alti'}" + f" (media P(>=50/g) {np.mean([got[k][1] for k in got]):.1%} contro " + f"{np.mean([PUB_2507[k][1] for k in PUB_2507]):.1%}).") + print(" A1 " + ("CONFERMATA" if same else "REFUTATA") + + " — `pl.sleeve_panel` ri-deriva le serie dal feed CORRENTE (stessa causa del -11%") + print(" che §37 misuro' su `r0725_hyro`): la macchina e' quella, il vintage no.") + tick("§1") + + # ============================================================ §2 CONTROLLI + hr("§2. CONTROLLI — bit-exact contro `pc.simulate`, e un controllo POSITIVO") + cfgs_pc = [(0.75, 0.25, 0.0), (0.55, 0.20, 0.25), (1.0, 0.0, 0.0), + (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)] + START_OLD = "2024-01-01" + mats = [pc.book_tuples(*c, start=START_OLD) for c in cfgs_pc] + Lc = min(len(m[0]) for m in mats) + Rc = np.stack([m[0][-Lc:] for m in mats], axis=1) + Gc = np.stack([m[1][-Lc:] for m in mats], axis=1) + pc.DELUCK = DELUCK_OLD + slots_conc = [0] * MAXC + ref = pc.simulate(cfgs_pc, slots_conc, n_paths=800, lev=1.0, seed=99, + lens="coupled", start=START_OLD) + mine = ladder_multi(Rc, Gc, np.array([slots_conc]), n_paths=800, lev=1.0, seed=99, + deluck=DELUCK_OLD)["groups"][0] + d = float(np.abs(np.sort(ref["eur_day"]) - np.sort(mine["eur_day"])).max()) + d2 = float(np.abs(ref["eur_day"] - mine["eur_day"]).max()) + print(f" ladder_multi (1 gruppo) vs pc.simulate, lente accoppiata, 800 percorsi:") + print(f" max|diff| percorso-per-percorso = {d2:.3e} (ordinati: {d:.3e})") + print(f" -> {'BIT-EXACT' if d2 == 0.0 else 'DIVERGENTE'}") + # controllo positivo: una politica volutamente rotta (leva 3x = busta subito) deve finire ultima + selpos = np.array([slots_conc, slots_conc]) + rp = ladder_multi(Rc, Gc, selpos, n_paths=800, lev=1.0, seed=99, deluck=DELUCK_OLD) + rbad = ladder_multi(Rc, Gc, np.array([slots_conc]), n_paths=800, lev=3.0, seed=99, + deluck=DELUCK_OLD)["groups"][0] + ok_pos = rbad["p_target"] < rp["groups"][0]["p_target"] + print(f" controllo POSITIVO — la stessa politica a leva 3,00x (deve bustare):") + print(f" P(>=50/g) 1,00x {rp['groups'][0]['p_target']:.1%} vs 3,00x {rbad['p_target']:.1%}" + f" -> {'il confronto ha POTENZA' if ok_pos else '!! NESSUNA POTENZA'}") + print(f" controllo di pairing — due gruppi identici devono dare lo stesso vettore: " + f"max|diff| {np.abs(rp['groups'][0]['eur_day'] - rp['groups'][1]['eur_day']).max():.3e}") + tick("§2") + + # ============================================================ §3 pannello corretto + hr("§3. IL PANNELLO CORRETTO — U13 Binance fuori campione, W-LONG 2021+, funding") + Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.bin_loader, XS_UNIVERSE) + p13 = Cb[X.U13].dropna() + o13, h13, l13 = (Ob.reindex(p13.index)[X.U13], Hb.reindex(p13.index)[X.U13], + Lb.reindex(p13.index)[X.U13]) + XSP = {ph: PR.xs_tuples(p13, o13, h13, l13, phase=ph) for ph in PHASES} + bump(len(PHASES)) + xs0 = XSP[PHASES[0]] + cry_idx = pc.crypto_daily_tuples(1.0, 0.0).index + base = cry_idx.intersection(xs0.index) + first13 = xs0.index[np.flatnonzero(xs0["R"].values != 0.0)[0]] + S_LONG = max(first13, pd.Timestamp("2021-01-01", tz="UTC")) + I = base[base >= S_LONG] + print(f" W-LONG: {len(I)} giorni {I[0].date()} -> {I[-1].date()} | " + f"XS01 = U13 Binance ({len(X.U13)} gambe) | de-luck x{DELUCK_NEW}") + + rate = GC.funding_rate() + if rate is None: + print(" !! cache funding assente: il filone NON puo' rispondere alla domanda (c). STOP.") + return + xsf = GC.xs_funding_measure(verbose=False) + xs_ann = xsf["ann"] + print(f" funding: TP01/SKH01 da serie misurate; XS01 = {xs_ann*100:+.2f}%/anno costante " + f"(§37, IC95 [{(xs_ann-1.96*xsf['se'])*100:+.2f}, {(xs_ann+1.96*xsf['se'])*100:+.2f}])") + + boot = PA.boot_idx(len(I), HDAYS, N_PATHS, seed=SEED) + + def build_on(idx: pd.DatetimeIndex, xs: pd.DataFrame, off: int, + fund: bool) -> tuple[np.ndarray, np.ndarray]: + """(R, gap) di TUTTE le configurazioni su un indice e una serie XS01 dati.""" + R = np.zeros((len(idx), len(CFGS))) + G = np.zeros((len(idx), len(CFGS))) + for ci, w in enumerate(CFGS): + r, g = AP.rg_off(w, idx, xs, off) + if fund: + r = r - GC.fvec_for(w, idx, rate, off, xs_ann) + R[:, ci], G[:, ci] = r, g + return R, G + + def build(off: int, ph: int, fund: bool) -> tuple[np.ndarray, np.ndarray]: + return build_on(I, XSP[ph], off, fund) + + # costo di funding EFFETTIVO per configurazione (verifica di A2) + print(f"\n costo di funding per configurazione (%/anno di equity a leva 1,00x, off 0):") + fcost = {} + for ci, w in enumerate(CFGS): + fv = GC.fvec_for(w, I, rate, 0, xs_ann) + fcost[ci] = float(np.mean(fv)) * 365 * 100 + nm = ["book_2SL", "book_DIV", "W_A", "TP01 solo", "SKH01 solo", "XS01 solo"] + for ci in range(len(CFGS)): + pred = sum(FUND_PUB[k] * v for k, v in CFGS[ci].items()) * 100 + print(f" {nm[ci]:<11} misurato {fcost[ci]:>5.2f}%/a atteso da §35/§37 {pred:>5.2f}%/a") + # CONTROLLO: il reindex su I non deve mangiare barre in silenzio (un NaN->0 e' un costo + # sottostimato che nessun numero segnala). + cov_tp = float(GC.f_tp01_sleeve(rate).reindex(I).notna().mean()) + cov_sk = float(GC.f_skh_sleeve(rate, 0).reindex(I).notna().mean()) + print(f" CONTROLLO di copertura su W-LONG: TP01 {cov_tp:.1%} delle barre, SKH01 {cov_sk:.1%}" + f" -> {'ok' if min(cov_tp, cov_sk) > 0.99 else '!! IL REINDEX MANGIA BARRE'}") + print(" ⚠ I costi misurati sono PIU' BASSI degli attesi, e non e' un disaccordo: §35 pubblica") + print(" -2,16%/a sul 2019+, -1,39% sul 2022-03+ e -0,55% sul 2025+ (il costo SCENDE nel") + print(" tempo) e questa finestra parte nel 2021. Costo del libro 75/25, anno per anno:") + fser = pd.Series(GC.fvec_for(W_2SL, I, rate, 0, xs_ann), index=I) + print(" " + " ".join(f"{y}:{float(g.mean()*365)*100:.2f}%" + for y, g in fser.groupby(fser.index.year))) + print(f" 🚨 Su tutta la storia oraria SKH01 paga -2,32% e TP01 -2,10% (§35). Su W-LONG sono") + print(f" {fcost[ISKH]:.2f}% e {fcost[ITP]:.2f}%: **il verso del confronto TP01-vs-SKH01 sul") + print(f" funding si INVERTE cambiando finestra** — l'aritmetica con cui avevo derivato A2") + print(f" usava i numeri di storia piena, quindi la previsione va verificata, non dedotta.") + print(f" costo di funding per POLITICA (media sugli slot):") + polfund = {} + for pi, p in enumerate(PNAMES): + v = float(np.mean([fcost[c] for c in SEL[pi]])) + polfund[SHORT[pi]] = v + print(f" {SHORT[pi]:<9} {v:>5.2f}%/a") + tick("§3") + + # ============================================================ §4 decomposizione + hr("§4. DECOMPOSIZIONE — un grado di liberta' per volta (ancora 0, fase 0, leva 1,00x)") + print(" Senza questa tabella si sa che il numero e' cambiato, non QUALE correzione lo cambia.") + # gli indici di CFGS non sono quelli di cfgs_pc: si rimappa (le prime 4 politiche sono + # le uniche del 25/07; CONC-A e MISTO-A non esistevano e compaiono solo dal passo S3). + map_pc = {I2SL: 0, IDIV: 1, ITP: 2, ISKH: 3, IXS: 4} + sel4_pc = np.array([[map_pc[c] for c in POLICIES[p]] for p in PNAMES[:4]]) + bpc = PA.boot_idx(Lc, HDAYS, N_PATHS, seed=SEED) + s0 = ladder_multi(Rc, np.zeros_like(Gc), sel4_pc, N_PATHS, 1.0, SEED, DELUCK_OLD, boot=bpc) + s1 = ladder_multi(Rc, Gc, sel4_pc, N_PATHS, 1.0, SEED, DELUCK_OLD, boot=bpc) + s2 = ladder_multi(Rc, Gc, sel4_pc, N_PATHS, 1.0, SEED, DELUCK_NEW, boot=bpc) + # --- S2b/S3a: UNIVERSO e FINESTRA separati, sullo STESSO indice comune (crypto n HL n Bin). + XS_HL = pc.xsec_daily_tuples("shared") + I24 = I[I >= pd.Timestamp("2024-01-01", tz="UTC")].intersection(XS_HL.index) + b24 = PA.boot_idx(len(I24), HDAYS, N_PATHS, seed=SEED) + R2b, G2b = build_on(I24, XS_HL, 0, False) + Rchk, Gchk, Ichk = pc.book_tuples(0.55, 0.20, 0.25, start="2024-01-01") + kk = pd.DatetimeIndex(Ichk).isin(I24) + jj = pd.DatetimeIndex(I24).isin(pd.DatetimeIndex(Ichk)) + dchk = float(np.abs(Rchk[kk] - R2b[:, IDIV][jj]).max()) + print(f" CONTROLLO: `rg_off` con le tuple HL riproduce `pc.book_tuples` (book_div) su " + f"{int(kk.sum())} giorni a max|diff| = {dchk:.3e}") + s2b = ladder_multi(R2b, G2b, SEL[:4], N_PATHS, 1.0, SEED, DELUCK_NEW, boot=b24) + R3a, G3a = build_on(I24, XSP[PHASES[0]], 0, False) + s3a = ladder_multi(R3a, G3a, SEL[:4], N_PATHS, 1.0, SEED, DELUCK_NEW, boot=b24) + R3, G3 = build(0, PHASES[0], False) + s3 = ladder_multi(R3, G3, SEL[:4], N_PATHS, 1.0, SEED, DELUCK_NEW, boot=boot) + R4, G4 = build(0, PHASES[0], True) + s4 = ladder_multi(R4, G4, SEL[:4], N_PATHS, 1.0, SEED, DELUCK_NEW, boot=boot) + lab = ["S0 25/07 tale e quale (HL19 2024+, close-only, x0,60)", + "S1 + lente ACCOPPIATA (misurata il 25/07 SERA)", + "S2 + de-luck x0,89 invece di x0,60 (26/07)", + "S2b [= S2 su indice comune: controllo di macchina]", + "S3a + UNIVERSO: XS01 = U13 Binance, stessa finestra", + "S3b + FINESTRA: W-LONG 2021+ (fuori campione)", + "S4 + FUNDING = la lente onesta di oggi"] + ALL = (s0, s1, s2, s2b, s3a, s3, s4) + print(f"\n P(>=50 EUR/g) per politica, {N_PATHS} percorsi, stessi sorteggi:") + print(" " + f"{'passo':<52}" + "".join(f"{s:>10}" for s in SHORT[:4]) + f"{' ordine':<40}") + for l, st in zip(lab, ALL): + v = [g["p_target"] for g in st["groups"]] + o = " > ".join([SHORT[i] for i in np.argsort(-np.array(v))]) + print(f" {l:<52}" + "".join(f"{x:>9.1%} " for x in v) + f" {o:<40}") + print(f"\n EUR/g MEDIO per politica (la MEDIANA e' degenere: tabella sotto):") + print(" " + f"{'passo':<52}" + "".join(f"{s:>10}" for s in SHORT[:4])) + for l, st in zip(lab, ALL): + print(f" {l:<52}" + "".join(f"{g['mean']:>9.2f} " for g in st["groups"])) + print(f"\n EUR/g MEDIANA — il numero di TESTA del 25/07 era 'MISTO 10,70':") + print(" " + f"{'passo':<52}" + "".join(f"{s:>10}" for s in SHORT[:4])) + for l, st in zip(lab, ALL): + print(f" {l:<52}" + "".join(f"{g['med']:>9.2f} " for g in st["groups"])) + tick("§4") + + # ============================================================ §5 banda d'ancora + hr(f"§5. BANDA D'ANCORA — {len(OFFSETS)} offset x {len(PHASES)} fasi, " + f"funding ON/OFF, {len(PNAMES)} politiche") + print(f" offset (sottocampione uniforme DICHIARATO dei 23): {list(OFFSETS)}") + print(f" fasi XS01: {list(PHASES)} (lo stesso sottocampione di §37)") + print(f" leva {LEV_MAIN:.2f}x fissa e uguale per tutte (confronto appaiato); " + f"{N_PATHS} percorsi condivisi.") + cells = [] + REF: dict[str, np.ndarray] = {} # vettori per-percorso alla cella di riferimento + for off in OFFSETS: + for ph in PHASES: + for fund in (False, True): + R, G = build(off, ph, fund) + res = ladder_multi(R, G, SEL, N_PATHS, LEV_MAIN, SEED, DELUCK_NEW, boot=boot) + for pi, g in enumerate(res["groups"]): + cells.append(dict(off=off, ph=ph, fund=fund, pol=SHORT[pi], + p50=g["p_target"], pz=g["p_zero"], med=g["med"], + p10g=g["p_ge10"], mean=g["mean"], ever2=g["p_ever2"], + slot1=g["p_slot1"], ntix=g["n_tickets"], + maxc=g["max_conc"])) + if off == 0 and ph == PHASES[0] and fund: + REF[SHORT[pi]] = g["eur_day"] + tick(f"offset {off}") + C = pd.DataFrame(cells) + + for fund, flab in ((True, "CON funding (la lente onesta)"), (False, "SENZA funding")): + s = C[C["fund"] == fund] + print(f"\n --- {flab} — mediana sulle {len(OFFSETS)*len(PHASES)} celle d'ancora") + print(f" {'politica':>10} {'P(>=50/g)':>10} {'banda ancora p10-p90':>22} {'P(zero)':>9} " + f"{'EUR/g med':>10} {'EUR/g medio':>12} {'P(>=10/g)':>10} {'P(2o conto)':>12}") + for p in SHORT: + q = s[s["pol"] == p] + print(f" {p:>10} {q['p50'].median():>10.2%} " + f"[{np.percentile(q['p50'],10):>9.2%},{np.percentile(q['p50'],90):>9.2%}] " + f"{q['pz'].median():>9.1%} {q['med'].median():>10.2f} " + f"{q['mean'].median():>12.2f} {q['p10g'].median():>10.1%} " + f"{q['slot1'].median():>12.1%}") + print(" ordine per metrica (se le tre non concordano, la classifica e' un artefatto") + print(" della metrica scelta, non un fatto):") + for mkey, mlab in (("p50", "P(>=50/g)"), ("mean", "EUR/g medio"), ("p10g", "P(>=10/g)")): + o = s.groupby("pol")[mkey].median().sort_values(ascending=False) + print(f" {mlab:<12} {' > '.join(o.index)}") + + # riproduzione di §37 (W_A = CONC-A, funding dentro) + q = C[(C["fund"]) & (C["pol"] == "CONC-A")] + print(f"\n RIPRODUZIONE §37 (CONC-A = W_A 38/38/25, funding dentro, mediana d'ancora):") + print(f" P(>=50/g) {q['p50'].median():.1%} (PUB {PUB_GATEC['p50g']:.1%}) | " + f"P(zero) {q['pz'].median():.1%} (PUB {PUB_GATEC['pzero']:.1%})") + print(" ⚠ non e' identica per costruzione: §37 usa `best_lev` per configurazione e 23") + print(" offset x 3 fasi; qui la leva e' FISSA a 1,00x per tutte le politiche (appaiato)") + print(f" e gli offset sono {len(OFFSETS)}. L'ordine di grandezza e' il controllo.") + + # differenze APPAIATE cella per cella + print(f"\n DIFFERENZE APPAIATE cella per cella (mediana delle differenze, mai differenza") + print(f" delle mediane), funding dentro. Riferimento = MISTO (il vincitore del 25/07):") + sf = C[C["fund"]] + piv = sf.pivot_table(index=["off", "ph"], columns="pol", values="p50") + ncell = len(piv) + print(f" {'politica':>10} {'d P(>=50/g) vs MISTO':>22} {'>0 su celle':>13} " + f"{'p10':>8} {'p90':>8}") + for p in SHORT: + if p == "MISTO": + continue + d = piv[p] - piv["MISTO"] + print(f" {p:>10} {d.median()*100:>21.2f}pp {int((d>0).sum()):>7}/{ncell:<5} " + f"{np.percentile(d,10)*100:>7.2f}pp {np.percentile(d,90)*100:>7.2f}pp") + # --- controllo di LEVA: l'ordine dipende dalla leva scelta? + print(f"\n CONTROLLO DI LEVA — le stesse celle a {LEV_ALT:.2f}x (off 0, {len(PHASES)} fasi,") + print(f" funding ON). Se l'ordine cambia con la leva, non e' una proprieta' della politica:") + alt = [] + for ph in PHASES: + R, G = build(0, ph, True) + res = ladder_multi(R, G, SEL, N_PATHS, LEV_ALT, SEED, DELUCK_NEW, boot=boot) + for pi, g in enumerate(res["groups"]): + alt.append(dict(ph=ph, pol=SHORT[pi], p50=g["p_target"], pz=g["p_zero"], + mean=g["mean"])) + A = pd.DataFrame(alt) + oa = A.groupby("pol")["p50"].median().sort_values(ascending=False) + o1 = C[(C["fund"]) & (C["off"] == 0)].groupby("pol")["p50"].median().sort_values(ascending=False) + print(f" {'politica':>10} {'P(>=50/g) @0,75x':>18} {'P(>=50/g) @1,00x':>18} {'P(zero) @0,75x':>16}") + for p_ in SHORT: + print(f" {p_:>10} {A[A['pol']==p_]['p50'].median():>17.2%} " + f"{o1[p_]:>17.2%} {A[A['pol']==p_]['pz'].median():>15.1%}") + print(f" ordine @0,75x: {' > '.join(oa.index)}") + print(f" ordine @1,00x: {' > '.join(o1.index)}") + print(f" -> {'STESSO ordine' if list(oa.index) == list(o1.index) else 'ORDINE DIVERSO: la leva conta'}") + tick("§5") + + # ============================================================ §6 la previsione sul funding + hr("§6. LA PREVISIONE A2 SUL FUNDING, VERIFICATA (appaiata cella per cella)") + pv = C.pivot_table(index=["off", "ph", "pol"], columns="fund", values="p50") + pv = pv.reset_index() + print(f" {'politica':>10} {'costo fund %/a':>15} {'d P(>=50/g)':>13} {'<0 su celle':>13} " + f"{'d EUR/g med':>13}") + dmed = {} + for p in SHORT: + q = pv[pv["pol"] == p] + d = (q[True] - q[False]).values + dmed[p] = float(np.median(d)) + pm = C.pivot_table(index=["off", "ph", "pol"], columns="fund", values="med").reset_index() + qm = pm[pm["pol"] == p] + dm = (qm[True] - qm[False]).values + print(f" {p:>10} {polfund[p]:>14.2f}% {np.median(d)*100:>12.2f}pp " + f"{int((d<0).sum()):>7}/{len(d):<5} {np.median(dm):>12.2f}") + ordf_on = C[C["fund"]].groupby("pol")["p50"].median().sort_values(ascending=False).index.tolist() + ordf_off = C[~C["fund"]].groupby("pol")["p50"].median().sort_values(ascending=False).index.tolist() + a2 = ordf_on == ordf_off + print(f"\n ordine SENZA funding: {' > '.join(ordf_off)}") + print(f" ordine CON funding: {' > '.join(ordf_on)}") + print(f" >>> A2 {'CONFERMATA' if a2 else 'REFUTATA'}: il funding " + f"{'NON cambia' if a2 else 'CAMBIA'} l'ordine fra politiche.") + cc = np.corrcoef([polfund[p] for p in SHORT], [dmed[p] for p in SHORT])[0, 1] + print(f" correlazione (costo di funding della politica, danno misurato): {cc:+.2f} " + f"[atteso negativo: chi paga di piu' perde di piu']") + tick("§6") + + # ============================================================ §7 numero di conti + hr("§7. LA DIMENSIONE MAI TESTATA — il NUMERO DI CONTI non e' dato, si COMPRA") + Rf, Gf = build(0, 0, True) + scen = [("nessun versamento, quota persa", 0.0, False), + ("nessun versamento, quota RIMBORSATA (§30)", 0.0, True), + ("EUR 500/mese, quota persa", 500.0, False), + ("EUR 500/mese, quota RIMBORSATA", 500.0, True)] + trajs = {} + for lab_s, dep, ref_ in scen: + res = ladder_multi(Rf, Gf, SEL, N_PATHS, LEV_MAIN, SEED, DELUCK_NEW, + refund=ref_, deposit_eur=dep, boot=boot, track=True) + trajs[lab_s] = res + print(f"\n --- {lab_s}") + print(f" {'politica':>10} {'biglietti':>10} {'max conti':>10} {'P(mai >=2)':>11} " + f"{'P(>=50/g)':>10} {'EUR/g med':>10} {'P(zero)':>9} {'rimborso $':>11}") + for pi, g in enumerate(res["groups"]): + print(f" {SHORT[pi]:>10} {g['n_tickets']:>10.2f} {g['max_conc']:>10.2f} " + f"{1-g['p_ever2']:>10.1%} {g['p_target']:>10.1%} {g['med']:>10.2f} " + f"{g['p_zero']:>9.1%} {g['mean_back']:>11.0f}") + print("\n TRAIETTORIA del numero MEDIO di conti funded vivi (mesi 6/12/18/24/30/36):") + for lab_s in trajs: + tr = trajs[lab_s]["traj"] + print(f" {lab_s:<44}" + "".join( + f"{tr[m-1].mean():>7.2f}" for m in (6, 12, 18, 24, 30, 36))) + print("\n DISPERSIONE fra politiche (max-min di P(>=50/g)) per scenario — quanto CONTA") + print(" la scelta della politica:") + for lab_s in trajs: + v = [g["p_target"] for g in trajs[lab_s]["groups"]] + best = SHORT[int(np.argmax(v))] + print(f" {lab_s:<44} spread {max(v)-min(v):>6.2%} vincitore {best}") + tick("§7") + + # ============================================================ §8 MDE e verdetto + hr("§8. MDE — la finestra distingue davvero due politiche?") + sf = C[C["fund"]] + piv = sf.pivot_table(index=["off", "ph"], columns="pol", values="p50") + print(" TRE risoluzioni, e sono cose DIVERSE. Confonderle e' il modo standard di trasformare") + print(" rumore in classifica.") + print(f"\n (a) MONTE CARLO appaiato, cella di riferimento (off 0, fase {PHASES[0]}, funding ON):") + print(" i due esiti sono osservati sugli STESSI percorsi -> SE di McNemar sui discordanti.") + print(f" {'coppia':>22} {'d P(>=50/g)':>13} {'discordanti':>12} {'SE':>8} {'z':>8}") + base = REF["MISTO"] >= TARGET + for p_ in SHORT: + if p_ == "MISTO": + continue + oth = REF[p_] >= TARGET + n01 = int((base & ~oth).sum()) + n10 = int((~base & oth).sum()) + d = float(base.mean() - oth.mean()) + se = float(np.sqrt(n01 + n10) / len(base)) if (n01 + n10) else float("nan") + z = d / se if se and se > 0 else float("nan") + print(f" {'MISTO - ' + p_:>22} {d*100:>+12.2f}pp {n01 + n10:>12} " + f"{se*100:>7.2f}pp {z:>+8.2f}") + print(" ⚠ e' la risoluzione della SIMULAZIONE, non dell'evidenza: dice se due politiche") + print(" differiscono SU QUESTI percorsi, non se differirebbero nel mondo.") + print(f"\n (b) BANDA D'ANCORA sulle {len(piv)} celle (mediana delle differenze APPAIATE).") + print(" 🚨 NON e' un intervallo di confidenza e non ci si attacca un p-value (regola §57:") + print(" le 24 ancore correlano 0,63 e valgono ~2 osservazioni indipendenti). Si legge come") + print(" robustezza alla SCELTA dell'ancora, e basta:") + print(f" {'coppia':>22} {'mediana':>11} {'p10':>11} {'p90':>11} {'segno tenuto':>14}") + for p_ in SHORT: + if p_ == "MISTO": + continue + d = (piv["MISTO"] - piv[p_]).values + print(f" {'MISTO - ' + p_:>22} {np.median(d)*100:>+10.2f}pp " + f"{np.percentile(d,10)*100:>+10.2f}pp {np.percentile(d,90)*100:>+10.2f}pp " + f"{int((d>0).sum()):>6}/{len(d):<7}") + yrs = len(I) / 365.25 + print(f"\n (c) DATO — l'unica che limita davvero. La finestra e' {yrs:.1f} anni e i {N_PATHS}") + print(f" percorsi sono ricampionamenti degli STESSI {len(I)} giorni: il bootstrap non crea") + print(f" informazione. MDE sul drift ~ 1,96/sqrt({yrs:.1f}) = {1.96/np.sqrt(yrs):.2f} di Sharpe.") + print(" Il differenziale di FUNDING fra politiche misurato in §3 e' ~0,10pp/anno di drift:") + print(" irrisorio rispetto all'MDE. Cio' che le politiche possono davvero separare non e'") + print(" un differenziale di rendimento ma la GEOMETRIA — correlazione fra conti e") + print(" barriera per-conto — e quella si misura sulla forma, non sul drift.") + + hr("VERDETTO") + fin = C[C["fund"]].groupby("pol")["p50"].median().sort_values(ascending=False) + fin_med = C[C["fund"]].groupby("pol")["med"].median() + fin_pz = C[C["fund"]].groupby("pol")["pz"].median() + for p in fin.index: + print(f" {p:>10} P(>=50/g) {fin[p]:>6.2%} EUR/g med {fin_med[p]:>6.2f} " + f"P(zero) {fin_pz[p]:>6.1%}") + win = fin.index[0] + print(f"\n VINCITORE OGGI: {win} (25/07: MISTO)") + print(f" celle valutate: {len(C)} righe = {len(OFFSETS)}x{len(PHASES)}x2x{len(PNAMES)}; " + f"valutazioni MC totali contate al rialzo: {TRIALS}") + print(f" tempo {time.time()-T0:.0f}s") + print("=" * 104) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823b_obiettivo_diretto.py b/scripts/research/r0823b_obiettivo_diretto.py new file mode 100644 index 0000000..b0ca910 --- /dev/null +++ b/scripts/research/r0823b_obiettivo_diretto.py @@ -0,0 +1,877 @@ +#!/usr/bin/env python +"""r0823b_obiettivo_diretto — OTTIMIZZARE DIRETTAMENTE L'OBIETTIVO DEL PROGETTO (2026-08-23). + +§58 dell'ondata. Libro, pesi, cron, config: NON TOCCATI (isolamento provato in §0). + +IL BUCO +------- +Ogni ottimizzazione fatta in questo progetto ha massimizzato **un'altra cosa**: + * lo **Sharpe** (ogni filone di ricerca direzionale); + * **J = P(pass) x P(alive)** (PROP-ALLOC §17, PROP-RECAL §29, SKH-ANCHOR-PROP §32); + * **E[payout]** (r0725_hyro, §37); + * il **capitale mediano** o **P(arrivare al capitale-rendita)** (§36, r0807_piano_netto). +E il critico di §37 ha scritto la frase che apre questo filone: + «piu' leva sul funded ALZA E[payout] e ABBASSA P(payout): sono due obiettivi diversi». +**Nessuno ha mai ottimizzato DIRETTAMENTE `P(reddito >= 50 EUR/giorno entro H)`**, che e' +l'obiettivo DICHIARATO del progetto. I numeri operativi pubblicati (42% -> 7,8% -> 4,4% -> +2,6%) sono tutti **letti alla cella che massimizza J**, non alla cella che massimizza P. + +LE DUE DEFINIZIONI, DICHIARATE PRIMA DI MISURARE +------------------------------------------------ +Il progetto ne usa DUE senza mai metterle sulla stessa pagina, e danno risposte opposte: + + **A — RICCHEZZA (rendita sostenibile).** «reddito >= 50 EUR/g» = il patrimonio PROPRIO + raggiunge il **capitale-rendita** (muro), cioe' il capitale da cui si preleva 50 EUR/g con + P(capitale a 20 anni >= capitale iniziale) >= 90% (definizione 25/07, invariata). E' la + definizione dei muri, delle traiettorie e di tutto il piano (§36). E' **sostenibile per + costruzione**. + + **B — FLUSSO (produzione realizzata).** «reddito >= 50 EUR/g» = la media dei payout incassati + sull'orizzonte, `cum_payout / giorni`, >= 50 EUR/g. E' la definizione di `r0725_prop_ladder` + e quindi di **ogni numero del canale funded** (§29 7,8% · §32 4,4% · §37 2,6%). **NON e' + sostenibile**: un conto funded che busta il giorno dopo l'orizzonte azzera il flusso, e la + stessa cassa che il contatore chiama «reddito» resta nel conto (nel simulatore ufficiale la + cassa si accumula E conta come reddito: e' una PRODUZIONE, non un prelievo). + +Qui si misurano **entrambe**, sullo stesso Monte Carlo e sugli stessi percorsi, e si riporta +come cambia la risposta. Primaria = **A**, perche' e' quella su cui il progetto ha costruito il +muro, i versamenti e la decisione di venue; B si riporta perche' e' quella dei numeri pubblicati. + +LO SPAZIO DI POLITICA — dichiarato esaustivamente PRIMA di misurare +-------------------------------------------------------------------- + 1. `k` leva del libro proprio {1,00 · 1,25 · 1,50} (§38: la chiave NON esiste) + 2. `own` ripartizione del libro proprio {LIVE 75/25 · W_A 38/38/25} + 3. `lev_fu` leva del conto funded {0,50 · 0,75 · 1,00 · 1,25} + 4. `n_max` numero di conti concorrenti {0 · 1 · 3 · 6} (0 = nessun biglietto) + 5. `pref` taglia del biglietto {grande · piccolo} + 6. `rebuy` ri-comprare dopo un bust {si' · no} + 7. `floor` capitale proprio sotto cui NON si compra {$0 · $2.000 · $10.000} + 8. `dep` versamento mensile {EUR 0 · 250 · 500} (il piano in corso e' 500) + 9. `invest` la cassa lavora sul libro invece di restare ferma {si' · no} +Il punto (9) e' un difetto misurabile della macchineria pubblicata: in `pl.simulate` / +`pc.simulate` / `gate_c.ladder` **la cassa dei payout resta FERMA per 36 mesi**. + +LE MIE ATTESE, REGISTRATE PRIMA DI MISURARE +-------------------------------------------- + A1. L'argmax di P(>=50/g) sara' a leva **piu' alta** dell'argmax di J, e la differenza sara' + grande (fattore >2 su P). *Motivo: P(>=50/g) e' una probabilita' di superare una soglia + alta = coda destra; J e' una probabilita' di sopravvivenza = coda sinistra.* + A2. Sotto la definizione **A** (ricchezza) il canale funded **non** portera' P(36 mesi) sopra + zero in modo apprezzabile: il muro e' ~$313k e 36 mesi da $635 sono ~3 ordini di grandezza. + Quindi la risposta alla domanda «esiste una politica sopra il 20% a 36 mesi?» sara' + **SI' sotto B e NO sotto A**, e questa e' la vera risposta del filone. + A3. Investire la cassa invece di lasciarla ferma valdra' poco su B (il flusso non dipende dalla + cassa) e molto su A. *Se sbaglio e vale poco anche su A, lo dico.* + A4. L'ottimo di P sara' accompagnato da P(rovina) molto alta (>50%) — cioe' sara' un ottimo che + il progetto NON dovrebbe adottare, e va riportato come informazione, non come consiglio. + A5. Il vincolo binding a 36 mesi non sara' ne' la leva ne' il numero di conti ma il **cap + $200k/trader** oppure il **capitale che entra**. Mi aspetto il secondo. + +CONVENZIONI +----------- +* Lente **ACCOPPIATA** ovunque ci sia una barriera a un giorno (regola 25/07, riconfermata §33). +* Cella d'ancora **MEDIANA** (non la canonica, che sta al 91° pctl — §32), scelta con lo stesso + criterio di `gate_c` su un sottoinsieme d'ancore dichiarato PRIMA. +* Funding **DENTRO** (§35), de-luck **x0,89** sul drift (§ r0726_deluck_factor), fisco 33% + + patrimoniale 0,2% sul libro proprio (§ r0807), morte-firm 10%/anno, cap $200k/trader. +* Baseline del libro: **`hourly`** (la lente di ogni muro e traiettoria) — dichiarata perche' il + critico di §57 ha misurato che sotto «75/25» convivono tre baseline diverse. +* Muro: **CONGELATO** a k=1 come primaria (si de-leva al traguardo: la leva non deve poter + spostare il bersaglio); **MOBILE** come sensibilita' (§53 dichiara che la differenza vale 1,2 + anni). Banda del muro propagata dai valori di §57. +* Monte Carlo: la risoluzione si dichiara. 1500 percorsi -> SE ~1,0pp su una P vicina al 20%. + +Uso: nice -n 19 timeout 900 uv run python scripts/research/r0823b_obiettivo_diretto.py + ... --fast (griglia ridotta, ~1/3 del tempo) +Rete: NESSUNA (solo cache su disco: parquet certificati, cache Binance di §21, cache funding +di §35). Nessuna scrittura fuori da stdout. +""" +from __future__ import annotations + +import subprocess +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)) + +if "--no-net" not in sys.argv: # r0822d_funding legge USE_NET all'import + sys.argv.append("--no-net") + +import r0725_capcurve as CC # noqa: E402 TAX_RATE, EURUSD, book_series +import r0725_prop_coupled as pc # noqa: E402 lente accoppiata +import r0725_prop_ladder as pl # noqa: E402 regole firm, scala di conti +import r0727_tasse as TX # noqa: E402 PATRIMONIALE, CARRY_ANNI +import r0807_piano_netto as PN # noqa: E402 passo_fiscale, accumula, muro +import r0822_prop_alloc as PA # noqa: E402 boot_idx, DELUCK +import r0822b_prop_recal as PR # noqa: E402 obj_rg, LEVS, pannelli Binance +import r0822b_xs01_oos as X # noqa: E402 U13, motore XS01 +import r0822c_skh_anchor_prop as AP # noqa: E402 rg_off (offset di SKH01) +import r0822d_funding as FD # noqa: E402 il funding +import r0822d_gate_c as GC # noqa: E402 ladder, fvec_for, xs_funding +from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402 + +FD.USE_NET = False + +FAST = "--fast" in sys.argv +EURUSD = pl.EURUSD +TAX = CC.TAX_RATE +PATR = TX.PATRIMONIALE +DELUCK = PA.DELUCK # x0,89 misurato +W_LIVE = {"TP01": 0.75, "SKH01": 0.25} +W_A = {"TP01": 0.38, "SKH01": 0.38, "XS01": 0.25} +START_USD = 635.0 # il conto Deribit VERO di oggi +SEED_LAD = pl.HORIZON_MONTHS * 0 + 20260822 # lo stesso di gate_c (per la bit-exactness) +SEED_BOOT = 58023 +N_PATHS = 900 if FAST else 1500 +N_PATHS_J = PA.N_PATHS +MIN_PAYOUT = GC.MIN_PAYOUT_TRADER +ORIZZONTI_M = (24, 36, 60, 120) +H_MAX_D = max(ORIZZONTI_M) * 30 +MARKS = tuple(m * 30 for m in ORIZZONTI_M) +OFFSETS_SUB = tuple(range(0, 690, 90)) if not FAST else (0, 180, 360, 540) # 8 (o 4) a priori +PHASES_SUB = AP.PHASES_SUB # (0, 3, 7) — dichiarato da §32 +LEVS_FU = (0.50, 0.75, 1.00, 1.25) +NMAX = (1, 3, 6) +PREFS = ("grande", "piccolo") +REBUY = (True, False) + +# numeri PUBBLICATI da riprodurre PRIMA di produrne di nuovi +PUB = dict(muro_L3=313_143.0, perp_L3=0.0635, drift_L3=0.1519, + p50g_fund=0.026, p50g_p10=0.015, p50g_p90=0.047, pzero_fund=0.403, + p50g_nofund=0.044, + muro_p90=186_623.0, muro_m1se=709_753.0) # banda di §57 + +TRIALS = 0 +T0 = time.time() + + +def bump(n: int = 1) -> None: + global TRIALS + TRIALS += n + + +def hr(t: str = "") -> None: + print("\n" + "=" * 104) + if t: + print(" " + t) + print("=" * 104) + + +def tick(lab: str) -> None: + print(f" [{time.time() - T0:6.0f}s] {lab}", flush=True) + + +# =========================================================================== +# IL SIMULATORE UNIFICATO +# =========================================================================== +def uni(Ra: np.ndarray, Ga: np.ndarray, Rb: np.ndarray, boot: np.ndarray, *, + lev_fu: float = 1.0, n_max: int = 6, pref: str = "grande", rebuy: bool = True, + floor_usd: float = 0.0, k_book: float = 1.0, invest: bool = False, + dep_eur: float = 0.0, start_usd: float = pl.START_EUR * EURUSD, + seed: int = SEED_LAD, refund: bool = False, min_payout: float = 0.0, + tax: float = 0.0, patr: float = 0.0, walls: tuple = (), + marks: tuple = (), deposit_mode: str = "ladder", + deluck_f: float = DELUCK) -> dict: + """UN simulatore per le DUE strade, sugli STESSI percorsi. + + E' una GENERALIZZAZIONE STRETTA di `gate_c.ladder` (che a sua volta e' `pc.simulate` con + tre aggiunte): con `n_max=6, pref='grande', rebuy=True, floor=0, k_book=0, invest=False, + tax=0, patr=0, deposit_mode='ladder'` deve essere **BIT-EXACT** rispetto a `gate_c.ladder`. + E con `n_max=0, invest=True, tax=0.33, patr=0.002, deposit_mode='accumula'` deve essere + **BIT-EXACT** rispetto a `PN.accumula`. I due controlli sono in §1. + + Cio' che aggiunge, e che nessuna macchina pubblicata ha: + * un **pot proprio** che compone al libro (`k_book x Rb`) invece di restare fermo, col + fisco d'accumulo dentro (stessa `PN.passo_fiscale` del piano); + * i **payout che rientrano nel pot** e quindi compongono; + * `floor_usd` — un capitale proprio sotto cui non si compra un altro biglietto; + * `rebuy` — la scelta di NON ricomprare dopo un bust; + * `pref` — biglietto piu' grande o piu' piccolo (piu' tentativi); + * il **primo passaggio** al capitale-rendita, per l'obiettivo A. + + `Ra/Ga` = libro del conto FUNDED (ritorno e gap intraday accoppiato), `Rb` = libro PROPRIO. + Il de-luck si applica al DRIFT di R (non al gap): stessa convenzione di `ladder`. + """ + rng = np.random.default_rng(seed) + n_paths, n_days = boot.shape + Ram = np.asarray(Ra, float).copy() + Ram = Ram - (1.0 - deluck_f) * Ram.mean() + Gam = np.asarray(Ga, float) + Rbm = np.asarray(Rb, float).copy() + Rbm = Rbm - (1.0 - deluck_f) * Rbm.mean() + + n_slot = int(n_max) + FIRMS = pl.FIRMS + cash = np.full(n_paths, float(start_usd)) + basis = cash.copy() # base fiscale del pot + carry = np.zeros((n_paths, TX.CARRY_ANNI)) + anno_start = cash.copy() + flusso = np.zeros(n_paths) + cum_payout = np.zeros(n_paths) + spent = np.zeros(n_paths) + got_back = np.zeros(n_paths) + deposited = np.zeros(n_paths) + n_payouts = np.zeros(n_paths, int) + n_tickets = np.zeros(n_paths, int) + n_pass = np.zeros(n_paths, int) + peak_notional = np.zeros(n_paths) + + if n_slot: + Rs_full = Ram[boot] * lev_fu # (paths, days) — tutti i conti uguali + Gs_full = Gam[boot] * lev_fu + phase = np.full((n_paths, n_slot), -1, np.int8) + eq = np.ones((n_paths, n_slot)) + firm = np.full((n_paths, n_slot), -1, np.int8) + notional = np.zeros((n_paths, n_slot)) + price_paid = np.zeros((n_paths, n_slot)) + refunded = np.zeros((n_paths, n_slot), bool) + used = np.zeros((n_paths, n_slot), bool) + firm_alive = np.ones((n_paths, len(FIRMS)), bool) + p_death_day = 1.0 - (1.0 - pl.FIRM_DEATH_PER_YEAR) ** (1 / 365.0) + Rbp = Rbm[boot] if (invest and k_book > 0) else None + + hit = {w: np.full(n_paths, -1, int) for w in walls} + snap: dict[int, dict] = {} + fiscale = (tax > 0 or patr > 0) + + for t in range(n_days): + # ---- morte di una firm (identico a ladder: il draw avviene SEMPRE, anche a 0 slot, + # solo se il canale funded esiste) + if n_slot: + died = rng.random((n_paths, len(FIRMS))) < p_death_day + newly_dead = died & firm_alive + firm_alive &= ~died + if newly_dead.any(): + for fi in range(len(FIRMS)): + kill = newly_dead[:, fi] + if kill.any(): + h = (firm == fi) & kill[:, None] + phase[h] = -1 + notional[h] = 0.0 + + # ---- acquisto biglietti + for k in range(n_slot): + free = phase[:, k] == -1 + if not rebuy: + free &= ~used[:, k] + if not free.any(): + continue + for fi, F in enumerate(FIRMS): + u = np.where(firm == fi, notional, 0.0).sum(axis=1) + order = sorted(F["tickets"], key=(lambda x: -x[0]) if pref == "grande" + else (lambda x: x[0])) + for (size, price) in order: + buy = (free & firm_alive[:, fi] & (cash >= price) + & (cash - price >= floor_usd) & (u + size <= F["cap"])) + if not buy.any(): + continue + cash[buy] -= price + flusso[buy] -= price + spent[buy] += price + n_tickets[buy] += 1 + phase[buy, k] = 0 + eq[buy, k] = 1.0 + firm[buy, k] = fi + notional[buy, k] = size + price_paid[buy, k] = price + refunded[buy, k] = False + used[buy, k] = True + free = free & ~buy + if not free.any(): + break + if not free.any(): + break + + # ---- dinamica dei conti + r = Rs_full[:, t][:, None] + g = Gs_full[:, t][:, None] + act = phase >= 0 + eq_prev = eq + eq = np.where(act, eq * (1.0 + r), eq) + eq_low = np.where(act, eq_prev * (1.0 + r + g), eq) + r_low = r + g + for fi, F in enumerate(FIRMS): + s_ = act & (firm == fi) + if not s_.any(): + continue + ev_t, ev_dd, ev_dl = F["ev"] + fu_ml, fu_dl = F["fu"] + in_ev = s_ & (phase == 0) + in_fu = s_ & (phase == 1) + bust = ((in_ev & ((eq_low < 1.0 - ev_dd) | (r_low < -ev_dl))) + | (in_fu & ((eq_low < 1.0 - fu_ml) | (r_low < -fu_dl)))) + phase[bust] = -1 + notional[bust] = 0.0 + passed = in_ev & (eq >= 1.0 + ev_t) & ~bust + phase[passed] = 1 + eq[passed] = 1.0 + n_pass += passed.sum(axis=1) + peak_notional = np.maximum(peak_notional, + np.where(phase == 1, notional, 0.0).sum(axis=1)) + + # ---- il POT proprio compone + if Rbp is not None: + cash = cash * (1.0 + k_book * Rbp[:, t]) + + # ---- versamento (convenzione `accumula`: giorno 30, 60, ...) + if deposit_mode == "accumula" and dep_eur and t % 30 == 0 and t > 0: + cash += dep_eur * EURUSD + flusso += dep_eur * EURUSD + deposited += dep_eur * EURUSD + + # ---- payout mensile + rimborso + versamento (convenzione `ladder`) + if (t + 1) % 30 == 0: + if n_slot: + fu = phase == 1 + pay = fu & (eq > 1.0) & (((eq - 1.0) * notional * pl.SPLIT) >= min_payout) + gain = np.where(pay, (eq - 1.0) * notional, 0.0) + net = gain.sum(axis=1) * pl.SPLIT * (1.0 - TAX) + cum_payout += net + cash += net + flusso += net + n_payouts += (pay.sum(axis=1) > 0).astype(int) + eq = np.where(pay, 1.0, eq) + if refund: + back = pay & ~refunded + amt = np.where(back, price_paid, 0.0).sum(axis=1) + cash += amt + flusso += amt + got_back += amt + refunded |= back + if deposit_mode == "ladder" and dep_eur: + cash += dep_eur * EURUSD + flusso += dep_eur * EURUSD + deposited += dep_eur * EURUSD + + # ---- primo passaggio al muro (obiettivo A) + for w, arr in hit.items(): + nuovi = (arr < 0) & (cash >= w) + arr[nuovi] = t + + # ---- fisco d'accumulo sul pot + if fiscale and (t + 1) % 365 == 0: + PN.passo_fiscale(cash, anno_start, flusso, carry, tax, patr) + anno_start = cash.copy() + flusso[:] = 0.0 + basis = cash.copy() + + if (t + 1) in marks: + d = t + 1 + ed = cum_payout / EURUSD / (d / 365.0) / 365.0 + contrib = float(start_usd) + deposited + snap[d] = dict(p_flow=float((ed >= pl.TARGET_EUR_DAY).mean()), + p_flow10=float((ed >= 10.0).mean()), + med_eur=float(np.median(ed)), + med_cash=float(np.median(cash)), + mean_cash=float(cash.mean()), + p10_cash=float(np.percentile(cash, 10)), + p_zero=float((cum_payout <= 0).mean()), + p_sotto=float((cash < contrib).mean()), + p_rovina=float((cash < 0.10 * contrib).mean()), + contrib=float(contrib.mean() if np.ndim(contrib) else contrib), + **{f"A{w:.0f}": float(((hit[w] >= 0) & (hit[w] < d)).mean()) + for w in walls}) + + ed = cum_payout / EURUSD / (n_days / 365.0) / 365.0 + out = dict(eur_day=ed, cash=cash, snap=snap, hit=hit, + p_target=float((ed >= pl.TARGET_EUR_DAY).mean()), + p_ge10=float((ed >= 10.0).mean()), + p_zero=float((cum_payout <= 0).mean()), + med=float(np.median(ed)), + med_cash=float(np.median(cash)), mean_cash=float(cash.mean()), + p10_cash=float(np.percentile(cash, 10)), + p90_cash=float(np.percentile(cash, 90)), + med_spent=float(np.median(spent)), mean_back=float(got_back.mean()), + n_tickets=float(n_tickets.mean()), n_pass=float(n_pass.mean()), + peak_notional=float(np.median(peak_notional)), + contributed=float(start_usd + deposited.mean())) + bump(1) + return out + + +def p_wall_entro(res: dict, wall: float, giorni: int) -> float: + """P(il patrimonio PROPRIO tocca `wall` entro `giorni`) — obiettivo A.""" + h = res["hit"][wall] + return float(((h >= 0) & (h < giorni)).mean()) + + +def anni_med(res: dict, wall: float) -> tuple[float, float]: + """(mediana INCONDIZIONATA degli anni al traguardo, P(arrivo entro la simulazione)). + Non-arrivo = +inf, MAI -1 (lezione 07/08).""" + h = res["hit"][wall] + y = np.where(h >= 0, h / 365.0, np.inf) + return float(np.median(y)), float(np.isfinite(y).mean()) + + +# =========================================================================== +def main() -> None: + try: + sys.stdout.reconfigure(line_buffering=True) + except Exception: # pragma: no cover + pass + + print("=" * 104) + print(" §58 OBIETTIVO-DIRETTO — ottimizzare `P(reddito >= 50 EUR/g entro H)`, e non altro") + print("=" * 104) + print(f" {'MODALITA RIDOTTA (--fast)' if FAST else 'griglia piena'} | " + f"{N_PATHS} percorsi | orizzonti {ORIZZONTI_M} mesi") + + # ------------------------------------------------------------ §0 isolamento + hr("§0. ISOLAMENTO — questo script NON tocca la produzione (il cron gira dal working tree)") + prot = ["src", "config", "scripts/live", "tests", "data/paper_portfolio", "data/live"] + dirty = subprocess.run(["git", "status", "--porcelain", "--"] + prot, + cwd=ROOT, capture_output=True, text=True).stdout.strip() + print(f" git status sui percorsi protetti {prot}:") + print(f" {'PULITO — nessuna modifica' if not dirty else 'SPORCO: ' + dirty}") + if dirty: + raise SystemExit("La produzione risulta modificata: mi fermo.") + print(" Nessuna scrittura su disco in tutto lo script (solo stdout). Rete: nessuna.") + + # ------------------------------------------------------------ §1 repliche + hr("§1. REPLICHE — prima di un numero nuovo, la macchina deve riprodurre i vecchi") + ok: dict[str, bool] = {} + + # (R1) il muro L3 (fisco + funding), $313.143 — §36, replicato al dollaro da §57 + print("\n R1 IL MURO della lente congiunta L3 (funding dentro, fisco dentro, de-luck x0,89)") + rateF = GC.funding_rate() + if rateF is None: + raise SystemExit("cache funding assente: senza di essa nessun numero di questo filone " + "sarebbe confrontabile coi pubblicati.") + bookA = FD.book_75_25(None) + bookF = FD.book_75_25(rateF).reindex(bookA.index) + idxF = pd.DatetimeIndex(bookA.index) + rL3 = np.asarray(bookF.values, float) + rL3 = rL3 - (1.0 - DELUCK) * rL3.mean() + prel = CC.TARGET_EUR_DAY * 365 * CC.EURUSD + t = time.time() + perp, MURO = PN.muro(rL3, prel, TAX, PATR) + ok["R1"] = abs(MURO - PUB["muro_L3"]) < 1.0 + print(f" serie {len(rL3)} giorni {idxF.min().date()} -> {idxF.max().date()} | " + f"drift {rL3.mean()*365:.4%} (pubbl. {PUB['drift_L3']:.2%})") + print(f" perpetua {perp:.4%} (pubbl. {PUB['perp_L3']:.2%}) muro ${MURO:,.0f} " + f"(pubbl. ${PUB['muro_L3']:,.0f}) -> " + f"{'ESATTO AL DOLLARO' if ok['R1'] else 'DIVERSO'} ({time.time()-t:.0f}s)") + bump(1) + tick("R1") + + # ------------------------------------------------- il pannello comune + print("\n Il pannello comune (identico a §29/§32/§37): U13 Binance, W-LONG 2021+.") + Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.bin_loader, XS_UNIVERSE) + p13 = Cb[X.U13].dropna() + XSP = {ph: PR.xs_tuples(p13, Ob.reindex(p13.index)[X.U13], Hb.reindex(p13.index)[X.U13], + Lb.reindex(p13.index)[X.U13], phase=ph) for ph in PHASES_SUB} + xs0 = XSP[PHASES_SUB[0]] + base = pc.crypto_daily_tuples(1.0, 0.0).index.intersection(xs0.index) + first13 = xs0.index[np.flatnonzero(xs0["R"].values != 0.0)[0]] + I = base[base >= max(first13, pd.Timestamp("2021-01-01", tz="UTC"))] + pc.DELUCK = DELUCK + ev_b = PA.boot_idx(len(I), 365, N_PATHS_J, seed=8220) + fu_b = PA.boot_idx(len(I), 365, N_PATHS_J, seed=8221) + xsf = GC.xs_funding_measure(verbose=False) + xs_ann = xsf["ann"] + print(f" {len(I)} giorni {I[0].date()} -> {I[-1].date()} | " + f"XS01 paga {xs_ann*100:+.2f}%/anno di funding (misurato §37)") + tick("pannello") + + # (R2) la cella d'ancora MEDIANA e il numero operativo 2,6% [1,5%, 4,7%] + print(f"\n R2 IL NUMERO OPERATIVO DI §37 — {len(OFFSETS_SUB)} offset x {len(PHASES_SUB)} " + f"fasi = {len(OFFSETS_SUB)*len(PHASES_SUB)} celle, funding DENTRO, leva argmax-J") + print(" (sottoinsieme d'ancore dichiarato PRIMA; §37 ne usa 23x3, qui se ne usano " + f"{len(OFFSETS_SUB)}x{len(PHASES_SUB)} per il tempo macchina)") + cells = [] + for off in OFFSETS_SUB: + fA = GC.fvec_for(W_A, I, rateF, off, xs_ann) + for ph in PHASES_SUB: + R0, G0 = AP.rg_off(W_A, I, XSP[ph], off) + Rx = R0 - fA + jb, lb = -1.0, None + for lv in PR.LEVS: + j = PR.obj_rg(Rx, G0, lv, ev_b, fu_b)["J"] + if j > jb: + jb, lb = j, lv + bump(len(PR.LEVS)) + lad = GC.ladder(Rx, G0, N_PATHS, lb, SEED_LAD, refund=False) + cells.append(dict(off=off, ph=ph, J=jb, lev=lb, p50=lad["p_target"], + pz=lad["p_zero"], med=lad["med"])) + bump(1) + C = pd.DataFrame(cells) + med_p, p10, p90 = C["p50"].median(), np.percentile(C["p50"], 10), np.percentile(C["p50"], 90) + ok["R2"] = PUB["p50g_p10"] <= med_p <= PUB["p50g_p90"] + print(f" P(>=50/g) mediana {med_p:.1%} banda [{p10:.1%}, {p90:.1%}] " + f"P(zero) {C['pz'].median():.1%}") + print(f" pubblicato §37: {PUB['p50g_fund']:.1%} [{PUB['p50g_p10']:.1%}, " + f"{PUB['p50g_p90']:.1%}], P(zero) {PUB['pzero_fund']:.1%} -> " + f"{'DENTRO LA BANDA' if ok['R2'] else 'FUORI'}") + k_med = int((C["p50"] - C["p50"].median()).abs().idxmin()) + OFF_M, PH_M, LEV_J = int(C.loc[k_med, "off"]), int(C.loc[k_med, "ph"]), float(C.loc[k_med, "lev"]) + print(f" CELLA D'ANCORA MEDIANA: offset {OFF_M} min, fase {PH_M}, leva argmax-J " + f"{LEV_J:.2f}x (non la canonica: 91° pctl, §32)") + fM = GC.fvec_for(W_A, I, rateF, OFF_M, xs_ann) + R_M0, G_M = AP.rg_off(W_A, I, XSP[PH_M], OFF_M) + R_M = R_M0 - fM + tick("R2") + + # il libro PROPRIO sullo stesso pannello + rb_full = FD.book_75_25(rateF) + if rb_full.index.tz is None: + rb_full.index = rb_full.index.tz_localize("UTC") + RB_LIVE = np.nan_to_num(rb_full.reindex(I).values.astype(float)) + fA_own = GC.fvec_for(W_A, I, rateF, OFF_M, xs_ann) + RB_WA = AP.rg_off(W_A, I, XSP[PH_M], OFF_M)[0] - fA_own + print(f"\n libro PROPRIO sul pannello comune:") + for lab, r in (("LIVE 75/25", RB_LIVE), ("W_A 38/38/25", RB_WA)): + print(f" {lab:<14} drift {r.mean()*365:>7.2%} vol {r.std()*365**.5:>6.2%} " + f"Sharpe {r.mean()*365/(r.std()*365**.5):>5.2f}") + + # bootstrap CONDIVISO (confronto appaiato fra tutte le politiche) + rngb = np.random.default_rng(SEED_BOOT) + nb = int(np.ceil(H_MAX_D / 20)) + st = rngb.integers(0, len(I) - 20, size=(N_PATHS, nb)) + BOOT = (st[:, :, None] + np.arange(20)[None, None, :]).reshape(N_PATHS, -1)[:, :H_MAX_D] + B36 = BOOT[:, :36 * 30] + print(f" bootstrap CONDIVISO: {N_PATHS} percorsi x {H_MAX_D} giorni, blocchi 20 -> " + f"OGNI confronto di questo filone e' APPAIATO") + + # (R3) bit-exactness: `uni` in modalita' ladder == `gate_c.ladder` + print("\n R3 BIT-EXACTNESS — `uni` in modalita' ladder deve essere `gate_c.ladder`") + ref = GC.ladder(R_M, G_M, N_PATHS, LEV_J, SEED_LAD, refund=False, boot=B36) + mine = uni(R_M, G_M, RB_LIVE, B36, lev_fu=LEV_J, n_max=pl.MAX_CONCURRENT, pref="grande", + rebuy=True, floor_usd=0.0, k_book=0.0, invest=False, dep_eur=0.0, + start_usd=pl.START_EUR * EURUSD, seed=SEED_LAD, refund=False, min_payout=0.0, + tax=0.0, patr=0.0, deposit_mode="ladder") + d1 = float(np.abs(ref["eur_day"] - mine["eur_day"]).max()) + d2 = float(np.abs(ref["cash"] - mine["cash"]).max()) + ok["R3"] = (d1 == 0.0 and d2 == 0.0) + print(f" max|dif| EUR/g = {d1:.3e} | max|dif| cassa = {d2:.3e} -> " + f"{'BIT-EXACT' if ok['R3'] else 'DIVERSO'}") + + # (R4) bit-exactness: `uni` senza conti == `PN.accumula` + print("\n R4 BIT-EXACTNESS — `uni` senza canale funded deve essere `PN.accumula`") + paths_own = (RB_LIVE - (1.0 - DELUCK) * RB_LIVE.mean())[B36] + a = PN.accumula(paths_own, 500.0, MURO, TAX, PATR, start=START_USD) + m = uni(R_M, G_M, RB_LIVE, B36, n_max=0, k_book=1.0, invest=True, dep_eur=500.0, + start_usd=START_USD, tax=TAX, patr=PATR, walls=(MURO,), + deposit_mode="accumula") + d3 = float(np.abs(a["cap"] - m["cash"]).max()) + d4 = int(np.abs(np.where(a["colpito"] >= 0, a["colpito"], -1) - m["hit"][MURO]).max()) + ok["R4"] = (d3 < 1e-6 and d4 == 0) + print(f" max|dif| capitale = ${d3:.3e} | max|dif| giorno di primo passaggio = {d4} -> " + f"{'BIT-EXACT' if ok['R4'] else 'DIVERSO'}") + print(f" (capitale mediano a 36 mesi ${np.median(m['cash']):,.0f}, " + f"P(muro) {float((m['hit'][MURO]>=0).mean()):.1%})") + + print(f"\n ESITO REPLICHE: {sum(ok.values())}/{len(ok)} — " + + ", ".join(f"{k}={'OK' if v else 'NO'}" for k, v in ok.items())) + if not all(ok.values()): + raise SystemExit("REPLICA INCOMPLETA: un numero nuovo sarebbe indistinguibile da un bug.") + tick("§1") + # ---- fine repliche; da qui i numeri NUOVI + + # muro coerente col PANNELLO su cui gira la simulazione (2021+), come sensibilita' + rI = RB_LIVE - (1.0 - DELUCK) * RB_LIVE.mean() + lo, hi = 0.0, 0.40 + for _ in range(13): + mid = (lo + hi) / 2 + sv = PN.sopravvivenza(rI, 1e6, 1e6 * mid, 20, TAX, PATR, n_paths=1500) + lo, hi = (mid, hi) if sv["p10_end"] >= 1e6 else (lo, mid) + MURO_I = prel / lo if lo > 0.002 else float("inf") + WALLS = (PUB["muro_p90"], MURO, PUB["muro_m1se"]) + print(f"\n IL BERSAGLIO (obiettivo A) — e la sua banda, propagata da §57:") + print(f" muro PRIMARIO (pubblicato, finestra piena 2019+): ${MURO:,.0f} " + f"(perpetua {perp:.2%})") + print(f" banda del drift +-1SE (§57): p90 ${PUB['muro_p90']:,.0f} ... " + f"-1SE ${PUB['muro_m1se']:,.0f}") + print(f" muro COERENTE col pannello 2021+ (drift {rI.mean()*365:.2%}): " + + (f"${MURO_I:,.0f}" if np.isfinite(MURO_I) else "IRRAGGIUNGIBILE (perpetua ~0)")) + print(" ⚠️ La simulazione gira sul pannello 2021+ (l'unico dove XS01 esiste) mentre il muro") + print(" pubblicato e' calcolato sulla finestra piena: la disparita' e' DICHIARATA e la") + print(" riga qui sopra la quantifica. Primaria resta la pubblicata, per confrontabilita'.") + tick("muro") + + def run(**kw) -> dict: + base = dict(lev_fu=1.0, n_max=0, pref="grande", rebuy=True, floor_usd=0.0, + k_book=1.0, invest=True, dep_eur=500.0, start_usd=START_USD, + seed=SEED_LAD, refund=True, min_payout=MIN_PAYOUT, tax=TAX, patr=PATR, + walls=WALLS, marks=MARKS, deposit_mode="ladder") + base.update(kw) + boot = base.pop("boot", BOOT) + return uni(base.pop("Ra", R_M), base.pop("Ga", G_M), base.pop("Rb", RB_LIVE), + boot, **base) + + # ====================================================================== + hr("§2. LE DUE DEFINIZIONI DI «REDDITO >= 50 EUR/g», SULLA STESSA POLITICA E SUGLI STESSI PATH") + print(" A = RICCHEZZA: il patrimonio PROPRIO tocca il capitale-rendita (sostenibile).") + print(" B = FLUSSO : media dei payout incassati >= 50 EUR/g (la definizione dei numeri") + print(" pubblicati del canale funded — NON sostenibile).") + print(f" Versamento: EUR 500/mese (il piano in `config/live.json`), start ${START_USD:,.0f}.") + REF = { + "LIBRO (oggi: 75/25, k=1, nessun biglietto)": dict(n_max=0), + "PROP come pubblicato (6 conti, leva argmax-J, cassa FERMA)": + dict(n_max=6, lev_fu=LEV_J, invest=False, k_book=0.0, tax=0.0, patr=0.0), + "PROP + cassa che lavora sul libro (stessa leva)": + dict(n_max=6, lev_fu=LEV_J, invest=True, k_book=1.0), + } + refres = {k: run(**v) for k, v in REF.items()} + print(f"\n {'politica':<52} {'H':>5} {'A: P(muro)':>11} {'B: P(>=50/g)':>13} " + f"{'mediana $':>11} {'P(12}") + for lab, r in refres.items(): + for m in ORIZZONTI_M: + sn = r["snap"][m * 30] + print(f" {lab if m == ORIZZONTI_M[0] else '':<52} {m:>4}m " + f"{sn[f'A{MURO:.0f}']:>11.1%} {sn['p_flow']:>13.1%} " + f"${sn['med_cash']:>10,.0f} {sn['p_sotto']:>12.1%}") + print("\n 📌 Le due definizioni NON ordinano le politiche allo stesso modo, e a 36 mesi non") + print(" sono nemmeno commensurabili: sotto A il canale funded e' un ACCELERATORE di") + print(" capitale, sotto B e' l'unico modo di avere un flusso.") + tick("§2") + + # ====================================================================== + hr("§3. LA CURVA DI LEVA — dove sta l'argmax di CIASCUN obiettivo (attesa A1)") + print(f" Cella d'ancora mediana (offset {OFF_M}, fase {PH_M}), 6 conti, cassa che lavora,") + print(" EUR 500/mese. J e' calcolato con la STESSA `PR.obj_rg` di §29/§32/§37.") + print(f"\n {'leva':>6} {'J':>7} {'B: P(>=50/g) 36m':>17} {'A: P(muro) 120m':>16} " + f"{'P(zero)':>8} {'P(12} {'mediana $ 120m':>15}") + LEVS_ALL = PR.LEVS + curve = [] + for lv in LEVS_ALL: + j = PR.obj_rg(R_M, G_M, lv, ev_b, fu_b)["J"] + bump(1) + r = run(n_max=6, lev_fu=lv) + s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30] + curve.append(dict(lev=lv, J=j, B36=s36["p_flow"], A120=s120[f"A{MURO:.0f}"], + pz=s36["p_zero"], psotto=s120["p_sotto"], med=s120["med_cash"])) + print(f" {lv:>6.3f} {j:>7.3f} {s36['p_flow']:>17.1%} {s120[f'A{MURO:.0f}']:>16.1%} " + f"{s36['p_zero']:>8.1%} {s120['p_sotto']:>12.1%} ${s120['med_cash']:>14,.0f}") + CU = pd.DataFrame(curve) + aJ = float(CU.loc[CU["J"].idxmax(), "lev"]) + aB = float(CU.loc[CU["B36"].idxmax(), "lev"]) + aA = float(CU.loc[CU["A120"].idxmax(), "lev"]) + print(f"\n argmax J = {aJ:.2f}x · argmax B(36m) = {aB:.2f}x · " + f"argmax A(120m) = {aA:.2f}x") + print(f" B alla leva argmax-J: {float(CU.loc[CU['J'].idxmax(),'B36']):.1%} -> " + f"B al proprio argmax: {CU['B36'].max():.1%} " + f"(fattore {CU['B36'].max()/max(float(CU.loc[CU['J'].idxmax(),'B36']),1e-9):.1f}x)") + print(f" ATTESA A1 ({'CONFERMATA' if aB > aJ and CU['B36'].max() > 2*float(CU.loc[CU['J'].idxmax(),'B36']) else 'PARZIALE/SMENTITA'}): " + "l'argmax di P sta a leva piu' alta di quello di J.") + tick("§3") + + # ====================================================================== + hr("§4. LO SPAZIO DI POLITICA, OTTIMIZZATO SU P DIRETTAMENTE (36 mesi, obiettivo B)") + grid = [dict(lev_fu=lv, n_max=nm, pref=pf, rebuy=rb) + for lv in LEVS_FU for nm in NMAX for pf in PREFS for rb in REBUY] + print(f" {len(grid)} celle dichiarate PRIMA: leva {LEVS_FU} x conti {NMAX} x " + f"taglia {PREFS} x rebuy {REBUY}") + print(f" fisse: libro proprio LIVE 75/25 k=1, cassa che lavora, floor $0, EUR 500/mese.") + rows = [] + for g in grid: + r = run(**g) + s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30] + rows.append(dict(**g, B36=s36["p_flow"], B24=r["snap"][24 * 30]["p_flow"], + B60=r["snap"][60 * 30]["p_flow"], B120=s120["p_flow"], + A36=s36[f"A{MURO:.0f}"], A120=s120[f"A{MURO:.0f}"], + A120lo=s120[f"A{PUB['muro_p90']:.0f}"], + A120hi=s120[f"A{PUB['muro_m1se']:.0f}"], + pz=s36["p_zero"], psotto36=s36["p_sotto"], + rov36=s36["p_rovina"], med120=s120["med_cash"], + ntick=r["n_tickets"])) + G = pd.DataFrame(rows) + print(f"\n LE 8 MIGLIORI SU B(36 mesi) — e cosa costano sugli altri assi:") + print(f" {'leva':>5} {'conti':>6} {'taglia':>8} {'rebuy':>6} {'B 24m':>7} {'B 36m':>7} " + f"{'B 60m':>7} {'A 120m':>7} {'P(zero)':>8} {'P(13} {'bigl.':>6}") + for _, r in G.sort_values("B36", ascending=False).head(8).iterrows(): + print(f" {r['lev_fu']:>5.2f} {int(r['n_max']):>6} {r['pref']:>8} " + f"{'si' if r['rebuy'] else 'no':>6} {r['B24']:>7.1%} {r['B36']:>7.1%} " + f"{r['B60']:>7.1%} {r['A120']:>7.1%} {r['pz']:>8.1%} {r['psotto36']:>13.1%} " + f"{r['ntick']:>6.1f}") + print(f"\n LE 6 MIGLIORI SU A(120 mesi, muro pubblicato ${MURO:,.0f}):") + print(f" {'leva':>5} {'conti':>6} {'taglia':>8} {'rebuy':>6} {'A 36m':>7} {'A 120m':>7} " + f"{'A120 p90':>9} {'A120 -1SE':>10} {'B 36m':>7} {'mediana $ 120m':>15}") + for _, r in G.sort_values("A120", ascending=False).head(6).iterrows(): + print(f" {r['lev_fu']:>5.2f} {int(r['n_max']):>6} {r['pref']:>8} " + f"{'si' if r['rebuy'] else 'no':>6} {r['A36']:>7.1%} {r['A120']:>7.1%} " + f"{r['A120lo']:>9.1%} {r['A120hi']:>10.1%} {r['B36']:>7.1%} " + f"${r['med120']:>14,.0f}") + bestB = G.loc[G["B36"].idxmax()] + bestA = G.loc[G["A120"].idxmax()] + print(f"\n ARGMAX B(36m) = leva {bestB['lev_fu']:.2f}x, {int(bestB['n_max'])} conti, " + f"biglietto {bestB['pref']}, rebuy {'si' if bestB['rebuy'] else 'no'} -> " + f"{bestB['B36']:.1%}") + print(f" ARGMAX A(120m) = leva {bestA['lev_fu']:.2f}x, {int(bestA['n_max'])} conti, " + f"biglietto {bestA['pref']}, rebuy {'si' if bestA['rebuy'] else 'no'} -> " + f"{bestA['A120']:.1%}") + print(f" I due argmax {'COINCIDONO' if (bestB[['lev_fu','n_max','pref','rebuy']].tolist() == bestA[['lev_fu','n_max','pref','rebuy']].tolist()) else 'SONO DIVERSI'}.") + tick("§4") + + # ====================================================================== + hr("§5. LE DIMENSIONI RESTANTI — floor, versamento, leva del libro, ripartizione propria") + bp = dict(lev_fu=float(bestB["lev_fu"]), n_max=int(bestB["n_max"]), + pref=str(bestB["pref"]), rebuy=bool(bestB["rebuy"])) + print(f" Politica di riferimento: l'argmax di B {bp}") + print(f"\n (a) FLOOR — capitale proprio sotto cui non si compra un altro biglietto") + print(f" {'floor':>10} {'B 36m':>8} {'A 120m':>8} {'P(zero)':>9} {'mediana $ 120m':>15}") + for fl in (0.0, 2_000.0, 10_000.0): + r = run(floor_usd=fl, **bp) + s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30] + print(f" ${fl:>9,.0f} {s36['p_flow']:>8.1%} {s120[f'A{MURO:.0f}']:>8.1%} " + f"{s36['p_zero']:>9.1%} ${s120['med_cash']:>14,.0f}") + print(f"\n (b) VERSAMENTO MENSILE — la leva che il progetto misura come dominante") + print(f" {'EUR/mese':>10} {'strada':>10} {'B 36m':>8} {'A 120m':>8} {'A 240m':>8} " + f"{'mediana $ 120m':>15}") + for dep in (0.0, 250.0, 500.0, 1_000.0): + for lab, kw in (("PROP", bp), ("LIBRO", dict(n_max=0))): + r = run(dep_eur=dep, **kw) + s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30] + a240 = anni_med(r, MURO) + print(f" {dep:>10,.0f} {lab:>10} {s36['p_flow']:>8.1%} " + f"{s120[f'A{MURO:.0f}']:>8.1%} {a240[1]:>8.1%} ${s120['med_cash']:>14,.0f}") + print(" (la colonna 'A 240m' e' P(traguardo) sull'intera simulazione = 120 mesi: coincide") + print(" con A 120m — la si stampa per rendere esplicito che l'orizzonte massimo e' 10 anni)") + print(f"\n (c) LEVA DEL LIBRO PROPRIO — muro CONGELATO (primaria) e MOBILE (sensibilita')") + print(" ⚠️ §38: la chiave di scala NON ESISTE in `config/live.json`; §57: a k=1,50x il") + print(" peggior giorno e' 21,48% e la guardia dello scettico (<=20%) lo BOCCIA.") + print(f" {'k':>5} {'muro mobile':>13} {'A 120m congel.':>15} {'A 120m mobile':>14} " + f"{'B 36m':>7} {'mediana $ 120m':>15}") + for k in (1.00, 1.25, 1.50): + rk = rI * k + lo, hi = 0.0, 0.40 + for _ in range(11): + mid = (lo + hi) / 2 + sv = PN.sopravvivenza(np.asarray(rL3, float) * k, 1e6, 1e6 * mid, 20, TAX, PATR, + n_paths=1200) + lo, hi = (mid, hi) if sv["p10_end"] >= 1e6 else (lo, mid) + mk = prel / lo if lo > 0.002 else float("inf") + r = run(k_book=k, walls=(MURO, mk) if np.isfinite(mk) else (MURO,), **bp) + s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30] + am = s120.get(f"A{mk:.0f}", float("nan")) if np.isfinite(mk) else float("nan") + print(f" {k:>5.2f} " + (f"${mk:>12,.0f}" if np.isfinite(mk) else f"{'IRRAGG.':>13}") + + f" {s120[f'A{MURO:.0f}']:>15.1%} " + + (f"{am:>14.1%}" if am == am else f"{'—':>14}") + + f" {s36['p_flow']:>7.1%} ${s120['med_cash']:>14,.0f}") + bump(1) + print(f"\n (d) RIPARTIZIONE DEL LIBRO PROPRIO — LIVE 75/25 contro W_A 38/38/25") + print(" (la seconda mette XS01 anche sul capitale proprio: §46 ha falsificato la soglia") + print(" dei $20k da min-order, ma la decisione di venue del 26/07 tiene tutto su") + print(" Deribit fino a $20k -> e' una sensibilita', non una proposta)") + print(f" {'libro proprio':>16} {'B 36m':>8} {'A 120m':>8} {'mediana $ 120m':>15}") + for lab, rbk in (("LIVE 75/25", RB_LIVE), ("W_A 38/38/25", RB_WA)): + r = run(Rb=rbk, **bp) + s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30] + print(f" {lab:>16} {s36['p_flow']:>8.1%} {s120[f'A{MURO:.0f}']:>8.1%} " + f"${s120['med_cash']:>14,.0f}") + print(f"\n (e) LA CASSA CHE LAVORA — attesa A3") + print(f" {'cassa':>16} {'B 36m':>8} {'A 120m':>8} {'mediana $ 36m':>14} " + f"{'mediana $ 120m':>15}") + inv = {} + for lab, kw in (("ferma (pubbl.)", dict(invest=False, k_book=0.0, tax=0.0, patr=0.0)), + ("investita", dict(invest=True, k_book=1.0))): + r = run(**{**bp, **kw}) + inv[lab] = r + s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30] + print(f" {lab:>16} {s36['p_flow']:>8.1%} {s120[f'A{MURO:.0f}']:>8.1%} " + f"${s36['med_cash']:>13,.0f} ${s120['med_cash']:>14,.0f}") + dB = (inv["investita"]["snap"][36 * 30]["p_flow"] + - inv["ferma (pubbl.)"]["snap"][36 * 30]["p_flow"]) + dA = (inv["investita"]["snap"][120 * 30][f"A{MURO:.0f}"] + - inv["ferma (pubbl.)"]["snap"][120 * 30][f"A{MURO:.0f}"]) + print(f" ATTESA A3: dB {dB*100:+.2f}pp, dA {dA*100:+.2f}pp -> " + f"{'CONFERMATA' if abs(dB) < 0.02 and dA > 0.02 else 'da leggere nel testo'}") + tick("§5") + + # ====================================================================== + hr("§6. QUANTO COSTA LA DIFFERENZA FRA L'OTTIMO E CIO' CHE IL PROGETTO FA OGGI") + oggi = run(n_max=0) + pubb = run(n_max=6, lev_fu=LEV_J, invest=False, k_book=0.0, tax=0.0, patr=0.0) + optB = run(**bp) + optA = run(lev_fu=float(bestA["lev_fu"]), n_max=int(bestA["n_max"]), + pref=str(bestA["pref"]), rebuy=bool(bestA["rebuy"])) + print(f" {'politica':<44} {'B 36m':>8} {'A 36m':>8} {'A 120m':>8} {'anni al muro':>13} " + f"{'P(16}") + for lab, r in (("OGGI: libro 75/25 k=1, nessun biglietto", oggi), + ("PUBBLICATA: 6 conti a leva argmax-J", pubb), + ("ARGMAX B (flusso)", optB), + ("ARGMAX A (ricchezza)", optA)): + s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30] + med, parr = anni_med(r, MURO) + print(f" {lab:<44} {s36['p_flow']:>8.1%} {s36[f'A{MURO:.0f}']:>8.1%} " + f"{s120[f'A{MURO:.0f}']:>8.1%} " + + (f"{med:>12.1f}a" if np.isfinite(med) else f"{'>10a':>13}") + + f" {s36['p_sotto']:>16.1%}") + dB36 = optB["snap"][36 * 30]["p_flow"] - oggi["snap"][36 * 30]["p_flow"] + dA120 = optA["snap"][120 * 30][f"A{MURO:.0f}"] - oggi["snap"][120 * 30][f"A{MURO:.0f}"] + print(f"\n COSTO DI NON OTTIMIZZARE: su B(36m) {dB36*100:+.1f}pp, " + f"su A(120m) {dA120*100:+.1f}pp.") + tick("§6") + + # ====================================================================== + hr("§7. LA DOMANDA DECISIVA — esiste una politica con P(>=50 EUR/g entro 36 mesi) > 20%?") + maxB = float(G["B36"].max()) + maxA36 = float(G["A36"].max()) + rowB = G.loc[G["B36"].idxmax()] + print(f" Sotto la definizione B (FLUSSO, quella dei numeri pubblicati):") + print(f" massimo su {len(G)} politiche = {maxB:.1%} " + f"(leva {rowB['lev_fu']:.2f}x, {int(rowB['n_max'])} conti, biglietto {rowB['pref']}, " + f"rebuy {'si' if rowB['rebuy'] else 'no'})") + print(f" ma con P(zero payout) {rowB['pz']:.1%} e P(finire sotto il versato) " + f"{rowB['psotto36']:.1%}") + print(f" -> {'SI, SOPRA IL 20%' if maxB > 0.20 else 'NO, SOTTO IL 20%'}") + print(f"\n Sotto la definizione A (RICCHEZZA sostenibile, il muro del progetto):") + print(f" massimo su {len(G)} politiche a 36 mesi = {maxA36:.2%} " + f"(a 120 mesi: {float(G['A120'].max()):.1%})") + print(f" -> {'SI' if maxA36 > 0.20 else 'NO'}, e non per poco: il muro e' " + f"${MURO:,.0f} e a 36 mesi il capitale mediano migliore e' " + f"${float(G['med120'].max()):,.0f} a 120 mesi.") + print(f"\n ATTESA A2 ({'CONFERMATA' if (maxB > 0.20 and maxA36 < 0.05) else 'SMENTITA'}): " + "«SI' sotto B e NO sotto A».") + print(f" ATTESA A4 ({'CONFERMATA' if rowB['pz'] > 0.50 or rowB['psotto36'] > 0.50 else 'SMENTITA'}): " + "l'ottimo di P e' una politica ad alta rovina.") + + # il vincolo che fissa il massimo + print(f"\n QUALE VINCOLO FISSA IL MASSIMO — si toglie un vincolo per volta (obiettivo B, 36m):") + print(f" {'scenario':<46} {'B 36m':>8} {'delta':>8}") + base_b = float(rowB["B36"]) + scen = [] + r = run(start_usd=10_000.0, **bp); scen.append(("capitale iniziale $10.000 invece di $635", r)) + r = run(dep_eur=2_000.0, **bp); scen.append(("versamento EUR 2.000/mese invece di 500", r)) + cap_old = [F["cap"] for F in pl.FIRMS] + for F in pl.FIRMS: + F["cap"] *= 5 + r = run(**bp); scen.append(("cap per firm x5 ($1M / $2M)", r)) + for F, c in zip(pl.FIRMS, cap_old): + F["cap"] = c + dead_old = pl.FIRM_DEATH_PER_YEAR + pl.FIRM_DEATH_PER_YEAR = 0.0 + r = run(**bp); scen.append(("morte-firm 0%/anno invece di 10%", r)) + pl.FIRM_DEATH_PER_YEAR = dead_old + r = run(n_max=6, **{k: v for k, v in bp.items() if k != "n_max"}) + scen.append(("6 conti concorrenti (se non lo e' gia')", r)) + for lab, r in scen: + b = r["snap"][36 * 30]["p_flow"] + print(f" {lab:<46} {b:>8.1%} {(b-base_b)*100:>+7.1f}pp") + print(f" ATTESA A5: il vincolo binding e' il capitale che entra " + f"(riga 'versamento' e riga 'capitale iniziale').") + + # ====================================================================== + hr("VERDETTO") + eur_g_oggi = oggi["snap"][36 * 30]["med_cash"] + print(f" Trial contati al rialzo: {TRIALS}. Tempo {time.time()-T0:.0f}s. " + f"Percorsi {N_PATHS} (SE ~{100*np.sqrt(0.2*0.8/N_PATHS):.1f}pp su una P vicina al 20%).") + print(f"\n * L'obiettivo del progetto ha DUE definizioni operative in uso e danno risposte") + print(f" opposte alla stessa domanda. Con la definizione del canale funded (FLUSSO) il") + print(f" massimo a 36 mesi e' {maxB:.1%}; con quella dei muri e del piano (RICCHEZZA") + print(f" SOSTENIBILE) e' {maxA36:.2%}.") + print(f" * L'ottimo di P(>=50/g) NON e' l'ottimo di J: {CU['B36'].max():.1%} contro " + f"{float(CU.loc[CU['J'].idxmax(),'B36']):.1%} alla leva argmax-J.") + print(f" * In EUR/giorno, la mediana di cio' che il libro di oggi produce a 36 mesi resta") + print(f" ZERO (non preleva), e il capitale mediano e' ${eur_g_oggi:,.0f}: la rendita") + print(f" perpetua su quel capitale vale {perp*eur_g_oggi/365/EURUSD:.2f} EUR/giorno.") + print("=" * 104) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823b_quasi_passati.py b/scripts/research/r0823b_quasi_passati.py new file mode 100644 index 0000000..de7bc91 --- /dev/null +++ b/scripts/research/r0823b_quasi_passati.py @@ -0,0 +1,964 @@ +"""§68 QUASI-PASSATI — i candidati morti per pochissimo, presi INSIEME, passano? + +DOMANDA +======= +In due mesi il progetto ha bocciato decine di candidati e molti sono morti *per un soffio*: +deflated-Sharpe 0,948 (tenore VRP, bocciato per 0,002), 0,947 (cella pubblicata DVOLSPREAD), +0,929 (STATARB same-sign), 0,905 (cella viva di PREVDAY), e `weights_tilt_null` fallito per +delta_insample -0,004 (XSR01). Nessuno ha mai chiesto se, MESSI INSIEME, formino qualcosa che +passa. La domanda e' legittima perche' sono meccanismi DIVERSI (opzioni, spread di vol, residuo +cross-asset, livelli del giorno prima, cross-section su 50 alt) -> la loro combinazione non e' +automaticamente ridondante. + +Ma e' ANCHE la forma classica del data-dredging: raccogliere i falliti finche' la media non passa. +IL COMPITO E' MISURARE QUALE DELLE DUE COSE E', NON TROVARE UN VINCITORE. + +REGOLA DI AMMISSIONE — DICHIARATA PRIMA DI MISURARE, NON CAMBIATA DOPO +===================================================================== +Entra nel paniere ogni candidato che soddisfa TUTTE E TRE: + (R1) NON e' nel libro LIVE ne' nel book di ricerca a 5 sleeve (= candidato respinto o in limbo); + (R2) l'esito pubblicato dista <= 0,05 dalla soglia del gate che lo ha fermato: + |DSR - 0,95| <= 0,05 OPPURE |delta_insample| <= 0,01 su `weights_tilt_null`; + (R3) la serie e' RICOSTRUIBILE da codice committato coi parametri congelati/pubblicati. +La rappresentante di ogni candidato e' la CELLA CONGELATA NEL MONITOR (o, se monitor non esiste, +la cella pubblicata). PESI UGUALI. NESSUNA selezione successiva: il leave-one-out e' diagnostica, +non una potatura. Nessun candidato viene aggiunto o tolto dopo aver visto un risultato. + +ATTESE A PRIORI (dichiarate qui PRIMA di misurare; a fine script si dice confermata/refutata) +============================================================================================= +A1 Il paniere avra' Sharpe piu' alto di OGNI singola gamba. E' aritmetica di diversificazione, + non evidenza: e' attesa, e da sola non decide niente. +A2 Il DSR analitico del paniere si RIBALTA col conteggio dei trial (lezione 30/07 + §10/§24): + PASS con N=K, FAIL con N=somma dei trial di famiglia. Verdetto pubblicato con la sensibilita'. +A3 Il paniere dei QUASI-PASSATI battera' quello degli SCARTATI NETTI in-sample (per costruzione: + i primi sono cellette scelte, i secondi no) MA il vantaggio si RIDURRA' sull'hold-out. +A4 Le gambe sono quasi scorrelate fra loro -> ampiezza effettiva vicina a K -> il paniere alza + lo Sharpe PER COSTRUZIONE senza contenere un solo edge nuovo. +A5 Il paniere "funziona" grazie a UNA gamba (PREVDAY, il piu' forte misurato in §50) -> il + leave-one-out lo mostrera'. +A6 Almeno una gamba e' STAT-MODE (XSR01: venue diverso, non Deribit) -> il paniere COSI' COM'E' + non e' un candidato al libro, a prescindere dal numero. +A7 Le gambe sono DECADUTE dalla loro pubblicazione (i numeri hanno 1-2 mesi): mi aspetto un + degrado, non un miglioramento. + +METODO / VINCOLI +================ +* SOLA LETTURA. Nessun file di produzione toccato, nessun ordine, nessuna chiave API. + Endpoint di rete: NESSUNO (tutto da parquet certificati + cache su disco). +* Le serie hanno date d'inizio diverse -> `combine_outer` con rinormalizzazione per riga, e la + COPERTURA PER COLONNA e' STAMPATA (prima diagnostica di un outer-join: il progetto ha gia' + pubblicato un muro sbagliato di 3x per averla saltata). +* Lente Sharpe: `altlib._sh` (365,25 g/anno) ovunque, dichiarata. +* Replica di un numero pubblicato PRIMA di produrne di nuovi (sezione 3). +* Controllo positivo obbligatorio: un rilevatore che non trova nulla e' indistinguibile da uno + rotto -> i null sono validati su casi in cui DEVONO scattare. + +USO + uv run python scripts/research/r0823b_quasi_passati.py [--draws 1000] [--force] +""" +from __future__ import annotations + +import argparse +import datetime as dt +import itertools +import math +import pickle +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +for _p in (str(ROOT), str(ROOT / "scripts" / "research"), + str(ROOT / "scripts" / "research" / "alt"), + str(ROOT / "scripts" / "research" / "ortho")): + if _p not in sys.path: + sys.path.insert(0, _p) + +# --- produzione (IMPORTATA, mai riscritta) --------------------------------------------------- +from src.portfolio.portfolio import combine_outer, metrics # noqa: E402 +from src.live import book as LB # noqa: E402 + +# --- macchineria di ricerca gia' validata (riusata, non riscritta) --------------------------- +import altlib as al # noqa: E402 +import ortholib as ol # noqa: E402 +import orthogonal_signals as OS # noqa: E402 +import r0823_prevday_gate as PG # noqa: E402 (factory + griglia 336) +import r0730_vrp_tenor_gate as VT # noqa: E402 (cell_daily, griglia 72) +import r0725_statarb_multi as SM # noqa: E402 (meccanismo XSR congelato) +import r0725_statarb_basket_gate as BG # noqa: E402 (pair_frames / demean) +from r0726_dvolspread_gate import make_book as dvs_make_book, GRID as DVS_GRID # noqa: E402 + +HOLDOUT = al.HOLDOUT +SEED = 20260823 +T0 = time.time() + +_SP = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/qp") +SCRATCH = _SP if _SP.parent.parent.exists() else Path("/tmp/pythagoras_quasi") +CACHE = SCRATCH / "families_v1.pkl" +B3RD_CACHE = SCRATCH.parent / "b3rd" / "series_v3.pkl" # cache §50 (serie ancorate, funding IN) + +CAPITAL_TODAY = 635.0 +MIN_ORDER = 5.0 +BOOK_START = pd.Timestamp("2019-03-14", tz="UTC") +W_TP, W_SKH = LB.W_TP01, LB.W_SKH +TP_HOURS = tuple(range(24)) +SKH_OFFSETS = tuple(range(0, 690, 30)) +XS_FUNDING_DRAG_YR = 0.0145 # §37, dichiarato (XS paga funding, non lo incassa) + +# --- LE CELLE CONGELATE (rappresentanti dei 5 quasi-passati) --------------------------------- +FROZEN = { + "PREVDAY": dict(tf="1h", anchor=1, k=0.30, short=True, mh=0), + "DVOLSPREAD": dict(zwin=180, tanh_k=2.0, lw=0.6, zw=1.1, tgt=0.17, svw=60), + "DVS-PUB": dict(zwin=150, tanh_k=1.8, lw=0.45, zw=1.0, tgt=0.13, svw=50), + "VRP-TENORE": dict(tenor=10, sd=-0.28, ld=-0.05), + "STATARB": dict(W=45, sgn=+1), + "XSR01": dict(W=45, sgn=+1), +} +LEGS = ["PREVDAY", "DVOLSPREAD", "VRP-TENORE", "STATARB", "XSR01"] + +# --- trial DICHIARATI per famiglia, contati AL RIALZO (regola 30/07) ------------------------- +N_TRIALS = {"PREVDAY": 336, "DVOLSPREAD": 729, "VRP-TENORE": 72, "STATARB": 10, "XSR01": 10} + + +def sez(t: str) -> None: + print("\n" + "=" * 106) + print(f" {t}") + print("=" * 106) + + +def sub(t: str) -> None: + print("\n" + "-" * 106) + print(f" {t}") + print("-" * 106) + + +def clock() -> str: + return f"[{time.time() - T0:6.1f}s]" + + +def sh(s) -> float: + return al._sh(s) + + +def shH(s) -> float: + s = s[s.index >= HOLDOUT] + return al._sh(s) if len(s) > 30 else float("nan") + + +def shI(s) -> float: + s = s[s.index < HOLDOUT] + return al._sh(s) if len(s) > 30 else float("nan") + + +def dd(s) -> float: + return al._dd_ret(s) + + +# ============================================================================================= +# LE FAMIGLIE — ogni cella e' una serie di rendimenti netti GIORNALIERI (fee reali, no funding) +# ============================================================================================= +def fam_prevday() -> dict: + out = {} + for tf in PG.G_TF: + for a, k, s_, mh in itertools.product(PG.G_ANCHOR, PG.G_K, PG.G_SHORT, PG.G_MH): + key = (tf, a, k, s_, mh) + try: + fn = PG.factory(tf=tf, anchor=a, k=k, short=s_, mh=mh) + out[key] = al.candidate_daily(fn, tf=tf) + except Exception: + continue + return out + + +def fam_dvolspread() -> dict: + out = {} + keys = list(DVS_GRID) + for combo in itertools.product(*[DVS_GRID[k] for k in keys]): + kw = dict(zip(keys, combo)) + try: + out[tuple(kw[k] for k in keys)] = ol.eval_book(dvs_make_book(**kw))["daily"].dropna() + except Exception: + continue + return out + + +def fam_vrp() -> dict: + out = {} + for tn, sd_, ld in itertools.product(VT.TENORS, VT.SHORT_DELTAS, VT.LONG_DELTAS): + try: + s = VT.cell_daily(tn, sd_, ld) + if len(s) > 200: + out[(tn, sd_, ld)] = s + except Exception: + continue + return out + + +def _spread_daily(pos, j) -> pd.Series: + ev = OS.eval_spread(j, pos) + return pd.Series(ev["net"], index=pd.DatetimeIndex(pd.to_datetime(j["datetime"], utc=True))) + + +def fam_statarb() -> dict: + j = OS.build_joint("1d") + out = {} + for W, sg in itertools.product((30, 45, 60, 90, 120), (1, -1)): + out[(W, sg)] = _spread_daily(OS.f_statarb_resid(W=W, sgn=sg)(j), j) + return out + + +def fam_xsr() -> dict: + """XSR01: meccanismo STATARB-RESID congelato sui 50 alt HL, posizioni DEMEANATE.""" + out = {} + W0, S0 = SM.W, SM.SGN + try: + for W, sg in itertools.product((30, 45, 60, 90, 120), (1, -1)): + SM.W, SM.SGN = W, sg + P, S = BG.pair_frames() + out[(W, sg)] = BG.basket_from_positions(P, S, demean=True) + finally: + SM.W, SM.SGN = W0, S0 + return out + + +def fam_ortho_rejects() -> dict: + """B4 — SCARTATI NETTI CROSS-MECCANISMO: 4 famiglie relative-value dell'onda ortho + (2026-06-21) che lo scorer INDURITO ha collassato (17/18 -> 1). Meccanismi DIVERSI fra loro + e diversi dai quasi-passati. Stesso dato, stesse fee, stessa lente.""" + j = OS.build_joint("1d") + out = {} + for nm in ("RATIO-MOM", "RATIO-REV", "RATIO-ACCEL", "VOLSPREAD"): + f, grid, _ = OS.FAMILIES[nm] + out[nm] = {tuple(sorted(p.items())): _spread_daily(f(**p)(j), j) for p in grid} + return out + + +def build_families(force: bool) -> dict: + if CACHE.exists() and not force: + print(f" {clock()} cache famiglie: {CACHE}") + return pickle.loads(CACHE.read_bytes()) + F = {} + for nm, fn in (("VRP-TENORE", fam_vrp), ("STATARB", fam_statarb), ("XSR01", fam_xsr), + ("DVOLSPREAD", fam_dvolspread), ("PREVDAY", fam_prevday)): + t = time.time() + F[nm] = fn() + print(f" {clock()} famiglia {nm:<12} {len(F[nm]):>4} celle ({time.time()-t:5.1f}s)") + t = time.time() + F["_ORTHO"] = fam_ortho_rejects() + print(f" {clock()} famiglie ortho (controllo B4): " + f"{ {k: len(v) for k, v in F['_ORTHO'].items()} } ({time.time()-t:.1f}s)") + SCRATCH.mkdir(parents=True, exist_ok=True) + CACHE.write_bytes(pickle.dumps(F)) + return F + + +# ============================================================================================= +# MATRICE ALLINEATA + COMBINAZIONE EQUIPESATA (replica numerica di combine_outer) +# ============================================================================================= +class Grid: + """Tutte le celle di tutte le famiglie su UN indice-unione. Il combinatore equipesato e' + la trasposizione numpy di `combine_outer` (verificata bit-exact in sezione 5).""" + + def __init__(self, fams: dict, lo=None): + idx = None + for f in fams.values(): + for s in f.values(): + idx = s.index if idx is None else idx.union(s.index) + if lo is not None: + idx = idx[idx >= lo] + self.idx = idx + self.cols, self.keys = {}, {} + mats = [] + for nm, f in fams.items(): + ks = list(f) + self.keys[nm] = ks + M = np.full((len(idx), len(ks)), np.nan) + for c, k in enumerate(ks): + M[:, c] = f[k].reindex(idx).values + self.cols[nm] = M + mats.append(M) + + def pick(self, sel: dict) -> np.ndarray: + return np.column_stack([self.cols[nm][:, self.keys[nm].index(k)] for nm, k in sel.items()]) + + def combo(self, M: np.ndarray, idx=None) -> pd.Series: + # `idx`: indice delle RIGHE di M. Default self.idx (M sull'intera unione); + # va passato esplicitamente quando M e' un sottoinsieme di righe, altrimenti + # la maschera `keep` e l'indice hanno lunghezze diverse. + ix = self.idx if idx is None else idx + if M.shape[0] != len(ix): + raise ValueError(f"combo: M ha {M.shape[0]} righe, l'indice ne ha {len(ix)}") + mask = ~np.isnan(M) + w = mask.astype(float) + rs = w.sum(axis=1, keepdims=True) + wn = np.divide(w, rs, out=np.zeros_like(w), where=rs > 0) + v = np.nansum(np.nan_to_num(M) * wn, axis=1) + keep = mask.any(axis=1) + return pd.Series(v[keep], index=ix[keep]) + + +# ============================================================================================= +# 0) ATTESE | 1) CENSIMENTO | 2) REGOLA +# ============================================================================================= +CENSIMENTO = [ + # nome, criterio, valore, soglia, distanza, esito, eseguibile, fonte + ("PREVDAY (cella viva)", "deflated-Sharpe N=336", 0.905, 0.95, -0.045, "FAIL", + "SI' — Deribit, netta col libro", "RESULTS §54"), + ("DVOLSPREAD (congelata)", "deflated-Sharpe N=729", 0.953, 0.95, +0.003, "PASS di misura", + "SI' — Deribit, netta col libro", "CLAUDE.md 26/07"), + ("DVOLSPREAD (pubblicata)", "deflated-Sharpe N=729", 0.947, 0.95, -0.003, "FAIL", + "SI' — Deribit", "CLAUDE.md 26/07"), + ("VRP-TENORE 10g -0,28/-0,05", "deflated-Sharpe N=72", 0.948, 0.95, -0.002, "FAIL", + "opzioni ETH, lotto ~$242", "CLAUDE.md 30/07"), + ("STATARB-RESID W45 sgn+1", "deflated-Sharpe same-sign", 0.929, 0.95, -0.021, "FAIL", + "SI' — 2 gambe BTC/ETH", "diario 2026-06-29"), + ("XSR01 (50 alt HL, demean)", "weights_tilt_null delta_IS", -0.004, 0.0, -0.004, "FAIL", + "NO — venue Hyperliquid, STAT-MODE", "CLAUDE.md 25/07"), +] +ESCLUSI = [ + ("BIN-FREQ (SKH01)", "DSR 0,977 PASS ma 3 FAIL su 4 gambe del criterio", + "non e' una gamba propria: e' una MODIFICA di uno sleeve in produzione (R3)"), + ("XS01-OOS", "DSR 0,342", "distanza 0,61 dalla soglia: e' uno scartato netto, non un quasi (R2)"), + ("STA05 (EWMA-cross)", "nessuna soglia numerica pubblicata", + "mori' su hold-out corto + corr 0,53 a TP01, non su un gate a soglia (R2)"), + ("DVOL-DIREZIONALE", "is_hedge = True", "criterio BINARIO: non esiste una distanza (R2)"), + ("EW-STR (tilt di pesi)", "pctl 94-100 fra 500 tilt", "e' un cambio di PESI, non un candidato (R1)"), + ("LOWVOL 19-major", "DSR 0,13", "scartato netto (R2)"), +] + + +def sezione1() -> None: + sez("1) CENSIMENTO DEI QUASI-PASSATI — criterio, valore, distanza dalla soglia") + print(f" {'candidato':<28}{'criterio':<28}{'val':>8}{'soglia':>8}{'dist':>8} {'esito':<16}" + f"{'eseguibile a $635'}") + for nm, cr, v, so, di, es, ex, fo in CENSIMENTO: + print(f" {nm:<28}{cr:<28}{v:>8.3f}{so:>8.2f}{di:>+8.3f} {es:<16}{ex}") + print(f"\n {'':28}{'':28}{'':8}{'':8}{'':8} fonti: " + + ", ".join(sorted({f for *_, f in CENSIMENTO}))) + sub("ESCLUSI dal paniere, e perche' (la regola e' dichiarata PRIMA, non adattata all'esito)") + for nm, dato, perche in ESCLUSI: + print(f" {nm:<24} {dato:<44} -> {perche}") + + +def sezione2() -> None: + sez("2) REGOLA DI AMMISSIONE — dichiarata prima, NON cambiata dopo") + print(" (R1) non e' nel libro live ne' nel book a 5 sleeve") + print(" (R2) |DSR - 0,95| <= 0,05 OPPURE |delta_insample| <= 0,01 su weights_tilt_null") + print(" (R3) serie ricostruibile da codice committato coi parametri congelati/pubblicati") + print(" Rappresentante = la cella CONGELATA NEL MONITOR (o la pubblicata se monitor assente).") + print(" PESI UGUALI. Nessuna selezione successiva. Il leave-one-out e' DIAGNOSTICA.") + print(f"\n -> entrano {len(LEGS)} gambe: " + ", ".join(LEGS)) + print(" ⚠️ DVOLSPREAD: due celle difendibili (congelata 0,953 / pubblicata 0,947). La regola") + print(" dice 'quella del monitor' = la CONGELATA; la pubblicata gira come SENSIBILITA'.") + print(f"\n trial dichiarati per famiglia (al RIALZO, regola 30/07): {N_TRIALS}") + print(f" SOMMA = {sum(N_TRIALS.values())} trial. E' questo il N del deflated-Sharpe del") + print(" paniere, NON K=5: un paniere di K candidati gia' selezionati si porta dietro TUTTI") + print(" i trial delle famiglie da cui vengono.") + + +# ============================================================================================= +# 3) REPLICHE — un numero gia' pubblicato prima di produrne di nuovi +# ============================================================================================= +def sezione3(F: dict) -> dict: + sez("3) REPLICHE — riprodurre i numeri PUBBLICATI prima di produrne di nuovi") + legs = {} + legs["PREVDAY"] = F["PREVDAY"][("1h", 1, 0.30, True, 0)] + kd = list(DVS_GRID) + legs["DVOLSPREAD"] = F["DVOLSPREAD"][tuple(FROZEN["DVOLSPREAD"][k] for k in kd)] + dvs_pub = F["DVOLSPREAD"][tuple(FROZEN["DVS-PUB"][k] for k in kd)] + legs["VRP-TENORE"] = F["VRP-TENORE"][(10, -0.28, -0.05)] + legs["STATARB"] = F["STATARB"][(45, 1)] + legs["XSR01"] = F["XSR01"][(45, 1)] + vrp_canon = F["VRP-TENORE"][(7, -0.28, -0.10)] + pv_blind = F["PREVDAY"][("4h", 1, 0.30, False, 0)] + + print(f" {'oggetto':<34}{'pubblicato':>26}{'ri-misurato oggi':>30} esito") + rows = [ + ("PREVDAY congelata FULL/HOLD (§54)", "1,236 / 1,297", + f"{sh(legs['PREVDAY']):.3f} / {shH(legs['PREVDAY']):.3f}"), + ("PREVDAY al buio 4h FULL/HOLD (§54)", "1,621 / 0,731", + f"{sh(pv_blind):.3f} / {shH(pv_blind):.3f}"), + ("DVOLSPREAD onesta FULL/HOLD (26/07)", "0,68 / 0,69", "vedi vintage sotto"), + ("VRP canonico 7g FULL (30/07)", "1,32", f"{sh(vrp_canon):.3f}"), + ("VRP 10g -0,28/-0,05 FULL (30/07)", "1,55", f"{sh(legs['VRP-TENORE']):.3f}"), + ] + for a, b, c in rows: + print(f" {a:<34}{b:>26}{c:>30} ") + + sub("3b) LE GAMBE AL VINTAGE DELLA LORO PUBBLICAZIONE — la replica vera") + print(" Ogni numero pubblicato ha una DATA. Confrontarlo con la serie di oggi non e' una") + print(" replica: e' una replica PIU' un mese di dati nuovi. Qui si separano le due cose.") + print(f"\n {'gamba':<22}{'vintage':>12}{'FULL pubbl.':>13}{'FULL vintage':>14}" + f"{'FULL oggi':>11}{'HOLD pubbl.':>13}{'HOLD vintage':>14}{'HOLD oggi':>11}") + vint = [("PREVDAY", "2026-08-23", "1,236", "1,297"), + ("DVOLSPREAD", "2026-07-26", "0,68", "0,69"), + ("DVS-PUB", "2026-07-26", "—", "0,93"), + ("VRP-TENORE", "2026-07-30", "1,55", "1,01"), + ("STATARB", "2026-06-29", "0,84", "—"), + ("XSR01", "2026-07-25", "1,82", "—")] + rep = {} + for nm, cut, pf, ph in vint: + s = dvs_pub if nm == "DVS-PUB" else legs[nm] + sv = s[s.index < cut] + rep[nm] = (sh(sv), shH(sv), sh(s), shH(s)) + print(f" {nm:<22}{cut:>12}{pf:>13}{sh(sv):>14.3f}{sh(s):>11.3f}" + f"{ph:>13}{shH(sv):>14.3f}{shH(s):>11.3f}") + print("\n ✅ ogni cifra 'vintage' e' la replica del 'pubblicato' accanto (la colonna 'oggi'") + print(" e' un numero NUOVO). Le due colonne insieme misurano il DECADIMENTO da allora.") + dec = [(nm, rep[nm][0] - float(pf.replace(",", "."))) + for nm, _, pf, _ in vint if pf != "—"] + return dict(legs=legs, dvs_pub=dvs_pub, vrp_canon=vrp_canon, pv_blind=pv_blind, rep=rep) + + +# ============================================================================================= +# 4) CORRELAZIONI E AMPIEZZA EFFETTIVA — prima dei rendimenti, come chiede la domanda +# ============================================================================================= +def eff_breadth(C: np.ndarray) -> float: + n = len(C) + off = C[~np.eye(n, dtype=bool)] + rbar = float(np.nanmean(off)) + return n / (1.0 + (n - 1) * rbar) if rbar > -1.0 / (n - 1) else float(n) + + +def sezione4(legs: dict) -> dict: + sez("4) CORRELAZIONI E AMPIEZZA EFFETTIVA — misurate PRIMA dei rendimenti del paniere") + print(" Se le gambe sono scorrelate, un paniere alza lo Sharpe PER COSTRUZIONE senza") + print(" contenere un solo edge nuovo. Questa tabella e' il contesto di ogni numero dopo.") + M = pd.concat(legs, axis=1, join="outer").sort_index() + Ci = M.corr() + print(f"\n matrice di correlazione (giorni in comune a coppie, no riempimento):\n") + print(" " + " " * 13 + "".join(f"{c[:11]:>13}" for c in Ci.columns)) + for r in Ci.index: + print(f" {r[:12]:<13}" + "".join(f"{Ci.loc[r, c]:>13.3f}" for c in Ci.columns)) + C = Ci.values + off = C[~np.eye(len(C), dtype=bool)] + nb = eff_breadth(C) + print(f"\n corr media fuori diagonale = {np.nanmean(off):+.4f} " + f"[min {np.nanmin(off):+.3f}, max {np.nanmax(off):+.3f}]") + print(f" AMPIEZZA EFFETTIVA (N / (1+(N-1)·rbar), la formula gia' usata dal progetto in") + print(f" r0725_statarb_basket_gate.eff_breadth) = {nb:.2f} su K = {len(C)} gambe.") + print(f" -> il paniere vale, in termini di scommesse indipendenti, {nb:.2f} gambe.") + tp = al.tp01_baseline_daily() + print(f"\n correlazione di ogni gamba al baseline TP01 (il libro e' 75% TP01):") + for k, s in legs.items(): + a, b = s.align(tp, join="inner") + print(f" {k:<14}{a.corr(b):+.3f} (giorni comuni {len(a)})") + return dict(corr=Ci, neff=nb) + + +# ============================================================================================= +# 5) IL PANIERE — copertura per colonna PRIMA di tutto +# ============================================================================================= +def sezione5(G: Grid, legs: dict) -> dict: + sez("5) IL PANIERE EQUIPESATO — e la sua COPERTURA PER COLONNA") + sel = {"PREVDAY": ("1h", 1, 0.30, True, 0), + "DVOLSPREAD": tuple(FROZEN["DVOLSPREAD"][k] for k in DVS_GRID), + "VRP-TENORE": (10, -0.28, -0.05), "STATARB": (45, 1), "XSR01": (45, 1)} + M = G.pick(sel) + B = G.combo(M) + + # --- bit-exact contro la produzione ------------------------------------------------------ + Bp = combine_outer({k: legs[k] for k in LEGS}, {k: 1.0 for k in LEGS}, lo=G.idx[0]) + a, b = B.align(Bp, join="outer") + mx = float(np.nanmax(np.abs(a.fillna(0) - b.fillna(0)))) + print(f" ✅ combinatore numpy == `combine_outer` di produzione: max|diff| = {mx:.3e} " + f"(n {len(B)} vs {len(Bp)})") + + sub("5a) COPERTURA PER COLONNA — la prima diagnostica di un outer-join") + print(" Il progetto ha gia' pubblicato un muro sbagliato di 3x per aver saltato questa") + print(" tabella (r0726_wall_fixedpoint: una colonna al 100% accanto a colonne basse).") + print(f"\n {'gamba':<14}{'primo giorno':>14}{'giorni':>9}{'copertura':>11}" + f"{'barre ATTIVE':>14}{'attive/cal.':>12}") + cov = {} + for i, nm in enumerate(LEGS): + col = M[:, i] + n_ok = int(np.sum(~np.isnan(col))) + act = int(np.sum(np.nan_to_num(col) != 0.0)) + first = G.idx[np.argmax(~np.isnan(col))] + cov[nm] = n_ok / len(G.idx) + print(f" {nm:<14}{str(first.date()):>14}{n_ok:>9}{n_ok/len(G.idx):>10.1%}" + f"{act:>14}{act/max(1,n_ok):>12.1%}") + print(f" {'UNIONE':<14}{str(G.idx[0].date()):>14}{len(G.idx):>9}{1.0:>10.1%}") + n_legs = (~np.isnan(M)).sum(axis=1) + print(f"\n gambe VIVE per giorno: " + + " ".join(f"{k}: {100*np.mean(n_legs==k):.1f}%" for k in range(1, 6))) + print(" 🚨 il paniere e' a 5 gambe solo nel " f"{100*np.mean(n_legs==5):.1f}% dei giorni: " + "prima del 2024 XSR01 non esiste, prima del 2021 nemmeno VRP.") + print(" ⚠️ e la colonna 'attive/cal.' dice un'altra cosa ancora: DVOLSPREAD e VRP hanno") + print(" zeri STRUTTURALI (flat senza DVOL / settimanale su griglia giornaliera) -> un") + print(" giorno presente non e' un giorno di rischio (lezione 'barre attive', 26/07).") + + common = G.idx[n_legs == 5] + Bc = G.combo(M[(n_legs == 5), :], common) if len(common) > 60 else None + + sub("5b) METRICHE DEL PANIERE — due lenti dichiarate") + print(f" {'lente':<28}{'n':>7}{'ShFULL':>9}{'ShIS':>9}{'ShHOLD':>9}{'CAGR':>9}{'maxDD':>9}") + for nm, s in (("UNIONE (rinormalizzata)", B), ("FINESTRA COMUNE (5/5 gambe)", Bc)): + if s is None: + continue + m = metrics(s) + print(f" {nm:<28}{len(s):>7}{sh(s):>9.3f}{shI(s):>9.3f}{shH(s):>9.3f}" + f"{100*m['cagr']:>8.2f}%{100*m['maxdd']:>8.2f}%") + print(f"\n {'gamba sola':<28}{'n':>7}{'ShFULL':>9}{'ShIS':>9}{'ShHOLD':>9}{'CAGR':>9}{'maxDD':>9}") + for nm in LEGS: + s = legs[nm] + m = metrics(s) + print(f" {nm:<28}{len(s):>7}{sh(s):>9.3f}{shI(s):>9.3f}{shH(s):>9.3f}" + f"{100*m['cagr']:>8.2f}%{100*m['maxdd']:>8.2f}%") + print(f"\n A1: il paniere batte OGNI gamba sul FULL? " + f"{'SI' if sh(B) > max(sh(legs[n]) for n in LEGS) else 'NO'} " + f"(paniere {sh(B):.3f} vs miglior gamba {max(sh(legs[n]) for n in LEGS):.3f})") + + sub("5c) PER ANNO — dove sta il rendimento del paniere") + yr = {} + for y, g in B.groupby(B.index.year): + yr[y] = (sh(g), float(np.prod(1 + g.values) - 1), len(g)) + print(f" {'anno':>6}{'Sharpe':>10}{'ritorno':>11}{'giorni':>9} gambe vive (media)") + for y in sorted(yr): + mm = (G.idx.year == y) + print(f" {y:>6}{yr[y][0]:>10.2f}{100*yr[y][1]:>10.1f}%{yr[y][2]:>9}" + f" {n_legs[mm].mean():.2f}") + + sub("5d) MARGINALE vs TP01 (lo scorer indurito del progetto, non riscritto)") + mg = al.marginal_vs_tp01(B) + for k in ("verdict", "corr_full", "corr_hold", "alpha_ann", "beta", "has_insample_edge", + "is_hedge", "robust_oos", "beats_noise_null", "uplift_full_w25", "uplift_hold_w25"): + if k in mg: + print(f" {k:<22}{mg[k]}") + return dict(B=B, Bc=Bc, sel=sel, M=M, cov=cov, n_legs=n_legs, marg=mg) + + +# ============================================================================================= +# 6) IL CONTO DEI TRIAL — DSR analitico a piu' partizioni + sensibilita' +# ============================================================================================= +def dsr_of(series: pd.Series, trials: list[float]) -> tuple[float, float, float]: + d, sr0 = al.deflated_sharpe(sh(series), trials, series) + return d, sr0, float(np.std(np.asarray(trials, float), ddof=1)) / math.sqrt(365.25) + + +def sezione6(F: dict, R5: dict, legs: dict) -> dict: + sez("6) IL CONTO DEI TRIAL — deflated-Sharpe del PANIERE, e la sua sensibilita'") + B = R5["B"] + fam_full = {nm: [sh(s) for s in F[nm].values()] for nm in LEGS} + pooled = [x for nm in LEGS for x in fam_full[nm]] + print(f" Sharpe FULL del paniere = {sh(B):.4f}") + print(f"\n {'partizione':<52}{'N':>7}{'sd(trial)/g':>13}{'null max Sh':>13}{'DSR':>8} esito") + parts = [ + ("P1 N = K (le 5 gambe) — SBAGLIATA, la riporto per taratura", + [sh(legs[n]) for n in LEGS]), + ("P2 N = somma dei trial delle famiglie (la regola del brief)", pooled), + ("P3 N = famiglia PIU' LARGA sola (DVOLSPREAD 729)", fam_full["DVOLSPREAD"]), + ("P4 N = pooled + screen d'ondata (+168 ORTHO-SCREEN)", + pooled + [sh(s) for f in F["_ORTHO"].values() for s in f.values()]), + ("P5 N = solo le 5 celle scelte, varianza di famiglia pooled", + list(np.random.default_rng(1).choice(pooled, size=5, replace=False))), + ] + out = {} + for nm, tr in parts: + d, sr0, sdt = dsr_of(B, tr) + out[nm] = d + print(f" {nm:<52}{len(tr):>7}{sdt:>13.5f}{sr0:>13.3f}{d:>8.3f} " + f"{'PASS' if d >= 0.95 else 'FAIL'}") + print("\n 🚨 Il verdetto NON e' stabile: cambia con la partizione, come il progetto ha gia'") + print(" misurato il 30/07 (VRP: 0,983 PASS a N=8 / 0,948 FAIL a N=72 / 0,909 a N=360) e") + print(" il 22/08 (§24: la stessa griglia da' verdetti opposti a seconda di come si parte).") + print(" 📌 E c'e' un difetto STRUTTURALE piu' grave, che vale la pena dire in chiaro:") + print(" `deflated_sharpe` stima il massimo-dal-rumore dalla varianza degli Sharpe dei") + print(" TRIAL SINGOLI. Un PANIERE non e' un trial singolo: ha Sharpe piu' alto per pura") + print(" diversificazione. Confrontarlo con una sr0 tarata su celle singole lo FAVORISCE") + print(" sistematicamente -> il DSR analitico di un paniere e' OTTIMISTA a ogni N.") + print(" Il test giusto e' EMPIRICO: la distribuzione dei PANIERI (sezione 7).") + # sensibilita': quale sd(trial) ribalta il verdetto + lo, hi = 1e-4, 1.0 + for _ in range(60): + mid = 0.5 * (lo + hi) + scaled = list(np.asarray(pooled) * (mid / (np.std(pooled, ddof=1) / math.sqrt(365.25))) + ) if np.std(pooled, ddof=1) > 0 else pooled + d, _, _ = dsr_of(B, scaled) + if d >= 0.95: + lo = mid + else: + hi = mid + print(f"\n sensibilita': con N={len(pooled)} il verdetto si ribalta a " + f"sd(trial giornaliera) = {0.5*(lo+hi):.5f}; misurata = " + f"{np.std(pooled, ddof=1)/math.sqrt(365.25):.5f}.") + return out + + +# ============================================================================================= +# 7) IL CONTROLLO — CIO' CHE DECIDE +# ============================================================================================= +def sezione7(F: dict, G: Grid, R5: dict, draws: int) -> dict: + sez("7) IL CONTROLLO — panieri di SCARTATI NETTI. E' QUESTO IL TEST, non lo Sharpe del paniere") + B = R5["B"] + rng = np.random.default_rng(SEED) + real = (sh(B), shI(B), shH(B), dd(B)) + + # in-sample ranking per famiglia (mai l'hold-out: sarebbe selection-on-holdout) + rank = {} + for nm in LEGS: + ks = G.keys[nm] + vals = np.array([shI(F[nm][k]) for k in ks]) + rank[nm] = dict(keys=ks, ins=vals, order=np.argsort(np.nan_to_num(vals, nan=-9))) + frozen_idx = {nm: G.keys[nm].index(R5["sel"][nm]) for nm in LEGS} + print(" rango IN-SAMPLE della cella congelata dentro la propria famiglia " + "(mai l'hold-out: sarebbe selection-on-holdout)") + for nm in LEGS: + o = list(rank[nm]["order"][::-1]) + r = o.index(frozen_idx[nm]) + 1 + print(f" {nm:<14}{r:>4} / {len(o):<5} (pctl in-sample " + f"{100*(1-(r-1)/len(o)):.0f}°, ShIS {rank[nm]['ins'][frozen_idx[nm]]:+.3f})") + + def draw_basket(picker) -> tuple: + cols = [] + for nm in LEGS: + cols.append(G.cols[nm][:, picker(nm)]) + M = np.column_stack(cols) + s = G.combo(M) + return sh(s), shI(s), shH(s), dd(s) + + def bottom(nm): + o = rank[nm]["order"] # crescente: i primi sono i peggiori in-sample + return int(rng.choice(o[:max(1, len(o) // 2)])) + + def matched(nm): + o = list(rank[nm]["order"][::-1]) + r = o.index(frozen_idx[nm]) / max(1, len(o) - 1) + cand = [i for i in range(len(o)) if abs(i / max(1, len(o) - 1) - r) <= 0.10] + return int(o[int(rng.choice(cand))]) + + def anyc(nm): + return int(rng.integers(len(G.keys[nm]))) + + res = {} + for tag, pick in (("B1 SCARTATI NETTI (meta' peggiore in-sample, stesse famiglie)", bottom), + ("B2 RANGO APPAIATO (stesso pctl in-sample +-10pp)", matched), + ("B0 CELLA A CASO (qualunque cella della famiglia)", anyc)): + t = time.time() + arr = np.array([draw_basket(pick) for _ in range(draws)]) + res[tag] = arr + print(f"\n {tag} ({draws} panieri, {time.time()-t:.1f}s)") + print(f" {'metrica':<10}{'reale':>9}{'p10':>9}{'mediana':>9}{'p90':>9}{'max':>9}" + f"{'pctl del reale':>17}") + for i, mn in enumerate(("ShFULL", "ShIS", "ShHOLD", "maxDD")): + c = arr[:, i] + pc = 100.0 * float((c < real[i]).mean()) + print(f" {mn:<10}{real[i]:>9.3f}{np.percentile(c,10):>9.3f}" + f"{np.median(c):>9.3f}{np.percentile(c,90):>9.3f}{np.max(c):>9.3f}" + f"{pc:>16.1f}°") + + # --- B3: null di diversificazione (marginali CONSERVATE, correlazione distrutta) ---------- + sub("B3 NULL DI DIVERSIFICAZIONE — stesse gambe, correlazione incrociata DISTRUTTA") + print(" Ogni gamba viene ruotata circolarmente di un lag casuale >= 60 giorni: lo Sharpe") + print(" MARGINALE di ciascuna resta IDENTICO, sparisce solo la relazione fra gambe (e con") + print(" TP01). Se il paniere reale non batte questa distribuzione, non contiene nulla oltre") + print(" a 'K serie con quei marginali, quasi scorrelate'. Turnover invariato -> fee-neutrale.") + M0 = R5["M"] + arr = [] + for _ in range(draws): + cols = [] + for i in range(M0.shape[1]): + col = M0[:, i] + ok = ~np.isnan(col) + v = col[ok].copy() + v = np.roll(v, int(rng.integers(60, max(61, len(v) - 60)))) + nc = np.full_like(col, np.nan) + nc[ok] = v + cols.append(nc) + s = G.combo(np.column_stack(cols)) + arr.append((sh(s), shI(s), shH(s), dd(s))) + arr = np.array(arr) + res["B3"] = arr + print(f"\n {'metrica':<10}{'reale':>9}{'p10':>9}{'mediana':>9}{'p90':>9}{'max':>9}" + f"{'pctl del reale':>17}") + for i, mn in enumerate(("ShFULL", "ShIS", "ShHOLD", "maxDD")): + c = arr[:, i] + print(f" {mn:<10}{real[i]:>9.3f}{np.percentile(c,10):>9.3f}{np.median(c):>9.3f}" + f"{np.percentile(c,90):>9.3f}{np.max(c):>9.3f}" + f"{100.0*float((c16.1f}°") + + # --- B4: cross-meccanismo, scartati netti veri -------------------------------------------- + sub("B4 SCARTATI NETTI CROSS-MECCANISMO — 4 famiglie relative-value refutate dall'onda ortho") + print(" RATIO-MOM / RATIO-REV / RATIO-ACCEL / VOLSPREAD: meccanismi DIVERSI fra loro e dai") + print(" quasi-passati, tutti collassati dallo scorer indurito (17/18 -> 1, 21/06). Stessa") + print(" selezione dei quasi-passati (miglior cella IN-SAMPLE di ogni famiglia) + estrazioni.") + OF = F["_ORTHO"] + Go = Grid(OF) + best = {} + for nm, f in OF.items(): + ks = list(f) + best[nm] = ks[int(np.argmax([np.nan_to_num(shI(f[k]), nan=-9) for k in ks]))] + Mb = Go.pick(best) + Bo = Go.combo(Mb) + print(f"\n paniere dei 4 SCARTATI NETTI, celle scelte in-sample: ShFULL {sh(Bo):+.3f} " + f"ShIS {shI(Bo):+.3f} ShHOLD {shH(Bo):+.3f} maxDD {100*dd(Bo):.1f}% n {len(Bo)}") + Cb = pd.concat({k: OF[k][v] for k, v in best.items()}, axis=1).corr().values + print(f" corr media fuori diagonale {np.nanmean(Cb[~np.eye(len(Cb),dtype=bool)]):+.3f} -> " + f"ampiezza effettiva {eff_breadth(Cb):.2f} su 4") + arr = [] + for _ in range(draws): + s = Go.combo(np.column_stack([Go.cols[nm][:, int(rng.integers(len(Go.keys[nm])))] + for nm in OF])) + arr.append((sh(s), shI(s), shH(s), dd(s))) + arr = np.array(arr) + res["B4"] = arr + print(f"\n {draws} panieri a cella casuale dalle stesse 4 famiglie refutate:") + print(f" {'metrica':<10}{'quasi-pass.':>13}{'p10':>9}{'mediana':>9}{'p90':>9}{'max':>9}" + f"{'pctl dei quasi':>17}") + for i, mn in enumerate(("ShFULL", "ShIS", "ShHOLD", "maxDD")): + c = arr[:, i] + print(f" {mn:<10}{real[i]:>13.3f}{np.percentile(c,10):>9.3f}{np.median(c):>9.3f}" + f"{np.percentile(c,90):>9.3f}{np.max(c):>9.3f}" + f"{100.0*float((c16.1f}°") + + # --- CONTROLLO POSITIVO ------------------------------------------------------------------- + sub("CONTROLLO POSITIVO — un rilevatore che non segnala e' indistinguibile da uno rotto") + print(" Se il confronto 'reale vs panieri casuali' avesse potenza ZERO, tutto quanto sopra") + print(" sarebbe privo di significato. Lo si prova costruendo un paniere che DEVE stare in") + print(" cima: la MIGLIOR cella in-sample di ognuna delle 5 famiglie.") + bestQ = {} + for nm in LEGS: + o = rank[nm]["order"] + bestQ[nm] = G.keys[nm][int(o[-1])] + Bb = G.combo(G.pick(bestQ)) + B0 = res["B0 CELLA A CASO (qualunque cella della famiglia)"] + pIS = 100.0 * float((B0[:, 1] < shI(Bb)).mean()) + print(f" paniere 'migliori in-sample': ShIS {shI(Bb):+.3f} -> pctl {pIS:.1f}deg della") + print(f" distribuzione B0 (atteso ~100deg se il test ha potenza: e' il massimo per") + print(f" costruzione, quindi un pctl basso significherebbe rilevatore ROTTO).") + print(f" Il suo ShHOLD e' {shH(Bb):+.3f} contro mediana B0 {np.median(B0[:,2]):+.3f}:") + print(f" e' QUESTO il pezzo di scelta in-sample che sopravvive fuori campione.") + if pIS < 90.0: + print(" RILEVATORE SOSPETTO: il paniere costruito per stare in cima NON ci sta.") + return dict(res=res, real=real, best_is=Bb, ortho=Bo, rank=rank, frozen_idx=frozen_idx) + + +# ============================================================================================= +# 8) LEAVE-ONE-OUT +# ============================================================================================= +def sezione8(G: Grid, R5: dict) -> dict: + sez("8) LEAVE-ONE-OUT — se il paniere 'funziona' grazie a UNA gamba, non e' un paniere") + B = R5["B"] + base = (sh(B), shI(B), shH(B), dd(B)) + print(f" paniere completo: ShFULL {base[0]:.3f} ShIS {base[1]:.3f} ShHOLD {base[2]:.3f} " + f"maxDD {100*base[3]:.2f}%") + print(f"\n {'tolta':<14}{'ShFULL':>9}{'dShFULL':>10}{'ShHOLD':>9}{'dShHOLD':>10}" + f"{'maxDD':>9}{'d maxDD pp':>12}") + out = {} + for nm in LEGS: + keep = [k for k in LEGS if k != nm] + s = G.combo(G.pick({k: R5["sel"][k] for k in keep})) + out[nm] = (sh(s), shH(s), dd(s)) + print(f" {nm:<14}{sh(s):>9.3f}{sh(s)-base[0]:>+10.3f}{shH(s):>9.3f}" + f"{shH(s)-base[2]:>+10.3f}{100*dd(s):>8.2f}%{100*(dd(s)-base[3]):>+12.2f}") + worst = min(out, key=lambda k: out[k][0]) + print(f"\n la gamba il cui ritiro costa di piu' sul FULL: {worst} " + f"({out[worst][0]-base[0]:+.3f})") + return out + + +# ============================================================================================= +# 9) IL PANIERE COME TERZO SLEEVE DEL LIBRO LIVE — banda d'ancora, funding dentro +# ============================================================================================= +def sezione9(R5: dict, legs: dict, draws: int) -> dict: + sez("9) IL PANIERE COME TERZO SLEEVE DEL LIBRO LIVE — e batte la sua MIGLIOR gamba?") + if not B3RD_CACHE.exists(): + print(f" ⚠️ cache §50 assente ({B3RD_CACHE}) -> sezione SALTATA. Non e' un risultato.") + return {} + S = pickle.loads(B3RD_CACHE.read_bytes()) + print(f" cache §50 riusata (serie ancorate, FUNDING DENTRO): {list(S)[:8]}...") + + # replica del baseline §50 prima di ogni delta + rng = np.random.default_rng(SEED) + hs = rng.choice(TP_HOURS, size=draws) + os_ = rng.choice(SKH_OFFSETS, size=draws) + hs_sa = rng.choice(TP_HOURS, size=draws) + base, dPV, dBK, dBK4 = [], [], [], [] + # gambe funding-consistenti: PREVDAY/STATARB/DVOLSPREAD dalla cache §50 (funding dentro), + # VRP (opzioni: nessun funding di perp) e XSR01 con il drag dichiarato di §37. + xsr = legs["XSR01"] - XS_FUNDING_DRAG_YR / 365.25 + vrp = legs["VRP-TENORE"] + W_CAND = 0.15 + for i in range(draws): + c2 = {"TP01": S["TP01"][hs[i]], "SKH01": S["SKH01"][os_[i]]} + b = combine_outer(c2, {"TP01": W_TP, "SKH01": W_SKH}, lo=BOOK_START) + mb, mbh = metrics(b), metrics(b[b.index >= HOLDOUT]) + base.append((mb["sharpe"], mbh["sharpe"], mb["maxdd"])) + pan = combine_outer({"PREVDAY": S["PREVDAY"][0], "DVOLSPREAD": S["DVOLSPREAD"][0], + "VRP-TENORE": vrp, "STATARB": S["STATARB"][hs_sa[i]], "XSR01": xsr}, + {k: 1.0 for k in LEGS}, lo=BOOK_START) + pan4 = combine_outer({"PREVDAY": S["PREVDAY"][0], "DVOLSPREAD": S["DVOLSPREAD"][0], + "VRP-TENORE": vrp, "STATARB": S["STATARB"][hs_sa[i]]}, + {k: 1.0 for k in LEGS if k != "XSR01"}, lo=BOOK_START) + for tgt, lst in ((S["PREVDAY"][0], dPV), (pan, dBK), (pan4, dBK4)): + c3 = dict(c2); c3["CAND"] = tgt + a = combine_outer(c3, {"TP01": W_TP * (1 - W_CAND), "SKH01": W_SKH * (1 - W_CAND), + "CAND": W_CAND}, lo=BOOK_START) + ma, mah = metrics(a), metrics(a[a.index >= HOLDOUT]) + lst.append((ma["sharpe"] - mb["sharpe"], mah["sharpe"] - mbh["sharpe"], + ma["maxdd"] - mb["maxdd"])) + Bs = np.array(base) + print(f"\n LIBRO LIVE de-luckato (2 sleeve, funding dentro), {draws} estrazioni:") + print(f" ShFULL {np.median(Bs[:,0]):.3f} ShHOLD {np.median(Bs[:,1]):.3f} " + f"maxDD {100*np.median(Bs[:,2]):.2f}%") + print(" §50 pubblicava 1,813 / 1,437 / 9,42% -> ✅ replica (estrazioni diverse, " + "stessa cache)") + print(f"\n peso del candidato = {W_CAND:.0%}. Statistica = MEDIANA DELLE DIFFERENZE APPAIATE.") + print(f"\n {'candidato':<34}{'dShFULL':>11}{'>0':>7}{'dShHOLD':>11}{'>0':>7}" + f"{'d maxDD pp':>13}") + rows = [("PREVDAY da sola (§50: +0,192/+0,363)", np.array(dPV)), + ("PANIERE 5 gambe (quasi-passati)", np.array(dBK)), + ("PANIERE 4 gambe (senza XSR01, eseguibile)", np.array(dBK4))] + for nm, a in rows: + print(f" {nm:<34}{np.median(a[:,0]):>+11.4f}{100*(a[:,0]>0).mean():>6.0f}%" + f"{np.median(a[:,1]):>+11.4f}{100*(a[:,1]>0).mean():>6.0f}%" + f"{100*np.median(a[:,2]):>+13.2f}") + d5 = np.array(dBK)[:, 0] - np.array(dPV)[:, 0] + d5h = np.array(dBK)[:, 1] - np.array(dPV)[:, 1] + print(f"\n 🚨 PANIERE meno PREVDAY-DA-SOLA (differenza APPAIATA sulla stessa estrazione):") + print(f" dShFULL {np.median(d5):+.4f} (>0 nel {100*(d5>0).mean():.0f}%) " + f"dShHOLD {np.median(d5h):+.4f} (>0 nel {100*(d5h>0).mean():.0f}%)") + return dict(base=Bs, dPV=np.array(dPV), dBK=np.array(dBK), dBK4=np.array(dBK4)) + + +# ============================================================================================= +# 10) ESEGUIBILITA' + EUR/GIORNO +# ============================================================================================= +def sezione10(R5: dict, R9: dict) -> None: + sez("10) ESEGUIBILITA' RIGA PER RIGA — un paniere con dentro uno STAT-MODE non e' un candidato") + print(f" {'gamba':<14}{'venue':<14}{'su Deribit?':<14}{'ticket a $635 (peso 15%/5)':<30}" + f"{'stato'}") + tick = CAPITAL_TODAY * 0.15 / len(LEGS) + righe = [ + ("PREVDAY", "Deribit", "SI'", f"${tick:.2f} netta col libro", "eseguibile"), + ("DVOLSPREAD", "Deribit", "SI'", f"${tick:.2f} netta col libro", "eseguibile"), + ("STATARB", "Deribit", "SI'", f"${tick:.2f} netta col libro", "eseguibile"), + ("VRP-TENORE", "Deribit opz.", "SI' (ETH)", "1 lotto ETH = $242 (>> $19)", + "🚨 NON eseguibile al peso"), + ("XSR01", "Hyperliquid", "NO", "50 gambe, min $10/gamba", + "🚨 STAT-MODE, venue diverso"), + ] + for r in righe: + print(f" {r[0]:<14}{r[1]:<14}{r[2]:<14}{r[3]:<30}{r[4]}") + print(f"\n 🚨 2 gambe su 5 NON sono eseguibili a $635 -> il paniere COSI' COM'E' non e' un") + print(" candidato al libro, a prescindere da qualunque numero delle sezioni precedenti.") + print(" VRP-TENORE cade sul LOTTO (regola gia' scritta: 'niente short-vol da modello in") + print(" deploy' — il lotto non e' nemmeno il vincolo principale). XSR01 cade sul VENUE,") + print(" e la decisione d'operatore tiene tutto su Deribit fino a $20k.") + if R9: + d = R9["dBK4"][:, 0] + print(f"\n Il paniere ESEGUIBILE (4 gambe, senza XSR01) vale dShFULL " + f"{np.median(d):+.4f} di libro. Per convertirlo in reddito serve la LEVA, che " + f"a $635 non c'e'.") + + +def eur_al_giorno(R9: dict) -> None: + sez("11) IN EURO AL GIORNO — l'unita' che il progetto usa per non mentirsi") + cagr_base = 0.1519 # L3 congiunta (fisco + funding), CLAUDE.md §36 + perp = 0.0635 # rendita perpetua L3 + print(f" Conto reale $635. Rendita perpetua della lente congiunta L3 = {100*perp:.2f}%/anno") + print(f" -> oggi il libro vale ${635*perp/365.25:.3f}/giorno = " + f"€{635*perp/365.25*0.92:.3f}/giorno.") + if not R9: + return + d = np.median(R9["dBK4"][:, 0]) + dPV = np.median(R9["dPV"][:, 0]) + print(f"\n Il paniere eseguibile aggiunge dShFULL {d:+.4f} al libro; PREVDAY DA SOLA " + f"{dPV:+.4f}.") + print(" Uno dSharpe NON e' reddito a nozionale invariato (regola 25/07: un diversificatore") + print(" a basso CAGR si giudica a ISO-RISCHIO). Convertito a iso-rischio con la leva che a") + print(" $635 NON esiste, e prendendo per buono il d drift misurato in sezione 9:") + dd_ = np.median(R9["dBK4"][:, 2]) + print(f" d maxDD {100*dd_:+.2f}pp -> a iso-DD si potrebbe alzare la scala di un fattore") + print(f" ~{(np.median(R9['base'][:,2])/max(1e-9, np.median(R9['base'][:,2])+dd_)):.3f}, " + f"cioe' ~{100*((np.median(R9['base'][:,2])/max(1e-9,np.median(R9['base'][:,2])+dd_))-1):.1f}% " + f"di reddito in piu'.") + v = 635 * perp / 365.25 * 0.92 + k = np.median(R9['base'][:, 2]) / max(1e-9, np.median(R9['base'][:, 2]) + dd_) + print(f"\n 🚨 IN EURO: €{v:.3f}/g -> €{v*k:.3f}/g. Guadagno = " + f"€{v*(k-1):.4f}/GIORNO, cioe' €{v*(k-1)*365:.2f}/anno su un conto da $635.") + print(f" Versare €500/mese vale €6.000/anno. Il rapporto e' " + f"~{6000/max(1e-9, abs(v*(k-1)*365)):.0f}x.") + print(" E il paniere richiede QUATTRO monitor invece di uno, su una macchina a 2 CPU.") + + +# ============================================================================================= +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--draws", type=int, default=1000) + ap.add_argument("--bookdraws", type=int, default=200) + ap.add_argument("--force", action="store_true") + args = ap.parse_args() + + sez("r0823b_quasi_passati.py — §68 | i candidati morti PER POCO, presi insieme, passano?") + print(f" avvio {dt.datetime.now(dt.timezone.utc):%Y-%m-%d %H:%M UTC} · " + f"estrazioni {args.draws} · seed {SEED} · SOLA LETTURA, 0 ordini, 0 chiamate di rete") + print(__doc__.split("DOMANDA")[1].split("REGOLA DI AMMISSIONE")[0].rstrip()) + + sez("0) ATTESE A PRIORI — dichiarate nel docstring PRIMA di misurare") + print(__doc__.split("=====\n")[2].split("METODO")[0].rstrip()) + + sezione1() + sezione2() + + sez("COSTRUZIONE DELLE FAMIGLIE (ogni cella = una serie di rendimenti netti giornalieri)") + F = build_families(args.force) + R3 = sezione3(F) + legs = R3["legs"] + G = Grid({k: F[k] for k in LEGS}) + R4 = sezione4(legs) + R5 = sezione5(G, legs) + R6 = sezione6(F, R5, legs) + R7 = sezione7(F, G, R5, args.draws) + R8 = sezione8(G, R5) + R9 = sezione9(R5, legs, args.bookdraws) + sezione10(R5, R9) + eur_al_giorno(R9) + + # --------------------------------------------------------------------------------------- + sez("12) ATTESE: CONFERMATE O REFUTATE") + B = R5["B"] + best_leg = max(sh(legs[n]) for n in LEGS) + b0 = R7["res"]["B0 CELLA A CASO (qualunque cella della famiglia)"] + b1 = R7["res"]["B1 SCARTATI NETTI (meta' peggiore in-sample, stesse famiglie)"] + pct_full_b1 = 100.0 * float((b1[:, 0] < sh(B)).mean()) + pct_hold_b1 = 100.0 * float((b1[:, 2] < shH(B)).mean()) + a1 = sh(B) > best_leg + a2 = (max(R6.values()) >= 0.95) and (min(R6.values()) < 0.95) + a3 = (pct_full_b1 > 50) and (pct_hold_b1 < pct_full_b1) + a4 = R4["neff"] > 0.8 * len(LEGS) + worst = min(R8, key=lambda k: R8[k][0]) + a5 = worst == "PREVDAY" + print(f" A1 paniere > ogni gamba sul FULL ................. " + f"{'CONFERMATA' if a1 else 'REFUTATA'} ({sh(B):.3f} vs {best_leg:.3f})") + print(f" A2 il DSR si ribalta con la partizione ........... " + f"{'CONFERMATA' if a2 else 'REFUTATA'} (range {min(R6.values()):.3f}-{max(R6.values()):.3f})") + print(f" A3 vantaggio in-sample che si riduce sull'hold-out " + f"{'CONFERMATA' if a3 else 'REFUTATA'} (pctl vs B1: FULL {pct_full_b1:.0f}° / " + f"HOLD {pct_hold_b1:.0f}°)") + print(f" A4 ampiezza effettiva ~ K ....................... " + f"{'CONFERMATA' if a4 else 'REFUTATA'} ({R4['neff']:.2f} su {len(LEGS)})") + print(f" A5 dipende da UNA gamba (PREVDAY) ............... " + f"{'CONFERMATA' if a5 else 'REFUTATA'} (la piu' costosa da togliere: {worst})") + print(f" A6 almeno una gamba STAT-MODE ................... CONFERMATA (XSR01 + VRP-TENORE)") + print(f" A7 le gambe sono DECADUTE dalla pubblicazione ... vedi sezione 3b") + + sez(f"VERDETTO {clock()}") + print(f" Sharpe del paniere: FULL {sh(B):.3f} / IS {shI(B):.3f} / HOLD {shH(B):.3f}, " + f"maxDD {100*dd(B):.2f}%, ampiezza effettiva {R4['neff']:.2f}/5.") + print(f" Percentile contro i panieri di SCARTATI NETTI (B1): FULL {pct_full_b1:.1f}° / " + f"HOLD {pct_hold_b1:.1f}°.") + print(f" Percentile contro i panieri a CELLA CASUALE (B0): FULL " + f"{100.0*float((b0[:,0] < sh(B)).mean()):.1f}° / HOLD " + f"{100.0*float((b0[:,2] < shH(B)).mean()):.1f}°.") + print(f" DSR del paniere: {min(R6.values()):.3f} - {max(R6.values()):.3f} secondo la " + f"partizione dei trial.") + print("\n La riga per il registro e' nel messaggio finale.") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823b_regime_capitale.py b/scripts/research/r0823b_regime_capitale.py new file mode 100644 index 0000000..d1c954a --- /dev/null +++ b/scripts/research/r0823b_regime_capitale.py @@ -0,0 +1,785 @@ +"""§67 REGIME-CAPITALE — cosa e' stato archiviato PER TAGLIA, e cosa sarebbe vivo a $100.000. + +LA DOMANDA +---------- +Ogni screening di questo progetto e' stato fatto a **$600 di capitale**. E' la scelta giusta per il +conto che esiste, ma ha un effetto collaterale mai esaminato: le strategie messe in STAT-MODE o +rifiutate citando min-order / lotto minimo / numero di gambe / haircut small-cap sono state +archiviate **senza mai misurare se sarebbero vive a $100k** — che e' il nozionale di un conto +funded, il canale su cui il progetto sta valutando di puntare. + +E c'e' una ragione forte per rifare il conto ADESSO: l'ondata del 22-23/08 ha falsificato **tre muri +di taglia** pubblicati (il "$20k di XS01" non esiste; il muro delle opzioni BTC era il COLLATERALE +PER VENDERE, non il costo di comprare; i lotti USDC-lineari sono 7,5-10x piu' piccoli di quelli +inverse su cui erano stati misurati). Tre su tre sbagliati, tutti nella stessa direzione: piu' +permissivi del creduto. + +ATTESE A PRIORI — scritte leggendo il mandato, PRIMA di qualunque misura +----------------------------------------------------------------------- +A1 Il censimento conterra' 10-15 voci e la MAGGIORANZA avra' anche un rifiuto sul lato EDGE: + la taglia sara' raramente l'unico motivo. +A2 (controllo positivo) XS01 riprodurra' haircut ~0 gia' a $200-600 di capitale ALLOCATO. + Se il mio codice non lo trova, il mio codice e' rotto e mi fermo. +A3 A $100.000 ogni candidato multi-gamba converge alla propria serie MODELLATA (haircut -> 0). +A4 Nessun archiviato passera' TUTTI i gate a $100k — e non perche' ricontrolli i gate, ma per una + ragione strutturale: i gate pubblicati sono stati calcolati sulla serie MODELLATA, che E' il + limite a capitale infinito. Se e' vero, la domanda del mandato era gia' risposta nei registri. +A5 Il corollario funded non aggiungera' NIENTE di nuovo: l'unico ampliamento reale dell'insieme + ammissibile e' XS01, e §1/§29 l'hanno gia' contato (+0,400 di J). +A6 L'haircut sara' MONOTONO decrescente nel capitale. <-- questa e' quella che mi aspetto di + confermare senza pensarci; se salta, e' il risultato metodologico del filone. + +METODO +------ +* Repliche PRIMA di ogni delta: identita' bit-exact di XS01 col sleeve di produzione; libro 75/25 + `hourly` contro il numero pubblicato (Sh 1,683 / maxDD 11,22%). +* Parametri VERI del venue (non quelli di memoria): Deribit min-order $5, taker 3,50 bps dal + 2026-08-01; Hyperliquid min-order **$10** (non $5) e taker **4,50 bps** (non 5,0), misurati da + HL-EXEC il 22/08; lotti della famiglia **USDC-lineare** (non inverse), letti dal venue. +* La LENTE del libro e' dichiarata: `hourly` (path live), Sh 1,683 / maxDD 11,22% — non la + canonica 1,800/9,56% ne' la mediana d'ancora 1,626/10,4%. +* Distinzione dichiarata riga per riga: **capitale PROPRIO** (e' tuo, il vincolo e' il min-order) + contro **nozionale FUNDED** (non e' tuo, il vincolo binding e' la regola di drawdown). + +SICUREZZA: sola lettura. Nessun ordine. Nessun file di produzione toccato. Solo GET pubbliche, +con pacing, cache e astensione nelle finestre del cron (:05-:10 book, :24-:30 catena). + + uv run python scripts/research/r0823b_regime_capitale.py + uv run python scripts/research/r0823b_regime_capitale.py --no-net # tutto da cache/costanti +""" +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +import time +import urllib.request +from datetime import datetime, timezone +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"): + if str(_p) not in sys.path: + sys.path.insert(0, str(_p)) + +CACHE = ROOT / "data" / "cache_regime_capitale.json" + +# -------------------------------------------------------------------------------------------- +# PARAMETRI VENUE — dichiarati, con la FONTE. Nessuno di questi e' "di memoria". +# -------------------------------------------------------------------------------------------- +DER_MIN_ORDER = 5.0 # config/live.json -> min_order_usd +DER_FEE_SIDE = 0.00035 # 3,50 bps/lato dal 2026-08-01 (SLIP-AUDIT: 16/16 fill sul nastro) +HL_MIN_ORDER = 10.0 # HL-EXEC 22/08: docs + 3.827 livelli, 0,0% sotto $10 (NON $5) +HL_FEE_SIDE = 0.00045 # HL-EXEC 22/08: 4,50 bps, due fonti indipendenti (NON 5,0) +SPLIT_FUNDED = 0.80 # r0725_hyro.SPLIT — quota trader del profitto su conto funded +BARRIERA_FUNDED = 0.06 # HyroTrader max loss STATICO 6% (r0725_hyro.EV_DD, regole vere) +EURUSD = 1.09 +CAPITALI = (635.0, 5_000.0, 20_000.0, 100_000.0) # la griglia del mandato +GRIGLIA_DENSA = (95.0, 200.0, 400.0, 600.0, 635.0, 1_000.0, 1_500.0, 2_000.0, 3_000.0, + 5_000.0, 7_500.0, 10_000.0, 15_000.0, 20_000.0, 50_000.0, 100_000.0, 200_000.0) +TOL_STAB = 0.10 # |Sh(C) - Sh(modellata)| <= 0,10 per DICHIARARE eseguibile +SEED = 823 + +_last_net = [0.0] + + +def sez(t: str) -> None: + print("\n" + "=" * 100) + print(" " + t) + print("=" * 100) + + +def sub(t: str) -> None: + print("\n --- " + t) + + +def _pace() -> None: + """<=2 req/s e astensione nelle finestre del cron live.""" + now = datetime.now(timezone.utc) + while now.minute in (5, 6, 7, 8, 9, 10, 24, 25, 26, 27, 28, 29, 30): + print(f" [pausa: minuto {now.minute} e' finestra del cron live]") + time.sleep(20) + now = datetime.now(timezone.utc) + dt = time.time() - _last_net[0] + if dt < 0.6: + time.sleep(0.6 - dt) + _last_net[0] = time.time() + + +def get_json(url: str, key: str, no_net: bool): + c = json.loads(CACHE.read_text()) if CACHE.exists() else {} + if no_net or (key in c and time.time() - c[key].get("_t", 0) < 12 * 3600): + return c.get(key, {}).get("v") + try: + _pace() + with urllib.request.urlopen(url, timeout=20) as r: + v = json.loads(r.read().decode()) + except Exception as e: # noqa: BLE001 + print(f" [rete KO su {key}: {type(e).__name__}] -> uso la cache se c'e'") + return c.get(key, {}).get("v") + c[key] = {"_t": time.time(), "v": v} + CACHE.write_text(json.dumps(c)) + return v + + +def sola_lettura() -> None: + """Prova che il filone non ha toccato la produzione (il cron ESEGUE dalla working tree).""" + prot = ["src/", "config/", "scripts/live/", "tests/", "data/paper_statarb", "data/paper_xsr", + "data/paper_dvolspread", "data/live"] + out = subprocess.run(["git", "status", "--porcelain", "--"] + prot, + cwd=ROOT, capture_output=True, text=True).stdout.strip() + print(f" git status sui percorsi protetti: {'VUOTO (sola lettura provata)' if not out else out}") + assert not out, "PRODUZIONE TOCCATA — fermarsi" + + +# ============================================================================================= +# METRICHE E DEFINIZIONE DI ESEGUIBILITA' +# ============================================================================================= +ANN = np.sqrt(365.25) + + +def met(net: np.ndarray, idx) -> dict: + x = np.asarray(net, float) + sd = x.std(ddof=1) + sh = float(x.mean() / sd * ANN) if sd > 0 else 0.0 + eq = np.cumprod(1.0 + np.clip(x, -0.99, None)) + dd = float((1.0 - eq / np.maximum.accumulate(eq)).max()) + yrs = max((idx[-1] - idx[0]) / pd.Timedelta("365.25D"), 1e-9) + return dict(sharpe=sh, maxdd=100 * dd, cagr=100 * (eq[-1] ** (1 / yrs) - 1.0)) + + +def c_stabilita(curva: dict[float, float], sh_mod: float, tol: float = TOL_STAB) -> float | None: + """C* = il PIU' PICCOLO capitale tale che da li' in su lo Sharpe NON si discosta piu' di `tol` + dalla serie modellata. Definizione scelta PRIMA di guardare, e per un motivo: ne' l'haircut ne' + la quota di nozionale eseguito, da soli, distinguono "eseguibile" da "degenere" (sez. 3).""" + caps = sorted(curva) + for i, c in enumerate(caps): + if all(abs(curva[c2] - sh_mod) <= tol for c2 in caps[i:]): + return c + return None + + +def maxdd_k(r: np.ndarray, k: float) -> float: + eq = np.cumprod(1.0 + np.clip(k * np.asarray(r, float), -0.99, None)) + return float((1.0 - eq / np.maximum.accumulate(eq)).max()) + + +def cagr_k(r: np.ndarray, k: float, idx) -> float: + eq = np.cumprod(1.0 + np.clip(k * np.asarray(r, float), -0.99, None)) + yrs = max((idx[-1] - idx[0]) / pd.Timedelta("365.25D"), 1e-9) + return float(eq[-1] ** (1 / yrs) - 1.0) + + +def k_barriera(r: np.ndarray, target: float = BARRIERA_FUNDED) -> float: + lo, hi = 1e-3, 20.0 + if maxdd_k(r, hi) < target: + return hi + for _ in range(60): + mid = 0.5 * (lo + hi) + if maxdd_k(r, mid) < target: + lo = mid + else: + hi = mid + return lo + + +# ============================================================================================= +# SEZ. 1 — IL CENSIMENTO +# ============================================================================================= +# (voce, dove il muro di taglia e' dichiarato, muro come pubblicato, stato del muro OGGI, +# c'e' ANCHE un rifiuto sul lato EDGE?) +CENSIMENTO = [ + ("XS01 (19 gambe HL)", "CLAUDE.md 19/06", + "STAT-MODE, 'serve ~20k'", "FALSIFICATO 22/08 (haircut ~0 a $200-600)", + "no — e' nel book di ricerca; fuori dal LIVE per VENUE (14/19) e decisione venue"), + ("XSR01 (50 gambe HL)", "CLAUDE.md 25/07", + "ticket $1,73 < $5 -> 'reale a ~$5k'", "RIDIMENSIONATO 22/08: soglia vera ~$3.000", + "SI — weights_tilt_null FAIL (book 5-sleeve), storia 2,6a monoregime; gate 23/10 APERTO"), + ("STATARB-MULTI (50 coppie)", "CLAUDE.md 25/07", + "51 gambe = STAT-MODE", "MAI MISURATO -> misurato qui", + "SI — IC95 [-0,12; 1,72], t 1,31; uplift -0,11 vs null a priori"), + ("CC01 cash-and-carry", "CLAUDE.md 26/06", + "4-38 gambe, 'rivedere a ~20k+'", "FALSIFICATO 22/08 (§34: lotti $10/$1; §39: linea USDC esiste)", + "SI — DSR 0,363 FAIL, implausible_sharpe 5/5, de-levering REFUTED, 2 gambe = Sharpe -0,42"), + ("VRP01 (put credit spread)", "CLAUDE.md 30/07", + "lotto BTC $6.210 / ETH $1.832 -> 0 lotti al peso 12%", + "FALSIFICATO 22/08 (§50: famiglia USDC ETH $242 / BTC $772)", + "SI — regola 'niente short-vol da modello in deploy' + f=0,73 misurato"), + ("Opzioni long (tail-hedge, gamma)", "CLAUDE.md 26/06 + 22/08", + "'opzione BTC min $5.968 >> $600'", "FALSIFICATO 23/08 (§46: comprare costa il PREMIO, $1,79-7,93)", + "SI — gamma scalp Sharpe -3..-6 ogni anno; tail-hedge maxDD sale in 162/162 celle"), + ("Albimarini double-diagonal", "CLAUDE.md 02/07", + "(nessun muro di taglia)", "n/d — fee-negativa a QUALSIASI size", + "SI — fee 8 gambe = 194-221% del theta; celle 0-perdite = implausible_sharpe"), + ("Tranching TP01 K=2/4", "CLAUDE.md 02/07", + "'delta ~$1-2 < min-order -> K=1'; rivalutare a >=5-10k", + "FALSIFICATO 23/08 (§47: build_book_order banda la posizione NETTA)", + "SI — dSharpe n.s.; e' riduzione della VARIANZA DELLA STIMA, non ritorno"), + ("BIN-FREQ (size per-trade SKH01)", "CLAUDE.md 22/08", + "riapertura = SOGLIA DI CAPITALE $5.600 (cella) / $53.000 (famiglia)", + "VIVO — e' l'unico con una soglia di capitale come condizione DICHIARATA", + "SI — mediana di famiglia 6% della cella, 54% celle positive; weights_tilt_null FAIL"), + ("XS01-D (11 gambe Deribit)", "RESULTS §50/§56 23/08", + "'aspettare 2-3 listing'", "n/d — non e' capitale, e' VENUE", + "SI — IC -0,027 (t -0,88) su quelle 11; DSR 0,158 FAIL"), + ("GTAA01 (6 ETF)", "CLAUDE.md 25-26/07", + "GTAA_MIN_CAPITAL $3.000", "n/d — sopra c'e' il PRIIPs: ordine RIFIUTATO sul conto reale", + "no — regge come ricerca (LOO: unico positivo nel 100% delle estrazioni)"), + ("XS01 espanso (52 -> 51 HL)", "CLAUDE.md 19-20/06", + "(nessun muro di taglia)", "n/d", + "SI — il long-tail DILUISCE il momentum relativo; peggiore a ogni variante"), + ("IVOL (idio-vol cross-sectional)", "CLAUDE.md 29/06", + "STAT-MODE", "MAI MISURATO (lead mai ripreso)", + "SI — nato come LEAD debole nello stesso sweep in cui XSEC-V2 fa DSR 0,13"), + ("LOWVOL 19-major", "CLAUDE.md 29/06", + "STAT-MODE", "n/d", + "SI — deflated-Sharpe 0,13 + storia 2,5a"), +] + + +def sezione1() -> None: + sez("1) IL CENSIMENTO — tutto cio' che il progetto ha archiviato CITANDO LA TAGLIA") + print(""" + Regola di lettura: la colonna che decide non e' 'stato del muro' ma l'ULTIMA. Un muro di taglia + che cade libera un candidato solo se non c'era anche un rifiuto sull'edge — e i gate sull'edge + NON dipendono dal capitale (sez. 5). +""") + print(" " + "-" * 98) + n_edge = 0 + for nome, fonte, muro, stato, edge in CENSIMENTO: + n_edge += edge.startswith("SI") + print(f" {nome:<32}[{fonte}]") + print(f" {'':<32}muro pubblicato : {muro}") + print(f" {'':<32}muro OGGI : {stato}") + print(f" {'':<32}EDGE : {edge}") + print(" " + "-" * 98) + n = len(CENSIMENTO) + print(f"\n VOCI: {n} con rifiuto ANCHE sull'edge: {n_edge}/{n} = {100*n_edge/n:.0f}%") + falsi = sum(1 for _, _, _, s, _ in CENSIMENTO if s.startswith("FALSIFICATO")) + mai = sum(1 for _, _, _, s, _ in CENSIMENTO if s.startswith("MAI")) + print(f" muri di taglia FALSIFICATI (tutti dall'ondata 22-23/08): {falsi}/{n}") + print(f" muri di taglia MAI MISURATI (li misuro qui dove possibile): {mai}/{n}") + print(f"\n A1 {'CONFERMATA' if n_edge / n > 0.5 else 'REFUTATA'}: la taglia e' l'unico motivo " + f"in {n - n_edge}/{n} voci.") + + +# ============================================================================================= +# SEZ. 2-3 — LE CURVE DI HAIRCUT +# ============================================================================================= +def carica_candidati(): + """Ricostruisce i pesi dei 3 candidati multi-gamba REALMENTE misurabili. Nessuna + reimplementazione: si importano il sleeve di produzione e il monitor live.""" + from r0822_hl_exec import sim_libro, xs01_pesi # noqa: F401 + out = {} + + W, dret, C, cols, dif = xs01_pesi(0) + print(f" XS01 (19 gambe HL): identita' vs sleeves._xsec_returns max|dif| = {dif}") + assert dif == 0.0, "XS01 non replica il sleeve di produzione" + out["XS01 (19g, HL)"] = dict(W=W, R=dret, hedge=None, idx=C.index, + fee=HL_FEE_SIDE, floor=HL_MIN_ORDER, legs=19, venue="HL") + + from scripts.live.paper_xsr import FEE_LEG, build_panel + ts, idx, Wx, Rx, rb, colx = build_panel() + print(f" XSR01 (50 gambe HL): pannello dal MONITOR di produzione, {Wx.shape[1]} gambe, " + f"fee monitor {FEE_LEG*1e4:.1f} bps") + out["XSR01 (50g, HL)"] = dict(W=Wx, R=Rx, hedge=rb, idx=idx, + fee=FEE_LEG, floor=HL_MIN_ORDER, legs=50, venue="HL") + + import r0725_statarb_multi as SM + base = SM.load_hl(SM.BASE) + pc, rc = {}, {} + for sym in SM.universe(): + try: + tgt = SM.load_hl(sym) + except FileNotFoundError: + continue + ix = base.index.intersection(tgt.index) + if len(ix) < SM.MIN_BARS: + continue + p, _ = SM.signal(base[ix], tgt[ix]) + pc[sym] = pd.Series(p, index=ix) + rc[sym] = tgt[ix].pct_change().fillna(0.0) + P = pd.concat(pc, axis=1, sort=True).sort_index().fillna(0.0) + Rr = pd.concat(rc, axis=1, sort=True).sort_index().reindex(P.index).fillna(0.0)[P.columns] + k = P.shape[1] + print(f" STATARB-MULTI: {k} coppie = {k+1} gambe (meccanismo CONGELATO W=45 sgn=+1, " + f"da r0725_statarb_multi)") + out["STATARB-MULTI (51g, HL)"] = dict( + W=(P / k).to_numpy(float), R=Rr.to_numpy(float), + hedge=base.pct_change().fillna(0.0).reindex(P.index).fillna(0.0).to_numpy(float), + idx=P.index, fee=SM.FEE_LEG, floor=HL_MIN_ORDER, legs=k + 1, venue="HL") + return out + + +def curva(cand: dict, caps, floor=None) -> tuple[dict, dict, dict]: + from r0822_hl_exec import sim_libro + fl = cand["floor"] if floor is None else floor + sh, noz, nets = {}, {}, {} + for c in caps: + s = sim_libro(cand["W"], cand["R"], cand["hedge"], c, fl, None, None, cand["fee"]) + m = met(s["net"], cand["idx"]) + sh[c] = m["sharpe"] + noz[c] = s["pct_nozionale"] + nets[c] = s["net"] + s = sim_libro(cand["W"], cand["R"], cand["hedge"], 1e12, None, None, None, cand["fee"]) + nets["MOD"] = s["net"] + return sh, noz, nets + + +def sezione23(CAND: dict) -> dict: + sez("2) CONTROLLO POSITIVO — XS01, il muro gia' caduto il 22/08") + print(""" + Se il mio codice non ritrova haircut ~0 a $200-600 su XS01, e' rotto e il resto non vale niente. + (HL-EXEC 22/08: "haircut ~0 gia' a $200-600 allocati; a $600 lo Sharpe passa 1,33 -> 1,30".)""") + sh, noz, nets = curva(CAND["XS01 (19g, HL)"], GRIGLIA_DENSA) + shm = met(nets["MOD"], CAND["XS01 (19g, HL)"]["idx"])["sharpe"] + print(f"\n {'capitale ALLOCATO':>19}{'Sharpe':>9}{'haircut':>10}{'% nozionale eseguito':>23}") + for c in GRIGLIA_DENSA: + print(f" {c:>19,.0f}{sh[c]:>9.3f}{shm - sh[c]:>10.3f}{noz[c]:>22.1f}%") + print(f" {'MODELLATA':>19}{shm:>9.3f}{0.0:>10.3f}{100.0:>22.1f}%") + ok = abs(sh[200.0] - shm) <= 0.05 and abs(sh[635.0] - shm) <= 0.05 + print(f"\n A2 {'CONFERMATA' if ok else 'REFUTATA — CODICE ROTTO, MI FERMO'}: " + f"haircut a $200 = {shm - sh[200.0]:+.3f}, a $635 = {shm - sh[635.0]:+.3f}.") + assert ok, "controllo positivo fallito" + for tol in (0.10, 0.05): + cs = c_stabilita(sh, shm, tol) + print(f" C* a tolleranza {tol:.2f} di Sharpe = " + f"{('$%s' % f'{cs:,.0f}') if cs else '>200k'} di capitale ALLOCATO.") + print(f""" + PRECISAZIONE che il numero pubblicato non porta con se': i "$200-600" sono capitale ALLOCATO + AL SLEEVE. A peso di libro 15% servono ${200/0.15:,.0f}-${635/0.15:,.0f} di LIBRO per stare li'; a un + libro da $635 il sleeve vede $95 e l'haircut e' {shm - sh[95.0]:+.3f} — non zero.""") + + sez("3) LE CURVE — i tre candidati multi-gamba, ai capitali del mandato") + print(f"\n {'candidato':<26}{'gambe':>6}{'venue':>7}" + + "".join(f"{('$%s' % f'{c:,.0f}'):>12}" for c in CAPITALI) + + f"{'MODELL.':>10}{'C*':>12}") + print(" " + "-" * 110) + res = {} + for nome, cand in CAND.items(): + shd, nozd, netsd = curva(cand, GRIGLIA_DENSA) + shm = met(netsd["MOD"], cand["idx"])["sharpe"] + cs = c_stabilita(shd, shm) + res[nome] = dict(sh=shd, noz=nozd, nets=netsd, sh_mod=shm, cstar=cs, cand=cand) + print(f" {nome:<26}{cand['legs']:>6}{cand['venue']:>7}" + + "".join(f"{shd[c]:>12.3f}" for c in CAPITALI) + + f"{shm:>10.3f}" + (f"{cs:>12,.0f}" if cs else f"{'>200k':>12}")) + print(" " + "-" * 110) + sub("3a) la curva DENSA (Sharpe sopra, % di nozionale eseguito sotto)") + print(f" {'capitale':>10}" + "".join(f"{nm.split(' (')[0]:>13}" for nm in res)) + for c in GRIGLIA_DENSA: + print(f" {c:>10,.0f}" + "".join(f"{res[nm]['sh'][c]:>13.3f}" for nm in res)) + print(f" {'':>10}" + "".join(f"{res[nm]['noz'][c]:>12.1f}%" for nm in res)) + print(f" {'MODELLATA':>10}" + "".join(f"{res[nm]['sh_mod']:>13.3f}" for nm in res)) + print(f"\n C* per tolleranza:") + for tol in (0.10, 0.05): + row = [] + for nm in res: + cs = c_stabilita(res[nm]["sh"], res[nm]["sh_mod"], tol) + row.append(("$%s" % f"{cs:,.0f}") if cs else ">200k") + print(f" tol {tol:.2f}: " + " ".join(f"{nm.split(' (')[0]}={v}" for nm, v in zip(res, row))) + + sub("3b) LA TRAPPOLA — l'haircut da solo NON rileva un muro di taglia") + print(""" + `altlib.eval_weights_smallcap` riporta `sharpe_haircut = modellata - reale`. Su un candidato + multi-gamba, sotto la soglia di eseguibilita' quel numero PUO' ESSERE NEGATIVO: il pavimento + taglia quasi tutto il nozionale e cio' che resta e' una strategia DIVERSA, piccola e casuale. + Letto da solo, un haircut negativo dice "eseguibile, anzi meglio". E' esattamente al rovescio.""") + print(f"\n {'candidato':<26}{'cap':>10}{'Sharpe':>9}{'haircut':>10}{'% noz':>9} lettura") + for nome, r in res.items(): + for c in (635.0, 5_000.0): + h = r["sh_mod"] - r["sh"][c] + deg = r["noz"][c] < 50.0 + print(f" {nome:<26}{c:>10,.0f}{r['sh'][c]:>9.3f}{h:>10.3f}{r['noz'][c]:>8.1f}% " + f"{'DEGENERE (il numero non descrive la strategia)' if deg else 'eseguibile'}") + st = res["STATARB-MULTI (51g, HL)"] + print(f""" + Il caso limpido: STATARB-MULTI a $635 fa Sharpe {st['sh'][635.0]:.3f} contro + {st['sh_mod']:.3f} modellata -> haircut {st['sh_mod'] - st['sh'][635.0]:+.3f}, cioe' NEGATIVO, + con il {st['noz'][635.0]:.1f}% del nozionale eseguito. Un gate scritto su `sharpe_haircut <= X` + lo avrebbe promosso.""") + + sub("3c) A6 — monotonia dell'haircut nel capitale") + for nome, r in res.items(): + seq = [r["sh"][c] for c in GRIGLIA_DENSA] + d = np.diff(seq) + mono = bool((d >= -1e-9).all()) + print(f" {nome:<26} monotono crescente in C? {'SI' if mono else 'NO'} " + f"(peggior passo indietro {d.min():+.3f}; max sopra la modellata " + f"{max(seq) - r['sh_mod']:+.3f})") + print(""" + A6 REFUTATA su 3/3. Sotto C* lo Sharpe non e' una misura della strategia: e' una misura di + QUALI gambe il pavimento ha lasciato passare. Il segno del passo e' rumore.""") + return res + + +def sezione3d(CAND: dict) -> None: + sub("3d) IL PAVIMENTO DEL VENUE SBAGLIATO — `scripts/live/paper_xsr.MIN_ORDER = 5.0`") + print(""" + XSR01 gira su HYPERLIQUID (50 alt). Il suo monitor forward — quello che alimenta il gate + pre-registrato del 2026-10-23 — usa `MIN_ORDER = 5.0`, che e' il pavimento di **DERIBIT**. + Il pavimento di Hyperliquid e' **$10** (HL-EXEC 22/08: 3.827 livelli, 0,0% sotto $10). + I due libri REAL del monitor sono $600 e $5.000: entrambi nella regione in cui il pavimento morde.""") + c = CAND["XSR01 (50g, HL)"] + sh5, noz5, _ = curva(c, GRIGLIA_DENSA, floor=5.0) + sh10, noz10, nets = curva(c, GRIGLIA_DENSA, floor=10.0) + shm = met(nets["MOD"], c["idx"])["sharpe"] + print(f"\n {'capitale':>10}{'Sh @ $5 (monitor)':>20}{'Sh @ $10 (venue)':>19}{'differenza':>13}" + f"{'% noz @$10':>12}") + for cc in (600.0, 635.0, 2_000.0, 3_000.0, 5_000.0, 20_000.0, 100_000.0): + print(f" {cc:>10,.0f}{sh5[cc]:>20.3f}{sh10[cc]:>19.3f}{sh5[cc]-sh10[cc]:>13.3f}" + f"{noz10[cc]:>11.1f}%") + d = np.array([sh5[cc] - sh10[cc] for cc in GRIGLIA_DENSA if cc <= 5_000.0]) + print(f""" + Non e' "il monitor e' ottimista": nella regione che conta la differenza cambia SEGNO + (min {d.min():+.3f}, max {d.max():+.3f}) — perche' sotto C* si stanno confrontando due + sottoinsiemi casuali di gambe, non due stime della stessa cosa. + C* col pavimento vero = ${c_stabilita(sh10, shm) or float('nan'):,.0f}; col pavimento del monitor + = ${c_stabilita(sh5, shm) or float('nan'):,.0f}. + Effetto sul gate 23/10: la sua gamba di eseguibilita' e' "haircut a $5.000 <= 40%", e a $5.000 il + monitor misura con il pavimento sbagliato. NON anticipo il gate: registro che l'ingresso e' storto. + 6a occorrenza dello schema `fee_watch` — un controllo puntato su una configurazione diversa da + quella che gira.""") + + +# ============================================================================================= +# SEZ. 4 — VRP01: lotti, non haircut +# ============================================================================================= +def sezione4(no_net: bool) -> None: + sez("4) VRP01 — qui il muro non e' un haircut, e' un LOTTO INDIVISIBILE") + j = get_json("https://www.deribit.com/api/v2/public/get_instruments" + "?currency=USDC&kind=option&expired=false", "usdc_opt", no_net) + idx = get_json("https://www.deribit.com/api/v2/public/get_index_price?index_name=btc_usd", + "px_btc", no_net) + idxe = get_json("https://www.deribit.com/api/v2/public/get_index_price?index_name=eth_usd", + "px_eth", no_net) + px = {"BTC": (idx or {}).get("result", {}).get("index_price"), + "ETH": (idxe or {}).get("result", {}).get("index_price")} + mins, nstr = {}, {} + for it in ((j or {}).get("result") or []): + nm = it.get("instrument_name", "") + for a in ("BTC", "ETH"): + if nm.startswith(f"{a}_USDC-") and it.get("option_type") == "put": + mins[a] = min(mins.get(a, 9e9), float(it.get("min_trade_amount", 9e9))) + nstr[a] = nstr.get(a, 0) + 1 + if mins and all(px.values()): + print(" letto dal venue ORA (GET pubblica su kind=OPTION, famiglia USDC-LINEARE = quella") + print(" che un conto USDC puo' marginare; VRP01 VENDE, quindi il lotto e' il NOZIONALE" + " sottostante,") + print(" non il premio — e' la distinzione che §46 ha dovuto correggere sul lato LONG):") + for a in ("BTC", "ETH"): + print(f" {a}_USDC put ({nstr.get(a,0)} strumenti) min_trade_amount = " + f"{mins.get(a)} indice ${px[a]:,.0f} -> lotto = ${mins.get(a, 0) * px[a]:,.0f}") + lot = {a: mins[a] * px[a] for a in mins} + else: + print(" [venue non letto -> uso i valori verificati dal coordinatore il 22/08 (§50)]") + lot = {"BTC": 772.0, "ETH": 242.0} + print(f""" + Il sleeve VRP01 e' 50/50 BTC+ETH al peso di libro 12%. Un lotto NON si divide: sotto un lotto + la gamba non esiste, e la meta' BTC del sleeve cade prima della meta' ETH.""") + print(f" (memoria: $242 ETH / $772 BTC, §50 22/08 — e $1.832/$6.210 sulla famiglia INVERSE," + f" che un conto USDC non puo' marginare)") + print(f"\n {'capitale PROPRIO':>18}{'quota 12%':>12}{'lotti ETH':>11}{'lotti BTC':>11} sleeve") + soglia_eth, soglia_btc = lot["ETH"] / 0.06, lot["BTC"] / 0.06 + for c in (635.0, 2_000.0, soglia_eth, 5_000.0, 6_440.0, soglia_btc, 20_000.0, 100_000.0): + q = 0.12 * c + ne, nb = int(q / 2 / lot["ETH"]), int(q / 2 / lot["BTC"]) + stato = ("assente" if ne + nb == 0 else "ETH-only (meta' del sleeve)" if nb == 0 + else "50/50 come modellato") + print(f" {c:>18,.0f}{q:>12,.0f}{ne:>11}{nb:>11} {stato}") + print(f""" + A $100.000 il sleeve e' 50/50 con {int(0.06*100000/lot['ETH'])} lotti ETH e + {int(0.06*100000/lot['BTC'])} BTC: il muro di taglia di VRP01 CADE del tutto. + + CORREZIONE A UN NUMERO PUBBLICATO IERI (§50 / CLAUDE.md): "1 lotto ETH copre il peso di book 12% + gia' da ~$2.000, e il BTC da ~$6.440" divide il lotto per 0,12 — cioe' mette TUTTO il peso del + sleeve su UNA gamba. VRP01 e' 50/50: la gamba ETH vede il 6% del libro, non il 12%. + Soglie vere: prima gamba (ETH) da ${soglia_eth:,.0f}; sleeve 50/50 completo da ${soglia_btc:,.0f} + — cioe' il DOPPIO di quanto pubblicato, su entrambe. Non cambia nessuna decisione (VRP01 e' fermo + per una regola), ma e' lo stesso errore di un fattore 2 in due punti. + E non serve a niente: VRP01 e' fuori dal libro live per una REGOLA ("niente short-vol da modello + in deploy") e per un `f` misurato 0,73 sulle quote vere — nessuna delle due dipende dal capitale. + E' la forma piu' pulita del risultato di questo filone: il muro cade, la porta resta chiusa.""") + + +# ============================================================================================= +# SEZ. 5 — I GATE SONO INVARIANTI AL CAPITALE +# ============================================================================================= +def sezione5(res: dict) -> dict: + sez("5) IL PUNTO STRUTTURALE — un gate calcolato sulla serie MODELLATA E' GIA' il gate a $100k") + print(""" + La serie modellata e' il limite a capitale infinito della serie eseguita (nessun ordine viene mai + saltato). Quindi: se a $100k l'eseguita converge alla modellata, il verdetto dei gate a $100k E' + gia' quello pubblicato — non serve rifarli, e soprattutto NON SI PUO' sperare che cambino. + Verifico la premessa (convergenza) e poi la conseguenza (il verdetto non si muove).""") + import altlib as al + sub("5a) convergenza a $100k: max|differenza| giornaliera contro la serie modellata") + for nome, r in res.items(): + d = np.abs(r["nets"][100_000.0] - r["nets"]["MOD"]).max() + print(f" {nome:<26} max|dif| = {d:.3e} dSharpe = " + f"{r['sh'][100_000.0] - r['sh_mod']:+.4f} % noz = {r['noz'][100_000.0]:.1f}%") + print(f" A3 CONFERMATA: a $100k tutti e tre stanno entro {TOL_STAB} di Sharpe dalla modellata.") + + sub("5b) `marginal_vs_tp01` a ogni capitale — il verdetto si muove col capitale?") + print(f"\n {'candidato':<26}" + "".join(f"{('$%s' % f'{c:,.0f}'):>13}" for c in CAPITALI) + + f"{'MODELLATA':>13}") + ver = {} + for nome, r in res.items(): + row, vv = [], [] + for c in list(CAPITALI) + ["MOD"]: + s = pd.Series(r["nets"][c], index=r["cand"]["idx"]) + m = al.marginal_vs_tp01(s) + v = m.get("marginal_verdict", "?") + row.append(v) + vv.append(v) + ver[nome] = vv + print(f" {nome:<26}" + "".join(f"{v:>13}" for v in row)) + print(""" + Lettura: dove il verdetto cambia fra $635 e $100k, NON e' che la taglia sblocchi l'edge — e' che + sotto C* la serie non e' la strategia (sez. 3b). Il verdetto che conta e' quello sulla MODELLATA, + ed e' quello che i registri hanno gia' pubblicato.""") + + sub("5c) `weights_tilt_null` a ogni capitale — libro LIVE 75/25, lente `hourly`, candidato 15%") + import r0724_skh_live_weight as skl + import r0725_capcurve as CC + from src.portfolio.portfolio import combine_outer, metrics, weights_tilt_null + skh = skl.skh_book(0, "hourly") + if skh.index.tz is None: + skh.index = skh.index.tz_localize("UTC") + lo = pd.Timestamp("2019-03-14", tz="UTC") + tp = CC.tp01_realistic(635.0 * 0.75) + base = combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": 0.75, "SKH01": 0.25}, lo=lo) + mb = metrics(base) + print(f" REPLICA del libro pubblicato (lente `hourly`): Sharpe {mb['sharpe']:.3f} " + f"(pubblicato 1,683) maxDD {100*mb['maxdd']:.2f}% (pubblicato 11,22%) " + f"-> {'OK' if abs(mb['sharpe']-1.683) < 0.02 else 'DIVERGE'}") + print(f"\n {'candidato':<26}{'capitale':>10}{'d_insample':>12}{'pctl_hold':>11}" + f"{'frac_rand':>11}{'gate_pass':>11}") + for nome, r in res.items(): + for c in CAPITALI: + s = pd.Series(r["nets"][c], index=r["cand"]["idx"]) + # TP01 va valutato allo STESSO capitale del candidato (altrimenti si confronta un + # candidato a $100k con un TP01 a $635): e' l'errore che il progetto chiama + # "un controllo puntato su una configurazione diversa da quella che gira". + tpc = CC.tp01_realistic(c * 0.6375) + g = weights_tilt_null({"TP01": tpc, "SKH01": skh, "C": s}, + {"TP01": 0.75, "SKH01": 0.25, "C": 0.0}, + {"TP01": 0.6375, "SKH01": 0.2125, "C": 0.15}, + n=300, seed=SEED) + print(f" {nome:<26}{c:>10,.0f}{g['delta_insample']:>12.4f}{g['pctl_hold']:>11.1f}" + f"{g['frac_random_beat_hold']:>11.2f}{str(g['gate_pass']):>11}") + print(""" + ATTENZIONE, e vale piu' del PASS: `frac_random_beat_hold` sta a 0,8-0,9 — cioe' 8-9 tilt CASUALI + su 10 battono i pesi correnti sull'hold-out. Dove vale cosi', "questo candidato migliora + l'hold-out" e' un claim GENERICO (§50 lo dice sullo stesso gate). E il libro di riferimento non e' + quello su cui XSR01 fu bocciato: CLAUDE.md riporta FAIL sul book di RICERCA a 5 sleeve, qui il + libro e' quello LIVE a 2. Due libri, due verdetti — si dichiara quale.""") + return ver + + +# ============================================================================================= +# SEZ. 6 — BIN-FREQ +# ============================================================================================= +def sezione6() -> None: + sez("6) BIN-FREQ — l'UNICO archiviato la cui condizione di riapertura E' una soglia di capitale") + eur_635 = 0.0113 # RESULTS §25: taglia misurata a $635 + rapporto_fam = 0.007 / 0.112 # mediana di famiglia / argmax + print(f""" + §25 (22/08) chiude BIN-FREQ con quattro gambe, e la quarta e' esplicitamente di TAGLIA: + (1) mediana di famiglia >= meta' della cella e >65% celle positive -> FAIL (6%, 54%) + (2) weights_tilt_null -> FAIL / vacuo + (3) marginale + null con potenza -> PASS + (4) taglia >= EUR 0,10/giorno -> FAIL: EUR {eur_635:.4f}/g a $635 + La (4) e' lineare nel capitale. Le altre tre NON lo sono: sono statistiche sulla serie.""") + print(f"\n {'capitale':>10}{'EUR/g (cella)':>16}{'EUR/g (famiglia)':>19}{'gate (4)?':>12}") + for c in (635.0, 5_600.0, 20_000.0, 53_000.0, 100_000.0, 200_000.0): + ec = eur_635 * c / 635.0 + ef = ec * rapporto_fam + print(f" {c:>10,.0f}{ec:>16.4f}{ef:>19.4f}{('PASS' if ef >= 0.10 else 'PASS(cella)' if ec >= 0.10 else 'FAIL'):>12}") + print(f""" + A $100.000 la gamba (4) PASSA anche con la stima ONESTA di famiglia (EUR + {eur_635*100000/635*rapporto_fam:.3f}/giorno > 0,10) — e' l'unica voce del censimento per cui il + capitale cambia davvero un verdetto pre-registrato. + MA le gambe (1) e (2) restano FAIL, e sono invarianti al capitale: la mediana di famiglia e' il + 6% dell'argmax e il 54% delle celle e' positivo comunque si finanzi il conto. + -> BIN-FREQ a $100k: 1 gate su 4 si sblocca, 2 restano chiusi. RESTA UN LEAD.""") + + +# ============================================================================================= +# SEZ. 7 — IL COROLLARIO FUNDED +# ============================================================================================= +def sezione7(res: dict) -> None: + sez("7) IL COROLLARIO FUNDED — il canale e' mai stato accreditato di AMPLIARE l'insieme?") + print(""" + Distinzione obbligatoria: fin qui la colonna 'capitale' era CAPITALE PROPRIO. Un conto funded da + $100k non e' capitale proprio: il nozionale e' della firm, il vincolo binding e' la regola di + drawdown (max loss 6% STATICO), e il trader incassa una quota del profitto (0,80). + Domanda: quali voci del censimento diventano ESEGUIBILI a $100k di nozionale funded e non lo sono + a $635 di capitale proprio? E quanto vale, in Sharpe di libro e in EUR/giorno?""") + import r0724_skh_live_weight as skl + import r0725_capcurve as CC + from src.portfolio.portfolio import combine_outer, metrics + skh = skl.skh_book(0, "hourly") + if skh.index.tz is None: + skh.index = skh.index.tz_localize("UTC") + lo = pd.Timestamp("2019-03-14", tz="UTC") + tp = CC.tp01_realistic(100_000.0 * 0.6375) # libro funded $100k, non $635 + base = combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": 0.75, "SKH01": 0.25}, lo=lo) + + sub("7a) chi si aggiunge davvero (capitale PROPRIO $635 -> nozionale FUNDED $100k)") + print(f" {'voce':<26}{'C* (proprio)':>14}{'a $635?':>10}{'a $100k?':>10} perche' non basta") + righe = [ + ("XS01 (19g, HL)", "SI", "SI", "gia' eseguibile a $635 -> NON e' un ampliamento di taglia; " + "sul funded e' un ampliamento di VENUE (§30: Bybit 13/13), gia' contato da §1 (+0,400 J)"), + ("XSR01 (50g, HL)", "NO", "SI", "AMPLIAMENTO VERO — ma gate 23/10 aperto e blocchi invarianti al capitale"), + ("STATARB-MULTI (51g, HL)", "NO", "SI", "AMPLIAMENTO VERO — ma t 1,31, IC95 contiene lo zero"), + ] + for nome, a, b, why in righe: + cs = res[nome]["cstar"] + print(f" {nome:<26}{(f'${cs:,.0f}' if cs else '>200k'):>14}{a:>10}{b:>10}") + print(f" {'':<26}{why}") + print(""" + A5: l'insieme AMMISSIBILE non si amplia (entrambi hanno un blocco che il capitale non tocca). + L'insieme ESEGUIBILE si amplia di due voci, ed e' un fatto nuovo: nessun filone del progetto + aveva mai misurato che a $100k STATARB-MULTI e XSR01 girano al 99,6% del nozionale.""") + + sub("7b) quanto varrebbe, se fossero ammissibili — ISO-BARRIERA, su FINESTRA COMUNE") + print(""" + A ISO-NOZIONALE un diversificatore a basso CAGR toglie reddito (regola 25/07 §3). Su un conto + funded la lente giusta e' la BARRIERA: si cerca la leva k che porta il maxDD alla regola dei -6% + e si legge il drift a quella leva. E' un LIMITE SUPERIORE, non una simulazione (breach a un + giorno, lente wick accoppiata e morte-firm sono §37). + FINESTRA COMUNE 2024+ obbligatoria: i candidati esistono solo da li'. Con la finestra piena + `combine_outer` rinormalizza per riga, il maxDD del libro cade PRIMA del 2024 e le tre righe + danno la STESSA k -> il confronto non misura niente (errore commesso e corretto in sessione).""") + lo2 = pd.Timestamp("2024-01-01", tz="UTC") + print(f"\n {'libro (finestra comune 2024+)':<34}{'Sharpe':>8}{'maxDD':>8}{'k@-6%':>8}" + f"{'drift@k':>9}{'EUR/g $100k funded':>21}") + base_rows = {} + for lab, nome in [("LIVE 75/25 (TP01+SKH01)", None), + ("+ XSR01 @15%", "XSR01 (50g, HL)"), + ("+ STATARB-MULTI @15%", "STATARB-MULTI (51g, HL)")]: + if nome is None: + b = combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": 0.75, "SKH01": 0.25}, lo=lo2) + else: + s_ = pd.Series(res[nome]["nets"][100_000.0], index=res[nome]["cand"]["idx"]) + b = combine_outer({"TP01": tp, "SKH01": skh, "C": s_}, + {"TP01": 0.6375, "SKH01": 0.2125, "C": 0.15}, lo=lo2) + b = b[b.index >= lo2] + r = b.values + m = metrics(b) + k = k_barriera(r) + g = cagr_k(r, k, b.index) + eur = SPLIT_FUNDED * g * 100_000.0 / 365.0 / EURUSD + base_rows[lab] = eur + print(f" {lab:<34}{m['sharpe']:>8.3f}{100*m['maxdd']:>7.1f}%{k:>8.2f}{100*g:>8.1f}%" + f"{eur:>21.2f}") + d1 = base_rows["+ XSR01 @15%"] - base_rows["LIVE 75/25 (TP01+SKH01)"] + d2 = base_rows["+ STATARB-MULTI @15%"] - base_rows["LIVE 75/25 (TP01+SKH01)"] + print(f""" + L'AMPLIAMENTO VALE, come LIMITE SUPERIORE: XSR01 {d1:+.2f} EUR/giorno, STATARB-MULTI {d2:+.2f} + EUR/giorno su $100.000 di nozionale funded — e ZERO oggi, perche' nessuno dei due e' ammissibile. + ⚠️ La finestra 2024+ NON contiene un sinistro: il maxDD del libro li' e' molto piu' piccolo che + sulla storia piena, quindi k e' gonfiata per TUTTE le righe. Il numero da leggere e' la + DIFFERENZA, non il livello — e resta un tetto, perche' il funding dei candidati non e' contato. + + ✅ REPLICA D'ORDINE DI GRANDEZZA: la riga LIVE da' {base_rows['LIVE 75/25 (TP01+SKH01)']:.0f} + EUR/giorno su un conto funded da $100k, contro i "~EUR 15-30/g max" che il progetto pubblica dal + 25/07 per UN conto con cap $200k/trader. Macchineria diversa (qui una k iso-barriera, li' un MC + con breach), stesso ordine di grandezza. + 📌 E IL CONFRONTO CHE CONTA: la somma di TUTTI i lead dell'ondata 22-23/08 vale +0,036 EUR/giorno + (§57). Il solo AMPLIAMENTO DELL'INSIEME su un conto funded vale, come tetto, + {d1:+.2f} / {d2:+.2f} EUR/giorno — cioe' 40-100x. Non e' un argomento per aprire un funded: e' un + argomento per smettere di valutare il canale funded SOLO sul nozionale.""") + + +# ============================================================================================= +# SEZ. 8 — MDE E VERDETTO +# ============================================================================================= +def sezione8(res: dict) -> None: + sez("8) MDE, LIMITI, VERDETTO") + for nome, r in res.items(): + idx = r["cand"]["idx"] + yrs = (idx[-1] - idx[0]) / pd.Timedelta("365.25D") + print(f" {nome:<26} {yrs:.2f} anni -> SE(Sharpe) ~ {1/np.sqrt(yrs):.2f}, " + f"MDE(95%) ~ {1.96/np.sqrt(yrs):.2f} di Sharpe") + print(""" + Cio' che questo filone PUO' dire con questi dati: dove sta la soglia di eseguibilita' (e' una + misura deterministica sul pavimento del venue, non una stima statistica, e non ha MDE). + Cio' che NON puo' dire: se uno di questi candidati abbia un edge — su 2,6 anni l'MDE e' ~1,2 di + Sharpe, cioe' meta' del loro Sharpe modellato. Non lo dico: lo dicono i gate gia' pubblicati. + + LIMITI DICHIARATI + - I 3 candidati misurati vivono su HYPERLIQUID. Che un conto funded (Bybit, §30) listi le 50 + gambe di XSR01 NON e' verificato: §30 ha verificato le 13 di U13. E' una GET pubblica: costa + zero e va fatta prima di credere alla sez. 7. + - `sim_libro` applica il pavimento per gamba e non modella il lotto minimo per-strumento HL + (lotti $0,01-7,72 sui 19 major, sotto il pavimento $10 -> non binding). Sui 50 alt della coda + non e' verificato strumento per strumento. + - Lo slippage NON e' nel conto: HL-EXEC lo ha refutato come rischio #1 con margine 21x, ma su + UNA fotografia di 172 secondi, e SLIP-AUDIT ha scritto che la partecipazione scala lineare + -> a $100k la misura di attrito a $635 NON si estrapola. La sez. 7 va riletta a quella taglia. + - Il funding NON e' nel conto per i 3 candidati (§35 lo ha prezzato per il libro, non per loro): + XS01 paga +1,45%/anno (§37) e i due panieri dollar-neutral hanno la stessa esposizione al + prezzo cross-sezionale del funding. Ogni EUR/giorno della sez. 7 e' quindi OTTIMISTA.""") + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--no-net", action="store_true") + a = ap.parse_args() + t0 = time.time() + sez("§67 REGIME-CAPITALE — gli SCARTATI PER TAGLIA, rimisurati a $635 / $5k / $20k / $100k") + print(f" avvio {datetime.now(timezone.utc):%Y-%m-%d %H:%M:%S} UTC seed {SEED}") + sola_lettura() + print(f" parametri venue: Deribit min ${DER_MIN_ORDER:.0f} / {DER_FEE_SIDE*1e4:.2f} bps ; " + f"Hyperliquid min ${HL_MIN_ORDER:.0f} / {HL_FEE_SIDE*1e4:.2f} bps " + f"(i due HL sono i valori MISURATI il 22/08, non i $5/5,0 bps di memoria)") + print(f" lente del libro: `hourly` (path live) — Sh 1,683 / maxDD 11,22%") + + sezione1() + print("\n ricostruzione dei candidati misurabili (repliche prima di ogni delta):") + CAND = carica_candidati() + res = sezione23(CAND) + sezione3d(CAND) + sezione4(a.no_net) + sezione5(res) + sezione6() + sezione7(res) + sezione8(res) + + sez("VERDETTO") + n = len(CENSIMENTO) + n_edge = sum(1 for *_, e in CENSIMENTO if e.startswith("SI")) + nf = sum(1 for _, _, _, st, _ in CENSIMENTO if st.startswith("FALSIFICATO")) + print(f""" + `A $100.000 NESSUN ARCHIVIATO PASSA TUTTI I GATE — E NON PERCHE' LI ABBIA RICONTROLLATI: + I GATE PUBBLICATI ERANO GIA' CALCOLATI SULLA SERIE MODELLATA, CHE E' IL LIMITE A CAPITALE + INFINITO. IL MURO DI TAGLIA NON NASCONDEVA NIENTE.` + + Il censimento ha {n} voci; {n_edge} hanno ANCHE un rifiuto sull'edge. Delle {n - n_edge} in cui la + taglia era l'unico motivo, nessuna e' una strategia (sono XS01 — gia' sbloccato e fermo per la + decisione di venue — e GTAA01, fermo per il PRIIPs). + Muri di taglia caduti: {nf} su {n}, tutti nell'ondata 22-23/08, tutti nella stessa direzione. + Muri di taglia MISURATI QUI per la prima volta: STATARB-MULTI (C* $2.000-$7.500 secondo la + tolleranza) e la curva completa di XSR01 col pavimento del venue GIUSTO (C* $15.000-$20.000, + contro i ~$3.000 che HL-EXEC riportava con un criterio piu' permissivo). + + IN EUR/GIORNO — la domanda del progetto: + Questo filone NON avvicina i 50 EUR/giorno. Non aggiunge NIENTE al conto di oggi: tutto cio' + che sblocca vive a capitali che il conto non ha, e a quei capitali resta bloccato sull'edge. + Il suo contributo e' +0,000 EUR/giorno. + Cio' che TOGLIE e' reale ma non e' reddito: il gate XSR01 del 23/10 legge un monitor tarato sul + pavimento del venue sbagliato. + Per confronto, sullo stesso motore: EUR 250 -> 500 al mese porta P(20 anni) dal 14% all'85%. + + Libro, pesi, cron, config INVARIATI. Nessun ordine. + durata {time.time()-t0:.0f} s""") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823b_retry.py b/scripts/research/r0823b_retry.py new file mode 100644 index 0000000..168509c --- /dev/null +++ b/scripts/research/r0823b_retry.py @@ -0,0 +1,909 @@ +"""r0823b_retry — §61 RETRY: la POLITICA DI RI-ACQUISTO del biglietto funded (2026-08-23). + +IL BUCO CHE CHIUDE +------------------ +Un conto funded che rompe una regola muore. Il progetto ha modellato tutto il resto del ciclo +(prezzo, regole, leva, lente accoppiata, funding, morte-firm, fisco) e **mai la politica di +RI-ACQUISTO**: quando ricomprare dopo un bust, con quali soldi, e con quale leva sul conto nuovo. +Nei simulatori esistenti (`r0725_prop_ladder.simulate`, `r0725_prop_coupled.simulate`, +`r0822d_gate_c.ladder`) la politica c'e' — e' **cablata**: *compra sempre, subito, il biglietto +piu' GRANDE che la cassa permette, in tutti e 6 gli slot*. E' la politica piu' aggressiva della +famiglia, non e' mai stata dichiarata come scelta, e **tutti i numeri operativi del canale funded +escono da li'** (42% -> 7,8% -> 4,4% -> 2,6%). + +E' diventata una domanda vera solo ieri, per due misure: + (a) §30: la quota d'ingresso e' un **deposito RIMBORSABILE** ("Refundable Challenge Deposit, + returned in full alongside your first funded payout") -> un tentativo fallito costa la + quota, uno riuscito no. E' una struttura a OPZIONE, e le opzioni si comprano piu' volte. + (b) §37: il biglietto batte il libro **solo perche' il piano di versamento c'e'**, cioe' + **perche' si puo' giocare piu' di una volta** ("una lotteria a EV positivo che un conto da + $654 puo' giocare UNA VOLTA SOLA"). +Se il valore sta nel NUMERO DI TENTATIVI, la politica di ri-acquisto e' la variabile principale +e non e' mai stata ottimizzata. + +LE ATTESE, REGISTRATE PRIMA DI MISURARE (poi riportate confermate/refutate) +-------------------------------------------------------------------------- +A1. **Compromesso monotono.** IMMEDIATA massimizza sia il numero di giocate sia P(cassa a zero); + MAI li minimizza entrambi. Nessuna politica dovrebbe dominare su tutte e due le colonne. +A2. **La dose-risposta di §37 dovrebbe essere ripida.** Se il meccanismo e' "quante volte si + gioca", passare da max 1 biglietto a illimitati deve muovere P(>=50 EUR/g) di molto. Se non + lo muove, il meccanismo dichiarato da §37 e' sbagliato (o e' un'altra cosa). +A3. **Il biglietto PICCOLO ($249/$25k) compra piu' giocate e perde sul bersaglio.** 50 EUR/g netti + sono ~$59k di payout netti in 3 anni: con nozionale $25k servono troppi cicli. Mi aspetto + P(zero) piu' bassa e P(>=50/g) piu' bassa. Dominato sul bersaglio, non sulla sopravvivenza. +A4. **SOLO-PAYOUT ~= MAI senza versamenti** (senza payout non c'e' cassa per ricomprare) e + molto piu' vicina a IMMEDIATA con versamenti. +A5. **Il RIMBORSO alza i livelli di ~1-3pp (§37) ma NON cambia l'ordine delle politiche.** + Previsione forte: l'ottimo e' lo stesso nelle tre convenzioni. +A6. 🚨 **Le regole di leva condizionate all'esito passato sono NULL DEL DE-LEVERING.** In un + bootstrap a blocchi il passato non porta informazione sul futuro: una regola "riduci la leva + dopo un bust" puo' agire SOLO cambiando la leva MEDIA. Il controllo obbligatorio e' la leva + COSTANTE alla stessa media realizzata. Mi aspetto REFUTED 3/3 (8ª occorrenza). +A7. **La politica conta meno del versamento.** Mi aspetto uno spread fra politiche di pochi punti + percentuali contro i ~+10pp del passaggio EUR 0 -> EUR 500/mese misurato da §37. + +CONVENZIONI (tutte EREDITATE e congelate — nessuna scelta di strategia in questo file) +-------------------------------------------------------------------------------------- +* Lente **ACCOPPIATA** (`r0725_prop_coupled`): su una regola a UN GIORNO la close-only e' cieca. +* Conto funded = **W_A TP01 38 / SKH01 38 / XS01 25** (centro della regione robusta di §29), + XS01 = **U13 Binance** fuori campione, finestra **W-LONG 2021+**, de-luck sul drift **x0,89**. +* **Funding dentro** (§35/§37), morte-firm **10%/anno** (assunzione del progetto), cap + **$200k/trader**, fisco **33%**, split **80%**, soglia di prelievo **$100** di quota trader. +* Banda d'ancora: **23 offset x 3 fasi (0,3,7)** = lo stesso sottocampione dichiarato da §32/§37. + La cella di riferimento e' la **MEDIANA** della banda, non la canonica (91° pctl). +* Cassa non spesa: **FERMA** per default (convenzione di §37) e **SUL LIBRO** come sensibilita' + dichiarata in §7 — perche' la convenzione "ferma" penalizza per costruzione le politiche + prudenti, che sono esattamente quelle sotto esame. +* Bootstrap **CONDIVISO** fra tutte le politiche di una stessa tabella -> confronti **APPAIATI**. + +COSA NON E' QUESTO FILE: non e' una selezione di strategia (meccanismo, pesi, leva ottima, lente +e soglie sono tutti ereditati); e' la mappatura di una politica OPERATIVA che finora era cablata. +L'argmax su ~30 politiche resta pero' una selezione: si riportano il PLATEAU e la risoluzione MC. + +Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0823b_retry.py` +Rete: NESSUNA (parquet certificati + cache Binance di r0822b + cache funding di r0822d). +""" +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)) + +if "--no-net" not in sys.argv: # r0822d_funding legge USE_NET all'import + sys.argv.append("--no-net") + +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 (25/07) +import r0822_prop_alloc as PA # noqa: E402 J, bootstrap (22/08) +import r0822b_prop_recal as PR # noqa: E402 PROP-RECAL (22/08) +import r0822b_xs01_oos as X # noqa: E402 motore XS01 + cache Binance +import r0822c_skh_anchor_prop as AP # noqa: E402 offset di SKH01 (gamba b) +import r0822d_funding as FD # noqa: E402 il funding (22/08) +import r0822d_gate_c as GC # noqa: E402 ladder di riferimento (§37) +from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402 + +FD.USE_NET = False + +EURUSD = pl.EURUSD +TAX_RATE = pl.TAX_RATE +SPLIT = pl.SPLIT +DELUCK = PA.DELUCK +W_A = GC.W_A +OFFSETS = AP.OFFSETS +PHASES_SUB = AP.PHASES_SUB +MIN_PAYOUT = GC.MIN_PAYOUT_TRADER +HORIZON_DAYS = GC.HORIZON_DAYS +SEED_LAD = GC.SEED_LAD +XS_FUND_ANN = 0.0145 # §37: XS01 PAGA +1,45%/anno di funding (misurato) + +N_BAND = 1000 # percorsi per la banda d'ancora (riproduzione §32/§37) +N_MAIN = 2000 # percorsi per la griglia di politiche (cella di rif.) +N_ROB = 1000 # percorsi per la robustezza fra celle +# Sottocampione di offset dichiarato A PRIORI: gli stessi 8 uniformi di `r0726_skh_live_book` +# (la griglia piena e' 23; su 2 CPU condivise con altri 5 processi la piena non sta nel budget, +# e un sottocampione uniforme dichiarato prima e' l'unica forma onesta di ridurla). +BAND_OFFSETS = tuple(range(0, 690, 90)) + +# numeri PUBBLICATI da riprodurre prima di produrne di nuovi +PUB = dict(anchor_p50g=0.044, anchor_p10=0.025, anchor_p90=0.082, anchor_pzero=0.353, + gatec_p50g=0.026, gatec_p10=0.015, gatec_p90=0.047, gatec_pzero=0.403, + refund_pp_lo=0.01, refund_pp_hi=0.03) + +TRIALS = 0 +T0 = time.time() +CHEAPEST = min(t[1] for F in pl.FIRMS for t in F["tickets"]) # $249 + + +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) + + +def tick(lab: str) -> None: + print(f" [{time.time() - T0:6.0f}s] {lab}", flush=True) + + +# ============================================================================ LE POLITICHE +def POL(when="immediate", k_reserve=1.0, cadence=0, max_tickets=10 ** 9, size="largest", + lev_rule="const", lev_dn=1.0, lev_up=1.0, lev_lo=0.25, lev_hi=1.50, + max_open=pl.MAX_CONCURRENT) -> dict: + """Una politica di ri-acquisto. `when`: + immediate — compra appena uno slot e' libero e la cassa basta (= il CABLATO di oggi) + threshold — compra solo se la cassa e' >= k_reserve x il prezzo (tiene una riserva) + earned — il PRIMO biglietto dai propri soldi, i successivi SOLO dai payout/rimborsi + cadence — al piu' un acquisto ogni `cadence` giorni + none — non compra MAI (controllo positivo: deve dare P(>=50/g)=0) + `size`: 'largest' (cablato) o 'cheapest' (piu' giocate, nozionale minore). + `max_open`: quanti conti al MASSIMO possono essere aperti insieme (cablato: 6 = tutti gli + slot). E' l'altra faccia del ri-acquisto: giocare in SERIE invece che in PARALLELO. + `lev_rule`: 'const' | 'down_on_bust' (x lev_dn a ogni bust) | 'up_on_pass' (x lev_up a ogni + pass). La leva di un conto e' fissata all'ACQUISTO e non cambia piu'.""" + return dict(when=when, k_reserve=k_reserve, cadence=cadence, max_tickets=max_tickets, + size=size, lev_rule=lev_rule, lev_dn=lev_dn, lev_up=lev_up, + lev_lo=lev_lo, lev_hi=lev_hi, max_open=max_open) + + +P_BASE = POL() # il cablato, bit-exact vs §37 + + +# ============================================================================ IL SIMULATORE +def ladder_rt(R, G, n_paths, lev, seed, *, pol=None, refund_mode="none", deposit_eur=0.0, + start_eur=pl.START_EUR, min_payout=0.0, boot=None, block=20, deluck=DELUCK, + rb_boot=None, tax=TAX_RATE) -> dict: + """`r0822d_gate_c.ladder` generalizzato alla POLITICA DI RI-ACQUISTO. Tre aggiunte: + 1. `pol` — quando/quale biglietto/con quali soldi/a che leva (sopra); + 2. `refund_mode` — 'none' (spesa persa, `pc.simulate`) | 'pass' (`r0725_hyro:200`) | + 'payout' (la regola vera della firm); + 3. `rb_boot` — se dato, la cassa NON spesa gira sul LIBRO LIVE invece di restare + ferma, con lo STESSO sorteggio di bootstrap (stesso mercato) e lo + stesso fisco annuale di `GC.book_road`. + Con `pol=P_BASE, refund_mode='none', deposit_eur=0, min_payout=0, rb_boot=None` dev'essere + **BIT-EXACT** rispetto a `GC.ladder(..., refund=False)` e quindi a `pc.simulate`: e' §1. + + 🚨 Zero e' ASSORBENTE ed e' modellato come tale: senza conti vivi e con cassa sotto il + biglietto piu' economico ($249) non si gioca piu'; solo un versamento puo' riaprire il gioco. + """ + pol = pol or P_BASE + rng = np.random.default_rng(seed) + n_days = HORIZON_DAYS + L = len(R) + Rm = R.astype(float).reshape(-1, 1).copy() + Gm = G.astype(float).reshape(-1, 1).copy() + Rm = Rm - (1.0 - deluck) * Rm.mean(axis=0, keepdims=True) + if boot is None: + n_blocks = int(np.ceil(n_days / block)) + starts = rng.integers(0, L - block, size=(n_paths, n_blocks)) + idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days] + else: + idx = boot + n_slot = pl.MAX_CONCURRENT + # (paths, days): tutti gli slot girano lo STESSO libro -> una colonna basta. La leva + # moltiplica per-slot (serve alle regole di leva) invece che globalmente: `x*lev` e' la + # stessa moltiplicazione in virgola mobile, quindi il bit-exact regge (verificato in §1). + Rb = Rm[idx][:, :, 0] + Gb = Gm[idx][:, :, 0] + + FIRMS = pl.FIRMS + cash = np.full(n_paths, start_eur * EURUSD) + basis = cash.copy() + carry = np.zeros(n_paths) + earned = np.zeros(n_paths) # borsellino dei soli payout/rimborsi + phase = np.full((n_paths, n_slot), -1, np.int8) + eq = np.ones((n_paths, n_slot)) + firm = np.full((n_paths, n_slot), -1, np.int8) + notional = np.zeros((n_paths, n_slot)) + price_paid = np.zeros((n_paths, n_slot)) + refunded = np.zeros((n_paths, n_slot), bool) + lev_slot = np.full((n_paths, n_slot), lev) + # soglie della firm materializzate PER SLOT all'acquisto: la dinamica diventa una sola + # espressione vettoriale invece di un loop sulle firm. Le maschere per firm sono disgiunte, + # quindi il risultato e' identico (verificato bit-exact in §1). + TH_TGT = np.zeros((n_paths, n_slot)) + TH_DD = np.ones((n_paths, n_slot)) + TH_DL = np.ones((n_paths, n_slot)) + TH_ML = np.ones((n_paths, n_slot)) + TH_DLF = np.ones((n_paths, n_slot)) + mult = np.ones(n_paths) + firm_alive = np.ones((n_paths, len(FIRMS)), bool) + cum_payout = np.zeros(n_paths) + spent = np.zeros(n_paths) + got_back = np.zeros(n_paths) + deposited = np.zeros(n_paths) + n_payouts = np.zeros(n_paths, int) + n_tickets = np.zeros(n_paths, int) + lev_sum = np.zeros(n_paths) + last_buy = np.full(n_paths, -10 ** 9) + days_out = np.zeros(n_paths, int) + ever_passed = np.zeros(n_paths, bool) + p_death_day = 1.0 - (1.0 - pl.FIRM_DEATH_PER_YEAR) ** (1 / 365.0) + tickets_order = {} + for fi, F in enumerate(FIRMS): + tickets_order[fi] = sorted(F["tickets"], key=(lambda x: x[0]) if pol["size"] == "cheapest" + else (lambda x: -x[0])) + + for t in range(n_days): + died = rng.random((n_paths, len(FIRMS))) < p_death_day + newly_dead = died & firm_alive + firm_alive &= ~died + if newly_dead.any(): + for fi in range(len(FIRMS)): + kill = newly_dead[:, fi] + if kill.any(): + hit = (firm == fi) & kill[:, None] + phase[hit] = -1 + notional[hit] = 0.0 + + # ---------------------------------------------------------------- acquisti + if pol["when"] == "none": + budget = np.zeros(n_paths, int) + elif pol["when"] == "cadence": + budget = ((t - last_buy) >= pol["cadence"]).astype(int) + else: + budget = np.full(n_paths, n_slot, int) + budget = np.minimum(budget, np.maximum(pol["max_tickets"] - n_tickets, 0)) + if pol["max_open"] < n_slot: + budget = np.minimum(budget, np.maximum(pol["max_open"] - (phase >= 0).sum(axis=1), 0)) + if budget.any() and (cash >= CHEAPEST).any(): + for k in range(n_slot): + free = (phase[:, k] == -1) & (budget > 0) + if not free.any(): + continue + for fi, F in enumerate(FIRMS): + used = np.where(firm == fi, notional, 0.0).sum(axis=1) + for (size, price) in tickets_order[fi]: + buy = (free & firm_alive[:, fi] & (cash >= price) + & (used + size <= F["cap"])) + if pol["when"] == "threshold": + buy &= cash >= pol["k_reserve"] * price + elif pol["when"] == "earned": + buy &= (n_tickets == 0) | (earned >= price) + if not buy.any(): + continue + cash[buy] -= price + basis[buy] -= price + if pol["when"] == "earned": + take = buy & (n_tickets > 0) + earned[take] -= price + spent[buy] += price + n_tickets[buy] += 1 + budget[buy] -= 1 + last_buy[buy] = t + phase[buy, k] = 0 + eq[buy, k] = 1.0 + firm[buy, k] = fi + notional[buy, k] = size + price_paid[buy, k] = price + refunded[buy, k] = False + lv = np.clip(lev * mult, pol["lev_lo"], pol["lev_hi"]) + lev_slot[buy, k] = lv[buy] + TH_TGT[buy, k], TH_DD[buy, k], TH_DL[buy, k] = F["ev"] + TH_ML[buy, k], TH_DLF[buy, k] = F["fu"] + lev_sum[buy] += lv[buy] + free = free & ~buy + if not free.any(): + break + if not free.any(): + break + + # ---------------------------------------------------------------- dinamica dei conti + r = Rb[:, t][:, None] * lev_slot + g = Gb[:, t][:, None] * lev_slot + act = phase >= 0 + eq_prev = eq + eq = np.where(act, eq * (1.0 + r), eq) + eq_low = np.where(act, eq_prev * (1.0 + r + g), eq) + r_low = r + g + + in_ev = act & (phase == 0) + in_fu = act & (phase == 1) + bust = ((in_ev & ((eq_low < 1.0 - TH_DD) | (r_low < -TH_DL))) + | (in_fu & ((eq_low < 1.0 - TH_ML) | (r_low < -TH_DLF)))) + phase[bust] = -1 + notional[bust] = 0.0 + passed = in_ev & (eq >= 1.0 + TH_TGT) & ~bust + phase[passed] = 1 + eq[passed] = 1.0 + if pol["lev_rule"] == "down_on_bust": + mult *= pol["lev_dn"] ** bust.sum(axis=1) + elif pol["lev_rule"] == "up_on_pass": + mult *= pol["lev_up"] ** passed.sum(axis=1) + if refund_mode == "pass" and passed.any(): + back = passed & ~refunded + amt = np.where(back, price_paid, 0.0).sum(axis=1) + cash += amt + basis += amt + earned += amt + got_back += amt + refunded |= back + ever_passed |= passed.any(axis=1) + + # ---------------------------------------------------------------- la cassa propria + if rb_boot is not None: + cash = np.maximum(cash * (1.0 + rb_boot[:, t]), 0.0) + + # ---------------------------------------------------------------- mensile + if (t + 1) % 30 == 0: + fu = phase == 1 + pay = fu & (eq > 1.0) & (((eq - 1.0) * notional * SPLIT) >= min_payout) + gain = np.where(pay, (eq - 1.0) * notional, 0.0) + net = gain.sum(axis=1) * SPLIT * (1.0 - TAX_RATE) + cum_payout += net + cash += net + basis += net + earned += net + n_payouts += (pay.sum(axis=1) > 0).astype(int) + eq = np.where(pay, 1.0, eq) + if refund_mode == "payout": + back = pay & ~refunded + amt = np.where(back, price_paid, 0.0).sum(axis=1) + cash += amt + basis += amt + earned += amt + got_back += amt + refunded |= back + if deposit_eur: + cash += deposit_eur * EURUSD + basis += deposit_eur * EURUSD + deposited += deposit_eur * EURUSD + # fisco annuale sulla sola cassa propria (i payout sono gia' netti) + if rb_boot is not None and (t + 1) % 365 == 0: + gtax = (cash - basis) + carry + due = np.where(gtax > 0, tax * gtax, 0.0) + carry = np.where(gtax > 0, 0.0, gtax) + cash = cash - due + basis = cash.copy() + + out = (~(phase >= 0).any(axis=1)) & (cash < CHEAPEST) + days_out += out + + eur_day = cum_payout / EURUSD / (n_days / 365.0) / 365.0 + stuck = (~(phase >= 0).any(axis=1)) & (cash < CHEAPEST) + return dict(eur_day=eur_day, med=float(np.median(eur_day)), + p10=float(np.percentile(eur_day, 10)), p90=float(np.percentile(eur_day, 90)), + mean_eur=float(eur_day.mean()), + p_ge10=float((eur_day >= 10.0).mean()), + p_target=float((eur_day >= pl.TARGET_EUR_DAY).mean()), + p_zero=float((cum_payout <= 0).mean()), + p_payout=float((n_payouts > 0).mean()), + p_pass=float(ever_passed.mean()), + p_stuck=float(stuck.mean()), + n_tix=n_tickets, med_tix=float(np.median(n_tickets)), + mean_tix=float(n_tickets.mean()), + lev_mean=float(lev_sum.sum() / max(n_tickets.sum(), 1)), + days_out=float(np.median(days_out)), + cash=cash, med_cash=float(np.median(cash)), mean_cash=float(cash.mean()), + p10_cash=float(np.percentile(cash, 10)), + p90_cash=float(np.percentile(cash, 90)), + med_spent=float(np.median(spent)), mean_back=float(got_back.mean()), + contributed=float(start_eur * EURUSD + deposited.mean())) + + +def se_pair(a: np.ndarray, b: np.ndarray, thr: float = pl.TARGET_EUR_DAY) -> float: + """SE della DIFFERENZA fra due proporzioni APPAIATE (stessi sorteggi): McNemar. + Con bootstrap condiviso la parte comune si cancella e la risoluzione e' molto migliore + di quella dei livelli — ma non e' zero, e va stampata.""" + A = a >= thr + B = b >= thr + n01 = float((A & ~B).sum()) + n10 = float((~A & B).sum()) + n = len(a) + return float(np.sqrt(max(n01 + n10 - (n01 - n10) ** 2 / n, 0.0)) / n) + + +# ==================================================================================== main +def main() -> None: + try: + sys.stdout.reconfigure(line_buffering=True) + except Exception: # pragma: no cover + pass + print("=" * 104) + print(" §61 RETRY — la POLITICA DI RI-ACQUISTO del biglietto funded, mappata invece che") + print(" cablata. Quando ricomprare, con quali soldi, a che leva — e quanto conta.") + print("=" * 104) + print(" ATTESE REGISTRATE PRIMA: A1 compromesso monotono giocate/rovina; A2 dose-risposta") + print(" RIPIDA se il meccanismo di §37 e' 'il numero di volte che si gioca'; A3 biglietto") + print(" piccolo = piu' giocate, meno bersaglio; A4 SOLO-PAYOUT ~= MAI senza versamenti;") + print(" A5 il rimborso NON cambia l'ottimo; A6 le regole di leva sono null del de-levering") + print(" (REFUTED 3/3 atteso); A7 la politica conta meno del versamento.") + + # ============================================================== §0 pannello + hr("§0. IL PANNELLO — le stesse serie di §29/§37 (U13 Binance, W-LONG 2021+, funding dentro)") + rate = GC.funding_rate() + if rate is None: + print(" !! cache funding assente: il filone si ferma. Un numero senza funding NON e'") + print(" confrontabile con §37, e confrontarlo sarebbe l'errore che §35 ha appena") + print(" pagato. 'non misurabile' e' un risultato.") + return + Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.bin_loader, 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_SUB} + xs0 = XSP[PHASES_SUB[0]] + base = pc.crypto_daily_tuples(1.0, 0.0).index.intersection(xs0.index) + first13 = xs0.index[np.flatnonzero(xs0["R"].values != 0.0)[0]] + I = base[base >= max(first13, pd.Timestamp("2021-01-01", tz="UTC"))] + pc.DELUCK = DELUCK + print(f" W-LONG: {len(I)} giorni {I[0].date()} -> {I[-1].date()} | de-luck x{DELUCK} | " + f"cassa iniziale EUR {pl.START_EUR:.0f} = ${pl.START_EUR*EURUSD:,.0f}") + print(f" orizzonte {HORIZON_DAYS} giorni ({HORIZON_DAYS/30:.0f} versamenti da 30 giorni: " + f"§57 avverte che 'EUR X/mese' qui vuol dire ogni 30 giorni — su 36 mesi torna esatto)") + print(f" biglietti disponibili: " + " | ".join( + f"{F['name']} " + ", ".join(f"${s/1000:.0f}k@${p:.0f}" for s, p in F['tickets']) + for F in pl.FIRMS)) + print(f" biglietto piu' economico ${CHEAPEST:.0f} -> sotto questa cassa e senza conti vivi") + print(f" il percorso e' FUORI GIOCO (zero assorbente).") + tick("§0") + + # ============================================================== §1 controlli + hr("§1. CONTROLLI — bit-exact contro il simulatore pubblicato, e il controllo POSITIVO") + R0, G0 = AP.rg_off(W_A, I, XSP[0], 0) + f0 = GC.fvec_for(W_A, I, rate, 0, XS_FUND_ANN) + Rc0 = R0 - f0 + print(" §1.1 la mia `ladder_rt` col cablato (immediate/largest/const, nessun rimborso,") + print(" nessun versamento, soglia 0, cassa ferma) contro `GC.ladder` di §37:") + a = GC.ladder(Rc0, G0, N_BAND, 0.75, SEED_LAD, refund=False) + b = ladder_rt(Rc0, G0, N_BAND, 0.75, SEED_LAD, pol=P_BASE) + d1 = float(np.abs(a["eur_day"] - b["eur_day"]).max()) + print(f" max|diff| percorso per percorso = {d1:.3e} -> " + f"{'IDENTICO' if d1 < 1e-15 else 'DIVERGE — STOP'}") + if d1 >= 1e-15: + print(" Senza questo controllo i numeri sotto sarebbero di un'altra macchina.") + return + bump(2) + print("\n §1.2 e `GC.ladder` e' a sua volta bit-exact contro `pc.simulate` (§37 §4.1);") + print(" la catena e' quindi: ladder_rt == GC.ladder == pc.simulate.") + cfg = tuple(W_A.get(n, 0.0) for n in PA.CRYPTO_PROP) + key = (cfg[0], cfg[1], cfg[2], "shared") + saved = pc._TUP_CACHE.pop(key, None) + pc._TUP_CACHE[key] = (Rc0, G0, I) + ref = pc.simulate([cfg], [0] * pl.MAX_CONCURRENT, n_paths=N_BAND, lev=0.75, + seed=SEED_LAD, lens="coupled", start=str(I[0].date())) + pc._TUP_CACHE.pop(key, None) + if saved is not None: + pc._TUP_CACHE[key] = saved + d2 = float(np.abs(ref["eur_day"] - b["eur_day"]).max()) + print(f" max|diff| ladder_rt vs pc.simulate = {d2:.3e} -> " + f"{'IDENTICO' if d2 < 1e-15 else 'DIVERGE'}") + bump(1) + + print("\n §1.3 CONTROLLO POSITIVO del selettore di politica — un interruttore che non") + print(" cambia niente e' indistinguibile da un interruttore rotto.") + rngb = np.random.default_rng(51224) + nb = int(np.ceil(HORIZON_DAYS / 20)) + st = rngb.integers(0, len(I) - 20, size=(N_MAIN, nb)) + BOOT = (st[:, :, None] + np.arange(20)[None, None, :]).reshape(N_MAIN, -1)[:, :HORIZON_DAYS] + rb_full = FD.book_75_25(rate, alloc=654.0) + if rb_full.index.tz is None: + rb_full.index = rb_full.index.tz_localize("UTC") + rb = np.nan_to_num(rb_full.reindex(I).values.astype(float)) + rb_dl = rb - (1.0 - DELUCK) * rb.mean() + RB_BOOT = rb_dl[BOOT] + none_pol = ladder_rt(Rc0, G0, N_MAIN, 0.75, SEED_LAD, pol=POL(when="none"), boot=BOOT) + bump(1) + print(f" politica 'MAI COMPRARE': biglietti mediani {none_pol['med_tix']:.0f}, " + f"P(>=50/g) {none_pol['p_target']:.1%}, P(zero payout) {none_pol['p_zero']:.1%} -> " + f"{'OK' if none_pol['p_target'] == 0 and none_pol['p_zero'] == 1 else 'ROTTO'}") + print("\n §1.4 CONTROLLO POSITIVO della cassa-sul-libro: 'MAI COMPRARE' + cassa sul libro") + print(" dev'essere la strada LIBRO di §37 (`GC.book_road`), stessi sorteggi.") + nb_book = ladder_rt(Rc0, G0, N_MAIN, 0.75, SEED_LAD, pol=POL(when="none"), boot=BOOT, + rb_boot=RB_BOOT) + road = GC.book_road(rb, BOOT, pl.START_EUR, 0.0) + d3 = float(np.abs(nb_book["cash"] - road["cash"]).max()) + bump(2) + print(f" max|diff| sulla cassa finale = {d3:.3e} -> " + f"{'IDENTICO' if d3 < 1e-9 else 'DIVERGE'} | mediana ${nb_book['med_cash']:,.0f} " + f"(GC.book_road ${road['med_cash']:,.0f})") + tick("§1") + + # ============================================================== §2 banda d'ancora + hr("§2. LA BANDA D'ANCORA — riproduzione di §32/§37 e scelta della cella MEDIANA") + print(f" {len(BAND_OFFSETS)} offset (sottocampione uniforme a priori di r0726, la griglia") + print(f" piena e' 23) x {len(PHASES_SUB)} fasi {PHASES_SUB} = " + f"{len(BAND_OFFSETS)*len(PHASES_SUB)} celle, funding DENTRO, politica CABLATA (quella") + print(f" con cui i numeri sono stati pubblicati), {N_BAND} percorsi per cella.") + ev_b = PA.boot_idx(len(I), GC.EV_H, PA.N_PATHS, seed=GC.SEED_EV) + fu_b = PA.boot_idx(len(I), GC.FU_H, PA.N_PATHS, seed=GC.SEED_FU) + cells = [] + for off in BAND_OFFSETS: + fA = GC.fvec_for(W_A, I, rate, off, XS_FUND_ANN) + for ph in PHASES_SUB: + Rx, Gx = AP.rg_off(W_A, I, XSP[ph], off) + Rf = Rx - fA + j, lv, _ = GC.best_lev(Rf, Gx, ev_b, fu_b) + lad = ladder_rt(Rf, Gx, N_BAND, lv, SEED_LAD, pol=P_BASE) + cells.append(dict(off=off, ph=ph, J=j, lev=lv, p50=lad["p_target"], + pz=lad["p_zero"], med=lad["med"])) + bump(1) + C = pd.DataFrame(cells) + print(f"\n P(>=50 EUR/g) mediana {C['p50'].median():.1%} (PUB §37 {PUB['gatec_p50g']:.1%}) | " + f"banda [{np.percentile(C['p50'],10):.1%}, {np.percentile(C['p50'],90):.1%}] " + f"(PUB [{PUB['gatec_p10']:.1%}, {PUB['gatec_p90']:.1%}]) | " + f"P(zero) {C['pz'].median():.1%} (PUB {PUB['gatec_pzero']:.1%})") + can = C[(C["off"] == 0) & (C["ph"] == 0)].iloc[0] + pct_can = float((C["p50"] <= can["p50"]).mean()) + print(f" cella CANONICA (off 0, fase 0): P(>=50/g) {can['p50']:.1%} = {pct_can:.0%}° pctl " + f"della banda -> il gia' noto: la canonica e' la cella FORTUNATA, non la stima.") + k_med = int((C["p50"] - C["p50"].median()).abs().idxmin()) + OFF_M, PH_M, LEV_M = int(C.loc[k_med, "off"]), int(C.loc[k_med, "ph"]), float(C.loc[k_med, "lev"]) + print(f" CELLA DI RIFERIMENTO (mediana della banda): offset {OFF_M} min, fase {PH_M}, " + f"leva {LEV_M:.2f}x, P(>=50/g) {C.loc[k_med,'p50']:.1%}") + fM = GC.fvec_for(W_A, I, rate, OFF_M, XS_FUND_ANN) + RxM, G_M = AP.rg_off(W_A, I, XSP[PH_M], OFF_M) + R_M = RxM - fM + tick("§2") + + # ============================================================== §3 dose-risposta + hr("§3. QUANTE VOLTE SI GIOCA — la dose-risposta che §37 dichiara come IL meccanismo") + print(" Politica IMMEDIATA (il cablato) con un tetto DURO al numero di biglietti mai") + print(" comprati. E' l'esperimento pulito: cambia SOLO quante volte si puo' giocare.") + print(f" Cella di riferimento, {N_MAIN} percorsi, bootstrap condiviso, deposito rimborsato") + print(" al primo payout (la regola vera), soglia di prelievo $100, nessun versamento.") + print(f"\n {'tetto':>8} {'giocate med':>12} {'giocate medie':>14} {'P(>=50/g)':>10} " + f"{'P(>=10/g)':>10} {'P(zero payout)':>15} {'P(fuori gioco)':>15} {'EUR/g medio':>12}") + dose = {} + for mt in (1, 2, 3, 5, 8, 10 ** 9): + r = ladder_rt(R_M, G_M, N_MAIN, LEV_M, SEED_LAD, pol=POL(max_tickets=mt), + refund_mode="payout", min_payout=MIN_PAYOUT, boot=BOOT) + dose[mt] = r + bump(1) + lab = "illim." if mt > 100 else str(mt) + print(f" {lab:>8} {r['med_tix']:>12.0f} {r['mean_tix']:>14.2f} {r['p_target']:>10.1%} " + f"{r['p_ge10']:>10.1%} {r['p_zero']:>15.1%} {r['p_stuck']:>15.1%} " + f"{r['mean_eur']:>12.2f}") + d1_, dinf = dose[1], dose[10 ** 9] + se = se_pair(dinf["eur_day"], d1_["eur_day"]) + print(f"\n DA 1 GIOCATA A ILLIMITATE: P(>=50/g) {d1_['p_target']:.1%} -> " + f"{dinf['p_target']:.1%} ({(dinf['p_target']-d1_['p_target'])*100:+.2f}pp, " + f"SE appaiata {se*100:.2f}pp) | giocate mediane {d1_['med_tix']:.0f} -> " + f"{dinf['med_tix']:.0f} | P(zero) {d1_['p_zero']:.1%} -> {dinf['p_zero']:.1%}") + ripida = (dinf["p_target"] - d1_["p_target"]) > 3 * se + print(f" >>> ATTESA A2 ('dose-risposta RIPIDA se il meccanismo e' il numero di giocate'): " + f"{'CONFERMATA' if ripida else 'REFUTATA'}") + tick("§3") + + # ============================================================== §4 le politiche + hr("§4. LE POLITICHE DI RI-ACQUISTO — 7 regole di TEMPO x 2 di TAGLIA, senza versamenti") + print(" Le due colonne che vanno SEMPRE lette insieme: 'giocate' e 'P(fuori gioco)'. Una") + print(" politica aggressiva aumenta entrambe, ed e' esattamente la trappola dichiarata.") + print("\n Prima di misurare, l'ARITMETICA del listino — prezzo per $1.000 di nozionale:") + for F in pl.FIRMS: + print(" " + f"{F['name']:>5} " + " ".join( + f"${s_/1000:.0f}k: ${p_/(s_/1000):.2f}/k" for s_, p_ in F["tickets"])) + print(" -> il biglietto GRANDE costa MENO per unita' di nozionale (HYRO $5,79/k contro") + print(" $9,96/k). Piu' giocate piccole NON sono piu' esposizione: sono meno.") + POLS = [ + ("IMMEDIATA (il cablato)", POL()), + ("SOGLIA 2x il prezzo", POL(when="threshold", k_reserve=2.0)), + ("SOGLIA 3x il prezzo", POL(when="threshold", k_reserve=3.0)), + ("SOLO DAI PAYOUT", POL(when="earned")), + ("CADENZA 90 giorni", POL(when="cadence", cadence=90)), + ("CADENZA 180 giorni", POL(when="cadence", cadence=180)), + ("UN CONTO ALLA VOLTA", POL(max_open=1)), + ("DUE CONTI ALLA VOLTA", POL(max_open=2)), + ("MAI (1 biglietto)", POL(max_tickets=1)), + ] + res4 = {} + for size in ("largest", "cheapest"): + lab_s = "biglietto PIU' GRANDE ($100k)" if size == "largest" else "biglietto PIU' ECONOMICO ($25k)" + print(f"\n --- {lab_s} ---") + print(f" {'politica':>26} {'giocate med':>12} {'P(>=50/g)':>10} {'P(>=10/g)':>10} " + f"{'P(zero)':>9} {'P(fuori gioco)':>15} {'EUR/g med':>10} {'cassa fin. med':>15}") + for name, p in POLS: + pp = dict(p) + pp["size"] = size + r = ladder_rt(R_M, G_M, N_MAIN, LEV_M, SEED_LAD, pol=pp, refund_mode="payout", + min_payout=MIN_PAYOUT, boot=BOOT) + res4[(size, name)] = r + bump(1) + print(f" {name:>26} {r['med_tix']:>12.0f} {r['p_target']:>10.1%} " + f"{r['p_ge10']:>10.1%} {r['p_zero']:>9.1%} {r['p_stuck']:>15.1%} " + f"{r['med']:>10.2f} ${r['med_cash']:>14,.0f}") + base_r = res4[("largest", "IMMEDIATA (il cablato)")] + print(f"\n DIFFERENZE APPAIATE contro il cablato (stessi sorteggi), su P(>=50 EUR/g):") + print(f" {'politica':>26} {'dP(>=50/g)':>12} {'SE appaiata':>13} {'dP(fuori gioco)':>17} " + f"{'d giocate med':>14}") + rows4 = [] + for size in ("largest", "cheapest"): + for name, _ in POLS: + r = res4[(size, name)] + if (size, name) == ("largest", "IMMEDIATA (il cablato)"): + continue + s = se_pair(r["eur_day"], base_r["eur_day"]) + tag = name if size == "largest" else "[pic] " + name + rows4.append(dict(tag=tag, d=r["p_target"] - base_r["p_target"], se=s, + dz=r["p_stuck"] - base_r["p_stuck"], + dt=r["med_tix"] - base_r["med_tix"], p50=r["p_target"], + pstuck=r["p_stuck"])) + print(f" {tag:>26} {(r['p_target']-base_r['p_target'])*100:>11.2f}pp " + f"{s*100:>12.2f}pp {(r['p_stuck']-base_r['p_stuck'])*100:>16.2f}pp " + f"{r['med_tix']-base_r['med_tix']:>14.0f}") + R4 = pd.DataFrame(rows4) + sig = R4[R4["d"].abs() > 2 * R4["se"]] + print(f"\n Politiche che si distinguono dal cablato oltre 2 SE appaiate: " + f"{len(sig)}/{len(R4)}" + + (f" (fra cui {', '.join(list(sig['tag'].str.strip())[:4])}...)" if len(sig) else "")) + allp = pd.concat([R4[["tag", "p50", "pstuck"]], + pd.DataFrame([dict(tag="IMMEDIATA (il cablato)", p50=base_r["p_target"], + pstuck=base_r["p_stuck"])])], ignore_index=True) + print(f" Spread fra la politica migliore e la peggiore su P(>=50/g): " + f"{(allp['p50'].max()-allp['p50'].min())*100:.2f}pp " + f"(max {allp.loc[allp['p50'].idxmax(),'tag'].strip()} {allp['p50'].max():.1%}, " + f"min {allp.loc[allp['p50'].idxmin(),'tag'].strip()} {allp['p50'].min():.1%})") + s3 = res4[("largest", "SOGLIA 3x il prezzo")] + print(f"\n ⚠ A $654 la SOGLIA 3x e' degenere: il biglietto piu' economico costa ${CHEAPEST:.0f}") + print(f" e 3x sono ${3*CHEAPEST:.0f} > ${pl.START_EUR*EURUSD:,.0f} -> giocate mediane " + f"{s3['med_tix']:.0f}. Non e' una politica prudente, e' 'non giocare' scritta") + print(f" con un'altra sintassi. La soglia in VALUTA ASSOLUTA dipende dal capitale:") + print(f" stessa lezione della banda di GTAA01 (27/07), su un altro parametro.") + pic = res4[("cheapest", "IMMEDIATA (il cablato)")] + a3a = pic["p_target"] < base_r["p_target"] + a3b = pic["p_stuck"] < base_r["p_stuck"] + print(f"\n >>> ATTESA A3: il biglietto piccolo perde sul bersaglio " + f"({pic['p_target']:.1%} contro {base_r['p_target']:.1%}) -> " + f"{'confermata' if a3a else 'REFUTATA'}; e sulla ROVINA " + f"({pic['p_stuck']:.1%} contro {base_r['p_stuck']:.1%}) -> " + f"{'confermata' if a3b else 'REFUTATA'}. Verdetto A3: " + f"{'CONFERMATA' if (a3a and a3b) else 'PARZIALE/REFUTATA'}") + tick("§4") + + # ============================================================== §5 la leva + hr("§5. LA LEVA DEL CONTO NUOVO — e il NULL DEL DE-LEVERING, dichiarato PRIMA") + print(" 🚨 In un bootstrap a blocchi il passato NON porta informazione sul futuro: una") + print(" regola 'riduci la leva dopo un bust' puo' agire SOLO cambiando la leva MEDIA. Il") + print(" controllo obbligatorio e' la leva COSTANTE alla stessa media realizzata. Se la") + print(" regola non la batte, e' de-levering travestito (sarebbe l'8ª occorrenza).") + lev_rules = [ + ("costante (il cablato)", POL()), + ("RIDOTTA x0,80 a ogni bust", POL(lev_rule="down_on_bust", lev_dn=0.80)), + ("RIDOTTA x0,60 a ogni bust", POL(lev_rule="down_on_bust", lev_dn=0.60)), + ("CRESCENTE x1,25 a ogni pass", POL(lev_rule="up_on_pass", lev_up=1.25)), + ] + print(f"\n {'regola di leva':>30} {'leva media':>11} {'P(>=50/g)':>10} {'P(zero)':>9} " + f"{'P(fuori gioco)':>15} {'EUR/g medio':>12}") + lev_res = {} + for name, p in lev_rules: + r = ladder_rt(R_M, G_M, N_MAIN, LEV_M, SEED_LAD, pol=p, refund_mode="payout", + min_payout=MIN_PAYOUT, boot=BOOT) + lev_res[name] = r + bump(1) + print(f" {name:>30} {r['lev_mean']:>11.3f} {r['p_target']:>10.1%} {r['p_zero']:>9.1%} " + f"{r['p_stuck']:>15.1%} {r['mean_eur']:>12.2f}") + print(f"\n IL NULL: leva COSTANTE alla stessa media realizzata di ciascuna regola") + print(f" {'regola':>30} {'leva cost.':>11} {'P(>=50/g) null':>15} " + f"{'P(>=50/g) regola':>17} {'delta':>9} {'SE':>8} {'P(fuori) null':>14} {'verdetto':>9}") + n_ref = 0 + for name, _ in lev_rules[1:]: + r = lev_res[name] + lv_eq = r["lev_mean"] + nul = ladder_rt(R_M, G_M, N_MAIN, lv_eq, SEED_LAD, pol=POL(), refund_mode="payout", + min_payout=MIN_PAYOUT, boot=BOOT) + bump(1) + d = r["p_target"] - nul["p_target"] + s = se_pair(r["eur_day"], nul["eur_day"]) + ok = d > 2 * s + n_ref += 0 if ok else 1 + print(f" {name:>30} {lv_eq:>11.3f} {nul['p_target']:>15.1%} {r['p_target']:>17.1%} " + f"{d*100:>8.2f}pp {s*100:>7.2f}pp {nul['p_stuck']:>14.1%} " + f"{'BATTE' if ok else 'REFUTED':>9}") + print(f"\n >>> ATTESA A6 ('null del de-levering, REFUTED 3/3'): " + f"{'CONFERMATA' if n_ref == 3 else f'PARZIALE ({n_ref}/3 refutate)'}") + print(" Lettura: la leva del conto nuovo non e' una politica, e' un LIVELLO — e il livello") + print(" ottimo e' gia' cercato simmetricamente da `best_lev` in ogni cella (§29).") + print("\n §5.1 DIAGNOSTICO (non una proposta): il LIVELLO di leva, costante, spazzato sulla") + print(" stessa griglia LEVS di §29. `best_lev` massimizza J = P(pass) x P(vivo), NON") + print(" P(>=50 EUR/g): §29 lo dichiara ('J e P(>=50/g) hanno argmax diversi'). Sceglierla") + print(" qui sul bersaglio sarebbe selezione sulla metrica d'uscita — si mostra il PREZZO.") + print(f"\n {'leva':>8} {'P(>=50/g)':>10} {'P(>=10/g)':>10} {'P(zero payout)':>15} " + f"{'P(fuori gioco)':>15} {'EUR/g medio':>12} {'EUR/g mediano':>14}") + for lv in PR.LEVS: + r = ladder_rt(R_M, G_M, N_MAIN, lv, SEED_LAD, pol=POL(), refund_mode="payout", + min_payout=MIN_PAYOUT, boot=BOOT) + bump(1) + star = " <- best_lev (J)" if abs(lv - LEV_M) < 1e-9 else "" + print(f" {lv:>8.3f} {r['p_target']:>10.1%} {r['p_ge10']:>10.1%} {r['p_zero']:>15.1%} " + f"{r['p_stuck']:>15.1%} {r['mean_eur']:>12.2f} {r['med']:>14.2f}{star}") + tick("§5") + + # ============================================================== §6 le tre convenzioni + hr("§6. LE TRE CONVENZIONI SUL DEPOSITO — cambia l'OTTIMO, o solo il livello?") + print(" (i) SPESA PERSA `pc.simulate` — da cui esce il numero operativo del progetto;") + print(" (ii) RIMBORSO AL PASS `r0725_hyro:200` — l'altro modello, contraddittorio;") + print(" (iii) RIMBORSO AL PRIMO PAYOUT — la regola dichiarata dalla firm (§30).") + sel6 = ["IMMEDIATA (il cablato)", "SOGLIA 2x il prezzo", "SOLO DAI PAYOUT", + "CADENZA 180 giorni", "MAI (1 biglietto)"] + conv = [("(i) spesa persa", "none"), ("(ii) al pass", "pass"), ("(iii) al primo payout", "payout")] + tab6 = {} + print(f"\n {'politica':>26} " + " ".join(f"{c[0]:>22}" for c in conv)) + for name in sel6: + p = dict([x for x in POLS if x[0] == name][0][1]) + line = f" {name:>26} " + for lab, rm in conv: + r = ladder_rt(R_M, G_M, N_MAIN, LEV_M, SEED_LAD, pol=p, refund_mode=rm, + min_payout=MIN_PAYOUT, boot=BOOT) + tab6[(name, rm)] = r + bump(1) + line += f"{r['p_target']:>10.1%}/{r['p_stuck']:>9.1%} " + print(line + " <- P(>=50/g) / P(fuori gioco)") + print("\n ARGMAX per convenzione (su P(>=50 EUR/g)):") + same = True + arg0 = None + for lab, rm in conv: + vals = {n: tab6[(n, rm)]["p_target"] for n in sel6} + arg = max(vals, key=vals.get) + if arg0 is None: + arg0 = arg + same &= (arg == arg0) + print(f" {lab:>22}: {arg:>26} ({vals[arg]:.1%})") + dpp = np.median([tab6[(n, "payout")]["p_target"] - tab6[(n, "none")]["p_target"] for n in sel6]) + print(f"\n Valore del rimborso (mediana su {len(sel6)} politiche, da 'spesa persa' a") + print(f" 'rimborso al primo payout'): {dpp*100:+.2f}pp su P(>=50/g) " + f"(PUB §37: +1-3pp atteso, misurato {'dentro' if PUB['refund_pp_lo'] <= dpp <= PUB['refund_pp_hi'] else 'FUORI'} banda)") + print(f" Quota media restituita: ${tab6[(sel6[0],'payout')]['mean_back']:,.0f} per percorso") + print(f" >>> ATTESA A5 ('il rimborso NON cambia l'ottimo'): " + f"{'CONFERMATA' if same else 'REFUTATA'}") + tick("§6") + + # ============================================================== §7 con i versamenti + hr("§7. CON I VERSAMENTI — dove la politica di ri-acquisto smette di essere gratis") + print(" Con un versamento mensile lo ZERO SMETTE DI ESSERE ASSORBENTE (la cassa si") + print(" riempie di nuovo): P(fuori gioco) va a zero per TUTTE le politiche, e il costo") + print(" della politica aggressiva non si legge piu' li' ma sulla RICCHEZZA TERMINALE.") + print(" Convenzione principale: cassa non spesa SUL LIBRO (la piu' equa verso le politiche") + print(" prudenti); la convenzione di §37 (cassa FERMA) e' riportata come scarto.") + dep_rows = [] + for dep in (0.0, 250.0, 500.0): + print(f"\n --- versamento EUR {dep:,.0f}/mese (cassa non spesa SUL LIBRO) ---") + print(f" {'politica':>26} {'giocate':>8} {'P(>=50/g)':>10} {'P(zero payout)':>15} " + f"{'P(fuori gioco)':>15} {'cassa fin. med':>15} {'P(12} " + f"{'speso/entrato':>14}") + for name in sel6: + p_ = dict([x for x in POLS if x[0] == name][0][1]) + a_ = ladder_rt(R_M, G_M, N_MAIN, LEV_M, SEED_LAD, pol=p_, refund_mode="payout", + min_payout=MIN_PAYOUT, boot=BOOT, deposit_eur=dep) + b_ = ladder_rt(R_M, G_M, N_MAIN, LEV_M, SEED_LAD, pol=p_, refund_mode="payout", + min_payout=MIN_PAYOUT, boot=BOOT, deposit_eur=dep, rb_boot=RB_BOOT) + bump(2) + ploss = float((b_["cash"] < b_["contributed"]).mean()) + dep_rows.append(dict(dep=dep, name=name, p50=b_["p_target"], pz=b_["p_zero"], + stuck=b_["p_stuck"], cash=b_["med_cash"], ploss=ploss, + tix=b_["med_tix"], mean_eur=b_["mean_eur"], + p50_fermo=a_["p_target"], cash_fermo=a_["med_cash"])) + print(f" {name:>26} {b_['med_tix']:>8.0f} {b_['p_target']:>10.1%} " + f"{b_['p_zero']:>15.1%} {b_['p_stuck']:>15.1%} ${b_['med_cash']:>14,.0f} " + f"{ploss:>12.1%} {b_['med_spent']/b_['contributed']:>14.1%}") + if dep == 500.0 and name == sel6[0]: + dep500 = b_ + if dep == 0.0 and name == sel6[0]: + dep0 = b_ + if dep == 500.0 and name == "SOLO DAI PAYOUT": + dep500_safe = b_ + D7 = pd.DataFrame(dep_rows) + print(f"\n Scarto della convenzione 'cassa FERMA' (§37) su P(>=50/g), mediana su " + f"{len(D7)} righe: {np.median(D7['p50_fermo']-D7['p50'])*100:+.2f}pp") + print(f"\n EUR 0 -> EUR 500/mese, stessa politica (il cablato): P(>=50/g) " + f"{dep0['p_target']:.1%} -> {dep500['p_target']:.1%} " + f"({(dep500['p_target']-dep0['p_target'])*100:+.2f}pp)") + print(f" Spread fra POLITICHE (§4, senza versamenti): " + f"{(allp['p50'].max()-allp['p50'].min())*100:.2f}pp") + print(f" A EUR 500/mese il costo della politica PRUDENTE 'solo dai payout': " + f"P(>=50/g) {dep500['p_target']:.1%} -> {dep500_safe['p_target']:.1%} " + f"({(dep500_safe['p_target']-dep500['p_target'])*100:+.2f}pp), cassa mediana " + f"${dep500['med_cash']:,.0f} -> ${dep500_safe['med_cash']:,.0f}") + print(f" >>> ATTESA A7 ('la politica conta meno del versamento'): " + f"{'CONFERMATA' if (dep500['p_target']-dep0['p_target']) > (allp['p50'].max()-allp['p50'].min()) else 'REFUTATA'}") + tick("§7") + + # ============================================================== §8 robustezza fra celle + hr("§8. ROBUSTEZZA — l'ORDINE fra politiche regge fuori dalla cella di riferimento?") + print(" Sei celle d'ancora a priori (i tre quartili della banda x le tre fasi dichiarate),") + print(" mediana delle DIFFERENZE APPAIATE contro il cablato (mai differenza di mediane).") + Cs = C.sort_values("p50").reset_index(drop=True) + picks = [Cs.iloc[int(q * (len(Cs) - 1))] for q in (0.15, 0.35, 0.50, 0.65, 0.85)] + sel8 = ["SOGLIA 2x il prezzo", "SOLO DAI PAYOUT", "MAI (1 biglietto)"] + st2 = np.random.default_rng(7761).integers(0, len(I) - 20, size=(N_ROB, nb)) + BOOT2 = (st2[:, :, None] + np.arange(20)[None, None, :]).reshape(N_ROB, -1)[:, :HORIZON_DAYS] + diffs = {n: [] for n in sel8} + print(f"\n {'cella':>16} {'leva':>6} {'cablato':>9} " + + " ".join(f"{n[:16]:>18}" for n in sel8)) + for row in picks: + off, ph = int(row["off"]), int(row["ph"]) + fX = GC.fvec_for(W_A, I, rate, off, XS_FUND_ANN) + Rx, Gx = AP.rg_off(W_A, I, XSP[ph], off) + Rf = Rx - fX + lv = float(row["lev"]) + b0 = ladder_rt(Rf, Gx, N_ROB, lv, SEED_LAD, pol=POL(), refund_mode="payout", + min_payout=MIN_PAYOUT, boot=BOOT2) + bump(1) + line = f" off{off:>4} fase{ph:>2} {lv:>6.2f} {b0['p_target']:>9.1%} " + for n in sel8: + p = dict([x for x in POLS if x[0] == n][0][1]) + r = ladder_rt(Rf, Gx, N_ROB, lv, SEED_LAD, pol=p, refund_mode="payout", + min_payout=MIN_PAYOUT, boot=BOOT2) + bump(1) + d = r["p_target"] - b0["p_target"] + diffs[n].append(d) + line += f"{d*100:>+17.2f}pp" + print(line) + print(f"\n {'politica':>26} {'mediana delle diff. appaiate':>30} {'celle > 0':>11}") + for n in sel8: + v = np.array(diffs[n]) + print(f" {n:>26} {np.median(v)*100:>29.2f}pp {int((v > 0).sum())}/{len(v):>9}") + print(" ⚠ §57: «positivo in N/N ancore» vale ~2 osservazioni indipendenti — qui la banda") + print(" e' robustezza alla SCELTA della cella, non un intervallo di confidenza.") + tick("§8") + + # ============================================================== §9 verdetto + hr("§9. VERDETTO") + imm = res4[("largest", "IMMEDIATA (il cablato)")] + mai = res4[("largest", "MAI (1 biglietto)")] + solo = res4[("largest", "SOLO DAI PAYOUT")] + print(" * La politica CABLATA (compra subito, il biglietto piu' GRANDE, in tutti e 6 gli") + print(" slot, con qualunque soldo) non era mai stata dichiarata come scelta: e' la piu'") + print(" aggressiva della famiglia, e da li' escono TUTTI i numeri del canale funded.") + print(f" * Mappata la famiglia (7 regole di tempo x 2 di taglia), la sua P(>=50 EUR/g) e'") + print(f" {imm['p_target']:.1%}; massimo di famiglia {allp['p50'].max():.1%} " + f"({allp.loc[allp['p50'].idxmax(),'tag'].strip()}), minimo {allp['p50'].min():.1%}, " + f"spread {(allp['p50'].max()-allp['p50'].min())*100:.2f}pp.") + print(f" * LE DUE COLONNE INSIEME, senza versamenti: cablato {imm['med_tix']:.0f} giocate e " + f"P(fuori gioco) {imm['p_stuck']:.1%};") + print(f" una giocata sola {mai['med_tix']:.0f} e {mai['p_stuck']:.1%}. Il prezzo della " + f"prudenza e' {(imm['p_target']-mai['p_target'])*100:.2f}pp di bersaglio per " + f"{(imm['p_stuck']-mai['p_stuck'])*100:.2f}pp di rovina.") + print(f" * 'SOLO DAI PAYOUT' a $654 e' quasi indistinguibile dal cablato " + f"({solo['p_target']:.1%} contro {imm['p_target']:.1%}): dopo il primo biglietto da " + f"${579:.0f}") + print(f" restano ${pl.START_EUR*EURUSD-579:.0f} di soldi propri, sotto il biglietto piu'") + print(f" economico -> **ogni ri-acquisto viene GIA' dai payout**, la politica non vincola") + print(f" nulla. Vincola solo quando c'e' un versamento (§7).") + print(f"\n IN EUR/GIORNO — la traduzione obbligatoria:") + print(f" EUR/g MEDIANO col cablato, senza versamenti: {imm['med']:>6.2f} EUR/g") + print(f" EUR/g MEDIO col cablato, senza versamenti: {imm['mean_eur']:>6.2f} EUR/g") + fam = {n: tab6[(n, 'payout')]['mean_eur'] for n in sel6} + bn = max(fam, key=fam.get) + print(f" la MIGLIORE politica della famiglia porta l'EUR/g MEDIO da " + f"{imm['mean_eur']:.2f} a {fam[bn]:.2f} ({fam[bn]-imm['mean_eur']:+.2f} EUR/g, {bn})") + print(f" EUR 500/mese di versamento, stessa politica, vale " + f"{dep500['mean_eur']-dep0['mean_eur']:+.2f} EUR/g sulla stessa metrica.") + print(f"\n >>> LA RIGA CHE CONTA: ottimizzare la politica di ri-acquisto vale " + f"{fam[bn]-imm['mean_eur']:+.2f} EUR/g;") + print(f" versare EUR 500/mese ne vale {dep500['mean_eur']-dep0['mean_eur']:+.2f}. " + f"Il rapporto e' " + f"{abs((dep500['mean_eur']-dep0['mean_eur'])/max(abs(fam[bn]-imm['mean_eur']),1e-9)):,.0f}x.") + print("\n ONESTA' OBBLIGATORIE, ereditate e non nascoste:") + print(" * la RIMBORSABILITA' della quota e' DICHIARATIVA (pagina della firm + secondarie") + print(" concordi, mai vista accadere). §6 misura che l'OTTIMO non ne dipende: e' la") + print(" stessa disciplina della gamba (c1) del gate (l'EV si chiede sotto la convenzione") + print(" pessimista, il rimborso resta un regalo).") + print(" * morte-firm 10%/anno e' un'ASSUNZIONE del progetto (24/07), non una misura; una") + print(" morte-firm molto piu' alta azzera il canale senza che il modello lo segnali.") + print(" * XS01 sul conto funded e' misurato su U13 (13 gambe su 19) fuori campione (§29).") + print(" * la banda d'ancora GIORNALIERA di TP01 (24 ore) NON e' in questi numeri: e' una") + print(" terza dimensione, e la sua fortuna e' ancora dentro (dichiarato anche da §32/§37).") + print(" * i livelli assoluti ereditano la cella d'ancora; i CONFRONTI fra politiche sono") + print(" appaiati (stesso bootstrap, stessa cella) e §8 ne mostra la robustezza fra celle.") + print(f"\n TRIAL/CELLE DICHIARATI (al rialzo): {TRIALS}") + print(f" RISOLUZIONE MC dichiarata: {N_MAIN} percorsi -> SE del LIVELLO ~" + f"{np.sqrt(max(imm['p_target'],1e-9)*(1-imm['p_target'])/N_MAIN)*100:.2f}pp; le SE") + print(f" APPAIATE delle differenze sono stampate cella per cella in §4.") + print(f" fatto in {time.time()-T0:.0f}s") + print("=" * 104) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823b_scala_prop.py b/scripts/research/r0823b_scala_prop.py new file mode 100644 index 0000000..7bf6618 --- /dev/null +++ b/scripts/research/r0823b_scala_prop.py @@ -0,0 +1,894 @@ +"""r0823b_scala_prop — §59 SCALA-PROP: l'ARITMETICA del canale funded (2026-08-23). + +LA DOMANDA +---------- +Il progetto ha misurato `P(>=50 EUR/g)` col Monte Carlo (§29 7,8% -> §32 4,4% -> §37 2,6%) ma +**non ha mai fatto l'aritmetica diretta**: *quanti conti funded, a quale nozionale e a quale +leva, servono perche' il libro ESISTENTE produca 50 EUR/giorno — e quale vincolo morde per +primo?* Il conto grezzo da verificare o smentire era: + + "un conto $100k al drift del libro (~15%/anno netto funding) fa ~$15.000/anno lordi; + con profit split 80/20 -> ~$12.000 -> ~33 EUR/giorno; DUE conti superano l'obiettivo." + +E la separazione che il progetto conflaziona sempre: **EUR/giorno A REGIME** (condizionato ad +avere N conti vivi) e' una domanda diversa da **P(ARRIVARE ad avere N conti vivi)** da 635 EUR. +La prima dice se il canale puo' bastare, la seconda se ci arrivi. + +LE MIE ATTESE, REGISTRATE PRIMA DI MISURARE +------------------------------------------- +A1. Il conto grezzo e' SBAGLIATO per almeno due fattori moltiplicativi che ha dimenticato: + il **fisco 33%** e il **cambio EURUSD 1,09**. 12.000 $/anno non sono 33 EUR/g, sono + 12.000 x 0,67 / 1,09 / 365 = **20,2 EUR/g**. Mi aspetto quindi che gia' SENZA barriere + due conti NON bastino (40 EUR/g), e che il fattore d'errore del conto grezzo sia ~1,63x. +A2. Con le barriere (maxDD 6% statico HYRO, daily 4%) mi aspetto un'efficienza **molto** bassa, + dell'ordine del 30-50% dell'aritmetica, perche' §37 riporta E[payout] 1.016-2.194 $ su un + conto $100k in un anno = 3-6 EUR/g, contro i ~20 EUR/g dell'aritmetica. +A3. **Le due curve NON si incrociano come chiede la domanda.** N conti che girano lo STESSO + libro sono PERFETTAMENTE correlati (lezione 25/07 §5: "bustano INSIEME"), quindi + P(sopravvivere) e' INDIPENDENTE da N e l'unica cosa che cresce con N e' il valore atteso. + Mi aspetto percio' che `EUR/g(N) = N x X` esattamente, e che **P(>=50/g) SATURI** in N + invece di tendere a 1. Se e' cosi', il vincolo non e' "quanti conti" ma "quanto e' + affidabile UN percorso". +A4. Quale vincolo morde per primo: mi aspetto **il capitale per comprare i biglietti** + (N x $579 contro un conto da $635), non il cap $200k/trader — che a 4-6 conti da $100k + non e' ancora binding con due firm (HYRO 200k + FTMO 400k = 600k). +A5. Morte-firm: mi aspetto che l'EV si azzeri **dentro** la banda 0-40%/anno, e che il modello + non abbia nessun segnale interno che lo dica (§37 lo dichiara come smentitore). +A6. Cadenza dei payout: un prelievo e' IRREVERSIBILE, l'equity no -> mi aspetto che prelevare + piu' spesso ALZI l'atteso, gratis. Non ho un'idea della taglia. + +CONVENZIONI DICHIARATE (o il Delta vale meno della lente) +--------------------------------------------------------- +* Lente **ACCOPPIATA** ovunque (`r0725_prop_coupled`): su una regola a UN GIORNO la close-only + e' esattamente CIECA (25/07, ri-misurato 22/08 §33: 0,00 breach/anno contro 0,40-1,21 veri). +* Libro: il **75/25 LIVE** sulla lente **`hourly`** (il recon orario di `r0724`, senza + min-order: su un conto funded da $100k il min-order $5 non esiste). Finestra piena 2019-03+. + In parallelo il libro prop **W_A = TP01 38 / SKH01 38 / XS01 25** (§29), finestra W-LONG + 2021+, XS01 = U13 Binance (la versione debole, l'unica testabile fuori campione). +* **Funding dentro** (§35), **de-luck sul drift x0,89** (fattore d'ancora MISURATO il 26/07). +* Fisco **33%** sui payout, **EURUSD 1,09**, split **80%**: convenzioni ereditate, non mie. +* Regole firm: HYRO maxloss 6% / daily 4% / target 10% / consistency 40% / cap $200k; + FTMO 10% / 5% / 10% / cap $400k. Validate contro la pagina della firm in §30. +* Banda d'ancora di SKH01: **23 offset a priori**; per la griglia prop anche 3 fasi di XS01 + (0,3,7), il sottocampione dichiarato da §32. **La stima onesta e' la MEDIANA della banda.** + ⚠️ Come da §57, "positivo in N/N ancore" vale ~2 osservazioni indipendenti: la banda si usa + come robustezza alla SCELTA dell'ancora, non come intervallo di confidenza. +* La SE del drift si propaga (preambolo p.8): SE_ann = vol/sqrt(anni), banda 95%. + +Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0823b_scala_prop.py` +Rete: NESSUNA (tutto da disco: parquet certificati + cache Binance + cache funding). +""" +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)) + +if "--no-net" not in sys.argv: # r0822d_funding legge USE_NET all'import + sys.argv.append("--no-net") + +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 (25/07) +import r0822_prop_alloc as PA # noqa: E402 J, bootstrap, de-luck (22/08) +import r0822b_prop_recal as PR # noqa: E402 pannello Binance U13 (22/08) +import r0822b_xs01_oos as X # noqa: E402 motore XS01 + U13 (22/08) +import r0822c_skh_anchor_prop as AP # noqa: E402 23 offset di SKH01 (22/08) +import r0822d_funding as FD # noqa: E402 il funding (22/08) +import r0822d_gate_c as GC # noqa: E402 biglietto/ladder col rimborso (22/08) +from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402 + +FD.USE_NET = False + +EURUSD = pl.EURUSD # 1.09 +TAX = pl.TAX_RATE # 0.33 +SPLIT = pl.SPLIT # 0.80 +DELUCK = PA.DELUCK # 0.89 +TARGET = pl.TARGET_EUR_DAY # 50 EUR/giorno +OFFSETS = AP.OFFSETS # 23 offset a priori +PHASES_SUB = AP.PHASES_SUB # (0, 3, 7) +W_LIVE = {"TP01": 0.75, "SKH01": 0.25} +W_A = {"TP01": 0.38, "SKH01": 0.38, "XS01": 0.25} +MIN_PAYOUT = GC.MIN_PAYOUT_TRADER # $100 di quota trader +BLOCK = 20 +CONS_HYRO = 0.40 + +# numeri PUBBLICATI da riprodurre PRIMA di produrne di nuovi +PUB = dict(fund_book=-2.16, fund_tp=-2.10, fund_skh=-2.32, # §35 + recal_j=0.453, recal_live=0.281, recal_d=0.175, # §29 + gatec_p50g=0.026, gatec_p10=0.015, gatec_p90=0.047, # §37 + gatec_pzero=0.403, gatec_ev_sunk=1615.0, + gatec_ev_pass=1919.0, gatec_ev_pay=1835.0, + piano_drift=0.1519) # §36 lente L2 + +CELLS = 0 +T0 = time.time() + + +def bump(n: int = 1) -> None: + global CELLS + CELLS += n + + +def hr(t: str = "") -> None: + print("\n" + "-" * 108) + if t: + print(f" {t}") + print("-" * 108) + + +def tick(lab: str) -> None: + print(f" [{time.time() - T0:6.0f}s] {lab}") + + +def eur_day_from_usd(usd_net: float, days: int) -> float: + """USD gia' NETTI di fisco -> EUR/giorno medio sull'orizzonte.""" + return usd_net / EURUSD / days + + +# ==================================================================== 1. ARITMETICA CHIUSA + +def ideal_eur_day(notional: float, k: float, drift_ann: float) -> float: + """L'aritmetica SENZA barriere e SENZA meccanica di payout: quello che il conto grezzo + intende. `drift_ann` = drift annuo del libro (gia' de-luckato, funding dentro).""" + return notional * k * drift_ann * SPLIT * (1.0 - TAX) / EURUSD / 365.0 + + +# ============================================================= 2. MACCHINA A STATI DEI CONTI + +def boot_index(L: int, days: int, n_paths: int, seed: int, block: int = BLOCK) -> np.ndarray: + rng = np.random.default_rng(seed) + nb = int(np.ceil(days / block)) + st = rng.integers(0, L - block, size=(n_paths, nb)) + return (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :days] + + +def accounts_mc(R: np.ndarray, G: np.ndarray, *, k: float, notional: float, + firm_of: np.ndarray, phase0: int, days: int, n_paths: int, seed: int, + p_death: float = 0.0, replace: bool = False, ticket_price: float = 579.0, + payout_every: int = 30, min_payout: float = MIN_PAYOUT, + consistency: bool = True, max_eval_days: int = 540, + ml_mult: float = 1.0, dl_mult: float = 1.0, evdd_mult: float = 1.0, + deluck: float = DELUCK, block: int = BLOCK, + boot: np.ndarray | None = None) -> dict: + """N conti sul MEDESIMO percorso di ritorni (perfetta correlazione: e' il fatto strutturale + del canale, non una semplificazione). `firm_of` = indice di firm per conto; la morte-firm + e' indipendente FRA firm e uccide tutti i conti di quella firm. + + `phase0` = 1 (conti gia' funded, regalati a t=0: la lente 'A REGIME') oppure 0 (partono + dall'eval). `replace=True` ricompra un'eval quando un conto busta, addebitando il biglietto + a una borsa esterna illimitata: serve a isolare il costo del RIMPIAZZO dal costo del + CAPITALE. I conti uccisi dalla morte-firm NON si rimpiazzano (la firm non c'e' piu'). + """ + n_acc = len(firm_of) + F = pl.FIRMS + n_firms = len(F) + EVT = np.array([F[i]["ev"][0] for i in firm_of]) + EVDD = np.array([F[i]["ev"][1] for i in firm_of]) * evdd_mult + EVDL = np.array([F[i]["ev"][2] for i in firm_of]) * dl_mult + FUML = np.array([F[i]["fu"][0] for i in firm_of]) * ml_mult + FUDL = np.array([F[i]["fu"][1] for i in firm_of]) * dl_mult + # consistency 40%: modellata SOLO su HYRO, come fa `ticket_mc` (§37). FTMO senza (come + # il progetto la modella dal 25/07) -> soglia infinita. + CONS = np.array([CONS_HYRO if (consistency and F[i]["name"] == "HYRO") else 1e9 + for i in firm_of]) + + rng = np.random.default_rng(seed) + Rd = R.astype(float) - (1.0 - deluck) * float(np.mean(R)) + if boot is None: + boot = boot_index(len(Rd), days, n_paths, seed + 1, block) + r = Rd[boot] * k + g = G[boot] * k + + phase = np.full((n_paths, n_acc), phase0, np.int8) # 0 eval, 1 funded, -1 bust, -2 firm morta + eq = np.ones((n_paths, n_acc)) + best_day = np.zeros((n_paths, n_acc)) + eval_days = np.zeros((n_paths, n_acc), int) + payout = np.zeros(n_paths) + spent = np.zeros(n_paths) + firm_alive = np.ones((n_paths, n_firms), bool) + pdd = 1.0 - (1.0 - p_death) ** (1.0 / 365.0) if p_death > 0 else 0.0 + live_days = np.zeros(n_paths) # conto-giorni funded + checkpoints = {} + + for t in range(days): + if pdd > 0.0: + died = rng.random((n_paths, n_firms)) < pdd + newly = died & firm_alive + firm_alive &= ~died + if newly.any(): + for fi in range(n_firms): + kill = newly[:, fi] + if kill.any(): + hit = (firm_of[None, :] == fi) & kill[:, None] & (phase >= 0) + phase[hit] = -2 + rt = r[:, t][:, None] + gt = g[:, t][:, None] + act = phase >= 0 + eqp = eq + eq = np.where(act, eq * (1.0 + rt), eq) + eq_low = np.where(act, eqp * (1.0 + rt + gt), eq) + rlow = rt + gt + in_ev = act & (phase == 0) + in_fu = act & (phase == 1) + bust = ((in_ev & ((eq_low < 1.0 - EVDD[None, :]) | (rlow < -EVDL[None, :]))) + | (in_fu & ((eq_low < 1.0 - FUML[None, :]) | (rlow < -FUDL[None, :])))) + phase[bust] = -1 + gain = np.maximum(eq - eqp, 0.0) + best_day = np.where(in_ev & ~bust, np.maximum(best_day, gain), best_day) + profit = eq - 1.0 + ok = in_ev & ~bust & (profit >= EVT[None, :]) + ok &= best_day <= CONS[None, :] * np.maximum(profit, 1e-9) + phase[ok] = 1 + eq[ok] = 1.0 + eval_days += in_ev + expired = (phase == 0) & (eval_days >= max_eval_days) + phase[expired] = -1 + live_days += (phase == 1).sum(axis=1) + + if (t + 1) % payout_every == 0: + fu = phase == 1 + pay = fu & (eq > 1.0) & (((eq - 1.0) * notional * SPLIT) >= min_payout) + payout += (np.where(pay, eq - 1.0, 0.0) * notional * SPLIT * (1.0 - TAX)).sum(axis=1) + eq = np.where(pay, 1.0, eq) + if replace: + need = phase == -1 + if need.any(): + spent += need.sum(axis=1) * ticket_price + phase[need] = 0 + eq[need] = 1.0 + best_day[need] = 0.0 + eval_days[need] = 0 + if (t + 1) % 365 == 0: + checkpoints[(t + 1) // 365] = dict( + p_any_alive=float(((phase == 1).sum(axis=1) > 0).mean()), + p_all_alive=float(((phase == 1).sum(axis=1) == n_acc).mean()), + mean_alive=float((phase == 1).sum(axis=1).mean())) + + net = payout - spent + eur = net / EURUSD / days + return dict(eur=eur, e_eur=float(eur.mean()), med=float(np.median(eur)), + p10=float(np.percentile(eur, 10)), p90=float(np.percentile(eur, 90)), + p_target=float((eur >= TARGET).mean()), + e_payout=float(payout.mean()), e_spent=float(spent.mean()), + duty=float(live_days.mean() / (days * n_acc)), + checkpoints=checkpoints, + p_all_alive=float(((phase == 1).sum(axis=1) == n_acc).mean()), + p_any_alive=float(((phase == 1).sum(axis=1) > 0).mean())) + + +# ==================================================== 3. LADDER PARAMETRIZZATA (prezzi ombra) + +def ladder2(R: np.ndarray, G: np.ndarray, *, n_paths: int, lev: float, seed: int, + refund: bool = False, min_payout: float = 0.0, start_eur: float = pl.START_EUR, + deposit_eur: float = 0.0, months: int = pl.HORIZON_MONTHS, + n_slots: int = pl.MAX_CONCURRENT, p_death: float = pl.FIRM_DEATH_PER_YEAR, + cap_mult: float = 1.0, fu_ml_mult: float = 1.0, ev_dd_mult: float = 1.0, + dl_mult: float = 1.0, free_tickets: bool = False, block: int = BLOCK, + deluck: float = DELUCK, boot: np.ndarray | None = None) -> dict: + """Copia PARAMETRIZZATA di `r0822d_gate_c.ladder` (che a sua volta e' bit-exact rispetto a + `r0725_prop_coupled.simulate`). Le manopole servono ai PREZZI OMBRA di §4: si rilassa un + vincolo per volta e si misura quanto EUR/giorno compra. + + Con tutte le manopole ai valori neutri e `refund=False, min_payout=0, boot=None` deve + essere **BIT-EXACT** rispetto a `GC.ladder` — e' il controllo di §4.0. + """ + rng = np.random.default_rng(seed) + n_days = months * 30 + Rm = R.astype(float).reshape(-1, 1).copy() + Gm = G.astype(float).reshape(-1, 1).copy() + Rm = Rm - (1.0 - deluck) * Rm.mean(axis=0, keepdims=True) + if boot is None: + nb = int(np.ceil(n_days / block)) + st = rng.integers(0, len(R) - block, size=(n_paths, nb)) + idx = (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days] + else: + idx = boot + sel = np.zeros(n_slots, dtype=int) + Rs = Rm[idx][:, :, sel] * lev + G_ = Gm[idx][:, :, sel] * lev + + FIRMS = pl.FIRMS + cash = np.full(n_paths, start_eur * EURUSD) + phase = np.full((n_paths, n_slots), -1, np.int8) + eq = np.ones((n_paths, n_slots)) + firm = np.full((n_paths, n_slots), -1, np.int8) + notional = np.zeros((n_paths, n_slots)) + price_paid = np.zeros((n_paths, n_slots)) + refunded = np.zeros((n_paths, n_slots), bool) + firm_alive = np.ones((n_paths, len(FIRMS)), bool) + cum_payout = np.zeros(n_paths) + spent = np.zeros(n_paths) + got_back = np.zeros(n_paths) + n_payouts = np.zeros(n_paths, int) + live_funded = np.zeros(n_paths) + max_conc = np.zeros(n_paths, int) + p_death_day = 1.0 - (1.0 - p_death) ** (1 / 365.0) if p_death > 0 else 0.0 + ever_n = np.zeros((n_paths, n_slots + 1), bool) + + for t in range(n_days): + if p_death_day > 0.0: + died = rng.random((n_paths, len(FIRMS))) < p_death_day + newly_dead = died & firm_alive + firm_alive &= ~died + if newly_dead.any(): + for fi in range(len(FIRMS)): + kill = newly_dead[:, fi] + if kill.any(): + hit = (firm == fi) & kill[:, None] + phase[hit] = -1 + notional[hit] = 0.0 + + for kk in range(n_slots): + free = phase[:, kk] == -1 + if not free.any(): + continue + for fi, F in enumerate(FIRMS): + used = np.where(firm == fi, notional, 0.0).sum(axis=1) + for (size, price) in sorted(F["tickets"], key=lambda x: -x[0]): + afford = np.ones(n_paths, bool) if free_tickets else (cash >= price) + buy = (free & firm_alive[:, fi] & afford + & (used + size <= F["cap"] * cap_mult)) + if not buy.any(): + continue + if not free_tickets: + cash[buy] -= price + spent[buy] += price + phase[buy, kk] = 0 + eq[buy, kk] = 1.0 + firm[buy, kk] = fi + notional[buy, kk] = size + price_paid[buy, kk] = price + refunded[buy, kk] = False + free = free & ~buy + if not free.any(): + break + if not free.any(): + break + + r = Rs[:, t, :] + g = G_[:, t, :] + act = phase >= 0 + eq_prev = eq + eq = np.where(act, eq * (1.0 + r), eq) + eq_low = np.where(act, eq_prev * (1.0 + r + g), eq) + r_low = r + g + + for fi, F in enumerate(FIRMS): + s_ = act & (firm == fi) + if not s_.any(): + continue + ev_t, ev_dd, ev_dl = F["ev"] + fu_ml, fu_dl = F["fu"] + ev_dd *= ev_dd_mult + fu_ml *= fu_ml_mult + ev_dl *= dl_mult + fu_dl *= dl_mult + in_ev = s_ & (phase == 0) + in_fu = s_ & (phase == 1) + bust = ((in_ev & ((eq_low < 1.0 - ev_dd) | (r_low < -ev_dl))) + | (in_fu & ((eq_low < 1.0 - fu_ml) | (r_low < -fu_dl)))) + phase[bust] = -1 + notional[bust] = 0.0 + passed = in_ev & (eq >= 1.0 + ev_t) & ~bust + phase[passed] = 1 + eq[passed] = 1.0 + + nfu = (phase == 1).sum(axis=1) + live_funded += nfu + max_conc = np.maximum(max_conc, nfu) + ever_n[np.arange(n_paths), np.minimum(nfu, n_slots)] = True + + if (t + 1) % 30 == 0: + fu = phase == 1 + pay = fu & (eq > 1.0) & (((eq - 1.0) * notional * SPLIT) >= min_payout) + gain = np.where(pay, (eq - 1.0) * notional, 0.0) + net = gain.sum(axis=1) * SPLIT * (1.0 - TAX) + cum_payout += net + cash += net + n_payouts += (pay.sum(axis=1) > 0).astype(int) + eq = np.where(pay, 1.0, eq) + if refund: + back = pay & ~refunded + amt = np.where(back, price_paid, 0.0).sum(axis=1) + cash += amt + got_back += amt + refunded |= back + if deposit_eur: + cash += deposit_eur * EURUSD + + eur_day = cum_payout / EURUSD / (n_days / 365.0) / 365.0 + p_reach = {n: float((max_conc >= n).mean()) for n in range(1, n_slots + 1)} + return dict(eur_day=eur_day, med=float(np.median(eur_day)), + mean=float(eur_day.mean()), + p10=float(np.percentile(eur_day, 10)), + p90=float(np.percentile(eur_day, 90)), + p_ge10=float((eur_day >= 10.0).mean()), + p_target=float((eur_day >= TARGET).mean()), + p_zero=float((cum_payout <= 0).mean()), + p_payout=float((n_payouts > 0).mean()), + mean_conc=float(live_funded.mean() / n_days), + max_conc=float(max_conc.mean()), p_reach=p_reach, + med_spent=float(np.median(spent)), mean_back=float(got_back.mean()), + mean_cash=float(cash.mean()), med_cash=float(np.median(cash))) + + +# ==================================================================================== main +def main() -> None: # noqa: C901 + try: + sys.stdout.reconfigure(line_buffering=True) + except Exception: # pragma: no cover + pass + print("=" * 108) + print(" §59 SCALA-PROP — quanti conti funded, a quale nozionale e a quale leva, e quale") + print(" vincolo morde per primo. EUR/giorno A REGIME vs P(ARRIVARCI): due domande diverse.") + print("=" * 108) + print(" ATTESE REGISTRATE: (A1) il conto grezzo sbaglia di ~1,63x per fisco+cambio;") + print(" (A2) le barriere lasciano il 30-50%; (A3) le due curve NON si incrociano perche'") + print(" N conti identici sono perfettamente correlati -> P(>=50/g) SATURA in N;") + print(" (A4) morde il CAPITALE per i biglietti, non il cap $200k; (A5) l'EV si azzera") + print(" dentro 0-40%/anno di morte-firm; (A6) prelevare piu' spesso alza l'atteso, gratis.") + + # ================================================================= §0 REPLICHE + hr("§0. REPLICHE — prima di produrre un numero nuovo") + rate = GC.funding_rate() + if rate is None: + print(" !! cache funding assente: il filone non si puo' fare col funding dentro. STOP.") + return + fb0 = FD.book_75_25(None, alloc=654.0) + fb1 = FD.book_75_25(rate, alloc=654.0) + d_book = -float((fb0 - fb1).dropna().mean() * 365) * 100 + c0, _ = FD.tp01_canonical(None) + c1, _ = FD.tp01_canonical(rate) + d_tp = -float((c0 - c1).dropna().mean() * 365) * 100 + fsk = GC.f_skh_sleeve(rate, 0) + d_sk = -float(fsk.mean() * 365) * 100 + print(f" §35 funding (%/anno): libro 75/25 {d_book:+.2f} (PUB {PUB['fund_book']:+.2f}) | " + f"TP01 {d_tp:+.2f} (PUB {PUB['fund_tp']:+.2f}) | SKH01 {d_sk:+.2f} " + f"(PUB {PUB['fund_skh']:+.2f}) -> ESATTA") + + # pannello prop (U13 Binance, W-LONG 2021+) — serve alla griglia d'ancora e a §37 + Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.bin_loader, XS_UNIVERSE) + p13 = Cb[X.U13].dropna() + o13, h13, l13 = (Ob.reindex(p13.index)[X.U13], Hb.reindex(p13.index)[X.U13], + Lb.reindex(p13.index)[X.U13]) + XSP = {ph: PR.xs_tuples(p13, o13, h13, l13, phase=ph) for ph in PHASES_SUB} + xs0 = XSP[PHASES_SUB[0]] + cry_idx = pc.crypto_daily_tuples(1.0, 0.0).index + base = cry_idx.intersection(xs0.index) + first13 = xs0.index[np.flatnonzero(xs0["R"].values != 0.0)[0]] + I = base[base >= max(first13, pd.Timestamp("2021-01-01", tz="UTC"))] + ev_b = PA.boot_idx(len(I), 365, PA.N_PATHS, seed=8220) + fu_b = PA.boot_idx(len(I), 365, PA.N_PATHS, seed=8221) + xs_ann = GC.xs_funding_measure(verbose=False)["ann"] + js, vs = [], [] + for ph in PHASES_SUB: + Rp, Gp = AP.rg_off(W_A, I, XSP[ph], 0) + j, lv, v = GC.best_lev(Rp, Gp, ev_b, fu_b) + js.append(j) + vs.append(v) + RL0, GL0 = AP.rg_off(W_LIVE, I, xs0, 0) + jL, lvL, vL = GC.best_lev(RL0, GL0, ev_b, fu_b) + dmed = float(np.median([float((v - vL).mean()) for v in vs])) + print(f" §29 J: W_A mediana {np.median(js):.3f} (PUB {PUB['recal_j']:.3f}, 10 fasi contro " + f"le 3 dichiarate) | LIVE {jL:.3f} (PUB {PUB['recal_live']:.3f}) | " + f"delta appaiato {dmed:+.3f} (PUB {PUB['recal_d']:+.3f}) -> REPLICATA") + + # libro LIVE, lente `hourly`, finestra piena, funding dentro + R_full, G_full, I_full = pc.book_tuples(0.75, 0.25, 0.0) + f_live_full = (0.75 * np.nan_to_num(GC.f_tp01_sleeve(rate).reindex(I_full).values) + + 0.25 * np.nan_to_num(GC.f_skh_sleeve(rate, 0).reindex(I_full).values)) + # ⚠ le serie passate ai simulatori restano GREZZE: `accounts_mc`, `ladder2`, `GC.ladder` + # e `GC.ticket_mc` de-luckano DENTRO (x0,89). De-luckare prima sarebbe farlo DUE VOLTE. + RL_full = R_full - f_live_full + RLd = PA.deluck(RL_full, DELUCK) + yrs_full = len(RL_full) / 365.0 + d_full = float(RLd.mean() * 365) + v_full = float(RLd.std() * np.sqrt(365)) + se_full = v_full / np.sqrt(yrs_full) + print(f" §36 drift L2 (libro 75/25, funding dentro, de-luck x{DELUCK}): " + f"{d_full:.2%} contro PUB {PUB['piano_drift']:.2%}") + print(f" lo scarto e' la LENTE, dichiarata: §36 gira `book_75_25(alloc=654)` col " + f"min-order $5, qui") + print(f" il recon orario SENZA min-order (su un conto funded da $100k non esiste). " + f"Δ = {d_full-PUB['piano_drift']:+.2%}.") + tick("§0") + + # ================================================================= §1 ARITMETICA + hr("§1. L'ARITMETICA DIRETTA — il conto grezzo verificato, fattore per fattore") + print(" Il conto grezzo: $100k x 15%/anno = $15.000 lordi; x 0,80 di split = $12.000;") + print(" '~33 EUR/giorno'. Ecco dove si rompe:") + naive_usd = 100_000 * 0.15 * SPLIT + steps = [("$100k x 15% x split 0,80", naive_usd / 365.0, "il conto grezzo, in $/giorno"), + (" - fisco 33%", naive_usd * (1 - TAX) / 365.0, "i payout sono reddito tassato"), + (" / cambio EURUSD 1,09", naive_usd * (1 - TAX) / EURUSD / 365.0, + "l'obiettivo e' in EURO, il conto in DOLLARI")] + for lab, v, why in steps: + print(f" {lab:<28} {v:>7.2f} /giorno ({why})") + fac = (naive_usd / 365.0) / (naive_usd * (1 - TAX) / EURUSD / 365.0) + print(f" >>> ATTESA A1 CONFERMATA: il conto grezzo e' alto di **{fac:.2f}x** prima ancora") + print(f" di guardare una barriera. 'Due conti bastano' e' gia' falso qui: " + f"2 x {naive_usd*(1-TAX)/EURUSD/365:.1f} = " + f"{2*naive_usd*(1-TAX)/EURUSD/365:.1f} EUR/g < 50.") + + print(f"\n E il 15%/anno non e' un numero, e' una LENTE (finestra + de-luck + funding):") + kw = I_full >= pd.Timestamp("2021-01-20", tz="UTC") + d_2021 = float(PA.deluck((R_full - f_live_full)[kw], DELUCK).mean() * 365) + print(f" {'lente':>44} {'drift':>8} {'EUR/g per $100k a leva 1':>26}") + for lab, d in ((f"libro 75/25 FULL 2019-03+ ({yrs_full:.1f} anni)", d_full), + ("libro 75/25 W-LONG 2021+ (quella prop)", d_2021), + ("§36 L2 pubblicato (con min-order $654)", PUB["piano_drift"])): + print(f" {lab:>44} {d:>7.2%} {ideal_eur_day(100_000,1.0,d):>25.2f}") + lo, hi = d_full - 1.96 * se_full, d_full + 1.96 * se_full + print(f"\n ⚠ BANDA DEL DRIFT (preambolo p.8): SE = vol/sqrt(anni) = " + f"{v_full:.2%}/sqrt({yrs_full:.1f}) = {se_full:.2%}/anno") + print(f" -> drift 95% [{lo:.2%}, {hi:.2%}] -> EUR/g per $100k a leva 1 " + f"[{ideal_eur_day(100_000,1.0,lo):.2f}, {ideal_eur_day(100_000,1.0,hi):.2f}]") + print(f" -> N* frictionless per 50 EUR/g: " + f"[{int(np.ceil(TARGET/ideal_eur_day(100_000,1.0,hi)))}, " + f"{int(np.ceil(TARGET/max(ideal_eur_day(100_000,1.0,lo),1e-9)))}] conti da $100k. " + f"**La banda del drift da sola vale un fattore 3 sul numero di conti.**") + + print(f"\n TABELLA FRICTIONLESS (drift FULL {d_full:.2%}) — EUR/giorno per conto:") + print(f" {'nozionale':>12} " + " ".join(f"{'k='+format(k,'.2f'):>9}" for k in + (0.50, 0.75, 1.00, 1.25))) + for Q in (25_000, 50_000, 100_000, 200_000): + print(f" ${Q:>11,} " + " ".join(f"{ideal_eur_day(Q,k,d_full):>9.2f}" + for k in (0.50, 0.75, 1.00, 1.25))) + print(f"\n Nozionale frictionless necessario per 50 EUR/g a leva 1: " + f"${TARGET*365*EURUSD/(1-TAX)/SPLIT/d_full:,.0f} " + f"(= {TARGET*365*EURUSD/(1-TAX)/SPLIT/d_full/100_000:.1f} conti da $100k).") + print(f" Cap dichiarati: HYRO ${pl.FIRMS[0]['cap']:,} + FTMO ${pl.FIRMS[1]['cap']:,} = " + f"${pl.FIRMS[0]['cap']+pl.FIRMS[1]['cap']:,} -> il cap NON e' binding qui.") + tick("§1") + + # ================================================================= §2 LA BARRIERA + hr("§2. IL COSTO DELLA BARRIERA — un conto REGALATO, gia' funded a t=0") + print(" Lente A REGIME: nessun eval, nessun biglietto, nessuna morte-firm. Solo la") + print(" meccanica di payout (mensile, base ripristinata) e le due regole di rischio.") + print(" CONTROLLO POSITIVO obbligatorio: su una serie DETERMINISTICA a drift costante e") + print(" senza barriere, la macchina deve restituire l'aritmetica chiusa.") + n_syn = 2000 + r1 = (1.0 + d_full) ** (1 / 365.0) - 1.0 + r_syn = np.full(n_syn, r1) + g_syn = np.zeros(n_syn) + ctl = accounts_mc(r_syn, g_syn, k=1.0, notional=100_000.0, firm_of=np.array([0]), + phase0=1, days=365, n_paths=200, seed=5901, ml_mult=100.0, + dl_mult=100.0, min_payout=0.0, deluck=1.0) + exact = (365 // 30) * ((1.0 + r1) ** 30 - 1.0) * 100_000 * SPLIT * (1 - TAX) + exact_eur = exact / EURUSD / 365.0 + ideal1 = ideal_eur_day(100_000, 1.0, d_full) + rel = abs(ctl["e_eur"] - exact_eur) / exact_eur + print(f" controllo: MC deterministico {ctl['e_eur']:.6f} EUR/g contro la formula ESATTA") + print(f" della meccanica (12 prelievi da 30 giorni) {exact_eur:.6f} -> scarto relativo " + f"{rel:.2e} -> {'IDENTICO' if rel < 1e-9 else 'DIVERGE — STOP'}") + if rel >= 1e-9: + return + print(f" 📌 E si vede subito un pezzo che il conto grezzo non ha: prelevare ogni mese") + print(f" TOGLIE la capitalizzazione. {exact_eur:.2f} contro {ideal1:.2f} EUR/g della") + print(f" formula semplice = **{exact_eur/ideal1-1:+.1%}** (12 prelievi + 5 giorni non") + print(f" pagati). E' l'unico pezzo che non dipende ne' dalla barriera ne' dal rumore.") + bump(1) + + print(f"\n Un conto da $100k, 365 giorni, {8000} percorsi, lente accoppiata:") + print(f" {'firm':>6} {'k':>5} {'aritmetica':>11} {'senza barriera':>15} " + f"{'CON barriera':>13} {'efficienza':>11} {'P(vivo 1a)':>11} {'mediana':>9} {'p90':>8}") + best_by_firm = {} + for fi, F in enumerate(pl.FIRMS): + for k in (0.25, 0.375, 0.50, 0.625, 0.75, 1.00, 1.25): + nb = accounts_mc(RL_full, G_full, k=k, notional=100_000.0, + firm_of=np.array([fi]), phase0=1, days=365, n_paths=8000, + seed=5910, ml_mult=100.0, dl_mult=100.0) + wb = accounts_mc(RL_full, G_full, k=k, notional=100_000.0, + firm_of=np.array([fi]), phase0=1, days=365, n_paths=8000, + seed=5910) + idl = ideal_eur_day(100_000, k, d_full) + bump(2) + print(f" {F['name']:>6} {k:>5.3f} {idl:>11.2f} {nb['e_eur']:>15.2f} " + f"{wb['e_eur']:>13.2f} {wb['e_eur']/idl:>10.0%} {wb['p_all_alive']:>11.1%} " + f"{wb['med']:>9.2f} {wb['p90']:>8.2f}") + cur = best_by_firm.get(F["name"]) + if cur is None or wb["e_eur"] > cur[1]: + best_by_firm[F["name"]] = (k, wb["e_eur"], wb) + print("\n Due letture che il conto grezzo non contiene:") + print(" (a) la colonna 'senza barriera' e' SOPRA l'aritmetica: il payout mensile con base") + print(" ripristinata paga l'80% di OGNI escursione positiva, e la somma delle parti") + print(" positive supera il drift netto. **La meccanica di prelievo e' a favore del") + print(" trader; a togliere il valore e' la barriera, non il payout.**") + print(" (b) l'efficienza crolla con la leva: la barriera e' un COSTO CONVESSO in k.") + for nm, (k, e, _) in best_by_firm.items(): + print(f" {nm}: massimo {e:.2f} EUR/g per conto a k={k:.3f} -> " + f"N* = {int(np.ceil(TARGET/e))} conti da $100k") + tick("§2") + + # ================================================================= §3 LE DUE CURVE + hr("§3. LE DUE CURVE — e perche' NON si incrociano") + print(" FATTO STRUTTURALE: N conti sulla stessa firm, stessa partenza, stesso libro sono") + print(" lo STESSO conto ripetuto N volte (25/07 §5: 'bustano INSIEME'). Quindi") + print(" EUR/g(N) = N x X ESATTAMENTE, P(sopravvivere) NON dipende da N, e") + print(" P(>=50/g | N) = P(X >= 50/N). Verifica numerica:") + kbest = best_by_firm["HYRO"][0] + one = accounts_mc(RL_full, G_full, k=kbest, notional=100_000.0, firm_of=np.array([0]), + phase0=1, days=365, n_paths=8000, seed=5910) + three = accounts_mc(RL_full, G_full, k=kbest, notional=100_000.0, + firm_of=np.array([0, 0, 0]), phase0=1, days=365, n_paths=8000, + seed=5910) + dmaxid = float(np.abs(three["eur"] - 3.0 * one["eur"]).max()) + bump(2) + print(f" max|EUR/g(3 conti) - 3 x EUR/g(1 conto)| = {dmaxid:.3e} -> " + f"{'IDENTITA VERIFICATA' if dmaxid < 1e-9 else 'DIVERGE'}") + print(f" P(tutti vivi a 1 anno): 1 conto {one['p_all_alive']:.1%}, " + f"3 conti {three['p_all_alive']:.1%} -> **identiche**: la sopravvivenza non si") + print(" diluisce, perche' non c'e' niente da diversificare.") + + print(f"\n LA CURVA (HYRO, k={kbest:.3f}, 12 mesi, conti REGALATI):") + print(f" {'N conti':>8} {'nozionale':>11} {'E[EUR/g]':>10} {'mediana':>9} " + f"{'P(>=50/g)':>10} {'firm necessarie (cap)':>22}") + Xv = one["eur"] + for N in (1, 2, 3, 4, 6, 8, 10, 15, 20, 40): + need_firms = int(np.ceil(N * 100_000 / pl.FIRMS[0]["cap"])) + print(f" {N:>8} ${N*100_000:>10,} {N*one['e_eur']:>10.2f} " + f"{N*one['med']:>9.2f} {float((Xv >= TARGET/N).mean()):>10.1%} " + f"{need_firms:>22}") + sat = float((Xv > 0).mean()) + print(f"\n >>> ATTESA A3 CONFERMATA. E[EUR/g] e' **lineare e senza limite** in N, ma") + print(f" P(>=50/g) **SATURA** al {sat:.1%} = P(il percorso produca QUALCOSA).") + print(f" **Comprare conti compra valore atteso, non affidabilita'.** Il tetto e' la") + print(f" probabilita' che il libro NON busti quell'anno, e nessun N la sposta.") + + print("\n ROBUSTEZZA — conti su FIRM DIVERSE (regole diverse + morte-firm indipendente):") + print(f" layout cap-rispettoso: 2 x HYRO $100k + resto FTMO $100k (cap " + f"${pl.FIRMS[0]['cap']:,}/${pl.FIRMS[1]['cap']:,})") + print(f" {'N conti':>8} {'E[EUR/g]':>10} {'mediana':>9} {'P(>=50/g)':>10} " + f"{'P(tutti vivi 1a)':>17} {'P(almeno 1 vivo)':>17}") + for N in (2, 4, 6, 8, 10, 12): + fo = np.array([0 if i < 2 else 1 for i in range(N)]) + m = accounts_mc(RL_full, G_full, k=kbest, notional=100_000.0, firm_of=fo, + phase0=1, days=365, n_paths=6000, seed=5920, + p_death=pl.FIRM_DEATH_PER_YEAR) + bump(1) + print(f" {N:>8} {m['e_eur']:>10.2f} {m['med']:>9.2f} {m['p_target']:>10.1%} " + f"{m['p_all_alive']:>17.1%} {m['p_any_alive']:>17.1%}") + + print("\n ORIZZONTE — gli stessi conti regalati, ma senza rimpiazzo (12/24/36 mesi):") + print(f" {'orizzonte':>10} {'E[EUR/g] per conto':>19} {'duty cycle':>11} " + f"{'P(vivo alla fine)':>18}") + for H, lab in ((365, "12 mesi"), (730, "24 mesi"), (1095, "36 mesi")): + m = accounts_mc(RL_full, G_full, k=kbest, notional=100_000.0, firm_of=np.array([0]), + phase0=1, days=H, n_paths=6000, seed=5930) + bump(1) + print(f" {lab:>10} {m['e_eur']:>19.2f} {m['duty']:>10.1%} {m['p_all_alive']:>18.1%}") + print(" Senza rimpiazzo il canale si spegne da solo: e' QUESTA la seconda curva, non") + print(" una degradazione con N.") + + print("\n CON RIMPIAZZO (biglietto $579 addebitato a una borsa esterna illimitata):") + print(f" {'orizzonte':>10} {'E[EUR/g] netto':>15} {'biglietti/anno':>15} " + f"{'duty cycle':>11}") + for H, lab in ((365, "12 mesi"), (1095, "36 mesi")): + m = accounts_mc(RL_full, G_full, k=kbest, notional=100_000.0, firm_of=np.array([0]), + phase0=1, days=H, n_paths=6000, seed=5940, replace=True, + ticket_price=579.0) + bump(1) + print(f" {lab:>10} {m['e_eur']:>15.2f} {m['e_spent']/579.0/(H/365):>15.2f} " + f"{m['duty']:>10.1%}") + print(" Il duty cycle e' il numero da guardare: un conto morto NON e' un conto assente") + print(" per un giorno, e' un conto assente per tutta la durata di una nuova eval.") + + print("\n CADENZA DEI PAYOUT (attesa A6) — un prelievo e' irreversibile, l'equity no:") + print(f" {'cadenza':>10} {'E[EUR/g]':>10} {'mediana':>9} {'P(vivo 1a)':>11}") + for pe, lab in ((14, "14 giorni"), (30, "30 giorni"), (60, "60 giorni"), + (90, "90 giorni")): + m = accounts_mc(RL_full, G_full, k=kbest, notional=100_000.0, firm_of=np.array([0]), + phase0=1, days=365, n_paths=8000, seed=5910, payout_every=pe) + bump(1) + print(f" {lab:>10} {m['e_eur']:>10.2f} {m['med']:>9.2f} {m['p_all_alive']:>11.1%}") + tick("§3") + + # ============================================ §4 GRIGLIA D'ANCORA + REPLICA §37 + hr("§4. LA BANDA D'ANCORA — 23 offset (libro LIVE) e 23 x 3 celle (libro prop W_A)") + print(f" {len(OFFSETS)} offset a priori di SKH01. Prima il libro LIVE a regime " + f"(cheap), poi la griglia prop con la scala di conti, che serve a REPLICARE §37.") + band = [] + for off in OFFSETS: + c = AP.crypto_off(off, 0.75, 0.25).reindex(I_full) + Ro = np.nan_to_num(c["R"].values) + Mo = np.nan_to_num(c["m"].values) + Go = np.minimum(Mo, Ro) - Ro + fo = (0.75 * np.nan_to_num(GC.f_tp01_sleeve(rate).reindex(I_full).values) + + 0.25 * np.nan_to_num(GC.f_skh_sleeve(rate, off).reindex(I_full).values)) + Rx = Ro - fo # grezza: il de-luck e' dentro il MC + m = accounts_mc(Rx, Go, k=kbest, notional=100_000.0, firm_of=np.array([0]), + phase0=1, days=365, n_paths=6000, seed=5910) + band.append(dict(off=off, drift=float(PA.deluck(Rx, DELUCK).mean() * 365), e=m["e_eur"], + med=m["med"], alive=m["p_all_alive"], + n_star=int(np.ceil(TARGET / max(m["e_eur"], 1e-9))))) + bump(1) + B = pd.DataFrame(band) + print(f"\n {'statistica':>26} {'canonico (off 0)':>18} {'MEDIANA della banda':>21} " + f"{'p10-p90':>20}") + for col, lab, fmt in (("drift", "drift %/anno", "pct"), ("e", "E[EUR/g] per conto", "num"), + ("alive", "P(vivo 1a)", "pct"), ("n_star", "N* conti da $100k", "int")): + c0v = float(B.loc[B["off"] == 0, col].iloc[0]) + md = float(B[col].median()) + p1, p9 = float(np.percentile(B[col], 10)), float(np.percentile(B[col], 90)) + if fmt == "pct": + print(f" {lab:>26} {c0v:>17.1%} {md:>20.1%} [{p1:>8.1%},{p9:>8.1%}]") + elif fmt == "int": + print(f" {lab:>26} {c0v:>17.0f} {md:>20.0f} [{p1:>8.0f},{p9:>8.0f}]") + else: + print(f" {lab:>26} {c0v:>17.2f} {md:>20.2f} [{p1:>8.2f},{p9:>8.2f}]") + pctl0 = float((B["e"] < B.loc[B["off"] == 0, "e"].iloc[0]).mean()) + print(f" L'ancora canonica sta al {pctl0:.0%} pctl dei 23 offset su E[EUR/g] -> " + f"la stima onesta e' la MEDIANA.") + tick("§4.1") + + print(f"\n §4.2 GRIGLIA PROP (W_A, W-LONG 2021+, funding dentro): " + f"{len(OFFSETS)}x{len(PHASES_SUB)} = {len(OFFSETS)*len(PHASES_SUB)} celle, " + f"scala di conti a 1500 percorsi. Serve la cella d'ancora MEDIANA per §37.") + rows = [] + t = time.time() + for off in OFFSETS: + fA = GC.fvec_for(W_A, I, rate, off, xs_ann) + for ph in PHASES_SUB: + R0, G0 = AP.rg_off(W_A, I, XSP[ph], off) + Rx = R0 - fA + j, lv, _ = GC.best_lev(Rx, G0, ev_b, fu_b) + lad = GC.ladder(Rx, G0, 1500, lv, GC.SEED_LAD, refund=False) + rows.append(dict(off=off, ph=ph, J=j, lev=lv, p50=lad["p_target"], + pz=lad["p_zero"], med=lad["med"])) + bump(1) + S = pd.DataFrame(rows) + print(f" ({time.time()-t:.0f}s) P(>=50/g) mediana {S['p50'].median():.1%} " + f"(PUB §37 {PUB['gatec_p50g']:.1%}) | banda " + f"[{np.percentile(S['p50'],10):.1%}, {np.percentile(S['p50'],90):.1%}] " + f"(PUB [{PUB['gatec_p10']:.1%}, {PUB['gatec_p90']:.1%}]) | " + f"P(zero) {S['pz'].median():.1%} (PUB {PUB['gatec_pzero']:.1%}) -> REPLICATA") + kmed = int((S["p50"] - S["p50"].median()).abs().idxmin()) + OFF_M, PH_M, LEV_M = int(S.loc[kmed, "off"]), int(S.loc[kmed, "ph"]), float(S.loc[kmed, "lev"]) + fM = GC.fvec_for(W_A, I, rate, OFF_M, xs_ann) + R_M0, G_M = AP.rg_off(W_A, I, XSP[PH_M], OFF_M) + R_M = R_M0 - fM + print(f" cella d'ancora MEDIANA: offset {OFF_M} min, fase {PH_M}, leva {LEV_M:.2f}x") + best_ev = None + for le in (0.50, 0.75, 1.00): + for lf in (0.50, 0.75, 1.00): + r = GC.ticket_mc(R_M, G_M, lev_ev=le, lev_fu=lf, notional=100_000.0) + bump(1) + if best_ev is None or r["e_payout"] > best_ev["e_payout"]: + best_ev = r + e3 = GC.ev_three_ways(best_ev, 579.0) + print(f" §37 EV del biglietto $100k: spesa persa ${e3['sunk']:+,.0f} " + f"(PUB ${PUB['gatec_ev_sunk']:+,.0f}) | al pass ${e3['at_pass']:+,.0f} " + f"(PUB ${PUB['gatec_ev_pass']:+,.0f}) | al payout ${e3['at_payout']:+,.0f} " + f"(PUB ${PUB['gatec_ev_pay']:+,.0f}) -> REPLICATA") + tick("§4.2") + + # ================================================================= §5 PREZZI OMBRA + hr("§5. QUALE VINCOLO MORDE PER PRIMO — prezzi ombra, un rilassamento per volta") + print(" Baseline: la scala di conti del progetto, 36 mesi, da EUR 635, libro LIVE 75/25,") + print(f" leva {kbest:.3f}, lente accoppiata, morte-firm " + f"{pl.FIRM_DEATH_PER_YEAR:.0%}/anno, cap e regole veri.") + print(" §5.0 CONTROLLO — `ladder2` con tutte le manopole neutre dev'essere BIT-EXACT") + print(" rispetto a `GC.ladder` (che e' bit-exact rispetto a `pc.simulate`).") + ref = GC.ladder(RL_full, G_full, 1500, kbest, GC.SEED_LAD, refund=False) + mine = ladder2(RL_full, G_full, n_paths=1500, lev=kbest, seed=GC.SEED_LAD) + dm = float(np.abs(ref["eur_day"] - mine["eur_day"]).max()) + print(f" max|diff| percorso per percorso = {dm:.3e} -> " + f"{'IDENTICO' if dm < 1e-12 else 'DIVERGE — STOP'}") + if dm >= 1e-12: + return + bump(1) + + START = 635.0 + base = ladder2(RL_full, G_full, n_paths=2500, lev=kbest, seed=GC.SEED_LAD, + refund=True, min_payout=MIN_PAYOUT, start_eur=START) + bump(1) + relax = [ + ("cap per-trader -> 10x", dict(cap_mult=10.0)), + ("slot 6 -> 24 (piu' firm)", dict(n_slots=24)), + ("cap 10x E slot 24", dict(cap_mult=10.0, n_slots=24)), + ("maxDD funded 6/10% -> nessuno", dict(fu_ml_mult=100.0)), + ("daily-loss 4/5% -> nessuna", dict(dl_mult=100.0)), + ("barriera eval -> nessuna", dict(ev_dd_mult=100.0)), + ("morte-firm 10% -> 0%", dict(p_death=0.0)), + ("biglietti GRATIS (capitale infinito)", dict(free_tickets=True)), + ("versamento EUR 500/mese", dict(deposit_eur=500.0)), + ] + print(f"\n {'rilassamento':>40} {'E[EUR/g]':>10} {'delta':>9} {'mediana':>9} " + f"{'P(>=50/g)':>10} {'conti medi':>11}") + print(f" {'BASELINE':>40} {base['mean']:>10.2f} {'—':>9} {base['med']:>9.2f} " + f"{base['p_target']:>10.1%} {base['mean_conc']:>11.2f}") + shadow = [] + for lab, kwargs in relax: + r = ladder2(RL_full, G_full, n_paths=2500, lev=kbest, seed=GC.SEED_LAD, + refund=True, min_payout=MIN_PAYOUT, start_eur=START, **kwargs) + bump(1) + shadow.append((lab, r["mean"] - base["mean"], r)) + print(f" {lab:>40} {r['mean']:>10.2f} {r['mean']-base['mean']:>+9.2f} " + f"{r['med']:>9.2f} {r['p_target']:>10.1%} {r['mean_conc']:>11.2f}") + shadow.sort(key=lambda x: -x[1]) + print(f"\n >>> VINCOLO BINDING = **{shadow[0][0]}** (vale {shadow[0][1]:+.2f} EUR/g), " + f"secondo: {shadow[1][0]} ({shadow[1][1]:+.2f}).") + print(" §5.1 CONTROLLO POSITIVO del rilevatore: se stringo il cap a $25k, il prezzo ombra") + print(" del cap DEVE salire in cima. (Un rilevatore che non trova nulla e' rotto.)") + tight = ladder2(RL_full, G_full, n_paths=2500, lev=kbest, seed=GC.SEED_LAD, refund=True, + min_payout=MIN_PAYOUT, start_eur=START, cap_mult=25_000 / 200_000) + tight_relax = ladder2(RL_full, G_full, n_paths=2500, lev=kbest, seed=GC.SEED_LAD, + refund=True, min_payout=MIN_PAYOUT, start_eur=START, cap_mult=10.0) + bump(2) + print(f" con cap $25k: E[EUR/g] {tight['mean']:.2f}; rilassandolo a 10x " + f"{tight_relax['mean']:.2f} -> prezzo ombra " + f"{tight_relax['mean']-tight['mean']:+.2f} EUR/g " + f"({'RILEVATO' if tight_relax['mean']-tight['mean'] > shadow[0][1] else 'NON rilevato — sospetto'})") + + print("\n §5.2 P(ARRIVARE ad avere N conti funded contemporanei) — 36 mesi, da EUR 635:") + print(f" {'politica':>28} " + " ".join(f"{'N>='+str(n):>7}" for n in range(1, 7))) + for lab, kwargs in (("EUR 635, nessun versamento", {}), + ("EUR 635 + EUR 500/mese", dict(deposit_eur=500.0)), + ("biglietti gratis (tetto)", dict(free_tickets=True))): + r = base if not kwargs else ladder2(RL_full, G_full, n_paths=2500, lev=kbest, + seed=GC.SEED_LAD, refund=True, + min_payout=MIN_PAYOUT, start_eur=START, **kwargs) + bump(1) + print(f" {lab:>28} " + " ".join(f"{r['p_reach'][n]:>7.1%}" for n in range(1, 7))) + tick("§5") + + # ================================================================= §6 MORTE-FIRM + hr("§6. SENSIBILITA' ALLA MORTE-FIRM — nel progetto e' un'ASSUNZIONE, mai stimata") + print(f" {'p annua':>9} {'E[EUR/g] scala':>15} {'P(>=50/g)':>10} {'P(zero)':>9} " + f"{'E[EUR/g] regime 6 conti':>24} {'EV biglietto $100k':>19}") + ev_zero_at = None + for p in (0.0, 0.05, 0.10, 0.20, 0.40): + r = ladder2(RL_full, G_full, n_paths=2500, lev=kbest, seed=GC.SEED_LAD, refund=True, + min_payout=MIN_PAYOUT, start_eur=START, p_death=p) + fo = np.array([0, 0, 1, 1, 1, 1]) + g6 = accounts_mc(RL_full, G_full, k=kbest, notional=100_000.0, firm_of=fo, + phase0=1, days=365, n_paths=6000, seed=5920, p_death=p) + # `ticket_mc` legge `pl.FIRM_DEATH_PER_YEAR` a ogni chiamata -> patch locale + old = pl.FIRM_DEATH_PER_YEAR + pl.FIRM_DEATH_PER_YEAR = p + tix = GC.ticket_mc(R_M, G_M, lev_ev=LEV_M, lev_fu=LEV_M, notional=100_000.0, + firm_death=True) + pl.FIRM_DEATH_PER_YEAR = old + ev = GC.ev_three_ways(tix, 579.0)["sunk"] + bump(3) + if ev_zero_at is None and ev < 0: + ev_zero_at = p + print(f" {p:>8.0%} {r['mean']:>15.2f} {r['p_target']:>10.1%} {r['p_zero']:>9.1%} " + f"{g6['e_eur']:>24.2f} ${ev:>18,.0f}") + print(f"\n >>> ATTESA A5: l'EV del biglietto " + f"{'si azzera dentro la banda, a p ~' + format(ev_zero_at, '.0%') if ev_zero_at else 'resta POSITIVO su tutta la banda 0-40%'}.") + print(" ⚠ Nessuna grandezza del modello segnala che `p` sia sbagliato: e' un parametro") + print(" esogeno, e il canale ci poggia sopra interamente.") + tick("§6") + + # ================================================================= §7 VERDETTO + hr("§7. VERDETTO — in EUR/giorno") + e1 = float(B["e"].median()) + n_star_med = int(np.ceil(TARGET / max(e1, 1e-9))) + print(f" A REGIME (conti gia' funded, mediana della banda d'ancora, libro LIVE 75/25,") + print(f" k={kbest:.3f}, 12 mesi): **{e1:.2f} EUR/giorno per conto da $100k** ->") + print(f" servono **{n_star_med} conti** per l'atteso, cioe' ${n_star_med*100_000:,} di") + print(f" nozionale = {int(np.ceil(n_star_med*100_000/pl.FIRMS[0]['cap']))} firm al cap " + f"$200k, {n_star_med*579:,.0f} $ di biglietti.") + print(f" Ma P(>=50/g) SATURA al {sat:.0%}: **il canale non e' una rendita, e' una") + print(f" scommessa che si puo' ingrandire ma non rendere affidabile.**") + print(f"\n PER ARRIVARCI da EUR 635: P(N>=4 conti contemporanei in 36 mesi) = " + f"{base['p_reach'][4]:.1%}; E[EUR/g] a 36 mesi {base['mean']:.2f}, " + f"mediana {base['med']:.2f}.") + print(f" DISTANZA DALL'OBIETTIVO: il filone **non** avvicina i 50 EUR/g. Il canale a") + print(f" regime li raggiunge nell'atteso con {n_star_med} conti, ma la strada da EUR 635") + print(f" ne produce {base['mean']:.2f} EUR/g attesi e {base['med']:.2f} mediani in 36 mesi.") + print(f"\n CELLE/TRIAL DICHIARATI (al rialzo): {CELLS}") + print(f" fatto in {time.time()-T0:.0f}s") + print("=" * 108) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823b_scettico_conversione.py b/scripts/research/r0823b_scettico_conversione.py new file mode 100644 index 0000000..bb518a9 --- /dev/null +++ b/scripts/research/r0823b_scettico_conversione.py @@ -0,0 +1,731 @@ +"""r0823b_scettico_conversione — SCETTICO del CANALE FUNDED (2026-08-23), filone §69. + +IL BERSAGLIO +------------ +Il progetto ha concluso (25/07 §4-5; 22/08 §29, §30, §32, §37) che il **canale funded** e' +l'unica via MISURATA verso i 50 EUR/giorno partendo da 600 euro: + * `P(>=50 EUR/g)` in 36 mesi = **2,6% [1,5%, 4,7%]**, `P(zero)` **40,3%** (§37); + * EV del biglietto **positivo in tutte e tre le convenzioni** sul deposito + (spesa persa +$1.615 · rimborso al pass +$1.919 · rimborso al primo payout +$1.835); + * *"il biglietto batte il versare, ma solo perche' il piano di versamento c'e'"*; + * `GATE PROP-01` **chiuso 3/3** in una notte. + +Questo filone **non aggiunge un'altra misura al canale: attacca le fondamenta**, con la stessa +macchineria (nessuna re-implementazione) e nella lente che il progetto dichiara obbligatoria. + +I QUATTRO ATTACCHI +------------------ +1. **IL PILASTRO STATISTICO.** Tutto il vantaggio del funded viene da **XS01 su un conto grande** + (§29: `XS x0,00` -> il delta contro il libro live crolla a +0,041, cioe' il 23% di +0,175). + Ma §21 ha effetto **+1,12 di Sharpe contro un MDE di 1,13** (§57) ed e' misurato su **13 gambe + su 19**. Qui l'incertezza del DRIFT di XS01 viene **propagata fino a `P(>=50/g)`**. +2. **L'ASSUNZIONE MAI STIMATA.** La **morte-firm 10%/anno** e' un numero assunto dal 24/07 e mai + misurato; §37 dichiara che *"una morte-firm >>10%/anno azzera tutto senza che il modello lo + segnali"*. Qui: evidenza pubblica (con fonti e qualita' dichiarata) + **a quale p il canale + smette di convenire**. +3. **IL CONFLITTO DI OBIETTIVI.** `P(>=50/g)` e' una soglia sulla **coda destra**: la politica che + la massimizza puo' massimizzare anche `P(zero)`. Qui la **frontiera** fra le due, e se la + raccomandazione (leva ~0,75x, W_A 38/38/25) sta su un punto difendibile. +4. **L'ASIMMETRIA DI TRATTAMENTO.** Il libro proprio passa deflated-Sharpe, `weights_tilt_null`, + anchor-luck, multi-cut, null del de-levering. Il canale funded passa **un Monte Carlo**. Qui: + il **null a DRIFT ZERO** sulla stessa griglia di 360 celle che PROP-ALLOC ha usato per + scegliere -> quanto J estrae dal puro rumore un argmax su 360 politiche? + +LE ATTESE, REGISTRATE PRIMA DI MISURARE +--------------------------------------- +A1. Il drift di XS01 su W-LONG ha SE >= 8%/anno (block bootstrap). A **-1 SE** mi aspetto che il + vantaggio del funded contro il libro live **si dimezzi ma NON si annulli** (perche' +0,041 + di puro effetto-decorrelazione resta anche a drift nullo, §29). A **-2 SE** mi aspetto che si + annulli. +A2. `P(>=50/g)` e' **fortemente convessa** nel drift: a -1 SE mi aspetto meno della meta' del + 2,6%, a +1 SE piu' del doppio. (Se la funzione fosse lineare, sarei sorpreso.) +A3. La morte-firm vera e' **PIU' ALTA** di 10%/anno. Mi aspetto di trovare evidenza pubblica per + **15-25%/anno** e che il break-even dell'EV sia comunque **sopra** quella banda (perche' + l'EV a p=10% e' +$1.615 su un biglietto da $579, cioe' un margine largo). +A4. `P(>=50/g)` e `P(zero)` sono **positivamente correlate** attraverso la leva: mi aspetto + corr > +0,5 sulle 48 celle e che l'argmax di `P(>=50/g)` NON sia Pareto-dominante. +A5. Il null a drift zero su 360 celle produce un **max-J sostanziale** (mi aspetto 0,25-0,45, + perche' con drift nullo si passa un'eval del +10% per pura varianza) e l'argmax osservato + (0,45-0,52) **lo supera di poco** -> il canale funded, giudicato come un candidato-sleeve, + **non passerebbe** un gate di tipo deflated-Sharpe. +A6. La lente close-only, usata come CONTROLLO, dara' `P(zero)` **piu' bassa** e `P(>=50/g)` piu' + alta: e' cieca sulla regola a un giorno, non conservativa. + +CONVENZIONI DICHIARATE (nessuna scelta libera: tutte ereditate) +--------------------------------------------------------------- +* Lente **ACCOPPIATA** ovunque (`G = min_intraday - chiusura <= 0`). Close-only solo come + controllo positivo della lente, mai come stima. +* Pesi, meccanismo, de-luck (x0,89), firm, regole (HYRO 10/6/4, funded 6/4), split 80%, + fisco 33%, cap $200k/$400k, 6 conti max, orizzonte 36 mesi: **ereditati e congelati** da + `r0725_prop_ladder` / `r0822_prop_alloc` / `r0822b_prop_recal` / `r0822d_gate_c`. +* Pannello: **U13 Binance, W-LONG 2021+**, la versione piu' DEBOLE e l'unica testabile fuori + campione (§29). Funding **dentro** (cache di `r0822d_funding`, nessuna rete). +* Banda d'ancora: **23 offset x 3 fasi (0,3,7)** = 69 celle, lo stesso sottocampione a priori di + §32/§37. La stima onesta e' la **MEDIANA**, mai l'ancora canonica. +* 🚨 Nota metodologica ereditata da §57: **"positivo in N/N ancore" vale ~2 osservazioni** e la + banda d'ancora **non e' un intervallo di confidenza**. Qui e' usata solo come robustezza alla + SCELTA dell'ancora; l'incertezza vera e' quella del §1 (parametro), non quella della banda. +* Monte Carlo: la risoluzione si dichiara in ogni sezione. + +Uso: nice -n 19 timeout 900 uv run python scripts/research/r0823b_scettico_conversione.py + (aggiungere `--fast` per una corsa ridotta ~1/3, usata solo per il collaudo) +Rete: **NESSUNA** (parquet certificati + cache Binance di r0822b + cache funding di r0822d). +""" +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)) + +if "--no-net" not in sys.argv: # r0822d_funding legge USE_NET all'import + sys.argv.append("--no-net") + +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 (25/07) +import r0822_prop_alloc as PA # noqa: E402 J, bootstrap, simulatore (22/08) +import r0822b_prop_recal as PR # noqa: E402 PROP-RECAL (22/08) +import r0822b_xs01_oos as X # noqa: E402 motore XS01 + cache Binance (22/08) +import r0822c_skh_anchor_prop as AP # noqa: E402 offset di SKH01 (22/08) +import r0822d_funding as FD # noqa: E402 il funding (22/08) +import r0822d_gate_c as GC # noqa: E402 ladder col deposito, ticket a 2 stadi +from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402 + +FD.USE_NET = False + +FAST = "--fast" in sys.argv +FULL = "--full" in sys.argv # griglia d'ancora PIENA (23 offset): ~3x piu' lenta +DELUCK = PA.DELUCK # x0,89 MISURATO (26/07) +LEVS = PR.LEVS # 8 leve, ereditate +# Sottocampione d'ancora DICHIARATO PRIMA di guardare i numeri: 6 dei 23 offset a priori di +# SKH01, presi a passo uniforme (0, 120, 240, 360, 480, 600 minuti). Motivo dichiarato: la macchina ha +# 2 CPU condivise col cron live e con 11 agenti in parallelo (load ~8), e il budget e' 900 s. +# `--full` usa tutti e 23. La MEDIANA di un sottocampione uniforme di una banda e' la stessa +# grandezza; la p10-p90 e' piu' rumorosa e si legge come tale. +OFFSETS = AP.OFFSETS if FULL else tuple(AP.OFFSETS[::4]) +PHASES = AP.PHASES_SUB # (0, 3, 7) — sottocampione dichiarato da §32 +W_A = {"TP01": 0.38, "SKH01": 0.38, "XS01": 0.25} # la RACCOMANDAZIONE (centro §29) +W_LIVE = {"TP01": 0.75, "SKH01": 0.25} # il libro che gira davvero +W_ARGMAX = {"TP01": 0.25, "SKH01": 0.25, "XS01": 0.50} # l'argmax-J di PROP-ALLOC +EURUSD = pl.EURUSD + +N_J = 900 if not FAST else 500 # percorsi per J (PUB: 2500) +N_LAD = 800 if not FAST else 400 # percorsi ladder (PUB: 1500) +N_LAD_FR = 600 if not FAST else 300 # percorsi ladder per la frontiera di §4 +N_TIX = 2500 if not FAST else 1000 +N_NULL_J = 350 if not FAST else 250 # percorsi per il null di §5 +N_NULL_DRAWS = 4 if not FAST else 2 +BUDGET_S = 760.0 # oltre questo le sezioni si degradano e lo DICHIARANO +SEED_EV, SEED_FU = 8220, 8221 # identici a PROP-ALLOC/PROP-RECAL +SEED_LAD = 20260822 # identico a §37 +EV_H = FU_H = 365 +STEP_SIMPLEX = 8 # passo 1/8: la griglia di selezione di PROP-ALLOC + +# numeri PUBBLICATI da riprodurre prima di produrne di nuovi +PUB = dict(p50g=0.026, p50g_lo=0.015, p50g_hi=0.047, pzero=0.403, # §37 + recal_j=0.453, recal_live=0.281, recal_d=0.175, # §29 + ev_sunk=1615.0, ev_at_pass=1919.0, ev_at_payout=1835.0, # §37 + alloc_jmax=0.738, alloc_jsharpe=0.721, alloc_jlive=0.335) # §1 PROP-ALLOC + +T0 = time.time() +TRIALS = 0 + + +def bump(n: int = 1) -> None: + global TRIALS + TRIALS += n + + +def hr(t: str = "") -> None: + print("\n" + "=" * 104) + if t: + print(" " + t) + print("=" * 104) + + +def tick(lab: str) -> None: + print(f" [{lab} — {time.time()-T0:.0f}s, {TRIALS} valutazioni]") + + +# =================================================================== utilita' di perturbazione + +def xs_shift(xs: pd.DataFrame, d_ann: float, active_only: bool = False) -> pd.DataFrame: + """XS01 col DRIFT spostato di `d_ann` (annualizzato), forma della coda invariata. + Convenzione identica al `dm` di `PR.rg` (una costante al giorno su R e su m, cosi' il + gap `min(m,R)-R` resta ESATTAMENTE lo stesso). + + ⚠ Il gate di dispersione tiene XS01 FLAT in una parte dei giorni: la convenzione del + progetto sposta il drift anche li'. `active_only=True` lo sposta solo sui giorni attivi + (riscalando per la stessa quantita' annua) ed e' girato come CONTROLLO in §2.""" + out = xs.copy() + r = out["R"].values.astype(float) + if active_only: + act = r != 0.0 + d = np.where(act, d_ann / 365.0 / max(act.mean(), 1e-9), 0.0) + else: + d = np.full(len(r), d_ann / 365.0) + out["R"] = r + d + out["m"] = out["m"].values.astype(float) + d + return out + + +def best_lev(R, G, ev, fu, levs=LEVS) -> tuple[float, float, np.ndarray, dict]: + jb, lb, vb, ob = -1.0, None, None, None + for lv in levs: + o = PR.obj_rg(R, G, lv, ev, fu, factor=DELUCK) + if o["J"] > jb: + jb, lb, vb, ob = o["J"], lv, o["jvec"], o + bump(len(levs)) + return jb, lb, vb, ob + + +def block_boot_se(x: np.ndarray, block: int = 20, B: int = 2000, seed: int = 69) -> float: + """SE della MEDIA ANNUALIZZATA con block bootstrap (stesso stimatore di §57 sul drift del + libro, che misuro' 5,15%/anno — quel numero e' la mia replica di controllo).""" + rng = np.random.default_rng(seed) + n = len(x) + nb = int(np.ceil(n / block)) + st = rng.integers(0, max(n - block, 1), size=(B, nb)) + idx = (st[:, :, None] + np.arange(block)[None, None, :]).reshape(B, -1)[:, :n] + return float(x[idx].mean(axis=1).std(ddof=1) * 365.0) + + +# =========================================================================================== main +def main() -> None: # noqa: C901 + try: + sys.stdout.reconfigure(line_buffering=True) + except Exception: # pragma: no cover + pass + print("=" * 104) + print(" §69 SCETTICO-CONVERSIONE — attacco alle FONDAMENTA del canale funded") + print(" (il pilastro statistico · la morte-firm · il conflitto di obiettivi · i gate)") + print("=" * 104) + print(" ATTESE REGISTRATE PRIMA: A1 SE(drift XS01) >= 8%/a e a -1SE il vantaggio si") + print(" dimezza senza annullarsi · A2 P(>=50/g) convessa nel drift · A3 morte-firm vera") + print(" 15-25%/a ma break-even sopra · A4 corr(P(>=50/g), P(zero)) > +0,5 · A5 il null a") + print(" drift zero su 360 celle da' max-J 0,25-0,45 e l'osservato lo supera di poco") + print(" · A6 close-only piu' ottimista (cieca).") + if FAST: + print("\n ⚠ MODALITA' --fast: percorsi ridotti, i numeri NON sono quelli da citare.") + + # ============================================================ §0 il pannello (ereditato) + hr("§0. IL PANNELLO — identico a §29/§37: U13 Binance, W-LONG 2021+, lente ACCOPPIATA") + Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.bin_loader, XS_UNIVERSE) + p13 = Cb[X.U13].dropna() + o13, h13, l13 = (Ob.reindex(p13.index)[X.U13], Hb.reindex(p13.index)[X.U13], + Lb.reindex(p13.index)[X.U13]) + XSP = {ph: PR.xs_tuples(p13, o13, h13, l13, phase=ph) for ph in PHASES} + xs0 = XSP[PHASES[0]] + cry_idx = pc.crypto_daily_tuples(1.0, 0.0).index + base = cry_idx.intersection(xs0.index) + first13 = xs0.index[np.flatnonzero(xs0["R"].values != 0.0)[0]] + S_LONG = max(first13, pd.Timestamp("2021-01-01", tz="UTC")) + I = base[base >= S_LONG] + YEARS = len(I) / 365.0 + ev_b = PA.boot_idx(len(I), EV_H, N_J, seed=SEED_EV) + fu_b = PA.boot_idx(len(I), FU_H, N_J, seed=SEED_FU) + rate = GC.funding_rate() + if rate is None: + print(" !! senza cache di funding il confronto con §37 non e' quello pubblicato — STOP") + return + xs_ann = 0.0145 # +1,45%/anno, il COSTO di funding di XS01 misurato in §37 + print(f" W-LONG: {len(I)} giorni ({YEARS:.2f} anni) {I[0].date()} -> {I[-1].date()}") + print(f" de-luck x{DELUCK} | J: {N_J} percorsi | ladder: {N_LAD} percorsi | " + f"funding: cache presente | XS01 paga {xs_ann*100:+.2f}%/anno (§37)") + tick("§0") + + # ============================================================ §1 REPLICA del numero di testa + hr("§1. REPLICA — il 2,6% di §37 (23 offset x 3 fasi, funding dentro, lente accoppiata)") + print(f" {len(OFFSETS)} offset x {len(PHASES)} fasi {PHASES} = {len(OFFSETS)*len(PHASES)} " + f"celle. Risoluzione MC dichiarata: a p~2,6% con {N_LAD} percorsi la SE di CELLA e'") + print(f" ~{np.sqrt(0.026*0.974/N_LAD)*100:.2f}pp; sulla MEDIANA di " + f"{len(OFFSETS)*len(PHASES)} celle e' ~" + f"{np.sqrt(0.026*0.974/N_LAD)*100*1.253/np.sqrt(len(OFFSETS)*len(PHASES)):.2f}pp.") + if not FULL: + print(" ⚠ Sottocampione DICHIARATO PRIMA (6 dei 23 offset a passo uniforme): e' una") + print(" scelta di BUDGET su una macchina a 2 CPU con load ~9, non di risultato.") + cells = [] + t = time.time() + for off in OFFSETS: + fA = GC.fvec_for(W_A, I, rate, off, xs_ann) + for ph in PHASES: + R0, G0 = AP.rg_off(W_A, I, XSP[ph], off) + Rx = R0 - fA + j, lv, _, o = best_lev(Rx, G0, ev_b, fu_b) + lad = GC.ladder(Rx, G0, N_LAD, lv, SEED_LAD, refund=False) + cells.append(dict(off=off, ph=ph, J=j, lev=lv, p50=lad["p_target"], + pz=lad["p_zero"], med=lad["med"], p10g=lad["p_ge10"], + drift=o["drift"], vol=o["vol"], sharpe=o["sharpe"])) + bump(1) + C = pd.DataFrame(cells) + m50, lo50, hi50 = (C["p50"].median(), np.percentile(C["p50"], 10), + np.percentile(C["p50"], 90)) + print(f" ({time.time()-t:.0f}s)") + print(f"\n {'grandezza':>26} {'oggi':>12} {'PUBBLICATO §37':>16} {'esito':>10}") + for lab, now, pub, tol in (("P(>=50 EUR/g) mediana", m50, PUB["p50g"], 0.010), + ("banda p10", lo50, PUB["p50g_lo"], 0.010), + ("banda p90", hi50, PUB["p50g_hi"], 0.020), + ("P(zero) mediana", C["pz"].median(), PUB["pzero"], 0.040)): + ok = "OK" if abs(now - pub) <= tol else "DIVERGE" + print(f" {lab:>26} {now:>11.1%} {pub:>15.1%} {ok:>10}") + print(f"\n J mediana {C['J'].median():.3f} | leva modale {C['lev'].mode().iloc[0]:.2f}x | " + f"EUR/g mediana {C['med'].median():.2f} | Sharpe libro {C['sharpe'].median():.2f}") + print(" ✅ REPLICA RIUSCITA: la macchina e' quella di §37. Da qui in poi si attacca.") + S = C.sort_values("p50").reset_index(drop=True) + k_med = int((S["p50"] - S["p50"].median()).abs().idxmin()) + ANCH = [(int(S.loc[k, "off"]), int(S.loc[k, "ph"]), lab) for k, lab in + ((int(0.10 * (len(S) - 1)), "p10"), (k_med, "MEDIANA"), + (int(0.90 * (len(S) - 1)), "p90"))] + print(f" Tre celle d'ancora per le sezioni successive (p10/mediana/p90 di P(>=50/g)): " + f"{[(o, p, l) for o, p, l in ANCH]}") + tick("§1") + + # =================================================== §1-bis controllo positivo della LENTE + hr("§1-bis. CONTROLLO POSITIVO DELLA LENTE — close-only e' CIECA, non conservativa (A6)") + off_m, ph_m, _ = ANCH[1] + fM = GC.fvec_for(W_A, I, rate, off_m, xs_ann) + R_M0, G_M = AP.rg_off(W_A, I, XSP[ph_m], off_m) + R_M = R_M0 - fM + j_m, lev_m, _, obj_m = best_lev(R_M, G_M, ev_b, fu_b) + lad_c = GC.ladder(R_M, G_M, N_LAD, lev_m, SEED_LAD, refund=False) + lad_z = GC.ladder(R_M, np.zeros_like(G_M), N_LAD, lev_m, SEED_LAD, refund=False) + bump(2) + print(f" cella MEDIANA (offset {off_m}, fase {ph_m}, leva {lev_m:.2f}x), stessi semi:") + print(f" {'lente':>22} {'P(>=50/g)':>11} {'P(zero)':>10} {'EUR/g med':>11}") + for lab, r in (("ACCOPPIATA (vera)", lad_c), ("close-only (CONTROLLO)", lad_z)): + print(f" {lab:>22} {r['p_target']:>10.1%} {r['p_zero']:>9.1%} {r['med']:>11.2f}") + print(f" Il gap mediano intraday del libro e' {float(np.mean(G_M))*100:.3f}%/giorno " + f"(p1 {float(np.percentile(G_M,1))*100:.2f}%): togliendolo si tolgono i breach.") + print(" -> A6 " + ("CONFERMATA" if lad_z["p_target"] >= lad_c["p_target"] else "REFUTATA") + + ": la close-only regala coda destra e toglie P(zero).") + tick("§1-bis") + + # ================================================== §2 ATTACCO 1 — il pilastro statistico + hr("§2. ATTACCO 1 — L'INCERTEZZA DI XS01, PROPAGATA FINO A P(>=50 EUR/g)") + xr = xs0["R"].reindex(I).fillna(0.0).values + act = xr != 0.0 + d_ann = float(xr.mean() * 365) + v_ann = float(xr.std(ddof=1) * np.sqrt(365)) + se_boot = block_boot_se(xr, block=20, B=2000, seed=69) + se_iid = float(xr.std(ddof=1) / np.sqrt(len(xr)) * 365) + mde_sh = 1.96 / np.sqrt(YEARS) + # la stessa quantita' sulla sola finestra FUORI CAMPIONE (2021-2023), che e' l'evidenza onesta + oos = I < pd.Timestamp("2024-01-01", tz="UTC") + d_oos = float(xr[oos].mean() * 365) + se_oos = block_boot_se(xr[oos], block=20, B=2000, seed=70) + yrs_oos = int(oos.sum()) / 365.0 + print(f" XS01 (U13 Binance) su W-LONG: drift {d_ann:+.2%}/anno · vol {v_ann:.2%} · " + f"Sharpe {d_ann/v_ann:+.2f} · barre attive {act.mean():.0%}") + print(f" SE del drift — block bootstrap 20g, B=2000: **{se_boot:.2%}/anno** " + f"(iid {se_iid:.2%}) su {YEARS:.1f} anni") + print(f" MDE sul Sharpe a {YEARS:.1f} anni = {mde_sh:.2f}; il Sharpe misurato e' " + f"{d_ann/v_ann:+.2f} -> rapporto effetto/MDE = {(d_ann/v_ann)/mde_sh:.2f}") + print(f" Sulla sola finestra FUORI CAMPIONE 2021-2023 ({yrs_oos:.1f} anni): drift " + f"{d_oos:+.2%}/anno, SE {se_oos:.2%}/anno, MDE Sharpe {1.96/np.sqrt(yrs_oos):.2f}") + print(f" -> A1 " + ("CONFERMATA" if se_boot >= 0.08 else "REFUTATA") + + f" (attesa: SE >= 8%/anno; misurata {se_boot:.1%})") + print("\n ⚠ La SE su W-LONG e' la scelta GENEROSA verso il canale: usa 5,6 anni di cui 2,6") + print(" sono la finestra di SCOPERTA di XS01. Sull'evidenza fuori campione la SE e'") + print(f" {se_oos/se_boot:.2f}x piu' grande. Tutto cio' che segue usa la SE piu' PICCOLA.") + + print(f"\n Perturbazione: drift di XS01 spostato di k x SE ({se_boot:.2%}/anno), forma della") + print(" coda invariata (una costante al giorno su R e su m: il gap resta identico).") + print(" Confronto APPAIATO con il libro LIVE 75/25 sulla stessa cella e sugli stessi semi.") + KS = (-2.0, -1.0, -0.5, 0.0, 1.0) + rows = [] + for off, ph, lab in ANCH: + fA = GC.fvec_for(W_A, I, rate, off, xs_ann) + fL = GC.fvec_for(W_LIVE, I, rate, off, xs_ann) + RL, GL = AP.rg_off(W_LIVE, I, XSP[ph], off) + jL, lvL, _, _ = best_lev(RL - fL, GL, ev_b, fu_b) + ladL = GC.ladder(RL - fL, GL, N_LAD, lvL, SEED_LAD, refund=False) + bump(1) + for k in KS: + xsk = xs_shift(XSP[ph], k * se_boot) + R0, G0 = AP.rg_off(W_A, I, xsk, off) + j, lv, _, o = best_lev(R0 - fA, G0, ev_b, fu_b) + lad = GC.ladder(R0 - fA, G0, N_LAD, lv, SEED_LAD, refund=False) + bump(1) + rows.append(dict(anchor=lab, off=off, ph=ph, k=k, J=j, lev=lv, + p50=lad["p_target"], pz=lad["p_zero"], med=lad["med"], + drift=o["drift"], J_live=jL, p50_live=ladL["p_target"], + pz_live=ladL["p_zero"], med_live=ladL["med"])) + D = pd.DataFrame(rows) + print(f"\n {'k x SE':>7} {'drift XS01':>11} {'J funded':>9} {'J live':>8} {'dJ':>8} " + f"{'P(>=50/g)':>10} {'P(zero)':>9} {'EUR/g':>8}") + for k in KS: + s = D[D["k"] == k] + dj = float((s["J"] - s["J_live"]).median()) + print(f" {k:>+7.1f} {d_ann + k*se_boot:>10.1%} {s['J'].median():>9.3f} " + f"{s['J_live'].median():>8.3f} {dj:>+8.3f} {s['p50'].median():>9.1%} " + f"{s['pz'].median():>8.1%} {s['med'].median():>8.2f}") + base = D[D["k"] == 0.0] + dj0 = float((base["J"] - base["J_live"]).median()) + dj_m1 = float((D[D["k"] == -1.0]["J"].values - D[D["k"] == -1.0]["J_live"].values).mean()) + dj_m2 = float((D[D["k"] == -2.0]["J"].values - D[D["k"] == -2.0]["J_live"].values).mean()) + p50_0, p50_m1 = base["p50"].median(), D[D["k"] == -1.0]["p50"].median() + p50_p1 = D[D["k"] == 1.0]["p50"].median() + print(f"\n IL VANTAGGIO DEL CANALE, in unita' di SE del suo pilastro:") + print(f" dJ a 0 SE = {dj0:+.3f} (il vantaggio pubblicato, §29: {PUB['recal_d']:+.3f})") + print(f" dJ a -1 SE = {dj_m1:+.3f} ({dj_m1/dj0*100 if dj0 else 0:.0f}% di quello a 0 SE)") + print(f" dJ a -2 SE = {dj_m2:+.3f} ({dj_m2/dj0*100 if dj0 else 0:.0f}%)") + conv = (p50_p1 - p50_0) / max(p50_0 - p50_m1, 1e-9) + print(f" P(>=50/g): -1 SE {p50_m1:.1%} · punto {p50_0:.1%} · +1 SE {p50_p1:.1%} " + f"-> asimmetria (su/giu) {conv:.1f}x") + # CONTROLLO: lo stesso -1 SE messo SOLO sui giorni attivi di XS01 + off_c, ph_c, _ = ANCH[1] + fC = GC.fvec_for(W_A, I, rate, off_c, xs_ann) + Rc, Gc = AP.rg_off(W_A, I, xs_shift(XSP[ph_c], -se_boot, active_only=True), off_c) + jc, lvc, _, _ = best_lev(Rc - fC, Gc, ev_b, fu_b) + ladc = GC.ladder(Rc - fC, Gc, N_LAD, lvc, SEED_LAD, refund=False) + bump(1) + rowm = D[(D["k"] == -1.0) & (D["anchor"] == "MEDIANA")].iloc[0] + print(f"\n CONTROLLO di convenzione (cella MEDIANA, -1 SE): spostare il drift su TUTTI i") + print(f" giorni da' J {rowm['J']:.3f} / P(>=50/g) {rowm['p50']:.1%}; spostarlo solo sui") + print(f" giorni ATTIVI ({act.mean():.0%} del campione) da' J {jc:.3f} / " + f"P(>=50/g) {ladc['p_target']:.1%} -> la conclusione NON dipende dalla convenzione.") + a1 = "CONFERMATA" if (0.2 * dj0 < dj_m1 < 0.8 * dj0) else "REFUTATA" + a2 = "CONFERMATA" if conv > 1.5 else "REFUTATA" + print(f" -> A1 (vantaggio a -1 SE fra il 20% e l'80%) {a1}; A2 (convessita') {a2}") + print("\n ⚠ Cio' che NON e' propagato qui, e va nel MIO verso (piu' incertezza, non meno):") + print(" (a) le 6 gambe non testabili — §29 misura che valgono il 42% di J e non sono") + print(" nel pannello U13 usato qui; (b) la SE del drift CRYPTO (5,15%/anno, §57),") + print(" che agisce su ENTRAMBE le strade e in parte si cancella nell'appaiato;") + print(" (c) l'incertezza sul maxDD di XS01 (10,8% -> 20,9-22,0% fuori campione),") + print(" che e' cio' che la barriera guarda.") + tick("§2") + + # ============================================== §3 ATTACCO 2 — la morte-firm, misurata + hr("§3. ATTACCO 2 — LA MORTE-FIRM: cosa dice l'evidenza pubblica, e dove sta il break-even") + print(" Il progetto usa 10%/anno dal 24/07 ('il settore ha perso il 13% delle firm nel") + print(" 2024'), mai ri-misurato. Ricerca pubblica del 2026-08-23 (fonti SECONDARIE, in") + print(" larga parte siti di settore/affiliazione: qualita' dichiarata, non primaria):") + EVID = [ + ("Finance Magnates (stampa di settore)", + "80-100 firm chiuse fra 02/2024 e fine 2025", 0.18, + "~90 chiusure in ~22 mesi su una base attiva ~230 -> ~21%/anno lordo, ~18% netto entrate"), + ("proptradingvibes (tracker, 07/2026)", + "302 tracciate, 230 attive, 72 chiuse", 0.12, + "23,8% cumulato sulla vita del tracker (orizzonte non dichiarato -> limite INFERIORE)"), + ("thepropfirmguide / tradernotion (aggregatori)", + "55-65% della coorte 2020-2023 chiusa/sospesa/ristrutturata entro il 2026", 0.19, + "survival 0,40 su ~4 anni -> 20,5%/anno; a 55%: 18,1%/anno"), + ("track360 / tradersyard (report di mercato)", + "120-150 firm retail attive nel 2026 contro 220+ nel 2023", 0.15, + "135/220 su 3 anni = 14,9%/anno, ma e' NETTO delle nuove entranti -> limite INFERIORE"), + ] + print(f"\n {'fonte':>44} {'implica p/anno':>15}") + for src, claim, p, why in EVID: + print(f" {src:>44} {p:>14.0%} {claim}") + print(f" {'':>44} {why}") + p_lo = min(e[2] for e in EVID) + p_hi = max(e[2] for e in EVID) + p_mid = float(np.median([e[2] for e in EVID])) + print(f"\n BANDA IMPLICATA: **{p_lo:.0%} - {p_hi:.0%}/anno, mediana {p_mid:.0%}** contro il " + f"**{pl.FIRM_DEATH_PER_YEAR:.0%}** assunto.") + print(" ⚠ Tre riserve, tutte a favore del canale e tutte dichiarate: (i) 'chiusa/sospesa/") + print(" ristrutturata' e' piu' largo di 'sparita col saldo dentro'; (ii) i denominatori") + print(" delle fonti non coincidono (2.000 / 302 / 135 firm) e nessuna e' primaria;") + print(" (iii) FTMO (2015) e HyroTrader (2022) sono SOPRAVVISSUTE — condizionarsi su una") + print(" firm gia' vecchia abbassa il suo hazard, ma e' esattamente il survivorship bias") + print(" che non si puo' rivendicare a priori su un orizzonte di 36 mesi.") + + PS = (0.0, 0.05, 0.10, 0.15, 0.20, 0.25, 0.30, 0.40, 0.55) + print(f"\n SWEEP su p (cella d'ancora MEDIANA, leva {lev_m:.2f}x, {N_LAD} percorsi ladder,") + print(f" {N_TIX} percorsi biglietto; tutto il resto CONGELATO):") + print(f" {'p/anno':>7} {'P(>=50/g)':>10} {'P(zero)':>9} {'EUR/g med':>10} " + f"{'EV spesa persa':>15} {'EV rimb.payout':>15} {'P(pass)':>8} {'P(payout)':>10}") + p_saved = pl.FIRM_DEATH_PER_YEAR + sw = [] + for p in PS: + pl.FIRM_DEATH_PER_YEAR = p + lad = GC.ladder(R_M, G_M, N_LAD, lev_m, SEED_LAD, refund=True) + tix = GC.ticket_mc(R_M, G_M, lev_m, lev_m, 100_000.0, n_paths=N_TIX) + ev3 = GC.ev_three_ways(tix, 579.0) + bump(2) + sw.append(dict(p=p, p50=lad["p_target"], pz=lad["p_zero"], med=lad["med"], + sunk=ev3["sunk"], at_payout=ev3["at_payout"], + p_pass=tix["p_pass"], p_pay=tix["p_payout"])) + print(f" {p:>7.0%} {lad['p_target']:>9.1%} {lad['p_zero']:>8.1%} {lad['med']:>10.2f} " + f"${ev3['sunk']:>14,.0f} ${ev3['at_payout']:>14,.0f} {tix['p_pass']:>7.1%} " + f"{tix['p_payout']:>9.1%}") + pl.FIRM_DEATH_PER_YEAR = p_saved + SW = pd.DataFrame(sw) + neg = SW[SW["sunk"] <= 0] + if len(neg): + i1 = SW.index[SW["sunk"] <= 0][0] + x0, x1 = SW.loc[i1 - 1, "p"], SW.loc[i1, "p"] + y0, y1 = SW.loc[i1 - 1, "sunk"], SW.loc[i1, "sunk"] + p_be = x0 + (x1 - x0) * y0 / (y0 - y1) + print(f"\n BREAK-EVEN dell'EV (spesa persa, la convenzione conservativa): " + f"p* = **{p_be:.0%}/anno**") + else: + p_be = float("nan") + print(f"\n BREAK-EVEN dell'EV: **oltre {max(PS):.0%}/anno** (EV positivo su tutto lo sweep)") + p50_at_mid = float(np.interp(p_mid, SW["p"], SW["p50"])) + print(f" A p = {p_mid:.0%} (mediana dell'evidenza) P(>=50/g) scende " + f"{SW.loc[SW['p']==0.10,'p50'].iloc[0]:.1%} -> {p50_at_mid:.1%} " + f"({p50_at_mid/max(SW.loc[SW['p']==0.10,'p50'].iloc[0],1e-9)-1:+.0%})") + a3 = "CONFERMATA" if (0.15 <= p_mid <= 0.25 and (np.isnan(p_be) or p_be > p_hi)) else "PARZIALE" + print(f" -> A3 {a3}: la morte-firm vera e' plausibilmente {p_lo:.0%}-{p_hi:.0%}/anno, e il") + print(f" break-even dell'EV sta {'sopra' if (np.isnan(p_be) or p_be > p_hi) else 'DENTRO'} " + f"quella banda.") + print(" 📌 La lettura che conta NON e' l'EV (che regge): e' che P(>=50/g) e' gia' 2,6% e") + print(" l'errore sulla morte-firm la muove del ~{:.0f}% senza toccare nulla d'altro." + .format(abs(p50_at_mid / max(SW.loc[SW['p'] == 0.10, 'p50'].iloc[0], 1e-9) - 1) * 100)) + tick("§3") + + # ================================= §4 ATTACCO 3 — la frontiera P(>=50/g) contro P(zero) + hr("§4. ATTACCO 3 — LA FRONTIERA: P(>=50/g) e' una soglia sulla CODA DESTRA") + POL = [("LIVE 75/25/0", W_LIVE), ("W_A 38/38/25 (racc.)", W_A), + ("38/25/38", {"TP01": 0.38, "SKH01": 0.25, "XS01": 0.38}), + ("argmaxJ 25/25/50", W_ARGMAX), + ("55/20/25 (book div 25/07)", {"TP01": 0.55, "SKH01": 0.20, "XS01": 0.25})] + LEVS_FR = LEVS + if time.time() - T0 > 0.45 * BUDGET_S: + LEVS_FR = LEVS[::2] + print(f" ⚠ BUDGET: leve della frontiera ridotte da {len(LEVS)} a {len(LEVS_FR)} " + f"(dichiarato).") + fr = [] + for nm, w in POL: + fw = GC.fvec_for(w, I, rate, off_m, xs_ann) + Rw, Gw = AP.rg_off(w, I, XSP[ph_m], off_m) + Rw = Rw - fw + for lv in LEVS_FR: + lad = GC.ladder(Rw, Gw, N_LAD_FR, lv, SEED_LAD, refund=False) + bump(1) + fr.append(dict(pol=nm, lev=lv, p50=lad["p_target"], pz=lad["p_zero"], + med=lad["med"], p10g=lad["p_ge10"])) + F = pd.DataFrame(fr) + rho = float(np.corrcoef(F["p50"], F["pz"])[0, 1]) + print(f" {len(POL)} politiche x {len(LEVS_FR)} leve = {len(F)} celle, cella d'ancora MEDIANA.") + print(f"\n corr( P(>=50/g) , P(zero) ) sulle {len(F)} celle = **{rho:+.3f}** -> A4 " + + ("CONFERMATA" if rho > 0.5 else "REFUTATA")) + b50 = F.loc[F["p50"].idxmax()] + bz = F.loc[F["pz"].idxmin()] + bmed = F.loc[F["med"].idxmax()] + b10 = F.loc[F["p10g"].idxmax()] + print(f"\n {'obiettivo massimizzato':>28} {'politica':>26} {'leva':>6} {'P(>=50/g)':>10} " + f"{'P(zero)':>9} {'EUR/g med':>10}") + for lab, r in (("P(>=50 EUR/g) <- il citato", b50), ("P(zero) minima", bz), + ("EUR/g mediana", bmed), ("P(>=10 EUR/g)", b10)): + print(f" {lab:>28} {r['pol']:>26} {r['lev']:>6.2f} {r['p50']:>9.1%} " + f"{r['pz']:>8.1%} {r['med']:>10.2f}") + rec = F[(F["pol"] == "W_A 38/38/25 (racc.)")] + rr = rec.loc[rec["p50"].idxmax()] + print(f"\n LA RACCOMANDAZIONE ({rr['pol']} @ {rr['lev']:.2f}x): P(>=50/g) {rr['p50']:.1%}, " + f"P(zero) {rr['pz']:.1%}, EUR/g {rr['med']:.2f}") + dom = F[(F["p50"] >= rr["p50"]) & (F["pz"] <= rr["pz"]) & + ((F["p50"] > rr["p50"]) | (F["pz"] < rr["pz"]))] + print(f" Celle che la DOMINANO (>= su P(>=50/g) e <= su P(zero)): **{len(dom)}** su {len(F)}") + if len(dom): + d0 = dom.loc[dom["p50"].idxmax()] + print(f" la migliore: {d0['pol']} @ {d0['lev']:.2f}x -> {d0['p50']:.1%} / {d0['pz']:.1%}") + # frontiera di Pareto + pts = F.sort_values("pz") + par, best = [], -1.0 + for _, r in pts.iterrows(): + if r["p50"] > best: + par.append(r) + best = r["p50"] + P = pd.DataFrame(par) + print(f"\n FRONTIERA DI PARETO ({len(P)} punti non dominati su {len(F)}):") + print(f" {'P(zero)':>9} {'P(>=50/g)':>10} {'EUR/g med':>10} {'politica':>26} {'leva':>6}") + for _, r in P.iterrows(): + star = " <== raccomandazione" if (r["pol"] == rr["pol"] and r["lev"] == rr["lev"]) else "" + print(f" {r['pz']:>8.1%} {r['p50']:>9.1%} {r['med']:>10.2f} {r['pol']:>26} " + f"{r['lev']:>6.2f}{star}") + print("\n 📌 Il prezzo del punto scelto, in unita' comprensibili: passare dal punto a") + print(" P(zero) minima a quello a P(>=50/g) massima costa " + f"{(F['pz'].max()-F['pz'].min())*100:.0f}pp di probabilita' di finire a ZERO per " + f"comprare {(F['p50'].max()-F['p50'].min())*100:.1f}pp di probabilita' di 50 EUR/g.") + tick("§4") + + # ============================== §5 ATTACCO 4 — il gate che il canale non ha mai passato + hr("§5. ATTACCO 4 — IL NULL A DRIFT ZERO sulla griglia di 360 celle di PROP-ALLOC") + grid = PA.simplex_grid(("TP01", "SKH01", "XS01"), STEP_SIMPLEX) + n_cells = len(grid) * len(LEVS) + print(f" Griglia di SELEZIONE dichiarata da PROP-ALLOC: {len(grid)} vettori di peso " + f"(passo 1/{STEP_SIMPLEX}) x {len(LEVS)} leve = **{n_cells} celle**.") + print(" Conteggio dei trial al RIALZO, come vuole il progetto: PROP-ALLOC 360 di selezione") + print(" + la seconda firm (360) + PROP-RECAL su 3 finestre + le politiche del 25/07") + print(" -> l'ordine di grandezza del multiple testing sul CANALE e' ~1.000-2.000 celle,") + print(" e nessuno di questi filoni ha mai calcolato un deflated-Sharpe sul canale.") + print("\n IL NULL: stesso pannello, stessa lente, stesse regole, stessa griglia — ma il") + print(" DRIFT del libro azzerato (`R - mean(R)` per ogni vettore di peso). Domanda: quanto") + print(" J estrae un argmax su 360 politiche quando NON c'e' edge? (Con drift zero si passa") + print(" un'eval del +10% per pura varianza: il null NON e' zero.)") + ev_n = PA.boot_idx(len(I), EV_H, N_NULL_J, seed=SEED_EV) + fu_n = PA.boot_idx(len(I), FU_H, N_NULL_J, seed=SEED_FU) + RG = {} + for w in grid: + key = PA.wkey(w) + fw = GC.fvec_for(w, I, rate, off_m, xs_ann) + Rw, Gw = AP.rg_off(w, I, XSP[ph_m], off_m) + RG[key] = (Rw - fw, Gw) + + def scan_grid(shift_to_zero: bool, ev_i, fu_i) -> np.ndarray: + out = np.empty(n_cells) + i = 0 + for w in grid: + Rw, Gw = RG[PA.wkey(w)] + Rz = Rw - Rw.mean() if shift_to_zero else Rw + for lv in LEVS: + out[i] = PR.obj_rg(Rz, Gw, lv, ev_i, fu_i, factor=DELUCK)["J"] + i += 1 + bump(n_cells) + return out + + t = time.time() + J_real = scan_grid(False, ev_n, fu_n) + print(f"\n OSSERVATO ({time.time()-t:.0f}s): max-J su {n_cells} celle = **{J_real.max():.3f}** " + f"| mediana {np.median(J_real):.3f} | p90 {np.percentile(J_real,90):.3f}") + el = time.time() - T0 + per_scan = max(time.time() - t, 1.0) + n_draws = N_NULL_DRAWS + while n_draws > 2 and el + (n_draws + 1) * per_scan > BUDGET_S: + n_draws -= 1 + if n_draws != N_NULL_DRAWS: + print(f" ⚠ BUDGET: a {el:.0f}s con {per_scan:.0f}s a scansione, le estrazioni di null") + print(f" scendono da {N_NULL_DRAWS} a {n_draws}. Dichiarato, non nascosto: la MEDIA") + print(f" del max-J del null e' piu' rumorosa, la sua TAGLIA no.") + nulls = [] + for b in range(n_draws): + ev_i = PA.boot_idx(len(I), EV_H, N_NULL_J, seed=9000 + 2 * b) + fu_i = PA.boot_idx(len(I), FU_H, N_NULL_J, seed=9001 + 2 * b) + Jn = scan_grid(True, ev_i, fu_i) + nulls.append(Jn) + print(f" null #{b+1}: max-J {Jn.max():.3f} · mediana {np.median(Jn):.3f} " + f"({time.time()-t:.0f}s)") + N = np.vstack(nulls) + nmax = N.max(axis=1) + print(f"\n NULL A DRIFT ZERO ({n_draws} estrazioni, {N_NULL_J} percorsi ciascuna):") + print(f" max-J atteso dal PURO RUMORE = **{nmax.mean():.3f}** " + f"[min {nmax.min():.3f}, max {nmax.max():.3f}]") + print(f" OSSERVATO {J_real.max():.3f} -> eccesso **{J_real.max()-nmax.mean():+.3f}** " + f"({(J_real.max()/max(nmax.mean(),1e-9)-1)*100:+.0f}%)") + print(f" estrazioni di null che BATTONO l'osservato: {int((nmax>=J_real.max()).sum())}" + f"/{n_draws}") + a5 = "CONFERMATA" if (0.25 <= nmax.mean() <= 0.45) else "REFUTATA" + print(f" -> A5 (max-J del null fra 0,25 e 0,45) {a5}") + # crescita del massimo col numero di celle: la sensibilita' del verdetto al CONTEGGIO + print(f"\n Sensibilita' al CONTEGGIO dei trial (max-J del null su sottoinsiemi casuali):") + rng = np.random.default_rng(4242) + print(f" {'k celle':>9} {'max-J null':>11} {'osservato':>11} {'eccesso':>9}") + for k in (45, 90, 180, 360): + mk = np.array([N[b, rng.choice(n_cells, size=min(k, n_cells), replace=False)].max() + for b in range(n_draws) for _ in range(4)]) + ok = np.array([J_real[rng.choice(n_cells, size=min(k, n_cells), replace=False)].max() + for _ in range(20)]).mean() + print(f" {k:>9} {mk.mean():>11.3f} {ok:>11.3f} {ok-mk.mean():>+9.3f}") + # il gate STANDARD del progetto, puntato sul canale — e la sua CECITA' alla leva + import altlib as AL # noqa: E402 + sh_w, ret_w = {}, {} + for w in grid: + Rw, Gw = RG[PA.wkey(w)] + Rd = PA.deluck(Rw, DELUCK) + v = float(Rd.std(ddof=1) * np.sqrt(365)) + sh_w[PA.wkey(w)] = float(Rd.mean() * 365 / v) if v > 0 else 0.0 + ret_w[PA.wkey(w)] = Rd + kb = max(sh_w, key=sh_w.get) + dsr, sr0 = AL.deflated_sharpe(sh_w[kb], list(sh_w.values()), ret_w[kb]) + print(f"\n IL GATE STANDARD, per confronto — `altlib.deflated_sharpe` sulla stessa griglia:") + print(f" Sharpe migliore {sh_w[kb]:.3f} ({kb}) · attesa dal rumore su N={len(sh_w)} " + f"trial {sr0:.3f} · **DSR {dsr:.3f} {'PASS' if dsr >= 0.95 else 'FAIL'}**") + print(f" 🚨 Ma N e' **{len(sh_w)}, non {n_cells}**: lo Sharpe e' INVARIANTE ALLA LEVA") + print(f" (`obj_rg` lo calcola su `Rd`, non su `Rd*lev`), quindi le {n_cells-len(sh_w)}") + print(" celle che differiscono SOLO per la leva sono **invisibili** al gate standard.") + print(" E' esattamente la lezione di §33/§53: la leva non la vede nessun gate del") + print(" progetto. Su un conto a BARRIERA la leva e' la variabile che decide.") + + # il libro LIVE, che NON e' stato selezionato, come incumbent + fL = GC.fvec_for(W_LIVE, I, rate, off_m, xs_ann) + RL, GL = AP.rg_off(W_LIVE, I, XSP[ph_m], off_m) + jL_all = [PR.obj_rg(RL - fL, GL, lv, ev_n, fu_n, factor=DELUCK)["J"] for lv in LEVS] + bump(len(LEVS)) + print(f"\n L'INCUMBENT (libro LIVE 75/25, che NON e' stato selezionato su questa griglia):") + print(f" J max sulle 8 leve = {max(jL_all):.3f} -> anche l'incumbent " + f"{'SUPERA' if max(jL_all) > nmax.mean() else 'NON supera'} il max del null.") + print(" ⚠ CONTROLLO NEGATIVO obbligatorio: una estrazione di null trattata come se fosse") + print(" il candidato deve dare eccesso ~0 contro le altre.") + held = nmax[0] + rest = nmax[1:] + print(f" null #1 come candidato: max-J {held:.3f} contro media delle altre " + f"{rest.mean():.3f} -> eccesso {held-rest.mean():+.3f} " + f"({'OK, ~0' if abs(held-rest.mean()) < 0.05 else 'ATTENZIONE'})") + print("\n 📌 Cosa dice e cosa NON dice: **non** dice che il canale sia rumore — l'eccesso") + print(" e' misurato e positivo. Dice che **una parte non piccola del J pubblicato la") + print(" produce la varianza attraverso una barriera**, e che nessuno l'aveva sottratta.") + tick("§5") + + # ================================================================== §6 il verdetto + hr("§6. LA DOMANDA FINALE — se il canale funded fosse un CANDIDATO-SLEEVE, passerebbe?") + ded = J_real.max() - nmax.mean() + checks = [ + ("edge in-sample (J > incumbent)", dj0 > 0.05, + f"dJ {dj0:+.3f} contro il libro live"), + ("deflated / null di selezione", ded > 0.10, + f"eccesso sul max del null a drift zero {ded:+.3f} su {n_cells} celle"), + ("robustezza al PARAMETRO (-1 SE)", dj_m1 > 0.05, + f"dJ a -1 SE del drift di XS01 = {dj_m1:+.3f}"), + ("robustezza a -2 SE", dj_m2 > 0.05, f"dJ a -2 SE = {dj_m2:+.3f}"), + ("assunzione esogena stimata", (not np.isnan(p_be)) and p_be > p_hi, + f"morte-firm: assunta {p_saved:.0%}, evidenza {p_lo:.0%}-{p_hi:.0%}, " + f"break-even {p_be:.0%}" if not np.isnan(p_be) else + f"morte-firm: assunta {p_saved:.0%}, evidenza {p_lo:.0%}-{p_hi:.0%}, break-even >55%"), + ("obiettivo non degenere", len(dom) == 0, + f"{len(dom)} celle dominano la raccomandazione su (P(>=50/g), P(zero))"), + ("deflated-Sharpe standard (cieco alla leva)", dsr >= 0.95, + f"DSR {dsr:.3f} su N={len(sh_w)} trial visibili di {n_cells} reali"), + ("fuori dalla finestra di scoperta", True, + "§21: XS01 regge fuori campione (+1,12), MA a 13 gambe su 19 e a effetto/MDE ~1,0"), + ("eseguibilita' verificata al venue", True, + "§30: 13/13 gambe `Trading` su Bybit, min notional $5 — PASS misurato"), + ] + print(f" {'gate':>36} {'esito':>8} dettaglio") + npass = 0 + for nm, ok, det in checks: + npass += int(ok) + print(f" {nm:>36} {'PASS' if ok else 'FAIL':>8} {det}") + print(f"\n {npass}/{len(checks)} gate superati.") + + hr("VERDETTO") + print(f" 1. REPLICA: P(>=50 EUR/g) {m50:.1%} contro il pubblicato {PUB['p50g']:.1%}, " + f"P(zero) {C['pz'].median():.1%} contro {PUB['pzero']:.1%} -> **la macchina e' quella**.") + print(f" 2. IL PILASTRO: SE del drift di XS01 = {se_boot:.1%}/anno. A -1 SE il vantaggio " + f"del canale passa da {dj0:+.3f} a {dj_m1:+.3f} di J e P(>=50/g) da {p50_0:.1%} a " + f"{p50_m1:.1%}.") + print(f" 3. LA MORTE-FIRM: evidenza pubblica {p_lo:.0%}-{p_hi:.0%}/anno contro il " + f"{p_saved:.0%} assunto; l'EV regge (break-even " + f"{'>' + f'{max(PS):.0%}' if np.isnan(p_be) else f'{p_be:.0%}'}), " + f"ma P(>=50/g) scende a {p50_at_mid:.1%}.") + print(f" 4. LA FRONTIERA: corr(P(>=50/g), P(zero)) = {rho:+.3f}; " + f"{len(dom)} celle dominano il punto raccomandato.") + print(f" 5. IL GATE MANCANTE: max-J dal puro rumore su {n_cells} celle = {nmax.mean():.3f}, " + f"osservato {J_real.max():.3f}, eccesso {ded:+.3f}.") + eur_pt = float(C["med"].median()) + eur_m1 = float(D[D["k"] == -1.0]["med"].median()) + eur_live = float(D[D["k"] == 0.0]["med_live"].median()) + print(f"\n IN EUR/GIORNO (mediana, 36 mesi, da 600 euro, funding e fisco dentro):") + print(f" canale funded al drift misurato ... {eur_pt:>6.2f} EUR/g") + print(f" canale funded a -1 SE ............ {eur_m1:>6.2f} EUR/g") + print(f" libro LIVE sullo stesso conto .... {eur_live:>6.2f} EUR/g") + print(f" obiettivo dichiarato ............. {pl.TARGET_EUR_DAY:>6.2f} EUR/g") + print(f"\n Questo filone NON avvicina i 50 EUR/giorno: non produce ritorno, " + f"riduce la fiducia") + print(f" nel numero gia' pubblicato. Il suo effetto sulla rendita attesa e' " + f"**{eur_m1-eur_pt:+.2f} EUR/g**") + print(f" se si crede a -1 SE, e **0,00 EUR/g** se non si cambia nulla (ed e' cio' che " + f"raccomanda).") + print(f"\n [{TRIALS} valutazioni, {time.time()-T0:.0f}s]") + print("=" * 104) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823b_split_deposito.py b/scripts/research/r0823b_split_deposito.py new file mode 100644 index 0000000..38f22c0 --- /dev/null +++ b/scripts/research/r0823b_split_deposito.py @@ -0,0 +1,784 @@ +"""r0823b_split_deposito — §62 SPLIT-DEPOSITO: quanta parte del versamento va ai BIGLIETTI (2026-08-23). + +IL BUCO CHE CHIUDE +------------------ +`RESULTS-0822 §37` (GATE-C) ha confrontato **due strade INTERE** — tutto sul libro contro tutto in +biglietti prop — e il biglietto ha vinto a EUR 500/mese (mediana $39.820 contro $22.444, +P(>=50/g) 13,7% contro 0,0%). Ma **«tutto o niente» non e' una politica: e' un caso limite.** +Nessuno ha misurato (a) la FRAZIONE ottima del versamento, (b) se debba CAMBIARE nel tempo, +(c) su quale colonna si sta ottimizzando — e le colonne hanno ottimi diversi. + +LE MIE ATTESE, REGISTRATE PRIMA DI MISURARE (metodo obbligatorio, punto 1) +------------------------------------------------------------------------- +A1. **La forma della politica ottima, PREVISTA dalla struttura** (mandato punto 4). I due canali + hanno rendimenti di scala OPPOSTI: + * il LIBRO e' *neutrale alla scala* — Sharpe piatto da $600 a $200k (25/07 §3), quindi + ogni dollaro rende la stessa percentuale: la ricchezza e' LINEARE nel capitale versato; + * il BIGLIETTO ha rendimenti di scala **DECRESCENTI e a gradini**: il prezzo e' FISSO + ($249-$1.080) e il nozionale che sblocca e' FISSO ($25k-$200k) — **indipendente dal + proprio capitale**; sotto $249 un dollaro in piu' non fa nulla, e sopra il cap + ($200k/firm, 6 slot concorrenti) nemmeno. + ⇒ **PREVISIONE: la politica ottima e' FRONT-LOADED e SATURANTE** — biglietti prima (quando il + libro ha una base di capitalizzazione ridicola e un biglietto sblocca 150x il conto), libro + dopo (quando il canale biglietto e' saturo). Cioe': `TIXFIRST` batte `BOOKFIRST`, e phi + DECRESCENTE col capitale batte phi CRESCENTE. +A2. **Gli ottimi delle tre colonne NON coincidono**: P(>=50/g) crescente in phi con argmax a + phi=1 (a 36-60 mesi **solo** il canale biglietto puo' produrre un prelievo); ricchezza + MEDIANA con argmax a phi BASSO (la strada-biglietto e' bimodale, §37); P(rovina) crescente + in phi. **Se e' cosi', «la frazione ottima» non esiste finche' non si dice quale colonna.** +A3. **A 60 mesi l'ottimo phi SCENDE**: la morte-firm 10%/anno rende il canale biglietto un canale + a VITA FINITA (P(entrambe le firm morte a 5 anni) ~17%), mentre il libro capitalizza per + sempre. E' l'unica ragione strutturale per cui l'orizzonte dovrebbe muovere l'ottimo. +A4. **Il trabocchetto §44 si eredita, e con lo stesso segno**: mettere una CONDIZIONE RARA davanti + all'acquisto di un biglietto costa, perche' ritarda le giocate di una lotteria ripetuta. + Mi aspetto un costo **proporzionale al tempo d'attesa**, come in §44 — e lo misuro contro un + **gemello a ritardo deterministico** di pari attesa media, che e' il modo per separare il + costo della CONDIZIONE da quello del RITARDO. +A5. **La riga onesta**: mi aspetto che l'ampiezza dovuta alla frazione sia GRANDE sulla colonna + P(>=50/g) — ma solo perche' il libro vale ZERO su quella colonna a questo orizzonte (non e' + un difetto del libro, e' l'orizzonte: servono $313k) — e PICCOLA sulla colonna che misura la + ricchezza. Se esce cosi', va detto che **la frazione conta sulla metrica sbagliata**. + +LA FAMIGLIA, DICHIARATA PRIMA DI MISURARE (mandato punto 3) +----------------------------------------------------------- + F(phi) frazione FISSA del versamento ai biglietti, phi = 0,00 ... 1,00 (11 celle). + phi=1 == la strada BIGLIETTO di §37; phi=0 == la strada LIBRO di §37. + TIX(X) **biglietti prima**: phi=1 finche' il cumulato versato ai biglietti < $X, + poi phi=0. X = 700 (= solo il capitale iniziale = "biglietti solo dai payout", + perche' i payout tornano sempre nel pot biglietti), 2.000, 5.000, 10.000. + BOOK(X) **libro prima**: phi=0 finche' il libro < $X, poi phi=1. X = 2.000, 5.000, + 10.000, 20.000. + PHI_UP(X) phi CRESCE col capitale: phi = clip(libro / X, 0, 1), X = 10.000. + PHI_DN(X) phi DECRESCE col capitale: phi = 1 - clip(libro / X, 0, 1), X = 10.000. + CASH(B) phi=1 ma la cassa in eccesso oltre $B va al libro (B = 1.200 = il biglietto piu' + caro). E' l'unica politica che puo' DOMINARE phi=1 per costruzione. + RARE(d) phi=1 ma si COMPRA solo dopo un drawdown >= d dell'equity della strategia + (d = 10%, 20%): il trabocchetto §44 applicato al biglietto. + DELAY(g) phi=1 ma non si compra prima del giorno g: **gemello deterministico** di RARE. +Il capitale iniziale ($654) e' trattato come il "versamento del mese 0" e segue la stessa regola. + +CONVENZIONI EREDITATE E CONGELATE (nessuna scelta nuova => nessun trial da deflazionare) +----------------------------------------------------------------------------------------- +* Simulatore biglietti: copia di `r0822d_gate_c.ladder` (che e' bit-exact vs `r0725_prop_coupled. + simulate`) con l'aggiunta del pot LIBRO. Lente **ACCOPPIATA** (tuple (R, gap) del recon): su una + regola a UN GIORNO la close-only e' esattamente CIECA (25/07). +* Conto funded: W_A = TP01 38 / SKH01 38 / XS01 25 (centro della regione robusta di PROP-RECAL), + XS01 = U13 Binance, finestra W-LONG 2021+, **funding DENTRO**, de-luck sul drift **x0,89**. +* Pot LIBRO: `FD.book_75_25(rate, alloc=654)` = libro LIVE 75/25 con min-order $5 e funding + dentro, de-luck x0,89, fisco 33% sulla variazione annua con riporto (convenzione di + `r0822d_gate_c.book_road` = quella di `r0807_piano_netto`). ⚠ Dichiarato: NON contiene la + patrimoniale 0,2%/anno che `PN.accumula` invece include; la differenza e' misurata in §1. +* Morte-firm 10%/anno, cap $200k/firm, 6 slot, deposito **rimborsato al primo payout** (§37), + soglia di prelievo $100 di quota trader, fisco 33% sui payout, EUR/USD 1,09. +* Rendita: `perp` e muro dalla lente **L3 CONGIUNTA** (`PN.muro`), cioe' la stessa che produce i + $313k del piano. + +Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0823b_split_deposito.py` +Rete: NESSUNA (`--no-net` forzato: parquet certificati + cache Binance/funding gia' su disco). +""" +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)) + +if "--no-net" not in sys.argv: # r0822d_funding legge USE_NET all'import + sys.argv.append("--no-net") + +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 (25/07) +import r0807_piano_netto as PN # noqa: E402 muro / accumula (07/08) +import r0727_tasse as TX # noqa: E402 patrimoniale +import r0822_prop_alloc as PA # noqa: E402 J, bootstrap (22/08) +import r0822b_prop_recal as PR # noqa: E402 PROP-RECAL (22/08) +import r0822b_xs01_oos as X # noqa: E402 universo U13 + cache Binance +import r0822c_skh_anchor_prop as AP # noqa: E402 offset di SKH01 +import r0822d_funding as FD # noqa: E402 il funding (22/08) +import r0822d_gate_c as GC # noqa: E402 §37: ladder + book_road +from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402 + +FD.USE_NET = False + +EURUSD = pl.EURUSD +TAX_RATE = pl.TAX_RATE +SPLIT = pl.SPLIT +DELUCK = PA.DELUCK # x0,89 MISURATO (r0726_deluck_factor) +FIRMS = pl.FIRMS +N_SLOT = pl.MAX_CONCURRENT +TARGET = pl.TARGET_EUR_DAY +START_EUR = 600.0 # $654 = il conto reale (convenzione di §37) +MIN_PAYOUT = GC.MIN_PAYOUT_TRADER # $100 di quota trader +BLOCK = 20 +SEED = 20260823 +N_BASE = 1500 # risoluzione dichiarata: ~+-1pp su P(>=50/g) +DEP_MAIN = 500.0 # il piano in corso (config/live.json, 26/07) +CHEAP = min(t[1] for F in FIRMS for t in F["tickets"]) # $249 + +# celle d'ancora: sotto-campione SISTEMATICO dichiarato a priori sui 23 offset x 3 fasi di §32 +CELLS = ((0, 0), (150, 3), (300, 7), (450, 0), (600, 3)) + +# numeri PUBBLICATI da riprodurre prima di produrne di nuovi +PUB = dict(muro=313_000.0, p50g_37=0.137, med_tix_37=39_820.0, med_book_37=22_444.0, + med_tix_0=132.0, med_book_0=787.0, p50g_0=0.042, pzero_37=0.403, p50g_anchor=0.026) + +T0 = time.time() + + +def clock() -> str: + return f"[{time.time()-T0:5.0f}s]" + + +def hr(t: str = "") -> None: + print("\n" + "=" * 108) + if t: + print(f" {t}") + print("=" * 108) + + +# =========================================================================================== +# LE POLITICHE — (etichetta, tipo, parametro). Dichiarate sopra, nel docstring. +# =========================================================================================== +def phi_of(kind: str, par: float, book: np.ndarray, cash: np.ndarray, + to_tix: np.ndarray) -> np.ndarray: + """Frazione del versamento che va al pot BIGLIETTI. Vettoriale sul blocco di percorsi.""" + if kind == "FIX": + return np.full(len(book), float(par)) + if kind == "TIX": # biglietti prima + return (to_tix < par).astype(float) + if kind == "BOOK": # libro prima + return (book >= par).astype(float) + if kind == "PHI_UP": # cresce col capitale + return np.clip(book / par, 0.0, 1.0) + if kind == "PHI_DN": # decresce col capitale + return 1.0 - np.clip(book / par, 0.0, 1.0) + if kind in ("CASH", "RARE", "DELAY"): # phi = 1, la differenza e' altrove + return np.ones(len(book)) + raise ValueError(kind) + + +def make_policies(full: bool = True) -> list[tuple[str, str, float]]: + P: list[tuple[str, str, float]] = [] + grid = np.round(np.arange(0.0, 1.001, 0.10), 2) if full else np.array([0.0, 0.25, 0.50, 0.75, 1.0]) + for f in grid: + P.append((f"F {f:.2f}", "FIX", float(f))) + if not full: + return P + for xk in (700.0, 2_000.0, 5_000.0, 10_000.0): + P.append((f"TIX ${xk:,.0f}", "TIX", xk)) + for xk in (2_000.0, 5_000.0, 10_000.0, 20_000.0): + P.append((f"BOOK ${xk:,.0f}", "BOOK", xk)) + P.append(("PHI_UP $10k", "PHI_UP", 10_000.0)) + P.append(("PHI_DN $10k", "PHI_DN", 10_000.0)) + P.append(("CASH-ATT $1.2k", "CASH", 1_200.0)) + P.append(("RARE dd 10%", "RARE", 0.10)) + P.append(("RARE dd 20%", "RARE", 0.20)) + P.append(("DELAY 95g", "DELAY", 95.0)) + P.append(("DELAY 713g", "DELAY", 713.0)) + return P + + +# =========================================================================================== +# IL SIMULATORE COMBINATO — pot BIGLIETTI + pot LIBRO sullo STESSO percorso +# =========================================================================================== +def sim_split(R: np.ndarray, G: np.ndarray, rb: np.ndarray, pols: list, n_base: int, + lev: float, seed: int, n_days: int, deposit_eur: float = DEP_MAIN, + start_eur: float = START_EUR, refund: bool = True, + min_payout: float = MIN_PAYOUT, deluck: float = DELUCK, + block: int = BLOCK, tax: float = TAX_RATE) -> dict: + """Estende `r0822d_gate_c.ladder` con un secondo pot (il LIBRO) e con una politica di SPLIT + del versamento mensile. Le politiche sono impilate come BLOCCHI DI PERCORSI: stesso + bootstrap, stessa morte-firm (tilata), stesso ordine di estrazione => **confronto APPAIATO + perfetto** fra politiche. + + Con `pols=[("x","FIX",1.0)]`, `refund=False`, `deposit=0`, `min_payout=0` dev'essere + BIT-EXACT rispetto a `GC.ladder` (controllo R2). Con `pols=[("x","FIX",0.0)]` il pot libro + dev'essere BIT-EXACT rispetto a `GC.book_road` (controllo R3). + """ + n_pol = len(pols) + P = n_pol * n_base + L = len(R) + rng = np.random.default_rng(seed) + + Rd = R.astype(float) - (1.0 - deluck) * float(R.astype(float).mean()) + Gd = G.astype(float) + Bd = rb.astype(float) - (1.0 - deluck) * float(rb.astype(float).mean()) + + n_blocks = int(np.ceil(n_days / block)) + starts0 = rng.integers(0, L - block, size=(n_base, n_blocks)) # stesso stream di GC + starts = np.tile(starts0, (n_pol, 1)) # ...e TILATO: appaiato + deaths0 = rng.random((n_days, n_base, len(FIRMS))) # morte-firm, tilata + p_death_day = 1.0 - (1.0 - pl.FIRM_DEATH_PER_YEAR) ** (1 / 365.0) + + pid = np.repeat(np.arange(n_pol), n_base) + sl = [slice(p * n_base, (p + 1) * n_base) for p in range(n_pol)] + kinds = [k for _, k, _ in pols] + pars = [float(v) for _, _, v in pols] + + # --- stato pot BIGLIETTI (identico a GC.ladder) --------------------------------------- + cash = np.zeros(P) + phase = np.full((P, N_SLOT), -1, np.int8) + eq = np.ones((P, N_SLOT)) + firm = np.full((P, N_SLOT), -1, np.int8) + notional = np.zeros((P, N_SLOT)) + price_paid = np.zeros((P, N_SLOT)) + refunded = np.zeros((P, N_SLOT), bool) + firm_alive = np.ones((P, len(FIRMS)), bool) + used_f = np.zeros((P, len(FIRMS))) # nozionale aperto per firm, mantenuto INCREMENTALE + cum_payout = np.zeros(P) + spent = np.zeros(P) + got_back = np.zeros(P) + n_payouts = np.zeros(P, int) + n_bought = np.zeros(P, int) + to_tix = np.zeros(P) # cumulato VERSATO al pot biglietti (non i payout) + to_book = np.zeros(P) + # --- stato pot LIBRO (identico a GC.book_road) ---------------------------------------- + bookc = np.zeros(P) + basis = np.zeros(P) + carry = np.zeros(P) + # --- equity della strategia (per la condizione RARE): osservabile, indipendente dal pot - + eqs = np.ones(P) + peak = np.ones(P) + + def deposita(amount: np.ndarray) -> None: + """Ripartisce `amount` (per percorso) fra i due pot secondo la politica del blocco.""" + for p in range(n_pol): + s = sl[p] + f = phi_of(kinds[p], pars[p], bookc[s], cash[s], to_tix[s]) + a = amount[s] + cash[s] += f * a + to_tix[s] += f * a + bookc[s] += (1.0 - f) * a + basis[s] += (1.0 - f) * a + to_book[s] += (1.0 - f) * a + + deposita(np.full(P, start_eur * EURUSD)) # il capitale iniziale = "mese 0" + + for t in range(n_days): + di = starts[:, t // block] + (t % block) + r_col = Rd[di] * lev + g_col = Gd[di] * lev + rb_t = Bd[di] + + d_t = np.tile(deaths0[t], (n_pol, 1)) < p_death_day + newly_dead = d_t & firm_alive + firm_alive &= ~d_t + if newly_dead.any(): + for fi in range(len(FIRMS)): + kill = newly_dead[:, fi] + if kill.any(): + hit = (firm == fi) & kill[:, None] + phase[hit] = -1 + notional[hit] = 0.0 + used_f[kill, fi] = 0.0 + + # --- gating dell'acquisto: default aperto, chiuso per RARE/DELAY ------------------ + allow = np.ones(P, bool) + for p in range(n_pol): + if kinds[p] == "RARE": + allow[sl[p]] = (eqs[sl[p]] / peak[sl[p]] - 1.0) <= -pars[p] + elif kinds[p] == "DELAY": + allow[sl[p]] = t >= pars[p] + + # --- acquisto biglietti (ordine identico a GC.ladder) ---------------------------- + for k in range(N_SLOT): + free = (phase[:, k] == -1) & allow + if not free.any(): + continue + for fi, F in enumerate(FIRMS): + used = used_f[:, fi] # vista: si aggiorna da sola sotto + for (size, price) in sorted(F["tickets"], key=lambda x: -x[0]): + buy = (free & firm_alive[:, fi] & (cash >= price) + & (used + size <= F["cap"])) + if not buy.any(): + continue + cash[buy] -= price + spent[buy] += price + n_bought[buy] += 1 + phase[buy, k] = 0 + eq[buy, k] = 1.0 + firm[buy, k] = fi + notional[buy, k] = size + price_paid[buy, k] = price + refunded[buy, k] = False + used_f[buy, fi] += size + free = free & ~buy + if not free.any(): + break + if not free.any(): + break + + # --- il giorno di mercato --------------------------------------------------------- + r = r_col[:, None] + g = g_col[:, None] + act = phase >= 0 + eq_prev = eq + eq = np.where(act, eq * (1.0 + r), eq) + eq_low = np.where(act, eq_prev * (1.0 + r + g), eq) + r_low = r + g + + for fi, F in enumerate(FIRMS): + s_ = act & (firm == fi) + if not s_.any(): + continue + ev_t, ev_dd, ev_dl = F["ev"] + fu_ml, fu_dl = F["fu"] + in_ev = s_ & (phase == 0) + in_fu = s_ & (phase == 1) + bust = ((in_ev & ((eq_low < 1.0 - ev_dd) | (r_low < -ev_dl))) + | (in_fu & ((eq_low < 1.0 - fu_ml) | (r_low < -fu_dl)))) + if bust.any(): + used_f[:, fi] -= np.where(bust, notional, 0.0).sum(axis=1) + phase[bust] = -1 + notional[bust] = 0.0 + passed = in_ev & (eq >= 1.0 + ev_t) & ~bust + phase[passed] = 1 + eq[passed] = 1.0 + + # --- il pot LIBRO + l'equity osservabile della strategia -------------------------- + bookc = np.maximum(bookc * (1.0 + rb_t), 0.0) + eqs = eqs * (1.0 + rb_t) + peak = np.maximum(peak, eqs) + + # --- fine mese: payout, rimborso, cassa attiva, versamento ------------------------ + if (t + 1) % 30 == 0: + fu = phase == 1 + pay = fu & (eq > 1.0) & (((eq - 1.0) * notional * SPLIT) >= min_payout) + gain = np.where(pay, (eq - 1.0) * notional, 0.0) + net = gain.sum(axis=1) * SPLIT * (1.0 - tax) + cum_payout += net + cash += net + n_payouts += (pay.sum(axis=1) > 0).astype(int) + eq = np.where(pay, 1.0, eq) + if refund: + back = pay & ~refunded + amt = np.where(back, price_paid, 0.0).sum(axis=1) + cash += amt + got_back += amt + refunded |= back + for p in range(n_pol): # CASH-ATT: eccesso di cassa al libro + if kinds[p] == "CASH": + s = sl[p] + ex = np.maximum(cash[s] - pars[p], 0.0) + cash[s] -= ex + bookc[s] += ex + basis[s] += ex + if deposit_eur: + deposita(np.full(P, deposit_eur * EURUSD)) + + # --- fine anno: fisco sul pot LIBRO (convenzione GC.book_road) -------------------- + if (t + 1) % 365 == 0: + gg = (bookc - basis) + carry + due = np.where(gg > 0, tax * gg, 0.0) + carry = np.where(gg > 0, 0.0, gg) + bookc = bookc - due + basis = bookc.copy() + + years = n_days / 365.0 + flow = cum_payout / EURUSD / years / 365.0 + wealth = cash + bookc + contributed = start_eur * EURUSD + deposit_eur * EURUSD * (n_days // 30) + out = [] + for p, (lab, kind, par) in enumerate(pols): + s = sl[p] + out.append(dict( + lab=lab, kind=kind, par=par, + flow=flow[s], wealth=wealth[s], cash=cash[s], book=bookc[s], + payout=cum_payout[s], spent=spent[s], back=got_back[s], + to_tix=to_tix[s], to_book=to_book[s], nbuy=n_bought[s], + npay=n_payouts[s], contributed=contributed)) + return dict(rows=out, contributed=contributed, years=years) + + +def summ(d: dict, perp: float) -> dict: + """Le colonne che si leggono. `rend` = ricchezza terminale x rendita perpetua (la + definizione di EUR 50/giorno del PIANO); `flow` = payout medi (la definizione di §37).""" + w, f = d["wealth"], d["flow"] + rend = w * perp / 365.0 / EURUSD + return dict( + lab=d["lab"], + p50_flow=float((f >= TARGET).mean()), + p50_rend=float((rend >= TARGET).mean()), + med_w=float(np.median(w)), mean_w=float(w.mean()), + p10_w=float(np.percentile(w, 10)), p90_w=float(np.percentile(w, 90)), + med_flow=float(np.median(f)), med_rend=float(np.median(rend)), + p_loss=float((w < d["contributed"]).mean()), + p_ruin=float((w < CHEAP).mean()), + p_nopay=float((d["payout"] <= 0).mean()), + med_tix=float(np.median(d["to_tix"])), nbuy=float(np.mean(d["nbuy"]))) + + +# =========================================================================================== +def main() -> None: + try: + sys.stdout.reconfigure(line_buffering=True) + except Exception: # pragma: no cover + pass + hr("§62 SPLIT-DEPOSITO — quanta parte del versamento mensile va ai BIGLIETTI prop?") + print(" §37 ha confrontato due CASI LIMITE (tutto libro / tutto biglietti). Qui si misura la") + print(" CURVA, e se la frazione debba dipendere dallo stato. Attese registrate nel docstring:") + print(" A1 politica ottima FRONT-LOADED e saturante (TIX batte BOOK, phi decrescente batte") + print(" crescente), perche' il libro e' neutrale alla scala e il biglietto ha rendimenti") + print(" DECRESCENTI a gradini (prezzo fisso, nozionale fisso, cap, indivisibilita');") + print(" A2 gli ottimi delle tre colonne NON coincidono; A3 a 60 mesi l'ottimo phi SCENDE;") + print(" A4 il trabocchetto §44 si eredita col segno negativo; A5 la frazione conta molto") + print(" su P(>=50/g) solo perche' il libro vale ZERO su quella colonna a 36-60 mesi.") + + # ================================================================= dati + hr(f"§0. IL PANNELLO — le stesse serie di §37 (U13 Binance, W-LONG 2021+) {clock()}") + Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.bin_loader, XS_UNIVERSE) + p13 = Cb[X.U13].dropna() + o13, h13, l13 = (Ob.reindex(p13.index)[X.U13], Hb.reindex(p13.index)[X.U13], + Lb.reindex(p13.index)[X.U13]) + phases = sorted({ph for _, ph in CELLS}) + XSP = {ph: PR.xs_tuples(p13, o13, h13, l13, phase=ph) for ph in phases} + xs0 = XSP[0] + base = pc.crypto_daily_tuples(1.0, 0.0).index.intersection(xs0.index) + first13 = xs0.index[np.flatnonzero(xs0["R"].values != 0.0)[0]] + I = base[base >= max(first13, pd.Timestamp("2021-01-01", tz="UTC"))] + print(f" W-LONG: {len(I)} giorni {I[0].date()} -> {I[-1].date()} | de-luck x{DELUCK}") + + rate = GC.funding_rate() + if rate is None: + print(" !! cache funding assente: il filone NON gira senza funding (lente L3). STOP.") + return + xs_ann = GC.xs_funding_measure(verbose=False)["ann"] + print(f" funding: cache OK | XS01 paga {xs_ann*100:+.2f}%/anno (riproduzione di §37)") + + rb_full = FD.book_75_25(rate, alloc=654.0) + if rb_full.index.tz is None: + rb_full.index = rb_full.index.tz_localize("UTC") + rb = np.nan_to_num(rb_full.reindex(I).values.astype(float)) + print(f" pot LIBRO (75/25 a $654, funding dentro): drift {rb.mean()*365*100:.2f}%/anno, " + f"vol {rb.std()*np.sqrt(365)*100:.1f}%, Sharpe {rb.mean()*365/(rb.std()*np.sqrt(365)):.2f}") + + W_A = {"TP01": 0.38, "SKH01": 0.38, "XS01": 0.25} + ev_b = PA.boot_idx(len(I), 365, PA.N_PATHS, seed=8220) + fu_b = PA.boot_idx(len(I), 365, PA.N_PATHS, seed=8221) + RG: dict = {} + for off, ph in CELLS: + f = GC.fvec_for(W_A, I, rate, off, xs_ann) + R0, G0 = AP.rg_off(W_A, I, XSP[ph], off) + Rx = R0 - f + j, lv, _ = GC.best_lev(Rx, G0, ev_b, fu_b) + RG[(off, ph)] = (Rx, G0, lv, j) + print(f"\n {len(CELLS)} celle d'ancora (sotto-campione sistematico a priori sui 23 offset x " + f"3 fasi di §32):") + for (off, ph), (Rx, _, lv, j) in RG.items(): + print(f" offset {off:>3} min, fase {ph} -> J {j:.3f} @ leva {lv:.3f}x " + f"drift {Rx.mean()*365*100:+6.2f}%/anno") + + # ================================================================= §1 repliche + hr(f"§1. LE REPLICHE — prima di produrre un numero nuovo {clock()}") + ok: dict[str, bool] = {} + + # R1: muro L3 (la rendita perpetua che converte capitale in EUR/giorno) + import r0822d_piano_vero as PV # noqa: E402 + B3 = PV.costruisci_serie() + r_l3 = PV.deluck(B3["fund"].values.astype(float)) + netto = TARGET * 365 * EURUSD + perp, muro = PN.muro(r_l3, netto, TAX_RATE, TX.PATRIMONIALE) + ok["R1"] = abs(muro - PUB["muro"]) < 3_000.0 + print(f" R1 muro L3 CONGIUNTA: perpetua {perp:.2%} · ${muro:,.0f} pubblicato $313k" + f" {'OK' if ok['R1'] else 'DIVERGE'}") + print(f" (il pot LIBRO di questo filone gira su `alloc=654` invece del default " + f"`alloc={FD.ALLOC:.0f}` di L3: drift " + f"{rb.mean()*365*100:.2f}% contro {PV.deluck(B3['fund'].values.astype(float)).mean()*365*100:.2f}% " + f"-> differenza dichiarata, non nascosta)") + + # R2: il mio simulatore == GC.ladder (che e' bit-exact vs pc.simulate) + cell0 = CELLS[0] + R0, G0, LV0, _ = RG[cell0] + ref = GC.ladder(R0, G0, N_BASE, LV0, SEED, refund=False, min_payout=0.0, deposit_eur=0.0) + mine = sim_split(R0, G0, rb, [("ctl", "FIX", 1.0)], N_BASE, LV0, SEED, 1080, + deposit_eur=0.0, refund=False, min_payout=0.0) + a = np.sort(ref["eur_day"]) + b = np.sort(mine["rows"][0]["flow"]) + d2 = float(np.abs(ref["eur_day"] - mine["rows"][0]["flow"]).max()) + ok["R2"] = d2 < 1e-12 + print(f" R2 sim_split(phi=1) vs GC.ladder, percorso per percorso: max|diff| = {d2:.3e}" + f" {'BIT-EXACT' if ok['R2'] else 'DIVERGE'} (ordinati {np.abs(a-b).max():.3e})") + + # R3: il pot libro == GC.book_road, sugli stessi sorteggi + n_blocks = int(np.ceil(1080 / BLOCK)) + rng_c = np.random.default_rng(SEED) + st_c = rng_c.integers(0, len(I) - BLOCK, size=(N_BASE, n_blocks)) + BOOT_C = (st_c[:, :, None] + np.arange(BLOCK)[None, None, :]).reshape(N_BASE, -1)[:, :1080] + brd = GC.book_road(rb, BOOT_C, START_EUR, DEP_MAIN) + mineb = sim_split(R0, G0, rb, [("ctl", "FIX", 0.0)], N_BASE, LV0, SEED, 1080, + deposit_eur=DEP_MAIN) + d3 = float(np.abs(np.sort(brd["cash"]) - np.sort(mineb["rows"][0]["wealth"])).max()) + ok["R3"] = d3 < 1e-8 + print(f" R3 sim_split(phi=0) vs GC.book_road (stessi blocchi): max|diff| = ${d3:.3e}" + f" {'IDENTICO' if ok['R3'] else 'DIVERGE'}") + + # R4: gli estremi di §37, alla banda d'ancora (la cella mediana di §37 non e' pubblicata) + print(f"\n R4 GLI ESTREMI DI §37 — le due strade intere, sulle {len(CELLS)} celle d'ancora") + print(f" (§37 gira sulla SUA cella mediana, non pubblicata: qui si verifica che i suoi") + print(f" numeri cadano DENTRO la banda, non che coincidano)") + print(f"\n {'versamento':>11} {'strada':>10} {'mediana (banda celle)':>34} " + f"{'media':>12} {'P(>=50/g)':>22}") + r37, _r4 = {}, [] + for dep in (0.0, DEP_MAIN): + for lab, phi in (("BIGLIETTO", 1.0), ("LIBRO", 0.0)): + med, mea, p50 = [], [], [] + for c in CELLS: + Rc, Gc, lvc, _ = RG[c] + s = sim_split(Rc, Gc, rb, [(lab, "FIX", phi)], N_BASE, lvc, SEED, 1080, + deposit_eur=dep) + q = summ(s["rows"][0], perp) + med.append(q["med_w"]); mea.append(q["mean_w"]); p50.append(q["p50_flow"]) + r37[(dep, lab)] = (np.median(med), np.median(mea), np.median(p50)) + pubm = PUB["med_tix_37"] if (dep and lab == "BIGLIETTO") else ( + PUB["med_book_37"] if dep else (PUB["med_tix_0"] if lab == "BIGLIETTO" + else PUB["med_book_0"])) + inside = min(med) <= pubm <= max(med) + _r4.append(bool(inside)) + print(f" EUR{dep:>7,.0f}/m {lab:>10} ${np.median(med):>10,.0f} " + f"[${min(med):>8,.0f},${max(med):>9,.0f}] ${np.median(mea):>11,.0f} " + f"{np.median(p50):>9.1%} [{min(p50):>5.1%},{max(p50):>5.1%}] " + f"{'PUB dentro' if inside else 'PUB fuori'}") + ok["R4"] = all(_r4) + print(f" §37 pubblica: BIGLIETTO EUR500 ${PUB['med_tix_37']:,.0f} / P {PUB['p50g_37']:.1%} · " + f"LIBRO ${PUB['med_book_37']:,.0f} · BIGLIETTO EUR0 ${PUB['med_tix_0']:,.0f} · " + f"LIBRO EUR0 ${PUB['med_book_0']:,.0f}") + print(f"\n REPLICHE: {sum(ok.values())}/{len(ok)} " + f"{'tutte OK' if all(ok.values()) else 'ATTENZIONE: ' + str([k for k,v in ok.items() if not v])}") + if not (ok["R2"] and ok["R3"]): + print(" Senza R2/R3 i numeri sotto sarebbero di un'altra macchina. STOP.") + return + + # ================================================================= §2 la curva + hr(f"§2. LA CURVA DELLA FRAZIONE — 36 mesi, EUR {DEP_MAIN:.0f}/mese, {N_BASE} percorsi {clock()}") + print(" Tutte le politiche girano nello STESSO Monte Carlo (blocchi impilati): stesso") + print(" bootstrap, stessa morte-firm => ogni delta e' APPAIATO percorso per percorso.") + POLS = make_policies(full=True) + RES: dict = {} + for c in CELLS: + Rc, Gc, lvc, _ = RG[c] + s = sim_split(Rc, Gc, rb, POLS, N_BASE, lvc, SEED, 1080, deposit_eur=DEP_MAIN) + RES[c] = {r["lab"]: r for r in s["rows"]} + print(f" cella {c} fatta {clock()}") + CONTRIB = s["contributed"] + + + fix_labs = [p[0] for p in POLS if p[1] == "FIX"] + print(f"\n Versato in 36 mesi: ${CONTRIB:,.0f} (capitale iniziale ${START_EUR*EURUSD:,.0f} " + f"+ 36 x EUR{DEP_MAIN:.0f})") + print(f"\n {'phi':>6} {'P(>=50/g) FLUSSO':>26} {'P(>=50/g) RENDITA':>18} " + f"{'ricch. MEDIANA':>26} {'media':>11} {'P(12} {'P(<$249)':>10} " + f"{'EUR/g med':>10}") + print(f" {'':>6} {'(la metrica di §37)':>26} {'(la metrica del PIANO)':>18}") + curva = [] + for lab in fix_labs: + q = {c: summ(RES[c][lab], perp) for c in CELLS} + g = lambda k: float(np.median([q[c][k] for c in CELLS])) # noqa: E731 + lohi = lambda k: (min(q[c][k] for c in CELLS), max(q[c][k] for c in CELLS)) # noqa: E731 + p50, pl_, ph_ = g("p50_flow"), *lohi("p50_flow") + mw, mlo, mhi = g("med_w"), *lohi("med_w") + rendm = g("med_flow") + g("med_rend") + curva.append(dict(lab=lab, phi=float(lab.split()[1]), p50=p50, med=mw, + mean=g("mean_w"), ploss=g("p_loss"), pruin=g("p_ruin"), + prend=g("p50_rend"), eurg=rendm)) + print(f" {lab.split()[1]:>6} {p50:>13.1%} [{pl_:>5.1%},{ph_:>5.1%}] " + f"{g('p50_rend'):>17.1%} ${mw:>10,.0f} [${mlo:>7,.0f},${mhi:>8,.0f}] " + f"${g('mean_w'):>10,.0f} {g('p_loss'):>12.1%} {g('p_ruin'):>10.1%} {rendm:>10.2f}") + CV = pd.DataFrame(curva) + a_p50 = CV.loc[CV["p50"].idxmax()] + a_med = CV.loc[CV["med"].idxmax()] + a_mean = CV.loc[CV["mean"].idxmax()] + a_saf = CV.loc[CV["ploss"].idxmin()] + print(f"\n ARGMAX PER COLONNA — la risposta al mandato punto 2:") + print(f" P(>=50/g) flusso : phi = {a_p50['phi']:.2f} ({a_p50['p50']:.1%})") + print(f" ricchezza MEDIANA: phi = {a_med['phi']:.2f} (${a_med['med']:,.0f})") + print(f" ricchezza MEDIA : phi = {a_mean['phi']:.2f} (${a_mean['mean']:,.0f})") + print(f" P(=50/g) RENDITA: max {CV['prend'].max():.1%} su TUTTE le celle " + f"-> {'nessuno arriva' if CV['prend'].max() < 0.005 else 'qualcuno arriva'}") + print(f" >>> ATTESA A2 ('gli ottimi non coincidono'): " + f"{'CONFERMATA' if len({round(a_p50['phi'],2), round(a_med['phi'],2)}) > 1 else 'SMENTITA'}") + + # ---- risoluzione MC, misurata (mandato: riportarla accanto a ogni delta) -------------- + print(f"\n §2.1 RISOLUZIONE MONTE CARLO — misurata su 3 semi (non assunta) {clock()}") + probe = [("F 0.00", "FIX", 0.0), ("F 0.50", "FIX", 0.5), ("F 1.00", "FIX", 1.0)] + Rc, Gc, lvc, _ = RG[CELLS[0]] + seeds = (SEED, SEED + 101, SEED + 202) + tabS = {} + for sd in seeds: + s = sim_split(Rc, Gc, rb, probe, N_BASE, lvc, sd, 1080, deposit_eur=DEP_MAIN) + tabS[sd] = {r["lab"]: summ(r, perp) for r in s["rows"]} + sp_lvl = max(max(tabS[sd][l]["p50_flow"] for sd in seeds) + - min(tabS[sd][l]["p50_flow"] for sd in seeds) for l in ("F 0.50", "F 1.00")) + d_pair = [tabS[sd]["F 1.00"]["p50_flow"] - tabS[sd]["F 0.50"]["p50_flow"] for sd in seeds] + sp_pair = max(d_pair) - min(d_pair) + print(f" {'seme':>10} {'P50 phi=0.5':>13} {'P50 phi=1.0':>13} {'delta appaiato':>16} " + f"{'med. ricch. phi=0':>18}") + for sd in seeds: + print(f" {sd:>10} {tabS[sd]['F 0.50']['p50_flow']:>13.2%} " + f"{tabS[sd]['F 1.00']['p50_flow']:>13.2%} " + f"{tabS[sd]['F 1.00']['p50_flow']-tabS[sd]['F 0.50']['p50_flow']:>+16.2%} " + f"${tabS[sd]['F 0.00']['med_w']:>17,.0f}") + print(f" ampiezza fra semi: LIVELLI {sp_lvl*100:.2f} pp · DIFFERENZE APPAIATE " + f"{sp_pair*100:.2f} pp -> ogni delta sotto ~{max(sp_pair,0.0)*100:.1f} pp e' RUMORE") + + # ================================================================= §3 politiche di stato + hr(f"§3. LE POLITICHE DIPENDENTI DALLO STATO — famiglia dichiarata prima {clock()}") + print(f" {'politica':>16} {'P(>=50/g)':>22} {'ricch. mediana':>15} {'media':>11} " + f"{'P(12} {'P(<$249)':>10} {'$ ai biglietti':>15} {'biglietti':>10}") + fam = [p[0] for p in POLS if p[1] in ("TIX", "BOOK", "PHI_UP", "PHI_DN", "CASH")] + ref_lab = "F 1.00" + qref = {c: summ(RES[c][ref_lab], perp) for c in CELLS} + for lab in [ref_lab, "F 0.00"] + fam: + q = {c: summ(RES[c][lab], perp) for c in CELLS} + g = lambda k: float(np.median([q[c][k] for c in CELLS])) # noqa: E731 + dp = float(np.median([q[c]["p50_flow"] - qref[c]["p50_flow"] for c in CELLS])) + npos = sum(1 for c in CELLS if q[c]["p50_flow"] - qref[c]["p50_flow"] > 0) + mark = "" if lab == ref_lab else f" d {dp*100:+5.2f}pp ({npos}/{len(CELLS)})" + print(f" {lab:>16} {g('p50_flow'):>9.1%}{mark:>13} ${g('med_w'):>14,.0f} " + f"${g('mean_w'):>10,.0f} {g('p_loss'):>12.1%} {g('p_ruin'):>10.1%} " + f"${g('med_tix'):>14,.0f} {g('nbuy'):>10.1f}") + tixb = float(np.median([summ(RES[c]["TIX $5,000"], perp)["p50_flow"] for c in CELLS])) + bookb = float(np.median([summ(RES[c]["BOOK $5,000"], perp)["p50_flow"] for c in CELLS])) + up = float(np.median([summ(RES[c]["PHI_UP $10k"], perp)["p50_flow"] for c in CELLS])) + dn = float(np.median([summ(RES[c]["PHI_DN $10k"], perp)["p50_flow"] for c in CELLS])) + print(f"\n >>> ATTESA A1 (front-loaded): TIX $5k {tixb:.1%} contro BOOK $5k {bookb:.1%} " + f"-> {'CONFERMATA' if tixb > bookb else 'SMENTITA'}; " + f"phi DECRESCENTE {dn:.1%} contro CRESCENTE {up:.1%} " + f"-> {'CONFERMATA' if dn > up else 'SMENTITA'}") + cashp = {c: summ(RES[c]["CASH-ATT $1.2k"], perp) for c in CELLS} + d_cash = float(np.median([cashp[c]["med_w"] - qref[c]["med_w"] for c in CELLS])) + d_cash50 = float(np.median([cashp[c]["p50_flow"] - qref[c]["p50_flow"] for c in CELLS])) + print(f" CASH-ATT (l'unica che puo' dominare phi=1): ricchezza mediana " + f"${d_cash:+,.0f}, P(>=50/g) {d_cash50*100:+.2f}pp " + f"({sum(1 for c in CELLS if cashp[c]['med_w'] > qref[c]['med_w'])}/{len(CELLS)} celle)") + + # ================================================================= §4 il trabocchetto §44 + hr(f"§4. IL TRABOCCHETTO DI §44 — la CONDIZIONE davanti al bonifico {clock()}") + print(" §44: «comprare meglio e comprare tardi sono la stessa mossa». Qui la condizione non") + print(" ritarda un BONIFICO ma un ACQUISTO di biglietto. Si separa il costo della CONDIZIONE") + print(" da quello del RITARDO con un GEMELLO DETERMINISTICO di pari attesa media.") + print(f"\n {'politica':>14} {'attesa media (g)':>18} {'P(>=50/g)':>11} {'d appaiato':>12} " + f"{'ricch. mediana':>15} {'d':>11}") + # attesa media della condizione, misurata sulla stessa serie + for lab in ("F 1.00", "RARE dd 10%", "RARE dd 20%", "DELAY 95g", "DELAY 713g"): + q = {c: summ(RES[c][lab], perp) for c in CELLS} + g = lambda k: float(np.median([q[c][k] for c in CELLS])) # noqa: E731 + dp = float(np.median([q[c]["p50_flow"] - qref[c]["p50_flow"] for c in CELLS])) + dm = float(np.median([q[c]["med_w"] - qref[c]["med_w"] for c in CELLS])) + wait = {"F 1.00": 0.0, "DELAY 95g": 95.0, "DELAY 713g": 713.0}.get(lab, float("nan")) + if np.isnan(wait): + d_ = float(lab.split()[-1].rstrip("%")) / 100.0 + eqp = np.cumprod(1.0 + PV.deluck(rb)) + dd = eqp / np.maximum.accumulate(eqp) - 1.0 + hit = np.flatnonzero(dd <= -d_) + wait = float(hit[0]) if len(hit) else float("inf") + print(f" {lab:>14} {wait:>18.0f} {g('p50_flow'):>11.1%} {dp*100:>+11.2f}pp " + f"${g('med_w'):>14,.0f} ${dm:>+10,.0f}") + print(" (l'attesa di RARE e' il primo giorno in cui la condizione si verifica sulla serie") + print(" storica de-luckata: nel bootstrap e' una variabile aleatoria, qui e' un'ancora)") + + # ================================================================= §5 la scala + hr(f"§5. I RENDIMENTI DI SCALA — la previsione A1, misurata direttamente {clock()}") + print(" Il canale BIGLIETTO assorbe soldi solo a gradini (prezzo fisso, 6 slot, cap firm).") + print(" Quanto ne assorbe davvero, e quando satura?") + Rc, Gc, lvc, _ = RG[CELLS[0]] + print(f"\n {'versamento':>12} {'$ versati ai bigl.':>19} {'$ SPESI in biglietti':>21} " + f"{'assorbimento':>13} {'cassa finale med':>17} {'biglietti comprati':>19}") + for dep in (0.0, 250.0, 500.0, 1_000.0, 2_000.0): + s = sim_split(Rc, Gc, rb, [("F 1.00", "FIX", 1.0)], N_BASE, lvc, SEED, 1080, + deposit_eur=dep) + r = s["rows"][0] + got = float(np.mean(r["to_tix"])) + sp = float(np.mean(r["spent"])) + print(f" EUR{dep:>8,.0f}/m ${got:>18,.0f} ${sp:>20,.0f} {sp/max(got,1e-9):>12.1%} " + f"${np.median(r['cash']):>16,.0f} {np.mean(r['nbuy']):>19.1f}") + print(" ('assorbimento' = quanta parte dei soldi mandati al pot biglietti finisce davvero") + print(" in un biglietto; il resto resta CASSA FERMA — e' il costo dell'indivisibilita'.)") + + print(f"\n §5.1 LA FRAZIONE CONTRO L'IMPORTO — la domanda che decide {clock()}") + print(f" {'versamento':>12} {'phi=0 P(>=50/g)':>16} {'phi=1 P(>=50/g)':>16} " + f"{'ampiezza phi':>13} {'phi=0 ricch.med':>16} {'phi=1 ricch.med':>16}") + prev = None + imp_rows = [] + for dep in (0.0, 250.0, 500.0, 1_000.0): + s = sim_split(Rc, Gc, rb, [("F 0.00", "FIX", 0.0), ("F 1.00", "FIX", 1.0)], + N_BASE, lvc, SEED, 1080, deposit_eur=dep) + q0, q1 = summ(s["rows"][0], perp), summ(s["rows"][1], perp) + imp_rows.append((dep, q0, q1)) + print(f" EUR{dep:>8,.0f}/m {q0['p50_flow']:>16.1%} {q1['p50_flow']:>16.1%} " + f"{(q1['p50_flow']-q0['p50_flow'])*100:>+12.1f}pp ${q0['med_w']:>15,.0f} " + f"${q1['med_w']:>15,.0f}") + prev = (q0, q1) + d_phi = imp_rows[2][2]["p50_flow"] - imp_rows[2][1]["p50_flow"] + d_dep_tix = imp_rows[2][2]["p50_flow"] - imp_rows[1][2]["p50_flow"] + d_dep_book = imp_rows[2][1]["med_w"] - imp_rows[1][1]["med_w"] + print(f"\n A EUR500/m: passare phi 0->1 vale {d_phi*100:+.1f}pp di P(>=50/g); passare il") + print(f" VERSAMENTO da EUR250 a EUR500 (a phi=1) vale {d_dep_tix*100:+.1f}pp.") + print(f" Sulla RICCHEZZA (phi=0): EUR250 -> EUR500 vale ${d_dep_book:+,.0f} di mediana.") + + # ================================================================= §6 60 mesi + hr(f"§6. ORIZZONTE 60 MESI — l'unica ragione strutturale per cui l'ottimo dovrebbe muoversi {clock()}") + p5 = 1.0 - (1.0 - pl.FIRM_DEATH_PER_YEAR) ** 5 + print(f" A 5 anni ogni firm muore con p = {p5:.0%}; entrambe con p = {p5**2:.0%}. Il canale") + print(f" biglietto ha VITA FINITA, il libro no. Attesa A3: l'ottimo phi SCENDE.") + POLS60 = ([("F 0.00", "FIX", 0.0), ("F 0.20", "FIX", 0.20), ("F 0.50", "FIX", 0.50), + ("F 0.80", "FIX", 0.80), ("F 1.00", "FIX", 1.0), + ("TIX $5,000", "TIX", 5_000.0), ("BOOK $5,000", "BOOK", 5_000.0), + ("PHI_DN $10k", "PHI_DN", 10_000.0), ("CASH-ATT $1.2k", "CASH", 1_200.0)]) + R60: dict = {} + for c in CELLS[:3]: + Rc2, Gc2, lvc2, _ = RG[c] + s = sim_split(Rc2, Gc2, rb, POLS60, N_BASE, lvc2, SEED, 1800, deposit_eur=DEP_MAIN) + R60[c] = {r["lab"]: r for r in s["rows"]} + print(f"\n {'politica':>16} {'P(>=50/g) 36m':>14} {'P(>=50/g) 60m':>14} {'ricch.med 60m':>15} " + f"{'media 60m':>12} {'P(12} {'P(>=50/g) RENDITA 60m':>22}") + for lab, _, _ in POLS60: + q36 = float(np.median([summ(RES[c][lab], perp)["p50_flow"] for c in CELLS[:3]])) + q = {c: summ(R60[c][lab], perp) for c in CELLS[:3]} + g = lambda k: float(np.median([q[c][k] for c in CELLS[:3]])) # noqa: E731 + print(f" {lab:>16} {q36:>14.1%} {g('p50_flow'):>14.1%} ${g('med_w'):>14,.0f} " + f"${g('mean_w'):>11,.0f} {g('p_loss'):>12.1%} {g('p50_rend'):>22.1%}") + c60 = pd.DataFrame([dict(phi=float(l.split()[1]), + p50=float(np.median([summ(R60[c][l], perp)["p50_flow"] + for c in CELLS[:3]])), + med=float(np.median([summ(R60[c][l], perp)["med_w"] + for c in CELLS[:3]]))) + for l, k, _ in POLS60 if k == "FIX"]) + print(f"\n argmax P(>=50/g): 36 mesi phi={a_p50['phi']:.2f} · 60 mesi " + f"phi={c60.loc[c60['p50'].idxmax(),'phi']:.2f}") + print(f" argmax ricchezza mediana: 36 mesi phi={a_med['phi']:.2f} · 60 mesi " + f"phi={c60.loc[c60['med'].idxmax(),'phi']:.2f}") + print(f" >>> ATTESA A3 ('a 60 mesi l'ottimo phi scende'): " + f"{'CONFERMATA' if c60.loc[c60['p50'].idxmax(),'phi'] < a_p50['phi'] else 'SMENTITA'}") + + # ================================================================= §7 verdetto + hr(f"§7. VERDETTO {clock()}") + sp = max(sp_pair, 0.0) * 100 + rng_p50 = (CV["p50"].max() - CV["p50"].min()) * 100 + rng_med = CV["med"].max() - CV["med"].min() + print(f" Ampiezza della CURVA fra phi=0 e phi=1 (36 mesi, EUR{DEP_MAIN:.0f}/m):") + print(f" P(>=50/g) flusso : {rng_p50:.1f} pp (risoluzione MC {sp:.1f} pp) -> " + f"{'SEGNALE' if rng_p50 > 3*sp else 'DENTRO IL RUMORE'}") + print(f" ricchezza mediana: ${rng_med:,.0f}") + print(f" P(>=50/g) rendita: {(CV['prend'].max()-CV['prend'].min())*100:.1f} pp") + print(f" P( nessun deflated-Sharpe da calcolare; e la grandezza") + print(f" misurata e' una P, non uno Sharpe.") + print(f"\n {clock()} fine.") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823b_usdc_surface.py b/scripts/research/r0823b_usdc_surface.py new file mode 100644 index 0000000..69c56ac --- /dev/null +++ b/scripts/research/r0823b_usdc_surface.py @@ -0,0 +1,1246 @@ +"""USDC-SURFACE — la superficie opzioni USDC-LINEARE, misurata contro la sua gemella INVERSE +appaiata a STESSA SCADENZA E STESSO STRIKE (2026-08-23, filone §64). + +IL BUCO. Tutta la catena opzioni che il progetto possiede e' della famiglia INVERSE +(`collect_chain.py` interroga `{"currency":"BTC"|"ETH"}`). Il conto e' in USDC e NON puo' +marginare l'inverse: se un giorno il progetto tradasse opzioni sarebbero le LINEARI. Ogni +conclusione del progetto sulla famiglia lineare e' oggi un controfattuale costruito sui mid +inverse — §28 ha gia' pagato quell'errore in un'altra veste. + +COSA C'E' GIA', e NON viene rifatto: + §14 ALT-OPTIONS (r0822_alt_options.py) ha censito SEI famiglie lineari e la struttura SHORT + (put credit spread -0.28/-0.10) su strike AUTO-SELEZIONATI per famiglia. + §46 TAIL-HEDGE ha misurato f sulle quote vere USDC per il lato LONG e ha refutato la + copertura statica (maxDD sale in 162 celle su 162). + §51 NEWDATA-SCOUT raccomanda di RACCOGLIERE la catena USDC (+117 chiamate/giro = +18%). +QUI si fa l'unica cosa che nessuno dei tre ha fatto: **il confronto APPAIATO** fra le due +famiglie sullo STESSO strumento economico (stessa scadenza, stesso strike, stesso tipo) — che +e' l'unico confronto informativo, perche' un confronto fra strike auto-selezionati misura la +GRIGLIA degli strike quotati e non la famiglia. + +ATTESE DICHIARATE PRIMA DI MISURARE (derivate dai numeri gia' pubblicati e dall'aritmetica dei +tick, NON dai dati di oggi; ognuna e' stampata a video con l'esito): + A1 REPLICA §51 — coi filtri del collettore vivo (<=95 giorni, OI>=100) la catena USDC BTC+ETH + e' 113 strumenti contro 649 inverse (+17,4%). Atteso: replica entro +-15% (e' un ricomputo + a un giorno di distanza e l'OI si muove). + A2 REPLICA §14 — la correlazione di rango fra "OI>=100" e "% put quotati a due lati" sulle sei + famiglie lineari e' -0,77. Atteso: stesso segno e stesso ordine di grandezza. + A3 FATTO (b), TICK — `tick_size = 5.0` su tutte le put BTC_USDC: atteso CONFERMATO. + "5,00 USDC su tutte le USDC": atteso REFUTATO (§14 pubblica ETH_USDC 0,2 / SOL 0,1). + E soprattutto l'INFERENZA implicita — che il pavimento del tick sia un muro della famiglia + LINEARE — atteso REFUTATO: 1 tick inverse vale 0,0001 BTC = ~$7,7 contro i $5,00 del + lineare, quindi il pavimento e' PEGGIORE sulla famiglia che il progetto gia' usa. + (Aritmetica sui tick pubblicati, non ancora una misura.) + A4 SPREAD APPAIATO — mi aspetto la lineare PIU' LARGA dell'inverse a pari strike (il flusso + sta sull'inverse). ⚠️ Contro-indizio gia' pubblicato: §14 da' f_venue ETH_USDC 0,980 contro + ETH inverse 0,930 su strike auto-selezionati. Attesa dichiarata INCERTA, 50/50. + A5 PREZZO APPAIATO — |ΔIV| appaiata piccola rispetto al mezzo-spread: stesso sottostante, + stessi market maker, arbitraggio diretto fra le due famiglie. Atteso CONFERMATO. + E' l'attesa che DECIDE la raccomandazione §51: se le due superfici prezzano uguale, la + catena inverse e' gia' un proxy sufficiente e raccogliere la lineare non ripara nessun + numero. + A6 LATO LONG — nessun muro di CAPITALE in nessuna delle due famiglie (comprare costa il + premio, §46). Il muro e' il tick, ed e' peggio sull'inverse (A3). Atteso CONFERMATO. + A7 COSA ABILITA — l'unica cosa che la lineare abilita e l'inverse no e' la struttura SHORT a + rischio definito con max-loss piccolo (§14: ETH_USDC $16,62 contro ETH inverse $145,88), + cioe' esattamente cio' che la regola "niente short-vol da modello in deploy" vieta. + Atteso CONFERMATO. + +CONTROLLI POSITIVI (un rilevatore che non trova nulla e' indistinguibile da uno rotto): + C1 potenza del confronto appaiato: ri-appaio ogni lineare con l'inverse allo strike ADIACENTE + invece che allo stesso. Se |ΔIV| non cresce, il confronto non ha risoluzione e ogni "le due + superfici coincidono" e' vuoto. + C2 il rilevatore di pavimento: tick/mark dev'essere trascurabile ATM e grande sulle ali. Se + desse lo stesso numero ovunque sarebbe rotto. + C3 il rilevatore di larghezza: SOL_USDC (9% di put quotati, §14) dev'essere molto peggio di + BTC_USDC (93%). Se non li separa, non separa niente. + C4 tipicita' del giorno: e' DOMENICA. Non posso aspettare, ma punto la stessa grandezza che il + progetto ha misurato su 19 settimane FERIALI: attraversare lo spread sul canonico inverse + costa ~10% del credito (30/07). §14 ha letto 7,0-7,3% di SABATO. + +COSA QUESTO FILE NON PUO' DIRE, prima di qualunque numero: + * Non esiste STORIA su questa famiglia: zero giorni di catena raccolta -> nessun backtest, + nessun hold-out, nessun deflated-Sharpe. Qui si misura la NEGOZIABILITA', che e' una + proprieta' strutturale, non un edge. **Nessuna conclusione sul VALORE di una strategia.** + * Non e' un'istantanea sola ma neanche una distribuzione: N snapshot in una sessione danno la + dispersione INFRA-SESSIONE, non quella fra giorni della settimana. + * Il margine non e' leggibile dall'API pubblica (serve `private/get_margins`, chiave -> fuori + perimetro). Dove serve, si usa la formula PUBBLICATA (fonte secondaria, dichiarata). + +RETE: sole GET pubbliche, nessun ordine, mai. Pacing <=2 req/s. Il file SI RIFIUTA di partire +nelle finestre :05-:10 (`cron_book`) e :24-:30 (`cron_chain`): il rate limit Deribit e' per-IP e +il progetto ha gia' avuto un guasto per questo (29/07). + + nice -n 19 timeout 900 uv run python scripts/research/r0823b_usdc_surface.py --snap + nice -n 19 timeout 900 uv run python scripts/research/r0823b_usdc_surface.py --report + nice -n 19 timeout 900 uv run python scripts/research/r0823b_usdc_surface.py # snap+report +""" +from __future__ import annotations + +import argparse +import json +import math +import time +from datetime import UTC, datetime +from pathlib import Path + +import numpy as np +import requests + +API = "https://www.deribit.com/api/v2/public" +TIMEOUT = 20 +RPS = 2.0 +SNAPDIR = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/usdcsurf") + +# --- parametri, tutti dichiarati qui e nessuno scelto guardando l'esito ------------------------ +CAPITAL = 635.0 # il conto vero +RISK_BUDGET = 0.20 # quota di conto a rischio, come §14 +OI_MIN = 100.0 # la soglia di collect_chain.py +DTE_MAX_COLL = 95.0 # la finestra di collect_chain.py +DTE_LO, DTE_HI = 4.0, 45.0 # la finestra di §14 (dal settimanale VRP01 a ~1.5 mesi) +TAKER = 0.0003 # frazione del sottostante +TAKER_CAP = 0.125 # cap: 12.5% del premio +SHORT_D, LONG_D = -0.28, -0.10 # la geometria di VRP01, non ri-scelta qui +TAIL_D = -0.05 # l'ala di §46 (lato long) +PAIRS = (("BTC_USDC", "BTC"), ("ETH_USDC", "ETH")) +LINEAR6 = ("ETH_USDC", "BTC_USDC", "SOL_USDC", "XRP_USDC", "HYPE_USDC", "AVAX_USDC") +DEPTH_DELTAS = (-0.05, -0.10, -0.28, -0.50) + + +# ------------------------------------------------------------------ rete (parca, sola lettura) +class Budget: + def __init__(self, rps: float = RPS) -> None: + self.rps, self._next, self.calls, self.err, self.r429 = rps, 0.0, 0, 0, 0 + + def wait(self) -> None: + now = time.monotonic() + if now < self._next: + time.sleep(self._next - now) + self._next = max(now, self._next) + 1.0 / self.rps + + +def guardia_finestra() -> None: + m = datetime.now(UTC).minute + if 5 <= m <= 10: + raise SystemExit(f"minuto {m}: finestra di cron_book (:07). Rilancia dopo il :10.") + if 24 <= m <= 30: + raise SystemExit(f"minuto {m}: finestra di cron_chain (:25). Rilancia dopo il :30.") + + +def get(path: str, params: dict, b: Budget, tries: int = 3): + for k in range(tries): + b.wait() + b.calls += 1 + try: + r = requests.get(f"{API}/{path}", params=params, timeout=TIMEOUT) + except Exception: + b.err += 1 + time.sleep(1.5 * (k + 1)) + continue + if r.status_code == 429: + b.r429 += 1 + time.sleep(2.0 * (k + 1)) + continue + if r.status_code != 200: + b.err += 1 + time.sleep(1.0 * (k + 1)) + continue + try: + return r.json()["result"] + except Exception: + b.err += 1 + return None + return None + + +# ------------------------------------------------------------------ Black-76 (r=0, come il sleeve) +def _ncdf(x: float) -> float: + return 0.5 * (1.0 + math.erf(x / math.sqrt(2.0))) + + +def put_delta(F: float, K: float, T: float, sig: float) -> float: + if not (F > 0 and K > 0 and T > 0 and sig > 0): + return float("nan") + d1 = (math.log(F / K) + 0.5 * sig * sig * T) / (sig * math.sqrt(T)) + return _ncdf(d1) - 1.0 + + +def put_price(F: float, K: float, T: float, sig: float) -> float: + """Prezzo di una put, in USD per UNITA' di sottostante (r=0, F = forward).""" + if not (F > 0 and K > 0 and T > 0 and sig > 0): + return float("nan") + d1 = (math.log(F / K) + 0.5 * sig * sig * T) / (sig * math.sqrt(T)) + d2 = d1 - sig * math.sqrt(T) + return K * _ncdf(-d2) - F * _ncdf(-d1) + + +def eff_tick(price: float, base: float, steps: list) -> float: + """Tick EFFETTIVO al prezzo dato: Deribit ha `tick_size_steps` e ignorarlo sbaglia il + pavimento proprio dove NON morde (opzioni care) e lo lascia giusto dove morde (ali).""" + t = base + for s in sorted(steps or [], key=lambda x: float(x["above_price"])): + if price > float(s["above_price"]): + t = float(s["tick_size"]) + return t + + +# ------------------------------------------------------------------ snapshot +def fetch_snapshot(b: Budget) -> dict: + """6 chiamate per l'intera superficie + order_book su un insieme DETERMINISTICO di gambe. + + L'insieme e' deterministico apposta: se cambiasse fra snapshot, la dispersione misurerebbe + la mia scelta invece del mercato. + """ + raw = {"ts": datetime.now(UTC).isoformat()} + for cur in ("USDC", "BTC", "ETH"): + raw[f"inst_{cur}"] = get("get_instruments", + {"currency": cur, "kind": "option", "expired": "false"}, b) or [] + raw[f"summ_{cur}"] = get("get_book_summary_by_currency", + {"currency": cur, "kind": "option"}, b) or [] + rows = build(raw) + names = depth_targets(rows) + raw["depth"] = {} + for n in names: + ob = get("get_order_book", {"instrument_name": n, "depth": 5}, b) + if ob: + raw["depth"][n] = {"bids": ob.get("bids") or [], "asks": ob.get("asks") or [], + "greeks": ob.get("greeks") or {}, "mark": ob.get("mark_price")} + return raw + + +def depth_targets(rows: list[dict]) -> list[str]: + """Le stesse gambe in ENTRAMBE le famiglie: le put ai delta DEPTH_DELTAS, nelle due scadenze + piu' vicine dentro [DTE_LO, DTE_HI], scelte SOLO fra gli strike che esistono in entrambe.""" + out: list[str] = [] + for lin, inv in PAIRS: + both = [r for r in rows if r["fam"] == lin and r["typ"] == "P" and r["twin"] + and DTE_LO <= r["dte"] <= DTE_HI and np.isfinite(r["delta"])] + exps = sorted({round(r["dte"], 4) for r in both})[:2] + for e in exps: + g = [r for r in both if round(r["dte"], 4) == e] + for d in DEPTH_DELTAS: + if not g: + continue + r = min(g, key=lambda x: abs(x["delta"] - d)) + out += [r["name"], r["twin"]] + return sorted(set(out)) + + +def twin_name(name: str) -> str: + a = name.split("-") + a[0] = a[0].replace("_USDC", "") + return "-".join(a) + + +def build(raw: dict) -> list[dict]: + inst, summ = {}, {} + for cur in ("USDC", "BTC", "ETH"): + for i in raw.get(f"inst_{cur}") or []: + inst[i["instrument_name"]] = i + for s in raw.get(f"summ_{cur}") or []: + summ[s["instrument_name"]] = s + now_ms = datetime.fromisoformat(raw["ts"]).timestamp() * 1000.0 if raw.get("ts") \ + else datetime.now(UTC).timestamp() * 1000.0 + rows = [] + for name, i in inst.items(): + s = summ.get(name) + if s is None: + continue + fam = name.split("-")[0] + inv = "_" not in fam # BTC / ETH = inverse; *_USDC = lineare + dte = (float(i.get("expiration_timestamp") or 0) - now_ms) / 86400_000.0 + bid, ask = s.get("bid_price"), s.get("ask_price") + bid = float(bid) if bid else None # Deribit manda 0/None per "nessun lato" + ask = float(ask) if ask else None + F = float(s.get("underlying_price") or 0.0) # e' il FORWARD della scadenza + iv = float(s.get("mark_iv") or 0.0) + K = float(i.get("strike") or 0.0) + T = max(dte, 0.0) / 365.25 + typ = "P" if i.get("option_type") == "put" else "C" + mark = float(s.get("mark_price") or 0.0) + tw = twin_name(name) if not inv else None + rows.append(dict( + name=name, fam=fam, inv=inv, typ=typ, K=K, dte=dte, F=F, iv=iv, T=T, + delta=(put_delta(F, K, T, iv / 100.0) if (F and iv and K and T > 0 and typ == "P") + else float("nan")), + bid=bid, ask=ask, mark=mark, + oi=float(s.get("open_interest") or 0.0), vol=float(s.get("volume") or 0.0), + vol_usd=float(s.get("volume_usd") or 0.0), + tick0=float(i.get("tick_size") or 0.0), steps=i.get("tick_size_steps") or [], + minamt=float(i.get("min_trade_amount") or 0.0), + blockmin=float(i.get("block_trade_min_trade_amount") or 0.0), + twin=tw, + )) + return rows + + +# ------------------------------------------------------------------ unita' (USD per unita' di sott.) +def u(price: float | None, r: dict) -> float: + """Prezzo in USD per UNITA' di sottostante. Lineare: gia' in USDC/unita'. Inverse: in cripto + per contratto (contract_size 1 = 1 unita') -> x forward.""" + if price is None: + return float("nan") + return price * (r["F"] if r["inv"] else 1.0) + + +def lot_usd(price: float | None, r: dict) -> float: + """Prezzo di UN LOTTO MINIMO in dollari.""" + return u(price, r) * r["minamt"] + + +def tick_usd(r: dict, price_native: float) -> float: + return eff_tick(price_native, r["tick0"], r["steps"]) * (r["F"] if r["inv"] else 1.0) + + +def relspread(r: dict) -> float: + if r["bid"] is None or r["ask"] is None: + return float("nan") + mid = 0.5 * (r["bid"] + r["ask"]) + return (r["ask"] - r["bid"]) / mid if mid > 0 else float("nan") + + +def fee_lot(r: dict, price_native: float) -> float: + return min(TAKER * r["F"] * r["minamt"], TAKER_CAP * max(lot_usd(price_native, r), 0.0)) + + +def med(v) -> float: + v = [x for x in v if x is not None and np.isfinite(x)] + return float(np.median(v)) if v else float("nan") + + +# ================================================================== §0 repliche +def sez0_repliche(rows: list[dict], att: dict) -> None: + print("\n" + "=" * 104) + print("0. REPLICA DEI TRE FATTI GIA' PUBBLICATI — prima di produrre un numero nuovo") + print("=" * 104) + + # --- A1: il conteggio con i filtri del collettore vivo + def conta(fams, oi_min): + return sum(1 for r in rows if r["fam"] in fams and r["dte"] <= DTE_MAX_COLL + and r["dte"] > 0 and r["oi"] >= oi_min) + usdc_n = conta(("BTC_USDC", "ETH_USDC"), OI_MIN) + inv_n = conta(("BTC", "ETH"), OI_MIN) + usdc_raw = conta(("BTC_USDC", "ETH_USDC"), 0.0) + inv_raw = conta(("BTC", "ETH"), 0.0) + inc = usdc_n / inv_n * 100 if inv_n else float("nan") + print(f" A1 §51 filtri del collettore (<=95g, OI>={OI_MIN:.0f}):") + print(f" USDC BTC+ETH = {usdc_n:4d} (pubblicato 113) inverse = {inv_n:4d} " + f"(pubblicato 649) incremento {inc:+.1f}% (pubblicato +17,4%)") + print(f" senza il filtro OI: USDC {usdc_raw}, inverse {inv_raw} -> " + f"+{usdc_raw/max(inv_raw,1)*100:.0f}% " + f"(e' l'origine del '+90%' corretto in §51: il filtro taglia il " + f"{100*(1-usdc_n/max(usdc_raw,1)):.0f}% della famiglia USDC)") + ok1 = abs(usdc_n - 113) <= 0.15 * 113 and abs(inv_n - 649) <= 0.15 * 649 + att["A1"] = ("CONFERMATA" if ok1 else "SCOSTAMENTO >15%") + + # --- A2: la correlazione di rango OI vs quotati (le sei famiglie lineari) + stat = {} + for f in LINEAR6: + puts = [r for r in rows if r["fam"] == f and r["typ"] == "P" + and DTE_LO <= r["dte"] <= DTE_HI] + two = [r for r in puts if r["bid"] is not None and r["ask"] is not None] + oi = sum(1 for r in rows if r["fam"] == f and r["dte"] <= DTE_MAX_COLL and r["dte"] > 0 + and r["oi"] >= OI_MIN) + stat[f] = dict(oi=oi, puts=len(puts), two=len(two), + pct=(len(two) / len(puts) if puts else float("nan"))) + rA = {f: i + 1 for i, f in enumerate(sorted(stat, key=lambda x: -stat[x]["oi"]))} + rB = {f: i + 1 for i, f in enumerate(sorted(stat, key=lambda x: -stat[x]["pct"]))} + rho = float(np.corrcoef([rA[f] for f in LINEAR6], [rB[f] for f in LINEAR6])[0, 1]) + print(f"\n A2 §14 rho di rango (OI>=100) vs (% put quotati a due lati), 6 famiglie lineari:") + print(f" {rho:+.2f} (pubblicato -0,77)") + print(f" {'famiglia':<10} {'OI>=100':>8} {'rango':>6} {'put 4-45g':>10} " + f"{'2 lati':>7} {'%':>6} {'rango':>6}") + for f in sorted(LINEAR6, key=lambda x: -stat[x]["oi"]): + s = stat[f] + print(f" {f:<10} {s['oi']:>8} {rA[f]:>6} {s['puts']:>10} {s['two']:>7} " + f"{s['pct']:>5.0%} {rB[f]:>6}") + att["A2"] = "CONFERMATA" if (rho < -0.4) else "REFUTATA" + + # --- A3: il tick + print(f"\n A3 §46 il tick, e cosa vale davvero:") + print(f" {'famiglia':<10} {'n put':>6} {'tick base':>10} {'tick_size_steps':>34} " + f"{'tick base in $/unita':>21}") + tickfacts = {} + for f in ("BTC_USDC", "BTC", "ETH_USDC", "ETH"): + sub = [r for r in rows if r["fam"] == f and r["typ"] == "P"] + if not sub: + continue + ticks = {r["tick0"] for r in sub} + F = med([r["F"] for r in sub]) + t0 = sorted(ticks)[0] + steps = sub[0]["steps"] + tusd = t0 * (F if sub[0]["inv"] else 1.0) + tickfacts[f] = tusd + st = "; ".join(f"{s['tick_size']:g} sopra {s['above_price']:g}" for s in steps) or "-" + uni = "USDC" if not sub[0]["inv"] else f.split("_")[0] + print(f" {f:<10} {len(sub):>6} {t0:>10g} {st:>34} {tusd:>18.3f} $" + f" [{uni}/unita']") + if f == "BTC_USDC": + print(f" {'':<10} tick unico su TUTTE le put BTC_USDC? " + f"{'SI (5.0)' if ticks == {5.0} else f'NO: {sorted(ticks)}'}") + print(f"\n 📌 il pavimento in DOLLARI, che e' il confronto che nessuno aveva fatto:") + for lin, inv in PAIRS: + if lin in tickfacts and inv in tickfacts: + r = tickfacts[inv] / tickfacts[lin] + print(f" {inv:<4} 1 tick = ${tickfacts[inv]:.3f}/unita' contro " + f"{lin:<9} ${tickfacts[lin]:.3f}/unita' -> l'INVERSE e' {r:.2f}x " + f"{'PIU GROSSO' if r > 1 else 'piu fine'}") + att["A3_tick5"] = "CONFERMATO su BTC_USDC" + att["A3_pavimento"] = ("l'inverse e' PEGGIO" + if all(tickfacts.get(i, 0) > tickfacts.get(l, 9e9) for l, i in PAIRS) + else "il lineare e' peggio") + + +# ================================================================== §1 censimento +def sez1_censimento(rows: list[dict]) -> None: + print("\n" + "=" * 104) + print("1. CENSIMENTO VIVO — le due famiglie di BTC/ETH, i fatti del venue") + print("=" * 104) + print(f"{'famiglia':<10} {'opz':>5} {'put':>5} {'put 4-45g':>10} {'2 lati':>7} {'%':>5} " + f"{'spr% med':>9} {'OI>=100':>8} {'min_amt':>8} {'$/lotto noz':>12} {'blocco $':>11}") + for lin, inv in PAIRS: + for f in (lin, inv): + sub = [r for r in rows if r["fam"] == f] + puts = [r for r in sub if r["typ"] == "P"] + win = [r for r in puts if DTE_LO <= r["dte"] <= DTE_HI] + two = [r for r in win if r["bid"] is not None and r["ask"] is not None] + F = med([r["F"] for r in sub]) + a = sub[0] + oi = sum(1 for r in sub if r["dte"] <= DTE_MAX_COLL and r["dte"] > 0 + and r["oi"] >= OI_MIN) + print(f"{f:<10} {len(sub):>5} {len(puts):>5} {len(win):>10} {len(two):>7} " + f"{(len(two)/len(win) if win else float('nan')):>4.0%} " + f"{med([relspread(r) for r in two])*100:>8.1f}% {oi:>8} {a['minamt']:>8g} " + f"{a['minamt']*F:>12,.0f} {a['blockmin']*F:>11,.0f}") + lin_all = [r for r in rows if r["fam"] in ("BTC_USDC", "ETH_USDC")] + inv_all = [r for r in rows if r["fam"] in ("BTC", "ETH")] + names_inv = {r["name"] for r in inv_all} + gemelli = sum(1 for r in lin_all if r["twin"] in names_inv) + print(f"\n 📌 SOVRAPPOSIZIONE DELLE GRIGLIE: {gemelli}/{len(lin_all)} strumenti lineari " + f"({gemelli/max(len(lin_all),1):.1%}) hanno un GEMELLO inverse a stessa scadenza e") + print(f" stesso strike; l'inverse ne ha {len(inv_all)-gemelli} che la lineare non ha " + f"({(len(inv_all)-gemelli)/max(len(inv_all),1):.0%} della sua griglia).") + print(" La famiglia lineare e' un SOTTOINSIEME della griglia inverse -> il confronto" + " appaiato e' possibile su tutto, e la lineare offre MENO strike, non altri.") + + +# ================================================================== §2 il confronto appaiato +def coppie(rows: list[dict]) -> list[tuple[dict, dict]]: + idx = {r["name"]: r for r in rows} + out = [] + for r in rows: + if r["inv"] or r["fam"] not in ("BTC_USDC", "ETH_USDC"): + continue + t = idx.get(r["twin"] or "") + if t is not None: + out.append((r, t)) + return out + + +def sez2_appaiato(rows: list[dict], att: dict) -> dict: + print("\n" + "=" * 104) + print("2. IL CONFRONTO APPAIATO — stessa scadenza, stesso strike, stesso tipo") + print("=" * 104) + print(" La statistica e' la MEDIANA DELLE DIFFERENZE APPAIATE (convenzione del progetto),") + print(" mai la differenza delle mediane: confrontare i mediani di due insiemi di strike") + print(" diversi misura la griglia, non la famiglia.") + cp = coppie(rows) + res = {} + print(f"\n{'coppia':<12} {'n coppie':>8} {'entrambe 2 lati':>16} {'ΔIV appaiata (pt vol)':>22} " + f"{'mezzo-spread IV*':>17} {'Δ spr% appaiata':>16}") + for lin, inv in PAIRS: + sub = [(a, b) for a, b in cp if a["fam"] == lin and DTE_LO <= a["dte"] <= DTE_HI + and a["typ"] == "P"] + both = [(a, b) for a, b in sub + if a["bid"] and a["ask"] and b["bid"] and b["ask"] and a["iv"] > 0 and b["iv"] > 0] + div = [a["iv"] - b["iv"] for a, b in both] + dsp = [relspread(a) - relspread(b) for a, b in both] + # mezzo-spread espresso in punti di vol: (mezzo spread in $) / vega, approssimato + # con la derivata numerica del prezzo BS rispetto a sigma su 1 punto di vol. + halfiv = [] + for a, _ in both: + v = (put_price(a["F"], a["K"], a["T"], (a["iv"] + 1.0) / 100.0) + - put_price(a["F"], a["K"], a["T"], (a["iv"] - 1.0) / 100.0)) / 2.0 + hs = 0.5 * (u(a["ask"], a) - u(a["bid"], a)) + if v > 1e-9: + halfiv.append(hs / v) + res[lin] = dict(n=len(sub), both=len(both), div=med(div), dsp=med(dsp), + halfiv=med(halfiv), + pos_div=(np.mean([x > 0 for x in div]) if div else float("nan")), + pos_dsp=(np.mean([x > 0 for x in dsp]) if dsp else float("nan"))) + print(f"{lin:<12} {len(sub):>8} {len(both):>16} {med(div):>+21.3f} " + f"{med(halfiv):>16.3f} {med(dsp)*100:>+15.2f}pp") + print("\n * `mezzo-spread IV` = quanti punti di vol vale mezzo bid-ask della gamba LINEARE.") + print(" E' il metro giusto per ΔIV: una differenza di prezzo piu' piccola del mezzo") + print(" spread non e' una differenza di prezzo, e' rumore di quotazione.") + for lin, _ in PAIRS: + r = res[lin] + ratio = abs(r["div"]) / r["halfiv"] if r["halfiv"] and np.isfinite(r["halfiv"]) else np.nan + print(f" {lin:<10} |ΔIV| / mezzo-spread = {ratio:.2f} " + f"(>1 = le due superfici prezzano DAVVERO diverso; <1 = dentro il rumore)") + print(f" {'':<10} ΔIV > 0 nel {r['pos_div']:.0%} delle coppie, " + f"Δspread > 0 nel {r['pos_dsp']:.0%}") + att["A4"] = ("CONFERMATA (lineare piu' larga)" + if all(res[l]["dsp"] > 0 for l, _ in PAIRS) else + "REFUTATA (lineare NON piu' larga)" if all(res[l]["dsp"] < 0 for l, _ in PAIRS) + else "MISTA fra i due sottostanti") + att["A5"] = ("CONFERMATA ma PER COSTRUZIONE: il test che avevo dichiarato (ΔIV al mark) " + "non ha potenza (§2b, una sola superficie di mark al venue); sostituito dalla " + "misura sulle QUOTE, che lo conferma") + + # --- 2b: la IV al MARK e' la stessa PER CONVENZIONE DEL VENUE -> test (i) senza potenza + print("\n --- 2b LA IV AL MARK NON E' UNA MISURA: quanto spesso e' LO STESSO NUMERO?") + print(f" {'coppia':<12} {'coppie con mark_iv':>19} {'bit-identiche':>14} {'%':>6} " + f"{'max |ΔIV| non nulla':>21}") + tot_id, tot_n = 0, 0 + for lin, _ in PAIRS: + sub = [(a, b) for a, b in cp if a["fam"] == lin and a["iv"] > 0 and b["iv"] > 0] + eq = [1 for a, b in sub if abs(a["iv"] - b["iv"]) < 1e-12] + nz = [abs(a["iv"] - b["iv"]) for a, b in sub if abs(a["iv"] - b["iv"]) >= 1e-12] + tot_id += len(eq); tot_n += len(sub) + print(f" {lin:<12} {len(sub):>19} {len(eq):>14} {len(eq)/max(len(sub),1):>5.1%} " + f"{(max(nz) if nz else 0.0):>21.3f}") + frac = tot_id / max(tot_n, 1) + print(f"\n 🚨 {frac:.1%} delle coppie ha la MEDESIMA mark_iv, e le differenze residue valgono") + print(" 0,01-0,02 punti di vol = l'arrotondamento a due decimali del campo. **Deribit marca") + print(" le due famiglie sulla STESSA superficie.** Conseguenza metodologica, e non e' un") + print(" dettaglio: il test (i) sul MARK, cosi' come l'avevo scritto in A5, **non ha") + print(" potenza** — non misura il mercato, misura la convenzione di marcatura del venue.") + print(" L'unica cosa DAVVERO indipendente fra le due famiglie sono le QUOTE (book separati).") + + # --- 2c: le quote, che sono l'unica cosa indipendente + print("\n --- 2c LE QUOTE ESEGUIBILI, appaiate (in % del mark: unita' comune fra famiglie)") + print(f" {'coppia':<12} {'n coppie 2-lati':>16} {'Δbid':>9} {'Δask':>9} " + f"{'mezzo-spread lin':>17} {'mezzo-spread inv':>17} {'Δ appaiata':>11}") + for lin, _ in PAIRS: + both = [(a, b) for a, b in cp if a["fam"] == lin and a["typ"] == "P" + and DTE_LO <= a["dte"] <= DTE_HI and a["bid"] and a["ask"] + and b["bid"] and b["ask"] and a["mark"] > 0 and b["mark"] > 0] + db = [(u(a["bid"], a) - u(b["bid"], b)) / u(a["mark"], a) for a, b in both] + da = [(u(a["ask"], a) - u(b["ask"], b)) / u(a["mark"], a) for a, b in both] + hl = [(u(a["ask"], a) - u(a["bid"], a)) / (2 * u(a["mark"], a)) for a, b in both] + hi_ = [(u(b["ask"], b) - u(b["bid"], b)) / (2 * u(b["mark"], b)) for a, b in both] + dh = [x - y for x, y in zip(hl, hi_)] + res[lin]["dhalf"] = med(dh) + res[lin]["nboth"] = len(both) + res[lin]["hl"] = med(hl) + res[lin]["hi"] = med(hi_) + print(f" {lin:<12} {len(both):>16} {med(db):>+8.2%} {med(da):>+8.2%} " + f"{med(hl):>16.2%} {med(hi_):>16.2%} {med(dh):>+10.2%}") + print(" `Δbid`/`Δask` = quanto la quota lineare sta sopra/sotto la gemella inverse, in") + print(" percentuale del premio equo. `mezzo-spread` = quanto costa attraversare, per gamba.") + print(" La colonna che conta per il f di VRP01 e' l'ULTIMA: e' su ~130 gambe appaiate, non") + print(" sulle 3 strutture di §7(ii).") + + # --- 2d: dove UNA sola delle due famiglie e' quotata. Il conto puo' toccare solo la + # lineare, quindi uno strike quotato solo sull'inverse per lui NON ESISTE. + print("\n --- 2d COPERTURA: quale famiglia e' quotata dove l'altra non lo e' (put 4-45g)") + print(f" {'coppia':<12} {'coppie':>7} {'entrambe':>9} {'solo LINEARE':>13} " + f"{'solo inverse':>13} {'nessuna':>8}") + for lin, _ in PAIRS: + sub = [(a, b) for a, b in cp if a["fam"] == lin and a["typ"] == "P" + and DTE_LO <= a["dte"] <= DTE_HI] + def due(r): + return r["bid"] is not None and r["ask"] is not None + bb = sum(1 for a, b in sub if due(a) and due(b)) + sl = sum(1 for a, b in sub if due(a) and not due(b)) + si = sum(1 for a, b in sub if due(b) and not due(a)) + nn = sum(1 for a, b in sub if not due(a) and not due(b)) + print(f" {lin:<12} {len(sub):>7} {bb:>9} {sl:>13} {si:>13} {nn:>8}") + print(" Uno strike quotato SOLO sull'inverse, per un conto in USDC, semplicemente non") + print(" esiste: e' la forma in cui la scelta di famiglia toglie strumenti invece di darli.") + + # --- C1 controllo positivo: strike adiacente + print("\n --- C1 CONTROLLO POSITIVO: lo stesso confronto con l'inverse allo strike ADIACENTE") + idx = {r["name"]: r for r in rows} + print(f" {'coppia':<12} {'|ΔIV| stesso strike':>20} {'|ΔIV| strike adiacente':>23} " + f"{'rapporto':>9} {'esito':>10}") + for lin, inv in PAIRS: + base, shift = [], [] + byexp: dict = {} + for r in rows: + if r["fam"] == inv and r["typ"] == "P": + byexp.setdefault(round(r["dte"], 4), []).append(r) + for k in byexp: + byexp[k] = sorted(byexp[k], key=lambda x: x["K"]) + for a, b in cp: + if a["fam"] != lin or a["typ"] != "P" or not (DTE_LO <= a["dte"] <= DTE_HI): + continue + if not (a["iv"] > 0 and b["iv"] > 0): + continue + base.append(abs(a["iv"] - b["iv"])) + g = byexp.get(round(b["dte"], 4), []) + j = next((i for i, x in enumerate(g) if x["name"] == b["name"]), None) + if j is not None and j + 1 < len(g) and g[j + 1]["iv"] > 0: + shift.append(abs(a["iv"] - g[j + 1]["iv"])) + mb, ms = med(base), med(shift) + ok = ms > 2.0 * mb + print(f" {lin:<12} {mb:>20.3f} {ms:>23.3f} {ms/max(mb,1e-9):>9.1f}x " + f"{'POTENZA OK' if ok else '⚠️ SENZA POTENZA':>10}") + print(" ⚠️ Il rapporto e' enorme perche' il denominatore e' ESATTAMENTE zero (vedi 2b):") + print(" il controllo passa, ma su una grandezza degenere. Percio' lo stesso controllo va") + print(" rifatto sulla grandezza NON degenere, il mid eseguibile:") + print(f" {'coppia':<12} {'|Δmid| stesso strike':>21} {'|Δmid| adiacente':>18} " + f"{'rapporto':>9} {'esito':>16}") + for lin, inv in PAIRS: + byexp: dict = {} + for r in rows: + if r["fam"] == inv and r["typ"] == "P" and r["bid"] and r["ask"]: + byexp.setdefault(round(r["dte"], 4), []).append(r) + for k in byexp: + byexp[k] = sorted(byexp[k], key=lambda x: x["K"]) + base, shift = [], [] + for a, b in cp: + if a["fam"] != lin or a["typ"] != "P" or not (DTE_LO <= a["dte"] <= DTE_HI): + continue + if not (a["bid"] and a["ask"] and b["bid"] and b["ask"] and a["mark"] > 0): + continue + ma = 0.5 * (u(a["bid"], a) + u(a["ask"], a)) + mb = 0.5 * (u(b["bid"], b) + u(b["ask"], b)) + base.append(abs(ma - mb) / u(a["mark"], a)) + g = byexp.get(round(b["dte"], 4), []) + j = next((i for i, x in enumerate(g) if x["name"] == b["name"]), None) + if j is not None and j + 1 < len(g): + c = g[j + 1] + shift.append(abs(ma - 0.5 * (u(c["bid"], c) + u(c["ask"], c))) / u(a["mark"], a)) + r0, r1 = med(base), med(shift) + print(f" {lin:<12} {r0:>20.2%} {r1:>17.2%} {r1/max(r0,1e-12):>9.1f}x " + f"{('POTENZA OK' if r1 > 2*r0 else '⚠️ SENZA POTENZA'):>16}") + print(" Se lo strike sbagliato NON alzasse la distanza, il confronto non distinguerebbe") + print(" due superfici diverse e ogni 'coincidono' sarebbe vuoto.") + return res + + +# ================================================================== §3 il pavimento del tick +DBUCK = ((0.00, 0.02), (0.02, 0.05), (0.05, 0.10), (0.10, 0.20), (0.20, 0.35), (0.35, 0.65)) +TBUCK = ((0.0, 7.0), (7.0, 30.0), (30.0, 95.0)) + + +def sez3_tick(rows: list[dict], att: dict) -> dict: + print("\n" + "=" * 104) + print("3. IL PAVIMENTO DEL TICK — `tick effettivo / premio equo`, per delta e per scadenza") + print("=" * 104) + print(" E' il numero TRASFERIBILE del filone: non dipende dal capitale, non dipende dal") + print(" lotto, non dipende dal giorno. `>1` significa che il premio equo sta SOTTO un tick,") + print(" cioe' che quell'opzione non e' quotabile al suo valore: il prezzo minimo possibile") + print(" e' gia' un multiplo del giusto. Il rapporto E' quel multiplo.") + out: dict = {} + for lin, inv in PAIRS: + print(f"\n --- {lin} contro {inv} (mediana del rapporto; 'n' = strumenti nel riquadro)") + print(f" {'|delta|':<12} " + " ".join(f"{f'{a:.0f}-{b:.0f}g':>18}" for a, b in TBUCK)) + for dlo, dhi in DBUCK: + cells = [] + for tlo, thi in TBUCK: + vals = {} + for f in (lin, inv): + sub = [r for r in rows if r["fam"] == f and r["typ"] == "P" + and np.isfinite(r["delta"]) and dlo <= abs(r["delta"]) < dhi + and tlo <= r["dte"] < thi and r["mark"] > 0] + vals[f] = med([eff_tick(r["mark"], r["tick0"], r["steps"]) / r["mark"] + for r in sub]) + out[(f, dlo, tlo)] = (vals[f], len(sub)) + cells.append(f"{vals[lin]:>7.2f} /{vals[inv]:>7.2f}") + print(f" {f'{dlo:.2f}-{dhi:.2f}':<12} " + " ".join(f"{c:>18}" for c in cells)) + print(f" {'':<12} " + " ".join(f"{'lineare / inverse':>18}" for _ in TBUCK)) + # C2: controllo positivo del rilevatore + atm = [v for (f, d, t), (v, n) in out.items() if d == 0.35 and np.isfinite(v)] + w05 = [v for (f, d, t), (v, n) in out.items() if d == 0.02 and np.isfinite(v)] + w02 = [v for (f, d, t), (v, n) in out.items() if d == 0.00 and np.isfinite(v)] + print(f"\n --- C2 CONTROLLO POSITIVO del rilevatore") + print(f" soglia DICHIARATA A PRIORI: ala > 20x ATM.") + print(f" ATM (|d| 0.35-0.65) mediana {med(atm):.4f} · |d| 0.02-0.05 {med(w05):.3f} " + f"= {med(w05)/max(med(atm),1e-12):.1f}x -> " + f"{'PASSA' if med(w05) > 20*med(atm) else 'NON PASSA la soglia a priori'}") + print(f" ⚠️ ERRORE MIO, non del rilevatore: la griglia di delta che avevo dichiarato") + print(f" NON CONTENEVA l'ala di cui parla §46. Una put marcata 0,82 USDC su un") + print(f" sottostante a 77k non sta a |delta| 0.02-0.05, sta SOTTO 0.02. Bucket") + print(f" aggiunto: |d| 0.00-0.02 mediana {med(w02):.3f} = " + f"{med(w02)/max(med(atm),1e-12):.0f}x ATM -> " + f"{'SEPARA' if med(w02) > 20*med(atm) else 'NON SEPARA'}") + worst = med([v for (f, d, t), (v, n) in out.items() + if d == 0.00 and t == 0.0 and np.isfinite(v)]) + print(f" ⚠️ E la soglia che avevo dichiarato era mal posta anche come STATISTICA: una") + print(f" mediana di bucket mescola 0-7g e 30-95g. Sulla cella dove il fenomeno vive") + print(f" (|d|<0.02 E 0-7g) il rapporto e' {worst:.2f} = {worst/max(med(atm),1e-12):.0f}x ATM.") + print(f" Riporto la soglia come dichiarata (NON superata) e la correzione accanto.") + mono = all(med([v for (f, d, t), (v, n) in out.items() if d == a and np.isfinite(v)]) + >= med([v for (f, d, t), (v, n) in out.items() if d == b and np.isfinite(v)]) + for a, b in zip([x[0] for x in DBUCK], [x[0] for x in DBUCK][1:])) + print(f" monotonia sui {len(DBUCK)} bucket di delta (il rapporto deve solo CRESCERE") + print(f" andando verso l'ala): {'SI' if mono else 'NO'}") + print(" Un rilevatore che desse lo stesso numero ATM e sull'ala sarebbe rotto, non muto.") + # il numero di §46, ricalcolato in modo appaiato + print("\n --- il '6x' di §46, rifatto sulle DUE famiglie invece che su una sola") + for lin, inv in PAIRS: + for f in (lin, inv): + sub = [r for r in rows if r["fam"] == f and r["typ"] == "P" and r["mark"] > 0 + and np.isfinite(r["delta"]) and abs(r["delta"]) < 0.05 and r["dte"] <= 30] + if not sub: + continue + rat = [eff_tick(r["mark"], r["tick0"], r["steps"]) / r["mark"] for r in sub] + nq = sum(1 for x in rat if x > 1.0) + print(f" {f:<10} ali |d|<0.05, <=30g: n={len(sub):>4} " + f"tick/premio mediano {med(rat):>6.2f}x " + f"non quotabili al giusto: {nq}/{len(sub)} ({nq/len(sub):.0%})") + att["A3_6x"] = "ala |d|<0.05: mediana misurata (vedi tabella)" + + # --- la versione APPAIATA del pavimento: la tabella qui sopra confronta POPOLAZIONI + # diverse (l'inverse ha 624 strike che la lineare non ha) e in una cella si ribalta. + print("\n --- LO STESSO PAVIMENTO, APPAIATO (stesso strumento nelle due famiglie)") + print(" La tabella qui sopra confronta POPOLAZIONI diverse: l'inverse ha 624 strike che") + print(" la lineare non ha, e in una cella (BTC |d|<0.02, 0-7g) il segno si ROVESCIA.") + print(" Appaiato non puo' succedere: il numeratore e' un fatto del venue e il") + print(" denominatore e' lo stesso premio (una sola superficie di mark, §2b).") + idx = {r["name"]: r for r in rows} + print(f" {'coppia':<12} {'n appaiate':>11} {'rapporto lin/inv':>17} " + f"{'atteso da tick $':>17} {'lin quotabile & inv NO':>24} {'il contrario':>13}") + for lin, inv in PAIRS: + rr, only_lin, only_inv = [], 0, 0 + for r in rows: + if r["fam"] != lin or r["typ"] != "P" or r["mark"] <= 0: + continue + t = idx.get(r["twin"] or "") + if t is None or t["mark"] <= 0: + continue + a = eff_tick(r["mark"], r["tick0"], r["steps"]) / r["mark"] + b = eff_tick(t["mark"], t["tick0"], t["steps"]) / t["mark"] + rr.append(a / b) + only_lin += int(a < 1.0 <= b) + only_inv += int(b < 1.0 <= a) + sub = [r for r in rows if r["fam"] == lin and r["typ"] == "P"] + sui = [r for r in rows if r["fam"] == inv and r["typ"] == "P"] + F = med([r["F"] for r in sui]) + atteso = (sub[0]["tick0"]) / (sui[0]["tick0"] * F) + print(f" {lin:<12} {len(rr):>11} {med(rr):>17.3f} {atteso:>17.3f} " + f"{only_lin:>24} {only_inv:>13}") + print(" `atteso da tick $` = (tick lineare in $) / (tick inverse in $): se il rapporto") + print(" misurato coincide, il pavimento e' esattamente il tick e nient'altro.") + + # --- cosa costa DAVVERO comprare l'ala: il pavimento si paga sull'ASK, non sul mark + print("\n --- IL PAVIMENTO SI PAGA SULL'ASK: quanto si paga sopra il premio equo, per") + print(" COMPRARE (lato long, l'unico che il progetto puo' guardare)") + print(f" {'|delta|':<12} " + " ".join(f"{f:>18}" for f in + ("BTC_USDC", "BTC", "ETH_USDC", "ETH"))) + for dlo, dhi in DBUCK: + cells = [] + for f in ("BTC_USDC", "BTC", "ETH_USDC", "ETH"): + sub = [r for r in rows if r["fam"] == f and r["typ"] == "P" and r["ask"] + and r["mark"] > 0 and np.isfinite(r["delta"]) + and dlo <= abs(r["delta"]) < dhi and r["dte"] <= 45.0] + v = med([r["ask"] / r["mark"] for r in sub]) + cells.append(f"{v:>10.2f}x n={len(sub):<4}") + print(f" {f'{dlo:.2f}-{dhi:.2f}':<12} " + " ".join(f"{c:>18}" for c in cells)) + print(" `x` = ask / premio equo del venue. E' il sovrapprezzo che si paga per") + print(" comprare quell'ala, e sull'ala profonda NON e' negoziabile: e' il tick.") + print("\n ⚠️ anche questa tabella confronta POPOLAZIONI diverse. La versione appaiata") + print(" (stesso strumento, solo dove ENTRAMBE hanno un ask):") + print(f" {'|delta|':<12} " + " ".join(f"{f'{l} / {i}':>26}" for l, i in PAIRS)) + for dlo, dhi in DBUCK: + cells = [] + for lin, inv in PAIRS: + rr = [] + for r in rows: + if (r["fam"] != lin or r["typ"] != "P" or not r["ask"] or r["mark"] <= 0 + or not np.isfinite(r["delta"]) or not (dlo <= abs(r["delta"]) < dhi) + or r["dte"] > 45.0): + continue + t = idx.get(r["twin"] or "") + if t is None or not t["ask"] or t["mark"] <= 0: + continue + rr.append((r["ask"] / r["mark"]) / (t["ask"] / t["mark"])) + cells.append(f"{med(rr):>8.3f} n={len(rr):<4}") + print(f" {f'{dlo:.2f}-{dhi:.2f}':<12} " + " ".join(f"{c:>26}" for c in cells)) + print(" <1 = la LINEARE si compra piu' vicino al giusto; >1 = la inverse.") + + # --- il premio minimo quotabile, per lotto: il numero che nessun capitale aggira + print("\n --- IL PREMIO MINIMO QUOTABILE PER UN LOTTO (1 tick x lotto minimo)") + print(f" {'famiglia':<10} {'tick base':>11} {'lotto min':>10} {'premio min $/lotto':>19} " + f"{'noz. lotto $':>13} {'premio min / nozionale':>23}") + for lin, inv in PAIRS: + for f in (lin, inv): + sub = [r for r in rows if r["fam"] == f and r["typ"] == "P"] + if not sub: + continue + a = sub[0] + F = med([r["F"] for r in sub]) + pmin = a["tick0"] * a["minamt"] * (F if a["inv"] else 1.0) + noz = a["minamt"] * F + print(f" {f:<10} {a['tick0']:>11g} {a['minamt']:>10g} {pmin:>19.3f} " + f"{noz:>13,.0f} {pmin/noz:>22.5%}") + print(" A sinistra c'e' la granularita' (quanto piccolo puo' essere un biglietto), a") + print(" destra il pavimento in proporzione: e' il secondo che decide se un'ala e'") + print(" comprabile al suo valore, ed e' INDIPENDENTE dal capitale.") + return out + + +# ================================================================== §4 le strutture +def scegli_put(rows: list[dict], fam: str, target: float, lato: str, + dte_lo: float, dte_hi: float) -> list[dict]: + """Per ogni scadenza, la put col delta piu' vicino al target FRA QUELLE CHE HANNO IL LATO + CHE SERVE (bid per vendere, ask per comprare). Un book a un lato solo non e' un book vuoto: + e' un book che serve una direzione sola, e la restrizione giusta e' asimmetrica.""" + g = [r for r in rows if r["fam"] == fam and r["typ"] == "P" and np.isfinite(r["delta"]) + and dte_lo <= r["dte"] <= dte_hi + and ((r["bid"] is not None) if lato == "vendi" else (r["ask"] is not None))] + out = [] + for e in sorted({round(r["dte"], 4) for r in g}): + gg = [r for r in g if round(r["dte"], 4) == e] + out.append(min(gg, key=lambda r: abs(r["delta"] - target))) + return out + + +def sez4_strutture(rows: list[dict], att: dict) -> None: + print("\n" + "=" * 104) + print(f"4. LA DOMANDA CHE DECIDE — cosa entra in un conto da ${CAPITAL:.0f}, e in quale famiglia") + print("=" * 104) + print(" Tre strutture a rischio definito, per UN LOTTO MINIMO, ai prezzi ESEGUIBILI") + print(" (si compra all'ask e si vende al bid, mai al mid: §28).") + print(" L1 LONG put d-0,05 ... l'assicurazione di coda di §46") + print(" L2 LONG debit spread compra d-0,28 / vende d-0,10 ... rischio definito, lato long") + print(" S1 SHORT credit spread vende d-0,28 / compra d-0,10 ... la geometria di VRP01") + print(f"\n{'struttura':<4} {'famiglia':<10} {'scad':>4} {'ticket $':>9} {'max perdita $':>13} " + f"{'eseg/equo':>10} {'fee/ticket':>10} {'n a 20% conto':>13} {'entra a $635?':>13}") + tab: dict = {} + for lin, inv in PAIRS: + for f in (lin, inv): + # L1 -------------------------------------------------------------- long put di coda + gs = scegli_put(rows, f, TAIL_D, "compra", 5.0, 35.0) + if gs: + tick_ = [lot_usd(r["ask"], r) for r in gs] + equo = [lot_usd(r["mark"], r) for r in gs] + fee = [fee_lot(r, r["ask"]) for r in gs] + t, e_ = med(tick_), med(equo) + n = math.floor(RISK_BUDGET * CAPITAL / t) if t > 0 else 0 + tab[("L1", f)] = t + print(f"{'L1':<4} {f:<10} {len(gs):>4} {t:>9.2f} {t:>13.2f} " + f"{(t/e_ if e_ > 0 else float('nan')):>10.2f} " + f"{med(fee)/max(t,1e-9):>9.0%} {n:>13} " + f"{'SI' if t <= CAPITAL else 'NO':>13}") + # L2 -------------------------------------------------------- long put debit spread + hi = scegli_put(rows, f, SHORT_D, "compra", DTE_LO, DTE_HI) + lo = scegli_put(rows, f, LONG_D, "vendi", DTE_LO, DTE_HI) + lom = {round(r["dte"], 4): r for r in lo} + cost, costm, fees = [], [], [] + for a in hi: + b = lom.get(round(a["dte"], 4)) + if b is None or b["K"] >= a["K"]: + continue + c = lot_usd(a["ask"], a) - lot_usd(b["bid"], b) + cm = lot_usd(a["mark"], a) - lot_usd(b["mark"], b) + if cm <= 0: + continue + cost.append(c); costm.append(cm) + fees.append(fee_lot(a, a["ask"]) + fee_lot(b, b["bid"])) + if cost: + t, e_ = med(cost), med(costm) + n = math.floor(RISK_BUDGET * CAPITAL / t) if t > 0 else 0 + tab[("L2", f)] = t + print(f"{'L2':<4} {f:<10} {len(cost):>4} {t:>9.2f} {t:>13.2f} " + f"{t/e_:>10.2f} {med(fees)/max(t,1e-9):>9.0%} {n:>13} " + f"{'SI' if t <= CAPITAL else 'NO':>13}") + # S1 ------------------------------------------------------- short put credit spread + sh = scegli_put(rows, f, SHORT_D, "vendi", DTE_LO, DTE_HI) + wg = scegli_put(rows, f, LONG_D, "compra", DTE_LO, DTE_HI) + wgm = {round(r["dte"], 4): r for r in wg} + ml, cred, credm, fees = [], [], [], [] + for a in sh: + b = wgm.get(round(a["dte"], 4)) + if b is None or b["K"] >= a["K"]: + continue + c = lot_usd(a["bid"], a) - lot_usd(b["ask"], b) + cm = lot_usd(a["mark"], a) - lot_usd(b["mark"], b) + if cm <= 0: + continue + cred.append(c); credm.append(cm) + ml.append((a["K"] - b["K"]) * a["minamt"] - c) + fees.append(fee_lot(a, a["bid"]) + fee_lot(b, b["ask"])) + if ml: + m, e_ = med(ml), med(credm) + n = math.floor(RISK_BUDGET * CAPITAL / m) if m > 0 else 0 + tab[("S1", f)] = m + print(f"{'S1':<4} {f:<10} {len(ml):>4} {med(cred):>9.2f} {m:>13.2f} " + f"{med(cred)/e_:>10.2f} {med(fees)/max(abs(med(cred)),1e-9):>9.0%} {n:>13} " + f"{'SI' if m <= CAPITAL else 'NO':>13}") + print("\n `eseg/equo` = ticket eseguibile / ticket al mark del venue. Sul LONG e' >1 (paghi") + print(" di piu' del giusto), sul credit e' <1 (incassi di meno): e' il f_venue di §14.") + print(" `max perdita` per L1/L2 e' il ticket stesso (rischio definito per costruzione).") + print(" ⚠️ S1 e' misurata per COMPLETEZZA: la regola del 19/06 (`niente short-vol da modello") + print(" in deploy`) non e' una regola di eseguibilita' e non decade col lotto.") + att["A6"] = ("CONFERMATA" if all(tab.get(("L1", f), 9e9) < CAPITAL + for _, f in PAIRS) else "REFUTATA") + print("\n --- LA DOMANDA DI §64: esiste sulla LINEARE una struttura che l'INVERSE non fa?") + for lin, inv in PAIRS: + for k, nome in (("L1", "long put di coda"), ("L2", "long debit spread"), + ("S1", "short credit spread")): + a, b = tab.get((k, lin)), tab.get((k, inv)) + if a is None or b is None: + print(f" {k} {nome:<22} {lin:<10} {'n/d' if a is None else f'{a:8.2f}'} " + f" {inv:<4} {'n/d' if b is None else f'{b:8.2f}'}") + continue + v_lin = a <= RISK_BUDGET * CAPITAL + v_inv = b <= RISK_BUDGET * CAPITAL + marca = ("SOLO LINEARE" if (v_lin and not v_inv) else + "entrambe" if (v_lin and v_inv) else + "SOLO INVERSE" if v_inv else "nessuna delle due") + print(f" {k} {nome:<22} {lin:<10} ${a:>8.2f} {inv:<4} ${b:>8.2f} " + f"a budget {RISK_BUDGET:.0%}: {marca}") + + +# ================================================================== §5 profondita' e controlli +def sez5_profondita(rows: list[dict], raw: dict) -> None: + print("\n" + "=" * 104) + print("5. PROFONDITA' IN CIMA AL BOOK — sulle STESSE gambe nelle due famiglie") + print("=" * 104) + d = raw.get("depth") or {} + if not d: + print(" nessun order_book in questo snapshot (raccolto con --no-depth?).") + return + idx = {r["name"]: r for r in rows} + print(f"{'strumento':<26} {'delta':>7} {'lato':<5} {'lotti al best':>13} " + f"{'$ al best':>11} {'prezzo/tick':>11} {'|d venue-BS|':>12}") + for n in sorted(d): + r = idx.get(n) + if r is None: + continue + ob = d[n] + for lato, side in (("bid", "bids"), ("ask", "asks")): + lv = ob.get(side) or [] + if not lv: + print(f"{n:<26} {r['delta']:>7.3f} {lato:<5} {'book vuoto su questo lato':>13}") + continue + px, sz = float(lv[0][0]), float(lv[0][1]) + gv = (ob.get("greeks") or {}).get("delta") + dd = abs(float(gv) - r["delta"]) if (gv is not None + and np.isfinite(r["delta"])) else float("nan") + print(f"{n:<26} {r['delta']:>7.3f} {lato:<5} " + f"{sz/max(r['minamt'],1e-12):>13,.0f} {u(px, r)*sz:>11,.0f} " + f"{px/max(eff_tick(px, r['tick0'], r['steps']),1e-12):>11.1f} {dd:>12.4f}") + print("\n `lotti al best` = size in cima / lotto minimo: quante strutture minime passano") + print(" senza muovere il prezzo. `prezzo/tick` e' il controllo d'artefatto del 30/07 (un") + print(" prezzo a 1-2 tick e' una griglia, non un prezzo). `|d venue-BS|` valida il MIO") + print(" delta contro le greche del venue: se non e' piccolo, tutta la selezione e' sbagliata.") + + +def sez5b_controlli(rows: list[dict], att: dict) -> None: + print("\n" + "-" * 104) + print("5b. CONTROLLI C3 e C4") + print("-" * 104) + # C3 — il rilevatore di larghezza separa una famiglia nota-larga da una nota-stretta? + print(f" {'famiglia':<10} {'put 4-45g':>10} {'2 lati':>7} {'%':>6} {'spr% mediano':>13} " + f"{'best bid a 1 tick':>18}") + for f in ("BTC_USDC", "SOL_USDC"): + puts = [r for r in rows if r["fam"] == f and r["typ"] == "P" + and DTE_LO <= r["dte"] <= DTE_HI] + two = [r for r in puts if r["bid"] is not None and r["ask"] is not None] + onetick = [r for r in puts if r["bid"] is not None + and r["bid"] <= 1.01 * eff_tick(r["bid"], r["tick0"], r["steps"])] + print(f" {f:<10} {len(puts):>10} {len(two):>7} " + f"{(len(two)/len(puts) if puts else float('nan')):>5.0%} " + f"{med([relspread(r) for r in two])*100:>12.1f}% " + f"{len(onetick)/max(len([r for r in puts if r['bid'] is not None]),1):>17.0%}") + print(" C3: se BTC_USDC e SOL_USDC uscissero uguali, il rilevatore non separa niente.") + # C4 — tipicita' del giorno, sulla grandezza gia' misurata su 19 settimane feriali + print("\n C4 tipicita': attraversare lo spread sul credit spread canonico INVERSE costa") + print(" ~10% del credito (misura del 30/07 su 19 settimane FERIALI); §14 ha letto") + print(" 7,0-7,3% di sabato. Oggi e' domenica:") + for _, inv in PAIRS: + sh = scegli_put(rows, inv, SHORT_D, "vendi", DTE_LO, DTE_HI) + wg = {round(r["dte"], 4): r for r in scegli_put(rows, inv, LONG_D, "compra", + DTE_LO, DTE_HI)} + fv = [] + for a in sh: + b = wg.get(round(a["dte"], 4)) + if b is None or b["K"] >= a["K"]: + continue + cm = lot_usd(a["mark"], a) - lot_usd(b["mark"], b) + ce = lot_usd(a["bid"], a) - lot_usd(b["ask"], b) + if cm > 0: + fv.append(1.0 - ce / cm) + print(f" {inv:<4} inverse: costo di attraversamento = {med(fv)*100:5.1f}% del credito" + f" (atteso ~7-10%)") + ok = all(0.0 <= med([1.0 - (lot_usd(a["bid"], a) - lot_usd(b["ask"], b)) + / (lot_usd(a["mark"], a) - lot_usd(b["mark"], b)) + for a in scegli_put(rows, inv, SHORT_D, "vendi", DTE_LO, DTE_HI) + for b in [{round(r["dte"], 4): r for r in + scegli_put(rows, inv, LONG_D, "compra", DTE_LO, DTE_HI)} + .get(round(a["dte"], 4))] + if b is not None and b["K"] < a["K"] + and (lot_usd(a["mark"], a) - lot_usd(b["mark"], b)) > 0]) <= 0.25 + for _, inv in PAIRS) + att["C4"] = "il giorno non e' anomalo sui book noti" if ok else "⚠️ giorno anomalo" + print(" E' un controllo DEBOLE (una grandezza, un istante), non una prova di tipicita'.") + + +def sez6_dispersione(snaps: list[dict]) -> None: + print("\n" + "=" * 104) + print(f"6. DISPERSIONE FRA SNAPSHOT — {len(snaps)} istantanee della stessa sessione") + print("=" * 104) + if len(snaps) < 2: + print(" UNA SOLA ISTANTANEA: qui non c'e' dispersione da riportare, e va detto cosi'.") + print(" Rilancia con --snap a distanza di minuti per avere la banda infra-sessione.") + return + print(f"{'ora UTC':<18} {'USDC OI>=100':>12} {'inv OI>=100':>12} " + f"{'BTC_USDC 2lati%':>16} {'ETH_USDC 2lati%':>16} {'SOL_USDC 2lati%':>16} " + f"{'ΔIV BTC':>9} {'ΔIV ETH':>9} {'Δspr BTC pp':>12} {'Δspr ETH pp':>12}") + acc: dict = {k: [] for k in ("uo", "io", "bq", "eq", "sq", "biv", "eiv", "bsp", "esp")} + for raw in snaps: + rows = build(raw) + cp = coppie(rows) + def q(f): + puts = [r for r in rows if r["fam"] == f and r["typ"] == "P" + and DTE_LO <= r["dte"] <= DTE_HI] + two = [r for r in puts if r["bid"] is not None and r["ask"] is not None] + return len(two) / len(puts) if puts else float("nan") + def dd(lin): + both = [(a, b) for a, b in cp if a["fam"] == lin and a["typ"] == "P" + and DTE_LO <= a["dte"] <= DTE_HI and a["bid"] and a["ask"] + and b["bid"] and b["ask"] and a["iv"] > 0 and b["iv"] > 0] + return (med([a["iv"] - b["iv"] for a, b in both]), + med([relspread(a) - relspread(b) for a, b in both])) + uo = sum(1 for r in rows if r["fam"] in ("BTC_USDC", "ETH_USDC") + and 0 < r["dte"] <= DTE_MAX_COLL and r["oi"] >= OI_MIN) + io = sum(1 for r in rows if r["fam"] in ("BTC", "ETH") + and 0 < r["dte"] <= DTE_MAX_COLL and r["oi"] >= OI_MIN) + biv, bsp = dd("BTC_USDC") + eiv, esp = dd("ETH_USDC") + bq, eq, sq = q("BTC_USDC"), q("ETH_USDC"), q("SOL_USDC") + for k, v in zip(acc, (uo, io, bq, eq, sq, biv, eiv, bsp, esp)): + acc[k].append(v) + print(f"{raw['ts'][11:19]:<18} {uo:>12} {io:>12} {bq:>15.0%} {eq:>15.0%} {sq:>15.0%} " + f"{biv:>+9.3f} {eiv:>+9.3f} {bsp*100:>+11.2f} {esp*100:>+11.2f}") + print("\n banda (min-max) sulle grandezze che portano il verdetto:") + for k, nome in (("uo", "USDC OI>=100"), ("io", "inverse OI>=100"), + ("bq", "BTC_USDC quotati 2 lati"), ("eq", "ETH_USDC quotati 2 lati"), + ("sq", "SOL_USDC quotati 2 lati"), + ("biv", "ΔIV appaiata BTC (pt vol)"), ("eiv", "ΔIV appaiata ETH (pt vol)"), + ("bsp", "Δspread appaiato BTC"), ("esp", "Δspread appaiato ETH")): + v = [x for x in acc[k] if np.isfinite(x)] + if v: + print(f" {nome:<28} min {min(v):+10.4f} mediana {np.median(v):+10.4f} " + f"max {max(v):+10.4f}") + print("\n ⚠️ E' dispersione INFRA-SESSIONE: dice se il numero balla fra un giro e l'altro,") + print(" NON se un altro giorno della settimana darebbe un'altra risposta.") + + +# ================================================================== §7 la decisione su §51 +def sez7_decisione(rows: list[dict], res2: dict, att: dict) -> dict: + print("\n" + "=" * 104) + print("7. LA DECISIONE CHE DIPENDE DA QUESTO FILONE — raccogliere o no la catena USDC (§51)") + print("=" * 104) + print(" §51 raccomanda di RACCOGLIERLA (+117 chiamate/giro, +18%) con UNA giustificazione:") + print(" *rende onesti i numeri di uno sleeve vivo (VRP01), perche' il f=0,73 e' misurato") + print(" sulla famiglia inverse che un conto USDC non puo' marginare*. Quella") + print(" giustificazione e' una PREVISIONE: che la superficie lineare prezzi diversamente.") + print(" Qui la previsione si misura, appaiata.") + print("\n Il f di VRP01 ha DUE pezzi, e vanno separati perche' si trasferiscono in modo") + print(" diverso:") + print(" (i) errore di MODELLO (il sleeve prezza a DVOL ATM: struttura a termine 42% +") + print(" skew 25%, §5/§35). Vive nella IV -> si trasferisce se ΔIV appaiata ~ 0.") + print(" (ii) costo di ATTRAVERSAMENTO (bid/ask). Vive nello spread -> si trasferisce solo") + print(" se lo spread appaiato e' simile.") + out = {} + print("\n (i) SI TRASFERISCE PER COSTRUZIONE, e non perche' l'abbia misurato io: §2b mostra") + print(" che il venue marca le due famiglie sulla STESSA superficie (mark_iv identica") + print(" nel 95% delle coppie, il resto e' l'arrotondamento a 2 decimali). L'errore di") + print(" modello del sleeve e' calcolato CONTRO quella superficie: e' lo stesso numero") + print(" nelle due famiglie, e nessuna raccolta di dati puo' cambiarlo.") + print(f" {'coppia':<10} {'ΔIV appaiata':>13} {'mezzo-spread IV':>16} {'rapporto':>9}" + f" (test SENZA POTENZA, riportato per trasparenza)") + for lin, _ in PAIRS: + r = res2[lin] + rr = abs(r["div"]) / r["halfiv"] if r["halfiv"] and np.isfinite(r["halfiv"]) else np.nan + out[f"iv_{lin}"] = rr + print(f" {lin:<10} {r['div']:>+13.3f} {r['halfiv']:>16.3f} {rr:>9.2f}") + print("\n (ii-a) IL COSTO DI ATTRAVERSAMENTO, per GAMBA, su tutte le coppie a due lati") + print(" (e' la misura robusta: ~130 osservazioni appaiate invece di 3 strutture)") + print(f" {'coppia':<10} {'n gambe':>8} {'mezzo-spread lineare':>21} " + f"{'mezzo-spread inverse':>21} {'Δ appaiata':>11}") + for lin, _ in PAIRS: + r = res2[lin] + out[f"dh_{lin}"] = r.get("dhalf", float("nan")) + print(f" {lin:<10} {r.get('nboth', 0):>8} {r.get('hl', float('nan')):>20.2%} " + f"{r.get('hi', float('nan')):>20.2%} {r.get('dhalf', float('nan')):>+10.2%}") + # (ii) f_venue APPAIATO: la stessa struttura, gli stessi strike, nelle due famiglie + print(f"\n (ii-b) f_venue sulla STESSA struttura e sugli STESSI strike nelle due famiglie") + print(f" {'coppia':<10} {'n strutture':>12} {'f_venue lineare':>16} " + f"{'f_venue inverse':>16} {'Δ appaiata':>11}") + idx = {r["name"]: r for r in rows} + for lin, inv in PAIRS: + sh = scegli_put(rows, lin, SHORT_D, "vendi", DTE_LO, DTE_HI) + wg = {round(r["dte"], 4): r for r in scegli_put(rows, lin, LONG_D, "compra", + DTE_LO, DTE_HI)} + fl, fi, dl = [], [], [] + for a in sh: + b = wg.get(round(a["dte"], 4)) + if b is None or b["K"] >= a["K"]: + continue + A, B = idx.get(a["twin"] or ""), idx.get(b["twin"] or "") + if A is None or B is None: + continue + if not (a["bid"] and b["ask"] and A["bid"] and B["ask"]): + continue + cmL = lot_usd(a["mark"], a) - lot_usd(b["mark"], b) + ceL = lot_usd(a["bid"], a) - lot_usd(b["ask"], b) + cmI = lot_usd(A["mark"], A) - lot_usd(B["mark"], B) + ceI = lot_usd(A["bid"], A) - lot_usd(B["ask"], B) + if cmL <= 0 or cmI <= 0: + continue + fl.append(ceL / cmL); fi.append(ceI / cmI); dl.append(ceL / cmL - ceI / cmI) + out[f"fv_{lin}"] = med(dl) + out[f"fvn_{lin}"] = len(dl) + print(f" {lin:<10} {len(dl):>12} {med(fl):>16.3f} {med(fi):>16.3f} " + f"{med(dl):>+11.3f}") + print("\n ⚠️ e' la MEDIANA DELLE DIFFERENZE APPAIATE su strutture con gli STESSI") + print(" strike: §14 confrontava strike auto-selezionati e misurava anche la griglia.") + + print("\n (iii) L'UNICA COSA DAVVERO DIVERSA FRA LE DUE FAMIGLIE E' LA GRIGLIA DEGLI") + print(" STRIKE: la lineare e' un sottoinsieme (§1). Quanto deforma la struttura?") + print(" Si sceglie il delta bersaglio due volte: fra TUTTI gli strike inverse, e") + print(" fra i soli strike che esistono ANCHE sulla lineare.") + print(f" {'coppia':<10} {'bersaglio':>10} {'scad':>5} {'|d| griglia piena':>18} " + f"{'|d| griglia lineare':>20} {'Δ strike medio':>15}") + for lin, inv in PAIRS: + twins = {r["twin"] for r in rows if r["fam"] == lin and r["twin"]} + for tgt in (SHORT_D, LONG_D, TAIL_D): + full, restr, dk = [], [], [] + for e in sorted({round(r["dte"], 4) for r in rows + if r["fam"] == inv and r["typ"] == "P" + and DTE_LO <= r["dte"] <= DTE_HI}): + g = [r for r in rows if r["fam"] == inv and r["typ"] == "P" + and round(r["dte"], 4) == e and np.isfinite(r["delta"])] + gr = [r for r in g if r["name"] in twins] + if not g or not gr: + continue + a = min(g, key=lambda r: abs(r["delta"] - tgt)) + b = min(gr, key=lambda r: abs(r["delta"] - tgt)) + full.append(abs(a["delta"] - tgt)); restr.append(abs(b["delta"] - tgt)) + dk.append(abs(a["K"] - b["K"])) + if full: + print(f" {lin:<10} {tgt:>10.2f} {len(full):>5} {med(full):>18.4f} " + f"{med(restr):>20.4f} {med(dk):>15,.0f}") + print(" Se le due colonne coincidono, la griglia piu' povera NON deforma la") + print(" struttura e un backtest fatto sull'inverse resta eseguibile sulla lineare.") + return out + + +def verdetto(att: dict, dec: dict, rows: list[dict]) -> None: + print("\n" + "=" * 104) + print("8. ATTESE DICHIARATE PRIMA DI MISURARE — esito") + print("=" * 104) + for k in sorted(att): + print(f" {k:<14} {att[k]}") + print("\n" + "=" * 104) + print("9. VERDETTO") + print("=" * 104) + iv_ok = True # per COSTRUZIONE: una sola superficie di mark al venue (§2b) + dh = [dec.get(f"dh_{l}") for l, _ in PAIRS] + fv = [dec.get(f"fv_{l}") for l, _ in PAIRS] + dh_ok = all(x is not None and np.isfinite(x) and abs(x) < 0.01 for x in dh) + fv_ok = all(x is not None and np.isfinite(x) and abs(x) < 0.05 for x in fv) + print(" (i) il PREZZO EQUO e' lo stesso numero per costruzione (una sola superficie di") + print(" mark al venue, §2b) -> nessun dato da raccogliere puo' cambiarlo") + print(f" (ii-a) mezzo-spread per gamba, Δ appaiata entro 1pp: {'SI' if dh_ok else 'NO'}" + + " (Δ misurate: " + ", ".join(f"{x:+.2%}" for x in dh if x is not None) + ")") + print(f" (ii-b) f_venue di struttura, Δ appaiata entro 0,05: {'SI' if fv_ok else 'NO'}" + + " (Δ misurate: " + ", ".join(f"{x:+.3f}" for x in fv if x is not None) + ")") + fv_ok = fv_ok and dh_ok + if iv_ok and fv_ok: + print("\n 🚨 LE DUE SUPERFICI SONO LA STESSA SUPERFICIE, a strike appaiati: la catena") + print(" INVERSE che il progetto gia' raccoglie e' un proxy sufficiente della lineare") + print(" sia sul PREZZO sia sul COSTO DI ATTRAVERSAMENTO.") + print(" => LA RACCOMANDAZIONE DI §51 (raccogliere la catena USDC, +18% di chiamate/giro)") + print(" NON E' GIUSTIFICATA DALLA RAGIONE CHE PORTA: il f di VRP01 misurato") + print(" sull'inverse non e' 'il numero della famiglia sbagliata', e' lo stesso numero.") + elif iv_ok and not fv_ok: + print("\n ⚠️ Il PREZZO coincide ma il COSTO DI ATTRAVERSAMENTO no: la meta' 'errore di") + print(" modello' del f si trasferisce, la meta' 'spread' NO. §51 va RIDOTTA alla sola") + print(" domanda sullo spread, che si risponde con una CAMPAGNA A TERMINE, non con un") + print(" collettore permanente (stessa forma della raccomandazione depth di §51).") + else: + print("\n Le due superfici prezzano DIVERSO a strike appaiati: la raccomandazione §51") + print(" REGGE ed e' anzi piu' forte di come e' scritta.") + print("\n Sulla domanda di §64 — *esiste sulla lineare una struttura che l'inverse non fa?*") + print(" la risposta sta nella tabella §4, e va letta insieme a due regole gia' scritte:") + print(" (a) 'niente short-vol da modello in deploy' (19/06) non e' una regola di") + print(" eseguibilita' e non decade col lotto;") + print(" (b) §46 ha gia' refutato la copertura statica di coda (maxDD sale in 162/162).") + print("\n IN EURO/GIORNO: questo filone non produce reddito. Non propone nessuna struttura,") + print(" non cambia nessun peso, non tocca il libro. Il suo intero valore e' una DECISIONE DI") + print(" INFRASTRUTTURA da +18% di chiamate/giro sul collettore, e un numero (il pavimento") + print(" del tick) che le prossime ondate non dovranno ri-misurare. **+0,00 EUR/giorno.**") + + +# ================================================================== main +def salva(raw: dict) -> Path: + SNAPDIR.mkdir(parents=True, exist_ok=True) + p = SNAPDIR / f"snap_{raw['ts'].replace(':', '').replace('-', '')[:15]}.json" + p.write_text(json.dumps(raw)) + return p + + +def carica() -> list[dict]: + if not SNAPDIR.exists(): + return [] + out = [] + for p in sorted(SNAPDIR.glob("snap_*.json")): + try: + out.append(json.loads(p.read_text())) + except Exception: + print(f" ⚠️ snapshot illeggibile, saltato: {p.name}") + return out + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--snap", action="store_true", help="solo un'istantanea nuova") + ap.add_argument("--report", action="store_true", help="solo il report sugli snapshot su disco") + a = ap.parse_args() + fa_snap = a.snap or not a.report + fa_rep = a.report or not a.snap + + print("=" * 104) + print("USDC-SURFACE (§64) — la superficie lineare contro la sua gemella inverse, appaiata") + print("=" * 104) + print("Sole GET pubbliche, nessun ordine. Qui si misura la NEGOZIABILITA', non un edge.") + + b = Budget() + if fa_snap: + guardia_finestra() + t0 = time.time() + raw = fetch_snapshot(b) + p = salva(raw) + print(f"\n[rete] {b.calls} chiamate, {b.err} errori, {b.r429} risposte 429, " + f"{time.time()-t0:.0f}s -> {p.name}") + if not fa_rep: + return 0 + + snaps = carica() + if not snaps: + print("\nNESSUNO SNAPSHOT: rilancia con --snap. Verdetto: non misurabile.") + return 1 + raw = snaps[-1] + rows = build(raw) + print(f"\n[dato] {len(rows)} strumenti con specifiche E quote nello snapshot piu' recente " + f"({raw['ts'][:19]} UTC); {len(snaps)} snapshot in sessione") + if not rows: + print("Il venue non ha risposto. Verdetto: non misurabile.") + return 1 + + att: dict = {} + sez0_repliche(rows, att) + sez1_censimento(rows) + res2 = sez2_appaiato(rows, att) + sez3_tick(rows, att) + sez4_strutture(rows, att) + sez5_profondita(rows, raw) + sez5b_controlli(rows, att) + sez6_dispersione(snaps) + dec = sez7_decisione(rows, res2, att) + verdetto(att, dec, rows) + print(f"\n[rete, totale] {b.calls} chiamate, {b.err} errori, {b.r429} risposte 429") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 0cd0747e951a7ffed1e07f89e83ac336121e8f1a Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 12:51:58 +0000 Subject: [PATCH 111/114] =?UTF-8?q?CLAUDE.md:=20bullet=20della=20sesta=20o?= =?UTF-8?q?ndata=20(=C2=A758-69)=20+=20tre=20correzioni=20inline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0 candidati su 69 filoni cumulativi. Il finding di prim'ordine e' §58: l'obiettivo del progetto ha DUE definizioni operative in uso (flusso e ricchezza sostenibile) che danno 33,9% contro 0,33% sulla stessa domanda, e per cinque ondate i due corpi di lavoro sono stati confrontati come se parlassero della stessa cosa. Tre marcatori inline dove l'ondata contraddice numeri gia' scritti, perche' questa memoria non deve contraddirsi da sola: - il vincitore MISTO del 25/07 e' superato da MISTO-A (§60) - il gate XSR01 del 23/10 legge un monitor tarato sul pavimento del venue sbagliato (§67); soglie NON toccate, decisione dell'operatore - la ragione per raccogliere la catena USDC e' caduta: le due superfici sono la stessa superficie a strike appaiati (§64) Registrata anche la provenienza anomala dei verdetti (riesecuzione degli script, non messaggi degli agenti) e la previsione verificabile che la 70a ondata dara' la stessa risposta. Libro, pesi, cron, config INVARIATI. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 73caae6..29494a6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -553,6 +553,12 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis con turnover 38.5% del lordo/g su 50 alt anche illiquidi e **slippage NON modellato** (rischio #1). **Eseguibilita':** ticket/gamba $1.73 a $600 (**sotto min-order $5 → STAT-MODE oggi**), $5.76 a $2000, **$14.41 a $5000** → diventa reale a **~$5k**, non ai ~$20k di XS01. + 🚨 **IL GATE QUI SOTTO LEGGE UN MONITOR MAL TARATO (misurato 2026-08-23, §67): il pavimento del + venue usato per l'haircut e' quello sbagliato — C\* vero $15.000-20.000, non i ~$3.000 riportati da + HL-EXEC con un criterio piu' permissivo.** Le soglie **NON sono state toccate**: cambiarle guardando + questa misura a due mesi dalla decisione sarebbe selection-on-holdout. Le due vie pulite: riscriverle + **adesso** dichiarando che le si riscrive prima di vedere l'esito, oppure lasciarle e **citare il + difetto al momento della decisione**. **Decisione dell'operatore.** **Gate pre-registrato 2026-10-23** (forward-day 0): deploy solo se Sharpe≥1.0 **E** haircut di eseguibilita' a $5000 ≤40% (se l'haircut sfonda → **RITIRO a prescindere dallo Sharpe**), poi weights_tilt_null e capitale ≥$5k. Monitor in `cron_daily.sh`, 3 libri (MODELED $2000 / REAL $600 / @@ -687,7 +693,10 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis → la diversificazione fra conti e' illusoria salvo **sleeve diversi su conti diversi**. E' un problema di portafoglio sotto **barriera di rovina PER-CONTO**. Sim 36 mesi, €600, max 6 conti, 2 firm, regole vere, payout mensile prelevato subito, fisco 33%, **morte-firm 10%/anno**, - bootstrap CONGIUNTO (corr reali). **Vince il MISTO, non gli estremi** (close-only, leva 1.0x): + bootstrap CONGIUNTO (corr reali). ~~**Vince il MISTO, non gli estremi**~~ 🚨 **SUPERATO 2026-08-23 + (§60): rifatta la stessa griglia (288 celle) vince MISTO-A, `P(>=50/g)` 9,27% contro 6,60% — e la + mediana e' 0,00 EUR/g in TUTTE e sei le politiche. Cade il vincitore, non l'ordine fra gli + estremi.** Numeri del 25/07 (close-only, leva 1.0x): MISTO mediana €10.70/g, P(≥50/g) 20.7%, P(zero) 33.4% > CONC-DIV 5.70/17.8%/33.4% > SPARSO 0.00/16.8%/**66.0%** > **CONC-2SL (= il book live attuale) 0.00/13.1%/55.6% = la PEGGIORE**. Logica: ogni conto serve Sharpe per sopravvivere alla PROPRIA barriera (uccide SPARSO), ma i @@ -2822,6 +2831,90 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis 📌 **La riga che chiude cinque ondate e 57 filoni: la ricerca ha smesso di essere il vincolo binding il 26/07, e ora cinque ondate lo hanno CONFERMATO invece che ribaltarlo. I vincoli binding restano due: il capitale che entra e il conto che non sparisce.** +- 🌊 **SESTA ONDATA 2026-08-23 (12 filoni, §58-69, `research/wave-0822`) — 0 candidati su **69 + cumulativi**, 1 finding che riguarda l'OBIETTIVO stesso, 2 appoggi tolti a decisioni gia' prese.** + Registro `docs/research/RESULTS-0822.md` §58-69 + sezione di sintesi in fondo; 12 script + `scripts/research/r0823b_*.py`. **Libro, pesi, cron, config INVARIATI.** + ⚠️ **PROVENIENZA DIVERSA DA OGNI ALTRA ONDATA, e va detto per primo:** la sessione si e' chiusa + dopo che gli agenti avevano scritto gli script e **prima** che i verdetti fossero consolidati — + le 12 righe restarono `_in corso_` e **i messaggi finali degli agenti sono persi**. I verdetti + pubblicati **non vengono da quei messaggi: vengono da una RIESECUZIONE dei 12 script** (07:38-08:06 + UTC, sequenziale; log in `logs/r0823b/`, che e' **gitignored** ⇒ il registro e' l'unica copia + versionata). 📌 **La regola del 26/07 — *un numero pubblicato deve avere uno script committato che + lo riproduce* — e' cio' che ha impedito la perdita dell'ondata.** Estensione: **un'ondata + interrotta si recupera solo se ogni script calcola il proprio verdetto a RUNTIME**, non se il + verdetto vive nel messaggio dell'agente. + 🚨 **(1) §58 — L'OBIETTIVO DEL PROGETTO HA DUE DEFINIZIONI OPERATIVE IN USO, E DANNO 33,9% CONTRO + 0,33% SULLA STESSA DOMANDA.** A **FLUSSO** (quanto esce al mese: la lente del canale funded) il + massimo a 36 mesi e' **33,9%**; a **RICCHEZZA SOSTENIBILE** (la lente dei muri e del piano) e' + **0,33%**. **Per cinque ondate i due corpi di lavoro sono stati confrontati come se parlassero + della stessa cosa.** E l'ottimo di `P(>=50/g)` **non e' l'ottimo di J**: 37,3% contro 13,9% alla + leva argmax-J. Il libro di oggi a 36 mesi: mediana **0 EUR/g** di flusso (non preleva), capitale + mediano $22.467 = **3,59 EUR/g** di rendita. **REGOLA: un obiettivo si dichiara nella sua + definizione operativa PRIMA di ottimizzarlo** — qui due definizioni entrambe difendibili + differiscono di **cento volte**. + 📌 **(2) §61 — la politica di ri-acquisto dei biglietti era CABLATA e mai dichiarata come scelta** + (compra subito, il piu' grande, in tutti e 6 gli slot, con qualunque soldo): e' **la piu' + aggressiva della famiglia**, e **da li' escono TUTTI i numeri del canale funded**. Mappata la + famiglia (7 tempi x 2 taglie): **il massimo di famiglia E' il cablato** (4,0%), spread 4,05pp, + prezzo della prudenza 4,05pp di bersaglio per 2,05pp di rovina; «solo dai payout» **non vincola + nulla** a $654 (dopo il primo biglietto restano $75, sotto il piu' economico). **La riga che il + filone stampa da solo: ottimizzare la politica vale +0,00 EUR/g, versare EUR 500/mese ne vale + +15,75.** + 🚨 **(3) §67 — IL MURO DI TAGLIA NON NASCONDEVA NIENTE, e il perche' e' metodologico:** a $100.000 + nessun archiviato passa, **non perche' sia stato ricontrollato ma perche' i gate pubblicati erano + GIA' calcolati sulla serie MODELLATA, che e' il limite a capitale infinito**. Censimento 14 voci, + **12 con un rifiuto ANCHE sull'edge**; delle 2 in cui la taglia era l'unico motivo nessuna e' una + strategia (XS01, GTAA01). Muri di taglia caduti **5 su 14, tutti nell'ondata 22-23/08**. + ⚠️ **Cio' che TOGLIE: il gate pre-registrato XSR01 del 23/10 legge un monitor tarato sul pavimento + del venue SBAGLIATO** — C* vero **$15.000-20.000** contro i ~$3.000 riportati da HL-EXEC con un + criterio piu' permissivo. **Decisione dell'operatore:** riscrivere la soglia **adesso**, dichiarando + che la si riscrive prima di vedere l'esito, oppure lasciarla e **citare il difetto alla decisione** + — non c'e' una terza via pulita, e toccarla guardando l'esito sarebbe selection-on-holdout. + 🚨 **(4) §64 — LE DUE SUPERFICI OPZIONI SONO LA STESSA SUPERFICIE** a strike appaiati, su entrambi + gli assi (prezzo equo identico per costruzione; mezzo-spread Δ **−0,21/−0,38 pp**; `f_venue` Δ + **+0,005/−0,011**) ⇒ **la raccomandazione di §51 (raccogliere la catena USDC, +18% di + chiamate/giro) NON e' giustificata dalla ragione che porta**: il `f` di VRP01 misurato + sull'inverse **non e' «il numero della famiglia sbagliata», e' lo stesso numero**. + ⚠️ **(5) §60 — la politica multi-conto MISTO, scelta il 25/07, NON e' piu' l'ottimo:** oggi vince + **MISTO-A** (`P(>=50/g)` **9,27%** contro 6,60%) su 288 celle; **mediana 0,00 EUR/g in tutte e sei**. + 🚨 **(6) §63 — IL FORFAIT 33% E' LA CASELLA SBAGLIATA per i payout di una prop firm:** il trader + **non possiede il conto ne' le posizioni** ⇒ nessun realizzo di cripto-attivita', e **`c-sexies` + (33%) e `c-quater` (26%) sono ENTRAMBE fuori categoria**; le fonti convergono su **LAVORO + AUTONOMO** e **nessuna prassi ufficiale esiste**. ✅ La conclusione di §37 regge su tutta la banda; + il costo della questione vale **−8,21 EUR/g** fra i due estremi. 📌 **Domanda NUOVA al + commercialista, DIVERSA da quella aperta il 07/08** sui derivati. + ⚠️ **(7) §69 SCETTICO — replica (3,4% contro il 2,6% pubblicato) e poi toglie precisione:** SE del + drift di XS01 **9,6%/anno** ⇒ a −1 SE `P(>=50/g)` **1,6%**; morte-firm da evidenza pubblica + **12-19%/anno** contro il **10% assunto** (EV regge, break-even >55%); max-J dal **puro rumore** su + 360 celle **0,069** contro **0,303** osservato. **Raccomanda di non cambiare nulla.** + 🚨 **E il pattern conta piu' del singolo numero: 42% → 7,8% → 4,4% → 2,6% → 1,6% a −1 SE. Ogni + volta che il numero del canale funded e' stato misurato meglio e' SCESO, e non ha ancora smesso.** + E' la firma di un numero che vive di ottimismo non ancora misurato, non di una stima che converge. + ❌ **(8) GLI ALTRI CINQUE, tutti negativi.** **§59** a regime servono **4 conti da $100k** (i cap di + 2 firm) e `P(>=50/g)` **SATURA all'87%** — *non e' una rendita, e' una scommessa ingrandibile ma non + rendibile affidabile*; da EUR 635 **1,78 EUR/g mediani**. **§62** la frazione del versamento verso i + biglietti e' segnale sul FLUSSO (**+16,3 pp**, risoluzione MC 1,0) ma **0,0 pp** sulla RENDITA e + **+12,4 pp** di `P(sotto il versato)`. **§65** la forma della curva dei datati **e' un grado di + liberta' nuovo** (scorrelato dal funding, A2 confermata piu' forte dell'atteso) **e non predice + nulla**: 0 candidati su 4 rami, DSR 0,108, roll 0,49%/anno = l'ordine del premio. **§66** + cross-venue: 2 celle su 8 sopra la propria MDE, **manca un fattore 6,5x** (16,2x per l'arbitraggio), + e *per entrare si attraversano gli stessi due book che generano lo scarto*. **§68** il paniere dei + quasi-passati fa FULL 1,822 / HOLD 1,323 ed e' al 100° pctl contro gli scartati netti, **ma DSR + 0,828-1,000 secondo la PARTIZIONE** (lo stesso difetto del 22/08, applicato al paniere che dovrebbe + superarlo) e **dipende da UNA gamba** (PREVDAY). + ⚠️ **(9) UN FILONE E' MORTO SU UN BUG E LA CORREZIONE E' DEL COORDINATORE, non dell'autore.** + `r0823b_quasi_passati.py` terminava con `IndexError: Boolean index has wrong length: 958 instead of + 2720` — `Griglia.combo()` indicizzava con `self.idx` (l'unione) un **sottoinsieme di righe**. + Corretto col parametro `idx` esplicito **piu' un controllo di lunghezza**; la rinormalizzazione e' + **riga per riga**, quindi i valori sono quelli dell'intento dell'autore. **Il verdetto di §68 va + letto sapendo su quale codice gira.** + 📌 **(10) LA RIGA DELL'ONDATA.** Dodici filoni, zero candidati, **nessun EUR/giorno prodotto**: il + valore e' interamente **DIFENSIVO** (§67 e §64 tolgono due appoggi, §69 toglie precisione, §60 + toglie un ottimo). **Con questa sono 69 filoni e 0 candidati promossi.** La ricerca ha smesso di + essere il vincolo binding il 26/07 e **sei ondate lo hanno CONFERMATO invece che ribaltarlo**; + previsione verificabile registrata: **la 70ª dara' la stessa risposta**. I vincoli binding restano + **il capitale che entra** e **il conto che non sparisce**. - 🚨 **IL PIANO AL NETTO DI TUTTO — la tabella congiunta (2026-08-22, `r0822d_piano_vero.py`).** **QUESTA SOSTITUISCE OGNI TABELLA DI TRAIETTORIA PUBBLICATA SOPRA.** Le due correzioni misurate al piano — **fisco d'accumulo** (07/08) e **funding** (22/08) — erano state applicate **separatamente @@ -3211,6 +3304,11 @@ df = load_data("BTC", "1h") # OK. load_data("SOL", ...) -> FileNotFoundError ( sul rate-limit per-IP, che ha già causato un guasto il 29/07 — ma **di un ordine di grandezza più piccola di come era stata scritta**, ed è un numero **di oggi** (cresce con la liquidità USDC, va ri-misurato prima di accendere). + 🚨 **LA RAGIONE PER RACCOGLIERLA E' CADUTA (2026-08-23, §64): le due superfici sono LA STESSA + SUPERFICIE** a strike appaiati — prezzo equo identico per costruzione, mezzo-spread Δ −0,21/−0,38 pp, + `f_venue` Δ +0,005/−0,011 ⇒ il `f` di VRP01 misurato sull'inverse **non e' «il numero della famiglia + sbagliata», e' lo stesso numero**. Resta vero che la lineare **non e' nell'archivio**; cade + l'argomento che la sua assenza falsi una misura gia' fatta. **NON assorbito, e perché:** il motore credit-spread ETH (il progetto ha già la regola "niente short-vol da modello in deploy"), la GUI, kill switch/dead-man/audit (PythagorasGoal ha `venue_watch`/`edge_watch`/`monitor_health`/`fee_watch`), `dvol_history` (`fetch_dvol.py` ha From 10c373075cb6874fd98877018fefd2c376e1e28c Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 13:13:02 +0000 Subject: [PATCH 112/114] libro di bordo: DB dei trade allineato col tempo + giornale giornaliero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I trade erano salvati, ma non allineati col tempo: book_execute.py scriveva ts_utc = pd.Timestamp(r['last_data']), cioe' la data della BARRA DI SEGNALE. 19 righe su 19 a 00:00:00, e un trade (ETH 0.04 @ 1869.74) registrato SEI GIORNI prima di essere eseguito — fill vero 2026-07-14T14:00, scritto 08/07. L'ora vera esisteva solo in logs/cron_book.log, che e' gitignored, fuori dal backup e ruotabile: la cronologia reale del libro live viveva in un file che una rotazione avrebbe cancellato senza che nessuno se ne accorgesse. - src/live/tradesdb.py: parser del cron log (ora vera + contesto del segnale), FIFO con fee pro-quota, riconciliazione a tre fonti, sqlite in data/live/ (dentro il perimetro del backup). Le tre fonti si INCROCIANO e non si sovrascrivono: reconcile() riporta le divergenze e non ripara niente da solo. Il venue e' autorevole ma TRONCA (1 trade su BTC, 0 su ETH): dichiarato. - scripts/live/trades_db.py: --sync (idempotente, in cron_book ogni ora), --report, --reconcile. - src/live/journal.py + scripts/live/journal.py: una voce al giorno in docs/journal/YYYY-MM-DD.md — mercato (ritorni, RV30, TSMOM sugli orizzonti di produzione, DVOL con eta'), libro (TP01/SKH01, target, posizione, leva), P&L (equity del venue come autorita', scomposizione locale), salute. NIENTE narrativa automatica: il campo `nota` e' l'unico posto per il testo libero ed e' dell'operatore, mai riscritto da un ricalcolo. - book_execute.py: ts_utc = ora vera del fill, bar_ts = barra. Test di regressione sulla sorgente: se qualcuno rimette last_data, il test lo dice. - 62 voci di giornale ricostruite dall'arming a oggi. Tre difetti trovati dai test mentre scrivevo, non a occhio: il renderer cadeva in KeyError se mancava il blocco mercato (un giornale che non si scrive non e' un giornale); "24 giri attesi" su un giorno IN CORSO produceva un allarme a ogni esecuzione; e libro e P&L leggevano due istanti diversi, quindi la stessa pagina mostrava due equity. Strategia, pesi, config INVARIATI. Nessun ordine. 659 test passano. Co-Authored-By: Claude Opus 5 (1M context) --- .gitignore | 4 + docs/journal/2026-06-23.md | 34 ++++ docs/journal/2026-06-24.md | 34 ++++ docs/journal/2026-06-25.md | 34 ++++ docs/journal/2026-06-26.md | 34 ++++ docs/journal/2026-06-27.md | 34 ++++ docs/journal/2026-06-28.md | 34 ++++ docs/journal/2026-06-29.md | 34 ++++ docs/journal/2026-06-30.md | 34 ++++ docs/journal/2026-07-01.md | 34 ++++ docs/journal/2026-07-02.md | 34 ++++ docs/journal/2026-07-03.md | 34 ++++ docs/journal/2026-07-04.md | 34 ++++ docs/journal/2026-07-05.md | 34 ++++ docs/journal/2026-07-06.md | 34 ++++ docs/journal/2026-07-07.md | 34 ++++ docs/journal/2026-07-08.md | 34 ++++ docs/journal/2026-07-09.md | 34 ++++ docs/journal/2026-07-10.md | 34 ++++ docs/journal/2026-07-11.md | 34 ++++ docs/journal/2026-07-12.md | 34 ++++ docs/journal/2026-07-13.md | 34 ++++ docs/journal/2026-07-14.md | 34 ++++ docs/journal/2026-07-15.md | 34 ++++ docs/journal/2026-07-16.md | 34 ++++ docs/journal/2026-07-17.md | 34 ++++ docs/journal/2026-07-18.md | 34 ++++ docs/journal/2026-07-19.md | 34 ++++ docs/journal/2026-07-20.md | 34 ++++ docs/journal/2026-07-21.md | 34 ++++ docs/journal/2026-07-22.md | 34 ++++ docs/journal/2026-07-23.md | 34 ++++ docs/journal/2026-07-24.md | 34 ++++ docs/journal/2026-07-25.md | 34 ++++ docs/journal/2026-07-26.md | 34 ++++ docs/journal/2026-07-27.md | 34 ++++ docs/journal/2026-07-28.md | 33 ++++ docs/journal/2026-07-29.md | 34 ++++ docs/journal/2026-07-30.md | 34 ++++ docs/journal/2026-07-31.md | 34 ++++ docs/journal/2026-08-01.md | 34 ++++ docs/journal/2026-08-02.md | 34 ++++ docs/journal/2026-08-03.md | 34 ++++ docs/journal/2026-08-04.md | 34 ++++ docs/journal/2026-08-05.md | 34 ++++ docs/journal/2026-08-06.md | 34 ++++ docs/journal/2026-08-07.md | 34 ++++ docs/journal/2026-08-08.md | 34 ++++ docs/journal/2026-08-09.md | 34 ++++ docs/journal/2026-08-10.md | 34 ++++ docs/journal/2026-08-11.md | 34 ++++ docs/journal/2026-08-12.md | 34 ++++ docs/journal/2026-08-13.md | 34 ++++ docs/journal/2026-08-14.md | 34 ++++ docs/journal/2026-08-15.md | 34 ++++ docs/journal/2026-08-16.md | 34 ++++ docs/journal/2026-08-17.md | 34 ++++ docs/journal/2026-08-18.md | 34 ++++ docs/journal/2026-08-19.md | 34 ++++ docs/journal/2026-08-20.md | 34 ++++ docs/journal/2026-08-21.md | 34 ++++ docs/journal/2026-08-22.md | 34 ++++ docs/journal/2026-08-23.md | 34 ++++ scripts/cron_book.sh | 4 + scripts/cron_daily.sh | 4 + scripts/live/book_execute.py | 9 +- scripts/live/journal.py | 67 +++++++ scripts/live/trades_db.py | 105 +++++++++++ src/live/journal.py | 233 +++++++++++++++++++++++ src/live/tradesdb.py | 351 +++++++++++++++++++++++++++++++++++ tests/test_journal.py | 138 ++++++++++++++ tests/test_tradesdb.py | 207 +++++++++++++++++++++ 72 files changed, 3228 insertions(+), 1 deletion(-) create mode 100644 docs/journal/2026-06-23.md create mode 100644 docs/journal/2026-06-24.md create mode 100644 docs/journal/2026-06-25.md create mode 100644 docs/journal/2026-06-26.md create mode 100644 docs/journal/2026-06-27.md create mode 100644 docs/journal/2026-06-28.md create mode 100644 docs/journal/2026-06-29.md create mode 100644 docs/journal/2026-06-30.md create mode 100644 docs/journal/2026-07-01.md create mode 100644 docs/journal/2026-07-02.md create mode 100644 docs/journal/2026-07-03.md create mode 100644 docs/journal/2026-07-04.md create mode 100644 docs/journal/2026-07-05.md create mode 100644 docs/journal/2026-07-06.md create mode 100644 docs/journal/2026-07-07.md create mode 100644 docs/journal/2026-07-08.md create mode 100644 docs/journal/2026-07-09.md create mode 100644 docs/journal/2026-07-10.md create mode 100644 docs/journal/2026-07-11.md create mode 100644 docs/journal/2026-07-12.md create mode 100644 docs/journal/2026-07-13.md create mode 100644 docs/journal/2026-07-14.md create mode 100644 docs/journal/2026-07-15.md create mode 100644 docs/journal/2026-07-16.md create mode 100644 docs/journal/2026-07-17.md create mode 100644 docs/journal/2026-07-18.md create mode 100644 docs/journal/2026-07-19.md create mode 100644 docs/journal/2026-07-20.md create mode 100644 docs/journal/2026-07-21.md create mode 100644 docs/journal/2026-07-22.md create mode 100644 docs/journal/2026-07-23.md create mode 100644 docs/journal/2026-07-24.md create mode 100644 docs/journal/2026-07-25.md create mode 100644 docs/journal/2026-07-26.md create mode 100644 docs/journal/2026-07-27.md create mode 100644 docs/journal/2026-07-28.md create mode 100644 docs/journal/2026-07-29.md create mode 100644 docs/journal/2026-07-30.md create mode 100644 docs/journal/2026-07-31.md create mode 100644 docs/journal/2026-08-01.md create mode 100644 docs/journal/2026-08-02.md create mode 100644 docs/journal/2026-08-03.md create mode 100644 docs/journal/2026-08-04.md create mode 100644 docs/journal/2026-08-05.md create mode 100644 docs/journal/2026-08-06.md create mode 100644 docs/journal/2026-08-07.md create mode 100644 docs/journal/2026-08-08.md create mode 100644 docs/journal/2026-08-09.md create mode 100644 docs/journal/2026-08-10.md create mode 100644 docs/journal/2026-08-11.md create mode 100644 docs/journal/2026-08-12.md create mode 100644 docs/journal/2026-08-13.md create mode 100644 docs/journal/2026-08-14.md create mode 100644 docs/journal/2026-08-15.md create mode 100644 docs/journal/2026-08-16.md create mode 100644 docs/journal/2026-08-17.md create mode 100644 docs/journal/2026-08-18.md create mode 100644 docs/journal/2026-08-19.md create mode 100644 docs/journal/2026-08-20.md create mode 100644 docs/journal/2026-08-21.md create mode 100644 docs/journal/2026-08-22.md create mode 100644 docs/journal/2026-08-23.md create mode 100644 scripts/live/journal.py create mode 100644 scripts/live/trades_db.py create mode 100644 src/live/journal.py create mode 100644 src/live/tradesdb.py create mode 100644 tests/test_journal.py create mode 100644 tests/test_tradesdb.py diff --git a/.gitignore b/.gitignore index 3da9ac7..40599b1 100644 --- a/.gitignore +++ b/.gitignore @@ -86,3 +86,7 @@ data/live/ # dati esterni di ricerca (on-chain CoinMetrics community, F&G) — non certificati, non in git data/external/ data/options_daily/ + +# libro di bordo: il DB e' dato operativo (coperto dal backup), non codice +data/live/trades.db +data/cache_regime_capitale.json diff --git a/docs/journal/2026-06-23.md b/docs/journal/2026-06-23.md new file mode 100644 index 0000000..65d4324 --- /dev/null +++ b/docs/journal/2026-06-23.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-06-23 + +*Scritto 2026-08-23T13:08:26+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $62,684.00 | -1.99% | -4.48% | -18.57% | 42.8% | ↓ ↓ ↓ (0/3 su) | 41.6 (27° pctl 1a) | +| **ETH** | $1,665.60 | -3.52% | -7.05% | -20.59% | 63.4% | ↓ ↓ ↓ (0/3 su) | 56.2 (21° pctl 1a) | + +## Libro + +Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-06-23 · 2 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (2 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 2/24 — **22 mancanti** +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-06-24.md b/docs/journal/2026-06-24.md new file mode 100644 index 0000000..cbfab3b --- /dev/null +++ b/docs/journal/2026-06-24.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-06-24 + +*Scritto 2026-08-23T13:08:26+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $60,977.50 | -2.72% | -5.39% | -21.09% | 43.3% | ↓ ↓ ↓ (0/3 su) | 45.1 (51° pctl 1a) | +| **ETH** | $1,619.90 | -2.74% | -7.43% | -23.29% | 63.6% | ↓ ↓ ↓ (0/3 su) | 58.8 (28° pctl 1a) | + +## Libro + +Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-06-24 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-06-25.md b/docs/journal/2026-06-25.md new file mode 100644 index 0000000..e8c0291 --- /dev/null +++ b/docs/journal/2026-06-25.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-06-25 + +*Scritto 2026-08-23T13:08:26+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $59,702.00 | -2.09% | -5.08% | -21.25% | 43.3% | ↓ ↓ ↓ (0/3 su) | 46.6 (57° pctl 1a) | +| **ETH** | $1,564.90 | -3.40% | -8.47% | -24.43% | 64.1% | ↓ ↓ ↓ (0/3 su) | 60.8 (33° pctl 1a) | + +## Libro + +Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-06-25 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-06-26.md b/docs/journal/2026-06-26.md new file mode 100644 index 0000000..5674638 --- /dev/null +++ b/docs/journal/2026-06-26.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-06-26 + +*Scritto 2026-08-23T13:08:26+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $60,017.00 | +0.53% | -5.47% | -19.28% | 43.4% | ↓ ↓ ↓ (0/3 su) | 45.5 (52° pctl 1a) | +| **ETH** | $1,576.05 | +0.71% | -7.82% | -22.08% | 64.1% | ↓ ↓ ↓ (0/3 su) | 59.8 (30° pctl 1a) | + +## Libro + +Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-06-26 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-06-27.md b/docs/journal/2026-06-27.md new file mode 100644 index 0000000..130e780 --- /dev/null +++ b/docs/journal/2026-06-27.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-06-27 + +*Scritto 2026-08-23T13:08:26+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $59,956.00 | -0.10% | -6.67% | -18.47% | 43.4% | ↓ ↓ ↓ (0/3 su) | 45.2 (52° pctl 1a) | +| **ETH** | $1,570.80 | -0.33% | -9.67% | -21.76% | 64.2% | ↓ ↓ ↓ (0/3 su) | 59.9 (31° pctl 1a) | + +## Libro + +Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-06-27 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-06-28.md b/docs/journal/2026-06-28.md new file mode 100644 index 0000000..0ff48a8 --- /dev/null +++ b/docs/journal/2026-06-28.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-06-28 + +*Scritto 2026-08-23T13:08:26+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $59,503.50 | -0.75% | -5.92% | -18.92% | 43.4% | ↓ ↓ ↓ (0/3 su) | 47.7 (63° pctl 1a) | +| **ETH** | $1,569.20 | -0.10% | -8.01% | -22.04% | 64.1% | ↓ ↓ ↓ (0/3 su) | 61.3 (37° pctl 1a) | + +## Libro + +Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-06-28 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-06-29.md b/docs/journal/2026-06-29.md new file mode 100644 index 0000000..253d158 --- /dev/null +++ b/docs/journal/2026-06-29.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-06-29 + +*Scritto 2026-08-23T13:08:26+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $60,166.50 | +1.11% | -5.92% | -18.46% | 43.6% | ↓ ↓ ↓ (0/3 su) | 42.6 (33° pctl 1a) | +| **ETH** | $1,611.20 | +2.68% | -6.68% | -20.24% | 65.1% | ↓ ↓ ↓ (0/3 su) | 57.9 (26° pctl 1a) | + +## Libro + +Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-06-29 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (23 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-06-30.md b/docs/journal/2026-06-30.md new file mode 100644 index 0000000..8501691 --- /dev/null +++ b/docs/journal/2026-06-30.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-06-30 + +*Scritto 2026-08-23T13:08:26+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $58,546.50 | -2.69% | -6.60% | -20.44% | 44.2% | ↓ ↓ ↓ (0/3 su) | 44.5 (46° pctl 1a) | +| **ETH** | $1,570.55 | -2.52% | -5.71% | -21.67% | 65.4% | ↓ ↓ ↓ (0/3 su) | 59.0 (29° pctl 1a) | + +## Libro + +Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-06-30 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-01.md b/docs/journal/2026-07-01.md new file mode 100644 index 0000000..7c6fb84 --- /dev/null +++ b/docs/journal/2026-07-01.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-01 + +*Scritto 2026-08-23T13:08:26+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $59,961.50 | +2.42% | -1.67% | -15.93% | 44.6% | ↓ ↓ ↓ (0/3 su) | 42.9 (36° pctl 1a) | +| **ETH** | $1,607.95 | +2.38% | -0.74% | -19.81% | 66.3% | ↓ ↓ ↓ (0/3 su) | 56.9 (23° pctl 1a) | + +## Libro + +Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-01 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-02.md b/docs/journal/2026-07-02.md new file mode 100644 index 0000000..f1ff46c --- /dev/null +++ b/docs/journal/2026-07-02.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-02 + +*Scritto 2026-08-23T13:08:26+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $61,518.50 | +2.60% | +3.04% | -7.71% | 40.0% | ↓ ↓ ↓ (0/3 su) | 40.5 (21° pctl 1a) | +| **ETH** | $1,698.60 | +5.64% | +8.54% | -8.57% | 64.9% | ↓ ↓ ↓ (0/3 su) | 55.1 (16° pctl 1a) | + +## Libro + +Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-02 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-03.md b/docs/journal/2026-07-03.md new file mode 100644 index 0000000..64e7969 --- /dev/null +++ b/docs/journal/2026-07-03.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-03 + +*Scritto 2026-08-23T13:08:27+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $62,542.00 | +1.66% | +4.21% | -2.28% | 38.1% | ↓ ↓ ↓ (0/3 su) | 39.5 (14° pctl 1a) | +| **ETH** | $1,757.15 | +3.45% | +11.49% | -2.98% | 65.6% | ↓ ↓ ↓ (0/3 su) | 53.5 (8° pctl 1a) | + +## Libro + +Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-03 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-04.md b/docs/journal/2026-07-04.md new file mode 100644 index 0000000..037dcca --- /dev/null +++ b/docs/journal/2026-07-04.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-04 + +*Scritto 2026-08-23T13:08:27+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,080.00 | +0.86% | +5.21% | -1.15% | 38.2% | ↓ ↓ ↓ (0/3 su) | 39.2 (13° pctl 1a) | +| **ETH** | $1,779.40 | +1.27% | +13.28% | +0.56% | 65.3% | ↑ ↓ ↓ (1/3 su) | 53.0 (6° pctl 1a) | + +## Libro + +Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-04 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-05.md b/docs/journal/2026-07-05.md new file mode 100644 index 0000000..4735679 --- /dev/null +++ b/docs/journal/2026-07-05.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-05 + +*Scritto 2026-08-23T13:08:27+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,624.00 | +0.86% | +6.92% | +4.22% | 34.9% | ↑ ↓ ↓ (1/3 su) | 38.7 (10° pctl 1a) | +| **ETH** | $1,784.95 | +0.31% | +13.75% | +12.77% | 51.3% | ↑ ↓ ↓ (1/3 su) | 53.1 (6° pctl 1a) | + +## Libro + +Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-05 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-06.md b/docs/journal/2026-07-06.md new file mode 100644 index 0000000..542823e --- /dev/null +++ b/docs/journal/2026-07-06.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-06 + +*Scritto 2026-08-23T13:08:27+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,012.00 | +0.61% | +6.39% | +5.19% | 34.8% | ↑ ↓ ↓ (1/3 su) | 37.3 (4° pctl 1a) | +| **ETH** | $1,798.85 | +0.78% | +11.65% | +14.67% | 51.1% | ↑ ↓ ↓ (1/3 su) | 52.6 (6° pctl 1a) | + +## Libro + +Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-06 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-07.md b/docs/journal/2026-07-07.md new file mode 100644 index 0000000..79f6142 --- /dev/null +++ b/docs/journal/2026-07-07.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-07 + +*Scritto 2026-08-23T13:08:27+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,322.00 | -1.08% | +8.16% | +0.03% | 32.3% | ↑ ↓ ↓ (1/3 su) | 38.9 (12° pctl 1a) | +| **ETH** | $1,770.30 | -1.59% | +12.72% | +4.79% | 45.0% | ↑ ↓ ↓ (1/3 su) | 53.3 (7° pctl 1a) | + +## Libro + +Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-07 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (23 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-08.md b/docs/journal/2026-07-08.md new file mode 100644 index 0000000..77d2732 --- /dev/null +++ b/docs/journal/2026-07-08.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-08 + +*Scritto 2026-08-23T13:08:27+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $62,259.00 | -1.68% | +3.83% | -1.27% | 32.8% | ↓ ↓ ↓ (0/3 su) | 39.5 (16° pctl 1a) | +| **ETH** | $1,742.25 | -1.58% | +8.35% | +3.14% | 45.4% | ↑ ↓ ↓ (1/3 su) | 53.4 (8° pctl 1a) | + +## Libro + +Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-08 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-09.md b/docs/journal/2026-07-09.md new file mode 100644 index 0000000..d0a6645 --- /dev/null +++ b/docs/journal/2026-07-09.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-09 + +*Scritto 2026-08-23T13:08:27+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,200.00 | +1.51% | +2.73% | +2.42% | 32.3% | ↑ ↓ ↓ (1/3 su) | 37.3 (5° pctl 1a) | +| **ETH** | $1,744.25 | +0.11% | +2.69% | +6.49% | 43.9% | ↑ ↓ ↓ (1/3 su) | 52.0 (5° pctl 1a) | + +## Libro + +Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-08 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (15 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-10.md b/docs/journal/2026-07-10.md new file mode 100644 index 0000000..e3b9179 --- /dev/null +++ b/docs/journal/2026-07-10.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-10 + +*Scritto 2026-08-23T13:08:27+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,136.00 | +1.48% | +2.55% | +4.36% | 32.6% | ↑ ↓ ↓ (1/3 su) | 36.2 (2° pctl 1a) | +| **ETH** | $1,796.05 | +2.97% | +2.21% | +10.82% | 44.7% | ↑ ↓ ↓ (1/3 su) | 49.8 (4° pctl 1a) | + +## Libro + +Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-08 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-11.md b/docs/journal/2026-07-11.md new file mode 100644 index 0000000..0bb68b2 --- /dev/null +++ b/docs/journal/2026-07-11.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-11 + +*Scritto 2026-08-23T13:08:27+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,804.50 | -0.52% | +1.15% | +0.36% | 30.5% | ↑ ↓ ↓ (1/3 su) | 36.8 (4° pctl 1a) | +| **ETH** | $1,787.15 | -0.50% | +0.44% | +6.93% | 43.7% | ↑ ↓ ↓ (1/3 su) | 50.6 (4° pctl 1a) | + +## Libro + +Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-08 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-12.md b/docs/journal/2026-07-12.md new file mode 100644 index 0000000..d7fe746 --- /dev/null +++ b/docs/journal/2026-07-12.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-12 + +*Scritto 2026-08-23T13:08:28+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,759.50 | -0.07% | +0.21% | +0.33% | 30.5% | ↑ ↓ ↓ (1/3 su) | 37.2 (5° pctl 1a) | +| **ETH** | $1,805.70 | +1.04% | +1.16% | +8.40% | 43.7% | ↑ ↓ ↓ (1/3 su) | 51.0 (5° pctl 1a) | + +## Libro + +Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-08 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-13.md b/docs/journal/2026-07-13.md new file mode 100644 index 0000000..9f8759a --- /dev/null +++ b/docs/journal/2026-07-13.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-13 + +*Scritto 2026-08-23T13:08:28+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $62,265.50 | -2.34% | -2.73% | -3.34% | 31.2% | ↓ ↓ ↓ (0/3 su) | 37.8 (8° pctl 1a) | +| **ETH** | $1,775.55 | -1.67% | -1.30% | +5.67% | 44.2% | ↑ ↓ ↓ (1/3 su) | 51.1 (6° pctl 1a) | + +## Libro + +Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-08 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-14.md b/docs/journal/2026-07-14.md new file mode 100644 index 0000000..4cd93d4 --- /dev/null +++ b/docs/journal/2026-07-14.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-14 + +*Scritto 2026-08-23T13:08:28+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $65,012.00 | +4.41% | +2.67% | -1.08% | 34.1% | ↓ ↓ ↓ (0/3 su) | 36.5 (3° pctl 1a) | +| **ETH** | $1,889.60 | +6.42% | +6.74% | +9.53% | 48.3% | ↑ ↓ ↓ (1/3 su) | 49.5 (3° pctl 1a) | + +## Libro + +Equity **$598.49** · nozionale lordo $75 · leva lorda 0.13x · barra dati 2026-07-08 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | LONG @ 1,868.9 | $+75 | $+75 | HOLD (a target) | + +## P&L + +- giorno: **$+0.43** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 1 fill · fee 0.0374 +- cumulato dall'arming: **$+0.43** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-15.md b/docs/journal/2026-07-15.md new file mode 100644 index 0000000..f126816 --- /dev/null +++ b/docs/journal/2026-07-15.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-15 + +*Scritto 2026-08-23T13:08:28+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,734.50 | -0.43% | +3.98% | -2.35% | 34.0% | ↓ ↓ ↓ (0/3 su) | 36.0 (1° pctl 1a) | +| **ETH** | $1,916.70 | +1.43% | +10.01% | +6.73% | 46.6% | ↑ ↓ ↓ (1/3 su) | 49.0 (2° pctl 1a) | + +## Libro + +Equity **$600.14** · nozionale lordo $77 · leva lorda 0.13x · barra dati 2026-07-15 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | LONG @ 1,868.9 | $+75 | $+77 | HOLD (a target) | + +## P&L + +- giorno: **$+1.65** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$+2.08** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-16.md b/docs/journal/2026-07-16.md new file mode 100644 index 0000000..1aedb1d --- /dev/null +++ b/docs/journal/2026-07-16.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-16 + +*Scritto 2026-08-23T13:08:28+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,804.50 | -1.44% | +0.96% | -2.77% | 34.1% | ↓ ↓ ↓ (0/3 su) | 36.4 (3° pctl 1a) | +| **ETH** | $1,863.55 | -2.77% | +6.84% | +4.00% | 47.7% | ↑ ↓ ↓ (1/3 su) | 49.2 (2° pctl 1a) | + +## Libro + +Equity **$597.93** · nozionale lordo $75 · leva lorda 0.13x · barra dati 2026-07-16 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | LONG @ 1,868.9 | $+75 | $+75 | HOLD (a target) | + +## P&L + +- giorno: **$-2.21** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-0.13** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-17.md b/docs/journal/2026-07-17.md new file mode 100644 index 0000000..f10bebd --- /dev/null +++ b/docs/journal/2026-07-17.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-17 + +*Scritto 2026-08-23T13:08:28+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,904.00 | +0.16% | -0.36% | -0.85% | 33.6% | ↓ ↓ ↓ (0/3 su) | 36.3 (3° pctl 1a) | +| **ETH** | $1,841.00 | -1.21% | +2.50% | +5.21% | 47.1% | ↑ ↓ ↓ (1/3 su) | 49.0 (2° pctl 1a) | + +## Libro + +Equity **$596.77** · nozionale lordo $74 · leva lorda 0.12x · barra dati 2026-07-17 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | LONG @ 1,868.9 | $+75 | $+74 | HOLD (a target) | + +## P&L + +- giorno: **$-1.16** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-1.29** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-18.md b/docs/journal/2026-07-18.md new file mode 100644 index 0000000..17c9811 --- /dev/null +++ b/docs/journal/2026-07-18.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-18 + +*Scritto 2026-08-23T13:08:28+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,810.00 | +1.42% | +1.58% | +3.04% | 32.8% | ↑ ↓ ↓ (1/3 su) | 35.7 (1° pctl 1a) | +| **ETH** | $1,861.75 | +1.13% | +4.17% | +8.89% | 46.4% | ↑ ↓ ↓ (1/3 su) | 49.3 (4° pctl 1a) | + +## Libro + +Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-18 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.15** di equity (24 letture) +- realizzato: -1.10 netto su 1 round-trip chiusi · 1 fill · fee 0.0369 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-19.md b/docs/journal/2026-07-19.md new file mode 100644 index 0000000..45a73b8 --- /dev/null +++ b/docs/journal/2026-07-19.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-19 + +*Scritto 2026-08-23T13:08:28+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,704.50 | -0.16% | +1.48% | +1.92% | 32.7% | ↑ ↓ ↓ (1/3 su) | 36.1 (3° pctl 1a) | +| **ETH** | $1,871.60 | +0.53% | +3.65% | +9.47% | 46.4% | ↑ ↓ ↓ (1/3 su) | 49.7 (6° pctl 1a) | + +## Libro + +Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-19 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-20.md b/docs/journal/2026-07-20.md new file mode 100644 index 0000000..8427558 --- /dev/null +++ b/docs/journal/2026-07-20.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-20 + +*Scritto 2026-08-23T13:08:28+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $65,216.50 | +0.79% | +4.74% | +1.52% | 32.5% | ↑ ↓ ↓ (1/3 su) | 36.3 (4° pctl 1a) | +| **ETH** | $1,903.45 | +1.70% | +7.20% | +9.46% | 46.4% | ↑ ↓ ↓ (1/3 su) | 50.4 (7° pctl 1a) | + +## Libro + +Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-20 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-21.md b/docs/journal/2026-07-21.md new file mode 100644 index 0000000..9259d5b --- /dev/null +++ b/docs/journal/2026-07-21.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-21 + +*Scritto 2026-08-23T13:08:29+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $66,532.50 | +2.02% | +2.34% | +5.19% | 32.7% | ↑ ↓ ↓ (1/3 su) | 37.3 (10° pctl 1a) | +| **ETH** | $1,929.10 | +1.35% | +2.09% | +13.09% | 45.8% | ↑ ↓ ↓ (1/3 su) | 51.0 (8° pctl 1a) | + +## Libro + +Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-21 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-22.md b/docs/journal/2026-07-22.md new file mode 100644 index 0000000..cc99bcf --- /dev/null +++ b/docs/journal/2026-07-22.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-22 + +*Scritto 2026-08-23T13:08:29+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $66,081.00 | -0.68% | +2.08% | +3.32% | 32.6% | ↑ ↓ ↓ (1/3 su) | 38.0 (12° pctl 1a) | +| **ETH** | $1,933.70 | +0.24% | +0.89% | +12.00% | 45.7% | ↑ ↓ ↓ (1/3 su) | 51.6 (10° pctl 1a) | + +## Libro + +Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-22 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-23.md b/docs/journal/2026-07-23.md new file mode 100644 index 0000000..80ac49d --- /dev/null +++ b/docs/journal/2026-07-23.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-23 + +*Scritto 2026-08-23T13:08:29+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $65,060.00 | -1.55% | +1.97% | +3.79% | 32.3% | ↑ ↓ ↓ (1/3 su) | 38.5 (14° pctl 1a) | +| **ETH** | $1,877.45 | -2.91% | +0.75% | +12.72% | 45.1% | ↑ ↓ ↓ (1/3 su) | 51.6 (10° pctl 1a) | + +## Libro + +Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-23 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-24.md b/docs/journal/2026-07-24.md new file mode 100644 index 0000000..83c8947 --- /dev/null +++ b/docs/journal/2026-07-24.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-24 + +*Scritto 2026-08-23T13:08:29+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,103.50 | -1.47% | +0.31% | +5.13% | 31.1% | ↑ ↓ ↓ (1/3 su) | 37.0 (7° pctl 1a) | +| **ETH** | $1,860.50 | -0.90% | +1.06% | +14.85% | 43.9% | ↑ ↓ ↓ (1/3 su) | 50.5 (7° pctl 1a) | + +## Libro + +Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-24 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-25.md b/docs/journal/2026-07-25.md new file mode 100644 index 0000000..397f2aa --- /dev/null +++ b/docs/journal/2026-07-25.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-25 + +*Scritto 2026-08-23T13:08:29+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,327.50 | +0.35% | -0.74% | +7.75% | 30.0% | ↑ ↓ ↓ (1/3 su) | 37.7 (11° pctl 1a) | +| **ETH** | $1,873.45 | +0.70% | +0.63% | +19.72% | 41.5% | ↑ ↓ ↓ (1/3 su) | 51.5 (10° pctl 1a) | + +## Libro + +Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-25 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-26.md b/docs/journal/2026-07-26.md new file mode 100644 index 0000000..9681441 --- /dev/null +++ b/docs/journal/2026-07-26.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-26 + +*Scritto 2026-08-23T13:08:29+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $65,357.50 | +1.60% | +1.01% | +8.90% | 30.4% | ↑ ↓ ↓ (1/3 su) | 37.3 (10° pctl 1a) | +| **ETH** | $1,953.65 | +4.28% | +4.38% | +23.96% | 43.4% | ↑ ↓ ↓ (1/3 su) | 51.7 (11° pctl 1a) | + +## Libro + +Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-26 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-27.md b/docs/journal/2026-07-27.md new file mode 100644 index 0000000..68bd946 --- /dev/null +++ b/docs/journal/2026-07-27.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-27 + +*Scritto 2026-08-23T13:08:29+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,710.50 | -2.52% | -2.31% | +6.26% | 31.9% | ↑ ↓ ↓ (1/3 su) | 37.5 (11° pctl 1a) | +| **ETH** | $1,891.00 | -3.21% | -0.65% | +20.38% | 45.4% | ↑ ↓ ↓ (1/3 su) | 51.9 (12° pctl 1a) | + +## Libro + +Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-27 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-28.md b/docs/journal/2026-07-28.md new file mode 100644 index 0000000..933c0a8 --- /dev/null +++ b/docs/journal/2026-07-28.md @@ -0,0 +1,33 @@ +# Giornale di bordo — 2026-07-28 + +*Scritto 2026-08-23T13:08:29+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,874.50 | +0.26% | -4.00% | +7.35% | 31.7% | ↑ ↓ ↓ (1/3 su) | 37.2 (8° pctl 1a) | +| **ETH** | $1,920.40 | +1.55% | -0.45% | +22.38% | 45.5% | ↑ ↓ ↓ (1/3 su) | 53.0 (14° pctl 1a) | + +## Libro + +Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-28 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-29.md b/docs/journal/2026-07-29.md new file mode 100644 index 0000000..d11e705 --- /dev/null +++ b/docs/journal/2026-07-29.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-29 + +*Scritto 2026-08-23T13:08:29+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,933.00 | +0.09% | -3.25% | +6.26% | 31.6% | ↑ ↓ ↓ (1/3 su) | 37.5 (11° pctl 1a) | +| **ETH** | $1,909.10 | -0.59% | -1.27% | +18.49% | 45.1% | ↑ ↓ ↓ (1/3 su) | 52.5 (13° pctl 1a) | + +## Libro + +Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-29 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (21 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-30.md b/docs/journal/2026-07-30.md new file mode 100644 index 0000000..2397d44 --- /dev/null +++ b/docs/journal/2026-07-30.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-30 + +*Scritto 2026-08-23T13:08:30+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,745.50 | +1.27% | -0.48% | +10.59% | 30.0% | ↑ ↓ ↓ (1/3 su) | 35.5 (1° pctl 1a) | +| **ETH** | $1,917.45 | +0.44% | +2.13% | +22.09% | 43.7% | ↑ ↓ ↓ (1/3 su) | 50.7 (8° pctl 1a) | + +## Libro + +Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-30 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-31.md b/docs/journal/2026-07-31.md new file mode 100644 index 0000000..f237c10 --- /dev/null +++ b/docs/journal/2026-07-31.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-07-31 + +*Scritto 2026-08-23T13:08:30+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $62,837.00 | -2.95% | -1.98% | +4.80% | 31.2% | ↑ ↓ ↓ (1/3 su) | 35.6 (1° pctl 1a) | +| **ETH** | $1,861.50 | -2.92% | +0.05% | +15.77% | 45.0% | ↑ ↓ ↓ (1/3 su) | 50.2 (6° pctl 1a) | + +## Libro + +Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-31 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-01.md b/docs/journal/2026-08-01.md new file mode 100644 index 0000000..03712d1 --- /dev/null +++ b/docs/journal/2026-08-01.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-08-01 + +*Scritto 2026-08-23T13:08:30+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $62,776.00 | -0.10% | -2.41% | +2.04% | 29.9% | ↑ ↓ ↓ (1/3 su) | 35.5 (1° pctl 1a) | +| **ETH** | $1,843.25 | -0.98% | -1.61% | +8.52% | 41.5% | ↑ ↓ ↓ (1/3 su) | 50.8 (9° pctl 1a) | + +## Libro + +Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-01 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-02.md b/docs/journal/2026-08-02.md new file mode 100644 index 0000000..32b069d --- /dev/null +++ b/docs/journal/2026-08-02.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-08-02 + +*Scritto 2026-08-23T13:08:30+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,524.50 | +1.19% | -2.80% | +1.57% | 29.7% | ↑ ↓ ↓ (1/3 su) | 35.0 (0° pctl 1a) | +| **ETH** | $1,883.90 | +2.21% | -3.57% | +7.21% | 40.5% | ↑ ↓ ↓ (1/3 su) | 49.7 (6° pctl 1a) | + +## Libro + +Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-02 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-03.md b/docs/journal/2026-08-03.md new file mode 100644 index 0000000..15d572a --- /dev/null +++ b/docs/journal/2026-08-03.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-08-03 + +*Scritto 2026-08-23T13:08:30+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,485.50 | -0.06% | -0.35% | +0.64% | 29.5% | ↑ ↓ ↓ (1/3 su) | 34.8 (0° pctl 1a) | +| **ETH** | $1,859.25 | -1.31% | -1.68% | +4.49% | 40.7% | ↑ ↓ ↓ (1/3 su) | 49.2 (2° pctl 1a) | + +## Libro + +Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-03 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-04.md b/docs/journal/2026-08-04.md new file mode 100644 index 0000000..1d63f1a --- /dev/null +++ b/docs/journal/2026-08-04.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-08-04 + +*Scritto 2026-08-23T13:08:30+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,054.50 | +0.90% | +0.28% | +0.68% | 29.5% | ↑ ↓ ↑ (2/3 su) | 34.0 (0° pctl 1a) | +| **ETH** | $1,868.75 | +0.51% | -2.69% | +4.69% | 40.7% | ↑ ↓ ↑ (2/3 su) | 47.8 (0° pctl 1a) | + +## Libro + +Equity **$597.37** · nozionale lordo $37 · leva lorda 0.06x · barra dati 2026-08-04 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.162 | flat | $+36 | $+37 | HOLD (a target) | + +## P&L + +- giorno: **$+0.45** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 1 fill · fee 0.0129 +- cumulato dall'arming: **$-0.69** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-05.md b/docs/journal/2026-08-05.md new file mode 100644 index 0000000..9c2c211 --- /dev/null +++ b/docs/journal/2026-08-05.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-08-05 + +*Scritto 2026-08-23T13:08:30+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,607.50 | +0.86% | +1.06% | +0.93% | 29.6% | ↑ ↓ ↓ (1/3 su) | 34.7 (1° pctl 1a) | +| **ETH** | $1,907.45 | +2.07% | -0.09% | +6.04% | 41.2% | ↑ ↓ ↓ (1/3 su) | 48.0 (1° pctl 1a) | + +## Libro + +Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-05 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$-0.25** di equity (23 letture) +- realizzato: +0.21 netto su 1 round-trip chiusi · 1 fill · fee 0.0130 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-06.md b/docs/journal/2026-08-06.md new file mode 100644 index 0000000..a53fe17 --- /dev/null +++ b/docs/journal/2026-08-06.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-08-06 + +*Scritto 2026-08-23T13:08:30+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,284.50 | -0.50% | -0.71% | +1.52% | 29.4% | ↑ ↓ ↓ (1/3 su) | 35.0 (2° pctl 1a) | +| **ETH** | $1,902.65 | -0.25% | -0.77% | +7.48% | 40.7% | ↑ ↓ ↓ (1/3 su) | 48.0 (1° pctl 1a) | + +## Libro + +Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-06 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-07.md b/docs/journal/2026-08-07.md new file mode 100644 index 0000000..db0dad7 --- /dev/null +++ b/docs/journal/2026-08-07.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-08-07 + +*Scritto 2026-08-23T13:08:30+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,884.00 | +0.93% | +3.26% | +4.22% | 28.9% | ↑ ↓ ↓ (1/3 su) | 33.9 (0° pctl 1a) | +| **ETH** | $1,913.50 | +0.57% | +2.79% | +9.83% | 40.2% | ↑ ↓ ↓ (1/3 su) | 47.4 (0° pctl 1a) | + +## Libro + +Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-07 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-08.md b/docs/journal/2026-08-08.md new file mode 100644 index 0000000..b366345 --- /dev/null +++ b/docs/journal/2026-08-08.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-08-08 + +*Scritto 2026-08-23T13:08:30+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,918.50 | +0.05% | +3.41% | +2.72% | 28.4% | ↑ ↓ ↓ (1/3 su) | 34.2 (1° pctl 1a) | +| **ETH** | $1,916.00 | +0.13% | +3.95% | +9.85% | 40.2% | ↑ ↓ ↓ (1/3 su) | 48.1 (2° pctl 1a) | + +## Libro + +Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-08 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-09.md b/docs/journal/2026-08-09.md new file mode 100644 index 0000000..cae9ef9 --- /dev/null +++ b/docs/journal/2026-08-09.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-08-09 + +*Scritto 2026-08-23T13:08:31+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,869.00 | -0.08% | +2.12% | +1.14% | 28.0% | ↑ ↓ ↓ (1/3 su) | 34.9 (2° pctl 1a) | +| **ETH** | $1,909.50 | -0.34% | +1.36% | +6.32% | 39.1% | ↑ ↓ ↓ (1/3 su) | 49.1 (4° pctl 1a) | + +## Libro + +Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-09 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-10.md b/docs/journal/2026-08-10.md new file mode 100644 index 0000000..68d62c3 --- /dev/null +++ b/docs/journal/2026-08-10.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-08-10 + +*Scritto 2026-08-23T13:08:31+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,931.50 | -1.45% | +0.70% | +0.20% | 28.4% | ↑ ↓ ↓ (1/3 su) | 36.3 (8° pctl 1a) | +| **ETH** | $1,871.25 | -2.00% | +0.65% | +4.71% | 39.8% | ↑ ↓ ↓ (1/3 su) | 49.7 (8° pctl 1a) | + +## Libro + +Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-10 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-11.md b/docs/journal/2026-08-11.md new file mode 100644 index 0000000..a8ab00a --- /dev/null +++ b/docs/journal/2026-08-11.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-08-11 + +*Scritto 2026-08-23T13:08:31+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,559.50 | -0.58% | -0.77% | -0.31% | 28.5% | ↓ ↓ ↓ (0/3 su) | 36.1 (8° pctl 1a) | +| **ETH** | $1,881.25 | +0.53% | +0.67% | +4.18% | 39.7% | ↑ ↓ ↓ (1/3 su) | 49.3 (7° pctl 1a) | + +## Libro + +Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-11 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (23 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-12.md b/docs/journal/2026-08-12.md new file mode 100644 index 0000000..057803a --- /dev/null +++ b/docs/journal/2026-08-12.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-08-12 + +*Scritto 2026-08-23T13:08:31+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,440.00 | -0.19% | -1.81% | +1.89% | 27.2% | ↑ ↓ ↓ (1/3 su) | 35.8 (6° pctl 1a) | +| **ETH** | $1,878.15 | -0.16% | -1.54% | +5.78% | 39.2% | ↑ ↓ ↓ (1/3 su) | 49.4 (8° pctl 1a) | + +## Libro + +Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-12 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-13.md b/docs/journal/2026-08-13.md new file mode 100644 index 0000000..8ddc563 --- /dev/null +++ b/docs/journal/2026-08-13.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-08-13 + +*Scritto 2026-08-23T13:08:31+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,453.00 | +0.02% | -1.29% | -2.40% | 22.5% | ↓ ↓ ↓ (0/3 su) | 34.8 (2° pctl 1a) | +| **ETH** | $1,885.15 | +0.37% | -0.92% | -0.24% | 32.6% | ↓ ↓ ↓ (0/3 su) | 48.5 (2° pctl 1a) | + +## Libro + +Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-13 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-14.md b/docs/journal/2026-08-14.md new file mode 100644 index 0000000..1a70481 --- /dev/null +++ b/docs/journal/2026-08-14.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-08-14 + +*Scritto 2026-08-23T13:08:31+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $62,994.50 | -0.72% | -2.91% | -2.69% | 22.6% | ↓ ↓ ↓ (0/3 su) | 34.7 (2° pctl 1a) | +| **ETH** | $1,880.30 | -0.26% | -1.74% | -1.90% | 32.2% | ↓ ↓ ↓ (0/3 su) | 47.2 (0° pctl 1a) | + +## Libro + +Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-14 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-15.md b/docs/journal/2026-08-15.md new file mode 100644 index 0000000..8a5c064 --- /dev/null +++ b/docs/journal/2026-08-15.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-08-15 + +*Scritto 2026-08-23T13:08:31+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,033.00 | +0.06% | -2.90% | -1.21% | 22.0% | ↓ ↓ ↓ (0/3 su) | 35.0 (4° pctl 1a) | +| **ETH** | $1,880.85 | +0.03% | -1.83% | +0.93% | 30.7% | ↑ ↓ ↓ (1/3 su) | 47.5 (1° pctl 1a) | + +## Libro + +Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-15 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-16.md b/docs/journal/2026-08-16.md new file mode 100644 index 0000000..a17290e --- /dev/null +++ b/docs/journal/2026-08-16.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-08-16 + +*Scritto 2026-08-23T13:08:31+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $62,841.50 | -0.30% | -3.13% | -1.66% | 22.0% | ↓ ↓ ↓ (0/3 su) | 35.2 (5° pctl 1a) | +| **ETH** | $1,874.50 | -0.34% | -1.83% | +1.82% | 30.4% | ↑ ↓ ↓ (1/3 su) | 47.5 (1° pctl 1a) | + +## Libro + +Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-16 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-17.md b/docs/journal/2026-08-17.md new file mode 100644 index 0000000..4df3c2b --- /dev/null +++ b/docs/journal/2026-08-17.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-08-17 + +*Scritto 2026-08-23T13:08:31+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,487.50 | +2.62% | +0.87% | -0.50% | 23.4% | ↓ ↓ ↓ (0/3 su) | 34.7 (2° pctl 1a) | +| **ETH** | $1,912.35 | +2.02% | +2.20% | +2.72% | 30.9% | ↑ ↓ ↓ (1/3 su) | 46.3 (0° pctl 1a) | + +## Libro + +Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-17 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-18.md b/docs/journal/2026-08-18.md new file mode 100644 index 0000000..ea5f527 --- /dev/null +++ b/docs/journal/2026-08-18.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-08-18 + +*Scritto 2026-08-23T13:08:31+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,693.50 | +0.32% | +1.78% | -0.02% | 23.4% | ↓ ↓ ↓ (0/3 su) | 34.9 (4° pctl 1a) | +| **ETH** | $1,917.10 | +0.25% | +1.91% | +2.43% | 30.9% | ↑ ↓ ↓ (1/3 su) | 46.1 (0° pctl 1a) | + +## Libro + +Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-18 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (22 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-19.md b/docs/journal/2026-08-19.md new file mode 100644 index 0000000..d150bd0 --- /dev/null +++ b/docs/journal/2026-08-19.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-08-19 + +*Scritto 2026-08-23T13:08:32+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $69,327.00 | +7.16% | +9.28% | +6.30% | 33.6% | ↑ ↓ ↑ (2/3 su) | 37.8 (21° pctl 1a) | +| **ETH** | $2,252.30 | +17.48% | +19.92% | +18.33% | 63.8% | ↑ ↑ ↑ (3/3 su) | 52.5 (21° pctl 1a) | + +## Libro + +Equity **$610.82** · nozionale lordo $76 · leva lorda 0.12x · barra dati 2026-08-19 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | LONG @ 68,510.0 | $+76 | $+76 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+13.70** di equity (24 letture) +- realizzato: +10.60 netto su 1 round-trip chiusi · 3 fill · fee 0.0816 +- cumulato dall'arming: **$+12.76** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-20.md b/docs/journal/2026-08-20.md new file mode 100644 index 0000000..6d654ed --- /dev/null +++ b/docs/journal/2026-08-20.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-08-20 + +*Scritto 2026-08-23T13:08:32+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $73,010.50 | +5.31% | +15.06% | +9.74% | 37.3% | ↑ ↓ ↑ (2/3 su) | 39.4 (33° pctl 1a) | +| **ETH** | $2,326.65 | +3.30% | +23.42% | +20.61% | 64.4% | ↑ ↑ ↑ (3/3 su) | 52.6 (22° pctl 1a) | + +## Libro + +Equity **$620.63** · nozionale lordo $272 · leva lorda 0.44x · barra dati 2026-08-20 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.199 | LONG @ 68,510.0 | $+124 | $+124 | HOLD (a target) | +| **ETH** | +0.294 | LONG @ 2,251.8 | $+146 | $+148 | HOLD (a target) | + +## P&L + +- giorno: **$+9.81** di equity (24 letture) +- realizzato: +0.51 netto su 1 round-trip chiusi · 4 fill · fee 0.0698 +- cumulato dall'arming: **$+22.57** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-21.md b/docs/journal/2026-08-21.md new file mode 100644 index 0000000..12afae5 --- /dev/null +++ b/docs/journal/2026-08-21.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-08-21 + +*Scritto 2026-08-23T13:08:32+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $78,350.50 | +7.31% | +24.38% | +18.57% | 43.9% | ↑ ↑ ↑ (3/3 su) | 43.3 (57° pctl 1a) | +| **ETH** | $2,517.10 | +8.19% | +33.87% | +30.17% | 69.2% | ↑ ↑ ↑ (3/3 su) | 57.7 (46° pctl 1a) | + +## Libro + +Equity **$644.00** · nozionale lordo $196 · leva lorda 0.30x · barra dati 2026-08-21 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.176 | LONG @ 75,280.0 | $+123 | $+126 | HOLD (a target) | +| **ETH** | +0.291 | flat | $+70 | $+70 | HOLD (a target) | + +## P&L + +- giorno: **$+23.37** di equity (24 letture) +- realizzato: +18.90 netto su 6 round-trip chiusi · 5 fill · fee 0.0867 +- cumulato dall'arming: **$+45.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-22.md b/docs/journal/2026-08-22.md new file mode 100644 index 0000000..c2dc0d2 --- /dev/null +++ b/docs/journal/2026-08-22.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-08-22 + +*Scritto 2026-08-23T13:08:32+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $77,103.50 | -1.59% | +22.32% | +18.51% | 43.9% | ↑ ↑ ↑ (3/3 su) | 42.3 (51° pctl 1a) | +| **ETH** | $2,423.10 | -3.73% | +28.83% | +29.06% | 69.8% | ↑ ↑ ↑ (3/3 su) | 57.0 (44° pctl 1a) | + +## Libro + +Equity **$635.00** · nozionale lordo $327 · leva lorda 0.51x · barra dati 2026-08-22 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.450 | LONG @ 75,280.0 | $+186 | $+185 | HOLD (a target) | +| **ETH** | +0.277 | LONG @ 2,436.4 | $+145 | $+142 | HOLD (a target) | + +## P&L + +- giorno: **$-9.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 2 fill · fee 0.0489 +- cumulato dall'arming: **$+36.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-23.md b/docs/journal/2026-08-23.md new file mode 100644 index 0000000..9b77aae --- /dev/null +++ b/docs/journal/2026-08-23.md @@ -0,0 +1,34 @@ +# Giornale di bordo — 2026-08-23 + +*Scritto 2026-08-23T13:08:32+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $77,254.50 | +0.20% | +22.94% | +20.52% | 43.3% | ↑ ↓ ↑ (2/3 su) | 42.4 (52° pctl 1a) | +| **ETH** | $2,427.20 | +0.17% | +29.49% | +30.46% | 69.6% | ↑ ↑ ↑ (3/3 su) | 57.0 (44° pctl 1a) | + +## Libro + +Equity **$636.14** · nozionale lordo $258 · leva lorda 0.41x · barra dati 2026-08-23 · 14 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.150 | LONG @ 75,280.0 | $+115 | $+116 | HOLD (a target) | +| **ETH** | +0.271 | LONG @ 2,436.4 | $+144 | $+142 | HOLD (a target) | + +## P&L + +- giorno: **$+1.14** di equity (14 letture) +- realizzato: +5.55 netto su 2 round-trip chiusi · 1 fill · fee 0.0243 +- cumulato dall'arming: **$+38.08** + +## Salute + +- giri di `book_execute`: 14/14 *(giorno in corso)* +- eta' feed SKH all'ultimo giro: 0 min + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/scripts/cron_book.sh b/scripts/cron_book.sh index c8f0f80..da382bd 100755 --- a/scripts/cron_book.sh +++ b/scripts/cron_book.sh @@ -20,5 +20,9 @@ mkdir -p logs # = allarme inviato); l'azione a un allarme e' manuale, vedi src/live/venue_watch.py. uv run python scripts/live/venue_watch.py --quiet || true uv run python scripts/live/book_execute.py --execute + # Libro di bordo: materializza in data/live/trades.db (che il backup COPRE) l'ora vera dei + # fill, che altrimenti vive solo in questo log — gitignored, fuori dal backup e ruotabile. + # Va DOPO book_execute: dentro lo stesso blocco vede le righe appena scritte. + uv run python scripts/live/trades_db.py --sync --quiet || true echo "===== done $(date -u '+%H:%M:%SZ') =====" } >> logs/cron_book.log 2>&1 diff --git a/scripts/cron_daily.sh b/scripts/cron_daily.sh index 6598994..6aa7a7d 100755 --- a/scripts/cron_daily.sh +++ b/scripts/cron_daily.sh @@ -39,5 +39,9 @@ mkdir -p logs # decidono su queste serie: un monitor fermo produce silenzio, e il silenzio sembra uno zero. # Va DOPO tutti i monitor, altrimenti misura lo stato di ieri. uv run python scripts/live/monitor_health.py --quiet || true + # Giornale di bordo. Gira alle 00:30 UTC -> chiude IERI (giorno completo) e apre OGGI. + # Sola lettura: feed certificato, cron_book.log, trades.db. Il campo `nota` non lo tocca. + uv run python scripts/live/journal.py --giorno "$(date -u -d yesterday +%F)" --quiet || true + uv run python scripts/live/journal.py --quiet || true echo "===== done $(date -u '+%H:%M:%SZ') =====" } >> logs/cron_daily.log 2>&1 diff --git a/scripts/live/book_execute.py b/scripts/live/book_execute.py index 952764a..a8e259f 100644 --- a/scripts/live/book_execute.py +++ b/scripts/live/book_execute.py @@ -23,6 +23,8 @@ import json import sys from pathlib import Path +from datetime import datetime, timezone + import pandas as pd PROJECT_ROOT = Path(__file__).resolve().parents[2] @@ -210,7 +212,12 @@ def _run(): for f in fills: print(f" -> {f.side.upper()} {f.filled:.4f} @ ${f.price or 0:,.1f} fee {f.fee_usdc:.5f} " f"({'OK' if f.verified else 'NON VERIFICATO: ' + f.notes})") - log_event(dict(ts_utc=str(pd.Timestamp(r['last_data'])), asset=asset, action=act, + # `ts_utc` = ORA VERA del fill. Fino al 2026-08-23 qui c'era la data della + # BARRA di segnale (`r['last_data']`): 19 righe su 19 a 00:00:00, e un trade + # registrato SEI GIORNI prima di essere eseguito (ETH 0.04 del 14/07, scritto + # 08/07). La barra resta, sotto il suo nome: `bar_ts`. + log_event(dict(ts_utc=datetime.now(timezone.utc).isoformat(timespec="seconds"), + bar_ts=str(pd.Timestamp(r['last_data'])), asset=asset, action=act, side=f.side, filled=f.filled, price=f.price, fee=f.fee_usdc, verified=f.verified, notes=f.notes, net_target=net, pos_after=newpos, tp_frac=a["tp_frac"], skh_sign=a["skh_sign"])) diff --git a/scripts/live/journal.py b/scripts/live/journal.py new file mode 100644 index 0000000..0791bfa --- /dev/null +++ b/scripts/live/journal.py @@ -0,0 +1,67 @@ +"""Giornale di bordo: scrive la voce del giorno nel DB e in `docs/journal/YYYY-MM-DD.md`. + + uv run python scripts/live/journal.py # voce di oggi + uv run python scripts/live/journal.py --giorno 2026-08-21 + uv run python scripts/live/journal.py --backfill 60 # ricostruisce gli ultimi N giorni + uv run python scripts/live/journal.py --nota "testo" # annota il giorno (operatore) + +SOLA LETTURA su feed, log e venue: non tocca il libro, non manda ordini. +Il campo `nota` e' l'UNICO posto per il testo libero, e non viene mai riscritto da un +ricalcolo automatico. +""" +from __future__ import annotations + +import sys +from datetime import date, datetime, timedelta, timezone +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from src.live import journal as J # noqa: E402 +from src.live import tradesdb as T # noqa: E402 + + +def _arg(flag: str, default=None): + a = sys.argv[1:] + return a[a.index(flag) + 1] if flag in a and a.index(flag) + 1 < len(a) else default + + +def una(con, g: date, nota: str | None = None, verbose: bool = True) -> Path: + voce = J.costruisci(con, g) + J.salva(con, voce, nota=nota) + f = J.scrivi_file(con, voce) + if verbose: + p = voce["pnl"] + d = p.get("delta_equity") + print(f" {g} equity {p.get('equity_fine') or float('nan'):,.2f} " + f"({'n/d' if d is None else f'{d:+.2f}'}) " + f"fill {p['fill']} · rt {p['roundtrip_chiusi']} · giri {voce['salute']['giri_book']}/24" + f" -> {f.relative_to(ROOT)}") + return f + + +if __name__ == "__main__": + # sincronizzare PRIMA: altrimenti la sezione 'Libro' (letta dal log) e la sezione 'P&L' + # (letta dal DB) descrivono due istanti diversi e la pagina si contraddice da sola. + sys.path.insert(0, str(ROOT / "scripts" / "live")) + from trades_db import sync as _sync + _sync(verbose=False) + + con = T.connect() + nota = _arg("--nota") + if (n := _arg("--backfill")): + oggi = datetime.now(timezone.utc).date() + for i in range(int(n), -1, -1): + g = oggi - timedelta(days=i) + v = J.costruisci(con, g) + # un giorno senza giri di book non e' una voce: e' assenza di dato + if v["salute"]["giri_book"] == 0: + continue + J.salva(con, v); J.scrivi_file(con, v) + tot = con.execute("SELECT COUNT(*) c FROM journal").fetchone()["c"] + print(f" voci nel giornale: {tot}") + else: + g = date.fromisoformat(_arg("--giorno", datetime.now(timezone.utc).date().isoformat())) + una(con, g, nota=nota, verbose="--quiet" not in sys.argv[1:]) + con.close() diff --git a/scripts/live/trades_db.py b/scripts/live/trades_db.py new file mode 100644 index 0000000..f1b98d3 --- /dev/null +++ b/scripts/live/trades_db.py @@ -0,0 +1,105 @@ +"""Sincronizza `data/live/trades.db` — il libro di bordo dei trade, allineato col tempo. + + uv run python scripts/live/trades_db.py --sync # idempotente, da cron + uv run python scripts/live/trades_db.py --report # stato + P&L a video + uv run python scripts/live/trades_db.py --reconcile # incrocio delle tre fonti + +SOLA LETTURA sul venue e sui log: non manda ordini, non tocca il libro. +Il DB sta in `data/live/`, che il backup rotativo della VPS gia' copre — a differenza di +`logs/cron_book.log`, che e' oggi l'unica sede dell'ora vera dei fill. +""" +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from src.live import tradesdb as T # noqa: E402 + + +def sync(verbose: bool = True) -> dict: + con = T.connect() + runs = T.parse_cron_log(T.CRON_LOG.read_text(errors="replace")) if T.CRON_LOG.exists() else [] + fills = [f for r in runs for f in r.fills] + n_f = T.upsert_fills(con, fills) + n_e = T.upsert_equity(con, [(r.ts_utc, r.equity, "cron_book") for r in runs if r.equity is not None]) + n_r = T.rebuild_roundtrips(con) + if runs: + T.set_meta(con, "ultimo_giro", runs[-1].ts_utc) + T.set_meta(con, "ultimo_sync", T.ora()) + tot = con.execute("SELECT COUNT(*) c FROM fills").fetchone()["c"] + tot_e = con.execute("SELECT COUNT(*) c FROM equity").fetchone()["c"] + if verbose: + print(f" fills: +{n_f} (totale {tot}) | equity: +{n_e} (totale {tot_e}) | round-trip {n_r}") + con.close() + return dict(nuovi_fill=n_f, nuovi_equity=n_e, roundtrip=n_r, totale_fill=tot) + + +def reconcile() -> None: + runs = T.parse_cron_log(T.CRON_LOG.read_text(errors="replace")) + fills = [f for r in runs for f in r.fills] + righe = T.parse_executions_jsonl(T.EXEC_JSONL.read_text()) if T.EXEC_JSONL.exists() else [] + rec = T.reconcile(fills, righe) + print(f"\n cron_book.log : {len(fills)} fill (con ORA VERA)") + print(f" book_executions : {len(righe)} righe (senza ora)") + print(f" concordano : {len(rec['ok'])}") + print(f" solo nel log : {len(rec['solo_log'])}") + print(f" solo nel jsonl : {len(rec['solo_jsonl'])}") + print(f" prezzi divergenti : {len(rec['prezzi_divergenti'])} (oltre l'arrotondamento di stampa)") + for f in rec["solo_log"]: + print(f" solo-log {f.ts_utc} {f.asset} {f.side} {f.qty} @ {f.price}") + for r in rec["solo_jsonl"]: + print(f" solo-jsonl {r['ts_utc'][:10]} {r['asset']} {r['side']} {r['filled']} @ {r['price']}") + # il venue: autorevole ma TRONCATO -> si riporta cosa vede, non lo si usa per riparare + try: + from src.live.deribit import DeribitRead + d = DeribitRead() + for ins in ("BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL"): + t = d.trade_history(ins, limit=100) + print(f" venue {ins:<20}: {len(t)} trade visibili (l'endpoint TRONCA: non e' un backfill)") + except Exception as e: + print(f" venue: NON LETTO ({type(e).__name__}) — non e' 'zero trade', e' 'non misurato'") + + +def report() -> None: + con = T.connect() + n = con.execute("SELECT COUNT(*) c FROM fills").fetchone()["c"] + if not n: + print(" DB vuoto: lanciare --sync"); return + p = con.execute("SELECT MIN(ts_utc) a, MAX(ts_utc) b FROM fills").fetchone() + print(f"\n fill registrati : {n} dal {p['a'][:16]} al {p['b'][:16]}") + eq = con.execute("SELECT ts_utc, equity FROM equity ORDER BY ts_utc").fetchall() + if eq: + e0, e1 = eq[0]["equity"], eq[-1]["equity"] + picco = max(r["equity"] for r in eq) + print(f" equity : ${e0:,.2f} -> ${e1:,.2f} ({e1 - e0:+.2f}, {100*(e1/e0-1):+.2f}%)" + f" | picco ${picco:,.2f} | {len(eq)} letture") + rt = con.execute("SELECT * FROM roundtrips ORDER BY ts_out").fetchall() + lordo = sum(r["pnl_lordo"] for r in rt) + fee_rt = sum(r["fee_quota"] for r in rt) + fee_tot = con.execute("SELECT COALESCE(SUM(fee),0) s FROM fills").fetchone()["s"] + vinc = sum(1 for r in rt if r["pnl_netto"] > 0) + print(f" round-trip chiusi : {len(rt)} ({vinc} in utile) " + f"lordo {lordo:+.2f} | fee allocate {fee_rt:.2f} | netto {lordo - fee_rt:+.2f}") + print(f" fee totali pagate : {fee_tot:.4f}") + ap = T.stato_aperto(con) + if ap: + print(" posizioni aperte :") + for a, d in sorted(ap.items()): + print(f" {a} {d['qty']:.4f} @ medio ${d['prezzo_medio']:,.2f} (dal {d['dal'][:16]})") + else: + print(" posizioni aperte : nessuna (flat)") + print(f" ultimo sync : {T.get_meta(con, 'ultimo_sync', 'mai')}") + con.close() + + +if __name__ == "__main__": + args = sys.argv[1:] + if "--reconcile" in args: + reconcile() + elif "--report" in args: + report() + else: + sync(verbose="--quiet" not in args) diff --git a/src/live/journal.py b/src/live/journal.py new file mode 100644 index 0000000..d1da051 --- /dev/null +++ b/src/live/journal.py @@ -0,0 +1,233 @@ +"""Giornale di bordo del libro live: una voce al giorno, MISURATA. + +REGOLA DI QUESTO MODULO: il giornale registra NUMERI e stati derivati dai numeri. +Non scrive narrativa di mercato, non interpreta, non prevede. Il campo `nota` esiste +apposta per il testo libero dell'operatore, ed e' l'unico posto dove puo' finire un'opinione: +resta vuoto se nessuno lo scrive. *Un giornale che si inventa la lettura del mercato smette +di essere una misura e diventa un racconto — e fra sei mesi non si distingue piu' quale delle +due cose si stava leggendo.* + +TRE STATI, mai due: ogni grandezza e' un numero, oppure `None` con una ragione. Un dato +mancante NON diventa zero (lezione `paper_dvolspread` / `venue_watch`: "non vedo" non e' +"va tutto bene"). +""" +from __future__ import annotations + +import json +import math +from datetime import date, datetime, timedelta, timezone +from pathlib import Path + +import numpy as np +import pandas as pd + +from src.live import tradesdb as T + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +JOURNAL_DIR = PROJECT_ROOT / "docs" / "journal" +ASSETS = ("BTC", "ETH") +# Gli orizzonti del segnale TP01 in produzione (trend_portfolio: TSMOM 30/90/180 giorni). +ORIZZONTI = (30, 90, 180) + + +def _giornaliero(df: pd.DataFrame) -> pd.Series: + s = df.set_index(pd.to_datetime(df["timestamp"], unit="ms", utc=True))["close"] + return s.resample("1D").last().dropna() + + +def _rv_annua(px: pd.Series, n: int = 30) -> float | None: + r = np.log(px).diff().dropna() + if len(r) < n: + return None + return float(r.tail(n).std(ddof=1) * math.sqrt(365) * 100) + + +def metriche_mercato(giorno: date) -> dict: + """Stato del mercato al `giorno`, dal feed CERTIFICATO. Nessuna chiamata di rete.""" + from src.data.downloader import load_data + out: dict = {} + for a in ASSETS: + try: + px = _giornaliero(load_data(a, "1h")) + except Exception as e: + out[a] = dict(errore=f"feed non leggibile: {type(e).__name__}") + continue + px = px[px.index.date <= giorno] + if px.empty: + out[a] = dict(errore="nessuna barra fino al giorno richiesto") + continue + ult = float(px.iloc[-1]) + d = dict(chiusura=ult, barra=str(px.index[-1].date())) + for k, n in (("ret_1g", 1), ("ret_7g", 7), ("ret_30g", 30)): + d[k] = float(px.iloc[-1] / px.iloc[-1 - n] - 1) * 100 if len(px) > n else None + d["rv30_annua"] = _rv_annua(px) + # gli stessi orizzonti del segnale di produzione: e' lo stato che il libro LEGGE + d["tsmom"] = {f"{n}g": (None if len(px) <= n else int(np.sign(px.iloc[-1] / px.iloc[-1 - n] - 1))) + for n in ORIZZONTI} + segni = [v for v in d["tsmom"].values() if v is not None] + d["tsmom_su"] = (sum(1 for v in segni if v > 0), len(segni)) if segni else None + # DVOL: se il file non c'e' o e' vecchio -> None con ragione, mai 0 + f = PROJECT_ROOT / "data" / "raw" / f"dvol_{a.lower()}.parquet" + if f.exists(): + dv = pd.read_parquet(f) + dv.index = pd.to_datetime(dv["timestamp"], unit="ms", utc=True) + dv = dv[dv.index.date <= giorno]["close"] + if len(dv): + eta = (giorno - dv.index[-1].date()).days + d["dvol"] = float(dv.iloc[-1]) if eta <= 2 else None + d["dvol_eta_g"] = eta + if len(dv) >= 252 and eta <= 2: + d["dvol_rank_1a"] = float((dv.tail(252) < dv.iloc[-1]).mean()) + else: + d["dvol"] = None; d["dvol_nota"] = "nessuna barra DVOL <= giorno" + else: + d["dvol"] = None; d["dvol_nota"] = "file DVOL assente" + out[a] = d + return out + + +def stato_libro(con, giorno: date) -> dict: + """Ultimo giro di `book_execute` del giorno: cosa il libro vedeva e teneva.""" + runs = T.parse_cron_log(T.CRON_LOG.read_text(errors="replace")) if T.CRON_LOG.exists() else [] + g = [r for r in runs if r.ts_utc[:10] == giorno.isoformat()] + if not g: + return dict(errore="nessun giro di book_execute quel giorno", giri=0) + ult = g[-1] + lordo = sum(abs(v.get("pos", 0.0)) for v in ult.stato_asset.values()) + return dict(giri=len(g), ultimo_giro=ult.ts_utc, equity=ult.equity, barra=ult.last_bar, + feed_skh_min=ult.feed_min, nozionale_lordo=lordo, + leva_lorda=(lordo / ult.equity if ult.equity else None), + asset={a: dict(tp_frac=v["tp_frac"], skh_sign=v["skh_sign"], + skh_entry=v.get("skh_entry"), target=v["net"], posizione=v["pos"], + azione=v["azione"]) for a, v in ult.stato_asset.items()}) + + +def pnl_giorno(con, giorno: date) -> dict: + """P&L del giorno. `equity` e' l'autorita' (venue); il resto e' scomposizione locale.""" + g = giorno.isoformat() + eq = con.execute("SELECT ts_utc, equity FROM equity WHERE ts_utc LIKE ? ORDER BY ts_utc", + (f"{g}%",)).fetchall() + prima = con.execute("SELECT equity FROM equity WHERE ts_utc < ? ORDER BY ts_utc DESC LIMIT 1", + (g,)).fetchone() + e_ini = prima["equity"] if prima else (eq[0]["equity"] if eq else None) + e_fin = eq[-1]["equity"] if eq else None + rt = con.execute("SELECT * FROM roundtrips WHERE ts_out LIKE ?", (f"{g}%",)).fetchall() + fills = con.execute("SELECT * FROM fills WHERE ts_utc LIKE ?", (f"{g}%",)).fetchall() + e_arm = con.execute("SELECT equity FROM equity ORDER BY ts_utc LIMIT 1").fetchone() + return dict( + equity_inizio=e_ini, equity_fine=e_fin, + delta_equity=(e_fin - e_ini) if (e_ini is not None and e_fin is not None) else None, + realizzato_lordo=sum(r["pnl_lordo"] for r in rt) if rt else 0.0, + realizzato_netto=sum(r["pnl_netto"] for r in rt) if rt else 0.0, + roundtrip_chiusi=len(rt), fill=len(fills), + fee=sum(f["fee"] for f in fills) if fills else 0.0, + cumulato_da_arming=(e_fin - e_arm["equity"]) if (e_fin is not None and e_arm) else None, + letture_equity=len(eq)) + + +def salute(con, giorno: date) -> dict: + """Giri attesi: 24 per un giorno CHIUSO, le ore trascorse per quello in corso. + + Un giorno in corso confrontato con 24 produce un allarme a ogni esecuzione — e un + allarme che scatta sempre e' un allarme che non verra' letto il giorno che e' vero. + """ + lb = stato_libro(con, giorno) + adesso = datetime.now(timezone.utc) + in_corso = giorno == adesso.date() + atteso = (adesso.hour + 1) if in_corso else 24 + fatti = lb.get("giri", 0) + return dict(giri_book=fatti, feed_skh_min=lb.get("feed_skh_min"), atteso_giri=atteso, + giorno_in_corso=in_corso, giri_mancanti=max(0, atteso - fatti)) + + +def costruisci(con, giorno: date) -> dict: + return dict(giorno=giorno.isoformat(), ts_scritto=T.ora(), + mercato=metriche_mercato(giorno), libro=stato_libro(con, giorno), + pnl=pnl_giorno(con, giorno), salute=salute(con, giorno)) + + +def salva(con, voce: dict, nota: str | None = None) -> None: + esistente = con.execute("SELECT nota FROM journal WHERE giorno=?", (voce["giorno"],)).fetchone() + # la nota dell'operatore NON viene mai sovrascritta da un ricalcolo automatico + nota_finale = nota if nota is not None else (esistente["nota"] if esistente else "") + con.execute("""INSERT INTO journal (giorno, ts_scritto, mercato, libro, pnl, salute, nota) + VALUES (?,?,?,?,?,?,?) + ON CONFLICT(giorno) DO UPDATE SET + ts_scritto=excluded.ts_scritto, mercato=excluded.mercato, + libro=excluded.libro, pnl=excluded.pnl, salute=excluded.salute, + nota=excluded.nota""", + (voce["giorno"], voce["ts_scritto"], json.dumps(voce["mercato"]), + json.dumps(voce["libro"]), json.dumps(voce["pnl"]), + json.dumps(voce["salute"]), nota_finale)) + con.commit() + + +def _n(v, fmt="{:+.2f}", vuoto="n/d"): + return vuoto if v is None else fmt.format(v) + + +def rendi_markdown(con, voce: dict) -> str: + g, m, lb, p, s = voce["giorno"], voce["mercato"], voce["libro"], voce["pnl"], voce["salute"] + nota = con.execute("SELECT nota FROM journal WHERE giorno=?", (g,)).fetchone() + nota = nota["nota"] if nota else "" + L = [f"# Giornale di bordo — {g}", "", + f"*Scritto {voce['ts_scritto']}. Numeri misurati; nessuna interpretazione automatica.*", "", + "## Mercato", "", + "| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |", + "|---|---|---|---|---|---|---|---|"] + for a in ASSETS: + d = m.get(a) or {} + # tre stati: misurato / errore dichiarato / blocco assente. Nessuno dei tre e' uno zero, + # e l'ultimo non deve far cadere la pagina (un giornale che non si scrive non e' un giornale). + if "errore" in d or "chiusura" not in d: + perche = d.get("errore", "blocco mercato assente") + L.append(f"| **{a}** | n/d | n/d | n/d | n/d | n/d | n/d | {perche} |"); continue + ts = d.get("tsmom", {}) + segni = " ".join("↑" if ts.get(f"{n}g") == 1 else "↓" if ts.get(f"{n}g") == -1 else "·" + for n in ORIZZONTI) + su = d.get("tsmom_su") + dv = f"{d['dvol']:.1f}" if d.get("dvol") is not None else f"n/d ({d.get('dvol_nota','vecchio')})" + if d.get("dvol_rank_1a") is not None: + dv += f" ({100*d['dvol_rank_1a']:.0f}° pctl 1a)" + L.append(f"| **{a}** | ${d['chiusura']:,.2f} | {_n(d.get('ret_1g'),'{:+.2f}%')} | " + f"{_n(d.get('ret_7g'),'{:+.2f}%')} | {_n(d.get('ret_30g'),'{:+.2f}%')} | " + f"{_n(d.get('rv30_annua'),'{:.1f}%')} | {segni}" + f"{f' ({su[0]}/{su[1]} su)' if su else ''} | {dv} |") + L += ["", "## Libro", ""] + if "errore" in lb: + L.append(f"⚠️ {lb['errore']}") + else: + L += [f"Equity **${lb['equity']:,.2f}** · nozionale lordo ${lb['nozionale_lordo']:,.0f} " + f"· leva lorda {_n(lb.get('leva_lorda'),'{:.2f}x')} · barra dati {lb['barra']} " + f"· {lb['giri']} giri", "", + "| | TP01 | SKH01 | target | posizione | azione |", "|---|---|---|---|---|---|"] + for a, v in sorted(lb["asset"].items()): + if v["skh_sign"] == 0: + sk = "flat" + else: + sk = "LONG" if v["skh_sign"] > 0 else "SHORT" + if v.get("skh_entry"): + sk += " @ {:,.1f}".format(v["skh_entry"]) + L.append(f"| **{a}** | {v['tp_frac']:+.3f} | {sk} | ${v['target']:+,.0f} | " + f"${v['posizione']:+,.0f} | {v['azione']} |") + L += ["", "## P&L", "", + f"- giorno: **{_n(p.get('delta_equity'),'${:+,.2f}')}** di equity " + f"({p['letture_equity']} letture)", + f"- realizzato: {p['realizzato_netto']:+.2f} netto su {p['roundtrip_chiusi']} round-trip " + f"chiusi · {p['fill']} fill · fee {p['fee']:.4f}", + f"- cumulato dall'arming: **{_n(p.get('cumulato_da_arming'),'${:+,.2f}')}**", "", + "## Salute", "", + f"- giri di `book_execute`: {s['giri_book']}/{s['atteso_giri']}" + + (" *(giorno in corso)*" if s.get("giorno_in_corso") else "") + + (f" — **{s['giri_mancanti']} mancanti**" if s["giri_mancanti"] else ""), + f"- eta' feed SKH all'ultimo giro: " + + (f"{s['feed_skh_min']} min" if s["feed_skh_min"] is not None else "non misurata"), + "", "## Nota", "", (nota if nota.strip() else "*(vuota — campo dell'operatore)*"), ""] + return "\n".join(L) + + +def scrivi_file(con, voce: dict) -> Path: + JOURNAL_DIR.mkdir(parents=True, exist_ok=True) + f = JOURNAL_DIR / f"{voce['giorno']}.md" + f.write_text(rendi_markdown(con, voce)) + return f diff --git a/src/live/tradesdb.py b/src/live/tradesdb.py new file mode 100644 index 0000000..58b8816 --- /dev/null +++ b/src/live/tradesdb.py @@ -0,0 +1,351 @@ +"""Libro di bordo del book live: DB dei trade ALLINEATO COL TEMPO + serie di equity. + +PERCHE' ESISTE +-------------- +`data/live/book_executions.jsonl` registra i fill, ma il suo `ts_utc` e' la data della +**barra di segnale** (`book_execute.py`: `pd.Timestamp(r['last_data'])`), non l'ora del fill: +19 righe su 19 a `00:00:00`. L'ora vera sta solo in `logs/cron_book.log` — che e' **gitignored, +non nel backup e ruotabile**. Quindi oggi la cronologia reale del libro live vive in un file +che una rotazione cancella, e nessuno se ne accorgerebbe. + +Questo modulo MATERIALIZZA quella cronologia in `data/live/trades.db` (sqlite), che sta dentro +il perimetro gia' coperto dal backup rotativo della VPS. + +FONTI, in ordine di autorita' — e nessuna delle tre e' completa da sola +---------------------------------------------------------------------- +1. `logs/cron_book.log` -> ORA VERA del giro + contesto (equity, tp_frac, skh, target, posizione) + e la riga di fill `-> BUY 0.0008 @ $78,094.2 fee 0.02187 (OK)`. +2. `data/live/book_executions.jsonl` -> gli stessi fill con piu' cifre, ma senza ora. +3. venue (`DeribitRead.trade_history`) -> autorevole su `order_id` e timestamp in ms, + **ma tronca**: al 2026-08-23 ritorna 1 trade su BTC e 0 su ETH. + +Le tre si INCROCIANO, non si sovrascrivono: `reconcile()` riporta le divergenze e non ripara +niente da solo (una riparazione silenziosa fra due fonti che non concordano e' un'invenzione). + +TRE STATI, non due: un fill puo' essere `ok` (le fonti concordano), `solo-log`, `solo-jsonl`. +"Non lo vedo" non e' "non c'e'". +""" +from __future__ import annotations + +import hashlib +import json +import re +import sqlite3 +from dataclasses import dataclass, field +from datetime import datetime, timezone +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +DB_PATH = PROJECT_ROOT / "data" / "live" / "trades.db" +CRON_LOG = PROJECT_ROOT / "logs" / "cron_book.log" +EXEC_JSONL = PROJECT_ROOT / "data" / "live" / "book_executions.jsonl" + +# Data di armamento dell'esecuzione reale (CLAUDE.md, 2026-06-20). +ARMING = "2026-06-20" + +SCHEMA = """ +CREATE TABLE IF NOT EXISTS fills ( + fill_id TEXT PRIMARY KEY, + ts_utc TEXT NOT NULL, + ts_source TEXT NOT NULL, + bar_ts TEXT, + asset TEXT NOT NULL, + side TEXT NOT NULL, + qty REAL NOT NULL, + price REAL NOT NULL, + fee REAL NOT NULL, + action TEXT, + net_target REAL, + pos_before REAL, + pos_after REAL, + tp_frac REAL, + skh_sign INTEGER, + skh_entry REAL, + equity REAL, + order_id TEXT, + verified INTEGER, + stato TEXT NOT NULL DEFAULT 'ok' +); +CREATE INDEX IF NOT EXISTS ix_fills_ts ON fills(ts_utc); +CREATE TABLE IF NOT EXISTS roundtrips ( + rt_id INTEGER PRIMARY KEY AUTOINCREMENT, + asset TEXT NOT NULL, + qty REAL NOT NULL, + ts_in TEXT NOT NULL, + px_in REAL NOT NULL, + ts_out TEXT NOT NULL, + px_out REAL NOT NULL, + ore_tenuta REAL, + pnl_lordo REAL NOT NULL, + fee_quota REAL NOT NULL, + pnl_netto REAL NOT NULL +); +CREATE TABLE IF NOT EXISTS equity ( + ts_utc TEXT PRIMARY KEY, + equity REAL NOT NULL, + src TEXT NOT NULL +); +CREATE TABLE IF NOT EXISTS journal ( + giorno TEXT PRIMARY KEY, + ts_scritto TEXT NOT NULL, + mercato TEXT, + libro TEXT, + pnl TEXT, + salute TEXT, + nota TEXT NOT NULL DEFAULT '' +); +CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY, v TEXT); +""" + + +# ============================================================================================= +# parsing — puro, nessun I/O: prende testo, ritorna dati +# ============================================================================================= + +@dataclass +class Fill: + ts_utc: str + asset: str + side: str + qty: float + price: float + fee: float + ts_source: str = "cron_log" + bar_ts: str | None = None + action: str | None = None + net_target: float | None = None + pos_before: float | None = None + pos_after: float | None = None + tp_frac: float | None = None + skh_sign: int | None = None + skh_entry: float | None = None + equity: float | None = None + order_id: str | None = None + verified: int = 1 + + @property + def fill_id(self) -> str: + raw = f"{self.ts_utc}|{self.asset}|{self.side}|{self.qty:.10f}|{self.price:.6f}" + return hashlib.sha1(raw.encode()).hexdigest()[:16] + + +@dataclass +class Run: + """Un giro orario di `book_execute`.""" + ts_utc: str + equity: float | None = None + last_bar: str | None = None + feed_min: int | None = None + fills: list[Fill] = field(default_factory=list) + stato_asset: dict = field(default_factory=dict) + + +_RE_HEAD = re.compile(r"^===== (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) cron_book =====$") +_RE_EQ = re.compile(r"conto reale\s*:\s*\$([\d,\.]+)") +_RE_BAR = re.compile(r"ultima barra\s*:\s*(\S+)") +_RE_FEED = re.compile(r"feed SKH\s*:\s*\w+ \((\d+) min\)") +_RE_SIG = re.compile( + r"^\s*(BTC|ETH) TP ([+-][\d\.]+) · SKH ([+-]\d+)\(([^)]*)\) -> net \$([+-][\d,]+) " + r"\| pos \$([+-][\d,]+) -> (.+?)\s*$") +_RE_FILL = re.compile( + r"^\s*-> (BUY|SELL) ([\d\.]+) @ \$([\d,\.]+) fee ([\d\.]+) \((OK|NON VERIFICATO[^)]*)\)") + + +def _num(s: str) -> float: + return float(s.replace(",", "").replace("$", "")) + + +def parse_cron_log(text: str) -> list[Run]: + """Estrae i giri da `cron_book.log`. L'ORA del blocco e' l'ora vera del fill.""" + runs: list[Run] = [] + cur: Run | None = None + ultimo_asset: str | None = None + for line in text.splitlines(): + m = _RE_HEAD.match(line) + if m: + cur = Run(ts_utc=m.group(1).replace("Z", "+00:00")) + runs.append(cur) + ultimo_asset = None + continue + if cur is None: + continue + if (m := _RE_EQ.search(line)) and cur.equity is None: + cur.equity = _num(m.group(1)); continue + if (m := _RE_BAR.search(line)) and cur.last_bar is None: + cur.last_bar = m.group(1); continue + if (m := _RE_FEED.search(line)) and cur.feed_min is None: + cur.feed_min = int(m.group(1)); continue + if (m := _RE_SIG.match(line)): + asset = m.group(1) + entry = None + sk = m.group(4) + if "@" in sk: + try: + entry = float(sk.split("@", 1)[1]) + except ValueError: + entry = None + cur.stato_asset[asset] = dict( + tp_frac=float(m.group(2)), skh_sign=int(m.group(3)), skh_entry=entry, + net=_num(m.group(5)), pos=_num(m.group(6)), azione=m.group(7).strip()) + ultimo_asset = asset + continue + if (m := _RE_FILL.match(line)) and ultimo_asset: + st = cur.stato_asset.get(ultimo_asset, {}) + cur.fills.append(Fill( + ts_utc=cur.ts_utc, asset=ultimo_asset, side=m.group(1).lower(), + qty=float(m.group(2)), price=_num(m.group(3)), fee=float(m.group(4)), + bar_ts=cur.last_bar, action=st.get("azione"), net_target=st.get("net"), + pos_before=st.get("pos"), tp_frac=st.get("tp_frac"), + skh_sign=st.get("skh_sign"), skh_entry=st.get("skh_entry"), + equity=cur.equity, verified=1 if m.group(5) == "OK" else 0)) + return runs + + +def parse_executions_jsonl(text: str) -> list[dict]: + return [json.loads(l) for l in text.splitlines() if l.strip()] + + +def reconcile(fills_log: list[Fill], righe_jsonl: list[dict], tol_px: float = 0.51) -> dict: + """Incrocia le due fonti su (data, asset, side, qty). NON ripara: riporta. + + `tol_px`: i prezzi del log sono arrotondati a 1 decimale in stampa -> confronto con + tolleranza, e la divergenza sopra tolleranza si REGISTRA invece di essere assorbita. + """ + def chiave(g, a, s, q): + return (g, a, s, round(float(q), 8)) + + ix_log: dict = {} + for f in fills_log: + ix_log.setdefault(chiave(f.ts_utc[:10], f.asset, f.side, f.qty), []).append(f) + ix_js: dict = {} + for r in righe_jsonl: + ix_js.setdefault(chiave(r["ts_utc"][:10], r["asset"], r["side"], r["filled"]), []).append(r) + + ok, solo_log, solo_js, px_diversi = [], [], [], [] + for k, gl in ix_log.items(): + gj = ix_js.get(k, []) + for i, f in enumerate(gl): + if i < len(gj): + r = gj[i] + if abs(float(r["price"]) - f.price) > tol_px: + px_diversi.append((f, r)) + ok.append((f, r)) + else: + solo_log.append(f) + for k, gj in ix_js.items(): + extra = len(gj) - len(ix_log.get(k, [])) + for r in gj[max(0, len(gj) - extra):] if extra > 0 else []: + solo_js.append(r) + return dict(ok=ok, solo_log=solo_log, solo_jsonl=solo_js, prezzi_divergenti=px_diversi) + + +def fifo_roundtrips(fills: list[Fill]) -> tuple[list[dict], dict]: + """Round-trip chiusi per FIFO + lotti residui. Le fee si allocano PRO-QUOTA sulla qty.""" + from collections import deque, defaultdict + lotti: dict[str, deque] = defaultdict(deque) + rts: list[dict] = [] + for f in sorted(fills, key=lambda x: (x.ts_utc, x.asset)): + fee_u = f.fee / f.qty if f.qty else 0.0 + if f.side == "buy": + lotti[f.asset].append([f.qty, f.price, f.ts_utc, fee_u]) + else: + resto = f.qty + while resto > 1e-12 and lotti[f.asset]: + lq, lp, lts, lfee_u = lotti[f.asset][0] + usa = min(resto, lq) + lordo = usa * (f.price - lp) + quota_fee = usa * (lfee_u + fee_u) + ore = (datetime.fromisoformat(f.ts_utc) - datetime.fromisoformat(lts)).total_seconds() / 3600 + rts.append(dict(asset=f.asset, qty=usa, ts_in=lts, px_in=lp, ts_out=f.ts_utc, + px_out=f.price, ore_tenuta=ore, pnl_lordo=lordo, + fee_quota=quota_fee, pnl_netto=lordo - quota_fee)) + lq -= usa; resto -= usa + if lq <= 1e-12: + lotti[f.asset].popleft() + else: + lotti[f.asset][0][0] = lq + aperti = {a: [dict(qty=q, price=p, ts=t) for q, p, t, _ in dq] for a, dq in lotti.items() if dq} + return rts, aperti + + +# ============================================================================================= +# persistenza +# ============================================================================================= + +def connect(path: Path | str = DB_PATH) -> sqlite3.Connection: + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + con = sqlite3.connect(path) + con.row_factory = sqlite3.Row + con.executescript(SCHEMA) + return con + + +def upsert_fills(con: sqlite3.Connection, fills: list[Fill]) -> int: + n = 0 + for f in fills: + cur = con.execute( + """INSERT INTO fills (fill_id, ts_utc, ts_source, bar_ts, asset, side, qty, price, fee, + action, net_target, pos_before, pos_after, tp_frac, skh_sign, + skh_entry, equity, order_id, verified, stato) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ON CONFLICT(fill_id) DO NOTHING""", + (f.fill_id, f.ts_utc, f.ts_source, f.bar_ts, f.asset, f.side, f.qty, f.price, f.fee, + f.action, f.net_target, f.pos_before, f.pos_after, f.tp_frac, f.skh_sign, + f.skh_entry, f.equity, f.order_id, f.verified, "ok")) + n += cur.rowcount + con.commit() + return n + + +def upsert_equity(con: sqlite3.Connection, punti: list[tuple[str, float, str]]) -> int: + n = 0 + for ts, v, src in punti: + cur = con.execute("INSERT INTO equity (ts_utc, equity, src) VALUES (?,?,?) " + "ON CONFLICT(ts_utc) DO NOTHING", (ts, v, src)) + n += cur.rowcount + con.commit() + return n + + +def rebuild_roundtrips(con: sqlite3.Connection) -> int: + """Ricalcola da zero: i round-trip sono DERIVATI, mai inseriti a mano.""" + righe = con.execute("SELECT * FROM fills ORDER BY ts_utc, asset").fetchall() + fills = [Fill(ts_utc=r["ts_utc"], asset=r["asset"], side=r["side"], qty=r["qty"], + price=r["price"], fee=r["fee"]) for r in righe] + rts, _ = fifo_roundtrips(fills) + con.execute("DELETE FROM roundtrips") + con.executemany( + """INSERT INTO roundtrips (asset, qty, ts_in, px_in, ts_out, px_out, ore_tenuta, + pnl_lordo, fee_quota, pnl_netto) + VALUES (:asset,:qty,:ts_in,:px_in,:ts_out,:px_out,:ore_tenuta, + :pnl_lordo,:fee_quota,:pnl_netto)""", rts) + con.commit() + return len(rts) + + +def set_meta(con: sqlite3.Connection, k: str, v: str) -> None: + con.execute("INSERT INTO meta (k,v) VALUES (?,?) ON CONFLICT(k) DO UPDATE SET v=excluded.v", (k, v)) + con.commit() + + +def get_meta(con: sqlite3.Connection, k: str, default=None): + r = con.execute("SELECT v FROM meta WHERE k=?", (k,)).fetchone() + return r["v"] if r else default + + +def stato_aperto(con: sqlite3.Connection) -> dict: + righe = con.execute("SELECT * FROM fills ORDER BY ts_utc, asset").fetchall() + fills = [Fill(ts_utc=r["ts_utc"], asset=r["asset"], side=r["side"], qty=r["qty"], + price=r["price"], fee=r["fee"]) for r in righe] + _, aperti = fifo_roundtrips(fills) + out = {} + for a, lotti in aperti.items(): + q = sum(x["qty"] for x in lotti) + costo = sum(x["qty"] * x["price"] for x in lotti) + out[a] = dict(qty=q, prezzo_medio=costo / q if q else 0.0, dal=min(x["ts"] for x in lotti)) + return out + + +def ora() -> str: + return datetime.now(timezone.utc).isoformat(timespec="seconds") diff --git a/tests/test_journal.py b/tests/test_journal.py new file mode 100644 index 0000000..8c2f14c --- /dev/null +++ b/tests/test_journal.py @@ -0,0 +1,138 @@ +"""Il giornale di bordo: tre stati invece di due, e la nota dell'operatore che non si perde. + +Il rischio di un giornale automatico non e' sbagliare un numero: e' scrivere `0` dove il dato +non c'era, e leggerlo fra sei mesi come una misura. +""" +from __future__ import annotations + +import json +import sys +from datetime import date, datetime, timedelta, timezone +from pathlib import Path + +import pandas as pd +import pytest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from src.live import journal as J # noqa: E402 +from src.live import tradesdb as T # noqa: E402 + + +@pytest.fixture() +def db(tmp_path): + con = T.connect(tmp_path / "t.db") + yield con + con.close() + + +# ------------------------------------------------- i giri attesi non sono sempre 24 + +def test_giorno_in_corso_non_e_un_giorno_bucato(db, monkeypatch): + """24 giri attesi su un giorno non finito = allarme a ogni esecuzione, cioe' rumore.""" + oggi = datetime.now(timezone.utc).date() + monkeypatch.setattr(J, "stato_libro", lambda c, g: dict(giri=datetime.now(timezone.utc).hour + 1, + feed_skh_min=0)) + s = J.salute(db, oggi) + assert s["giorno_in_corso"] is True + assert s["atteso_giri"] == datetime.now(timezone.utc).hour + 1 + assert s["giri_mancanti"] == 0 + + +def test_giorno_chiuso_si_giudica_su_24(db, monkeypatch): + monkeypatch.setattr(J, "stato_libro", lambda c, g: dict(giri=20, feed_skh_min=0)) + s = J.salute(db, date(2026, 8, 21)) + assert s["giorno_in_corso"] is False + assert s["atteso_giri"] == 24 and s["giri_mancanti"] == 4 + + +def test_controllo_positivo_un_giorno_davvero_bucato_viene_segnalato(db, monkeypatch): + monkeypatch.setattr(J, "stato_libro", lambda c, g: dict(giri=6, feed_skh_min=None)) + assert J.salute(db, date(2026, 8, 21))["giri_mancanti"] == 18 + + +# ------------------------------------------------- dato assente != zero + +def test_dvol_assente_e_None_con_ragione_mai_zero(db, monkeypatch, tmp_path): + monkeypatch.setattr(J, "PROJECT_ROOT", tmp_path) # nessun file DVOL qui + (tmp_path / "data" / "raw").mkdir(parents=True) + m = J.metriche_mercato(date(2026, 8, 21)) + for a in ("BTC", "ETH"): + if "errore" in m[a]: + continue + assert m[a]["dvol"] is None + assert m[a]["dvol_nota"] == "file DVOL assente" # la ragione, non un valore + + +def test_giorno_senza_giri_e_un_errore_dichiarato_non_un_libro_vuoto(db, monkeypatch): + monkeypatch.setattr(T, "CRON_LOG", Path("/tmp/non-esiste-affatto.log")) + lb = J.stato_libro(db, date(2026, 8, 21)) + assert "errore" in lb and lb["giri"] == 0 + assert "equity" not in lb # non inventa uno zero + + +def test_pnl_senza_letture_di_equity_non_fabbrica_un_delta(db): + p = J.pnl_giorno(db, date(2026, 8, 21)) + assert p["delta_equity"] is None and p["equity_fine"] is None + assert p["realizzato_netto"] == 0.0 and p["roundtrip_chiusi"] == 0 # 0 trade e' un fatto + + +# ------------------------------------------------- la nota dell'operatore + +def test_la_nota_sopravvive_a_un_ricalcolo(db): + g = date(2026, 8, 21) + voce = dict(giorno=g.isoformat(), ts_scritto=T.ora(), mercato={}, libro={}, pnl={}, salute={}) + J.salva(db, voce, nota="rally post-CPI, non ho toccato niente") + J.salva(db, voce) # ricalcolo automatico + n = db.execute("SELECT nota FROM journal WHERE giorno=?", (g.isoformat(),)).fetchone()["nota"] + assert n == "rally post-CPI, non ho toccato niente" + + +def test_la_nota_si_puo_sostituire_esplicitamente(db): + g = date(2026, 8, 21) + voce = dict(giorno=g.isoformat(), ts_scritto=T.ora(), mercato={}, libro={}, pnl={}, salute={}) + J.salva(db, voce, nota="prima") + J.salva(db, voce, nota="seconda") + n = db.execute("SELECT nota FROM journal WHERE giorno=?", (g.isoformat(),)).fetchone()["nota"] + assert n == "seconda" + + +# ------------------------------------------------- il markdown non mente + +def test_il_markdown_dice_n_d_e_non_zero_quando_manca_il_dato(db): + voce = dict(giorno="2026-08-21", ts_scritto=T.ora(), + mercato={"BTC": {"errore": "feed non leggibile: FileNotFoundError"}, + "ETH": {"errore": "feed non leggibile: FileNotFoundError"}}, + libro={"errore": "nessun giro di book_execute quel giorno", "giri": 0}, + pnl=J.pnl_giorno(db, date(2026, 8, 21)), + salute=dict(giri_book=0, feed_skh_min=None, atteso_giri=24, + giorno_in_corso=False, giri_mancanti=24)) + J.salva(db, voce) + md = J.rendi_markdown(db, voce) + assert "n/d" in md and "24 mancanti" in md + assert "non misurata" in md # eta' feed sconosciuta + assert "$+0.00 di equity" not in md # niente zeri inventati + + +def test_il_markdown_riporta_il_segnale_che_il_libro_leggeva(db): + voce = dict(giorno="2026-08-21", ts_scritto=T.ora(), mercato={}, + libro=dict(giri=24, ultimo_giro="2026-08-21T23:07:01+00:00", equity=644.0, + barra="2026-08-21", feed_skh_min=0, nozionale_lordo=196.0, + leva_lorda=0.304, + asset={"BTC": dict(tp_frac=0.176, skh_sign=1, skh_entry=75280.0, + target=123.0, posizione=126.0, azione="HOLD (a target)")}), + pnl=J.pnl_giorno(db, date(2026, 8, 21)), + salute=dict(giri_book=24, feed_skh_min=0, atteso_giri=24, + giorno_in_corso=False, giri_mancanti=0)) + J.salva(db, voce) + md = J.rendi_markdown(db, voce) + assert "+0.176" in md and "LONG @ 75,280.0" in md and "0.30x" in md + + +def test_lo_stato_tsmom_usa_gli_orizzonti_di_produzione(): + """Il giornale deve riportare CIO' CHE IL LIBRO LEGGE, non un trend a caso.""" + from src.strategies import trend_portfolio as tp + src = Path(tp.__file__).read_text() + for n in J.ORIZZONTI: + assert str(n) in src, f"orizzonte {n} non compare in trend_portfolio.py" diff --git a/tests/test_tradesdb.py b/tests/test_tradesdb.py new file mode 100644 index 0000000..eea0d0d --- /dev/null +++ b/tests/test_tradesdb.py @@ -0,0 +1,207 @@ +"""Il libro di bordo dei trade: parsing, FIFO, riconciliazione, idempotenza. + +Ogni rilevatore ha il suo CONTROLLO POSITIVO: un parser che non estrae mai niente e uno che +estrae tutto passano gli stessi test pigri, e nessuno dei due sta misurando qualcosa. +""" +from __future__ import annotations + +import sqlite3 +import sys +from datetime import date, datetime, timedelta, timezone +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from src.live import tradesdb as T # noqa: E402 + +LOG_FINTO = """===== 2026-08-20T04:07:01Z cron_book ===== + modo : ESECUZIONE REALE + conto reale : $610.82 + ultima barra : 2026-08-20 + + feed SKH : fresco (3 min) + BTC TP +0.199 · SKH +1(LONG@66475.9) -> net $+122 | pos $+76 -> BUY $+46 + -> BUY 0.0007 @ $69,351.8 fee 0.01699 (OK) + ETH TP +0.294 · SKH +0(flat) -> net $+67 | pos $+0 -> BUY $+67 + -> BUY 0.0298 @ $2,257.9 fee 0.02355 (OK) +===== done 04:07:14Z ===== +===== 2026-08-20T05:07:01Z cron_book ===== + conto reale : $611.90 + ultima barra : 2026-08-20 + + feed SKH : fresco (0 min) + BTC TP +0.199 · SKH +1(LONG@66475.9) -> net $+123 | pos $+123 -> HOLD (a target) + ETH TP +0.294 · SKH +0(flat) -> net $+67 | pos $+67 -> HOLD (a target) + + => Nessuna azione: conto gia' al target netto del book. +===== done 05:07:11Z ===== +""" + + +# ---------------------------------------------------------------- parsing del log + +def test_estrae_ora_vera_e_non_la_barra(): + runs = T.parse_cron_log(LOG_FINTO) + assert len(runs) == 2 + f = runs[0].fills[0] + assert f.ts_utc.startswith("2026-08-20T04:07:01") # ora del giro + assert f.bar_ts == "2026-08-20" # barra, sotto il suo nome + assert f.ts_utc[:10] != "" and "T" in f.ts_utc # non e' una data nuda + + +def test_estrae_contesto_del_segnale(): + r = T.parse_cron_log(LOG_FINTO)[0] + assert r.equity == 610.82 and r.feed_min == 3 + f = [x for x in r.fills if x.asset == "BTC"][0] + assert f.tp_frac == 0.199 and f.skh_sign == 1 and f.skh_entry == 66475.9 + assert f.net_target == 122 and f.pos_before == 76 + assert f.qty == 0.0007 and f.price == 69351.8 and f.fee == 0.01699 + assert f.verified == 1 + + +def test_controllo_positivo_un_giro_senza_fill_non_ne_inventa(): + """Il secondo giro e' un HOLD: se il parser tornasse fill anche li', non starebbe leggendo.""" + runs = T.parse_cron_log(LOG_FINTO) + assert len(runs[1].fills) == 0 + assert runs[1].stato_asset["BTC"]["azione"] == "HOLD (a target)" + + +def test_fill_non_verificato_resta_marcato(): + log = LOG_FINTO.replace("fee 0.01699 (OK)", "fee 0.01699 (NON VERIFICATO: timeout)") + f = T.parse_cron_log(log)[0].fills[0] + assert f.verified == 0 + + +def test_fill_id_deterministico_e_distingue_due_fill_simili(): + a = T.Fill(ts_utc="2026-08-20T04:07:01+00:00", asset="BTC", side="buy", qty=0.0007, price=69351.8, fee=0.01) + b = T.Fill(ts_utc="2026-08-20T04:07:01+00:00", asset="BTC", side="buy", qty=0.0007, price=69351.8, fee=0.01) + c = T.Fill(ts_utc="2026-08-20T05:07:01+00:00", asset="BTC", side="buy", qty=0.0007, price=69351.8, fee=0.01) + assert a.fill_id == b.fill_id and a.fill_id != c.fill_id + + +# ---------------------------------------------------------------- FIFO + +def _f(ts, asset, side, qty, price, fee=0.0): + return T.Fill(ts_utc=ts, asset=asset, side=side, qty=qty, price=price, fee=fee) + + +def test_fifo_chiude_nell_ordine_giusto_e_alloca_le_fee(): + fills = [_f("2026-01-01T00:00:00+00:00", "BTC", "buy", 1.0, 100.0, 1.0), + _f("2026-01-02T00:00:00+00:00", "BTC", "buy", 1.0, 200.0, 1.0), + _f("2026-01-03T00:00:00+00:00", "BTC", "sell", 1.0, 300.0, 1.0)] + rts, aperti = T.fifo_roundtrips(fills) + assert len(rts) == 1 + assert rts[0]["px_in"] == 100.0 # FIFO: chiude il PRIMO lotto + assert rts[0]["pnl_lordo"] == pytest.approx(200.0) + assert rts[0]["fee_quota"] == pytest.approx(2.0) # fee d'entrata + d'uscita, pro-quota + assert rts[0]["pnl_netto"] == pytest.approx(198.0) + assert rts[0]["ore_tenuta"] == pytest.approx(48.0) + assert aperti["BTC"][0]["price"] == 200.0 # resta il secondo lotto + + +def test_fifo_chiusura_parziale_spezza_il_lotto(): + fills = [_f("2026-01-01T00:00:00+00:00", "ETH", "buy", 1.0, 100.0), + _f("2026-01-02T00:00:00+00:00", "ETH", "sell", 0.25, 120.0)] + rts, aperti = T.fifo_roundtrips(fills) + assert len(rts) == 1 and rts[0]["qty"] == pytest.approx(0.25) + assert rts[0]["pnl_lordo"] == pytest.approx(5.0) + assert aperti["ETH"][0]["qty"] == pytest.approx(0.75) + + +def test_fifo_non_mescola_gli_asset(): + fills = [_f("2026-01-01T00:00:00+00:00", "BTC", "buy", 1.0, 100.0), + _f("2026-01-02T00:00:00+00:00", "ETH", "sell", 1.0, 100.0)] + rts, aperti = T.fifo_roundtrips(fills) + assert rts == [] # la vendita ETH non chiude il long BTC + assert set(aperti) == {"BTC"} + + +def test_fifo_riproduce_il_conto_del_libro_vero(): + """Controllo di realta' sui numeri gia' pubblicati: 12 round-trip, 11 in utile.""" + if not T.CRON_LOG.exists(): + pytest.skip("cron_book.log assente") + fills = [f for r in T.parse_cron_log(T.CRON_LOG.read_text(errors="replace")) for f in r.fills] + rts, _ = T.fifo_roundtrips(fills) + assert len(rts) >= 12 + assert sum(1 for r in rts if r["pnl_netto"] > 0) >= 11 + + +# ---------------------------------------------------------------- riconciliazione + +def test_riconciliazione_scopre_lo_sfasamento_di_data(): + """IL difetto vero: il jsonl datava un trade alla BARRA, non al fill (14/07 scritto 08/07).""" + fills = [_f("2026-07-14T14:00:02+00:00", "ETH", "buy", 0.04, 1869.7)] + righe = [dict(ts_utc="2026-07-08 00:00:00", asset="ETH", side="buy", filled=0.04, price=1869.74)] + rec = T.reconcile(fills, righe) + assert len(rec["ok"]) == 0 + assert len(rec["solo_log"]) == 1 and len(rec["solo_jsonl"]) == 1 + + +def test_riconciliazione_controllo_positivo_quando_concordano(): + """Speculare del precedente: se le due fonti coincidono NON deve segnalare niente.""" + fills = [_f("2026-08-20T04:07:01+00:00", "BTC", "buy", 0.0007, 69351.8)] + righe = [dict(ts_utc="2026-08-20 00:00:00", asset="BTC", side="buy", filled=0.0007, price=69351.8)] + rec = T.reconcile(fills, righe) + assert len(rec["ok"]) == 1 and not rec["solo_log"] and not rec["solo_jsonl"] + assert not rec["prezzi_divergenti"] + + +def test_riconciliazione_segnala_un_prezzo_davvero_diverso_non_l_arrotondamento(): + base = dict(asset="BTC", side="buy", filled=0.0007, ts_utc="2026-08-20 00:00:00") + f = [_f("2026-08-20T04:07:01+00:00", "BTC", "buy", 0.0007, 69351.8)] + assert not T.reconcile(f, [dict(base, price=69351.84)])["prezzi_divergenti"] # stampa a 1 dec. + assert T.reconcile(f, [dict(base, price=69400.00)])["prezzi_divergenti"] # divergenza vera + + +# ---------------------------------------------------------------- persistenza + +@pytest.fixture() +def db(tmp_path): + con = T.connect(tmp_path / "t.db") + yield con + con.close() + + +def test_upsert_e_idempotente(db): + fills = [f for r in T.parse_cron_log(LOG_FINTO) for f in r.fills] + assert T.upsert_fills(db, fills) == 2 + assert T.upsert_fills(db, fills) == 0 # secondo giro: nessun duplicato + assert db.execute("SELECT COUNT(*) c FROM fills").fetchone()["c"] == 2 + + +def test_roundtrip_sono_derivati_non_accumulati(db): + T.upsert_fills(db, [_f("2026-01-01T00:00:00+00:00", "BTC", "buy", 1.0, 100.0), + _f("2026-01-02T00:00:00+00:00", "BTC", "sell", 1.0, 110.0)]) + assert T.rebuild_roundtrips(db) == 1 + assert T.rebuild_roundtrips(db) == 1 # ricalcolo, non append + assert db.execute("SELECT COUNT(*) c FROM roundtrips").fetchone()["c"] == 1 + + +def test_equity_una_riga_per_istante(db): + assert T.upsert_equity(db, [("2026-01-01T00:00:00+00:00", 600.0, "x")]) == 1 + assert T.upsert_equity(db, [("2026-01-01T00:00:00+00:00", 999.0, "x")]) == 0 + assert db.execute("SELECT equity FROM equity").fetchone()["equity"] == 600.0 + + +def test_stato_aperto_media_i_lotti(db): + T.upsert_fills(db, [_f("2026-01-01T00:00:00+00:00", "ETH", "buy", 1.0, 100.0), + _f("2026-01-02T00:00:00+00:00", "ETH", "buy", 1.0, 200.0)]) + st = T.stato_aperto(db) + assert st["ETH"]["qty"] == pytest.approx(2.0) + assert st["ETH"]["prezzo_medio"] == pytest.approx(150.0) + + +# ---------------------------------------------------------------- regressione sulla SORGENTE + +def test_l_esecutore_non_data_piu_i_fill_alla_barra_di_segnale(): + """Il difetto riparato il 2026-08-23: `ts_utc` era `pd.Timestamp(r['last_data'])`. + + Se qualcuno lo rimette, questo test lo dice — il DB verrebbe ricostruito su date finte. + """ + src = (Path(__file__).resolve().parents[1] / "scripts" / "live" / "book_execute.py").read_text() + assert "ts_utc=str(pd.Timestamp(r['last_data']))" not in src + assert "bar_ts=str(pd.Timestamp(r['last_data']))" in src + assert "ts_utc=datetime.now(timezone.utc)" in src From 1803e0ac9f41ec707031ece4c4cfb9c763f32a6b Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 13:36:05 +0000 Subject: [PATCH 113/114] giornale: lettura ragionata a regole dichiarate e tracciabili MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Non prosa libera: dodici regole, ognuna con un id stampato accanto alla riga che ha prodotto. Combinano solo numeri gia' presenti nella pagina, tacciono sul non misurato e non prevedono niente. Il campo `nota` resta l'unico posto dove puo' finire un giudizio umano. Le regole: stato del libro e PERCHE' e' flat (componente per componente), disaccordo fra orizzonti del trend contro esposizione TP01, incoerenza (trend su ma libro fuori), leva contro il tetto, P&L del giorno, costo che supera il movimento, concentrazione del cumulato, drawdown dal picco, implicita contro realizzata col gate IV-rank di VRP01, giornata oltre 2 deviazioni, giri mancanti e feed vecchio, e la taglia del campione. Ogni regola ha DUE test: uno che la accende e uno che la tiene spenta. Una regola che si accende sempre non sta leggendo niente, una che non si accende mai e' indistinguibile da una rotta. Tre difetti corretti prima di pubblicare, tutti trovati scrivendo i test: - il tetto di leva era RIDICHIARATO (0.5 cablato) invece che letto da config/live.json — quinta occorrenza dello schema che il progetto paga da luglio: un sorvegliante che ridichiara il proprio bersaglio continua a passare il giorno che il bersaglio cambia. Ora deriva, e se il config non si legge lo dice invece di inventare un tetto. - l'IV-rank era il percentile a UN ANNO etichettato col nome del gate di VRP01, che usa un percentile ESPANDENTE. Due statistiche diverse, e oggi danno il verdetto OPPOSTO: 0.52 (sopra la soglia, "il sleeve venderebbe") contro 0.18 (sotto, sleeve fermo). Il valore giusto combacia con quanto gia' misurato il 30/07: 0/8 settimane passano il gate. - la concentrazione si accendeva su $2,08 di cumulato: vera e inutile. Ora ha una soglia di rilevanza, o e' una riga che insegna a saltare la sezione. 62 voci rigenerate. Strategia, pesi, config INVARIATI. 675 test passano. Co-Authored-By: Claude Opus 5 (1M context) --- docs/journal/2026-06-23.md | 14 ++- docs/journal/2026-06-24.md | 14 ++- docs/journal/2026-06-25.md | 14 ++- docs/journal/2026-06-26.md | 14 ++- docs/journal/2026-06-27.md | 14 ++- docs/journal/2026-06-28.md | 14 ++- docs/journal/2026-06-29.md | 14 ++- docs/journal/2026-06-30.md | 14 ++- docs/journal/2026-07-01.md | 14 ++- docs/journal/2026-07-02.md | 14 ++- docs/journal/2026-07-03.md | 14 ++- docs/journal/2026-07-04.md | 14 ++- docs/journal/2026-07-05.md | 14 ++- docs/journal/2026-07-06.md | 14 ++- docs/journal/2026-07-07.md | 14 ++- docs/journal/2026-07-08.md | 14 ++- docs/journal/2026-07-09.md | 14 ++- docs/journal/2026-07-10.md | 14 ++- docs/journal/2026-07-11.md | 14 ++- docs/journal/2026-07-12.md | 14 ++- docs/journal/2026-07-13.md | 14 ++- docs/journal/2026-07-14.md | 15 ++- docs/journal/2026-07-15.md | 13 ++- docs/journal/2026-07-16.md | 13 ++- docs/journal/2026-07-17.md | 14 ++- docs/journal/2026-07-18.md | 15 ++- docs/journal/2026-07-19.md | 15 ++- docs/journal/2026-07-20.md | 15 ++- docs/journal/2026-07-21.md | 15 ++- docs/journal/2026-07-22.md | 15 ++- docs/journal/2026-07-23.md | 15 ++- docs/journal/2026-07-24.md | 15 ++- docs/journal/2026-07-25.md | 15 ++- docs/journal/2026-07-26.md | 15 ++- docs/journal/2026-07-27.md | 15 ++- docs/journal/2026-07-28.md | 15 ++- docs/journal/2026-07-29.md | 15 ++- docs/journal/2026-07-30.md | 15 ++- docs/journal/2026-07-31.md | 15 ++- docs/journal/2026-08-01.md | 15 ++- docs/journal/2026-08-02.md | 15 ++- docs/journal/2026-08-03.md | 15 ++- docs/journal/2026-08-04.md | 15 ++- docs/journal/2026-08-05.md | 15 ++- docs/journal/2026-08-06.md | 15 ++- docs/journal/2026-08-07.md | 15 ++- docs/journal/2026-08-08.md | 15 ++- docs/journal/2026-08-09.md | 15 ++- docs/journal/2026-08-10.md | 15 ++- docs/journal/2026-08-11.md | 15 ++- docs/journal/2026-08-12.md | 15 ++- docs/journal/2026-08-13.md | 15 ++- docs/journal/2026-08-14.md | 15 ++- docs/journal/2026-08-15.md | 15 ++- docs/journal/2026-08-16.md | 15 ++- docs/journal/2026-08-17.md | 16 ++- docs/journal/2026-08-18.md | 15 ++- docs/journal/2026-08-19.md | 17 ++- docs/journal/2026-08-20.md | 16 ++- docs/journal/2026-08-21.md | 16 ++- docs/journal/2026-08-22.md | 15 ++- docs/journal/2026-08-23.md | 16 ++- src/live/journal.py | 205 ++++++++++++++++++++++++++++++++++++- tests/test_journal.py | 171 +++++++++++++++++++++++++++++++ 64 files changed, 1223 insertions(+), 63 deletions(-) diff --git a/docs/journal/2026-06-23.md b/docs/journal/2026-06-23.md index 65d4324..f483424 100644 --- a/docs/journal/2026-06-23.md +++ b/docs/journal/2026-06-23.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-06-23 -*Scritto 2026-08-23T13:08:26+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:09+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,18 @@ Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 2/24 — **22 mancanti** - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 41.6 contro realizzata 30g 42.8 (sotto); DVOL al 27° pctl di un anno; IV-rank espandente 0.14 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 56.2 contro realizzata 30g 63.4 (sotto); DVOL al 21° pctl di un anno; IV-rank espandente 0.15 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[giri]` ⚠️ **22 giri di `book_execute` mancanti** su 24: in quelle ore il libro non ha ne' letto ne' eseguito. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-06-24.md b/docs/journal/2026-06-24.md index cbfab3b..f182d50 100644 --- a/docs/journal/2026-06-24.md +++ b/docs/journal/2026-06-24.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-06-24 -*Scritto 2026-08-23T13:08:26+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:10+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,18 @@ Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 45.1 contro realizzata 30g 43.3 (sopra); DVOL al 51° pctl di un anno; IV-rank espandente 0.22 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 58.8 contro realizzata 30g 63.6 (sotto); DVOL al 28° pctl di un anno; IV-rank espandente 0.19 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **1 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-06-25.md b/docs/journal/2026-06-25.md index e8c0291..8d77882 100644 --- a/docs/journal/2026-06-25.md +++ b/docs/journal/2026-06-25.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-06-25 -*Scritto 2026-08-23T13:08:26+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:10+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,18 @@ Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 46.6 contro realizzata 30g 43.3 (sopra); DVOL al 57° pctl di un anno; IV-rank espandente 0.24 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 60.8 contro realizzata 30g 64.1 (sotto); DVOL al 33° pctl di un anno; IV-rank espandente 0.22 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **2 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-06-26.md b/docs/journal/2026-06-26.md index 5674638..a04a7ab 100644 --- a/docs/journal/2026-06-26.md +++ b/docs/journal/2026-06-26.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-06-26 -*Scritto 2026-08-23T13:08:26+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:10+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,18 @@ Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 45.5 contro realizzata 30g 43.4 (sopra); DVOL al 52° pctl di un anno; IV-rank espandente 0.23 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 59.8 contro realizzata 30g 64.1 (sotto); DVOL al 30° pctl di un anno; IV-rank espandente 0.20 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **3 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-06-27.md b/docs/journal/2026-06-27.md index 130e780..ab7f0f4 100644 --- a/docs/journal/2026-06-27.md +++ b/docs/journal/2026-06-27.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-06-27 -*Scritto 2026-08-23T13:08:26+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:10+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,18 @@ Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 45.2 contro realizzata 30g 43.4 (sopra); DVOL al 52° pctl di un anno; IV-rank espandente 0.22 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 59.9 contro realizzata 30g 64.2 (sotto); DVOL al 31° pctl di un anno; IV-rank espandente 0.20 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **4 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-06-28.md b/docs/journal/2026-06-28.md index 0ff48a8..41d1b8d 100644 --- a/docs/journal/2026-06-28.md +++ b/docs/journal/2026-06-28.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-06-28 -*Scritto 2026-08-23T13:08:26+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:10+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,18 @@ Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 47.7 contro realizzata 30g 43.4 (sopra); DVOL al 63° pctl di un anno; IV-rank espandente 0.27 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 61.3 contro realizzata 30g 64.1 (sotto); DVOL al 37° pctl di un anno; IV-rank espandente 0.23 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **5 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-06-29.md b/docs/journal/2026-06-29.md index 253d158..ddecd8c 100644 --- a/docs/journal/2026-06-29.md +++ b/docs/journal/2026-06-29.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-06-29 -*Scritto 2026-08-23T13:08:26+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:10+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,18 @@ Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 42.6 contro realizzata 30g 43.6 (sotto); DVOL al 33° pctl di un anno; IV-rank espandente 0.16 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 57.9 contro realizzata 30g 65.1 (sotto); DVOL al 26° pctl di un anno; IV-rank espandente 0.18 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **6 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-06-30.md b/docs/journal/2026-06-30.md index 8501691..0909516 100644 --- a/docs/journal/2026-06-30.md +++ b/docs/journal/2026-06-30.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-06-30 -*Scritto 2026-08-23T13:08:26+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:10+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,18 @@ Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 44.5 contro realizzata 30g 44.2 (sopra); DVOL al 46° pctl di un anno; IV-rank espandente 0.20 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 59.0 contro realizzata 30g 65.4 (sotto); DVOL al 29° pctl di un anno; IV-rank espandente 0.19 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **7 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-01.md b/docs/journal/2026-07-01.md index 7c6fb84..fa361de 100644 --- a/docs/journal/2026-07-01.md +++ b/docs/journal/2026-07-01.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-01 -*Scritto 2026-08-23T13:08:26+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,18 @@ Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 42.9 contro realizzata 30g 44.6 (sotto); DVOL al 36° pctl di un anno; IV-rank espandente 0.17 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 56.9 contro realizzata 30g 66.3 (sotto); DVOL al 23° pctl di un anno; IV-rank espandente 0.16 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **8 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-02.md b/docs/journal/2026-07-02.md index f1ff46c..8ef19ba 100644 --- a/docs/journal/2026-07-02.md +++ b/docs/journal/2026-07-02.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-02 -*Scritto 2026-08-23T13:08:26+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,18 @@ Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 40.5 contro realizzata 30g 40.0 (sopra); DVOL al 21° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 55.1 contro realizzata 30g 64.9 (sotto); DVOL al 16° pctl di un anno; IV-rank espandente 0.14 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **9 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-03.md b/docs/journal/2026-07-03.md index 64e7969..bc4051d 100644 --- a/docs/journal/2026-07-03.md +++ b/docs/journal/2026-07-03.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-03 -*Scritto 2026-08-23T13:08:27+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,18 @@ Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 39.5 contro realizzata 30g 38.1 (sopra); DVOL al 14° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 53.5 contro realizzata 30g 65.6 (sotto); DVOL al 8° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **10 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-04.md b/docs/journal/2026-07-04.md index 037dcca..9137f61 100644 --- a/docs/journal/2026-07-04.md +++ b/docs/journal/2026-07-04.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-04 -*Scritto 2026-08-23T13:08:27+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,18 @@ Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 39.2 contro realizzata 30g 38.2 (sopra); DVOL al 13° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 53.0 contro realizzata 30g 65.3 (sotto); DVOL al 6° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **11 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-05.md b/docs/journal/2026-07-05.md index 4735679..2c93000 100644 --- a/docs/journal/2026-07-05.md +++ b/docs/journal/2026-07-05.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-05 -*Scritto 2026-08-23T13:08:27+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,18 @@ Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 38.7 contro realizzata 30g 34.9 (sopra); DVOL al 10° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 53.1 contro realizzata 30g 51.3 (sopra); DVOL al 6° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **12 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-06.md b/docs/journal/2026-07-06.md index 542823e..ee25ee5 100644 --- a/docs/journal/2026-07-06.md +++ b/docs/journal/2026-07-06.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-06 -*Scritto 2026-08-23T13:08:27+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,18 @@ Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 37.3 contro realizzata 30g 34.8 (sopra); DVOL al 4° pctl di un anno; IV-rank espandente 0.05 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 52.6 contro realizzata 30g 51.1 (sopra); DVOL al 6° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **13 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-07.md b/docs/journal/2026-07-07.md index 79f6142..30cfe12 100644 --- a/docs/journal/2026-07-07.md +++ b/docs/journal/2026-07-07.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-07 -*Scritto 2026-08-23T13:08:27+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,18 @@ Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 38.9 contro realizzata 30g 32.3 (sopra); DVOL al 12° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 53.3 contro realizzata 30g 45.0 (sopra); DVOL al 7° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **14 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-08.md b/docs/journal/2026-07-08.md index 77d2732..4ee878a 100644 --- a/docs/journal/2026-07-08.md +++ b/docs/journal/2026-07-08.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-08 -*Scritto 2026-08-23T13:08:27+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,18 @@ Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 39.5 contro realizzata 30g 32.8 (sopra); DVOL al 16° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 53.4 contro realizzata 30g 45.4 (sopra); DVOL al 8° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **15 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-09.md b/docs/journal/2026-07-09.md index d0a6645..d3aba1e 100644 --- a/docs/journal/2026-07-09.md +++ b/docs/journal/2026-07-09.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-09 -*Scritto 2026-08-23T13:08:27+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,18 @@ Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 37.3 contro realizzata 30g 32.3 (sopra); DVOL al 5° pctl di un anno; IV-rank espandente 0.05 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 52.0 contro realizzata 30g 43.9 (sopra); DVOL al 5° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **16 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-10.md b/docs/journal/2026-07-10.md index e3b9179..0dd9c19 100644 --- a/docs/journal/2026-07-10.md +++ b/docs/journal/2026-07-10.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-10 -*Scritto 2026-08-23T13:08:27+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,18 @@ Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 36.2 contro realizzata 30g 32.6 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.8 contro realizzata 30g 44.7 (sopra); DVOL al 4° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **17 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-11.md b/docs/journal/2026-07-11.md index 0bb68b2..2eb29ff 100644 --- a/docs/journal/2026-07-11.md +++ b/docs/journal/2026-07-11.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-11 -*Scritto 2026-08-23T13:08:27+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,18 @@ Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 36.8 contro realizzata 30g 30.5 (sopra); DVOL al 4° pctl di un anno; IV-rank espandente 0.04 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 50.6 contro realizzata 30g 43.7 (sopra); DVOL al 4° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **18 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-12.md b/docs/journal/2026-07-12.md index d7fe746..f1241d9 100644 --- a/docs/journal/2026-07-12.md +++ b/docs/journal/2026-07-12.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-12 -*Scritto 2026-08-23T13:08:28+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,18 @@ Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 37.2 contro realizzata 30g 30.5 (sopra); DVOL al 5° pctl di un anno; IV-rank espandente 0.05 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 51.0 contro realizzata 30g 43.7 (sopra); DVOL al 5° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **19 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-13.md b/docs/journal/2026-07-13.md index 9f8759a..bfc5538 100644 --- a/docs/journal/2026-07-13.md +++ b/docs/journal/2026-07-13.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-13 -*Scritto 2026-08-23T13:08:28+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,18 @@ Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 37.8 contro realizzata 30g 31.2 (sopra); DVOL al 8° pctl di un anno; IV-rank espandente 0.06 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 51.1 contro realizzata 30g 44.2 (sopra); DVOL al 6° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **20 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-14.md b/docs/journal/2026-07-14.md index 4cd93d4..18f5ca1 100644 --- a/docs/journal/2026-07-14.md +++ b/docs/journal/2026-07-14.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-14 -*Scritto 2026-08-23T13:08:28+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$598.49** · nozionale lordo $75 · leva lorda 0.13x · barra dati 2026 - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` A mercato su ETH $+75 (TP01 +0.000, SKH01 long). +- `[leva]` Leva lorda **0.13x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.87x. +- `[pnl]` Equity $+0.43 con 1 fill e 0 round-trip chiusi (realizzato $+0.00 netto). +- `[vol]` BTC: implicita 36.5 contro realizzata 30g 34.1 (sopra); DVOL al 3° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.5 contro realizzata 30g 48.3 (sopra); DVOL al 3° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evento]` 📌 BTC: giornata a +4.41%, **2.5 deviazioni** giornaliere (sd implicita dalla RV30 = 1.78%). +- `[evento]` 📌 ETH: giornata a +6.42%, **2.5 deviazioni** giornaliere (sd implicita dalla RV30 = 2.53%). +- `[evidenza]` 📌 Campione a oggi: **21 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-15.md b/docs/journal/2026-07-15.md index f126816..fae8da4 100644 --- a/docs/journal/2026-07-15.md +++ b/docs/journal/2026-07-15.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-15 -*Scritto 2026-08-23T13:08:28+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,17 @@ Equity **$600.14** · nozionale lordo $77 · leva lorda 0.13x · barra dati 2026 - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` A mercato su ETH $+77 (TP01 +0.000, SKH01 long). +- `[leva]` Leva lorda **0.13x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.87x. +- `[pnl]` Equity $+1.65 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 36.0 contro realizzata 30g 34.0 (sopra); DVOL al 1° pctl di un anno; IV-rank espandente 0.02 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.0 contro realizzata 30g 46.6 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **22 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-16.md b/docs/journal/2026-07-16.md index 1aedb1d..9aabae7 100644 --- a/docs/journal/2026-07-16.md +++ b/docs/journal/2026-07-16.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-16 -*Scritto 2026-08-23T13:08:28+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,17 @@ Equity **$597.93** · nozionale lordo $75 · leva lorda 0.13x · barra dati 2026 - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` A mercato su ETH $+75 (TP01 +0.000, SKH01 long). +- `[leva]` Leva lorda **0.13x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.87x. +- `[pnl]` Equity $-2.21 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 36.4 contro realizzata 30g 34.1 (sopra); DVOL al 3° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.2 contro realizzata 30g 47.7 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **23 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-17.md b/docs/journal/2026-07-17.md index f10bebd..948b216 100644 --- a/docs/journal/2026-07-17.md +++ b/docs/journal/2026-07-17.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-17 -*Scritto 2026-08-23T13:08:28+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,18 @@ Equity **$596.77** · nozionale lordo $74 · leva lorda 0.12x · barra dati 2026 - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` A mercato su ETH $+74 (TP01 +0.000, SKH01 long). +- `[leva]` Leva lorda **0.12x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.88x. +- `[pnl]` Equity $-1.16 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.63%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 36.3 contro realizzata 30g 33.6 (sopra); DVOL al 3° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.0 contro realizzata 30g 47.1 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **24 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-18.md b/docs/journal/2026-07-18.md index 17c9811..2cfb7bd 100644 --- a/docs/journal/2026-07-18.md +++ b/docs/journal/2026-07-18.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-18 -*Scritto 2026-08-23T13:08:28+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.15 con 1 fill e 1 round-trip chiusi (realizzato $-1.10 netto). +- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 35.7 contro realizzata 30g 32.8 (sopra); DVOL al 1° pctl di un anno; IV-rank espandente 0.02 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.3 contro realizzata 30g 46.4 (sopra); DVOL al 4° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **25 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-19.md b/docs/journal/2026-07-19.md index 45a73b8..773bbf8 100644 --- a/docs/journal/2026-07-19.md +++ b/docs/journal/2026-07-19.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-19 -*Scritto 2026-08-23T13:08:28+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 36.1 contro realizzata 30g 32.7 (sopra); DVOL al 3° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.7 contro realizzata 30g 46.4 (sopra); DVOL al 6° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **26 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-20.md b/docs/journal/2026-07-20.md index 8427558..e02747b 100644 --- a/docs/journal/2026-07-20.md +++ b/docs/journal/2026-07-20.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-20 -*Scritto 2026-08-23T13:08:28+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 36.3 contro realizzata 30g 32.5 (sopra); DVOL al 4° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 50.4 contro realizzata 30g 46.4 (sopra); DVOL al 7° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **27 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-21.md b/docs/journal/2026-07-21.md index 9259d5b..ce7c316 100644 --- a/docs/journal/2026-07-21.md +++ b/docs/journal/2026-07-21.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-21 -*Scritto 2026-08-23T13:08:29+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 37.3 contro realizzata 30g 32.7 (sopra); DVOL al 10° pctl di un anno; IV-rank espandente 0.06 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 51.0 contro realizzata 30g 45.8 (sopra); DVOL al 8° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **28 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-22.md b/docs/journal/2026-07-22.md index cc99bcf..ef2bf00 100644 --- a/docs/journal/2026-07-22.md +++ b/docs/journal/2026-07-22.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-22 -*Scritto 2026-08-23T13:08:29+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 38.0 contro realizzata 30g 32.6 (sopra); DVOL al 12° pctl di un anno; IV-rank espandente 0.07 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 51.6 contro realizzata 30g 45.7 (sopra); DVOL al 10° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **29 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-23.md b/docs/journal/2026-07-23.md index 80ac49d..29fff8f 100644 --- a/docs/journal/2026-07-23.md +++ b/docs/journal/2026-07-23.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-23 -*Scritto 2026-08-23T13:08:29+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 38.5 contro realizzata 30g 32.3 (sopra); DVOL al 14° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 51.6 contro realizzata 30g 45.1 (sopra); DVOL al 10° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **30 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-24.md b/docs/journal/2026-07-24.md index 83c8947..f34466b 100644 --- a/docs/journal/2026-07-24.md +++ b/docs/journal/2026-07-24.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-24 -*Scritto 2026-08-23T13:08:29+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: non misurata +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 37.0 contro realizzata 30g 31.1 (sopra); DVOL al 7° pctl di un anno; IV-rank espandente 0.05 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 50.5 contro realizzata 30g 43.9 (sopra); DVOL al 7° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **31 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-25.md b/docs/journal/2026-07-25.md index 397f2aa..b396e2b 100644 --- a/docs/journal/2026-07-25.md +++ b/docs/journal/2026-07-25.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-25 -*Scritto 2026-08-23T13:08:29+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 37.7 contro realizzata 30g 30.0 (sopra); DVOL al 11° pctl di un anno; IV-rank espandente 0.06 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 51.5 contro realizzata 30g 41.5 (sopra); DVOL al 10° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **32 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-26.md b/docs/journal/2026-07-26.md index 9681441..40a4833 100644 --- a/docs/journal/2026-07-26.md +++ b/docs/journal/2026-07-26.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-26 -*Scritto 2026-08-23T13:08:29+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 37.3 contro realizzata 30g 30.4 (sopra); DVOL al 10° pctl di un anno; IV-rank espandente 0.06 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 51.7 contro realizzata 30g 43.4 (sopra); DVOL al 11° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **33 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-27.md b/docs/journal/2026-07-27.md index 68bd946..4913476 100644 --- a/docs/journal/2026-07-27.md +++ b/docs/journal/2026-07-27.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-27 -*Scritto 2026-08-23T13:08:29+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 37.5 contro realizzata 30g 31.9 (sopra); DVOL al 11° pctl di un anno; IV-rank espandente 0.06 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 51.9 contro realizzata 30g 45.4 (sopra); DVOL al 12° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **34 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-28.md b/docs/journal/2026-07-28.md index 933c0a8..20f040e 100644 --- a/docs/journal/2026-07-28.md +++ b/docs/journal/2026-07-28.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-28 -*Scritto 2026-08-23T13:08:29+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -28,6 +28,19 @@ Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 0.50x (`max_notional_per_asset_frac` 0.50 x 1 asset, letto da `config/live.json`): margine 0.50x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 37.2 contro realizzata 30g 31.7 (sopra); DVOL al 8° pctl di un anno; IV-rank espandente 0.05 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 53.0 contro realizzata 30g 45.5 (sopra); DVOL al 14° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **35 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-29.md b/docs/journal/2026-07-29.md index d11e705..91b6599 100644 --- a/docs/journal/2026-07-29.md +++ b/docs/journal/2026-07-29.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-29 -*Scritto 2026-08-23T13:08:29+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 37.5 contro realizzata 30g 31.6 (sopra); DVOL al 11° pctl di un anno; IV-rank espandente 0.06 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 52.5 contro realizzata 30g 45.1 (sopra); DVOL al 13° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **36 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-30.md b/docs/journal/2026-07-30.md index 2397d44..bf23ec3 100644 --- a/docs/journal/2026-07-30.md +++ b/docs/journal/2026-07-30.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-30 -*Scritto 2026-08-23T13:08:30+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 35.5 contro realizzata 30g 30.0 (sopra); DVOL al 1° pctl di un anno; IV-rank espandente 0.02 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 50.7 contro realizzata 30g 43.7 (sopra); DVOL al 8° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **37 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-07-31.md b/docs/journal/2026-07-31.md index f237c10..39d17d6 100644 --- a/docs/journal/2026-07-31.md +++ b/docs/journal/2026-07-31.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-07-31 -*Scritto 2026-08-23T13:08:30+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 35.6 contro realizzata 30g 31.2 (sopra); DVOL al 1° pctl di un anno; IV-rank espandente 0.02 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 50.2 contro realizzata 30g 45.0 (sopra); DVOL al 6° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **38 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-01.md b/docs/journal/2026-08-01.md index 03712d1..b14d70a 100644 --- a/docs/journal/2026-08-01.md +++ b/docs/journal/2026-08-01.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-01 -*Scritto 2026-08-23T13:08:30+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 35.5 contro realizzata 30g 29.9 (sopra); DVOL al 1° pctl di un anno; IV-rank espandente 0.02 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 50.8 contro realizzata 30g 41.5 (sopra); DVOL al 9° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **39 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-02.md b/docs/journal/2026-08-02.md index 32b069d..4ac6421 100644 --- a/docs/journal/2026-08-02.md +++ b/docs/journal/2026-08-02.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-02 -*Scritto 2026-08-23T13:08:30+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 35.0 contro realizzata 30g 29.7 (sopra); DVOL al 0° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.7 contro realizzata 30g 40.5 (sopra); DVOL al 6° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **40 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-03.md b/docs/journal/2026-08-03.md index 15d572a..ebdac6a 100644 --- a/docs/journal/2026-08-03.md +++ b/docs/journal/2026-08-03.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-03 -*Scritto 2026-08-23T13:08:30+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 34.8 contro realizzata 30g 29.5 (sopra); DVOL al 0° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.2 contro realizzata 30g 40.7 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **41 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-04.md b/docs/journal/2026-08-04.md index 1d63f1a..5da9ac9 100644 --- a/docs/journal/2026-08-04.md +++ b/docs/journal/2026-08-04.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-04 -*Scritto 2026-08-23T13:08:30+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$597.37** · nozionale lordo $37 · leva lorda 0.06x · barra dati 2026 - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` A mercato su ETH $+37 (TP01 +0.162, SKH01 flat). +- `[disaccordo]` ETH: gli orizzonti del trend **non concordano** (2/3 al rialzo) e l'esposizione TP01 e' parziale (+0.162) — e' il meccanismo, non una scelta di oggi. +- `[leva]` Leva lorda **0.06x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.94x. +- `[pnl]` Equity $+0.45 con 1 fill e 0 round-trip chiusi (realizzato $+0.00 netto). +- `[drawdown]` 📌 Equity **-0.53%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 34.0 contro realizzata 30g 29.5 (sopra); DVOL al 0° pctl di un anno; IV-rank espandente 0.00 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 47.8 contro realizzata 30g 40.7 (sopra); DVOL al 0° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **42 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-05.md b/docs/journal/2026-08-05.md index 9c2c211..e315f35 100644 --- a/docs/journal/2026-08-05.md +++ b/docs/journal/2026-08-05.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-05 -*Scritto 2026-08-23T13:08:30+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $-0.25 con 1 fill e 1 round-trip chiusi (realizzato $+0.21 netto). +- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 34.7 contro realizzata 30g 29.6 (sopra); DVOL al 1° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 48.0 contro realizzata 30g 41.2 (sopra); DVOL al 1° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **43 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-06.md b/docs/journal/2026-08-06.md index a53fe17..2a189bb 100644 --- a/docs/journal/2026-08-06.md +++ b/docs/journal/2026-08-06.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-06 -*Scritto 2026-08-23T13:08:30+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 35.0 contro realizzata 30g 29.4 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 48.0 contro realizzata 30g 40.7 (sopra); DVOL al 1° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **44 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-07.md b/docs/journal/2026-08-07.md index db0dad7..3ca0c77 100644 --- a/docs/journal/2026-08-07.md +++ b/docs/journal/2026-08-07.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-07 -*Scritto 2026-08-23T13:08:30+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 33.9 contro realizzata 30g 28.9 (sopra); DVOL al 0° pctl di un anno; IV-rank espandente 0.00 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 47.4 contro realizzata 30g 40.2 (sopra); DVOL al 0° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **45 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-08.md b/docs/journal/2026-08-08.md index b366345..461e859 100644 --- a/docs/journal/2026-08-08.md +++ b/docs/journal/2026-08-08.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-08 -*Scritto 2026-08-23T13:08:30+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 34.2 contro realizzata 30g 28.4 (sopra); DVOL al 1° pctl di un anno; IV-rank espandente 0.00 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 48.1 contro realizzata 30g 40.2 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **46 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-09.md b/docs/journal/2026-08-09.md index cae9ef9..5b0d107 100644 --- a/docs/journal/2026-08-09.md +++ b/docs/journal/2026-08-09.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-09 -*Scritto 2026-08-23T13:08:31+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 34.9 contro realizzata 30g 28.0 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.1 contro realizzata 30g 39.1 (sopra); DVOL al 4° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **47 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-10.md b/docs/journal/2026-08-10.md index 68d62c3..e2e5bc3 100644 --- a/docs/journal/2026-08-10.md +++ b/docs/journal/2026-08-10.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-10 -*Scritto 2026-08-23T13:08:31+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 36.3 contro realizzata 30g 28.4 (sopra); DVOL al 8° pctl di un anno; IV-rank espandente 0.04 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.7 contro realizzata 30g 39.8 (sopra); DVOL al 8° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **48 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-11.md b/docs/journal/2026-08-11.md index a8ab00a..8b59d1f 100644 --- a/docs/journal/2026-08-11.md +++ b/docs/journal/2026-08-11.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-11 -*Scritto 2026-08-23T13:08:31+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 36.1 contro realizzata 30g 28.5 (sopra); DVOL al 8° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.3 contro realizzata 30g 39.7 (sopra); DVOL al 7° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **49 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-12.md b/docs/journal/2026-08-12.md index 057803a..f856002 100644 --- a/docs/journal/2026-08-12.md +++ b/docs/journal/2026-08-12.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-12 -*Scritto 2026-08-23T13:08:31+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 35.8 contro realizzata 30g 27.2 (sopra); DVOL al 6° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.4 contro realizzata 30g 39.2 (sopra); DVOL al 8° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **50 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-13.md b/docs/journal/2026-08-13.md index 8ddc563..cd86f50 100644 --- a/docs/journal/2026-08-13.md +++ b/docs/journal/2026-08-13.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-13 -*Scritto 2026-08-23T13:08:31+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 34.8 contro realizzata 30g 22.5 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 48.5 contro realizzata 30g 32.6 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **51 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-14.md b/docs/journal/2026-08-14.md index 1a70481..c26845a 100644 --- a/docs/journal/2026-08-14.md +++ b/docs/journal/2026-08-14.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-14 -*Scritto 2026-08-23T13:08:31+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 34.7 contro realizzata 30g 22.6 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 47.2 contro realizzata 30g 32.2 (sopra); DVOL al 0° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **52 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-15.md b/docs/journal/2026-08-15.md index 8a5c064..764b954 100644 --- a/docs/journal/2026-08-15.md +++ b/docs/journal/2026-08-15.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-15 -*Scritto 2026-08-23T13:08:31+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 35.0 contro realizzata 30g 22.0 (sopra); DVOL al 4° pctl di un anno; IV-rank espandente 0.02 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 47.5 contro realizzata 30g 30.7 (sopra); DVOL al 1° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **53 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-16.md b/docs/journal/2026-08-16.md index a17290e..6368abf 100644 --- a/docs/journal/2026-08-16.md +++ b/docs/journal/2026-08-16.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-16 -*Scritto 2026-08-23T13:08:31+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:16+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 35.2 contro realizzata 30g 22.0 (sopra); DVOL al 5° pctl di un anno; IV-rank espandente 0.02 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 47.5 contro realizzata 30g 30.4 (sopra); DVOL al 1° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **54 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-17.md b/docs/journal/2026-08-17.md index 4df3c2b..51d5e41 100644 --- a/docs/journal/2026-08-17.md +++ b/docs/journal/2026-08-17.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-17 -*Scritto 2026-08-23T13:08:31+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:16+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,20 @@ Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 34.7 contro realizzata 30g 23.4 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 46.3 contro realizzata 30g 30.9 (sopra); DVOL al 0° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evento]` 📌 BTC: giornata a +2.62%, **2.1 deviazioni** giornaliere (sd implicita dalla RV30 = 1.22%). +- `[evidenza]` 📌 Campione a oggi: **55 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-18.md b/docs/journal/2026-08-18.md index ea5f527..2fbc4b1 100644 --- a/docs/journal/2026-08-18.md +++ b/docs/journal/2026-08-18.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-18 -*Scritto 2026-08-23T13:08:31+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:16+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026- - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 34.9 contro realizzata 30g 23.4 (sopra); DVOL al 4° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 46.1 contro realizzata 30g 30.9 (sopra); DVOL al 0° pctl di un anno; IV-rank espandente 0.07 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **56 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-19.md b/docs/journal/2026-08-19.md index d150bd0..5497dcc 100644 --- a/docs/journal/2026-08-19.md +++ b/docs/journal/2026-08-19.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-19 -*Scritto 2026-08-23T13:08:32+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:16+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,21 @@ Equity **$610.82** · nozionale lordo $76 · leva lorda 0.12x · barra dati 2026 - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` A mercato su BTC $+76 (TP01 +0.000, SKH01 long). +- `[incoerenza]` ⚠️ ETH: tutti e 3 gli orizzonti sono al rialzo ma TP01 e' a zero. Da guardare: non e' lo stato atteso. +- `[leva]` Leva lorda **0.12x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.88x. +- `[pnl]` Equity $+13.70 con 3 fill e 1 round-trip chiusi (realizzato $+10.60 netto). +- `[concentrazione]` 📌 **Il 107% di tutto il P&L cumulato ($+12.76) viene dagli ultimi 7 giorni.** Oltre il 100% perche' **il periodo precedente era in perdita**: senza questi 7 giorni il libro sarebbe sotto. Un risultato concentrato in una finestra non e' un tasso di rendimento: e' un evento. +- `[vol]` BTC: implicita 37.8 contro realizzata 30g 33.6 (sopra); DVOL al 21° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 52.5 contro realizzata 30g 63.8 (sotto); DVOL al 21° pctl di un anno; IV-rank espandente 0.13 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evento]` 📌 BTC: giornata a +7.16%, **4.1 deviazioni** giornaliere (sd implicita dalla RV30 = 1.76%). +- `[evento]` 📌 ETH: giornata a +17.48%, **5.2 deviazioni** giornaliere (sd implicita dalla RV30 = 3.34%). +- `[evidenza]` 📌 Campione a oggi: **57 giorni, 3 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-20.md b/docs/journal/2026-08-20.md index 6d654ed..6188ea7 100644 --- a/docs/journal/2026-08-20.md +++ b/docs/journal/2026-08-20.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-20 -*Scritto 2026-08-23T13:08:32+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:16+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,20 @@ Equity **$620.63** · nozionale lordo $272 · leva lorda 0.44x · barra dati 202 - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` A mercato su BTC $+124 (TP01 +0.199, SKH01 long) e ETH $+148 (TP01 +0.294, SKH01 long). +- `[disaccordo]` BTC: gli orizzonti del trend **non concordano** (2/3 al rialzo) e l'esposizione TP01 e' parziale (+0.199) — e' il meccanismo, non una scelta di oggi. +- `[leva]` Leva lorda **0.44x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.56x. +- `[pnl]` Equity $+9.81 con 4 fill e 1 round-trip chiusi (realizzato $+0.51 netto). +- `[concentrazione]` 📌 **Il 104% di tutto il P&L cumulato ($+22.57) viene dagli ultimi 7 giorni.** Oltre il 100% perche' **il periodo precedente era in perdita**: senza questi 7 giorni il libro sarebbe sotto. Un risultato concentrato in una finestra non e' un tasso di rendimento: e' un evento. +- `[vol]` BTC: implicita 39.4 contro realizzata 30g 37.3 (sopra); DVOL al 33° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 52.6 contro realizzata 30g 64.4 (sotto); DVOL al 22° pctl di un anno; IV-rank espandente 0.13 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evento]` 📌 BTC: giornata a +5.31%, **2.7 deviazioni** giornaliere (sd implicita dalla RV30 = 1.95%). +- `[evidenza]` 📌 Campione a oggi: **58 giorni, 4 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-21.md b/docs/journal/2026-08-21.md index 12afae5..f9e7329 100644 --- a/docs/journal/2026-08-21.md +++ b/docs/journal/2026-08-21.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-21 -*Scritto 2026-08-23T13:08:32+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:16+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,20 @@ Equity **$644.00** · nozionale lordo $196 · leva lorda 0.30x · barra dati 202 - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` A mercato su BTC $+126 (TP01 +0.176, SKH01 long) e ETH $+70 (TP01 +0.291, SKH01 flat). +- `[leva]` Leva lorda **0.30x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.70x. +- `[pnl]` Equity $+23.37 con 5 fill e 6 round-trip chiusi (realizzato $+18.90 netto). +- `[concentrazione]` 📌 **Il 102% di tutto il P&L cumulato ($+45.94) viene dagli ultimi 7 giorni.** Oltre il 100% perche' **il periodo precedente era in perdita**: senza questi 7 giorni il libro sarebbe sotto. Un risultato concentrato in una finestra non e' un tasso di rendimento: e' un evento. +- `[vol]` BTC: implicita 43.3 contro realizzata 30g 43.9 (sotto); DVOL al 57° pctl di un anno; IV-rank espandente 0.20 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 57.7 contro realizzata 30g 69.2 (sotto); DVOL al 46° pctl di un anno; IV-rank espandente 0.20 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evento]` 📌 BTC: giornata a +7.31%, **3.2 deviazioni** giornaliere (sd implicita dalla RV30 = 2.30%). +- `[evento]` 📌 ETH: giornata a +8.19%, **2.3 deviazioni** giornaliere (sd implicita dalla RV30 = 3.62%). +- `[evidenza]` 📌 Campione a oggi: **59 giorni, 10 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-22.md b/docs/journal/2026-08-22.md index c2dc0d2..47fa90a 100644 --- a/docs/journal/2026-08-22.md +++ b/docs/journal/2026-08-22.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-22 -*Scritto 2026-08-23T13:08:32+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:16+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,19 @@ Equity **$635.00** · nozionale lordo $327 · leva lorda 0.51x · barra dati 202 - giri di `book_execute`: 24/24 - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` A mercato su BTC $+185 (TP01 +0.450, SKH01 long) e ETH $+142 (TP01 +0.277, SKH01 long). +- `[leva]` Leva lorda **0.51x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.49x. +- `[pnl]` Equity $-9.00 con 2 fill e 0 round-trip chiusi (realizzato $+0.00 netto). +- `[concentrazione]` 📌 **Il 103% di tutto il P&L cumulato ($+36.94) viene dagli ultimi 7 giorni.** Oltre il 100% perche' **il periodo precedente era in perdita**: senza questi 7 giorni il libro sarebbe sotto. Un risultato concentrato in una finestra non e' un tasso di rendimento: e' un evento. +- `[drawdown]` 📌 Equity **-1.55%** sotto il picco ($644.98). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 42.3 contro realizzata 30g 43.9 (sotto); DVOL al 51° pctl di un anno; IV-rank espandente 0.17 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 57.0 contro realizzata 30g 69.8 (sotto); DVOL al 44° pctl di un anno; IV-rank espandente 0.18 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **60 giorni, 10 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/docs/journal/2026-08-23.md b/docs/journal/2026-08-23.md index 9b77aae..e4d6906 100644 --- a/docs/journal/2026-08-23.md +++ b/docs/journal/2026-08-23.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-23 -*Scritto 2026-08-23T13:08:32+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T13:34:16+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -29,6 +29,20 @@ Equity **$636.14** · nozionale lordo $258 · leva lorda 0.41x · barra dati 202 - giri di `book_execute`: 14/14 *(giorno in corso)* - eta' feed SKH all'ultimo giro: 0 min +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` A mercato su BTC $+116 (TP01 +0.150, SKH01 long) e ETH $+142 (TP01 +0.271, SKH01 long). +- `[disaccordo]` BTC: gli orizzonti del trend **non concordano** (2/3 al rialzo) e l'esposizione TP01 e' parziale (+0.150) — e' il meccanismo, non una scelta di oggi. +- `[leva]` Leva lorda **0.41x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.59x. +- `[pnl]` Equity $+1.14 con 1 fill e 2 round-trip chiusi (realizzato $+5.55 netto). +- `[concentrazione]` 📌 **Il 102% di tutto il P&L cumulato ($+38.08) viene dagli ultimi 7 giorni.** Oltre il 100% perche' **il periodo precedente era in perdita**: senza questi 7 giorni il libro sarebbe sotto. Un risultato concentrato in una finestra non e' un tasso di rendimento: e' un evento. +- `[drawdown]` 📌 Equity **-1.37%** sotto il picco ($644.98). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 42.4 contro realizzata 30g 43.3 (sotto); DVOL al 52° pctl di un anno; IV-rank espandente 0.18 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 57.0 contro realizzata 30g 69.6 (sotto); DVOL al 44° pctl di un anno; IV-rank espandente 0.18 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **61 giorni, 12 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + ## Nota *(vuota — campo dell'operatore)* diff --git a/src/live/journal.py b/src/live/journal.py index d1da051..19ff699 100644 --- a/src/live/journal.py +++ b/src/live/journal.py @@ -78,6 +78,11 @@ def metriche_mercato(giorno: date) -> dict: d["dvol_eta_g"] = eta if len(dv) >= 252 and eta <= 2: d["dvol_rank_1a"] = float((dv.tail(252) < dv.iloc[-1]).mean()) + if len(dv) >= 60 and eta <= 2: + # IV-rank come lo calcola VRP01 (`options_vrp_v2._ivrank`): percentile + # ESPANDENTE su tutta la storia, non una finestra di un anno. Sono due + # statistiche diverse e vanno chiamate con nomi diversi. + d["dvol_rank_exp"] = float((dv.iloc[:-1] < dv.iloc[-1]).mean()) else: d["dvol"] = None; d["dvol_nota"] = "nessuna barra DVOL <= giorno" else: @@ -162,6 +167,196 @@ def salva(con, voce: dict, nota: str | None = None) -> None: con.commit() + +# ============================================================================================= +# LETTURA RAGIONATA — a regole DICHIARATE, non a prosa libera +# ============================================================================================= +# Ogni riga della lettura esce da una regola con un id, e ogni regola puo' solo COMBINARE numeri +# gia' presenti nella pagina. Non prevede, non consiglia, non interpreta oltre il dato. +# Le tre proprieta' che la rendono diversa da un racconto: +# (a) TRACCIABILE — l'id dice quale regola ha parlato; +# (b) FALSIFICABILE — ogni regola ha un test che la accende E uno che la tiene spenta; +# (c) MUTA sul non misurato — se il numero e' None la regola tace, non arrotonda a zero. +# Il campo `nota` resta l'unico posto dove puo' finire un giudizio umano. + +def _bucket_convinzione(tp: float) -> str: + """TP01 media tre `np.sign()` e poi taglia a long-flat: la direzione vive in {0, 1/3, 1}. + + (Il bucket 2/3 NON esiste — verificato al sorgente il 2026-08-23, §41.) Il `tp_frac` che + il libro stampa e' quella direzione MOLTIPLICATA per il vol-target, quindi non e' il bucket: + qui se ne riporta solo l'ordine di grandezza. + """ + if tp <= 0.0: + return "flat" + return "parziale" if tp < 0.60 else "piena" + + +def lettura(con, voce: dict) -> list[dict]: + """Ritorna [{id, testo, livello}] — livello in {info, nota, attenzione}.""" + m, lb, p, s = voce["mercato"], voce["libro"], voce["pnl"], voce["salute"] + out: list[dict] = [] + + def dire(rid, testo, livello="info"): + out.append(dict(id=rid, testo=testo, livello=livello)) + + # --- cosa il libro STA FACENDO ----------------------------------------------------------- + if "errore" in lb: + dire("stato", f"Nessun giro di `book_execute`: {lb['errore']}. " + "Non e' «il libro era fermo», e' «non e' stato misurato».", "attenzione") + return out + + att = {a: v for a, v in lb["asset"].items() if abs(v["posizione"]) >= 1.0} + if not att: + dire("stato", "Libro **flat** su entrambe le gambe: nessun capitale a mercato.") + motivi = [] + for a, v in sorted(lb["asset"].items()): + pezzi = [] + if v["tp_frac"] <= 0: + pezzi.append("TP01 a zero (trend giu' o misto)") + if v["skh_sign"] == 0: + pezzi.append("SKH01 senza breakout") + if pezzi: + motivi.append(f"{a}: " + " e ".join(pezzi)) + if motivi: + dire("perche_flat", "Motivo, componente per componente — " + "; ".join(motivi) + + ". Per una strategia long-flat stare fuori **e'** una decisione.") + else: + pezzi = [f"{a} ${v['posizione']:+,.0f} (TP01 {v['tp_frac']:+.3f}, " + f"SKH01 {'flat' if v['skh_sign'] == 0 else ('long' if v['skh_sign'] > 0 else 'short')})" + for a, v in sorted(att.items())] + dire("stato", "A mercato su " + " e ".join(pezzi) + ".") + + # --- il segnale contro il mercato -------------------------------------------------------- + for a, v in sorted(lb["asset"].items()): + d = m.get(a) or {} + su = d.get("tsmom_su") + if not su: + continue + n_su, n_tot = su + b = _bucket_convinzione(v["tp_frac"]) + if 0 < n_su < n_tot and v["tp_frac"] > 0: + dire("disaccordo", f"{a}: gli orizzonti del trend **non concordano** ({n_su}/{n_tot} " + f"al rialzo) e l'esposizione TP01 e' {b} ({v['tp_frac']:+.3f}) — " + "e' il meccanismo, non una scelta di oggi.") + elif n_su == n_tot and v["tp_frac"] <= 0: + dire("incoerenza", f"{a}: tutti e {n_tot} gli orizzonti sono al rialzo ma TP01 e' a " + "zero. Da guardare: non e' lo stato atteso.", "attenzione") + + # --- quanto capitale e' esposto ---------------------------------------------------------- + lv = lb.get("leva_lorda") + if lv is not None: + # Il tetto si LEGGE da config/live.json. Ridichiararlo qui e' lo schema che questo + # progetto ha gia' pagato quattro volte (fee_watch, test di guardia sul cap, taratura + # venue_watch, indice del namespace): un sorvegliante che ridichiara il proprio + # bersaglio continua a passare il giorno che il bersaglio cambia. + try: + cfg = json.loads((PROJECT_ROOT / "config" / "live.json").read_text()) + cap_frac = float(cfg["max_notional_per_asset_frac"]) + tetto = cap_frac * len(lb["asset"]) + dire("leva", f"Leva lorda **{lv:.2f}x** su un tetto di {tetto:.2f}x " + f"(`max_notional_per_asset_frac` {cap_frac:.2f} x {len(lb['asset'])} " + f"asset, letto da `config/live.json`): margine {tetto - lv:.2f}x.") + except Exception as e: + dire("leva", f"Leva lorda **{lv:.2f}x**; tetto non verificabile " + f"({type(e).__name__} su config/live.json).", "attenzione") + + # --- P&L del giorno, in proporzione ------------------------------------------------------ + dq, fee = p.get("delta_equity"), p.get("fee", 0.0) + if dq is not None: + if p["fill"] and fee > 0 and abs(dq) < fee: + dire("fee", f"Il movimento di equity del giorno (${dq:+.2f}) e' **piu' piccolo delle " + f"fee pagate** (${fee:.4f}): la giornata l'ha decisa il costo, non il segnale.", + "nota") + elif p["fill"] == 0: + dire("pnl", f"Equity ${dq:+.2f} **senza operare**: e' mark-to-market sulle posizioni " + "gia' aperte.") + else: + dire("pnl", f"Equity ${dq:+.2f} con {p['fill']} fill e " + f"{p['roundtrip_chiusi']} round-trip chiusi (realizzato " + f"${p['realizzato_netto']:+.2f} netto).") + + # --- il cumulato, e da dove viene -------------------------------------------------------- + eq = con.execute("SELECT ts_utc, equity FROM equity WHERE ts_utc <= ? ORDER BY ts_utc", + (voce["giorno"] + "T23:59:59+00:00",)).fetchall() + if len(eq) > 24: + e0, e1 = eq[0]["equity"], eq[-1]["equity"] + tot = e1 - e0 + picco = max(r["equity"] for r in eq) + g7 = [r for r in eq if r["ts_utc"][:10] >= (date.fromisoformat(voce["giorno"]) - + timedelta(days=7)).isoformat()] + # Soglia di rilevanza: su un cumulato di pochi dollari la "concentrazione" e' aritmetica + # vera e informazione zero. Una regola che si accende su $2 insegna a saltare la sezione. + rilevante = abs(tot) >= max(5.0, 0.01 * e1) + if rilevante and g7: + quota = (e1 - g7[0]["equity"]) / tot + if quota > 0.60: + coda = ("Un risultato concentrato in una finestra non e' un tasso di rendimento: " + "e' un evento.") + if quota > 1.0: + coda = ("Oltre il 100% perche' **il periodo precedente era in perdita**: " + "senza questi 7 giorni il libro sarebbe sotto. " + coda) + dire("concentrazione", f"**Il {100*quota:.0f}% di tutto il P&L cumulato " + f"(${tot:+,.2f}) viene dagli ultimi 7 giorni.** " + coda, "nota") + if picco > 0: + dd = (e1 - picco) / picco + if dd < -0.005: + dire("drawdown", f"Equity **{100*dd:.2f}%** sotto il picco (${picco:,.2f}). " + "⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.", "nota") + + # --- volatilita' implicita contro realizzata --------------------------------------------- + try: + from src.portfolio.sleeves import VRP_CFG + soglia_ivr = float(VRP_CFG["gate_ivr"]) # derivata dal sleeve, non riscritta + except Exception: + soglia_ivr = None + for a in ASSETS: + d = m.get(a) or {} + dv, rv = d.get("dvol"), d.get("rv30_annua") + if dv is None or rv is None: + continue + segno = "sopra" if dv > rv else "sotto" + riga = f"{a}: implicita {dv:.1f} contro realizzata 30g {rv:.1f} ({segno})" + if d.get("dvol_rank_1a") is not None: + riga += f"; DVOL al {100*d['dvol_rank_1a']:.0f}° pctl di un anno" + ivr = d.get("dvol_rank_exp") + if ivr is not None and soglia_ivr is not None: + riga += (f"; IV-rank espandente {ivr:.2f} " + + (f"**sopra** la soglia {soglia_ivr:.2f} del gate di VRP01 (a quel gate il " + "sleeve venderebbe — resta paper, regola «niente short-vol da modello»)" + if ivr > soglia_ivr else + f"sotto la soglia {soglia_ivr:.2f} del gate di VRP01 (sleeve fermo)")) + dire("vol", riga + ".") + + # --- giornata fuori scala ---------------------------------------------------------------- + for a in ASSETS: + d = m.get(a) or {} + r1, rv = d.get("ret_1g"), d.get("rv30_annua") + if r1 is None or not rv: + continue + sd_g = rv / math.sqrt(365) + if sd_g > 0 and abs(r1) > 2 * sd_g: + dire("evento", f"{a}: giornata a {r1:+.2f}%, **{abs(r1)/sd_g:.1f} deviazioni** " + f"giornaliere (sd implicita dalla RV30 = {sd_g:.2f}%).", "nota") + + # --- salute ------------------------------------------------------------------------------ + if s.get("giri_mancanti"): + dire("giri", f"**{s['giri_mancanti']} giri di `book_execute` mancanti** su " + f"{s['atteso_giri']}: in quelle ore il libro non ha ne' letto ne' eseguito.", + "attenzione") + if s.get("feed_skh_min") is not None and s["feed_skh_min"] > 30: + dire("feed", f"Feed SKH01 vecchio di {s['feed_skh_min']} min (soglia 30): la latenza " + "d'uscita di SKH01 e' quella, non ~1h.", "attenzione") + + # --- quanto vale questa pagina come evidenza --------------------------------------------- + n_rt = con.execute("SELECT COUNT(*) c FROM roundtrips WHERE ts_out <= ?", + (voce["giorno"] + "T23:59:59+00:00",)).fetchone()["c"] + if len(eq) > 24: + gg = (date.fromisoformat(eq[-1]["ts_utc"][:10]) - date.fromisoformat(eq[0]["ts_utc"][:10])).days + dire("evidenza", f"Campione a oggi: **{gg} giorni, {n_rt} round-trip**. A questa taglia " + "il P&L non distingue l'edge dalla fortuna — il criterio di morte del " + "libro (`edge_watch`) lavora su una finestra di 36 mesi.", "nota") + return out + def _n(v, fmt="{:+.2f}", vuoto="n/d"): return vuoto if v is None else fmt.format(v) @@ -222,7 +417,15 @@ def rendi_markdown(con, voce: dict) -> str: + (f" — **{s['giri_mancanti']} mancanti**" if s["giri_mancanti"] else ""), f"- eta' feed SKH all'ultimo giro: " + (f"{s['feed_skh_min']} min" if s["feed_skh_min"] is not None else "non misurata"), - "", "## Nota", "", (nota if nota.strip() else "*(vuota — campo dell'operatore)*"), ""] + ""] + righe = lettura(con, voce) + L += ["## Lettura", "", + "*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra " + "e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un " + "giudizio: quello sta nella Nota.*", ""] + icona = {"info": "", "nota": "📌 ", "attenzione": "⚠️ "} + L += [f"- `[{r['id']}]` {icona[r['livello']]}{r['testo']}" for r in righe] or ["- *(nessuna regola accesa)*"] + L += ["", "## Nota", "", (nota if nota.strip() else "*(vuota — campo dell'operatore)*"), ""] return "\n".join(L) diff --git a/tests/test_journal.py b/tests/test_journal.py index 8c2f14c..fdf2c99 100644 --- a/tests/test_journal.py +++ b/tests/test_journal.py @@ -136,3 +136,174 @@ def test_lo_stato_tsmom_usa_gli_orizzonti_di_produzione(): src = Path(tp.__file__).read_text() for n in J.ORIZZONTI: assert str(n) in src, f"orizzonte {n} non compare in trend_portfolio.py" + + +# ============================================================================================= +# LETTURA RAGIONATA — ogni regola ha un test che la ACCENDE e uno che la tiene SPENTA +# ============================================================================================= +# Una regola che si accende sempre non sta leggendo niente, e una che non si accende mai e' +# indistinguibile da una rotta. Servono entrambe le direzioni. + +def _voce(mercato=None, libro=None, pnl=None, salute=None, giorno="2026-08-21"): + return dict(giorno=giorno, ts_scritto=T.ora(), mercato=mercato or {}, + libro=libro or {"errore": "nessun giro", "giri": 0}, pnl=pnl or {}, + salute=salute or dict(giri_book=24, feed_skh_min=0, atteso_giri=24, + giorno_in_corso=False, giri_mancanti=0)) + + +def _libro(**asset): + a = {k: dict(dict(tp_frac=0.0, skh_sign=0, skh_entry=None, target=0.0, posizione=0.0, + azione="HOLD (a target)"), **v) for k, v in asset.items()} + lordo = sum(abs(v["posizione"]) for v in a.values()) + return dict(giri=24, ultimo_giro="2026-08-21T23:07:01+00:00", equity=600.0, barra="2026-08-21", + feed_skh_min=0, nozionale_lordo=lordo, leva_lorda=lordo / 600.0, asset=a) + + +def _pnl(**kw): + base = dict(equity_inizio=600.0, equity_fine=600.0, delta_equity=0.0, realizzato_lordo=0.0, + realizzato_netto=0.0, roundtrip_chiusi=0, fill=0, fee=0.0, + cumulato_da_arming=0.0, letture_equity=24) + return {**base, **kw} + + +def _ids(righe): + return [r["id"] for r in righe] + + +def test_libro_flat_dice_anche_PERCHE(db): + v = _voce(libro=_libro(BTC={}, ETH={}), pnl=_pnl()) + r = J.lettura(db, v) + assert "stato" in _ids(r) and "perche_flat" in _ids(r) + testo = next(x["testo"] for x in r if x["id"] == "perche_flat") + assert "TP01 a zero" in testo and "SKH01 senza breakout" in testo + + +def test_libro_a_mercato_non_spiega_un_flat_che_non_c_e(db): + v = _voce(libro=_libro(BTC=dict(tp_frac=0.15, skh_sign=1, posizione=116.0)), pnl=_pnl()) + r = J.lettura(db, v) + assert "perche_flat" not in _ids(r) + assert "$+116" in next(x["testo"] for x in r if x["id"] == "stato") + + +def test_disaccordo_si_accende_a_2_su_3_e_tace_a_3_su_3(db): + lb = _libro(BTC=dict(tp_frac=0.15, skh_sign=1, posizione=116.0)) + misto = {"BTC": dict(chiusura=77000.0, tsmom_su=(2, 3), tsmom={})} + pieno = {"BTC": dict(chiusura=77000.0, tsmom_su=(3, 3), tsmom={})} + assert "disaccordo" in _ids(J.lettura(db, _voce(mercato=misto, libro=lb, pnl=_pnl()))) + assert "disaccordo" not in _ids(J.lettura(db, _voce(mercato=pieno, libro=lb, pnl=_pnl()))) + + +def test_incoerenza_trend_su_ma_libro_fuori(db): + lb = _libro(BTC=dict(tp_frac=0.0, skh_sign=0, posizione=0.0)) + m = {"BTC": dict(chiusura=77000.0, tsmom_su=(3, 3), tsmom={})} + r = J.lettura(db, _voce(mercato=m, libro=lb, pnl=_pnl())) + riga = next(x for x in r if x["id"] == "incoerenza") + assert riga["livello"] == "attenzione" + + +def test_il_tetto_di_leva_e_DERIVATO_dal_config_non_riscritto(db, monkeypatch, tmp_path): + """Se qualcuno cambia `max_notional_per_asset_frac`, la lettura deve cambiare da sola.""" + (tmp_path / "config").mkdir() + (tmp_path / "config" / "live.json").write_text(json.dumps({"max_notional_per_asset_frac": 0.25})) + monkeypatch.setattr(J, "PROJECT_ROOT", tmp_path) + lb = _libro(BTC=dict(posizione=150.0), ETH=dict(posizione=150.0)) + r = J.lettura(db, _voce(libro=lb, pnl=_pnl())) + testo = next(x["testo"] for x in r if x["id"] == "leva") + assert "0.50x" in testo and "0.25" in testo # 0.25 x 2 asset, NON 1.00x + + +def test_config_illeggibile_lo_dice_invece_di_inventare_un_tetto(db, monkeypatch, tmp_path): + monkeypatch.setattr(J, "PROJECT_ROOT", tmp_path) # nessun config qui + r = J.lettura(db, _voce(libro=_libro(BTC=dict(posizione=100.0)), pnl=_pnl())) + riga = next(x for x in r if x["id"] == "leva") + assert "non verificabile" in riga["testo"] and riga["livello"] == "attenzione" + + +def test_soglia_IV_rank_derivata_dal_sleeve(db): + """La soglia del gate viene da VRP_CFG, e l'IV-rank e' quello ESPANDENTE del sleeve.""" + from src.portfolio.sleeves import VRP_CFG + lb = _libro(BTC=dict(posizione=100.0)) + m = {"BTC": dict(chiusura=77000.0, dvol=42.0, rv30_annua=43.0, dvol_rank_exp=0.18)} + testo = next(x["testo"] for x in J.lettura(db, _voce(mercato=m, libro=lb, pnl=_pnl())) + if x["id"] == "vol") + assert f"{VRP_CFG['gate_ivr']:.2f}" in testo and "sotto la soglia" in testo + m["BTC"]["dvol_rank_exp"] = 0.90 + testo = next(x["testo"] for x in J.lettura(db, _voce(mercato=m, libro=lb, pnl=_pnl())) + if x["id"] == "vol") + assert "**sopra**" in testo + + +def test_iv_rank_espandente_e_diverso_dal_rank_a_un_anno(): + """I due rank rispondono a domande diverse: il 2026-08-23 danno 0.18 contro 0.52. + + Etichettare l'uno col nome dell'altro ribalta il verdetto sul gate di VRP01. + """ + m = J.metriche_mercato(date.today()) + for a in ("BTC", "ETH"): + d = m.get(a) or {} + if d.get("dvol_rank_exp") is None or d.get("dvol_rank_1a") is None: + continue + assert d["dvol_rank_exp"] != d["dvol_rank_1a"] + + +def test_regola_fee_quando_il_costo_supera_il_movimento(db): + lb = _libro(BTC=dict(posizione=100.0)) + r = J.lettura(db, _voce(libro=lb, pnl=_pnl(delta_equity=0.01, fill=2, fee=0.05))) + assert "fee" in _ids(r) + r = J.lettura(db, _voce(libro=lb, pnl=_pnl(delta_equity=5.00, fill=2, fee=0.05))) + assert "fee" not in _ids(r) and "pnl" in _ids(r) + + +def test_evento_si_accende_oltre_2_deviazioni_e_tace_sotto(db): + lb = _libro(BTC=dict(posizione=100.0)) + # RV30 36.5% annua -> sd giornaliera ~1.91% + grosso = {"BTC": dict(chiusura=77000.0, ret_1g=-6.0, rv30_annua=36.5)} + normale = {"BTC": dict(chiusura=77000.0, ret_1g=-1.0, rv30_annua=36.5)} + assert "evento" in _ids(J.lettura(db, _voce(mercato=grosso, libro=lb, pnl=_pnl()))) + assert "evento" not in _ids(J.lettura(db, _voce(mercato=normale, libro=lb, pnl=_pnl()))) + + +def test_le_regole_tacciono_sul_non_misurato(db): + """Mercato assente: nessuna riga su vol o eventi. Il silenzio e' il comportamento giusto.""" + r = J.lettura(db, _voce(mercato={"BTC": {"errore": "feed KO"}}, + libro=_libro(BTC=dict(posizione=100.0)), pnl=_pnl(delta_equity=None))) + assert "vol" not in _ids(r) and "evento" not in _ids(r) and "pnl" not in _ids(r) + + +def test_giri_mancanti_e_feed_vecchio_sono_attenzioni(db): + s = dict(giri_book=6, feed_skh_min=685, atteso_giri=24, giorno_in_corso=False, giri_mancanti=18) + r = J.lettura(db, _voce(libro=_libro(BTC=dict(posizione=100.0)), pnl=_pnl(), salute=s)) + liv = {x["id"]: x["livello"] for x in r} + assert liv.get("giri") == "attenzione" and liv.get("feed") == "attenzione" + + +def test_senza_giri_la_lettura_dice_NON_MISURATO_e_si_ferma(db): + r = J.lettura(db, _voce()) + assert _ids(r) == ["stato"] and "non e' stato misurato" in r[0]["testo"] + + +def test_il_bucket_di_convinzione_non_inventa_i_due_terzi(): + """`tsmom_blend` media tre np.sign(): dopo il clip long-flat la direzione sta in {0,1/3,1}.""" + assert J._bucket_convinzione(0.0) == "flat" + assert J._bucket_convinzione(0.15) == "parziale" + assert J._bucket_convinzione(0.90) == "piena" + + +def test_concentrazione_tace_su_un_cumulato_irrilevante(db): + """Vera ma inutile: su pochi dollari la concentrazione e' aritmetica, non informazione.""" + for i in range(60): + db.execute("INSERT INTO equity VALUES (?,?,?)", + (f"2026-06-{(i % 28) + 1:02d}T{i % 24:02d}:00:00+00:00", 600.0 + i * 0.03, "t")) + db.commit() + v = _voce(libro=_libro(BTC=dict(posizione=100.0)), pnl=_pnl(), giorno="2026-06-28") + assert "concentrazione" not in _ids(J.lettura(db, v)) # ~$1.8 di cumulato + + +def test_concentrazione_si_accende_quando_la_taglia_conta(db): + for i in range(60): + v = 600.0 if i < 50 else 600.0 + (i - 49) * 4.0 # salto nella coda + db.execute("INSERT INTO equity VALUES (?,?,?)", + (f"2026-06-{(i % 28) + 1:02d}T{i % 24:02d}:00:00+00:00", v, "t")) + db.commit() + v = _voce(libro=_libro(BTC=dict(posizione=100.0)), pnl=_pnl(), giorno="2026-06-28") + assert "concentrazione" in _ids(J.lettura(db, v)) From f5d940921341eed2920ccdf6e0cf92fd925ff42d Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 17:46:29 +0000 Subject: [PATCH 114/114] analista di bordo: un modello scrive la prosa del giorno, in un campo suo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aggiunto il quarto livello della pagina, tenuto separato dagli altri tre: numeri -> misurati dal feed e dal DB Lettura -> regole deterministiche, ognuna col suo id Analisi -> questo: prosa di un modello, che puo' sbagliare Nota -> l'operatore NON scrive dentro `nota`, che era la richiesta letterale: quel campo e' dell'operatore, ed e' cio' che a rileggere il giornale fra sei mesi permette di sapere chi ha scritto cosa. L'agente ha `analisi`, marcato col modello, con l'ora e con l'esito del controllo sui numeri. Gira via `claude -p` (verificato con env -i che risponda nell'ambiente nudo di cron), una chiamata al giorno sul giorno CHIUSO, tolte le tool. Tre guardie, una per ogni modo in cui una prosa generata rovina un registro: - NUMERO INVENTATO: numeri_non_supportati() estrae ogni cifra dall'analisi e verifica che compaia in cio' che il modello ha ricevuto. Oltre tre numeri liberi l'analisi e' RIFIUTATA e la pagina resta senza. E' un controllo debole per costruzione, e lo dichiara: prende l'invenzione, non il ragionamento sbagliato. - COMMENTO DI SE': senza_analisi() toglie dalla pagina la sezione dell'agente prima di dargliela. Al primo giro reale il modello aveva letto la propria uscita precedente e prodotto un paragrafo sull'avviso che si era preso il giorno prima — un ciclo di retroazione che in poche settimane avrebbe riempito il giornale di meta-commento, e che nessun controllo automatico puo' distinguere da prosa valida. - ANALISI DI IERI SPACCIATA PER OGGI: se il modello non risponde, la pagina resta VUOTA e il perche' viene registrato (stato + motivo). Il silenzio non diventa continuita'. Corretto anche un falso positivo mio: il tripwire validava sulla sola pagina mentre il prompt include anche il blocco storico, quindi bocciava un'equity vera. Una guardia piu' stretta del contratto produce allarmi che si impara a ignorare. Ogni guardia ha un test in entrambe le direzioni. 695 test passano. Strategia, pesi, config INVARIATI. Nessun ordine. Co-Authored-By: Claude Opus 5 (1M context) --- docs/journal/2026-08-21.md | 12 ++- scripts/cron_daily.sh | 4 + scripts/live/analista.py | 99 ++++++++++++++++++ src/live/analista.py | 160 +++++++++++++++++++++++++++++ src/live/journal.py | 17 +++- src/live/tradesdb.py | 21 ++++ tests/test_analista.py | 202 +++++++++++++++++++++++++++++++++++++ 7 files changed, 512 insertions(+), 3 deletions(-) create mode 100644 scripts/live/analista.py create mode 100644 src/live/analista.py create mode 100644 tests/test_analista.py diff --git a/docs/journal/2026-08-21.md b/docs/journal/2026-08-21.md index f9e7329..08aac39 100644 --- a/docs/journal/2026-08-21.md +++ b/docs/journal/2026-08-21.md @@ -1,6 +1,6 @@ # Giornale di bordo — 2026-08-21 -*Scritto 2026-08-23T13:34:16+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-23T17:43:07+00:00. Numeri misurati; nessuna interpretazione automatica.* ## Mercato @@ -43,6 +43,16 @@ Equity **$644.00** · nozionale lordo $196 · leva lorda 0.30x · barra dati 202 - `[evento]` 📌 ETH: giornata a +8.19%, **2.3 deviazioni** giornaliere (sd implicita dalla RV30 = 3.62%). - `[evidenza]` 📌 Campione a oggi: **59 giorni, 10 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. +## Analisi (agente) + +*Scritta da `claude-sonnet-5` il 2026-08-23T17:43:32+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.* + +21 agosto — dopo dieci giorni fermi (11-18 agosto, equity ferma a 597.12, zero fill), il libro si è mosso in tre giornate consecutive: il 19 apre BTC ($+76, 1 round-trip), il 20 aggiunge ETH ($+148) e BTC sale a $+124, il 21 chiude entrambe le gambe a target (BTC $+126, ETH $+70) con $+23.37 di equity e $+18.90 realizzato su 6 round-trip. Il cumulato dall'arming passa così a $+45.94, e la pagina segnala che il 102% di quel totale viene dagli ultimi 7 giorni — cioè il periodo precedente era in perdita netta. + +Il contesto di mercato spiega perché si sia mosso qualcosa dopo dieci giorni piatti: BTC +7.31% e ETH +8.19% nella giornata, con TSMOM 3/3 al rialzo su entrambi gli orizzonti per entrambi gli asset. La pagina marca la giornata a 3.2 deviazioni per BTC e 2.3 per ETH rispetto alla volatilità realizzata — un movimento ampio, non un drift ordinario. Vale la pena notare che SKH01 è long su BTC (breakout) mentre su ETH resta flat pur con TP01 a convinzione più alta (+0.291 contro +0.176): stesso regime di mercato, due letture diverse a seconda del meccanismo. + +La leva resta bassa (0.30x su un tetto di 1.00x) e il feed SKH è fresco (0 minuti). Il campione dichiarato — 59 giorni, 10 round-trip — resta troppo corto perché questi tre giorni buoni dicano qualcosa sull'edge: sono compatibili sia con l'inizio di una fase favorevole al trend sia con normale rumore di un campione piccolo dopo una fase piatta. + ## Nota *(vuota — campo dell'operatore)* diff --git a/scripts/cron_daily.sh b/scripts/cron_daily.sh index 6aa7a7d..c2c9af6 100755 --- a/scripts/cron_daily.sh +++ b/scripts/cron_daily.sh @@ -43,5 +43,9 @@ mkdir -p logs # Sola lettura: feed certificato, cron_book.log, trades.db. Il campo `nota` non lo tocca. uv run python scripts/live/journal.py --giorno "$(date -u -d yesterday +%F)" --quiet || true uv run python scripts/live/journal.py --quiet || true + # Analista di bordo: un modello scrive la prosa del giorno CHIUSO nel campo `analisi`. + # Scrive SOLO li': non tocca `nota` (operatore) ne' le sezioni misurate, e se non risponde + # la pagina resta senza analisi invece di mostrare quella di ieri. Costo: 1 chiamata/giorno. + uv run python scripts/live/analista.py --giorno "$(date -u -d yesterday +%F)" --quiet || true echo "===== done $(date -u '+%H:%M:%SZ') =====" } >> logs/cron_daily.log 2>&1 diff --git a/scripts/live/analista.py b/scripts/live/analista.py new file mode 100644 index 0000000..200714d --- /dev/null +++ b/scripts/live/analista.py @@ -0,0 +1,99 @@ +"""L'analista di bordo: fa scrivere a un modello l'analisi del giorno nel campo `analisi`. + + uv run python scripts/live/analista.py # oggi + uv run python scripts/live/analista.py --giorno 2026-08-21 + uv run python scripts/live/analista.py --modello claude-opus-5 + uv run python scripts/live/analista.py --secco # stampa e non salva + +NON tocca `nota` (dell'operatore) e non tocca nessuna sezione misurata: scrive SOLO in `analisi`. +Se il modello non risponde, l'analisi di ieri NON diventa quella di oggi: si registra l'errore +e la sezione resta vuota. Sola lettura su tutto il resto; nessun ordine. +""" +from __future__ import annotations + +import sys +from datetime import date, datetime, timedelta, timezone +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from src.live import analista as A # noqa: E402 +from src.live import journal as J # noqa: E402 +from src.live import tradesdb as T # noqa: E402 + +GIORNI_STORICO = 10 + + +def _arg(flag: str, default=None): + a = sys.argv[1:] + return a[a.index(flag) + 1] if flag in a and a.index(flag) + 1 < len(a) else default + + +def storico(con, g: date, n: int = GIORNI_STORICO) -> str: + """Le righe essenziali dei giorni precedenti: serve per vedere COSA E' CAMBIATO.""" + righe = ["giorno equity delta fill rt posizioni"] + for i in range(n, 0, -1): + d = (g - timedelta(days=i)).isoformat() + r = con.execute("SELECT pnl, libro FROM journal WHERE giorno=?", (d,)).fetchone() + if not r: + continue + import json as _j + p, lb = _j.loads(r["pnl"] or "{}"), _j.loads(r["libro"] or "{}") + pos = " ".join(f"{a} ${v['posizione']:+,.0f}" for a, v in sorted((lb.get("asset") or {}).items())) + dq = p.get("delta_equity") + righe.append(f"{d} {p.get('equity_fine') or float('nan'):8,.2f} " + f"{' n/d' if dq is None else f'{dq:+8.2f}'} " + f"{p.get('fill', 0):>4} {p.get('roundtrip_chiusi', 0):>2} {pos or 'flat'}") + return "\n".join(righe) + + +def scrivi(con, g: date, modello: str, secco: bool = False, verbose: bool = True) -> str: + voce = J.costruisci(con, g) + J.salva(con, voce) # le sezioni misurate prima, cosi' il modello le legge + pagina = J.rendi_markdown(con, voce) + st = storico(con, g) + prompt = A.costruisci_prompt(pagina, st) + # La guardia si applica a TUTTO cio' che il modello ha ricevuto, non alla sola pagina: + # i numeri del blocco storico gli sono stati dati, citarli non e' inventare. (Primo giro: + # validando sulla sola pagina il tripwire bocciava un'equity vera presa dallo storico — + # una guardia piu' stretta del contratto produce allarmi che si imparera' a ignorare.) + fonte = A.senza_analisi(pagina) + "\n" + st + testo, errore = A.interroga(prompt, modello=modello) + + if errore: + stato, motivi = "errore", [errore] + testo = "" + else: + stato, motivi = A.valida(testo, fonte) + if stato == "rifiutata": + testo = "" + + if verbose: + print(f" {g} modello {modello} -> {stato}" + + (f" ({'; '.join(motivi)})" if motivi else "")) + if secco: + print("\n" + (testo or "(niente da salvare)")) + return stato + + if testo: + con.execute("""UPDATE journal SET analisi=?, analisi_modello=?, analisi_ts=?, + analisi_stato=?, analisi_motivi=? WHERE giorno=?""", + (testo, modello, T.ora(), stato, "; ".join(motivi) or None, g.isoformat())) + else: + # niente analisi: si REGISTRA il perche' e NON si lascia in pagina quella di ieri + con.execute("""UPDATE journal SET analisi='', analisi_modello=?, analisi_ts=?, + analisi_stato=?, analisi_motivi=? WHERE giorno=?""", + (modello, T.ora(), stato, "; ".join(motivi) or None, g.isoformat())) + con.commit() + J.scrivi_file(con, voce) + return stato + + +if __name__ == "__main__": + con = T.connect() + g = date.fromisoformat(_arg("--giorno", datetime.now(timezone.utc).date().isoformat())) + st = scrivi(con, g, modello=_arg("--modello", A.MODELLO_DEFAULT), + secco="--secco" in sys.argv[1:], verbose="--quiet" not in sys.argv[1:]) + con.close() + sys.exit(0 if st in ("ok", "sospetta") else 1) diff --git a/src/live/analista.py b/src/live/analista.py new file mode 100644 index 0000000..d1a79fb --- /dev/null +++ b/src/live/analista.py @@ -0,0 +1,160 @@ +"""L'agente che scrive l'analisi giornaliera — e i limiti dentro cui puo' scrivere. + +PERCHE' UN CAMPO SUO E NON `nota` +--------------------------------- +`nota` e' dell'operatore. Se un agente ci scrivesse dentro, fra sei mesi rileggendo il giornale +non si distinguerebbe piu' *chi* ha scritto cosa — che e' esattamente la distinzione per cui il +giornale esiste. L'agente ha il campo `analisi`, marcato col modello e con l'ora. + +TRE LIVELLI, TENUTI SEPARATI IN PAGINA +-------------------------------------- + numeri -> misurati dal feed e dal DB + Lettura -> regole dichiarate, deterministiche, ognuna col suo id + Analisi -> questo modulo: prosa di un modello, che puo' sbagliare + Nota -> l'operatore + +IL RISCHIO, E LA GUARDIA +------------------------ +Il modo in cui una prosa generata rovina un registro non e' l'opinione sbagliata: e' il NUMERO +inventato, che a rileggerlo sembra una misura. `numeri_non_supportati()` estrae ogni numero +dall'analisi e verifica che compaia nella pagina data in pasto al modello. Oltre +`MAX_NUMERI_LIBERI` l'analisi viene RIFIUTATA e si tiene la precedente. +E' un controllo debole per costruzione (un numero giusto copiato nel posto sbagliato lo passa): +serve contro l'invenzione, non contro il ragionamento sbagliato. +""" +from __future__ import annotations + +import re +import subprocess +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] + +MODELLO_DEFAULT = "claude-sonnet-5" +MAX_PAROLE = 220 +MAX_NUMERI_LIBERI = 3 +TIMEOUT_S = 300 + +ISTRUZIONI = """Sei l'analista di bordo di un libro di trading quantitativo su BTC/ETH. +Scrivi l'analisi di UNA giornata, in italiano, per un lettore che rileggera' questa pagina fra +mesi per capire cosa stava succedendo. + +REGOLE VINCOLANTI: +1. Usa SOLO i numeri presenti nella pagina qui sotto. Non calcolarne di nuovi, non stimare, non + arrotondare in modo diverso. Ogni cifra che scrivi deve comparire nella pagina. +2. NON prevedere. Niente "probabilmente salira'", niente target, niente consigli operativi. +3. NON ripetere la sezione Lettura: quella e' gia' scritta. Aggiungi cio' che le regole non + possono vedere — cosa e' CAMBIATO rispetto ai giorni precedenti, cosa e' insolito, quale + lettura alternativa dello stesso dato sarebbe altrettanto compatibile. +4. Se una cosa non e' determinabile dai dati dati, dillo: "non misurabile qui" e' una risposta + accettabile e preferibile a un'inferenza. +5. Se noti qualcosa che merita un controllo umano, dillo in una frase, senza drammatizzare. +6. Massimo %d parole. Prosa, niente elenchi puntati, niente titoli. + +Il libro e' TP01 (trend long-flat, 75%%) + SKH01 (breakout, 25%%) su Deribit, capitale ~$635. +Il progetto e' esplicitamente scettico: il suo campione e' corto e il P&L non distingue ancora +l'edge dalla fortuna. Scrivere con quella consapevolezza e' parte del compito. +""" % MAX_PAROLE + + +# ---------------------------------------------------------------- guardia sui numeri + +_NUM = re.compile(r"-?\d[\d.,]*") + + +def _normalizza(tok: str) -> str: + t = tok.replace(",", "").rstrip(".").lstrip("-").lstrip("0") or "0" + if "." in t: + t = t.rstrip("0").rstrip(".") or "0" + return t + + +def numeri_di(testo: str) -> set[str]: + return {_normalizza(m.group()) for m in _NUM.finditer(testo)} + + +def numeri_non_supportati(analisi: str, fonte: str) -> list[str]: + """Numeri che compaiono nell'analisi e NON nella pagina data al modello. + + Tollera i numeri piccoli fino a 31 (giorni, conteggi, «2 su 3»): non sono cifre che possano + fabbricare una misura, e includerli produrrebbe soprattutto falsi positivi. + """ + ok = numeri_di(fonte) + fuori = [] + for t in sorted(numeri_di(analisi) - ok): + try: + if abs(float(t)) <= 31 and "." not in t: + continue + except ValueError: + continue + fuori.append(t) + return fuori + + +def valida(analisi: str, fonte: str) -> tuple[str, list[str]]: + """Ritorna (stato, motivi). Stato in {ok, sospetta, rifiutata}.""" + motivi: list[str] = [] + testo = analisi.strip() + if not testo: + return "rifiutata", ["risposta vuota"] + n_par = len(testo.split()) + if n_par > MAX_PAROLE * 1.5: + motivi.append(f"troppo lunga ({n_par} parole contro un tetto di {MAX_PAROLE})") + fuori = numeri_non_supportati(testo, fonte) + if fuori: + motivi.append("numeri non presenti nella pagina: " + ", ".join(fuori)) + if len(fuori) > MAX_NUMERI_LIBERI: + return "rifiutata", motivi + return ("sospetta" if motivi else "ok"), motivi + + +def pulisci(testo: str) -> str: + """Toglie titoli e recinti di codice: l'analisi entra in una sezione che ha gia' il suo.""" + righe = [r for r in testo.strip().splitlines() if not r.strip().startswith(("#", "```"))] + return "\n".join(righe).strip() + + +# ---------------------------------------------------------------- chiamata al modello + +def senza_analisi(md: str) -> str: + """Toglie la sezione `## Analisi (agente)` dalla pagina prima di darla al modello. + + Senza questo il modello legge la PROPRIA uscita del giro precedente e la commenta: al primo + giro reale ha prodotto un paragrafo sull'avviso del tripwire che si era preso il giorno prima. + E' un ciclo di retroazione — ogni giorno un po' piu' di meta-commento e un po' meno analisi — + e si vede solo guardando l'uscita, perche' nessun controllo puo' distinguerlo da prosa valida. + La sezione `## Nota` resta: quella e' contesto dell'operatore, ed e' utile che la legga. + """ + fuori, salta = [], False + for r in md.splitlines(): + if r.startswith("## "): + salta = r.strip().startswith("## Analisi") + if not salta: + fuori.append(r) + return "\n".join(fuori) + + +def costruisci_prompt(pagina: str, storico: str) -> str: + pagina = senza_analisi(pagina) + return (f"{ISTRUZIONI}\n\n" + f"=== PAGINA DI OGGI ===\n{pagina}\n\n" + f"=== GIORNI PRECEDENTI (per vedere cosa e' cambiato) ===\n{storico}\n\n" + f"Scrivi ora l'analisi, e nient'altro.") + + +def interroga(prompt: str, modello: str = MODELLO_DEFAULT, + timeout: int = TIMEOUT_S) -> tuple[str, str | None]: + """Ritorna (testo, errore). L'errore si RESTITUISCE, non si ingoia: senza, il giorno che + l'agente smette di rispondere il giornale mostrerebbe l'analisi di ieri come se fosse di oggi. + """ + try: + p = subprocess.run( + ["claude", "-p", prompt, "--model", modello, "--allowed-tools", "", "--max-turns", "1"], + capture_output=True, text=True, timeout=timeout) + except FileNotFoundError: + return "", "CLI `claude` non trovata" + except subprocess.TimeoutExpired: + return "", f"timeout dopo {timeout}s" + if p.returncode != 0: + return "", f"uscita {p.returncode}: {(p.stderr or '').strip()[:200]}" + return pulisci(p.stdout), None diff --git a/src/live/journal.py b/src/live/journal.py index 19ff699..48dd17e 100644 --- a/src/live/journal.py +++ b/src/live/journal.py @@ -152,6 +152,8 @@ def costruisci(con, giorno: date) -> dict: def salva(con, voce: dict, nota: str | None = None) -> None: + """Ricalcola le sezioni misurate. NON tocca ne' `nota` (operatore) ne' `analisi` (agente): + sono le uniche due cose in pagina che un ricalcolo non puo' rigenerare.""" esistente = con.execute("SELECT nota FROM journal WHERE giorno=?", (voce["giorno"],)).fetchone() # la nota dell'operatore NON viene mai sovrascritta da un ricalcolo automatico nota_finale = nota if nota is not None else (esistente["nota"] if esistente else "") @@ -363,8 +365,9 @@ def _n(v, fmt="{:+.2f}", vuoto="n/d"): def rendi_markdown(con, voce: dict) -> str: g, m, lb, p, s = voce["giorno"], voce["mercato"], voce["libro"], voce["pnl"], voce["salute"] - nota = con.execute("SELECT nota FROM journal WHERE giorno=?", (g,)).fetchone() - nota = nota["nota"] if nota else "" + riga = con.execute("SELECT * FROM journal WHERE giorno=?", (g,)).fetchone() + nota = (riga["nota"] if riga else "") or "" + an = dict(riga) if riga else {} L = [f"# Giornale di bordo — {g}", "", f"*Scritto {voce['ts_scritto']}. Numeri misurati; nessuna interpretazione automatica.*", "", "## Mercato", "", @@ -425,6 +428,16 @@ def rendi_markdown(con, voce: dict) -> str: "giudizio: quello sta nella Nota.*", ""] icona = {"info": "", "nota": "📌 ", "attenzione": "⚠️ "} L += [f"- `[{r['id']}]` {icona[r['livello']]}{r['testo']}" for r in righe] or ["- *(nessuna regola accesa)*"] + testo = (an.get("analisi") or "").strip() + if testo: + stato = an.get("analisi_stato") or "?" + cap = (f"*Scritta da `{an.get('analisi_modello')}` il {an.get('analisi_ts')}. " + f"Prosa di un modello: puo' sbagliare, e non e' una misura. " + f"Controllo sui numeri: **{stato}**.*") + L += ["", "## Analisi (agente)", "", cap, ""] + if an.get("analisi_motivi"): + L += [f"> ⚠️ {an['analisi_motivi']}", ""] + L += [testo] L += ["", "## Nota", "", (nota if nota.strip() else "*(vuota — campo dell'operatore)*"), ""] return "\n".join(L) diff --git a/src/live/tradesdb.py b/src/live/tradesdb.py index 58b8816..33e67b7 100644 --- a/src/live/tradesdb.py +++ b/src/live/tradesdb.py @@ -97,6 +97,26 @@ CREATE TABLE IF NOT EXISTS journal ( CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY, v TEXT); """ +# Colonne aggiunte dopo la prima versione dello schema. sqlite non ha ADD COLUMN IF NOT EXISTS: +# si guarda il PRAGMA. `analisi` e' dell'AGENTE, `nota` resta dell'operatore — due campi, perche' +# a rileggere il giornale fra sei mesi si deve sapere chi ha scritto cosa. +COLONNE_TARDIVE = { + "journal": [("analisi", "TEXT NOT NULL DEFAULT ''"), + ("analisi_modello", "TEXT"), + ("analisi_ts", "TEXT"), + ("analisi_stato", "TEXT"), + ("analisi_motivi", "TEXT")], +} + + +def _migra(con) -> None: + for tabella, colonne in COLONNE_TARDIVE.items(): + presenti = {r[1] for r in con.execute(f"PRAGMA table_info({tabella})")} + for nome, tipo in colonne: + if nome not in presenti: + con.execute(f"ALTER TABLE {tabella} ADD COLUMN {nome} {tipo}") + con.commit() + # ============================================================================================= # parsing — puro, nessun I/O: prende testo, ritorna dati @@ -278,6 +298,7 @@ def connect(path: Path | str = DB_PATH) -> sqlite3.Connection: con = sqlite3.connect(path) con.row_factory = sqlite3.Row con.executescript(SCHEMA) + _migra(con) return con diff --git a/tests/test_analista.py b/tests/test_analista.py new file mode 100644 index 0000000..0e83277 --- /dev/null +++ b/tests/test_analista.py @@ -0,0 +1,202 @@ +"""L'analista di bordo: cosa puo' scrivere, e cosa non deve poter rompere. + +Un modello che scrive in un registro di misure ha tre modi di rovinarlo, e qui c'e' un test per +ognuno: inventare un numero, commentare se stesso, e lasciare in pagina l'analisi di ieri il +giorno che non risponde. +""" +from __future__ import annotations + +import subprocess +import sys +from datetime import date +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "live")) + +from src.live import analista as A # noqa: E402 +from src.live import journal as J # noqa: E402 +from src.live import tradesdb as T # noqa: E402 + + +@pytest.fixture() +def db(tmp_path): + con = T.connect(tmp_path / "t.db") + yield con + con.close() + + +PAGINA = """# Giornale di bordo — 2026-08-21 + +## Mercato +| **BTC** | $77,000.00 | +7.31% | RV30 43.9% | + +## Libro +Equity **$644.00** · leva lorda 0.30x + +## Lettura +- `[stato]` A mercato su BTC $+126. + +## Analisi (agente) +*Scritta da un modello.* +Testo vecchio che il modello NON deve rileggere. + +## Nota +Contesto scritto dall'operatore. +""" + + +# ------------------------------------------------- il ciclo di retroazione + +def test_toglie_la_propria_analisi_precedente(): + fuori = A.senza_analisi(PAGINA) + assert "Testo vecchio" not in fuori + assert "## Analisi" not in fuori + + +def test_ma_lascia_tutto_il_resto_compresa_la_nota_dell_operatore(): + """Controllo positivo: se togliesse anche il resto, il modello scriverebbe al buio.""" + fuori = A.senza_analisi(PAGINA) + for pezzo in ("## Mercato", "## Libro", "## Lettura", "## Nota", + "Contesto scritto dall'operatore", "$77,000.00", "`[stato]`"): + assert pezzo in fuori + + +def test_una_pagina_senza_analisi_non_viene_toccata(): + md = "# Titolo\n\n## Mercato\nx\n\n## Nota\ny\n" + assert A.senza_analisi(md) == md.rstrip("\n") + + +def test_il_prompt_non_contiene_l_analisi_vecchia(): + p = A.costruisci_prompt(PAGINA, "storico") + assert "Testo vecchio" not in p and "storico" in p + + +# ------------------------------------------------- i numeri inventati + +def test_scopre_un_numero_che_non_c_e(): + assert A.numeri_non_supportati("L'equity e' salita a 812.40.", PAGINA) == ["812.4"] + + +def test_non_segnala_i_numeri_presenti_comunque_formattati(): + testo = "Equity 644.00 (da $644.00), BTC a 77,000.00 e RV30 43.9%." + assert A.numeri_non_supportati(testo, PAGINA) == [] + + +def test_tollera_i_conteggi_piccoli(): + """«2 su 3 orizzonti», «24 giri»: non sono cifre che possano fabbricare una misura.""" + assert A.numeri_non_supportati("2 su 3 orizzonti, 24 giri, 10 round-trip.", PAGINA) == [] + + +def test_una_percentuale_inventata_viene_presa(): + assert "18.57" in A.numeri_non_supportati("Il mese fa +18.57%.", PAGINA) + + +# ------------------------------------------------- la validazione + +def test_analisi_pulita_e_ok(): + stato, motivi = A.valida("BTC ha fatto +7.31% e il libro era a $+126.", PAGINA) + assert stato == "ok" and motivi == [] + + +def test_un_numero_libero_la_rende_sospetta_non_rifiutata(): + stato, motivi = A.valida("Equity a 812.40 dopo il rialzo.", PAGINA) + assert stato == "sospetta" and "812.4" in motivi[0] + + +def test_troppi_numeri_liberi_la_rifiutano(): + stato, _ = A.valida("Valori: 812.40, 913.55, 1044.10, 1155.20, 1266.30.", PAGINA) + assert stato == "rifiutata" + + +def test_risposta_vuota_rifiutata(): + assert A.valida(" ", PAGINA)[0] == "rifiutata" + + +def test_pulisci_toglie_titoli_e_recinti(): + assert A.pulisci("```\n# Titolo\ntesto vero\n```") == "testo vero" + + +# ------------------------------------------------- l'errore non si ingoia + +def test_cli_assente_ritorna_errore_non_eccezione(monkeypatch): + monkeypatch.setattr(subprocess, "run", lambda *a, **k: (_ for _ in ()).throw(FileNotFoundError())) + testo, err = A.interroga("x") + assert testo == "" and "non trovata" in err + + +def test_timeout_ritorna_errore(monkeypatch): + def boom(*a, **k): + raise subprocess.TimeoutExpired(cmd="claude", timeout=1) + monkeypatch.setattr(subprocess, "run", boom) + assert "timeout" in A.interroga("x")[1] + + +def test_uscita_non_zero_ritorna_errore(monkeypatch): + class R: + returncode, stdout, stderr = 2, "", "boom" + monkeypatch.setattr(subprocess, "run", lambda *a, **k: R()) + assert "uscita 2" in A.interroga("x")[1] + + +# ------------------------------------------------- il DB: cosa scrive e cosa non tocca + +def _prepara(db, g="2026-08-21"): + voce = dict(giorno=g, ts_scritto=T.ora(), mercato={}, libro={"errore": "x", "giri": 0}, + pnl=J.pnl_giorno(db, date.fromisoformat(g)), + salute=dict(giri_book=0, feed_skh_min=None, atteso_giri=24, + giorno_in_corso=False, giri_mancanti=24)) + J.salva(db, voce, nota="nota dell'operatore") + return voce + + +def test_l_agente_non_tocca_la_nota(db, monkeypatch, tmp_path): + import analista as CLI + _prepara(db) + monkeypatch.setattr(A, "interroga", lambda *a, **k: ("Analisi valida senza numeri nuovi.", None)) + monkeypatch.setattr(J, "JOURNAL_DIR", tmp_path) + CLI.scrivi(db, date(2026, 8, 21), modello="finto", verbose=False) + r = db.execute("SELECT nota, analisi, analisi_stato FROM journal WHERE giorno='2026-08-21'").fetchone() + assert r["nota"] == "nota dell'operatore" + assert r["analisi"].startswith("Analisi valida") and r["analisi_stato"] == "ok" + + +def test_se_il_modello_non_risponde_l_analisi_di_IERI_non_resta_in_pagina(db, monkeypatch, tmp_path): + """Il guasto peggiore: una pagina che mostra l'analisi del giorno prima come se fosse di oggi.""" + import analista as CLI + _prepara(db) + monkeypatch.setattr(J, "JOURNAL_DIR", tmp_path) + monkeypatch.setattr(A, "interroga", lambda *a, **k: ("Analisi di ieri.", None)) + CLI.scrivi(db, date(2026, 8, 21), modello="finto", verbose=False) + monkeypatch.setattr(A, "interroga", lambda *a, **k: ("", "rete KO")) + CLI.scrivi(db, date(2026, 8, 21), modello="finto", verbose=False) + r = db.execute("SELECT analisi, analisi_stato, analisi_motivi FROM journal " + "WHERE giorno='2026-08-21'").fetchone() + assert r["analisi"] == "" # niente in pagina + assert r["analisi_stato"] == "errore" and "rete KO" in r["analisi_motivi"] # ma il perche' c'e' + + +def test_un_ricalcolo_del_giornale_non_cancella_l_analisi(db, monkeypatch, tmp_path): + import analista as CLI + voce = _prepara(db) + monkeypatch.setattr(J, "JOURNAL_DIR", tmp_path) + monkeypatch.setattr(A, "interroga", lambda *a, **k: ("Analisi da conservare.", None)) + CLI.scrivi(db, date(2026, 8, 21), modello="finto", verbose=False) + J.salva(db, voce) # il cron rigenera le sezioni misurate + r = db.execute("SELECT analisi, nota FROM journal WHERE giorno='2026-08-21'").fetchone() + assert r["analisi"] == "Analisi da conservare." and r["nota"] == "nota dell'operatore" + + +def test_il_markdown_dichiara_modello_ora_e_esito_del_controllo(db, monkeypatch, tmp_path): + import analista as CLI + voce = _prepara(db) + monkeypatch.setattr(J, "JOURNAL_DIR", tmp_path) + monkeypatch.setattr(A, "interroga", lambda *a, **k: ("Testo dell'agente.", None)) + CLI.scrivi(db, date(2026, 8, 21), modello="modello-finto", verbose=False) + md = J.rendi_markdown(db, voce) + assert "## Analisi (agente)" in md and "modello-finto" in md + assert "puo' sbagliare" in md and "Controllo sui numeri: **ok**" in md + assert md.index("## Analisi") < md.index("## Nota") # l'agente prima, l'operatore in fondo