r0910: risk-off ±2h intorno a FOMC/CPI su TP01 REFUTED 0/3 (ΔSh −0,097, −1,24%/anno; le ore dei dati sono fra le migliori)
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016kqvff47UBGeYfj1QeN4zE
This commit is contained in:
@@ -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.
|
||||||
@@ -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/
|
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 →
|
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
|
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
|
- **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
|
parallela onesta su aree inesplorate (harness `altlib`+`xsec_v2_nonmom`, tutti i gate incl. il nuovo
|
||||||
|
|||||||
@@ -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).
|
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.
|
**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`.
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
Reference in New Issue
Block a user