fix(정합성): 틱 lag wall-clock 정합 + 3벤더 DB 저장 스위치 통일
- 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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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 규칙
|
||||
```
|
||||
|
||||
423
docs/rust_engine_parity_port_plan.md
Normal file
423
docs/rust_engine_parity_port_plan.md
Normal file
@@ -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<time, Vec<Signal>>` → 시각순 이터레이션
|
||||
- `max_stocks`, `total_budget_krw`, 1시각 1매수, `slot_money`, vol_fill_cap
|
||||
- 유니버스 timeline 은 Python에서 미리 계산해 Rust에 `Vec<(time, Vec<code>)>` 전달 (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<Orderbook>` 전달 (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<time>` 로 시각 정렬 → Python `sorted(all_times)` 동일 순서 |
|
||||
| 유니버스 timeline DB 재조회 부하 | Python에서 미리 계산해 Rust에 넘김 (Rust는 DB 안 침) |
|
||||
| 틱 데이터 메모리 폭증 (수백만 tick) | 종목별 lazy load + LRU 캐시 or 시간 windowing |
|
||||
| `apply_bt_post_filters` 이중 필터 | P2 완료 후 사후 필터 기본 OFF, 회귀 테스트 모드에서만 ON |
|
||||
| Rust 컴파일 실패 | `cargo build --release` 실패 시 `.so` 유지 (교체 안 함) |
|
||||
|
||||
### 마일스톤 제안
|
||||
|
||||
| M | 산출물 | 예상 규모 |
|
||||
|---|---|---|
|
||||
| M1 | P3(안전조치) 즉시 반영: Optuna 기본 Python 복귀 + 브릿지 3건 정정 | 소규모 (1턴) |
|
||||
| M2 | P0 포트폴리오 + 틱 청산 Rust 모듈 + golden test 프레임 | 중간 (수 턴) |
|
||||
| M3 | P1 청산 순서 4전략 완성 | 중간~대규모 |
|
||||
| M4 | P2 whipsaw/호가/프로그램 필터 이식 | 중간 |
|
||||
| M5 | P3 표준 키 확장 · post_filter 격하 · CI · 문서 갱신 | 소규모 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 새 대화 시작 시 사용할 프롬프트 템플릿
|
||||
|
||||
```
|
||||
목표: docs/rust_engine_parity_port_plan.md 의 Phase P0 부터 시작.
|
||||
|
||||
준비:
|
||||
1. 이 대화의 실매 스모크는 통과 상태 (2026-09-06 완료 로그: logs/test_live_execution_validation_20260906_180432.log)
|
||||
2. `bt_post_filters.py`, `optuna_web_jobs.py`, `backtest_web.py`, `static/js/backtest.js`, `param_search_scalping.py`, `param_search_apply_snapshot.py` 는 2026-09-06 통일(whipsaw_filter_enabled 표준 키) + 시그니처 정정(orderbook_reject_for_entry current_price=) 완료.
|
||||
3. 옵투나 잡 삭제 API/UI 신설 완료 (`/api/optuna/delete/<job_id>`)
|
||||
4. 리아퍼 훅으로 refine 완료 시 phase1/2 결과 JSON 자동 register 완료.
|
||||
|
||||
절차:
|
||||
1. P0-0: `param_search_optuna.py:48` 의 `BACKTEST_USE_RUST=1` 강제를 optional 로 (안전조치)
|
||||
2. P0-1: golden test 프레임 (`scripts/golden_run.py`) 신설 — Python vs Rust 결과 diff
|
||||
3. P0-2: `session_manager.rs` 에 tick 저장·조회 확장
|
||||
4. P0-3: 포트폴리오 시각순 시뮬 Rust 모듈 (`kis_rust_src/src/portfolio_engine.rs`) 신설
|
||||
5. P0-4: 각 전략 `run_engine_trial_*_portfolio` 신설, 기존 `run_engine_trial_*` 은 종목별 독립 모드로 유지
|
||||
|
||||
각 단계 완료 후:
|
||||
- `cargo build --release`
|
||||
- `.so` 교체
|
||||
- `python3 -u scripts/test_live_execution_validation.py` 통과 확인
|
||||
- golden test diff = 0 확인
|
||||
- 다음 단계 착수 전 사용자 승인
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 참고 문서 · 규칙
|
||||
|
||||
- `.cursorrules` CRITICAL §0 (봉 freeze · skip_hts · OHLC 폴백 금지)
|
||||
- `.cursor/rules/no-ohlc-fallback-parity.mdc`
|
||||
- `.cursor/rules/live-backtest-optuna-parity.mdc` (룰 15)
|
||||
- `.cursor/rules/bt-form-postfilter-key-parity.mdc` (룰 28, 2026-09-06 신설)
|
||||
- `.cursor/rules/optuna-job-engine-label.mdc` (룰 29, 2026-09-06 신설)
|
||||
- `.cursor/rules/live-execution-validation.mdc`
|
||||
- `.cursor/rules/verify-before-conclude-cross-strategy.mdc` §2 (버그 딥다이브 · 근본 3안)
|
||||
- `docs/정합성.md` §8 (봉 정합)
|
||||
- `docs/호가.md` (호가 필터 TTL 분리)
|
||||
|
||||
---
|
||||
|
||||
## 부록 A: 이번 대화(2026-09-06) 에서 완료된 사전 정지 작업
|
||||
|
||||
| # | 항목 | 파일 |
|
||||
|---|---|---|
|
||||
| A1 | `bt_post_filters.py:75` `orderbook_reject_for_entry` 시그니처 정정 (`price=` → `current_price=`, 튜플 unpack) | `kis_trader/backtest/bt_post_filters.py` |
|
||||
| A2 | `save_job()` tmp 파일에 pid 접미 (다중 프로세스 rename 경합 해소) | `kis_trader/backtest/optuna_web_jobs.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) |
|
||||
|
||||
## 부록 B: 발견된 Rust 정합 이슈 12건 (우선순위)
|
||||
|
||||
| # | 항목 | 리스크 | 파일:라인 | Phase |
|
||||
|---|---|---|---|---|
|
||||
| 1 | Optuna 기본 = Rust OHLC 단순 엔진 (BACKTEST_USE_RUST=1 강제) | 랭킹 왜곡 | `param_search_optuna.py:48` | P3-1 |
|
||||
| 2 | 모멘텀 MomentumParams 인자 누락 (`min_hold_sec`) → TypeError | trial 전부 빈 trades | `momentum_backtest_common.py:620-653` | P3-2 |
|
||||
| 3 | 틱 청산 ON 시 Rust `c.high` 로 max_price 선반영 | look-ahead | `tail.rs:186,199` | P0-4 |
|
||||
| 4 | 청산 우선순위 (EOD·TP 우선 vs 어깨·래칫 우선) | reason·PnL 상이 | 각 `*.rs` vs Python 엔진 | P1-1 |
|
||||
| 5 | 꼬리 skip_hts_scan_dupes 기본값 반대 (Rust True vs Python False) | TRIGGER ON/OFF 반대 | `tail_backtest_common.py:822` | P3-2 |
|
||||
| 6 | 꼬리 experimental TailParams 27인자 vs Rust 19인자 | TypeError | `tail_engine.py:2547-2567` | P3-2 |
|
||||
| 7 | 스캘핑 브릿지 `skip_hts` vs Python `skip_hts_scan_dupes` | 필터 스킵 다름 | `scalping_backtest_common.py:366` | P3-2 |
|
||||
| 8 | 스캘핑 TP/SL 종가 pnl% (intrabar high/low 미사용) | 체결가 차이 | `scalp.rs:206-235` | P1-1 |
|
||||
| 9 | 돌파 SL Python ATR vs Rust 고정% | 손절선 상이 | `breakout.rs:217` | P1-4 |
|
||||
| 10 | 모멘텀 ATR/래칫/min_hold/time_stop Rust 미구현 | 고급 청산 무시 | `momentum.rs` | P1-1 |
|
||||
| 11 | 스캘핑 trade dict `peak_price = sell_price` | MFE 왜곡 | `scalping_backtest_common.py:381` | P1-1 |
|
||||
| 12 | 레인지 `updown_box.rs` 단순 무지성 실험 수준 | 레거시 | `updown_box.rs:65-131` | 보류 |
|
||||
|
||||
---
|
||||
|
||||
**끝.** 이 문서 기준으로 새 대화에서 P0 부터 착수.
|
||||
76
docs/정합성.md
76
docs/정합성.md
@@ -198,3 +198,79 @@ freeze가 안정된 **뒤에만**. 지금은 필수 아님.
|
||||
|
||||
- 신호/진입을 ±1분으로 땜빵하지 않는다
|
||||
- freeze를 끄거나 `*_SKIP_HTS_SCAN_DUPES`를 true로 바꾸지 않는다
|
||||
|
||||
---
|
||||
|
||||
## 9. 틱 lag 정합 (2026-09-06 · 실매 RAM ↔ 백테 wall-clock 일치)
|
||||
|
||||
이전 사고: 옵투나에서 **12~22% 봉이 "쓰레기"** 로 스킵. 원인은 3가지가 겹침.
|
||||
|
||||
### 9.1 문제 (근본원인 요약)
|
||||
|
||||
| # | 코드 | 문제 |
|
||||
|---|------|------|
|
||||
| 1 | `bar_is_garbage` 가 **봉끝 시각(bar_end)** 을 기준으로 `lag = bar_end - tick_time` | 실매 RAM은 `wall-clock - tick_time` 로 판정 → 백테는 봉 시작 근처 유동성 낮은 종목 **부당 스킵** |
|
||||
| 2 | `param_search_optuna.py` 가 import 시점에 `CANDLE_GARBAGE_FALLBACK=False` 강제 세팅 | 사용자가 UI에서 켜도 무시 |
|
||||
| 3 | 벤더별 DB 저장 정책 제각각 (kiwoom: `KIWOOM_TICK_LIVE_MAX_LAG_SEC>0` 이면 컷 · kis: 무조건 저장 · ls: RAM skip 시 저장 안 함) | 통계·재현·백테 정합이 벤더마다 다름 |
|
||||
|
||||
### 9.2 결정 (사용자 승인)
|
||||
|
||||
1. **`bar_is_garbage` = wall-clock 정합** — 각 틱의 `recv_ts` 를 기준으로 `lag = recv_ts - tick_time` 계산.
|
||||
→ 실매 RAM 3초컷과 **동일 논리**. 유동성 낮은 종목 부당 스킵 해소.
|
||||
2. **DB 는 전부 수집** — 벤더별 지연 통계·재현·백테 정합용. 3벤더 공통 정책.
|
||||
3. **`KIWOOM_TICK_LIVE_MAX_LAG_SEC` 완전 폐기** → 새 스위치 `WS_TICK_DB_SAVE_LAG_CUT_ENABLED` 하나로 통일.
|
||||
4. **`param_search_optuna.py` 강제 세팅 제거** — DB env 로만 관리 (사용자 UI 존중).
|
||||
|
||||
### 9.3 새 스위치: `WS_TICK_DB_SAVE_LAG_CUT_ENABLED` (bool, 기본 **false** · 3벤더 공통)
|
||||
|
||||
| 값 | 동작 | 용도 |
|
||||
|----|------|------|
|
||||
| **false (기본)** | 키움/KIS/LS 모든 틱을 lag 무관 `ws_ticks` 전부 저장 | **B안** — 벤더 통계·재현·백테 정합 |
|
||||
| true | `lag > LIVE_FEED_FALLBACK_MAX_AGE_SEC` (기본 3초) 인 틱은 DB 미저장 | **A안** — 통계 확신 후 저장·매매 정합 통일 |
|
||||
|
||||
- 매매 RAM 은 **항상** `LIVE_FEED_FALLBACK_MAX_AGE_SEC` (기본 3초) 로 컷 (변경 없음).
|
||||
- 이 스위치는 **DB 저장 컷** 만 제어.
|
||||
|
||||
### 9.4 코드 진실 (수정 후)
|
||||
|
||||
| 경로 | 동작 |
|
||||
|------|------|
|
||||
| `kis_trader/engine/feed_fallback.py::bar_is_garbage` | **각 틱의 `recv_ts` 기준** wall-clock lag. `recv_ts` 없으면 봉끝 폴백 |
|
||||
| `kis_trader/engine/feed_fallback.py::candle_garbage_fallback_enabled` | 기본 **True** 복원 (실매 정합 논리 정정 후) |
|
||||
| `kis_trader/backtest/param_search_optuna.py` | `CANDLE_GARBAGE_FALLBACK`/`BACKTEST_USE_RUST` 강제 세팅 **제거** |
|
||||
| `kis_trader/ws/kiwoom_ws.py` | `_skip_persist` = `WS_TICK_DB_SAVE_LAG_CUT_ENABLED and lag > LIVE_FEED_FALLBACK` |
|
||||
| `kis_trader/ws/kis_ws.py` | `_skip_persist_kis` = 동일 로직 (기존엔 무조건 저장) |
|
||||
| `kis_trader/ws/ls_ws.py` | `_skip_persist_ls` = 동일 로직 (기존엔 skip_ram 시 저장 안 함) |
|
||||
|
||||
### 9.5 Parquet 초고속 틱 로더 (백테)
|
||||
|
||||
`kis_trader/backtest/breakout_tick_loader.py` · `kis_trader/utils/export_ticks_parquet.py`
|
||||
|
||||
- **Parquet = DB 스냅샷** (`export_ticks_parquet.py` 가 `ws_ticks` 를 필터 없이 그대로 export).
|
||||
- 스위치 OFF (기본) 상태 → DB 는 전부 저장 → Parquet 도 전부 포함 → **DB 로드와 완전 동일**.
|
||||
- 로드 후 `bar_is_garbage` (wall-clock recv_ts 기준) 가 동일하게 적용 → 실매 정합.
|
||||
- 즉 **Parquet 로더는 "로드 경로 초고속화"만** 이고, 쓰레기 스킵 판정은 후단(`candle_series`) 에서 동일.
|
||||
|
||||
### 9.6 시나리오 재검증 (수정 후)
|
||||
|
||||
| 시나리오 | 이전 (bar_end 기준) | 이후 (wall-clock 기준) | 실매 RAM |
|
||||
|----------|---------------------|------------------------|----------|
|
||||
| 유동성 낮은 종목: 09:00:05 유일 틱 | lag=55s → 쓰레기 스킵 ❌ | lag ≈ 0s (recv_ts 즉시) → 유효 ✅ | 유효 ✅ |
|
||||
| WS 재연결: 09:00:00 틱을 09:00:10 수신 | lag=59s → 스킵 | recv_ts-tick_time=10s → 스킵 ✅ | 스킵 (RAM 3초컷) ✅ |
|
||||
| 정상 실시간 틱 | lag < 60s → 유효 | lag ≈ 0s → 유효 ✅ | 유효 ✅ |
|
||||
|
||||
**결론: 백테 = 실매 정합 완료.**
|
||||
|
||||
### 9.7 향후 A안 (통계 확신 후)
|
||||
|
||||
1. `feed_collect_stats` 탭에서 벤더별 `lag > 3s` 비율 확인 (수 주간).
|
||||
2. 확신 서면 UI 에서 `WS_TICK_DB_SAVE_LAG_CUT_ENABLED = true` 로 전환.
|
||||
3. 이후 `ws_ticks` 는 lag > 3s 인 틱 제외되어 저장 (매매 RAM 컷과 완전 일치).
|
||||
4. `bar_is_garbage` 는 그대로 유지 (안전장치).
|
||||
|
||||
### 9.8 무엇을 하지 말 것
|
||||
|
||||
- ❌ 스위치를 **선제적으로 true** 로 바꾸지 말 것 (통계 부재 상태에서 벤더별 커버리지 손실 위험).
|
||||
- ❌ `bar_is_garbage` 를 다시 봉끝 기준으로 되돌리지 말 것 (실매 정합 깨짐).
|
||||
- ❌ `CANDLE_GARBAGE_FALLBACK=False` 로 다시 끄지 말 것 (12~22% 쓰레기는 봉끝 기준의 버그였음. wall-clock 기준으로는 훨씬 낮음).
|
||||
- ❌ `param_search_optuna.py` 에 `os.environ["..."]="..."` 강제 세팅 재도입 금지 (DB env 우회 → UI 무력화).
|
||||
|
||||
Reference in New Issue
Block a user