Tuần trước bạn gửi tôi cuốn sổ ghi chép: cùng tín hiệu, cùng tập tài sản, cùng 14 tháng dữ liệu hợp đồng vĩnh cửu BTCUSDT. Chỉ có khâu thực thi là thay đổi. Bạn không còn vượt spread mà bắt đầu đặt bid chờ khớp thấp hơn 1 tick, và Sharpe tăng từ 0.42 lên 2.14. Bạn hỏi liệu kết quả đó có thật không, hay bạn đã làm hỏng thứ gì đó.
Bạn đã làm hỏng một thứ. Tôi muốn chỉ rõ chính xác ở đâu, vì lỗi chỉ nằm trong 1 dòng, còn bài học đằng sau nó lớn hơn dòng đó nhiều.
Đây là quy tắc khớp lệnh của bạn, sao chép từ engine:
if bar.low <= limit_price: fill(limit_price)
Quy tắc này nói rằng: nếu thị trường có giao dịch ở mức giá bằng hoặc thấp hơn giá của tôi trong phút đó, lệnh của tôi được khớp tại giá của tôi. Nhưng điều nó thực sự mã hóa là giá đã chạm mức của bạn. Chạm không đồng nghĩa với khớp. Giữa 2 việc đó là một hàng đợi, và bạn đã mô hình hóa hàng đợi như thể nó trống không.
Những gì đang đứng trước bạn
Khi bạn đặt bid ở mức 84,120.0 trên hợp đồng vĩnh cửu BTCUSDT, bạn nhập vào cuối hàng của tất cả lệnh đang chờ tại đó. Ở mức giá tốt nhất của hợp đồng này, quy mô hàng đợi thường dao động từ 4 đến 30 BTC tùy thời điểm trong ngày; trung vị trong khoảng mẫu của bạn vào khoảng 12 BTC. Lệnh của bạn là 0.4 BTC. Để lệnh của bạn khớp, bên bán phải đánh vào mức giá đó với tổng khối lượng đủ để khớp hết những người vào hàng trước bạn, và việc này phải xảy ra trước khi mức giá bị hủy hết ngay dưới chân bạn hoặc thị trường bật lên khỏi đó.
Vì vậy câu hỏi backtest của bạn cần đặt ra không phải là “giá có chạm 84,120.0 không” mà là “có ít nhất 12.4 BTC lệnh bán thị trường khớp tại 84,120.0 trong lúc lệnh của tôi nằm chờ ở đó không”. Đây là 2 sự kiện hoàn toàn khác nhau. Trong dữ liệu của bạn, chúng chênh nhau khoảng 3 lần.
3 quy tắc khớp lệnh, 3 chiến lược khác nhau
Tôi chạy lại tín hiệu của bạn với cùng điểm vào và 3 mô hình thực thi. Cùng alpha, cùng phí, cùng khoản funding. Chỉ có logic khớp lệnh thay đổi.
| Quy tắc khớp lệnh | Số lần khớp | Edge trung bình 60s sau khi khớp | Sharpe |
|---|---|---|---|
Chạm: low <= limit | 4,180 | +2.6 bp | 2.14 |
Xuyên hẳn: low < limit - 1 tick | 1,712 | +0.4 bp | 0.61 |
| Mô phỏng hàng đợi theo khối lượng tại mức giá | 1,306 | +0.9 bp | 0.77 |
Hàng giữa là cách sửa sơ sài mà ai cũng nghĩ đến đầu tiên: chỉ tính là khớp nếu thị trường giao dịch vượt hẳn qua giá của bạn, với lập luận rằng nếu giá đi qua mức đó thì hẳn đã khớp hết lệnh của bạn. Hướng suy luận này đúng và loại bỏ phần lớn ảo tưởng. Nhưng nó cũng gây ra một dạng sai lệch nghiêm trọng, tôi sẽ nói đến sau.
Hàng cuối là mô hình bạn nên xây dựng. Không cần nguồn dữ liệu L3 hay dựng lại từng lệnh. Bạn đã có gần đủ dữ liệu rồi.
Mô phỏng hàng đợi có thể xây từ aggTrades
Lấy luồng giao dịch tổng hợp thay vì kline. Mỗi giao dịch có giá, khối lượng, dấu thời gian và cờ phía maker cho biết bên chủ động là bên mua hay bên bán. Như vậy là đủ để mô phỏng tương đối ổn lệnh thụ động của chính bạn:
- Khi lệnh của bạn được đặt, chụp lại khối lượng đang chờ tại mức giá đó. Nếu bạn chỉ có nguồn dữ liệu sổ lệnh mỗi 100ms, hãy dùng ảnh chụp gần nhất; sai số này nhỏ so với sai số bạn đang sửa.
- Đặt
queue_ahead = resting_size. Hãy giả định theo hướng bi quan rằng bạn ở cuối hàng. Bạn sẽ ở đó, trừ khi chính bạn tạo ra mức giá này. - Duyệt luồng giao dịch. Mỗi giao dịch do bên bán chủ động khớp ở mức giá bằng hoặc thấp hơn giá của bạn sẽ giảm
queue_aheadđi một lượng tương ứng với khối lượng giao dịch. Khi giá trị này âm, lệnh của bạn được khớp tại giá của bạn, ở dấu thời gian đó. - Nếu giá dịch chuyển đi 1 tick, đừng đặt lại hàng đợi về 0. Hãy cho nó hao hụt dần. Một số lệnh đứng trước bạn bị hủy khi mức giá trở nên cũ, một số thì không. Mức hao hụt 30-40% cho mỗi giây trọn vẹn khi giá không còn chạm mức đó khớp với kết quả tôi dựng lại hơn cả 2 giả định cực đoan.
- Nếu chiến lược của bạn sẽ hủy lệnh rồi đặt lại, hãy mô hình hóa lệnh đặt lại như một lệnh mới ở cuối hàng tại mức giá mới. Đây là bước mọi người hay bỏ qua, và cũng là nơi ảo tưởng còn sót lại.
Bước 2 là chỗ bạn sẽ muốn tranh luận với tôi. Đúng, đôi khi bạn đứng gần đầu hàng vì đã đặt lệnh ngay khi mức giá vừa hình thành. Được thôi — hãy đo lường điều đó, đừng mặc định như vậy. Ghi lại khối lượng đang chờ tại thời điểm đặt lệnh và để dữ liệu cho bạn biết tỷ lệ lệnh thực sự vào hàng sớm. Trong chiến lược của bạn, tỷ lệ đó là 11%, vì tín hiệu phát ra sau một nhịp biến động, nghĩa là mức giá bạn tham gia đã hình thành và đã có đám đông chờ sẵn.
Những lệnh được khớp lại chính là những lệnh bạn ước mình đừng nhận
Giờ đến phần thực sự quan trọng, cũng là lý do quy tắc xuyên hẳn bị sai lệch.
Hãy nghĩ xem khi nào bid của bạn được khớp hết. Điều đó xảy ra khi áp lực bán đủ lớn để khớp hết toàn bộ khối lượng ở mức giá đó. Nói cách khác, theo đúng cách cấu thành của nó, đó là lúc thị trường đang đi xuống xuyên qua giá của bạn. Những lệnh bạn chắc chắn được khớp nhất cũng là những lệnh khiến vị thế của bạn lập tức bất lợi.
Chia các lệnh đã khớp theo cách chúng được khớp và đo mark-out sau 60 giây:
| Loại khớp lệnh | Tỷ trọng trong số lệnh đã khớp | Mark-out sau 60s |
|---|---|---|
| Giao dịch tại mức giá đó, giá bật lên | 38% | +3.1 bp |
| Giao dịch tại mức giá đó, giá đi ngang | 21% | +0.2 bp |
| Giá giao dịch xuyên qua từ 2 tick trở lên | 41% | −2.4 bp |
Backtest ngây thơ của bạn tính cả 3 nhóm và coi như khớp miễn phí. Quy tắc xuyên hẳn hầu như chỉ cho bạn nhóm thứ 3, vì vậy edge của nó sụt mạnh hơn cả mô phỏng hàng đợi. Cả 2 đều không đúng. Mô phỏng hàng đợi cho bạn một cơ cấu thực tế hơn, và chính cơ cấu đó quyết định tất cả: khớp thụ động giúp bạn hưởng spread nhưng khiến bạn chịu lựa chọn bất lợi; tỷ lệ giữa 2 yếu tố đó mới là chiến lược thực sự của bạn.
Cách nhìn từ bàn giao dịch cổ phiếu ngày trước vẫn đúng: lệnh maker là một quyền chọn miễn phí bạn viết ra cho thị trường. Ai đó thực hiện quyền chọn ấy khi nó có lợi cho họ. Backtest của bạn thu phí quyền chọn nhưng quên mất rằng quyền chọn cũng có phần chi trả.
Những giao dịch bạn không được khớp làm thay đổi chiến lược, chứ không chỉ chi phí
Đây là điều tôi muốn bạn suy ngẫm nhất. Khi mô hình hóa khớp taker không tốt, bạn vẫn có đúng các giao dịch nhưng ở sai mức giá, và phần lớn vấn đề sẽ được giải quyết bằng cách điều chỉnh phí. Khi mô hình hóa khớp maker không tốt, bạn có cả một tập hợp giao dịch sai hoàn toàn. Khoảng 2,900 trong số 4,180 điểm vào của bạn đã không bao giờ xảy ra. Một số trong đó là những tín hiệu tốt nhất, trên các cây nến tăng vọt rồi đảo chiều — đúng kiểu biến động khiến thị trường chạy mất mà bạn không tham gia được.
Vì thế nhánh xử lý khi lệnh không khớp cần có logic thực tế. Chiến lược làm gì khi lệnh vào chưa khớp mà tín hiệu đã hết hiệu lực? Đuổi theo bằng lệnh taker và trả spread cộng thêm tác động giá? Đặt lại thấp hơn và chấp nhận mức giá vào lệnh khác? Bỏ qua giao dịch và giữ vị thế bằng 0? Mỗi lựa chọn tạo ra đường vốn khác biệt đáng kể, và không lựa chọn nào có nghĩa là “giả định lệnh đã khớp”. Trong các lần chạy của chúng tôi, thêm quy tắc đuổi theo trung thực (vượt spread sau 20 giây chưa khớp, giới hạn trượt giá ở 3 bp) đã lấy lại khoảng 1/3 số giao dịch bị mất và khoảng một nửa chênh lệch Sharpe giữa mô hình ngây thơ và mô phỏng hàng đợi. Đây là một kết quả thực sự đáng chú ý, và nó chỉ xuất hiện khi mô hình khớp lệnh đủ thực tế để câu hỏi đó có ý nghĩa.
Kiểm tra nhanh tính hợp lý, trong 10 phút: lấy nhật ký giao dịch giả lập trực tiếp và backtest của bạn trong cùng khoảng thời gian. So sánh tỷ lệ khớp, không phải PnL. Nếu backtest khớp 100% lệnh chờ còn giao dịch giả lập chỉ khớp 34%, thì đây không phải là so sánh chiến lược; đó là lỗi mô hình khớp lệnh. Hãy sửa lỗi đó trước khi xem bất kỳ con số lợi nhuận nào.
Nhân tiện, còn 2 điểm nhỏ nữa
Lệnh post-only bị từ chối. Nếu bạn dùng post-only để đảm bảo mức phí maker và sổ lệnh thay đổi trong lúc chờ từ khi bạn quyết định đến khi sàn xác nhận, lệnh sẽ bị từ chối thay vì được đặt chờ. Trong nhật ký giao dịch giả lập của chúng tôi, tỷ lệ này là 3-6% số lần thử trên BTCUSDT vào giờ bình thường và vượt 15% trong phút sau khi Mỹ công bố chỉ số CPI. Lệnh bị từ chối không phải là lệnh đã khớp, cũng không phải lệnh chờ chưa khớp; đó là giao dịch chưa từng tồn tại. Nếu backtest không có trạng thái này, số giao dịch của bạn sẽ bị thổi phồng đúng vào những giai đoạn bạn quan tâm nhất.
Phòng tránh tự khớp và dấu chân lệnh của chính bạn. Với 0.4 BTC, bạn không làm dịch chuyển BTCUSDT, nên có thể bỏ qua điểm này. Nhưng bạn có nói muốn chạy chiến lược trên hợp đồng vĩnh cửu của altcoin vốn hóa trung bình, nơi giá trị danh nghĩa ở mức giá tốt nhất thường dưới $15k. Ở đó, lệnh của bạn có tỷ trọng đáng kể trong hàng đợi, và con số khối lượng đứng trước bạn chụp lại đã tính đến ảnh hưởng từ sự hiện diện của lệnh trước đó của bạn. Khi quy mô lệnh vượt khoảng 10% khối lượng đang chờ, mô phỏng cần tính đến việc những người tham gia khác phản ứng với bạn; và nói thật, đến lúc đó tôi tin giao dịch giả lập hơn bất kỳ mô phỏng nào bạn hay tôi có thể viết.
Hãy chạy lại bằng mô phỏng hàng đợi rồi gửi tôi bảng mark-out, chia theo loại khớp lệnh. Nếu nhóm giá bật lên vẫn giữ phần lớn edge và nhóm xuyên qua không ăn hết edge đó, có thể bạn có thứ đáng để đưa vào giao dịch giả lập. Nếu toàn bộ kết quả đến từ 2,900 lệnh bạn vốn không thể khớp được, thì tốt hơn hết hãy biết điều đó ngay bây giờ thay vì mất 4 tuần nhìn tài khoản giả lập không làm được như cuốn sổ ghi chép đã hứa hẹn.
← Tất cả bài viết


