AlgoLab Blog · 백테스트 데이터 · 2026

분봉 데이터 수집 — 당일 30건·과거 120건 API가 다르다

데이터 · 퀀트 2026-08-14 · 약 8분 읽기 · 알고랩 AlgoLab
한 줄 요약 국내주식 분봉은 당일치와 과거일자의 API가 아예 다릅니다. 한국투자증권 KIS Developers 기준으로 당일 분봉은 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를 하나로 알고 접근하면 절반은 못 받습니다.

이 글은 국내주식 분봉을 어디서 어떻게 받아 오는가 하나만 다룹니다. 파라미터·건수 제한·페이지네이션·허봉 처리까지 실제 코드로 정리했습니다.

이 글의 순서

  1. 왜 pykrx로는 분봉이 안 나오나
  2. KIS 분봉 API는 두 개다 — 한눈에 비교
  3. 당일 분봉 — FHKST03010200과 30건의 벽
  4. 과거 분봉 — FHKST03010230과 1년의 벽
  5. 응답 필드 매핑 · DataFrame으로 만들기
  6. 1년치를 다 받으려면 몇 번 호출해야 하나
  7. 키움 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/
inquire-time-itemchartprice
/uapi/domestic-stock/v1/quotations/
inquire-time-dailychartprice
tr_idFHKST03010200FHKST03010230
1회 건수최대 30건최대 120건
조회 범위당일만 (전일자 미제공)과거일자 가능 (보관분 한정, 최대 1년)
날짜 지정없음FID_INPUT_DATE_1
허봉 옵션없음FID_FAKE_TICK_INCU_YN
모의투자동일 tr_id 사용실전 기준 안내
분봉이 필요하다 어느 날짜의 분봉인가? 오늘 과거 일자 FHKST03010200 inquire-time-itemchartprice 1회 30건 · 당일만 FHKST03010230 inquire-time-dailychartprice 1회 120건 · 최대 1년
같은 "분봉"이라도 날짜에 따라 부르는 API가 갈린다

가장 흔한 실패. 당일용 FHKST03010200에 날짜 파라미터를 억지로 끼워 넣고 어제 데이터를 기다리는 경우입니다. 이 API는 날짜 파라미터 자체가 없고, 공식 예제에도 "당일 분봉 데이터만 제공됩니다(전일자 분봉 미제공)"라고 명시돼 있습니다. HTTP 200에 rt_cd0으로 오는데 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_CODENX·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체결이 하나도 없던 분을 결과에 포함할지 정하는 옵션입니다. 거래가 뜸한 종목은 분 단위로 빈 구간이 자주 생기는데, 이걸 포함하면 시간 축이 균일해지고, 빼면 배열 길이가 실제 체결 분 수만큼만 나옵니다.

보관 한도가 실질적인 상한입니다. 한국투자증권 공식 예제 코드의 설명에는 "과거 분봉 조회 시 당사 서버에서 보관하고 있는 만큼의 데이터만 확인 가능(최대 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 초당 거래건수 초과가 돌아옵니다.

7. 키움 REST API 쪽 대안

한국투자증권 대신 키움증권 REST API를 쓴다면 분봉차트 조회 TR이 별도로 있습니다 (종목코드·틱 범위·수정주가 반영 여부를 파라미터로 받는 형태). 키움은 응답이 잘렸을 때 연속조회 키를 헤더로 돌려주는 방식이라 페이지네이션 구현이 KIS와 다릅니다 — 키움 REST API 가이드연속조회 처리에 정리해 두었습니다.

TR 코드와 파라미터 이름·보관 기간은 증권사마다 다르고 개편도 잦으므로 구현 직전에 각 사 공식 문서에서 현재 스펙을 확인하십시오.

8. 백테스트에 넣기 전 점검

분봉이 모였다고 바로 전략을 돌리면 수치가 실제보다 좋게 나옵니다. 최소한 아래는 확인하십시오.

자주 묻는 질문

Q. pykrx나 FinanceDataReader로 분봉을 받을 수 있나요?

없습니다. 둘 다 일봉이 기본 단위입니다. 분봉은 증권사 API로 가야 하고, 한국투자증권 KIS와 키움증권 REST API 모두 분봉 전용 엔드포인트를 따로 두고 있습니다.

Q. 어제 분봉을 조회했는데 빈 배열이 옵니다.

엔드포인트를 잘못 골랐을 가능성이 큽니다. FHKST03010200은 당일 전용이라 전일자 분봉을 제공하지 않습니다. 과거 일자는 FHKST03010230FID_INPUT_DATE_1을 넣어 조회하십시오.

Q. 과거 분봉은 얼마나 오래된 것까지 받을 수 있나요?

한국투자증권 공식 예제 설명 기준으로 최대 1년입니다. 그보다 긴 기간이 필요하면 매일 받아 자체 DB에 쌓는 구조로 접근해야 합니다. 보관 기간은 변경될 수 있으므로 공식 문서에서 확인하십시오.

Q. 수집이 느린데 호출을 더 빠르게 돌려도 되나요?

권장하지 않습니다. 계정 단위 유량 제한을 넘기면 EGW00201 같은 응답이 돌아오고, 같은 계정을 쓰는 주문 봇까지 같이 막힙니다. 속도는 호출 빈도가 아니라 중복 수집 제거로 올리십시오.

확인 캐치. 이 글의 엔드포인트 경로·tr_id·파라미터명·건수 제한은 2026년 8월 14일 기준 한국투자증권이 공개한 오픈API 예제 코드의 명세에 근거합니다. API 스펙과 데이터 보관 기간은 증권사 사정으로 변경될 수 있으므로 구현 전 KIS Developers 공식 문서에서 현재 값을 확인하십시오. 본 글은 데이터 수집 방법을 다룰 뿐 특정 종목이나 수익률에 대한 어떠한 예측이나 권유도 담고 있지 않으며, 과거 데이터로 얻은 결과가 미래 성과를 보장하지 않습니다.

마무리

정리하면 두 줄입니다. 오늘 분봉은 FHKST03010200으로 30건씩, 과거 분봉은 FHKST03010230으로 120건씩 받는다. 그리고 보관 한도가 1년이라, 그보다 긴 데이터는 오늘부터 쌓기 시작하는 수밖에 없습니다.

분봉 수집기가 필요하다면

데이터 적재부터 전략 검증까지 실제로 도는 상태로 만들어 드립니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기