From a4e942c8bb20e41195a25e4d5c4866a084ad45ae Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Thu, 10 Sep 2026 14:56:29 +0000 Subject: [PATCH] =?UTF-8?q?r0910:=20risk-off=20=C2=B12h=20intorno=20a=20FO?= =?UTF-8?q?MC/CPI=20su=20TP01=20REFUTED=200/3=20(=CE=94Sh=20=E2=88=920,097?= =?UTF-8?q?,=20=E2=88=921,24%/anno;=20le=20ore=20dei=20dati=20sono=20fra?= =?UTF-8?q?=20le=20migliori)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calendario letto dal web (Fed 61 riunioni + BLS 88 release CPI, 2019-2026), TP01 orario dalla stessa held di sleeves, null location-matched 1000 estrazioni (Δ vero al 4,2° pctl), per anno 2/8. Registro §78, memoria 20-ondate (D-bis), diario 2026-09-10b. Fixes #9 Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_016kqvff47UBGeYfj1QeN4zE --- docs/diary/2026-09-10b-macro-riskoff.md | 52 +++++ docs/memory/20-ondate-e-scartati.md | 6 +- docs/research/RESULTS-0822.md | 11 + scripts/research/r0910_macro_riskoff.py | 298 ++++++++++++++++++++++++ 4 files changed, 366 insertions(+), 1 deletion(-) create mode 100644 docs/diary/2026-09-10b-macro-riskoff.md create mode 100644 scripts/research/r0910_macro_riskoff.py diff --git a/docs/diary/2026-09-10b-macro-riskoff.md b/docs/diary/2026-09-10b-macro-riskoff.md new file mode 100644 index 0000000..302f914 --- /dev/null +++ b/docs/diary/2026-09-10b-macro-riskoff.md @@ -0,0 +1,52 @@ +# Risk-off ±2h intorno a FOMC e CPI su TP01 — REFUTED, e il segno e' l'opposto + +**Data:** 2026-09-10 14:52-14:55Z · **Script:** `scripts/research/r0910_macro_riskoff.py` (N11: il +verdetto lo calcola lo script) · **Issue:** #9 · **Origine:** domanda dell'operatore da un articolo su +EA forex a griglia con «agente fondamentale» che approva/rifiuta i segnali intorno ai dati macro. + +## Cosa si e' misurato + +TP01 canonico (posizione decisa alla chiusura giornaliera, tenuta 24 barre 1h), 2019-03 → 2026-09, +65.676 ore. Variante RISK-OFF: esposizione zero nelle barre che intersecano [t−2h, t+2h] intorno a +**61 dichiarazioni FOMC** (14:00 ET, piu' le due d'emergenza di marzo 2020, notation vote esclusi) e +**88 release CPI** (08:30 ET), con fee di chiusura e riapertura. Calendario **letto dal web** (Fed e +BLS), non ricordato; l'archivio BLS raggruppa per anno di riferimento e le release di gennaio/febbraio +vanno spostate di +1 anno (verificato sulla schedule 2026). 684 ore in finestra = **1,04%** del tempo. + +Verdetto pre-registrato prima del primo numero: LEAD solo se (a) ΔSharpe con fee > 0, (b) Δ al ≥95° +percentile del null location-matched (stessi conteggi per anno, stesse ore del giorno, giorni a caso, +1.000 estrazioni — M18), (c) Δ > 0 in ≥70% degli anni (M9). + +## Numeri + +| | Sharpe giornaliero | drift/anno | +|---|---|---| +| TP01 base | 1,302 | +15,73% | +| risk-off con fee | 1,205 | +14,49% | +| risk-off senza fee | 1,228 | +14,77% | +| **Δ** | **−0,097** | **−1,24%** ≈ **−0,14 €/giorno** a $4.455 | + +Per tipo: FOMC ΔSh −0,012 (P&L rinunciato +0,79%), **CPI −0,084 (+6,36%)**. Per anno: Δ negativo in +6/8, positivo solo 2024 e 2026 (+0,02/+0,03). + +Null location-matched: ΔSh mediana −0,033 [p05 −0,095, p95 +0,027]. **Il Δ vero sta al 4,2° +percentile**: le finestre vere sono fra le PEGGIORI da appiattire, cioe' fra le migliori in cui +essere esposti. Dentro la finestra TP01 fa **+1,05 bp/ora contro +0,17 fuori** (t = 1,57 su 684 ore). + +(a) FAIL · (b) FAIL · (c) 2/8 FAIL ⇒ **REFUTED**. + +## Lettura + +1. **Il risk-off toglie P&L, non rischio.** TP01 e' long-flat vol-targeted: nelle ore dei dati macro + il trend che sta cavalcando accelera piu' spesso di quanto si rovesci, e l'1% del tempo in finestra + porta il 7% del guadagno cumulato. E' la stessa lezione del weekend (§3, 17/07: il 38% del gross + nel 31% del tempo): **la coda del trend vive nelle ore «pericolose»**. +2. **Il segno opposto non e' un lead.** «Esporsi DI PIU' intorno ai dati» sarebbe un'ipotesi nuova + (M12), con t = 1,57 e 684 ore: sotto qualunque MDE, e la selezione sull'esito la vieta. Non si apre. +3. Il gate «fondamentale» dell'articolo, applicato al nostro libro, costerebbe **−1,24%/anno di drift** + — la stessa taglia del funding non modellato (−2,16%), ma con segno scelto da chi lo propone. +4. Coerente con `macro-regime-gate` (29/06, ridondante col trend) e con la conclusione di §3: **ogni + risk-off parte REFUTED** e questo lo e' rimasto. + +Non modellato (dichiarato): funding (identico nelle due varianti salvo l'1% delle ore); slippage +della finestra (rende il risk-off *piu'* costoso, non meno). Costo: 3 minuti di calcolo, 0 ordini. diff --git a/docs/memory/20-ondate-e-scartati.md b/docs/memory/20-ondate-e-scartati.md index 7513b78..515212c 100644 --- a/docs/memory/20-ondate-e-scartati.md +++ b/docs/memory/20-ondate-e-scartati.md @@ -69,7 +69,11 @@ chi lo riapre deve battere il motivo, non ripetere l'esperimento. solo LOWVOL 19-major regge standalone (FULL/HOLD 1.07) ma deflated-Sharpe 0.13 + storia 2.5a → **DEBOLE/ forward STAT-MODE** (`2026-06-29-xsec-v2-nonmom.md`). (D) **MACRO regime-gate** (equity/credito/oro/tassi → de-risk crypto): **RIDONDANTE col trend** (corr→TP01 0.989; il gate lavora solo nel 2-3% dei giorni, TP01 - già flat nei crash) → SCARTATO (`2026-06-29-macro-regime-gate.md`). + già flat nei crash) → SCARTATO (`2026-06-29-macro-regime-gate.md`). (D-bis, 10/09) **MACRO RISK-OFF ±2h intorno a + FOMC/CPI su TP01** (`r0910_macro_riskoff.py`, §78): **REFUTED 0/3** — ΔSh −0,097, drift −1,24%/anno, Δ vero al + 4,2° pctl del null location-matched: nell'1% delle ore in finestra TP01 fa il 7% del guadagno (+1,05 bp/h + contro +0,17). Come il weekend: la coda del trend vive nelle ore «pericolose». Un gate «fondamentale» + che rifiuta i segnali intorno ai dati costerebbe quanto il funding, con segno scelto da chi lo propone. - **Sweep strategie a 5 thread (2026-06-29) — 0 nuovi sleeve, 1 LEAD che rompe 2 muri su 3.** Ricerca parallela onesta su aree inesplorate (harness `altlib`+`xsec_v2_nonmom`, tutti i gate incl. il nuovo diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 24162cb..7f7dd78 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -4058,3 +4058,14 @@ Opzioni USDC-lineari su 7 sottostanti (SOL 676, HYPE 442, XRP 442, TRX, AVAX). T negoziabile) e **PAXG perp** (oro, dal 2024-12, $0,18M/g: non misurato, non morto, muri di SOL/XRP). **VERDETTO: `QUATTRO STRUMENTI LIQUIDI, TRE GIA' NEL LIBRO O MISURATI`** — l'universo "piu' monete" di Deribit e' finito. +## 78 — MACRO-RISKOFF (TP01 flat ±2h intorno a FOMC e CPI: vale qualcosa?) + +`scripts/research/r0910_macro_riskoff.py` (10/09, issue #9, ~3 min). Calendario letto dal web (Fed + BLS, +61 FOMC + 88 CPI, 2019-03 → 2026-09), 684 ore in finestra = 1,04% del tempo. Verdetto pre-registrato +(ΔSh>0 con fee · ≥95° pctl del null location-matched a 1.000 estrazioni · positivo in ≥70% degli anni). +**REFUTED 0/3**: ΔSharpe giornaliero **−0,097**, drift **−1,24%/anno** (≈ −0,14 €/g a $4.455), negativo in +6/8 anni, e il Δ vero sta al **4,2° percentile** del null — le ore intorno ai dati macro sono fra le MIGLIORI +in cui TP01 e' esposto (+1,05 bp/h contro +0,17, t = 1,57): l'1% del tempo porta il 7% del guadagno. Stessa +lezione del weekend (§3): la coda del trend vive nelle ore «pericolose». Il segno opposto NON si apre +(ipotesi nuova, sotto l'MDE, selezionata sull'esito). Diario `2026-09-10b-macro-riskoff.md`. + diff --git a/scripts/research/r0910_macro_riskoff.py b/scripts/research/r0910_macro_riskoff.py new file mode 100644 index 0000000..7ae43da --- /dev/null +++ b/scripts/research/r0910_macro_riskoff.py @@ -0,0 +1,298 @@ +#!/usr/bin/env python +"""r0910_macro_riskoff.py — «risk-off» di TP01 nella finestra ±2h intorno a FOMC e CPI: vale qualcosa? + +DOMANDA (operatore, 2026-09-10, da un articolo su EA forex con «agente fondamentale»): se TP01 si +mettesse FLAT nelle due ore prima e dopo la dichiarazione FOMC (14:00 ET) e la release CPI +(08:30 ET), guadagnerebbe Sharpe o drift? E' un risk-off, quindi per la regola di §3 («non +de-esporre il weekend») PARTE REFUTED e deve superare il null del de-levering (M5) e un null +location-matched (M18) prima di essere creduto. + +VERDETTO PRE-REGISTRATO (scritto PRIMA di vedere un numero): + LEAD solo se TUTTE: (a) ΔSharpe(riskoff − base) > 0 con le fee di chiusura/riapertura; + (b) il Δ vero sta al ≥95° percentile del null location-matched (stesso numero di eventi per + anno, stesse ore del giorno, giorni estratti a caso); (c) Δ positivo in ≥70% degli anni con ≥5 + eventi (M9). Altrimenti REFUTED. E l'effetto si cita anche in €/giorno all'equity di oggi. + +COSA NON C'E' (dichiarato): il funding (mai in nessun backtest, −2,16%/anno di drift) — irrilevante +qui perche' identico nelle due varianti salvo ~1% delle ore; il rimbalzo di prezzo nella finestra +(slippage: TP01 chiude e riapre a mid, in un'ora in cui lo spread e' largo — se il risultato fosse +positivo andrebbe rimisurato col costo vero della finestra, C1). SKH01 non e' qui: e' un breakout su +barra 230m, appiattirlo cambierebbe il percorso del segnale. + +CALENDARIO (letto dal web il 2026-09-10, non ricordato — regola globale): + FOMC: federalreserve.gov/monetarypolicy/fomccalendars.htm (2021-2026) e fomchistorical2019/2020. + Ore: 14:00 ET per le riunioni programmate (standard Fed); le due d'emergenza del 2020 + (03/03 10:00 ET, 15/03 17:00 ET) con la loro ora; i «notation vote» ESCLUSI (nessuna + dichiarazione a orario). ET→UTC con America/New_York (DST). + CPI: bls.gov/bls/news-release/cpi.htm (archivio 2019-2026), 08:30 ET (bls.gov/schedule). + L'archivio raggruppa per anno di RIFERIMENTO: le release di gennaio/febbraio stanno sotto + l'anno prima — corrette qui (+1 anno), e la correzione e' verificata sul 2026 (13/01 e + 13/02 stanno nella schedule ufficiale 2026). + +TP01 ORARIO: posizione decisa alla chiusura giornaliera (00:00 UTC) e tenuta per le 24 barre 1h del +giorno (la stessa `held` di sleeves._tp01_returns, spalmata sulle ore); P&L orario = held × rendimento +1h; fee sul turnover giornaliero. RISK-OFF: held = 0 nelle barre 1h che intersecano [t−2h, t+2h], +con fee_side su chiusura e riapertura (|held| × 2 × fee). + + uv run python scripts/research/r0910_macro_riskoff.py +""" +from __future__ import annotations + +import sys +from datetime import datetime +from pathlib import Path +from zoneinfo import ZoneInfo + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from src.data.downloader import load_data # noqa: E402 +from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio, resample_1d # noqa: E402 + +ASSETS = ("BTC", "ETH") +ET = ZoneInfo("America/New_York") +FINESTRA_H = 2.0 +N_NULL = 1000 +SEED = 20260910 +EQUITY_OGGI = 4454.83 # trades_db --report 10/09 14:21Z, per il €/giorno +EURUSD = 1.08 # ordine di grandezza, dichiarato: serve solo per la riga in €/g + +# ------------------------------------------------------------------ calendario +FOMC_PROGRAMMATE = """ +2019-01-30 2019-03-20 2019-05-01 2019-06-19 2019-07-31 2019-09-18 2019-10-30 2019-12-11 +2020-01-29 2020-04-29 2020-06-10 2020-07-29 2020-09-16 2020-11-05 2020-12-16 +2021-01-27 2021-03-17 2021-04-28 2021-06-16 2021-07-28 2021-09-22 2021-11-03 2021-12-15 +2022-01-26 2022-03-16 2022-05-04 2022-06-15 2022-07-27 2022-09-21 2022-11-02 2022-12-14 +2023-02-01 2023-03-22 2023-05-03 2023-06-14 2023-07-26 2023-09-20 2023-11-01 2023-12-13 +2024-01-31 2024-03-20 2024-05-01 2024-06-12 2024-07-31 2024-09-18 2024-11-07 2024-12-18 +2025-01-29 2025-03-19 2025-05-07 2025-06-18 2025-07-30 2025-09-17 2025-10-29 2025-12-10 +2026-01-28 2026-03-18 2026-04-29 2026-06-17 2026-07-29 +""".split() +FOMC_EMERGENZA = [("2020-03-03", "10:00"), ("2020-03-15", "17:00")] + +# archivio BLS, ordine della pagina (per anno di riferimento): le release di gen/feb vanno +1 anno +CPI_ARCHIVIO = """ +2026-08-12 2026-07-14 2026-06-10 2026-05-12 2026-04-10 2026-03-11 2026-02-13 +2025-01-13 2025-12-18 2025-10-24 2025-09-11 2025-08-12 2025-07-15 2025-06-11 2025-05-13 2025-04-10 2025-03-12 2025-02-12 +2024-01-15 2024-12-11 2024-11-13 2024-10-10 2024-09-11 2024-08-14 2024-07-11 2024-06-12 2024-05-15 2024-04-10 2024-03-12 2024-02-13 +2023-01-11 2023-12-12 2023-11-14 2023-10-12 2023-09-13 2023-08-10 2023-07-12 2023-06-13 2023-05-10 2023-04-12 2023-03-14 +2022-02-14 2022-01-12 2022-12-13 2022-11-10 2022-10-13 2022-09-13 2022-08-10 2022-07-13 2022-06-10 2022-05-11 2022-04-12 2022-03-10 +2021-02-10 2021-01-12 2021-12-10 2021-11-10 2021-10-13 2021-09-14 2021-08-11 2021-07-13 2021-06-10 2021-05-12 2021-04-13 2021-03-10 +2020-02-10 2020-01-13 2020-12-10 2020-11-12 2020-10-13 2020-09-11 2020-08-12 2020-07-14 2020-06-10 2020-05-12 2020-04-10 2020-03-11 +2019-02-13 2019-01-14 2019-12-11 2019-11-13 2019-10-10 2019-09-12 2019-08-13 2019-07-11 2019-06-12 2019-05-10 2019-04-10 2019-03-12 +""".split() + + +def cpi_date_corrette(seq: list[str]) -> list[str]: + """Una data di gen/feb che PRECEDE (nell'ordine della pagina, discendente) una data di + marzo-dicembre dello STESSO anno e' la release dell'anno dopo.""" + out = [] + for i, d in enumerate(seq): + y, m = int(d[:4]), int(d[5:7]) + if m <= 2: + j = i + 1 + while j < len(seq) and int(seq[j][5:7]) <= 2: + j += 1 + if j < len(seq) and int(seq[j][:4]) == y: + d = f"{y + 1}{d[4:]}" + out.append(d) + return sorted(set(out)) + + +def eventi() -> pd.DataFrame: + rows = [] + for d in FOMC_PROGRAMMATE: + rows.append(("FOMC", datetime.fromisoformat(f"{d} 14:00").replace(tzinfo=ET))) + for d, hm in FOMC_EMERGENZA: + rows.append(("FOMC", datetime.fromisoformat(f"{d} {hm}").replace(tzinfo=ET))) + for d in cpi_date_corrette(CPI_ARCHIVIO): + rows.append(("CPI", datetime.fromisoformat(f"{d} 08:30").replace(tzinfo=ET))) + ev = pd.DataFrame(rows, columns=["tipo", "t_et"]) + ev["t"] = ev["t_et"].map(lambda x: pd.Timestamp(x.astimezone(ZoneInfo("UTC")))) + return ev.sort_values("t").reset_index(drop=True) + + +# ------------------------------------------------------------------ TP01 orario +def tp01_orario() -> pd.DataFrame: + """-> DataFrame orario con held_BTC, held_ETH, r_BTC, r_ETH, fee_day (fee del turnover + giornaliero, sulla prima barra del giorno).""" + tp = TrendPortfolio(**CANONICAL) + parts = {} + for a in ASSETS: + h = load_data(a, "1h") + hidx = pd.to_datetime(h["datetime"], utc=True) + r1h = pd.Series(h["close"].values.astype(float), index=hidx).pct_change().fillna(0.0) + d = resample_1d(h) + tgt = tp.target_series(d) + held = np.zeros(len(tgt)); held[1:] = tgt[:-1] + didx = pd.to_datetime(d["datetime"], utc=True).dt.floor("D") + held_d = pd.Series(held, index=didx) + turn_d = pd.Series(np.abs(np.diff(held, prepend=0.0)) * tp.fee_side, index=didx) + day_of_h = hidx.dt.floor("D") + held_h = held_d.reindex(day_of_h).values + fee_h = np.where(hidx.dt.hour.values == 0, turn_d.reindex(day_of_h).values, 0.0) + parts[a] = pd.DataFrame({f"held_{a}": held_h, f"r_{a}": r1h.values, f"fee_{a}": fee_h}, index=hidx) + J = pd.concat(parts.values(), axis=1, join="inner").dropna() + return J + + +def pnl(J: pd.DataFrame, flat_mask: np.ndarray | None, fee_side: float) -> pd.Series: + """Rendimento orario del portafoglio 50/50. `flat_mask` True = barra a esposizione zero + (con fee di chiusura all'ingresso nella finestra e di riapertura all'uscita).""" + out = np.zeros(len(J)) + for a in ASSETS: + held = J[f"held_{a}"].values.copy() + fee = J[f"fee_{a}"].values.copy() + if flat_mask is not None: + h0 = held.copy() + held = np.where(flat_mask, 0.0, held) + # transizioni dentro/fuori finestra: |Δheld| × fee, oltre al turnover giornaliero + extra = np.abs(np.diff(held, prepend=held[0])) - np.abs(np.diff(h0, prepend=h0[0])) + fee = fee + np.clip(extra, 0, None) * fee_side + out += 0.5 * (held * J[f"r_{a}"].values - fee) + return pd.Series(out, index=J.index) + + +def maschera(J: pd.DataFrame, tempi: pd.Series, w_h: float = FINESTRA_H) -> np.ndarray: + """Barre 1h (open-labeled) che intersecano [t−w, t+w] per almeno un evento.""" + idx = J.index.values.astype("datetime64[ns]") + m = np.zeros(len(J), dtype=bool) + w = np.timedelta64(int(w_h * 3600), "s") + one_h = np.timedelta64(3600, "s") + for t in tempi: + t64 = np.datetime64(pd.Timestamp(t).tz_convert("UTC").tz_localize(None)) + lo, hi = np.searchsorted(idx, t64 - w - one_h, side="right"), np.searchsorted(idx, t64 + w, side="left") + m[lo:hi] = True + return m + + +def sharpe_h(s: pd.Series) -> float: + return float(s.mean() / s.std() * np.sqrt(24 * 365.25)) if s.std() > 0 else float("nan") + + +def sharpe_d(s: pd.Series) -> float: + d = s.groupby(s.index.floor("D")).sum() + return float(d.mean() / d.std() * np.sqrt(365.25)) if d.std() > 0 else float("nan") + + +def drift_annuo(s: pd.Series) -> float: + return float(s.mean() * 24 * 365.25) + + +# ------------------------------------------------------------------ misura +def misura() -> dict: + J = tp01_orario() + fee_side = TrendPortfolio(**CANONICAL).fee_side + ev = eventi() + ev = ev[(ev["t"] >= J.index[0]) & (ev["t"] <= J.index[-1])].reset_index(drop=True) + base = pnl(J, None, fee_side) + m_all = maschera(J, ev["t"]) + ro = pnl(J, m_all, fee_side) + ro_nofee = pnl(J, m_all, 0.0) + + out = dict(n_ore=len(J), da=str(J.index[0]), a=str(J.index[-1]), n_ev=len(ev), + n_fomc=int((ev.tipo == "FOMC").sum()), n_cpi=int((ev.tipo == "CPI").sum()), + ore_finestra=int(m_all.sum()), quota_ore=float(m_all.mean()), + base=dict(sh_d=sharpe_d(base), sh_h=sharpe_h(base), drift=drift_annuo(base)), + riskoff=dict(sh_d=sharpe_d(ro), sh_h=sharpe_h(ro), drift=drift_annuo(ro)), + riskoff_nofee=dict(sh_d=sharpe_d(ro_nofee), drift=drift_annuo(ro_nofee))) + out["d_sh_d"] = out["riskoff"]["sh_d"] - out["base"]["sh_d"] + out["d_drift"] = out["riskoff"]["drift"] - out["base"]["drift"] + # cosa succede DENTRO la finestra (il P&L che il risk-off rinuncia) + dentro = base[m_all]; fuori = base[~m_all] + esposto = (J.loc[m_all, [f"held_{a}" for a in ASSETS]].abs().sum(axis=1) > 0) + out["dentro"] = dict(n=int(m_all.sum()), n_esposte=int(esposto.sum()), media_h=float(dentro.mean()), + t=float(dentro.mean() / dentro.std() * np.sqrt(len(dentro))) if dentro.std() > 0 else float("nan"), + somma=float(dentro.sum()), media_fuori_h=float(fuori.mean())) + # per tipo + out["per_tipo"] = {} + for tipo in ("FOMC", "CPI"): + m = maschera(J, ev.loc[ev.tipo == tipo, "t"]) + r = pnl(J, m, fee_side) + out["per_tipo"][tipo] = dict(n=int((ev.tipo == tipo).sum()), d_sh_d=sharpe_d(r) - out["base"]["sh_d"], + d_drift=drift_annuo(r) - out["base"]["drift"], somma_dentro=float(base[m].sum())) + # per anno (M9) + anni = {} + for y, g in base.groupby(base.index.year): + my = m_all[base.index.year == y] + ry = ro[base.index.year == y] + ney = int((ev["t"].dt.year == y).sum()) + anni[int(y)] = dict(n_ev=ney, d_sh_d=sharpe_d(ry) - sharpe_d(g), somma_dentro=float(g[my].sum()), + d_cum=float((1 + ry).prod() - (1 + g).prod())) + out["anni"] = anni + # null location-matched (M18): stessi conteggi per anno e stesse ORE del giorno, giorni a caso + rng = np.random.default_rng(SEED) + giorni_per_anno = {y: np.array(sorted(set(J.index[J.index.year == y].normalize()))) for y in J.index.year.unique()} + d_null = np.empty(N_NULL) + for k in range(N_NULL): + t_fake = [] + for y, g in ev.groupby(ev["t"].dt.year): + pool = giorni_per_anno.get(int(y)) + if pool is None or len(pool) == 0: + continue + days = rng.choice(pool, size=len(g), replace=False) + for d, t in zip(days, g["t"]): + t_fake.append(pd.Timestamp(d) + (t - t.normalize())) + mk = maschera(J, pd.Series(t_fake)) + d_null[k] = sharpe_d(pnl(J, mk, fee_side)) - out["base"]["sh_d"] + out["null"] = dict(n=N_NULL, media=float(d_null.mean()), p50=float(np.median(d_null)), + p95=float(np.percentile(d_null, 95)), p05=float(np.percentile(d_null, 5)), + pctl_vero=float((d_null < out["d_sh_d"]).mean() * 100)) + # €/giorno all'equity di oggi + out["eur_giorno"] = out["d_drift"] * EQUITY_OGGI / EURUSD / 365.25 + return out + + +def verdetto(m: dict) -> tuple[str, list[str]]: + a = m["d_sh_d"] > 0 + b = m["null"]["pctl_vero"] >= 95.0 + anni_ok = [y for y, v in m["anni"].items() if v["n_ev"] >= 5] + pos = sum(1 for y in anni_ok if m["anni"][y]["d_sh_d"] > 0) + c = anni_ok and pos / len(anni_ok) >= 0.70 + righe = [f"(a) ΔSharpe giornaliero con fee > 0: {m['d_sh_d']:+.3f} → {'PASS' if a else 'FAIL'}", + f"(b) Δ vero al ≥95° pctl del null location-matched: {m['null']['pctl_vero']:.1f}° → {'PASS' if b else 'FAIL'}", + f"(c) Δ > 0 in ≥70% degli anni con ≥5 eventi: {pos}/{len(anni_ok)} → {'PASS' if c else 'FAIL'}"] + return ("LEAD" if (a and b and c) else "REFUTED"), righe + + +def main() -> int: + print("=" * 88) + print(" r0910 — RISK-OFF ±2h intorno a FOMC e CPI su TP01 (lente oraria, posizione giornaliera)") + print("=" * 88) + m = misura() + print(f" ore: {m['n_ore']} {m['da'][:10]} → {m['a'][:10]} eventi: {m['n_ev']} " + f"(FOMC {m['n_fomc']}, CPI {m['n_cpi']}) ore in finestra: {m['ore_finestra']} = {m['quota_ore']:.2%}") + print(f"\n {'':14s} {'Sh giorn.':>10s} {'Sh orario':>10s} {'drift/anno':>11s}") + for k in ("base", "riskoff"): + v = m[k]; print(f" {k:14s} {v['sh_d']:>10.3f} {v['sh_h']:>10.3f} {v['drift']:>+11.2%}") + v = m["riskoff_nofee"]; print(f" {'riskoff nofee':14s} {v['sh_d']:>10.3f} {'':>10s} {v['drift']:>+11.2%}") + print(f"\n Δ riskoff − base: Sharpe giornaliero {m['d_sh_d']:+.3f} · drift {m['d_drift']:+.2%}/anno " + f"· ≈ {m['eur_giorno']:+.3f} €/giorno a ${EQUITY_OGGI:,.0f}") + d = m["dentro"] + print(f" dentro la finestra: {d['n']} ore ({d['n_esposte']} esposte), media {d['media_h']*1e4:+.2f} bp/h " + f"(fuori {d['media_fuori_h']*1e4:+.2f} bp/h), t = {d['t']:+.2f}, somma {d['somma']:+.2%}") + print("\n per tipo:") + for t, v in m["per_tipo"].items(): + print(f" {t:5s} n={v['n']:3d} ΔSh {v['d_sh_d']:+.3f} Δdrift {v['d_drift']:+.2%} P&L dentro {v['somma_dentro']:+.2%}") + print("\n per anno (M9):") + print(f" {'anno':>4s} {'ev':>3s} {'ΔSh':>7s} {'P&L dentro':>11s} {'Δcum':>8s}") + for y, v in m["anni"].items(): + print(f" {y:>4d} {v['n_ev']:>3d} {v['d_sh_d']:>+7.3f} {v['somma_dentro']:>+11.2%} {v['d_cum']:>+8.2%}") + n = m["null"] + print(f"\n null location-matched (M18, {n['n']} estrazioni): ΔSh mediana {n['p50']:+.3f} " + f"[p05 {n['p05']:+.3f}, p95 {n['p95']:+.3f}] — il Δ vero sta al {n['pctl_vero']:.1f}° percentile") + v, righe = verdetto(m) + print("\n VERDETTO PRE-REGISTRATO:") + for r in righe: + print(" " + r) + print(f"\n ⇒ {v}") + print("\n non modellato: funding (identico nelle due varianti salvo ~1% delle ore), slippage della " + "finestra (spread largo: se il risultato fosse positivo andrebbe rimisurato col costo vero, C1).") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())