feat(lotto): run_forward_purchase 3전략 구매·채점·저장

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

View File

@@ -148,6 +148,56 @@ def backfill_calibration(batch: int = 50, sample_m: int = 2000) -> Dict[str, Any
return {"calibrated": n, "remaining": max(0, len(todo) - batch)} return {"calibrated": n, "remaining": max(0, len(todo) - batch)}
def run_forward_purchase(draw_no: int, k: int = 5000, pool_n: int = 20000,
sample_seed: Optional[int] = None) -> Dict[str, Any]:
"""회차 추첨 '직전' 시점 데이터로 3전략 구매 → 당첨번호로 채점 → 저장(멱등).
engine_w: 그 주 weight_trials 6개(없으면 current_base 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"]]
bonus = row["bonus"]
cache = build_analysis_cache(pit)
nw = build_number_weights(cache)
pool = generate_pool(cache, nw, n=pool_n, seed=sample_seed)
def _store(strategy, label, weight_json, trial_id, tickets):
graded = grade_tickets(tickets, winning6, bonus)
avg_meta = (sum(score_combination(t, cache)["score_total"] for t in tickets)
/ max(len(tickets), 1))
db.save_backtest_run(
draw_no=draw_no, strategy=strategy, weight_label=label,
weight_json=weight_json, trial_id=trial_id, n_tickets=len(tickets),
hist=graded, best_match=graded["best_match"], avg_meta_score=avg_meta,
)
# 1) engine_w — 그 주 trials(있으면) 아니면 current_base
from . import weight_evolver as we
week_start = we.get_week_start()
trials = db.get_weekly_trials(week_start) if hasattr(db, "get_weekly_trials") else []
if trials:
for t in trials:
bought = purchase_tickets(pool, cache, t["weight"], k)
_store("engine_w", f"w{t['day_of_week']}", t["weight"], t["id"], bought)
else:
base = db.get_current_base() or [0.2] * 5
bought = purchase_tickets(pool, cache, base, k)
_store("engine_w", "base", base, None, bought)
# 2) random_null
_store("random_null", "-", None, None, random_null_tickets(k, seed=sample_seed))
# 3) coverage
_store("coverage", "-", None, None, coverage_tickets(k, seed=sample_seed))
return {"ok": True, "draw_no": draw_no}
def coverage_tickets(k: int, seed: Optional[int] = None) -> List[List[int]]: def coverage_tickets(k: int, seed: Optional[int] = None) -> List[List[int]]:
"""greedy 커버리지 — 아직 덜 쓰인 번호를 우선 배치해 번호를 넓게 분산. """greedy 커버리지 — 아직 덜 쓰인 번호를 우선 배치해 번호를 넓게 분산.
(휠링/보장설계는 향후. 현재는 distinct + 번호 사용 균등화)""" (휠링/보장설계는 향후. 현재는 distinct + 번호 사용 균등화)"""

View File

@@ -90,6 +90,22 @@ def test_backfill_calibration_idempotent(monkeypatch):
assert r3["calibrated"] == 0 assert r3["calibrated"] == 0
def test_run_forward_purchase_persists_all_strategies(monkeypatch):
db = _fresh_db(monkeypatch)
_seed_draws(db, 40)
from app import backtest as bt
# 작은 규모로 빠르게
res = bt.run_forward_purchase(draw_no=40, k=20, pool_n=500, sample_seed=5)
assert res["ok"] is True
rows = db.get_backtest_runs(draw_no=40)
strategies = {r["strategy"] for r in rows}
assert "random_null" in strategies
assert "coverage" in strategies
assert "engine_w" in strategies # base 가중치로 최소 1건
for r in rows:
assert r["n_tickets"] == 20
def test_get_calibrated_draw_nos(monkeypatch): def test_get_calibrated_draw_nos(monkeypatch):
"""저장된 draw_no 집합이 get_calibrated_draw_nos에 포함되어야 한다.""" """저장된 draw_no 집합이 get_calibrated_draw_nos에 포함되어야 한다."""
db = _fresh_db(monkeypatch) db = _fresh_db(monkeypatch)