Giới thiệu
Trong thế giới giao dịch tần số cao (HFT) và định lượng, việc backtest chiến lược trên dữ liệu lịch sử thực tế là yếu tố quyết định sự thành bại của một hệ thống giao dịch. Tardis Protocol là một trong những công cụ mạnh mẽ nhất hiện nay cho việc replay dữ liệu orderbook từ các sàn giao dịch tiền mã hóa như Binance, Coinbase, Bybit. Bài viết này sẽ hướng dẫn chi tiết cách chuẩn bị dữ liệu orderbook cho backtest, từ việc thu thập raw data đến định dạng sẵn sàng cho strategy testing.
Tardis là gì và tại sao cần nó?
Tardis (tardis.dev) là nền tảng cung cấp dữ liệu market data chất lượng cao cho crypto, bao gồm full orderbook snapshots, trades, candles và funding rates. Khác với việc sử dụng API của sàn giao dịch trực tiếp (thường bị giới hạn rate và thiếu dữ liệu lịch sử sâu), Tardis cung cấp:
- Dữ liệu orderbook với độ sâu đầy đủ (full depth L2)
- Replay theo thời gian thực với độ trễ thấp
- Hỗ trợ nhiều sàn: Binance, Coinbase, Bybit, OKX, Kraken
- Format chuẩn hóa, dễ xử lý
Case Study: Startup Trading Firm ở Singapore
Một đội ngũ quantitative trading gồm 5 người ở Singapore đã xây dựng hệ thống market-making cho các cặp giao dịch trên Binance Futures. Trước đây, họ sử dụng phương pháp thủ công:
- Tải dữ liệu từ nhiều nguồn khác nhau (sàn, third-party providers)
- Viết script Python để normalize data mỗi lần backtest
- Mất 2-3 ngày chỉ để chuẩn bị dữ liệu cho một strategy
Sau khi tích hợp Tardis vào pipeline, thời gian chuẩn bị dữ liệu giảm từ 72 giờ xuống còn 4 giờ. Họ có thể backtest 50 strategy variants mỗi tuần thay vì 5. Kết quả: Sharpe Ratio trung bình tăng 23% nhờ việc test được nhiều biến thể hơn trong cùng thời gian.
Kiến trúc dữ liệu Orderbook
Trước khi đi vào chi tiết kỹ thuật, cần hiểu cấu trúc dữ liệu orderbook:
Cấu trúc Level 2 Orderbook
{
"symbol": "BTC-USDT",
"timestamp": 1704067200000,
"bids": [
{"price": 42000.00, "size": 1.5},
{"price": 41999.50, "size": 0.8}
],
"asks": [
{"price": 42001.00, "size": 2.1},
{"price": 42002.00, "size": 0.5}
]
}
Orderbook gồm hai phía: bids (lệnh mua) và asks (lệnh bán). Mỗi level chứa price và size (khối lượng). Độ sâu của orderbook thể hiện likvidity của thị trường tại thời điểm đó.
Snapshot vs Incremental Update
Tardis cung cấp hai loại dữ liệu:
- Snapshots: Toàn bộ orderbook tại một thời điểm
- Updates/Deltas: Thay đổi chỉ những level bị ảnh hưởng
Để replay chính xác, cần load snapshot ban đầu rồi apply các updates theo thứ tự thời gian.
Cài đặt Tardis SDK và Dependencies
npm install tardis-dev
hoặc với yarn
yarn add tardis-dev
Python client
pip install tardis
Docker cho production deployment
docker pull ghcr.io/tardis-dev/tardis:latest
Kết nối và lấy dữ liệu Orderbook
import { TardisInvestorClient } from 'tardis-dev';
// Khởi tạo client với API key
const client = new TardisInvestorClient({
apiKey: process.env.TARDIS_API_KEY,
format: 'json' // hoặc 'csv', 'bincode'
});
async function fetchOrderbookData() {
// Lấy dữ liệu orderbook cho BTC-USDT từ Binance
const orderbookStream = client.replay({
exchange: 'binance',
symbols: ['BTC-USDT'],
from: new Date('2024-01-01'),
to: new Date('2024-01-31'),
channels: ['orderbook'],
type: 'incremental' // incremental hoặc snapshot
});
let count = 0;
const chunks = [];
for await (const data of orderbookStream) {
chunks.push(data);
count++;
// Log progress mỗi 10,000 messages
if (count % 10000 === 0) {
console.log(Processed ${count} messages);
}
}
console.log(Total: ${count} orderbook updates);
return chunks;
}
fetchOrderbookData()
.then(data => console.log('Data fetched successfully'))
.catch(console.error);
Xử lý và Normalize dữ liệu cho Backtest
import { OrderBook } from './orderbook-reconstructor';
class BacktestDataPreparator {
constructor(options = {}) {
this.orderbook = new OrderBook();
this.trades = [];
this.orderbookSnapshots = [];
this.snapshotInterval = options.snapshotInterval || 60000; // 1 phút
this.lastSnapshotTime = 0;
}
// Process raw orderbook update từ Tardis
processOrderbookUpdate(update) {
// Apply update vào orderbook state
for (const bid of update.b || update.bids || []) {
if (bid[1] === '0') {
this.orderbook.removeBid(bid[0]);
} else {
this.orderbook.setBid(bid[0], bid[1]);
}
}
for (const ask of update.a || update.asks || []) {
if (ask[1] === '0') {
this.orderbook.removeAsk(ask[0]);
} else {
this.orderbook.setAsk(ask[0], ask[1]);
}
}
// Take snapshot định kỳ
if (update.timestamp - this.lastSnapshotTime >= this.snapshotInterval) {
this.orderbookSnapshots.push({
timestamp: update.timestamp,
bids: this.orderbook.getBids(),
asks: this.orderbook.getAsks()
});
this.lastSnapshotTime = update.timestamp;
}
}
// Tính toán features cho ML model
extractFeatures(snapshot) {
const topBid = snapshot.bids[0];
const topAsk = snapshot.asks[0];
const spread = (topAsk.price - topBid.price) / topBid.price;
let bidVolume = 0;
let askVolume = 0;
// Tính volume trong 10 levels đầu
for (let i = 0; i < 10 && i < snapshot.bids.length; i++) {
bidVolume += snapshot.bids[i].size;
askVolume += snapshot.asks[i].size;
}
const imbalance = (bidVolume - askVolume) / (bidVolume + askVolume);
return {
timestamp: snapshot.timestamp,
spread,
topBidPrice: topBid.price,
topAskPrice: topAsk.price,
bidVolume,
askVolume,
imbalance,
midPrice: (topBid.price + topAsk.price) / 2
};
}
// Export dữ liệu đã xử lý
export() {
return {
snapshots: this.orderbookSnapshots,
features: this.orderbookSnapshots.map(s => this.extractFeatures(s)),
metadata: {
totalSnapshots: this.orderbookSnapshots.length,
dateRange: {
from: this.orderbookSnapshots[0]?.timestamp,
to: this.orderbookSnapshots[this.orderbookSnapshots.length - 1]?.timestamp
}
}
};
}
}
// Sử dụng
const preparator = new BacktestDataPreparator({
snapshotInterval: 30000 // 30 giây
});
// Giả sử có rawData từ bước trước
for (const update of rawOrderbookUpdates) {
preparator.processOrderbookUpdate(update);
}
const preparedData = preparator.export();
console.log('Features sample:', preparedData.features[0]);
Tardis vs Alternatives: So sánh chi tiết
| Tiêu chí | Tardis | Binance API trực tiếp | CCXT Library |
|---|---|---|---|
| Độ sâu dữ liệu lịch sử | 2017 - nay (đầy đủ) | 7 ngày (k9) / 1 ngày (incremental) | Phụ thuộc exchange |
| Rate limit | Không giới hạn | 1200 request/phút | 120 request/phút |
| Replay thời gian thực | Có (built-in) | Không | Không |
| Định dạng | JSON, CSV, Bincode | JSON only | JSON (đa số) |
| Giá tham khảo | Từ $199/tháng | Miễn phí (giới hạn) | Miễn phí |
| Hỗ trợ | 10+ sàn giao dịch | Binance only | 100+ sàn |
| Độ trễ dữ liệu | <1 phút cho historical | Real-time | Real-time |
Phù hợp và không phù hợp với ai
✅ Nên dùng Tardis khi:
- Bạn đang xây dựng chiến lược market-making hoặc arbitrage
- Cần backtest trên dữ liệu orderbook đầy đủ (full depth L2)
- Muốn test strategy với nhiều biến thể trong thời gian ngắn
- Phát triển bot giao dịch tần số cao (HFT)
- Cần dữ liệu từ nhiều sàn giao dịch để so sánh
❌ Không cần Tardis khi:
- Chỉ cần dữ liệu OHLCV (candles) - dùng free API của sàn
- Strategy đơn giản, không cần orderbook level
- Backtest trong khung thời gian ngắn (<7 ngày)
- Ngân sách hạn chế và có thể chấp nhận giới hạn của free tier
Cấu hình cho các sàn giao dịch phổ biến
import { TardisInvestorClient } from 'tardis-dev';
const client = new TardisInvestorClient({
apiKey: process.env.TARDIS_API_KEY
});
// Cấu hình cho từng sàn khác nhau
const exchangeConfigs = {
binance: {
// Binance Futures perpetual
symbols: ['BTC-USDT', 'ETH-USDT', 'SOL-USDT'],
channels: ['orderbook', 'trade'],
type: 'incremental'
},
coinbase: {
// Coinbase Advanced Trade
symbols: ['BTC-USD', 'ETH-USD'],
channels: ['level2', 'matches'],
granularity: 'RAW' // hoặc 'FIFTEEN_SECONDS'
},
bybit: {
// ByBit Unified Trading
symbols: ['BTCUSDT', 'ETHUSDT'],
channels: ['orderbook.200ms'],
category: 'linear' // linear, inverse, spot
}
};
// Ví dụ: Lấy dữ liệu từ nhiều sàn cùng lúc
async function multiExchangeBacktestData() {
const results = {};
for (const [exchange, config] of Object.entries(exchangeConfigs)) {
console.log(Fetching ${exchange} data...);
const stream = client.replay({
exchange,
...config,
from: new Date('2024-06-01'),
to: new Date('2024-06-30')
});
const data = [];
for await (const chunk of stream) {
data.push(chunk);
}
results[exchange] = {
messageCount: data.length,
symbols: config.symbols
};
}
return results;
}
Tối ưu hóa hiệu suất xử lý
Khi làm việc với dữ liệu orderbook lớn (hàng triệu messages), cần tối ưu để tránh memory overflow và giảm thời gian xử lý:
// Sử dụng streaming với batching
class StreamingBacktestPreparator {
constructor(options = {}) {
this.batchSize = options.batchSize || 10000;
this.outputPath = options.outputPath;
this.currentBatch = [];
}
async *processStream(dataStream) {
for await (const update of dataStream) {
const processed = this.processOrderbookUpdate(update);
this.currentBatch.push(processed);
if (this.currentBatch.length >= this.batchSize) {
yield this.currentBatch;
this.currentBatch = [];
}
}
// Yield remaining data
if (this.currentBatch.length > 0) {
yield this.currentBatch;
}
}
// Parallel processing với worker threads
async processInParallel(dataChunks, workerCount = 4) {
const { Worker } = require('worker_threads');
const workers = [];
for (let i = 0; i < workerCount; i++) {
const worker = new Worker('./orderbook-worker.js', {
workerData: dataChunks[i]
});
workers.push(worker);
}
const results = await Promise.all(
workers.map(w => new Promise((resolve, reject) => {
w.on('message', resolve);
w.on('error', reject);
}))
);
return results.flat();
}
}
// Sử dụng với backpressure control
async function controlledProcessing() {
const preparator = new StreamingBacktestPreparator({
batchSize: 50000,
outputPath: './output/backtest-data.json'
});
let processed = 0;
const startTime = Date.now();
for await (const batch of preparator.processStream(rawDataStream)) {
// Write batch to disk immediately
await fs.appendFile(
preparator.outputPath,
JSON.stringify(batch) + '\n'
);
processed += batch.length;
const elapsed = (Date.now() - startTime) / 1000;
const rate = processed / elapsed;
console.log(Processed: ${processed} | Rate: ${rate.toFixed(0)} msg/s);
}
}
Đánh giá chất lượng dữ liệu
Trước khi sử dụng dữ liệu cho backtest, cần kiểm tra chất lượng để tránh "garbage in, garbage out":
class DataQualityChecker {
constructor() {
this.issues = [];
}
checkOrderbookConsistency(snapshots) {
let lastMidPrice = null;
for (const snapshot of snapshots) {
const midPrice = (snapshot.bids[0].price + snapshot.asks[0].price) / 2;
// Kiểm tra spread hợp lý (không quá lớn)
const spread = (snapshot.asks[0].price - snapshot.bids[0].price) / midPrice;
if (spread > 0.01) { // 1%
this.issues.push({
type: 'HIGH_SPREAD',
timestamp: snapshot.timestamp,
spread,
severity: 'warning'
});
}
// Kiểm tra mid price jump (flash crash detection)
if (lastMidPrice !== null) {
const priceChange = Math.abs(midPrice - lastMidPrice) / lastMidPrice;
if (priceChange > 0.05) { // 5% jump
this.issues.push({
type: 'PRICE_JUMP',
timestamp: snapshot.timestamp,
change: priceChange,
severity: 'critical'
});
}
}
lastMidPrice = midPrice;
}
return {
isClean: this.issues.filter(i => i.severity === 'critical').length === 0,
issues: this.issues,
summary: {
totalChecked: snapshots.length,
criticalIssues: this.issues.filter(i => i.severity === 'critical').length,
warnings: this.issues.filter(i => i.severity === 'warning').length
}
};
}
checkDataCompleteness(data) {
// Kiểm tra gaps trong timeline
const gaps = [];
for (let i = 1; i < data.snapshots.length; i++) {
const gap = data.snapshots[i].timestamp - data.snapshots[i-1].timestamp;
const expectedInterval = 60000; // 1 phút
if (gap > expectedInterval * 1.5) {
gaps.push({
from: data.snapshots[i-1].timestamp,
to: data.snapshots[i].timestamp,
duration: gap
});
}
}
return {
hasGaps: gaps.length > 0,
gaps,
coverage: ((data.snapshots.length * 60000) / (data.metadata.dateRange.to - data.metadata.dateRange.from)) * 100
};
}
}
// Sử dụng
const checker = new DataQualityChecker();
const consistencyResult = checker.checkOrderbookConsistency(preparedData.snapshots);
const completenessResult = checker.checkDataCompleteness(preparedData);
console.log('Quality Check:', consistencyResult);
console.log('Completeness:', completenessResult);
Lỗi thường gặp và cách khắc phục
1. Lỗi "Memory limit exceeded" khi xử lý dữ liệu lớn
// ❌ Sai: Load toàn bộ dữ liệu vào memory
const allData = await client.getAllData({ from, to });
// → Crash với dữ liệu lớn
// ✅ Đúng: Sử dụng streaming và chunking
async function processLargeDataset() {
const MAX_MEMORY_MB = 512;
let totalProcessed = 0;
const stream = client.replay({
from: startDate,
to: endDate,
channels: ['orderbook']
});
for await (const chunk of stream) {
// Xử lý từng chunk
await processChunk(chunk);
// Force garbage collection nếu cần
if (global.gc) {
global.gc();
}
totalProcessed += chunk.length;
console.log(Processed: ${totalProcessed} messages);
}
}
// Hoặc sử dụng option "limit" để chia nhỏ
const chunks = await client.replay({
from: new Date('2024-01-01'),
to: new Date('2024-02-01'),
limit: 1000000 // chunks 1M messages
}, (data) => {
// Process mỗi chunk
});
2. Lỗi đồng bộ thời gian (Timestamp mismatch)
// ❌ Sai: Giả sử timestamp từ exchange đã chính xác
const timestamp = data.timestamp; // Có thể sai timezone
// ✅ Đúng: Luôn convert về UTC và validate
function normalizeTimestamp(data, exchange) {
let timestamp = data.timestamp;
// Một số sàn trả về milliseconds, một số trả về seconds
if (timestamp < 1e12) {
timestamp *= 1000; // Convert seconds to milliseconds
}
// Parse và validate
const date = new Date(timestamp);
if (isNaN(date.getTime())) {
throw new Error(Invalid timestamp: ${data.timestamp});
}
// Check timezone offset (Binance dùng UTC+0)
if (exchange === 'binance') {
// Binance timestamps đã ở UTC, không cần adjust
return date.toISOString();
}
return date.toISOString();
}
// Validate sequence
function validateSequence(updates) {
for (let i = 1; i < updates.length; i++) {
if (updates[i].timestamp < updates[i-1].timestamp) {
console.warn(Out of sequence at index ${i});
// Xử lý: sort lại hoặc bỏ qua
}
}
}
3. Lỗi "Stale orderbook" sau khi reconnect
// ❌ Sai: Không xử lý reconnect, dữ liệu sai sau reconnect
const stream = client.replay({ exchange, symbols });
for await (const data of stream) {
applyUpdate(data); // Có thể sai nếu stream bị gián đoạn
}
// ✅ Đúng: Implement reconnection với state reset
class RobustOrderbookReplayer {
constructor(client, options) {
this.client = client;
this.orderbook = new OrderBook();
this.lastSequenceId = null;
this.reconnectAttempts = 0;
this.maxRetries = 5;
}
async replay(params) {
while (this.reconnectAttempts < this.maxRetries) {
try {
const stream = this.client.replay(params);
for await (const update of stream) {
// Kiểm tra sequence number (nếu có)
if (update.sequenceId !== undefined) {
if (this.lastSequenceId !== null && update.sequenceId !== this.lastSequenceId + 1) {
console.warn('Sequence gap detected, reinitializing...');
this.orderbook.reset();
}
this.lastSequenceId = update.sequenceId;
}
this.processUpdate(update);
}
break; // Thành công, thoát loop
} catch (error) {
this.reconnectAttempts++;
console.error(Reconnect attempt ${this.reconnectAttempts}/${this.maxRetries});
// Wait before retrying
await new Promise(r => setTimeout(r, 1000 * this.reconnectAttempts));
// Reset state
this.orderbook.reset();
this.lastSequenceId = null;
}
}
}
}
4. Lỗi định dạng dữ liệu khác nhau giữa các sàn
// ❌ Sai: Giả sử tất cả sàn có cùng format
const bid = update.b[0]; // Binance format
const ask = update.a[0];
// ✅ Đúng: Normalize dữ liệu theo từng sàn
const normalizers = {
binance: (data) => ({
bids: (data.b || data.bids || []).map(([p, s]) => ({ price: parseFloat(p), size: parseFloat(s) })),
asks: (data.a || data.asks || []).map(([p, s]) => ({ price: parseFloat(p), size: parseFloat(s) })),
timestamp: data.E || data.timestamp
}),
coinbase: (data) => ({
bids: (data.bids || []).map(([p, s]) => ({ price: parseFloat(p), size: parseFloat(s) })),
asks: (data.asks || []).map(([p, s]) => ({ price: parseFloat(p), size: parseFloat(s) })),
timestamp: data.time || data.timestamp
}),
bybit: (data) => ({
bids: (data.b || data.bids || []).map(([p, s]) => ({ price: parseFloat(p), size: parseFloat(s) })),
asks: (data.a || data.asks || []).map(([p, s]) => ({ price: parseFloat(p), size: parseFloat(s) })),
timestamp: data.ts || data.timestamp
})
};
function normalizeUpdate(update, exchange) {
const normalizer = normalizers[exchange.toLowerCase()];
if (!normalizer) {
throw new Error(Unsupported exchange: ${exchange});
}
return normalizer(update);
}
// Sử dụng
for await (const data of stream) {
const normalized = normalizeUpdate(data, 'binance');
applyToOrderbook(normalized);
}
Các best practices cho Backtest Data Preparation
- Luôn validate dữ liệu trước khi backtest: Sử dụng DataQualityChecker để phát hiện anomalies
- Store raw data riêng, processed data riêng: Không xử lý trực tiếp trên source data
- Sử dụng snapshot interval phù hợp: Chiến lược HFT cần 100ms, swing trading có thể dùng 1 phút
- Test trên nhiều giai đoạn thị trường: Bull, bear, sideways, high volatility
- Monitor memory usage: Set alert khi memory > 80% để tránh crash
- Version control dữ liệu: Lưu lại hash của dataset để reproducibility
Kết luận
Việc chuẩn bị dữ liệu orderbook cho backtest là bước nền tảng quyết định chất lượng của chiến lược giao dịch. Tardis cung cấp giải pháp toàn diện với dữ liệu chất lượng cao, API dễ sử dụng và khả năng replay linh hoạt. Tuy nhiên, cần lưu ý các vấn đề về memory management, data normalization và quality checking để đảm bảo backtest results đáng tin cậy.
Đầu tư thời gian vào data preparation infrastructure sẽ tiết kiệm rất nhiều chi phí (cả tiền bạc và cơ hội) về sau, tránh được việc deploy một chiến lược "perfect on paper" nhưng thất bại trên thị trường thực.
Tài nguyên bổ sung
- Tardis Documentation
- Tardis Node.js SDK
- Blog về Market Data Analysis
- FastDataStep - Alternative Data Provider
Nếu bạn đang xây dựng hệ thống trading với AI/ML models và cần tối ưu chi phí inference, đăng ký HolySheep AI để nhận giá cực kỳ cạnh tranh: DeepSeek V3.2 chỉ $0.42/MTok, Gemini 2.5 Flash $2.50/MTok — tiết kiệm đến 85% so với các provider khác.