바이낸스 WebSocket 실시간 시세의 최적 솔루션은 wss://stream.binance.com:9443/stream?streams= 조합 스트림 엔드포인트를 사용하는 것입니다. 하나의 연결로 최대 1024개의 스트림을 구독할 수 있으며, 메시지 지연 시간은 50ms 미만이고 가중치(Weight) 소모는 거의 제로에 가깝습니다. 이 글에서는 단일 스트림, 조합 스트림, 동적 구독, 로컬 호가창 매칭, 연결 끊김 재연결을 포함한 전체 Python 및 Node.js 코드를 제공하며, 모두 즉시 실행 가능합니다. 시작하기 전에 아직 바이낸스 계정이 없다면 바이낸스 공식 사이트에서 공식 접속 경로를 확인하거나, 무료 가입을 통해 계정을 만드실 수 있습니다.
1. WebSocket 엔드포인트 개요
| 카테고리 | URL | 용도 |
|---|---|---|
| 현물(Spot) 단일 스트림 | wss://stream.binance.com:9443/ws/{stream} | 하나의 스트림만 구독 |
| 현물(Spot) 조합 스트림 | wss://stream.binance.com:9443/stream?streams={s1/s2/...} | 여러 스트림 구독, 메시지에 스트림 이름 포함 |
| 현물(Spot) 예비 | wss://data-stream.binance.vision/ws/{stream} | 공식 백업 라인 |
| 선물(Futures) USDT 기반 | wss://fstream.binance.com/stream?streams= | 무기한 선물 |
| 선물(Futures) 코인 기반 | wss://dstream.binance.com/stream?streams= | 코인 기반 선물 |
| 테스트넷(Testnet) | wss://stream.testnet.binance.vision/ws | 테스트 네트워크 |
연결 수 제한: 단일 IP당 최대 300개의 WebSocket 연결, 연결당 최대 1024개의 스트림 구독이 가능하며, 24시간마다 한 번씩 자동으로 연결이 끊어지므로 클라이언트에서 재연결을 구현해야 합니다.
2. 스트림(Stream) 유형 목록
바이낸스는 11가지 주요 시세 스트림을 제공합니다:
| 스트림 | 형식 | 전송 빈도 | 설명 |
|---|---|---|---|
| aggTrade | {symbol}@aggTrade |
실시간 | 통합 체결 |
| trade | {symbol}@trade |
실시간 | 개별 체결 |
| kline_1m | {symbol}@kline_1m |
매초 | K라인(1m/5m/1h/...) |
| miniTicker | {symbol}@miniTicker |
1000ms | 요약 시세 |
| ticker | {symbol}@ticker |
1000ms | 24h 전체 통계 |
| bookTicker | {symbol}@bookTicker |
실시간 | 최우선 매수/매도 호가 |
| depth | {symbol}@depth |
1000ms | 전체 호가창(20/50/100단계) |
| depth@100ms | {symbol}@depth@100ms |
100ms | 호가창 차분(증량) |
| !miniTicker@arr | !miniTicker@arr |
1000ms | 전 시장 요약 시세 |
| !ticker@arr | !ticker@arr |
1000ms | 전 시장 전체 통계 |
| !bookTicker | !bookTicker |
실시간 | 전 시장 최우선 매수/매도 호가 |
명명 규칙: 소문자 + @ 기호로 구분합니다. 예: BTCUSDT@ticker가 아닌 btcusdt@ticker.
3. Python asyncio 단일 스트림 구독
import asyncio, json
import websockets
async def subscribe_ticker(symbol: str):
url = f"wss://stream.binance.com:9443/ws/{symbol.lower()}@ticker"
async with websockets.connect(url, ping_interval=180) as ws:
async for message in ws:
data = json.loads(message)
print(f"{data['s']}: 최신가 {data['c']}, "
f"24h 변동 {data['P']}%, "
f"거래량 {data['v']}")
asyncio.run(subscribe_ticker("BTCUSDT"))
출력 예시:
BTCUSDT: 최신가 68420.12, 24h 변동 2.35%, 거래량 45231.2
BTCUSDT: 최신가 68421.00, 24h 변동 2.36%, 거래량 45231.8
4. 여러 거래 페어 조합 스트림 구독
하나의 연결로 10개 거래 페어의 ticker + 5개 거래 페어의 K라인을 구독하는 예제입니다:
import asyncio, json
import websockets
STREAMS = [
"btcusdt@ticker", "ethusdt@ticker", "bnbusdt@ticker",
"solusdt@ticker", "xrpusdt@ticker", "adausdt@ticker",
"dogeusdt@ticker", "maticusdt@ticker", "dotusdt@ticker",
"avaxusdt@ticker",
"btcusdt@kline_1m", "ethusdt@kline_1m",
"bnbusdt@kline_1m", "solusdt@kline_1m", "xrpusdt@kline_1m"
]
async def combined_stream():
streams = "/".join(STREAMS)
url = f"wss://stream.binance.com:9443/stream?streams={streams}"
async with websockets.connect(url, ping_interval=180) as ws:
async for message in ws:
payload = json.loads(message)
stream = payload["stream"]
data = payload["data"]
if "@ticker" in stream:
print(f"[{stream}] {data['s']}: {data['c']}")
elif "@kline" in stream:
k = data["k"]
if k["x"]: # K라인이 마감될 때만 출력
print(f"[{stream}] {k['s']} {k['i']} "
f"O:{k['o']} H:{k['h']} L:{k['l']} C:{k['c']}")
asyncio.run(combined_stream())
5. 동적 구독/취소 (SUBSCRIBE 메서드)
연결이 수립된 후 JSON 메시지를 통해 구독을 동적으로 추가하거나 제거할 수 있습니다:
import asyncio, json
import websockets
async def dynamic_sub():
url = "wss://stream.binance.com:9443/ws"
async with websockets.connect(url) as ws:
# BTC 및 ETH의 bookTicker 구독
await ws.send(json.dumps({
"method": "SUBSCRIBE",
"params": ["btcusdt@bookTicker", "ethusdt@bookTicker"],
"id": 1
}))
count = 0
async for msg in ws:
data = json.loads(msg)
if "result" in data:
print(f"구독 확인: {data}")
continue
count += 1
print(f"{data['s']} 매수1 {data['b']} 매도1 {data['a']}")
# 50개 메시지를 받은 후 ETH 구독 취소, BTC만 유지
if count == 50:
await ws.send(json.dumps({
"method": "UNSUBSCRIBE",
"params": ["ethusdt@bookTicker"],
"id": 2
}))
print("ETH 구독 취소됨")
asyncio.run(dynamic_sub())
6. 호가창 증량 통합 (로컬 오더북 유지 관리)
고주파 전략에서는 로컬 매칭 엔진 수준의 오더북이 필요한 경우가 많습니다. 프로세스는 다음과 같습니다:
@depth@100ms차분 스트림을 구독하고 버퍼에 넣습니다.- REST API로
GET /api/v3/depth?symbol=X&limit=1000스냅샷을 가져옵니다. - 버퍼에서
u < lastUpdateId인 메시지를 폐기합니다. U <= lastUpdateId+1 <= u인 메시지를 순차적으로 적용합니다.- 이후 메시지는 직접 적용하며,
pu는 이전 메시지의u와 같아야 합니다.
import asyncio, json, aiohttp
import websockets
from sortedcontainers import SortedDict
class OrderBook:
def __init__(self, symbol):
self.symbol = symbol.lower()
self.bids = SortedDict() # 가격 -> 수량
self.asks = SortedDict()
self.last_update_id = 0
self.buffer = []
def apply_update(self, bids, asks):
for price, qty in bids:
price, qty = float(price), float(qty)
if qty == 0:
self.bids.pop(price, None)
else:
self.bids[price] = qty
for price, qty in asks:
price, qty = float(price), float(qty)
if qty == 0:
self.asks.pop(price, None)
else:
self.asks[price] = qty
async def load_snapshot(self):
async with aiohttp.ClientSession() as s:
url = f"https://api.binance.com/api/v3/depth?symbol={self.symbol.upper()}&limit=1000"
async with s.get(url) as r:
snap = await r.json()
self.last_update_id = snap["lastUpdateId"]
self.apply_update(snap["bids"], snap["asks"])
async def run(self):
url = f"wss://stream.binance.com:9443/ws/{self.symbol}@depth@100ms"
async with websockets.connect(url) as ws:
# 1단계: 먼저 일정 시간 동안 차분 데이터 수집
asyncio.create_task(self._collect(ws))
await asyncio.sleep(1)
# 2단계: 스냅샷 로드
await self.load_snapshot()
# 3단계: 버퍼에서 유효한 차분 데이터 적용
for diff in self.buffer:
if diff["u"] < self.last_update_id:
continue
self.apply_update(diff["b"], diff["a"])
self.last_update_id = diff["u"]
self.buffer = None
# 4단계: 이후 데이터 즉시 적용
async for msg in ws:
diff = json.loads(msg)
self.apply_update(diff["b"], diff["a"])
self.last_update_id = diff["u"]
best_bid = self.bids.keys()[-1] if self.bids else 0
best_ask = self.asks.keys()[0] if self.asks else 0
print(f"매수1 {best_bid} 매도1 {best_ask} 차이 {best_ask-best_bid:.2f}")
async def _collect(self, ws):
if self.buffer is None:
return
async for msg in ws:
if self.buffer is None:
break
self.buffer.append(json.loads(msg))
ob = OrderBook("BTCUSDT")
asyncio.run(ob.run())
7. 연결 끊김 재연결 및 하트비트
바이낸스 WebSocket은 클라이언트가 3분 이내에 pong 응답을 해야 하며, 연결은 24시간 후 강제로 끊어집니다:
import asyncio, json, websockets, logging
async def resilient_ws(url, on_message, max_retries=1000):
retry = 0
while retry < max_retries:
try:
async with websockets.connect(
url,
ping_interval=180, # 3분마다 ping 전송
ping_timeout=10,
close_timeout=5
) as ws:
retry = 0 # 연결 성공 시 초기화
async for message in ws:
await on_message(json.loads(message))
except (websockets.ConnectionClosed, asyncio.TimeoutError) as e:
retry += 1
wait = min(2 ** retry, 60) # 지수 백오프, 최대 60초
logging.warning(f"WS 연결 끊김 {e}, {wait}초 후 재연결 시도 ({retry}회차)")
await asyncio.sleep(wait)
8. Node.js WebSocket 예제
const WebSocket = require('ws');
const streams = ['btcusdt@bookTicker', 'ethusdt@bookTicker'].join('/');
const url = `wss://stream.binance.com:9443/stream?streams=${streams}`;
function connect() {
const ws = new WebSocket(url);
ws.on('open', () => console.log('연결 성공'));
ws.on('message', (raw) => {
const { stream, data } = JSON.parse(raw);
console.log(`${data.s} 매수1 ${data.b} 매도1 ${data.a}`);
});
ws.on('close', () => {
console.log('연결 끊김, 3초 후 재연결');
setTimeout(connect, 3000);
});
ws.on('error', (err) => console.error('오류 발생', err.message));
}
connect();
9. 자주 묻는 질문 FAQ
Q1: WebSocket도 가중치(Weight)를 소모하나요?
A: 연결 수립 자체에 가중치 2가 소모되지만, 이후 푸시되는 메시지는 가중치를 전혀 소모하지 않습니다. 단, IP당 연결 수가 300개를 초과하면 거부될 수 있습니다.
Q2: 구독 후 메시지가 전혀 오지 않는 이유는 무엇인가요?
A: 세 가지 흔한 원인이 있습니다: 1) 스트림 이름은 반드시 소문자여야 합니다 (BTCUSDT가 아닌 btcusdt). 2) 거래 페어를 잘못 작성했습니다 (예: BTCUSDT가 아닌 BTC-USDT 사용). 3) 구독 메시지는 /stream 엔드포인트가 아닌 wss://.../ws 엔드포인트에서 전송해야 합니다.
Q3: @depth와 @depth@100ms의 차이점은 무엇인가요?
A: @depth는 1000ms마다 한 번씩 전체 스냅샷(20단계)을 보내고, @depth@100ms는 100ms마다 한 번씩 차분(증량) 데이터를 보냅니다. 로컬 오더북 유지가 목적이라면 @100ms를, 단순 화면 표시용이라면 @depth를 사용하면 됩니다.
Q4: WebSocket으로 주문을 낼 수 있나요?
A: 가능합니다. 바이낸스는 서명 주문을 위한 WebSocket API(wss://ws-api.binance.com:443/ws-api/v3)를 제공하며, 이는 REST API보다 지연 시간이 30-50ms 낮습니다. 하지만 생태계가 아직 성숙하지 않아 대부분의 전략은 여전히 REST 주문 + WS 리스닝 방식을 사용합니다.
Q5: 24시간 자동 연결 끊김을 어떻게 매끄럽게 처리하나요?
A: 듀얼 연결 롤링 방식을 권장합니다. 연결 후 23시간이 지났을 때 새 연결 B를 수립하고, B가 메시지를 안정적으로 받기 시작하면 기존 연결 A를 닫아 데이터 누락을 방지합니다. python-binance와 같은 실무급 프레임워크에는 이 로직이 내장되어 있습니다.
WebSocket 솔루션을 살펴보셨다면, 카테고리 내비게이션으로 돌아가 「API 연동」 카테고리의 다른 SDK教程을 확인해 보세요.