From d0047c2b9df41174c06cfb1061c1b8f8d3c0ae6c Mon Sep 17 00:00:00 2001 From: gahusb Date: Fri, 22 May 2026 03:21:23 +0900 Subject: [PATCH] =?UTF-8?q?feat(lotto-evolver):=20=ED=85=94=EB=A0=88?= =?UTF-8?q?=EA=B7=B8=EB=9E=A8=20=EC=A3=BC=EA=B0=84=20evolution=20report=20?= =?UTF-8?q?=ED=8F=AC=EB=A7=B7=20+=20=EB=B0=9C=EC=86=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent-office/app/notifiers/telegram_lotto.py | 67 ++++++++++++++++++- .../tests/test_lotto_evolution_format.py | 60 +++++++++++++++++ 2 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 agent-office/tests/test_lotto_evolution_format.py diff --git a/agent-office/app/notifiers/telegram_lotto.py b/agent-office/app/notifiers/telegram_lotto.py index 8673e51..f86035d 100644 --- a/agent-office/app/notifiers/telegram_lotto.py +++ b/agent-office/app/notifiers/telegram_lotto.py @@ -1,6 +1,6 @@ """로또 큐레이션·당첨 알림 — 텔레그램 푸시.""" import logging -from typing import Dict, Any +from typing import Dict, Any, List # 기존 에이전트들과 동일한 패턴: send_raw(text, reply_markup=None, chat_id=None) # chat_id 생략 시 기본 TELEGRAM_CHAT_ID로 자동 발송. @@ -159,3 +159,68 @@ async def send_signal_summary(digest: Dict[str, Any]) -> None: await send_raw(text) except Exception as e: logger.warning(f"[telegram_lotto] digest send failed: {e}") + + +# ---------- Weight Evolver 주간 리포트 ---------- + +_DAY_NAMES = ["월", "화", "수", "목", "금", "토"] +_METRIC_NAMES = ["freq", "finger", "gap", "cooccur", "divers"] +_REASON_LABEL = { + "winner_4plus": "4개 이상 일치 → base 교체", + "ema_blend": "3개 일치 → EMA blend (0.3)", + "unchanged": "유효 성과 없음 → base 유지", + "cold_start": "초기 균등 적용", +} + + +def _format_evolution_report(eval_result: Dict[str, Any], current_base: List[float]) -> str: + """주간 weight evolution 텔레그램 메시지. ok=False 또는 winner 없으면 빈 문자열.""" + if not eval_result or "winner" not in eval_result: + return "" + + draw_no = eval_result.get("draw_no", "?") + winner = eval_result["winner"] + new_base = eval_result.get("new_base") or [0.0] * 5 + reason = eval_result.get("update_reason", "") + dow = winner.get("day_of_week", 0) + day_name = _DAY_NAMES[dow] if 0 <= dow < len(_DAY_NAMES) else "?" + + lines = [ + f"🧬 로또 학습 주간 리포트 ({draw_no}회차)", + "", + f"이번주 시도: 6일 × {winner.get('n_picks', 5)}세트", + "", + f"🏆 Winner: {day_name}요일", + f" W = [" + ", ".join( + f"{name} {w:.2f}" for name, w in zip(_METRIC_NAMES, winner["weight"]) + ) + "]", + f" 최고 적중: {winner.get('max_correct', 0)}개 일치 (max={winner.get('max_correct', 0)})", + f" 평균 점수: {winner.get('avg_score', 0):.2f}", + "", + f"📊 다음주 base 변경 ({reason}):", + ] + base_now = current_base or [0.2] * 5 + for i, (cur, new) in enumerate(zip(base_now, new_base)): + diff = new - cur + if abs(diff) < 0.005: + marker = "=" + elif diff > 0: + marker = "+" if diff < 0.05 else "++" + else: + marker = "-" if diff > -0.05 else "--" + lines.append(f" {_METRIC_NAMES[i]:8s} {cur:.2f} → {new:.2f} ({marker})") + lines.append("") + lines.append(f" → {_REASON_LABEL.get(reason, reason)}") + lines.append("") + lines.append(f"[웹에서 차트 보기] ({LOTTO_URL}/evolver)") + return "\n".join(lines) + + +async def send_evolution_report(eval_result: Dict[str, Any], current_base: List[float]) -> None: + text = _format_evolution_report(eval_result, current_base) + if not text: + return + try: + await send_raw(text) + except Exception as e: + logger.warning(f"[telegram_lotto] evolution report send failed: {e}") diff --git a/agent-office/tests/test_lotto_evolution_format.py b/agent-office/tests/test_lotto_evolution_format.py new file mode 100644 index 0000000..2085ff6 --- /dev/null +++ b/agent-office/tests/test_lotto_evolution_format.py @@ -0,0 +1,60 @@ +import sys, os +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) + +from app.notifiers.telegram_lotto import _format_evolution_report + + +def test_evolution_report_winner_4plus(): + eval_result = { + "ok": True, + "draw_no": 1225, + "week_start": "2026-05-18", + "winner": { + "day_of_week": 3, + "weight": [0.18, 0.32, 0.20, 0.22, 0.08], + "avg_score": 0.42, + "max_correct": 4, + "n_picks": 5, + }, + "new_base": [0.18, 0.32, 0.20, 0.22, 0.08], + "update_reason": "winner_4plus", + "per_day": [ + {"day_of_week": 0, "avg_score": 0.20, "max_correct": 2}, + {"day_of_week": 3, "avg_score": 0.42, "max_correct": 4}, + ], + } + current_base = [0.20, 0.20, 0.20, 0.20, 0.20] + text = _format_evolution_report(eval_result, current_base) + assert "🧬" in text + assert "1225" in text + assert "목요일" in text or "Winner" in text + assert "4개 일치" in text or "max=4" in text + assert "winner_4plus" in text + + +def test_evolution_report_unchanged(): + eval_result = { + "ok": True, + "draw_no": 1226, + "week_start": "2026-05-25", + "winner": { + "day_of_week": 1, + "weight": [0.21, 0.19, 0.20, 0.20, 0.20], + "avg_score": 0.10, + "max_correct": 2, + "n_picks": 5, + }, + "new_base": [0.20, 0.20, 0.20, 0.20, 0.20], + "update_reason": "unchanged", + "per_day": [], + } + current_base = [0.20, 0.20, 0.20, 0.20, 0.20] + text = _format_evolution_report(eval_result, current_base) + assert "unchanged" in text or "유지" in text + assert "2개 일치" in text or "max=2" in text + + +def test_evolution_report_empty_returns_empty(): + """evaluate가 ok=False면 빈 문자열 (발송 skip).""" + text = _format_evolution_report({"ok": False, "reason": "no_trials"}, [0.2]*5) + assert text == ""