# 캔들 수집·저장·매매 흐름 (스캘핑 vs 꼬리잡기)

## 1. 어떤 봉이 쌓이나? (전략별)

| 전략 | CandleAggregator timeframes | 주전략 봉 | 비고 |
|------|-----------------------------|-----------|------|
| **스캘핑** (kis_scalping_ver1/ver2) | [1, 3, 15, 60] | 1분봉 (SCALP_CANDLE_TIMEFRAME=1) | 4분봉 없음 |
| **꼬리잡기** (kis_short_ver2/ver3) | [3, 15, 60] | 3분봉 | 1분봉·4분봉 없음 |

- **4분봉**은 어디에도 사용하지 않음. 1분 / 3분 / 15분 / 60분만 집계·저장.
- 스캘핑은 1분봉 중심, 꼬리잡기는 3분봉만 매수 신호에 사용.

---

## 2. 메모리 vs DB — 전략별 PK 여부

- **ws_candles** 테이블은 **전략별 PK가 없음**.  
  `UNIQUE KEY (code, timeframe, candle_time)` 하나로  
  스캘핑이 넣은 1·3·15·60분봉과 꼬리잡기가 넣은 3·15·60분봉이 **같은 테이블**에 쌓임.
- **꼬리잡기**도 3분봉을 **메모리(CandleAggregator)** 에 먼저 쌓고,  
  봉이 **확정될 때마다** Queue → 백그라운드 스레드가 **ws_candles에 배치 INSERT**.

흐름 요약:

1. **트랙 1 (매매 두뇌)**  
   WebSocket 틱 → `on_tick()` → **RAM**에서만 OHLCV 갱신 →  
   매수/매도 판단은 `get_candles()` / `get_latest_confirmed()` 등 **메모리만** 사용 (DB 대기 없음).
2. **트랙 2 (기록)**  
   봉 **확정** 시점에만 dict를 Queue에 `put_nowait()` →  
   `_db_writer` 스레드가 BATCH_SIZE(50)개 또는 FLUSH_INTERVAL(2초)마다 **ws_candles**에 배치 INSERT.  
   → **봉 모을 때까지 기다리지 않고**, 확정되는 대로 메모리에 반영되고, DB는 그 뒤에 비동기로 저장.

---

## 3. 매수 시 캔들 조회 — 세 가지 분기 (DB / 메모리 / 키움·KIS REST)

꼬리잡기 매수 신호(`check_buy_signal_tail_catch`)에서 3분봉을 가져오는 순서:

| 순서 | 경로 | 설명 |
|------|------|------|
| 1 | **메모리** | `candle_agg.get_candles(code, 3, 50)` — 확정봉만, DB/네트워크 없음 |
| 2 | **DB** | 메모리에 부족하면 `db.get_ws_candles(code, 3, limit=50, confirmed_only=True)` |
| 3 | **REST** | 그래도 없으면 `_get_candles_df()` → 실패 시 `client.get_minute_chart(code, period="3", limit=20)` (KIS). 갭보정은 키움 우선, 없으면 KIS. |

- **매수가 확정봉일 때만 가능**하도록 되어 있음:  
  - 1·2번은 애초에 확정봉만 반환.  
  - 3번(REST)에서 가져온 DataFrame은 **마지막 행(진행봉) 제거** `df.iloc[:-1]` 후 신호 판단에 사용.

위 분기는 **kis_short_ver3.py** `check_buy_signal_tail_catch()` 안에 그대로 구현되어 있음 (메모리 → DB → REST 순).

---

## 4. “캔들 넣고 바로 매매” — 현재 동작

- 봉이 **확정되는 순간** 이미 **RAM(_confirmed)** 에 들어가므로,  
  매수 루프는 **DB 쓰기 완료를 기다리지 않고** 바로 그 데이터로 신호 판단.
- DB는 **그 뒤에** 배치로 저장되므로, “캔들 DB에 넣고 나서 매매”를 **기다릴 필요 없음**.  
  다만 **봇 기동 직후**에는 메모리에 봉이 없을 수 있음 →  
  `_fill_all_gaps()`에서 키움(우선) 또는 KIS REST로 과거 봉을 가져와 `fill_gap_from_rest()`로  
  **RAM + Queue(→ DB)** 에 채움. 이 갭보정이 끝나면 해당 종목은 바로 매수 체크 가능.

정리: **봉 모을 때까지 기다리지 않고**, 확정봉이 메모리에 들어오는 즉시 매매 로직에 쓰이고, DB는 비동기로 쌓인다.
