English Русский Español Deutsch 日本語 Português
利用 HTML 报告分析交易结果

利用 HTML 报告分析交易结果

MetaTrader 5统计分析 | 21 二月 2019, 07:52
1 836 0
Dmitry Fedoseev
Dmitry Fedoseev

概述

如果交易者试图吸引投资者,而他们很可能会要检查他或她的交易成果。 所以交易者应该能够提供交易历史来展示成果。 MetaTrader 5 允许将交易历史保存到文件(工具箱 — 交易选项卡 — 关联菜单 — 报告)。 报告可以保存为 XLSX(在 Microsoft Excel 中进行分析),和可以在任何浏览器中查看的 HTML文件。 第二种选项显然更受欢迎,因为有些用户可能没有 Excel,而所有人都有浏览器。 因此,含有交易结果的 HTML 报告更适合潜在投资者。

除了此类报告中提供的标准衡量值之外,您可能还希望利用从报告中提取的交易数据来自行计算隐秘数值。 在本文中,我们将研究从 HTML 报告中提取数据的方法。 首先,我们将分析报告的结构,然后我们将编写一个函数来解析它们。 报告的名称会传递给该函数。 然后该函数返回包含相关数据的结构。 此结构能够直接访问任何报告数值。 使用此结构,您能够按照任意期望度量值轻松迅速地生成自己的报告。

除交易报告外,MetaTrader 5 还能够保存智能交易系统的测试和优化报告。 与交易历史类似,测试报告可以按照两种格式保存:XLSX 和 HTML,而优化报告则以 XML 格式保存。

在本文中,我们将研究 HTML 测试报告,XML 优化报告和 HTML 交易历史报告。

HTML 和 XML 文件

HTML 文件实际上是一个由文本(可显示数据)和标签组成的文本文件,它指明数据应如何显示(图例 1)。 所有标签都以字符 "<" 开头,以 ">" 结尾。 例如,标签 <br> 意为其随后的文本应以新行开头,而 <p> 意为文本应以新段落开头(不仅是新行,且在一个空行之后)。 其他属性可以位于标签内,例如 <p color="red"> 意为标签后面的文本应以新段落开头,并以红色书写。 

记事本中的 HTML 文件
图例 1 在记事本中打开 HTML 文件的一个片段

为了取消标签动作,使用了一个封闭标签,它与起始标签相似,并且在开头还有一个斜杠 "/"。 例如,</p> 是段落的封闭标签。 某些标签在使用时无需对应的封闭,例如 <br>。 某些标签可以选择性使用封闭标签。 新段落可以在不封闭前一段落的情况下起始。 然而,如果在标签内部使用了颜色属性,就像上面带有 <p> 的示例一样,则必须使用封闭标签来取消未来的文本涂色。 有些标签需要封闭,例如 <table>。 table 应始终以封闭标签 </table> 结尾。 table 由 <tr> 开头且由封闭标签 </tr> 结束所指示的行组成。 行内的单元格由 <td> 和 </ td> 定义。 有时候单元格会用 <th> 标签(标题单元格)。 同一个表格可能二者皆有,即 <td> 标签和 <th> 标签的单元格。 行和单元格封闭标签是必需的。

目前,大多数 html 属性实际上都没用过。 而是用了一个“style(样式)”属性,其中描述了元素外观。 例如:<p style="color: red"> 表示红色的段落,但这也并非一个非常流行的方法。 最常见的方法是使用“class”属性,其中只指定样式类名称,例如 <p class="small">,而类本身(样式描述)位于 HTML 文档的开头,或单独的 CSS 文件(层叠样式表)。 

XML 文件 (图例 2) 与 HTML 非常相似。 主要区别在于 HTML 支持严格限制的标签集,而 XML 允许扩展标签集,并添加自定义标签。

记事本里的 XML 文件
图例 2 在记事本中打开的 XML 文件的一个片段

HTML 的主要目的是数据显示,这就是为什么它有一组标准的标签。 由于这个 HTML 文档在不同的浏览器中具有几乎相同的外观。 XML 的主要目的是保存和传递数据,因此支持任意数据结构的可能性很重要,而使用此文件的用户必须了解这样做的目的和需要提取的数据。

字符串函数或正则表达式

通常,正则表达式用来从文本中提取数据。 在 代码库 中,您可以找到 RegularExpressions 函数库来处理表达式。 您还可以查看如何使用它们的说明文章:“交易者的正则表达式”。 当然,正则表达式构成了解析文本数据的非常强大且方便的工具。 如果您经常需要处理数据解析,且执行不同的任务时,肯定需要使用正则表达式。

不过,正则表达式有一些缺点:您需要在使用它们之前正确研究表达式。 与正则表达式相关的整体思想非常广泛。 您不能指望“在需要时翻查参考”来使用它们。 你必须彻底研究这个理论并掌握实用技巧。 我认为使用正则表达式所需的思维方式与解决普通编程任务所需的思维方式大不相同。 即使有能力使用正则表达式,在切换到使用正则表达式再返回时也可能出现障碍。 

如果数据解析任务不复杂且并不频繁,则可以使用标准 字符串函数。 甚或,有一种观点认为字符串函数运行得更快。 最可能的是,速度取决于任务的类型,以及应用正则表达式的条件。 然而,字符串函数为数据解析任务提供了一个很好的解决方案。

在语言参考中可用的所有字符串函数中,我们只需要少量几个:Stringring()StringSubstr()StringReplace()。 我们可能还需要一些非常简单的函数,例如 StringLen()StringTrimLeft()StringTrimRight()

测试器报告

在测试报告中包含了很大的数据量,因此我们将从头开始。 这将令我们能够理解任务,并处理可能出现的所有障碍。 在创建本文时,我使用了默认参数的 ExpertMACD EA 测试报告(报告附在下面)。 在浏览器中打开报告以便查看我们正在处理的内容(图例 3)。

HTML 报告部分
图例 3 HTML 格式的测试报告,在浏览器中打开。 红色行蓝色文本显示主要报告部分

报告数据分为几个部分:报告名称,参数,结果,图表,订单成交,总计。  

每个浏览器都有一个查看页面代码的命令:右键单击页面打开关联菜单,然后选择“查看页面源码”或类似命令。 

通过对源代码的直观检查,我们可以发现所有报告数据都排布在表中(<table> 标签)。 测试器报告有两个表格。 第一个表格包含一般数据:从报告名称,直到“Average position holding time(平均持仓时间)”值,即直到订单部分。 第二个表格包含有关订单,成交和最终存款状态的数据。 使用“colspan”属性将不同类型的数据置于一个表格中,该属性将若干个单元格合并在一起。 常用参数的名称及其数值位于表格的不同单元格中,有时这些单元格出现在同一行中,有时它们位于不同的行中。

一个重要的时刻是“id”属性的可用性,它们是标签的唯一标识符。 id 值可用于查找包含所需数据的单元格。 但是,“id”属性未使用。 这就是我们通过计算行和单元格来查找参数的原因。 例如,“策略测试器报告”名称位于第一个表格,第一行和第一个单元格中。 

第二个表格中的订单和成交数量在所有报告中都会有所不同,因此我们需要查找订单的结束和成交的开始。 成交之后跟随一个单元格的行,然后是标题行 — 这是分隔数据的指示。 现在我们不会详述行数和单元格的数量。 稍后将介绍一种便洁的方法。

所有数据都位于表格单元格中,因此我们在初始阶段需要做的就是提取表格、行和单元格。

我们来分析交易历史和优化报告。 历史报告与测试报告非常相似,除了附加的最终存款状态部分(图例 4)。

HTML 格式的交易历史记录报告,在浏览器中打开。

图例 4 HTML 格式的交易历史记录报告,在浏览器中打开。 红色行蓝色文本显示主要报告部分

通过检查交易历史 HTML 代码,我们可以看到 单元格不仅由 <td> 标签定义,还由<th> 定义,它代表标题单元格。 此外,所有数据都位于一个表格之中。

优化报告非常简单 — 它在一个表格中只有一个数据部分。 从它的源代码中,我们看到它是使用 <Table> 标签创建的,行是用 <Row> 定义的,单元格是用 <Cell> 定义的(所有标签都与相应的封闭标签一起使用)。

文件加载

在我们可以使用报告数据执行任何操作之前,我们需要从文件中读取它们。 我们使用以下函数,将整个文件内容返回到一个字符串变量中:

bool FileGetContent(string aFileName,string & aContent){
   int h=FileOpen(aFileName,FILE_READ|FILE_TXT);
   if(h==-1)return(false);
   aContent="";
   while(!FileIsEnding(h)){
      aContent=aContent+FileReadString(h);
   }
   FileClose(h);
   return(true);
}

传递给函数的第一个参数是报告文件的名称。 第二个参数是包含文件内容的变量。 该函数根据结果返回 true/false。

提取表格

我们使用两种结构来定位表格数据。 包含一个表格行的结构:

struct Str{
   string td[];
};

td[] 数组的每个元素将包含一个单元格的内容。

包含整个表格(所有行)的结构:

struct STable{
   Str tr[];
};

将从报告中提取数据如下:首先,我们要找到表格起始标签的开始位置。 由于标签可能含有属性,因此我们仅搜索标签开头:“<table”。 找到起始标签的开头后,我们继续找到它的结尾,“>”。 然后搜索表格结束,即封闭标签“</ table>”。 这很容易,因为报告不含嵌套表格,即每个起始标签后跟一个封闭标签。

在找到表格之后,我们针对表格行重复相同动作,这次使用“<tr”作为起始,“</ tr”作为结束。 然后,在每个表格行中,我们将查找单元格,这次是“<td”起始,并用 </td> 结束。 表格行的任务有点复杂,因为单元格可以同时包含 <td> 和 <th> 标签。 出于此原因,我们将使用自定义 TagFind() 函数来替代 StringFind():

int TagFind(string aContent,string & aTags[],int aStart,string & aTag){
   int rp=-1;
   for(int i=0;i<ArraySize(aTags);i++){
      int p=StringFind(aContent,"<"+aTags[i],aStart);
      if(p!=-1){
         if(rp==-1){
            rp=p;
            aTag=aTags[i];
         }
         else{
            if(p<rp){
               rp=p;
               aTag=aTags[i];
            }
         }      
      }
   }
   return(rp);
}

函数参数:

  • string aContent — 我们要搜索的字符串;
  • string & aTags[] — 存储标签的数组;
  • int aStart — 搜索的起始位置;
  • string & aTag — 找到的标签通过引用返回。

与 StringFind() 不同,是将数组传递给 TagFind() 函数,而非要搜索的字符串。 起始“<”被添加到函数中的搜索字符串。 该函数返回最近标签的位置。

使用 TagFind(),我们将始终搜索起始和封闭标记。 它们之间发现的所有内容都将被收集到一个数组中:

int TagsToArray(string aContent,string & aTags[],string & aArray[]){
   ArrayResize(aArray,0);
   int e,s=0;
   string tag;
   while((s=TagFind(aContent,aTags,s,tag))!=-1 && !IsStopped()){  
      s=StringFind(aContent,">",s);
      if(s==-1)break;
      s++; 
      e=StringFind(aContent,"</"+tag,s);   
      if(e==-1)break;  
      ArrayResize(aArray,ArraySize(aArray)+1);
      aArray[ArraySize(aArray)-1]=StringSubstr(aContent,s,e-s);  
   }
   return(ArraySize(aArray));
}

函数参数:

  • string aContent — 我们要搜索的字符串;
  • string & aTags[] — 存储标签的数组;
  • string aArray[] — 返回包含所有已发现标签内容的数组的引用。

只有在解析单元格时,才会将搜索到的标签数组传递给 TagsToArray()。 这就是为什么,对于所有其他情况,我们将使用通常的字符串参数来编写函数模拟:

int TagsToArray(string aContent,string aTag,string & aArray[]){
   string Tags[1];
   Tags[0]=aTag;
   return(TagsToArray(aContent,Tags,aArray));
}

函数参数:

  • string aContent — 我们要搜索的字符串;
  • string & aTag — 搜索的标签;
  • string aArray[] — 返回包含所有已发现标签内容的数组的引用。

现在我们继续完善可提取表格内容的函数。 含有已解析文件名称的字符串参数 aFileName 将传递给该函数。 函数中用到一个局部字符串变量保存文件内容,以及一个局部数组保存 STable 结构:

STable tables[];
string FileContent;

使用 FileGetContent 函数获取整个报表内容:

if(!FileGetContent(aFileName,FileContent)){
   return(true);
}

这是辅助变量:

string tags[]={"td","th"};
string ttmp[],trtmp[],tdtmp[];
int tcnt,trcnt,tdcnt;

'tags' 数组中提供了单元格标签的可能选项。 表格,行和单元格的内容将分别放置到临时字符串数组 ttmp[],trtmp[] 和 tdtmp[] 当中。 

从表格中检索数据:

tcnt=TagsToArray(FileContent,"table",ttmp);
ArrayResize(tables,tcnt);

循环遍历所有表格并提取行,然后从行中提取单元格:

for(int i=0;i<tcnt;i++){
   trcnt=TagsToArray(ttmp[i],"tr",trtmp);
   ArrayResize(tables[i].tr,trcnt);      
   for(int j=0;j<trcnt;j++){         
      tdcnt=TagsToArray(trtmp[j],tags,tdtmp);
      ArrayResize(tables[i].tr[j].td,tdcnt);
      for(int k=0;k<tdcnt;k++){  
         tables[i].tr[j].td[k]=RemoveTags(tdtmp[k]);
      }
   }
} 

首先接收表格,行和单元格并保存到临时数组,然后将单元格数组复制到结构中。 在复制期间中,单元格由 RemoveTags() 函数过滤。 表格单元格可以有嵌套标签。 RemoveTags() 会删除它们,只留下必需的标签。

RemoveTags() 函数:

string RemoveTags(string aStr){
   string rstr="";
   int e,s=0;
   while((e=StringFind(aStr,"<",s))!=-1){
      if(e>s){

         rstr=rstr+StringSubstr(aStr,s,e-s);
      }
      s=StringFind(aStr,">",e);
      if(s==-1)break;
      s++;
   }
   if(s!=-1){
      rstr=rstr+StringSubstr(aStr,s,StringLen(aStr)-s);
   }
   StringReplace(rstr,"&nbsp;"," ");
   while(StringReplace(rstr,"  "," ")>0);
   StringTrimLeft(rstr);
   StringTrimRight(rstr);
   return(rstr);
}

我们研究一下 RemoveTags() 函数。 “s”变量位于所用数据的开头。 它的数据首先等于 0,因为数据可以从行首开始。 在“while”循环中搜索起始尖括号“<”,意即标签起始。 找到起始标签时,从“s”变量指定的位置,直到发现关键字的位置,之间所有数据都将复制到“rstr”变量中。 之后,搜索标签的结尾,即下一个新的有效数据的起始。 在循环之后,如果“s”变量的值不等于 -1(这意味着字符串以有效数据结尾,但它们尚未被复制),则数据被复制到“rstr”变量。 在函数结束时,空格指代符 &nbsp; 用一个简单的空格替换,同时删除重复的空格以及字符串开头和结尾的空格。

在这一步,我们的“tables”数组结构由纯表格数据填充。 我们将此数组保存到文本文件中。 在保存时,我们为表格,行和单元格设置数量(数据保存到文件 1.txt):

int h=FileOpen("1.txt",FILE_TXT|FILE_WRITE);

for(int i=0;i<ArraySize(tables);i++){
   FileWriteString(h,"table "+(string)i+"\n");
   for(int j=0;j<ArraySize(tables[i].tr);j++){      
      FileWriteString(h,"   tr "+(string)j+"\n");         
      for(int k=0;k<ArraySize(tables[i].tr[j].td);k++){
         FileWriteString(h,"      td "+(string)k+": "+tables[i].tr[j].td[k]+"\n");
      }
   }
}

FileClose(h);

通过此文件,我们可以轻松了解数据所在的单元格。 以下是文件片段:

table 0
   tr 0
      td 0: Strategy Test report
   tr 1
      td 0: IMPACT-Demo (Build 1940)
   tr 2
      td 0: 
   tr 3
      td 0: Settings
   tr 4
      td 0: Expert Advisor:
      td 1: ExpertMACD
   tr 5
      td 0: Symbol:
      td 1: USDCHF
   tr 6
      td 0: Period:
      td 1: H1 (2018.11.01 - 2018.12.01)
   tr 7
      td 0: Parameters:
      td 1: Inp_Expert_Title=ExpertMACD
   tr 8
      td 0: 
      td 1: Inp_Signal_MACD_PeriodFast=12
   tr 9
      td 0: 
      td 1: Inp_Signal_MACD_PeriodSlow=24
   tr 10
      td 0: 
      td 1: Inp_Signal_MACD_PeriodSignal=9
   tr 11

报告数据的结构

现在我们有一些无聊的例行公事:我们需要编写一个与报表数据相对应的结构,并使用 tables 数组中的数据填充此结构。 该报告分为几个部分。 所以,我们将使用若干种结构来组合成一种通用结构。

Settings 部分的结构:

struct SSettings{
   string Expert;
   string Symbol;
   string Period;
   string Inputs;
   string Broker;
   string Currency;
   string InitialDeposit;
   string Leverage;
};

结构字段的目的顾名思义。 结构的所有字段均包含与报告中显示的数据完全相同的数据。 智能交易系统参数列表将位于一个字符串变量“Inputs”当中。

Results 数据部分的结构:

struct SResults{
   string HistoryQuality;
   string Bars;
   string Ticks;
   string Symbols;
   string TotalNetProfit;
   string BalanceDrawdownAbsolute;
   string EquityDrawdownAbsolute;
   string GrossProfit;
   string BalanceDrawdownMaximal;
   string EquityDrawdownMaximal;
   string GrossLoss;
   string BalanceDrawdownRelative;
   string EquityDrawdownRelative;
   string ProfitFactor;
   string ExpectedPayoff;
   string MarginLevel;
   string RecoveryFactor;
   string SharpeRatio;
   string ZScore;
   string AHPR;
   string LRCorrelation;
   string OnTesterResult;
   string GHPR;
   string LRStandardError;
   string TotalTrades;
   string ShortTrades_won_pers;
   string LongTrades_won_perc;
   string TotalDeals;
   string ProfitTrades_perc_of_total;
   string LossTrades_perc_of_total;
   string LargestProfitTrade;
   string LargestLossTrade;
   string AverageProfitTrade;
   string AverageLossTrade;
   string MaximumConsecutiveWins_cur;
   string MaximumConsecutiveLosses_cur;
   string MaximalConsecutiveProfit_count;
   string MaximalConsecutiveLoss_count;
   string AverageConsecutiveWins;
   string AverageConsecutiveLosses;
   string Correlation_Profits_MFE;
   string Correlation_Profits_MAE;
   string Correlation_MFE_MAE;      
   string MinimalPositionHoldingTime;
   string MaximalPositionHoldingTime;
   string AveragePositionHoldingTime;
};

有关单笔订单的数据结构:

struct SOrder{
   string OpenTime;
   string Order;
   string Symbol;
   string Type;
   string Volume;
   string Price;
   string SL;
   string TP;
   string Time;
   string State;
   string Comment;
};

有关单笔成交的数据结构:

struct SDeal{
   string Time;
   string Deal;
   string Symbol;
   string Type;
   string Direction;
   string Volume;
   string Price;
   string Order;
   string Commission;
   string Swap;
   string Profit;
   string Balance;
   string Comment;
};

最终存款状态的结构:

struct SSummary{
   string Commission;
   string Swap;
   string Profit;
   string Balance;
};

一般结构:

struct STestingReport{
   SSettings Settings;
   SResults Results;
   SOrder Orders[];
   SDeal Deals[];
   SSummary Summary;
};

SOrder 和 SDeals 结构数组用于订单和成交。

用数据填充结构

这一点例行工作也需要注意。 查看以前收到的含有表格数据的文本文件,并填写结构:

aTestingReport.Settings.Expert=tables[0].tr[4].td[1];
aTestingReport.Settings.Symbol=tables[0].tr[5].td[1];
aTestingReport.Settings.Period=tables[0].tr[6].td[1];
aTestingReport.Settings.Inputs=tables[0].tr[7].td[1];

在上述代码示例的最后一行中,为 Inputs 字段分配了一个值,但之后该字段将只存储一个参数的数据。 然后收集其他参数。 这些参数的位置从第 8 行开始,而每个参数行中的第一个单元格为空。 只要行中的第一个单元格为空,循环就会执行:

int i=8;
while(i<ArraySize(tables[0].tr) && tables[0].tr[i].td[0]==""){
   aTestingReport.Settings.Inputs=aTestingReport.Settings.Inputs+", "+tables[0].tr[i].td[1];
   i++;
}

以下是完整的测试报告解析函数:

bool TestingHTMLReportToStruct(string aFileName,STestingReport & aTestingReport){

   STable tables[];

   string FileContent;
   
   if(!FileGetContent(aFileName,FileContent)){
      return(true);
   }

   string tags[]={"td","th"};
   string ttmp[],trtmp[],tdtmp[];
   int tcnt,trcnt,tdcnt;
   
   tcnt=TagsToArray(FileContent,"table",ttmp);

   ArrayResize(tables,tcnt);
   
   for(int i=0;i<tcnt;i++){
      trcnt=TagsToArray(ttmp[i],"tr",trtmp);
      ArrayResize(tables[i].tr,trcnt);      
      for(int j=0;j<trcnt;j++){         
         tdcnt=TagsToArray(trtmp[j],tags,tdtmp);
         ArrayResize(tables[i].tr[j].td,tdcnt);
         for(int k=0;k<tdcnt;k++){  
            tables[i].tr[j].td[k]=RemoveTags(tdtmp[k]);
         }
      }
   }   
   
   // 设置部分
   
   aTestingReport.Settings.Expert=tables[0].tr[4].td[1];
   aTestingReport.Settings.Symbol=tables[0].tr[5].td[1];
   aTestingReport.Settings.Period=tables[0].tr[6].td[1];
   aTestingReport.Settings.Inputs=tables[0].tr[7].td[1];
   int i=8;
   while(i<ArraySize(tables[0].tr) && tables[0].tr[i].td[0]==""){
      aTestingReport.Settings.Inputs=aTestingReport.Settings.Inputs+", "+tables[0].tr[i].td[1];
      i++;
   }
   aTestingReport.Settings.Broker=tables[0].tr[i++].td[1];
   aTestingReport.Settings.Currency=tables[0].tr[i++].td[1];  
   aTestingReport.Settings.InitialDeposit=tables[0].tr[i++].td[1];
   aTestingReport.Settings.Leverage=tables[0].tr[i++].td[1];   
   
   // 结果部分
   
   i+=2;
   aTestingReport.Results.HistoryQuality=tables[0].tr[i++].td[1];
   aTestingReport.Results.Bars=tables[0].tr[i].td[1];
   aTestingReport.Results.Ticks=tables[0].tr[i].td[3];
   aTestingReport.Results.Symbols=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.TotalNetProfit=tables[0].tr[i].td[1];
   aTestingReport.Results.BalanceDrawdownAbsolute=tables[0].tr[i].td[3];
   aTestingReport.Results.EquityDrawdownAbsolute=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.GrossProfit=tables[0].tr[i].td[1];
   aTestingReport.Results.BalanceDrawdownMaximal=tables[0].tr[i].td[3];
   aTestingReport.Results.EquityDrawdownMaximal=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.GrossLoss=tables[0].tr[i].td[1];
   aTestingReport.Results.BalanceDrawdownRelative=tables[0].tr[i].td[3];
   aTestingReport.Results.EquityDrawdownRelative=tables[0].tr[i].td[5];
   i+=2;
   aTestingReport.Results.ProfitFactor=tables[0].tr[i].td[1];
   aTestingReport.Results.ExpectedPayoff=tables[0].tr[i].td[3];
   aTestingReport.Results.MarginLevel=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.RecoveryFactor=tables[0].tr[i].td[1];
   aTestingReport.Results.SharpeRatio=tables[0].tr[i].td[3];
   aTestingReport.Results.ZScore=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.AHPR=tables[0].tr[i].td[1];
   aTestingReport.Results.LRCorrelation=tables[0].tr[i].td[3];
   aTestingReport.Results.tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.GHPR=tables[0].tr[i].td[1];
   aTestingReport.Results.LRStandardError=tables[0].tr[i].td[3];
   i+=2;
   aTestingReport.Results.TotalTrades=tables[0].tr[i].td[1];
   aTestingReport.Results.ShortTrades_won_pers=tables[0].tr[i].td[3];
   aTestingReport.Results.LongTrades_won_perc=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.TotalDeals=tables[0].tr[i].td[1];
   aTestingReport.Results.ProfitTrades_perc_of_total=tables[0].tr[i].td[3];
   aTestingReport.Results.LossTrades_perc_of_total=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.LargestProfitTrade=tables[0].tr[i].td[2];
   aTestingReport.Results.LargestLossTrade=tables[0].tr[i].td[4];
   i++;
   aTestingReport.Results.AverageProfitTrade=tables[0].tr[i].td[2];
   aTestingReport.Results.AverageLossTrade=tables[0].tr[i].td[4];
   i++;
   aTestingReport.Results.MaximumConsecutiveWins_cur=tables[0].tr[i].td[2];
   aTestingReport.Results.MaximumConsecutiveLosses_cur=tables[0].tr[i].td[4];
   i++;
   aTestingReport.Results.MaximalConsecutiveProfit_count=tables[0].tr[i].td[2];
   aTestingReport.Results.MaximalConsecutiveLoss_count=tables[0].tr[i].td[4];
   i++;
   aTestingReport.Results.AverageConsecutiveWins=tables[0].tr[i].td[2];
   aTestingReport.Results.AverageConsecutiveLosses=tables[0].tr[i].td[4];    
   i+=6;
   aTestingReport.Results.Correlation_Profits_MFE=tables[0].tr[i].td[1];
   aTestingReport.Results.Correlation_Profits_MAE=tables[0].tr[i].td[3];
   aTestingReport.Results.Correlation_MFE_MAE=tables[0].tr[i].td[5];    
   i+=3;
   aTestingReport.Results.MinimalPositionHoldingTime=tables[0].tr[i].td[1];
   aTestingReport.Results.MaximalPositionHoldingTime=tables[0].tr[i].td[3];
   aTestingReport.Results.AveragePositionHoldingTime=tables[0].tr[i].td[5];   
   
   // 订单

   ArrayFree(aTestingReport.Orders);
   int ocnt=0;
   for(i=3;i<ArraySize(tables[1].tr);i++){
      if(ArraySize(tables[1].tr[i].td)==1){
         break;
      }   
      ArrayResize(aTestingReport.Orders,ocnt+1);
      aTestingReport.Orders[ocnt].OpenTime=tables[1].tr[i].td[0];
      aTestingReport.Orders[ocnt].Order=tables[1].tr[i].td[1];
      aTestingReport.Orders[ocnt].Symbol=tables[1].tr[i].td[2];
      aTestingReport.Orders[ocnt].Type=tables[1].tr[i].td[3];
      aTestingReport.Orders[ocnt].Volume=tables[1].tr[i].td[4];
      aTestingReport.Orders[ocnt].Price=tables[1].tr[i].td[5];
      aTestingReport.Orders[ocnt].SL=tables[1].tr[i].td[6];
      aTestingReport.Orders[ocnt].TP=tables[1].tr[i].td[7];
      aTestingReport.Orders[ocnt].Time=tables[1].tr[i].td[8];
      aTestingReport.Orders[ocnt].State=tables[1].tr[i].td[9];
      aTestingReport.Orders[ocnt].Comment=tables[1].tr[i].td[10];      
      ocnt++;
   }
   
   // 成交
   
   i+=3;
   ArrayFree(aTestingReport.Deals);
   int dcnt=0;
   for(;i<ArraySize(tables[1].tr);i++){
      if(ArraySize(tables[1].tr[i].td)!=13){
         if(ArraySize(tables[1].tr[i].td)==6){
            aTestingReport.Summary.Commission=tables[1].tr[i].td[1];
            aTestingReport.Summary.Swap=tables[1].tr[i].td[2];
            aTestingReport.Summary.Profit=tables[1].tr[i].td[3];
            aTestingReport.Summary.Balance=tables[1].tr[i].td[4];            
         }
         break;
      }   
      ArrayResize(aTestingReport.Deals,dcnt+1);   
      aTestingReport.Deals[dcnt].Time=tables[1].tr[i].td[0];
      aTestingReport.Deals[dcnt].Deal=tables[1].tr[i].td[1];
      aTestingReport.Deals[dcnt].Symbol=tables[1].tr[i].td[2];
      aTestingReport.Deals[dcnt].Type=tables[1].tr[i].td[3];
      aTestingReport.Deals[dcnt].Direction=tables[1].tr[i].td[4];
      aTestingReport.Deals[dcnt].Volume=tables[1].tr[i].td[5];
      aTestingReport.Deals[dcnt].Price=tables[1].tr[i].td[6];
      aTestingReport.Deals[dcnt].Order=tables[1].tr[i].td[7];
      aTestingReport.Deals[dcnt].Commission=tables[1].tr[i].td[8];
      aTestingReport.Deals[dcnt].Swap=tables[1].tr[i].td[9];
      aTestingReport.Deals[dcnt].Profit=tables[1].tr[i].td[10];
      aTestingReport.Deals[dcnt].Balance=tables[1].tr[i].td[11];
      aTestingReport.Deals[dcnt].Comment=tables[1].tr[i].td[12];
      dcnt++;
   }
   return(true);
}

报告文件的名称将传递给函数。 此函数中要填充的 STestingReport 结构也通过引用传递。

请注意以“Orders”和“Deals”开头的注释代码部分。 订单列表开头的行号已定义,而订单列表的末尾则要基于检测含有单个单元格的行:

if(ArraySize(tables[1].tr[i].td)==1){
   break;
}  

订单后会跳过三行:

// 成交
   
i+=3;

之后收集成交数据。 交易清单的结尾由含有 6 个单元格的行确定 - 该行包含有关最终存款状态的数据。 在退出循环之前,填充最终存款状态的结构:

if(ArraySize(tables[1].tr[i].td)!=13){
   if(ArraySize(tables[1].tr[i].td)==6){
      aTestingReport.Summary.Commission=tables[1].tr[i].td[1];
      aTestingReport.Summary.Swap=tables[1].tr[i].td[2];
      aTestingReport.Summary.Profit=tables[1].tr[i].td[3];
      aTestingReport.Summary.Balance=tables[1].tr[i].td[4];            
   }
   break;
} 

含有报告数据的结构已完全准备就绪。

交易历史报告

交易历史报告可如同策略测试器报告那样进行解析,尽管其中包含的最终数据结构,以及结构所含内容会有所不同:

struct SHistory{
   SHistoryInfo Info;
   SOrder Orders[];
   SDeal Deals[];
   SSummary Summary;  
   SDeposit Deposit;
   SHistoryResults Results;
};

SHistory 结构包含以下结构: SHistoryInfo — 一般帐户信息,含有订单和成交数据结构的数组, SSummary — 交易结果, SDeposit — 最终存款状态, SHistoryResults — 一般数值 (盈利, 成交数量, 回撤, 等等)。

解析交易报告的完整 full HistoryHTMLReportToStruct() 函数代码如下所附。 两个参数传递给函数:

  • string FileName — 报告文件的名称
  • SHistory History — 填写交易报告的数据结构

优化报告

关于优化报告的第一个区别是不同的文件类型。 报告以 ANSI 格式保存,因此我们将使用另一个函数来读取其内容:

bool FileGetContentAnsi(string aFileName,string & aContent){
   int h=FileOpen(aFileName,FILE_READ|FILE_TXT|FILE_ANSI);
   if(h==-1)return(false);
   aContent="";
   while(!FileIsEnding(h)){
      aContent=aContent+FileReadString(h);
   }
   FileClose(h);
   return(true);
}

另一个区别在于标签。 <table>, <tr> 和 <td>, 会由以下标签替代: <Table>, <Row> 和 <Cell>. 最后一个主要区别是与数据结构相关:

struct SOptimization{
   string ParameterName[];
   SPass Pass[];
};

该结构包括一个字符串数组,其中包含要优化的参数名称(最右边的报告列)和 SPass 结构数组:

struct SPass{
   string Pass;
   string Result;
   string Profit;
   string ExpectedPayoff;
   string ProfitFactor;
   string RecoveryFactor;
   string SharpeRatio;
   string Custom;
   string EquityDD_perc;
   string Trades;
   string Parameters[];
};

该结构包括报告列中包含的字段。 最后一个字段是字符串数组,根据 ParameterName() 数组中的名称,它包含相应的需进行优化的参数值。

解析优化报告的 OptimizerXMLReportToStruct() 函数的完整代码如下所附。 两个参数传递给函数:

  • string FileName — 报告文件的名称
  • SOptimization Optimization — 含有要填充的优化报告数据的结构

辅助函数

如果我们将本文中创建的所有结构和函数安排在一个包含文件中,那么报告解析将在三行代码中实现。 

1. 包含文件:

#include <HTMLReport.mqh>

2. 声明结构:

SHistory History;

3. 调用函数:

HistoryHTMLReportToStruct("ReportHistory-555849.html",History);

之后,所有报告数据都将位于相应的结构字段中,与报告中的排列方式完全相同。 虽然所有字段都是字符串类型,但它们可以很容易地用于计算。 为此,我们只需要将字符串转换为数字。 不过,某些报告字段和相应的结构字段包含两个数值。 例如,订单交易量写入两个数值“0.1 / 0.1”,其中第一个数值是订单交易量,第二个值表示已填单的交易量。 一些总计也有双重数值,主要数值和括号中的附加数值。 例如,交易数量可写为“11 (54.55%)” — 实际数量和相对百分比值。 因此,我们要编写辅助函数来简化这些数值的使用。

用于提取单个交易量数值的函数:

string Volume1(string aStr){
   int p=StringFind(aStr,"/",0);
   if(p!=-1){
      aStr=StringSubstr(aStr,0,p);
   }
   StringTrimLeft(aStr);
   StringTrimRight(aStr);
   return(aStr);
}

string Volume2(string aStr){
   int p=StringFind(aStr,"/",0);
   if(p!=-1){
      aStr=StringSubstr(aStr,p+1);
   }
   StringTrimLeft(aStr);
   StringTrimRight(aStr);
   return(aStr);
}

Volume1() 函数从类似“0.1 / 0.1”的字符串中提取第一个交易量数值,Volume2() 提取第二个数值。

提取双重数据的函数:

string Value1(string aStr){
   int p=StringFind(aStr,"(",0);
   if(p!=-1){
      aStr=StringSubstr(aStr,0,p);
   }
   StringTrimLeft(aStr);
   StringTrimRight(aStr);
   return(aStr);
}

string Value2(string aStr){
   int p=StringFind(aStr,"(",0);
   if(p!=-1){
      aStr=StringSubstr(aStr,p+1);
   }
   StringReplace(aStr,")","");
   StringReplace(aStr,"%","");
   StringTrimLeft(aStr);
   StringTrimRight(aStr);
   return(aStr);
}

Value1() 函数从类型为“8.02 (0.08%)”的字符串中提取第一个数值,Value2() 提取第二个数值,删除右括号和百分号(如果有)。

可能需要的另一个有用的函数,就是测试报告结构的参数数据转换。 我们将使用以下结构将其转换为便利的形式:

struct SInput{
   string Name,
   string Value;
}

Name 字段用于参数名称,Value 用于其数值。 数值类型是事先已知的,这就是 Value 字段具有字符串类型的原因。

以下函数将参数字符串转换为 SInputs 结构数组:

void InputsToStruct(string aStr,SInput & aInputs[]){
   string tmp[];
   string tmp2[];
   StringSplit(aStr,',',tmp);
   int sz=ArraySize(tmp);
   ArrayResize(aInputs,sz);
   for(int i=0;i<sz;i++){
      StringSplit(tmp[i],'=',tmp2);
      StringTrimLeft(tmp2[0]);
      StringTrimRight(tmp2[0]);      
      aInputs[i].Name=tmp2[0];
      if(ArraySize(tmp2)>1){
         StringTrimLeft(tmp2[1]);
         StringTrimRight(tmp2[1]);       
         aInputs[i].Value=tmp2[1];
      }
      else{
         aInputs[i].Value="";
      }
   }
}

参数字符串以“,”字符分割到数组。 然后,以字符“=”将结果数组的每个元素分割为名称和数值。

现在,分析所提取数据并生成自定义报告的一切准备工作就绪。

创建自定义报告

现在我们可以访问报告数据,我们可以用任何偏好方式对其进行分析。 这有很多种选择。 使用交易结果,我们可以计算一般度量值,例如锋锐比率,恢复因子,等等。 自定义 HTML 报告可以访问所有 HTML,CSS 和 JavaScript 功能。 使用 HTML,我们可以重新排列报告。 例如,我们可以创建订单和成交表格,或买入成交和卖出成交的单独表格,以及其他类型的报告

CSS 支持数据着色,令报告更易于分析。 JavaScript 有助于令报告具有交互性。 例如,将鼠标悬停在成交行上可高亮显示订单表格中的相应订单行。 可以生成图像并将其添加到 HTML 报告中。 可选地,JavaScript 允许直接在 html5 中的 canvas 元素内绘制图像。 这一切都取决于您的需求,想象力和技能。

我们lai1创建一个自定义的 HTML 交易报告。 我们将订单和成交合并到一个表格中。 取消的订单将以灰色显示,因为它们不提供感兴趣的信息。 灰色也将用于市价订单,因为它们提供的信息与订单被执行后的成交结果相同。 成交将以明亮的颜色高亮显示:蓝色和红色。 填单的挂单也应该有显眼的颜色,如粉红色和蓝色。 

自定义报告表将包含订单表和成交表格中的标题。 我们来准备相应的数组:

   string TableHeader[]={  "Time",
                           "Order",
                           "Deal",
                           "Symbol",
                           "Type",
                           "Direction",
                           "Volume",
                           "Price",
                           "Order",
                           "S/L",
                           "T/P",
                           "Time",
                           "State",
                           "Commission",
                           "Swap",
                           "Profit",
                           "Balance",
                           "Comment"};

我们接收到包含报告数据的结构:

SHistory History;
HistoryHTMLReportToStruct("ReportHistory-555849.html",History);

我们打开生成报告的文件,并编写 HTMLStart() 函数形成的 HTML 页面的标准开头:

   int h=FileOpen("Report.htm",FILE_WRITE);
   if(h==-1)return;

   FileWriteString(h,HTMLStart("Report"));
   FileWriteString(h,"<table>\n");
   
   FileWriteString(h,"\t<tr>\n");   
   for(int i=0;i<ArraySize(TableHeader);i++){
      FileWriteString(h,"\t\t<th>"+TableHeader[i]+"</th>\n"); 
   }
   FileWriteString(h,"\t</tr>\n");     

HTMLStart() 函数代码如下所示:

string HTMLStart(string aTitle,string aCSSFile="style.css"){
   string str="<!DOCTYPE html>\n";
   str=str+"<html>\n";
   str=str+"<head>\n";
   str=str+"<link href=\""+aCSSFile+"\" rel=\"stylesheet\" type=\"text/css\">\n";
   str=str+"<title>"+aTitle+"</title>\n";
   str=str+"</head>\n";  
   str=str+"<body>\n";     
   return str;
}

带有 <title> 标签的页面标题的字符串和带有样式的文件名将传递给该函数。

遍历所有订单,定义字符串显示类型并形成它:

int j=0;
for(int i=0;i<ArraySize(History.Orders);i++){
   
   string sc="";
   
   if(History.Orders[i].State=="canceled"){
      sc="PendingCancelled";
   }
   else if(History.Orders[i].State=="filled"){
      if(History.Orders[i].Type=="buy"){
         sc="OrderMarketBuy";
      }
      else if(History.Orders[i].Type=="sell"){
         sc="OrderMarketSell";
      }
      if(History.Orders[i].Type=="buy limit" || History.Orders[i].Type=="buy stop"){
         sc="OrderPendingBuy";
      }
      else if(History.Orders[i].Type=="sell limit" || History.Orders[i].Type=="sell stop"){
         sc="OrderMarketSell";
      }         
   }

   FileWriteString(h,"\t<tr class=\""+sc+"\">\n");   
   FileWriteString(h,"\t\t<td>"+History.Orders[i].OpenTime+"</td>\n");  // 时间
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Order+"</td>\n");     // 订单 
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // 成交 
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Symbol+"</td>\n");    // 品种 
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Type+"</td>\n");      // 类型 
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // 方向       
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Volume+"</td>\n");    // 成交量    
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Price+"</td>\n");     // 价格  
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // 订单        
   FileWriteString(h,"\t\t<td>"+History.Orders[i].SL+"</td>\n");        // 止损
   FileWriteString(h,"\t\t<td>"+History.Orders[i].TP+"</td>\n");        // 止盈
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Time+"</td>\n");      // 时间    
   FileWriteString(h,"\t\t<td>"+History.Orders[i].State+"</td>\n");     // 状态
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // 佣金
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // 掉期利率
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // 盈利     
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // 余额    
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Comment+"</td>\n");   // 注释     
   FileWriteString(h,"\t</tr>\n");   
   

如果订单已填单,则找到相应的成交,定义其显示样式并形成其 HTML 代码:  

// 查找一笔成交

if(History.Orders[i].State=="filled"){
   for(;j<ArraySize(History.Deals);j++){
      if(History.Deals[j].Order==History.Orders[i].Order){
         
         sc="";
         
         if(History.Deals[j].Type=="buy"){
            sc="DealBuy";
         }
         else if(History.Deals[j].Type=="sell"){
            sc="DealSell";
         }
      
         FileWriteString(h,"\t<tr class=\""+sc+"\">\n");   
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Time+"</td>\n");     // 时间
         FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");     // 订单 
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Deal+"</td>\n");     // 成交 
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Symbol+"</td>\n");     // 品种 
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Type+"</td>\n");     // 类型 
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Direction+"</td>\n");     // 方向
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Volume+"</td>\n");     // 成交量
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Price+"</td>\n");     // 价格  
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Order+"</td>\n");     // 订单 
         FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");     // 止损
         FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");     // 止盈
         FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");     // 时间    
         FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");     // 状态
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Commission+"</td>\n");                    // 佣金
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Swap+"</td>\n");     // 掉期利率
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Profit+"</td>\n");     // 盈利     
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Balance+"</td>\n");     // 余额    
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Comment+"</td>\n");     // 注释     
         FileWriteString(h,"\t</tr>\n"); 
         break;
      }
   }
}

之后我们关闭表格,并添加标准 HTML 页面结尾,它是由 HTMLEnd() 函数形成的:

FileWriteString(h,"</table>\n");   
FileWriteString(h,HTMLEnd("Report"));  

HTMLEnd() 函数代码:

string HTMLEnd(){
   string str="</body>\n";
   str=str+"</html>\n";
   return str;
}

现在我们只需要编写样式文件 style.css。 学习 CSS 超出了本文的范围,因此我们不会详细研究这一点。 您可以查看下面附带的文件。 附件还包含创建报告的脚本 - HTMLReportCreate.mq5。

这是现成的报告:

自定义 HTML 报告
图例 5 自定义 HTML 报告的片段

结束语

您也许想知道运用正则表达式是否更容易。 整体结构和原则是一样的。 我们将分别接收一个包含表格内容的数组,然后是行和单元格。 我们将使用含正则表达式的函数,来替代 TagsToArray()。 其余的操作将非常相似。

本文中描述的创建自定义报告的示例只是呈现报告的选项之一。 它仅作为示例。 您可以使用方便自己理解的表格。 本文最重要的结果是您可以轻松访问所有报告数据。

附件

  • Include/HTMLReport.mqh — 包含报告解析函数的文件。
  • Scripts/HTMLReportTest.mq5 — 使用 HTMLReport.mqh 解析测试、优化和历史报告的示例。
  • Scripts/HTMLReportCreate.mq5 — 创建自定义 HTML 报告的示例。
  • Files/ReportTester-555849.html — 策略测试器报告。
  • Files/ReportOptimizer-555849.xml — 优化报告。
  • Files/ReportHistory-555849.html — 交易历史报告。
  • Files/Report.htm — 使用 HTMLReportCreate 脚本创建的报告文件。
  • Files/style.css — Report.htm 的样式表


本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/5436

附加的文件 |
MQL5.zip (24.2 KB)
使用 MQL5 和 MQL4 开发的选择与导航工具: 增加自动模式搜索和显示侦测到的交易品种 使用 MQL5 和 MQL4 开发的选择与导航工具: 增加自动模式搜索和显示侦测到的交易品种
在本文中, 我们继续扩展用于收集和在交易品种之间导航工具的功能。这一次,我们将创建新的选项卡,只显示满足一些必需参数的交易品种,并且研究如何根据所需的挑选规则简单添加自定义选项卡。
分离策略在趋势和盘整条件下的优化 分离策略在趋势和盘整条件下的优化
本文探讨了在分离在不同市场条件下的优化方法,分离优化意味着分别为上涨趋势和下跌趋势分别定义交易系统的最佳参数. 为了减少错误信号的影响,提高盈利能力,系统变得灵活,这意味着它们有一些特定的设置或输入数据,这是合理的,因为市场行为不断变化。
在算法交易中 KOHONEN 神经网络的实际应用  第二部分优化和预测 在算法交易中 KOHONEN 神经网络的实际应用 第二部分优化和预测
在设计使用 Kohonen 网络的通用工具的基础上,我们建立了优化EA参数的分析和选择系统,并探讨了时间序列的预测。在第一部分中,我们修正和改进了公开的神经网络类,增加了必要的算法。现在,是时候在实际应用中使用它们了。
在算法交易中 Kohonen 神经网络的实际应用。 第 I 部分 工具 在算法交易中 Kohonen 神经网络的实际应用。 第 I 部分 工具
本文依据之前发表文献中所介绍的思路,开发在 MetaTrader 5 中运用 Kohonen 映像。 改进并强化的类提供了解决应用程序任务的工具。