#!/usr/bin/env python3 """Low-turnover rescue test: gross cross-venue spread is +21% with 0.71 persistence, but naive daily rebalancing is cost-killed. Test if HYSTERESIS (hold a coin until its spread decays below an exit floor) + lower maker-fee cost flips net positive. If yes -> real but needs patient execution; if no -> cost-walled.""" import json import math import numpy as np panel = json.load(open("data/surfer/xvenue2/panel2.json")) dates = sorted(set().union(*[set(v) for v in panel.values()])) di = {d: i for i, d in enumerate(dates)} coins = list(panel) T, N = len(dates), len(coins) spread = np.full((T, N), np.nan) for j, c in enumerate(coins): for d, v in panel[c].items(): vals = list(v.values()) if len(vals) >= 2: spread[di[d], j] = max(vals) - min(vals) def sim(K, entry, exit_, cost): held = {} # col -> True rets = [] for t in range(T - 1): s_t, s_n = spread[t], spread[t + 1] # drop held coins whose spread decayed below exit (or data gone) held = {j: 1 for j in held if np.isfinite(s_t[j]) and s_t[j] > exit_} # fill up to K from fresh entries above entry hurdle if len(held) < K: cand = [j for j in np.where(np.isfinite(s_t) & (s_t > entry))[0] if j not in held] cand.sort(key=lambda j: -s_t[j]) for j in cand[:K - len(held)]: held[j] = 1 if not held: rets.append(0.0); continue w = 1.0 / len(held) realized = sum(w * s_n[j] for j in held if np.isfinite(s_n[j])) # turnover: this sim only changes the set on entry/exit crossings (low churn) rets.append(realized) sim._prev = set(held) return np.array(rets) def turnover_cost(K, entry, exit_, cost): """Re-run tracking set changes to charge cost properly.""" held, rets, prev = {}, [], set() for t in range(T - 1): s_t, s_n = spread[t], spread[t + 1] held = {j: 1 for j in held if np.isfinite(s_t[j]) and s_t[j] > exit_} if len(held) < K: cand = [j for j in np.where(np.isfinite(s_t) & (s_t > entry))[0] if j not in held] cand.sort(key=lambda j: -s_t[j]) for j in cand[:K - len(held)]: held[j] = 1 cur = set(held) turn = len(cur ^ prev) / max(len(cur), 1) if cur else 0 realized = (sum(s_n[j] for j in held if np.isfinite(s_n[j])) / len(held)) if held else 0.0 rets.append(realized - turn * (cost / 2)); prev = cur return np.array(rets) def stats(r): ann = r.mean() * 365; vol = r.std() * math.sqrt(365) return ann, (ann / vol if vol > 0 else float("nan")) print(f"low-turnover rescue: {N} coins, {T} days") print(f"{'config':>34} {'annNET%':>8} {'Sharpe':>7}") for K in [10, 20]: for entry, exit_ in [(0.0005, 0.0003), (0.0010, 0.0005), (0.0020, 0.0010)]: for cost, cl in [(0.0006, "maker6bp"), (0.0010, "taker10bp")]: r = turnover_cost(K, entry, exit_, cost) a, sh = stats(r) print(f" K={K:>2} entry{entry*1e4:.0f}/exit{exit_*1e4:.0f}bp {cl:>9} {100*a:>+8.1f} {sh:>+7.2f}") print("\nVERDICT: any config net Sharpe>1 = edge survives with patient/low-turnover execution.") print("If all still negative = cost-walled even with hysteresis -> not deployable.")