- crypto_mft_xsec / mft_*: intraday/MFT XS price edge FALSIFIED (fee-trap) - crypto_cascade_reversion: liquidation-cascade reversion FALSIFIED (continuation) - crypto_trend_sizing: TS-trend return-engine reconfirmed (Sharpe ~1.25) - crypto_stablecoin_dislocation/harden/intraday: short-rich peg-reversion VALIDATED (bounded, uncorrelated); long-cheap = death-spiral trap Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
121 lines
4.9 KiB
Python
121 lines
4.9 KiB
Python
#!/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()
|