Trong thế giới giao dịch tiền mã hóa hiện đại, việc backtest (kiểm thử ngược) chiến lược là yếu tố sống còn quyết định sự thành bại của một hệ thống trading. Một công ty trading desk tại TP.HCM — chuyên phát triển bot giao dịch futures trên Binance — đã phải đối mặt với bài toán nan giản: làm sao có được dữ liệu orderbook lịch sử chất lượng cao, độ trễ thấp, và chi phí hợp lý để backtest các chiến lược arbitrage và market-making của họ?

Bối cảnh kinh doanh: Đội ngũ 8 người với 3 trader chuyên nghiệp và 5 kỹ sư quant. Họ vận hành các chiến lược delta-neutral, arbitrage biên độ giá giữa spot và futures, và market-making trên các cặp BTC/USDT, ETH/USDT perpetual futures.

Điểm đau của nhà cung cấp cũ: Ban đầu, đội ngũ sử dụng dữ liệu ticker và trade history miễn phí từ Binance API. Kết quả? Chi phí infrastructure để lưu trữ và xử lý dữ liệu lên tới $3,200/tháng (server, storage, bandwidth). Độ trễ trung bình khi truy xuất dữ liệu orderbook lịch sử là 2.3 giây do phải query từ PostgreSQL database không được tối ưu. Quan trọng hơn, chất lượng dữ liệu không đủ để backtest các chiến lược đòi hỏi độ chính xác cao — nhiều khoảng trống (gap) trong dữ liệu, thiếu các mức giá trong orderbook.

Lý do chọn Tardis.dev: Sau khi đánh giá 4 giải pháp (CoinAPI, Kaiko, Bloomberg API, và Tardis.dev), đội ngũ quyết định chọn Tardis.dev vì 3 lý do chính: (1) Dữ liệu orderbook được replay với độ phân giải millisecond, (2) API thiết kế theo chuẩn WebSocket reactive, phù hợp với kiến trúc Python async hiện tại, (3) Chi phí tính theo volume data thay vì subscription cố định — phù hợp với mô hình startup đang scale.

Chi phí hóa đơn hàng tháng: $4,200 → $680 sau khi migrate sang Tardis.dev. Độ trễ truy xuất giảm từ 2.3 giây xuống còn 180ms trung bình. Thời gian để hoàn thành một bộ backtest đầy đủ giảm từ 6 giờ xuống còn 45 phút.

Tardis.dev là gì và tại sao nó phù hợp cho backtest?

Tardis.dev là nền tảng cung cấp dữ liệu market data theo thời gian thực và lịch sử (historical data) cho các sàn giao dịch tiền mã hóa. Khác với các API thông thường chỉ trả về OHLCV (Open, High, Low, Close, Volume), Tardis.dev cung cấp:

Điểm mạnh của Tardis.dev so với Binance chính thức API là khả năng truy xuất historical data với cấu trúc normalized, chuẩn hóa giữa các sàn, và không bị giới hạn rate limit như khi dùng public API của sàn.

Cài đặt môi trường và thư viện

Trước khi bắt đầu, bạn cần cài đặt Python 3.10+ và các thư viện cần thiết. Chúng tôi khuyến nghị sử dụng virtual environment để quản lý dependencies.

# Tạo virtual environment và cài đặt dependencies
python -m venv venv_tardis
source venv_tardis/bin/activate  # Linux/Mac

hoặc: venv_tardis\Scripts\activate # Windows

Cài đặt tardis-client - thư viện chính thức của Tardis.dev

pip install tardis-dev

Các thư viện bổ sung cho data analysis

pip install pandas numpy matplotlib jupyter

Thư viện hỗ trợ async programming

pip install asyncio nest-asyncio

Kiểm tra version

python -c "import tardis; print(tardis.__version__)"

Tardis.dev cung cấp API endpoint chính tại https://api.tardis.dev/v1 với các endpoint cho futures data:

Kết nối và truy xuất dữ liệu Orderbook

Để truy xuất dữ liệu orderbook lịch sử từ Binance Futures, chúng ta sử dụng Tardis.dev client với exchange name là binance-futures. Dưới đây là code hoàn chỉnh để kết nối và truy xuất dữ liệu.

import asyncio
from tardis.devices.exchanges.binance_futures import BinanceFuturesDevice
from tardis.interface.ts_dataclasses import BookL1
from datetime import datetime, timedelta
import pandas as pd
from typing import List, Dict

class BinanceFuturesDataFetcher:
    """Class truy xuất dữ liệu orderbook từ Tardis.dev cho Binance Futures"""
    
    def __init__(self, api_key: str):
        """
        Khởi tạo Data Fetcher
        
        Args:
            api_key: API key từ Tardis.dev dashboard
        """
        self.api_key = api_key
        self.device = BinanceFuturesDevice(
            api_key=api_key,
            exchange="binance-futures"
        )
        self.base_url = "https://api.tardis.dev/v1"
        
    async def fetch_orderbook_snapshot(
        self, 
        symbol: str, 
        date: datetime
    ) -> List[BookL1]:
        """
        Lấy một snapshot orderbook tại thời điểm cụ thể
        
        Args:
            symbol: Cặp giao dịch, ví dụ: 'BTCUSDT'
            date: Thời điểm cần lấy dữ liệu
            
        Returns:
            Danh sách BookL1 objects chứa bid/ask levels
        """
        # Format timestamp theo chuẩn ISO 8601
        timestamp = date.strftime("%Y-%m-%dT%H:%M:%S.000Z")
        
        # Call API endpoint
        endpoint = f"{self.base_url}/exchanges/binance-futures/book"
        params = {
            "symbol": symbol,
            "timestamp": timestamp,
            "limit": 100  # Số lượng levels mỗi side
        }
        
        response = await self.device.http_get(endpoint, params=params)
        return response
    
    async def fetch_historical_orderbook(
        self,
        symbol: str,
        start_date: datetime,
        end_date: datetime,
        interval: str = "1m"
    ) -> pd.DataFrame:
        """
        Lấy dữ liệu orderbook trong khoảng thời gian
        
        Args:
            symbol: Cặp giao dịch
            start_date: Thời điểm bắt đầu
            end_date: Thời điểm kết thúc
            interval: Khoảng thời gian giữa các snapshot
            
        Returns:
            DataFrame chứa dữ liệu orderbook
        """
        snapshots = []
        current_date = start_date
        
        # Tính interval delta
        interval_map = {
            "1m": timedelta(minutes=1),
            "5m": timedelta(minutes=5),
            "15m": timedelta(minutes=15),
            "1h": timedelta(hours=1)
        }
        delta = interval_map.get(interval, timedelta(minutes=1))
        
        print(f"Fetching orderbook data for {symbol}...")
        print(f"Period: {start_date} to {end_date}")
        
        while current_date < end_date:
            try:
                snapshot = await self.fetch_orderbook_snapshot(
                    symbol, current_date
                )
                snapshots.append(snapshot)
                
                # Progress indicator
                progress = (current_date - start_date) / (end_date - start_date)
                print(f"Progress: {progress*100:.1f}%", end="\r")
                
                current_date += delta
                
                # Rate limit handling - Tardis.dev cho phép 10 requests/giây
                await asyncio.sleep(0.1)
                
            except Exception as e:
                print(f"\nError at {current_date}: {e}")
                # Retry sau 5 giây
                await asyncio.sleep(5)
                continue
                
        return self._process_snapshots(snapshots)
    
    def _process_snapshots(self, snapshots: List) -> pd.DataFrame:
        """Xử lý raw snapshots thành DataFrame"""
        records = []
        for snapshot in snapshots:
            for bid in snapshot.bids:
                records.append({
                    'timestamp': snapshot.timestamp,
                    'side': 'bid',
                    'price': float(bid.price),
                    'size': float(bid.size),
                    'level': bid.level
                })
            for ask in snapshot.asks:
                records.append({
                    'timestamp': snapshot.timestamp,
                    'side': 'ask',
                    'price': float(ask.price),
                    'size': float(ask.size),
                    'level': ask.level
                })
        
        df = pd.DataFrame(records)
        if not df.empty:
            df['spread'] = df[df['side']=='ask']['price'] - df[df['side']=='bid']['price']
            df['mid_price'] = (df[df['side']=='ask']['price'] + df[df['side']=='bid']['price']) / 2
            df['imbalance'] = self._calculate_imbalance(df)
        return df
    
    def _calculate_imbalance(self, df: pd.DataFrame) -> pd.Series:
        """Tính orderbook imbalance"""
        bids = df[df['side']=='bid'].groupby('timestamp')['size'].sum()
        asks = df[df['side']=='ask'].groupby('timestamp')['size'].sum()
        imbalance = (bids - asks) / (bids + asks)
        return imbalance

Sử dụng example

async def main(): fetcher = BinanceFuturesDataFetcher(api_key="YOUR_TARDIS_API_KEY") # Lấy dữ liệu 1 ngày cho BTCUSDT perpetual df = await fetcher.fetch_historical_orderbook( symbol="BTCUSDT", start_date=datetime(2026, 1, 15, 0, 0, 0), end_date=datetime(2026, 1, 16, 0, 0, 0), interval="1m" ) # Lưu vào CSV để sử dụng sau df.to_csv("btcusdt_orderbook_20260115.csv", index=False) print(f"\nData saved: {len(df)} records") print(df.head()) asyncio.run(main())

Xây dựng chiến lược Backtest với dữ liệu Orderbook

Sau khi có dữ liệu orderbook, bước tiếp theo là xây dựng framework backtest để đánh giá chiến lược giao dịch. Dưới đây là một ví dụ hoàn chỉnh về chiến lược market-making dựa trên orderbook imbalance.

import pandas as pd
import numpy as np
from dataclasses import dataclass
from typing import List, Optional
from datetime import datetime

@dataclass
class Trade:
    """Biểu diễn một giao dịch"""
    timestamp: datetime
    side: str  # 'buy' or 'sell'
    price: float
    size: float
    pnl: float = 0.0
    fees: float = 0.0

@dataclass
class OrderbookLevel:
    """Một level trong orderbook"""
    price: float
    size: float
    
class MarketMakingStrategy:
    """
    Chiến lược Market Making cơ bản
    - Mua khi orderbook imbalance > threshold
    - Bán khi orderbook imbalance < -threshold
    - Spread được tính dựa trên volatility
    """
    
    def __init__(
        self,
        symbol: str,
        base_size: float = 0.001,  # Kích thước lệnh cơ bản (BTC)
        imbalance_threshold: float = 0.3,  # Ngưỡng imbalance
        max_spread_bps: float = 50,  # Spread tối đa (basis points)
        maker_fee: float = 0.0002,  # Phí maker 0.02%
    ):
        self.symbol = symbol
        self.base_size = base_size
        self.imbalance_threshold = imbalance_threshold
        self.max_spread_bps = max_spread_bps
        self.maker_fee = maker_fee
        
        self.position = 0.0
        self.trades: List[Trade] = []
        self.equity_curve = []
        
    def calculate_spread(self, mid_price: float, volatility: float) -> float:
        """
        Tính spread tối ưu dựa trên mid price và volatility
        
        Args:
            mid_price: Giá trung vị hiện tại
            volatility: Độ biến động (tính bằng standard deviation)
            
        Returns:
            Spread tối ưu theo đơn vị price
        """
        # Adaptive spread: cao hơn khi volatility cao
        base_spread = mid_price * (self.max_spread_bps / 10000)
        adaptive_spread = base_spread * (1 + volatility * 10)
        return min(adaptive_spread, mid_price * 0.001)  # Cap at 0.1%
    
    def calculate_order_size(
        self, 
        imbalance: float, 
        base_size: float
    ) -> float:
        """
        Tính kích thước lệnh dựa trên orderbook imbalance
        
        Args:
            imbalance: Giá trị imbalance [-1, 1]
            base_size: Kích thước cơ bản
            
        Returns:
            Kích thước lệnh điều chỉnh
        """
        # Khi imbalance > 0 (nhiều bid hơn), tăng size bán
        # Khi imbalance < 0 (nhiều ask hơn), tăng size mua
        size_multiplier = 1 + abs(imbalance) * 0.5
        return base_size * size_multiplier
    
    def generate_signal(
        self,
        mid_price: float,
        imbalance: float,
        volatility: float
    ) -> Optional[dict]:
        """
        Sinh tín hiệu giao dịch từ dữ liệu orderbook
        
        Args:
            mid_price: Giá trung vị
            imbalance: Orderbook imbalance
            volatility: Độ biến động
            
        Returns:
            Dictionary chứa tín hiệu hoặc None
        """
        spread = self.calculate_spread(mid_price, volatility)
        half_spread = spread / 2
        
        # Kiểm tra ngưỡng imbalance
        if imbalance > self.imbalance_threshold:
            # Imbalance cao -> có áp lực mua -> chúng ta bán
            size = self.calculate_order_size(imbalance, self.base_size)
            return {
                'action': 'sell',
                'price': mid_price + half_spread,
                'size': size,
                'reason': f'High bid imbalance: {imbalance:.3f}'
            }
        elif imbalance < -self.imbalance_threshold:
            # Imbalance thấp -> có áp lực bán -> chúng ta mua
            size = self.calculate_order_size(imbalance, self.base_size)
            return {
                'action': 'buy',
                'price': mid_price - half_spread,
                'size': abs(size),
                'reason': f'High ask imbalance: {imbalance:.3f}'
            }
        
        return None
    
    def execute_trade(
        self, 
        timestamp: datetime,
        signal: dict
    ) -> Trade:
        """Thực thi một giao dịch và cập nhật position"""
        trade = Trade(
            timestamp=timestamp,
            side=signal['action'],
            price=signal['price'],
            size=signal['size'],
            fees=signal['price'] * signal['size'] * self.maker_fee
        )
        
        if signal['action'] == 'buy':
            self.position += signal['size']
            trade.pnl = -signal['size'] * signal['price'] - trade.fees
        else:
            self.position -= signal['size']
            trade.pnl = signal['size'] * signal['price'] - trade.fees
            
        self.trades.append(trade)
        return trade

class BacktestEngine:
    """Engine thực thi backtest"""
    
    def __init__(self, strategy: MarketMakingStrategy, initial_capital: float = 100000):
        self.strategy = strategy
        self.initial_capital = initial_capital
        self.current_capital = initial_capital
        self.equity_curve = []
        
    def run(
        self, 
        orderbook_df: pd.DataFrame,
        volatility_window: int = 60
    ) -> dict:
        """
        Chạy backtest với dữ liệu orderbook
        
        Args:
            orderbook_df: DataFrame chứa dữ liệu orderbook
            volatility_window: Window để tính volatility
            
        Returns:
            Dictionary chứa kết quả backtest
        """
        print(f"Starting backtest for {self.strategy.symbol}")
        print(f"Initial capital: ${self.initial_capital:,.2f}")
        
        # Group by timestamp để xử lý theo snapshot
        grouped = orderbook_df.groupby('timestamp')
        
        results = []
        for timestamp, group in grouped:
            # Tính mid price và imbalance
            bids = group[group['side']=='bid'].sort_values('price', ascending=False)
            asks = group[group['side']=='ask'].sort_values('price')
            
            if bids.empty or asks.empty:
                continue
                
            best_bid = bids.iloc[0]['price']
            best_ask = asks.iloc[0]['price']
            mid_price = (best_bid + best_ask) / 2
            
            # Tính imbalance
            total_bid_size = bids['size'].sum()
            total_ask_size = asks['size'].sum()
            imbalance = (total_bid_size - total_ask_size) / (total_bid_size + total_ask_size)
            
            # Tính volatility từ dữ liệu quá khứ
            volatility = self._calculate_volatility(
                orderbook_df, timestamp, volatility_window
            )
            
            # Sinh tín hiệu
            signal = self.strategy.generate_signal(
                mid_price, imbalance, volatility
            )
            
            if signal:
                trade = self.strategy.execute_trade(timestamp, signal)
                self.current_capital += trade.pnl
                
                results.append({
                    'timestamp': timestamp,
                    **signal,
                    'position': self.strategy.position,
                    'capital': self.current_capital,
                    'pnl': trade.pnl,
                    'fees': trade.fees
                })
            
            # Cập nhật equity curve
            if len(results) > 0:
                position_value = self.strategy.position * mid_price
                self.equity_curve.append({
                    'timestamp': timestamp,
                    'total_equity': self.current_capital + position_value,
                    'cash': self.current_capital,
                    'position_value': position_value
                })
        
        return self._generate_report(results)
    
    def _calculate_volatility(
        self, 
        df: pd.DataFrame, 
        current_time: datetime,
        window: int
    ) -> float:
        """Tính volatility của mid price trong window"""
        historical = df[df['timestamp'] < current_time].tail(window)
        if len(historical) < 2:
            return 0.0
        return historical['mid_price'].std() / historical['mid_price'].mean()
    
    def _generate_report(self, trades: List[dict]) -> dict:
        """Tạo báo cáo kết quả backtest"""
        if not trades:
            return {'error': 'No trades executed'}
        
        trades_df = pd.DataFrame(trades)
        
        total_pnl = self.current_capital - self.initial_capital
        total_return = (total_pnl / self.initial_capital) * 100
        
        # Tính các metrics
        winning_trades = trades_df[trades_df['pnl'] > 0]
        losing_trades = trades_df[trades_df['pnl'] <= 0]
        
        win_rate = len(winning_trades) / len(trades_df) * 100
        
        avg_win = winning_trades['pnl'].mean() if len(winning_trades) > 0 else 0
        avg_loss = abs(losing_trades['pnl'].mean()) if len(losing_trades) > 0 else 0
        profit_factor = abs(winning_trades['pnl'].sum() / losing_trades['pnl'].sum()) if len(losing_trades) > 0 and losing_trades['pnl'].sum() != 0 else float('inf')
        
        # Tính Sharpe Ratio (simplified)
        returns = trades_df['pnl'].pct_change().dropna()
        sharpe = returns.mean() / returns.std() * np.sqrt(252) if returns.std() > 0 else 0
        
        # Max Drawdown
        equity_df = pd.DataFrame(self.equity_curve)
        equity_df['cummax'] = equity_df['total_equity'].cummax()
        equity_df['drawdown'] = (equity_df['total_equity'] - equity_df['cummax']) / equity_df['cummax']
        max_drawdown = equity_df['drawdown'].min() * 100
        
        report = {
            'symbol': self.strategy.symbol,
            'period': f"{trades_df['timestamp'].min()} to {trades_df['timestamp'].max()}",
            'total_trades': len(trades_df),
            'initial_capital': self.initial_capital,
            'final_capital': self.current_capital,
            'total_pnl': total_pnl,
            'total_return_pct': total_return,
            'win_rate': win_rate,
            'avg_win': avg_win,
            'avg_loss': avg_loss,
            'profit_factor': profit_factor,
            'sharpe_ratio': sharpe,
            'max_drawdown_pct': max_drawdown,
            'trades': trades_df
        }
        
        self._print_report(report)
        return report
    
    def _print_report(self, report: dict):
        """In báo cáo ra console"""
        print("\n" + "="*60)
        print("BACKTEST REPORT")
        print("="*60)
        print(f"Symbol: {report['symbol']}")
        print(f"Period: {report['period']}")
        print(f"\nTotal Trades: {report['total_trades']}")
        print(f"\nCapital:")
        print(f"  Initial: ${report['initial_capital']:,.2f}")
        print(f"  Final:   ${report['final_capital']:,.2f}")
        print(f"  P&L:     ${report['total_pnl']:,.2f} ({report['total_return_pct']:.2f}%)")
        print(f"\nPerformance Metrics:")
        print(f"  Win Rate: {report['win_rate']:.2f}%")
        print(f"  Avg Win:  ${report['avg_win']:.4f}")
        print(f"  Avg Loss: ${report['avg_loss']:.4f}")
        print(f"  Profit Factor: {report['profit_factor']:.2f}")
        print(f"  Sharpe Ratio: {report['sharpe_ratio']:.2f}")
        print(f"  Max Drawdown: {report['max_drawdown_pct']:.2f}%")
        print("="*60)

Chạy backtest

async def run_backtest(): # Load dữ liệu orderbook đã lưu df = pd.read_csv("btcusdt_orderbook_20260115.csv") df['timestamp'] = pd.to_datetime(df['timestamp']) # Khởi tạo strategy strategy = MarketMakingStrategy( symbol="BTCUSDT", base_size=0.01, # 0.01 BTC per trade imbalance_threshold=0.35, max_spread_bps=40 ) # Chạy backtest engine = BacktestEngine( strategy=strategy, initial_capital=50000 # $50,000 starting capital ) report = engine.run(df) return report

asyncio.run(run_backtest())

Phân tích kết quả và tối ưu hóa tham số

Sau khi chạy backtest, việc phân tích kết quả và tối ưu hóa tham số là bước quan trọng để cải thiện performance của chiến lược. Dưới đây là code để visualize kết quả và thực hiện parameter optimization.

import matplotlib.pyplot as plt
import seaborn as sns
from itertools import product
import pandas as pd
import numpy as np

class ParameterOptimizer:
    """Tối ưu hóa tham số chiến lược sử dụng grid search"""
    
    def __init__(self, base_strategy_class, orderbook_df: pd.DataFrame):
        self.StrategyClass = base_strategy_class
        self.orderbook_df = orderbook_df
        self.results = []
        
    def optimize(
        self,
        param_grid: dict,
        metric: str = 'total_return_pct'
    ) -> pd.DataFrame:
        """
        Grid search để tìm tham số tối ưu
        
        Args:
            param_grid: Dictionary chứa các tham số cần tối ưu
            metric: Metric để đánh giá
            
        Returns:
            DataFrame chứa kết quả tất cả combinations
        """
        # Generate all combinations
        keys = param_grid.keys()
        values = param_grid.values()
        combinations = [dict(zip(keys, v)) for v in product(*values)]
        
        print(f"Testing {len(combinations)} parameter combinations...")
        
        for i, params in enumerate(combinations):
            print(f"[{i+1}/{len(combinations)}] Testing: {params}")
            
            strategy = self.StrategyClass(
                symbol="BTCUSDT",
                **params
            )
            
            engine = BacktestEngine(
                strategy=strategy,
                initial_capital=50000
            )
            
            report = engine.run(self.orderbook_df)
            report['params'] = str(params)
            
            self.results.append(report)
        
        return self._analyze_results()