"""Slipstream — target-book computation (runs on the HF Space). Portable (pandas/numpy only): reads the shipped bundle in data/ and reproduces the Slipstream rule — regime overlay + dynamic sector cap — so momentum and sector strength are computed on the Space at rebalance time. The regime test additionally extends SPY with live Alpaca closes, so the single biggest switch the fund makes never lags the bundle. """ from __future__ import annotations import json import threading import time from pathlib import Path import numpy as np import pandas as pd DATA = Path(__file__).resolve().parent / "data" TOPN, NAME_CAP, MOM_N = 20, 5, 50 CAP_BASE, CAP_LO, CAP_HI = 0.30, 0.15, 0.45 MAX_POS_W = 0.10 # max weight in any ONE position (v2, Oct 2026: Study 7 — same Sharpe as 15%, top-5 48% vs 56%) MIN_POS_W = 0.01 # min weight in any one position (see _name_floor) _CACHE_TTL_S = 15 * 60 def _load(): cand = pd.read_parquet(DATA / "candidates.parquet") px = pd.read_parquet(DATA / "prices.parquet") meta = json.loads((DATA / "meta.json").read_text()) wide = px.pivot_table(index="date", columns="ticker", values="px", aggfunc="last").sort_index() wide.index = pd.to_datetime(wide.index) return cand, wide, meta def _dyn_caps(wide, sectors, universe): """30% ± each sector's trailing 12m-1m relative strength vs SPY, clipped [15,45]%.""" if "SPY" not in wide or len(wide) < 260: return {} secs = sorted(set(sectors.get(t, "?") for t in universe) - {"?"}) sret = {s: wide[[t for t in universe if sectors.get(t) == s and t in wide.columns]] .pct_change(fill_method=None).mean(axis=1) for s in secs} sidx = (1 + pd.DataFrame(sret).fillna(0)).cumprod() spyidx = (1 + wide["SPY"].pct_change(fill_method=None).fillna(0)).cumprod() rs = (sidx.iloc[-21] / sidx.iloc[-252] - 1) - (spyidx.iloc[-21] / spyidx.iloc[-252] - 1) return {s: float(np.clip(CAP_BASE + rs.get(s, 0.0), CAP_LO, CAP_HI)) for s in secs} def _apply_sector_cap(w, sectors, caps): sec = pd.Series({t: sectors.get(t, "?") for t in w.index}) held = list(sec.unique()) cv = {s: caps.get(s, CAP_BASE) for s in held} if sum(cv.values()) < 1.0: f = 1 / sum(cv.values()); cv = {s: min(1.0, c * f) for s, c in cv.items()} w = w.copy() for _ in range(30): sw = w.groupby(sec).sum() over = {s: sw[s] for s in sw.index if sw[s] > cv.get(s, 1.0) + 1e-9} if not over: break under = ~sec.isin(list(over)); ex = 0.0 for s, val in over.items(): nm = sec[sec == s].index; ex += w[nm].sum() - cv[s]; w[nm] *= cv[s] / val if w[under].sum() <= 0: break w[under] += ex * w[under] / w[under].sum() return w / w.sum() def _spy_live(wide): """The bundle's SPY closes, extended with live Alpaca daily bars past the bundle's last date. Returns (series, through_date_str | None). The rest of the book (momentum, sector RS) keeps using bundle prices: their 12-1m windows are insensitive to a couple of weeks of lag, and extending 970 tickers live is not worth the quota. No-op on any failure.""" spx = wide["SPY"].dropna() if "SPY" in wide else pd.Series(dtype=float) if spx.empty: return spx, None try: import alpaca_client as alpaca last = spx.index.max() bars = alpaca.spy_series((last + pd.Timedelta(days=1)).strftime("%Y-%m-%d")) live = {} for b in bars: d = pd.to_datetime(b["t"], utc=True).tz_convert("America/New_York").normalize().tz_localize(None) if d > last: live[d] = float(b["c"]) if not live: return spx, None ext = pd.Series(live).sort_index() return pd.concat([spx, ext]), str(ext.index.max().date()) except Exception: return spx, None def _regime_on(spx: pd.Series): """SPY at/above its 200-day MA now AND ~1 month ago (confirmation) → risk-on.""" if spx is None or len(spx) < 221: return True spx = spx.ffill() now = spx.iloc[-1] >= spx.iloc[-200:].mean() prev = spx.iloc[-22] >= spx.iloc[-221:-21].mean() return bool(now and prev) def _pick_tickers(cand, sectors, n, excluded=frozenset()): """Top-n tickers by combined score (≤NAME_CAP/sector), skipping any excluded name — so an excluded holding is deterministically replaced by the next-ranked candidate.""" picks, sec_ct = [], {} for _, r in cand.sort_values("score", ascending=False).iterrows(): t = r["ticker"] if t in excluded: continue s = sectors.get(t, "?") if sec_ct.get(s, 0) >= NAME_CAP: continue sec_ct[s] = sec_ct.get(s, 0) + 1 picks.append(t) if len(picks) >= n: break return picks def _name_cap(w, cap=MAX_POS_W): """Clip any single position to `cap`, redistributing the excess pro-rata. Raw 13F-dollar weighting concentrates the book into a handful of mega-caps (the live book ran ~78% in its top 5, an effective breadth of ~6 names for a nominal 20-stock portfolio, with the tail rounding to sub-$100 positions). A per-name cap keeps the dollar-conviction tilt — which the backtest shows is genuinely informative, equal-weight is materially WORSE — while removing the extreme. See Quant Model/scripts/weighting_study.py: over 2014-2026 (top-20, monthly, 40bps) every cap from 8-30% beat the uncapped book on Sharpe, a smooth plateau rather than a single lucky point; 15% gave the best drawdown-adjusted return (Calmar 0.475 vs 0.387 uncapped, maxDD -35.0% vs -39.5%). Rule v2 (Oct 2026) uses 10%: on the concentrated-manager + ROA book it kept Sharpe (0.98 vs 0.98, 2016-26) while cutting the top-5 weight from 56% to 48% (data/research/slipstream/quality, Study 7).""" for _ in range(50): over = w > cap + 1e-12 if not over.any(): break excess = float((w[over] - cap).sum()) w[over] = cap under = ~over if w[under].sum() <= 0: break w[under] += excess * w[under] / w[under].sum() return w / w.sum() def _name_floor(w, floor=MIN_POS_W): """Raise any position below `floor` to it, funding the lift pro-rata from the weight the other names carry ABOVE the floor. The cap above only truncates the top of the dollar-weighted book. The 13F `size` column spans ~140× (ONON $0.5B → GOOGL $74B), so the tail still rounded to nothing: the live 2026-09 target had BSX at 0.03%, PODD 0.01%, DINO 0.07% — $14–$71 on a $98k account, below the executor's $493 min-trade deadband, so those names were never bought and the "20-stock" fund permanently held 16. A 1% floor ($~1k) is the smallest weight that clears the deadband with margin at $100k. Not part of weighting_study.py (which only tested caps); it moves ~3-5% of the book from the top names to the tail.""" for _ in range(50): under = w < floor - 1e-12 if not under.any(): break need = float((floor - w[under]).sum()) w[under] = floor pool = (w[~under] - floor).clip(lower=0) if pool.sum() <= 0: break w[~under] -= need * pool / pool.sum() return w / w.sum() def _base_book(cand, wide, sectors, universe, excluded=frozenset()): """Top-20 by combined score (≤5/sector, minus excluded), $-weighted with a per-name cap AND floor, then dynamic sector cap.""" picks = _pick_tickers(cand, sectors, TOPN, excluded) sel = cand[cand["ticker"].isin(picks)].drop_duplicates("ticker").set_index("ticker").loc[picks] w = sel["size"].clip(lower=0).astype(float) if w.sum() <= 0: w = pd.Series(1.0, index=picks) # The constraints interact: the sector cap redistributes weight pro-rata and can push a # name back above MAX_POS_W or below MIN_POS_W. Alternate them until all hold (converges # in a few passes; the final sector pass is the binding one). caps = _dyn_caps(wide, sectors, universe) w = w / w.sum() for _ in range(20): w = _apply_sector_cap(_name_floor(_name_cap(w)), sectors, caps) if w.max() <= MAX_POS_W + 1e-6 and w.min() >= MIN_POS_W - 1e-6: break return w def _momentum_book(wide, universe, sectors): """Risk-off sleeve: top-50 large-cap by 12m-1m return, equal-weight.""" if len(wide) < 253: return pd.Series(dtype=float) u = [t for t in universe if t in wide.columns] mom = (wide[u].iloc[-21] / wide[u].iloc[-252] - 1).dropna() pick = mom.nlargest(MOM_N).index return pd.Series(1.0 / len(pick), index=pick) def _compute_book(screen: bool = True) -> dict: cand, wide, meta = _load() sectors = meta.get("sectors", {}) universe = meta.get("universe", []) spx, spy_through = _spy_live(wide) on = _regime_on(spx) screen_meta = {"enabled": False, "scanned": 0, "cleared": 0, "excluded": [], "as_of": None} if on: # AI event-risk overlay: screen the candidate pool (final 20 + substitutes) for concrete # adverse events and hard-exclude high/critical; excluded names are replaced by the # next-ranked candidate inside _base_book. Degrades to no exclusions on any failure. excluded = set() if screen: try: import risk_screen pool = _pick_tickers(cand, sectors, TOPN + 6) scr = risk_screen.screen(pool) excluded, screen_meta = scr["excluded"], scr["meta"] except Exception: pass w = _base_book(cand, wide, sectors, universe, excluded=excluded) book = "base" else: w = _momentum_book(wide, universe, sectors) # broad 50-name rotation — event screen n/a book = "momentum" holdings = [{"ticker": t, "weight": round(float(x), 5), "sector": sectors.get(t, "?")} for t, x in w.sort_values(ascending=False).items()] prices_through = str(wide.index.max().date()) return { "fund": "Slipstream", "signals_as_of": meta.get("as_of"), "prices_through": prices_through, "spy_through": spy_through or prices_through, "regime": "risk-on · smart-money book" if on else "risk-off · momentum rotation", "book": book, "n_holdings": len(holdings), "holdings": holdings, "risk_screen": screen_meta, "rule": "concentrated-manager 13F consensus · top-1000 · quality(ROA top-30% in sector) · top-20 $-wt " "(1–10%/name) · dynamic sector cap 15–45% · AI critical-event exclusion · regime→momentum (SPY live) · monthly", } _cache_lock = threading.Lock() _cache: dict = {} def compute_book(screen: bool = True, max_age_s: float = _CACHE_TTL_S) -> dict: """Target book, memoised for 15 minutes. Every dashboard load and dry-run preview calls this on a public Space (a 2 MB parquet pivot + Alpaca + the daily-cached risk screen), and the inputs only change on deploy or the daily screen — pass max_age_s=0 for the real rebalance so it always trades the freshest book.""" with _cache_lock: hit = _cache.get(screen) if hit and max_age_s > 0 and time.time() - hit[0] < max_age_s: return hit[1] result = _compute_book(screen) with _cache_lock: _cache[screen] = (time.time(), result) return result if __name__ == "__main__": print(json.dumps(compute_book(), indent=1))