feat(lotto): calibrate_winner + backfill (멱등·청크)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 17:06:00 +09:00
parent aaba4fbc46
commit c196da4902
2 changed files with 68 additions and 0 deletions

View File

@@ -105,6 +105,49 @@ def calibrate_winner_compute(draws, target_draw_no, winning6,
return {"scores": scores, "percentile": percentile, "cache_draws": len(pit)}
MIN_HISTORY = 30 # point-in-time 캐시 최소 회차 (이 미만은 캘리브레이션 skip)
def _db():
from . import db as _db_mod
return _db_mod
def calibrate_winner(draw_no: int, sample_m: int = 2000) -> Dict[str, Any]:
"""DB 진입점: 회차 1개 캘리브레이션 후 저장 (멱등)."""
db = _db()
draws = db.get_all_draw_numbers()
row = db.get_draw(draw_no)
if row is None:
return {"ok": False, "reason": "no_draw"}
pit = point_in_time_draws(draws, draw_no)
if len(pit) < MIN_HISTORY:
return {"ok": False, "reason": "insufficient_history"}
winning6 = [row["n1"], row["n2"], row["n3"], row["n4"], row["n5"], row["n6"]]
res = calibrate_winner_compute(draws, draw_no, winning6, sample_m=sample_m)
db.save_winner_calibration(
draw_no=draw_no, winning=winning6, scores=res["scores"],
percentile=res["percentile"], my_pick_avg=None,
cache_draws=res["cache_draws"],
)
return {"ok": True, "draw_no": draw_no, **res}
def backfill_calibration(batch: int = 50, sample_m: int = 2000) -> Dict[str, Any]:
"""미처리 회차만 batch개 캘리브레이션 (멱등·재개 가능)."""
db = _db()
draws = db.get_all_draw_numbers()
done = db.get_calibrated_draw_nos()
todo = [d for d, _ in draws if d not in done and d > MIN_HISTORY]
todo.sort()
n = 0
for draw_no in todo[:batch]:
r = calibrate_winner(draw_no, sample_m=sample_m)
if r.get("ok"):
n += 1
return {"calibrated": n, "remaining": max(0, len(todo) - batch)}
def coverage_tickets(k: int, seed: Optional[int] = None) -> List[List[int]]:
"""greedy 커버리지 — 아직 덜 쓰인 번호를 우선 배치해 번호를 넓게 분산.
(휠링/보장설계는 향후. 현재는 distinct + 번호 사용 균등화)"""

View File

@@ -65,6 +65,31 @@ def test_winner_calibration_upsert(monkeypatch):
assert row["score_total"] == 2.00
def _seed_draws(db, n=40):
rows = []
import random as _r; _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_backfill_calibration_idempotent(monkeypatch):
db = _fresh_db(monkeypatch)
_seed_draws(db, 40)
from app import backtest as bt
r1 = bt.backfill_calibration(batch=15, sample_m=200)
# 첫 회차는 point-in-time 데이터가 빈약 → min_history 이후만 처리
done1 = len(db.get_calibrated_draw_nos())
assert done1 > 0
r2 = bt.backfill_calibration(batch=100, sample_m=200) # 나머지
done2 = len(db.get_calibrated_draw_nos())
assert done2 >= done1
r3 = bt.backfill_calibration(batch=100, sample_m=200) # 재실행 → 추가 0
assert r3["calibrated"] == 0
def test_get_calibrated_draw_nos(monkeypatch):
"""저장된 draw_no 집합이 get_calibrated_draw_nos에 포함되어야 한다."""
db = _fresh_db(monkeypatch)