สัปดาห์ก่อนคุณส่งโน้ตบุ๊กมาให้ดู: สัญญาณเดิม ยูนิเวิร์สเดิม ข้อมูล BTCUSDT perp 14 เดือนเท่าเดิม สิ่งเดียวที่เปลี่ยนคือการส่งคำสั่ง คุณเลิกข้ามสเปรดแล้วเริ่มตั้ง bid ค้างไว้ต่ำกว่าราคาหนึ่ง tick และ Sharpe ก็ขยับจาก 0.42 เป็น 2.14 คุณถามว่านี่เป็นผลจริงหรือคุณทำอะไรพังไป
คุณทำอะไรพังไปสักอย่าง ผมอยากพาคุณดูให้ชัดว่าพังตรงไหน เพราะบั๊กอยู่ในบรรทัดเดียว แต่บทเรียนเบื้องหลังนั้นใหญ่กว่าบรรทัดนี้มาก
นี่คือกฎการจับคู่ของคุณ คัดมาจาก engine:
if bar.low <= limit_price: fill(limit_price)
ความหมายคือ ถ้าตลาดมีรายการซื้อขายที่ราคาของฉันหรือต่ำกว่านั้นระหว่างนาทีนี้ ฉันก็ได้จับคู่ที่ราคาของฉัน แต่สิ่งที่โค้ดกำลังระบุจริง ๆ คือราคาแตะระดับของคุณ การแตะราคาไม่ได้แปลว่าจับคู่ ระหว่างสองอย่างนี้มีคิวอยู่ และคุณจำลองคิวราวกับว่าไม่มีใครอยู่เลย
อะไรอยู่ข้างหน้าคุณ
เมื่อคุณตั้ง bid ที่ 84,120.0 บน BTCUSDT perp คุณจะไปต่อท้ายคำสั่งทั้งหมดที่ตั้งค้างอยู่แล้วในระดับนั้น ที่ระดับ top-of-book ของสัญญานี้ ปริมาณมักอยู่ระหว่าง 4 ถึง 30 BTC ขึ้นอยู่กับช่วงเวลา โดยค่ามัธยฐานในช่วงตัวอย่างของคุณอยู่ราว 12 BTC คำสั่งของคุณมีขนาด 0.4 BTC กว่าจะมีการซื้อขายกับคำสั่งของคุณ ฝั่งผู้ขายต้องส่งคำสั่งขายเข้าชนระดับนั้นด้วยปริมาณรวมมากพอที่จะจับคู่กับทุกคนที่มาก่อนคุณ และต้องเกิดขึ้นก่อนที่คำสั่งในระดับนั้นจะถูกยกเลิกไปต่อหน้าต่อตา หรือราคาจะขยับหนีขึ้นไป
ดังนั้นคำถามที่แบ็กเทสต์ควรถามไม่ใช่ “ราคาไปถึง 84,120.0 หรือยัง” แต่เป็น “มี market sell อย่างน้อย 12.4 BTC จับคู่ที่ 84,120.0 ระหว่างที่คำสั่งของฉันตั้งค้างอยู่หรือเปล่า” นี่เป็นเหตุการณ์ที่ต่างกันมาก ในข้อมูลของคุณ สองเหตุการณ์นี้เกิดต่างกันประมาณ 3 เท่า
กฎการจับคู่ 3 แบบ ได้กลยุทธ์ 3 แบบ
ผมรันสัญญาณของคุณใหม่โดยใช้จุดเข้าเดิมกับโมเดลการส่งคำสั่ง 3 แบบ alpha เดิม ค่าธรรมเนียมเดิม funding เดิม เปลี่ยนแค่ตรรกะการจับคู่
| กฎการจับคู่ | จำนวนที่จับคู่ | Edge เฉลี่ยใน 60s หลังจับคู่ | Sharpe |
|---|---|---|---|
แตะราคา: low <= limit | 4,180 | +2.6 bp | 2.14 |
ทะลุราคาอย่างชัดเจน: low < limit - 1 tick | 1,712 | +0.4 bp | 0.61 |
| จำลองคิวด้วยปริมาณซื้อขายที่ระดับราคา | 1,306 | +0.9 bp | 0.77 |
แถวกลางคือวิธีแก้แบบหยาบ ๆ ที่ทุกคนมักลองก่อน: นับว่าจับคู่ก็ต่อเมื่อตลาดซื้อขายทะลุราคาของคุณไป โดยคิดว่าถ้าราคาเลยคุณไปแล้ว ก็แปลว่าคำสั่งต้องถูกจับคู่ไปแล้ว วิธีนี้คิดถูกในภาพรวมและช่วยขจัดการมโนไปได้เกือบหมด แต่ก็มีอคติร้ายแรง ซึ่งเดี๋ยวผมจะอธิบาย
แถวล่างคือวิธีที่คุณควรสร้าง ใช้ข้อมูล L3 ก็ไม่ต้อง และไม่ต้องสร้างข้อมูลคำสั่งซื้อขายทีละรายการขึ้นใหม่ คุณมีข้อมูลเกือบครบอยู่แล้ว
จำลองคิวจาก aggTrades ได้เอง
ดึงสตรีมรายการซื้อขายรวมแทน klines ทุกรายการมีราคา ปริมาณ timestamp และ flag ฝั่ง maker ซึ่งบอกว่าใครเป็นฝ่ายเริ่มซื้อหรือขาย แค่นี้ก็จำลองคำสั่งแบบ passive ของคุณได้ดีพอแล้ว:
- เมื่อคำสั่งของคุณถูกตั้ง ให้บันทึก snapshot ของปริมาณคำสั่งที่ค้างอยู่ ณ ราคาของคุณ ถ้าคุณมีแค่ book feed ทุก 100ms ก็ใช้ snapshot ล่าสุด ความคลาดเคลื่อนตรงนี้เล็กน้อยเมื่อเทียบกับความคลาดเคลื่อนที่คุณกำลังแก้
- กำหนด
queue_ahead = resting_sizeให้มองในแง่ร้ายไว้ก่อนและสมมติว่าคุณอยู่ท้ายคิว ซึ่งก็เป็นอย่างนั้น เว้นแต่คุณจะเป็นคนสร้างระดับราคานั้นขึ้นมาเอง - ไล่ดูสตรีมการซื้อขาย รายการซื้อขายทุกครั้งที่ฝั่งผู้ขายเป็น aggressor และมีราคาที่ระดับเดียวกับหรือต่ำกว่าราคาของคุณ จะลดค่า
queue_aheadตามปริมาณของรายการนั้น เมื่อค่าติดลบ แปลว่าคุณถูกจับคู่ที่ราคาของคุณ ณ timestamp นั้น - ถ้าราคาขยับหนีไป 1 tick อย่าตั้งคิวกลับเป็นศูนย์ ให้ค่อย ๆ ลดลงแทน คำสั่งบางส่วนที่อยู่ข้างหน้าคุณถูกยกเลิกเมื่อระดับราคาเริ่มไม่เคลื่อนไหว แต่บางส่วนก็ยังค้างอยู่ การลดลง 30-40% ต่อวินาทีเต็มที่ราคาไม่แตะระดับนั้น ให้ผลใกล้เคียงกับข้อมูลที่ผมสร้างย้อนกลับได้มากกว่าการสมมติสุดโต่งทั้งสองด้าน
- ถ้ากลยุทธ์ของคุณจะยกเลิกแล้วตั้งคำสั่งใหม่ ให้จำลองการตั้งใหม่นั้นเป็นคำสั่งใหม่ที่ไปต่อท้ายคิวของระดับราคาใหม่ นี่คือขั้นตอนที่คนมักข้าม และเป็นที่ซ่อนของการมโนส่วนที่เหลือ
ขั้นที่ 2 คือจุดที่คุณอาจอยากเถียงกับผม ใช่ บางครั้งคุณก็อยู่ใกล้หัวแถว เพราะคุณตั้งคำสั่งทันทีที่ระดับราคาก่อตัว ก็ได้ — วัดมัน อย่าตั้งสมมติฐาน บันทึกปริมาณที่ตั้งค้างอยู่ตอนส่งคำสั่ง แล้วใช้ข้อมูลดูว่าคำสั่งของคุณเป็นคำสั่งแรก ๆ จริงกี่ส่วน สำหรับกลยุทธ์ของคุณมีแค่ 11% เพราะสัญญาณทำงานหลังเกิดการเคลื่อนไหว หมายความว่าระดับราคาที่คุณกำลังเข้าร่วมมีอยู่ก่อนแล้วและมีคนอยู่ในคิวก่อนแล้ว
คำสั่งที่จับคู่ได้คือคำสั่งที่คุณอยากให้ไม่ถูกจับคู่
ทีนี้มาถึงประเด็นที่สำคัญจริง ๆ และเหตุผลที่กฎทะลุราคาอย่างชัดเจนมีอคติ
ลองคิดดูว่าตอนไหน bid ของคุณถูกจับคู่จนหมด มันเกิดขึ้นเมื่อแรงขายมากพอจะกินปริมาณทั้งหมดในระดับนั้น ซึ่งโดยนิยามแล้วคือจังหวะที่ตลาดกำลังเคลื่อนลงผ่านราคาของคุณ การจับคู่ที่คุณมั่นใจที่สุดคือการจับคู่ที่ทำให้คุณติดลบทันที
แยกการจับคู่ตามลักษณะที่เกิดขึ้น แล้ววัด mark-out ที่ 60 วินาที:
| ประเภทการจับคู่ | สัดส่วนของรายการที่จับคู่ | Mark-out ที่ 60s |
|---|---|---|
| ซื้อขายที่ระดับราคาแล้วราคาดีดขึ้น | 38% | +3.1 bp |
| ซื้อขายที่ระดับราคาแล้วราคาไม่เปลี่ยน | 21% | +0.2 bp |
| ราคาทะลุไป 2+ ticks | 41% | −2.4 bp |
แบ็กเทสต์แบบง่ายของคุณได้การจับคู่มาทั้ง 3 กลุ่ม และตีราคาให้ทุกกลุ่มเหมือนได้มาฟรี กฎทะลุราคาอย่างชัดเจนได้เกือบแต่กลุ่มที่ 3 นั่นจึงเป็นเหตุให้ edge หายไปมากกว่าการจำลองคิว ทั้งคู่ไม่ถูกต้อง การจำลองคิวให้ส่วนผสมที่สมจริง และส่วนผสมนี่แหละคือหัวใจของเกม: การส่งคำสั่งแบบ passive ทำให้คุณได้สเปรด แต่ก็ต้องจ่ายคืนเป็น adverse selection อัตราส่วนระหว่างสองอย่างนี้คือกลยุทธ์จริงของคุณ
กรอบคิดของโต๊ะเทรดหุ้นแบบเก่ายังใช้ได้ดี: คำสั่ง maker คือออปชันฟรีที่คุณเขียนขายให้ตลาด คนอื่นใช้สิทธิเมื่อการใช้สิทธินั้นคุ้มค่า แบ็กเทสต์ของคุณเก็บค่า premium ไว้ แต่ลืมไปว่าออปชันมีส่วนที่ต้องจ่ายผลตอบแทนด้วย
รายการซื้อขายที่คุณไม่ได้จะเปลี่ยนกลยุทธ์ ไม่ใช่แค่ต้นทุน
นี่คือประเด็นที่ผมอยากให้คุณลองคิดให้ดี เมื่อจำลองการส่งคำสั่ง taker ได้แย่ คุณยังได้รายการซื้อขายที่ถูกต้องแต่ได้ราคาผิด และการปรับค่าธรรมเนียมก็แก้ปัญหาได้เกือบหมด แต่เมื่อจำลองการส่งคำสั่ง maker ได้แย่ คุณจะได้รายการซื้อขายคนละชุดไปเลยราว 2,900 จาก 4,180 จุดเข้าของคุณไม่เคยเกิดขึ้นจริง บางรายการเป็นสัญญาณที่ดีที่สุดของคุณ บนแท่งที่พุ่งทะลุแล้วกลับตัว ซึ่งเป็นรูปแบบที่ตลาดหนีไปโดยไม่มีคุณพอดี
ดังนั้นแขนงกรณีที่คำสั่งไม่ถูกจับคู่ต้องมีตรรกะจริง กลยุทธ์ทำอย่างไรเมื่อคำสั่งเข้าไม่ถูกจับคู่ก่อนสัญญาณหมดอายุ ไล่ราคาด้วยคำสั่ง taker แล้วยอมจ่ายสเปรดบวก market impact หรือเปล่า ตั้งคำสั่งใหม่ให้ต่ำลงและยอมรับต้นทุนเข้าที่ต่างออกไปไหม หรือข้ามการซื้อขายแล้วถือสถานะว่าง แต่ละทางเลือกทำให้เส้น equity ต่างกันอย่างมีนัยสำคัญ และไม่มีทางเลือกไหนที่เรียกว่า “สมมติว่าจับคู่แล้ว” ในการรันทดสอบของเรา การเพิ่มกฎไล่ราคาที่สมจริง (ข้ามสเปรดหลังไม่ถูกจับคู่ 20 วินาที จำกัด slippage ไว้ที่ 3 bp) ช่วยให้ได้รายการที่หายไปกลับมาราว 1 ใน 3 และกู้คืนช่องว่างระหว่าง Sharpe แบบง่ายกับแบบจำลองคิวได้ราวครึ่งหนึ่ง ผลนี้น่าสนใจจริง ๆ และจะเห็นได้ก็ต่อเมื่อโมเดลการจับคู่สมจริงพอที่จะทำให้คำถามนี้มีความหมาย
วิธีตรวจสอบคร่าว ๆ ใช้เวลา 10 นาที: เอาบันทึก paper-trading จริงกับแบ็กเทสต์มาเทียบกันในช่วงเวลาเดียวกัน เปรียบเทียบอัตราการจับคู่ ไม่ใช่ PnL ถ้าแบ็กเทสต์จับคู่คำสั่งที่ตั้งค้างไว้ 100% แต่ paper-trading จับคู่ได้ 34% คุณไม่ได้กำลังเปรียบเทียบกลยุทธ์ แต่กำลังเจอบั๊กในโมเดลการจับคู่ แก้ตรงนี้ก่อนจะดูตัวเลขผลตอบแทนแม้แต่ตัวเดียว
อีก 2 เรื่องเล็ก ๆ ที่อยากบอก
คำสั่ง post-only ถูกปฏิเสธ ถ้าคุณใช้ post-only เพื่อให้ได้ค่าธรรมเนียม maker และ order book ขยับระหว่างที่คุณตัดสินใจส่งคำสั่งกับตอนที่ exchange ตอบรับ คำสั่งจะถูกปฏิเสธแทนที่จะตั้งค้างไว้ ในบันทึก paper ของเรา ความถี่อยู่ที่ 3-6% ของความพยายามส่งคำสั่งบน BTCUSDT ในชั่วโมงปกติ และเกิน 15% ในนาทีหลังประกาศ CPI ของสหรัฐฯ คำสั่งที่ถูกปฏิเสธไม่ใช่คำสั่งที่จับคู่แล้ว และไม่ใช่คำสั่งที่ตั้งค้างไว้แต่ไม่ถูกจับคู่ด้วย มันคือรายการซื้อขายที่ไม่เคยเกิดขึ้นเลย และถ้าแบ็กเทสต์ของคุณไม่มีสถานะนี้ จำนวนรายการซื้อขายก็จะสูงเกินจริงในช่วงตลาดที่คุณให้ความสำคัญที่สุดพอดี
การป้องกัน self-trade และผลกระทบจากคำสั่งของคุณเอง ที่ขนาด 0.4 BTC คุณไม่ได้ขยับ BTCUSDT ข้ามประเด็นนี้ไปได้เลย แต่คุณบอกว่าอยากลองใช้กับ perp ของ alt ที่มี market cap ระดับกลาง ซึ่งมูลค่า top-of-book มักต่ำกว่า $15k ในกรณีนั้นคำสั่งของคุณเป็นสัดส่วนสำคัญของคิว และตัวเลขปริมาณที่อยู่ข้างหน้าซึ่งคุณบันทึกไว้ก็รวมผลจากการมีคำสั่งก่อนหน้าของคุณอยู่ในตลาดแล้ว เมื่อขนาดคำสั่งของคุณเกินราว 10% ของระดับที่ตั้งค้างอยู่ การจำลองต้องคำนึงถึงผู้ร่วมตลาดรายอื่นที่ตอบสนองต่อคุณ และพูดกันตรง ๆ ถึงจุดนั้นผมเชื่อถือ paper trading มากกว่าการจำลองใด ๆ ที่คุณหรือผมเขียนได้
รันใหม่ด้วยการจำลองคิว แล้วส่งตาราง mark-out ที่แยกตามประเภทการจับคู่มาให้ผม ถ้ากลุ่มที่ราคาดีดกลับยังมี edge ส่วนใหญ่ และกลุ่มที่ราคาทะลุไม่ได้กิน edge ไปจนหมด คุณอาจมีอะไรที่น่าลองทดสอบบน paper ถ้าทั้งหมดเกิดจากการจับคู่ 2,900 ครั้งที่คุณไม่มีทางได้ ก็ดีกว่าที่จะรู้ตอนนี้ แทนที่จะใช้เวลา 4 สัปดาห์นั่งดูบัญชี paper ทำสิ่งที่โน้ตบุ๊กบอกว่าจะทำไม่ได้
← บทความทั้งหมด


