키움 ka10039 증권사별매매상위 — 종목코드 _AL 함정
ka10039(증권사별매매상위요청)는 "특정 증권사(회원사) 창구로 많이 순매수·순매도된 종목"을 주는 TR이고, 요청에 회원사코드 mmcm_cd가 필수라 ka10102(회원사 리스트)를 먼저 불러야 합니다. 명세 예시대로 stex_tp 3(통합)으로 부르면 응답 종목코드가 219550_AL처럼 접미사가 붙어 와서 다른 TR의 6자리 코드와 그대로는 안 맞습니다. 기간 dt에는 이웃 TR에 있는 20일이 없고(0·1·5·10·60), 금액 필드는 천원 단위이며, 명세의 응답 예시는 끝에 쉼표가 남아 json.loads로 읽히지 않습니다. (공식 명세 kiwoom_api_spec.json·예제, main 953e5db 2026-09-15 기준, 2026-10-19 확인)
목차
- ka10039는 무엇을 주나 — ka10037·ka10040과 비교
- 요청·응답 실물
- 함정 6가지
- 최소 호출 코드
- 자주 묻는 질문
1. ka10039는 무엇을 주나 — ka10037·ka10040과 비교
거래원(회원사) 데이터를 다루는 키움 REST TR은 방향이 둘입니다. ka10040 당일주요거래원은 종목 → 증권사("삼성전자를 어느 증권사 창구가 샀나")이고, ka10039는 반대로 증권사 → 종목("교보증권 창구로 무엇이 많이 순매수됐나")입니다. ka10037 외국계창구매매상위는 외국계 회원사 전체를 묶은 합계라 특정 회원사를 고를 수 없습니다.
| 항목 | ka10039 증권사별매매상위 | ka10037 외국계창구매매상위 |
|---|---|---|
| 집계 단위 | 회원사 1곳 (mmcm_cd) | 외국계 창구 합계 |
| 요청 body | mmcm_cd·trde_qty_tp·trde_tp·dt·stex_tp | mrkt_tp·dt·trde_tp·sort_tp·stex_tp |
| 시장 구분 | 없음 (코스피·코스닥 필터 불가) | mrkt_tp 000 전체 / 001 코스피 / 101 코스닥 |
| 거래량 하한 | trde_qty_tp 0:전체, 5:5000주 … 1000:100만주 | 없음 |
trde_tp | 1:순매수, 2:순매도 | 1:순매수, 2:순매도, 3:매수, 4:매도 |
dt | 0·1·5·10·60 (20 없음) | 0·1·5·10·20·60 |
| 금액 단위 | netprps_amt 등 천원 | netprps_prica 백만원 |
| 목록 키 | sec_trde_upper | frgn_wicket_trde_upper |
둘 다 경로는 POST /api/dostk/rkinfo이고 헤더 api-id만 다릅니다. 순위 TR 전체 구조는 rkinfo 순위조회 글에 정리했습니다.
2. 요청·응답 실물
명세의 request_example 원문입니다. 공식 예제 examples/국내주식/순위정보/get_domestic_broker_trade_top.py의 __main__도 같은 값(mmcm_cd='001', 전일, 통합)입니다.
POST https://api.kiwoom.com/api/dostk/rkinfo
api-id: ka10039
authorization: Bearer {접근토큰}
{
"mmcm_cd": "001",
"trde_qty_tp": "0",
"trde_tp": "1",
"dt": "1",
"stex_tp": "3"
}
응답 response_example 원문(2행 중 1행)입니다. 값은 전부 문자열이고 매수 쪽은 +, 매도 쪽은 -가 붙습니다.
{
"sec_trde_upper": [
{
"rank": "1",
"stk_cd": "219550_AL",
"stk_nm": "디와이디",
"prid_stkpc_flu": "+9",
"flu_rt": "+3.52",
"prid_trde_qty": "17608995",
"netprps": "+406895",
"buy_trde_qty": "+427268",
"sel_trde_qty": "-20373",
"netprps_amt": "+119706",
"buy_amt": "+125569",
"sell_amt": "-5863"
},
],
"return_code": 0,
"return_msg": "정상적으로 처리되었습니다"
}
| element | 한글명 | 명세 설명 |
|---|---|---|
prid_stkpc_flu | 기간중주가등락 | 단위: 원, 부호 포함 |
flu_rt | 등락율 | 단위: %, 부호 포함 소수점 둘째 자리 |
prid_trde_qty | 기간중거래량 | 단위: 1주 (시장 전체 거래량) |
netprps · buy_trde_qty · sel_trde_qty | 순매수 · 매수거래량 · 매도거래량 | 단위: 1주, 부호 포함 |
netprps_amt · buy_amt · sell_amt | 순매수금액 · 매수금액 · 매도금액 | 단위: 천원, 부호 포함 |
예시 1행으로 산수를 맞춰 보면 427268 + (−20373) = 406895로 netprps와 정확히 같고, 금액도 125569 + (−5863) = 119706입니다. 2행(317120_AL 라닉스)은 수량은 맞고 금액은 430339 − 20391 = 409948인데 응답은 409949 — 천원 단위 반올림 차이로 보이는 1이 어긋납니다.
3. 함정 6가지
① mmcm_cd는 ka10102에서 받아 온다
필드 설명이 "회원사 코드는 ka10102 조회" 한 줄입니다. ka10102(POST /api/dostk/stkinfo, body {})는 list[].code·name·gb를 주고, 명세 예시에는 001 교보 · 003 한국투자증권 · 004 대신 · 012 NH투자증권 등 10개가 있습니다. 주의할 점이 셋입니다.
name의 한글명이 명세와 공식 예제 모두 "업종명"으로 적혀 있지만 실제 값은 회원사 이름입니다.- 이름에 공백이 끼어 있습니다 —
"교 보","신 영","한 양". 이름으로 코드를 찾으려면 공백을 지운 뒤 비교하십시오. gb(구분)는 설명이 비어 있고 예시 10개가 모두"0"입니다. 외국계 여부를 이 값으로 판정하지 마십시오.
명세 전체에서 mmcm_cd를 요구하는 TR은 ka10039 · ka10043(거래원매물대분석) · ka10052(거래원순간거래량) · ka10078(증권사별종목매매동향) 네 개입니다. 한 번 받은 ka10102 결과를 하루 단위로 캐시해 네 TR이 같이 쓰면 됩니다.
② 통합 조회의 종목코드는 _AL이 붙는다
키움 REST의 거래소별 종목코드 규칙은 KRX: 039490, NXT: 039490_NX, SOR: 039490_AL입니다(ka10040 등의 stk_cd 설명 원문). ka10039 예시는 stex_tp "3"(통합) 요청에 219550_AL, 317120_AL을 돌려줍니다. 이 코드를 그대로 ka10001(주식기본정보)·KIS pdno·FinanceDataReader 종목코드와 조인하면 한 건도 매칭되지 않습니다. 명세 예시 중 응답 종목코드에 접미사가 보이는 TR은 ka10039와 ka10051(업종별투자자순매수) 둘뿐이라, 다른 순위 TR에서 쓰던 파서를 그대로 가져오면 여기서 처음 터집니다. NXT·SOR이 무엇인지는 NXT·KRX 주문 라우팅 글을 참고하십시오.
③ 기간에 20일이 없다
ka10039의 dt는 0:당일, 1:전일, 5:5일, 10:10일, 60:60일입니다. 같은 rkinfo 경로의 ka10034·ka10036·ka10037은 20:20일이 있고, ka10038은 아예 1·4·9·19·39·59·119처럼 "기준일 빼고 며칠" 체계를 씁니다. 순위 TR을 공통 함수로 감싸며 dt="20"을 넘기면 ka10039에서만 문제가 되니 TR별 허용값을 상수로 두고 미리 막으십시오. 명세에 없는 값을 넣었을 때 서버가 오류를 주는지 다른 기간으로 처리하는지는 명세에 없습니다.
④ 금액은 천원 — 다른 TR과 섞을 때 1,000배
netprps_amt·buy_amt·sell_amt는 천원, ka10037의 netprps_prica는 백만원입니다. 천원 단위는 예시로도 검산됩니다 — 라닉스 2행의 buy_amt 430339 × 1,000 ÷ buy_trde_qty 212678 = 평균 매수가 약 2,023원이고, prid_stkpc_flu +467·flu_rt +29.78로 역산한 종가는 약 2,035원이라 맞아떨어집니다. 단 디와이디 1행은 같은 계산이 294원 대 265원으로 어긋나므로 예시 값은 형식 확인용으로만 보십시오. 저장할 때 원 단위로 통일하면 이 문제가 사라집니다.
⑤ 명세 응답 예시는 JSON으로 읽히지 않는다
예시의 마지막 행 뒤에 쉼표가 남아 있습니다(}, 다음 바로 ]). 단위 테스트용 목(mock) 응답을 만들려고 예시를 그대로 붙여 넣으면 이렇게 실패합니다.
json.decoder.JSONDecodeError: Expecting value: line 31 column 5 (char 900)
쉼표를 지우면 pandas 2.2.3에서 12개 열의 DataFrame이 되는데 모든 열이 object(문자열)입니다. 공식 예제의 NUMERIC_COLUMNS는 화면 표시용 쉼표 처리에만 쓰이고, 그 목록에도 순매수(netprps)는 빠져 있습니다. 숫자 변환은 직접 해야 합니다 — pd.to_numeric은 "+406895"·"-20373"을 부호대로 정상 변환합니다(실측).
⑥ 순위는 수량 순으로 보인다 — 정렬 옵션이 없다
예시에서 1위 디와이디는 순매수 406,895주·119,706천원, 2위 라닉스는 202,658주·409,949천원입니다. 금액은 2위가 세 배 넘게 큰데 순위는 수량이 많은 쪽이 앞입니다. ka10037처럼 sort_tp(금액/수량)를 고르는 필드가 ka10039에는 없으니, 금액 기준 목록이 필요하면 받아서 netprps_amt로 다시 정렬해야 합니다. 예시 2행에서 읽은 것이라 운영 응답으로 한 번 확인하십시오.
명세가 비워 둔 것 — 한 페이지에 몇 종목이 오는지, dt 0(당일)이 장중 몇 시 기준인지, stex_tp 1(KRX)·2(NXT) 조회에서도 접미사가 붙는지(규칙상 KRX는 6자리, NXT는 _NX로 예상되지만 예시는 통합뿐), 시장 구분이 없는데 코넥스·ETF가 섞이는지는 명세에 없습니다. 첫 운영 호출에서 확인이 필요합니다.
4. 최소 호출 코드
토큰은 환경변수에서 읽고, 연속조회는 공식 예제와 같은 MAX_PAGES = 10·REQUEST_DELAY_SECONDS = 0.2입니다. ①~⑥을 코드에서 막습니다.
import os, re, 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", "60"} # ③ 20 없음
NUM = ["prid_stkpc_flu", "flu_rt", "prid_trde_qty", "netprps", "buy_trde_qty",
"sel_trde_qty", "netprps_amt", "buy_amt", "sell_amt"]
def post(api_id, path, body, cont_yn=None, next_key=None):
h = {"Content-Type": "application/json;charset=UTF-8",
"authorization": f"Bearer {TOKEN}", "api-id": api_id}
if cont_yn == "Y":
h.update({"cont-yn": "Y", "next-key": next_key})
r = requests.post(BASE + path, headers=h, json=body, timeout=10)
j = r.json()
if j.get("return_code") not in (None, 0):
raise RuntimeError(f"{api_id} {j.get('return_code')}: {j.get('return_msg')}")
return j, r.headers.get("cont-yn"), r.headers.get("next-key")
def broker_codes(): # ① ka10102 — 하루 1번 캐시
j, _, _ = post("ka10102", "/api/dostk/stkinfo", {})
return {re.sub(r"\s+", "", x["name"]): x["code"] for x in j["list"]}
def ka10039(mmcm_cd, side="1", dt="1", stex_tp="3", qty_tp="0", max_pages=10):
if str(dt) not in DT_OK:
raise ValueError(f"ka10039 dt={dt} 불가")
body = {"mmcm_cd": mmcm_cd, "trde_qty_tp": qty_tp, "trde_tp": side,
"dt": str(dt), "stex_tp": stex_tp}
rows, cy, nk = [], None, None
for _ in range(max_pages):
j, cy, nk = post("ka10039", "/api/dostk/rkinfo", body, cy, nk)
rows += j.get("sec_trde_upper", [])
if cy != "Y":
break
time.sleep(0.2)
df = pd.DataFrame(rows)
df[NUM] = df[NUM].apply(pd.to_numeric, errors="coerce") # ⑤
df["code6"] = df["stk_cd"].str.replace(r"_(AL|NX)$", "", regex=True) # ②
df["netprps_won"] = df["netprps_amt"] * 1_000 # ④ 원 단위
df["mmcm_cd"], df["dt"] = mmcm_cd, dt
return df.sort_values("netprps_won", ascending=False) # ⑥ 금액순 재정렬
codes = broker_codes()
df = ka10039(codes["한국투자증권"], side="1", dt="5")
return_code가 0이 아닐 때의 의미는 키움 return_code 글, cont-yn·next-key 처리는 연속조회 글에 있습니다. 회원사 여러 곳을 돌며 부르면 호출 수가 회원사 수만큼 늘어나니 키움 429 호출 제한을 먼저 계산하십시오.
봇에서 쓴다면 장 마감 뒤 관심 회원사 몇 곳만 dt="1"로 받아 날짜와 함께 저장하고, 같은 종목의 창구별 흐름은 ka10040(당일 상위 5개 거래원)이나 ka10078(증권사별종목매매동향, 기간 지정)로 확인하는 구성이 무난합니다. 외국인 투자자 전체 흐름은 ka10034가 맞습니다. 창구 데이터는 누가 실제로 주문했는지를 알려 주지 않고 해석이 갈리는 정보라, 이 목록만으로 매매 근거를 삼을 수는 없으며 이 글은 특정 종목이나 전략을 권하지 않습니다. 명세는 바뀔 수 있으니 키움 REST API 공식 문서로 현재 값을 확인하십시오.
5. 자주 묻는 질문
ka10039의 mmcm_cd에는 무엇을 넣나요?
회원사(증권사) 코드 3자리입니다. 명세 설명이 '회원사 코드는 ka10102 조회'라서 ka10102(회원사 리스트)를 먼저 호출해 code 값을 받아 넣습니다. 명세 예시 기준 001 교보, 003 한국투자증권, 012 NH투자증권이고, ka10040 예시에는 036 모건스탠리, 050 키움증권이 나옵니다. 코드는 하드코딩하지 말고 ka10102 결과를 캐시해 쓰십시오.
ka10039 응답 종목코드 뒤에 _AL이 붙는 이유는 무엇인가요?
stex_tp 3(통합)으로 조회했기 때문입니다. 키움 REST의 거래소별 종목코드 규칙은 KRX 039490, NXT 039490_NX, SOR 039490_AL이고, 명세 응답 예시도 stex_tp 3 요청에 219550_AL·317120_AL을 돌려줍니다. 다른 TR의 6자리 코드와 조인하려면 접미사를 떼어 내십시오.
ka10039로 20일 기간 순매수 상위를 볼 수 있나요?
없습니다. ka10039의 dt는 0:당일, 1:전일, 5:5일, 10:10일, 60:60일 다섯 개뿐입니다. 같은 rkinfo 경로의 ka10034·ka10036·ka10037에는 20:20일이 있어서 공통 함수로 dt=20을 넘기면 ka10039에서만 문제가 됩니다.
ka10039의 netprps_amt 단위는 무엇인가요?
천원입니다. 명세상 netprps_amt·buy_amt·sell_amt 모두 '단위: 천원'이고 수량 필드는 1주입니다. 외국계창구매매상위 ka10037의 netprps_prica는 백만원 단위라서 두 TR 금액을 섞으면 1,000배가 어긋납니다.
창구 수급을 기록하는 키움 봇, 만들어 드립니다
보고 싶은 회원사와 조건만 알려 주시면 TR 선택·코드 정규화·이력 저장까지 맞춰 드립니다. 24시간 빠른 답변 가능합니다.
무료 상담 시작하기