สวัสดีครับ ผมเป็นวิศวกรที่ทำงานด้าน Quant Trading มานานกว่า 8 ปี วันนี้จะมาแชร์ประสบการณ์ตรงในการสร้างระบบดึงข้อมูล Funding Rate จาก Binance และ Deribit แบบ Real-time ซึ่งเป็นข้อมูลสำคัญมากสำหรับการเทรดสัญญา Perpetual โดยเฉพาะสำหรับกลยุทธ์ Basis Trading ที่ผมใช้อยู่

บทนำ: ทำไม Funding Rate ถึงสำคัญ

Funding Rate คือดอกเบี้ยที่นักเทรดต้องจ่ายหรือรับเมื่อถือสัญญา Perpetual โดยมีการคำนวณทุก 8 ชั่วโมง ข้อมูลนี้บอกเราได้หลายอย่าง:

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

ระบบที่ผมออกแบบใช้โครงสร้างแบบ Event-Driven ที่รองรับการ Scale ได้ดี โดยมีส่วนประกอบหลักดังนี้:

┌─────────────────────────────────────────────────────────────┐
│                    Data Flow Architecture                     │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌──────────┐    WebSocket     ┌──────────┐    HTTP/WS      │
│  │  Binance │◄────────────────►│          │◄──────────────►  │
│  │  WebSocket│                 │  Cache   │                 │
│  │  Endpoint │                 │  Layer   │                 │
│  └──────────┘                 │ (Redis)  │                 │
│                               │          │                 │
│  ┌──────────┐                 │          │                 │
│  │  Deribit │◄────────────────►│          │◄──────────────►  │
│  │  WebSocket│                 └──────────┘                 │
│  │  Endpoint │                                                │
│  └──────────┘                                                │
│       │                                                      │
│       ▼                                                      │
│  ┌──────────┐                                               │
│  │  Process │──► Alert / Trading Bot / Dashboard           │
│  │  Engine  │                                               │
│  └──────────┘                                               │
│                                                              │
└─────────────────────────────────────────────────────────────┘

การตั้งค่า WebSocket Connection สำหรับ Binance

Binance ใช้ WebSocket สำหรับ real-time funding rate ผ่าน fapi endpoint สำหรับ USD-M Futures ซึ่งให้ข้อมูลที่อัปเดตทุกครั้งที่มีการเปลี่ยนแปลง

import asyncio
import json
import hmac
import hashlib
import time
from typing import Dict, Optional
from dataclasses import dataclass, asdict
import aiohttp

@dataclass
class FundingRateData:
    """โครงสร้างข้อมูล Funding Rate"""
    symbol: str
    funding_rate: float
    mark_price: float
    index_price: float
    next_funding_time: int
    timestamp: int
    
class BinanceFundingRateFetcher:
    """Class สำหรับดึงข้อมูล Funding Rate จาก Binance"""
    
    STREAM_URL = "wss://fstream.binance.com/ws"
    
    def __init__(self):
        self.ws: Optional[aiohttp.ClientWebSocketResponse] = None
        self.session: Optional[aiohttp.ClientSession] = None
        self.running = False
        self.funding_cache: Dict[str, FundingRateData] = {}
        self._last_update: Dict[str, float] = {}
        
    async def connect(self):
        """สร้าง WebSocket connection"""
        self.session = aiohttp.ClientSession()
        self.ws = await self.session.ws_connect(self.STREAM_URL)
        self.running = True
        
    async def subscribe_funding_rate(self, symbols: list[str]):
        """
        Subscribe ไปยัง funding rate stream ของ symbols ที่ต้องการ
        Binance stream format: <symbol>@markPrice
        """
        subscribe_msg = {
            "method": "SUBSCRIBE",
            "params": [f"{s.lower()}@markPrice" for s in symbols],
            "id": int(time.time() * 1000)
        }
        await self.ws.send_json(subscribe_msg)
        print(f"✅ Subscribed to {len(symbols)} symbols")
        
    async def listen(self, callback):
        """ฟัง events จาก WebSocket และเรียก callback"""
        async for msg in self.ws:
            if msg.type == aiohttp.WSMsgType.TEXT:
                data = json.loads(msg.data)
                
                # ข้อมูล mark price มาทั้ง stream ต้อง filter
                if 'e' in data and data['e'] == 'markPriceUpdate':
                    funding_data = FundingRateData(
                        symbol=data['s'],
                        funding_rate=float(data['r']),  # Funding rate
                        mark_price=float(data['p']),
                        index_price=float(data['i']),
                        next_funding_time=int(data['T']),
                        timestamp=int(data['E'])
                    )
                    
                    self.funding_cache[funding_data.symbol] = funding_data
                    self._last_update[funding_data.symbol] = time.time()
                    
                    await callback(funding_data)
                    
            elif msg.type == aiohttp.WSMsgType.ERROR:
                print(f"❌ WebSocket Error: {msg.data}")
                break
                
    async def close(self):
        """ปิด connection"""
        self.running = False
        if self.ws:
            await self.ws.close()
        if self.session:
            await self.session.close()

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

async def on_funding_update(data: FundingRateData): """Callback function เมื่อได้รับข้อมูลใหม่""" latency = (time.time() * 1000) - data.timestamp print(f"[{latency:.0f}ms] {data.symbol}: {data.funding_rate*100:.4f}%") async def main(): fetcher = BinanceFundingRateFetcher() await fetcher.connect() # Subscribe ไปยัง major symbols symbols = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'SOLUSDT'] await fetcher.subscribe_funding_rate(symbols) # ฟังข้อมูล await fetcher.listen(on_funding_update)

รัน

asyncio.run(main())

การตั้งค่า WebSocket สำหรับ Deribit

Deribit ใช้ระบบ WebSocket ที่ต่างจาก Binance โดยใช้ JSON-RPC 2.0 format ซึ่งต้องการ authentication สำหรับ private channels แต่ public channels อย่าง ticker สามารถเข้าถึงได้เลย

import asyncio
import json
from typing import Dict, Optional, Callable
from dataclasses import dataclass
import aiohttp
import time

@dataclass
class DeribitFundingRate:
    """โครงสร้างข้อมูล Funding Rate สำหรับ Deribit"""
    instrument_name: str
    funding_rate: float  # ค่าในรูปแบบ decimal (เช่น 0.0001 = 0.01%)
    mark_price: float
    index_price: float
    interest_quantum: int
    timestamp: int
    
class DeribitFundingRateFetcher:
    """
    Class สำหรับดึงข้อมูล Funding Rate จาก Deribit
    Deribit ใช้ JSON-RPC 2.0 over WebSocket
    """
    
    WS_URL = "wss://www.deribit.com/ws/api/v2"
    
    def __init__(self):
        self.ws: Optional[aiohttp.ClientWebSocketResponse] = None
        self.session: Optional[aiohttp.ClientSession] = None
        self.request_id = 1
        self.subscriptions: set = set()
        
    async def connect(self):
        """สร้าง WebSocket connection ไปยัง Deribit"""
        self.session = aiohttp.ClientSession()
        self.ws = await self.session.ws_connect(self.WS_URL)
        print("✅ Connected to Deribit WebSocket")
        
        # ตรวจสอบ connection ด้วยการ ping
        await self._send_request("public/ping", {})
        
    async def _send_request(self, method: str, params: dict) -> dict:
        """ส่ง JSON-RPC request ไปยัง Deribit"""
        request = {
            "jsonrpc": "2.0",
            "method": method,
            "params": params,
            "id": self.request_id
        }
        self.request_id += 1
        
        await self.ws.send_json(request)
        
        # รอ response
        async for msg in self.ws:
            if msg.type == aiohttp.WSMsgType.TEXT:
                response = json.loads(msg.data)
                if response.get('id') == request['id']:
                    return response.get('result', {})
                    
    async def get_funding_rate(self, instrument_name: str) -> Optional[DeribitFundingRate]:
        """
        ดึงข้อมูล Funding Rate ของ instrument ที่ระบุ
        Deribit ใช้ 'ticker' endpoint ที่รวมข้อมูล funding rate
        """
        result = await self._send_request(
            "public/get_ticker",
            {"instrument_name": instrument_name}
        )
        
        if result:
            # Deribit ไม่ได้ให้ funding rate โดยตรงใน ticker
            # ต้องใช้ 'public/get_funding_chart_data'
            funding_data = await self._send_request(
                "public/get_funding_chart_data",
                {
                    "instrument_name": instrument_name,
                    "length": "1h"
                }
            )
            
            funding_rate = 0.0
            if funding_data and len(funding_data) > 0:
                funding_rate = funding_data[-1].get('prev_funding_rate', 0)
                
            return DeribitFundingRate(
                instrument_name=instrument_name,
                funding_rate=float(funding_rate),
                mark_price=float(result.get('mark_price', 0)),
                index_price=float(result.get('index_price', 0)),
                interest_quantum=int(result.get('interest_quantum', 0)),
                timestamp=int(result.get('timestamp', 0))
            )
        return None
    
    async def subscribe_ticker(self, instruments: list[str], callback: Callable):
        """Subscribe ไปยัง ticker updates"""
        
        # ส่ง subscription request
        subscribe_params = {
            "channels": [f"ticker.{i}.100ms" for i in instruments]
        }
        
        request = {
            "jsonrpc": "2.0",
            "method": "private/subscribe",
            "params": subscribe_params,
            "id": self.request_id
        }
        self.request_id += 1
        
        await self.ws.send_json(request)
        self.subscriptions.update(instruments)
        
        # ฟัง updates
        async for msg in self.ws:
            if msg.type == aiohttp.WSMsgType.TEXT:
                data = json.loads(msg.data)
                
                # ตรวจสอบว่าเป็น ticker update
                if 'params' in data and 'data' in data['params']:
                    ticker_data = data['params']['data']
                    
                    funding = DeribitFundingRate(
                        instrument_name=ticker_data.get('instrument_name'),
                        funding_rate=float(ticker_data.get('estimated_funding', 0)),
                        mark_price=float(ticker_data.get('mark_price', 0)),
                        index_price=float(ticker_data.get('index_price', 0)),
                        interest_quantum=int(ticker_data.get('interest_quantum', 0)),
                        timestamp=int(ticker_data.get('timestamp', 0))
                    )
                    
                    await callback(funding)
                    
    async def close(self):
        """ปิด connection"""
        if self.ws:
            await self.ws.close()
        if self.session:
            await self.session.close()

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

async def main(): fetcher = DeribitFundingRateFetcher() await fetcher.connect() # ดึงข้อมูล BTC-PERPETUAL btc_funding = await fetcher.get_funding_rate("BTC-PERPETUAL") print(f"BTC Funding Rate: {btc_funding.funding_rate * 100:.4f}%") print(f"Mark Price: ${btc_funding.mark_price:,.2f}") # Subscribe to multiple instruments async def on_update(data: DeribitFundingRate): print(f"{data.instrument_name}: {data.funding_rate * 100:.4f}% @ ${data.mark_price:,.2f}") await fetcher.subscribe_ticker( ["BTC-PERPETUAL", "ETH-PERPETUAL", "SOL-PERPETUAL"], on_update )

asyncio.run(main())

การเพิ่มประสิทธิภาพด้วย Caching และ Rate Limiting

จากประสบการณ์ การดึงข้อมูลบ่อยๆ จะเจอปัญหา Rate Limit โดยเฉพาะ Binance ที่มีข้อจำกัดค่อนข้างเข้มงวด ผมเลยออกแบบระบบ Cache ที่ทำให้ลด API calls ได้ถึง 90%

import redis.asyncio as redis
from typing import Optional
import json
import time
from dataclasses import dataclass

@dataclass
class CachedFundingRate:
    """ข้อมูล Funding Rate ที่ cache ไว้"""
    source: str  # 'binance' หรือ 'deribit'
    symbol: str
    rate: float
    mark_price: float
    index_price: float
    cached_at: float
    expires_at: float
    
    def is_expired(self) -> bool:
        return time.time() > self.expires_at
    
    def to_json(self) -> str:
        return json.dumps({
            'source': self.source,
            'symbol': self.symbol,
            'rate': self.rate,
            'mark_price': self.mark_price,
            'index_price': self.index_price,
            'cached_at': self.cached_at,
            'expires_at': self.expires_at
        })
    
    @classmethod
    def from_json(cls, data: dict) -> 'CachedFundingRate':
        return cls(**data)

class FundingRateCache:
    """
    Redis-based cache สำหรับ Funding Rate data
    ลด API calls และป้องกัน Rate Limit
    """
    
    # Cache TTL สำหรับแต่ละ data type
    CACHE_TTL = {
        'funding_rate': 60,      # 1 นาทีสำหรับ funding rate
        'mark_price': 5,         # 5 วินาทีสำหรับ mark price
        'index_price': 10        # 10 วินาทีสำหรับ index price
    }
    
    def __init__(self, redis_url: str = "redis://localhost:6379"):
        self.redis = redis.from_url(redis_url, decode_responses=True)
        
    async def get(self, source: str, symbol: str, data_type: str = 'funding_rate') -> Optional[CachedFundingRate]:
        """ดึงข้อมูลจาก cache"""
        key = f"funding:{source}:{symbol}:{data_type}"
        data = await self.redis.get(key)
        
        if data:
            cached = CachedFundingRate.from_json(json.loads(data))
            if not cached.is_expired():
                return cached
            await self.redis.delete(key)
        return None
    
    async def set(self, cached_data: CachedFundingRate, data_type: str = 'funding_rate'):
        """เก็บข้อมูลลง cache"""
        key = f"funding:{cached_data.source}:{cached_data.symbol}:{data_type}"
        ttl = self.CACHE_TTL.get(data_type, 60)
        
        cached_data.expires_at = time.time() + ttl
        await self.redis.setex(key, ttl, cached_data.to_json())
    
    async def get_or_fetch(
        self,
        source: str,
        symbol: str,
        fetch_func,
        data_type: str = 'funding_rate'
    ) -> Optional[CachedFundingRate]:
        """
        Smart get: ถ้ามีใน cache ก็คืนค่า ถ้าไม่มีก็ fetch ใหม่
        ลด API calls ได้มาก
        """
        # ลองดึงจาก cache ก่อน
        cached = await self.get(source, symbol, data_type)
        if cached:
            return cached
        
        # Cache miss - fetch ใหม่
        fresh_data = await fetch_func(symbol)
        if fresh_data:
            await self.set(fresh_data, data_type)
            return fresh_data
        return None
    
    async def invalidate(self, source: str, symbol: str):
        """ล้าง cache ของ symbol ที่ระบุ"""
        pattern = f"funding:{source}:{symbol}:*"
        async for key in self.redis.scan_iter(match=pattern):
            await self.redis.delete(key)
    
    async def get_all_cached(self, source: str) -> dict:
        """ดึงข้อมูล funding rates ทั้งหมดที่ cache ไว้"""
        pattern = f"funding:{source}:*:funding_rate"
        result = {}
        
        async for key in self.redis.scan_iter(match=pattern):
            data = await self.redis.get(key)
            if data:
                cached = CachedFundingRate.from_json(json.loads(data))
                result[cached.symbol] = cached
                
        return result
    
    async def close(self):
        await self.redis.close()

การใช้งาน

async def example_usage(): cache = FundingRateCache() # Mock fetch function (แทนที่ด้วยการเรียก APIจริง) async def fetch_btc_rate(symbol): return CachedFundingRate( source='binance', symbol=symbol, rate=0.0001, mark_price=45000.0, index_price=44980.0, cached_at=time.time(), expires_at=time.time() + 60 ) # ครั้งแรก - ต้อง fetch result = await cache.get_or_fetch('binance', 'BTCUSDT', lambda s: fetch_btc_rate(s)) print(f"Result: {result.rate}") # ครั้งต่อไป - ดึงจาก cache result2 = await cache.get_or_fetch('binance', 'BTCUSDT', lambda s: fetch_btc_rate(s)) print(f"From cache: {result2.rate}") await cache.close()

การควบคุม Concurrency และ Connection Pooling

เมื่อต้องดึงข้อมูลจากหลาย Exchange พร้อมกัน การจัดการ Connection และ Concurrency ที่ดีจะช่วยลด Latency และป้องกันปัญหา Resource Exhaustion

import asyncio
from typing import List, Dict
import aiohttp
from contextlib import asynccontextmanager
import time

class ConnectionPool:
    """
    Connection Pool สำหรับจัดการ WebSocket connections
    รองรับ auto-reconnect และ health check
    """
    
    def __init__(self, max_connections: int = 10):
        self.max_connections = max_connections
        self.connections: Dict[str, aiohttp.ClientWebSocketResponse] = {}
        self.session: Optional[aiohttp.ClientSession] = None
        self._lock = asyncio.Lock()
        self.health_check_interval = 30  # วินาที
        
    async def __aenter__(self):
        # สร้าง session พร้อม connection limits
        connector = aiohttp.TCPConnector(
            limit=self.max_connections,
            limit_per_host=5,
            ttl_dns_cache=300,
            enable_cleanup_closed=True
        )
        
        timeout = aiohttp.ClientTimeout(
            total=30,
            connect=10,
            sock_read=10
        )
        
        self.session = aiohttp.ClientSession(
            connector=connector,
            timeout=timeout
        )
        
        # เริ่ม health check task
        self._health_task = asyncio.create_task(self._health_check_loop())
        
        return self
    
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        # ยกเลิก health check
        if hasattr(self, '_health_task'):
            self._health_task.cancel()
            
        # ปิด connections ทั้งหมด
        for name, ws in self.connections.items():
            try:
                await ws.close()
            except Exception as e:
                print(f"Error closing {name}: {e}")
                
        if self.session:
            await self.session.close()
            
    async def get_connection(self, name: str, url: str) -> aiohttp.ClientWebSocketResponse:
        """ดึงหรือสร้าง WebSocket connection"""
        async with self._lock:
            if name not in self.connections:
                ws = await self.session.ws_connect(url)
                self.connections[name] = ws
                return ws
            return self.connections[name]
            
    async def reconnect(self, name: str, url: str, max_retries: int = 3):
        """Reconnect เมื่อ connection หลุด"""
        for attempt in range(max_retries):
            try:
                async with self._lock:
                    if name in self.connections:
                        await self.connections[name].close()
                        
                ws = await self.session.ws_connect(url)
                async with self._lock:
                    self.connections[name] = ws
                print(f"✅ Reconnected {name}")
                return True
                
            except Exception as e:
                wait_time = 2 ** attempt
                print(f"❌ Reconnect attempt {attempt+1} failed: {e}")
                await asyncio.sleep(wait_time)
                
        return False
        
    async def _health_check_loop(self):
        """ตรวจสอบ connection health เป็นระยะ"""
        while True:
            await asyncio.sleep(self.health_check_interval)
            
            for name, ws in list(self.connections.items()):
                if ws.closed:
                    print(f"⚠️ {name} connection closed, scheduling reconnect")
                    # Reconnect will be handled by the consumer


class MultiExchangeRateAggregator:
    """
    Aggregator สำหรับดึงข้อมูลจากหลาย Exchange พร้อมกัน
    ใช้ asyncio.gather สำหรับ concurrent requests
    """
    
    def __init__(self):
        self.binance_fetcher = BinanceFundingRateFetcher()
        self.deribit_fetcher = DeribitFundingRateFetcher()
        self.cache = FundingRateCache()
        
    async def fetch_all_rates(self, symbols: List[str]) -> Dict[str, dict]:
        """
        ดึงข้อมูล funding rates จากทุก Exchange พร้อมกัน
        ใช้ asyncio.gather เพื่อ parallel execution
        """
        tasks = []
        
        # Binance tasks
        for symbol in symbols:
            tasks.append(self._fetch_binance_rate(symbol))
            
        # Deribit tasks
        deribit_symbols = self._convert_to_deribit_format(symbols)
        for symbol in deribit_symbols:
            tasks.append(self._fetch_deribit_rate(symbol))
            
        # Execute all tasks concurrently
        results = await asyncio.gather(*tasks, return_exceptions=True)
        
        # Organize results
        aggregated = {}
        for i, result in enumerate(results):
            if isinstance(result, Exception):
                print(f"Error fetching: {result}")
                continue
                
            if result:
                key = f"{result['source']}:{result['symbol']}"
                aggregated[key] = result
                
        return aggregated
    
    async def _fetch_binance_rate(self, symbol: str) -> Optional[dict]:
        """ดึงข้อมูลจาก Binance"""
        # ดึงจาก cache ก่อน
        cached = await self.cache.get('binance', symbol, 'funding_rate')
        if cached and not cached.is_expired():
            return {
                'source': 'binance',
                'symbol': symbol,
                'rate': cached.rate,
                'mark_price': cached.mark_price,
                'cached': True
            }
            
        # Fetch ใหม่ (ใช้ REST API เพื่อความเสถียร)
        # ตัวอย่าง: https://fapi.binance.com/fapi/v1/premiumIndex
        try:
            # ... fetch logic ...
            return {
                'source': 'binance',
                'symbol': symbol,
                'rate': 0.0001,
                'mark_price': 45000.0,
                'cached': False
            }
        except Exception as e:
            print(f"Binance fetch error: {e}")
            return None
            
    async def _fetch_deribit_rate(self, symbol: str) -> Optional[dict]:
        """ดึงข้อมูลจาก Deribit"""
        try:
            result = await self.deribit_fetcher.get_funding_rate(symbol)
            if result:
                return {
                    'source': 'deribit',
                    'symbol': symbol,
                    'rate': result.funding_rate,
                    'mark_price': result.mark_price,
                    'cached': False
                }
        except Exception as e:
            print(f"Deribit fetch error: {e}")
        return None
        
    def _convert_to_deribit_format(self, symbols: List[str]) -> List[str]:
        """แปลง symbol format จาก Binance เป็น Deribit"""
        conversion = {
            'BTCUSDT': 'BTC-PERPETUAL',
            'ETHUSDT': 'ETH-PERPETUAL',
            'SOLUSDT': 'SOL-PERPETUAL'
        }
        return [conversion.get(s, s) for s in symbols]

การใช้งาน

async def main(): async with ConnectionPool(max_connections=10) as pool: aggregator = MultiExchangeRateAggregator() symbols = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT'] all_rates = await aggregator.fetch_all_rates(symbols) for key, data in all_rates.items(): print(f"{key}: {data['rate']*100:.4f}%")

asyncio.run(main())

การเพิ่มประสิทธิภาพต้นทุน: ใช้ HolySheep AI สำหรับ Analysis

หลังจากดึงข้อมูล Funding Rate มาแล้ว ขั้นตอนสำคัญคือการวิเคราะห์และหา Pattern ซึ่งผมใช้ HolySheep AI เป็น LLM Backend สำหรับทำ Market Analysis เนื่องจากมีข้อดีหลายอย่าง:

"""
ตัวอย่างการใช้ HolySheep AI สำหรับวิเคราะห์ Funding Rate Data
"""
import asyncio
import aiohttp
import json
from typing import List, Dict
from dataclasses import dataclass

Base URL และ API Key สำหรับ HolyShe