3 / 12 · AUTO TRADING WIKI

한국투자증권 API 인증과 현재가 조회

한국투자증권 Open API의 서버·키 환경을 맞추고 인증과 현재가 한 건 조회를 확인합니다. 준비물, 실행 순서, 정상 출력과 오류 대응을 안내합니다.

발행
수정
문서 검토
2026-10-28
본문 버전
1.2.0-web.20261028

한국투자증권 Open API와 연결할 때 첫 결과는 주문 체결이 아니라 인증 성공과 시세 한 건의 조회다. 이 장에서는 개인 설정을 코드에서 분리하고, 인증과 정해진 시세 조회만 수행하는 클라이언트를 만든다.

앞 장의 .venv와 패키지가 준비되어 있어야 한다. 계좌 개설, Open API 서비스 신청, 모의투자 이용 조건은 한국투자증권의 현재 안내에서 확인한다. 문서에 나온 키·계좌 정보는 모두 자리표시자이며 실제 값을 채팅이나 Git에 붙여 넣지 않는다.

인증과 현재가 조회까지 가는 순서

처음 연결한다면 아래 세 단계를 차례로 진행한다. 이미 설정을 마쳤다면 한 종목 조회부터 실행하고, 실패한 단계가 있으면 오류 대응으로 이동한다.

  1. 프로젝트 Python을 확인한다. 앞 장의 환경 점검으로 같은 .venv에서 requests와 dotenv를 import할 수 있는지 확인한다.
  2. 조회 도구를 작성한다. 개인 설정과 인증·조회 코드를 만든다. 선택한 서버와 키의 환경이 같고 문법 검사에 통과하면 다음 단계로 간다.
  3. 현재가 한 건을 확인한다. 프로젝트 루트에서 4절의 명령으로 src.check_kis를 실행한다. 환경·종목코드·양의 가격과 quotation OK가 표시되면 이번 장의 조회 확인이 끝난다.

이 장에서 만들 파일은 config/example.env, 개인용 config/.env, src/kis_readonly.py, src/check_kis.py다. 먼저 아래 본문을 따라 파일을 작성한 뒤 4절의 실행 명령을 사용한다. .venv가 없다면 Python 환경 준비로 돌아간다.

1. 서버 환경과 프로그램 실행 모드 구분하기

한국투자증권은 실전과 모의투자 환경을 구분한다. 공식 예제도 각 환경의 앱 키와 앱 시크릿을 따로 선택한다. 여기서는 서버 선택을 KIS_ENV, 프로젝트의 동작 범위를 RUN_MODE로 나눈다. 한국투자증권 공식 저장소

설정 이번 장의 값 의미
KIS_ENV mock 증권사 모의투자 서버와 그 서버의 인증 정보를 사용한다
KIS_ENV live 증권사 실전 서버와 그 서버의 인증 정보를 사용한다
RUN_MODE read_only 프로젝트가 인증과 조회만 수행한다

mock은 증권사 서버 환경이다. 뒤에서 만드는 paper는 우리 프로그램이 가상 주문을 장부에 기록하는 모드다. 둘은 같은 개념이 아니다. 모의투자 서버를 사용하더라도 주문 API를 호출하면 증권사 모의 주문이 발생할 수 있으므로, 이번 클라이언트에는 주문 메서드를 만들지 않는다.

실전 서버로 시세를 조회할 수 있다는 사실과 실전 주문을 허용한다는 것은 별개다. 이번 장의 예제는 서버 선택과 무관하게 인증 POST와 지정한 시세 GET만 제공한다.

2. 개인 설정 파일 만들기

공유용 config/example.env에는 다음 항목만 둔다.

RUN_MODE=read_only
KIS_ENV=mock
KIS_APP_KEY=REPLACE_WITH_YOUR_KEY
KIS_APP_SECRET=REPLACE_WITH_YOUR_SECRET

이 파일을 로컬의 config/.env로 복사하고, 자신이 발급받은 해당 환경의 값을 입력한다. 현재가 조회에는 계좌번호를 보내지 않으므로 이 장에서 계좌 정보를 추가할 필요가 없다.

if (-not (Test-Path -LiteralPath 'config/.env')) {
    Copy-Item -LiteralPath 'config/example.env' -Destination 'config/.env'
}

이미 config/.env가 있다면 복사로 덮어쓰지 말고 필요한 항목만 보완한다. 이 책은 dotenv_values로 지정 파일을 직접 읽는다. 운영체제 환경변수가 우연히 같은 이름으로 존재해도 자동으로 섞지 않는다. python-dotenv 공식 문서

Git의 제외 대상에 config/.env가 있는지 확인한다. 앱 시크릿을 잘못 공개했다면 파일에서 지우는 것만으로 끝내지 말고 발급처에서 폐기·재발급 절차를 진행한다. 토큰도 인증 수단이므로 화면이나 로그에 원문을 출력하지 않는다.

3. 인증과 시세 조회 코드 작성하기

src/kis_readonly.py에 다음 코드를 저장한다. 종목코드는 ASCII 숫자와 영문 대문자를 조합한 6자리 문자열로 받는다. 005930의 앞자리 0을 보존하고 0126Z0 같은 영문 포함 코드도 허용한다. 소문자나 다른 문자 체계의 숫자를 임의 변환하지 않는다. 예제는 한 프로세스 안에서 발급받은 토큰을 재사용한다. 호출할 때마다 새 토큰을 요청하지 않도록 클라이언트도 한 번 만들어 재사용한다.

인증 경로와 서버 주소는 공식 인증 예제를 확인했다. 토큰의 발급 제한·만료 정책은 바뀔 수 있으므로 반복 운영에 들어가기 전에는 공식 안내에 맞춰 만료 시각 관리와 안전한 캐시를 추가한다.

from pathlib import Path
import re

import requests
from dotenv import dotenv_values

ROOT = Path(__file__).resolve().parents[1]
SERVERS = {
    "mock": "https://openapivts.koreainvestment.com:29443",
    "live": "https://openapi.koreainvestment.com:9443",
}
QUOTATIONS = {
    "price": (
        "/uapi/domestic-stock/v1/quotations/inquire-price",
        "FHKST01010100",
    ),
    "daily": (
        "/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice",
        "FHKST03010100",
    ),
}


def load_config():
    path = ROOT / "config" / ".env"
    if not path.is_file():
        raise FileNotFoundError("config/.env is missing")
    return dict(dotenv_values(path, encoding="utf-8-sig"))


class ReadOnlyKis:
    def __init__(self, config):
        mode = config.get("RUN_MODE", "read_only")
        if mode not in {"read_only", "paper"}:
            raise ValueError("This client is for read_only or local paper mode")
        env = config.get("KIS_ENV", "mock")
        if env not in SERVERS:
            raise ValueError("KIS_ENV must be mock or live")
        self.base_url = SERVERS[env]
        self.key = config.get("KIS_APP_KEY") or ""
        self.secret = config.get("KIS_APP_SECRET") or ""
        if any(not v or v.startswith("REPLACE_") for v in (self.key, self.secret)):
            raise ValueError("KIS credentials are not configured")
        self.session = requests.Session()
        self.token = None

    @staticmethod
    def _json(response):
        if response.status_code != 200:
            raise RuntimeError(f"KIS HTTP error: {response.status_code}")
        try:
            body = response.json()
        except ValueError as exc:
            raise RuntimeError("KIS returned non-JSON content") from exc
        if not isinstance(body, dict):
            raise RuntimeError("KIS returned an unexpected JSON structure")
        return body

    def authenticate(self):
        if self.token:
            return
        response = self.session.post(
            self.base_url + "/oauth2/tokenP",
            json={
                "grant_type": "client_credentials",
                "appkey": self.key,
                "appsecret": self.secret,
            },
            timeout=(5, 20),
        )
        body = self._json(response)
        token = body.get("access_token")
        if not isinstance(token, str) or not token:
            raise RuntimeError("KIS authentication failed; check the API portal")
        self.token = token

    def quotations(self, kind, params):
        if kind not in QUOTATIONS:
            raise ValueError("Unsupported quotation kind")
        self.authenticate()
        path, tr_id = QUOTATIONS[kind]
        response = self.session.get(
            self.base_url + path,
            headers={
                "authorization": f"Bearer {self.token}",
                "appkey": self.key,
                "appsecret": self.secret,
                "tr_id": tr_id,
                "custtype": "P",
            },
            params=params,
            timeout=(5, 20),
        )
        body = self._json(response)
        if body.get("rt_cd") != "0":
            code = str(body.get("msg_cd", "unknown"))[:40]
            raise RuntimeError(f"KIS quotation rejected: {code}")
        return body

    def current_price(self, symbol):
        if not re.fullmatch(r"[A-Z0-9]{6}", symbol):
            raise ValueError("A six-character uppercase ASCII stock symbol is required")
        body = self.quotations("price", {
            "FID_COND_MRKT_DIV_CODE": "J",
            "FID_INPUT_ISCD": symbol,
        })
        output = body.get("output")
        if not isinstance(output, dict):
            raise RuntimeError("Price output is missing")
        price = output.get("stck_prpr")
        if not isinstance(price, str) or not re.fullmatch(r"[0-9]+", price) or int(price) <= 0:
            raise RuntimeError("Price must be a positive integer")
        return output

    def close(self):
        self.session.close()

RUN_MODE=paper 허용은 뒤의 로컬 페이퍼 실행에서 같은 시세 조회기를 재사용하기 위한 것이다. 이 허용이 주문 기능을 추가하지는 않는다. URL을 외부 입력으로 받지 않고 QUOTATIONS의 두 경로만 선택하므로 코드의 동작 범위를 읽고 확인할 수 있다.

현재가 경로·거래 ID·필수 파라미터는 공식 현재가 조회 예제를 기준으로 했다. 이 예제의 J는 KRX 시장 구분이다. 다른 시장 구분을 사용할 때는 데이터의 시장 기준까지 함께 바꿔야 한다.

4. 한 종목만 조회하기

공식 예제에서 사용하는 005930은 종목코드 형식을 확인하기 위한 입력이다. 특정 종목에 대한 투자 제안이 아니다. src/check_kis.py를 다음처럼 작성한다.

from src.kis_readonly import ReadOnlyKis, load_config


def main():
    config = load_config()
    client = ReadOnlyKis(config)
    try:
        output = client.current_price("005930")
        price = int(output["stck_prpr"])
        print(f"env={config.get('KIS_ENV', 'mock')} symbol=005930 price={price}")
        print("quotation OK; credentials and token omitted")
    finally:
        client.close()


if __name__ == "__main__":
    main()

프로젝트 루트에서 먼저 문법을 확인하고, 설정을 준비한 뒤 조회를 실행한다.

.\.venv\Scripts\python.exe -m py_compile src/kis_readonly.py src/check_kis.py
.\.venv\Scripts\python.exe -m src.check_kis

실제 출력의 가격은 조회 시점에 따라 달라진다. 성공 기준은 양의 가격이 파싱되고, 환경과 종목코드가 표시되며, 키·토큰·요청 헤더가 출력되지 않는 것이다. HTTP 응답이 200이어도 업무 결과 코드가 실패일 수 있으므로 코드에서 rt_cd를 별도로 검사한다.

출력 형식은 다음과 같다. <조회한 양의 정수>는 실제로 표시될 가격을 설명하는 자리표시자이며 실행 결과나 현재 시세가 아니다.

env=mock symbol=005930 price=<조회한 양의 정수>
quotation OK; credentials and token omitted

문법 검사만 통과한 경우에는 인증 성공으로 기록하지 않는다. quotation OK가 표시되지 않으면 실패한 단계와 오류 코드를 확인하고, 토큰이나 전체 응답을 복사해 공유하지 않는다.

이 장에서는 조회 실패를 자동으로 무한 재시도하지 않는다. 네트워크 타임아웃은 요청이 끝나지 않은 상황, 인증 실패는 자격 증명 문제, 업무 오류는 서버가 요청을 거절한 상황으로 구분한다. Requests의 타임아웃과 예외 의미는 공식 Quickstart에서 확인할 수 있다.

5. Codex에 구현과 검증 범위 전달하기

auto-trading-lab에 한국투자증권 조회 클라이언트를 만들어줘.
config/.env를 직접 읽고 KIS_ENV=mock|live와 RUN_MODE를 구분해줘.
인증 POST, 현재가 GET, 기간별 일봉 GET만 허용하고 임의 URL 입력은 받지 마.
키·시크릿·토큰·전체 헤더와 전체 응답은 로그에 출력하지 마.
공식 예제에서 현재 파라미터와 거래 ID를 확인해 출처를 기록해줘.
먼저 가짜 응답으로 성공, HTTP 오류, rt_cd 실패, 빈 output을 검사해줘.
실제 접속을 실행했는지, 문법·가짜 응답만 검사했는지 구분해서 보고해줘.
주문 함수, 잔고 변경, 증권사 모의 주문도 이번 작업에 추가하지 마.

가짜 응답 검사는 실제 키 없이도 오류 처리와 파싱을 확인하는 방법이다. 인증이 실제로 성공했다는 증거는 자신의 계정으로 실행한 조회 결과여야 한다.

6. 오류 대응과 완료 기준

증상 확인 순서
설정 파일을 찾지 못함 config/.env의 위치와 .env.txt로 저장되지 않았는지 확인한다
인증 실패 서비스 신청 상태 → KIS_ENV → 같은 환경의 앱 키·시크릿 순서로 본다
HTTP·업무 오류 시각·환경·요청 종류·오류 코드만 남기고 공식 오류 안내를 확인한다
잦은 토큰 요청 거절 실행을 멈추고 토큰 발급 정책과 프로세스 재시작 빈도를 확인한다
조회는 성공했지만 값이 비어 있음 종목코드·시장 구분·해당 환경의 지원 API를 확인한다

실패 원인을 확인하기 전에 모의 설정을 실전으로 바꾸거나 요청 수를 늘리지 않는다. 조회 가능 범위와 한도는 한국투자증권 API 포털의 현재 안내를 따른다.

완료 기록에는 사용 환경, 시세 조회 종류, 결과 코드, 확인 일시를 남긴다. 키와 토큰을 기록하지 않고도 인증과 조회가 어디까지 성공했는지 설명할 수 있어야 한다. 다음 장에서는 같은 클라이언트로 일봉 데이터를 수집한다.

시세 한 건을 읽었다면 코스피200 데이터 수집으로 이어간다. 여러 프로그램이 인증을 반복하는 문제가 생겼을 때는 토큰을 여러 실행기에서 공유한 개발 사례를 참고한다. 첫 조회를 마치기 전에 토큰 공유 구조부터 추가할 필요는 없다.

학습의 다음 단계를 남겨 두세요

공유 링크를 복사해 이어서 읽을 수 있습니다.