  - id: read-path-parity
    content: "실매/Optuna 읽기: 메인 WS(키움 또는 KIS) 우선, 없는 분만 kiwoom+rest(키움 갭보정). rest는 ws_candles 테이블 행이지 별도 테이블 아님"
    status: pending

  - id: read-path-parity
    content: "실매/Optuna 기본=키움 ws→rest구멍. CANDLE_SOURCE=kis는 kis+ws만(빈 분은 스킵, 키움 REST로 안 메움)"
    status: pending

- **KIS REST 갭보정은 쓰지 않는다.** 페이지 조회라 너무 느림. 갭보정 API는 **항상 키움 ka10080**. 결과는 `ws_candles`에 `source=kiwoom, channel=rest`로 넣는다 (별도 rest 테이블 없음).
- 틱 메인(키움/KIS)과 갭보정(키움 REST)은 **다른 층**. 실매가 KIS 메인이어도 구멍은 키움 REST로 메움. Optuna `CANDLE_SOURCE=kis`도 동일.

- **KIS REST 갭보정은 쓰지 않는다.** 페이지 조회라 너무 느림. `WS_GAP_FILL_KIS_FALLBACK` 는 기본 OFF로 두고, 설계·주석·UI에서 “켜도 실매 경로로 쓰지 않음 / 키움 ka10080만” 으로 못 박는다. KIS 시리즈는 **WS 틱 집계만** (구멍은 비워 둠).

```mermaid
flowchart LR
  subgraph write [저장_테이블은_전부_ws_candles]
    KW_WS[키움_WS틱]
    KIS_WS[KIS_WS틱]
    KW_REST[키움_REST_ka10080]
    KW_WS --> R1["행 source=kiwoom channel=ws"]
    KIS_WS --> R2["행 source=kis channel=ws"]
    KW_REST --> R3["행 source=kiwoom channel=rest"]
  end
  subgraph read [읽기_실매와_Optuna_동일]
    R2 --> KIS_MAIN[CANDLE_SOURCE_kis]
    R3 --> KIS_MAIN
    R1 --> KW_MAIN[기본_키움메인]
    R3 --> KW_MAIN
  end
```

```mermaid
flowchart LR
  subgraph liveSig [실매_신호]
    KW_WS[키움_WS틱]
    KW_REST[키움_REST_갭보정]
    KW_WS --> KW_BAR[ws_candles_source_kiwoom]
    KW_REST -->|"구멍만 freeze"| KW_BAR
  end
  subgraph liveFill [실매_체결]
    KIS_WS[KIS_WS틱]
    KIS_WS --> KIS_BAR[ws_candles_source_kis]
  end
  subgraph permLS [영구구독_LS]
    LS_WS[LS_WS]
    LS_WS -->|"영구만 DB"| LS_TICK[ls_ws_ticks]
    LS_WS -->|"후보 spill"| RAM[RAM만_폴백]
    BTN[탭_수동적재_버튼]
    BTN -->|"키움_확정봉_on_demand"| KW_BAR
  end
  Optuna -->|기본_키움_ws후rest구멍| KW_BAR
  Optuna -->|CANDLE_SOURCE_kis_WS만_구멍스킵| KIS_BAR
```

- 갭보정 **쓰기**: `fill_gap_from_rest` → **같은 테이블 `ws_candles`**, 행 라벨만 `source=kiwoom` + `channel=rest`. `kis+rest` 행을 만들지 않음. rest 전용 테이블 없음.
- freeze: 그 증권사 `channel=ws`가 이미 있으면 rest는 **그 증권사 칸을 안 덮음**. rest 행은 “WS가 없었던 분”용 INSERT.
- 읽기 (실매 = Optuna, [bt_candle_source.py](kis_trader/backtest/bt_candle_source.py)):
  - **틱 메인과 갭보정은 분리.** 메인이 키움이든 KIS든, 구멍은 **키움 REST 행**으로 메움.
  - `LIVE_TICK_PROVIDER=kiwoom` / `CANDLE_SOURCE` 빈값 → 분마다 `kiwoom+ws` 있으면 그것, **없으면** `kiwoom+rest`.
  - `CANDLE_SOURCE=kis` (실매 KIS 메인과 같게) → 분마다 `kis+ws` 있으면 그것, **없으면** `kiwoom+rest`. 빈 분을 스킵하지 않고 키움 갭보정으로 채움 (실매와 동일).
  - 키움 WS 봉을 KIS 메인 경로의 1순위로는 쓰지 않음 (메인은 고른 증권사 WS). 갭보정만 키움 REST.

- 갭보정 쓰기: [kis_trader/ws/kis_ws.py](kis_trader/ws/kis_ws.py) `fill_gap_from_rest` 가 넣는 source를 `kiwoom` + `channel=rest` 로. **KIS 시리즈에 키움 REST를 넣지 않음.**
- freeze: **같은 (source, channel) 또는 같은 source의 ws가 이미 있으면 rest는 그 칸을 안 덮음.** 구멍(그 source에 그 분이 없음)만 INSERT.
- 읽기 (실매 RAM 병합 + Optuna [bt_candle_source.py](kis_trader/backtest/bt_candle_source.py)):
  - 메인 `LIVE_TICK_PROVIDER=kiwoom` / `CANDLE_SOURCE` 빈값 → 분마다 **kiwoom+ws 우선, 없으면 kiwoom+rest**.
  - `CANDLE_SOURCE=kis` → **kis+ws만**. rest로 메우지 않음. 그 분에 행이 없으면 백테는 그 분을 **검사하지 않음**(웜업 봉수 미달이면 매수 스킵). 키움 REST를 KIS 시리즈에 섞지 않음.

- 영구구독 탭에 **「확정봉 가져오기」(버튼)** : 키움 ka10080 1회 → **`ws_candles` 테이블**에 `source=kiwoom, channel=rest` INSERT-only. (`ls_ws_ticks` / `ls_ws_candles` 아님. rest 전용 테이블도 아님.)

- 영구구독 탭에 **「확정봉 가져오기」(버튼)** 하나: 선택/전체 enabled KR에 대해 키움 REST를 **그때 1회** `ws_candles` (`kiwoom`+`rest`)에 INSERT-only. 기존 해외 US 적재 API와 같은 “눌러야 함” 모델. 실매 루프·갭 워커와 분리.

## B2. Optuna를 KIS로 돌려도 갭보정은 키움

실매: 틱 메인 = KIS여도 **봉 구멍은 키움 REST**. Optuna `CANDLE_SOURCE=kis`도 그 규칙을 복제한다.

| 그 분에 있는 행 | KIS 메인(실매/Optuna)이 쓰는 봉 |
|---|---|
| `kis` + `ws` | 그것 (KIS 틱 집계) |
| kis ws 없음, `kiwoom` + `rest` 있음 | 그것 (키움 갭보정) |
| 둘 다 없음 | 그 분 없음 (스킵) |

저장은 안 섞음: REST 결과는 항상 `kiwoom+rest` 행. 읽기 때만 KIS 메인이 그 행을 **구멍 메우기용으로 조회**.

이전에 적었던 “KIS Optuna는 rest 없이 거래가 적어진다”는 **실매와 불일치**라서 폐기. 공정 재현은 “KIS WS + 키움 REST 구멍”이다.

## B2. Optuna를 KIS로 돌리면 (rest 없음)

KIS 시리즈에는 갭보정이 없다 = **그 증권사가 그날 WS로 남긴 분만** 시험하는 것.

- `fetch_ws_candles_for_code`는 DB에 있는 행만 시간순으로 줌. 빈 분을 만들어 채우지 않음.
- 그 분에 `kis+ws`가 없으면 → 그 분 신호 없음. RSI 등 lookback이 모자라면 매수 스킵(지금과 동일 “봉 부족”).
- 그래서 KIS Optuna는 키움보다 **거래 수가 적을 수 있음** (KIS 구독 한도·미구독 구간). 그게 버그가 아니라 “KIS WS만으로 실매했을 때” 재현.
- 키움 rest로 KIS 구멍을 메우면 증권사 시리즈가 다시 섞여서, 이중 저장을 하는 이유가 사라짐.

비교할 때: 기본(키움 ws+rest구멍) vs `CANDLE_SOURCE=kis`(WS only). 공정 비교가 아니라 **피드 차이 비교**로 읽는다.

- 키움 REST로 **KIS 행을 덮거나** `source=kis channel=rest`를 만들지 않음 (라벨 혼동). 읽기 구멍 메우기는 `kiwoom+rest` 조회.

- 키움 REST로 KIS 분봉 메우기

- **실매 신호**: 고른 틱 메인의 `+ws`, 없으면 `kiwoom+rest`.
- **백테/Optuna 기본**: 키움 메인과 같게 (`kiwoom+ws` → `kiwoom+rest`).
- **`CANDLE_SOURCE=kis`**: 실매 KIS 메인과 같게 (`kis+ws` → `kiwoom+rest`). 구멍을 비워 두지 않음.

- **백테/Optuna 기본**: 같은 키움 시리즈(ws→rest 구멍).
- **`CANDLE_SOURCE=kis`**: KIS WS 행만. 없는 분은 스킵. 키움 REST로 안 채움.


StrReplace

overview: 키움/KIS WS 분봉은 ws_candles에 증권사별로 따로 저장하고, 키움 REST 갭보정은 같은 테이블에 channel=rest로 넣는다. 읽기는 틱 메인 WS 우선·구멍은 항상 키움 rest. 영구구독은 LS 틱만 자동, 확정봉은 탭 버튼.

overview: 키움/KIS 분봉을 증권사별로 따로 저장하고(갭보정은 키움 REST만·키움 시리즈 구멍만), 영구구독은 LS 고정+마스터 스위치+LS DB는 영구구독만 쌓게 한다. KIS REST 갭보정은 설계에서 제외한다.


맞습니다. 제가 Optuna KIS를 “구멍 방치”로 적은 게 실매랑 안 맞았습니다. 계획에서 고쳤습니다.

### rest는 어느 테이블?

**새 테이블 없습니다.** 전부 [`ws_candles`](database.py)입니다.

| 넣는 것 | 테이블 | 행 라벨 |
|---|---|---|
| 키움 틱으로 만든 1분봉 | `ws_candles` | `source=kiwoom`, `channel=ws` |
| KIS 틱으로 만든 1분봉 | `ws_candles` | `source=kis`, `channel=ws` |
| 키움 갭보정 / 「확정봉 가져오기」 | **같은 `ws_candles`** | `source=kiwoom`, `channel=rest` |
| 영구구독 평소 틱 | `ls_ws_ticks` | LS 전용 |

`channel=rest`는 컬럼 값이지, rest 테이블이 아닙니다.

영구구독 버튼도 키움 ka10080 → **`ws_candles`에 `kiwoom+rest` 한 줄**. LS 틱 테이블이 아닙니다.

---

### 왜 KIS 메인이어도 갭보정은 키움인데, Optuna만 구멍을 비우나? → 그렇게 하면 안 됨

층이 둘입니다.

- **틱 메인**: 키움이든 KIS든, 그 증권사 **WS로 만든 봉**을 먼저 씀
- **갭보정**: 메인이 누구든 **키움 REST(ka10080)** 로 빈 분만 채움. KIS REST는 안 씀

실매가 KIS 메인이면: 그 분에 KIS WS 봉이 있으면 그걸 쓰고, **없으면 키움 REST 봉**을 씁니다. Optuna `CANDLE_SOURCE=kis`도 **똑같이** 가야 정합입니다.

저장은 안 섞습니다. REST 결과는 항상 `kiwoom + rest` **행**으로만 넣고, KIS 메인은 읽을 때 그 행을 **구멍용으로만** 봅니다.

| 그 분 | KIS 메인 실매 / Optuna |
|---|---|
| `kis+ws` 있음 | 그거 |
| 없고 `kiwoom+rest` 있음 | 그거 (키움 갭보정) |
| 둘 다 없음 | 그때만 스킵 |

이전에 “KIS Optuna는 거래가 적어진다”는 **틀린 설명**이었습니다. 폐기했습니다.


<timestamp>Thursday, Aug 13, 2026, 10:12 PM (UTC+9)</timestamp>
<user_query>
마지막 하나 더 ls 영구구독 갭보정 버튼이 키움rest인데 이건 ls에 저장해야되는게 아닌가? 따로 저기에 저장하면 또 영구구독 섞이고 일단 ls쪽 백테할때 쓸데없이 조인해야되지 않나?
</user_query>


맞습니다. 영구구독 확정봉은 `ws_candles`가 아니라 LS 테이블로 넣는 쪽이 섞임·조인을 피할 수 있습니다. 스키마만 확인하고 계획에 반영하겠습니다.


  - id: gapfill-kiwoom-only
    content: 후보/보유 자동 갭보정만 ws_candles(kiwoom+rest). 영구구독 버튼은 ls_ws_candles 구멍만(ws_candles 금지)
    status: pending

  - id: gapfill-kiwoom-only
    content: 후보/보유만 키움 REST 자동 갭보정. 영구구독은 자동 제외, 탭 버튼으로 확정봉 수동 적재. KIS REST 금지
    status: pending

- 영구구독 KR은 **LS WS 고정**. 틱·분봉 백테는 `ls_ws_ticks` / `ls_ws_candles`만. 키움 REST로 가져온 확정봉도 **`ls_ws_candles`에 구멍만** 넣는다 (`ws_candles`에 영구구독 섞지 않음).

- 영구구독 KR은 **LS WS 고정** (길게 보는 종목, 키움/KIS 슬롯 안 씀). 후보 spill 용 LS와 **구독은 같이** 탈 수 있지만 **DB 적재는 영구구독만**.

- 영구구독 탭 **「확정봉 가져오기」**: API는 키움 ka10080이지만 **저장은 `ls_ws_candles`**. `ws_candles`에 넣지 않음 (후보·키움/KIS 매매 시리즈와 섞임 + LS 백테가 `ws_candles` 조인해야 함).
  - 이미 LS WS로 쌓인 `(code, datetime, tf_min)` 있으면 **덮지 않음** (구멍만 INSERT). 현재 `upsert_ls_ws_candle`의 ON DUPLICATE UPDATE는 이 버튼 경로에서 쓰지 않음.
  - LS 백테(`history_source=ls`, [ls_history_loaders.py](kis_trader/backtest/ls_history_loaders.py))는 계속 `ls_ws_candles`만 읽으면 됨.

- 영구구독 탭에 **「확정봉 가져오기」(버튼)** : 키움 ka10080 1회 → **`ws_candles` 테이블**에 `source=kiwoom, channel=rest` INSERT-only. (`ls_ws_ticks` / `ls_ws_candles` 아님. rest 전용 테이블도 아님.)

- **영구구독**: 마스터 스위치, LS 틱만 자동 DB. 확정봉 버튼 → **`ls_ws_candles` 구멍**. `ws_candles` 비사용.

- **영구구독**: 구독 마스터 OFF 가능, 켜면 LS 틱만 DB. 자동 갭보정 없음. 확정봉은 탭 버튼.

- 영구구독 확정봉을 `ws_candles`에 넣기 (LS 백테 조인·시리즈 혼입)

- 영구구독을 장중 자동 `_gap_refill_codes`에 넣기 (버튼만)

overview: 키움/KIS WS 분봉은 ws_candles에 증권사별로 저장하고, 키움 REST 갭보정은 같은 테이블 channel=rest(후보·보유). 영구구독은 LS 테이블만(틱 자동, 확정봉 버튼은 ls_ws_candles 구멍). 읽기는 틱 메인 WS 우선·구멍은 키움 rest.

overview: 키움/KIS WS 분봉은 ws_candles에 증권사별로 따로 저장하고, 키움 REST 갭보정은 같은 테이블에 channel=rest로 넣는다. 읽기는 틱 메인 WS 우선·구멍은 항상 키움 rest. 영구구독은 LS 틱만 자동, 확정봉은 탭 버튼.


맞습니다. **키움 REST로 가져오더라도, 영구구독 봉은 LS 쪽 공책에 넣어야** 합니다.

| | 후보·보유 (키움/KIS 매매) | 영구구독 (LS) |
|---|---|---|
| 틱 | `ws_ticks` | `ls_ws_ticks` |
| 갭보정/확정봉 버튼 | `ws_candles` (`kiwoom`+`rest`) | **`ls_ws_candles`** |
| 백테 | `ws_candles`만 | `ls_ws_candles`만 (`history_source=ls`) |

`ws_candles`에 넣으면 삼성전자 같은 영구종목이 후보 시리즈에 섞이고, LS 백테할 때 `ws_candles`를 또 조인해야 합니다. 그건 낭비입니다.

버튼: API만 키움 ka10080, **저장은 `ls_ws_candles`**. 이미 LS WS로 있는 분은 **덮지 않고 구멍만**. LS 백테는 계속 `ls_ws_candles` 한 테이블만 보면 됩니다.

`ls_ws_candles`에는 source/channel이 없어서, 키움에서 가져온 숫자도 LS 봉처럼 보입니다. 영구구독 시리즈의 “빈칸 채움”으로 쓰는 거라 백테 조인은 필요 없습니다. 나중에 “이건 REST로 메운 칸”을 구분하고 싶으면 그때 `channel`을 LS 테이블에만 추가하면 됩니다. 지금은 한 테이블·구멍만으로 충분합니다.