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

키움 ka10061 투자자기관별합계 — '--' 부호 함정

키움 REST · 종목정보 2026-11-02 · 약 6분 읽기 · 알고랩 AlgoLab
한 줄 요약

ka10061(종목별투자자기관별합계요청)은 종목 하나에 대해 strt_dt~end_dt 기간의 개인·외국인·기관계·기관 세부 7곳·국가·기타법인·내외국인 순매수 합계를 한 줄(stk_invsr_orgn_tot)로 주는 TR입니다. 일자별로 받는 ka10059와 URL(/api/dostk/stkinfo)은 같고, dt 대신 기간 두 칸을 받으며 trde_tp는 0(순매수)뿐입니다. 가장 큰 함정은 공식 응답 예시의 음수가 "--28837"처럼 마이너스 두 개로 온다는 점입니다. pd.to_numeric(errors="coerce")로 넘기면 음수만 NaN이 되고, 개인+외국인+기관+기타법인 합이 원래 −2114인데 66865로 조용히 바뀝니다. (키움 공식 명세 kiwoom_api_spec.json, 저장소 main 953e5db 기준, pandas 2.2.3 실측, 2026-11-02 확인)

목차

  1. ka10059와 무엇이 다른가
  2. 요청·응답 실물
  3. '--28837'을 숫자로 바꾸면 생기는 일 (실측)
  4. 검산 — 합은 0, natfor를 넣느냐가 예시마다 다르다
  5. 받아서 저장하는 코드
  6. 자주 묻는 질문

1. ka10059와 무엇이 다른가

항목ka10059 (일자별)ka10061 (기간 합계)
URLPOST /api/dostk/stkinfo — 같음, api-id 헤더로 구분
날짜 입력dt 하루strt_dt · end_dt (YYYYMMDD, 둘 다 필수)
trde_tp0:순매수, 1:매수, 2:매도0:순매수 하나뿐
응답 리스트stk_invsr_orgn — 날짜별 여러 행stk_invsr_orgn_tot — 합계 1행
가격·거래량 칸dt·cur_prc·pre_sig·pred_pre·flu_rt·acc_trde_qty·acc_trde_prica없음
투자자 칸ind_invsr·frgnr_invsr·orgn·fnnc_invt·insrnc·invtrt·etc_fnnc·bank·penfnd_etc·samo_fund·natn·etc_corp·natfor — 13칸 같음
음수 표기 (공식 예시)"-61779""--28837"

정리하면 ka10061은 "지난 한 달 동안 외국인이 이 종목을 얼마나 순매수했나"를 한 번에 묻는 TR입니다. ka10059로 30일치를 받아 직접 더해도 같은 질문에 답할 수 있지만, 연속조회(cont-yn·next-key)로 여러 페이지를 받아 합쳐야 하고 백만원 단위 반올림이 날마다 쌓입니다. 종목 수십 개를 매일 아침 스크리닝한다면 호출 1번으로 끝나는 ka10061이 레이트리밋 면에서 훨씬 가볍습니다. 날짜별 흐름(언제부터 사기 시작했나)이 필요하면 ka10059나 차트형 ka10060을 씁니다.

2. 요청·응답 실물

POST https://api.kiwoom.com/api/dostk/stkinfo      (모의: https://mockapi.kiwoom.com)
Content-Type: application/json;charset=UTF-8
api-id: ka10061
authorization: Bearer {접근토큰}

{
    "stk_cd": "005930",
    "strt_dt": "20241007",
    "end_dt": "20241107",
    "amt_qty_tp": "1",
    "trde_tp": "0",
    "unit_tp": "1000"
}

명세 원문 값 정의는 amt_qty_tp 1:금액·2:수량, trde_tp 0:순매수, unit_tp 1000:천주·1:단주입니다. stk_cd는 KRX 039490, NXT 039490_NX, SOR 039490_AL처럼 접미사로 거래소를 고릅니다. 아래가 명세에 실린 응답 예시 전문입니다.

{
    "stk_invsr_orgn_tot": [
        {
            "ind_invsr": "--28837",
            "frgnr_invsr": "--40142",
            "orgn": "+64891",
            "fnnc_invt": "+72584",
            "insrnc": "--9071",
            "invtrt": "--7790",
            "etc_fnnc": "+35307",
            "bank": "+526",
            "penfnd_etc": "--22783",
            "samo_fund": "--3881",
            "natn": "0",
            "etc_corp": "+1974",
            "natfor": "+2114"
        }
    ],
    "return_code": 0,
    "return_msg": "정상적으로 처리되었습니다"
}

양수는 +가 붙고 음수는 --가 붙습니다. 금액 모드라 단위는 백만원이고, orgn +64891은 기간 중 기관계 순매수 약 649억원입니다. 같은 저장소 Postman 컬렉션(kiwoom-openapi.postman_collection.json)의 ka10061 요청 URL에는 strt_dt=20260101&end_dt=20260507이 들어 있어, 기간을 몇 달로 잡는 것도 예시 범위 안입니다. 최대 조회 기간은 명세에 적혀 있지 않으니 처음 쓸 때 긴 기간으로 한 번 확인하세요.

3. '--28837'을 숫자로 바꾸면 생기는 일 (실측)

예시 1행을 그대로 DataFrame에 넣고 흔히 쓰는 변환을 하나씩 돌렸습니다(pandas 2.2.3).

① pd.to_numeric(df["ind_invsr"], errors="coerce")  →  [nan]
② pd.to_numeric(df["ind_invsr"])                    →  ValueError: Unable to parse string "--28837" at position 0
③ int("--28837")                                     →  ValueError: invalid literal for int() with base 10: '--28837'
⑦ coerce 후 ind+frgnr+orgn+etc_corp 합계             →  66865.0   (올바른 값 -2114)

②·③은 에러가 나서 차라리 낫습니다. 위험한 건 ①입니다. 데이터 수집 코드에 errors="coerce"를 습관처럼 넣어 두면 음수만 골라서 NaN이 되고, pandas sum()은 NaN을 건너뛰기 때문에 순매도가 통째로 사라진 '매수 우위' 숫자가 에러 없이 저장됩니다. 외국인·기관 순매수로 종목을 거르는 필터라면 순매도 종목이 필터를 통과하는 셈입니다.

이중 부호는 ka10061만의 일이 아닙니다. 사이트에서 정리한 ka10043 거래원매물대분석의 전일대비도 "--1100"으로 왔습니다. 반대로 허브 글의 ka10059 예시는 "-61779" 한 개입니다. TR마다 다를 수 있으니 키움 REST 숫자 칸은 전부 하나의 파서를 거치게 하는 게 안전합니다.

def kw_num(v: str) -> int:
    """키움 REST 숫자 문자열 → int.  '+64891', '--28837', '-61779', '0' 모두 처리"""
    v = (v or "0").strip()
    neg = v.startswith("-")
    v = v.lstrip("+-")
    return -int(v) if neg else int(v)

kw_num("--28837"), kw_num("+64891"), kw_num("-61779")   # → (-28837, 64891, -61779)

4. 검산 — 합은 0, natfor를 넣느냐가 예시마다 다르다

'--'를 음수로 읽는 게 맞는지는 순매수의 성질로 확인할 수 있습니다. 누가 사면 누가 판 것이니 시장 전체 합은 0이어야 합니다.

ind_invsr + frgnr_invsr + orgn + etc_corp           = -28837 - 40142 + 64891 + 1974 = -2114
ind_invsr + frgnr_invsr + orgn + etc_corp + natfor  = -2114 + 2114                   =     0
orgn  vs  기관 세부 7곳 합(fnnc_invt~samo_fund)      = 64891 vs 64892  (백만원 반올림 1)
ka10061 공식 예시 (백만원, 순매수 합계) 개인 −28837 외국인 −40142 기관계 +64891 기타법인 +1974 내외국인 +2114 다섯 항목 합 = 0
'--'를 음수로 읽어야 합이 0이 된다. coerce로 음수를 지우면 +66865가 남는다.

흥미로운 건 natfor(내외국인)입니다. ka10061 예시는 natfor까지 더해야 0이 되는데, 어제 다룬 ka10060 예시(20241107)는 natfor를 빼야 0이었습니다(그날 natfor는 1이라 차이가 작았습니다). 명세에 natfor 정의가 따로 없어 어느 쪽이 규칙인지 단정할 수 없으니, 검산은 두 식 중 하나라도 허용오차 안이면 통과로 두는 게 현실적입니다. 둘 다 크게 벗어나면 부호 파싱이나 단위가 틀린 것입니다.

5. 받아서 저장하는 코드

import requests, pandas as pd

URL = "https://api.kiwoom.com/api/dostk/stkinfo"
COLS = ["ind_invsr", "frgnr_invsr", "orgn", "fnnc_invt", "insrnc", "invtrt", "etc_fnnc",
        "bank", "penfnd_etc", "samo_fund", "natn", "etc_corp", "natfor"]

def ka10061(token: str, stk_cd: str, strt_dt: str, end_dt: str, amt_qty_tp="1", unit_tp="1000"):
    headers = {"Content-Type": "application/json;charset=UTF-8",
               "api-id": "ka10061", "authorization": f"Bearer {token}"}
    body = {"stk_cd": stk_cd, "strt_dt": strt_dt, "end_dt": end_dt,
            "amt_qty_tp": amt_qty_tp, "trde_tp": "0", "unit_tp": unit_tp}
    r = requests.post(URL, headers=headers, json=body, timeout=10)
    js = r.json()
    if js.get("return_code") != 0:
        raise RuntimeError(f"ka10061 {stk_cd}: {js.get('return_code')} {js.get('return_msg')}")
    row = {k: kw_num(js["stk_invsr_orgn_tot"][0].get(k, "0")) for k in COLS}

    base = row["ind_invsr"] + row["frgnr_invsr"] + row["orgn"] + row["etc_corp"]
    if min(abs(base), abs(base + row["natfor"])) > 5:          # 백만원 반올림 허용
        print(f"[검산 경고] {stk_cd} {strt_dt}~{end_dt} 합={base}, +natfor={base + row['natfor']}")
    return {"stk_cd": stk_cd, "strt_dt": strt_dt, "end_dt": end_dt,
            "amt_qty_tp": amt_qty_tp, **row}

# 예: 관심종목 20개의 최근 1개월 외국인·기관 순매수 합계
# df = pd.DataFrame([ka10061(tok, c, "20261002", "20261030") for c in codes])
# df.sort_values("frgnr_invsr", ascending=False)

저장 키는 (stk_cd, strt_dt, end_dt, amt_qty_tp)로 잡아야 금액·수량이나 기간이 다른 결과를 덮어쓰지 않습니다. 여러 종목을 연달아 호출할 때는 키움 공식 차트 예제가 쓰는 0.2초 간격(REQUEST_DELAY_SECONDS)처럼 요청 사이에 쉬어야 하고, 키움 REST 공통 구조(토큰 발급·return_code 처리)는 키움 REST API 자동매매 가이드에 있습니다. 이렇게 모은 합계를 매매 필터로 쓰는 방법은 체결강도·거래원·프로그램매매로 봇 필터 만들기를 참고하세요.

6. 자주 묻는 질문

ka10061과 ka10059는 어떻게 다른가요?

둘 다 POST /api/dostk/stkinfo이고 api-id 헤더로 구분합니다. ka10059는 dt 하루를 넣고 날짜별 여러 행(현재가·등락률·거래량 포함)을 받고, ka10061은 strt_dt·end_dt 기간을 넣고 stk_invsr_orgn_tot 리스트로 투자자 13칸 합계만 받습니다. trde_tp도 ka10061은 0(순매수) 하나뿐입니다.

'--28837'은 무슨 뜻인가요?

음수 -28837입니다. 공식 응답 예시에서 이 값을 음수로 보고 ind_invsr + frgnr_invsr + orgn + etc_corp + natfor를 더하면 정확히 0이 되어 부호 해석을 검산할 수 있습니다. pd.to_numeric과 int()는 이 문자열을 읽지 못하므로 앞의 부호 문자를 정리하는 파서가 필요합니다.

errors='coerce'로 넘기면 안 되나요?

안 됩니다. 이중 부호 값만 NaN이 되고 pandas sum()은 NaN을 건너뛰므로, 공식 예시에서 개인·외국인·기관·기타법인 합이 원래 -2114인데 66865로 나옵니다. 에러 없이 음수만 사라져 매수 우위로 왜곡됩니다.

ka10061 금액 단위는 무엇인가요?

명세상 amt_qty_tp=1(금액)이면 백만원, 2(수량)이면 unit_tp에 따라 1000주 또는 1주입니다. 공식 예시 orgn '+64891'은 기간 중 기관계 순매수 약 649억원이라는 뜻입니다.

외국인·기관 수급 필터 봇, 만들어 드립니다

ka10061·ka10059로 관심종목의 기간 순매수를 매일 받아 부호·단위를 검산해 저장하고, 매매 조건으로 쓰는 키움 REST 봇을 맞춤 제작합니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기