Spaces:
Sleeping
Sleeping
| """ | |
| 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() | |