เรารัน Backtest ที่บันทึกไว้ซ้ำแล้วได้เส้นทุนที่ต่างออกไป โค้ดกลยุทธ์เดิม ช่วงวันที่เดิม สัญลักษณ์เดิม แต่ยอดเงินปลายทางคลาดเคลื่อน 1.8% และมีรายการซื้อขาย 3 รายการที่เลื่อนไป 1 แท่ง
แค่นี้ก็ทำให้ยากที่จะเชื่อถือผลการวิจัยแล้ว หากเพื่อนร่วมทีม ตัวคุณในอนาคต หรือบริการ paper trading ทำซ้ำการรันนี้ไม่ได้ คุณก็แยกไม่ออกว่าการเปลี่ยนแปลงช่วยให้กลยุทธ์ดีขึ้น หรือแค่เปลี่ยนการทดลอง นี่คือขั้นตอนที่เราใช้ค้นหาความคลาดเคลื่อน
วันที่ 1: เราจดบันทึกว่า “การรันแบบเดิม” หมายถึงอะไร
ความผิดพลาดแรกของเราคือคิดว่าไฟล์กลยุทธ์คือการทดลองทั้งหมด แต่ไม่ใช่ การรันยังขึ้นอยู่กับข้อมูลนำเข้า เวอร์ชันของ engine ปฏิทิน ข้อมูลกำกับเครื่องมือ และการตั้งค่าการส่งคำสั่งด้วย โค้ดอธิบายเพียงส่วนหนึ่งของการคำนวณเท่านั้น
เราจัดทำ manifest ของการรันก่อนเปลี่ยนแปลงสิ่งใด โดยบันทึก commit ของกลยุทธ์ ตัวระบุ snapshot ของข้อมูล ช่วงวันที่ venue ตารางค่าธรรมเนียม แหล่งข้อมูล funding โมเดลการจับคู่คำสั่ง และเวอร์ชันซอฟต์แวร์ เรายังบันทึกคำสั่งและรายการจับคู่ที่ได้ไว้ด้วย เพราะเส้นทุนเพียงอย่างเดียวบอกไม่ได้ว่าการรันสองครั้งเริ่มแตกต่างกันตรงไหน
| สิ่งที่ต้องบันทึก | รายละเอียดที่ควรบันทึก | เหตุผลที่สำคัญ |
|---|---|---|
| ข้อมูลตลาด | Snapshot ID, เวอร์ชัน schema, การปรับแก้ข้อมูล | ผู้ให้บริการแก้ไขประวัติและปรับปรุง corporate actions |
| การส่งคำสั่ง | ระดับค่าธรรมเนียม, ชุดข้อมูล funding, การตั้งค่าการจับคู่และผลกระทบต่อราคา | ค่าเริ่มต้นและสมมติฐานเกี่ยวกับบัญชีทำให้ผลลัพธ์เปลี่ยนได้ |
| สภาพแวดล้อมขณะรัน | commit ของโค้ด, เวอร์ชัน engine และ dependency | ไลบรารีอาจเปลี่ยนลำดับ การปัดเศษ หรือตัวชี้วัด |
| ผลลัพธ์ | คำสั่ง รายการจับคู่ สถานะถือครอง และตัวชี้วัด | ช่วยให้เห็นว่าการรันเริ่มไม่ตรงกันตรงไหน |
วันที่ 2: เราเทียบรายการซื้อขาย ไม่ใช่ Sharpe
ตัวชี้วัดสรุปทำให้เราเสียสมาธิ ทั้งสองการรันมี Sharpe ใกล้เคียงกันมาก แต่บันทึกการจับคู่คำสั่งเผยให้เห็นความไม่ตรงกันครั้งแรกที่การชำระ funding การรันหนึ่งคิดอัตรากับสถานะที่เปิดอยู่ ณ เวลาชำระ ส่วนอีกการรันใช้สถานะหลังการปรับสมดุลของเวลานั้น
โค้ดกลยุทธ์ไม่ได้เปลี่ยน แต่ลำดับเหตุการณ์ของ engine เปลี่ยนไป การอัปเดตเวอร์ชันเล็กน้อยทำให้ลำดับชัดเจนขึ้น จากเดิมที่ขึ้นอยู่กับว่าเหตุการณ์สองรายการถูกจัดเรียงอย่างไร
เราแก้ข้อตกลงการรันให้ระบุลำดับไว้อย่างชัดเจน: คิด funding กับสถานะที่ถือเข้ามาก่อนถึงเวลาชำระ จากนั้นจึงประมวลผลการตัดสินใจของกลยุทธ์ ณ เวลานั้น ธรรมเนียมที่แน่นอนอาจต่างกันไปตาม venue และ engine การปล่อยให้ไม่ระบุไว้นั่นแหละคือข้อผิดพลาด
วันที่ 3: ไฟล์ข้อมูลที่คิดว่า “เหมือนเดิม” กลับไม่เหมือนเดิม
หลังตรึงลำดับเหตุการณ์แล้ว ความไม่ตรงกันที่เหลือกระจุกตัวอยู่ในรายการซื้อขายหุ้นไม่กี่รายการ ผู้ให้บริการแก้ไขการปรับ split ย้อนหลัง ไฟล์ของเรามีชื่อและจำนวนแถวเท่าเดิม จึงทำให้ดูเหมือนไม่มีการเปลี่ยนแปลง
ตอนนี้เรา fingerprint snapshot ข้อมูลแต่ละชุดที่แก้ไขไม่ได้ และเก็บนโยบายการปรับแก้ไว้คู่กัน ค่า hash บอกได้ว่าไบต์เปลี่ยนหรือไม่ แต่ไม่ได้อธิบายว่าทำไม manifest จึงเก็บแหล่งที่มา เวลาที่ดึงข้อมูล และเวอร์ชันการแปลงข้อมูลไว้ด้วย สำหรับข้อมูลที่มีการแก้ไข รายละเอียดเหล่านี้เป็นส่วนหนึ่งของผลลัพธ์
Backtest ที่ทำซ้ำได้ต้องตอบคำถามว่า “มันเห็นอดีตเวอร์ชันไหน?”
วันที่ 4: เราพบค่าเริ่มต้นที่ซ่อนอยู่
ความแตกต่างสุดท้ายคือค่าธรรมเนียม maker ที่ตั้งไว้เป็นศูนย์ เพราะไม่ได้ระบุฟิลด์นี้ไว้ใน config ของกลยุทธ์ engine เวอร์ชันใหม่ใช้ค่าธรรมเนียมเริ่มต้นของบัญชี ค่าเริ่มต้นเพียงค่าเดียวนี้เปลี่ยนรายการซื้อขายที่มีผลต่อผลลัพธ์เพียงเล็กน้อยมากพอจะอธิบายความต่างของยอดเงินปลายทางส่วนใหญ่ได้
เราระบุการตั้งค่าที่มีผลทางเศรษฐกิจไว้อย่างชัดเจน และให้ engine พิมพ์การตั้งค่าที่สรุปแล้วลงในบันทึกการรัน ค่าเริ่มต้นสะดวกในช่วงสำรวจ แต่ใช้เป็นหลักฐานได้ไม่ดีเมื่อต้องเปรียบเทียบผลลัพธ์ข้ามช่วงเวลา
สิ่งที่เราจะข้ามไปในครั้งหน้า
เราใช้เวลาครึ่งวันเปรียบเทียบตัวชี้วัดรวม ก่อนจะตรวจดูรายการจับคู่คำสั่งรายการแรกที่ต่างกัน อย่าเริ่มจากตรงนั้น เรียงบันทึกเหตุการณ์ทั้งสองชุดตามเวลาแล้วเปรียบเทียบจุดที่เริ่มแตกต่าง ความต่างที่ตามมามักเกิดจากสาเหตุเดียวกัน
เราจะเลิกคิดด้วยว่า image ของ container เพียงอย่างเดียวทำให้การรันทำซ้ำได้ มันตรึงสภาพแวดล้อมซอฟต์แวร์ส่วนใหญ่ไว้ได้ แต่ไม่ได้ตรึงไฟล์ข้อมูลภายนอก ตารางค่าธรรมเนียมที่ดึงมาตอนรัน หรือประวัติข้อมูลที่ผู้ให้บริการแก้ไข
เมื่อ Backtest เปลี่ยนแปลง ให้เก็บ manifest และบันทึกของการรันทั้งสองชุดไว้ แล้วแก้สาเหตุของความคลาดเคลื่อนทีละอย่าง ผลลัพธ์ที่มีประโยชน์ไม่ใช่แค่เส้นทุนที่รันซ้ำได้ แต่คือบันทึกที่อธิบายว่าข้อมูลและสมมติฐานใดสร้างผลลัพธ์นั้น และเหตุใดการรันครั้งถัดไปจึงอาจต่างออกไป
← บทความทั้งหมด


