diff --git a/stock-lab/app/screener/router.py b/stock-lab/app/screener/router.py index 3cc81b6..c57224e 100644 --- a/stock-lab/app/screener/router.py +++ b/stock-lab/app/screener/router.py @@ -6,6 +6,7 @@ import datetime as dt import json import os import sqlite3 +from typing import Optional from fastapi import APIRouter, HTTPException @@ -198,3 +199,78 @@ def post_run(body: schemas.RunRequest): telegram_payload=schemas.TelegramPayload(**payload), warnings=result.warnings, ) + + +# ---------- /snapshot/refresh ---------- + +from . import snapshot as _snap + + +@router.post("/snapshot/refresh") +def post_snapshot_refresh(asof: Optional[str] = None): + asof_date = dt.date.fromisoformat(asof) if asof else dt.date.today() + if asof_date.weekday() >= 5: + return {"asof": asof_date.isoformat(), "status": "skipped_weekend"} + with _conn() as c: + summary = _snap.refresh_daily(c, asof_date) + return summary + + +# ---------- /runs ---------- + +@router.get("/runs", response_model=list[schemas.RunSummary]) +def list_runs(limit: int = 30): + with _conn() as c: + rows = c.execute( + "SELECT id,asof,mode,status,started_at,finished_at,top_n," + "survivors_count,telegram_sent FROM screener_runs " + "ORDER BY asof DESC, id DESC LIMIT ?", (limit,), + ).fetchall() + return [ + schemas.RunSummary( + id=r[0], asof=r[1], mode=r[2], status=r[3], + started_at=r[4], finished_at=r[5], top_n=r[6], + survivors_count=r[7], telegram_sent=bool(r[8]), + ) + for r in rows + ] + + +@router.get("/runs/{run_id}") +def get_run(run_id: int): + with _conn() as c: + meta = c.execute( + "SELECT id,asof,mode,status,started_at,finished_at,top_n," + "survivors_count,telegram_sent,weights_json,node_params_json,gate_params_json " + "FROM screener_runs WHERE id=?", + (run_id,), + ).fetchone() + if not meta: + raise HTTPException(404, "run not found") + rows = c.execute( + "SELECT rank,ticker,name,total_score,scores_json,close,market_cap," + "entry_price,stop_price,target_price,atr14 " + "FROM screener_results WHERE run_id=? ORDER BY rank", + (run_id,), + ).fetchall() + + return { + "meta": { + "id": meta[0], "asof": meta[1], "mode": meta[2], "status": meta[3], + "started_at": meta[4], "finished_at": meta[5], "top_n": meta[6], + "survivors_count": meta[7], "telegram_sent": bool(meta[8]), + "weights": json.loads(meta[9]), + "node_params": json.loads(meta[10]), + "gate_params": json.loads(meta[11]), + }, + "results": [ + { + "rank": r[0], "ticker": r[1], "name": r[2], + "total_score": r[3], "scores": json.loads(r[4]), + "close": r[5], "market_cap": r[6], + "entry_price": r[7], "stop_price": r[8], "target_price": r[9], + "atr14": r[10], + } + for r in rows + ], + } diff --git a/stock-lab/app/test_screener_router.py b/stock-lab/app/test_screener_router.py index 09db5a9..630e5e3 100644 --- a/stock-lab/app/test_screener_router.py +++ b/stock-lab/app/test_screener_router.py @@ -122,3 +122,25 @@ def test_run_manual_save_writes_row(client): c = sqlite3.connect(db_path) cnt = c.execute("SELECT count(*) FROM screener_runs").fetchone()[0] assert cnt == 1 + + +def test_runs_list_and_detail(client): + db_path = os.environ["STOCK_DB_PATH"] + c = sqlite3.connect(db_path) + _seed_min(c) + c.close() + + saved = client.post( + "/api/stock/screener/run", + json={"mode": "manual_save", "asof": "2026-05-12"}, + ).json() + run_id = saved["run_id"] + + list_r = client.get("/api/stock/screener/runs?limit=5") + assert list_r.status_code == 200 + assert any(r["id"] == run_id for r in list_r.json()) + + detail = client.get(f"/api/stock/screener/runs/{run_id}") + assert detail.status_code == 200 + assert detail.json()["meta"]["id"] == run_id + assert isinstance(detail.json()["results"], list)