Files
Adriano Dal Pastro 73d74c5e53 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>
2026-07-02 22:12:22 +00:00

516 lines
24 KiB
Python

#!/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()