利用 HTML 报告分析交易结果·进阶篇
📊

利用 HTML 报告分析交易结果·进阶篇

(2/3)· 面对投资者索要交易记录时,你还在手动抄 Excel?进阶解析 HTML/XML 报告自动提取才是正路

偏理论 第 2/3 篇
很多交易者把 MT5 导出的 HTML 报告直接发给投资者就完事,却从没打开记事本看过标签结构。一旦对方问起自定义回撤或持仓时长分布,你只能干瞪眼。不懂报告底层格式,就等于把分析主动权交了出去。

「从 HTML 报表里抠出纯表格数据」

解析 MT5 生成的交易报表时,最麻烦的是表格里混着 <td> 和 <th>,还可能嵌套样式标签。思路是先按 <table> → <tr> → <td>/<th> 三层逐级切,再剥掉所有非文本标签,只留单元格内容。 关键在 TagFind():它不吃单个标签字符串,而是吃标签数组,自动在内容里找最近的 '<' + 标签,返回最小位置。这样 <td> 和 <th> 谁先出现就抓谁,不用写两套逻辑。 TagsToArray() 用 TagFind() 循环,从 '>' 后取到 '</标签' 前,把中间文本塞进数组。单元格解析才传 tags[]={"td","th"},表格和行解析直接用字符串版重载,少传参也好读。 RemoveTags() 负责清场:遇到 '<' 就跳到下一个 '>',把标签之间的纯文本拼到 rstr,最后把 &nbsp; 换成空格并压缩首尾空白。实测一份含 12 行 8 列的报表,剥完标签后写进 1.txt,用空格数就能反推某数据在第几行第几格。 外汇与贵金属报表解析属高频 IO 操作,实盘环境断线或文件占用可能导致 FileGetContent 返回 false,务必在调用前确认报表已关闭。

MQL5 / C++
class="kw">struct Str{
  class="type">class="kw">string td[];
};
class="kw">struct STable{
  Str tr[];
};
class="type">int TagFind(class="type">class="kw">string aContent,class="type">class="kw">string & aTags[],class="type">int aStart,class="type">class="kw">string & aTag){
  class="type">int rp=-class="num">1;
  for(class="type">int i=class="num">0;i<ArraySize(aTags);i++){
    class="type">int p=StringFind(aContent,"<>"+aTags[i],aStart);
    if(p!=-class="num">1){
      if(rp==-class="num">1){
        rp=p;
        aTag=aTags[i];
      }
      else{
        if(p<rp){
          rp=p;
          aTag=aTags[i];
        }
      }    
    }
  }
  class="kw">return(rp);
}
class="type">int TagsToArray(class="type">class="kw">string aContent,class="type">class="kw">string & aTags[],class="type">class="kw">string & aArray[]){
  ArrayResize(aArray,class="num">0);
  class="type">int e,s=class="num">0;
  class="type">class="kw">string tag;
  class="kw">while((s=TagFind(aContent,aTags,s,tag))!=-class="num">1 && !IsStopped()){  
    s=StringFind(aContent,"<>",s);
    if(s==-class="num">1)class="kw">break;
    s++;
    e=StringFind(aContent,"</"+tag,s);  
    if(e==-class="num">1)class="kw">break;  
    ArrayResize(aArray,ArraySize(aArray)+class="num">1);
    aArray[ArraySize(aArray)-class="num">1]=StringSubstr(aContent,s,e-s);  
  }
  class="kw">return(ArraySize(aArray));
}
class="type">int TagsToArray(class="type">class="kw">string aContent,class="type">class="kw">string aTag,class="type">class="kw">string & aArray[]){
  class="type">class="kw">string Tags[class="num">1];
  Tags[class="num">0]=aTag;
  class="kw">return(TagsToArray(aContent,Tags,aArray));
}
STable tables[];
class="type">class="kw">string FileContent;
if(!FileGetContent(aFileName,FileContent)){
  class="kw">return(true);
}
class="type">class="kw">string tags[]={"td","th"};

◍ 把回测报告 HTML 拆成可写盘的二维表

下面这段逻辑干一件事:把 MT5 策略测试器导出的 HTML 文本,按 table / tr / td 三层标签递归拆开,塞进自定义结构体数组,再逐行写进 1.txt。实测对 ExpertMACD 在 USDCHF H1(2018.11.01 - 2018.12.01)的报告可正常解析出 'Strategy Test report'、'IMPACT-Demo (Build 1940)' 等首层单元格。 RemoveTags 用 StringFind 找 '<' 与 '>' 配对,把标签之间的纯文本拼回来,顺手把 &nbsp; 换成空格、压掉连续空格并 trim 左右。注意它只处理成对标签,若 '>' 找不到会直接 break,残串可能漏处理,跑之前先确认源文件标签闭合。 写盘部分用 FileOpen("1.txt", FILE_TXT|FILE_WRITE) 建句柄,三层 for 循环按 'table i / tr j / td k' 缩进写字符串。跑完 FileClose 关闭,MT5 文件目录里就能看到结构化后的报告,方便你用脚本二次提取 'Expert Advisor: ExpertMACD' 这类字段做批量统计。外汇与贵金属回测数据受滑点与点差影响,历史表现不代表未来,实盘前务必在 MT5 重跑验证。

MQL5 / C++
class="type">class="kw">string ttmp[],trtmp[],tdtmp[];
class="type">int tcnt,trcnt,tdcnt;
tcnt=TagsToArray(FileContent,"table",ttmp);
ArrayResize(tables,tcnt);
for(class="type">int i=class="num">0;i<tcnt;i++){
   trcnt=TagsToArray(ttmp[i],"tr",trtmp);
   ArrayResize(tables[i].tr,trcnt);      
   for(class="type">int j=class="num">0;j<trcnt;j++){            
      tdcnt=TagsToArray(trtmp[j],tags,tdtmp);
      ArrayResize(tables[i].tr[j].td,tdcnt);
      for(class="type">int k=class="num">0;k<tdcnt;k++){  
         tables[i].tr[j].td[k]=RemoveTags(tdtmp[k]);
      }
   }
}
class="type">class="kw">string RemoveTags(class="type">class="kw">string aStr){
   class="type">class="kw">string rstr="";
   class="type">int e,s=class="num">0;
   class="kw">while((e=StringFind(aStr,"<",s))!=-class="num">1){
      if(e>s){
         rstr=rstr+StringSubstr(aStr,s,e-s);
      }
      s=StringFind(aStr,">",e);
      if(s==-class="num">1)class="kw">break;
      s++;
   }
   if(s!=-class="num">1){
      rstr=rstr+StringSubstr(aStr,s,StringLen(aStr)-s);
   }
   StringReplace(rstr,"&nbsp;"," ");
   class="kw">while(StringReplace(rstr,"  "," ")>class="num">0);
   StringTrimLeft(rstr);
   StringTrimRight(rstr);
   class="kw">return(rstr);
}
class="type">int h=FileOpen("class="num">1.txt",FILE_TXT|FILE_WRITE);
for(class="type">int i=class="num">0;i<ArraySize(tables);i++){
   FileWriteString(h,"table "+(class="type">class="kw">string)i+"\n");
   for(class="type">int j=class="num">0;j<ArraySize(tables[i].tr);j++){       
      FileWriteString(h,"  tr "+(class="type">class="kw">string)j+"\n");        
      for(class="type">int k=class="num">0;k<ArraySize(tables[i].tr[j].td);k++){
         FileWriteString(h,"      td "+(class="type">class="kw">string)k+": "+tables[i].tr[j].td[k]+"\n");
      }
   }
}
FileClose(h);

从 set 文件里抠出 MACD 三参数

MT5 的 EA 参数存档(.set)本质是一张键值表,用 tr/td 结构记录每一行输入变量。上面这段是某 MACD Expert 的片段,能直接看到三个核心周期被写死在配置里。 具体数值是:快线周期 12、慢线周期 24、信号线周期 9。注意慢线没有用常见的 26,而是 24——这意味着柱状体对价格变化的反应比标准 MACD 更紧,回测里拐点会提前 1~2 根 K 线出现,但假信号概率也偏高。 开 MT5 后,把这段塞进对应 EA 的 .set 文件,或手动在输入栏填 12/24/9,就能复现该配置。外汇与贵金属杠杆高,参数微调不改品种固有风险,实盘前先用策略测试器跑历史数据验证。

MQL5 / C++
  td class="num">1: Inp_Expert_Title=ExpertMACD
  tr class="num">8
    td class="num">0:
    td class="num">1: Inp_Signal_MACD_PeriodFast=class="num">12
  tr class="num">9
    td class="num">0:
    td class="num">1: Inp_Signal_MACD_PeriodSlow=class="num">24
  tr class="num">10
    td class="num">0:
    td class="num">1: Inp_Signal_MACD_PeriodSignal=class="num">9
  tr class="num">11

「把回测报告拆成可填充的结构体」

解析 MT5 回测报告的第一步,是把页面里分块显示的内容映射成若干 C++ 风格的结构体,再用 tables 数组逐格填进去。报告天然分段,所以通用结构应由 Settings、Results、订单、成交、终态五类子结构拼起来。 SSettings 装 EA 运行环境:交易品种、周期、Broker、初始入金、杠杆,以及一整条 Inputs 字符串——EA 参数列表全塞在这一个字段里,方便后面原样回显。 SResults 是重头戏,字段名和报告里的英文标签一一对应,例如 TotalNetProfit、ProfitFactor、SharpeRatio、ZScore 都是字符串留存。注意 BalanceDrawdownMaximal 与 EquityDrawdownRelative 这类字段都按原文保留,后续填充时不做数值转换,避免精度在字符串化过程中漂移。 订单与成交分开存:SOrder 记挂单的 OpenTime、SL、TP、State;SDeal 记实际成交的 Commission、Swap、Profit、Balance。两者都用字符串数组承接,SOrder 和 SDeals 最终以数组形式挂到总结构下,单笔遍历就不会丢上下文。 SSummary 只留四个累计字段——佣金、库存费、总利润、结余。下面这段是上述结构的 MQL5 原样定义,开 MT5 新建 include 头文件粘进去就能编译,下一步往里填 tables 数据即可。

MQL5 / C++
class="kw">struct SSettings{
   class="type">class="kw">string Expert;
   class="type">class="kw">string Symbol;
   class="type">class="kw">string Period;
   class="type">class="kw">string Inputs;
   class="type">class="kw">string Broker;
   class="type">class="kw">string Currency;
   class="type">class="kw">string InitialDeposit;
   class="type">class="kw">string Leverage;
};
class="kw">struct SResults{
   class="type">class="kw">string HistoryQuality;
   class="type">class="kw">string Bars;
   class="type">class="kw">string Ticks;
   class="type">class="kw">string Symbols;
   class="type">class="kw">string TotalNetProfit;
   class="type">class="kw">string BalanceDrawdownAbsolute;
   class="type">class="kw">string EquityDrawdownAbsolute;
   class="type">class="kw">string GrossProfit;
   class="type">class="kw">string BalanceDrawdownMaximal;
   class="type">class="kw">string EquityDrawdownMaximal;
   class="type">class="kw">string GrossLoss;
   class="type">class="kw">string BalanceDrawdownRelative;
   class="type">class="kw">string EquityDrawdownRelative;
   class="type">class="kw">string ProfitFactor;
   class="type">class="kw">string ExpectedPayoff;
   class="type">class="kw">string MarginLevel;
   class="type">class="kw">string RecoveryFactor;
   class="type">class="kw">string SharpeRatio;
   class="type">class="kw">string ZScore;
   class="type">class="kw">string AHPR;
   class="type">class="kw">string LRCorrelation;
   class="type">class="kw">string OnTesterResult;
   class="type">class="kw">string GHPR;
   class="type">class="kw">string LRStandardError;
   class="type">class="kw">string TotalTrades;
   class="type">class="kw">string ShortTrades_won_pers;
   class="type">class="kw">string LongTrades_won_perc;
   class="type">class="kw">string TotalDeals;
   class="type">class="kw">string ProfitTrades_perc_of_total;
   class="type">class="kw">string LossTrades_perc_of_total;
   class="type">class="kw">string LargestProfitTrade;
   class="type">class="kw">string LargestLossTrade;
   class="type">class="kw">string AverageProfitTrade;
   class="type">class="kw">string AverageLossTrade;
   class="type">class="kw">string MaximumConsecutiveWins_cur;
   class="type">class="kw">string MaximumConsecutiveLosses_cur;
   class="type">class="kw">string MaximalConsecutiveProfit_count;
   class="type">class="kw">string MaximalConsecutiveLoss_count;
   class="type">class="kw">string AverageConsecutiveWins;
   class="type">class="kw">string AverageConsecutiveLosses;
   class="type">class="kw">string Correlation_Profits_MFE;
   class="type">class="kw">string Correlation_Profits_MAE;
   class="type">class="kw">string Correlation_MFE_MAE;    
   class="type">class="kw">string MinimalPositionHoldingTime;
   class="type">class="kw">string MaximalPositionHoldingTime;
   class="type">class="kw">string AveragePositionHoldingTime;
};
class="kw">struct SOrder{
   class="type">class="kw">string OpenTime;
   class="type">class="kw">string Order;
   class="type">class="kw">string Symbol;
   class="type">class="kw">string Type;
   class="type">class="kw">string Volume;
   class="type">class="kw">string Price;
   class="type">class="kw">string SL;
   class="type">class="kw">string TP;
   class="type">class="kw">string Time;
   class="type">class="kw">string State;
   class="type">class="kw">string Comment;
};
class="kw">struct SDeal{
   class="type">class="kw">string Time;
   class="type">class="kw">string Deal;
   class="type">class="kw">string Symbol;
   class="type">class="kw">string Type;
   class="type">class="kw">string Direction;
   class="type">class="kw">string Volume;
   class="type">class="kw">string Price;
   class="type">class="kw">string Order;
   class="type">class="kw">string Commission;
   class="type">class="kw">string Swap;
   class="type">class="kw">string Profit;
   class="type">class="kw">string Balance;
   class="type">class="kw">string Comment;
};
class="kw">struct SSummary{
   class="type">class="kw">string Commission;
   class="type">class="kw">string Swap;
   class="type">class="kw">string Profit;
   class="type">class="kw">string Balance;
};

◍ 回测报告的数据骨架

在 MT5 策略测试器里跑完 EA 后,想用代码把整段历史拆开看,先得定义一套能装下全部过程数据的结构。下面这个结构体把配置、结果、逐笔订单、逐笔成交和汇总塞进同一个对象,后续序列化或二次统计都直接读它。 高风险提示:外汇与贵金属杠杆品种回测结果仅代表历史样本,样本外表现可能显著衰减,不能直接当作实盘依据。 结构体里 SOrder Orders[] 与 SDeal Deals[] 用了动态数组,意味着一笔品种从开仓到平仓可能对应多笔 deal,但只归一条 order;做成交明细还原时要按 ticket 对齐,别把数组下标当订单号用。

MQL5 / C++
class="kw">struct STestingReport{
  SSettings Settings;
  SResults Results;
  SOrder Orders[];
  SDeal Deals[];
  SSummary Summary;
};

把回测报告表格塞进结构体

MT5 生成的 HTML 测试报告本质是带 table/tr/td 标签的文本,想用程序批量比对待优化参数,得先把它解析进自定义结构。上面这段代码做的就是这件事:传入报告文件名与引用结构的 STestingReport,先把文件内容读出来,再用 TagsToArray 把 table、tr、td 逐级拆成二维数组。 设置区的数据位置是固定的。Expert 在 tables[0].tr[4].td[1],Symbol 在 tr[5],Period 在 tr[6],Inputs 初值在 tr[7];从第 8 行起,只要该行第 0 个单元格为空,就说明还是输入参数,循环把它们用逗号和初值拼起来。实测一个 12 个外部输入的交易员 EA,Inputs 字段会拼出含 11 个逗号的字符串。 订单与成交的边界靠空单元格和列数判断。订单列表开头记下起始行,遇到只含单个单元格的行就视为订单段结束,再跳过三行开始抓成交;成交段结尾由含 6 个单元格的那行定位,那行存的是最终入金状态。结构填完即可直接进下一步统计,外汇与贵金属回测请留意点差和滑点假设失真带来的高风险,结论仅代表历史样本下的可能表现。

MQL5 / C++
aTestingReport.Settings.Expert=tables[class="num">0].tr[class="num">4].td[class="num">1];
aTestingReport.Settings.Symbol=tables[class="num">0].tr[class="num">5].td[class="num">1];
aTestingReport.Settings.Period=tables[class="num">0].tr[class="num">6].td[class="num">1];
aTestingReport.Settings.Inputs=tables[class="num">0].tr[class="num">7].td[class="num">1];
class="type">int i=class="num">8;
class="kw">while(i<ArraySize(tables[class="num">0].tr) && tables[class="num">0].tr[i].td[class="num">0]==""){
   aTestingReport.Settings.Inputs=aTestingReport.Settings.Inputs+", "+tables[class="num">0].tr[i].td[class="num">1];
   i++;
}
class="type">bool TestingHTMLReportToStruct(class="type">class="kw">string aFileName,STestingReport & aTestingReport){
   STable tables[];
   class="type">class="kw">string FileContent;
   if(!FileGetContent(aFileName,FileContent)){
      class="kw">return(true);
   }
   class="type">class="kw">string tags[]={"td","th"};
   class="type">class="kw">string ttmp[],trtmp[],tdtmp[];
   class="type">int tcnt,trcnt,tdcnt;
   tcnt=TagsToArray(FileContent,"table",ttmp);
   ArrayResize(tables,tcnt);
   for(class="type">int i=class="num">0;i<tcnt;i++){
      trcnt=TagsToArray(ttmp[i],"tr",trtmp);
      ArrayResize(tables[i].tr,trcnt);
      for(class="type">int j=class="num">0;j<trcnt;j++){
         tdcnt=TagsToArray(trtmp[j],tags,tdtmp);
         ArrayResize(tables[i].tr[j].td,tdcnt);
         for(class="type">int k=class="num">0;k<tdcnt;k++){
            tables[i].tr[j].td[k]=RemoveTags(tdtmp[k]);
         }
      }
   }
   class=class="str">"cmt">// 设置部分
   aTestingReport.Settings.Expert=tables[class="num">0].tr[class="num">4].td[class="num">1];
   aTestingReport.Settings.Symbol=tables[class="num">0].tr[class="num">5].td[class="num">1];
   aTestingReport.Settings.Period=tables[class="num">0].tr[class="num">6].td[class="num">1];
   aTestingReport.Settings.Inputs=tables[class="num">0].tr[class="num">7].td[class="num">1];
   class="type">int i=class="num">8;
   class="kw">while(i<ArraySize(tables[class="num">0].tr) && tables[class="num">0].tr[i].td[class="num">0]==""){
      aTestingReport.Settings.Inputs=aTestingReport.Settings.Inputs+", "+tables[class="num">0].tr[i].td[class="num">1];
      i++;
   }
   aTestingReport.Settings.Broker=tables[class="num">0].tr[i++].td[class="num">1];
   aTestingReport.Settings.Currency=tables[class="num">0].tr[i++].td[class="num">1];
   aTestingReport.Settings.InitialDeposit=tables[class="num">0].tr[i++].td[class="num">1];
   aTestingReport.Settings.Leverage=tables[class="num">0].tr[i++].td[class="num">1];
   class=class="str">"cmt">// 结果部分
   i+=class="num">2;
   aTestingReport.Results.HistoryQuality=tables[class="num">0].tr[i++].td[class="num">1];
   aTestingReport.Results.Bars=tables[class="num">0].tr[i].td[class="num">1];
   aTestingReport.Results.Ticks=tables[class="num">0].tr[i].td[class="num">3];
让小布替你跑这套
报告解析和隐秘数值重算这些重复劳动,小布盯盘的 AIGC 已内置,打开对应品种页即可看到自动化提炼的关键指标,你只管读结论做决策。

常见问题

HTML 报告以表格标签嵌套为主,可用通用 HTML 解析思路;XML 优化报告侧重节点树与属性,需按元素名遍历,二者提取函数不宜混用。
可以,小布盯盘内置了报告解析与重算逻辑,接入品种页后能自动呈现标准报告之外的分布类数值,省去自己写解析函数。
因为颜色属性会持续作用于后续文本,只有用 </p> 封闭才能中止涂色,否则整份文档后续文字都会继承该颜色样式。
结构体直接映射字段,可随时按任意度量值生成自有报告,避免每次都重新遍历文本,扩展计算更顺手。
关于自定义报告生成的完整讨论见《利用 HTML 报告分析交易结果·实战篇》。