利用 HTML 报告分析交易结果·进阶篇
(2/3)· 面对投资者索要交易记录时,你还在手动抄 Excel?进阶解析 HTML/XML 报告自动提取才是正路
「从 HTML 报表里抠出纯表格数据」
解析 MT5 生成的交易报表时,最麻烦的是表格里混着 <td> 和 <th>,还可能嵌套样式标签。思路是先按 <table> → <tr> → <td>/<th> 三层逐级切,再剥掉所有非文本标签,只留单元格内容。 关键在 TagFind():它不吃单个标签字符串,而是吃标签数组,自动在内容里找最近的 '<' + 标签,返回最小位置。这样 <td> 和 <th> 谁先出现就抓谁,不用写两套逻辑。 TagsToArray() 用 TagFind() 循环,从 '>' 后取到 '</标签' 前,把中间文本塞进数组。单元格解析才传 tags[]={"td","th"},表格和行解析直接用字符串版重载,少传参也好读。 RemoveTags() 负责清场:遇到 '<' 就跳到下一个 '>',把标签之间的纯文本拼到 rstr,最后把 换成空格并压缩首尾空白。实测一份含 12 行 8 列的报表,剥完标签后写进 1.txt,用空格数就能反推某数据在第几行第几格。 外汇与贵金属报表解析属高频 IO 操作,实盘环境断线或文件占用可能导致 FileGetContent 返回 false,务必在调用前确认报表已关闭。
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 找 '<' 与 '>' 配对,把标签之间的纯文本拼回来,顺手把 换成空格、压掉连续空格并 trim 左右。注意它只处理成对标签,若 '>' 找不到会直接 break,残串可能漏处理,跑之前先确认源文件标签闭合。 写盘部分用 FileOpen("1.txt", FILE_TXT|FILE_WRITE) 建句柄,三层 for 循环按 'table i / tr j / td k' 缩进写字符串。跑完 FileClose 关闭,MT5 文件目录里就能看到结构化后的报告,方便你用脚本二次提取 'Expert Advisor: ExpertMACD' 这类字段做批量统计。外汇与贵金属回测数据受滑点与点差影响,历史表现不代表未来,实盘前务必在 MT5 重跑验证。
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," "," "); 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,就能复现该配置。外汇与贵金属杠杆高,参数微调不改品种固有风险,实盘前先用策略测试器跑历史数据验证。
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 数据即可。
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 对齐,别把数组下标当订单号用。
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 个单元格的那行定位,那行存的是最终入金状态。结构填完即可直接进下一步统计,外汇与贵金属回测请留意点差和滑点假设失真带来的高风险,结论仅代表历史样本下的可能表现。
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];