見出し画像

エンジニアのためのシステムトレード入門:「移動平均線 + ボリンジャーバンドのバックテスト」

前回は、テクニカル指標をどのように組み合わせてシステムトレードにおける売買判断を行うか、主要な3つのパターンを解説しました。

今回は実際に「移動平均線 + ボリンジャーバンド」のバックテストを実施し、戦略の有効性を確認していきます。


1. バックテスト条件

トヨタ自動車(銘柄コード: 7203.T)の株価データを用いてバックテストを実施します。
下記のチャートは、2024-01-01~2024-12-31 の株価データに移動平均線とボリンジャーバンドをプロットしたものです。
※チャートを描画するプログラムは記事の最後に掲載しています

chart 1.1

今回のバックテストでは、以下の取引ルールを適用します。

1-1. 取引ルール:

  • 買いシグナル(エントリー):

    •  短期移動平均線(SMA 10)が長期移動平均線(SMA 30)を上抜けた(ゴールデンクロス

    • 終値が短期移動平均線(SMA 10)より上

    • 終値がボリンジャーバンドの下限(-2σ)以下

  • 売りシグナル(エグジット):

    • 短期移動平均線(SMA 10)が長期移動平均線(SMA 30)を下抜けた(デッドクロス

    • 終値が短期移動平均線(SMA 10)より下

    • 終値がボリンジャーバンドの上限(+2σ)以上

2. バックテスト実施

2-1. バックテスト実行

さっそくバックテストのプログラムを動かしてみましょう。
初期資金は100万円に設定しています。

取引ルールに従って、買いシグナルと売りシグナルをプロットしたのが下記チャートです。

chart 2.1

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()

いいなと思ったら応援しよう!