FX自動売買ツール(EA)のバックテストには複数の方法が存在します。MT4やMT5を使うものや、ティックデータを入手してPythonなどで独自にデータ処理する方法などです。
ここでは、Google Colabを用いて、Pythonでバックテストを行う方法をご紹介します。
- Goolge ColabはPython実行環境を整える面倒な準備は不要で、すぐにPythonが使える。
- Pythonを使えばMT4やMT5のバックテストでは確認することが難しかった細かい指標やグラフによる視覚化も実装可能で、オリジナルのバックテストツールを作成できる。
- Pythonは機械学習に必要なパッケージも充実していて、機械学習を取り入れたFX自動売買ツールのバックテストも行うことが出来る。
以下の記事ではnumbaを用いて高速化したPythonによるバックテストをご紹介しましたが、今回はそれをさらに拡張し、テクニカル指標も反映して、より実践的なバックテストを行います。
一度こちらの記事をお読みいただいてから進んでいただけるとスムーズです。
各種初期設定
Google Driveにマウント
まず、Google Driveにマウントしておきます。
from google.colab import drive
drive.mount('/content/drive')
これで、ColabからGoogle Drive内のファイルを読み込んだり、Google Drive内にファイルを出力したりすることが可能になります。
Ta-Libのインストール
Ta-Libとは、テクニカル指標を計算するためのライブラリです。ColabではTa-Libがそのまま使える状態ではないので、インストール処理が必要です。以下のコードを実行します。
!curl -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz -O && tar xzvf ta-lib-0.4.0-src.tar.gz
!cd ta-lib && ./configure --prefix=/usr && make && make install && cd - && pip install ta-lib
少し時間がかかりますが、最後に「Successfully installed ta-lib-0.4.25」のような表示が出ればインストール成功です。
各種ライブラリのインポート
各種ライブラリをインポートします。
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import talib
import numba
from tqdm.notebook import tqdm
import os
Ta-Lib以外は、Colabに備わっているので、インストール処理は不要で上記コードのみでそのままインポート可能になっています。
ティックデータにテクニカル指標を追加
使用するティックデータは、前回と同じMT5からダウンロードしたGOLD#(XMTrading KIWAMI極口座のゴールド)の2022年12月分です。
ティックデータを準備
まず、前回の記事を参考に、以下のようなdfを用意します。(前回の記事で作成したものです)
テクニカル指標の追加
さらに、以下の記事を参考に、ティックデータにテクニカル指標を追加します。
すると、以下のようなdfが作成出来ます。
追加したテクニカル指標を簡単に解説しておきます。
- MA_M1:1分足の単純移動平均線
- DMI_flg:「+DI > -DIであれば、1」、「+DI < -DIであれば、-1」というフラグ。それぞれ、1分足、5分足、15分足、1時間足
- ATR_H1:1時間足のATR
このdfをGoogle Driveのマイドライブ直下のbacktestフォルダに、「GOLD#_tick_202212.pkl」というファイル名で保存しておきます。
取引履歴の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 = 5 # 最大ポジション数
martin_factor = 2.0 # マーチン倍率
entry_range = 60 # エントリー幅
lower_ATR = 2.8 # エントリーATR下限
upper_ATR = 10.0 # エントリーATR上限
前回の記事から、ナンピン幅、利確幅、ロスカット幅を削除して、エントリー幅、エントリーATR下限、エントリーATR上限の3つを追加しています。
取引期間の設定
取引期間も設定しておきます。
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のロジック部分です。ここが肝心なところではあります。結構長いのでパートごとに簡単に解説します。
インプットの設定
まずはインプットの設定です。MA、DMI、ATRといったテクニカル指標もインプットに加えています。
@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,
entry_range,
current_buy_ATR,
current_sell_ATR,
lower_ATR,
upper_ATR,
tick_MA_M1,
tick_DMI_M1_flg,
tick_DMI_M5_flg,
tick_DMI_M15_flg,
tick_DMI_H1_flg,
tick_ATR_H1,
):
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
#flg判定
MA_M1 = tick_MA_M1[i]
DMI_M1_flg = tick_DMI_M1_flg[i]
DMI_M5_flg = tick_DMI_M5_flg[i]
DMI_M15_flg = tick_DMI_M15_flg[i]
DMI_H1_flg = tick_DMI_H1_flg[i]
ATR_H1 = tick_ATR_H1[i]
###entry_flgロジック
if MA_M1+spread-entry_range*point > ask and spread<spread_limit*point:
entry_flg = 1
elif MA_M1+entry_range*point < bid and spread<spread_limit*point:
entry_flg = -1
else:
entry_flg = 0
if lower_ATR <= ATR_H1 and ATR_H1 < upper_ATR:
ATR_flg = 1
else:
ATR_flg = 0
if DMI_M1_flg==1 and DMI_M5_flg==1 and DMI_M15_flg==1 and DMI_H1_flg==1:
DMI_flg = 1
elif DMI_M1_flg==-1 and DMI_M5_flg==-1 and DMI_M15_flg==-1 and DMI_H1_flg==-1:
DMI_flg = -1
else:
DMI_flg = 0
if entry_flg==1 and ATR_flg==1 and DMI_flg==1:
order_flg=1
elif entry_flg==-1 and ATR_flg==1 and DMI_flg==-1:
order_flg=-1
else:
order_flg=0
ここでエントリー条件のロジックを入れています。
- 現在地がMA_M1よりentry_rangeだけ乖離していて、
- ATR_H1が指定の範囲内で、
- 1分足〜1時間足までのDMI_flgが全て揃ったら、
- buy or sellのフラグが立つ
というようなロジックです。
ポジションの確認
ポジションを確認して含み損益を計算しています。
##ポジションの含み損益の確認
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
それをordersに出力しておきます。後ほど、DD(ドローダウン)の計算に使えます。
新規エントリー注文
新規エントリー注文です。order_flgが1または-1かどうかで、buyエントリーまたはsellエントリーが発生します。
##新規buyエントリー
if buy_position == 0 and order_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ポジションの平均価格
current_buy_ATR =ATR_H1
##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 order_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ポジションの平均価格
current_sell_ATR =ATR_H1
##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
また、その注文情報をordersに出力しておきます。
追加エントリー(ナンピン)注文
ナンピン注文です。ここはナンピン幅を一つ前にポジションを持った時のATR(1時間足)という設定にしています。
##追加buyエントリー
if (
buy_position > 0 and
buy_position < position_limit and
order_flg == 1 and
ask < current_buy_price - current_buy_ATR ): 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ポジションの平均価格 current_buy_ATR =ATR_H1 ##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 order_flg == -1 and bid > current_sell_price + current_sell_ATR
):
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ポジションの平均価格
current_sell_ATR =ATR_H1
##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
クローズ注文(利確)
ポジションクローズ注文です。利確条件は、ポジションの平均価格がATR(1時間足)だけの利確幅に達したら、です。
##buyクローズtp
if buy_position >= 1 and bid > buy_price + current_buy_ATR:
##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 - current_sell_ATR:
##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ポジションの平均価格の初期化
クローズ注文(損切り)
今度は損切りの注文です。損切り条件は、最終ポジションの価格よりもATR(1時間足)だけさらに動いたら、です。
##buyクローズlc
if buy_position >= position_limit and bid < current_buy_price - current_buy_ATR: ##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 + current_sell_ATR:
##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,
current_buy_ATR,
current_sell_ATR
)
ループ処理
上記の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
current_buy_ATR=0.00
current_sell_ATR=0.00
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/tick/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,
current_buy_ATR, current_sell_ATR
)= 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,
entry_range=entry_range,
lower_ATR=lower_ATR,
upper_ATR=upper_ATR,
current_buy_ATR=current_buy_ATR,
current_sell_ATR=current_sell_ATR,
tick_len=len(df_tick),
tick_bid=df_tick['bid'].values,
tick_ask=df_tick['ask'].values,
tick_MA_M1=df_tick['MA_M1'].values,
tick_DMI_M1_flg=df_tick['DMI_M1_flg'].values,
tick_DMI_M5_flg=df_tick['DMI_M5_flg'].values,
tick_DMI_M15_flg=df_tick['DMI_M15_flg'].values,
tick_DMI_H1_flg=df_tick['DMI_H1_flg'].values,
tick_ATR_H1=df_tick['ATR_H1'].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/'+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)
今回は最大ポジション数を5にしていて、それからさらにATRだけ動いてしまったら損切りするというロジックにしています。つまり、最後の損切りしているところ以外はポジション数5以内で利益を出せたという結果ですね。
最大含み損(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(ドローダウン)で、その動きがより正確になっています。
5万円くらい利益を積み重ねたところで3万円くらい損切りして、結果2万円くらいの利益となっています。
まとめ
以上の通り、MT5からダウンロードしたGOLD#のティックデータをインプットとして、Python(Google Colab)でバックテストを行う方法についてご紹介しました。
今回はエントリー条件にテクニカル指標を取り入れるパターンでご紹介しました。
少し長くなってしまいましたが、一つずつ追っていけば、それぞれのコードの意味がわかるはずです。こちらをベースに細かくカスタマイズすれば、お好みのロジックでMT4やMT5のバックテストと同等以上のものは作れるのではないかと思います。
キャッシュバック口座の開設
作成したEAを利用して実際に運用する際は、通常の口座開設ではなく、キャッシュバック口座を開設するのがおすすめです。以下の記事で、キャッシュバックサイト経由で海外FX口座を開設する際のメリットとデメリットをまとめていますのでご覧ください。
リアル口座をどのように開設するのが最適かを考える参考になると思います。