#!/usr/bin/env python3 """Stablecoin peg-reversion existence test — the cleanest crypto-FX dislocation. Stablecoins are supposed to be worth $1; mint/redeem arbitrage (forced flow) pulls a deviation back to parity. This tests whether that reversion is harvestable NET OF COST on free Binance spot data, and — critically — how bad the DEATH-SPIRAL tail is (some "depegs" never revert: UST->0, BUSD wind-down). Pre-registered: pair STABLE/USDT, deviation d=close-1. Reversion bet when |d|>thresh: position = -sign(d) (rich->short, cheap->long), hold H days, pnl_net = -sign(d)*(close[t+H]/close[t]-1) - cost_rt. Report per-pair + pooled: trade count, mean net pnl (bp), hit%, WORST trade (bp, the death-spiral tail), and a daily-strategy Sharpe. Cost levels bracket Binance stablecoin fees {0,2,10}bp round-trip. KILL: pooled net mean<=0 OR edge entirely from one terminal name OR worst-trade tail dwarfs mean edge. LIMITATION: overlapping forward windows (first-look existence test, not a hardened backtest); daily granularity (intraday depeg spikes under-sampled). Flagged, not hidden. """ import json import math import os import time import urllib.request import numpy as np OUT = "data/surfer/stablecoin" PAIRS = ["USDCUSDT", "FDUSDUSDT", "TUSDUSDT", "DAIUSDT", "BUSDUSDT", "USDPUSDT"] DAY_MS = 86_400_000 THRESHOLDS_BP = [10, 25, 50] HOLDS = [1, 2, 5] COSTS_BP = [0.0, 2.0, 10.0] def _get(u): return json.load(urllib.request.urlopen(urllib.request.Request(u, headers={"User-Agent": "curl/8"}), timeout=30)) def fetch(sym): cache = f"{OUT}/{sym}.npz" if os.path.exists(cache): d = np.load(cache); return d["ts"], d["close"] start = 1_546_300_800_000 # 2019-01-01 end = int(time.time() * 1000) rows, cur = [], start for _ in range(400): k = _get(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 reversion(close, thresh_bp, H, cost_bp): """Event reversion bet: enter when |d|>thresh, pnl=-sign(d)*(fwd_ret)-cost_rt. Returns net-pnl array (bp).""" d = close - 1.0 th = thresh_bp / 1e4 pnls = [] for t in range(len(close) - H): if abs(d[t]) > th and close[t] > 0: fwd = close[t + H] / close[t] - 1.0 pnl = -np.sign(d[t]) * fwd - cost_bp / 1e4 pnls.append(pnl * 1e4) # in bp return np.array(pnls) def run(): series = {} for s in PAIRS: ts, c = fetch(s) if len(ts) > 60: series[s] = c print("\n========== STABLECOIN PEG-REVERSION (crypto-FX dislocation, free Binance spot daily) ==========") print(f"pairs: {', '.join(f'{s}({len(c)}d)' for s, c in series.items())}\n") # 1) dislocation frequency per pair print("--- dislocation magnitude (|close-1|) per pair ---") print(f"{'pair':>9} {'days':>5} {'med_bp':>7} {'p95_bp':>7} {'max_bp':>8} {'>10bp%':>7} {'>50bp%':>7}") for s, c in series.items(): d = np.abs(c - 1.0) * 1e4 print(f"{s:>9} {len(c):>5} {np.median(d):>7.1f} {np.percentile(d,95):>7.1f} {d.max():>8.0f} " f"{100*np.mean(d>10):>6.1f}% {100*np.mean(d>50):>6.1f}%") # 2) reversion edge, pooled across pairs, by (thresh, H, cost) print("\n--- reversion edge (pooled all pairs); pnl in bp/trade, net of round-trip cost ---") print(f"{'thr_bp':>6} {'H':>2} {'cost':>5} {'trades':>7} {'mean_bp':>8} {'hit%':>6} {'worst_bp':>9} {'daily_SR':>9}") for th in THRESHOLDS_BP: for H in HOLDS: for cost in COSTS_BP: allp = np.concatenate([reversion(c, th, H, cost) for c in series.values()]) if series else np.array([]) if len(allp) < 10: continue sr = (allp.mean() / allp.std() * math.sqrt(365 / H)) if allp.std() > 0 else float("nan") print(f"{th:>6} {H:>2} {cost:>4.0f}b {len(allp):>7} {allp.mean():>+8.1f} " f"{100*np.mean(allp>0):>5.1f}% {allp.min():>+9.0f} {sr:>+9.2f}") print(" " + "-" * 60) # 3) per-pair edge at a fixed mid setting (thr=25bp, H=2, cost=2bp) — is it one terminal name? print("\n--- per-pair edge @ thr=25bp H=2 cost=2bp (is the edge concentrated/terminal?) ---") print(f"{'pair':>9} {'trades':>7} {'mean_bp':>8} {'hit%':>6} {'worst_bp':>9}") for s, c in series.items(): p = reversion(c, 25, 2, 2.0) if len(p) >= 5: print(f"{s:>9} {len(p):>7} {p.mean():>+8.1f} {100*np.mean(p>0):>5.1f}% {p.min():>+9.0f}") else: print(f"{s:>9} {len(p):>7} (too few events)") print("\nKILL if: pooled mean<=0, OR edge is one terminal name, OR |worst_bp| dwarfs mean (death-spiral tail).") if __name__ == "__main__": run()