วันนี้เราจะมาทดลองใช้ Library Backtestในการเขียนแบบจำลองการซื้อขายหุ้นอย่างง่ายกันนะครับ ที่จริงแล้วมันก็เหมือนที่เราเคยเขียนกันเองแบบไม่ใช้ Library ในคอร์ส Python for Finance แต่เมื่อเราเข้าพื้นฐานแล้วมันก็จะสะดวกกว่าที่เราจะทดลองไอเดียของเราแบบเร็วๆ โดยไม่ต้องเขียนทุกอย่างขึ้นมาเองให้เสียเวลา!!!(แต่ถ้าเรามีเวลา ผมคิดว่าเราควรเขียนขึ้นมาเองเพื่อจะความเข้าใจได้คุม environment ได้แบบสมบูรณ์ครับ)
จริงๆแล้ว Library ที่ใช้ในการ Backtest มีหลายตัว ตัวที่โด่งดังอย่าง Backtrader, PyAlgotrade, bt, Zipline(อดีตเคยดังแต่ตอนนี้ไม่มีคนดูแลแล้วข้ามได้ก็ข้ามครับ) แต่เราจะขอยกตัวที่เราคิดว่ามีความเรียบง่ายที่สุดมาให้ทดลองใช้กันครับ ซึ่งก็คือ Backtesting.py นี่เอง
Backtesting.py นั้นมีความเรียบง่ายมากเหมือนกับเป็นการตัดเอาฟีเจอร์ที่สำคัญของ Backtest ตัวอื่น เช่น Backtrader ออกมาและทำให้มันมีขนาดเล็กที่สุดมีเฉพาะฟีเจอร์ที่สำคัญจริงๆเท่านั้น แต่ก็ยังทำงานได้ครบถ้วนพอสมควรครับ
ติดตั้ง
ก่อนอื่นถ้าเรายังไม่มี Backtesting.py ในเครื่อง เราก็ลง Library การเปิด ใน Anaconda prompt และพิมพ์คำสั่ง
pip3 install backtesting
หรือเปิด Notebook ของเราละใช้คำสั่ง
!pip3 install backtesting
เราสามารถเข้าไปเยี่ยมชมเว็บไซต์ได้ที่ https://kernc.github.io/backtesting.py/
เริ่มโค้ด
import pandas as pd
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
import yfinance as yf
Import Library ที่ต้องใช้ จากนั้นเริ่มดึงข้อมูลหุ้นที่จะ Backtest ด้วย
df = yf.download("DIS",start="2010-01-01", end="2022-12-01")
ดึงข้อมูลหุ้นที่ต้องการ ในที่นี้เราใช้ตัวอย่างหุ้น Disney เริ่มตั้งแต่ปี 2010 จนถึงปี 2022
def SMA_creation(asset, n):
return pd.Series(asset).rolling(n).mean()
จากนั้นเราสร้างฟังก์ชั่นสำหรับคำนวณ Simple Moving Average และก็จะเริ่มเข้าเนื้อหาของการใช้งาน Backtest.py
Class Backtesting
class Backtesting(Strategy):
fast = 10
slow = 100
def init(self):
self.sma_fast = self.I(SMA_creation, self.data.Close, self.fast)
self.sma_slow = self.I(SMA_creation, self.data.Close, self.slow)
def next(self):
if crossover(self.sma_fast, self.sma_slow):
self.position.close()
self.buy()
elif crossover(self.sma_slow, self.sma_fast):
self.position.close()
self.sell()
เราสร้าง Clasee Backtesting ขึ้นมา
กำหนด parameter สองตัวเป็นค่าตั้งต้นของ SMA fast และ SMA slow เรากำหนดไว้แบบนี้เพื่อที่จะมา Optimize ภายหลัง
กำหนดฟังก์ชัน init ซึ่งจะเป็นฟังก์ชันที่เหมือนเป็นค่าเริ่มต้นของ Class เมื่อคุณเรียกคลาส init ก็จะถูกเรียกทำงานก่อนฟังก์ชันอื่น ฉะนั้นเราจะกำหนดค่าเริ่มต้นที่ฟังก์ชั่น init นี้ โดยการสร้าง Moving average ทั้ง 2 เส้นขึ้นมาด้วยการเรียกฟังก์ชั่น SMA_creation เข้ามา
จากนั้นฟังก์ชัน next ตรงนี้เราจะสามารภกำหนดเงื่อนไขในการซือขาย
ใช้เงื่อนไขตรวจฟังก์ชั่น Crossover ถ้า SMA fast ตัด SMA slow ก็เป็น สัญญาณซื้อ
- self.position.close() เราจะกำหนดให้ปิด Position เดิม(ถ้ามี)
- self.buy() และเปิด Position ใหม่เป็น buy หรือว่า ซื้อ
และถ้า SMA slow ตัด SMA fast ลง ก็เป็น ก็เป็นสัญญาณขาย
- self.position.close() เราจะกำหนดให้ปิด Position เดิม(ถ้ามี)
- self.sell() และเปิด Position ใหม่เป็น sell หรือ short(ขาย)
เริ่มใช้งาน Class Backtesting
bt = Backtest(df, Backtesting, cash=10000, commission=0.0025)
เริ่มใช้ฟังก์ชั่น Backtest โดยกำหนด Data เข้าไปเป็นหุ้น Disney ใส่ Class Backtestingที่เรากำหนดเงื่อนไขการซื้อขายไว้ด้านบน แล้วก็กำหนดเงินเริ่มต้น กำหนดค่าคอมมิชชั่น etc. จากนั้นสั่ง run
stats = bt.run()
stats

ง่ายๆแค่นี้เองครับเราก็ได้ผลงาน Metric มาหลายตัว โดย Minumum Code มากๆ
ในที่นี้เงินตั้งต้น 10000 เหรียญ ในปี 2010 จะมาเป็น 13618 เหรียญ Equity Final [$] ในปี 2022 คิดเป็นกำไร Return [%] 36% ใน 12ปี ซึ่งไม่ได้ดีเท่าไหร่ โดยเฉลี่ย Return (Ann.) [%] เป็นปีละ 2.4% ดีกว่าเงินฝากธนาคารนิดเดียว แถม ถือเฉยๆ Buy & Hold Return [%] ที่ได้กำไรไป 197% ก็ยังสู้ไม่ได้
พวก Metric ก็มีให้ดูมากมาย
- Volatility (Ann.) [%]: 23.48%
- Sharpe Ratio: 0.10,
- Sortino Ratio: 0.15
- Calmar Ratio: 0.05
- Max. Drawdown [%]: -40%
- Avg. Drawdown Duration: 70 วัน
- #Trades จำนวนการเทรด 49 ครั้ง
เราสามารถพล๊อตรูปดูว่าเกิดอะไรขึ้นในช่วง Backtest นี้บ้าง
bt.plot()

Visualize ดู จะได้กราฟที่เป็น Interactive อันนี้จะเป็นของ Bokeh(คู่แข่ง Plotly) มีรายละเอียดว่าเราซื้อขายเมื่อไหร่ มี PNL แต่ละไม้เป็นยังไง Equity Curve หน้าตาเป็นยังไง มี Drawdown เท่าไหร่ ด้วยความที่เป็น Interactive เราสามารมซูมดูรายละเอียดได้

Optimization
อีกความสะดวกหนึ่งของ Family ตัวนี้คือเขามีฟังก์ชันในการ Optimization ให้แบบอัตโนมัติ แต่เราต้องกำหนดว่าเราต้องการจะ Optimization อะไรให้ได้มากสุด กำไรมากสุด? ความเสี่ยงน้อยสุด? และกำหนดพารามิเตอร์ในการ Optimization จากเท่าไหร่ไปจนถึงเท่าไหร่ ในที่นี้ก็คือ SMA fast และ Slow
จุดดีคือเราบอกเขาได้ว่าเราต้องการจะ Maximize อะไรอีกด้วยตาม Metric ด้านบน
stats = bt.optimize(fast=range(10, 50, 5),
slow=range(20, 200, 10),
maximize='Return [%]',
constraint=lambda param: param.fast < param.slow)
stats
ในที่นี้เราจะ Maximize Return [%] ซึ่งก็คือผลกำไรเป็นเปอร์เซ็นต์ เราสามารถเปลี่ยนตรงนี้ได้หลายตัว
ที่ constraint=lambda param: param.fast < param.slow คือการกำหนดเงื่อนไขของพารามิเตอร์ กำหนดว่าเราไม่ต้องการให้มีครั้งไหนที่เส้น SMA fast ช้ากว่า SMA slow ค่าที่ได้

นี่เป็นผลกำไรที่มากที่สุดเท่าที่เราจะทำได้ บนเงื่อนไขที่ SMA fast, slow อยู่ใน Range ที่กำหนด
ก็เพิ่มมาเป็น 466% เอาชนะการ Buy and Hold ที่ 197% ไปได้อย่างสวยงาม แต่ถ้าถามว่าอันนี้ดีสุดแล้วหรือไม่ ยังตอบไม่ได้ เราต้องดู Metrics ด้วย ถ้าลองดู Metrics อื่นๆก็พัฒนาขึ้นอย่างเห็นได้ชัด เราลองใช้คำสั่งเพื่อดู Parameter ที่เราต้องการ Optimize
stats._strategy

ค่าที่ได้ก็คือ SMA fast และ slow ที่ทำกำไรให้เรามากสุดในการ Backtest นี้นั่นเอง
แต่นี่ดีสุดไหมเรายังไม่แน่ใจ แต่ดีที่เราสามารถ บอกมันได้เลยว่าเราอยากจะ Maximize ตัวไหนเช่น สมมุติเรากลัวความเสี่ยงมาก Avg. Drawdown เราก็เปลี่ยนเงื่อนไขได้เลย
stats = bt.optimize(fast=range(10, 50, 5),
slow=range(20, 200, 10),
maximize='Avg. Drawdown [%]',
constraint=lambda param: param.fast < param.slow)
stats

กลายเป็นว่าเทรดไปเทรดมากำไรก็พอๆกับ Buy ad Hold แต่ด้วยเงื่อนไขนี้ Avg. Drawdown [%] ก็ลดลงอย่างมีนัยยะสำคัญ แต่อย่างไรก็ตามผลกำไรสูงสุด หรือ ความเสี่ยงต่ำสุดไม่สามารถเอามาบอกได้ว่าอะไรดีกว่ากัน จะดีกว่าถ้าเรา Optimize Metric ทีเกิดมาเพื่อวัดเสี่ยงต่อผลกำไร เช่น Sharpe, Calman หรือ Sortino Ratio
stats = bt.optimize(fast=range(10, 50, 5),
slow=range(20, 200, 10),
maximize='Sortino Ratio',
constraint=lambda param: param.fast < param.slow)
stats

กลายเป็นว่าในกรณีนี้ กำไรสูงสุด ก็เป็นตัวเดียวกับที่ มีความเสี่ยงต่อกำไรต่ำดีสดด้วยเช่นกัน(แต่ไม่ใช่ทุกเคส)
stats._strategy

ดูพารามิเตอร์เพื่อความชัวร์ว่าเป็นตัวเดียวกับตอนแรกไหม
bt.plot()

จะเห็นว่าจำนวนครั้งของการเทรดลดลงอย่างเห็นได้ชัด อย่างไรก็ตามเราสามารถดู Statistic ของการเทรดแต่ละครั้งได้ด้วย
stats._trades.head(10)

รายละเอียดมีทั้งเข้าแบบไหน Long หรือ Short เข้าที่ Bar ที่เท่าไหร่ ราคาเท่าไหร่ ปิด Position ที่ราคาเท่าไหร่ กำไรหรือขาดทุน พร้อมทั้งระยะเวลาว่าไม้นั้นๆเราถือกี่วัน เพื่อนำไปวิเคราะห์ต่อเป็นต้น
สรุปแล้ว Backtesting.py เป็น Library ที่ดีใช้งานง่าย Function ไม่เยอะ แต่ก็ครบพอดีสำหรับมือใหม่ที่จะนำไปทดลองใช้กันครับ ส่วนของ การ Optimize นั้น ถ้ามองจากมุมมอง Quant แล้วใช้ไม่ดีเราก็แค่ Overfiting Cruve มันฉะนั้นต้องระวังกันหน่อย (ไม่ได้แปลว่าการใช้ Walk Forward เพิ่มมาแล้วมันจะไม่ Overfiting นะครับ แต่มันคงไม่เกี่ยวกับประเด็นแนะนำ Library ของเราก็ขอยกไว้ประเด็นอื่นวันหลัง)