AlgoLab Blog · 키움 REST API 실무 · 2026

키움 REST 투자자별 매매 ka10059 — 코드 함정 6가지

키움 REST · 수급 데이터 2026-10-11 · 약 9분 읽기 · 알고랩 AlgoLab
한 줄 요약

키움 REST API에서 한 종목의 개인·외국인·기관 매매를 일자별로 받는 TR은 ka10059(종목별투자자기관별요청)이고, POST /api/dostk/stkinfo에 body 5개(dt·stk_cd·amt_qty_tp·trde_tp·unit_tp)를 보냅니다. 기간 합계는 같은 URL의 ka10061입니다. 문제는 투자자 관련 TR 10여 개가 코드를 제각각 쓴다는 점입니다 — ka10059에서 amt_qty_tp=1은 '금액'인데 ka10131·ka10051에서는 '수량'이고, trde_tp의 순매수 코드는 TR에 따라 0·2·1로 갈립니다. 여러 TR을 섞는 봇은 TR별 코드표를 따로 두는 정규화 층이 있어야 숫자가 조용히 틀리지 않습니다.

목차

  1. 스펙 한 장 — ka10059와 형제 TR
  2. 요청·응답 실물
  3. 함정 6가지
  4. 정규화 층 — TR별 코드표 한 곳에
  5. 봇에서 쓰는 법 — 수급 필터
  6. 자주 묻는 질문

"외국인과 기관이 사흘 연속 순매수한 종목만 매수 후보로" 같은 조건은 자동매매 의뢰에서 자주 나오는 수급 필터입니다. 키움증권 REST API로 이걸 만들 때 처음 부딪히는 문제는 어느 TR을 쓰느냐보다 TR마다 같은 뜻의 코드가 다르다는 점입니다. 이 글의 출처는 키움증권 공식 GitHub 저장소 Kiwoom-Securities/Kiwoom-REST-API의 단일 명세 kiwoom/_data/kiwoom_api_spec.json(API 337개), Postman 컬렉션 kiwoom-openapi.postman_collection.json, examples/국내주식/종목정보/get_domestic_stock_investor_by_institution.py입니다(2026-10-11 main 기준). REST 전반의 구조는 허브 글 키움 REST API 자동매매 가이드에 있습니다.

1. 스펙 한 장 — ka10059와 형제 TR

항목값
API명국내주식 > 종목정보 > 종목별투자자기관별요청 (ka10059)
메서드 · URLPOST https://api.kiwoom.com/api/dostk/stkinfo (모의 mockapi.kiwoom.com)
헤더api-id: ka10059 · authorization: Bearer {토큰} · cont-yn · next-key
bodydt(YYYYMMDD) · stk_cd · amt_qty_tp(1:금액, 2:수량) · trde_tp(0:순매수, 1:매수, 2:매도) · unit_tp(1000:천주, 1:단주) — 전부 필수
응답stk_invsr_orgn LIST — 행당 일자·현재가·등락 + 투자자 14구분
연속조회응답 헤더 cont-yn=Y면 next-key를 실어 다시 호출

투자자 관련 TR은 이것 하나가 아닙니다. 명세에서 '투자자·기관·외국인'이 이름에 들어간 TR을 모으면 용도가 이렇게 나뉩니다.

용도TRURL
종목 1개 · 일자별 투자자 매매ka10059/api/dostk/stkinfo
종목 1개 · 기간 합계ka10061/api/dostk/stkinfo
종목 1개 · 차트형ka10060/api/dostk/chart
투자자 1개 기준 · 일별 매매 종목ka10058/api/dostk/stkinfo
시장 · 기관/외국인 연속 순매수ka10131/api/dostk/frgnistt
업종별 투자자 순매수ka10051/api/dostk/sect
장중 / 장마감후 투자자별 매매ka10063 / ka10066/api/dostk/mrkcond
외국인 보유·한도소진률ka10008/api/dostk/frgnistt
외국인·기관 매매 상위ka90009 · ka10065/api/dostk/rkinfo

키움 REST는 URL이 같아도 api-id 헤더가 TR을 정합니다.

2. 요청·응답 실물

Postman 컬렉션 /PRD/국내주식/종목정보/종목별투자자기관별요청(ka10059)의 요청 body 원문입니다. 공식 예제 파일도 같은 값(005930, 20241107)을 씁니다.

{
  "dt": "20241107",
  "stk_cd": "005930",
  "amt_qty_tp": "1",
  "trde_tp": "0",
  "unit_tp": "1000"
}

응답 필드는 명세의 response.body가 전부입니다. 아래는 구조를 보여 주기 위한 형태 예시로, 숫자는 형식 설명용 가상 값입니다(명세에 응답 예시값이 없습니다).

{
  "stk_invsr_orgn": [
    {
      "dt": "20241107", "cur_prc": "+57000", "pre_sig": "2", "pred_pre": "+500", "flu_rt": "+88",
      "acc_trde_qty": "1000000", "acc_trde_prica": "57000",
      "ind_invsr": "-1200", "frgnr_invsr": "+900", "orgn": "+300",
      "fnnc_invt": "+100", "insrnc": "+20", "invtrt": "+80", "etc_fnnc": "0",
      "bank": "0", "penfnd_etc": "+90", "samo_fund": "+10", "natn": "0",
      "etc_corp": "0", "natfor": "0"
    }
  ],
  "return_code": 0,
  "return_msg": "..."
}

실무에 걸리는 필드 설명만 추리면 이렇습니다.

필드한글명명세 설명
flu_rt등락율"우측 2자리 소수점자리수" — 다른 TR은 "소수점 둘째 자리까지 포맷된 백분율"
acc_trde_prica누적거래대금단위: 백만원
ind_invsr · frgnr_invsr · orgn개인 · 외국인 · 기관계금액 단위 백만원, 수량 단위 1000주 혹은 1주(unit_tp)
fnnc_invt … samo_fund금융투자·보험·투신·기타금융·은행·연기금등·사모펀드위와 같음
natn국가설명 공란
etc_corp · natfor기타법인 · 내외국인위와 같음

앱키·시크릿은 코드에 쓰지 않고 환경변수에서 읽습니다(API 키 보안). 연속조회까지 넣은 최소 호출 코드입니다.

import os, time, requests

BASE = "https://api.kiwoom.com"            # 모의: https://mockapi.kiwoom.com
TOKEN = os.environ["KIWOOM_TOKEN"]

def kiwoom_post(api_id, path, body, max_pages=10, delay=0.2):
    rows, cont_yn, next_key = [], None, None
    for _ in range(max_pages):              # 공식 예제도 MAX_PAGES=10, 0.2초 간격
        headers = {
            "Content-Type": "application/json;charset=UTF-8",
            "authorization": f"Bearer {TOKEN}",
            "api-id": api_id,
        }
        if cont_yn == "Y":
            headers.update({"cont-yn": "Y", "next-key": next_key})
        r = requests.post(BASE + path, headers=headers, json=body, timeout=5)
        data = r.json()
        if data.get("return_code") not in (None, 0):
            raise RuntimeError(f"{api_id} {data.get('return_code')} {data.get('return_msg')}")
        rows.append(data)
        cont_yn, next_key = r.headers.get("cont-yn"), r.headers.get("next-key")
        if cont_yn != "Y":
            break
        time.sleep(delay)
    return rows

pages = kiwoom_post("ka10059", "/api/dostk/stkinfo",
                    {"dt": "20241107", "stk_cd": "005930",
                     "amt_qty_tp": "1", "trde_tp": "0", "unit_tp": "1000"})

return_code가 0이 아닐 때 메시지 해석은 키움 REST return_code 오류, cont-yn·next-key 처리의 세부는 키움 REST 연속조회에 정리했습니다.

3. 함정 6가지

① 같은 "금액"이 TR마다 1이기도 하고 0이기도 하다

amt_qty_tp(금액수량구분)는 이름이 같은데 값 정의가 TR마다 다릅니다. 명세 원문 그대로 옮기면 이렇습니다.

TRamt_qty_tp 명세 원문
ka10059 · ka10060 · ka10061 · ka10064 · ka10066 · ka100651:금액, 2:수량
ka10051 (업종별투자자순매수)금액:0, 수량:1
ka10131 (기관외국인연속매매현황)0:금액, 1:수량
ka10063 (장중투자자별매매)1: 금액&수량 (값 하나뿐)
ka90009 (외국인기관매매상위)1:금액(천만), 2:수량(천)

ka10059에서 쓰던 "1"을 그대로 ka10131에 넣으면 오류 없이 수량이 옵니다. 필드명이 orgn_nettrde_amt(금액)·orgn_nettrde_qty(수량)로 나뉜 ka10131은 그나마 응답에서 티가 나지만, ka10059처럼 같은 필드에 금액이나 수량이 들어오는 TR은 요청 코드가 틀리면 알아챌 방법이 없습니다.

② 순매수 코드 방향이 TR마다 반대다

trde_tp(매매구분)도 같은 문제가 있고, 이쪽은 방향이 뒤집힌다는 점에서 더 위험합니다.

TRtrde_tp 명세 원문순매수 코드
ka10059 · ka10060 · ka10064 · ka100660:순매수, 1:매수, 2:매도0
ka100610:순매수 (값 하나)0
ka10058 · ka100441:순매도, 2:순매수2
ka10065 (장중투자자별매매상위)1:순매수, 2:순매도1

ka10065에서 "순매수 상위"를 뽑으려고 "1"을 쓰던 코드를 ka10058로 옮기면 순매도 종목 목록을 받습니다. ka10131은 아예 netslmt_tp라는 다른 이름에 2:순매수(고정값)입니다. 순위형 TR 전반의 파라미터 차이는 키움 REST 순위 조회에서 더 다뤘습니다.

③ 투자자 코드 체계가 세 가지다

"외국인만" 같은 투자자 지정 파라미터는 TR마다 이름도, 값 체계도 다릅니다.

ka10059는 이런 파라미터 없이 14구분을 한 행에 다 주므로, 종목 단위 필터라면 ka10059 하나로 받고 필요한 열만 고르는 쪽이 코드 체계를 덜 섞습니다.

④ 단위가 백만원·억원·천만원으로 갈린다

ka10059의 금액은 백만원, 수량은 unit_tp에 따라 1000주 또는 1주입니다. 그런데 업종 TR ka10051은 "단위: 억원 혹은 1000주", ka90009는 "금액(천만)·수량(천)", ka10131은 백만원·1주입니다. 종목 순매수(백만원)를 업종 순매수(억원)와 비율로 비교하면 100배 어긋납니다. 또 ka10059·ka10061의 natn(국가)은 단위 설명이 공란인데, 같은 데이터를 차트로 주는 ka10060은 natn을 "단위: 백만원"으로 적습니다. 공란이라고 다른 단위라는 근거는 없지만, 처음 붙일 때 ka10060 값과 한 번 대조해 두십시오.

⑤ KRX·NXT 구분을 넣는 방식이 둘이다

ka10059·ka10061·ka10008에는 거래소 파라미터가 없고, stk_cd에 접미사를 붙여 구분합니다 — 039490(KRX), 039490_NX(NXT), 039490_AL(SOR). 반면 ka10051·ka10131·ka10058·ka10063·ka10066은 별도 stex_tp(1:KRX, 2:NXT, 3:통합)를 받습니다. 같은 봇 안에서 "통합 기준"으로 맞추려면 앞쪽은 _AL, 뒤쪽은 stex_tp=3을 써야 합니다. NXT 시간대와 라우팅 차이는 NXT·KRX 주문 라우팅에 정리했습니다.

⑥ 응답은 부호 붙은 문자열, 당일 행은 확정치 여부가 명세에 없다

모든 숫자 필드가 String이고 "+900"·"-1200"처럼 부호가 붙어 옵니다. int("+900")은 파이썬에서 되지만 빈 문자열은 실패하므로 변환 함수를 하나 두는 게 안전합니다. 그리고 dt에 날짜 하나를 넣는데 응답은 LIST라 일자별 행이 여러 개 옵니다. 장중에 호출했을 때 당일 행이 확정치인지 잠정치인지는 명세에 적혀 있지 않습니다. 장중 집계용 ka10063과 장마감 후용 ka10066이 따로 있다는 점을 보면, 필터 판정에 쓰는 '어제까지' 데이터는 장마감 후 다시 받아 저장하는 편이 재현 가능합니다.

보내는 값 "1" 하나가 TR마다 뜻이 다르다 amt_qty_tp = "1" ka10059 → 금액 ka10131 → 수량 trde_tp = "1" ka10059 → 매수 ka10058 → 순매도 ka10065 → 순매수 TR별 코드표 1곳 CODEBOOK[api_id]
그림 — 키움 REST 공식 명세(kiwoom_api_spec.json, 2026-10-11) 기준. 오류 없이 다른 데이터가 오므로 코드표를 한곳에 모아 둔다.

4. 정규화 층 — TR별 코드표 한 곳에

해법은 단순합니다. 봇의 전략 코드는 "금액"·"순매수" 같은 뜻만 말하고, 실제 코드값은 TR별 표에서 꺼내게 합니다. 표는 명세 원문을 그대로 옮긴 것이라 키움 공지로 바뀌면 이 한 곳만 고치면 됩니다.

CODEBOOK = {
    "ka10059": {"amt": "1", "qty": "2", "net_buy": "0", "buy": "1", "sell": "2"},
    "ka10061": {"amt": "1", "qty": "2", "net_buy": "0"},
    "ka10051": {"amt": "0", "qty": "1"},
    "ka10131": {"amt": "0", "qty": "1", "net_buy_field": ("netslmt_tp", "2")},
    "ka10058": {"net_buy": "2", "net_sell": "1"},
    "ka10065": {"amt": "1", "qty": "2", "net_buy": "1", "net_sell": "2"},
}
UNIT_KRW = {"ka10059": 1_000_000, "ka10061": 1_000_000, "ka10131": 1_000_000,
            "ka10051": 100_000_000, "ka90009": 10_000_000}

def to_num(s):
    s = (s or "").strip()
    return int(s) if s not in ("", "+", "-") else 0

def investor_daily(code, dt, exchange="KRX"):
    suffix = {"KRX": "", "NXT": "_NX", "SOR": "_AL"}[exchange]
    cb = CODEBOOK["ka10059"]
    pages = kiwoom_post("ka10059", "/api/dostk/stkinfo", {
        "dt": dt, "stk_cd": code + suffix,
        "amt_qty_tp": cb["amt"], "trde_tp": cb["net_buy"], "unit_tp": "1"})
    out = []
    for p in pages:
        for row in p.get("stk_invsr_orgn", []):
            out.append({"dt": row["dt"],
                        "frgn_krw": to_num(row["frgnr_invsr"]) * UNIT_KRW["ka10059"],
                        "orgn_krw": to_num(row["orgn"]) * UNIT_KRW["ka10059"],
                        "ind_krw":  to_num(row["ind_invsr"]) * UNIT_KRW["ka10059"]})
    return out

모든 금액을 원 단위로 환산해 저장하면 ④의 단위 문제도 같이 사라집니다. 저장 시 TR 코드와 조회 시각을 함께 남기면, 나중에 숫자가 이상할 때 어느 TR의 어느 코드였는지 바로 추적됩니다(자동매매 로그 설계).

5. 봇에서 쓰는 법 — 수급 필터

장마감 후 후보 종목마다 ka10059를 한 번씩 받아 저장하고, 다음 날 장 시작 전 필터를 계산합니다. 종목 수가 많으면 키움 REST 호출 제한에 걸리므로 공식 예제처럼 간격을 두고, 429 응답 처리는 키움 REST 429 대응을 따르십시오.

def foreign_and_orgn_streak(rows, days=3):
    recent = sorted(rows, key=lambda r: r["dt"], reverse=True)[:days]
    return len(recent) == days and all(r["frgn_krw"] > 0 and r["orgn_krw"] > 0 for r in recent)

candidates = ["005930", "000660"]
passed = []
for code in candidates:
    rows = investor_daily(code, dt="20261008")   # 장마감 후 확정 데이터로
    if foreign_and_orgn_streak(rows):
        passed.append(code)
    time.sleep(0.2)

시장 전체에서 연속 순매수 종목을 먼저 추리고 싶다면 ka10131(dt=3, stk_inds_tp=0)로 후보를 줄인 뒤 ka10059로 확인하는 2단 구성이 호출 수를 줄입니다. 이때 ①처럼 ka10131의 금액 코드는 "0"입니다. 체결강도·거래원·프로그램매매까지 묶은 수급 필터 설계는 수급 데이터로 봇 필터 만들기에, 한국투자증권 쪽 같은 데이터는 KIS 투자자별 순매수 API에 정리했습니다.

투자자별 매매 데이터는 지나간 거래 주체의 기록입니다. 외국인·기관 연속 순매수가 이후 가격을 보장하지 않으며, 필터의 효과는 비용을 넣은 백테스트로 따로 확인해야 합니다. TR 코드값과 단위는 2026-10-11 키움증권 공식 저장소 명세 기준이며, 변경 공지는 키움 REST API 포털 공지사항을 확인하십시오.

6. 자주 묻는 질문

키움 REST API에서 종목별 외국인·기관 순매수는 어떤 TR로 받나요?

일자별로 받으려면 ka10059(종목별투자자기관별요청), 기간 합계는 ka10061(종목별투자자기관별합계요청)을 씁니다. 둘 다 POST /api/dostk/stkinfo 이고 api-id 헤더로 구분합니다. ka10059의 필수 body는 dt, stk_cd, amt_qty_tp(1:금액, 2:수량), trde_tp(0:순매수, 1:매수, 2:매도), unit_tp(1000:천주, 1:단주) 다섯 개입니다.

amt_qty_tp에 1을 넣었는데 수량이 나옵니다. 왜 그런가요?

TR마다 코드가 다르기 때문입니다. ka10059·ka10060·ka10061·ka10064·ka10066은 1이 금액, 2가 수량이지만 ka10051(업종별투자자순매수)과 ka10131(기관외국인연속매매현황)은 0이 금액, 1이 수량입니다. 같은 1을 보내도 TR에 따라 금액과 수량이 바뀌므로 TR별 코드표를 따로 두어야 합니다.

ka10059 금액 단위는 원인가요?

공식 명세상 금액 단위는 백만원입니다. 수량은 unit_tp에 따라 1000주 또는 1주입니다. 다만 업종 단위 ka10051은 억원 혹은 1000주, 외국인기관매매상위 ka90009는 금액(천만)·수량(천)으로 단위가 달라 여러 TR을 섞을 때는 원·주 단위로 환산한 뒤 비교해야 합니다.

외국인이 연속 순매수하는 종목은 오르나요?

그렇게 단정할 수 없습니다. 투자자별 매매 데이터는 과거 거래 주체의 기록일 뿐 이후 가격 방향을 알려 주지 않습니다. 자동매매에서는 매수 신호 자체보다 다른 조건과 함께 쓰는 보조 필터로 두고, 백테스트로 효과를 따로 확인하는 것이 일반적입니다.

수급 필터까지 넣은 키움 봇, 만들어 드립니다

원하는 조건만 말로 알려 주시면 됩니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기