diff --git a/scripts/surfer/xvenue_sim_oos2.py b/scripts/surfer/xvenue_sim_oos2.py new file mode 100644 index 000000000..2a2c171ca --- /dev/null +++ b/scripts/surfer/xvenue_sim_oos2.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""HONEST cross-venue OOS: track the FIXED venue pair chosen at entry (not the daily max-min). + +The earlier sim booked daily max-min funding (assumes you always hold the optimal venue pair = too +optimistic; capture 0.68 shows ~32% reshuffle loss). Realistic: at entry pick A=argmax venue, +B=argmin; hold THAT pair; book realized = funding_A - funding_B on the FIXED pair each day; exit +when your pair's spread decays below exit. Clean OOS split (IS-pick config -> OOS-blind). +""" +import json +import math + +import numpy as np + +PANEL = "data/surfer/xvenue2/panel2.json" +COST = 0.0010 + + +def main(): + panel = json.load(open(PANEL)) + dates = sorted(set().union(*[set(v) for v in panel.values()])) + coins = list(panel) + T = len(dates) + F = {c: panel[c] for c in coins} # F[coin][date] = {venue: daily_funding} + split = int(0.60 * T) + print(f"HONEST cross-venue OOS (fixed held pair): {len(coins)} coins, {T} days") + print(f" IS {dates[0]}..{dates[split-1]} | OOS {dates[split]}..{dates[-1]}") + + def hyst(t0, t1, K, entry, exit_, cost): + held = {} # coin -> (A, B) + rets, prev = [], set() + for t in range(t0, t1 - 1): + dt, dn = dates[t], dates[t + 1] + # exit: keep held coins whose FIXED-pair spread still > exit + keep = {} + for c, (A, B) in held.items(): + ft = F[c].get(dt) + if ft and A in ft and B in ft and (ft[A] - ft[B]) > exit_: + keep[c] = (A, B) + held = keep + # entry: rank fresh coins by current max-min, add above entry hurdle + cands = [] + for c in coins: + if c in held: + continue + ft = F[c].get(dt) + if ft and len(ft) >= 2: + A = max(ft, key=ft.get); B = min(ft, key=ft.get) + if ft[A] - ft[B] > entry: + cands.append((ft[A] - ft[B], c, A, B)) + cands.sort(reverse=True, key=lambda x: x[0]) + for sp, c, A, B in cands: + if len(held) >= K: + break + held[c] = (A, B) + # realized NEXT day on the FIXED pair (short A receive f_A, long B receive -f_B) + if held: + tot = 0.0 + for c, (A, B) in held.items(): + fn = F[c].get(dn) + if fn and A in fn and B in fn: + tot += fn[A] - fn[B] + realized = tot / len(held) + else: + realized = 0.0 + cur = set(held) + turn = len(cur ^ prev) / max(len(cur), 1) if cur else 0 + rets.append(realized - turn * (cost / 2)); prev = cur + r = np.array(rets) + ann = r.mean() * 365 if len(r) else float("nan") + vol = r.std() * math.sqrt(365) if len(r) else float("nan") + return ann, (ann / vol if vol > 0 else float("nan")), r + + grid = [(K, e, x) for K in (10, 20) for e in (0.0005, 0.0010, 0.0020) for x in (0.0003, 0.0005, 0.0010) if x <= e] + scored = [(cfg, hyst(0, split, *cfg, COST)[1], hyst(split, T, *cfg, COST)[1], hyst(split, T, *cfg, COST)[0]) for cfg in grid] + best = max(scored, key=lambda s: s[1]) + cfg, is_sh, oos_sh, oos_ann = best + print(f"\n IS-BEST (blind to OOS): K={cfg[0]} entry={cfg[1]*1e4:.0f}bp exit={cfg[2]*1e4:.0f}bp") + print(f" IS Sharpe {is_sh:+.1f} -> OOS Sharpe {oos_sh:+.1f} | OOS ann {100*oos_ann:+.1f}% (realistic ~/2.5 = {oos_sh/2.5:+.1f})") + good = [s for s in scored if s[1] > 2]; robust = [s for s in good if s[2] > 1] + print(f" robustness: {len(robust)}/{len(good)} IS-good configs also OOS Sharpe>1") + _, _, r = hyst(split, T, *cfg, COST) + mo = [dates[t][:7] for t in range(split, T - 1)] + print(" OOS per-month Sharpe:") + for m in sorted(set(mo)): + seg = r[np.array(mo) == m] + if len(seg) > 6 and seg.std() > 0: + print(f" {m}: {seg.mean()/seg.std()*math.sqrt(365):+.1f}") + print("\n VERDICT (honest, fixed-pair): OOS Sharpe>1 realistic + months positive = genuinely deployable.") + + +if __name__ == "__main__": + main()