คุณส่งโน้ตบุ๊กมาให้ผมเมื่อคืนวันอาทิตย์ และผมเปิดมันค้างไว้บนจอที่สองตั้งแต่นั้น โมเมนตัมแบบภาคตัดขวาง เลือกเพอร์เพทชวล Binance USDⓈ-M 150 อันดับแรกตามปริมาณซื้อขายรายวันเป็นดอลลาร์ในช่วง 30 วัน ปรับพอร์ตทุกสัปดาห์ ซื้อกลุ่ม 10% บนสุดและขายชอร์ตกลุ่ม 10% ล่างสุด ช่วง 2021-01 ถึง 2026-06 ค่า Sharpe 2.31, การขาดทุนสูงสุด 14.2% และแบบจำลองต้นทุนที่ดูสมเหตุสมผลสำหรับผม: เข้าแบบ taker ออกแบบ taker คิด funding สะสมตามแต่ละช่วงเวลา และมีองค์ประกอบ slippage ที่ปรับตามขนาดคำสั่งของคุณเมื่อเทียบกับสมุดคำสั่ง คุณทำส่วนที่ยากได้ถูกต้องแล้ว จากนั้นคุณถามว่าทำไมการทดลองเทรดด้วยเงินจริงในบัญชีจำลองตลอด 6 สัปดาห์จึงให้ผลนิ่งสนิท และความได้เปรียบเสื่อมลงหรือเปล่า
มันไม่ได้เสื่อมลง เพราะมันไม่เคยอยู่ในการทดสอบย้อนหลังเลย ดูเซลล์ที่ 4:
info = requests.get(BASE + "/fapi/v1/exchangeInfo").json()
symbols = [s["symbol"] for s in info["symbols"]
if s["status"] == "TRADING" and s["quoteAsset"] == "USDT"]
คุณเรียก endpoint นั้นในเดือนมิถุนายน 2026 แล้วใช้คำตอบกำหนดว่าสินทรัพย์อะไรซื้อขายได้ในเดือนมีนาคม 2021 ทุกรายการในลิสต์นั้นยังอยู่รอดมาจนถึงเดือนมิถุนายน 2026 ตามวิธีที่คุณสร้างมันขึ้นมา นั่นคือบั๊กทั้งหมด และผลกระทบมีค่ามากกว่าแบบจำลองต้นทุนส่วนที่เหลือของคุณรวมกันเสียอีก
สิ่งที่ endpoint ไม่ยอมบอกคุณ
ใน exchangeInfo ไม่มีประวัติ ไม่มีพารามิเตอร์ asOf ไม่มีคลังข้อมูลเก่า ไม่มีบันทึกการเปลี่ยนแปลง มันเป็นภาพถ่ายของตอนนี้ และ Binance ไม่เคยรับปากอย่างอื่น เมื่อสัญญาถูกถอดออกจากรายการ รายการนั้นก็ถูกลบจากคำตอบ และในมุมของ API ticker นั้นก็หยุดมีอยู่ Binance เคยลิสต์เพอร์เพทชวล USDⓈ-M มากกว่า 600 รายการตั้งแต่ปี 2019 และถอดออกไปมากกว่าร้อยรายการ BTS, COCOS, TOMO, RAY, FTT, SC และสัญญาอัลต์คอยน์อีกมากมายจากฤดูกาลอัลต์ปี 2021 ที่เคยรุ่งโรจน์อยู่ไตรมาสเดียว ก่อนปริมาณจะค่อย ๆ หายไปจนแพลตฟอร์มถอดออก
ลองถามตัวเองว่าตัวคัดกรองโมเมนตัม 30 วันชอบชื่อแบบไหน ไม่ใช่ BTC แต่เป็นเหรียญที่เพิ่งพุ่งขึ้น 3 เท่าจากแรงซื้อช่วงลิสต์และกระแสบน Twitter กลุ่มเหรียญแบบนั้นทับซ้อนกับกลุ่มที่ท้ายที่สุดจะถูกถอดออกอย่างมาก และตัวกรองจักรวาลของคุณก็ลบส่วนที่ทับซ้อนกันนั้นทิ้งไป
ผมรันกลยุทธ์ของคุณใหม่โดยใช้คลัง snapshot ของเรา จักรวาลสินทรัพย์ ณ เวลานั้น สัญญาณเดิม และต้นทุนเดิม ค่า Sharpe ออกมา 0.74 พร้อมการขาดทุน 31% ช่องว่างประมาณสองในห้าส่วนมาจากเหรียญที่ถูกถอดออก ซึ่งคุณไม่มีทางถือได้ อีกหนึ่งในสี่มาจากปัญหาที่เป็นภาพสะท้อนอีกด้าน ซึ่งซับซ้อนกว่า และผมคิดว่าคุณคงชอบมันน้อยกว่า
อีกด้านของไทม์แมชชีน: สัญลักษณ์ที่ยังไม่มีอยู่ในตอนนั้น
ฟีเจอร์ของคุณคือผลตอบแทน 90 วัน แต่หน้าต่าง rolling ของคุณใช้ min_periods=20 เพราะคุณตั้งค่าไว้ครั้งเดียวเพื่อไม่ให้ช่วง warmup กินข้อมูลไตรมาสแรกของตัวอย่าง แล้วก็ไม่ได้กลับมาทบทวนอีก สัญญาที่เพิ่งเริ่มซื้อขายเมื่อ 21 วันก่อนจึงได้คะแนนโมเมนตัมจากการเคลื่อนไหวของราคาหลังลิสต์เพียง 3 สัปดาห์ ติดกลุ่ม 10% บนสุดแทบทุกครั้งที่การลิสต์ไปได้สวย แล้วเข้าพอร์ตของคุณ
ส่วนนี้พอจะถือว่าสมเหตุสมผลได้ แต่สิ่งต่อไปนี้ไม่ใช่: ผู้ให้บริการข้อมูลของคุณเติมข้อมูลย้อนหลังให้บางสัญลักษณ์ด้วยข้อมูล spot หรือดัชนีก่อนที่เพอร์เพทชวลจะมีอยู่ ทำให้สัญญาบางรายการมีประวัติ kline ที่เก่ากว่า onboardDate ของตัวเอง คุณตรวจเรื่องนี้ได้ในเวลาประมาณ 1 นาที นำตารางราคามา join กับวันที่เริ่มลิสต์ แล้วนับแถวที่อยู่ก่อนวันลิสต์ ในข้อมูลของคุณมี 41 สัญลักษณ์ที่มีแท่งราคาก่อนลิสต์ และหนึ่งในนั้นในช่วงฤดูร้อนปี 2023 สร้างกำไร 6% ในสัปดาห์เดียวให้เส้น equity จากสถานะในสัญญาที่จะยังไม่มีอยู่ไปอีก 9 วัน
ticker string ไม่ใช่รหัสประจำตัว แต่มันเป็นป้ายชื่อที่แพลตฟอร์มให้ยืมใช้ บางครั้งก็ให้ใช้ซ้ำสองครั้ง
เรื่องนี้พาไปถึงการเปลี่ยนชื่อ MATICUSDT กลายเป็น POLUSDT ส่วน FTMUSDT กลายเป็น SUSDT หลังแปลงสัดส่วน 1:1 เหตุการณ์ LUNA ในเดือนพฤษภาคม 2022 ทิ้ง LUNCUSDT ไว้ แล้วภายหลังก็มี LUNAUSDT ตัวใหม่เอี่ยมที่ใช้ราก ticker เดียวกับสินทรัพย์ที่สูญมูลค่าไปแทบทั้งหมด หาก loader ของคุณใช้สตริง symbol เป็นคีย์แล้วนำไฟล์ที่เจอทั้งหมดมาต่อกัน คุณก็มีอย่างน้อยหนึ่งอนุกรมที่มีจุดไม่ต่อเนื่องซึ่งไม่ได้เกิดจากการเคลื่อนไหวของราคา แล้วฟีเจอร์โมเมนตัมของคุณจะอ่านจุดไม่ต่อเนื่องนั้นเป็นสัญญาณที่แรงที่สุดในกลุ่มสินทรัพย์
ข้อมูลอื่น ๆ ที่ค่อย ๆ เปลี่ยนไปโดยที่คุณไม่รู้ตัว
เมื่อยอมรับแล้วว่ารายชื่อสัญลักษณ์เปลี่ยนไปตามเวลา เหตุผลเดียวกันนี้ก็ใช้กับทุกฟิลด์อื่นในคำตอบนั้น คุณกำลังใช้ค่าของวันนี้กับทุกฟิลด์
| ฟิลด์ | เปลี่ยนแปลงอย่างไร | เกิดปัญหาอะไร |
|---|---|---|
| status | TRADING → SETTLING → ถูกถอดออก | อคติจากการอยู่รอด; จำลองออกจากสถานะที่ราคาปิดซึ่งไม่เคยมีการซื้อขายจริง |
| onboardDate | มีรายการใหม่ทุกสัปดาห์; ไม่มีข้อมูลสำหรับสัญลักษณ์ที่ตายไปแล้ว | ซื้อขายสัญญาก่อนวันที่มีอยู่จริง |
| tickSize / stepSize | ปรับใหม่เมื่อระดับราคาเปลี่ยน | การปัดเศษคำสั่ง และราคาลิมิตที่ถูกปฏิเสธในเวลานั้น |
| minNotional | ปรับเพิ่มขึ้นตามเวลาในสมุดคำสั่งที่บาง | สถานะขนาดเล็กที่ router จริงของคุณจะปฏิเสธ |
| fundingIntervalHours | ใช้ 8h มาหลายปี แล้วหลายสัญลักษณ์เปลี่ยนเป็น 4h หรือ 1h | คำนวณ carry ผิดไป 2–3× กับอัลต์คอยน์กลุ่มเดียวกับที่ตัวคัดกรองของคุณเลือกถือ |
| leverage brackets | มีการแก้ไขขั้นและ maintenance margin | การจำลอง liquidation และความสามารถในการใช้ margin |
กรณี funding กระทบหนักที่สุดสำหรับกลยุทธ์ของคุณ ลูปการสะสม funding สมมติว่ามีการจ่าย 3 ครั้งต่อวันตลอดประวัติ มีสัดส่วนสำคัญของพอร์ตอัลต์ที่เปลี่ยนไปใช้ funding ทุก 4 ชั่วโมง และสถานะขายชอร์ตในเหรียญที่มี funding สูงคือแหล่งที่มาของ PnL จำลองส่วนใหญ่ คุณไม่ได้คลาดเคลื่อนแค่จากการปัดเศษ แต่คลาดเคลื่อนเป็นเท่าตัว
การถอดเหรียญออกเป็นเหตุการณ์หนึ่ง และคุณไม่ได้จำลองมัน
ในการรันทดสอบใหม่ด้วยข้อมูล ณ เวลานั้น ผมให้กลยุทธ์ได้ออกจากสถานะอย่างเอื้อเฟื้อ การถอดเหรียญออกจริงเป็นขั้นตอนตามกำหนด: ประกาศล่วงหน้า โดยปกติ 7 ถึง 14 วัน จากนั้นเข้าสู่ช่วงลดสถานะได้อย่างเดียว แล้วจึงบังคับชำระที่ราคา mark ประกาศนั้นเป็นข้อมูลสาธารณะและคุณลงมือได้ ดังนั้นการจำลองที่สมเหตุสมผลคือออกที่ราคาปิดของวันประกาศ แต่ราคาปิดนั้นมักต่ำกว่าสัปดาห์ก่อนหน้าอยู่แล้ว 10-20% สำหรับการถอดอัลต์คอยน์ทั่วไป สมุดคำสั่งก็บาง และองค์ประกอบ slippage ของคุณต้องรู้ว่ากำลังทำงานอยู่ในสภาวะตลาดอีกแบบ หากระบบจำลองการจับคู่คำสั่งให้ราคาชำระบัญชีโดยไม่มีผลกระทบต่อตลาด คุณก็ทำให้สินทรัพย์ที่กำลังตายซื้อขายได้ในมูลค่ายุติธรรมอย่างเงียบ ๆ
ถ้าคุณไม่เคยเก็บ exchangeInfo ไว้ ก็ยังไม่สายเกินไป public dump ที่ data.binance.vision/data/futures/um/monthly/klines/ ยังคงเก็บไดเรกทอรีของสัญลักษณ์ที่ถูกถอดออกไปนานหลัง API ลืมมันไปแล้ว ไล่ดูรายการไดเรกทอรี แล้วใช้ไฟล์รายเดือนแรกสุดและท้ายสุดของแต่ละสัญลักษณ์เพื่อประมาณช่วงเวลาลิสต์และถอดออกได้ โดยไม่ต้องพึ่งผู้ให้บริการข้อมูล นี่เป็นการสร้างประวัติขึ้นใหม่ ไม่ใช่บันทึกจริง และกู้คืน tick size หรือช่วงเวลา funding ไม่ได้ แต่จะบอกได้ว่าสินทรัพย์ใดมีอยู่เมื่อใด ซึ่งคิดเป็น 80% ของสิ่งที่คุณต้องใช้ในวันจันทร์
สิ่งที่ผมอยากให้คุณสร้างก่อนกลับไปแตะสัญญาณอีกครั้ง
- ตั้ง cron ให้ดึง exchangeInfo จากทุกแพลตฟอร์มที่คุณทำวิจัยทุกวัน แล้วเขียนลง object storage โดยใช้วันที่เป็นคีย์ เมื่อ gzipped แล้วมีขนาดไม่ถึงครึ่งเมกะไบต์ เก็บไว้ 10 ปีมีค่าใช้จ่ายเล็กน้อยมากใน S3 แต่ให้ความสามารถในการทำวิจัยที่คุณซื้อทีหลังไม่ได้
- สร้าง asset master จาก snapshot เหล่านั้น: 1 แถวต่อ (venue, symbol, valid_from, valid_to) พร้อมฟิลด์ครบชุด เปรียบเทียบ snapshot ที่อยู่ติดกันเพื่อสร้างตารางนี้ และให้ถือว่าการเปลี่ยนแปลงใด ๆ ของฟิลด์เป็นแถวใหม่
- สร้างฟังก์ชันจักรวาลสินทรัพย์ที่บังคับให้ระบุ timestamp
universe(ts)ต้องใช้ข้อมูล ณ เวลานั้นเสมอuniverse()อย่าใช้ค่าปัจจุบันเป็นค่าเริ่มต้น ทำให้เรียกใช้วิธีผิดพลาดไม่ได้ เพื่อไม่ให้ใครก็ตาม รวมถึงตัวคุณในอนาคตตอนตี 1 เผลอกลับไปใช้รายชื่อผู้รอด - เพิ่ม assertion ใน data loader เพื่อตรวจข้อมูลก่อนลิสต์: ต้องไม่มีแท่งราคาอยู่ก่อน
onboard_tsลบ 1 วัน ให้การรันล้มเหลว อย่าแค่เตือน - สร้างรหัสเครื่องมือภายในที่คงเดิมแม้มีการเปลี่ยนชื่อ และลดบทบาท ticker ให้เป็นเพียงแอตทริบิวต์ จับคู่ POL กับ MATIC ไว้ใน ID เดียว และทำเครื่องหมายการเปลี่ยนหน่วยเพื่อให้ตรรกะความต่อเนื่องปฏิเสธการเชื่อมข้อมูลข้ามช่วงดังกล่าว
ทำสิ่งเหล่านี้แล้วรันใหม่ ผมเดาว่าผลจะอยู่ใกล้ 0.74 ที่ผมได้ และคำถามที่น่าสนใจคือค่า 0.74 ซึ่งรวมเหรียญที่ตายไปแล้วนั้นยังมีอะไรที่ควรค่าแก่การทดลองเทรดด้วยเงินจริงในบัญชีจำลองหรือไม่ อาจมีก็ได้ โมเมนตัมแบบภาคตัดขวางในเพอร์เพทชวลไม่ได้ไร้ค่า และส่วนหนึ่งของสิ่งที่เหลือหลังแก้จักรวาลสินทรัพย์ก็คือ carry จริงจากฝั่งขายชอร์ต คุณจะพบด้วยว่าผลการทดลองเทรดและการทดสอบย้อนหลังเริ่มสอดคล้องกัน เพราะการทดลองเทรดด้วยเงินจริงในบัญชีจำลองใช้จักรวาลสินทรัพย์ ณ เวลานั้นมาตลอด มันไม่มีทางเลือกอื่น
ป.ล. เรื่องนี้ไม่ใช่ปัญหาเฉพาะของคริปโท แค่เห็นชัดกว่าเท่านั้น นักวิจัยหุ้นต่อสู้กับผลตอบแทนจากหุ้นที่ถูกถอดออกและ ticker ที่นำกลับมาใช้ใหม่มาตั้งแต่ CRSP เริ่มเผยแพร่ข้อมูล ส่วนตลาดพยากรณ์ไปไกลสุดทาง: สัญญาทุกฉบับหมดอายุตามการออกแบบ ดังนั้นจักรวาลจึงประกอบด้วยการลิสต์และการสิ้นสุดทั้งหมด หากคุณนำตัวคัดกรองนี้ไปใช้กับ Kalshi เมื่อไร ให้สร้าง asset master ก่อน เพราะประวัติที่นั่นไม่มีรูปแบบอื่น
← บทความทั้งหมด

