키움 REST API 잔고조회 — kt00018 파싱 함정
kt00018(계좌평가잔고내역요청),
POST /api/dostk/acnt입니다.
필수 항목은 조회구분 qry_tp(1 합산 / 2 개별)와
국내거래소구분 dmst_stex_tp(KRX / NXT) 둘뿐이라 붙이기는 쉽습니다.
어려운 건 읽는 쪽입니다.
보유 종목 목록은 acnt_evlt_remn_indv_tot라는 긴 이름으로 오고,
그 안의 종목번호는 005930이 아니라 A005930
(접두어 A 주식 / J ELW / Q ETN)입니다.
금액·수량은 부호 포함 15자리를 0으로 채운 문자열이고,
rmnd_qty(보유수량)와 trde_able_qty(매매가능수량)는 다른 값입니다.
봇이 매도 판단을 하려면 "지금 뭘 몇 주 들고 있나"를 알아야 합니다. 키움 REST API 주식 주문(kt10000)으로 주문을 내고, ka10075로 미체결을 추적하는 데까지 왔다면 남은 조각이 이 잔고입니다.
그런데 kt00018은 붙이는 난이도와 읽는 난이도가 크게 다릅니다.
요청 항목이 두 개뿐이라 5분이면 200을 받습니다.
문제는 그다음 — 받은 값을 그대로 쓰면 주문이 거부되거나, 에러 없이 틀린 수량이 나갑니다.
이 글은 붙이는 방법이 아니라 읽는 방법을 다룹니다.
목차
- 요청 실물 — 항목이 두 개뿐이다
- 응답은 두 층으로 온다
- 함정 ① 종목코드에 접두어가 붙는다
- 함정 ② 숫자가 0으로 채워진 문자열이다
- 함정 ③ 보유수량 ≠ 매매가능수량
- 함정 ④ dmst_stex_tp는 KRX / NXT 문자열이다
- 포지션 구조체로 정규화하기
- 자주 묻는 것
1. 요청 실물 — 항목이 두 개뿐이다
| 항목 | 값 |
|---|---|
| API ID | kt00018 (계좌평가잔고내역요청) |
| Method / URL | POST /api/dostk/acnt |
| 실전 / 모의 도메인 | https://api.kiwoom.com / https://mockapi.kiwoom.com |
| 필수 헤더 | api-id · authorization(Bearer + 접근토큰) · Content-Type |
| 필수 요청 항목 | qry_tp(조회구분 · 1 합산 / 2 개별)dmst_stex_tp(국내거래소구분 · KRX / NXT) |
| 연속조회 | 응답 헤더 cont-yn · next-key |
import requests
BASE = "https://api.kiwoom.com" # 모의투자는 https://mockapi.kiwoom.com
TOKEN = "..." # au10001로 발급받은 접근토큰
def balance(qry_tp="1", stex="KRX"):
headers = {
"Content-Type": "application/json;charset=UTF-8",
"authorization": f"Bearer {TOKEN}",
"api-id": "kt00018",
}
body = {
"qry_tp": qry_tp, # 1:합산, 2:개별
"dmst_stex_tp": stex, # KRX 또는 NXT ← 숫자가 아니다
}
r = requests.post(f"{BASE}/api/dostk/acnt", headers=headers, json=body, timeout=5)
r.raise_for_status()
return r.json()
계좌번호를 넣는 항목이 없다는 점을 눈여겨보십시오.
키움 REST API는 접근토큰이 계좌를 이미 지정하기 때문에
요청에 계좌번호를 실을 자리가 없습니다.
한국투자증권 KIS가 CANO와 ACNT_PRDT_CD를 매 요청에 요구하는 것과 반대입니다.
토큰이 어디서 나오는지는
키움 접근토큰 유효기간과 expires_dt에 정리해 두었습니다.
2. 응답은 두 층으로 온다
kt00018 응답은 계좌 전체 요약이 최상위에 있고,
보유 종목 목록이 그 아래 리스트로 들어 있는 구조입니다.
| 층 | 항목 | 의미 |
|---|---|---|
| 계좌 전체 | tot_pur_amt | 총매입금액 |
tot_evlt_amt | 총평가금액 | |
tot_evlt_pl | 총평가손익금액 | |
tot_prft_rt | 총수익률(%) — 소수점 둘째 자리 | |
prsm_dpst_aset_amt | 추정예탁자산 | |
| 종목별 ( acnt_evlt_remn_indv_tot) | stk_cd | 종목번호 — 접두어 1자리 + 코드 6자리 |
stk_nm | 종목명 | |
rmnd_qty | 보유수량 | |
trde_able_qty | 매매가능수량 | |
pur_pric · cur_prc | 매입가 · 현재가 | |
evltv_prft · prft_rt | 평가손익 · 수익률(%) | |
pur_amt · evlt_amt | 매입금액 · 평가금액 | |
pur_cmsn · sell_cmsn · tax · sum_cmsn | 매입수수료 · 평가수수료 · 세금 · 수수료합 |
이 밖에 pred_buyq·pred_sellq(전일 매수·매도수량), tdy_buyq·tdy_sellq(금일 매수·매도수량), poss_rt(보유비중), crd_tp·crd_tp_nm·crd_loan_dt(신용구분·대출일)가 함께 옵니다.
응답의 형태는 이렇게 생겼습니다(항목명은 공식 명세, 값은 이해를 돕기 위한 예시입니다).
{
"tot_pur_amt": "000000000718000",
"tot_evlt_amt": "000000000730000",
"tot_evlt_pl": "000000000012000",
"tot_prft_rt": "1.67",
"acnt_evlt_remn_indv_tot": [
{
"stk_cd": "A005930",
"stk_nm": "삼성전자",
"rmnd_qty": "000000000000010",
"trde_able_qty": "000000000000007",
"pur_pric": "000000000071800",
"cur_prc": "000000073000",
"evltv_prft": "000000000012000",
"prft_rt": "1.67",
"poss_rt": "100.00"
}
],
"return_code": 0,
"return_msg": "정상적으로 처리되었습니다"
}
이 한 덩어리 안에 오늘 다룰 함정 세 개가 전부 들어 있습니다.
A005930, 000000000000010, 그리고 rmnd_qty 10과 trde_able_qty 7의 차이입니다.
3. 함정 ① 종목코드에 접두어가 붙는다
공식 명세는 kt00018 응답의 stk_cd를
접두어 1자리 + 종목코드 6자리로 정의하고,
접두어를 A 주식 / J ELW / Q ETN으로 설명합니다.
그래서 삼성전자는 A005930으로 옵니다.
여기서 사고가 납니다.
주문 TR kt10000은 접두어 없는 6자리 종목코드를 받습니다.
잔고에서 읽은 A005930을 그대로 주문에 넣으면
매도해야 할 종목을 찾지 못합니다.
반대로 접두어를 무조건 잘라 내면 ELW·ETN인지 아닌지를 잃어버립니다.
PREFIX = {"A": "STOCK", "J": "ELW", "Q": "ETN"}
def split_code(raw):
"""'A005930' → ('005930', 'STOCK')"""
raw = (raw or "").strip()
if len(raw) == 7 and raw[0] in PREFIX:
return raw[1:], PREFIX[raw[0]]
return raw, "UNKNOWN" # 6자리로 오는 경우도 방어
code, kind = split_code(row["stk_cd"]) # ('005930', 'STOCK')
종목코드 표기가 자리마다 다른 것은 키움만의 일이 아닙니다. 마스터 파일에서 코드를 뽑을 때도 같은 문제가 생기고, 코스피·코스닥 종목코드 마스터 파일 파싱에 그 계열의 함정을 정리해 두었습니다. 내부에서는 항상 6자리 순수 코드 하나로 통일하고, 증권사별 표기는 어댑터 경계에서만 변환하는 것이 원칙입니다.
4. 함정 ② 숫자가 0으로 채워진 문자열이다
명세는 금액 항목을 "좌측 0-padding 처리된 부호 포함 15자리 숫자"로,
현재가·전일종가는 12자리로 정의합니다. 수량도 같은 방식입니다.
즉 "000000000000010"은 10주라는 뜻이고 앞의 0에는 의미가 없습니다.
파이썬에서는 int()가 앞의 0을 알아서 무시하므로 변환만 하면 끝납니다.
위험한 건 변환하지 않고 그대로 쓰는 경우입니다.
# 나쁜 예 — 전부 조용히 틀린다
if row["rmnd_qty"] > "0": # 문자열 사전순 비교
...
qty = row["rmnd_qty"][-3:] # 자릿수를 세어 자르기 (수량이 커지면 깨진다)
# 좋은 예 — 경계에서 한 번만 변환한다
def i(v, d=0):
try: return int(str(v).strip() or d)
except (TypeError, ValueError): return d
def f(v, d=0.0):
try: return float(str(v).strip() or d)
except (TypeError, ValueError): return d
qty = i(row["rmnd_qty"]) # 10
rate = f(row["prft_rt"]) # 1.67
수익률만 타입이 다릅니다.
prft_rt와 tot_prft_rt는 소수점 둘째 자리까지 포맷된 백분율이라
int()로 변환하면 ValueError가 납니다.
금액·수량은 int, 비율은 float로 나누어 처리하십시오.
부호가 앞에 붙어 올 수 있으므로 문자열을 잘라서 판단하지 마십시오 —
손실 구간에서만 자릿수가 하나 밀립니다.
5. 함정 ③ 보유수량 ≠ 매매가능수량
예시에서 rmnd_qty는 10인데 trde_able_qty는 7이었습니다.
3주가 묶여 있다는 뜻입니다. 대표적인 이유는 이렇습니다.
- 이미 매도 주문을 걸어 둔 물량 — 호가창에 나가 있는 동안 잠깁니다.
- 결제가 끝나지 않은 당일 매수분 — 국내 주식은 매수 즉시 전량이 자유롭게 처분되지 않는 구간이 있습니다.
- 신용·대주 관련 제약 —
crd_tp가 붙은 물량입니다.
매도 수량을 rmnd_qty로 계산하면 주문이 거부됩니다.
그리고 이 거부는 봇 입장에서 가장 나쁜 시점에 납니다 —
손절 조건이 걸려 급히 팔아야 할 때입니다.
매도 가능 수량은 trde_able_qty를 기준으로 하고,
여기에 더해 지금 미체결로 걸어 둔 매도 주문 수량까지 빼야 중복 주문을 막습니다.
def sellable(row, pending_sell_qty=0):
"""실제로 지금 낼 수 있는 매도 수량"""
return max(0, i(row["trde_able_qty"]) - pending_sell_qty)
pending_sell_qty는 ka10075 미체결 조회에서
같은 종목의 매도 주문 oso_qty를 합산해 얻습니다.
잔고와 미체결을 같이 읽어야 비로소 "낼 수 있는 수량"이 나옵니다.
둘 중 하나만 보는 봇은 반드시 언젠가 중복 주문을 냅니다.
6. 함정 ④ dmst_stex_tp는 KRX / NXT 문자열이다
같은 /api/dostk/acnt 아래에 있는 다른 TR들은 거래소를
stex_tp라는 이름의 숫자 문자열(0 통합 / 1 KRX / 2 NXT)로 받습니다.
그런데 kt00018만 dmst_stex_tp라는 다른 이름이고
값도 "KRX" · "NXT" 같은 문자열입니다.
# 다른 TR과 헷갈리기 쉬운 지점
ka10075_body = {"all_stk_tp": "0", "trde_tp": "0", "stex_tp": "0"} # 숫자
kt00018_body = {"qry_tp": "1", "dmst_stex_tp": "KRX"} # 문자열
NXT 출범 이후 같은 종목이 두 시장에서 거래되기 때문에 이 구분이 생겼습니다. 어느 쪽 기준으로 평가된 잔고를 받을지가 여기서 갈리므로, 봇의 주문 라우팅과 잔고 조회의 기준을 같은 값으로 맞춰 두십시오. 배경은 NXT·KRX 주문 라우팅에 정리돼 있습니다.
2026-10-29 18시부터 kt00018은 초당 3회다
키움이 2026-10-27 공지로 kt00018의 유량제어를 초당 5회에서 초당 3회로 낮췄습니다. 적용은 10월 29일 오후 6시 이후이고, 다른 TR의 기준은 그대로입니다.
초과하면 유량제어 오류로 조회가 제한될 수 있다는 것이 공지 내용입니다. 공통 호출 함수 간격을 초당 5회(0.2초)에 맞춰 두었다면 이 TR만 따로 늦춰야 합니다.
cont-yn·next-key로 넘기는 연속조회도 호출이므로 페이지 사이에 같은 간격을 둡니다.
import time
KT00018_MIN_GAP = 0.34 # 초당 3회 (키움 공지 2026-10-27)
_last_kt00018 = 0.0
def wait_kt00018():
global _last_kt00018
gap = time.monotonic() - _last_kt00018
if gap < KT00018_MIN_GAP:
time.sleep(KT00018_MIN_GAP - gap)
_last_kt00018 = time.monotonic()
이 함수는 프로세스 하나 안에서만 간격을 지킵니다. 잔고는 봇 시작 때 한 번 전체를 읽고, 장 시작 전 대조는 장전 점검 5가지처럼 하루 일과에 넣으면 호출을 크게 늘리지 않아도 됩니다.
7. 포지션 구조체로 정규화하기
지금까지의 함정을 한 곳에 모으면, 잔고를 읽는 계층은 이렇게 정리됩니다. 이 함수 바깥에서는 문자열도, 접두어도, 0-padding도 보이지 않아야 합니다.
def positions(stex="KRX"):
res = balance(qry_tp="1", stex=stex)
rows = res.get("acnt_evlt_remn_indv_tot", []) # ← 리스트 이름 주의
out = []
for r in rows:
code, kind = split_code(r["stk_cd"])
out.append({
"code": code, # '005930'
"kind": kind, # 'STOCK'
"name": r["stk_nm"],
"qty": i(r["rmnd_qty"]), # 보유수량
"sellable": i(r["trde_able_qty"]), # 매매가능수량
"avg": i(r["pur_pric"]), # 매입가
"last": i(r["cur_prc"]), # 현재가
"pl": i(r["evltv_prft"]), # 평가손익
"pl_rate": f(r["prft_rt"]), # 수익률 %
})
summary = {
"buy": i(res["tot_pur_amt"]),
"eval": i(res["tot_evlt_amt"]),
"pl": i(res["tot_evlt_pl"]),
"rate": f(res["tot_prft_rt"]),
}
return summary, out
보유 종목이 많다면 연속조회를 잊지 마십시오.
응답 헤더 cont-yn이 Y면 목록이 잘린 것이고
next-key로 이어 받아야 합니다.
이걸 빼먹으면 앞부분 종목만 보고 "나머지는 없다"고 판단합니다.
구현 패턴은 키움 REST API 연속조회 — cont-yn·next-key에 있습니다.
한국투자증권 KIS를 함께 쓰신다면 대응 TR은 잔고조회이고 항목명 체계가 전혀 다릅니다. KIS 잔고조회와 연속조회 처리와 비교해 보면, 두 증권사를 하나의 포지션 구조체로 정규화하는 어댑터가 왜 필요한지가 분명해집니다.
자주 묻는 것
qry_tp 1(합산)과 2(개별)는 뭐가 다른가요?
같은 종목을 여러 번에 걸쳐 매수했을 때 한 줄로 합쳐서 볼지, 나누어 볼지의 차이입니다.
봇에서 포지션 수량과 평균단가만 필요하다면 1(합산)이 다루기 쉽습니다.
매입 시점별로 구분해 관리하는 전략이라면 2(개별)를 씁니다.
잔고에 현금(예수금)은 왜 안 보이나요?
kt00018은 평가잔고가 주제입니다.
최상위의 prsm_dpst_aset_amt(추정예탁자산)로 대략을 볼 수 있지만,
주문 가능 금액을 정확히 알아야 한다면 예수금 계열 TR을 따로 호출하는 편이 맞습니다.
"평가금액이 있으니 그만큼 살 수 있다"고 계산하면 매수 주문이 거부됩니다.
kt00018은 초당 몇 번까지 부를 수 있나요?
키움 공지(2026-10-27)에 따라 2026-10-29 오후 6시 이후 초당 3회입니다. 이전 기준은 초당 5회였고 다른 TR의 기준은 바뀌지 않았습니다. 연속조회 페이지도 호출에 포함해 간격을 두십시오.
이 값들을 그대로 믿어도 되나요?
이 글의 API ID·URL·요청/응답 항목명과 접두어·자릿수 규칙은 2026-08-20 시점 키움증권 REST API 공식 명세에서 확인한 것이고, 코드 안의 값은 이해를 돕기 위한 예시입니다. 증권사 명세는 예고 없이 바뀌므로 운영에 넣기 전 개발자 포털의 현재 명세로 대조하시고, 호출 빈도는 요청 개수 제한을 감안해 설계하시기 바랍니다. 유량제어 값은 2026-11-04에 키움 공지로 다시 확인했습니다.
계좌 상태를 정확히 읽는 봇이 필요하다면
잔고·미체결·예수금을 하나의 상태로 묶어 실제로 돌아가는 자동매매 프로그램으로 만들어 드립니다. 24시간 빠른 답변 가능합니다.
무료 상담 시작하기