Files
quant-trading/research/prod_impact.py
Gahow Wang 92af4e5ac0 feat(research): Tiingo backfill of delisted names; honest PIT panels
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.
2026-10-08 09:48:05 -07:00

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()