Spaces:
Running
Running
File size: 11,589 Bytes
134649f ea01671 134649f ea01671 134649f 261cef9 ea01671 134649f ea01671 134649f ea01671 134649f ea01671 134649f 2071eed 134649f 2071eed 134649f 2071eed 91d902c 261cef9 91d902c ea01671 2071eed 91d902c ea01671 2071eed ea01671 134649f 2071eed ea01671 91d902c 134649f ea01671 91d902c 134649f ea01671 134649f ea01671 2071eed 134649f 2071eed 134649f 2071eed 134649f ea01671 134649f ea01671 134649f 2071eed 261cef9 134649f ea01671 134649f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 | """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))
|