feat(lotto-signals): urgent 텔레그램 발송 + throttle/cap + daily digest 발송 + baseline_mu/sigma 노출

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 03:13:29 +09:00
parent 8552cbc184
commit 17321d948e
3 changed files with 79 additions and 8 deletions

View File

@@ -30,8 +30,7 @@ class LottoAgent(BaseAgent):
async def run_signal_check(self, source: str = "light") -> dict:
"""비-LLM 시그널 평가 (light/sim) 또는 deep_check (LLM 호출 후).
Phase 2: 텔레그램 발송 안 함. lotto_signals INSERT만.
Phase 3 (Task 9): urgent 시그널 텔레그램 발송 + throttle 추가.
Phase 3 (Task 9): urgent 시그널 텔레그램 발송 + throttle/daily-cap 추가.
"""
from ..curator.signal_runner import run_signal_check
from ..config import LOTTO_Z_NORMAL, LOTTO_Z_URGENT
@@ -62,17 +61,86 @@ class LottoAgent(BaseAgent):
self.agent_id,
f"signal_check({source}) → overall={outcome['overall_fire']} results={len(outcome['results'])}",
)
# --- Throttle + 텔레그램 urgent 발송 ---
from ..config import LOTTO_THROTTLE_HOURS, LOTTO_URGENT_DAILY_MAX
from ..db import (
get_last_signal_notification, get_recent_urgent_count,
mark_signal_notified,
)
from ..notifiers.telegram_lotto import send_urgent_signal
if outcome["overall_fire"] == "urgent":
if get_recent_urgent_count(hours=24) >= LOTTO_URGENT_DAILY_MAX:
add_log(
self.agent_id,
"urgent daily cap 도달 → normal로 강등 (digest 합류)",
level="warning",
)
else:
blocked = False
for r in outcome["results"]:
if r["fire_level"] in ("normal", "urgent"):
last = get_last_signal_notification(
metric=r["metric"], fire_level=r["fire_level"],
hours=LOTTO_THROTTLE_HOURS,
)
if last:
blocked = True
break
if not blocked:
from datetime import datetime, timezone
event = {
"fire_level": "urgent",
"triggered_at": datetime.now(timezone.utc).isoformat(),
"results": outcome["results"],
}
await send_urgent_signal(event)
for r in outcome["results"]:
if r["fire_level"] in ("normal", "urgent"):
mark_signal_notified(r["signal_id"])
add_log(self.agent_id, f"urgent 텔레그램 발송 완료 (시그널 {len(outcome['results'])}개 마킹)")
return {"ok": True, **outcome}
except Exception as e:
add_log(self.agent_id, f"signal_check 예외: {e}", level="error")
return {"ok": False, "message": f"{type(e).__name__}: {e}"}
async def run_daily_digest(self) -> dict:
"""Phase 2: 발화 카운트만 반환. Phase 3 (Task 9)에서 텔레그램 발송 추가."""
from ..db import get_recent_lotto_signals, add_log
"""일일 요약 — 지난 24h normal/urgent 발화를 묶어 텔레그램 1통."""
from ..db import (
get_recent_lotto_signals, get_signals_history, add_log,
get_baseline,
)
from ..notifiers.telegram_lotto import send_signal_summary
sigs = get_recent_lotto_signals(hours=24, min_fire="normal")
add_log(self.agent_id, f"daily_digest: 지난 24h 발화 {len(sigs)}")
return {"ok": True, "count": len(sigs), "signals": sigs}
total_24h = get_signals_history(days=1)
evaluated = len(total_24h)
# weights_trend: drift_weights_cache의 prev/curr 차이
trend = {}
try:
cache = get_baseline("drift_weights_cache")
if cache and isinstance(cache["window_values"], list) and len(cache["window_values"]) >= 2:
prev_w = cache["window_values"][-2]
curr_w = cache["window_values"][-1]
trend = {
k: curr_w.get(k, 0.0) - prev_w.get(k, 0.0)
for k in (set(prev_w) | set(curr_w))
}
except Exception as e:
add_log(self.agent_id, f"weights_trend 계산 실패: {e}", level="warning")
digest = {
"evaluated": evaluated,
"fired": len(sigs),
"signals": sigs,
"weights_trend": trend,
}
await send_signal_summary(digest)
add_log(self.agent_id, f"daily_digest 발송: 평가 {evaluated} / 발화 {len(sigs)}")
return {"ok": True, **digest}
async def _run(self, source: str) -> dict:
task_id = create_task(self.agent_id, "curate_weekly", {"source": source})

View File

@@ -80,6 +80,8 @@ def evaluate_metric_and_persist(
"signal_id": sid,
"metric": metric,
"value": value,
"baseline_mu": bl.mu if bl.size > 0 else None,
"baseline_sigma": bl.sigma if bl.size >= 2 else None,
"z_score": z,
"fire_level": fire,
"payload": payload or {},

View File

@@ -88,10 +88,11 @@ def _format_urgent_signal(event: Dict[str, Any]) -> str:
mu = r.get("baseline_mu")
sigma = r.get("baseline_sigma")
z = r.get("z_score")
v_text = f"{v:.2f}" if v is not None else "N/A"
if mu is not None and sigma is not None and z is not None:
lines.append(f"{label} {v:.2f} (μ={mu:.2f}, σ={sigma:.2f}) z={z:.1f}")
lines.append(f"{label} {v_text} (μ={mu:.2f}, σ={sigma:.2f}) z={z:.1f}")
else:
lines.append(f"{label} {v:.2f}")
lines.append(f"{label} {v_text}")
# drift 페이로드 — 어떤 전략이 변동했는지 한 줄
for r in fired: