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

การพิสูจน์ว่าไม่มี Look-Ahead ใน Multi-Timeframe Backtest: รบกวนอนาคต พิสูจน์ว่าอดีตมองไม่เห็นมัน

การพิสูจน์ว่าไม่มี Look-Ahead ใน Multi-Timeframe Backtest: รบกวนอนาคต พิสูจน์ว่าอดีตมองไม่เห็นมัน
#อัลกอริทึมเทรดดิง
#Backtest
#อคติ Look-ahead
#Multi-Timeframe
#การรั่วไหลของข้อมูล
#การตรวจสอบความถูกต้อง
🎯
Part 2 of 9 · Collection
Backtesting Without Fooling Yourself

ส่วนหนึ่งของซีรีส์ "Backtests Without Illusions"

บทความก่อนหน้า จบลงด้วยการวินิจฉัยหนึ่งอย่าง คือ one-bar shift test เลื่อนการ fill ทุกครั้งไปอีกหนึ่งแท่ง (bar) ถ้าผลตอบแทนพังทลายลง แสดงว่าคุณกำลังเทรดอยู่ในอดีต การทดสอบนั้นเพียงพอสำหรับกลยุทธ์แบบ single-timeframe ที่ look-ahead ซ่อนอยู่ใน การส่งคำสั่ง (execution) — คุณตัดสินใจที่แท่ง i และ fill ที่แท่ง i เช่นกัน

กลยุทธ์แบบ multi-timeframe มีจุดรั่วไหลที่สองซึ่งเงียบกว่านั้น และ shift test เข้าไม่ถึงจุดนั้นเลย เมื่อการตัดสินใจบนแท่ง 1 นาทีอ้างอิงเทรนด์ 1 ชั่วโมง การรั่วไหลจะไม่ได้อยู่ที่ fill อีกต่อไป — แต่อยู่ใน indicator ที่คำนวณจากแท่ง higher-timeframe ที่ ยังก่อตัวไม่เสร็จ บทความนี้ว่าด้วยการรั่วไหลนั้น และว่าด้วยวิธีพิสูจน์ — ด้วยกลไก ไม่ใช่ด้วยการจ้องมองโค้ด — ว่าเอนจินของคุณไม่มีมัน

ข้อกล่าวอ้างที่เราจะสรุปในตอนท้ายนั้นหนักแน่นมาก: เอนจิน multi-TF ของเราผ่านการตรวจสอบ parity และการรั่วไหลครบ 25 จาก 25 รายการ บนข้อมูล ETHUSDT 1 นาทีจริงจำนวน 86,400 แท่ง รวมถึง shifted-future probe ที่รบกวนทุกแท่งในอนาคตและยืนยันว่าไม่มีการตัดสินใจในอดีตแม้แต่ครั้งเดียวขยับเลย และเพราะเอนจินนี้เหมือนกับแกนหลักตามหลักการ (canonical core) ของบอทที่รันจริงแบบบิตต่อบิต การพิสูจน์นี้จึงเป็นคุณสมบัติของระบบ ที่รันจริง ไม่ใช่แค่ของ backtest เท่านั้น

ทุ่นระเบิดของ multi-timeframe

การตัดสินใจที่แท่ง 1 นาทีที่เอื้อมขึ้นไปยังสามไทม์เฟรมพร้อมกัน โดยแท่งเทียนของไทม์เฟรมสูงสุดยังก่อตัวไม่เสร็จ และราคาปิดสุดท้ายของมันถูกซ่อนอยู่หลังขอบเขตของเวลา

กลยุทธ์แบบ multi-timeframe (multi-TF) ตัดสินใจบนนาฬิกาที่เดินเร็ว ในขณะที่อ้างอิงนาฬิกาที่เดินช้ากว่า เอนจินของเราเป็นตัวอย่างที่เป็นรูปธรรม คือกฎ momentum แบบ triple-TF ที่เทรนด์ของ higher-timeframe (HTF) และเทรนด์ของ mid-timeframe (MTF) ทำหน้าที่ เปิดประตู (gate) ให้กับการเข้าเทรด ส่วนการตัด (cross) ของ lower-timeframe (LTF) เป็นตัว กระตุ้น (trigger) บนนาฬิกาฐาน 1 นาที การเข้าเทรดจะเกิดขึ้นก็ต่อเมื่อการตัดกันของ HMA/HMA3 บนไทม์เฟรม 1 นาทีสอดคล้องกับทิศทางของเทรนด์ 1 ชั่วโมงและเทรนด์ 15 นาที ส่วนการออกจากเทรดจะเกิดขึ้นเมื่อเทรนด์ของ higher-timeframe กลับทิศสวนทางกับโพซิชัน หรือเมื่อ LTF ตัดกลับ

อันตรายนี้อยู่ในโครงสร้างเอง ค่าของ higher-timeframe ทุกค่าต้องถูกคำนวณตอบใหม่ใน ทุก แท่ง 1 นาที — 60 ครั้งต่อชั่วโมง — และคำตอบแต่ละครั้งต้องใช้เฉพาะสิ่งที่บอทที่รันจริงจะรู้ ณ ขณะนั้นเท่านั้น เพียงแค่การอ่านค่าต่อแท่งหนึ่งใน 180 กว่าครั้ง (สามไทม์เฟรม เทรนด์ขาเข้าและขาออก เกตของระยะห่าง) แตะต้องแท่งที่ยังไม่ปรากฏขึ้นจริง คุณก็มี look-ahead ทันที พื้นที่ที่จะเกิดข้อผิดพลาดแบบ off-by-one นั้นมหาศาล และต่างจากการรั่วไหลจาก execution ตรงที่มันไม่ได้ประกาศตัวเองด้วยการขยับ fill

แท่งที่กำลังก่อตัวยังไม่มีราคาปิดสุดท้าย

นี่คือกับดักที่แท้จริง สมมติว่าตอนนี้เวลา 10:37 น. และคุณกำลังตัดสินใจบนแท่ง 1 นาทีที่เพิ่งปิดไป กฎของคุณต้องการเทรนด์ 1 ชั่วโมง แท่งเทียน 1 ชั่วโมงที่ครอบคลุมช่วง 10:00–11:00 น. ยังก่อตัวไม่เสร็จ — ราคาปิดสุดท้ายของมันจะยังไม่เป็นที่รู้จนกว่าจะถึง 10:59:59 น. แล้วคุณรู้อะไรจริง ๆ ที่เวลา 10:37 น.? รู้แค่ราคาปิดที่วิ่งอยู่ (running close) ของแท่งเทียนนั้นเท่าที่ผ่านมา ซึ่งก็คือราคาปิดของแท่ง 10:37 น. นั่นเอง ส่วนราคาปิด สุดท้าย ของแท่งเทียนนั้นยังอยู่ในอนาคตอีก 23 นาที

backtest แบบ multi-TF ที่ไร้เดียงสาทำสิ่งที่ดูเหมือนไม่มีพิษภัยเลย นั่นคือ resample ข้อมูลชุด 1 นาทีทั้งหมดให้เป็น 1 ชั่วโมงเพียงครั้งเดียวล่วงหน้า แล้วสำหรับแต่ละแท่ง 1 นาที ก็อ่านค่า "ราคาปิด 1 ชั่วโมง" แต่ค่าที่มันอ่านสำหรับทุกแท่ง 1 นาทีระหว่าง 10:00 ถึง 11:00 น. คือราคาปิด สุดท้าย ของช่วง 10:00–11:00 — ตัวเลขที่ในเวลาจริงยังไม่มีอยู่จนกว่าชั่วโมงนั้นจะจบลง ทุกการตัดสินใจภายในชั่วโมงนั้นถูกส่งต่อไปยังอนาคตถึง 59 นาทีโดยไม่รู้ตัว และนี่ไม่ใช่การรั่วไหลเล็ก ๆ เพราะราคาปิดของ higher-timeframe ถือเป็นตัวพยากรณ์ผลตอบแทน 1 นาทีในอนาคตอันใกล้ที่ทรงพลังที่สุดตัวหนึ่งเท่าที่จะเป็นไปได้ ซึ่งคุณกำลังจะใช้มันเทรด ดังนั้นการรั่วไหลของค่านี้จึงแทบจะเท่ากับการแอบอ่านเฉลยข้อสอบ นี่คือช่องทางการรั่วไหลของ indicator จากอนุกรมวิธานที่ถูกขยายให้รุนแรงขึ้น การแอบดูไม่ใช่แค่หนึ่งแท่งอีกต่อไป แต่อาจมากถึงหนึ่ง HTF period เต็ม ๆ

one-bar shift test จากบทความก่อนหน้าจับสิ่งนี้ไม่ได้ คุณสามารถเลื่อน fill ไปอีกหนึ่งแท่งได้ แต่ชุดข้อมูล 1 ชั่วโมงที่ resample มาก็ยังคงปนเปื้อนอยู่ดี — การรั่วไหลอยู่ในวิธีที่ indicator ถูกสร้างขึ้นมา ไม่ใช่อยู่ที่ว่าคุณทำธุรกรรมเมื่อไหร่

กฎของบอทที่รันจริง จำลองขึ้นมาใหม่อย่างตรงเป๊ะ: closed-bar semantics

บัฟเฟอร์ของ higher-timeframe ที่แท่ง i: แถวของราคาปิดจากแท่งเทียนที่ปิดสมบูรณ์แล้ว ตามด้วยแท่งเทียนหนึ่งแท่งที่กำลังก่อตัวซึ่งมีค่าเท่ากับราคาปิดฐาน (base close) ปัจจุบัน โดยราคาปิดสุดท้ายที่ยังถูกซ่อนอยู่ถูกขีดฆ่าออก

กฎที่ถูกต้องคือกฎเดียวกับที่บอทที่รันจริงใช้งานอยู่แล้ว ใน codebase ของเรา มันคือคลาสเล็ก ๆ ชื่อ RunningCandleBuffer ที่ย้ายมาจาก tick simulator ของบอทที่รันจริงแบบคำต่อคำ (verbatim) มันสตรีมแท่งฐาน (base bar) เข้าไปเป็นแท่งเทียนที่มีคาบเวลาคงที่ และในทุกแท่งฐาน จะคำนวณ indicator ของ HTF จาก array ที่เจาะจงมากตัวหนึ่ง:

all_closes = np.array(self.closes + [self.current_close], dtype=np.float64)

อ่านมันตามตัวอักษรตรง ๆ self.closes คือราคาปิด สุดท้าย ของแท่งเทียนที่ ปิดไปแล้ว — แท่งเทียนจะถูกเพิ่มเข้าไปในลิสต์ก็ต่อเมื่อมีการข้ามขอบเขตคาบเวลาใหม่เท่านั้น และค่าที่เก็บไว้คือราคาปิดของแท่งฐานสุดท้ายภายในคาบนั้น (candle_buffer.py:39–44) ส่วนแท่งเทียนที่กำลังก่อตัวจะให้ค่าเพียงตัวเดียวคือ self.current_close ซึ่งเป็นราคาปิดแบบ running — คือราคาปิดของแท่งฐานล่าสุด base_close[i] นี่คือปริมาณที่รู้ได้ที่แท่ง i โดยนิยาม ราคาปิด สุดท้าย ของแท่งเทียนที่กำลังก่อตัวจะไม่ถูกใช้เลย เพราะมันยังไม่มีอยู่จริง

ดังนั้น indicator ของ HTF ที่เวลา 10:37 น. จะเห็น [..., close(9:00 candle), close(10:37 so far)] เมื่อ 10:38 น. ปรากฏขึ้น ช่องสุดท้ายจะอัปเดตเป็น close(10:38) เมื่อข้าม 11:00 น. close(10:59) จะกลายเป็นค่าสุดท้ายของแท่งเทียนที่เพิ่งปิดไป และช่องแท่งที่กำลังก่อตัวใหม่จะเปิดขึ้น ไม่มีจุดไหนเลยที่การตัดสินใจภายในชั่วโมงนั้นจะแตะต้องราคาปิดสุดท้ายของช่วง 10:00–11:00 น. นั่นแหละ คือความหมายแบบ closed-bar: แท่งเทียนที่ปิดแล้วให้ราคาปิดที่เสร็จสมบูรณ์ ส่วนแท่งเทียนที่กำลังก่อตัวให้เพียงราคาปิดแบบ running ของมันเท่านั้น

เอนจินความเร็วสูงของเรา (engine_multitf.py) คือการ reimplement เรื่องนี้ในรูปแบบ vectorized ที่คอมไพล์ด้วย numba แทนที่จะใช้ loop ของ Python กับลิสต์ที่ขยายขึ้นเรื่อย ๆ มันจะคำนวณล่วงหน้าสำหรับทุกแท่งฐาน i ว่ามีแท่งเทียนกี่แท่งที่ปิดสมบูรณ์แล้ว (n_closed[i]) และจัดวาง window ของ HMA/HMA3 เป็น [closed candle closes…, base_close[i]] — คือปักราคาปิดแบบ running ไว้ในช่องสุดท้าย (engine_multitf.py:168–169) มันคือคณิตศาสตร์แบบเดียวกัน เพียงแต่คลี่ (unroll) ออกมาเพื่อความเร็วในสามไทม์เฟรม พร้อมกับเกตของระยะห่างเชิงทิศทาง (directional separation gates) สัญญาที่ชัดเจนคือ: ค่าที่แท่ง i ขึ้นอยู่กับ base_close[0..i] เท่านั้น

นั่นคือข้อกล่าวอ้าง ส่วนที่เหลือของบทความนี้คือวิธีที่เรา พิสูจน์ มัน เพราะข้อกล่าวอ้างที่เขียนไว้ใน docstring นั้นไม่มีค่าอะไรเลย

Parity คือสิ่งจำเป็น แต่ไม่เพียงพอ

เอนจินแบบ vectorized ที่ใช้ numba ซึ่งคลี่คลาสแบบสตรีมมิงออกมาเป็น loop ที่ชัดเจนนั้น คือจุดที่ข้อผิดพลาดแบบ off-by-one เพาะพันธุ์ได้ดีที่สุด ดังนั้นเกตแรกคือการทดสอบ parity แบบ bit-for-bit เทียบกับ reference ที่เป็นต้นแบบ (canonical) บนข้อมูลจริงชุดหนึ่ง — ETHUSDT 1 นาที ช่วงมกราคม–กุมภาพันธ์ 2024 จำนวน 86,400 แท่ง

เราตรวจสอบสองสิ่งที่เป็นอิสระจากกันเทียบกับ reference สองชุดที่เป็นอิสระจากกัน:

  • Indicator และการตัดกัน (cross) เทียบกับ RunningCandleBuffer ที่รันทีละแท่ง สำหรับแต่ละไทม์เฟรม เรา replay คลาสของบอทที่รันจริงตลอดทั้ง 86,400 แท่ง แล้วเปรียบเทียบเหตุการณ์การตัดกัน — แท่งที่เกิด ทิศทาง ระยะห่าง (separation) — ให้ตรงกัน แบบเป๊ะ พร้อมทั้งเปรียบเทียบค่า HMA/HMA3 ในระดับ floating-point tolerance (reference ใช้ np.dot ส่วนเอนจินใช้ loop ที่ชัดเจน ทำให้ลำดับการบวกต่างกันที่ระดับ ~1e-15) ผลการตัดกันตรงกันเป๊ะ: 408 ครั้ง บน HTF (1 ชั่วโมง, ความยาว HMA 21), 2,792 ครั้ง บน MTF (15 นาที/14), 3,691 ครั้ง บน LTF (1 นาที/50) ไม่มีเหตุการณ์การตัดกันแม้แต่ครั้งเดียวที่ต่างกันทั้งในแง่แท่งหรือทิศทาง
  • การเทรด เทียบกับการจำลองกฎการเทรดแบบ pure-Python ที่เป็นอิสระจากกัน ซึ่งขับเคลื่อนด้วยการตัดกันของ reference เอง สิ่งนี้จำลอง loop ของ backtest ที่รันจริงขึ้นมาใหม่ — เทรนด์ขาเข้าเปิดประตูให้กับการเข้าเทรด การกลับทิศของเทรนด์ขาออกหรือการตัดกันสวนทางของ LTF จะปิดโพซิชัน fill ที่ open[i+1] ค่าธรรมเนียมแบบไปกลับ (round-trip fee) 0.09% force-close ที่แท่งสุดท้าย — โดยไม่มีกลไก numba ของเอนจินเข้ามาเกี่ยวข้องเลย จากนั้นเราเปรียบเทียบทีละการเทรด: แท่งเข้า/ออก ฝั่ง (side) ราคาเข้า/ออก PnL เหตุผลในการออก และเวลารวมที่อยู่ในโพซิชัน (active-time)

threshold ของระยะห่าง (separation) ในการทดสอบไม่ใช่ค่า default ที่ไม่มีพิษภัย แต่ถูกเลือกมาเพื่อชนกับมุมที่เล่นยาก — threshold ของเทรนด์ ขาออก ของ MTF ที่ตั้งไว้สูงกว่า threshold ของเทรนด์ ขาเข้า ที่จับคู่กัน — ซึ่งบังคับให้เข้าสู่กิ่ง (branch) ของ "การนิยามเทรนด์ขาออกครั้งแรกในขณะที่โพซิชันยังเปิดอยู่" ซึ่ง reference ปฏิบัติต่อมันเสมือนเป็นการกลับทิศ (reversal) parity ต้องคงอยู่ได้แม้ในกรณีมุมเหล่านี้ ไม่ใช่แค่บนเส้นทางที่ง่าย

เมื่อเทียบทีละฟิลด์ การเทรดเหมือนกันทุกประการ: 466 การเทรด สำหรับคอนฟิกแบบ dual, 211 การเทรด สำหรับแบบ triple โดย PnL รวมตรงกันถึงระดับ 1e-12 และทุกฟิลด์ของการเทรดตรงกันภายใน tolerance ที่กำหนด สอง implementation ที่ไม่มีโค้ดร่วมกันเลย — เอนจินแบบ vectorized ที่คอมไพล์แล้ว กับ Python loop แบบไร้เดียงสาที่ทำงานบนการตัดกันของ implementation ที่สาม — ให้ผลการเทรดที่เหมือนกันจนถึงทศนิยมตัวสุดท้าย

นี่คือผลลัพธ์ที่แข็งแรงมาก แต่มัน ไม่ใช่การพิสูจน์ว่าไม่มี look-ahead parity บอกแค่ว่าเอนจินความเร็วสูงจำลอง reference ได้อย่างซื่อสัตย์ ถ้า reference เองรั่วไหล — ถ้า RunningCandleBuffer แอบดู — parity ก็จะจำลองการรั่วไหลนั้นออกมาอย่างซื่อสัตย์เช่นกันและผ่านการทดสอบไป ความสอดคล้องกันระหว่าง implementation บอกแค่ว่าทั้งสอง เหมือนกัน ไม่ได้บอกว่าทั้งสอง เป็นเหตุเป็นผล (causal) สำหรับความเป็นเหตุเป็นผล คุณต้องการการทดสอบอีกแบบหนึ่ง ที่ถามเอนจินโดยตรงว่าอดีตมองเห็นอนาคตได้หรือไม่

Shifted-future Probe: การพิสูจน์ที่แท้จริง

การตัดแนวตั้งที่ 60 เปอร์เซ็นต์ของชุดข้อมูล: ทุกอย่างทางด้านขวาถูกคูณและเขย่า ทุกอย่างทางด้านซ้ายถูกแช่แข็งให้เหมือนกันแบบ bit-for-bit

นิยามของ look-ahead เป็นเชิงปฏิบัติการ (operational) ดังนั้นจงทดสอบมันในเชิงปฏิบัติการเช่นกัน look-ahead หมายถึง การตัดสินใจในอดีตขึ้นอยู่กับข้อมูลในอนาคต ข้อความแย้งกลับ (contrapositive) คือการทดสอบที่คุณสามารถรันได้จริง: ถ้าคุณเปลี่ยนแปลงอนาคต แล้วการตัดสินใจในอดีตใด ๆ ขยับ นั่นแปลว่าอดีตกำลังอ่านอนาคตอยู่ ดังนั้นจงเปลี่ยนอนาคต — อย่างรุนแรง — แล้วจับตาดูอดีต

เลือกจุดตัด j ที่ 60% ของชุดข้อมูล (แท่งที่ 51,840 จาก 86,400) รบกวน (perturb) ทุก แท่งตั้งแต่ j เป็นต้นไป: คูณราคาปิดและราคาเปิดในอนาคตทั้งหมดด้วย 1.05 คำนวณ signal stack ทั้งหมดใหม่สำหรับทั้งสามไทม์เฟรมบนข้อมูลที่ถูกรบกวนแล้ว จากนั้นยืนยันว่าทุกอย่างที่อยู่ก่อนหน้า j อย่างเคร่งครัด เหมือนกันแบบ bitwise กับการรันที่ไม่ถูกรบกวน:

j = int(n * 0.6)                       # bar 51,840
cl2 = cl.copy(); cl2[j:] *= 1.05       # shove the future up 5%
op2 = op.copy(); op2[j:] *= 1.05

base = [precompute_tf_signals(cl,  ts, p, L) for (p, L) in tf_params]
pert = [precompute_tf_signals(cl2, ts, p, L) for (p, L) in tf_params]

for s0, s1 in zip(base, pert):
    assert eq_nan(s0.hma[:j],  s1.hma[:j])      # HMA identical, NaNs included
    assert eq_nan(s0.hma3[:j], s1.hma3[:j])
    assert np.array_equal(s0.cross[:j], s1.cross[:j])   # every cross event
    assert np.array_equal(s0.sep[:j],   s1.sep[:j])     # every separation

ไม่ใช่แค่ "ใกล้เคียง" ไม่ใช่แค่ "อยู่ใน tolerance" แต่คือ np.array_equal โดยที่ NaN ต้องตรงกับ NaN — ค่า HMA ทุกค่า ค่า HMA3 ทุกค่า แฟล็กการตัดกันทุกอัน และระยะห่างทุกอันบน 51,840 แท่งในอดีต ต้องเป็น float ตัวเดียวกันเป๊ะ จากนั้นก็ยืนยันแบบเดียวกันกับการเทรด: ทุกการเทรดที่จุดออกอยู่ก่อน j ต้องไม่เปลี่ยนแปลงในทุกฟิลด์ ถ้าการดันอนาคตขึ้น 5% ทำให้ HMA ในอดีตแม้แต่ตัวเดียวขยับที่ทศนิยมตำแหน่งที่สิบสอง แสดงว่าการตัดสินใจในอดีตได้อ้างอิงอนาคต และ probe นี้ก็ล้มเหลว

เอนจินของเราผ่านการทดสอบนี้ — ทั้งสามไทม์เฟรม และทั้งการจำลองการเทรดแบบ dual และ triple การรบกวนแท่งในอนาคต 34,560 แท่ง ทิ้งแท่งในอดีตไว้ 51,840 แท่งและทุกการเทรดที่ปิดอยู่ในนั้นให้เหมือนเดิมทุกประการ นี่ไม่ใช่แค่ความสอดคล้องกันระหว่าง implementation แต่เป็นการสาธิตโดยตรงว่าขอบเขตของข้อมูลใน เวลา นั้นยังคงอยู่จริง

การทดสอบที่ล้มเหลวไม่ได้เลยนั้นพิสูจน์อะไรไม่ได้เลย

มีวิธีหนึ่งที่จะผ่าน probe ข้างต้นได้โดยไม่พิสูจน์อะไรเลย: ถ้าการรบกวนนั้นเป็น no-op — ถ้าการคูณอนาคตด้วย 1.05 ไม่เปลี่ยนแปลง อะไรเลยที่ไหนเลย — งั้น "อดีตไม่เปลี่ยนแปลง" ก็จะเป็นจริงแบบซ้ำซาก (trivially true) และไม่มีข้อมูลอะไรเลย เครื่องหมายถูกสีเขียวบนการทดสอบที่ล้มเหลวไม่ได้เลยนั้นแย่ยิ่งกว่าไม่มีการทดสอบเสียอีก เพราะมันสร้างความมั่นใจปลอม ๆ ขึ้นมา ดังนั้น probe จึงมี assertion เพิ่มอีกสองอันเพื่อให้มัน "มีเขี้ยวเล็บ"

อนาคตต้องเปลี่ยนแปลงจริง ๆ เรายืนยันว่าการรบกวนนี้ ได้ เปลี่ยนแปลงการตัดกันที่ไหนสักแห่งใน [j, n) จริง:

assert not np.array_equal(s0.cross[j:], s1.cross[j:])   # probe has teeth

ตอนนี้ผลลัพธ์มีความหมายแล้ว: การดันขึ้น 5% แบบเดียวกันที่เขียนอนาคตใหม่ทั้งหมดนั้น ทิ้งอดีตไว้ให้เหมือนกันแบบ bit-for-bit การรบกวนนี้เป็นของจริง มันแพร่กระจายไปข้างหน้า และหยุดสนิทที่จุดตัด การรั่วไหลแบบทางเดียว — อดีตอ่านอนาคต — จะต้องซึมย้อนกลับข้าม j แต่มันไม่เป็นเช่นนั้น

ขอบเขตอยู่ที่แท่งปัจจุบันพอดี — ไม่เร็วไปหนึ่งแท่ง ไม่ช้าไปหนึ่งแท่ง ความล้มเหลวที่ละเอียดอ่อนกว่านั้นคือเอนจินที่เป็นเหตุเป็นผล (causal) แต่ ล้าหลัง (stale): มันเพิกเฉยต่อราคาปิดแบบ running ของแท่งปัจจุบัน แล้วตอบสนองช้าไปหนึ่งแท่ง (ไม่รั่วไหล แต่มี lag ที่การเทรดจริงจะไม่มี) หรือเอนจินที่ตอบสนองเร็วไปหนึ่งแท่ง (รั่วไหลหนึ่งแท่ง) ดังนั้นเราจึงรบกวนแท่งเดียว j (ด้วยค่า 1.02) และยืนยันสองสิ่งพร้อมกัน: อดีต [0, j) ไม่ถูกแตะต้อง และ hma[j] ตอบสนองทันที

cl3 = cl.copy(); cl3[j] *= 1.02        # nudge exactly one bar
s3 = precompute_tf_signals(cl3, ts, p_ltf, L_ltf)
assert eq_nan(s0.hma[:j], s3.hma[:j])  # nothing before j moves
assert s0.hma[j] != s3.hma[j]          # bar j reacts on the same bar

นี่ตรึงขอบเขตไว้อย่างแม่นยำ ราคาปิดแบบ running ของแท่งเทียนที่กำลังก่อตัวเข้าสู่ indicator ที่แท่ง j โดย ไม่มีความล่าช้าเลย และ ไม่มีการล่วงรู้ล่วงหน้าเลย: แท่ง j เห็นราคาปิดของตัวเองในทันที และไม่มีแท่งไหนก่อนหน้านั้นเห็นมันเลยแม้แต่น้อย นั่นคือขอบมีดที่แม่นยำที่ closed-bar semantics ควรจะยืนอยู่ และการทดสอบก็ยืนยันว่าเอนจินยืนอยู่บนขอบนั้นจริง

นี่คือเกตทั้งหมด — การตรวจสอบทั้ง 25 รายการที่ยืนขวางกั้นระหว่างเอนจินตัวนี้กับ backtest ที่ถูกปลอมแปลงขึ้นมา:

กลุ่ม สิ่งที่แต่ละการตรวจสอบยืนยัน จำนวน
Indicator และการตัดกัน (×3 ไทม์เฟรม) เหตุการณ์การตัดกันตรงเป๊ะเทียบกับ RunningCandleBuffer; ระยะห่างที่จุดตัดกัน; ค่า HMA/HMA3 (rtol 1e-9) 9
การเทรด (dual + triple) จำนวนการเทรด; ตรงกันทีละฟิลด์; PnL รวมถึงระดับ 1e-12; เวลาที่อยู่ในโพซิชัน (active-time) 8
Shifted-future probe (dual + triple) สัญญาณในอดีตไม่เปลี่ยนแปลงแบบ bitwise; probe มีเขี้ยวเล็บ (อนาคต เปลี่ยนแปลง จริง); การเทรดก่อน j ไม่เปลี่ยนแปลง; การรบกวนแท่งเดียวถูกจำกัดขอบเขตอย่างแม่นยำ 8
รวม 25

สองกลุ่มแรกยืนยันว่าเอนจินความเร็วสูง คือ live reference นั้นเอง กลุ่มที่สามยืนยันว่า reference นั้น เป็นเหตุเป็นผล (causal) คุณต้องการทั้งสามอย่าง: เอนจินความเร็วสูงที่รั่วไหล เอนจินที่รั่วไหลแต่เป็นเหตุเป็นผล และเอนจินที่เป็นเหตุเป็นผลแต่ล้าหลัง (lag) ล้วนเป็นความล้มเหลวสามแบบที่ต่างกัน และเกตนี้ตัดความเป็นไปได้ของแต่ละแบบออกไปทั้งหมด

ทำไม probe นี้จึงไม่ยึดติดกับไทม์เฟรมใดไทม์เฟรมหนึ่ง

ความสง่างามของ shifted-future probe คือมันไม่รู้และไม่สนใจว่าการรั่วไหลจะ อยู่ที่ไหน มันไม่เคยกล่าวถึงไทม์เฟรม การ resample หรือขอบเขตของแท่งเทียนเลย มันถามแค่ว่า: การรบกวนอนาคตทำให้อดีตขยับหรือไม่? นั่นทำให้มันเป็นเครื่องมือที่เหมาะสมพอดีสำหรับการรั่วไหลแบบ multi-TF ซึ่งเป็นสิ่งที่ shift-the-fill test พลาดไป

ลองพิจารณาบั๊กแบบ resample-ทั้งชุดข้อมูล-แบบไร้เดียงสาโดยตรง ถ้าสตรีม 1 ชั่วโมงถูกสร้างขึ้นโดยการ resample ทั้งชุดข้อมูลล่วงหน้า "ราคาปิด 1 ชั่วโมง" ที่อ่านที่แท่ง j-1000 (ซึ่งอยู่ลึกเข้าไปในชั่วโมงที่ปิดหลังจาก j) จะเป็นราคาปิด สุดท้าย ของแท่งเทียนที่ราคาปิดสุดท้ายของมันขึ้นอยู่กับแท่งที่ j และหลังจากนั้น คูณอนาคตด้วย 1.05 แล้วราคาปิดสุดท้ายนั้นก็เปลี่ยนไป — ดังนั้น indicator ของ HTF ที่ j-1000 ก็เปลี่ยน เกตที่ j-1000 ก็เปลี่ยน และการตัดสินใจในอดีตก็ขยับ probe จะสว่างขึ้นทันทีบนสตรีมของ HTF ที่แท่งซึ่งอยู่ก่อนจุดตัดไปนับพันก้าว

สตรีม HTF ของเอนจินเราไม่ขยับ เพราะที่ j-1000 แท่งเทียนที่กำลังก่อตัวให้เพียง base_close[j-1000] — ราคาปิดในอดีต — และราคาปิดสุดท้ายของแท่งเทียนนั้นก็ไม่เคยถูกอ้างอิงเลยจนกว่าจะข้ามขอบเขตไป probe นี้มองไม่เห็นกลไกภายในเลย แต่ก็ยังจับบั๊กได้ ซึ่งนี่คือสิ่งที่คุณต้องการจากการพิสูจน์อย่างแท้จริง: มันจำกัด พฤติกรรม (ไม่มีการตัดสินใจในอดีตขึ้นอยู่กับข้อมูลในอนาคต) แทนที่จะตรวจสอบ implementation (เรา index การ resample ถูกต้องหรือเปล่า?) พฤติกรรมคือสิ่งที่เทรดจริง ส่วน implementation คือสิ่งที่คุณหวังว่าจะตรงกับพฤติกรรมนั้น

Backtest และการเทรดจริงมีความจริงเดียวกัน

สองแผงที่มีแกนกลางร่วมกันหนึ่งเดียวซึ่งมีป้ายกำกับว่า RunningCandleBuffer: การกวาดค่า (sweep) แบบ backtest อยู่ด้านหนึ่ง บอทที่รันจริงอยู่อีกด้านหนึ่ง ทั้งสองเชื่อมต่อเข้ากับแกนเดียวกัน

ยังมีอีกเหตุผลหนึ่งที่ทำให้เรื่องนี้สำคัญยิ่งกว่าการตรวจสอบ backtest ทั่วไป reference ที่เอนจินถูกพิสูจน์เทียบด้วย — RunningCandleBuffer — ไม่ใช่ test fixture ที่ถูกเขียนขึ้นมาเพื่อให้ backtest ดูดี แต่มันคือตรรกะแท่งเทียนของบอทที่รันจริงเอง ที่ถูกยกมาแบบคำต่อคำจาก tick simulator ที่รันอยู่ใน production กฎ closed-bar ที่ probe ตรวจสอบนี้ คือกฎเดียวกับที่บอทที่รันจริงใช้ทำงานจริง ทีละแท่ง

ดังนั้น parity gate นี้จึงทำหน้าที่สองต่อ มันพิสูจน์ว่าเอนจินความเร็วสูงตรงกับ reference และเพราะ reference คือ แกนของบอทที่รันจริง มันจึงพิสูจน์ด้วยว่าเอนจินความเร็วสูงตรงกับการเทรดจริง บทความก่อนหน้าเตือนไว้ว่าการรั่วไหลคือคำอธิบายที่สะอาดที่สุดสำหรับช่องว่าง parity ระหว่าง backtest กับการเทรดจริง — บอทที่รันจริงคือจุดเดียวที่คุณไม่สามารถแอบดูได้ในเชิงกลไก ดังนั้น backtest ที่แอบดูจะเบี่ยงเบนทันทีที่มันไปทำงานจริง ในที่นี้ ช่องว่างนั้นถูกปิดโดยการออกแบบตั้งแต่ต้น: backtest กับบอทใช้บัฟเฟอร์แท่งเทียนเดียวกัน กฎการตัดกันเดียวกัน นิยามเดียวกันของ "รู้ ณ แท่ง i" ตัวเลขที่การค้นหา (search) เพิ่มประสิทธิภาพให้ ก็คือตัวเลขเดียวกับที่บอทสามารถเทรดได้จริง

นี่คือประเด็นทั้งหมดของการพิสูจน์ว่าไม่มี look-ahead แทนที่จะสมมติเอาเองว่าไม่มี การค้นหาแบบ multi-TF รันคอนฟิกนับพันตัว ถ้าเอนจินรั่วไหล การค้นหาก็จะพบคอนฟิกที่ใช้ประโยชน์จากการรั่วไหลนั้นอย่างดุดันที่สุด แล้วยื่นผู้ชนะที่ถูกปลอมแปลงให้คุณ — โหมดความล้มเหลวที่อนุกรมวิธานวัดได้ที่ Sharpe 15 จาก noise ล้วน ๆ probe นี้คือสิ่งที่ทำให้คุณเชื่อได้ว่าผู้ชนะนั้นเป็นของจริง ก่อนที่คุณจะนำมันไปเชื่อมต่อกับเงินทุนจริง

สิ่งที่ probe พิสูจน์และไม่พิสูจน์

ความเข้มงวดเกี่ยวกับการทดสอบนี้ตัดได้สองทาง ดังนั้นจงระบุขอบเขตของมันให้ชัดเจน shifted-future probe พิสูจน์คุณสมบัติเพียงหนึ่งเดียวที่เจาะจง: บนข้อมูลชุดนี้ ไม่มีสัญญาณหรือการตัดสินใจในการเทรดที่แท่ง j หรือก่อนหน้านั้นขึ้นอยู่กับแท่งใด ๆ หลัง j — ขอบเขตของข้อมูลใน เวลา ยังคงอยู่ตลอดเส้นทางของ indicator การตัดกัน และการเทรด นี่คือการรั่วไหลแบบ multi-TF อย่างเจาะจงที่เราตั้งเป้าจะกำจัดออกไป และเป็นคุณสมบัติที่การ code review ไม่สามารถยืนยันได้

มัน ไม่ พิสูจน์ว่ากลยุทธ์นี้มี edge เอนจินที่เป็นเหตุเป็นผล (causal) อย่างสมบูรณ์แบบก็ยังขาดทุนได้อย่างสุจริต และ probe นี้ก็ไม่ได้พูดถึงเรื่องนั้นเลย — ซึ่งก็ควรเป็นเช่นนั้น การพิสูจน์ว่าไม่มีการรั่วไหลกับการค้นหา edge ที่แท้จริงเป็นงานคนละอย่างกัน และการปนกันสองเรื่องนี้เข้าด้วยกันคือวิธีที่ backtest ที่รั่วไหลถูกนำไปใช้งานจริง มันไม่ครอบคลุมอคติที่ไม่เกี่ยวกับเวลา (non-temporal biases): survivorship ของตราสาร selection bias จากการรัน probe นี้หลังจากที่เอนจินดูดีแล้วเท่านั้น หรือโมเดลค่าธรรมเนียมที่ใจดีเกินจริง และในตัวมันเองก็ไม่ได้พิสูจน์ว่า fill ในการเทรดจริงตรงกับ fill ใน backtest — slippage และ latency เป็นช่องว่างจริงที่ probe นี้มองไม่เห็น เพราะมันทำงานอยู่บนเส้นทางการตัดสินใจ ไม่ใช่ที่ execution venue สิ่งที่ปิดช่องว่างนั้นคือข้อเท็จจริงแยกต่างหากที่ว่าแกนแท่งเทียนของเอนจินนั้นเหมือนกับของบอทที่รันจริงแบบบิตต่อบิต

ข้อควรระวังอย่างตรงไปตรงมาข้อหนึ่งเกี่ยวกับการออกแบบของ probe เอง: มันตัดที่จุดเดียวคือ j (60% ของชุดข้อมูล) คุณสมบัติที่มันตรวจสอบนั้นเป็นเนื้อเดียวกัน (uniform) ตลอด j — ไม่มีอะไรพิเศษเกี่ยวกับแท่ง 51,840 — ดังนั้นการตัดครั้งเดียวจึงเป็นการทดสอบที่ยุติธรรมสำหรับคุณสมบัติเชิงโครงสร้าง แต่เวอร์ชันที่ระแวดระวังยิ่งกว่าจะกวาด (sweep) j ไปทั่วทั้งชุดข้อมูล เราเห็นว่าการตัดครั้งเดียวที่เลือกมาอย่างดี บวกกับการตรวจสอบการระบุตำแหน่งแบบแท่งเดียว (single-bar localization) นั้นเพียงพอแล้ว เพราะการรั่วไหลที่ซ่อนตัวจากการรบกวนอนาคต 34,560 แท่ง แต่ปรากฏขึ้นที่จุดตัดอื่นนั้น จะต้องเป็นบั๊กที่แปลกประหลาดมาก ประเด็นคือการรู้ขอบเขตของหลักฐานของคุณ ไม่ใช่การแสร้งทำว่าการทดสอบเดียวเป็นตัวบ่งชี้แบบครอบจักรวาล (universal quantifier)

สรุปประเด็นสำคัญ

  1. กลยุทธ์แบบ multi-timeframe รั่วไหลผ่านแท่งที่กำลังก่อตัว ไม่ใช่ผ่าน fill การตัดสินใจภายในคาบ HTF ที่ยังไม่จบต้องใช้เพียงราคาปิดแบบ running ของแท่งเทียนนั้น (ราคาปิดฐานล่าสุด) เท่านั้น ไม่ใช่ราคาปิดสุดท้ายของมันเลย backtest แบบ resample-ทั้งชุดข้อมูล มอบทุกการตัดสินใจภายในคาบให้กับอนาคตได้มากถึงหนึ่ง HTF period เต็ม ๆ
  2. one-bar shift test เข้าไม่ถึงการรั่วไหลนี้ มันจับ look-ahead ที่เกิดจาก execution ได้ แต่การรั่วไหลแบบ multi-TF อยู่ในวิธีที่ indicator ถูกสร้างขึ้นมา คุณต้องใช้ probe แบบอื่น
  3. จำลองกฎของบอทที่รันจริงขึ้นมาใหม่อย่างตรงเป๊ะ แล้วจึงพิสูจน์มัน เราสร้าง closed-bar semantics ของ RunningCandleBuffer ของบอทขึ้นมาใหม่เป็นเอนจินแบบ vectorized และกั้นมันไว้หลังการตรวจสอบ 25 รายการ: การตัดกันตรงเป๊ะเทียบกับ reference (408 / 2,792 / 3,691) การเทรดตรงกันทีละฟิลด์ (466 dual, 211 triple) PnL ถึงระดับ 1e-12
  4. Parity เป็นสิ่งจำเป็น แต่ไม่เพียงพอ การตรงกับ reference พิสูจน์แค่ว่าคุณเหมือนกัน ไม่ได้พิสูจน์ว่าคุณเป็นเหตุเป็นผล reference ที่รั่วไหลแล้วถูกจำลองมาอย่างซื่อสัตย์ ก็ยังคงรั่วไหลอยู่ดี
  5. Shifted-future probe คือการพิสูจน์ที่แท้จริง รบกวนทุกแท่งที่ j หรือหลังจากนั้น ยืนยันว่าทุกสัญญาณและการเทรดก่อน j ไม่เปลี่ยนแปลงแบบ bitwise ถ้าอนาคตสามารถขยับอดีตได้ แสดงว่าคุณมี look-ahead
  6. ให้ probe มีเขี้ยวเล็บ ยืนยันว่าอนาคต เปลี่ยนแปลง จริง (การรบกวนไม่ใช่ no-op) และการรบกวนแท่งเดียวตอบสนองบนแท่งเดียวกันนั้นเอง (ไม่รั่วไหล ไม่มี lag) การทดสอบที่ล้มเหลวไม่ได้เลยนั้นพิสูจน์อะไรไม่ได้เลย
  7. เมื่อ backtest กับการเทรดจริงใช้แกนเดียวกัน การพิสูจน์ก็ถ่ายทอดไปด้วย เพราะเอนจินเหมือนกับตรรกะแท่งเทียนของบอทที่รันจริงแบบบิตต่อบิต การไม่มี look-ahead จึงเป็นคุณสมบัติของระบบที่รันจริงด้วยเช่นกัน — และช่องว่าง parity ระหว่าง backtest กับการเทรดจริงก็ถูกปิดโดยการออกแบบตั้งแต่ต้น

บทความก่อนหน้าแสดงให้เห็นว่าการรั่วไหลเพียงบรรทัดเดียวปลอมแปลง Sharpe ให้ได้ถึง 15 ได้อย่างไร บทความนี้แสดงวินัยที่ตรงกันข้าม: ไม่ใช่ "การรั่วไหลหลอกคุณได้อย่างไร" แต่เป็นวิธี พิสูจน์ อย่างเป็นกลไกและบนข้อมูลจริงว่าเอนจินตัวหนึ่งไม่มีการรั่วไหล จงรบกวนอนาคต ถ้าอดีตไม่สะทกสะท้าน แสดงว่าคุณกำลังเทรดอยู่ในปัจจุบัน

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

ผู้เขียน

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 เฉพาะ การวิเคราะห์ตลาด และการอัปเดตแพลตฟอร์ม

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