FX自動売買ツール(EA)のバックテストには複数の方法が存在します。MT4やMT5を使うものや、ティックデータを入手してPythonなどで独自にデータ処理する方法などです。
ここでは、Google Colabを用いて、Pythonでバックテストを行う方法をご紹介します。
- Goolge ColabはPython実行環境を整える面倒な準備は不要で、すぐにPythonが使える。
- Pythonを使えばMT4やMT5のバックテストでは確認することが難しかった細かい指標やグラフによる視覚化も実装可能で、オリジナルのバックテストツールを作成できる。
- Pythonは機械学習に必要なパッケージも充実していて、機械学習を取り入れたFX自動売買ツールのバックテストも行うことが出来る。
以下の記事でもPythonによるバックテストをご紹介していますが、この記事で改めて紹介する方法は、numbaを用いて高速化していたり、より実用性が向上させたタイプになります。
Google Colabおよびインプットデータの準備
この記事では、「Google Colabが使える状態であること」、「インプットデータであるティックデータが準備出来ていること」を前提に進めていきます。
詳しい準備方法については、先ほどご紹介した記事で解説していますので、一緒に試してみたい方は、それをご覧になって準備が整った状態で進んでいただければと思います。
各種初期設定
Google Driveにマウント
まず、Google Driveにマウントしておきます。
from google.colab import drive
drive.mount('/content/drive')
これで、ColabからGoogle Drive内のファイルを読み込んだり、Google Drive内にファイルを出力したりすることが可能になります。
各種ライブラリのインポート
各種ライブラリをインポートします。
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import numba
from tqdm.notebook import tqdm
import os
全てColabに備わっているので、インストール処理は不要で上記コードのみでそのままインポート可能になっています。
ティックデータの整形
続いてティックデータの整形です。今回は、MT5からダウンロードしたGOLD#(XMTrading KIWAMI極口座のゴールド)のティックデータの2022年12月分をインプットデータとして使用します。ダウンロード方法についても、先ほどご紹介した記事でご確認ください。
csvデータの読み込み
以下のように、csvファイルをdf(Pandasのデータフレーム)として読み込みます。
YYYYMM = '202212'
df = pd.read_table('/content/drive/My Drive/backtest/GOLD#_tick_'+YYYYMM+'.csv')
MT5からダウンロードしたcsvファイルを、Google Driveのマイドライブ直下に作成したbacktestフォルダに”GOLD#_tick_202212.csv”というファイル名で保存しておき、それを読み込んでいます。
データを整形して.pklで保存
上記dfを少し整形した後に、pickle形式で保存しておきます。
df['time'] = pd.to_datetime(df['<DATE>']+ ' '+ df['<TIME>'])
df = df.set_index('time')
df = df.rename(columns={'<BID>':'bid','<ASK>':'ask'})
df = df[['bid','ask']]
df = df.fillna(method='ffill')
df['spread'] = df['ask'] - df['bid']
df.to_pickle('/content/drive/My Drive/backtest/GOLD#_tick_'+YYYYMM+'.pkl')
整形内容は、bidとaskだけにして、NaN値を穴埋めした後、askとbidの差をspreadとしています。
出来上がったdfは以下のようになっています。
取引履歴のdfを用意
以下の通り、取引履歴を格納するためのdf_ordersを用意しておきます。
cols = [
'time',
'bid',
'ask',
'type', # 注文タイプ
'profit', # 実現損益
'buy_position', # buyポジション数
'buy_lot', # buyポジションのlot数合計
'buy_price', # buyポジションの平均価格
'buy_profit', # buyポジションの含み損益
'sell_position', # sellポジション数
'sell_lot', # sellポジションのlot数合計
'sell_price', # sellポジションの平均価格
'sell_profit' # sellポジションの含み損益
]
df_orders = pd.DataFrame(index=[], columns=cols)
パラメーター設定
ここでパラメーターを設定しておきます。
first_lot = 0.01 # 初期ロット
pip_value = 135 # ピップ値
point = 0.01 # 価格の最小単位
spread_limit = 15 # 許容スプレッド
position_limit = 18 # 最大ポジション数
martin_factor =1.5 # マーチン倍率
nanpin_range = 200 # ナンピン幅
tp_range = 100 # 利確幅
lc_range = 1000 # ロスカット幅
ここで想定しているのは、単純なナンピンマーチンEAです。
取引期間の設定
取引期間も設定しておきます。
date_index = pd.date_range("2022-12-01", periods=31, freq="D")
AC_date_list = date_index.to_series().dt.strftime("%Y-%m-%d")
AC_date_list0 = AC_date_list[:5]
AC_date_list[-1]
ここで出来上がるAC_date_listというのは、単純に2022-12-01から2022-12-31までの日付の文字列を格納したのみのリストです。後ほどループ処理する際に使います。
EAのロジック部分(calc_orders)
続いて、EAのロジック部分です。ここが肝心なところではあります。結構長いですが、そのまま載せます。
@numba.njit
def calc_orders(
first_lot,
current_buy_lot,
current_buy_price,
current_sell_lot,
current_sell_price,
buy_position,
buy_lot,
buy_price,
buy_profit,
sell_position,
sell_lot,
sell_price,
sell_profit,
point,
spread_limit,
tick_len,
tick_bid,
tick_ask,
):
orders_time = tick_bid.copy()
orders_time[:] = np.nan
orders_bid = tick_bid.copy()
orders_bid[:] = np.nan
orders_ask = tick_bid.copy()
orders_ask[:] = np.nan
orders_type = tick_bid.copy()
orders_type[:] = np.nan
orders_buy_position = tick_bid.copy()
orders_buy_position[:] = np.nan
orders_sell_position = tick_bid.copy()
orders_sell_position[:] = np.nan
orders_profit = tick_bid.copy()
orders_profit[:] = np.nan
orders_buy_profit = tick_bid.copy()
orders_buy_profit[:] = np.nan
orders_sell_profit = tick_bid.copy()
orders_sell_profit[:] = np.nan
orders_buy_lot = tick_bid.copy()
orders_buy_lot[:] = np.nan
orders_sell_lot = tick_bid.copy()
orders_sell_lot[:] = np.nan
orders_buy_price = tick_bid.copy()
orders_buy_price[:] = np.nan
orders_sell_price = tick_bid.copy()
orders_sell_price[:] = np.nan
if tick_len > 0:
for i in range(tick_len):
##現在価格の取得
bid = tick_bid[i]
ask = tick_ask[i]
spread = ask - bid
###entry_flgロジック
if spread < spread_limit * point:
entry_flg = 1
else:
entry_flg = 0
##ポジションの含み損益の確認
if buy_lot == 0:
buy_profit = 0
else:
buy_profit = round((bid - buy_price) * buy_lot * pip_value / 0.01, 0)
if sell_lot == 0:
sell_profit = 0
else:
sell_profit = round(-(ask - sell_price) * sell_lot * pip_value / 0.01, 0)
##ordersに出力
orders_bid[i] = bid
orders_ask[i] = ask
orders_buy_position[i] = buy_position
orders_buy_lot[i] = buy_lot
orders_buy_price[i] = buy_price
orders_buy_profit[i] = buy_profit
orders_sell_position[i] = sell_position
orders_sell_lot[i] = sell_lot
orders_sell_price[i] = sell_price
orders_sell_profit[i] = sell_profit
#####
##新規buyエントリー
if buy_position == 0 and entry_flg == 1:
buy_position = 1 # buyポジション数
current_buy_lot = first_lot # 最新のbuyポジションのlot数
buy_lot = first_lot # buyポジションのlot数合計
current_buy_price = ask # 最新のbuyポジション価格
buy_price = ask # buyポジションの平均価格
##ordersに出力
orders_type[i] = 1
orders_buy_position[i] = buy_position
orders_profit[i] = 0
orders_buy_lot[i] = current_buy_lot
orders_buy_price[i] = current_buy_price
##新規sellエントリー
if sell_position == 0 and entry_flg == 1:
sell_position = 1 # sellポジション数
current_sell_lot = first_lot # 最新のsellポジションのlot数
sell_lot = first_lot # sellポジションのlot数合計
current_sell_price = bid # 最新のsellポジション価格
sell_price = bid # sellポジションの平均価格
##ordersに出力
orders_type[i] = 2
orders_sell_position[i] = sell_position
orders_profit[i] = 0
orders_sell_lot[i] = current_sell_lot
orders_sell_price[i] = current_sell_price
##追加buyエントリー
if (
buy_position > 0 and
buy_position < position_limit and
entry_flg == 1 and
ask < current_buy_price - nanpin_range * point
):
buy_position += 1 # buyポジション数
x = buy_lot * buy_price # 平均価格算出用
current_buy_lot = round(current_buy_lot * martin_factor + 0.001, 2) # 最新のbuyポジションのlot数
buy_lot += current_buy_lot # buyポジションのlot数合計
current_buy_price = ask # 最新のbuyポジション価格
y = current_buy_lot * current_buy_price # 平均価格算出用
buy_price = round(( x + y ) / buy_lot, 2) # buyポジションの平均価格
##ordersに出力
orders_type[i] = 3
orders_buy_position[i] = buy_position
orders_profit[i] = 0
orders_buy_lot[i] = current_buy_lot
orders_buy_price[i] = current_buy_price
##追加sellエントリー
if (
sell_position > 0 and
sell_position < position_limit and
entry_flg == 1 and
bid > current_sell_price + nanpin_range * point
):
sell_position += 1 # sellポジション数
x = sell_lot * sell_price # 平均価格算出用
current_sell_lot = round(current_sell_lot * martin_factor + 0.001, 2) # 最新のsellポジションのlot数
sell_lot += current_sell_lot # sellポジションのlot数合計
current_sell_price = bid # 最新のsellポジション価格
y = current_sell_lot * current_sell_price # 平均価格算出用
sell_price = round(( x + y ) / sell_lot, 2) # sellポジションの平均価格
##ordersに出力
orders_type[i] = 4
orders_sell_position[i] = sell_position
orders_profit[i] = 0
orders_sell_lot[i] = current_sell_lot
orders_sell_price[i] = current_sell_price
##buyクローズtp
if buy_position >= 1 and bid > buy_price + tp_range * point / buy_position:
##ordersに出力
orders_type[i] = 5
orders_profit[i] = buy_profit
orders_buy_profit[i] = 0
#####
buy_position = 0 # buyポジション数の初期化
buy_profit = 0 # buy_profitの初期化
current_buy_lot = 0 # 最新のbuyポジションのlot数の初期化
buy_lot = 0 # buyポジションのlot数合計の初期化
current_buy_price = 0 # 最新のbuyポジション価格の初期化
buy_price = 0 # buyポジションの平均価格の初期化
##sellクローズtp
if sell_position >= 1 and ask < sell_price - tp_range * point / sell_position:
##ordersに出力
orders_type[i] = 6
orders_profit[i] = sell_profit
orders_sell_profit[i] = 0
#####
sell_position = 0 # sellポジション数の初期化
sell_profit = 0 # sell_profitの初期化
current_sell_lot = 0 # 最新のsellポジションのlot数の初期化
sell_lot = 0 # sellポジションのlot数合計の初期化
current_sell_price = 0 # 最新のsellポジション価格の初期化
sell_price = 0 # sellポジションの平均価格の初期化
##buyクローズlc
if buy_position >= position_limit and bid < current_buy_price - lc_range * point:
##ordersに出力
orders_type[i] = 7
orders_profit[i] = buy_profit
orders_buy_profit[i] = 0
#####
buy_position = 0 # buyポジション数の初期化
buy_profit = 0 # buy_profitの初期化
current_buy_lot = 0 # 最新のbuyポジションのlot数の初期化
buy_lot = 0 # buyポジションのlot数合計の初期化
current_buy_price = 0 # 最新のbuyポジション価格の初期化
buy_price = 0 # buyポジションの平均価格の初期化
##sellクローズlc
if sell_position >= position_limit and ask > current_sell_price + lc_range*point:
##ordersに出力
orders_type[i] = 8
orders_profit[i] = sell_profit
orders_sell_profit[i] = 0
#####
sell_position = 0 # sellポジション数の初期化
sell_profit = 0 # sell_profitの初期化
current_sell_lot = 0 # 最新のsellポジションのlot数の初期化
sell_lot = 0 # sellポジションのlot数合計の初期化
current_sell_price = 0 # 最新のsellポジション価格の初期化
sell_price = 0 # sellポジションの平均価格の初期化
return (orders_bid,
orders_ask,
orders_type,
orders_profit,
orders_buy_position,
orders_buy_lot,
orders_buy_price,
orders_buy_profit,
orders_sell_position,
orders_sell_lot,
orders_sell_price,
orders_sell_profit,
current_buy_lot,
current_buy_price,
current_sell_lot,
current_sell_price,
buy_position,
buy_lot,
buy_price,
buy_profit,
sell_position,
sell_lot,
sell_price,
sell_profit)
ロジック自体は、以下の記事のナンピンマーチンEAとほぼ同じですので、詳しい解説はこちらをご参照お願いします。
ループ処理
上記のcalc_orders関数を用いて、orders(取引履歴)をdf_ordersというハコに納めていきます。
current_buy_lot=0.00
current_buy_price=0.00
current_sell_lot=0.00
current_sell_price=0.00
buy_position=0
buy_lot=0.00
buy_price=0.00
buy_profit=0
sell_position=0
sell_lot=0.00
sell_price=0.00
sell_profit=0
for AC_date in tqdm(AC_date_list):
YYYYMM = AC_date[:4] + AC_date[5:7]
df = pd.read_pickle('/content/drive/My Drive/backtest/GOLD#_tick_'+YYYYMM+'.pkl')
df = df[AC_date + ' 00:00:00' : AC_date + ' 23:59:59' ]
df_tick = df.reset_index()
if(len(df_tick)>0):
df = pd.DataFrame(index=[], columns=cols)
(
df['bid'], df['ask'], df['type'], df['profit'],
df['buy_position'], df['buy_lot'], df['buy_price'], df['buy_profit'],
df['sell_position'], df['sell_lot'], df['sell_price'], df['sell_profit'],
current_buy_lot, current_buy_price,
current_sell_lot, current_sell_price,
buy_position, buy_lot, buy_price, buy_profit,
sell_position, sell_lot, sell_price, sell_profit
)= calc_orders(
first_lot = first_lot,
current_buy_lot = current_buy_lot,
current_buy_price = current_buy_price,
current_sell_lot = current_sell_lot,
current_sell_price = current_sell_price,
buy_position = buy_position,
buy_lot = buy_lot,
buy_price = buy_price,
buy_profit = buy_profit,
sell_position = sell_position,
sell_lot = sell_lot,
sell_price = sell_price,
sell_profit = sell_profit,
point = point,
spread_limit = spread_limit,
tick_len = len(df_tick),
tick_bid = df_tick['bid'].values,
tick_ask = df_tick['ask'].values,
)
df['time'] = df_tick['time']
df['spread'] = df_tick['spread']
df['pip_value'] = pip_value
#df = df.dropna( )
df.to_pickle('/content/drive/My Drive/backtest/outputs/df_orders_'+AC_date+'.pkl')
取引期間
以下の通り、AC_data_listの日数分だけ繰り返す処理です。今回は先ほど指定した通り、2022-12-01から2022-12-31までの31日分だけループします。
for AC_date in tqdm(AC_date_list):
tqdm()というのは、Pythonでfor文の処理状況を確認するのに役立つライブラリです。以下のようにループ処理の進捗状況を表示してくれます。
ティックデータの読み込み
ここは、GOLD#のティックデータが「GOLD#_tick_YYYYMM.pkl」という形でbactestフォルダに年月毎に保存されている前提のコードです。
YYYYMM = AC_date[:4] + AC_date[5:7]
df = pd.read_pickle('/content/drive/My Drive/backtest/GOLD#_tick_'+YYYYMM+'.pkl')
df = df[AC_date + ' 00:00:00' : AC_date + ' 23:59:59' ]
df_tick = df.reset_index()
AC_date=2022-12-01であれば、YYYYMM=202212となり、2022年12月のティックデータを読みに行きます。さらに、2022年12月1日だけのティックデータに絞り込んでから、処理を始めます。
if(len(df_tick)>0):
これは、絞り込んだ後のdf_tickにレコードが存在しているかの確認です。土日だとティックデータも存在しないので、その日はスキップするような処理です。
calc_orders関数部分
初期パラメータ情報やポジション情報およびティックデータをインプット、各取引履歴の情報をアウトプットとして、df_ordersを作っていきます。このcalc_orders関数部分ではpandasのdfは直接使わずに、@numba.njitで高速化しています。これをするのとしないのとでは、かなり処理速度が変わってくるかと思います。
#df = df.dropna( )
最後の行の一つ前に、#df = df.dropna( )というコードを入れています。#が入っているため有効化されていませんが、これを有効化すると、取引を行なったタイミングのレコードだけを取り出したデータになります。
これを有効化していないと、ティックデータ全てに対して、取引情報が付加されたデータになります。取引が行われていないレコードは該当箇所がNaNになります。
dr_ordersの統合
さて、これまでの処理を行えば、Google Drive内に以下のようなファイルが出来上がっているはずです。
これを統合する処理を行います。
for AC_date in AC_date_list0:
file_exists = os.path.exists('/content/drive/My Drive/backtest/outputs/df_orders_'+AC_date+'.pkl')
if file_exists:
df = pd.read_pickle('/content/drive/My Drive/backtest/outputs/df_orders_'+AC_date+'.pkl')
df_orders = df.iloc[0:0]
for AC_date in AC_date_list:
file_exists = os.path.exists('/content/drive/My Drive/backtest/outputs/df_orders_'+AC_date+'.pkl')
if file_exists:
df = pd.read_pickle('/content/drive/My Drive/backtest/outputs/df_orders_'+AC_date+'.pkl')
df_orders = df_orders.append(df)
df_orders.to_pickle('/content/drive/My Drive/backtest/outputs/df_orders_summary.pkl')
簡単に処理概要を解説しておきますと、outputsフォルダ内の指定の日付のファイルが存在するか確認して、存在する場合はdf_ordersに追加していっています。
結果の確認
累積利益の確認
以下の通り、df_orders_summaryのprofitを累積してみた結果を示しておきます。
initial_fund = 1000000
df = df_orders_summary
df = df.reset_index(drop=True)
df = df.set_index('time')
df['total_profit'] = df['profit'].cumsum()
df['total_profit'] = df['total_profit'] + initial_fund
df['total_profit'].plot(figsize=(18,6),lw=3)
初期資金(initial_fund)を100万円として、約50万円の利益です。しかし、これには少し落とし穴があります。
最大含み損(DD)の確認
以下のように、利益だけでなく含み損も考慮してグラフ化してみます。
initial_fund = 1000000
df = df_orders
df = df.reset_index(drop=True)
df = df.set_index('time')
df['total_profit'] = df['profit'].cumsum()
df['total_profit'] = df['total_profit'].fillna(method='ffill')
df['total_profit'] = df['total_profit'] + initial_fund
df['margin'] = df['total_profit'] + df['buy_profit'] + df['sell_profit']
df['total_profit'].plot(figsize=(18, 6),lw=3)
df['margin'].plot(figsize=(18, 6),lw=1,color='darkcyan',style=':')
緑の線がいわゆるDD(ドローダウン)ですが、−600万円くらいまで行っています。つまり、初期資金100万円程度だとこのタイミングで余裕で破綻しているという結果ですね。
まとめ
以上の通り、MT5からダウンロードしたGOLD#のティックデータをインプットとして、Python(Google Colab)でバックテストを行う方法についてご紹介しました。
最後のグラフ化等も含めて、細かくカスタマイズすればMT4やMT5のバックテストと同等以上のものは作れるのではないかと思います。
今回は単純なナンピンマーチンEAロジックとしましたが、calc_orders関数部分のロジックを変えていけば、様々なロジックのEAのバックテストも可能です。
テクニカル指標を反映したバックテスト方法
また、今回はテクニカル指標は全く扱いませんでしたが、今回の内容を少し拡張して、テクニカル指標をロジックに組み込んだパターンのEAについてバックテストを行う方法について以下の記事でご紹介しています。続きの記事になります。よろしければどうぞ。
キャッシュバック口座の開設
作成したEAを利用して実際に運用する際は、通常の口座開設ではなく、キャッシュバック口座を開設するのがおすすめです。以下の記事で、キャッシュバックサイト経由で海外FX口座を開設する際のメリットとデメリットをまとめていますのでご覧ください。
リアル口座をどのように開設するのが最適かを考える参考になると思います。