docs(spec): Lotto Weight Evolver — 자율 학습 루프 설계 (v2)
Why: v1 능동 모니터링 위에 매주 6가지 가중치 시도+토요일 회고+ winner 기반 base 갱신 루프를 lotto-lab에 추가. 5종 시뮬 점수 가중치를 사람 없이 자가 학습. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
419
docs/superpowers/specs/2026-05-22-lotto-weight-evolver-design.md
Normal file
419
docs/superpowers/specs/2026-05-22-lotto-weight-evolver-design.md
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
# Lotto Weight Evolver — 자율 학습 루프 설계 (v2)
|
||||||
|
|
||||||
|
- **상태**: Draft (사용자 리뷰 대기)
|
||||||
|
- **작성일**: 2026-05-22
|
||||||
|
- **대상 컨테이너**: lotto (lotto-lab) + agent-office (텔레그램 보고)
|
||||||
|
- **선행 작업**: v1 LottoAgent 능동성 확장 (2026-05-20 배포)
|
||||||
|
- **목표**: 5종 시뮬 점수 가중치를 매주 6가지로 변형 시도 → 토요일 회고 → winner 가중치를 다음 주 base로 적용 → 무한 반복 자가 학습 루프
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 문제 정의
|
||||||
|
|
||||||
|
현재 `analyzer.score_combination()`은 5종 점수(`score_frequency`, `score_fingerprint`, `score_gap`, `score_cooccur`, `score_diversity`)를 **균등 합산**으로 `score_total`을 계산한다. 어떤 메트릭이 실제 추첨 결과와 더 잘 상관되는지에 대한 학습 없이 가중치가 고정.
|
||||||
|
|
||||||
|
또한 `purchase_history` 기반 `strategy_evolver`는 **사용자가 실제 구매한 번호만** 학습 시그널로 사용. 사람이 안 사면 학습 안 됨.
|
||||||
|
|
||||||
|
사용자 요구: 에이전트가 사람 없이도 **매일 다른 가중치로 시뮬레이션 → 번호 시도 → 토요일 추첨 후 best 가중치 식별 → 다음주 base 갱신**의 무한 학습 루프.
|
||||||
|
|
||||||
|
## 2. 의사결정 요약
|
||||||
|
|
||||||
|
| 결정 사항 | 선택 | 비고 |
|
||||||
|
|---|---|---|
|
||||||
|
| 학습 대상 | 시뮬 점수 5종 가중치 (`W = [w_freq, w_finger, w_gap, w_cooccur, w_diversity]`) | 메타 전략 가중치는 strategy_evolver가 별도 학습 (v2에서 손대지 않음) |
|
||||||
|
| 탐험 전략 | 현재 base 주변 4개 perturbation + Dirichlet 무작위 2개 | 매주 월요일 6개 후보 |
|
||||||
|
| 일일 시도량 | N = 5 세트/일 × 6일 = 30 세트/주 | 통계적 의미 + 비용 균형 |
|
||||||
|
| 평가 시그널 | strategy_evolver의 `RANK_BONUS` + `correct/6` | 기존 패턴 재사용으로 일관성 |
|
||||||
|
| Base 적용 강도 | Hybrid — winner_max_correct ≥ 4면 교체, =3이면 EMA blend (0.3), ≤2면 유지 | 노이즈에 base가 헤매지 않도록 보호 |
|
||||||
|
| v1과의 결합 | W가 `analyzer.score_combination`에 반영 → best_picks 점수 자동 영향 → v1 시그널 자동 cascade | 별도 통합 코드 없음 |
|
||||||
|
| strategy_evolver와의 상호작용 | strategy_evolver는 `score_total`을 그대로 입력으로 사용 → W 변경 시 입력 분포가 함께 변함. **의도된 간접 영향** | v3에서 메타 가중치도 함께 학습할 때 명시적으로 분리 검토 |
|
||||||
|
| 자동 구매 | v2 비포함 | 사람 결정 영역 — purchase_history는 사람이 등록 |
|
||||||
|
|
||||||
|
## 3. 아키텍처
|
||||||
|
|
||||||
|
### 3.1 컴포넌트 다이어그램
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ lotto-lab (자율 학습 루프 추가) │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ weight_evolver.py (신규) │ │
|
||||||
|
│ │ • generate_weekly_candidates() ← 월 09:00 │ │
|
||||||
|
│ │ • apply_today_weight() ← 매일 09:00 │ │
|
||||||
|
│ │ • evaluate_weekly() ← 토 22:00 │ │
|
||||||
|
│ │ • update_base() ← evaluate 안에서 │ │
|
||||||
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
|
│ ↓ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ analyzer.score_combination(numbers, cache, │ │
|
||||||
|
│ │ weights=None) 확장 │ │
|
||||||
|
│ │ • weights=None → 균등 합산 (기존 호환) │ │
|
||||||
|
│ │ • weights=[..] → 가중 합산 │ │
|
||||||
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
|
│ ↓ │
|
||||||
|
│ lotto.db 신규 테이블 3개 │
|
||||||
|
│ • weight_trials (주별 6일치 후보 가중치) │
|
||||||
|
│ • auto_picks (매일 N=5 시도 번호 + 채점 결과) │
|
||||||
|
│ • weight_base_history (base 변경 이력) │
|
||||||
|
│ │
|
||||||
|
│ 기존 시뮬 cron (00/04/08/12/16/20:05) — 변경 없음. │
|
||||||
|
│ 단 best_picks 재계산 시 활성 W를 읽어 적용. │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓ (HTTP)
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ agent-office │
|
||||||
|
│ │
|
||||||
|
│ cron 신규 1종: lotto_evolution_weekly (토 22:15) │
|
||||||
|
│ LottoAgent.run_weekly_evolution_report() (신규) │
|
||||||
|
│ notifiers/telegram_lotto.send_evolution_report() (신규) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 책임 경계
|
||||||
|
|
||||||
|
- **lotto-lab**: 가중치 생성·적용·평가·base 갱신 + DB CRUD + API. 시그널/알림 책임 없음.
|
||||||
|
- **agent-office**: 토요일 22:15 lotto-lab API 폴링 → 텔레그램 보고 1통.
|
||||||
|
- **v1 (signals layer)**: 변경 없음. W 변경의 효과는 best_picks 분포 변화로 자동 흡수.
|
||||||
|
- **strategy_evolver (메타 가중치 5종)**: 그대로 둠.
|
||||||
|
|
||||||
|
## 4. 가중치 진화 알고리즘
|
||||||
|
|
||||||
|
### 4.1 Weight Vector
|
||||||
|
|
||||||
|
```
|
||||||
|
W = [w_freq, w_finger, w_gap, w_cooccur, w_diversity]
|
||||||
|
제약: w_i ≥ 0.05, sum(W) = 1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
(MIN_WEIGHT=0.05는 한 메트릭이 죽지 않도록 보호. strategy_evolver의 MIN_WEIGHT 패턴.)
|
||||||
|
|
||||||
|
### 4.2 주간 6개 후보 생성
|
||||||
|
|
||||||
|
`generate_weekly_candidates()` — 매주 월요일 09:00 KST.
|
||||||
|
|
||||||
|
```python
|
||||||
|
W_base = get_current_base() # weight_base_history 최신 row, 없으면 [0.2]*5
|
||||||
|
|
||||||
|
# 4개 Local Perturbation
|
||||||
|
for i in range(4):
|
||||||
|
noise = np.random.normal(0, 0.05, size=5)
|
||||||
|
W_i = W_base + noise
|
||||||
|
W_i = clamp(W_i, min=0.05)
|
||||||
|
W_i = W_i / W_i.sum()
|
||||||
|
save_trial(week_start, day=i, W_i, source='perturb', base=W_base)
|
||||||
|
|
||||||
|
# 2개 Dirichlet 탐험
|
||||||
|
for i in range(4, 6):
|
||||||
|
W_i = np.random.dirichlet([2.0]*5)
|
||||||
|
W_i = clamp(W_i, min=0.05)
|
||||||
|
W_i = W_i / W_i.sum()
|
||||||
|
save_trial(week_start, day=i, W_i, source='dirichlet', base=W_base)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `σ=0.05` 정규분포: 각 메트릭 ±10%p 안쪽 변동
|
||||||
|
- `α=2.0` Dirichlet: 균등 분포에 약간 치우치게, 극단 가중치도 포함
|
||||||
|
|
||||||
|
### 4.3 일일 W 적용
|
||||||
|
|
||||||
|
`apply_today_weight()` — 매일 09:00 KST.
|
||||||
|
|
||||||
|
```python
|
||||||
|
W_today = get_trial(week_start, day_of_week=today)
|
||||||
|
set_active_weight(W_today) # 메모리 캐시 or DB row (W_active 테이블 또는 file)
|
||||||
|
generate_n_picks(N=5, weight=W_today) # auto_picks에 5세트 저장
|
||||||
|
```
|
||||||
|
|
||||||
|
같은 W로 그날 기존 시뮬 cron (4시간마다 6회) best_picks 재계산.
|
||||||
|
|
||||||
|
### 4.4 토요일 회고
|
||||||
|
|
||||||
|
`evaluate_weekly()` — 매주 토요일 22:00 KST (추첨 20:35 KST + sync 21:10 → 22:00 안전).
|
||||||
|
|
||||||
|
```python
|
||||||
|
winning_numbers = get_latest_draw().numbers # 1224, 1225, ...
|
||||||
|
trials = get_trials(week_start) # 6 trials
|
||||||
|
|
||||||
|
scores_per_day = []
|
||||||
|
for trial in trials:
|
||||||
|
picks = get_auto_picks(trial.id) # N=5
|
||||||
|
day_score = mean(
|
||||||
|
calc_pick_score(p.numbers, winning_numbers) for p in picks
|
||||||
|
)
|
||||||
|
max_correct = max(
|
||||||
|
count_match(p.numbers, winning_numbers) for p in picks
|
||||||
|
)
|
||||||
|
update_pick_grades(picks, winning_numbers) # auto_picks 채점 결과 저장
|
||||||
|
scores_per_day.append({
|
||||||
|
"trial_id": trial.id,
|
||||||
|
"day": trial.day_of_week,
|
||||||
|
"weight": trial.weight,
|
||||||
|
"score": day_score,
|
||||||
|
"max_correct": max_correct,
|
||||||
|
})
|
||||||
|
|
||||||
|
winner = max(scores_per_day, key=lambda s: s.score)
|
||||||
|
update_base(winner)
|
||||||
|
```
|
||||||
|
|
||||||
|
**점수 함수** (strategy_evolver `calc_draw_score` 패턴, 단순화):
|
||||||
|
|
||||||
|
v2에서는 보너스 번호를 평가에 포함하지 않음 → 5개 일치를 2등/3등으로 구분 불가. 따라서 보너스 무시한 단순 매핑:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# correct → rank 매핑 (보너스 제외)
|
||||||
|
RANK_BY_CORRECT = {
|
||||||
|
6: 1, # 1등
|
||||||
|
5: 3, # 3등 (보너스 평가 안 함 → 2등 표시 X)
|
||||||
|
4: 4,
|
||||||
|
3: 5,
|
||||||
|
}
|
||||||
|
RANK_BONUS = {1: 1.0, 2: 0.8, 3: 0.6, 4: 0.3, 5: 0.1}
|
||||||
|
|
||||||
|
def calc_pick_score(pick_numbers, winning_numbers):
|
||||||
|
correct = count_match(pick_numbers, winning_numbers[:6])
|
||||||
|
base = correct / 6.0
|
||||||
|
rank = RANK_BY_CORRECT.get(correct)
|
||||||
|
bonus = RANK_BONUS.get(rank, 0)
|
||||||
|
return base + bonus
|
||||||
|
```
|
||||||
|
|
||||||
|
(rank=2의 보너스 0.8은 매핑되지 않으므로 v2 점수에 등장하지 않음. v3에서 보너스 번호 평가 도입 시 활성화.)
|
||||||
|
|
||||||
|
### 4.5 Base 갱신 규칙 (Hybrid)
|
||||||
|
|
||||||
|
```python
|
||||||
|
if winner.max_correct >= 4:
|
||||||
|
W_base_next = winner.weight
|
||||||
|
reason = "winner_4plus"
|
||||||
|
elif winner.max_correct == 3:
|
||||||
|
W_base_next = 0.3 * winner.weight + 0.7 * W_base_current
|
||||||
|
reason = "ema_blend"
|
||||||
|
else:
|
||||||
|
W_base_next = W_base_current
|
||||||
|
reason = "unchanged"
|
||||||
|
|
||||||
|
save_to_weight_base_history(W_base_next, reason, winner)
|
||||||
|
```
|
||||||
|
|
||||||
|
성과가 약할 때 base를 그대로 두는 게 핵심 — base가 노이즈에 따라 헤매지 않음.
|
||||||
|
|
||||||
|
### 4.6 Cold start (첫 주)
|
||||||
|
|
||||||
|
`weight_base_history`가 비어있으면 `W_base = [0.2]*5` (균등) 가정. 첫 주는 4 perturbation이 모두 균등 주변, 2 Dirichlet 탐험.
|
||||||
|
|
||||||
|
## 5. 데이터 모델
|
||||||
|
|
||||||
|
### 5.1 weight_trials
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE IF NOT EXISTS weight_trials (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
week_start TEXT NOT NULL, -- 'YYYY-MM-DD' (해당 주 월요일)
|
||||||
|
day_of_week INTEGER NOT NULL, -- 0=월 .. 5=토
|
||||||
|
weight_json TEXT NOT NULL, -- '[0.18, 0.22, ...]'
|
||||||
|
source TEXT NOT NULL, -- 'perturb' | 'dirichlet'
|
||||||
|
base_at_gen TEXT, -- 생성 시점 W_base (참조용)
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
||||||
|
UNIQUE(week_start, day_of_week)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_wt_week ON weight_trials(week_start, day_of_week);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 auto_picks
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE IF NOT EXISTS auto_picks (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
trial_id INTEGER NOT NULL REFERENCES weight_trials(id) ON DELETE CASCADE,
|
||||||
|
pick_no INTEGER NOT NULL, -- 1..5
|
||||||
|
numbers TEXT NOT NULL, -- JSON 정렬 6개
|
||||||
|
meta_score REAL, -- 활성 W로 계산한 score_total
|
||||||
|
correct INTEGER, -- 채점 후 채워짐
|
||||||
|
rank INTEGER, -- 1..5 또는 NULL
|
||||||
|
graded_at TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
||||||
|
UNIQUE(trial_id, pick_no)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_ap_trial ON auto_picks(trial_id);
|
||||||
|
CREATE INDEX idx_ap_graded ON auto_picks(graded_at);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 weight_base_history
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE IF NOT EXISTS weight_base_history (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
effective_from TEXT NOT NULL, -- 'YYYY-MM-DD' (적용 시작 월요일)
|
||||||
|
weight_json TEXT NOT NULL,
|
||||||
|
source_trial_id INTEGER REFERENCES weight_trials(id), -- NULL=cold start
|
||||||
|
update_reason TEXT, -- 'winner_4plus' | 'ema_blend' | 'unchanged' | 'cold_start'
|
||||||
|
winner_score REAL,
|
||||||
|
winner_max_correct INTEGER,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
마이그레이션: `lotto/app/db.py`의 `init_db()`에 `CREATE TABLE IF NOT EXISTS` 추가만으로 idempotent. 기존 테이블 영향 없음.
|
||||||
|
|
||||||
|
## 6. analyzer.score_combination 시그니처 확장
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 기존
|
||||||
|
def score_combination(numbers, cache) -> Dict[str, float]:
|
||||||
|
...
|
||||||
|
return {
|
||||||
|
"score_frequency": ...,
|
||||||
|
"score_fingerprint": ...,
|
||||||
|
"score_gap": ...,
|
||||||
|
"score_cooccur": ...,
|
||||||
|
"score_diversity": ...,
|
||||||
|
"score_total": sum(5 scores) # 균등 합산
|
||||||
|
}
|
||||||
|
|
||||||
|
# 변경
|
||||||
|
def score_combination(numbers, cache, weights: Optional[List[float]] = None) -> Dict[str, float]:
|
||||||
|
...
|
||||||
|
scores = [s_freq, s_finger, s_gap, s_cooccur, s_diversity]
|
||||||
|
if weights is None:
|
||||||
|
total = sum(scores)
|
||||||
|
else:
|
||||||
|
total = sum(s * w for s, w in zip(scores, weights))
|
||||||
|
return {
|
||||||
|
"score_frequency": ...,
|
||||||
|
...
|
||||||
|
"score_total": total
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 기본값 None → 기존 호출 호환 (변경 없는 효과)
|
||||||
|
- 시뮬 cron / smart_recommendation 등은 `get_active_weight()` 결과 전달
|
||||||
|
- 활성 W가 없으면 (cold start 이전) None 그대로 → 균등 합산 폴백
|
||||||
|
|
||||||
|
### 6.1 활성 W 조회 (`get_active_weight()`)
|
||||||
|
|
||||||
|
별도 캐시 테이블 없이 `weight_trials`에서 오늘 요일 row 직접 조회:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_active_weight() -> Optional[List[float]]:
|
||||||
|
today = datetime.now(KST).date()
|
||||||
|
week_start = today - timedelta(days=today.weekday()) # 이번주 월요일
|
||||||
|
day_of_week = today.weekday() # 0=월, 6=일
|
||||||
|
if day_of_week == 6: # 일요일은 trial 없음 → 직전 토요일 W 유지
|
||||||
|
day_of_week = 5
|
||||||
|
row = db.get_weight_trial(week_start.isoformat(), day_of_week)
|
||||||
|
return json.loads(row["weight_json"]) if row else None
|
||||||
|
```
|
||||||
|
|
||||||
|
- 컨테이너 재시작·timezone 변화에 영향 없음 (DB 진실 기준)
|
||||||
|
- 일요일(6)은 토요일 W를 그대로 사용 (회고 cron 22:00 전까지)
|
||||||
|
- 첫 주 월요일 generate가 안 끝났을 때만 None 반환 → 균등 폴백
|
||||||
|
|
||||||
|
## 7. API 추가 (lotto-lab)
|
||||||
|
|
||||||
|
| 메서드 | 경로 | 설명 |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/api/lotto/evolver/status` | 현재 base + 이번주 6 trials + 진행 상황 |
|
||||||
|
| GET | `/api/lotto/evolver/history?weeks=12` | 주별 winner + base 변경 이력 |
|
||||||
|
| GET | `/api/lotto/evolver/trials/{week_start}` | 특정 주 trials + 채점 결과 |
|
||||||
|
| POST | `/api/lotto/evolver/generate-now` | 수동 트리거 (다음 월요일 후보 생성) |
|
||||||
|
| POST | `/api/lotto/evolver/evaluate-now` | 수동 채점 (디버그) |
|
||||||
|
|
||||||
|
## 8. 스케줄러 cron (lotto-lab)
|
||||||
|
|
||||||
|
```python
|
||||||
|
scheduler.add_job(generate_weekly_candidates, "cron", day_of_week="mon", hour=9, minute=0, id="weight_evolver_weekly")
|
||||||
|
scheduler.add_job(apply_today_weight, "cron", hour=9, minute=0, id="weight_evolver_daily")
|
||||||
|
scheduler.add_job(evaluate_weekly, "cron", day_of_week="sat", hour=22, minute=0, id="weight_evolver_eval")
|
||||||
|
```
|
||||||
|
|
||||||
|
순서 보장: 월요일 09:00에 generate가 먼저 row 저장 후 commit, 그 다음 같은 시각 apply가 그 row 읽음. APScheduler가 동일 시간 job 직렬 실행 보장하지 않으므로 **월요일에 generate 함수 마지막에 inline으로 apply_today_weight 호출** — race 제거.
|
||||||
|
|
||||||
|
## 9. agent-office 통합 (텔레그램 주간 보고)
|
||||||
|
|
||||||
|
### 9.1 cron 추가
|
||||||
|
|
||||||
|
```python
|
||||||
|
scheduler.add_job(_run_lotto_weekly_evolution_report, "cron", day_of_week="sat", hour=22, minute=15, id="lotto_evolution_weekly")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 LottoAgent.run_weekly_evolution_report (신규)
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def run_weekly_evolution_report(self) -> dict:
|
||||||
|
from ..service_proxy import lotto_evolver_status
|
||||||
|
from ..notifiers.telegram_lotto import send_evolution_report
|
||||||
|
status = await lotto_evolver_status()
|
||||||
|
await send_evolution_report(status)
|
||||||
|
return {"ok": True, **status}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.3 텔레그램 메시지 폼
|
||||||
|
|
||||||
|
```
|
||||||
|
🧬 로또 학습 주간 리포트 (1225회차)
|
||||||
|
|
||||||
|
이번주 시도: 6일 × 5세트 = 30번
|
||||||
|
|
||||||
|
🏆 Winner: 목요일 (W_4)
|
||||||
|
W = [freq 0.18, finger 0.32, gap 0.20, cooccur 0.22, divers 0.08]
|
||||||
|
최고 적중: 4개 일치 (1세트)
|
||||||
|
평균 점수: 0.42 (vs 다른 요일 0.18~0.30)
|
||||||
|
|
||||||
|
📊 다음주 base 변경:
|
||||||
|
freq 0.20 → 0.18 (-)
|
||||||
|
finger 0.20 → 0.32 (+)
|
||||||
|
gap 0.20 → 0.20 (=)
|
||||||
|
cooccur 0.20 → 0.22 (+)
|
||||||
|
divers 0.20 → 0.08 (--)
|
||||||
|
reason: winner_4plus (4개 이상 일치 → base 교체)
|
||||||
|
|
||||||
|
[웹에서 차트 보기] (/lotto/evolver)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. v1 시그널과의 연동 (자동 cascade)
|
||||||
|
|
||||||
|
별도 코드 추가 없음. 활성 W가 `analyzer.score_combination`에 반영되면:
|
||||||
|
1. 매 4시간 시뮬 cron이 새 W로 best_picks 갱신
|
||||||
|
2. score 분포 자체가 변하므로 v1의 `sim_consensus_score`가 자동으로 새 분포 평가
|
||||||
|
3. W 변경 직후 outlier 패턴이 나오면 자연스럽게 sim_signal urgent fire
|
||||||
|
|
||||||
|
→ 사용자는 두 종류 텔레그램 받음:
|
||||||
|
- **🧬 토 22:15 weekly evolution report** (정해진 리듬)
|
||||||
|
- **🚨 평시 v1 urgent / 📊 v1 digest** (시그널 기반)
|
||||||
|
|
||||||
|
## 11. 구현 Phase
|
||||||
|
|
||||||
|
| Phase | 범위 | 검증 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | DB 마이그레이션 + `weight_evolver.py` (순수 함수: generate/evaluate + 점수 함수) + 단위 테스트 | pytest로 perturbation·Dirichlet·점수·base 갱신 룰 검증 |
|
||||||
|
| 2 | analyzer.score_combination 시그니처 확장 + active weight 캐시 | 기존 시뮬 cron이 새 시그니처로 정상 동작 (regression X) |
|
||||||
|
| 3 | cron 3종 등록 + API 5종 | 수동 트리거로 generate→apply→evaluate 전체 흐름 확인 |
|
||||||
|
| 4 | agent-office 통합 (cron + 텔레그램 폼 + 테스트) | 토요일 22:15 자동 발송 확인 |
|
||||||
|
|
||||||
|
각 Phase 끝 commit + 자동 배포.
|
||||||
|
|
||||||
|
## 12. 비기능 요구
|
||||||
|
|
||||||
|
- **백워드 호환**: `analyzer.score_combination` 기본값 None → 기존 호출 그대로 작동
|
||||||
|
- **장애 격리**: 가중치 적용 실패 시 균등 합산 폴백, evaluate 실패해도 다음 주 base는 직전 값 유지
|
||||||
|
- **테스트**:
|
||||||
|
- `weight_evolver` 순수 함수 (clamp, normalize, perturbation, base update rule) — 단위 테스트
|
||||||
|
- `analyzer.score_combination(weights=...)` — 가중 합산 정확성 테스트
|
||||||
|
- `evaluate_weekly` mock 추첨번호 시나리오 — base 갱신 분기 3가지 (winner_4plus / ema_blend / unchanged)
|
||||||
|
- **관측**: `weight_base_history` 테이블로 모든 base 변경 추적 가능 (rollback도 가능)
|
||||||
|
|
||||||
|
## 13. 비목표 (Out of scope)
|
||||||
|
|
||||||
|
- 메타 전략(combined/simulation/heatmap/manual/custom) 가중치 학습 — strategy_evolver 영역, v3 후속
|
||||||
|
- 6일 trials의 day-transition에서 이전 W로 계산된 best_picks를 새 W로 재계산하는 처리 — 다음 시뮬 cron에서 자동 덮어씀
|
||||||
|
- Multi-objective 학습 (적중 + 분포 균등 등 복합 점수)
|
||||||
|
- 자동 구매 (purchase_history 자동 채움)
|
||||||
|
- 프론트 `/lotto/evolver` UI — v2 백엔드 완성 후 별도 PR (web-ui repo)
|
||||||
|
|
||||||
|
## 14. v3 후속 검토
|
||||||
|
|
||||||
|
- Multi-armed bandit (UCB1) — 탐험·활용 균형 더 정교
|
||||||
|
- 메타 전략 가중치도 함께 학습 (2-layer Bayesian Optimization)
|
||||||
|
- 가중치 공간을 RL agent로 학습 (policy gradient)
|
||||||
|
- 자동 구매 후보 픽 (winner W로 1주 N장 자동 발주, 사람 승인 후)
|
||||||
Reference in New Issue
Block a user