- 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
5.0 KiB
Python
121 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Stablecoin peg-reversion — INTRADAY EXECUTION validation (the make-or-break-on-fills test).
|
|
|
|
Daily test found short-rich reversion ~+25bp/SR4. This checks it survives REALISTIC intraday execution:
|
|
* 1h bars, REALISTIC fills: enter at NEXT bar's OPEN (reaction lag — you can't transact at the peak),
|
|
exit at the reverting bar's close; pay round-trip spread (calibrated from live book: ~1bp/side).
|
|
* deviation HALF-LIFE: of rich events at hour t, what % still rich at t+1/+2/+4/+8 (do you have time?).
|
|
* short-rich (validated, bounded) focus; long-cheap with skip-falling-knife for contrast.
|
|
Universe = live-tradeable liquid stables only (USDC/FDUSD/TUSD/USDP; DAI/BUSD books are empty).
|
|
KILL: edge gone after next-open fills + spread, OR deviations vanish within 1h (no reaction window).
|
|
"""
|
|
import math
|
|
import os
|
|
|
|
import numpy as np
|
|
|
|
OUT = "data/surfer/stable1h"
|
|
PAIRS = ["USDCUSDT", "FDUSDUSDT", "TUSDUSDT", "USDPUSDT"]
|
|
HOUR_MS = 3_600_000
|
|
THRESHES = [25, 50]
|
|
EXIT_BP = 10
|
|
MAX_HOLD_H = 48
|
|
SPREAD_BP = 1.0 # per side, from live book; round-trip = 2*SPREAD_BP
|
|
|
|
|
|
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["open"], d["close"]
|
|
g = lambda u: json.load(urllib.request.urlopen(urllib.request.Request(u, headers={"User-Agent": "curl/8"}), timeout=30))
|
|
start, end = int(time.time() * 1000) - 3 * 365 * 86_400_000, int(time.time() * 1000)
|
|
rows, cur = [], start
|
|
for _ in range(800):
|
|
k = g(f"https://api.binance.com/api/v3/klines?symbol={sym}&interval=1h&startTime={cur}&limit=1000")
|
|
if not k:
|
|
break
|
|
rows += k; cur = k[-1][0] + HOUR_MS
|
|
if len(k) < 1000 or cur >= end:
|
|
break
|
|
time.sleep(0.04)
|
|
if not rows:
|
|
return np.array([]), np.array([]), np.array([])
|
|
o = {int(r[0]): (float(r[1]), float(r[4])) for r in rows}
|
|
ts = np.array(sorted(o)); op = np.array([o[t][0] for t in ts]); cl = np.array([o[t][1] for t in ts])
|
|
os.makedirs(OUT, exist_ok=True); np.savez(cache, ts=ts, open=op, close=cl)
|
|
return ts, op, cl
|
|
|
|
|
|
def walk(op, cl, thresh, side, skip=False):
|
|
"""Non-overlapping. side='short'(rich) or 'long'(cheap). enter next-bar OPEN, exit revert close, pay spread."""
|
|
n = len(cl); d = cl - 1.0; th = thresh / 1e4; ex = EXIT_BP / 1e4; rt = 2 * SPREAD_BP / 1e4
|
|
out = []; i = 1
|
|
while i < n - 2:
|
|
rich = d[i] > th; cheap = d[i] < -th
|
|
take = (side == "short" and rich) or (side == "long" and cheap)
|
|
if not take:
|
|
i += 1; continue
|
|
if skip and side == "long" and cl[i] < cl[i - 1]: # don't catch a still-falling knife
|
|
i += 1; continue
|
|
direction = -1.0 if rich else 1.0
|
|
entry = op[i + 1] # realistic: fill at next bar open
|
|
j = i + 2
|
|
while j < n and (j - (i + 1)) <= MAX_HOLD_H:
|
|
if abs(cl[j] - 1.0) < ex:
|
|
break
|
|
j += 1
|
|
j = min(j, n - 1)
|
|
gross = direction * (cl[j] / entry - 1.0)
|
|
out.append((gross - rt) * 1e4)
|
|
i = j + 1
|
|
return np.array(out)
|
|
|
|
|
|
def half_life(cl, thresh):
|
|
d = cl - 1.0; th = thresh / 1e4
|
|
ev = np.where(np.abs(d) > th)[0]; ev = ev[ev < len(cl) - 9]
|
|
if len(ev) == 0:
|
|
return None
|
|
return {h: 100 * np.mean(np.abs(d[ev + h]) > th) for h in (1, 2, 4, 8)}
|
|
|
|
|
|
def st(p):
|
|
if len(p) < 3:
|
|
return f"n={len(p)} (few)"
|
|
sr = p.mean() / p.std() * math.sqrt(365 * 24 / (MAX_HOLD_H / 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 PAIRS:
|
|
ts, op, cl = fetch(s)
|
|
if len(ts) > 500:
|
|
data[s] = (op, cl, ts)
|
|
print("\n===== STABLECOIN INTRADAY EXECUTION (1h, next-open fills, spread 2bp rt) =====")
|
|
print(f"pairs: {', '.join(f'{s}({len(v[1])}h)' for s,v in data.items())}\n")
|
|
|
|
print("--- deviation HALF-LIFE: % of events still beyond threshold after H hours (reaction window) ---")
|
|
for s, (op, cl, ts) in data.items():
|
|
for th in THRESHES:
|
|
hl = half_life(cl, th)
|
|
if hl:
|
|
print(f" {s:>9} thr{th}: t+1={hl[1]:.0f}% t+2={hl[2]:.0f}% t+4={hl[4]:.0f}% t+8={hl[8]:.0f}%")
|
|
print()
|
|
|
|
for th in THRESHES:
|
|
sh = np.concatenate([walk(d[0], d[1], th, "short") for d in data.values()]) if data else np.array([])
|
|
lo = np.concatenate([walk(d[0], d[1], th, "long", skip=True) for d in data.values()]) if data else np.array([])
|
|
print(f"thr={th}bp SHORT-rich : {st(sh)}")
|
|
print(f" LONG-cheap*: {st(lo)} (*skip-falling-knife)")
|
|
# per-pair short
|
|
for s, (op, cl, ts) in data.items():
|
|
print(f" {s:>9} short: {st(walk(op, cl, th, 'short'))}")
|
|
print()
|
|
print("READ: edge survives if SHORT-rich stays +ve net of next-open+spread AND half-life>~2h (time to act).")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run()
|