한국투자증권 KIS API로 실시간 시세 받기 (WebSocket 실전)
주식 자동매매 봇의 심장은 실시간 시세입니다. REST 폴링만으로는 초 단위 시그널을 만들 수 없습니다. 한국투자증권(KIS) 오픈API는 WebSocket으로 호가와 체결을 밀어주는데, 실전에서 부딪히는 지점들을 자동매매 봇을 만들며 정리한 노트입니다.
⚠️ 이 글은 교육·정보 제공 목적입니다. 투자 권유가 아니며, 투자 손실 책임은 본인에게 있습니다.
두 개의 TR을 구독한다
KIS 실시간에서 자주 쓰는 TR은 두 가지입니다.
- H0STASP0 — 실시간 호가 (매수·매도 1~10호가)
- H0STCNT0 — 실시간 체결가 (현재가, 시가, 전일종가, 누적거래량)
시가 기준 전략을 쓴다면 H0STCNT0이 필수입니다. 체결 메시지 안에 시가(stck_oprc)가 들어 있어서, 장 시작 첫 체결로 그날의 기준선을 잡을 수 있기 때문입니다.
종목당 두 TR을 구독하므로, 종목이 늘어나면 구독 건수가 빠르게 증가합니다. KIS는 한 연결에 동시 구독 한도(수십 건)가 있으니, 중복 종목은 seen 맵으로 걸러 한 번만 구독합니다.
seen := make(map[string]bool)
addSym := func(code string) {
if code == "" || seen[code] {
return // 빈 코드·중복은 skip
}
seen[code] = true
subscribeKIS(conn, approvalKey, "H0STASP0", code) // 호가
time.Sleep(50 * time.Millisecond)
subscribeKIS(conn, approvalKey, "H0STCNT0", code) // 체결
time.Sleep(50 * time.Millisecond)
}
구독 사이에 짧은 sleep을 두는 이유는, 여러 구독 프레임을 한꺼번에 몰아 보내면 서버가 일부를 놓치는 경우가 있어서입니다.
seen := make(map[string]bool)
addSym := func(code string) {
if code == "" || seen[code] {
return // 빈 코드·중복은 skip
}
seen[code] = true
subscribeKIS(conn, approvalKey, "H0STASP0", code) // 호가
time.Sleep(50 * time.Millisecond)
subscribeKIS(conn, approvalKey, "H0STCNT0", code) // 체결
time.Sleep(50 * time.Millisecond)
}
구독 사이에 짧은 sleep을 두는 이유는, 여러 구독 프레임을 한꺼번에 몰아 보내면 서버가 일부를 놓치는 경우가 있어서입니다.
틱 파싱: 인덱스는 반드시 raw 로그로 검증
체결 메시지는 ^로 구분된 필드 배열입니다. 문제는 필드 인덱스입니다. 공시 문서만 믿고 상수를 박아두면, 실제 메시지 형식이 조금만 달라도 엉뚱한 값을 시가로 잡습니다. 그래서 저는 항상 첫 수신 메시지 1건을 통째로 로그로 남긴 뒤 인덱스를 확정합니다.
func parseKISTick(msg []byte) {
fields := splitAfterThirdPipe(msg, '^')
if len(fields) < 14 {
return
}
symbol := strings.TrimSpace(string(fields[0]))
if len(symbol) != 6 {
return
}
price, _ := strconv.ParseFloat(string(fields[2]), 64) // 체결가
openPrice, _ := strconv.ParseFloat(string(fields[10]), 64) // 시가
prevClose, _ := strconv.ParseFloat(string(fields[13]), 64) // 전일종가
if price <= 0 {
return // 이상값 방어
}
// realtimeTicks[symbol] 갱신 후 시그널 트리거
}
price <= 0 같은 방어 코드는 사소해 보여도, 개장 직후나 데이터 이상 시 봇 전체를 지키는 안전벨트입니다.
헬스 모니터와 재연결
WebSocket은 반드시 끊깁니다. 서버 점검, 네트워크 순단, 방화벽… 원인은 다양합니다. 그래서 "마지막 수신 시각"을 atomic으로 계속 갱신하고, 두 단계로 감시합니다.
- 정규장 중 30초 이상 무수신 →
WsError플래그를 세워 대시보드·알림에 노출 - 60초 이상 무수신 →
conn.Close()로 강제 종료하고 재연결 루프 진입
핵심은 "느려짐"과 "죽음"을 구분하는 것입니다. 30초는 경고, 60초는 재연결 — 이 이중 임계값 덕분에 잠깐의 지연에는 과민 반응하지 않으면서도 진짜 끊김은 놓치지 않습니다.
정리
실시간 시세 파이프라인의 실전 체크리스트입니다.
- 필요한 TR(H0STASP0/H0STCNT0)만 구독하고 중복은 seen 맵으로 제거
- 필드 인덱스는 실제 raw 로그로 검증 후 상수화
- 모든 파싱 경로에 0/음수 방어
- 30초 경고 / 60초 재연결의 이중 헬스 모니터
시세가 안정적으로 흐르면, 그 위에 얹는 전략은 오히려 단순해집니다. 다음 글에서는 이 데이터로 만드는 레버리지·인버스 ETF 갭 전략을 다룹니다.
댓글
댓글 쓰기