Mình đã dành 3 tháng làm việc với API của các sàn giao dịch tiền mã hóa, và trải nghiệm đau đớn nhất là khi nhận được lỗi "ConnectionError: timeout after 30000ms" vào lúc 2 giờ sáng khi bot giao dịch của mình ngừng hoạt động. Hôm nay mình sẽ chia sẻ cách subscribe dữ liệu order book từ OKX một cách chính xác, tránh những cạm bẫy thường gặp, và xây dựng một hệ thống real-time data subscription ổn định.
Order Book là gì và tại sao nó quan trọng?
Order Book là danh sách các lệnh mua/bán đang chờ được khớp trên sàn giao dịch. Với trader giao dịch khối lượng lớn hoặc market maker, độ sâu order book (depth of market) cho biết:
- Xu hướng ngắn hạn của thị trường
- Khối lượng liquidity tại các mức giá
- Điểm có thể xảy ra breakout hoặc reversal
- Spread thực tế giữa bid và ask
OKX cung cấp 3 loại channel order book:
- books - Snapshot 400 mức giá, cập nhật mỗi 100ms
- books5 - Snapshot 5 mức giá, cập nhật real-time
- bbo-tbt - Best bid/offer có tick-by-tick update
Kết nối WebSocket và Subscribe Order Book
2.1. Cài đặt thư viện và thiết lập project
Tạo virtual environment và cài đặt dependencies
python3 -m venv okx_env
source okx_env/bin/activate
Cài đặt websocket-client cho WebSocket connection
pip install websocket-client==0.59.0
Hoặc sử dụng library khác theo preference
pip install websockets==12.0 # Alternative async library
Kiểm tra phiên bản
python3 -c "import websocket; print(websocket.__version__)"
2.2. Subscribe Order Book với WebSocket
import websocket
import json
import time
from datetime import datetime
class OKXOrderBookClient:
"""
Client subscribe dữ liệu order book từ OKX Exchange
Documentation: https://www.okx.com/docs-v5/en/#websocket-api
"""
def __init__(self):
# OKX WebSocket endpoint - Production
self.wss_url = "wss://ws.okx.com:8443/ws/v5/public"
# OKX WebSocket endpoint - Demo trading
# self.wss_url = "wss://wspap.okx.com:8443/ws/v5/public"
self.ws = None
self.order_book_data = {}
self.is_connected = False
self.reconnect_delay = 1
self.max_reconnect_delay = 30
def connect(self):
"""Thiết lập kết nối WebSocket đến OKX"""
print(f"[{datetime.now()}] Đang kết nối đến OKX...")
self.ws = websocket.WebSocketApp(
self.wss_url,
on_message=self._on_message,
on_error=self._on_error,
on_close=self._on_close,
on_open=self._on_open
)
# run_forever() blocking - chạy trong thread riêng nếu cần
self.ws.run_forever(ping_interval=25, ping_timeout=10)
def _on_open(self, ws):
"""Callback khi kết nối thành công"""
self.is_connected = True
self.reconnect_delay = 1
print(f"[{datetime.now()}] ✓ Kết nối WebSocket thành công!")
# Subscribe order book BTC-USDT với depth 400 levels
subscribe_message = {
"op": "subscribe",
"args": [
{
"channel": "books",
"instId": "BTC-USDT",
"instType": "SPOT"
}
]
}
ws.send(json.dumps(subscribe_message))
print(f"[{datetime.now()}] Đã gửi subscribe request cho BTC-USDT")
def _on_message(self, ws, message):
"""Xử lý tin nhắn từ OKX server"""
try:
data = json.loads(message)
# OKX gửi heartbeat định kỳ
if message == "pong":
print(f"[{datetime.now()}] Heartbeat received")
return
# Xử lý event subscription
if "event" in data:
if data["event"] == "subscribe":
print(f"[{datetime.now()}] ✓ Subscription thành công: {data}")
elif data["event"] == "error":
print(f"[{datetime.now()}] ✗ Subscription lỗi: {data}")
return
# Xử lý dữ liệu order book
if "data" in data and len(data["data"]) > 0:
ob = data["data"][0]
# Trích xất thông tin order book
self.order_book_data = {
"timestamp": ob["ts"],
"asks": ob["asks"], # Format: [[price, size, decimal], ...]
"bids": ob["bids"],
"msg_id": ob.get("id", "N/A")
}
# Tính toán thông tin quan trọng
best_ask = float(ob["asks"][0][0]) if ob["asks"] else 0
best_bid = float(ob["bids"][0][0]) if ob["bids"] else 0
spread = best_ask - best_bid
spread_pct = (spread / best_bid * 100) if best_bid > 0 else 0
print(f"[{datetime.now()}] Order Book: Best Ask={best_ask:.2f}, "
f"Best Bid={best_bid:.2f}, Spread={spread:.2f} ({spread_pct:.4f}%)")
except json.JSONDecodeError as e:
print(f"Lỗi parse JSON: {e}")
except Exception as e:
print(f"Lỗi xử lý message: {e}")
def _on_error(self, ws, error):
"""Xử lý lỗi WebSocket"""
print(f"[{datetime.now()}] ✗ WebSocket Error: {error}")
def _on_close(self, ws, close_status_code, close_msg):
"""Callback khi kết nối đóng"""
self.is_connected = False
print(f"[{datetime.now()}] Kết nối đóng: {close_status_code} - {close_msg}")
self._attempt_reconnect()
def _attempt_reconnect(self):
"""Tự động reconnect với exponential backoff"""
print(f"[{datetime.now()}] Đang chuẩn bị reconnect sau {self.reconnect_delay}s...")
time.sleep(self.reconnect_delay)
self.reconnect_delay = min(self.reconnect_delay * 2, self.max_reconnect_delay)
self.connect()
if __name__ == "__main__":
client = OKXOrderBookClient()
try:
client.connect()
except KeyboardInterrupt:
print("\nCtrl+C detected - Đang dừng client...")
if client.ws:
client.ws.close()
2.3. Chạy và kiểm tra
Chạy script
python3 okx_orderbook_basic.py
Output mong đợi:
[2026-01-15 10:30:00.123] Đang kết nối đến OKX...
[2026-01-15 10:30:00.456] ✓ Kết nối WebSocket thành công!
[2026-01-15 10:30:00.789] Đã gửi subscribe request cho BTC-USDT
[2026-01-15 10:30:01.012] ✓ Subscription thành công: {'event': 'subscribe', 'arg': {...}}
[2026-01-15 10:30:01.234] Order Book: Best Ask=42350.50, Best Bid=42348.20, Spread=2.30 (0.0054%)
[2026-01-15 10:30:01.456] Order Book: Best Ask=42351.00, Best Bid=42348.50, Spread=2.50 (0.0059%)
Lỗi thường gặp và cách khắc phục
3.1. Lỗi "401 Unauthorized" - Subscription Fail
Mô tả lỗi: Khi subscribe, nhận được response lỗi 401 Unauthorized
Response lỗi từ OKX
{
"event": "error",
"msg": "Illegal request: verify timestamp failed",
"code": "20004"
}
Nguyên nhân:
1. Timestamp request không chính xác
2. Request signature không đúng (nếu dùng private channel)
3. Parameter format không đúng
Giải pháp:
def validate_subscription_params(inst_id, channel):
"""Validate tham số trước khi subscribe"""
# Danh sách các cặp tiền hợp lệ trên OKX SPOT
valid_spot_pairs = [
"BTC-USDT", "ETH-USDT", "SOL-USDT", "XRP-USDT",
"DOGE-USDT", "ADA-USDT", "AVAX-USDT", "DOT-USDT"
]
# Danh sách perpetual futures pairs
valid_perp_pairs = [
"BTC-USDT-SWAP", "ETH-USDT-SWAP", "SOL-USDT-SWAP"
]
all_valid = valid_spot_pairs + valid_perp_pairs
if inst_id not in all_valid:
raise ValueError(
f"InstID '{inst_id}'