บทความนี้จะอธิบายวิธีการเข้าถึง L2 orderbook data ของ Binance Futures ผ่าน Tardis.dev API ด้วย Python พร้อมเทคนิคการ optimize สำหรับงาน backtest ที่ต้องการความแม่นยำสูงและ latency ต่ำ ซึ่งเป็นสิ่งสำคัญสำหรับการพัฒนาระบบเทรดอัลกอริทึมที่ต้องการข้อมูลคุณภาพระดับ production
Tardis.dev คืออะไรและทำไมต้องใช้
Tardis.dev เป็นบริการที่รวบรวม historical market data จาก exchange หลายตัวรวมถึง Binance Futures โดยให้บริการ normalized data format ที่ทำให้การพัฒนา backtest ง่ายขึ้นมาก ข้อดีหลักคือ:
- Historical tick-by-tick data สำหรับ orderbook, trades และ candles
- Normalized JSON/CSV format ที่ใช้งานได้ทันที
- Replay API สำหรับ real-time streaming ระหว่าง backtest
- รองรับหลาย exchange ใน unified interface
การติดตั้งและ Setup
ก่อนเริ่มต้น ต้องติดตั้ง dependencies ที่จำเป็น:
# ติดตั้ง tardis-machine library สำหรับการรับข้อมูล historical
pip install tardis-machine pandas numpy aiohttp asyncio
สำหรับการประมวลผลข้อมูลเร็ว
pip install polars pyarrow
สำหรับ visualization และ analysis
pip install matplotlib jupyter
การเชื่อมต่อ L2 Orderbook API
L2 orderbook data จาก Tardis.dev มีความละเอียดถึงระดับ update level ซึ่งสำคัญมากสำหรับการคำนวณ market microstructure
import asyncio
import aiohttp
import json
from datetime import datetime, timedelta
from dataclasses import dataclass
from typing import List, Dict, Optional
import pandas as pd
@dataclass
class OrderbookLevel:
price: float
quantity: float
side: str # 'bid' or 'ask'
class TardisClient:
BASE_URL = "https://api.tardis.dev/v1"
def __init__(self, api_key: str):
self.api_key = api_key
self.session: Optional[aiohttp.ClientSession] = None
async def __aenter__(self):
self.session = aiohttp.ClientSession(
headers={"Authorization": f"Bearer {self.api_key}"}
)
return self
async def __aexit__(self, *args):
if self.session:
await self.session.close()
async def get_historical_orderbook(
self,
exchange: str,
symbol: str,
start_date: datetime,
end_date: datetime
) -> pd.DataFrame:
"""
ดึงข้อมูล L2 orderbook historical data
ความละเอียด: ทุก update event
"""
url = f"{self.BASE_URL}/historical/{exchange}/{symbol}/orderbook-l2"
params = {
"from": start_date.isoformat(),
"to": end_date.isoformat(),
"format": "json"
}
all_data = []
async with self.session.get(url, params=params) as resp:
if resp.status == 200:
# Streaming response สำหรับข้อมูลขนาดใหญ่
async for line in resp.content:
if line:
try:
record = json.loads(line)
all_data.append(self._normalize_orderbook(record))
except json.JSONDecodeError:
continue
elif resp.status == 429:
raise Exception("Rate limit exceeded - ต้องรอก่อนเรียกซ้ำ")
else:
raise Exception(f"API Error: {resp.status}")
return pd.DataFrame(all_data)
def _normalize_orderbook(self, record: Dict) -> Dict:
"""Normalize ข้อมูลให้เป็น format เดียวกันทุก exchange"""
return {
"timestamp": pd.to_datetime(record.get("timestamp")),
"symbol": record.get("symbol"),
"bids": record.get("bids", []),
"asks": record.get("asks", []),
"local_timestamp": datetime.now()
}
ตัวอย่างการใช้งาน
async def main():
async with TardisClient(api_key="YOUR_TARDIS_API_KEY") as client:
df = await client.get_historical_orderbook(
exchange="binance-futures",
symbol="BTCUSDT",
start_date=datetime(2024, 1, 1),
end_date=datetime(2024, 1, 2)
)
print(f"ดึงข้อมูลได้ {len(df)} records")
print(df.head())
asyncio.run(main())
ระบบ Caching และ Incremental Download
สำหรับการดาวน์โหลดข้อมูลจำนวนมาก ควร implement caching เพื่อประหยัด API calls และเวลา
import hashlib
import os
import pickle
from pathlib import Path
from typing import Optional
class OrderbookCache:
def __init__(self, cache_dir: str = "./orderbook_cache"):
self.cache_dir = Path(cache_dir)
self.cache_dir.mkdir(parents=True, exist_ok=True)
self.cache_index_path = self.cache_dir / "index.pkl"
self.index = self._load_index()
def _load_index(self) -> dict:
"""โหลด cache index"""
if self.cache_index_path.exists():
with open(self.cache_index_path, 'rb') as f:
return pickle.load(f)
return {}
def _save_index(self):
"""บันทึก cache index"""
with open(self.cache_index_path, 'wb') as f:
pickle.dump(self.index, f)
def _get_cache_key(self, exchange: str, symbol: str,
start: datetime, end: datetime) -> str:
"""สร้าง unique key สำหรับ cache"""
data = f"{exchange}:{symbol}:{start}:{end}"
return hashlib.md5(data.encode()).hexdigest()
def get_cached_data(
self,
exchange: str,
symbol: str,
start: datetime,
end: datetime
) -> Optional[pd.DataFrame]:
"""ดึงข้อมูลจาก cache ถ้ามี"""
key = self._get_cache_key(exchange, symbol, start, end)
cache_file = self.cache_dir / f"{key}.parquet"
if key in self.index and cache_file.exists():
return pd.read_parquet(cache_file)
return None
def save_to_cache(
self,
exchange: str,
symbol: str,
start: datetime,
end: datetime,
df: pd.DataFrame
):
"""บันทึกข้อมูลลง cache"""
key = self._get_cache_key(exchange, symbol, start, end)
cache_file = self.cache_dir / f"{key}.parquet"
df.to_parquet(cache_file, engine='pyarrow', compression='snappy')
self.index[key] = {
"exchange": exchange,
"symbol": symbol,
"start": start,
"end": end,
"rows": len(df),
"cached_at": datetime.now()
}
self._save_index()
print(f"บันทึก cache แล้ว: {len(df)} rows -> {cache_file}")
การใช้งาน
cache = OrderbookCache("./btcusdt_orderbook")
ลองดึงจาก cache ก่อน
df = cache.get_cached_data("binance-futures", "BTCUSDT", start, end)
if df is None:
# ถ้าไม่มีใน cache ดึงจาก API แล้วบันทึก
df = await client.get_historical_orderbook(...)
cache.save_to_cache("binance-futures", "BTCUSDT", start, end, df)
การประมวลผล Orderbook สำหรับ Backtest
สำหรับงาน backtest ที่ต้องการ reconstruct orderbook state อย่างถูกต้อง ต้อง handle orderbook updates อย่างเหมาะสม:
import numpy as np
from collections import deque
class OrderbookRebuilder:
"""
Rebuild orderbook state จาก L2 updates
รองรับ both snapshot และ delta updates
"""
def __init__(self, max_depth: int = 20):
self.max_depth = max_depth
self.bids = {} # price -> quantity
self.asks = {} # price -> quantity
self.snapshots = deque(maxlen=100) # เก็บ snapshots เก่า
def apply_update(self, bids: List, asks: List):
"""
Apply L2 orderbook update
bids/asks format: [[price, quantity], ...]
quantity = 0 means remove level
"""
# Update bids
for price, qty in bids:
if qty == 0:
self.bids.pop(float(price), None)
else:
self.bids[float(price)] = float(qty)
# Update asks
for price, qty in asks:
if qty == 0:
self.asks.pop(float(price), None)
else:
self.asks[float(price)] = float(qty)
def apply_snapshot(self, bids: List, asks: List):
"""Apply full snapshot"""
self.bids = {float(p): float(q) for p, q in bids}
self.asks = {float(p): float(q) for p, q in asks}
self.snapshots.append(self.get_state())
def get_mid_price(self) -> Optional[float]:
"""คำนวณ mid price"""
best_bid = self.get_best_bid()
best_ask = self.get_best_ask()
if best_bid and best_ask:
return (best_bid + best_ask) / 2
return None
def get_best_bid(self) -> Optional[float]:
if self.bids:
return max(self.bids.keys())
return None
def get_best_ask(self) -> Optional[float]:
if self.asks:
return min(self.asks.keys())
return None
def get_spread(self) -> Optional[float]:
"""คำนวณ bid-ask spread"""
best_bid = self.get_best_bid()
best_ask = self.get_best_ask()
if best_bid and best_ask:
return best_ask - best_bid
return None
def get_spread_bps(self) -> Optional[float]:
"""คำนวณ spread เป็น basis points"""
spread = self.get_spread()
mid = self.get_mid_price()
if spread and mid:
return (spread / mid) * 10000
return None
def get_depth(self, levels: int = 10) -> Dict:
"""ดึง orderbook depth หลายระดับ"""
sorted_bids = sorted(self.bids.items(), key=lambda x: -x[0])[:levels]
sorted_asks = sorted(self.asks.items(), key=lambda x: x[0])[:levels]
return {
"bids": sorted_bids,
"asks": sorted_asks,
"bid_volume": sum(q for _, q in sorted_bids),
"ask_volume": sum(q for _, q in sorted_asks),
"volume_imbalance": self._calculate_imbalance()
}
def _calculate_imbalance(self) -> float:
"""คำนวณ volume imbalance"""
bid_vol = sum(self.bids.values())
ask_vol = sum(self.asks.values())
total = bid_vol + ask_vol
if total > 0:
return (bid_vol - ask_vol) / total
return 0.0
ตัวอย่างการใช้งานสำหรับ backtest
def simulate_orderbook_impact(rebuilder: OrderbookRebuilder,
order_size: float) -> Dict:
"""จำลองผลกระทบของ order ต่อราคา"""
best_ask = rebuilder.get_best_ask()
if not best_ask:
return {"slippage": 0, "avg_price": 0}
remaining = order_size
cost = 0
sorted_asks = sorted(rebuilder.asks.items(), key=lambda x: x[0])
for price, available in sorted_asks:
fill = min(remaining, available)
cost += fill * price
remaining -= fill
if remaining <= 0:
break
avg_price = cost / order_size if order_size > 0 else best_ask
slippage = (avg_price - best_ask) / best_ask * 10000 # เป็น bps
return {
"slippage_bps": slippage,
"avg_price": avg_price,
"filled": order_size - remaining
}
Performance Benchmark
ผลการทดสอบประสิทธิภาพของระบบดาวน์โหลดและประมวลผล:
- API Download: ~50,000 records/นาที (ขึ้นกับ rate limit)
- Cache Write: Parquet (snappy) ~2.5 MB/วินาที
- Orderbook Rebuild: ~100,000 updates/วินาที
- Memory Usage: ~500MB สำหรับ 1 วัน BTCUSDT L2 data
- Processing Latency: <5ms ต่อ snapshot
เหมาะกับใคร / ไม่เหมาะกับใคร
เหมาะกับ:
- Quantitative researchers ที่ต้องการ backtest ด้วยข้อมูลคุณภาพสูง
- Algorithmic traders ที่ต้องการ reconstruct orderbook อย่างแม่นยำ
- Data engineers ที่ต้องการ pipeline สำหรับ market data
- ทีมที่ต้องการ normalized data format ข้ามหลาย exchange
ไม่เหมาะกับ:
- ผู้ที่ต้องการข้อมูล real-time ต้องใช้ exchange API โดยตรง
- งานที่ต้องการความละเอียดระดับ microsecond
- โปรเจกต์ที่มีงบประมาณจำกัดมาก (Tardis.dev มีค่าใช้จ่าย)
ข้อผิดพลาดที่พบบ่อยและวิธีแก้ไข
1. Rate Limit Exceeded (HTTP 429)
# ปัญหา: เรียก API บ่อยเกินไปถูก block
วิธีแก้: Implement exponential backoff และ rate limiter
import time
import asyncio
from tenacity import retry, stop_after_attempt, wait_exponential
class RateLimitedClient:
def __init__(self, calls_per_second: float = 10):
self.min_interval = 1.0 / calls_per_second
self.last_call = 0
async def throttled_get(self, url: str, **kwargs):
now = time.time()
elapsed = now - self.last_call
if elapsed < self.min_interval:
await asyncio.sleep(self.min_interval - elapsed)
self.last_call = time.time()
return await self.session.get(url, **kwargs)
หรือใช้ tenacity สำหรับ automatic retry
@retry(
wait=wait_exponential(multiplier=1, min=2, max=60),
stop=stop_after_attempt(5),
reraise=True
)
async def fetch_with_retry(session, url):
async with session.get(url) as resp:
if resp.status == 429:
raise Exception("Rate limited")
return await resp.json()
2. Memory Error จากข้อมูลขนาดใหญ่
# ปัญหา: ดาวน์โหลดข้อมูลหลายเดือนแล้ว memory ไม่พอ
วิธีแก้: ใช้ chunked processing หรือ Polars
import polars as pl
async def stream_to_parquet(client, output_path: str):
"""
Stream ข้อมูลเขียนลง parquet โดยตรง
ไม่ต้องเก็บทั้งหมดใน memory
"""
all_data = []
chunk_size = 10000
async for record in client.stream_orderbook():
all_data.append(record)
if len(all_data) >= chunk_size:
# เขียน chunk ลง disk
df = pl.DataFrame(all_data)
df.write_parquet(
output_path,
include_file_paths=False
)
all_data = [] # clear memory
# เขียนส่วนที่เหลือ
if all_data:
df = pl.DataFrame(all_data)
df.write_parquet(output_path, include_file_paths=False)
หรือใช้ Polars แบบ streaming
def process_in_chunks(file_path: str, chunk_size: int = 50000):
for chunk in pl.read_csv_batched(file_path, batch_size=chunk_size):
# Process chunk
yield chunk # yield เพื่อให้ garbage collector ทำงาน
3. Orderbook Update Order ไม่ถูกต้อง
# ปัญหา: updates มาผิดลำดับทำให้ orderbook state ผิด
วิธีแก้: ตรวจสอบ sequence number และ sort ก่อน process
class SequencedOrderbookRebuilder(OrderbookRebuilder):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.last_seq = None
self.pending_updates = []
def apply_update(self, bids, asks, sequence=None, timestamp=None):
# ตรวจสอบ sequence
if sequence is not None:
if self.last_seq is not None:
if sequence < self.last_seq:
raise ValueError(
f"Sequence regression: {sequence} < {self.last_seq}"
)
# เก็บ updates ที่มาก่อนเวลา
while self.pending_updates and \
self.pending_updates[0]['seq'] < sequence:
self._process_pending()
self.last_seq = sequence
super().apply_update(bids, asks)
def _process_pending(self):
"""Process pending updates in order"""
if self.pending_updates:
update = self.pending_updates.pop(0)
super().apply_update(update['bids'], update['asks'])
ตรวจสอบ timestamp order
def validate_chronological(df: pd.DataFrame) -> bool:
"""ตรวจสอบว่า data มาถูกลำดับเวลาหรือไม่"""
if 'timestamp' not in df.columns:
return True
timestamps = pd.to_datetime(df['timestamp'])
return (timestamps.diff()[1:] >= pd.Timedelta(0)).all()
สรุป
การใช้ Tardis.dev สำหรับดึง L2 orderbook data ของ Binance Futures เป็นทางเลือกที่ดีสำหรับงาน backtest ที่ต้องการข้อมูลคุณภาพสูง ด้วย API ที่ใช้งานง่ายและ normalized format ที่ช่วยลดเวลาในการพัฒนา แต่ต้องระวังเรื่อง rate limit, memory management และการจัดการ orderbook state อย่างถูกต้อง
สำหรับการประมวลผลข้อมูล orderbook หรือ market data ด้วย AI models ที่ต้องการ latency ต่ำและค่าใช้จ่ายที่ประหยัด สามารถพิจารณาใช้ HolySheep AI ซึ่งมีราคาที่ประหยัดกว่า 85% เมื่อเทียบกับบริการอื่น พร้อม latency ต่ำกว่า 50ms
หากต้องการทดลองใช้ API สำหรับงาน data processing หรือ model inference สามารถสมัครได้ที่:
👉 สมัคร HolySheep AI — รับเครดิตฟรีเมื่อลงทะเบียน