분봉 데이터 수집 — 당일 30건·과거 120건 API가 다르다
inquire-time-itemchartprice(tr_id = FHKST03010200)이고
1회 호출에 최대 30건, 전일자 분봉은 제공하지 않습니다.
과거 일자 분봉은 inquire-time-dailychartprice(tr_id = FHKST03010230)로
1회 최대 120건이고 FID_INPUT_DATE_1로 날짜를 지정하며,
공식 예제 설명 기준 보관 한도는 최대 1년입니다.
"어제 분봉이 안 나온다"는 문제의 대부분은 버그가 아니라 엔드포인트를 잘못 고른 것입니다.
백테스트를 분 단위로 돌리려는 순간 첫 번째 벽이 데이터입니다. 일봉은 pykrx·FinanceDataReader로 몇 줄이면 끝나는데, 같은 방식으로 분봉을 부르면 함수 자체가 없습니다. 그래서 증권사 API로 넘어가는데, 여기서 두 번째 벽을 만납니다 — 분봉 API를 하나로 알고 접근하면 절반은 못 받습니다.
이 글은 국내주식 분봉을 어디서 어떻게 받아 오는가 하나만 다룹니다. 파라미터·건수 제한·페이지네이션·허봉 처리까지 실제 코드로 정리했습니다.
이 글의 순서
- 왜 pykrx로는 분봉이 안 나오나
- KIS 분봉 API는 두 개다 — 한눈에 비교
- 당일 분봉 — FHKST03010200과 30건의 벽
- 과거 분봉 — FHKST03010230과 1년의 벽
- 응답 필드 매핑 · DataFrame으로 만들기
- 1년치를 다 받으려면 몇 번 호출해야 하나
- 키움 REST API 쪽 대안 · 백테스트 전 점검 · FAQ
1. 왜 pykrx로는 분봉이 안 나오나
pykrx는 KRX와 포털의 공개 화면을 긁어 오는 라이브러리입니다.
그 화면들이 일 단위로 집계된 값을 보여 주기 때문에,
stock.get_market_ohlcv_by_date()처럼 일봉을 돌려주는 함수는 있어도
분봉을 돌려주는 함수는 없습니다. FinanceDataReader도 마찬가지로 일봉이 기본 단위입니다.
즉 분봉이 없는 것은 사용법을 몰라서가 아니라 소스에 그 데이터가 없기 때문입니다. 개인이 접근하는 정식 통로는 증권사 API뿐입니다.
| 소스 | 일봉 | 분봉 | 비고 |
|---|---|---|---|
| pykrx | 제공 | 없음 | KRX·포털 화면 스크래핑 |
| FinanceDataReader | 제공 | 없음 | 일봉 중심 인터페이스 |
| 한국투자증권 KIS REST API | 제공 | 제공(2종) | 당일용·과거일자용이 분리 |
| 키움증권 REST API | 제공 | 제공 | 분봉차트 조회 TR 별도 |
2. KIS 분봉 API는 두 개다 — 한눈에 비교
여기가 이 글의 핵심입니다. 한국투자증권 KIS Developers의 국내주식 기본시세 카테고리에는 분봉 엔드포인트가 두 개 있고, 이름이 비슷해서 자주 혼동됩니다.
| 주식당일분봉조회 | 주식일별분봉조회 | |
|---|---|---|
| 경로 | /uapi/domestic-stock/v1/quotations/ | /uapi/domestic-stock/v1/quotations/ |
tr_id | FHKST03010200 | FHKST03010230 |
| 1회 건수 | 최대 30건 | 최대 120건 |
| 조회 범위 | 당일만 (전일자 미제공) | 과거일자 가능 (보관분 한정, 최대 1년) |
| 날짜 지정 | 없음 | FID_INPUT_DATE_1 |
| 허봉 옵션 | 없음 | FID_FAKE_TICK_INCU_YN |
| 모의투자 | 동일 tr_id 사용 | 실전 기준 안내 |
가장 흔한 실패. 당일용 FHKST03010200에
날짜 파라미터를 억지로 끼워 넣고 어제 데이터를 기다리는 경우입니다.
이 API는 날짜 파라미터 자체가 없고, 공식 예제에도
"당일 분봉 데이터만 제공됩니다(전일자 분봉 미제공)"라고 명시돼 있습니다.
HTTP 200에 rt_cd도 0으로 오는데 output2만 비어 있어서
버그처럼 보이지만 버그가 아닙니다.
3. 당일 분봉 — FHKST03010200과 30건의 벽
파라미터는 다섯 개입니다. 이름이 전부 FID_로 시작해서 낯설지만 의미는 단순합니다.
| 파라미터 | 의미 | 값 예시 |
|---|---|---|
FID_COND_MRKT_DIV_CODE | 시장 분류 | J(KRX) · NX(NXT) · UN(통합) |
FID_INPUT_ISCD | 종목코드 6자리 | 005930 |
FID_INPUT_HOUR_1 | 기준 시각 | 093000 |
FID_PW_DATA_INCU_YN | 과거 데이터 포함 | Y / N |
FID_ETC_CLS_CODE | 기타 구분 | 빈 문자열 |
FID_COND_MRKT_DIV_CODE에 NX·UN이 생긴 것은
대체거래소가 열리면서 추가된 값입니다. 체결 장소에 따라 시세가 갈리는 문제는
NXT·KRX 주문 라우팅에서 따로 다뤘습니다.
분봉을 받을 때도 어느 시장 기준의 봉인지를 먼저 정해야 합니다.
import requests
BASE = "https://openapi.koreainvestment.com:9443"
PATH = "/uapi/domestic-stock/v1/quotations/inquire-time-itemchartprice"
headers = {
"content-type": "application/json; charset=utf-8",
"authorization": f"Bearer {access_token}",
"appkey": APP_KEY,
"appsecret": APP_SECRET,
"tr_id": "FHKST03010200", # 당일 분봉
"custtype": "P",
}
params = {
"FID_COND_MRKT_DIV_CODE": "J", # KRX 기준
"FID_INPUT_ISCD": "005930",
"FID_INPUT_HOUR_1": "093000", # 이 시각을 기준으로 거슬러 30건
"FID_PW_DATA_INCU_YN": "Y",
"FID_ETC_CLS_CODE": "",
}
res = requests.get(BASE + PATH, headers=headers, params=params, timeout=5)
data = res.json()
if data.get("rt_cd") != "0":
raise RuntimeError(f"KIS 에러 [{data.get('msg_cd')}] {data.get('msg1','').strip()}")
rows = data["output2"] # 분봉 배열 (최대 30건)
print(len(rows), rows[0]["stck_cntg_hour"], rows[-1]["stck_cntg_hour"])
30건씩 거슬러 올라가는 루프
정규장 하루는 09:00~15:30, 1분봉으로 약 390개입니다.
1회 30건이므로 하루치를 다 받으려면 13번 이상 호출해야 합니다.
이 API에는 next_key 같은 연속조회 키가 없고,
FID_INPUT_HOUR_1을 직접 뒤로 밀어 가며 페이지를 넘깁니다.
(키움 REST API의 next-key 방식과는 다릅니다 —
키움 연속조회 처리 참고)
from datetime import datetime, timedelta
import time
def fetch_today_minutes(iscd="005930", start="153000", end="090000"):
"""당일 분봉을 뒤에서 앞으로 30건씩 되감아 수집"""
out, cursor = [], start
while cursor > end:
params["FID_INPUT_ISCD"] = iscd
params["FID_INPUT_HOUR_1"] = cursor
d = requests.get(BASE + PATH, headers=headers,
params=params, timeout=5).json()
if d.get("rt_cd") != "0":
raise RuntimeError(f"[{d.get('msg_cd')}] {d.get('msg1','').strip()}")
rows = d.get("output2") or []
if not rows:
break # 더 없음 = 장 시작 이전
out.extend(rows)
# 가장 오래된 봉의 1분 전으로 커서를 민다
oldest = rows[-1]["stck_cntg_hour"] # 'HHMMSS'
t = datetime.strptime(oldest, "%H%M%S") - timedelta(minutes=1)
cursor = t.strftime("%H%M%S")
time.sleep(0.3) # 유량 제한 회피
return out
주의할 응답 특성 두 가지.
① FID_INPUT_HOUR_1에 미래 시각을 넣으면 오류가 아니라
현재가 기준으로 조회됩니다. 오전 10시에 113000을 넣으면
10:00~11:30 구간이 전부 10시 값으로 나옵니다.
② output2 첫 배열의 체결량 cntg_vol은
해당 분의 첫 체결이 나기 전까지 직전 분봉의 값이 그 자리에 표시됩니다.
장중에 실시간으로 받는 코드라면 가장 최근 봉 1개는 확정된 값으로 취급하지 마십시오.
4. 과거 분봉 — FHKST03010230과 1년의 벽
백테스트에 실제로 필요한 것은 이쪽입니다.
FID_INPUT_DATE_1에 날짜(YYYYMMDD)를 넣어 과거 일자를 지정하고,
1회에 최대 120건을 받습니다. 당일용의 4배라 수집 호출 수가 크게 줄어듭니다.
PATH_D = "/uapi/domestic-stock/v1/quotations/inquire-time-dailychartprice"
headers["tr_id"] = "FHKST03010230" # 과거 일자 분봉
params_d = {
"FID_COND_MRKT_DIV_CODE": "J",
"FID_INPUT_ISCD": "005930",
"FID_INPUT_HOUR_1": "130000", # 기준 시각
"FID_INPUT_DATE_1": "20260813", # ← 이 파라미터가 당일용에는 없다
"FID_PW_DATA_INCU_YN": "N",
"FID_FAKE_TICK_INCU_YN": "", # 허봉 포함 여부
}
d = requests.get(BASE + PATH_D, headers=headers, params=params_d, timeout=5).json()
print(d["rt_cd"], len(d.get("output2") or [])) # '0' 120
허봉(fake tick)을 넣을 것인가
FID_FAKE_TICK_INCU_YN은 체결이 하나도 없던 분을 결과에 포함할지 정하는 옵션입니다.
거래가 뜸한 종목은 분 단위로 빈 구간이 자주 생기는데,
이걸 포함하면 시간 축이 균일해지고, 빼면 배열 길이가 실제 체결 분 수만큼만 나옵니다.
- 지표 계산용이라면 포함하는 쪽이 편합니다 — 이동평균·RSI 같은 지표는 등간격 시계열을 가정합니다.
- 체결 가능성을 따지는 시뮬레이션이라면 빼는 쪽이 정직합니다 — 체결이 없던 분에 주문이 체결됐다고 가정하면 백테스트가 실제보다 좋게 나옵니다.
보관 한도가 실질적인 상한입니다. 한국투자증권 공식 예제 코드의 설명에는 "과거 분봉 조회 시 당사 서버에서 보관하고 있는 만큼의 데이터만 확인 가능(최대 1년 분봉 보관)"이라고 적혀 있습니다. 3년치 분봉을 이 API로 한 번에 채우는 것은 불가능하며, 장기 데이터가 필요하면 매일 조금씩 받아 자체 DB에 적재하는 구조로 가야 합니다. 보관 기간·건수는 증권사 정책에 따라 바뀔 수 있으니 구현 전 KIS Developers 공식 문서에서 현재 값을 확인하십시오.
5. 응답 필드 매핑 · DataFrame으로 만들기
두 API 모두 output1(종목 요약)과 output2(분봉 배열)로 나옵니다.
분봉은 output2이고, 필드명은 아래와 같습니다.
전부 문자열로 오므로 숫자 변환을 반드시 거쳐야 합니다.
| 필드 | 의미 | 비고 |
|---|---|---|
stck_bsop_date | 주식 영업일자 | YYYYMMDD |
stck_cntg_hour | 주식 체결시간 | HHMMSS |
stck_oprc | 주식 시가 | 해당 분의 시가 |
stck_hgpr | 주식 최고가 | 고가 |
stck_lwpr | 주식 최저가 | 저가 |
stck_prpr | 주식 현재가 | 분봉의 종가 자리 |
cntg_vol | 체결 거래량 | 해당 분 거래량 |
acml_tr_pbmn | 누적 거래대금 | 누적값(차분 필요) |
여기서 한 번은 헷갈립니다. 분봉의 종가에 해당하는 필드가
stck_clpr이 아니라 stck_prpr(현재가)입니다.
일봉 API의 필드명과 달라서 일봉 코드를 복사해 오면 종가가 통째로 비어 나옵니다.
그리고 acml_tr_pbmn은 그 분의 거래대금이 아니라 누적이라
분 단위로 쓰려면 차분을 취해야 합니다.
import pandas as pd
def to_frame(rows):
df = pd.DataFrame(rows)
df["dt"] = pd.to_datetime(
df["stck_bsop_date"] + df["stck_cntg_hour"], format="%Y%m%d%H%M%S")
num = ["stck_oprc", "stck_hgpr", "stck_lwpr", "stck_prpr", "cntg_vol"]
df[num] = df[num].apply(pd.to_numeric, errors="coerce")
df = (df.rename(columns={"stck_oprc": "open", "stck_hgpr": "high",
"stck_lwpr": "low", "stck_prpr": "close",
"cntg_vol": "volume"})
.loc[:, ["dt", "open", "high", "low", "close", "volume"]]
.drop_duplicates("dt") # 되감기 루프에서 경계가 겹친다
.sort_values("dt")
.reset_index(drop=True))
return df
df = to_frame(rows)
print(df.tail(3))
# dt open high low close volume
# 387 2026-08-13 15:28:00 71200 71300 71200 71300 14283
# 388 2026-08-13 15:29:00 71300 71400 71200 71200 20117
# 389 2026-08-13 15:30:00 71200 71200 71100 71100 88402
drop_duplicates를 빼지 마십시오.
커서를 되감는 방식은 페이지 경계에서 같은 봉이 두 번 들어오는 경우가 있습니다.
정렬 전에 중복을 걷어내지 않으면 거래량이 두 배로 잡힌 봉이 백테스트에 그대로 들어갑니다.
분봉 적재·결측 보정·전략 검증까지 도는 상태로 만들어 드립니다. 24시간 빠른 답변 가능합니다.
무료 상담 시작하기 →6. 1년치를 다 받으려면 몇 번 호출해야 하나
계산이 단순합니다. 정규장 1분봉은 하루 약 390개, 과거일자 API가 1회 120건이니 종목당 하루 4번입니다. 국내 거래일이 연 240일 안팎이므로 한 종목의 1년치에 약 960번. 코스피200을 다 받으려면 19만 번대 호출이 됩니다.
| 범위 | 대략 호출 수 | 초당 2회로 환산 |
|---|---|---|
| 1종목 · 1일 | 약 4회 | 수 초 |
| 1종목 · 1년 | 약 960회 | 약 8분 |
| 200종목 · 1년 | 약 192,000회 | 약 27시간 |
표의 "초당 2회"는 설명을 위한 가정치이지 공식 한도가 아닙니다.
실제 허용 유량은 계정·API별로 다르고 변경되므로 KIS Developers 공식 안내에서 확인해야 합니다.
다만 방향은 분명합니다 — 분봉 수집은 유량 제한에 가장 먼저 부딪히는 작업이고,
제한을 넘기면 EGW00201 초당 거래건수 초과가 돌아옵니다.
- 리미터를 수집기 안이 아니라 클라이언트 공용으로 두십시오. 주문 봇과 수집기가 같은 계정을 쓰면 호출량이 합산됩니다 (호출 제한 설계).
- 받은 구간은 즉시 저장하고, 재실행 시 이미 있는 날짜는 건너뛰게 하십시오. 27시간짜리 작업이 중간에 끊겼을 때 처음부터 다시 받는 구조면 영영 못 끝냅니다.
- 휴장일에는 호출하지 마십시오. 빈 응답을 받으려고 하루 4번씩 부르는 것도 유량입니다 (개장일 판별).
7. 키움 REST API 쪽 대안
한국투자증권 대신 키움증권 REST API를 쓴다면 분봉차트 조회 TR이 별도로 있습니다 (종목코드·틱 범위·수정주가 반영 여부를 파라미터로 받는 형태). 키움은 응답이 잘렸을 때 연속조회 키를 헤더로 돌려주는 방식이라 페이지네이션 구현이 KIS와 다릅니다 — 키움 REST API 가이드와 연속조회 처리에 정리해 두었습니다.
TR 코드와 파라미터 이름·보관 기간은 증권사마다 다르고 개편도 잦으므로 구현 직전에 각 사 공식 문서에서 현재 스펙을 확인하십시오.
8. 백테스트에 넣기 전 점검
분봉이 모였다고 바로 전략을 돌리면 수치가 실제보다 좋게 나옵니다. 최소한 아래는 확인하십시오.
- 중복·결측 — 되감기 경계의 중복 봉, 거래 없는 분의 빈칸 처리 방식을 정했는가
- 수정주가 — 액면분할·유상증자 구간에서 가격이 튀지 않는가. 분봉은 일봉보다 보정이 까다롭습니다
- 거래정지·단일가 — 정지 구간과 시간외 단일가 구간을 정규장 봉과 섞지 않았는가
- 체결 가정 — 그 분의 종가에 무조건 체결됐다고 가정하지 않았는가. 슬리피지·수수료를 넣으면 결과가 달라집니다 (거래비용 반영)
- 미래참조 — 아직 확정되지 않은 마지막 봉을 신호 계산에 쓰지 않았는가
자주 묻는 질문
Q. pykrx나 FinanceDataReader로 분봉을 받을 수 있나요?
없습니다. 둘 다 일봉이 기본 단위입니다. 분봉은 증권사 API로 가야 하고, 한국투자증권 KIS와 키움증권 REST API 모두 분봉 전용 엔드포인트를 따로 두고 있습니다.
Q. 어제 분봉을 조회했는데 빈 배열이 옵니다.
엔드포인트를 잘못 골랐을 가능성이 큽니다. FHKST03010200은 당일 전용이라
전일자 분봉을 제공하지 않습니다. 과거 일자는 FHKST03010230에
FID_INPUT_DATE_1을 넣어 조회하십시오.
Q. 과거 분봉은 얼마나 오래된 것까지 받을 수 있나요?
한국투자증권 공식 예제 설명 기준으로 최대 1년입니다. 그보다 긴 기간이 필요하면 매일 받아 자체 DB에 쌓는 구조로 접근해야 합니다. 보관 기간은 변경될 수 있으므로 공식 문서에서 확인하십시오.
Q. 수집이 느린데 호출을 더 빠르게 돌려도 되나요?
권장하지 않습니다. 계정 단위 유량 제한을 넘기면 EGW00201 같은 응답이 돌아오고,
같은 계정을 쓰는 주문 봇까지 같이 막힙니다.
속도는 호출 빈도가 아니라 중복 수집 제거로 올리십시오.
확인 캐치. 이 글의 엔드포인트 경로·tr_id·파라미터명·건수 제한은
2026년 8월 14일 기준 한국투자증권이 공개한 오픈API 예제 코드의 명세에 근거합니다.
API 스펙과 데이터 보관 기간은 증권사 사정으로 변경될 수 있으므로
구현 전 KIS Developers 공식 문서에서 현재 값을 확인하십시오.
본 글은 데이터 수집 방법을 다룰 뿐 특정 종목이나 수익률에 대한 어떠한 예측이나 권유도 담고 있지 않으며,
과거 데이터로 얻은 결과가 미래 성과를 보장하지 않습니다.
마무리
정리하면 두 줄입니다.
오늘 분봉은 FHKST03010200으로 30건씩, 과거 분봉은 FHKST03010230으로 120건씩 받는다.
그리고 보관 한도가 1년이라, 그보다 긴 데이터는 오늘부터 쌓기 시작하는 수밖에 없습니다.