Files
web-page-backend/docs/superpowers/specs/2026-05-31-lotto-self-learning-backtest-design.md
gahusb f3f6cccd33 docs(spec): 로또 자가학습 백테스트 & 캘리브레이션 설계
3종 스마트 에이전트 고도화 중 로또 1번. forward 가상구매(수천 장/회차)
+ winner 캘리브레이션(역대 백필) + 일요 회고 브리핑 + weight_evolver
학습 신호 강화(W-무관 결함 수정). null-model 베이스라인 내장으로
무작위 대비 우위를 정직하게 측정. NAS-first, Windows WSL 이전 가능 설계.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 16:37:25 +09:00

192 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 로또 자가학습 백테스트 & 캘리브레이션 — 설계 Spec
- **작성일**: 2026-05-31
- **상태**: 설계 승인 (구현 plan 대기)
- **대상 서비스**: `lotto` (lotto-lab) + `agent-office` (LottoAgent) + `web-ui` (/lotto 자율학습 탭)
- **사이클**: 스마트 에이전트 고도화 3종(로또/주식/인스타) 중 **1번 로또**. 주식·인스타는 후속 사이클.
---
## 1. 배경 & 목표
사용자(CEO)는 로또 에이전트를 "분석 번호를 계속 가상구매해 시도횟수를 늘리고, 실제 당첨조합을 역분석해 스스로 학습·디벨롭하며 일요일에 회고 브리핑하는 스마트 에이전트"로 고도화하길 원한다. 명시 목표는 "로또 1등".
### ⚠️ 정직성 전제 (설계의 토대)
로또는 매 회차 균등·독립 추첨이다. C(45,6)=8,145,060 조합이 전부 동일 확률이며 회차 간 독립이다. 따라서:
- **과거 데이터(빈도·갭·공동출현)의 미래 예측력은 수학적으로 0.** 통계 분석으로 1등 확률을 올릴 수 없다.
- 고정 예산 N장으로 1등 확률을 최대화하는 유일한 방법은 **서로 다른(distinct) 조합 N개**를 사는 것이다.
이 사실을 부정하지 않고 **시스템에 내장**한다. 본 프로젝트의 가치는 "예측"이 아니라:
1. **정직한 측정** — "내 분석 엔진이 무작위를 이기는가?"를 null-model 대조군으로 매번 엄밀히 검정.
2. **자가학습 엔진 인프라** — 측정→학습→회고 루프 자체의 엔지니어링.
3. **커버리지 최적화** — 1등이 목표라면 distinct 조합 커버리지 최대화가 수학적 최적.
→ 사용자 결정(2026-05-31): **"정직한 측정 + 커버리지 최적"** 프레이밍 채택. 패턴 학습은 계속하되 모든 백테스트에 null-model 베이스라인을 내장한다.
### 기존 자산 (100% 재활용, 신규 ML 없음)
- `analyzer.build_analysis_cache(draws)` / `score_combination(numbers, cache, weights)` — 임의 조합의 5개 sub-score + 종합점수(0~1) = **"분석치"**.
- `analyzer.build_number_weights` + `utils.weighted_sample_6` — 가중 후보 생성.
- `generator.run_simulation` — 20k 후보를 `score_combination(·, active_weights)`로 랭킹→best_picks. **W가 선택을 바꾸는 경로가 이미 존재.**
- `weight_evolver` — 토 22:00 주간 6 가중치 후보 채점→base 갱신.
### 발견된 잠재 결함 (본 작업으로 수정)
`weight_evolver.apply_today_and_pick``recommend_numbers(draws)`(W 미사용)로 픽을 뽑은 뒤 W로 점수만 매긴다. 즉 **현재 daily 픽은 W와 무관**하고, evolver가 평가하는 매칭 결과도 W-독립이라 가중치 진화가 픽 품질에 연결돼 있지 않다. → forward 가상구매를 **시뮬레이션 선택 경로(풀 생성→W 랭킹→상위 K 구매)**로 구현하면 W가 결과를 실제로 바꿔 가중치 학습이 비로소 의미를 갖고 이 결함도 해소된다.
---
## 2. 핵심 개념 — Self-Learning Backtest Loop
세 축으로 구성:
### 축 A — Forward 가상구매 (매주, 회차당 수천 장)
매 회차 추첨 후, 각 전략별로 대량 후보를 생성·랭킹해 상위 K장을 "구매"로 간주 → 실제 당첨번호로 채점 → **회차별 집계 1행만 영구 저장**. 개별 티켓 미저장.
- 전략: `engine_w`(6개 trial 가중치 각각) / `random_null`(무작위 대조군) / `coverage`(distinct 최대화).
- 이 매칭 결과가 evolver의 학습 신호가 된다.
### 축 B — Winner 캘리브레이션 (역대 전체 백필 + 매주 증분)
각 회차의 **실제 당첨조합을 그 시점 이전 데이터로 만든 캐시(point-in-time)에 넣어** 5개 분석치 + 종합점수 + percentile을 기록.
- percentile = 당첨조합 score_total이 그 시점 무작위 M개 표본 분포에서 차지하는 위치.
- "내 엔진이 실제 당첨번호에 높은 점수를 주는가?"의 가장 정직한 신호. 당첨조합이 일관되게 낮은 percentile이면 엔진은 헛다리.
### 축 C — 일요일 회고 브리핑
토 추첨(20:45)→동기화(21:10)→기존 evolver 리포트(토 22:15) 이후, **일 09:00**에 차분히 회고. 이번 회차 forward 성적 + 당첨조합 역분석 + 내 추천과 비교 + 캘리브레이션 추세 + 가중치 진화를 텔레그램 1통 + UI.
---
## 3. 데이터 모델 (lotto.db 신규)
집계 전용 — row 수 ≈ 회차 × 전략 (수천 규모, 무시 가능).
### `backtest_runs` — forward 가상구매 집계
```
id INTEGER PK
draw_no INTEGER NOT NULL -- 채점 대상(당첨 확정된) 회차
strategy TEXT NOT NULL -- 'engine_w' | 'random_null' | 'coverage'
weight_label TEXT NOT NULL -- engine_w는 trial day_of_week('w0'..'w5'), 그 외 '-'
weight_json TEXT -- 사용한 W (random/coverage는 NULL)
trial_id INTEGER -- FK weight_trials (engine_w만, nullable)
n_tickets INTEGER NOT NULL -- 구매(채점) 장수
m3 INTEGER NOT NULL DEFAULT 0 -- 3개 일치 장수
m4 INTEGER NOT NULL DEFAULT 0
m5 INTEGER NOT NULL DEFAULT 0
m6 INTEGER NOT NULL DEFAULT 0
bonus_hits INTEGER NOT NULL DEFAULT 0 -- 5+보너스(2등) 장수
best_match INTEGER NOT NULL DEFAULT 0
avg_meta_score REAL -- 구매 티켓 평균 분석치
created_at TEXT NOT NULL
UNIQUE(draw_no, strategy, weight_label) -- 멱등
```
- 등수 매핑: 1등=m6, 2등=bonus_hits, 3등=m5bonus_hits, 4등=m4, 5등=m3.
### `winner_calibration` — 회차별 당첨조합 역분석
```
draw_no INTEGER PK -- 멱등
winning_json TEXT NOT NULL -- [n1..n6] (보너스 별도 보관 안 함)
score_total REAL NOT NULL
score_frequency REAL NOT NULL
score_fingerprint REAL NOT NULL
score_gap REAL NOT NULL
score_cooccur REAL NOT NULL
score_diversity REAL NOT NULL
percentile REAL -- 0~1, 무작위 M표본 대비 당첨조합 점수 위치
my_pick_avg REAL -- 그 회차 engine 추천 평균 분석치(있으면)
cache_draws INTEGER NOT NULL -- point-in-time 캐시에 쓰인 회차 수
created_at TEXT NOT NULL
```
> 누적 성적표(track record)는 `backtest_runs` SUM 집계로 on-the-fly 계산 — 별도 테이블 불필요.
---
## 4. 컴포넌트
### 4.1 lotto-lab `app/backtest.py` (순수 연산 — FastAPI 의존성 0, Windows 이전 대비)
- `generate_pool(cache, number_weights, n) -> list[tuple]``weighted_sample_6` 반복으로 distinct 후보 풀.
- `purchase_tickets(pool, cache, W, k) -> list[dict]` — 풀을 `score_combination(·, W)`로 랭킹→상위 k장 distinct.
- `coverage_select(pool, k) -> list` — distinct 보장 상위 커버리지(초기엔 단순 distinct, 휠링은 향후).
- `grade_tickets(tickets, winning6, bonus) -> dict` — 매칭 히스토그램 + 등수 카운트 + best_match + avg_meta. `bonus`는 draws 레코드에서 가져옴(2등=5일치+보너스 판정용).
- `run_forward_purchase(draw_no, k=5000, pool_n=20000) -> dict` — engine(6 W)+random_null+coverage 각각 **전략당 k=5000장(수천 장)** 구매·채점·`backtest_runs` 저장(멱등). 풀 pool_n=20000에서 랭킹.
- `calibrate_winner(draw_no, sample_m=2000) -> dict``draws[:idx]`(대상 회차 제외) 캐시로 당첨조합 채점 + 무작위 sample_m 표본 percentile → `winner_calibration` 저장(멱등).
- `backfill_calibration(batch=50) -> dict` — 미처리 회차만 청크 처리, 재개 가능.
- `build_review_payload(draw_no) -> dict` — 회고 브리핑용 조립(당첨조합 분해 + 내 추천 비교 + forward 성적 + 캘리브레이션 추세 + 진화 결과).
### 4.2 lotto-lab `app/routers/backtest.py`
| 메서드 | 경로 | 설명 |
|--------|------|------|
| GET | `/api/lotto/backtest/track-record` | 누적 성적표(전략별 등수 카운트, engine vs random) |
| GET | `/api/lotto/backtest/calibration?weeks=N` | 캘리브레이션 이력 + 추세 |
| GET | `/api/lotto/backtest/review/{draw_no}` | 회고 payload |
| POST | `/api/lotto/backtest/run-forward?draw_no=` | forward 수동 트리거 |
| POST | `/api/lotto/backtest/backfill` | 캘리브레이션 백필(백그라운드) |
### 4.3 weight_evolver 업그레이드
- `evaluate_weekly`: 학습 신호를 N=5(W-무관)에서 **forward 가상구매(engine_w 6전략) + null-model 대비 lift**로 승격.
- lift = engine_w 등수 점수 random_null 등수 점수(동일 회차).
- 승자 = lift 최대 trial. **모든 W의 lift가 노이즈 범위(±ε) 내면 base `unchanged`** → 노이즈 과적합 방지.
- `decide_base_update` 규칙은 유지하되 입력(winner)을 backtest 기반으로 교체.
- 기존 `auto_picks` 경로는 하위호환·일일 활동표시용으로 유지(evolver 결정에는 미사용).
---
## 5. 플로우
1. **캘리브레이션 백필 (1회)**: `POST /backtest/backfill` → 백그라운드 청크(50회차/배치, 멱등 재개). 이후 회차마다 증분.
2. **주간 forward**: 당첨번호 동기화 직후 `run_forward_purchase(latest)`. 참고: 6 W × 20k 풀은 기존 시뮬이 **하루 6회** 돌리는 부하보다 가벼움 → NAS 부담 작음.
3. **일 09:00 회고 (agent-office 신규 cron)**: `LottoAgent.run_sunday_review()` → forward+calibration 보장 → `GET /backtest/review/{latest}` → 텔레그램 1통.
4. **evolver (토 22:00, 기존 cron)**: backtest 집계를 학습 신호로 소비.
### Windows 이전 경로 (NAS 부하 측정 후 필요시)
`backtest.py`가 순수 함수라, lotto-lab은 system-of-record 유지 + 무거운 연산만 Windows WSL docker 워커에 위임(`/api/internal/lotto/*` webhook, 기존 music/video/image 워커 패턴 재활용) + agent 폴링. 코드 경계가 깨끗해 마이그레이션 비용 최소. **초기 구현은 NAS-first**, 측정 후 결정.
---
## 6. 출력
### 6.1 텔레그램 (일 09:00, `notifiers/telegram_lotto.py` 신규 섹션)
이번 당첨조합 5분석치 분해 + 내 추천 평균과 비교 + 이번주 forward 성적(등수 카운트, **무작위 대비 lift**) + 캘리브레이션 percentile 추세 + 가중치 진화 결과.
### 6.2 web-ui `/lotto` "자율 학습" 탭 확장 (`.lotto-evolver-*` 다크 네임스페이스 재활용)
- **TrackRecordCard**: 누적 "매주 전략당 5,000장 샀다면" 등수 — engine vs random_null 나란히 + 총지출 대비 당첨금(정직하게 적자 표시).
- **CalibrationChart**: 당첨조합 score_total 추세 + 내 추천 평균 오버레이 + percentile 밴드 → "우위 없음"을 시각화.
- **WinnerAnalysisCard**: 이번 회차 당첨조합 5분석치 레이더 + 내 추천 비교.
---
## 7. 에러·성능·멱등
- **멱등성**: `winner_calibration` UNIQUE(draw_no), `backtest_runs` UNIQUE(draw_no,strategy,weight_label) → 재실행 skip.
- **NAS 성능**: 주간 forward는 기존 시뮬보다 가벼움. 백필만 1회 무거움(≈1100 point-in-time 캐시 재구성) → 청크+백그라운드+멱등 재개. 야간/유휴 트리거 권장.
- **텔레그램 실패**: 로그만 남기고 job은 성공 처리(기존 패턴). 회고 데이터는 이미 DB에 있어 UI는 영향 없음.
## 8. 테스트 전략
- 등수 매핑(m3~m6/bonus → 1~5등) 단위 테스트.
- null-model 기대값 + lift 계산.
- percentile 계산 정확성.
- **point-in-time 캐시가 대상 회차를 제외하는지** (calibrate_winner 정직성 핵심).
- 멱등 백필(재실행 시 중복 row 없음, 중단 후 재개).
- evolver의 lift-over-random 승자 선택 + ε-게이팅(노이즈 시 unchanged).
- 기존 `count_match`/`calc_pick_score` 테스트 유지.
## 9. 리스크 & 완화
| 리스크 | 완화 |
|--------|------|
| 무작위성 → 실제 우위 없음 | null-model 정직 프레이밍, 우위 없음을 데이터로 보고하는 게 목표 |
| Celeron 백필 부하 | 청크+1회성+멱등 재개, 필요시 Windows 이전 |
| evolver 노이즈 추종 | lift-over-random + ε-게이팅으로 unchanged 처리 |
| DB 증가 | 집계 전용, row 수 무시 가능 |
| forward 풀 중복으로 커버리지 손실 | distinct 강제 + coverage 전략 별도 측정 |
## 10. 결정 로그 (2026-05-31 brainstorming)
1. 3종 중 **로또 먼저**, 주식·인스타는 후속 사이클.
2. 회고 브리핑 = **토 추첨 직후 일 09:00**.
3. 시도 규모 = **수천 장/회차 + 집계만 저장**.
4. 자율성 = **가중치 자동튜닝 강화**(산식 구조 고정).
5. 백테스트 범위 = **캘리브레이션 전체 백필 + 가상구매 forward**.
6. 출력 = **텔레그램 + 기존 자율학습 탭 확장**.
7. 프레이밍 = **정직한 측정(null-model) + 커버리지 최적**.
8. 연산 위치 = **NAS-first, 필요시 Windows WSL 이전**.
## 11. 스코프 밖 / 향후
- 주식 에이전트(보유종목 집중 분석+차트 매수/매도 시그널), 인스타 에이전트(자율 카드 발급) — 별도 사이클.
- 휠링/커버링 디자인(하위 등수 최소 보장) — coverage 전략 고도화로 향후.
- Windows WSL 워커 분리 — NAS 부하 측정 후.