업비트 자동매매 봇
맞춤 제작
업비트(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가 뜹니다. 아래에 실제 요청·응답을 그대로 실었습니다.
업비트 자동매매로 자주 요청되는 유형
- 다중 종목 지표 기반 매매 — 상위 시가총액 코인 10~20개 대상 RSI·볼린저·MACD 조건 매매
- 그리드 봇 — 특정 구간 횡보장 종목에 일정 간격 매수·매도 반복
- 이동평균 골든크로스 추종 — 5일·20일·60일 이평선 크로스 시점 진입·청산
- 돌파 매매 — 전일 고점·특정 저항선 돌파 시 자동 매수, 5~10% 수익 실현
- 알림 전용 봇 — 매매 없이 조건 충족 시 텔레그램 알림만 발송
- 김치프리미엄 차익거래 — 업비트 ↔ 바이낸스 가격 차이 자동 실행 (양방향 취소 로직 필수)
기술 구조
업비트는 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 타입으로 코인 수량을 지정합니다. 이를 헷갈리면 주문이 리젝되거나 의도치 않은 금액이 체결됩니다.
핵심 기능 구성 요소
- 실시간 호가·체결 처리: WebSocket ticker/orderbook/trade 스트림 구독, 재연결·heartbeat 로직
- 지표 계산: pandas-ta 로 RSI·MACD·볼린저·이동평균·ATR 실시간 계산
- 주문 모듈: 지정가·시장가(매수 원화/매도 수량 구분)·손절·익절 자동 관리
- 리스크 관리: 일일 최대 손실 한도, 동시 포지션 한도, 종목별 배분 비율
- 잔고 동기화: 주기적 /v1/accounts 폴링 + 주문 체결 후 즉시 갱신
- 텔레그램 알림: 진입·청산·오류 즉시 알림, 일일 손익 리포트 자동 발송
업비트 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=limit에 price(호가)와 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/order에 uuid를 넘깁니다.
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일 |
| + 백테스팅 · 파라미터 최적화 · GUI | 180~280만원 | 10~16일 |
| 그리드 봇 · 차익거래 등 복합 전략 | 280~500만원 | 21~30일 |
자주 묻는 질문
API Key 발급은 어떻게 하나요?
업비트 로그인 후 "마이페이지 → Open API 관리" 에서 발급. IP 화이트리스트는 반드시 설정하고, 출금 권한은 꺼두시는 것을 권장합니다. 자산조회·주문·체결 권한만 활성화해도 자동매매에 충분합니다.
업비트는 시장가 주문이 잘 안 나가던데요?
위 콜아웃에서 설명드린 것처럼 매수·매도의 파라미터가 달라서 혼동이 흔합니다. 알고랩에서 제작한 봇은 내부적으로 매수/매도에 맞는 파라미터를 자동 선택합니다.
지정가 주문이 체결 안 되면 어떻게 처리하나요?
N초 후에도 미체결이면 자동 취소하고 재주문하도록 구성합니다. 또는 호가 변동이 있으면 기존 주문 취소 후 새 호가로 재주문하는 "트레일링 주문" 옵션도 지원합니다.
선물은 없나요?
업비트는 현물만 거래 가능합니다. 선물·레버리지가 필요하면 바이낸스 USDT-M 선물 또는 바이비트로 별도 제작합니다.
invalid_query_payload 오류가 계속 납니다.
토큰을 만들 때 해시한 쿼리 문자열과 실제로 보낸 값이 다르기 때문입니다. 위 ①번 코드처럼 urlencode 결과를 변수 하나에 담아 해시와 요청에 같은 값을 쓰면 대부분 해결됩니다. 파라미터를 딕셔너리로 두 번 따로 직렬화하는 순간 순서나 인코딩이 어긋날 수 있습니다.
봇을 재시작하면 이전 주문 상태는 어떻게 되나요?
메모리에 있던 주문 정보는 사라지지만, 거래소에 접수된 주문은 그대로 살아 있습니다. 기동 직후 /v1/orders/open으로 미체결 목록을 복원한 뒤 전략 루프를 시작하도록 만듭니다. 이 과정을 빠뜨리면 봇이 모르는 주문이 장중에 갑자기 체결됩니다. 장애·복구 설계 전반은 봇 장애·복구 플레이북을 참고하세요.
제작 사례
업비트 자동매매 실제 제작 사례는 포트폴리오에서 확인하실 수 있습니다.