บทนำ: ทำไมข้อมูล Tick-by-Tick ถึงสำคัญ

ในโลกของ Algorithmic Trading และ Quantitative Research ข้อมูลการซื้อขายรายวินาที (Tick Data) คือ "ทองคำ" ของการวิเคราะห์ตลาด ผมเคยทำโปรเจกต์สร้างระบบ Backtest สำหรับสกุลเงินดิจิทัล และพบว่าข้อมูล OHLCV ธรรมดาไม่เพียงพอต่อการตรวจจับ Order Flow หรือ Liquidity Sniper ที่แม่นยำ OKX Exchange เป็นหนึ่งใน Exchange ที่มี API ให้บริการอย่างครบวงจร วันนี้ผมจะพาทุกท่านไปสร้าง Pipeline สำหรับเก็บข้อมูล Tick-by-Tick ผ่าน WebSocket และ Archive เป็น CSV สำหรับการวิเคราะห์ในอนาคต

สถาปัตยกรรมระบบโดยรวม

ระบบที่เราจะสร้างประกอบด้วย 3 ส่วนหลัก:
  1. Ingestion Layer: WebSocket Client สำหรับเชื่อมต่อ OKX Public API
  2. Processing Layer: Python Script สำหรับ Parse, Validate และ Aggregate ข้อมูล
  3. Storage Layer: Local File System สำหรับเก็บ CSV แบบ Daily Partition

การตั้งค่า Environment และ Dependencies

ก่อนเริ่มต้น ติดตั้ง Package ที่จำเป็น:
pip install okx-sdk websockets pandas numpy python-dotenv asyncio
สร้างไฟล์ .env สำหรับเก็บ Configuration:
# OKX API Configuration
OKX_WS_ENDPOINT=wss://ws.okx.com:8443/ws/v5/public
OKX_ENABLE_SANDBOX=false

Storage Configuration

DATA_OUTPUT_DIR=./tick_data LOG_LEVEL=INFO

WebSocket Client สำหรับ OKX Trade Channel

OKX ใช้ WebSocket Protocol สำหรับการรับข้อมูล Real-time โดย Trade Channel จะส่งข้อมูลทุกครั้งที่มี Order Match เกิดขึ้น:
import asyncio
import json
import websockets
import pandas as pd
from datetime import datetime
from pathlib import Path
from typing import List, Dict
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class OKXTradeCollector:
    """
    WebSocket Client สำหรับเก็บข้อมูล Tick-by-Tick จาก OKX
    รองรับหลาย Symbols พร้อมกัน
    """
    
    def __init__(self, symbols: List[str], output_dir: str = "./tick_data"):
        self.ws_endpoint = "wss://ws.okx.com:8443/ws/v5/public"
        self.symbols = [s.upper() for s in symbols]
        self.output_dir = Path(output_dir)
        self.output_dir.mkdir(parents=True, exist_ok=True)
        self.trade_buffer = []
        self.buffer_size = 1000  # Flush ทุก 1000 records
        
    def _build_subscribe_message(self) -> dict:
        """สร้าง Subscribe Message สำหรับ OKX WebSocket"""
        args = []
        for symbol in self.symbols:
            # OKX ใช้ format: {instType}-{instId}
            # สำหรับ SPOT: BTC-USDT
            inst_id = symbol.replace("/", "-")
            args.append({
                "channel": "trades",
                "instId": symbol
            })
        return {
            "op": "subscribe",
            "args": args
        }
    
    async def _write_to_csv(self, data: List[Dict]):
        """เขียนข้อมูลลง CSV แบบ Daily Partition"""
        if not data:
            return
            
        df = pd.DataFrame(data)
        
        # แปลง Timestamp
        df['ts'] = pd.to_datetime(df['ts'], unit='ms')
        date_str = df['ts'].dt.strftime('%Y%m%d').iloc[0]
        
        # เพิ่ม Metadata columns
        df['date'] = date_str
        df['collected_at'] = datetime.now().isoformat()
        
        # ตรวจสอบว่า Symbol เป็น Spot หรือ Futures
        symbol = df['instId'].iloc[0]
        if '-' in symbol:
            inst_type = 'SPOT' if len(symbol.split('-')) == 2 else 'FUTURES'
        else:
            inst_type = 'UNKNOWN'
        df['inst_type'] = inst_type
        
        # เขียนลง CSV
        output_file = self.output_dir / f"trades_{symbol}_{date_str}.csv"
        mode = 'a' if output_file.exists() else 'w'
        header = mode == 'w'
        df.to_csv(output_file, mode=mode, header=header, index=False)
        
        logger.info(f"Written {len(data)} trades to {output_file.name}")
    
    async def _process_message(self, message: str):
        """Process ข้อมูลที่ได้รับจาก WebSocket"""
        try:
            data = json.loads(message)
            
            # Handle subscribe confirmation
            if 'event' in data:
                logger.info(f"Subscription event: {data}")
                return
                
            # Handle trade data
            if 'data' in data:
                for trade in data['data']:
                    processed_trade = {
                        'instId': trade['instId'],
                        'tradeId': trade['tradeId'],
                        'px': float(trade['px']),
                        'sz': float(trade['sz']),
                        'side': trade['side'],
                        'ts': int(trade['ts']),
                        'tickPx': float(trade.get('tickPx', trade['px'])),
                        'markPx': float(trade.get('markPx', 0))
                    }
                    self.trade_buffer.append(processed_trade)
                
                # Flush buffer เมื่อถึง threshold
                if len(self.trade_buffer) >= self.buffer_size:
                    await self._write_to_csv(self.trade_buffer)
                    self.trade_buffer = []
                    
        except json.JSONDecodeError as e:
            logger.error(f"JSON decode error: {e}")
        except Exception as e:
            logger.error(f"Process error: {e}")
    
    async def start(self, duration_seconds: int = None):
        """เริ่มเชื่อมต่อ WebSocket และเก็บข้อมูล"""
        logger.info(f"Starting OKX Trade Collector for: {self.symbols}")
        
        subscribe_msg = self._build_subscribe_message()
        
        try:
            async with websockets.connect(self.ws_endpoint) as ws:
                # Subscribe to channels
                await ws.send(json.dumps(subscribe_msg))
                logger.info("Subscription message sent")
                
                # Receive messages
                start_time = asyncio.get_event_loop().time()
                
                while True:
                    if duration_seconds:
                        elapsed = asyncio.get_event_loop().time() - start_time
                        if elapsed >= duration_seconds:
                            logger.info(f"Duration {duration_seconds}s reached, stopping...")
                            break
                    
                    message = await asyncio.wait_for(ws.recv(), timeout=30.0)
                    await self._process_message(message)
                    
        except websockets.exceptions.ConnectionClosed as e:
            logger.warning(f"Connection closed: {e}")
        except asyncio.TimeoutError:
            logger.warning("Connection timeout, attempting reconnect...")
        finally:
            # Flush remaining buffer
            if self.trade_buffer:
                await self._write_to_csv(self.trade_buffer)


ตัวอย่างการใช้งาน

async def main(): collector = OKXTradeCollector( symbols=["BTC-USDT", "ETH-USDT", "SOL-USDT"], output_dir="./tick_data" ) # เก็บข้อมูล 1 ชั่วโมง (3600 วินาที) await collector.start(duration_seconds=3600) if __name__ == "__main__": asyncio.run(main())

Batch Download Historical Data ผ่าน REST API

นอกจาก Real-time WebSocket แล้ว OKX ยังมี REST API สำหรับดาวน์โหลด Historical Trades ซึ่งเหมาะสำหรับการ Backfill ข้อมูลย้อนหลัง:
import requests
import pandas as pd
import time
from datetime import datetime, timedelta
from typing import List, Optional
from concurrent.futures import ThreadPoolExecutor, as_completed


class OKXHistoricalTradeDownloader:
    """
    Download Historical Trade Data จาก OKX REST API
    รองรับการดาวน์โหลดแบบ Batch พร้อม Rate Limiting
    """
    
    BASE_URL = "https://www.okx.com"
    
    def __init__(self, output_dir: str = "./historical_trades"):
        self.output_dir = Path(output_dir)
        self.output_dir.mkdir(parents=True, exist_ok=True)
        self.session = requests.Session()
        self.session.headers.update({
            'Content-Type': 'application/json',
            'User-Agent': 'OKX-TickData-Downloader/1.0'
        })
        
    def _rate_limit(self, calls_per_second: int = 10):
        """Simple Rate Limiter"""
        time.sleep(1 / calls_per_second)
    
    def get_historical_trades(
        self,
        instId: str,
        after: Optional[int] = None,
        before: Optional[int] = None,
        limit: int = 100
    ) -> List[dict]:
        """
        ดึงข้อมูล Trade History จาก OKX API
        
        Args:
            instId: Instrument ID เช่น BTC-USDT
            after: Cursor สำหรับดึงข้อมูลหลัง timestamp นี้ (milliseconds)
            before: Cursor สำหรับดึงข้อมูลก่อน timestamp นี้
            limit: จำนวน records ต่อ request (max 100)
            
        Returns:
            List of trade records
        """
        endpoint = f"{self.BASE_URL}/api/v5/market/trades"
        
        params = {
            'instId': instId,
            'limit': min(limit, 100)
        }
        
        if after:
            params['after'] = after
        if before:
            params['before'] = before
            
        response = self.session.get(endpoint, params=params)
        response.raise_for_status()
        
        data = response.json()
        
        if data.get('code') != '0':
            raise Exception(f"API Error: {data.get('msg')}")
            
        return data.get('data', [])
    
    def download_date_range(
        self,
        instId: str,
        start_date: datetime,
        end_date: datetime,
        progress_callback=None
    ) -> str:
        """
        ดาวน์โหลดข้อมูลในช่วงวันที่กำหนด
        
        Returns:
            Path ของไฟล์ CSV ที่ดาวน์โหลดสำเร็จ
        """
        all_trades = []
        
        # Convert dates to milliseconds
        current_ts = int(end_date.timestamp() * 1000)
        end_ts = int(start_date.timestamp() * 1000)
        
        print(f"Downloading {instId} from {start_date} to {end_date}")
        
        while current_ts > end_ts:
            self._rate_limit(10)  # OKX rate limit
            
            trades = self.get_historical_trades(
                instId=instId,
                after=current_ts,
                limit=100
            )
            
            if not trades:
                break
                
            all_trades.extend(trades)
            
            # ใช้ timestamp ของ record ล่าสุดเป็น cursor สำหรับ request ถัดไป
            last_ts = int(trades[-1]['ts'])
            current_ts = last_ts - 1  # ขอข้อมูลก่อน record ล่าสุด
            
            if progress_callback:
                progress_callback(len(all_trades))
                
            print(f"  Downloaded {len(all_trades)} trades... (last ts: {last_ts})")
            
        # Convert to DataFrame and save
        df = pd.DataFrame(all_trades)
        
        # Parse timestamp
        df['datetime'] = pd.to_datetime(df['ts'], unit='ms')
        df['date'] = df['datetime'].dt.strftime('%Y%m%d')
        
        # Convert numeric columns
        df['px'] = df['px'].astype(float)
        df['sz'] = df['sz'].astype(float)
        df['tradeSz'] = df['tradeSz'].astype(float)
        
        # Sort by timestamp
        df = df.sort_values('ts').reset_index(drop=True)
        
        # Save to CSV
        date_str = start_date.strftime('%Y%m%d')
        output_file = self.output_dir / f"trades_{instId}_{date_str}.csv"
        df.to_csv(output_file, index=False)
        
        print(f"Saved {len(df)} records to {output_file}")
        
        return str(output_file)
    
    def batch_download(
        self,
        symbols: List[str],
        days_back: int = 7
    ):
        """
        ดาวน์โหลดข้อมูลหลาย Symbols พร้อมกัน
        """
        end_date = datetime.now()
        start_date = end_date - timedelta(days=days_back)
        
        results = []
        
        with ThreadPoolExecutor(max_workers=3) as executor:
            futures = {
                executor.submit(
                    self.download_date_range,
                    symbol,
                    start_date,
                    end_date
                ): symbol
                for symbol in symbols
            }
            
            for future in as_completed(futures):
                symbol = futures[future]
                try:
                    result = future.result()
                    results.append(result)
                    print(f"✓ {symbol} completed: {result}")
                except Exception as e:
                    print(f"✗ {symbol} failed: {e}")
                    
        return results


ตัวอย่างการใช้งาน

if __name__ == "__main__": downloader = OKXHistoricalTradeDownloader(output_dir="./historical_trades") symbols = ["BTC-USDT", "ETH-USDT", "SOL-USDT"] # ดาวน์โหลดย้อนหลัง 7 วัน results = downloader.batch_download(symbols, days_back=7)

Data Schema และ Field Description

ข้อมูล Tick-by-Tick ที่ได้จาก OKX ประกอบด้วย Field สำคัญดังนี้:
FieldTypeDescriptionExample
tradeIdstringUnique Trade ID จาก Exchange496823348674851858
instIdstringInstrument IDBTC-USDT
pxfloatราคาที่เกิดการ Match42150.50
szfloatขนาดของ Order ที่ถูก Match0.001
sidestringฝั่งของ Order ที่ถูก Match (buy/sell)buy
tsintegerTimestamp เป็น milliseconds1703894400000
tickPxfloatราคา Tick ล่าสุด42150.50
indexPxfloatราคา Index (สำหรับ Derivatives)42148.23
markPxfloatราคา Mark (สำหรับ Derivatives)42149.10

การใช้ Tick Data ร่วมกับ AI Models สำหรับ Trading Analysis

ข้อมูล Tick-by-Tick สามารถนำไปใช้กับ AI Models ได้หลากหลาย เช่น: สำหรับการประมวลผลข้อมูลจำนวนมากด้วย AI Models ผมแนะนำให้ใช้ HolySheep AI ซึ่งมีความเร็วในการประมวลผลต่ำกว่า 50ms และรองรับ Models หลากหลาย เช่น GPT-4.1, Claude Sonnet 4.5, Gemini 2.5 Flash และ DeepSeek V3.2

ตัวอย่างการใช้ HolySheep AI สำหรับ Order Flow Analysis

หลังจากเก็บข้อมูล Tick แล้ว คุณสามารถใช้ HolySheep API สำหรับวิเคราะห์ด้วย LLM ได้ทันที:
import requests
import pandas as pd
from pathlib import Path

HolySheep AI Configuration

HOLYSHEEP_API_KEY = "YOUR_HOLYSHEEP_API_KEY" HOLYSHEEP_BASE_URL = "https://api.holysheep.ai/v1" def analyze_order_flow_with_ai(csv_file: str, api_key: str) -> dict: """ ใช้ AI วิเคราะห์ Order Flow จากไฟล์ CSV """ # อ่านข้อมูล Tick df = pd.read_csv(csv_file) # คำนวณ Order Flow Metrics buy_volume = df[df['side'] == 'buy']['sz'].sum() sell_volume = df[df['side'] == 'sell']['sz'].sum() total_volume = buy_volume + sell_volume buy_ratio = buy_volume / total_volume if total_volume > 0 else 0.5 # สร้าง Summary สำหรับ AI summary = f""" Order Flow Analysis Summary: - Total Trades: {len(df)} - Buy Volume: {buy_volume:.4f} - Sell Volume: {sell_volume:.4f} - Buy Ratio: {buy_ratio:.2%} - Price Range: {df['px'].min():.2f} - {df['px'].max():.2f} - Unique Timestamps: {df['ts'].nunique()} """ # เรียก HolySheep AI API response = requests.post( f"{HOLYSHEEP_BASE_URL}/chat/completions", headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" }, json={ "model": "gpt-4.1", "messages": [ { "role": "system", "content": """You are an expert in cryptocurrency order flow analysis. Analyze the provided order flow metrics and provide insights about: 1. Market sentiment (bullish/bearish/neutral) 2. Potential whale activities 3. Recommendations for trading decisions """ }, { "role": "user", "content": f"Analyze this order flow data:\n{summary}" } ], "temperature": 0.3, "max_tokens": 500 } ) result = response.json() return { "metrics": { "buy_volume": buy_volume, "sell_volume": sell_volume, "buy_ratio": buy_ratio }, "ai_analysis": result.get("choices", [{}])[0].get("message", {}).get("content", ""), "usage": result.get("usage", {}) } def batch_analyze_trading_days( data_dir: str = "./tick_data", symbol: str = "BTC-USDT" ): """ วิเคราะห์ Order Flow หลายวันพร้อมกัน """ import os csv_files = [ os.path.join(data_dir, f) for f in os.listdir(data_dir) if symbol in f and f.endswith('.csv') ] results = [] for csv_file in csv_files: print(f"Analyzing {csv_file}...") analysis = analyze_order_flow_with_ai( csv_file, HOLYSHEEP_API_KEY ) results.append({ "file": Path(csv_file).name, "buy_ratio": analysis["metrics"]["buy_ratio"], "analysis": analysis["ai_analysis"] }) print(f" Buy Ratio: {analysis['metrics']['buy_ratio']:.2%}") # สร้างรายงานสรุป summary_df = pd.DataFrame(results) summary_df.to_csv(f"./order_flow_summary_{symbol}.csv", index=False) return results

ตัวอย่างการใช้งาน

if __name__ == "__main__": result = analyze_order_flow_with_ai( "./tick_data/trades_BTC-USDT_20240130.csv", HOLYSHEEP_API_KEY ) print("AI Analysis:") print(result["ai_analysis"]) print(f"\nToken Usage: {result['usage']}")

Best Practices สำหรับ Production Deployment

1. Database Storage แทน File System

สำหรับข้อมูลจำนวนมาก แนะนำให้ใช้ TimescaleDB หรือ InfluxDB แทน CSV:
from sqlalchemy import create_engine, Column, BigInteger, Float, String, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from datetime import datetime

Base = declarative_base()


class TradeRecord(Base):
    __tablename__ = 'trades'
    
    id = Column(BigInteger, primary_key=True, autoincrement=True)
    trade_id = Column(String(50), unique=True, index=True)
    inst_id = Column(String(20), index=True)
    price = Column(Float)
    size = Column(Float)
    side = Column(String(10))
    timestamp = Column(BigInteger, index=True)
    created_at = Column(DateTime, default=datetime.utcnow)
    
    # Composite index for time-range queries
    __table_args__ = (
        Index('idx_inst_timestamp', 'inst_id', 'timestamp'),
    )


class TradeDatabaseManager:
    def __init__(self, connection_string: str):
        self.engine = create_engine(connection_string)
        Base.metadata.create_all(self.engine)
        self.Session = sessionmaker(bind=self.engine)
    
    def bulk_insert(self, trades: List[dict], batch_size: int = 1000):
        session = self.Session()
        
        try:
            for i in range(0, len(trades), batch_size):
                batch = trades[i:i + batch_size]
                records = [
                    TradeRecord(
                        trade_id=t['tradeId'],
                        inst_id=t['instId'],
                        price=float(t['px']),
                        size=float(t['sz']),
                        side=t['side'],
                        timestamp=int(t['ts'])
                    )
                    for t in batch
                ]
                session.bulk_save_objects(records)
                session.commit()
                
            logger.info(f"Inserted {len(trades)} trades")
            
        except Exception as e:
            session.rollback()
            logger.error(f"Insert failed: {e}")
            raise
        finally:
            session.close()
    
    def query_range(
        self, 
        inst_id: str, 
        start_ts: int, 
        end_ts: int
    ) -> pd.DataFrame:
        session = self.Session()
        
        try:
            result = session.query(TradeRecord).filter(
                TradeRecord.inst_id == inst_id,
                TradeRecord.timestamp >= start_ts,
                TradeRecord.timestamp <= end_ts
            ).order_by(TradeRecord.timestamp).all()
            
            return pd.DataFrame([{
                'tradeId': r.trade_id,
                'instId': r.inst_id,
                'px': r.price,
                'sz': r.size,
                'side': r.side,
                'ts': r.timestamp
            } for r in result])
            
        finally:
            session.close()

2. High Availability Setup

สำหรับ Production แนะนำให้ใช้ Redis สำหรับ Reconnection และ State Management:
import redis.asyncio as redis
import json
import asyncio


class RedisStateManager:
    def __init__(self, redis_url: str = "redis://localhost:6379"):
        self.redis = redis.from_url(redis_url, decode_responses=True)
        
    async def save_last_cursor(self, symbol: str, ts: int):
        """เก็บ Cursor ล่าสุดสำหรับ Reconnection"""
        key = f"okx:cursor:{symbol}"
        await self.redis.set(key, str(ts))
        await self.redis.expire(key, 86400 * 7)  # 7 days TTL
        
    async def get_last_cursor(self, symbol: str) -> int:
        """ดึง Cursor ล่าสุด"""
        key = f"okx:cursor:{symbol}"
        cursor = await self.redis.get(key)
        return int(cursor) if cursor else None
    
    async def increment_counter(self, metric: str):
        """นับจำนวน Trades ที่รับได้"""
        key = f"okx:counter:{metric}"
        return await self.redis.incr(key)
    
    async def health_check(self) -> bool:
        """ตรวจสอบ Connection"""
        try:
            await self.redis.ping()
            return True
        except:
            return False

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

1. WebSocket Connection Closed Unexpectedly (Error Code 1006)

ปัญหานี้เกิดจาก Server ปิด Connection โดยไม่มี Close Frame ซึ่งมักเกิดจาก:
# ❌ วิธีที่ไม่ถูกต้อง - ไม่มี Reconnection Logic
async def connect_websocket():
    async with websockets.connect(WSS_URL) as ws:
        while True:
            msg = await ws.recv()
            process(msg)
# ✅ วิธีที่ถูกต้อง - Implement Reconnection with Exponential Backoff
import asyncio


class RobustWebSocketClient:
    def __init__(self, url, max_retries=10, base_delay=1):
        self.url = url
        self.max_retries = max_retries
        self.base_delay = base_delay
        
    async def connect_with_retry(self):
        retries = 0
        
        while retries < self.max_retries:
            try:
                async with websockets.connect(self.url) as ws:
                    logger.info(f"Connected successfully")
                    retries = 0  # Reset on successful connection
                    
                    while True:
                        msg = await asyncio.wait_for(ws.recv(), timeout=30)
                        await self.process(msg)
                        
            except websockets.exceptions.ConnectionClosed as e:
                retries += 1
                delay = min(self.base_delay * (2 ** retries), 60)
                
                logger.warning(
                    f"Connection closed: {e.code} - "
                    f"Reconnecting in {delay}s (attempt {retries}/{self.max_retries})"
                )
                
                await asyncio.sleep(delay