fix(lotto): 학습 게이트 정직화 (engine-best vs random-best 6trial·명시적 gated·정체성 일관)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -182,6 +182,7 @@ def test_track_record_and_review_payload(monkeypatch):
|
||||
|
||||
tr = bt.track_record()
|
||||
assert "random_null" in tr["by_strategy"]
|
||||
# 이제 random_null은 N_NULL_TRIALS=6 행이므로 6*20=120장
|
||||
assert tr["by_strategy"]["random_null"]["n_tickets"] >= 20
|
||||
|
||||
payload = bt.build_review_payload(40)
|
||||
@@ -191,3 +192,129 @@ def test_track_record_and_review_payload(monkeypatch):
|
||||
assert "calibration_trend" in payload
|
||||
assert payload["winner_analysis"] is not None
|
||||
assert "score_total" in payload["winner_analysis"]
|
||||
|
||||
|
||||
def test_run_forward_purchase_random_null_count(monkeypatch):
|
||||
"""run_forward_purchase는 random_null을 N_NULL_TRIALS=6개 저장해야 한다."""
|
||||
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=7)
|
||||
assert res["ok"] is True
|
||||
rows = db.get_backtest_runs(draw_no=40)
|
||||
null_rows = [r for r in rows if r["strategy"] == "random_null"]
|
||||
assert len(null_rows) == bt.N_NULL_TRIALS # 6개
|
||||
null_labels = {r["weight_label"] for r in null_rows}
|
||||
assert null_labels == {f"r{i}" for i in range(bt.N_NULL_TRIALS)}
|
||||
for r in null_rows:
|
||||
assert r["n_tickets"] == 20
|
||||
|
||||
|
||||
def test_evaluate_weekly_gated_keeps_base_unchanged(monkeypatch):
|
||||
"""Fix 5 통합 테스트 (end-to-end gated path).
|
||||
|
||||
접근: DB에 draws, weight_trials, auto_picks, backtest_runs, base_history를 직접 심어
|
||||
evaluate_weekly()의 gated 분기가 base를 바꾸지 않음을 검증한다.
|
||||
|
||||
gated 조건: engine_w 최고 prize_score − random_best < LIFT_EPSILON(10.0).
|
||||
engine_best=5, random_best=20 → lift=-15 → gated.
|
||||
|
||||
evaluate_weekly 내부 흐름:
|
||||
- get_weekly_trials(week_start) : _today_kst() 기준 week_start 사용
|
||||
- get_latest_draw() : draws 테이블에서 max(drw_no) 반환
|
||||
두 참조가 같은 날짜 기준이어야 하므로 _today_kst를 monkeypatch로 고정하고
|
||||
draws의 최신 회차 날짜(drw_date)를 해당 주의 날짜로 맞춘다.
|
||||
"""
|
||||
import json as _json
|
||||
from datetime import date, timedelta, datetime as _dt, timezone as _tz, timedelta as _td
|
||||
|
||||
db = _fresh_db(monkeypatch)
|
||||
|
||||
# KST 오늘 날짜 — evaluate_weekly가 이 날짜를 기준으로 week_start 계산
|
||||
KST = _tz(_td(hours=9))
|
||||
today_kst = _dt.now(KST).date()
|
||||
from app import weight_evolver as we
|
||||
week_start = we.get_week_start(today_kst)
|
||||
|
||||
# 1) draws 심기 — 최신 회차의 drw_date를 week_start 주 안의 날짜로 맞춤
|
||||
import random as _r; _r.seed(99)
|
||||
rows = []
|
||||
for i in range(1, 41):
|
||||
s = sorted(_r.sample(range(1, 46), 6))
|
||||
# 마지막 회차(40)는 오늘 날짜 사용 (week_start 주 내)
|
||||
if i == 40:
|
||||
drw_date = today_kst.isoformat()
|
||||
else:
|
||||
drw_date = f"2020-01-{(i % 28) + 1:02d}"
|
||||
rows.append({
|
||||
"drw_no": i, "drw_date": drw_date,
|
||||
"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)
|
||||
latest = db.get_latest_draw()
|
||||
assert latest is not None
|
||||
assert latest["drw_date"] == today_kst.isoformat()
|
||||
|
||||
# 2) weight trial 1개 심기 (day_of_week=0, week_start=오늘 주)
|
||||
trial_w = [0.2, 0.2, 0.2, 0.2, 0.2]
|
||||
db.save_weight_trial(week_start, 0, trial_w, "perturb")
|
||||
trial_rows = db.get_weekly_trials(week_start)
|
||||
assert len(trial_rows) == 1
|
||||
trial_id = trial_rows[0]["id"]
|
||||
|
||||
# 3) auto_picks 1개 심기 (winning 번호와 2개 일치 → max_correct=2)
|
||||
winning6 = [latest["n1"], latest["n2"], latest["n3"],
|
||||
latest["n4"], latest["n5"], latest["n6"]]
|
||||
pick = winning6[:2] + [40, 41, 42, 43]
|
||||
db.save_auto_pick(trial_id, 1, pick, meta_score=0.5)
|
||||
|
||||
# 4) backtest_runs: engine_w prize_score=5, random_null 6개 prize_score=20 (gated 확실)
|
||||
LOW_HIST = {"m3": 5, "m4": 0, "m5": 0, "m6": 0, "bonus_hits": 0} # prize=5
|
||||
HIGH_HIST = {"m3": 20, "m4": 0, "m5": 0, "m6": 0, "bonus_hits": 0} # prize=20
|
||||
draw_no = latest["drw_no"]
|
||||
db.save_backtest_run(
|
||||
draw_no=draw_no, strategy="engine_w", weight_label="w0",
|
||||
weight_json=_json.dumps(trial_w), trial_id=trial_id, n_tickets=20,
|
||||
hist=LOW_HIST, best_match=2, avg_meta_score=0.5,
|
||||
)
|
||||
from app import backtest as bt
|
||||
for i in range(bt.N_NULL_TRIALS):
|
||||
db.save_backtest_run(
|
||||
draw_no=draw_no, strategy="random_null", weight_label=f"r{i}",
|
||||
weight_json=None, trial_id=None, n_tickets=20,
|
||||
hist=HIGH_HIST, best_match=3, avg_meta_score=0.5,
|
||||
)
|
||||
|
||||
# 5) current base 저장 (이전 주 월요일 effective_from)
|
||||
base_w = [0.2, 0.2, 0.2, 0.2, 0.2]
|
||||
prev_monday = (today_kst - timedelta(weeks=1, days=today_kst.weekday())).isoformat()
|
||||
db.save_base_history(
|
||||
effective_from=prev_monday,
|
||||
weight=base_w,
|
||||
source_trial_id=None,
|
||||
update_reason="cold_start",
|
||||
winner_score=None,
|
||||
winner_max_correct=None,
|
||||
)
|
||||
assert db.get_current_base() == base_w
|
||||
|
||||
# 6) evaluate_weekly 호출 — _today_kst()를 monkeypatch로 오늘 날짜 고정
|
||||
monkeypatch.setattr(we, "_today_kst", lambda: today_kst)
|
||||
|
||||
result = we.evaluate_weekly()
|
||||
|
||||
assert result.get("ok") is True, f"evaluate_weekly 실패: {result}"
|
||||
|
||||
# gated path 검증
|
||||
update_reason = result.get("update_reason", "")
|
||||
assert update_reason in ("unchanged_gated", "idempotent_skip"), (
|
||||
f"gated여야 하는데 reason='{update_reason}' — 게이팅 로직 깨짐"
|
||||
)
|
||||
|
||||
# base가 바뀌지 않았는지 검증
|
||||
new_base = result.get("new_base")
|
||||
assert new_base == base_w, (
|
||||
f"gated인데 base가 변경됨: {new_base} != {base_w}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user