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

키움 ka10065 장중투자자별매매상위 — 1이 순매수다

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

ka10065(장중투자자별매매상위요청)는 장중에 외국인·기관 등 투자자 유형 하나를 골라 순매수(또는 순매도) 상위 종목을 주는 TR이고, trde_tp는 "1"이 순매수, "2"가 순매도입니다. 같은 장중 투자자 데이터인 짝 TR ka10064는 "0"이 순매수이고 "1"은 매수입니다. 경로는 /api/dostk/rkinfo, body는 trde_tp·mrkt_tp·orgn_tp 필수에 amt_qty_tp 선택입니다. orgn_tp 11개에 개인 코드는 없고, 응답은 종목코드·종목명·매도·매수·순매수 5필드뿐이며, 순매수는 매수+매도와 ±1 어긋날 수 있습니다. 거래소 구분 stex_tp도 받지 않습니다. (공식 명세 kiwoom_api_spec.json·예제, main 953e5db 2026-09-15 기준, 2026-10-26 확인)

목차

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

1. ka10065는 무엇을 주나

"지금 장중에 외국인이 가장 많이 사고 있는 종목"을 한 번에 뽑는 순위 TR입니다. 응답 배열 이름은 opmr_invsr_trde_upper(장중 투자자 매매 상위)입니다. 키움 영웅문 도움말의 같은 화면(장중투자자별매매상위, 잠정)은 이 데이터를 거래소 내 상주회원사 기준으로 집계한 추정치이고 장중 정해진 시간에 주기적으로 갱신된다고 설명합니다. 즉 확정치가 아니라 잠정치이고, 실시간 체결처럼 매 순간 바뀌지도 않습니다. 비슷한 수급 TR과 나란히 놓으면 쓰임새가 갈립니다.

TR경로무엇시점trde_tp "1"
ka10065rkinfo투자자 1유형의 종목 순위장중 잠정순매수
ka10064chart종목 1개의 투자자별 장중 시계열장중 잠정매수(0이 순매수)
ka10062rkinfo기관+외국인 동시 순매매 순위기간순매수
ka10058stkinfo투자자별 일별 매매 종목일별순매도

ka10065는 순위정보 TR 묶음 /api/dostk/rkinfo에 속합니다. 같은 경로의 TR 23개 전체 구성은 키움 rkinfo 순위정보 TR 모음에 정리했습니다.

2. 요청·응답 실물

명세의 request_example 원문입니다.

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

{
    "trde_tp": "1",
    "mrkt_tp": "000",
    "orgn_tp": "9000",
    "amt_qty_tp": "1"
}
element필수명세 설명(원문)
trde_tpY1:순매수, 2:순매도
mrkt_tpY000:전체, 001:코스피, 101:코스닥
orgn_tpY9000:외국인, 9100:외국계, 1000:금융투자, 3000:투신, 5000:기타금융, 4000:은행, 2000:보험, 6000:연기금, 7000:국가, 7100:기타법인, 9999:기관계
amt_qty_tpN1:금액, 2:수량

response_example 원문입니다(7행 중 3행).

{
    "opmr_invsr_trde_upper": [
        {
            "stk_cd": "005930",
            "stk_nm": "삼성전자",
            "sel_qty": "-39420",
            "buy_qty": "+73452",
            "netslmt": "+34033"
        },
        {
            "stk_cd": "005930",
            "stk_nm": "삼성전자",
            "sel_qty": "-13970",
            "buy_qty": "+25646",
            "netslmt": "+11676"
        },
        ...
        {
            "stk_cd": "005930",
            "stk_nm": "삼성전자",
            "sel_qty": "-27306",
            "buy_qty": "+34853",
            "netslmt": "+7547"
        }
    ],
    "return_code": 0,
    "return_msg": "정상적으로 처리되었습니다"
}

응답 필드는 stk_cd·stk_nm·sel_qty·buy_qty·netslmt 다섯 개가 전부입니다. 순위 번호, 현재가, 등락률이 없으므로 "외국인 순매수 상위인데 오늘 내린 종목" 같은 조건은 ka10001 주식기본정보 등으로 종목마다 한 번 더 조회해야 합니다.

3. 함정 6가지

① trde_tp "1"의 뜻이 TR마다 셋으로 갈린다

명세 337개 TR에서 투자자·거래원 계열의 trde_tp 설명을 전수로 모았습니다.

"1"의 의미TR명세 설명(원문)
순매수ka10065 · ka10062 · ka10039 · ka300021:순매수, 2:순매도
순매수ka100371:순매수, 2:순매도, 3:매수, 4:매도
순매도ka100441:순매도, 2:순매수
순매도ka10058순매도:1, 순매수:2
순매도ka100341:순매도, 2:순매수, 3:순매매
매수(0이 순매수)ka10064 · ka10059 · ka10060 · ka100660:순매수, 1:매수, 2:매도

가장 헷갈리는 쌍이 ka10065와 ka10064입니다. 둘 다 "장중 투자자별 매매"이고 메뉴 이름도 상위·차트만 다른데, ka10064에 ka10065용 상수 "1"을 넣으면 순매수가 아니라 총매수 시계열이 옵니다. 반대로 ka10058에 같은 "1"을 넣으면 순매도 종목 목록이 옵니다. 세 경우 모두 에러 없이 정상 응답(return_code 0)이라 값만 보고는 알아채기 어렵습니다. 수급 TR 여러 개를 함수 하나로 감쌀 때는 "순매수"를 TR별 코드로 바꿔 주는 표를 따로 두십시오.

② 개인 코드가 없다 — 외국계 9100은 여기만 있다

orgn_tp 11개를 다른 투자자 코드표와 비교하면 차이가 보입니다.

코드ka10065 orgn_tpka10058 invsr_tp
8000 개인없음있음
3100 사모펀드없음있음
9100 외국계있음없음
9000 외국인 · 9999 기관계 등 나머지 10개있음있음

명세 전체에서 9100을 받는 곳은 ka10065 하나, 8000(개인)을 받는 곳은 ka10058 하나입니다. "장중 개인 순매수 상위"는 ka10065로 받을 수 없고, 명세에 없는 8000을 넣었을 때 서버가 어떻게 답하는지도 문서에 없습니다. 또 9100 외국계는 외국인 투자자(9000)가 아니라 외국계 증권사 창구 쪽 개념으로 읽히는데, 명세에 정의가 따로 없으니 둘을 합치거나 같은 뜻으로 쓰지 마십시오. 외국계 창구 데이터의 성격은 ka10040 거래원 글을 참고하십시오.

③ amt_qty_tp는 선택이지만 항상 넣는다

금액·수량 구분을 받는 TR 16개 중 amt_qty_tp가 선택(required N)인 곳은 ka10065와 ka90013 둘뿐이고 나머지 14개는 필수입니다. 빼고 보내면 금액으로 오는지 수량으로 오는지 명세에 없습니다. 공식 예제 함수도 기본값을 amt_qty_tp: str | None = '1'(금액)로 두고 있으니 직접 호출할 때도 늘 명시하는 편이 안전합니다. 필드 이름이 sel_qty·buy_qty라 "수량"처럼 보이지만, "1"을 넣으면 이 칸에 금액(백만원)이 들어옵니다. 명세 한글명도 '매도금액/매도량'입니다.

④ 순매수 = 매수 + 매도, 그런데 ±1 어긋난다

sel_qty는 음수 부호로 옵니다. 그래서 순매수는 buy_qty − sel_qty가 아니라 buy_qty + sel_qty입니다. 공식 응답 예시 7행을 그대로 검산했습니다(Python 3.10.4, 2026-10-26).

buy  73452 + sel  -39420 =  34032  netslmt  34033  diff +1
buy  25646 + sel  -13970 =  11676  netslmt  11676  diff +0
buy  21167 + sel  -10063 =  11104  netslmt  11104  diff +0
buy  47604 + sel  -37542 =  10062  netslmt  10061  diff -1
buy  10874 + sel   -2310 =   8564  netslmt   8564  diff +0
buy  33114 + sel  -24912 =   8202  netslmt   8203  diff +1
buy  34853 + sel  -27306 =   7547  netslmt   7547  diff +0

7행 중 3행이 1씩 어긋납니다. 원 단위 매수·매도·순매수를 각각 백만원으로 반올림해 보내면 이런 차이가 생깁니다(예를 들어 원 단위 값이 매수 73,452.4백만, 매도 −39,419.6백만이었다면 각각 73,452와 −39,420으로 반올림되지만 순매수 34,032.8은 34,033이 됩니다). 합계 검사를 ==로 걸면 정상 데이터를 오류로 버리게 되니 abs(차이) <= 1로 두십시오. 같은 백만원 반올림 문제는 짝 순위 TR인 ka10062의 금액 0 문제에서도 나옵니다. 참고로 netslmt 설명은 단위를 "trde_tp파라미터로 설정"이라고 적고 있는데, 다른 두 필드처럼 amt_qty_tp의 오기로 보입니다.

⑤ 문자열 그대로 정렬하면 순위가 뒤집힌다

string sort desc:  ['+8564', '+8203', '+7547', '+34033', '+11676', '+11104', '+10061']
numeric sort desc: [34033, 11676, 11104, 10061, 8564, 8203, 7547]

값이 "+34033" 같은 부호 붙은 문자열이라 pandas에서 그대로 sort_values를 하면 사전순이 됩니다. 1위 34,033이 4위로 밀립니다. 공식 예제의 NUMERIC_COLUMNS에도 '매도금액/매도량'·'매수금액/매수량' 두 개만 있고 '순매수/순매도'는 빠져 있습니다. 응답 자체는 순매수 내림차순으로 오지만, 여러 시장(001·101)을 합쳐 다시 정렬하는 순간 이 문제가 드러납니다. 숫자로 바꾼 뒤 정렬하십시오.

⑥ stex_tp가 없고, 예시 종목은 전부 005930이다

같은 "trde_tp": "1" 을 보냈을 때 ka10065 · ka10062 ka10039 · ka10037 순매수 ka10058 · ka10044 ka10034 순매도 ka10064 · ka10059 ka10060 · ka10066 매수 (0이 순매수) 세 경우 모두 return_code 0 — 에러 없이 다른 데이터가 온다
키움 명세 trde_tp의 번호 체계는 셋이다. TR마다 상수를 따로 둘 것.

4. 안전하게 받는 코드

토큰은 환경변수에서 읽고 ①~⑤를 코드에서 막습니다. 응답 해석부(parse)는 명세의 response_example로 실행해 확인했습니다.

import os, time, requests
import pandas as pd

BASE  = "https://api.kiwoom.com"                     # 모의: https://mockapi.kiwoom.com
NET   = {"buy": "1", "sell": "2"}                    # ① ka10065 전용 (ka10064는 0이 순매수)
ORGN  = {"외국인": "9000", "외국계": "9100", "금융투자": "1000", "투신": "3000",
         "기타금융": "5000", "은행": "4000", "보험": "2000", "연기금": "6000",
         "국가": "7000", "기타법인": "7100", "기관계": "9999"}   # ② 개인 없음
NUM   = ["sel_qty", "buy_qty", "netslmt"]

def parse(rows):
    df = pd.DataFrame(rows)
    for c in NUM:                                      # ⑤ 문자열 → 숫자
        df[c] = pd.to_numeric(df[c].str.replace("+", "", regex=False), errors="coerce")
    gap = df["netslmt"] - (df["buy_qty"] + df["sel_qty"])   # ④ 매도는 음수
    bad = df[gap.abs() > 1]
    if len(bad):
        print("순매수 검산 불일치(±1 초과):", bad["stk_cd"].tolist())
    return df.sort_values("netslmt", ascending=False).reset_index(drop=True)

def ka10065(investor="외국인", side="buy", market="000", unit="1", max_pages=3):
    token = os.environ["KIWOOM_TOKEN"]
    body = {"trde_tp": NET[side], "mrkt_tp": market,
            "orgn_tp": ORGN[investor], "amt_qty_tp": unit}   # ③ 항상 명시
    rows, cont, key = [], None, None
    for _ in range(max_pages):
        h = {"api-id": "ka10065", "authorization": f"Bearer {token}",
             "Content-Type": "application/json;charset=UTF-8"}
        if cont == "Y":
            h.update({"cont-yn": "Y", "next-key": key})
        r = requests.post(BASE + "/api/dostk/rkinfo", headers=h, json=body, timeout=(3.05, 10))
        j = r.json()
        if j.get("return_code") not in (0, None):
            raise RuntimeError(f'ka10065 {j.get("return_code")} {j.get("return_msg")}')
        rows += j.get("opmr_invsr_trde_upper", [])
        cont, key = r.headers.get("cont-yn"), r.headers.get("next-key")
        if cont != "Y":
            break
        time.sleep(0.2)
    return parse(rows)

명세 예시 7행을 parse에 넣은 결과입니다. ±1 차이 3행은 경고 없이 통과하고 숫자 순으로 정렬됩니다.

   stk_cd stk_nm  sel_qty  buy_qty  netslmt
0  005930   삼성전자   -39420    73452    34033
1  005930   삼성전자   -13970    25646    11676
2  005930   삼성전자   -10063    21167    11104
3  005930   삼성전자   -37542    47604    10061
4  005930   삼성전자    -2310    10874     8564
5  005930   삼성전자   -24912    33114     8203
6  005930   삼성전자   -27306    34853     7547

장중 잠정치는 정해진 시각에 갱신되므로 몇 초 간격으로 계속 부를 이유가 없습니다. 투자자 유형 11개 × 시장 3개를 루프로 돌리면 한 번에 33회 호출이 되어 키움 429 호출 제한에 걸리기 쉬우니, 필요한 조합만 몇 분 간격으로 받는 구성을 권합니다. 장 마감 뒤 확정치는 ka10066(장마감후투자자별매매)이나 ka10058(일별)로 따로 받으십시오.

외국인이 장중에 순매수했다는 사실은 잠정 집계일 뿐 이후 가격을 알려 주지 않습니다. 이 글은 특정 종목이나 전략을 권하지 않으며, 수급 필터의 효과는 본인 데이터로 백테스트해 확인하십시오. 명세는 바뀔 수 있으니 키움 REST API 공식 문서로 현재 값을 확인하십시오.

5. 자주 묻는 질문

ka10065의 trde_tp 1은 순매수인가요 순매도인가요?

ka10065에서는 1이 순매수, 2가 순매도입니다. 그런데 같은 장중 투자자 데이터인 ka10064(장중투자자별매매차트)는 0이 순매수이고 1은 매수, 2는 매도입니다. ka10058·ka10044는 1이 순매도입니다. 키움 명세의 trde_tp에는 이렇게 세 가지 번호 체계가 섞여 있어 TR마다 따로 상수를 두어야 합니다.

ka10065로 개인 순매수 상위를 볼 수 있나요?

명세상 볼 수 없습니다. orgn_tp는 9000 외국인, 9100 외국계, 1000 금융투자, 3000 투신, 5000 기타금융, 4000 은행, 2000 보험, 6000 연기금, 7000 국가, 7100 기타법인, 9999 기관계 11개뿐이고 개인(8000)이 없습니다. 개인 코드 8000은 ka10058(투자자별일별매매종목)에만 있으며 이쪽은 장중 잠정치가 아니라 일별 데이터입니다.

ka10065의 netslmt가 buy_qty와 sel_qty를 더한 값과 다른 이유는?

sel_qty는 음수 부호로 오므로 순매수는 buy_qty + sel_qty입니다. 공식 응답 예시 7행 중 4행은 정확히 맞고 3행은 1씩 어긋납니다(34032 대 34033 등). 백만원 단위로 각 필드를 따로 반올림하기 때문으로 보입니다. 합계 검사를 넣는다면 ±1 허용 오차를 두십시오.

ka10065 응답을 netslmt로 정렬했는데 순서가 이상합니다.

값이 '+34033' 같은 부호 붙은 문자열이라 그대로 정렬하면 사전순이 됩니다. 공식 예시를 문자열로 내림차순 정렬하면 +8564가 +34033보다 앞에 옵니다. pd.to_numeric이나 int()로 숫자로 바꾼 뒤 정렬하십시오. 공식 예제의 NUMERIC_COLUMNS에도 순매수/순매도 열은 빠져 있습니다.

장중 수급을 보는 키움 봇, 만들어 드립니다

보고 싶은 투자자 유형과 시장만 알려 주시면 ka10065·ka10062·ka10059 연결, 코드 번호 변환, 검산까지 맞춰 드립니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기