วันนี้ผมจะมาเล่าประสบการณ์จริงที่เจอมาตอนพัฒนาระบบเทรดอัตโนมัติด้วย Bybit WebSocket ซึ่งเป็นตัวเลือกยอดนิยมสำหรับดึงข้อมูลราคาแบบเรียลไทม์ พร้อมวิธีแก้ปัญหาที่เจอระหว่างทาง
ทำไมต้องใช้ Bybit WebSocket?
สำหรับเทรดเดอร์หรือนักพัฒนาที่ต้องการดึงข้อมูลราคาสัญญา Perpetual ของ Bybit แบบเรียลไทม์ WebSocket คือคำตอบที่ดีที่สุด เพราะมีข้อดีหลายอย่าง:
- Latency ต่ำกว่า REST API มาก (ประมาณ 50-100ms)
- ไม่ต้องส่ง request ซ้ำๆ ทำให้ประหยัด resource
- รองรับการ subscribe หลาย topic พร้อมกัน
- ข้อมูลอัพเดททุกครั้งที่มีการเปลี่ยนแปลงราคา
สถานการณ์ข้อผิดพลาดจริงที่เจอ
ตอนเริ่มต้นพัฒนา ผมเจอปัญหาหลายอย่าง เช่น:
- ConnectionError: timeout - WebSocket ต่อไม่ได้เลย
- 401 Unauthorized - Authentication ล้มเหลว
- 1006 Abnormal Closure - Connection หลุดทันทีหลังต่อสำเร็จ
- ไม่ได้รับข้อมูล orderbook - subscribe แล้วแต่ไม่มี data ส่งกลับมา
บทความนี้จะพาทุกคนแก้ปัญหาเหล่านี้ทีละจุด
พื้นฐาน WebSocket ของ Bybit
ก่อนจะเข้าสู่โค้ด มาทำความเข้าใจโครงสร้าง WebSocket endpoint ของ Bybit กันก่อน:
Endpoint: wss://stream.bybit.com/v5/public/perp
Protocol: WebSocket (ws:// หรือ wss:// สำหรับ SSL)
Port: มาตรฐาน 443
Topics ที่สำคัญ:
- orderbook.50 (orderbook 50 level)
- publicTrade (trade ล่าสุด)
- ticker (ข้อมูล OHLCV)
- position (ตำแหน่ง - ต้อง authentication)
โค้ด Python สำหรับเชื่อมต่อ WebSocket
import websocket
import json
import time
class BybitWebSocket:
def __init__(self, symbol="BTCPERP"):
self.symbol = symbol
self.ws = None
self.url = f"wss://stream.bybit.com/v5/public/perp"
self.is_connected = False
def on_open(self, ws):
"""เรียกเมื่อ connection เปิดสำเร็จ"""
print(f"✅ Connected to Bybit WebSocket")
# Subscribe orderbook 50 levels
subscribe_msg = {
"op": "subscribe",
"args": [
f"orderbook.50.{self.symbol}",
f"publicTrade.{self.symbol}",
f"tickers.{self.symbol}"
]
}
ws.send(json.dumps(subscribe_msg))
print(f"📡 Subscribed to: orderbook, publicTrade, tickers")
def on_message(self, ws, message):
"""เรียกเมื่อได้รับข้อความใหม่"""
data = json.loads(message)
# แยกประเภทข้อความ
if "topic" in data:
topic = data.get("topic", "")
payload = data.get("data", {})
if "orderbook" in topic:
print(f"📊 Orderbook Update:")
print(f" Bid: {payload.get('b', [])[:3]}")
print(f" Ask: {payload.get('a', [])[:3]}")
elif "tickers" in topic:
print(f"📈 Ticker: ${payload.get('lastPrice', 'N/A')}")
elif "publicTrade" in topic:
for trade in payload:
print(f"🔔 Trade: {trade.get('s')} @ ${trade.get('p')}")
def on_error(self, ws, error):
"""เรียกเมื่อเกิด error"""
print(f"❌ WebSocket Error: {error}")
def on_close(self, ws, close_status_code, close_msg):
"""เรียกเมื่อ connection ปิด"""
print(f"⚠️ Connection closed: {close_status_code} - {close_msg}")
self.is_connected = False
def connect(self):
"""เปิดการเชื่อมต่อ WebSocket"""
self.ws = websocket.WebSocketApp(
self.url,
on_open=self.on_open,
on_message=self.on_message,
on_error=self.on_error,
on_close=self.on_close
)
self.is_connected = True
self.ws.run_forever(ping_interval=30)
def disconnect(self):
"""ปิดการเชื่อมต่อ"""
if self.ws:
self.ws.close()
วิธีใช้งาน
if __name__ == "__main__":
client = BybitWebSocket(symbol="BTCPERP")
try:
client.connect()
except KeyboardInterrupt:
client.disconnect()
print("👋 Disconnected")
โค้ดสำหรับ Authentication (สำหรับ Private Data)
หากต้องการดึงข้อมูลส่วนตัว เช่น ตำแหน่ง (position) หรือคำสั่งซื้อ (order) จะต้องใช้ authentication:
import hashlib
import hmac
import time
import websocket
import json
class BybitAuthWebSocket:
def __init__(self, api_key, api_secret):
self.api_key = api_key
self.api_secret = api_secret
self.ws = None
self.url = "wss://stream.bybit.com/v5/private"
def generate_auth_params(self):
"""สร้าง parameters สำหรับ authentication"""
expires = int(time.time() * 1000) + 10000
# สร้าง signature
signature_str = f"GET/realtime{expires}"
signature = hmac.new(
self.api_secret.encode('utf-8'),
signature_str.encode('utf-8'),
hashlib.sha256
).hexdigest()
return {
"op": "auth",
"args": [self.api_key, expires, signature]
}
def on_open(self, ws):
"""เรียกเมื่อ connection เปิดสำเร็จ"""
print("✅ Connected to Bybit Private WebSocket")
# ส่ง authentication
auth_params = self.generate_auth_params()
ws.send(json.dumps(auth_params))
def on_message(self, ws, message):
"""เรียกเมื่อได้รับข้อความใหม่"""
data = json.loads(message)
# ตรวจสอบว่า auth สำเร็จหรือไม่
if data.get("op") == "auth":
if data.get("success"):
print("🔐 Authentication successful!")
# Subscribe position updates
subscribe_msg = {
"op": "subscribe",
"args": ["position"]
}
ws.send(json.dumps(subscribe_msg))
else:
print(f"❌ Authentication failed: {data}")
return
# ประมวลผลข้อมูล position
if "topic" in data and data.get("topic") == "position":
positions = data.get("data", [])
for pos in positions:
print(f"📍 Position: {pos.get('symbol')}")
print(f" Size: {pos.get('size')}")
print(f" Entry: ${pos.get('avgPrice')}")
print(f" PnL: ${pos.get('unrealisedPnl')}")
def on_error(self, ws, error):
"""เรียกเมื่อเกิด error"""
print(f"❌ Error: {error}")
def on_close(self, ws, close_status_code, close_msg):
"""เรียกเมื่อ connection ปิด"""
print(f"⚠️ Connection closed")
def connect(self):
"""เปิดการเชื่อมต่อ WebSocket แบบ authenticated"""
self.ws = websocket.WebSocketApp(
self.url,
on_open=self.on_open,
on_message=self.on_message,
on_error=self.on_error,
on_close=self.on_close
)
self.ws.run_forever(ping_interval=20)
วิธีใช้งาน
if __name__ == "__main__":
# ⚠️ อย่า hardcode API key ในโค้ด production!
client = BybitAuthWebSocket(
api_key="YOUR_API_KEY",
api_secret="YOUR_API_SECRET"
)
client.connect()
โค้ดรวม WebSocket พร้อม Error Handling และ Reconnection
import websocket
import json
import time
import threading
from datetime import datetime
class RobustBybitWebSocket:
"""WebSocket client ที่มี auto-reconnect และ error handling"""
def __init__(self, symbols, topics):
self.symbols = symbols
self.topics = topics # e.g., ["orderbook.50", "publicTrade", "tickers"]
self.ws = None
self.url = "wss://stream.bybit.com/v5/public/perp"
self.is_running = False
self.reconnect_delay = 1
self.max_reconnect_delay = 60
self.last_health_check = time.time()
def build_subscribe_message(self):
"""สร้าง subscribe message สำหรับทุก symbols และ topics"""
args = []
for symbol in self.symbols:
for topic in self.topics:
args.append(f"{topic}.{symbol}")
return {"op": "subscribe", "args": args}
def process_message(self, data):
"""ประมวลผลข้อความที่ได้รับ"""
try:
if "topic" in data:
topic = data["topic"]
payload = data["data"]
if "orderbook" in topic:
# ประมวลผล orderbook
self.process_orderbook(payload)
elif "tickers" in topic:
# ประมวลผล ticker
self.process_ticker(payload)
elif "publicTrade" in topic:
# ประมวลผล trades
self.process_trades(payload)
# อัพเดท health check
self.last_health_check = time.time()
except Exception as e:
print(f"⚠️ Error processing message: {e}")
def process_orderbook(self, data):
"""ประมวลผล orderbook data"""
bids = data.get('b', [])
asks = data.get('a', [])
if bids and asks:
best_bid = float(bids[0][0])
best_ask = float(asks[0][0])
spread = best_ask - best_bid
spread_pct = (spread / best_ask) * 100
print(f"[{datetime.now().strftime('%H:%M:%S')}] "
f"Bid: ${best_bid:.2f} | Ask: ${best_ask:.2f} | "
f"Spread: {spread:.2f} ({spread_pct:.3f}%)")
def process_ticker(self, data):
"""ประมวลผล ticker data"""
symbol = data.get('symbol', '')
price = data.get('lastPrice', '')
high = data.get('highPrice24h', '')
low = data.get('lowPrice24h', '')
print(f"📈 {symbol}: ${price} | 24h: ${low} - ${high}")
def process_trades(self, data):
"""ประมวลผล trade data"""
for trade in data:
side = "🟢 BUY" if trade.get('S') == 'Buy' else "🔴 SELL"
price = trade.get('p', '')
size = trade.get('v', '')
time_str = datetime.fromtimestamp(
int(trade.get('T', 0)) / 1000
).strftime('%H:%M:%S')
print(f"{side} {size} @ ${price} [{time_str}]")
def on_open(self, ws):
"""เรียกเมื่อ connection เปิดสำเร็จ"""
print(f"✅ Connected to Bybit WebSocket at {self.url}")
# Reset reconnect delay
self.reconnect_delay = 1
# Subscribe to topics
subscribe_msg = self.build_subscribe_message()
ws.send(json.dumps(subscribe_msg))
print(f"📡 Subscribed: {subscribe_msg['args']}")
def on_message(self, ws, message):
"""เรียกเมื่อได้รับข้อความใหม่"""
try:
data = json.loads(message)
# ตรวจสอบว่าเป็น response ของ subscribe หรือไม่
if "success" in data:
if data["success"]:
print(f"✅ Subscribe successful: {data.get('ret_msg', '')}")
else:
print(f"❌ Subscribe failed: {data.get('ret_msg', '')}")
return
# ประมวลผลข้อมูล
self.process_message(data)
except json.JSONDecodeError as e:
print(f"❌ JSON decode error: {e}")
def on_error(self, ws, error):
"""เรียกเมื่อเกิด error"""
print(f"❌ WebSocket Error: {error}")
def on_close(self, ws, close_status_code, close_msg):
"""เรียกเมื่อ connection ปิด"""
print(f"⚠️ Connection closed: {close_status_code} - {close_msg}")
self.is_running = False
def run_with_reconnect(self):
"""รัน WebSocket พร้อม auto-reconnect"""
self.is_running = True
while self.is_running:
try:
self.ws = websocket.WebSocketApp(
self.url,
on_open=self.on_open,
on_message=self.on_message,
on_error=self.on_error,
on_close=self.on_close
)
# run_forever จะ block จนกว่า connection จะปิด
self.ws.run_forever(
ping_interval=30,
ping_timeout=10
)
except Exception as e:
print(f"❌ Unexpected error: {e}")
# รอก่อน reconnect
if self.is_running:
print(f"⏳ Waiting {self.reconnect_delay}s before reconnect...")
time.sleep(self.reconnect_delay)
# Exponential backoff
self.reconnect_delay = min(
self.reconnect_delay * 2,
self.max_reconnect_delay
)
def start(self):
"""เริ่ม WebSocket ใน thread แยก"""
thread = threading.Thread(
target=self.run_with_reconnect,
daemon=True
)
thread.start()
print(f"🚀 WebSocket thread started")
return thread
def stop(self):
"""หยุด WebSocket"""
print("🛑 Stopping WebSocket...")
self.is_running = False
if self.ws:
self.ws.close()
วิธีใช้งาน
if __name__ == "__main__":
# สร้าง client
client = RobustBybitWebSocket(
symbols=["BTCPERP", "ETHPERP"],
topics=["orderbook.50", "tickers", "publicTrade"]
)
# เริ่มเชื่อมต่อ
client.start()
# รัน 60 วินาที
try:
time.sleep(60)
except KeyboardInterrupt:
pass
finally:
client.stop()
ข้อผิดพลาดที่พบบ่อยและวิธีแก้ไข
1. ConnectionError: timeout หรือ Connection refused
สาเหตุ: URL ไม่ถูกต้อง หรือ firewall บล็อกการเชื่อมต่อ หรือ network มีปัญหา
# ❌ URL ที่ผิด (จะทำให้ timeout)
url = "wss://stream.bybit.com/ws" # ผิด!
✅ URL ที่ถูกต้อง (v5 public endpoint)
url = "wss://stream.bybit.com/v5/public/perp"
หรือสำหรับ private endpoint
url = "wss://stream.bybit.com/v5/private"
วิธีแก้: ตรวจสอบ network และใช้ try-except
import socket
def check_connectivity():
try:
socket.create_connection(("stream.bybit.com", 443), timeout=5)
print("✅ Network OK")
return True
except OSError as e:
print(f"❌ Network error: {e}")
return False
2. 401 Unauthorized
สาเหตุ: API key ผิด หรือ signature ไม่ถูกต้อง หรือ timestamp ไม่ตรงกัน
# ❌ วิธีที่ผิด: ใช้ signature แบบง่ายเกินไป
signature = hashlib.md5(f"{api_key}{timestamp}".encode()).hexdigest()
✅ วิธีที่ถูกต้อง: HMAC-SHA256 กับ path ที่ถูกต้อง
import hmac
import hashlib
def generate_signature(api_secret, expires):
# Path ต้องเป็น "GET/realtime" เท่านั้น
signature_str = f"GET/realtime{expires}"
signature = hmac.new(
api_secret.encode('utf-8'),
signature_str.encode('utf-8'),
hashlib.sha256
).hexdigest()
return signature
วิธีแก้: ตรวจสอบเวลา server ตรงกัน
import ntplib
from time import ntp_time
def sync_time():
try:
client_ntp = ntplib.NTPClient()
response = client_ntp.request('pool.ntp.org')
return response.tx_time
except:
return time.time()
expires ต้องใช้เวลาจริง
expires = int(sync_time() * 1000) + 10000
3. 1006 Abnormal Closure / Connection หลุดทันที
สาเหตุ: ไม่ได้ส่ง ping/pong ภายในเวลาที่กำหนด หรือ server ปฏิเสธ connection
# ❌ ไม่ได้กำหนด ping_interval
ws.run_forever() # จะหลุดบ่อย!
✅ กำหนด ping_interval และ ping_timeout
ws.run_forever(
ping_interval=30, # ส่ง ping ทุก 30 วินาที
ping_timeout=10, # รอ pong 10 วินาที
reconnect=5 # reconnect ทุก 5 วินาที (websocket-client เวอร์ชันใหม่)
)
หรือใช้วิธี manual ping
def send_ping(self, ws):
while self.is_running:
time.sleep(25) # ส่งก่อน ping_interval จะหมด
try:
ws.ping("keepalive")
except:
break
4. ไม่ได้รับข้อมูลหลัง subscribe
สาเหตุ: Topic format ไม่ถูกต้อง หรือ symbol ไม่มีในระบบ หรือ subscribe message ผิด format
# ❌ Format ที่ผิด
args = ["orderbook.50.BTC"] # ผิด! ต้องมี .perp
args = ["orderbook.50.BTCPERP.BTC"] # ผิด! ซ้ำเกินไป
✅ Format ที่ถูกต้องสำหรับ perpetual
args = ["orderbook.50.BTCPERP"] # ถูกต้อง
args = ["publicTrade.BTCPERP"] # ถูกต้อง
หรือสำหรับ spot
args = ["orderbook.50.BTCUSDT"] # Spot ใช้ USDT
ตรวจสอบ topic ที่รองรับ
SUPPORTED_TOPICS = {
"perp": ["orderbook.50", "orderbook.200", "publicTrade", "tickers", "kline"],
"spot": ["orderbook.50", "publicTrade", "tickers", "kline"]
}
def validate_topic(symbol, topic):
# Perpetual symbol จะลงท้ายด้วย PERP
is_perp = symbol.endswith("PERP")
topic_type = "perp" if is_perp else "spot"
if topic in SUPPORTED_TOPICS.get(topic_type, []):
return True
return False
5. Rate Limit / Too Many Requests
สาเหตุ: Subscribe ซ้ำๆ หรือ reconnect บ่อยเกินไป
# ❌ Subscribe ซ้ำทุกครั้งที่ on_open
def on_open(self, ws):
# ส่ง subscribe ทุกครั้งที่ reconnect
ws.send(json.dumps({"op": "subscribe", "args": ["..."]}))
✅ ส่ง subscribe ครั้งเดียว และตรวจสอบว่ายังไม่ได้ subscribe
class SmartWebSocket:
def __init__(self):
self.subscribed = set()
def safe_subscribe(self, ws, topic):
if topic not in self.subscribed:
ws.send(json.dumps({"op": "subscribe", "args": [topic]}))
self.subscribed.add(topic)
print(f"📡 Subscribed to {topic}")
def on_open(self, ws):
# Subscribe แบบปลอดภัย
for topic in self.topics:
self.safe_subscribe(ws, topic)
หรือใช้ exponential backoff สำหรับ reconnect
self.reconnect_delay = min(
self.reconnect_delay * 2, # เพิ่มขึ้น 2 เท่า
60 # สูงสุด 60 วินาที
)
สถานการณ์ข้อผิดพลาดจริง: กรณีศึกษา
ปัญหาจริง: Latency สูงกว่าที่คาดหวัง
ตอนพัฒนาระบบเทรด scalping ผมพบว่า latency ของ WebSocket อยู่ที่ประมาณ 200-300ms ซึ่งสูงเกินไปสำหรับการเทรดระยะสั้น
# วัด latency จริง
import time
from datetime import datetime
class LatencyMeasurer:
def __init__(self):
self.orderbook_timestamps = {}
self.latencies = []
def measure_orderbook_latency(self, data):
"""วัดความเร็วในการรับ orderbook update"""
recv_time = time.time()
if "orderbook" in data.get("topic", ""):
# ดึง timestamp จากข้อมูล
data_time = data.get("data", {}).get("ts", 0)
if data_time:
latency_ms = (recv_time * 1000) - data_time
self.latencies.append(latency_ms)
if len(self.latencies) >= 100:
avg = sum(self.latencies) / len(self.latencies)
print(f"📊 Avg latency: {avg:.2f}ms | "
f"Max: {max(self.latencies):.2f}ms | "
f"Min: {min(self.latencies):.2f}ms")
self.latencies = []
def on_message(self, ws, message):
data = json.loads(message)
self.measure_orderbook_latency(data)
สาเหตุของ latency สูง:
1. Network distance (เช่น เทรดจากไทย ไป server SG)
2. Message parsing overhead
3. Print statement ใน on_message
วิธีแก้:
1. ใช้ server ที่ใกล้ Bybit (Singapore)
2. ปรับโค้ดให้ parse JSON ทีเดียว
3. ลด print หรือใช้ queue แยก
เหมาะกับใคร / ไม่เหมาะกับใคร
| เหมาะกับ | ไม่เหมาะกับ |
|---|---|
| นักพัฒนาระบบเทรดอัตโนมัติ (Bot) | ผู้เริ่มต้นที่ไม่มีพื้นฐาน Python |
| เทรดเดอร์ scalping ที่ต้องการ latency ต่ำ | คนที่ต้องการ GUI สำเร็จรูปใช้ง่าย |
| นักวิจัยด้าน DeFi ที่ต้องการข้อมูลเรียลไทม์ | ผู้ที่ต้องการแค่ดูกราฟ (ควรใช้ TradingView แทน) |
| ผู้พัฒนา DApp ที่ต้องการราคาจาก Bybit | ผู้ที่มีงบจำกัด (เพราะ server cost) |
ราคาและ ROI
| บริการ | ราคา/เดือน | ROI โดยประมาณ |
|---|---|---|
| Bybit Official WebSocket (ฟรี) | ฟรี | เหมาะสำหรับทดลอง/เรียนรู้ |
| Server VPS Singapore | ~$20-50/เดือน | คุ้มค่าหากทำ bot เทรดจริง |
| HolySheep AI | เริ่มต้น $0 (มี free credits) | ROI สูงสุด - ประหยัด 85%+ |
ทำไมต้องเลือก HolySheep
หากคุณกำลังพัฒนาระบบเทรด