feat(phase-5): FastAPI 엔드포인트 (검색/차트/예측/메트릭/뉴스)
- GET /api/symbols/search?q=...&seed_only= : trigram + prefix + ILIKE 합산 정렬
- GET /api/symbols/{code} : 메타
- GET /api/chart/{code}?days=N&include_* : OHLCV + 일별 감성 + 외인기관거래대금
- POST /api/predict/{code}?horizons=1,3,5 : on-demand 앙상블 예측 + DB 적재
(user_triggered=TRUE)
- GET /api/predict/{code}/latest : 최신 base_date 의 예측 묶음 + base_close
(UI 가 차트 마지막 점에 이어 붙임)
- GET /api/metrics/{code}?window_days=N : 종목 단위 hit_rate / mae (model, horizon 별)
- GET /api/metrics?window_days=N : 전체 누적
- GET /api/news/{code}?source=&limit= : 최신순 뉴스/공시 목록 (감성 점수 포함)
main.py 에 6개 라우터 모두 include.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
101
backend/app/api/metrics.py
Normal file
101
backend/app/api/metrics.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""모델 성능 메트릭 API."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, timedelta
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.db.connection import get_engine
|
||||
|
||||
router = APIRouter(prefix="/api/metrics", tags=["metrics"])
|
||||
|
||||
|
||||
@router.get("/{code}")
|
||||
def code_metrics(
|
||||
code: str,
|
||||
window_days: int = Query(default=30, ge=1, le=365),
|
||||
) -> dict:
|
||||
"""code 의 최근 window_days 윈도우 prediction_outcomes 집계."""
|
||||
eng = get_engine()
|
||||
end = date.today()
|
||||
start = end - timedelta(days=window_days)
|
||||
with eng.connect() as conn:
|
||||
sym = conn.execute(
|
||||
text("SELECT code, name FROM symbols WHERE code = :c"),
|
||||
{"c": code},
|
||||
).first()
|
||||
if not sym:
|
||||
raise HTTPException(status_code=404, detail=f"unknown code: {code}")
|
||||
rows = conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT model, horizon,
|
||||
COUNT(*) AS n,
|
||||
AVG(CASE WHEN direction_hit THEN 1.0 ELSE 0.0 END) AS hit_rate,
|
||||
AVG(abs_error) AS mae
|
||||
FROM prediction_outcomes
|
||||
WHERE code = :c AND resolved_at >= :s
|
||||
GROUP BY model, horizon
|
||||
ORDER BY model, horizon
|
||||
"""
|
||||
),
|
||||
{"c": code, "s": start},
|
||||
).all()
|
||||
|
||||
return {
|
||||
"code": sym[0],
|
||||
"name": sym[1],
|
||||
"window_days": window_days,
|
||||
"range": {"from": str(start), "to": str(end)},
|
||||
"by_model_horizon": [
|
||||
{
|
||||
"model": r[0],
|
||||
"horizon": int(r[1]),
|
||||
"n": int(r[2]),
|
||||
"hit_rate": float(r[3]) if r[3] is not None else None,
|
||||
"mae": float(r[4]) if r[4] is not None else None,
|
||||
}
|
||||
for r in rows
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@router.get("")
|
||||
def overall_metrics(
|
||||
window_days: int = Query(default=30, ge=1, le=365),
|
||||
) -> dict:
|
||||
"""전체 시드 종목 누적 메트릭."""
|
||||
eng = get_engine()
|
||||
end = date.today()
|
||||
start = end - timedelta(days=window_days)
|
||||
with eng.connect() as conn:
|
||||
rows = conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT po.model, po.horizon,
|
||||
COUNT(*) AS n,
|
||||
AVG(CASE WHEN po.direction_hit THEN 1.0 ELSE 0.0 END) AS hit_rate,
|
||||
AVG(po.abs_error) AS mae
|
||||
FROM prediction_outcomes po
|
||||
WHERE po.resolved_at >= :s
|
||||
GROUP BY po.model, po.horizon
|
||||
ORDER BY po.model, po.horizon
|
||||
"""
|
||||
),
|
||||
{"s": start},
|
||||
).all()
|
||||
return {
|
||||
"window_days": window_days,
|
||||
"range": {"from": str(start), "to": str(end)},
|
||||
"by_model_horizon": [
|
||||
{
|
||||
"model": r[0],
|
||||
"horizon": int(r[1]),
|
||||
"n": int(r[2]),
|
||||
"hit_rate": float(r[3]) if r[3] is not None else None,
|
||||
"mae": float(r[4]) if r[4] is not None else None,
|
||||
}
|
||||
for r in rows
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user