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:
- Thu nhập đến từ spread chứ không phải từ việc đoán đúng hướng giá
- Cần volume lớn để tạo ra lợi nhuận đáng kể
- Rủi ro inventory (danh mục) nếu giá di chuyển ngược hướng
- Đòi hỏi tốc độ phản hồi cực nhanh (dưới 50ms)
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:
- Order book (sổ lệnh) chi tiết với độ sâu đầy đủ
- Dữ liệu trade (giao dịch) với độ trễ microsecond
- Hỗ trợ nhiều sàn: Binance, Bybit, OKX, Coinbase...
- Lưu trữ dài hạn lên đến 5 nă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)