Là một nhà giao dịch tần suất cao đã dành 3 năm xây dựng hệ thống market making, tôi hiểu rõ cảm giác khi chiến lược của bạn hoạt động hoàn hảo trên giấy nhưng lại thua lỗ nghiêm trọng trên thị trường thật. Bài viết này sẽ hướng dẫn bạn từng bước cách sử dụng dữ liệu lịch sử Tardis để backtest và tối ưu hóa tham số chiến lược market making một cách khoa học, giúp bạn tránh những sai lầm đắt giá nhất.

1. Giới thiệu về Market Making và Backtest

1.1. Market Making là gì?

Market Making (tạo lập thị trường) là chiến lược giao dịch mà nhà tạo lập liên tục đặt lệnh mua và bán một tài sản, hưởng chênh lệch giá (spread) giữa hai phía. Ví dụ, nếu bạn mua cổ phiếu ở mức 100 và bán ngay lập tức ở mức 100.05, bạn đã kiếm được 0.05 đô la chênh lệch.

Đặc điểm quan trọng:

1.2. Tardis là gì?

Tardis là nền tảng cung cấp dữ liệu lịch sử chất lượng cao cho thị trường tiền mã hóa, bao gồm:

2. Thiết lập môi trường từ con số 0

2.1. Cài đặt Python và thư viện cần thiết

Đầu tiên, bạn cần cài đặt Python 3.9 trở lên. Tải tại python.org.

# Tạo môi trường ảo (virtual environment)
python -m venv market_making_env

Kích hoạt môi trường

Windows:

market_making_env\Scripts\activate

macOS/Linux:

source market_making_env/bin/activate

Cài đặt các thư viện cần thiết

pip install pandas numpy matplotlib requests pip install tardis-client # SDK chính thức của Tardis pip install python-dotenv # Quản lý API key

2.2. Cấu trúc thư mục dự án

# Cấu trúc thư mục được khuyến nghị
market_making_project/
├── config/
│   ├── __init__.py
│   └── settings.py          # Cấu hình chung
├── data/
│   ├── raw/                  # Dữ liệu thô từ Tardis
│   └── processed/            # Dữ liệu đã xử lý
├── strategies/
│   ├── __init__.py
│   ├── base_strategy.py      # Lớp chiến lược cơ sở
│   └── simple_mm.py          # Chiến lược market making đơn giản
├── backtest/
│   ├── __init__.py
│   ├── engine.py             # Engine backtest
│   └── optimizer.py          # Bộ tối ưu hóa tham số
├── notebooks/
│   └── analysis.ipynb        # Phân tích trong Jupyter
├── logs/                     # File log
├── .env                      # API keys (KHÔNG commit lên git)
├── main.py                   # Entry point
└── requirements.txt

3. Kết nối Tardis API và tải dữ liệu

3.1. Lấy API key từ Tardis

Đăng ký tài khoản tại tardis.dev và lấy API key từ dashboard. Tardis cung cấp gói free với 100,000 message/ngày.

3.2. Tải dữ liệu order book lịch sử

# config/settings.py
import os
from dotenv import load_dotenv

load_dotenv()

Cấu hình Tardis

TARDIS_API_KEY = os.getenv("TARDIS_API_KEY") TARDIS_BASE_URL = "https://api.tardis.dev/v1"

Cấu hình HolySheep AI cho phân tích dữ liệu

HOLYSHEEP_API_KEY = os.getenv("HOLYSHEEP_API_KEY") HOLYSHEEP_BASE_URL = "https://api.holysheep.ai/v1"

Thông số backtest

BACKTEST_CONFIG = { "exchange": "binance", "symbol": "BTC-USDT", "start_date": "2024-01-01", "end_date": "2024-03-31", "timeframe": "1m" }

Thông số chiến lược mặc định

DEFAULT_STRATEGY_PARAMS = { "spread_bps": 5, # Spread 5 basis points (0.05%) "order_size": 0.001, # Kích thước mỗi lệnh (BTC) "inventory_target": 0, # Mục tiêu inventory trung tính "max_position": 0.1, # Vị thế tối đa cho phép "rebalance_threshold": 0.05, # Ngưỡng cân bằng lại }
# scripts/fetch_tardis_data.py
import os
import requests
import pandas as pd
from datetime import datetime, timedelta
import time

def fetch_orderbook_snapshot(exchange, symbol, timestamp, api_key):
    """
    Tải một snapshot order book tại thời điểm cụ thể.
    
    Args:
        exchange: Tên sàn (vd: 'binance')
        symbol: Cặp giao dịch (vd: 'btcusdt')
        timestamp: Unix timestamp (miligiây)
        api_key: Tardis API key
    
    Returns:
        DataFrame chứa order book
    """
    url = f"https://api.tardis.dev/v1/orderbook_snapshots"
    
    params = {
        "exchange": exchange,
        "symbol": symbol,
        "timestamp": timestamp,
        "limit": 500  # Lấy 500 level mỗi bên
    }
    
    headers = {
        "Authorization": f"Bearer {api_key}"
    }
    
    response = requests.get(url, params=params, headers=headers)
    
    if response.status_code == 200:
        data = response.json()
        return parse_orderbook_data(data)
    else:
        print(f"Lỗi {response.status_code}: {response.text}")
        return None

def parse_orderbook_data(data):
    """
    Parse dữ liệu order book từ Tardis thành DataFrame.
    """
    bids = pd.DataFrame(data.get('bids', []), columns=['price', 'quantity'])
    asks = pd.DataFrame(data.get('asks', []), columns=['price', 'quantity'])
    
    bids['side'] = 'bid'
    asks['side'] = 'ask'
    
    combined = pd.concat([bids, asks], ignore_index=True)
    combined['price'] = combined['price'].astype(float)
    combined['quantity'] = combined['quantity'].astype(float)
    
    return combined

def fetch_trades(exchange, symbol, start_time, end_time, api_key):
    """
    Tải dữ liệu trades trong khoảng thời gian.
    """
    url = f"https://api.tardis.dev/v1/trades"
    
    params = {
        "exchange": exchange,
        "symbol": symbol,
        "from": start_time,
        "to": end_time,
        "limit": 10000
    }
    
    headers = {
        "Authorization": f"Bearer {api_key}"
    }
    
    all_trades = []
    page = 1
    
    while True:
        params['page'] = page
        response = requests.get(url, params=params, headers=headers)
        
        if response.status_code == 200:
            trades = response.json()
            if not trades:
                break
            all_trades.extend(trades)
            
            # Rate limit: 10 request/giây
            time.sleep(0.1)
            page += 1
        else:
            print(f"Lỗi: {response.status_code}")
            break
    
    df = pd.DataFrame(all_trades)
    if not df.empty:
        df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
        df['price'] = df['price'].astype(float)
        df['quantity'] = df['quantity'].astype(float)
    
    return df

Ví dụ sử dụng

if __name__ == "__main__": from dotenv import load_dotenv load_dotenv() api_key = os.getenv("TARDIS_API_KEY") # Tải trades của BTC-USDT trong 1 giờ end_time = int(datetime.now().timestamp() * 1000) start_time = int((datetime.now() - timedelta(hours=1)).timestamp() * 1000) trades = fetch_trades( exchange="binance", symbol="btcusdt", start_time=start_time, end_time=end_time, api_key=api_key ) print(f"Đã tải {len(trades)} giao dịch") print(trades.head())

4. Xây dựng Engine Backtest cho Market Making

4.1. Lớp chiến lược cơ sở

# strategies/base_strategy.py
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Dict, List, Optional
import pandas as pd
import numpy as np

@dataclass
class Order:
    """Đại diện cho một lệnh trong backtest."""
    order_id: int
    side: str  # 'bid' hoặc 'ask'
    price: float
    quantity: float
    timestamp: pd.Timestamp
    filled: bool = False
    fill_price: Optional[float] = None
    fill_time: Optional[pd.Timestamp] = None

@dataclass
class Trade:
    """Đại diện cho một giao dịch thị trường."""
    timestamp: pd.Timestamp
    price: float
    quantity: float
    side: str
    aggressor: str  # 'buy' hoặc 'sell'

@dataclass
class Position:
    """Theo dõi vị thế hiện tại."""
    quantity: float  # Số dương = long, âm = short
    avg_entry: float
    unrealized_pnl: float = 0.0
    realized_pnl: float = 0.0

class BaseStrategy(ABC):
    """
    Lớp cơ sở cho tất cả chiến lược market making.
    Định nghĩa interface chuẩn để backtest engine tương tác.
    """
    
    def __init__(self, params: Dict):
        self.params = params
        self.orders: List[Order] = []
        self.position = Position(quantity=0, avg_entry=0)
        self.order_id_counter = 0
        
    @abstractmethod
    def on_trade(self, trade: Trade, orderbook: pd.DataFrame) -> List[Order]:
        """
        Xử lý mỗi trade xảy ra.
        Trả về danh sách lệnh mới cần đặt.
        """
        pass
    
    @abstractmethod
    def calculate_spread(self, mid_price: float, volatility: float) -> float:
        """
        Tính toán spread dựa trên tham số và điều kiện thị trường.
        """
        pass
    
    def update_position(self, trade: Trade):
        """Cập nhật vị thế khi có lệnh khớp."""
        if not self.orders:
            return
            
        for order in self.orders:
            if order.filled:
                continue
                
            # Kiểm tra nếu trade khớp với lệnh của chúng ta
            if trade.side == 'buy' and order.side == 'ask' and trade.price >= order.price:
                self._fill_order(order, trade)
            elif trade.side == 'sell' and order.side == 'bid' and trade.price <= order.price:
                self._fill_order(order, trade)
    
    def _fill_order(self, order: Order, trade: Trade):
        """Xử lý khớp lệnh."""
        order.filled = True
        order.fill_price = trade.price
        order.fill_time = trade.timestamp
        
        if order.side == 'ask':
            # Bán: tăng position (vì chúng ta đang short ask)
            self.position.quantity += order.quantity
        else:
            # Mua: giảm position (vì chúng ta đang long bid)
            self.position.quantity -= order.quantity
            
    def cancel_order(self, order_id: int):
        """Hủy lệnh chưa khớp."""
        for order in self.orders:
            if order.order_id == order_id and not order.filled:
                self.orders.remove(order)
                break
    
    def get_metrics(self) -> Dict:
        """Trả về metrics hiệu tại của chiến lược."""
        return {
            'position': self.position.quantity,
            'realized_pnl': self.position.realized_pnl,
            'unrealized_pnl': self.position.unrealized_pnl,
            'open_orders': len([o for o in self.orders if not o.filled])
        }

4.2. Chiến lược Market Making đơn giản

# strategies/simple_mm.py
import pandas as pd
import numpy as np
from strategies.base_strategy import BaseStrategy, Order, Trade

class SimpleMarketMaker(BaseStrategy):
    """
    Chiến lược market making cơ bản:
    - Đặt lệnh bid và ask cách mid price một spread cố định
    - Tự động cân bằng inventory về target
    """
    
    def __init__(self, params: dict):
        super().__init__(params)
        
        # Các tham số từ config
        self.spread_bps = params.get('spread_bps', 5)  # Spread tính bằng basis points
        self.order_size = params.get('order_size', 0.001)
        self.inventory_target = params.get('inventory_target', 0)
        self.max_position = params.get('max_position', 0.1)
        self.rebalance_threshold = params.get('rebalance_threshold', 0.02)
        
        # State
        self.last_mid_price = None
        self.inventory_history = []
        
    def on_trade(self, trade: Trade, orderbook: pd.DataFrame) -> list:
        """Xử lý mỗi trade và quyết định lệnh mới."""
        new_orders = []
        
        # Tính mid price từ orderbook
        if orderbook.empty:
            mid_price = trade.price
        else:
            best_bid = orderbook[orderbook['side']=='bid']['price'].max()
            best_ask = orderbook[orderbook['side']=='ask']['price'].min()
            mid_price = (best_bid + best_ask) / 2
        
        self.last_mid_price = mid_price
        
        # Tính spread trong đơn vị giá
        spread = mid_price * (self.spread_bps / 10000)
        
        # Xác định bid và ask price
        bid_price = mid_price - spread / 2
        ask_price = mid_price + spread / 2
        
        # Kiểm tra giới hạn position
        current_pos = self.position.quantity
        
        # Điều chỉnh size dựa trên inventory
        adjusted_size = self._calculate_order_size(current_pos)
        
        # Đặt lệnh bid (mua) nếu không vượt max position
        if current_pos < self.max_position and adjusted_size > 0:
            bid_order = Order(
                order_id=self._next_order_id(),
                side='bid',
                price=bid_price,
                quantity=adjusted_size,
                timestamp=trade.timestamp
            )
            new_orders.append(bid_order)
        
        # Đặt lệnh ask (bán) nếu có position để bán
        if current_pos > -self.max_position and adjusted_size > 0:
            ask_order = Order(
                order_id=self._next_order_id(),
                side='ask',
                price=ask_price,
                quantity=adjusted_size,
                timestamp=trade.timestamp
            )
            new_orders.append(ask_order)
        
        # Cập nhật orders
        self.orders.extend(new_orders)
        
        # Ghi log inventory
        self.inventory_history.append({
            'timestamp': trade.timestamp,
            'position': current_pos,
            'mid_price': mid_price
        })
        
        # Cân bằng lại nếu cần
        if abs(current_pos - self.inventory_target) > self.rebalance_threshold:
            self._rebalance(current_pos, mid_price)
        
        return new_orders
    
    def calculate_spread(self, mid_price: float, volatility: float) -> float:
        """Tính spread dựa trên volatility (cải tiến cho phiên bản nâng cao)."""
        # Spread tối thiểu
        min_spread = mid_price * (self.spread_bps / 10000)
        
        # Spread dựa trên volatility (Kelly criterion đơn giản)
        vol_adjustment = mid_price * volatility * 0.5
        
        return max(min_spread, vol_adjustment)
    
    def _calculate_order_size(self, current_position: float) -> float:
        """
        Tính kích thước lệnh dựa trên vị thế hiện tại.
        Giảm size khi position nghiêng về một phía.
        """
        base_size = self.order_size
        
        # Giảm 50% size nếu position vượt 50% max
        if abs(current_position) > self.max_position * 0.5:
            base_size *= 0.5
            
        # Giảm 75% size nếu position vượt 75% max
        if abs(current_position) > self.max_position * 0.75:
            base_size *= 0.25
            
        return base_size
    
    def _rebalance(self, current_position: float, mid_price: float):
        """Cân bằng lại position về target."""
        diff = self.inventory_target - current_position
        
        if diff > 0:
            # Cần mua thêm (giảm short)
            # Đặt lệnh limit buy cao hơn để cân bằng
            rebalance_order = Order(
                order_id=self._next_order_id(),
                side='bid',
                price=mid_price * 1.001,  # Mua hơi cao hơn để khớp nhanh
                quantity=min(diff, self.order_size * 2),
                timestamp=pd.Timestamp.now()
            )
            self.orders.append(rebalance_order)
        else:
            # Cần bán bớt (giảm long)
            rebalance_order = Order(
                order_id=self._next_order_id(),
                side='ask',
                price=mid_price * 0.999,
                quantity=min(abs(diff), self.order_size * 2),
                timestamp=pd.Timestamp.now()
            )
            self.orders.append(rebalance_order)
    
    def _next_order_id(self) -> int:
        self.order_id_counter += 1
        return self.order_id_counter

5. Xây dựng Backtest Engine hoàn chỉnh

# backtest/engine.py
import pandas as pd
import numpy as np
from typing import List, Dict, Callable
from datetime import datetime
from strategies.base_strategy import BaseStrategy, Trade
from dataclasses import dataclass, field

@dataclass
class BacktestResult:
    """Kết quả backtest chi tiết."""
    total_pnl: float = 0
    realized_pnl: float = 0
    unrealized_pnl: float = 0
    total_trades: int = 0
    winning_trades: int = 0
    losing_trades: int = 0
    max_drawdown: float = 0
    sharpe_ratio: float = 0
    avg_spread_captured: float = 0
    inventory_history: List = field(default_factory=list)
    trade_log: List = field(default_factory=list)
    
    def to_dict(self) -> Dict:
        return {
            'total_pnl': self.total_pnl,
            'realized_pnl': self.realized_pnl,
            'unrealized_pnl': self.unrealized_pnl,
            'total_trades': self.total_trades,
            'win_rate': self.winning_trades / max(self.total_trades, 1),
            'max_drawdown': self.max_drawdown,
            'sharpe_ratio': self.sharpe_ratio,
            'avg_spread_captured': self.avg_spread_captured
        }

class BacktestEngine:
    """
    Engine backtest cho chiến lược market making.
    Mô phỏng thực thi lệnh với độ trễ và slippage.
    """
    
    def __init__(self, 
                 trades_df: pd.DataFrame,
                 orderbook_snapshots: List[pd.DataFrame] = None,
                 latency_ms: int = 50,
                 slippage_bps: float = 1.0):
        """
        Khởi tạo backtest engine.
        
        Args:
            trades_df: DataFrame chứa dữ liệu trades
            orderbook_snapshots: Danh sách snapshot order book
            latency_ms: Độ trễ mô phỏng khi đặt lệnh (miligiây)
            slippage_bps: Slippage dự kiến (basis points)
        """
        self.trades = trades_df.copy()
        self.orderbook_snapshots = orderbook_snapshots or []
        self.latency_ms = latency_ms
        self.slippage_bps = slippage_bps
        
        # State
        self.current_time = None
        self.strategy: BaseStrategy = None
        
    def run(self, strategy: BaseStrategy) -> BacktestResult:
        """
        Chạy backtest với chiến lược được cung cấp.
        """
        self.strategy = strategy
        result = BacktestResult()
        
        # Sắp xếp trades theo timestamp
        self.trades = self.trades.sort_values('timestamp').reset_index(drop=True)
        
        equity_curve = []
        peak_equity = 0
        
        print(f"Bắt đầu backtest với {len(self.trades)} trades...")
        
        for idx, row in self.trades.iterrows():
            trade = Trade(
                timestamp=pd.Timestamp(row['timestamp']),
                price=float(row['price']),
                quantity=float(row['quantity']),
                side=row['side'],
                aggressor=row.get('aggressor', 'unknown')
            )
            
            self.current_time = trade.timestamp
            
            # Lấy orderbook snapshot gần nhất
            orderbook = self._get_orderbook_at(trade.timestamp)
            
            # Xử lý trade với chiến lược
            new_orders = strategy.on_trade(trade, orderbook)
            
            # Kiểm tra khớp lệnh với trade này
            strategy.update_position(trade)
            
            # Tính PnL
            metrics = strategy.get_metrics()
            
            # Cập nhật unrealized PnL
            if self.current_time and self.strategy.last_mid_price:
                position = metrics['position']
                # Giả định position được đánh giá theo mid price
                metrics['unrealized_pnl'] = position * (trade.price - 
                    (self.strategy.last_mid_price or trade.price))
            
            # Cập nhật equity
            current_equity = metrics['realized_pnl'] + metrics['unrealized_pnl']
            equity_curve.append({
                'timestamp': trade.timestamp,
                'equity': current_equity,
                'position': metrics['position']
            })
            
            # Tracking max drawdown
            peak_equity = max(peak_equity, current_equity)
            drawdown = (peak_equity - current_equity) / max(peak_equity, 1)
            result.max_drawdown = max(result.max_drawdown, drawdown)
            
            # Progress
            if idx % 10000 == 0:
                print(f"  Đã xử lý {idx}/{len(self.trades)} trades...")
        
        # Tổng hợp kết quả
        result.total_pnl = equity_curve[-1]['equity'] if equity_curve else 0
        result.realized_pnl = metrics.get('realized_pnl', 0)
        result.unrealized_pnl = metrics.get('unrealized_pnl', 0)
        result.total_trades = len(self.trades)
        result.inventory_history = equity_curve
        
        # Tính Sharpe Ratio (giả định 252 trading days)
        if len(equity_curve) > 1:
            returns = pd.Series([e['equity'] for e in equity_curve]).pct_change().dropna()
            if returns.std() > 0:
                result.sharpe_ratio = returns.mean() / returns.std() * np.sqrt(252)
        
        # Tính spread đã capture trung bình
        filled_orders = [o for o in strategy.orders if o.filled]
        if filled_orders:
            spreads = []
            for o in filled_orders:
                if o.fill_price and strategy.last_mid_price:
                    spread_pct = abs(o.fill_price - strategy.last_mid_price) / strategy.last_mid_price
                    spreads.append(spread_pct)
            result.avg_spread_captured = np.mean(spreads) * 10000 if spreads else 0  # in bps
        
        print(f"Backtest hoàn tất. PnL: {result.total_pnl:.4f}")
        
        return result
    
    def _get_orderbook_at(self, timestamp: pd.Timestamp) -> pd.DataFrame:
        """Lấy orderbook snapshot gần nhất tại thời điểm timestamp."""
        # Trong thực tế, bạn sẽ interpolate hoặc lấy snapshot gần nhất
        # Đây là simplified version
        if self.orderbook_snapshots:
            for snapshot in reversed(self.orderbook_snapshots):
                if pd.Timestamp(snapshot['timestamp'].iloc[0]) <= timestamp:
                    return snapshot
        return pd.DataFrame()

def optimize_parameters(trades_df: pd.DataFrame, 
                       param_grid: Dict) -> pd.DataFrame:
    """
    Tối ưu hóa tham số bằng grid search.
    
    Args:
        trades_df: Dữ liệu trades
        param_grid: Dictionary chứa các giá trị cần thử
                   vd: {'spread_bps': [3, 5, 7, 10], 'order_size': [0.001, 0.002]}
    
    Returns:
        DataFrame chứa kết quả của tất cả combinations
    """
    from itertools import product
    
    # Tạo tất cả combinations
    keys = param_grid.keys()
    values = param_grid.values()
    combinations = list(product(*values))
    
    results = []
    
    print(f"Bắt đầu tối ưu hóa với {len(combinations)} combinations...")
    
    for i, combo in enumerate(combinations):
        params = dict(zip(keys, combo))
        
        # Khởi tạo chiến lược với params
        strategy = SimpleMarketMaker(params)
        
        # Chạy backtest
        engine = BacktestEngine(trades_df)
        result = engine.run(strategy)
        
        # Lưu kết quả
        result_dict = result.to_dict()
        result_dict.update(params)
        results.append(result_dict)
        
        print(f"  [{i+1}/{len(combinations)}] {params} -> PnL: {result.total_pnl:.4f}")
    
    return pd.DataFrame(results)

6. Chạy Backtest và