Mở vị thế mua ở 84,120. Dừng lỗ tại 84,036, chốt lời tại 84,271. Nến của phút tiếp theo xuất hiện: mở cửa 84,118, cao nhất 84,290, thấp nhất 84,010, đóng cửa 84,240.
Cả hai mức đều nằm trong nến đó. Giá đã chạm mức dừng lỗ và cũng chạm mức chốt lời, nhưng 4 con số OHLC hoàn toàn không cho biết mức nào bị chạm trước. Dù vậy, backtest vẫn trả về một con số. Ở đâu đó trong vòng lặp, một dòng mã đã quyết định — có lẽ là dòng mà khi viết, bạn không nghĩ nó đang thể hiện một giả định mô hình hóa.
Đây là nguồn tạo ra hiệu suất ảo lớn nhất mà tôi thấy trong các chiến lược ngắn hạn, còn hơn cả phí và trượt giá, bởi nó chẳng giống một giả định. Nó trông như phần nối dây kỹ thuật.
Cách sai thứ nhất: để chuỗi if tự quyết định
Cấu trúc thường thấy:
if bar.high >= target:
exit(target, "tp")
elif bar.low <= stop:
exit(stop, "sl")
Không ai chủ ý chọn “chốt lời được xử lý trước dừng lỗ”. Phần kiểm tra chốt lời chỉ được viết trước vì đó là nhánh thuận lợi và cũng là điều bạn đang nghĩ đến. Đổi thứ tự 2 nhánh là đường vốn thay đổi; chỉ riêng điều đó cũng đủ cho thấy P&L của chiến lược một phần phụ thuộc vào trình soạn thảo của bạn.
Tôi chạy thử một chiến lược lướt sóng hồi quy về trung bình hết sức bình thường trên hợp đồng vĩnh cửu BTCUSDT, với dữ liệu nến 1-minute trong 3 tháng, 4,812 giao dịch, mức dừng lỗ 0.10% và chốt lời 0.18% so với giá vào lệnh. Tín hiệu giống nhau, phí giống nhau, chỉ đổi quy ước phân định khi cả hai mức cùng bị chạm.
Một phần tám số giao dịch quyết định toàn bộ kết quả. Đó là phép tính của một cặp dừng lỗ và chốt lời đặt gần nhau: những giao dịch mơ hồ là những giao dịch giá đi theo cả hai hướng, vốn là phần lớn các tình huống đáng quan tâm, và mỗi giao dịch có thể mang lại toàn bộ khoảng cách từ dừng lỗ đến chốt lời tùy theo kết quả tung đồng xu. 12.6% giao dịch × khoảng cách 0.28% tương đương 3.5% doanh số danh nghĩa gộp trên mỗi đơn vị mẫu, lớn hơn hẳn lợi thế thực tế của chiến lược.
Tỷ lệ này phụ thuộc vào kích thước nến so với khoảng cách giữa các mức lệnh, và nhanh chóng tăng khi bạn chuyển sang nến dài hơn. Cùng mức dừng lỗ và chốt lời, cùng tín hiệu, đổi cách lấy mẫu:
| Khoảng thời gian nến | Giao dịch có cả hai mức trong cùng một nến | Sharpe (chốt lời trước) |
|---|---|---|
| 1s | 0.3% | 0.91 |
| 1m | 12.6% | 2.31 |
| 5m | 34% | 3.60 |
| 15m | 49% | 4.42 |
| 1h | 71% | 5.88 |
Hãy xem bảng đó thực sự nói lên điều gì. Nến thô hơn khiến backtest có kết quả tốt hơn. Bản năng của mọi nhà nghiên cứu là cho rằng nến giờ mang tính thận trọng hơn: ít nhiễu hơn, ít bị khớp quá mức với vi cấu trúc thị trường hơn. Nhưng khi quy ước xử lý trong nến luôn có lợi cho bạn, nến thô hơn chỉ là một chiếc hộp lớn hơn, bên trong đó bạn được phép giả định mình gặp may. Với nến 1h, 7 trong 10 giao dịch hoàn toàn phụ thuộc vào quy ước. Backtest đó không kiểm tra một chiến lược; nó kiểm tra thứ tự của 2 câu lệnh `if` tới 3,400 lần.
Cách sai thứ hai: giả định lệnh dừng lỗ khớp đúng tại giá dừng
Giả sử bạn đã sửa thứ tự. Dừng lỗ được xử lý trước, bạn ghi nhận khoản lỗ đúng bằng 0.10% cộng phí taker, và cảm thấy mình đã nghiêm ngặt. Nhưng vẫn còn 2 điểm sai riêng biệt.
Điểm thứ nhất là lệnh dừng lỗ kích hoạt lệnh, chứ không đảm bảo khớp lệnh. Trên Binance USDⓈ-M, lệnh STOP_MARKET trở thành lệnh thị trường ngay khi điều kiện kích hoạt được đáp ứng, rồi khớp với bất cứ lượng thanh khoản nào trong sổ lệnh. Trong một phút yên ắng, trượt giá chỉ là 1 hoặc 2 bước giá. Nhưng trong chính phút kích hoạt lệnh dừng lỗ của bạn — phút có thân nến 40 điểm và một đợt thanh lý dây chuyền bên dưới — sổ lệnh mỏng đúng ở phía bạn đang giao dịch xuyên qua. Trong mẫu của tôi, khi đối chiếu các lần kích hoạt dừng lỗ với dữ liệu tick, giá khớp trung vị thấp hơn mức kích hoạt 1.4 bps, còn phân vị thứ 95 là 11 bps. Với mức dừng lỗ 10 bps, trường hợp ở đuôi phân phối khiến bạn mất thêm một phần mười rủi ro mà bạn tưởng mình đã xác định.
Điểm thứ hai tinh vi hơn và đặc thù với hợp đồng vĩnh cửu: giá nào kích hoạt lệnh. Binance mặc định dùng giá mark để kích hoạt lệnh dừng, mà giá mark được tính từ chỉ số cộng với basis đã làm mượt, chứ không dựa trên giao dịch cuối cùng tại sàn đó. Chuỗi OHLCV của bạn là giá giao dịch cuối cùng. Đây là 2 chuỗi khác nhau, và chúng lệch nhau nhiều nhất trong chính những sự kiện kích hoạt lệnh dừng.
| Giá giao dịch cuối (klines của bạn) | Giá mark (kích hoạt mặc định) | |
|---|---|---|
| Nguồn | giao dịch trên sàn này | chỉ số từ nhiều sàn + basis |
| Đặc điểm râu nến | toàn bộ biên độ biến động | được làm mượt mạnh |
| Mức chênh lệch điển hình | 1–3 bps khi yên ắng, 20–35 bps trong phút xảy ra thanh lý dây chuyền | |
| Hệ quả với backtest | lệnh dừng bị kích hoạt dù không nên, và ngược lại | |
Vậy nên, một râu nến 25 bps trên biểu đồ giá giao dịch cuối sẽ khiến backtest thoát lệnh, trong khi giá mark thực tế chưa bao giờ cách mức kích hoạt của bạn dưới 10 bps. Hoặc điều ngược lại xảy ra vào ngày chỉ số biến động còn sàn của bạn phản ứng chậm. Nếu đặt workingType thành CONTRACT_PRICE, ít nhất bạn sẽ căn chỉnh hành vi thực tế với dữ liệu, và đó thường là lựa chọn đúng cho nhà nghiên cứu, vì mô phỏng trung thực một điều kiện kích hoạt theo giá mark đồng nghĩa với việc phải đưa thêm một chuỗi giá xuyên suốt bộ máy khớp lệnh.
Phiên bản tôi nhớ rõ nhất: có người trong nhóm “cải thiện” chiến lược bằng cách đổi mức chốt lời từ 0.18% thành 0.21%. Sharpe tăng từ 2.3 lên 3.1. Không có lợi thế mới nào cả. Mức chốt lời chỉ đơn giản được dời ra ngoài phần dày đặc của phân phối râu nến 1-minute, nên ít giao dịch rơi vào nhóm mơ hồ hơn, nơi mã đang âm thầm trao chiến thắng cho chúng. Họ đã tối ưu hóa quy tắc phân định.
Cách sai thứ ba: luôn giả định trường hợp xấu nhất rồi gọi đó là thận trọng
Phản xạ đầu tiên là chọn kịch bản bi quan. Nếu cả hai mức đều bị chạm, tính là dừng lỗ. Xong, hết lạc quan, triển khai thôi.
Tôi từng làm vậy. Cách này tốt hơn lựa chọn còn lại nhưng vẫn sai, vì 2 lý do.
Nó loại bỏ cả những chiến lược vốn ổn. Cách xử lý bi quan trên 12.6% giao dịch khiến chiến lược này mất 2.1 điểm Sharpe so với mức 0.94 khi xử lý bằng dữ liệu tick. Nếu con số thực là 0.94 mà quy ước của bạn báo 0.18, bạn sẽ vứt bỏ ý tưởng đó rồi đi làm thứ gì kém hơn. Sự thận trọng sai lệch 2 điểm Sharpe không còn là thận trọng; đó là nhiễu khoác lên mình vẻ đạo đức.
Tệ hơn, nó làm sai lệch quá trình tối ưu hóa. Đưa một lượt quét tham số cho bộ tối ưu dùng quy tắc phân định bi quan, nó sẽ học cách tránh sự mơ hồ, vì mơ hồ giờ đồng nghĩa với bị phạt hoàn toàn. Nó sẽ tìm đến mức dừng lỗ rộng và mức chốt lời gần, hoặc chọn nến dài nơi 2 mức hiếm khi cùng xuất hiện, rồi đưa ra các tham số được chọn theo quy ước khớp lệnh của bạn thay vì theo thị trường. Đây là cùng một kiểu sai lệch như phiên bản ưu ái, nhưng trái dấu và cũng khó nhận ra không kém trên báo cáo hiệu suất.
Quy tắc kinh nghiệm chúng tôi dùng trước mọi thứ khác: nếu stop_distance + target_distance nhỏ hơn biên độ ở phân vị thứ 75 của khung thời gian nến, giả định về biến động trong nến ảnh hưởng đến P&L nhiều hơn tín hiệu của bạn. Hãy tính cả 2 con số. Chỉ mất 4 dòng mã, nhưng phép kiểm tra này đã kết thúc nhiều buổi đánh giá chiến lược hơn bất kỳ phép kiểm tra đơn lẻ nào khác.
Cách thực sự hiệu quả
Đường đi của giá bên trong nến là dữ liệu. Hãy tìm lấy nó, hoặc giới hạn sai số ở phần bạn không thể lấy được.
- Xử lý trên chuỗi dữ liệu chi tiết nhất bạn có. Dữ liệu aggTrades của Binance trong những phút cần thiết chỉ gồm vài trăm dòng và giải quyết dứt điểm câu hỏi: mức nào bị chạm trước, và giá nào khớp lệnh khi giá quét qua mức đó. Bạn không cần dữ liệu tick cho toàn bộ backtest, chỉ cần cho những nến mơ hồ. Trong mẫu của tôi, đó là 606 phút trên tổng số 129,600 phút. Tải lượng dữ liệu nhỏ, không cần xây dựng cả hạ tầng.
- Nếu không có dữ liệu tick, hãy giảm khung thời gian xuống 1 hoặc 2 bậc chỉ để xử lý lệnh thoát. Tín hiệu dựa trên 15m, lệnh thoát được xử lý trên nến 1s hoặc 1m. Tỷ lệ mơ hồ giảm từ 49% xuống còn một phần nhỏ của 1%, đủ thấp để có thể trung thực xem như không đáng kể.
- Luôn báo cáo khoảng kết quả. Chạy mỗi backtest 2 lần, với cách xử lý lạc quan và bi quan, rồi in cả 2 chỉ số Sharpe bên cạnh kết quả đã được xác định bằng dữ liệu. Độ chênh đó thể hiện mức bất định trong nến và cần xuất hiện trên báo cáo hiệu suất, bên cạnh khoảng tin cậy của chính Sharpe. Khi khoảng kết quả là 0.2–2.3, không kết luận nào nằm trong đó là đáng tin cậy.
- Theo dõi tỷ lệ mơ hồ như một chỉ số cốt lõi. Chỉ số này nằm ở đầu mỗi thẻ chiến lược của chúng tôi, cạnh số lượng giao dịch và doanh số giao dịch. Tỷ lệ trên khoảng 5% có nghĩa là logic thoát lệnh mới là thứ đang được kiểm tra, chứ không phải logic vào lệnh.
- Mô hình hóa riêng điều kiện kích hoạt và giá khớp. Dùng chuỗi giá mà sàn thực sự sử dụng để kích hoạt lệnh; tính giá khớp bằng giá kích hoạt cộng với mức trượt giá lấy ngẫu nhiên theo phân phối đã hiệu chỉnh từ dữ liệu giao dịch, chứ không dùng chính giá kích hoạt.
Cổ phiếu cũng gặp vấn đề tương tự, chỉ khác cách biểu hiện. Lệnh dừng tại 62.00 với một cổ phiếu mở cửa ở 58.40 sau khi giảm giá qua đêm sẽ không khớp ở 62.00; lệnh khớp ở một mức nào đó thấp hơn giá mở cửa. Backtest theo nến ngày ghi nhận trượt giá −$0.00 cho các trường hợp giá nhảy qua mức dừng sẽ sẵn sàng cho bạn biết rằng lớp lệnh dừng lỗ đã cải thiện mức sụt giảm tối đa. Thực tế không phải vậy. Nó chỉ chưa từng được kiểm chứng vào những ngày quan trọng. Cổ phiếu bị tạm ngừng giao dịch cũng vậy: lệnh dừng thực sự được khớp ở phiên đấu giá mở cửa trở lại, tại mức giá mà giá thấp nhất của nến không thể hiện.
Không có gì trong số này quá phức tạp. Chỉ cần nhận ra rằng một cây nến là bản tóm tắt, còn chiến lược dừng lỗ và chốt lời là một ván cược vào thứ tự của các sự kiện mà bản tóm tắt đã lược bỏ. Khi bộ máy giao dịch thử nghiệm cuối cùng chạy chiến lược trên dữ liệu thị trường trực tiếp, dữ liệu sẽ cho biết thứ tự đó, và chưa bao giờ nó quan tâm bạn viết nhánh nào của câu lệnh if trước.
← Tất cả bài viết


