Files
kis_trader/docs/정합성.md
Your Name 1d69c217e2 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>
2026-09-06 19:23:01 +09:00

277 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 정합성
> **최종안 (2026-07-17)** — 실매 · 웹백테 · Optuna가 **같은 분봉(OHLCV) 진실**을 쓰게 하는 규칙.
> 신호/진입 바 오프셋(±1) 땜빵은 **금지**. 봉이 확정 후에도 커지는 것이 근본 원인이다.
관련: [CANDLE_FLOW.md](./CANDLE_FLOW.md) · [SCALP_BACKTEST_VS_LIVE.md](./SCALP_BACKTEST_VS_LIVE.md) · [BACKTEST_ALIGNMENT_FINAL.md](./BACKTEST_ALIGNMENT_FINAL.md)
---
## 0. 한 줄 원칙
```
실매가 확정한 1분봉(그 순간의 OHLCV) = DB에 동결 = 웹백테/Optuna가 읽는 봉
```
- **틱** = 체결가(진입/청산 슬리피지)
- **분봉** = 매수 여부(RSI·거래량배수·평균 등)
- **EOD** = 포지션 장마감 청산(≈15:25). **봉 동결이 아님.**
### 0.1 누가 무엇을 읽나 (오해 방지)
| 곳 | 읽는 것 | 아님 |
|----|---------|------|
| **실매(장중)** | WS로 막 확정된 봉 → RAM + DB에 **첫 INSERT** | — |
| **웹백테 · 파람/Optuna** | DB **`ws_candles`(+`ws_ticks`)** 과거 재생 | Optuna가 **실시간 WS에 붙지 않음** |
| **아침 기동** | 같은 DB 로드 + 없는 분만 INSERT | “새벽 INSERT만 보는” 전용 분기 **없음** |
대화에서 쓰인 “실매가 확정한 **실시간** 분봉” = **장중 확정 순간의 숫자**를 뜻함.
→ 파람을 실시간 피드로 바꾸라는 뜻이 **아님**.
→ 고칠 코드 = **존재 시 UPDATE 금지**뿐. 파람/아침용 봉 소스를 둘로 나누지 않음.
### 0.2 증권사별 저장 · 읽기 (2026-08-14)
`ws_candles.source` = 증권사(`kiwoom`|`kis`), `channel` = `ws`|`rest`|`rollup`.
UNIQUE = `(code, timeframe, candle_time, source, channel)`.
| 쓰기 | 규칙 |
|------|------|
| 키움/KIS WS 틱 분봉 | `source=증권사, channel=ws` |
| 갭보정 | **항상 키움 ka10080**`source=kiwoom, channel=rest`. KIS REST 페이지 갭보정 **사용 금지** |
| 1M→nM 롤업 | `source=kiwoom, channel=rollup` |
| 영구구독 KR | LS WS → `ls_ws_candles` 만. **`ws_candles`에 넣지 않음**. 자동 갭보정 제외. 탭 「확정봉 가져오기」=키움 REST → `ls_ws_candles` INSERT IGNORE |
읽기(실매 RAM = Optuna `bt_candle_source`): 틱 메인 WS 한 칸, 없으면 `kiwoom+rest`, 그다음 롤업.
`CANDLE_SOURCE=kis` 여도 구멍은 키움 REST. 키움 REST로 KIS WS 행을 덮지 않음.
영구구독 마스터 `PERMANENT_SUBSCRIBE_ENABLED`(기본 true): OFF면 행 유지, KR LS / US 해외 WS 구독만 안 함.
### 0.3 호가필터 구멍 ≠ 봉 ±1 (혼동 금지)
봉 정합(본 문서)과 **별개**. 진입 호가필터가 저장 TTL로 `None`이 되면 필터 ON인데 통과한다.
고치는 키: `WS_ORDERBOOK_FILTER_MAX_AGE_SEC`(기본 0=마지막 RAM). 저장은 `WS_ORDERBOOK_TICK_MAX_AGE_SEC`.
사진 한 장도 없으면 `WS_ORDERBOOK_FILTER_REJECT_IF_EMPTY`(기본 true)로 안 삼.
상세 `docs/호가.md`. ±1봉 보정으로 맞추지 말 것.
---
## 1. 잠긴 매매 규칙 (변경 금지)
| 항목 | 규칙 |
|------|------|
| 신호 봉 | **T1 확정봉** (`live_backtest_align=True`) |
| 진입 봉 | **T 확정봉** |
| 유니버스 | 실매=키움 RAM 푸시/팝. 백테=`target_candidates_history` 완성 스냅샷 재생 (`event_time` 마이크로초). 분 슬롯 ±1 해킹 금지 |
| HTS | SCAN 후보 참고. `*_SKIP_HTS_SCAN_DUPES` 기본 **false** 유지 |
| 차트봉 | 매매·백테·Optuna 경로에 사용하지 않음 (`ws_candles` + `ws_ticks`) |
---
## 2. 근본 원인 (측정, 2026-07-16)
| 현상 | 수치/관찰 | 의미 |
|------|-----------|------|
| 장중 확정 후 재기록 | 봉 마감 +5분 이후 `updated_at`**76%** | 갭보정 REST가 기존 봉 **UPDATE**(큰 volume 우선) |
| 새벽/매도 후 백필 | 다음날 `updated_at`**3.8%** (00:48대), 매도 종목과 100% 겹침 | `POST_SELL_CANDLE_BACKFILL`이 hold 구간 UPSERT |
| 갭 구멍 INSERT | ENTER 시 결측분 흔함 | 웜업용 — **INSERT는 유지**, 덮어쓰기만 문제 |
| 샘표류(007540) | 실매 vol_avg≈1328 vs DB≈634 | 같은 공식, **lookback 시리즈가 다름** |
**정합을 깨는 본체 = 확정 후 UPDATE.** INSERT(구멍 메우기)가 아니다.
---
## 3. 최종 설계: Freeze-on-confirm
### 3.1 규칙
1. **행이 이미 있으면 OHLCV·RSI 덮어쓰기 금지** (RAM `merge_confirmed_bars` + DB upsert + 매도/보유 백필 동일).
2. **없을 때만 INSERT** — 갭 구멍·웜업 유지.
3. 진행 중(미확정) 버킷은 기존처럼 confirmed에 넣지 않음 (`WS_GAP_FILL_SKIP_INCOMPLETE_BUCKET`).
4. (옵션, 후속) 거래량을 틱 합으로 통일 — 1번 안정화 후.
### 3.2 수정 대상 (구현 시)
| 경로 | 현재 | 목표 |
|------|------|------|
| `kis_ws.merge_confirmed_bars` | 기존 봉 + `new_vol > old_vol` → upsert | **존재 시 skip** |
| `kis_ws` DB batch / `_flush_batch` | `ON DUPLICATE KEY UPDATE` volume 등 덮음 | **존재 시 no-op** (또는 INSERT IGNORE / 조건부) |
| `post_sell_candle_backfill` `_INSERT_SQL` | `volume=IF(VALUES>volume…)` | **존재 시 skip**, 없는 분만 INSERT |
| 기타 REST/키움 갭보정 → merge | 위 merge 경유 | merge 규칙만 바꿔도 대부분 흡수 |
### 3.3 하지 않을 것
- 09:05 시작·새벽 수집 전면 중지만으로 “해결” 선언 (보조책은 가능, 본체 아님)
- 신호/유니버스 ±1분 보정
- “EOD 했으니 당일 봉만 보면 된다” (EOD≠봉동결; lookback·전일시가 필요)
- 매수 직전 REST 재조회로 “안정화 체크”를 본치료로 쓰기 (느림·429·나중에 또 덮이면 무의미)
### 3.4 운영 보조 (선택, 본치료 아님)
| 아이디어 | 기대 | 한계 |
|----------|------|------|
| 09:05 기동 (조건식·봉 정리 후) | 장초 혼선 ↓ | 장중 UPDATE·새벽 백필 미해결 |
| 새벽/장전 **덮어쓰기** 금지 | freeze와 동일 방향 | 수집 자체 금지가 아니라 INSERT only |
| 실시간 “봉 안정” 폴링 | 체감용 | freeze 없으면 DB는 결국 갈라짐 |
---
## 4. 구현 순서 (승인 후)
1. **존재 시 덮어쓰기 금지**
`merge_confirmed_bars` / DB upsert / 백필 — 공통 규칙.
2. **스모크**
확정 직후 volume vs 1시간 뒤 · 다음날 — **동일**해야 함.
3. **7/16 샘표류 대조**
vol 평균·PASS/FAIL이 실매 저널과 같은지 (웹백테 동일 파라미터).
코드 변경 전: 핵심 매매 로직이 아닌 **저장/병합 계층** 패치.
시그널 T1/T·청산식은 이 작업에서 건드리지 않음.
---
## 5. 검증 체크리스트
- [ ] 확정 봉 DB row: 이후 REST/백필이 volume을 키우지 않음
- [ ] 결측 분은 여전히 INSERT로 채워짐 (웜업 0건 폭주 없음)
- [ ] 재시작 후 RAM 재로드 → 확정분과 DB 일치
- [ ] 갭보정 로그: `update=` 가 0에 수렴(또는 skip 카운트), `insert=` 만 정상
- [ ] 전략 2개 이상 경로 스모크 (예: SCALP + TAIL) — 공유 `ws_candles` 부작용 없음
- [ ] 웹백테 / Optuna: 동일 `ws_candles` → 실매 저널과 PASS·지표 근접
- [ ] `POST_SELL_CANDLE_BACKFILL` 켠 상태에서도 hold 구간 **구멍만** 채움
---
## 6. 영향 범위 분류
| 구분 | 영향 |
|------|------|
| **실매** | RAM/DB에 남는 확정봉이 “첫 확정값”으로 고정 → 이후 매수 판정 입력 안정 |
| **웹백테·Optuna** | 같은 DB를 읽으므로 실매와 입력 정렬 (엔진식 변경 없음) |
| **과거 DB** | 이미 덮여 커진 봉은 자동 복구 안 됨. 필요 시 해당일 재수집·재백테는 별도 |
---
## 7. 승인 상태
| 항목 | 상태 |
|------|------|
| 방향: freeze-on-confirm / INSERT only | **사용자 승인·구현 진행 (2026-07-17)** |
| env | `WS_CANDLE_FREEZE_ON_CONFIRM` 기본 **true** (false=레거시 덮어쓰기) |
| 구현 | `kis_ws` merge/confirm/DB flush · DB시드 · `post_sell_candle_backfill` · `fill_kiwoom_candles` |
| 재시작 | `db_seed=N` 로그 = DB 확정봉을 RAM에 시드 (REST로 안 덮음) |
첫 패치: **존재 시 UPDATE 금지** (확정행 OHLCV 동결). 미확정→확정 갱신은 허용.
재시작 시 RAM이 비면 REST가 “첫 삽입”처럼 보이므로, **DB에 이미 있으면 DB값으로만 RAM 시드**.
---
## 8. 다음에 할 일 (쉬운 말 · 우선순위)
> 코드(freeze)는 이미 켜져 있다. 아래는 **새 기능이 아니라 “잘 됐는지 확인”** 순서다.
### 1순위 — 깨끗한 장일 하루만 검증
**말:** 봉이 끝난 직후 적힌 거래량(volume)이, 한 시간 뒤·다음날에도 **그대로**여야 한다.
중간에 REST·갭보정·매도백필이 숫자를 키우면 실패.
**보는 법:**
- 확정 직후 volume vs 1시간 뒤 · 다음날 → **같아야 함**
- 갭보정 로그: `freeze_skip` 있고, `update≈0`, **`insert`만** 정상 (구멍 메우기)
### 2순위 — 웹백테 1회 (같은 날 · 같은 파라미터)
**말:** 그날 실매가 본 봉으로, 웹 백테도 같은 시험을 한 번 돌려 본다.
PASS/지표가 실매 저널과 **크게 안 벌어지면** OK.
**주의:** 이미 덮여 커진 날(예: **7/1516**)은 참고만. **정합 합격 기준으로 쓰지 말 것.**
### 3순위 — 이 문서 §5 체크리스트 닫기
위 1·2가 통과하면 §5 미체크 칸을 체크한다.
그때 “정합 검증 완료”라고 말해도 된다.
### 선택(후속) — 틱 합으로 volume 통일
freeze가 안정된 **뒤에만**. 지금은 필수 아님.
### 안 하는 것 (여기선 안내만 — 강제 금지는 `.cursorrules`)
- 신호/진입을 ±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 무력화).