AlgoLab Blog · 키움 REST API 실무 · 2026

키움 REST API 조건검색 — CNSRLST로 실시간 편입 받기

키움증권 · WebSocket 2026-08-08 · 약 8분 읽기 · 알고랩 AlgoLab
한 줄 요약 키움 REST API의 조건검색은 GET·POST 엔드포인트가 아니라 WebSocket 위에서 주고받는 JSON 메시지입니다. wss://api.kiwoom.com:10000/api/dostk/websocket에 접속한 뒤 순서는 넷입니다 — trnm: "LOGIN"으로 access_token 인증 → ② trnm: "CNSRLST"로 내 조건식 목록과 seq(일련번호)를 받고 → ③ trnm: "CNSRREQ"에 그 seq를 실어 검색 요청 → ④ search_type"0"이면 단발 조회, "1"이면 실시간 등록이라 편입·이탈이 계속 밀려 들어옵니다. 해제는 CNSRCLR. 그리고 서버가 보내는 trnm: "PING"을 그대로 되돌려 보내지 않으면 몇 분 뒤 세션이 조용히 끊어집니다 — 실시간이 멈추는 사고의 1순위 원인입니다.

키움 REST API 자동매매 완전 가이드에서 토큰·TR·WebSocket을 전반적으로 다뤘지만, 조건검색 하나만 검색해서 들어오는 분이 꾸준합니다. 이유는 분명합니다 — 시세 조회나 주문은 HTTP 요청 한 번으로 끝나는데, 조건검색만 유독 방식이 다르기 때문입니다. API 목록에서 ka10171을 찾아 놓고 "그래서 이걸 어느 주소로 부르지?"에서 막힙니다.

검색식을 만드는 것 자체와 "내가 만든 검색식이 자동 매수가 되는 원리"따로 정리한 글이 있습니다. 이 글은 그중 REST API로 구현하는 부분 하나만 다룹니다.

목차

  1. 왜 REST 호출이 아니라 WebSocket인가
  2. 4단계 흐름과 api-id 대응표
  3. 주고받는 JSON 실물
  4. 파이썬 구현 전문
  5. 자주 막히는 4가지
  6. 편입 신호를 주문으로 옮길 때

1. 왜 REST 호출이 아니라 WebSocket인가

조건검색은 결과가 한 번에 끝나지 않는 조회이기 때문입니다. 잔고 조회는 물어보면 답이 나오고 끝이지만, 조건검색은 장중 내내 종목이 들어오고 빠집니다. "거래량 급증"이라는 조건이라면 오전 9시 3분에 세 종목, 9시 12분에 두 종목이 더 붙는 식입니다.

이걸 REST로 하려면 봇이 몇 초마다 계속 물어봐야 하고, 그 순간 호출 제한(429)에 부딪힙니다. 그래서 키움은 조건검색을 서버가 밀어 주는 방식으로 설계했고, 그 통로가 WebSocket입니다.

구분주소
실전wss://api.kiwoom.com:10000/api/dostk/websocket
모의투자wss://mockapi.kiwoom.com:10000/api/dostk/websocket

모의투자는 KRX만 지원되는 것으로 안내되어 있습니다. 넥스트레이드(NXT)를 포함한 주문 라우팅까지 검증하려면 실전 환경이 필요합니다. 주소·포트·지원 범위는 정책에 따라 바뀔 수 있으니 키움증권 공식 개발자 문서에서 확인하십시오.

2. 4단계 흐름과 api-id 대응표

문서에서 보이는 ka10171~ka10174기능 번호이고, 실제 메시지에 실어 보내는 것은 trnm 값입니다. 이 둘이 따로 놀아서 헷갈립니다.

단계api-idtrnm하는 일
0LOGINaccess_token으로 세션 인증
1ka10171CNSRLST내 조건식 목록 + seq 받기
2ka10172CNSRREQ (search_type "0")단발 조회 — 지금 조건에 맞는 종목
3ka10173CNSRREQ (search_type "1")실시간 등록 — 편입·이탈 수신
4ka10174CNSRCLR실시간 해제
상시PING서버가 보냄 → 그대로 돌려보내야 함
내 봇 키움 서버 LOGIN { token: access_token } CNSRLST ← 조건식 목록 요청 data: [["0","거래량급증"], ["1","골든크로스"] ...] CNSRREQ seq:"1" search_type:"1" stex_tp:"K" 최초 결과 → 이후 편입·이탈이 계속 push PING → 그대로 되돌려 보내지 않으면 세션 종료
LOGIN → CNSRLST → CNSRREQ → push. PING은 전 구간에 걸쳐 계속 온다

3. 주고받는 JSON 실물

① 접속 직후 — LOGIN

access_tokenREST 쪽과 동일하게 /oauth2/token에서 받은 값을 씁니다. 조건검색용 별도 토큰이 있는 게 아닙니다.

// 보내는 것
{
  "trnm": "LOGIN",
  "token": "eyJhbGciOi...(access_token)"
}

// 돌아오는 것
{
  "trnm": "LOGIN",
  "return_code": 0,
  "return_msg": ""
}

return_code0이면 정상입니다. 0이 아니면 대개 토큰이 만료됐거나 appkey·appsecret으로 받은 토큰이 아닌 경우입니다.

② 조건식 목록 — CNSRLST

// 보내는 것
{ "trnm": "CNSRLST" }

// 돌아오는 것 (형태 예시)
{
  "trnm": "CNSRLST",
  "return_code": 0,
  "return_msg": "",
  "data": [
    ["0", "거래량급증"],
    ["1", "골든크로스"],
    ["2", "52주신고가"]
  ]
}

이 목록은 HTS에서 만든 조건식이 그대로 내려오는 것입니다. API로 조건식을 만들 수는 없습니다. 검색식 작성은 영웅문에서 하고, API는 그것을 불러다 쓰는 역할만 합니다. 목록이 비어 있다면 API 문제가 아니라 HTS에 저장된 조건식이 없거나 서버 저장이 안 된 것입니다.

③ 검색 요청 — CNSRREQ

// 실시간 등록 (search_type "1")
{
  "trnm": "CNSRREQ",
  "seq": "1",             // CNSRLST에서 받은 일련번호
  "search_type": "1",     // "0" = 단발 조회, "1" = 실시간 등록
  "stex_tp": "K",         // 거래소 구분
  "cont_yn": "N",         // 연속조회 여부
  "next_key": ""          // cont_yn 이 "Y"일 때 사용
}

cont_yn·next_key결과가 많아 한 번에 안 올 때 쓰는 연속조회 파라미터입니다. 응답에 이어받을 키가 있으면 cont_yn"Y"로, next_key에 그 값을 넣어 다시 요청합니다. 조건이 넓어 수백 종목이 잡히는 검색식이라면 첫 응답만 처리하고 끝내는 코드는 종목을 놓칩니다.

필드 값은 반드시 한 번 직접 찍어 보십시오. stex_tp의 허용값, 실시간 push 메시지의 필드명(종목코드가 어느 키에 들어오는지, 편입·이탈을 어떤 값으로 구분하는지)은 버전에 따라 달라질 수 있습니다. 아래 구현에서 원본 메시지를 그대로 로그로 남기게 해 둔 이유가 이것입니다 — 문서를 믿고 파서를 먼저 쓰는 것보다, 실제로 온 것을 보고 파서를 쓰는 편이 빠릅니다. 정확한 규격은 키움증권 공식 개발자 문서를 기준으로 확인하십시오.

4. 파이썬 구현 전문

websockets 라이브러리 기준입니다. 수신 루프의 첫 분기를 PING으로 두는 것이 이 코드의 핵심입니다.

import asyncio, json, websockets

WS_URL = "wss://api.kiwoom.com:10000/api/dostk/websocket"

class ConditionClient:
    def __init__(self, token: str):
        self.token = token
        self.ws = None
        self.conditions = {}      # {조건식이름: seq}
        self.registered = None    # 재접속 시 다시 등록할 seq

    async def connect(self):
        self.ws = await websockets.connect(WS_URL)
        await self.send({"trnm": "LOGIN", "token": self.token})

    async def send(self, payload: dict):
        await self.ws.send(json.dumps(payload))

    async def run(self):
        await self.connect()
        async for raw in self.ws:
            msg = json.loads(raw)
            trnm = msg.get("trnm")

            # ── 1순위: PING을 그대로 되돌려 보낸다 (안 하면 세션 종료) ──
            if trnm == "PING":
                await self.ws.send(raw)
                continue

            if trnm == "LOGIN":
                if msg.get("return_code") != 0:
                    raise RuntimeError(f"로그인 실패: {msg.get('return_msg')}")
                await self.send({"trnm": "CNSRLST"})

            elif trnm == "CNSRLST":
                # data = [[seq, 조건식이름], ...]
                self.conditions = {row[1]: row[0] for row in msg.get("data", [])}
                print("조건식 목록:", self.conditions)
                await self.register("거래량급증")     # 이름으로 지목 (번호 하드코딩 금지)

            elif trnm == "CNSRREQ":
                print("검색 결과 원본:", raw[:500])   # 구조 확인용 — 파서는 이걸 보고 쓴다
                await self.on_result(msg)

            else:
                print("기타 수신:", raw[:300])        # 실시간 push도 여기서 먼저 눈으로 본다

    async def register(self, name: str):
        seq = self.conditions.get(name)
        if seq is None:
            raise KeyError(f"조건식 '{name}' 없음 — HTS에 저장돼 있는지 확인")
        self.registered = seq
        await self.send({
            "trnm": "CNSRREQ", "seq": seq,
            "search_type": "1",        # 실시간
            "stex_tp": "K",
            "cont_yn": "N", "next_key": "",
        })

    async def unregister(self):
        if self.registered:
            await self.send({"trnm": "CNSRCLR", "seq": self.registered})

    async def on_result(self, msg: dict):
        ...   # 6장 참조 — 여기서 바로 주문하지 말 것

async def main(token):
    client = ConditionClient(token)
    while True:                                   # 끊기면 다시 붙고 다시 등록
        try:
            await client.run()
        except Exception as e:
            print("세션 끊김, 5초 후 재접속:", e)
            await asyncio.sleep(5)

재접속 루프가 왜 필수인가. 실시간 등록은 세션에 붙어 있는 상태입니다. 네트워크가 순간 끊기면 등록도 함께 사라지고, 봇은 살아 있는데 신호만 안 오는 가장 알아채기 어려운 고장이 됩니다. 위처럼 재접속 후 CNSRLSTregister()가 자동으로 다시 돌게 해 두고, 별도로 "N분 동안 아무 메시지도 안 왔으면 경고" 감시를 붙이면 확실합니다.

5. 자주 막히는 4가지

증상진짜 원인대응
몇 분 뒤 조용히 끊김PING 미응답수신 루프 첫 분기에서 받은 메시지 그대로 회신
어제 되던 조건식이 안 잡힘seq 하드코딩 + HTS에서 조건식 추가·삭제매 실행마다 CNSRLST 조회 후 이름으로 번호 조회
결과가 한 번만 오고 끝search_type"0"으로 보냄실시간이 필요하면 "1"
종목이 일부만 잡힘연속조회 미처리응답에 이어받을 키가 있으면 cont_yn: "Y"로 재요청

네 번째는 특히 조건식이 넓을수록 티가 납니다. 백테스트에서는 200종목이 잡히던 조건이 실전에서는 매번 앞쪽 몇십 개만 들어오는 식이라, 전략이 틀린 게 아니라 데이터가 잘린 것인데 원인을 전략에서 찾게 됩니다.

🛠️
조건검색은 붙였는데 그다음이 막힌다면

편입 신호를 주문으로 옮기는 구간이 실제로는 가장 손이 많이 갑니다. 중복 차단·수량 산정·호출 제한까지 포함해 설계해 드립니다.

자동매매 제작 상담하기 →

6. 편입 신호를 주문으로 옮길 때

여기가 진짜 어려운 구간입니다. 편입 = 매수로 바로 연결하면 거의 반드시 사고가 납니다.

그래서 최소한 이 세 겹은 사이에 둡니다.

bought = set()                 # ① 이미 산 종목은 다시 안 산다
MAX_POSITIONS = 5              # ② 동시 보유 종목 수 상한
queue = asyncio.Queue()        # ③ 신호를 큐에 넣고 정해진 속도로만 주문

async def on_signal(code: str):
    if code in bought or len(bought) >= MAX_POSITIONS:
        return
    bought.add(code)
    await queue.put(code)

async def order_worker():
    while True:
        code = await queue.get()
        # 여기서 키움 주문 REST 호출 (/api/dostk/ordr)
        # 응답의 return_code 를 반드시 확인하고, 실패 시 bought 에서 제거
        await asyncio.sleep(1.0)     # 초당 1건으로 유량 고정

주문 실패를 성공으로 착각하지 마십시오. 위 코드에서 bought.add()를 주문 전에 한 이유는 중복 진입을 막기 위해서인데, 주문이 실패했다면 반드시 되돌려야 합니다. 그러지 않으면 사지도 않은 종목이 "보유 중"으로 남아 그 자리를 계속 차지합니다. 그리고 전체를 멈추는 킬 스위치는 조건검색 봇에서 특히 중요합니다 — 검색식 하나를 잘못 고치면 수백 종목이 한꺼번에 편입될 수 있습니다.

종목코드를 조건검색이 아니라 직접 관리하고 싶다면, 코스피·코스닥 전 종목 목록을 받는 방법은 종목코드 마스터 파일 파싱에 따로 정리해 두었습니다.

자주 묻는 질문

키움 REST API 조건검색은 왜 GET으로 호출이 안 되나요?

조건검색은 결과가 계속 갱신되는 조회WebSocket 위에서 동작하도록 설계돼 있습니다. wss://api.kiwoom.com:10000/api/dostk/websocket에 접속한 뒤 trnmCNSRLST·CNSRREQ인 JSON을 보내는 방식입니다.

CNSRLST를 먼저 호출해야 하는 이유는?

조건식을 이름이 아니라 일련번호(seq)로 지목하기 때문입니다. 번호는 조건식을 추가·삭제하면 달라질 수 있으므로 하드코딩하지 말고 매번 목록을 받아 이름으로 찾으십시오.

search_type 0과 1은 무엇이 다른가요?

"0"은 지금 조건에 맞는 종목을 한 번 받고 끝, "1"실시간 등록이라 편입·이탈이 계속 들어옵니다. 해제는 CNSRCLR입니다.

WebSocket이 몇 분 만에 끊어집니다.

서버가 보내는 trnm: "PING"에 응답하지 않은 경우가 대부분입니다. 받은 메시지를 그대로 되돌려 보내십시오. 그와 별개로 재접속·재등록 로직은 반드시 필요합니다.

조건검색으로 잡힌 종목을 바로 주문에 연결해도 되나요?

권하지 않습니다. 중복 차단·동시 보유 상한·주문 큐 세 겹을 사이에 두십시오. 장 시작 직후 수십 종목이 한꺼번에 편입되는 상황을 전제로 설계해야 합니다.

확인 캐치. 이 글의 주소·trnm 값·api-id2026년 8월 8일 기준 키움증권 공식 개발자 가이드와 공개된 구현 사례를 대조해 정리했습니다. 파라미터 허용값과 실시간 메시지의 필드명은 개정될 수 있으므로, 실제 구현 전 키움증권 공식 문서에서 확인하시고 원본 메시지를 한 번 찍어 보시기 바랍니다. 이 글은 기술 자료이며 특정 종목 추천이나 투자 권유가 아닙니다.

마무리

LOGINCNSRLSTCNSRREQ, 그리고 PING 회신. 조건검색 연동에서 실제로 새로 배워야 하는 건 이 넷뿐입니다. 나머지는 키움 REST API의 일반적인 규칙과 같습니다.

어려운 쪽은 오히려 그 신호를 어떻게 주문으로 바꾸느냐입니다. 검색식은 HTS에서 클릭 몇 번이면 만들어지지만, 그 결과를 중복 없이·감당 가능한 속도로·실패를 실패로 처리하며 주문에 옮기는 부분이 봇 개발의 실제 분량입니다.

조건검색 봇, 신호부터 주문까지 통째로

검색식 연동·중복 차단·주문 유량 관리까지 포함해 제작합니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기