交易员生存诀窍: 若干测试的比较报告
Vladimir Karputov | 23 十二月, 2016
内容
- 概论
- 必要动作
- 1. 输入参数选择智能交易系统进行测试
- 2. 再次关注 common.ini
- 2.1. common.ini -> original.ini
- 2.2. 使用正则表达式搜索 [Common] 段落
- 2.3. 创建四个文件: myconfiguration1.ini, myconfiguration2.ini, myconfiguration3.ini 和 myconfiguration.ini
- 2.4. 编辑 ini 文件 (复制 [Common] 段落和独立的 [Tester] 段落)
- 3. 解析并编辑所选智能交易系统的 mq5 文件
- 3.1秘密 #3
- 3.2. 包含 "#include"
- 3.3. 包含 "double OnTester()"
- 3.4. 复杂情况: 代码已经包括 DistributionOfProfits.mqh 和/或 OnTester()
- 4. 将智能交易系统复制到从属终端的文件夹
- 5. 启动从属终端
- 6. 比较报告
- 结论
概论
在不同的品种上多次测试智能交易系统并无说服力, 因为您需要将每个品种的测试结果保存为单独的文件, 然后比较结果。我建议改变此方法, 即同时运行若干个智能交易系统测试多个品种。在此情况下, 测试结果可以保存到统一位置, 并可直观比较。
在以前的文章中已经讨论过一些解决方案:
测试场景:
- 选择用于测试的智能交易系统 (Win API)
- 解析智能交易系统代码并在其内添加调用图形报告库 (Win API, MQL5, 和正则表达式)
- 解析主终端的 common.ini 并为每个终端准备独立的 common.ini (Win API, MQL5, 和正则表达式)
- 将独立的 common.ini 复制到从属终端的文件夹 (Win API)
- 将独立的 common.ini 复制到从属终端的文件夹 (Win API)
- 解析从属终端的报告
- 将从属终端的结果添加到一个公共报告中
必要动作
在启动智能交易系统之前, 我们需要 "同步" 主站和从属终端。
- 主站和从属终端应连接到同一个交易账户。
- 在所有从属终端的设置中, 允许使用 DLL。如果您使用 \Portable 键启动终端, 进入终端安装目录 (使用文件资源管理器和其它文件管理器), 启动终端 "terminal64.exe" 并设置 "允许导入 DLL"。
- 库文件 "DistributionOfProfits.mqh" 应添加到所有从属终端的数据文件夹 (数据文件夹\MQL5\Include\DistributionOfProfits.mqh)。
1. 输入参数。选则一款智能交易系统进行测试
由于我的计算机有四个内核, 我只能运行四个测试代理。因此, 并发 (或有几秒钟的小延迟), 我只能运行四个终端, 即每个代理占用一个终端。这就是为什么在输入参数中有四组可用设置的原因:
参数:
- MetaTrader#ххх 的安装文件夹
- 终端 #xxx 的测试品种
- 终端 #xxx 的测试周期
- 正确的终端文件名
- 暂歇的毫秒数 — 从属终端之间的启动暂停
- 开始测试日期 (仅年, 月和日)
- 结束测试日期 (仅年, 月和日)
- 初始本金
- 杠杆
在基本算法开始之前, 我们需要链接从属终端的安装文件夹和 AppData 文件夹中的数据目录。此处是一个简单的脚本 Check_TerminalPaths.mq5:
//+------------------------------------------------------------------+ //| Check_TerminalPaths.mq5 | //| 版权所有 2009, MetaQuotes 软件公司| //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "2009, MetaQuotes 软件公司" #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| 脚本程序开始函数 | //+------------------------------------------------------------------+ void OnStart() { //--- Print("TERMINAL_PATH = ",TerminalInfoString(TERMINAL_PATH)); Print("TERMINAL_DATA_PATH = ",TerminalInfoString(TERMINAL_DATA_PATH)); Print("TERMINAL_COMMONDATA_PATH = ",TerminalInfoString(TERMINAL_COMMONDATA_PATH)); } //+------------------------------------------------------------------+
脚本打印三个参数:
- TERMINAL_PATH — 终端运行所在的文件夹
- TERMINAL_DATA_PATH — 终端数据存储的文件夹
- TERMINAL_COMMONDATA_PATH — 计算机上所有安装终端的公共文件夹
三个终端的示例 (它们当中之一使用 /Portable 键运行):
// 终端启动为主要模式 TERMINAL_PATH = C:\Program Files\MetaTrader 5 TERMINAL_DATA_PATH = C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075 TERMINAL_COMMONDATA_PATH = C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common // 终端启动为主要模式 TERMINAL_PATH = D:\MetaTrader 5 3 TERMINAL_DATA_PATH = C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\0C46DDCEB43080B0EC647E0C66170465 TERMINAL_COMMONDATA_PATH = C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common // 终端启动为可移动模式 TERMINAL_PATH = D:\MetaTrader 5 5 TERMINAL_DATA_PATH = D:\MetaTrader 5 5 TERMINAL_COMMONDATA_PATH = C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common
您可阅读我以前的一篇文章的以下段落, 了解有关终端文件夹和 AppData 中文件夹的对应关系的更多信息:
使用系统的 "打开文件" 对话框 (GetOpenFileNameW 函数) 选择一款智能交易系统:
有关调用打开文件对话框的细节已在我之前的文章 "交易员生存诀窍: 四遍回测优于一遍: 4.2. 利用系统 "打开文件" 对话框选择一款 EA" 中讨论。
当前版本 (文件 GetOpenFileNameW.mqh, 版本 1.003) 改变的功能在 OpenFileName 函数:
//+------------------------------------------------------------------+ //| 创建打开文件对话框 | //+------------------------------------------------------------------+ string OpenFileName(const string filter_description="可编辑代码", const string filter="\0*.mq5\0", const string title="选择源文件") { string path=NULL; if(GetOpenFileName(path,filter_description+filter,TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Experts\\",title)) return(path); else { PrintFormat("引发失败的错误: %x",kernel32::GetLastError()); return(NULL); } }
现在它设置文件搜索过滤器已变得更方便。还请注意, 现在过滤器搜索推荐 *.mq5 格式的文件 (在上一篇文章中是搜索编译的 *.ex5 文件)。
2. 再次关注 common.ini
现在是时候来描述在 'Compare multiple tests.mq5' 中的 CopyCommonIni() 函数。
从属终端通过 指定的配置文件 来启动。我们有四个从属终端, 所以我们需要创建四个 *.ini 文件: myconfiguration1.ini, myconfiguration2.ini, myconfiguration3.ini, myconfiguration4.ini。文件 myconfigurationХ.ini 是基于终端的 common.ini 文件创建, 来自我们已启动的智能交易系统。文件 common.ini 的路径:
TERMINAL_DATA_PATH\config\common.ini
创建和编辑 myconfiguration.ini 文件的算法如下所示:
- 拷贝 common.ini 至文件夹 TERMINAL_COMMONDATA_PATH\Files\original.ini (WinAPI CopyFileW)
- 在原始 original.ini 文件里, 查找段落 [Common] (MQL5 + 正则表达式)。
对于我的主终端此段落看起来像这样 (终端尚未登录 mql5 社区):
[Common] Login=5116256 ProxyEnable=0 ProxyType=0 ProxyAddress= ProxyAuth= CertInstall=0 NewsEnable=0 NewsLanguages=
- 创建四个文件: myconfiguration1.ini, myconfiguration2.ini, myconfiguration3.ini 和 myconfiguration4.ini (MQL5)
- 编辑这四个文件 (拷贝 [Common] 和独立的 [Tester] 段落) (MQL5)
2.1. common.ini -> original.ini
这可能是最简单的代码: 保存路径 "数据文件夹" 和 "公共数据文件夹" 至变量, 并使用 "original.ini" 的值初始化一个变量
string terminal_data_path=TerminalInfoString(TERMINAL_DATA_PATH); // 数据文件夹的路径 string common_data_path=TerminalInfoString(TERMINAL_COMMONDATA_PATH);// 公共数据文件夹的路径 string original_ini="original.ini"; string arr_common[]; //--- string full_name_common_ini=terminal_data_path+"\\config\\common.ini"; // 文件 common.ini 的完整路径 string full_name_original_ini=common_data_path+"\\Files\\"+original_ini; // 文件 original.ini 的完整路径 //--- common.ini -> original.ini if(!CopyFileW(full_name_common_ini,full_name_original_ini,false)) { PrintFormat("引发失败的错误: %x",kernel32::GetLastError()); return(false); }
使用 Win API 的 CopyFileW 函数拷贝 "common.ini" 配置文件至 "original.ini"。
我们使用正则表达式以便搜索并拷贝 [Common] 段落。这并非一个正常的任务, 因为 common.ini 文件包含非常短的行, 在其末尾总是使用换行符 (不可见字符)。有两种方法:
一次读取一行 | 读取整个文件存入一个变量 |
---|---|
|
|
测试文件 "test_original.ini":
[Charts] ProfileLast=Default MaxBars=100000 PrintColor=0 SaveDeleted=0 TradeLevels=1 TradeLevelsDrag=0 ObsoleteLasttime=1475473485 [Common] Login=1783501 ProxyEnable=0 ProxyType=0 ProxyAddress= ProxyAuth= CertInstall=0 NewsEnable=0 [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
文件 "test_original.ini" 可用于训练 "Receiving lines.mq5" 脚本的正则表达式。在脚本设置中可以选择两种操作模式:
- 一次读取一行, 并在每行中搜索
- 或者将整个文件读入一个变量
两种方法的几个比较示例:
一次读取一行 | 读取整个文件存入一个变量 |
---|---|
查询: "Prox(.*)0" - 搜索 "Prox" - 后跟任何符号, 除了换行符或另一个 Unicode 字符串的分隔符, 找到零次或多次 (贪婪) "(.*)" - 一旦发现 "0" 个匹配, 搜索必须结束 | |
12: 0: ProxyEnable=0, 13: 0: ProxyType=0, | : 0: ProxyEnable=0ProxyType=0ProxyAddress=ProxyAuth=CertInstall=0NewsEnable=0[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, |
如您所见, 输出两个结果 | 其一包含一个结果, 其中有很多不必要的项目 (贪婪请求的结果) |
查询: "Prox(.*?)0" - 搜索 "Prox" - 后跟任何符号, 除了换行符或另一个 Unicode 字符串的分隔符, 找到零次或多次 (贪婪) "(.*)" - 一旦发现 "0" 个匹配, 搜索必须结束 | |
12: 0: ProxyEnable=0, 13: 0: ProxyType=0, | : 0: ProxyEnable=0, 1: ProxyType=0, 2: ProxyAddress=ProxyAuth=CertInstall=0, |
我们再次得到两个结果 | 在此情况下, 我们有三个结果, 第三个结果不是我们预期的结果。 |
我们应该使用什么方法来提取整个 "[Common]" 块 — 每次读取一行, 或读入一个变量?我选择了一次读取一行和以下算法:
- 搜索 "[Common]" (MQL5);
- 一旦发现, 写入数组;
- 然后我们继续向数组中写入行, 直到正则表达式找到 "[" 符号。
实现这一方式的例程就在 "Receiving lines v.2.mq5" 脚本:
//+------------------------------------------------------------------+ //| Receiving lines v.2.mq5 | //| 版权所有 2016, MetaQuotes 软件公司| //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "版权所有 2016, MetaQuotes 软件公司" #property link "https://www.mql5.com" #property version "1.000" #property description "单独块 \"[Common]\"" #property script_show_inputs #include <RegularExpressions\Regex.mqh> //--- input string file_name="test_original.ini"; // f文件名 input string str_format="(\\[)(.*?)(\\])"; //--- int m_handel; bool m_found_Common=false; // 发现关键词 "[Common]" - 标志将被设为 true //+------------------------------------------------------------------+ //| 脚本程序开始函数 | //+------------------------------------------------------------------+ void OnStart() { string arr_text[]; // 结果数组 //--- Print("格式: ",str_format); m_handel=FileOpen(file_name,FILE_READ|FILE_ANSI|FILE_TXT); if(m_handel==INVALID_HANDLE) { Print("FileOpen 操作失败, 错误 ",GetLastError()); return; } Regex *rgx=new Regex(str_format); while(!FileIsEnding(m_handel)) { string str=FileReadString(m_handel); if(str=="[Common]") { m_found_Common=true; int size=ArraySize(arr_text); ArrayResize(arr_text,size+1,10); arr_text[size]=str; continue; // 跳转至 while... } if(m_found_Common) { MatchCollection *matches=rgx.Matches(str); int count=matches.Count(); if(count>0) { if(count>1) { Print("警报!matches.Count()==",count); return; } delete matches; break; // 跳转至 FileClose... } else { delete matches; // 如果未发现匹配 } int size=ArraySize(arr_text); ArrayResize(arr_text,size+1,10); arr_text[size]=str; } } FileClose(m_handel); delete rgx; Regex::ClearCache(); //--- 测试 int size=ArraySize(arr_text); for(int i=0;i<size;i++) { Print(arr_text[i]); } } //+------------------------------------------------------------------+
脚本执行结果:
2016.10.05 06:58:09.276 接收行 v.2 (EURUSD,M1) 格式: (\[)(.*?)(\]) 2016.10.05 06:58:09.277 接收行 v.2 (EURUSD,M1) [Common] 2016.10.05 06:58:09.277 接收行 v.2 (EURUSD,M1) Login=1783501 2016.10.05 06:58:09.277 接收行 v.2 (EURUSD,M1) ProxyEnable=0 2016.10.05 06:58:09.277 接收行 v.2 (EURUSD,M1) ProxyType=0 2016.10.05 06:58:09.277 接收行 v.2 (EURUSD,M1) ProxyAddress= 2016.10.05 06:58:09.277 接收行 v.2 (EURUSD,M1) ProxyAuth= 2016.10.05 06:58:09.277 接收行 v.2 (EURUSD,M1) CertInstall=0 2016.10.05 06:58:09.277 接收行 v.2 (EURUSD,M1) NewsEnable=0
如您所见, 脚本已准确地从"test_original.ini" 文件中识别出 "[Common]" 参数块。我借用的 "Receiving lines v.2.mq5" 脚本中的 SearchBlock() 函数几乎未有修改。如果 "[Common]" 块被成功发现, SearchBlock() 函数将把此块写入服务数组 arr_common[]。
2.3. 创建四个文件: myconfiguration1.ini, myconfiguration2.ini, myconfiguration3.ini 和 myconfiguration.ini
这四个文件是通过顺序调用以下代码创建的 (注意打开文件时使用的标志):
//+------------------------------------------------------------------+ //| 打开新文件 | //+------------------------------------------------------------------+ bool IniFileOpen(const string name_file,int &handle) { handle=FileOpen(name_file,FILE_WRITE|FILE_ANSI|FILE_TXT|FILE_COMMON); if(handle==INVALID_HANDLE) { Print("操作 FileOpen 文件 ",name_file," 失败, 错误 ",GetLastError()); return(false); } //--- return(true); }
2.4. 编辑 ini 文件 (复制 [Common] 段落和独立的 [Tester] 段落)
早前, "[Common]" 参数块已写入服务数组 arr_common[]。现在此数组已 写入所有四个文件:
//--- 记录块 "[Common]" int arr_common_size=ArraySize(arr_common); for(int i=0;i<arr_common_size;i++) { FileWrite(handle1,arr_common[i]); FileWrite(handle2,arr_common[i]); FileWrite(handle3,arr_common[i]); FileWrite(handle4,arr_common[i]); } //--- 记录块 "[Tester]" string expert_short_name="D0E820_test"; WriteBlockTester(handle1,expert_short_name,ExtTerminal1Symbol,ExtTerminal1Timeframes,ExtDeposit, ExtLeverage,ExtTerminaTick,ExtFromDate,ExtToDate,expert_short_name,3000); WriteBlockTester(handle2,expert_short_name,ExtTerminal2Symbol,ExtTerminal2Timeframes,ExtDeposit, ExtLeverage,ExtTerminaTick,ExtFromDate,ExtToDate,expert_short_name,3001); WriteBlockTester(handle3,expert_short_name,ExtTerminal3Symbol,ExtTerminal3Timeframes,ExtDeposit, ExtLeverage,ExtTerminaTick,ExtFromDate,ExtToDate,expert_short_name,3002); WriteBlockTester(handle4,expert_short_name,ExtTerminal4Symbol,ExtTerminal4Timeframes,ExtDeposit, ExtLeverage,ExtTerminaTick,ExtFromDate,ExtToDate,expert_short_name,3003); //--- 关闭文件 FileClose(handle1); FileClose(handle2); FileClose(handle3); FileClose(handle4);
之后形成 [Tester] 参数块: 为每个终端准备独有参数 (品种和时间帧) 和公用参数 (测试开始和结束日期, 初始本金, 杠杆)。
创建的文件 myconfiguration1.ini, myconfiguration2.ini, myconfiguration3.ini, 和 myconfiguration4.ini 保存在公用数据文件夹 (TERMINAL_COMMONDATA_PATH\Files\)。这些文件的句柄应被关闭。
3. 解析并编辑所选智能交易系统的 mq5 文件
需要解决的问题:
- 添加包含调用图形分析的文件 (详请参阅 从策略测试器中运行分析图表);
- 将图形分析调用集成到智能交易系统的 OnTester() 函数里。
为什么秘密数字是三?秘密 #1 和 秘密 #2 已在早前的 交易员生存诀窍: 四遍回测优于一遍 里发表。
考虑到以下情况: 终端从命令行启动, 并同时指定配置 ini 文件。在 ini 文件中, 我们指定终端启动时在测试器中启动的智能交易系统的名称。在这种情况下, 我们要记住, 我们指定的智能交易系统名字尚未编译。
秘密 #3。
在写入智能交易系统的名称时必须不能带扩展名。这就是为何它像 文章里那样:
NewsEnable=0 [Tester] Expert=D0E820_test Symbol=GBPAUD
在开始时, 终端首先搜索已完成的文件 (在本篇文章内, 终端将搜索 Expert=D0E820_test.ex5)。只有无法找到已编译文件的情况下, 终端才会开始编译在 ini 文件中指定的智能交易系统。
为此, 在开始编辑所选智能交易系统之前, 我们需要遍历终端的文件夹, 删除所选智能交易系统的编译版本 (在本例中我们需要删除文件 'D0E820_test.ex5')。我们将使用 DeleteFileW Win API 函数删除文件:
return(INIT_FAILED);
//--- 删除所有文件: expert_short_name+".ex5"
ResetLastError();
string common_data_path=TerminalInfoString(TERMINAL_COMMONDATA_PATH); // 公共数据文件夹路径
//---
string edited_expert=common_data_path+"\\Files\\"+expert_short_name+".mq5";
//--- 删除智能系统文件名+".ex5" 文件
string compiled_expert=expert_short_name+".ex5";
DeleteFileW(slaveTerminalDataPath1+"\\MQL5\\Experts\\"+compiled_expert);
DeleteFileW(slaveTerminalDataPath2+"\\MQL5\\Experts\\"+compiled_expert);
DeleteFileW(slaveTerminalDataPath3+"\\MQL5\\Experts\\"+compiled_expert);
DeleteFileW(slaveTerminalDataPath4+"\\MQL5\\Experts\\"+compiled_expert);
//--- 删除智能系统文件名+".set" 文件
现在有必要删除 *.set 文件。原因是, 如果您编辑所选智能交易系统的输入参数, 测试器仍会以上次运行期间使用的参数开始。所以我们要删除 *.set 文件:
string set_files=expert_short_name+".set";
DeleteFileW(slaveTerminalDataPath1+"\\Tester\\"+set_files);
DeleteFileW(slaveTerminalDataPath2+"\\Tester\\"+set_files);
DeleteFileW(slaveTerminalDataPath3+"\\Tester\\"+set_files);
DeleteFileW(slaveTerminalDataPath4+"\\Tester\\"+set_files);
//--- 删除智能系统文件名+".htm" 文件 (报告)
此外, 删除从属终端文件夹中的测试报告文件:
//--- 删除智能系统文件名+".htm" 文件 (报告)
string file_report=expert_short_name+".htm";
DeleteFileW(slaveTerminalDataPath1+"\\"+file_report);
DeleteFileW(slaveTerminalDataPath2+"\\"+file_report);
DeleteFileW(slaveTerminalDataPath3+"\\"+file_report);
DeleteFileW(slaveTerminalDataPath4+"\\"+file_report);
//--- 在 TERMINAL_COMMONDATA_PATH\Files 文件夹内复制智能系统
if(!CopyFileW(expert_full_name,edited_expert,false))
为什么我们需要删除报告文件?通过删除报告, 我们将能够识别所有从属终端中创建新报告文件的时刻, 然后我们可以解析这些文件来创建多品种测试结果的比较页面。
只有删除已编译的文件之后, 我们才能将所选的智能交易系统文件复制到 TERMINAL_COMMONDATA_PATH 文件夹, 以便使用 MQL5 工具进一步处理文件:
//--- 在 TERMINAL_COMMONDATA_PATH\Files 文件夹里拷贝智能系统
if(!CopyFileW(expert_full_name,edited_expert,false))
{
PrintFormat("引发 CopyFileW 智能系统全名失败的错误: %x",kernel32::GetLastError());
return(INIT_FAILED);
}
//--- 解析智能系统文件
多测试比较 tests.mq5::ParsingEA() 的说明。
一般来说, 我们需要确定智能交易系统文件是否已经包含 "#include <DistributionOfProfits.mqh>"。如果没有, 我们需要在 EA 里添加一行。然而, 可以存在不同的变体:
变体 | 优/劣 |
---|---|
"#include <DistributionOfProfits.mqh>" | 优秀变体 (理想) |
"#include<DistributionOfProfits.mqh>" | 较好 (在此变体里 "#include" 后跟一个制表符, 而非空格) |
"#include <DistributionOfProfits.mqh>" | 较好 (此变体里, 在 "#include" 之前用制表符替代了空格) |
"//#include <DistributionOfProfits.mqh>" | 较差变体 (它就是一个注释) |
还有可能的变体, 当 "#include" 后面并非一个空格, 而是一个制表符或多个空格。因此为搜索创建了以下正则表达式:
"(\\s+?#include|^#include)(.*?)(<DistributionOfProfits.mqh)"
这就是如何解释表达式 (\\s+?#include|^#include): (一个或更多的空格, 非贪婪, 之后 "#include") 或是 (此行开头为 "#include")。使用 NumberRegulars() 函数执行搜索。引入新的变量: "name_Object_CDistributionOfProfits", 我们将用它保存 CDistributionOfProfits 对象的名字。如果我们需要执行复杂的搜索, 这可能是有用的。
//+------------------------------------------------------------------+ //| 插入 #include <DistributionOfProfits.mqh> | //| 插入调用交易的图形分析 | //+------------------------------------------------------------------+ bool ParsingEA() { //--- 查找 #include <DistributionOfProfits.mqh> int number=0; string name_Object_CDistributionOfProfits="ExtDistribution"; // CDistributionOfProfits 对象名 string expressions="(\\s+?#include|^#include)(.*?)(<DistributionOfProfits.mqh)"; if(!NumberRegulars(expert_short_name+".mq5",expressions,number)) return(false); if(number==0) // 正则表达式未发现 { //--- 添加 #include <DistributionOfProfits.mqh> string array[]; ArrayResize(array,2); array[0]="#include <DistributionOfProfits.mqh>"; array[1]="CDistributionOfProfits "+name_Object_CDistributionOfProfits+";"; if(!InsertLine(expert_short_name+".mq5",0,array)) return(false); Print("行 \"#include\" 已插入");
如果未发现字符串, 则我们需要将其插入我们的智能交易系统 (InsertLine() 函数)。操作原理如下: 将智能交易系统逐行读入临时数组。当行数与设置 "position" 匹配时, 将适当的代码片段插入到数组中 (代码片段取自 "text" 数组)。一旦读取完成后, 智能交易系统文件将被删除, 并创建一个具有相同名称的新文件。来自临时数组的信息将写入文件:
//+------------------------------------------------------------------+ //| 在文件中插入一行 | //+------------------------------------------------------------------+ bool InsertLine(const string name_file,const uint position,string &array_text[]) { int handle; int size_arr=ArraySize(array_text); //--- handle=FileOpen(name_file,FILE_READ|FILE_ANSI|FILE_TXT|FILE_COMMON); if(handle==INVALID_HANDLE) { Print("操作 FileOpen 文件 ",name_file," 失败, 错误 ",GetLastError()); return(false); } int line=0; string arr_temp[]; ArrayResize(arr_temp,0,1000); while(!FileIsEnding(handle)) { string str_text=FileReadString(handle,-1); if(line==position) { for(int i=0;i<size_arr;i++) { int size=ArraySize(arr_temp); ArrayResize(arr_temp,size+1,1000); arr_temp[size]=array_text[i]; } } int size=ArraySize(arr_temp); ArrayResize(arr_temp,size+1,1000); arr_temp[size]=str_text; line++; } FileClose(handle); FileDelete(name_file,FILE_COMMON); //--- handle=FileOpen(name_file,FILE_WRITE|FILE_ANSI|FILE_TXT|FILE_COMMON); if(handle==INVALID_HANDLE) { Print("操作 FileOpen 文件 ",name_file," 失败, 错误 ",GetLastError()); return(false); } int size=ArraySize(arr_temp); for(int i=0;i<size;i++) { FileWrite(handle,arr_temp[i]); } FileClose(handle); //--- return(true); }
现在任务变得更加困难, 因为单词 "OnTester" 可以在程序代码中发生许多不同的变化。例如, 最简单的情况是在代码中没有 "OnTester"。经典版本如下:
double OnTester() {
这不是很困难。但开发人员是不同的, 有时我们可以满足以下编程风格:
double OnTester() {
也许这是最困难的情况之一:
/*
//+-------------------------------+
//| |
//+-------------------------------+
double OnTester()
{
...
}
...
*/
所以, 为了找出代码是否包含 OnTetster 函数的声明, 让我们使用下面的正则表达式:
"(\\s+?double|^double)(.+?)(OnTester\\(\\))(.*)" | |
"(\\s+?double" | \\s 一个空格, \\s+ 至少出现一个空格, \\s+? 至少出现一次空格, 非贪婪操作符, \\s+?double 至少出现一个空格, 非贪婪操作符, 以及 "double" 单词。 |
"|" | | or |
"^double)" | 该行以 double 单词开头 |
"(.+?)" | . 任何非新行符号或任何其它 Unicode 字符串的分隔符, .+ 任何非新行符号或任何其它 Unicode 字符串的分隔符, 出现 一次 或多次, .+? 任何非新行符号或任何其它 Unicode 字符串的分隔符, 出现一次或多次, 非贪婪 |
"(OnTester\\(\\))" | OnTester\\(\\) 单词 OnTester() |
"(.*)" | . 任何非新行符号或任何其它 Unicode 字符串的分隔符, .* 任何非新行符号或任何其它 Unicode 字符串的分隔符出现 零次 或多次 |
在最简单情况下当 正则表达式在搜索后返回零, 插入 OnTester() 函数调用:
//--- expressions="(\\s+?double|^double)(.+?)(OnTester\\(\\))(.*)"; if(!NumberRegulars(expert_short_name+".mq5",expressions,number)) return(false); if(number==0) // 正则表达式未发现 { //--- 添加函数 OnTester if(!InsertLine(expert_short_name+".mq5",2, "double OnTester()"+ " {"+ " double ret=0.0;"+ " ExtDistribution.AnalysisTradingHistory(0);"+ " ExtDistribution.ShowDistributionOfProfits();"+ " return(ret);"+ " }")) return(false); Print("已插入行 \"OnTester\""); }
所以, 如果代码既没有 "#include <DistributionOfProfits.mqh>" 也没有 "OnTester()", 源代码将如下 (例如, 如果我们选择 MACD Sample.mq5):
#include <DistributionOfProfits.mqh> CDistributionOfProfits ExtDistribution; double OnTester() { double ret=0.0; ExtDistribution.AnalysisTradingHistory(0); ExtDistribution.ShowDistributionOfProfits(); return(ret); } //+------------------------------------------------------------------+ //| MACD Sample.mq5 | //| 版权所有 2009-2016, MetaQuotes 软件公司| //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "版权所有 2009-2016, MetaQuotes 软件公司" #property link "https://www.mql5.com" #property version "5.50"
代码看起来不是很美观, 然而它会执行其任务。在第 3.1 和 3.2 段中, 我们讨论了简单情况 (最简单的情况) — 当智能交易系统的代码最初既不包含图形分析库声明, 也不包含 OnTester() 函数时。接下来, 我们研究更复杂的情况, 其中代码最初包含图形分析库, 和/或 OnTester() 函数的声明。
3.4. 复杂情况: 代码已经包括 DistributionOfProfits.mqh 和/或 OnTester()
在AdvancedSearch() 函数中执行复杂搜索:
//+------------------------------------------------------------------+ //| 高级搜素 | //| only_ontester=true | //| - 仅搜索 OnTester() 函数 | //| only_ontester=false | //| - 搜索 #include <DistributionOfProfits.mqh> | //| and function OnTester() | //+------------------------------------------------------------------+ bool AdvancedSearch(const string name_file,const string name_object,const bool only_ontester)
参数:
- name_file — 智能交易系统文件名
- name_object — CDistributionOfProfits 类对象的名
- only_ontester — 搜索标志, 如果 only_ontester=true 我们仅搜索 OnTester()。
开始时, 整个文件被读入临时数组
string arr_temp[];
— 所有它将很容易处理
然后顺序调用几个服务代码:
RemovalMultiLineComments() — 在此代码里, 所有多行注释从数组里清除;
RemovalComments() — 单行注释在此删除;
DeleteZeroLine() — 所有零长度的行从数组里清除。
如果 only_ontester==false, 我们搜索 "#include <DistributionOfProfits.mqh> ", 这会使用 FindInclude() 函数来完成:
FindInclude() 搜索出现的 "#include <DistributionOfProfits.mqh>" 并将行号保存到 "function_position" 变量 (在 3.1 段包含 "#include" 我们使用正则表达式来判断代码已经包括 "#include <DistributionOfProfits.mqh>")。然后尝试找到 "CDistributionOfProfits"。如果找到这一行, 我们从该行获得 "CDistributionOfProfits" 类的变量名。如果未发现这行, 我们需要将它插入到 "function_position" 旁边的位置。
如果 only_ontester==true, 则我们开始搜索 OnTester()。一旦发现, 我们在这行中使用 FindFunctionOnTester() 函数搜索图形分析库调用。
4. 将智能交易系统复制到从属终端的文件夹
智能交易系统在 OnInit() 里复制:
//--- 解析智能系统文件 if(!ParsingEA()) return(INIT_FAILED); //--- 将智能交易系统复制到终端文件夹 ResetLastError(); if(!CopyFileW(edited_expert,slaveTerminalDataPath1+"\\MQL5\\Experts\\"+expert_short_name+".mq5",false)) { PrintFormat("引发 CopyFileW #1 失败的错误: %x",kernel32::GetLastError()); return(INIT_FAILED); } if(!CopyFileW(edited_expert,slaveTerminalDataPath2+"\\MQL5\\Experts\\"+expert_short_name+".mq5",false)) { PrintFormat("引发 CopyFileW #2 失败的错误: %x",kernel32::GetLastError()); return(INIT_FAILED); } if(!CopyFileW(edited_expert,slaveTerminalDataPath3+"\\MQL5\\Experts\\"+expert_short_name+".mq5",false)) { PrintFormat("引发 CopyFileW #3 失败的错误: %x",kernel32::GetLastError()); return(INIT_FAILED); } if(!CopyFileW(edited_expert,slaveTerminalDataPath4+"\\MQL5\\Experts\\"+expert_short_name+".mq5",false)) { PrintFormat("引发 CopyFileW #4 失败的错误: %x",kernel32::GetLastError()); return(INIT_FAILED); }
5. 启动从属终端
在启动从属终端之前, 请检查以下事项: 在所有启动智能交易系统的从属终端中必须使用主终端中的交易账户。此外, 您应该在所有从属终端上允许 DLL:
如果不允许 DLL, 则从属终端将无法运行智能交易系统 (请记住, 我们的智能交易系统主动使用 Win API 调用), 并且以下消息将打印在测试器的 "日志" 栏中:
2016.10.13 11:28:57 Core 1 2016.02.03 00:00:00 DLL loading is not allowed
更多有关的系统函数 ShellExecuteW: ShellExecuteW。在终端启动之间进行暂停, 并且通过 "LaunchSlaveTerminal" 函数进行启动。
if(!CopyFileW(edited_expert,slaveTerminalDataPath4+"\\MQL5\\Experts\\"+expert_short_name+".mq5",false)) { PrintFormat("引发 CopyFileW #4 失败的错误: %x",kernel32::GetLastError()); return(INIT_FAILED); } //--- 启动从属终端 Sleep(ExtSleeping); LaunchSlaveTerminal(ExtInstallationPathTerminal_1,common_data_path+"\\Files\\myconfiguration1.ini"); Sleep(ExtSleeping); LaunchSlaveTerminal(ExtInstallationPathTerminal_2,common_data_path+"\\Files\\myconfiguration2.ini"); Sleep(ExtSleeping); LaunchSlaveTerminal(ExtInstallationPathTerminal_3,common_data_path+"\\Files\\myconfiguration3.ini"); Sleep(ExtSleeping); LaunchSlaveTerminal(ExtInstallationPathTerminal_4,common_data_path+"\\Files\\myconfiguration4.ini"); } //--- return(INIT_SUCCEEDED); }
6. 比较报告
我们已为所选的智能交易系统代码做了如此多的解析工作, 其目的是将与开仓时间有关的仓位图形分析库的调用插入到代码中 (该库在文章 交易员生存诀窍: "静默" 优化或绘制交易分布" 里描述)。此插入代码令从属终端中的每个智能交易系统均可在测试后创建并自动打开以下 html 页面:
早前, 我们在配置文件的 [Tester] 块里添加了 "Report" 参数:
Expert=D0E820_test
Symbol=GBPAUD
Period=PERIOD_H1
Deposit=100000
Leverage=1:100
Model=0
ExecutionMode=0
FromDate=2016.10.03
ToDate=2016.10.15
ForwardMode=0
Report=D0E820_test
ReplaceReport=1
Port=3000
ShutdownTerminal=0
这是终端在测试完成后将保存报告的文件名 (D0E820_test.htm)。从该报告 (对于每个从属终端) 里, 我们需要提取以下数据: 智能交易系统进行测试的品名和周期, 来自 "回测" 块和余额图的值。基于所有从属终端的结果生成以下比较报告:
从属终端在其数据目录的根文件夹中保存测试报告 (在此情况下为 htm 格式)。这意味着我们的智能交易系统需要运行在从属终端上, 然后定期在这些目录中查找测试报告文件。一旦找到所有四份报告, 我们就可以继续生成一份合并的比较报告。
首先我们引入 "find_report" 标志允许 EA 开始搜索报告文件:
//---
string arr_path[][2];
bool find_report=false;
//+------------------------------------------------------------------+
//| 开始应用程序的枚举 |
//+------------------------------------------------------------------+
enum EnSWParam
此外, 我们添加 OnTimer() 函数:
{
//--- 创建计时器
EventSetTimer(9);
ArrayFree(arr_path);
find_report=false; // true - 标志允许搜索报告
if(!FindDataFolders(arr_path))
return(INIT_SUCCEEDED);
//| 计时器函数 |
//+------------------------------------------------------------------+
void OnTimer()
{
//---
if(!find_report)
return;
}
我们将在 OnTimer() 中搜索 "智能系统名"+".htm" 文件。这是一个单层搜索, 只在每个从属终端的数据目录的根文件夹中执行。为达此目的, 我们将使用 ListingFilesDirectory.mqh::FindFile() 函数。
由于搜索是在 "沙盒" 之外执行的, 我们将使用 Win API 函数 FindFirstFileW。有关 FindFirstFileW 的更多信息, 请参阅前篇文章:
在此代码中, 我们 比较结果文件名, 若它匹配指定的名称, 将返回 true; 搜索句柄应先行关闭:
//| 查找文件 |
//+------------------------------------------------------------------+
bool FindFile(const string path,const string name)
{
//---
WIN32_FIND_DATA ffd;
long hFirstFind_0;
ArrayInitialize(ffd.cFileName,0);
ArrayInitialize(ffd.cAlternateFileName,0);
//--- 阶段搜索 №0。
string filter_0=path+"\\*.*"; // filter_0==C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\*.*
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(false);
}
//--- 目录之内所有文件清单的有关信息
bool rezult=0;
do
{
string name_0="";
for(int i=0;i<MAX_PATH;i++)
{
name_0+=ShortToString(ffd.cFileName[i]);
}
if(name_0==name)
{
WinAPI_FindClose(hFirstFind_0);
return(true);
}
ArrayInitialize(ffd.cFileName,0);
ArrayInitialize(ffd.cAlternateFileName,0);
ResetLastError();
rezult=WinAPI_FindNextFile(hFirstFind_0,ffd);
}
while(rezult!=0); //if(hFirstFind_1==INVALID_HANDLE), we appear here
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);
//---
return(false);
}
智能交易系统检查所有四个从属终端的文件夹中内报告文件是否可用: 这表示所有终端已完成测试。
现在我们需要处理这些信息。这四个报告文件和它们的四个图形文件 (余额图表) 将被复制到沙箱 TERMINAL_COMMONDATA_PATH\Files:
string path=TerminalInfoString(TERMINAL_COMMONDATA_PATH);
if(!CopyFileW(slaveTerminalDataPath1+"\\"+expert_short_name+".htm",path+"\\Files\\"+"report_1"+".htm",false))
{
PrintFormat("失败错误: %x",kernel32::GetLastError());
return;
}
if(!CopyFileW(slaveTerminalDataPath1+"\\"+expert_short_name+".png",path+"\\Files\\"+"report_1"+".png",false))
{
PrintFormat("失败错误: %x",kernel32::GetLastError());
return;
}
if(!CopyFileW(slaveTerminalDataPath2+"\\"+expert_short_name+".htm",path+"\\Files\\"+"report_2"+".htm",false))
{
PrintFormat("失败错误: %x",kernel32::GetLastError());
return;
}
if(!CopyFileW(slaveTerminalDataPath2+"\\"+expert_short_name+".png",path+"\\Files\\"+"report_2"+".png",false))
{
PrintFormat("失败错误: %x",kernel32::GetLastError());
return;
}
if(!CopyFileW(slaveTerminalDataPath3+"\\"+expert_short_name+".htm",path+"\\Files\\"+"report_3"+".htm",false))
{
PrintFormat("失败错误: %x",kernel32::GetLastError());
return;
}
if(!CopyFileW(slaveTerminalDataPath3+"\\"+expert_short_name+".png",path+"\\Files\\"+"report_3"+".png",false))
{
PrintFormat("失败错误: %x",kernel32::GetLastError());
return;
}
if(!CopyFileW(slaveTerminalDataPath4+"\\"+expert_short_name+".htm",path+"\\Files\\"+"report_4"+".htm",false))
{
PrintFormat("失败错误: %x",kernel32::GetLastError());
return;
}
if(!CopyFileW(slaveTerminalDataPath4+"\\"+expert_short_name+".png",path+"\\Files\\"+"report_4"+".png",false))
{
PrintFormat("失败错误: %x",kernel32::GetLastError());
return;
}
但是生成的报表文件包含大量不必要的信息, 这令正则表达式的使用变得非常复杂。这就是为什么在 "Compare multiple tests.mq5::ParsingReportToArray" 函数中执行一些操作, 之后文件将看起来像这样:
该文件更便于使用正则表达式 "(>)(.*?)(<)", 即在 ">" 和 "<" 之间搜索任何符号, 这些符号的数字以零开始。
使用正则表达式的结果将添加到四个数组中: arr_report_1, arr_report_2, arr_report_3 和 arr_report_4。来自这些数组的信息将用于生成最终比较报告的代码。在创建最终报告之后, 我们调用 'ShellExecuteW' 函数 (更多有关 ShellExecuteW 参阅 此处) 并启动浏览器:
这将打开一个浏览器页面, 我们可在其中比较四个不同品种的智能交易系统测试的结果。
结论
在本文中, 我们讨论了另外一种评估四个不同品种的智能交易系统测试结果的方法。在此情况下, 在四个终端上同时执行四个品种的并行测试, 之后我们接收这些测试结果的汇总表。