Mở Đầu: Kịch Bản Thất Bại Thực Tế
Tôi nhớ rõ buổi tối thứ Sáu cách đây 3 tháng. Đội ngũ trader của chúng tôi đã xây dựng một chiến lược arbitrage bot sử dụng dữ liệu OHLCV 5 phút trong 2 năm qua — tổng cộng 210,240 candle data points trên 15 cặp tiền. Khi chạy backtest lần đầu, script chạy suốt 45 phút rồi crash với lỗi MemoryError. Lần thứ hai, tôi thêm caching nhưng vẫn timeout ở phút 38. Đó là lúc tôi nhận ra: framework backtest mặc định không được thiết kế cho khối lượng dữ liệu crypto thực tế.
Bài viết này là tổng hợp 6 tháng tối ưu hóa hiệu suất backtest từ kinh nghiệm thực chiến của tôi — bao gồm cả việc chuyển đổi sang HolySheep AI cho API calls và giảm chi phí từ $127 xuống còn $18/tháng.
Tại Sao Backtest Crypto Chậm: Phân Tích Nguyên Nhân Gốc
Trước khi tối ưu, cần hiểu rõ bottleneck ở đâu. Trong kiến trúc backtest tiêu chuẩn, có 3 điểm nghẽn chính:
- I/O Bound: Đọc dữ liệu từ SQLite/PostgreSQL với index không tối ưu
- CPU Bound: Tính toán indicator (RSI, MACD, Bollinger Bands) lặp lại nhiều lần
- Memory Bound: Load toàn bộ dataset vào RAM thay vì streaming
Chiến Lược Tối Ưu Hóa Hiệu Suất Framework Backtest
1. Vectorized Operations Với NumPy Thay Vì Loop
Lỗi phổ biến nhất mà tôi thấy là dùng Python for-loop để tính indicator. Với 210K data points, điều này tạo ra ~420K vòng lặp lãng phí. NumPy vectorized operations giảm thời gian tính toán từ 15 phút xuống còn 23 giây.
import numpy as np
import pandas as pd
from numba import jit
❌ SAI: Dùng loop chậm
def calculate_rsi_slow(prices, period=14):
rsi = []
for i in range(len(prices)):
if i < period:
rsi.append(np.nan)
else:
delta = prices[i] - prices[i-1]
gain = delta if delta > 0 else 0
loss = -delta if delta < 0 else 0
avg_gain = np.mean([prices[j] - prices[j-1] for j in range(i-period+1, i+1) if prices[j] > prices[j-1]])
avg_loss = np.mean([prices[j-1] - prices[j] for j in range(i-period+1, i+1) if prices[j] < prices[j-1]])
rs = avg_gain / avg_loss if avg_loss != 0 else 100
rsi.append(100 - (100 / (1 + rs)))
return np.array(rsi)
✅ ĐÚNG: Vectorized với NumPy
def calculate_rsi_vectorized(prices, period=14):
deltas = np.diff(prices, prepend=prices[0])
gains = np.where(deltas > 0, deltas, 0)
losses = np.where(deltas < 0, -deltas, 0)
avg_gains = np.zeros_like(prices)
avg_losses = np.zeros_like(prices)
# EMA-style calculation
avg_gains[period] = np.mean(gains[1:period+1])
avg_losses[period] = np.mean(losses[1:period+1])
for i in range(period + 1, len(prices)):
avg_gains[i] = (avg_gains[i-1] * (period - 1) + gains[i]) / period
avg_losses[i] = (avg_losses[i-1] * (period - 1) + losses[i]) / period
rs = np.divide(avg_gains, avg_losses, where=avg_losses != 0, out=np.full_like(avg_gains, 100))
rsi = 100 - (100 / (1 + rs))
rsi[:period] = np.nan
return rsi
Benchmark: 210,240 candles, BTC/USDT 5m timeframe
prices = np.random.randn(210240).cumsum() + 45000
%timeit calculate_rsi_slow(prices) # ~45.2s
%timeit calculate_rsi_vectorized(prices) # ~0.18s (251x faster)
2. Parquet Storage Thay Vì CSV/SQLite
Đọc 2 năm dữ liệu OHLCV từ CSV mất ~3 phút. Chuyển sang Parquet với columnar compression giảm xuống còn 4 giây — và tiết kiệm 78% disk space.
import pyarrow.parquet as pq
import pandas as pd
from pathlib import Path
class CryptoDataStore:
def __init__(self, data_dir="./crypto_parquet"):
self.data_dir = Path(data_dir)
self.data_dir.mkdir(exist_ok=True)
def save_ohlcv_parquet(self, df: pd.DataFrame, symbol: str, timeframe: str):
"""Lưu với Parquet compression - giảm 78% storage"""
path = self.data_dir / f"{symbol}_{timeframe}.parquet"
# Schema