feat(surfer): residual (beta-stripped) momentum upgrade — net +0.40->+0.57 @$20M
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user