# Python → Rust 엔진 완전 이식 설계안 (B안) > 작성일: 2026-09-06 > 대상: `kis_rust_src/` 백테 엔진 확장 (whipsaw · 호가 · 포트폴리오 · 틱 · 청산 우선순위) > 목표: **Optuna(Rust) 결과 = 웹 백테(Python) 결과 = 실매 엔진** 을 1원 단위로 정합 > 진행: **이 대화에서는 이 문서 작성까지만.** 실제 구현은 새 대화에서 이 문서 기준으로 진행. --- ## 0. 왜 필요한가 (배경) `param_search_optuna.py:44-48` 이 import 시점에 `BACKTEST_USE_RUST=1` 을 **강제 설정**하여, Optuna는 사실상 Rust 엔진으로만 돌아간다: ```44:48:/home/hoon/kis_bot/kis_trader/backtest/param_search_optuna.py os.environ["BACKTEST_CANDLE_GARBAGE_OFF"] = "1" os.environ["CANDLE_GARBAGE_FALLBACK"] = "False" # 파이썬 엔진 Parquet 틱 로더 활성화 os.environ["BACKTEST_USE_RUST"] = "1" ``` 문제: Rust 엔진은 실제로 **OHLC 단일종목·종목별 병렬 시뮬레이터** 수준. 아래 12개 항목이 Python 실매/백테와 다르므로 **Optuna 랭킹 ≠ 웹 백테 결과 ≠ 실매**. - 이로 인해 Optuna best 를 실매 DB에 적용하면 예상과 다른 엔진으로 매매 → 사고 위험. - CRITICAL §0 「봉 freeze / OHLC 폴백 금지 / skip_hts_scan_dupes false 유지」 룰 관점에서도 Rust는 위반 상태. --- ## 1. 목표 (성공 조건) | # | 조건 | 검증 | |---|---|---| | G1 | Rust ON + post_filters OFF 결과 = Python OFF 결과 (동일 candles/ticks) | golden test 1원 단위 diff = 0 | | G2 | Rust ON + whipsaw+orderbook ON = Python 동일 조건 결과 | Optuna best JSON 웹 백테 재실행 시 1원 일치 (룰 28) | | G3 | 실매 코어 스모크 통과 유지 | `scripts/test_live_execution_validation.py` | | G4 | 봉 freeze / skip_hts_scan_dupes=false / OHLC 폴백 OFF 절대 준수 | Rust 코드에 look-ahead 금지 + freeze 존중 | | G5 | 백테 웹 UI(파라미터·차트·거래내역) 무변경 유지 | 브라우저 검증 (룰 14) | --- ## 2. 현재 상태 인벤토리 (요약) ### 2.1 Rust 노출 함수 (`kis_rust_src/src/lib.rs`) | 종류 | 심볼 | 라인 | |---|---|---| | `#[pyfunction]` | `run_dummy_backtest` | 20–26 | | `#[pymodule]` | `kis_rust_core` | 29–60 | | 전략 | `run_tail_backtest_fast`, `run_engine_trial_tail` | 32–33 | | 전략 | `run_scalp_backtest_fast`, `run_engine_trial_scalp` | 35, 42 | | 전략 | `run_breakout_backtest_fast`, `run_engine_trial_breakout` | 36–37 | | 전략 | `run_momentum_backtest_fast`, `run_engine_trial_momentum` | 38–39 | | 전략 | `run_box_backtest_fast`, `run_engine_trial_box` | 40–41 (레거시) | | 세션 | `init_backtest_session`, `init_backtest_session_json`, `clear_backtest_session` | 43–45 | | 로더 | `db_loader::load_candles_from_db`, `parquet_loader::load_parquet_ticks_fast` | 34, 46 | **Cargo deps** (`Cargo.toml:10-19`): `pyo3 0.20 (extension-module)`, `serde`, `sqlx(mysql)`, `tokio`, `rayon`, `serde_json`, `polars(parquet,lazy)`, `chrono`, `lazy_static`. ### 2.2 Rust vs Python 기능 갭 (요약) | 기능 | Python | Rust | 이식 우선순위 | |---|---|---|---| | 포트폴리오 큐 (max_stocks / slot_money / 시각순 경쟁) | ✅ | ❌ | **P0** | | 틱 청산 (`resolve_backtest_sell` → `try_sell_on_ticks`) | ✅ | ❌ (SESSION 저장만) | **P0** | | T−1 신호 · T 시가 진입 (align) | ✅ | 부분 (tail trial만) | **P0** | | 봉 freeze / OHLC 폴백 금지 (틱 ON) | ✅ | ❌ (봉 high 선반영) | **P0** | | 청산 우선순위 (전략별 상이) | ✅ | ❌ (EOD·TP 우선) | **P1** | | 래칫 tiers | ✅ | ❌ | **P1** | | ATR 동적 SL (돌파) | ✅ | ❌ (고정%만) | **P1** | | `min_hold_sec` | ✅ | 필드만·미사용 | **P1** | | 금액손실컷 (KRW×qty) | ✅ | 근사 (drop_pct만) | **P1** | | whipsaw 필터 (진입 시점) | ✅ | ❌ (post_filter만) | **P2** | | 호가 필터 (진입 · exit · stop) | ✅ | ❌ (post_filter만) | **P2** | | 프로그램매매 필터 | ✅ | ❌ | **P2** | | Optuna 브릿지 키 정합 | 부분 | 3건 버그 | **P3** | --- ## 3. Python 실매/백테 엔진 청산 순서 (전략별 · 이식 목표) ### 3.1 꼬리 — `kis_trader/engine/tail_engine.py:1406-1518` | 순위 | 조건 | 라인 | params/env | |---|---|---|---| | gate | `min_hold_sec` | 1436-1444 | `min_hold_sec` | | 1 | 래칫 / 어깨 | 1452-1472 | `ratchet_tiers`, `TAIL_RATCHET_TIERS`, `shoulder_min_high`, `shoulder_cut_pct` | | 2 | 익절 (target=ATR 캡) | 1474-1477 | `target` | | 3 | 손절 | 1478-1481 | `stop` | | 4 | 트레일 | 1482-1493 | `trail_pct`, `trail_arm_pct` | | 5 | 금액손실컷 | 1494-1502 | `max_loss_krw`, `min_drop_pct_for_loss_cut`, `qty` | | 6 | 시간컷 | 1503-1510 | `TAIL_MAX_HOLD_BARS`(분) | | 7 | 장마감 | 1511-1514 | `is_eod` | 포트폴리오: `run_tail_backtest_portfolio` (`tail_engine.py:1799`). ### 3.2 모멘텀 — `kis_trader/engine/momentum_hts_logic.py:678-801` | 순위 | reason | 라인 | params | |---|---|---|---| | gate | min_hold | 717-723 | `min_hold_sec` | | 1 | 래칫컷 | 733-743 | `ratchet_tiers` | | 2 | 어깨컷 | 745-750 | `shoulder_min_high`, `shoulder_cut_pct` | | 3 | 호가컷 (exit_ob) | 752-757 | `_ob_or_history` | | 4 | 손절호가 (stop_ob) | 759-761 | 동상 | | 5 | 손절 | 763-765 | `sl_pct` | | 6 | 트레일컷 | 767-773 | `trail_pct`, `trail_arm_pct` | | 7 | 시간컷 | 775-779 | `max_hold_bars` | | 8 | 금액손실컷 | 781-791 | `max_loss_krw`, `min_drop_pct_for_loss_cut` | | 9 | 익절 | 793-795 | `tp_pct` | | 10 | 장마감 | 797-799 | `is_eod` | 포트폴리오: `momentum_portfolio_backtest.py:625`. ### 3.3 돌파 — `kis_trader/strategies/breakout.py:1217-1306` - ATR SL 함수: `_breakout_sl_line` (170-192) — `sl_mode='atr'` + `entry_atr` → `atr_sl_mult`, `atr_sl_min_pct`, `atr_sl_max_pct`. - 청산 순서: EOD → TP → shoulder/ratchet → 호가컷 → 손절호가 → SL(ATR or 고정) → trailing → max_hold. - 포트폴리오: `breakout_portfolio_backtest.py:135`. ### 3.4 스캘핑 — `kis_trader/engine/scalping_engine.py:1281-1360` | 순위 | reason | 라인 | params | |---|---|---|---| | gate | min_hold | 1319-1326 | `min_hold_sec` | | 1 | 어깨컷 | 1334-1340 | `shoulder_*` | | 2 | 익절 | 1341-1343 | `target` | | 3 | 손절 | 1344-1346 | `stop` | | 4 | 금액손실컷 | 1347-1353 | `max_loss_krw`, `min_drop_pct_for_loss_cut` | | 5 | 장마감 | 1354-1356 | `is_eod` | 백테 intrabar: `check_sell_signal_backtest_bar` (1220-1278) → `resolve_backtest_sell`. 포트폴리오: `scalping_portfolio_backtest.py:110`. --- ## 4. Rust 이식 로드맵 (P0 → P3) ### Phase P0 — 정합 붕괴 원인 (엔진 코어) **목표**: Rust ON 결과가 Python OFF (틱 청산 + 포트폴리오 켜진 상태)와 1원 일치. 1. **포트폴리오 시각순 시뮬** 신규 crate 모듈 `portfolio_engine` - Rust `HashMap>` → 시각순 이터레이션 - `max_stocks`, `total_budget_krw`, 1시각 1매수, `slot_money`, vol_fill_cap - 유니버스 timeline 은 Python에서 미리 계산해 Rust에 `Vec<(time, Vec)>` 전달 (Rust에서 DB 재조회 없음) - 참조: `tail_engine.py:1799-1946`, `backtest_portfolio_common.py:265-310` 2. **틱 청산 통합** - `session_manager.rs:BacktestSessionData.ticks_by_code` 를 실제로 소비 - `try_sell_on_ticks` Rust 포팅: tick 시간순 → max_price ← tick price, `sell_fn` 훅 - `use_tick_exit=true` 시 봉 OHLC intrabar 폴백 금지 (`tick_exit_common.py:357-369`) 3. **T−1 / T 진입 align 통일** - 모든 전략 trial 경로: `candles[i+1].open` 진입 (현재는 tail만 반영, 나머지는 same-bar close) - `live_backtest_align=True` 시나리오 재현 4. **봉 freeze 존중** - Rust 진입 시점에 봉이 “확정봉인지” 판정 (freeze ON이면 아직 진행 중인 봉의 high 사용 금지) - Python 백테는 과거 데이터라 이미 확정 상태이지만, tick 청산 시 max_price 업데이트 규칙은 “틱 우선, 봉 high 선반영 금지” **검증**: - 동일 candles + ticks + params 로 Python `run_*_backtest_portfolio` vs Rust `run_engine_trial_*_portfolio` 결과 diff = 0 (golden test). --- ### Phase P1 — 청산 우선순위 · 파라미터 완성 **목표**: 각 전략의 매도 판정이 Python 실매와 스텝 단위로 일치. 1. **청산 함수 1:1 포팅** (전략별) - 꼬리 V4: 위 §3.1 표 - 모멘텀 10단계: 위 §3.2 표 - 돌파: `_breakout_sl_line` 포함 (ATR SL) - 스캘핑: 어깨 + 금액손실컷 2. **`min_hold_sec` 게이트** (전 전략) - 매수 시각 대비 초 단위 hold 시간 미달이면 어떤 청산도 발동 안 함 3. **`max_loss_krw` × qty 금액손실컷** (Rust는 현재 `drop_pct` 근사) - qty 필요 → 포트폴리오 slot_money/entry 로 산출 4. **ATR 계산 활용** (`compute_atr` Wilder RMA · `breakout.rs:131-156`) - 현재는 `atr_entry` 저장만 → SL 라인 실제 사용 **검증**: 각 전략 5-10 종목·1일 골든 케이스로 청산 reason·price·time 일치. --- ### Phase P2 — TRIGGER 진입 필터 (whipsaw / 호가 / 프로그램) **목표**: 진입 시점에 whipsaw·호가·프로그램 필터가 걸리는 종목은 Rust도 스킵. 1. **whipsaw 필터 Rust 포팅** (`whipsaw_filter.py:449-533` 기준) - `_whipsaw_ticks` (Rust SESSION의 ticks 재사용) - `subbar_sec`, `lookback_sec`, `dip_pct` 오버레이 - tick_mode ON 시 OHLC 폴백 스킵 2. **호가 필터 Rust 포팅** (`orderbook_filter.py:79-103`) - `WS_ORDERBOOK_FILTER_MAX_AGE_SEC`(판정) vs `WS_ORDERBOOK_TICK_MAX_AGE_SEC`(저장) 분리 유지 - 호가 데이터는 Python에서 DB 로드해 Rust SESSION 에 `Vec` 전달 (Rust는 DB 안 침) - `_ob_ask_max_mult`, `_ob_bid_min_ratio` 3. **프로그램매매 필터** (`_program_filter_enabled`) 4. **`apply_bt_post_filters` 역할 재정의** - 현재는 Rust 진입 후 Python 사후 필터 (엔진 정합 아님) - P2 이후엔 Rust 엔진 내부에 이미 필터가 있으므로 post_filter 는 **선택적 검증 계층** 으로 격하 (기본 OFF or 회귀 테스트 전용) **검증**: 웹 백테(휩쏘 ON + 호가 ON) = Rust Optuna best 재실행 1원 일치. --- ### Phase P3 — Optuna 브릿지 · 표준 키 · 운영 1. **`param_search_optuna.py:48` 의 `BACKTEST_USE_RUST=1` 강제 제거** - Optuna 기본은 Python 유지. Rust ON 은 UI 토글 or CLI 명시 인자로만. - 이 항목은 P0 진입 전 **즉시 실행 가능** (안전조치). B안 이식이 완료되면 다시 Rust 기본으로 복귀 검토. 2. **브릿지 인자 3건 정정** - momentum `min_hold_sec` 누락 (`momentum_backtest_common.py:620-653`) - scalp 브릿지 `skip_hts` vs Python `skip_hts_scan_dupes` 키명 통일 (`scalping_backtest_common.py:366`) - tail Rust 브릿지 default `skip_hts_scan_dupes=True` → **False** (`tail_backtest_common.py:822`) 3. **표준 키 전량 Rust Params 확장** (2026-09-06 통일 키) - `whipsaw_filter_enabled`, `whipsaw_subbar_sec`, `whipsaw_lookback_sec`, `whipsaw_dip_pct` - `_orderbook_filter_enabled`, `_ob_ask_max_mult`, `_ob_bid_min_ratio` - `min_hold_sec`, `max_loss_krw`, `sl_mode`, `atr_*`, `ratchet_tiers` - `portfolio_mode`, `max_stocks`, `slot_money`, `total_budget_krw` 4. **빌드 · 배포 스크립트 문서화** - 표준 절차: ```bash cd /home/hoon/kis_bot/kis_rust_src cargo build --release cp target/release/libkis_rust_core.so ../kis_rust_core.so # venv .so 교체 (site-packages/kis_rust_core/) ``` - CI 훅 (선택): `cargo build --release` + `python -c "import kis_rust_core"` 스모크 --- ## 5. 각 전략별 이식 체크리스트 ### 5.1 꼬리 (tail) - [ ] TailParams 필드 확장 (`min_hold_sec`, `ratchet_tiers`, whipsaw/호가 표준 키) - [ ] 청산 순서 V4 (§3.1 표 그대로) - [ ] `_universe_codes_at` 로 유니버스 시각별 필터 - [ ] 브릿지 `rsi_threshold` ↔ Rust `rsi_limit` 매핑 통일 - [ ] `skip_hts_scan_dupes` 기본 False - [ ] entry_mode: align (T-1 신호 / T 시가), limit_atr, tick 진입 ### 5.2 모멘텀 (momentum) - [ ] MomentumParams 필드 확장 (`min_hold_sec` 전달·사용, EMA, 호가/whipsaw) - [ ] 10단계 청산 (§3.2 표) - [ ] E confirm · vol pulse · high chase · RSI · EMA 진입 로직 - [ ] intrabar `BACKTEST_EXIT_CHECKS_PER_BAR` 대응 ### 5.3 돌파 (breakout) - [ ] BreakoutParams 확장 (`sl_mode`, `atr_sl_mult`, `atr_sl_min_pct`, `atr_sl_max_pct`) - [ ] `_breakout_sl_line` Rust 포팅 - [ ] `shoulder_min_high` 를 trail_armed 로 오용한 부분(breakout.rs:227) 수정 - [ ] 청산 순서 (§3.3) ### 5.4 스캘핑 (scalp) - [ ] ScalpParams 확장 (whipsaw/호가/`max_loss_krw`) - [ ] intrabar high/low TP/SL (현재는 종가 pnl% only) - [ ] 어깨 + 금액손실컷 (§3.4) - [ ] 브릿지 `skip_hts` → `skip_hts_scan_dupes` - [ ] trade dict `peak_price` = 실제 max_price (현재는 `sell_price` 로 왜곡) ### 5.5 레인지 (updown_box, 레거시) - 우선순위 낮음. 실매 미사용이면 이식 보류. --- ## 6. 정합 검증 방법 (golden test) ### 6.1 데이터 준비 - 국내: `ws_ticks`, `kis_candles`, `ls_ws_candles` 에서 최근 거래일 5-10 종목 subset 을 파일(json/parquet) 로 export - 파라미터: 실매 DB 스냅샷 + Optuna best 몇 개 - 유니버스: `build_universe_timeline` 결과를 파일로 저장 ### 6.2 실행 ```bash # Python 기준 python3 scripts/golden_run.py --engine python --strategy tail --date 2026-09-04 # Rust 후보 python3 scripts/golden_run.py --engine rust --strategy tail --date 2026-09-04 ``` ### 6.3 diff 기준 | 단계 | 허용 diff | |---|---| | Phase P0 완료 | 거래 수 / 진입 시각 / 청산 시각 / 청산 reason / entry / exit — **완전 일치** | | Phase P1 완료 | + PnL·MDD·PF — **1원 단위 일치** | | Phase P2 완료 | + 필터 ON 조건도 동일 | diff 발생 시 첫 다른 trade 의 candle/tick/params 스냅샷을 남기고 그 케이스만 좁혀 재현. ### 6.4 웹 백테 정합 (룰 15 · 28) Optuna best JSON → 웹 백테 재실행 → 1원 단위 일치 (`bt-form-postfilter-key-parity.mdc`). --- ## 7. 빌드 · 배포 절차 ### 7.1 개발 빌드 ```bash cd /home/hoon/kis_bot/kis_rust_src cargo build --release cp target/release/libkis_rust_core.so /home/hoon/kis_bot/kis_rust_core.so # venv 반영 cp target/release/libkis_rust_core.so \ /home/hoon/kis_bot/.venv/lib/python3.12/site-packages/kis_rust_core/kis_rust_core.cpython-312-x86_64-linux-gnu.so ``` ### 7.2 스모크 ```bash python3 -c "import kis_rust_core as k; print(dir(k))" python3 -u scripts/test_live_execution_validation.py ``` ### 7.3 롤백 - 빌드 전 `.so` 를 `kis_rust_core.so.bak` 로 백업 - Optuna는 `BACKTEST_USE_RUST=0` 로 즉시 Python 폴백 가능 - UI Rust 뱃지는 `use_rust=False` job JSON 재저장 시 자동 반영 --- ## 8. 리스크 · 마일스톤 | 리스크 | 대응 | |---|---| | Rust 이식 중 실매 사고 | 실매 코어는 Python. Rust는 백테/Optuna만. **실매 경로에 Rust 미사용** (원칙) | | 포트폴리오 시각순 큐의 미묘한 순서 차이 | Rust `BTreeMap