2023年8月5日 星期六

Quant Day 1 - Backtesting.py

最近轉換到 Python 環境下開發選股系統

確實比以前用 Excel VBA 硬刻要輕鬆許多

今天整理一下使用回測套件的一些心得

在Python環境上,常見的交易回測套件有下列三個

[1] backtesting.py [官網]
 - 相當簡單的說明文檔
 - 簡潔、上手容易
 - 網路上沒有太多的討論,主要看GitHub裡的討論 [Discussions]

[2] backtrader [官網]
 - 使用人口眾多
 - 據說在回測功能上可以有許多的參數調整
 - 當然入門的門檻就高些

[3] FinMind
 - 台灣的開發者,值得推薦一下
 - 主要是在尋找台股股價資料源時看到,對於量化投資的入門者而言,是相當值得期待的平台
 - 但是因為筆者已經具備有自己多年下來的投資架構,故只是採用其資料源


筆者直接就使用了 Backtesting.py

原因也很簡單

因為一用就上手,也沒有什麼特別覺得欠缺的

所以也就持續用下來了

以下就彙整一下使用上的心得

[1] 在 Colab 安裝
# Colab沒有預設,第一次用得自己安裝
!pip install backtesting > log.txt
#引入回測和交易策略功能
from backtesting import Backtest, Strategy
#從lib子模組引入判斷均線交會功能
from backtesting.lib import crossover
#從test子模組引入繪製均線功能
from backtesting.test import SMA 

[2] Strategy的撰寫
以筆者現在在用的策略撰寫說明,紅字標示的部分,就是它的關鍵語法
這是一個只做多的策略,寫得很簡單
各位可以網路上在多看看範例

class MA_Slope_v20230727(Strategy): #交易策略命名
  def init(self):
    super().init() #繼承Strategy的初始化函式
  def next(self):
    if (self.data.sma_buy > self.data.sma_buy_lag
      and self.data.Close > self.data.sma_100
      and self.data.sma_buy > self.data.sma_100
      and self.data.sma_sell > self.data.sma_100
      and self.data.sma_sell > self.data.sell_down) and (not self.position):
      self.buy() #  Buy
    elif self.position.is_long and (self.position.pl_pct<-0.10
                                    or (self.position.pl_pct>0 and self.data.sma_sell < self.data.sma_100)
                                    or (self.position.pl_pct>0 and self.data.sma_sell < self.data.sell_down)):
      self.position.close() # Buy Exit


self.position.is_long 是指目前部位是否為做多
self.position.pl_pct 是指目前部位的損益率
這些都可以在 [官網這裡] 去了解,筆者也只是找了需要的功能了解一下該如何用

[3] 執行回測
第一個要注意的是,backtesting.py 有資料欄位名稱的要求,以下是個例子

#將欄位名稱更換為basktesting套件的設定, 並指定給df_back
df_back = my_stock.rename(columns={"date":"Date","開盤價": "Open", "最高價": "High", "最低價": "Low", "收盤價": "Close", "成交量_股": "Volume"})


第二個是回溯測試的參數設定

# 設定
bt = Backtest(df_back, MA_Slope_v0727, cash=100000,  commission=0, exclusive_orders=False, 
trade_on_close=False)

# 'cash' is the initial cash to start with.

# 'commission' is the commission ratio. E.g. if your broker's commission is 1% of trade value, set commission to 0.01. Note, if you wish to account for bid-ask spread, you can approximate doing so by increasing the commission, e.g. set it to 0.0002 for commission-less forex trading where the average spread is roughly 0.2‰ of asking price.

# 'margin' is the required margin (ratio) of a leveraged account. No difference is made between initial and maintenance margins. To run the backtest using e.g. 50:1 leverge that your broker allows, set margin to 0.02 (1 / leverage).

# If 'trade_on_close' is True, market orders will be filled with respect to the current bar's closing price instead of the next bar's open.
#          => True=賣出訊號於第T日出現,則以T日收盤價賣出 / False=賣出訊號於第T日出現,則以T+1日開盤價賣出 => 對盤後才計算買賣訊號者,False比較合理

# If 'hedging is True', allow trades in both directions simultaneously. If False, the opposite-facing orders first close existing trades in a FIFO manner.

# If 'exclusive_orders' is True, each new order auto-closes the previous trade/position, making at most a single trade (long or short) in effect at each time.

[4] 取得回溯測試的結果

# 執行
output = bt.run() 

回溯結果就存在 output 裡,以下是取用的範例

print(output)
print(output['_trades'])
print(output['_equity_curve'])

my_data=[all_stock.iloc[my_j-1,0],my_ticker,all_stock.iloc[my_j-1,2],output['Buy & Hold Return [%]'],output['Return [%]'],
              my_god_index,output['# Trades'],output['Win Rate [%]'],output['Profit Factor'],
              output['Return (Ann.) [%]'],output['Volatility (Ann.) [%]'],output['Sharpe Ratio'],output['Start'].strftime("%Y-%m-%d"),output['End'].strftime("%Y-%m-%d"),
              my_set_ma_buy,my_set_ma_sell,my_set_loc_BetweenMaxMin,my_set_loc_BelowMax,my_set_shift,my_set_buy_up,my_set_sell_down,output['_strategy']]

 
my_last_trade=[all_stock.iloc[my_j-1,0],my_ticker,all_stock.iloc[my_j-1,2],my_market_value,output['Start'].strftime("%Y-%m-%d"),output['# Trades'],
                     output['Win Rate [%]'],output['Profit Factor'],my_trade_active]+output['_trades'].iloc[-1].values.tolist() # list 的相加


以上,供各位參考。