# 정합성

> **최종안 (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 금지**뿐. 파람/아침용 봉 소스를 둘로 나누지 않음.

---

## 1. 잠긴 매매 규칙 (변경 금지)

| 항목 | 규칙 |
|------|------|
| 신호 봉 | **T−1 확정봉** (`live_backtest_align=True`) |
| 진입 봉 | **T 확정봉** |
| 유니버스 | 초 단위 `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로 바꾸지 않는다
