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>
13 KiB
로또 자가학습 백테스트 & 캘리브레이션 — 설계 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개를 사는 것이다.
이 사실을 부정하지 않고 시스템에 내장한다. 본 프로젝트의 가치는 "예측"이 아니라:
- 정직한 측정 — "내 분석 엔진이 무작위를 이기는가?"를 null-model 대조군으로 매번 엄밀히 검정.
- 자가학습 엔진 인프라 — 측정→학습→회고 루프 자체의 엔지니어링.
- 커버리지 최적화 — 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_runsSUM 집계로 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회):
POST /backtest/backfill→ 백그라운드 청크(50회차/배치, 멱등 재개). 이후 회차마다 증분. - 주간 forward: 당첨번호 동기화 직후
run_forward_purchase(latest). 참고: 6 W × 20k 풀은 기존 시뮬이 하루 6회 돌리는 부하보다 가벼움 → NAS 부담 작음. - 일 09:00 회고 (agent-office 신규 cron):
LottoAgent.run_sunday_review()→ forward+calibration 보장 →GET /backtest/review/{latest}→ 텔레그램 1통. - 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_calibrationUNIQUE(draw_no),backtest_runsUNIQUE(draw_no,strategy,weight_label) → 재실행 skip. - NAS 성능: 주간 forward는 기존 시뮬보다 가벼움. 백필만 1회 무거움(≈1100 point-in-time 캐시 재구성) → 청크+백그라운드+멱등 재개. 야간/유휴 트리거 권장.
- 텔레그램 실패: 로그만 남기고 job은 성공 처리(기존 패턴). 회고 데이터는 이미 DB에 있어 UI는 영향 없음.
8. 테스트 전략
- 등수 매핑(m3
m6/bonus → 15등) 단위 테스트. - 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)
- 3종 중 로또 먼저, 주식·인스타는 후속 사이클.
- 회고 브리핑 = 토 추첨 직후 일 09:00.
- 시도 규모 = 수천 장/회차 + 집계만 저장.
- 자율성 = 가중치 자동튜닝 강화(산식 구조 고정).
- 백테스트 범위 = 캘리브레이션 전체 백필 + 가상구매 forward.
- 출력 = 텔레그램 + 기존 자율학습 탭 확장.
- 프레이밍 = 정직한 측정(null-model) + 커버리지 최적.
- 연산 위치 = NAS-first, 필요시 Windows WSL 이전.
11. 스코프 밖 / 향후
- 주식 에이전트(보유종목 집중 분석+차트 매수/매도 시그널), 인스타 에이전트(자율 카드 발급) — 별도 사이클.
- 휠링/커버링 디자인(하위 등수 최소 보장) — coverage 전략 고도화로 향후.
- Windows WSL 워커 분리 — NAS 부하 측정 후.