内容
- 概述
- 背离和趋合 (概念定义)
- 判断价格和指标走向的方法
- 利用柱线定义顶部/底部
- 利用阀值定义顶部/底部
- 最大值/最小值, 高于/低于指标中线
- 背离分类系统
- 价格和指标走向的完整系统化
- 三重背离
- 用于定义背离的通用指标
- 选择一款振荡器
- 创建一款新指标
- 应用一款通用振荡器
- 定义振荡器极值
- 极限值定义类
- 利用柱线定义极值
- 利用阀值定义极值
- 通过相对于振荡器中点的位置来定义极值
- 设置背离类型的方法
- 用于检查背离条件的类
- 检查背离条件的准备
- 定义背离
- 指标完成
- 结束语
- 附件
概述
术语 "背离" 来自拉丁语 "divergere" ("检测差异")。背离通常定义为指标读数和价格走势间的差异。反义词是来自拉丁语 "convergo" ("我汇集在一起") 的 "趋合"。有更广泛的背离/趋合分类系统, 包括 "隐藏背离", "扩展背离", A, B 和 C 种类的背离等定义。
在本文中, 我们将首先处理基本术语: 背离和趋合。之然后我们将介绍其它分类方法, 并进行比较分析, 辨别优缺点。最后, 我们将开发自己的分类, 更加完整且没有明显的缺陷, 以及在图表上搜索和显示背离/趋合的通用指标。
背离和趋合 (概念定义)
因此, 背离是指标读数和价格走势的差异。我们还有第二个术语 — 趋合 — 具含义恰好相反。根据这个逻辑, 如果背离是指标读数和价格走势间的差异, 那么趋合意味着协调一致。然而, 情况并非如此, 因为一致性不等同于趋合。
为了令两个术语 — 背离和趋合 — 有一个更精确的含义, 它们需要更狭义的定义。我们来看看价格和指标图表。如果价格上升, 而指标下降时, 我们遇到背离。如果价格下降, 而指标上升时, 我们遇到趋合 (图例. 1)。
图例. 1。价格和指标走势的差异。在左侧, 价格上升,
而指标下行 — 背离。在右侧, 价格下行, 指标上升 — 趋合
我们事实上习惯于将指标置于价格图表的下方, 因此这个定义似乎可以接受。然而, 如果我们扭转指标和价格图表的位置, 一切都会发生根本变化: 背离变为趋合, 反之亦然(图例, 2)。
图例. 2。价格和指标走势的差异。在左侧, 价格上升,
而指标下行 — 趋合。在右侧, 价格下行, 指标上升 — 背离
现在从选择交易方向的角度来看看图例, 1 和图例. 2。假设价格上升, 指标下行, 所以我们决定卖出。按照类比, 当价格下行时, 我们应该卖出, 而指标上升 (图例. 3)。
图例. 3。左侧: 卖出条件, 价格和指标背离。右侧条件
买入 (类似于卖出), 价格和指标趋合
事实证明, 在卖出的情况下, 价格和指标背离, 而在买入的情况下, 它们趋合, 尽管买卖条件相同, 但结果相反。这意味着我们可以称其中一个条件看跌, 而第二个是看涨。这意味着指标低于价格图表的位置不足以定义趋合和背离。
所有已提供的定义都与卖出方向有关, 而买入的情况则相反。但是基于技术分析的最本质, 有一个更简单和更精确的定义版本。如果我们将未来价格走势的推测加到背离和趋合的定义上, 它就变得简单而明晰。
背离 是指标读数和价格走势方向不一致时的价格反转信号
由于这是一个反转信号, 价格应该对于买盘先升, 后降 — 对于卖盘。为了使上述背离出现, 指标应分别下降、上升移动。在这个定义中, 卖出用作标的方向, 而指标应放在价格图表之下。展示在图例. 3 上的背离遵循这个定义。
趋合表现为相反的情况: 现在, 价格应该下降移动, 而指标上升移动。然而, 预估的价格方向并没有改变。因此, 以下趋合定义:
趋合 是指标读数和价格走势方向不一致时的趋势延续信号
由于这是一个延续信号, 价格应该对于卖盘先降, 后升 — 对于买盘。指标应分别上升、下降移动 (图例. 4)。
图例. 4。趋合信号
当然, 有争议的一点是背离是否为反转信号, 趋合是否为延续信号。但这已经是技术分析在实际应用中的可行性问题了。
图例. 5 同时展示背离和趋合信号令您掌握这一术语。
图例. 5。背离和趋合信号
判断价格和指标走向的方法
指标和价格图表线到目前为止都是直线。。但这只是一个抽象, 与实际价格走势无关。因此, 我们来研究能用来定义价格和指标方向并检测背离的方法。那么我们将在实践中看看背离分类系统。
一般来说, 我们首先需要识别图表上的价格或是顶部和底部, 然后比较它们的价值。看涨背离 (买入信号) 由底部检测: 如果一个底部高于前一个底部, 然后指标指向上升, 反之亦然。看跌背离则检测顶部 (卖出信号)。
在图表上有三种检测极值点的方法。
- 利用柱线。
- 利用自最后一个高/低价位的阈值超越。
- 最大/最小值要高于/低于指标中线。
利用柱线检测顶部/底部。我们利用一定数量的顶部/底部柱线。例如, 如果参数为 2, 则顶部柱线上的指标值应该超过左、右侧的两根柱线。有鉴于此, 底部的值应该低于相邻柱线的值 (图例. 6)。
图例. 6。用两根柱线定义顶部和底部。左向顶部定义。有箭头标记的柱线, 它成为已知的
由对勾标记示出的顶部。右向底部定义
左向和右向的顶部/底部所需的柱线数量可能有所不同: 例如, 左向 5 和右向 2 (图例. 7)。
图例. 7。顶部由左边的五根柱线和右边的两根柱线定义
利用阈值定义顶部/底部。当指标上升, 系统跟踪其最大值。在没有新极值的柱线上, 当前值与前一个固定的顶部/底部相比较。如果差值超过外部参数设定的阀值, 则假设指标已改变了方向, 而该根已到达最大值/最小值的柱线可看作顶部/底部 (图例. 8)。
图例. 8。利用阈值定义顶部和底部。阀值 显示 在左上角。
直至柱线 2, 柱线上升, 最大值固定在柱线 2, 而在柱线 5, 数值
已降至阀值, 此即意味着指标改变了其方向。在柱线 6, 指标
再次超越阀值并改变方向。
利用柱线定义是最方便的, 因为它不依赖于指标。与之对比, 阈值则要取决于指标类型。例如, 对于振荡范围 0-100 的 RSI, 阀值可为 5。对于 Momentum, 阀值为 0.1-1, 因为指标在 100 附近略微波动。此外, 这些波动的大小取决于时间帧。这些进一步使阈值的使用复杂化。
最大值/最小值要高于/低于指标中线。此方法比其它方法使用频率更低。它也取决于所应用的指标, 因为并非所有指标的平均值都为 0 (例如, RSI 的平均值为 50)。但其主要缺点是过于滞后 (图例. 9)。
图例. 9。利用中线穿越定义顶部和底部。只有中线在柱线 2 上穿越之后
我们才会确认顶部 1。在柱线 4 穿越后
我们才会确认底部 3
背离分类系统
您可以在网上找到很多有关背离的文章。它们以各种方法描述了不同的术语, 以及将背离和趋合系统化的原则。您可以找到简单, 经典, 隐藏和扩展背离。有些人, 以及作者本人将其分成 A, B 和 C 类。我们不会在本文中分析主要来源。代之, 我们来通览一些已辨识的类型。
经典背离. 这种类型已经在上面描述过并在图例. 5 中示出。
隐藏背离. 隐藏背离与经典的不同之处在于价格变动和指标变动的方向。换言之, 隐含背离与趋合相似。
扩展背离. 到目前为止, 我们只讨论了价格和指标的上升、下降运动。如果我们加上横向运动, 则选项数量增加。尽管我们可以通过组合三个价格走向和三个指标走向来获得多种选择, 但仅提出了一个版本的扩展背离:
- 横向价格走势, 指标向下移动 — 延伸看跌背离 (卖出信号)
- 横向价格走势, 指标上升移动 — 延伸看涨背离 (买入信号)。
种类: A, B, C。А 类是经典的背离, 而 B 和 C 类是扩展的背离版本。
B 类:
- 横向价格走势, 指标向下移动 — 看跌 (卖出信号)。
- 横向价格走势, 指标上升移动 — 看涨 (买入信号)。
С 类:
- 价格上升, 指标顶部处于一个等级 — 看跌 (卖出信号)。
- 价格下降, 指标底部处于一个等级 — 看涨 (买入信号)。
我们可以看到, B 和 С 类是扩展背离的方法。B 类完全重复上述定义。
在浏览有关背离的材料时, 我得出的主要结论是缺乏清晰的术语, 有的版本可能覆盖不完整。因此, 我们将分析各种价格和指标走向组合的选项, 并对其进行系统化。
价格和指标走向的完整系统化
首先, 我们定义两个可能的价格和指标走向。
- 两个运动方向: 上升和下降。
- 三个运动方向: 上升, 下降, 横向。
第一种情况只提供四种可能的组合。以卖出信号为例, 我们来看看。
- 价升, 指标升。
- 价升, 指标降 (背离)。
- 价降, 指标升 (趋合)。
- 价降, 指标降。
现在我们已经处理了方向定义方法, 我们来看这些选项 (图例. 10)。
图例. 10。在两个可用方向的情况下, 价格和指标变动的所有可能组合
在三个方向的情况下, 已经有九种组合。
- 价升, 指标升。
- 价升, 指标横。
- 价升, 指标降 (背离)。
- 价横, 指标升。
- 价横, 指标横。
- 价横, 指标降。
- 价降, 指标升 (趋合)。
- 价降, 指标横。
- 价降, 指标降。
所有这些组合都显示在图例. 11 中。
图例. 11。在三个可用方向的情况下, 价格和指标变动的所有可能组合
如果您创建一款可以选择任意已研究选项的指标, 之后选择您认为是正确的背离, 趋合, 隐藏或扩展背离。换言之, 我们得到一款通用的指标, 即便对于不认同系统化和本文中所做定义的那些人也是有用的。
三重背离
截至目前, 价格和指标走向是由两个点来判定: 上升、下降。我们也许要增加第三点来提升可能的价格和指标走势选项的数量。总共有九个选项:
- 升, 升。
- 升, 横。
- 升, 降。
- 横, 升。
- 横, 横。
- 横, 降。
- 降, 升。
- 降, 横。
- 降, 降。
在这种情况下, 更正确的说法是走势而非方向。在图例. 12 中显示了有三个顶部的走势类型。
图例. 12。基于三个顶部的各种可能走势
在图例. 13 中显示相应的基于底部的走势。
图例. 13。各种基于三个底部的可能走势
通过将 9 种类型的价格走势与 9 种指标走势组合起来, 我们可以获得 81 种变化的三重背离。
因此, 您可以通过任意数量的点确定走势。如果我们加入第四点, 我们将有 81 个变化的价格或指标走势和 6561 (81 * 81) 个可能的组合版本。当然, 还有更多可能的选项, 但好似少见。也许, 应用第四点没有任何意义, 但在本文中的指标对于定义走势类型的点数没有限制。
用于定义背离的通用指标
现在我们已经处理了这个理论, 我们来开发这个指标。
选择一款振荡器。 为了不受单一振荡器定义背离的限制, 我们将使用 本文 中描述的通用振荡器。附件: iUniOsc (通用振荡器) 和 iUniOscGUI (与图形界面相同的振荡器)。我们将使用基础版 — iUniOsc。
创建一款新指标。我们在 MetaEditor 中创建新的 iDivergence 指标。我们会用到 OnCalculate 函数。OnTimer() 函数不是必需的。勾选 "单独窗口中的指标" 选项。我们创建三个缓冲区: 一个显示振荡器曲线, 以及两个出现背离时绘制箭头的缓冲区。在编辑器中打开一个新文件后, 更改缓冲区名称: 1 — buf_osc, 2 — buf_buy, 3 — buf_sell。应该在数组声明的地方以及OnInit() 函数中更改名称。我们还可以调整缓冲区属性: indicator_label1, indicator_label2, indicator_label3 — 将鼠标悬停在曲线或指标标签上以及数据窗口中时, 这些属性的值将显示在工具提示中。我们称呼它们为 "osc", "buy" 和 "sell"。
应用通用振荡器。将 iUniOsc 指标的所有外部参数插入新指标。属性窗口中不需要 ColorLine1, ColorLine2 和 ColorHisto 参数。我们将它们隐藏。Type 参数含有 UniOsc/UniOscDefines.mqh 文件中描述的自定义 OscUni_RSI 类型。我们要包含这个文件。默认情况下, Type 参数值设置为 OscUni_ATR — ATR 指标。但 ATR 不依赖于价格走势的方向, 这意味着它不适合定义背离。所以, 设置 OscUni_RSI — RSI 指标 — 作为默认:
#include <UniOsc/UniOscDefines.mqh> input EOscUniType Type = OscUni_RSI; input int Period1 = 14; input int Period2 = 14; input int Period3 = 14; input ENUM_MA_METHOD MaMethod = MODE_EMA; input ENUM_APPLIED_PRICE Price = PRICE_CLOSE; input ENUM_APPLIED_VOLUME Volume = VOLUME_TICK; input ENUM_STO_PRICE StPrice = STO_LOWHIGH; color ColorLine1 = clrLightSeaGreen; color ColorLine2 = clrRed; color ColorHisto = clrGray;
将通用振荡器的句柄声明在外部变量下方:
int h;
在 OnInit() 函数的开头下载通用振荡器:
h=iCustom(Symbol(),Period(),"iUniOsc", Type, Period1, Period2, Period3, MaMethod, Price, Volume, StPrice, ColorLine1, ColorLine2, ColorHisto); if(h==INVALID_HANDLE){ Alert("不能加载指标"); return(INIT_FAILED); }
在 OnCalculate() 函数中, 将通用振荡器的数据复制到 buf_osc 缓冲区:
int cnt; if(prev_calculated==0){ cnt=rates_total; } else{ cnt=rates_total-prev_calculated+1; } if(CopyBuffer(h,0,0,cnt,buf_osc)<=0){ return(0); }
在这个阶段, 我们可通过将 iDivergence 指标挂载到图表来验证所执行操作的正确性。如果一切正常, 您可以在子窗口中看到振荡器的曲线。
定义振荡器极值。我们已经研究过三种定义极值的方法。我们将它们包括在指标中, 并提供选择其中任何一个的可能性 (外部变量带有一个下拉列表)。在 Include 文件夹中, 我们创建了 UniDiver 文件夹, 所有代码所需的其它文件将位于其中。创建 UniDiver/UniDiverDefines.mqh 包含文件并在其中编写 EExtrType 枚举:
enum EExtrType{
ExtrBars,
ExtrThreshold,
ExtrMiddle
};
枚举选项:
- ExtrBars — 利用柱线。
- ExtrThreshold — 利用自最后一个高/低价位的阈值超越;
- ExtrMiddle — 指标最大值或最小值是否高于或低于其中线。
在指标中, 创建 ExtremumType 外部参数并将其插入所有其它外部参数之上。当利用柱线定义极值时, 我们将需要两个参数 — 极值左侧和右侧的柱线数量, 而在利用阈值定义时, 我们需要用于计算阈值的参数:
input EExtrType ExtremumType = ExtrBars; // 极值类型 input int LeftBars = 2; // ExtrBars 左侧柱线数量 input int RightBars = -1; // ExtrBars 右侧柱线数量 input double MinMaxThreshold = 5; // ExtrThreshold 的阀值
我们来实现使用一个参数 RightBars 或 LeftBars, 以及同时使用两个参数的可能性。RightBars 默认等于 -1。这意味着不会用到这个值, 第二个参数的值降会分配给它。
极值定义类。在指标工作期间不需要改变极值定义方法, 因此使用 OOP 而不是 'if' 和 'switch' 操作符是更合理的。创建基类并派生三个定义极值方法的类。在启动指标时选择其中一个派生类。这些类将用于定义极值, 并执行所有必要的操作来发现背离。它们在定义极值的方式上是不同的, 而在所有情况下, 趋合的定义完全相同。因此, 背离定义函数位于基类中, 并从派生类中调用。但首先, 我们需要提供对所有指标极值的便捷访问 (就像在 "沃尔夫波浪" 一文中之字折线做到的那样)。
SExtremum 结构用于存储一个极值的的有关数据。结构描述位于 UniDiverDefines:
struct SExtremum{ int SignalBar; int ExtremumBar; datetime ExtremumTime; double IndicatorValue; double PriceValue; };
结构字段:
- SignalBar — 柱线, 形成的极值在其上确认
- ExtremumBar — 极值柱线
- ExtremumTime — 极值柱线的时间
- IndicatorValue — 极值点的指标值
- PriceValue — 指标极值柱线的价格
这些结构的两个数组将用于存储关于所有顶部和底部的数据。它们将成为基类的成员。
用于定义极值的类位于 UniDiver/CUniDiverExtremums.mqh 文件中, 基类名称为 CDiverBase。现在, 我们来研究只有基本方法的类结构。其余的将在必要时添加。
class CDiverBase{ protected: SExtremum m_upper[]; SExtremum m_lower[]; void AddExtremum( SExtremum & a[], int & cnt, double iv, double pv, int mb, int sb, datetime et); void CheckDiver( int i, int ucnt, int lcnt, const datetime & time[], const double &high[], const double &low[], double & buy[], double & sell[], double & osc[] ); public: virtual void Calculate( const int rates_total, const int prev_calculated, const datetime &time[], const double &high[], const double &low[], double & osc[], double & buy[], double & sell[] ); };
Calculate() 虚方法允许您选择极限值定义选项。AddExtremum() 和 CheckDiver() 方法位于 'protected' 部分 — 它们会从派生类的 Calculate() 方法中调用。AddExtremum() 方法收集有关顶部和底部的数据到 m_upper[] 和 m_lower[] 数组。CheckDiver() 方法检查是否满足背离条件并设置指示箭头。下面我们更详细地研究所有这些方法, 但是现在让我们熟悉派生类的其它定义极值的方法。
利用柱线定义极值。利用柱线定义极限值的类:
class CDiverBars:public CDiverBase{ private: SPseudoBuffers1 Cur; SPseudoBuffers1 Pre; int m_left,m_right,m_start,m_period; public: void CDiverBars(int Left,int Right); void Calculate( const int rates_total, const int prev_calculated, const datetime &time[], const double &high[], const double &low[], double & osc[], double & buy[], double & sell[] ); };
用于定义极值的参数 (LeftBars 和 RightBars 外部变量) 传递给类构造函数, 如果需要, 它们的值将会被验证和修改, 并计算其它参数:
void CDiverBars(int Left,int Right){ m_left=Left; m_right=Right; if(m_left<1)m_left=m_right; // Left 参数未设置 if(m_right<1)m_right=m_left; // Right 参数未设置 if(m_left<1 && m_right<1){ // 两个参数均未设置 m_left=2; m_right=2; } m_start=m_left+m_right; // 平移间隔起始点 m_period=m_start+1; // 间隔柱线数 }
首先验证参数值。如果其中某些非正数 (未设置), 则会分配第二个参数的值。如果是单个参数未设置, 则会为它们分配默认值 (2)。此后计算用于搜索极值 (m_start) 的初始柱线缩进和极值柱线总数 (m_period)。
Calculate() 方法与标准的 OnCalculate() 函数相同, 但它只接收所需的价格数组, 而非全部: time[], high[], low[] 和 osc[] 指标缓冲区 (振荡器数据), buy[] 和 sell[] (箭头)。如同以往, 已计算的柱线范围在 OnCalculte() 函数中定义。指标极值 (ArrayMaximum() 和 ArrayMinimum() 函数) 在标准指标循环之后定义。在检测到极点后, 调用 AddExtremum() 方法将数据添加到 m_upper[ ]或 m_lower[] 数组中。最后, 调用 CheckDiver() 方法来分析含有极值的数组数据。如果检测到背离, 则放置箭头。
void Calculate( const int rates_total, const int prev_calculated, const datetime &time[], const double &high[], const double &low[], double & osc[], double & buy[], double & sell[] ){ int start; // 初始索引变量 if(prev_calculated==0){ // 完全指标计算 start=m_period; // 定义初始计算柱线 m_LastTime=0; // 为了定义新柱线而重置变量 Cur.Reset(); // 重置辅助结构 Pre.Reset(); // 重置辅助结构 } else{ // 仅当新柱线计算 start=prev_calculated-1; // 计算柱线索引, 从而恢复计算 } for(int i=start;i<rates_total;i++){ // 指标主要循环 if(time[i]>m_LastTime){ // 新柱线 m_LastTime=time[i]; Pre=Cur; } else{ // 重计算相同柱线 Cur=Pre; } // 计算最高/最低搜索参数 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]); } // 检查背离 this.CheckDiver(i,Cur.UpperCnt,Cur.LowerCnt,time,high,low,buy,sell,osc); } }
我们来详细研究这段代码。在指标循环的开始:
if(time[i]>m_LastTime){ // 新柱线 m_LastTime=time[i]; Pre=Cur; } else{ // 重计算相同柱线 Cur=Pre; }
m_LastTime 变量已在基类中声明。如果 time[i] 柱线的时间超过变量, 则为首次计算柱线。将柱线时间分配给 m_LastTime 变量, 而将 Cur 变量值分配给 Pre 变量。相反, 当重新计算相同的柱线时, 将 Pre 变量分配给 Cur。在 这篇文章 中详细讨论了使用 Cur 和 Pre 变量。 Pre 和 Cur 变量是 UniDiverDefines 文件中描述的SPseudoBuffers1 类型:
struct SPseudoBuffers1{ int UpperCnt; int LowerCnt; void Reset(){ UpperCnt=0; LowerCnt=0; } };
结构包含两个字段:
- UpperCount — 使用的 m_upper[] 数组元素数量;
- LowerCount — 使用的 m_lower[] 数组元素数量;
为了快速重置所有结构字段, 创建了 Reset() 方法。
在使用了 Cur 和 Pre 变量后, 计算搜索极值的柱线索引:
// 计算搜索最高/最低值的参数 int sb=i-m_start; // 柱线索引, 间隔从此处开始 int mb=i-m_right; // 顶部/底部的柱线索引
将搜索最高/最低开始的柱线索引分配给 "sb" 变量。将最高/最低值的柱线索引分配给 'mb' 变量。
使用 ArrayMaximum() 和 ArrayMinimum() 函数定义顶部或底部:
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]); }
如果 ArrayMaximum() 或 ArrayMinimum() 函数返回 'mb', 这意味着指定数量的柱线位于顶部/底部的左侧和右侧。反过来, 这表明所需的顶部/底部已经形成。调用 AddExtremum() 方法, 并将数据添加到 m_upper[] 或 m_lower[] 数组中。
我们来看一下简单的 AddExtremum() 方法:
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++; // 增加已占用的数组元素数量 }
要添加新数据的 a[] 数组, 通过参数传递给方法。可以是 m_upper[] 或 m_lower[] 数组。通过 'cnt' 变量传递 a[] 数组被占用元素的数量。可以是 Cur.UpperCnt 或 Cur.LowerCnt 变量。数组 a[] 和 'cnt' 变量由引用传递, 因为它们要在方法中被更改。
iv — 基于极值的指标值, pv — 基于极值的价格, mb — 极值所在柱线的价格, sb — 信号柱线 (即确认极值到达的柱线), et — 极值所在柱线的时间。
在 AddExtremum() 方法的开始处检查数组大小。如果它已满, 可增加其大小, 直到 1024 个元素。数据添加之后 'cnt' 变量递增。
稍后我们将查看 CheckDiver() 方法。
利用阈值定义极值。
利用阈值定义的类不同于利用柱线定义的类, 主要在于 Cur 和 Pre 变量类型: 这是 UniDiverDefines.mqh 文件中描述的 SPseudoBuffers2 类型:
struct SPseudoBuffers2{ int UpperCnt; int LowerCnt; double MinMaxVal; int MinMaxBar; int Trend; void Reset(){ UpperCnt=0; LowerCnt=0; MinMaxVal=0; MinMaxBar=0; Trend=1; } };
SPseudoBuffers2 结构就像 SPseudoBuffers1 一样有相同的字段, 加上少量额外字段:
- MinMaxVal — 指标最大或最小值的变量
- MinMaxBar — 发现指标最大或最小值的柱线索引变量
- Trend — 指标走向变量。值 1 表示指标向上移动并跟踪其最大值。在 -1 的情况下, 跟踪最小值。
阈值的外部参数 — MinMaxThreshold 变量 — 传递到类的构造函数。它的值被保存在 "私有" 部分中声明的 m_threshold 变量当中。
该类的 Calculate() 方法在极值定义方法中有所不同:
switch(Cur.Trend){ // 指标当前方向 case 1: // 上升 if(osc[i]>Cur.MinMaxVal){ // 新的最大值 Cur.MinMaxVal=osc[i]; Cur.MinMaxBar=i; } if(osc[i]<Cur.MinMaxVal-m_threshold){ // 超过阀值 // 加入到数组 this.AddExtremum(m_upper,Cur.UpperCnt,Cur.MinMaxVal,high[Cur.MinMaxBar],Cur.MinMaxBar,i,time[Cur.MinMaxBar]); Cur.Trend=-1; // 改变跟踪方向 Cur.MinMaxVal=osc[i]; // 初始化最小值 Cur.MinMaxBar=i; // 初始化最小值的柱线 } break; case -1: // 下降 if(osc[i]<Cur.MinMaxVal){ // 新的最小值 Cur.MinMaxVal=osc[i]; Cur.MinMaxBar=i; } if(osc[i]>Cur.MinMaxVal+m_threshold){ // 超过阀值 // 将底部加入数组 this.AddExtremum(m_lower,Cur.LowerCnt,Cur.MinMaxVal,low[Cur.MinMaxBar],Cur.MinMaxBar,i,time[Cur.MinMaxBar]); Cur.Trend=1; // 改变跟踪方向 Cur.MinMaxVal=osc[i]; // 初始化最大值 Cur.MinMaxBar=i; // 初始化最大值的柱线 } break; }
如果 Cur.Trend 变量为 1, 则将振荡器值与 Curve.MiniMaxValue 值进行比较。如果新的振荡器值超过变量的值, 则更新变量值。将检测到新的最高值所在柱线索引被分配给 Cur.MinMaxBar 变量。还要确保振荡器值自最后已知的最大值尚未减去 m_threshold 值。如果已经减掉, 振荡器就改变了方向。调用 AddExtremum() 方法, 将新的极值上的数据保存在数组中, Cur.Trend 值被替换为相反的值, 而新的最小值的初始参数在 Cur.MinMaxVal 和 Cur.MinMaxBar 变量中设置。由于 Cur.Trend 变量的值已更改, 因此另一个 "case" 部分 — 跟踪最小振荡器值以及上穿阈值 — 从现在开始执行。
利用相对于振荡器中点的位置来定义极值。所应用的振荡器的类型被传递给类构造器。根据该类型, 定义振荡器中间值:
void CDiverMiddle(EOscUniType type){ if(type==OscUni_Momentum){ m_level=100.0; } else if(type==OscUni_RSI || type==OscUni_Stochastic){ m_level=50.0; } else if(type==OscUni_WPR){ m_level=-50.0; } else{ m_level=0.0; } }
对于 Momentum, 值为 100, 对于 RSI 和 Stochastic, 是 50, 对于 WPR, 是 -50, 对于其它振荡器, 是 0。
确定极值的方法在许多方面与阈值法相似:
switch(Cur.Trend){ case 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=-1; Cur.MinMaxVal=osc[i]; Cur.MinMaxBar=i; } break; case -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=1; Cur.MinMaxVal=osc[i]; Cur.MinMaxBar=i; } break; }
唯一的区别是比较振荡器的中值来完成方向改变: osc[i]<m_level 或 osc[i]>m_level。
设置背离类型的方法。在外部参数里加入用来选择所辨识的背离类型的变量:
input int Number = 3;
默认参数值为 3。根据图例, 11, 这意味着经典的背离。图例. 11 中显示了价格和指标走势的 9 种组合。我们加入另一个选项 — "not checked"。现在, 我们有 10 个组合。因此, 使用通常的十进制数, 我们可以描述任意数量 (包括三重背离) 的不同走势的任何组合。事实证明, 一位数字对应于简单的背离 (两个顶部/底部), 两位数字是三重数量, 依此类推。例如, 13 的数量对应于图例. 14 所示的组合。
图例. 14。指标与价格走势的组合
当 Number = 13 时卖出
用于检查背离条件的类。创建基类和用于检查背离条件的派生类。Number 参数值将在指标启动时进行分析。最重要的属性是它的长度, 因为它影响到指向类的指针的数组大小。然后, 根据条件类型为每个数组元素生成相应的对象。
条件验证类位于 UniDiver/CUniDiverConditions.mqh 文件中。基类被命名为 CDiverConditionsBase。我们来看看它:
class CDiverConditionsBase{ protected: double m_pt; double m_it; public: void SetParameters(double pt,double it){ m_pt=pt; m_it=it; } virtual bool CheckBuy(double i1,double p1,double i2,double p2){ return(false); } virtual bool CheckSell(double i1,double p1,double i2,double p2){ return(false); } };
该类有两个虚方法来比较两个相邻顶部。需要传递给它的参数:
- i1 — 点 1 处的指标值
- p1 — 点 1 处的价格
- i2 — 点 2 处的指标值
- p2 — 点 2 处的价格
点的计数是从右到左, 从一开始。
SetParameters() 方法用于设置附加比较参数: pt — 可用的价格差价, 假定价格点位于单一等级。it — 相似比较指标顶部的参数。这些参数的值通过属性窗口进行设置:
input double IndLevel = 0; input int PriceLevel = 0;
其中一个派生类的代码如下所示:
class CDiverConditions1:public CDiverConditionsBase{ private: public: bool CheckBuy(double i1,double p1,double i2,double p2){ return((p1>p2+m_pt) && (i1>i2+m_it)); } bool CheckSell(double i1,double p1,double i2,double p2){ return((p1<p2-m_pt) && (i1<i2-m_it)); } };
在 CheckBuy() 方法中, 检查点 1 的价格是否超过点 2。指标也同样如此: 点 1 处价格应该超过点 2 的数值。CheckSell() 方法与 CheckBuy() 方法相互镜像对称。所有其它类均相似的, 仅在逻辑表达式中不同, 但 CDiverConditions0 除外。CheckSell() 和 CheckBuy() 方法在类中返回 'true'。当条件检查被禁用时 (任何选项都是可能的) 使用它。
准备检查背离条件。数组和其大小的变量在 CDiverBase 类的 "protected" 部分中声明:
CDiverConditionsBase * m_conditions[];
int m_ccnt;
在 SetConditions() 方法中, 更改 m_conditions 数组大小, 并创建背离条件验证对象:
void SetConditions(int num, // 背离数量 double pt, // PriceLevel 参数 double it){ // IndLevel 参数 if(num<1)num=1; // 背离数量不应小于 1 ArrayResize(m_conditions,10); // 最大可能的条件数量 m_ccnt=0; // 条件数量的实际计数值 while(num>0){ int cn=num%10; // 新极值对之间的差值变化 m_conditions[m_ccnt]=CreateConditions(cn); // 创建一个对象 m_conditions[m_ccnt].SetParameters(pt,it); // 设置条件验证参数 num=num/10; // 移到下一个条件 m_ccnt++; // 条件数量计数 } // 根据实际的条件数量调整数组大小 ArrayResize(m_conditions,m_ccnt); }
以下参数传递给该方法:
- num — Number 外部参数;
- pt — PriceLevel 外部参数;
- it — IndLevel 外部参数。
首先要检查 'num' 参数:
if(num<1)num=1; // 背离数量不应小于 1
然后, m_conditions 数组增加到最大可能的大小 (10 — "int" 变量的最大值长度)。之后, 通过 CreateConditions() 方法进行条件验证的对象被创建, 并根据 "num" 的每个数字的值, 在 'while' 循环中使用 SetParameters() 设置参数。循环后, 数组大小根据应用条件的实际数量而变化。
我们来看一下 CreateConditions() 方法:
CDiverConditionsBase * CreateConditions(int i){ switch(i){ case 0: return(new CDiverConditions0()); break; case 1: return(new CDiverConditions1()); break; case 2: return(new CDiverConditions2()); break; case 3: return(new CDiverConditions3()); break; case 4: return(new CDiverConditions4()); break; case 5: return(new CDiverConditions5()); break; case 6: return(new CDiverConditions6()); break; case 7: return(new CDiverConditions7()); break; case 8: return(new CDiverConditions8()); break; case 9: return(new CDiverConditions9()); break; } return(new CDiverConditions0()); }
该方法很简单: 根据i参数, 创建相应的对象, 并返回它的引用。
定义背离。现在, 我们可以考虑 CDiverBase 类的 CheckDivergence() 方法。首先, 我们研究所有方法的代码, 然后逐一审查:
void CheckDiver( int i, // 计算柱线索引 int ucnt, // m_upper 数组中的顶部数量 int lcnt, // m_lower 数组中的顶部数量 const datetime & time[], // 柱线时间数组 const double &high[], // 最高柱线的数组 const double &low[], // 最低柱线的数组 double & buy[], // 指标的上升箭头缓冲区 double & sell[], // 指标的下降箭头缓冲区 double & osc[] // 指标的振荡器数值缓冲区 ){ // 清除箭头缓冲区 buy[i]=EMPTY_VALUE; sell[i]=EMPTY_VALUE; // 删除图形对象 this.DelObjects(time[i]); // 对于顶部 (卖出信号) if(ucnt>m_ccnt){ // 顶部数量足够 if(m_upper[ucnt-1].SignalBar==i){ // 已计算柱线上检测到一个顶部 bool check=true; // 假设背离已经发生 for(int j=0;j<m_ccnt;j++){ // 所有顶部 // 验证新的一对顶部符合条件 bool result=m_conditions[j].CheckSell( m_upper[ucnt-1-j].IndicatorValue, m_upper[ucnt-1-j].PriceValue, m_upper[ucnt-1-j-1].IndicatorValue, m_upper[ucnt-1-j-1].PriceValue ); if(!result){ // 不满足条件 check=false; // 没有背离 break; } } if(check){ // 背离已经发生 // 设置指标箭头缓冲区 sell[i]=osc[i]; // 绘制价格图表的附加曲线 和/或 箭头 this.DrawSellObjects(time[i],high[i],ucnt); } } } // 对于底部 (买入信号) if(lcnt>m_ccnt){ if(m_lower[lcnt-1].SignalBar==i){ bool check=true; for(int j=0;j<m_ccnt;j++){ bool result=m_conditions[j].CheckBuy( m_lower[lcnt-1-j].IndicatorValue, m_lower[lcnt-1-j].PriceValue, m_lower[lcnt-2-j].IndicatorValue, m_lower[lcnt-2-j].PriceValue ); if(!result){ check=false; break; } } if(check){ buy[i]=osc[i]; this.DrawBuyObjects(time[i],low[i],lcnt); } } } }
以下参数传递给该方法:
- i — 当前计算柱线的索引;
- ucnt — 应用的 m_upper[] 数组元素的数量;
- lcnt — 应用的 m_lower[] 数组元素的数量;
- time[] — 柱线时间数组;
- high[] — 最高价柱线数组;
- low[] — 最低价柱线数组;
- buy[] — 指标买入箭头缓冲区;
- sell[] — 指标卖出箭头缓冲区;
- osc[] — 指标振荡器值的缓冲区。
箭头缓冲区首先被清除:
// 清除箭头缓冲区 buy[i]=EMPTY_VALUE; sell[i]=EMPTY_VALUE;
已计算柱线对应的图形对象被删除:
// 删除图形对象 this.DelObjects(time[i]);
除了箭头, iDivergence 应用图形对象在价格图上显示箭头, 并用线条将价格图表上的极值连接到振荡器图形上的顶部/底部。
然后有两段相同的代码段来检查卖出和买入的条件。我们来查看第一段卖出。可用振荡器顶部的数量应该大于 1 个检查条件数量。因此, 执行检查:
// 对于顶部 (卖出信号) if(ucnt>m_ccnt){ // 有足够数量的顶部 }
之后, 我们检查已计算柱线上的顶部是否存在。这取决于已计算柱线的索引与来自顶部/底部数组的索引之间的对应关系:
if(m_upper[ucnt-1].SignalBar==i){ // 在已计算柱线上检测到的顶部 }
检查条件的结果需要辅助变量:
bool check=true; // 假设背离发生
在 'for' 循环中, 遍历顶部传递数据所有条件:
for(int j=0;j<m_ccnt;j++){ // 遍历所有顶部对 // 在下一对顶部检查符合条件 bool result=m_conditions[j].CheckSell( m_upper[ucnt-1-j].IndicatorValue, m_upper[ucnt-1-j].PriceValue, m_upper[ucnt-1-j-1].IndicatorValue, m_upper[ucnt-1-j-1].PriceValue ); if(!result){ // 不满足条件 check=false; // 没有背离 break; } }
如果任何条件未达成, 退出循环。分配给 'check' 变量 'false'。如果满足所有条件, 'check' 保存 "true", 将设置一个箭头并创建一个图形对象:
if(check){ // 背离发生 // 放置指标缓冲区箭头 sell[i]=osc[i]; // 在价格图表上绘制辅助线和/或箭头 this.DrawSellObjects(time[i],high[i],ucnt); }
可在指标属性窗口中启用/禁用显示图形对象。为此声明了以下变量:
input bool ArrowsOnChart = true; input bool DrawLines = true; input color ColBuy = clrAqua; input color ColSell = clrDeepPink;
- ArrowsOnChart — 在价格图表上启用箭头图形对象
- DrawLines — 启用绘制连接价格和指标最高和最低值的线条
- ColBuy and ColSell — 买卖信号的图形对象颜色。
在 CDiverBase 类的 "protected" 部分, 声明相应的变量:
bool m_arrows; // 对应于 ArrowsOnChart 变量 bool m_lines; // 对应于 DrawLines 变量 color m_cbuy; // 对应于 ColBuy 变量 color m_csell; // 对应于 ColSell 变量
这些变量的值在 SetDrawParmeters() 方法中设置:
void SetDrawParmeters(bool arrows,bool lines,color cbuy,color csell){ m_arrows=arrows; m_lines=lines; m_cbuy=cbuy; m_csell=csell; }
研究使用图形对象的方法。切除:
void DelObjects(datetime bartime){ // 已启用绘制线条 if(m_lines){ // 形成共同的前缀 string pref=MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString((long)bartime)+"_"; for(int j=0;j<m_ccnt;j++){ // 依据背离条件数量 ObjectDelete(0,pref+"bp_"+IntegerToString(j)); // 买入信号的情况下, 价格图上的线条 ObjectDelete(0,pref+"bi_"+IntegerToString(j)); // 买入信号的情况下, 指标图形上的线条 ObjectDelete(0,pref+"sp_"+IntegerToString(j)); // 卖出信号的情况下, 价格图上的线条 ObjectDelete(0,pref+"si_"+IntegerToString(j)); // 卖出信号的情况下, 指标图形上的线条 } } if(m_arrows){ // 启用价格图表上的箭头 // ObjectDelete(0,MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString((long)bartime)+"_ba"); // ObjectDelete(0,MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString((long)bartime)+"_sa"); } }
连接顶部/底部和箭头的线条分别移除。所有图形对象的名称都以指标名称开始, 后跟检测到的背离柱线时间。在与 m_ccnt 条件数相对应的循环中进一步形成名称。"bp" 在价格图表上显示买入信号, "bi" 在指标图形上显示买入信号。与此类似, "sp" 和 "ip" 是用于卖出信号。索引 j 要添加到名称末尾。"_ba" (买入信号箭头) 或是 "_sa" (卖出信号箭头) 要添入到箭头名称。
在 DrawSellObjects() 和 DrawBuyObjects() 方法中执行图形对象的创建。我们来研究其中之一:
void DrawSellObjects(datetime bartime,double arprice,int ucnt){ if(m_lines){ // 启用显示线条 // 形成共同的前缀 string pref=MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString((long)bartime)+"_"; for(int j=0;j<m_ccnt;j++){ // 依据所有背离条件 // 价格图表上的线条 fObjTrend( pref+"sp_"+IntegerToString(j), m_upper[ucnt-1-j].ExtremumTime, m_upper[ucnt-1-j].PriceValue, m_upper[ucnt-2-j].ExtremumTime, m_upper[ucnt-2-j].PriceValue, m_csell); // 指标图形上的线条 fObjTrend( pref+"si_"+IntegerToString(j), m_upper[ucnt-1-j].ExtremumTime, m_upper[ucnt-1-j].IndicatorValue, m_upper[ucnt-2-j].ExtremumTime, m_upper[ucnt-2-j].IndicatorValue, m_csell, ChartWindowFind(0,MQLInfoString(MQL_PROGRAM_NAME))); } } if(m_arrows){ // 已启用价格图表上的箭头 fObjArrow(MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString((long)bartime)+"_sa", bartime, arprice, 234, m_csell, ANCHOR_LOWER); } }
对象名称的形式与删除时的方式相同。之后, 使用 fObjTrend() 和 fObjArrow() 函数创建图形对象。它们位于 UniDiver/UniDiverGObjects.mqh 包含文件中。函数相当简单, 没有任何意义分析它们。
指标完成。我们只需要在指标中应用创建的类。根据所选定义极限值的类型, 在 OnInit() 函数中创建相应的对象:
switch(ExtremumType){ case ExtrBars: diver=new CDiverBars(LeftBars,RightBars); break; case ExtrThreshold: diver=new CDiverThreshold(MinMaxThreshold); break; case ExtrMiddle: diver=new CDiverMiddle(Type); break; }
外部参数之一 PriceLevel - 以点为单位进行测量, 因此很容易根据报价的小数位调整它。为此, 声明另一个变量可以关闭此调整:
input bool Auto5Digits = true;
接下来, 声明一个辅助变量以便修正参数, 并在 OnInit() 函数中进行调整:
int pl=PriceLevel; if(Auto5Digits && (Digits()==5 || Digits()==3)){ pl*=10; }
设置背离和显示参数:
diver.SetConditions(Number,Point()*pl,IndLevel);
diver.SetDrawParmeters(ArrowsOnChart,DrawLines,ColBuy,ColSell);
OnCalculate() 函数中的若干字符串仍然存在。调用基本的 Calculate() 方法:
diver.Calculate( rates_total, prev_calculated, time, high, low, buf_osc, buf_buy, buf_sell);
在应用图形对象的情况下, 我们应该加速绘图:
if(ArrowsOnChart || DrawLines){ ChartRedraw(); }
当指标完成操作时, 应删除图形对象。这在 CDiverBase 类的析构函数中完成。背离条件验证对象也被删除:
void ~CDiverBase(){ for(int i=0;i<ArraySize(m_conditions);i++){ // 所有条件 if(CheckPointer(m_conditions[i])==POINTER_DYNAMIC){ delete(m_conditions[i]); // 删除对象 } } // 删除图形对象 ObjectsDeleteAll(0,MQLInfoString(MQL_PROGRAM_NAME)); ChartRedraw(); }
在此, 指标开发的主要阶段已经结束。图例. 15 显示加载了指标 (在子窗口中) 的图表, 启用了在价格图表上显示箭头并在顶部之间绘制线条。
图例. 15。价格图表上的背离指标, 在价格图表上显示箭头以及连接极点的线条
现在, 我们只需要添加警报功能。它很简单, 已经在其它文章中叙述过了。在下面的附件中, 您可以找到一个带有警报功能的现成指标, 以及指标所需的全部文件。
结束语
尽管指标具有多功能性, 但我们也要注意到其缺点。主要缺点是 IndLevel 参数依赖于应用振荡器的类型, 以及 PriceLevel 参数对时间帧的依赖性。为了排除这种依赖性, 这些参数的默认值为 0。但与此同时, 价格和指标走势的某些组合条件几乎不可能实现。如果在背离验证中包含横向移动的检查, 则其实现是不可能的。在这种情况下, 背离选项 1, 3, 7 和 9 仍然存在。仅当使用测试器来优化使用指标的 EA 时这也许是个问题。
在调整相应优化方法的情况下, 这不是一个问题, 因为通常在单一品种和时间帧上执行优化。首先, 我们需要确定应用的品种和时间帧, 并为 PriceLevel 参数设置适当的值。然后. 我们需要选择所应用的振荡器, 并为 IndLevel 参数设置适当的值。由于除了它们之外还有许多其它优化参数, 所以无法自动优化应用的振荡器类型和 PriceLevel 和 IndLevel 参数值。首先, 是背离类型 (数字变量) 和振荡器周期。
附件
为了 iDivergence 的操作, 您需要一款来自文章 "带有图形界面的通用振荡器" 的通用振荡器。振荡器和所有必需的文件可以在应用程序中找到。
所有附加文件:
- Include/InDiver/CUniDiverConditions.mqh — 用于检查背离条件的类文件;
- Include/InDiver/CUniDiverExtremums.mqh — 用于定义极值的类文件;
- Include/InDiver/UniDiverDefines.mqh — 结构和枚举描述;
- Include/InDiver/UniDiverGObjects.mqh — 用于处理图形对象的函数;
- Indicators/iDivergence.mq5 — 指标;
- Indicators/iUniOsc.mq5 — 通用振荡器;
- Include/UniOsc/CUniOsc.mqh — 通用振荡器类文件;
- Include/UniOsc/UniOscDefines.mqh — 通用振荡器的结构和枚举说明。