AlgoLab Blog · KIS 데이터 실무 · 2026

KIS API 일봉 데이터 — FHKST03010100 100건 제한

한국투자증권 · 과거 데이터 2026-08-24 · 약 7분 읽기 · 알고랩 AlgoLab
한 줄 요약 한국투자증권 KIS API로 일봉을 받는 TR은 국내주식기간별시세(일/주/월/년), tr_idFHKST03010100, GET /uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice입니다. 막히는 지점은 두 개입니다 — ① 한 번의 호출에 최대 100건이라 3년치를 한 번에 못 받고 FID_INPUT_DATE_2를 과거로 밀며 나눠 호출해야 하고, ② FID_ORG_ADJ_PRC0이 수정주가, 1이 원주가이름과 값이 직관과 반대입니다. 그리고 봉 배열은 output1이 아니라 output2에 들어 있습니다.

KIS Developers에서 앱키를 받고 토큰까지 발급했다면 대개 다음에 하는 일이 과거 데이터 받기입니다. 전략을 검증하려면 캔들이 필요하니까요. 그런데 문서를 보고 그대로 호출하면 이런 일이 벌어집니다.

>>> # 2023-01-01 ~ 2026-08-24, 3년치를 달라고 했는데
>>> len(res["output2"])
100

에러가 안 납니다. rt_cd0이고 응답도 정상입니다. 그냥 100개만 옵니다. 이 글은 그 100건 제한과, 그보다 더 조용한 수정주가 코드값 문제를 다룹니다. 키움 쪽 대응 TR과의 차이는 키움 REST API 차트 데이터 — ka10081 수정주가 함정에 있습니다.

목차

  1. 요청 한 장 — 파라미터 6개가 전부 필수
  2. 함정 ① 100건에서 잘린다
  3. 함정 ② FID_ORG_ADJ_PRC — 0이 수정주가다
  4. 함정 ③ 봉은 output2에 있다
  5. 함정 ④ 락 구분 코드와 분할 비율
  6. 실전 코드 — N년치 일봉 수집기
  7. pykrx·FinanceDataReader와 무엇이 다른가
  8. 자주 묻는 질문

1. 요청 한 장 — 파라미터 6개가 전부 필수

한국투자증권이 GitHub에 공개한 공식 예제 기준으로 요청은 다음과 같습니다.

구분
MethodGET
URL/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice
헤더 tr_idFHKST03010100실전·모의 동일
헤더 custtypeP (개인)
헤더 authorizationBearer {접근토큰} · appkey · appsecret

쿼리 파라미터는 여섯 개가 전부 필수입니다.

파라미터
FID_COND_MRKT_DIV_CODE조건 시장 분류 코드J:KRX, NX:NXT, UN:통합
FID_INPUT_ISCD입력 종목코드예: 005930
FID_INPUT_DATE_1조회 시작일자YYYYMMDD
FID_INPUT_DATE_2조회 종료일자YYYYMMDD최대 100개
FID_PERIOD_DIV_CODE기간분류코드D:일봉 W:주봉 M:월봉 Y:년봉
FID_ORG_ADJ_PRC수정주가 원주가 가격 여부0:수정주가 1:원주가

파이썬으로 옮기면 이렇게 됩니다.

import requests

BASE = "https://openapi.koreainvestment.com:9443"
PATH = "/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice"

def daily_chart(token, appkey, appsecret, code, d1, d2,
                period="D", adj="0", market="J"):
    res = requests.get(
        BASE + PATH,
        headers={
            "content-type": "application/json; charset=utf-8",
            "authorization": f"Bearer {token}",
            "appkey": appkey,
            "appsecret": appsecret,
            "tr_id": "FHKST03010100",
            "custtype": "P",
        },
        params={
            "FID_COND_MRKT_DIV_CODE": market,   # J:KRX
            "FID_INPUT_ISCD": code,             # 005930
            "FID_INPUT_DATE_1": d1,             # 시작일 YYYYMMDD
            "FID_INPUT_DATE_2": d2,             # 종료일 YYYYMMDD
            "FID_PERIOD_DIV_CODE": period,      # D 일봉
            "FID_ORG_ADJ_PRC": adj,             # 0 수정주가 ← 0이다
        },
        timeout=10,
    )
    res.raise_for_status()
    return res.json()

모의투자에서도 같은 tr_id입니다. KIS는 주문 계열 TR에서 실전 TTTC...를 모의 VTTC...로 바꾸는 규칙이 있지만, 이 시세 TR은 F로 시작해 그 치환 대상이 아닙니다. 즉 도메인만 모의로 바꾸면 되고 tr_id는 그대로입니다. 실전과 모의를 오갈 때 생기는 사고는 KIS API 모의에서 실전 전환에 정리돼 있습니다.

2. 함정 ① 100건에서 잘린다

공식 예제의 설명에 “실전계좌/모의계좌의 경우, 한 번의 호출에 최대 100건까지 확인 가능합니다”가 그대로 적혀 있고, FID_INPUT_DATE_2의 설명도 “조회 종료일자 (최대 100개)”입니다.

일봉은 1년이 대략 245거래일입니다. 그래서 실제로는 이렇게 됩니다.

요청 기간대략 거래일필요한 호출 횟수
3개월약 60일1회
1년약 245일3회
3년약 735일8회
10년약 2,450일25회

여기서 나오는 사고FID_INPUT_DATE_1에 2016년을 넣고 “10년치 받았다”고 생각한 채 백테스트를 돌리는 것입니다. 실제로는 가장 최근 100거래일만 들어 있는데 rt_cd0이고 데이터도 멀쩡해 보입니다. 수집기는 반드시 받은 봉의 개수와 실제 시작 날짜를 검증하고 로그로 남겨야 합니다.

요청한 것 — 3년 한 번에 2023-08 ......................... 실제로 오는 것은 이만큼 → 해야 하는 것 — 100건씩 8구간 FID_INPUT_DATE_2를 과거로 밀며 반복 호출 다음 종료일 = 이번에 받은 가장 오래된 날짜 - 1일 호출 사이 간격을 두지 않으면 유량 제한(EGW00201)에 걸린다
100건 제한을 넘는 유일한 방법은 구간을 쪼개 반복 호출하는 것

3. 함정 ② FID_ORG_ADJ_PRC — 0이 수정주가다

이쪽이 100건 제한보다 훨씬 조용한 사고입니다. 공식 예제의 설명은 0:수정주가 1:원주가이고, 같은 예제의 실행 코드는 fid_org_adj_prc="1"원주가를 호출합니다.

항목 이름이 “수정주가 원주가 가격 여부”라 앞에 적힌 수정주가가 1일 것 같지만 반대입니다. 수정주가를 원하면 0을 넣어야 합니다.

의미액면분할 이전 구간이 어떻게 보이나
0수정주가지금 기준으로 환산된 가격 — 백테스트용
1원주가당시 실제 호가 — 분할 시점에 큰 갭이 생긴다

원주가로 받은 데이터를 그대로 백테스트에 넣으면 액면분할 시점에 가격이 수십분의 일로 뚝 떨어지는 봉이 생깁니다. 추세 전략은 그 지점을 거대한 하락 신호로 읽고, 변동성 기반 전략은 그날 하루의 변동성을 비정상적으로 크게 잡습니다. 에러는 나지 않고 결과만 틀립니다.

수집기 규칙FID_ORG_ADJ_PRC는 상수로 고정하고, 저장하는 테이블에 어떤 값으로 받았는지 컬럼을 하나 두십시오. 수정주가와 원주가를 섞어 저장하면 나중에 구분할 방법이 없습니다. 백테스트가 실전과 벌어지는 흔한 원인 중 하나이기도 합니다 — 백테스트와 실전이 벌어지는 이유를 함께 보십시오.

4. 함정 ③ 봉은 output2에 있다

응답이 두 덩어리로 옵니다. 공식 예제도 두 개를 각각 pd.DataFrame으로 만들어 돌려줍니다.

타입내용
output1객체 1개종목 요약 — hts_kor_isnm(종목명) · stck_prpr(현재가) · lstn_stcn(상장주수) · hts_avls(시가총액) · per · eps · pbr
output2배열봉 목록stck_bsop_date · stck_clpr · stck_oprc · stck_hgpr · stck_lwpr · acml_vol · acml_tr_pbmn

실제 응답은 이런 모양입니다.

{
  "rt_cd": "0",
  "output1": {
    "hts_kor_isnm": "삼성전자",
    "stck_prpr":    "71500",
    "lstn_stcn":    "5919637922",
    "per":          "13.45",
    "eps":          "5316.00",
    "pbr":          "1.21"
  },
  "output2": [
    {"stck_bsop_date":"20260824","stck_clpr":"71500","stck_oprc":"71000",
     "stck_hgpr":"71900","stck_lwpr":"70800","acml_vol":"12345678",
     "acml_tr_pbmn":"881234567890","flng_cls_code":"00","prtt_rate":"0.00",
     "mod_yn":"N","prdy_vrss_sign":"2","prdy_vrss":"500","revl_issu_reas":""}
  ]
}

※ 위 값은 구조를 보이기 위한 예시이며 실제 시세가 아닙니다. 항목명은 공식 예제 기준입니다.

모든 값이 String입니다. stck_clpr을 그대로 더하면 문자열 연결이 되고, pandas로 읽어도 object 타입으로 들어옵니다. DataFrame을 만든 뒤 숫자 컬럼을 명시적으로 변환하는 계층이 반드시 필요합니다.

5. 함정 ④ 락 구분 코드와 분할 비율

output2의 각 봉에는 가격·거래량 외에 이벤트 표식이 함께 옵니다. 수집기를 오래 돌릴 생각이면 이쪽을 무시하지 않는 편이 좋습니다.

항목한글명왜 보나
flng_cls_code락 구분 코드배당락·권리락 등 가격이 이론적으로 조정된 날 표식
prtt_rate분할 비율액면분할·병합이 있었던 날 확인
revl_issu_reas재평가사유코드가격 기준이 바뀐 사유
mod_yn변경 여부데이터가 사후 수정됐는지
prdy_vrss / prdy_vrss_sign전일 대비 / 부호계산값과 대조해 정합성 검증

mod_yn이 특히 실무적입니다. 이미 저장한 과거 봉이 나중에 수정될 수 있다는 뜻이라, “한 번 받은 구간은 다시 안 받는다”는 캐시 전략만 쓰면 낡은 값이 남습니다. 최근 며칠 구간은 주기적으로 다시 받아 덮어쓰는 편이 안전합니다.

6. 실전 코드 — N년치 일봉 수집기

100건 제한을 넘기는 방법은 하나뿐입니다 — 받은 봉 중 가장 오래된 날짜에서 하루를 빼서 다음 종료일자로 쓰는 역방향 루프입니다.

import time
from datetime import datetime, timedelta

def collect_daily(token, appkey, appsecret, code,
                  start="20230101", end=None, sleep=0.35):
    """FHKST03010100으로 start~end 일봉을 전부 모은다."""
    end = end or datetime.now().strftime("%Y%m%d")
    rows, cursor, guard = [], end, 0

    while cursor >= start and guard < 200:      # 무한루프 방지
        guard += 1
        res = daily_chart(token, appkey, appsecret, code,
                          d1=start, d2=cursor, period="D", adj="0")

        if res.get("rt_cd") != "0":
            raise RuntimeError(res.get("msg1", "unknown error"))

        chunk = res.get("output2") or []
        chunk = [r for r in chunk if r.get("stck_bsop_date")]
        if not chunk:
            break                                # 휴장 구간이거나 더 없음

        rows.extend(chunk)

        oldest = min(r["stck_bsop_date"] for r in chunk)
        if oldest <= start:
            break
        cursor = (datetime.strptime(oldest, "%Y%m%d")
                  - timedelta(days=1)).strftime("%Y%m%d")

        time.sleep(sleep)                        # 유량 제한 회피

    # 중복 제거 + 날짜 오름차순
    uniq = {r["stck_bsop_date"]: r for r in rows}
    return [uniq[d] for d in sorted(uniq)]

받은 결과는 숫자로 바꿔서 써야 합니다.

import pandas as pd

NUM = ["stck_clpr", "stck_oprc", "stck_hgpr", "stck_lwpr",
       "acml_vol", "acml_tr_pbmn"]

def to_frame(rows):
    df = pd.DataFrame(rows)
    df["date"] = pd.to_datetime(df["stck_bsop_date"], format="%Y%m%d")
    for c in NUM:
        df[c] = pd.to_numeric(df[c], errors="coerce")   # String → 숫자
    df = df.set_index("date").sort_index()

    # 검증 — 실제로 몇 건이 왔는지 반드시 확인
    print(f"{len(df)}건 · {df.index.min():%Y-%m-%d} ~ {df.index.max():%Y-%m-%d}")
    return df

sleep을 빼지 마십시오. 10년치를 받으려면 종목 하나에 25회 안팎, 200종목이면 5,000회입니다. 간격 없이 돌리면 KIS API 호출 유량 제한에 걸려 EGW00201이 떨어집니다. 대응은 EGW00201 초당 호출 제한 해결에 정리돼 있고, 다른 오류 코드는 KIS API 에러코드 정리를 보십시오.

7. pykrx·FinanceDataReader와 무엇이 다른가

“그냥 pykrx 쓰면 되지 않나”가 당연한 질문입니다. 맞습니다 — 백테스트용 과거 일봉만 필요하면 pykrxFinanceDataReader가 훨씬 편합니다. 100건 제한도, 토큰도, 유량 제한도 없습니다.

FHKST03010100을 쓰는 이유는 따로 있습니다.

셋을 어떻게 나눠 쓰는지는 파이썬 주식 데이터 — pykrx·FinanceDataReader 비교에 정리해 두었고, 분봉이 필요하면 TR이 아예 다릅니다 — 분봉 데이터 수집 — 당일 30건·과거 120건을 보십시오.

8. 자주 묻는 질문

Q. 주봉·월봉도 같은 TR인가요?

같습니다. FID_PERIOD_DIV_CODEW(주봉)·M(월봉)·Y(년봉)으로 바꾸면 됩니다. 100건 제한은 봉 개수 기준이므로 월봉 100건이면 8년이 넘습니다. 즉 장기 데이터를 볼 때는 기간분류코드를 올리는 것만으로 호출 횟수가 크게 줍니다.

Q. 미래 날짜나 휴장일을 넣으면 어떻게 되나요?

해당 구간에 거래일이 없으면 output2빈 배열로 옵니다. 에러가 아니라 정상 응답이라 위 수집기 코드가 if not chunk: break로 빠져나가도록 만든 것입니다. 이 처리를 안 넣으면 커서가 움직이지 않아 무한 루프가 됩니다.

Q. 이 데이터로 바로 수익률을 계산해도 되나요?

수정주가로 받았다면 입력 데이터로서는 준비된 셈입니다. 다만 가격만으로 나온 수익률은 실제 결과와 다릅니다 — 수수료·세금·슬리피지가 빠져 있습니다. 반영 방법은 백테스트 거래비용 반영에 있습니다. 과거 성과는 미래를 보장하지 않으며, 이 글이 다루는 것은 수익 예측이 아니라 데이터 정합성입니다.

마무리

FHKST03010100에서 사람을 잡는 것은 인증도 엔드포인트도 아닙니다. 100건에서 조용히 잘리는 것, FID_ORG_ADJ_PRC0이 수정주가라는 것, 봉이 output2에 있다는 것, 전부 String이라는 것 — 네 가지입니다. 넷 중 어느 것도 에러를 내지 않고, 전부 숫자만 조용히 틀리게 만듭니다.

※ 본문의 URL·tr_id·파라미터·응답 항목명은 한국투자증권이 GitHub에 공개한 공식 예제(open-trading-api) 기준이며 2026-08-24 확인한 내용입니다. KIS API의 스펙과 정책은 변경될 수 있으므로 제작 직전에는 KIS Developers 공식 문서를 다시 확인하십시오. 본 글은 수익이나 시장 방향을 예측하지 않습니다.

데이터 수집부터 봇까지 통째로 맡기시겠어요?

100건 분할·수정주가·유량 제한까지 정리된 수집기와 전략 봇을 함께 제작합니다.
24시간 빠른 답변 가능합니다.

제작 상담하기