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

Mô phỏng khớp lệnh: bậc thang từ ảo tưởng giá đóng cửa đến thực tế nhận biết hàng đợi

Mô phỏng khớp lệnh: bậc thang từ ảo tưởng giá đóng cửa đến thực tế nhận biết hàng đợi
#mô phỏng khớp lệnh
#backtest
#lệnh giới hạn
#vị trí hàng đợi
#khớp lệnh một phần
#market making
#thực thi lệnh
#cấu trúc vi mô thị trường

Backtest của bạn chứa hai mô hình: một mô hình về alpha và một mô hình về khớp lệnh. Hầu hết mọi người dành 95% công sức cho mô hình đầu tiên và kế thừa mô hình thứ hai từ bất kỳ framework nào họ tình cờ sử dụng. Điều này là ngược đời. Một tín hiệu tầm thường với mô hình khớp lệnh trung thực sẽ cho ra một ước tính PnL tầm thường nhưng thực. Một tín hiệu tuyệt vời với fill_price = candle.close cho ra một con số không phải là ước tính của bất cứ điều gì — nó là kết quả của một giả định mà bạn chưa bao giờ kiểm chứng.

Đối với các chiến lược taker trên các công cụ thanh khoản cao, mô hình khớp lệnh chỉ là một số hạng hiệu chỉnh. Nhưng với bất cứ thứ gì liên quan đến lệnh giới hạn nằm chờ — market making, vào lệnh thụ động, thu hoạch rebate post-only — mô hình khớp lệnh chính là chiến lược. Việc bạn có được khớp hay không, khi nào, với khối lượng bao nhiêu, và phụ thuộc vào biến động giá tiếp theo nào sẽ quyết định dấu của PnL, chứ không chỉ độ lớn của nó.

Trong bài Backtest-live parity chúng ta đã lập bản đồ toàn bộ phân loại các sai lệch giữa backtest và live, và xếp hạng sai lệch thực thi ở mức nghiêm trọng 5/5 — hạng tệ nhất. Bài viết đó đưa ra ba mức độ chính xác thô của mô phỏng khớp lệnh rồi chuyển sang chủ đề khác. Bài này là phần đào sâu: bậc thang đầy đủ, từng nấc một, với vòng đời lệnh như một máy trạng thái, các cận xác suất khớp lệnh mà bạn thực sự có thể tính toán, và một thí nghiệm đo lường cho thấy chính xác ở đâu trên bậc thang một chiến lược maker "có lãi" sẽ chết.

Bậc thang

Bậc thang năm nấc về độ trung thực của mô phỏng khớp lệnh

Mỗi nấc đòi hỏi nhiều dữ liệu hơn và nhiều code hơn, và mỗi nấc loại bỏ một sai lệch hệ thống cụ thể. Các nấc được sắp xếp theo những gì chúng làm sai, chứ không chỉ theo chi phí.

Nấc 0: khớp theo giá đóng cửa

fill_price = bar.close

Lệnh được khớp ngay lập tức, toàn bộ, tại giá đóng của thanh nến đã sinh ra tín hiệu. Tín hiệu được tính từ chính giá đóng đó, vì vậy bạn đang giao dịch trên dữ liệu chưa tồn tại tại thời điểm giá được in ra: đây là look-ahead bias khoác áo thực thi lệnh. Bất kỳ chiến lược nào có turnover đều trông ổn ở nấc này.

Nấc 1: mở cửa thanh tiếp theo

fill_price = next_bar.open

Đây là mô hình trung thực tối thiểu cho logic taker trên bar. Tín hiệu được tính trên bar tt, việc khớp lệnh diễn ra tại mức giá quan sát được đầu tiên của bar t+1t+1. Điều này loại bỏ look-ahead nhưng vẫn giả định spread bằng không, tác động thị trường bằng không, thanh khoản vô hạn tại giá mở, và chắc chắn khớp lệnh 100%. Đối với lệnh giới hạn, nó suy biến thành logic touch-fill (lý do đây là chất độc sẽ được nói rõ hơn bên dưới).

Nấc 2: spread + trượt giá cố định

half_spread = mid * spread_bps / 2e4
slip        = mid * slippage_bps / 1e4
fill_price  = mid + side * (half_spread + slip)   # side: +1 buy, -1 sell

Giờ đây mọi giao dịch taker phải trả nửa spread cộng với một hằng số được hiệu chỉnh. Đây là nấc đầu tiên mà một chiến lược turnover cao có thể chết trong backtest — và đó chính là mục đích. Sai số còn lại: trượt giá không phải là hằng số. Nó tỉ lệ với kích thước lệnh so với độ sâu hiển thị và bùng nổ chính xác vào lúc chiến lược của bạn muốn giao dịch nhất. Một con số 5 bps cố định là mức trung bình qua các chế độ thị trường; chiến lược của bạn không giao dịch ở chế độ trung bình.

Nấc 3: đi bộ qua độ sâu L2 cho lệnh thị trường

Với snapshot sổ lệnh, bạn không còn phải đoán trượt giá nữa mà tính toán nó. Một lệnh mua thị trường khối lượng QQ đi bộ qua bên ask từng mức một; giá khớp là trung bình gia quyền theo khối lượng qua các mức đã tiêu thụ:

Pfill(Q)=1Qipimin ⁣(qi,  Qj<iqj)P_{\text{fill}}(Q) = \frac{1}{Q}\sum_{i} p_i \cdot \min\!\left(q_i,\; Q - \textstyle\sum_{j<i} q_j\right)

def depth_walk(levels: list[tuple[float, float]], qty: float) -> tuple[float, float]:
    """levels: [(price, size), ...] sorted best-first. Returns (vwap, filled_qty)."""
    remaining, cost = qty, 0.0
    for price, size in levels:
        take = min(size, remaining)
        cost += take * price
        remaining -= take
        if remaining <= 0:
            break
    filled = qty - remaining
    return (cost / filled if filled > 0 else float("nan"), filled)

Hai hiệu chỉnh khiến mô hình này trung thực hơn đáng kể. Thứ nhất, độ trễ: đi bộ qua sổ lệnh như nó đứng tại thời điểm Δt\Delta t sau dấu thời gian ra quyết định của bạn, trong đó Δt\Delta t là độ trễ tín hiệu-đến-sàn đã đo được — sổ lệnh bạn nhìn thấy không phải là sổ lệnh bạn đánh vào. Thứ hai, khớp lệnh một phần theo giá thị trường: nếu độ sâu hiển thị trong phạm vi giá giới hạn của bạn nhỏ hơn QQ, mô hình phải trả về một khớp lệnh một phần và để lại phần dư nằm chờ, việc này chuyển vấn đề sang máy trạng thái bên dưới.

Sai số còn lại ở nấc 3 là tác động thị trường và bổ sung thanh khoản: bạn tiêu thụ sổ lệnh như một đối tượng tĩnh, nhưng sổ lệnh thực tế bổ sung một phần (và các đối tác thực tế phản ứng). Đối với khối lượng lệnh dưới vài phần trăm độ sâu top-of-book, sai số này nhỏ; đối với khối lượng lớn hơn, bạn cần một mô hình tác động thị trường (Almgren-Chriss 2001) đặt chồng lên trên.

Nấc 4: khớp lệnh xác suất theo vị trí hàng đợi cho lệnh giới hạn

Các nấc 0-3 trả lời câu hỏi "lệnh chủ động của tôi khớp ở giá nào." Nấc 4 trả lời câu hỏi khó hơn: lệnh thụ động của tôi có được khớp hay không — và đây là nấc duy nhất có thể định giá một chiến lược maker. Một lệnh giới hạn nằm chờ tại giá pp được khớp khi khối lượng giao dịch lũy kế tại pp vượt quá khối lượng hàng đợi đứng trước nó. Điều đó đòi hỏi phải theo dõi vị trí của bạn trong một hàng đợi FIFO mà bạn không thể quan sát trực tiếp.

Cỗ máy ước tính vị trí hàng đợi — vị trí ban đầu, quy tắc cập nhật khi có giao dịch so với hủy lệnh, họ xác suất f(x)f(x) để phân bổ các hủy lệnh không quan sát được — là nguyên tố mà chúng tôi đã xây dựng trong bài Queue inside the wall. Tôi sẽ không suy diễn lại nó ở đây; bộ mô phỏng sẽ tiêu thụ nó. Điều nấc 4 bổ sung là quy tắc quyết định khớp lệnh dựa trên ước tính đó, được trình bày trong phần xác suất khớp lệnh bên dưới.

Tầm quan trọng của nấc này được định lượng trong tài liệu học thuật: Moallemi và Yuan (2016), "A Model for Queue Position Valuation in a Limit Order Book," cho thấy rằng đối với các công cụ có tick lớn, giá trị kinh tế của một vị trí đầu hàng đợi so với cuối hàng đợi tương đương với nửa spread — cùng bậc độ lớn với toàn bộ lợi thế lý thuyết của một chiến lược market making. Một mô hình khớp lệnh bỏ qua vị trí hàng đợi không chỉ ước tính sai PnL của maker; nó ước tính PnL của một chiến lược hoàn toàn khác.

Có một nấc 5 — mô phỏng dựa trên agent đầy đủ nơi thị trường phản ứng với lệnh của bạn (các mô hình queue-reactive theo nghĩa của Huang, Lehalle và Rosenbaum 2015; các framework đa agent như ABIDES, Byrd et al. 2020). Việc phát lại lịch sử, dù có nhận biết hàng đợi, vẫn giả định rằng lệnh của bạn không thay đổi hành vi của bất kỳ ai khác. Giả định đó ổn ở quy mô nhà đầu tư cá nhân và ngày càng sai khi báo giá của bạn trở thành một phần đáng kể có thể nhìn thấy của mức giá đó. Nấc 5 nằm ngoài phạm vi ở đây; chỉ cần biết rằng bậc thang không dừng lại ở nấc 4.

Khớp lệnh một phần như một máy trạng thái

Các nấc 0-2 có thể giả vờ rằng một lệnh là một lời gọi hàm: gửi, nhận giá, xong. Từ nấc 3 trở lên, một lệnh là một quy trình có vòng đời, và bộ mô phỏng phải mô hình hóa nó như một máy trạng thái, nếu không nó sẽ âm thầm xử lý sai những trường hợp quan trọng nhất.

Máy trạng thái vòng đời lệnh với chú thích vị trí hàng đợi

enum OrderState {
    PendingNew,                                  // sent, not yet acked (latency window)
    Resting     { remaining: f64, q_ahead: f64 }, // in book, queue position estimated
    PartialFill { remaining: f64, q_ahead: f64 }, // some qty done, rest still queued
    PendingAmend,                                 // amend in flight
    PendingCancel,                                // cancel in flight
    Filled,
    Canceled    { filled_qty: f64 },              // may be partially filled at cancel time
    Rejected,
}

Các chuyển đổi trạng thái mang theo tính kinh tế:

  • PendingNewResting: lệnh gia nhập hàng đợi đứng sau mọi thứ đã hiện diện tại thời điểm ack, chứ không phải thời điểm ra quyết định. Vị trí hàng đợi của bạn được khởi tạo bằng khối lượng của mức giá tại thời điểm t+Δtackt + \Delta t_{\text{ack}}. Các bộ mô phỏng khởi tạo tại thời điểm ra quyết định sẽ hệ thống hóa việc đánh giá quá cao mức ưu tiên hàng đợi của bạn — chính xác bằng khối lượng đã đến trong cửa sổ độ trễ của bạn, tức là phần lớn khối lượng trong các đợt bùng nổ.
  • RestingPartialFill: một giao dịch tại mức giá của bạn lớn hơn hàng đợi phía trước sẽ khớp lệnh cho bạn một phần. Phần còn lại giữ vị trí (giờ đã là đầu hàng đợi) của nó. Khớp lệnh một phần không phải là nhiễu — chúng là thông tin: được khớp 0.3 trên 1.0 rồi chứng kiến giá bật ngược trở lại là một sự kiện PnL khác hẳn so với một lần khớp toàn bộ, và quá trình tồn kho của maker được xây dựng từ những mảnh vỡ này.
  • RestingPendingAmendResting: cái bẫy. Trên hầu như mọi sàn crypto, một lệnh amend là một cancel/replace — cancelReplace của Binance là atomic để chống việc khớp lệnh hai lần, nhưng trả về một mã lệnh mới ở cuối hàng đợi. Ngay cả các sàn có ngữ nghĩa modify gốc (CME Globex) cũng chỉ giữ ưu tiên thời gian đối với việc giảm khối lượng; thay đổi giá hoặc tăng khối lượng sẽ mất quyền ưu tiên đó. Vì vậy trong bộ mô phỏng: bất kỳ lần amend giá nào cũng reset q_ahead về toàn bộ khối lượng hiện tại của mức giá. Một engine báo giá tự re-peg mỗi 500ms không phải là "duy trì một báo giá" — nó liên tục tự đưa mình vào cuối hàng đợi, và hồ sơ khớp lệnh thực tế của nó gần như là adverse selection thuần túy. Kết hợp với kết quả của Moallemi-Yuan: việc re-quote có một cái giá, và cái giá đó chính là vị trí hàng đợi của bạn.
  • PendingCancelPartialFillCanceled: hủy lệnh cũng mất độ trễ. Trong khoảng thời gian giữa việc quyết định rút báo giá và lệnh hủy đến được matching engine, bạn vẫn có thể bị khớp lệnh — và đó là những lần khớp lệnh tệ nhất mà bạn từng nhận, bởi vì lý do bạn rút báo giá chính là thị trường sắp cán qua bạn. Một bộ mô phỏng không có độ trễ hủy lệnh sẽ xóa đi chính xác những lần khớp độc hại nhất khỏi lịch sử của bạn.

Máy trạng thái cũng là thứ khiến việc hạch toán của bộ mô phỏng trở nên trung thực: phí tích lũy theo từng sự kiện khớp lệnh, tồn kho cập nhật theo từng sự kiện khớp lệnh, và thống kê thời gian-trong-trạng-thái (lệnh nằm chờ bao lâu trước khi khớp so với bị hủy) trở nên có thể so sánh trực tiếp với log thực tế — đây chính là thứ nuôi vòng lặp hiệu chỉnh ở phần cuối bài.

Xác suất khớp lệnh giới hạn: ba mô hình và một khung

Cho một lệnh nằm chờ tại giá pp (giả sử là giá mua), khi nào bộ mô phỏng công bố một lần khớp? Ba quy tắc quyết định, theo mức độ trung thực tăng dần:

1. Touch-fill (ngây thơ). Khớp lệnh nếu giá chạm vào mức của bạn: lowtp\text{low}_t \le p. Đây là một quy tắc thời gian-qua-lần-đầu (first-passage-time), và thất bại của nó đã được đo lường từ một phần tư thế kỷ trước: Lo, MacKinlay và Zhang (2002), "Econometric Models of Limit-Order Executions" (Journal of Financial Economics 65), đã khớp các mô hình sống sót (survival models) với dữ liệu lệnh giới hạn thực tế và kết luận rằng các lần khớp lệnh giả định được xây dựng từ thời gian-qua-lần-đầu "là những đại diện rất kém cho các lần khớp lệnh giới hạn thực tế." Kiểu thất bại này mang tính cấu trúc: khi giá chạm vào mức của bạn rồi bật ngược, cú chạm đó đã tiêu thụ phần đầu của hàng đợi — những trader đã báo giá trước bạn. Touch-fill trao cho bạn những lần khớp của họ. Tệ hơn, nó trao cho bạn chính xác những lần khớp tốt (touch-and-bounce là kịch bản có lãi đối với một maker), trong khi tập khớp lệnh thực tế của bạn lại thiên về touch-and-run-through — những lần bị adverse selection.

2. Trade-through (cận bảo thủ). Chỉ khớp lệnh nếu giá giao dịch xuyên qua nghiêm ngặt mức của bạn: lowt<pϵ\text{low}_t < p - \epsilon, hoặc trên dữ liệu tick, khối lượng giao dịch lũy kế tại pp vượt quá toàn bộ mức giá đó. Nếu giá đã giao dịch xuyên qua, toàn bộ hàng đợi tại pp đã bị tiêu thụ, vì vậy bạn chắc chắn được khớp bất kể vị trí. Điều này không bao giờ trao cho bạn một lần khớp mà bạn lẽ ra không nhận được. Thiên lệch của nó là hình ảnh phản chiếu của touch-fill: nó từ chối mọi lần khớp mà hàng đợi đã cạn đến vị trí của bạn nhưng chưa xuyên thủng hoàn toàn, và những lần khớp mà nó cấp phát lại thiên lệch không cân xứng về phía run-through (bị adverse selection). Một backtest maker dưới trade-through là một bài kiểm tra căng thẳng, không phải một ước tính.

3. Ước tính cạn hàng đợi. Theo dõi khối lượng giao dịch lũy kế MtM_t tại giá của bạn từ tape giao dịch và khối lượng bị hủy CtC_t suy ra từ các delta L2. Hàng đợi phía trước ước tính của bạn:

Q^ahead(t)=max ⁣(Q0MtϕCt,  0)\hat{Q}_{\text{ahead}}(t) = \max\!\Big(Q_0 - M_t - \phi \cdot C_t,\; 0\Big)

trong đó Q0Q_0 là khối lượng của mức giá tại thời điểm (ack) lệnh được vào và ϕ[0,1]\phi \in [0,1] là tỉ lệ hủy lệnh giả định đến từ phía trước bạn — cái núm điều chỉnh mà dạng nguyên lý của nó (họ f(x)f(x)) được suy diễn trong bài viết về vị trí hàng đợi. Việc khớp lệnh bắt đầu khi Q^ahead=0\hat{Q}_{\text{ahead}} = 0; khối lượng được khớp của bạn là khối lượng giao dịch vượt quá mức đó, điều này tự nhiên tạo ra các lần khớp một phần:

filled(t)=min ⁣(S,  max(MtQ0+ϕCt,0))\text{filled}(t) = \min\!\Big(S,\; \max(M_t - Q_0 + \phi\, C_t,\, 0)\Big)

Đặt ϕ=1\phi = 1 (tất cả hủy lệnh đều ở phía trước bạn) cho ra cận lạc quan của mô hình này; ϕ=0\phi = 0 cho cận bi quan. Khi bạn chỉ có snapshot L2 thưa thớt và không có tape giao dịch tại mức giá đó — thường gặp với các feed crypto được throttle 100ms — bạn có thể quay về một prior dựa trên mô hình: Cont, Stoikov và Talreja (2010), "A Stochastic Model for Order Book Dynamics" (Operations Research 58), mô hình hóa mỗi mức giá như một hàng đợi sinh-diệt (birth-death queue) và tính toán, thông qua biến đổi Laplace, xác suất một lệnh tại giá mua được khớp trước khi mid di chuyển, có điều kiện theo kích thước hàng đợi hiện tại. Đó là một oracle xác suất khớp lệnh phân tích: thô so với việc phát lại tape, nhưng tốt hơn nhiều so với touch-fill, và đủ rẻ để đánh giá bên trong một vòng lặp backtest nóng.

Kỷ luật khung giới hạn

Ba quy tắc này không phải là đối thủ cạnh tranh — chúng là một trình tự sắp xếp:

fillstrade-through    fillsqueue    fillstouch\text{fills}_{\text{trade-through}} \;\subseteq\; \text{fills}_{\text{queue}} \;\subseteq\; \text{fills}_{\text{touch}}

điều này tạo ra một khung PnL. Chạy mọi backtest maker ba lần và báo cáo khoảng:

PnLtrade-through    PnLlivePnLtouch\text{PnL}_{\text{trade-through}} \;\le\; \text{PnL}_{\text{live}} \lesssim \text{PnL}_{\text{touch}}

(bất đẳng thức trên là xấp xỉ — touch-fill có thể xếp hạng sai các chiến lược, chứ không chỉ thổi phồng chúng, bởi vì nó trao cho bạn những lần khớp tốt phản-thực-tế). Quy tắc quyết định theo sau: một chiến lược maker chỉ có thể triển khai được nếu nó sống sót ở cận bảo thủ của khung, và khung đó đủ hẹp để ước tính điểm còn có ý nghĩa. Một chiến lược cho ra +9ktouch/9k touch / -3k trade-through có một khung rộng 12k;bnkho^ngbie^ˊtgıˋngoaˋivicgiảđịnhcabmo^phngđangla^ˊnaˊtalphacabn.Mtchie^ˊnlượcchora+12k; bạn không biết gì ngoài việc giả định của bộ mô phỏng đang lấn át alpha của bạn. Một chiến lược cho ra +2.1k / +$0.4k đang nói cho bạn biết điều gì đó thực.

Thí nghiệm: đưa một chiến lược maker xuống bậc thang

PnL của chiến lược maker sụp đổ từng nấc xuống bậc thang mô hình khớp lệnh

Lấy một chiến lược maker cố tình đơn giản — báo giá đối xứng tại giá mua/bán tốt nhất, khối lượng cố định 0.05 BTC, giới hạn tồn kho ở ±0.5 BTC với việc san bằng bằng taker tại giới hạn. Một tháng dữ liệu BTCUSDT perpetual: bar 1 phút cho các nấc thấp, diff L2 100ms cộng với tape giao dịch cho các nấc cao; phí maker 1.0 bps, taker 4.0 bps. Cùng một code tín hiệu ở mọi nấc (lõi dùng chung, vì vậy biến số duy nhất là mô hình khớp lệnh). Các con số dưới đây là một lần chạy đại diện của chúng tôi — độ lớn của bạn sẽ khác nhau tùy theo sàn, tháng, và quy mô; nhưng hình dạng thì không:

Nấc Mô hình khớp lệnh Tỉ lệ khớp báo giá Số lần khớp PnL tháng Kết luận
0 close-fill 98% 41,200 +$14,800 ảo tưởng
1 next-bar-open / touch trên bar 1m 89% 37,400 +$9,600 ảo tưởng có độ trễ
2 touch + spread & trượt giá cố định trên taker flattens 89% 37,400 +$7,100 chi phí đã mô hình hóa, khớp lệnh vẫn hư cấu
3 + L2 depth-walk trên taker flattens 89% 37,400 +$6,400 thoát lệnh trung thực, vào lệnh vẫn hư cấu
4a trade-through (bảo thủ) 21% 8,900 -$3,900 cận stress
4b queue-depletion, ϕ\phi đã hiệu chỉnh 37% 15,600 -$700 ước tính tốt nhất
4c queue-depletion, ϕ=1\phi = 1 (lạc quan) 44% 18,700 +$1,900 cận trên
chạy shadow thực tế, cùng tháng 35% 14,100 -$1,150 thực tế

Đọc bảng từ trên xuống dưới và theo dõi xem chiến lược chết ở đâu. Không phải ở nấc 2 — phí và trượt giá cắt đi 26% và chiến lược vẫn trông có lãi một cách vững chắc. Nó chết giữa nấc 3 và nấc 4, và nó chết vì một lý do mà không mô hình chi phí nào có thể nắm bắt được: lựa chọn khớp lệnh. Touch-fill trao 37,400 lần khớp mà phần lớn là touch-and-bounce — thu spread thuần túy. Mô hình nhận biết hàng đợi đã xóa 58% số lần khớp đó, và những lần khớp bị xóa lại thiên lệch không cân xứng về phía có lãi: khi mức giá bị chạm nhẹ, hàng đợi phía trước một maker re-quote với độ trễ retail hấp thụ mọi thứ. Những lần khớp còn sống sót đến nấc 4 thiên lệch về phía các đợt quét dọn sạch mức giá (level-clearing sweeps) — những lần khớp mà giá đã và đang di chuyển xuyên qua bạn. Tỉ lệ khớp giảm 2.4 lần; PnL đảo dấu. Sự bất cân xứng đó — mất đi những lần khớp tốt, giữ lại những lần khớp xấu — chính là adverse selection được thể hiện một cách máy móc rõ ràng, và nó vô hình ở mọi nấc dưới nấc 4.

Cũng cần lưu ý những gì khung giới hạn đã làm: [-3,900,+3,900, +1,900] bao trùm quanh số không với ước tính đã hiệu chỉnh ở -700vaˋthcte^ˊ700 và thực tế ở -1,150. Bộ mô phỏng không đóng đinh chính xác PnL thực tế đến từng đô la — nó nắm được dấu, độ lớn, và tỉ lệ khớp lệnh trong phạm vi 2 điểm phần trăm. Đó chính là mục đích của một mô hình khớp lệnh. Backtest ở nấc 1 đã sai lệch so với PnL thực tế tới $10,750 trên một chiến lược mà toàn bộ lợi thế gộp hàng tháng chỉ là vài nghìn đô la: sai số mô hình khớp lệnh lớn gấp khoảng 3 lần kích thước của alpha. Do đó luận điểm: mô hình khớp lệnh của bạn là một giả định lớn hơn cả alpha của bạn.

Một lưu ý cho các nấc thấp: nếu bạn buộc phải sống với dữ liệu bar (nấc 0-2), ít nhất hãy giải quyết sự mơ hồ trong bar bằng adaptive drill-down — đào sâu từ 1 phút xuống 1 giây/100ms/giao dịch khi các mức SL, TP, hoặc báo giá rơi vào trong phạm vi của bar. Drill-down khắc phục lỗi trình tự (mức nào bị chạm trước) nhưng không thể khắc phục lỗi hàng đợi; nó là phần bổ trợ về độ phân giải dữ liệu cho bài viết này, chứ không phải một thứ thay thế cho nấc 4.

Vòng lặp hiệu chỉnh: khép kín đối chiếu với khớp lệnh thực tế

Một bộ mô phỏng nấc 4 có các tham số tự do — ϕ\phi, độ trễ Δt\Delta t, độ trễ hủy lệnh, giả định bổ sung mức giá. Nếu không hiệu chỉnh, nó chỉ là một phỏng đoán có hình dạng khác. Vòng lặp biến nó thành một công cụ đo lường:

1. Ghi log mọi thứ trong môi trường live. Mọi sự kiện lệnh với dấu thời gian của sàn: gửi, ack, mỗi lần khớp một phần, ack của amend, ack của cancel. Cộng với trạng thái L2 tại thời điểm gửi lệnh. Đây là cùng một kỷ luật ghi log mà backtest-live parity yêu cầu cho DivergenceMonitor của nó — hiệu chỉnh mô hình khớp lệnh là lớp sâu nhất của monitor đó.

2. Phát lại cùng các lệnh qua bộ mô phỏng. Đưa dữ liệu thị trường đã ghi lại và các chỉ thị lệnh đã ghi lại (không phải các lần khớp) vào bộ mô phỏng. Giờ bạn có các cặp kết quả: với mỗi lệnh thực tế, một số phận được mô phỏng.

3. So sánh phân phối, không phải trung bình, theo từng nhóm. Một sự khớp tỉ lệ khớp toàn cục duy nhất có thể che giấu các sai số bù trừ nhau (quá lạc quan trong chế độ yên tĩnh, quá bi quan trong các đợt bùng nổ — bù trừ nhau thành "đã hiệu chỉnh"). Chia nhóm theo các yếu tố dẫn dắt:

import numpy as np
from scipy.stats import ks_2samp

def calibration_report(pairs, bucket_key):
    """pairs: [{'bucket':…, 'live_filled':bool, 'sim_filled':bool,
                'live_ttf':float|None, 'sim_ttf':float|None}, …]"""
    out = {}
    for b in sorted({p['bucket'] for p in pairs}):
        grp = [p for p in pairs if p['bucket'] == b]
        live_fr = np.mean([p['live_filled'] for p in grp])
        sim_fr  = np.mean([p['sim_filled']  for p in grp])
        live_ttf = [p['live_ttf'] for p in grp if p['live_ttf'] is not None]
        sim_ttf  = [p['sim_ttf']  for p in grp if p['sim_ttf']  is not None]
        ks = ks_2samp(live_ttf, sim_ttf) if len(live_ttf) > 20 and len(sim_ttf) > 20 else None
        out[b] = {
            'n': len(grp),
            'fill_rate_live': live_fr,
            'fill_rate_sim':  sim_fr,
            'fill_rate_gap':  sim_fr - live_fr,      # signed: + means sim optimistic
            'ttf_ks_pvalue':  ks.pvalue if ks else None,
        }
    return out

Hai thống kê cho mỗi nhóm: khoảng cách tỉ lệ khớp có dấu (mô phỏng trừ thực tế) và một kiểm định KS trên phân phối thời-gian-đến-khi-khớp trong số các lệnh đã khớp. So sánh thời-gian-đến-khi-khớp là điểm sắc bén nhất — một bộ mô phỏng có thể khớp tỉ lệ khớp lệnh nhưng khớp tại những thời điểm sai một cách hệ thống, điều này làm hỏng mọi thống kê tồn kho và adverse-selection ở downstream. Đây chính xác là bài học từ khung phân tích survival của Lo-MacKinlay-Zhang: việc thực thi lệnh là một bài toán thời-gian-đến-sự-kiện, vì vậy hãy kiểm chứng nó như vậy.

4. Khớp các núm điều chỉnh, theo thứ tự khả năng nhận dạng. Độ trễ trước tiên (đo trực tiếp từ dấu thời gian ack — không phải khớp mô hình). Sau đó ϕ\phi bằng cách tối thiểu hóa khoảng cách tỉ lệ khớp qua các nhóm độ sâu hàng đợi. Sau đó kiểm tra các nhóm chế độ biến động: một khoảng cách lạc quan dai dẳng tập trung ở các nhóm bùng nổ thường có nghĩa là bộ mô phỏng của bạn mô hình hóa thiếu độ trễ hủy lệnh độc hại hoặc bổ sung mức giá, chứ không phải ϕ\phi.

5. Chạy lại khung. Sau khi hiệu chỉnh, ước tính queue-depletion nên nằm bên trong khung gần với thực tế, và — bài kiểm tra chấp nhận thực sự — thứ hạng của các biến thể chiến lược dưới bộ mô phỏng nên khớp với thứ hạng của chúng trong chế độ shadow. Sau đó đóng băng các tham số và hiệu chỉnh lại theo lịch trình; động lực khớp lệnh trôi dạt cùng với các bậc phí của sàn, thay đổi tick-size, và quần thể HFT, và một ϕ\phi được hiệu chỉnh vào tháng Ba chỉ là một giả thuyết vào tháng Bảy.

Kỳ vọng hội tụ từ các lần chạy của chúng tôi: một bộ mô phỏng nấc 4 chưa hiệu chỉnh thường nằm trong khoảng ±10-15 điểm phần trăm so với tỉ lệ khớp thực tế; sau một lần hiệu chỉnh, ±3-5 điểm phần trăm, với p-value KS của thời-gian-đến-khi-khớp không còn bị bác bỏ một cách đồng loạt. Bạn sẽ không làm tốt hơn thế với việc phát lại lịch sử — phần dư còn lại là phản ứng của thị trường đối với bạn, đó là vấn đề của nấc 5.

Những điều cần ghi nhớ

  1. Gọi tên nấc thang của bạn. Mọi backtest đều đứng trên bậc thang này bất kể bạn có chọn nấc đó hay framework của bạn đã chọn thay bạn. Nếu bạn không thể gọi tên nấc của mô hình khớp lệnh và các thiên lệch đã biết của nó, con số PnL của bạn có sai số mà bạn chưa từng nhìn thấy.
  2. Taker có thể dừng ở nấc 3. Depth-walk cộng với độ trễ đo được định giá thực thi chủ động một cách trung thực ở quy mô retail. Hãy dành công sức tiết kiệm được cho chất lượng dữ liệu.
  3. Maker bắt đầu từ nấc 4. Dưới nấc đó, logic khớp lệnh giới hạn không phải là gần đúng — nó lựa chọn một tập khớp lệnh phản-thực-tế có độ lệch adverse-selection ngược lại. Các backtest touch-fill của chiến lược maker là bộ sinh chiến lược đáng tin cậy nhất khiến chúng chết ngay khi chạm vào production.
  4. Mô hình hóa vòng đời, không chỉ lần khớp. Khớp lệnh một phần, amend-reset-hàng-đợi, độ trễ hủy lệnh — máy trạng thái là nơi những lần khớp độc hại sinh sống, và những lần khớp độc hại là nơi PnL của maker chết đi.
  5. Báo cáo khung giới hạn. Các cận bảo thủ và lạc quan chỉ tốn thêm hai lần chạy backtest và biến "backtest của tôi nói +7k"thaˋnh"thcte^ˊna˘ˋmđa^uđoˊtrong[7k" thành "thực tế nằm đâu đó trong [-3.9k, +$1.9k]" — đó là một quyết định khác, và tốt hơn.
  6. Hiệu chỉnh đối chiếu với khớp lệnh thực tế theo nhóm. Một mô hình khớp lệnh chỉ được kiểm chứng ở mức tổng hợp là một mô hình khớp lệnh có sai số bù trừ ẩn giấu. Khoảng cách tỉ lệ khớp theo nhóm cộng với kiểm định KS thời-gian-đến-khi-khớp, hiệu chỉnh lại theo quý.

Các nấc của bậc thang này không phải là những phân cấp học thuật — mỗi nấc là một lời nói dối cụ thể mà backtest của bạn ngừng kể cho bạn nghe. Hãy leo lên cho đến khi những lời nói dối nhỏ hơn lợi thế của bạn.

Liên kết hữu ích

  1. Moallemi, C., Yuan, K. — A Model for Queue Position Valuation in a Limit Order Book (2016)
  2. Cont, R., Stoikov, S., Talreja, R. — A Stochastic Model for Order Book Dynamics, Operations Research 58(3), 549-563 (2010)
  3. Lo, A., MacKinlay, C., Zhang, J. — Econometric Models of Limit-Order Executions, Journal of Financial Economics 65(1), 31-71 (2002)
  4. Huang, W., Lehalle, C.-A., Rosenbaum, M. — Simulating and Analyzing Order Book Data: The Queue-Reactive Model, JASA 110(509), 107-122 (2015)
  5. Almgren, R., Chriss, N. — Optimal Execution of Portfolio Transactions (2001)
  6. Byrd, D., Hybinette, M., Balch, T. — ABIDES: Towards High-Fidelity Multi-Agent Market Simulation (2020)
  7. Binance API — Cancel-Replace order semantics
  8. CME Globex — Order modification and time priority rules

Citation

@article{soloviov2026fillsimulation,
  author = {Soloviov, Eugen},
  title = {Fill simulation: the ladder from close-price fantasy to queue-aware reality},
  year = {2026},
  url = {https://marketmaker.cc/blog/fill-simulation-partial-fills-backtest},
  description = {Five rungs of fill simulation fidelity — from close-price fills to probabilistic queue-position models. Partial fills as a state machine, limit-fill probability bounds as a PnL bracket, and a calibration loop against live fills.}
}
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.