Platform · 암호화폐

업비트 자동매매 봇
맞춤 제작

업비트(Upbit)의 원화·BTC·USDT 마켓 전 종목을 대상으로 24시간 자동매매하는 봇을 맞춤 제작합니다. JWT 인증, WebSocket 실시간 시세, 지표 기반 매매, 리스크 관리, 텔레그램 알림까지 한 번에. 알고랩(AlgoLab)이 지난 4년간 제작한 업비트 자동매매 60건+ 경험을 반영합니다.

한 줄 요약 업비트 봇 제작에서 실제로 갈리는 지점은 세 가지입니다 — ① 인증: 요청마다 JWT를 새로 만들고, 파라미터가 있으면 쿼리 문자열을 SHA512로 해시한 query_hash를 페이로드에 넣어야 합니다(여기서 invalid_query_payload가 가장 많이 납니다). ② 주문: 시장가 매수는 ord_type=price + 원화 금액, 시장가 매도는 ord_type=market + 코인 수량으로 파라미터가 아예 다릅니다. ③ 호출량: Remaining-Req 헤더를 읽어 속도를 스스로 줄이지 않으면 429가 뜹니다. 아래에 실제 요청·응답을 그대로 실었습니다.

업비트 자동매매로 자주 요청되는 유형

기술 구조

업비트는 REST + WebSocket 조합이 표준입니다. 알고랩은 아래 구조로 안정 운영되는 봇을 제작합니다.

항목사양
API 문서docs.upbit.com (공식)
인증JWT (Access Key + Secret Key, IP 화이트리스트 설정 권장)
언어Python 3.10+ 64bit (Windows · Linux · macOS 모두 가능)
Rate Limit초당 10회 (EXCHANGE), 초당 30회 (QUOTATION) — 초과 시 429
실시간 시세WebSocket (wss://api.upbit.com/websocket/v1) 연결 유지 + 재연결 로직
마켓 구분KRW(원화), BTC(비트코인), USDT(테더)
주문 방식limit(지정가), price(시장가 매수 — 원화 금액 지정), market(시장가 매도 — 코인 수량 지정)

업비트 시장가 주문 주의: 매수·매도의 파라미터가 다릅니다. 시장가 매수price 타입으로 원화 금액을 지정, 시장가 매도market 타입으로 코인 수량을 지정합니다. 이를 헷갈리면 주문이 리젝되거나 의도치 않은 금액이 체결됩니다.

핵심 기능 구성 요소

업비트 API는 실제로 어떻게 호출하나

기술 구조만 보면 감이 안 오실 수 있어, 실제 요청과 응답을 그대로 싣습니다. 업비트 Open API가 다른 거래소와 가장 크게 다른 점은 인증 토큰 안에 요청 내용의 해시를 넣는다는 것입니다.

① 인증 — 요청마다 JWT를 새로 만든다

페이로드에 access_key와 매번 새로 만드는 nonce를 넣습니다. 파라미터가 있는 요청이라면 실제로 보낼 쿼리 문자열을 SHA512로 해시query_hash에 담고, query_hash_alg에는 "SHA512"를 적습니다.

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

SERVER = "https://api.upbit.com"

def auth_header(params: dict | None = None) -> dict:
    payload = {"access_key": ACCESS_KEY, "nonce": str(uuid.uuid4())}
    if params:
        query = urlencode(params)                    # ★ 딱 한 번만 만든다
        m = hashlib.sha512()
        m.update(query.encode())
        payload["query_hash"] = m.hexdigest()
        payload["query_hash_alg"] = "SHA512"
    token = jwt.encode(payload, SECRET_KEY)          # HS256
    return {"Authorization": f"Bearer {token}"}

# 파라미터가 없는 요청은 query_hash 자체를 넣지 않는다
res = requests.get(f"{SERVER}/v1/accounts", headers=auth_header(), timeout=5)
print(res.json())
# [{"currency":"KRW","balance":"1000000.0","locked":"0.0", ...},
#  {"currency":"BTC","balance":"0.015","locked":"0.0","avg_buy_price":"...", ...}]

가장 많이 보는 오류가 이겁니다. 토큰을 만들 때 해시한 쿼리와 실제로 보낸 쿼리가 한 글자라도 다르면 이렇게 돌아옵니다.

{
  "error": {
    "name": "invalid_query_payload",
    "message": "Jwt의 query를 검증하는데 실패하였습니다."
  }
}

원인은 거의 항상 하나입니다 — 쿼리 문자열을 두 번 따로 만든 것. 업비트는 요청에 담긴 문자열을 그대로 해시하며 파라미터 순서를 재정렬하지 않습니다. 위 코드처럼 query 변수를 한 번만 만들어 해시와 요청 양쪽에 같은 값을 쓰세요.

② 주문 — 매수와 매도의 파라미터가 다르다

지정가는 ord_type=limitprice(호가)와 volume(수량)을 모두 넣습니다. 시장가는 앞의 콜아웃대로 매수와 매도가 서로 다른 필드를 씁니다.

# 지정가 매수 — KRW-BTC를 95,000,000원에 0.001개
params = {
    "market": "KRW-BTC",
    "side": "bid",            # bid=매수, ask=매도
    "ord_type": "limit",
    "price": "95000000",
    "volume": "0.001",
}
query = urlencode(params)
res = requests.post(f"{SERVER}/v1/orders", params=params,
                    headers=auth_header(params), timeout=5)

# 시장가 매수: 금액으로 지정 (volume 없음)
{"market": "KRW-BTC", "side": "bid", "ord_type": "price", "price": "50000"}

# 시장가 매도: 수량으로 지정 (price 없음)
{"market": "KRW-BTC", "side": "ask", "ord_type": "market", "volume": "0.001"}

주문이 접수되면 uuid가 내려옵니다. 이 값이 없으면 나중에 그 주문을 취소할 수 없습니다.

{
  "uuid": "cdd92199-2897-4e14-9448-f923320408ad",
  "side": "bid",
  "ord_type": "limit",
  "price": "95000000.0",
  "state": "wait",              ← wait=미체결 대기, done=체결, cancel=취소됨
  "market": "KRW-BTC",
  "volume": "0.001",
  "remaining_volume": "0.001",  ← 남은 수량 (부분 체결이면 줄어든다)
  "executed_volume": "0.0"
}

③ 미체결 취소 — uuid로 지목한다

지정가가 안 붙었을 때 거둬들이는 요청입니다. DELETE /v1/orderuuid를 넘깁니다.

params = {"uuid": "cdd92199-2897-4e14-9448-f923320408ad"}
res = requests.delete(f"{SERVER}/v1/order", params=params,
                      headers=auth_header(params), timeout=5)

# 남은 미체결 목록 조회 — 봇 재시작 시 상태 복원에 쓴다
requests.get(f"{SERVER}/v1/orders/open",
             params={"market": "KRW-BTC", "state": "wait"},
             headers=auth_header({"market": "KRW-BTC", "state": "wait"}), timeout=5)

이 구조는 국내 증권사와 개념이 같습니다. 한국투자증권 KIS API에서 같은 작업을 어떻게 하는지는 KIS API 주문 취소·정정에 정리해 뒀습니다. 부분 체결이 남은 상태에서 취소하면 이미 체결된 수량은 그대로 남는다는 점도 동일합니다.

④ 호출 한도 — 헤더를 읽어 스스로 줄인다

업비트는 응답 헤더 Remaining-Req남은 요청 수를 알려 줍니다. 이 값을 무시하고 밀어붙이면 429가 뜹니다.

res = requests.get(f"{SERVER}/v1/orders/open", params=params,
                   headers=auth_header(params), timeout=5)

print(res.headers.get("Remaining-Req"))
# group=order; min=59; sec=4      ← 이 그룹에서 남은 분당·초당 요청 수

if res.status_code == 429:
    time.sleep(1.0)   # 고정 sleep보다 지수 백오프 + 지터를 권장

한도는 그룹별로 따로 걸립니다. 주문 계열과 시세 조회 계열이 별도로 관리되므로, 시세를 자주 본다고 주문 한도가 줄지는 않습니다. 다만 수치는 정책에 따라 변경될 수 있으니 업비트 공식 문서에서 현재 값을 확인하고 설정으로 빼 두시기 바랍니다. 한도 안에서 도는 구조를 처음부터 설계하는 방법은 호출 제한 설계에 정리돼 있습니다.

⑤ WebSocket — 종목이 늘면 REST 폴링으로는 못 버틴다

종목 20개를 초 단위로 확인하려면 REST로는 금세 한도에 닿습니다. 실시간은 wss://api.upbit.com/websocket/v1 구독으로 옮기는 것이 정석입니다.

import json, websockets

async def stream():
    async with websockets.connect("wss://api.upbit.com/websocket/v1",
                                  ping_interval=60) as ws:
        await ws.send(json.dumps([
            {"ticket": str(uuid.uuid4())},
            {"type": "ticker", "codes": ["KRW-BTC", "KRW-ETH", "KRW-XRP"]},
            {"format": "DEFAULT"},
        ]))
        async for raw in ws:
            data = json.loads(raw.decode("utf-8"))   # 응답은 바이너리로 온다
            print(data["code"], data["trade_price"], data["signed_change_rate"])

응답이 바이너리로 오므로 decode를 빠뜨리면 파싱이 실패합니다. 연결 유지·재연결 처리를 포함한 상세 구현은 업비트 REST·WebSocket 실전에, 파이썬 봇의 전체 뼈대는 업비트 자동매매 봇 30분 만들기에 있습니다. 지표를 계산해 신호를 낼 때 라이브러리마다 값이 갈리는 문제는 파이썬 RSI 계산 쪽을 참고하세요.

제작 비용·기간 가이드

유형예상 비용제작 기간
단일 지표 기반 단일 종목 봇50~100만원5~7일
+ 다중 종목 + 텔레그램 알림100~180만원7~10일
+ 백테스팅 · 파라미터 최적화 · GUI180~280만원10~16일
그리드 봇 · 차익거래 등 복합 전략280~500만원21~30일

자주 묻는 질문

API Key 발급은 어떻게 하나요?

업비트 로그인 후 "마이페이지 → Open API 관리" 에서 발급. IP 화이트리스트는 반드시 설정하고, 출금 권한은 꺼두시는 것을 권장합니다. 자산조회·주문·체결 권한만 활성화해도 자동매매에 충분합니다.

업비트는 시장가 주문이 잘 안 나가던데요?

위 콜아웃에서 설명드린 것처럼 매수·매도의 파라미터가 달라서 혼동이 흔합니다. 알고랩에서 제작한 봇은 내부적으로 매수/매도에 맞는 파라미터를 자동 선택합니다.

지정가 주문이 체결 안 되면 어떻게 처리하나요?

N초 후에도 미체결이면 자동 취소하고 재주문하도록 구성합니다. 또는 호가 변동이 있으면 기존 주문 취소 후 새 호가로 재주문하는 "트레일링 주문" 옵션도 지원합니다.

선물은 없나요?

업비트는 현물만 거래 가능합니다. 선물·레버리지가 필요하면 바이낸스 USDT-M 선물 또는 바이비트로 별도 제작합니다.

invalid_query_payload 오류가 계속 납니다.

토큰을 만들 때 해시한 쿼리 문자열과 실제로 보낸 값이 다르기 때문입니다. 위 ①번 코드처럼 urlencode 결과를 변수 하나에 담아 해시와 요청에 같은 값을 쓰면 대부분 해결됩니다. 파라미터를 딕셔너리로 두 번 따로 직렬화하는 순간 순서나 인코딩이 어긋날 수 있습니다.

봇을 재시작하면 이전 주문 상태는 어떻게 되나요?

메모리에 있던 주문 정보는 사라지지만, 거래소에 접수된 주문은 그대로 살아 있습니다. 기동 직후 /v1/orders/open으로 미체결 목록을 복원한 뒤 전략 루프를 시작하도록 만듭니다. 이 과정을 빠뜨리면 봇이 모르는 주문이 장중에 갑자기 체결됩니다. 장애·복구 설계 전반은 봇 장애·복구 플레이북을 참고하세요.

제작 사례

업비트 자동매매 실제 제작 사례는 포트폴리오에서 확인하실 수 있습니다.

전략 아이디어부터 바로 상담해보세요

요구사항이 구체적이지 않아도 괜찮습니다.
알고랩이 24시간 빠르게 답변드립니다.

무료 상담 시작하기 요금제 보기