#!/usr/bin/env python3 """Crypto TS-trend sizing — reconfirm the validated return engine + find the deployable CAGR. The TSMOM crypto sleeve is VALIDATED (memory: Sharpe ~1.23, CAGR 75.7% @ 61% vol, corr~0 to funding). The 76% CAGR is just 61%-vol sizing, not a free lunch. This re-runs the SAME pre-registered config (LONG/FLAT, lookbacks 20/60/120, on crypto_pit daily close, liquidity floor on qvol) and sweeps a vol-target overlay to answer the #2 question: at a SANE vol target, what is the deployable CAGR and does the Sharpe hold (it should — vol-target is a rescale, but per-period sizing adds timing). NOT a search: the 20/60/120 long/flat config is replicated verbatim from the validated harness. """ import glob import math import numpy as np PIT = "data/surfer/crypto_pit" LOOKBACKS = [20, 60, 120] COST_BP = 5.0 ADV_FLOOR = 5e6 # $5M trailing quote-volume liquidity floor VOL_TARGETS = [0.10, 0.15, 0.20] LEV_CAP = 3.0 PPY = 365 # crypto trades every day def build(): days = set() raw = {} for f in sorted(glob.glob(f"{PIT}/*.npz")): d = np.load(f) if len(d["day"]) < 130: continue sym = f.split("/")[-1][:-4] raw[sym] = {int(dd): (c, q) for dd, c, q in zip(d["day"], d["close"], d["qvol"])} days.update(int(x) for x in d["day"]) days = sorted(days) di = {d: i for i, d in enumerate(days)} syms = sorted(raw) C = np.full((len(days), len(syms)), np.nan) V = np.full((len(days), len(syms)), np.nan) for j, s in enumerate(syms): for dd, (c, q) in raw[s].items(): C[di[dd], j] = c; V[di[dd], j] = q return np.array(days), syms, C, V def maxdd(equity): peak = np.maximum.accumulate(equity) return float((equity / peak - 1).min()) def run(): days, syms, C, V = build() T, N = C.shape logC = np.log(C) ret = np.full((T, N), np.nan) ret[1:] = C[1:] / C[:-1] - 1.0 # long/flat signal = mean over lookbacks of 1[trailing-L log return > 0] sig = np.zeros((T, N)) cnt = np.zeros((T, N)) for L in LOOKBACKS: tr = np.full((T, N), np.nan) tr[L:] = logC[L:] - logC[:-L] on = np.where(np.isfinite(tr), (tr > 0).astype(float), np.nan) m = np.isfinite(on) sig[m] += on[m]; cnt[m] += 1 sig = np.where(cnt > 0, sig / np.maximum(cnt, 1), 0.0) # 0..1 long/flat conviction print(f"\n===== CRYPTO TS-TREND SIZING (long/flat {LOOKBACKS}, ${ADV_FLOOR/1e6:.0f}M ADV floor, {COST_BP}bp/leg) =====") print(f"panel: {T} days x {N} coins (survivorship-free incl. dead)\n") # base book (gross 1, daily rebalance, equal-weight by conviction among liquid+on coins) w_prev = np.zeros(N) book = np.zeros(T) for t in range(1, T): eligible = np.isfinite(ret[t]) & np.isfinite(V[t - 1]) & (V[t - 1] > ADV_FLOOR) s = sig[t - 1] * eligible g = s.sum() w = s / g if g > 0 else np.zeros(N) turn = np.abs(w - w_prev).sum() book[t] = float(np.nansum(w * ret[t])) - turn * COST_BP / 1e4 w_prev = w valid = np.arange(T) > max(LOOKBACKS) base = book[valid] base_sr = base.mean() / base.std() * math.sqrt(PPY) if base.std() > 0 else float("nan") base_vol = base.std() * math.sqrt(PPY) base_cagr = float(np.prod(1 + base) ** (PPY / len(base)) - 1) print(f"BASE (un-vol-targeted): Sharpe {base_sr:+.2f} vol {base_vol*100:.0f}% CAGR {base_cagr*100:+.1f}% maxDD {maxdd(np.cumprod(1+base))*100:.1f}%") print(f" (memory reference: Sharpe ~1.23, CAGR ~76% @ ~61% vol — replicating)\n") print(f"{'vol_tgt':>8} {'Sharpe':>7} {'CAGR':>7} {'real_vol':>9} {'maxDD':>7} {'avg_lev':>8} {'per-year SR':>30}") print("-" * 86) # vol-target overlay on the base book (trailing 30d realized, lagged, leverage-capped) rv = np.full(T, np.nan) for t in range(31, T): w = book[t - 30:t] rv[t] = w.std() * math.sqrt(PPY) yrs = 1970 + days // 365 for vt in VOL_TARGETS: lev = np.where(np.isfinite(rv) & (rv > 0), np.clip(vt / rv, 0, LEV_CAP), 0.0) tgt = book * np.concatenate([[0], lev[:-1]]) # lag leverage by 1 day s = tgt[valid] sr = s.mean() / s.std() * math.sqrt(PPY) if s.std() > 0 else float("nan") vol = s.std() * math.sqrt(PPY) cagr = float(np.prod(1 + s) ** (PPY / len(s)) - 1) dd = maxdd(np.cumprod(1 + s)) avglev = float(np.mean(lev[valid])) yv = {} for r, y in zip(s, yrs[valid]): yv.setdefault(int(y), []).append(r) ystr = " ".join(f"{y}:{(np.mean(v)/(np.std(v)+1e-12)*math.sqrt(PPY)):+.1f}" for y, v in sorted(yv.items()) if len(v) >= 30) print(f"{vt*100:>6.0f}% {sr:>+7.2f} {cagr*100:>+6.1f}% {vol*100:>8.0f}% {dd*100:>+6.1f}% {avglev:>8.2f} {ystr}") print("-" * 86) print("Deployable read: pick the vol target whose maxDD you can stomach; Sharpe should ~hold across targets.") if __name__ == "__main__": run()