AlgoLab Blog · 키움증권 실무 · 2026

키움 OpenAPI+ → REST 마이그레이션 — 바뀌는 5곳

키움 · 마이그레이션 2026-09-24 · 약 7분 읽기 · 알고랩 AlgoLab
한 줄 요약

키움은 OPT10081 ↔ ka10081 같은 공식 TR 대응표를 내놓지 않았습니다. 그래서 마이그레이션은 이름 바꾸기가 아니라 구조 교체입니다. 코드가 실제로 바뀌는 곳은 다섯 군데 — ① 로그인 CommConnect·OnEventConnect → 접근토큰 발급(au10001, /oauth2/token) ② 요청 SetInputValue+CommRqData → api-id 헤더 + JSON 본문 ③ 응답 OnReceiveTrData+GetCommData+GetRepeatCnt → HTTP 응답 JSON 배열 ④ 연속조회 nPrevNext 0/2 → cont-yn·next-key 헤더 ⑤ 실시간 SetRealReg+GetCommRealData → WebSocket trnm: "REG". 대응표 대신 쓸 수 있는 것은 공식 저장소 examples/ 예제 362개가 파일 머리에 달아 둔 api_id·api_name·api_url 주석이고, 그대로 살아남는 것은 9203·913·10 같은 FID 번호입니다.

이 글은 키움 REST API 자동매매 완전 가이드에서 기존 봇을 옮기는 문제 하나만 떼어 낸 글입니다. 키 발급·토큰·TR limit 같은 전체 그림은 그쪽을 보시고, 여기서는 “내 OnReceiveTrData 코드는 어디로 가나” 만 다룹니다.

목차

  1. 공식 대응표가 없다 — 대신 쓸 것
  2. ① 로그인 — 영웅문 창이 사라진다
  3. ② 요청 — SetInputValue 가 JSON 키가 된다
  4. ③ 응답 — 콜백 대기가 리턴으로 바뀐다
  5. ④ 연속조회 — nPrevNext 에서 next-key 로
  6. ⑤ 실시간 — FID 번호는 살아남는다
  7. 사라지는 것 · 새로 생기는 것
  8. 주문 — 번호 체계가 다르다
  9. 자주 묻는 질문

1. 공식 대응표가 없다 — 대신 쓸 것

마이그레이션을 시작하면 누구나 먼저 대응표를 찾습니다. 없습니다. 키움증권이 배포하는 kiwoom_openapi_plus_devguide(53페이지)에도, 공식 저장소 Kiwoom-Securities/Kiwoom-REST-API 에도 OPT·OPW 계열 TR 과 ka·kt 계열 api-id 를 1대1로 짝지은 표는 없습니다.

대신 쓸 수 있는 것이 있습니다. 공식 저장소의 examples/ 폴더에는 함수명 기반 파이썬 예제 362개(OAuth 2 · 국내주식 226 · 미국주식 134)가 있고, 모든 파일 맨 위에 메타 주석이 붙어 있습니다.

# ---
# api_id: ka10081
# api_name: 주식일봉차트조회요청
# category: 국내주식
# sub_category: 차트
# template: rest
# api_url: /api/dostk/chart
# menu_path: 국내주식 > 차트 > 주식일봉차트조회요청(ka10081)
# ---

이 주석을 긁으면 기능명 → api_id 역인덱스를 직접 만들 수 있습니다. 예전 TR 의 한글 이름(예: 주식일봉차트조회)을 기억하고 있다면 그것으로 검색하면 됩니다.

# examples/ 의 머리 주석을 긁어 기능명 → api_id 역인덱스를 만든다
import pathlib, re
import pandas as pd

ROOT = pathlib.Path("Kiwoom-REST-API/examples")
FIELD = re.compile(r"^#\s*(api_id|api_name|api_url|category|sub_category):\s*(.+)$")

rows = []
for f in ROOT.rglob("*.py"):
    meta = {}
    for line in f.read_text(encoding="utf-8").splitlines()[:12]:
        m = FIELD.match(line)
        if m:
            meta[m.group(1)] = m.group(2).strip()
    if "api_id" in meta:
        meta["file"] = str(f.relative_to(ROOT))
        rows.append(meta)

idx = pd.DataFrame(rows)
print(len(idx), "개 API")
print(idx[idx["api_name"].str.contains("일봉")][["api_id", "api_name", "api_url"]])

추정으로 짝지으면 안 됩니다. 번호가 비슷해 보인다고 OPT10080 을 ka10080 으로 단정하면, 에러 없이 다른 데이터를 받는 경우가 생깁니다. 반드시 위 역인덱스의 api_name 과 실제 응답 필드를 대조해 확인하십시오. 예제 개수와 스펙은 바뀔 수 있으므로 키움증권 공식 저장소와 포털 안내로 현재 내용을 확인하시기 바랍니다.

2. ① 로그인 — 영웅문 창이 사라진다

OpenAPI+ 는 LONG CommConnect() 를 호출하면 로그인 윈도우가 떠서 사람이 입력하고, 결과가 void OnEventConnect(LONG nErrCode) 이벤트로 돌아옵니다(nErrCode 가 0 이면 성공). 그래서 무인 운영을 하려면 로그인 창 자동화라는 별도 문제를 풀어야 했습니다.

REST 에서는 그 창이 없습니다. 공식 예제의 api_id 는 au10001, 경로는 /oauth2/token 이고 응답 필드는 token·token_type·expires_dt 입니다. 이후 모든 호출은 authorization 헤더에 그 토큰을 실어 보냅니다.

OpenAPI+키움 REST
CommConnect() → 로그인 창POST /oauth2/token (au10001)
OnEventConnect(nErrCode) 대기응답의 token·expires_dt
GetConnectState() 0/1expires_dt 비교 + 만료 시 재발급
GetLoginInfo("ACCOUNT_CNT")계좌 조회 API 로 분리

여기서 새로 생기는 일이 토큰 수명 관리입니다. 만료 시점·재발급·폐기 절차는 키움 REST 토큰 만료·폐기 편에 정리해 뒀습니다. 반대로 없어지는 일은 32bit 파이썬·OCX 등록·영웅문 자동 로그인입니다 — 그 고생의 목록은 OpenAPI+ 설치 트러블슈팅 편에 남아 있습니다.

3. ② 요청 — SetInputValue 가 JSON 키가 된다

OpenAPI+ 의 조회는 두 단계였습니다. void SetInputValue(BSTR sID, BSTR sValue) 로 입력값을 하나씩 밀어 넣고, LONG CommRqData(BSTR sRQName, BSTR sTrCode, long nPrevNext, BSTR sScreenNo) 로 전송합니다. 공식 가이드에 nPrevNext 는 0 조회 / 2 연속, sScreenNo 는 4자리 화면번호로 적혀 있습니다.

# OpenAPI+ (32bit · PyQt5)
self.ocx.dynamicCall("SetInputValue(QString, QString)", "종목코드", "005930")
self.ocx.dynamicCall("SetInputValue(QString, QString)", "기준일자", "20260924")
self.ocx.dynamicCall("SetInputValue(QString, QString)", "수정주가구분", "1")
self.ocx.dynamicCall("CommRqData(QString, QString, int, QString)",
                     "일봉조회", "OPT10081", 0, "0101")   # ← sRQName, sTrCode, nPrevNext, sScreenNo
# 여기서 끝나지 않는다 — OnReceiveTrData 를 기다려야 한다

REST 에서는 입력값이 전부 JSON 본문의 키가 되고, TR 코드가 api-id 헤더로 올라갑니다. 공식 런타임이 예약해 둔 헤더는 content-type·api-id·authorization 세 개입니다.

# 키움 REST (OS·비트 무관)
import requests

r = requests.post(
    "https://api.kiwoom.com/api/dostk/chart",      # api_url
    headers={
        "content-type": "application/json;charset=UTF-8",
        "api-id": "ka10081",                       # ← 옛 sTrCode 자리
        "authorization": f"Bearer {access_token}",
    },
    json={                                          # ← 옛 SetInputValue 자리
        "stk_cd": "005930",
        "base_dt": "20260924",
        "upd_stkpc_tp": "1",
    },
    timeout=10,
)
data = r.json()          # 여기서 이미 데이터가 와 있다

운영과 모의투자는 호스트가 다릅니다. 위 예시는 구조를 보이기 위한 것이며, 실제 호스트와 파라미터명은 키움 차트 API(일봉·분봉·틱) 편과 공식 저장소 예제로 확인하십시오.

4. ③ 응답 — 콜백 대기가 리턴으로 바뀐다

여기가 코드가 가장 많이 줄어드는 곳입니다. OpenAPI+ 는 요청 후 이벤트를 기다립니다. 공식 가이드의 시그니처는 인자가 9개입니다.

void OnReceiveTrData(LPCTSTR sScrNo, LPCTSTR sRQName, LPCTSTR sTrCode,
                     LPCTSTR sRecordName, LPCTSTR sPreNext, LONG nDataLength,
                     LPCTSTR sErrorCode, LPCTSTR sMessage, LPCTSTR sSplmMsg)

이벤트가 오면 LONG GetRepeatCnt(LPCTSTR sTrCode, LPCTSTR sRecordName) 로 반복 개수를 세고, GetCommData() 로 행 인덱스와 필드 한글명을 문자열로 지정해 값을 하나씩 꺼냅니다. 가이드의 예시가 openApi.GetCommData("OPT00001", RQName, 0, "종목명") 형태입니다.

# OpenAPI+ — 이벤트 안에서 한 칸씩 꺼낸다
def on_receive_tr_data(self, scr_no, rq_name, tr_code, record_name,
                       pre_next, *args):
    n = self.ocx.dynamicCall("GetRepeatCnt(QString, QString)", tr_code, rq_name)
    rows = []
    for i in range(n):
        rows.append({
            "일자":  self._get(tr_code, rq_name, i, "일자"),
            "현재가": self._get(tr_code, rq_name, i, "현재가"),
            "거래량": self._get(tr_code, rq_name, i, "거래량"),
        })
    self.result = rows
    self.loop.exit()          # QEventLoop 대기 해제

REST 응답은 배열이 그대로 옵니다. ka10081 의 경우 공식 예제가 응답 테이블 키를 stk_dt_pole_chart_qry 로, 컬럼을 dt(일자)·cur_prc(현재가)·open_pric·high_pric·low_pric·trde_qty·trde_prica·trde_tern_rt 로 매핑해 두고 있습니다.

# 키움 REST — 인덱스 루프도, 필드 문자열도, 이벤트 대기도 없다
import pandas as pd

body = r.json()
if body.get("return_code") != 0:
    raise RuntimeError(f'{body.get("return_code")} {body.get("return_msg")}')

df = pd.DataFrame(body["stk_dt_pole_chart_qry"])
df = df.rename(columns={"dt": "일자", "cur_prc": "현재가", "trde_qty": "거래량"})

실무에서 체감하는 변화 — QEventLoop·QAxWidget·시그널 연결·타임아웃 감시 코드가 통째로 사라집니다. 대신 HTTP 예외·타임아웃·재시도가 새 관심사가 되고, 요청 속도를 넘기면 HTTP 429 로 돌아옵니다.

5. ④ 연속조회 — nPrevNext 에서 next-key 로

OpenAPI+ 의 연속조회는 두 값이 짝을 이뤘습니다. 보낼 때 CommRqData 의 nPrevNext 에 2 를 주고, 받을 때 OnReceiveTrData 의 sPreNext 로 더 있는지 판단합니다.

REST 는 이것을 헤더 두 개로 옮겼습니다. 공식 런타임의 클라이언트가 추가 헤더로 cont-yn 과 next-key 를 붙이도록 만들어져 있습니다.

단계OpenAPI+키움 REST
첫 요청nPrevNext = 0헤더 없음
더 있는지 판단sPreNext 값 확인응답 헤더 cont-yn · next-key
다음 요청nPrevNext = 2 (같은 입력 재설정)요청 헤더 cont-yn: Y + next-key: <값>
화면번호sScreenNo 4자리 필요없음

주의할 점은 next-key 가 불투명한 커서라는 것입니다. 옛 코드처럼 입력값을 다시 세팅하는 대신 받은 키를 그대로 되돌려주는 방식이라, 키를 파싱하거나 가공하면 안 됩니다. 페이징 구현 상세는 키움 연속조회 next-key 편에 있습니다.

6. ⑤ 실시간 — FID 번호는 살아남는다

실시간은 등록 방식이 완전히 바뀌지만, 가장 많이 외워 둔 지식은 그대로 쓸 수 있습니다.

OpenAPI+ 는 SetRealReg(LPCTSTR strScreenNo, LPCTSTR strCodeList, LPCTSTR strFidList, LPCTSTR strRealType) 로 등록했습니다. 종목은 ; 로, FID 도 ; 로 이어 붙이고, strRealType 이 0 이면 기존 등록을 지우고 1 이면 유지하는 구조입니다. 해제는 SetRealRemove, 값 꺼내기는 GetCommRealData, 주문 관련 통보는 OnReceiveChejanData(sGubun, nItemCnt, sFidList) + GetChejanData(9203) 였습니다.

REST 는 WebSocket(/api/dostk/websocket)에 JSON 패킷을 보냅니다.

# 키움 REST 실시간 등록 — trnm 이 REG, 해제는 REMOVE
body = {
    "trnm": "REG",      # 등록
    "grp_no": "1",      # 그룹번호  (옛 sScreenNo 자리에 해당하는 개념)
    "refresh": "1",     # 1: 기존 등록 유지 / 0: 교체  (옛 strRealType 과 같은 의미)
    "data": [{"item": ["005930"], "type": ["0B"]}],   # 종목 + 실시간 타입
}

그런데 돌아오는 값의 키는 여전히 FID 번호입니다. 공식 예제의 주문체결(00) 컬럼 매핑이 그 증거입니다.

FID뜻FID뜻
9201계좌번호905주문구분
9203주문번호908주문/체결시간
913주문상태910체결가
900주문수량911체결량
902미체결수량919거부사유
10현재가2134거래소구분
27 · 28최우선 매도·매수호가2136SOR여부

9203·913·10·27·28 은 OpenAPI+ 시절과 같은 번호입니다. 즉 FID 사전은 이관되고 등록 절차만 새로 배우면 됩니다. 다만 2134 거래소구분·2136 SOR여부처럼 새로 추가된 FID가 있습니다 — 대체거래소 도입 이후 생긴 필드라 옛 파서에는 자리가 없습니다. 시세 타입(0B 체결 · 0D 호가)은 WebSocket 0B·0D, 주문체결(00)·잔고(04)는 주문체결 00·잔고 04 편을 보십시오.

7. 사라지는 것 · 새로 생기는 것

대응되는 것이 없어 그냥 지우는 개념이 셋 있습니다.

반대로 에러 처리 체계는 통째로 갈립니다. OpenAPI+ 는 음수 상수였습니다 — 공식 가이드에 -300 OP_ERR_ORD_WRONG_INPUT, -301 OP_ERR_ORD_WRONG_ACCTNO, -303 OP_ERR_MIS_2BILL_EXC, -304 OP_ERR_MIS_5BILL_EXC, -309 OP_ERR_MIS_300CNT_EXC, -310 OP_ERR_MIS_500CNT_EXC, -206 OP_ERR_OVER_MAX_FID 같은 코드가 나열돼 있습니다.

REST 는 HTTP 상태코드 + 응답 본문의 return_code·return_msg 조합입니다. 즉 기존 OP_ERR_* 분기문은 옮길 곳이 없고 새로 써야 합니다.

구분OpenAPI+키움 REST
성공 판정리턴값 0(OP_ERR_NONE)HTTP 2xx + return_code 확인
주문 거부OP_ERR_ORD_* 음수return_code·return_msg
속도 초과OP_ERR_SISE_OVERFLOW 등HTTP 429
실시간 한도OP_ERR_OVER_MAX_FID구독 한도 — 공식 안내 확인

8. 주문 — 번호 체계가 다르다

가장 조용하게 사고가 나는 곳입니다. OpenAPI+ 의 주문은 인자 9개짜리 단일 함수였습니다.

LONG SendOrder(BSTR sRQName, BSTR sScreenNo, BSTR sAccNo, LONG nOrderType,
               BSTR sCode, LONG nQty, LONG nPrice, BSTR sHogaGb, BSTR sOrgOrderNo)
# nOrderType : 1 신규매수 · 2 신규매도 · 3 매수취소 · 4 매도취소 · 5 매수정정 · 6 매도정정
# sHogaGb    : 00 지정가 · 03 시장가 · 05 · 06 · 07 · 10 · 13 · 16 · 20 · 23 · 26 · 61 · 62 · 81

키움 REST 는 이것을 기능별 api-id 로 쪼갰습니다. 주식 매수주문은 kt10000, 경로는 /api/dostk/ordr 이고 파라미터가 dmst_stex_tp·stk_cd·ord_qty·trde_tp·ord_uv·cond_uv, 응답은 ord_no·dmst_stex_tp 입니다.

함정 — 주문 구분 번호가 다릅니다. OpenAPI+ 의 sHogaGb 는 "00" 지정가 / "03" 시장가 처럼 두 자리 문자열인데, REST kt10000 의 trde_tp 는 0 보통(지정가) / 3 시장가 입니다. 앞자리 0 이 빠진 형태라 기존 상수를 그대로 옮기면 값이 맞지 않습니다. 시장가일 때 ord_uv 를 비우는 규칙도 별도로 지켜야 합니다. 전체 값 표는 키움 주식주문 kt10000 편과 키움증권 공식 문서에서 확인하십시오.

또 하나 — 계좌번호가 파라미터에서 빠졌습니다. OpenAPI+ 는 sAccNo 를 매번 넘겼지만 REST 는 토큰에 묶인 계좌를 씁니다. 여러 계좌를 돌리던 봇은 계좌 선택 로직부터 다시 설계해야 합니다.

옮길까, 새로 만들까 — 위 다섯 곳이 다 바뀐다는 뜻은, 전략 로직(진입·청산 조건)만 재사용되고 인프라 계층은 새로 쓴다는 뜻입니다. 그래서 실무에서는 이식보다 전략 함수만 뽑아내고 껍데기는 새로 짜는 편이 빠릅니다. 무엇을 먼저 정해야 하는지는 키움 자동매매 프로그램 제작 전 확인 6가지에 정리해 뒀습니다.

확인 안내 — 이 글의 OpenAPI+ 시그니처는 키움증권이 배포한 개발가이드 원문, REST 쪽은 키움증권 공식 저장소 Kiwoom-Securities/Kiwoom-REST-API 의 런타임·예제 원문을 대조해 적었습니다. 다만 스펙·한도·지원 범위는 수시로 바뀌고(실제로 일부 TR 이 삭제된 전례가 있습니다 — 시간외단일가 TR 삭제), OpenAPI+ 의 지원 종료 여부도 공식 공지로 확인되지 않았습니다. 마이그레이션 전에 키움증권 공식 안내로 현재 내용을 직접 확인하십시오. 알고랩은 투자자문업·투자일임업을 영위하지 않으며 도구와 코드만 제공합니다.

9. 자주 묻는 질문

Q. 공식 TR 대응표가 정말 없나요?

없습니다. 대신 공식 저장소 examples/ 의 예제 362개가 머리 주석에 api_id·api_name·api_url·menu_path 를 달고 있어, 이를 긁어 기능명 → api_id 역인덱스를 직접 만들 수 있습니다. 번호가 비슷하다고 추정으로 짝지으면 에러 없이 다른 데이터를 받게 됩니다.

Q. CommRqData 는 무엇으로 바뀌나요?

HTTP POST 한 번입니다. SetInputValue 로 넣던 값은 JSON 본문의 키가 되고, sTrCode 는 api-id 헤더로, 인증은 authorization: Bearer 로 갑니다. sRQName·sScreenNo 는 대응되는 것이 없어 사라집니다.

Q. OnReceiveTrData 가 없어지면 구조가 어떻게 되나요?

이벤트 대기가 함수 리턴으로 바뀝니다. GetRepeatCnt로 개수를 세고 GetCommData 로 필드를 한 칸씩 꺼내던 코드가, 응답 JSON 배열을 바로 DataFrame 으로 만드는 두세 줄이 됩니다. QEventLoop·QAxWidget 대기 패턴도 함께 사라집니다.

Q. FID 번호를 다시 외워야 하나요?

아닙니다. 등록만 WebSocket trnm: "REG" 로 바뀌고 FID 번호는 그대로입니다 — 9203 주문번호·913 주문상태·902 미체결수량·10 현재가·27·28 최우선호가가 동일합니다. 다만 2134 거래소구분·2136 SOR여부처럼 새로 생긴 FID 가 있습니다.

Q. 주문 코드에서 특히 조심할 곳은 어디인가요?

주문 구분 값의 번호 체계입니다. SendOrder 의 sHogaGb 는 "00" 지정가·"03" 시장가인데 REST kt10000 의 trde_tp 는 0 보통(지정가)·3 시장가입니다. 앞자리 0 이 빠져 기존 상수를 그대로 옮기면 값이 맞지 않습니다. 또 매수·매도·정정·취소가 각각 다른 api-id 로 갈리고, 계좌번호는 파라미터에서 빠집니다.

OpenAPI+ 봇, REST 로 옮겨 드립니다

전략 로직은 살리고 인프라만 새로 짜는 방식으로 이식합니다. 기존 코드를 보여 주시면 범위부터 잡아 드립니다.
24시간 빠른 답변 가능합니다.

제작 상담하기