From 1d69c217e207efb827bfdcd50f02e4df64688ad3 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 6 Sep 2026 19:23:01 +0900 Subject: [PATCH] =?UTF-8?q?fix(=EC=A0=95=ED=95=A9=EC=84=B1):=20=ED=8B=B1?= =?UTF-8?q?=20lag=20wall-clock=20=EC=A0=95=ED=95=A9=20+=203=EB=B2=A4?= =?UTF-8?q?=EB=8D=94=20DB=20=EC=A0=80=EC=9E=A5=20=EC=8A=A4=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - feed_fallback.bar_is_garbage: 봉끝 기준 → 각 틱의 recv_ts wall-clock 기준으로 정정 실매 RAM 3초컷과 동일 논리 → 유동성 낮은 종목 부당 스킵 해소 - candle_garbage_fallback_enabled: 기본 True 복원 (wall-clock 정정 후 안전) - param_search_optuna·run_tail_backtest_cli: CANDLE_GARBAGE_FALLBACK·BACKTEST_USE_RUST 강제 os.environ 세팅 제거 → DB env·CLI 플래그로만 관리 (UI 존중) - WS_TICK_DB_SAVE_LAG_CUT_ENABLED 신설 (bool, 기본 false, 3벤더 공통) OFF=키움/KIS/LS 모든 틱 lag 무관 전부 저장 (벤더 통계·재현·백테 정합) ON=lag > LIVE_FEED_FALLBACK_MAX_AGE_SEC 이면 미저장 (미래 A안) - KIWOOM_TICK_LIVE_MAX_LAG_SEC 완전 폐기 → 위 스위치로 통일 - kiwoom_ws: _skip_persist 로직 새 스위치로 교체 - kis_ws·ls_ws: _skip_persist_kis/_ls 신규 (벤더별 상이했던 정책 통일) - docs/정합성.md §9 신설 (문제·결정·시나리오·향후 A안 전환법) - docs/rust_engine_parity_port_plan.md (신규 설계) 실매 스모크: logs/test_live_execution_validation_20260906_191845.log → 최종: 통과 · 👑 완결 브라우저 검증: http://192.168.0.149:5050/#liveconfig → 새 스위치 노출, JS 오류 없음 영향: 실매(DB 저장 정책 통일, RAM 컷 변경 없음) + 백테/Optuna(실매 정합) Co-authored-by: Cursor --- database.py | 7 +- docs/like_mcp.md/MODIFICATION_GUIDE.md | 6 +- docs/rust_engine_parity_port_plan.md | 423 +++++++++++++++++++++ docs/정합성.md | 76 ++++ kis_trader/backtest/param_search_optuna.py | 10 +- kis_trader/engine/feed_fallback.py | 45 ++- kis_trader/web/live_config_schema.py | 11 +- kis_trader/ws/kis_ws.py | 19 +- kis_trader/ws/kiwoom_ws.py | 14 +- kis_trader/ws/ls_ws.py | 20 +- scratch/explain_025980_lag28.py | 2 +- scripts/run_tail_backtest_cli.py | 9 +- 12 files changed, 600 insertions(+), 42 deletions(-) create mode 100644 docs/rust_engine_parity_port_plan.md diff --git a/database.py b/database.py index b0adb3a..9477ecc 100644 --- a/database.py +++ b/database.py @@ -587,6 +587,10 @@ ENV_CONFIG_KEYS = ( "WS_PRICE_MAX_AGE_SEC", # 실매·옵투나 읽기 폴백 나이(초). 메인 체결시각 vs 지금. 넘기면 그 벤더 실패→2차→3차. "LIVE_FEED_FALLBACK_MAX_AGE_SEC", + # ws_ticks DB 저장 시 lag 컷 스위치 (2026-09-06 신설, 3벤더 공통 · docs/정합성.md §9) + # false(기본): DB 는 모든 틱 저장 (벤더 통계·재현·백테 정합용) + # true: lag > LIVE_FEED_FALLBACK_MAX_AGE_SEC 인 틱은 미저장 (미래 A안 전환용) + "WS_TICK_DB_SAVE_LAG_CUT_ENABLED", # 수집통계 탭: 종목별 recv_ts 공백(끊김) 소프트/하드/상한(초) "FEED_STATS_DISCONNECT_SOFT_SEC", "FEED_STATS_DISCONNECT_HARD_SEC", @@ -1494,8 +1498,7 @@ ENV_CONFIG_KEYS = ( "KIWOOM_WS_REG_REFRESH", # 키움 0B FID20(체결시간)이 recv 대비 이 값(초) 초과 stale 이면 wall-clock 으로 tick_time 보정 "KIWOOM_TICK_TIME_MAX_LAG_SEC", - # 0=ws_ticks 전부 적재. 양수=FID20 vs 지금 초과 시 틱만 미저장. RAM skip 은 LIVE_FEED_FALLBACK. - "KIWOOM_TICK_LIVE_MAX_LAG_SEC", + # KIWOOM_TICK_LIVE_MAX_LAG_SEC 삭제 (2026-09-06): WS_TICK_DB_SAVE_LAG_CUT_ENABLED 로 통일 "KIWOOM_TICK_STALE_LOG_GAP_SEC", # 키움 WS 재연결: 토큰거부(805004) 연타 후 영구 OFF 방지 (기본=쿨다운 리셋) "KIWOOM_WS_HARD_DISABLE_ON_MAX_RECONNECT", diff --git a/docs/like_mcp.md/MODIFICATION_GUIDE.md b/docs/like_mcp.md/MODIFICATION_GUIDE.md index 24c2833..7f4d8e2 100644 --- a/docs/like_mcp.md/MODIFICATION_GUIDE.md +++ b/docs/like_mcp.md/MODIFICATION_GUIDE.md @@ -89,7 +89,7 @@ align 스터디는 `limit_atr_mult` 제외. fixed 스터디는 `atr_sl_*` 제외 - `LS_FEED_FALLBACK_SUBSCRIBE`(기본 false) → spill/permanent만. ON=후보∪보유∪grace LS RAM 미러(레거시). - `LS_WS_TICK_SAVE` → `ls_ws_ticks` INSERT. 게이트=`_ls_is_subscribed`(구독 전체, 호가와 동일). 봉·VI만 `should_persist_ls`(영구). - `BT_TICK_LS_THIRD_FALLBACK`(기본 true) → 백테/옵투나 `load_breakout_ticks_by_code` 가 같은 초 1·2차 없을 때 `ls_ws_ticks` 채움. 나이=`LIVE_FEED_FALLBACK_MAX_AGE_SEC`. -- 키움 RAM/리스너/봉 skip = 읽기 나이. `ws_ticks` 적재는 기본 전부(`KIWOOM_TICK_LIVE_MAX_LAG_SEC=0`). 같이 내리지 말 것. `ws_ticks.channel`(기본 ws)은 봉과 같은 경로 라벨. ka10007을 틱 INSERT 하지 말 것. +- 키움/KIS/LS RAM/리스너/봉 skip = 읽기 나이(`LIVE_FEED_FALLBACK_MAX_AGE_SEC`, 기본 3). `ws_ticks` 적재는 스위치 `WS_TICK_DB_SAVE_LAG_CUT_ENABLED`(기본 **false**=3벤더 전부 저장). 같이 내리지 말 것. `ws_ticks.channel`(기본 ws)은 봉과 같은 경로 라벨. ka10007을 틱 INSERT 하지 말 것. (2026-09-06: `KIWOOM_TICK_LIVE_MAX_LAG_SEC` 폐기 · 통일) - 시세 REST 키 = `KIWOOM_WS_FORCE_REAL`(기본 true) 실키. `_get_kiwoom_creds` 가 `KIS_MOCK` 모의키로 가면 안 됨 (8001). - `LS_GAP_FILL_CANDIDATES` 기본 OFF. - `LS_WS_TICK_SAVE` 기본 false 가능(운영 ON 권장 검증기간). `_tick_recorder` 를 같이 끄면 호가 틱동기도 0건. `LS_WS_ALSO_HOGA` 기본 true(UH1 구독). `LS_WS_ORDERBOOK_SAVE` 기본 true(구독 종목). 호가필터 `FILTER_MAX_AGE=0` 을 저장 TTL 과 다시 합치지 말 것. @@ -241,10 +241,12 @@ grep -rn "trigger_e_confirm" /home/hoon/kis_bot --include="*.py" ### 키움 0B catch-up / REG refresh ``` -KIWOOM_TICK_LIVE_MAX_LAG_SEC (기본 5): FID20 vs 수신시각 lag 초과 → RAM·봉·호가동기·ws_ticks 미반영. +LIVE_FEED_FALLBACK_MAX_AGE_SEC (기본 3): 3벤더 공통 RAM 읽기 나이. FID20/chetime vs 지금 초과 → 매매 RAM/봉/호가동기 미반영. +WS_TICK_DB_SAVE_LAG_CUT_ENABLED (기본 false): 3벤더 공통 DB 저장 컷. OFF=ws_ticks 전부 저장(통계 유지). ON=RAM 컷과 동일 기준으로 미저장. KIWOOM_TICK_TIME_MAX_LAG_SEC (기본 120): 같은 FID20 가 이 초 이상일 때만 아침 동결 예외. KIWOOM_WS_REG_REFRESH (기본 1): 0은 LOGIN 일괄 REG 첫 청크만. 장중 추가 REG는 항상 1. ``` +(2026-09-06: `KIWOOM_TICK_LIVE_MAX_LAG_SEC` 삭제 → `WS_TICK_DB_SAVE_LAG_CUT_ENABLED` 로 통일. bar_is_garbage 는 recv_ts 기반 wall-clock.) ### SKIP_HTS_SCAN_DUPES 규칙 ``` diff --git a/docs/rust_engine_parity_port_plan.md b/docs/rust_engine_parity_port_plan.md new file mode 100644 index 0000000..130afdc --- /dev/null +++ b/docs/rust_engine_parity_port_plan.md @@ -0,0 +1,423 @@ +# 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