키움 REST API 테마 조회 — ka90001·ka90002 함정 6가지
키움 REST API 로 테마주 목록을 받으려면 두 번 호출합니다. ka90001 테마그룹별요청으로 테마 목록과 thema_grp_cd(테마그룹코드)를 받고, 그 코드를 ka90002 테마구성종목요청에 넣어 thema_comp_stk 배열로 구성 종목을 받습니다. 두 TR 은 같은 경로 POST /api/dostk/thme 를 쓰고 api-id 헤더로만 갈립니다. 걸리는 곳은 여섯입니다 — ⑴ 테마명으로 종목을 바로 받는 TR 은 없다 ⑵ qry_tp=2 로 종목→테마 역조회가 된다 ⑶ date_tp 가 두 TR 에서 필수 여부·길이가 다르게 적혀 있다 ⑷ cur_prc·flu_rt 는 부호가 붙은 문자열이다 ⑸ 과거 시점의 편입 종목을 주지 않는다(백테스트 미래참조) ⑹ 테마 전체 순회는 조회 TR 초당 5회 한도에 바로 닿는다.
이 글은 키움 REST API 자동매매 완전 가이드에서 테마 조회 한 가지만 떼어 낸 글입니다. 토큰 발급·api-id 구조 같은 공통 부분은 그 글을 먼저 보십시오. 본문의 필드명·설명은 키움증권 공식 GitHub 저장소 Kiwoom-REST-API 의 명세 파일 kiwoom_api_spec.json 과 공식 예제 examples/국내주식/테마/ 두 파일을 2026-09-28 기준으로 직접 대조한 결과입니다.
1. 구조 — 테마 목록 한 번, 구성 종목 한 번
키움 REST API 의 국내주식 > 테마 메뉴에는 TR 이 두 개뿐입니다.
| api-id | 이름 | 필수 입력 | 응답 배열 |
|---|---|---|---|
ka90001 | 테마그룹별요청 | qry_tp · date_tp · flu_pl_amt_tp · stex_tp | thema_grp |
ka90002 | 테마구성종목요청 | thema_grp_cd · stex_tp | thema_comp_stk |
도메인은 실전 https://api.kiwoom.com, 모의투자 https://mockapi.kiwoom.com 이고, 공식 Postman 컬렉션에는 PRD 와 MOCK 폴더 양쪽에 두 TR 이 모두 들어 있습니다. 아래는 공식 컬렉션의 요청 바디 원문입니다.
// ka90001 테마그룹별요청 — 공식 Postman 컬렉션 원문
POST https://api.kiwoom.com/api/dostk/thme
api-id: ka90001
{
"qry_tp": "0",
"stk_cd": "",
"date_tp": "10",
"thema_nm": "",
"flu_pl_amt_tp": "1",
"stex_tp": "1"
}
// ka90002 테마구성종목요청 — 같은 경로, api-id 만 다르다
POST https://api.kiwoom.com/api/dostk/thme
api-id: ka90002
{
"date_tp": "2",
"thema_grp_cd": "100",
"stex_tp": "1"
}
2. 같은 경로, 다른 api-id — 경로가 틀리면 1504
키움 REST API 는 경로 하나에 여러 TR 을 묶습니다. 테마는 /api/dostk/thme, 순위는 /api/dostk/rkinfo, 종목정보는 /api/dostk/stkinfo 입니다. 순위 조회 코드를 복사해 api-id 만 ka90001 로 바꾸면 경로가 남아 짝이 어긋납니다. 명세 파일의 오류코드 표에는 이런 상황을 가리키는 코드가 따로 있습니다.
1504 해당 URI에서는 지원하는 API ID가 아닙니다. API ID={?}, URI={?}
1505 해당 API ID는 존재하지 않습니다. API ID={?}
1511 필수 입력 값에 값이 존재하지 않습니다. 필수입력 파라미터={?}
1517 입력 값 형식이 올바르지 않습니다. 파라미터={?} 실패사유= {?}
1700 허용된 API 요청 개수를 초과하였습니다. 유량={?}, API ID={?}
키움 REST 는 업무 오류를 HTTP 200 과 본문의 return_code 로 알려 주는 경우가 많습니다. raise_for_status() 만 걸어 두면 빈 thema_grp 가 “오늘은 테마가 없다”로 흘러갑니다. 판별 순서는 키움 return_code 오류 판별에 정리했습니다. 경로와 api-id 는 한 쌍의 상수로 묶어 두십시오 — 순위 편 키움 순위조회 rkinfo에서 겪은 사고와 같은 구조입니다.
3. qry_tp 와 flu_pl_amt_tp — 목록의 모양을 정하는 두 값
ka90001 의 검색구분 qry_tp 는 무엇으로 찾을지, 등락수익구분 flu_pl_amt_tp 는 어떤 순서로 줄 세울지를 정합니다. 명세 원문 값은 다음과 같습니다.
| 필드 | 값 | 의미(명세 원문) | 함께 넣는 값 |
|---|---|---|---|
qry_tp | 0 | 전체검색 | — |
1 | 테마검색 | thema_nm(테마명) | |
2 | 종목검색 | stk_cd(6자리 종목코드) | |
flu_pl_amt_tp | 1 | 상위기간수익률 | date_tp 가 기간을 정함 |
2 | 하위기간수익률 | ||
3 | 상위등락률 | ||
4 | 하위등락률 | ||
stex_tp | 1·2·3 | KRX · NXT · 통합 | 두 TR 공통 필수 |
실무에서 가장 쓸모 있는 건 qry_tp=2 입니다. 조건검색이나 키움 조건검색식으로 잡힌 종목이 어떤 테마에 묶여 있는지를 역으로 찾아, 같은 테마의 다른 종목이 함께 움직이는지 확인하는 용도입니다. 응답은 전체검색과 같은 thema_grp 배열이라 파서를 따로 만들 필요가 없습니다.
stex_tp 는 전략과 검증에서 같은 값으로 고정하십시오. NXT(넥스트레이드)가 들어온 뒤로 같은 종목도 거래소 구분에 따라 시세가 따로 집계됩니다. 장중에는 통합(3)으로 보고 기록은 KRX(1)로 쌓는 식으로 섞으면 등락률이 서로 맞지 않습니다. 거래소 구분 전반은 NXT·KRX 주문 라우팅에서 다뤘습니다.
4. date_tp — 명세 안에서 서로 다르게 적혀 있다
date_tp(날짜구분)는 기간수익률 dt_prft_rt 를 며칠 전 기준으로 계산할지를 정합니다. 설명 문구는 두 TR 이 같습니다 — “1일 ~ 99일 날짜입력”. 그런데 필수 여부와 길이가 다릅니다.
| TR | required | length | 공식 예제 값 |
|---|---|---|---|
ka90001 | Y | 2 | "10" |
ka90002 | N | 1 | "2" |
길이 1 인데 99일까지 넣으라는 설명은 서로 맞지 않습니다. 명세 표기 오류일 수도 있고 실제 제약일 수도 있습니다. 이 글은 어느 쪽인지 단정하지 않습니다. ka90002 에 두 자리 값(예: "20")을 쓸 계획이라면 같은 테마를 ka90001 과 ka90002 로 각각 받아 기간수익률이 맞는지 한 번 검산하고, 안 맞으면 ka90001 의 값만 쓰십시오. 공식 예제 함수 get_domestic_theme_stocks() 는 date_tp 기본값을 '2' 로 두고 있습니다.
5. 응답 파싱 — 부호가 붙은 문자열
응답 필드는 전부 String 입니다. 명세 설명을 그대로 옮기면 cur_prc·pred_pre·sel_bid·buy_bid 는 “단위: 원, 부호가 포함된 숫자”, flu_rt·dt_prft_rt 는 “부호 포함 소수점 둘째 자리까지 포맷된 백분율”입니다. 가격에 붙은 부호는 방향 표시이므로 가격으로 쓸 때는 절댓값을 취해야 하고, 상승·하락 판단은 부호 대신 flu_sig(1 상한가 · 2 상승 · 3 보합 · 4 하한가 · 5 하락)를 쓰는 편이 안전합니다.
import time, requests
BASE = "https://api.kiwoom.com" # 모의투자: https://mockapi.kiwoom.com
PATH = "/api/dostk/thme" # ka90001 · ka90002 공통
def post(api_id, body, token, cont_yn="N", next_key=""):
r = requests.post(BASE + PATH, json=body, timeout=10, headers={
"Content-Type": "application/json;charset=UTF-8",
"authorization": f"Bearer {token}",
"api-id": api_id, "cont-yn": cont_yn, "next-key": next_key})
r.raise_for_status()
data = r.json()
if data.get("return_code") not in (0, None): # 업무 오류는 200 으로 온다
raise RuntimeError(f"{api_id} rc={data.get('return_code')} {data.get('return_msg')}")
return data, r.headers.get("cont-yn"), r.headers.get("next-key")
def num(s):
"""'+1.25' · '-12500' · '' 같은 부호 붙은 문자열을 float 로."""
s = (s or "").strip().replace(",", "")
return float(s) if s not in ("", "+", "-") else 0.0
def theme_groups(token, days="10", order="3", stex="1"):
"""ka90001 — 등락률 상위(order=3) 테마 목록. 연속조회까지 모두 받는다."""
body = {"qry_tp": "0", "stk_cd": "", "date_tp": days, "thema_nm": "",
"flu_pl_amt_tp": order, "stex_tp": stex}
rows, cy, nk = [], "N", ""
for _ in range(10): # 공식 예제도 MAX_PAGES = 10
data, cy, nk = post("ka90001", body, token, cy, nk)
rows += data.get("thema_grp") or []
if cy != "Y":
break
time.sleep(0.2) # 공식 예제 REQUEST_DELAY_SECONDS
return rows
def theme_stocks(token, grp_cd, stex="1"):
"""ka90002 — 한 테마의 구성 종목. 가격은 abs() 로 부호를 뗀다."""
data, _, _ = post("ka90002", {"date_tp": "2", "thema_grp_cd": grp_cd,
"stex_tp": stex}, token)
return [{"code": x["stk_cd"], "name": x["stk_nm"],
"price": abs(num(x.get("cur_prc"))),
"chg_pct": num(x.get("flu_rt")),
"up": x.get("flu_sig") in ("1", "2")}
for x in data.get("thema_comp_stk") or []]
연속조회 헤더 cont-yn·next-key 를 넘기는 방식은 다른 키움 TR 과 같습니다. 자세한 규칙은 키움 연속조회 next-key를 참고하십시오. 하나 더 — thema_grp 의 main_stk(주요종목)은 명세상 길이 20 의 문자열 하나입니다. 구성 종목 전체가 아니니 이 필드로 매매 대상을 정하지 말고 반드시 ka90002 를 부르십시오.
6. 백테스트 — 과거 편입 종목을 주지 않는다
두 TR 의 요청 필드를 다시 보면 기준일자가 없습니다. date_tp 는 수익률 계산 구간일 뿐, “석 달 전 이 테마에 어떤 종목이 있었나”를 돌려주지 않습니다. 그래서 오늘 받은 구성 종목으로 1년 치를 돌리면 최근에 편입된 종목이 1년 전부터 테마 소속이었던 것처럼 계산됩니다. 테마는 이슈가 생긴 뒤에 종목이 붙는 경우가 많아 이 오차가 결과를 좋게 만드는 방향으로 작동하기 쉽습니다.
지금의 구성 종목으로 과거를 돌리면 미래참조입니다. 테마 전략을 검증하려면 매일 장 마감 뒤 ka90002 결과를 날짜와 함께 저장해 두고, 그 날짜의 목록만으로 그날을 시뮬레이션해야 합니다. 이 편향의 일반형은 생존편향 — 상장폐지 종목이 빠진 백테스트에서 수치로 다뤘습니다. 이 글은 특정 테마나 종목의 매매를 권하지 않으며, 과거 데이터는 미래 결과를 보장하지 않습니다.
7. 호출 한도 — 테마 전체 순회는 설계가 필요하다
키움증권 REST API 게시판 공지 「REST API 유량 정책 안내(2026.07.02 기준)」는 국내주식을 계좌별(토큰별) 조회 TR 1초당 5회, 주문 TR 1초당 5회로, 모의투자는 TR 1개당 1초 1회로 안내합니다. 테마가 N개면 구성 종목을 모두 받는 데만 ka90002 가 N회 이상이고, 같은 토큰으로 시세·주문 조회도 함께 돌고 있습니다. 한도를 넘으면 1700 계열이 돌아옵니다(키움 429 대응).
| 시점 | 호출 | 이유 |
|---|---|---|
| 장 시작 전 1회 | ka90001 전체 + 관심 테마만 ka90002 | 구성 종목은 장중에 잘 바뀌지 않으니 캐시한다 |
| 장중 주기 | ka90001(flu_pl_amt_tp=3)만 | 테마 등락률 순위만 갱신 |
| 장 마감 후 1회 | 관심 테마 ka90002 재조회 → 날짜와 저장 | 백테스트용 편입 이력 |
장중에 개별 종목 시세가 필요하면 REST 로 반복 호출하지 말고 WebSocket 0B(주식체결)로 등록하십시오. 같은 공지 기준 세션 1개당 실시간 시세는 200종목까지입니다. 테마와 비슷하게 “수급 쪽 묶음”을 다루는 키움 프로그램매매 ka90003도 같은 ka9000x 번대라 함께 설계하면 호출 예산을 나누기 쉽습니다.
붙이기 전 체크리스트
테마 조회를 봇에 넣기 전에 여섯 줄만 확인하십시오.
- 경로
/api/dostk/thme와api-id를 한 쌍으로 상수화했는가 - 모든 응답에서
return_code를 먼저 보는가 ka90002의date_tp두 자리 값을 쓴다면 한 번 검산했는가- 가격 필드에
abs(), 방향은flu_sig로 보는가 stex_tp를 전략·기록에서 같은 값으로 고정했는가- 백테스트용 편입 이력을 날짜와 함께 쌓고 있는가
고지. 이 글은 기술 자료이며 특정 테마·종목·전략을 권유하지 않습니다. api-id·경로·필드·코드값은 키움증권 공식 GitHub 저장소의 명세 파일과 예제, 키움증권 REST API 게시판 공지를 2026-09-28 기준으로 대조해 정리했지만, 증권사 API 스펙과 유량 정책은 예고 없이 바뀌므로 실제 적용 전 키움증권 공식 문서를 다시 확인하십시오. 알고랩(퀀트웍스)은 투자자문업·투자일임업을 영위하지 않으며, 고객이 지정한 규칙을 프로그램으로 구현해 드리는 도구 제공자입니다.