KIS 모의투자 실전 전환 — tr_id·도메인 바꿀 것 6가지
openapivts…:29443 → openapi…:9443) ② tr_id 앞글자(모의 V → 실전 T, 예: VTTC0802U → TTTC0802U) ③ 환경별 appkey·appsecret과 access_token 분리 ④ 계좌번호 CANO·ACNT_PRDT_CD ⑤ 모의투자 미지원 TR ⑥ 호출 한도 차이. 이 6개를 한 곳에서 갈아끼우도록 짜 두면 전환이 한 줄로 끝납니다.
한국투자증권 KIS API로 봇을 만들면 거의 모두 모의투자에서 먼저 돌립니다. 그리고 실전으로 넘어가는 날 대부분 한 번은 막힙니다. 원인은 코드 실력이 아니라, KIS가 모의투자와 실전투자를 완전히 분리된 두 개의 서버로 운영하고 있어서입니다. 주소만 바꾸면 될 것 같지만 실제로 갈아끼워야 할 것이 6개입니다.
이 글은 그 6개만 다룹니다. 발급 자체가 처음이라면 KIS API 발급부터 첫 주문까지 30분을, 지금 에러코드를 받아 든 상태라면 KIS API 에러코드 11가지를 먼저 보세요.
전환할 때 바꿔야 하는 6가지
- 도메인과 포트 — REST와 WebSocket 모두
- tr_id — 앞글자 V와 T
- appkey · appsecret · access_token — 환경별 별도
- 계좌번호 — CANO와 ACNT_PRDT_CD
- 모의투자 미지원 TR
- 호출 한도 — 모의가 훨씬 빡빡합니다
0아직 안 만들었다면 — 모의투자 준비물 4가지
이 글의 본론은 "이미 모의에서 돌던 봇"을 실전으로 넘기는 얘기입니다. 그런데 그 앞 단계, "돈 넣기 전에 모의로 먼저 돌려 보고 싶다"는 분들이 자주 막히는 지점이 따로 있습니다. 모의투자는 실전 계정의 '테스트 모드'가 아니라 별도 환경이라, 키부터 계좌까지 새로 준비해야 합니다.
| 준비물 | 모의투자 | 실전투자 |
|---|---|---|
| REST 도메인 | https://openapivts.koreainvestment.com:29443 | https://openapi.koreainvestment.com:9443 |
appkey·appsecret | 모의투자 전용으로 따로 발급 | 실전용 별도 |
| 계좌번호 | 모의투자 계좌 (주식/선물옵션 각각) | 실계좌 |
| 계좌상품코드 | 01 주식·위탁 / 03 선물옵션 | 01·03·08·22·29 등 |
한국투자증권 공식 오픈API 샘플의 인증 모듈을 보면 이 분리가 코드에 그대로 드러나 있습니다. 환경 구분자는 실전 prod / 모의 vps이고, 모의 쪽은 paper_app·paper_sec이라는 별도 키 이름을 읽습니다. 계좌도 모의 주식용·모의 선물옵션용을 따로 둡니다.
# 한국투자증권 공식 샘플의 환경 구분 (요약)
# svr="prod" -> 실전 : my_app / my_sec / openapi.koreainvestment.com:9443
# svr="vps" -> 모의 : paper_app / paper_sec / openapivts.koreainvestment.com:29443
import kis_auth as ka
ka.auth(svr="vps", product="01") # 모의투자 · 주식(위탁)계좌
trenv = ka.getTREnv()
print(trenv.my_url) # 모의 도메인이 잡혔는지 먼저 확인
print(ka.isPaperTrading()) # True 여야 모의 환경
가장 흔한 첫 실수는 "실전 앱키로 모의 도메인을 호출"하는 것입니다. 도메인만 29443으로 바꾸고 키는 그대로 두면 인증부터 실패합니다. 키·계좌·도메인은 항상 같은 환경끼리 묶여 다녀야 합니다 — 그래서 아래 3번 섹션처럼 MODE 한 곳에서 세트로 갈아끼우는 구조를 처음부터 잡아 두는 편이 낫습니다.
모의투자 계좌와 모의용 앱키 신청 절차는 KIS Developers 포털과 한국투자증권 안내를 따르시고(정책이 바뀔 수 있습니다), 발급 절차 자체가 처음이라면 이 글 아래에 링크된 발급 가이드를 먼저 보시는 편이 빠릅니다.
왜 이런 구조인가
KIS Developers는 모의투자와 실전투자를 서로 다른 서버·다른 인증 체계로 운영합니다. 실전 주문이 모의 서버로 흘러가거나 그 반대가 생기면 사고이기 때문에, 애초에 섞이지 않도록 설계돼 있습니다. 이 분리가 안전장치이면서 동시에 전환 시 걸림돌이 됩니다.
1도메인과 포트
| 구분 | 모의투자 | 실전투자 |
|---|---|---|
| REST | openapivts.koreainvestment.com:29443 | openapi.koreainvestment.com:9443 |
| WebSocket | ops.koreainvestment.com:31000 | ops.koreainvestment.com:21000 |
| 공식 라이브러리 인자 | svr="vps" | svr="prod" |
한국투자증권 공식 예제 저장소(koreainvestment/open-trading-api)도 인증 함수에 svr="prod"와 svr="vps"를 넘겨 환경을 고르는 방식을 씁니다. 도메인 문자열을 코드 곳곳에 흩뿌리지 말고 한 곳에 모으라는 뜻입니다.
모의투자 도메인의 vts가 힌트입니다. openapi와 openapivts는 눈으로 보면 거의 같아 보여서, 설정 파일에서 한 글자 차이를 놓치면 원인을 못 찾고 몇 시간을 씁니다. 포트(29443 vs 9443)도 함께 바뀐다는 점을 기억하세요. 주소는 변경될 수 있으니 최종 확인은 KIS Developers 포털에서 하시기 바랍니다.
2tr_id — 앞글자 V와 T
KIS API는 어떤 업무를 부르는지를 tr_id 헤더로 지정합니다. 그리고 이 값의 맨 앞 글자가 환경을 구분합니다. 모의투자는 V로 시작하고, 실전투자는 T로 시작합니다.
| 업무 | 모의투자 | 실전투자 |
|---|---|---|
| 국내주식 현금 매수 주문 | VTTC0802U | TTTC0802U |
| 국내주식 현금 매도 주문 | VTTC0801U | TTTC0801U |
| 주식 잔고 조회 | VTTC8434R | TTTC8434R |
tr_id는 KIS 공지로 바뀔 수 있습니다. 위 값은 널리 쓰이는 예시이며, 실제 개발 시에는 KIS Developers 포털의 해당 API 문서에 적힌 실전·모의 tr_id를 그때그때 확인하세요. 코드에 흩어 놓지 말고 아래처럼 매핑 테이블 한 곳에 모아 두면 공지가 떠도 한 줄만 고치면 됩니다.
MODE = "prod" # "vps" = 모의투자, "prod" = 실전투자
ENV = {
"vps": {"base": "https://openapivts.koreainvestment.com:29443",
"ws": "ws://ops.koreainvestment.com:31000",
"appkey": os.environ["KIS_VPS_APPKEY"],
"appsecret": os.environ["KIS_VPS_APPSECRET"],
"cano": os.environ["KIS_VPS_CANO"]},
"prod": {"base": "https://openapi.koreainvestment.com:9443",
"ws": "ws://ops.koreainvestment.com:21000",
"appkey": os.environ["KIS_PROD_APPKEY"],
"appsecret": os.environ["KIS_PROD_APPSECRET"],
"cano": os.environ["KIS_PROD_CANO"]},
}
TR = { # 업무명 → (모의, 실전)
"buy": ("VTTC0802U", "TTTC0802U"),
"sell": ("VTTC0801U", "TTTC0801U"),
"balance": ("VTTC8434R", "TTTC8434R"),
}
def tr_id(job):
return TR[job][0] if MODE == "vps" else TR[job][1]
def cfg(k):
return ENV[MODE][k]
이렇게 두면 전환은 MODE 한 줄입니다. 반대로 tr_id를 각 함수 안에 문자열로 박아 두면, 전환일에 파일 전체를 뒤지며 V를 T로 고치는 작업이 되고 하나라도 빠뜨리면 그 기능만 조용히 실패합니다.
시세 조회처럼 환경 구분이 없는 tr_id도 있습니다(국내주식 현재가 시세의 FHKST01010100 계열). 모두 V/T로 갈리는 것은 아니니 매핑 테이블에는 실제 문서에서 확인한 값만 넣으세요.
3appkey·appsecret과 토큰은 환경별로 따로
모의투자 appkey로 실전 도메인을 호출하면 인증에서 막힙니다. KIS Developers에서 앱을 등록할 때 모의투자용과 실전투자용을 각각 신청해야 하고, 그 키로 받은 access_token도 환경별로 따로 발급됩니다.
import os, json, time, requests
def issue_token():
res = requests.post(f"{cfg('base')}/oauth2/tokenP",
headers={"content-type": "application/json"},
data=json.dumps({
"grant_type": "client_credentials",
"appkey": cfg("appkey"),
"appsecret": cfg("appsecret"),
}), timeout=10)
body = res.json()
if "access_token" not in body:
raise RuntimeError(f"토큰 발급 실패: {body}") # msg_cd로 원인 확인
return body["access_token"], body.get("expires_in")
TOKEN_CACHE = f".kis_token_{MODE}.json" # ★ 환경별로 파일 분리
토큰 캐시 파일을 환경별로 나누지 않으면 모의에서 받은 토큰이 실전 호출에 섞여 들어가고, 원인을 알 수 없는 인증 오류가 간헐적으로 뜹니다. 파일명·환경변수·Redis 키 어디에 저장하든 MODE를 키에 포함시키세요.
토큰 발급은 짧은 간격으로 반복 요청하면 차단되므로 캐시가 필수입니다. 관련 에러코드는 EGW00121(유효하지 않은 토큰)·EGW00123(만료)·EGW00133(발급 요청 과다) 세 가지를 구분해 처리하세요.
4계좌번호 — CANO와 ACNT_PRDT_CD
KIS API의 주문·잔고 요청은 계좌번호를 두 조각으로 나눠 받습니다. 앞자리는 CANO, 상품코드는 ACNT_PRDT_CD입니다. 그리고 모의투자 계좌번호와 실전 계좌번호는 당연히 다릅니다.
def order_cash(code, qty, price=0, buy=True):
url = f"{cfg('base')}/uapi/domestic-stock/v1/trading/order-cash"
body = {
"CANO": cfg("cano"), # 계좌 앞 8자리
"ACNT_PRDT_CD": "01", # 상품코드 2자리
"PDNO": code, # 종목코드 (예: "005930")
"ORD_DVSN": "01" if price == 0 else "00", # 01=시장가, 00=지정가
"ORD_QTY": str(qty),
"ORD_UNPR": str(price),
}
headers = {
"content-type": "application/json",
"authorization": f"Bearer {get_token()}",
"appkey": cfg("appkey"),
"appsecret": cfg("appsecret"),
"tr_id": tr_id("buy" if buy else "sell"), # ★ 환경 자동 반영
"custtype": "P", # 개인
}
res = requests.post(url, headers=headers, data=json.dumps(body), timeout=10)
out = res.json()
if out.get("rt_cd") != "0":
raise RuntimeError(f"주문 실패 [{out.get('msg_cd')}] {out.get('msg1')}")
return out
응답은 언제나 rt_cd(0이면 성공)·msg_cd·msg1 세 필드로 판정합니다. 실전 전환 직후 자주 보는 실패 응답은 이렇게 생겼습니다.
{"rt_cd":"1","msg_cd":"40580000","msg1":"모의투자 미지원 API 입니다."}
{"rt_cd":"1","msg_cd":"EGW00201","msg1":"초당 거래건수를 초과하였습니다."}
주문 API는 hashkey를 함께 요구하는 경우가 있고, 계좌 상품코드도 계좌 종류에 따라 다릅니다. 본인 계좌 기준 값은 KIS Developers 포털과 계좌 정보에서 직접 확인하세요.
5모의투자 미지원 TR
가장 늦게 발견되는 함정입니다. 모의투자는 일부 TR을 아예 지원하지 않습니다. 그래서 "모의에서 다 통과했다"는 말이 실제로는 "모의에서 실행된 코드 경로만 통과했다"인 경우가 많습니다.
어떤 API가 모의투자를 지원하는지는 KIS Developers 포털의 각 API 문서에 표기돼 있고, 정책에 따라 바뀔 수 있습니다. 목록을 외우려 하지 말고 내 봇이 실제로 호출하는 TR을 나열해 놓고 하나씩 확인하는 편이 확실합니다.
반대 방향의 위험도 있습니다. 모의에서 지원되지 않아 한 번도 실행되지 않은 코드가 실전에서 처음 돌면서 버그가 드러나는 것입니다. 그래서 실전 전환은 "스위치를 켠다"가 아니라 최소 수량으로 한 번 왕복시켜 보는 단계를 반드시 끼워야 합니다 → 실거래 전 검증 3단계.
모의투자로 확인되는 것과, 끝까지 확인 안 되는 것
"모의에서 한 달 잘 돌았으니 됐다"가 위험한 이유는 모의가 검증해 주는 범위가 정해져 있기 때문입니다. 모의 단계에서 무엇을 확인한 것인지 명확히 알아야, 실전 첫날에 무엇을 조심해야 하는지도 정해집니다.
| 모의로 확인되는 것 | 모의로 확인 안 되는 것 |
|---|---|
|
· 토큰 발급·갱신 흐름 · 요청 파라미터·헤더 구성 · 주문 접수/거절 분기 처리 · 잔고·미체결 응답 파싱 · 스케줄러·재시작 복원 · 예외·타임아웃 재시도 |
· 실제 체결가와 슬리피지 · 호가 잔량 소진·시장 충격 · 모의 미지원 TR을 타는 코드 경로 · 실전 기준 호출 한도에서의 안정성 · 수수료·제세금이 반영된 실제 손익 · 증거금·미수 관련 거절 |
오른쪽 칸이 전부 "돈이 실제로 움직여야 드러나는 것"입니다. 그래서 실전 전환은 스위치가 아니라 최소 수량 왕복 → 소액 며칠 → 정상 수량 순서여야 합니다.
특히 성과 집계 코드는 모의에서 검증이 안 될 수 있어 따로 챙겨야 합니다. 손익 조회 계열 API는 모의 지원 여부가 API마다 다르므로, 모의 단계에서 한 번 호출해 보고 안 되면 체결 내역 기반 자체 집계로 우회하는 경로를 미리 만들어 두십시오 → KIS 기간별매매손익 조회 TTTC8715R.
6호출 한도 — 모의가 더 빡빡합니다
KIS는 실전과 모의의 초당 호출 한도가 다르고, 모의 쪽이 훨씬 낮습니다. 그래서 흔한 두 가지 착시가 생깁니다.
- 모의에서
EGW00201이 뜬다 → 코드가 잘못된 게 아니라 모의 한도가 낮은 것일 수 있습니다. - 모의에서 잘 참던 봇이 실전에서 더 공격적으로 돈다 → 한도가 올라간 만큼 종목 수를 늘리다가 실전에서도 결국 걸립니다.
정확한 한도 숫자는 공식 안내에서 확인하세요. 값은 변경될 수 있습니다. 설계 관점에서 중요한 건 숫자 자체가 아니라 리미터 값을 환경별 설정으로 빼 두는 것입니다.
RATE = {"vps": 0.8, "prod": 15} # 초당 호출 — 공식 한도보다 낮게 마진
limiter = RateLimiter(RATE[MODE]) # 환경 바꾸면 리미터도 같이 바뀜
전략을 여러 개 돌리면 호출량이 합산된다는 점도 잊기 쉽습니다. 큐잉·백오프까지 포함한 설계는 API 호출 제한 설계에서 다뤘습니다.
전환 전 체크리스트
MODE한 곳만 바꾸면 도메인·tr_id·키·계좌·리미터가 전부 따라 바뀌는가- 토큰 캐시가 환경별로 분리돼 있는가
- 내 봇이 호출하는 TR 목록을 뽑아 모의 지원 여부를 확인했는가
- 실전 계좌의
CANO·ACNT_PRDT_CD를 실제 값으로 확인했는가 - 첫 실전 주문은 최소 수량 1주로 매수·매도 왕복을 해 봤는가
- 주문 수량·금액 상한과 킬 스위치가 켜져 있는가 (킬 스위치 설계)
- 실패 응답의
msg_cd와msg1이 로그에 그대로 남는가
알고랩은 KIS 봇을 제작할 때 환경 전환을 설정 한 줄로 만들고, 첫 실거래는 최소 수량 왕복부터 함께 확인합니다. 지금 만들던 코드가 있다면 그 상태로 가져오셔도 됩니다.
상담 시작하기 →자주 묻는 질문
모의투자 계좌는 어디서 만드나요?
한국투자증권 모의투자 서비스에서 참가 신청을 한 뒤, KIS Developers에서 모의투자용 앱 등록을 별도로 해야 API를 쓸 수 있습니다. 실전 앱과는 신청 자체가 분리돼 있어, 실전 appkey가 있어도 모의는 다시 신청해야 합니다.
같은 코드로 모의와 실전을 동시에 돌릴 수 있나요?
프로세스를 나누면 가능합니다. 다만 전역 변수 하나로 MODE를 두면 한 프로세스 안에서 섞이므로, 실전용과 모의용을 별도 프로세스(별도 환경변수)로 띄우는 편이 안전합니다. 한 프로세스 안에서 토글하는 구조는 사고 위험이 큽니다.
키움 REST API도 같은 구조인가요?
개념은 비슷합니다. 키움 REST API 역시 운영과 모의투자의 엔드포인트와 앱키가 분리돼 있어 환경 전환 설계가 필요합니다. 다만 헤더 이름과 업무 코드 체계가 KIS와 달라서 코드를 그대로 옮길 수는 없습니다 → 키움 REST API 자동매매 완전 가이드, 키움 OpenAPI vs KIS API 비교.
모의투자로 며칠이나 돌려 보고 넘어가면 되나요?
기간보다 무엇이 한 번씩은 실행됐는가가 기준입니다. 최소한 ① 매수·매도가 한 번씩 체결되고 ② 주문이 거절되는 경우를 한 번 만나 보고 ③ 장 시작·종료를 넘겨 스케줄러가 하루를 완주하고 ④ 봇을 강제 종료했다가 재시작해 상태 복원이 되는 것까지 확인되면, 그 다음은 모의에서 더 오래 돌려도 새로 알게 되는 것이 거의 없습니다. 반대로 이 네 가지가 안 됐다면 몇 주를 돌려도 검증된 것이 아닙니다. 그 뒤에는 모의에 머무르지 말고 최소 수량 실거래로 넘어가 슬리피지·체결 같은 나머지 항목을 확인하는 편이 낫습니다.
모의투자에서 수익이 났으면 실전에서도 되나요?
같다고 볼 수 없습니다. 모의투자는 체결 방식과 호가 반영이 실제 시장과 다르고 슬리피지·비용이 그대로 재현되지 않습니다. 모의 성과나 과거 성과가 미래 수익을 보장하지 않습니다. 모의는 코드가 도는지 확인하는 배선 점검으로 쓰고, 전략 유효성은 백테스트와 실거래 괴리 관점에서 따로 검증하세요.
KIS 자동매매 봇 맞춤 제작
모의·실전 환경 분리, 토큰 자동 갱신, 에러 핸들링, 24시간 무중단 운영까지 — 알고랩이 통합 패키지로 제작합니다.
24시간 빠른 답변 가능합니다.