- 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>
277 lines
14 KiB
Markdown
277 lines
14 KiB
Markdown
# 정합성
|
||
|
||
> **최종안 (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. 잠긴 매매 규칙 (변경 금지)
|
||
|
||
| 항목 | 규칙 |
|
||
|------|------|
|
||
| 신호 봉 | **T−1 확정봉** (`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이 실매 저널과 같은지 (웹백테 동일 파라미터).
|
||
|
||
코드 변경 전: 핵심 매매 로직이 아닌 **저장/병합 계층** 패치.
|
||
시그널 T−1/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/15–16**)은 참고만. **정합 합격 기준으로 쓰지 말 것.**
|
||
|
||
### 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 무력화).
|