วิธีทำ Backtest ด้วย Python ให้ผลลัพธ์แม่นยำและทำซ้ำได้
เรียนรู้วิธีทำ Backtest ใน Python แบบทำซ้ำได้โดยไม่ต้องใช้ API key ผ่านการติดตั้งเวอร์ชันที่กำหนดและข้อมูลตัวอย่างที่แม่นยำ พร้อมวิเคราะห์ข้อจำกัดของเส้นกราฟผลตอบแทนที่นักลงทุนควรทราบ
การทำ Backtest ที่ทำซ้ำได้จริง
การทำ Backtest ใน Python ที่ทำซ้ำได้จริง คือการที่บุคคลอื่นสามารถนำโค้ดไปรันบนเครื่องคอมพิวเตอร์ที่สะอาดและได้ผลลัพธ์ตัวเลขที่ตรงกับต้นฉบับทุกประการ โดยไม่ต้องใช้บัญชีผู้ใช้หรือ API key ใดๆ บทเรียนส่วนใหญ่สอบตกตั้งแต่บรรทัดแรก เนื่องจากมีการดึงข้อมูลสดซึ่งทำให้ผู้อ่านได้รับประวัติราคาที่แตกต่างจากที่ผู้เขียนได้รับเล็กน้อย บทความนี้จะใช้เครื่องมือแบบ open-source หนึ่งตัวคือ quantjourney-bt ที่เวอร์ชัน 0.12.4 โดยรันตัวอย่างที่มาพร้อมกับโปรแกรมโดยไม่ต้องใช้ข้อมูลยืนยันตัวตน จากนั้นจะใช้ข้อมูลตลาดจริงเพื่อแสดงให้เห็นว่าการรันโปรแกรมที่สะอาดเพียงครั้งเดียว ยังคงไม่สามารถบอกข้อมูลบางอย่างแก่คุณได้
อะไรคือสิ่งที่ทำให้การทำ Backtest สามารถทำซ้ำได้?
ความสามารถในการทำซ้ำ (Reproducibility) ในบริบทนี้มีความหมายที่เฉพาะเจาะจงและตรวจสอบได้ นั่นคือบุคคลที่สองสามารถรันคำสั่งเพียงคำสั่งเดียวบนเครื่องที่สะอาดและได้ตัวเลขกลับมาตรงกับของคุณทุกทศนิยม มีสองสิ่งที่คนทั่วไปมักทำพลาดจนทำให้กระบวนการนี้ล้มเหลว
สิ่งแรกคือโค้ด ไลบรารีที่อยู่ในเวอร์ชัน 0.x ไม่มีการรับประกันความเข้ากันได้ระหว่างการอัปเดตเวอร์ชันย่อย การเปลี่ยนชื่อ การเปลี่ยนค่าเริ่มต้น การสลับลำดับคอลัมน์ ทั้งหมดนี้อาจทำให้สคริปต์ของคุณยังคงทำงานได้ตามปกติ แต่กลับรายงานผลลัพธ์ที่ผิดเพี้ยนไปโดยที่คุณไม่รู้ตัว
สิ่งที่สองคือข้อมูล บทเรียนที่บรรทัดแรกเริ่มด้วยการดาวน์โหลดข้อมูลแบบสด (live download) ไม่เคยมีความสามารถในการทำซ้ำได้จริง ผู้ให้บริการข้อมูลมักมีการแก้ไขประวัติ ปรับปรุงข้อมูลตามการแตกหุ้น (splits) และเติมเต็มข้อมูลที่ขาดหายไป ทำให้สคริปต์เดิมให้ตัวเลขที่ต่างออกไปเมื่อผ่านไปหนึ่งเดือน เมื่อถึงเวลานั้น คุณจะไม่สามารถแยกแยะได้เลยว่าผลลัพธ์ที่เปลี่ยนไปเกิดจากการเปลี่ยนแปลงของโค้ดหรือการเปลี่ยนแปลงของข้อมูล
รูปแบบที่ควรนำมาปรับใช้จากโปรเจกต์นี้คือ การจับคู่เวอร์ชันของเอนจินที่คงที่เข้ากับชุดข้อมูลขนาดเล็กที่รวมอยู่ในแพ็กเกจนั้นเอง
การติดตั้งแบบระบุเวอร์ชัน: pip install quantjourney-bt==0.12.4
quantjourney-bt คือเครื่องมือทดสอบกลยุทธ์ย้อนหลัง (backtester) ของ QuantJourney ซึ่งเผยแพร่ภายใต้สัญญาอนุญาต Apache License 2.0 และต้องใช้ Python 3.11 หรือใหม่กว่า เวอร์ชัน 0.12.4 ได้รับการเผยแพร่เมื่อวันที่ 21 กรกฎาคม 2026 และเป็นเวอร์ชันที่คำสั่งทั้งหมดด้านล่างนี้อ้างอิงถึง ตามที่มีการบันทึกไว้ในเดือนสิงหาคม 2026
ให้ทำงานภายในสภาพแวดล้อมที่แยกส่วน (isolated environment) python3 -m venv .venv ใช้สำหรับสร้างสภาพแวดล้อม source .venv/bin/activate ใช้สำหรับเข้าสู่สภาพแวดล้อม และ python -m pip install -U pip ใช้สำหรับอัปเดตตัวติดตั้งภายในสภาพแวดล้อมนั้น จากนั้นให้ใช้เวอร์ชันที่ระบุไว้คือ pip install quantjourney-bt==0.12.4
เอกสารของโครงการระบุรูปแบบที่ไม่ได้ล็อกเวอร์ชันไว้คือ pip install quantjourney-bt ส่วน ==0.12.4 เป็นความรับผิดชอบของคุณ และในระดับ 0.x มันถือว่าคุ้มค่าที่จะใช้งาน ให้จดบันทึกเวอร์ชันที่ล็อกไว้ในที่ที่ผู้อื่นจะหาพบได้ง่าย pip freeze > requirements.txt จะบันทึกทุก dependency ที่ได้รับการแก้ไขแล้ว รวมถึงรายการที่คุณไม่ได้ระบุชื่อไว้ด้วย
มีส่วนเสริมทางเลือกสองรายการ pip install "quantjourney-bt[wf]" เพิ่ม Optuna สำหรับตัวอย่างการทำ walk-forward และการปรับค่าให้เหมาะสม (optimization) pip install "quantjourney-bt[data]" เพิ่ม yfinance สำหรับใช้เป็นข้อมูลสำรองสำหรับเกณฑ์มาตรฐาน (benchmarks)
Apache-2.0 เป็นสัญญาอนุญาตแบบเปิดกว้าง คุณสามารถใช้และแก้ไขโค้ดเพื่อการพาณิชย์ได้ โดยคุณต้องเก็บไฟล์สัญญาอนุญาตและประกาศแจ้งเตือนไว้พร้อมกับการแจกจ่ายต่อ และผู้ร่วมพัฒนาจะให้สิทธิในสิทธิบัตรไว้อย่างชัดเจน
วิธีรันตัวอย่าง SMA แบบรวมชุดโดยไม่ต้องใช้ API key
คลังข้อมูลนี้มาพร้อมกับสคริปต์ตัวเรียกใช้งานและกลยุทธ์ตัวอย่างที่รันได้จริงห้าสิบรายการ โดยแบ่งออกเป็นเส้นทางที่อิงตามน้ำหนักและเส้นทางที่อิงตามคำสั่งซื้อขาย ซึ่งรวมถึงเวิร์กโฟลว์แบบ walk-forward ห้ารายการ ./strategy.sh --list จะแสดงรายการทั้งหมด ./strategy.sh example_weights_01_sma_daily --check จะนำเข้ากลยุทธ์เพียงรายการเดียวโดยไม่มีการดึงข้อมูลใด ๆ ซึ่งเป็นวิธีที่เร็วที่สุดในการยืนยันว่าการติดตั้งสมบูรณ์
การรันเดโมทำได้ในบรรทัดเดียว: ./strategy.sh example_weights_01_sma_daily --sample-data --output /tmp/qj-sample
แฟล็ก --sample-data คือหัวใจสำคัญของเรื่องนี้ โดยโครงการได้อธิบายชุดข้อมูลที่อยู่เบื้องหลังไว้ดังนี้:
ชุดข้อมูลตัวอย่างมีขนาดเล็กและทำซ้ำได้โดยเจตนา ซึ่งมีประโยชน์สำหรับการตรวจสอบการติดตั้ง การสร้างรายงาน และการอ่านกระบวนการทำงานของ engine โดยไม่จำเป็นต้องสร้างบัญชี
ที่มา: quantjourney-bt README, เวอร์ชัน 0.12.4, อ่านเมื่อวันที่ 6 สิงหาคม 2026
การรันจะสร้างไดเรกทอรีแทนที่จะแสดงผลลัพธ์บนคอนโซล: summary.txt และ summary.json, metrics.csv, equity_curve.csv ที่อยู่ถัดจาก equity_curve.png, dashboard.html, โฟลเดอร์ plots/ และ run_metadata.json ที่บันทึกวิธีการตั้งค่าการรัน ไฟล์สุดท้ายนี้เป็นไฟล์ที่คนส่วนใหญ่มักข้ามไป แต่เป็นไฟล์ที่ทำให้ผลลัพธ์สามารถตรวจสอบย้อนหลังได้ในอีกหนึ่งปีให้หลัง
โปรดอ่านตัวชี้วัดที่ได้มาอย่างตรงไปตรงมา ชุดข้อมูลที่รวมมานั้นมีขนาดเล็กและเป็นเพียงตัวอย่าง ดังนั้น Sharpe ratio และ maximum drawdown ที่แสดงใน summary.txt จึงอธิบายได้เพียงไฟล์ตัวอย่างเท่านั้น สิ่งเหล่านี้ไม่ใช่หลักฐานยืนยันประสิทธิภาพของกลยุทธ์ และการมองว่าสิ่งเหล่านี้เป็นผลลัพธ์คือความผิดพลาดประการแรกที่คุณอาจทำได้
สิ่งที่การรันนี้พิสูจน์ได้และมีประโยชน์คือ การติดตั้งใช้งานได้จริง และกระบวนการทำงานทั้งหมดของ engine ตั้งแต่สัญญาณ ไปจนถึงน้ำหนักเป้าหมาย และการคำนวณมูลค่าพอร์ตการลงทุนใหม่ สามารถสร้างผลลัพธ์บนเครื่องของคุณได้โดยไม่ต้องใช้ข้อมูลรับรองใด ๆ ทั้งสิ้น สำหรับการทำ backtest บนข้อมูลประวัติจริงนั้นมีเส้นทางที่ต้องใช้ข้อมูลรับรองสำหรับบริการข้อมูลของโครงการโดยเฉพาะ ซึ่งเส้นทางดังกล่าวมีเอกสารกำกับไว้แล้ว และคำแนะนำนี้จะสิ้นสุดลงเพียงเท่านี้ ในส่วนที่ไม่จำเป็นต้องพึ่งพาใครทั้งสิ้น
สิ่งที่เส้นกราฟส่วนทุน (equity curve) จากข้อมูลชุดเดียวบอกคุณไม่ได้
การทดสอบด้วยข้อมูลชุดเดียวจะแสดงเส้นกราฟส่วนทุนเพียงเส้นเดียว นี่คือสิ่งที่เส้นกราฟดังกล่าวไม่สามารถบอกได้ โดยวัดจากข้อมูลตลาดจริงแทนที่จะเป็นไฟล์ตัวอย่าง
ตารางด้านล่างใช้แนวคิดเดียวกับกลยุทธ์ตัวอย่าง คือการใช้เส้นค่าเฉลี่ยเคลื่อนที่ 20 วันตัดกับเส้น 50 วัน โดยนำไปใช้กับ SPY และรายงานผลแยกรายปีปฏิทินตั้งแต่ปี 2017 จนถึงมิถุนายน 2026 สถานะในแต่ละวันถูกกำหนดโดยราคาปิดของวันก่อนหน้า ดังนั้นกฎนี้จึงไม่เคยเทรดโดยใช้ตัวเลขที่ยังไม่เกิดขึ้นจริง
SQL เบื้องหลังตัวเลขแต่ละตัว
WITH daily AS
(
SELECT
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= '2016-01-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY d
),
averaged AS
(
SELECT
d,
px,
avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
d,
px,
if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
toYear(d) AS year,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
round((exp(sum(log(px / px_prior))) - 1) * 100, 1) AS hold_pct,
countIf(long_today != long_prior) AS crossover_count
FROM positioned
WHERE px_prior > 0
AND toYear(d) >= 2017
GROUP BY year
ORDER BY yearกฎข้อเดียวที่ไม่เปลี่ยนแปลง ถูกวัดผล 9 ครั้งแยกกัน โปรดอ่านคอลัมน์เปอร์เซ็นต์ทั้งสองคอลัมน์ก่อนอ่านส่วนอื่น ใน 2017 กฎนี้ทำผลงานสิ้นปีได้ 16% เทียบกับ 19.4% สำหรับการถือครอง SPY ในช่วงเวลาเดียวกัน ใน 2025 คอลัมน์ทั้งสองนั้นแสดงค่า 10.4% และ 16.4% โค้ดที่ใช้เหมือนกันทุกประการในทั้งสองแถว มีเพียงช่วงเวลาที่เลื่อนออกไปเท่านั้น
คอลัมน์การตัดกัน (crossover) แสดงให้เห็นว่าหลักฐานเบื้องหลังนั้นเบาบางเพียงใด การเปลี่ยนสถานะ 4 ครั้งตลอดระยะเวลา 2025 หมายความว่าเส้นกราฟส่วนทุนตลอดทั้งปีตั้งอยู่บนการตัดสินใจเพียงไม่กี่ครั้ง ซึ่งเป็นกลุ่มตัวอย่างที่เล็กเกินกว่าจะสรุปว่าเป็นผลลัพธ์ที่เชื่อถือได้
กฎเดียวกันนี้ให้ผลลัพธ์ในลักษณะเดียวกันกับหุ้นตัวอื่นหรือไม่
การเปลี่ยนช่วงเวลาเป็นวิธีหนึ่งในการตรวจสอบเส้นกราฟเพียงเส้นเดียว ส่วนการเปลี่ยนกลุ่มสินทรัพย์คืออีกวิธีหนึ่ง ตารางด้านล่างนี้กำหนดค่าพารามิเตอร์ให้คงที่และใช้กฎเดียวกันกับหุ้นที่มีสภาพคล่องสูงห้าตัวในช่วงห้าปีปฏิทินตั้งแต่ปี 2021 ถึง 2025
SQL เบื้องหลังตัวเลขแต่ละตัว
WITH daily AS
(
SELECT
ticker,
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('SPY', 'QQQ', 'AAPL', 'MSFT', 'KO')
AND window_start >= '2020-07-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY ticker, d
),
averaged AS
(
SELECT
ticker,
d,
px,
avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (PARTITION BY ticker ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
ticker,
d,
px,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
ticker AS symbol,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
round((exp(sum(log(px / px_prior))) - 1) * 100, 1) AS hold_pct,
round(avg(long_prior) * 100, 0) AS days_long_pct
FROM positioned
WHERE px_prior > 0
AND d >= toDate('2021-01-01')
GROUP BY symbol
ORDER BY rule_pct DESCQQQ อยู่ที่ด้านบนของตารางที่ 40.2% และแถวล่างสุดคือ KO อยู่ที่ 3.8% คอลัมน์ days_long_pct รายงานสัดส่วนเวลาที่แต่ละเวอร์ชันถือครองสถานะจริงในช่วงเวลาดังกล่าว โดยอยู่ที่ 67% สำหรับแถวบนสุด พารามิเตอร์ชุดเดียว กลุ่มสินทรัพย์ห้ากลุ่ม และส่วนต่างที่กว้างพอจะพิสูจน์ได้ว่าการเลือกผู้ชนะหลังจากเหตุการณ์เกิดขึ้นแล้วนั้น ไม่ได้บ่งบอกอะไรถึงผลลัพธ์ของการเทรดที่คุณยังไม่ได้เริ่มทำ
เนื้อหาทั้งหมดนี้ไม่ใช่คำแนะนำในการเทรดด้วยกลยุทธ์ crossover โดย crossover เป็นเพียงเครื่องมือวัดผลสำหรับการทำ backtest และสิ่งที่เรากำลังวัดอยู่ก็คือตัว backtest นั้นเอง
จุดที่ Look-ahead bias แฝงตัวเข้ามาในการทดสอบย้อนหลังแบบถ่วงน้ำหนัก
กลไกการคำนวณน้ำหนักจะเปลี่ยนสัญญาณให้เป็นน้ำหนักเป้าหมาย จำลองการจับคู่คำสั่งซื้อขายตามน้ำหนักเหล่านั้น แล้วจึงสร้างมูลค่าพอร์ตการลงทุนขึ้นใหม่จากสถานะที่เกิดขึ้น ความผิดพลาดมักซ่อนอยู่ในจุดเชื่อมต่อระหว่างสัญญาณกับน้ำหนัก หากน้ำหนักของวันนี้มาจากราคาปิดของวันนี้และได้รับผลตอบแทนของวันนี้ด้วย การทดสอบย้อนหลังนั้นถือว่าได้ซื้อขายโดยใช้ข้อมูลที่ยังไม่เกิดขึ้นจริงในขณะที่ควรจะส่งคำสั่งซื้อขายไปแล้ว สิ่งนี้เรียกว่า look-ahead bias ซึ่งระบบจะไม่แจ้งเตือนข้อผิดพลาดใด ๆ แต่จะทำให้ผลลัพธ์ทุกอย่างดูดีเกินจริง
ตารางด้านล่างแสดงผลการทดสอบกฎเดียวกันทั้งสองรูปแบบโดยใช้ข้อมูลย้อนหลังของ SPY
SQL เบื้องหลังตัวเลขแต่ละตัว
WITH daily AS
(
SELECT
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= '2016-01-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY d
),
averaged AS
(
SELECT
d,
px,
avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
d,
px,
if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
toYear(d) AS year,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS next_bar_pct,
round((exp(sum(log(if(long_today = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS same_bar_pct,
round(abs(exp(sum(log(if(long_today = 1, px / px_prior, 1.0))))
- exp(sum(log(if(long_prior = 1, px / px_prior, 1.0))))) * 100, 1) AS gap_pp
FROM positioned
WHERE px_prior > 0
AND toYear(d) >= 2017
GROUP BY year
ORDER BY yearใน 2017 เวอร์ชันที่ใช้ข้อมูลรอบก่อนหน้าแสดงผล 16% ในขณะที่เวอร์ชันที่ใช้ข้อมูลรอบเดียวกันแสดงผล 17.1% ซึ่งห่างกัน 1.1 เปอร์เซ็นต์ ใน 2025 ระยะห่างระหว่างทั้งสองค่าอยู่ที่ 1.6 เปอร์เซ็นต์ มีเพียงคอลัมน์เดียวเท่านั้นที่สามารถสร้างขึ้นได้โดยเครื่องมือที่ไม่ทราบราคาปิดของรอบนั้นมาก่อน และความแตกต่างระหว่างทั้งสองค่าเป็นเพียงการปรับแต่งทางบัญชีที่ปราศจากแนวคิด ทักษะ หรือการซื้อขายจริงรองรับ
กลไกนี้ได้ระบุจุดยืนของตนเองในประเด็นเรื่องช่วงเวลา ซึ่งมีค่ามากกว่าคำสัญญา:
สำหรับการจับคู่คำสั่งซื้อขายที่ราคาเปิด (Open) การคำนวณ Slippage ที่คำนึงถึงช่วงราคาจะพิจารณาเฉพาะแท่งราคาที่สมบูรณ์ก่อนหน้าเท่านั้น และความสามารถในการรองรับปริมาณการซื้อขาย (Volume capacity) จะถูกคาดการณ์จากข้อมูลย้อนหลัง โดยกลไกนี้จะไม่ใช้ข้อมูลราคาสูงสุด ราคาต่ำสุด ราคาปิด หรือปริมาณการซื้อขายรวมของวันนั้น ๆ
ที่มา: quantjourney-bt README, เวอร์ชัน 0.12.4, อ่านเมื่อวันที่ 6 สิงหาคม 2026
สมมติฐานที่มีการบันทึกไว้สามารถตรวจสอบเทียบกับซอร์สโค้ดที่คุณติดตั้งไว้แล้วได้ ส่วนสมมติฐานที่ไม่มีการบันทึกไว้ก็เป็นเพียงการคาดเดาเท่านั้น
เหตุผลที่ต้องมีส่วนเสริม walk-forward
ตัวอย่างการทำ walk-forward ตั้งแต่ WF01 ถึง WF05 มาพร้อมกับส่วนเสริม [wf] และการพึ่งพาไลบรารี Optuna กระบวนการ walk-forward จะทำการปรับจูนพารามิเตอร์บนข้อมูลย้อนหลังช่วงหนึ่ง แล้ววัดผลบนช่วงถัดไป จากนั้นจึงเลื่อนหน้าต่างข้อมูลไปข้างหน้าและทำซ้ำอีกครั้ง รูปแบบการเลื่อนหน้าต่างแบบ rolling และ expanding มีความแตกต่างกันตรงที่หน้าต่างการปรับจูนจะตัดข้อมูลเก่าที่สุดออกเมื่อขยับไปข้างหน้าหรือไม่ นอกจากนี้ยังมีตัวอย่างที่เพิ่มการทำ purge และ embargo ในแต่ละรอยต่อ เพื่อตัดข้อมูลที่อยู่ใกล้จุดรอยต่อออกไป ป้องกันไม่ให้ข้อมูลจากช่วงที่ใช้ปรับจูนรั่วไหลเข้าไปในข้อมูลช่วงที่ใช้ทดสอบ
กระบวนการเหล่านี้ไม่ได้เปลี่ยนแนวคิดที่อ่อนแอให้กลายเป็นแนวคิดที่ใช้งานได้จริง แต่มันเป็นการเปลี่ยนตัวเลขเพียงค่าเดียวให้กลายเป็นกลุ่มการกระจายตัวของตัวเลขที่คุณสามารถนำมาวิเคราะห์โต้แย้งได้ ซึ่งนั่นคือการยกระดับทั้งหมด ขั้นตอนถัดจากนี้ยังไม่ใช่การใช้เงินจริง: การทดสอบด้วยพอร์ตจำลองก่อนใช้เงินจริง จะวัดผลในสิ่งที่การทำ backtest ไม่สามารถมองเห็นได้ในเชิงโครงสร้าง เริ่มตั้งแต่การตรวจสอบว่าคำสั่งซื้อขายของคุณได้รับการจับคู่ในราคาที่ใกล้เคียงกับที่โปรแกรมจำลองสมมติไว้หรือไม่ และ ระบบตัดวงจรสำหรับบอทเทรด จะครอบคลุมถึงการทำงานของโค้ดในวันที่ระบบเกิดความผิดปกติ สำหรับสถิติที่อยู่เบื้องหลังทั้งหมดนี้ บันทึกของเราใน หนังสือ quant trading แบบโอเพนซอร์ส จะเจาะลึกในรายละเอียดที่มากขึ้นอีกระดับหนึ่ง
คำถามที่พบบ่อย
คุณสามารถทำ Backtest กลยุทธ์โดยไม่มี API key ได้หรือไม่
ได้ quantjourney-bt มาพร้อมกับชุดข้อมูลตัวอย่างที่รวมอยู่ภายใต้ flag --sample-data และกลยุทธ์ตัวอย่างสามารถรันโดยใช้ข้อมูลดังกล่าวได้โดยไม่ต้องมีบัญชีหรือข้อมูลรับรองใดๆ ชุดข้อมูลนี้มีขนาดเล็กและมีไว้เพื่อเป็นตัวอย่างเท่านั้น ดังนั้นให้ถือว่าการรันดังกล่าวเป็นการตรวจสอบการติดตั้งและขั้นตอนการทำงาน (pipeline) มากกว่าจะเป็นการพิสูจน์ประสิทธิภาพของกลยุทธ์
ทำไมต้องระบุเวอร์ชัน (pin) ของแพ็กเกจ Python สำหรับทำ Backtest
แพ็กเกจที่อยู่ในเวอร์ชัน 0.x ไม่มีการรับประกันความเข้ากันได้ระหว่างรุ่นย่อย (minor releases) และการเปลี่ยนแปลงค่าเริ่มต้นหรือการเปลี่ยนชื่อตัวชี้วัดจะไม่แจ้งเตือนให้ทราบ การระบุเวอร์ชันด้วย pip install quantjourney-bt==0.12.4 และการบันทึกสภาพแวดล้อมไว้ในไฟล์ requirements หมายความว่าผลลัพธ์ที่คุณสร้างในวันนี้จะสามารถนำมาสร้างใหม่ได้ในปีหน้าด้วย Engine ตัวเดิมที่สร้างมันขึ้นมา
quantjourney-bt เผยแพร่ภายใต้ใบอนุญาตใด
Apache License 2.0 ซึ่งอนุญาตให้ใช้ในเชิงพาณิชย์และดัดแปลงได้ โดยมีข้อกำหนดให้เก็บไฟล์ใบอนุญาตและประกาศแจ้งไว้พร้อมกับการแจกจ่ายต่อ และรวมถึงการให้สิทธิบัตรโดยชัดแจ้งจากผู้ร่วมพัฒนา เวอร์ชัน 0.12.4 ถูกเผยแพร่เมื่อวันที่ 21 กรกฎาคม 2026 และต้องการ Python 3.11 หรือใหม่กว่า
ผลลัพธ์ Backtest ที่แข็งแกร่งหมายความว่ากลยุทธ์นั้นใช้ได้ผลจริงหรือไม่
ไม่ ผลลัพธ์จาก Backtest เป็นเพียงการวัดค่าหนึ่งครั้ง ในช่วงเวลาหนึ่ง บนกลุ่มสินทรัพย์หนึ่งเท่านั้น แผงข้อมูลด้านบนแสดงให้เห็นว่ากฎเกณฑ์เดียวที่ไม่เปลี่ยนแปลงสามารถสร้างตัวเลขรายปีที่แตกต่างกันอย่างมากใน ticker เดียวกัน และให้ตัวเลขที่แตกต่างกันอย่างมากในห้าชื่อ ซึ่งเป็นช่องว่างที่การทำ walk-forward validation และ out-of-sample testing ถูกออกแบบมาเพื่อเปิดเผย
วิธีการคำนวณแผงข้อมูลด้านบน
ราคาปิดรายวันคือราคาซื้อขาย (print) นาทีสุดท้ายของช่วงเวลาปกติในแต่ละวัน โดยอ้างอิงตามเวลาของนิวยอร์ก ระหว่างเวลา 9:30 น. ถึง 16:00 น. ซึ่งช่วยให้การคำนวณในวันที่มีการปิดตลาดเร็วถูกต้องโดยไม่ต้องกำหนดความยาวของช่วงเวลาซื้อขายแบบตายตัว ค่าเฉลี่ยเร็วครอบคลุม 20 ช่วงการซื้อขายและค่าเฉลี่ยช้าครอบคลุม 50 ช่วงการซื้อขาย ทั้งสองเป็นแบบ Simple Moving Average โดย 49 ช่วงการซื้อขายแรกของทุกชุดข้อมูลถือเป็นช่วง warm-up ซึ่งจะไม่มีการถือสถานะ ตัวเลขรายปีคำนวณแบบทบต้นจากการเคลื่อนไหวของราคาปิดในแต่ละช่วงการซื้อขายที่กฎระบุให้ถือสถานะ Long ส่วนคอลัมน์ hold จะคำนวณแบบทบต้นทุกช่วงการซื้อขายในปีเดียวกันเพื่อใช้เปรียบเทียบ ชื่อหุ้นทั้งห้าในกลุ่มตัวอย่างถูกเลือกมาเนื่องจากมีประวัติข้อมูลต่อเนื่องและไม่มีการแตกหุ้น (split) ภายในช่วงเวลาดังกล่าว ดังนั้นชุดข้อมูลราคาปิดจึงไม่จำเป็นต้องมีการปรับค่า ช่วงเวลาถูกกำหนดไว้ตายตัวในอดีต ดังนั้นแผงข้อมูลเหล่านี้จึงแสดงตัวเลขเดิมทุกครั้งที่มีการประมวลผลใหม่
แผงข้อมูลทุกชุดในที่นี้มาพร้อมกับคำสั่ง SQL ที่ใช้จริง ซึ่งทำให้ตัวเลขบนหน้านี้สามารถรันซ้ำได้เช่นเดียวกับการติดตั้งที่ระบุเวอร์ชันไว้ หากต้องการวัดผลกฎเกณฑ์ในช่วงเวลาของคุณเองก่อนที่จะเขียนโค้ด Backtest สามารถสอบถามข้อมูลด้วยภาษาอังกฤษทั่วไปได้ที่ Strasmore terminal