AlgoLab Blog · 키움증권 시세 실무 · 2026

키움 REST API 공매도 추이 ka10014 — 함정 6가지

키움증권 · 공매도 데이터 2026-09-03 · 약 8분 읽기 · 알고랩 AlgoLab
한 줄 요약

키움 REST API의 ka10014 공매도추이요청POST /api/dostk/shsastk_cd·strt_dt·end_dt·tm_tp를 보내면 shrts_trnsn 배열로 일자별 공매도량·누적공매도량·매매비중·공매도평균가·공매도거래대금을 돌려줍니다. 걸리는 곳은 여섯입니다. ⑴ 이 API에 공매도 '잔고'는 없습니다 — 잔고 성격의 값은 대차거래 쪽 ka20068rmnd·remn_amt이고, 대차잔고와 공매도잔고는 같은 개념이 아닙니다. ⑵ ovr_shrts_qty조회 구간 누적이지 미결제 잔량이 아닙니다. ⑶ tm_tp0=시작일 / 1=기간이고 기본값이 1입니다. ⑷ 연속조회가 MAX_PAGES=10에서 조용히 잘립니다. ⑸ 수치가 전부 문자열이고 예제의 포매터는 표시 전용입니다. ⑹ 업무 오류도 HTTP 200으로 오고, 종목코드는 거래소별로 _NX·_AL이 붙습니다.

목차

  1. ka10014가 실제로 주는 것
  2. 함정 1 — 여기엔 '잔고'가 없다
  3. 함정 2 — ovr_shrts_qty는 구간 누적이다
  4. 함정 3 — tm_tp 0과 1
  5. 함정 4 — 10페이지에서 조용히 잘린다
  6. 함정 5 — 값은 전부 문자열이다
  7. 함정 6 — HTTP 200과 거래소 접미사
  8. 실전 코드
  9. 자주 묻는 질문

1. ka10014가 실제로 주는 것

키움증권 공식 REST API 저장소의 examples/국내주식/공매도/get_domestic_short_sale_trend.py 머리말에 스펙이 그대로 적혀 있습니다.

# ---
# api_id: ka10014
# api_name: 공매도추이요청
# category: 국내주식
# sub_category: 공매도
# api_url: /api/dostk/shsa
# menu_path: 국내주식 > 공매도 > 공매도추이요청(ka10014)
# ---

요청 바디는 네 개뿐입니다. stk_cd(종목코드)·strt_dt(시작일자 YYYYMMDD)·end_dt(종료일자)가 필수이고 tm_tp(시간구분)가 선택입니다. 예제 코드는 앞의 셋이 비어 있으면 ValueError를 먼저 던집니다.

body = {
    "stk_cd": "005930",     # 종목코드
    "strt_dt": "20250501",  # 시작일자
    "end_dt":  "20250519",  # 종료일자
}
if tm_tp is not None:
    body["tm_tp"] = tm_tp   # 시간구분 — 0:시작일, 1:기간

응답은 shrts_trnsn이라는 키 아래 배열로 옵니다. 예제의 COLUMNS 매핑이 필드 의미의 1차 출처입니다.

필드의미봇에서 쓰는 법
dt일자YYYYMMDD 문자열 — 인덱스로 쓸 때 파싱 필요
close_pric종가shrts_avg_pric과 비교할 기준선
pred_pre_sig전일대비기호부호 코드값. 의미는 공식 문서로 확인
pred_pre전일대비부호가 붙은 문자열
flu_rt등락율소수·부호 포함 문자열
trde_qty거래량trde_wght의 분모
shrts_qty공매도량그날 체결된 공매도 수량
ovr_shrts_qty누적공매도량잔고가 아니다 — 3절 참고
trde_wght매매비중공매도량 ÷ 거래량 성격의 비율
shrts_trde_prica공매도거래대금금액 기준 규모
shrts_avg_pric공매도평균가그날 공매도 체결의 평균 단가

2. 함정 1 — 여기엔 '잔고'가 없다

공매도 데이터를 찾는 사람의 절반은 사실 잔고를 찾고 있습니다. "이 종목에 공매도가 얼마나 쌓여 있나"가 궁금한 것이지, "어제 하루 얼마나 팔았나"가 궁금한 게 아니기 때문입니다. 그런데 위 열한 개 필드 어디에도 잔고가 없습니다.

잔고 성격의 값은 다른 카테고리에 있습니다. 같은 저장소의 examples/국내주식/대차거래/에 있는 ka20068 대차거래추이요청(종목별)입니다.

구분ka10014 공매도추이ka20068 대차거래추이(종목별)
경로/api/dostk/shsa/api/dostk/slb
응답 키shrts_trnsndbrt_trde_trnsn
바디stk_cd·strt_dt·end_dt·tm_tpstk_cd·strt_dt·end_dt·all_tp
거래 수량shrts_qty 공매도량dbrt_trde_cntrcnt 체결주수 / dbrt_trde_rpy 상환주수
잔고없음rmnd 잔고주수 · remn_amt 잔고금액

대차잔고 ≠ 공매도잔고ka20068rmnd대차거래 잔고입니다. 주식을 빌린 뒤 아직 갚지 않은 수량이고, 대차는 공매도 외의 목적으로도 발생합니다. 그래서 대차잔고를 공매도잔고로 그대로 갖다 쓰면 과대 계상됩니다. 두 값의 성격 차이를 코드 주석에 명시해 두고, 공시 기준의 공매도 잔고가 필요하면 KRX 정보데이터시스템과 금융감독원 공시를 별도로 확인하십시오. 같은 /api/dostk/slb 경로에는 전체 추이 ka10068과 상위10종목 ka10069도 함께 있습니다.

3. 함정 2 — ovr_shrts_qty는 구간 누적이다

필드명이 누적공매도량이라 잔량으로 읽기 쉽지만, 이 값은 조회 구간 안에서 공매도 거래량이 쌓인 합의 성격입니다. 빌린 주식을 갚으면 줄어드는 잔고와 달리, 거래량 누적은 상환이 반영되지 않습니다. 즉 되갚기가 아무리 많아도 이 숫자는 내려가지 않습니다.

실무에서는 이렇게 구분해 쓰는 편이 안전합니다.

구간을 바꾸면 값이 바뀐다 — 누적값이 조회 구간에 종속되므로, strt_dt를 다르게 잡으면 같은 날짜의 ovr_shrts_qty가 달라질 수 있습니다. 봇에서 이 값을 캐시할 계획이라면 어떤 구간으로 받은 값인지를 함께 저장하십시오. 확인 방법은 간단합니다 — 같은 종료일에 시작일만 바꿔 두 번 호출해 마지막 행을 비교해 보면 됩니다.

4. 함정 3 — tm_tp 0과 1

예제 함수의 docstring에 적힌 설명은 한 줄입니다.

tm_tp: 시간구분 — 0:시작일, 1:기간          # 기본값 '1'

기본값 '1'strt_dt~end_dt 구간 조회입니다. '0'은 시작일 기준이라 응답 행 수가 달라집니다. 선택 파라미터라 None이면 바디에서 아예 빠지는데, 그 경우 서버 기본 동작이 무엇인지는 예제만으로 확정할 수 없습니다. 봇에 넣기 전에 두 값을 각각 한 번씩 호출해 응답 행 수를 눈으로 비교하고, 쓰는 값을 명시적으로 고정하십시오. 이런 코드값 계열은 차트 조회의 기간 구분에서도 같은 방식으로 확인해야 합니다.

5. 함정 4 — 10페이지에서 조용히 잘린다

키움 REST API의 연속조회는 응답 헤더의 cont-ynnext-key를 다음 요청에 다시 실어 보내는 구조입니다. 공식 예제의 루프는 이렇게 생겼습니다.

MAX_PAGES = 10             # 최대 조회 페이지 수
REQUEST_DELAY_SECONDS = 0.2

for page in range(MAX_PAGES):
    response = client.fetch_page(api_id=API_ID, path=API_URL, body=body,
                                 cont_yn=next_cont_yn, next_key=next_key)
    ...
    next_cont_yn = response.continuation.cont_yn
    next_key     = response.continuation.next_key

    if next_cont_yn != "Y":
        break
    if page + 1 >= MAX_PAGES:
        break                      # ← 경고도 예외도 없이 그냥 끝난다
    time.sleep(REQUEST_DELAY_SECONDS)

여기가 가장 늦게 발견되는 버그입니다 — 마지막 break아직 cont_ynY인 상태에서도 루프를 끝냅니다. 예외도 로그도 없어서, 몇 년치를 요청하면 앞쪽 일부만 받고 정상 종료한 것처럼 보입니다. 예제를 복사해 쓸 거라면 최소한 if next_cont_yn == "Y": logging.warning("잘림") 한 줄은 넣으십시오. 호출 간격 0.2초는 예제값이며, 실제 한도는 키움 REST API 429 대응을 참고하시고 공식 문서로 현재 값을 확인하십시오.

연속조회 자체의 동작 방식은 cont-yn·next-key 연속조회에서 더 자세히 다뤘습니다.

6. 함정 5 — 값은 전부 문자열이다

예제에 들어 있는 _format_display_value를 값 변환 함수로 착각하면 안 됩니다. 실제 동작은 이렇습니다.

text = str(value).strip()
sign = "-" if text.startswith("-") else ""
unsigned = text[1:] if sign else text
...
if unsigned.isdigit() and len(unsigned) >= 6:
    return f"{sign}{int(unsigned or '0'):,}"     # 6자리 이상만 콤마
return value                                      # 나머지는 원본 그대로

부호를 먼저 떼고, 여섯 자리 이상일 때만 콤마를 찍습니다. 다섯 자리 값은 콤마 없이 그대로 나가므로 출력 서식이 자릿수에 따라 달라집니다. 이건 화면에 예쁘게 찍기 위한 코드지 숫자 변환이 아닙니다. 계산에 쓸 값은 별도 함수로 처리하십시오.

def to_num(v, default=0.0):
    if v is None:
        return default
    s = str(v).replace(",", "").strip()
    if s in ("", "-", "+"):
        return default
    try:
        return float(s)
    except ValueError:
        return default

wght = to_num(row["trde_wght"])          # 매매비중
qty  = to_num(row["shrts_qty"])          # 공매도량
avgp = to_num(row["shrts_avg_pric"])     # 공매도평균가

7. 함정 6 — HTTP 200과 거래소 접미사

키움 REST API는 업무 오류도 HTTP 200으로 내려보내고 실패 여부는 응답 본문의 return_code에만 담습니다. 예제도 return_codeNone0도 아닐 때 return_msg와 함께 별도 메시지 표로 모아 둡니다. requestsraise_for_status()만 믿으면 실패를 성공으로 처리하게 됩니다.

종목코드도 주의해야 합니다. stk_cd 설명에 거래소별 표기가 명시돼 있습니다.

거래소stk_cd 표기
KRX039490
NXT039490_NX
SOR039490_AL

같은 종목이라도 접미사에 따라 다른 요청이 됩니다. 거래소 분리 자체가 봇 설계에 주는 영향은 NXT·KRX 주문 라우팅에서 다뤘습니다.

8. 실전 코드

공식 예제를 그대로 쓰되 앞의 함정 세 개(잘림 경고·숫자 변환·return_code)를 막은 형태입니다.

import logging, time
import pandas as pd
from kiwoom import get_client, KiwoomError

API_ID, API_URL = "ka10014", "/api/dostk/shsa"
COLUMNS = {"dt":"일자","close_pric":"종가","trde_qty":"거래량",
           "shrts_qty":"공매도량","ovr_shrts_qty":"누적공매도량",
           "trde_wght":"매매비중","shrts_trde_prica":"공매도거래대금",
           "shrts_avg_pric":"공매도평균가"}

def short_sale_trend(stk_cd, strt_dt, end_dt, tm_tp="1", max_pages=50):
    client = get_client()
    rows, cont_yn, next_key = [], None, None

    for page in range(max_pages):
        res = client.fetch_page(api_id=API_ID, path=API_URL,
                                body={"stk_cd": stk_cd, "strt_dt": strt_dt,
                                      "end_dt": end_dt, "tm_tp": tm_tp},
                                cont_yn=cont_yn, next_key=next_key)
        b = res.body
        if b.get("return_code") not in (None, 0):          # HTTP 200이어도 실패일 수 있다
            raise KiwoomError(f'{b.get("return_code")} {b.get("return_msg")}')

        rows.extend(r for r in b.get("shrts_trnsn", []) if isinstance(r, dict))
        cont_yn, next_key = res.continuation.cont_yn, res.continuation.next_key
        if cont_yn != "Y":
            break
        if page + 1 >= max_pages:
            logging.warning("연속조회가 %d페이지에서 잘렸다 — 구간을 줄이거나 max_pages를 늘릴 것",
                            max_pages)
            break
        time.sleep(0.2)

    df = pd.DataFrame(rows).rename(columns=COLUMNS)
    for c in ("거래량","공매도량","누적공매도량","매매비중","공매도거래대금","공매도평균가","종가"):
        if c in df:
            df[c] = pd.to_numeric(df[c].astype(str).str.replace(",", ""), errors="coerce")
    return df.assign(일자=pd.to_datetime(df["일자"], format="%Y%m%d")).set_index("일자").sort_index()

if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    df = short_sale_trend("005930", "20250501", "20250519")
    print(df[["종가","공매도량","매매비중","공매도평균가"]].tail())

제도 변화가 데이터에 섞인다 — 금융위원회는 2025년 3월 31일부터 공매도를 전면 재개한다고 발표한 바 있습니다. 그 전후 구간을 한 시계열로 이어 붙이면 제도 변화가 그대로 데이터에 들어갑니다. 백테스트에 넣을 때는 구간을 나눠 보시고, 사라진 종목까지 포함한 유니버스 문제는 생존편향 백테스트에서 따로 다뤘습니다. 현재 제도와 수치는 KRX·금융위원회 공식 자료로 확인하십시오.

9. 정리

고지 — 알고랩(퀀트웍스)은 투자자문업·투자일임업을 영위하지 않으며, 고객의 규칙을 코드로 옮기는 도구 제공만 합니다. 이 글은 특정 종목이나 매매 전략을 추천하지 않고 수익을 보장하지 않습니다. 본문의 API 스펙은 2026-09-03 기준 키움증권 공식 REST API 저장소의 예제 코드를 대조한 것이며 공지로 변경될 수 있으니, 개발 전 키움증권 개발자 포털의 현재 명세를 직접 확인하십시오. 공매도 제도와 공시 수치는 KRX·금융위원회 공식 자료가 기준입니다.

키움 REST API로 돌아가는 봇, 만들어 드립니다

데이터 수집부터 주문·잔고까지 요구사항만 주시면 됩니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기