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}")