키움 ka10038 종목별증권사순위 — rank_1은 수량이다
ka10038(종목별증권사순위요청)의 맨 위 rank_1·rank_2·rank_3는 순위가 아니라 기간 누적 매수량·매도량·순매수 수량입니다. 회원사별 순위는 stk_sec_rank 배열 안에 따로 있습니다. 경로는 /api/dostk/rkinfo, 필수 body는 stk_cd·qry_tp 2개입니다. 매도량은 "-13253"처럼 음수로 오므로 순매수 = 매수 + 매도이고, 매수 − 매도로 계산하면 틀립니다. 기간 dt는 일수−1("4"=5일)이며, 시작·종료일로 조회할 때는 dt를 빈값으로 보내야 하는데 공식 예제와 Postman 컬렉션은 날짜와 dt를 함께 보냅니다. (공식 명세 kiwoom_api_spec.json·예제, main 953e5db 2026-09-15 기준, 2026-10-24 확인)
목차
- ka10038은 무엇을 주나
- 요청·응답 실물
- 함정 5가지
- 안전하게 받는 코드
- 자주 묻는 질문
1. ka10038은 무엇을 주나
종목 하나를 넣으면 "이 기간에 어느 증권사 창구가 얼마나 사고팔았나"를 회원사별 수량으로 돌려줍니다. 같은 rkinfo 경로의 ka10042(순매수거래원순위)와 짝을 이루는데, 두 TR은 서로 빠진 것을 채웁니다.
| TR | 필수 body | 회원사코드 | 수량 | 방향 선택 | 기간 dt |
|---|---|---|---|---|---|
| ka10038 | stk_cd·qry_tp | 없음(mmcm_nm만) | 있음 | qry_tp 1·2 | 일수−1, 날짜 조회 시 빈값 |
| ka10042 | stk_cd·qry_dt_tp·pot_tp·sort_base | 있음 | 없음 | 순매수만 | 일수 그대로, qry_dt_tp로 전환 |
| ka10040 | stk_cd | 있음 | 있음 | 매수·매도 상위 5 | 당일 |
당일 상위 5개 창구면 ka10040이 간단하고, 5일·20일처럼 기간을 넓혀 전체 창구의 수량을 보려면 ka10038을 씁니다.
2. 요청·응답 실물
명세의 request_example 원문입니다.
POST https://api.kiwoom.com/api/dostk/rkinfo
api-id: ka10038
authorization: Bearer {접근토큰}
{
"stk_cd": "005930",
"strt_dt": "20241106",
"end_dt": "20241107",
"qry_tp": "2",
"dt": "1"
}
| element | 필수 | 명세 설명 |
|---|---|---|
stk_cd | Y | 거래소별 종목코드 (KRX:039490, NXT:039490_NX, SOR:039490_AL) · 길이 6 |
strt_dt / end_dt | N | YYYYMMDD |
qry_tp | Y | 1:순매도순위정렬, 2:순매수순위정렬 |
dt | N | 1:전일, 4:5일, 9:10일, 19:20일, 39:40일, 59:60일, 119:120일 ※ 시작일자와 종료일자로 조회를 원하는 경우 기간(dt)값은 빈값('')으로 설정 |
response_example 원문입니다.
{
"rank_1": "+34881",
"rank_2": "-13253",
"rank_3": "+21628",
"prid_trde_qty": "43",
"stk_sec_rank": [
{ "rank": "1", "mmcm_nm": "키움증권", "buy_qty": "+9800", "sell_qty": "-2813", "acc_netprps_qty": "+6987" },
{ "rank": "2", "mmcm_nm": "키움증권", "buy_qty": "+3459", "sell_qty": "-117", "acc_netprps_qty": "+3342" },
{ "rank": "3", "mmcm_nm": "키움증권", "buy_qty": "+3321", "sell_qty": "-125", "acc_netprps_qty": "+3196" },
{ "rank": "4", "mmcm_nm": "키움증권", "buy_qty": "+3941", "sell_qty": "-985", "acc_netprps_qty": "+2956" }
],
"return_code": 0,
"return_msg": "정상적으로 처리되었습니다"
}
3. 함정 5가지
① rank_1·rank_2·rank_3는 순위가 아니다
필드 이름만 보면 1·2·3위 회원사 같지만 명세의 한글명은 이렇습니다.
| element | 한글명(원문) | 설명(원문) |
|---|---|---|
rank_1 | 기간별 누적 매수량 | 단위: 1주, 부호가 포함된 숫자 |
rank_2 | 기간별 누적 매도량 | 단위: 1주, 부호가 포함된 숫자 |
rank_3 | 기간별 누적 순매수 | 단위: 1주, 부호가 포함된 숫자 |
prid_trde_qty | 기간중거래량 | 단위: 1주 |
명세 337개 TR에서 rank_로 시작하는 응답 필드를 전수로 찾으면 ka00198의 rank_chg(순위 등락), usa01990의 rank_flu 같은 순위 관련 필드뿐이고, 순위가 아닌 수량을 rank_N으로 이름 붙인 곳은 ka10038 하나입니다. 응답을 json_normalize로 펼쳐 rank* 컬럼을 일괄 정수 순위로 다루는 코드는 여기서 엉뚱한 값을 만듭니다. 공식 예제도 이 넷을 회원사 표와 분리해 "요약" 표로 따로 만듭니다.
② 매도는 음수다 — 순매수 = 매수 + 매도
응답 예시 4행으로 두 가지 계산을 해 본 결과입니다(Python 3.10.4·pandas 2.2.3, 2026-10-24 실측).
rank buy_qty sell_qty acc_netprps_qty buy_minus_sell buy_plus_sell
1 9800 -2813 6987 12613 6987
2 3459 -117 3342 3576 3342
3 3321 -125 3196 3446 3196
4 3941 -985 2956 4926 2956
rank_1+rank_2 = 21628 rank_3 = 21628 prid_trde_qty = 43
회원사 행에서도 요약 줄에서도 매수 + 매도 = 순매수가 맞고, 흔히 쓰는 매수 − 매도는 12613처럼 순매수의 두 배 가까운 값을 냅니다. 파이썬 int("+34881")은 34881로 잘 바뀌니 부호 문자열은 그대로 변환하고, 매도 총량을 따로 쓸 때만 abs()를 씌우면 됩니다.
마지막 줄의 prid_trde_qty = 43도 눈여겨볼 만합니다. 기간 거래량이 누적 매수량 34,881주보다 작을 수는 없으니, 응답 예시의 이 값은 자리채움입니다. 회원사명 4개가 전부 "키움증권"인 것도 마찬가지입니다. 예시값으로 단위나 비율 검증 로직을 만들지 마십시오.
③ 기간 dt는 일수−1, 당일은 없다
명세에서 dt에 일수를 받는 TR을 비교하면 ka10038만 번호가 하나씩 작습니다.
| TR | dt 명세 원문 | 날짜 조회 전환 |
|---|---|---|
| ka10038 | 1:전일, 4:5일, 9:10일, 19:20일, 39:40일, 59:60일, 119:120일 | dt 빈값 |
| ka10042 · ka10043 | 5:5일, 10:10일, 20:20일, 40:40일, 60:60일, 120:120일 | qry_dt_tp "1" |
| ka10039 | 0:당일, 1:전일, 5:5일, 10:10일, 60:60일 | — |
| ka10131 | 1:최근일, 3:3일, 5:5일, 10:10일, 20:20일, 120:120일, 0:시작일자/종료일자로 조회 | dt "0" |
같은 "날짜로 조회"를 TR마다 qry_dt_tp, dt="0", dt="" 세 방식으로 받습니다. ka10038에는 ka10039에 있는 0:당일이 없으니, 장중 당일 창구 수량은 ka10040으로 받는 쪽이 명세에 맞습니다.
④ 공식 예제는 날짜와 dt를 함께 보낸다
공식 예제 examples/국내주식/순위정보/get_domestic_stock_broker_rank.py의 함수 기본값은 strt_dt='20241106', end_dt='20241107', dt='1'이고, None이 아닌 값은 모두 body에 넣습니다. Postman 컬렉션도 "strt_dt": "20260101", "end_dt": "20260507"와 "dt": "1"을 같이 보냅니다. 명세 주석은 "날짜로 조회하려면 dt는 빈값"이라고 하니 둘이 충돌합니다. 셋이 함께 오면 서버가 어느 쪽을 쓰는지는 명세에 없습니다. 의도한 기간을 확실히 받으려면 둘 중 하나만 보내십시오.
{"stk_cd": "005930", "qry_tp": "2", "dt": "4"} # 최근 5일
{"stk_cd": "005930", "qry_tp": "2", "strt_dt": "20261001", "end_dt": "20261023", "dt": ""} # 날짜 지정
예제에는 작은 빈틈이 두 개 더 있습니다. 표시용 숫자 목록 NUMERIC_COLUMNS에 '기간별 누적 매수량'은 있는데 '기간별 누적 매도량'·'기간별 누적 순매수'는 없고, 연속조회(cont-yn·next-key)로 페이지를 넘길 때마다 요약 행을 다시 추가해 페이지 수만큼 같은 요약이 쌓입니다. 요약은 첫 페이지 것만 쓰십시오(연속조회 글).
⑤ 회원사코드가 없다 — 이름으로 맞춰야 한다
응답 회원사 필드는 mmcm_nm(이름)뿐입니다. 회원사코드가 필요한 ka10039·ka10043에 넘기려면 이름을 코드로 바꿔야 하는데, ka10102(회원사 리스트) 예시는 "교 보"·"대 신"·"신 영"처럼 이름 가운데 공백 두 칸이 들어 있습니다. 공백을 지운 뒤 비교하고, 그래도 안 맞는 이름은 로그로 남겨 손으로 대응표를 보강하는 편이 안전합니다. 두 TR이 같은 회원사를 같은 문자열로 주는지는 명세에 없습니다.
4. 안전하게 받는 코드
토큰은 환경변수에서 읽고, 위 ①~⑤를 코드에서 막습니다. 오류 응답 해석은 키움 return_code 글을 참고하십시오.
import os, requests
import pandas as pd
BASE = "https://api.kiwoom.com" # 모의: https://mockapi.kiwoom.com
TOKEN = os.environ["KIWOOM_TOKEN"]
DT_38 = {1: "1", 5: "4", 10: "9", 20: "19", 40: "39", 60: "59", 120: "119"} # ③ 일수-1
def ka10038(stk_cd, days=None, start=None, end=None, net_sell=False):
body = {"stk_cd": stk_cd, "qry_tp": "1" if net_sell else "2"}
if start and end:
body.update(strt_dt=start, end_dt=end, dt="") # ④ 날짜 조회면 dt 빈값
else:
body["dt"] = DT_38[days or 5] # ④ 날짜는 보내지 않음
h = {"api-id": "ka10038", "authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json;charset=UTF-8"}
j = requests.post(BASE + "/api/dostk/rkinfo", headers=h, json=body, timeout=10).json()
if j.get("return_code") not in (0, None):
raise RuntimeError(f'ka10038 {j.get("return_code")} {j.get("return_msg")}')
summary = { # ① 순위가 아니라 수량
"buy": int(j["rank_1"]), "sell": int(j["rank_2"]),
"net": int(j["rank_3"]), "volume": int(j["prid_trde_qty"]),
}
assert summary["buy"] + summary["sell"] == summary["net"], summary # ② 더하기
df = pd.DataFrame(j.get("stk_sec_rank", []))
for c in ("rank", "buy_qty", "sell_qty", "acc_netprps_qty"):
df[c] = pd.to_numeric(df[c].str.replace("+", "", regex=False))
df["name_key"] = df["mmcm_nm"].str.replace(" ", "", regex=False) # ⑤ 공백 제거
return summary, df.sort_values("rank")
if __name__ == "__main__":
s, df = ka10038("005930", days=5)
print(s)
print(df.head())
assert 줄은 부호 규칙이 실제 응답에서도 성립하는지 첫 실행에서 확인하려는 장치입니다. 실계좌 데이터로 깨지면 그 응답을 저장해 두고 규칙을 다시 보십시오. 종목 여러 개를 짧은 간격으로 돌리면 키움 429 호출 제한에 걸리기 쉬우니 장 마감 뒤 하루 한 번 받는 구성을 권합니다.
창구 수량은 그 증권사 고객 주문을 합친 값이라 누가 샀는지 알려 주지 않으므로 이것만으로 매매 근거를 삼을 수는 없습니다. 이 글은 특정 종목이나 전략을 권하지 않습니다. 명세는 바뀔 수 있으니 키움 REST API 공식 문서로 현재 값을 확인하십시오.
5. 자주 묻는 질문
ka10038 응답의 rank_1, rank_2, rank_3는 무엇인가요?
순위가 아닙니다. 공식 명세상 rank_1은 기간별 누적 매수량, rank_2는 기간별 누적 매도량, rank_3는 기간별 누적 순매수이며 단위는 1주, 부호가 포함된 숫자입니다. 회원사별 순위는 stk_sec_rank 배열의 rank 필드에 있습니다.
ka10038에서 순매수는 매수 빼기 매도로 계산하나요?
아니요. 매도수량(sell_qty, rank_2)이 '-2813'처럼 음수로 오므로 순매수는 매수 + 매도입니다. 응답 예시에서 9800 + (-2813) = 6987이 acc_netprps_qty와 같고, rank_1 + rank_2 = rank_3도 성립합니다. 매수 - 매도로 계산하면 12613처럼 거래 총량에 가까운 값이 나옵니다.
ka10038의 기간 dt 값은 어떻게 넣나요?
1:전일, 4:5일, 9:10일, 19:20일, 39:40일, 59:60일, 119:120일처럼 일수에서 1을 뺀 값을 넣습니다. 시작일자·종료일자로 조회하려면 명세 주석대로 dt를 빈값('')으로 보내야 합니다. 당일(0) 선택지는 명세에 없습니다.
ka10038에는 회원사코드가 없는데 다른 TR과 어떻게 연결하나요?
ka10038 응답에는 회원사명(mmcm_nm)만 있습니다. ka10102 회원사 리스트의 name과 공백을 제거해 맞추거나, 회원사코드를 주는 ka10042 응답과 이름으로 병합한 뒤 코드를 zfill(3)으로 3자리로 맞춰 ka10039·ka10043에 넘깁니다.
창구 데이터를 묶어 쓰는 키움 봇, 만들어 드립니다
보고 싶은 종목과 기간만 알려 주시면 ka10038·ka10042·ka10043 연결, 부호·기간 변환, 이름 대응 검사까지 맞춰 드립니다. 24시간 빠른 답변 가능합니다.
무료 상담 시작하기