AlgoLab Blog · KIS 트러블슈팅 · 2026

KIS 모의투자 실전 전환 — tr_id·도메인 바꿀 것 6가지

KIS · 한국투자 2026-08-02 · 약 8분 읽기 · 알고랩 AlgoLab
한 줄 요약 KIS 모의투자에서 잘 돌던 코드가 실전에서 막히는 이유는 대개 6가지입니다. ① 도메인·포트(openapivts…:29443openapi…:9443) ② tr_id 앞글자(모의 V → 실전 T, 예: VTTC0802UTTTC0802U) ③ 환경별 appkey·appsecretaccess_token 분리계좌번호 CANO·ACNT_PRDT_CD모의투자 미지원 TR호출 한도 차이. 이 6개를 한 곳에서 갈아끼우도록 짜 두면 전환이 한 줄로 끝납니다.

한국투자증권 KIS API로 봇을 만들면 거의 모두 모의투자에서 먼저 돌립니다. 그리고 실전으로 넘어가는 날 대부분 한 번은 막힙니다. 원인은 코드 실력이 아니라, KIS가 모의투자와 실전투자를 완전히 분리된 두 개의 서버로 운영하고 있어서입니다. 주소만 바꾸면 될 것 같지만 실제로 갈아끼워야 할 것이 6개입니다.

이 글은 그 6개만 다룹니다. 발급 자체가 처음이라면 KIS API 발급부터 첫 주문까지 30분을, 지금 에러코드를 받아 든 상태라면 KIS API 에러코드 11가지를 먼저 보세요.

전환할 때 바꿔야 하는 6가지

  1. 도메인과 포트 — REST와 WebSocket 모두
  2. tr_id — 앞글자 V와 T
  3. appkey · appsecret · access_token — 환경별 별도
  4. 계좌번호 — CANO와 ACNT_PRDT_CD
  5. 모의투자 미지원 TR
  6. 호출 한도 — 모의가 훨씬 빡빡합니다

왜 이런 구조인가

KIS Developers는 모의투자와 실전투자를 서로 다른 서버·다른 인증 체계로 운영합니다. 실전 주문이 모의 서버로 흘러가거나 그 반대가 생기면 사고이기 때문에, 애초에 섞이지 않도록 설계돼 있습니다. 이 분리가 안전장치이면서 동시에 전환 시 걸림돌이 됩니다.

내 봇 (같은 코드) MODE = "vps" | "prod" 모의투자 (vps) openapivts.koreainvestment.com 포트 29443 · WS 31000 tr_id = VTTC0802U 모의용 appkey / appsecret 일부 TR 미지원 · 한도 낮음 실전투자 (prod) openapi.koreainvestment.com 포트 9443 · WS 21000 tr_id = TTTC0802U 실전용 appkey / appsecret 실계좌 · 실제 체결
KIS는 모의와 실전이 서버부터 키까지 완전히 분리돼 있습니다

1도메인과 포트

구분모의투자실전투자
RESTopenapivts.koreainvestment.com:29443openapi.koreainvestment.com:9443
WebSocketops.koreainvestment.com:31000ops.koreainvestment.com:21000
공식 라이브러리 인자svr="vps"svr="prod"

한국투자증권 공식 예제 저장소(koreainvestment/open-trading-api)도 인증 함수에 svr="prod"svr="vps"를 넘겨 환경을 고르는 방식을 씁니다. 도메인 문자열을 코드 곳곳에 흩뿌리지 말고 한 곳에 모으라는 뜻입니다.

모의투자 도메인의 vts가 힌트입니다. openapiopenapivts는 눈으로 보면 거의 같아 보여서, 설정 파일에서 한 글자 차이를 놓치면 원인을 못 찾고 몇 시간을 씁니다. 포트(29443 vs 9443)도 함께 바뀐다는 점을 기억하세요. 주소는 변경될 수 있으니 최종 확인은 KIS Developers 포털에서 하시기 바랍니다.

2tr_id — 앞글자 V와 T

KIS API는 어떤 업무를 부르는지를 tr_id 헤더로 지정합니다. 그리고 이 값의 맨 앞 글자가 환경을 구분합니다. 모의투자는 V로 시작하고, 실전투자는 T로 시작합니다.

업무모의투자실전투자
국내주식 현금 매수 주문VTTC0802UTTTC0802U
국내주식 현금 매도 주문VTTC0801UTTTC0801U
주식 잔고 조회VTTC8434RTTTC8434R

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를 각 함수 안에 문자열로 박아 두면, 전환일에 파일 전체를 뒤지며 VT로 고치는 작업이 되고 하나라도 빠뜨리면 그 기능만 조용히 실패합니다.

시세 조회처럼 환경 구분이 없는 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단계.

6호출 한도 — 모의가 더 빡빡합니다

KIS는 실전과 모의의 초당 호출 한도가 다르고, 모의 쪽이 훨씬 낮습니다. 그래서 흔한 두 가지 착시가 생깁니다.

정확한 한도 숫자는 공식 안내에서 확인하세요. 값은 변경될 수 있습니다. 설계 관점에서 중요한 건 숫자 자체가 아니라 리미터 값을 환경별 설정으로 빼 두는 것입니다.

RATE = {"vps": 0.8, "prod": 15}      # 초당 호출 — 공식 한도보다 낮게 마진

limiter = RateLimiter(RATE[MODE])    # 환경 바꾸면 리미터도 같이 바뀜

전략을 여러 개 돌리면 호출량이 합산된다는 점도 잊기 쉽습니다. 큐잉·백오프까지 포함한 설계는 API 호출 제한 설계에서 다뤘습니다.

전환 전 체크리스트

  1. MODE 한 곳만 바꾸면 도메인·tr_id·키·계좌·리미터가 전부 따라 바뀌는가
  2. 토큰 캐시가 환경별로 분리돼 있는가
  3. 내 봇이 호출하는 TR 목록을 뽑아 모의 지원 여부를 확인했는가
  4. 실전 계좌의 CANO·ACNT_PRDT_CD를 실제 값으로 확인했는가
  5. 첫 실전 주문은 최소 수량 1주로 매수·매도 왕복을 해 봤는가
  6. 주문 수량·금액 상한과 킬 스위치가 켜져 있는가 (킬 스위치 설계)
  7. 실패 응답의 msg_cdmsg1로그에 그대로 남는가
🛠️
실전 전환이 무서운 게 정상입니다

알고랩은 KIS 봇을 제작할 때 환경 전환을 설정 한 줄로 만들고, 첫 실거래는 최소 수량 왕복부터 함께 확인합니다. 지금 만들던 코드가 있다면 그 상태로 가져오셔도 됩니다.

상담 시작하기 →

자주 묻는 질문

모의투자 계좌는 어디서 만드나요?

한국투자증권 모의투자 서비스에서 참가 신청을 한 뒤, KIS Developers에서 모의투자용 앱 등록을 별도로 해야 API를 쓸 수 있습니다. 실전 앱과는 신청 자체가 분리돼 있어, 실전 appkey가 있어도 모의는 다시 신청해야 합니다.

같은 코드로 모의와 실전을 동시에 돌릴 수 있나요?

프로세스를 나누면 가능합니다. 다만 전역 변수 하나로 MODE를 두면 한 프로세스 안에서 섞이므로, 실전용과 모의용을 별도 프로세스(별도 환경변수)로 띄우는 편이 안전합니다. 한 프로세스 안에서 토글하는 구조는 사고 위험이 큽니다.

키움 REST API도 같은 구조인가요?

개념은 비슷합니다. 키움 REST API 역시 운영과 모의투자의 엔드포인트와 앱키가 분리돼 있어 환경 전환 설계가 필요합니다. 다만 헤더 이름과 업무 코드 체계가 KIS와 달라서 코드를 그대로 옮길 수는 없습니다 → 키움 REST API 자동매매 완전 가이드, 키움 OpenAPI vs KIS API 비교.

모의투자에서 수익이 났으면 실전에서도 되나요?

같다고 볼 수 없습니다. 모의투자는 체결 방식과 호가 반영이 실제 시장과 다르고 슬리피지·비용이 그대로 재현되지 않습니다. 모의 성과나 과거 성과가 미래 수익을 보장하지 않습니다. 모의는 코드가 도는지 확인하는 배선 점검으로 쓰고, 전략 유효성은 백테스트와 실거래 괴리 관점에서 따로 검증하세요.

KIS 자동매매 봇 맞춤 제작

모의·실전 환경 분리, 토큰 자동 갱신, 에러 핸들링, 24시간 무중단 운영까지 — 알고랩이 통합 패키지로 제작합니다.
24시간 빠른 답변 가능합니다.

무료 상담 시작하기