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:
@@ -30,8 +30,7 @@ class LottoAgent(BaseAgent):
|
|||||||
async def run_signal_check(self, source: str = "light") -> dict:
|
async def run_signal_check(self, source: str = "light") -> dict:
|
||||||
"""비-LLM 시그널 평가 (light/sim) 또는 deep_check (LLM 호출 후).
|
"""비-LLM 시그널 평가 (light/sim) 또는 deep_check (LLM 호출 후).
|
||||||
|
|
||||||
Phase 2: 텔레그램 발송 안 함. lotto_signals INSERT만.
|
Phase 3 (Task 9): urgent 시그널 텔레그램 발송 + throttle/daily-cap 추가.
|
||||||
Phase 3 (Task 9): urgent 시그널 텔레그램 발송 + throttle 추가.
|
|
||||||
"""
|
"""
|
||||||
from ..curator.signal_runner import run_signal_check
|
from ..curator.signal_runner import run_signal_check
|
||||||
from ..config import LOTTO_Z_NORMAL, LOTTO_Z_URGENT
|
from ..config import LOTTO_Z_NORMAL, LOTTO_Z_URGENT
|
||||||
@@ -62,17 +61,86 @@ class LottoAgent(BaseAgent):
|
|||||||
self.agent_id,
|
self.agent_id,
|
||||||
f"signal_check({source}) → overall={outcome['overall_fire']} results={len(outcome['results'])}",
|
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}
|
return {"ok": True, **outcome}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
add_log(self.agent_id, f"signal_check 예외: {e}", level="error")
|
add_log(self.agent_id, f"signal_check 예외: {e}", level="error")
|
||||||
return {"ok": False, "message": f"{type(e).__name__}: {e}"}
|
return {"ok": False, "message": f"{type(e).__name__}: {e}"}
|
||||||
|
|
||||||
async def run_daily_digest(self) -> dict:
|
async def run_daily_digest(self) -> dict:
|
||||||
"""Phase 2: 발화 카운트만 반환. Phase 3 (Task 9)에서 텔레그램 발송 추가."""
|
"""일일 요약 — 지난 24h normal/urgent 발화를 묶어 텔레그램 1통."""
|
||||||
from ..db import get_recent_lotto_signals, add_log
|
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")
|
sigs = get_recent_lotto_signals(hours=24, min_fire="normal")
|
||||||
add_log(self.agent_id, f"daily_digest: 지난 24h 발화 {len(sigs)}건")
|
total_24h = get_signals_history(days=1)
|
||||||
return {"ok": True, "count": len(sigs), "signals": sigs}
|
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:
|
async def _run(self, source: str) -> dict:
|
||||||
task_id = create_task(self.agent_id, "curate_weekly", {"source": source})
|
task_id = create_task(self.agent_id, "curate_weekly", {"source": source})
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ def evaluate_metric_and_persist(
|
|||||||
"signal_id": sid,
|
"signal_id": sid,
|
||||||
"metric": metric,
|
"metric": metric,
|
||||||
"value": value,
|
"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,
|
"z_score": z,
|
||||||
"fire_level": fire,
|
"fire_level": fire,
|
||||||
"payload": payload or {},
|
"payload": payload or {},
|
||||||
|
|||||||
@@ -88,10 +88,11 @@ def _format_urgent_signal(event: Dict[str, Any]) -> str:
|
|||||||
mu = r.get("baseline_mu")
|
mu = r.get("baseline_mu")
|
||||||
sigma = r.get("baseline_sigma")
|
sigma = r.get("baseline_sigma")
|
||||||
z = r.get("z_score")
|
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:
|
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:
|
else:
|
||||||
lines.append(f"• {label} {v:.2f}")
|
lines.append(f"• {label} {v_text}")
|
||||||
|
|
||||||
# drift 페이로드 — 어떤 전략이 변동했는지 한 줄
|
# drift 페이로드 — 어떤 전략이 변동했는지 한 줄
|
||||||
for r in fired:
|
for r in fired:
|
||||||
|
|||||||
Reference in New Issue
Block a user