Files
foxhunt/scripts/surfer/crypto_funding_oos.py
jgrusewski df5c591441 spec(crypto): funding harvest — clean OOS validated + deployable spec
Clean OOS test (filter chosen on 2019-24, applied blind to 2025-26): IS-best filter holds
OOS Sharpe +5.1; 9/12 strong-IS filters also OOS-positive; naive no-filter FAILS OOS (-5.8)
= validated, not curve-fit. Wrote deployable spec: delta-neutral long-spot/short-perp harvest,
tf30>5bp regime filter, equal-weight, daily rebalance, 10bp cost budget. Counterparty/exchange
tail (-100pct) is THE risk (Sharpe-blind) -> venue selection + collateral spreading + withdrawal
discipline + deleverage kill-switch. Realistic ~2-3.6 Sharpe market-neutral (after basis-vol
haircut). Phased rollout: paper-forward -> micro-live -> small-live -> scale, gate each.
The one validated edge past the 0.7 ceiling; price of admission is crypto + counterparty risk.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 01:25:23 +02:00

93 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""Clean OOS validation of the funding-harvest regime filter.
Protocol (no peeking): choose the regime-filter params on IN-SAMPLE 2019-2024 by IS Sharpe, then
apply that EXACT filter BLIND to OUT-OF-SAMPLE 2025-2026. If the IS-best filter still works OOS,
the edge is validated, not curve-fit. Also reports the full grid's OOS so we can see if it's
robust across reasonable filters (real) or only the lucky one (suspicious). Strategy structure
fixed; only the regime filter (trailing window, hurdle) is the tuned degree of freedom.
Raw Sharpe is funding-only (vol understated ~2-3x); realistic ~ raw / 2.5.
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import pit_sweep # noqa: E402
def sharpe(r):
r = r[np.isfinite(r)]
return float("nan") if len(r) < 20 or r.std() == 0 else r.mean() / r.std() * math.sqrt(365)
def apr(r):
r = r[np.isfinite(r)]
return 100 * r.sum() * 365 / max(len(r), 1)
def main():
syms, days, close, qv, fund = pit_sweep.load()
T, N = fund.shape
year = (1970 + days / 365.25).astype(int)
qv30 = np.full_like(qv, np.nan)
for t in range(30, T):
qv30[t] = np.nanmean(qv[t - 30:t], axis=0)
elig = np.isfinite(qv30) & (qv30 > 5e6) & np.isfinite(fund)
f = np.nan_to_num(fund)
# precompute trailing means
trailmean = {}
for tr in (14, 30):
tm = np.full_like(fund, np.nan)
for t in range(tr, T):
tm[t] = np.nanmean(fund[t - tr:t], axis=0)
trailmean[tr] = tm
def harvest(trail, hurdle, c=0.0010):
cond = (fund > hurdle) if trail == 0 else (trailmean[trail] > hurdle)
sig = np.zeros((T, N))
sig[1:] = np.where(elig[1:] & cond[:-1], 1.0, 0.0) # causal
w = sig / np.maximum(sig.sum(1, keepdims=True), 1)
gross = np.sum(w[:-1] * f[1:], axis=1)
turn = np.sum(np.abs(w[1:] - w[:-1]), axis=1)
net = gross - turn * (c / 2)
inmkt = (sig.sum(1)[1:] > 0)
return net, inmkt
yr = year[1:]
IS = yr <= 2024
OOS = yr >= 2025
grid = [(tr, h) for tr in (0, 14, 30) for h in (0.0, 0.0002, 0.0003, 0.0005)]
rows = []
for tr, h in grid:
net, inmkt = harvest(tr, h)
rows.append(dict(tr=tr, h=h, net=net, inmkt=inmkt,
is_sr=sharpe(net[IS]), oos_sr=sharpe(net[OOS]),
oos_apr=apr(net[OOS]), oos_inmkt=float(inmkt[OOS].mean())))
print(f"\n===== CLEAN OOS VALIDATION — IS 2019-2024 / OOS 2025-2026 =====")
print(f"{'filter':>18} {'IS Sharpe':>10} {'OOS Sharpe':>11} {'OOS APR%':>9} {'OOS in-mkt%':>11}")
for r in rows:
nm = "naive thr=0" if (r["tr"] == 0 and r["h"] == 0) else (f"inst>{r['h']*1e4:.0f}bp" if r["tr"] == 0 else f"tf{r['tr']}>{r['h']*1e4:.0f}bp")
print(f"{nm:>18} {r['is_sr']:>+10.1f} {r['oos_sr']:>+11.1f} {r['oos_apr']:>+9.1f} {100*r['oos_inmkt']:>10.0f}")
best = max(rows, key=lambda r: r["is_sr"])
bnm = f"tf{best['tr']}>{best['h']*1e4:.0f}bp" if best["tr"] else f"inst>{best['h']*1e4:.0f}bp"
print(f"\n IS-BEST filter (chosen blind to OOS): {bnm} -> IS Sharpe {best['is_sr']:+.1f}")
print(f" ITS OOS RESULT: Sharpe {best['oos_sr']:+.1f} APR {best['oos_apr']:+.1f}% in-market {100*best['oos_inmkt']:.0f}% of days")
print(f" (realistic Sharpe after basis vol ~ raw / 2.5 = {best['oos_sr']/2.5:+.1f})")
# robustness: how many filters with IS Sharpe>2 also have OOS Sharpe>1
good = [r for r in rows if r["is_sr"] > 2]
robust = [r for r in good if r["oos_sr"] > 1]
print(f"\n robustness: of {len(good)} filters with IS Sharpe>2, {len(robust)} also have OOS Sharpe>1")
print("\nVERDICT: IS-best filter OOS Sharpe>1 (realistic) + broad robustness = VALIDATED, deployable.")
print("If IS-best collapses OOS or only 1 filter works = curve-fit, not real.")
if __name__ == "__main__":
main()