- 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>
14 KiB
정합성
최종안 (2026-07-17) — 실매 · 웹백테 · Optuna가 같은 분봉(OHLCV) 진실을 쓰게 하는 규칙.
신호/진입 바 오프셋(±1) 땜빵은 금지. 봉이 확정 후에도 커지는 것이 근본 원인이다.
관련: CANDLE_FLOW.md · SCALP_BACKTEST_VS_LIVE.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 규칙
- 행이 이미 있으면 OHLCV·RSI 덮어쓰기 금지 (RAM
merge_confirmed_bars+ DB upsert + 매도/보유 백필 동일). - 없을 때만 INSERT — 갭 구멍·웜업 유지.
- 진행 중(미확정) 버킷은 기존처럼 confirmed에 넣지 않음 (
WS_GAP_FILL_SKIP_INCOMPLETE_BUCKET). - (옵션, 후속) 거래량을 틱 합으로 통일 — 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. 구현 순서 (승인 후)
- 존재 시 덮어쓰기 금지
merge_confirmed_bars/ DB upsert / 백필 — 공통 규칙. - 스모크
확정 직후 volume vs 1시간 뒤 · 다음날 — 동일해야 함. - 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 결정 (사용자 승인)
bar_is_garbage= wall-clock 정합 — 각 틱의recv_ts를 기준으로lag = recv_ts - tick_time계산.
→ 실매 RAM 3초컷과 동일 논리. 유동성 낮은 종목 부당 스킵 해소.- DB 는 전부 수집 — 벤더별 지연 통계·재현·백테 정합용. 3벤더 공통 정책.
KIWOOM_TICK_LIVE_MAX_LAG_SEC완전 폐기 → 새 스위치WS_TICK_DB_SAVE_LAG_CUT_ENABLED하나로 통일.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안 (통계 확신 후)
feed_collect_stats탭에서 벤더별lag > 3s비율 확인 (수 주간).- 확신 서면 UI 에서
WS_TICK_DB_SAVE_LAG_CUT_ENABLED = true로 전환. - 이후
ws_ticks는 lag > 3s 인 틱 제외되어 저장 (매매 RAM 컷과 완전 일치). 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 무력화).