在MQL5中置换价格柱(基础篇)
📘

在MQL5中置换价格柱(基础篇)

第 1/3 篇

「把价格序列整体平移做对照」

MQL5 里想看「当前 K 线」和「N 根之前」的形态差异,最直接的方法是新建一个复制缓冲,把原价格序列整体后移。这样主图数组索引不变,对照逻辑只在缓冲层发生,不污染原始数据。 实际写指标时,常见做法是声明一个 double 类型数组,用 ArrayInitialize 清零后在 OnCalculate 里用 shift 思路填充。以 2024 年 4 月社区公开示例为参照,该置换演示帖在发布当周获得 849 次查看、2 条留言,说明「价格柱平移」是实盘前做形态回看的高频需求。 外汇与贵金属品种波动受杠杆和突发事件影响,平移对照仅用于历史形态观察,任何「过去偏移后重合」都只是概率现象,不能推导出后续方向。

测试器里的漂亮曲线未必真能赚钱

MT5 策略测试器是评估 EA 潜力的常用工具,功能够用,但老手能利用它造出“特技”EA,把净值曲线伪装得极其漂亮。很多卖家截图里的曲线第一眼惊艳,实盘一跑却完全是另一条线。 要避免被这种廉价把戏坑,关键不是看回测截图,而是拆掉它的烟幕。本文要演示的置换测试(permutation test)就是这类手段:用打乱数据的方式重跑策略,看原曲线是不是只靠运气或数据巧合堆出来的。 前一篇我们已经实现过置换分时数据(tick)的算法,这一节换一个层面——直接对价格柱(bar)做置换,思路不同但目的一致:逼出策略在随机数据下的真实表现倾向。

◍ 置换 OHLC 时不能动的几条边界

做价格柱置换,核心不是随机打乱,而是保住原始序列的「形」。趋势方向、开盘到收盘之间的波动离散度,以及相邻柱收盘与下一柱开盘的价差分布,这三样必须和原数据一致,否则回测策略会被无声扭曲。 硬边界只有一条:任意一根柱的 Open 或 Close,不允许突破该柱自身 High/Low 的上下限。也就是说,置换后仍是合法 OHLC,不能出现开比高还高、收比低还低的荒谬柱。 唯一允许自由变动的,是第一根到最后一根之间每个 OHLC 的绝对值——特征分布锁死,绝对价位洗牌。外汇与贵金属属高风险品种,置换仅用于方法验证,不构成任何方向暗示。 具体实现可复用 CPermuteTicks 的思路,封装进 PermuteRates.mqh 的 CPermuteRates 类,开 MT5 把这类类挂到自己的蒙特卡罗脚本里就能跑。

「把K线拆成相对差值再洗牌」

做价格置换前,先把原始 MqlRates 转成一组相对差值。CRelRates 结构只存四个对数差:rel_open 是当前柱开盘价与上一柱收盘价的对数差,rel_high / rel_low / rel_close 分别是柱内高、低、收相对于开盘价的对数差。MqlRates 里的时间、成交量等字段不动,只动 OHLC。 置换逻辑分两层:先打乱柱间变化(rel_open 序列),再打乱柱内变化(rel_high、rel_low、rel_close)。新开盘价由被打乱的 rel_open 重建,再叠回对应的柱内差值,得到一套统计特征接近、但顺序被重排的行情。 CPermuteRates 类用 m_rates[] 存原序列,m_differenced[] 存对数差,m_random 指向一个均匀随机数发生器。Permute() 里先用 ArraySize(m_rates)-2 作为游标,从尾向前做 Fisher–Yates 式交换,只对 rel_open 洗牌;若未 Initialize 直接调 Permute,会打印 Initialization error 并清空输出数组返回 false。 开 MT5 把下面结构抄进 PermuteRate.mqh,先跑 Initialize 再调 Permute,能直接看到原序列的柱间相关性被打破、柱内波动分布大致保留。外汇与贵金属杠杆高,这类重排仅用于方法验证,不代表任何方向判断。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| class="kw">struct to handle relative values of rate data to be worked on    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct CRelRates
  {
   class="type">class="kw">double              rel_open;
   class="type">class="kw">double              rel_high;
   class="type">class="kw">double              rel_low;
   class="type">class="kw">double              rel_close;
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class to enable permuation of a collection of ticks in an array  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CPermuteRates
  {
class="kw">private :
   class="type">MqlRates            m_rates[];        class=class="str">"cmt">//original rates to be shuffled
   CRelRates           m_differenced[];  class=class="str">"cmt">//log difference of rates
   class="type">bool                m_initialized;   class=class="str">"cmt">//flag to signal state of random number object
   CUniFrand           *m_random;       class=class="str">"cmt">//random number generator
class="kw">public :
   class=class="str">"cmt">//constructor
                     CPermuteRates(class="type">void);
   class=class="str">"cmt">//desctructor
                    ~CPermuteRates(class="type">void);
   class="type">bool              Initialize(class="type">MqlRates &in_rates[]);
   class="type">bool              Permute(class="type">MqlRates &out_rates[]);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Permute the bars                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CPermuteRates::Permute(class="type">MqlRates &out_rates[])
  {
class=class="str">"cmt">//---
   if(!m_initialized)
     {
      Print("Initialization error");
      ZeroMemory(out_rates);
      class="kw">return false;
     }
class=class="str">"cmt">//---
   class="type">int i,j;
   class="type">class="kw">double temp=class="num">0.0;
class=class="str">"cmt">//---
   i=ArraySize(m_rates)-class="num">2;
class=class="str">"cmt">//---
   while(i > class="num">1 && !IsStopped())
     {
      j = (class="type">int)(m_random.RandomDouble() * i) ;
      if(j >= i)
         j = i - class="num">1 ;
      --i ;
      temp = m_differenced[i+class="num">1].rel_open ;
      m_differenced[i+class="num">1].rel_open = m_differenced[j+class="num">1].rel_open ;
      m_differenced[j+class="num">1].rel_open = temp ;
     }
class=class="str">"cmt">//---
   i =ArraySize(m_rates)-class="num">2;
class=class="str">"cmt">//---

用 Fisher-Yates 重排差分再还原价格序列

这段逻辑干了两件事:先对差分结构里的相对高低收做随机重排,再把重排后的相对变化通过指数还原回真实报价。重排用的是标准 Fisher-Yates 洗牌,循环里每次从 0~i 区间取随机数 j,越界就钳到 i-1,然后交换 rel_high / rel_low / rel_close 三组值。 随机源来自 m_random.RandomDouble(),返回 [0,1) 浮点,乘 i 再强转 int 得到下标。注意循环条件是 i>1 且 !IsStopped(),意味着 EA 被用户停止时会立刻退出,不会把半洗牌的状态写出去。 洗牌结束后用 ArrayCopy 把原 m_rates 拷进 out_rates,若拷贝长度不等于 m_rates.Size() 就清空输出、打印错误号并返回 false——这一步是防数组尺寸漂移导致后面越界。 还原阶段从 i=1 开始:open 用上一根 close 的自然对数加 rel_open 再 exp 回去;high / low / close 则基于本根 open 的对数加对应 rel 值 exp。也就是说,重排只动了「变化量」的顺序,级别锚定在前一根 close,符合对数收益可加的性质。 外汇和贵金属价格序列重排后仅用于蒙特卡洛式的路径扰动测试,实盘直接套用重排序列属于高风险误用,价格可能完全脱离真实波动结构。

MQL5 / C++
  while(i > class="num">1 && !IsStopped())
    {
      j = (class="type">int)(m_random.RandomDouble() * i) ;
      if(j >= i)
        j = i - class="num">1 ;
      --i ;
      temp = m_differenced[i].rel_high;
      m_differenced[i].rel_high = m_differenced[j].rel_high ;
      m_differenced[j].rel_high = temp ;
      temp = m_differenced[i].rel_low ;
      m_differenced[i].rel_low = m_differenced[j].rel_low ;
      m_differenced[j].rel_low = temp ;
      temp = m_differenced[i].rel_close ;
      m_differenced[i].rel_close = m_differenced[j].rel_close ;
      m_differenced[j].rel_close = temp ;
    }
class=class="str">"cmt">//---
  if(ArrayCopy(out_rates,m_rates)!=class="type">int(m_rates.Size()))
    {
      ZeroMemory(out_rates);
      Print("Copy error ", GetLastError());
      class="kw">return false;
    }
class=class="str">"cmt">//---
  for(i=class="num">1 ; i<ArraySize(out_rates) && !IsStopped() ; i++)
    {
      out_rates[i].open  = MathExp(((MathLog(out_rates[i-class="num">1].close)) + m_differenced[i-class="num">1].rel_open)) ;
      out_rates[i].high  = MathExp(((MathLog(out_rates[i].open)) + m_differenced[i-class="num">1].rel_high)) ;
      out_rates[i].low   = MathExp(((MathLog(out_rates[i].open)) + m_differenced[i-class="num">1].rel_low)) ;
      out_rates[i].close = MathExp(((MathLog(out_rates[i].open)) + m_differenced[i-class="num">1].rel_close)) ;
    }
class=class="str">"cmt">//---
  if(IsStopped())
    class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
class=class="str">"cmt">//---
  }

◍ 置换分时类改用自写随机数后只打乱买价

旧的置换分时类和新的 CPermuteTicks 之间差别不小,其中之一是用上了自定义随机数生成器。实测它在 MT5 里比直接调内置随机函数略快一点,所以新类也沿用了这套逻辑。 为了效率,新版本砍掉了不必要的中间运算,只打乱 bid(买价)这一列。其余分时属性直接从原始 tick 序列里复制过来,这顺手修掉了一个老毛病——之前置换出来的分时点差有时会离谱到不真实。 CPermuteTicks 对外行为和旧版一致,CPermuteRates 也差不多,区别仅在一个吃分时、一个吃报价。下面这段 UniformRandom 头里的核心类,就是新类依赖的均匀随机数发生器,默认种子 123456789、carry 初值 362436,开 MT5 把代码丢进 include 目录就能跑。 自定义随机源在外汇与贵金属回测里只是工具,市场高杠杆高风险,置换结果仅代表一种可能的历史重排,不代表未来走势。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 UniformRandom.mqh |
class=class="str">"cmt">//|                     Copyright class="num">2023, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                                         https://www.MQL5.com |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Software Corp."
class="macro">#class="kw">property link      "https:class=class="str">"cmt">//www.MQL5.com"
class=class="str">"cmt">//+----------------------------------------------------------------------+
class=class="str">"cmt">//|   CUniFrand class: Uniformly distributed random class="num">0 - class="num">1 number generator|
class=class="str">"cmt">//+----------------------------------------------------------------------+
class CUniFrand
  {
class="kw">private :
   class="type">uint                m_m[class="num">256];
   class="type">int                 m_mwc_initialized;
   class="type">int                 m_mwc_seed;
   class="type">uint                m_carry;
   class="type">uint                random(class="type">void);
class="kw">public :
   class=class="str">"cmt">//constructor
                     CUniFrand(class="type">void);
   class=class="str">"cmt">//desctructor
                    ~CUniFrand(class="type">void);
   class=class="str">"cmt">//optionally set a seed for number generator
   class="type">void               SetSeed(const class="type">int iseed);
   class=class="str">"cmt">//get random number between class="num">0 and class="num">1
   class="type">class="kw">double             RandomDouble(class="type">void);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|   Default constructor                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
CUniFrand::CUniFrand(class="type">void)
  {
  m_mwc_initialized=class="num">0;
  m_mwc_seed=class="num">123456789;
  m_carry=class="num">362436;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|    Destructor                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CUniFrand::~CUniFrand(class="type">void)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| creates and returns random integer number                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">uint CUniFrand::random(class="type">void)
  {
  class="type">uint t,a=class="num">809430660;

常见问题

平移只是把历史重演一遍,没产生新信息;测试器里的漂亮曲线多来自未来函数或过度拟合,实盘外汇贵金属高风险,大概率失效。
时间轴与成交量不可动,开盘价若按收盘还原需保持当日跳空逻辑;只动高低收的相对差值,否则会造出不存在的K线。
小布可读取你的品种页并标注序列平移或差分还原中的边界越界点,直接提示哪段曲线疑似用了未来数据。
差分取相邻收盘对数差,重排后逐根累加还原;只要原序列为正且首根锚定真实开盘,就不会出现负价。
弃用平台内置随机,自写线性同余发生器按柱索引播种,仅重排买价数组,卖价与成交量保持原序即可。