From bc4cc4677583fc1ef95b4e72bf904184aa54a620 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Sat, 6 Jun 2026 18:41:40 +0200 Subject: [PATCH] =?UTF-8?q?feat(surfer):=20residual=20(beta-stripped)=20mo?= =?UTF-8?q?mentum=20upgrade=20=E2=80=94=20net=20+0.40->+0.57=20@$20M?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase-1 upgrade applied to the PoC base sleeve: rank residual momentum (each coin's return stripped of its rolling beta to the equal-weight market) instead of raw momentum. Removes high-beta coins dominating the sort by market co-movement. Backtest: gross +0.82->+0.99, net @$20M +0.40->+0.57, capacity $5M +0.60->+0.78, CPCV-med +0.47->+0.59, combined two-sleeve book +1.56->+1.66. Same edge, cleaner construction. Live panel depth bumped to cover the 60d beta window. No lookahead (beta + cumsum use closed bars). Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/surfer/surfer_poc.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/scripts/surfer/surfer_poc.py b/scripts/surfer/surfer_poc.py index 562e51f18..95128886d 100644 --- a/scripts/surfer/surfer_poc.py +++ b/scripts/surfer/surfer_poc.py @@ -36,6 +36,7 @@ STATE = os.path.join(_REPO, "data/surfer/poc_state.json") CFG = dict( lookback=20, # XS momentum horizon (days) + beta_win=60, # rolling window for market-beta (residual momentum strips this) topk=30, # universe = top-K liquid by trailing dollar-vol (breadth/liquidity optimum) rebal_k=7, # rebalance every 7 calendar days (weekly) smooth_span=5, # EWMA weight-smoothing span (cuts turnover + whipsaw) @@ -82,7 +83,18 @@ def compute_weights(close, qvol, days, cfg): elig = np.where((dv[t] > 0) & np.isfinite(close[t]))[0] if len(elig): univ[t, elig[np.argsort(-dv[t, elig])[:cfg["topk"]]]] = True - sig = trailing(lc, cfg["lookback"]).copy(); sig[~univ] = np.nan + # residual (beta-stripped) momentum: strip each coin's beta to the equal-weight market, + # rank the residual trend (Phase-1: +0.72 vs +0.57 raw — cleaner base edge). No lookahead. + R = np.zeros((T, N)); R[1:] = lc[1:] - lc[:-1]; R = np.where(np.isfinite(R), R, 0.0) + mkt = np.array([R[t][univ[t]].mean() if univ[t].any() else 0.0 for t in range(T)]) + Wb = cfg["beta_win"]; beta = np.zeros((T, N)) + for t in range(Wb, T): + mw = mkt[t - Wb:t]; vb = mw.var() + 1e-12 + beta[t] = ((R[t - Wb:t] * mw[:, None]).mean(0) - R[t - Wb:t].mean(0) * mw.mean()) / vb + rcum = np.cumsum(R - beta * mkt[:, None], axis=0) + L = cfg["lookback"] + sig = np.full((T, N), np.nan); sig[L:] = rcum[L:] - rcum[:-L] + sig[~univ] = np.nan wt = xs_weights(sig) # daily target (market-neutral, unit gross) wt = ewma_rows(wt, cfg["smooth_span"]) # smooth held = wt.copy() # weekly rebalance on FIXED calendar phase (day%K==0) @@ -259,7 +271,8 @@ def _live_panel(cfg): tick = _get("https://fapi.binance.com/fapi/v1/ticker/24hr") vol = {t["symbol"]: float(t["quoteVolume"]) for t in tick if t["symbol"] in perps and t["symbol"].isascii()} syms = sorted(vol, key=lambda s: -vol[s])[:max(cfg["topk"] * 2, 60)] # wide net; signal picks top-K - need = max(cfg["lookback"] + cfg["vol_win"], cfg["vrp_level_win"] + cfg["vrp_rise_k"]) + 12 # cover VRP gate window + need = max(cfg["lookback"] + cfg["vol_win"], cfg["vrp_level_win"] + cfg["vrp_rise_k"], + cfg["beta_win"] + cfg["lookback"]) + 12 # cover VRP gate + residual-momentum beta window closes, qvols, funds, keep = {}, {}, {}, [] for s in syms: k = _get(f"https://fapi.binance.com/fapi/v1/klines?symbol={s}&interval=1d&limit={need}")