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

13 KiB
Raw Blame History

로또 자가학습 백테스트 & 캘리브레이션 — 설계 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_pickrecommend_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) -> dictdraws[: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. 테스트 전략

  • 등수 매핑(m3m6/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)

  1. 3종 중 로또 먼저, 주식·인스타는 후속 사이클.
  2. 회고 브리핑 = 토 추첨 직후 일 09:00.
  3. 시도 규모 = 수천 장/회차 + 집계만 저장.
  4. 자율성 = 가중치 자동튜닝 강화(산식 구조 고정).
  5. 백테스트 범위 = 캘리브레이션 전체 백필 + 가상구매 forward.
  6. 출력 = 텔레그램 + 기존 자율학습 탭 확장.
  7. 프레이밍 = 정직한 측정(null-model) + 커버리지 최적.
  8. 연산 위치 = NAS-first, 필요시 Windows WSL 이전.

11. 스코프 밖 / 향후

  • 주식 에이전트(보유종목 집중 분석+차트 매수/매도 시그널), 인스타 에이전트(자율 카드 발급) — 별도 사이클.
  • 휠링/커버링 디자인(하위 등수 최소 보장) — coverage 전략 고도화로 향후.
  • Windows WSL 워커 분리 — NAS 부하 측정 후.