AlgoLab Blog · 한국투자증권 KIS API · 인증 · 2026

KIS 모의·실전 토큰 충돌 — kis_auth 남은 버그 2개

KIS API · 인증 2026-10-31 · 약 7분 읽기 · 알고랩 AlgoLab
한 줄 요약

한국투자증권 공식 샘플 kis_auth.py는 2026-10-27(커밋 3a9be59) 전까지 실전·모의 토큰을 ~/KIS/config/KIS20261031 같은 날짜 이름 파일 하나에 저장했습니다. 그래서 같은 날 실전으로 auth()한 뒤 모의로 auth(svr="vps")하면, 모의 도메인에 실전 토큰이 실려 나가 인증이 실패했습니다. 수정 후에는 KIS20261031_prod·KIS20261031_vps로 파일이 나뉩니다. 다만 고쳐진 곳은 examples_user·examples_llm 두 폴더뿐이고 backtester·strategy_builder의 kis_auth.py는 그대로입니다. 또 모든 사본에 남은 버그가 2개 있습니다 — reAuth()의 .seconds >= 86400은 영원히 거짓이고, 모의용 _smartSleep = 0.5는 지역 변수라 실제 간격은 늘 0.1초입니다. (공식 저장소 koreainvestment/open-trading-api main 기준, 2026-10-31 확인·로컬 재현)

목차

  1. 무엇이 바뀌었나 — 커밋 3a9be59 실물
  2. 재현 — 수정 전·후를 그대로 돌려 본 출력
  3. 고쳐진 폴더, 안 고쳐진 폴더
  4. 모든 사본에 남은 버그 2개
  5. 내 봇 점검 순서와 패치 코드
  6. 자주 묻는 질문

1. 무엇이 바뀌었나 — 커밋 3a9be59 실물

KIS Open API를 파이썬으로 시작하는 사람 대부분이 공식 GitHub 저장소 open-trading-api의 kis_auth.py를 그대로 가져다 씁니다(공식 깃허브 예제 실행법). 이 파일은 /oauth2/tokenP로 받은 접근토큰을 로컬 파일에 캐시해 두고, 다음 실행 때 만료 전이면 재사용합니다. 파일 이름은 이렇게 정해집니다.

config_root = os.path.join(os.path.expanduser("~"), "KIS", "config")
token_tmp = os.path.join(
    config_root, f"KIS{datetime.today().strftime('%Y%m%d')}"
)  # 토큰 로컬저장시 파일명 년월일

날짜만 들어가고 실전(prod)인지 모의(vps)인지는 이름에 없습니다. 수정 전 read_token()은 인자 없이 이 파일 하나를 읽었고, save_token()도 이 파일 하나에 썼습니다. 2026-10-27 커밋 메시지는 이 문제를 이렇게 적었습니다 — "같은 날 실전·모의를 함께 사용할 때 토큰 파일 충돌로 인증이 실패하던 문제 수정". 바뀐 핵심은 아래 diff입니다.

+def _token_path(svr):
+    return f"{token_tmp}_{svr}"

-def save_token(my_token, my_expired):
+def save_token(my_token, my_expired, svr=None):
 ...
-def read_token():
+def read_token(svr=None):
 ...
 def auth(svr="prod", product=_cfg["my_prod"], url=None):
+    global _last_svr
+    _last_svr = svr
 ...
-    saved_token = read_token()  # 기존 발급 토큰 확인
+    saved_token = read_token(svr)  # 기존 발급 토큰 확인
 ...
-            save_token(my_token, my_expired)
+            save_token(my_token, my_expired, svr)

같은 커밋에 두 가지가 더 들어 있습니다. 백테스터가 전략 코드를 main.py로 쓸 때 write_text(code, encoding="utf-8")를 명시해 윈도우에서 Lean 컨테이너가 main.py를 못 읽던 문제를 고쳤고(공식 백테스터 글 참고), stocks_info 종목 마스터 스크립트의 경로 결합을 base_dir + "\\..."에서 os.path.join으로 바꿔 리눅스·맥에서도 돌게 했습니다. 직전 커밋 f89a8cc는 KIS Trading MCP 주문 API의 'CANO' TypeError 수정입니다.

수정 전 (~10-27) auth("prod") auth("vps") KIS20261031 모의가 실전 토큰을 읽어 29443 포트로 보냄 수정 후 (10-27~) auth("prod") auth("vps") KIS20261031_prod KIS20261031_vps 환경마다 파일이 따로라 서로 덮어쓰지 않음 단, backtester/·strategy_builder/의 kis_auth.py는 아직 왼쪽 구조 (2026-10-31 main)
KIS 토큰 캐시 파일 구조 · 공식 저장소 커밋 3a9be59 전후

2. 재현 — 수정 전·후를 그대로 돌려 본 출력

말로만 보면 감이 안 오니 직접 돌려 봤습니다. 수정 직전 파일(f89a8cc 시점 examples_user/kis_auth.py)과 현재 파일을 한 줄도 고치지 않고 import한 뒤, 토큰 서버 응답만 가짜로 바꿨습니다. 가짜 서버는 도메인이 openapivts면 TOKEN_VPS_..., 아니면 TOKEN_PROD_...를 돌려줍니다. 실제 앱키는 쓰지 않았습니다. 순서는 실전 auth("prod") → 모의 auth("vps")입니다.

# 수정 전 (f89a8cc)
prod auth -> r TOKEN_PROD_REAL_APPKEY | url https://openapi.koreainvestment.com:9443
vps  auth -> r TOKEN_PROD_REAL_APPKEY | url https://openapivts.koreainvestment.com:29443
token POST 횟수: ['prod']
token files: ['KIS20261031', 'kis_devlp.yaml']

# 수정 후 (3a9be59)
prod auth -> r TOKEN_PROD_REAL_APPKEY | url https://openapi.koreainvestment.com:9443
vps  auth -> r TOKEN_VPS_PAPER_APPKEY | url https://openapivts.koreainvestment.com:29443
token POST 횟수: ['prod', 'vps']
token files: ['KIS20261031', 'KIS20261031_prod', 'KIS20261031_vps', 'kis_devlp.yaml']

수정 전에는 토큰 발급 요청(POST /oauth2/tokenP)이 한 번만 나갔습니다. 모의 auth가 파일에서 아직 만료 안 된 실전 토큰을 찾았으니 "기존 토큰 사용" 분기로 빠진 겁니다. 그 결과 authorization: Bearer 헤더에는 실전 토큰, appkey·appsecret에는 모의 키, 도메인은 모의 29443 포트라는 섞인 요청이 됩니다. 실제 서버에서는 토큰 오류로 돌아옵니다 — KIS 토큰 오류는 보통 EGW00121(유효하지 않은 token)·EGW00123(기간이 만료된 token) 계열이니 응답의 msg_cd를 확인하세요(KIS 에러코드 정리).

이 문제는 프로세스가 둘이어도 똑같이 납니다. 실전 봇과 모의 테스트 스크립트가 같은 PC, 같은 사용자 계정에서 돌면 둘 다 ~/KIS/config/의 같은 날짜 파일을 보기 때문입니다. 반대 순서(모의 먼저)면 실전 봇이 모의 토큰을 들고 9443 포트로 갑니다. 아침에 모의로 전략을 시험하고 장 시작 직전에 실전 봇을 켜는 흔한 순서에서 실전 봇이 첫 주문부터 막히는 셈입니다.

참고로 kis_auth.py 주석은 토큰 규칙을 "유효기간 1일, 6시간 이내 발급시 기존 token값 유지, 발급시 알림톡 무조건 발송"이라고 적고 있습니다. 파일을 지워 강제로 재발급하면 카카오 알림톡이 매번 옵니다. 재발급 빈도 제한과 캐싱 원칙은 KIS 토큰 만료·재발급 규칙에 정리해 두었습니다.

3. 고쳐진 폴더, 안 고쳐진 폴더

저장소에는 kis_auth.py 사본이 여러 개 있습니다. 2026-10-31 기준 main을 통째로 받아 _token_path 유무를 grep한 결과입니다.

경로토큰 파일 분리reAuth 86400 버그_smartSleep 버그
examples_user/kis_auth.py✅ 수정됨남음남음
examples_llm/kis_auth.py✅ 수정됨 (위와 동일 파일)남음남음
backtester/kis_auth.py❌ 단일 파일남음남음
strategy_builder/kis_auth.py❌ 단일 파일남음남음
legacy/rest/kis_auth.py❌ 단일 파일남음—

backtester로 실전 데이터를 받으면서 examples_user로 모의 주문을 돌리는 조합이 가장 위험합니다. 백테스터 쪽은 여전히 접미사 없는 KIS20261031을 읽고 쓰는데, 수정된 쪽도 import 시점에 같은 이름의 빈 파일을 만들어 두기 때문입니다(open(token_tmp, "w+")). 그리고 무엇보다, 몇 달 전 자기 프로젝트에 복사해 둔 kis_auth.py는 저장소가 고쳐져도 그대로입니다. 외주로 받은 봇이라면 납품 코드 안의 사본을 확인해야 합니다.

4. 모든 사본에 남은 버그 2개

① reAuth()는 하루가 지나도 발동하지 않는다

def reAuth(svr="prod", product=_cfg["my_prod"]):
    n2 = datetime.now()
    if (n2 - _last_auth_time).seconds >= 86400:  # 유효시간 1일
        auth(svr, product)

파이썬 timedelta.seconds는 전체 초가 아니라 일(days)을 떼고 남은 초입니다. 범위가 0~86399라 86400 이상이 될 수 없습니다. 같은 조건이 웹소켓용 reAuth_ws()에도 있습니다. 직접 찍어 보면 이렇습니다.

>>> from datetime import datetime, timedelta
>>> t0 = datetime(2026, 10, 30, 8, 0, 0)
>>> for h in [23, 24, 25, 48]:
...     d = (t0 + timedelta(hours=h)) - t0
...     print(h, d.seconds, d.seconds >= 86400, d.total_seconds())
23 82800 False 82800.0
24 0 False 86400.0
25 3600 False 90000.0
48 0 False 172800.0

기본값 _autoReAuth = False라 대부분은 이 함수를 타지 않지만, 24시간 켜 두는 봇을 만들며 _autoReAuth = True로 바꾼 사람은 "자동 갱신된다"고 믿게 됩니다. 실제로는 토큰 만료 후 EGW00123이 날 때까지 아무 일도 일어나지 않습니다. 하나 더 — _getBaseHeader()는 reAuth()를 인자 없이 부르므로 기본값 svr="prod"가 들어갑니다. 조건을 고쳐 발동하게 만들면 이번엔 모의 봇이 실전 키로 재인증합니다. 둘을 같이 고쳐야 합니다.

② 모의투자 간격 0.5초가 실제로는 0.1초다

_smartSleep = 0.1          # 모듈 변수
...
def changeTREnv(token_key, svr="prod", product=_cfg["my_prod"]):
    ...
    if svr == "prod":
        _smartSleep = 0.05     # global 선언 없음 → 지역 변수
    elif svr == "vps":
        _smartSleep = 0.5      # 지역 변수

changeTREnv 안에 global _isPaper는 있는데 global _smartSleep은 없습니다. 위 재현 스크립트 끝에서 모의 auth 직후 모듈 값을 찍으면 _smartSleep after vps auth: 0.1이 나옵니다(수정 전·후 동일). 샘플의 smart_sleep()을 호출 간격으로 쓰는 코드는 모의에서도 0.1초 간격으로 돌고, 모의는 실전보다 호출 한도가 낮으니 EGW00201(초당 거래건수 초과)이 잦아집니다. 모의 호출 제한은 KIS 모의투자 한계, 초당 제한 대응은 EGW00201 해결에 따로 정리했습니다.

5. 내 봇 점검 순서와 패치 코드

# 1) reAuth: 전체 초로 비교하고, 마지막으로 인증한 환경을 쓴다
def reAuth(svr=None, product=_cfg["my_prod"]):
    svr = svr or _last_svr or "prod"
    if (datetime.now() - _last_auth_time).total_seconds() >= 86400 - 600:
        auth(svr, product)

# 2) changeTREnv: 모듈 변수를 실제로 바꾼다
def changeTREnv(token_key, svr="prod", product=_cfg["my_prod"]):
    cfg = dict()
    global _isPaper, _smartSleep
    ...

만료 10분 전(86400 - 600)에 갱신하도록 여유를 둔 것은 제 선택입니다. 더 정확하게 하려면 access_token_token_expired 응답값을 저장해 그 시각 기준으로 판단하는 편이 낫습니다 — 수정된 _read_token_file()도 파일의 valid-date와 현재 시각을 비교합니다. 같은 봇을 두 번 띄워 토큰 파일과 주문이 엉키는 문제는 봇 중복 실행 방지를 참고하세요.

KIS로 무엇이 되고 안 되는지 전체 그림은 KIS API 자동매매 개요에, 앱키 발급부터는 KIS API 키 발급 30분에 있습니다.

6. 자주 묻는 질문

KIS 모의투자와 실전을 같은 날 같이 쓰면 왜 인증이 실패하나요?

공식 샘플 kis_auth.py가 2026-10-27 수정 전까지 토큰을 날짜 이름 파일 하나(예: KIS20261031)에 저장했기 때문입니다. 실전으로 먼저 auth()하면 실전 토큰이 그 파일에 저장되고, 이어서 모의로 auth(svr='vps')하면 같은 파일에서 아직 유효한 실전 토큰을 읽어 모의 도메인(openapivts.koreainvestment.com:29443)에 보냅니다. 토큰은 발급받은 환경에서만 유효하므로 토큰 오류가 납니다.

10월 27일 수정으로 모든 kis_auth.py가 고쳐졌나요?

아닙니다. 커밋 3a9be59로 고쳐진 것은 examples_user/kis_auth.py와 examples_llm/kis_auth.py 두 개입니다. 같은 저장소의 backtester/kis_auth.py와 strategy_builder/kis_auth.py는 여전히 파일 하나에 저장하는 수정 전 구조입니다(2026-10-31 main 기준). 자기 프로젝트에 예전에 복사해 둔 kis_auth.py도 직접 바꿔야 합니다.

reAuth()를 켜 두면 하루가 지나면 토큰이 자동으로 갱신되나요?

공식 코드 그대로는 갱신되지 않습니다. 조건이 (n2 - _last_auth_time).seconds >= 86400인데, 파이썬 timedelta의 .seconds는 일(day)을 뺀 나머지 초라 0~86399만 나옵니다. 24시간이 지나면 0, 25시간이면 3600이 되어 조건이 영원히 거짓입니다. total_seconds()로 바꾸거나 토큰의 만료 시각으로 판단해야 합니다.

모의투자에서 공식 샘플은 0.5초 간격으로 호출하나요?

의도는 그렇지만 실제로는 0.1초입니다. changeTREnv() 안에서 _smartSleep = 0.5를 대입하지만 global 선언이 없어 함수 안의 지역 변수만 바뀌고, smart_sleep()이 읽는 모듈 변수는 처음 값 0.1 그대로입니다. 모의에서 초당 호출 제한(EGW00201)이 잦다면 이 부분을 먼저 확인하세요.

실전·모의를 나눠 쓰는 KIS 봇, 만들어 드립니다

토큰 캐시·재인증·호출 간격을 환경별로 분리하고, 모의 검증 후 실전 전환까지 한 번에 되는 한국투자증권 API 자동매매를 맞춤 제작합니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기