
エンジニアのためのシステムトレード入門:「移動平均線 + ボリンジャーバンドのバックテスト」
前回は、テクニカル指標をどのように組み合わせてシステムトレードにおける売買判断を行うか、主要な3つのパターンを解説しました。
今回は実際に「移動平均線 + ボリンジャーバンド」のバックテストを実施し、戦略の有効性を確認していきます。
1. バックテスト条件
トヨタ自動車(銘柄コード: 7203.T)の株価データを用いてバックテストを実施します。
下記のチャートは、2024-01-01~2024-12-31 の株価データに移動平均線とボリンジャーバンドをプロットしたものです。
※チャートを描画するプログラムは記事の最後に掲載しています

今回のバックテストでは、以下の取引ルールを適用します。
1-1. 取引ルール:
買いシグナル(エントリー):
短期移動平均線(SMA 10)が長期移動平均線(SMA 30)を上抜けた(ゴールデンクロス)
終値が短期移動平均線(SMA 10)より上
終値がボリンジャーバンドの下限(-2σ)以下
売りシグナル(エグジット):
短期移動平均線(SMA 10)が長期移動平均線(SMA 30)を下抜けた(デッドクロス)
終値が短期移動平均線(SMA 10)より下
終値がボリンジャーバンドの上限(+2σ)以上
2. バックテスト実施
2-1. バックテスト実行
さっそくバックテストのプログラムを動かしてみましょう。
初期資金は100万円に設定しています。
取引ルールに従って、買いシグナルと売りシグナルをプロットしたのが下記チャートです。

2-2. バックテスト結果
プログラムの実行結果はこうなりました。
※実際に動かしたプログラムは記事の最後に掲載しています

最終残高: 1039859.16 円
損益: 39859.16 円
リターン: 3.99 %
最後は株を保有したままになってしまったので、最終日の価格で精算したものの、3.99%のリターンが得られました。
3. 取引ルールの考察
3-1. ゴールデンクロス(SMA_10 > SMA_30)の有効性
・トレンド相場だった場合、ゴールデンクロス戦略は有効に働いた可能性が高い。
・特に、2024年のトヨタ株が上昇トレンドを持っていた場合、移動平均のクロスを利用した戦略が機能し、リターンがプラスになったと考えられる。
3-2. 適切なエントリーとエグジットのタイミング
・ゴールデンクロスのエントリータイミングが適切であれば、利益を獲得できる可能性が高い。
・ただし、単純にデッドクロスで売る戦略では、利益を最大化できない可能性がある。
3-3. リターンが3.99%とそこまで高くない理由
・売買回数が少なかった可能性(エントリーとエグジットのシグナルが限られた)。
・一度のエントリーでフルポジションを取る戦略では、大きな利益を狙いにくい。
・トレードの最適化が不足しており、さらなる改善の余地がある。
4. リターンをさらに増やすための改善策
4-1. トレードロジックの高度化
(1) 売買判断の精度向上
• 単純なSMAクロスだけではなく、他の指標を併用する:
• RSI(相対力指数):ゴールデンクロスが発生したときに RSI > 50 なら買いエントリー
• ボリンジャーバンド:SMA_10 がSMA_30を上抜けした際に、価格がボリンジャーバンドの中央線以上ならエントリー
• MACD(移動平均収束拡散):クロスの発生時に、MACDがプラスならエントリーを強化
(2) 売却ルールの最適化
• トレーリングストップの導入
• 現在は デッドクロス(SMA_10 < SMA_30)で売るルールだが、価格が一定以上上がった場合、トレーリングストップ(一定割合下落したら売る)を適用すると、利益を伸ばしやすい。
4-2. 資金管理の最適化
(1) 部分売買の導入
• すべての資金を1回で投じるのではなく、分割エントリーをする
• 例:最初に 50% エントリーし、価格が5%上昇するごとに 25% ずつ追加購入
• 逆に価格が2%下がったら一部損切り(リスク軽減)
(2) 最大損失額の制御
• 全額投資ではなく、1回のトレードで資金の5%~10%までのリスクを取る
• ストップロスを設定し、大きな損失を回避
4-3. 最適なパラメータチューニング
(1) SMAの最適化
• 現在は SMA_10 と SMA_30 を使っているが、これが最適とは限らない。
• 最適な短期・長期SMAを決定するためにバックテストを行い、最もリターンの高いパラメータを探る。
(2) ボラティリティに応じたパラメータ調整
• 過去の値動きのボラティリティに応じてSMA期間を動的に変更する
• ボラティリティが高いときは短期間(SMA_5, SMA_20)
• ボラティリティが低いときは長期間(SMA_15, SMA_50)
5. 実行プログラム
5-1. chart 1.1
# 株価データに移動平均線とボリンジャーバンドをプロットしたチャートを描画
import yfinance as yf
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
# トヨタ自動車のティッカーシンボル
ticker = '7203.T'
# データの取得
data = yf.download(ticker, start='2024-01-01', end='2024-12-31')
# データの表示
print(data.head())
# 移動平均線の計算
data['SMA_10'] = data['Close'].rolling(window=10).mean()
data['SMA_30'] = data['Close'].rolling(window=30).mean()
# ボリンジャーバンドの計算
data['SMA_20'] = data['Close'].rolling(window=20).mean()
data['STD_20'] = data['Close'].rolling(window=20).std()
data['Upper_Band'] = data['SMA_20'] + (data['STD_20'] * 2)
data['Lower_Band'] = data['SMA_20'] - (data['STD_20'] * 2)
# ゴールデンクロスとデッドクロスの検出
data.loc[:, 'Signal'] = 0
valid_idx = data.dropna().index # NaNを除いたデータのインデックス
data.loc[valid_idx, 'Signal'] = np.where(data.loc[valid_idx, 'SMA_10'] > data.loc[valid_idx, 'SMA_30'], 1, 0)
data.loc[:, 'Position'] = data['Signal'].diff()
# チャートの描画
plt.figure(figsize=(14, 7))
plt.plot(data.index, data['Close'], label='Close Price', color='blue')
plt.plot(data.index, data['SMA_10'], label='10-Day SMA', color='green')
plt.plot(data.index, data['SMA_30'], label='30-Day SMA', color='red')
plt.plot(data.index, data['Upper_Band'], label='Upper Bollinger Band', color='gray')
plt.plot(data.index, data['Lower_Band'], label='Lower Bollinger Band', color='gray')
plt.title('Toyota Motor Corp. (7203.T) Stock Price with Trading Signals')
plt.xlabel('Date')
plt.ylabel('Price (JPY)')
plt.legend()
plt.grid()
plt.show()
5-2. chart 2.1
# 移動平均線とボリンジャーバンドのチャートに、買いシグナルと売りシグナルをプロット
import yfinance as yf
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
# トヨタ自動車のティッカーシンボル
ticker = '7203.T'
# データの取得
data = yf.download(ticker, start='2024-01-01', end='2024-12-31')
# データの表示
print(data.head())
# 移動平均線の計算
data['SMA_10'] = data['Close'].rolling(window=10).mean()
data['SMA_30'] = data['Close'].rolling(window=30).mean()
# ボリンジャーバンドの計算
data['SMA_20'] = data['Close'].rolling(window=20).mean()
data['STD_20'] = data['Close'].rolling(window=20).std()
data['Upper_Band'] = data['SMA_20'] + (data['STD_20'] * 2)
data['Lower_Band'] = data['SMA_20'] - (data['STD_20'] * 2)
# ゴールデンクロスとデッドクロスの検出
data.loc[:, 'Signal'] = 0
valid_idx = data.dropna().index # NaNを除いたデータのインデックス
data.loc[valid_idx, 'Signal'] = np.where(data.loc[valid_idx, 'SMA_10'] > data.loc[valid_idx, 'SMA_30'], 1, 0)
data.loc[:, 'Position'] = data['Signal'].diff()
# チャートの描画
plt.figure(figsize=(14, 7))
plt.plot(data.index, data['Close'], label='Close Price', color='blue')
plt.plot(data.index, data['SMA_10'], label='10-Day SMA', color='green')
plt.plot(data.index, data['SMA_30'], label='30-Day SMA', color='red')
plt.plot(data.index, data['Upper_Band'], label='Upper Bollinger Band', color='gray')
plt.plot(data.index, data['Lower_Band'], label='Lower Bollinger Band', color='gray')
# ゴールデンクロスのプロット
plt.plot(data[data['Position'] == 1].index,
data['SMA_10'][data['Position'] == 1],
'^', markersize=10, color='g', lw=0, label='Buy Signal')
# デッドクロスのプロット
plt.plot(data[data['Position'] == -1].index,
data['SMA_10'][data['Position'] == -1],
'v', markersize=10, color='r', lw=0, label='Sell Signal')
plt.title('Toyota Motor Corp. (7203.T) Stock Price with Trading Signals')
plt.xlabel('Date')
plt.ylabel('Price (JPY)')
plt.legend()
plt.grid()
plt.show()
5-3. chart 2.2
# バックテスト実施
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
pd.options.display.width = 200
# トヨタ自動車のティッカーシンボル
ticker = '7203.T'
# データの取得
data = yf.download(ticker, start='2024-01-01', end='2024-12-31')
# 移動平均線の計算
data['SMA_10'] = data['Close'].rolling(window=10).mean()
data['SMA_30'] = data['Close'].rolling(window=30).mean()
# シグナル生成
data['Signal'] = 0
data.loc[data['SMA_10'] > data['SMA_30'], 'Signal'] = 1 # ゴールデンクロス
data['Position'] = data['Signal'].diff()
# バックテストの初期設定
initial_balance = 1000000 # 初期資金(円)
cash = initial_balance
shares = 0
portfolio_value = []
trade_history = []
data = data.dropna() # NaNのある行を除去
# バックテストの実施
for i in range(len(data)):
date = data.index[i]
price = data['Close'].iloc[i]
position = data['Position'].iloc[i]
if position == 1: # ゴールデンクロスで買い
shares = cash // price
cash -= shares * price
trade_history.append({'Date': date, 'Type': 'BUY', 'Price': price, 'Shares': shares})
elif position == -1: # デッドクロスで売り
cash += shares * price
trade_history.append({'Date': date, 'Type': 'SELL', 'Price': price, 'Shares': shares})
shares = 0
total_value = cash + shares * price
portfolio_value.append(total_value)
data = data.iloc[:len(portfolio_value)]
data['Portfolio_Value'] = portfolio_value
# 取引履歴のデータフレーム化
trade_df = pd.DataFrame(trade_history)
# パフォーマンスの評価
# data['Position'] が 1 のものを抽出し、その結果から data['Portfolio_Value'] の最後の行を取得
filtered_data = data[data['Position'] == 1]
last_portfolio_value = filtered_data['Portfolio_Value'].iloc[-1] if not filtered_data.empty else None
final_balance = float(last_portfolio_value) # 明示的に float に変換
profit = final_balance - initial_balance
return_rate = (profit / initial_balance) * 100
data.to_csv("data.csv", index=False)
# 結果を出力
print(f'最終残高: {final_balance:.2f} 円')
print(f'損益: {profit:.2f} 円')
print(f'リターン: {return_rate:.2f} %')
# 取引履歴の出力
print("\n取引履歴:")
print(trade_df)
trade_df.to_csv("Bollinger_bands_backtest.csv", index=False)
# パフォーマンスの可視化(Stock Price のスケールを適正化)
fig, ax1 = plt.subplots(figsize=(14, 7))
# 左軸: 株価
ax1.set_xlabel('Date')
ax1.set_ylabel('Stock Price (JPY)', color='blue')
ax1.plot(data.index, data['Close'], label='Stock Price', color='blue', linestyle='-')
ax1.tick_params(axis='y', labelcolor='blue')
# 右軸: ポートフォリオ価値
ax2 = ax1.twinx()
ax2.set_ylabel('Portfolio Value (JPY)', color='orange')
ax2.plot(data.index, data['Portfolio_Value'], label='Portfolio Value', color='orange', linestyle='-')
ax2.tick_params(axis='y', labelcolor='orange')
# 取引履歴をプロット
buy_signals = trade_df[trade_df['Type'] == 'BUY']
sell_signals = trade_df[trade_df['Type'] == 'SELL']
ax1.scatter(buy_signals['Date'], buy_signals['Price'], marker='^', color='g', label='Buy Signal', s=100)
ax1.scatter(sell_signals['Date'], sell_signals['Price'], marker='v', color='r', label='Sell Signal', s=100)
fig.suptitle('Backtest Performance of SMA Strategy')
ax1.legend(loc='upper left')
ax2.legend(loc='upper right')
ax1.grid()
plt.show()