トレーダーの為のライフハック:1つのバックテストは良いが、4つは更に良い
最初のテストをする時にトレーダーには「4つのモードのうちのどれを使ったらいいのだろうか?」という一つの疑問が浮かんでくると思います。モードにはそれぞれにそれぞれの利点と特徴があるので、それを簡単にして、ワンクリックで一度に全てのモードを起動させましょう!この記事では、Win APIとちょっとしたマジックを使って、4つのテストチャートを一度に表示する方法をご紹介します。
目次
- 概論
- 1. 一般原則
- 2. 入力パラメータ
- 3. 下位ターミナルのAppDataフォルダと設定フォルダの比較
- 3.1. 秘密1
- 3.2. FindFirstFileW、FindNextFileW
- 3.3. FindFirstFileWとFindNextFileWの使用例
- 3.4. ターミナルフォルダの中を見てみましょう
- 3.5. CopyFileW
- 3.6. "origin.txt"ファイルを使ってみましょう
- 3.7. 仕上げ
- 4. テスト用のエキスパートアドバイザの選択
- 4.1. GetOpenFileName
- 4.2. システムダイアログ『ファイルを開く』を使ってエキスパートアドバイザを選択しましょう
- 4.3. 設定iniファイル
- 4.4. 秘密2
- 4.5. ターミナル(幅、高さ)のサイズを設定します。ファイルの途中での行の挿入
- 5. 下位端末のテストへの起動
- 6. 考えられるエラー
- まとめ
概論
この記事の主な目的は、1つのターミナルから(これをマスターターミナルとします)4つのターミナル(これらを下位ターミナルとし、それぞれに#1、#2、#3、#4と番号をふります)に一度にエキスパートアドバイザの単一のテスト(最適化ではなく、テストです!)を実行することです。この時、下位ターミナルでのストラテジーテスターは様々なティック生成モードで実行されます。
- ターミナル#1-『リアルティックに基づいた全てのティック』
- ターミナル#2-『全ティック』
- ターミナル#3-『1分足OHLC』
- ターミナル#4-『始値のみ』
重要な制限:
- マスターターミナルは/portableキーなしで起動する必要があります。
- 最低でも5つのインストールしたMetaTrader 5が必要です。
- マスターターミナルの取引口座は、マスター口座とし、下位ターミナルのそれぞれに最低でも一回起動する必要があります。これはこの記事のエキスパートアドバイザが、パスワードをiniファイルを介して取引口座から下位ターミナルに送信しないためです。パスワードの代わりにストラテジーテスターを起動する必要がある取引口座の番号だけが送られ、この番号は常にマスター口座の番号と一致します。
これはエキスパートアドバイザのテストが、同じ取引口座で様々なティック生成モードで実行する必要があるので理に適っています。 - エキスパートアドバイザの起動前に、最大限にコアプロセッサを軽くしてください(オンラインゲームやメディアプレイヤー、その他のリソースを消費するプログラムをオフにする)。そうでない場合、プロセッサコアの一つがブロックされ、このコアでテストが実行されないことがあります。
1. 一般原則
多すぎるのは良くない(ポーランドの諺)。
私はいつもソフトウェアの標準機能を使用することを好んでいます。MetaTrader 5取引ターミナルに対するルールは次のようなものです。『絶対に/portableキーでターミナルを起動しない、絶対にOSでユーザーアカウントの制御(UAC)を無効にしない。』これに基づき、エキスパートアドバイザのファイルの使用は、AppDataフォルダで実行されます。
記事に引用した全てのスクリーンショットは、最新かつ完全にライセンスされたものであるので、Windows 10でデモンストレーションされます。ここではこの記事に書かれた全コードが検証されます。
検証するエキスパートアドバイザは、MQL5の一連の機能を伴い、dllを幅広く使用しています。
図1. 依存関係
特に、このようなWindows API関数の呼び出しが使用されています。
- CopyFileW — MQL5の『サンドボックス』から『サンドボックス』にファイルをコピーします。
- FindClose — 検索ハンドルを閉じます。
- FindFirstFileW —指定したファイル名と一致するファイルディレクトリまたはサブディレクトリを検索します。
- FindNextFileW — FindFirstFile関数の前の呼び出しからファイルの検索を続行します。
- GetOpenFileNameW — ファイルを開くシステムダイアログを呼び出します。
- ShellExecuteW — 下位ターミナルの起動の為に使用します。
図2. ファイルを開きます
①と②のアイコンについては4.2で詳説します。システムダイアログ『ファイルを開く』を使ってエキスパートアドバイザを選択します。
2. 入力パラメータ
図3. 入力パラメータ
folder of the MetaTrader#Х installation — 下位ターミナルを設定するパスです。mq5へのパスを設定する時にダブルスラッシュを登録する必要があります。またパスの最後に二重のバックスラッシュを置くことが重要です。
//--- input parameters input string ExtInstallationPathTerminal_1="C:\\Program Files\\MetaTrader 5 1\\"; // folder of the MetaTrader#1 installation input string ExtInstallationPathTerminal_2="D:\\MetaTrader 5 2\\"; // folder of the MetaTrader#2 installation input string ExtInstallationPathTerminal_3="D:\\MetaTrader 5 3\\"; // folder of the MetaTrader#3 installation input string ExtInstallationPathTerminal_4="D:\\MetaTrader 5 4\\"; // folder of the MetaTrader#4 installation input string ExtTerminalName="terminal64.exe"; // correct name of the file of the terminal
terminal64.exeのターミナル名は、64ビットOSの為に与えられています。
AppDataフォルダ内での設定フォルダとデータフォルダの間の接続
通常の方法でまたは/portableキーを使ってターミナルを起動させると、ターミナルはTERMINAL_DATA_PATH変数の為の様々なパスを提供します。C:\Program Files\MetaTrader 5 1フォルダへインストールされたマスターターミナルの例でこの状況を見ていきましょう。
マスターターミナルを/portableキーで起動すると、このターミナルからMQLはこのような結果を出します。
TERMINAL_PATH = C:\Program Files\MetaTrader 5 TERMINAL_DATA_PATH = C:\Program Files\MetaTrader 5 TERMINAL_COMMONDATA_PATH = C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common
/portableキーなしのこのターミナルの結果はこのようになります。
TERMINAL_PATH = C:\Program Files\MetaTrader 5 TERMINAL_DATA_PATH = C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962 TERMINAL_COMMONDATA_PATH = C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common
しかし、これは現在のターミナルからパラメータを取得する場合にのみ必要になるものです。では、エキスパートアドバイザのテストを実行する下位ターミナルの場合はどうしたらいいのでしょうか?下位ターミナルの設定パスとそれらのデータディレクトリのパスを連結させるにはどうしたらいいのでしょうか?
ここで、何故AppDataフォルダでデータディレクトリへのパスを知ることが重要なのかを明らかにしたいと思います(ヘルプからの引用)。
Program FilesにインストールされたプログラムのデフォルトのMS Windows Vistaから始めると、設定ディレクトリにデータを格納することはできません。全てのデータは個別のWindowsユーザデータディレクトリに格納する必要があります。
言い換えれば、私達のエキスパートアドバイザは自由にフォルダ内のファイルを作成し変更することができます(C:\Users\ユーザー名\AppData\Roaming\MetaQuotes\Terminal\ターミナル識別子\MQL5\Files)。ここでの『ターミナル識別子』とは、マスターターミナルの識別子です。
3. 下位ターミナルのAppDataフォルダと設定フォルダの比較
エキスパートアドバイザは、構成ファイルを指定することで下位ターミナルの起動を実行します。また、各ターミナルにはそれぞれの構成ファイルが使用されます。各構成ファイルには、ターミナルの起動時にすぐに指定したエキスパートアドバイザのテストを開始する為の指示があります。対応するコマンドは構成ファイルの[Tester]セクションに配置されます。
... [Tester] Expert=test ...
お分かりのように、パスは指定されず、つまりテストするエキスパートアドバイザは、専らMQL5の『サンドボックス』に配置されるということです。下位ターミナル1の例では2つのパスがあり得ます。
- C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\MQL5\Experts
- もしくはC:\Program Files\MetaTrader 5 1\MQL5\Experts
バージョン№2を、Windows Vistaから始めるとProgram Filesフォルダに書き込むことができないというセキュリティポリシーに従い排除します。バージョン№1が残りますが、これはつまり全ての下位ターミナルの為にインストールフォルダとAppDataフォルダを比較する必要があります。
各データディレクトリには『origin.txt』ファイルがあります。下位ターミナル1の例:
図4. origin.txtファイル
とorigin.txtファイルの内容:
C:\Program Files\MetaTrader 5 1
この記録はファイルの中で『C:\Program Files\MetaTrader 5 1』で設定されたターミナルが『C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962』フォルダを作成するように指定します。
3.2. FindFirstFileW, FindNextFileW
FindFirstFileW — ファイルや特定の名前(または特殊文字を使用している場合、名前の一部)と一致する名前を持つサブディレクトリの為のディレクトリの検索を行います。
HANDLE FindFirstFileW(
string lpFileName, //
WIN32_FIND_DATA &lpFindFileData //
);
パラメータ
lpFileName
[in]は、アスタリスク(*)または疑問符(?)などのワイルドカードを含むファイル名のパスまたはディレクトリです。
lpFindFileData
[in][out]は、検出したファイルまたはディレクトリについての情報を取得するWIN32_FIND_DATA構造体へのポインタです。
戻り値
関数が正常に動作した場合、FindNextFileまたはFindCloseの次の呼び出しで使用される検索ハンドルが戻り値となり、lpFindFileDataパラメータは最初のファイルまたは検出したフォルダについての情報を含んでいます。
関数が失敗したまたはlpFileNameパラメータで検索文字列からファイルを見つけることができなかった場合、INVALID_HANDLE_VALUE値を返し、含まれるlpFindFileDataは不明となります。エラーについての追加情報を取得するには、GetLastError関数を呼びだしてください。
関数が動作しない場合は、対応するファイルが見つからなかった為、GetLastError関数はERROR_FILE_NOT_FOUNDを返します。
FindNextFileW — 前のFindFirstFile、FindFirstFileEx、またはFindFirstFileTransacted関数の呼び出しからファイルの検索を続けます。
bool FindNextFileW(
HANDLE FindFile, //
WIN32_FIND_DATA &lpFindFileData //
);
パラメータ
FindFile
[in]は前のFindFirstFileまたはFindFirstFileEx関数の呼び出しで返される検索ハンドルです。
lpFindFileData
[in][out]は、検出したファイルまたはディレクトリについての情報を取得するWIN32_FIND_DATA構造体へのポインタです。
戻り値
関数が正常に動作した場合、戻り値はゼロではなく、lpFindFileDataパラメータは次のファイルもしくは検出したディレクトリについての情報を含みます。
関数が失敗した場合、戻り値はゼロとなり、lpFindFileDataの内容は不明となります。エラーについての追加情報を取得するにはGetLastError関数を呼び出してください。
関数が失敗した場合、それ以上ファイルを見つけられなかった為、GetLastError関数はERROR_NO_MORE_FILESを返します。
FindFirstFileWとFindNextFileW関数(コードは添付のListingFilesDirectory.mqhファイルから引用しました):
#define MAX_PATH 0x00000104 // #define FILE_ATTRIBUTE_DIRECTORY 0x00000010 // #define ERROR_NO_MORE_FILES 0x00000012 //there are no more files #define ERROR_FILE_NOT_FOUND 0x00000002 //the system cannot find the file specified //+------------------------------------------------------------------+ //| FILETIME structure | //+------------------------------------------------------------------+ struct FILETIME { uint dwLowDateTime; uint dwHighDateTime; }; //+------------------------------------------------------------------+ //| WIN32_FIND_DATA structure | //+------------------------------------------------------------------+ struct WIN32_FIND_DATA { uint dwFileAttributes; FILETIME ftCreationTime; FILETIME ftLastAccessTime; FILETIME ftLastWriteTime; uint nFileSizeHigh; uint nFileSizeLow; uint dwReserved0; uint dwReserved1; ushort cFileName[MAX_PATH]; ushort cAlternateFileName[14]; }; #ifndef _IsX64 #define HANDLE long #else #define HANDLE int #endif #import "kernel32.dll" int GetLastError(); HANDLE FindFirstFileW(string lpFileName,WIN32_FIND_DATA &lpFindFileData); bool FindNextFileW(HANDLE FindFile,WIN32_FIND_DATA &lpFindFileData); bool FindClose(HANDLE &hFindFile); #import
3.3. FindFirstFileWとFindNextFileWの使用例
『ListingFilesDirectory.mq5』スクリプト — 実質的にエキスパートアドバイザの動作コードの完全なコピーであり、同時に一つの例です。言い換えれば、このコードは最大限に現実に近いものです。
課題:TERMINAL_COMMONDATA_PATH — 『Common』のパスの為の全てのフォルダの名前を取得すること。
私のコンピュータの例ではTERMINAL_COMMONDATA_PATHは『C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common』値を返します。つまり、この『Common』パスから切り離すと、必要なパス『C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\』を取得することができます。
図5. Find First
通常、全てのファイルの検索には『*.*』検索マスクが適用されます。つまり、次の文字列を持つ2つの操作を行う必要があります。『Common』の文字を排除し、それから『*.*』マスクを追加します。
string common_data_path=TerminalInfoString(TERMINAL_COMMONDATA_PATH); int pos=StringFind(common_data_path,"Common",0); if(pos!=-1) { common_data_path=StringSubstr(common_data_path,0,pos-1); } else return; string path_addition="\\*.*"; string mask_path=common_data_path+path_addition; printf("mask_path=%s",mask_path);
最終的にどのようになったか確認してみましょう。その為にブレークポイントを設定し、デバッグを起動します。
図6. デバッグ
結果:
mask_path=C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.*
今のところ全て正しく、全てのファイルと『C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\』ディレクトリ内のフォルダの検索マスクは用意できました。
次に進み、『hFind』検索ハンドル(私の場合これを習慣で呼び出しました)を初期化し、Win APIのFindFirstFileW関数を呼び出します。
printf("mask_path=%s",mask_path); hFind=-100; hFind=FindFirstFileW(mask_path,ffd); if(hFind==INVALID_HANDLE) { PrintFormat("Failed FindFirstFile (hFind) with error: %x",kernel32::GetLastError()); return; } // List all the files in the directory with some info about them
FindFirstFileWの呼び出しが失敗に終わった場合、『hFind』検索ハンドルは『INVALID_HANDLE』と等しくなり、スクリプトが終了します。
FindFirstFileWの呼び出しに失敗した場合、do whileサイクルを作成します(フォルダまたはファイル名を取得し、サイクルの最後でWin APIのFindNextFileW関数を呼び出すもの)。
// List all the files in the directory with some info about them PrintFormat("hFind=%d",hFind); bool rezult=0; do { string name=""; for(int i=0;i<MAX_PATH;i++) { name+=ShortToString(ffd.cFileName[i]); } Print("\"",name,"\", File Attribute Constants (dec): ",ffd.dwFileAttributes); //--- ArrayInitialize(ffd.cFileName,0); ArrayInitialize(ffd.cAlternateFileName,0); ffd.dwFileAttributes=-100; ResetLastError(); rezult=FindNextFileW(hFind,ffd); } while(rezult!=0); if(kernel32::GetLastError()!=ERROR_NO_MORE_FILES) PrintFormat("Failed FindNextFileW (hFind) with error: %x",kernel32::GetLastError()); FindClose(hFind);
Win APIのFindNextFileW関数の呼び出しがゼロの値を返すまでdo whileサイクルは継続されます。Win APIのFindNextFileWERROR_NO_MORE_FILES関数の呼び出しがゼロを返し、エラーが『ERROR_NO_MORE_FILES』と等しくない場合、重大なエラーが発生したということです。
スクリプトの終わりで検索ハンドルを閉じます。
『ListingFilesDirectory.mq5』スクリプトの動作結果:
mask_path=C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.* hFind=-847293552 ".", File Attribute Constants (dec): 16 "..", File Attribute Constants (dec): 16 "038C9E8FAFF9EA373522ECC6D5159962", File Attribute Constants (dec): 16 "0C46DDCEB43080B0EC647E0C66170465", File Attribute Constants (dec): 16 "2A6A33B25AA0984C6AB9D7F28665B88E", File Attribute Constants (dec): 16 "50CA3DFB510CC5A8F28B48D1BF2A5702", File Attribute Constants (dec): 16 "BC11041F9347CD71C5F8926F53AA908A", File Attribute Constants (dec): 16 "Common", File Attribute Constants (dec): 16 "Community", File Attribute Constants (dec): 16 "D0E8209F77C8CF37AD8BF550E51FF075", File Attribute Constants (dec): 16 "D3852169A6E781B7F35488A051432620", File Attribute Constants (dec): 16 "EE57F715BA53F2E183D6731C9376293D", File Attribute Constants (dec): 16 "Help", File Attribute Constants (dec): 16
上記の例は『C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\』フォルダでのトップレベルの動作を示しました。しかし、私達は項目3.1の事を覚えています。秘密1は全てのサブフォルダの中を見る必要があるということに基づいています。
これの為に、サブファイル内でWin APIのFindFirstFileW関数の為にこのような一次検索のマスクを使用する必要があるので、2段階検索を作成します。
『C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\』 + 上位レベルの検出されたフォルダ名 + 『origin.txt』。
このように、サブフォルダ内でのFindFirstFileWの一時検索は、『origin.txt』ファイルだけを探します。
FindDataPath()関数の完全なリストは以下の通りです。
//+------------------------------------------------------------------+ //| Find and read the origin.txt | //+------------------------------------------------------------------+ void FindDataPath(string &array[][2]) { //--- WIN32_FIND_DATA ffd; HANDLE hFirstFind_0,hFirstFind_1; ArrayInitialize(ffd.cFileName,0); ArrayInitialize(ffd.cAlternateFileName,0); //+------------------------------------------------------------------+ //| Get common path for all of the terminals installed on a computer.| //| The common path on my computer: | //| C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common | //+------------------------------------------------------------------+ string common_data_path=TerminalInfoString(TERMINAL_COMMONDATA_PATH); int pos=StringFind(common_data_path,"Common",0); if(pos!=-1) { //+------------------------------------------------------------------+ //| Cuts "Common" ... and we get: | //| C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal | //+------------------------------------------------------------------+ common_data_path=StringSubstr(common_data_path,0,pos-1); } else return; //--- stage Search №0. string filter_0=common_data_path+"\\*.*"; // filter_0==C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.* hFirstFind_0=FindFirstFileW(filter_0,ffd); //--- string str_handle=""; if(hFirstFind_0==INVALID_HANDLE) str_handle="INVALID_HANDLE"; else str_handle=IntegerToString(hFirstFind_0); Print("filter_0: \"",filter_0,"\", handle hFirstFind_0: ",str_handle); //--- if(hFirstFind_0==INVALID_HANDLE) { PrintFormat("Failed FindFirstFile (hFirstFind_0) with error: %x",kernel32::GetLastError()); return; } //--- list all the files in the directory with some info about them bool rezult=0; do { if((ffd.dwFileAttributes &FILE_ATTRIBUTE_DIRECTORY)==FILE_ATTRIBUTE_DIRECTORY) { string name_0=""; for(int i=0;i<MAX_PATH;i++) { name_0+=ShortToString(ffd.cFileName[i]); } if(name_0!="." && name_0!="..") { ArrayInitialize(ffd.cFileName,0); ArrayInitialize(ffd.cAlternateFileName,0); //--- stage Search №1. search origin.txt file in the folder string filter_1=common_data_path+"\\"+name_0+"\\origin.txt"; ResetLastError(); hFirstFind_1=FindFirstFileW(filter_1,ffd); //--- if(hFirstFind_1==INVALID_HANDLE) str_handle="INVALID_HANDLE"; else str_handle=IntegerToString(hFirstFind_1); Print(" filter_1: \"",filter_1,"\", handle hFirstFind_1: ",str_handle); //--- if(hFirstFind_1==INVALID_HANDLE) { if(kernel32::GetLastError()!=ERROR_FILE_NOT_FOUND) { PrintFormat("Failed FindFirstFile (hFirstFind_1) with error: %x",kernel32::GetLastError()); break; } FindClose(hFirstFind_1); ArrayInitialize(ffd.cFileName,0); ArrayInitialize(ffd.cAlternateFileName,0); ResetLastError(); rezult=FindNextFileW(hFirstFind_0,ffd); continue; } //--- origin.txt file in this folder is found bool rezultTwo=0; string name_1=""; for(int i=0;i<MAX_PATH;i++) { name_1+=ShortToString(ffd.cFileName[i]); } string origin=CopiedAndReadFile(filter_1); //--- receiving a string of the file found origin.txt if(origin!=NULL) { //--- write a string into an array int size=ArrayRange(array,0); ArrayResize(array,size+1,0); array[size][0]=common_data_path+"\\"+name_0; //value array[][0]==C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962 array[size][1]=origin; //value array[][1]==C:\Program Files\MetaTrader 5 1\ } FindClose(hFirstFind_1); } } ArrayInitialize(ffd.cFileName,0); ArrayInitialize(ffd.cAlternateFileName,0); ResetLastError(); rezult=FindNextFileW(hFirstFind_0,ffd); } while(rezult!=0); //if(hFirstFind_1==INVALID_HANDLE), we appear here if(kernel32::GetLastError()!=ERROR_NO_MORE_FILES) PrintFormat("Failed FindNextFileW (hFirstFind_0) with error: %x",kernel32::GetLastError()); else Print("filter_0: \"",filter_0,"\", handle hFirstFind_0: ",hFirstFind_0,", NO_MORE_FILES"); FindClose(hFirstFind_0); }
FindDataPath()関数はこのような情報を印字します。
filter_0: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.*", handle hFirstFind_0: 1901014212592 filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\origin.txt", handle hFirstFind_1: 1901014213744 filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\0C46DDCEB43080B0EC647E0C66170465\origin.txt", handle hFirstFind_1: 1901014213840 filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\2A6A33B25AA0984C6AB9D7F28665B88E\origin.txt", handle hFirstFind_1: INVALID_HANDLE filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\50CA3DFB510CC5A8F28B48D1BF2A5702\origin.txt", handle hFirstFind_1: 1901014218448 filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\BC11041F9347CD71C5F8926F53AA908A\origin.txt", handle hFirstFind_1: 1901014213936 filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common\origin.txt", handle hFirstFind_1: INVALID_HANDLE filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Community\origin.txt", handle hFirstFind_1: INVALID_HANDLE filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\origin.txt", handle hFirstFind_1: 1901014216720 filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D3852169A6E781B7F35488A051432620\origin.txt", handle hFirstFind_1: 1901014217104 filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\EE57F715BA53F2E183D6731C9376293D\origin.txt", handle hFirstFind_1: 1901014218640 filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Help\origin.txt", handle hFirstFind_1: INVALID_HANDLE filter_0: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.*", handle hFirstFind_0: 1901014212592, NO_MORE_FILES
印字の最初の文字列の説明:最初に一次検索の『filter_0』フィルターを作成し(フィルターは『C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.*』と同等)、 1901014212592と等しい『hFirstFind_0』一次検索ハンドルを取得します。『hFirstFind_0』値が『INVALID_HANDLE』と等しくない場合、Win APIのFindFirstFileW(filter_0,ffd)関数に引き渡された一次検索のfilter_0フィルターは正しいということです。FindFirstFileW(filter_0,ffd)の呼び出しが正常に行われると、最初に得られたフォルダ名 (このフォルダは『038C9E8FAFF9EA373522ECC6D5159962』)を取得します。
次に038C9E8FAFF9EA373522ECC6D5159962フォルダ内の『origin.txt』ファイルの検索を実行する必要があります。これを行う為に、マスクフィルターを作成します。例えば、038C9E8FAFF9EA373522ECC6D5159962フォルダには、このマスクはこのようなものになります。:『C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\origin.txt』。『hFirstFind_1』ハンドルが『INVALID_HANDLE』と等しくなる場合、指定したフォルダ(038C9E8FAFF9EA373522ECC6D5159962) に必要なファイル(origin.txt)があるということです。
印字されたものを見ると、サブフォルダ内の一次検索は時々『INVALID_HANDLE』を返すということがよくわかります。これはつまり、指定したフォルダの中に『origin.txt』ファイルがないということです。
『origin.txt』ファイルがサブフォルダ内で検出されたときに何をするかに焦点を当ててみましょう。
CopyFileW — 既存のファイルを新しいファイルにコピーします。
bool CopyFileW( string lpExistingFileName, // string lpNewFileName, // bool bFailIfExists // );
パラメータ
lpExistingFileName
[in]は既存のファイルの名前です。
ここでは強制的に名前の長さが制限(MAX_PATH)され、私達の例ではこれは十分なものです。
lpExistingFileNameという名前のファイルがない場合、関数は正常に動作せず、 GetLastErrorはERROR_FILE_NOT_FOUNDを返します。
lpNewFileName
bFailIfExists[in]は新しいファイルの名前です。
ここでは強制的に名前の長さが制限(MAX_PATH)され、私達の例にはこれは十分なものです。
[in]このパラメータがTRUEでlpNewFileNameに指定した新しいファイルがある場合、関数は正常に動作しません。このパラメータがFALSEで新しいファイルがある場合、関数は既存のファイルを上書きし、正常に動作を完了します。
戻り値
関数が正常に動作した場合、戻り値はゼロと等しくなりません。
関数が正常に動作しなかった場合、戻り値はゼロと等しくなります。エラーについての追加情報を取得するには、GetLastError関数を呼び出してください。
Win APIのCopyFileW関数の宣言の例(コードは添付ファイルListingFilesDirectory.mqhから引用しました):
#import "kernel32.dll" int GetLastError(); bool CopyFileW(string lpExistingFileName,string lpNewFileName,bool bFailIfExists); #import
3.6. "origin.txt"ファイルを使ってみましょう
ListingFilesDirectory.mqh::CopiedAndReadFile関数の動作の説明(string full_file_name)。
入力パラメータの中で関数はサブフォルダのうちの一つで見つかった『origin.txt』ファイルの完全な名前を取得します。これはこのようなパスになります:『C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\origin.txt』。『origin.txt』ファイルを開き、その内容の読み込みはMQL5を使用して行いますが、これはファイルは『サンドボックス』にある必要があるということです。つまり、サブフォルダから『origin.txt』ファイルをサンドボックスにコピーする必要があります(今回の場合、全てのターミナルの共有ファイルの『サンドボックス』)。このようなコピーはWin APIのCopyFileW関数の呼び出しを使って実行します。
『new_path』変数にサンドファイルの『origin.txt』ファイルへのパスを記述します。
//+------------------------------------------------------------------+ //| Copying to the Common Data Folder | //| for all client terminals ***\Terminal\Common\Files | //+------------------------------------------------------------------+ string CopiedAndReadFile(string full_file_name) { string new_path=TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\Files\\origin.txt"; // => new_path==C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common\Files\origin.txt //--- Win API
そしてfalseと同等の3つのパラメータを持つWin APIのCopyFileW関数を呼び出します。サンドボックスの『origin.txt』ファイルの上書きを許可します。
//--- Win API if(!CopyFileW(full_file_name,new_path,false)) { Print("Error CopyFile ",full_file_name," to ",new_path); return(NULL); } //--- open the file using MQL5
MQL5ツールで読み込みの為に『origin.txt』ファイルを開きますが、ファイルが共有ファイルのフォルダにある為、FILE_COMMONフラグを指定するのを忘れないようにしましょう。
//--- open the file using MQL5 string str; ResetLastError(); int file_handle=FileOpen("origin.txt",FILE_READ|FILE_TXT|FILE_COMMON); if(file_handle!=INVALID_HANDLE) { //--- read a string using the MQL5 str=FileReadString(file_handle,-1)+"\\"; //--- close the file using the MQL5 FileClose(file_handle); } else { PrintFormat("File %s open failed , MQL5 error=%d","origin.txt",GetLastError()); return(NULL); } return(str); }
読み込みは一度だけ、一行だけ行い、その最後に『\\』を加え、取得した結果を返します。
エキスパートアドバイザの入力パラメータでは、4つのターミナルの為のフォルダへのパスが設定されています。
//--- input parameters input string ExtInstallationPathTerminal_1="C:\\Program Files\\MetaTrader 5 1\\"; // folder of the MetaTrader#1 installation input string ExtInstallationPathTerminal_2="D:\\MetaTrader 5 2\\"; // folder of the MetaTrader#2 installation input string ExtInstallationPathTerminal_3="D:\\MetaTrader 5 3\\"; // folder of the MetaTrader#3 installation input string ExtInstallationPathTerminal_4="D:\\MetaTrader 5 4\\"; // folder of the MetaTrader#4 installation
これらのパスはハードコードで書かれており、これらはターミナルの設定フォルダを正確に指定する必要があります。
また、グローバルスコープでさらに4つの文字列変数と1つの配列が宣言されています。
string slaveTerminalDataPath1=NULL; // the path to the Data Folder of the terminal #1 string slaveTerminalDataPath2=NULL; // the path to the Data Folder of the terminal #2 string slaveTerminalDataPath3=NULL; // the path to the Data Folder of the terminal #3 string slaveTerminalDataPath4=NULL; // the path to the Data Folder of the terminal #4 //--- string arr_path[][2];
これらの変数にはAppData内のターミナルフォルダへのパスを格納する必要があり、2次元配列がその役に立ちます。ここで、下位ターミナルの設定フォルダとAppDataのそれらのフォルダを比較する概要を書くことができます。
GetStatsFromAccounts_EA.mq5::OnInit() >вызов> GetStatsFromAccounts_EA.mq5::FindDataFolders(arr_path)
>вызов> ListingFilesDirectory.mqh::FindDataPath(string &array[][2]) >вызов> CopiedAndReadFile(string
full_file_name)
string origin=CopiedAndReadFile(filter_1); //--- receiving a string of the file found origin.txt if(origin!=NULL) { //--- write a string into an array int size=ArrayRange(array,0); ArrayResize(array,size+1,0); array[size][0]=common_data_path+"\\"+name_0; //value array[][0]==C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962 array[size][1]=origin; //value array[][1]==C:\Program Files\MetaTrader 5 1\ } FindClose(hFirstFind_1);
『origin.txt』ファイルがターミナルのサブフォルダで検出された時、ListingFilesDirectory.mqh::FindDataPath(string &array[][2])関数で、CopiedAndReadFile(string full_file_name)関数が呼び出され、その呼び出しの後に2次元配列への記録が行われます。『0』の配列の次元ではAppDataのターミナルのフォルダへのパスが書かれ、『1』の配列の次元では設定フォルダへのパスが書かれます(このパスは検出した『origin.txt』ファイルから取得します)。
>制御を返す>GetStatsFromAccounts_EA.mq5::FindDataFolders(arr_path):
ここでは、簡単に2次元配列をクロールして、slaveTerminalDataPath1、slaveTerminalDataPath2、slaveTerminalDataPath3、slaveTerminalDataPath4変数を埋めます。
FindDataPath(array); for(int i=0;i<ArrayRange(array,0);i++) { //Print("array[",i,"][0]: ",array[i][0]); //Print("array[",i,"][1]: ",array[i][1]); if(StringCompare(ExtInstallationPathTerminal_1,array[i][1],true)==0) slaveTerminalDataPath1=array[i][0]; if(StringCompare(ExtInstallationPathTerminal_2,array[i][1],true)==0) slaveTerminalDataPath2=array[i][0]; if(StringCompare(ExtInstallationPathTerminal_3,array[i][1],true)==0) slaveTerminalDataPath3=array[i][0]; if(StringCompare(ExtInstallationPathTerminal_4,array[i][1],true)==0) slaveTerminalDataPath4=array[i][0]; } if(slaveTerminalDataPath1==NULL || slaveTerminalDataPath2==NULL || slaveTerminalDataPath3==NULL || slaveTerminalDataPath4==NULL) { Print("slaveTerminalDataPath1 ",slaveTerminalDataPath1,", slaveTerminalDataPath2 ",slaveTerminalDataPath2); Print("slaveTerminalDataPath3 ",slaveTerminalDataPath3,", slaveTerminalDataPath4 ",slaveTerminalDataPath4); return(false); }
この段階まで来たということは、エキスパートアドバイザがターミナルの設定とAppDataのそれらのフォルダのパスを比較したということです。AppDataでターミナルのフォルダへのパスが1つでも見つからない場合(つまりNULLと等しくなる)、全てのパスは最後の文字列で印字され、エキスパートアドバイザはエラーで終了します。
4. テスト用のエキスパートアドバイザの選択
4つの下位ターミナルの起動前に初めにテストするエキスパートアドバイザのファイルを選択する必要があります。このエキスパートアドバイザは事前にコンパイルされ、マスターターミナルのデータディレクトリに配置されたものである必要があります。
GetOpenFileName — ファイルのセットまたはファイル、フォルダ、ディスクを開くためのダイアログウィンドウ『開く』を作成するものです。ダイアログウィンドウ『開く』の宣言と実装は、添付ファイルGetOpenFileNameW.mqhで紹介されています。
4.2. システムダイアログ『ファイルを開く』を使ってエキスパートアドバイザを選択しましょう
システムダイアログウィンドウ『開く』はエキスパートアドバイザのOnInit()から呼び出されます。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { ArrayFree(arr_path); if(!FindDataFolders(arr_path)) return(INIT_SUCCEEDED); //--- if(MessageBox("Ready?",NULL,MB_YESNO)==IDYES) { expert_name=OpenFileName(); if(expert_name==NULL) return(INIT_FAILED); //--- editing and copying of the ini-file in the folder of the terminals
GetOpenFileNameW.mqh::OpenFileName(void)の呼び出しが行われる場所
//+------------------------------------------------------------------+ //| Creates an Open dialog box | //+------------------------------------------------------------------+ string OpenFileName(void) { string path=NULL; string filter=NULL; if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian") filter=『コンパイルされたコード』; else filter="Compiled code"; if(GetOpenFileName(path,filter+"\0*.ex5\0",TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Experts\\","Select source file")) return(path); else { PrintFormat("Failed with error: %x",kernel32::GetLastError()); return(NULL); } }
Win APIのGetOpenFileName関数の呼び出しに成功した際の『path』変数は、選択したファイルのフルネームを含み、このようになります。:『C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\MQL5\Experts\Examples\MACD\MACD Sample.ex5』。
『filter』変数は画像2のテキスト①を担当しています2. 『\0*.ex5\0』文字列はファイルの種類のフィルタを担当しています(画像2の②)。『TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Experts\\"』文字列は、システムダイアログ『開く』で開かれるフォルダへのパスを指定します。
コマンド文字列(またはWin APIを使用して)からエキスパートアドバイザのテストにターミナルを起動させるためには、[Tester]セクションと必要な指示がある設定iniファイルを持つ必要があります。
[Tester] Expert=test //テストに起動する必要があるエキスパートアドバイザのファイル名 Symbol=EURUSD //テストの主要通貨ペアとして使用される通貨ペア名 Period=H1 //テストのチャート期間 Deposit=10000 //テストの為の初期証拠金の額 Model=4 //ティックの生成モード Optimization=0 //最適化のオン/オフとその種類の指定 FromDate=2016.01.22 //テストの開始日 ToDate=2016.06.06 //テストの終了日 Report=TesterReport //テスト結果のレポートが保存されるファイル名 ReplaceReport=1 //レポートファイルの上書きの許可/禁止 UseLocal=1 //テストの為のローカルエージェントを使用する機能のオン/オフ Port=3000 //テストのエージェントのポート Visual=0 //ビジュアルモードでのテストのオン/オフ ShutdownTerminal=0 //テスト実行による取引プラットフォームのオン/オフ
先取りしていうと、この[Tester]セクションは自分でファイルへ追加します。
ベースにはマスターターミナルのiniファイルが引用されています。このファイル(common.ini)は、『config』フォルダのターミナルのデータディレクトリに配置されています。私のターミナルではこのようになります:『C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\config\common.ini』。
iniファイルを使用した構図はこのようになります。
- マスターターミナルの『common.ini』への完全なパスを取得します。完全なパスは、このような文字列です:
『C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\config\common.ini』。(MQL5) - サンドボックス『\Files』iniファイルへの新しいパスを取得します。新しいパスは、これはこのような文字列です:
マスターターミナルの『C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\MQL5\Files\myconfiguration.ini』。(MQL5) - 『myconfiguration.ini』の『common.ini』ファイルのコピー。(WIn APIのCopyFileW関数)。
- 『myconfiguration.ini』ファイルの編集。(MQL5)
- 下位ターミナルのサンドボックスのiniファイルへの新しいパスを取得します。この文字列はこのようになります(私の下位ターミナル№1の例)
『C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\MQL5\Files\myconfiguration.ini』。(MQL5) - 編集された『myconfiguration.ini』のiniファイルをマスターターミナルのサンドボックスから下位ターミナルのサンドボックスにコピーします。(WIn APIのCopyFileW関数)
- マスターターミナルのサンドボックスから『myconfiguration.ini』ファイルを削除します。(MQL5)
各下位ターミナルの為にこの構図をテストする必要があります。ここには最適化の為の場所がありますが、このプロセスをこの記事で記述する予定はありませんでした。
設定iniファイルの編集には、テストの為にすでにエキスパートアドバイザGetStatsFromAccounts_EA.mq5::OnInit()が選択された後に取り掛かります。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { ArrayFree(arr_path); if(!FindDataFolders(arr_path)) return(INIT_SUCCEEDED); //--- if(MessageBox("Ready?",NULL,MB_YESNO)==IDYES) { expert_name=OpenFileName(); if(expert_name==NULL) return(INIT_FAILED); //--- editing and copying of the ini-file in the folder of the terminals if(!CopyCommonIni()) return(INIT_FAILED); if(!CopyTerminalIni()) return(INIT_FAILED); //--- сopying an expert in the terminal folders
下位ターミナル№1の例でのiniファイルの使用スキーム(GetStatsFromAccounts_EA.mq5::CopyCommonIni())。
//+------------------------------------------------------------------+ //| Copying common.ini - file in a shared folder of client | //| terminals. Edit the ini-file and copy obtained | //| ini-files into folders | //| ...\AppData\Roaming\MetaQuotes\Terminal\"id terminal"\MQL5\Files | //+------------------------------------------------------------------+ bool CopyCommonIni() { //0 — "Evey tick", "1 — 1 minute OHLC", 2 — "Open price only" //3 — "Math calculations", 4 — "Every tick based on real ticks" //--- path to Data Folder string terminal_data_path=TerminalInfoString(TERMINAL_DATA_PATH); //--- path to Commomm Data Folder string common_data_path=TerminalInfoString(TERMINAL_COMMONDATA_PATH); //--- string existing_file_name=terminal_data_path+"\\config\\common.ini"; // full path to the ini-file string temp_name_ini=terminal_data_path+"\\MQL5\\Files\\"+common_file_name; string test=NULL; //--- terminal #1 if(!CopyFileW(existing_file_name,temp_name_ini,false)) { PrintFormat("Failed with error: %x",kernel32::GetLastError()); return(false); } EditCommonIniFile(common_file_name,3000,4); test=slaveTerminalDataPath1+"\\MQL5\\Files\\"+common_file_name; if(!CopyFileW(temp_name_ini,test,false)) { PrintFormat("Failed with error: %x",kernel32::GetLastError()); return(false); } ResetLastError(); if(!FileDelete(common_file_name,0)) Print("#1 file ",common_file_name," not deleted, an error ",GetLastError()); //--- terminal #2
EditCommonIniFile(common_file_name,3000,4)関数の呼び出しで引き渡されます。
common_file_name — 編集する必要があるiniファイル名。 3000 — テストのエージェントポートナンバー。各ターミナルは自分のテストエージェントで起動する必要があります。エージェントのナンバリングは3000から行われます。テストエージェントのポート番号を表示する方法は次のようになります。MetaTrader 5ターミナルでストラテジーテスターに移動し、ストラテジーテスターの『操作ログ』のタブで右クリックします。ドロップダウンメニューで、テストエージェントのポート番号を見ることができます。
図7. テストエージェント
4 - テストタイプ
- 0 — 『全ティック』
- 1 — 『1分足OHLC』
- 2 — 『始値のみ』
- 3 — 『数値計算』
- 4 — 『リアルティックに基づいた全てのティック』
commom.ini設定ファイルの編集は、GetStatsFromAccounts_EA.mq5::EditCommonIniFile(string name,const int port,const int model)関数で行われ、ファイルを開く/ファイルからの読み取り/ファイルへの書き込みの操作はMQL5ツールで行われます。
//+------------------------------------------------------------------+ //| Editing common.ini file | //+------------------------------------------------------------------+ bool EditCommonIniFile(string name,const int port,const int model) { bool tester=false; // if false - means the section [Tester] not found int count_tester=0; // counter discoveries section [Tester] //--- откроем файл ResetLastError(); int file_handle=FileOpen(name,FILE_READ|FILE_WRITE|FILE_TXT); if(file_handle!=INVALID_HANDLE) { //--- auxiliary variable string str; //--- read data while(!FileIsEnding(file_handle)) { //--- read line str=FileReadString(file_handle,-1); //--- find [Tester] if(StringFind(str,"[Tester]",0)!=-1) { tester=true; count_tester++; } } if(!tester) { FileWriteString(file_handle,"[Tester]\n",-1); FileWriteString(file_handle,"Expert=test\n",-1); FileWriteString(file_handle,"Symbol=EURUSD\n",-1); FileWriteString(file_handle,"Period=H1\n",-1); FileWriteString(file_handle,"Deposit=10000\n",-1); //0 — "Evey tick", "1 — 1 minute OHLC", 2 — "Open price only" //3 — "Math calculations", 4 — "Every tick based on real ticks" FileWriteString(file_handle,"Model="+IntegerToString(model)+"\n",-1); FileWriteString(file_handle,"Optimization=0\n",-1); FileWriteString(file_handle,"FromDate=2016.01.22\n",-1); FileWriteString(file_handle,"ToDate=2016.06.06\n",-1); FileWriteString(file_handle,"Report=TesterReport\n",-1); FileWriteString(file_handle,"ReplaceReport=1\n",-1); FileWriteString(file_handle,"UseLocal=1\n",-1); FileWriteString(file_handle,"Port="+IntegerToString(port)+"\n",-1); FileWriteString(file_handle,"Visual=0\n",-1); FileWriteString(file_handle,"ShutdownTerminal=0\n",-1); } //--- close file FileClose(file_handle); } else { PrintFormat("Unable to open file %s, error = %d",name,GetLastError()); return(false); } return(true); }
動作を完了する前にMetaTrader 5ターミナルは、ウィンドウとパネルの配置を保存し、それらのサイズは『terminal.ini』ファイルへ保存します。ファイル自体は、『config』サブフォルダのデータディレクトリにあります。例として、私の下位ターミナル№1での、『terminal.ini』への完全なパスはこのようになります。
『C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\config\terminal.ini』。
terminal.iniファイル自体では、[Window]ブロックのみが重要になります。MetaTrader 5ターミナルをウィンドウに復元します。ターミナルはおよそこのようなサイズになります。
図8. ウィンドウに復元したターミナル
このターミナルを閉じると、terminal.iniファイルの[Window]ブロックはこのようになります。
Arrange=1 [Window] Fullscreen=0 Type=1 Left=412 Top=65 Right=1212 Bottom=665 LSave=412
つまり、[Window]ブロックはターミナルの座標とその状態を保存します。
4.5. ターミナルのサイズ(幅、高さ)を指定とファイルの途中への文字列の挿入
下位ターミナルのterminal.iniファイルでの座標の変更は、起動時に4つの下位ターミナル全てが整列するようにするために必要なものです。
図9. ターミナルの位置
上記の通り、各下位ターミナルの為に『terminal.imi』ファイルを編集する必要があります。ここでは、文字列は『terminal.ini』ファイルの末尾ではなく、途中に挿入する必要がある点に注意してください。以下に、この手順の特徴を説明します。
ターミナルの『サンドボックス』に『test.txt』ファイルがあるという例で説明をします。『test.txt』ファイルの内容:
s=0 df=12 asf=3 g=3 n=0 param_f=123
以下のようになるように、2つ目と3つ目の文字列で情報を変更する必要があります。
s=0 df=1256 asf=5 g=3 n=0 param_f=123
初見では以下を実行する必要があります。
- 読み取りや書き込みの為にファイルを開く、最初の文字列を読み取る(この操作は2つ目の文字列の先頭にファイルポインタを移動します)。
- 2つ目の文字列に新しい数値『df=1256』を書き込む。
- 3つ目の文字列に新しい数値『asf=5』を書き込む。
- ファイルを閉じる。
//+------------------------------------------------------------------+ //| InsertRowsMistakenly.mq5 | //| Copyright © 2016, Vladimir Karputov | //| http://wmua.ru/slesar/ | //+------------------------------------------------------------------+ #property copyright "Copyright © 2016, Vladimir Karputov" #property link "http://wmua.ru/slesar/" #property version "1.00" //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- open file ResetLastError(); string name="test.txt"; int file_handle=FileOpen(name,FILE_READ|FILE_WRITE|FILE_TXT); if(file_handle!=INVALID_HANDLE) { FileReadString(file_handle,-1); FileWriteString(file_handle,"df=1256"+"\r\n",-1); FileWriteString(file_handle,"asf=5"+"\r\n",-1); //--- close file FileClose(file_handle); } else { PrintFormat("Unable to open file %s, error = %d",name,GetLastError()); return; } } //+------------------------------------------------------------------+
予期しない結果となり、4つ目の文字列に『g=』が入りました。
前 | 後 |
---|---|
s=0 df=12 asf=3 g=3 n=0 param_f=123 |
s=0 df=1256 asf=5 3 n=0 param_f=123 |
どうしてこのようになったのでしょうか?お互いに従っているセルの集合体からファイルが構成されていることを想像してみてください。各セルには1つの文字が配置されます。したがって、ファイルの途中から書き込むと、実際にはセルを上書きしているのです。始めからこの場所にあった文字よりも、多い文字を追加する場合(上記の例では、私達は『df=12』だったところに、2文字多い『df=1256』を書き込みました)、余計な文字は今後のコードに悪影響を及ぼします。このようになります。
図10. 情報の破損。
ファイルの途中に文字列を挿入する際の情報の破損を避けるために、次の事を実行しましょう。
- 下位ターミナルの『terminal.ini』ファイルをマスターターミナルのサンドボックスの『terminal_ext.ini』)という名前のファイにコピーする(Win API CopyFileW)。
- マスターターミナルのサンドボックスに『terminal.ini』ファイルを作成し、それを書き込むために開く(MQL5)。
- マスターターミナルのサンドボックスで『terminal_ext.ini』ファイルを読み取りの為に開く(MQL5)。
- マスターボックスのサンドボックスで『terminal_ext.ini』から文字列を読み取り、それを『terminal.ini』ファイルに書き込む(MQL5)。
- 読み取った文字列が[Window]と等しくなった場合にのみ、新しい座標を『terminal.ini』ファイルに書き込み(6つの文字列)、『terminal_ext.ini』ファイルでファイルポインタも6つの文字列に移動する(MQL5)。
- マスターターミナルのサンドボックスで、『terminal_ext.ini』から文字列を読み取り、それを『terminal_ext.ini』ファイルの末尾を検出するまで、『terminal.ini』ファイルに書き込む(MQL5)。
- マスターターミナルのサンドボックスで、『terminal.ini』と『terminal_ext.ini』のファイルを閉じる(MQL5)。
- マスターターミナルのサンドボックスの『terminal.ini』を下位ターミナルの『terminal.ini』ファイルへコピーする(Win API CopyFileW)。
- マスターターミナルのサンドボックスで、『terminal.ini』と『terminal_ext.ini』のファイルを削除する(MQL5)。
関数呼び出しの順序:
GetStatsFromAccounts_EA.mq5::OnInit() >вызов> GetStatsFromAccounts_EA.mq5::CopyTerminalIni()
//+------------------------------------------------------------------+ //| Editing Files "terminal.ini" | //+------------------------------------------------------------------+ bool CopyTerminalIni() { //--- path to the terminal data folder string terminal_data_path=TerminalInfoString(TERMINAL_DATA_PATH); //--- string existing_file_name=NULL; string ext_ini=terminal_data_path+"\\MQL5\\Files\\terminal_ext.ini"; string ini=terminal_data_path+"\\MQL5\\Files\\terminal.ini"; int left=0; int top=0; int right=0; int bottom=0; //--- for(int i=1;i<5;i++) { switch(i) { case 1: existing_file_name=slaveTerminalDataPath1+"\\config\\terminal.ini"; left=0; top=0; right=682; bottom=420; break; case 2: existing_file_name=slaveTerminalDataPath2+"\\config\\terminal.ini"; left=682; top=0; right=1366; bottom=420; break; case 3: existing_file_name=slaveTerminalDataPath3+"\\config\\terminal.ini"; left=0; top=738-413; right=682; bottom=738; break; case 4: existing_file_name=slaveTerminalDataPath4+"\\config\\terminal.ini"; left=682; top=738-413; right=1366; bottom=738; break; } //--- if(!CopyFileW(existing_file_name,ext_ini,false)) { PrintFormat("Failed with error: %x",kernel32::GetLastError()); return(false); } if(!EditTerminalIniFile("terminal_ext.ini",left,top,right,bottom)) return(false); if(!CopyFileW(ini,existing_file_name,false)) { PrintFormat("Failed with error: %x",kernel32::GetLastError()); return(false); } ResetLastError(); if(!FileDelete("terminal.ini",0)) Print("#",i," file terminal.ini not deleted, an error ",GetLastError()); ResetLastError(); if(!FileDelete("terminal_ext.ini",0)) Print("#",i," file terminal_ext.ini not deleted, an error ",GetLastError()); } //--- return(true); }
>вызов> GetStatsFromAccounts_EA.mq5::EditTerminalIniFile
//+------------------------------------------------------------------+ //| Editing terminal.ini file | //+------------------------------------------------------------------+ bool EditTerminalIniFile(string ext_name,const int Left=0,const int Top=0,const int Right=1366,const int Bottom=738) { //--- creates and opens files string name="terminal.ini"; ResetLastError(); int terminal_ini_handle=FileOpen(name,FILE_WRITE|FILE_TXT); int terminal_ext_ini__handle=FileOpen(ext_name,FILE_READ|FILE_TXT); if(terminal_ini_handle==INVALID_HANDLE) { PrintFormat("Unable to open file %s, error = %d",name,GetLastError()); } if(terminal_ext_ini__handle==INVALID_HANDLE) { PrintFormat("Unable to open file %s, error = %d",ext_name,GetLastError()); } if(terminal_ini_handle==INVALID_HANDLE && terminal_ext_ini__handle==INVALID_HANDLE) { FileClose(terminal_ext_ini__handle); FileClose(terminal_ini_handle); return(false); } //--- auxiliary variable string str=NULL; //--- read data while(!FileIsEnding(terminal_ext_ini__handle)) { //--- read line str=FileReadString(terminal_ext_ini__handle,-1); FileWriteString(terminal_ini_handle,str+"\r\n",-1); //--- find [Window] if(StringFind(str,"[Window]",0)!=-1) { FileReadString(terminal_ext_ini__handle,-1); FileWriteString(terminal_ini_handle,"Fullscreen=0\r\n",-1); FileReadString(terminal_ext_ini__handle,-1); FileWriteString(terminal_ini_handle,"Type=1\r\n",-1); FileReadString(terminal_ext_ini__handle,-1); FileWriteString(terminal_ini_handle,"Left="+IntegerToString(Left)+"\r\n",-1); FileReadString(terminal_ext_ini__handle,-1); FileWriteString(terminal_ini_handle,"Top="+IntegerToString(Top)+"\r\n",-1); FileReadString(terminal_ext_ini__handle,-1); FileWriteString(terminal_ini_handle,"Right="+IntegerToString(Right)+"\r\n",-1); FileReadString(terminal_ext_ini__handle,-1); FileWriteString(terminal_ini_handle,"Bottom="+IntegerToString(Bottom)+"\r\n",-1); } } //--- close files FileClose(terminal_ext_ini__handle); FileClose(terminal_ini_handle); return(true); }
このように、図9のように起動することができるように、下位ターミナルで『terminal.ini』ファイルが編集されます。9. またテストチャートを観察し、様々なモードでテストの精度を比較することができます。
5. 下位端末のテストへの起動
これでエキスパートアドバイザのテストモードで下位ターミナルの起動の準備が全て整いました。
- 全ての下位ターミナルの為に『myconfiguration.ini』設定ファイルを準備しました。
- 全ての下位ターミナルの『terminal.ini』ファイルを編集しました。
- テストが行われるエキスパートアドバイザの名前を認識しました。
5.1. 下位ターミナルのフォルダへのエキスパートアドバイザのコピー
以前に選択したエキスパートアドバイザのコピー(その名前は『expert_name』変数で保存されます)はOnInit()で行われます。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { ArrayFree(arr_path); if(!FindDataFolders(arr_path)) return(INIT_SUCCEEDED); //--- if(MessageBox("Ready?",NULL,MB_YESNO)==IDYES) { expert_name=OpenFileName(); if(expert_name==NULL) return(INIT_FAILED); //--- editing and copying of the ini-file in the folder of the terminals if(!CopyCommonIni()) return(INIT_FAILED); if(!CopyTerminalIni()) return(INIT_FAILED); //--- сopying an expert in the terminal folders ResetLastError(); if(!CopyFileW(expert_name,slaveTerminalDataPath1+"\\MQL5\\Experts\\test.ex5",false)) { PrintFormat("Failed CopyFileW #1 with error: %x",kernel32::GetLastError()); return(INIT_FAILED); } if(!CopyFileW(expert_name,slaveTerminalDataPath2+"\\MQL5\\Experts\\test.ex5",false)) { PrintFormat("Failed CopyFileW #2 with error: %x",kernel32::GetLastError()); return(INIT_FAILED); } if(!CopyFileW(expert_name,slaveTerminalDataPath3+"\\MQL5\\Experts\\test.ex5",false)) { PrintFormat("Failed CopyFileW #3 with error: %x",kernel32::GetLastError()); return(INIT_FAILED); } if(!CopyFileW(expert_name,slaveTerminalDataPath4+"\\MQL5\\Experts\\test.ex5",false)) { PrintFormat("Failed CopyFileW #4 with error: %x",kernel32::GetLastError()); return(INIT_FAILED); } //--- Sleep(sleeping);
ShellExecuteW — 指定したファイルで操作を実行します。
//--- x64 long ShellExecuteW( long hwnd, // string lpOperation, // string lpFile, // string lpParameters, // string lpDirectory, // int nShowCmd // ); //--- x32 int ShellExecuteW( int hwnd, // string lpOperation, // string lpFile, // string lpParameters, // string lpDirectory, // int nShowCmd // );
パラメータ
hwnd
[in]はユーザーインターフェイスの表示とエラーメッセージの為に使用する親ウィンドウのハンドルです。操作がウィンドウに関係がない場合、この数値はNULLとなります。
lpOperation
[in]は実行される動作を特定するコマンド名を持つ文字列です。利用可能なコマンドのセットは、特定のファイルまたはフォルダに依存します。原則として、オブジェクトのコンテキストメニューから利用可能な動作です。通常次のコマンドが使用されます。
"edit"
エディターを起動し、編集の為にドキュメントを開きます。lpFileがドキュメントファイルではない場合、関数は実行されません。
"explore"
lpFileで指定されたフォルダを開きます。
"find"
lpDirectoryで指定されたディレクトリで始まる検索を開始します。
"open"
lpFileパラメータで指定された要素を開きます。この要素はファイルまたはフォルダとなります。
"print"
指定されたlpFileを印字します。lpFile がドキュメントファイルではない場合、関数はエラーで終了します。
"NULL"
デフォルトのコマンド名があればそれが使用されます。このようなコマンドがない場合、『open』コマンドが使用されます。1つもコマンドが使用されない場合、システムはレジストリで指定された最初のコマンドを使用します。
lpFile[in]はコマンドを実行することができるファイルまたはオブジェクトを指定する文字列です。フルネームが引き渡されます(ファイル名だけでなく、そこへのパスを含む)。オブジェクトは全てのコマンドをサポートしているわけではないことにご注意ください。例えば、全てのドキュメントが『print』コマンドをサポートしているわけではありません。lpDirectoryパラメータの為に相対パスが使用される場合、lpFileの為の相対パスを使用しないでください。
lpParameters[in]はlpFileが実行ファイルを指す場合、このパラメータはアプリケーションに引き渡されるパラメータを特定する文字列となります。この文字列の形式は実行すべきコマンドの名前によって決定されます。lpFileがドキュメントのファイルを指す場合、lpParameters はNULLとなります。
lpDirectory[in]は動作ディレクトリを決定する文字列です。この数値がNULLの場合、現在の動作ディレクトリが使用されます。相対パスがlpFileで指定されている場合はlpDirectoryの為の相対パスは使用しないでください。
nShowCmd[in]は、アプリケーションを開いた時に、アプリケーションがどのように表示されるべきかを指定するフラグです。lpFileがドキュメントファイルを指定する場合、フラグは関連するアプリケーションに引き渡されます。使用するフラグ:
//+------------------------------------------------------------------+ //| Enumeration command to start the application | //+------------------------------------------------------------------+ enum EnSWParam { //+------------------------------------------------------------------+ //| Displays the window as a minimized window. This value is similar | //| to SW_SHOWMINIMIZED, except the window is not activated. | //+------------------------------------------------------------------+ SW_SHOWMINNOACTIVE=7, //+------------------------------------------------------------------+ //| Activates and displays a window. If the window is minimized or | //| maximized, the system restores it to its original size and | //| position. An application should specify this flag when | //| displaying the window for the first time. | //+------------------------------------------------------------------+ SW_SHOWNORMAL=1, //+------------------------------------------------------------------+ //| Activates the window and displays it as a minimized window. | //+------------------------------------------------------------------+ SW_SHOWMINIMIZED=2, //+------------------------------------------------------------------+ //| Activates the window and displays it as a maximized window. | //+------------------------------------------------------------------+ SW_SHOWMAXIMIZED=3, //+------------------------------------------------------------------+ //| Hides the window and activates another window. | //+------------------------------------------------------------------+ SW_HIDE=0, //+------------------------------------------------------------------+ //| Activates the window and displays it in its current size | //| and position. | //+------------------------------------------------------------------+ SW_SHOW=5, };
戻り値
関数が正常に終了した場合、32よりも大きい値を返します。
Win APIのShellExecuteW関数の宣言例:
#import "shell32.dll" int GetLastError(); //+------------------------------------------------------------------+ //| ShellExecute function | //| https://msdn.microsoft.com/en-us/library/windows/desktop/bb762153(v=vs.85).aspx //| Performs an operation on a specified file | //+------------------------------------------------------------------+ //--- x64 long ShellExecuteW(long hwnd,string lpOperation,string lpFile,string lpParameters,string lpDirectory,int nShowCmd); //--- x32 int ShellExecuteW(int hwnd,string lpOperation,string lpFile,string lpParameters,string lpDirectory,int nShowCmd); #import #import "kernel32.dll" //+------------------------------------------------------------------+ //| Enumeration command to start the application | //+------------------------------------------------------------------+ enum EnSWParam { //+------------------------------------------------------------------+ //| Displays the window as a minimized window. This value is similar | //| to SW_SHOWMINIMIZED, except the window is not activated. | //+------------------------------------------------------------------+ SW_SHOWMINNOACTIVE=7, //+------------------------------------------------------------------+ //| Activates and displays a window. If the window is minimized or | //| maximized, the system restores it to its original size and | //| position. An application should specify this flag when | //| displaying the window for the first time. | //+------------------------------------------------------------------+ SW_SHOWNORMAL=1, //+------------------------------------------------------------------+ //| Activates the window and displays it as a minimized window. | //+------------------------------------------------------------------+ SW_SHOWMINIMIZED=2, //+------------------------------------------------------------------+ //| Activates the window and displays it as a maximized window. | //+------------------------------------------------------------------+ SW_SHOWMAXIMIZED=3, //+------------------------------------------------------------------+ //| Hides the window and activates another window. | //+------------------------------------------------------------------+ SW_HIDE=0, //+------------------------------------------------------------------+ //| Activates the window and displays it in its current size | //| and position. | //+------------------------------------------------------------------+ SW_SHOW=5, };
OnInit()から下位ターミナルを起動します。
//--- Sleep(sleeping); LaunchSlaveTerminal(ExtInstallationPathTerminal_1,slaveTerminalDataPath1+"\\MQL5\\Files\\"+common_file_name); Sleep(sleeping); LaunchSlaveTerminal(ExtInstallationPathTerminal_2,slaveTerminalDataPath2+"\\MQL5\\Files\\"+common_file_name); Sleep(sleeping); LaunchSlaveTerminal(ExtInstallationPathTerminal_3,slaveTerminalDataPath3+"\\MQL5\\Files\\"+common_file_name); Sleep(sleeping); LaunchSlaveTerminal(ExtInstallationPathTerminal_4,slaveTerminalDataPath4+"\\MQL5\\Files\\"+common_file_name); } //--- return(INIT_SUCCEEDED); }
この時、起動の間にエキスパートアドバイザはミリ秒の『sleeping』を待ちます。デフォルトでは、『sleeping』パラメータは9000と等しいです(つまり、9秒)。下位ターミナルでエージェントの認証エラーが発生した場合は、このパラメータを増加させてください。
Win API関数に引き渡されるパラメータ(私の下位ターミナル№1の場合)はこのようになります。
LaunchSlaveTerminal("C:\Program Files\MetaTrader 5 1\", "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\MQL5\Files\myconfiguration.ini");
6. 考えられるエラー
下位ターミナルのうちの1つが起動するが、テスターがテストエージェントに接続することができないという状況が起こりえます。
テスターの『操作ログ』タブでは、このような記録となります。
2016.07.15 15:10:48.327 Tester EURUSD: history data begins from 2014.01.14 00:00 2016.07.15 15:10:49.212 Core 1 agent process started 2016.07.15 15:10:49.717 Core 1 connecting to 127.0.0.1:3002 2016.07.15 15:11:00.771 Core 1 tester agent authorization error 2016.07.15 15:11:01.417 Core 1 connection closed
エージェントのログではこのような記録となります。
2016.07.15 16:08:45.416 Startup MetaTester 5 x64 build 1368 (13 Jul 2016) 2016.07.15 16:08:45.612 Server MetaTester 5 started on 127.0.0.1:3000 2016.07.15 16:08:45.612 Startup initialization finished 2016.07.15 16:09:36.811 Server MetaTester 5 stopped 2016.07.15 16:09:38.422 Tester shutdown tester machine
このような場合は、ターミナル間の起動の間隔を大きくし(『sleeping』変数)、コアプロセッサを使用するリソースを大量に消費する全てのアプリケーションをアンロードすることをお勧めします。
まとめ
選択したエキスパートアドバイザのテストを実行するタスクは、一度に4つのテストモードで実行されました。エキスパートアドバイザの起動後、4つのターミナルでテストの経過をほぼ同時に観察することができます。
また記事ではこのようなWin API関数を呼び出す方法を紹介しました。
- CopyFileW — MQL5の『サンドボックス』から、また『サンドボックス』へファイルをコピーします。
- FindClose — 検索ハンドルを閉じます。
- FindFirstFileW — 指定したファイル名と一致するファイルディレクトリまたはサブディレクトリを探します。
- FindNextFileW — FindFirstFile関数の前回の呼び出しからファイル検索を続行します。
- GetOpenFileNameW — ファイルを開くシステムダイアログを呼び出します。
- ShellExecuteW — アプリケーションを起動させます。
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/2552
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索