KIS API 외국인·기관 순매수 — 당일치는 장 끝나야 나온다
GET /uapi/domestic-stock/v1/quotations/inquire-investor, tr_id는 FHKST01010900입니다.
그런데 한국투자증권 공식 예제의 유의사항에 "당일 데이터는 장 종료 후 제공됩니다"라고 적혀 있습니다.
즉 장중에 이 TR을 부르면 오늘 값이 아니라 전 거래일 값이 마지막 행일 수 있습니다.
장중에 화면에서 보이는 외국인·기관 숫자는 HHPTJ04160200·FHPTJ04400000의 가집계이고,
이것은 증권사 직원이 하루 네 번 입력하는 추정치입니다.
"외국인이 사는 종목만 담게 해 주세요." 자동매매 제작 문의에서 손에 꼽게 자주 나오는 조건입니다. 그런데 이 조건을 코드로 옮기는 순간, 대부분의 봇이 자기도 모르게 어제 숫자를 보고 오늘 주문을 냅니다. API가 에러를 내지 않기 때문에 이 사실을 알아채기까지 오래 걸립니다.
이 글은 2026-08-26 시점 한국투자증권 공식 저장소(koreainvestment/open-trading-api)의
국내주식 기본시세·시세분석 예제와 컬럼 매핑 파일에서 확인한 항목명·유의사항을 기준으로,
수급 데이터를 봇에 넣을 때 반드시 알아야 하는 시점 문제를 정리한 것입니다.
파이썬으로 주식 데이터를 모으는 전체 그림은
파이썬 주식 데이터 — pykrx·FinanceDataReader 비교 쪽에 있습니다.
1. 확정치를 주는 TR — FHKST01010900
이름은 "주식현재가 투자자"지만 실제로 돌아오는 것은 일자별 시계열입니다. 파라미터는 현재가 조회와 똑같이 두 개뿐입니다.
import requests
BASE = "https://openapi.koreainvestment.com:9443"
def inquire_investor(token, appkey, appsecret, code, mrkt="J"):
url = BASE + "/uapi/domestic-stock/v1/quotations/inquire-investor"
headers = {
"content-type": "application/json; charset=utf-8",
"authorization": "Bearer " + token,
"appkey": appkey,
"appsecret": appsecret,
"tr_id": "FHKST01010900", # 실전·모의 동일
"custtype": "P",
}
params = {
"FID_COND_MRKT_DIV_CODE": mrkt, # J:KRX, NX:NXT, UN:통합
"FID_INPUT_ISCD": code, # 005930
}
r = requests.get(url, headers=headers, params=params, timeout=5)
body = r.json()
if body["rt_cd"] != "0":
raise RuntimeError(body["msg_cd"] + " " + body["msg1"])
return body["output"] # 일자별 리스트
여기서 output은 리스트입니다.
현재가 조회 FHKST01010100은 객체 하나가 오는 것과 다릅니다.
같은 "주식현재가" 계열이라 헷갈리기 쉬운 지점입니다.
{
"rt_cd": "0",
"output": [
{
"stck_bsop_date": "20260825", <- 반드시 이 값을 확인할 것
"stck_clpr": "71800",
"prdy_vrss": "-900",
"prdy_vrss_sign": "5",
"prsn_ntby_qty": "412350", <- 개인 순매수 수량
"frgn_ntby_qty": "-238100", <- 외국인 순매수 수량 (음수면 순매도)
"orgn_ntby_qty": "-171420", <- 기관계 순매수 수량
"prsn_ntby_tr_pbmn": "29612", <- 순매수 거래대금 (단위는 명세 확인)
"frgn_ntby_tr_pbmn": "-17098",
"orgn_ntby_tr_pbmn": "-12310"
},
{ "stck_bsop_date": "20260822", "...": "..." }
]
}
2. 응답 항목은 3 x 3 구조다
공식 저장소의 컬럼 매핑을 보면 규칙이 단순합니다. 주체 3개 × 방향 3개이고, 각각 수량과 거래대금 두 벌이 있습니다.
| 접두어 | 주체 | 중간 | 방향 | 접미어 | 단위 |
|---|---|---|---|---|---|
prsn_ | 개인 | ntby | 순매수 | _qty | 수량 |
frgn_ | 외국인 | shnu | 매수 | _vol | 거래량 |
orgn_ | 기관계 | seln | 매도 | _tr_pbmn | 거래대금 |
조합하면 frgn_ntby_qty(외국인 순매수 수량), orgn_seln_tr_pbmn(기관계 매도 거래대금),
prsn_shnu_vol(개인 매수 거래량)처럼 읽힙니다.
순매수 항목만 쓰면 매수·매도 양쪽이 모두 컸던 날과 거래가 없었던 날이 구분되지 않습니다.
거래 자체가 활발했는지 보려면 shnu·seln을 같이 봐야 합니다.
그리고 이 응답도 현재가 조회와 마찬가지로 값이 전부 문자열입니다.
frgn_ntby_qty가 "-238100"처럼 오므로 int()로 바꾸지 않고 부등호 비교를 하면 부호 판단이 깨집니다.
3. 핵심 함정 — 당일 데이터는 장 종료 후
공식 예제의 함수 설명에 유의사항이 두 줄 붙어 있습니다. 두 번째 줄이 이겁니다.
"당일 데이터는 장 종료 후 제공됩니다."
그래서 오전 10시에 FHKST01010900을 호출하면
output[0]의 stck_bsop_date가 오늘이 아니라 전 거래일일 수 있습니다.
output[0]을 무조건 "오늘 수급"으로 간주하는 코드는
장중 내내 어제 숫자로 매수·매도 판단을 합니다.
에러도 안 나고 값도 그럴싸해서 몇 달을 그대로 돌리는 경우가 많습니다.
from datetime import datetime
rows = inquire_investor(...)
today = datetime.now().strftime("%Y%m%d")
latest = rows[0]
if latest["stck_bsop_date"] != today:
# 오늘 확정치가 아직 없다 = 정상 상황
log.info("당일 수급 미확정, 기준일=%s", latest["stck_bsop_date"])
# 판단은 항상 '확정된 날짜' 기준으로
ref_date = latest["stck_bsop_date"]
frgn = int(latest["frgn_ntby_qty"])
orgn = int(latest["orgn_ntby_qty"])
4. 그럼 장중에 보이는 그 숫자는 뭔가 — 가집계 2종
HTS·MTS에서 장중에도 외국인·기관 수치가 움직이는 것을 보셨을 겁니다. 그것은 확정치가 아니라 가집계입니다. 공식 저장소에 이 가집계를 주는 API가 두 개 있습니다.
| 구분 | tr_id | 경로 끝 | 범위 |
|---|---|---|---|
| 주식현재가 투자자 (확정) | FHKST01010900 | inquire-investor | 종목 1개 · 일자별 |
| 종목별 외인기관 추정가집계 | HHPTJ04160200 | investor-trend-estimate | 종목 1개 · 장중 추정 |
| 국내기관·외국인 매매종목가집계 | FHPTJ04400000 | foreign-institution-total | 시장 전체 순매수 상위 |
두 가집계 API의 공식 설명에는 똑같은 문장이 붙어 있습니다. "증권사 직원이 장중에 집계·입력한 자료를 단순 누계한 수치"라는 것입니다. 그리고 입력 시각까지 명시되어 있습니다.
| 주체 | 입력 시각(공식 안내) |
|---|---|
| 외국인 | 09:30 · 11:20 · 13:20 · 14:30 |
| 기관종합 | 10:00 · 11:20 · 13:20 · 14:30 |
공식 안내는 여기에 "입력한 시간은 10분 정도 차이가 발생할 수 있으며, 장 운영 사정에 따라 변동될 수 있습니다"라는 단서를 붙입니다.
여기서 나오는 실무 결론: 수급 데이터를 초 단위로 폴링할 이유가 없습니다.
값은 하루 네 번 바뀌는데 1초마다 부르면 호출 예산만 태우고
EGW00201 초당 거래건수 초과에 가까워집니다.
갱신 시각 직후에만 한 번씩 부르고, 나머지 호출 예산은 시세·주문 쪽에 쓰는 게 맞습니다.
5. 시장 전체에서 상위를 뽑는 FHPTJ04400000
"외국인 순매수 상위 종목"을 유니버스로 쓰고 싶다면 종목을 하나씩 도는 대신
foreign-institution-total을 부르는 편이 호출 수가 훨씬 적습니다.
다만 파라미터가 여섯 개라 처음에는 헷갈립니다.
| 파라미터 | 뜻 | 공식 예제 값 |
|---|---|---|
FID_COND_MRKT_DIV_CODE | 시장 분류 | V |
FID_COND_SCR_DIV_CODE | 조건화면분류 | 16449 |
FID_INPUT_ISCD | 대상 | 0000 전체 / 0001 코스피 / 1001 코스닥 |
FID_DIV_CLS_CODE | 정렬 기준 | 0 수량 / 1 금액 |
FID_RANK_SORT_CLS_CODE | 순위 방향 | 0 순매수 상위 / 1 순매도 상위 |
FID_ETC_CLS_CODE | 주체 | 0 전체 / 1 외국인 / 2 기관계 / 3 기타 |
주의할 점은 이 TR의 FID_COND_MRKT_DIV_CODE가 V라는 것입니다.
시세 조회에서 쓰는 J/NX/UN 체계와 다릅니다.
TR마다 같은 이름의 파라미터가 다른 값 체계를 쓰는 경우가 있으므로 서로 복사하면 안 됩니다.
이 화면은 HTS의 외국인·기관 매매종목 가집계 화면을 API로 옮긴 것이라,
같은 화면을 띄워 놓고 비교하면 필드 의미를 훨씬 빨리 파악할 수 있습니다.
순위 계열 TR로 후보를 줄이는 패턴 자체는
거래량순위 API로 종목 스캐너 만들기와 같습니다.
6. 함정 — '외국인'의 정의와 이름이 겹치는 항목들
공식 유의사항의 첫 줄은 이렇습니다.
"외국인은 외국인(외국인투자등록 고유번호가 있는 경우) + 기타 외국인을 지칭합니다."
즉 frgn_ntby_qty는 등록 외국인만이 아니라 기타 외국인까지 합친 값입니다.
다른 데이터 소스의 "외국인"과 숫자가 정확히 맞지 않아도 이상한 게 아닙니다.
더 자주 사고가 나는 건 이름이 겹치는 항목입니다.
| 항목명 | 어디에 있나 | 무슨 뜻인가 |
|---|---|---|
frgn_ntby_qty | FHKST01010900 | 그 일자의 외국인 순매수 수량 (시계열) |
frgn_ntby_qty | FHKST01010100 | 현재가 스냅샷에 붙은 외국인 순매수 수량 |
pgtr_ntby_qty | FHKST01010100 | 프로그램매매 순매수 수량 — 외국인과 다른 개념 |
hts_frgn_ehrt | FHKST01010100 | 외국인 소진율 — 흐름이 아니라 비율 |
frgn_hldn_qty | FHKST01010100 | 외국인 보유 수량 — 흐름이 아니라 잔량 |
이름이 같다고 같은 데이터가 아닙니다. 수집 코드에서 컬럼명을 그대로 변수명으로 쓰면 두 TR의 값이 한 변수에 섞입니다. TR 이름을 접두어로 붙여 두는 편이 안전합니다. 다른 KIS TR에서 나는 오류들은 KIS API 에러코드 정리에 모아 두었습니다.
7. 수량이냐 금액이냐 — 지표가 뒤집히는 지점
_qty(수량)와 _tr_pbmn(거래대금)은 순위를 다르게 만듭니다.
주가가 낮은 종목은 수량 기준에서 크게 보이고, 주가가 높은 종목은 금액 기준에서 크게 보입니다.
"외국인 순매수 상위 20종목"이라는 같은 문장이 기준에 따라 전혀 다른 목록이 됩니다.
FHPTJ04400000이 FID_DIV_CLS_CODE로 수량 정렬과 금액 정렬을 아예 파라미터로 갈라 둔 것도 그래서입니다.
봇의 조건을 정할 때 어느 쪽을 쓰는지 문서에 적어 두십시오.
나중에 결과가 달라졌을 때 원인을 찾는 시간이 크게 줄어듭니다.
8. 봇에 넣을 때 지켜야 할 규칙 3개
| 규칙 | 이유 |
|---|---|
조건은 확정치로만 — stck_bsop_date를 반드시 읽는다 | 장중 output[0]은 전 거래일일 수 있다 |
| 가집계는 참고용 — 하루 4회 갱신, 사람이 입력한 추정치 | 실시간 체결 집계가 아니다 |
| 호출은 갱신 시각 직후에만 | 값이 안 바뀌는 구간의 폴링은 호출 예산 낭비 |
백테스트에서는 여기에 하나가 더 붙습니다. 당일 확정 수급은 장 종료 후에 나오므로, 그날 종가에 매수하면서 그날 수급을 조건으로 쓰면 실제로는 알 수 없었던 정보를 쓴 셈이 됩니다. 전 거래일 확정치를 조건으로 쓰고 매매는 다음 거래일에 하는 식으로 시점을 어긋나게 잡아야 실행 가능한 검증이 됩니다. 이런 유형의 오류를 걸러 내는 방법은 백테스트 데이터 스누핑 쪽에 정리해 두었습니다.
수익·방향에 대한 단정은 하지 않습니다. 이 글은 데이터를 어떻게 받아 오고 언제 갱신되는지를 다룬 것이지, 외국인·기관 순매수가 이후 주가 방향을 예측한다는 뜻이 아닙니다. 수급 지표는 과거 데이터로 검증하는 학술적 개념이며 과거의 패턴이 미래에도 반복된다는 보장은 없습니다. 항목의 정의·제공 시점·호출 한도는 개발자 포털의 현재 명세로 확인하십시오.
자주 묻는 질문
며칠치까지 한 번에 조회되나요?
FHKST01010900은 조회 기간을 지정하는 파라미터가 없습니다.
필수 파라미터는 시장 구분과 종목코드 둘뿐이고, 최근 구간을 서버가 정한 만큼 돌려줍니다.
긴 기간의 수급 시계열이 필요하면 매일 받아 자기 DB에 쌓는 구조가 맞습니다.
조회로 과거를 언제든 되살릴 수 있다고 가정하지 마십시오.
모의투자에서도 되나요?
공식 예제는 env_dv를 real과 demo로 받고 양쪽 모두 tr_id가 FHKST01010900으로 같습니다.
환경을 가르는 것은 tr_id가 아니라 도메인입니다.
다만 모의 환경에서 지원되지 않는 TR도 있으므로, 가집계 계열은 실계좌 환경에서 먼저 응답을 확인해 보시는 편이 확실합니다.
NXT 체결분도 포함되나요?
FID_COND_MRKT_DIV_CODE에 무엇을 넣느냐에 따라 달라집니다.
공식 예제의 설명은 J:KRX, NX:NXT, UN:통합입니다.
주문은 통합·SOR로 내면서 수급은 J로 보는 조합이면 기준이 어긋납니다.
시세·수급·주문이 같은 시장 구분 설정을 공유하도록 잡아 두십시오.
키움 REST API에도 같은 데이터가 있나요?
키움에도 투자자별 매매동향 계열 TR이 있지만 항목명 체계와 갱신 시점이 KIS와 다릅니다. 두 증권사를 함께 쓰는 봇이라면 각각의 명세로 따로 확인해야 하고, 한쪽 항목명을 다른 쪽에 복사하면 안 됩니다. 증권사별 API 범위 차이는 국내 증권사 API 비교에 정리해 두었습니다.
2026-08-26 시점 한국투자증권 공식 저장소(koreainvestment/open-trading-api)의
국내주식 기본시세·시세분석 예제 코드와 컬럼 매핑 파일에서 확인한 항목명·유의사항을 기준으로 썼고,
본문의 응답 예시 값은 구조 이해를 돕기 위한 것입니다.
증권사 명세와 데이터 제공 시점은 예고 없이 바뀝니다.
운영에 넣기 전 개발자 포털의 현재 명세로 대조하시기 바랍니다.
수급 조건을 제대로 반영한 봇이 필요하다면
확정치와 가집계를 구분해 쓰고, 갱신 시각에 맞춰 호출하고, 백테스트 시점까지 맞춘 자동매매 프로그램으로 만들어 드립니다. 24시간 빠른 답변 가능합니다.
무료 상담 시작하기