在MQL5中置换价格柱·进阶篇
📘

在MQL5中置换价格柱·进阶篇

第 2/3 篇

◍ 用 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 实例已正确接管随机源,可放心接进置换逻辑。

MQL5 / C++
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 项。

MQL5 / C++
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 曲线的离散程度。

MQL5 / C++
  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 数组,以及两个打乱器指针和自定义品种对象数组。

MQL5 / C++
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 出来,置换器才算就绪。外汇与贵金属合成数据回测属高杠杆环境,下载失败可能源于品种休市或经纪商限制,需自行核对。

MQL5 / C++
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。

MQL5 / C++
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;
   }

常见问题

建议用当前品种最后一根真实 tick 的时间戳做种子,再叠一个非零常数偏移;这样不同品种跑出来序列基本不重复。
拉出原序列和置换序列的相邻差绝对值分布,对比中位数和极值;若中位数偏差超过 15% 就要重调随机层参数。
可以。把置换符号推到小布,它会自动建数据管道并画出排列符号的价差分布,你只管看异常点。
它保留原品种 tick 间隔与跳空特征,只打乱顺序;比纯随机数据更贴近真实盘口微观结构。
直接落 CSV 带时间戳和价列即可,后续用脚本读入重建比二进制省去版本兼容麻烦。