MQL5 Cookbook: 减少过度配合的影响以及处理报价缺失(基础篇)
「过拟合与缺报价的底层坑」
做 MT5 策略回测时,过度配合(overfitting)常来自样本太短或参数空间乱扫。一个常见现象是:在 2013 年欧元兑美元 M15 上精调出的参数,换到 2014 年同周期往往失效,这指向样本内拟合掩盖了真实分布。外汇与贵金属属高风险品种,这种失效可能直接放大实盘回撤。 另一个被低估的问题是报价缺失。实盘tick流在流动性差的时段(如周日重开、节假日)会断点,若 EA 只认『每根 bar 必完整』,就会漏算信号或重复下单。MT5 的 CopyTicks() 拉到的 tick 数组,遇到空段返回 0 条也是正常行为,不是 bug。 处理思路是先做样本外切割,再对缺失报价做显式判断,而不是假装数据连续。下面这段用于检测当前品种最近 N 毫秒内是否有 tick 注入,没有就直接跳过交易逻辑。
class="type">int ticks=CopyTicks(Symbol(),NULL,COPY_TICKS_ALL,class="num">0,class="num">100); if(ticks==class="num">0) { class=class="str">"cmt">// 最近无报价,退出本次处理 class="kw">return; } class=class="str">"cmt">// 否则继续正常策略逻辑
参数怎么选才不算自欺欺人
不少交易者拿到回测里跑得漂亮的参数就心里打鼓:这玩意儿上了实盘还能不能活?核心矛盾不在于算法本身够不够复杂,而在于你拿什么样本、怎么验证它。 常规做法是先用优化期(回测)挑出好参数,再丢进一段没参与优化的前瞻期去验。但前瞻测试并非必须,它的结论完全可以从历史数据里反推出来。 真正的坑是:到底喂多少历史数据才够优化?这没有标准答案,取决于你想捕捉的波动尺度。对小时图内的短线交易,现有数据通常够用;换到日线以上周期,同样的数据量就未必撑得住了。 一个可验证的规律是:重复模式越多、成交次数越多,统计结论对未来效率的可信度就越高。若某品种自身数据太少、重复样本不足,让人心里没底,最直接的解法是把同类型所有可用资产的数据合并进来一起算。外汇与贵金属波动剧烈,合并数据能降疑但无法消除爆仓风险。
◍ 多品种通用参数到底行不行
在动手写 MT5 程序前,先看一个 NeuroShell DayTrader 专业版里的现成例子。它的交易模块设置里能针对多个品种编译的系统做参数优化:既可以给每个品种单独搜最优参数,也能在「优化」页里强行找一套全品种通用的参数集。 作者用 8 个交易品种、2000—2013 年的日线柱做了对照测试。上方曲线是各品种独立优化后的结果,下方是套同一组通用参数的结果。 数据上,分品种独立优化明显比通用参数好看;但通用参数虽弱,却更能证明系统在不同价格模式(波动幅度、趋势/震荡占比)下都还站得住。外汇与贵金属属高风险品种,这种跨品种稳健性只是概率上的参考,不是保本凭证。 顺着这个思路,用更长时间窗口的数据去优化更合理。像 EURUSD 的价格行为在两年、五年甚至十年里可能完全变脸,而某段 GBPUSD 的走势又可能和早年的 EURUSD 高度相似。任何资产都有这种代际切换,做跨品种系统时要提前把这个当常态。
「用 TXT 分区列表接管多币种优化范围」
MT5 策略测试器自带的「市场报价所选品种」模式只是逐品种跑当前参数,并不做优化。想自己控盘测哪些品种、分几组测,可以绕开它:把品种名写进一个 txt,用带 # 的区段编号切分集合,EA 按外部参数去读对应区段。 文件必须落在终端本地 Metatrader 5\MQL5\Files 下;若放通用目录,MQL5 云网络优化就用不了。区段抬头前得有 # 符号,EA 靠它定位集合起点,换分隔符就得同步改代码里的判断逻辑。 外部参数加一个 SectionOfSymbolList(整型,≥0),指定本次测第几组。若数值超出文件里实际集合数,EA 往日志写告警,然后只在当前图表品种上跑。原多币种 EA 里那些外部参数数组全删掉,只留动态数组 InputSymbols[],大小由 txt 里该集合的品种数决定;实时模式下数组长度恒为 1。 报告侧也加了料:除各品种余额曲线、局部最大回撤百分比外,现在把回撤金额也写进文件,便于横向比哪组品种吃掉更多权益。下面这段是 EA 头部的文件声明与外部参数定义,云优化要读外部文件就得靠 tester_file 那行。
class=class="str">"cmt">//--- 允许云网络在优化过程中访问外部文件和指标 class="macro">#class="kw">property tester_file "SymbolsList.txt" class="macro">#class="kw">property tester_indicator "EventsSpy.ex5" class=class="str">"cmt">//--- EA交易的外部参数 sinput class="type">int SectionOfSymbolList = class="num">1; class=class="str">"cmt">// 交易品种列表中的区域编号 sinput class="type">bool UpdateReport = false; class=class="str">"cmt">// 更新报告 sinput class="type">class="kw">string delimeter_00=""; class=class="str">"cmt">// -------------------------------- sinput class="type">long MagicNumber = class="num">777; class=class="str">"cmt">// 幻数 sinput class="type">int Deviation = class="num">10; class=class="str">"cmt">// 滑点 sinput class="type">class="kw">string delimeter_01=""; class=class="str">"cmt">// -------------------------------- input class="type">int IndicatorPeriod = class="num">5; class=class="str">"cmt">// 指标周期数 input class="type">class="kw">double TakeProfit = class="num">100; class=class="str">"cmt">// 获利 input class="type">class="kw">double StopLoss = class="num">50; class=class="str">"cmt">// 止损 input class="type">class="kw">double TrailingStop = class="num">10; class=class="str">"cmt">// 跟踪止损 input class="type">bool Reverse = true; class=class="str">"cmt">// 反向仓位 input class="type">class="kw">double Lot = class="num">0.1; class=class="str">"cmt">// 手数 input class="type">class="kw">double VolumeIncrease = class="num">0.1; class=class="str">"cmt">// 仓位交易量增加值 input class="type">class="kw">double VolumeIncreaseStep = class="num">10; class=class="str">"cmt">// 交易量增加步长 class=class="str">"cmt">//+------------------------------------------------------------------+
品种列表的兜底与回退逻辑
在 MT5 里做多品种扫描,最怕文件里的品种名写错或者段号越界。这段初始化函数把退路留得很死:一旦 SYMBOLS_COUNT 为 0,不管是测试还是实时,都只盯当前图表品种 _Symbol,不会让 EA 直接崩掉。
测试模式下它会去读 SymbolsList.txt,用 ReadSymbolsFromFile 取字符串数并填临时数组;若 Section of List Symbols 参数比文件实际段数大,就会打印提示并退回到单品种。实时模式(IsRealtime() 为真)则根本不读文件,直接走 message_02 那条路。
外汇与贵金属杠杆高、滑点跳空频繁,多品种回测若品种不可交易却硬跑,结果会严重失真。把这段抄进你的 InitializeArraySymbols(),开 MT5 用策略测试器故意填错文件名,看日志是否只落一句「只处理当前交易品种」即可验证。
class="type">void InitializeArraySymbols() { class="type">int strings_count =class="num">0; class=class="str">"cmt">// 交易品种文件的字符串数量 class="type">class="kw">string checked_symbol =""; class=class="str">"cmt">// 检查交易品种在交易服务器上是否可以访问 class=class="str">"cmt">//--- 测试模式的信息 class="type">class="kw">string message_01="<--- 在 <- SymbolsList.txt -> 文件中的交易品种名称不正确 ... --->\n" "<--- ... 或者 \"Section of List Symbols\" 参数的值比 " "文件段数大!--- >\n" "<--- 这样我们只会测试当前交易品种. --->"; class=class="str">"cmt">//--- 实时模式下的信息 class="type">class="kw">string message_02="<--- 在实时模式下, 我们只处理当前交易品种. --->"; class=class="str">"cmt">//--- 如果是实时模式 if(!IsRealtime()) { class=class="str">"cmt">//--- 从文件中取得指定交易品种区域字符串的数量并填充交易品种的临时数组 strings_count=ReadSymbolsFromFile("SymbolsList.txt"); class=class="str">"cmt">//--- 从指定的集合中迭代所有交易品种 for(class="type">int s=class="num">0; s<strings_count; s++) { class=class="str">"cmt">//--- 如果检查交易品种之后返回了正确的字符串 if((checked_symbol=GetSymbolByName(temporary_symbols[s]))!="") { class=class="str">"cmt">//--- 增加计数器 SYMBOLS_COUNT++; class=class="str">"cmt">//--- 设置/增加数组大小 ArrayResize(InputSymbols,SYMBOLS_COUNT); class=class="str">"cmt">//--- 交易品种名称索引 InputSymbols[SYMBOLS_COUNT-class="num">1]=checked_symbol; } } } class=class="str">"cmt">//--- 如果全部交易品种名称没有正确输入或者目前是工作在实时模式下 if(SYMBOLS_COUNT==class="num">0) { class=class="str">"cmt">//--- 实时模式信息 if(IsRealtime()) Print(message_02); class=class="str">"cmt">//--- 测试模式信息 if(!IsRealtime()) Print(message_01); class=class="str">"cmt">//--- 我们只处理当前交易品种 SYMBOLS_COUNT=class="num">1; class=class="str">"cmt">//--- 设置数组大小并且 ArrayResize(InputSymbols,SYMBOLS_COUNT); class=class="str">"cmt">//--- 记录当前交易品种名称索引 InputSymbols[class="num">0]=_Symbol; } } class="type">int ReadSymbolsFromFile(class="type">class="kw">string file_name) { class="type">ulong offset =class="num">0; class=class="str">"cmt">// 确定文件指针位置的偏移量 class="type">class="kw">string delimeter ="#"; class=class="str">"cmt">// 区段起始部分的标示符 class="type">class="kw">string read_line =""; class=class="str">"cmt">// 用于检查读取的字符串