交易者的 LifeHack: 四次回测比一次好

Vladimir Karputov | 2 九月, 2016

在第一次单独测试之前,每个交易者都面对相同的问题 — "四种模式中使用那一种呢?" 每个提供的模式都有其优点和特点,所以我们将会使用简单的方法 - 使用一个按钮一起运行全部四种模式!本文展示了如何使用 Win API 和一点魔术来同时看到全部四个测试图表。

目录


简介

本文的主要目的是展示如何从一个终端到同时在四个终端(它们将被称为从终端,并使用#1,#2,#3和#4表示)上运行一个EA交易的单个测试(不是优化,只是测试!)。与此同时,从终端中的策略测试器将以不同的订单生成模式来运行:

重要的局限:

  1. 主终端(Master terminal)一定不能使用/portable参数启动;
  2. 至少要安装五个 MetaTrader 5 终端;
  3. 主终端的交易账户 — 让我们称之为主帐户 — 必须在每个从终端上至少激活一次。这是有必要的,因为本文中的EA交易不会使用INI文件把交易账户密码传给从终端,它只是传送运行策略测试器的交易账户的编号,并且这个编号永远对应着主帐户的编号。这样的行为是符合逻辑的,因为应该在同一个交易账户上使用不同的订单生成模式来测试EA交易。
  4. 在启动EA交易之前,需要使CPU尽可能空闲: 关闭在线游戏,媒体播放器和其他消耗资源较多的程序,否则,一个CPU核心可能会被阻挡,那个核上可能就无法进行测试。

1. 一般原则

过犹不及

我一直倾向于使用软件的标准功能,关于 MetaTrader 5 交易终端, 它有如下说法: "永远不要使用 /portable 关键字启动终端, 永远不要在操作系统中禁止用户账户控制(UAC)"。在此基础上,所介绍的EA交易将操作位于 AppData 文件夹下的文件。

所有文章中的屏幕截图都是在 Windows 10 中完成的,因为它是最新的许可证完整的系统,所有文中的应用程序代码都会考虑这一点。

相关的EA交易广泛使用了DLL和其他MQL5特性:


图 1. 依赖关系 

特别指出,调用了以下的 Windows API 函数:


2. 输入参数


图 3. 输入参数 

"folder of the MetaTrader#Х installation(MetaTrader #X 的安装文件夹)"路径就是从终端的安装文件夹,当在mq5代码中指定路径时,需要使用双斜线书写。另外很重要的就是在路径的末尾使用双反斜线:

//--- 输入参数                                 
input string   ExtInstallationPathTerminal_1="C:\\Program Files\\MetaTrader 5 1\\";    // MetaTrader#1 的安装文件夹
input string   ExtInstallationPathTerminal_2="D:\\MetaTrader 5 2\\";                   // MetaTrader#2 的安装文件夹
input string   ExtInstallationPathTerminal_3="D:\\MetaTrader 5 3\\";                   // MetaTrader#3 的安装文件夹
input string   ExtInstallationPathTerminal_4="D:\\MetaTrader 5 4\\";                   // MetaTrader#4 的安装文件夹
input string   ExtTerminalName="terminal64.exe";                                       // 正确的终端文件名称

在64位操作系统中,终端程序的文件名称是"terminal64.exe"。

 

绑定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

这对于只从当前终端接收参数是有用的。那么将要运行EA测试的从终端还需要怎样处理呢?如何绑定从终端的安装目录和它们的数据目录呢?

这就需要解释为什么知道数据文件夹是在 AppData 文件夹下是很重要的了(引用自帮助):

从 MS Windows Vista 开始, 安装在 Program Files 文件夹下的应用程序,不允许在安装目录下保存数据,默认的所有用户(All)的数据应该保存在独立的Windows用户目录下。

换句话说,EA可以自由创建和修改位于如下文件夹下的文件: C:\Users\user_name\AppData\Roaming\MetaQuotes\Terminal\terminal_identifier\MQL5\Files. 在这里,"terminal_identifier"是主终端的标示符。


3. 匹配安装文件夹与从终端(Slave terminals)的AppData文件夹

EA 根据指定的配置文件在从终端中启动,另外,每个终端使用的是单独的配置文件,每个配置文件都有指示,使终端启动之后就开始测试指定的EA交易。对应的命令位于配置文件的 [Tester] 部分:

...
[Tester]
Expert=test
...

您可以看到,并没有指定路径,意思是测试的 EA 交易可以独立位于 MQL5 "沙盒"中,以从终端1为例,可以有两个路径:

  1. C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\MQL5\Experts
  2. 或者 C:\Program Files\MetaTrader 5 1\MQL5\Experts

位置 №2 被排除掉,因为根据从 Windows Vista 开始的安全策略,禁止在 "Program Files" 文件夹下写入,只剩下位置 №1 — 这就是说所有的从终端都需要进行安装目录和AppData中的数据目录进行匹配。 

3.1. 秘密 №1

每个数据目录都包含了一个 "origin.txt" 文件,以从终端1为例:


 

图 4. "origin.txt" 文件 

以及 origin.txt 文件的内容:

C:\Program Files\MetaTrader 5 1

文件中的这个记录指出,"C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962" 文件夹是由安装在 "C:\Program Files\MetaTrader 5 1" 目录下的终端创建的。

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_VALUElpFindFileData中的内容将是未定义的,为了取得错误的进一步信息,调用GetLastError函数。

如果函数因为没有找到对应文件而触发,GetLastError函数返回ERROR_FILE_NOT_FOUND


FindNextFileW继续前一次调用FindFirstFileFindFirstFileEx, 或者 FindFirstFileTransacted 函数之后的搜索。

bool  FindNextFileW(
   HANDLE           FindFile,           //
   WIN32_FIND_DATA  &lpFindFileData     //
   );

参数

FindFile

[in] 之前调用 FindFirstFile 或 FindFirstFileEx 函数返回的句柄。

lpFindFileData

[in][out]  WIN32_FIND_DATA 结构的指针,用于接收找到文件或目录的信息。 

返回值

如果函数成功,返回值是非0值,并且lpFindFileData参数包含了找到的文件或者文件夹,

如果函数以出错结束,返回值为0,并且lpFindFileData的内容是未定义的。为了获取错误的额外信息,调用GetLastError函数。

如果函数是因为没有找到任何文件而出错,GetLastError 函数返回 ERROR_NO_MORE_FILES

声明 Win API 中 FindFirstFileWFindNextFileW 函数的实例 (代码来自所包含的 ListingFilesDirectory.mqh 文件):

#define MAX_PATH                 0x00000104  //
#define FILE_ATTRIBUTE_DIRECTORY 0x00000010  //
#define ERROR_NO_MORE_FILES      0x00000012  //没有更多文件
#define ERROR_FILE_NOT_FOUND     0x00000002  //系统没有找到指定的文件
//+------------------------------------------------------------------+
//| FILETIME 结构                            |
//+------------------------------------------------------------------+
struct FILETIME
  {
   uint              dwLowDateTime;
   uint              dwHighDateTime;
  };
//+------------------------------------------------------------------+
//| WIN32_FIND_DATA 结构                        |
//+------------------------------------------------------------------+
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];
  };

#import "kernel32.dll"
int      GetLastError();
long     FindFirstFileW(string lpFileName,WIN32_FIND_DATA  &lpFindFileData);
int      FindNextFileW(long FindFile,WIN32_FIND_DATA &lpFindFileData);
int      FindClose(long hFindFile);
int      FindNextFileW(int FindFile,WIN32_FIND_DATA &lpFindFileData);
int      FindClose(int hFindFile);
int      CopyFileW(string lpExistingFileName,string lpNewFileName,bool bFailIfExists);
#import

bool WinAPI_FindClose(long hFindFile)
  {
   bool res;
   if(_IsX64)
      res=FindClose(hFindFile)!=0;      
   else
      res=FindClose((int)hFindFile)!=0;      
//---
   return(res);
  }
  
bool WinAPI_FindNextFile(long hFindFile,WIN32_FIND_DATA &lpFindFileData)
  {
   bool res;
   if(_IsX64)
      res=FindNextFileW(hFindFile,lpFindFileData)!=0;      
   else
      res=FindNextFileW((int)hFindFile,lpFindFileData)!=0;      
//---
   return(res);
  }

3.3. 使用 FindFirstFileW, FindNextFileW 的实例

"ListingFilesDirectory.mq5"脚本程序既是实例也是EA中的完整工作代码,换句话说,代码会尽可能接近实际使用。

目标: 获得 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(找到第一个) 

通常, "*.*" 搜索字串用于寻找所有文件,所以,需要使用以下字符串进行两次操作: 消去"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("FindFirstFile (hFind) 失败,错误代码: %x",kernel32::GetLastError());
      return;
     }

// 列出目录下的所有文件以及一些相关信息

如果调用 FindFirstFileW 失败, "hFind" 搜索句柄将等于 "INVALID_HANDLE",而脚本程序将会终止。

如果调用 FindFirstFileW 函数成功, 创建一个do while循环, 在其中获得文件或者文件夹的名称, 并且在循环的末尾将会调用Win API 的 FindNextFileW 函数:

// 列出目录下的所有文件以及一些相关信息
   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,"\", 文件属性常数 (dec): ",ffd.dwFileAttributes);
      //---
      ArrayInitialize(ffd.cFileName,0);
      ArrayInitialize(ffd.cAlternateFileName,0);
      ffd.dwFileAttributes=-100;
      ResetLastError();
      rezult=WinAPI_FindNextFile(hFind,ffd);
     }
   while(rezult!=0);
   if(kernel32::GetLastError()!=ERROR_NO_MORE_FILES)
      PrintFormat("FindNextFileW (hFind) 失败,错误代码: %x",kernel32::GetLastError());
   WinAPI_FindClose(hFind);

'do while' 循环在 Win API 的 FindNextFileW 函数返回非零值时会一直继续,如果调用 Win API 的 FindNextFileW 函数返回0,并且错误代码不等于 "ERROR_NO_MORE_FILES" — 意思就是遇到了严重错误。

在脚本程序运行的末尾,搜索句柄会被关闭。 

"ListingFilesDirectory.mq5" 脚本程序的运行结果:

mask_path=C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.*
hFind=-847293552
".", 文件属性常数 (dec): 16
"..", 文件属性常数 (dec): 16
"038C9E8FAFF9EA373522ECC6D5159962", 文件属性常数 (dec): 16
"0C46DDCEB43080B0EC647E0C66170465", 文件属性常数 (dec): 16
"2A6A33B25AA0984C6AB9D7F28665B88E", 文件属性常数 (dec): 16
"50CA3DFB510CC5A8F28B48D1BF2A5702", 文件属性常数 (dec): 16
"BC11041F9347CD71C5F8926F53AA908A", 文件属性常数 (dec): 16
"Common", 文件属性常数 (dec): 16
"Community", 文件属性常数 (dec): 16
"D0E8209F77C8CF37AD8BF550E51FF075", 文件属性常数 (dec): 16
"D3852169A6E781B7F35488A051432620", 文件属性常数 (dec): 16
"EE57F715BA53F2E183D6731C9376293D", 文件属性常数 (dec): 16
"Help", 文件属性常数 (dec): 16

3.4. 在终端目录之中

以上描述的实例演示了顶端的工作 — 在 "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\" 目录中,但是请注意章节3.1. 秘密 №1, 还有必要深入查看所有的子文件夹。

为此,要组织一次二级搜索,在子目录中搜索需要使用 Win API 中 FineFirstFileW的主搜索字符串:

"C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\" + name of found top-level folder + "origin.txt".

这样 FindFirstFileW 的主搜索将只会在子目录中查找单个文件 — "origin.txt".

这里是 FindDataPath() 函数的完整代码:

//+------------------------------------------------------------------+
//| 找到并读取 origin.txt 文件                     |
//+------------------------------------------------------------------+
void FindDataPath(string &array[][2])
  {
//---
   WIN32_FIND_DATA ffd;
   long            hFirstFind_0,hFirstFind_1;

   ArrayInitialize(ffd.cFileName,0);
   ArrayInitialize(ffd.cAlternateFileName,0);
//+------------------------------------------------------------------+
//| 取得计算机中安装的所有终端的通用路径.                              |
//| 我的电脑中的通用路径:                          |
//| 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)
     {
      //+------------------------------------------------------------------+
      //| 去掉 "Common" ... 我们可以得到:                   |
      //| C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal         |
      //+------------------------------------------------------------------+
      common_data_path=StringSubstr(common_data_path,0,pos-1);
     }
   else
      return;

//--- 搜索阶段 №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("FindFirstFile (hFirstFind_0) 失败,错误代码: %x",kernel32::GetLastError());
      return;
     }

//--- 列出目录下的全部文件以及一些相关信息
   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);
            //--- 搜索阶段 №1. 在文件夹下搜索 origin.txt 文件
            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("FindFirstFile (hFirstFind_1) 失败,错误代码: %x",kernel32::GetLastError());
                  break;
                 }
               WinAPI_FindClose(hFirstFind_1);
               ArrayInitialize(ffd.cFileName,0);
               ArrayInitialize(ffd.cAlternateFileName,0);
               ResetLastError();
               rezult=WinAPI_FindNextFile(hFirstFind_0,ffd);
               continue;
              }
            //--- 在文件夹下找到了 origin.txt 文件
            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); //--- 从 origin.txt 文件中得到一个字符串
            if(origin!=NULL)
              {
               //--- 在数组中写下字符串
               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\
              }
            WinAPI_FindClose(hFirstFind_1);
           }
        }
      ArrayInitialize(ffd.cFileName,0);
      ArrayInitialize(ffd.cAlternateFileName,0);
      ResetLastError();
      rezult=WinAPI_FindNextFile(hFirstFind_0,ffd);
     }
   while(rezult!=0); //if(hFirstFind_1==INVALID_HANDLE), 我们在这里出现
   if(kernel32::GetLastError()!=ERROR_NO_MORE_FILES)
      PrintFormat("FindNextFileW (hFirstFind_0) 失败,错误代码: %x",kernel32::GetLastError());
   else
      Print("filter_0: \"",filter_0,"\", handle hFirstFind_0: ",hFirstFind_0,", NO_MORE_FILES");
   WinAPI_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\*.*") 并获得 "hFirstFind_0" 的主搜索句柄, 它等于 1901014212592。因为 "hFirstFind_0" 的值不等于 "INVALID_HANDLE" — 那么传给 Win API 中 FindFirstFileW(filter_0,ffd) 的搜索过滤字符串就是正确的。在成功调用了 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"后应该怎样做。

3.5. CopyFileW

CopyFileW — 把已经存在的文件复制成一个新的文件。

bool  CopyFileW(
   string lpExistingFileName,     //
   string lpNewFileName,          //
   bool bFailIfExists             //
   );

参数

lpExistingFileName

[in] 已存在文件的名称。

在此, 名称长度有一个限制 — 最多有MAX_PATH个字符, 这对例子来说肯定是足够的。

如果名称为lpExistingFileName的文件不存在,函数会失败并且GetLastError返回ERROR_FILE_NOT_FOUND

lpNewFileName

[in]  新文件的名称。 

在此, 名称长度有一个限制 — 最多有MAX_PATH个字符, 这对例子来说肯定是足够的。

bFailIfExists
[in] 
如果此参数为TRUE并且在lpNewFileName中指定的新文件已经存在,函数就会失败。如果此参数为FALSE并且新文件存在,函数会覆盖已有文件并且成功结束。

返回值

如果函数成功,返回值不等于0。

如果函数以出错结束,返回值为0。为了取得错误的额外信息,需要调用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 函数来进行的,

把沙盒中 "origin.txt" 的路径写到 "new_path" 变量中:

//+------------------------------------------------------------------+
//| 复制到通用数据文件夹                        |
//| 即所有客户终端的 ***\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

并调用 Win API 的 CopyFileW 函数,第三个参数设为 false — 允许覆盖沙盒中的 "origin.txt" 文件:

//--- Win API
   if(!CopyFileW(full_file_name,new_path,false))
     {
      Print("错误的 CopyFile,从 ",full_file_name," 到 ",new_path);
      return(NULL);
     }
//--- 使用 MQL5 打开文件

打开 "origin.txt" 文件用于读取,并且不要忘记设置 FILE_COMMON 标志, 因为文件是在通用文件夹中:

//--- 使用 MQL5 打开文件
   string str;
   ResetLastError();
   int file_handle=FileOpen("origin.txt",FILE_READ|FILE_TXT|FILE_COMMON);
   if(file_handle!=INVALID_HANDLE)
     {
      //--- 使用 MQL5 读取一个字符串
      str=FileReadString(file_handle,-1)+"\\";
      //--- 使用 MQL5 关闭文件
      FileClose(file_handle);
     }
   else
     {
      PrintFormat("文件 %s 打开失败 , MQL5 错误=%d","origin.txt",GetLastError());
      return(NULL);
     }
   return(str);
  }

只读取一次 — 一个字符串,在它的末尾加上 "\\" 并返回获得的结果。

3.7. 完成工作

四个终端的安装目录在输入参数中设置:

//--- 输入参数                                 
input string   ExtInstallationPathTerminal_1="C:\\Program Files\\MetaTrader 5 1\\";    // MetaTrader#1 安装文件夹
input string   ExtInstallationPathTerminal_2="D:\\MetaTrader 5 2\\";                   // MetaTrader#2 安装文件夹
input string   ExtInstallationPathTerminal_3="D:\\MetaTrader 5 3\\";                   // MetaTrader#3 安装文件夹
input string   ExtInstallationPathTerminal_4="D:\\MetaTrader 5 4\\";                   // MetaTrader#4 安装文件夹

这些路径是固定写成的,它们必须正确指向安装的目录。

另外,在全局还要声明另外四个字符串变量和一个数组:

string         slaveTerminalDataPath1=NULL;                                // 终端 #1 的数据文件夹路径
string         slaveTerminalDataPath2=NULL;                                // 终端 #2 的数据文件夹路径
string         slaveTerminalDataPath3=NULL;                                // 终端 #3 的数据文件夹路径
string         slaveTerminalDataPath4=NULL;                                // 终端 #4 的数据文件夹路径
//---
string         arr_path[][2];

在 AppData 中的终端文件夹路径需要保存在那些变量中,二维数组将有所帮助。现在可以大致描绘如何匹配从终端的安装目录和它们在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); //--- 从 origin.txt 文件中得到一个字符串
            if(origin!=NULL)
              {
               //--- 在数组中写下字符串
               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) 函数,在调用之后就在二维数组中加入一条记录。数组的"0"维包含AppData中的终端路径,而"1"维保存着安装路径(提醒一下,这个路径可以从找到的"origin.txt"文件中获得)。

>把控制权返回给> GetStatsFromAccounts_EA.mq5::FindDataFolders(arr_path): 

在此,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);
     }

如果达到了这一步,EA就匹配了安装路径和它们在AppData文件夹下的路径。如果至少有一个在AppData下的终端路径没有找到 (也就是说等于 NULL), 那么会在最后几行打印所有路径,而 EA 因错误而终止。


4. 选择用于测试的 EA

测试EA的文件应该在运行四个从终端之前选择好,该EA交易必须预先编译好并放在主终端的数据目录中。

4.1. GetOpenFileName

GetOpenFileName — 创建"打开文件"对话框, 用户可以指定驱动器, 文件夹和将要打开文件(或一组文件)的名称。"打开文件"对话框的声明和实现都完整位于所包含的 GetOpenFileNameW.mqh 文件中。

4.2. 使用 "打开文件" 系统对话框选择一个 EA交易

"打开文件"系统对话框是在EA的OnInit()中被调用的:

//+------------------------------------------------------------------+
//| EA交易初始化函数                           |
//+------------------------------------------------------------------+
int OnInit()
  {
   ArrayFree(arr_path);
   if(!FindDataFolders(arr_path))
      return(INIT_SUCCEEDED);
//---
   if(MessageBox("准备好了吗?",NULL,MB_YESNO)==IDYES)
     {
      expert_name=OpenFileName();
      if(expert_name==NULL)
         return(INIT_FAILED);
      //--- 在终端的文件夹中编辑和复制 ini 文件

在那里调用了 GetOpenFileNameW.mqh::OpenFileName(void)。

//+------------------------------------------------------------------+
//| 创建打开文件对话框                          |
//+------------------------------------------------------------------+
string OpenFileName(void)
  {
   string path=NULL;
   string filter=NULL;
   if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian")
      filter="Компилированный код";
   else
      filter="已编译的代码";
   if(GetOpenFileName(path,filter+"\0*.ex5\0",TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Experts\\","选择源文件"))
      return(path);
   else
     {
      PrintFormat("失败,错误编号: %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中的文字 ① 的。"\0*.ex5\0" 字符串用于过滤文件类型 (图2中的 ②)。"TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Experts\\"" 字符串指定了 "打开文件"系统对话框的文件夹的路径。

4.3. INI 配置文件

为了从命令行运行(或者使用Win API)测试EA的终端,需要有一个配置INI文件,其中必须包含以下的[Tester]部分以及所需的指令:

[Tester]
Expert=test             //在测试模式下自动运行的EA交易的名称。
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文件的步骤是:

  1. 取得主终端中 "common.ini" 的完整路径,完整路径的字符串形式:
    "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\config\common.ini". (MQL5)
  2. 取得INI文件在 "\Files" 沙盒中的新路径,新路径的字符串形式:
    对于主终端,是"C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\MQL5\Files\myconfiguration.ini",(MQL5)
  3. 把"common.ini"文件复制为"myconfiguration.ini",(Win API 的 CopyFileW 函数)。
  4. 编辑"myconfiguration.ini"文件,(MQL5);
  5. 取得INI文件在从终端沙盒中的新路径,它的字符串形式(以从终端 №1 为例)
    "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\MQL5\Files\myconfiguration.ini". (MQL5)
  6. 把编辑过的"myconfiguration.ini"INI文件从主终端的沙盒中复制到从终端的沙盒之中(Win API 的 CopyFileW 函数);
  7. 从主终端的沙盒中删除"myconfiguration.ini"文件,(MQL5)。

这个过程必须在每个从终端中重复进行。尽管还有优化的空间,但这些过程不在本文讨论之列。 

在选择好了需要测试的EA交易之后,就开始编辑配置INI文件,GetStatsFromAccounts_EA.mq5::OnInit():

//+------------------------------------------------------------------+
//| EA交易初始化函数                           |
//+------------------------------------------------------------------+
int OnInit()
  {
   ArrayFree(arr_path);
   if(!FindDataFolders(arr_path))
      return(INIT_SUCCEEDED);
//---
   if(MessageBox("准备好了吗?",NULL,MB_YESNO)==IDYES)
     {
      expert_name=OpenFileName();
      if(expert_name==NULL)
         return(INIT_FAILED);
      //--- 在终端的文件夹中编辑和复制 ini 文件
      if(!CopyCommonIni())
         return(INIT_FAILED);
      if(!CopyTerminalIni())
         return(INIT_FAILED);
      //--- 在终端文件夹下复制EA交易

操作INI文件的过程,以从终端 №1 为例, GetStatsFromAccounts_EA.mq5::CopyCommonIni():

//+------------------------------------------------------------------+
//| 复制 common.ini - 文件位于客户终端的共享                 |
//| 文件夹下,编辑 ini-文件,并把获得的文件复制                     |
//| 到文件夹中                             |
//| ...\AppData\Roaming\MetaQuotes\Terminal\"终端id"\MQL5\Files    |
//+------------------------------------------------------------------+
bool CopyCommonIni()
  {
//0 — "每一订单", "1 — 1分钟OHLC", 2 — "仅开盘价"
//3 — "数学计算", 4 — "基于真实订单的每一订单" 
//--- 数据文件夹的路径
   string terminal_data_path=TerminalInfoString(TERMINAL_DATA_PATH);
//--- 通用数据文件夹的路径
   string common_data_path=TerminalInfoString(TERMINAL_COMMONDATA_PATH);
//---
   string existing_file_name=terminal_data_path+"\\config\\common.ini"; // ini文件的完整路径                                                       
   string temp_name_ini=terminal_data_path+"\\MQL5\\Files\\"+common_file_name;
   string test=NULL;
//--- 终端 #1
   if(!CopyFileW(existing_file_name,temp_name_ini,false))
     {
      PrintFormat("失败,错误编号: %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("失败,错误编号: %x",kernel32::GetLastError());
      return(false);
     }
   ResetLastError();
   if(!FileDelete(common_file_name,0))
      Print("#1 file ",common_file_name," not deleted, an error ",GetLastError());
//--- 终端 #2

对 EditCommonIniFile(common_file_name,3000,4) 函数的调用传入以下参数:

common_file_name — 将要编辑的 INI 文件;

3000 — 测试代理的端口编号. 每个终端必须由它们自己的测试代理运行,代理编号从 3000 开始,如需查看测试代理的端口编号: 在 MetaTrader 5终端中,进入策略测试器并使用鼠标右键点击策略测试器的 "日志" 页面,测试代理的端口编号可以在下拉菜单中看到:


 

图 7. 测试代理 

4 - 测试的类型: 

在 GetStatsFromAccounts_EA.mq5::EditCommonIniFile(string name,const int port,const int model) 函数中编辑 common.ini 配置文件 — 打开文件的操作,文件的读写都是使用 MQL5 的方式执行的:

//+------------------------------------------------------------------+
//| 编辑 common.ini 文件                        |
//+------------------------------------------------------------------+
bool EditCommonIniFile(string name,const int port,const int model)
  {
   bool tester=false;      // 如果是 false - 意思是没有找到 [Tester] 部分
   int  count_tester=0;    // 寻找 [Tester] 部分的计数器
//--- 打开文件 
   ResetLastError();
   int file_handle=FileOpen(name,FILE_READ|FILE_WRITE|FILE_TXT);
   if(file_handle!=INVALID_HANDLE)
     {
      //--- 辅助变量
      string str;
      //--- 读取数据
      while(!FileIsEnding(file_handle))
        {
         //--- 读取一行 
         str=FileReadString(file_handle,-1);
         //--- 寻找 [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 — "每一订单", "1 — 1 分钟 OHLC", 2 — "仅开盘价"
         //3 — "数学计算", 4 — "基于真实订单的每一订单" 
         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);
        }
      //--- 关闭文件
      FileClose(file_handle);
     }
   else
     {
      PrintFormat("无法打开文件 %s, 错误编号 = %d",name,GetLastError());
      return(false);
     }
   return(true);
  }

4.4. 秘密 №2

在关闭之前,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 中的坐标,可以在从终端启动时按以下形式进行排布:

 

图 9. 终端的分布

上面已经说到,每个从终端的 "terminal.ini" 文件都需要修改,请注意,文字需要被插入,不是在末尾,而是在"terminal.ini"文件的中间。以下是这个过程的特点,

这里是一个例子: 终端的"沙盒"中有一个"test.txt"文件,"test.txt" 文件的内容:

s=0
df=12
asf=3
g=3
n=0
param_f=123

需要修改第二行和第三行以达到以下效果:

s=0
df=1256
asf=5
g=3
n=0
param_f=123

乍一看来, 应该这样做:

让我们以此为准写出"InsertRowsMistakenly.mq5"脚本程序的例子代码:
//+------------------------------------------------------------------+
//|                                         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()
  {
//--- 打开文件
   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);
      //--- 关闭文件
      FileClose(file_handle);
     }
   else
     {
      PrintFormat("无法打开文件 %s, 错误编号 = %d",name,GetLastError());
      return;
     }
  }
//+------------------------------------------------------------------+


收到了一个未预料到的结果 — 第四行缺少了"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

为什么会这样呢?把文件看作包含了一系列相互连接的单元,每个单元包含一个字符,所以,当从文件的中间开始写一些内容时,单元会被覆盖,如果加入了比之前多的字符(在上面的例子中: 开始有"df=12", 后来多加了两个字符 - "df=1256"), 多出来的字符就打乱了后来的代码,所以看起来就成了这样:

写字符串

图 10. 信息被打乱

为了避免打乱信息,当在文件中间插入文字时,要按以下方式做

函数调用顺序:

GetStatsFromAccounts_EA.mq5::OnInit() >调用> GetStatsFromAccounts_EA.mq5::CopyTerminalIni()

//+------------------------------------------------------------------+
//| 编辑 "terminal.ini" 文件                      |
//+------------------------------------------------------------------+
bool CopyTerminalIni()
  {
//--- 终端数据文件夹路径 
   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("失败,错误编号: %x",kernel32::GetLastError());
         return(false);
        }
      if(!EditTerminalIniFile("terminal_ext.ini",left,top,right,bottom))
         return(false);
      if(!CopyFileW(ini,existing_file_name,false))
        {
         PrintFormat("失败,错误编号: %x",kernel32::GetLastError());
         return(false);
        }
      ResetLastError();
      if(!FileDelete("terminal.ini",0))
         Print("#",i," terminal.ini 文件未被删除, 错误为 ",GetLastError());
      ResetLastError();
      if(!FileDelete("terminal_ext.ini",0))
         Print("#",i," terminal_ext.ini 文件未被删除, 错误为 ",GetLastError());
     }
//---
   return(true);
  }

 >调用> GetStatsFromAccounts_EA.mq5::EditTerminalIniFile

//+------------------------------------------------------------------+
//| 编辑 terminal.ini 文件                       |
//+------------------------------------------------------------------+
bool EditTerminalIniFile(string ext_name,const int Left=0,const int Top=0,const int Right=1366,const int Bottom=738)
  {
//--- 创建并打开文件
   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("无法打开文件 %s, 错误编号 = %d",name,GetLastError());
     }
   if(terminal_ext_ini__handle==INVALID_HANDLE)
     {
      PrintFormat("无法打开文件 %s, 错误 = %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);
     }

//--- 辅助变量
   string str=NULL;
//--- 读取数据
   while(!FileIsEnding(terminal_ext_ini__handle))
     {
      //--- 读取文字
      str=FileReadString(terminal_ext_ini__handle,-1);
      FileWriteString(terminal_ini_handle,str+"\r\n",-1);
      //--- 找到 [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);
        }
     }
//--- 关闭文件
   FileClose(terminal_ext_ini__handle);
   FileClose(terminal_ini_handle);
   return(true);
  }

这样,从终端中的 "terminal.ini" 文件就编辑好了,就可以像图9种那样启动了,就可以观察测试图表,比较不同测试模式下的精确度了。 


5. 在从终端中运行测试

在 EA 交易测试模式下运行从终端的准备工作都就绪了:

只剩下两个任务了: 把选择的EA复制到从终端的沙盒中并且运行这些终端。

5.1. 把EA交易复制到从终端的文件夹中

在OnInit()中复制之前所选的EA交易 (它的名称保存在 "expert_name" 变量中):

//+------------------------------------------------------------------+
//| EA交易初始化函数                           |
//+------------------------------------------------------------------+
int OnInit()
  {
   ArrayFree(arr_path);
   if(!FindDataFolders(arr_path))
      return(INIT_SUCCEEDED);
//---
   if(MessageBox("准备好了吗?",NULL,MB_YESNO)==IDYES)
     {
      expert_name=OpenFileName();
      if(expert_name==NULL)
         return(INIT_FAILED);
      //--- 在终端的文件夹中编辑和复制 ini 文件
      if(!CopyCommonIni())
         return(INIT_FAILED);
      if(!CopyTerminalIni())
         return(INIT_FAILED);
      //--- 在终端文件夹下复制EA交易
      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("CopyFileW #2 失败,错误代码: %x",kernel32::GetLastError());
         return(INIT_FAILED);
        }

      if(!CopyFileW(expert_name,slaveTerminalDataPath3+"\\MQL5\\Experts\\test.ex5",false))
        {
         PrintFormat("CopyFileW #3 失败,错误代码: %x",kernel32::GetLastError());
         return(INIT_FAILED);
        }

      if(!CopyFileW(expert_name,slaveTerminalDataPath4+"\\MQL5\\Experts\\test.ex5",false))
        {
         PrintFormat("CopyFileW #4 失败,错误代码: %x",kernel32::GetLastError());
         return(INIT_FAILED);
        }
      //---
      Sleep(sleeping);

5.2. ShellExecuteW

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"命令,如果两个命令都没有,系统会使用注册表中指定的第一个命令。

lpFile 

[in] 用于设定执行命令的文件或者对象的字符串,传入的是全名(不仅包含文件名,还包含路径)。请注意,对象可能不支持所有的命令,例如,不是所有的文档都支持"print"命令。如果在lpDirectory参数中使用了相对路径,就不要在lpFile中使用相对路径。

lpParameters

[in] 如果lpFile指向了一个可执行文件,这个参数就是一个定义了传给应用程序参数的字符串,字符串的格式由将要执行命令的名称决定。如果lpFile指向一个文档文件,lpParameters必须为NULL

lpDirectory

[in] 定义工作目录的字符串。如果这个值是NULL, 就使用当前的工作目录。如果在lpFile中指定了相对路径, 就不要在lpDirectory中使用相对路径。

nShowCmd

[in] 决定应用程序在打开时如何显示的标志,如果lpFile指定了一个文档文件,这个标志就传给对应的应用程序。使用的标志:

//+------------------------------------------------------------------+
//| 启动应用程序的枚举命令                           |
//+------------------------------------------------------------------+
enum EnSWParam
  {
   //+------------------------------------------------------------------+
   //| 把窗口显示为最小化窗口,这个值是类似于 |
   //| SW_SHOWMINIMIZED, 除了窗口没有被激活。         |
   //+------------------------------------------------------------------+
   SW_SHOWMINNOACTIVE=7,
   //+------------------------------------------------------------------+
   //| 激活并显示一个窗口,如果窗口被最小化或者   |
   //| 最大化, 系统会把它恢复到它原来的大小和       |
   //| 位置。应用程序应该指定这个标志,当           |
   //| 第一次显示窗口的时候。                       |
   //+------------------------------------------------------------------+
   SW_SHOWNORMAL=1,
   //+------------------------------------------------------------------+
   //| 激活窗口并把它显示为最小化窗口                        .      |
   //+------------------------------------------------------------------+
   SW_SHOWMINIMIZED=2,
   //+------------------------------------------------------------------+
   //| 激活窗口并把它显示为最大化窗口。                            |
   //+------------------------------------------------------------------+
   SW_SHOWMAXIMIZED=3,
   //+------------------------------------------------------------------+
   //| 隐藏窗口并激活另一个窗口。                        |
   //+------------------------------------------------------------------+
   SW_HIDE=0,
   //+------------------------------------------------------------------+
   //| 激活窗口并以它当前的大小                                  |
   //| 和位置显示。                             |
   //+------------------------------------------------------------------+
   SW_SHOW=5,
  };

返回值

如果函数成功,它返回一个大于 32 的值。

调用 Win API 的 ShellExecuteW 的例子:

#import  "shell32.dll"
int  GetLastError();
//+------------------------------------------------------------------+
//| ShellExecute 函数                         |
//| https://msdn.microsoft.com/en-us/library/windows/desktop/bb762153(v=vs.85).aspx
//| 对指定文件进行某项操作                       |
//+------------------------------------------------------------------+
//--- 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"

//+------------------------------------------------------------------+
//| 启动应用程序的枚举命令                           |
//+------------------------------------------------------------------+
enum EnSWParam
  {
   //+------------------------------------------------------------------+
   //| 把窗口显示为最小化窗口,这个值是类似于 |
   //| SW_SHOWMINIMIZED, 除了窗口没有被激活。         |
   //+------------------------------------------------------------------+
   SW_SHOWMINNOACTIVE=7,
   //+------------------------------------------------------------------+
   //| 激活并显示一个窗口,如果窗口被最小化或者   |
   //| 最大化, 系统会把它恢复到它原来的大小和       |
   //| 位置。应用程序应该指定这个标志,当           |
   //| 第一次显示窗口的时候。                       |
   //+------------------------------------------------------------------+
   SW_SHOWNORMAL=1,
   //+------------------------------------------------------------------+
   //| 激活窗口并把它显示为最小化窗口                        .      |
   //+------------------------------------------------------------------+
   SW_SHOWMINIMIZED=2,
   //+------------------------------------------------------------------+
   //| 激活窗口并把它显示为最大化窗口。                            |
   //+------------------------------------------------------------------+
   SW_SHOWMAXIMIZED=3,
   //+------------------------------------------------------------------+
   //| 隐藏窗口并激活另一个窗口。                        |
   //+------------------------------------------------------------------+
   SW_HIDE=0,
   //+------------------------------------------------------------------+
   //| 激活窗口并以它当前的大小                                  |
   //| 和位置显示。                             |
   //+------------------------------------------------------------------+
   SW_SHOW=5,
  };

5.3. 运行终端

从终端在 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);
  }

同时,EA交易在每次运行之间等待"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. 可能的错误

当从终端启动,而测试器没有连接上测试代理的时候可能会出现状况,

测试器将在"日志"页面出现类似如下的记录:

2016.07.15 15:10:48.327 Tester  EURUSD: 历史数据开始于 2014.01.14 00:00
2016.07.15 15:10:49.212 Core 1  代理的处理开始
2016.07.15 15:10:49.717 Core 1  连接到 127.0.0.1:3002
2016.07.15 15:11:00.771 Core 1  测试器代理认证错误
2016.07.15 15:11:01.417 Core 1  连接关闭

测试代理记录包含这些内容:

2016.07.15 16:08:45.416 启动 MetaTester 5 x64 build 1368 (13 Jul 2016)
2016.07.15 16:08:45.612 服务器  MetaTester 5 开始于 127.0.0.1:3000
2016.07.15 16:08:45.612 启动初始化结束
2016.07.15 16:09:36.811 服务器  MetaTester 5 停止
2016.07.15 16:09:38.422 Tester  关闭测试机

在这种情况下,推荐增加终端运行的延迟 ("sleeping" 变量), 并且退出所有可能阻止CPU内核使用的消耗资源的应用程序。


结论

同时在四种测试模式下测试所选EA交易的任务就这样结束了,在启动EA交易后,就可以几乎同时在全部四个终端中观察测试。

本文也展示了如何调用 Win API 函数,例如: