← Quay lại danh sách bài viết
July 2, 2026
5 phút đọc

Thuế Framework: Khi Thư Viện Backtest Của Bạn Chậm Hơn Cả Một Vòng Lặp Pandas Ngây Thơ

Thuế Framework: Khi Thư Viện Backtest Của Bạn Chậm Hơn Cả Một Vòng Lặp Pandas Ngây Thơ
#algotrading
#backtest
#hiệu năng
#tìm kiếm tham số
#vector hóa
#benchmark
Part 2 of 10 · Collection
High-Performance Backtest Engines

Một phần của loạt bài "Backtests Without Illusions".

Có một giả định dễ chịu nằm ở nền tảng của hầu hết các dự án algo-trading: rằng một thư viện backtesting trưởng thành, được nhiều sao trên GitHub, hẳn nhiên là nhanh. Nó có nhiều năm đóng góp, một event loop thực thụ, một mô hình broker, một cơ chế tính phí hoa hồng. Chắc chắn nó phải vượt qua vòng lặp pandas cẩu thả mà bạn tự viết ra. Vậy nên bạn dùng nó, kết nối chiến lược của mình, và bắt đầu một parameter sweep — vài nghìn cấu hình, một tác vụ chạy qua đêm. Sáng hôm sau quay lại, nó vẫn đang chạy.

Chúng tôi đã đo hiệu năng tám engine backtest trên cùng một parameter sweep giống hệt nhau và phát hiện ra điều đáng lẽ phải thay đổi cách bạn chọn công cụ cho việc tìm kiếm. Hai trong số các framework event-driven mã nguồn mở phổ biến nhất — backtraderbt — chạy sweep này chậm hơn cả một vòng lặp pandas ngây thơ mà chúng tôi viết ra chỉ để làm baseline dùng-rồi-bỏ. Không phải chậm hơn chút ít. backtrader mất thời gian gấp khoảng 2,5 lần so với baseline pandas; bt khoảng 4,7 lần. Trong khi đó, một engine vectorized/compiled trên cùng khối lượng công việc chạy nhanh hơn bt khoảng 13.000 lần.

Đây chính là thuế framework. Các backtester phổ biến được xây dựng cho một lần chạy nghiêm túc — một chiến lược, một tập dữ liệu, khớp lệnh cẩn thận, một broker hành xử đúng như một broker. Đó chính xác là những gì bạn cần cho một lần xác nhận cuối cùng hay kiểm tra tính tương đồng với giao dịch thực (live-parity). Và đó cũng chính xác là công cụ sai cho việc mà nghiên cứu algo thực sự dành phần lớn thời gian vào: chạy cùng một chiến lược mười nghìn lần với các tham số hơi khác nhau. Bài viết này đo lường khoản thuế đó, giải thích cơ chế của nó, và đưa ra một quy tắc quyết định về khi nào một framework backtesting "thực thụ" là lựa chọn sai.

Mọi con số ở đây đều đến từ một bộ khai thác (harness) có thể tái tạo được (benchmarks/bench_oss_engines.py, commit 250dbb5) trên một khối lượng công việc giống hệt nhau, được khóa chặt về tính tương đương. Ở đâu chúng tôi không tự chạy một engine, chúng tôi nói rõ điều đó và đặt nó vào một mục riêng về sự trung thực thay vì bịa ra một con số.

Một lần chạy so với mười nghìn lần

Cùng một phép tính chiến lược chảy xuống theo ba con đường phân kỳ: một tuyến event-driven màu đỏ-cam bị buộc phải dừng lại ở mỗi trạm-bar, một cây weights/rebalance gồm các node algo màu hổ phách, và một chùm tia vectorized màu ngọc lục bảo-xanh lam duy nhất giải quyết toàn bộ chuỗi cùng lúc — mô hình kiến trúc chính là định mệnh cho tốc độ sweep

Sự thật quyết định của việc tìm kiếm tham số là engine chạy hàng nghìn lần nhưng phân tích chỉ diễn ra một lần. Bất kể chi phí cố định nào bạn phải trả để thiết lập một backtest — dựng event loop, khởi tạo một broker, cấp phát một đối tượng cho mỗi bar — bạn đều phải trả nó ở mỗi tổ hợp. Một chi phí vô hình trong một lần chạy ("ai quan tâm đến 6 giây?") trở thành toàn bộ hóa đơn trên cả một sweep ("6 giây × 10.000 = 16,6 giờ").

Các engine backtest rơi vào ba mô hình kiến trúc, và mô hình kiến trúc chính là định mệnh cho hiệu năng sweep:

  1. Event-driven — engine đi qua từng bar một, phát ra các sự kiện, gọi callback next()/onBars() của bạn, định tuyến lệnh qua một đối tượng broker. Đây là kiến trúc của backtrader, backtesting.py, PyAlgoTrade, zipline, và nautilus_trader. Nó phản ánh đúng cách giao dịch thực (live trading) hoạt động, chính vì thế nó được tin tưởng về tính chân thực — và cũng chính vì thế mà nó chậm: chi phí phụ trội Python theo từng bar phải trả 150.000 lần cho mỗi tổ hợp.
  2. Weights/rebalancebt nằm ở đây. Bạn đưa cho nó một ma trận trọng số mục tiêu và nó tái cân bằng vào những ngày bạn chỉ định. Không có callback theo từng bar, nhưng vẫn có một đồ thị đối tượng theo từng sự kiện (một cây algo, một sổ cái giao dịch) được đánh giá trong Python.
  3. Vectorized / compiled — toàn bộ chiến lược được biểu diễn dưới dạng các phép toán mảng (vectorbt), một kernel biên dịch JIT (numba), hoặc mã native (Rust, một kernel GPU MLX). Không hề có Python theo từng bar. Vòng lặp, nếu có, chạy ở tốc độ máy.

Phần còn lại của bài viết này là hệ quả thực nghiệm của phân loại đó. Chúng tôi đã xây dựng một khối lượng công việc, buộc mọi engine thực hiện cùng một công việc đã được chứng minh là giống hệt nhau, và đo thời gian.

Khối lượng công việc: một chiến lược, tám mươi tham số, giống hệt cho mọi engine

Một benchmark chỉ trung thực nếu mọi engine đều làm cùng một việc. Benchmark của chúng tôi cố tình đơn giản — một khối lượng công việc mà điều duy nhất khác nhau giữa các engine chính là bản thân engine đó.

  • Dữ liệu. Một chuỗi giá đóng cửa tổng hợp theo chuyển động Brown hình học (geometric-Brownian-motion) duy nhất: 150.000 bar, seed=42, độ biến động theo từng bar sigma=0.0008, x0=30000. Có tính tất định, nên bất kỳ ai cũng có thể tái tạo lại chính xác từng bit. Chỉ có giá đóng cửa theo cấu trúc — OHLC được đặt bằng giá đóng cửa ở mọi chân nến, vì chiến lược này là một close-cross.
  • Chiến lược. Một giao cắt Hull Moving Average: một HMA có độ dài L so với một biến thể HMA-một-phần-ba nhanh hơn (HMA so với HMA3). Luôn ở trong thị trường, đảo chiều long/short mỗi khi có giao cắt. Đây là một chỉ báo thực sự, không tầm thường — hai đường trung bình động có trọng số lồng nhau cộng thêm một bộ làm mượt kiểu cửa sổ căn bậc hai — chứ không phải một SMA đồ chơi, nên khối lượng công việc theo từng bar mang tính đại diện.
  • Sweep. 80 độ dài HMA trải từ 6..200. Đó là "mười nghìn lần chạy" được thu nhỏ đủ để đo trực tiếp: 80 tổ hợp độc lập, mỗi tổ hợp là một backtest đầy đủ trên 150 nghìn bar.
  • Chi phí. Một khoản phí khứ hồi 0,09%, chia theo từng chiều cho các engine mô hình hóa phí hoa hồng theo từng chiều. Khớp lệnh cùng bar tại close[i] — tín hiệu ở bar i được thực thi tại giá đóng cửa của chính bar đó, quy ước mà engine sản xuất của chúng tôi sử dụng.

Bộ đếm giờ theo từng tổ hợp bao trọn đúng hai việc: tính trước HMA bằng numpy và chạy engine. Phần thiết lập thực sự chỉ diễn ra một lần (nạp dữ liệu, dựng các đối tượng bar) nằm ngoài bộ đếm giờ. Có một lần chạy khởi động (warmup), sau đó lặp lại lấy tốt-nhất-trong-N lần — và vì các engine event-driven chậm đến mức 80 tổ hợp đầy đủ sẽ mất nhiều phút cho tới hơn một giờ — chúng tôi đo thời gian trên một mẫu đồng đều của lưới tham số rồi ngoại suy tuyến tính. Các tổ hợp độc lập với nhau, nên ngoại suy tuyến tính là chính xác theo kỳ vọng; cùng một quy ước đó được áp dụng cho baseline pandas, nên không engine nào được hưởng lợi từ cách làm này.

Tính tương đương: chứng minh mọi engine làm cùng một công việc

Đây là cái bẫy mà một phép so sánh engine ngây thơ dễ rơi vào: một engine "nhanh" có thể chỉ đơn giản là làm ít việc hơn. Nếu backtrader ghi nhận 2.700 lệnh còn engine vectorized của bạn chỉ ghi nhận 40, thì engine vectorized đó không nhanh hơn — nó sai, và phép so sánh trở nên vô nghĩa.

Vì vậy chúng tôi khóa chặt phép so sánh bằng một kiểm tra tính tương đương về số lệnh. Tại L=104, tham chiếu numpy tạo ra chính xác 2.707 lệnh đã đóng. Mọi engine phải tái tạo con số đó trong dung sai ±1, nếu không lần chạy sẽ bị hủy với một khẳng định work-parity FAILED. Dung sai này tồn tại chỉ vì các engine bất đồng về quy ước ghi sổ — liệu vị thế mở cuối cùng có bị buộc đóng và tính vào hay không, liệu lệnh vào ban đầu có được tính là một "lệnh" hay không — chứ không phải bất đồng về bản thân các lệnh:

Engine Số lệnh báo cáo @ L=104 Quy ước
tham chiếu numpy 2707 round-trip đã đóng
backtesting.py 2708 +1: vị thế cuối bị buộc đóng ở cuối
backtrader 2707 vị thế mở cuối cùng không được tính
bt 2708 +1: lệnh vào ban đầu được tính là một giao dịch
PyAlgoTrade 2708 +1: lệnh vào ban đầu được tính là một lần khớp

Mọi engine đều rơi vào khoảng 2707 ± 1. Dù sự khác biệt về tốc độ sau đó có ra sao, chúng không phải là hệ quả của việc một engine nào đó âm thầm bỏ bớt công việc. Đây chính là kỷ luật cho phép chúng tôi đặt một framework event-driven và một kernel GPU vào cùng một bảng và thực sự có ý nghĩa khi làm vậy.

Kết quả

Đây là toàn bộ bảng, được sắp xếp từ nhanh nhất đến chậm nhất. combos/s là thông lượng (throughput); cột cuối cùng là thời gian để chạy hết sweep 80 tổ hợp đầy đủ. Hàng baseline là M0 — engine pandas ngây thơ, một vòng lặp for qua các bar với sổ sách kiểu vô hướng (scalar), thứ mà bạn sẽ viết trong một buổi chiều rồi vứt đi. Mọi thứ chậm hơn baseline đó được in đậm.

Engine combos/s Mô hình kiến trúc Sweep 80-tổ-hợp đầy đủ
kernel GPU MLX 779 vectorized (GPU Apple) 0,10 giây
Rust native ~350 compiled 0,23 giây
mp + numba 246 JIT compiled + đa tiến trình 0,33 giây
vectorbt 56,9 vectorized (numpy/numba) 1,4 giây
numba (đơn nhân) 39,7 JIT compiled 2,0 giây
backtesting.py 1,42 event-driven 56 giây
PyAlgoTrade 0,51 event-driven 2,6 phút
M0 — pandas ngây thơ + vòng lặp 0,28 baseline vô hướng 4,8 phút
backtrader 0,11 event-driven 12,7 phút
bt 0,06 weights / rebalance 22,5 phút

Đọc bảng từ trên xuống dưới, mô hình kiến trúc tự nó sắp xếp thành trật tự: năm hàng trên cùng đều là vectorized hoặc compiled, năm hàng dưới cùng đều là event-driven hoặc dựa trên đồ thị đối tượng — với vòng lặp pandas ngây thơ nằm phía trên hai framework trưởng thành, phổ biến. Khoảng cách từ trên xuống dưới là bốn bậc độ lớn. Trên cùng một khối lượng công việc 2.707 lệnh y hệt, kernel MLX hoàn thành sweep trong một phần mười giây; bt cần hai mươi hai phút rưỡi. Đó là một hệ số khoảng 13.000 lần.

Vụ bê bối ở giữa bảng

Nghịch lý ở giữa bảng: một trình chạy vòng lặp for ngây thơ màu xám-xanh trơn trọi băng qua vạch đích trước cả hai cỗ máy framework event-driven cầu kỳ, đầy tính năng, phát sáng màu đỏ-cam vẫn còn ì ạch phía sau, trĩu nặng bởi các mô hình broker và chồng analyzer — baseline khiêm tốn đánh bại các framework trưởng thành trên cùng khối lượng công việc 2.707 lệnh y hệt nhau

Những con số bắt mắt nằm ở hai đầu cực, nhưng kết quả mang tính chỉ dạy lại nằm ở giữa: backtrader (0,11 combos/s) và bt (0,06 combos/s) đều chậm hơn baseline pandas ngây thơ (0,28 combos/s).

Điều này đáng để ngẫm kỹ. M0 không phải một engine tinh vi. Nó là một vòng lặp for trong Python đánh chỉ số vào một DataFrame, theo dõi vị thế và tiền mặt bằng các biến vô hướng thuần túy, và thêm các lệnh vào một danh sách — "đối chứng" cố tình không được tối ưu hóa mà chúng tôi đưa vào để có thứ gì đó rõ ràng là tệ để so sánh vượt qua. Việc truy cập theo từng hàng của pandas nổi tiếng là chậm, và chúng tôi đã cố tình tận dụng điều đó. Vậy mà hai trong số những thư viện backtesting được khuyên dùng nhiều nhất trong hệ sinh thái lại thua nó: backtrader thua 2,5 lần, bt thua 4,7 lần.

Điểm tinh tế giữ cho điều này trung thực: không phải mọi engine event-driven đều chậm hơn pandas. backtesting.py (1,42 combos/s) vượt baseline 5 lần, vì nó là một event loop gọn nhẹ, dựa trên numpy, giữ việc tạo đối tượng theo từng bar ở mức tối thiểu. PyAlgoTrade (0,51) cũng nhỉnh hơn baseline một chút. Vậy nên "event-driven" không phải tự động là án tử — nhưng cỗ máy theo từng bar càng nặng nề thì càng tệ hơn, và backtrader cùng bt mang trên mình cỗ máy nặng nề nhất ở đây. Mô hình kiến trúc thiết lập trần giới hạn; cách triển khai quyết định bạn hạ cánh ở đâu dưới cái trần đó.

Vấn đề không phải là những thư viện này tệ. Mô hình broker của backtrader và thiết kế cây-các-algo của bt tồn tại để đổi lấy tính đúng đắn và khả năng biểu đạt — xử lý lệnh chân thực, tái cân bằng danh mục, các bộ phân tích (analyzer). Những tính năng đó có chi phí runtime, và chi phí đó vô hình khi bạn chỉ chạy một lần. Trên cả một sweep, đó là toàn bộ câu chuyện.

Tại sao các engine event-driven phải trả khoản thuế này

Nơi khoản thuế framework bị đánh vào, theo từng bar: một lần lặp-bar duy nhất được bung ra thành các thành phần tốn kém của nó — một đối tượng bar được hiện thực hóa, một khung callback, một broker định tuyến một lệnh, một sổ cái đang được ghi thêm — mỗi thứ là một trạm thu phí đỏ-cam phát sáng đang gạt lấy một đồng xu, lặp lại dọc theo một hành lang trải dài xa tít gồm mười hai triệu lần lặp-bar giống hệt nhau

Cơ chế này không hề bí ẩn. Một backtest event-driven, ở mỗi bar, làm điều gì đó như sau:

  1. Đẩy đồng hồ tiến lên, cắt bar tiếp theo ra khỏi luồng dữ liệu, và hiện thực hóa nó thành một đối tượng (một Bar, một Line, một dict).
  2. Kích hoạt một callback vào mã người dùng (next(), onBars()), đây là một lệnh gọi hàm Python với khung ngăn xếp (frame) riêng của nó.
  3. Bên trong callback, truy vấn trạng thái broker/vị thế, một lần nữa thông qua các lệnh gọi phương thức và tra cứu thuộc tính.
  4. Nếu một lệnh được tạo ra, định tuyến nó qua broker: xác thực nó, kiểm tra ký quỹ/tiền mặt, lên lịch khớp lệnh, biến đổi một đối tượng danh mục, thêm vào một sổ cái giao dịch.
  5. Cập nhật các bộ phân tích, bộ quan sát, và mọi việc ghi sổ mà framework duy trì.

Bây giờ nhân với 150.000 bar, rồi nhân với 80 tổ hợp: mười hai triệu lần lặp-bar cho mỗi sweep, mỗi lần là cả một nắm các phép cấp phát đối tượng ở cấp độ Python và các lệnh gọi động (dynamic dispatch). Chi phí phụ trội theo từng thao tác của Python — vài chục đến vài trăm nano giây cho một lần tra cứu thuộc tính hay một lần cấp phát nhỏ — là không đáng kể khi xảy ra một lần và tai hại khi xảy ra mười hai triệu lần. Trường hợp của bt là một biến thể của cùng một căn bệnh: dù nó chỉ tái cân bằng vào những ngày có giao dịch chứ không phải mỗi bar, mỗi lần tái cân bằng vẫn phải đánh giá một cây đối tượng algo và chạm vào một sổ cái danh mục dựa trên pandas, và có 2.707 lần như thế cho mỗi tổ hợp, nhân với 80.

Vòng lặp pandas ngây thơ đánh bại backtrader và bt vì một lý do thẳng thắn: nó làm ít việc hơn trên mỗi bar. Nó bỏ qua broker, các đối tượng sự kiện, chồng bộ phân tích, cỗ máy trạng thái định tuyến lệnh. Nó phải trả khoản thuế xấu xí theo từng hàng của pandas, nhưng một khoản thuế xấu xí duy nhất đó vẫn rẻ hơn khoản thuế gọn gàng, đầy đủ tính năng, một-đối-tượng-mỗi-sự-kiện của framework. Khi bạn lột trần một backtest xuống còn "vị thế × lợi suất kỳ tiếp theo − phí", phần lớn những gì một framework làm trên mỗi bar là chi phí phụ trội mà bạn không dùng đến trong lúc tìm kiếm.

Và đây chính là cái bẫy: chi phí phụ trội đó chính là lý do bạn chọn framework đó. Bạn muốn một broker chân thực. Bạn muốn các bộ phân tích. Trong lúc xác nhận cuối cùng, bạn muốn tất cả những thứ đó. Trong một cuộc tìm kiếm 10.000 tổ hợp mà bạn chỉ đọc ra một giá trị mục tiêu vô hướng duy nhất ở đầu kia, bạn đang trả tiền thuê limousine để chạy vòng quanh sân tập.

Đầu kia của bảng: vectorized và compiled

Phần trên cùng của bảng là những gì xảy ra khi bạn xóa bỏ hoàn toàn Python theo từng bar.

  • vectorbt (56,9 combos/s) biểu diễn toàn bộ chiến lược dưới dạng các phép toán mảng numpy/numba. Không có vòng lặp bar nào trong Python — tín hiệu, vị thế, PnL đều ở cấp độ mảng. Nó chạy sweep trong 1,4 giây so với 22,5 phút của bt: nhanh hơn khoảng 950 lần trên cùng một công việc. (Chúng tôi bàn sâu hơn về thiết kế của vectorbt trong bài tổng quan về vectorbt và bài so sánh rộng hơn pandas-vs-polars.)
  • numba (39,7) chính là vòng lặp pandas, không thay đổi về hình dạng, được biên dịch JIT thành mã native. Cùng thuật toán như M0, nhưng @njit biến 0,28 combos/s thành 39,7 — một cú tăng tốc ~140 lần chỉ từ một decorator, vì chi phí phụ trội của trình thông dịch từng chi phối vòng lặp vô hướng đơn giản biến mất.
  • mp + numba (246) chạy kernel đã biên dịch trên nhiều nhân CPU. Các tổ hợp là "song song đến mức lúng túng" (embarrassingly parallel) — mỗi tổ hợp độc lập — nên đa tiến trình mở rộng gần như tuyến tính trên nền JIT.
  • Rust native (~350) loại bỏ nốt phần keo dán Python cuối cùng: toàn bộ sweep là native.
  • GPU MLX (779) ánh xạ sweep lên một kernel GPU của chip Apple-silicon. 80 tổ hợp trở thành 80 làn tính toán song song; sweep hoàn tất trước khi bạn kịp nhả phím enter.

Có hai điều đáng nói chính xác. Thứ nhất, numba chứng minh rằng mô hình kiến trúc quan trọng hơn ngôn ngữ. M0 và numba chạy cùng một thuật toán — khác biệt duy nhất là một cái là Python được thông dịch theo từng bar còn cái kia được biên dịch. Đó là toàn bộ khoản thuế framework trong một phép so sánh A/B có kiểm soát: ~140 lần chỉ nhờ xóa bỏ trình thông dịch khỏi vòng lặp trong cùng. Thứ hai, bước nhảy từ numba (39,7) lên mp+numba (246) rồi lên MLX (779) không còn liên quan đến engine nữa — mà là về điều phối và phần cứng. Một khi khoản thuế theo từng bar đã biến mất, tốc độ trở thành câu hỏi về việc bạn chạy bao nhiêu tổ hợp song song và trên loại chip nào. Chúng tôi đi qua toàn bộ tiến trình đó trong bậc thang tốc độ của các engine backtest, và lý do vì sao dặm cuối cùng lại bị chi phối bởi chi phí tiến trình/tuần tự hóa nằm trong bài viết về thuế-IPC.

Những gì chúng tôi không đo (và lý do chúng tôi nói ra điều đó)

Độ tin cậy của một benchmark nằm ở những gì nó từ chối làm giả. Chúng tôi đã chạy tám engine từ đầu đến cuối dưới điều kiện tương đương. Có vài framework nổi tiếng mà chúng tôi không đưa ra con số, và chúng tôi thà liệt kê chúng một cách trung thực còn hơn ngoại suy ra một con số mà chúng tôi chưa từng đo:

  • zipline / zipline-reloaded — event-driven, có nguồn gốc từ Quantopian. Thiết lập nặng nề (một lịch giao dịch đầy đủ và một gói dữ liệu), khiến việc đo thời gian theo từng tổ hợp một cách công bằng trở nên phức tạp. Về mặt kiến trúc, nó nằm cùng phe event-driven với backtrader; chúng tôi kỳ vọng nó sẽ nằm gần đầu đó của bảng, nhưng chúng tôi chưa chứng minh được điều này.
  • nautilus_trader — event-driven với lõi Rust/Cython, được thiết kế rõ ràng cho tính tương đồng với giao dịch thực. Lõi của nó được biên dịch, nên nó là engine event-driven có khả năng cao nhất không phải trả trọn khoản thuế Python — một phép đo thực sự thú vị mà chúng tôi chưa thực hiện.
  • QuantConnect Lean — dựa trên C#, một runtime hoàn toàn khác; không thể so sánh trực tiếp trong một harness Python.
  • Jesse — event-driven, tập trung vào tiền mã hóa; chúng tôi đã xem xét thiết kế của nó trong một ghi chú riêng nhưng không benchmark nó ở đây.
  • QSTrader — event-driven, hướng danh mục đầu tư; cùng những lưu ý về mô hình kiến trúc như trên.
  • fastquant — chúng tôi đã thử; việc cài đặt/API bị hỏng trong môi trường của chúng tôi, nên không có con số nào. Chúng tôi sẽ không đoán mò.

Hai lưu ý trung thực về các con số mà chúng tôi báo cáo. Các con số của vectorbt, numba, mp+numba, Rust-native và MLX đến từ bậc thang engine của riêng chúng tôi trên cùng khối lượng công việc, chứ không phải từ bộ khai thác mã nguồn mở đã tạo ra bốn hàng event-driven — chúng là cùng một khối lượng công việc nhưng khác giàn đo, và con số Rust-native là một giá trị xấp xỉ ~350, không phải một phép đo chặt chẽ. Và combos/s tuyệt đối phụ thuộc vào phần cứng cụ thể; điều còn giữ nguyên giá trị là thứ tự và các tỷ lệ, vốn đủ lớn (13.000 lần từ trên xuống dưới, 2,5–4,7 lần cho sự đảo ngược pandas-so-với-framework) đến mức không có khác biệt phần cứng hợp lý nào có thể lật ngược chúng.

Bênh vực các engine event-driven

Engine event-driven trong đúng vai trò của nó: một lần chạy xác nhận độ trung thực cao, tỉ mỉ, duy nhất của một chiến lược sống sót, được thể hiện như một công cụ ngọc lục bảo-và-xanh lam chính xác mô phỏng một lần khớp lệnh chân thực, tôn trọng ký quỹ và vòng đời lệnh — cùng cỗ máy theo từng bar từng là lãng phí thuần túy trong lúc tìm kiếm nay được thể hiện ở đây như một sự khéo léo có giá trị, một buổi tổng duyệt cẩn trọng trước khi triển khai

Sẽ dễ dàng để đọc điều này như "các backtester event-driven là tệ." Đó là bài học sai, và không công bằng.

Các engine event-driven được xây dựng cho một công việc khác, và chúng giỏi việc đó. Broker theo từng bar, vòng đời lệnh, logic khớp lệnh, các bộ phân tích — cỗ máy đó tồn tại để khiến một backtest giống với giao dịch thực nhất có thể. Khi mục tiêu của bạn là một buổi tổng duyệt đáng tin cậy, duy nhất cho một chiến lược mà bạn sắp triển khai, bạn muốn engine đó phải toát mồ hôi vì từng lần khớp lệnh, mô phỏng các lần khớp một phần, tôn trọng ký quỹ, và từ chối để bạn giao dịch ở mức giá mà bạn không thể có được. Độ trung thực đó chính là sản phẩm. Chi phí runtime của nó là cái giá của sự chân thực, và với một lần chạy, cái giá đó không đáng kể.

Thất bại không nằm ở engine; mà nằm ở việc dùng nó sai giai đoạn. Nghiên cứu có hai giai đoạn riêng biệt với những yêu cầu đối lập nhau:

  • Tìm kiếm cần thông lượng. Bạn đang khám phá một không gian, phần lớn trong đó là rác, và bạn cần đánh giá hàng nghìn điểm để tìm ra vài điểm đáng để xem xét lại. Độ trung thực trên mỗi điểm hầu như không quan trọng — bạn đang xếp hạng, không phải triển khai. Ở đây khoản thuế framework là lãng phí thuần túy.
  • Xác nhận cần độ trung thực. Bạn có một số ít ứng viên và cần biết, chính xác nhất có thể, liệu chúng có sống sót qua việc thực thi chân thực, phí, trượt giá, và các bẫy nhìn-trước-tương-lai vốn thổi phồng lợi nhuận trên giấy. Ở đây engine event-driven xứng đáng với chi phí của nó.

Sai lầm mà khoản thuế framework trừng phạt là chạy phần tìm kiếm của bạn trên engine xác nhận của bạn — trả giá thuê limousine để khám phá một không gian mà bạn sẽ vứt bỏ 99% của nó.

Quy tắc quyết định

Kết luận thực tiễn cô đọng thành một quyết định duy nhất:

Tìm kiếm trên một engine vectorized/compiled. Xác nhận những chiến lược sống sót trên một engine event-driven.

Cụ thể:

  1. Xây dựng hoặc mượn một kernel nhanh cho việc sweep. vectorbt nếu bạn muốn dùng ngay có sẵn; một vòng lặp biên dịch bằng numba nếu chiến lược của bạn không vector hóa được một cách gọn gàng (chỉ riêng decorator @njit đã mang lại ~140 lần ở đây). Chạy toàn bộ không gian tham số qua nó.
  2. Không bao giờ chạy một sweep lớn trên backtrader, bt, zipline, hay bất kỳ framework event-driven nặng nề nào. Nếu một sweep trên một trong số đó là điểm nghẽn của bạn, cách khắc phục không phải là một cỗ máy lớn hơn — mà là engine sai. Ngay cả vòng lặp pandas ngây thơ cũng sẽ đánh bại hai trong số chúng.
  3. Đưa một danh sách rút gọn lên engine event-driven để kiểm tra độ trung thực. Lấy một số ít ứng viên sống sót và chạy lại chúng trên engine chân thực, nơi mô hình broker và logic khớp lệnh có thể phơi bày những vấn đề mà kernel nhanh đã trừu tượng hóa đi.
  4. Bắt buộc tính tương đương giữa hai bên. Engine nhanh và engine trung thực phải đồng thuận về số lệnh và PnL trên một cấu hình cố định (phép kiểm tra ±1-lệnh của chúng tôi tại L=104), nếu không thì việc tìm kiếm và việc xác nhận đang đo lường hai chiến lược khác nhau, và toàn bộ đường ống xử lý là một lời nói dối.

Đây chính là kiến trúc hai-tốc-độ tương tự xuất hiện bất cứ khi nào tìm kiếm và xác nhận có cấu trúc chi phí đối lập nhau, và đó là lý do vì sao stack của chính chúng tôi luôn giữ một lối đi vectorized/compiled nhanh cho tìm kiếm tham số và dành riêng cỗ máy nặng nề cho đánh giá hàm mục tiêu cuối cùng và kiểm tra bình nguyên (plateau).

Những điểm chính cần rút ra

  1. Phổ biến ≠ nhanh cho việc sweep. Trên một khối lượng công việc giống hệt, được khóa chặt về tính tương đương (150 nghìn bar, 80 tổ hợp HMA-cross, 2.707 lệnh), backtrader (0,11 combos/s) và bt (0,06) đều chạy chậm hơn một vòng lặp pandas ngây thơ (0,28). Một framework trưởng thành, được nhiều sao, không tự động là lựa chọn nhanh.
  2. Khoản thuế framework tính theo từng bar, và một sweep nhân nó lên. Mười hai triệu lần lặp-bar cho mỗi sweep, mỗi lần mang theo một đối tượng sự kiện, một callback, và một vòng khứ hồi với broker. Một chi phí vô hình trong một lần chạy là toàn bộ hóa đơn trên mười nghìn lần.
  3. Mô hình kiến trúc thiết lập trần giới hạn. Các engine vectorized/compiled (vectorbt 56,9, numba 39,7, mp+numba 246, Rust native ~350, MLX 779) đánh bại các engine event-driven từ hai đến bốn bậc độ lớn — lên tới khoảng ~13.000 lần từ trên xuống dưới. Cùng một thuật toán, chỉ đơn giản được biên dịch JIT, đã nhanh hơn 140 lần.
  4. Không phải mọi engine event-driven đều ngang nhau. backtesting.py (1,42) và PyAlgoTrade (0,51) vẫn đánh bại baseline ngây thơ; khoản thuế tỷ lệ theo mức độ nặng nề của cỗ máy theo từng bar. Cách triển khai quyết định bạn hạ cánh ở đâu dưới cái trần đó.
  5. Hai engine, hai giai đoạn. Tìm kiếm trên một kernel vectorized/compiled; xác nhận những ứng viên sống sót trên engine event-driven chân thực. Bắt buộc tính tương đương về số lệnh/PnL giữa chúng để cả hai đều đang đo cùng một chiến lược.
  6. Trung thực về những gì bạn đã đo. Chúng tôi đã đo hiệu năng tám engine dưới điều kiện tương đương và liệt kê những cái chúng tôi chưa đo (zipline, nautilus_trader, Lean, Jesse, QSTrader, và bản cài đặt fastquant không chịu chạy) thay vì bịa ra con số cho chúng.

Tóm tắt khó chịu: nếu một parameter sweep là điểm nghẽn của bạn, vấn đề có lẽ không nằm ở máy tính của bạn cũng không nằm ở chiến lược của bạn. Vấn đề là bạn đang chạy một cuộc tìm kiếm trên một engine được xây dựng cho một lần chạy nghiêm túc duy nhất — và bạn sẽ nhanh hơn với vòng lặp pandas mà bạn từng quá ngại ngùng để giữ lại.

Toàn bộ bộ khai thác, các khẳng định về tính tương đương, và kết quả JSON thô theo từng engine nằm trong benchmarks/bench_oss_engines.pybenchmarks/results_oss/ tại commit 250dbb5. Đối với đầu compiled/GPU của bậc thang, xem bậc thang tốc độ của các engine backtestphân tích thuế-IPC.

Tuyên bố miễn trừ trách nhiệm: Thông tin được cung cấp trong bài viết này chỉ nhằm mục đích giáo dục và thông tin, không cấu thành lời khuyên về tài chính, đầu tư hoặc giao dịch. Giao dịch tiền mã hóa tiềm ẩn rủi ro thua lỗ đáng kể.

Tác Giả

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

Đi Trước Thị Trường

Đăng ký nhận bản tin của chúng tôi để có những thông tin chuyên sâu độc quyền về AI trading, phân tích thị trường và các cập nhật nền tảng.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất kỳ lúc nào.