Cùng một chiến lược, cùng một bộ dữ liệu, chỉ đổi phần mềm backtest thì kết quả có giống nhau không?
Cùng một chiến lược, cùng một bộ dữ liệu
Hi ae, sau khi tìm hiểu về lỗi dữ liệu tương lai, phí giao dịch và overfitting, mình từng nghĩ rằng chỉ cần mô tả strategy đủ rõ rồi chọn một thư viện backtest phổ biến là có thể tin vào kết quả. Nhưng gần đây mình đọc được một nghiên cứu khá thú vị về một loại rủi ro ít được nhắc đến: implementation risk, tức rủi ro phát sinh ngay từ phần mềm và cách code backtest được triển khai.
Hiểu đơn giản, hai người có thể dùng cùng một chiến lược, cùng dữ liệu giá, cùng mức phí và cùng thời gian kiểm tra, nhưng nếu chạy bằng hai backtest engine khác nhau thì vẫn có khả năng nhận được equity curve, lợi nhuận và drawdown khác nhau. Nguyên nhân không nhất thiết nằm ở strategy, mà có thể nằm ở cách mỗi engine xử lý phí, thời điểm khớp lệnh, thứ tự mua bán, lượng tiền mặt còn lại và cách ghi nhận giá trị danh mục.
Một nghiên cứu công bố tháng 3/2026 đã kiểm tra vấn đề này trên 15 chiến lược thuộc 5 nhóm, từ buy-and-hold, equal weight, inverse volatility, momentum, machine learning cho đến các chiến lược rotation có turnover cao. Các chiến lược được chạy qua một bản triển khai tham chiếu và bốn engine mã nguồn mở, trên 30 nhóm tài sản không trùng nhau được lấy từ 180 cổ phiếu thuộc S&P 500 trong giai đoạn 2020–2024. Nhóm tác giả cố định strategy, dữ liệu và mức phí; thứ duy nhất thay đổi là engine thực hiện backtest.
Khi đặt chi phí giao dịch bằng 0, cả năm engine cho ra equity curve giống hệt nhau. Nhưng khi thêm chi phí, kết quả bắt đầu lệch. Với các chiến lược đơn giản, độ lệch tối đa giữa các engine chỉ khoảng 0,18%. Nhóm signal và machine learning có thể lệch khoảng 0,28–0,49%. Riêng các chiến lược rotation thay gần như toàn bộ danh mục thường xuyên, độ lệch lớn nhất lên tới khoảng 3,71%. Điều này cho thấy strategy càng giao dịch nhiều thì một khác biệt nhỏ trong cách engine tính phí hoặc xử lý lệnh càng bị tích lũy lớn theo thời gian.
Có một chi tiết mình thấy rất thực tế liên quan đến Backtrader. Theo phân tích của nhóm tác giả, nếu người dùng nhập:
commission = 0.0018
với ý định áp dụng mức phí 0,18%, tức 18 basis points, thì một cấu hình mặc định của Backtrader có thể tiếp tục chia con số đó cho 100. Kết quả là engine chỉ tính khoảng 0,0018%, tương đương 0,18 basis points, thấp hơn mức người dùng muốn tới 100 lần. Muốn tránh tình trạng này phải cấu hình rõ percabs=True. Code vẫn chạy, biểu đồ vẫn đẹp và không hề báo lỗi, nhưng lợi nhuận ròng có thể bị thổi phồng chỉ vì phí được tính sai.
Đây cũng là lý do mình nghĩ không nên chỉ nhìn vào dòng code khai báo phí mà phải kiểm tra số tiền bị trừ sau một giao dịch cụ thể. Ví dụ, mua 100 triệu đồng cổ phiếu với mức phí 0,15% thì chi phí phải là:
Phí mua = 100.000.000 × 0,15% = 150.000 đồng
Nếu log giao dịch chỉ trừ 1.500 đồng thì chương trình có thể vẫn chạy hoàn toàn bình thường, nhưng toàn bộ kết quả phía sau đã không còn đúng. Một unit test rất đơn giản có thể bắt lỗi này:
notional = 100_000_000
fee_rate = 0.0015
expected_fee = 150_000
calculated_fee = notional * fee_rate
assert calculated_fee == expected_fee
Với chiến lược giao dịch nhiều, phần lợi nhuận ròng có thể được biểu diễn tối thiểu như sau:
Lợi nhuận ròng
= Lợi nhuận gộp
− Turnover × Chi phí mỗi đơn vị giao dịch
Giả sử strategy tạo ra lợi nhuận gộp 18% một năm, turnover bằng 1.500% và chi phí mỗi chiều là 0,15%, phần chi phí ước tính đã là:
Chi phí = 1.500% × 0,15% = 2,25%/năm
Nếu engine chỉ tính phí thấp hơn thực tế 100 lần, nó có thể ghi nhận chi phí 0,0225% thay vì 2,25%. Lúc đó backtest sẽ báo lợi nhuận gần 18%, trong khi kết quả đúng chỉ còn khoảng 15,75%, chưa tính spread, slippage, thuế và market impact.
Nhưng phí chưa phải lỗi duy nhất. Nghiên cứu còn phát hiện trường hợp engine xử lý lệnh mua trước lệnh bán khi tái cân bằng danh mục. Ví dụ, mình đang giữ cổ phiếu A và muốn bán A để lấy tiền mua B. Nếu engine kiểm tra lệnh mua B trước khi tiền bán A được ghi nhận, nó có thể từ chối lệnh mua vì thiếu tiền mặt. Cùng một target portfolio, engine xử lý “bán trước, mua sau” sẽ tạo được danh mục, còn engine xử lý theo thứ tự khác có thể tạo ra kết quả hoàn toàn khác.
Một phương pháp mình thấy khá hữu ích là chạy zero-cost ablation test. Trước tiên đặt toàn bộ phí và slippage bằng 0 rồi chạy cùng strategy trên hai engine. Nếu equity curve vẫn khác nhau thì vấn đề nhiều khả năng nằm ở timing, signal, khớp lệnh hoặc cách tính lợi nhuận. Nếu khi phí bằng 0 hai kết quả giống nhau, nhưng vừa thêm phí đã bắt đầu lệch, mình có thể tập trung kiểm tra cost model.
result_a = run_engine_a(
data=data,
fee=0,
slippage=0
)
result_b = run_engine_b(
data=data,
fee=0,
slippage=0
)
assert np.allclose(
result_a["equity"],
result_b["equity"],
rtol=1e-10
)
Sau đó mới bật chi phí và kiểm tra từng thành phần:
Giá trị sau giao dịch
= Giá trị trước giao dịch
− Giá trị lệnh
− Commission
− Slippage
− Thuế và các chi phí khác
Nhóm nghiên cứu khuyến nghị với các strategy turnover cao hoặc nhạy cảm với chi phí nên có ít nhất hai cách triển khai độc lập, chẳng hạn một engine event-driven và một engine vectorized. Quan trọng hơn, không chỉ so sánh Sharpe hay CAGR cuối cùng mà phải so cả ngày phát sinh tín hiệu, ngày khớp lệnh, số lượng cổ phiếu, giá khớp, phí từng lệnh và giá trị danh mục sau mỗi phiên. Trong trường hợp xấu nhất của nghiên cứu, mức độ không chắc chắn do cách triển khai được tác giả quy đổi thành khoảng 37 triệu USD mỗi năm trên danh mục 1 tỷ USD.
Mình rút ra là backtest có ít nhất ba lớp rủi ro khác nhau. Lớp thứ nhất là strategy không có edge. Lớp thứ hai là overfitting, tức strategy chỉ đẹp trên dữ liệu quá khứ. Lớp thứ ba là implementation risk: ý tưởng có thể đúng, dữ liệu có thể sạch nhưng phần mềm mô phỏng lại thực hiện không đúng điều mình nghĩ.
Vì vậy, lần sau khi nhìn thấy một strategy có Sharpe 2 hay CAGR 30%, ngoài việc hỏi dùng dữ liệu nào và test ngoài mẫu chưa, có lẽ mình nên hỏi thêm: phí được tính trên giá trị giao dịch hay toàn bộ danh mục, tín hiệu khớp ở thời điểm nào, lệnh bán và mua được xử lý theo thứ tự nào, có log từng giao dịch không, đã thử chạy bằng một engine thứ hai chưa và có unit test cho các tình huống đơn giản hay không.
AI có thể viết một hệ thống backtest trong vài phút, nhưng chính vì code được tạo ra quá nhanh nên phần kiểm định implementation càng quan trọng. Một equity curve đẹp chưa chắc chứng minh strategy tốt. Đôi khi nó chỉ chứng minh rằng engine đã tính phí “rất có lợi” cho người viết backtest.
