fix(lotto): Phase 2 리뷰 반영 (engine_w 회차주 기준·누출제거·N+1제거·테스트 보강)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -113,10 +113,12 @@ def _db():
|
|||||||
return _db_mod
|
return _db_mod
|
||||||
|
|
||||||
|
|
||||||
def calibrate_winner(draw_no: int, sample_m: int = 2000) -> Dict[str, Any]:
|
def calibrate_winner(draw_no: int, sample_m: int = 2000, draws=None) -> Dict[str, Any]:
|
||||||
"""DB 진입점: 회차 1개 캘리브레이션 후 저장 (멱등)."""
|
"""DB 진입점: 회차 1개 캘리브레이션 후 저장 (멱등).
|
||||||
|
draws를 외부에서 전달하면 N+1 조회를 방지한다."""
|
||||||
db = _db()
|
db = _db()
|
||||||
draws = db.get_all_draw_numbers()
|
if draws is None:
|
||||||
|
draws = db.get_all_draw_numbers()
|
||||||
row = db.get_draw(draw_no)
|
row = db.get_draw(draw_no)
|
||||||
if row is None:
|
if row is None:
|
||||||
return {"ok": False, "reason": "no_draw"}
|
return {"ok": False, "reason": "no_draw"}
|
||||||
@@ -142,7 +144,7 @@ def backfill_calibration(batch: int = 50, sample_m: int = 2000) -> Dict[str, Any
|
|||||||
todo.sort()
|
todo.sort()
|
||||||
n = 0
|
n = 0
|
||||||
for draw_no in todo[:batch]:
|
for draw_no in todo[:batch]:
|
||||||
r = calibrate_winner(draw_no, sample_m=sample_m)
|
r = calibrate_winner(draw_no, sample_m=sample_m, draws=draws)
|
||||||
if r.get("ok"):
|
if r.get("ok"):
|
||||||
n += 1
|
n += 1
|
||||||
return {"calibrated": n, "remaining": max(0, len(todo) - batch)}
|
return {"calibrated": n, "remaining": max(0, len(todo) - batch)}
|
||||||
@@ -177,16 +179,18 @@ def run_forward_purchase(draw_no: int, k: int = 5000, pool_n: int = 20000,
|
|||||||
hist=graded, best_match=graded["best_match"], avg_meta_score=avg_meta,
|
hist=graded, best_match=graded["best_match"], avg_meta_score=avg_meta,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 1) engine_w — 그 주 trials(있으면) 아니면 current_base
|
# 1) engine_w — 그 주 trials(있으면) 아니면 uniform fallback (leak-free)
|
||||||
|
from datetime import date as _date
|
||||||
from . import weight_evolver as we
|
from . import weight_evolver as we
|
||||||
week_start = we.get_week_start()
|
draw_date = _date.fromisoformat(row["drw_date"])
|
||||||
trials = db.get_weekly_trials(week_start) if hasattr(db, "get_weekly_trials") else []
|
week_start = we.get_week_start(draw_date)
|
||||||
|
trials = db.get_weekly_trials(week_start)
|
||||||
if trials:
|
if trials:
|
||||||
for t in trials:
|
for t in trials:
|
||||||
bought = purchase_tickets(pool, cache, t["weight"], k)
|
bought = purchase_tickets(pool, cache, t["weight"], k)
|
||||||
_store("engine_w", f"w{t['day_of_week']}", t["weight"], t["id"], bought)
|
_store("engine_w", f"w{t['day_of_week']}", t["weight"], t["id"], bought)
|
||||||
else:
|
else:
|
||||||
base = db.get_current_base() or [0.2] * 5
|
base = [0.2] * 5
|
||||||
bought = purchase_tickets(pool, cache, base, k)
|
bought = purchase_tickets(pool, cache, base, k)
|
||||||
_store("engine_w", "base", base, None, bought)
|
_store("engine_w", "base", base, None, bought)
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,60 @@ def test_run_forward_purchase_persists_all_strategies(monkeypatch):
|
|||||||
assert r["n_tickets"] == 20
|
assert r["n_tickets"] == 20
|
||||||
|
|
||||||
|
|
||||||
|
def test_calibrate_winner_no_draw(monkeypatch):
|
||||||
|
"""DB에 없는 회차 번호 → ok=False, reason='no_draw'."""
|
||||||
|
db = _fresh_db(monkeypatch)
|
||||||
|
_seed_draws(db, 40)
|
||||||
|
from app import backtest as bt
|
||||||
|
r = bt.calibrate_winner(99999)
|
||||||
|
assert r["ok"] is False
|
||||||
|
assert r["reason"] == "no_draw"
|
||||||
|
|
||||||
|
|
||||||
|
def test_calibrate_winner_insufficient_history(monkeypatch):
|
||||||
|
"""point-in-time 이력이 MIN_HISTORY(30) 미만인 회차 → reason='insufficient_history'.
|
||||||
|
draw_no=20이면 PIT 이력이 19개(draws 1~19)로 30 미만."""
|
||||||
|
db = _fresh_db(monkeypatch)
|
||||||
|
_seed_draws(db, 40)
|
||||||
|
from app import backtest as bt
|
||||||
|
r = bt.calibrate_winner(20)
|
||||||
|
assert r["ok"] is False
|
||||||
|
assert r["reason"] == "insufficient_history"
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_forward_purchase_with_trials(monkeypatch):
|
||||||
|
"""그 주 weight_trials가 존재하면 engine_w 행의 weight_label이 'w0'..'w5' 형식이어야 한다."""
|
||||||
|
db = _fresh_db(monkeypatch)
|
||||||
|
_seed_draws(db, 40)
|
||||||
|
# draw 40: drw_date='2020-01-13' → week_start='2020-01-13'
|
||||||
|
from datetime import date, timedelta
|
||||||
|
draw_date = date.fromisoformat("2020-01-13")
|
||||||
|
ws = (draw_date - timedelta(days=draw_date.weekday())).isoformat()
|
||||||
|
# 해당 주에 trial 2개 심기 (day_of_week 0, 1)
|
||||||
|
db.save_weight_trial(ws, 0, [0.1, 0.3, 0.2, 0.2, 0.2], "perturb")
|
||||||
|
db.save_weight_trial(ws, 1, [0.25, 0.25, 0.25, 0.15, 0.1], "perturb")
|
||||||
|
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)
|
||||||
|
engine_w_labels = {r["weight_label"] for r in rows if r["strategy"] == "engine_w"}
|
||||||
|
# trials가 있으므로 'base'가 아닌 'w0', 'w1' 형식이어야 한다
|
||||||
|
assert "base" not in engine_w_labels
|
||||||
|
assert any(lbl.startswith("w") for lbl in engine_w_labels)
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_forward_purchase_idempotent(monkeypatch):
|
||||||
|
"""run_forward_purchase 두 번 호출 시 upsert — 행 수 변화 없음."""
|
||||||
|
db = _fresh_db(monkeypatch)
|
||||||
|
_seed_draws(db, 40)
|
||||||
|
from app import backtest as bt
|
||||||
|
bt.run_forward_purchase(draw_no=40, k=20, pool_n=500, sample_seed=5)
|
||||||
|
count_after_first = len(db.get_backtest_runs(draw_no=40))
|
||||||
|
bt.run_forward_purchase(draw_no=40, k=20, pool_n=500, sample_seed=5)
|
||||||
|
count_after_second = len(db.get_backtest_runs(draw_no=40))
|
||||||
|
assert count_after_second == count_after_first
|
||||||
|
|
||||||
|
|
||||||
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