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:
116
scripts/surfer/crypto_cascade_reversion.py
Normal file
116
scripts/surfer/crypto_cascade_reversion.py
Normal 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()
|
||||
Reference in New Issue
Block a user