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

키움 ka10063 장중투자자별매매 — 예시에 금액 7칸이 없다

키움 REST · 시세 2026-11-03 · 약 6분 읽기 · 알고랩 AlgoLab
한 줄 요약

ka10063(장중투자자별매매요청)은 종목코드 없이 시장(mrkt_tp)과 투자자 하나(invsr)를 고르면, 그 투자자의 장중 매수·매도·순매수와 '이전 시점' 대비 증감을 종목 목록(opmr_invsr_trde)으로 주는 TR입니다. URL은 POST /api/dostk/mrkcond이고 amt_qty_tp는 1(금액&수량) 하나뿐입니다. 함정은 공식 예시에 있습니다. 명세의 응답 필드는 21개인데 예시 행에는 14개만 있고 netprps_amt·buy_amt·sell_amt 등 금액 7칸이 통째로 빠져 있으며, 매도 수량은 "--30000"처럼 마이너스가 두 번 붙어 pd.to_numeric이 읽지 못합니다. 투자자 코드는 6=외국인, 7=기관계이고 개인은 고를 수 없습니다. (키움 공식 명세 kiwoom_api_spec.json·공식 예제, 저장소 main 953e5db 기준, pandas 2.2.3 실측, 2026-11-03 확인)

목차

  1. 형제 TR과 무엇이 다른가
  2. 요청·응답 실물
  3. 예시를 그대로 믿으면 깨지는 곳 4가지 (실측)
  4. 검산 — 매수·매도·증감 세 식이 맞는다
  5. 받아서 저장하는 코드
  6. 자주 묻는 질문

1. 형제 TR과 무엇이 다른가

키움 REST에는 이름이 비슷한 장중·장후 투자자 TR이 네 개 있습니다. 요청 필드를 명세 원문으로 대조하면 성격이 전부 다릅니다.

TRURL종목코드투자자 지정매매구분금액/수량
ka10063 장중투자자별매매mrkcond없음 (시장 전체 목록)invsr 1자리 (6 외국인 · 7 기관계 …)없음 — 매수·매도·순매수 모두 옴1 금액&수량 하나
ka10064 장중투자자별매매차트chartstk_cd 필수없음 (투자자 칸이 열로 옴)trde_tp 0 순매수1 금액 · 2 수량
ka10065 장중투자자별매매상위rkinfo없음 (순위)orgn_tp 4자리 (9000 외국인 …)trde_tp 1 순매수1 · 2 (선택)
ka10066 장마감후투자자별매매mrkcond없음 (시장 전체 목록)없음 (투자자 칸이 열로 옴)trde_tp 0 순매수1 · 2

정리하면 ka10063은 "외국인(또는 기관계) 한 주체가 지금 장중에 어떤 종목을 얼마나 사고팔고 있나"를 시장 전체로 훑는 TR입니다. 종목 하나의 시각별 흐름은 ka10064, 순위만 필요하면 ka10065, 장 마감 후 확정치는 ka10066입니다. 같은 '외국인'인데 ka10063은 6, ka10065는 9000이라 코드 상수를 공유하면 안 됩니다.

2. 요청·응답 실물

필드한글명값 (명세 원문)
mrkt_tp시장구분000 전체 · 001 코스피 · 101 코스닥
amt_qty_tp금액수량구분1 금액&수량 (다른 값 없음)
invsr투자자별6 외국인 · 7 기관계 · 1 투신 · 0 보험 · 2 은행 · 3 연기금 · 4 국가 · 5 기타법인
frgn_all외국계전체1 체크 · 0 미체크
smtm_netprps_tp동시순매수구분1 체크 · 0 미체크
stex_tp거래소구분1 KRX · 2 NXT · 3 통합 (원문 표기 "3.통합")

여섯 칸 모두 필수(required: Y)입니다. 헤더에는 api-id: ka10063과 authorization: Bearer …를 넣고, 다음 페이지가 있으면 응답 헤더의 cont-yn·next-key를 그대로 돌려줍니다. 명세의 요청 예시입니다.

{
    "mrkt_tp": "000",
    "amt_qty_tp": "1",
    "invsr": "6",
    "frgn_all": "0",
    "smtm_netprps_tp": "0",
    "stex_tp": "3"
}

명세 response_example 원문 중 두 번째 행입니다(첫 행도 같은 005930).

{
    'stk_cd': '005930',
    'stk_nm': '삼성전자',
     'cur_prc': '284',
     'pre_sig': '3',
     'pred_pre': '0',
     'flu_rt': '0.00',
     'acc_trde_qty': '0',
     'netprps_qty': '--261000',
     'prev_pot_netprps_qty': '--347000',
     'netprps_irds': '+86000',
     'buy_qty': '+2728000',
     'buy_qty_irds': '+108000',
     'sell_qty': '--2989000',
     'sell_qty_irds': '+22000'
},
...
'return_code': 0,
'return_msg': '정상적으로 처리되었습니다'

명세상 단위는 금액 칸이 전부 백만원, 수량 칸이 1주입니다(ka10044의 netprps_qty가 100주였던 것과 다릅니다 — ka10044 글).

3. 예시를 그대로 믿으면 깨지는 곳 4가지 (실측)

명세 예시를 테스트 픽스처로 쓰거나, 예시를 보고 파서를 짜면 아래에서 막힙니다. Python 3.10.4 · pandas 2.2.3에서 돌린 결과입니다.

json.loads -> JSONDecodeError Expecting property name enclosed in double quotes: line 2 column 5 (char 6)
spec fields 21 example fields 14
missing in example: ['netprps_amt', 'prev_netprps_amt', 'buy_amt', 'netprps_amt_irds', 'buy_amt_irds', 'sell_amt', 'sell_amt_irds']
naive to_numeric(coerce) sell_qty: [nan, nan]
int('--30000') -> ValueError invalid literal for int() with base 10: '--30000'
to_numeric raise -> ValueError Unable to parse string "--30000" at position 0
duplicate stk_cd: [False, True]

① 금액 7칸이 예시에 없다

명세 응답 본문은 stk_cd부터 sell_qty_irds까지 21칸인데, 예시 행은 14칸입니다. 빠진 것은 netprps_amt·prev_netprps_amt·buy_amt·netprps_amt_irds·buy_amt_irds·sell_amt·sell_amt_irds — 금액 칸 전부입니다. amt_qty_tp가 "금액&수량"이니 실제 응답엔 둘 다 올 것으로 보이지만, 예시만 보고 짠 코드는 금액 칸이 없는 것으로 가정하고, 반대로 명세만 보고 row["netprps_amt"]로 짠 코드는 예시 픽스처에서 KeyError가 납니다. 실계좌(또는 모의) 첫 응답의 키 목록을 로그로 남기고, 코드는 reindex(columns=...)로 21칸을 고정해 두는 쪽이 안전합니다.

② 매도 수량은 '--' 이중 부호로 온다

sell_qty "--30000"·"--2989000", netprps_qty "--261000"처럼 음수에 마이너스가 두 개 붙어 있습니다. int()·pd.to_numeric()은 ValueError, errors="coerce"는 조용히 NaN을 만듭니다. 같은 현상이 ka10061에도 있었습니다. 매도 수량은 부호가 음수로 실려 오는 칸이라, NaN이 되면 '매도 0'처럼 보여 순매수 계산이 매수 쪽으로 쏠립니다.

③ 공식 예제의 표시 함수도 이 값을 못 다룬다

키움 공식 예제 examples/국내주식/시세/get_domestic_intraday_investor_trade.py는 MAX_PAGES = 10, REQUEST_DELAY_SECONDS = 0.2로 연속조회를 하고 pandas.DataFrame으로 돌려줍니다. 값은 문자열 그대로이고, 출력용 _format_display_value()에 예시 값을 넣으면 이렇게 나옵니다.

_format_display_value('--30000') -> '--30000'
_format_display_value('+1083000') -> '+1083000'
_format_display_value('-261000') -> '-261,000'
_format_display_value('1083000') -> '1,083,000'
_format_display_value('+86000') -> '+86000'
COLUMNS 21 NUMERIC 16 not numeric: ['대비기호', '순매수증감', '전일대비', '종목명', '종목코드']

마이너스 하나짜리만 천 단위 쉼표가 붙고, +로 시작하거나 --로 시작하는 값은 원문 그대로 남습니다. 또 NUMERIC_COLUMNS(16개)에 순매수증감(netprps_irds)과 전일대비(pred_pre)가 빠져 있습니다. 예제는 출력용일 뿐 숫자 변환은 해 주지 않으니, 계산하려면 직접 파싱해야 합니다.

④ 같은 종목코드가 두 행이다

예시 두 행이 모두 005930 삼성전자입니다. cur_prc도 '64'·'284'로 실제 가격이 아닌 자리채움 값입니다. 예시는 형식 견본일 뿐이라는 뜻이지만, stk_cd를 딕셔너리 키나 DB 기본키로 쓰는 코드는 이런 응답을 받으면 앞 행을 덮어씁니다. 저장 키는 (조회시각, invsr, mrkt_tp, stex_tp, stk_cd)로 잡고, 중복이 오면 버리지 말고 경고 로그를 남기십시오. 특히 stex_tp=3(통합)에서 KRX·NXT 행이 따로 오는지는 명세로 확인되지 않으니 첫 응답에서 직접 보셔야 합니다.

4. 검산 — 매수·매도·증감 세 식이 맞는다

앞의 -를 음수로, 나머지 부호 문자를 지우는 파서로 읽으면 예시 두 행 모두 세 식이 정확히 맞습니다. 부호 해석이 옳다는 증거입니다.

buy + sell == net: [True, True]            # sell_qty 는 음수로 실려 온다
net - prev == irds: [True, True]           # netprps_qty - prev_pot_netprps_qty == netprps_irds
buy_irds - sell_irds == irds: [True, True] # 108000 - 22000 == 86000
예시 2행 · 단위 1주 buy_qty +2,728,000 sell_qty --2,989,000 (= −2,989,000) netprps_qty −261,000 = 매수 + 매도 이전 시점 −347,000 → 지금 −261,000 netprps_irds +86,000 = buy_qty_irds 108,000 − sell_qty_irds 22,000
매도가 음수로 실려 오므로 순매수 = 매수 + 매도. 증감 = 매수 증감 − 매도 증감(매도 증감은 크기 기준)

마지막 식에서 sell_qty_irds +22000은 매도 '크기'가 22,000주 늘었다는 뜻으로 읽어야 맞습니다. 같은 응답 안에서 매도 수량은 음수로, 매도 증감은 양수로 오는 셈이라 두 칸을 같은 규칙으로 더하면 틀립니다. '이전 시점'이 정확히 언제인지(직전 조회인지, 일정 간격인지)는 명세에 적혀 있지 않습니다. 증감 칸을 신호로 쓰려면 같은 조건으로 두 번 연달아 호출해 prev_pot_netprps_qty가 직전 응답의 netprps_qty와 같은지부터 확인하십시오.

5. 받아서 저장하는 코드

import os, time, requests, pandas as pd

URL = "https://api.kiwoom.com/api/dostk/mrkcond"      # 모의: https://mockapi.kiwoom.com
COLS = ["stk_cd", "stk_nm", "cur_prc", "pre_sig", "pred_pre", "flu_rt", "acc_trde_qty",
        "netprps_amt", "prev_netprps_amt", "buy_amt", "netprps_amt_irds", "buy_amt_irds",
        "sell_amt", "sell_amt_irds", "netprps_qty", "prev_pot_netprps_qty", "netprps_irds",
        "buy_qty", "buy_qty_irds", "sell_qty", "sell_qty_irds"]          # 명세 21칸
NUM = [c for c in COLS if c not in ("stk_cd", "stk_nm", "pre_sig")]

def num(s):
    """'--30000' -> -30000, '+1083000' -> 1083000, '' -> NaN"""
    s = str(s).strip().replace(",", "")
    if s in ("", "None", "nan"):
        return float("nan")
    neg = s.startswith("-")
    s = s.lstrip("+-")
    return -float(s) if neg else float(s)

def ka10063(token, invsr="6", mrkt_tp="000", stex_tp="1", max_pages=10):
    body = {"mrkt_tp": mrkt_tp, "amt_qty_tp": "1", "invsr": invsr,
            "frgn_all": "0", "smtm_netprps_tp": "0", "stex_tp": stex_tp}
    rows, cont, key = [], None, None
    for _ in range(max_pages):
        h = {"api-id": "ka10063", "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(URL, headers=h, json=body, timeout=10)
        j = r.json()
        if j.get("return_code") != 0:
            raise RuntimeError(f"ka10063 {j.get('return_code')} {j.get('return_msg')}")
        rows += j.get("opmr_invsr_trde", [])
        cont, key = r.headers.get("cont-yn"), r.headers.get("next-key")
        if cont != "Y":
            break
        time.sleep(0.2)                                   # 공식 예제와 같은 간격
    df = pd.DataFrame(rows)
    missing = [c for c in COLS if c not in df.columns]
    if missing:
        print("응답에 없는 칸:", missing)                 # ① 금액 7칸 확인용
    df = df.reindex(columns=COLS)
    df[NUM] = df[NUM].apply(lambda s: s.map(num))
    bad = df[(df.buy_qty + df.sell_qty) != df.netprps_qty]
    if len(bad):
        print("검산 불일치", len(bad), "행")               # 부호 해석이 틀렸다는 신호
    if df.stk_cd.duplicated().any():
        print("중복 종목코드", df.stk_cd[df.stk_cd.duplicated()].tolist())
    df.insert(0, "asof", pd.Timestamp.now(tz="Asia/Seoul"))
    df.insert(1, "invsr", invsr)
    return df

# token = os.environ["KIWOOM_TOKEN"]     # 키·토큰은 코드에 적지 말 것
# foreign = ka10063(token, invsr="6")    # 외국인
# foreign.nlargest(20, "netprps_qty")[["stk_nm", "netprps_qty", "netprps_irds"]]

토큰 발급과 return_code 처리 같은 키움 REST 공통 구조는 키움 REST API 자동매매 가이드에 있습니다. 장중에 몇 분마다 돌릴 때는 다른 TR과 합쳐 초당 호출 수를 넘지 않게 간격을 두시고, 이렇게 모은 외국인·기관 장중 순매수를 매매 조건으로 쓰는 방법은 체결강도·거래원·프로그램매매로 봇 필터 만들기를 참고하세요. 장중 수급 수치는 잠정치라 장 마감 후 확정치(ka10066)와 다를 수 있습니다.

6. 자주 묻는 질문

ka10063과 ka10064는 어떻게 다른가요?

ka10063은 POST /api/dostk/mrkcond로 종목코드 없이 시장과 투자자 하나(invsr)를 골라 종목 목록을 받습니다. ka10064는 /api/dostk/chart로 stk_cd 종목 하나의 장중 시각별 투자자 흐름을 받습니다. 시장 전체를 훑을 때는 ka10063, 고른 종목을 따라갈 때는 ka10064입니다.

ka10063으로 개인 매매도 볼 수 있나요?

명세상 invsr 값은 6 외국인, 7 기관계, 1 투신, 0 보험, 2 은행, 3 연기금, 4 국가, 5 기타법인뿐이라 개인은 고를 수 없습니다.

sell_qty '--30000'은 어떻게 읽나요?

음수 -30000입니다. 이렇게 읽으면 공식 예시 두 행 모두 buy_qty + sell_qty = netprps_qty가 정확히 맞습니다. int()와 pd.to_numeric은 이 문자열에서 ValueError를 내고, errors='coerce'는 NaN을 만들어 매도가 0처럼 보이게 합니다.

netprps_irds는 무엇과 비교한 증감인가요?

예시에서 netprps_irds는 netprps_qty - prev_pot_netprps_qty와 정확히 같고, buy_qty_irds - sell_qty_irds와도 같습니다. 다만 '이전 시점'이 직전 조회인지 일정 간격인지는 명세에 없으므로, 같은 조건으로 두 번 호출해 직접 확인해야 합니다.

장중 외국인·기관 수급을 보는 봇, 만들어 드립니다

ka10063·ka10064로 장중 순매수를 몇 분마다 받아 부호·단위를 검산해 저장하고, 매매 조건으로 쓰는 키움 REST 봇을 맞춤 제작합니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기