키움 ka10036 외인한도소진율 — 소진율증가는 %p다
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 기준)
목차
- ka10034·ka10035와 어디가 다른가
- 요청·응답 실물
- 숫자 세 개의 관계 — 검산
- 함정 5가지
- 최소 호출 코드
- 자주 묻는 질문
1. ka10034·ka10035와 어디가 다른가
어제 정리한 ka10034 외인기간별매매상위는 "기간 동안 외국인이 몇 주를 샀나"(순매수량)를 줍니다. ka10036은 같은 /api/dostk/rkinfo 경로, 같은 dt 코드표를 쓰지만 보는 숫자가 다릅니다. 외국인이 살 수 있는 한도 중 얼마를 채웠나(한도소진율)가 기준이고, 그게 기간 동안 많이 오른 순서로 줄을 세웁니다.
| 항목 | ka10036 소진율증가상위 | ka10034 기간별매매상위 | ka10035 연속순매매상위 |
|---|---|---|---|
| 요청 body | mrkt_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_upper | for_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
두 가지가 확인됩니다.
- 한도소진율 = 보유 ÷ (보유 + 취득가능) × 100. 519,785 ÷ 31,924,499 = 1.628% →
+1.63. 보유와 취득가능을 더한 값이 곧 외국인 한도입니다. 같은 관계가 ka10008(주식외국인종목별매매동향) 예시에서도 성립합니다 —poss_stkcnt6,663,509 +gain_pos_stkcnt18,863,197 =frgnr_limit25,526,706, 소진률+26.10. - 소진율증가 = 한도소진율 − 기준한도소진율. 1행은 정확히 맞고, 2행은 0.01 차이가 납니다. 서버가 반올림 전 값으로 빼고 나서 반올림하기 때문으로 보이며, 직접 다시 계산하면 끝자리가 어긋날 수 있다는 뜻입니다. 정렬과 필터에는 서버 값
exh_rt_incrs를 그대로 쓰십시오.
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 쪽이 간단합니다.