MQL5 クックブック:オーバーフィットの影響低減とクオート不足への対処
Anatoli Kazharski | 25 11月, 2015
はじめに
多くのトレーダーはトレーディングシステムに対する最適なパラメータについて困った経験が何度もあると思います。実際トレードのアルゴリズムだけで十分です。それが将来どのように利用できるのか知りたいのです。どのようなトレーディング戦略を使っていようと、それがシンプルであろうと複雑であろうと、単一インスツルメントか複数インスツルメントであろうと、将来の収益を確保するためどのパラメータを選択すべきかという疑問は避けることのできないものです。
われわれは最適化期間(バックテスト)とそれに続く期間(フォワードテスト)をとおして良い結果を示すパラメータのあるトレーディングシステムを確認する傾向にあります。フォワードテストは実際あまり必要ではありません。適切な結果は履歴データを使って取得できるものなのです。
この方法では確かな答えを得ることのできないおおきな疑問を投げかけます。トレーディングシステムを最適化するにはどのくらいの量の履歴データが必要なのだろうか?問題は選択肢が多く存在することです。すべては利用しようとする価格変動の範囲によるのです。
最適化に必要な履歴の量に戻ると、利用可能なデータは時間内トレードで十分です。これはより長いタイムフレームに対してはつねに正しいとは限りません。一定のパターンの繰り返し数が増えるほど、すなわちトレード数が増えると、将来見ることを期待できる検証されるトレーディングシステムのパフォーマンスはより正しくなります。
特定インスツルメントの価格データが十分な数量の繰り返しを得、より確実に思うのに十分でなければどうでしょうか?答えは、利用可能なインスツルメントすべてからデータを利用する、です。
NeuroShell DayTrader Professionaの例l
MetaTrader 5でのプログラミングに進む前に NeuroShell DayTrader Professionalでの例を見てみましょう。それは複数のシンボルについてトレーディングシステム(コンストラクタに一致した)のパラメータを最適化する優れた機能を提供してくれます。トレードモジュール設定に必要なパラメータをセットし、各シンボル個別に最適化を行うか一度にすべてのシンボルに対して最適なパラメータ設定を見つけることができます。このオプションは 最適化 タブ内に見つけることができます。
図1 NeuroShell DayTrader Professionalのトレードモジュール内最適化タブ
われわれの場合、2とおりの最適化手法の結果を比較する必要があるだけなので、任意のシンプルなトレーディングシステムが行います。よってシステム選択は現時点ではほとんど重要ではありません。
トレーディング戦略を NeuroShell DayTrader Professional でコンパイルする方法についての情報は、私のブログ(適切な情報を検索するかタグをつけることができます)にある別の記事で見ることができます。また NeuroShell DayTrader Professionalと互換性のあるフォーマットで MetaTrader 5 からクオートをダウンロードできるスクリプトを利用する方法について述べ例示している記事 "How to Prepare MetaTrader 5 Quotes for Other Applications"を読むことをお薦めします。
この検証を行うために、私は2000年から2013年1月まで8種類のシンボルについての日時バーあら取得したデータを準備しました。
図2 NeuroShell DayTrader Professionalのテスト用シンボルリスト
下図は2とおりの最適化結果を表示しています。上部は各シンボルがそれ自体のパラメータを取得する最適化結果です。一方下部はパラメータが全シンボル共通の最適化結果を示しています。
図3 2とおりのパラメータ最適化モードの結果比較
共通パラメータを示す結果は各シンボルでパラメータが異なる場合ほどよくありません。それはトレーディングシステムが全シンボルに対して同一パラメータにより数多くの多様な価格変動パターン(変動率、トレンド/フラットの数)を対象とすると信頼性を高めます。
同じテーマを続けると、ボリュームの大きいデータを用いた最適化において論理的に別の引数を見つけることができます。特定の通過ペア、たとえば EURUSDの価格変動は後に(2、5、10年後)かなり異なるようです。たとえば GBPUSD の価格トレンドは EURUSD の過去の価格変動に似ており、その逆もしかりです。これは任意のインスツルメントに対して真であるのでそれに準備が必要です。
MetaTrader 5での例
MetaTrader 5ではどんなパラメータ最適化モードが提供されるか見ていきましょう。以下で最適化モードのドロップダウンリストにある矢印で3マークされている銘柄リストで選択されるあらゆるシンボル最適化モードを確認することができます。
図4 MetaTrader 5 ストラテジーテスタの最適化モード
このモードにより各シンボルについて一つずつ現パラメータを持つ EA の検証のみ可能です。検証に使用されるシンボルは現在銘柄ウィンドウで選択されているものです。すなわち、パラメータ最適化のこの場合行われません。ただし MetaTrader 5 および MQL5 によりこの考えをみなさんご自身で実装することが可能となります。
ここでそのような EAの実装方法を確認する必要があります。シンボルリストはテキストファイル(*.txt)で提供されます。のちにシンボルリストを複数セット格納する機能を実装します。各セットはセクション番号を持つ各々のヘッダのある個別セクションに設定されます。番号は視覚的確認をよりよく行うために必要です。
番号の前 # を入れることは重要である点に注意が必要です。それによりシンボルの配列に書き込む際 Expert Advisor が正しいデータセットを取得することができます。通常、ヘッダには任意の記号が入りますが常に # を持つ必要があります。番号記号は Expert Advisor がセクションを判断/数えるシンボルに従い任意の別の記号と置き換えることができます。その場合、置き換えはコードに反映する必要があります。
以下で検証用の3種類のシンボルセットを持つ SymbolsList.txt ファイルを確認することができます。表示されているこのファイルはのちにメソッド検証の際使用されます。
図5 検証用にテキストファイル内に提供される複数のシンボルセット
外部パラメータにはもう一つ別のパラメータSectionOfSymbolListを追加し、現検証で Expert Advisor が使用するシンボルセットを指示します。このパラメータはシンボルセットを決める値(ゼロ以上)を取ります。その値が利用可能なセット数を超えれば、Expert Advisor はログに対応する入力を下記、検証は現シンボルに対してのみ行われます。
SymbolsList.txt はローカル端末ディレクトリの Metatrader 5\MQL5\Files下に入れる必要があります。それは共通フォルダに入れることもできますが、この場合は MQL5 クラウドネットワークのパラメータ最適化には利用できません。また検証用ファイルと適切なカスタムインディケータへのアクセスが可能となるようにファイルの冒頭に以下の行を書き込む必要があります。
//--- Allow access to the external file and indicator for optimization in the cloud #property tester_file "SymbolsList.txt" #property tester_indicator "EventsSpy.ex5"
われわれのOur Expert Advisor は記事 "MQL5 Cookbook: Developing a Multi-Currency Expert Advisor with Unlimited Number of Parameters"で取り上げられている既製のマルチ通貨 Expert Advisor を基にしています。そこにあるトレーディング戦略はひじょうにシンプルですが、メソッドの有効性を検証するには十分なものです。不要な部分を消去し、必要なものを追加し既存の適切なコードを修正するだけです。シリーズの以前に発表されている記事 "MQL5 Cookbook: Writing the History of Deals to a File and Creating Balance Charts for Each Symbol in Excel"に広範にわたって記載のある機能を保存するレポートによって確かにわれわれのExpert Advisor を強化します。対象メソッドの有効性を判定するにはすべてのシンボルに対する残高チャートも必要です。
Expert Advisor の外部パラメータは次のように変更します。
//--- External parameters of the Expert Advisor sinput int SectionOfSymbolList = 1; // Section number in the symbol lists sinput bool UpdateReport = false; // Report update sinput string delimeter_00=""; // -------------------------------- sinput long MagicNumber = 777; // Magic number sinput int Deviation = 10; // Slippage sinput string delimeter_01=""; // -------------------------------- input int IndicatorPeriod = 5; // Indicator period input double TakeProfit = 100; // Take Profit input double StopLoss = 50; // Stop Loss input double TrailingStop = 10; // Trailing Stop input bool Reverse = true; // Position reversal input double Lot = 0.1; // Lot input double VolumeIncrease = 0.1; // Position volume increase input double VolumeIncreaseStep = 10; // Volume increase step
外部パラメータに関する配列はすべて削除する必要があります。それらはのちに必要となりコード全体をとおして外部変数と置き換えるためですシンボルの動的配列、InputSymbols[] だけ残します。そのサイズはSymbolsList.txt ファイルにあるセットの一組から使用するシンボル数に依存します。Expert Advisor がストラテジーテスタ外で使用されるなら、その配列サイズは 1 です。それは実時間モードでは Expert Advisor はシンボル1個とのみ連携するためです。
対応する変更も配列初期化ファイル、InitializeArrays.mqh内で行われる必要があります。よって外部変数の配列初期化を行うすべての関数は削除します。よってInitializeArraySymbols() 関数は以下のように記述されます。
//+------------------------------------------------------------------+ //| Filling the array of symbols | //+------------------------------------------------------------------+ void InitializeArraySymbols() { int strings_count =0; // Number of strings in the symbol file string checked_symbol =""; // To check the accessibility of the symbol on the trade server //--- Test mode message string message_01="<--- All symbol names in the <- SymbolsList.txt -> file are incorrect ... --->\n" "<--- ... or the value of the \"Section of List Symbols\" parameter is greater, " "than the number of file sections! --->\n" "<--- Therefore we will test only the current symbol. --->"; //--- Real-time mode message string message_02="<--- In real-time mode, we only work with the current symbol. --->"; //--- If in real-time mode if(!IsRealtime()) { //--- Get the number of strings from the specified symbol set in the file and fill the temporary array of symbols strings_count=ReadSymbolsFromFile("SymbolsList.txt"); //--- Iterate over all symbols from the specified set for(int s=0; s<strings_count; s++) { //--- If the correct string is returned following the symbol check if((checked_symbol=GetSymbolByName(temporary_symbols[s]))!="") { //--- increase the counter SYMBOLS_COUNT++; //--- set/increase the array size ArrayResize(InputSymbols,SYMBOLS_COUNT); //--- index with the symbol name InputSymbols[SYMBOLS_COUNT-1]=checked_symbol; } } } //--- If all symbol names were not input correctly or if currently working in real-time mode if(SYMBOLS_COUNT==0) { //--- Real-time mode message if(IsRealtime()) Print(message_02); //--- Test mode message if(!IsRealtime()) Print(message_01); //--- We will work with the current symbol only SYMBOLS_COUNT=1; //--- set the array size and ArrayResize(InputSymbols,SYMBOLS_COUNT); //--- index with the current symbol name InputSymbols[0]=_Symbol; } }
ReadSymbolsFromFile() 関数のコードもまた変更の必要があります。それは前はシンボルリスト全体を読みましたが、これからは指定のシンボルセットだけを読むようにします。以下が修正済みの関数コードです。
//+------------------------------------------------------------------+ //| Returning the number of strings (symbols) from the specified | //| set in the file and filling the temporary array of symbols | //+------------------------------------------------------------------+ //--- When preparing the file, symbols in the list should be separated with a line break int ReadSymbolsFromFile(string file_name) { ulong offset =0; // Offset for determining the position of the file pointer string delimeter ="#"; // Identifier of the section start string read_line =""; // For the check of the read string int limit_count =0; // Counter limiting the number of the possibly open charts int strings_count =0; // String counter int sections_count =-1; // Section counter //--- Message 01 string message_01="<--- The <- "+file_name+" -> file has not been prepared appropriately! --->\n" "<--- The first string does not contain the section number identifier ("+delimeter+")! --->"; //--- Message 02 string message_02="<--- The <- "+file_name+" -> file has not been prepared appropriately! --->\n" "<--- There is no line break identifier in the last string, --->\n" "<--- so only the current symbol will be involved in testing. --->"; //--- Message 03 string message_03="<--- The <- "+file_name+" -> file could not be found! --->" "<--- Only the current symbol will be involved in testing. --->"; //--- Open the file (get the handle) for reading in the local directory of the terminal int file_handle=FileOpen(file_name,FILE_READ|FILE_ANSI,'\n'); //--- If the file handle has been obtained if(file_handle!=INVALID_HANDLE) { //--- Read until the current position of the file pointer // reaches the end of the file or until the program is deleted while(!FileIsEnding(file_handle) || !IsStopped()) { //--- Read until the end of the string or until the program is deleted while(!FileIsLineEnding(file_handle) || !IsStopped()) { //--- Read the whole string read_line=FileReadString(file_handle); //--- If the section number identifier has been found if(StringFind(read_line,delimeter,0)>-1) //--- Increase the section counter sections_count++; //--- If the section has been read, exit the function if(sections_count>SectionOfSymbolList) { FileClose(file_handle); // Close the file return(strings_count); // Return the number of strings in the file } //--- If this is the first iteration and the first string does not contain the section number identifier if(limit_count==0 && sections_count==-1) { PrepareArrayForOneSymbol(strings_count,message_01); //--- Close the file FileClose(file_handle); //--- Return the number of strings in the file return(strings_count); } //--- Increase the counter limiting the number of the possibly open charts limit_count++; //--- If the limit has been reached if(limit_count>=CHARTS_MAX) { PrepareArrayForOneSymbol(strings_count,message_02); //--- Close the file FileClose(file_handle); //--- Return the number of strings in the file return(strings_count); } //--- Get the position of the pointer offset=FileTell(file_handle); //--- If this is the end of the string if(FileIsLineEnding(file_handle)) { //--- Go to the next string if this is not the end of the file // For this purpose, increase the offset of the file pointer if(!FileIsEnding(file_handle)) offset++; //--- move it to the next string FileSeek(file_handle,offset,SEEK_SET); //--- If we are not in the specified section of the file, exit the loop if(sections_count!=SectionOfSymbolList) break; //--- Otherwise, else { //--- if the string is not empty if(read_line!="") { //--- increase the string counter strings_count++; //--- increase the size of the array of strings, ArrayResize(temporary_symbols,strings_count); //--- write the string to the current index temporary_symbols[strings_count-1]=read_line; } } //--- Exit the loop break; } } //--- If this is the end of the file, terminate the entire loop if(FileIsEnding(file_handle)) break; } //--- Close the file FileClose(file_handle); } else PrepareArrayForOneSymbol(strings_count,message_03); //--- Return the number of strings in the file return(strings_count); }
上記コードの文字列の中に強調表示されているのが判ります。それら文字列にはエラーの場合シンボル1個(現)に対する配列を準備するだけのPrepareArrayForOneSymbol() 関数が含まれます。
//+------------------------------------------------------------------+ //| Preparing an array for one symbol | //+------------------------------------------------------------------+ void PrepareArrayForOneSymbol(int &strings_count,string message) { //--- Print the message to the log Print(message); //--- Array size strings_count=1; //--- Set the size of the array of symbols ArrayResize(temporary_symbols,strings_count); //--- Write the string with the current symbol name to the current index temporary_symbols[0]=_Symbol; }
これでパラメータ最適化目祖度検証を行うための準備がすべて整いました。検証に進む前に、レポートにもう一つ別のデータシリーズを追加します前にすべてのシンボルのバランスに加え、レポートファイルはパーセント表示のローカル極大値からのすべてのドローダウンを持ちました。ここではレポートは金額ベースのドローダウンもすべて対象とします。同時にレポートが作成される CreateSymbolBalanceReport() 関数を変更します。
以下はCreateSymbolBalanceReport() 関数コードです。
//+------------------------------------------------------------------+ //| Creating test report on deals in .csv format | //+------------------------------------------------------------------+ void CreateSymbolBalanceReport() { int file_handle =INVALID_HANDLE; // File handle string path =""; // File path //--- If an error occurred when creating/getting the folder, exit if((path=CreateInputParametersFolder())=="") return; //--- Create a file to write data in the common folder of the terminal file_handle=FileOpen(path+"\\LastTest.csv",FILE_CSV|FILE_WRITE|FILE_ANSI|FILE_COMMON); //--- If the handle is valid (file created/opened) if(file_handle>0) { int digits =0; // Number of decimal places in the price int deals_total =0; // Number of deals in the specified history ulong ticket =0; // Deal ticket double drawdown_max =0.0; // Drawdown double balance =0.0; // Balance string delimeter =","; // Delimiter string string_to_write =""; // To generate the string for writing static double percent_drawdown =0.0; // Drawdown expressed as percentage static double money_drawdown =0.0; // Drawdown in monetary terms //--- Generate the header string string headers="TIME,SYMBOL,DEAL TYPE,ENTRY TYPE,VOLUME," "PRICE,SWAP($),PROFIT($),DRAWDOWN(%),DRAWDOWN($),BALANCE"; //--- If more than one symbol is involved, modify the header string if(SYMBOLS_COUNT>1) { for(int s=0; s<SYMBOLS_COUNT; s++) StringAdd(headers,","+InputSymbols[s]); } //--- Write the report headers FileWrite(file_handle,headers); //--- Get the complete history HistorySelect(0,TimeCurrent()); //--- Get the number of deals deals_total=HistoryDealsTotal(); //--- Resize the array of balances according to the number of symbols ArrayResize(symbol_balance,SYMBOLS_COUNT); //--- Resize the array of deals for each symbol for(int s=0; s<SYMBOLS_COUNT; s++) ArrayResize(symbol_balance[s].balance,deals_total); //--- Iterate in a loop and write the data for(int i=0; i<deals_total; i++) { //--- Get the deal ticket ticket=HistoryDealGetTicket(i); //--- Get all the deal properties GetHistoryDealProperties(ticket,D_ALL); //--- Get the number of digits in the price digits=(int)SymbolInfoInteger(deal.symbol,SYMBOL_DIGITS); //--- Calculate the overall balance balance+=deal.profit+deal.swap+deal.commission; //--- Calculate the max drawdown from the local maximum TesterDrawdownMaximum(i,balance,percent_drawdown,money_drawdown); //--- Generate a string for writing using concatenation StringConcatenate(string_to_write, deal.time,delimeter, DealSymbolToString(deal.symbol),delimeter, DealTypeToString(deal.type),delimeter, DealEntryToString(deal.entry),delimeter, DealVolumeToString(deal.volume),delimeter, DealPriceToString(deal.price,digits),delimeter, DealSwapToString(deal.swap),delimeter, DealProfitToString(deal.symbol,deal.profit),delimeter, DrawdownToString(percent_drawdown),delimeter, DrawdownToString(money_drawdown),delimeter, DoubleToString(balance,2)); //--- If more than one symbol is involved, write their balance values if(SYMBOLS_COUNT>1) { //--- Iterate over all symbols for(int s=0; s<SYMBOLS_COUNT; s++) { //--- If the symbols are equal and the deal result is non-zero if(deal.symbol==InputSymbols[s] && deal.profit!=0) { //--- Display the deal in the balance for the corresponding symbol // Take into consideration swap and commission symbol_balance[s].balance[i]=symbol_balance[s].balance[i-1]+ deal.profit+ deal.swap+ deal.commission; //--- Add to the string StringAdd(string_to_write,","+DoubleToString(symbol_balance[s].balance[i],2)); } //--- Otherwise write the previous value else { //--- If the deal type is "Balance" (the first deal) if(deal.type==DEAL_TYPE_BALANCE) { //--- the balance is the same for all symbols symbol_balance[s].balance[i]=balance; StringAdd(string_to_write,","+DoubleToString(symbol_balance[s].balance[i],2)); } //--- Otherwise write the previous value to the current index else { symbol_balance[s].balance[i]=symbol_balance[s].balance[i-1]; StringAdd(string_to_write,","+DoubleToString(symbol_balance[s].balance[i],2)); } } } } //--- Write the generated string FileWrite(file_handle,string_to_write); //--- Mandatory zeroing out of the variable for the next string string_to_write=""; } //--- Close the file FileClose(file_handle); } //--- If the file could not be created/opened, print the appropriate message else Print("Error creating the file! Error: "+IntegerToString(GetLastError())+""); }
かつてはドローダウンを DrawdownMaximumToString() 関数d計算しました。今回は TesterDrawdownMaximum() 関数で行います。同時にドローダウン値は基本の DrawdownToString() 関数を用いて文字列に変換されます。
以下がTesterDrawdownMaximum() 関数のコードです。
//+------------------------------------------------------------------+ //| Returning the max drawdown from the local maximum | //+------------------------------------------------------------------+ void TesterDrawdownMaximum(int deal_number, double balance, double &percent_drawdown, double &money_drawdown) { ulong ticket =0; // Deal ticket string str =""; // The string to be displayed in the report //--- To calculate the local maximum and drawdown static double max =0.0; static double min =0.0; //--- If this is the first deal if(deal_number==0) { //--- There is no drawdown yet percent_drawdown =0.0; money_drawdown =0.0; //--- Set the initial point as the local maximum max=balance; min=balance; } else { //--- If the current balance is greater than in the memory, then... if(balance>max) { //--- Calculate the drawdown using the previous values: // in monetary terms money_drawdown=max-min; // expressed as percentage percent_drawdown=100-((min/max)*100); //--- Update the local maximum max=balance; min=balance; } //--- Otherwise else { //--- Return zero value of the drawdown money_drawdown=0.0; percent_drawdown=0.0; //--- Update the minimum min=fmin(min,balance); //--- If the deal ticket by its position in the list has been obtained, then... if((ticket=HistoryDealGetTicket(deal_number))>0) { //--- ...get the deal comment GetHistoryDealProperties(ticket,D_COMMENT); //--- Flag of the last deal static bool last_deal=false; //--- The last deal in the test can be identified by the "end of test" comment if(deal.comment=="end of test" && !last_deal) { //--- Set the flag last_deal=true; //--- Update the drawdown values: // in monetary terms money_drawdown=max-min; // expressed as percentage percent_drawdown+=100-((min/max)*100); } } } } }
以下は DrawdownToString() 関数のコードです。
//+------------------------------------------------------------------+ //| Converting drawdown to a string | //+------------------------------------------------------------------+ string DrawdownToString(double drawdown) { return((drawdown<=0) ? "" : DoubleToString(drawdown,2)); }
これで Expert Advisor の検証と結果分析の準備がすべて整いました。本稿の最初に既製のファイル例を確認しました。以下のように行います。2番目のセット(シンボルは3セットあります。EURUSD、AUDUSD、USDCHFです)でシンボルに対するパラメータの最適化を行い、それに続いて3番目のセット(合計シンボル7個)からすべてのシンボルを用いて検証を実行し、パラメータ最適化の対象となっていないシンボルのデータに対する結果を確認します。
パラメータの最適化と Expert Advisorの検証
「ストラテジーテスタ」は以下のように設定します。
図6 最適化のためのストラテジーテスタ設置
パラメータ最適化のため Expert Advisor 設定は以下です。
図7 パラメータ最適化のため Expert Advisor 設定
最適化は3個のシンボルを対象としており、各シンボルに対してポジションボリュームの増加が有効なので、ポジションオープンおよびポジションボリューム増加のために最小ロットを設定します。われわれの場合、値は 0.01です。
最適化後、最大リカバリーファクターの一番上の結果を選択し、ロットに対して VolumeIncrease パラメータを 0.1に設定します。結果は以下のとおりです。
図8 MetaTrader 5での検証結果
以下で Excel 2010に表示された結果を確認することができます。
図9 Excel 2010で表示される3シンボルについての検証結果
金額ベースのドローダウンは2番目(補助的)スケールに関してチャートの下側にグリーンでマークして表示されています。
Excel 2010でのチャート化の限界についても知る必要があります(仕様と制限の完全リストはマイクロソフトオフィスウェブサイトの Excel specifications and limits ページで確認することができます)。
図10 Excel 2010 dのチャート化仕様および制約
表では同時に 255 シンボルに対して検証を実行し結果をすべてチャートに表示していることを示しています。制約を受けるのはコンピュータリソースのみです。
現パラメータで3番目のセットから7シンボルについて検証を行い結果を確認します。
図11 Excel 2010での7シンボルに対する検証結果
対象の7シンボルで 6901 ディールを行います。チャートのデータは Excel 2010でひじょうに迅速に更新されます。
おわりに
紹介された方法はわれわれが使用したシンプルなトレーディング戦略でさえも良好な結果を示したことで注目に値すると思います。ここで最適化は7シンボル中3シンボルに対してのみ行われたことを念頭に置く必要があります。同時にすべてのシンボルに対して最適化を行うことで結果を改善することができます。ただまず第一にトレーディングシステムを改善する、またはいっそさまざまなトレーディングシステムのポートフォリオを持つことを目指す必要があります。この考えにはのちに戻ります。
それでおしまいです。われわれはマルチ通貨のトレーディング戦略の結果を調査するひじょうに便利なツールを入手しました。以下はみなさんの検討用 Expert Advisor のファイルのある zip ファイルでダウンロードできるものです。
ファイル抽出後、それを MetaTrader 5\MQL5\Experts ディレクトリのReduceOverfittingEAフォルダに入れます。そののち EventsSpy.mq5 インディケータはMetaTrader 5\MQL5\Indicatorsに入れる必要があります。SymbolsList.txt は MetaTrader 5\MQL5\Filesの下位にあります。