The +14.9 OOS Sharpe was the max-min optimism (assumes daily free re-positioning to optimal venue pair). Honest fixed-pair version (hold the entry-chosen pair, book its actual funding diff): OOS Sharpe collapses +14.9->+0.8, OOS ann +15.3%->+1.1% (realistic ~breakeven net of cost), 0/12 configs robust OOS, feb month -8.2. Capture 0.68 was the tell (32% reshuffle loss). Cross-venue arb joins the cost-walled/in-sample-only list. User's OOS insistence caught it. Only deployable thing remains simple 60/40 (~0.7). Every crypto-funding variant marginal once honestly tested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
93 lines
4.1 KiB
Python
93 lines
4.1 KiB
Python
#!/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()
|