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)}
|
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 + 번호 사용 균등화)"""
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user