diff --git a/docs/superpowers/specs/2026-05-31-lotto-self-learning-backtest-design.md b/docs/superpowers/specs/2026-05-31-lotto-self-learning-backtest-design.md new file mode 100644 index 0000000..0459b79 --- /dev/null +++ b/docs/superpowers/specs/2026-05-31-lotto-self-learning-backtest-design.md @@ -0,0 +1,191 @@ +# 로또 자가학습 백테스트 & 캘리브레이션 — 설계 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등=m5−bonus_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 부하 측정 후.