AlgoLab Blog · 한국투자증권 KIS · 종목정보 · 2026

KIS 예탁원정보 API 12종 — 배당락·액면분할 미리 잡기

KIS · 기업 행위 일정 2026-09-13 · 약 8분 읽기 · 알고랩 AlgoLab
한 줄 요약 배당·무상증자·액면분할·합병 같은 기업 행위 일정은 KIS API의 예탁원정보 12종에 들어 있습니다. 경로는 /uapi/domestic-stock/v1/ksdinfo/…이고 tr_idHHKDB669100C0부터 HHKDB669111C0까지 연속 12개입니다. 12개 모두 F_DT·T_DT로 기간을 주고 SHT_CD공백으로 두면 전 종목이 옵니다. 다만 배당일정 응답에는 배당락일 필드가 없습니다 — 오는 것은 기준일 record_date뿐이라, 봇이 직전 영업일을 직접 계산해야 합니다.

봇을 몇 달 돌리면 반드시 한 번은 겪습니다. 어느 날 아침 보유 종목 가격이 이유 없이 절반이 되어 있고 봇이 그걸 폭락으로 읽어 손절을 때립니다 — 액면분할이었습니다. 또는 주문 자체가 안 들어갑니다 — 합병으로 거래가 정지돼 있었습니다.

이건 전략의 문제가 아니라 달력의 문제입니다. 시세 API는 "오늘 얼마냐"만 답하지 "내일 이 종목에 무슨 일이 있냐"는 답하지 않습니다. 한국투자증권 KIS API에서 그 답을 주는 곳이 예탁원정보(ksdinfo) 계열 12개입니다. 현재가 조회일봉 차트의 수정주가이미 지나간 사건을 되돌리는 쪽이라면, 여기서 다루는 건 아직 오지 않은 일정입니다. 기준은 공식 저장소 koreainvestment/open-trading-apiexamples_llm/domestic_stock/ksdinfo_* 예제 원문과 각 chk_*.py의 컬럼 매핑입니다.

목차

  1. 예탁원정보 12종 전체 — tr_id가 연속으로 붙어 있다
  2. 함정 ① 배당일정에 배당락일이 없다
  3. 함정 ② 권리락일은 증자 2종에만 있다
  4. 함정 ③ 매매거래정지기간은 3종에만 있다
  5. 함정 ④ 파라미터가 미묘하게 다르다
  6. 실전 코드 — 하루 한 번 긁어 캘린더로
  7. 연속조회 — CTS와 tr_cont
  8. 자주 묻는 질문

1. 예탁원정보 12종 전체 — tr_id가 연속으로 붙어 있다

카탈로그 번호 [국내주식-143]~[국내주식-154]tr_id HHKDB669100C0~HHKDB669111C0한 칸씩 나란히 갑니다 — 6691 뒤 두 자리만 세면 됩니다.

카탈로그엔드포인트tr_id봇에서 왜 필요한가
143ksdinfo/paidin-capin
유상증자일정
HHKDB669100C0권리락으로 가격이 내려간다
144ksdinfo/bonus-issue
무상증자일정
HHKDB669101C0권리락 + 주식 수가 늘어난다
145ksdinfo/dividend
배당일정
HHKDB669102C0배당락으로 가격이 내려간다
146ksdinfo/purreq
주식매수청구일정
HHKDB669103C0매수청구가격이 하방을 만든다
147ksdinfo/merger-split
합병·분할일정
HHKDB669104C0거래정지 + 종목 자체가 바뀐다
148ksdinfo/rev-split
액면교체일정
HHKDB669105C0거래정지 + 가격이 배수로 튄다
149ksdinfo/cap-dcrs
자본감소일정
HHKDB669106C0거래정지 + 감자비율만큼 주식 수 감소
150ksdinfo/list-info
상장정보일정
HHKDB669107C0신규 상장·추가 상장 물량
151ksdinfo/pub-offer
공모주청약일정
HHKDB669108C0공모가·청약기간·환불일
152ksdinfo/forfeit
실권주일정
HHKDB669109C0실권주 공모 물량
153ksdinfo/mand-deposit
의무예치일정
HHKDB669110C0보호예수 해제 = 잠재 매도 물량
154ksdinfo/sharehld-meet
주주총회일정
HHKDB669111C0의안·의결권 주식 총수

12개 전부가 공유하는 구조. 파라미터에 CTS(연속조회 키, 처음엔 공백), F_DT·T_DT(조회 기간 YYYYMMDD), SHT_CD(종목코드)가 공통으로 들어갑니다. 그리고 SHT_CD를 공백으로 두면 "전체"입니다 — 즉 종목을 하나씩 돌 필요가 없습니다. 하루 12번 호출이면 그 기간의 전 종목 기업 행위 캘린더가 완성됩니다.

기업 행위가 봇에 일으키는 사고 3종 ① 가격이 불연속으로 뛴다 배당락 · 권리락 · 액면분할 → 봇이 폭락으로 오독 ② 주문이 아예 안 들어간다 감자 · 합병 · 액면교체 → td_stop_dt 구간 ③ 보유 수량이 바뀐다 무상증자 · 감자 · 분할 → 잔고 대조가 어긋남 예탁원정보 12종 = 이 셋을 전부 미리 알려 주는 유일한 계열 HHKDB669100C0 ~ HHKDB669111C0 · SHT_CD 공백 = 전 종목 · 하루 1회면 충분 단, 배당락일·권리락일 계산은 봇 몫 (아래 2절)
기업 행위가 봇에 일으키는 사고 3종과 예탁원정보 계열의 위치

2. 함정 ① 배당일정에 배당락일이 없다

가장 많이 틀리는 지점입니다. 배당일정(HHKDB669102C0) 응답의 컬럼 매핑은 공식 chk_ksdinfo_dividend.py 기준 12개인데, 날짜 필드는 네 개뿐입니다.

응답 필드한글명봇에서의 의미
record_date기준일배당락일이 아니다 — 주주명부 확정일
divi_kind배당종류결산/중간 구분
per_sto_divi_amt현금배당금1주당 금액
divi_rate현금배당률(%)액면 기준 비율
stk_divi_rate주식배당률(%)주식으로 주는 비율
divi_pay_dt · stk_div_pay_dt · odd_pay_dt배당금·주식배당·단주대금 지급일실제로 들어오는 날 — 락일과 무관
face_val · stk_kind · high_divi_gb · sht_cd액면가·주식종류·고배당여부·종목코드필터용

배당락일 필드는 없습니다. 국내 주식은 T+2 결제라 기준일에 주주명부에 오르려면 기준일의 2영업일 전까지 매수를 끝내야 하고, 그래서 배당락은 기준일 직전 영업일에 발생합니다. 즉 record_date만 받아 놓고 "그날 가격이 빠지겠구나"라고 달력을 맞추면 하루가 어긋납니다.

그리고 이 계산은 달력 날짜가 아니라 영업일 기준입니다. 기준일 앞에 공휴일이나 주말이 끼면 매수 마감일이 더 앞당겨집니다. 그래서 이 계산에는 휴장일 정보가 반드시 같이 필요합니다 — 휴장일 체크와 봇 스케줄링에 KRX 휴장일을 API로 받아 캐시하는 방법을 정리해 뒀습니다.

# 거래일 목록(trading_days)은 휴장일 API나 pykrx로 미리 만들어 둔다.
from bisect import bisect_left

def prev_trading_day(day: str, trading_days: list[str]) -> str | None:
    """day 직전 영업일. 'YYYYMMDD' 문자열."""
    i = bisect_left(trading_days, day)
    return trading_days[i - 1] if i > 0 else None

# 배당락일 = 기준일 직전 영업일
ex_date = prev_trading_day(record_date, trading_days)
# T+2 결제 → 배당을 받으려면 배당락일의 직전 영업일까지 사야 한다
last_buy = prev_trading_day(ex_date, trading_days)

배당 절차와 기준일 지정 방식은 회사마다 다를 수 있고 제도도 바뀝니다. 위 계산은 T+2 결제라는 공통 규칙에서 나오는 일반형이며, 개별 종목의 실제 일정은 각 사 공시와 한국거래소 안내로 확인하십시오.

3. 함정 ② 권리락일은 증자 2종에만 있다

그럼 "락일"을 API가 직접 주는 곳은 없느냐 하면, 두 곳은 줍니다. right_dt(권리락일) 필드를 가진 것은 무상증자일정(HHKDB669101C0)유상증자일정(HHKDB669100C0)뿐입니다. 나머지 10개에는 이 필드가 아예 없습니다.

필드있는 엔드포인트없는 곳에서는
right_dt
권리락일
무상증자 · 유상증자 (2개)배당은 record_date에서 직접 계산
td_stop_dt
매매거래정지기간
자본감소 · 합병분할 · 액면교체 (3개)나머지는 거래정지 예고가 없다
record_date
기준일
10개상장정보는 list_dt, 의무예치는 depo_date
odd_pay_dt
단주대금지급일
배당 · 무상증자 (합병분할은 odd_amt_pay_dt)같은 뜻인데 이름이 다르다

표 마지막 줄이 실제로 사람을 잡습니다. 단주 대금 지급일이 배당·무상증자에서는 odd_pay_dt인데 합병·분할에서는 odd_amt_pay_dt입니다. 12종을 한 파서로 돌리려고 odd_pay_dt만 찾도록 짜면 합병·분할 쪽에서 조용히 None이 됩니다. 같은 이유로 상장일도 갈립니다 — 무상증자·유상증자는 list_date, 나머지는 list_dt입니다. 밑줄 하나 차이라 눈으로는 안 보입니다.

4. 함정 ③ 매매거래정지기간은 3종에만 있다

봇 입장에서 가격이 튀는 것보다 더 나쁜 건 주문이 거부되는 것입니다 — 포지션을 잡아 둔 상태에서 손절이 안 들어가면 대응 수단이 없습니다. td_stop_dt(매매거래정지기간)를 주는 곳은 세 개입니다.

액면교체가 분할인지 병합인지는 두 필드를 비교해 판정합니다. inter_bf_face_amt > inter_af_face_amt이면 액면분할(5,000원 → 100원, 주식 수 50배), 반대면 액면병합입니다. 가격 배수도 여기서 그대로 나옵니다. API가 "분할/병합"이라는 라벨을 따로 주지 않으므로 이 비교가 판정 로직입니다.

또 하나. 액면교체일정은 12종 중 유일하게 MARKET_GB(시장구분) 파라미터를 받습니다0 전체, 1 코스피, 2 코스닥. 나머지 11개에는 시장 구분이 없어서 KOSPI만 보고 싶어도 전부 받아 sht_cd로 걸러야 합니다.

이 필드는 예정 일정입니다. 거래정지는 불성실공시·조회공시 같은 사유로 거래소가 별도로 걸 수도 있고 그런 건 여기 안 나옵니다. 실제 주문 직전에는 현재가 조회의 관리종목·거래정지 구분값을 같이 보는 편이 안전하고, 장중 변동성완화장치까지 고려한다면 VI 발동 시 봇 주문 처리도 같이 보십시오.

5. 함정 ④ 파라미터가 미묘하게 다르다

12종이 거의 같은 모양이라 함수 하나를 복사해 12번 쓰고 싶어집니다. 그런데 세 곳만 파라미터가 다릅니다.

엔드포인트추가 파라미터
ksdinfo/dividendGB1 (필수) · HIGH_GB0 배당전체 / 1 결산배당 / 2 중간배당. HIGH_GB는 공백
ksdinfo/paidin-capinGB11 청약일별 / 2 기준일별 — 같은 이름인데 값의 뜻이 배당과 다르다
ksdinfo/rev-splitMARKET_GB0 전체 / 1 코스피 / 2 코스닥
나머지 9종없음CTS · F_DT · T_DT · SHT_CD 네 개뿐

GB1이 두 곳에 나오는데 값 체계가 완전히 다릅니다 — 배당의 1은 결산배당, 유상증자의 1은 청약일별 정렬입니다. 공통 상수로 빼 두면 정확히 여기서 사고가 납니다.

# 공식 예제 ksdinfo_dividend.py 가 실제로 갖고 있는 필수 검증
if not gb1:
    logger.error("gb1 is required. (e.g. '0')")
    raise ValueError("gb1 is required. (e.g. '0')")
if not f_dt:
    raise ValueError("f_dt is required. (e.g. '20230101')")
if not t_dt:
    raise ValueError("t_dt is required. (e.g. '20231231')")

6. 실전 코드 — 하루 한 번 긁어 캘린더로

기업 행위 일정은 장중에 바뀌는 데이터가 아닙니다. 장 시작 전에 한 번 받아 캐시에 넣고 종일 재사용하는 게 맞습니다. 호출 예산을 아끼는 쪽이 언제나 유리합니다 (호출 제한 설계).

import requests, datetime as dt

BASE = "https://openapi.koreainvestment.com:9443"   # 실전 도메인

# 봇이 실제로 봐야 하는 5종 — 가격·수량·주문 가능 여부가 바뀌는 사건만
CORPORATE_ACTIONS = {
    "dividend":     ("HHKDB669102C0", {"GB1": "0", "HIGH_GB": ""}),  # 배당락
    "bonus-issue":  ("HHKDB669101C0", {}),                            # 무상증자 권리락
    "paidin-capin": ("HHKDB669100C0", {"GB1": "2"}),                  # 유상증자(기준일별)
    "rev-split":    ("HHKDB669105C0", {"MARKET_GB": "0"}),            # 액면분할·병합
    "merger-split": ("HHKDB669104C0", {}),                            # 합병·분할(거래정지)
    "cap-dcrs":     ("HHKDB669106C0", {}),                            # 감자(거래정지)
}

def fetch_schedule(token, appkey, appsecret, path, days=30):
    """SHT_CD 공백 = 전 종목. 앞으로 days일치를 한 번에 받는다."""
    tr_id, extra = CORPORATE_ACTIONS[path]
    today = dt.date.today()
    params = {"CTS": "", "SHT_CD": "",     # ← SHT_CD 공백이 '전 종목'이다
              "F_DT": today.strftime("%Y%m%d"),
              "T_DT": (today + dt.timedelta(days=days)).strftime("%Y%m%d"), **extra}
    headers = {"content-type": "application/json; charset=utf-8",
               "authorization": f"Bearer {token}", "appkey": appkey,
               "appsecret": appsecret, "tr_id": tr_id, "custtype": "P"}
    rows, tr_cont = [], ""
    while True:
        headers["tr_cont"] = tr_cont
        res = requests.get(f"{BASE}/uapi/domestic-stock/v1/ksdinfo/{path}",
                           headers=headers, params=params, timeout=10)
        res.raise_for_status()
        body = res.json()
        rows += body.get("output1") or []
        tr_cont = res.headers.get("tr_cont", "")
        if tr_cont != "M":                 # M 이면 다음 페이지가 있다
            break
        params["CTS"] = body.get("cts", "")
    return rows

받은 행을 종목코드별로 묶어 두면 주문 직전에 "오늘 이 종목에 예정된 사건이 있는가"를 한 줄로 물을 수 있습니다. 있으면 신규 진입을 건너뛰거나 수량을 줄입니다.

from collections import defaultdict

def build_calendar(all_rows: dict[str, list]) -> dict[str, list]:
    """{'005930': [('dividend','20261230'), ('rev-split','20261210')], ...}"""
    cal = defaultdict(list)
    for kind, rows in all_rows.items():
        for r in rows:
            code = (r.get("sht_cd") or "").strip()
            # 기준일 이름이 엔드포인트마다 다르다 — 세 후보를 모두 본다
            when = r.get("record_date") or r.get("list_dt") or r.get("depo_date")
            if code and when:
                cal[code].append((kind, when))
    return dict(cal)

며칠을 피하는 것이 좋은지는 이 글이 답할 수 없습니다. 특정 회피 기간이 수익을 올린다고 주장하지 않습니다. 필터를 넣으면 진입 횟수가 줄어 백테스트 표본 수가 작아지고 결과가 우연에 가까워집니다.

7. 연속조회 — CTS와 tr_cont

SHT_CD를 비우고 기간을 길게 잡으면 한 번에 다 오지 않습니다. 응답 헤더 tr_contM이면 다음 페이지가 있다는 뜻이고, 응답 본문의 cts를 다음 요청의 CTS에 넣어 다시 부릅니다. 공식 예제는 이걸 재귀로 짜면서 max_depth=10을 걸어 뒀습니다 — 기간을 넓게 잡으면 10페이지에서 조용히 잘립니다.

# 공식 예제의 안전장치 — 그대로 쓰면 데이터가 잘릴 수 있다
if depth >= max_depth:
    logger.warning("Maximum recursion depth (%d) reached. Stopping further requests.", max_depth)
    return dataframe if dataframe is not None else pd.DataFrame()

그래서 실무에서는 기간을 잘라 여러 번 부르는 쪽이 안전합니다. 한 달씩 끊어 12번 부르는 게 1년을 한 번에 부르고 10페이지에서 끊기는 것보다 낫습니다.

8. 백테스트에 쓸 때 — 두 가지 편향

과거 데이터로 잘 나온 규칙이 앞으로도 통한다는 보장은 없습니다. 이 글은 특정 종목이나 전략을 권하지 않으며 투자 판단은 본인 책임입니다.

9. 자주 묻는 질문

KIS API로 배당 일정을 조회하려면 어느 엔드포인트를 쓰나요?

/uapi/domestic-stock/v1/ksdinfo/dividend + tr_id HHKDB669102C0. GB1 필수(0 전체 / 1 결산 / 2 중간)이고 SHT_CD 공백이면 전 종목입니다.

배당일정 응답에 배당락일이 들어 있나요?

없습니다. 날짜는 record_date(기준일)와 지급일 3종뿐입니다. 배당락일은 T+2 결제 규칙에 따라 기준일 직전 영업일로 직접 계산해야 하고, 공휴일이 끼면 앞당겨집니다. 실제 일정은 각 사 공시와 거래소 안내로 확인하십시오.

봇이 매매거래정지 기간을 미리 알 수 있나요?

td_stop_dt를 주는 것은 자본감소(HHKDB669106C0)·합병분할(HHKDB669104C0)·액면교체(HHKDB669105C0) 세 곳입니다. 다만 예정 일정이므로 공시성 거래정지는 잡히지 않습니다.

12개를 전부 호출해야 하나요?

보통 5~6개면 충분합니다 — 배당·무상증자·유상증자·액면교체·합병분할·자본감소. 하루 한 번 장 전에 긁어 캐시하고, 기간은 한 달씩 끊어 부르십시오 (KIS 에러코드 정리 · 초당 한도는 포털의 현재 값 확인).

백테스트에 그대로 써도 되나요?

권하지 않습니다. 정보가 공개된 시점을 따로 기록하지 않으면 미래참조가 되고, 폐지 종목이 빠지면 생존편향이 생깁니다. 투자 판단은 본인 책임입니다.

기업 행위까지 챙기는 봇이 필요하다면

배당락·액면분할·거래정지를 미리 읽고 스스로 피하는 구조로 맞춰 만들어 드립니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기