fund-style-drift / scripts /pregenerate.py
Nav772's picture
Week 7-9: pre-generation script, 33 funds generated
9c8e40b
Raw
History Blame Contribute Delete
4.61 kB
# scripts/pregenerate.py
#
# Pre-generates fund style drift reports for all 33 funds in the universe.
# Outputs one JSON file per fund to outputs/reports/.
# Run this script to refresh the gallery before deploying to HF Spaces.
#
# Usage:
# python -m scripts.pregenerate
# python -m scripts.pregenerate --tickers QQQ ARKK VFINX (subset)
# python -m scripts.pregenerate --refresh (re-run all, overwrite existing)
import os
import json
import argparse
import time
from pathlib import Path
from agent.agent import run_agent
OUTPUT_DIR = Path("outputs/reports")
FUND_UNIVERSE = [
# ETFs
"SPY", "VOO", "QQQ", "IWM", "SPYG", "SPYV", "VTV", "VUG",
"MTUM", "QUAL", "USMV", "SPLV", "VYM", "DGRO", "ARKK", "ARKG",
"XLK", "XLF", "XLE", "XLV",
# Mutual funds
"VFINX", "VWUSX", "FCNTX", "FXAIX", "AGTHX", "AIVSX",
"PRGFX", "PRFDX", "MFEKX", "LMOPX", "CGMFX", "SEQUX", "DODGX"
]
def save_report(ticker: str, report: dict) -> Path:
"""Save a report dict to outputs/reports/{ticker}.json"""
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
path = OUTPUT_DIR / f"{ticker}.json"
# Charts are large JSON strings - keep them in the output
# but strip rolling_windows from the drift section to reduce file size.
# Rolling windows are only needed for chart rendering, which is done at generation time.
report_to_save = report.copy()
if report_to_save.get("drift"):
report_to_save["drift"] = {
"num_windows": report_to_save["drift"]["num_windows"],
"drift_events": report_to_save["drift"]["drift_events"],
# rolling_windows excluded - already baked into chart JSON
}
with open(path, "w") as f:
json.dump(report_to_save, f, indent=2)
return path
def run_pregeneration(tickers: list, refresh: bool = False) -> None:
results = []
total = len(tickers)
print(f"\nPre-generating {total} fund reports...")
print(f"Output directory: {OUTPUT_DIR.resolve()}")
print(f"Refresh mode: {refresh}")
print("="*60)
for i, ticker in enumerate(tickers, 1):
output_path = OUTPUT_DIR / f"{ticker}.json"
# Skip if already exists and not refreshing
if output_path.exists() and not refresh:
print(f"[{i}/{total}] {ticker}: skipping (already exists)")
results.append((ticker, "skipped", None))
continue
print(f"[{i}/{total}] {ticker}: running...")
try:
report = run_agent(ticker)
pipeline_error = report.get("error")
if pipeline_error:
print(f" ERROR: {pipeline_error}")
results.append((ticker, "failed", pipeline_error))
continue
path = save_report(ticker, report)
size_kb = path.stat().st_size / 1024
narrative_len = len(report.get("narrative", "") or "")
holdings_error = report.get("holdings", {}).get("error")
status = "ok" if not holdings_error else "ok (no holdings)"
print(f" Done: {size_kb:.0f}KB, narrative={narrative_len} chars, status={status}")
results.append((ticker, status, None))
except Exception as e:
print(f" EXCEPTION: {e}")
results.append((ticker, "exception", str(e)))
# Small delay between funds to avoid rate limiting
if i < total:
time.sleep(2)
# Final summary
print("\n" + "="*60)
print("PRE-GENERATION SUMMARY")
print("="*60)
ok = [r for r in results if r[1] in ("ok", "ok (no holdings)")]
skipped = [r for r in results if r[1] == "skipped"]
failed = [r for r in results if r[1] in ("failed", "exception")]
print(f"Success: {len(ok)}/{total}")
print(f"Skipped: {len(skipped)}/{total}")
print(f"Failed: {len(failed)}/{total}")
if failed:
print("\nFailures:")
for ticker, status, error in failed:
print(f" {ticker}: {error}")
print(f"\nReports saved to: {OUTPUT_DIR.resolve()}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Pre-generate fund style drift reports")
parser.add_argument(
"--tickers",
nargs="+",
help="Specific tickers to generate (default: all 33)"
)
parser.add_argument(
"--refresh",
action="store_true",
help="Re-run and overwrite existing reports"
)
args = parser.parse_args()
tickers = args.tickers if args.tickers else FUND_UNIVERSE
tickers = [t.upper() for t in tickers]
run_pregeneration(tickers, refresh=args.refresh)