fix(lotto): Phase 3 리뷰 반영 (run-forward 백그라운드·review 404·track_record distinct·테스트 보강)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -207,6 +207,7 @@ def track_record() -> Dict[str, Any]:
|
|||||||
db = _db()
|
db = _db()
|
||||||
rows = db.get_backtest_runs()
|
rows = db.get_backtest_runs()
|
||||||
agg: Dict[str, Dict[str, int]] = {}
|
agg: Dict[str, Dict[str, int]] = {}
|
||||||
|
draw_sets: Dict[str, set] = {}
|
||||||
for r in rows:
|
for r in rows:
|
||||||
a = agg.setdefault(r["strategy"], {
|
a = agg.setdefault(r["strategy"], {
|
||||||
"n_tickets": 0, "1st": 0, "2nd": 0, "3rd": 0, "4th": 0, "5th": 0, "draws": 0})
|
"n_tickets": 0, "1st": 0, "2nd": 0, "3rd": 0, "4th": 0, "5th": 0, "draws": 0})
|
||||||
@@ -214,7 +215,9 @@ def track_record() -> Dict[str, Any]:
|
|||||||
a["n_tickets"] += r["n_tickets"]
|
a["n_tickets"] += r["n_tickets"]
|
||||||
for tier in ("1st", "2nd", "3rd", "4th", "5th"):
|
for tier in ("1st", "2nd", "3rd", "4th", "5th"):
|
||||||
a[tier] += p[tier]
|
a[tier] += p[tier]
|
||||||
a["draws"] += 1
|
draw_sets.setdefault(r["strategy"], set()).add(r["draw_no"])
|
||||||
|
for strat, s in draw_sets.items():
|
||||||
|
agg[strat]["draws"] = len(s)
|
||||||
return {"by_strategy": agg}
|
return {"by_strategy": agg}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ from .routers import briefing as briefing_router
|
|||||||
from .routers import review as review_router
|
from .routers import review as review_router
|
||||||
from .routers import backtest as backtest_router
|
from .routers import backtest as backtest_router
|
||||||
from .jobs.grade_weekly_review import run_for_latest as grade_run_for_latest
|
from .jobs.grade_weekly_review import run_for_latest as grade_run_for_latest
|
||||||
|
from . import backtest
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
install_access_log(app)
|
install_access_log(app)
|
||||||
@@ -86,9 +87,8 @@ def on_startup():
|
|||||||
_refresh_perf_cache() # 새 채점 결과 반영 → 즉시 갱신
|
_refresh_perf_cache() # 새 채점 결과 반영 → 즉시 갱신
|
||||||
# 자가학습 백테스트 — 새 회차 forward 구매 + 당첨조합 캘리브레이션
|
# 자가학습 백테스트 — 새 회차 forward 구매 + 당첨조합 캘리브레이션
|
||||||
try:
|
try:
|
||||||
from . import backtest as _backtest
|
backtest.run_forward_purchase(draw_no=res["drawNo"])
|
||||||
_backtest.run_forward_purchase(draw_no=res["drawNo"])
|
backtest.calibrate_winner(res["drawNo"])
|
||||||
_backtest.calibrate_winner(res["drawNo"])
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"backtest 갱신 실패: {e}")
|
logger.warning(f"backtest 갱신 실패: {e}")
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,21 @@ def calibration(weeks: int = Query(52, ge=1, le=520)):
|
|||||||
|
|
||||||
@router.get("/review/{draw_no}")
|
@router.get("/review/{draw_no}")
|
||||||
def review(draw_no: int):
|
def review(draw_no: int):
|
||||||
|
if db.get_draw(draw_no) is None:
|
||||||
|
from fastapi import HTTPException
|
||||||
|
raise HTTPException(404, f"no draw {draw_no}")
|
||||||
return backtest.build_review_payload(draw_no)
|
return backtest.build_review_payload(draw_no)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/run-forward")
|
@router.post("/run-forward")
|
||||||
def run_forward(draw_no: int = Query(...), k: int = 5000, pool_n: int = 20000):
|
def run_forward(
|
||||||
return backtest.run_forward_purchase(draw_no=draw_no, k=k, pool_n=pool_n)
|
background_tasks: BackgroundTasks,
|
||||||
|
draw_no: int = Query(...),
|
||||||
|
k: int = Query(5000, ge=1, le=5000),
|
||||||
|
pool_n: int = Query(20000, ge=1000, le=20000),
|
||||||
|
):
|
||||||
|
background_tasks.add_task(backtest.run_forward_purchase, draw_no, k, pool_n)
|
||||||
|
return {"ok": True, "queued": True, "draw_no": draw_no}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/backfill")
|
@router.post("/backfill")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import os, sys, tempfile
|
import os, sys, tempfile, random as _r
|
||||||
|
|
||||||
# _shared lives in web-backend/_shared; add the parent dir so it can be found
|
# _shared lives in web-backend/_shared; add the parent dir so it can be found
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||||
@@ -13,6 +13,18 @@ def _client(monkeypatch):
|
|||||||
from app.main import app
|
from app.main import app
|
||||||
return TestClient(app), db
|
return TestClient(app), db
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_draws(db, n=40):
|
||||||
|
rows = []
|
||||||
|
_r.seed(2)
|
||||||
|
for i in range(1, n + 1):
|
||||||
|
s = sorted(_r.sample(range(1, 46), 6))
|
||||||
|
rows.append({"drw_no": i, "drw_date": f"2020-01-{(i % 28) + 1:02d}",
|
||||||
|
"n1": s[0], "n2": s[1], "n3": s[2], "n4": s[3],
|
||||||
|
"n5": s[4], "n6": s[5], "bonus": ((s[5] % 45) + 1)})
|
||||||
|
db.upsert_many_draws(rows)
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_endpoints(monkeypatch):
|
def test_backtest_endpoints(monkeypatch):
|
||||||
client, db = _client(monkeypatch)
|
client, db = _client(monkeypatch)
|
||||||
r = client.get("/api/lotto/backtest/track-record")
|
r = client.get("/api/lotto/backtest/track-record")
|
||||||
@@ -21,3 +33,43 @@ def test_backtest_endpoints(monkeypatch):
|
|||||||
r2 = client.get("/api/lotto/backtest/calibration?weeks=4")
|
r2 = client.get("/api/lotto/backtest/calibration?weeks=4")
|
||||||
assert r2.status_code == 200
|
assert r2.status_code == 200
|
||||||
assert isinstance(r2.json().get("history"), list)
|
assert isinstance(r2.json().get("history"), list)
|
||||||
|
|
||||||
|
|
||||||
|
def test_track_record_with_data(monkeypatch):
|
||||||
|
"""seed 40 draws + forward run → track-record contains random_null."""
|
||||||
|
client, db_mod = _client(monkeypatch)
|
||||||
|
_seed_draws(db_mod, 40)
|
||||||
|
from app import backtest as bt
|
||||||
|
bt.run_forward_purchase(40, k=20, pool_n=500, sample_seed=5)
|
||||||
|
r = client.get("/api/lotto/backtest/track-record")
|
||||||
|
assert r.status_code == 200
|
||||||
|
body = r.json()
|
||||||
|
assert "by_strategy" in body
|
||||||
|
assert "random_null" in body["by_strategy"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_review_known_and_unknown(monkeypatch):
|
||||||
|
"""Known draw with calibration → 200 + non-null winner_analysis; unknown → 404."""
|
||||||
|
client, db_mod = _client(monkeypatch)
|
||||||
|
_seed_draws(db_mod, 40)
|
||||||
|
from app import backtest as bt
|
||||||
|
bt.run_forward_purchase(40, k=20, pool_n=500, sample_seed=5)
|
||||||
|
bt.calibrate_winner(40, sample_m=200)
|
||||||
|
|
||||||
|
r = client.get("/api/lotto/backtest/review/40")
|
||||||
|
assert r.status_code == 200
|
||||||
|
body = r.json()
|
||||||
|
assert body["winner_analysis"] is not None
|
||||||
|
assert "score_total" in body["winner_analysis"]
|
||||||
|
|
||||||
|
r2 = client.get("/api/lotto/backtest/review/99999")
|
||||||
|
assert r2.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_calibration_weeks_bounds(monkeypatch):
|
||||||
|
"""weeks=0 and weeks=521 should be rejected with 422."""
|
||||||
|
client, _ = _client(monkeypatch)
|
||||||
|
r0 = client.get("/api/lotto/backtest/calibration?weeks=0")
|
||||||
|
assert r0.status_code == 422
|
||||||
|
r521 = client.get("/api/lotto/backtest/calibration?weeks=521")
|
||||||
|
assert r521.status_code == 422
|
||||||
|
|||||||
@@ -189,3 +189,5 @@ def test_track_record_and_review_payload(monkeypatch):
|
|||||||
assert "winner_analysis" in payload # 당첨조합 5분석치
|
assert "winner_analysis" in payload # 당첨조합 5분석치
|
||||||
assert "forward" in payload # 이번 회차 전략별 성적
|
assert "forward" in payload # 이번 회차 전략별 성적
|
||||||
assert "calibration_trend" in payload
|
assert "calibration_trend" in payload
|
||||||
|
assert payload["winner_analysis"] is not None
|
||||||
|
assert "score_total" in payload["winner_analysis"]
|
||||||
|
|||||||
Reference in New Issue
Block a user