예제 소스
한국투자증권 API와 LLM 주식 응용 프로그램 · 파일 19개
main.py
# -*- coding: utf-8 -*-
"""
한국투자증권 Open API(KIS Developers) 연동 예시.
베이스: https://github.com/koreainvestment/open-trading-api 의 examples_user 흐름
(oauth2/tokenP 발급 → domestic-stock inquire-price GET).
환경 변수(.env.local 권장):
APP_KEY, APP_SECRET — KIS 앱키/앱시크릿
KIS_PAPER=1 — 모의투자(선택, 기본은 실전 도메인)
"""
from __future__ import annotations
import json
import os
import sys
from pathlib import Path
from typing import Any
import requests
from dotenv import load_dotenv
# 저장소 샘플과 동일한 REST 베이스 URL
BASE_REAL = "https://openapi.koreainvestment.com:9443"
BASE_PAPER = "https://openapivts.koreainvestment.com:29443"
# 국내주식 현재가 시세 (examples_user/domestic_stock_functions.inquire_price 와 동일 TR)
TR_INQUIRE_PRICE = "FHKST01010100"
API_INQUIRE_PRICE = "********"
def _load_env() -> None:
root = Path(__file__).resolve().parent
for name in (".env.local", ".env"):
p = root / name
if p.is_file():
load_dotenv(p, override=False)
return
load_dotenv(override=False)
def _truthy(val: str | None) -> bool:
if not val:
return False
return val.strip().lower() in ("1", "true", "yes", "y", "on")
class KISClient:
"""open-trading-api 의 kis_auth.auth + _url_fetch(GET) 패턴을 단일 클래스로 정리."""
def __init__(self, *, paper: bool | None = None) -> None:
_load_env()
self.app_key = (os.environ.get("APP_KEY") or os.environ.get("KIS_APP_KEY") or "").strip()
self.app_secret = (
os.environ.get("APP_SECRET") or os.environ.get("KIS_APP_SECRET") or ""
).strip()
if not self.app_key or not self.app_secret:
print(
"APP_KEY / APP_SECRET 이 비어 있습니다. .env.local 에 설정해 주세요.",
file=sys.stderr,
)
sys.exit(1)
if paper is None:
paper = _truthy(os.environ.get("KIS_PAPER"))
self.paper = paper
self.base_url = BASE_PAPER if paper else BASE_REAL
self._token: str | None = None
self._user_agent = (
os.environ.get("KIS_USER_AGENT")
or "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
)
def _base_headers(self) -> dict[str, str]:
h: dict[str, str] = {
"Content-Type": "application/json; charset=utf-8",
"Accept": "application/json",
"User-Agent": self._user_agent,
}
if self._token:
h["authorization"] = f"Bearer {self._token}"
h["appkey"] = self.app_key
h["appsecret"] = self.app_secret
return h
def issue_access_token(self) -> str:
"""POST /oauth2/tokenP — 샘플 kis_auth.auth 와 동일."""
url = f"{self.base_url}/oauth2/tokenP"
body = {
"grant_type": "client_credentials",
"appkey": self.app_key,
"appsecret": self.app_secret,
}
r = requests.post(url, headers=self._base_headers(), data=json.dumps(body), timeout=30)
if r.status_code != 200:
raise RuntimeError(f"토큰 발급 HTTP {r.status_code}: {r.text}")
data = r.json()
token = data.get("access_token")
if not token:
raise RuntimeError(f"토큰 응답에 access_token 없음: {data}")
self._token = token
return token
def _tr_id_for_domestic(self, tr_id: str) -> str:
"""모의투자 시 T/J/C 로 시작하는 TR 은 V 접두로 바꿈 (kis_auth._url_fetch)."""
if self.paper and tr_id and tr_id[0] in ("T", "J", "C"):
return "V" + tr_id[1:]
return tr_id
def get_json(
self,
path: str,
tr_id: str,
params: dict[str, Any],
*,
tr_cont: str = "",
) -> dict[str, Any]:
if not self._token:
self.issue_access_token()
url = f"{self.base_url}{path}"
headers = self._base_headers()
headers["tr_id"] = self._tr_id_for_domestic(tr_id)
headers["custtype"] = "P"
headers["tr_cont"] = tr_cont
r = requests.get(url, headers=headers, params=params, timeout=30)
if r.status_code != 200:
raise RuntimeError(f"API HTTP {r.status_code}: {r.text}")
return r.json()
def inquire_domestic_price(
self,
*,
env_dv: str = "real",
fid_cond_mrkt_div_code: str = "J",
fid_input_iscd: str = "005930",
) -> dict[str, Any]:
"""
주식 현재가 시세 (국내).
env_dv: 'real' 실전 / 'demo' 모의 (샘플 domestic_stock_functions 와 동일).
fid_cond_mrkt_div_code: J=KRX 등.
"""
if env_dv not in ("real", "demo"):
raise ValueError("env_dv 는 'real' 또는 'demo' 만 허용됩니다.")
params = {
"FID_COND_MRKT_DIV_CODE": fid_cond_mrkt_div_code,
"FID_INPUT_ISCD": fid_input_iscd,
}
return self.get_json(API_INQUIRE_PRICE, TR_INQUIRE_PRICE, params)
def main() -> None:
paper = _truthy(os.environ.get("KIS_PAPER"))
client = KISClient(paper=paper)
client.issue_access_token()
print("접근토큰 발급 완료.")
# 삼성전자 KRX 현재가 시세 (README 예제와 동일 파라미터)
env_label = "demo" if paper else "real"
body = client.inquire_domestic_price(
env_dv=env_label,
fid_cond_mrkt_div_code="J",
fid_input_iscd="005930",
)
rt = body.get("rt_cd")
msg = body.get("msg1", "")
if rt != "0":
print("API 오류:", rt, msg, file=sys.stderr)
print(json.dumps(body, ensure_ascii=False, indent=2))
sys.exit(1)
out = body.get("output")
print("조회 성공:", msg or "ok")
print(json.dumps(out, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()