MQL5 Cookbook: 减少过度配合的影响以及处理报价缺失·进阶篇
🔭

MQL5 Cookbook: 减少过度配合的影响以及处理报价缺失·进阶篇

(2/3)· 当单一品种历史不够做前瞻验证,借多资产联合优化把未来失效概率压下来

实战向进阶 第 2/3 篇

多数交易者把优化结果锁死在单一品种的历史区间里,却忽略了一个事实:样本重复次数不够,参数再漂亮也只是回测里的幻觉。当某资产日图数据只够跑出几十笔交易,你凭什么相信它在下一季度还管用。

「正文」

&nbsp;&nbsp; <span class="keyword">int</span>&nbsp;&nbsp;&nbsp;&nbsp;limit_count&nbsp;&nbsp;&nbsp;&nbsp;=<span class="number">0</span>;&nbsp;&nbsp; <span class="comment">// 限制可能开启图表数目的计数器</span> &nbsp;&nbsp; <span class="keyword">int</span>&nbsp;&nbsp;&nbsp;&nbsp;strings_count&nbsp;&nbsp;=<span class="number">0</span>;&nbsp;&nbsp; <span class="comment">// 字符串计数器</span> &nbsp;&nbsp; <span class="keyword">int</span>&nbsp;&nbsp;&nbsp;&nbsp;sections_count =-<span class="number">1</span>;&nbsp;&nbsp;<span class="comment">// 区段计数器</span> &nbsp;&nbsp; <span class="comment">//--- Message 01</span> &nbsp;&nbsp; <span class="keyword">string</span> message_01=<span class="string">"&lt;--- &lt;- "</span>+file_name+<span class="string">" -&gt; 文件还没有正确准备好!---&gt;\n"</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="string">"&lt;--- 第一个字符串没有包含区段编号标识符 ("</span>+delimeter+<span class="string">")!---&gt;"</span>; <span class="comment">//--- Message 02</span> &nbsp;&nbsp; <span class="keyword">string</span> message_02=<span class="string">"&lt;--- &lt;- "</span>+file_name+<span class="string">" -&gt; 文件还没有正确准备好!---&gt;\n"</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="string">"&lt;--- 最后的字符串没有换行符号, ---&gt;\n"</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;

◍ 文件游标与图表上限的临界处理

这段逻辑发生在遍历品种列表文件的过程中,核心是解决「文件指针走到行尾」与「MT5 图表总数封顶」两个临界状态。limit_count 每读一段就自增,一旦触到 CHARTS_MAX 宏定义的上限,就调用 PrepareArrayForOneSymbol 收尾并 FileClose 退出,返回已收集的行数。 当 FileIsLineEnding 判定当前位于行末时,先通过 FileTell 取偏移;若未到文件末尾则用 offset++ 跳过换行符,再 FileSeek 以 SEEK_SET 重定位到下一行起点。这里有个区段守卫:sections_count 不等于 SectionOfSymbolList 就直接 break,避免误读其他区块。 只有在区段匹配且 read_line 非空时,strings_count 才累加,并用 ArrayResize 动态扩 temporary_symbols 数组,把当前行写入末位索引。外汇与贵金属品种列表解析属于高频 IO 操作,MT5 终端在极端行情下可能限频,实盘前建议在策略测试器里用样本 txt 跑一遍确认行数对齐。

MQL5 / C++
        class="kw">return(strings_count);
      }
      class=class="str">"cmt">//--- 增加可能开启图表限制的计数器
      limit_count++;
      class=class="str">"cmt">//--- 如果达到了限制
      if(limit_count>=CHARTS_MAX)
        {
         PrepareArrayForOneSymbol(strings_count,message_02);
         class=class="str">"cmt">//--- 关闭文件
         FileClose(file_handle);
         class=class="str">"cmt">//--- 返回文件的字符串数目
         class="kw">return(strings_count);
        }
      class=class="str">"cmt">//--- 取得指针位置
      offset=FileTell(file_handle);
      class=class="str">"cmt">//--- 如果这是字符串末尾
      if(FileIsLineEnding(file_handle))
        {
         class=class="str">"cmt">//--- 如果还没有到文件末尾,读取下一个字符串
         class=class="str">"cmt">//    为了这个目的,增加文件指针的偏移
         if(!FileIsEnding(file_handle))
            offset++;
         class=class="str">"cmt">//--- 移动到下一个字符串
         FileSeek(file_handle,offset,SEEK_SET);
         class=class="str">"cmt">//--- 如果我们不在文件的指定区段,退出循环
         if(sections_count!=SectionOfSymbolList)
            break;
         class=class="str">"cmt">//--- 否则,
         else
           {
            class=class="str">"cmt">//--- 如果字符串不为空
            if(read_line!="")
              {
               class=class="str">"cmt">//--- 增加字符串计数器
               strings_count++;
               class=class="str">"cmt">//--- 增加字符串数组的大小,
               ArrayResize(temporary_symbols,strings_count);
               class=class="str">"cmt">//--- 把字符串写到当前索引处
               temporary_symbols[strings_count-class="num">1]=read_line;
              }
           }
         class=class="str">"cmt">//--- 退出循环
         break;
        }
      }

把回测结果导成可读的 CSV

读取完参数文件后,若句柄无效就直接调用 PrepareArrayForOneSymbol 给当前品种建一个只有 1 行的临时数组,并把 _Symbol 写进下标 0。这一步保证单品种模式下也不会因为数组越界崩在半路。 PrepareArrayForOneSymbol 里把 strings_count 硬性设为 1,再用 ArrayResize(temporary_symbols,1) 定型。实盘里若你改了逻辑想支持多品种,这里不跟着放大就会截断数据。

真正的落盘在 CreateSymbolBalanceReport:先尝试在通用目录建 LastTest.csv,标志位用 FILE_CSVFILE_WRITEFILE_ANSIFILE_COMMON。文件句柄大于 0 才继续,否则静默 return,不会抛错到图表。

函数内预置了 digits、deals_total、ticket、drawdown_max、balance 等变量,其中 percent_drawdown 与 money_drawdown 用了 static 修饰,意味着两次调用之间回撤值会保留上次结果。外汇与贵金属杠杆高,回测最大回撤仅代表历史样本,换周期可能明显放大,请仅作概率参考。 写入前先拼 headers 字符串 "TIME,SYMBOL,DEAL TYPE,ENTRY TYPE,VOLUME,",分隔符锁定为逗号。你在 MT5 里跑完策略测试,打开终端公共目录下的 LastTest.csv 就能直接看每笔成交的概览。

MQL5 / C++
if(FileIsEnding(file_handle))
   break;
}
FileClose(file_handle);
 }
 else
   PrepareArrayForOneSymbol(strings_count,message_03);
class=class="str">"cmt">//--- 返回文件字符串数量
 class="kw">return(strings_count);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 为一个交易品种准备一个数组                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void PrepareArrayForOneSymbol(class="type">int &strings_count,class="type">class="kw">string message)
 {
 Print(message);
 strings_count=class="num">1;
 ArrayResize(temporary_symbols,strings_count);
 temporary_symbols[class="num">0]=_Symbol;
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建 .csv 格式的交易测试报告                                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CreateSymbolBalanceReport()
 {
 class="type">int    file_handle =INVALID_HANDLE; class=class="str">"cmt">// 文件句柄
 class="type">class="kw">string path        ="";             class=class="str">"cmt">// 文件路径
 if((path=CreateInputParametersFolder())=="")
    class="kw">return;
 file_handle=FileOpen(path+"\\LastTest.csv",FILE_CSV|FILE_WRITE|FILE_ANSI|FILE_COMMON);
 if(file_handle>class="num">0)
    {
    class="type">int    digits           =class="num">0;    class=class="str">"cmt">// 价格中的小数点位数
    class="type">int    deals_total       =class="num">0;    class=class="str">"cmt">// 指定历史的交易数量
    class="type">ulong  ticket            =class="num">0;    class=class="str">"cmt">// 交易订单号
    class="type">class="kw">double drawdown_max      =class="num">0.0;  class=class="str">"cmt">// 回撤
    class="type">class="kw">double balance           =class="num">0.0;  class=class="str">"cmt">// 余额
    class="type">class="kw">string delimeter         =",";  class=class="str">"cmt">// 分隔符
    class="type">class="kw">string string_to_write   ="";   class=class="str">"cmt">// 生成写入的字符串
    class="kw">static class="type">class="kw">double percent_drawdown =class="num">0.0; class=class="str">"cmt">// 回撤表现为百分率
    class="kw">static class="type">class="kw">double money_drawdown   =class="num">0.0; class=class="str">"cmt">// 回撤钱数
    class="type">class="kw">string headers="TIME,SYMBOL,DEAL TYPE,ENTRY TYPE,VOLUME,"

「多品种回测报告的逐笔落盘」

做跨品种策略复盘时,最直接的坑是报告抬头写死成单品种字段。下面这段逻辑在 SYMBOLS_COUNT>1 时,用循环把 InputSymbols 数组里的每个品种名追加到 headers 字符串后面,保证 CSV 第一行就能区分多列数据源。 文件写入前必须先 HistorySelect(0,TimeCurrent()) 拉全历史,再用 HistoryDealsTotal() 拿到 deals_total。注意这里 ArrayResize(symbol_balance, SYMBOLS_COUNT) 和每个品种的 balance 子数组都按 deals_total resized,否则多品种资金曲线会越界报错。 逐笔循环里,balance+=deal.profit+deal.swap+deal.commission 是净值累加,而 TesterDrawdownMaximum(i,balance,percent_drawdown,money_drawdown) 实时算从局部峰值的回撤。最后 StringConcatenate 把时间、品种、类型、开平仓、手数、价格(按 SYMBOL_DIGITS 取小数位)、库存费、利润、两项回撤、余额拼成一行写盘——开 MT5 把这段塞进 EA 的 OnTester 或独立脚本,跑完能在文件目录拿到可透视的逐笔账单。外汇与贵金属杠杆高,回测最大回撤仅代表历史样本,实盘可能放大。

MQL5 / C++
  "PRICE,SWAP($),PROFIT($),DRAWDOWN(%),DRAWDOWN($),BALANCE";
  class=class="str">"cmt">//--- 如果选了多个交易品种, 修改抬头字符串
  if(SYMBOLS_COUNT>class="num">1)
    {
     for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++)
        StringAdd(headers,","+InputSymbols[s]);
    }
  class=class="str">"cmt">//--- 写报告抬头
  FileWrite(file_handle,headers);
  class=class="str">"cmt">//--- 取得完整历史
  HistorySelect(class="num">0,TimeCurrent());
  class=class="str">"cmt">//--- 取得交易数量
  deals_total=HistoryDealsTotal();
  class=class="str">"cmt">//---根据交易品种数量修改余额数组大小
  ArrayResize(symbol_balance,SYMBOLS_COUNT);
  class=class="str">"cmt">//--- 修改每个交易品种的交易数组大小
  for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++)
    ArrayResize(symbol_balance[s].balance,deals_total);
  class=class="str">"cmt">//--- 在循环中迭代并写入数据
  for(class="type">int i=class="num">0; i<deals_total; i++)
    {
     class=class="str">"cmt">//--- 取得交易订单号码
     ticket=HistoryDealGetTicket(i);
     class=class="str">"cmt">//--- 取得所有的交易属性
     GetHistoryDealProperties(ticket,D_ALL);
     class=class="str">"cmt">//--- 取得价格中的小数位数
     digits=(class="type">int)SymbolInfoInteger(deal.symbol,SYMBOL_DIGITS);
     class=class="str">"cmt">//--- 计算总体余额
     balance+=deal.profit+deal.swap+deal.commission;
     class=class="str">"cmt">//--- 计算从局部最大值的最大回撤
     TesterDrawdownMaximum(i,balance,percent_drawdown,money_drawdown);
     class=class="str">"cmt">//--- 使用连接生成用于写入的字符串
     StringConcatenate(string_to_write,
                  deal.time,delimeter,
                  DealSymbolToString(deal.symbol),delimeter,
                  DealTypeToString(deal.type),delimeter,
                  DealEntryToString(deal.entry),delimeter,
                  DealVolumeToString(deal.volume),delimeter,
                  DealPriceToString(deal.price,digits),delimeter,
                  DealSwapToString(deal.swap),delimeter,
                  DealProfitToString(deal.symbol,deal.profit),delimeter,
                  DrawdownToString(percent_drawdown),delimeter,
                  DrawdownToString(money_drawdown),delimeter,
                  DoubleToString(balance,class="num">2));

◍ 多品种余额的逐笔累加逻辑

当监控列表里挂了不止一个交易品种(SYMBOLS_COUNT>1)时,就不能只写总账户曲线,得把每个品种的净余额分开记。下面这段就是按成交记录逐笔往对应品种余额上叠加减值的实现。 核心判断在于 deal.symbol 是否命中当前遍历的品种,且 deal.profit 不等于 0 才视为有效交易结果。此时把上一根索引的余额,加上该笔的 profit、swap、commission 三项,得到本索引的新余额,并用 DoubleToString 保留 2 位小数拼进写出字符串。 若这笔成交不属于当前品种,则走 else 分支:如果是 DEAL_TYPE_BALANCE 类型(通常是入金首笔),所有品种共用同一个 balance 值;否则直接复制 i-1 的前值,保证 CSV 里每个品种每一行都有连续数值,不会出现断档。 开 MT5 把这段嵌进你的成交回放脚本,跑一个跨品种实盘账户,重点看 swap 和 commission 是否被重复计入——这是多品种余额报表最容易算错的地方。外汇与贵金属杠杆交易风险较高,余额曲线仅作技术验证,不构成任何方向建议。

MQL5 / C++
   class=class="str">"cmt">//--- 如果选择了多个交易品种,写下它们的余额
   if(SYMBOLS_COUNT>class="num">1)
     {
      class=class="str">"cmt">//--- 迭代所有交易品种
      for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++)
        {
         class=class="str">"cmt">//--- 如果交易品种相同而交易结果不等于零
         if(deal.symbol==InputSymbols[s] && deal.profit!=class="num">0)
           {
            class=class="str">"cmt">//--- 显示对应交易品种的交易余额
            class=class="str">"cmt">//    考虑库存费和手续费
            symbol_balance[s].balance[i]=symbol_balance[s].balance[i-class="num">1]+
                                         deal.profit+
                                         deal.swap+
                                         deal.commission;
            class=class="str">"cmt">//--- 加到字符串上
            StringAdd(string_to_write,","+DoubleToString(symbol_balance[s].balance[i],class="num">2));
           }
         class=class="str">"cmt">//--- 否则写下前值
         else
           {
            class=class="str">"cmt">//--- 如果交易类型为 "余额" (第一笔交易)
            if(deal.type==DEAL_TYPE_BALANCE)
              {
               class=class="str">"cmt">//--- 余额对所有交易品种都相同
               symbol_balance[s].balance[i]=balance;
               StringAdd(string_to_write,","+DoubleToString(symbol_balance[s].balance[i],class="num">2));
              }
            class=class="str">"cmt">//--- 否则在当前索引写下前值
            else
              {
               symbol_balance[s].balance[i]=symbol_balance[s].balance[i-class="num">1];
               StringAdd(string_to_write,","+DoubleToString(symbol_balance[s].balance[i],class="num">2));
              }
           }
        }
     }
把跨品种验证交给小布盯盘
小布盯盘的 AIGC 已内置多品种参数稳健性诊断,打开对应品种页即可看到联合优化后的通用解与单品种解的偏差提示,你只需判断要不要采纳。

常见问题

可引入所有可用资产的数据做联合优化,用更大的样本量提升参数在未来时段的可信度,但需注意通用参数在单品种上可能略逊于独立优化。
并非强制,相关结论可通过合理切分历史数据获得,但样本外验证能暴露过度配合倾向,建议作为常规流程。
目前小布提供诊断视图而非代跑优化,你把 MT5 导出的联合优化结果贴入对应品种页,它能标出哪些参数跨品种衰减最明显。
思路通用:在优化器里选多品种同参数集模式即可;但 MQL5 需自行用脚本聚合报价,预处理成本高于现成商业模块。
会,缺失段若不做对齐填补,会让该品种权重失真;下篇将专门拆解用代理序列补报价的实现方式。