Download scripts/backtest_fit_policy.py from s-ttp/vitals: direct link, hf CLI and curl.
- Browser
- Download file 5.37 kB
-
https://huggingface.co/spaces/s-ttp/vitals/resolve/main/scripts/backtest_fit_policy.py
- Command line
-
hf download hf://spaces/s-ttp/vitals/scripts/backtest_fit_policy.py
-
curl -L -o backtest_fit_policy.py https://huggingface.co/spaces/s-ttp/vitals/resolve/main/scripts/backtest_fit_policy.py
5.37 kB
| """Policy validation: does the construction rule actually improve risk-adjusted outcomes? | |
| For each seed book (concentrated tech / income / mid-cap), at each quarter-end over the | |
| last ~5 years: estimate the shrinkage covariance from the trailing 2 years, pick 3 adds | |
| from a fixed universe by (a) the greedy variance-reduction rule, (b) random draws (mean of | |
| 30), (c) top trailing-12-month momentum, size each add at 8% pro-rata, hold 12 months, and | |
| record realized volatility, max drawdown, return and Sharpe. Walk-forward: only information | |
| available at the rebalance date is used. Output: data/policy_backtest.json + a markdown table. | |
| Run: .venv/bin/python scripts/backtest_fit_policy.py | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import sys | |
| from pathlib import Path | |
| import numpy as np | |
| import pandas as pd | |
| ROOT = Path(__file__).resolve().parent.parent | |
| sys.path.insert(0, str(ROOT)) | |
| from src import construct as CX # noqa: E402 | |
| from src.market_data import fetch_history, load_instruments # noqa: E402 | |
| SEEDS = { | |
| "tech_concentrated": {"NVDA": 0.35, "AMD": 0.2, "PLTR": 0.15, "TSLA": 0.15, "MSFT": 0.15}, | |
| "income_rates": {"O": 0.25, "DUK": 0.2, "VICI": 0.2, "ENB": 0.2, "SPG": 0.15}, | |
| "quality_midcap": {"CROX": 0.25, "DECK": 0.25, "ETSY": 0.2, "ROKU": 0.15, "CELH": 0.15}, | |
| } | |
| UNIVERSE = ["ED", "NOC", "BSX", "CRH", "MO", "VLO", "EXPD", "JLL", "ES", "AMP", "CMCSA", "CL", | |
| "UBER", "TSM", "AMZN", "VEEV", "LIN", "JNJ", "PG", "KO", "XOM", "CAT", "UNH", "WMT", | |
| "JPM", "BRK-B", "LLY", "ABBV", "PEP", "HD", "GOOGL", "META", "COST", "AVGO", "V"] | |
| UNIVERSE += [i["ticker"] for i in load_instruments()] | |
| W_ADD, HOLD_DAYS, N_RANDOM = 0.08, 252, 30 | |
| def _stats(ret: pd.Series) -> dict: | |
| if len(ret) < 100: | |
| return {} | |
| curve = (1 + ret).cumprod() | |
| dd = float((curve / curve.cummax() - 1).min()) | |
| vol = float(ret.std() * np.sqrt(252)) | |
| tot = float(curve.iloc[-1] - 1) | |
| return {"return": tot, "vol": vol, "max_dd": dd, "sharpe": float(ret.mean() / ret.std() * np.sqrt(252)) if ret.std() > 0 else 0.0} | |
| def _hold(px: pd.DataFrame, w: dict, start, end) -> pd.Series | None: | |
| sub = px.loc[(px.index > start) & (px.index <= end), list(w)] | |
| if len(sub) < 100: | |
| return None | |
| rets = sub.pct_change(fill_method=None).fillna(0.0) | |
| wv = np.array([w[t] for t in sub.columns]); wv = wv / wv.sum() | |
| return pd.Series(rets.to_numpy() @ wv, index=sub.index) | |
| def main() -> None: | |
| tickers = sorted(set(sum((list(s) for s in SEEDS.values()), [])) | set(UNIVERSE)) | |
| px = fetch_history(tickers, period="8y") | |
| px.index = pd.to_datetime(px.index) | |
| px = px.dropna(axis=1, thresh=int(len(px) * 0.6)) | |
| dates = pd.date_range("2021-03-31", "2025-06-30", freq="QE") | |
| rng = np.random.default_rng(0) | |
| results: dict = {} | |
| for seed_name, seed in SEEDS.items(): | |
| seed = {t: w for t, w in seed.items() if t in px.columns} | |
| rows = {"greedy": [], "random": [], "momentum": [], "seed_only": []} | |
| for d in dates: | |
| hist = px.loc[px.index <= d] | |
| end = d + pd.Timedelta(days=int(HOLD_DAYS * 365 / 252)) | |
| if end > px.index[-1]: | |
| break | |
| univ = [t for t in UNIVERSE if t in hist.columns and t not in seed | |
| and int(hist[t].iloc[-500:].notna().sum()) >= 400] | |
| if len(univ) < 6: | |
| continue | |
| rets = CX.returns_matrix(hist, None, list(seed) + univ) | |
| cov, tick, _ = CX.shrunk_cov(rets) | |
| cands = [{"ticker": t, "fit": {"score": 0.0}} for t in univ if t in tick] | |
| picks = [p["ticker"] for p in CX.greedy_select(seed, cands, cov, tick, max_adds=3, w_add=W_ADD)] | |
| mom = hist[univ].iloc[-1] / hist[univ].iloc[-252] - 1 | |
| mom_picks = list(mom.dropna().sort_values(ascending=False).index[:3]) | |
| def book(adds): | |
| w = {t: v * (1 - W_ADD * len(adds)) for t, v in seed.items()} | |
| for a in adds: | |
| w[a] = W_ADD | |
| return w | |
| for name, adds in (("greedy", picks), ("momentum", mom_picks), ("seed_only", [])): | |
| r = _hold(px, book(adds), d, end) | |
| if r is not None: | |
| rows[name].append(_stats(r)) | |
| rand = [] | |
| for _ in range(N_RANDOM): | |
| adds = list(rng.choice(univ, size=3, replace=False)) | |
| r = _hold(px, book(adds), d, end) | |
| if r is not None: | |
| rand.append(_stats(r)) | |
| if rand: | |
| rows["random"].append({k: float(np.mean([x[k] for x in rand])) for k in rand[0]}) | |
| results[seed_name] = {name: {k: round(float(np.mean([x[k] for x in v])), 4) for k in v[0]} | {"n_periods": len(v)} | |
| for name, v in rows.items() if v} | |
| out = ROOT / "data" / "policy_backtest.json" | |
| out.write_text(json.dumps(results, indent=1)) | |
| print("| seed | policy | periods | avg 12m return | avg vol | avg max DD | avg Sharpe |") | |
| print("|---|---|---|---|---|---|---|") | |
| for s, pols in results.items(): | |
| for p, m in pols.items(): | |
| print(f"| {s} | {p} | {m['n_periods']} | {m['return'] * 100:+.1f}% | {m['vol'] * 100:.1f}% | {m['max_dd'] * 100:.1f}% | {m['sharpe']:.2f} |") | |
| print("saved", out) | |
| if __name__ == "__main__": | |
| main() | |