fix(backtest): 22.8% 쓰레기스킵 → 5.2% (bt_candle_source 로더 통일)

## 근본원인
bt_candle_source._load_ticks_by_code_bulk / _ticks_for_bar_garbage 가
자체 SELECT 로 recv_ts/tick_time_raw 미포함 → bar_is_garbage 가
_lag_sec/recv_ts 없이 봉끝 폴백 → WS 봉의 76% 대량 컷.

엔진 로더(breakout_tick_loader.load_common_ticks_by_code) 는 이미
recv_ts + _lag_sec 완비였지만 두 로더가 발산 상태 → 이번에 사고 촉발.

## 수정 (C안: 로더 통일)
- _load_ticks_by_code_bulk: breakout_tick_loader._fetch_ws_ticks_day_rows
  위임. flat 리턴은 _ingest_flat_tick_rows (신설) 로 어댑팅.
  recv_ts + _lag_sec 모두 포함 → 엔진 로더와 100% 동일 SELECT/계산.
- _ticks_for_bar_garbage: 단일 종목용으로 _load_ticks_by_code_bulk
  (codes_filter=[code]) 재사용. 별도 SELECT 제거.

## 검증
Optuna scalp TPE 10 trials · 2026-09-04 · Python 엔진:
- Before: 슬롯 63671 · 쓰레기스킵 14495 (22.8%) · hole 7.4% · kis/ws pick 11%
- After:  슬롯 63671 · 쓰레기스킵  3324 ( 5.2%) · hole 3.7% · kis/ws pick 20%

실매 스모크 (scripts/test_live_execution_validation.py): 최종 통과 · 완결
로그: logs/test_live_execution_validation_20260906_201535.log

## 문서 (rust_engine_parity_port_plan.md)
- 부록 A: A6~A11 사전 정지 작업 추가 (wall-clock 정합·로더 통일·파케이·게이팅·뱃지)
- 부록 C (신설): D안 로드맵 — Rust 세션이 현재 봉만 받고 틱/호가/프로그램매매 미전달.
  Python 정합 개선을 받아도 Rust 는 여전히 데이터 절반짜리 상태. P0 최우선 6단계.

## 룰
- CRITICAL §0 (봉 freeze · OHLC 폴백 금지) 준수
- 룰 19 (근본원인 딥다이브 · 3안 선보고) — B/C/D 안 제시 후 사용자 C안 선택
- 룰 25-1 (교차검증) — 두 로더 발산 재발 방지

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Your Name
2026-09-06 20:18:11 +09:00
parent 152bf40a4c
commit d8ac1595c1
2 changed files with 178 additions and 61 deletions

View File

@@ -400,6 +400,12 @@ python3 -u scripts/test_live_execution_validation.py
| A3 | reaper 훅 — mode_refine 완료 시 phase1/2 JSON 자동 `register_result_json_as_job` (결과 잡아이디 UI 원복) | `kis_trader/backtest/optuna_web_jobs.py` |
| A4 | 옵투나 잡 삭제 API/UI 신설 (`/api/optuna/delete/<job_id>`, `/api/optuna/delete_bulk`, JS 🗑 버튼) | `optuna_web_jobs.py`, `backtest_web.py`, `static/js/backtest.js` |
| A5 | whipsaw 표준 키 통일 (`whipsaw_enabled` 옛 alias 완전 제거 → `whipsaw_filter_enabled` 단일) | 6개 파일 (룰 28) |
| A6 | `bar_is_garbage` wall-clock(recv_ts) 정합 + `bt_candle_source` 로더 통일 (breakout_tick_loader 위임 · 22.8% → 5.2%) | `bt_candle_source.py`, `feed_fallback.py`, `breakout_tick_loader.py` |
| A7 | 파케이 초고속 로드 게이트 해제 (`BACKTEST_USE_RUST` 무관 항상 사용) — I/O 최적화이지 엔진 계산 아님 | `breakout_tick_loader.py:277-282` |
| A8 | 4전략 Optuna 파일에서 Rust 세션 초기화·`_rust_session_id` 세팅을 `BACKTEST_USE_RUST==1` 로 게이팅 (Python 라디오 = 진짜 Python) | `param_search_optuna.py`, `optuna_scalping.py`, `optuna_breakout.py`, `optuna_momentum.py` |
| A9 | `optuna_web_jobs.py` 의 `BACKTEST_USE_RUST` 세팅 반전 로직 정정 (`use_rust=True → "1"`, False → "0" 명시) | `optuna_web_jobs.py:3763` |
| A10 | `annotate_optuna_period_daily_avg` 가 결과 JSON 에 `use_rust` 자동 기록 → auto-register 뱃지 ❔ 재발 방지 | `optuna_common.py:112` |
| A11 | `KIWOOM_TICK_LIVE_MAX_LAG_SEC` 폐기 → 3벤더 공통 `WS_TICK_DB_SAVE_LAG_CUT_ENABLED` 스위치 (기본 OFF=전부 저장, 통계 유지) | `kiwoom_ws.py`, `kis_ws.py`, `ls_ws.py`, `database.py` |
## 부록 B: 발견된 Rust 정합 이슈 12건 (우선순위)
@@ -420,4 +426,59 @@ python3 -u scripts/test_live_execution_validation.py
---
**끝.** 이 문서 기준으로 새 대화에서 P0 부터 착수.
## 부록 C: D안 — Rust 엔진 데이터 절반짜리 상태 (2026-09-06 발견 · P0 최우선)
### C.1 사실 (실측)
**Rust 세션 초기화** (`kis_rust_core.init_backtest_session_json(session_id, candles_json)`) 는 **candles(봉) 만** 넘긴다.
`run_engine_trial_scalp/breakout/momentum/tail(session, params)` 도 신규 데이터 없이 파라미터만 받는다.
즉 **현재 Rust 엔진은 데이터 절반짜리**:
| 데이터 | Python 엔진 (`engine_params`) | Rust 세션 |
|-------|:---:|:---:|
| 봉 (`codes_candles`) | ✅ | ✅ |
| 틱 (`ticks_by_code`) | ✅ | ❌ **미전달** |
| 호가 (`_bt_orderbook_by_code`) | ✅ | ❌ **미전달** |
| 프로그램매매 (`_bt_program_by_code`) | ✅ | ❌ **미전달** |
- 확인 위치: `optuna_scalping.py:428-435`, `optuna_breakout.py:428-435`, `optuna_momentum.py:492-500`, `param_search_optuna.py:518-525`
- Rust 소비처: `scalping_backtest_common.py:337`, `breakout_backtest_common.py`, `momentum_backtest_common.py`, `tail_backtest_common.py` 모두 `if rust_session:` 분기에서 params 만 넘김
### C.2 의미
Python 엔진이 정합 개선(A6~A11) 을 받아도, **Rust 세션은 여전히 봉만 보고 판정** → 다음이 그대로 갭:
1. **틱 청산** (WS 체결가·max_price 갱신·트레일 스톱) → Rust 는 종가 OHLC 기반 근사
2. **호가 필터** (진입/exit/stop 3구간) → Rust 는 무필터 (post_filter 만 있음)
3. **프로그램매매 필터** → Rust 완전 없음
4. **whipsaw 필터 (진입 시점)** → Rust 는 진입 이후 post_filter (실제 진입 자체를 못 막음)
이 상태에서 Optuna Rust 결과를 실매에 적용하면 **실매는 위 4개 필터로 진입 자체가 안 되는 후보** 가 Rust 상 최적화 대상이 되어 랭킹 왜곡.
### C.3 P0 로드맵 (D안 · 사용자 승인 시 착수)
| 단계 | 파일 | 작업 |
|-----|-----|------|
| D-1 | `kis_rust_src/src/session_manager.rs` | `Session` 구조체에 `ticks: HashMap<String, HashMap<String, Vec<Tick>>>` (code→minute→ticks), `orderbook_snaps: HashMap<String, Vec<OBSnap>>`, `program_ticks: HashMap<String, Vec<PgTick>>` 필드 추가 |
| D-2 | `kis_rust_src/src/lib.rs` | `init_backtest_session_json` 시그니처 확장: `(session_id, candles_json, ticks_json, orderbook_json, program_json)`. 옛 시그니처는 wrapper 로 유지 (하위호환) |
| D-3 | `kis_rust_src/src/{scalp,breakout,momentum,tail}.rs` | `try_sell_on_ticks` Rust 포팅 (Python `resolve_backtest_sell` 미러). 봉 high 로 max_price 선반영 금지 (룰 §부록 B #3) |
| D-4 | `kis_rust_src/src/filters.rs` (신설) | `orderbook_reject_for_entry`, `whipsaw_filter_hit`, `program_filter_reject` Rust 포팅. Python 과 1원 단위 일치 golden test |
| D-5 | 4전략 Optuna 파일 | `init_backtest_session_json` 호출 시 `ticks_by_code`, `orderbook_map`, `program_map` 모두 JSON 직렬화해서 전달 |
| D-6 | golden test (`scripts/golden_run.py`) | Rust ON + Python OFF 결과 diff = 0 (같은 필터·같은 데이터에서) |
### C.4 리스크
- Rust 크레이트 대공사 (`session_manager.rs` 필드 대량 추가 · 새 소비 로직)
- `ticks_by_code` 전체 JSON 직렬화 → 200종목 1일치 ~100MB. **JSON 대신 shared memory 또는 Arrow IPC 고려 필요** (P0-6 서브태스크)
- 이번 세션에서 착수하지 않음. 이 문서는 **P0 최우선 항목 (부록 B #3 대체·확장)** 으로 다음 세션 시작점
### C.5 임시 대응 (D안 완료 전까지)
- Optuna 실행은 **Python 라디오 기본** 유지 (A8 로 이미 가능)
- Rust 는 봉만 필요한 단순 백테·golden 진행에만 사용
- Optuna best 를 실매 적용 전 반드시 **웹 백테(Python) 로 재검증** (룰 15)
---
**끝.** 이 문서 기준으로 새 대화에서 **부록 C P0 (D안 · 데이터 완전 전달)** 부터 착수.