바이낸스 API 사용법 — 엔드포인트 7개와 응답 읽는 법
/api/v3/klines·/api/v3/ticker/price로 받고, 내 계정에 손대는 /api/v3/account·/api/v3/order는 timestamp + HMAC SHA256 서명 + X-MBX-APIKEY 헤더가 필수입니다. 그리고 주문이 거부되는 원인의 대부분은 전략이 아니라 /api/v3/exchangeInfo의 심볼 필터(LOT_SIZE·PRICE_FILTER·명목가치)를 안 맞춘 것입니다.
바이낸스 API를 처음 붙일 때 헷갈리는 지점은 "어떤 주소로, 무엇을 보내고, 돌아온 JSON에서 무엇을 봐야 하는가"입니다. 라이브러리가 감싸 놓은 함수만 따라 쓰면 잘 되다가도, 주문 한 줄에서 -1013이 뜨는 순간 아무것도 못 하게 됩니다. 껍데기 아래에서 실제로 오가는 요청이 무엇인지 모르기 때문입니다.
그래서 이 글은 봇을 만드는 순서가 아니라 엔드포인트 자체를 하나씩 봅니다. 봇 골격부터 만들고 싶다면 바이낸스 자동매매 봇 30분 안에 만들기를, API 키가 아직 없다면 바이낸스 API 발급 완벽 가이드를 먼저 보시면 됩니다.
이 글의 순서
- 구조 — 공개 요청과 서명 요청 두 종류뿐
- 서명 만들기 — timestamp · HMAC SHA256 · X-MBX-APIKEY
- 엔드포인트 7개 (exchangeInfo · klines · ticker · account · order · openOrders · myTrades)
- 주문 응답 읽는 법 — status와 fills
- 거부되는 4가지 이유 — -1013 · -1021 · -1022 · -2010
- 호출 가중치와 429 · 418
- 테스트넷으로 배선만 점검하기
1. 구조 — 요청은 두 종류뿐
바이낸스 스팟 REST API의 기본 주소는 https://api.binance.com이고, 스팟 경로는 /api/v3/... 아래에 모여 있습니다. 사용법 관점에서 중요한 건 요청이 딱 두 종류라는 점입니다.
| 종류 | 예시 | 필요한 것 |
|---|---|---|
| 공개(NONE / MARKET_DATA) | /api/v3/klines, /api/v3/ticker/price, /api/v3/exchangeInfo | 없음(또는 API Key만) |
| 서명(SIGNED / USER_DATA) | /api/v3/account, /api/v3/order, /api/v3/openOrders, /api/v3/myTrades | X-MBX-APIKEY 헤더 + timestamp + signature |
2. 서명 만들기 — 여기서 절반이 막힙니다
서명 요청의 규칙은 단순합니다. 보낼 파라미터를 쿼리스트링으로 만들고, 그 문자열 전체를 Secret Key로 HMAC SHA256 해시한 값을 signature라는 파라미터로 맨 뒤에 붙입니다. API Key는 쿼리에 넣지 않고 X-MBX-APIKEY 헤더로 보냅니다.
import time, hmac, hashlib, requests
from urllib.parse import urlencode
BASE = "https://api.binance.com"
API_KEY = "..." # 코드에 직접 쓰지 말고 환경변수로
API_SECRET = "..."
def signed_get(path, params=None):
params = dict(params or {})
params["timestamp"] = int(time.time() * 1000)
params["recvWindow"] = 5000
query = urlencode(params) # 서명 대상 = 이 문자열 그대로
sig = hmac.new(API_SECRET.encode(), query.encode(), hashlib.sha256).hexdigest()
url = f"{BASE}{path}?{query}&signature={sig}"
r = requests.get(url, headers={"X-MBX-APIKEY": API_KEY}, timeout=10)
r.raise_for_status()
return r.json()
가장 흔한 실수: urlencode로 만든 문자열과 실제로 전송하는 쿼리스트링이 한 글자라도 다르면 서명이 깨져 -1022가 납니다. params=로 따로 넘기면 requests가 순서나 인코딩을 다르게 조립할 수 있으니, 위 코드처럼 URL에 직접 이어 붙이는 방식이 안전합니다.
recvWindow는 "내 요청이 몇 밀리초 안에 도착해야 유효한가"를 지정하는 값입니다. 넣지 않으면 기본값이 쓰이고, 서버 시계와 내 PC 시계가 어긋나면 -1021이 납니다. 이 문제는 값을 키우는 것보다 서버 시간 동기화로 푸는 게 정석입니다.
# 시계 오차 확인 — 수백 ms 이상 벌어지면 NTP 동기화 필요
srv = requests.get(f"{BASE}/api/v3/time", timeout=10).json()["serverTime"]
print("오차(ms):", srv - int(time.time() * 1000))
# 리눅스 서버라면: sudo timedatectl set-ntp true
24시간 돌아가는 봇이라면 시계 드리프트가 며칠에 걸쳐 누적되므로 서버 쪽 NTP 설정을 아예 켜 두는 편이 낫습니다.
3. 엔드포인트 7개
1GET /api/v3/exchangeInfo — 가장 먼저 부를 주소
바이낸스 API 사용법에서 초보와 실무자를 가르는 지점이 여기입니다. 이 엔드포인트는 심볼별 거래 규칙(필터)을 알려주는데, 주문 거부의 대부분이 이 값을 안 지켜서 생깁니다.
info = requests.get(f"{BASE}/api/v3/exchangeInfo",
params={"symbol": "BTCUSDT"}, timeout=10).json()["symbols"][0]
for f in info["filters"]:
print(f["filterType"], f)
돌아오는 필터 중 봇이 반드시 읽어야 하는 것은 세 가지입니다.
| filterType | 핵심 필드 | 의미 |
|---|---|---|
LOT_SIZE | minQty / stepSize | 수량은 stepSize 배수여야 함 |
PRICE_FILTER | tickSize | 지정가는 tickSize 배수여야 함 |
| 명목가치 계열 | minNotional | 수량 × 가격이 최소 금액 이상이어야 함 |
필터 이름은 바뀔 수 있습니다. 최소 주문금액 필터는 MIN_NOTIONAL과 NOTIONAL 두 이름으로 존재해 왔습니다. 그래서 filterType을 하드코딩해 하나만 찾으면 어느 날 조용히 None이 됩니다. 필터 목록을 순회해 있는 것을 쓰고, 최신 규격은 바이낸스 공식 API 문서에서 확인하세요.
from decimal import Decimal, ROUND_DOWN
def load_rules(info):
rules = {}
for f in info["filters"]: # 이름을 찾아 쓰지 말고 순회
t = f["filterType"]
if t == "LOT_SIZE": rules["step"] = Decimal(f["stepSize"])
elif t == "PRICE_FILTER": rules["tick"] = Decimal(f["tickSize"])
elif t in ("NOTIONAL", "MIN_NOTIONAL"): # 두 이름 모두 수용
rules["min_notional"] = Decimal(f.get("minNotional", "0"))
return rules
def fit_qty(qty, step):
"""stepSize 배수로 내림 — 이걸 안 하면 -1013"""
return (Decimal(str(qty)) / step).quantize(Decimal("1"), ROUND_DOWN) * step
2GET /api/v3/ticker/price — 현재가 한 줄
>>> requests.get(f"{BASE}/api/v3/ticker/price",
... params={"symbol": "BTCUSDT"}).json()
{'symbol': 'BTCUSDT', 'price': '64231.15000000'}
숫자가 아니라 문자열로 옵니다. 바이낸스 API는 정밀도 손실을 막으려고 가격·수량을 전부 문자열로 내려줍니다. 그대로 계산에 넣으면 타입 에러가 나고, 무심코 float()로 바꾸면 소수점 오차가 stepSize 계산을 망가뜨립니다. 금액 계산은 Decimal이 원칙입니다.
3GET /api/v3/klines — 캔들, 응답이 배열입니다
rows = requests.get(f"{BASE}/api/v3/klines",
params={"symbol": "BTCUSDT", "interval": "1h", "limit": 200}).json()
# 한 행 예시(축약):
# [1754092800000, "64100.00", "64550.00", "63980.00", "64231.15", "812.4", 1754096399999, ...]
# [0] openTime [1] open [2] high [3] low [4] close [5] volume [6] closeTime
klines는 딕셔너리가 아니라 인덱스로 접근하는 배열이라 처음 보면 당황합니다. 실무에서는 곧바로 pandas 데이터프레임으로 바꿔 openTime을 unit="ms"로 변환해 씁니다.
마지막 캔들은 아직 안 끝났습니다. limit=200으로 받으면 맨 마지막 행은 진행 중인 봉이라 close가 계속 바뀝니다. 신호 계산에 그대로 넣으면 같은 조건이 껐다 켜졌다 하는 이른바 재도색이 생깁니다. 확정봉만 쓰려면 마지막 행을 버리세요(df.iloc[:-1]). 이 함정만 따로 다룬 글 → 바이낸스 봇 만들 때 놓치기 쉬운 5가지.
4GET /api/v3/account — 잔고 (서명 필요)
>>> acc = signed_get("/api/v3/account")
>>> [b for b in acc["balances"] if float(b["free"]) > 0][:2]
[{'asset': 'USDT', 'free': '532.41000000', 'locked': '0.00000000'},
{'asset': 'BTC', 'free': '0.00412000', 'locked': '0.00100000'}]
balances는 보유량이 0인 자산까지 전부 담겨 오므로 필터링이 필요합니다. 그리고 free(주문 가능)와 locked(미체결 주문에 묶임)를 구분해야 합니다. 이 둘을 합쳐서 주문을 내면 -2010(잔고 부족)이 납니다.
5POST /api/v3/order — 주문
def signed_post(path, params):
params = dict(params, timestamp=int(time.time() * 1000), recvWindow=5000)
query = urlencode(params)
sig = hmac.new(API_SECRET.encode(), query.encode(), hashlib.sha256).hexdigest()
r = requests.post(f"{BASE}{path}?{query}&signature={sig}",
headers={"X-MBX-APIKEY": API_KEY}, timeout=10)
r.raise_for_status()
return r.json()
# 시장가 매수 — 수량 기준
signed_post("/api/v3/order", {
"symbol": "BTCUSDT", "side": "BUY", "type": "MARKET",
"quantity": "0.00100000"})
# 지정가 매도 — timeInForce 필수
signed_post("/api/v3/order", {
"symbol": "BTCUSDT", "side": "SELL", "type": "LIMIT",
"timeInForce": "GTC", "quantity": "0.00100000", "price": "65000.00",
"newClientOrderId": "algolab-20260802-001"}) # 내 쪽 식별자
| 파라미터 | 언제 쓰나 |
|---|---|
type=MARKET + quantity | 기초자산 수량 기준 (0.001 BTC 매수) |
type=MARKET + quoteOrderQty | 견적통화 금액 기준 (50 USDT어치 매수) |
type=LIMIT | price와 timeInForce(GTC/IOC/FOK)가 함께 필요 |
newClientOrderId | 중복 주문 방지·재시도 추적용 — 봇이라면 사실상 필수 |
newClientOrderId를 꼭 쓰세요. 네트워크가 끊겨 응답을 못 받았을 때, 같은 ID로 재시도하면 거래소가 중복을 걸러 줍니다. 이 값이 없으면 "주문이 들어갔는지 모르겠어서 한 번 더 냈다가 2배로 체결"되는 사고가 납니다. 재시도·복구 설계는 봇 장애 복구 플레이북에서 다뤘습니다.
6GET /api/v3/openOrders · DELETE /api/v3/order
# 미체결 조회 — symbol을 생략하면 전체 조회라 가중치가 크게 올라갑니다
opens = signed_get("/api/v3/openOrders", {"symbol": "BTCUSDT"})
# 취소는 DELETE /api/v3/order — orderId 또는 origClientOrderId 중 하나로 지정
# (서명 방식은 위 signed_get과 동일, requests.delete만 다름)
7GET /api/v3/myTrades — 실제 체결가와 수수료
>>> signed_get("/api/v3/myTrades", {"symbol": "BTCUSDT", "limit": 1})
[{'symbol': 'BTCUSDT', 'id': 1839221, 'orderId': 28457921,
'price': '64228.90000000', 'qty': '0.00100000',
'commission': '0.00000100', 'commissionAsset': 'BTC',
'isBuyer': True, 'isMaker': False, 'time': 1754093112345}]
손익을 계산하려면 주문서에 적은 가격이 아니라 여기 찍힌 price와 commission을 써야 합니다. isMaker에 따라 수수료율이 다르고, commissionAsset이 BTC일 수도 BNB일 수도 있어서 통화를 맞춰 환산해야 합니다. 이 차이를 무시하면 백테스트와 실거래 성과가 벌어집니다 → 백테스트와 실거래는 왜 달라지나.
4. 주문 응답 읽는 법
주문을 내면 이런 JSON이 옵니다. 봇이 확인해야 할 필드는 정해져 있습니다.
{"symbol": "BTCUSDT", "orderId": 28457921,
"clientOrderId": "algolab-20260802-001", "transactTime": 1754093112345,
"price": "0.00000000", "origQty": "0.00100000",
"executedQty": "0.00100000", "cummulativeQuoteQty": "64.22890000",
"status": "FILLED", "type": "MARKET", "side": "BUY",
"fills": [{"price": "64228.90000000", "qty": "0.00100000",
"commission": "0.00000100", "commissionAsset": "BTC"}]}
| 필드 | 봇이 봐야 할 이유 |
|---|---|
status | NEW(대기) / PARTIALLY_FILLED(일부) / FILLED(전량) / CANCELED / REJECTED / EXPIRED |
executedQty | 실제 체결 수량 — origQty와 다를 수 있음 |
cummulativeQuoteQty | 실제로 쓴 견적통화 금액 ÷ executedQty = 평균 체결가 |
fills | MARKET 주문의 체결 조각 — 여러 호가에 나눠 체결됐는지 여기서 보임 |
price가 0인 것은 오류가 아닙니다. 시장가 주문은 지정가가 없으니 price가 "0.00000000"으로 옵니다. 여기서 체결가를 읽으려 하면 손익이 전부 망가집니다. 평균 체결가는 cummulativeQuoteQty ÷ executedQty로 계산하세요.
def avg_fill_price(res):
q = Decimal(res["executedQty"])
if q == 0:
return None # 미체결 — 로그만 남기고 재시도 판단
return Decimal(res["cummulativeQuoteQty"]) / q
5. 거부되는 4가지 이유
바이낸스 API는 실패해도 HTTP 200이 아니라 4xx와 함께 code·msg를 돌려줍니다. 코드만 보면 원인이 바로 나옵니다.
| code | 실제 응답 msg | 원인과 조치 |
|---|---|---|
-1013 | Filter failure: LOT_SIZE | 수량이 stepSize 배수가 아님 → exchangeInfo로 보정 |
-1021 | Timestamp for this request is outside of the recvWindow | 서버 시계 오차 → NTP 동기화 |
-1022 | Signature for this request is not valid | 서명한 문자열 ≠ 실제 전송 쿼리 |
-2010 | Account has insufficient balance for requested action | locked 제외한 free만으로 계산 |
{"code": -1013, "msg": "Filter failure: LOT_SIZE"}
{"code": -1021, "msg": "Timestamp for this request is outside of the recvWindow."}
권한·IP 문제로 나는 -2015와 심볼 오류 -1121은 키 설정 쪽 문제라 성격이 다릅니다.
6. 호출 가중치 — 429가 뜨기 전에
바이낸스 API는 "초당 몇 번"이 아니라 엔드포인트별 가중치(weight) 합계로 한도를 셉니다. /api/v3/ticker/price를 심볼 지정 없이 부르면 가중치가 훨씬 커지는 식입니다.
한도 숫자를 코드에 박지 마세요. 값은 바이낸스가 조정합니다. GET /api/v3/exchangeInfo 응답의 rateLimits에 현재 한도가 들어 있으니 거기서 읽는 것이 언제나 정확합니다.
>>> requests.get(f"{BASE}/api/v3/exchangeInfo").json()["rateLimits"]
# [{'rateLimitType': 'REQUEST_WEIGHT', 'interval': 'MINUTE', 'limit': ...}, ...]
>>> r.headers.get("X-MBX-USED-WEIGHT-1M") # 실제 소진량은 응답 헤더로
'128'
한도를 넘기면 HTTP 429가 오고, 그걸 무시하고 계속 때리면 418로 IP가 일정 시간 차단됩니다. 429를 받으면 Retry-After 헤더를 보고 쉬어야 합니다. 전략을 여러 개 돌릴수록 합산 호출량이 문제가 되므로 계정 단위 공용 리미터가 필요합니다 → API 호출 제한 설계. 실시간 시세는 REST를 반복 호출하지 말고 WebSocket 스트림을 쓰는 것이 정답입니다.
7. 테스트넷 — 배선만 점검하는 용도
스팟 테스트넷(testnet.binance.vision)에서 별도 키를 받아 같은 /api/v3 경로 구조로 연습할 수 있습니다. BASE만 바꾸면 위 코드가 그대로 돕니다.
테스트넷 성과는 성과가 아닙니다. 호가 두께와 체결이 실거래와 다르고 지원 심볼도 제한적입니다. 서명·파라미터·에러 처리가 맞는지 확인하는 배선 점검용으로만 쓰고, 전략이 통하는지는 별도 백테스트로 검증하세요 → 백테스팅 완전 가이드. 과거 성과가 미래 수익을 보장하지 않는다는 점도 그대로입니다.
서명·필터·재시도·리미터는 전략과 무관한 배관 작업입니다. 알고랩은 이 배관이 이미 끝난 상태에서 전략만 얹는 방식으로 바이낸스 봇을 제작합니다. 사양을 정리해 오시면 범위와 비용을 먼저 알려드립니다.
상담 시작하기 →자주 묻는 질문
python-binance나 ccxt를 쓰면 이걸 몰라도 되나요?
평소에는 몰라도 됩니다. 문제는 에러가 났을 때입니다. 라이브러리는 -1013을 예외로 감싸서 던질 뿐, stepSize를 대신 맞춰 주지는 않습니다. ccxt는 여러 거래소를 같은 인터페이스로 묶어 주는 대신 원본 응답 필드를 자기 형식으로 바꿔 놓기 때문에, 바이낸스 고유 필드(cummulativeQuoteQty 등)를 볼 때는 결국 원본 스펙을 알아야 합니다.
선물(Futures)도 같은 주소인가요?
아닙니다. 스팟은 api.binance.com의 /api/v3, USDT 무기한 선물은 별도 호스트의 /fapi/v1 계열로 경로가 다릅니다. 파라미터 이름도 positionSide·reduceOnly처럼 선물 전용이 추가됩니다. 레버리지가 붙는 만큼 청산 처리도 따로 설계해야 합니다 → 바이낸스 선물 청산 방지 7가지 안전장치.
이 코드를 그대로 실거래에 써도 되나요?
설명용 최소 코드라 그대로 쓰기엔 부족합니다. 최소한 재시도·타임아웃·중복 주문 방지·킬 스위치가 붙어야 실계좌에 올릴 수 있습니다 → 킬 스위치와 서킷 브레이커. 그리고 API 스펙은 바뀌므로 운영 전에는 항상 바이낸스 공식 API 문서에서 최신 규격을 확인하세요.
바이낸스 자동매매 봇 맞춤 제작
서명·필터 보정·레이트리밋·재시도·24시간 무중단 운영까지 — 알고랩이 통합 패키지로 제작합니다.
24시간 빠른 답변 가능합니다.