fix(lotto-evolver): previous_base diff + 일요일 cron skip + idempotent evaluate
- weight_evolver.evaluate_weekly: save_base_history 직전에 current_base를 previous_base로 캡처해 return dict에 포함 → formatter가 진짜 diff 표시 가능 - evaluate_weekly: same effective_from row 이미 존재 시 save skip + idempotent return (토 22:00 lotto cron과 agent-office 22:15 재호출 중복 row 방지) - main._run_weight_evolver_daily: 일요일(weekday=6) 도 skip — 토요일 trial을 INSERT OR REPLACE로 덮어쓰는 문제 방지 - telegram_lotto._format_evolution_report: eval_result.previous_base 우선 사용 (없으면 current_base 폴백) → diff 자기 자신 비교 버그 수정 - test_lotto_evolution_format: previous_base 키 추가 + 새 diff 검증 테스트 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -199,7 +199,8 @@ def _format_evolution_report(eval_result: Dict[str, Any], current_base: List[flo
|
|||||||
"",
|
"",
|
||||||
f"📊 다음주 base 변경 ({reason}):",
|
f"📊 다음주 base 변경 ({reason}):",
|
||||||
]
|
]
|
||||||
base_now = current_base or [0.2] * 5
|
# 우선순위: eval_result.previous_base > current_base (eval 직후 stale) > 균등 fallback
|
||||||
|
base_now = eval_result.get("previous_base") or current_base or [0.2] * 5
|
||||||
for i, (cur, new) in enumerate(zip(base_now, new_base)):
|
for i, (cur, new) in enumerate(zip(base_now, new_base)):
|
||||||
diff = new - cur
|
diff = new - cur
|
||||||
if abs(diff) < 0.005:
|
if abs(diff) < 0.005:
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ def test_evolution_report_winner_4plus():
|
|||||||
"n_picks": 5,
|
"n_picks": 5,
|
||||||
},
|
},
|
||||||
"new_base": [0.18, 0.32, 0.20, 0.22, 0.08],
|
"new_base": [0.18, 0.32, 0.20, 0.22, 0.08],
|
||||||
|
"previous_base": [0.20, 0.20, 0.20, 0.20, 0.20],
|
||||||
"update_reason": "winner_4plus",
|
"update_reason": "winner_4plus",
|
||||||
"per_day": [
|
"per_day": [
|
||||||
{"day_of_week": 0, "avg_score": 0.20, "max_correct": 2},
|
{"day_of_week": 0, "avg_score": 0.20, "max_correct": 2},
|
||||||
@@ -58,3 +59,29 @@ def test_evolution_report_empty_returns_empty():
|
|||||||
"""evaluate가 ok=False면 빈 문자열 (발송 skip)."""
|
"""evaluate가 ok=False면 빈 문자열 (발송 skip)."""
|
||||||
text = _format_evolution_report({"ok": False, "reason": "no_trials"}, [0.2]*5)
|
text = _format_evolution_report({"ok": False, "reason": "no_trials"}, [0.2]*5)
|
||||||
assert text == ""
|
assert text == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_evolution_report_uses_previous_base_for_diff():
|
||||||
|
"""previous_base와 new_base 차이가 메시지 diff에 정확히 반영됨."""
|
||||||
|
eval_result = {
|
||||||
|
"ok": True,
|
||||||
|
"draw_no": 1227,
|
||||||
|
"winner": {
|
||||||
|
"day_of_week": 0,
|
||||||
|
"weight": [0.30, 0.20, 0.20, 0.20, 0.10],
|
||||||
|
"avg_score": 0.50,
|
||||||
|
"max_correct": 4,
|
||||||
|
"n_picks": 5,
|
||||||
|
},
|
||||||
|
"new_base": [0.30, 0.20, 0.20, 0.20, 0.10],
|
||||||
|
"previous_base": [0.20, 0.20, 0.20, 0.20, 0.20],
|
||||||
|
"update_reason": "winner_4plus",
|
||||||
|
}
|
||||||
|
# current_base는 stale (post-update 값) — previous_base가 우선 적용되어야 함
|
||||||
|
text = _format_evolution_report(eval_result, [0.30, 0.20, 0.20, 0.20, 0.10])
|
||||||
|
# freq: 0.20 → 0.30 (+0.10 = "++")
|
||||||
|
# divers: 0.20 → 0.10 (-0.10 = "--")
|
||||||
|
assert "0.20 → 0.30" in text # freq 증가
|
||||||
|
assert "0.20 → 0.10" in text # divers 감소
|
||||||
|
assert "(++)" in text or "(+)" in text # freq marker
|
||||||
|
assert "(--)" in text or "(-)" in text # divers marker
|
||||||
|
|||||||
@@ -133,11 +133,11 @@ async def _run_weight_evolver_weekly():
|
|||||||
|
|
||||||
|
|
||||||
async def _run_weight_evolver_daily():
|
async def _run_weight_evolver_daily():
|
||||||
"""매일 09:00 (월요일 제외 — 월은 weekly cron이 inline으로 처리)."""
|
"""매일 09:00 (월/일 제외 — 월=weekly inline, 일=토 trial 보호)."""
|
||||||
try:
|
try:
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
KST = timezone(timedelta(hours=9))
|
KST = timezone(timedelta(hours=9))
|
||||||
if datetime.now(KST).weekday() == 0:
|
if datetime.now(KST).weekday() in (0, 6):
|
||||||
return
|
return
|
||||||
apply_today_and_pick(n=5)
|
apply_today_and_pick(n=5)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -277,8 +277,24 @@ def evaluate_weekly() -> Dict[str, Any]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
next_monday = today + timedelta(days=(7 - today.weekday()) % 7 or 7)
|
next_monday = today + timedelta(days=(7 - today.weekday()) % 7 or 7)
|
||||||
|
next_monday_iso = next_monday.isoformat()
|
||||||
|
|
||||||
|
# Idempotent guard: 같은 effective_from으로 이미 저장된 row가 있으면 skip
|
||||||
|
existing = db.get_base_history(limit=1)
|
||||||
|
if existing and existing[0]["effective_from"] == next_monday_iso:
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"draw_no": latest["drw_no"],
|
||||||
|
"week_start": week_start,
|
||||||
|
"previous_base": existing[0].get("weight"),
|
||||||
|
"winner": winner,
|
||||||
|
"new_base": existing[0]["weight"], # 이미 저장된 값
|
||||||
|
"update_reason": existing[0].get("update_reason", "idempotent_skip"),
|
||||||
|
"per_day": per_day,
|
||||||
|
}
|
||||||
|
|
||||||
db.save_base_history(
|
db.save_base_history(
|
||||||
effective_from=next_monday.isoformat(),
|
effective_from=next_monday_iso,
|
||||||
weight=new_base,
|
weight=new_base,
|
||||||
source_trial_id=winner["trial_id"],
|
source_trial_id=winner["trial_id"],
|
||||||
update_reason=reason,
|
update_reason=reason,
|
||||||
@@ -290,6 +306,7 @@ def evaluate_weekly() -> Dict[str, Any]:
|
|||||||
"ok": True,
|
"ok": True,
|
||||||
"draw_no": latest["drw_no"],
|
"draw_no": latest["drw_no"],
|
||||||
"week_start": week_start,
|
"week_start": week_start,
|
||||||
|
"previous_base": current_base, # save 이전에 캡처한 값 — diff 계산용
|
||||||
"winner": winner,
|
"winner": winner,
|
||||||
"new_base": new_base,
|
"new_base": new_base,
|
||||||
"update_reason": reason,
|
"update_reason": reason,
|
||||||
|
|||||||
Reference in New Issue
Block a user