Spaces:
Running
Running
File size: 8,023 Bytes
261cef9 | 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 | #!/usr/bin/env python3
"""Slipstream — data-bundle exporter (runs LOCALLY, where the Sharadar cache lives).
Rule (v2, Oct 2026 — validated in data/research/slipstream/quality, Studies 4–8):
managers every 13F filer that is a concentrated stock-picker that quarter (10–150 positions,
$250M–$100B, top-10 ≥ 40%, filed the prior quarter) — built by
scripts/slipstream/manager_universe.py --sets CONC (no curated list, no insiders)
signal consensus = 2·new + add − 2·exit conviction positions; eligible if > 0; size = 13F $
quality ROA (SF1 ART, latest filing public by the signal date) in the top 30% of its sector,
ranked across the month's top-1,000 universe
This script exports a small, self-contained bundle the HF Space computes the fund from:
data/candidates.parquet — latest eligible names (top-1000 & ROA-30 & consensus BUY),
with score, $-size and sector
data/prices.parquet — daily adj close for the top-1000 universe + SPY, last ~400
trading days (drives regime, momentum and sector-RS on HF)
data/meta.json — as-of date, universe, generated-at
13F is quarterly and ~45d lagged, so re-running this quarterly (and committing the bundle
to the Space) keeps the fund current. Run: python3 engine/export_bundle.py
"""
from __future__ import annotations
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
import pandas as pd
import os as _os
# env-overridable: this runs inside the Refinery container too, where the Quant Model
# code lives at /app/quantmodel rather than on the Mac.
QM = Path(_os.environ.get("QM_DIR", "/Users/sttp/Desktop/Quant Model"))
sys.path.insert(0, str(QM))
import scripts.follow_the_money_backtest as B # noqa: E402
DATA = Path(_os.environ.get("SLIP_BUNDLE_DIR", Path(__file__).resolve().parent.parent / "data"))
LOOKBACK_DAYS = 400 # ≥ 252 for momentum / 200 for regime / 252 for sector-RS
# the concentrated-manager panel written by manager_universe.py (refresh_everything step 7b)
CONC_DIR = Path(_os.environ.get("SLIP_CONC_DIR", QM / "data/research/conc/panels/CONC"))
PULL = Path(_os.environ.get("SLIP_PULL_DIR", QM / "data/raw/ftm_pull"))
ROA_KEEP = 0.30 # top 30% of each sector by ROA
# parity testing only: restrict to signal dates before this day / prices through this day
MAX_DATE = _os.environ.get("SLIP_EXPORT_BEFORE")
PRICES_THROUGH = _os.environ.get("SLIP_PRICES_THROUGH")
def consensus_frame(rows: pd.DataFrame) -> pd.DataFrame:
"""2·new + add − 2·exit among concentrated managers (scripts/slipstream/backtest.signal_frame 'consensus')."""
df = rows.copy()
col = lambda c: df[c].fillna(0.0) if c in df.columns else pd.Series(0.0, index=df.index)
df["score"] = 2 * col("guru_new_buys") + col("guru_adds") - 2 * col("guru_exits")
df["ok"] = df["score"] > 0
df["size"] = col("guru_conv_value").clip(lower=0)
return df
def roa_gate(d: pd.Timestamp, universe: list, sector: dict) -> set:
"""Names whose latest public ROA is in the top ROA_KEEP of their sector across `universe`
(scripts/slipstream/backtest.Data.roa_pct / quality_set 'roa_sector')."""
import duckdb
f = duckdb.connect().execute(f"""
SELECT ticker, CAST(datekey AS DATE) AS dk, TRY_CAST(roa AS DOUBLE) AS roa
FROM read_csv_auto('{PULL / "SF1.csv"}', header=true, all_varchar=true)
WHERE dimension = 'ART' AND datekey IS NOT NULL AND TRY_CAST(roa AS DOUBLE) IS NOT NULL
AND CAST(datekey AS DATE) >= DATE '2012-06-01' AND CAST(datekey AS DATE) <= DATE '{d.date()}'""").df()
f["dk"] = pd.to_datetime(f["dk"])
f = f[f["ticker"].isin(universe)].sort_values("dk")
x = f.groupby("ticker")["roa"].last().to_frame()
x["sector"] = [sector.get(t, "?") for t in x.index]
x["pct"] = x.groupby("sector")["roa"].rank(pct=True)
return set(x.index[x["pct"] > 1 - ROA_KEEP])
def main():
DATA.mkdir(parents=True, exist_ok=True)
B.PANEL, B.PRICES = CONC_DIR / "ftm_panel.parquet", CONC_DIR / "ftm_prices.parquet"
B.DAILY, B.SF1, B.TICKERS = PULL / "DAILY.csv", PULL / "SF1.csv", PULL / "TICKERS.csv"
if not B.PANEL.exists():
raise SystemExit(f"missing {B.PANEL} — run scripts/slipstream/manager_universe.py --sets CONC first")
base_panel = QM / "data/research/ftm_panel.parquet"
if not _os.environ.get("SLIP_CONC_DIR") and base_panel.exists() and B.PANEL.stat().st_mtime < base_panel.stat().st_mtime:
# the concentrated-manager panel must be rebuilt AFTER the base panel in the same run,
# or the bundle would ship last week's 13F signals as if fresh
raise SystemExit(f"stale {B.PANEL}: older than {base_panel} — the conc_panel step did not run")
panel, R, spy, sector, wide = B.load()
if PRICES_THROUGH:
wide = wide.loc[:PRICES_THROUGH]
R = R.loc[:PRICES_THROUGH]
valid = set(R.columns)
bdr = {pd.Timestamp(d): g for d, g in panel.groupby("date")}
# Use the LATEST signal rows in the panel (weekly cadence), not only monthly firsts.
# The monthly-only pick made every live rebalance trade the prior month's signal row,
# which — because 13F filings land mid-quarter (45-day deadline) — lagged the fresh
# quarter by a whole cycle: on 2026-09-01 the fund would have traded the Aug-3 row
# built on Q1 13F, while the Aug-17 weekly row already carried Q2. The backtest's PIT
# semantics assume signals as-of the rebalance date, so freshest-usable matches the
# validated rule more closely, not less. Only the last few dates are screened
# (universe/quality checks hit the multi-GB DAILY/SF1 CSVs per date).
dts = sorted(pd.to_datetime(panel["date"]).unique())
if MAX_DATE:
dts = [x for x in dts if pd.Timestamp(x) < pd.Timestamp(MAX_DATE)]
dts = dts[-6:]
uni = B.mktcap_universe(dts, 1000)
# latest rebalance date with a usable book
d = next((pd.Timestamp(c) for c in reversed(dts)
if bdr.get(pd.Timestamp(c)) is not None and (valid & uni.get(pd.Timestamp(c), set()))), None)
if d is None:
raise SystemExit("no valid rebalance date")
universe = sorted(uni.get(d, set()) & valid)
qual = roa_gate(d, universe, sector)
# candidates: eligible names with a consensus BUY among concentrated managers at d
sf = consensus_frame(bdr[d])
elig = valid & set(universe) & qual
cand = sf[sf["ok"] & sf["ticker"].isin(elig)].copy()
cand["sector"] = cand["ticker"].map(lambda t: sector.get(t, "?"))
cand = cand[["ticker", "sector", "score", "size"]].reset_index(drop=True)
cand.to_parquet(DATA / "candidates.parquet", index=False)
# prices: universe + SPY, last LOOKBACK_DAYS trading days → long form
cols = [t for t in (set(universe) | {"SPY"} | set(cand["ticker"])) if t in wide.columns]
px = wide[cols].tail(LOOKBACK_DAYS)
long = px.reset_index().melt(id_vars=px.index.name or "index", var_name="ticker",
value_name="px").dropna()
long.columns = ["date", "ticker", "px"]
long.to_parquet(DATA / "prices.parquet", index=False)
meta = {
"fund": "Slipstream",
"rule_version": "v2-2026-10 · CONC consensus · ROA top-30% in sector · 10% cap",
"as_of": str(d.date()),
"universe": universe,
"sectors": {t: sector.get(t, "?") for t in cols},
"n_candidates": int(len(cand)),
"price_start": str(px.index.min().date()),
"price_end": str(px.index.max().date()),
"generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC"),
}
(DATA / "meta.json").write_text(json.dumps(meta, indent=1))
print(f"→ bundle in {DATA}")
print(f" as_of {meta['as_of']} · {len(cand)} candidates · {len(universe)} universe · "
f"prices {meta['price_start']}→{meta['price_end']} ({len(long):,} rows)")
if __name__ == "__main__":
main()
|