""" Free-tier walk-forward accuracy seed. Generates a small set of historical forecast_snapshots with honest statistical p50 forecasts and true closes so /accuracy is non-empty without paid APIs. Usage (from backend/ with venv): python scripts/seed_accuracy_backtest.py --symbols AAPL,MSFT,GOOGL --horizons 1,5,20 --windows 30 Keeps Yahoo + Turso writes tiny (default ~ a few hundred rows). """ from __future__ import annotations import argparse import asyncio import os import sys import time from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from dotenv import load_dotenv load_dotenv() from app.services.accuracy_service import close_on_or_before, compute_errors from app.services.data_service import StockDataService from app.services.database_service import DatabaseService from app.services.timesfm_service import TimesFMService async def seed( symbols: list[str], horizons: list[int], windows: int, delay: float, ) -> dict: db = DatabaseService(url=os.getenv("TURSO_URL"), token=os.getenv("TURSO_TOKEN")) data = StockDataService() model = TimesFMService() # Free-tier seed: skip heavy model load; statistical path is honest and labeled model.available = False model.model = None written = 0 evaluated = 0 try: await db.initialize() for symbol in symbols: hist = await data.get_stock_data(symbol, period="2y") if hist is None or len(hist) < 80: print(f"[skip] {symbol}: insufficient history") continue closes = [float(x) for x in hist["Close"].tolist()] # Walk back from end: use windows ending before last bar max_h = max(horizons) end_idx = len(closes) - 1 start_idx = max(60, end_idx - windows - max_h) for i in range(start_idx, end_idx - max_h): context = closes[: i + 1] entry = context[-1] # Forecast date ≈ bar timestamp ts = hist.index[i] forecast_date = int(ts.timestamp()) // 86400 * 86400 for h in horizons: target_i = i + h if target_i >= len(closes): continue pred = await model.predict(context, horizon=h) p50 = float(pred["quantiles"]["p50"][-1]) # Insert snapshot as if made on forecast_date await db._execute( """ INSERT OR IGNORE INTO forecast_snapshots (symbol, country, exchange, forecast_date, horizon_days, target_date, p50_price, entry_price, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, [ symbol.upper(), "us", "", forecast_date, h, forecast_date + h * 86400, p50, entry, forecast_date, ], ) written += 1 # Evaluate immediately with known actual actual = float(closes[target_i]) pct_err, direction_ok = compute_errors(entry, p50, actual) await db._execute( """ UPDATE forecast_snapshots SET actual_price = ?, percentage_error = ?, direction_correct = ?, evaluated_at = ? WHERE symbol = ? AND horizon_days = ? AND forecast_date = ? AND evaluated_at IS NULL """, [ actual, pct_err, direction_ok, int(time.time()), symbol.upper(), h, forecast_date, ], ) evaluated += 1 print(f"[ok] {symbol}") await asyncio.sleep(delay) finally: await db.close() return {"written": written, "evaluated": evaluated} def main(): p = argparse.ArgumentParser() p.add_argument("--symbols", default="AAPL,MSFT,GOOGL,AMZN,META,NVDA,JPM,XOM,JNJ,WMT") p.add_argument("--horizons", default="1,5,20,60") p.add_argument("--windows", type=int, default=40, help="Lookback windows per symbol (keep small)") p.add_argument("--delay", type=float, default=0.3) args = p.parse_args() symbols = [s.strip().upper() for s in args.symbols.split(",") if s.strip()] horizons = [int(x) for x in args.horizons.split(",") if x.strip()] # Cap to protect free Turso write quota windows = min(args.windows, 60) result = asyncio.run(seed(symbols, horizons, windows, args.delay)) print(result) if __name__ == "__main__": main()