AlgoLab Blog · 키움증권 REST · 수급 데이터 · 2026

키움 ka10036 외인한도소진율 — 소진율증가는 %p다

키움 REST · 순위정보 2026-10-17 · 약 6분 읽기 · 알고랩 AlgoLab
한 줄 요약

ka10036(외인한도소진율증가상위)이 주는 exh_rt_incrs(소진율증가)는 "몇 % 늘었나"가 아니라 limit_exh_rt(한도소진율) − base_limit_exh_rt(기준한도소진율), 즉 퍼센트포인트(%p) 차이입니다. 한도소진율 자체는 poss_stkcnt ÷ (poss_stkcnt + gain_pos_stkcnt) × 100으로 공식 예시와 소수 둘째 자리까지 맞습니다. 요청은 mrkt_tp·dt·stex_tp 세 개뿐이라 감소 상위는 받을 수 없고, 공식 예시의 기준한도소진율은 −283.33처럼 음수라 그대로 쓰면 안 됩니다. (공식 명세 kiwoom_api_spec.json·예제, 2026-10-17 main 953e5db 기준)

목차

  1. ka10034·ka10035와 어디가 다른가
  2. 요청·응답 실물
  3. 숫자 세 개의 관계 — 검산
  4. 함정 5가지
  5. 최소 호출 코드
  6. 자주 묻는 질문

1. ka10034·ka10035와 어디가 다른가

어제 정리한 ka10034 외인기간별매매상위는 "기간 동안 외국인이 몇 주를 샀나"(순매수량)를 줍니다. ka10036은 같은 /api/dostk/rkinfo 경로, 같은 dt 코드표를 쓰지만 보는 숫자가 다릅니다. 외국인이 살 수 있는 한도 중 얼마를 채웠나(한도소진율)가 기준이고, 그게 기간 동안 많이 오른 순서로 줄을 세웁니다.

항목ka10036 소진율증가상위ka10034 기간별매매상위ka10035 연속순매매상위
요청 bodymrkt_tp·dt·stex_tp (3개)+ trde_tp (4개)trde_tp·base_dt_tp 등
방향 선택없음 — 증가만1:순매도·2:순매수·3:순매매연속순매도·연속순매수
정렬 기준exh_rt_incrs (%p)netprps_qty (주)tot (D-1~D-3 합)
목록 키for_limit_exh_rt_incrs_upperfor_dt_trde_upper—
호가없음sel_bid·buy_bid없음

즉 ka10036은 "많이 산 종목"이 아니라 "외국인 비중이 빠르게 차오르는 종목"을 찾는 TR입니다. 대형주는 순매수량이 커도 소진율은 조금 움직이고, 유통 주식이 적은 종목은 적게 사도 소진율이 크게 뜁니다. 두 목록이 다르게 나오는 게 정상입니다.

2. 요청·응답 실물

명세의 request_example 원문입니다. 공식 예제 examples/국내주식/순위정보/get_domestic_foreign_investor_limit_exhaustion_rate_increase_top.py의 __main__도 mrkt_tp='000'·dt='1'·stex_tp='1'로 같습니다.

POST https://api.kiwoom.com/api/dostk/rkinfo
api-id: ka10036
authorization: Bearer {접근토큰}

{
    "mrkt_tp": "000",
    "dt": "1",
    "stex_tp": "1"
}

응답 response_example 원문(2행 중 2행째)입니다. 숫자가 전부 부호 붙은 문자열로 옵니다.

{
    "for_limit_exh_rt_incrs_upper": [
        {
            "rank": "2",
            "stk_cd": "005930",
            "stk_nm": "삼성전자",
            "cur_prc": "1590",
            "pred_pre_sig": "3",
            "pred_pre": "0",
            "trde_qty": "0",
            "poss_stkcnt": "519785",
            "gain_pos_stkcnt": "31404714",
            "base_limit_exh_rt": "-101.25",
            "limit_exh_rt": "+1.63",
            "exh_rt_incrs": "+102.87"
        }
    ],
    "return_code": 0,
    "return_msg": "정상적으로 처리되었습니다"
}
element한글명명세 설명
poss_stkcnt보유주식수단위: 1주
gain_pos_stkcnt취득가능주식수단위: 1주
base_limit_exh_rt기준한도소진율단위: %, 부호 포함 소수점 둘째 자리까지
limit_exh_rt한도소진율단위: %, 부호 포함 소수점 둘째 자리까지
exh_rt_incrs소진율증가단위: %, 부호 포함 소수점 둘째 자리까지
cur_prc · pred_pre · pred_pre_sig현재가 · 전일대비 · 기호기호 1:상한가, 2:상승, 3:보합, 4:하한가, 5:하락

3. 숫자 세 개의 관계 — 검산

명세는 각 필드에 "단위: %"만 적고 계산식을 주지 않습니다. 그래서 공식 예시 두 행으로 직접 맞춰 봤습니다(pandas 2.2.3, Python 3.10.4, 명세 JSON의 response_example을 그대로 읽음).

df["calc_rt"]    = (df.poss_stkcnt / (df.poss_stkcnt + df.gain_pos_stkcnt) * 100).round(2)
df["calc_incrs"] = (df.limit_exh_rt - df.base_limit_exh_rt).round(2)

 rank  poss_stkcnt  gain_pos_stkcnt  limit_exh_rt  calc_rt  base_limit_exh_rt  exh_rt_incrs  calc_incrs
    1            0           600000          0.00     0.00            -283.33        283.33      283.33
    2       519785         31404714          1.63     1.63            -101.25        102.87      102.88

두 가지가 확인됩니다.

외국인 한도 = poss_stkcnt + gain_pos_stkcnt 기준일 base 30% 오늘 limit 40% exh_rt_incrs = +10 (%p) "33% 늘었다"가 아니라 "10%p 올랐다" — 막대 길이는 개념도
소진율증가는 두 막대의 끝 차이(%p)다

4. 함정 5가지

① 비율 변화로 읽으면 순위를 오해한다

소진율 30%→40%와 2%→12%는 둘 다 exh_rt_incrs +10입니다. "증가율"로 읽어 33%와 500%로 바꿔 비교하면 저소진 종목이 위로 튀는 다른 목록이 됩니다. 봇에 넣을 때 변수 이름부터 exh_pp처럼 %p임을 드러내 두면 나중에 실수가 줄어듭니다.

② 공식 예시의 기준소진율은 음수다

예시의 base_limit_exh_rt는 -283.33·-101.25입니다. 소진율은 0~100% 범위여야 하므로 이 값은 형식 확인용 자리표시자로 봐야 합니다(종목코드도 두 행 모두 005930, 현재가는 14,255원·1,590원). 이 예시로 단위 테스트를 만들면 범위 검사를 넣는 순간 테스트가 깨집니다. 반대로 운영 응답에 0 미만이나 100 초과가 섞여 오면 결측으로 처리하는 방어 코드를 두십시오.

③ 감소 상위는 없다 — trde_tp가 빠졌다

ka10034에는 있는 매매구분이 ka10036 요청에는 없습니다. "외국인 비중이 빠지는 종목"을 매도 신호 후보로 쓰고 싶다면 이 TR로는 안 됩니다. 종목을 정해 두고 ka10008의 일자별 limit_exh_rt로 직접 차이를 계산하거나, ka10001(주식기본정보)의 for_exh_rt(외인소진률)를 매일 저장해 비교해야 합니다. ka10008 요청은 stk_cd 하나라 종목 수만큼 호출이 늘어나니 키움 429 호출 제한을 먼저 계산하십시오.

④ dt 오타 10;10일과 기준일 없음 — ka10034와 같다

dt 설명은 0:당일, 1:전일, 5:5일, 10;10일, 20:20일, 60:60일 그대로입니다. 콜론 대신 세미콜론이 들어간 곳이 있어 split(':') 파서가 ValueError를 냅니다(재현 결과는 ka10034 글 ②). 기준일 필드가 없어 "작년 3월의 20일 소진율 증가 상위"는 다시 받을 수 없다는 점도 같습니다. 백테스트에 쓰려면 오늘부터 매일 저장하는 수밖에 없습니다.

⑤ 공식 예제의 숫자 변환 목록에 보유주식수가 없다

공식 예제의 NUMERIC_COLUMNS는 거래량·기준한도소진율·소진율증가·취득가능주식수·한도소진율·현재가 여섯 개입니다. 보유주식수와 전일대비는 빠져 있습니다. 이 목록은 화면 출력용 천 단위 쉼표 처리(_format_display)에만 쓰이고, 반환 DataFrame은 12개 열이 전부 object(문자열)입니다. 예제를 그대로 가져다 poss_stkcnt로 정렬하면 "9" > "519785" 같은 문자열 순서가 나옵니다. 숫자 변환은 직접 하십시오.

명세가 비워 둔 것 — stex_tp 1(KRX)·2(NXT)·3(통합)에 따라 소진율 숫자가 달라지는지, dt 0을 장중에 불렀을 때 잠정치인지, 몇 종목까지 오는지는 명세에 없습니다. 키움 HTS 도움말(0241 외국인한도소진율상위)은 외국인 정보가 금융감독원 제공 데이터이고 장외거래·DR 입고·전환사채 전환·증자 결과까지 포함한다고 설명하므로, 장중 체결과 실시간으로 맞물리는 숫자라고 가정하지 마십시오. 첫 운영 호출에서 확인이 필요합니다.

5. 최소 호출 코드

토큰은 환경변수에서 읽고, 연속조회는 공식 예제와 같은 MAX_PAGES = 10·REQUEST_DELAY_SECONDS = 0.2입니다. ①·②·⑤를 코드에서 막습니다.

import os, time, requests
import pandas as pd

BASE  = "https://api.kiwoom.com"              # 모의: https://mockapi.kiwoom.com
TOKEN = os.environ["KIWOOM_TOKEN"]
DT_OK = {"0", "1", "5", "10", "20", "60"}
NUM   = ["poss_stkcnt", "gain_pos_stkcnt", "trde_qty",
         "base_limit_exh_rt", "limit_exh_rt", "exh_rt_incrs"]

def ka10036(dt="5", mrkt_tp="000", stex_tp="1", max_pages=10, delay=0.2):
    if str(dt) not in DT_OK:
        raise ValueError(f"ka10036 dt={dt} 불가")
    body = {"mrkt_tp": mrkt_tp, "dt": str(dt), "stex_tp": stex_tp}
    rows, cont_yn, next_key = [], None, None
    for _ in range(max_pages):
        h = {"Content-Type": "application/json;charset=UTF-8",
             "authorization": f"Bearer {TOKEN}", "api-id": "ka10036"}
        if cont_yn == "Y":
            h.update({"cont-yn": "Y", "next-key": next_key})
        r = requests.post(BASE + "/api/dostk/rkinfo", headers=h, json=body, timeout=10)
        j = r.json()
        if j.get("return_code") not in (None, 0):
            raise RuntimeError(f"ka10036 {j.get('return_code')}: {j.get('return_msg')}")
        rows += j.get("for_limit_exh_rt_incrs_upper", [])
        cont_yn, next_key = r.headers.get("cont-yn"), r.headers.get("next-key")
        if cont_yn != "Y":
            break
        time.sleep(delay)
    df = pd.DataFrame(rows)
    df[NUM] = df[NUM].apply(pd.to_numeric, errors="coerce")       # ⑤ 전부 숫자로
    for c in ["base_limit_exh_rt", "limit_exh_rt"]:                # ② 0~100 밖은 결측
        df.loc[~df[c].between(0, 100), c] = pd.NA
    df = df.rename(columns={"exh_rt_incrs": "exh_pp"})             # ① %p임을 이름에
    df["limit_qty"] = df["poss_stkcnt"] + df["gain_pos_stkcnt"]    # 외국인 한도(주)
    df["dt"], df["asof"] = dt, pd.Timestamp.now().strftime("%Y-%m-%d")
    return df

return_code가 0이 아닐 때의 번호별 의미는 키움 return_code 글, cont-yn·next-key 헤더는 연속조회 글에 있습니다. 같은 경로의 다른 순위 TR과 api-id만 바꿔 쓸 때 생기는 일은 rkinfo 글을 보십시오.

봇에서 쓴다면 장 마감 뒤 dt 5로 받아 asof와 함께 저장하고, 같은 날 ka10034 dt 5 순매수 목록과 겹치는 종목을 따로 표시하는 정도가 무난합니다. 한국투자증권 계좌라면 순위 TR 대신 주식현재가 시세(FHKST01010100) 응답의 hts_frgn_ehrt(HTS 외국인 소진율)를 종목별로 쌓는 방식이 됩니다. 외국인 수급은 해석이 갈리는 데이터라 이 목록 자체가 매매 근거가 되지는 않으며, 이 글은 특정 종목이나 전략을 권하지 않습니다. 명세는 바뀔 수 있으니 키움 REST API 공식 문서로 현재 값을 확인하십시오.

6. 자주 묻는 질문

ka10036의 exh_rt_incrs 283.33은 283% 늘었다는 뜻인가요?

아닙니다. 공식 예시에서 exh_rt_incrs는 limit_exh_rt(한도소진율)에서 base_limit_exh_rt(기준한도소진율)를 뺀 값과 일치합니다. 단위는 퍼센트포인트(%p)이고 비율 변화가 아닙니다. 283.33이라는 값 자체는 예시용 자리표시자라 실제 운영 응답에서는 0~100 범위인지부터 확인하십시오.

ka10036으로 외국인 소진율이 줄어든 종목도 볼 수 있나요?

명세상 방향을 고르는 필드가 없습니다. 요청 body는 mrkt_tp·dt·stex_tp 세 개뿐이고 trde_tp가 없어 '증가 상위'만 옵니다. 감소 종목이 필요하면 ka10008(주식외국인종목별매매동향)로 종목별 limit_exh_rt 일자 이력을 받아 직접 계산해야 합니다.

한도소진율과 외국인 지분율은 같은 숫자인가요?

분모가 다릅니다. 한도소진율은 보유주식수 ÷ (보유주식수 + 취득가능주식수), 즉 외국인 한도 대비 보유 비율입니다. 키움 HTS 도움말에 따르면 외국인 한도는 원칙적으로 발행주식의 100%이고 일부 종목만 별도 한도가 있으므로, 한도가 100%인 종목에서는 두 숫자가 거의 같고 한도가 정해진 종목에서는 다릅니다.

한국투자증권 API에도 외국인 소진율이 있나요?

순위 형태는 아니지만 종목 단위로는 있습니다. 주식현재가 시세(FHKST01010100)와 일자별 시세 응답에 hts_frgn_ehrt(HTS 외국인 소진율) 필드가 있습니다. 시장 전체에서 증가 상위를 한 번에 받는 TR은 키움 ka10036 쪽이 간단합니다.

외국인 수급 필터를 넣은 키움 봇, 만들어 드립니다

원하는 조건만 말로 알려 주시면 TR 선택과 이력 저장까지 맞춰 드립니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기