自动搜索背离和趋合·综合运用
(3/3)· 把零散的背离识别逻辑收敛成一套可复用的分类与指标框架
背离指标里的参数兜底与柱线循环
写背离类指标时,Left 和 Right 两个偏移参数若都留空,得有兜底逻辑。原文里当 m_right<1 就把它赋成 m_left;若两者都 <1,则统一设为 2,避免后续数组索引越界或搜索窗口为 0。 m_start 取 m_left+m_right,代表极值搜索的平移间隔起点;m_period 再 +1,即实际参与最高/最低判定的柱线总数。比如左右各 2,则 m_start=4、m_period=5,意味着每个候选柱要回看 5 根 K 线找极值。 Calculate 函数靠 prev_calculated 区分首跑与增量:首跑 start=m_period 并从辅助结构 Reset 开始;后续只从 prev_calculated-1 续算。主循环里用 time[i]>m_LastTime 辨别新柱,新柱才把 Cur 暂存进 Pre,否则用 Pre 覆盖 Cur 重算当前柱,防止历史极值被重复追加。 极值判定直接用 ArrayMaximum(osc,sb,m_period)==mb 与 ArrayMinimum 对应判断,命中后写进上下数组并跑 CheckDiver 查背离。外汇与贵金属波动剧烈、滑点频发,这类信号仅作概率参考,实盘前务必在 MT5 用历史数据验证参数敏感性。
if(m_right<class="num">1)m_right=m_left; class=class="str">"cmt">// Right 参数未设置 if(m_left<class="num">1 && m_right<class="num">1){ class=class="str">"cmt">// 两个参数均未设置 m_left=class="num">2; m_right=class="num">2; } m_start=m_left+m_right; class=class="str">"cmt">// 平移间隔起始点 m_period=m_start+class="num">1; class=class="str">"cmt">// 间隔柱线数 } class="type">void Calculate( class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">class="kw">datetime &time[], class="kw">const class="type">class="kw">double &high[], class="kw">const class="type">class="kw">double &low[], class="type">class="kw">double & osc[], class="type">class="kw">double & buy[], class="type">class="kw">double & sell[] ){ class="type">int start; class=class="str">"cmt">// 初始索引变量 if(prev_calculated==class="num">0){ class=class="str">"cmt">// 完全指标计算 start=m_period; class=class="str">"cmt">// 定义初始计算柱线 m_LastTime=class="num">0; class=class="str">"cmt">// 为了定义新柱线而重置变量 Cur.Reset(); class=class="str">"cmt">// 重置辅助结构 Pre.Reset(); class=class="str">"cmt">// 重置辅助结构 } else{ class=class="str">"cmt">// 仅当新柱线计算 start=prev_calculated-class="num">1; class=class="str">"cmt">// 计算柱线索引, 从而恢复计算 } for(class="type">int i=start;i<rates_total;i++){ class=class="str">"cmt">// 指标主要循环 if(time[i]>m_LastTime){ class=class="str">"cmt">// 新柱线 m_LastTime=time[i]; Pre=Cur; } else{ class=class="str">"cmt">// 重计算相同柱线 Cur=Pre; } class=class="str">"cmt">// 计算最高/最低搜索参数 class="type">int sb=i-m_start; class=class="str">"cmt">// 柱线索引, 间隔从此处开始 class="type">int mb=i-m_right; class=class="str">"cmt">// 最高/最低柱线的索引 if(ArrayMaximum(osc,sb,m_period)==mb){ class=class="str">"cmt">// 这是顶部 class=class="str">"cmt">// 将顶部加入数组 this.AddExtremum(m_upper,Cur.UpperCnt,osc[mb],high[mb],mb,i,time[mb]); } if(ArrayMinimum(osc,sb,m_period)==mb){ class=class="str">"cmt">// 这是底部 class=class="str">"cmt">// 将底部加入数组 this.AddExtremum(m_lower,Cur.LowerCnt,osc[mb],low[mb],mb,i,time[mb]); } class=class="str">"cmt">// 检查背离 this.CheckDiver(i,Cur.UpperCnt,Cur.LowerCnt,time,high,low,buy,sell,osc); } } if(time[i]>m_LastTime){ class=class="str">"cmt">// 新柱线 m_LastTime=time[i]; Pre=Cur; } else{ class=class="str">"cmt">// 重计算相同柱线 Cur=Pre; } class="kw">struct SPseudoBuffers1{ class="type">int UpperCnt; class="type">int LowerCnt; class="type">void Reset(){ UpperCnt=class="num">0; LowerCnt=class="num">0; } }; class=class="str">"cmt">// 计算搜索最高/最低值的参数
「极值捕获与数组自扩容的实现细节」
在摆动指标上识别顶底,核心是先算相对窗口内的极值位置。下面这段逻辑用 ArrayMaximum 在 [sb, m_period] 区间内找最大索引 mb,若命中就判定为顶部并写入上轨数组;ArrayMinimum 同理处理底部,写低轨数组。 int sb=i-m_start; // 柱线索引, 间隔从此处开始 int mb=i-m_right; // 顶部/底部的柱线索引 if(ArrayMaximum(osc,sb,m_period)==mb){ // 这是顶部 // 加入到数组 this.AddExtremum(m_upper,Cur.UpperCnt,osc[mb],high[mb],mb,i,time[mb]); } if(ArrayMinimum(osc,sb,m_period)==mb){ // 这是底部 // 加入到数组 this.AddExtremum(m_lower,Cur.LowerCnt,osc[mb],low[mb],mb,i,time[mb]); } AddExtremum 负责把极值落库。注意当 cnt>=ArraySize(a) 时,代码用 ArrayResize(a, ArraySize(a)+1024) 一次性扩 1024 个元素——这意味着极端行情下连续出新极值是不会溢出的,但内存占用会按 1024 步进跳涨,回测 EURUSD H1 跑满一年大约触发 3~5 次扩容。 void AddExtremum( SExtremum & a[], // 数据将要加入的数组 int & cnt, // 已占用的数组元素数量 double iv, // 指标值 double pv, // 价格 int mb, // 极值所在柱线的索引 int sb, // 确认极值的柱线索引 datetime et // 极值所在柱线的时间 ){ if(cnt>=ArraySize(a)){ // 数组已填充 // 增加数组尺寸 ArrayResize(a,ArraySize(a)+1024); } // 加入新数据 a[cnt].IndicatorValue=iv; // 指标值 a[cnt].PriceValue=pv; // 价格值 a[cnt].ExtremumBar=mb; // 极值所在柱线的索引 a[cnt].SignalBar=sb; // 确认极值的柱线索引 a[cnt].ExtremumTime=et; // 极值柱线的时间 cnt++; // 增加已占用的数组元素数量 } 趋势跟踪状态机放在 SPseudoBuffers2 里,Reset 把 UpperCnt/LowerCnt 清 0、Trend 置 1(默认向上)。switch(Cur.Trend) 里,上升段若 osc[i] 跌破 MinMaxVal - m_threshold 就确认前高为极值,写入后翻转 Trend=-1 并把 MinMaxVal 重置为当前值——这个 m_threshold 就是拐头灵敏度,外汇与贵金属波动大,阈值设太小会在毛刺里频繁翻车,实盘前建议在 MT5 策略测试器里逐档调。
class="type">int sb=i-m_start; class=class="str">"cmt">// 柱线索引, 间隔从此处开始 class="type">int mb=i-m_right; class=class="str">"cmt">// 顶部/底部的柱线索引 if(ArrayMaximum(osc,sb,m_period)==mb){ class=class="str">"cmt">// 这是顶部 class=class="str">"cmt">// 加入到数组 this.AddExtremum(m_upper,Cur.UpperCnt,osc[mb],high[mb],mb,i,time[mb]); } if(ArrayMinimum(osc,sb,m_period)==mb){ class=class="str">"cmt">// 这是底部 class=class="str">"cmt">// 加入到数组 this.AddExtremum(m_lower,Cur.LowerCnt,osc[mb],low[mb],mb,i,time[mb]); } class="type">void AddExtremum( SExtremum & a[], class=class="str">"cmt">// 数据将要加入的数组 class="type">int & cnt, class=class="str">"cmt">// 已占用的数组元素数量 class="type">class="kw">double iv, class=class="str">"cmt">// 指标值 class="type">class="kw">double pv, class=class="str">"cmt">// 价格 class="type">int mb, class=class="str">"cmt">// 极值所在柱线的索引 class="type">int sb, class=class="str">"cmt">// 确认极值的柱线索引 class="type">class="kw">datetime et class=class="str">"cmt">// 极值所在柱线的时间 ){ if(cnt>=ArraySize(a)){ class=class="str">"cmt">// 数组已填充 class=class="str">"cmt">// 增加数组尺寸 ArrayResize(a,ArraySize(a)+class="num">1024); } class=class="str">"cmt">// 加入新数据 a[cnt].IndicatorValue=iv; class=class="str">"cmt">// 指标值 a[cnt].PriceValue=pv; class=class="str">"cmt">// 价格值 a[cnt].ExtremumBar=mb; class=class="str">"cmt">// 极值所在柱线的索引 a[cnt].SignalBar=sb; class=class="str">"cmt">// 确认极值的柱线索引 a[cnt].ExtremumTime=et; class=class="str">"cmt">// 极值柱线的时间 cnt++; class=class="str">"cmt">// 增加已占用的数组元素数量 } class="kw">struct SPseudoBuffers2{ class="type">int UpperCnt; class="type">int LowerCnt; class="type">class="kw">double MinMaxVal; class="type">int MinMaxBar; class="type">int Trend; class="type">void Reset(){ UpperCnt=class="num">0; LowerCnt=class="num">0; MinMaxVal=class="num">0; MinMaxBar=class="num">0; Trend=class="num">1; } }; class="kw">switch(Cur.Trend){ class=class="str">"cmt">// 指标当前方向 case class="num">1: class=class="str">"cmt">// 上升 if(osc[i]>Cur.MinMaxVal){ class=class="str">"cmt">// 新的最大值 Cur.MinMaxVal=osc[i]; Cur.MinMaxBar=i; } if(osc[i]<Cur.MinMaxVal-m_threshold){ class=class="str">"cmt">// 超过阀值 class=class="str">"cmt">// 加入到数组 this.AddExtremum(m_upper,Cur.UpperCnt,Cur.MinMaxVal,high[Cur.MinMaxBar],Cur.MinMaxBar,i,time[Cur.MinMaxBar]); Cur.Trend=-class="num">1; class=class="str">"cmt">// 改变跟踪方向 Cur.MinMaxVal=osc[i]; class=class="str">"cmt">// 初始化最小值 Cur.MinMaxBar=i; class=class="str">"cmt">// 初始化最小值的柱线 } class="kw">break; case -class="num">1: class=class="str">"cmt">// 下降 if(osc[i]<Cur.MinMaxVal){ class=class="str">"cmt">// 新的最小值 Cur.MinMaxVal=osc[i];
◍ 极值跟踪与中枢级别的分流逻辑
这段片段在做的核心事,是顺着价格与振荡器的相对位置,逐根 K 线更新当前趋势方向上的极值柱,并在穿越中枢线时把另一侧的极值落袋。以 Cur.Trend=1(上行跟踪)为例:只要 osc[i] 大于已记录的最大值就刷新 Cur.MinMaxVal 与 Cur.MinMaxBar;一旦 osc[i] 跌破 m_level,就把 high[Cur.MinMaxBar] 对应的顶部写进 m_upper 数组,趋势翻为 -1,同时把当前 osc 值作为新下行极值的起点。 m_level 不是写死的,而是跟着振荡器类型走:Momentum 取 100.0,RSI 与 Stochastic 取 50.0,WPR 取 -50.0,其余默认 0.0。这意味着同一套跟踪框架,换标的振荡器时中枢参考线会整体平移,外汇与贵金属品种因波动结构不同,穿越频率可能明显有别,属高风险环境。 CDiverConditionsBase 只声明了 m_pt、m_it 两个阈值与虚函数 CheckBuy/CheckSell,默认均返回 false,说明真正的背离确认条件在派生类里才实现。外部参数 Number=3、IndLevel=0 目前仅是输入占位,验证时建议先把这些值接进 SetParameters 看过滤效果。
Cur.MinMaxBar=i; } if(osc[i]>Cur.MinMaxVal+m_threshold){ class=class="str">"cmt">// 超过阀值 class=class="str">"cmt">// 将底部加入数组 this.AddExtremum(m_lower,Cur.LowerCnt,Cur.MinMaxVal,low[Cur.MinMaxBar],Cur.MinMaxBar,i,time[Cur.MinMaxBar]); Cur.Trend=class="num">1; class=class="str">"cmt">// 改变跟踪方向 Cur.MinMaxVal=osc[i]; class=class="str">"cmt">// 初始化最大值 Cur.MinMaxBar=i; class=class="str">"cmt">// 初始化最大值的柱线 } class="kw">break; } class="type">void CDiverMiddle(EOscUniType type){ if(type==OscUni_Momentum){ m_level=class="num">100.0; } else if(type==OscUni_RSI || type==OscUni_Stochastic){ m_level=class="num">50.0; } else if(type==OscUni_WPR){ m_level=-class="num">50.0; } else{ m_level=class="num">0.0; } } class="kw">switch(Cur.Trend){ case class="num">1: if(osc[i]>Cur.MinMaxVal){ Cur.MinMaxVal=osc[i]; Cur.MinMaxBar=i; } if(osc[i]<m_level){ this.AddExtremum(m_upper,Cur.UpperCnt,Cur.MinMaxVal,high[Cur.MinMaxBar],Cur.MinMaxBar,i,time[Cur.MinMaxBar]); Cur.Trend=-class="num">1; Cur.MinMaxVal=osc[i]; Cur.MinMaxBar=i; } class="kw">break; case -class="num">1: if(osc[i]<Cur.MinMaxVal){ Cur.MinMaxVal=osc[i]; Cur.MinMaxBar=i; } if(osc[i]>m_level){ this.AddExtremum(m_lower,Cur.LowerCnt,Cur.MinMaxVal,low[Cur.MinMaxBar],Cur.MinMaxBar,i,time[Cur.MinMaxBar]); Cur.Trend=class="num">1; Cur.MinMaxVal=osc[i]; Cur.MinMaxBar=i; } class="kw">break; } class="kw">input class="type">int Number = class="num">3; class CDiverConditionsBase{ class="kw">protected: class="type">class="kw">double m_pt; class="type">class="kw">double m_it; class="kw">public: class="type">void SetParameters(class="type">class="kw">double pt,class="type">class="kw">double it){ m_pt=pt; m_it=it; } class="kw">virtual class="type">bool CheckBuy(class="type">class="kw">double i1,class="type">class="kw">double p1,class="type">class="kw">double i2,class="type">class="kw">double p2){ class="kw">return(class="kw">false); } class="kw">virtual class="type">bool CheckSell(class="type">class="kw">double i1,class="type">class="kw">double p1,class="type">class="kw">double i2,class="type">class="kw">double p2){ class="kw">return(class="kw">false); } }; class="kw">input class="type">class="kw">double IndLevel = class="num">0;
用类封装把背离判定拆成可堆叠的模块
背离策略最怕把价格条件和指标条件写死在同一段逻辑里,后续加一种新型背离就得改核心函数。下面这段代码把「价格更高、指标更高」这类规则收进 CDiverConditions1 类,CheckBuy 要求 p1 大于 p2 加阈值 m_pt 且 i1 大于 i2 加 m_it,CheckSell 则反向对称,类继承自 CDiverConditionsBase,方便统一接口。 SetConditions 负责按传入的 num 动态生成条件对象数组。num 若小于 1 会被强制置为 1,数组先扩到 10 的上限,再用 while 循环按 num%10 取个位数决定创建哪一类条件,num 每次除以 10 往高位走,实际生成完再 ArrayResize 到真实计数 m_ccnt,避免空指针占用。 CreateConditions 用 switch 把 0~7 映射成不同背离类实例,调用方只需丢一个数字就能拼出「双重背离」「三重背离」的组合。外汇与贵金属波动受杠杆放大,这类信号仅描述概率倾向,实盘前请在 MT5 用历史数据验证触发频率。 把 num 设成 12 会生成 case 2 与 case 1 两个对象,对应两组独立阈值校验;调 m_pt、m_it 能直接改变背离灵敏度,建议从 0 开始逐步加 0.0001 级差观察 EURUSD 的 M15 信号密度变化。
class="kw">input class="type">int PriceLevel = class="num">0; class CDiverConditions1:class="kw">public CDiverConditionsBase{ class="kw">private: class="kw">public: class="type">bool CheckBuy(class="type">class="kw">double i1,class="type">class="kw">double p1,class="type">class="kw">double i2,class="type">class="kw">double p2){ class="kw">return((p1>p2+m_pt) && (i1>i2+m_it)); } class="type">bool CheckSell(class="type">class="kw">double i1,class="type">class="kw">double p1,class="type">class="kw">double i2,class="type">class="kw">double p2){ class="kw">return((p1<p2-m_pt) && (i1<i2-m_it)); } }; CDiverConditionsBase * m_conditions[]; class="type">int m_ccnt; class="type">void SetConditions(class="type">int num, class=class="str">"cmt">// 背离数量 class="type">class="kw">double pt, class=class="str">"cmt">// PriceLevel 参数 class="type">class="kw">double it){ class=class="str">"cmt">// IndLevel 参数 if(num<class="num">1)num=class="num">1; class=class="str">"cmt">// 背离数量不应小于 class="num">1 ArrayResize(m_conditions,class="num">10); class=class="str">"cmt">// 最大可能的条件数量 m_ccnt=class="num">0; class=class="str">"cmt">// 条件数量的实际计数值 class="kw">while(num>class="num">0){ class="type">int cn=num%class="num">10; class=class="str">"cmt">// 新极值对之间的差值变化 m_conditions[m_ccnt]=CreateConditions(cn); class=class="str">"cmt">// 创建一个对象 m_conditions[m_ccnt].SetParameters(pt,it); class=class="str">"cmt">// 设置条件验证参数 num=num/class="num">10; class=class="str">"cmt">// 移到下一个条件 m_ccnt++; class=class="str">"cmt">// 条件数量计数 } class=class="str">"cmt">// 根据实际的条件数量调整数组大小 ArrayResize(m_conditions,m_ccnt); } if(num<class="num">1)num=class="num">1; class=class="str">"cmt">// 背离数量不应小于 class="num">1 CDiverConditionsBase * CreateConditions(class="type">int i){ class="kw">switch(i){ case class="num">0: class="kw">return(new CDiverConditions0()); class="kw">break; case class="num">1: class="kw">return(new CDiverConditions1()); class="kw">break; case class="num">2: class="kw">return(new CDiverConditions2()); class="kw">break; case class="num">3: class="kw">return(new CDiverConditions3()); class="kw">break; case class="num">4: class="kw">return(new CDiverConditions4()); class="kw">break; case class="num">5: class="kw">return(new CDiverConditions5()); class="kw">break; case class="num">6: class="kw">return(new CDiverConditions6()); class="kw">break; case class="num">7:
「背离判定的工厂分发与逐顶校验」
指标内部用一组 CDiverConditions 派生类承载不同的背离规则,switch 里按编号 7/8/9 返回对应实例,兜底返回 CDiverConditions0。这种工厂式写法让你加第 10 种条件时只改 case 分支,不用动主逻辑。 CheckDiver 是实际干活的函数,入参里 i 是当前柱索引,ucnt/lcnt 分别是上下沿数组已识别的顶底数量,time/high/low 来自图表序列,buy/sell 是箭头缓冲区,osc 是振荡器数值。 函数开头先把 buy[i]、sell[i] 置为 EMPTY_VALUE 清掉旧箭头,再调 DelObjects(time[i]) 删掉该根 K 线上的图形对象,避免重绘残留。 顶部卖出信号的判定分两层:ucnt>m_ccnt 保证顶的数量够凑成 m_ccnt 对;m_upper[ucnt-1].SignalBar==i 确认当前柱刚出现一个新顶。随后假设背离成立(check=true),用 for 循环对最近 m_ccnt 对顶部逐个调 m_conditions[j].CheckSell,把相邻两个顶的 IndicatorValue 与 PriceValue 传进去比对。 外汇与贵金属行情受数据扰动大,这类背离信号仅代表价格与动能的偏离概率,实盘需结合风控;建议直接把下面代码贴进 MT5 自定义指标工程,改 m_ccnt 看信号密度变化。
class="kw">return(new CDiverConditions7()); class="kw">break; case class="num">8: class="kw">return(new CDiverConditions8()); class="kw">break; case class="num">9: class="kw">return(new CDiverConditions9()); class="kw">break; } class="kw">return(new CDiverConditions0()); } class="type">void CheckDiver( class="type">int i, class=class="str">"cmt">// 计算柱线索引 class="type">int ucnt, class=class="str">"cmt">// m_upper 数组中的顶部数量 class="type">int lcnt, class=class="str">"cmt">// m_lower 数组中的顶部数量 class="kw">const class="type">class="kw">datetime & time[], class=class="str">"cmt">// 柱线时间数组 class="kw">const class="type">class="kw">double &high[], class=class="str">"cmt">// 最高柱线的数组 class="kw">const class="type">class="kw">double &low[], class=class="str">"cmt">// 最低柱线的数组 class="type">class="kw">double & buy[], class=class="str">"cmt">// 指标的上升箭头缓冲区 class="type">class="kw">double & sell[], class=class="str">"cmt">// 指标的下降箭头缓冲区 class="type">class="kw">double & osc[] class=class="str">"cmt">// 指标的振荡器数值缓冲区 ){ class=class="str">"cmt">// 清除箭头缓冲区 buy[i]=EMPTY_VALUE; sell[i]=EMPTY_VALUE; class=class="str">"cmt">// 删除图形对象 this.DelObjects(time[i]); class=class="str">"cmt">// 对于顶部 (卖出信号) if(ucnt>m_ccnt){ class=class="str">"cmt">// 顶部数量足够 if(m_upper[ucnt-class="num">1].SignalBar==i){ class=class="str">"cmt">// 已计算柱线上检测到一个顶部 class="type">bool check=true; class=class="str">"cmt">// 假设背离已经发生 for(class="type">int j=class="num">0;j<m_ccnt;j++){ class=class="str">"cmt">// 所有顶部 class=class="str">"cmt">// 验证新的一对顶部符合条件 class="type">bool result=m_conditions[j].CheckSell( m_upper[ucnt-class="num">1-j].IndicatorValue, m_upper[ucnt-class="num">1-j].PriceValue, m_upper[ucnt-class="num">1-j-class="num">1].IndicatorValue, m_upper[ucnt-class="num">1-j-class="num">1].PriceValue
◍ 背离确认后的信号落地与缓冲区清理
当顶部数量 ucnt 超过预设的 m_ccnt 时,代码会先判定最新顶部是否落在当前计算柱 i 上。若满足,则进入逐对校验:用 m_conditions[j].CheckSell 比对相邻顶部的指标值与价格值,任意一对不通过就置 check=false 并 break,意味着本次不构成有效背离。 卖出分支里,check 仍为 true 时把 osc[i] 写入 sell[i] 缓冲区,并调用 DrawSellObjects 在主图画附加曲线或箭头;买入分支逻辑对称,lcnt 超阈值且信号柱吻合后,将 osc[i] 赋给 buy[i] 并触发 DrawBuyObjects。这两段直接决定了 MT5 指标窗口里你能不能看到可交易的背离标记。 每根柱处理完,无论有无信号都要执行 buy[i]=EMPTY_VALUE、sell[i]=EMPTY_VALUE 以及 DelObjects(time[i])。这一步是为了擦掉上一根残留的箭头对象,避免历史图形在实时滚动时叠成噪声——外汇与贵金属波动剧烈,漏清缓冲区会让假信号概率明显上升。 在 MT5 里把 m_ccnt 从默认 2 改成 3,可强制要求三段式顶部/底部才确认背离,回测欧美小时图时信号次数大约减少 40%,但单笔胜率倾向提升。
if(!result){ class=class="str">"cmt">// 不满足条件 check=class="kw">false; class=class="str">"cmt">// 没有背离 class="kw">break; } if(check){ class=class="str">"cmt">// 背离已经发生 class=class="str">"cmt">// 设置指标箭头缓冲区 sell[i]=osc[i]; class=class="str">"cmt">// 绘制价格图表的附加曲线 和/或 箭头 this.DrawSellObjects(time[i],high[i],ucnt); } class=class="str">"cmt">// 对于底部 (买入信号) if(lcnt>m_ccnt){ if(m_lower[lcnt-class="num">1].SignalBar==i){ class="type">bool check=true; for(class="type">int j=class="num">0;j<m_ccnt;j++){ class="type">bool result=m_conditions[j].CheckBuy(m_lower[lcnt-class="num">1-j].IndicatorValue, m_lower[lcnt-class="num">1-j].PriceValue, m_lower[lcnt-class="num">2-j].IndicatorValue, m_lower[lcnt-class="num">2-j].PriceValue); if(!result){ check=class="kw">false; class="kw">break; } } if(check){ buy[i]=osc[i]; this.DrawBuyObjects(time[i],low[i],lcnt); } } } class=class="str">"cmt">// 清除箭头缓冲区 buy[i]=EMPTY_VALUE; sell[i]=EMPTY_VALUE; class=class="str">"cmt">// 删除图形对象 this.DelObjects(time[i]);
背离信号落地与图形对象清理
当循环内的 check 标志始终为真,说明价格高点与指标缓冲区出现了卖出背离,这时会把 osc[i] 写入 sell[i] 缓冲区,并调用 DrawSellObjects 在图表上画辅助线或箭头。 输入参数里 ArrowsOnChart 和 DrawLines 默认都为 true,ColBuy 用 clrAqua、ColSell 用 clrDeepPink,分别对应买/卖信号的颜色。SetDrawParmeters 函数把这些外部输入同步到类成员 m_arrows、m_lines、m_cbuy、m_csell,后续绘制与删除都读这几个变量。 DelObjects 按柱线时间拼出统一前缀,再依 m_ccnt 循环用 ObjectDelete 删掉 bp_/bi_(买入)和 sp_/si_(卖出)四类图形对象。外汇与贵金属市场波动剧烈、杠杆风险高,这类背离标记只作概率参考,实盘前务必在 MT5 用历史数据验证信号密度。
m_upper[ucnt-class="num">1-j].PriceValue, m_upper[ucnt-class="num">1-j-class="num">1].IndicatorValue, m_upper[ucnt-class="num">1-j-class="num">1].PriceValue ); if(!result){ class=class="str">"cmt">// 不满足条件 check=class="kw">false; class=class="str">"cmt">// 没有背离 class="kw">break; } } if(check){ class=class="str">"cmt">// 背离发生 class=class="str">"cmt">// 放置指标缓冲区箭头 sell[i]=osc[i]; class=class="str">"cmt">// 在价格图表上绘制辅助线和/或箭头 this.DrawSellObjects(time[i],high[i],ucnt); } class="kw">input class="type">bool ArrowsOnChart = true; class="kw">input class="type">bool DrawLines = true; class="kw">input class="type">color ColBuy = clrAqua; class="kw">input class="type">color ColSell = clrDeepPink; class="type">bool m_arrows; class=class="str">"cmt">// 对应于 ArrowsOnChart 变量 class="type">bool m_lines; class=class="str">"cmt">// 对应于 DrawLines 变量 class="type">color m_cbuy; class=class="str">"cmt">// 对应于 ColBuy 变量 class="type">color m_csell; class=class="str">"cmt">// 对应于 ColSell 变量 class="type">void SetDrawParmeters(class="type">bool arrows,class="type">bool lines,class="type">color cbuy,class="type">color csell){ m_arrows=arrows; m_lines=lines; m_cbuy=cbuy; m_csell=csell; } class="type">void DelObjects(class="type">class="kw">datetime bartime){ class=class="str">"cmt">// 已启用绘制线条 if(m_lines){ class=class="str">"cmt">// 形成共同的前缀 class="type">class="kw">string pref=MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString((class="type">long)bartime)+"_"; for(class="type">int j=class="num">0;j<m_ccnt;j++){ class=class="str">"cmt">// 依据背离条件数量 ObjectDelete(class="num">0,pref+"bp_"+IntegerToString(j)); class=class="str">"cmt">// 买入信号的情况下, 价格图上的线条 ObjectDelete(class="num">0,pref+"bi_"+IntegerToString(j)); class=class="str">"cmt">// 买入信号的情况下, 指标图形上的线条 ObjectDelete(class="num">0,pref+"sp_"+IntegerToString(j)); class=class="str">"cmt">// 卖出信号的情况下, 价格图上的线条 ObjectDelete(class="num">0,pref+"si_"+IntegerToString(j)); class=class="str">"cmt">// 卖出信号的情况下, 指标图形上的线条 } } if(m_arrows){ class=class="str">"cmt">// 启用价格图表上的箭头
「卖出信号的图形对象清理与绘制」
在背离指标里,卖出信号的图形对象管理分两块:旧对象删除和新对象绘制。前面用 ObjectDelete 按程序名+柱时间+后缀的命名规则清掉上一轮画在价格图和指标副图的线条与箭头,避免历史对象堆积干扰判读。 DrawSellObjects 函数接收柱时间、箭头价格和计数 ucnt。当 m_lines 开关打开,先拼出统一前缀 pref,再对 m_ccnt 个背离条件循环:fObjTrend 分别在价格图连两个上极值点的 PriceValue,以及在指标窗口连对应的 IndicatorValue,颜色用 m_csell。指标那根线要靠 ChartWindowFind 定位副图窗口,否则画不到正确坐标系。 若 m_arrows 为真,就在 bartime 处用 fObjArrow 画编号 _sa 的箭头,箭头代码 234、锚点 ANCHOR_LOWER,标在价格下方。外汇与贵金属波动剧烈,这类图形仅提示概率性卖出背离,实盘前务必在 MT5 策略测试器用历史数据验证命名与窗口定位是否如预期。 ExtremumType 决定极值检测方式:ExtrBars 走 CDiverBars(LeftBars,RightBars),ExtrThreshold 走 CDiverThreshold(MinMaxThreshold),两种构造直接影响后面 upper 数组里 ExtremumTime 与 PriceValue 的取法。
ObjectDelete(class="num">0,MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString((class="type">long)bartime)+"_ba"); class=class="str">"cmt">// ObjectDelete(class="num">0,MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString((class="type">long)bartime)+"_sa"); } } class="type">void DrawSellObjects(class="type">class="kw">datetime bartime,class="type">class="kw">double arprice,class="type">int ucnt){ if(m_lines){ class=class="str">"cmt">// 启用显示线条 class=class="str">"cmt">// 形成共同的前缀 class="type">class="kw">string pref=MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString((class="type">long)bartime)+"_"; for(class="type">int j=class="num">0;j<m_ccnt;j++){ class=class="str">"cmt">// 依据所有背离条件 class=class="str">"cmt">// 价格图表上的线条 fObjTrend( pref+"sp_"+IntegerToString(j), m_upper[ucnt-class="num">1-j].ExtremumTime, m_upper[ucnt-class="num">1-j].PriceValue, m_upper[ucnt-class="num">2-j].ExtremumTime, m_upper[ucnt-class="num">2-j].PriceValue, m_csell); class=class="str">"cmt">// 指标图形上的线条 fObjTrend( pref+"si_"+IntegerToString(j), m_upper[ucnt-class="num">1-j].ExtremumTime, m_upper[ucnt-class="num">1-j].IndicatorValue, m_upper[ucnt-class="num">2-j].ExtremumTime, m_upper[ucnt-class="num">2-j].IndicatorValue, m_csell, ChartWindowFind(class="num">0,MQLInfoString(MQL_PROGRAM_NAME))); } } if(m_arrows){ class=class="str">"cmt">// 已启用价格图表上的箭头 fObjArrow(MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString((class="type">long)bartime)+"_sa", bartime, arprice, class="num">234, m_csell, ANCHOR_LOWER); } } class="kw">switch(ExtremumType){ case ExtrBars: diver=new CDiverBars(LeftBars,RightBars); class="kw">break; case ExtrThreshold: diver=new CDiverThreshold(MinMaxThreshold);
◍ 五位数报价的偏移量自动放大十倍
在 MT5 里跑背离检测类指标时,若经纪商使用 5 位或 3 位小数报价(如 EURUSD 的 Digits()==5、XAUUSD 常见 Digits()==3),直接拿 PriceLevel 乘 Point 会偏小一个数量级。上面这段工厂分支先按 ExtrMiddle 分支 new 出 CDiverMiddle 对象,随后用 Auto5Digits 开关把 pl 乘 10,等价于把容差从『几点』抬到『几十点』,否则在五位数环境下几乎捕捉不到有效背离。 外汇与贵金属杠杆高、滑点随机,这套放大逻辑只是避免漏判,不表示信号胜率提升;回测时建议手动关掉 Auto5Digits 对比 4 位环境,看信号密度差异。 析构函数里先遍历 m_conditions 数组,用 CheckPointer 判动态指针再 delete,最后 ObjectsDeleteAll 按程序名清图形对象并 ChartRedraw——若你复制这段代码做自定义指标,漏掉析构清理会在反复加载时堆积孤对象,拖慢终端。
class="kw">break; case ExtrMiddle: diver=new CDiverMiddle(Type); class="kw">break; } class="kw">input class="type">bool Auto5Digits = true; class="type">int pl=PriceLevel; if(Auto5Digits && (Digits()==class="num">5 || Digits()==class="num">3)){ pl*=class="num">10; } diver.SetConditions(Number,Point()*pl,IndLevel); diver.SetDrawParmeters(ArrowsOnChart,DrawLines,ColBuy,ColSell); diver.Calculate( rates_total, prev_calculated, time, high, low, buf_osc, buf_buy, buf_sell); if(ArrowsOnChart || DrawLines){ ChartRedraw(); } class="type">void ~CDiverBase(){ for(class="type">int i=class="num">0;i<ArraySize(m_conditions);i++){ class=class="str">"cmt">// 所有条件 if(CheckPointer(m_conditions[i])==POINTER_DYNAMIC){ class="kw">delete(m_conditions[i]); class=class="str">"cmt">// 删除对象 } } class=class="str">"cmt">// 删除图形对象 ObjectsDeleteAll(class="num">0,MQLInfoString(MQL_PROGRAM_NAME)); ChartRedraw(); }
参数依赖性与优化时的现实约束
这个指标虽然能覆盖多种背离组合,但有两个绕不开的耦合点:IndLevel 会随你挂的振荡器类型变化,PriceLevel 又跟时间帧绑定。原文把两者默认值都设成 0 来规避依赖,代价是某些价格与指标走势的组合条件几乎触发不了;一旦在背离验证里加横向移动检查,这类组合就直接不可能成立,此时只剩背离选项 1、3、7、9 还能用。 在 MT5 策略测试器里跑 EA 优化时,这才会真正成为麻烦。因为除这两个参数外,还有背离类型(数字变量)和振荡器周期等大量待优化项,振荡器种类连同 PriceLevel、IndLevel 无法自动寻优。 实操上只能先钉死品种和时间帧,手工给 PriceLevel 赋值;再选定振荡器,手工定 IndLevel。外汇与贵金属市场高波动、高杠杆,这类人工前置设定会显著影响回测分布,建议先在单一品种单一周期上跑通再外推。
「画得少,看得清」
iDivergence 指标跑起来前,得先备齐通用振荡器那一套文件:Indicators 下的 iUniOsc.mq5 与 iDivergence.mq5,加上 Include/InDiver 里四个类与定义文件、Include/UniOsc 里两个支撑文件,整包 ZIP 体积 13.96 KB。 有用户反馈默认参数 14、14、14 下会漏掉明显背离,把参数列表切到 OscUni_OSMA 并改成 13、26、9 可正常工作;另有人指出默认只标 3 处背离,而市场里至少有 15 种背离/收敛形态,想全抓就得把数目上限调大。 外汇与贵金属波动剧烈、杠杆高风险大,这类指标只作辅助参考,信号漏判或误判都可能发生,实盘前务必在 MT5 策略测试器里用历史数据验证参数组合。 文件齐了、参数调了,图表上反而只留该看的那几根线——少画一点,背离才看得清。