stock-prediction-api / scripts /seed_accuracy_backtest.py
cramamoorthy's picture
Deploy backend
99938b8
Raw
History Blame Contribute Delete
5.22 kB
"""
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()