MetaTrader 5中的蒙特卡罗置换测试·综合运用
(3/3)· 前两步铺垫了概念与单EA改造,这里把随机置换价格、自动脚本与实跑示例拼成完整闭环
「用基类 tick 批量造自定义品种的类骨架」
想在 MT5 里做价格行为回测,又嫌真实历史太少?可以写一个类,把某个基类品种(比如 XAUUSD)的 tick 数据抽出来做排列,生成一批自定义符号。下面这个类 CGenerateSymbols 就是干这事的基础结构。 类的私有成员里,m_basesymbol 存基类名,m_symbols_id 给新符号加统一前缀,m_tickrangestart 和 m_tickrangestop 用 long 型毫秒时间戳框定抽取区间,m_permutations 记录要生成多少组排列。m_baseticks 装原始 tick,m_permutedticks 装洗牌后的 tick,m_csymbols 是指向已建符号对象的指针数组,m_shuffler 负责具体打乱。 Initiate 方法是入口:先清掉上次的排列计数,SymbolSelect 把基类加进观察列表,否则直接返回 false 并打印错误。日期区间若 start>=stop 会被拒,合法时乘 1000 转成毫秒存进成员变量。最后检查 m_shuffler 指针,无效就报错——这部分截断在原文里,但逻辑很直接:对象没建起来后面没法洗数据。 开 MT5 自己验证时,注意自定义品种和真实账户交易无关,外汇贵金属杠杆高、自定义数据不代表未来概率,仅作策略压力测试用。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|CGenerateSymbols class | class=class="str">"cmt">//| creates custom symbols from an existing base symbol&class="macro">#x27;s tick data | class=class="str">"cmt">//| symbols represent permutations of base symbol&class="macro">#x27;s ticks | class=class="str">"cmt">//+------------------------------------------------------------------+ class CGenerateSymbols { class="kw">private: class="type">class="kw">string m_basesymbol; class=class="str">"cmt">//base symbol class="type">class="kw">string m_symbols_id; class=class="str">"cmt">//common identifier added to names of new symbols class="type">long m_tickrangestart; class=class="str">"cmt">//beginning date for range of base symbol&class="macro">#x27;s ticks class="type">long m_tickrangestop; class=class="str">"cmt">//ending date for range of base symbol&class="macro">#x27;s ticks class="type">uint m_permutations; class=class="str">"cmt">//number of permutations and ultimately the number of new symbols to create class="type">MqlTick m_baseticks[]; class=class="str">"cmt">//base symbol&class="macro">#x27;s ticks class="type">MqlTick m_permutedticks[];class=class="str">"cmt">//permuted ticks; CNewSymbol *m_csymbols[]; class=class="str">"cmt">//array of created symbols CPermuteTicks *m_shuffler; class=class="str">"cmt">//object used to shuffle tick data class="kw">public: CGenerateSymbols(class="type">void); ~CGenerateSymbols(class="type">void); class="type">bool Initiate(class="kw">const class="type">class="kw">string base_symbol,class="kw">const class="type">class="kw">string symbols_id,class="kw">const class="type">class="kw">datetime start_date,class="kw">const class="type">class="kw">datetime stop_date); class="type">uint Generate(class="kw">const class="type">uint permutations); }; class=class="str">"cmt">//+-----------------------------------------------------------------------------------------+ class=class="str">"cmt">//|set and check parameters for symbol creation, download ticks and initialize tick shuffler| class=class="str">"cmt">//+-----------------------------------------------------------------------------------------+ class="type">bool CGenerateSymbols::Initiate(class="kw">const class="type">class="kw">string base_symbol,class="kw">const class="type">class="kw">string symbols_id,class="kw">const class="type">class="kw">datetime start_date,class="kw">const class="type">class="kw">datetime stop_date) { class=class="str">"cmt">//---reset number of permutations previously done m_permutations=class="num">0; class=class="str">"cmt">//---set base symbol m_basesymbol=base_symbol; class=class="str">"cmt">//---make sure base symbol is selected, ie, visible in WatchList if(!SymbolSelect(m_basesymbol,true)) { Print("Failed to select ", m_basesymbol," error ", GetLastError()); class="kw">return class="kw">false; } class=class="str">"cmt">//---set symbols id m_symbols_id=symbols_id; class=class="str">"cmt">//---check, set ticks date range if(start_date>=stop_date) { Print("Invalid date range "); class="kw">return class="kw">false; } else { m_tickrangestart=class="type">long(start_date)*class="num">1000; m_tickrangestop=class="type">long(stop_date)*class="num">1000; } class=class="str">"cmt">//---check shuffler object if(CheckPointer(m_shuffler)==POINTER_INVALID) { Print("CPermuteTicks object creation failed");
◍ Tick抓取与排列符号的生成循环
下载基准品种的tick是后续所有排列重采样的前提。代码里用 CopyTicksRange 按时间区间拉取 COPY_TICKS_ALL 全量tick,若单次返回小于等于0就 Sleep(500) 后重试,最多跑 MAX_DOWNLOAD_ATTEMPTS 次;连续失败会 Print 错误码并直接返回 false,实盘里若经纪商限制历史tick深度,这里大概率卡在 attempts 耗尽。 Generate() 负责批量造排列符号。入参 permutations 为0会被拒,数组按 m_permutations+permutations 用 ArrayResize 预留空间;循环从已有排列数起,逐个 new CNewSymbol 并拼出形如 BASE_<id>_<序号> 的名称,Create 返回大于0才继续。 克隆基准品种后调用 m_shuffler.Permute 打乱tick,再 TicksAdd 写回新符号。外汇与贵金属tick重采样属于高概率偏离真实市场的合成数据,仅适合做统计压力测试,不能直接当行情信号。
class="kw">return class="kw">false; } class=class="str">"cmt">//---download ticks Comment("Downloading ticks"); class="type">uint attempts=class="num">0; class="type">int downloaded=-class="num">1; class="kw">while(attempts<MAX_DOWNLOAD_ATTEMPTS) { downloaded=CopyTicksRange(m_basesymbol,m_baseticks,COPY_TICKS_ALL,m_tickrangestart,m_tickrangestop); if(downloaded<=class="num">0) { Sleep(class="num">500); ++attempts; } else class="kw">break; } class=class="str">"cmt">//---check download result if(downloaded<=class="num">0) { Print("Failed to get tick data for ",m_basesymbol," error ", GetLastError()); class="kw">return class="kw">false; } Comment("Ticks downloaded"); class=class="str">"cmt">//---class="kw">return shuffler initialization result class="kw">return m_shuffler.Initialize(m_baseticks); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| generate symbols class="kw">return newly created or refreshed symbols | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">uint CGenerateSymbols::Generate(class="kw">const class="type">uint permutations) { class=class="str">"cmt">//---check permutations if(!permutations) { Print("Invalid parameter value for Permutations "); class="kw">return class="num">0; } class=class="str">"cmt">//---resize m_csymbols if(m_csymbols.Size()!=m_permutations+permutations) ArrayResize(m_csymbols,m_permutations+permutations,RESIZE_RESERVE); class=class="str">"cmt">//--- class="type">class="kw">string symspath=m_basesymbol+m_symbols_id+"_PermutedTicks"; class="type">int exists; class=class="str">"cmt">//---do more permutations for(class="type">uint i=m_permutations; i<m_csymbols.Size(); i++) { if(CheckPointer(m_csymbols[i])==POINTER_INVALID) m_csymbols[i]=new CNewSymbol(); exists=m_csymbols[i].Create(m_basesymbol+m_symbols_id+"_"+ class="type">class="kw">string(i+class="num">1),symspath,m_basesymbol); if(exists>class="num">0) { Comment("new symbol created "+m_basesymbol+m_symbols_id+"_"+ class="type">class="kw">string(i+class="num">1) ); if(!m_csymbols[i].Clone(m_basesymbol) || !m_shuffler.Permute(m_permutedticks)) class="kw">break; else { m_csymbols[i].Select(true); Comment("adding permuted ticks"); if(m_csymbols[i].TicksAdd(m_permutedticks)>class="num">0)
用置换重排给已有品种注入扰动 tick
这段逻辑处在品种遍历的 else 分支里:当目标符号已经存在于市场观察列表时,不再新建,而是直接对它动手。先通过 Select(true) 把该品种置为当前活跃符号,后续 tick 操作才有作用对象。 核心动作是调用 m_shuffler.Permute(m_permutedticks) 对原 tick 序列做置换,若返回失败立即 break 退出循环,避免脏数据写入。置换成功后用 TicksReplace 把重排后的 tick 写回品种,返回值大于 0 才把 m_permutations 计数器加一,否则同样 break。 从计数语义看,m_permutations 只统计「真正成功替换 tick 的品种数」,不是遍历次数。你在 MT5 里接这段时,可先打印 m_permutations 与 i 的终值,若两者差很多,说明中途有品种替换失败——外汇与贵金属品种在高波动时可能拒绝 tick 写入,属正常高风险表现,需查日志而非盲目重试。
m_permutations++;
}
}
else
{
Comment("symbol exists "+m_basesymbol+m_symbols_id+"_"+"class="type">class="kw">string"(i+class="num">1) );
m_csymbols[i].Select(true);
if(!m_shuffler.Permute(m_permutedticks))
class="kw">break;
Comment("replacing ticks ");
if(m_csymbols[i].TicksReplace(m_permutedticks)>class="num">0)
m_permutations++;
else
class="kw">break;
}
}
class=class="str">"cmt">//---class="kw">return successful number of permutated symbols
Comment("");
class=class="str">"cmt">//---
class="kw">return m_permutations;
}「用置换品种跑出 p 值的水位线」
拿 MT5 自带的 MACD 示例 EA 做载体,在 AUDUSD 上先跑脚本生成 100 个置换分时品种,再进策略测试器做优化。测试器结果页会按你选的绩效标准降序排这些品种,右上角下拉菜单切标准即可。 原始 AUDUSD 的测试数字落在结果列表靠下位置,有 10 个以上置换品种表现更好,这直接说明 p 值高于 0.05,不用手算也能读出。 但这一轮测试周期太短,结论只能当方法演示,外汇与贵金属杠杆高、价格跳变频繁,真用于实盘前必须把测试期拉长到覆盖多种行情。 想精确算 p 值,右键从测试器导出 xml,用 OpenOffice Calc 打开后删掉无关品种行,在表底加一行写宏:参考置换品种性能(图中第 18 行)和每列对应值,几次点击就能出数。 会 python 或 mql5 的,也可以自己解析 xml,导出时挑个好找的目录就行。
◍ 把这条线请下神坛
置换测试的价值已经说透:它不挑 EA,不需要源码,靠的是不假设分布的稳健统计,能在你掏钱买策略前先戳穿伪优势。但代价也实在——跑一套下来既吃 CPU 也吃硬盘,生成置换品种和分时数据会快速占满空间,普通笔记本可能跑不动完整轮次。 附带的四个文件是落地这套思路的钥匙:GenerateSymbols.mqh 定义置换品种生成类,NewSymbol.mqh 管自定义品种创建,PermuteTicks.mqh 做分时数组洗牌,PrepareSymbolsForPermutationTests.mq5 一键备好测试环境。开 MT5 把 ZIP 里的 9.91 KB 包解进去,先拿自己怀疑的 EA 跑一遍置换,p 值若掉不到 0.05 以下,策略优势倾向只是样本巧合。 下面这段是对分时做对数变换的辅助方法,避免零值和负值捣乱: //+------------------------------------------------------------------+ //|应用对数变换的辅助方法 //+------------------------------------------------------------------+ bool CPermuteTicks::LogTransformTicks(void) { if(m_logticks.Size()!=m_ticks.Size()) ArrayResize(m_logticks,m_ticks.Size()); for(uint i=0; i<m_ticks.Size(); i++) { m_logticks[i].bid_d=(m_ticks[i].bid>0)?MathLog(m_ticks[i].bid):MathLog(1e0); m_logticks[i].ask_d=(m_ticks[i].ask>0)?MathLog(m_ticks[i].ask):MathLog(1e0); m_logticks[i].vol_d=(m_ticks[i].volume>0)?MathLog(m_ticks[i].volume):MathLog(1e0); m_logticks[i].volreal_d=(m_ticks[i].volume_real>0)?MathLog(m_ticks[i].volume_real):MathLog(1e0); } return true; } 逐行看:先按原 tick 数组长度重整日志数组;循环里对 bid、ask、volume、volume_real 四个字段做条件对数变换,大于 0 才取真值对数,否则落到一个 1e0 的安全底上,防止 MathLog 算崩。外汇与贵金属杠杆高、滑点凶,置换结论只减误判概率,不替你挡实盘风险。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|应用对数变换的辅助方法 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CPermuteTicks::LogTransformTicks(class="type">void) { if(m_logticks.Size()!=m_ticks.Size()) ArrayResize(m_logticks,m_ticks.Size()); for(class="type">uint i=class="num">0; i<m_ticks.Size(); i++) { m_logticks[i].bid_d=(m_ticks[i].bid>class="num">0)?MathLog(m_ticks[i].bid):MathLog(class="num">1e0); m_logticks[i].ask_d=(m_ticks[i].ask>class="num">0)?MathLog(m_ticks[i].ask):MathLog(class="num">1e0); m_logticks[i].vol_d=(m_ticks[i].volume>class="num">0)?MathLog(m_ticks[i].volume):MathLog(class="num">1e0); m_logticks[i].volreal_d=(m_ticks[i].volume_real>class="num">0)?MathLog(m_ticks[i].volume_real):MathLog(class="num">1e0); } class="kw">return true; }