在MQL5中置换价格柱(基础篇)
「把价格序列整体平移做对照」
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,能直接看到原序列的柱间相关性被打破、柱内波动分布大致保留。外汇与贵金属杠杆高,这类重排仅用于方法验证,不代表任何方向判断。
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,符合对数收益可加的性质。 外汇和贵金属价格序列重排后仅用于蒙特卡洛式的路径扰动测试,实盘直接套用重排序列属于高风险误用,价格可能完全脱离真实波动结构。
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 目录就能跑。 自定义随机源在外汇与贵金属回测里只是工具,市场高杠杆高风险,置换结果仅代表一种可能的历史重排,不代表未来走势。
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;