feat(stock-lab): /snapshot/refresh + /runs 리스트·상세 라우터
This commit is contained in:
@@ -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
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user