# 돌파 실매↔백테 괴리 Q&A (2026-07-16 기준)

> **패치 적용 완료 (2026-07-17).** 실매 봇은 재시작하지 않음. 백테 웹만 재시작.

---

## 1번 — 웜업 넣으면 되나?

**네 → 적용됨.**

- env: `BREAKOUT_BACKTEST_CANDLE_WARMUP_BARS` (기본 **50**, DB 시드)
- `prepend_breakout_candle_warmup` — 기간 시작 전 1분봉 prepend
- 포트폴리오 `all_times`는 `_backtest_period_start_key` 이후만 (전일 시계 오염 방지)
- 모멘텀 `MOMENTUM_BACKTEST_CANDLE_WARMUP_BARS`와 동일 계열

---

## 2번 — 유니버스 sticky/grace

**실매 따라가기 → 적용됨.**

### 동작

- 공통 `resolve_universe_exit_debounce_sec()` → 기본값 = `CONDITION_EXIT_GRACE_SEC`(운영 **60**, 기존 120→2026-07-17 축소)
- 돌파/꼬리/모멘텀 타임라인 `debounce_sec`에 연결
- 전략별 오버라이드: `BREAKOUT_UNIVERSE_EXIT_DEBOUNCE_SEC` / `TAIL_*` / `MOMENTUM_*` (있으면 우선, `0`=OFF)

### 09:07 1종 스냅샷 점검 결과

| event_time | n |
|---|---|
| 03:16 | 22 |
| 09:07 | **1** (248170만) |
| 09:11 | 2 |

- history는 **effective 유니버스 저장** 설계 → 09:07에 1종이면 그 tick의 live effective도 1종으로 보는 게 맞음 (저장이 grace를 빼먹은 버그로 단정하지 않음). 새벽 스냅샷 후 장초 첫 변동·콜드스타트 가능성이 큼.
- 디바운스 ON 시: 09:07 스냅샷에서도 overnight 종목을 **60초** 유지 → 이후 신규 스냅샷 기준으로 정리. 실매 EXIT grace와 같은 전환 규칙.

### 영향 범위

- **백테 유니버스 재생** (돌파·꼬리·모멘텀 등 타임라인 사용 전략). 실매 주문 로직 자체는 변경 없음.
- 망가뜨리는 패치가 아니라 **실매 grace 정합** 쪽. 백테 거래수/PnL은 실매에 가까워지도록 **바뀔 수 있음**.

---

## 3번 — 슬롯 늘려서 테스트

**사용자 검증용 (코드 변경 없음).** 총한도/동시보유↑로 슬롯 경쟁 효과 확인.

---

## 4번 — 미청산 flatten / EOD

**적용됨 (돌파·모멘텀·RANGE_BREAK).**

- 공통 `flatten_remaining_portfolio_trades` — 루프 끝 미청산을 **마지막 확정봉 종가**로 장부 기록
- `eod_enabled`면 `sell_reason=eod`, 아니면 `bt_flatten`
- skip_stats: `bt_flatten_count`
- 모멘텀: 웜업은 기존, **flatten은 이번에 추가** (이전 “미완” 해소)

---

## (부수) invest_cap 이중 % 스케일

**적용됨 (돌파·RANGE_BREAK).**

- `_resolve_breakout_sl_pct_ui`: `stop_loss_pct`(비율) → UI% ; `sl_pct`만 있으면 UI%/비율 자동 판별
- `breakout_invest_amount_krw`에 **UI% 한 번만** 전달 (×100 이중 제거)

---

## 패치 상태

| 순위 | 항목 | 상태 |
|------|------|------|
| 1 | 돌파 백테 **전일 봉 웜업** | **완료** |
| 2 | 유니버스 **sticky/grace 디바운스** + 09:07 점검 | **완료** |
| 3 | 슬롯/한도 검증용 백테 | 사용자 |
| 4 | 백테 종료 **미청산 flatten** | **완료** (돌파·모멘텀·레인지) |
| 부수 | `invest_cap` `%` 이중 스케일 | **완료** |

---

## 관련 파일

- 웜업·디바운스: `kis_trader/backtest/breakout_backtest_common.py`
- 공통 디바운스: `kis_trader/backtest/universe_timeline.py` (`resolve_universe_exit_debounce_sec`)
- flatten: `kis_trader/backtest/backtest_portfolio_common.py`
- 포트폴리오: `breakout_portfolio_backtest.py`, `momentum_portfolio_backtest.py`, `range_break_portfolio_backtest.py`
- 꼬리 디바운스: `tail_backtest_common.py`
- env 키: `database.py` (`BREAKOUT_BACKTEST_CANDLE_WARMUP_BARS` 등)

---

## 검증

- `py_compile` OK
- 스모크: debounce(03:16→09:07 유지→09:11 정리) / invest_cap(30만) / flatten(`eod`) → `SMOKE_OK`
- DB: `BREAKOUT_BACKTEST_CANDLE_WARMUP_BARS=50` 시드, config 컬럼 추가
- `kis_backtest_web.service` 재시작 (`active` + HTTP 200)
- 브라우저: `http://192.168.0.149:5050/` → **돌파매매 백테스트** 탭 클릭, 폼·「백테스트 실행」버튼 정상 표시

### 잔여 / 다음 확인

| 순위 | 항목 | 상태 |
|------|------|------|
| 다음 | **2번 검증** — 장초 이노테나·한울 `직전봉약세` | **근본원인 확정 + 갭보정 진행분 제외 적용** (봇 재시작 시 반영) |
| 후보 | 1번 A — 마지막 봉 경과 시 포트폴리오 루프에서 **즉시** 슬롯 해제 (전 전략 공통) | 대기 — 저유동은 C로도 분 생성 불가 시 필요 |
| 진행 | **1번 C — 판 뒤 1회 REST 백필** | **적용** (아래) |
| — | 3번 슬롯 경쟁 | 패스 |

### 1번 C 적용 (2026-07-17)

- 모듈: `kis_trader/engine/post_sell_candle_backfill.py`
- 실매 훅: `order_manager._finalize_sell_fill` → 매도 후 **비동기 1회** (시세=키움 **실키**)
- env: `POST_SELL_CANDLE_BACKFILL` (기본 true), `POST_SELL_CANDLE_ROLLUP_3M`, sleep 1~3초
- 즉시 백필 CLI: `scripts/backfill_trade_candles.py --like '2026-07-16%'`
- 로그: `/tmp/backfill_trade_candles_0716c.log` (및 선행 0716b)
- 한계: 거래 자체가 없는 분(저유동)은 REST에도 없음 → **C만으로는 완전 메꿈 불가**, A 보완 여지

- 웹 돌파 **2026-07-16** 재백테 ↔ 실매 비교는 C 반영 후 / 장초 직전봉 수정과 병행
- 실매 봇: post-sell 훅 반영하려면 **재시작 필요** (백필 CLI는 봇 없이 완료 가능)

---

## 장초 직전봉 (이노테나·한울) — 2번 검증 결과 (2026-07-17)

### 현상

| | 이노테나 `333050` | 한울 `320000` |
|--|--|--|
| 실매 매수 | 09:01:09 @5350 | 09:01:42 @11590 |
| 실매 로그 `prevChg` | **1.34%** | **0.61%** |
| BT (DB+웜업) | `탈락-직전봉약세` **0.19%** | `탈락-직전봉약세` **-2.37%** |
| `PREV_CHG_MIN` | 0.3 (실매=BT 동일) | 동일 |

### 숫자 재현 (스펙: 전전봉 종가 → 직전봉 종가 %)

- 이노테나 09:00: O=5220 C=5290 / 전일 15:30 C=5280  
  - **전일대비 C2C** `(5290-5280)/5280` = **0.19%** ← BT 탈락  
  - **당일 몸통** `(5290-5220)/5220` = **1.34%** ← 실매 `prevChg`와 **일치**
- 한울: 전일 15:58 C=11810 → 09:00 C=11530 → C2C **-2.37%**; 몸통 ≈ **0.70%** ≈ 실매 0.61%(RAM OHLC 미세차)

### 실매 journal 타임라인 (결정적)

1. `09:00:25` 333050 갭보정 REST 빈응답 → `09:00:34` **500봉 RAM 적재** (진행 중 **당일 09:00** 포함)
2. `09:00:34~09:01:01` 내내 `탈락-직전봉약세 prev=-1.14%`  
   - `(5220-5280)/5280 = -1.14%` → 갭보정이 넣은 **미완성 09:00(종가≈시가 5220)** 이 확정봉으로 잡힌 상태
3. `09:01:01` `봉강제확정` 09:00 C=5290
4. `09:01:02~` `탈락-저항미돌파` (직전봉 게이트 **통과**) → `09:01:06` `BREAKOUT-B ... prevChg=1.34%` 매수

한울도 동일 패턴: 장중 `prev=-2.96%`(갭/전일) → 09:00 확정 후 저항 대기 → `prevChg=0.61%`로 매수.

### 근본원인

**갭보정(REST ka10080)이 장중에 “아직 진행 중인 당일 1분봉”을 `is_confirmed`로 RAM에 넣음.**

- 실매 `check_buy` = `get_candles`(확정) + `get_current_candle`(진행) → **같은 09:00이 확정·진행 양쪽에** 있거나, 확정 체인이 **전일 종가 대신 시가(불완전봉 종가)** 를 `prev_prev`로 물음.
- 그 결과 직전봉 필터가 문서/코드 스펙(전전봉→직전봉 **종가대비**)이 아니라 **당일 첫 봉 몸통%** 에 가깝게 통과.
- BT는 DB 확정봉(+웜업 전일 종가)만 쓰므로 **0.19%/-2.37%로 올바르게 거름** → 장초 실매만 사는 괴리.

코드 위치: `kis_ws.merge_confirmed_bars` / `fill_gap_from_rest` (현재 분 제외 없음) + `breakout.check_buy` (`virtual = confirmed + forming`).

### 수정 방향 (승인 후 — 아직 미적용)

1. **갭보정 시 `candle_time >= 현재 진행 분` 봉은 confirmed merge 제외** (또는 `_current`만 갱신). 실매·BT 모두 전일 종가 C2C로 통일.
2. (선택) 매수 성공 로그에 `confirmed[-2/-1] candle_time/close` 남겨 재발 추적.
3. 스펙을 “직전봉 몸통%”로 바꾸려면 실매·웹·Optuna를 **한 세트**로 바꿔야 함 — 비권장(HTS/주석과 불일치).

**권장: 1번(진행분 REST 제외).** 핵심 매매 임계값 변경이 아니라 인프라 정합.

### 수정 적용 (2026-07-17)

- env: `WS_GAP_FILL_SKIP_INCOMPLETE_BUCKET` (기본 **true**, DB 시드)
- `fill_gap_from_rest` / `merge_confirmed_bars`: 진행 중 버킷(`>=` 현재 봉시작) **insert 금지 + RAM purge**
- 스모크: `scripts/smoke_candle_upsert_rollup.py` (진행분 skip/purge)
- **실매 반영: `kis_trader_main` 재시작 필요** (미재시작 시 기존 프로세스 구코드)
