KIS 관심종목 API — 30종목 시세를 1번에 받는다
FHKST11300006(관심종목 멀티종목 시세조회)를 씁니다.
FID_COND_MRKT_DIV_CODE_1·FID_INPUT_ISCD_1 부터 30번까지 쌍으로 넣으면
한 호출에 최대 30종목이 돌아옵니다. 종목별로 FHKST01010100 현재가 조회를 부르면
30종목이 30호출이 되는 일을 1호출로 끝냅니다.
대신 관리종목·VI·PER·시가총액 같은 필드는 돌아오지 않습니다 —
그래서 실무는 멀티시세로 좁히고 단일 현재가로 확인하는 2단 구조가 됩니다.
자동매매 봇을 만들면 거의 항상 같은 벽에 부딪힙니다.
감시 종목이 20개를 넘어가는 순간, 종목마다 현재가를 부르던 루프가
EGW00201 초당 거래건수 초과를 뱉기 시작합니다.
이때 대부분 호출 사이에 sleep을 넣어 루프를 느리게 만드는데,
그러면 감시 주기가 종목 수에 비례해 늘어납니다.
200종목이면 한 바퀴 도는 데 수십 초가 걸리고, 그 사이에 신호는 지나갑니다.
한국투자증권 KIS API에는 이 문제를 위해 만들어진 복수 종목 전용 시세 API가 따로 있습니다.
아래 스펙은 2026-09-12에 한국투자증권 공식 저장소
koreainvestment/open-trading-api의 main 브랜치 원문
(examples_llm/domestic_stock/intstock_grouplist,
intstock_stocklist_by_group, intstock_multprice)을
직접 내려받아 확인한 것입니다.
이 글에서 확인할 것
- 호출 예산 — 30종목이 30호출이 되는 구조
- 3단 호출 체인과
tr_id3개 - 함정 1 —
USER_ID에 HTS ID가 필요하다 - 함정 2 — 30종목 상한, 31번째부터는 잘라야 한다
- 함정 3 — 돌아오는 필드가 단일 현재가와 다르다
- 함정 4 — 시장 코드가 종목마다 따로 붙는다(
J/NX) - 봇 설계 — 2단 파이프라인
1. 호출 예산 — 30종목이 30호출이 되는 구조
KIS의 단일 종목 현재가 조회는 FHKST01010100입니다.
필드가 풍부한 대신 종목 하나만 돌려줍니다.
그래서 코드가 이렇게 생깁니다.
# 흔한 구조 — 종목 수 = 호출 수
for code in universe: # universe 가 200개라면
res = inquire_price("J", code) # tr_id = FHKST01010100
time.sleep(0.2) # EGW00201 회피용 지연
# 한 바퀴: 200 호출 + 40초 지연
여기에서 sleep을 줄이면 EGW00201이 뜨고,
늘리면 감시 주기가 늘어납니다. 둘 다 지는 선택입니다.
호출 예산 설계 자체는 레이트리밋 설계 글에서 다뤘고,
이 글은 호출 수 자체를 줄이는 쪽을 다룹니다.
2. 3단 호출 체인과 tr_id 3개
공식 예제의 docstring은 이 API를 세 단계 체인으로 설명합니다. 원문 문장은 이렇습니다 — "① 관심종목 그룹조회 → ② 관심종목 그룹별 종목조회 → ③ 관심종목(멀티종목) 시세조회 순서대로 호출하셔서 관심종목 시세 조회 가능합니다."
| 단계 | tr_id | 엔드포인트 | 무엇을 돌려주나 |
|---|---|---|---|
| ① 그룹 목록 | HHKCM113004C7 | /uapi/domestic-stock/v1/quotations/intstock-grouplist | 내 관심그룹 목록(output2) |
| ② 그룹별 종목 | HHKCM113004C6 | /uapi/domestic-stock/v1/quotations/intstock-stocklist-by-group | 그룹 안의 종목코드·시장구분 |
| ③ 멀티 시세 | FHKST11300006 | /uapi/domestic-stock/v1/quotations/intstock-multprice | 최대 30종목 시세 |
중요한 것은 ③이 독립적으로 쓰인다는 점입니다. ③은 종목코드를 직접 파라미터로 받기 때문에, 봇이 자체 유니버스 (거래량 순위 스캐너로 만든 목록)를 갖고 있다면 ①·②를 건너뛰고 ③만 호출하면 됩니다. "관심종목"이라는 이름 때문에 HTS에 등록해야만 쓸 수 있는 API로 오해하기 쉬운데, 그렇지 않습니다.
3. 함정 1 — USER_ID에 HTS ID가 필요하다
①과 ②는 다릅니다. 두 API는 파라미터에 USER_ID를 필수로 요구합니다.
공식 예제가 이 자리에 넘기는 값은 trenv.my_htsid —
즉 계좌번호도 appkey도 아닌 HTS 사용자 ID입니다.
# 공식 예제 원문 — intstock_grouplist.py
tr_id = "HHKCM113004C7" # 관심종목 그룹조회
params = {
"TYPE": type, # 관심종목구분코드 (ex. 1)
"FID_ETC_CLS_CODE": fid_etc_cls_code, # FID 기타 구분 코드 (ex. 00)
"USER_ID": user_id # 사용자 ID ← trenv.my_htsid
}
실무에서 이게 왜 함정이냐면, KIS 앱키 발급과 HTS ID는 별개이기 때문입니다.
앱키·앱시크릿만 받아 둔 상태에서는
USER_ID 자리에 넣을 값이 없고, 관심종목 그룹 자체도 존재하지 않습니다.
①·②를 쓰려면 HTS나 MTS에서 관심종목 그룹을 먼저 만들어 둬야 합니다.
②는 여기에 INTER_GRP_CODE(관심 그룹 코드, 예 001)와
FID_ETC_CLS_CODE(예 4)가 추가로 필수입니다.
선택 파라미터로 DATA_RANK·INTER_GRP_NAME·HTS_KOR_ISNM·CNTG_CLS_CODE가 있고,
응답은 output1(그룹 요약)과 output2(종목 목록) 두 덩어리로 나뉩니다.
pandas로 받을 때 output1은 단일 dict라 DataFrame([output1])처럼 리스트로 감싸야 하는데,
공식 예제도 정확히 그렇게 처리합니다.
정리하면 — 사람이 HTS에서 고른 목록을 봇이 그대로 따라가게 만들고 싶으면 ①→②→③. 봇이 스스로 유니버스를 만든다면 ③만. 후자가 운영에 더 안전합니다. HTS에서 누가 관심종목을 지우면 봇의 유니버스가 조용히 바뀌는 사고가 없기 때문입니다.
4. 함정 2 — 30종목 상한, 31번째부터는 잘라야 한다
세 파일의 docstring이 똑같은 문장을 세 번 반복합니다 —
"※ 한 번의 호출에 최대 30종목의 시세 확인 가능합니다."
③의 함수 시그니처도 fid_cond_mrkt_div_code_1/fid_input_iscd_1 부터
_30까지 정확히 30쌍으로 끝납니다. 31번째 파라미터는 존재하지 않습니다.
그래서 봇 쪽 코드는 이렇게 잘라 보내는 형태가 됩니다.
import itertools, time
def chunk(seq, n=30):
it = iter(seq)
while (batch := list(itertools.islice(it, n))):
yield batch
def build_params(batch):
"""[('J','005930'), ('NX','000660'), ...] -> KIS 파라미터 dict"""
params = {}
for i, (mkt, code) in enumerate(batch, start=1):
params[f"FID_COND_MRKT_DIV_CODE_{i}"] = mkt # J=KRX, NX=NXT
params[f"FID_INPUT_ISCD_{i}"] = code
return params # 최대 60개 키 = 30종목
universe = [("J", c) for c in code_list] # 200종목
for batch in chunk(universe, 30):
rows = call_kis("/uapi/domestic-stock/v1/quotations/intstock-multprice",
tr_id="FHKST11300006", params=build_params(batch))
handle(rows)
time.sleep(0.1) # 호출 수가 줄어도 초당 제한은 그대로 있다
200종목 감시라면 200호출 → 7호출입니다.
다만 호출이 줄었다고 EGW00201 대비를 빼면 안 됩니다.
잔고·미체결·주문 같은 다른 호출이 같은 초에 겹치면 여전히 한도에 닿습니다.
초당 허용 건수의 현재 값은 KIS Developers 공식 문서에서 확인하십시오 — 정책은 공지 없이 바뀝니다.
5. 함정 3 — 돌아오는 필드가 단일 현재가와 다르다
여기가 이 API의 진짜 비용입니다. 공식 예제 chk_intstock_multprice.py의
COLUMN_MAPPING에 응답 필드가 전부 나열돼 있는데, 30개입니다.
단일 현재가 조회가 주던 항목 중 상당수가 여기에 없습니다.
| 항목 | 멀티시세 FHKST11300006 | 단일 현재가 FHKST01010100 |
|---|---|---|
| 현재가·시가·고가·저가 | 있음 inter2_prpr 등 | 있음 |
| 상한가·하한가 | 있음 inter2_mxpr·inter2_llam | 있음 |
| 매도·매수 1호가와 잔량 | 있음 inter2_askp·total_bidp_rsqn | 있음 |
| 누적 거래량·거래대금 | 있음 acml_vol·acml_tr_pbmn | 있음 |
| 예상체결가·예상거래량 | 있음 intr_antc_cntg_vrss·intr_antc_vol | 별도 필드 체계 |
| 관리종목·투자주의 구분 | 없음 | 있음 mang_issu_cls_code |
| VI 발동 구분 | 없음 | 있음 |
| PER·PBR·시가총액 | 없음 | 있음 |
| 52주 최고·최저 | 없음 | 있음 |
대신 멀티시세에만 있는 것도 있습니다. kospi_kosdaq_cls_name(코스피/코스닥 구분 명),
mrkt_trtm_cls_name(시장 조치 구분 명), hour_cls_code(시간 구분 코드),
inter2_sdpr(기준가), oprc_vrss_hgpr_rate(시가 대비 최고가 비율)입니다.
응답 한 종목은 대략 이런 모양입니다.
{
"kospi_kosdaq_cls_name": "KOSPI",
"hour_cls_code": "0",
"inter_shrn_iscd": "005930",
"inter_kor_isnm": "삼성전자",
"inter2_prpr": "00000000", // 현재가
"inter2_prdy_vrss": "00000000", // 전일 대비
"prdy_vrss_sign": "2",
"prdy_ctrt": "0.00",
"acml_vol": "00000000",
"inter2_oprc": "00000000", "inter2_hgpr": "00000000", "inter2_lwpr": "00000000",
"inter2_mxpr": "00000000", "inter2_llam": "00000000",
"inter2_askp": "00000000", "inter2_bidp": "00000000",
"seln_rsqn": "0", "shnu_rsqn": "0",
"total_askp_rsqn": "0", "total_bidp_rsqn": "0",
"intr_antc_vol": "0", "inter2_sdpr": "00000000"
}
※ 위 JSON은 공식 예제의 COLUMN_MAPPING 필드명을 그대로 옮긴 구조 예시이며,
값은 자리표시자입니다. 실제 값은 계좌로 호출해 확인하십시오.
여기서 나오는 사고가 있습니다.
단일 현재가 조회로 만들어 둔 필터 코드를 멀티시세로 갈아끼우면,
mang_issu_cls_code를 읽던 줄이 조용히 None이 됩니다.
파이썬에서 dict.get()으로 읽고 있었다면 예외도 안 나고,
관리종목 필터가 통째로 무력화된 채 봇이 계속 돕니다.
필드명이 아예 다르기 때문에(stck_prpr → inter2_prpr) 파서를 공유하지 말고 분리하십시오.
6. 함정 4 — 시장 코드가 종목마다 따로 붙는다
③의 파라미터를 다시 보면 FID_COND_MRKT_DIV_CODE가
종목마다 하나씩 붙습니다. 공식 예제의 설명은
"조건 시장 분류 코드1 (J:KRX, NX:NXT)" 입니다.
한 호출 안에서 종목별로 KRX와 NXT를 섞어 지정할 수 있다는 뜻입니다.
이게 편의 기능처럼 보이지만 운영에서는 일관성 관리 대상입니다.
같은 종목을 어제는 J, 오늘은 NX로 조회하면 비교 대상이 달라집니다.
대체거래소 도입 이후의 주문 라우팅은 별도 주제지만,
시세 조회 쪽도 시장 코드를 설정 파일 한 곳에서 관리하는 편이 안전합니다.
시장 코드 목록은 공식 문서에서 현재 값을 확인하십시오.
7. 봇 설계 — 2단 파이프라인
5절의 대조표가 곧 설계도입니다. 넓게 훑는 단계와 좁게 확인하는 단계를 분리하면 됩니다.
| 단계 | 쓰는 API | 대상 | 호출 수(200종목 기준) |
|---|---|---|---|
| 1차 스크리닝 | FHKST11300006 멀티시세 | 전체 유니버스 | 7 |
| 2차 정밀 확인 | FHKST01010100 단일 현재가 | 조건 통과 종목만(보통 0~5개) | 0~5 |
| 3차 주문 직전 | 호가·잔고·매수가능 | 실제 주문 종목 | 종목당 2~3 |
def scan_once(universe):
hits = []
for batch in chunk(universe, 30): # 1차: 넓게
for row in multprice(batch): # tr_id FHKST11300006
if float(row["prdy_ctrt"]) >= 3.0: # 예: 전일대비 3% 이상
hits.append(row["inter_shrn_iscd"])
ready = []
for code in hits: # 2차: 좁게
d = inquire_price("J", code) # tr_id FHKST01010100
if d.get("mang_issu_cls_code") == "N": # 관리종목 제외
ready.append(code)
return ready
1차에서 조건을 통과하는 종목은 실제로 많지 않습니다.
그래서 총 호출 수가 200에서 10 안팎으로 떨어지고,
sleep을 늘리지 않아도 감시 주기를 유지할 수 있습니다.
실시간이 꼭 필요하면 방향이 다릅니다. 멀티시세는 어디까지나 폴링(주기적 조회)입니다. 체결이 일어나는 순간을 받아야 한다면 KIS WebSocket 실시간 시세 쪽이 맞고, 그쪽은 등록 종목 수 제한이라는 다른 제약이 있습니다. 폴링과 WebSocket을 섞어 쓰는 것이 일반적인 구성입니다.
8. 확인한 것과 확인하지 않은 것
이 글의 tr_id·엔드포인트 경로·파라미터 이름·응답 필드명·30종목 상한 문구는
2026-09-12에 공식 저장소 원문에서 직접 확인했습니다.
반면 확인하지 않은 것도 분명히 적어 둡니다 —
TYPE·FID_ETC_CLS_CODE의 전체 코드표는 예제에 예시값(1,00,4)만 있습니다. 다른 값의 의미는 KIS Developers 명세를 보십시오.mrkt_trtm_cls_name(시장 조치 구분 명)이 관리종목·거래정지를 어떤 문자열로 돌려주는지는 코드표가 공개돼 있지 않습니다. 이 필드로 필터를 만들기 전에 실제 응답을 먼저 확인하십시오.- 모의투자 지원 여부는 이 세 예제에 실전·모의 분기 코드가 없어 판단하지 않았습니다. 모의투자로 검증되는 범위는 별도 글을 참고하십시오.
- 초당 호출 허용 건수는 이 글에서 수치로 단정하지 않았습니다. 공식 문서의 현재 값을 쓰십시오.
자주 묻는 질문
KIS API로 여러 종목 시세를 한 번에 받을 수 있나요?
있습니다. intstock-multprice(tr_id FHKST11300006)가 그 용도이고,
한 호출에 최대 30종목입니다. 30종목을 FHKST01010100으로 돌면 30호출인 일을 1호출로 끝냅니다.
관심종목 시세조회에 USER_ID는 왜 필요한가요?
①HHKCM113004C7·②HHKCM113004C6은 HTS 사용자 ID 기준으로 저장된 그룹을 읽기 때문입니다
(공식 예제는 trenv.my_htsid를 넘깁니다). ③은 종목코드를 직접 받으므로 USER_ID가 필요 없습니다.
멀티종목 시세조회로는 못 받는 정보가 있나요?
관리종목 구분·VI·PER·PBR·시가총액·52주 최고저가 없습니다. 그래서 관리종목 필터는 이 API 단독으로 만들 수 없고, 단일 현재가 조회로 2차 확인하는 구조가 필요합니다.
30종목이 넘으면 어떻게 하나요?
30개씩 잘라 여러 번 호출합니다. 200종목이면 7호출입니다.
호출이 줄어도 초당 제한은 그대로이므로 EGW00201 재시도 로직은 유지하십시오.
NXT 종목도 같이 조회할 수 있나요?
시장 코드가 종목마다 붙고 공식 예제가 J=KRX, NX=NXT라고 적어 두었으므로
한 호출 안에서 섞을 수 있습니다. 다만 같은 종목을 항상 같은 시장 코드로 조회하도록 설정으로 고정하십시오.