สวัสดีครับ วันนี้ผมจะมาแชร์ประสบการณ์การใช้งาน Bybit WebSocket ที่ทำให้ระบบ Trading Bot ของผมหยุดทำงานไปเกือบ 3 ชั่วโมง จนกระทั่งเจอสาเหตุที่แท้จริงของ ConnectionError: timeout after 5000ms

ทำไมต้องใช้ WebSocket แทน REST API

สำหรับคนที่ทำ Trading Bot หรือระบบวิเคราะห์ตลาด การดึงข้อมูลแบบ Polling ด้วย REST API นั้นช้าและเปลืองโควต้า ในขณะที่ WebSocket ช่วยให้เรารับข้อมูลได้ทันทีที่มีการอัปเดต โดยไม่ต้องส่งคำขอซ้ำๆ

การตั้งค่าเริ่มต้น

ก่อนจะเริ่ม เราต้องติดตั้งไลบรารีที่จำเป็นก่อน:

# ติดตั้งไลบรารีที่จำเป็น
pip install websocket-client asyncio aiohttp python-dotenv

หรือใช้ Poetry

poetry add websocket-client asyncio aiohttp python-dotenv

โค้ดพื้นฐาน การเชื่อมต่อ WebSocket กับ Bybit

นี่คือโค้ดที่ผมใช้งานจริง ซึ่งรวบรวมข้อผิดพลาดต่างๆ ที่เจอมาดัดแปลงแล้ว:

import websocket
import json
import time
import threading
from datetime import datetime

class BybitWebSocket:
    """คลาสสำหรับเชื่อมต่อ Bybit WebSocket"""
    
    BYBIT_WS_URL = "wss://stream.bybit.com/v5/public/spot"
    
    def __init__(self, symbols=None):
        self.symbols = symbols or ["BTCUSDT", "ETHUSDT"]
        self.ws = None
        self.is_running = False
        self.last_ping = 0
        
    def on_message(self, ws, message):
        """จัดการเมื่อได้รับข้อความ"""
        try:
            data = json.loads(message)
            
            # ตรวจสอบประเภทข้อความ
            msg_type = data.get("type", "")
            
            if msg_type == "pong":
                print(f"✅ Pong received at {datetime.now().strftime('%H:%M:%S')}")
                self.last_ping = time.time()
                
            elif msg_type == "subscribe":
                print(f"📢 Subscription confirmed: {data}")
                
            elif msg_type == "message":
                # ข้อมูลราคา
                topic = data.get("topic", "")
                if "tickers" in topic:
                    ticker_data = data.get("data", {})
                    symbol = ticker_data.get("symbol", "")
                    price = ticker_data.get("lastPrice", "N/A")
                    print(f"💰 {symbol}: ${price}")
                    
            elif msg_type == "error":
                print(f"❌ Error: {data.get('msg', 'Unknown error')}")
                
        except json.JSONDecodeError as e:
            print(f"❌ JSON Parse Error: {e}")
        except Exception as e:
            print(f"❌ Unexpected Error: {e}")
    
    def on_error(self, ws, error):
        """จัดการข้อผิดพลาด WebSocket"""
        error_type = type(error).__name__
        print(f"🔴 WebSocket Error [{error_type}]: {error}")
        
        # ล็อกรายละเอียดเพิ่มเติม
        if "timeout" in str(error).lower():
            print("⚠️  Connection timeout - checking network and firewall...")
        elif "401" in str(error):
            print("⚠️  Authentication error - check API credentials...")
            
    def on_close(self, ws, close_status_code, close_msg):
        """จัดการเมื่อ connection ปิด"""
        print(f"🔵 Connection closed: {close_status_code} - {close_msg}")
        if self.is_running:
            print("🔄 Attempting to reconnect in 5 seconds...")
            time.sleep(5)
            self.connect()
            
    def on_open(self, ws):
        """จัดการเมื่อ connection เปิดสำเร็จ"""
        print("🟢 WebSocket connected successfully!")
        
        # สมัครรับข้อมูล Ticker สำหรับแต่ละ symbol
        for symbol in self.symbols:
            subscribe_msg = {
                "op": "subscribe",
                "args": [f"tickers.{symbol}"]
            }
            ws.send(json.dumps(subscribe_msg))
            print(f"📡 Subscribed to: {symbol}")
            
        # เริ่มส่ง ping ทุก 20 วินาที
        self.start_ping()
        
    def start_ping(self):
        """ส่ง ping เพื่อรักษาการเชื่อมต่อ"""
        def ping_loop():
            while self.is_running:
                try:
                    ping_msg = {"op": "ping"}
                    self.ws.send(json.dumps(ping_msg))
                    print(f"🏓 Ping sent at {datetime.now().strftime('%H:%M:%S')}")
                    time.sleep(20)  # Bybit แนะนำ ping ทุก 20-30 วินาที
                except Exception as e:
                    print(f"❌ Ping failed: {e}")
                    break
                    
        self.last_ping = time.time()
        ping_thread = threading.Thread(target=ping_loop, daemon=True)
        ping_thread.start()
        
    def connect(self):
        """เชื่อมต่อ WebSocket"""
        self.is_running = True
        
        # ปิด connection เก่าถ้ามี
        if self.ws:
            try:
                self.ws.close()
            except:
                pass
                
        self.ws = websocket.WebSocketApp(
            self.BYBIT_WS_URL,
            on_message=self.on_message,
            on_error=self.on_error,
            on_close=self.on_close,
            on_open=self.on_open
        )
        
        # รัน WebSocket ใน thread แยก
        ws_thread = threading.Thread(
            target=self.ws.run_forever,
            kwargs={"ping_interval": None},  # ปิด auto ping ของ library
            daemon=True
        )
        ws_thread.start()
        print(f"🔗 Connecting to {self.BYBIT_WS_URL}...")
        
    def disconnect(self):
        """ตัดการเชื่อมต่อ"""
        self.is_running = False
        if self.ws:
            self.ws.close()
            print("🔴 Disconnected from Bybit WebSocket")

วิธีใช้งาน

if __name__ == "__main__": # สร้าง instance และเชื่อมต่อ client = BybitWebSocket(symbols=["BTCUSDT", "ETHUSDT"]) client.connect() # รัน 60 วินาที try: time.sleep(60) except KeyboardInterrupt: print("\n👋 Shutting down...") finally: client.disconnect()

การประมวลผลข้อมูล Order Book

ต่อไปนี้คือตัวอย่างการรับข้อมูล Order Book ที่ลึกกว่า ซึ่งเหมาะสำหรับทำ Market Making หรือ Arbitrage:

import asyncio
import aiohttp
import json
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Dict, List, Optional
import time

@dataclass
class OrderBookEntry:
    """โครงสร้างข้อมูล Order Book"""
    price: float
    quantity: float
    side: str  # "bid" หรือ "ask"
    
    @property
    def total_value(self) -> float:
        return self.price * self.quantity

@dataclass
class OrderBook:
    """คลาสจัดการ Order Book"""
    symbol: str
    bids: Dict[float, float] = field(default_factory=dict)  # price -> quantity
    asks: Dict[float, float] = field(default_factory=dict)
    last_update: float = field(default_factory=time.time)
    
    @property
    def best_bid(self) -> Optional[OrderBookEntry]:
        if not self.bids:
            return None
        best_price = max(self.bids.keys())
        return OrderBookEntry(best_price, self.bids[best_price], "bid")
    
    @property
    def best_ask(self) -> Optional[OrderBookEntry]:
        if not self.asks:
            return None
        best_price = min(self.asks.keys())
        return OrderBookEntry(best_price, self.asks[best_price], "ask")
    
    @property
    def spread(self) -> Optional[float]:
        if self.best_bid and self.best_ask:
            return self.best_ask.price - self.best_bid.price
        return None
    
    @property
    def spread_percent(self) -> Optional[float]:
        if self.spread and self.best_bid:
            return (self.spread / self.best_bid.price) * 100
        return None
        
    def update(self, data: dict):
        """อัปเดต Order Book จากข้อมูล WebSocket"""
        update_data = data.get("data", {})
        
        # อัปเดต bids
        for item in update_data.get("b", []):
            price = float(item[0])
            quantity = float(item[1])
            if quantity == 0:
                self.bids.pop(price, None)
            else:
                self.bids[price] = quantity
                
        # อัปเดต asks
        for item in update_data.get("a", []):
            price = float(item[0])
            quantity = float(item[1])
            if quantity == 0:
                self.asks.pop(price, None)
            else:
                self.asks[price] = quantity
                
        self.last_update = time.time()
        
    def get_depth(self, levels: int = 5) -> Dict[str, List[OrderBookEntry]]:
        """ดึงข้อมูล N ระดับของ Order Book"""
        sorted_bids = sorted(self.bids.items(), reverse=True)[:levels]
        sorted_asks = sorted(self.asks.items())[:levels]
        
        return {
            "bids": [OrderBookEntry(p, q, "bid") for p, q in sorted_bids],
            "asks": [OrderBookEntry(p, q, "ask") for p, q in sorted_asks]
        }
        
    def calculate_vwap(self, depth_levels: int = 10) -> Dict[str, float]:
        """คำนวณ Volume Weighted Average Price"""
        depth = self.get_depth(depth_levels)
        
        for side in ["bids", "asks"]:
            entries = depth[side]
            if not entries:
                continue
                
            total_value = sum(e.total_value for e in entries)
            total_volume = sum(e.quantity for e in entries)
            
            if total_volume > 0:
                depth[side] = total_value / total_volume
            else:
                depth[side] = 0.0
                
        return depth


class BybitOrderBookManager:
    """จัดการ Order Book หลาย symbols พร้อมกัน"""
    
    WS_URL = "wss://stream.bybit.com/v5/public/spot"
    
    def __init__(self):
        self.order_books: Dict[str, OrderBook] = {}
        self.ws = None
        self.is_running = False
        self.message_count = 0
        self.start_time = 0
        
    async def on_message(self, ws, message):
        """จัดการข้อความเข้ามา"""
        try:
            data = json.loads(message)
            self.message_count += 1
            
            if data.get("type") == "message":
                topic = data.get("topic", "")
                
                # แยก symbol จาก topic
                if "orderbook" in topic:
                    symbol = topic.split(".")[-1]
                    
                    if symbol not in self.order_books:
                        self.order_books[symbol] = OrderBook(symbol)
                        
                    self.order_books[symbol].update(data)
                    
                    # แสดงข้อมูลทุก 100 ข้อความ
                    if self.message_count % 100 == 0:
                        self._print_summary(symbol)
                        
        except Exception as e:
            print(f"❌ Message processing error: {e}")
            
    def _print_summary(self, symbol: str):
        """แสดงสรุป Order Book"""
        ob = self.order_books.get(symbol)
        if not ob:
            return
            
        elapsed = time.time() - self.start_time
        msg_rate = self.message_count / elapsed if elapsed > 0 else 0
        
        print(f"\n📊 Order Book Summary - {symbol}")
        print(f"   Best Bid: ${ob.best_bid.price:,.2f} ({ob.best_bid.quantity} BTC)")
        print(f"   Best Ask: ${ob.best_ask.price:,.2f} ({ob.best_ask.quantity} BTC)")
        print(f"   Spread: ${ob.spread:.2f} ({ob.spread_percent:.4f}%)")
        print(f"   Messages/sec: {msg_rate:.1f}")
        
    async def subscribe(self, symbols: List[str]):
        """สมัครรับ Order Book สำหรับหลาย symbols"""
        subscribe_msg = {
            "op": "subscribe",
            "args": [f"orderbook.50.{symbol}" for symbol in symbols]
        }
        await self.ws.send_json(subscribe_msg)
        print(f"📡 Subscribed to {len(symbols)} order books")
        
    async def run(self, symbols: List[str]):
        """เริ่มการทำงาน"""
        self.is_running = True
        self.start_time = time.time()
        
        async with aiohttp.ClientSession() as session:
            async with session.ws_connect(self.WS_URL) as ws:
                self.ws = ws
                await self.subscribe(symbols)
                
                async for msg in ws:
                    if msg.type == aiohttp.WSMsgType.TEXT:
                        await self.on_message(ws, msg.data)
                    elif msg.type == aiohttp.WSMsgType.CLOSED:
                        print("🔴 Connection closed by server")
                        break
                    elif msg.type == aiohttp.WSMsgType.ERROR:
                        print(f"❌ WebSocket error: {ws.exception()}")
                        break

วิธีใช้งาน

async def main(): manager = BybitOrderBookManager(["BTCUSDT", "ETHUSDT"]) await manager.run(["BTCUSDT", "ETHUSDT"]) if __name__ == "__main__": asyncio.run(main())

ข้อผิดพลาดที่พบบ่อยและวิธีแก้ไข

จากประสบการณ์ที่ผมเจอมา รวบรวมข้อผิดพลาดที่พบบ่อยที่สุดพร้อมวิธีแก้ไข:

ข้อผิดพลาด สาเหตุ วิธีแก้ไข
ConnectionError: timeout after 5000ms Firewall หรือ Proxy บล็อก connection, เซิร์ฟเวอร์ Bybit มีปัญหา ตรวจสอบ Firewall, ใช้ WebSocket over HTTPS proxy, เพิ่ม ping interval
1006 - Abnormal Closure Connection idle นานเกินไป, Server restart, Network instability ส่ง ping ทุก 20 วินาที, เพิ่ม Auto-reconnect logic
Subscription limit exceeded สมัครรับ topics มากเกินไป (เกิน 20 topics) รวม symbols หลายตัวใน topic เดียว, ลดจำนวน subscriptions
Invalid JSON format ข้อมูลที่ได้รับเป็น binary แทน JSON, encoding ผิดพลาด ตรวจสอบว่าใช้ Public channel, เพิ่ม try-catch สำหรับ JSON parsing
Message queue full ประมวลผลข้อมูลช้าเกินไป, ใช้ single-thread ใช้ asyncio หรือ thread pool, ใช้ batch processing

รายละเอียดวิธีแก้ไขข้อผิดพลาด

# วิธีแก้ไข: Auto-reconnect อัจฉริยะ
import time
import random

class SmartReconnect:
    """Auto-reconnect ที่มี exponential backoff"""
    
    def __init__(self, max_retries=10, base_delay=1, max_delay=60):
        self.max_retries = max_retries
        self.base_delay = base_delay
        self.max_delay = max_delay
        self.retry_count = 0
        
    def should_retry(self) -> bool:
        return self.retry_count < self.max_retries
        
    def get_delay(self) -> float:
        """คำนวณ delay ด้วย exponential backoff + jitter"""
        delay = min(
            self.base_delay * (2 ** self.retry_count),
            self.max_delay
        )
        # เพิ่ม random jitter 10-20% เพื่อป้องกัน thundering herd
        jitter = delay * random.uniform(0.1, 0.2)
        return delay + jitter
        
    def on_failure(self, error: str):
        """เรียกเมื่อ connection ล้มเหลว"""
        self.retry_count += 1
        print(f"⚠️  Attempt {self.retry_count}/{self.max_retries} failed: {error}")
        print(f"   Waiting {self.get_delay():.1f}s before retry...")
        
    def on_success(self):
        """เรียกเมื่อ reconnect สำเร็จ"""
        print("✅ Reconnected successfully!")
        self.retry_count = 0
        
    def run_with_reconnect(self, connect_func, *args, **kwargs):
        """เรียก connect_func พร้อม auto-reconnect"""
        while self.should_retry():
            try:
                connect_func(*args, **kwargs)
                self.on_success()
                return True
            except Exception as e:
                self.on_failure(str(e))
                time.sleep(self.get_delay())
                
        print(f"❌ Max retries ({self.max_retries}) reached. Giving up.")
        return False

วิธีใช้งาน

reconnector = SmartReconnect(max_retries=5) reconnector.run_with_reconnect(your_connect_function)
# วิธีแก้ไข: ตรวจสอบ subscription limit
def check_subscription_limit(topics: List[str]) -> bool:
    """ตรวจสอบว่าไม่เกิน limit"""
    # Bybit V5 จำกัด 20 subscriptions ต่อ connection
    MAX_SUBSCRIPTIONS = 20
    
    if len(topics) > MAX_SUBSCRIPTIONS:
        print(f"❌ Too many subscriptions: {len(topics)} > {MAX_SUBSCRIPTIONS}")
        return False
    return True

วิธีใช้งาน

symbols = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT", "XRPUSDT"]

ถ้ามีหลาย symbols ใช้ wildcard หรือ batch

if len(symbols) > 5: # Option 1: ใช้ wildcard subscription (ถ้า Bybit รองรับ) # topics = ["tickers.BTCUSDT", "tickers.ETHUSDT"] # Option 2: สมัครเฉพาะ symbols ที่จำเป็น topics = [f"tickers.{s}" for s in symbols[:5]] # จำกัดแค่ 5 ตัว print(f"📡 Limited to {len(topics)} subscriptions")

Performance Optimization

สำหรับระบบ Production ที่ต้องรับข้อมูลปริมาณมาก ผมแนะนำให้ใช้เทคนิคต่อไปนี้:

# Performance: ใช้ Queue แยก Thread
import queue
import threading
from typing import Callable, Any

class AsyncWebSocketProcessor:
    """WebSocket พร้อม Queue สำหรับ async processing"""
    
    def __init__(self, batch_size: int = 100, batch_interval: float = 1.0):
        self.queue = queue.Queue(maxsize=10000)
        self.batch_size = batch_size
        self.batch_interval = batch_interval
        self.processors: list[Callable] = []
        
    def add_processor(self, processor: Callable[[list], None]):
        """เพิ่ม processor function"""
        self.processors.append(processor)
        
    def start_processing(self):
        """เริ่ม processing loop ใน thread แยก"""
        def process_loop():
            batch = []
            last_flush = time.time()
            
            while True:
                try:
                    # รอข้อมูล พร้อม timeout
                    item = self.queue.get(timeout=0.1)
                    batch.append(item)
                    
                    # Flush เมื่อครบ batch_size หรือเกิน interval
                    should_flush = (
                        len(batch) >= self.batch_size or
                        (time.time() - last_flush) >= self.batch_interval
                    )
                    
                    if should_flush and batch:
                        for processor in self.processors:
                            try:
                                processor(batch)
                            except Exception as e:
                                print(f"❌ Processor error: {e}")
                        batch = []
                        last_flush = time.time()
                        
                except queue.Empty:
                    # ถ้าไม่มีข้อมูล รอต่อ แล้ว flush ถ้าถึงเวลา
                    if (time.time() - last_flush) >= self.batch_interval and batch:
                        for processor in self.processors:
                            processor(batch)
                        batch = []
                        last_flush = time.time()
                        
        processor_thread = threading.Thread(target=process_loop, daemon=True)
        processor_thread.start()
        print("⚡ Async processor started")
        
    def enqueue(self, data: Any):
        """เพิ่มข้อมูลเข้า queue"""
        try:
            self.queue.put_nowait(data)
        except queue.Full:
            print("⚠️  Queue full, dropping oldest message")
            try:
                self.queue.get_nowait()
                self.queue.put_nowait(data)
            except:
                pass

วิธีใช้งาน

processor = AsyncWebSocketProcessor(batch_size=50, batch_interval=0.5) def save_to_database(batch): """บันทึกข้อมูลลง Database""" print(f"💾 Saving {len(batch)} records to database...") processor.add_processor(save_to_database) processor.start_processing()

ส่งข้อมูลเข้า queue

processor.enqueue({"symbol": "BTCUSDT", "price": 50000})

การใช้งานร่วมกับ AI Trading Bot

ถ้าคุณกำลังสร้าง AI Trading Bot ที่ใช้ Machine Learning หรือ LLM ในการวิเคราะห์ ผมแนะนำให้ใช้ HolySheep AI เป็น AI backend เพราะมีความเร็วในการตอบสนองต่ำกว่า 50 มิลลิวินาที ซึ่งเหมาะมากสำหรับ Trading ที่ต้องการ Latency ต่ำ

# ตัวอย่าง: Trading Bot ที่ใช้ WebSocket + AI Analysis
import aiohttp
import json

class AITradingBot:
    """Trading Bot ที่ใช้ WebSocket + HolySheep AI"""
    
    HOLYSHEEP_API_URL = "https://api.holysheep.ai/v1/chat/completions"
    
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.ws_manager = None
        self.position = None
        
    async def analyze_market(self, market_data: dict) -> str:
        """วิเคราะห์ตลาดด้วย AI"""
        prompt = f"""Analyze this market data and suggest action:
        
Current Data:
- Symbol: {market_data.get('symbol')}
- Price: ${market_data.get('price')}
- 24h Change: {market_data.get('change_24h')}%
- Volume: {market_data.get('volume')}
- Best Bid: ${market_data.get('best_bid')}
- Best Ask: ${market_data.get('best_ask')}

Respond with: BUY, SELL, or HOLD and brief reason."""
        
        headers = {
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json"
        }
        
        payload = {
            "model": "gpt-4.1",
            "messages": [{"role": "user", "content": prompt}],
            "temperature": 0.3,  # ลดความสุ่มสำหรับ trading
            "max_tokens": 100
        }
        
        async with aiohttp.ClientSession() as session:
            async with session.post(
                self.HOLYSHEEP_API_URL,
                headers=headers,
                json=payload
            ) as response:
                if response.status == 200:
                    result = await response.json()
                    return result["choices"][0]["message"]["content"]
                else:
                    return "HOLD - AI analysis failed"
                    
    async def on_ticker_update(self, ticker_data: dict):
        """จัดการเมื่อได้รับ Ticker update"""
        market_info = {
            "symbol": ticker_data.get("symbol"),