Bootstrap khoảng tin cậy cho backtest
Một đường cong vốn chỉ là một mẫu. Khoảng tin cậy bootstrap của Sharpe backtest thường đủ rộng để bao gồm mức 0. Tìm hiểu cách xây dựng và đọc khoảng này.
Khoảng tin cậy của backtest trả lời một câu hỏi: đường cong vốn phản ánh bao nhiêu phần do chiến lược tạo ra, và bao nhiêu phần đến từ chính giai đoạn lịch sử được dùng để chạy backtest. Phương pháp bootstrap cho backtest xây dựng khoảng này bằng cách lấy mẫu lại chuỗi lợi suất nhiều lần, tính lại chỉ số trên từng mẫu, rồi xác định các phân vị của kết quả thu được. Sharpe ở mức 1,4 được đo trên một năm dữ liệu quan sát hằng ngày có thể có khoảng tin cậy 95% đủ rộng để bao gồm cả mức 0. Các biểu đồ bên dưới cho thấy nguyên nhân của điều này.
Vì sao một đường cong vốn chỉ là một mẫu
Một backtest chỉ cung cấp một con số cho mỗi thống kê: một Sharpe ratio, một mức lợi nhuận quy năm, một mức drawdown tồi tệ nhất và một hit rate. Không con số nào trong đó là giá trị thực của chiến lược. Mỗi con số là một ước tính được xây dựng từ một chuỗi ngày giao dịch hữu hạn. Một giai đoạn khác có cùng độ dài sẽ cho ra kết quả khác.
Biểu đồ dưới đây loại bỏ hoàn toàn chiến lược khỏi câu hỏi. Biểu đồ đo lường vị thế đơn giản nhất có thể: nắm giữ SPY trong từng năm dương lịch, dựa trên lợi nhuận giá từ giá đóng cửa đến giá đóng cửa.
Câu lệnh SQL chính xác đằng sau mỗi con số
WITH daily AS
(
SELECT
date,
toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= '2011-12-01'
AND date < '2026-01-01'
)
SELECT
toString(toYear(date)) AS year,
count() AS obs_count,
round(avg(ret) * 252 * 100, 2) AS ann_return_pct,
round(stddevSamp(ret) * sqrt(252) * 100, 2) AS ann_vol_pct,
round(avg(ret) / stddevSamp(ret) * sqrt(252), 2) AS sharpe_ratio
FROM daily
WHERE date >= '2012-01-01'
AND ret IS NOT NULL
GROUP BY year
ORDER BY yearMỗi hàng bao quát khoảng 250 phiên, gần với một năm giao dịch tiêu chuẩn. Vị thế không thay đổi trong bất kỳ giai đoạn nào. Trong 2012, Sharpe quy năm được đo ở mức 1.06; trong 2025, chỉ số này ở mức 0.88, với 14 năm được đưa vào biểu đồ. Chỉ một năm nắm giữ một chỉ số diện rộng cũng khiến con số tiêu đề biến động nhiều hơn mức mà phần lớn độc giả có thể xem là nhiễu. Vì vậy, backtest chiến lược có cùng độ dài ít nhất cũng sẽ chịu mức biến động đó. Quy ước quy năm đứng sau cột này được trình bày trong hướng dẫn về Sharpe ratio của chúng tôi. Cơ chế tính lợi nhuận theo kỳ được trình bày trong cách đo lường lợi nhuận theo tháng.
Biên độ quanh Sharpe của backtest rộng đến đâu?
Sai số chuẩn của ước tính Sharpe thường giảm gần tương ứng với căn bậc hai của số quan sát. Thay vì chỉ dựa vào công thức đó, hãy đo trực tiếp độ phân tán. Chia hai mươi năm giao dịch thành các khoảng thời gian không chồng lấn có độ dài cố định, tính Sharpe annualized trong từng khoảng, rồi xem các kết quả cách nhau bao xa.
Câu lệnh SQL chính xác đằng sau mỗi con số
WITH daily AS
(
SELECT
date,
toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= '2005-12-01'
AND date < '2026-01-01'
),
indexed AS
(
SELECT
ret,
row_number() OVER (ORDER BY date) AS i
FROM daily
WHERE date >= '2006-01-01'
AND ret IS NOT NULL
),
blocks AS
(
SELECT
w,
intDiv(i, w) AS blk,
count() AS n,
avg(ret) / nullIf(stddevSamp(ret), 0) * sqrt(252) AS sharpe
FROM indexed
CROSS JOIN (SELECT arrayJoin([21, 63, 126, 252, 504]) AS w) AS ws
GROUP BY w, blk
HAVING n = w
)
SELECT
concat(toString(w), ' sessions') AS horizon,
count() AS block_count,
round(quantileDeterministic(0.05)(sharpe, blk), 2) AS sharpe_p05,
round(quantileDeterministic(0.50)(sharpe, blk), 2) AS sharpe_p50,
round(quantileDeterministic(0.95)(sharpe, blk), 2) AS sharpe_p95,
round(quantileDeterministic(0.95)(sharpe, blk)
- quantileDeterministic(0.05)(sharpe, blk), 2) AS sharpe_band_width
FROM blocks
WHERE sharpe IS NOT NULL
GROUP BY w
ORDER BY wVới 21 sessions mỗi khoảng, phân vị thứ 5 đến phân vị thứ 95 của các mức Sharpe đo được nằm trong khoảng từ -3.37 đến 6.97. Như vậy, biên độ là 10.34 điểm Sharpe trên 238 khoảng thời gian. Kéo dài mỗi khoảng lên 504 sessions, biên độ giảm còn 1.67 điểm, nhưng lúc này chỉ được đo trên 8 khoảng thời gian. Hai yếu tố cùng thay đổi. Ước tính trở nên chính xác hơn khi quy mô mẫu tăng, nhưng số mẫu độc lập mà dữ liệu lịch sử có thể cung cấp lại giảm mạnh. Mâu thuẫn đó là trọng tâm của vấn đề.
Cách bootstrap khoảng tin cậy cho backtest
Quy trình này đủ ngắn để trình bày đầy đủ.
- Bắt đầu với chuỗi lợi nhuận thực tế của chiến lược, mỗi kỳ một giá trị, tổng cộng N giá trị.
- Lấy ngẫu nhiên N lợi nhuận từ danh sách đó, có hoàn lại. Một số giá trị có thể xuất hiện hai lần, trong khi một số khác không xuất hiện.
- Tính lại chỉ số trên mẫu resample.
- Lặp lại vài nghìn lần và lưu toàn bộ kết quả.
- Sắp xếp các giá trị đã lưu, rồi lấy percentile 2,5 và 97,5 để xác định khoảng tin cậy 95%.
Đặt seed cho bộ sinh số ngẫu nhiên trước bước 2 và ghi seed bên cạnh khoảng tin cậy được công bố. Bootstrap là một ước tính Monte Carlo: hai lần chạy không đặt seed sẽ khác nhau ở các chữ số cuối. Nếu reviewer không thể chạy lại để tái tạo khoảng tin cậy, họ không có cách kiểm tra kết quả. Đây cũng là nguyên tắc được nêu trong thiết lập backtest có thể tái lập.
Lợi nhuận trung bình và Sharpe đi qua bước 2 một cách phù hợp, vì cả hai đều xử lý danh sách lợi nhuận như một tập hợp. Maximum drawdown thì không. Drawdown phụ thuộc vào thứ tự của đường đi. Một mẫu resample làm xáo trộn thứ tự lợi nhuận có thể tạo ra mức drawdown tệ nhất mà không thứ tự nào trong chuỗi giao dịch thực tế từng tạo ra. Tuy vậy, vẫn đáng thực hiện bootstrap cho chỉ số này, với điều kiện ghi rõ bản chất của kết quả: đó là phân phối drawdown trên các lịch sử bị xáo trộn, không phải dự báo cho drawdown tiếp theo. Định nghĩa được nêu trong maximum drawdown.
Vì sao bootstrap IID không phù hợp với lợi suất thị trường
Bước 2 giả định mọi lợi suất đều độc lập và có cùng phân phối, tức giả định IID. Lợi suất hằng ngày vi phạm giả định này theo cách làm thay đổi độ rộng của khoảng. Lợi suất có dấu chỉ thể hiện mức ghi nhớ trong một ngày khá yếu. Nhưng độ lớn của lợi suất lại có tính cụm: các biến động lớn thường xuất hiện liền nhau, còn những ngày yên ắng cũng thường kéo dài thành chuỗi.
Câu lệnh SQL chính xác đằng sau mỗi con số
WITH daily AS
(
SELECT
ticker,
date,
toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
FROM global_markets.stocks_daily_aggs
WHERE ticker IN ('SPY', 'MSFT', 'KO', 'XOM', 'JNJ', 'PG')
AND date >= '2015-11-01'
AND date < '2026-01-01'
),
lagged AS
(
SELECT
ticker,
date,
ret,
lagInFrame(ret, 1) OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS ret_prev
FROM daily
WHERE ret IS NOT NULL
AND abs(ret) < 0.35
)
SELECT
ticker,
count() AS obs_count,
round(corr(ret, ret_prev), 3) AS return_autocorr,
round(corr(abs(ret), abs(ret_prev)), 3) AS abs_return_autocorr
FROM lagged
WHERE date >= '2016-01-01'
AND ret_prev IS NOT NULL
GROUP BY ticker
ORDER BY abs_return_autocorr DESCĐối với SPY, tự tương quan bậc trễ một của lợi suất hằng ngày tuyệt đối được đo ở mức 0.366 qua 2514 phiên, so với -0.133 của lợi suất có dấu trong cùng các phiên đó. Hãy so sánh hai cột của bất kỳ mã nào trên biểu đồ. Việc xáo trộn chuỗi lợi suất sẽ phá hủy tính cụm này. Khi chạy bootstrap IID trên dữ liệu đó, khoảng thu được sẽ hẹp hơn mức mà mẫu dữ liệu hỗ trợ. Sai số này đi theo hướng bất lợi nhất: khiến chiến lược trông hiệu quả hơn thực tế.
Bootstrap theo khối trượt và cách chọn độ dài khối
Cách khắc phục là lấy mẫu lại các khối liên tiếp thay vì từng mức sinh lời riêng lẻ. Chọn độ dài khối L, lấy ngẫu nhiên có hoàn lại các khối gồm L mức sinh lời liên tiếp, rồi ghép chúng nối tiếp cho đến khi chuỗi tổng hợp có độ dài N. Mối phụ thuộc bên trong một khối được giữ nguyên: các mức sinh lời vẫn theo đúng thứ tự ban đầu. Chỉ các điểm nối giữa các khối là nhân tạo.
Độ dài khối tạo ra sự đánh đổi giữa hai loại sai số, và không có thiết lập nào loại bỏ được cả hai. Khối ngắn hoạt động gần giống IID bootstrap và đánh giá thấp khoảng, tạo ra sai lệch. Khối dài giữ lại nhiều mối phụ thuộc hơn nhưng số khối riêng biệt để lấy mẫu ít hơn. Vì vậy, mỗi mẫu lại lặp lại những đoạn lịch sử lớn, khiến chính khoảng ước tính trở nên nhiễu hơn; đây là phương sai. Các quy tắc kinh nghiệm được công bố thường xác định độ dài khối tăng theo lũy thừa một phần ba của N. Hãy xem đó là điểm khởi đầu.
Một phép kiểm tra rẻ hơn là xem phương sai thay đổi như thế nào theo khoảng thời gian. Khi các quan sát độc lập, phương sai của tổng mức sinh lời trong k ngày bằng k lần phương sai một ngày, và tỷ lệ giữa hai đại lượng này bằng 1.
Câu lệnh SQL chính xác đằng sau mỗi con số
WITH daily AS
(
SELECT
date,
toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= '2005-12-01'
AND date < '2026-01-01'
),
indexed AS
(
SELECT
ret,
row_number() OVER (ORDER BY date) AS i
FROM daily
WHERE date >= '2006-01-01'
AND ret IS NOT NULL
),
base AS
(
SELECT varSamp(ret) AS var_1d FROM indexed
),
blocks AS
(
SELECT
k,
intDiv(i, k) AS blk,
count() AS n,
sum(ret) AS block_ret
FROM indexed
CROSS JOIN (SELECT arrayJoin([2, 3, 5, 10, 21, 42, 63]) AS k) AS ks
GROUP BY k, blk
HAVING n = k
)
SELECT
concat(toString(k), ' sessions') AS block_length,
count() AS block_count,
round(varSamp(block_ret) / (k * any(var_1d)), 3) AS variance_ratio
FROM blocks
CROSS JOIN base
GROUP BY k
ORDER BY kTại 2 sessions, tỷ lệ đo được là 0.844; tại 63 sessions, tỷ lệ đo được là 0.584, được tính trên 78 khối không chồng lấn. Các giá trị gần 1 cho thấy tổng mức sinh lời tăng theo cách mà các quan sát độc lập sẽ tăng trong khoảng thời gian đó. Các giá trị lệch khỏi 1 cho thấy mối phụ thuộc vẫn còn tác động trong những khoảng thời gian tương ứng. Đây là phạm vi mà độ dài khối phải bao phủ. Hãy chọn khối ngắn nhất đáp ứng điều kiện đó, rồi xem số lượng khối còn lại là bao nhiêu.
Tái lấy mẫu theo nhóm và số quan sát bên cạnh mỗi khoảng
Tái lấy mẫu trong từng nhóm, theo chế độ biến động hoặc theo tháng dương lịch, giúp duy trì điều kiện khiến câu hỏi ban đầu trở nên đáng quan tâm. Nếu lập luận cho rằng một chiến lược đạt Sharpe trong các chế độ biến động cao, thì khoảng được xây dựng chỉ từ những ngày có biến động cao mới là phép kiểm định phù hợp. Đổi lại, quy mô mẫu bị thu hẹp. Chia 250 quan sát thành bốn nhóm chỉ còn khoảng 60 quan sát mỗi nhóm, và bootstrap với 60 quan sát tạo ra khoảng rộng gần gấp đôi so với mẫu đầy đủ.
Vì vậy, mỗi lần báo cáo một khoảng, hãy luôn ghi số quan sát của nhóm ngay bên cạnh khoảng đó. Cột block_count trong các bảng phía trên thể hiện rõ nguyên tắc này: phân vị 5 được tính từ chín cửa sổ là một đại lượng khác với phân vị 5 được tính từ hai trăm cửa sổ, ngay cả khi cả hai đều được hiển thị với hai chữ số thập phân.
Bootstrap không thể khắc phục điều gì
Bootstrap chỉ định lượng một yếu tố: nhiễu lấy mẫu trong một thống kê được tính từ dữ liệu bạn có. Nó không cho biết liệu dữ liệu đó có thể đạt được trong thực tế hay không.
Một backtest bị nhiễm thiên lệch nhìn trước tạo ra chuỗi lợi nhuận chưa từng có thể giao dịch, còn bootstrap của chuỗi đó chỉ trả về một khoảng hẹp và có vẻ đáng tin quanh một kết quả hư cấu. Một tập hợp tài sản được xây dựng từ các thành viên chỉ số hiện nay sẽ mang thiên lệch sống sót, và mọi lần lấy mẫu lại từ tập hợp đó đều kế thừa thiên lệch này. Khoảng trống thứ ba là hiệu ứng lựa chọn: chạy 200 biến thể, giữ lại biến thể tốt nhất, rồi khoảng bootstrap của biến thể đó chỉ mô tả nhiễu lấy mẫu của riêng biến thể này mà bỏ qua 199 kết quả đã được dùng để lựa chọn. Các khoảng trung thực rất hữu ích. Nhưng chúng không thể thay thế dữ liệu ngoài mẫu.
Ghi chú dữ liệu và phương pháp
- Lợi nhuận được tính từ giá đóng cửa đến giá đóng cửa trên dữ liệu bar ngày. Cổ tức không được tính, nên mức của mọi con số lợi nhuận và Sharpe đều bị đánh giá thấp khoảng bằng dividend yield. Độ phân tán được đề cập trong bài viết này hầu như không bị ảnh hưởng.
- Các cửa sổ và block không chồng lấn, nên mỗi thống kê đo lường sử dụng một phần lịch sử riêng biệt. Các cửa sổ chồng lấn sẽ làm tăng giả tạo số lượng mẫu.
- Các quantile sử dụng một estimator xác định, nên khi chạy lại một panel, kết quả trả về cùng một percentile thay vì một giá trị xấp xỉ mới.
- Panel autocorrelation sử dụng sáu mã vốn hóa lớn không có lần stock split nào trong cửa sổ quan sát, đồng thời loại bỏ mọi ngày có mức biến động vượt 35%, qua đó loại các sai lệch do điều chỉnh giá khỏi phép tương quan.
Câu hỏi thường gặp
Khoảng tin cậy bootstrap cho bạn biết gì về một backtest?
Khoảng này cho biết phạm vi các giá trị mà thống kê có thể nhận một cách hợp lý nếu cùng một quy trình được lấy mẫu lại trong cùng số kỳ. Khoảng 95% chứa số 0 có nghĩa là mẫu quá ngắn để phân biệt chiến lược với trường hợp hoàn toàn không có lợi thế.
Cần bao nhiêu lần lấy mẫu lại bằng bootstrap?
Với khoảng 95%, vài nghìn lần lấy mẫu lại thường đã đủ ổn định. 10.000 lần là thiết lập mặc định phổ biến và tốn ít chi phí tính toán. Các phân vị ở đuôi sâu cần nhiều lần hơn: phân vị thứ 1 của 1.000 lần rút mẫu được xác định từ khoảng mười giá trị.
Bootstrap khối di động nên sử dụng độ dài khối bao nhiêu?
Không có độ dài nào đúng trong mọi trường hợp. Quy tắc kinh nghiệm là chọn độ dài tỷ lệ với lũy thừa một phần ba của quy mô mẫu. Một cách kiểm tra thực tế là xem tại chân trời nào phương sai không còn tăng tuyến tính; panel tỷ số phương sai ở trên đo lường điều này. Hãy công bố độ dài đã sử dụng cùng với khoảng tin cậy.
Có thể dùng bootstrap để ước tính mức sụt giảm tối đa không?
Có, nhưng cần lưu ý về thứ tự. Mức sụt giảm phụ thuộc vào chuỗi lợi suất. Vì vậy, mẫu lại được tạo bằng cách xáo trộn sẽ cho mức sụt giảm của một lịch sử đã bị sắp xếp lại. Bootstrap khối giữ nguyên các chuỗi ngắn liên tiếp và phù hợp hơn với các thống kê phụ thuộc vào đường đi của lợi suất.
Bootstrap có điều chỉnh hiện tượng overfitting không?
Không. Phương pháp này đo lường nhiễu lấy mẫu trong một chuỗi lợi suất duy nhất. Look-ahead bias và ảnh hưởng lựa chọn phát sinh khi kiểm định nhiều biến thể đều nằm ngoài phạm vi mà bootstrap có thể nhận diện.
Mỗi panel ở đây đều kèm theo SQL đã tạo ra dữ liệu đó. Hãy thay ticker hoặc độ dài khoảng thời gian, rồi tự chạy trên terminal Strasmore.