research(wave): ondata 3 filoni — esecuzione SKH01, DVOLSPREAD, XSR01 fuori dal crypto
Tre questioni che il progetto aveva lasciato aperte e decidibili oggi. T1 — ESECUZIONE SKH01. Testato l'unico meccanismo che non e' un cron: ordini resting on-book (TP=limit al livello per costruzione + fee maker, SL=stop-market). A offset 0 recupera il 94% del degrado, ma l'audit 02/07 aveva de-luckato i numeri headline di SKH01 e NON il degrado. Sulla banda appaiata dei 23 offset il degrado recuperabile e' +0.054 Sh di book, non -0.35 di sleeve. Errore di metodo corretto in sessione: mediana(A)-mediana(B) fra offset confronta offset diversi; serve la mediana delle DIFFERENZE appaiate. Col fix il verdetto si ribalta: solo TP a limite = +0.054 FULL / +0.061 HOLD, positivo in 19/23 e 21/23 offset; lo SL on-book PEGGIORA (mediana -0.010, positivo in 11/23) perche' cristallizza la perdita mentre l'exit ritardata incassa il rimbalzo. Raccomandazione NON eseguita: TP a limite si, SL strategico on-book no. T2 — DVOLSPREAD esce dal limbo (fermo dal 21/06). Passato ai due gate che allora non esistevano. La griglia dichiarata "72 celle" ne contiene 729: valutate tutte. Plateau reale (729/729 hold-out positivo). Selection-on-holdout confermata ma mite: cella pubblicata 83a/729 sull'hold-out, 471a/729 in-sample. Cella onesta FULL 0.68 / HOLD 0.69 (non 0.93) e DSR 0.953 PASS; la pubblicata fallisce 0.947. Promosso a forward-monitor coi parametri onesti, NON nel book (hold-out attivo 1.6 anni, DSR sul filo, weights_tilt_null mai affrontato). T3 — XSR01 non generalizza. Meccanismo congelato su 9 settoriali (1998+) e 28 ETF (30 anni), versione DEMEANATA (il test del 25/07 era a coppie). Lordo +0.24 p=0.193 e -0.14 p=0.747 vs null a fee zero. Il risultato che conta e' l'ampiezza: il demean fa 4.5->37.4 (8x) sul crypto ma 5.3->6.2 (1.2x) sulle azioni, perche' sul crypto il residuo-vs-BTC lascia un enorme fattore comune e sulle azioni il residuo-vs-SPY e' gia' indipendente. XSR01 e' crypto-specifico; NON e' falso. Soglie del gate 23/10 non toccate. Book/pesi/cron INVARIATI. Suite 245 verdi. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env python
|
||||
"""r0726_dvolspread_gate — T2 dell'ondata 2026-07-26: chiudere il lead DVOLSPREAD, in limbo
|
||||
dal 2026-06-21.
|
||||
|
||||
CHI E'. `agent_14_dvol_spread` (onda ortho, 2026-06-21) e' **l'unico sopravvissuto** al marginal
|
||||
scorer indurito: dei 18 book relative-value che facevano "ADDS" con lo scorer vecchio, l'indurito
|
||||
ne ha lasciato in piedi **1**, questo — "l'unico con edge in-sample reale". Fu lasciato in
|
||||
forward-monitor con due riserve dichiarate: *multiple-testing* e *storia DVOL corta*. Da allora
|
||||
non e' mai stato ripreso: non e' nel book, non e' in un paper monitor, non e' stato rifiutato.
|
||||
E' semplicemente rimasto li'.
|
||||
|
||||
PERCHE' RIPRENDERLO ORA. Perche' nel frattempo il progetto ha codificato **due gate che il 21/06
|
||||
non esistevano**, e sono esattamente quelli che colpiscono la sua riserva:
|
||||
|
||||
1. **`deflated_sharpe`** (29/06). Il docstring dell'agente dichiara *da se'* uno **sweep di 72
|
||||
celle** su ZWIN/LW/ZW/TGT/SVW. 72 trial sono precisamente cio' che il DSR sconta. Nel giugno
|
||||
"72/72 celle ADDS" veniva letto come prova di robustezza; il DSR dice che e' anche 72 tentativi.
|
||||
2. **`select_cell_insample` / `study_family_honest`** (29/06). Il plateau dell'agente e' descritto
|
||||
in termini di **`uplift_hold`** ("ogni vicino resta ADDS con uplift_hold in [0.34,0.39]"),
|
||||
cioe' la cella e' stata guardata SULL'HOLD-OUT. E' la firma che il gate SELECTION-ON-HOLDOUT
|
||||
e' stato scritto per catturare — lo stesso difetto che uccise il lead ERM intraday.
|
||||
|
||||
DOMANDA SECCA: scegliendo la cella con il solo in-sample, e scontando i 72 trial, DVOLSPREAD
|
||||
resta uno sleeve o era un falso positivo sopravvissuto solo perche' i gate non c'erano ancora?
|
||||
|
||||
Terza verifica, indipendente dalle prime due: la **storia DVOL** parte 2021-03, quindi il book e'
|
||||
FLAT per meta' del campione. Si misura quanto e' corto il campione ATTIVO (non quello nominale),
|
||||
perche' e' su quello che vanno letti tutti i numeri.
|
||||
|
||||
Uso: `uv run python scripts/research/r0726_dvolspread_gate.py`
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
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"))
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "research" / "ortho"))
|
||||
|
||||
import altlib as al # noqa: E402
|
||||
import ortholib as ol # noqa: E402
|
||||
|
||||
# griglia DICHIARATA dall'agente stesso (docstring PLATEAU): 72 celle
|
||||
GRID = dict(zwin=[120, 150, 180], tanh_k=[1.5, 1.8, 2.0],
|
||||
lw=[0.3, 0.45, 0.6], zw=[0.9, 1.0, 1.1],
|
||||
tgt=[0.11, 0.13, 0.17], svw=[40, 50, 60])
|
||||
CHOSEN = dict(zwin=150, tanh_k=1.8, lw=0.45, zw=1.0, tgt=0.13, svw=50)
|
||||
LEG_CAP = 0.5
|
||||
|
||||
|
||||
def make_book(zwin, tanh_k, lw, zw, tgt, svw):
|
||||
"""Fabbrica il book dell'agente a parametri arbitrari (stessa formula, nessuna variante)."""
|
||||
def _book(btc, eth):
|
||||
import warnings
|
||||
bc = btc["close"].values.astype(float)
|
||||
ec = eth["close"].values.astype(float)
|
||||
rb = ol.simple_returns(bc)
|
||||
re = ol.simple_returns(ec)
|
||||
db = al.dvol(btc, "BTC")
|
||||
de = al.dvol(eth, "ETH")
|
||||
with np.errstate(invalid="ignore", divide="ignore"):
|
||||
ldvr = np.log(db) - np.log(de)
|
||||
ldvr = np.where(np.isfinite(ldvr), ldvr, np.nan)
|
||||
level = np.tanh(tanh_k * np.nan_to_num(ldvr, nan=0.0))
|
||||
zdev = np.tanh(tanh_k * np.nan_to_num(ol.zscore(ldvr, zwin), nan=0.0))
|
||||
g_dir = np.where(np.isfinite(ldvr), lw * level + zw * zdev, 0.0)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", category=RuntimeWarning)
|
||||
spvol = ol.realized_vol(re - rb, svw, 365.25)
|
||||
scal = np.where((spvol > 0) & np.isfinite(spvol), tgt / spvol, 0.0)
|
||||
g = np.nan_to_num(np.clip(g_dir * scal, -LEG_CAP, LEG_CAP), nan=0.0)
|
||||
return g, -g
|
||||
return _book
|
||||
|
||||
|
||||
_EVAL_CACHE: dict = {}
|
||||
|
||||
|
||||
def evaluate(**kw) -> dict:
|
||||
"""Valutazione col MOTORE ORIGINALE dell'agente (ortholib.eval_book): stesso shift, stesse
|
||||
fee su entrambe le gambe, stessa serie daily. Riusarlo — invece di riscriverlo — e' cio' che
|
||||
rende questo un RE-TEST dello stesso oggetto e non di una sua approssimazione."""
|
||||
key = tuple(sorted(kw.items()))
|
||||
if key not in _EVAL_CACHE:
|
||||
_EVAL_CACHE[key] = ol.eval_book(make_book(**kw))
|
||||
return _EVAL_CACHE[key]
|
||||
|
||||
|
||||
def daily_returns(**kw) -> pd.Series:
|
||||
return evaluate(**kw)["daily"].dropna()
|
||||
|
||||
|
||||
def sh(s: pd.Series, lo=None, hi=None) -> float:
|
||||
x = s.dropna()
|
||||
if lo is not None:
|
||||
x = x[x.index >= lo]
|
||||
if hi is not None:
|
||||
x = x[x.index < hi]
|
||||
return float(x.mean() / x.std() * np.sqrt(365.25)) if len(x) > 30 and x.std() > 0 else 0.0
|
||||
|
||||
|
||||
HOLDOUT = "2025-01-01"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("=" * 100)
|
||||
print(" T2 — DVOLSPREAD: il lead del 21/06 passa i gate del 29/06?")
|
||||
print("=" * 100)
|
||||
|
||||
base = daily_returns(**CHOSEN)
|
||||
act = base[base != 0.0]
|
||||
print(f"\n campione NOMINALE: {len(base)} giorni ({base.index[0].date()} -> {base.index[-1].date()})")
|
||||
print(f" campione ATTIVO : {len(act)} giorni ({len(act)/len(base):.0%}) — "
|
||||
f"prima del 2021-03 non c'e' DVOL e il book e' FLAT")
|
||||
print(f" attivo pre-holdout {len(act[act.index < HOLDOUT])} g | "
|
||||
f"attivo hold-out {len(act[act.index >= HOLDOUT])} g "
|
||||
f"({len(act[act.index >= HOLDOUT])/365:.1f} anni)")
|
||||
|
||||
# ------------------------------------------------------- griglia dichiarata dall'agente
|
||||
print("\n" + "-" * 100)
|
||||
print(" A. LA GRIGLIA CHE L'AGENTE DICHIARA DA SE' (72 celle: ZWIN x K x LW x ZW x TGT x SVW)")
|
||||
print("-" * 100)
|
||||
import itertools
|
||||
keys = list(GRID)
|
||||
cells = []
|
||||
for combo in itertools.product(*[GRID[k] for k in keys]):
|
||||
kw = dict(zip(keys, combo))
|
||||
r = daily_returns(**kw)
|
||||
cells.append(dict(kw=kw, full=sh(r), is_=sh(r, hi=HOLDOUT), hold=sh(r, lo=HOLDOUT)))
|
||||
df = pd.DataFrame([{**c["kw"], "full": c["full"], "is": c["is_"], "hold": c["hold"]}
|
||||
for c in cells])
|
||||
print(f" celle valutate: {len(df)}")
|
||||
print(f" Sharpe FULL : min {df['full'].min():.2f} | mediana {df['full'].median():.2f} | "
|
||||
f"max {df['full'].max():.2f}")
|
||||
print(f" Sharpe HOLD : min {df['hold'].min():.2f} | mediana {df['hold'].median():.2f} | "
|
||||
f"max {df['hold'].max():.2f}")
|
||||
print(f" celle con HOLD > 0: {int((df['hold'] > 0).sum())}/{len(df)}")
|
||||
|
||||
# ------------------------------------------------------- selezione IN-SAMPLE vs hold-out
|
||||
print("\n" + "-" * 100)
|
||||
print(" B. SELECTION-ON-HOLDOUT — la cella scelta in-sample e' la stessa scelta sull'hold-out?")
|
||||
print("-" * 100)
|
||||
i_is = int(df["is"].idxmax())
|
||||
i_ho = int(df["hold"].idxmax())
|
||||
i_ch = int(df[[*CHOSEN]].eq(pd.Series(CHOSEN)).all(axis=1).idxmax())
|
||||
for lab, i in (("cella scelta IN-SAMPLE (onesta)", i_is),
|
||||
("cella scelta sull'HOLD-OUT (proibita)", i_ho),
|
||||
("cella PUBBLICATA dall'agente", i_ch)):
|
||||
row = df.loc[i]
|
||||
pars = {k: row[k] for k in CHOSEN}
|
||||
print(f" {lab:>40}: FULL {row['full']:>5.2f} IS {row['is']:>5.2f} "
|
||||
f"HOLD {row['hold']:>5.2f} {pars}")
|
||||
rank_ch_hold = int((df["hold"] > df.loc[i_ch, "hold"]).sum()) + 1
|
||||
rank_ch_is = int((df["is"] > df.loc[i_ch, "is"]).sum()) + 1
|
||||
print(f"\n rango della cella PUBBLICATA: {rank_ch_hold}/{len(df)} sull'HOLD-OUT, "
|
||||
f"{rank_ch_is}/{len(df)} sull'IN-SAMPLE")
|
||||
print(" (se e' molto meglio classificata sull'hold-out che in-sample -> selezione-sull'hold-out)")
|
||||
|
||||
# ------------------------------------------------------- deflated Sharpe sui 72 trial
|
||||
print("\n" + "-" * 100)
|
||||
print(" C. DEFLATED SHARPE sui trial DICHIARATI DALL'AGENTE STESSO")
|
||||
print("-" * 100)
|
||||
r_is = daily_returns(**{k: df.loc[i_is, k] for k in CHOSEN})
|
||||
for lab, i, r in (("cella IN-SAMPLE", i_is, r_is),
|
||||
("cella PUBBLICATA", i_ch, base)):
|
||||
dsr, sr0 = al.deflated_sharpe(sh(r), list(df["full"].values), r.dropna().values)
|
||||
verdict = "PASS" if (np.isfinite(dsr) and dsr >= 0.95) else "FAIL"
|
||||
print(f" {lab:>20}: Sharpe FULL {sh(r):>5.2f} | max atteso sotto il null "
|
||||
f"{sr0:>5.2f} | DSR {dsr:>6.3f} -> {verdict}")
|
||||
|
||||
# ------------------------------------------------------- marginale vs TP01
|
||||
print("\n" + "-" * 100)
|
||||
print(" D. MARGINALE vs TP01 (scorer indurito) — sulla cella scelta IN-SAMPLE")
|
||||
print("-" * 100)
|
||||
tp = al.tp01_baseline_daily()
|
||||
for lab, r, kwc in (("cella IN-SAMPLE", r_is, {k: df.loc[i_is, k] for k in CHOSEN}),
|
||||
("cella PUBBLICATA", base, CHOSEN)):
|
||||
J = pd.concat({"tp": tp, "c": r}, axis=1, join="inner").fillna(0.0)
|
||||
corr = float(J["tp"].corr(J["c"]))
|
||||
for w in (0.15, 0.25):
|
||||
bl = (1 - w) * J["tp"] + w * J["c"]
|
||||
print(f" {lab:>20} w={w:.0%}: corr {corr:>+5.2f} | "
|
||||
f"dSharpe FULL {sh(bl) - sh(J['tp']):>+5.2f} | "
|
||||
f"dSharpe HOLD {sh(bl, lo=HOLDOUT) - sh(J['tp'], lo=HOLDOUT):>+5.2f}")
|
||||
mg = al.marginal_vs_tp01(r)
|
||||
print(f" {'':>20} verdetto {mg['marginal_verdict']} | robust_oos={mg['robust_oos']} | "
|
||||
f"multicut={mg['multicut_persistent']} | is_hedge={mg['is_hedge']} | "
|
||||
f"insample_edge={mg['has_insample_edge']} | beats_noise={mg['beats_noise_null']}")
|
||||
print(f" {'':>20} jackknife_min_uplift {mg['jackknife_min_uplift']:+.3f} | "
|
||||
f"alpha_ann {mg['alpha_ann']:+.1%} | beta_TP01 {mg['beta_to_tp01']:+.3f} | "
|
||||
f"uplift TP01-up {mg['uplift_tp01_up']:+.3f} / TP01-down {mg['uplift_tp01_down']:+.3f}")
|
||||
|
||||
print("\n" + "=" * 100)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,346 @@
|
||||
#!/usr/bin/env python
|
||||
"""r0726_skh_onbook — T1 dell'ondata 2026-07-26: il -0.35 Sharpe del path live di SKH01
|
||||
e' recuperabile con ORDINI RESTING ON-BOOK invece dell'exit software del cron?
|
||||
|
||||
CONTESTO. Il book live Deribit (TP01 0.75 + SKH01 0.25) gira SKH01 col cron ORARIO e uscite
|
||||
SOFTWARE: il codice guarda il prezzo una volta all'ora e, se il livello SL/TP e' stato superato,
|
||||
chiude a mercato. Costo misurato (audit 2026-07-02, riconfermato il 24/07): alla lente 50/50 il
|
||||
book passa FULL 1.46 -> 1.19, HOLD 1.64 -> 1.15, DD 18% -> 25%. E' il **buco singolo piu' grande
|
||||
del progetto** (~ -0.35 di Sharpe), e il 24/07 ha gia' stabilito che **un cron piu' veloce non lo
|
||||
recupera** ("nessun cron riporta l'exit software sul livello SL dentro una barra che gappa";
|
||||
allineare la cadenza a 230m vale ~ +0.01/+0.02 Sharpe = rumore).
|
||||
|
||||
L'ANGOLO MAI TESTATO: l'unico meccanismo che non e' un cron — gli **ordini resting on-book**.
|
||||
E li' c'e' un'ASIMMETRIA che nessuno ha sfruttato:
|
||||
|
||||
* il **TP e' un LIMIT order** appoggiato sul book al livello. Se il prezzo ci arriva, il fill
|
||||
e' AL LIVELLO **per costruzione** (sei tu il maker: il prezzo non puo' attraversare il tuo
|
||||
limite senza eseguirlo). -> il lato TP e' **integralmente recuperabile**, e per giunta a
|
||||
fee MAKER invece che taker.
|
||||
* lo **SL e' uno STOP-MARKET**: al trigger diventa un ordine a mercato. In condizioni normali
|
||||
riempie a ~pochi bps dal livello; **in un gap riempie molto peggio**. -> il lato SL e'
|
||||
recuperabile solo IN PARTE, e il gap resta (com'e' giusto: e' un rischio vero, non un
|
||||
artefatto di modello).
|
||||
|
||||
Da cui l'ipotesi da falsificare: **il grosso del -0.35 e' sul lato TP, quindi e' recuperabile
|
||||
senza prendersi nessun rischio nuovo.** Se invece il grosso e' sul lato SL, la conclusione del
|
||||
24/07 ("il degrado e' strutturale") si estende anche agli ordini on-book e non c'e' niente da fare.
|
||||
|
||||
MODALITA' SIMULATE (stesse trade boundaries, cambia SOLO il prezzo d'esecuzione)
|
||||
canonical : entry a close[i], exit AL LIVELLO -> il BACKTEST (riferimento ottimista)
|
||||
hourly : entry/exit al close 5m del prossimo multiplo orario -> il PATH LIVE DI OGGI
|
||||
onbook : entry al prossimo multiplo orario (l'ingresso resta una decisione del cron:
|
||||
e' un segnale a chiusura di barra, non si puo' appoggiare in anticipo), ma:
|
||||
- TP -> fill AL LIVELLO, fee MAKER (limit resting)
|
||||
- SL -> stop-market: fill al livello se la barra 5m del trigger lo ATTRAVERSA;
|
||||
fill all'OPEN della barra 5m se la barra ci GAPPA dentro (open gia'
|
||||
oltre il livello), in entrambi i casi meno SLIPPAGE dichiarato
|
||||
- time-> resta al prossimo multiplo orario (e' il cron che decide)
|
||||
onbook_tp : contro-fattuale che ISOLA il lato TP (TP on-book, SL e time come oggi) -> dice
|
||||
quanta parte del recupero viene dal lato che NON aggiunge rischio.
|
||||
|
||||
ONESTA'
|
||||
* la rilevazione del gap e' a risoluzione **5m** (il gap vero e' piu' fine) -> il fill SL
|
||||
modellato e' un OTTIMISMO residuo su quel lato; il lato TP invece non ha questo problema
|
||||
(un limit resting non ha bisogno di risoluzione: o il prezzo tocca il livello o no).
|
||||
* si ignora la POSIZIONE IN CODA sul limit TP: a $300 di nozionale su BTC perp e' una
|
||||
semplificazione benigna (size irrilevante rispetto al book), ma e' dichiarata.
|
||||
* risultati riportati sull'off 0 E sulla **banda dei 23 offset** a priori: il progetto ha gia'
|
||||
stabilito che l'off 0 e' al 93-98 pctl (best-of-23) -> la mediana di banda e' la lente onesta.
|
||||
* niente file di produzione toccato. Se il risultato regge, la modifica al live e' una
|
||||
RACCOMANDAZIONE, non un fatto compiuto.
|
||||
|
||||
Uso: `uv run python scripts/research/r0726_skh_onbook.py` (~5-10 min: 23 offset x 2 asset)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
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"))
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
|
||||
|
||||
import r0702_anchor_skh01 as r02 # noqa: E402
|
||||
from src.portfolio.portfolio import metrics # noqa: E402
|
||||
|
||||
MSH = 3_600_000
|
||||
MS_LTF = r02.MS_LTF
|
||||
FEE_TAKER_SIDE = 0.0005 # Deribit taker ~0.05%/lato (il RT 0.10% del progetto)
|
||||
FEE_MAKER_SIDE = 0.0000 # limit resting: maker (Deribit futures ~0, a volte rebate)
|
||||
SL_SLIP = 0.0005 # slippage dichiarata sullo stop-market (5 bps)
|
||||
HOLDOUT = "2025-01-01"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- simulazione
|
||||
|
||||
def sim_equity_exec(ltf: pd.DataFrame, ent: list, mode: str,
|
||||
ts5_close: np.ndarray, o5: np.ndarray, h5: np.ndarray,
|
||||
l5: np.ndarray, c5: np.ndarray) -> np.ndarray:
|
||||
"""Come r0702.sim_equity ma con le modalita' d'esecuzione ON-BOOK.
|
||||
|
||||
Le trade boundaries (barra d'ingresso, barra di trigger, non-overlap) sono IDENTICHE al
|
||||
canonico in ogni modalita': cambia solo il prezzo a cui si esegue e la fee del lato.
|
||||
"""
|
||||
c = ltf["close"].values.astype(float)
|
||||
h = ltf["high"].values.astype(float)
|
||||
lo = ltf["low"].values.astype(float)
|
||||
n = len(c)
|
||||
close_ts = ltf["timestamp"].values.astype(np.int64) + MS_LTF
|
||||
|
||||
def px_hour(t: int) -> float:
|
||||
hb = ((t + MSH - 1) // MSH) * MSH
|
||||
j = np.searchsorted(ts5_close, hb, side="left")
|
||||
return c5[min(j, len(c5) - 1)]
|
||||
|
||||
def stop_fill(t_start: int, t_end: int, level: float, direction: int) -> float:
|
||||
"""Prezzo di fill di uno STOP-MARKET on-book fra due timestamp, a risoluzione 5m.
|
||||
|
||||
Trova la prima barra 5m che tocca il livello: se ci GAPPA dentro (open gia' oltre)
|
||||
il fill e' l'open (il gap e' un rischio vero, non un artefatto); altrimenti e' il
|
||||
livello. In entrambi i casi meno la slippage dichiarata.
|
||||
"""
|
||||
a = int(np.searchsorted(ts5_close, t_start, side="left"))
|
||||
b = int(np.searchsorted(ts5_close, t_end, side="right"))
|
||||
for j in range(a, min(b + 1, len(c5))):
|
||||
touched = (l5[j] <= level) if direction == 1 else (h5[j] >= level)
|
||||
if touched:
|
||||
gapped = (o5[j] < level) if direction == 1 else (o5[j] > level)
|
||||
px = o5[j] if gapped else level
|
||||
return px * (1.0 - SL_SLIP * direction)
|
||||
return level * (1.0 - SL_SLIP * direction) # non trovato a 5m: cap al livello
|
||||
|
||||
initial = 1000.0
|
||||
capital = initial
|
||||
equity = np.full(n, capital, dtype=float)
|
||||
busy_until = -1
|
||||
stats = dict(sl=0, tp=0, time=0, gap_sl=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_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 == "canonical" else px_hour(close_ts[i])
|
||||
fee_entry = FEE_TAKER_SIDE
|
||||
|
||||
# --- detection identica al canonico (SL prioritario)
|
||||
exit_idx = min(i + max_bars, n - 1)
|
||||
exit_lvl = c[exit_idx]
|
||||
kind = "time"
|
||||
for j in range(i + 1, min(i + max_bars + 1, n)):
|
||||
hit_sl = sl is not None and (
|
||||
(direction == 1 and lo[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 lo[j] <= tp))
|
||||
if hit_sl:
|
||||
exit_lvl, exit_idx, kind = sl, j, "sl"
|
||||
break
|
||||
if hit_tp:
|
||||
exit_lvl, exit_idx, kind = tp, j, "tp"
|
||||
break
|
||||
exit_lvl, exit_idx = c[j], j
|
||||
stats[kind] += 1
|
||||
|
||||
# --- prezzo d'uscita per modalita'
|
||||
if mode == "canonical":
|
||||
exit_price, fee_exit = exit_lvl, FEE_TAKER_SIDE
|
||||
elif mode == "hourly":
|
||||
exit_price, fee_exit = px_hour(close_ts[exit_idx]), FEE_TAKER_SIDE
|
||||
elif mode in ("onbook", "onbook_tp"):
|
||||
if kind == "tp":
|
||||
exit_price, fee_exit = tp, FEE_MAKER_SIDE # limit resting: al livello
|
||||
elif kind == "sl" and mode == "onbook":
|
||||
t0 = int(ltf["timestamp"].values[exit_idx])
|
||||
exit_price = stop_fill(t0, t0 + MS_LTF, sl, direction)
|
||||
fee_exit = FEE_TAKER_SIDE
|
||||
if (direction == 1 and exit_price < sl * (1 - SL_SLIP) - 1e-9) or \
|
||||
(direction == -1 and exit_price > sl * (1 + SL_SLIP) + 1e-9):
|
||||
stats["gap_sl"] += 1
|
||||
else: # sl in onbook_tp, o time
|
||||
exit_price, fee_exit = px_hour(close_ts[exit_idx]), FEE_TAKER_SIDE
|
||||
else:
|
||||
raise ValueError(mode)
|
||||
|
||||
gross = (exit_price - entry) / entry * direction
|
||||
net = gross - (fee_entry + fee_exit)
|
||||
capital = max(capital + capital * net, 1.0)
|
||||
equity[i:exit_idx + 1] = capital
|
||||
busy_until = exit_idx
|
||||
return equity, stats
|
||||
|
||||
|
||||
def skh_series(off: int, mode: str) -> tuple[pd.Series, dict]:
|
||||
"""Rendimenti daily dello sleeve SKH01 (50/50 BTC+ETH) a un offset e una modalita'."""
|
||||
out = {}
|
||||
agg = dict(sl=0, tp=0, time=0, gap_sl=0)
|
||||
for asset in ("BTC", "ETH"):
|
||||
_, _, ltf, ent = r02.run_asset(asset, off)
|
||||
df5 = r02.get5m(asset)
|
||||
ts5 = df5["timestamp"].values.astype(np.int64) + 300_000
|
||||
eq, st = sim_equity_exec(ltf, ent, mode, ts5,
|
||||
df5["open"].values.astype(float),
|
||||
df5["high"].values.astype(float),
|
||||
df5["low"].values.astype(float),
|
||||
df5["close"].values.astype(float))
|
||||
for k in agg:
|
||||
agg[k] += st[k]
|
||||
idx = pd.to_datetime(ltf["timestamp"].values + MS_LTF, unit="ms", utc=True)
|
||||
s = pd.Series(eq, index=idx).resample("1D").last().ffill()
|
||||
out[asset] = s.pct_change().fillna(0.0)
|
||||
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), agg
|
||||
|
||||
|
||||
def book(skh: pd.Series, w: float) -> pd.Series:
|
||||
tp = r02.al.tp01_baseline_daily()
|
||||
tp = tp.dropna()
|
||||
if tp.index.tz is None:
|
||||
tp.index = tp.index.tz_localize("UTC")
|
||||
tp.index = tp.index.floor("D")
|
||||
s = skh.copy()
|
||||
s.index = s.index.floor("D")
|
||||
J = pd.concat({"tp": tp, "skh": s}, axis=1, join="inner").fillna(0.0)
|
||||
return pd.Series((1 - w) * J["tp"].values + w * J["skh"].values, index=J.index)
|
||||
|
||||
|
||||
def sh3(s: pd.Series) -> tuple[float, float, float, float]:
|
||||
m_all = metrics(s)
|
||||
m_is = metrics(s[s.index < HOLDOUT])
|
||||
m_ho = metrics(s[s.index >= HOLDOUT])
|
||||
return m_all["sharpe"], m_is["sharpe"], m_ho["sharpe"], m_all["maxdd"]
|
||||
|
||||
|
||||
MODES = ("canonical", "hourly", "onbook_tp", "onbook")
|
||||
LABEL = {"canonical": "canonical (backtest, fill-al-livello)",
|
||||
"hourly": "hourly (PATH LIVE DI OGGI)",
|
||||
"onbook_tp": "onbook_tp (solo TP a limite)",
|
||||
"onbook": "onbook (TP limite + SL stop-market)"}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
global SL_SLIP
|
||||
print("=" * 100)
|
||||
print(" T1 — SKH01: il degrado del path live e' recuperabile con ordini RESTING ON-BOOK?")
|
||||
print("=" * 100)
|
||||
print(f"\n fee: taker {FEE_TAKER_SIDE:.4%}/lato, maker {FEE_MAKER_SIDE:.4%}/lato | "
|
||||
f"slippage stop-market {SL_SLIP:.2%} | hold-out {HOLDOUT}")
|
||||
|
||||
# ------------------------------------------------ offset 0 (la griglia che gira davvero)
|
||||
print("\n" + "-" * 100)
|
||||
print(" A. OFFSET 0 — la griglia che gira davvero nel live")
|
||||
print("-" * 100)
|
||||
ser0 = {}
|
||||
print(f" {'modalita':>40} {'SKH standalone':>28} {'book 75/25':>26}")
|
||||
print(f" {'':>40} {'FULL':>6} {'IS':>6} {'HOLD':>6} {'DD':>6} "
|
||||
f"{'FULL':>6} {'IS':>6} {'HOLD':>6} {'DD':>6}")
|
||||
stats0 = {}
|
||||
for m in MODES:
|
||||
s, st = skh_series(0, m)
|
||||
ser0[m] = s
|
||||
stats0[m] = st
|
||||
a = sh3(s)
|
||||
b = sh3(book(s, 0.25))
|
||||
print(f" {LABEL[m]:>40} {a[0]:>6.2f} {a[1]:>6.2f} {a[2]:>6.2f} {a[3]:>5.1%} "
|
||||
f"{b[0]:>6.2f} {b[1]:>6.2f} {b[2]:>6.2f} {b[3]:>5.1%}")
|
||||
st = stats0["onbook"]
|
||||
tot = st["sl"] + st["tp"] + st["time"]
|
||||
print(f"\n composizione delle uscite (off 0, BTC+ETH): {tot} trade — "
|
||||
f"SL {st['sl']} ({st['sl']/tot:.0%}) | TP {st['tp']} ({st['tp']/tot:.0%}) | "
|
||||
f"time {st['time']} ({st['time']/tot:.0%})")
|
||||
print(f" di cui SL con GAP dentro la barra 5m (fill peggiore del livello): "
|
||||
f"{st['gap_sl']} = {st['gap_sl']/max(st['sl'],1):.0%} degli SL")
|
||||
|
||||
# ------------------------------------------------ scomposizione del recupero
|
||||
print("\n" + "-" * 100)
|
||||
print(" B. DOVE VIVE IL DEGRADO — scomposizione del gap canonical -> live")
|
||||
print("-" * 100)
|
||||
b_can, b_hr = sh3(book(ser0["canonical"], 0.25)), sh3(book(ser0["hourly"], 0.25))
|
||||
b_tp, b_ob = sh3(book(ser0["onbook_tp"], 0.25)), sh3(book(ser0["onbook"], 0.25))
|
||||
gap_tot = b_can[0] - b_hr[0]
|
||||
print(f" book 75/25, Sharpe FULL: canonical {b_can[0]:.3f} -> live {b_hr[0]:.3f} "
|
||||
f"(degrado {gap_tot:+.3f})")
|
||||
if abs(gap_tot) > 1e-9:
|
||||
print(f" recupero col solo TP a limite : {b_tp[0]-b_hr[0]:+.3f} "
|
||||
f"= {(b_tp[0]-b_hr[0])/gap_tot:>5.0%} del degrado [NESSUN rischio nuovo]")
|
||||
print(f" recupero TP + SL on-book : {b_ob[0]-b_hr[0]:+.3f} "
|
||||
f"= {(b_ob[0]-b_hr[0])/gap_tot:>5.0%} del degrado")
|
||||
print(f" residuo NON recuperabile : {b_can[0]-b_ob[0]:+.3f} "
|
||||
f"= {(b_can[0]-b_ob[0])/gap_tot:>5.0%} del degrado [gap + entry del cron]")
|
||||
|
||||
# ------------------------------------------------ banda dei 23 offset (la lente onesta)
|
||||
print("\n" + "-" * 100)
|
||||
print(" C. BANDA DEI 23 OFFSET — la lente de-luckata (off 0 e' al 93-98 pctl, audit 02/07)")
|
||||
print("-" * 100)
|
||||
rows = {m: [] for m in MODES}
|
||||
for off in r02.OFFSETS:
|
||||
for m in MODES:
|
||||
s, _ = skh_series(off, m)
|
||||
rows[m].append(sh3(book(s, 0.25)))
|
||||
print(f" {'modalita':>40} {'FULL med':>9} {'IS med':>8} {'HOLD med':>9} "
|
||||
f"{'DD med':>7} {'HOLD min':>9} {'HOLD max':>9}")
|
||||
med = {}
|
||||
A = {m: np.array(rows[m]) for m in MODES}
|
||||
for m in MODES:
|
||||
med[m] = np.median(A[m], axis=0)
|
||||
print(f" {LABEL[m]:>40} {med[m][0]:>9.2f} {med[m][1]:>8.2f} {med[m][2]:>9.2f} "
|
||||
f"{med[m][3]:>6.1%} {A[m][:, 2].min():>9.2f} {A[m][:, 2].max():>9.2f}")
|
||||
|
||||
# ⚠️ gli offset sono coppie APPAIATE: la statistica giusta e' la mediana delle DIFFERENZE
|
||||
# per-offset, non la differenza delle mediane (che confronta offset diversi fra loro).
|
||||
print("\n differenze APPAIATE per-offset (mediana [min, max] sui 23 offset):")
|
||||
print(f" {'':>40} {'dFULL':>22} {'dHOLD':>22} {'dDD':>20}")
|
||||
for m in ("canonical", "onbook_tp", "onbook"):
|
||||
d0 = A[m][:, 0] - A["hourly"][:, 0]
|
||||
d2 = A[m][:, 2] - A["hourly"][:, 2]
|
||||
d3 = A[m][:, 3] - A["hourly"][:, 3]
|
||||
print(f" {LABEL[m]:>40} "
|
||||
f"{np.median(d0):>+7.3f} [{d0.min():+.2f},{d0.max():+.2f}] "
|
||||
f"{np.median(d2):>+7.3f} [{d2.min():+.2f},{d2.max():+.2f}] "
|
||||
f"{np.median(d3):>+6.2%} [{d3.min():+.1%},{d3.max():+.1%}]")
|
||||
|
||||
dmax = np.median(A["canonical"][:, 0] - A["hourly"][:, 0])
|
||||
dob = np.median(A["onbook"][:, 0] - A["hourly"][:, 0])
|
||||
dtp = np.median(A["onbook_tp"][:, 0] - A["hourly"][:, 0])
|
||||
print(f"\n degrado recuperabile al massimo (canonical-hourly, mediana appaiata): {dmax:+.3f} FULL")
|
||||
if abs(dmax) > 1e-9:
|
||||
print(f" recuperato dal solo TP a limite: {dtp:+.3f} = {dtp/dmax:>5.0%}")
|
||||
print(f" recuperato da TP + SL on-book : {dob:+.3f} = {dob/dmax:>5.0%}")
|
||||
print("\n in quanti dei 23 offset la modalita' MIGLIORA il path live di oggi?")
|
||||
for m in ("onbook_tp", "onbook"):
|
||||
nf = int((A[m][:, 0] - A["hourly"][:, 0] > 0).sum())
|
||||
nh = int((A[m][:, 2] - A["hourly"][:, 2] > 0).sum())
|
||||
nd = int((A[m][:, 3] - A["hourly"][:, 3] < 0).sum())
|
||||
print(f" {LABEL[m]:>40}: FULL {nf}/23 HOLD {nh}/23 DD {nd}/23")
|
||||
# il lato SL da solo = differenza fra onbook (TP+SL) e onbook_tp (solo TP)
|
||||
dsl = A["onbook"][:, 0] - A["onbook_tp"][:, 0]
|
||||
print(f"\n contributo del SOLO lato SL on-book (onbook - onbook_tp), FULL: "
|
||||
f"mediana {np.median(dsl):+.3f} [{dsl.min():+.2f},{dsl.max():+.2f}], "
|
||||
f"positivo in {int((dsl > 0).sum())}/23 offset")
|
||||
|
||||
# ------------------------------------------------ robustezza alla slippage assunta
|
||||
print("\n" + "-" * 100)
|
||||
print(" D. SENSIBILITA' ALLA SLIPPAGE ASSUNTA sullo stop-market (off 0, book 75/25)")
|
||||
print("-" * 100)
|
||||
keep = SL_SLIP
|
||||
print(f" {'slippage':>10} {'FULL':>7} {'HOLD':>7} {'DD':>7} (live di oggi: "
|
||||
f"FULL {b_hr[0]:.2f} / HOLD {b_hr[2]:.2f})")
|
||||
for slip in (0.0000, 0.0005, 0.0020, 0.0050, 0.0100):
|
||||
SL_SLIP = slip
|
||||
s, _ = skh_series(0, "onbook")
|
||||
r = sh3(book(s, 0.25))
|
||||
flag = "" if r[0] > b_hr[0] else " <- peggio del live"
|
||||
print(f" {slip:>9.2%} {r[0]:>7.2f} {r[2]:>7.2f} {r[3]:>6.1%}{flag}")
|
||||
SL_SLIP = keep
|
||||
print("\n" + "=" * 100)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python
|
||||
"""r0726_xsr_equity — T3 dell'ondata 2026-07-26: XSR01 regge fuori dal crypto e fuori dai
|
||||
2.6 anni monoregime? Test di FALSIFICAZIONE su 26+ anni di ETF settoriali.
|
||||
|
||||
PERCHE'. XSR01 (candidato in forward-monitor dal 25/07, gate pre-registrato al 2026-10-23) ha
|
||||
superato tutto quello che gli e' stato chiesto — marginale ADDS, deflated-Sharpe 0.985, corr ~0
|
||||
a tutti e 5 gli sleeve, tre null a fee-neutrale — MA la sua debolezza numero uno e' dichiarata:
|
||||
|
||||
"storia 2.6 anni MONOREGIME e CRESCENTE (Sharpe 2024 1.03 / 2025 1.98 / 2026 3.11):
|
||||
l'edge e' recente."
|
||||
|
||||
Con 2.6 anni non si distingue un meccanismo reale da un regime fortunato. L'unico modo di
|
||||
guadagnare potenza statistica senza aspettare anni e' portare il meccanismo CONGELATO su un
|
||||
pannello **diverso per asset class e lungo decenni**, e vedere se sopravvive. Se regge su 26 anni
|
||||
di settoriali US, l'ipotesi "e' un artefatto del bear alt 2024-2026" e' molto piu' difficile da
|
||||
sostenere. Se non regge, il gate del 23/10 deve saperlo.
|
||||
|
||||
NON E' IL TEST DEL 25/07. `r0725_statarb_eq.py` ha gia' portato il meccanismo sulle azioni ed e'
|
||||
stato SCARTATO (paniere Sharpe -1.00 netta, -0.17 lorda) — ma quello era il meccanismo a **COPPIE**
|
||||
(12 coppie a priori, ampiezza effettiva 9.9). XSR01 non e' un paniere di coppie: e' la versione
|
||||
**DEMEANATA cross-sezionalmente**, ed e' proprio il demeaning che ha cambiato tutto (ampiezza
|
||||
effettiva 4.5 -> 37.4) annullando algebricamente la gamba comune. Testare il demean su equity e'
|
||||
quindi una domanda DIVERSA, mai posta. Il risultato lordo del 25/07 (-0.17, cioe' il residuo
|
||||
REVERTE debolmente sulle azioni mentre CONTINUA sul crypto) e' l'ipotesi nulla di questo test.
|
||||
|
||||
DISEGNO
|
||||
* meccanismo CONGELATO importato da r0725_statarb_multi (W=45, sgn=+1, residuo OLS causale su
|
||||
un fattore comune, z-score, tanh, vol-target 20%, cap 2x) — nessuna reimplementazione;
|
||||
* demeaning cross-sezionale giornaliero, identico a r0725_statarb_basket_gate;
|
||||
* fattore comune = **SPY** (analogo di BTC: il fattore che il demeaning deve annullare);
|
||||
* due universi, entrambi a priori:
|
||||
SECT9 = i 9 settoriali SPDR classici (1998+, ~26 anni) — cross-section OMOGENEA, il
|
||||
parallelo piu' stretto degli alt (un solo fattore comune forte);
|
||||
ALL28 = tutti gli ETF su disco tranne SPY — parallelo del "ALL50" di XSR01;
|
||||
* split NON aggiustati riparati in lettura (eqlib.load_eq -> src/data/eq_splits) — senza questo
|
||||
IWM/EFA avvelenerebbero il pannello (difetto trovato il 25/07);
|
||||
* LORDO **e** netto: la lettura del 25/07 e' che sulle azioni il segno vive nel lordo e il
|
||||
turnover se lo mangia. Un venue a PAVIMENTO FISSO (IB) e' anche peggio di una fee
|
||||
proporzionale a questa taglia (lezione 25/07) -> il netto proporzionale e' gia' generoso;
|
||||
* null di permutazione cross-sezionale **A FEE ZERO** (lezione XSR01: permutare un segnale ne
|
||||
fa esplodere il turnover, quindi un null a fee piena perderebbe per COSTO invece che per
|
||||
assenza d'informazione, e regalerebbe un p-value trionfale e falso);
|
||||
* decomposizione per DECENNIO: 26 anni servono a questo, non a fare una media unica.
|
||||
|
||||
Uso: `uv run python scripts/research/r0726_xsr_equity.py`
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
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"))
|
||||
|
||||
from eqlib import SECTORS_CLASSIC, load_eq # noqa: E402
|
||||
from r0725_statarb_multi import (FEE_LEG, SGN, W, _dd, _sh, signal) # noqa: E402
|
||||
|
||||
RAW = ROOT / "data" / "raw"
|
||||
BASE = "SPY"
|
||||
MIN_BARS = 400
|
||||
N_PERM = 300
|
||||
BLOCK = 20
|
||||
SEED = 20260726
|
||||
ANN = np.sqrt(252.0) # ⚠️ giorni di BORSA, non 365 (lezione 25/07 sull'annualizzazione)
|
||||
|
||||
|
||||
def all_symbols() -> list[str]:
|
||||
out = []
|
||||
for p in sorted(RAW.glob("eq_*_1d.parquet")):
|
||||
s = p.stem.replace("eq_", "").replace("_1d", "").upper()
|
||||
if s != BASE:
|
||||
out.append(s)
|
||||
return out
|
||||
|
||||
|
||||
def _px(sym: str) -> pd.Series:
|
||||
s = load_eq(sym)["close"].astype(float)
|
||||
return s[~s.index.duplicated(keep="last")].sort_index()
|
||||
|
||||
|
||||
def build(cols: list[str]) -> tuple[pd.DataFrame, pd.DataFrame]:
|
||||
"""Posizioni e spread per ogni gamba, col meccanismo congelato vs SPY."""
|
||||
base = _px(BASE)
|
||||
P, S = {}, {}
|
||||
for sym in cols:
|
||||
try:
|
||||
t = _px(sym)
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
ix = base.index.intersection(t.index)
|
||||
if len(ix) < MIN_BARS:
|
||||
continue
|
||||
p, s = signal(base[ix], t[ix])
|
||||
P[sym] = pd.Series(p, index=ix)
|
||||
S[sym] = pd.Series(s, index=ix)
|
||||
return (pd.concat(P, axis=1, sort=True).sort_index(),
|
||||
pd.concat(S, axis=1, sort=True).sort_index())
|
||||
|
||||
|
||||
def pnl_fee(pos: np.ndarray, spread: np.ndarray, fee_leg: float) -> np.ndarray:
|
||||
held = np.concatenate([[0.0], pos[:-1]])
|
||||
turn = np.abs(np.diff(held, prepend=0.0))
|
||||
return held * spread - 2.0 * fee_leg * turn
|
||||
|
||||
|
||||
def basket(P: pd.DataFrame, S: pd.DataFrame, demean: bool, fee_leg: float) -> pd.Series:
|
||||
Q = P.sub(P.mean(axis=1), axis=0) if demean else P
|
||||
out = {c: pd.Series(pnl_fee(Q[c].to_numpy(float), S[c].to_numpy(float), fee_leg), index=Q.index)
|
||||
for c in Q.columns}
|
||||
return pd.concat(out, axis=1, sort=True).mean(axis=1, skipna=True).dropna()
|
||||
|
||||
|
||||
def eff_breadth(P: pd.DataFrame, S: pd.DataFrame, demean: bool) -> float:
|
||||
Q = P.sub(P.mean(axis=1), axis=0) if demean else P
|
||||
M = pd.concat({c: pd.Series(pnl_fee(Q[c].to_numpy(float), S[c].to_numpy(float), 0.0),
|
||||
index=Q.index) for c in Q.columns}, axis=1)
|
||||
C = M.corr().values
|
||||
n = C.shape[0]
|
||||
off = (C.sum() - np.trace(C)) / (n * (n - 1))
|
||||
return float(n / (1.0 + (n - 1) * off))
|
||||
|
||||
|
||||
def sh(r) -> float:
|
||||
r = np.asarray(r, float)
|
||||
r = r[np.isfinite(r)]
|
||||
return float(r.mean() / r.std() * ANN) if len(r) > 30 and r.std() > 0 else 0.0
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("=" * 100)
|
||||
print(" T3 — XSR01 fuori dal crypto: il meccanismo congelato su 26 anni di ETF settoriali")
|
||||
print("=" * 100)
|
||||
print(f"\n meccanismo: W={W}, sgn={SGN:+d}, residuo OLS causale vs {BASE}, z-score, tanh,")
|
||||
print(" vol-target 20%, cap 2x, demean cross-sezionale giornaliero [CONGELATO, importato]")
|
||||
print(f" annualizzazione: sqrt(252) = giorni di BORSA (NON 365)")
|
||||
|
||||
universes = {
|
||||
"SECT9 9 settoriali SPDR (1998+)": SECTORS_CLASSIC,
|
||||
"ALL28 tutti gli ETF su disco": all_symbols(),
|
||||
}
|
||||
|
||||
store = {}
|
||||
for name, cols in universes.items():
|
||||
P, S = build(cols)
|
||||
store[name] = (P, S)
|
||||
print("\n" + "-" * 100)
|
||||
print(f" {name} — {P.shape[1]} gambe, {len(P)} barre, "
|
||||
f"{P.index[0].date()} -> {P.index[-1].date()}")
|
||||
print("-" * 100)
|
||||
print(f" {'variante':>28} {'ampiezza eff.':>14} {'Sharpe LORDA':>13} "
|
||||
f"{'Sharpe NETTA':>13} {'maxDD':>8} {'ret/anno':>9}")
|
||||
for lab, dm in (("coppie (no demean)", False), ("DEMEAN (= XSR01)", True)):
|
||||
g = basket(P, S, dm, 0.0)
|
||||
n = basket(P, S, dm, FEE_LEG)
|
||||
eb = eff_breadth(P, S, dm)
|
||||
cagr = float(np.nanmean(n.values)) * 252.0
|
||||
print(f" {lab:>28} {eb:>14.1f} {sh(g):>13.2f} {sh(n):>13.2f} "
|
||||
f"{_dd(n.values):>7.1%} {cagr:>8.1%}")
|
||||
|
||||
# ------------------------------------------------------------------ per decennio
|
||||
print("\n" + "-" * 100)
|
||||
print(" A. PER DECENNIO — 26 anni servono a questo (variante DEMEAN, la stessa di XSR01)")
|
||||
print("-" * 100)
|
||||
print(f" {'universo':>34} {'periodo':>14} {'anni':>5} {'Sh LORDA':>9} {'Sh NETTA':>9} {'giorni':>7}")
|
||||
for name, (P, S) in store.items():
|
||||
g = basket(P, S, True, 0.0)
|
||||
n = basket(P, S, True, FEE_LEG)
|
||||
for lo, hi in (("1998", "2005"), ("2005", "2012"), ("2012", "2019"), ("2019", "2027")):
|
||||
kg = (g.index >= lo) & (g.index < hi)
|
||||
kn = (n.index >= lo) & (n.index < hi)
|
||||
if kg.sum() < 250:
|
||||
continue
|
||||
print(f" {name.split()[0]:>34} {lo + '-' + hi:>14} {kg.sum() / 252:>5.1f} "
|
||||
f"{sh(g[kg]):>9.2f} {sh(n[kn]):>9.2f} {int(kg.sum()):>7d}")
|
||||
|
||||
# ------------------------------------------------------------------ null a fee ZERO
|
||||
print("\n" + "-" * 100)
|
||||
print(" B. NULL DI PERMUTAZIONE CROSS-SEZIONALE — a FEE ZERO (lezione XSR01)")
|
||||
print("-" * 100)
|
||||
print(" Permutare un segnale ne fa esplodere il turnover: a fee piena il null perderebbe")
|
||||
print(" per COSTO invece che per assenza d'informazione -> p-value trionfale e falso.")
|
||||
rng = np.random.default_rng(SEED)
|
||||
print(f"\n {'universo':>34} {'candidato LORDO':>16} {'null medio':>11} {'null p95':>9} "
|
||||
f"{'null max':>9} {'p-value':>8}")
|
||||
for name, (P, S) in store.items():
|
||||
cand = sh(basket(P, S, True, 0.0))
|
||||
Pv = P.to_numpy(float)
|
||||
draws = []
|
||||
for _ in range(N_PERM):
|
||||
idx = rng.permutation(Pv.shape[1])
|
||||
Pp = pd.DataFrame(Pv[:, idx], index=P.index, columns=P.columns)
|
||||
draws.append(sh(basket(Pp, S, True, 0.0)))
|
||||
d = np.array(draws)
|
||||
pval = float((d >= cand).mean())
|
||||
print(f" {name.split()[0]:>34} {cand:>16.2f} {d.mean():>11.2f} "
|
||||
f"{np.percentile(d, 95):>9.2f} {d.max():>9.2f} {pval:>8.3f}")
|
||||
|
||||
# ------------------------------------------------------------------ confronto col crypto
|
||||
print("\n" + "-" * 100)
|
||||
print(" C. CONFRONTO DIRETTO col candidato crypto (numeri del 25/07, non ricalcolati)")
|
||||
print("-" * 100)
|
||||
print(" XSR01 (50 alt HL, 2.6 anni): ampiezza eff. 37.4 | LORDA 2.70 | NETTA 1.82 | DD -2.6%")
|
||||
print(" STATARB-EQ a COPPIE (25/07): ampiezza eff. 9.9 | LORDA -0.17 | NETTA -1.00")
|
||||
print("\n La domanda del test: il DEMEAN — che sul crypto ha fatto 4.5 -> 37.4 di ampiezza e")
|
||||
print(" ha creato l'edge — fa la stessa cosa sulle azioni, o li' il residuo resta reverting?")
|
||||
print("\n" + "=" * 100)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user