docs(spec): LottoAgent 능동성 확장 설계 (능동 시그널·일일 요약)
Why: 매주 1회 무조건 큐레이션만 있는 현 구조를 다중 트리거+적응형 시그널 모니터링으로 확장. 좋은 수치(z≥1.5) 일 때만 텔레그램 보고. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
301
docs/superpowers/specs/2026-05-20-lotto-active-agent-design.md
Normal file
301
docs/superpowers/specs/2026-05-20-lotto-active-agent-design.md
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
# LottoAgent 능동성 확장 설계
|
||||||
|
|
||||||
|
- **상태**: Draft (사용자 리뷰 대기)
|
||||||
|
- **작성일**: 2026-05-20
|
||||||
|
- **대상 컨테이너**: agent-office
|
||||||
|
- **영향 외부 도메인**: lotto-lab (read-only API 소비만)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 문제 정의
|
||||||
|
|
||||||
|
현재 LottoAgent는 매주 월요일 09:05 cron으로 무조건 큐레이션을 1회 실행하고 헤드라인을 텔레그램으로 푸시한다. "결과가 좋지 않은 회차"도 동일하게 발화되며, **정량적 시그널이 평소보다 강할 때 별도로 알리는 능동성**이 없다.
|
||||||
|
|
||||||
|
사용자 의도: 통계·시뮬레이션·전략 가중치를 에이전트가 스스로 모니터링하다가 "좋은 수치"가 나오면 능동적으로 보고하는 패턴.
|
||||||
|
|
||||||
|
## 2. 의사결정 요약
|
||||||
|
|
||||||
|
| 결정 사항 | 선택 | 비고 |
|
||||||
|
|---|---|---|
|
||||||
|
| 분석 주기 | 다중 트리거 혼합 | 매일 정기 + 시뮬레이션 후 + 회차 후 |
|
||||||
|
| 시그널 종류 | 3종 — Sim Consensus / Strategy Drift / Confidence | Hot/Cold 변화는 제외 (노이즈) |
|
||||||
|
| 알림 정책 | 일일 요약 + 긴급 즉시 | 2개 동시 발화 OR 단일 z≥2.5 → 긴급 |
|
||||||
|
| 임계치 전략 | 적응형 (최근 8회 μ + σ) | warmup·보수적 단계 포함 |
|
||||||
|
| 시뮬 강도 조절 (Layer B) | v1 미포함 | 운영 검증 후 v2에서 도입 검토 |
|
||||||
|
|
||||||
|
## 3. 아키텍처
|
||||||
|
|
||||||
|
### 3.1 컴포넌트 다이어그램
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ agent-office │
|
||||||
|
│ │
|
||||||
|
│ cron (scheduler.py) │
|
||||||
|
│ ├─ lotto_light_check 매일 09:15 │
|
||||||
|
│ ├─ lotto_sim_check 4시간마다 :15 │
|
||||||
|
│ ├─ lotto_deep_check 일/수 21:15 │
|
||||||
|
│ ├─ lotto_daily_digest 매일 09:25 │
|
||||||
|
│ └─ lotto_curate 월요일 09:05 (기존 유지) │
|
||||||
|
│ ↓ │
|
||||||
|
│ curator/signals.py (신규) │
|
||||||
|
│ ├─ evaluate_sim_consensus() ← lotto_best API │
|
||||||
|
│ ├─ evaluate_strategy_drift() ← strategy/weights API │
|
||||||
|
│ ├─ evaluate_confidence() ← deep_check 시 큐레이션 결과 │
|
||||||
|
│ └─ adaptive_baseline() ← μ, σ 갱신 │
|
||||||
|
│ ↓ │
|
||||||
|
│ agent_office.db │
|
||||||
|
│ ├─ lotto_signals (이벤트 이력) │
|
||||||
|
│ └─ lotto_baselines (롤링 8회 윈도우) │
|
||||||
|
│ ↓ │
|
||||||
|
│ notifiers/telegram_lotto.py │
|
||||||
|
│ ├─ send_urgent_signal() ← 긴급 │
|
||||||
|
│ └─ send_signal_summary() ← 일일 요약 │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↑ (HTTP GET, 기존 lotto-lab API 재사용, 변경 없음)
|
||||||
|
│
|
||||||
|
lotto:8000
|
||||||
|
├─ /api/lotto/best
|
||||||
|
├─ /api/lotto/strategy/weights
|
||||||
|
└─ /api/lotto/curator/*
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 책임 경계
|
||||||
|
|
||||||
|
- **lotto-lab**: 변경 없음. 기존 GET API만 소비.
|
||||||
|
- **agent-office**: 능동 모니터링 layer 전부 담당. DB도 `agent_office.db` 안에 분리해서 lotto.db와 결합 없음.
|
||||||
|
- **프론트엔드**: Phase 4 별도 (web-ui repo). 본 spec 범위 밖.
|
||||||
|
|
||||||
|
## 4. 시그널 평가 로직
|
||||||
|
|
||||||
|
### 4.1 Sim Consensus Score
|
||||||
|
|
||||||
|
```
|
||||||
|
best_picks 20개의 점수 5종 (s1..s5) 사용
|
||||||
|
|
||||||
|
normalize(s_k) = (s_k - min_k) / (max_k - min_k) per metric across 20 picks
|
||||||
|
consensus_i = geomean( normalize(s1_i), ..., normalize(s5_i) )
|
||||||
|
sim_signal = mean( sorted(consensus_i, desc)[:10] )
|
||||||
|
```
|
||||||
|
|
||||||
|
- 기하평균: 5종 점수가 **동시에** 높을 때만 강한 시그널. 단일 폭주는 감쇠.
|
||||||
|
- top-10 평균: 전체 20개 분포에서 강한 후보군의 농도 측정.
|
||||||
|
|
||||||
|
### 4.2 Strategy Drift Score
|
||||||
|
|
||||||
|
```
|
||||||
|
drift_t = Σ | w_strategy_t - w_strategy_{t-1} | for each strategy in strategy_weights
|
||||||
|
```
|
||||||
|
|
||||||
|
- 회차 단위로 비교. 한 전략이 EMA로 큰 폭 이동했을 때 누적값이 큼.
|
||||||
|
- 시스템이 "지난 회차에서 의미 있게 학습한" 시그널.
|
||||||
|
|
||||||
|
### 4.3 Confidence Score
|
||||||
|
|
||||||
|
`curator.pipeline.curate_weekly()` 반환의 `validated.confidence` (0~1) 그대로.
|
||||||
|
- light_check / sim_check: N/A (LLM 호출 없음)
|
||||||
|
- deep_check: 직전 큐레이션 confidence를 baseline 윈도우에 push
|
||||||
|
|
||||||
|
### 4.4 Adaptive Baseline
|
||||||
|
|
||||||
|
```
|
||||||
|
lotto_baselines.window_values = [v_{t-7}, v_{t-6}, ..., v_t] (FIFO 8)
|
||||||
|
mu = mean(window_values)
|
||||||
|
sigma = stddev(window_values, ddof=1)
|
||||||
|
z_now = (v_now - mu) / sigma
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Cold start**: window 크기 < 4 → fire_level='warmup', 발화 X
|
||||||
|
- **준비 단계**: window 4~7 → 임계치 z=2.0 (false positive 줄임)
|
||||||
|
- **정상 운영**: window 8 풀 → z_normal=1.5, z_urgent=2.5
|
||||||
|
|
||||||
|
### 4.5 Trigger × Metric 매트릭스
|
||||||
|
|
||||||
|
| Trigger | Sim Consensus | Strategy Drift | Confidence |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `light_check` (매일 09:15) | ✓ 평가 | ✓ 회차 변경 시만 | — |
|
||||||
|
| `sim_check` (4h마다) | ✓ 평가 | ✓ 회차 변경 시만 | — |
|
||||||
|
| `deep_check` (일/수 21:15) | ✓ 평가 | ✓ 회차 변경 시만 | ✓ (큐레이션 후) |
|
||||||
|
| `lotto_curate` (월 09:05) | — | — | ✓ 큐레이션 결과 직접 push |
|
||||||
|
|
||||||
|
**회차 변경 가드**: Strategy Drift / Confidence는 **회차 단위 메트릭**. baseline 윈도우에 push할 때 `last_pushed_draw_no`를 비교, 동일 회차면 skip. 같은 회차 내에서 값 비교는 가능하지만 baseline 갱신은 회차당 1회만.
|
||||||
|
|
||||||
|
```
|
||||||
|
if metric in ('drift', 'confidence'):
|
||||||
|
if current_draw_no == baselines[metric].last_pushed_draw_no:
|
||||||
|
# baseline 윈도우는 그대로, z-score만 현재값으로 비교
|
||||||
|
skip_window_update = True
|
||||||
|
```
|
||||||
|
|
||||||
|
Sim Consensus는 회차 무관 (4시간마다 시뮬 자체가 갱신) → 매 평가 시 window push.
|
||||||
|
|
||||||
|
### 4.6 Fire 결정
|
||||||
|
|
||||||
|
```
|
||||||
|
fires = [m for m in [sim, drift, conf] if m.z >= LOTTO_Z_NORMAL]
|
||||||
|
if len(fires) >= 2 or any(m.z >= LOTTO_Z_URGENT for m in fires):
|
||||||
|
fire_level = 'urgent'
|
||||||
|
elif len(fires) == 1:
|
||||||
|
fire_level = 'normal'
|
||||||
|
else:
|
||||||
|
fire_level = 'noop'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 알림 흐름
|
||||||
|
|
||||||
|
### 5.1 트리거→발송 다이어그램
|
||||||
|
|
||||||
|
```
|
||||||
|
cron / signal_check
|
||||||
|
↓
|
||||||
|
signals.evaluate_all()
|
||||||
|
↓
|
||||||
|
lotto_signals INSERT (all results)
|
||||||
|
↓
|
||||||
|
fire_level == 'urgent' → send_urgent_signal() → 텔레그램 즉시
|
||||||
|
fire_level == 'normal' → 09:25 digest 합류
|
||||||
|
fire_level == 'noop' → 기록만
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 텔레그램 메시지 폼
|
||||||
|
|
||||||
|
**Urgent**:
|
||||||
|
```
|
||||||
|
🚨 로또 능동 신호
|
||||||
|
|
||||||
|
[2026-05-20 16:18]
|
||||||
|
강한 시그널 2종 동시 발화:
|
||||||
|
• Sim Consensus 1.84 (μ=1.02, σ=0.21) z=3.9
|
||||||
|
• Strategy Drift 0.18 (μ=0.06, σ=0.04) z=3.0
|
||||||
|
|
||||||
|
요인: gap_focus 전략이 지난 3회차 EMA +22%p
|
||||||
|
다음 시뮬: 20:05
|
||||||
|
|
||||||
|
[자세히 보기] (→ /lotto/agent)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Daily digest** (09:25):
|
||||||
|
```
|
||||||
|
📊 로또 일일 요약 (지난 24h)
|
||||||
|
|
||||||
|
평가 6회 / 발화 2회
|
||||||
|
• Sim Consensus normal z=1.7 (16:18)
|
||||||
|
• Confidence normal z=1.6 (월 09:05)
|
||||||
|
|
||||||
|
전략 가중치 추세 (최근 8회 baseline):
|
||||||
|
gap_focus ↑ +12%
|
||||||
|
hot_focus → -2%
|
||||||
|
pair_bias ↓ -8%
|
||||||
|
```
|
||||||
|
|
||||||
|
- 24h 내 발화 0건이면 digest 자체 skip (조용한 날 강제 알림 없음).
|
||||||
|
|
||||||
|
### 5.3 Throttle 규칙
|
||||||
|
|
||||||
|
| 규칙 | 동작 |
|
||||||
|
|---|---|
|
||||||
|
| 같은 metric + 같은 fire_level이 6시간 이내 재발화 | 두 번째는 DB 기록만, 텔레그램 skip |
|
||||||
|
| urgent 누적 ≥ 3통/day | 4번째부터 normal로 강등 → digest 합류 |
|
||||||
|
| digest 24h 발화 0건 | digest skip |
|
||||||
|
| Anthropic / 텔레그램 실패 | 평가는 success로 기록, 메시지만 60초 후 1회 retry |
|
||||||
|
|
||||||
|
## 6. 데이터 모델
|
||||||
|
|
||||||
|
### 6.1 lotto_signals
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE IF NOT EXISTS lotto_signals (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
triggered_at TEXT NOT NULL, -- ISO8601 UTC
|
||||||
|
source TEXT NOT NULL, -- 'light' | 'sim' | 'deep'
|
||||||
|
metric TEXT NOT NULL, -- 'sim_signal' | 'drift' | 'confidence'
|
||||||
|
value REAL NOT NULL,
|
||||||
|
baseline_mu REAL,
|
||||||
|
baseline_sigma REAL,
|
||||||
|
z_score REAL,
|
||||||
|
fire_level TEXT NOT NULL, -- 'noop' | 'warmup' | 'normal' | 'urgent'
|
||||||
|
notified_at TEXT, -- 텔레그램 발송 시각 (NULL=미발송)
|
||||||
|
payload TEXT -- JSON 부가 정보
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_ls_triggered ON lotto_signals(triggered_at DESC);
|
||||||
|
CREATE INDEX idx_ls_fire ON lotto_signals(fire_level, notified_at);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 lotto_baselines
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE IF NOT EXISTS lotto_baselines (
|
||||||
|
metric TEXT PRIMARY KEY,
|
||||||
|
window_values TEXT NOT NULL, -- JSON: [v1..v8]
|
||||||
|
mu REAL NOT NULL,
|
||||||
|
sigma REAL NOT NULL,
|
||||||
|
last_pushed_draw_no INTEGER, -- 회차 단위 메트릭의 중복 push 방지 (drift, confidence)
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
마이그레이션: `agent-office/app/db.py`의 `init_db()`에 `CREATE TABLE IF NOT EXISTS` 추가만으로 idempotent. 기존 테이블 영향 없음.
|
||||||
|
|
||||||
|
## 7. API 추가
|
||||||
|
|
||||||
|
| 메서드 | 경로 | 설명 |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/api/agent-office/lotto/signals?days=7` | 시그널 이력 (timeline, 차트용) |
|
||||||
|
| GET | `/api/agent-office/lotto/baselines` | 현재 baseline μ/σ 조회 |
|
||||||
|
| POST | `/api/agent-office/lotto/signal-check` | 수동 트리거 (디버깅·테스트용) |
|
||||||
|
|
||||||
|
## 8. 환경 변수
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LOTTO_SIGNAL_WINDOW=8 # baseline 윈도우 크기
|
||||||
|
LOTTO_Z_NORMAL=1.5 # normal fire 임계치
|
||||||
|
LOTTO_Z_URGENT=2.5 # urgent fire 임계치
|
||||||
|
LOTTO_DIGEST_HOUR=9 # digest cron hour (KST)
|
||||||
|
LOTTO_DIGEST_MIN=25
|
||||||
|
LOTTO_THROTTLE_HOURS=6 # 같은 메트릭 재발화 throttle
|
||||||
|
LOTTO_URGENT_DAILY_MAX=3 # urgent 하루 cap
|
||||||
|
```
|
||||||
|
|
||||||
|
모두 default 있음. `.env` 미설정 시 default로 동작.
|
||||||
|
|
||||||
|
## 9. 스케줄러 cron
|
||||||
|
|
||||||
|
```python
|
||||||
|
scheduler.add_job(lotto_light_check, "cron", hour=9, minute=15, id="lotto_light_check")
|
||||||
|
scheduler.add_job(lotto_sim_check, "cron", minute=15, hour="0,4,8,12,16,20", id="lotto_sim_check")
|
||||||
|
scheduler.add_job(lotto_deep_check, "cron", day_of_week="sun,wed", hour=21, minute=15, id="lotto_deep_check")
|
||||||
|
scheduler.add_job(lotto_daily_digest, "cron", hour=9, minute=25, id="lotto_digest")
|
||||||
|
# 기존: lotto_curate (월 09:05) 유지
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. 구현 Phase
|
||||||
|
|
||||||
|
| Phase | 범위 | 검증 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | DB 마이그레이션 + `signals.py` (순수 함수, LLM X) | `POST /lotto/signal-check`로 수동 호출 → z-score 계산 확인 |
|
||||||
|
| 2 | cron 4개 + `lotto_signals` INSERT (텔레그램 X) | 24h 가동 → DB에 시그널 누적 |
|
||||||
|
| 3 | 텔레그램 urgent / digest + throttle | dry-run env로 메시지 검증 후 실제 발송 |
|
||||||
|
| 4 | 프론트 (web-ui) — `/lotto/agent` 시그널 timeline UI | 별도 PR (본 spec 범위 외) |
|
||||||
|
|
||||||
|
Phase 1~3이 백엔드 능동성 완성. 각 Phase 끝에 commit + 자동 배포.
|
||||||
|
|
||||||
|
## 11. 비기능 요구
|
||||||
|
|
||||||
|
- **백워드 호환**: 기존 월요일 큐레이션 cron 변경 없음
|
||||||
|
- **장애 격리**: signals 평가 실패해도 LottoAgent.state는 idle 유지 (try/except + add_log warning)
|
||||||
|
- **테스트**: `signals.py`의 메트릭 함수는 input/output 순수형 → 단위 테스트 작성 가능
|
||||||
|
- **관측**: `agent_logs` 테이블 그대로 활용 (별도 로깅 추가 없음)
|
||||||
|
|
||||||
|
## 12. 비목표 (Out of scope)
|
||||||
|
|
||||||
|
- 자동 구매·자동 픽 갱신 (보고만)
|
||||||
|
- 시뮬레이션 강도 자동 조절 (Layer B는 v2)
|
||||||
|
- 텔레그램 인라인 키보드 (v2에서 자동 액션 도입 시 함께)
|
||||||
|
- 핫넘버/콜드넘버 시그널 (노이즈 위험, v1 제외)
|
||||||
|
- 프론트 UI (별도 PR)
|
||||||
|
|
||||||
|
## 13. v2 후속 검토
|
||||||
|
|
||||||
|
- Layer B 시뮬 강도 조절 (모호 시그널 시 deep_simulate)
|
||||||
|
- 사용자 피드백 루프 (텔레그램 [좋아요/별로] 버튼 → 임계치 가중 조정)
|
||||||
|
- 회차 retrospective 자동 분석 (당첨번호 vs 추천번호 패턴 학습)
|
||||||
Reference in New Issue
Block a user