【Python】Backtesting.pyで株取引をバックテストして戦略を最適化

今回は,Pythonによる株取引シミュレーションのバックテストについて解説します
バックテストとは
株取引やFXには,移動平均線やローソク足,MACD,ボリンジャーバンドなどのテクニカル指標を用いて売買する手法があります.
詳しくは,MACDの説明記事やボリンジャーバンドの説明記事をご覧ください.
ときには,オリジナルな売買手法を考えるときもあるでしょう.
売買の手法を考えたら,それがどれくらい良い戦略なのか(どれくらい儲けが出るのか)を試すために過去のデータを使ってシミュレーションを行いたいです.なぜなら,考えた戦略でいきなり取引するのは少し怖いですから.
この「過去のデータで手法の精度を確認する」ことをバックテストといいます.
Pythonによるバックテスト
Pythonでバックテストを行う方法はいくつかあると思いますが,ここでは『Backtesting.py』というライブラリを用いてバックテストを行います.
Backtesting.pyでは,戦略を最適化する機能(例えば,リターンが最大になるように移動平均線の日数を変化させる)もあるので,そちらも試してみます.
Backtesting.pyのインストール
pip install backtesting
でできます.
株価データの取得
まずは,バックテストを行うための株価を取得しましょう.
今回は,株価の取得にはpandas-datareaderを用います.
ただし,一度株価を取得したらCSVファイルに保存し,初回以降はそれを読み込むことをおすすめします(方法はこちら).
今回は,2018/1/1~現在までのKO(コカ・コーラ)の株価を取得したいと思います.
import pandas_datareader.data as web
import datetime
start = datetime.date(2018,1,1)
end = datetime.date.today()
data = web.DataReader('KO', 'yahoo', start, end)
ここに関しては,Pythonのpandas datareaderをインストールして株価データを取得する方法【日本株・米国株】を参考にして好きな銘柄の株価を取得して下さい.
pandas-datareaderで取得したデータは以下のように,High(高値),Low(低値),Open(始値),Close(終値),Volume(出来高),Adj Close(調整後終値)を持つDataFarameとなっています.
High Low Open Close Volume Adj Close
Date
2018-01-02 45.939999 45.509998 45.910000 45.540001 10872200.0 40.913391
2018-01-03 45.689999 45.340000 45.490002 45.439999 12635600.0 40.823551
2018-01-04 46.220001 45.450001 45.560001 46.080002 12709400.0 41.398529
2018-01-05 46.200001 45.790001 46.020000 46.070000 13113100.0 41.389545
2018-01-08 46.099998 45.880001 45.950001 46.000000 7068600.0 41.326656
... ... ... ... ... ... ...
2021-04-12 53.549999 53.099998 53.330002 53.349998 8565300.0 53.349998
2021-04-13 53.290001 52.810001 53.040001 53.090000 11071700.0 53.090000
2021-04-14 53.189999 52.650002 52.980000 53.080002 9787600.0 53.080002
2021-04-15 53.660000 53.119999 53.130001 53.330002 13078100.0 53.330002
2021-04-16 53.799999 53.380001 53.740002 53.680000 17958400.0 53.680000
Backtesting.pyを使用するには,データが"Open", “High", “Low", “Close"のカラムを持つ必要があります.pandas-datareaderで取得した場合はすでにこれらのカラムを持っているので問題ないですね.
Backtesting.pyでバックテスト
まずは,Backtesting.pyを動かすのに必要なモジュールをインポートしましょう.
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import SMA
売買手法を作成
次に,手法を書きましょう.
今回は2つのSMA(単純移動平均線)によるゴールデンクロス,デッドクロスで売買をする手法で行います.この手法の説明は,【投資の基本】移動平均線とは?ゴールデンクロスとデッドクロスで売買をご覧ください.
class SmaCross(Strategy):
n1 = 10 # 短期SMA
n2 = 30 # 長期SMA
def init(self):
self.sma1 = self.I(SMA, self.data.Close, self.n1)
self.sma2 = self.I(SMA, self.data.Close, self.n2)
def next(self): #チャートデータの行ごとに呼び出される
if crossover(self.sma1, self.sma2): #sma1がsma2を上回った時
self.buy() # 買い
elif crossover(self.sma2, self.sma1):
self.position.close() # 売り
売り判定のところでself.position.close()ではなくself.sell()にしてしまうと,空売りありになってしまうようです.空売りを禁止するならposition.close()を使いましょう.
バックテストを実行
バックテストの条件は以下のようにbt = Backtest( )の中に書きます.あとは以下のようにbt.run()とすればバックテストが実行されます.
bt = Backtest(
data, # チャートデータ
SmaCross, # 売買戦略
cash=1000, # 最初の所持金
commission=0.00495, # 取引手数料
margin=1.0, # レバレッジ倍率の逆数(0.5で2倍レバレッジ)
trade_on_close=True, # True:現在の終値で取引,False:次の時間の始値で取引
exclusive_orders=True #自動でポジションをクローズ
)
output = bt.run() # バックテスト実行
print(output) # 実行結果(データ)
bt.plot() # 実行結果(グラフ)
所持金は株価よりも多く設定しておかないと購入できずに終わりますから,適切な値にしましょう.
取引手数料は楽天証券の米国株式手数料の0.495%としました.
結果
上のコードを実行すると,以下のような結果(データ)とグラフが表示されると思います.
Start 2018-01-02 00:00:00
End 2021-04-16 00:00:00
Duration 1200 days 00:00:00
Exposure Time [%] 63.0435
Equity Final [$] 938.099
Equity Peak [$] 1205.96
Return [%] -6.19008
Buy & Hold Return [%] 17.8744
Return (Ann.) [%] -1.92598
Volatility (Ann.) [%] 14.1436
Sharpe Ratio 0
Sortino Ratio 0
Calmar Ratio 0
Max. Drawdown [%] -28.3291
Avg. Drawdown [%] -3.40125
Max. Drawdown Duration 420 days 00:00:00
Avg. Drawdown Duration 50 days 00:00:00
# Trades 15
Win Rate [%] 33.3333
Best Trade [%] 15.0922
Worst Trade [%] -7.21554
Avg. Trade [%] -0.449168
Max. Trade Duration 177 days 00:00:00
Avg. Trade Duration 49 days 00:00:00
Profit Factor 0.881756
Expectancy [%] -0.293761
SQN -0.288313
_strategy SmaCross
_equity_curve ...
_trades Size EntryB...




結果の見方は,記事の最後に載せておきます.
15回取引を行い,所持金1000に対して最終資産は938,リターンは-6.2%という結果になりました.
この手法(SMAの期間)では損してしまうということですね.それでは,手法を最適化してみましょう.
売買手法を最適化
SMAの期間を変えることで,最終資産額が大きくなるように最適化してみたいと思います.
Backtesting.pyでは,以下のようにすることでSMAを10~70日の範囲で5日刻みで変化させてEquity Final(最終資産額)が最大になるようなSMAの期間を探してくれます.
※maximizeはデフォルトではSQN(System Quality Number)です
#最適化
output2=bt.optimize(n1=range(10, 70, 5), n2=range(10, 70, 5), maximize='Equity Final [$]')
print(output2)
bt.plot()
consraintオプションを付けることで短期SMAが長期SMAより長くなることを禁止できます.もしも結果が長期SMAよりも短期SMAの方が長くなってしまった場合は以下のようにしましょう.
output2=bt.optimize(n1=range(10, 70, 5), n2=range(10, 70, 5), maximize='Equity Final [$]', constraint=lambda p: p.n1 < p.n2)
最適化の結果は以下のようになりました.
最適化結果
Start 2018-01-02 00:00:00
End 2021-04-16 00:00:00
Duration 1200 days 00:00:00
Exposure Time [%] 68.7198
Equity Final [$] 3217.68
Equity Peak [$] 3414.71
Return [%] 221.768
Buy & Hold Return [%] 211.529
Return (Ann.) [%] 42.715
Volatility (Ann.) [%] 32.9843
Sharpe Ratio 1.29501
Sortino Ratio 2.87914
Calmar Ratio 1.69859
Max. Drawdown [%] -25.1473
Avg. Drawdown [%] -3.24492
Max. Drawdown Duration 227 days 00:00:00
Avg. Drawdown Duration 24 days 00:00:00
# Trades 13
Win Rate [%] 61.5385
Best Trade [%] 55.2328
Worst Trade [%] -6.4954
Avg. Trade [%] 9.68661
Max. Trade Duration 175 days 00:00:00
Avg. Trade Duration 62 days 00:00:00
Profit Factor 7.3015
Expectancy [%] 10.9874
SQN 1.86309
_strategy SmaCross(n1=20,n...
_equity_curve ...
_trades Size EntryB...




最終資産は3217,リターンは221%という結果になりました.
移動平均線が20日と25日,取引回数が13回となっています.
このような取引をしていれば,利益がプラスとなることになります.もちろん,これは「過去のデータでおこなった場合」なので,この日数が未来においても最適なのかを考えるにはいろいろな期間を設定して良い結果を出すのかを調べてみる必要があります.
まとめコード
import pandas_datareader.data as web
import datetime
start = datetime.date(2018,1,1)
end = datetime.date.today()
data = web.DataReader('KO', 'yahoo', start, end) #データの取得
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import SMA
class SmaCross(Strategy):
n1 = 10 # 短期SMA
n2 = 30 # 長期SMA
def init(self):
self.sma1 = self.I(SMA, self.data.Close, self.n1)
self.sma2 = self.I(SMA, self.data.Close, self.n2)
def next(self): # チャートデータの行ごとに呼び出される
if crossover(self.sma1, self.sma2): # sma1がsma2を上回った時
self.buy() # 買い
elif crossover(self.sma2, self.sma1):
self.position.close() # 売り
# バックテストを設定
bt = Backtest(
data, # チャートデータ
SmaCross, # 売買戦略
cash=1000, # 最初の所持金
commission=0.00495, # 取引手数料
margin=1.0, # レバレッジ倍率の逆数(0.5で2倍レバレッジ)
trade_on_close=True, # True:現在の終値で取引,False:次の時間の始値で取引
exclusive_orders=True #自動でポジションをクローズ
)
output = bt.run() # バックテスト実行
print(output) # 実行結果(データ)
bt.plot() # 実行結果(グラフ)
#最適化
output2=bt.optimize(n1=range(10, 70, 5),n2=range(10, 70, 5))
print(output2)
bt.plot()
結果の見方
データ
Start 2018-01-02 00:00:00 #バックテスト開始日
End 2021-04-09 00:00:00 #バックテスト終了日
Duration 1193 days 00:00:00 #バックテスト期間
Exposure Time [%] 34.5079 #ポジション保有期間
Equity Final [$] 1.25808e+06 #最終資産額
Equity Peak [$] 1.31175e+06 #期間中の最高資産額
Return [%] 25.8079 #リターン
Buy & Hold Return [%] 16.7765 #|終了時の価格-開始時の価格|/開始時の価格
Return (Ann.) [%] 7.28285 #年平均リターン
Volatility (Ann.) [%] 18.9324 #年平均ボラティリティ(標準偏差)
Sharpe Ratio 0.384676 #シャープレシオ(Return/Volatility)
Sortino Ratio 0.574517 #ソルティノレシオ(下落リスクに対するリターン)
Calmar Ratio 0.209777 #カルマーレシオ(最大損失率に対する年間平均収益)
Max. Drawdown [%] -34.717 #最大下落率
Avg. Drawdown [%] -2.96757 #平均下落率
Max. Drawdown Duration 382 days 00:00:00 #最長下落期間
Avg. Drawdown Duration 43 days 00:00:00 #平均下落期間
# Trades 12 #トレード回数
Win Rate [%] 83.3333 #勝率(勝ち回数/取引回数)
Best Trade [%] 11.0289 #1回の取引での利益の最大値/所持金
Worst Trade [%] -21.52 #1回の取引での利益の最小値/所持金
Avg. Trade [%] 1.93162 #取引での利益の平均値/所持金
Max. Trade Duration 63 days 00:00:00 #1回の取引最長期間
Avg. Trade Duration 33 days 00:00:00 #取引平均期間
Profit Factor 2.15154 #総利益/総損失
Expectancy [%] 2.29776 #損益期待値
SQN 0.770112 #SQN(SystemQualityNumber)
_strategy SmaCross(n1=42,n... #手法の関数名とパラメータ
_equity_curve ...
_trades Size Entry...
グラフ(上から)
1つ目:資産額(Equity)の推移
2つ目:各取引での損益状況
緑と赤の三角の見方:
上緑:売りによる益
上赤:売りによる損
(下緑:空売りによる益)
(下赤:空売りによる損)
3つ目:株価推移とSMAおよび取引(縞の直線)
緑の縞:その取引による益
赤の縞:その取引による損
4つ目:出来高(Volume)
参考記事
ディスカッション
コメント一覧
各記事、とても参考になり自環境でも動かして試させてもらってます。やはり単体指標では良い結果を出すのは難しいという現実を痛感しました。
> output2=bt.optimize(n1=range(2, 50, 10),n2=range(2, 50, 10), maximize=’Equity Final [$]’)
説明にて「SMAを10~50の範囲で変化させて」とあったので、range関数の引数は range(10, 50, 2) になるのでは、と思いました。他記事の最適化に関する記事(MACD、ベイズ最適化)に掲載しているコードでも同様なので確認いただけますとありがたいです。
https://docs.python.org/ja/3/library/stdtypes.html#range
> 短期SMAが31日,長期SMAが1日と
おそらくご存知かつ蛇足でしょうが、optimizeメソッドにはconstraint引数が用意されていて短期>長期にならないようにできますね。
https://kernc.github.io/backtesting.py/doc/backtesting/backtesting.html#backtesting.backtesting.Backtest.optimize
コメントありがとうございます.
>説明にて「SMAを10~50の範囲で変化させて」とあったので、range関数の引数は range(10, 50, 2) になるのでは、と思いました。他記事の最適化に関する記事(MACD、ベイズ最適化)に掲載しているコードでも同様なので確認いただけますとありがたいです。
私もはじめに違和感を覚え,なにか理由があってこのようにした気がしますが,range(10, 50, 2)で問題ないならこちらの方がいい気がしますね.確認してみます.
>おそらくご存知かつ蛇足でしょうが、optimizeメソッドにはconstraint引数が用意されていて短期>長期にならないようにできますね。
constraint引数については確認していませんでした.ありがとうございます.constraint引数を使って再度やってみたいと思います.
ありがとうございました.