From a425bb8809d42a0b1525f516a69f12bf9332a689 Mon Sep 17 00:00:00 2001 From: gahusb Date: Sun, 31 May 2026 17:19:05 +0900 Subject: [PATCH] =?UTF-8?q?feat(lotto):=20track=5Frecord=20+=20build=5Frev?= =?UTF-8?q?iew=5Fpayload=20=EC=A7=91=EA=B3=84?= 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 | 39 +++++++++++++++++++++++++++++++++ lotto/tests/test_backtest_db.py | 18 +++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/lotto/app/backtest.py b/lotto/app/backtest.py index ed44d77..c1b0c9e 100644 --- a/lotto/app/backtest.py +++ b/lotto/app/backtest.py @@ -202,6 +202,45 @@ def run_forward_purchase(draw_no: int, k: int = 5000, pool_n: int = 20000, return {"ok": True, "draw_no": draw_no} +def track_record() -> Dict[str, Any]: + """전략별 누적 등수 집계 (engine_w는 라벨 합산).""" + db = _db() + rows = db.get_backtest_runs() + agg: Dict[str, Dict[str, int]] = {} + for r in rows: + a = agg.setdefault(r["strategy"], { + "n_tickets": 0, "1st": 0, "2nd": 0, "3rd": 0, "4th": 0, "5th": 0, "draws": 0}) + p = prize_counts(r) + a["n_tickets"] += r["n_tickets"] + for tier in ("1st", "2nd", "3rd", "4th", "5th"): + a[tier] += p[tier] + a["draws"] += 1 + return {"by_strategy": agg} + + +def build_review_payload(draw_no: int) -> Dict[str, Any]: + """일요 회고 브리핑용 조립.""" + db = _db() + cal = db.get_winner_calibration(draw_no) + runs = db.get_backtest_runs(draw_no=draw_no) + hist = db.get_calibration_history(limit=12) + forward = [] + for r in runs: + forward.append({"strategy": r["strategy"], "label": r["weight_label"], + "prizes": prize_counts(r), "best_match": r["best_match"], + "avg_meta_score": r["avg_meta_score"]}) + return { + "draw_no": draw_no, + "winner_analysis": cal, # score_* + percentile + "forward": forward, + "track_record": track_record()["by_strategy"], + "calibration_trend": [ + {"draw_no": h["draw_no"], "score_total": h["score_total"], + "percentile": h["percentile"]} for h in hist + ], + } + + def coverage_tickets(k: int, seed: Optional[int] = None) -> List[List[int]]: """greedy 커버리지 — 아직 덜 쓰인 번호를 우선 배치해 번호를 넓게 분산. (휠링/보장설계는 향후. 현재는 distinct + 번호 사용 균등화)""" diff --git a/lotto/tests/test_backtest_db.py b/lotto/tests/test_backtest_db.py index 6159124..4bf7bd8 100644 --- a/lotto/tests/test_backtest_db.py +++ b/lotto/tests/test_backtest_db.py @@ -171,3 +171,21 @@ def test_get_calibrated_draw_nos(monkeypatch): nos = db.get_calibrated_draw_nos() assert isinstance(nos, set) assert {301, 302, 303}.issubset(nos) + + +def test_track_record_and_review_payload(monkeypatch): + db = _fresh_db(monkeypatch) + _seed_draws(db, 40) + from app import backtest as bt + bt.run_forward_purchase(draw_no=40, k=20, pool_n=500, sample_seed=5) + bt.calibrate_winner(40, sample_m=200) + + tr = bt.track_record() + assert "random_null" in tr["by_strategy"] + assert tr["by_strategy"]["random_null"]["n_tickets"] >= 20 + + payload = bt.build_review_payload(40) + assert payload["draw_no"] == 40 + assert "winner_analysis" in payload # 당첨조합 5분석치 + assert "forward" in payload # 이번 회차 전략별 성적 + assert "calibration_trend" in payload