- 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>
146 lines
6.3 KiB
Python
146 lines
6.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Stablecoin peg-reversion — HARDENED. Make-or-break test of the crypto-FX dislocation edge.
|
|
|
|
The spike (crypto_stablecoin_dislocation.py) found a real reversion edge (SR 3-4) but was blind to:
|
|
(1) survivorship — all sampled stables reverted; the true tail is buying a cheap stable that goes to
|
|
ZERO (UST -75%->delist). (2) overlap inflation. (3) stress slippage. (4) long/short asymmetry.
|
|
|
|
This hardens all four:
|
|
* UST included (the real death-spiral). FRAX excluded (garbage prints). USTC excluded (post-death).
|
|
* NON-OVERLAPPING event walk with explicit exit (revert / max_hold / stop-loss).
|
|
* deviation-SCALED slippage: cost_rt = base_bp + slip_k*|deviation| (big depeg = thin book).
|
|
* LONG-cheap (unbounded -100% tail) vs SHORT-rich (bounded) decomposed separately.
|
|
* collapse filters compared: raw / stop-loss / skip-falling-knife(+stop).
|
|
* per-year sign + BTC-stress correlation proxy (does it lose when crypto crashes?).
|
|
KILL: long-side net<=0 after UST+filters, OR edge only pre-cost, OR loses concentrated in BTC crashes.
|
|
"""
|
|
import math
|
|
import os
|
|
|
|
import numpy as np
|
|
|
|
OUT = "data/surfer/stablecoin"
|
|
HEALTHY = ["USDCUSDT", "FDUSDUSDT", "TUSDUSDT", "BUSDUSDT", "USDPUSDT", "SUSDUSDT", "DAIUSDT"]
|
|
TERMINAL = ["USTUSDT"]
|
|
DAY_MS = 86_400_000
|
|
THRESH_BP = 50
|
|
EXIT_BP = 10
|
|
MAX_HOLD = 5
|
|
BASE_BP = 2.0
|
|
SLIP_K = 0.15 # slippage = 15% of the entry deviation (stress thinness)
|
|
STOP = 0.03 # 3% stop-loss
|
|
|
|
|
|
def fetch(sym):
|
|
import json, time, urllib.request
|
|
cache = f"{OUT}/{sym}.npz"
|
|
if os.path.exists(cache):
|
|
d = np.load(cache); return d["ts"], d["close"]
|
|
g = lambda u: json.load(urllib.request.urlopen(urllib.request.Request(u, headers={"User-Agent": "curl/8"}), timeout=30))
|
|
start, end = 1_546_300_800_000, int(time.time() * 1000)
|
|
rows, cur = [], start
|
|
for _ in range(400):
|
|
k = g(f"https://api.binance.com/api/v3/klines?symbol={sym}&interval=1d&startTime={cur}&limit=1000")
|
|
if not k:
|
|
break
|
|
rows += k; cur = k[-1][0] + DAY_MS
|
|
if len(k) < 1000 or cur >= end:
|
|
break
|
|
time.sleep(0.05)
|
|
if not rows:
|
|
return np.array([]), np.array([])
|
|
d = {int(r[0]): float(r[4]) for r in rows}
|
|
ts = np.array(sorted(d)); close = np.array([d[t] for t in ts])
|
|
os.makedirs(OUT, exist_ok=True); np.savez(cache, ts=ts, close=close)
|
|
return ts, close
|
|
|
|
|
|
def walk(ts, close, mode):
|
|
"""Non-overlapping reversion trades. mode: 'raw'|'stop'|'skip'. Returns list of (dir, year, net_bp, entry_dev_bp)."""
|
|
n = len(close); d = close - 1.0; th = THRESH_BP / 1e4; ex = EXIT_BP / 1e4
|
|
trades = []; i = 1
|
|
while i < n - 1:
|
|
if abs(d[i]) <= th:
|
|
i += 1; continue
|
|
direction = -1.0 if d[i] > 0 else 1.0 # rich->short(-1), cheap->long(+1)
|
|
# skip-falling-knife: only buy a cheap stable if it is NOT still dropping (bounce confirmation)
|
|
if mode == "skip" and direction > 0 and close[i] < close[i - 1]:
|
|
i += 1; continue
|
|
entry = close[i]; j = i + 1; stop = STOP if mode in ("stop", "skip") else 9.9
|
|
while j < n and (j - i) <= MAX_HOLD:
|
|
pnl = direction * (close[j] / entry - 1.0)
|
|
if pnl <= -stop or abs(close[j] - 1.0) < ex:
|
|
break
|
|
j += 1
|
|
j = min(j, n - 1)
|
|
gross = direction * (close[j] / entry - 1.0)
|
|
slip = (BASE_BP + SLIP_K * abs(d[i]) * 1e4) / 1e4
|
|
yr = 1970 + int(ts[i]) // (365 * DAY_MS)
|
|
trades.append((direction, yr, (gross - slip) * 1e4, abs(d[i]) * 1e4))
|
|
i = j + 1
|
|
return trades
|
|
|
|
|
|
def stats(pnls):
|
|
p = np.array(pnls)
|
|
if len(p) < 3:
|
|
return f"n={len(p)} (too few)"
|
|
sr = p.mean() / p.std() * math.sqrt(365 / ((MAX_HOLD + 1) / 2)) if p.std() > 0 else float("nan")
|
|
return f"n={len(p):>4} mean={p.mean():>+7.1f}bp hit={100*np.mean(p>0):>4.1f}% worst={p.min():>+7.0f}bp SR~{sr:>+5.2f}"
|
|
|
|
|
|
def run():
|
|
data = {}
|
|
for s in HEALTHY + TERMINAL:
|
|
ts, c = fetch(s)
|
|
if len(ts) > 30:
|
|
data[s] = (ts, c)
|
|
print("\n===== STABLECOIN PEG-REVERSION — HARDENED (non-overlap, UST-in, slip-scaled, long/short split) =====")
|
|
print(f"pairs: {', '.join(data)} thr={THRESH_BP}bp hold<={MAX_HOLD}d exit<{EXIT_BP}bp slip={BASE_BP}+{SLIP_K}*|dev| stop={STOP*100:.0f}%\n")
|
|
|
|
for mode in ("raw", "stop", "skip"):
|
|
allt = []
|
|
for s in data:
|
|
allt += [(s, *t) for t in walk(*data[s], mode)]
|
|
longs = [t[3] for t in allt if t[1] > 0]
|
|
shorts = [t[3] for t in allt if t[1] < 0]
|
|
ust = [t[3] for t in allt if t[0] == "USTUSDT"]
|
|
print(f"[{mode:>4}] ALL : {stats([t[3] for t in allt])}")
|
|
print(f" LONG(cheap, -100% tail): {stats(longs)}")
|
|
print(f" SHORT(rich, bounded) : {stats(shorts)}")
|
|
print(f" UST trades only : {stats(ust)}")
|
|
print()
|
|
|
|
# per-year (SHORT-only, skip mode = the candidate deployable core)
|
|
print("--- per-year SHORT-rich net mean bp (the bounded-risk core), skip mode ---")
|
|
by_yr = {}
|
|
for s in data:
|
|
for t in walk(*data[s], "skip"):
|
|
if t[0] < 0:
|
|
by_yr.setdefault(t[1], []).append(t[2])
|
|
print(" ".join(f"{y}:{np.mean(v):+.0f}({len(v)})" for y, v in sorted(by_yr.items()) if v))
|
|
|
|
# BTC-stress proxy: does the LONG side lose during BTC crashes?
|
|
bts, btc = fetch("BTCUSDT")
|
|
if len(btc) > 100:
|
|
bret = {int(bts[k]) // DAY_MS: (btc[k] / btc[k - 1] - 1.0) for k in range(1, len(btc))}
|
|
crash, calm = [], []
|
|
for s in data:
|
|
ts, c = data[s]
|
|
for dirn, yr, net, dev in walk(ts, c, "skip"):
|
|
pass
|
|
# simpler: tag each long trade entry day with BTC 1d return
|
|
for s in data:
|
|
ts, c = data[s]; d = c - 1.0; th = THRESH_BP / 1e4
|
|
for i in range(1, len(c) - 1):
|
|
if abs(d[i]) > th and d[i] < 0: # long-cheap entries
|
|
br = bret.get(int(ts[i]) // DAY_MS, 0.0)
|
|
(crash if br < -0.05 else calm).append(1)
|
|
print(f"\nLONG-cheap entries on BTC-crash days (>-5%): {len(crash)} vs calm: {len(calm)} "
|
|
f"({100*len(crash)/max(1,len(crash)+len(calm)):.0f}% in crashes = tail-correlation flag)")
|
|
print("\nREAD: deployable core = SHORT-rich (bounded). LONG-cheap viable only if filters tame the UST tail.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run()
|