PythonでMT5のウォークフォワードテストを少しだけ楽にする

言い訳がましいタイトルだが(笑)、PythonでMT5のウォークフォワードテストを少しだけ楽にするコードを書いてみた。残念ながらウォークフォワードテストそのものができるわけではない。

MT5側で最適化をする準備をした上でPyhtonでMT5を起動し、指定した開始時間と終了時間で最適化を実行する。

ただそれだけの機能だが、Python側で開始時間と終了時間をずらしながらループすれば、イン期間での最適化の繰り返しを自動でやってくれるというわけだ。ウォークフォワードテストではその部分が面倒なので、いくらかは楽になる。

文章だけ見ると、これはこれで面倒そうだが、一度やってみれば後は楽だ(と思う)。

なお、PythonでMT5を起動し、最適化する関数を作るにあたっては豊嶋先生の以下の記事を参考にした。

環境

  • Python: 3.9.16 64-bit
  • Spyder IDE: 5.4.1
  • OANDA MetaTrader 5: 5.00 build 4153
  • MetaEditor: 5.00 build 4153

MT5を起動し、最適化する関数

ここのコードと下にある「Pyhonで最適化のループ」のコードはSpyderで実行することを想定している。

Spyderの「IPython Console」にコピペして「Enter」キーを押せば実行できる。

なお、実行中、Spyderの作業フォルダに「myconfig.ini」というファイルを作ってバックテストに必要な設定を書き込んでいるが、もし万が一(そんな偶然はないと思うが)同フォルダ内に同名のファイルがあると上書きしてしまうので注意。

import pandas as pd
import subprocess
from bs4 import BeautifulSoup

def optimize(expert,symbol,period,start_year,start_month,start_day,end_year,
             end_month,end_day,model,optimization_criterion,install_folder,
             data_folder):
    from_date='FromDate='+str(start_year)+'.'+str(start_month)+'.'+str(start_day)
    to_date='ToDate='+str(end_year)+'.'+str(end_month)+'.'+str(end_day)
    f=open('myconfig.ini','w')
    param=['[Tester]\n',
           'Expert='+expert+'\n',
           'Symbol='+symbol+'\n',
           'Period='+period+'\n',
           'Model='+str(model)+'\n',
           'ExecutionMode=0\n',
           'Optimization=1\n',
           'OptimizationCriterion='+str(optimization_criterion)+'\n',
           'ForwardMode=0\n',
           'Report=report\n',
           'ReplaceReport=1\n',
           'ShutdownTerminal=1\n',
           from_date+'\n',
           to_date]
    f.writelines(param)
    f.close()
    subprocess.run(install_folder+'\\terminal64.exe /config:myconfig.ini')
    with open(data_folder+'\\report.xml') as fp:
        soup = BeautifulSoup(fp, 'xml')
    df = pd.DataFrame()
    for r,row in enumerate(soup.find_all('Row')):
        for c,cell in enumerate(row.find_all('Data')):
            df.loc[r, c] = cell.string
    df.columns=df.iloc[0]
    df=df.iloc[1]
    df=df.iloc[2:]
    return df

最適化をする前の準備

インストールフォルダーのパス

先ずMT5をインストールしているフォルダーのパスを調べる。

OANDAのMT5なら

C:\Program Files\OANDA MetaTrader 5

になると思うが、これを「'」で囲み、「\」は2つにして

'C:\\Program Files\\OANDA MetaTrader 5'

とする。

別の業者であればそれに合わせる。

データフォルダーのパス

次にMT5のデータフォルダーのパスを調べる。

その手順は

  1. MT5のメニューバーの「ファイル」をクリックする。
  2. 「データフォルダを開く」をクリックする。
  3. パスをコピーする。

恐らく

C:\Users\****\AppData\Roaming\MetaQuotes\Terminal\********************************

のようになっていると思うので、やはり、これを「'」で囲み、「\」は2つにして

'C:\\Users\\****\\AppData\\Roaming\\MetaQuotes\\Terminal\\********************************'

とする。

MT5側の準備

ここではサンプルEAの「MACD Sample」を使って最適化することにする。

最適化するパラメータは「Take Profit (pips)」と「Trailing Stop Level (pips)」の2つとし、20pipsから200pipsまで20pips刻みで最適化しよう。

その他のパラメータはデフォルトのまま。

  1. MT5のストラテジーテスターで「エクスパート」を「Examples\MACD\MACD Sample.ex5」にする。
  2. 「パラメータ」タブをクリックする。
  3. 「Take Profit (pips)」にチェックを入れ、「スタート」、「ステップ」、「ストップ」をそれぞれ20、20、200とする。
  4. 「Trailing Stop Level (pips)」にチェックを入れ、「スタート」、「ステップ」、「ストップ」をそれぞれ20、20、200とする。
  5. MT5を終了する。

開始時間と終了時間をずらしながら最適化するにはPythonによってMT5を起動する必要があるので、一旦終了させる。

Pyhonで最適化のループ

ここではユーロドルの1時間足を使って最適化することにしよう。

「モデル」は「始値のみ」、「オプティマイズ」の指標は「残高最大」にする。

なお、上の関数により、「フォワード」は「キャンセル」、「延滞」は「延滞ゼロ、理想的な実行」に自動的に変更される。

また、「入金」の数字と通貨、「レバレッジ」は普段使っている設定が使われる。ここでは「1000000」、「JPY」、「1:25」の設定で実行した。

開始期間は2009年1月1日、終了時間は2014年1月1日として、1年ずつ後ろにずらし、計10回ループする。

そして終了年と、オプティマイズ指標で最良だったパラメータ値でのパフォーマンス等を出力する。

ループが1回終了するたびにMT5は再起動する。

expert='Examples\MACD\MACD Sample.ex5' # 指定しないと正常に動作しない
symbol='EURUSD' # 指定しないと直前の設定が使われる
period='H1' # 指定しないとH1になる
start_year=2009
start_month=1
start_day=1
end_year=2014
end_month=1
end_day=1
model=2 # 0:全ティック、1:1分足OHLC、2:始値のみ、3:数値計算、4:リアルティックに基づいたすべてのティック
optimization_criterion=0 # 0:残高最大、1:最大利益率、2:最大予想ペイオフ、3:最小ドローダウン、4:最大回復係数、5:最大シャープレシオ、6:カスタム最大
install_folder='C:\\Program Files\\OANDA MetaTrader 5'
data_folder='C:\\Users\\****\\AppData\\Roaming\\MetaQuotes\\Terminal\\********************************'

for i in range(10):
    df=optimize(expert,symbol,period,start_year+i,start_month,start_day,
                end_year+i,end_month,end_day,model,optimization_criterion,
                install_folder,data_folder)
    print(end_year+i)
    print(df)
    print('\n')

出力

2014
0
Profit                  159821
Expected Payoff    1664.802083
Profit Factor         2.704830
Recovery Factor       4.692750
Sharpe Ratio          3.122384
Custom                       0
Equity DD %             3.2884
Trades                      96
InpTakeProfit               40
InpTrailingStop             20
Name: 1, dtype: object


2015
0
Profit                  190314
Expected Payoff    2409.037975
Profit Factor         2.377659
Recovery Factor       4.625334
Sharpe Ratio          2.146057
Custom                       0
Equity DD %             3.6932
Trades                      79
InpTakeProfit              200
InpTrailingStop            200
Name: 1, dtype: object


2016
0
Profit                  178058
Expected Payoff    2000.651685
Profit Factor         1.950870
Recovery Factor       1.881802
Sharpe Ratio          1.940135
Custom                       0
Equity DD %             8.0390
Trades                      89
InpTakeProfit              200
InpTrailingStop             40
Name: 1, dtype: object


2017
0
Profit                  140856
Expected Payoff    1697.060241
Profit Factor         1.627961
Recovery Factor       1.488634
Sharpe Ratio          1.359581
Custom                       0
Equity DD %             8.4097
Trades                      83
InpTakeProfit              200
InpTrailingStop             40
Name: 1, dtype: object


2018
0
Profit                  234116
Expected Payoff    2787.095238
Profit Factor         2.168535
Recovery Factor       2.474250
Sharpe Ratio          1.816929
Custom                       0
Equity DD %             8.5059
Trades                      84
InpTakeProfit              160
InpTrailingStop            200
Name: 1, dtype: object


2019
0
Profit                  203823
Expected Payoff    2516.333333
Profit Factor         1.983635
Recovery Factor       2.154099
Sharpe Ratio          1.554359
Custom                       0
Equity DD %             9.1116
Trades                      81
InpTakeProfit              160
InpTrailingStop            200
Name: 1, dtype: object


2020
0
Profit                  183006
Expected Payoff    2506.931507
Profit Factor         1.962571
Recovery Factor       1.934095
Sharpe Ratio          1.424284
Custom                       0
Equity DD %             9.2862
Trades                      73
InpTakeProfit              160
InpTrailingStop            200
Name: 1, dtype: object


2021
0
Profit                   79357
Expected Payoff    1167.014706
Profit Factor         1.461464
Recovery Factor       0.905034
Sharpe Ratio          0.990896
Custom                       0
Equity DD %             7.7021
Trades                      68
InpTakeProfit              120
InpTrailingStop             40
Name: 1, dtype: object


2022
0
Profit                   72227
Expected Payoff    1146.460317
Profit Factor         1.585237
Recovery Factor       0.823719
Sharpe Ratio          1.146981
Custom                       0
Equity DD %             7.8130
Trades                      63
InpTakeProfit              120
InpTrailingStop             40
Name: 1, dtype: object


2023
0
Profit                  20315
Expected Payoff    369.363636
Profit Factor        1.149393
Recovery Factor      0.232065
Sharpe Ratio         0.341220
Custom                      0
Equity DD %            8.5149
Trades                     55
InpTakeProfit              60
InpTrailingStop           200
Name: 1, dtype: object

イン期間ではそこそこ勝っているようだが、パラメータ値はあまり安定しておらず、果たしてアウト期間で勝てるかどうかといったところ。

また、1時間足で始値のみなので、利食い損切りのどちらが先にヒットしたかの判定もあいまいになり、この結果はあまり信用できないとも言える。

今回はあくまでも使い方の説明なので、細かいところには立ち入らない。

ウォークフォワードテストをする場合

ウォークフォワードテストをする場合は、上で調べた最適なパラメータ値を利用する。

アウト期間を1年とするなら、上の出力ではイン期間の終了年を表示させているので、その年をアウト期間にすればいい。

2014年から2023年まで実行するなら以下のようなコードをEAに加える。

そしてparam1が「Take Profit (pips)」に、param2が「Trailing Stop Level (pips)」に設定されるようにEAに修正を加える。

double param1=0;
double param2=0;
MqlDateTime mdt;
TimeCurrent(mdt);
if(mdt.year==2014)
  {
  param1=40;
  param2=20;
  }
(中略)
if(mdt.year==2023)
  {
  param1=60;
  param2=200;
  }