見出し画像

#AI自作 No.5: 株売買のバックテスト・最適化をやってみる

こんにちは。
先日はAI関連のツールが一般の人も使えるようになり、今回のAIブームは大衆化が特徴であるとお伝えしました。

このAIの活用が急速に広がっているのが、プログラミングの領域です。
Microsoftもシステム開発者向けのCopilotの展開を急速に推し進めているようです。
以前からPythonで株式売買戦略の有効性を過去データを利用して検証してみたかったので、今回AIの力を借りてやってみました。
プログラミング部分はすべてAIに任せ、こちらはやりたいことを指示するだけという方針で進めました。
実施した内容を共有したいと思います。


背景

FXで非常に興味をひかれたトレード戦略が紹介されていていました。
FXではなく指数や株式にも適用できるのでないか、その有効性を確認したら面白いことになるかもと思ったことが背景です。
そのトレードとは、利幅は少ないが勝率が非常いものを継続し、利益を積み上げるトレード方法です。
この勝率を上げるためのポイントは以下の2点です。

  • 利確幅を狭く、損切幅を広く調整し

  • より安定的に利確できるよう価格変動の低下したタイミングで取引

なお、この戦略は一日一回取引を行い、ポジションも持ち越しません。
勉強の意味も込めてまずやってみようということで実施した内容を共有します。

また、この記事ではコードの詳細な説明などはしませんので、Pythonでのバックテスト、特にBacktestingライブラリを利用した実装・実行方法についてはQittaサイトなどに記事がありますので、そちらを参照ください。


価格変動の分析

今回は検証対象としてS & P 500 ETF (SPY)です。

これを選んだ理由は、過去10年以上の1分足データが無料で入手できたためです。
分析のために日中の株価データが必要になりますが、過去10年ものデータとなると有料で購入する以外ですと入手できるものが限られます。

さて、まず日中の価格変動が少ない時間帯を確認します。
価格変動が激しいと損切りの割合が高くなり、この戦略が破綻します。
まず1年分のプログラムをAI君に作ってもらいました。
なお、AI ChatとしてBing.com Copilotを利用しました。

以下の形式の株式データを読み、1日の内、6分ごとの時間枠で最も価格変動の少ない時間枠を見つけるPythonプロクラムを作成してください。

株式データ例:
date,open,high,low,close,volume
2022-01-03 09:30:00,476.3,476.585,476.22,476.47,683867

処理ステップは次のイメージです。
1.すべての入力データを読み込み
2.データを6分ごとにグループ化し、各グループ内でopen,high,low,closeのデータを作成
3.さらに各6分間隔のデータごとに、high-lowの値を算出、保存します。標準偏差を算出し、保存します。
4.算出された6分の時間帯のグループで、high-lowの平均値を算出し、折れ線グラフ表示する。
X軸は時間帯、Y軸はHigh-low平均値とします。

時間帯は株式データのタイムスタンプより時間(hour)を整数と、分は6分で割り切り捨てたものを小数部とした数値にします。
例として、9:40は40/6=6.6なので 9.6の時間帯になります。

Copilotに依頼した内容

このプログラムは以下の手順で動作します:株式データを読み込み、pandasresampleメソッドを使用して6分ごとにデータをグループ化します。
各グループ内でopen, high, low, closeのデータを作成し、high-lowの値を算出します。
各6分間隔のデータごとに、high-lowの値を計算し、時間帯ごとに平均値を算出します。
算出された6分の時間帯のグループで、high-lowの平均値を折れ線グラフで表示します。
最も価格変動の少ない時間枠を見つけて表示します。

このプログラムを実行することで、指定された形式の株式データを基に、6分ごとの時間枠で最も価格変動の少ない時間枠を見つけることができます。

Copilotからの回答
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# 株式データを読み込む
data = pd.read_csv('stock_data.csv', parse_dates=['date'])

# 6分ごとにデータをグループ化
data.set_index('date', inplace=True)
resampled_data = data.resample('6T').agg({
    'open': 'first',
    'high': 'max',
    'low': 'min',
    'close': 'last',
    'volume': 'sum'
}).dropna()

# high-lowの値を算出
resampled_data['high_low_diff'] = resampled_data['high'] - resampled_data['low']

# 時間帯を計算
resampled_data['time_period'] = resampled_data.index.hour + resampled_data.index.minute // 6 * 0.1

# 各時間帯のhigh-lowの平均値を算出
time_period_avg = resampled_data.groupby('time_period')['high_low_diff'].mean()

# 折れ線グラフを描画
plt.figure(figsize=(10, 6))
plt.plot(time_period_avg.index, time_period_avg.values, marker='o')
plt.xlabel('時間帯')
plt.ylabel('High-Low平均値')
plt.title('6分ごとの時間帯別High-Low平均値')
plt.grid(True)
plt.show()

# 最も価格変動の少ない時間枠を見つける
min_variation_period = time_period_avg.idxmin()
print(f'最も価格変動の少ない時間枠: {min_variation_period}')

Copilotから上記のコードが提示されました。
読み込み対象の入力ファイル名を直して実行するとエラーなく実行できました。

文字化けしていたので、「グラフのラベルが文字化けしているので直してください」と依頼したところ、修正されたプログラムが提示され再実行してみると、以下のとおり日本語ラベルが表示されました。

次に、残りの年分のデータも処理できるように修正を依頼しました。

入力データが、spy_2013.csv~spy_2024.csvのファイルで13年分あります。このすべてのデータを読み込み、グラフに各年のデータを出力するようにプログラムを変更して

Copilotに追加の依頼

修正されたプログラムを再実行すると、以下のグラフが出力されました。

このグラフを確認すると年ごとに変動幅(6分足high-low差分)は12時台~13時台が最も低いことが確認できます。
プログラムからの出力では、"最も価格変動の少ない時間枠: 12.9"とでており、全データで 12:54分前後が一番価格差の変動が少ないという結果になりました。
また、その時間帯の変動幅は平均で0.8以内と読み取れます。

バックテストでの検証

上記の分析から、価格変動が少なく、損切りされる可能性が低いと思われる時間帯、変動幅の目安が分かりました。
次に、Copilotにバックテスト用のプログラム作成を依頼する前に、売買戦略を決める必要があります。
以下の売買ルールをベースとしてテストしてみます。

売買ルール:

  • 12:54分に買付け、同時に+0.1での利確(指値)、-1.0での損切り(逆指値)の指定、また、買付けの条件として価格が一日始値より+2.0以内であること

  • ポジションが残る場合には、15:59分の大引けで手じまい

なお、買付け時間、利確幅、損切り幅、始値との差異は最適化で調整できるようにパラメータ化します。

Pythonのbacktestingライブラリを利用してバックテスト用のプログラムを作成して。
入力用の株式データが1年分ごとのファイルで13年分あり、spy_2013.csv~spy_2024.csvのファイルになります。
このファイルを読み取り、1年分ごとにバックテストを実施します。
売買戦略は、以下のとおり。
・12:54分に買付け、同時に+0.1での利確(指値)、-1.0での損切り(逆指値)の指定、また、買付けの条件として価格が一日始値より+2.0以内であること
・ポジションが残る場合には、15:59分の大引けで手じまい

なお、買付け時間、利確幅、損切り幅、始値との差異は最適化で調整できるようにパラメータ化して。
また、バックテストの結果とバックテストでの売買データはファイルに出力し後から確認できるようにして。

Copilotに依頼した内容

Copilotが作成したプログラムがこちらです。
一部手直しが必要でしたが、ほぼそのまま動きます。

import pandas as pd
from backtesting import Backtest, Strategy
from backtesting.lib import SignalStrategy, TrailingStrategy
from backtesting.lib import crossover

class CustomStrategy(Strategy):
    buy_time = 12 * 60 + 54  # 12:54 in minutes
    sell_time = 13 * 60 + 59  # 13:59 in minutes
    take_profit = 0.1
    stop_loss = -1.0
    max_open_diff = 2.0

    def init(self):
        pass

    def next(self):
        current_time = self.data.index[-1].hour * 60 + self.data.index[-1].minute
        open_price = self.data.Open[-1]
        current_price = self.data.Close[-1]

        if current_time == self.buy_time and current_price <= open_price + self.max_open_diff:
            self.buy(sl=current_price + self.stop_loss, tp=current_price + self.take_profit)
        
        if current_time == self.sell_time and self.position:
            self.position.close()

def run_backtest(file_path):
    data = pd.read_csv(file_path, index_col=0, parse_dates=True)
    bt = Backtest(data, CustomStrategy, cash=10000, commission=.002)
    stats = bt.run()
    bt.plot()
    return stats

if __name__ == "__main__":
    results = []
    for year in range(2013, 2025):
        file_path = f'spy_{year}.csv'
        stats = run_backtest(file_path)
        results.append(stats)
        stats.to_csv(f'results_{year}.csv')

backtestingライブラリはオープンソースとは言え、ここまでしっかりしたプログラムを作ってくれることは驚きです。
最初の実行として2年分のみで実行した結果は、以下のとおりです。
なお、初期予算は700ドル、手数料なし、証拠金率10%、売買数は10ユニットに設定してあります。
(cash=700,margin=0.1, commission=0)

この結果だけみるとこの戦略は使い物になりそうもありませんが、勝率(Win Rate%)が88.7 or 84.9と高くでています。
これをもっと引き上げるように最適化を実施し、勝率および利益のでるものになるでしょうか?

Backtest最適化の実施

再び、Copilotに最適化ロジックをお願いしたら、Backtestingライブラリの機能を使わなかったので、改めて明示的にお願いした。

Baktestクラスのoptimizeを利用して最適化を実施して

Co;ilotに依頼

追加してくれたのが、以下の "# Optimize the strategy parameters"以下の部分です。

def run_backtest(file_path):
    data = pd.read_csv(file_path, index_col=0, parse_dates=True)
    data = data.rename(columns={'open':'Open','high':'High','low':'Low','close':'Close','volume':'Volume'})
    bt = Backtest(data, CustomStrategy, cash=700, commission=.000, margin=0.1, trade_on_close=True)

    # Optimize the strategy parameters
    stats = bt.optimize(
        buy_time=range(12 * 60 + 54, 12 * 60 + 56, 1),  # 12:54 to 12:55
        take_profit=[0.1, 0.2],
        stop_loss=[1.0, 1.5],
        max_open_diff=[2.0, 2.5],
        maximize='Equity Final [$]',
        constraint=lambda param: param.take_profit > 0 and param.stop_loss > 0

最適化のパラメータを以下のとおり変更して、実行してみました。
なお、最適化の目標として勝率を最大化する設定にしてあります。 maximize='Win Rate [%]'

    # Optimize the strategy parameters
    stats = bt.optimize(
        buy_time=[1115,1130,1145,1200,1215,1230,1245],
        take_profit=[0.07,0.1,0.12],
        stop_loss=[0.9,1.1,1.3],
        max_open_diff=[2.0, 2.2],
        maximize='Win Rate [%]'
    )

上記のパラメータは、7 x 3 x 3 x 2 = 126パターンなり、先ほど実行したバックテストを126回繰り返すイメージになります。
さて、結果はどうなるでしょうか。

40分ほど経過し、出力された結果は以下のとおりでした。
CustomStrategy(buy_time=1115,take_profit=0.07,stop_loss=0.9,max_open_diff=2.0)
これで現時点の最適と思われるパラメータが決まりました。

最終結果の確認と今後の検討

改めてこのパラメータにてバックテストを実行し、詳細を確認します。
以下のとおりoptimize部分をコメントアウトし、backtest.run()を呼び出します。

    stats = bt.run()

    # Optimize the strategy parameters
    # stats = bt.optimize(
    #     buy_time=[1115,1130,1145,1200,1215,1230,1245],
    #     take_profit=[0.07,0.1,0.12],
    #     stop_loss=[0.9,1.1,1.3],
    #     max_open_diff=[2.0, 2.2],
    #     maximize='Win Rate [%]'
    # )
    return stats

いよいよ最適化の結果ですが、2023年と2024年分では以下のとおりです。

2023年は、年利回り10.61%、2024年は6.09%になりました。
最適化前に比べると劇的に改善されました。
特に、勝率が96~94%まで向上したことはこの戦略が使い物になる可能性を秘めているように思います。
ただし、実運用のためにはさらなる改善、検証が必要です。
すくなくとも、以下の検討・確認をしていく必要があるかと考えます。

  • 他の年分の過去データで検証、かつ、パラメータチューニングの実施

  • 特に、最適な時間帯についてはもっと細かく検証するのがベター

  • 他の指数、または、他の株式で変動幅のより少ないものとの比較

最後に私が利用したプログラムを共有いたします。
AIとチャットしながら自分でつくるのも楽しいと思いますが、なんらかの参考になればうれしいです。

価格分析用

バックテスト用 

最後に

この記事はお気に入りいただけましたでしょうか?
内容お役にたちましたらうれしく思います。
また、サポートなど応援いただけましたら幸いです

記事の内容を有効に活用できたなど、記事を気に入っていただけたようでしたらサポートしていただけますと嬉しいです。 また、こんなことを知りたい、あんなことができないかなど記事にしたいことがございましたら、サポートの有無にかかわらずお知らせくださいませ。