feat(surfer): execution-realism stress — honest momentum magnitude ~0.4-0.6
Realism layers on PIT mom_20 TOPK=50: robust to 20bp (+0.59) and liquidity floor (top-50 already deep, no effect); death-spiral exclusion +0.60. Winsorize +/-20%/day cuts to +0.50 and weakens 2021/22 -> edge leans partly on large moves. COMBINED realistic stress +0.18 (CPCV~0); HARSH negative. winsor likely over-conservative -> true deployable Sharpe ~0.4-0.6, not the 0.76 headline. Real but modest edge; recent years strongest. Deploy needs signal-robustness refinement + diversifying 2nd edge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
87
scripts/surfer/pit_realism.py
Normal file
87
scripts/surfer/pit_realism.py
Normal file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execution-realism stress for point-in-time crypto momentum — is the magnitude believable?
|
||||
|
||||
Layers on the realistic frictions that could inflate the paper Sharpe:
|
||||
- cost 10→20→30 bp on turnover (smaller coins cost more)
|
||||
- WINSORIZE per-coin daily returns to ±cap (you cannot fill a short into an 80%/day collapse)
|
||||
- EXCLUDE each coin's final K days (delisting death-spiral: untradeable → zero PnL there)
|
||||
- liquidity FLOOR: only trade coins with trailing dollar-vol > floor
|
||||
If mom_20/30 stays positive EVERY year under the combined stress, the edge magnitude is real.
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import pit_sweep as ps # noqa: E402
|
||||
from signal_sweep import xs_weights, pnl_w, validate, sharpe_t # noqa: E402
|
||||
import torch # noqa: E402
|
||||
|
||||
DEV = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
|
||||
|
||||
def trailing(lc, L):
|
||||
out = np.full_like(lc, np.nan); out[L:] = lc[L:] - lc[:-L]; return out
|
||||
|
||||
|
||||
def main():
|
||||
syms, days, close, qv, fund = ps.load()
|
||||
T, N = close.shape
|
||||
lc = np.log(close)
|
||||
R = np.zeros((T, N)); R[1:] = lc[1:] - lc[:-1]; R = np.where(np.isfinite(R), R, 0.0)
|
||||
fund = np.where(np.isfinite(fund), fund, 0.0)
|
||||
qv = np.nan_to_num(qv)
|
||||
dv30 = np.zeros((T, N))
|
||||
for t in range(30, T):
|
||||
dv30[t] = qv[t - 30:t].mean(axis=0)
|
||||
year = (1970 + days / 365.25).astype(int)
|
||||
|
||||
# death-spiral mask: each coin's final 5 valid days = untradeable
|
||||
tradeable = np.ones((T, N), bool)
|
||||
for j in range(N):
|
||||
idx = np.where(np.isfinite(close[:, j]))[0]
|
||||
if len(idx):
|
||||
tradeable[max(0, idx[-1] - 4):idx[-1] + 1, j] = False
|
||||
|
||||
def variant(cost_bp, winsor, excl_death, dv_floor, L=20, TOPK=50):
|
||||
Reff = (R - fund)
|
||||
if winsor is not None:
|
||||
Reff = np.clip(Reff, -winsor, winsor)
|
||||
if excl_death:
|
||||
Reff = np.where(tradeable, Reff, 0.0)
|
||||
univ = np.zeros((T, N), bool)
|
||||
for t in range(T):
|
||||
elig = np.where((dv30[t] > dv_floor) & np.isfinite(close[t]))[0]
|
||||
if len(elig) == 0:
|
||||
continue
|
||||
k = min(TOPK, len(elig))
|
||||
univ[t, elig[np.argsort(-dv30[t, elig])[:k]]] = True
|
||||
sig = trailing(lc, L).copy(); sig[~univ] = np.nan
|
||||
pnl = pnl_w(xs_weights(sig), Reff, cost_bp=cost_bp)
|
||||
v = validate(pnl, days, 6)
|
||||
py = [sharpe_t(torch.tensor(pnl[year[1:] == y], device=DEV, dtype=torch.float64))
|
||||
if (year[1:] == y).sum() > 30 else float("nan") for y in range(2020, 2027)]
|
||||
return v, py
|
||||
|
||||
print(f"\n===== EXECUTION-REALISM STRESS — PIT momentum (mom_20, TOPK=50) — {N} coins =====")
|
||||
print(f"{'variant':>34} {'full':>6} {'IS':>6} {'OOS':>6} {'CPCVmed':>8} | per-year 2020..2026")
|
||||
cfgs = [
|
||||
("baseline 10bp", dict(cost_bp=10, winsor=None, excl_death=False, dv_floor=0)),
|
||||
("20bp cost", dict(cost_bp=20, winsor=None, excl_death=False, dv_floor=0)),
|
||||
("winsor ±20%", dict(cost_bp=10, winsor=0.20, excl_death=False, dv_floor=0)),
|
||||
("excl death-spiral (last 5d)", dict(cost_bp=10, winsor=None, excl_death=True, dv_floor=0)),
|
||||
("dv-floor $5M/day", dict(cost_bp=10, winsor=None, excl_death=False, dv_floor=5e6)),
|
||||
("COMBINED 20bp+wins20+death+$5M", dict(cost_bp=20, winsor=0.20, excl_death=True, dv_floor=5e6)),
|
||||
("HARSH 30bp+wins10+death+$10M", dict(cost_bp=30, winsor=0.10, excl_death=True, dv_floor=1e7)),
|
||||
]
|
||||
for nm, cfg in cfgs:
|
||||
v, py = variant(**cfg)
|
||||
pys = " ".join(f"{p:>+5.2f}" if not math.isnan(p) else " n/a" for p in py)
|
||||
print(f"{nm:>34} {v['full']:>+6.2f} {v['is_']:>+6.2f} {v['oos']:>+6.2f} {v['med']:>+8.2f} | {pys}")
|
||||
print("\nVERDICT: positive every year under COMBINED/HARSH => believable, sizable edge (not paper).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user