AlgoLab Blog · 키움증권 REST · 거래원 데이터 · 2026

키움 ka10038 종목별증권사순위 — rank_1은 수량이다

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

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 확인)

목차

  1. ka10038은 무엇을 주나
  2. 요청·응답 실물
  3. 함정 5가지
  4. 안전하게 받는 코드
  5. 자주 묻는 질문

1. ka10038은 무엇을 주나

종목 하나를 넣으면 "이 기간에 어느 증권사 창구가 얼마나 사고팔았나"를 회원사별 수량으로 돌려줍니다. 같은 rkinfo 경로의 ka10042(순매수거래원순위)와 짝을 이루는데, 두 TR은 서로 빠진 것을 채웁니다.

TR필수 body회원사코드수량방향 선택기간 dt
ka10038stk_cd·qry_tp없음(mmcm_nm만)있음qry_tp 1·2일수−1, 날짜 조회 시 빈값
ka10042stk_cd·qry_dt_tp·pot_tp·sort_base있음없음순매수만일수 그대로, qry_dt_tp로 전환
ka10040stk_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_cdY거래소별 종목코드 (KRX:039490, NXT:039490_NX, SOR:039490_AL) · 길이 6
strt_dt / end_dtNYYYYMMDD
qry_tpY1:순매도순위정렬, 2:순매수순위정렬
dtN1:전일, 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만 번호가 하나씩 작습니다.

TRdt 명세 원문날짜 조회 전환
ka100381:전일, 4:5일, 9:10일, 19:20일, 39:40일, 59:60일, 119:120일dt 빈값
ka10042 · ka100435:5일, 10:10일, 20:20일, 40:40일, 60:60일, 120:120일qry_dt_tp "1"
ka100390:당일, 1:전일, 5:5일, 10:10일, 60:60일—
ka101311:최근일, 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이 같은 회원사를 같은 문자열로 주는지는 명세에 없습니다.

요약 (순위 아님) rank_1 누적 매수 +34881 + rank_2 누적 매도 -13253 = rank_3 순매수 +21628 stk_sec_rank[] (진짜 순위) rank · mmcm_nm · buy_qty · sell_qty(음수) · acc_netprps_qty 회원사코드 없음 → ka10102 이름(공백 제거)으로 대응
위 세 칸은 수량 요약, 아래 배열의 rank가 회원사 순위다. 순매수는 더하기로 계산한다.

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시간 빠른 답변 가능합니다.

무료 상담 시작하기