利用 HTML 报告分析交易结果·综合运用
📊

利用 HTML 报告分析交易结果·综合运用

(3/3)· 当标准衡量值不够用时,学会拆解 MT5 的 HTML/XML 报告自己算隐秘数值

含代码示例偏理论 第 3/3 篇
很多交易者把 MT5 导出的 HTML 报告直接发给投资者就完事,却没意识到标准字段之外藏着更能说明风险的细节。只靠平台默认摘要,容易漏掉连败区间和杠杆峰值。读懂文件结构,才能自己提取真正想看的东西。

回测报告字段的表格逐行抓取

MT5 的策略测试器会把核心统计结果渲染成 HTML 表格,想用程序自动解析报告,就得按行按列把单元格搬进自定义结构体。下面这段逻辑直接操作 tables[0] 的 tr / td 节点,用自增索引 i 顺着表格往下扫,把净值、回撤、盈利因子等字段逐一赋值。 注意 td 的列下标并不统一:总净利润、毛利、亏损等落在 td[1],而绝对回撤常出现在 td[3]、td[5],最大盈利单和最大亏损单却用 td[2] 与 td[4]。如果表格结构随版本微调,i 的步进(如 i+=2、i+=6)就会错位,解析出脏数据。 其中有一行 aTestingReport.Results.tables[0].tr[i].td[5]; 明显是笔误——左侧没有具体字段名,编译会直接报错,复制前必须补上对应成员。外汇与贵金属回测含高杠杆风险,这类自动解析仅用于本地验证,不代表任何实盘收益可能。 跑通后建议打印 aTestingReport.Results.ProfitFactor 与 SharpeRatio,对照 MT5 界面手算值,偏差超过 0.01% 就说明索引偏移,需重新数表。

MQL5 / C++
  aTestingReport.Results.Symbols=tables[class="num">0].tr[i].td[class="num">5];
  i++;
  aTestingReport.Results.TotalNetProfit=tables[class="num">0].tr[i].td[class="num">1];
  aTestingReport.Results.BalanceDrawdownAbsolute=tables[class="num">0].tr[i].td[class="num">3];
  aTestingReport.Results.EquityDrawdownAbsolute=tables[class="num">0].tr[i].td[class="num">5];
  i++;
  aTestingReport.Results.GrossProfit=tables[class="num">0].tr[i].td[class="num">1];
  aTestingReport.Results.BalanceDrawdownMaximal=tables[class="num">0].tr[i].td[class="num">3];
  aTestingReport.Results.EquityDrawdownMaximal=tables[class="num">0].tr[i].td[class="num">5];
  i++;
  aTestingReport.Results.GrossLoss=tables[class="num">0].tr[i].td[class="num">1];
  aTestingReport.Results.BalanceDrawdownRelative=tables[class="num">0].tr[i].td[class="num">3];
  aTestingReport.Results.EquityDrawdownRelative=tables[class="num">0].tr[i].td[class="num">5];
  i+=class="num">2;
  aTestingReport.Results.ProfitFactor=tables[class="num">0].tr[i].td[class="num">1];
  aTestingReport.Results.ExpectedPayoff=tables[class="num">0].tr[i].td[class="num">3];
  aTestingReport.Results.MarginLevel=tables[class="num">0].tr[i].td[class="num">5];
  i++;
  aTestingReport.Results.RecoveryFactor=tables[class="num">0].tr[i].td[class="num">1];
  aTestingReport.Results.SharpeRatio=tables[class="num">0].tr[i].td[class="num">3];
  aTestingReport.Results.ZScore=tables[class="num">0].tr[i].td[class="num">5];
  i++;
  aTestingReport.Results.AHPR=tables[class="num">0].tr[i].td[class="num">1];
  aTestingReport.Results.LRCorrelation=tables[class="num">0].tr[i].td[class="num">3];
  aTestingReport.Results.tables[class="num">0].tr[i].td[class="num">5];
  i++;
  aTestingReport.Results.GHPR=tables[class="num">0].tr[i].td[class="num">1];
  aTestingReport.Results.LRStandardError=tables[class="num">0].tr[i].td[class="num">3];
  i+=class="num">2;
  aTestingReport.Results.TotalTrades=tables[class="num">0].tr[i].td[class="num">1];
  aTestingReport.Results.ShortTrades_won_pers=tables[class="num">0].tr[i].td[class="num">3];
  aTestingReport.Results.LongTrades_won_perc=tables[class="num">0].tr[i].td[class="num">5];
  i++;
  aTestingReport.Results.TotalDeals=tables[class="num">0].tr[i].td[class="num">1];
  aTestingReport.Results.ProfitTrades_perc_of_total=tables[class="num">0].tr[i].td[class="num">3];
  aTestingReport.Results.LossTrades_perc_of_total=tables[class="num">0].tr[i].td[class="num">5];
  i++;
  aTestingReport.Results.LargestProfitTrade=tables[class="num">0].tr[i].td[class="num">2];
  aTestingReport.Results.LargestLossTrade=tables[class="num">0].tr[i].td[class="num">4];
  i++;
  aTestingReport.Results.AverageProfitTrade=tables[class="num">0].tr[i].td[class="num">2];
  aTestingReport.Results.AverageLossTrade=tables[class="num">0].tr[i].td[class="num">4];
  i++;
  aTestingReport.Results.MaximumConsecutiveWins_cur=tables[class="num">0].tr[i].td[class="num">2];
  aTestingReport.Results.MaximumConsecutiveLosses_cur=tables[class="num">0].tr[i].td[class="num">4];
  i++;
  aTestingReport.Results.MaximalConsecutiveProfit_count=tables[class="num">0].tr[i].td[class="num">2];
  aTestingReport.Results.MaximalConsecutiveLoss_count=tables[class="num">0].tr[i].td[class="num">4];
  i++;
  aTestingReport.Results.AverageConsecutiveWins=tables[class="num">0].tr[i].td[class="num">2];
  aTestingReport.Results.AverageConsecutiveLosses=tables[class="num">0].tr[i].td[class="num">4];
  i+=class="num">6;
  aTestingReport.Results.Correlation_Profits_MFE=tables[class="num">0].tr[i].td[class="num">1];
  aTestingReport.Results.Correlation_Profits_MAE=tables[class="num">0].tr[i].td[class="num">3];
  aTestingReport.Results.Correlation_MFE_MAE=tables[class="num">0].tr[i].td[class="num">5];
  i+=class="num">3;
  aTestingReport.Results.MinimalPositionHoldingTime=tables[class="num">0].tr[i].td[class="num">1];
  aTestingReport.Results.MaximalPositionHoldingTime=tables[class="num">0].tr[i].td[class="num">3];

◍ 从回测 HTML 表里抠出订单与成交

把测试报告网页表格的第二张表(tables[1])拆开,前半段是订单、后半段是成交,中间还夹着一行六列的汇总。代码从 i=3 开始扫订单行,遇到只有 1 个 td 的行就 break,说明订单区结束。 订单循环里每读一行就 ArrayResize 扩一位,把开仓时间、订单号、品种、类型、手数、挂单价、SL、TP、平仓时间、状态、注释共 11 个字段写进 aTestingReport.Orders[]。注意 td 下标从 0 到 10,漏一个列就会错位。 订单扫完 i+=3 跳到成交区,先 ArrayFree 清空旧 Deal 数组。成交行必须是 13 个 td 才进库;若碰到 6 个 td 则不是成交而是佣金/库存费/净利/结余汇总,顺手塞进 aTestingReport.Summary。外汇与贵金属回测的高风险在于:Swap 和 Commission 若没抓准,净值曲线会系统性偏离实盘。 下面这段是原文核心逻辑,逐行拆给你看: // 先存平均持仓时间(来自第一张表第5列) 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++; } // 成交:跳过3行,清空后按13列抓取 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]; 开 MT5 跑一遍自带回测,把生成的 HTML 丢进这套解析,订单数 ocnt 与成交数 dcnt 应当能对上表内行数;对不上就查 td 列数是否被券商模板改过。

MQL5 / C++
aTestingReport.Results.AveragePositionHoldingTime=tables[class="num">0].tr[i].td[class="num">5];

class=class="str">"cmt">// 订单
ArrayFree(aTestingReport.Orders);
class="type">int ocnt=class="num">0;
for(i=class="num">3;i<ArraySize(tables[class="num">1].tr);i++){
   if(ArraySize(tables[class="num">1].tr[i].td)==class="num">1){
       class="kw">break;
   }
   ArrayResize(aTestingReport.Orders,ocnt+class="num">1);
   aTestingReport.Orders[ocnt].OpenTime=tables[class="num">1].tr[i].td[class="num">0];
   aTestingReport.Orders[ocnt].Order=tables[class="num">1].tr[i].td[class="num">1];
   aTestingReport.Orders[ocnt].Symbol=tables[class="num">1].tr[i].td[class="num">2];
   aTestingReport.Orders[ocnt].Type=tables[class="num">1].tr[i].td[class="num">3];
   aTestingReport.Orders[ocnt].Volume=tables[class="num">1].tr[i].td[class="num">4];
   aTestingReport.Orders[ocnt].Price=tables[class="num">1].tr[i].td[class="num">5];
   aTestingReport.Orders[ocnt].SL=tables[class="num">1].tr[i].td[class="num">6];
   aTestingReport.Orders[ocnt].TP=tables[class="num">1].tr[i].td[class="num">7];
   aTestingReport.Orders[ocnt].Time=tables[class="num">1].tr[i].td[class="num">8];
   aTestingReport.Orders[ocnt].State=tables[class="num">1].tr[i].td[class="num">9];
   aTestingReport.Orders[ocnt].Comment=tables[class="num">1].tr[i].td[class="num">10];
   ocnt++;
}

class=class="str">"cmt">// 成交

i+=class="num">3;
ArrayFree(aTestingReport.Deals);
class="type">int dcnt=class="num">0;
for(;i<ArraySize(tables[class="num">1].tr);i++){
   if(ArraySize(tables[class="num">1].tr[i].td)!=class="num">13){
       if(ArraySize(tables[class="num">1].tr[i].td)==class="num">6){
           aTestingReport.Summary.Commission=tables[class="num">1].tr[i].td[class="num">1];
           aTestingReport.Summary.Swap=tables[class="num">1].tr[i].td[class="num">2];
           aTestingReport.Summary.Profit=tables[class="num">1].tr[i].td[class="num">3];
           aTestingReport.Summary.Balance=tables[class="num">1].tr[i].td[class="num">4];
       }
       class="kw">break;
   }
   ArrayResize(aTestingReport.Deals,dcnt+class="num">1);
   aTestingReport.Deals[dcnt].Time=tables[class="num">1].tr[i].td[class="num">0];
   aTestingReport.Deals[dcnt].Deal=tables[class="num">1].tr[i].td[class="num">1];
   aTestingReport.Deals[dcnt].Symbol=tables[class="num">1].tr[i].td[class="num">2];
   aTestingReport.Deals[dcnt].Type=tables[class="num">1].tr[i].td[class="num">3];
   aTestingReport.Deals[dcnt].Direction=tables[class="num">1].tr[i].td[class="num">4];
   aTestingReport.Deals[dcnt].Volume=tables[class="num">1].tr[i].td[class="num">5];
   aTestingReport.Deals[dcnt].Price=tables[class="num">1].tr[i].td[class="num">6];

「从回测表抓取逐笔成交字段」

这段逻辑干的事很直接:把 HTML 回测报告里第二张表(tables[1])的每一行成交记录,按列索引搬进自定义结构数组 aTestingReport.Deals。订单号取 td[7]、佣金 td[8]、库存费 td[9]、盈亏 td[10]、余额 td[11]、注释 td[12],然后 dcnt++ 推进下标。 循环里有个硬退出判断:当某行只有 1 个单元格(ArraySize==1)时直接 break,说明已经扫到表格末尾的空行或分隔行。 继续往下走,i 每次加 3 跳到下一笔成交块;若当前行不是 13 列,就检查是不是 6 列的那一行——那是汇总行,把佣金、库存费、盈亏、余额分别读进 aTestingReport.Summary 的对应字段后 break。外汇与贵金属回测中,佣金和 swap 的符号方向常因经纪商设置而异,复制这段代码进 MT5 脚本后,先打印几行 Deals 数组确认列索引没错位,再谈统计。

MQL5 / C++
   aTestingReport.Deals[dcnt].Order=tables[class="num">1].tr[i].td[class="num">7];
   aTestingReport.Deals[dcnt].Commission=tables[class="num">1].tr[i].td[class="num">8];
   aTestingReport.Deals[dcnt].Swap=tables[class="num">1].tr[i].td[class="num">9];
   aTestingReport.Deals[dcnt].Profit=tables[class="num">1].tr[i].td[class="num">10];
   aTestingReport.Deals[dcnt].Balance=tables[class="num">1].tr[i].td[class="num">11];
   aTestingReport.Deals[dcnt].Comment=tables[class="num">1].tr[i].td[class="num">12];
   dcnt++;
  }
  class="kw">return(true);
}
if(ArraySize(tables[class="num">1].tr[i].td)==class="num">1){
  class="kw">break;
}
class=class="str">"cmt">// 成交

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

拆开交易历史报告的数据骨架

MT5 导出的交易历史报告,结构和策略测试器报告同源但字段更偏真实账户侧。它最终落进一套 SHistory 聚合结构,里面分了一般账户信息、订单与成交数组、交易结果、存款状态和回撤等数值块,和回测报告那种纯绩效视角不一样。 具体看,SHistory 并不是扁平表,而是嵌套体:Info 装账户与基础统计,Orders[] 和 Deals[] 分别挂单与成交明细,Summary 汇总交易结果,Deposit 记录终态资金,Results 收口胜率、笔数、回撤等标量。解析时常用 full HistoryHTMLReportToStruct() 一口吞进 HTML 报告文件,再回填到这个结构里。 函数只接两个实参:string FileName 是报告文件名路径,SHistory History 是待填充结构。你开 MT5 把某段实盘历史导成 HTML,用这套结构接住,就能绕开肉眼翻报表,直接写脚本算自己的最大回撤和连亏笔数。外汇与贵金属杠杆高,历史回撤不代表未来风险更低,参数验证请在模拟环境先跑。

MQL5 / C++
class="kw">struct SHistory{
   SHistoryInfo Info;
   SOrder Orders[];
   SDeal Deals[];
   SSummary Summary;   
   SDeposit Deposit;
   SHistoryResults Results;
};

◍ 啃下优化报告的 ANSI 与 XML 差异

MT5 跑完参数优化导出的报告,默认是 ANSI 编码的 XML 变体,和常规 UTF-8 网页标签不一样。最直观的三处坑:文件要用 ANSI 方式读;标签从小写 <table>/<tr>/<td> 换成首字母大写的 <Table>/<Row>/<Cell>;数据结构得自己定义,不能套用普通报表解析逻辑。 报告解析的核心是两个自定义结构。SOptimization 里挂了一个 ParameterName 字符串数组(对应报告最右列的参数名)和一个 SPass 数组,每条 Pass 存一轮优化的全部指标。SPass 则逐列收录 Pass、Result、Profit、ExpectedPayoff、ProfitFactor、RecoveryFactor、SharpeRatio、Custom、EquityDD_perc、Trades,以及一个 Parameters 字符串数组——后者按下标和 ParameterName 对齐,装的是该轮具体参数值。 下面这段是读取 ANSI 文件并声明结构的底子代码。FileGetContentAnsi 用 FILE_ANSI 标志打开,循环 FileReadString 拼满内容;两个 struct 直接决定了你后面能否把报告映射成可遍历的数组。外汇与贵金属优化结果受点差和滑点影响大,回测优异不代表实盘概率占优,高风险。

MQL5 / C++
class="type">bool FileGetContentAnsi(class="type">class="kw">string aFileName,class="type">class="kw">string & aContent){
   class="type">int h=FileOpen(aFileName,FILE_READ|FILE_TXT|FILE_ANSI);
   if(h==-class="num">1)class="kw">return(false);
   aContent="";
   class="kw">while(!FileIsEnding(h)){
      aContent=aContent+FileReadString(h);
   }
   FileClose(h);
   class="kw">return(true);
}
class="kw">struct SOptimization{
   class="type">class="kw">string ParameterName[];
   SPass Pass[];
};
class="kw">struct SPass{
   class="type">class="kw">string Pass;
   class="type">class="kw">string Result;
   class="type">class="kw">string Profit;
   class="type">class="kw">string ExpectedPayoff;
   class="type">class="kw">string ProfitFactor;
   class="type">class="kw">string RecoveryFactor;
   class="type">class="kw">string SharpeRatio;
   class="type">class="kw">string Custom;
   class="type">class="kw">string EquityDD_perc;
   class="type">class="kw">string Trades;
   class="type">class="kw">string Parameters[];
};

「把报告里的脏字符串拆成能算的数」

MT5 回测导出的 HTML 报告里,很多字段是字符串但夹着双值。比如订单交易量写成 "0.1 / 0.1",前面是挂单量、后面是成交量大概率;交易总数可能写成 "11 (54.55%)",括号里是相对占比。直接拿去做统计会踩坑,得先拆。 Volume1() 和 Volume2() 专门切 "0.1 / 0.1" 这种斜杠结构:找 '/' 位置,取左或取右再 trim 空格。Value1()/Value2() 对付带括号的百分比,Value2() 还会顺手删掉 ')' 和 '%',吐出纯数字字符串方便 StringToDouble。 测试参数那块更乱,原始串是 "Name=Val,Name2=Val2" 的逗号等号嵌套。InputsToStruct() 先按 ',' 劈成数组,再对每个元素按 '=' 分 Name/Value,塞进 SInput 结构数组。这样你遍历 aInputs[] 就能直接读参数名和值。 把上面这些塞进一个 .mqh 包含文件后,解析报告只需三行:include、声明 SHistory、调 HistoryHTMLReportToStruct。之后所有字段原样落在结构里,配合这几个辅助函数就能开算。外汇和贵金属回测高风险,拆出来的数字只用于复盘,不代表实盘倾向。

MQL5 / C++
class="macro">#include <HTMLReport.mqh>
SHistory History;
HistoryHTMLReportToStruct("ReportHistory-class="num">555849.html",History);
class="type">class="kw">string Volume1(class="type">class="kw">string aStr){
  class="type">int p=StringFind(aStr,"/",class="num">0);
  if(p!=-class="num">1){
    aStr=StringSubstr(aStr,class="num">0,p);
  }
  StringTrimLeft(aStr);
  StringTrimRight(aStr);
  class="kw">return(aStr);
}
class="type">class="kw">string Volume2(class="type">class="kw">string aStr){
  class="type">int p=StringFind(aStr,"/",class="num">0);
  if(p!=-class="num">1){
    aStr=StringSubstr(aStr,p+class="num">1);
  }
  StringTrimLeft(aStr);
  StringTrimRight(aStr);
  class="kw">return(aStr);
}
class="type">class="kw">string Value1(class="type">class="kw">string aStr){
  class="type">int p=StringFind(aStr,"(",class="num">0);
  if(p!=-class="num">1){
    aStr=StringSubstr(aStr,class="num">0,p);
  }
  StringTrimLeft(aStr);
  StringTrimRight(aStr);
  class="kw">return(aStr);
}
class="type">class="kw">string Value2(class="type">class="kw">string aStr){
  class="type">int p=StringFind(aStr,"(",class="num">0);
  if(p!=-class="num">1){
    aStr=StringSubstr(aStr,p+class="num">1);
  }
  StringReplace(aStr,")","");
  StringReplace(aStr,"%","");
  StringTrimLeft(aStr);
  StringTrimRight(aStr);
  class="kw">return(aStr);
}
class="kw">struct SInput{
  class="type">class="kw">string Name,
  class="type">class="kw">string Value;
}
class="type">void InputsToStruct(class="type">class="kw">string aStr,SInput & aInputs[]){
  class="type">class="kw">string tmp[];
  class="type">class="kw">string tmp2[];
  StringSplit(aStr,&class="macro">#x27;,&class="macro">#x27;,tmp);
  class="type">int sz=ArraySize(tmp);
  ArrayResize(aInputs,sz);
  for(class="type">int i=class="num">0;i<sz;i++){
    StringSplit(tmp[i],&class="macro">#x27;=&class="macro">#x27;,tmp2);
    StringTrimLeft(tmp2[class="num">0]);
    StringTrimRight(tmp2[class="num">0]);
    aInputs[i].Name=tmp2[class="num">0];
    if(ArraySize(tmp2)>class="num">1){
      StringTrimLeft(tmp2[class="num">1]);
      StringTrimRight(tmp2[class="num">1]);
      aInputs[i].Value=tmp2[class="num">1];
    }
    else{
      aInputs[i].Value="";
    }
  }
}

把订单成交揉进一张可交互 HTML 表

拿到回测或实盘的历史结构后,报告长什么样完全看你用什么前端手段。HTML 负责重排布局,CSS 做行级着色,JavaScript 能把鼠标悬停联动做成订单—成交互查;外汇与贵金属波动剧烈、杠杆风险高,这类自定义报表只是辅助复盘,不代表任何收益预期。 下面这段把订单和成交并表:取消单和市价单用灰显(市价单信息与成交重复),成交用蓝/红高亮,填单的挂单走粉/蓝。表头数组直接复用了订单与成交两边字段,共 18 列,从 Time、Order、Deal 一直到 Comment。 核心调用是先 HistoryHTMLReportToStruct 把源报告解析进 SHistory 结构,再 FileOpen 写 Report.htm,HTMLStart 拼出标准页头(含 title 与 style.css 链接)。跑完脚本 HTMLReportCreate.mq5 就能在 MT5 终端目录拿到自定义 htm,用浏览器打开即可看着色表格。

MQL5 / C++
  class="type">class="kw">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-class="num">555849.html",History);
  class="type">int h=FileOpen("Report.htm",FILE_WRITE);
  if(h==-class="num">1)class="kw">return;
  FileWriteString(h,HTMLStart("Report"));
  FileWriteString(h,"<table>\n");
  
  FileWriteString(h,"\t<tr>\n");  
  for(class="type">int i=class="num">0;i<ArraySize(TableHeader);i++){
      FileWriteString(h,"\t\t<th>"+TableHeader[i]+"</th>\n");
  }
  FileWriteString(h,"\t</tr>\n");    
class="type">class="kw">string HTMLStart(class="type">class="kw">string aTitle,class="type">class="kw">string aCSSFile="style.css"){
  class="type">class="kw">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";

◍ 把历史订单导成 HTML 表格的逐行写法

这段逻辑干一件事:遍历 History.Orders 数组,把每一笔订单按状态映射成 CSS 类名,再逐字段写进 HTML 表格行。注意 canceled 状态单独标成 PendingCancelled,filled 里再按 buy/sell 及挂单类型细分。 代码里有个容易踩的坑:sell limit 和 sell stop 被错误赋成了 OrderMarketSell,而不是 OrderPendingSell,会导致回测报告里挂单被误显示为市价卖。你在 MT5 里跑之前最好把那行改掉,否则品种统计会偏。 FileWriteString 每次只写一格,注释标了时间、订单号、品种、类型、成交量、价格、SL、TP 等列。若 History.Orders 有 200 条,就会生成 200 个 <tr>,外汇与贵金属品种的高杠杆特性意味着这类历史单里滑点和止损触发可能密集,导出后建议用浏览器直接看颜色区分。

MQL5 / C++
  str=str+"<body>\n";
    class="kw">return str;
}
class="type">int j=class="num">0;
for(class="type">int i=class="num">0;i<ArraySize(History.Orders);i++){
  
  class="type">class="kw">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");  class=class="str">"cmt">// 时间
  FileWriteString(h,"\t\t<td>"+History.Orders[i].Order+"</td>\n");      class=class="str">"cmt">// 订单 
  FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                      class=class="str">"cmt">// 成交 
  FileWriteString(h,"\t\t<td>"+History.Orders[i].Symbol+"</td>\n");     class=class="str">"cmt">// 品种 
  FileWriteString(h,"\t\t<td>"+History.Orders[i].Type+"</td>\n");       class=class="str">"cmt">// 类型 
  FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                      class=class="str">"cmt">// 方向      
  FileWriteString(h,"\t\t<td>"+History.Orders[i].Volume+"</td>\n");     class=class="str">"cmt">// 成交量    
  FileWriteString(h,"\t\t<td>"+History.Orders[i].Price+"</td>\n");      class=class="str">"cmt">// 价格  
  FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                      class=class="str">"cmt">// 订单      
  FileWriteString(h,"\t\t<td>"+History.Orders[i].SL+"</td>\n");         class=class="str">"cmt">// 止损
  FileWriteString(h,"\t\t<td>"+History.Orders[i].TP+"</td>\n");         class=class="str">"cmt">// 止盈
  FileWriteString(h,"\t\t<td>"+History.Orders[i].Time+"</td>\n");       class=class="str">"cmt">// 时间    

「把订单和成交拼进同一张 HTML 表」

导出历史报表时,订单行先写状态、注释等字段,佣金/掉期/盈利/余额暂用 &nbsp; 占位,避免表格列错位。订单的 State 为 "filled" 才往下找对应成交,这是多数脚本漏掉的判断点。 查找成交靠 Order 字段对齐:内层循环从 j 开始而非 0,匹配到 History.Deals[j].Order == History.Orders[i].Order 即落行。买/卖分别打 DealBuy / DealSell 的 class,方便后续用 CSS 上色。 成交行写出时间、成交号、品种、类型、方向、成交量、价格共 7 列。若你跑 EURUSD 的 H1 回测,成交 Price 一般精确到小数点后 5 位(如 1.08453),直接拼字符串即可,无需格式化函数。 别把占位格当永久方案:佣金和掉期留空会让报表在统计端失效,真要用的话得从 History.Deals 的 Commission / Swap 字段补数。

MQL5 / C++
  FileWriteString(h,"\t\t<td>"+History.Orders[i].State+"</td>\n");       class=class="str">"cmt">// 状态
  FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                      class=class="str">"cmt">// 佣金
  FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                      class=class="str">"cmt">// 掉期利率
  FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                      class=class="str">"cmt">// 盈利   
  FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                      class=class="str">"cmt">// 余额  
  FileWriteString(h,"\t\t<td>"+History.Orders[i].Comment+"</td>\n");    class=class="str">"cmt">// 注释   
  FileWriteString(h,"\t</tr>\n");  

class=class="str">"cmt">// 查找一笔成交
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");       class=class="str">"cmt">// 时间
           FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");       class=class="str">"cmt">// 订单 
           FileWriteString(h,"\t\t<td>"+History.Deals[j].Deal+"</td>\n");       class=class="str">"cmt">// 成交 
           FileWriteString(h,"\t\t<td>"+History.Deals[j].Symbol+"</td>\n");       class=class="str">"cmt">// 品种 
           FileWriteString(h,"\t\t<td>"+History.Deals[j].Type+"</td>\n");       class=class="str">"cmt">// 类型 
           FileWriteString(h,"\t\t<td>"+History.Deals[j].Direction+"</td>\n");       class=class="str">"cmt">// 方向
           FileWriteString(h,"\t\t<td>"+History.Deals[j].Volume+"</td>\n");       class=class="str">"cmt">// 成交量
           FileWriteString(h,"\t\t<td>"+History.Deals[j].Price+"</td>\n");       class=class="str">"cmt">// 价格 

把每笔成交导成 HTML 表格行

这段逻辑在遍历成交历史时,把单笔成交的关键字段逐行写进 HTML 表格。注意订单号、佣金、掉期、盈利、余额、注释是真实取值,而止损、止盈、时间、状态字段当前写的是 &nbsp; 占位空串,说明原型里这几列还没接数据。 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; } } } FileWriteString(h,"</table>\n"); FileWriteString(h,HTMLEnd("Report")); string HTMLEnd(){ string str="</body>\n"; str=str+"</html>\n"; return str; } 匹配到一条成交后 break,意味着内层循环只处理首个吻合项,若同一订单有多笔平仓需改逻辑才能全抓。HTMLEnd 写死收尾标签,调用时传的 "Report" 参数实际没被使用,属于可清理的冗余。 开 MT5 把这段塞进你自己的成交报表脚本,先跑出带空列的 HTML,再决定止损止盈那几列接 Deal 的哪几个字段。外汇与贵金属杠杆高,报表只是复盘工具,不预示未来盈亏。

MQL5 / C++
FileWriteString(h,"\t\t<td>"+History.Deals[j].Order+"</td>\n");  class=class="str">"cmt">// 订单
FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");  class=class="str">"cmt">// 止损
FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");  class=class="str">"cmt">// 止盈
FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");  class=class="str">"cmt">// 时间
FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");  class=class="str">"cmt">// 状态
FileWriteString(h,"\t\t<td>"+History.Deals[j].Commission+"</td>\n");  class=class="str">"cmt">// 佣金
FileWriteString(h,"\t\t<td>"+History.Deals[j].Swap+"</td>\n");  class=class="str">"cmt">// 掉期利率
FileWriteString(h,"\t\t<td>"+History.Deals[j].Profit+"</td>\n");  class=class="str">"cmt">// 盈利
FileWriteString(h,"\t\t<td>"+History.Deals[j].Balance+"</td>\n");  class=class="str">"cmt">// 余额
FileWriteString(h,"\t\t<td>"+History.Deals[j].Comment+"</td>\n");  class=class="str">"cmt">// 注释
FileWriteString(h,"\t</tr>\n");
class="kw">break;
}
}
}
FileWriteString(h,"</table>\n");
FileWriteString(h,HTMLEnd("Report"));
class="type">class="kw">string HTMLEnd(){
  class="type">class="kw">string str="</body>\n";
  str=str+"</html>\n";
  class="kw">return str;
}

◍ 正则表达式替代标签拆分的思路

如果觉得用标签函数拆 HTML 表格太绕,换成正则表达式也是同一条路:先抓出整张表的内容数组,再逐层切出 tr 行与 td 单元格,只是把 TagsToArray() 换成正则匹配函数,后续遍历逻辑几乎不动。 前面演示的自定义报告生成只是其中一种取法,表格长什么样完全看你习惯,关键不在于排版花哨。真正落地的价值是——报告里的每一行数据你都能在 MQL5 里直接读到、直接算,不用再手动从终端拷贝。 在 MT5 里跑一段正则提取脚本,接上你自己的账户历史,验证一下单元格是否能被逐个索引,比看任何示例都实在。外汇与贵金属报表涉及真实仓位,回测与实盘统计均有较高风险,结论仅作方法参考。

「随包文件与读者反馈的实情」

这一节列的是随文发布的压缩包内容,MQL5.zip 体积 24.2 KB,里面分两类:Include 下的 HTMLReport.mqh 提供了解析测试、优化、历史三种报告的函数;Scripts 里两个 mq5 示例分别演示解析和自建 HTML 报告;Files 中带了测试器、优化器、交易历史各一份样例文件,以及用脚本生成的 Report.htm 和配套 style.css。 有读者在评论里提到跑 HTMLReportTest 时撞到『数组超出范围 (470,42)』,作者原意也不是交一份漂亮图表,而是交一个按文件名取报告数据的结构,拿到结构后你想怎么二次处理都行。外汇与贵金属报表解析涉及实盘数据,高风险,回测结论只代表历史样本。 讨论里有人希望把多家经纪商报告并到一个实时页面,作者没接这需求,只把底层解析留给了用户自己扩。打开 MT5 把 ZIP 解到对应目录,先跑 HTMLReportTest 验证能否读出 ReportTester-555849.html,比看说明来得直接。

把报告解析交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到从历史报告提炼的连败与回撤分布,你不必每次手写解析函数。

常见问题

HTML 可用任意浏览器打开,适合没有 Excel 的投资者查看;XLSX 便于在表格软件里做进一步计算。两者数据同源,但 HTML 结构更适合程序化提取。
可以,小布盯盘的品种页已内置从历史报告解析连败、回撤和杠杆使用的视图,省去自己写解析函数的重复劳动。
优化结果含参数组合与多维度量,XML 以纯数据结构描述更适合机器读取,HTML 侧重展示,测试报告才同时提供两种。
table 必须被 </table> 封闭,否则后续内容可能被浏览器误判为表格一部分,导致提取行和单元格时错位、读不到真实数值。
先从解析结构拿到净利润和最大回撤,两者比值即恢复因子;关于完整度量的讨论见《利用 HTML 报告分析交易结果·基础篇》。