research(surfer): crypto edge-falsification probes — intraday/cascade dead, stablecoin peg-reversion validated

- 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>
This commit is contained in:
jgrusewski
2026-06-21 10:40:34 +02:00
parent 6878aac950
commit f4bd1e2432
9 changed files with 1180 additions and 0 deletions

View File

@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""Cascade reversion — do crypto coins BOUNCE after an extreme forced-flow down-move?
Liquidation cascades are forced deleveraging that overshoots; the harvestable consequence is the
mean-reversion bounce. Binance killed the historical liquidation feed (live websocket only), so we
test the PROXY already on disk: extreme negative trailing-H return = a cascade. Pre-registered:
formation: trailing-H log return per coin (cross-sectional)
signal: LONG the bottom-q fraction (biggest losers) — long-only on crashed names
hold: next H hours, NON-OVERLAPPING rebalance
cost: 5 bp / leg on actual weight turnover
two builds: raw_long (equal-weight losers, has crypto beta)
hedged (long losers short equal-weight UNIVERSE basket; beta-neutral, short leg
is the diversified basket NOT a single coin → dodges the short-melt-up ruin
that killed generic XS reversal)
KILL: net annualized SR < 0.5 OR not positive in most years (chrono) → cascade reversion closed.
LIMITATION: npz panel has close only (no volume) → cannot condition on the volume spike that
confirms a true liquidation cascade; the extreme-return tail is the proxy. Flagged, not hidden.
"""
import math
import os
import numpy as np
OUT = "data/surfer/crypto1h"
COST_BP = 5.0
SYMS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "XRPUSDT", "BNBUSDT", "DOGEUSDT", "ADAUSDT", "AVAXUSDT",
"LINKUSDT", "LTCUSDT", "DOTUSDT", "TRXUSDT", "BCHUSDT", "ETCUSDT", "FILUSDT", "ATOMUSDT"]
HOUR_MS = 3_600_000
HOLDS = [1, 2, 4, 8, 12, 24] # formation == hold, hours
Q = 0.20 # bottom quintile = biggest losers
def build_panel():
series = {}
for s in SYMS:
cache = f"{OUT}/{s}.npz"
if not os.path.exists(cache):
continue
d = np.load(cache)
if len(d["ts"]) > 24 * 60:
series[s] = (d["ts"], d["close"])
allts = sorted(set().union(*[set((ts // HOUR_MS).tolist()) for ts, _ in series.values()]))
tindex = {t: i for i, t in enumerate(allts)}
syms = sorted(series)
P = np.full((len(allts), len(syms)), np.nan)
for j, s in enumerate(syms):
ts, c = series[s]
for t, px in zip(ts // HOUR_MS, c):
P[tindex[int(t)], j] = px
yrs = np.array([1970 + int(t) // int(365.25 * 24) for t in allts])
return P, syms, yrs
def ann(rets, per_year_factor):
rets = np.asarray(rets)
if len(rets) < 20 or rets.std() == 0:
return float("nan"), float("nan")
sr = rets.mean() / rets.std()
t = rets.mean() / (rets.std() / math.sqrt(len(rets)))
return sr * per_year_factor, t
def run():
P, syms, yrs = build_panel()
T, N = P.shape
logP = np.log(P)
print(f"\n===== CRYPTO CASCADE REVERSION (long the crashed names, net {COST_BP}bp/leg) =====")
print(f"panel: {T} hours x {N} coins years {int(yrs.min())}-{int(yrs.max())} q={Q} (bottom quintile)")
print("LONG bottom-q losers; hedged = long losers - short equal-wt universe basket\n")
hdr = f"{'hold':>5} {'periods':>8} {'build':>8} {'net_SR':>8} {'t(net)':>7} {'hit%':>6} {'per-year net-SR (chrono)':>30}"
print(hdr); print("-" * len(hdr))
for H in HOLDS:
pf = math.sqrt(24 * 365 / H)
idx = np.arange(H, T - H, H) # need t-H for formation, t+H for fwd
raw, hed, yr_raw, yr_hed = [], [], [], []
w_raw_prev = np.zeros(N); w_hed_prev = np.zeros(N)
for t in idx:
past = logP[t] - logP[t - H]
fwd = logP[t + H] - logP[t]
ok = np.isfinite(past) & np.isfinite(fwd)
n_ok = int(ok.sum())
if n_ok < 6:
continue
okidx = np.where(ok)[0]
k = max(1, int(round(Q * n_ok)))
losers = okidx[np.argsort(past[okidx])[:k]] # most-negative trailing return
# raw long
w_raw = np.zeros(N); w_raw[losers] = 1.0 / k
# hedged: long losers - short equal-weight universe basket
w_hed = np.zeros(N); w_hed[losers] += 1.0 / k; w_hed[okidx] -= 1.0 / n_ok
r_raw = float(np.nansum(w_raw * fwd)) - np.abs(w_raw - w_raw_prev).sum() * COST_BP / 1e4
r_hed = float(np.nansum(w_hed * fwd)) - np.abs(w_hed - w_hed_prev).sum() * COST_BP / 1e4
raw.append(r_raw); hed.append(r_hed)
yr_raw.append(int(yrs[t])); yr_hed.append(int(yrs[t]))
w_raw_prev = w_raw; w_hed_prev = w_hed
if len(raw) < 20:
print(f"{H:>5} (too few periods)"); continue
for name, series, yrl in (("raw_long", raw, yr_raw), ("hedged", hed, yr_hed)):
sr, t = ann(series, pf)
hit = 100.0 * np.mean(np.asarray(series) > 0)
py = {}
for r, y in zip(series, yrl):
py.setdefault(y, []).append(r)
pystr = " ".join(f"{y}:{(np.mean(v)/(np.std(v)+1e-12)):+.2f}"
for y, v in sorted(py.items()) if len(v) >= 10)
tag = f"{H}h" if name == "raw_long" else ""
print(f"{tag:>5} {len(series):>8} {name:>8} {sr:>+8.2f} {t:>+7.2f} {hit:>6.1f} {pystr}")
print("-" * len(hdr))
print("PASS: hedged net SR > 0.5, t>=2, positive in most years. Else cascade reversion closed.")
if __name__ == "__main__":
run()