← กลับไปยังบทความ
July 16, 2026
อ่าน 5 นาที

การจำลอง Fill: บันไดจากภาพลวงตาราคาปิดสู่ความจริงที่รับรู้คิว

การจำลอง Fill: บันไดจากภาพลวงตาราคาปิดสู่ความจริงที่รับรู้คิว
#การจำลอง fill
#backtest
#limit orders
#queue position
#partial fills
#market making
#execution
#market microstructure

Backtest ของคุณประกอบด้วยสองโมเดล คือโมเดลของ alpha และโมเดลของ fill คนส่วนใหญ่ทุ่มความพยายาม 95% ให้กับอย่างแรก แล้วรับอย่างที่สองมาจากเฟรมเวิร์กที่บังเอิญใช้อยู่โดยไม่ตั้งคำถาม นี่คือการทำสิ่งต่างๆ กลับหัวกลับหาง สัญญาณธรรมดาๆ ที่มาพร้อมโมเดล fill ที่ซื่อสัตย์ จะให้ค่าประมาณ PnL ที่ธรรมดาแต่ เป็นจริง สัญญาณที่ยอดเยี่ยมแต่ใช้ fill_price = candle.close จะให้ตัวเลขที่ไม่ใช่ค่าประมาณของอะไรเลย มันคือผลลัพธ์ของสมมติฐานที่คุณไม่เคยตรวจสอบ

สำหรับกลยุทธ์ taker บนสินทรัพย์ที่มีสภาพคล่องสูง โมเดล fill เป็นแค่พจน์ปรับแก้ (correction term) แต่สำหรับสิ่งใดก็ตามที่เกี่ยวข้องกับออเดอร์ limit ที่วางรอ (resting) ไม่ว่าจะเป็น market making, การเข้าแบบ passive, หรือการเก็บ rebate จาก post-only โมเดล fill คือ ตัวกลยุทธ์เอง คุณจะได้ fill หรือไม่ เมื่อไหร่ ในปริมาณเท่าไหร่ และขึ้นอยู่กับการเคลื่อนไหวของราคาหลังจากนั้นอย่างไร สิ่งเหล่านี้กำหนดเครื่องหมาย (sign) ของ PnL ไม่ใช่แค่ขนาดของมัน

ใน Backtest-live parity เราได้แจกแจงอนุกรมวิธานทั้งหมดของความคลาดเคลื่อนระหว่าง backtest กับการเทรดจริง และให้คะแนนความคลาดเคลื่อนด้าน execution ที่ระดับความรุนแรง 5/5 ซึ่งเป็นระดับที่แย่ที่สุด บทความนั้นให้ระดับความแม่นยำของการจำลอง fill แบบหยาบๆ สามระดับแล้วก็ผ่านไป บทความนี้คือภาคลึกที่ตามมา บันไดฉบับเต็ม ทีละขั้น พร้อมวงจรชีวิตของออเดอร์ในรูปแบบ state machine ขอบเขตความน่าจะเป็นของการ fill ที่คุณคำนวณได้จริง และการทดลองที่วัดผลจริงซึ่งแสดงให้เห็นอย่างชัดเจนว่ากลยุทธ์ maker ที่ "ทำกำไรได้" ตายลงที่ขั้นไหนของบันได

บันได

บันไดความสมจริงของการจำลอง fill ห้าขั้น

แต่ละขั้นต้องการข้อมูลและโค้ดมากขึ้น และแต่ละขั้นก็กำจัดอคติเชิงระบบ (systematic bias) เฉพาะอย่างออกไป บันไดถูกเรียงลำดับตามสิ่งที่มันทำผิด ไม่ใช่แค่ตามต้นทุน

ขั้นที่ 0: fill ที่ราคาปิด

fill_price = bar.close

ออเดอร์ fill ทันที เต็มจำนวน ที่ราคาปิดของแท่งที่สร้างสัญญาณนั้นขึ้นมา สัญญาณถูกคำนวณจากราคาปิดเดียวกันนั้นเอง ดังนั้นคุณกำลังเทรดบนข้อมูลที่ยังไม่มีอยู่จริงตอนที่ราคาถูกพิมพ์ออกมา นี่คือ look-ahead bias ที่สวมหน้ากาก execution กลยุทธ์ที่มี turnover สูงจะดูดีเสมอในขั้นนี้

ขั้นที่ 1: เปิดของแท่งถัดไป

fill_price = next_bar.open

โมเดลที่ซื่อสัตย์ขั้นต่ำสุดสำหรับตรรกะ taker บนแท่งราคา สัญญาณถูกคำนวณที่แท่ง tt การ fill เกิดขึ้นที่ราคาแรกที่สังเกตได้ของแท่ง t+1t+1 วิธีนี้กำจัด look-ahead ออกไป แต่ยังคงสมมติว่า spread เป็นศูนย์ impact เป็นศูนย์ สภาพคล่องไม่จำกัดที่ราคาเปิด และความแน่นอนของการ fill อยู่ที่ 100% สำหรับออเดอร์ limit มันจะเสื่อมลงเป็นตรรกะ touch-fill (จะอธิบายว่าทำไมสิ่งนี้เป็นพิษในภายหลัง)

ขั้นที่ 2: spread + slippage คงที่

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

ตอนนี้ทุกการเทรดแบบ taker ต้องจ่ายครึ่งหนึ่งของ spread บวกกับค่าคงที่ที่คาลิเบรตไว้ นี่คือขั้นแรกที่กลยุทธ์ turnover สูงอาจ ตาย ใน backtest ซึ่งนั่นคือประเด็นสำคัญ ความคลาดเคลื่อนที่เหลืออยู่คือ slippage ไม่ใช่ค่าคงที่ มันแปรผันตามขนาดออเดอร์เทียบกับความลึก (depth) ที่แสดง และมันจะพุ่งสูงขึ้นพอดีตอนที่กลยุทธ์ของคุณอยากเทรดมากที่สุด ค่าคงที่ 5 bps เป็นเพียงค่าเฉลี่ยข้ามหลาย regime กลยุทธ์ของคุณไม่ได้เทรดใน regime ค่าเฉลี่ยนั้น

ขั้นที่ 3: L2 depth-walk สำหรับ market order

เมื่อมี order book snapshot คุณจะเลิกเดา slippage แล้วคำนวณมันแทน คำสั่ง market buy ขนาด QQ จะไล่เดินไปตามฝั่ง ask ทีละระดับ ราคา fill คือค่าเฉลี่ยถ่วงน้ำหนักด้วยปริมาณ (volume-weighted average) ข้ามระดับที่ถูกกินไป:

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)

การปรับแก้สองอย่างทำให้สิ่งนี้ซื่อสัตย์ขึ้นอย่างมีนัยสำคัญ อย่างแรกคือ latency: ไล่เดินไปตาม book ตามที่มันเป็นอยู่ Δt\Delta t หลังจาก timestamp ที่คุณตัดสินใจ โดย Δt\Delta t คือ latency ที่วัดได้จริงจากสัญญาณไปถึง exchange — book ที่คุณเห็นไม่ใช่ book ที่คุณโดนตี อย่างที่สองคือ partial marketable fill: ถ้าความลึกที่แสดงภายในราคา limit-through ของคุณน้อยกว่า QQ โมเดลต้องคืนค่าเป็น partial fill และเหลือส่วนที่ยังรอ (resting remainder) ซึ่งจะส่งต่อปัญหาไปยัง state machine ด้านล่าง

ความคลาดเคลื่อนที่เหลืออยู่ในขั้นที่ 3 คือ impact และการเติมใหม่ (refill) คุณกินเข้าไปในตัว book ราวกับมันเป็นวัตถุนิ่ง แต่ book จริงจะเติมกลับบางส่วน (และคู่ค้าจริงก็มีปฏิกิริยาตอบสนอง) สำหรับขนาด clip ที่ต่ำกว่าไม่กี่เปอร์เซ็นต์ของความลึกที่ top-of-book ความคลาดเคลื่อนนี้มีขนาดเล็ก สำหรับ clip ที่ใหญ่กว่านั้นคุณต้องมีโมเดล impact (Almgren-Chriss 2001) ซ้อนทับเข้าไปด้วย

ขั้นที่ 4: การ fill ออเดอร์ limit แบบความน่าจะเป็นตามตำแหน่งในคิว

ขั้นที่ 0-3 ตอบคำถามว่า "ออเดอร์ที่ aggressive ของฉันจะ fill ที่ราคาเท่าไหร่" ขั้นที่ 4 ตอบคำถามที่ยากกว่า: ออเดอร์ passive ของฉันจะ fill หรือไม่เลย — และนี่เป็นขั้นเดียวที่สามารถประเมินราคากลยุทธ์ maker ได้จริง ออเดอร์ limit ที่วางรออยู่ที่ราคา pp จะ fill เมื่อปริมาณการเทรดสะสมที่ราคา pp เกินกว่าปริมาณในคิวที่อยู่ข้างหน้ามัน ซึ่งต้องอาศัยการติดตามตำแหน่งของคุณในคิวแบบ FIFO ที่คุณสังเกตโดยตรงไม่ได้

กลไกการประมาณตำแหน่งในคิว — ตำแหน่งเริ่มต้น กฎการอัปเดตเมื่อมีเทรดกับการยกเลิก ตระกูลฟังก์ชันความน่าจะเป็น f(x)f(x) สำหรับการจัดสรรการยกเลิกที่สังเกตไม่ได้ — คือหน่วยพื้นฐานที่เราสร้างไว้ใน Queue inside the wall ผมจะไม่อนุพันธ์ซ้ำที่นี่ ตัวจำลองใช้มันเป็นข้อมูลนำเข้า สิ่งที่ขั้นที่ 4 เพิ่มเข้ามาคือ กฎการตัดสินใจ fill ที่วางซ้อนบนค่าประมาณนั้น ซึ่งครอบคลุมในหัวข้อความน่าจะเป็นของการ fill ด้านล่าง

เหตุผลว่าทำไมขั้นนี้ถึงสำคัญถูกวัดผลไว้ในวรรณกรรมวิชาการ: Moallemi and Yuan (2016), "A Model for Queue Position Valuation in a Limit Order Book," แสดงให้เห็นว่าสำหรับสินทรัพย์ที่มี tick ใหญ่ มูลค่าทางเศรษฐกิจของตำแหน่งหน้าคิวเทียบกับท้ายคิวนั้นเทียบเท่ากับครึ่งหนึ่งของ spread — ขนาดเดียวกันกับ edge ทางทฤษฎีทั้งหมดของกลยุทธ์ market making เลยทีเดียว โมเดล fill ที่ละเลยตำแหน่งในคิวไม่ได้แค่ประเมิน PnL ของ maker ผิดพลาด มันกำลังประเมิน PnL ของกลยุทธ์คนละแบบไปเลย

ยังมีขั้นที่ 5 อยู่ด้วย นั่นคือการจำลองแบบ agent-based เต็มรูปแบบ ที่ตลาด ตอบสนอง ต่อออเดอร์ของคุณ (queue-reactive model ในความหมายของ Huang, Lehalle and Rosenbaum 2015; เฟรมเวิร์กแบบ multi-agent เช่น ABIDES, Byrd et al. 2020) การ replay ข้อมูลย้อนหลัง แม้จะรับรู้คิวแล้วก็ตาม ยังคงสมมติว่าออเดอร์ของคุณไม่เปลี่ยนแปลงพฤติกรรมของคนอื่นเลย สมมติฐานนั้นใช้ได้ดีในขนาด retail แต่จะยิ่งผิดมากขึ้นเรื่อยๆ เมื่อ quote ของคุณกลายเป็นสัดส่วนที่มองเห็นได้ของระดับราคานั้น ขั้นที่ 5 อยู่นอกขอบเขตของบทความนี้ แต่ควรรู้ไว้ว่าบันไดไม่ได้จบแค่ขั้นที่ 4

Partial fill ในรูปแบบ state machine

ขั้นที่ 0-2 สามารถแสร้งทำเป็นว่าออเดอร์คือการเรียกฟังก์ชัน: ส่ง รับราคา จบ ตั้งแต่ขั้นที่ 3 ขึ้นไป ออเดอร์คือ กระบวนการ ที่มีวงจรชีวิต และตัวจำลองต้องสร้างแบบจำลองมันในรูปแบบ state machine ไม่เช่นนั้นมันจะจัดการกรณีที่สำคัญที่สุดผิดพลาดไปโดยไม่รู้ตัว

State machine ของวงจรชีวิตออเดอร์พร้อมคำอธิบายตำแหน่งในคิว

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,
}

การเปลี่ยนสถานะ (transition) เหล่านี้แบกรับตรรกะทางเศรษฐศาสตร์ไว้:

  • PendingNewResting: ออเดอร์เข้าคิว ต่อท้าย ทุกอย่างที่มีอยู่แล้วในเวลาที่ได้รับ ack ไม่ใช่ในเวลาที่ตัดสินใจ ตำแหน่งในคิวของคุณถูกกำหนดเริ่มต้นด้วยปริมาณของระดับราคา ณ เวลา t+Δtackt + \Delta t_{\text{ack}} ตัวจำลองที่กำหนดตำแหน่งเริ่มต้น ณ เวลาตัดสินใจจะประเมินความสำคัญของตำแหน่งในคิว (queue priority) สูงเกินไปอย่างเป็นระบบ — สูงเกินไปพอดีเท่ากับปริมาณที่เข้ามาในช่วง latency window ของคุณ ซึ่งเป็นปริมาณส่วนใหญ่ในช่วงที่ตลาดคึกคัก (burst)
  • RestingPartialFill: เทรดที่ระดับราคาของคุณซึ่งใหญ่กว่าคิวที่อยู่ข้างหน้าจะทำให้คุณได้ fill บางส่วน ส่วนที่เหลือจะคงตำแหน่งของมันไว้ (ตอนนี้อยู่หน้าคิวแล้ว) Partial fill ไม่ใช่สัญญาณรบกวน — มันคือ ข้อมูล การได้ fill 0.3 จาก 1.0 แล้วเห็นราคาสะท้อนหนีไป เป็นเหตุการณ์ PnL ที่แตกต่างจากการ fill เต็มจำนวน และกระบวนการ inventory ของ maker ถูกสร้างขึ้นจากเศษเสี้ยวเหล่านี้
  • RestingPendingAmendResting: กับดัก บนแทบทุกแพลตฟอร์มคริปโต การ amend คือ cancel/replace — cancelReplace ของ Binance เป็น atomic เพื่อป้องกันการ execute ซ้ำ แต่มันคืน order ID ใหม่ที่ท้ายคิว แม้แต่แพลตฟอร์มที่มี native modify semantics (CME Globex) ก็รักษา time priority ไว้เฉพาะสำหรับ การลดปริมาณ เท่านั้น การเปลี่ยนราคาหรือเพิ่มปริมาณจะเสียสิทธิ์นั้นไป ดังนั้นในตัวจำลอง: การ amend ราคาใดๆ ต้อง reset q_ahead ให้เป็นปริมาณเต็มของระดับราคาปัจจุบัน เอนจิ้น quoting ที่ re-peg ทุก 500ms ไม่ได้กำลัง "รักษา quote" อยู่ มันกำลังเข้าคิวใหม่ที่ท้ายคิวอยู่ตลอดเวลา และโปรไฟล์การ fill ที่สมจริงของมันเกือบจะเป็น adverse selection ล้วนๆ เมื่อรวมกับผลลัพธ์ของ Moallemi-Yuan: การ re-quote มีราคาของมัน และราคานั้นคือตำแหน่งในคิวของคุณ
  • PendingCancelPartialFillCanceled: การยกเลิกก็มี latency เช่นกัน ในช่วงเวลาระหว่างการตัดสินใจถอน quote จนถึงการที่คำสั่งยกเลิกไปถึง matching engine คุณยังคงได้ fill ได้ — และการ fill เหล่านั้นคือการ fill ที่ แย่ที่สุด ที่คุณจะได้รับ เพราะเหตุผลที่คุณกำลังถอน quote ก็คือตลาดกำลังจะวิ่งทับคุณ ตัวจำลองที่ไม่มี cancel latency จะลบการ fill ที่เป็นพิษที่สุดออกจากประวัติของคุณไปพอดี

State machine ยังเป็นตัวที่ทำให้การบัญชีของตัวจำลองซื่อสัตย์: ค่าธรรมเนียมเกิดขึ้นต่อเหตุการณ์ fill inventory อัปเดตต่อเหตุการณ์ fill และสถิติเวลาในแต่ละสถานะ (ออเดอร์รอนานแค่ไหนก่อนจะ fill เทียบกับถูกยกเลิก) กลายเป็นสิ่งที่เปรียบเทียบได้โดยตรงกับ log จริง ซึ่งเป็นสิ่งที่ลูปการคาลิเบรตในตอนท้ายใช้เป็นอาหาร

ความน่าจะเป็นของการ fill ออเดอร์ limit: สามโมเดลและกรอบขอบเขต

เมื่อมีออเดอร์วางรออยู่ที่ราคา pp (สมมติว่าเป็น bid) ตัวจำลองจะประกาศว่า fill เมื่อไหร่ กฎการตัดสินใจสามแบบ เรียงตามความซื่อสัตย์ที่เพิ่มขึ้น:

1. Touch-fill (แบบไร้เดียงสา) Fill เมื่อราคาแตะระดับของคุณ: lowtp\text{low}_t \le p นี่คือกฎแบบ first-passage-time และความล้มเหลวของมันถูกวัดไว้ตั้งแต่หนึ่งในสี่ศตวรรษที่แล้ว: Lo, MacKinlay and Zhang (2002), "Econometric Models of Limit-Order Executions" (Journal of Financial Economics 65) ใช้โมเดล survival กับข้อมูลออเดอร์ limit จริง และสรุปว่าการ execution ตามสมมติฐานที่สร้างจาก first-passage time นั้น "เป็นตัวแทนที่แย่มากสำหรับการ execution ของออเดอร์ limit จริง" รูปแบบความล้มเหลวนี้เป็นเชิงโครงสร้าง: เมื่อราคาแตะระดับของคุณแล้วสะท้อนกลับ การแตะนั้นกินคิวที่ อยู่หน้า — เทรดเดอร์ที่ตั้ง quote ไว้ก่อนคุณ Touch-fill มอบ fill ของพวกเขาให้คุณ ที่แย่ไปกว่านั้นคือมันมอบพอดีการ fill ที่ ดี (touch-and-bounce คือสถานการณ์ที่ทำกำไรให้ maker) ในขณะที่ชุดการ fill จริงของคุณจะเอียงไปทาง touch-and-run-through — พวกที่โดน adverse selection

2. Trade-through (ขอบเขตแบบระมัดระวัง) Fill เฉพาะเมื่อราคาเทรดผ่านระดับของคุณอย่างชัดเจน: lowt<pϵ\text{low}_t < p - \epsilon หรือในข้อมูลระดับ tick ปริมาณเทรดสะสมที่ pp เกินกว่าปริมาณของ ทั้งระดับ ถ้าราคาเทรดผ่าน คิวทั้งหมดที่ pp ก็ถูกกินหมด ดังนั้นคุณได้ fill แน่นอนไม่ว่าตำแหน่งของคุณจะอยู่ตรงไหน วิธีนี้ไม่มีทางมอบ fill ที่คุณไม่ควรจะได้รับ อคติของมันเป็นภาพสะท้อนตรงข้ามกับ touch-fill: มันปฏิเสธทุก fill ที่คิวหมดลงจนถึงตำแหน่งของคุณโดยไม่มีการเจาะทะลุเต็มที่ และ fill ที่มันให้มาก็เอียงไปทางพวก run-through (โดน adverse selection) อย่างไม่สมส่วน Backtest ของ maker ภายใต้ trade-through คือการทดสอบความเครียด (stress test) ไม่ใช่ค่าประมาณ

3. ค่าประมาณการหมดคิว (queue-depletion) ติดตามปริมาณเทรดสะสม MtM_t ที่ราคาของคุณจาก trade tape และปริมาณที่ยกเลิก CtC_t ที่อนุมานได้จาก L2 delta คิวโดยประมาณที่อยู่ข้างหน้าคุณ:

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

โดยที่ Q0Q_0 คือปริมาณของระดับราคา ณ เวลาที่ออเดอร์เข้า (ack-time) และ ϕ[0,1]\phi \in [0,1] คือสัดส่วนของการยกเลิกที่สมมติว่ามาจากด้านหน้าของคุณ — ปุ่มปรับที่รูปแบบหลักการของมัน (ตระกูล f(x)f(x)) ถูกอนุพันธ์ไว้ในบทความเรื่องตำแหน่งในคิว การ fill เริ่มต้นเมื่อ Q^ahead=0\hat{Q}_{\text{ahead}} = 0 ปริมาณที่คุณได้ fill คือปริมาณเทรดที่เกินกว่านั้น ซึ่งจะให้ partial fill ออกมาตามธรรมชาติ:

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

การตั้ง ϕ=1\phi = 1 (การยกเลิกทั้งหมดอยู่หน้าคุณ) ให้ขอบที่เป็นมองโลกในแง่ดีของโมเดลนี้ ส่วน ϕ=0\phi = 0 ให้ขอบที่มองโลกในแง่ร้าย เมื่อคุณมีเพียง L2 snapshot แบบเบาบางและไม่มี trade tape ที่ระดับราคานั้น — ซึ่งพบได้บ่อยกับฟีดคริปโตที่ถูก throttle ที่ 100ms — คุณสามารถพึ่งพา prior ที่อิงโมเดลได้: Cont, Stoikov and Talreja (2010), "A Stochastic Model for Order Book Dynamics" (Operations Research 58) สร้างแบบจำลองแต่ละระดับราคาเป็นคิวแบบ birth-death และคำนวณ ผ่าน Laplace transform ความน่าจะเป็นที่ออเดอร์ที่ bid จะ execute ก่อนที่ mid จะเคลื่อนที่ โดยมีเงื่อนไขจากขนาดคิวปัจจุบัน มันคือ oracle ความน่าจะเป็นของการ fill เชิงวิเคราะห์: หยาบเมื่อเทียบกับการ replay tape แต่ดีกว่า touch-fill มาก และคำนวณได้ถูกพอที่จะประเมินภายใน backtest loop ที่ทำงานหนัก

วินัยของกรอบขอบเขต (bracket)

กฎทั้งสามนี้ไม่ใช่คู่แข่งกัน — พวกมันคือลำดับชั้น:

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

ซึ่งก่อให้เกิดกรอบขอบเขต PnL รัน backtest ของ maker ทุกตัว สามครั้ง และรายงานเป็นช่วง:

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

(อสมการด้านบนเป็นค่าประมาณ — touch-fill สามารถจัดอันดับกลยุทธ์ผิดได้ ไม่ใช่แค่ทำให้สูงเกินจริง เพราะมันมอบ fill ที่ดีแบบสมมติให้คุณ) กฎการตัดสินใจที่ตามมาคือ: กลยุทธ์ maker จะนำไปใช้งานได้ก็ต่อเมื่อมันรอดที่ขอบระมัดระวังของกรอบขอบเขตนี้เท่านั้น และกรอบขอบเขตนั้นต้องแคบพอที่ค่าประมาณจุดจะมีความหมาย กลยุทธ์ที่แสดง +9kแบบtouch/9k แบบ touch / -3k แบบ trade-through มีกรอบขอบเขตกว้างถึง 12kคุณไม่รู้อะไรเลยนอกจากว่าสมมติฐานของตัวจำลองของคุณครอบงำalphaของคุณกลยุทธ์ที่แสดง+12k คุณไม่รู้อะไรเลยนอกจากว่าสมมติฐานของตัวจำลองของคุณครอบงำ alpha ของคุณ กลยุทธ์ที่แสดง +2.1k / +$0.4k กำลังบอกอะไรที่เป็นจริงกับคุณ

การทดลอง: เดินกลยุทธ์ maker หนึ่งตัวลงบันได

PnL ของกลยุทธ์ maker ที่ล่มสลายทีละขั้นลงบันไดโมเดล fill

ลองใช้ maker แบบเรียบง่ายจงใจหนึ่งตัว — quote แบบสมมาตรที่ best bid/ask, clip ขนาดคงที่ 0.05 BTC, inventory จำกัดที่ ±0.5 BTC พร้อม taker flattening เมื่อถึงขีดจำกัด ใช้ข้อมูล BTCUSDT perpetual หนึ่งเดือน: แท่ง 1 นาทีสำหรับขั้นต่ำๆ, L2 diff 100ms บวก trade tape สำหรับขั้นสูงๆ ค่าธรรมเนียม maker 1.0 bps, taker 4.0 bps ใช้โค้ดสัญญาณเดียวกันในทุกขั้น (shared core ดังนั้นตัวแปรเดียวคือโมเดล fill) ตัวเลขด้านล่างนี้คือการรันหนึ่งครั้งที่เป็นตัวแทนของเรา ขนาดของคุณจะแตกต่างไปตามแพลตฟอร์ม เดือน และขนาด แต่รูปทรงจะไม่แตกต่าง:

ขั้น โมเดล fill อัตรา fill ของ quote จำนวน fill PnL รายเดือน ข้อสรุป
0 close-fill 98% 41,200 +$14,800 ภาพลวงตา
1 next-bar-open / touch บนแท่ง 1 นาที 89% 37,400 +$9,600 ภาพลวงตาที่มี latency
2 touch + spread & slippage คงที่บน taker flatten 89% 37,400 +$7,100 สร้างโมเดลต้นทุนแล้ว แต่ fill ยังคงเป็นเรื่องสมมติ
3 + L2 depth-walk บน taker flatten 89% 37,400 +$6,400 การออกซื่อสัตย์ การเข้ายังคงเป็นเรื่องสมมติ
4a trade-through (ระมัดระวัง) 21% 8,900 -$3,900 ขอบเขต stress
4b queue-depletion, คาลิเบรต ϕ\phi แล้ว 37% 15,600 -$700 ค่าประมาณที่ดีที่สุด
4c queue-depletion, ϕ=1\phi = 1 (มองโลกในแง่ดี) 44% 18,700 +$1,900 ขอบบน
รัน shadow จริง เดือนเดียวกัน 35% 14,100 -$1,150 ความเป็นจริง

อ่านตารางจากบนลงล่างแล้วดูว่ากลยุทธ์ตายลงที่ไหน มัน ไม่ใช่ ขั้นที่ 2 — ค่าธรรมเนียมและ slippage เฉือนออกไป 26% แต่กลยุทธ์ก็ยังดูมีกำไรอย่างมั่นคง มันตายลงระหว่างขั้นที่ 3 กับขั้นที่ 4 และมันตายด้วยเหตุผลที่โมเดลต้นทุนใดๆ จับไม่ได้: การเลือก fill (fill selection) Touch-fill มอบ fill 37,400 ครั้ง ซึ่งส่วนใหญ่เป็น touch-and-bounce — การเก็บ spread ล้วนๆ โมเดลที่รับรู้คิวลบ fill เหล่านั้นออกไป 58% และ fill ที่มันลบออกไปนั้น เป็นพวกที่ทำกำไรอย่างไม่สมส่วน เมื่อระดับราคาถูกแตะเบาๆ คิวที่อยู่หน้า maker ที่ re-quote ด้วย latency แบบ retail จะดูดซับทุกอย่างไปหมด fill ที่รอดมาถึงขั้นที่ 4 เอียงไปทางการกวาดที่เคลียร์ทั้งระดับ (level-clearing sweep) — fill ที่ราคากำลังเคลื่อนผ่านคุณไปแล้ว อัตรา fill ลดลง 2.4 เท่า PnL เปลี่ยนเครื่องหมาย ความไม่สมมาตรนั้น — สูญเสีย fill ที่ดี เก็บ fill ที่แย่ไว้ — คือ adverse selection ที่ถูกทำให้ชัดเจนเชิงกลไก และมันมองไม่เห็นเลยในทุกขั้นที่ต่ำกว่า 4

สังเกตด้วยว่ากรอบขอบเขตทำอะไร: [-3,900,+3,900, +1,900] ครอบคลุมศูนย์ไว้ โดยค่าประมาณที่คาลิเบรตแล้วอยู่ที่ -700และค่าจริงอยู่ที่700 และค่าจริงอยู่ที่ -1,150 ตัวจำลองไม่ได้ทำนาย PnL จริงได้แม่นเป๊ะถึงดอลลาร์เดียว — แต่มันได้ เครื่องหมาย ได้ ขนาด และอัตรา fill ที่คลาดเคลื่อนเพียง 2 จุด นั่นคือสิ่งที่โมเดล fill มีไว้ทำ Backtest ขั้นที่ 1 พลาดจาก PnL จริงไป $10,750 บนกลยุทธ์ที่ edge รวมทั้งเดือนมีเพียงไม่กี่พันดอลลาร์: ความคลาดเคลื่อนของโมเดล fill มีขนาดใหญ่ราว 3 เท่าของตัว alpha เอง นี่คือที่มาของวิทยานิพนธ์หลัก: โมเดล fill ของคุณเป็นสมมติฐานที่ใหญ่กว่า alpha ของคุณเสียอีก

ข้อควรระวังหนึ่งอย่างสำหรับขั้นต่ำๆ: ถ้าคุณต้องอยู่กับข้อมูลแท่งราคา (ขั้นที่ 0-2) อย่างน้อยที่สุดให้แก้ไขความกำกวมภายในแท่ง (intrabar) ด้วย adaptive drill-down — เจาะลึกจาก 1 นาทีลงไปที่ 1 วินาที/100ms/เทรด เมื่อระดับ SL, TP หรือ quote ตกอยู่ภายในช่วงของแท่ง Drill-down แก้ไขข้อผิดพลาดด้าน ลำดับเหตุการณ์ (ระดับไหนถูกแตะก่อน) แต่แก้ไขข้อผิดพลาดด้าน คิว ไม่ได้ มันเป็นภาคเสริมด้านความละเอียดของข้อมูลสำหรับบทความนี้ ไม่ใช่ตัวแทนของขั้นที่ 4

ลูปการคาลิเบรต: ปิดช่องว่างกับ fill จริง

ตัวจำลองขั้นที่ 4 มีพารามิเตอร์อิสระ — ϕ\phi, latency Δt\Delta t, cancel-latency, สมมติฐานการเติมระดับราคาใหม่ ถ้าไม่คาลิเบรต มันก็เป็นแค่การเดาที่มีรูปทรงต่างออกไป ลูปที่เปลี่ยนมันให้เป็นเครื่องมือ:

1. บันทึกทุกอย่างจากการเทรดจริง ทุกเหตุการณ์ของออเดอร์พร้อม timestamp ของ exchange: submit, ack, แต่ละ partial fill, amend ack, cancel ack บวกกับสถานะ L2 ณ เวลา submit นี่คือวินัยการบันทึก log แบบเดียวกับที่ backtest-live parity เรียกร้องสำหรับ DivergenceMonitor ของมัน — การคาลิเบรตโมเดล fill คือชั้นที่ลึกที่สุดของ monitor นั้น

2. Replay ออเดอร์เดียวกันผ่านตัวจำลอง ป้อนข้อมูลตลาดที่บันทึกไว้และ คำสั่ง ออเดอร์ที่บันทึกไว้ (ไม่ใช่ fill) เข้าไปในตัวจำลอง ตอนนี้คุณมีผลลัพธ์เป็นคู่: สำหรับแต่ละออเดอร์จริง มีชะตากรรมจำลองหนึ่งอัน

3. เปรียบเทียบการกระจายตัว ไม่ใช่ค่าเฉลี่ย แบบแยกกลุ่ม (bucket) อัตรา fill รวมทั้งหมดตัวเดียวสามารถซ่อนข้อผิดพลาดที่ชดเชยกันได้ (มองโลกในแง่ดีเกินไปใน regime เงียบสงบ มองโลกในแง่ร้ายเกินไปในช่วง burst — หักลบกันจนดูเหมือน "คาลิเบรตแล้ว") แยกกลุ่มตามปัจจัยขับเคลื่อน:

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

สถิติสองตัวต่อแต่ละ bucket: ช่องว่างอัตรา fill ที่มีเครื่องหมาย (simulator ลบ live) และการทดสอบ KS บนการกระจายตัวของ เวลาจนถึง fill ในบรรดาออเดอร์ที่ได้ fill การเปรียบเทียบเวลาจนถึง fill คือตัวที่เฉียบคมที่สุด — ตัวจำลองสามารถจับคู่ อัตรา fill ได้ในขณะที่ fill ผิด เวลา อย่างเป็นระบบ ซึ่งจะทำให้สถิติ inventory และ adverse-selection ที่ตามมาผิดเพี้ยนไปหมด นี่คือบทเรียนของกรอบแนวคิด survival-analysis ของ Lo-MacKinlay-Zhang โดยตรง: execution คือปัญหาแบบ time-to-event ดังนั้นจึงต้องตรวจสอบมันในฐานะปัญหาแบบนั้น

4. ปรับปุ่มควบคุม ตามลำดับความสามารถในการระบุค่า (identifiability) Latency ก่อนเป็นอันดับแรก (วัดโดยตรงจาก ack timestamp — ไม่ใช่การ fit) แล้วจึงปรับ ϕ\phi โดยการลดช่องว่างอัตรา fill ให้น้อยที่สุดข้าม bucket ความลึกของคิว จากนั้นตรวจสอบ bucket ของ regime ความผันผวน: ช่องว่างที่มองโลกในแง่ดีอย่างต่อเนื่องซึ่งกระจุกตัวอยู่ใน bucket ของช่วง burst มักหมายความว่าตัวจำลองของคุณสร้างแบบจำลอง fill ที่เป็นพิษจาก cancel-latency หรือการเติมระดับราคาใหม่ต่ำเกินไป ไม่ใช่ปัญหาที่ ϕ\phi

5. รันกรอบขอบเขตอีกครั้ง หลังจากคาลิเบรตแล้ว ค่าประมาณ queue-depletion ควรอยู่ภายในกรอบขอบเขตใกล้กับค่าจริง และ — การทดสอบการยอมรับที่แท้จริง — การจัดอันดับ ของกลยุทธ์รุ่นต่างๆ ภายใต้ตัวจำลองควรตรงกับการจัดอันดับของพวกมันในโหมด shadow จากนั้นตรึงพารามิเตอร์ไว้และคาลิเบรตใหม่ตามตารางเวลา พลวัตของการ fill เปลี่ยนแปลงไปตามระดับค่าธรรมเนียมของแพลตฟอร์ม การเปลี่ยนแปลง tick-size และประชากร HFT และ ϕ\phi ที่คาลิเบรตในเดือนมีนาคมก็เป็นเพียงสมมติฐานภายในเดือนกรกฎาคม

ความคาดหวังในการลู่เข้า (convergence) จากการรันของเรา: ตัวจำลองขั้นที่ 4 ที่ยังไม่ได้คาลิเบรตมักจะอยู่ในช่วง ±10-15 จุดจากอัตรา fill จริง หลังจากคาลิเบรตหนึ่งรอบ เหลือ ±3-5 จุด โดยค่า p-value ของ KS สำหรับเวลาจนถึง fill ไม่ปฏิเสธสมมติฐานอย่างสม่ำเสมออีกต่อไป คุณจะไม่สามารถทำได้ดีกว่านั้นด้วยการ replay ข้อมูลย้อนหลัง — ส่วนที่เหลือคือปฏิกิริยาของตลาดต่อตัวคุณเอง ซึ่งเป็นปัญหาของขั้นที่ 5

สิ่งที่ควรจดจำ

  1. ระบุขั้นของคุณ Backtest ทุกตัวอยู่บนบันไดนี้ ไม่ว่าคุณจะเลือกขั้นเองหรือเฟรมเวิร์กของคุณเลือกให้ ถ้าคุณระบุขั้นของโมเดล fill และอคติที่รู้จักของมันไม่ได้ ตัวเลข PnL ของคุณก็มี error bar ที่คุณยังไม่เคยเห็น
  2. Taker หยุดที่ขั้นที่ 3 ได้ Depth-walk บวกกับ latency ที่วัดได้ ให้ราคาการ execute แบบ aggressive ที่ซื่อสัตย์ในขนาด retail ใช้ความพยายามที่ประหยัดได้ไปกับคุณภาพข้อมูลแทน
  3. Maker เริ่มที่ขั้นที่ 4 ต่ำกว่านั้น ตรรกะการ fill ออเดอร์ limit ไม่ใช่แค่ประมาณคร่าวๆ — มันเลือกชุด fill แบบสมมติที่มี skew ของ adverse selection ตรงกันข้าม Backtest แบบ touch-fill ของกลยุทธ์ maker คือตัวสร้างกลยุทธ์ที่ตายทันทีที่สัมผัสกับ production ที่น่าเชื่อถือที่สุด
  4. สร้างแบบจำลองวงจรชีวิต ไม่ใช่แค่การ fill Partial fill, amend-reset-queue, cancel latency — state machine คือที่ที่ fill ที่เป็นพิษอาศัยอยู่ และ fill ที่เป็นพิษคือที่ที่ PnL ของ maker ไปตาย
  5. รายงานกรอบขอบเขต ขอบเขตแบบระมัดระวังและมองโลกในแง่ดีมีต้นทุนแค่การรัน backtest เพิ่มอีกสองครั้ง และเปลี่ยน "backtest ของฉันบอกว่า +7k"ให้กลายเป็น"ความเป็นจริงอยู่ที่ไหนสักแห่งใน[7k" ให้กลายเป็น "ความเป็นจริงอยู่ที่ไหนสักแห่งใน [-3.9k, +$1.9k]" — ซึ่งเป็นการตัดสินใจที่แตกต่างและดีกว่า
  6. คาลิเบรตกับ fill จริงแบบแยกกลุ่ม โมเดล fill ที่ตรวจสอบเฉพาะในภาพรวมคือโมเดล fill ที่มีข้อผิดพลาดชดเชยกันซ่อนอยู่ ช่องว่างอัตรา fill แบบแยกกลุ่มบวกกับการทดสอบ KS ของเวลาจนถึง fill คาลิเบรตใหม่ทุกไตรมาส

ขั้นบันไดเหล่านี้ไม่ใช่การแบ่งระดับเชิงวิชาการ — แต่ละขั้นคือคำโกหกเฉพาะอย่างที่ backtest ของคุณเลิกบอกคุณ ไต่ขึ้นไปจนกว่าคำโกหกจะเล็กกว่า edge ของคุณ

ลิงก์ที่เป็นประโยชน์

  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.}
}
ข้อจำกัดความรับผิดชอบ: ข้อมูลที่ให้ไว้ในบทความนี้มีไว้เพื่อการศึกษาและให้ข้อมูลเท่านั้น และไม่ถือเป็นคำแนะนำทางการเงิน การลงทุน หรือการเทรด การเทรดสกุลเงินดิจิทัลมีความเสี่ยงสูงที่จะขาดทุน

ผู้เขียน

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

ก้าวนำหน้าตลาด

สมัครรับจดหมายข่าวของเราเพื่อรับข้อมูลเชิงลึกการเทรดด้วย AI เฉพาะ การวิเคราะห์ตลาด และการอัปเดตแพลตฟอร์ม

เราเคารพความเป็นส่วนตัวของคุณ ยกเลิกการสมัครได้ทุกเมื่อ