research(anchor-audit): timing-luck confermato su XS01 e SKH01 — 3/3 sleeve ancorati, book de-luckato HOLD ~2.0
Chiude il pendente dell'ondata timing 2026-07-02. Due audit indipendenti (sanity replica bit-exact, ancore a priori, zero tuning per-fase, bootstrap): - XS01 (10 fasi ciclo H=10): fortuna nel DD (15° pctl: 10.8% vs 15.5% tipico, 29% peggiore) e FULL (85°), non nell'hold-out (65°); P(spike)~0.91-0.94. Lens onesta = ensemble di fase FULL 1.25 / HOLD 1.31 / DD 11%. Ammissione @15% regge, i numeri 1.50/1.71/11% no. - SKH01 (23 offset griglia 230m/690m): canonico = 93-98° pctl di OGNI metrica, minHold/blend/book-HOLD = massimo dei 23; il gate DD<30% (criterio di selezione V2-DD) fallisce in 15/23 offset. Regge: uplift blend positivo a tutte le 23 fasi (min +0.18) + corr ~0.08 -> ADDS ridimensionato. Path live reale (cron orario + exit software): book FULL 1.46->1.19 / HOLD 1.64->1.15 / DD 18->25%, gap-through-stop nei crash (sl2% -> -11/-23%). - Book 5-sleeve: HOLD 2.46 eredita ~+0.10/+0.17/+0.5 di fortuna d'ancora (TP01/XS01/SKH01) -> stima de-luckata HOLD ~1.9-2.1, FULL ~2.0-2.2, DD ~6%. Nessun cambio operativo (pesi/book live invariati; ogni cambio passa weights_tilt_null). Narrativa aggiornata (CLAUDE.md, docstring skyhook). Follow-up: anchor_luck_band() in altlib, cadenza 230m, peso SKH live. 168 test verdi. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,513 @@
|
||||
#!/usr/bin/env python
|
||||
"""r0702_anchor_skh01.py — AUDIT timing-luck della FASE della griglia dual-TF di SKH01-V2-DD.
|
||||
|
||||
CONTESTO (diario 2026-07-02-timing-crt-wave.md): l'hold-out di TP01 (Sharpe 0.31) si e'
|
||||
rivelato la MIGLIORE delle 24 ancore orarie possibili (mediana 0.04) — timing-luck
|
||||
dell'ancora, dimensione di multiple-testing non contata dal deflated-Sharpe. SKH01 e' nel
|
||||
BOOK LIVE Deribit (TP01+SKH01 75/25) -> audit analogo, con rilevanza operativa diretta.
|
||||
|
||||
SPAZIO DI LUCK: l'origine della griglia. SKH01 resampla il 5m certificato a 230m (exec) e
|
||||
690m (segnale) con origin='epoch'. Shiftando l'origin di k x 5m si spostano COERENTEMENTE
|
||||
entrambe le griglie (690 = 3 x 230 -> i confini HTF restano sottoinsieme dei LTF); la
|
||||
struttura congiunta si ripete con periodo 690m = 138 step da 5m. Campioniamo 23 OFFSET
|
||||
UNIFORMI (ogni 30m) su [0, 690), dichiarati A PRIORI — offset 0 = canonico. Parametri
|
||||
IDENTICI (SKH01_V2_DD) su tutti gli offset; nessuna selezione.
|
||||
|
||||
NB strutturale: 230m e 690m NON dividono 24h -> la griglia MIGRA attraverso la giornata
|
||||
(nessun offset "possiede" un'ora del giorno) — l'ancora e' meno "speciale" a priori di
|
||||
quella daily di TP01, ma va misurato.
|
||||
|
||||
COSA FA:
|
||||
1. SANITY: a offset 0 riproduce ESATTAMENTE la serie daily di _skyhook_returns()
|
||||
(max|dif| ~0) e i confini HTF c LTF per ogni offset.
|
||||
2. Per 23 offset x {BTC,ETH}: Sharpe FULL/IS/HOLD (equity daily-step, convenzione
|
||||
canonica), maxDD (equity harness), n trade -> tabella + min/med/max + pctl di off 0.
|
||||
3. GATE ammissione ri-valutati alla mediana/peggiore: (a) maxDD<30% entrambi gli asset,
|
||||
(b) minFull ~0.99 / minHold ~1.26, (c) blend 0.75*TP01+0.25*SKH hold-out (baseline
|
||||
al.tp01_baseline_daily; claim 0.31->1.17), (d) corr a TP01 (~0.09).
|
||||
4. BOOK 5-sleeve 33/15/12/20/20 (combine_outer, outer-join rinormalizzato, era crypto):
|
||||
HOLD/FULL con SKH a ogni offset + ensemble degli offset (lens de-luckato, NON eseguibile).
|
||||
5. BOOTSTRAP (block ~20g, maniera scettico r0702_skeptic_offset): P(un offset qualsiasi
|
||||
mostri uno spike >= quello del canonico sull'hold-out del blend).
|
||||
6. RILEVANZA LIVE: il cron del book e' ORARIO (crontab 0 * * * * -> scripts/cron_book.sh)
|
||||
ma i confini 230m NON sono allineati all'ora (230 mod 60 = 50 -> ciclo di 6 barre,
|
||||
ritardo confine->prossima-ora in {0,10,20,30,40,50} min). Quantifica la distribuzione
|
||||
e l'impatto: re-sim con fill al close 5m del PROSSIMO multiplo orario dopo la chiusura
|
||||
della barra di segnale/exit (entry E exit software ritardati; detection dei trade
|
||||
INVARIATA -> isola il puro effetto prezzo-esecuzione). Sanity: la modalita' canonica
|
||||
della re-sim riproduce backtest_signals bit-exact.
|
||||
|
||||
CAVEAT DICHIARATI:
|
||||
- equity daily-step (lens Sharpe) come il canonico — non e' mark-to-market intrabar;
|
||||
- costi di ribilanciamento del book 230m gia' flaggati a deploy (diario skyhook);
|
||||
- l'ensemble di offset NON e' eseguibile live (una sola griglia gira): serve SOLO come
|
||||
stima de-luckata;
|
||||
- la sim 'hourly' modella fill a close 5m del multiplo orario (cron a minuto 0, runtime
|
||||
del job trascurato) e NON modella slippage/parziali.
|
||||
|
||||
TECNICA: mai .view('int64') su tz-aware (epoca esplicita in ms); htf_features/merge_htf_to_ltf
|
||||
RIUSATE via skyhook_entries (importate, non riscritte). Vincoli: nessun file toccato fuori da
|
||||
questo script; niente commit. Runtime ~3-6 min (46 run skyhook + book + bootstrap).
|
||||
"""
|
||||
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.backtest.harness import backtest_signals # noqa: E402
|
||||
from src.data.downloader import load_data # noqa: E402
|
||||
from src.portfolio.portfolio import combine_outer, to_daily # noqa: E402
|
||||
from src.strategies.skyhook import ( # noqa: E402
|
||||
HTF_MIN, LTF_MIN, SKH01_V2_DD, skyhook_entries)
|
||||
|
||||
HOLDOUT = al.HOLDOUT
|
||||
ASSETS = ("BTC", "ETH")
|
||||
OFFSETS = tuple(range(0, 690, 30)) # 23 offset a priori (ogni 30m su [0,690)), 0 = canonico
|
||||
MS5 = 300_000
|
||||
MSH = 3_600_000
|
||||
MS_LTF = LTF_MIN * 60_000
|
||||
BLEND_W = {"TP": 0.75, "SKH": 0.25}
|
||||
BOOK_W = {"TP": 0.33, "XS": 0.15, "VRP": 0.12, "SKH": 0.20, "GTAA": 0.20}
|
||||
B_BOOT = 4000
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dati + resample con fase spostata (identico a skyhook.resample_5m + offset)
|
||||
# ---------------------------------------------------------------------------
|
||||
@lru_cache(maxsize=4)
|
||||
def get5m(asset: str) -> pd.DataFrame:
|
||||
df = load_data(asset, "5m").reset_index(drop=True)
|
||||
if "datetime" not in df.columns:
|
||||
df["datetime"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
return df
|
||||
|
||||
|
||||
def resample_off(df5: pd.DataFrame, minutes: int, off: int) -> pd.DataFrame:
|
||||
"""Identico a skyhook.resample_5m ma con griglia spostata di `off` minuti:
|
||||
origin='epoch' + offset -> confini a epoch + off + n*minutes. Con `off` comune a
|
||||
230m e 690m (690=3x230) i confini HTF restano sottoinsieme dei confini LTF."""
|
||||
g = df5[["timestamp", "open", "high", "low", "close", "volume"]].copy()
|
||||
g.index = pd.to_datetime(g["timestamp"], unit="ms", utc=True)
|
||||
out = (g.resample(f"{minutes}min", label="left", closed="left", origin="epoch",
|
||||
offset=pd.Timedelta(minutes=off))
|
||||
.agg({"open": "first", "high": "max", "low": "min",
|
||||
"close": "last", "volume": "sum"})
|
||||
.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"]]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Run per (asset, offset) — pipeline IDENTICA a sleeves._skyhook_returns
|
||||
# ---------------------------------------------------------------------------
|
||||
_CACHE: dict = {}
|
||||
|
||||
|
||||
def run_asset(asset: str, off: int):
|
||||
"""(daily equity-step series, Metrics, ltf, entries) per un asset a fase `off`."""
|
||||
key = (asset, off)
|
||||
if key in _CACHE:
|
||||
return _CACHE[key]
|
||||
df5 = get5m(asset)
|
||||
ltf = resample_off(df5, LTF_MIN, off)
|
||||
htf = resample_off(df5, HTF_MIN, off)
|
||||
# confini HTF c confini LTF: vale per TUTTE le barre tranne al piu' la PRIMA (parziale:
|
||||
# se il feed parte a meta' di un bin 690m il sub-bin 230m con la stessa label puo' essere
|
||||
# vuoto). Identico al comportamento del canonico build_frames (origin='epoch').
|
||||
assert np.isin(htf["timestamp"].values[1:], ltf["timestamp"].values).all(), \
|
||||
f"confini HTF NON sottoinsieme dei LTF (asset={asset}, off={off})"
|
||||
ent = skyhook_entries(ltf, htf, SKH01_V2_DD) # riusa htf_features/merge_htf_to_ltf
|
||||
m = backtest_signals(ltf, ent, fee_rt=0.001, leverage=1.0, asset=asset, tf="230m")
|
||||
s = pd.Series(m.equity, index=pd.DatetimeIndex(pd.to_datetime(m.eq_index, utc=True)))
|
||||
daily = s.resample("1D").last().ffill().pct_change().dropna()
|
||||
_CACHE[key] = (daily, m, ltf, ent)
|
||||
return _CACHE[key]
|
||||
|
||||
|
||||
@lru_cache(maxsize=32)
|
||||
def skh_port(off: int) -> pd.Series:
|
||||
"""Book 50/50 BTC+ETH daily-step alla fase `off` (convenzione di _skyhook_returns)."""
|
||||
series = {a: run_asset(a, off)[0] for a in ASSETS}
|
||||
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
|
||||
return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)
|
||||
|
||||
|
||||
def sh3(s: pd.Series) -> tuple[float, float, float]:
|
||||
return (al._sh(s), al._sh(s[s.index < HOLDOUT]), al._sh(s[s.index >= HOLDOUT]))
|
||||
|
||||
|
||||
def pctl_of_first(v: np.ndarray) -> float:
|
||||
return float((v < v[0]).mean() + 0.5 * (v == v[0]).mean()) * 100
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# (1) SANITY
|
||||
# ---------------------------------------------------------------------------
|
||||
def sanity() -> None:
|
||||
from src.portfolio.sleeves import _skyhook_returns
|
||||
mine = skh_port(0)
|
||||
ref = _skyhook_returns()
|
||||
assert len(mine) == len(ref), f"sanity len: {len(mine)} vs {len(ref)}"
|
||||
dmax = float(np.max(np.abs(mine.values - ref.values)))
|
||||
assert dmax < 1e-12, f"sanity offset 0: max|dif| = {dmax:.2e}"
|
||||
f, i, h = sh3(mine)
|
||||
print(f"[SANITY] offset 0 == _skyhook_returns(): max|dif| = {dmax:.2e} "
|
||||
f"su {len(mine)} giorni (FULL {f:.3f} / IS {i:.3f} / HOLD {h:.3f})")
|
||||
for a in ASSETS:
|
||||
_, m, _, _ = run_asset(a, 0)
|
||||
print(f"[SANITY] {a} off=0: maxDD harness {m.max_dd:.1%}, trade {m.n_trades} "
|
||||
f"(diario: BTC 21.4% / ETH 27.4%)")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# (6) LIVE — re-sim con fill al prossimo multiplo orario (path del cron)
|
||||
# ---------------------------------------------------------------------------
|
||||
def sim_equity(ltf: pd.DataFrame, ent: list, mode: str,
|
||||
ts5_close: np.ndarray | None = None,
|
||||
c5: np.ndarray | None = None) -> np.ndarray:
|
||||
"""Replica del loop di backtest_signals con prezzi d'esecuzione iniettabili.
|
||||
mode='canonical': entry a close[i], exit AL LIVELLO sl/tp (SL prioritario) o close
|
||||
-> DEVE riprodurre backtest_signals bit-exact (sanity della re-sim).
|
||||
mode='barclose': stessi trade (stessa detection), fill entry/exit al CLOSE della barra
|
||||
230m di segnale/trigger -> quota dell'ottimismo 'fill al livello' senza cron.
|
||||
mode='hourly': fill entry/exit al close 5m del PROSSIMO multiplo orario dopo la
|
||||
chiusura della barra di segnale/trigger (path reale del cron 0 * * * *).
|
||||
In tutti i modi le TRADE BOUNDARIES (barra entry, barra trigger, non-overlap) sono
|
||||
identiche al canonico: cambia solo il prezzo d'esecuzione."""
|
||||
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) + MS_LTF
|
||||
|
||||
def px_hour(t: int) -> float:
|
||||
hb = ((t + MSH - 1) // MSH) * MSH # prossimo multiplo orario >= t
|
||||
j = np.searchsorted(ts5_close, hb, side="left")
|
||||
return c5[min(j, len(c5) - 1)]
|
||||
|
||||
initial = 1000.0
|
||||
capital = initial
|
||||
equity = np.full(n, capital, dtype=float)
|
||||
busy_until = -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:
|
||||
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 price per modalita'
|
||||
if mode == "canonical":
|
||||
entry = c[i]
|
||||
elif mode == "barclose":
|
||||
entry = c[i]
|
||||
else: # hourly
|
||||
entry = px_hour(close_ts[i])
|
||||
|
||||
exit_idx = min(i + max_bars, n - 1)
|
||||
exit_lvl = c[exit_idx] # default: time exit a close
|
||||
hit_kind = "time"
|
||||
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, hit_kind = sl, j, "sl"
|
||||
break
|
||||
if hit_tp:
|
||||
exit_lvl, exit_idx, hit_kind = tp, j, "tp"
|
||||
break
|
||||
exit_lvl, exit_idx = c[j], j
|
||||
if mode == "canonical":
|
||||
exit_price = exit_lvl # fill al livello (come harness)
|
||||
elif mode == "barclose":
|
||||
exit_price = c[exit_idx] # fill al close della barra trigger
|
||||
else:
|
||||
exit_price = px_hour(close_ts[exit_idx]) # fill al prossimo multiplo orario
|
||||
|
||||
gross = (exit_price - entry) / entry * direction
|
||||
net = gross - 0.001 # fee_rt 0.10%, leverage 1
|
||||
capital += capital * net
|
||||
capital = max(capital, 1.0)
|
||||
equity[i:exit_idx + 1] = capital
|
||||
busy_until = exit_idx
|
||||
_ = hit_kind
|
||||
|
||||
# stessa forward-fill robusta del harness
|
||||
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 live_delay_section() -> None:
|
||||
print("\n" + "=" * 100)
|
||||
print("(6) RILEVANZA LIVE — cron ORARIO (0 * * * *) vs confini 230m non allineati all'ora")
|
||||
print("=" * 100)
|
||||
# distribuzione del ritardo confine-230m -> prossima ora (struttura: 230 mod 60 = 50)
|
||||
_, _, ltf0, _ = run_asset("BTC", 0)
|
||||
close_ts = ltf0["timestamp"].values.astype(np.int64) + MS_LTF
|
||||
delay_min = ((MSH - (close_ts % MSH)) % MSH) // 60_000
|
||||
vals, cnts = np.unique(delay_min, return_counts=True)
|
||||
tot = cnts.sum()
|
||||
print("ritardo chiusura-barra-230m -> prossimo run orario del cron:")
|
||||
for v, cn in zip(vals, cnts):
|
||||
print(f" {int(v):>3} min : {cn / tot:>6.1%}")
|
||||
print(f" media {float(delay_min.mean()):.1f} min, max {int(delay_min.max())} min "
|
||||
f"(ciclo di 6 barre = 23h: la griglia migra attraverso la giornata)")
|
||||
|
||||
# impatto: re-sim canonico (sanity bit-exact) / barclose / hourly
|
||||
print(f"\nimpatto sull'equity daily-step (offset 0, canonico; fill hourly = close 5m del "
|
||||
f"prossimo multiplo orario dopo la chiusura della barra di segnale/exit):")
|
||||
print(f"{'asset':<5} {'modo':<10} {'ShFULL':>7} {'ShIS':>7} {'ShHOLD':>7} {'maxDD':>7}")
|
||||
dailies: dict[str, dict[str, pd.Series]] = {m: {} for m in ("canonical", "barclose", "hourly")}
|
||||
for a in ASSETS:
|
||||
_, m0, ltf, ent = run_asset(a, 0)
|
||||
df5 = get5m(a)
|
||||
ts5_close = df5["timestamp"].values.astype(np.int64) + MS5
|
||||
c5 = df5["close"].values.astype(float)
|
||||
for mode in ("canonical", "barclose", "hourly"):
|
||||
eq = sim_equity(ltf, ent, mode, ts5_close=ts5_close, c5=c5)
|
||||
if mode == "canonical":
|
||||
dmax = float(np.max(np.abs(eq - m0.equity)))
|
||||
assert dmax < 1e-6, f"re-sim canonica != harness ({a}): max|dif|={dmax:.2e}"
|
||||
print(f" [sanity] {a}: re-sim canonica == backtest_signals "
|
||||
f"(max|dif equity| = {dmax:.2e})")
|
||||
s = pd.Series(eq, index=pd.DatetimeIndex(pd.to_datetime(ltf["datetime"], utc=True)))
|
||||
d = s.resample("1D").last().ffill().pct_change().dropna()
|
||||
dailies[mode][a] = d
|
||||
f, i_, h = sh3(d)
|
||||
dd = al._dd_ret(d)
|
||||
print(f"{a:<5} {mode:<10} {f:>7.3f} {i_:>7.3f} {h:>7.3f} {dd:>6.1%}")
|
||||
print("\nbook 50/50 BTC+ETH per modo:")
|
||||
print(f"{'modo':<10} {'ShFULL':>7} {'ShIS':>7} {'ShHOLD':>7} {'maxDD':>7}")
|
||||
port_mode = {}
|
||||
for mode in ("canonical", "barclose", "hourly"):
|
||||
J = pd.concat(dailies[mode], axis=1, join="inner").fillna(0.0)
|
||||
p = pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)
|
||||
port_mode[mode] = p
|
||||
f, i_, h = sh3(p)
|
||||
print(f"{mode:<10} {f:>7.3f} {i_:>7.3f} {h:>7.3f} {al._dd_ret(p):>6.1%}")
|
||||
dfull = al._sh(port_mode['hourly']) - al._sh(port_mode['canonical'])
|
||||
dhold = (al._sh(port_mode['hourly'][port_mode['hourly'].index >= HOLDOUT])
|
||||
- al._sh(port_mode['canonical'][port_mode['canonical'].index >= HOLDOUT]))
|
||||
print(f"\n-> delta hourly vs canonico: FULL {dfull:+.3f}, HOLD {dhold:+.3f} "
|
||||
f"(barclose isola il fill-al-livello; hourly aggiunge il ritardo 0-50 min)")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
def main() -> None:
|
||||
t0 = time.time()
|
||||
print("=" * 100)
|
||||
print("r0702 — SKH01-V2-DD: timing-luck della FASE della griglia dual-TF 230m/690m")
|
||||
print(f"23 offset a priori (ogni 30m su [0,690)), parametri IDENTICI, fee 0.10% RT, "
|
||||
f"HOLD-OUT >= {HOLDOUT.date()}")
|
||||
print("=" * 100)
|
||||
|
||||
sanity()
|
||||
|
||||
# ---- (2) per-offset x asset -------------------------------------------
|
||||
rows = []
|
||||
for off in OFFSETS:
|
||||
rec = {"off": off}
|
||||
for a in ASSETS:
|
||||
d, m, _, _ = run_asset(a, off)
|
||||
f, i_, h = sh3(d)
|
||||
rec[f"{a}_full"] = f; rec[f"{a}_is"] = i_; rec[f"{a}_hold"] = h
|
||||
rec[f"{a}_dd"] = m.max_dd; rec[f"{a}_ntr"] = m.n_trades
|
||||
p = skh_port(off)
|
||||
f, i_, h = sh3(p)
|
||||
rec["P_full"] = f; rec["P_is"] = i_; rec["P_hold"] = h
|
||||
rec["P_dd"] = al._dd_ret(p)
|
||||
rec["minFull"] = min(rec["BTC_full"], rec["ETH_full"])
|
||||
rec["minHold"] = min(rec["BTC_hold"], rec["ETH_hold"])
|
||||
rows.append(rec)
|
||||
print(f" [{time.time()-t0:5.0f}s] offset {off:>3}m fatto "
|
||||
f"(minFull {rec['minFull']:+.2f}, minHold {rec['minHold']:+.2f}, "
|
||||
f"DD {rec['BTC_dd']:.0%}/{rec['ETH_dd']:.0%})")
|
||||
T = pd.DataFrame(rows).set_index("off")
|
||||
|
||||
print("\n--- (2) PER-OFFSET (equity daily-step, come il canonico) ---")
|
||||
for a in ASSETS:
|
||||
print(f"\n{a}:")
|
||||
print(f"{'off':>4} {'ShFULL':>7} {'ShIS':>7} {'ShHOLD':>7} {'maxDD':>7} {'trade':>6}")
|
||||
for off, r in T.iterrows():
|
||||
tag = " <- canonico" if off == 0 else ""
|
||||
print(f"{off:>4} {r[f'{a}_full']:>7.3f} {r[f'{a}_is']:>7.3f} "
|
||||
f"{r[f'{a}_hold']:>7.3f} {r[f'{a}_dd']:>6.1%} {int(r[f'{a}_ntr']):>6}{tag}")
|
||||
print(" min/med/max [pctl canonico]:")
|
||||
for col, lbl in ((f"{a}_full", "ShFULL"), (f"{a}_is", "ShIS"),
|
||||
(f"{a}_hold", "ShHOLD"), (f"{a}_dd", "maxDD")):
|
||||
v = T[col].values
|
||||
print(f" {lbl:<7} {v.min():>7.3f} / {np.median(v):>7.3f} / {v.max():>7.3f} "
|
||||
f" [off0 = {v[0]:.3f}, {pctl_of_first(v):.0f} pctl]")
|
||||
|
||||
print(f"\nBOOK 50/50 (portafoglio SKH01 standalone):")
|
||||
print(f"{'off':>4} {'ShFULL':>7} {'ShIS':>7} {'ShHOLD':>7} {'maxDD':>7}")
|
||||
for off, r in T.iterrows():
|
||||
tag = " <- canonico" if off == 0 else ""
|
||||
print(f"{off:>4} {r.P_full:>7.3f} {r.P_is:>7.3f} {r.P_hold:>7.3f} {r.P_dd:>6.1%}{tag}")
|
||||
for col, lbl in (("P_full", "ShFULL"), ("P_is", "ShIS"), ("P_hold", "ShHOLD"),
|
||||
("P_dd", "maxDD"), ("minFull", "minFull"), ("minHold", "minHold")):
|
||||
v = T[col].values
|
||||
print(f" {lbl:<8} min {v.min():>7.3f} / med {np.median(v):>7.3f} / max {v.max():>7.3f} "
|
||||
f" [off0 = {v[0]:.3f}, {pctl_of_first(v):.0f} pctl]")
|
||||
|
||||
# ---- (3) gate di ammissione ------------------------------------------
|
||||
print("\n--- (3) GATE DI AMMISSIONE ri-valutati sui 23 offset ---")
|
||||
viol_b = T["BTC_dd"] >= 0.30
|
||||
viol_e = T["ETH_dd"] >= 0.30
|
||||
viol = viol_b | viol_e
|
||||
print(f"(a) maxDD<30%: violazioni su 23 offset: BTC {int(viol_b.sum())}, "
|
||||
f"ETH {int(viol_e.sum())}, almeno-un-asset {int(viol.sum())} "
|
||||
f"({[int(o) for o in T.index[viol]]})")
|
||||
print(f" mediana DD: BTC {T['BTC_dd'].median():.1%}, ETH {T['ETH_dd'].median():.1%}; "
|
||||
f"peggiore: BTC {T['BTC_dd'].max():.1%} (off {int(T['BTC_dd'].idxmax())}), "
|
||||
f"ETH {T['ETH_dd'].max():.1%} (off {int(T['ETH_dd'].idxmax())})")
|
||||
print(f"(b) minFull (canonico +0.99): mediana {T['minFull'].median():+.2f}, "
|
||||
f"peggiore {T['minFull'].min():+.2f} (off {int(T['minFull'].idxmin())})")
|
||||
print(f" minHold (canonico +1.26): mediana {T['minHold'].median():+.2f}, "
|
||||
f"peggiore {T['minHold'].min():+.2f} (off {int(T['minHold'].idxmin())})")
|
||||
|
||||
# (c) blend 0.75 TP01 + 0.25 SKH — TP01 baseline canonico FISSO
|
||||
B = al.tp01_baseline_daily()
|
||||
b_hold = al._sh(B[B.index >= HOLDOUT])
|
||||
blends_hold, blends_full, corrs = [], [], []
|
||||
blend_series_hold = {}
|
||||
for off in OFFSETS:
|
||||
s = skh_port(off)
|
||||
bl = combine_outer({"TP": B, "SKH": s}, BLEND_W)
|
||||
bl = bl[bl.index >= B.index.min()]
|
||||
blends_hold.append(al._sh(bl[bl.index >= HOLDOUT]))
|
||||
blends_full.append(al._sh(bl))
|
||||
blend_series_hold[off] = bl[bl.index >= HOLDOUT]
|
||||
J = pd.concat({"TP": B, "SKH": s}, axis=1, join="inner").dropna()
|
||||
corrs.append(float(J["TP"].corr(J["SKH"])))
|
||||
bh = np.array(blends_hold); bf = np.array(blends_full); co = np.array(corrs)
|
||||
print(f"(c) blend 0.75*TP01+0.25*SKH (TP01 h=0 canonico; suo HOLD da solo = {b_hold:.2f}):")
|
||||
print(f" Sharpe HOLD blend: off0 {bh[0]:.2f} | min {bh.min():.2f} / med "
|
||||
f"{np.median(bh):.2f} / max {bh.max():.2f} [off0 al {pctl_of_first(bh):.0f} pctl]"
|
||||
f" (claim: 0.31 -> 1.17)")
|
||||
print(f" uplift HOLD vs TP01 solo: off0 {bh[0]-b_hold:+.2f} | min {bh.min()-b_hold:+.2f} "
|
||||
f"/ med {np.median(bh)-b_hold:+.2f} / max {bh.max()-b_hold:+.2f}")
|
||||
print(f" Sharpe FULL blend: off0 {bf[0]:.2f} | min {bf.min():.2f} / med "
|
||||
f"{np.median(bf):.2f} / max {bf.max():.2f}")
|
||||
print(f"(d) corr(SKH, TP01) full: off0 {co[0]:+.3f} | min {co.min():+.3f} / med "
|
||||
f"{np.median(co):+.3f} / max {co.max():+.3f} (claim ~0.09)")
|
||||
|
||||
med_minfull = T["minFull"].median(); med_minhold = T["minHold"].median()
|
||||
med_uplift = float(np.median(bh) - b_hold)
|
||||
med_dd_ok = T["BTC_dd"].median() < 0.30 and T["ETH_dd"].median() < 0.30
|
||||
worst_ok = (not viol.any()) and T["minFull"].min() > 0 and (bh.min() - b_hold) > 0
|
||||
print("\nVERDETTO ammissione alla fase MEDIANA: "
|
||||
f"DD<30% {'PASS' if med_dd_ok else 'FAIL'} alla mediana"
|
||||
f" (violazioni puntuali {int(viol.sum())}/23), minFull {med_minfull:+.2f}, "
|
||||
f"minHold {med_minhold:+.2f}, uplift blend HOLD {med_uplift:+.2f}, "
|
||||
f"corr {np.median(co):+.2f}")
|
||||
print(f" al PEGGIORE dei 23: DD {'PASS tutti' if not viol.any() else 'FAIL su ' + str(int(viol.sum())) + ' offset'}, "
|
||||
f"minFull {T['minFull'].min():+.2f}, minHold {T['minHold'].min():+.2f}, "
|
||||
f"uplift blend HOLD {bh.min()-b_hold:+.2f} "
|
||||
f"-> {'regge anche al peggiore' if worst_ok else 'NON regge al peggiore'}")
|
||||
|
||||
# ---- (4) book 5-sleeve -------------------------------------------------
|
||||
print("\n--- (4) BOOK 5-SLEEVE (TP 33 / XS 15 / VRP 12 / SKH 20 / GTAA 20) ---")
|
||||
from src.portfolio.sleeves import (_gtaa_daily_returns, _tp01_returns,
|
||||
_vrp_combo_returns, _xsec_returns)
|
||||
tp_d = to_daily(_tp01_returns())
|
||||
cols_fixed = dict(TP=tp_d, XS=to_daily(_xsec_returns()),
|
||||
VRP=to_daily(_vrp_combo_returns()), GTAA=to_daily(_gtaa_daily_returns()))
|
||||
lo = tp_d.index.min()
|
||||
bkh, bkf = [], []
|
||||
for off in OFFSETS:
|
||||
s = combine_outer(dict(SKH=skh_port(off), **cols_fixed), BOOK_W, lo=lo)
|
||||
bkh.append(al._sh(s[s.index >= HOLDOUT])); bkf.append(al._sh(s))
|
||||
bkh = np.array(bkh); bkf = np.array(bkf)
|
||||
ens = pd.concat({o: skh_port(o) for o in OFFSETS}, axis=1).mean(axis=1)
|
||||
s_ens = combine_outer(dict(SKH=ens, **cols_fixed), BOOK_W, lo=lo)
|
||||
i_med_h = int(np.argsort(bkh)[len(bkh) // 2]); i_worst_h = int(np.argmin(bkh))
|
||||
print(f"HOLD: off0 {bkh[0]:.2f} | min {bkh.min():.2f} (off {OFFSETS[i_worst_h]}) / med "
|
||||
f"{np.median(bkh):.2f} (off {OFFSETS[i_med_h]}) / max {bkh.max():.2f} "
|
||||
f"[off0 al {pctl_of_first(bkh):.0f} pctl] | ENSEMBLE "
|
||||
f"{al._sh(s_ens[s_ens.index >= HOLDOUT]):.2f}")
|
||||
print(f"FULL: off0 {bkf[0]:.2f} | min {bkf.min():.2f} / med {np.median(bkf):.2f} / max "
|
||||
f"{bkf.max():.2f} [off0 al {pctl_of_first(bkf):.0f} pctl] | ENSEMBLE {al._sh(s_ens):.2f}")
|
||||
print("(l'ensemble di offset NON e' eseguibile live — una sola griglia gira: e' solo la "
|
||||
"stima de-luckata)")
|
||||
|
||||
# ---- (5) bootstrap alla maniera dello scettico -------------------------
|
||||
print("\n--- (5) BOOTSTRAP (block) — lo spike del canonico sull'hold-out del blend ---")
|
||||
Mdf = pd.concat(blend_series_hold, axis=1, join="inner").dropna()
|
||||
M = Mdf.values
|
||||
def _sh_cols(R: np.ndarray) -> np.ndarray:
|
||||
mu = R.mean(axis=1); sd = R.std(axis=1)
|
||||
return np.where(sd > 0, mu / sd, 0.0) * np.sqrt(365.25)
|
||||
sh_obs = np.array([al._sh(Mdf[c]) for c in Mdf.columns])
|
||||
g0_obs = float(sh_obs[0] - np.median(sh_obs[1:]))
|
||||
corrM = np.corrcoef(M.T); iu = np.triu_indices(M.shape[1], 1)
|
||||
print(f"hold-out: {M.shape[0]} giorni x {M.shape[1]} offset; Sh blend off0 {sh_obs[0]:.3f}, "
|
||||
f"mediana altri {np.median(sh_obs[1:]):.3f}, spike osservato g0 = {g0_obs:+.3f}")
|
||||
print(f"corr daily fra i 23 blend (hold-out): mediana {np.median(corrM[iu]):.3f}, "
|
||||
f"min {corrM[iu].min():.3f}")
|
||||
n, K = M.shape
|
||||
for blk in (10, 20, 40):
|
||||
rng = np.random.default_rng(42 + blk)
|
||||
nblocks = int(np.ceil(n / blk))
|
||||
gmaxs, g0s = [], []
|
||||
done = 0
|
||||
while done < B_BOOT:
|
||||
b = min(500, B_BOOT - done)
|
||||
starts = rng.integers(0, n, size=(b, nblocks))
|
||||
idx = (starts[:, :, None] + np.arange(blk)[None, None, :]) % n
|
||||
idx = idx.reshape(b, -1)[:, :n]
|
||||
R = M[idx] # (b, n, K)
|
||||
Sh = np.stack([_sh_cols(R[:, :, k]) for k in range(K)], axis=1)
|
||||
med_others = np.empty_like(Sh)
|
||||
for k in range(K):
|
||||
med_others[:, k] = np.median(np.delete(Sh, k, axis=1), axis=1)
|
||||
g = Sh - med_others
|
||||
gmaxs.append(g.max(axis=1)); g0s.append(g[:, 0])
|
||||
done += b
|
||||
gmax = np.concatenate(gmaxs); g0 = np.concatenate(g0s)
|
||||
print(f" block={blk:>2}: P(spike di UN offset qualsiasi >= {g0_obs:+.2f}) = "
|
||||
f"{float(np.mean(gmax >= g0_obs)):.3f} | P(g0<=0) = {float(np.mean(g0 <= 0)):.3f} "
|
||||
f"| CI95 g0 [{np.percentile(g0, 2.5):+.2f},{np.percentile(g0, 97.5):+.2f}]")
|
||||
|
||||
# ---- (6) live ----------------------------------------------------------
|
||||
live_delay_section()
|
||||
|
||||
# ---- (7) caveat --------------------------------------------------------
|
||||
print("\n--- (7) CAVEAT ---")
|
||||
print("- equity daily-step (lens Sharpe), stessa convenzione del canonico;")
|
||||
print("- costi di ribilanciamento del book 230m gia' flaggati a deploy (diario skyhook);")
|
||||
print("- ensemble di offset NON eseguibile live (una sola griglia gira) -> solo stima de-luckata;")
|
||||
print("- sim 'hourly': fill a close 5m del multiplo orario (cron minuto 0), niente slippage/parziali;")
|
||||
print("- nessuna selezione: 23 offset uniformi dichiarati a priori, parametri identici ovunque.")
|
||||
print(f"\nFatto in {time.time()-t0:.0f}s.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,378 @@
|
||||
#!/usr/bin/env python
|
||||
"""r0702_anchor_xs01.py — TIMING-LUCK della FASE del ciclo di ribilanciamento di XS01.
|
||||
|
||||
XS01 (`src/portfolio/sleeves._xsec_returns`, XS_CFG H=10) ribilancia ogni 10 giorni quando
|
||||
`i % H == 0` con `i` = indice di riga della matrice prezzi inner-joined (start 2024-01-01).
|
||||
La FASE del ciclo (quale dei 10 giorni possibili) e' quindi un artefatto della prima riga
|
||||
dei dati — esattamente come l'ancora oraria di TP01 (vedi r0702_tp01_offset.py +
|
||||
r0702_skeptic_offset.py, diario 2026-07-02-timing-crt-wave.md). Spazio di luck: 10 fasi.
|
||||
|
||||
Questo script (metodologia identica all'audit TP01, ZERO tuning per-fase):
|
||||
1. REPLICA `_xsec_returns` con parametro `phase` (i % H == phase). SANITY OBBLIGATORIO:
|
||||
phase=0 deve riprodurre ESATTAMENTE la serie di `_xsec_returns()` (max|dif| ~ 0).
|
||||
Il gate di dispersione (percentile ESPANDENTE causale) e' ricalcolato PER FASE:
|
||||
disp_hist accumula solo nei giorni di ribilanciamento di QUELLA fase (replica fedele).
|
||||
2. Tabella delle 10 fasi: Sharpe/CAGR/maxDD FULL (serie dal 2024) e HOLD-OUT 2025-26;
|
||||
min/mediana/max + percentile della fase canonica (phase=0).
|
||||
3. ENSEMBLE delle 10 fasi (media dei ritorni = 1/10 del capitale per fase) vs canonica.
|
||||
4. IMPATTO SUL BOOK 5-sleeve (TP 33 / XS 15 / VRP 12 / SKH 20 / GTAA 20, combine_outer
|
||||
con pesi rinormalizzati, attivazione era crypto): XS canonica vs fase mediana vs fase
|
||||
peggiore vs ensemble vs senza-XS -> HOLD e FULL del book.
|
||||
5. GATE DI AMMISSIONE: plateau (lookback singoli/blend, disp_pct 15-35) ricalcolato alla
|
||||
fase MEDIANA e alla canonica: i numeri di ammissione (FULL 1.50/HOLD 1.71/DD 11%) reggono?
|
||||
6. BOOTSTRAP alla maniera dello scettico: block-bootstrap congiunto delle 10 fasi
|
||||
sull'hold-out -> P(una fase qualsiasi mostri uno spike >= quello della canonica)
|
||||
+ CI95 (storia ~2.5y -> quantifica l'ampiezza).
|
||||
|
||||
Tecnica: mai .view su tz-aware (epoca via pd.to_datetime unit='ms', come l'originale);
|
||||
posizioni shiftate come nell'originale (gross[1:] = W[:-1]*dret[1:]); vol-target rolling
|
||||
ricalcolato per fase sulla PROPRIA serie netta. Nessun file toccato fuori da questo script.
|
||||
Runtime ~2-4 min (il grosso e' SKH01 dal 5m per il book).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
ROOT = Path("/opt/docker/PythagorasGoal")
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.portfolio.portfolio import HOLDOUT, combine_outer, to_daily # noqa: E402
|
||||
from src.portfolio.sleeves import (_HL_DIR, XS_CFG, XS_UNIVERSE, # noqa: E402
|
||||
_xsec_returns)
|
||||
|
||||
RNG_SEED = 42
|
||||
B_BOOT = 4000
|
||||
H = XS_CFG["H"] # 10 -> 10 fasi possibili
|
||||
FULL_START = pd.Timestamp("2024-01-01", tz="UTC")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Metriche (convenzioni identiche ad altlib._sh/_dd_ret e a r0702_tp01_offset.dmetrics)
|
||||
# ---------------------------------------------------------------------------
|
||||
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 _dd(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 _cagr(s) -> float:
|
||||
r = np.asarray(pd.Series(s).dropna().values, float)
|
||||
eq = float(np.prod(1.0 + r))
|
||||
yrs = len(r) / 365.25
|
||||
return eq ** (1 / yrs) - 1 if eq > 0 and yrs > 0 else -1.0
|
||||
|
||||
|
||||
def dmetrics(s: pd.Series) -> dict:
|
||||
s = s.dropna()
|
||||
ho = s[s.index >= HOLDOUT]
|
||||
return dict(sh_full=_sh(s), cagr_full=_cagr(s), dd_full=_dd(s),
|
||||
sh_hold=_sh(ho), cagr_hold=_cagr(ho), dd_hold=_dd(ho), n=len(s))
|
||||
|
||||
|
||||
def pctl_of(v: np.ndarray, x: float) -> float:
|
||||
return float((v < x).mean() + 0.5 * (v == x).mean()) * 100.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Prezzi: caricamento IDENTICO a _xsec_returns (stesso ordine di operazioni)
|
||||
# ---------------------------------------------------------------------------
|
||||
_PX_CACHE: pd.DataFrame | None = None
|
||||
|
||||
|
||||
def load_prices() -> pd.DataFrame:
|
||||
global _PX_CACHE
|
||||
if _PX_CACHE is not None:
|
||||
return _PX_CACHE
|
||||
cols = {}
|
||||
for sym in XS_UNIVERSE:
|
||||
p = _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))
|
||||
if len(cols) < 10:
|
||||
raise FileNotFoundError("universo Hyperliquid XS01 incompleto")
|
||||
_PX_CACHE = pd.concat(cols, axis=1, join="inner").sort_index().dropna()
|
||||
return _PX_CACHE
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# REPLICA di _xsec_returns con parametro `phase` (+ override cfg per il plateau).
|
||||
# Codice copiato 1:1 da src/portfolio/sleeves._xsec_returns; UNICA differenza:
|
||||
# `i % H == 0` -> `i % H == phase`. Il disp_hist (percentile espandente causale)
|
||||
# accumula SOLO nei giorni di ribilanciamento della fase -> ricalcolato per fase.
|
||||
# ---------------------------------------------------------------------------
|
||||
def xs_phase(phase: int, lookbacks=None, disp_pct=None) -> pd.Series:
|
||||
cfg = dict(XS_CFG)
|
||||
if lookbacks is not None:
|
||||
cfg["lookbacks"] = lookbacks
|
||||
if disp_pct is not None:
|
||||
cfg["disp_pct"] = disp_pct
|
||||
C = load_prices()
|
||||
px = C.values
|
||||
n, A = px.shape
|
||||
lookbacks_, Hc, k, mode, tv = (cfg["lookbacks"], cfg["H"], cfg["k"],
|
||||
cfg["mode"], cfg["target_vol"])
|
||||
disp_pct_ = cfg.get("disp_pct", 0)
|
||||
minhist = cfg.get("disp_minhist", 20)
|
||||
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 % Hc == 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[:k], 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(A)
|
||||
disp_hist.append(disp_i)
|
||||
W[i] = w
|
||||
gross = np.zeros(n)
|
||||
gross[1:] = np.sum(W[:-1] * dret[1:], axis=1) # posizioni SHIFTATE (held t+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)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. SANITY: replica a fase canonica == _xsec_returns() (bit-exact)
|
||||
# ---------------------------------------------------------------------------
|
||||
def sanity() -> None:
|
||||
ref = _xsec_returns()
|
||||
mine = xs_phase(0)
|
||||
assert len(mine) == len(ref) and mine.index.equals(ref.index), "SANITY index mismatch"
|
||||
dmax = float(np.max(np.abs(mine.values - ref.values)))
|
||||
assert dmax == 0.0, f"SANITY FAIL: replica phase=0 != _xsec_returns (max|dif|={dmax:.3e})"
|
||||
m = dmetrics(ref)
|
||||
print(f"[SANITY] replica phase=0 == _xsec_returns(): OK (max|dif| = {dmax:.1e}, "
|
||||
f"n={len(ref)}, start {ref.index.min().date()})")
|
||||
print(f"[SANITY] canonica oggi: FULL Sh {m['sh_full']:.3f} / HOLD {m['sh_hold']:.3f} / "
|
||||
f"DD {m['dd_full']:.1%} (dichiarati all'ammissione: 1.50 / 1.71 / 11% — dati mossi da allora)")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 6. BOOTSTRAP alla maniera dello scettico (r0702_skeptic_offset.block_boot_stats)
|
||||
# ---------------------------------------------------------------------------
|
||||
def _sh_mat(R: np.ndarray) -> np.ndarray:
|
||||
mu = R.mean(axis=1)
|
||||
sd = R.std(axis=1)
|
||||
return np.where(sd > 0, mu / sd, 0.0) * np.sqrt(365.25)
|
||||
|
||||
|
||||
def block_boot(M: np.ndarray, B: int, block: int, rng) -> dict:
|
||||
n, K = M.shape
|
||||
nblocks = int(np.ceil(n / block))
|
||||
g0s, gmaxs, meds, sh0s = [], [], [], []
|
||||
done = 0
|
||||
while done < B:
|
||||
b = min(500, B - done)
|
||||
starts = rng.integers(0, n, size=(b, nblocks))
|
||||
idx = (starts[:, :, None] + np.arange(block)[None, None, :]) % n
|
||||
idx = idx.reshape(b, -1)[:, :n]
|
||||
R = M[idx] # (b, n, K)
|
||||
Sh = np.stack([_sh_mat(R[:, :, kk]) for kk in range(K)], axis=1)
|
||||
gg = np.empty_like(Sh)
|
||||
for h in range(K):
|
||||
gg[:, h] = Sh[:, h] - np.median(np.delete(Sh, h, axis=1), axis=1)
|
||||
g0s.append(gg[:, 0])
|
||||
gmaxs.append(gg.max(axis=1))
|
||||
meds.append(np.median(Sh, axis=1))
|
||||
sh0s.append(Sh[:, 0])
|
||||
done += b
|
||||
return dict(g0=np.concatenate(g0s), gmax=np.concatenate(gmaxs),
|
||||
med=np.concatenate(meds), sh0=np.concatenate(sh0s))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
def main() -> None:
|
||||
print("=" * 100)
|
||||
print("r0702 — XS01 rebalance PHASE timing-luck: 10 fasi del ciclo H=10 "
|
||||
"(metodologia = audit ancore TP01)")
|
||||
print(f"XS_CFG={XS_CFG} universo={len(load_prices().columns)} major HL "
|
||||
f"HOLD-OUT >= {HOLDOUT.date()}")
|
||||
print("=" * 100)
|
||||
|
||||
sanity()
|
||||
|
||||
# ---- (2) tabella delle 10 fasi ----------------------------------------
|
||||
phases = {p: xs_phase(p) for p in range(H)}
|
||||
T = pd.DataFrame({p: dmetrics(s) for p, s in phases.items()}).T
|
||||
print("\n--- (2) PER-FASE (parametri IDENTICI, zero tuning; fase = i % 10 del ciclo) ---")
|
||||
print(f"{'fase':>4} {'ShFULL':>7} {'CAGRf':>7} {'DDfull':>7} {'ShHOLD':>7} "
|
||||
f"{'CAGRh':>7} {'DDhold':>7}")
|
||||
for p, r in T.iterrows():
|
||||
tag = " <- canonica" if p == 0 else ""
|
||||
print(f"{p:>4} {r.sh_full:>7.3f} {r.cagr_full:>6.1%} {r.dd_full:>6.1%} "
|
||||
f"{r.sh_hold:>7.3f} {r.cagr_hold:>6.1%} {r.dd_hold:>6.1%}{tag}")
|
||||
print("\nDistribuzione fra le 10 fasi (min / mediana / max / std) [percentile della canonica]:")
|
||||
for col, lbl in (("sh_full", "Sharpe FULL"), ("sh_hold", "Sharpe HOLD"),
|
||||
("cagr_full", "CAGR FULL"), ("dd_full", "maxDD FULL"),
|
||||
("dd_hold", "maxDD HOLD")):
|
||||
v = T[col].values.astype(float)
|
||||
print(f" {lbl:<14} {v.min():>7.3f} / {np.median(v):>7.3f} / {v.max():>7.3f} "
|
||||
f"/ std {v.std():.3f} canonica al {pctl_of(v, v[0]):.0f}° pctl (val {v[0]:.3f})")
|
||||
|
||||
hold_v = T["sh_hold"].values.astype(float)
|
||||
full_v = T["sh_full"].values.astype(float)
|
||||
med_hold = float(np.median(hold_v))
|
||||
p_med = int(min(range(H), key=lambda p: (abs(hold_v[p] - med_hold), p)))
|
||||
p_worst = int(np.argmin(hold_v))
|
||||
p_best = int(np.argmax(hold_v))
|
||||
print(f"\n fase MEDIANA (per ShHOLD) = {p_med} | PEGGIORE = {p_worst} | MIGLIORE = {p_best}")
|
||||
|
||||
# correlazione fra fasi (contestualizza il bootstrap)
|
||||
Jf = pd.concat(phases, axis=1, join="inner").dropna()
|
||||
Mh = Jf[Jf.index >= HOLDOUT].values
|
||||
cor = np.corrcoef(Mh.T)
|
||||
iu = np.triu_indices(H, 1)
|
||||
print(f" correlazione daily fra fasi (hold-out): mediana {np.median(cor[iu]):.3f}, "
|
||||
f"min {cor[iu].min():.3f}")
|
||||
|
||||
# ---- (3) ensemble delle 10 fasi ----------------------------------------
|
||||
ens = pd.Series(Jf.values.mean(axis=1), index=Jf.index)
|
||||
me, m0 = dmetrics(ens), dmetrics(phases[0])
|
||||
print("\n--- (3) ENSEMBLE (media dei ritorni delle 10 fasi = 1/10 capitale per fase) ---")
|
||||
print(f"{'config':<18} {'ShFULL':>7} {'CAGRf':>7} {'DDfull':>7} {'ShHOLD':>7} {'DDhold':>7}")
|
||||
for name, m in (("canonica (f0)", m0), ("ensemble 10 fasi", me)):
|
||||
print(f"{name:<18} {m['sh_full']:>7.3f} {m['cagr_full']:>6.1%} {m['dd_full']:>6.1%} "
|
||||
f"{m['sh_hold']:>7.3f} {m['dd_hold']:>6.1%}")
|
||||
|
||||
# ---- (4) impatto sul book 5-sleeve -------------------------------------
|
||||
print("\n--- (4) BOOK 5-sleeve (TP 33 / XS 15 / VRP 12 / SKH 20 / GTAA 20, combine_outer, "
|
||||
"attivazione era crypto) ---")
|
||||
from src.portfolio.sleeves import (_gtaa_daily_returns, _skyhook_returns,
|
||||
_tp01_returns, _vrp_combo_returns)
|
||||
TPd = to_daily(_tp01_returns())
|
||||
fixed = dict(TP=TPd, VRP=to_daily(_vrp_combo_returns()),
|
||||
SKH=to_daily(_skyhook_returns()), GTAA=to_daily(_gtaa_daily_returns()))
|
||||
Wt = dict(TP=0.33, XS=0.15, VRP=0.12, SKH=0.20, GTAA=0.20)
|
||||
lo = TPd.index.min()
|
||||
|
||||
def book(xs: pd.Series | None) -> pd.Series:
|
||||
cols = dict(fixed)
|
||||
if xs is not None:
|
||||
cols["XS"] = to_daily(xs)
|
||||
wt = {k: v for k, v in Wt.items() if k in cols}
|
||||
return combine_outer(cols, wt, lo=lo)
|
||||
|
||||
rows = [("XS canonica (f0)", phases[0]), (f"XS fase mediana ({p_med})", phases[p_med]),
|
||||
(f"XS fase peggiore ({p_worst})", phases[p_worst]),
|
||||
(f"XS fase migliore ({p_best})", phases[p_best]),
|
||||
("XS ensemble 10f", ens), ("senza XS01 (rinorm.)", None)]
|
||||
print(f"{'book con':<24} {'ShHOLD':>7} {'ShFULL':>7} {'DDfull':>7} {'DDhold':>7}")
|
||||
book_stats = {}
|
||||
for name, xs in rows:
|
||||
b = book(xs)
|
||||
bh = b[b.index >= HOLDOUT]
|
||||
book_stats[name] = (_sh(bh), _sh(b))
|
||||
print(f"{name:<24} {_sh(bh):>7.3f} {_sh(b):>7.3f} {_dd(b):>6.1%} {_dd(bh):>6.1%}")
|
||||
bh0 = book_stats["XS canonica (f0)"][0]
|
||||
print(f"\n fortuna di fase ereditata dal book HOLD: canonica {bh0:.3f} vs mediana "
|
||||
f"{book_stats[f'XS fase mediana ({p_med})'][0]:.3f} "
|
||||
f"(delta {bh0 - book_stats[f'XS fase mediana ({p_med})'][0]:+.3f}) | vs peggiore "
|
||||
f"{book_stats[f'XS fase peggiore ({p_worst})'][0]:.3f} "
|
||||
f"(delta {bh0 - book_stats[f'XS fase peggiore ({p_worst})'][0]:+.3f}) | vs senza-XS "
|
||||
f"{book_stats['senza XS01 (rinorm.)'][0]:.3f}")
|
||||
|
||||
# ---- (5) gate di ammissione alla fase mediana: plateau ------------------
|
||||
print("\n--- (5) GATE DI AMMISSIONE — plateau ricalcolato alla fase MEDIANA "
|
||||
f"({p_med}) vs canonica (0) ---")
|
||||
print("(ammissione XS01: FULL 1.50 / HOLD 1.71 / DD 11%, plateau lookback + disp_pct 15-35)")
|
||||
print(f"{'variante':<28} {'f0 FULL':>8} {'f0 HOLD':>8} {'f0 DD':>7} "
|
||||
f"{'fMED FULL':>9} {'fMED HOLD':>9} {'fMED DD':>8}")
|
||||
grid = ([(f"disp_pct={dp} (lb 30,90)", dict(disp_pct=dp)) for dp in (15, 20, 25, 30, 35)]
|
||||
+ [(f"lookbacks={lb} (p30)", dict(lookbacks=lb))
|
||||
for lb in ((30,), (60,), (90,), (30, 60, 90))])
|
||||
for name, kw in grid:
|
||||
a = dmetrics(xs_phase(0, **kw))
|
||||
b = dmetrics(xs_phase(p_med, **kw))
|
||||
star = " *" if kw == dict(disp_pct=30) or kw.get("lookbacks") == (30, 90) else ""
|
||||
print(f"{name:<28} {a['sh_full']:>8.3f} {a['sh_hold']:>8.3f} {a['dd_full']:>6.1%} "
|
||||
f"{b['sh_full']:>9.3f} {b['sh_hold']:>9.3f} {b['dd_full']:>7.1%}{star}")
|
||||
mmed = dmetrics(phases[p_med])
|
||||
print(f"\n cella canonica alla fase mediana: FULL {mmed['sh_full']:.3f} "
|
||||
f"(ammesso con 1.50) / HOLD {mmed['sh_hold']:.3f} (ammesso con 1.71) / "
|
||||
f"DD {mmed['dd_full']:.1%} (ammesso con 11%)")
|
||||
n_full_pos = int((full_v >= 1.0).sum())
|
||||
n_hold_pos = int((hold_v >= 1.0).sum())
|
||||
print(f" fasi con FULL>=1.0: {n_full_pos}/10 | fasi con HOLD>=1.0: {n_hold_pos}/10 | "
|
||||
f"fasi con HOLD<=0: {int((hold_v <= 0).sum())}/10")
|
||||
|
||||
# ---- (6) bootstrap: la canonica e' speciale? ----------------------------
|
||||
print("\n--- (6) BOOTSTRAP (block, congiunto sulle 10 fasi, hold-out) ---")
|
||||
sh_hold_obs = _sh_mat(Mh.T)
|
||||
g0_obs = float(sh_hold_obs[0] - np.median(sh_hold_obs[1:]))
|
||||
print(f"hold-out: {Mh.shape[0]} giorni, 10 fasi; Sh canonica {sh_hold_obs[0]:.3f}, "
|
||||
f"mediana altre {np.median(sh_hold_obs[1:]):.3f}, spike osservato g0 = {g0_obs:+.3f}")
|
||||
for blk in (10, 20, 40):
|
||||
bs = block_boot(Mh, B_BOOT, blk, np.random.default_rng(RNG_SEED + blk))
|
||||
p_any = float(np.mean(bs["gmax"] >= g0_obs))
|
||||
ci_g0 = np.percentile(bs["g0"], [2.5, 97.5])
|
||||
ci_med = np.percentile(bs["med"], [2.5, 97.5])
|
||||
ci_sh0 = np.percentile(bs["sh0"], [2.5, 97.5])
|
||||
print(f" block={blk:>2}: P(spike di UNA QUALSIASI fase >= {g0_obs:+.2f}) = {p_any:.3f} | "
|
||||
f"CI95 g0 [{ci_g0[0]:+.2f},{ci_g0[1]:+.2f}] | CI95 Sh mediana-fasi "
|
||||
f"[{ci_med[0]:+.2f},{ci_med[1]:+.2f}] | CI95 Sh canonica [{ci_sh0[0]:+.2f},{ci_sh0[1]:+.2f}]")
|
||||
|
||||
# CI sulla FULL della canonica (storia corta -> quanto sono larghi?)
|
||||
Mf = Jf.values
|
||||
bsf = block_boot(Mf, B_BOOT, 20, np.random.default_rng(RNG_SEED))
|
||||
ci_f0 = np.percentile(bsf["sh0"], [2.5, 97.5])
|
||||
ci_fmed = np.percentile(bsf["med"], [2.5, 97.5])
|
||||
print(f" FULL (block=20): CI95 Sh canonica [{ci_f0[0]:+.2f},{ci_f0[1]:+.2f}] | "
|
||||
f"CI95 Sh mediana-fasi [{ci_fmed[0]:+.2f},{ci_fmed[1]:+.2f}] "
|
||||
f"(~2.5 anni di storia: intervalli larghi)")
|
||||
|
||||
# ---- (7) caveat ---------------------------------------------------------
|
||||
print("\n--- (7) CAVEAT DICHIARATI ---")
|
||||
print(" (a) storia corta ~2.5y (914g, hold-out ~548g): i CI95 qui sopra quantificano "
|
||||
"l'incertezza — nessuna stima puntuale di Sharpe e' affidabile a +/-1.")
|
||||
print(" (b) dimensione ORA-DEL-GIORNO non auditata: le barre HL in data/raw sono solo 1d "
|
||||
"(ancora daily fissa del feed) -> luck residua non testabile con i dati certificati.")
|
||||
print(" (c) XS01 e' STAT-MODE (19 gambe, non eseguito live): l'impatto e' sulla STIMA del "
|
||||
"book (reporting/ammissione), non sull'operativita' del book live Deribit.")
|
||||
print("\nFatto. Nessuna selezione per-fase: parametri identici, giudizio su distribuzione "
|
||||
"delle fasi + ensemble; l'hold-out e' solo riportato.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user