research(wave-0702): ondata timing + CRT — 8 filoni, 0 nuovi sleeve, finding anchor timing-luck TP01
Goal: "altre strategie su Deribit con timing differenti". 8 filoni multi-agente + scettico: - event-clock bars, expiry calendar Deribit, clock lenti/bande, regime-speed: SCARTATI - CRT (Candle Range Theory) base/multi-TF/contesto: SCARTATA 3/3 (DSR~0, ritest = informazione negativa; sottoprodotto: FOLLOW>FADE sui livelli prior-day ogni anno, conferma il lead prevday) - FINDING (confermato da scettico indipendente): hold-out 0.31 di TP01 = migliore delle 24 ancore orarie (mediana 0.04, banda [-0.13,+0.30]) -> narrativa corretta in CLAUDE.md e docstring: l'hold-out non risolve l'edge di ritorno, regge il taglio DD a ogni ancora. Tranching K=2/4 = solo varianza della stima, no deploy a $600. Audit d'ancora pendente su XS01/SKH01. Book live e portafoglio INVARIATI. Test 168/168. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,515 @@
|
||||
#!/usr/bin/env python
|
||||
"""r0702_skeptic_offset.py — VERIFICA AVVERSARIALE INDIPENDENTE di r0702_tp01_offset.py.
|
||||
|
||||
Linee d'attacco (tutte con codice INDIPENDENTE dal finding, cross-check contro le sue funzioni):
|
||||
A. COSTRUZIONE: daily-offset ricostruito via floor-division su epoca ms (niente pandas.resample);
|
||||
h=0 deve == al.get('1d') e == tp01_baseline_daily; mapping daily->1h via searchsorted (niente
|
||||
merge_asof); guardia troncamento del feed 1h (nessun look-ahead a h!=0).
|
||||
B. STATISTICA: block-bootstrap congiunto delle 24 ancore sull'hold-out — lo spike di h=0
|
||||
(Sh(h0) - mediana(altri)) e' speciale o e' il massimo atteso di 24 stime correlate?
|
||||
+ hold-out finti (2020..2024): l'ancora migliore e' stabile o gira a caso?
|
||||
C. TRANCHING: identita' K=4 == EW dei 4 book ancorati (netting non nasconde nulla)?
|
||||
turnover verificato; DD del K=4 vs DD della ROTAZIONE TIPICA (non vs h=0 sfortunato);
|
||||
bootstrap appaiato della differenza IS.
|
||||
D. IMPATTO: blend TP+SKH 75/25 e book 5-sleeve ricalcolati con TP01 alle 24 ancore.
|
||||
|
||||
Nessun file toccato fuori da questo script. Runtime ~3-6 min (SKH/XS/VRP/GTAA inclusi).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
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 / "scripts" / "research"))
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
import altlib as al # noqa: E402
|
||||
import r0702_tp01_offset as RF # il finding, SOLO per cross-check # noqa: E402
|
||||
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402
|
||||
|
||||
TP = TrendPortfolio(**CANONICAL)
|
||||
HOLDOUT = al.HOLDOUT
|
||||
ASSETS = ("BTC", "ETH")
|
||||
MS_H = 3_600_000
|
||||
MS_D = 86_400_000
|
||||
FEE = al.FEE_SIDE
|
||||
RNG = np.random.default_rng(42)
|
||||
B_BOOT = 4000
|
||||
BLOCK = 20
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# A. COSTRUZIONE INDIPENDENTE
|
||||
# ===========================================================================
|
||||
@lru_cache(maxsize=8)
|
||||
def get1h(asset: str) -> pd.DataFrame:
|
||||
return al.get(asset, "1h")
|
||||
|
||||
|
||||
@lru_cache(maxsize=64)
|
||||
def sk_daily(asset: str, h: int) -> pd.DataFrame:
|
||||
"""Daily-offset costruito a mano: day_id = (ts - h*1h) // 24h su epoca ms (open-labeled)."""
|
||||
df = get1h(asset)
|
||||
ts = df["timestamp"].values.astype(np.int64)
|
||||
day = (ts - h * MS_H) // MS_D
|
||||
uday, first = np.unique(day, return_index=True)
|
||||
o = df["open"].values.astype(float)
|
||||
hi = df["high"].values.astype(float)
|
||||
lo = df["low"].values.astype(float)
|
||||
c = df["close"].values.astype(float)
|
||||
v = df["volume"].values.astype(float)
|
||||
last = np.r_[first[1:], len(ts)] - 1
|
||||
out = pd.DataFrame(dict(
|
||||
timestamp=uday * MS_D + h * MS_H,
|
||||
open=o[first],
|
||||
high=np.maximum.reduceat(hi, first),
|
||||
low=np.minimum.reduceat(lo, first),
|
||||
close=c[last],
|
||||
volume=np.add.reduceat(v, first),
|
||||
))
|
||||
out["datetime"] = pd.to_datetime(out["timestamp"], unit="ms", utc=True)
|
||||
return out
|
||||
|
||||
|
||||
def sk_net_daily(asset: str, h: int) -> pd.Series:
|
||||
"""Rendimenti netti TP01 sul grid daily-offset (pipeline mia: shift+fee espliciti)."""
|
||||
d = sk_daily(asset, h)
|
||||
c = d["close"].values.astype(float)
|
||||
r = np.zeros(len(c)); r[1:] = c[1:] / c[:-1] - 1.0
|
||||
tgt = TP.target_series(d)
|
||||
pos = np.zeros(len(tgt)); pos[1:] = tgt[:-1]
|
||||
net = pos * r - FEE * np.abs(np.diff(pos, prepend=0.0))
|
||||
net[0] = 0.0
|
||||
return pd.Series(net, index=pd.DatetimeIndex(d["datetime"]))
|
||||
|
||||
|
||||
@lru_cache(maxsize=32)
|
||||
def sk_port_daily(h: int) -> pd.Series:
|
||||
J = pd.concat({a: sk_net_daily(a, h) for a in ASSETS}, axis=1, join="inner").fillna(0.0)
|
||||
return al._to_daily(0.5 * J["BTC"] + 0.5 * J["ETH"])
|
||||
|
||||
|
||||
def sk_pos_hourly(asset: str, hs: tuple, df1h: pd.DataFrame | None = None) -> np.ndarray:
|
||||
"""Posizione TENUTA durante ogni barra 1h (ensemble media delle ancore hs), via searchsorted:
|
||||
pos durante barra i = target dell'ultima barra daily-offset con close nominale <= open(barra i)."""
|
||||
df = get1h(asset) if df1h is None else df1h
|
||||
open_ms = df["timestamp"].values.astype(np.int64)
|
||||
pos = np.zeros(len(open_ms))
|
||||
for h in hs:
|
||||
d = sk_daily(asset, h) if df1h is None else sk_daily_from(df, h)
|
||||
tgt = np.nan_to_num(TP.target_series(d), nan=0.0)
|
||||
close_ms = d["timestamp"].values.astype(np.int64) + MS_D
|
||||
j = np.searchsorted(close_ms, open_ms, side="right") - 1
|
||||
p = np.where(j >= 0, tgt[np.clip(j, 0, None)], 0.0)
|
||||
pos += p / len(hs)
|
||||
return pos
|
||||
|
||||
|
||||
def sk_daily_from(df1h: pd.DataFrame, h: int) -> pd.DataFrame:
|
||||
"""sk_daily ma da un frame 1h arbitrario (per il test di troncamento)."""
|
||||
ts = df1h["timestamp"].values.astype(np.int64)
|
||||
day = (ts - h * MS_H) // MS_D
|
||||
uday, first = np.unique(day, return_index=True)
|
||||
c = df1h["close"].values.astype(float)
|
||||
last = np.r_[first[1:], len(ts)] - 1
|
||||
out = pd.DataFrame(dict(timestamp=uday * MS_D + h * MS_H, close=c[last]))
|
||||
out["datetime"] = pd.to_datetime(out["timestamp"], unit="ms", utc=True)
|
||||
return out
|
||||
|
||||
|
||||
def sk_book_hourly(hs: tuple) -> tuple[pd.Series, float, dict]:
|
||||
"""Book 0.5/0.5 sul grid 1h con posizioni ensemble; ritorna (daily, turnover/y, per-asset net)."""
|
||||
nets, turns = {}, 0.0
|
||||
for a in ASSETS:
|
||||
df = get1h(a)
|
||||
c = df["close"].values.astype(float)
|
||||
r = np.zeros(len(c)); r[1:] = c[1:] / c[:-1] - 1.0
|
||||
pos = sk_pos_hourly(a, hs)
|
||||
turn = np.abs(np.diff(pos, prepend=0.0))
|
||||
net = pos * r - FEE * turn
|
||||
net[0] = 0.0
|
||||
idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
|
||||
nets[a] = pd.Series(net, index=idx)
|
||||
yrs = len(net) / (24 * 365.25)
|
||||
turns += 0.5 * turn.sum() / yrs
|
||||
J = pd.concat(nets, axis=1, join="inner").fillna(0.0)
|
||||
return al._to_daily(0.5 * J["BTC"] + 0.5 * J["ETH"]), turns, nets
|
||||
|
||||
|
||||
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 part_A() -> None:
|
||||
print("=" * 100)
|
||||
print("A. COSTRUZIONE — ricostruzione indipendente (floor-division ms / searchsorted)")
|
||||
print("=" * 100)
|
||||
# A1: daily-offset mio vs al.get('1d') (h=0) e vs il loro resample_offset (h campionati)
|
||||
for a in ASSETS:
|
||||
ref = al.get(a, "1d")
|
||||
mine0 = sk_daily(a, 0)
|
||||
assert len(mine0) == len(ref), f"A1 len mismatch {a}"
|
||||
for col in ("timestamp", "open", "high", "low", "close", "volume"):
|
||||
rtol = 1e-9 if col == "volume" else 0.0 # volume: solo ordine di sommatoria float
|
||||
assert np.allclose(mine0[col].values.astype(float), ref[col].values.astype(float),
|
||||
atol=0, rtol=rtol), f"A1 h=0 mismatch {a}:{col}"
|
||||
for h in (1, 5, 11, 13, 21, 23):
|
||||
theirs = RF.daily_off(a, h)
|
||||
m = sk_daily(a, h)
|
||||
assert len(m) == len(theirs), f"A1 len mismatch {a} h={h}"
|
||||
for col in ("timestamp", "open", "high", "low", "close", "volume"):
|
||||
rtol = 1e-9 if col == "volume" else 0.0
|
||||
assert np.allclose(m[col].values.astype(float),
|
||||
theirs[col].values.astype(float), atol=0, rtol=rtol), \
|
||||
f"A1 h={h} mismatch {a}:{col}"
|
||||
print("[A1] daily-offset: costruzione mia == al.get('1d') (h=0) == loro resample_offset "
|
||||
"(h=1,5,11,13,21,23, tutte le colonne, bit-exact): OK")
|
||||
|
||||
# A2: pipeline completa h=0 vs baseline del progetto
|
||||
mine = sk_port_daily(0)
|
||||
base = al.tp01_baseline_daily()
|
||||
assert len(mine) == len(base) and np.allclose(mine.values, base.values, atol=1e-12), "A2 FAIL"
|
||||
f, i, ho = sh3(mine)
|
||||
print(f"[A2] portafoglio h=0 (pipeline mia) == tp01_baseline_daily: OK "
|
||||
f"(FULL {f:.4f} / IS {i:.4f} / HOLD {ho:.4f})")
|
||||
|
||||
# A3: troncamento del feed 1h -> posizioni orarie IDENTICHE su tutto il range troncato
|
||||
for a in ASSETS:
|
||||
df = get1h(a)
|
||||
for cut in (len(df) - 3000, len(df) - 777):
|
||||
dtr = df.iloc[:cut].reset_index(drop=True)
|
||||
for h in (0, 5, 13, 21):
|
||||
p_full = sk_pos_hourly(a, (h,))
|
||||
p_tr = sk_pos_hourly(a, (h,), df1h=dtr)
|
||||
assert np.allclose(p_full[:cut], p_tr, atol=1e-12), \
|
||||
f"A3 look-ahead {a} h={h} cut={cut}"
|
||||
print("[A3] troncamento 1h (2 cut x 4 ancore x 2 asset): posizioni orarie invariate "
|
||||
"sul prefisso -> nessun look-ahead nel mapping daily->1h: OK")
|
||||
|
||||
# A4: vol-target ricalcolata per-offset? (fatto strutturale + evidenza numerica)
|
||||
for a in ASSETS:
|
||||
for h in (5, 13):
|
||||
assert TP._bpd(sk_daily(a, h)) == 1, "A4 bpd"
|
||||
t0 = TP.target_series(sk_daily(a, 0))
|
||||
t13 = TP.target_series(sk_daily(a, 13))
|
||||
m = min(len(t0), len(t13))
|
||||
d = np.abs(t0[300:m] - t13[300:m])
|
||||
print(f"[A4] {a}: target h=0 vs h=13 stesso giorno-calendario, |diff| media "
|
||||
f"{np.nanmean(d):.4f} (max {np.nanmax(d):.3f}) -> vol e segnale RICALCOLATI "
|
||||
f"sul grid dell'ancora (target_series riceve il grid offset)")
|
||||
|
||||
# A5: cross-check book orario mio vs loro (K=1 h0 e K=4)
|
||||
for name, hs in (("K=1 h0", (0,)), ("K=4", (0, 6, 12, 18))):
|
||||
mine_s, mine_t, _ = sk_book_hourly(hs)
|
||||
theirs_s, theirs_t = RF.port_hourly(hs)
|
||||
common = mine_s.index.intersection(theirs_s.index)
|
||||
dmax = float(np.max(np.abs(mine_s.loc[common].values - theirs_s.loc[common].values)))
|
||||
print(f"[A5] {name}: book 1h mio vs loro — max|diff ret giornaliero| {dmax:.2e}, "
|
||||
f"turn/y {mine_t:.2f} vs {theirs_t:.2f}")
|
||||
assert dmax < 1e-10, f"A5 mismatch {name}"
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# B. STATISTICA — lo spike h=0 e' speciale?
|
||||
# ===========================================================================
|
||||
@lru_cache(maxsize=2)
|
||||
def anchor_matrix() -> pd.DataFrame:
|
||||
cols = {f"h{h:02d}": sk_port_daily(h) for h in range(24)}
|
||||
return pd.concat(cols, axis=1, join="inner").dropna()
|
||||
|
||||
|
||||
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_stats(M: np.ndarray, B: int, block: int, rng) -> dict:
|
||||
n, K = M.shape
|
||||
nblocks = int(np.ceil(n / block))
|
||||
g0s, gmaxs, med_all, 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[:, :, k]) for k in range(K)], axis=1)
|
||||
med_others = np.empty_like(Sh)
|
||||
for h in range(K):
|
||||
others = np.delete(Sh, h, axis=1)
|
||||
med_others[:, h] = np.median(others, axis=1)
|
||||
g = Sh - med_others
|
||||
g0s.append(g[:, 0])
|
||||
gmaxs.append(g.max(axis=1))
|
||||
med_all.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(med_all), sh0=np.concatenate(sh0s))
|
||||
|
||||
|
||||
def part_B() -> None:
|
||||
print("\n" + "=" * 100)
|
||||
print("B. STATISTICA — spike h=0 sull'hold-out: speciale o massimo atteso di 24 stime correlate?")
|
||||
print("=" * 100)
|
||||
Mdf = anchor_matrix()
|
||||
Mh = Mdf[Mdf.index >= HOLDOUT].values
|
||||
sh_hold = _sh_mat(Mh.T)
|
||||
med_others_obs = np.median(sh_hold[1:])
|
||||
g0_obs = sh_hold[0] - med_others_obs
|
||||
corr = np.corrcoef(Mh.T)
|
||||
iu = np.triu_indices(24, 1)
|
||||
print(f"hold-out: {Mh.shape[0]} giorni, 24 ancore; Sh h=0 {sh_hold[0]:.3f}, "
|
||||
f"mediana altri {med_others_obs:.3f}, spike osservato g0 = {g0_obs:.3f}")
|
||||
print(f"correlazione daily fra ancore (hold-out): mediana {np.median(corr[iu]):.3f}, "
|
||||
f"min {corr[iu].min():.3f}")
|
||||
|
||||
for blk in (10, 20, 40):
|
||||
bs = block_boot_stats(Mh, B_BOOT, blk, np.random.default_rng(42 + blk))
|
||||
p_any = float(np.mean(bs["gmax"] >= g0_obs))
|
||||
p_g0 = float(np.mean(bs["g0"] <= 0.0))
|
||||
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(max-spike di UNA QUALSIASI ancora >= {g0_obs:.2f}) = "
|
||||
f"{p_any:.3f} | P(g0<=0) = {p_g0:.3f} | CI95 g0 [{ci_g0[0]:+.2f},{ci_g0[1]:+.2f}] "
|
||||
f"| CI95 Sh mediana-ancore [{ci_med[0]:+.2f},{ci_med[1]:+.2f}] "
|
||||
f"| CI95 Sh h=0 [{ci_sh0[0]:+.2f},{ci_sh0[1]:+.2f}]")
|
||||
|
||||
# hold-out finti: l'ancora migliore per finestra e' stabile?
|
||||
print("\n finestre annuali (hold-out finti) — best/worst anchor, h=0, spread:")
|
||||
print(f" {'finestra':<9} {'best':>5} {'ShBest':>7} {'worst':>6} {'ShWorst':>8} "
|
||||
f"{'mediana':>8} {'h=0':>6} {'pctl h0':>8} {'max-med':>8}")
|
||||
years = [2020, 2021, 2022, 2023, 2024]
|
||||
windows: list[tuple[str, pd.DataFrame]] = [
|
||||
(str(y), Mdf[Mdf.index.year == y]) for y in years] + [("2025+", Mdf[Mdf.index >= HOLDOUT])]
|
||||
sh_by_win = {}
|
||||
from scipy.stats import spearmanr
|
||||
for name, W in windows:
|
||||
sh = _sh_mat(W.values.T)
|
||||
sh_by_win[name] = sh
|
||||
pctl0 = float((sh < sh[0]).mean() + 0.5 * (sh == sh[0]).mean()) * 100
|
||||
print(f" {name:<9} {int(np.argmax(sh)):>5} {sh.max():>7.3f} {int(np.argmin(sh)):>6} "
|
||||
f"{sh.min():>8.3f} {np.median(sh):>8.3f} {sh[0]:>6.3f} {pctl0:>7.0f}° "
|
||||
f"{sh.max() - np.median(sh):>8.3f}")
|
||||
names = [n for n, _ in windows]
|
||||
print("\n stabilita' del ranking ancore (Spearman fra finestre consecutive):")
|
||||
for a, b in zip(names[:-1], names[1:]):
|
||||
rho, p = spearmanr(sh_by_win[a], sh_by_win[b])
|
||||
print(f" {a} vs {b}: rho={rho:+.2f} (p={p:.2f})")
|
||||
# l'ancora migliore di ogni finestra, quanto rende NELLE ALTRE finestre? (pctl medio)
|
||||
print(" best-anchor di ogni finestra valutata nelle ALTRE finestre (pctl medio su 24):")
|
||||
for name in names:
|
||||
h_star = int(np.argmax(sh_by_win[name]))
|
||||
pct = [float((sh_by_win[o] < sh_by_win[o][h_star]).mean()) * 100
|
||||
for o in names if o != name]
|
||||
print(f" best({name}) = h={h_star:>2} -> pctl medio altrove {np.mean(pct):.0f}° "
|
||||
f"(per finestra: {', '.join(f'{p:.0f}' for p in pct)})")
|
||||
|
||||
# ritorno totale hold-out per ancora (per la narrativa '+3.5%')
|
||||
tot = np.prod(1 + Mh, axis=0) - 1
|
||||
print(f"\n ritorno TOTALE hold-out per ancora: min {tot.min():+.1%} / mediana "
|
||||
f"{np.median(tot):+.1%} / max {tot.max():+.1%} (h=0: {tot[0]:+.1%})")
|
||||
dd = [al._dd_ret(pd.Series(Mh[:, k])) for k in range(24)]
|
||||
print(f" maxDD hold-out per ancora: min {min(dd):.1%} / mediana {np.median(dd):.1%} / "
|
||||
f"max {max(dd):.1%} (h=0: {dd[0]:.1%}) [B&H 50/50 2025-26: DD ~60%]")
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# C. TRANCHING — gratis davvero?
|
||||
# ===========================================================================
|
||||
def part_C() -> None:
|
||||
print("\n" + "=" * 100)
|
||||
print("C. TRANCHING — identita' EW, turnover, DD vs rotazione tipica, significativita' IS")
|
||||
print("=" * 100)
|
||||
|
||||
# C1: K=4 book == EW dei 4 book ancorati? (identita' esatta, incluse fee)
|
||||
for a in ASSETS:
|
||||
df = get1h(a)
|
||||
c = df["close"].values.astype(float)
|
||||
r = np.zeros(len(c)); r[1:] = c[1:] / c[:-1] - 1.0
|
||||
hs = (0, 6, 12, 18)
|
||||
pos_e = sk_pos_hourly(a, hs)
|
||||
net_e = pos_e * r - FEE * np.abs(np.diff(pos_e, prepend=0.0)); net_e[0] = 0.0
|
||||
nets_1 = []
|
||||
turns_1 = []
|
||||
for h in hs:
|
||||
p = sk_pos_hourly(a, (h,))
|
||||
t = np.abs(np.diff(p, prepend=0.0))
|
||||
n1 = p * r - FEE * t; n1[0] = 0.0
|
||||
nets_1.append(n1)
|
||||
turns_1.append(t.sum())
|
||||
ew = np.mean(nets_1, axis=0)
|
||||
turn_e = np.abs(np.diff(pos_e, prepend=0.0)).sum()
|
||||
print(f"[C1] {a}: max|net K4 - EW(4 book singoli)| = {np.max(np.abs(net_e - ew)):.2e} ; "
|
||||
f"turnover K4 {turn_e:.1f} vs media singoli {np.mean(turns_1):.1f} "
|
||||
f"(rapporto {turn_e / np.mean(turns_1):.4f})")
|
||||
|
||||
# C2: tutte le rotazioni (mie): livelli e dispersione, DD compreso
|
||||
fams = {"singole(24)": [(h,) for h in range(24)],
|
||||
"K=2(12)": [(h, h + 12) for h in range(12)],
|
||||
"K=4(6)": [tuple(h + 6 * j for j in range(4)) for h in range(6)]}
|
||||
stats = {}
|
||||
for fam, rots in fams.items():
|
||||
rec = []
|
||||
for hs in rots:
|
||||
s, t, _ = sk_book_hourly(hs)
|
||||
f, i, ho = sh3(s)
|
||||
rec.append(dict(hs=hs, full=f, is_=i, hold=ho, dd=al._dd_ret(s),
|
||||
dd_h=al._dd_ret(s[s.index >= HOLDOUT]), turn=t))
|
||||
stats[fam] = pd.DataFrame(rec)
|
||||
print("\n[C2] rotazioni complete (book 1h, misura identica per tutte):")
|
||||
print(f" {'famiglia':<12} {'IS med[min,max]':>24} {'HOLD med[min,max]':>26} "
|
||||
f"{'maxDD med[min,max]':>24} {'turn/y med':>10}")
|
||||
for fam, T in stats.items():
|
||||
print(f" {fam:<12} {T.is_.median():>8.3f} [{T.is_.min():.3f},{T.is_.max():.3f}]"
|
||||
f" {T.hold.median():>9.3f} [{T.hold.min():+.3f},{T.hold.max():+.3f}]"
|
||||
f" {T.dd.median():>8.1%} [{T.dd.min():.1%},{T.dd.max():.1%}]"
|
||||
f" {T.turn.median():>8.2f}")
|
||||
s24, _, _ = sk_book_hourly(tuple(range(24)))
|
||||
f24, i24, h24 = sh3(s24)
|
||||
print(f" K=24 IS {i24:.3f} HOLD {h24:+.3f} maxDD {al._dd_ret(s24):.1%}")
|
||||
T1 = stats["singole(24)"]
|
||||
T4 = stats["K=4(6)"]
|
||||
print(f"\n -> claim 'maxDD 14.7->11.9': h=0 singolo DD {T1.dd.iloc[0]:.1%} ma la MEDIANA "
|
||||
f"delle 24 singole e' {T1.dd.median():.1%}; K=4 mediano {T4.dd.median():.1%} "
|
||||
f"=> beneficio del tranching vs ancora TIPICA = {T1.dd.median() - T4.dd.median():+.1%}pt, "
|
||||
f"vs h=0 = {T1.dd.iloc[0] - T4.dd.median():+.1%}pt (in gran parte 'h=0 era sfortunato sul DD')")
|
||||
print(f" -> claim 'IS 1.49->1.54/1.56': mediana IS delle 24 singole = {T1.is_.median():.3f} "
|
||||
f"(K=4 mediano {T4.is_.median():.3f}) => il 'miglioramento' e' tornare alla MEDIA delle "
|
||||
f"ancore, h=0 era al {(T1.is_ < T1.is_.iloc[0]).mean() * 100:.0f}° pctl IS")
|
||||
|
||||
# C3: significativita' IS del K=4 vs h=0 (bootstrap appaiato a blocchi)
|
||||
s0 = sk_book_hourly((0,))[0]
|
||||
s4 = sk_book_hourly((0, 6, 12, 18))[0]
|
||||
common = s0.index.intersection(s4.index)
|
||||
A = s4.loc[common]; Bser = s0.loc[common]
|
||||
mask = common < HOLDOUT
|
||||
Ai, Bi = A[mask].values, Bser[mask].values
|
||||
n = len(Ai)
|
||||
nblocks = int(np.ceil(n / BLOCK))
|
||||
d_obs = al._sh(A[mask]) - al._sh(Bser[mask])
|
||||
ds = []
|
||||
rng = np.random.default_rng(7)
|
||||
for _ in range(B_BOOT // 500):
|
||||
starts = rng.integers(0, n, size=(500, nblocks))
|
||||
idx = (starts[:, :, None] + np.arange(BLOCK)[None, None, :]) % n
|
||||
idx = idx.reshape(500, -1)[:, :n]
|
||||
Ra, Rb = Ai[idx], Bi[idx]
|
||||
sa = Ra.mean(1) / Ra.std(1) * np.sqrt(365.25)
|
||||
sb = Rb.mean(1) / Rb.std(1) * np.sqrt(365.25)
|
||||
ds.append(sa - sb)
|
||||
ds = np.concatenate(ds)
|
||||
print(f"\n[C3] IS: Sh(K4) - Sh(h0) = {d_obs:+.3f}; bootstrap appaiato (block {BLOCK}, "
|
||||
f"B={len(ds)}): CI95 [{np.percentile(ds, 2.5):+.3f}, {np.percentile(ds, 97.5):+.3f}], "
|
||||
f"P(diff<=0) = {np.mean(ds <= 0):.3f}")
|
||||
# e vs l'ancora mediana (piu' onesto): K4 confrontato con OGNI singola
|
||||
dvs = [d for h in range(24)
|
||||
for d in [al._sh(A[mask]) - al._sh(sk_book_hourly((h,))[0].loc[common][mask])]]
|
||||
print(f" Sh_IS(K4) - Sh_IS(singola h) sulle 24 ancore: min {min(dvs):+.3f} / "
|
||||
f"mediana {np.median(dvs):+.3f} / max {max(dvs):+.3f} "
|
||||
f"-> vs ancora tipica il guadagno IS e' ~{np.median(dvs):+.2f}, non +0.05/+0.07")
|
||||
|
||||
# C4: small-cap $600 (mia implementazione min-order)
|
||||
print("\n[C4] small-cap $600 (min order $5, quota 0.5/asset):")
|
||||
for name, hs in (("K=1 h0", (0,)), ("K=2", (0, 12)), ("K=4", (0, 6, 12, 18)),
|
||||
("K=24", tuple(range(24)))):
|
||||
nets_r, nets_m, ntr = {}, {}, 0
|
||||
for a in ASSETS:
|
||||
df = get1h(a)
|
||||
c = df["close"].values.astype(float)
|
||||
r = np.zeros(len(c)); r[1:] = c[1:] / c[:-1] - 1.0
|
||||
t = 0.5 * sk_pos_hourly(a, hs)
|
||||
held = np.empty(len(t)); cur = 0.0
|
||||
for i in range(len(t)):
|
||||
if abs(t[i] - cur) * 600.0 >= 5.0:
|
||||
cur = t[i]; ntr += 1
|
||||
held[i] = cur
|
||||
pos = np.zeros(len(held)); pos[1:] = held[:-1]
|
||||
nr = pos * r - FEE * np.abs(np.diff(pos, prepend=0.0)); nr[0] = 0.0
|
||||
posm = np.zeros(len(t)); posm[1:] = t[:-1]
|
||||
nm = posm * r - FEE * np.abs(np.diff(posm, prepend=0.0)); nm[0] = 0.0
|
||||
idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
|
||||
nets_r[a] = pd.Series(nr, index=idx); nets_m[a] = pd.Series(nm, index=idx)
|
||||
Jr = pd.concat(nets_r, axis=1, join="inner").fillna(0.0)
|
||||
Jm = pd.concat(nets_m, axis=1, join="inner").fillna(0.0)
|
||||
dr = al._to_daily(Jr["BTC"] + Jr["ETH"]); dm = al._to_daily(Jm["BTC"] + Jm["ETH"])
|
||||
yrs = len(dr) / 365.25
|
||||
print(f" {name:<8} Sh real {al._sh(dr):.3f} (model {al._sh(dm):.3f}, haircut "
|
||||
f"{al._sh(dm) - al._sh(dr):+.3f}) trade/y {ntr / yrs:.0f}")
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# D. IMPATTO sui numeri del progetto (blend SKH e book 5-sleeve, TP01 per ancora)
|
||||
# ===========================================================================
|
||||
def part_D() -> None:
|
||||
print("\n" + "=" * 100)
|
||||
print("D. IMPATTO — blend TP+SKH 75/25 e book 5-sleeve con TP01 alle 24 ancore")
|
||||
print("=" * 100)
|
||||
from src.portfolio.portfolio import combine_outer, to_daily
|
||||
try:
|
||||
from src.portfolio.sleeves import (_gtaa_daily_returns, _skyhook_returns,
|
||||
_vrp_combo_returns, _xsec_returns)
|
||||
skh = to_daily(_skyhook_returns())
|
||||
except Exception as e:
|
||||
print(f" [SKIP] sleeve non calcolabili: {type(e).__name__}: {e}")
|
||||
return
|
||||
|
||||
def hold_sh(s: pd.Series) -> float:
|
||||
return al._sh(s[s.index >= HOLDOUT])
|
||||
|
||||
# blend deribit book 75/25
|
||||
blends = []
|
||||
for h in range(24):
|
||||
tp = sk_port_daily(h)
|
||||
b = combine_outer({"TP": tp, "SKH": skh}, {"TP": 0.75, "SKH": 0.25})
|
||||
b = b[b.index >= tp.index.min()]
|
||||
blends.append(hold_sh(b))
|
||||
b24 = combine_outer({"TP": sk_book_hourly(tuple(range(24)))[0], "SKH": skh},
|
||||
{"TP": 0.75, "SKH": 0.25})
|
||||
print(f"[D1] blend 0.75*TP01(h)+0.25*SKH — Sharpe HOLD: h=0 {blends[0]:.2f} | "
|
||||
f"min {min(blends):.2f} / mediana {np.median(blends):.2f} / max {max(blends):.2f} | "
|
||||
f"TP=K24 {hold_sh(b24[b24.index >= sk_port_daily(0).index.min()]):.2f} "
|
||||
f"(claim del progetto: 0.31 -> 1.17)")
|
||||
|
||||
# book 5-sleeve (pesi CLAUDE.md), attivazione era crypto
|
||||
try:
|
||||
cols_fixed = dict(XS=to_daily(_xsec_returns()), VRP=to_daily(_vrp_combo_returns()),
|
||||
SKH=skh, GTAA=to_daily(_gtaa_daily_returns()))
|
||||
except Exception as e:
|
||||
print(f" [SKIP 5-sleeve] {type(e).__name__}: {e}")
|
||||
return
|
||||
W = dict(TP=0.33, XS=0.15, VRP=0.12, SKH=0.20, GTAA=0.20)
|
||||
lo = sk_port_daily(0).index.min()
|
||||
books = []
|
||||
for h in range(24):
|
||||
cols = dict(TP=sk_port_daily(h), **cols_fixed)
|
||||
s = combine_outer(cols, W, lo=lo)
|
||||
books.append((hold_sh(s), al._sh(s)))
|
||||
bh = [b[0] for b in books]; bf = [b[1] for b in books]
|
||||
s24b = combine_outer(dict(TP=sk_book_hourly(tuple(range(24)))[0], **cols_fixed), W, lo=lo)
|
||||
print(f"[D2] book 5-sleeve (TP 33/XS 15/VRP 12/SKH 20/GTAA 20) — Sharpe HOLD: "
|
||||
f"h=0 {bh[0]:.2f} | min {min(bh):.2f} / mediana {np.median(bh):.2f} / max {max(bh):.2f} "
|
||||
f"| TP=K24 {hold_sh(s24b):.2f}")
|
||||
print(f" Sharpe FULL: h=0 {bf[0]:.2f} | min {min(bf):.2f} / mediana {np.median(bf):.2f} "
|
||||
f"/ max {max(bf):.2f} | TP=K24 {al._sh(s24b):.2f}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
part_A()
|
||||
part_B()
|
||||
part_C()
|
||||
part_D()
|
||||
print("\nFatto (scettico).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user