Giới Thiệu Tổng Quan
Khi xây dựng các hệ thống giao dịch algorithm (algo trading), backtesting, hoặc phân tích thị trường, dữ liệu order book là yếu tố quyết định độ chính xác của chiến lược. Bài viết này sẽ hướng dẫn chi tiết cách sử dụng Tardis.dev Python API để tải dữ liệu order book theo tick-by-tick từ Binance Futures, bao gồm cách parse dữ liệu, lưu trữ hiệu quả, và mô phỏng撮合 (matching engine) để tái tạo trạng thái thị trường.
Bảng So Sánh: HolySheep vs API Chính Thức vs Các Dịch Vụ Relay
Trước khi đi vào chi tiết kỹ thuật, hãy cùng xem bảng so sánh toàn diện giữa các phương án tiếp cận dữ liệu thị trường hiện nay:
| Tiêu chí |
HolySheep AI |
Binance API Chính Thức |
Tardis.dev |
ccxt |
| Chi phí hàng tháng |
$15-200/tháng |
Miễn phí (rate limited) |
$99-999/tháng |
Miễn phí |
| Chi phí per token (GPT-4.1) |
$8/MTok |
$8/MTok |
$8/MTok |
$8/MTok |
| Độ trễ trung bình |
<50ms |
100-300ms |
60-150ms |
150-400ms |
| Dữ liệu lịch sử |
Limited |
7 ngày (klines) |
Full history |
Phụ thuộc exchange |
| WebSocket real-time |
Có |
Có |
Có |
Có |
| Hỗ trợ thanh toán |
WeChat/Alipay/Visa |
Chỉ card quốc tế |
Card quốc tế |
N/A |
| Tín dụng miễn phí đăng ký |
Có ($5-20) |
Không |
Có ($5) |
N/A |
| Setup ban đầu |
5 phút |
30-60 phút |
15-30 phút |
20-40 phút |
Tardis.dev Là Gì và Tại Sao Cần Nó?
Tardis.dev là dịch vụ cung cấp dữ liệu thị trường cryptocurrency dưới dạng normalized format, hỗ trợ replay historical data với độ chính xác cao. Khác với API chính thức của sàn, Tardis.dev cung cấp:
- Dữ liệu order book chi tiết từng tick với timestamp chính xác đến microsecond
- Replay data với chế độ simulation để backtest chính xác hơn
- Normalized format across nhiều sàn (Binance, Bybit, OKX, Bitfinex...)
- Historical snapshots và incremental updates
Phù Hợp Với Ai
Nên dùng Tardis.dev Python API khi:
- Bạn cần dữ liệu order book lịch sử để backtest chiến lược market making
- Muốn mô phỏng撮合 engine để test order matching logic
- Cần replay dữ liệu với timing chính xác
- Xây dựng bot giao dịch với độ trễ thấp và cần historical data để validate
- Phân tích liquidity và order book dynamics
Không phù hợp khi:
- Chỉ cần real-time data và không quan tâm đến lịch sử
- Ngân sách hạn chế (phí Tardis.dev khá cao: $99-999/tháng)
- Cần dữ liệu từ nhiều sàn với format khác nhau mà không muốn normalize
Cài Đặt Môi Trường và Dependencies
# Cài đặt các thư viện cần thiết
pip install tardis-dev pandas numpy asyncio aiohttp msgpack
Hoặc sử dụng poetry
poetry add tardis-dev pandas numpy aiohttp msgpack
Kiểm tra phiên bản
python -c "import tardis; print(tardis.__version__)"
Output: 3.2.1 hoặc newer
Kết Nối và Tải Dữ Liệu Order Book Từ Binance
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
Cấu hình Tardis.dev API
TARDIS_API_KEY = "YOUR_TARDIS_API_KEY"
BASE_URL = "https://api.tardis.dev/v1"
@dataclass
class OrderBookEntry:
"""Cấu trúc một entry trong order book"""
price: float
size: float
side: str # 'bid' hoặc 'ask'
timestamp: int # microseconds
local_timestamp: int
@dataclass
class OrderBookSnapshot:
"""Snapshot đầy đủ của order book tại một thời điểm"""
symbol: str
bids: List[OrderBookEntry]
asks: List[OrderBookEntry]
timestamp: int
exchange: str = "binance-futures"
class BinanceOrderBookDownloader:
"""Download và xử lý dữ liệu order book từ Tardis.dev"""
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = BASE_URL
async def get_order_book_replay(
self,
symbol: str = "BTC-USDT-PERP",
start_date: datetime = None,
end_date: datetime = None,
channels: List[str] = None
):
"""
Tải dữ liệu order book replay từ Tardis.dev
Args:
symbol: Cặp giao dịch (format: BTC-USDT-PERP)
start_date: Thời điểm bắt đầu
end_date: Thời điểm kết thúc
channels: Các kênh dữ liệu cần subscribe
"""
if channels is None:
channels = ["book_snapshot", "book_update"]
# Format thời gian theo ISO 8601
start_str = start_date.isoformat() if start_date else None
end_str = end_date.isoformat() if end_date else None
params = {
"symbol": symbol,
"channels": ",".join(channels),
"from": start_str,
"to": end_str,
"format": "json"
}
headers = {
"Authorization": f"Bearer {self.api_key}",
"Accept": "application/x-ndjson"
}
all_data = []
async with aiohttp.ClientSession() as session:
async with session.get(
f"{self.base_url}/replay",
params=params,
headers=headers
) as response:
if response.status != 200:
error = await response.text()
raise Exception(f"API Error {response.status}: {error}")
# Đọc dữ liệu dạng newline-delimited JSON
async for line in response.content:
if line.strip():
try:
data = json.loads(line)
all_data.append(data)
except json.JSONDecodeError:
continue
return all_data
Sử dụng
downloader = BinanceOrderBookDownloader(TARDIS_API_KEY)
Tải dữ liệu 1 ngày
start = datetime(2024, 3, 15, 0, 0, 0)
end = datetime(2024, 3, 16, 0, 0, 0)
data = await downloader.get_order_book_replay(
symbol="BTC-USDT-PERP",
start_date=start,
end_date=end
)
print(f"Đã tải {len(data)} records")
Xây Dựng Order Book Manager với Chức Năng Mô Phỏng撮合
import heapq
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Dict, List, Tuple
import time
@dataclass
class Order:
"""Cấu trúc một lệnh trong hệ thống"""
order_id: str
price: float
size: float
side: str # 'buy' hoặc 'sell'
timestamp: int
status: str = "pending" # pending, filled, partially_filled, cancelled
class MatchingEngine:
"""
Mô phỏng撮合 engine của Binance Futures
Xử lý matching giữa buy và sell orders
"""
def __init__(self, symbol: str):
self.symbol = symbol
# Heap cho bids (max-heap simulation bằng negative prices)
self.bids = [] # List of (price, order)
# Heap cho asks (min-heap)
self.asks = []
self.order_map: Dict[str, Order] = {}
self.trade_history: List[Dict] = []
self.sequence = 0
def apply_snapshot(self, snapshot: Dict):
"""
Áp dụng order book snapshot từ Tardis.dev
"""
self.bids = []
self.asks = []
# Parse bids
for price, size in snapshot.get('b', []):
order = Order(
order_id=f"snap_b_{self.sequence}",
price=float(price),
size=float(size),
side="buy",
timestamp=snapshot.get('E', 0)
)
self.sequence += 1
heapq.heappush(self.bids, (-order.price, order))
self.order_map[order.order_id] = order
# Parse asks
for price, size in snapshot.get('a', []):
order = Order(
order_id=f"snap_a_{self.sequence}",
price=float(price),
size=float(size),
side="sell",
timestamp=snapshot.get('E', 0)
)
self.sequence += 1
heapq.heappush(self.asks, (order.price, order))
self.order_map[order.order_id] = order
def apply_update(self, update: Dict):
"""
Áp dụng incremental update
"""
updates = update.get('u', [])
for price, size in updates:
price = float(price)
size = float(size)
if size == 0:
# Remove order
self._remove_order(price)
else:
# Add or update order
self._add_or_update_order(price, size)
def _remove_order(self, price: float):
"""Xóa order ở mức giá xác định"""
# Tìm và remove từ bids
new_bids = []
for neg_p, order in self.bids:
if abs(order.price - price) > 1e-8:
new_bids.append((neg_p, order))
self.bids = new_bids
heapq.heapify(self.bids)
# Tìm và remove từ asks
new_asks = []
for p, order in self.asks:
if abs(order.price - price) > 1e-8:
new_asks.append((p, order))
self.asks = new_asks
heapq.heapify(self.asks)
def _add_or_update_order(self, price: float, size: float):
"""Thêm hoặc cập nhật order"""
side = "buy" if price < self.get_mid_price() else "sell"
# Tìm xem đã có order ở mức giá này chưa
existing = self._find_order(price)
if existing:
existing.size = size
if size == 0:
self._remove_order(price)
else:
order = Order(
order_id=f"upd_{self.sequence}",
price=price,
size=size,
side=side,
timestamp=int(time.time() * 1000000)
)
self.sequence += 1
self.order_map[order.order_id] = order
if side == "buy":
heapq.heappush(self.bids, (-price, order))
else:
heapq.heappush(self.asks, (price, order))
def _find_order(self, price: float) -> Optional[Order]:
"""Tìm order ở mức giá xác định"""
for neg_p, order in self.bids:
if abs(order.price - price) < 1e-8:
return order
for p, order in self.asks:
if abs(order.price - price) < 1e-8:
return order
return None
def get_mid_price(self) -> float:
"""Lấy giá giữa bid-ask"""
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 0.0
def get_best_bid(self) -> float:
"""Lấy best bid price"""
if self.bids:
return -self.bids[0][0]
return 0.0
def get_best_ask(self) -> float:
"""Lấy best ask price"""
if self.asks:
return self.asks[0][0]
return 0.0
def get_spread(self) -> float:
"""Tính spread"""
return self.get_best_ask() - self.get_best_bid()
def get_top_n_levels(self, n: int = 10) -> Dict:
"""Lấy top N levels của order book"""
# Sort bids descending
sorted_bids = sorted(self.bids, key=lambda x: -x[0])[:n]
# Sort asks ascending
sorted_asks = sorted(self.asks, key=lambda x: x[0])[:n]
return {
'bids': [(order.price, order.size) for _, order in sorted_bids],
'asks': [(order.price, order.size) for _, order in sorted_asks],
'mid_price': self.get_mid_price(),
'spread': self.get_spread(),
'spread_bps': (self.get_spread() / self.get_mid_price() * 10000) if self.get_mid_price() else 0
}
def submit_order(self, price: float, size: float, side: str) -> List[Dict]:
"""
Submit một order mới và thực hiện matching
Trả về list các trades được tạo ra
"""
trades = []
order = Order(
order_id=f"ord_{self.sequence}",
price=price,
size=size,
side=side,
timestamp=int(time.time() * 1000000)
)
self.sequence += 1
self.order_map[order.order_id] = order
if side == "buy":
# Match với các sell orders
remaining = size
while remaining > 0 and self.asks:
best_ask_price, best_ask_order = self.asks[0]
if price >= best_ask_price:
fill_size = min(remaining, best_ask_order.size)
trade = {
'order_id': order.order_id,
'counter_order_id': best_ask_order.order_id,
'price': best_ask_price,
'size': fill_size,
'side': side,
'timestamp': order.timestamp
}
trades.append(trade)
self.trade_history.append(trade)
best_ask_order.size -= fill_size
remaining -= fill_size
if best_ask_order.size == 0:
heapq.heappop(self.asks)
else:
break
elif side == "sell":
# Match với các buy orders
remaining = size
while remaining > 0 and self.bids:
neg_best_bid_price, best_bid_order = self.bids[0]
best_bid_price = -neg_best_bid_price
if price <= best_bid_price:
fill_size = min(remaining, best_bid_order.size)
trade = {
'order_id': order.order_id,
'counter_order_id': best_bid_order.order_id,
'price': best_bid_price,
'size': fill_size,
'side': side,
'timestamp': order.timestamp
}
trades.append(trade)
self.trade_history.append(trade)
best_bid_order.size -= fill_size
remaining -= fill_size
if best_bid_order.size == 0:
heapq.heappop(self.bids)
else:
break
# Cập nhật remaining size
order.size = remaining
order.status = "filled" if remaining == 0 else "partially_filled"
# Thêm phần còn lại vào order book
if remaining > 0:
if side == "buy":
heapq.heappush(self.bids, (-price, order))
else:
heapq.heappush(self.asks, (price, order))
return trades
Ví dụ sử dụng
engine = MatchingEngine("BTC-USDT-PERP")
Tạo snapshot giả lập
snapshot = {
'b': [['50000.0', '10.5'], ['49999.5', '8.2'], ['49999.0', '15.0']],
'a': [['50000.5', '12.0'], ['50001.0', '9.5'], ['50001.5', '20.0']],
'E': 1710500000000000
}
engine.apply_snapshot(snapshot)
print(f"Best Bid: {engine.get_best_bid()}")
print(f"Best Ask: {engine.get_best_ask()}")
print(f"Spread: {engine.get_spread()} ({engine.get_spread() / engine.get_mid_price() * 10000:.2f} bps)")
Submit một buy order
trades = engine.submit_order(50001.0, 5.0, 'buy')
print(f"Tạo {len(trades)} trades")
for trade in trades:
print(f" Trade: price={trade['price']}, size={trade['size']}")
Xây Dựng Data Pipeline Hoàn Chỉnh
import asyncio
import json
import msgpack
from pathlib import Path
from typing import Generator, AsyncGenerator
import pandas as pd
from datetime import datetime
class OrderBookDataPipeline:
"""
Pipeline xử lý dữ liệu order book từ Tardis.dev
Bao gồm: download, parse, validate, store
"""
def __init__(self, tardis_key: str, cache_dir: str = "./data_cache"):
self.downloader = BinanceOrderBookDownloader(tardis_key)
self.cache_dir = Path(cache_dir)
self.cache_dir.mkdir(exist_ok=True)
async def download_and_process(
self,
symbol: str,
date: datetime,
batch_size: int = 10000
) -> pd.DataFrame:
"""
Download và xử lý dữ liệu cho một ngày
"""
start = datetime(date.year, date.month, date.day, 0, 0, 0)
end = start + timedelta(days=1)
print(f"Bắt đầu download {symbol} cho ngày {date.date()}")
# Check cache
cache_file = self.cache_dir / f"{symbol}_{date.strftime('%Y%m%d')}.parquet"
if cache_file.exists():
print(f"Đọc từ cache: {cache_file}")
return pd.read_parquet(cache_file)
# Download
raw_data = await self.downloader.get_order_book_replay(
symbol=symbol,
start_date=start,
end_date=end
)
print(f"Đã download {len(raw_data)} records raw")
# Process thành DataFrame
records = self._parse_raw_data(raw_data)
df = pd.DataFrame(records)
# Validate
df = self._validate_and_clean(df)
# Lưu cache
df.to_parquet(cache_file)
print(f"Đã lưu {len(df)} records vào cache")
return df
def _parse_raw_data(self, raw_data: List[Dict]) -> List[Dict]:
"""Parse raw data thành normalized format"""
records = []
for item in raw_data:
channel = item.get('channel', '')
data = item.get('data', item)
if channel == 'book_snapshot' or 'b' in data:
# Snapshot
record = {
'type': 'snapshot',
'timestamp': data.get('E', data.get('ts', 0)),
'local_timestamp': data.get('T', 0),
'symbol': data.get('s', ''),
'bids': json.dumps(data.get('b', [])),
'asks': json.dumps(data.get('a', [])),
'bid_count': len(data.get('b', [])),
'ask_count': len(data.get('a', [])),
'best_bid': float(data['b'][0][0]) if data.get('b') else None,
'best_ask': float(data['a'][0][0]) if data.get('a') else None,
'mid_price': self._calc_mid(data),
'spread': self._calc_spread(data),
'spread_bps': self._calc_spread_bps(data)
}
records.append(record)
elif channel == 'book_update' or 'u' in data:
# Update
record = {
'type': 'update',
'timestamp': data.get('E', data.get('ts', 0)),
'local_timestamp': data.get('T', 0),
'symbol': data.get('s', ''),
'update_id': data.get('u', data.get('seq', 0)),
'updates': json.dumps(data.get('u', [])),
'update_count': len(data.get('u', []))
}
records.append(record)
elif 'trade' in channel:
# Trade
record = {
'type': 'trade',
'timestamp': data.get('E', data.get('ts', 0)),
'local_timestamp': data.get('T', 0),
'symbol': data.get('s', ''),
'price': float(data.get('p', 0)),
'size': float(data.get('q', 0)),
'side': data.get('m', 'unknown'), # m=true means buyer is maker
'trade_id': data.get('t', data.get('i', ''))
}
records.append(record)
return records
def _calc_mid(self, data: Dict) -> float:
"""Tính mid price"""
bids = data.get('b', [])
asks = data.get('a', [])
if bids and asks:
return (float(bids[0][0]) + float(asks[0][0])) / 2
return 0.0
def _calc_spread(self, data: Dict) -> float:
"""Tính spread tuyệt đối"""
bids = data.get('b', [])
asks = data.get('a', [])
if bids and asks:
return float(asks[0][0]) - float(bids[0][0])
return 0.0
def _calc_spread_bps(self, data: Dict) -> float:
"""Tính spread theo basis points"""
bids = data.get('b', [])
asks = data.get('a', [])
if bids and asks:
mid = (float(bids[0][0]) + float(asks[0][0])) / 2
if mid > 0:
return (float(asks[0][0]) - float(bids[0][0])) / mid * 10000
return 0.0
def _validate_and_clean(self, df: pd.DataFrame) -> pd.DataFrame:
"""Validate và clean data"""
# Remove duplicates
df = df.drop_duplicates(subset=['timestamp', 'type'], keep='last')
# Sort by timestamp
df = df.sort_values('timestamp').reset_index(drop=True)
# Handle missing values
if 'mid_price' in df.columns:
df['mid_price'] = df['mid_price'].fillna(method='ffill')
return df
def analyze_order_book(self, df: pd.DataFrame) -> Dict:
"""Phân tích order book"""
snapshot_df = df[df['type'] == 'snapshot'].copy()
if snapshot_df.empty:
return {}
analysis = {
'total_snapshots': len(snapshot_df),
'avg_spread_bps': snapshot_df['spread_bps'].mean(),
'median_spread_bps': snapshot_df['spread_bps'].median(),
'max_spread_bps': snapshot_df['spread_bps'].max(),
'min_spread_bps': snapshot_df['spread_bps'].min(),
'avg_bid_count': snapshot_df['bid_count'].mean(),
'avg_ask_count': snapshot_df['ask_count'].mean(),
'time_span_ms': snapshot_df['timestamp'].max() - snapshot_df['timestamp'].min()
}
return analysis
Sử dụng pipeline
async def main():
pipeline = OrderBookDataPipeline(
tardis_key="YOUR_TARDIS_API_KEY",
cache_dir="./orderbook_data"
)
# Download một ngày dữ liệu
date = datetime(2024, 3, 15)
df = await pipeline.download_and_process(
symbol="BTC-USDT-PERP",
date=date
)
# Phân tích
analysis = pipeline.analyze_order_book(df)
print("=== Order Book Analysis ===")
for key, value in analysis.items():
print(f"{key}: {value:.4f}" if isinstance(value, float) else f"{key}: {value}")
# Replay với matching engine
engine = MatchingEngine("BTC-USDT-PERP")
for _, row in df[df['type'] == 'snapshot'].head(100).iterrows():
snapshot = {
'b': json.loads(row['bids']),
'a': json.loads(row['asks']),
'E': row['timestamp']
}
engine.apply_snapshot(snapshot)
# Log trạng thái
if _ % 10 == 0:
levels = engine.get_top_n_levels(5)
print(f"\nSnapshot {_}:")
print(f" Mid: {levels['mid_price']:.2f}, Spread: {levels['spread_bps']:.2f} bps")
if __name__ == "__main__":
asyncio.run(main())
Tối Ưu Hóa Hiệu Suất cho Dữ Liệu Lớn
import numpy as np
from numba import jit
import numba
@jit(nopython=True)
def calculate_vwap_numba(prices: np.ndarray, volumes: np.ndarray) -> float:
"""Tính VWAP sử dụng numba JIT"""
total_pv = 0.0
total_vol = 0.0
for i in range(len(prices)):
total_pv += prices[i] * volumes[i]
total_vol += volumes[i]
return total_pv / total_vol if total_vol > 0 else 0.0
@jit(nopython=True)
def calculate_order_imbalance_numba(
bid_prices: np.ndarray, bid_sizes: np.ndarray,
ask_prices: np.ndarray, ask_sizes: np.ndarray
) -> float:
"""
Tính Order Book Imbalance (OBI)
OBI = (BidVolume - AskVolume) / (BidVolume + AskVolume)
Giá trị dương = bullish, giá trị âm = bearish
"""
total_bid_vol = 0.0
total_ask_vol = 0.0
for i in range(len(bid_sizes)):
total_bid_vol += bid_sizes[i]
for i in range(len(ask_sizes)):
total_ask_vol += ask_sizes[i]
total = total_bid_vol + total_ask_vol
if total == 0:
return 0.0
return (total_bid_vol - total_ask_vol) / total
class OrderBookAnalyzer:
"""Phân tích order book với hiệu suất cao"""
def __init__(self):
self.history = []
def analyze_snapshot_fast(self, bids: List[List], asks: List[List]) -> Dict:
"""Phân tích nhanh một snapshot"""
# Convert sang numpy arrays
bid_prices = np.array([float(b[0]) for b in bids])
bid_sizes = np.array([float(b[1]) for b in bids])
ask_prices = np.array([float(a[0]) for a in asks])
ask_sizes = np.array([float(a[1]) for a in asks])
# Tính các metrics
obi = calculate_order_imbalance_numba(
bid_prices, bid_sizes, ask_prices, ask_sizes
)
best_bid = bid_prices[0] if len(bid_prices) > 0 else 0
best_ask = ask_prices[0] if len(ask_prices) > 0 else 0
mid_price = (best_bid + best_ask) / 2
# Tính volume weighted metrics
bid_volumes = bid_sizes
ask_volumes = ask_sizes
# Volume ở các mức giá khác nhau
depth_levels = 10
bid_cumvol = np.cumsum(bid_sizes[:depth_levels])
ask_cumvol = np.cumsum(ask_sizes[:depth_levels])
return {
'best_bid': best_bid,
'best_ask': best_ask,
'spread': best_ask - best_bid,
'spread_bps': (best_ask - best_bid) / mid_price * 10000 if mid_price > 0 else 0,
'order_imbalance': obi,
'total_bid_volume': np.sum(bid_sizes),
'total_ask_volume': np.sum(ask_sizes),
'bid_depth_1pct': self._calc_depth_at_level(bid_prices, bid_sizes, mid_price, 0.01),
'ask_depth_1pct': self._calc_depth_at_level(ask_prices, ask_sizes, mid_price, 0.01),
'bid_vwap_levels': np.sum(bid_prices[:5] * bid_sizes[:5]) / np.sum(bid_sizes[:5]) if np.sum(bid_sizes[:5]) > 0 else 0,
'ask_vwap_levels': np.sum(ask_prices[:5] * ask_sizes[:5]) / np.sum(ask_sizes[:5]) if np.sum(ask_sizes[:5]) > 0 else 0
}
@staticmethod
@jit(nopython=True)
def _calc_depth_at_level(
prices: np.ndarray,
sizes: np.ndarray,
mid_price: float,
level_pct: float
) -> float:
"""Tính volume tích lũy trong khoảng level_pct từ mid price"""
threshold = mid_price * level_pct
total = 0.0
for i in range(len(prices)):
if abs(prices[i] - mid_price) <= threshold:
total += sizes[i]
return total
def add_to_history(self, analysis: Dict):
"""Thêm kết quả phân tích vào lịch sử"""
self.history.append(analysis)
def get_momentum_signal(self, window: int = 100) -> float:
"""
Tính momentum signal dựa trên OBI history
"""
if len(self.history) < window:
return 0.0
recent_obi = [h['order_imbalance'] for h in self.history[-window:]]
return np.mean(recent_obi)
def detect_liquidity_events(self, threshold_bps: float = 50.0) -> List[Dict]:
"""Phát hiện các sự kiện liquidity"""
events = []
for i, h in enumerate(self.history):
Tài nguyên liên quan
Bài viết liên quan