在MQL5中置换价格柱·进阶篇
◍ 用 MWC 算法给 Tick 洗牌打底
做价格行为回测时,若直接拿原始 Tick 序列跑蒙特卡洛,样本顺序会带入隐性偏差。下面这段 uniform random 生成器用的是 Multiply-With-Carry(MWC)算法,初始化阶段先把 256 长度的种子表用 69069*j+12345 递推填满,保证每次会话的伪随机流可复现。 SetSeed 把外部传入的 iseed 存进 m_mwc_seed 并清零 m_mwc_initialized,下次调用 random() 会重新铺种子表;RandomDouble 则用 1.0/UINT_MAX 把 32 位整数映射到 [0,1) 区间,精度取决于 UINT_MAX 约 42.9 亿的上限。 CPermuteTicks 类头文件里直接 #include<UniformRandom.mqh>,私有成员挂了 MqlTick 数组 m_ticks 和差分数组 m_differenced,并持有一个 CUniFrand 指针 m_random。它的 Initialize 接收 MqlTick 引用数组,后续洗牌就靠前面那个随机流驱动——外汇与贵金属 Tick 重采样属高风险实验,结论仅具概率意义。 在 MT5 里把这两段拼进 Include,用 SetSeed(20231001) 固定种子后连跑两次 RandomDouble,若两次输出完全一致,说明你的 MWC 实例已正确接管随机源,可放心接进置换逻辑。
class="kw">static class="type">uchar i; if(!m_mwc_initialized) { class="type">uint k,j=m_mwc_seed; m_mwc_initialized=class="num">1; for(k=class="num">0; k<class="num">256; k++) { j = class="num">69069 * j + class="num">12345; m_m[k]=j; } } t=a*m_m[++i] + m_carry; m_carry = (class="type">uint)(t>>class="num">32); m_m[i] = (class="type">uint)(t&UINT_MAX); class="kw">return m_m[i]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Optionally set the seed for random number generator | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CUniFrand::SetSeed(const class="type">int iseed) { m_mwc_seed=iseed; m_mwc_initialized=class="num">0; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| returns a random number between class="num">0 and class="num">1 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CUniFrand::RandomDouble(class="type">void) { class="type">class="kw">double mult =class="num">1.0/UINT_MAX; class="kw">return mult * random(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//class="macro">#include<UniformRandom.mqh> class CPermuteTicks { class="kw">private : class="type">MqlTick m_ticks[]; class=class="str">"cmt">//original tick data to be shuffled class="type">class="kw">double m_differenced[]; class=class="str">"cmt">//log difference of tick data class="type">bool m_initialized; class=class="str">"cmt">//flag representing proper preparation of a dataset CUniFrand *m_random; class="kw">public : CPermuteTicks(class="type">void); ~CPermuteTicks(class="type">void); class="type">bool Initialize(class="type">MqlTick &in_ticks[]);
「置换tick序列的初始化与差分处理」
做蒙特卡洛式的tick重排,第一步是把真实行情 tick 灌进类里并做对数差分。CPermuteTicks::Initialize 干的就是这件事:先卡随机数与数据量,再算相邻 bid 的对数收益。 代码里有个硬门槛——传入的 in_ticks 数组长度必须 ≥5,否则直接返回 false 并打印 Insufficient amount of data supplied。这意味着在 MT5 里你至少得抓 5 个以上真实 tick 才能跑这套置换,实盘或回测前先确认订阅深度。 差分环节跳过了第 0 根 tick,从 i=1 开始循环:m_differenced[i-1] = MathLog(m_ticks[i].bid / m_ticks[i-1].bid)。这等于把价格序列转成对数收益率,后续置换只在收益率层面打乱,再还原就能保留边际分布、破坏时间依赖。外汇与贵金属 tick 流具有高杠杆高风险,重排结果仅用于检验序列相关性,不构成方向判断。 初始化成功会把 m_initialized 置 true,Permute 若发现该标志为 false 会打印 not initialized 并退出。开 MT5 把这段逻辑挂到 EA 里,先喂一组 EURUSD 的 10 个 tick 试试,能直观看到差分数组比原数组少 1 项。
class="type">bool CPermuteTicks::Initialize(class="type">MqlTick &in_ticks[]) { if(m_random==NULL) { Print("Critical internal error, failed to initialize random number generator"); class="kw">return false; } m_initialized=false; if(in_ticks.Size()<class="num">5) { Print("Insufficient amount of data supplied "); class="kw">return false; } if(ArrayCopy(m_ticks,in_ticks)!=class="type">int(in_ticks.Size())) { Print("Error copying ticks ", GetLastError()); class="kw">return false; } if(m_differenced.Size()!=m_ticks.Size()-class="num">1) ArrayResize(m_differenced,m_ticks.Size()-class="num">1); for(class="type">uint i=class="num">1; i<m_ticks.Size() && !IsStopped(); i++) { m_differenced[i-class="num">1]=MathLog(m_ticks[i].bid/m_ticks[i-class="num">1].bid); } m_initialized=true; class="kw">return true; }
用随机置换与指数还原重建 tick 序列
这段代码做的是把差分后的 tick 数据做随机洗牌,再按指数变换还原成可输出的报价序列。外汇与贵金属 tick 重采样属于高风险实验,结果只反映数学重构、不预示行情方向。 循环从末位索引 i = m_ticks.Size()-1 向下走到 1,每次用 m_random.RandomDouble()*i 取一个随机位置 j 并与当前位交换 m_differenced 中的差值,实现 Fisher–Yates 式的随机置换;若脚本被停止(IsStopped)则直接返回 false。 置换完后先用 ArrayCopy 把原始 tick 时间等成员拷进 out_ticks,失败会打印错误码并返回 false。随后从 k=1 开始:out_ticks[k].bid 用前一根 bid 的日志值加上差分项再做 MathExp 还原,ask 则沿用原 spread(m_ticks[k].ask - m_ticks[k].bid)偏移。 整段未参与置换的只有首 tick 与时间成员;若中途未停止,函数返回 true,否则返回 false。在 MT5 里把这段接进自定义指标,改 m_random 种子可观察不同重排下 bid 曲线的离散程度。
class="kw">return false; } class=class="str">"cmt">//--- class="type">int i,j; class="type">class="kw">double tempvalue; i=(class="type">int)m_ticks.Size()-class="num">1; while(i>class="num">1 && !IsStopped()) { j=(class="type">int)(m_random.RandomDouble()*i); if(j>=i) j=i-class="num">1; --i; class=class="str">"cmt">//---swap tick data randomly tempvalue=m_differenced[i]; m_differenced[i]=m_differenced[j]; m_differenced[j]=tempvalue; } class=class="str">"cmt">//---- if(IsStopped()) class="kw">return false; class=class="str">"cmt">//---copy the first tick if(ArrayCopy(out_ticks,m_ticks)!=class="type">int(m_ticks.Size())) { Print(__FUNCTION__," array copy failure ", GetLastError()); class="kw">return false; } 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() && !IsStopped(); k++) { out_ticks[k].bid=MathExp((MathLog(out_ticks[k-class="num">1].bid) + m_differenced[k-class="num">1]));class=class="str">"cmt">//MathExp(m_logticks[k]); out_ticks[k].ask=out_ticks[k].bid + (m_ticks[k].ask - m_ticks[k].bid); } class=class="str">"cmt">//--- if(IsStopped()) class="kw">return false; else class="kw">return true; }
◍ 用现有品种造一批置换数据自定义品种
想在 MT5 里做价格序列的随机性检验,光靠原始分时或报价不够,需要一批「统计性质相似但顺序被打乱」的对照品种。CPermutedSymbolData 这个类就是干这个的:以某个已存在的交易品种为基底,按你指定的模式生成若干自定义品种,其数据来自基底分时或报价的置换。 构造时用一个参数决定打乱什么——ENUM_RATES_TICKS 枚举里只有两个选项:ENUM_USE_RATES=0 表示置换报价(K线),ENUM_USE_TICKS 表示置换分笔。实例化后必须调 Initiate() 传入品种名和起止日期,界定置换所基于的数据窗口;只有它返回 true,才说明基底数据拉到了。 随后调 Generate(置换数),方法返回成功写入置换数据的自定义品种计数。比如传 50,就可能得到 50 个新符号用于蒙特卡洛式的排列检验。外汇与贵金属杠杆高、滑点随机,这类合成数据仅用于方法验证,实盘信号不可直接外推。 下面这段是类的核心声明与枚举、宏定义,逐行拆一下关键处:enum 定义打乱目标;两个 #define 分别限制下载重试 10 次、数组预留 100 元素防频繁重分配;类内私有成员存基底与置换后的 MqlTick / MqlRates 数组,以及两个打乱器指针和自定义品种对象数组。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Permute rates or ticks of symbol | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_RATES_TICKS { ENUM_USE_RATES=class="num">0,class=class="str">"cmt">//Use rates ENUM_USE_TICKSclass=class="str">"cmt">//Use ticks }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| defines:max number of data 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 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|CPermuteSymbolData class | class=class="str">"cmt">//| creates custom symbols from an existing base symbol&class="macro">#x27;s data | class=class="str">"cmt">//| symbols represent permutations of base symbol&class="macro">#x27;s data | class=class="str">"cmt">//+------------------------------------------------------------------+ class CPermuteSymbolData { class="kw">private: ENUM_RATES_TICKS m_use_rates_or_ticks;class=class="str">"cmt">//permute either ticks or rates 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">class="kw">datetime m_datarangestart; class=class="str">"cmt">//beginning date for range of base symbol&class="macro">#x27;s data class="type">class="kw">datetime m_datarangestop; class=class="str">"cmt">//ending date for range of base symbol&class="macro">#x27;s data 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 tick class="type">MqlTick m_permutedticks[]; class=class="str">"cmt">//permuted ticks; class="type">MqlRates m_baserates[]; class=class="str">"cmt">//base symbol&class="macro">#x27;s rates class="type">MqlRates m_permutedrates[]; class=class="str">"cmt">//permuted rates; CPermuteRates *m_rates_shuffler; class=class="str">"cmt">//object used to shuffle rates CPermuteTicks *m_ticks_shuffler; class=class="str">"cmt">//object used to shuffle ticks CNewSymbol *m_csymbols[]; class=class="str">"cmt">//array of created symbols class="kw">public: CPermuteSymbolData(const ENUM_RATES_TICKS mode); ~CPermuteSymbolData(class="type">void);
「初始化置换符号的数据管道」
做合成行情回测前,先得把基类 CPermuteSymbolData 的 Initiate 跑通。它接收基础品种名、合成 ID、起止时间四个参数,第一步就把历史置换计数 m_permutations 归零,避免上一次乱序残留影响本轮。 函数里用 SymbolSelect(base_symbol,true) 强制把基础品种塞进观察列表,若返回 false 直接打印错误码并退出——很多自建合成品种在脚本启动时看不见,就是卡在这一行。 日期区间有硬校验:start_date>=stop_date 会触发 Invalid date range 并 return false,所以传参时 stop 必须严格晚于 start,否则后面全白做。 数据下载分 tick 与 M1 两种模式,由 m_use_rates_or_ticks 决定。循环最多尝试 MAX_DOWNLOAD_ATTEMPTS 次,每次失败 Sleep(500) 后重试;tick 模式调 CopyTicksRange 时把 datetime 乘 1000 转成毫秒传给服务器,这是 MT5 tick 接口的铁规矩。 下载条数 <=0 会打印 Failed to download data 并清掉 Comment 提示;只有拿到数据后,才按模式给 m_ticks_shuffler 或对应乱序器 new 出来,置换器才算就绪。外汇与贵金属合成数据回测属高杠杆环境,下载失败可能源于品种休市或经纪商限制,需自行核对。
class="type">bool Initiate(const class="type">class="kw">string base_symbol,const class="type">class="kw">string symbols_id,const class="type">class="kw">datetime start_date,const class="type">class="kw">datetime stop_date); class="type">uint Generate(const class="type">uint permutations); }; class=class="str">"cmt">//+-----------------------------------------------------------------------------------------+ class=class="str">"cmt">//|set and check parameters for symbol creation, download data and initialize data shuffler | class=class="str">"cmt">//+-----------------------------------------------------------------------------------------+ class="type">bool CPermuteSymbolData::Initiate(const class="type">class="kw">string base_symbol,const class="type">class="kw">string symbols_id,const class="type">class="kw">datetime start_date,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 false; } class=class="str">"cmt">//---set symbols id m_symbols_id=symbols_id; class=class="str">"cmt">//---check, set data date range if(start_date>=stop_date) { Print("Invalid date range "); class="kw">return false; } else { m_datarangestart= start_date; m_datarangestop = stop_date; } class=class="str">"cmt">//---download data Comment("Downloading data"); class="type">uint attempts=class="num">0; class="type">int downloaded=-class="num">1; while(attempts<MAX_DOWNLOAD_ATTEMPTS && !IsStopped()) { downloaded=(m_use_rates_or_ticks==ENUM_USE_TICKS)?CopyTicksRange(m_basesymbol,m_baseticks,COPY_TICKS_ALL,class="type">long(m_datarangestart)*class="num">1000,class="type">long(m_datarangestop)*class="num">1000):CopyRates(m_basesymbol,PERIOD_M1,m_datarangestart,m_datarangestop,m_baserates); 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 download data for ",m_basesymbol," error ", GetLastError()); Comment(""); class="kw">return false; } class=class="str">"cmt">//Print(downloaded," Ticks downloaded ", " data start ",m_basedata[class="num">0].time, " data end ", m_basedata[m_basedata.Size()-class="num">1].time); class=class="str">"cmt">//---class="kw">return shuffler initialization result class="kw">switch(m_use_rates_or_ticks) { case ENUM_USE_TICKS: { if(m_ticks_shuffler==NULL) m_ticks_shuffler=new CPermuteTicks();
排列符号的生成与落地
CPermuteSymbolData::Generate() 负责把基础品种复制成多组重排数据品种。入参 permutations 为本次要新增的排列数,若传 0 会直接 Print 报错并返回 0,不会动任何已有符号。 函数先按 m_permutations+permutations 做 ArrayResize,用 RESIZE_RESERVE 留余量,避免循环中频繁重分配。路径 symspath 由基础名+ID+“_PermutedData”拼成,所有新符号都挂在这条路径下。 主循环从 m_permutations 跑到数组末尾,每次 new CNewSymbol 后调 Create 建名(如 BaseID_5),Clone 基础品种,再按模式调 m_ticks_shuffler.Permute 或 m_rates_shuffler.Permute 拿到乱序数据。若 Clone 或 Permute 失败直接 break,成功则 Select(true) 并调 TicksReplace / RatesUpdate 写回,写成功才让 m_permutations 加 1。 循环里每步都查 IsStopped(),用户点停止会立刻退出并返回 0;正常结束返回 m_permutations 即已生成排列符号总数。在 MT5 里跑这套,若看到某 _PermutedData 品种缺数据,优先查对应 Permute 分支是否返回了 false。
class="type">uint CPermuteSymbolData::Generate(const class="type">uint permutations) { class=class="str">"cmt">//---check permutations if(!permutations) { Print("Invalid parameter value for Permutations "); Comment(""); 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+"_PermutedData"; class=class="str">"cmt">//---do more permutations for(class="type">uint i=m_permutations; i<m_csymbols.Size() && !IsStopped(); i++) { if(CheckPointer(m_csymbols[i])==POINTER_INVALID) m_csymbols[i]=new CNewSymbol(); if(m_csymbols[i].Create(m_basesymbol+m_symbols_id+"_"+class="type">class="kw">string(i+class="num">1),symspath,m_basesymbol)<class="num">0) class="kw">continue; Comment("Processing Symbol "+m_basesymbol+m_symbols_id+"_"+class="type">class="kw">string(i+class="num">1)); if(!m_csymbols[i].Clone(m_basesymbol) || (m_use_rates_or_ticks==ENUM_USE_TICKS && !m_ticks_shuffler.Permute(m_permutedticks)) || (m_use_rates_or_ticks==ENUM_USE_RATES && !m_rates_shuffler.Permute(m_permutedrates))) class="kw">break; else { m_csymbols[i].Select(true); Comment("Adding permuted data"); if(m_use_rates_or_ticks==ENUM_USE_TICKS) m_permutations+=(m_csymbols[i].TicksReplace(m_permutedticks)>class="num">0)?class="num">1:class="num">0; else m_permutations+=(m_csymbols[i].RatesUpdate(m_permutedrates)>class="num">0)?class="num">1:class="num">0; } } class=class="str">"cmt">//---class="kw">return successfull number of permutated symbols Comment(""); class=class="str">"cmt">//--- if(IsStopped()) class="kw">return class="num">0; class=class="str">"cmt">//--- class="kw">return m_permutations; }