Yahoo purges delisted members: the S&P research panel was missing 209 ever-members (AGN/CELG/ESRX/ATVI/TWTR/XLNX/...), the production store 89, and the S&P 400/600 panels ~180 more. The gap inflated momentum-family factors (overnight_mom holdout 16.9->14.8%/yr, rec_126 14.7->10.7 on the research panel; factor_panel_check). research/lab/fetch_tiingo.py pulls full EOD histories (free tier: 500 unique symbols/month, 50/hour; paced, resume-safe, key never logged). research/lab/tiingo_merge.py overlays the caches on the research S&P panel at build time (>=50% PIT-coverage gate, median-overlap cross-check, stub-column replacement, rename splices GPS<-GAP, DISCA<-WBD, WLTW<-WTW, ADS<-BFH, CCE<-CCEP, ESV<-VAL, FBHS<-FBIN, HFC<-DINO) and can apply the same to the production store (--production [--apply]; data/us*.csv, .bak-tiingo backups, under the panel lock). load_us() rebuilds when the overlay changes (_tiingo_sig); fetch_us_mid.load_bars falls back to converted CSVs. Round 1 (2026-10-08): 165/209 S&P names and 183 mid/small PIT members restored (2011-19 snapshots were 15-20% short of PIT members). prod_impact A/B on registered replays: overnight_recovery_top10 23.8%/0.83/-38.3% vs 27.8%/0.88/-47.3% backfill-off; lowrisk_top20 6.1%/0.58 vs 8.6%/0.60 - honest long returns with shallower drawdowns; trend_rider_v10 (ETF path) unchanged at 55.0%/1.64. Official precompute refreshed (backup at data/precomputed/backtest_results.json.pretiingo). midsmall_scan on the corrected panel: no factor beats the EW benchmark (0.60 holdout Sharpe vs 0.51-0.52 best) - no mid/small sleeve.
76 lines
2.4 KiB
Python
76 lines
2.4 KiB
Python
"""A/B the Tiingo backfill's effect on registered replays.
|
|
|
|
Runs the requested strategies through backtest_runner on the current
|
|
production store, then again with the pre-backfill snapshot (data/us*.csv
|
|
.bak-tiingo) swapped in, and prints CAGR / Sharpe / MaxDD deltas. Files are
|
|
swapped under the data_manager panel lock and restored afterwards.
|
|
|
|
PYTHONPATH=. uv run python -m research.prod_impact [STRATEGY ...]
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import sys
|
|
|
|
import metrics
|
|
import backtest_runner as br
|
|
import data_manager
|
|
|
|
FILES = ["us.csv", "us_open.csv", "us_high.csv", "us_low.csv", "us_volume.csv"]
|
|
START = "2018-01-01"
|
|
|
|
|
|
def run(names: list[str]) -> dict:
|
|
r = br.run_market("us", names, None, 10_000.0, 0.001, 2.0, refresh=False, start=START)
|
|
out = {}
|
|
for s in r["strategy_runs"]:
|
|
eq = s["equity"]
|
|
out[s["name"]] = {
|
|
"cagr": metrics.annualized_return(eq),
|
|
"sharpe": metrics.sharpe_ratio(eq.pct_change().dropna()),
|
|
"mdd": metrics.max_drawdown(eq),
|
|
}
|
|
return out
|
|
|
|
|
|
def swap(pre_backfill: bool) -> None:
|
|
"""Swap the .bak-tiingo snapshot in or out (live files preserved as .merged-tmp)."""
|
|
for f in FILES:
|
|
live = os.path.join("data", f)
|
|
bak = live + ".bak-tiingo"
|
|
tmp = live + ".merged-tmp"
|
|
if not os.path.exists(bak) and not os.path.exists(tmp):
|
|
continue
|
|
if pre_backfill:
|
|
if os.path.exists(bak):
|
|
shutil.copy2(live, tmp)
|
|
shutil.copy2(bak, live)
|
|
else:
|
|
if os.path.exists(tmp):
|
|
shutil.copy2(tmp, live)
|
|
os.remove(tmp)
|
|
|
|
|
|
def main() -> None:
|
|
names = sys.argv[1:] or ["overnight_recovery_top10", "lowrisk_top20"]
|
|
with data_manager._panel_lock("us"):
|
|
print("=== merged (Tiingo backfill) ===")
|
|
a = run(names)
|
|
swap(pre_backfill=True)
|
|
try:
|
|
print("=== pre-backfill snapshot ===")
|
|
b = run(names)
|
|
finally:
|
|
with data_manager._panel_lock("us"):
|
|
swap(pre_backfill=False)
|
|
print("\nstrategy cagr% sharpe mdd%")
|
|
for n in a:
|
|
print(f"{n:34s} {a[n]['cagr']*100:6.1f}->{b[n]['cagr']*100:6.1f} "
|
|
f"{a[n]['sharpe']:5.2f}->{b[n]['sharpe']:5.2f} "
|
|
f"{a[n]['mdd']*100:6.1f}->{b[n]['mdd']*100:6.1f}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|