AlgoLab Blog · KIS API 트러블슈팅 · 2026

KIS 주문체결조회 — TTTC8001R 대신 TTTC0081R

한국투자증권 · 주문 2026-08-15 · 약 8분 읽기 · 알고랩 AlgoLab
한 줄 요약 주문이 체결됐는지 확인하는 API는 주식일별주문체결조회 (/uapi/domestic-stock/v1/trading/inquire-daily-ccld)이고, 한국투자증권 공식 예제 코드가 쓰는 tr_idTTTC0081R입니다. 검색하면 널리 나오는 TTTC8001R은 그 이전부터 인용돼 온 값입니다. 같은 예제에서 3개월 이전 조회는 CTSC9215R, 모의계좌는 VTTC0081R·VTSC9215R로 갈라집니다. 한 번에 오는 건수는 실전 100건·모의 15건이라 그 이상은 CTX_AREA_NK100 연속조회로 받아야 하고, 미체결만 보려면 CCLD_DVSN02를 넣습니다.

KIS API 발급 가이드대로 앱키를 받고 TTTC0802U로 첫 매수 주문을 넣으면 응답에 ODNO(주문번호)가 돌아옵니다. 여기서 많은 사람이 "응답이 200이니 체결된 것"으로 처리합니다. 아닙니다. 그 응답은 "주문이 접수됐다"까지입니다. 지정가로 걸어 둔 주문은 하루 종일 미체결로 남을 수 있고, 절반만 체결될 수도 있습니다.

이 글은 그 다음을 확인하는 API 하나만 다룹니다. KIS 에러코드 목록에 안 나오는 문제인데, 호출은 성공하는데 tr_id가 옛 값이면 조회 자체가 서지 않거나 결과가 비어 오기 때문입니다.

이 글의 순서

  1. tr_id 4종 — 실전·모의 × 3개월 안팎
  2. 요청 파라미터 — 필수 7개와 선택 5개
  3. 실물 코드 — 오늘 미체결만 뽑기
  4. 100건에서 끊길 때 — 연속조회
  5. 공식 예제 코드 안의 함정 2가지
  6. 폴링과 웹소켓 체결통보를 같이 쓰는 법
  7. 자주 묻는 질문

1. tr_id 4종 — 실전·모의 × 3개월 안팎

이 API의 첫 관문은 파라미터가 아니라 tr_id입니다. 계좌 종류(실전·모의)와 조회 기간(3개월 이내·이전)의 조합으로 네 개가 존재합니다. 한국투자증권이 GitHub에 공개한 공식 예제 저장소 open-trading-apiexamples_llm/domestic_stock/inquire_daily_ccld 파일(상단 작성일 20250601) 기준으로 정리하면 이렇습니다.

계좌조회 기간tr_id비고
실전3개월 이내TTTC0081R가장 많이 쓰는 값
실전3개월 이전CTSC9215R장 종료 후 조회 권장
모의3개월 이내VTTC0081R한 번에 15건
모의3개월 이전VTSC9215R

검색으로 나오는 코드와 값이 다릅니다. 블로그·위키독스·오래된 샘플에서는 실전 3개월 이내가 TTTC8001R, 3개월 이전이 CTSC9115R로 인용된 경우가 많습니다. 더 헷갈리는 것은 위 공식 예제 파일 자체가 주석에서는 CTSC9115R을 언급하면서 실제 코드는 CTSC9215R을 쓴다는 점입니다. 공식 자료 안에서도 표기가 엇갈리므로 tr_id를 소스에 상수로 박지 말고 설정 파일로 빼 두고, 최종 값은 KIS Developers 포털의 현재 명세로 확인하시기 바랍니다.

"3개월"이라는 경계도 그냥 관례가 아닙니다. 같은 URL, 같은 파라미터인데 tr_id만 바뀌면 다른 저장소를 조회하는 구조입니다. 그래서 봇이 INQR_STRT_DT를 4개월 전으로 잡아 놓고 TTTC0081R로 부르면 기대한 데이터가 안 옵니다. 조회 시작일이 오늘 기준 3개월을 넘는지 계산해 tr_id를 자동으로 갈아 끼우는 분기를 만들어 두는 편이 낫습니다.

공식 예제는 3개월 이전 조회에 대해 이렇게 안내합니다. 장중에는 거래량이 많아 DB 응답이 밀릴 수 있으니 ① 가급적 장 종료 이후(15:30 이후)에 조회하고 ② 조회 시작일과 종료일 간격을 짧게 나누라는 것입니다. 과거 체결 내역을 한 번에 1년치 긁으려는 정산 스크립트가 있다면 이 안내를 먼저 반영하세요.

2. 요청 파라미터 — 필수 7개와 선택 5개

엔드포인트는 /uapi/domestic-stock/v1/trading/inquire-daily-ccld 하나입니다. CANOACNT_PRDT_CD계좌번호 앞 8자리와 뒤 2자리를 나눠 넣는 규칙을 그대로 따릅니다.

파라미터필수
CANO필수종합계좌번호 8자리
ACNT_PRDT_CD필수계좌상품코드 2자리
INQR_STRT_DT필수조회 시작일 YYYYMMDD
INQR_END_DT필수조회 종료일 YYYYMMDD
SLL_BUY_DVSN_CD필수00 전체 / 01 매도 / 02 매수
CCLD_DVSN필수00 전체 / 01 체결 / 02 미체결
INQR_DVSN필수00 역순 / 01 정순
INQR_DVSN_3필수00 전체 / 01 현금 / 02 신용 / 03 담보 / 04 대주 / 05 대여 …
PDNO선택종목코드. 비우면 전 종목
ODNO선택주문번호. 특정 주문 1건만 추적할 때
ORD_GNO_BRNO선택주문채번지점번호
INQR_DVSN_1선택공란 전체 / 1 ELW / 2 프리보드
EXCG_ID_DVSN_CD선택KRX / NXT / SOR / ALL
CTX_AREA_FK100
CTX_AREA_NK100
연속조회최초 호출은 공란, 이후 직전 응답값

맨 아래 EXCG_ID_DVSN_CD는 최근에 의미가 커진 항목입니다. 대체거래소가 열리면서 같은 종목이라도 주문이 어디로 나갔는지가 갈리기 때문에, KRX·NXT·SOR 주문 라우팅을 쓰는 봇이라면 조회에서도 이 값을 명시하는 편이 결과 해석이 쉽습니다. 구분 없이 전부 보고 싶다면 ALL입니다.

3. 실물 코드 — 오늘 미체결만 뽑기

가장 자주 쓰는 형태는 "오늘 낸 주문 중 아직 안 붙은 것"을 뽑는 것입니다. CCLD_DVSN02로 두면 됩니다. 아래는 requests만 쓰는 최소 코드입니다. ACCESS_TOKEN은 발급받아 캐시해 둔 값을 씁니다.

import requests, datetime

BASE = "https://openapi.koreainvestment.com:9443"
PATH = "/uapi/domestic-stock/v1/trading/inquire-daily-ccld"
today = datetime.date.today().strftime("%Y%m%d")

headers = {
    "content-type": "application/json; charset=utf-8",
    "authorization": f"Bearer {ACCESS_TOKEN}",
    "appkey":        APP_KEY,
    "appsecret":     APP_SECRET,
    "tr_id":         "TTTC0081R",   # 실전 · 3개월 이내
    "custtype":      "P",           # 개인
    "tr_cont":       "",            # 최초 조회는 공란
}

params = {
    "CANO": CANO, "ACNT_PRDT_CD": ACNT_PRDT_CD,
    "INQR_STRT_DT": today, "INQR_END_DT": today,
    "SLL_BUY_DVSN_CD": "00",   # 매수·매도 전체
    "CCLD_DVSN": "02",         # 02 = 미체결만
    "INQR_DVSN": "00",         # 역순(최신 주문부터)
    "INQR_DVSN_3": "00",
    "PDNO": "", "ODNO": "", "ORD_GNO_BRNO": "", "INQR_DVSN_1": "",
    "EXCG_ID_DVSN_CD": "KRX",
    "CTX_AREA_FK100": "", "CTX_AREA_NK100": "",
}

r = requests.get(BASE + PATH, headers=headers, params=params, timeout=5)
body = r.json()

for row in body["output1"]:
    print(row["odno"], row["prdt_name"],
          "주문", row["ord_qty"], "체결", row["tot_ccld_qty"],
          "잔여", row["rmn_qty"], "취소", row["cncl_yn"])

output1은 주문 한 건씩의 배열이고, output2는 합계 한 덩어리입니다. 봇에서 실제로 보게 되는 필드는 이 정도입니다.

필드봇에서 쓰는 법
odno주문번호주문 시 받은 ODNO와 매칭
orgn_odno원주문번호정정·취소 주문의 원본 추적
ord_qty주문수량기준값
tot_ccld_qty총체결수량0이면 전량 미체결
rmn_qty잔여수량부분체결 판정의 핵심
rjct_qty거부수량0이 아니면 주문이 거부된 것
cncl_yn취소여부취소 처리된 주문 제외
ord_tmd주문시각같은 종목 다중 주문 구분
ord_dvsn_name주문구분명지정가·시장가 표시

rmn_qty를 보세요. CCLD_DVSN=02로 걸러도 부분체결 주문은 미체결에도 잡힙니다. 1,000주 주문에 300주가 붙었으면 tot_ccld_qty=300, rmn_qty=700입니다. 봇이 "미체결이니까 아직 안 샀다"로 판단해 같은 수량을 다시 주문하면 의도치 않게 1,300주를 들고 있게 됩니다. 실제 포지션은 잔고조회로 한 번 더 대조하세요.

주문 TTTC0802U → ODNO 수신 접수 실시간 체결통보 H0STCNI0 · 빠름/유실 주문체결조회 TTTC0081R · 느림/확정 둘을 대조해 상태 확정 rmn_qty · tot_ccld_qty
주문 응답의 ODNO는 시작점일 뿐 — 상태는 두 경로를 대조해 확정한다

4. 100건에서 끊길 때 — 연속조회

공식 예제 설명에 따르면 실전계좌는 한 번의 호출에 최대 100건, 모의계좌는 최대 15건입니다. 모의로 테스트하다가 15건에서 잘리는 것을 버그로 오해하는 경우가 많은데 정상 동작입니다. 나머지는 잔고조회와 동일한 연속조회 규약으로 받습니다.

# 응답 헤더 tr_cont 가 M 또는 F → 다음 페이지 있음
rows, fk, nk, cont = [], "", "", ""
while True:
    headers["tr_cont"] = cont
    params["CTX_AREA_FK100"], params["CTX_AREA_NK100"] = fk, nk
    r = requests.get(BASE + PATH, headers=headers, params=params, timeout=5)
    b = r.json()
    if b.get("rt_cd") != "0":
        raise RuntimeError(b.get("msg_cd"), b.get("msg1"))
    rows += b["output1"]

    if r.headers.get("tr_cont") not in ("M", "F"):
        break
    fk, nk, cont = b["ctx_area_fk100"], b["ctx_area_nk100"], "N"
    time.sleep(0.3)          # 초당 호출 제한 회피
print(len(rows), "건")

tr_contM·F면 다음 페이지가 있고, D·E면 마지막입니다. 다음 호출의 요청 헤더 tr_contN으로 바꿔야 한다는 점을 자주 빠뜨립니다. time.sleep을 뺐다가 EGW00201을 만나는 것도 흔한 순서인데, 그 에러는 초당 거래건수 초과 해결에 따로 정리해 뒀습니다.

5. 공식 예제 코드 안의 함정 2가지

이 API는 공식 샘플을 그대로 가져다 쓰는 경우가 많은데, 2025-06-01자 inquire_daily_ccld.py에는 그대로 복사하면 곤란한 지점이 두 곳 있습니다. 비난하려는 게 아니라, 붙여넣고 나서 원인을 못 찾는 일이 실제로 생기기 때문입니다.

① 연속조회 재귀 호출의 인자 순서

함수 시그니처는 … sll_buy_dvsn_cd, ccld_dvsn, inqr_dvsn, inqr_dvsn_3, pdno … 순인데, 다음 페이지를 부르는 재귀 호출은 … sll_buy_dvsn_cd, pdno, ccld_dvsn, inqr_dvsn, inqr_dvsn_3 … 순으로 넘깁니다. 키워드 인자가 아니라 위치 인자로 넘기기 때문에 2페이지부터 pdnoccld_dvsn 자리로 들어갑니다. 1페이지는 멀쩡한데 2페이지부터 결과가 이상해지는 전형적인 증상입니다. 재귀 호출을 전부 키워드 인자로 바꾸면 끝납니다.

② 컬럼 한글 매핑표의 라벨 밀림

같은 폴더의 chk_inquire_daily_ccld.py에 있는 COLUMN_MAPPING 끝부분은 tot_ccld_amt를 "매입평균가격", prsm_tlex_smtl을 "총체결금액", pchs_avg_pric을 "추정제비용합계"로 붙여 놓았습니다. 이름과 라벨이 한 칸씩 밀려 있습니다. 출력만 보고 금액을 읽으면 총체결금액과 매입평균가를 뒤집어 해석하게 됩니다. 표시용 한글 라벨을 믿지 말고 영문 필드명으로 접근하세요.

정산·세금 계산에 이 값을 쓰는 경우 특히 주의하세요. 금액 필드를 한 칸 밀려 읽은 상태로 몇 달치를 집계하면 나중에 되돌리기가 번거롭습니다. 숫자가 맞는지는 증권사 HTS의 거래내역과 하루치라도 눈으로 대조해 두는 것이 가장 빠릅니다.

6. 폴링과 웹소켓 체결통보를 같이 쓰는 법

체결 확인 방법은 두 가지이고, 둘 중 하나만 쓰면 각각 다른 방식으로 틀립니다.

실무에서 쓰는 조합은 단순합니다. 웹소켓을 주 경로로 두고, ① 재접속 직후 ② 주문 후 일정 시간이 지나도 통보가 없을 때 ③ 장 마감 후 하루 마감 처리, 이 세 시점에만 조회를 부릅니다. 매초 폴링하면 호출 제한에 걸리고, 안 부르면 유실을 못 잡습니다.

취소·정정 대상만 찾는 거라면 이 API가 아닙니다. 아직 정정이나 취소가 가능한 주문만 뽑는 것이 목적이면 정정취소가능주문조회(TTTC8036R)가 더 직접적입니다. 주식일별주문체결조회는 기간 단위 기록 조회이고, 그쪽은 지금 손댈 수 있는 주문 목록입니다. 실제 취소·정정 요청 방법은 아래 관련 글에 따로 정리돼 있습니다.

체결 상태 관리까지 붙인 봇이 필요하다면

주문 → 체결 확인 → 잔고 반영 → 재시도까지 끊기지 않고 도는 상태로 만들어 드립니다. 어떤 증권사, 어떤 전략인지만 알려 주시면 됩니다.

무료로 상담해 보기 →

7. 자주 묻는 질문

모의투자에서도 같은 코드가 도나요?

도메인과 tr_id만 바꾸면 됩니다. 모의는 https://openapivts.koreainvestment.com:29443이고 tr_idVTTC0081R입니다. 건수 상한이 15건이라는 점, 그리고 모의계좌에서는 체결 시뮬레이션이 실전과 다르게 동작할 수 있다는 점만 감안하세요. 실전 전환 시 바꿔야 할 항목은 도메인과 tr_id만이 아니라는 점도 함께 기억해 두세요.

조회는 되는데 output1이 빈 배열로 옵니다

rt_cd0인데 결과가 비었다면 거의 조건 문제입니다. ① INQR_STRT_DT가 휴장일이거나 ② CCLD_DVSN01(체결)인데 아직 체결이 없거나 ③ EXCG_ID_DVSN_CDKRX로 고정했는데 주문이 다른 경로로 나갔거나 ④ tr_id가 기간과 안 맞는 경우입니다. ①은 휴장일 판별을 스케줄러에 붙이면 애초에 안 부르게 됩니다.

주문 직후에 바로 조회하면 나오나요?

보통은 나오지만 0.1초 뒤에 부르는 것을 전제로 로직을 짜지는 마세요. 접수와 기록 사이에는 지연이 있을 수 있고, 그 짧은 창에 비어 있다고 해서 주문이 없는 것이 아닙니다. "조회 결과가 없으면 재주문" 같은 분기는 중복 주문의 가장 흔한 원인입니다. 최소 몇 초의 유예와 ODNO 기준 멱등 처리를 함께 두세요.

이 값들을 그대로 믿어도 되나요?

이 글의 tr_id·건수 상한·파라미터는 2026-08-15 시점에 공개된 한국투자증권 공식 예제 코드를 근거로 정리한 것입니다. 증권사 API 명세는 예고 없이 바뀌고, 앞서 본 것처럼 공식 자료 안에서도 표기가 엇갈리는 경우가 있습니다. 운영에 쓰기 전 KIS Developers 포털의 현재 명세로 반드시 대조하시기 바랍니다.

주문 상태 관리를 맡기고 싶다면

체결 확인·부분체결 처리·재시도까지 실제로 도는 상태로 만들어 드립니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기