先进的自适应指标理论及在 MQL5 中的实施·进阶篇
(2/3)·复数相量和中值滤波如何把噪音周期变成可交易信号
◍ 指标入口与数组初始化的坑
自定义指标跑起来第一道关就是 OnCalculate 的参数表,MT5 把每根 K 线的开高低收、时间、成交量、点差全以数组引用丢进来,rates_total 是已就绪的柱数,prev_calculated 是上一轮已算过的柱数,靠这两个值判断是冷启动还是增量刷新。 这段实现里先用 CopyTickVolume 抓最新 1 根柱的 tick 量,若取不到就直接 return(rates_total) 让系统下次再调,相当于跳过了这轮计算。随后把 high、low 设成倒序序列,并用 ArrayResize 按当前品种周期的总 Bars 数给 Smooth、Cycle、CyclePeriod 等一堆全局数组分配内存——注意这里没判断 Bars 是否变化,若品种切换或周期重载,旧数组长度可能不匹配。 冷启动或 tickCnt[0]==1 时会进入重算分支,nLimit 先取 rates_total-prev_calculated-1 作为循环起点,但马上被钳制到 rates_total-7 以内,说明算法至少需要前 7 根柱的历史才能稳定递推。 别把正态当圣经:这种 ArrayResize 放在 OnCalculate 里每次重算都调,虽能容错但会带来隐性开销;实盘贵金属或外汇短线高波动品种,建议在 OnInit 里按首屏 Bars 预分配一次,避免每 tick 反复申请内存。
class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">class="kw">datetime &time[], const class="type">class="kw">double &open[], const class="type">class="kw">double &high[], const class="type">class="kw">double &low[], const class="type">class="kw">double &close[], const class="type">long &tick_volume[], const class="type">long &volume[], const class="type">int &spread[]) { class=class="str">"cmt">//--- class="type">long tickCnt[class="num">1]; class="type">int i; class="type">int ticks=CopyTickVolume(Symbol(), class="num">0, class="num">0, class="num">1, tickCnt); if(ticks!=class="num">1) class="kw">return(rates_total); class="type">class="kw">double DC, MedianDelta; Comment(tickCnt[class="num">0]); if(prev_calculated==class="num">0 || tickCnt[class="num">0]==class="num">1) { class=class="str">"cmt">//--- 最近计数的柱将被重新计数 class="type">int nLimit=rates_total-prev_calculated-class="num">1; class=class="str">"cmt">// 计算起点索引 ArraySetAsSeries(high,true); ArraySetAsSeries(low,true); ArrayResize(Smooth,Bars(_Symbol,_Period)); ArrayResize(Cycle,Bars(_Symbol,_Period)); class=class="str">"cmt">//ArrayResize(Price,Bars(_Symbol,_Period)); ArrayResize(CyclePeriod,Bars(_Symbol,_Period)); ArrayResize(InstPeriod,Bars(_Symbol,_Period)); ArrayResize(Q1,Bars(_Symbol,_Period)); ArrayResize(I1,Bars(_Symbol,_Period)); ArrayResize(DeltaPhase,Bars(_Symbol,_Period)); if (nLimit>rates_total-class="num">7) class=class="str">"cmt">// 为最近的柱做调整 nLimit=rates_total-class="num">7; for(i=nLimit;i>=class="num">0 && !IsStopped();i--) { Smooth[i] = (Price(i)+class="num">2*Price(i+class="num">1)+class="num">2*Price(i+class="num">2)+Price(i+class="num">3))/class="num">6.0; if (i<rates_total-class="num">7) {
「周期提取里的相位限幅与中值平滑」
这段逻辑紧接前面的平滑处理,核心是把价格二次差分后的循环分量 Cycle 转成可交易的周期读数。当处于初始化区间之外,Cycle 用带 InpAlpha 的带通加权递归,否则直接取 (Price(i)-2*Price(i+1)+Price(i+2))/4.0 作为裸差分,两种分支在 MT5 里切换时肉眼能看出前几根柱子数值跳变。 Q1 是 Cycle 跨 6 根柱子的加权组合再乘 (0.5+0.08*InstPeriod[i+1]),I1 简单取滞后 3 根的 Cycle。DeltaPhase 用反正切近似公式算相邻相位差,但代码把下限锁 0.1、上限锁 0.9——这意味着瞬时周期不会被极端噪声压到 7 以下或顶到 63 以上。 MedianDelta 取最近 5 根 DeltaPhase 的中值,若中值为 0 则 DC 硬性给 15.0,否则 DC = 6.28318/MedianDelta + 0.5。InstPeriod 再对 DC 做 0.33/0.67 的 EMA,CyclePeriod 用更慢的 0.15/0.85 平滑,Trigger 直接等于上一根 CyclePeriod。外汇与贵金属波动跳空多,这种限幅能避免假周期,但高频噪音下仍可能给出偏短周期,实盘需结合小布盯盘的 AIGC 滤波二次确认。 Median 函数把目标区间拷进临时数组并排序取中值,偶长度取中间两数平均。你可以把 m_len 从 5 改成 9 在 EURUSD 的 M15 上回看,DC 曲线会明显变钝,信号延迟约增加 2~3 根柱子。
Cycle[i] = (class="num">1.0-class="num">0.5*InpAlpha) * (class="num">1.0-class="num">0.5*InpAlpha) * (Smooth[i]-class="num">2.0*Smooth[i+class="num">1]+Smooth[i+class="num">2]) +class="num">2.0*(class="num">1.0-InpAlpha)*Cycle[i+class="num">1]-(class="num">1.0-InpAlpha)*(class="num">1.0-InpAlpha)*Cycle[i+class="num">2]; } else { Cycle[i]=(Price(i)-class="num">2.0*Price(i+class="num">1)+Price(i+class="num">2))/class="num">4.0; } Q1[i] = (class="num">0.0962*Cycle[i]+class="num">0.5769*Cycle[i+class="num">2]-class="num">0.5769*Cycle[i+class="num">4]-class="num">0.0962*Cycle[i+class="num">6])*(class="num">0.5+class="num">0.08*InstPeriod[i+class="num">1]); I1[i] = Cycle[i+class="num">3]; if (Q1[i]!=class="num">0.0 && Q1[i+class="num">1]!=class="num">0.0) DeltaPhase[i] = (I1[i]/Q1[i]-I1[i+class="num">1]/Q1[i+class="num">1])/(class="num">1.0+I1[i]*I1[i+class="num">1]/(Q1[i]*Q1[i+class="num">1])); if (DeltaPhase[i] < class="num">0.1) DeltaPhase[i] = class="num">0.1; if (DeltaPhase[i] > class="num">0.9) DeltaPhase[i] = class="num">0.9; MedianDelta = Median(DeltaPhase, i, class="num">5); if (MedianDelta == class="num">0.0) DC = class="num">15.0; else DC = (class="num">6.28318/MedianDelta) + class="num">0.5; InstPeriod[i] = class="num">0.33 * DC + class="num">0.67 * InstPeriod[i+class="num">1]; CyclePeriod[i] = class="num">0.15 * InstPeriod[i] + class="num">0.85 * CyclePeriod[i+class="num">1]; Trigger[i] = CyclePeriod[i+class="num">1]; } } class=class="str">"cmt">//--- 返回 prev_calculated 值用于下次调用 class="kw">return(rates_total); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double Median(class="type">class="kw">double& arr[], class="type">int idx, class="type">int m_len) { class="type">class="kw">double MedianArr[]; class="type">int copied; class="type">class="kw">double result = class="num">0.0; ArraySetAsSeries(MedianArr, true); ArrayResize(MedianArr, m_len); copied = ArrayCopy(MedianArr, arr, class="num">0, idx, m_len); if (copied == m_len) { ArraySort(MedianArr); if (m_len %class="num">2 == class="num">0) result = (MedianArr[m_len/class="num">2] + MedianArr[(m_len/class="num">2)+class="num">1])/class="num">2.0; else result = MedianArr[m_len / class="num">2]; }
中值复制失败的兜底处理
在 MQL5 里做中值计算时,若数组元素拷贝数目与预期不符,必须显式报错并立即返回结果,否则后续统计会基于残缺数据跑出误导性信号。 上面这段兜底逻辑用 Print 输出 __FILE__ 与 __FUNCTION__ 定位出错位置,并提示“中值错误 - 复制元素数目错误.”,随后直接 return result 中断流程。 实盘验证时可在 MT5 策略测试器故意传入长度不匹配的数组,观察日志是否精确打印文件名与函数名,从而确认拷贝分支的健壮性。外汇与贵金属波动剧烈,这类底层数组错误若不被捕获,可能让 EA 在极端行情下发出错误订单,属高风险环节。
else Print(__FILE__+__FUNCTION__+"中值错误 - 复制元素数目错误."); class="kw">return result; }
◍ 循环周期线怎么在MT5里跑起来
循环周期指标本质是一个高通滤波器,只保留价格序列里的周期模式分量,两柱、三柱这类短周期组件再经有限脉冲响应低通滤波器平滑提取。它和同系列其他指标外观接近,但底层算法差异很大,属于价格行为里剥离趋势噪声看摆动的工具。 原始算法思路很直白:周期线上穿触发线时倾向买入,下穿时倾向卖出。外汇与贵金属波动受杠杆和消息面影响,这类信号可能出现假突破,实盘前务必在MT5策略测试器里验证。 下面这段MQL5把周期线和红色触发线画在独立窗口,默认alpha=0.07控制平滑强度,调大则线更灵敏、噪点也多。 //+------------------------------------------------------------------+
| // | CyberCycle.mq5 |
|---|---|
| // | Copyright 2011, Investeo.pl |
| // | http://Investeo.pl |
//+------------------------------------------------------------------+ #property copyright "Copyright 2011, Investeo.pl" #property link "http://Investeo.pl" #property version "1.00" #property indicator_separate_window #property description "CyberCycle 指标 - 由 John F. Ehlers 在" #property description " \"股票和期货的控制分析\"一书中介绍" #property description "本指标可以免费下载." #property indicator_buffers 2 #property indicator_plots 2 #property indicator_width1 1 #property indicator_width2 1 #property indicator_type1 DRAW_LINE #property indicator_type2 DRAW_LINE #property indicator_color1 Green #property indicator_color2 Red #property indicator_label1 "Cycle" #property indicator_label2 "Trigger Line" #define Price(i) ((high[i]+low[i])/2.0) double Smooth[]; double Cycle[]; double Trigger[]; input double InpAlpha=0.07; // alpha //+------------------------------------------------------------------+
| // | 自定义指标初始化函数 |
|---|
//+------------------------------------------------------------------+ int OnInit() { //--- 指标缓冲区映射 ArraySetAsSeries(Cycle,true); ArraySetAsSeries(Trigger,true); ArraySetAsSeries(Smooth,true); SetIndexBuffer(0,Cycle,INDICATOR_DATA); SetIndexBuffer(1,Trigger,INDICATOR_DATA); PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,0.0); PlotIndexSetDouble(1,PLOT_EMPTY_VALUE,0.0); return(0); } //+------------------------------------------------------------------+
| // | 自定义指标迭代函数 |
|---|
//+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, 逐行拆解:
#property indicator_separate_window让指标显示在副图,不和K线挤在一起。#define Price(i) ((high[i]+low[i])/2.0)取每根K线高低中点作为滤波输入,比收盘价更抗影线干扰。input double InpAlpha=0.07是平滑系数,0.07属于慢速档,欧元兑美元H1上约过滤掉小于3根K线的杂波。OnInit里三个ArraySetAsSeries(...,true)把数组倒序,使索引0对应最新柱,方便实时计算。SetIndexBuffer把Cycle和Trigger绑到绘图缓冲区,绿色周期线、红色触发线由此而来。
建议把alpha从0.07改成0.1~0.12,在XAUUSD的M15上对比穿越信号次数,感受灵敏度与噪点的权衡。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CyberCycle.mq5 | class=class="str">"cmt">//| Copyright class="num">2011, Investeo.pl | class=class="str">"cmt">//| http://Investeo.pl | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2011, Investeo.pl" class="macro">#class="kw">property link "http:class=class="str">"cmt">//Investeo.pl" class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property indicator_separate_window class="macro">#class="kw">property description "CyberCycle 指标 - 由 John F. Ehlers 在" class="macro">#class="kw">property description " \"股票和期货的控制分析\"一书中介绍" class="macro">#class="kw">property description "本指标可以免费下载." class="macro">#class="kw">property indicator_buffers class="num">2 class="macro">#class="kw">property indicator_plots class="num">2 class="macro">#class="kw">property indicator_width1 class="num">1 class="macro">#class="kw">property indicator_width2 class="num">1 class="macro">#class="kw">property indicator_type1 DRAW_LINE class="macro">#class="kw">property indicator_type2 DRAW_LINE class="macro">#class="kw">property indicator_color1 Green class="macro">#class="kw">property indicator_color2 Red class="macro">#class="kw">property indicator_label1 "Cycle" class="macro">#class="kw">property indicator_label2 "Trigger Line" class="macro">#define Price(i) ((high[i]+low[i])/class="num">2.0) class="type">class="kw">double Smooth[]; class="type">class="kw">double Cycle[]; class="type">class="kw">double Trigger[]; input class="type">class="kw">double InpAlpha=class="num">0.07; class=class="str">"cmt">// alpha class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- 指标缓冲区映射 ArraySetAsSeries(Cycle,true); ArraySetAsSeries(Trigger,true); ArraySetAsSeries(Smooth,true); SetIndexBuffer(class="num">0,Cycle,INDICATOR_DATA); SetIndexBuffer(class="num">1,Trigger,INDICATOR_DATA); PlotIndexSetDouble(class="num">0,PLOT_EMPTY_VALUE,class="num">0.0); PlotIndexSetDouble(class="num">1,PLOT_EMPTY_VALUE,class="num">0.0); class="kw">return(class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标迭代函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated,
「用逐笔成交量驱动重算的平滑周期滤波」
这段 OnCalculate 的核心不是算指标,而是先拿 CopyTickVolume 取最新 1 根 K 线的逐笔成交量 tickCnt[0],若不等于 1 就直接 return(rates_total) 跳过,等于 1 才认为新 tick 到达、需要重算最近一根柱。 重算起点 nLimit = rates_total - prev_calculated - 1,若 nLimit 超过 rates_total-4 则钳到 rates_total-4,保证 Smooth/Cycle 数组至少有 4 根前值的缓冲区。 Smooth[i] 用 (Price(i)+2*Price(i+1)+2*Price(i+2)+Price(i+3))/6.0 做对称加权,等价于 3 点三角平滑后再平滑一次;Cycle 对 i<rates_total-5 走 (1-0.5α)^2*(Δ²Smooth)+2(1-α)Cycle[i+1]-(1-α)^2*Cycle[i+2] 的带通递归,对最新 5 根则用 (Price(i)-2Price(i+1)+Price(i+2))/4.0 做冷启动近似。 Trigger[i] 直接取 Cycle[i+1],把上一根的周期分量当触发线。开 MT5 把 InpAlpha 从 0.07 调到 0.2,Cycle 振幅会明显变窄,外汇与贵金属波动下这种递归对滑点敏感,属高风险验证。
const class="type">class="kw">datetime &time[], const class="type">class="kw">double &open[], const class="type">class="kw">double &high[], const class="type">class="kw">double &low[], const class="type">class="kw">double &close[], const class="type">long &tick_volume[], const class="type">long &volume[], const class="type">int &spread[]) { class=class="str">"cmt">//--- class="type">long tickCnt[class="num">1]; class="type">int i; class="type">int ticks=CopyTickVolume(Symbol(), class="num">0, class="num">0, class="num">1, tickCnt); if(ticks!=class="num">1) class="kw">return(rates_total); Comment(tickCnt[class="num">0]); if(prev_calculated==class="num">0 || tickCnt[class="num">0]==class="num">1) { class=class="str">"cmt">//--- 最近计数的柱将被重新计数 class="type">int nLimit=rates_total-prev_calculated-class="num">1; class=class="str">"cmt">// 计算起点索引 ArraySetAsSeries(high,true); ArraySetAsSeries(low,true); ArrayResize(Smooth,Bars(_Symbol,_Period)); ArrayResize(Cycle,Bars(_Symbol,_Period)); if(nLimit>rates_total-class="num">4) class=class="str">"cmt">// 为最新的柱做调整 nLimit=rates_total-class="num">4; for(i=nLimit;i>=class="num">0 && !IsStopped();i--) { Smooth[i]=(Price(i)+class="num">2*Price(i+class="num">1)+class="num">2*Price(i+class="num">2)+Price(i+class="num">3))/class="num">6.0; if(i<rates_total-class="num">5) { Cycle[i]=(class="num">1.0-class="num">0.5*InpAlpha)*(class="num">1.0-class="num">0.5*InpAlpha)*(Smooth[i]-class="num">2.0*Smooth[i+class="num">1]+Smooth[i+class="num">2]) +class="num">2.0*(class="num">1.0-InpAlpha)*Cycle[i+class="num">1]-(class="num">1.0-InpAlpha)*(class="num">1.0-InpAlpha)*Cycle[i+class="num">2]; } else { Cycle[i]=(Price(i)-class="num">2.0*Price(i+class="num">1)+Price(i+class="num">2))/class="num">4.0; } class=class="str">"cmt">//Print(__FILE__+__FUNCTION__+" 接收数值: ",rCnt); Trigger[i]=Cycle[i+class="num">1]; } } class=class="str">"cmt">//--- 返回 prev_calculated 值用于下次调用 class="kw">return(rates_total); }
把静态周期换成动态读数
让指标具备自适应的核心,是用动态循环周期替代写死的参数。做法很直接:先挂上 CyclePeriod 自定义指标拿句柄,再在 OnCalculate() 里实时读它的缓冲,把当前主导周期喂给后续计算。 Ehlers 在自适应数码周期里,把主导循环周期当作指数平滑系数 alpha1 的分母项,关系是 alpha1 = 2/(周期+1)。这意味着行情周期缩短时平滑更灵敏,拉长时更迟钝,比固定长度均线更跟得上节奏。 实测对照里,自适应版相对非自适应版,买卖信号往往早发出一根柱。外汇与贵金属波动跳变频繁,这类早一柱的提示可能提高入场效率,但也放大假突破概率,属高风险用法,需自行在 MT5 回测验证。 下面这段是从初始化到取数的关键片段,直接拷进 MQ5 就能跑通句柄与周期读取:
hCyclePeriod=iCustom(NULL,class="num">0,"CyclePeriod",InpAlpha); if(hCyclePeriod==INVALID_HANDLE) { Print("CyclePeriod 指标不可用!"); class="kw">return(-class="num">1); } class="type">int copied=CopyBuffer(hCyclePeriod,class="num">0,i,class="num">1,CyclePeriod); if(copied<=class="num">0) { Print("失败: 无法从 CyclePeriod 指标中取得数据."); class="kw">return -class="num">1; } alpha1 = class="num">2.0/(CyclePeriod[class="num">0]+class="num">1.0); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| AdaptiveCyberCycle.mq5 | class=class="str">"cmt">//| Copyright class="num">2011, Investeo.pl | class=class="str">"cmt">//| http://Investeo.pl | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2011, Investeo.pl" class="macro">#class="kw">property link "http:class=class="str">"cmt">//Investeo.pl" class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property indicator_separate_window class="macro">#class="kw">property description "自适应 CyberCycle 指标 - 由 John F. Ehlers 在" class="macro">#class="kw">property description " \"股票和期货的控制分析\"一书中介绍" class="macro">#class="kw">property description "本指标可以免费下载." class="macro">#class="kw">property indicator_buffers class="num">2 class="macro">#class="kw">property indicator_plots class="num">2 class="macro">#class="kw">property indicator_width1 class="num">1 class="macro">#class="kw">property indicator_width2 class="num">1 class="macro">#class="kw">property indicator_type1 DRAW_LINE class="macro">#class="kw">property indicator_type2 DRAW_LINE class="macro">#class="kw">property indicator_color1 Green class="macro">#class="kw">property indicator_color2 Red class="macro">#class="kw">property indicator_label1 "Cycle" class="macro">#class="kw">property indicator_label2 "Trigger Line" class="macro">#define Price(i) ((high[i]+low[i])/class="num">2.0) class="type">class="kw">double Smooth[]; class="type">class="kw">double Cycle[]; class="type">class="kw">double Trigger[]; class="type">int hCyclePeriod; input class="type">class="kw">double InpAlpha=class="num">0.07; class=class="str">"cmt">// 循环周期的 alpha class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- 指标缓冲区映射 ArraySetAsSeries(Cycle,true); ArraySetAsSeries(Trigger,true); ArraySetAsSeries(Smooth,true); SetIndexBuffer(class="num">0,Cycle,INDICATOR_DATA); SetIndexBuffer(class="num">1,Trigger,INDICATOR_DATA);