Strasmore Research
Phân tích chuyên sâu Matt ConnorBởi Matt Connor · Cập nhật 2026-08-08

Backtest Python có thể tái lập, không cần API key

Chạy backtest Python có thể tái lập với bản cài đặt được ghim và dữ liệu mẫu xác định, không cần API key, đồng thời hiểu đúng giới hạn của một đường vốn.

Một backtest có thể tái lập bằng Python là backtest mà một người hoàn toàn xa lạ có thể chạy lại trên một máy tính sạch và nhận được đúng các con số của bạn, không bị cản trở bởi yêu cầu mở tài khoản hay API key. Phần lớn các hướng dẫn không vượt qua được bài kiểm tra này ngay từ dòng đầu tiên, khi một lượt tải dữ liệu trực tiếp cung cấp cho người đọc tiếp theo lịch sử giá hơi khác với dữ liệu đã cung cấp cho tác giả. Bài hướng dẫn này cố định một engine mã nguồn mở, quantjourney-bt ở phiên bản 0.12.4, chạy ví dụ đi kèm mà không cần thông tin xác thực, sau đó sử dụng dữ liệu thị trường thực để cho thấy ngay cả một lần chạy sạch duy nhất vẫn không thể cho bạn biết điều gì.

Điều gì khiến một backtest có thể tái lập?

Tính tái lập ở đây có ý nghĩa hẹp và có thể kiểm thử: một người khác chạy một lệnh duy nhất trên một máy sạch và nhận lại chính xác các con số của bạn, đến từng chữ số thập phân. Hai yếu tố thông thường có thể phá vỡ điều này.

Thứ nhất là mã nguồn. Một thư viện ở phiên bản 0.x không cam kết khả năng tương thích giữa các bản phát hành thứ cấp. Chỉ cần đổi tên, thay đổi giá trị mặc định hoặc sắp xếp lại một cột, script của bạn vẫn tiếp tục chạy nhưng âm thầm báo cáo kết quả khác.

Thứ hai là dữ liệu. Một tutorial có dòng đầu tiên là lệnh tải dữ liệu trực tiếp chưa bao giờ có tính tái lập. Nhà cung cấp có thể điều chỉnh dữ liệu lịch sử, điều chỉnh theo các đợt chia tách cổ phiếu và bổ sung dữ liệu còn thiếu. Vì vậy, cùng một script có thể cho ra các con số khác sau một tháng. Sau đó, bạn không thể tách riêng ảnh hưởng của thay đổi mã nguồn khỏi thay đổi dữ liệu.

Cách làm đáng học theo từ dự án này là kết hợp một phiên bản engine cố định với một bộ dữ liệu nhỏ được đóng gói sẵn và đi kèm ngay trong package.

Cố định phiên bản cài đặt: pip install quantjourney-bt==0.12.4

quantjourney-bt là công cụ backtest QuantJourney, được phát hành theo Apache License 2.0 và yêu cầu Python 3.11 trở lên. Phiên bản 0.12.4 được phát hành ngày 21 tháng 7 năm 2026. Đây là phiên bản được mọi lệnh bên dưới sử dụng, theo tài liệu cập nhật vào tháng 8 năm 2026.

Hãy làm việc trong một môi trường cô lập. python3 -m venv .venv tạo môi trường này, source .venv/bin/activate kích hoạt môi trường, còn python -m pip install -U pip cập nhật trình cài đặt bên trong môi trường đó. Sau đó, cài đúng phiên bản: pip install quantjourney-bt==0.12.4.

Tài liệu của dự án ghi dạng không cố định phiên bản là pip install quantjourney-bt. Phần ==0.12.4 là trách nhiệm của bạn; với các phiên bản 0.x, việc này đặc biệt quan trọng. Hãy ghi lại phiên bản cố định ở nơi người tiếp theo có thể tìm thấy: pip freeze > requirements.txt lưu toàn bộ dependency đã được phân giải, bao gồm cả những dependency bạn không chỉ định trực tiếp.

Có hai tùy chọn bổ sung. pip install "quantjourney-bt[wf]" cài thêm Optuna cho các ví dụ về walk-forward và tối ưu hóa. pip install "quantjourney-bt[data]" cài thêm phương án dự phòng sử dụng yfinance cho các benchmark.

Apache-2.0 là giấy phép tương đối thoáng. Bạn có thể sử dụng và sửa đổi mã cho mục đích thương mại. Khi phân phối lại, bạn phải giữ các tệp giấy phép và thông báo đi kèm. Những người đóng góp cũng cấp rõ ràng quyền sử dụng bằng sáng chế.

Cách chạy ví dụ SMA đi kèm mà không cần API key

Repository cung cấp một script khởi chạy cùng với năm mươi chiến lược mẫu có thể chạy, được chia thành nhánh dựa trên tỷ trọng và nhánh dựa trên lệnh. Trong số đó có năm quy trình walk-forward. ./strategy.sh --list in danh mục. ./strategy.sh example_weights_01_sma_daily --check import một chiến lược duy nhất và hoàn toàn không truy cập dữ liệu, nên đây là cách nhanh nhất để xác nhận bản cài đặt hoạt động bình thường.

Lệnh chạy demo chỉ có một dòng: ./strategy.sh example_weights_01_sma_daily --sample-data --output /tmp/qj-sample

Cờ --sample-data là phần cốt lõi. Dự án mô tả dataset phía sau cờ này như sau:

Dataset mẫu được thiết kế có chủ đích với quy mô nhỏ và khả năng tái lập. Dataset này hữu ích để kiểm tra cài đặt, tạo báo cáo và đọc luồng vận hành của engine mà không cần tạo tài khoản.
Nguồn: quantjourney-bt README, phiên bản 0.12.4, đọc ngày 6 tháng 8 năm 2026.

Lệnh chạy tạo ra một thư mục thay vì thông báo kết quả trên console: summary.txtsummary.json, một metrics.csv, một equity_curve.csv nằm cạnh equity_curve.png của nó, một dashboard.html, một thư mục plots/ và một run_metadata.json ghi lại cấu hình của lần chạy. Tệp cuối cùng này thường bị bỏ qua, nhưng lại là tệp giúp kiểm toán kết quả sau một năm.

Hãy đọc các chỉ số kết quả một cách thận trọng. Dataset đi kèm có quy mô nhỏ và chỉ mang tính minh họa, vì vậy Sharpe ratio và mức sụt giảm tối đa được in trong summary.txt chỉ mô tả một tệp mẫu. Chúng không phải bằng chứng về hiệu quả của một chiến lược. Coi chúng là kết quả chính là sai lầm đầu tiên bạn có thể mắc phải.

Điều lần chạy này xác nhận vẫn rất hữu ích: bản cài đặt hoạt động, và toàn bộ luồng vận hành của engine, từ tín hiệu đến tỷ trọng mục tiêu rồi đến giá trị danh mục được tái dựng, tạo ra các tệp kết quả trên máy của bạn mà không cần bất kỳ thông tin xác thực nào. Dự án có một quy trình sử dụng thông tin xác thực để truy cập dịch vụ dữ liệu riêng cho các backtest trên dữ liệu lịch sử thực tế. Quy trình đó đã được tài liệu hóa. Phần hướng dẫn này dừng tại đây, ở bước không cần đến dữ liệu hay quyền truy cập từ bên ngoài.

Một đường cong vốn trong mẫu không thể cho bạn biết điều gì

Lần chạy mẫu tạo ra một đường cong vốn. Dưới đây là những điều đường cong đó không thể cho biết, khi được đo trên dữ liệu thị trường thực thay vì một tệp demo.

Bảng dưới đây áp dụng cùng ý tưởng mà chiến lược ví dụ sử dụng: đường trung bình động 20 phiên cắt đường 50 phiên. Quy tắc này được áp dụng cho SPY và báo cáo riêng từng năm dương lịch từ 2017 đến 2025. Vị thế trong mỗi phiên được xác định bằng giá đóng cửa của phiên trước đó. Vì vậy, quy tắc không bao giờ giao dịch dựa trên một con số chưa có tại thời điểm ra quyết định.

Truy vấnTín hiệu giao cắt đường trung bình động 20/50 trên SPY theo từng năm, so với nắm giữ
Câu lệnh SQL chính xác đằng sau mỗi con số
WITH daily AS
(
    SELECT
        toDate(toTimeZone(window_start, 'America/New_York')) AS d,
        toFloat64(argMax(close, window_start))               AS px
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker = 'SPY'
      AND window_start >= '2016-01-01 00:00:00'
      AND window_start <  '2026-01-01 05:00:00'
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
    GROUP BY d
),
averaged AS
(
    SELECT
        d,
        px,
        avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
        avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
        row_number() OVER (ORDER BY d)                                      AS session_no
    FROM daily
),
positioned AS
(
    SELECT
        d,
        px,
        if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
        lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
            OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
        lagInFrame(px, 1)
            OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
    FROM averaged
)
SELECT
    toYear(d)                                                                   AS year,
    round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
    round((exp(sum(log(px / px_prior))) - 1) * 100, 1)                          AS hold_pct,
    countIf(long_today != long_prior)                                           AS crossover_count
FROM positioned
WHERE px_prior > 0
  AND toYear(d) >= 2017
GROUP BY year
ORDER BY year
Run this yourself

Một quy tắc không thay đổi được đo trong 9 lần riêng biệt. Hãy đọc hai cột phần trăm trước khi xem bất kỳ thông tin nào khác. Trong 2017, quy tắc kết thúc năm với mức 16%, so với 19.4% khi nắm giữ SPY trong cùng giai đoạn. Trong 2025, hai cột đó lần lượt là 10.4% và 16.4%. Mã lệnh hoàn toàn giống nhau ở cả hai dòng. Chỉ có khoảng thời gian được thay đổi.

Cột giao cắt cho thấy bằng chứng thực nghiệm về tài sản cơ sở mỏng đến mức nào. 4 lần thay đổi vị thế trong 2025 có nghĩa là toàn bộ đường cong vốn của một năm chỉ dựa trên một số ít quyết định. Đây là một mẫu rất nhỏ để có thể coi là một kết quả đáng tin cậy.

Quy tắc tương tự có vận hành theo cùng cách trên các mã khác không?

Thay đổi khoảng thời gian là một cách để kiểm tra một đường cong đơn lẻ. Thay đổi tập mã là cách còn lại. Bảng dưới đây giữ nguyên các tham số và chạy cùng một quy tắc trên năm mã có thanh khoản cao trong năm năm dương lịch từ 2021 đến 2025.

Truy vấnCùng quy tắc 20/50 trên năm mã có thanh khoản cao, giai đoạn 2021–2025
Câu lệnh SQL chính xác đằng sau mỗi con số
WITH daily AS
(
    SELECT
        ticker,
        toDate(toTimeZone(window_start, 'America/New_York')) AS d,
        toFloat64(argMax(close, window_start))               AS px
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker IN ('SPY', 'QQQ', 'AAPL', 'MSFT', 'KO')
      AND window_start >= '2020-07-01 00:00:00'
      AND window_start <  '2026-01-01 05:00:00'
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
    GROUP BY ticker, d
),
averaged AS
(
    SELECT
        ticker,
        d,
        px,
        avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
        avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
        row_number() OVER (PARTITION BY ticker ORDER BY d)                                      AS session_no
    FROM daily
),
positioned AS
(
    SELECT
        ticker,
        d,
        px,
        lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
            OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
        lagInFrame(px, 1)
            OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
    FROM averaged
)
SELECT
    ticker                                                                      AS symbol,
    round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
    round((exp(sum(log(px / px_prior))) - 1) * 100, 1)                          AS hold_pct,
    round(avg(long_prior) * 100, 0)                                             AS days_long_pct
FROM positioned
WHERE px_prior > 0
  AND d >= toDate('2021-01-01')
GROUP BY symbol
ORDER BY rule_pct DESC
Run this yourself

QQQ đứng đầu bảng ở mức 40.2%, còn hàng cuối, KO, đạt 3.8%. Cột days_long_pct cho biết mỗi phiên bản đã nắm giữ một vị thế trong bao nhiêu phần của khoảng thời gian; con số này là 67% đối với hàng đầu tiên. Một bộ tham số, năm tập mã và chênh lệch đủ lớn để việc chọn mã thắng cuộc sau khi sự việc đã xảy ra không nói lên điều gì về giao dịch mà bạn chưa thực hiện.

Không nội dung nào ở đây là khuyến nghị giao dịch theo chiến lược crossover. Crossover chỉ là thước đo cho backtest, còn backtest mới là đối tượng chúng ta đang đo lường.

Look-ahead bias len vào backtest trọng số như thế nào

Một engine dựa trên trọng số sẽ chuyển tín hiệu thành trọng số mục tiêu, mô phỏng khớp lệnh theo các trọng số đó, rồi tính lại giá trị danh mục từ các vị thế phát sinh. Lỗi nằm ở bước ghép giữa tín hiệu và trọng số. Nếu trọng số của hôm nay được tính từ giá đóng cửa hôm nay rồi dùng để hưởng lợi từ mức sinh lời của chính hôm nay, backtest đã giao dịch dựa trên thông tin chưa tồn tại tại thời điểm lệnh được đặt. Đó là look-ahead bias, và nó không tạo ra lỗi nào. Nó chỉ khiến mọi kết quả trông tốt hơn.

Bảng dưới đây chạy hai phiên bản của cùng một quy tắc trên cùng chuỗi dữ liệu lịch sử của SPY.

Truy vấnCùng quy tắc, tín hiệu phiên trước so với tín hiệu cùng phiên, SPY theo từng năm
Câu lệnh SQL chính xác đằng sau mỗi con số
WITH daily AS
(
    SELECT
        toDate(toTimeZone(window_start, 'America/New_York')) AS d,
        toFloat64(argMax(close, window_start))               AS px
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker = 'SPY'
      AND window_start >= '2016-01-01 00:00:00'
      AND window_start <  '2026-01-01 05:00:00'
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
    GROUP BY d
),
averaged AS
(
    SELECT
        d,
        px,
        avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
        avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
        row_number() OVER (ORDER BY d)                                      AS session_no
    FROM daily
),
positioned AS
(
    SELECT
        d,
        px,
        if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
        lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
            OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
        lagInFrame(px, 1)
            OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
    FROM averaged
)
SELECT
    toYear(d)                                                                   AS year,
    round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS next_bar_pct,
    round((exp(sum(log(if(long_today = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS same_bar_pct,
    round(abs(exp(sum(log(if(long_today = 1, px / px_prior, 1.0))))
              - exp(sum(log(if(long_prior = 1, px / px_prior, 1.0))))) * 100, 1) AS gap_pp
FROM positioned
WHERE px_prior > 0
  AND toYear(d) >= 2017
GROUP BY year
ORDER BY year
Run this yourself

Trong 2017, phiên bản sử dụng phiên trước cho kết quả 16%, còn phiên bản sử dụng cùng phiên cho kết quả 17.1%, chênh nhau 1.1 điểm phần trăm. Trong 2025, khoảng cách giữa hai kết quả là 1.6 điểm phần trăm. Chỉ một trong hai cột có thể được tạo ra bởi một máy không biết trước phiên đó sẽ đóng cửa ở đâu. Phần chênh lệch giữa chúng chỉ là vấn đề hạch toán, không gắn với ý tưởng, kỹ năng hay giao dịch thực tế nào.

Engine này nêu rõ lập trường về vấn đề thời điểm. Điều đó có giá trị hơn một lời cam kết:

Với các lệnh khớp tại giá mở cửa, mô hình trượt giá nhạy với biên độ chỉ sử dụng bar đã hoàn tất trước đó, còn khả năng hấp thụ khối lượng được dự báo từ các quan sát có độ trễ. Engine không sử dụng mức cao nhất, thấp nhất, giá đóng cửa hoặc tổng khối lượng của cả ngày đó ở thời điểm về sau.
Nguồn: quantjourney-bt README, phiên bản 0.12.4, đọc ngày 6 tháng 8 năm 2026.

Một giả định được tài liệu hóa có thể được đối chiếu với mã nguồn bạn đã cài đặt. Một giả định không được tài liệu hóa chỉ là phỏng đoán.

Vì sao phần mở rộng walk-forward tồn tại

Các ví dụ walk-forward, từ WF01 đến WF05, đi kèm phần mở rộng [wf] và dependency Optuna. Walk-forward ước tính tham số trên một đoạn dữ liệu lịch sử, đo lường kết quả trên đoạn dữ liệu ngay sau đó, rồi dịch chuyển cặp cửa sổ này về phía trước và lặp lại. Biến thể rolling và expanding khác nhau ở việc cửa sổ ước tính có loại bỏ dữ liệu cũ nhất khi dịch chuyển hay không. Một ví dụ khác thêm bước purge và embargo tại mỗi ranh giới, loại bỏ các quan sát gần đường nối để phần dữ liệu dùng ước tính không thể rò rỉ vào phần dữ liệu đang được đo lường.

Không yếu tố nào trong số đó biến một ý tưởng yếu thành một ý tưởng hiệu quả. Quy trình này thay một con số duy nhất bằng một phân phối gồm nhiều con số để bạn có thể chất vấn, và đó chính là phần cải thiện cốt lõi. Bước tiếp theo vẫn chưa phải là giao dịch bằng tiền thật: paper trading trước khi dùng tiền thật đo lường những yếu tố mà backtest về mặt cấu trúc không thể quan sát, trước hết là việc lệnh của bạn có được khớp ở mức giá gần với giả định của trình mô phỏng hay không; còn circuit breaker cho trading bot kiểm soát những gì mã của bạn thực hiện vào ngày hệ thống gặp sự cố. Để tìm hiểu sâu hơn về phần thống kê làm nền tảng cho toàn bộ quy trình, hãy xem ghi chú của chúng tôi về sách giao dịch định lượng mã nguồn mở.

Câu hỏi thường gặp

Có thể backtest một chiến lược mà không cần API key không?

Có. quantjourney-bt cung cấp sẵn một bộ dữ liệu mẫu thông qua cờ --sample-data. Các chiến lược trong ví dụ có thể chạy trên bộ dữ liệu này mà không cần tài khoản hay thông tin xác thực. Bộ dữ liệu nhỏ và chỉ mang tính minh họa. Vì vậy, hãy xem kết quả chạy đó là bước kiểm tra cài đặt và pipeline, không phải bằng chứng về hiệu quả của một chiến lược.

Vì sao cần ghim phiên bản của một package backtesting Python?

Một package ở phiên bản 0.x không đảm bảo khả năng tương thích giữa các bản phát hành nhỏ. Một giá trị mặc định bị thay đổi hoặc một chỉ số bị đổi tên cũng có thể không được thông báo. Việc ghim phiên bản bằng pip install quantjourney-bt==0.12.4 và ghi lại môi trường trong file requirements giúp bạn có thể tái tạo kết quả hôm nay vào năm sau trên đúng engine đã tạo ra kết quả đó.

quantjourney-bt được phát hành theo giấy phép nào?

Apache License 2.0. Giấy phép này cho phép sử dụng và chỉnh sửa cho mục đích thương mại. Khi phân phối lại, bạn phải giữ các file giấy phép và thông báo đi kèm. Giấy phép cũng bao gồm điều khoản cấp quyền sáng chế rõ ràng từ các bên đóng góp. Phiên bản 0.12.4 được phát hành ngày 21 tháng 7 năm 2026 và yêu cầu Python 3.11 trở lên.

Kết quả backtest tốt có nghĩa là chiến lược sẽ hiệu quả không?

Không. Backtest chỉ là một phép đo trên một khoảng thời gian và một nhóm tài sản cụ thể. Các bảng ở trên cho thấy cùng một quy tắc không thay đổi có thể tạo ra các kết quả hằng năm rất khác nhau trên một ticker, cũng như các kết quả rất khác nhau giữa năm mã. Đây là khoảng cách mà walk-forward validation và out-of-sample testing được thiết kế để phát hiện.

Cách tính các bảng ở trên

Giá đóng cửa theo ngày là mức giá của giao dịch được thực hiện ở phút cuối cùng của phiên thông thường trong từng ngày. Dữ liệu sử dụng giờ New York, trong khoảng từ 9:30 sáng đến 4:00 chiều. Cách này bảo đảm các phiên giao dịch kết thúc sớm được xử lý chính xác mà không cần cố định độ dài phiên. Đường trung bình nhanh bao phủ 20 phiên và đường trung bình chậm bao phủ 50 phiên; cả hai đều là đường trung bình đơn giản. 49 phiên đầu tiên của mỗi chuỗi là giai đoạn khởi động và không nắm giữ vị thế. Các số liệu hằng năm được tính bằng cách lũy kế biến động từ giá đóng cửa phiên này đến giá đóng cửa phiên kế tiếp trong những phiên mà quy tắc duy trì vị thế mua. Cột hold lũy kế biến động của mọi phiên trong cùng năm để đối chiếu. Năm mã trong phần so sánh chéo được chọn dựa trên lịch sử liên tục và không có lần split nào trong khoảng thời gian phân tích, nên chuỗi giá đóng cửa không cần điều chỉnh. Các khoảng thời gian đều được cố định trong quá khứ. Vì vậy, các bảng này sẽ trả về cùng một bộ số liệu sau mỗi lần tái tạo.


Mỗi bảng ở đây đều có phần SQL chính xác ngay bên dưới. Nhờ đó, bạn có thể chạy lại các số liệu trên trang này, tương tự như việc cài đặt phiên bản đã được ghim. Để đo lường một quy tắc trong khoảng thời gian của riêng bạn trước khi viết bất kỳ mã backtest nào, hãy đặt câu hỏi bằng tiếng Anh thông thường trên terminal Strasmore.

#backtesting#python#open source#reproducibility#quantjourney