미국 주식을 코드로 사고파는 봇을 만들겠다고 마음먹고 나서 제일 먼저 붙잡고 있었던 건 증권사 API였다. 시세를 읽든 잔고를 보든 주문을 내든, 증권사 서버와 말이 통해야 뭐라도 할 수 있으니까.
나는 한국투자증권(KIS) Open API로 미국주식 자동매매 봇을 만들어 돌리고 있다. 계좌를 몇 칸으로 나눠 전략을 따로 굴리는 구조인데, 혼자서는 미장봇이라고 부른다. 처음 시작할 때만 해도 선택지는 사실상 KIS 하나였다. 그런데 2026년에 토스증권이 REST API를 공개하면서 사정이 조금 달라졌다. 그래서 첫 편에는 KIS로 봇을 만들면서 직접 겪은 것들과 새로 나온 토스증권 API를 나란히 놓고, 지금 시작한다면 무엇을 고를지를 적어 보려고 한다.
왜 REST API였나
예전의 증권사 자동화는 HTS를 켜 둔 채로 윈도우 전용 COM/OCX 인터페이스에 붙는 방식이 대부분이었다. PC를 끄면 봇도 같이 죽고, 윈도우가 아니면 아예 돌릴 수가 없었다. REST API는 그냥 HTTP 요청이라 따로 설치할 것이 없고, 리눅스 서버든 클라우드든 라즈베리파이든 어디서나 돌아간다. 언어도 파이썬이든 자바스크립트든 상관없다.
미국 장은 한국 시간으로 밤부터 새벽까지 열린다. 사람이 깨어 있기 힘든 시간에 돌아가야 하니, 꺼지지 않는 서버에 올려 둘 수 있다는 점이 생각보다 컸다. 지금 미장봇도 맥과 리눅스에서 데몬으로 계속 떠 있다.
모든 요청은 토큰 하나에서 시작한다
KIS든 토스든 첫 단계는 OAuth 2.0 토큰을 받는 것이다. 발급받은 앱 키와 시크릿을 보내 액세스 토큰을 받고, 그 뒤로는 모든 요청 헤더에 이 토큰을 실어 보낸다. 두 곳 모두 client_credentials 방식이라 한쪽을 이해하면 다른 쪽도 거의 그대로 통한다. KIS에서 토큰을 받는 요청은 이렇게 생겼다. 엔드포인트는 /oauth2/tokenP 이다.
curl -s -X POST "https://openapi.koreainvestment.com:9443/oauth2/tokenP" \
-H "Content-Type: application/json" \
-d '{
"grant_type": "client_credentials",
"appkey": "'"$KIS_APP_KEY"'",
"appsecret": "'"$KIS_APP_SECRET"'"
}'
# 응답: {"access_token":"eyJ...","token_type":"Bearer","expires_in":86400}KIS가 토스와 다른 점이 하나 있는데, 토큰만으로 끝나지 않고 appkey와 appsecret까지 매 요청 헤더에 같이 넣으라고 한다. 토스는 Bearer 토큰만 있으면 된다. 미장봇의 헤더는 이렇게 만든다.
@property
def headers(self) -> dict:
return {
"content-type": "application/json; charset=utf-8",
"authorization": f"Bearer {self.token}",
"appkey": self.app_key, # KIS는 헤더에도 키가 필요
"appsecret": self.app_secret,
}
처음에 가장 빠지기 쉬운 함정은 요청할 때마다 토큰을 새로 받는 것이다. KIS는 토큰 발급을 분당 1회 수준으로 제한한다. 시세를 조회할 때마다 토큰을 새로 받으면 몇 분 안에 EGW00103 락아웃에 걸린다. 그래서 미장봇은 받은 토큰을 config/token.json 파일에 저장해 두고, 유효기간 약 24시간 가운데 만료 60초 전까지는 그대로 다시 쓴다.
이 파일에는 시크릿이 담기기 때문에 권한을 0600으로 잠갔다. 그리고 실전 계좌 토큰과 모의투자 토큰이 섞이지 않도록 is_virtual 값을 같이 저장해서, 환경이 다르면 캐시를 버리게 했다.
def _save_cached_token(self):
with open(TOKEN_FILE, "w") as f:
json.dump({
"token": self._token,
"expires": self._token_expires,
"is_virtual": self.is_virtual, # 실전/모의 구분
}, f)
os.chmod(TOKEN_FILE, 0o600) # 소유자만 읽기
def _load_cached_token(self):
data = json.load(open(TOKEN_FILE))
if data.get("is_virtual") != self.is_virtual:
return None # 실전/모의 환경이 다르면 캐시를 버린다
return data # 만료 60초 전까지 재사용4월에 하루를 통째로 잠갔던 일
토큰 발급이 실패했을 때의 처리는 사고를 치고 나서야 제대로 넣었다. 2026년 4월 11일부터 13일 사이에 KIS 서버에 일시 장애가 있었는데, 그때 미장봇은 30초 간격으로 재시도를 하고 있었다. 이 재시도가 분당 1회 토큰 제한을 계속 어기면서 4월 12일부터 403이 뜨기 시작했고, 13일에는 하루치 락아웃으로 번졌다. 키에는 아무 문제가 없었다. 문제는 내 재시도 루프였다.
그 뒤로는 실패할 때마다 다음 시도까지 기다리는 시간을 1분, 5분, 15분, 30분, 1시간으로 늘려 가도록 바꿨다. 쓸 만한 캐시 토큰도 없고 기다리는 중이라면 빈 문자열을 돌려주고 CRITICAL 로그를 한 줄 남긴다. 401이 수십 줄 쌓이는 것보다 원인이 한 줄로 보이는 편이 나중에 들여다보기 훨씬 편하다.
# 토큰 발급은 분당 1회 제한. 실패가 누적되면 여러 시간짜리 락아웃으로 번진다.
_BACKOFF_SCHEDULE = (60, 300, 900, 1800, 3600) # 1분, 5분, 15분, 30분, 1시간새로 띄운 봇의 첫 1분
봇을 막 띄운 직후 15초에서 60초 정도는 KIS가 대부분의 GET 요청에 HTTP 500과 빈 응답을 돌려주는 구간이 있다. 서버 쪽 세션이 준비되는 동안의 지연이다. 하필 이때 자본 동기화(_sync_capital)가 돌면 capital이 0으로 잡히고, 그날 주문 수량이 int(capital/price), 그러니까 0으로 굳어서 하루 매매가 통째로 막힌다.
안에 있던 1초, 2초, 4초짜리 짧은 재시도로는 이 구간을 넘지 못했다. 그래서 초기화 직후에 가벼운 시세 엔드포인트를 10초 간격으로 최대 90초까지 찔러 보는 워밍업을 끼워 넣었다. 200이 오면 바로 빠져나오고, 끝내 안 와도 봇은 그냥 진행한다. 다른 호출들은 각자 재시도 경로를 갖고 있어서다.
def warm_up(self, max_secs=90, poll_interval=10) -> bool:
# 가벼운 quote 엔드포인트를 재시도 없이 10초 간격으로 폴링하고,
# 200이 오면 바로 끝낸다. 실패해도 봇은 진행한다.
url = f"{self.base_url}/uapi/overseas-price/v1/quotations/price"
... # 200이면 True, 시간 초과면 False키는 한 곳에만 둔다
API 키와 시크릿을 코드에 박아 두면 안 된다. 깃에 한 번 올라가면 사실상 영영 노출된 것이다. 미장봇은 비밀값을 전부 프로젝트 루트의 .env 한 곳에 모아 두고, .gitignore에 넣어서 커밋 자체를 막았다. 처음에는 TRADING_MODE=paper로 모의투자부터 돌렸다.
KIS_APP_KEY=PSxxxxxxxxxxxxxxxxxxxxxxxx
KIS_APP_SECRET=xxxxxxxx...base64...padding==
KIS_ACCOUNT_NO=12345678-01
TRADING_MODE=paper # 처음엔 모의로
TEST_MODE=on # 1주 고정 검증 모드여기서 꽤 헤맨 적이 있다. 시크릿이 base64라 =로 끝나는 경우, 셸 스크립트로 .env를 읽다가 마지막 1바이트가 잘려 나갈 수 있다. 그러면 키도 헤더도 토큰도 전부 멀쩡해 보이는데 인증만 실패하고, 겉으로 드러나는 증상은 HTTP 500이다. 그 뒤로는 셋업을 마치면 셸이 읽은 시크릿 길이와 파일 원본 길이를 한 번씩 맞춰 본다.
# 셸이 export한 길이와 .env 원본 길이가 다르면 파싱하다 1바이트가 잘린 것
echo "exported = ${#KIS_APP_SECRET}"
grep '^KIS_APP_SECRET=' .env | sed 's/^KIS_APP_SECRET=//' | tr -d '\n' | wc -c로그도 조심해야 한다. 디버깅하다가 키나 토큰을 그대로 찍으면 그 로그 파일이 곧 유출 경로가 된다. 미장봇은 비밀값을 앞 네 자리만 남기고 가려서 찍는다.
잔고 API가 셋으로 갈라져 있다
KIS를 쓰면서 헷갈렸던 것 하나는 비슷하게 생긴 API가 용도별로 나뉘어 있다는 점이다. 잔고와 주문가능금액 쪽이 특히 그렇고, 이 구분을 놓치면 HTTP 500이 난다. 내가 정리해 둔 매핑은 이렇다.
| 용도 | 엔드포인트 | tr_id | 필수 파라미터 |
|---|---|---|---|
| USD 예수금·출금가능 | inquire-present-balance | CTRP6504R | WCRC_FRCR_DVSN_CD=02, NATN_CD=840 |
| 특정 종목 주문가능수량 | inquire-psamount | TTTS3007R | ITEM_CD + OVRS_ORD_UNPR (둘 다 필수) |
| 보유 포지션·평가손익 | inquire-balance | TTTS3012R | 없음 (현금 정보는 주지 않는다) |
그리고 KIS는 실전 계좌와 별개로 모의투자 서버를 준다. 베이스 URL에 openapivts가 들어가면 모의 환경이다. 돈을 걸기 전에 주문 로직 전체를 돌려 볼 수 있다는 것이 내가 KIS를 고른 가장 큰 이유였다.
토스증권이 API를 열었다
2026년 5월쯤 토스증권이 Open API를 공개했다. 새벽 자동매매와 AI 계좌분석을 앞세웠고, ChatGPT나 Claude 같은 챗봇에 자기 API 키를 물려서 “내 포트폴리오 수익률을 분석해줘” 식으로 대화하며 쓰는 모습까지 홍보했다. 자동매매와 직접 관계는 없지만, REST에 OpenAPI 스펙까지 갖춘 표준 구조라 외부 도구와 엮기 쉽다는 건 분명한 장점이다.
가장 눈에 띄는 건 국내(KRX)와 미국 주식을 한 인터페이스로 묶었다는 점이다. 인증은 KIS와 같은 OAuth 2.0 client_credentials이고, /oauth2/token 으로 받는 토큰의 유효기간은 1시간 정도다. 기능은 시세, 종목 정보, 시장 정보, 계좌, 주문, 인증의 여섯 갈래로 나뉜다. 환율(/exchange-rate)과 양쪽 시장 캘린더(/market-calendar)를 한 API에서 주는 것도 국내와 미국을 같이 굴리는 봇에는 꽤 쓸모 있어 보였다.
신청은 간단하다. 토스증권 계좌가 있으면 앱에서 더보기, Open API, 신청 순서로 들어가 Client ID와 Secret을 받으면 된다. 매매 수수료는 기존 계좌 요율 그대로이고 API를 쓴다고 따로 붙는 수수료는 없다. 국내 주식 수수료는 2026년 6월까지 면제 프로모션이 있었다.
다만 이 글을 처음 쓸 무렵에는 비어 있는 것들이 있었다. 모의투자 환경이 없어서 테스트를 하려면 소액이라도 실제로 체결해야 하고, 공식 WebSocket이 없어 실시간 시세는 REST로 계속 물어봐야 한다. 언어별 SDK도 없었는데, OpenAPI 스펙(JSON)은 있으니 코드를 자동으로 뽑아 쓸 수는 있다. 이런 부분은 나중에 추가될 수 있으니 도입 전에 공식 문서를 꼭 다시 확인하는 게 좋다.
둘을 나란히 놓으면
자동매매를 기준으로 두 API를 비교하면 이렇다. 인증 구조는 거의 같고, 갈리는 곳은 모의투자와 실시간 수단이다. 토스 쪽 항목은 발행 시점의 서드파티 가이드와 뉴스를 기준으로 적었다.
| 항목 | 한국투자증권(KIS) | 토스증권 |
|---|---|---|
| 인증 | OAuth 2.0 (tokenP) | OAuth 2.0 (/oauth2/token) |
| 토큰 유효기간 | 약 24시간 | 약 1시간 |
| 지원 시장 | 국내 + 미국 + 선물 등 | 국내 + 미국 (통합 인터페이스) |
| 모의투자 | 있음 | 없음 (소액 실거래) |
| 공식 실시간 WebSocket | 있음 | 없음 (REST 폴링) |
| 언어 SDK | 일부, 커뮤니티 예제가 많음 | 없음 (OpenAPI 자동생성) |
| 헤더 요구 | Bearer + appkey + appsecret | Bearer 토큰 |
| 성격 | 오래됐고 기능 폭이 넓음 | 2026년 신규, 웹 표준 REST 우선 |

그래서 나는 무엇을 골랐나
미장봇은 모의투자로 주문 로직부터 검증해야 했기 때문에 KIS를 골랐다. 틱 단위로 실시간 체결을 받아야 하는 전략이거나, 선물처럼 폭넓은 상품과 커뮤니티 예제가 필요하다면 지금도 KIS 쪽이 맞는 것 같다.
그런데 지금 처음부터 다시 시작한다면 생각이 조금 다르다. 분이나 일 단위로 리밸런스하는 저빈도 전략이라면 REST로 물어보는 것으로 충분하고, 국내와 미국을 한 API로 다룰 수 있는 토스증권도 충분히 현실적인 후보다. 소액으로 실거래 테스트를 하는 게 부담스럽지 않다면 말이다. 두 API의 인증과 요청 구조가 거의 같아서, 한쪽으로 만든 봇을 다른 쪽으로 옮기는 비용도 생각만큼 크지 않다.
돌아보면 연동하면서 시간을 가장 많이 잡아먹은 건 증상은 서버 탓처럼 보이는데 원인은 내 쪽에 있던 경우들이었다. 키가 틀렸다는 에러가 떠도 사실은 토큰을 너무 자주 받아서였고, 서버가 500을 돌려줘도 사실은 시크릿 1바이트가 잘려서였다. 이런 에러들은 4편에 따로 모아 두었다.
다음 편에서는 이 API 위에서 시세, 잔고, 주문을 부르는 기본 함수를 어떻게 감쌌는지 코드로 이어 간다.
이 글은 자동매매 시스템을 직접 만들며 남긴 개발 기록이고, 특정 증권사나 상품을 권하는 글이 아니다. API 스펙과 수수료, 정책은 각 증권사 공식 문서가 기준이며 시점에 따라 달라질 수 있다.