ในโลกของสัญญาซื้อขายแลกเปลี่ยนถาวร (Perpetual Futures) การเข้าถึงข้อมูล tick-by-tick ที่แม่นยำเป็นรากฐานของกลยุทธ์ market making และ arbitrage บทความนี้จะพาคุณสำรวจเชิงลึกเกี่ยวกับ Tardis Data API วิธีดึงข้อมูลจาก OKX และ Bybit พร้อมเทคนิคการจัดการ funding rates และ order book depth snapshots ที่ใช้งานจริงใน production
Tardis Data API คืออะไร และทำไมถึงสำคัญ
Tardis เป็น data aggregator ที่รวบรวม raw market data จาก exchanges หลายตัวผ่าน normalized API เดียว ข้อดีคือไม่ต้องจัดการ connection หลายตัว และได้ข้อมูลที่ cleaned แล้ว สำหรับ OKX และ Bybit ซึ่งเป็น top-tier derivatives exchanges นั้น Tardis ให้ความแม่นยำของ timestamp ที่ <1ms และรองรับทั้ง trade ticks, funding rate updates, และ depth snapshots
การตั้งค่า Environment และ Dependencies
# requirements.txt
tardis-client==2.1.0
websockets==12.0
pandas==2.0.3
numpy==1.24.3
pyarrow==14.0.1
aiokafka==0.10.0
redis==5.0.1
สำหรับ HolySheep AI ในการประมวลผลข้อมูลเพิ่มเติม
httpx==0.27.0
orjson==3.9.12
# config.py
import os
from dataclasses import dataclass
from typing import List
@dataclass
class ExchangeConfig:
exchange: str
symbols: List[str]
channels: List[str]
api_key: str
api_secret: str
@dataclass
class TardisConfig:
api_key: str
base_url: str = "wss://tardis.dev:9000"
reconnect_delay: float = 5.0
max_reconnect_attempts: int = 10
heartbeat_interval: int = 30
@dataclass
class HolySheepConfig:
api_key: str = "YOUR_HOLYSHEEP_API_KEY"
base_url: str = "https://api.holysheep.ai/v1"
timeout: int = 30
max_retries: int = 3
OKX Perpetual Config
OKX_CONFIG = ExchangeConfig(
exchange="okx",
symbols=["BTC-USDT-SWAP", "ETH-USDT-SWAP"],
channels=["trades", "funding_rate", "booksL2"],
api_key=os.getenv("OKX_API_KEY", ""),
api_secret=os.getenv("OKX_API_SECRET", "")
)
Bybit Perpetual Config
BYBIT_CONFIG = ExchangeConfig(
exchange="bybit",
symbols=["BTCUSDT", "ETHUSDT"],
channels=["trades", "funding", "orderbook"],
api_key=os.getenv("BYBIT_API_KEY", ""),
api_secret=os.getenv("BYBIT_API_SECRET", "")
)
HolySheep AI Config สำหรับ AI-powered analysis
HOLYSHEEP_CONFIG = HolySheepConfig(
api_key="YOUR_HOLYSHEEP_API_KEY", # รับได้จาก https://www.holysheep.ai/register
base_url="https://api.holysheep.ai/v1",
timeout=30,
max_retries=3
)
Client หลักสำหรับเชื่อมต่อ Tardis Data API
# tardis_client.py
import asyncio
import json
import logging
from datetime import datetime
from typing import Dict, Callable, Optional, List
from dataclasses import dataclass, field
import pandas as pd
import aiohttp
import websockets
from websockets.exceptions import ConnectionClosed
from config import TardisConfig, OKX_CONFIG, BYBIT_CONFIG
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@dataclass
class TickData:
exchange: str
symbol: str
timestamp: datetime
price: float
size: float
side: str
trade_id: str
@dataclass
class FundingRateData:
exchange: str
symbol: str
timestamp: datetime
funding_rate: float
next_funding_time: datetime
@dataclass
class DepthSnapshot:
exchange: str
symbol: str
timestamp: datetime
bids: List[tuple] # [(price, size), ...]
asks: List[tuple] # [(price, size), ...]
class TardisDataClient:
def __init__(self, config: TardisConfig):
self.config = config
self.ws: Optional[websockets.WebSocketClientProtocol] = None
self.subscriptions: Dict[str, set] = {}
self.handlers: Dict[str, Callable] = {}
self._running = False
self._reconnect_count = 0
self._last_ping = None
async def connect(self):
"""เชื่อมต่อ WebSocket กับ Tardis API"""
auth_url = f"{self.config.base_url}?auth={self.config.api_key}"
try:
self.ws = await websockets.connect(auth_url)
self._running = True
self._reconnect_count = 0
logger.info(f"เชื่อมต่อ Tardis สำเร็จ: {self.config.base_url}")
except Exception as e:
logger.error(f"ไม่สามารถเชื่อมต่อ: {e}")
raise
async def subscribe(self, exchange: str, symbols: List[str], channels: List[str]):
"""สมัครรับข้อมูลจาก exchange เฉพาะ"""
for symbol in symbols:
for channel in channels:
sub_id = f"{exchange}:{symbol}:{channel}"
subscribe_msg = {
"type": "subscribe",
"exchange": exchange,
"channel": channel,
"symbol": symbol
}
await self.ws.send(json.dumps(subscribe_msg))
logger.info(f"สมัครรับ: {sub_id}")
if exchange not in self.subscriptions:
self.subscriptions[exchange] = set()
self.subscriptions[exchange].add(sub_id)
async def receive_loop(self):
"""Loop หลักสำหรับรับข้อมูล tick-by-tick"""
buffer = []
buffer_size = 1000
try:
async for message in self.ws:
try:
data = json.loads(message)
parsed = self._parse_message(data)
if parsed:
buffer.append(parsed)
# Batch processing เพื่อลด CPU overhead
if len(buffer) >= buffer_size:
await self._process_buffer(buffer)
buffer = []
except json.JSONDecodeError as e:
logger.warning(f"JSON decode error: {e}")
continue
except ConnectionClosed as e:
logger.error(f"Connection closed: {e}")
await self._handle_reconnect()
def _parse_message(self, data: dict) -> Optional[dict]:
"""Parse ข้อมูลตามประเภท message"""
msg_type = data.get("type", "")
if msg_type == "trade":
return TickData(
exchange=data["exchange"],
symbol=data["symbol"],
timestamp=datetime.fromisoformat(data["timestamp"].replace("Z", "+00:00")),
price=float(data["price"]),
size=float(data["size"]),
side=data["side"],
trade_id=data["id"]
)
elif msg_type in ["funding", "funding_rate"]:
return FundingRateData(
exchange=data["exchange"],
symbol=data["symbol"],
timestamp=datetime.fromisoformat(data["timestamp"].replace("Z", "+00:00")),
funding_rate=float(data["fundingRate"]),
next_funding_time=datetime.fromisoformat(data["nextFundingTime"].replace("Z", "+00:00"))
)
elif msg_type in ["book", "bookSnapshot", "orderbook"]:
return DepthSnapshot(
exchange=data["exchange"],
symbol=data["symbol"],
timestamp=datetime.fromisoformat(data["timestamp"].replace("Z", "+00:00")),
bids=[(float(p), float(s)) for p, s in data.get("bids", [])],
asks=[(float(p), float(s)) for p, s in data.get("asks", [])]
)
return None
async def _process_buffer(self, buffer: List):
"""Process batch ของ ticks"""
# แบ่งตามประเภท
trades = [t for t in buffer if isinstance(t, TickData)]
fundings = [f for f in buffer if isinstance(f, FundingRateData)]
depths = [d for d in buffer if isinstance(d, DepthSnapshot)]
if trades:
await self._process_trades(trades)
if fundings:
await self._process_fundings(fundings)
if depths:
await self._process_depths(depths)
async def _process_trades(self, trades: List[TickData]):
logger.debug(f"จำนวน trades: {len(trades)}")
# Implement trade processing logic
pass
async def _process_fundings(self, fundings: List[FundingRateData]):
logger.debug(f"จำนวน funding updates: {len(fundings)}")
# Implement funding processing logic
pass
async def _process_depths(self, depths: List[DepthSnapshot]):
logger.debug(f"จำนวน depth snapshots: {len(depths)}")
# Implement depth processing logic
pass
async def _handle_reconnect(self):
"""จัดการการ reconnect เมื่อ connection หลุด"""
if self._reconnect_count >= self.config.max_reconnect_attempts:
logger.error("เกินจำนวนครั้งที่กำหนดสำหรับ reconnect")
return
self._reconnect_count += 1
delay = self.config.reconnect_delay * (2 ** (self._reconnect_count - 1))
logger.info(f"พยายาม reconnect ใน {delay} วินาที (ครั้งที่ {self._reconnect_count})")
await asyncio.sleep(delay)
await self.connect()
# Resubscribe to all previous subscriptions
for exchange, subs in self.subscriptions.items():
for sub in subs:
parts = sub.split(":")
if len(parts) == 3:
await self.subscribe(parts[0], [parts[1]], [parts[2]])
การจัดการ Funding Rate อย่างมีประสิทธิภาพ
Funding rate เป็นกลไกสำคัญที่ทำให้ราคา perpetual futures อยู่ใกล้ spot price มากที่สุด ใน OKX และ Bybit funding rate จะถูกคำนวณทุก 8 ชั่วโมง แต่การอัปเดตจะเกิดขึ้นบ่อยกว่านั้นมาก การติดตาม funding rate changes อย่างใกล้ชิดช่วยให้เข้าใจ sentiment ของตลาดและหาจังหวะ arbitrage ที่ดี
# funding_rate_tracker.py
import asyncio
from datetime import datetime, timedelta
from typing import Dict, List, Optional
from collections import defaultdict
import pandas as pd
from dataclasses import dataclass, asdict
from tardis_client import FundingRateData, TardisDataClient, TardisConfig
@dataclass
class FundingRateSnapshot:
exchange: str
symbol: str
current_rate: float
predicted_rate: float
timestamp: datetime
hours_to_funding: float
annualised_rate: float
premium_index: float
interest_rate: float
class FundingRateTracker:
"""Track และวิเคราะห์ funding rate ข้าม exchanges"""
def __init__(self, storage_enabled: bool = True):
self.history: Dict[str, List[FundingRateData]] = defaultdict(list)
self.snapshots: Dict[str, List[FundingRateSnapshot]] = defaultdict(list)
self.rate_cache: Dict[str, float] = {}
self._window_size = 72 # 3 วันที่ผ่านมา
self._storage_enabled = storage_enabled
self._save_interval = 300 # 5 นาที
def process_funding_update(self, funding: FundingRateData):
"""Process funding rate update และคำนวณ derived metrics"""
key = f"{funding.exchange}:{funding.symbol}"
# เก็บ history
self.history[key].append(funding)
# รักษา window size
if len(self.history[key]) > self._window_size:
self.history[key] = self.history[key][-self._window_size:]
# คำนวณ snapshot
snapshot = self._calculate_snapshot(funding)
self.snapshots[key].append(snapshot)
# Cache ค่าปัจจุบัน
self.rate_cache[key] = funding.funding_rate
# ตรวจจับ funding rate spike
if self._detect_spike(key):
self._alert_funding_spike(key, snapshot)
def _calculate_snapshot(self, funding: FundingRateData) -> FundingRateSnapshot:
"""คำนวณ metrics ที่ derived จาก funding rate"""
hours_to_funding = (funding.next_funding_time - datetime.now(funding.timestamp.tzinfo)).total_seconds() / 3600
# Annualized rate (compounded 3 times daily)
annualised = ((1 + funding.funding_rate) ** (3 * 365)) - 1 if funding.funding_rate else 0
# Predicted rate จาก moving average
key = f"{funding.exchange}:{funding.symbol}"
predicted = self._predict_rate(key)
return FundingRateSnapshot(
exchange=funding.exchange,
symbol=funding.symbol,
current_rate=funding.funding_rate,
predicted_rate=predicted,
timestamp=funding.timestamp,
hours_to_funding=hours_to_funding,
annualised_rate=annualised,
premium_index=funding.funding_rate * 3, # Approximate
interest_rate=0.0001 # Bybit/OKX standard
)
def _predict_rate(self, key: str) -> float:
"""Predict funding rate จาก historical data ใช้ EMA"""
if len(self.history[key]) < 3:
return self.rate_cache.get(key, 0.0)
rates = [h.funding_rate for h in self.history[key][-24:]] # 24 ชั่วโมง
if not rates:
return 0.0
# EMA with alpha = 0.3
alpha = 0.3
ema = rates[0]
for rate in rates[1:]:
ema = alpha * rate + (1 - alpha) * ema
return ema
def _detect_spike(self, key: str) -> bool:
"""ตรวจจับ funding rate spike (เปลี่ยนแปลงเกิน 50% จาก average)"""
if len(self.history[key]) < 6:
return False
recent = self.history[key][-6:]
avg = sum(h.funding_rate for h in recent) / len(recent)
current = self.history[key][-1].funding_rate
if abs(avg) < 0.0001: # Near zero
return abs(current) > 0.001
change = abs((current - avg) / avg)
return change > 0.5
def _alert_funding_spike(self, key: str, snapshot: FundingRateSnapshot):
"""ส่ง alert เมื่อตรวจพบ funding spike"""
print(f"[ALERT] Funding Rate Spike Detected!")
print(f" Exchange: {snapshot.exchange}")
print(f" Symbol: {snapshot.symbol}")
print(f" Current: {snapshot.current_rate:.6f}")
print(f" Predicted: {snapshot.predicted_rate:.6f}")
print(f" Annualized: {snapshot.annualised_rate:.2%}")
def get_arbitrage_opportunity(self) -> List[Dict]:
"""หา arbitrage opportunity ระหว่าง OKX และ Bybit"""
opportunities = []
for symbol in ["BTC-USDT-SWAP", "ETH-USDT-SWAP"]:
okx_key = f"okx:{symbol}"
bybit_key = f"bybit:{symbol.replace('-USDT-SWAP', 'USDT')}"
okx_rate = self.rate_cache.get(okx_key)
bybit_rate = self.rate_cache.get(bybit_key)
if okx_rate is not None and bybit_rate is not None:
diff = abs(okx_rate - bybit_rate)
if diff > 0.0001: # มากกว่า 0.01%
opportunities.append({
"symbol": symbol,
"okx_rate": okx_rate,
"bybit_rate": bybit_rate,
"diff": diff,
"direction": "long_okx_short_bybit" if okx_rate > bybit_rate else "long_bybit_short_okx",
"annualised_spread": diff * 3 * 365
})
return opportunities
def calculate_funding_impact(self, position_size: float, exchange: str, symbol: str) -> Dict:
"""คำนวณผลกระทบของ funding rate ต่อ position"""
key = f"{exchange}:{symbol}"
rate = self.rate_cache.get(key, 0.0)
# Funding per 8 hours
funding_per_period = position_size * rate
# Daily funding
daily_funding = funding_per_period * 3
# Monthly funding
monthly_funding = daily_funding * 30
return {
"position_size": position_size,
"rate": rate,
"per_period": funding_per_period,
"daily": daily_funding,
"monthly": monthly_funding,
"annualised": monthly_funding * 12
}
async def run_funding_tracker():
"""Run funding rate tracker เป็น standalone process"""
config = TardisConfig(api_key=os.getenv("TARDIS_API_KEY"))
client = TardisDataClient(config)
tracker = FundingRateTracker()
await client.connect()
# Subscribe to funding channels
await client.subscribe("okx", ["BTC-USDT-SWAP", "ETH-USDT-SWAP"], ["funding_rate"])
await client.subscribe("bybit", ["BTCUSDT", "ETHUSDT"], ["funding"])
# Handler for funding updates
async def handle_funding(funding: FundingRateData):
tracker.process_funding_update(funding)
# Check for arbitrage
opp = tracker.get_arbitrage_opportunity()
if opp:
print(f"[ARBITRAGE] พบโอกาส: {opp}")
# Run with periodic reporting
while True:
await asyncio.sleep(60) # Report every minute
print(f"[STATUS] Tracking {len(tracker.rate_cache)} symbols")
print(f"[CACHE] Current rates: {tracker.rate_cache}")
import os
การจัดการ Order Book Depth Snapshot
Depth snapshot จาก order book เป็นข้อมูลสำคัญสำหรับการคำนวณ slippage, market impact, และ liquidity analysis OKX และ Bybit มีรูปแบบการส่ง depth data ที่แตกต่างกัน ต้อง handle อย่างถูกต้องเพื่อให้ได้ข้อมูลที่ consistent
# depth_analyzer.py
import asyncio
from datetime import datetime, timedelta
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass, field
from collections import deque
import numpy as np
from tardis_client import DepthSnapshot, TardisDataClient, TardisConfig
@dataclass
class OrderBookLevel:
price: float
size: float
orders: int = 1
@dataclass
class ProcessedDepth:
exchange: str
symbol: str
timestamp: datetime
best_bid: float
best_ask: float
spread: float
spread_pct: float
mid_price: float
imbalance: float # Bid/Ask size ratio
depth_5: float # Combined size in top 5 levels
depth_20: float # Combined size in top 20 levels
weighted_mid: float
def to_dict(self) -> dict:
return {
"exchange": self.exchange,
"symbol": self.symbol,
"timestamp": self.timestamp.isoformat(),
"best_bid": self.best_bid,
"best_ask": self.best_ask,
"spread": self.spread,
"spread_pct": self.spread_pct,
"mid_price": self.mid_price,
"imbalance": self.imbalance,
"depth_5": self.depth_5,
"depth_20": self.depth_20,
"weighted_mid": self.weighted_mid
}
class DepthAnalyzer:
"""วิเคราะห์ order book depth สำหรับ OKX และ Bybit"""
# ค่าคงที่สำหรับแต่ละ exchange
OKX_LEVEL_PRECISION = {
"BTC-USDT-SWAP": 0.1,
"ETH-USDT-SWAP": 0.01
}
BYBIT_LEVEL_PRECISION = {
"BTCUSDT": 0.01,
"ETHUSDT": 0.01
}
def __init__(self, symbol: str):
self.symbol = symbol
self.bids: List[OrderBookLevel] = []
self.asks: List[OrderBookLevel] = []
self.last_update: Optional[datetime] = None
self.sequence: int = 0
# Rolling history สำหรับ analysis
self.history: deque = deque(maxlen=1000)
self.imbalance_history: deque = deque(maxlen=100)
def update_depth(self, snapshot: DepthSnapshot):
"""Update order book จาก depth snapshot"""
self.bids = [OrderBookLevel(price=p, size=s) for p, s in snapshot.bids]
self.asks = [OrderBookLevel(price=p, size=s) for p, s in snapshot.asks]
self.last_update = snapshot.timestamp
self.sequence += 1
# Calculate processed depth
processed = self._process_depth(snapshot)
self.history.append(processed)
# Track imbalance
self.imbalance_history.append(processed.imbalance)
def _process_depth(self, snapshot: DepthSnapshot) -> ProcessedDepth:
"""Process raw depth เป็น derived metrics"""
bids = [(p, s) for p, s in snapshot.bids[:20]]
asks = [(p, s) for p, s in snapshot.asks[:20]]
if not bids or not asks:
return None
best_bid = bids[0][0]
best_ask = asks[0][0]
spread = best_ask - best_bid
spread_pct = spread / ((best_ask + best_bid) / 2) if best_bid and best_ask else 0
mid_price = (best_bid + best_ask) / 2
# Calculate imbalance (bid/ask ratio)
bid_size = sum(s for _, s in bids[:5])
ask_size = sum(s for _, s in asks[:5])
imbalance = (bid_size - ask_size) / (bid_size + ask_size) if (bid_size + ask_size) > 0 else 0
# Depth calculations
depth_5 = sum(s for _, s in bids[:5]) + sum(s for _, s in asks[:5])
depth_20 = sum(s for _, s in bids) + sum(s for _, s in asks)
# Volume-weighted mid price
total_bid_vol = sum(s for _, s in bids[:5])
total_ask_vol = sum(s for _, s in asks[:5])
bid_prices = [p for p, _ in bids[:5]]
ask_prices = [p for p, _ in asks[:5]]
if total_bid_vol + total_ask_vol > 0:
weighted_mid = (
sum(p * s for p, s in bids[:5]) + sum(p * s for p, s in asks[:5])
) / (total_bid_vol + total_ask_vol)
else:
weighted_mid = mid_price
return ProcessedDepth(
exchange=snapshot.exchange,
symbol=snapshot.symbol,
timestamp=snapshot.timestamp,
best_bid=best_bid,
best_ask=best_ask,
spread=spread,
spread_pct=spread_pct,
mid_price=mid_price,
imbalance=imbalance,
depth_5=depth_5,
depth_20=depth_20,
weighted_mid=weighted_mid
)
def calculate_slippage(self, side: str, size: float) -> Tuple[float, float]:
"""คำนวณ slippage สำหรับ order ขนาด size"""
if side == "buy":
levels = self.asks
else:
levels = self.bids
remaining_size = size
total_cost = 0
filled_size = 0
for level in levels:
fill_size = min(remaining_size, level.size)
total_cost += fill_size * level.price
filled_size += fill_size
remaining_size -= fill_size
if remaining_size <= 0:
break
if filled_size == 0:
return 0, float('inf')
avg_price = total_cost / filled_size
slippage = abs(avg_price - self.mid_price) if self.mid_price else 0
return slippage, slippage / self.mid_price if self.mid_price else 0