feat(stock-lab): /snapshot/refresh + /runs 리스트·상세 라우터
This commit is contained in:
@@ -6,6 +6,7 @@ import datetime as dt
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
@@ -198,3 +199,78 @@ def post_run(body: schemas.RunRequest):
|
|||||||
telegram_payload=schemas.TelegramPayload(**payload),
|
telegram_payload=schemas.TelegramPayload(**payload),
|
||||||
warnings=result.warnings,
|
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)
|
c = sqlite3.connect(db_path)
|
||||||
cnt = c.execute("SELECT count(*) FROM screener_runs").fetchone()[0]
|
cnt = c.execute("SELECT count(*) FROM screener_runs").fetchone()[0]
|
||||||
assert cnt == 1
|
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