From add433233a43645a431a6a654bea05ee9c76a44a Mon Sep 17 00:00:00 2001 From: gahusb Date: Sun, 31 May 2026 17:30:10 +0900 Subject: [PATCH] =?UTF-8?q?fix(lotto):=20Phase=203=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20(run-forward=20=EB=B0=B1=EA=B7=B8=EB=9D=BC?= =?UTF-8?q?=EC=9A=B4=EB=93=9C=C2=B7review=20404=C2=B7track=5Frecord=20dist?= =?UTF-8?q?inct=C2=B7=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B3=B4=EA=B0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- lotto/app/backtest.py | 5 ++- lotto/app/main.py | 6 ++-- lotto/app/routers/backtest.py | 13 ++++++-- lotto/tests/test_backtest_api.py | 54 +++++++++++++++++++++++++++++++- lotto/tests/test_backtest_db.py | 2 ++ 5 files changed, 73 insertions(+), 7 deletions(-) diff --git a/lotto/app/backtest.py b/lotto/app/backtest.py index c1b0c9e..c86a809 100644 --- a/lotto/app/backtest.py +++ b/lotto/app/backtest.py @@ -207,6 +207,7 @@ def track_record() -> Dict[str, Any]: db = _db() rows = db.get_backtest_runs() agg: Dict[str, Dict[str, int]] = {} + draw_sets: Dict[str, set] = {} for r in rows: a = agg.setdefault(r["strategy"], { "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"] for tier in ("1st", "2nd", "3rd", "4th", "5th"): 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} diff --git a/lotto/app/main.py b/lotto/app/main.py index ccaa177..9e6f86b 100644 --- a/lotto/app/main.py +++ b/lotto/app/main.py @@ -49,6 +49,7 @@ from .routers import briefing as briefing_router from .routers import review as review_router from .routers import backtest as backtest_router from .jobs.grade_weekly_review import run_for_latest as grade_run_for_latest +from . import backtest app = FastAPI() install_access_log(app) @@ -86,9 +87,8 @@ def on_startup(): _refresh_perf_cache() # 새 채점 결과 반영 → 즉시 갱신 # 자가학습 백테스트 — 새 회차 forward 구매 + 당첨조합 캘리브레이션 try: - from . import backtest as _backtest - _backtest.run_forward_purchase(draw_no=res["drawNo"]) - _backtest.calibrate_winner(res["drawNo"]) + backtest.run_forward_purchase(draw_no=res["drawNo"]) + backtest.calibrate_winner(res["drawNo"]) except Exception as e: logger.warning(f"backtest 갱신 실패: {e}") diff --git a/lotto/app/routers/backtest.py b/lotto/app/routers/backtest.py index 6206d2f..b445419 100644 --- a/lotto/app/routers/backtest.py +++ b/lotto/app/routers/backtest.py @@ -16,12 +16,21 @@ def calibration(weeks: int = Query(52, ge=1, le=520)): @router.get("/review/{draw_no}") 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) @router.post("/run-forward") -def run_forward(draw_no: int = Query(...), k: int = 5000, pool_n: int = 20000): - return backtest.run_forward_purchase(draw_no=draw_no, k=k, pool_n=pool_n) +def run_forward( + 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") diff --git a/lotto/tests/test_backtest_api.py b/lotto/tests/test_backtest_api.py index 40daf6c..459d54e 100644 --- a/lotto/tests/test_backtest_api.py +++ b/lotto/tests/test_backtest_api.py @@ -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 sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..")) @@ -13,6 +13,18 @@ def _client(monkeypatch): from app.main import app 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): client, db = _client(monkeypatch) 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") assert r2.status_code == 200 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 diff --git a/lotto/tests/test_backtest_db.py b/lotto/tests/test_backtest_db.py index 4bf7bd8..1a7dad2 100644 --- a/lotto/tests/test_backtest_db.py +++ b/lotto/tests/test_backtest_db.py @@ -189,3 +189,5 @@ def test_track_record_and_review_payload(monkeypatch): assert "winner_analysis" in payload # 당첨조합 5분석치 assert "forward" in payload # 이번 회차 전략별 성적 assert "calibration_trend" in payload + assert payload["winner_analysis"] is not None + assert "score_total" in payload["winner_analysis"]