feat(lotto): calibrate_winner + backfill (멱등·청크)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 + 번호 사용 균등화)"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user