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

ภาษีของเฟรมเวิร์ก: เมื่อไลบรารี Backtest ของคุณช้ากว่าลูป Pandas แบบง่ายๆ

ภาษีของเฟรมเวิร์ก: เมื่อไลบรารี Backtest ของคุณช้ากว่าลูป Pandas แบบง่ายๆ
#algotrading
#backtest
#ประสิทธิภาพ
#parameter search
#vectorization
#benchmark
Part 2 of 10 · Collection
High-Performance Backtest Engines

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

มีข้อสมมติที่สบายใจอยู่ที่รากฐานของโปรเจกต์ algo-trading ส่วนใหญ่ นั่นคือไลบรารี backtesting ที่เติบโตเต็มที่และมีดาวเยอะบน GitHub นั้น เร็ว มันมีการมีส่วนร่วมมาหลายปี มี event loop จริง มีโมเดล broker มีระบบค่าคอมมิชชัน มันย่อมเอาชนะลูป pandas สุกเอาเผากินที่คุณอาจเขียนเองได้แน่นอน คุณจึงหยิบมันมาใช้ ต่อสายกลยุทธ์ของคุณเข้าไป แล้วเริ่ม parameter sweep — คอนฟิกูเรชันสองสามพันแบบ งานที่รันข้ามคืน คุณกลับมาตอนเช้า มันยังรันอยู่

เราทำเบนช์มาร์กเอนจิน backtest แปดตัวบน parameter sweep ชุดเดียวกัน และพบสิ่งที่ควรเปลี่ยนวิธีที่คุณเลือกเครื่องมือสำหรับการค้นหา เฟรมเวิร์ก event-driven โอเพนซอร์สยอดนิยมสองตัว — backtrader และ bt — รัน sweep นี้ ช้ากว่าลูป pandas ธรรมดาที่เราเขียนขึ้นมาเป็น baseline ทิ้งขว้าง ไม่ใช่ช้ากว่านิดหน่อย backtrader ใช้เวลาประมาณ 2.5 เท่าของ baseline pandas ส่วน bt ประมาณ 4.7 เท่า ในขณะเดียวกันเอนจินแบบ vectorized/compiled บนงานเดียวกันนี้ทำงานเร็วกว่า bt ประมาณ 13,000 เท่า

นี่คือภาษีของเฟรมเวิร์ก เครื่องมือ backtest ยอดนิยมถูกสร้างมาสำหรับการรันที่ตรงไปตรงมา ครั้งเดียว — กลยุทธ์เดียว ชุดข้อมูลเดียว การจับคู่คำสั่งซื้อขายที่พิถีพิถัน broker ที่ทำตัวเหมือน broker จริง นั่นคือสิ่งที่คุณต้องการพอดีสำหรับการตรวจสอบขั้นสุดท้ายหรือการเช็ก live-parity มันคือเครื่องมือที่ผิดพอดีสำหรับสิ่งที่งานวิจัย algo ใช้เวลาไปกับมันจริงๆ นั่นคือการรันกลยุทธ์เดียวกันหมื่นครั้งด้วยปุ่มปรับที่ต่างกันเล็กน้อย บทความนี้วัดภาษีนั้น อธิบายกลไก และให้กฎการตัดสินใจว่าเมื่อไหร่ที่เฟรมเวิร์ก backtesting "แบบจริงจัง" คือตัวเลือกที่ผิด

ตัวเลขทุกตัวในบทความนี้มาจากชุดทดสอบที่ทำซ้ำได้ชุดเดียว (benchmarks/bench_oss_engines.py, commit 250dbb5) บนภาระงานที่เหมือนกันทุกประการและล็อก parity ไว้ ในกรณีที่เราไม่ได้รันเอนจินตัวใดเอง เราจะบอกตรงๆ และแยกไว้ในหัวข้อที่ซื่อสัตย์ต่างหาก แทนที่จะกุตัวเลขขึ้นมา

หนึ่งครั้งกับหนึ่งหมื่นครั้ง

The same strategy computation flowing down three divergent paths: an event-driven track in red-orange forced to stop at every bar-station, a weights/rebalance tree of algo nodes in amber, and a single vectorized emerald-cyan beam that resolves the whole series at once — paradigm is destiny for sweep speed

ข้อเท็จจริงที่นิยาม parameter search คือ เอนจิน รันเป็นพันครั้ง แต่ การวิเคราะห์ เกิดขึ้นเพียงครั้งเดียว ต้นทุนคงที่ใดๆ ที่คุณต้องจ่ายเพื่อตั้งค่า backtest — สร้าง event loop, สร้างอินสแตนซ์ broker, จัดสรรอ็อบเจกต์ต่อแท่งราคา — คุณต้องจ่ายมันในทุกๆ คอมโบ ต้นทุนที่มองไม่เห็นในการรันครั้งเดียว ("ใครจะสนใจ 6 วินาที?") กลายเป็นบิลทั้งหมดข้าม sweep ("6 วินาที × 10,000 = 16.6 ชั่วโมง")

เอนจิน backtest แบ่งออกเป็นสามพาราไดม์ และพาราไดม์คือชะตากรรมของประสิทธิภาพการ sweep:

  1. Event-driven — เอนจินเดินไปทีละแท่งราคา ปล่อยเหตุการณ์ เรียก callback next()/onBars() ของคุณ ส่งคำสั่งซื้อขายผ่านอ็อบเจกต์ broker นี่คือสถาปัตยกรรมของ backtrader, backtesting.py, PyAlgoTrade, zipline และ nautilus_trader มันสะท้อนวิธีที่การเทรดจริงทำงานจริงๆ ซึ่งเป็นเหตุผลว่าทำไมมันจึงได้รับความเชื่อถือด้านความสมจริง — และเป็นเหตุผลเดียวกันว่าทำไมมันช้า: ต้นทุนแฝง Python ต่อแท่งราคาถูกจ่ายไป 150,000 ครั้งต่อคอมโบ
  2. Weights/rebalancebt อยู่ตรงนี้ คุณป้อนเมทริกซ์น้ำหนักเป้าหมายให้มัน และมัน rebalance ในวันที่คุณกำหนด ไม่มี callback ต่อแท่งราคา แต่ยังมีกราฟอ็อบเจกต์ต่อเหตุการณ์ (ต้นไม้ของ algo, บัญชีธุรกรรม) ที่ประเมินผลใน Python
  3. Vectorized / compiled — กลยุทธ์ทั้งหมดถูกแสดงเป็นการดำเนินการแบบอาร์เรย์ (vectorbt) เคอร์เนลที่คอมไพล์แบบ JIT (numba) หรือโค้ดเนทีฟ (Rust, เคอร์เนล GPU ของ MLX) ไม่มี Python ต่อแท่งราคาเลย ลูป ถ้ามี จะรันด้วยความเร็วระดับเครื่อง

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

ภาระงาน: กลยุทธ์เดียว ปุ่มปรับแปดสิบตัว เหมือนกันสำหรับทุกคน

เบนช์มาร์กจะซื่อสัตย์ก็ต่อเมื่อทุกเอนจินทำงานเดียวกัน ของเราตั้งใจทำให้เรียบง่าย — ภาระงานที่สิ่ง เดียว ที่ต่างกันระหว่างเอนจินคือตัวเอนจินเอง

  • ข้อมูล ซีรีส์ราคาปิดแบบ geometric-Brownian-motion สังเคราะห์ชุดเดียว: 150,000 แท่งราคา, seed=42, ความผันผวนต่อแท่ง sigma=0.0008, x0=30000 กำหนดแน่นอน จึงทำให้ใครก็ตามสามารถทำซ้ำได้ตรงบิตต่อบิต เป็น close-only โดยการออกแบบ — OHLC ถูกตั้งให้เท่ากับราคาปิดในทุกช่วง เพราะกลยุทธ์คือการตัดกันของราคาปิด
  • กลยุทธ์ การตัดกันของ Hull Moving Average: HMA ที่มีความยาว L เทียบกับตัวแปร HMA-of-a-third ที่เร็วกว่า (HMA เทียบ HMA3) อยู่ในตลาดตลอดเวลา สลับ long/short ทุกครั้งที่เกิดการตัดกัน นี่คืออินดิเคเตอร์จริงที่ไม่ใช่ของเล่น — moving average ถ่วงน้ำหนักที่ซ้อนกันสองชั้นบวกกับตัวปรับให้เรียบแบบ square-root-window — ไม่ใช่ SMA ของเล่น ดังนั้นงานต่อแท่งราคาจึงเป็นตัวแทนที่แท้จริง
  • Sweep ความยาว HMA จำนวน 80 ค่าครอบคลุม 6..200 นั่นคือ "หมื่นครั้งของการรัน" ที่ถูกย่อให้เล็กพอจะวัดได้โดยตรง: 80 คอมโบที่เป็นอิสระจากกัน แต่ละอันคือ backtest เต็มรูปแบบบน 150k แท่งราคา
  • ต้นทุน ค่าธรรมเนียมไปกลับ 0.09% แบ่งต่อฝั่งสำหรับเอนจินที่โมเดลค่าคอมมิชชันแยกฝั่ง การจับคู่คำสั่งในแท่งราคาเดียวกันที่ close[i] — สัญญาณในแท่งราคา i ถูกดำเนินการที่ราคาปิดของแท่งนั้น ซึ่งเป็นข้อตกลงที่เอนจินโปรดักชันของเราใช้

ตัวจับเวลาต่อคอมโบครอบคลุมสองสิ่งพอดี: การคำนวณ HMA ล่วงหน้าด้วย numpy และการรันเอนจิน การตั้งค่าที่เป็นแบบครั้งเดียวจริงๆ (โหลดข้อมูล, สร้างอ็อบเจกต์แท่งราคา) อยู่นอกตัวจับเวลา มีการวอร์มอัพหนึ่งครั้ง จากนั้นทำซ้ำแบบ best-of-N — และเนื่องจากเอนจิน event-driven ช้าพอที่ 80 คอมโบเต็มจะใช้เวลาหลายนาทีไปจนถึงมากกว่าหนึ่งชั่วโมง เราจึงจับเวลาตัวอย่างที่สม่ำเสมอของกริดแล้วประมาณค่าแบบเชิงเส้น เนื่องจากคอมโบเป็นอิสระจากกัน การประมาณค่าเชิงเส้นจึงแม่นยำตามความคาดหวัง และข้อตกลงเดียวกันนี้ถูกใช้กับ baseline pandas ด้วย ดังนั้นจึงไม่มีเอนจินใดได้เปรียบจากมัน

Parity: พิสูจน์ว่าทุกคนทำงานเดียวกัน

นี่คือกับดักที่การเปรียบเทียบเอนจินแบบไม่ระมัดระวังตกลงไป: เอนจินที่ "เร็ว" อาจแค่ทำงาน น้อยกว่า ถ้า backtrader บันทึกเทรด 2,700 รายการ และเอนจิน vectorized ของคุณบันทึกแค่ 40 รายการ เอนจิน vectorized ไม่ได้เร็วกว่า — มันผิด และการเปรียบเทียบก็ไร้ความหมาย

ดังนั้นเราจึงล็อกการเปรียบเทียบด้วย การตรวจสอบ parity ของจำนวนเทรด ที่ L=104 ตัวอ้างอิง numpy ให้เทรดที่ปิดแล้วพอดี 2,707 รายการ ทุกเอนจินต้องผลิตผลลัพธ์นั้นภายในค่าคลาดเคลื่อน ±1 มิฉะนั้นการรันจะยกเลิกด้วย assertion work-parity FAILED ค่าคลาดเคลื่อนนี้มีอยู่ก็เพราะเอนจินต่างๆ ไม่เห็นพ้องกันในเรื่องข้อตกลงการบันทึกบัญชี — ไม่ว่าจะเป็นการบังคับปิดโพซิชันสุดท้ายที่ยังเปิดอยู่และนับมันหรือไม่ หรือการเข้าครั้งแรกนับเป็น "เทรด" หรือไม่ — ไม่ใช่ในตัวเทรดเอง:

เอนจิน จำนวนเทรดที่รายงาน @ L=104 ข้อตกลง
ตัวอ้างอิง numpy 2707 round-trip ที่ปิดแล้ว
backtesting.py 2708 +1: บังคับปิดโพซิชันสุดท้ายเมื่อจบ
backtrader 2707 ไม่นับโพซิชันเปิดสุดท้าย
bt 2708 +1: นับการเข้าครั้งแรกเป็นธุรกรรม
PyAlgoTrade 2708 +1: นับการเข้าครั้งแรกเป็น fill

ทุกเอนจินลงเอยที่ 2707 ± 1 ไม่ว่าความแตกต่างของความเร็วจะออกมาเป็นอย่างไร มันไม่ใช่สิ่งประดิษฐ์จากเอนจินตัวใดตัวหนึ่งที่แอบข้ามงานไป นี่คือวินัยที่ทำให้เราวางเฟรมเวิร์ก event-driven กับเคอร์เนล GPU ไว้ในตารางเดียวกันได้อย่างมีความหมาย

ผลลัพธ์

นี่คือตารางทั้งหมด เรียงจากเร็วที่สุดไปช้าที่สุด combos/s คือปริมาณงาน คอลัมน์สุดท้ายคือเวลาที่ sweep 80 คอมโบเต็มใช้ แถว baseline คือ M0 — เอนจิน pandas ธรรมดา ลูป for วนบนแท่งราคาพร้อมการบันทึกบัญชีแบบสเกลาร์ สิ่งที่คุณอาจเขียนในบ่ายวันหนึ่งแล้วทิ้งไป ทุกอย่างที่ ช้ากว่า baseline นั้นตัวหนา

เอนจิน combos/s พาราไดม์ Sweep 80 คอมโบเต็ม
MLX GPU kernel 779 vectorized (Apple GPU) 0.10 วินาที
native Rust ~350 compiled 0.23 วินาที
mp + numba 246 compiled JIT + multiprocess 0.33 วินาที
vectorbt 56.9 vectorized (numpy/numba) 1.4 วินาที
numba (single core) 39.7 compiled JIT 2.0 วินาที
backtesting.py 1.42 event-driven 56 วินาที
PyAlgoTrade 0.51 event-driven 2.6 นาที
M0 — naive pandas + loop 0.28 scalar baseline 4.8 นาที
backtrader 0.11 event-driven 12.7 นาที
bt 0.06 weights / rebalance 22.5 นาที

อ่านตารางจากบนลงล่างแล้วพาราไดม์จะจัดเรียงตัวเอง: ห้าอันดับบนล้วนเป็น vectorized หรือ compiled ห้าอันดับล่างล้วนเป็น event-driven หรือ object-graph — โดยลูป pandas ธรรมดาอยู่ เหนือ เฟรมเวิร์กเติบโตเต็มที่ยอดนิยมสองตัว ช่วงจากบนสุดถึงล่างสุดคือสี่อันดับของขนาด บนภาระงาน 2,707 เทรดที่เหมือนกันทุกประการ เคอร์เนล MLX จบ sweep ในหนึ่งในสิบวินาที ส่วน bt ต้องใช้ยี่สิบสองนาทีครึ่ง นั่นคือปัจจัยประมาณ 13,000 เท่า

เรื่องอื้อฉาวตรงกลางตาราง

The paradox at the middle of the table: a plain grey-blue naive for-loop runner crossing the finish line ahead of two ornate, feature-heavy event-driven framework machines glowing red-orange that are still laboring behind, weighed down by broker models and analyzer stacks — the humble baseline beating the mature frameworks on the identical 2,707-trade workload

ตัวเลขที่สะดุดตาอยู่ที่สุดขั้ว แต่ผลลัพธ์ที่ ให้บทเรียน อยู่ตรงกลาง: backtrader (0.11 combos/s) และ bt (0.06 combos/s) ต่างก็ช้ากว่า baseline pandas ธรรมดา (0.28 combos/s)

สิ่งนี้สมควรที่จะซึมซับ M0 ไม่ใช่เอนจินที่ฉลาด มันคือลูป for ของ Python ที่ทำ index เข้าไปใน DataFrame ติดตามโพซิชันและเงินสดด้วยสเกลาร์ธรรมดา และ append เทรดเข้าไปในลิสต์ — "ตัวควบคุม" ที่ไม่ปรับแต่งใดๆ โดยเจตนา ที่เรารวมไว้เพื่อมีบางอย่างที่แย่อย่างชัดเจนให้เอาชนะ การเข้าถึงข้อมูลรายแถวของ pandas นั้นขึ้นชื่อเรื่องความช้า และเราก็อาศัยจุดนั้น กระนั้นไลบรารี backtesting ที่ถูกแนะนำมากที่สุดสองตัวในระบบนิเวศกลับแพ้มัน: backtrader แพ้ 2.5 เท่า bt แพ้ 4.7 เท่า

รายละเอียดที่ทำให้เรื่องนี้ยังคงซื่อสัตย์: ไม่ใช่ทุกเอนจิน event-driven จะช้ากว่า pandas backtesting.py (1.42 combos/s) ชนะ baseline ไป 5 เท่า เพราะมันเป็น event loop ที่คล่องตัวและอาศัย numpy ที่ลดการสร้างอ็อบเจกต์ต่อแท่งราคาให้น้อยที่สุด PyAlgoTrade (0.51) ก็แซง baseline ไปเช่นกัน ดังนั้น "event-driven" จึงไม่ใช่โทษประหารโดยอัตโนมัติ — แต่ยิ่งกลไกต่อแท่งราคาหนักเท่าไหร่ ก็ยิ่งแย่ลงเท่านั้น และ backtrader กับ bt แบกกลไกที่หนักที่สุดตรงนี้ พาราไดม์กำหนดเพดาน การอิมพลีเมนต์ตัดสินว่าคุณจะลงจอดตรงไหนใต้เพดานนั้น

ประเด็นไม่ใช่ว่านี่เป็นไลบรารีที่แย่ โมเดล broker ของ backtrader และการออกแบบต้นไม้ของ algo ของ bt มีอยู่เพื่อซื้อ ความถูกต้องและความสามารถในการแสดงออก ให้คุณ — การจัดการคำสั่งซื้อขายที่สมจริง การ rebalance พอร์ต ตัววิเคราะห์ คุณสมบัติเหล่านั้นมีต้นทุนรันไทม์ และต้นทุนนั้นมองไม่เห็นเมื่อคุณรันครั้งเดียว ข้าม sweep มันคือเรื่องราวทั้งหมด

ทำไมเอนจิน event-driven จึงจ่ายภาษี

Where the framework tax is levied, per bar: a single bar-iteration exploded into its costly components — a materialized bar object, a callback frame, a broker routing an order, a ledger being appended — each a glowing red-orange toll booth skimming a coin, repeated down a vast receding corridor of twelve million identical bar-iterations

กลไกนี้ไม่ใช่เรื่องลึกลับ backtest แบบ event-driven ต่อแท่งราคาหนึ่งแท่งทำสิ่งประมาณนี้:

  1. เดินนาฬิกาไปข้างหน้า ตัดแท่งราคาถัดไปออกจาก data feed และสร้างมันขึ้นเป็นอ็อบเจกต์ (Bar, Line, dict)
  2. ยิง callback เข้าไปในโค้ดผู้ใช้ (next(), onBars()) ซึ่งเป็นการเรียกฟังก์ชัน Python ที่มี frame ของตัวเอง
  3. ภายใน callback สอบถามสถานะ broker/โพซิชัน ผ่านการเรียกเมธอดและการค้นหา attribute อีกครั้ง
  4. ถ้ามีการสร้างคำสั่งซื้อขาย ให้ส่งมันผ่าน broker: ตรวจสอบความถูกต้อง ตรวจ margin/เงินสด กำหนดตาราง fill เปลี่ยนแปลงอ็อบเจกต์พอร์ต append เข้าไปในบัญชีธุรกรรม
  5. อัปเดตตัววิเคราะห์ ตัวสังเกตการณ์ และการบันทึกบัญชีใดๆ ที่เฟรมเวิร์กดูแลอยู่

ทีนี้คูณด้วย 150,000 แท่งราคา แล้วคูณด้วย 80 คอมโบ: สิบสองล้านการวนรอบแท่งราคาต่อ sweep แต่ละครั้งคือกำมือของการจัดสรรอ็อบเจกต์ระดับ Python และการ dispatch แบบไดนามิก ต้นทุนแฝงต่อการดำเนินการของ Python — หลักสิบถึงหลักร้อยนาโนวินาทีสำหรับการค้นหา attribute หรือการจัดสรรเล็กๆ — เป็นเรื่องเล็กน้อยเมื่อเกิดครั้งเดียว แต่หายนะเมื่อเกิดสิบสองล้านครั้ง กรณีของ bt เป็นตัวแปรของโรคเดียวกัน แม้ว่ามันจะ rebalance เฉพาะในวันที่มีการเทรดแทนที่จะเป็นทุกแท่งราคา แต่การ rebalance แต่ละครั้งประเมินต้นไม้ของอ็อบเจกต์ algo และแตะบัญชีพอร์ตที่อาศัย pandas และมี 2,707 ครั้งของสิ่งนั้นต่อคอมโบ คูณด้วย 80

ลูป pandas ธรรมดาชนะ backtrader และ bt ด้วยเหตุผลตรงไปตรงมา: มันทำงานน้อยกว่าต่อแท่งราคา มันข้าม broker ข้ามอ็อบเจกต์เหตุการณ์ ข้ามกองตัววิเคราะห์ ข้าม state machine การส่งคำสั่งซื้อขาย มันจ่ายภาษีต่อแถวที่น่าเกลียดของ pandas แต่ภาษีน่าเกลียดตัวเดียวนั้นยังถูกกว่าภาษีที่เป็นระเบียบ ครบครัน และเป็นอ็อบเจกต์ต่อเหตุการณ์ของเฟรมเวิร์ก เมื่อคุณตัด backtest ให้เหลือแค่ "โพซิชัน × ผลตอบแทนถัดไป − ค่าธรรมเนียม" สิ่งที่เฟรมเวิร์กทำต่อแท่งราคาส่วนใหญ่คือต้นทุนแฝงที่คุณไม่ได้ใช้ระหว่างการค้นหา

และนี่คือกับดัก: ต้นทุนแฝงคือ เหตุผลที่คุณเลือกเฟรมเวิร์กนั้น คุณต้องการ broker ที่สมจริง คุณต้องการตัววิเคราะห์ ระหว่างการตรวจสอบขั้นสุดท้าย คุณต้องการมันทั้งหมด ระหว่างการค้นหา 10,000 คอมโบที่คุณอ่านแค่ objective ตัวเดียวออกมาจากปลายอีกด้าน คุณกำลังจ่ายเงินเพื่อรถลีมูซีนวิ่งรอบสนาม

อีกฝั่งของตาราง: vectorized และ compiled

ด้านบนของตารางคือสิ่งที่เกิดขึ้นเมื่อคุณลบ Python ต่อแท่งราคาออกไปทั้งหมด

  • vectorbt (56.9 combos/s) แสดงกลยุทธ์ทั้งหมดเป็นการดำเนินการอาร์เรย์ numpy/numba ไม่มีลูปแท่งราคาใน Python เลย — สัญญาณ โพซิชัน PnL ล้วนเป็นระดับอาร์เรย์ มันรัน sweep ใน 1.4 วินาที เทียบกับ 22.5 นาทีของ bt: เร็วกว่าประมาณ 950 เท่าบนงานเดียวกันทุกประการ (เราครอบคลุมการออกแบบของ vectorbt อย่างลึกซึ้งกว่านี้ใน vectorbt overview และการเปรียบเทียบที่กว้างขึ้นใน pandas-vs-polars)
  • numba (39.7) คือลูป pandas ที่ไม่เปลี่ยนรูปร่าง แต่ถูกคอมไพล์แบบ JIT เป็นโค้ดเนทีฟ อัลกอริทึมเดียวกับ M0 แต่ @njit เปลี่ยน 0.28 combos/s ให้เป็น 39.7 — เร็วขึ้นประมาณ 140 เท่าจากดีคอเรเตอร์ตัวเดียว เพราะต้นทุนแฝงของ interpreter ที่ครอบงำลูปสเกลาร์เพียงแค่ระเหยหายไป
  • mp + numba (246) รันเคอร์เนลที่คอมไพล์ข้ามคอร์ CPU คอมโบเป็นแบบขนานอย่างง่ายดาย — แต่ละอันเป็นอิสระจากกัน — ดังนั้น multiprocessing จึงขยายตัวเกือบเป็นเส้นตรงบน JIT
  • native Rust (~350) ลบกาว Python ตัวสุดท้ายออก: sweep ทั้งหมดเป็นเนทีฟ
  • MLX GPU (779) ทำแผนที่ sweep ไปยังเคอร์เนล GPU ของ Apple-silicon 80 คอมโบกลายเป็น 80 เลนขนานของการคำนวณเลขคณิต sweep จบก่อนที่คุณจะปล่อยปุ่ม enter

มีสองสิ่งที่ควรระบุให้ชัดเจน ประการแรก numba พิสูจน์ว่าพาราไดม์สำคัญกว่าภาษา M0 และ numba รัน อัลกอริทึมเดียวกัน — ความแตกต่างคือหนึ่งเป็น Python แบบ interpreted ต่อแท่งราคา ส่วนอีกอันคอมไพล์แล้ว นั่นคือภาษีของเฟรมเวิร์กทั้งหมดใน A/B ที่ควบคุมได้: ~140 เท่าจากการลบ interpreter ออกจากลูปด้านใน ประการที่สอง การกระโดดจาก numba (39.7) ไปยัง mp+numba (246) ไปยัง MLX (779) ไม่ได้เกี่ยวกับเอนจินอีกต่อไป — มันเกี่ยวกับการจัดการและฮาร์ดแวร์ เมื่อภาษีต่อแท่งราคาหายไป ความเร็วก็กลายเป็นคำถามว่าคุณรันกี่คอมโบแบบขนานและบนซิลิคอนแบบไหน เราเดินผ่านความก้าวหน้าเต็มรูปแบบนั้นใน backtest engine speed ladder และเหตุผลที่ไมล์สุดท้ายถูกครอบงำโดยต้นทุน process/serialization ใน บทความ IPC-tax

สิ่งที่เราไม่ได้วัด (และทำไมเราถึงบอกคุณ)

ความน่าเชื่อถือของเบนช์มาร์กอยู่ที่สิ่งที่มันปฏิเสธที่จะปลอมแปลง เรารันเอนจินแปดตัวแบบ end-to-end ภายใต้ parity เฟรมเวิร์กที่มีชื่อเสียงหลายตัวที่เรา ไม่ได้ ใส่ตัวเลขให้ และเราขอลิสต์มันอย่างซื่อสัตย์แทนที่จะประมาณตัวเลขที่เราไม่ได้วัด:

  • zipline / zipline-reloaded — event-driven สายเลือด Quantopian การตั้งค่าหนักมาก (ปฏิทินการเทรดเต็มรูปแบบและ data bundle) ซึ่งทำให้การจับเวลาต่อคอมโบแบบเทียบเท่ากันยุ่งยาก ในเชิงสถาปัตยกรรมมันอยู่ในกลุ่ม event-driven เดียวกับ backtrader เราคาดว่ามันจะอยู่ใกล้ปลายด้านนั้นของตาราง แต่เราไม่ได้พิสูจน์มัน
  • nautilus_trader — event-driven ด้วยแกน Rust/Cython ออกแบบมาอย่างชัดเจนสำหรับ live-parity แกนของมันคอมไพล์แล้ว ดังนั้นมันจึงเป็นเอนจิน event-driven ที่มีแนวโน้มมากที่สุดที่จะ ไม่ จ่ายภาษี Python เต็มจำนวน — เป็นการวัดที่น่าสนใจอย่างแท้จริงที่เรายังไม่ได้รัน
  • QuantConnect Lean — อิงกับ C# เป็นรันไทม์ที่ต่างออกไปโดยสิ้นเชิง ไม่สามารถเปรียบเทียบโดยตรงในชุดทดสอบ Python ได้
  • Jesse — event-driven เน้นคริปโต เราวิจารณ์การออกแบบของมันใน บันทึกแยกต่างหาก แต่ไม่ได้เบนช์มาร์กมันที่นี่
  • QSTrader — event-driven เน้นพอร์ตโฟลิโอ ข้อจำกัดพาราไดม์เดียวกัน
  • fastquant — เราลองแล้ว แต่การติดตั้ง/API เสียหายในสภาพแวดล้อมของเรา จึงไม่มีตัวเลข เราจะไม่เดา

ข้อควรระวังที่ซื่อสัตย์สองข้อเกี่ยวกับตัวเลขที่เรา รายงาน จริง ตัวเลขของ vectorbt, numba, mp+numba, native-Rust และ MLX มาจาก engine ladder ของเราเองบนภาระงานเดียวกัน ไม่ใช่จากชุดทดสอบ OSS ที่ผลิตสี่แถว event-driven — มันคืองานเดียวกันแต่ใช้อุปกรณ์การวัดที่ต่างกัน และตัวเลข native-Rust เป็นค่าประมาณ ~350 ไม่ใช่การวัดที่แม่นยำ และ combos/s แบบสัมบูรณ์ขึ้นอยู่กับฮาร์ดแวร์เฉพาะ สิ่งที่ยังคงเป็นจริงคือ ลำดับและอัตราส่วน ซึ่งใหญ่พอ (13,000 เท่าจากบนสุดถึงล่างสุด, 2.5–4.7 เท่าสำหรับการกลับด้าน pandas-เทียบกับ-เฟรมเวิร์ก) ที่ไม่มีความแตกต่างของฮาร์ดแวร์ที่สมเหตุสมผลใดจะพลิกมันได้

ในการปกป้องเอนจิน event-driven

The event-driven engine in its proper role: a single meticulous high-fidelity validation run of one surviving strategy, rendered as a precise emerald-and-cyan instrument modelling a realistic fill, respecting margin and the order lifecycle — the same per-bar machinery that was pure waste during search shown here as valuable craftsmanship, one careful dress rehearsal before deployment

การอ่านเรื่องนี้ว่า "เอนจิน event-driven แย่" นั้นง่ายเกินไป นั่นคือบทเรียนที่ผิด และไม่ยุติธรรม

เอนจิน event-driven ถูกสร้างมาสำหรับงานที่ต่างออกไป และมัน เก่งในสิ่งนั้น broker ต่อแท่งราคา วงจรชีวิตของคำสั่งซื้อขาย ตรรกะการ fill ตัววิเคราะห์ — กลไกเหล่านั้นมีอยู่เพื่อทำให้ backtest คล้ายกับการเทรดจริงมากที่สุดเท่าที่จะเป็นไปได้ เมื่อเป้าหมายของคุณคือการซ้อมใหญ่ที่น่าเชื่อถือเพียงครั้งเดียวของกลยุทธ์ที่คุณกำลังจะนำไปใช้จริง คุณ ต้องการ ให้เอนจินเหงื่อออกกับทุก fill จำลองการดำเนินการบางส่วน เคารพ margin และปฏิเสธที่จะให้คุณเทรดที่ราคาที่คุณไม่สามารถได้รับจริงๆ ความสมจริงนั้นคือตัวผลิตภัณฑ์ ต้นทุนรันไทม์ของมันคือราคาของความสมจริง และสำหรับการรันครั้งเดียวราคานั้นแทบไม่มีนัยสำคัญ

ความล้มเหลวไม่ใช่เอนจิน แต่คือการใช้มันในเฟสที่ผิด งานวิจัยมีสองเฟสที่แตกต่างกันโดยมีข้อกำหนดตรงข้ามกัน:

  • การค้นหา ต้องการปริมาณงาน คุณกำลังสำรวจภูมิทัศน์ ซึ่งส่วนใหญ่เป็นขยะ และคุณต้องประเมินจุดเป็นพันเพื่อหาไม่กี่จุดที่คุ้มค่าที่จะดูรอบสอง ความสมจริงต่อจุดแทบไม่สำคัญ — คุณกำลังจัดอันดับ ไม่ใช่นำไปใช้จริง ตรงนี้ภาษีของเฟรมเวิร์กเป็นการสูญเปล่าล้วนๆ
  • การตรวจสอบ ต้องการความสมจริง คุณมีผู้สมัครไม่กี่คน และคุณต้องรู้ให้แม่นยำที่สุดเท่าที่จะเป็นไปได้ว่าพวกมันรอดจากการดำเนินการที่สมจริง ค่าธรรมเนียม การเลื่อนราคา และ กับดัก look-ahead ที่ทำให้ผลตอบแทนบนกระดาษพองตัว หรือไม่ ตรงนี้เอนจิน event-driven คุ้มค่าต้นทุนของมัน

ความผิดพลาดที่ภาษีของเฟรมเวิร์กลงโทษคือการรัน การค้นหา ของคุณบนเอนจิน การตรวจสอบ ของคุณ — จ่ายในราคารถลีมูซีนเพื่อสำรวจภูมิทัศน์ที่คุณจะทิ้ง 99% ไป

กฎการตัดสินใจ

บทสรุปเชิงปฏิบัติย่อลงเหลือการตัดสินใจเดียว:

ค้นหาบนเอนจินแบบ vectorized/compiled ตรวจสอบผู้รอดชีวิตบนเอนจิน event-driven

โดยเฉพาะเจาะจง:

  1. สร้างหรือยืมเคอร์เนลที่เร็วสำหรับ sweep vectorbt ถ้าคุณต้องการของสำเร็จรูป ลูปที่คอมไพล์ด้วย numba ถ้ากลยุทธ์ของคุณไม่ vectorize ได้อย่างเรียบร้อย (ดีคอเรเตอร์ @njit เพียงตัวเดียวซื้อความเร็วขึ้นประมาณ 140 เท่าที่นี่) รันพื้นที่พารามิเตอร์ทั้งหมดผ่านมัน
  2. อย่ารัน sweep ขนาดใหญ่บน backtrader, bt, zipline หรือเฟรมเวิร์ก event-driven หนักตัวใดก็ตาม ถ้า sweep บนหนึ่งในนั้นคือคอขวดของคุณ ทางแก้ไม่ใช่เครื่องที่ใหญ่กว่า — มันคือเอนจินที่ผิด แม้แต่ลูป pandas ธรรมดาก็ยังชนะสองในนั้น
  3. เลื่อนขั้นรายชื่อสั้นๆ ไปยังเอนจิน event-driven สำหรับการตรวจสอบความสมจริง เอาผู้รอดชีวิตไม่กี่คนมารันใหม่บนเอนจินที่สมจริง ที่ซึ่งโมเดล broker และตรรกะ fill สามารถเปิดเผยปัญหาที่เคอร์เนลเร็วนั้นทำให้เป็นนามธรรมไป
  4. บังคับ parity ระหว่างสองอย่าง เอนจินที่เร็วและเอนจินความสมจริงต้องเห็นพ้องกันในจำนวนเทรดและ PnL บนคอนฟิกูเรชันคงที่ (การตรวจสอบ ±1-เทรดของเราที่ L=104) ไม่เช่นนั้นการค้นหาและการตรวจสอบกำลังวัดกลยุทธ์ที่ต่างกัน และไปป์ไลน์ทั้งหมดก็เป็นเรื่องโกหก

นี่คือสถาปัตยกรรมสองความเร็วเดียวกันกับที่ปรากฏขึ้นเมื่อใดก็ตามที่การค้นหาและการตรวจสอบมีโปรไฟล์ต้นทุนตรงข้ามกัน และเป็นเหตุผลว่าทำไมสแต็กของเราเองจึงเก็บพาธแบบ vectorized/compiled ที่เร็วไว้สำหรับ parameter search และสงวนกลไกหนักไว้สำหรับ การประเมิน objective ขั้นสุดท้ายและ การตรวจสอบ plateau

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

  1. ยอดนิยม ≠ เร็วสำหรับ sweep บนภาระงานที่เหมือนกันทุกประการและล็อก parity ไว้ชุดเดียว (150k แท่งราคา, 80 คอมโบ HMA-cross, 2,707 เทรด) backtrader (0.11 combos/s) และ bt (0.06) ต่างก็รัน ช้ากว่า ลูป pandas ธรรมดา (0.28) เฟรมเวิร์กที่เติบโตเต็มที่และมีดาวเยอะไม่ใช่ตัวเลือกที่เร็วโดยอัตโนมัติ
  2. ภาษีของเฟรมเวิร์กเป็นแบบต่อแท่งราคา และ sweep ทวีคูณมัน สิบสองล้านการวนรอบแท่งราคาต่อ sweep แต่ละครั้งพกพาอ็อบเจกต์เหตุการณ์ callback และการเดินทางไปกลับของ broker ต้นทุนที่มองไม่เห็นในการรันครั้งเดียวคือบิลทั้งหมดข้ามหมื่นครั้ง
  3. พาราไดม์กำหนดเพดาน เอนจินแบบ vectorized/compiled (vectorbt 56.9, numba 39.7, mp+numba 246, native Rust ~350, MLX 779) ชนะเอนจิน event-driven ด้วยสองถึงสี่อันดับของขนาด — สูงถึง ~13,000 เท่าจากบนสุดถึงล่างสุด อัลกอริทึมเดียวกัน เพียงแค่คอมไพล์แบบ JIT เร็วขึ้น 140 เท่า
  4. ไม่ใช่เอนจิน event-driven ทุกตัวจะเท่ากัน backtesting.py (1.42) และ PyAlgoTrade (0.51) ยังคงชนะ baseline ธรรมดา ภาษีขยายตัวตามความหนักของกลไกต่อแท่งราคา การอิมพลีเมนต์ตัดสินว่าคุณจะลงจอดตรงไหนใต้เพดาน
  5. สองเอนจิน สองเฟส ค้นหาบนเคอร์เนลแบบ vectorized/compiled ตรวจสอบผู้รอดชีวิตบนเอนจิน event-driven ที่สมจริง บังคับ parity ของจำนวนเทรด/PnL ระหว่างกัน เพื่อให้ทั้งสองวัดกลยุทธ์เดียวกัน
  6. ซื่อสัตย์เกี่ยวกับสิ่งที่คุณวัด เราเบนช์มาร์กเอนจินแปดตัวภายใต้ parity และลิสต์ตัวที่เราไม่ได้ทำ (zipline, nautilus_trader, Lean, Jesse, QSTrader และการติดตั้ง fastquant ที่รันไม่ได้) แทนที่จะกุตัวเลขขึ้นมาสำหรับมัน

บทสรุปที่ไม่สบายใจ: ถ้า parameter sweep คือคอขวดของคุณ ปัญหาน่าจะไม่ใช่เครื่องของคุณและไม่ใช่กลยุทธ์ของคุณ มันคือคุณกำลังรันการค้นหาบนเอนจินที่ถูกสร้างมาสำหรับการรันที่ตรงไปตรงมาครั้งเดียว — และคุณจะเร็วกว่าด้วยลูป pandas ที่คุณอายเกินกว่าจะเก็บไว้

ชุดทดสอบเต็ม, assertion parity และผลลัพธ์ JSON ดิบต่อเอนจินอยู่ใน benchmarks/bench_oss_engines.py และ benchmarks/results_oss/ ที่ commit 250dbb5 สำหรับปลายด้าน compiled/GPU ของ ladder ดู backtest engine speed ladder และ การวิเคราะห์ IPC-tax

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

ผู้เขียน

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

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