feat(lotto): run_forward_purchase 3전략 구매·채점·저장
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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)}
|
||||
|
||||
|
||||
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]]:
|
||||
"""greedy 커버리지 — 아직 덜 쓰인 번호를 우선 배치해 번호를 넓게 분산.
|
||||
(휠링/보장설계는 향후. 현재는 distinct + 번호 사용 균등화)"""
|
||||
|
||||
@@ -90,6 +90,22 @@ def test_backfill_calibration_idempotent(monkeypatch):
|
||||
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):
|
||||
"""저장된 draw_no 집합이 get_calibrated_draw_nos에 포함되어야 한다."""
|
||||
db = _fresh_db(monkeypatch)
|
||||
|
||||
Reference in New Issue
Block a user