PythonのライブラリbacktestingでFXの売買戦略のバックテストと最適化する方法【FXの自動売買プログラムをPythonで自作しよう⑨】

この記事には広告を含む場合があります。

記事内で紹介する商品を購入することで、当サイトに売り上げの一部が還元されることがあります。

これまでの記事で自動でFXのトレードができるようになりました。

しかし、考案した戦略をいきなり本番環境で運用するのはリスクが高いです。

考案した戦略が過去の相場でどれくらいの成績なのかをシュミレーションをしておくと、戦略の有効性をある程度把握することができます。

本記事では、PythonでFXの戦略のバックテスト・最適化する方法について解説します。

バックテスト用ライブラリのbacktesting

Pythonにはバックテスト用のライブラリがいくつかありますが、今回はbacktestingを解説します。

backtestingのインストール

まずpipでインストールしましょう。

Python
pip install backtesting

または、PyCharmでしたら画面下部のPythonパッケージをクリックして、backtestingを検索、インストールをクリックして最新のバージョンを選択するとインストールできます。

サンプルコード

以下は公式で紹介されているサンプルコードです。

Googleの株価データを使用して、移動平均線(10日と20日)のゴールデンクロスとデッドクロスを使ってトレードした結果をシミュレーションしています。

バックテストの結果の詳細を表示するためにprint(output)を追加しています。

Python
from backtesting import Backtest, Strategy
from backtesting.lib import crossover

from backtesting.test import SMA, GOOG


class SmaCross(Strategy):
    n1 = 10
    n2 = 20

    def init(self):
        close = self.data.Close
        self.sma1 = self.I(SMA, close, self.n1)
        self.sma2 = self.I(SMA, close, self.n2)

    def next(self):
        if crossover(self.sma1, self.sma2):
            self.buy()
        elif crossover(self.sma2, self.sma1):
            self.sell()


bt = Backtest(GOOG, SmaCross,
              cash=10000, commission=.002,
              exclusive_orders=True)

output = bt.run()
bt.plot()

print(output)

このプログラムを実行するとコンソールにバックテストの詳細が表示されます。

主な項目は次の通りです。

  • Return:最終損益
  • Sharpe Ratio:シャープレシオ
  • Max. Drawdown:最大ドローダウン
  • Avg. Drawdown:平均ドローダウン
  • Trades:取引数
  • Win Rate:勝率
  • Best Trade:最大利益
  • Worst Trade :最大損失
  • Avg. Trade:平均利益
  • Profit Factor:期待値

また、ブラウザにトレード履歴のチャートが表示されます。

トレードのタイミングや資産の推移を確認することができます。

売買戦略のバックテスト

それでは前回までに作成したプログラムのバックテストをしてみましょう。

資金管理の2%ルールは実装せず、購入価格から0.4円上昇または下降で利確、0.2円上昇または下降で損切りとします。

次のコードがサンプルコードを参考にして、今回のバックテスト用に変更したプログラムです。

Python
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import SMA
import datetime
import requests
import pandas as pd


# 設定項目
interval = '1hour'  # 使用する時間軸
get_price_period = 200  # 四本値を取得する日数


# 四本値を取得する関数
def get_price_data():

    price_data = []
    response_data = []

    today = datetime.datetime.today().date()

    for i in range(get_price_period):
        period = i - get_price_period
        date = (today + datetime.timedelta(days=period)).strftime('%Y%m%d')
        endPoint = 'https://forex-api.coin.z.com/public'
        path = '/v1/klines?symbol=USD_JPY&priceType=ASK&interval=' + interval + '&date=' + date

        response = requests.get(endPoint + path)
        response_data.append(response.json())

    for j in range(len(response_data)):
        for i in response_data[j]['data']:
            price_data.append({'datetime': datetime.datetime.fromtimestamp(int(i['openTime'][:-3])),
                               'Open': i['open'],
                               'High': i['high'],
                               'Low': i['low'],
                               'Close': i['close']})

    df_price_data = pd.DataFrame(price_data)

    # datatimeをindexに指定
    df_price_data.set_index('datetime', inplace=True)

    # 数値データをfloatに変換
    df_price_data = df_price_data.astype(float)

    return df_price_data


# RSIを計算する関数
def calculate_rsi(values, period):

    close = pd.Series(values)
    df_diff = close.diff()

    df_up = df_diff.copy()
    df_down = df_diff.copy()

    df_up[df_up < 0] = 0
    df_down[df_down > 0] = 0

    df_up_sum = df_up.rolling(window=period).mean()
    df_down_sum = df_down.abs().rolling(window=period).mean()

    df_rsi = df_up_sum / (df_up_sum + df_down_sum) * 100

    return df_rsi


# 売買戦略を設定する関数
class Strategy(Strategy):
    n1 = 5
    n2 = 15
    n3 = 10

    def init(self):
        close = self.data.Close
        self.sma1 = self.I(SMA, close, self.n1)
        self.sma2 = self.I(SMA, close, self.n2)
        self.rsi = self.I(calculate_rsi, close, self.n3)

    def next(self):
        if crossover(self.sma1, self.sma2) and self.rsi < 50:
            self.buy(tp=self.data.Close[-1] + 0.4, sl=self.data.Close[-1] - 0.2)
        elif crossover(self.sma2, self.sma1) and self.rsi > 50:
            self.sell(tp=self.data.Close[-1] - 0.4, sl=self.data.Close[-1] + 0.2)


price_data = get_price_data()
bt = Backtest(price_data, Strategy,
              cash=10000, commission=0.00001,
              exclusive_orders=True,)

output = bt.run()
bt.optimize(n1=range(5, 10), n2=(10, 20), n3=(5, 20))
bt.plot()

print(output)

順番に解説します。

ライブラリのインポート

Python
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import SMA
import datetime
import requests
import pandas as pd

backtestingのライブラリとその他の必要なライブラリをインポートします。

設定項目

Python
# 設定項目
interval = '1hour'  # 使用する時間軸
get_price_period = 200  # 四本値を取得する日数

時間軸と四本値を取得する日数の設定をしています。

時間軸は必要に応じて変更してみてください。

四本値を取得については、GMOコインの取扱開始日以降になるように設定してください。

四本値を取得する関数

Python
# 四本値を取得する関数
def get_price_data():

    price_data = []
    response_data = []

    today = datetime.datetime.today().date()

    for i in range(get_price_period):
        period = i - get_price_period
        date = (today + datetime.timedelta(days=period)).strftime('%Y%m%d')
        endPoint = 'https://forex-api.coin.z.com/public'
        path = '/v1/klines?symbol=USD_JPY&priceType=ASK&interval=' + interval + '&date=' + date

        response = requests.get(endPoint + path)
        response_data.append(response.json())

    for j in range(len(response_data)):
        for i in response_data[j]['data']:
            price_data.append({'datetime': datetime.datetime.fromtimestamp(int(i['openTime'][:-3])),
                               'Open': i['open'],
                               'High': i['high'],
                               'Low': i['low'],
                               'Close': i['close']})

    df_price_data = pd.DataFrame(price_data)

    # datatimeをindexに指定
    df_price_data.set_index('datetime', inplace=True)

    # 数値データをfloatに変換
    df_price_data = df_price_data.astype(float)

    return df_price_data

これまでと同様にGMOコインのAPIを利用してデータを取得します。

ただし、backtestingは使用するデータの仕様に指定があるので少し変更を加えています。

具体的には、日付の型やカラム名の最初は大文字(Openなど)にしないといけないといったところです。

RSIを計算する関数

Python
# RSIを計算する関数
def calculate_rsi(values, period):

    close = pd.Series(values)
    df_diff = close.diff()

    df_up = df_diff.copy()
    df_down = df_diff.copy()

    df_up[df_up < 0] = 0
    df_down[df_down > 0] = 0

    df_up_sum = df_up.rolling(window=period).mean()
    df_down_sum = df_down.abs().rolling(window=period).mean()

    df_rsi = df_up_sum / (df_up_sum + df_down_sum) * 100

    return df_rsi

こちらも前回までのコードをbacktesting用に少し変更を加えています。

売買戦略を設定する関数

Python
# 売買戦略を設定する関数
class Strategy(Strategy):
    n1 = 5
    n2 = 15
    n3 = 10

    def init(self):
        close = self.data.Close
        self.sma1 = self.I(SMA, close, self.n1)
        self.sma2 = self.I(SMA, close, self.n2)
        self.rsi = self.I(calculate_rsi, close, self.n3)

    def next(self):
        if crossover(self.sma1, self.sma2) and self.rsi < 50:
            self.buy(tp=self.data.Close[-1] + 0.4, sl=self.data.Close[-1] - 0.2)
        elif crossover(self.sma2, self.sma1) and self.rsi > 50:
            self.sell(tp=self.data.Close[-1] - 0.4, sl=self.data.Close[-1] + 0.2)

n1、n2、n3はそれぞれ、短期移動平均の日数、長期移動平均の日数、RSIの日数を設定しています。

def init(self):では、使用するデータの用意をしています。

close = self.data.Closeは、取得したデータの終値をcloseに代入しています。

self.I(関数名, close, 引数)で、インジケーターを計算する関数、終値のデータ、設定した日数を引き渡しています。

def next(self):で売買条件を指定しています。

if文で条件を指定して、条件を満たしたらbuyで買い、sellで売りを注文する事ができます。よく使う注文は、次の3つです。

  • self.buy(): 買い
  • self.sell(): 売り
  • self.position.close(): 保有ポジションの手仕舞い

また、引数を設定することで指値や逆指値などの売買条件も指定できます。

  • size: 売買数
  • limit: 指値
  • stop: 逆指値
  • tp: 利確注文
  • sl: 損切注文

今回はリスクリワードレシオが2になるように、買いの時の利確と損切りが「tp=self.data.Close[-1] + 0.4, sl=self.data.Close[-1] – 0.2」、売りの時の利確と損切りが「tp=self.data.Close[-1] – 0.4, sl=self.data.Close[-1] + 0.2」としています。

メイン処理

Python
price_data = get_price_data()
bt = Backtest(price_data, Strategy,
              cash=10000, commission=0.00001,
              exclusive_orders=True,)

output = bt.run()
bt.optimize(n1=range(5, 10), n2=range(10, 20), n3=range(5, 20))
bt.plot()

print(output)

上から順番に、価格データを取得します。

次にBacktestのクラスを定義します。指定したデータは次の通りです。

  • 第1引数(必須):価格データ(price_data)
  • 第2引数(必須):Stretegyのクラス(Strategy)
  • cash: バックテスト開始時の所持金 デフォルトは10,000
  • commission: 手数料 デフォルトで0.0 今回は0.00001
  • exclusive_orders: Trueなら売買条件を満たした当日の終値で注文、Falseなら翌日の始値で注文 デフォルトはFalse

output = bt.run()でバックテストが実行されます。

bt.optimize(n1=range(5, 10), n2=range(10, 20), n3=range(5, 20))でパラメータの最適化ができます。rangeの意味ですが、n1は5から10、n2は10から20、n3は5から20までで、最もパフォーマンスの良いパラメータを総当りでシミュレーションしてくれます。

今回の場合は、n1=6、n2=18、n3=8が最もパフォーマンスが良かったパラメータでした。

bt.plot()でチャートを出力できます。先程のパラメータは、HTMLのタイトルやチャート上で確認できます。

print(output)でバックテストの詳細が出力できます。

バックテストの結果

今回のバックテストの結果は次の通りです。

最終的なリターンは0.7%でした。約半年のシミュレーションですが物足りない感じです。

取引回数は59回で、自動売買として運用するには少ないと思います。もう少しエントリーできるタイミングを増やす必要がありそうです。

勝率が42%と順張りにしてはまずまずでした。RSIでフィルタリングができた効果だと思います。

おわりに

backtestingでは、比較的簡単にバックテストやパラメータの最適化をすることができます。

バックテストで事前にシミュレーションすることで、戦略をブラッシュアップすることができます。

しかし注意して頂きたいのが、バックテストの結果が過去のものであって、未来も同じような結果にならないというところです。いくらパラメータを最適化したところで、この先も通用するとは限りません。

パラメータを少し変えただけで結果が大きく変わるような戦略ではなく、普遍的に利益の出せる戦略を見つけることが大切です。

今回、紹介した戦略以外のインジケーターを使用したり、売買条件を変更したりして、オリジナル戦略の開発に挑戦してみてください。