AlgoLab Blog · 키움 계좌 실무 · 2026

키움 REST API 실현손익 ka10072·ka10073 함정

키움증권 · 계좌·손익 2026-08-27 · 약 7분 읽기 · 알고랩 AlgoLab
한 줄 요약 키움 REST API의 실현손익은 하나가 아니라 네 개입니다. ka10072(일자별종목별·일자), ka10073(일자별종목별·기간), ka10074(일자별 합계), ka10077(당일 상세)이고 경로는 넷 다 /api/dostk/acnt, 구분은 헤더의 api-id로만 합니다. 가장 흔한 사고는 이름이 거의 같은 ka10072ka10073의 응답 리스트 키가 dt_stk_div_rlzt_pldt_stk_rlzt_pl로 다르다는 것입니다. 파싱 코드를 복사하면 에러 없이 빈 데이터프레임이 나옵니다.

봇이 주문을 내기 시작하면 곧바로 다음 질문이 옵니다. "그래서 얼마 벌었나." 잔고 조회로는 평가손익만 보이고, 판 것까지 포함한 실현손익은 별도 API입니다. 그런데 키움 REST에서 이 영역은 api_id가 네 개로 쪼개져 있고, 이름이 비슷한 것들의 응답 구조가 서로 다릅니다.

이 글은 키움 REST API 자동매매 가이드에서 계좌 영역만 떼어 낸 글입니다. 아래 항목명은 키움증권 공식 저장소(Kiwoom-Securities/Kiwoom-REST-API)의 examples/국내주식/계좌/ 샘플 코드 원문에서 확인했습니다.

이 글에서 다루는 것

  1. 실현손익 api_id 4개 — 무엇을 언제 쓰나
  2. 함정 1 — 리스트 키가 다르다
  3. 함정 2 — ka10072에는 종료일자가 없다
  4. 함정 3 — 손익과 비용이 따로 온다
  5. 함정 4 — 값이 전부 문자열이다
  6. 함정 5 — 연속조회를 안 돌리면 앞부분만 나온다
  7. 봇에 넣는 일일 손익 집계 (전체 코드)

1. 실현손익 api_id 4개 — 무엇을 언제 쓰나

네 개 모두 POST /api/dostk/acnt로 나가고, 요청 헤더 api-id로만 갈립니다. 잔고조회 kt00018과 같은 경로를 쓴다는 뜻입니다.

api-id이름요청 본문응답 리스트 키
ka10072일자별종목별실현손익요청_일자strt_dt, stk_cddt_stk_div_rlzt_pl
ka10073일자별종목별실현손익요청_기간strt_dt, end_dt, stk_cddt_stk_rlzt_pl
ka10074일자별실현손익요청strt_dt, end_dtdt_rlzt_pl + 요약
ka10077당일실현손익상세요청stk_cdtdy_rlzt_pl_dtl + 요약

고르는 기준은 간단합니다.

POST /api/dostk/acnt 경로는 하나 헤더 api-id로 분기 ka10072 하루 · 종목별 dt_stk_div_rlzt_pl ka10073 기간 · 종목별 dt_stk_rlzt_pl ka10074 기간 · 날짜별 합계 dt_rlzt_pl ka10077 당일 · 종목 지정 tdy_rlzt_pl_dtl 이름은 한 글자 차이, 리스트 키는 다름 → 파싱 코드 재사용 금지
같은 경로, 네 개의 api-id, 서로 다른 응답 키

2. 함정 1 — 리스트 키가 다르다

ka10072ka10073은 이름이 일자별종목별실현손익요청_일자일자별종목별실현손익요청_기간으로 끝 두 글자만 다릅니다. 그래서 하나를 먼저 붙이고 다른 하나를 복사해 붙이는 순서가 자연스럽습니다. 여기서 사고가 납니다.

# ka10072 — 공식 예제의 결과 키
TABLE_KEYS = {"dt_stk_div_rlzt_pl": "일자별종목별실현손익"}

# ka10073 — 같은 이름인데 키가 다르다
TABLE_KEYS = {"dt_stk_rlzt_pl": "일자별종목별실현손익"}

dt_stk_div_rlzt_pl vs dt_stk_rlzt_pl — 가운데 div 네 글자가 있고 없고의 차이입니다. response.json().get("dt_stk_rlzt_pl", [])처럼 기본값을 준 코드는 KeyError도 안 나고 빈 리스트를 돌려줍니다. 집계가 0원으로 나오는데 API는 return_code 0으로 정상 응답합니다. 원인을 찾는 데 반나절이 걸리는 유형의 버그입니다.

방어법은 하나입니다. 키를 상수로 박지 말고 api-id와 같이 묶어 두는 것입니다.

LIST_KEY = {
    "ka10072": "dt_stk_div_rlzt_pl",   # 일자
    "ka10073": "dt_stk_rlzt_pl",       # 기간
    "ka10074": "dt_rlzt_pl",
    "ka10077": "tdy_rlzt_pl_dtl",
}

rows = body.get(LIST_KEY[api_id])
if rows is None:
    raise RuntimeError(f"{api_id}: 응답에 {LIST_KEY[api_id]} 키가 없음 — 명세 변경 의심")

.get(key, [])가 아니라 .get(key) 뒤에 None 검사를 두는 것이 핵심입니다. 빈 결과와 키 오타를 구분할 수 있어야 합니다.

3. 함정 2 — ka10072에는 종료일자가 없다

ka10072의 요청 본문은 strt_dtstk_cd 둘뿐입니다. end_dt를 넣어도 받는 자리가 없습니다. 기간으로 뽑으려면 ka10073으로 가야 합니다.

# ka10072 — 시작일자 + 종목코드
{"strt_dt": "20260827", "stk_cd": "005930"}

# ka10073 — 시작일자 + 종료일자 + 종목코드
{"strt_dt": "20260801", "end_dt": "20260827", "stk_cd": "005930"}

# ka10074 — 기간만, 종목 구분 없이 날짜별 합계
{"strt_dt": "20260801", "end_dt": "20260827"}

ka10074는 종목코드를 받지 않는 대신 요약 항목이 함께 옵니다. 총매수금액 tot_buy_amt, 총매도금액 tot_sell_amt, 실현손익 rlzt_pl, 매매수수료 trde_cmsn, 매매세금 trde_tax입니다. 월간 리포트를 만들 때 가장 값싼 호출이 이것입니다. 종목별로 쪼갤 필요 없이 한 번에 총계가 나옵니다.

ka10077(당일실현손익상세)은 반대로 stk_cd가 필수입니다. 전 종목을 한 번에 볼 수 없어서 보유 종목 수만큼 호출해야 합니다. 오늘치 종목별 내역이 목적이라면 시작일자를 오늘로 준 ka10072 한 번이 호출 수가 훨씬 적습니다.

4. 함정 3 — 손익과 비용이 따로 온다

ka10072의 응답 항목은 아래와 같습니다.

항목명의미
stk_cd / stk_nm종목코드 / 종목명
cntr_qty체결량
buy_uv매입단가
cntr_pric체결가
tdy_sel_pl당일매도손익
pl_rt손익율
tdy_trde_cmsn당일매매수수료
tdy_trde_tax당일매매세금
wthd_alowa인출가능금액
loan_dt / crd_tp대출일 / 신용구분

손익 금액 tdy_sel_pl수수료·세금이 별도 항목으로 내려옵니다. ka10073에는 여기에 당일hts매도수수료 tdy_htssel_cmsn이 하나 더 붙습니다. 즉 두 API의 컬럼 집합도 완전히 같지 않습니다.

비용이 별도 항목으로 오는 구조이므로, 봇의 "순손익"을 어떻게 정의할지는 직접 정하고 실계좌로 대조해야 합니다. 소액으로 한 종목만 사고팔아 본 뒤 HTS 화면의 손익과 API 값을 맞춰 보는 것이 가장 확실합니다. 백테스트 수치와 실계좌가 벌어지는 원인 중 큰 몫이 이 비용 처리입니다 — 백테스트 거래비용 반영에서 따로 다뤘습니다.

그리고 이 값들은 거래 집계이지 세무 신고 기준이 아닙니다. 국내주식·해외주식·파생의 과세 방식이 각각 다르고 연도별로 바뀝니다. 자동매매 세금 정리는 개념 정리용이고, 실제 신고는 세무 전문가와 국세청 공식 안내로 확인하십시오.

5. 함정 4 — 값이 전부 문자열이다

키움 REST 응답의 숫자 항목은 문자열로 옵니다. 손익은 음수가 나올 수 있어서 부호가 붙은 문자열이 섞입니다.

{
  "return_code": 0,
  "return_msg": "정상적으로 처리되었습니다",
  "dt_stk_div_rlzt_pl": [
    {
      "stk_cd": "005930",
      "stk_nm": "삼성전자",
      "cntr_qty": "10",
      "buy_uv": "71200",
      "cntr_pric": "72500",
      "tdy_sel_pl": "11420",
      "pl_rt": "1.60",
      "tdy_trde_cmsn": "230",
      "tdy_trde_tax": "1305",
      "wthd_alowa": "1842300",
      "loan_dt": "",
      "crd_tp": "00"
    }
  ]
}

sum(r["tdy_sel_pl"] for r in rows)문자열 이어붙이기가 됩니다. 에러도 안 납니다. 손익 합계가 "11420-3200" 같은 문자열로 남습니다. 정렬도 마찬가지여서 문자열 "9000"이 문자열 "71800"보다 크다고 나옵니다. 읽는 즉시 숫자로 바꾸는 층을 하나 두는 편이 안전합니다.

from decimal import Decimal, InvalidOperation

NUMERIC = ("tdy_sel_pl", "pl_rt", "tdy_trde_cmsn", "tdy_trde_tax",
           "buy_uv", "cntr_pric", "wthd_alowa")

def coerce(row):
    out = dict(row)
    for k in NUMERIC:
        v = (out.get(k) or "").strip()
        try:
            out[k] = Decimal(v) if v else Decimal(0)
        except InvalidOperation:
            out[k] = Decimal(0)      # 빈 값·구분자 섞인 값 방어
    return out

loan_dt처럼 빈 문자열이 정상인 항목도 있습니다. 신용·대출 거래가 아니면 비어 있는 것이 맞으므로, 빈 값을 에러로 처리하면 안 됩니다.

6. 함정 5 — 연속조회를 안 돌리면 앞부분만 나온다

기간을 길게 잡으면 한 번의 응답에 다 안 들어옵니다. 키움 REST는 이때 응답 헤더에 cont-yn: Ynext-key를 내려주고, 다음 요청 헤더에 그 두 값을 실어 보내야 이어지는 페이지가 옵니다.

headers = {
    "authorization": f"Bearer {token}",
    "api-id": "ka10073",
    "cont-yn": cont_yn,      # 첫 요청은 생략, 이후 직전 응답의 값
    "next-key": next_key,
}

공식 예제 코드는 반복 상한을 10페이지로 두고 요청 사이에 0.2초를 쉬는 형태로 작성돼 있습니다. 상한을 두는 이유는 무한 루프 방지이고, 간격을 두는 이유는 호출 한도입니다. 연속조회 헤더를 다루는 방법은 키움 REST 연속조회 cont-yn·next-key에, 한도를 넘겼을 때의 429 처리는 키움 REST 429 대응에 정리했습니다.

7. 봇에 넣는 일일 손익 집계 (전체 코드)

import time
from decimal import Decimal
import requests

BASE = "https://api.kiwoom.com"          # 모의는 mockapi.kiwoom.com
PATH = "/api/dostk/acnt"
LIST_KEY = {"ka10072": "dt_stk_div_rlzt_pl",
            "ka10073": "dt_stk_rlzt_pl",
            "ka10074": "dt_rlzt_pl",
            "ka10077": "tdy_rlzt_pl_dtl"}

def fetch_all(token, api_id, body, max_pages=10, delay=0.2):
    """cont-yn이 Y가 아닐 때까지 이어서 읽는다."""
    key = LIST_KEY[api_id]
    rows, cont_yn, next_key = [], None, None

    for _ in range(max_pages):
        headers = {"authorization": "Bearer " + token,
                   "api-id": api_id,
                   "Content-Type": "application/json;charset=UTF-8"}
        if cont_yn:
            headers["cont-yn"] = cont_yn
            headers["next-key"] = next_key

        r = requests.post(BASE + PATH, json=body, headers=headers, timeout=10)
        r.raise_for_status()
        data = r.json()

        if data.get("return_code") not in (None, 0):
            raise RuntimeError(f"{api_id}: {data.get('return_msg')}")

        page = data.get(key)
        if page is None:                  # 키 오타·명세 변경을 빈 결과와 구분
            raise RuntimeError(f"{api_id}: 응답에 {key} 없음")
        rows.extend(page)

        cont_yn  = r.headers.get("cont-yn")
        next_key = r.headers.get("next-key")
        if cont_yn != "Y":
            break
        time.sleep(delay)

    return rows


def daily_pnl(token, ymd):
    """오늘 하루 종목별 실현손익 합계 — ka10072 한 번으로 끝낸다."""
    rows = fetch_all(token, "ka10072", {"strt_dt": ymd, "stk_cd": ""})

    gross = sum(Decimal(r["tdy_sel_pl"] or 0) for r in rows)
    fee   = sum(Decimal(r["tdy_trde_cmsn"] or 0) for r in rows)
    tax   = sum(Decimal(r["tdy_trde_tax"] or 0) for r in rows)

    return {"date": ymd, "rows": len(rows),
            "sel_pl": gross, "cmsn": fee, "tax": tax}

stk_cd를 빈 문자열로 두면 전 종목으로 도는지, 종목 지정이 필요한지는 계좌 상태와 명세에 따라 달라질 수 있습니다. 운영에 넣기 전 모의 도메인(mockapi.kiwoom.com)에서 먼저 확인하십시오. 실전 전환 절차는 키움 REST 가이드에 정리돼 있습니다.

8. 정리 — 체크리스트

본문의 api-id·요청 본문 항목·응답 리스트 키와 컬럼명은 2026-08-27 확인 기준 키움증권 공식 저장소 Kiwoom-Securities/Kiwoom-REST-APIexamples/국내주식/계좌/ 샘플 코드에서 확인한 값이고, 응답 예시의 숫자는 구조 이해를 돕기 위한 것입니다. 증권사 명세와 호출 한도 정책은 예고 없이 바뀝니다. 운영에 넣기 전 개발자 포털의 현재 명세로 대조하시고, 도메인·api-id·리스트 키는 코드에 흩어 두지 말고 한 곳에 모아 두시기 바랍니다.

손익 집계까지 자동으로 나오는 봇이 필요하다면

주문·체결·잔고에 더해 일일 실현손익 리포트와 비용 반영까지 묶어서 실제로 돌아가는 키움 REST 자동매매 프로그램으로 만들어 드립니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기