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:

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:

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:

Để 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:

❌ Không cần Tardis khi:

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

  1. Luôn validate dữ liệu trước khi backtest: Sử dụng DataQualityChecker để phát hiện anomalies
  2. Store raw data riêng, processed data riêng: Không xử lý trực tiếp trên source data
  3. 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
  4. Test trên nhiều giai đoạn thị trường: Bull, bear, sideways, high volatility
  5. Monitor memory usage: Set alert khi memory > 80% để tránh crash
  6. 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


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.

👉 Đăng ký HolySheep AI — nhận tín dụng miễn phí khi đăng ký