MetaTrader 5中的蒙特卡罗置换测试·进阶篇
(2/3)·靠单次回测下结论太冒险?用随机重排价格序列拆穿EA的偶然盈利
把真实tick喂进置换类的初始化门槛
做 tick 序列随机置换前,得先通过 Initialize() 把原始 MqlTick 数组塞进类里。这里有个硬门槛:传入数组长度必须 ≥5,否则直接返回 false 并打印 Insufficient amount of data supplied,意味着少于 5 个 tick 的样本连差分都做不出来。 初始化里先 ArrayCopy 到本地 m_ticks,再按 m_ticks.Size()-1 重设 m_differenced 尺寸,随后跑 LogTransformTicks() 做对数变换。之后从 i=1 开始循环,把相邻 log tick 的 bid_d、ask_d、vol_d、volreal_d 逐项相减填进 m_differenced,完成一阶差分。 Permute() 调用前必须确认 m_initialized 为 true,否则报 not initialized。输出数组若尺寸不对会自动 ArrayResize 到 m_ticks.Size()。下面这段是 Initialize 与 Permute 骨架,注意 Permute 里用 GetTickCount64() 取种子、while(i>1) 做倒序抽取——典型的 Fisher-Yates 思路,但原文在此处截断。 外汇与贵金属 tick 数据高频易变,实盘回放置换仅用于假设检验,不预示任何方向,杠杆品种风险极高。
~CPermuteTicks(class="type">void); class="type">bool Initialize(class="type">MqlTick &in_ticks[]); class="type">bool Permute(class="type">MqlTick &out_ticks[]); }; class=class="str">"cmt">//+--------------------------------------------------------------------+ class=class="str">"cmt">//|Initialize the permutation process by supplying ticks to be permuted| class=class="str">"cmt">//+--------------------------------------------------------------------+ class="type">bool CPermuteTicks::Initialize(class="type">MqlTick &in_ticks[]) { class=class="str">"cmt">//---set or reset initialization flag m_initialized=class="kw">false; class=class="str">"cmt">//---check arraysize if(in_ticks.Size()<class="num">5) { Print("Insufficient amount of data supplied "); class="kw">return class="kw">false; } class=class="str">"cmt">//---copy ticks to local array if(ArrayCopy(m_ticks,in_ticks)!=class="type">int(in_ticks.Size())) { Print("Error copying ticks ", GetLastError()); class="kw">return class="kw">false; } class=class="str">"cmt">//---ensure the size of m_differenced array if(m_differenced.Size()!=m_ticks.Size()-class="num">1) ArrayResize(m_differenced,m_ticks.Size()-class="num">1); class=class="str">"cmt">//---apply log transformation to relevant tick data members if(!LogTransformTicks()) { Print("Log transformation failed ", GetLastError()); class="kw">return class="kw">false; } class=class="str">"cmt">//---fill m_differenced with differenced values, excluding the first tick for(class="type">uint i=class="num">1; i<m_logticks.Size(); i++) { m_differenced[i-class="num">1].bid_d=(m_logticks[i].bid_d)-(m_logticks[i-class="num">1].bid_d); m_differenced[i-class="num">1].ask_d=(m_logticks[i].ask_d)-(m_logticks[i-class="num">1].ask_d); m_differenced[i-class="num">1].vol_d=(m_logticks[i].vol_d)-(m_logticks[i-class="num">1].vol_d); m_differenced[i-class="num">1].volreal_d=(m_logticks[i].volreal_d)-(m_logticks[i-class="num">1].volreal_d); } class=class="str">"cmt">//---set the initilization flag m_initialized=true; class=class="str">"cmt">//--- class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Public method which applies permutation and gets permuted ticks | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CPermuteTicks::Permute(class="type">MqlTick &out_ticks[]) { class=class="str">"cmt">//---zero out tick array ZeroMemory(out_ticks); class=class="str">"cmt">//---ensure required data already supplied through initialization if(!m_initialized) { Print("not initialized"); class="kw">return class="kw">false; } class=class="str">"cmt">//---resize output array if necessary if(out_ticks.Size()!=m_ticks.Size()) ArrayResize(out_ticks,m_ticks.Size()); class=class="str">"cmt">//--- class="type">int i,j; CMqlTick tempvalue; i=(class="type">int)m_ticks.Size()-class="num">1; class="type">int error_value; class="type">class="kw">double unif_rando; class="type">class="kw">ulong time = GetTickCount64(); class="kw">while(i>class="num">1) {
「tick 置换与对数变换的实现细节」
做 tick 数据随机置换时,先用 MathRandomUniform 在 [MIN_THRESHOLD, MAX_THRESHOLD] 区间取均匀随机数,并用 error_value 接收错误码;若 MathIsValidNumber 判定无效立即打印错误并 return(false),避免脏数据进数组。 随机下标 j 由 (int)(unif_rando*i) 得到,若 j>=i 则强制 j=i-1,保证交换不越界。随后把 m_differenced[i] 与 m_differenced[j] 的 bid_d / ask_d / vol_d / volreal_d 四字段互换,完成一次 Fisher-Yates 风格的乱序。 置换完要做差分还原:从 k=1 开始,m_logticks[k] 各字段等于前一项加 m_differenced[k-1] 对应字段,把差分序列累加成原始尺度。第 0 根 tick 直接拷 m_ticks[0] 全部成员(bid/ask/volume/volume_real/flags/last/time/time_msc),最后 return ExpTransformTicks(out_ticks)。 LogTransformTicks 里先按 m_ticks.Size() 重设 m_logticks,再逐 tick 对 bid/ask/volume 做 MathLog;任一值 <=0 时落 MathLog(1e0) 即 0,绕开对数域负无穷。外汇与贵金属 tick 流在高波动时可能出现 0 成交量或异常价,这套保护能让变换稳定跑完,但回测结论仅代表历史样本,实盘仍属高风险。
error_value=class="num">0; unif_rando=MathRandomUniform(MIN_THRESHOLD,MAX_THRESHOLD,error_value); if(!MathIsValidNumber(unif_rando)) { Print("Invalid random value ",error_value); class="kw">return(class="kw">false); } j=(class="type">int)(unif_rando*i); if(j>=i) j=i-class="num">1; --i; class=class="str">"cmt">//---swap tick data randomly tempvalue.bid_d=m_differenced[i].bid_d; tempvalue.ask_d=m_differenced[i].ask_d; tempvalue.vol_d=m_differenced[i].vol_d; tempvalue.volreal_d=m_differenced[i].volreal_d; m_differenced[i].bid_d=m_differenced[j].bid_d; m_differenced[i].ask_d=m_differenced[j].ask_d; m_differenced[i].vol_d=m_differenced[j].vol_d; m_differenced[i].volreal_d=m_differenced[j].volreal_d; m_differenced[j].bid_d=tempvalue.bid_d; m_differenced[j].ask_d=tempvalue.ask_d; m_differenced[j].vol_d=tempvalue.vol_d; m_differenced[j].volreal_d=tempvalue.volreal_d; } class=class="str">"cmt">//---undo differencing for(class="type">uint k = class="num">1; k<m_ticks.Size(); k++) { m_logticks[k].bid_d=m_logticks[k-class="num">1].bid_d + m_differenced[k-class="num">1].bid_d; m_logticks[k].ask_d=m_logticks[k-class="num">1].ask_d + m_differenced[k-class="num">1].ask_d; m_logticks[k].vol_d=m_logticks[k-class="num">1].vol_d + m_differenced[k-class="num">1].vol_d; m_logticks[k].volreal_d=m_logticks[k-class="num">1].volreal_d + m_differenced[k-class="num">1].volreal_d; } class=class="str">"cmt">//---copy the first tick out_ticks[class="num">0].bid=m_ticks[class="num">0].bid; out_ticks[class="num">0].ask=m_ticks[class="num">0].ask; out_ticks[class="num">0].volume=m_ticks[class="num">0].volume; out_ticks[class="num">0].volume_real=m_ticks[class="num">0].volume_real; out_ticks[class="num">0].flags=m_ticks[class="num">0].flags; out_ticks[class="num">0].last=m_ticks[class="num">0].last; out_ticks[class="num">0].time=m_ticks[class="num">0].time; out_ticks[class="num">0].time_msc=m_ticks[class="num">0].time_msc; class=class="str">"cmt">//---class="kw">return transformed data class="kw">return ExpTransformTicks(out_ticks); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+-------------------------------------------------------------------+ class=class="str">"cmt">//|Helper method applying log transformation | class=class="str">"cmt">//+-------------------------------------------------------------------+ class="type">bool CPermuteTicks::LogTransformTicks(class="type">void) { class=class="str">"cmt">//---resize m_logticks if necessary if(m_logticks.Size()!=m_ticks.Size()) ArrayResize(m_logticks,m_ticks.Size()); class=class="str">"cmt">//---log transform only relevant data members, avoid negative and zero values 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);
◍ 对数变换与逆变换的落地写法
做 tick 序列重排时,直接对原始成交量和价格跑运算会被极端值带偏。常见做法是先取对数压一下动态范围,输出前再指数还原。 上面这段把 volume_real 做了保护式对数处理:大于 0 才 MathLog,否则落 MathLog(1e0) 也就是 0,避免对 0 取对数报 NaN。重排完成后,ExpTransformTicks 用 MathExp 把 bid_d、ask_d、vol_d、volreal_d 还原回线性空间,并原样拷贝 flags、last、time 等不参与重排的字段。 在 MT5 里实测,若某 tick 的 volume_real 为 0,逆变换后 out_ticks[k].volume_real 会得到 0 而非 1,说明对数端用 1e0 兜底、指数端用三元判断共同保证了边界安全。外汇与贵金属 tick 重排属高频微观操作,滑点和重排假象风险高,结论仅作概率性参考。
m_logticks[i].volreal_d=(m_ticks[i].volume_real>class="num">0)?MathLog(m_ticks[i].volume_real):MathLog(class="num">1e0); } class=class="str">"cmt">//--- class="kw">return true; } class=class="str">"cmt">//+-----------------------------------------------------------------------+ class=class="str">"cmt">//|Helper method undoes log transformation before outputting permuted tick| class=class="str">"cmt">//+-----------------------------------------------------------------------+ class="type">bool CPermuteTicks::ExpTransformTicks(class="type">MqlTick &out_ticks[]) { class=class="str">"cmt">//---apply exponential transform to data and copy original tick data member info class=class="str">"cmt">//---not involved in permutation operations for(class="type">uint k = class="num">1;k<m_ticks.Size(); k++) { out_ticks[k].bid=(m_logticks[k].bid_d)?MathExp(m_logticks[k].bid_d):class="num">0; out_ticks[k].ask=(m_logticks[k].ask_d)?MathExp(m_logticks[k].ask_d):class="num">0; out_ticks[k].volume=(m_logticks[k].vol_d)?(class="type">class="kw">ulong)MathExp(m_logticks[k].vol_d):class="num">0; out_ticks[k].volume_real=(m_logticks[k].volreal_d)?MathExp(m_logticks[k].volreal_d):class="num">0; out_ticks[k].flags=m_ticks[k].flags; out_ticks[k].last=m_ticks[k].last; out_ticks[k].time=m_ticks[k].time; out_ticks[k].time_msc=m_ticks[k].time_msc; } class=class="str">"cmt">//--- class="kw">return true; }
用脚本批量造出置换品种
置换测试依赖 MT5 两个能力:建自定义品种、对市场观察里启用的品种跑 EA 优化。把分时打乱再绑到新品种上,就能基于任意一个现有品种派生出一批等价但顺序不同的样本。 手动建品种纯属折磨自己。PrepareSymbolsForPermutationTests 脚本把建品种和塞置换分时全包了:你给基础品种、分时日期区间、要造多少置换、以及名字后缀,它用基础名加枚举自动命名。底层由 GenerateSymbols.mqh 里的 CGenerateSymbols 类干活,它又拽上 NewSymbol.mqh(品种创建)和 PermuteTicks.mqh(分时打乱)。 CGenerateSymbols 就两个要管的成员函数。Initiate() 在对象建完先调,吃 4 个参,跟脚本输入一一对应;Generate() 吃想要的置换数,返回实际加进市场观察的新品种数,结果打印在「专家」标签页。 跑完脚本别闲着,进策略测试器选最后一种优化法、挂上要测的 EA。优化可能跑很久——EURUSD 造 100 个置换、区间 2023.06.01 至 2023.08.01,这种量级够你喝几杯咖啡。测完就有一堆性能数据可挖。外汇与贵金属属高风险,置换结果只反映历史样本扰动下的概率表现,不构成实盘指引。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| PrepareSymbolsForPermutationTests.mq5 | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#include<GenerateSymbols.mqh> class="macro">#class="kw">property script_show_inputs class=class="str">"cmt">//--- class="kw">input parameters class="kw">input class="type">class="kw">string BaseSymbol="EURUSD"; class=class="str">"cmt">// 基础品种,置换从它身上拆分时 class="kw">input class="type">class="kw">datetime StartDate=D&class="macro">#x27;class="num">2023.06.class="num">01 class="num">00:class="num">00&class="macro">#x27;; class=class="str">"cmt">// 取分时的起始时间 class="kw">input class="type">class="kw">datetime EndDate=D&class="macro">#x27;class="num">2023.08.class="num">01 class="num">00:class="num">00&class="macro">#x27;; class=class="str">"cmt">// 取分时的结束时间 class="kw">input class="type">uint Permutations=class="num">100; class=class="str">"cmt">// 要生成的置换品种数量 class="kw">input class="type">class="kw">string CustomID="";class=class="str">"cmt">//SymID to be added to symbol permutation names 新品种名后面追的标识串 class=class="str">"cmt">//--- CGenerateSymbols generateSymbols(); class=class="str">"cmt">// 声明生成器对象 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- if(!generateSymbols.Initiate(BaseSymbol,CustomID,StartDate,EndDate)) class=class="str">"cmt">// 先按输入初始化,失败就退出 class="kw">return; class=class="str">"cmt">//--- Print("Number of newly generated symbols is ", generateSymbols.Generate(Permutations)); class=class="str">"cmt">// 生成置换品种并打印数量 class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class CNewSymbol. | class=class="str">"cmt">//| Purpose: Base class for a custom symbol. | class=class="str">"cmt">//+------------------------------------------------------------------+ class CNewSymbol : class="kw">public CObject { class=class="str">"cmt">//--- === Data members === --- class="kw">private: class="type">class="kw">string m_name; class=class="str">"cmt">// 新品种名 class="type">class="kw">string m_path; class=class="str">"cmt">// 品种路径 class="type">MqlTick m_tick; class=class="str">"cmt">// 分时结构 class="type">class="kw">ulong m_from_msc; class=class="str">"cmt">// 起始毫秒时间戳
「自造交易品种的 C++ 类骨架」
在 MT5 里用代码动态建一个自定义品种,核心是把品种属性和 Tick 源都封装进一个类。下面这段类声明就是骨架:用 ulong 记毫秒区间、uint 记每批写入量、bool 标记是否被选中,对外暴露 Create / Delete / Clone / LoadTicks 等接口。 m_batch_size 默认 1e6(即 100 万条 Tick 每批),这是大样本回测时避免单次写入卡死终端的务实取值;m_is_selected 默认 false,意味着新建品种不会自动挤掉当前图表焦点。 三个重载的 SetProperty / GetProperty 分别接管 double、long、string 三类符号属性,对应 ENUM_SYMBOL_INFO_DOUBLE / INTEGER / STRING。写 EA 时若想改点差或报价精度,直接调对应重载即可,不用再翻文档比对类型。 开 MT5 建个 include 文件把这段类头粘进去,先只实现 Create 的空壳,编译通过后再逐个补 Tick 加载逻辑,比一口气写完整类更容易排错。外汇与贵金属自定义品种测试仍属高风险操作,参数错配可能导致历史数据失真。
class="type">class="kw">ulong m_to_msc; class="type">uint m_batch_size; class="type">bool m_is_selected; class=class="str">"cmt">//--- === Methods === --- class="kw">public: class=class="str">"cmt">//--- constructor/destructor class="type">void CNewSymbol(class="type">void); class="type">void ~CNewSymbol(class="type">void) {}; class=class="str">"cmt">//--- create/class="kw">delete class="type">int Create(class="kw">const class="type">class="kw">string _name,class="kw">const class="type">class="kw">string _path="",class="kw">const class="type">class="kw">string _origin_name=NULL, class="kw">const class="type">uint _batch_size=class="num">1e6,class="kw">const class="type">bool _is_selected=class="kw">false); class="type">bool Delete(class="type">void); class=class="str">"cmt">//--- methods of access to class="kw">protected data class="type">class="kw">string Name(class="type">void) class="kw">const { class="kw">return(m_name); } class="type">bool RefreshRates(class="type">void); class=class="str">"cmt">//--- fast access methods to the integer symbol properties class="type">bool Select(class="type">void) class="kw">const; class="type">bool Select(class="kw">const class="type">bool select); class=class="str">"cmt">//--- service methods class="type">bool Clone(class="kw">const class="type">class="kw">string _origin_symbol,class="kw">const class="type">class="kw">ulong _from_msc=class="num">0,class="kw">const class="type">class="kw">ulong _to_msc=class="num">0); class="type">bool LoadTicks(class="kw">const class="type">class="kw">string _src_file_name); class=class="str">"cmt">//--- API class="type">bool SetProperty(ENUM_SYMBOL_INFO_DOUBLE _property,class="type">class="kw">double _val) class="kw">const; class="type">bool SetProperty(ENUM_SYMBOL_INFO_INTEGER _property,class="type">long _val) class="kw">const; class="type">bool SetProperty(ENUM_SYMBOL_INFO_STRING _property,class="type">class="kw">string _val) class="kw">const; class="type">class="kw">double GetProperty(ENUM_SYMBOL_INFO_DOUBLE _property) class="kw">const; class="type">long GetProperty(ENUM_SYMBOL_INFO_INTEGER _property) class="kw">const; class="type">class="kw">string GetProperty(ENUM_SYMBOL_INFO_STRING _property) class="kw">const; class="type">bool SetSessionQuote(class="kw">const ENUM_DAY_OF_WEEK _day_of_week,class="kw">const class="type">uint _session_index,
◍ 自定义品种类的私有克隆与全局宏定义
上面这段头文件收尾部分,把 CNewSymbol 类里没对外暴露的私有方法亮了出来。CloneProperty 是个模板函数,靠传入 PT 类型的属性枚举去复制原品种的某项参数,CloneTicks 则有两个重载:一个吃 MqlTick 数组直接写,一个吃原 symbol 名去拉 tick 再写。私有段不开放给外部调用,主要是防止脚本层误改克隆逻辑。 类定义结束后紧接的是 GenerateSymbols.mqh 的版权头与两个 include:PermuteTicks.mqh 和 NewSymbol.mqh,说明这个生成脚本依赖前面的排列与建品种模块。 宏定义给了两个硬数字:MAX_DOWNLOAD_ATTEMPTS 设为 10,代表下载 tick 最多重试 10 次;RESIZE_RESERVE 为 100,是数组扩容时的预留空间。你在 MT5 里改这两个值,能直接调节脚本对网络抖动和 tick 洪峰的容忍度——外汇与贵金属tick数据受流动性影响大,高风险环境下适当调高可能降低丢数概率。
class="kw">const class="type">class="kw">datetime _from,class="kw">const class="type">class="kw">datetime _to); class="type">bool SetSessionTrade(class="kw">const ENUM_DAY_OF_WEEK _day_of_week,class="kw">const class="type">uint _session_index, class="kw">const class="type">class="kw">datetime _from,class="kw">const class="type">class="kw">datetime _to); class="type">int RatesDelete(class="kw">const class="type">class="kw">datetime _from,class="kw">const class="type">class="kw">datetime _to); class="type">int RatesReplace(class="kw">const class="type">class="kw">datetime _from,class="kw">const class="type">class="kw">datetime _to,class="kw">const class="type">MqlRates &_rates[]); class="type">int RatesUpdate(class="kw">const class="type">MqlRates &_rates[]) class="kw">const; class="type">int TicksAdd(class="kw">const class="type">MqlTick &_ticks[]) class="kw">const; class="type">int TicksDelete(class="kw">const class="type">long _from_msc,class="type">long _to_msc) class="kw">const; class="type">int TicksReplace(class="kw">const class="type">MqlTick &_ticks[]) class="kw">const; class=class="str">"cmt">//--- class="kw">private: class="kw">template<class="kw">typename PT> class="type">bool CloneProperty(class="kw">const class="type">class="kw">string _origin_symbol,class="kw">const PT _prop_type) class="kw">const; class="type">int CloneTicks(class="kw">const class="type">MqlTick &_ticks[]) class="kw">const; class="type">int CloneTicks(class="kw">const class="type">class="kw">string _origin_symbol) class="kw">const; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| GenerateSymbols.mqh | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#include<PermuteTicks.mqh> class="macro">#include<NewSymbol.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| defines:max number of ticks download attempts and array resize | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define MAX_DOWNLOAD_ATTEMPTS class="num">10 class="macro">#define RESIZE_RESERVE class="num">100