AlgoLab Blog · 실전 가이드

업비트 자동매매 — REST vs WebSocket, 실전 아키텍처

업비트 2026-04-22 · 약 5분 읽기 · 알고랩 AlgoLab

업비트는 국내 코인 거래소 중 API 가 가장 깔끔한 편이지만, 처음 자동매매를 만들면 JWT 인증 헷갈림 → 시장가 주문 리젝 → Rate Limit 429 → WebSocket 끊김 순서로 막히는 게 거의 정해진 수순입니다. 이 글은 지난 3년간 알고랩에서 업비트 자동매매 60건+ 제작하며 반복 마주친 이슈 4가지를 정리합니다.

1. JWT 인증 — JSON Web Token 이지 API Key + Secret 이 아님

업비트는 요청마다 JWT 를 생성해서 Authorization 헤더에 넣는 방식입니다. 다른 거래소처럼 단순히 API Key + Secret 을 헤더에 붙이는 게 아니라서, 첫 구현 시 혼동이 자주 발생.

import jwt, uuid, hashlib, requests
from urllib.parse import urlencode

ACCESS_KEY = "your_access_key"
SECRET_KEY = "your_secret_key"

# 주문 요청 시
params = {"market": "KRW-BTC", "side": "bid", "price": "50000", "ord_type": "price"}
query_string = urlencode(params)
m = hashlib.sha512()
m.update(query_string.encode())
query_hash = m.hexdigest()

payload = {
    "access_key": ACCESS_KEY,
    "nonce": str(uuid.uuid4()),
    "query_hash": query_hash,
    "query_hash_alg": "SHA512",
}
token = jwt.encode(payload, SECRET_KEY)
headers = {"Authorization": f"Bearer {token}"}
res = requests.post("https://api.upbit.com/v1/orders", params=params, headers=headers)

매 요청마다 nonce (UUID) 와 query_hash 계산이 필요합니다. pyupbit 같은 라이브러리 쓰면 이 과정이 감춰지지만, 디버깅 시 어떤 레이어에서 막혔는지 알기 어려워집니다.

2. 시장가 주문 — 매수와 매도 파라미터가 완전히 다름 (함정)

업비트 시장가 주문에서 가장 자주 만나는 실수입니다.

시장가 매수

시장가 매도

흔한 실수: 매수에 volume 넣고 ord_type: "market" 하면 400 에러 또는 "코인 수량만큼 매수" 로 해석되어 계좌 잔고 탕진 가능. 이 에러는 백테스트에서 잡히지 않음 (백테스트는 주로 지정가 기반).

3. Rate Limit — 초당 10회, 429 회피 전략

업비트 Rate Limit (EXCHANGE 기준):

해결: 요청 간 최소 간격 보장 + 429 만나면 지수 백오프.

import time

class UpbitClient:
    def __init__(self):
        self._last_request = 0
        self._min_interval = 0.12  # 초당 ~8회 (여유 2회)

    def _throttle(self):
        elapsed = time.time() - self._last_request
        if elapsed < self._min_interval:
            time.sleep(self._min_interval - elapsed)
        self._last_request = time.time()

    def request(self, ...):
        self._throttle()
        for attempt in range(3):
            res = requests.post(...)
            if res.status_code == 429:
                time.sleep(2 ** attempt)  # 1s → 2s → 4s
                continue
            return res

특히 다중 종목 시세 폴링 (20개 종목 × 초당 2회 조회) 같은 구조는 즉시 429 를 유발합니다. 이 때문에 다중 종목 시세는 REST 가 아닌 WebSocket 을 써야 합니다 (아래 4번).

4. WebSocket — 실시간 시세는 여기서, 연결 관리가 핵심

업비트 WebSocket (wss://api.upbit.com/websocket/v1) 은 실시간 시세 수신 표준입니다.

장점

주의사항 3가지

  1. 연결 끊김 감지: 업비트 WebSocket 은 일정 시간 메시지 없으면 자동 종료. ping/pong heartbeat 로 유지.
  2. 재연결 시 구독 상태 재등록: 재연결 후 구독 종목을 다시 지정해야 함. 자동화 필수.
  3. JSON 파싱 실패 대비: 간헐적 이상 메시지로 봇이 죽는 경우가 있음. try/except 로 둘러싸기.
import asyncio, websockets, json

async def upbit_ws():
    url = "wss://api.upbit.com/websocket/v1"
    markets = ["KRW-BTC", "KRW-ETH", "KRW-XRP"]
    backoff = 1
    while True:
        try:
            async with websockets.connect(url, ping_interval=60) as ws:
                backoff = 1
                # 구독 등록
                await ws.send(json.dumps([
                    {"ticket": "algolab"},
                    {"type": "ticker", "codes": markets},
                ]))
                async for raw in ws:
                    try:
                        data = json.loads(raw)
                        handle_tick(data)
                    except Exception as e:
                        logger.warning(f"parse error: {e}")
        except Exception as e:
            logger.warning(f"WS 재연결 ({backoff}s 후): {e}")
            await asyncio.sleep(backoff)
            backoff = min(backoff * 2, 30)

추천 아키텍처 — 시세는 WS, 주문·잔고는 REST

실전에서 안정 운영되는 구조는 다음과 같습니다.

WebSocket 으로 체결 이벤트를 직접 받을 수도 있지만, Private WebSocket 은 설정이 번거롭고 안정성이 REST 폴링만 못합니다. 초단타 스캘핑 아니면 "시세 WS + 주문 REST" 조합으로 충분합니다.

업비트 봇 맞춤 제작

위 구조를 기본으로 + 고객 전략 반영 + 텔레그램 알림까지 포함.
24시간 빠른 답변 가능합니다.

무료 상담 시작하기