内容
概述
检测沃尔夫波形的规则
选择使用的之字折线
收集关于之字折线峰值的数据
一点几何
检测波形
绘制波形和目标
删除图形对象
警报功能
专家交易系统
更多来自比尔·沃尔夫所著书籍的提示
结论
附件
概述
沃尔夫波形 (Wolfe W*e) 是 Bill Wolfe 发现并描述的图形分析形态。此图案看起来像一个三角形或楔形 (沃尔夫称之为 '上升的楔子'), 并具有一些特殊的细微差别。比尔·沃尔夫 (Bill Wolfe) 提出的图形化方法可以检测到一种形态, 根据此形态可以找到入场的时刻和方向, 并且还有益于预测价格应达到的目标, 以及达到目标的时间。
在本文中, 我们将详细研究沃尔夫波形的检测和解释规则。我们将根据 通用之字折线 文章中的之字折线指标, 创建自动检测并显示波形的指标。我们还将根据结果指标创建一个简单的专家交易系统。此 EA 将允许我们测试指标绩效, 并得到比尔·沃尔夫所提出的图形分析的第一印象, 之后会在本文中讨论。
检测沃尔夫波形的规则
我们来研究买入示例中的沃尔夫波形 (图例.1)。价格形成两个连续下降的低峰 (蓝线, 点 1 和 3), 以及两个连续下降的高峰 (点 2 和 4)。在点 4 逆转并形成高峰之后, 价格继续下滑。一旦价格触及点 1 — 点 3 的延长线, 就进行买入操作 (点 5)。
图例. 1. 买入沃尔夫波形。蓝线是价格, 红线是形成的检测目标。在点 5 执行入场, 目标是点 7
1—3 和 2—4 延长线的交汇点 6 则为检测目标的到达时间。目标价位 (点 7) 的定义是 1—4 延长线与通过点 6 绘制的垂直线的交点。此方法不提供止损计算算法, 通常建议是您自行决定使用止损。以上是沃尔夫书中讲述的波形检测规则。
当开发本文的指标时, 还会发现更多的规则。
点 3 必须远低于点 1, 以下条件应予以检查:
v3<v1-d1
此处:
v3 — 点 3 的价位;
v1 — 点 1 的价位;
d1 — 点 1 和 点 2 (线段 1''-2'') 之间的垂直距离乘以 K1 (K1 是属性窗口的参数, 其默省缺值为 0.1)。
检测目标的 1—4 延长线必须向上, 即点 4 必须远高于 点 1。以下条件应予以检查:
v4>v1+d1;
v4 — 点 4 的价位。
点 4 必须远低于点 2, 以下条件应予以检查:
v4<v2-d2;
此处 v2 是点 2 的价位, d2 是点 2 和点 3 (线段 2''-3'') 之间的垂直距离乘以 K2 (K2 是属性窗口的参数, 其默省缺值为 0.1)。
检测目标到达时间的 2-4 与 1-3 延长线必须交汇点于右侧, 所以 2-2' 的高度必须远高于 4-4' 的高度。在此必须执行以下检查:
h2-h4>K3*h2;
此处 h2 是线段 2-2' 的高度, h4 是线段 4-4' 的高度, K3 是比率 (K3 是属性窗口的参数, 其默省缺值为 0.1)。
这些规则声明并未假定绝对正确。以后我们在指标的创建过程中还要详细描述。基于这些素材, 您可以根据自己的想法调整代码。
选择使用的之字折线
开始之前, 我们下载 附件, 它包含许多 通用之字折线 一文中的多种版本之字折线指标。我们需要从中选择一个在我们的文章中使用。我们不使用 iUniZigZagPrice 和 iUniZigZagPriceSW, 它们设计时基于图表上运行的其它指标进行计算, 因此它们仅对视觉分析有用。其它指标似乎更有趣。它们当中的每一个都可用来创建专家交易系统。此外, 我们不会使用 iCloseZigZag 和 iHighLowZigZag, 它们只是如何创建之字折线的初始示例。剩下两个版本, 即 iUniZigZag 和 iUniZigZagSW。在子窗口中工作的 iUniZigZagSW 指标更适合我们, 因为它提供了更广泛的功能。附件中也包含 iUniZigZagSWEvents 指标, 并展示了使用 iCustom() 函数访问 iUniZigZagSW 指标的示例。我们将使用此变体, 因为它将允许我们使用 iUniZigZagSW 指标的所有可能性, 且可另行将沃尔夫波形的检测代码与之字折线代码分离。
iUniZigZagSWEvents 指标显示在价格图表上, 四个缓冲区用于绘制指标: 两个带箭头的缓冲区, 另外两个画点。这些就是我们检测定沃尔夫波形所需要的。箭头将指示形态识别位置, 点则用于目标。我们的指标将用到 图形对象, 特别是 趋势线 绘制波形和构型以检测目标。如果您将其绘制为线段而非延伸射线, 那么它会是显示不同构型的非常方便的工具。
除了检测入场时刻和方向外, 沃尔夫波形也用于预测目标。所以, 当使用 iUniZigZagSW 时会出现困难。指标带有 SrcSelect 参数, 可以根据所绘制的之字折线选择分析数据的来源。可以选择以下四个选项之一:
Src_HighLow — 按照最高价和最低价;
Src_Close — 按照收盘价;
Src_RSI — 按照 RSI 指标;
Src_MA — 按照移动均线。
基于我们现正创建的指标创建专家交易系统。此即为什么如果我们利用价格来构建之字折线, 那么预测的目标就可以用来放置止盈位。在图表上显示目标没有任何问题。但是如果使用RSI (SrcSelect=Src_RSI) 计算之字折线, 则预测目标将是 RSI 指标, 而非价格。所以, 一旦 RSI 指标达到目标值, 我们就需要市价平仓, 而不可能在图表上显示目标价格和附加构型。
当使用基于价格 (Src_HighLow 或 Src_Close) 绘制的之字折线时, 目标价格和附加构型将显示在图表上。在所有其它情况下, 只会显示一个箭头, 表示已发现的结构及其方向。目标值仍然在适相应的价格缓冲区中提供 (为了能够让专家交易系统以市价平仓来应对任何其它目的), 但不会显示。
很有可能在实践中, 当指标达到目标价位时, 市价平仓的想法无法实现。大多数指标的值在一定范围内变化, 目标结果可能在此范围之外。但是, 在任何情况下, 缓冲区都将包含目标值。
收集关于之字折线峰值的数据
我们现在开始创建指标。我们在编辑器中打开 iUniZigZagSWEvents, 文件并将其保存为 iWolfeW*es。我们将操控这个指标。
直接访问所有的之字折线峰值非常方便 — 在此情况下, 我们不必每次都在历史中搜索它们。我们来创建一个数组保存数值。现在, 每当之字折线改变方向时, 一个新的元素将被添加到数组中。如果指标简单地延伸最后一个线段 (更新极值), 则数组的最后一个元素将被更新。
对于每个峰值, 我们将保存峰值、方向和所在柱线的索引 (索引从左到右)。为此目的, 我们将使用一个含有三个字段的 结构:
struct SPeackTrough{ double Val; // 峰值 int Dir; // 方向 int Bar; // 柱线索引};
我们来创建一个这些结构的数组:
SPeackTrough PeackTrough[];
如果之字折线仅仅基于最高价和最低价 (SrcSelect = Src_HighLow), 当方向改变的情况下将数组递增就足够了, 设置数值并用指标最后延伸的一段更新最后的元素。基于收盘价 (SrcSelect = Src_Close) 或任何其它指标数据的之字折线更难办。在柱线形成期间, 方向改变, 之字折线可以返回原来的状态 (即当前柱线开盘之前)。这意味着对于相同柱线的每次重新计算, 峰值数组需要返回到前一根柱线的初始状态。如果我们经常更改数组大小, 这可能会减慢指标的运行。因此, 我们来引入一个附加的变量, 所用数组大小将被保存其内。必要时, 数组将以块为单位进行修改, 只允许尺寸增长。重新计算同一根柱线之前, 我们要返回此变量的初始值。
我们将使用两个变量来存储数组大小。在一个变量中, 存储前一根柱线时的数组大小。当前计算的柱线时的大小将存储在第二个变量当中:
int PreCount; // 前一根柱线时 PeackTrough 数组的大小int CurCount; // 当前计算柱线时 PeackTrough 数组的大小
在柱线形成并计算完成后, 或计算历史柱线之后, CurCount 变量的值应转移到 PreCount 变量。然后, 在每次计算新形成的柱线之前, 我们要把数值从 PreCount 移动到 CurCount。只有 CurCount 变量将会用于所有的计算。PreCount 变量只是辅助。关于柱线结构完毕的信息只能在下一根柱线开盘 (或计算切换到历史中的下一根柱线) 时才知道。新柱线的出现将由时间决定: 如果柱线时间已经改变, 则会出现一根新柱线 (或计算历史中的下一根柱线已经开始)。需要一个辅助变量以便确定新的柱线:
datetime LastTime;
PreCount, LastCount 和 LastTime 是指标的全局变量。但是它们也可以在 OnCalculate() 指标函数里声明为静态变量。
我们转进到 OnCalculate() 函数。基于 prev_calculated 的值, 判断是第一次执行指标计算还是仅计算新的柱线。0 意即全部计算。在此情况下, 变量 PreCount, CurCount 和 LastTime 需要初始化。以下代码位于 OnCalculte() 函数的最上面, 定义计算的柱线范围, 并初始化辅助变量:
int start; // 起始计算的柱线的索引变量if(prev_calculated==0){ // 计算全部的柱线 start=1; CurCount=0; PreCount=0; LastTime=0; }else{ // 计算新柱线 start=prev_calculated-1; }
现在我们来处理标准的指标循环。在一开始, 我们要将变量 PreCount, CurCount 中的数值组织转移:
for(int i=start;i<rates_total;i++){ if(time[i]>LastTime){ // 计算新 (下一次) 柱线 LastTime=time[i]; PreCount=CurCount; PreDir=CurDir; } else{ // 柱线重新计算 CurCount=PreCount; CurDir=PreDir; }
在所有的计算当中, 仅适用 CurCount 变量, 而 PreCount is 仅设计用于维护当前的 CurCount 值。在新柱线开盘伊始, CurCount 首先包含前一根柱线计算后获得的数值。这就是为什么我们要把这个数值移动到 PreCount。在新柱线计算之后, CurCount 的数值可以改变。但是, 我们只能在下一根柱线的开盘时才能确定该值是最终的。这就是为什么在重新计算同一柱线的情况下, PreCount 变量的数值被放置到 CurCount。
主要指标循环应包含取自 iUniZigZagSWEvents 指标的以下代码:
UpArrowBuffer[i]=EMPTY_VALUE; DnArrowBuffer[i]=EMPTY_VALUE; UpDotBuffer[i]=EMPTY_VALUE; DnDotBuffer[i]=EMPTY_VALUE; // 方向double dir[2];if(CopyBuffer(handle,3,rates_total-i-1,2,dir)<=0){ return(0); }if(dir[0]==1 && dir[1]==-1){ DnArrowBuffer[i]=high[i]; c++; }else if(dir[0]==-1 && dir[1]==1){ UpArrowBuffer[i]=low[i]; c++; }// 新的最高价double lhb[2];if(CopyBuffer(handle,4,rates_total-i-1,2,lhb)<=0){ return(0); }if(lhb[0]!=lhb[1]){ UpDotBuffer[i]=high[i]; }// 新的最低价double llb[2];if(CopyBuffer(handle,5,rates_total-i-1,2,llb)<=0){ return(0); }if(llb[0]!=llb[1]){ DnDotBuffer[i]=low[i]; }
绘制箭头的代码部分将不会被使用, 所以我们来删除它。
在操作期间, 由于需要最后一个线段来检测点 5 (见图例.1), 指标将监控之字折线的每次变化, 包括方向改变和最后线段的每次延伸。我们将使用上述片段中的部分代码, 这与绘制新极值有关。
若要监控之字折线的方向并确定其变化, 我们将需要几个类似于 CurCount 和 PreCount 的变量: PreDir 和 CurDir:
int PreDir; // 前一根柱线的之字折线方向int CurDir; // 当前柱线的之字折线方向
在 OnCalculate() 中它们即可以是全局的, 也可是静态的。在指标计算伊始, 我们还需要初始化这些变量, 并在柱线计算之后移动数值, 类似于 PreCount 和 CurCount。以下是 OnCalculate() 的最终代码, 如同指标创建的当前步骤:
int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { int start; // 用于计算起始柱线的变量 if(prev_calculated==0){ // 全部计算 start=1; CurCount=0; PreCount=0; CurDir=0; PreDir=0; LastTime=0; } else{ // 仅计算新的柱线 start=prev_calculated-1; } // 指标主循环 for(int i=start;i<rates_total;i++){ if(time[i]>LastTime){ // 新柱线 LastTime=time[i]; PreCount=CurCount; PreDir=CurDir; } else{ // 柱线重新计算 CurCount=PreCount; CurDir=PreDir; } // 清除绘制箭头和点的缓冲区 UpArrowBuffer[i]=EMPTY_VALUE; DnArrowBuffer[i]=EMPTY_VALUE; UpDotBuffer[i]=EMPTY_VALUE; DnDotBuffer[i]=EMPTY_VALUE; // 辅助变量 double hval[1]; double lval[1]; double zz[1]; // 新的最高价 double lhb[2]; // 接收缓冲区的两个元素, 内带新的最高价的柱线索引 if(CopyBuffer(handle,4,rates_total-i-1,2,lhb)<=0){ return(0); } if(lhb[0]!=lhb[1]){ // 这是新的最高价 // 获取最高价格 (或基于之字折线计算的数据) if(CopyBuffer(handle,0,rates_total-i-1,1,hval)<=0){ return(0); } if(CurDir==1){ // 已知的最后向上方向 // 更新有关最后一个之字折线拐点的信息 RefreshLast(i,hval[0]); } else{ // 之字折线方向已改变 // 加入新的数值 AddNew(i,hval[0],1); } // 此处, 我们将添加检查条件以便识别向下的沃尔夫波形 } // 新的最低价 double llb[2]; // 接收缓冲区的两个元素, 内带新的最低价的柱线索引 if(CopyBuffer(handle,5,rates_total-i-1,2,llb)<=0){ return(0); } if(llb[0]!=llb[1]){ // 这是新的最低价 // 获取最低价格 (或基于之字折线计算的数据) if(CopyBuffer(handle,1,rates_total-i-1,1,lval)<=0){ return(0); } if(CurDir==-1){ // 已知的最后向下方向 // 更新有关最后一个之字折线拐点的信息 RefreshLast(i,lval[0]); } else{ // 之字折线方向已改变 // 加入新的数值 AddNew(i,lval[0],-1); } // 此处, 我们将添加检查条件以便识别向上的沃尔夫波形 } } return(rates_total); }
此代码包含 AddNew() 和 RefreshLast() 函数。之字折线变化的柱线索引和新的极值被传递给这两个函数。之字折线的方向也一并传递给 AddNew()。
添加新点的 AddNew() 函数:
void AddNew(int i,double v,int d){ if(CurCount>=ArraySize(PeackTrough)){ // 数组中没有可用元素 ArrayResize(PeackTrough,ArraySize(PeackTrough)+1024); // 增加数组大小 } PeackTrough[CurCount].Dir=d; // 设置方向 PeackTrough[CurCount].Val=v; // 设置数值 PeackTrough[CurCount].Bar=i; // 设置柱线 CurCount++; // 使用数组占用元素的数量增加变量 CurDir=d; // 记忆最后的之字折线方向}
RefreshLast() 函数用来刷新最后一个点:
void RefreshLast(int i,double v){ PeackTrough[CurCount-1].Bar=i; // 设置新柱线 PeackTrough[CurCount-1].Val=v; // 设置新数值}
指标可以在现阶段得以保存, 它可作为开发各种定义之字折线形态的指标的基础。在以下附件中, 指标名称为 "iWolfeW*es_Step_1"。
一点几何
当识别沃尔夫波形并添加检测目标的形状时, 我们需要一点几何知识。我们分开看这些问题, 并编写函数来解决它们。
问题 #1. 一条直线由一对点 x-y 设置, 其中 x 是柱线索引, y 是数值 (价格或指标值)。我们知道第三点的 x 坐标, 我们需要找到这一点在线上的数值 (图例. 2)。
图例. 2. 给定: X1, Y1, X2, Y2, X3. 我们需要找出 Y3。
问题 #1 解决方案。直线沿 X 轴的数值每增加一个单位, 我们检测一次直线沿 Y 轴的增加值:
D=(Y2-Y1)/(X2-X1)
此处 D 是增量, Y1 是点 1 处的价格或指标值, Y2 是点 2 处的价格或指标值, X1 是点 1 处的柱线索引, X2 是点 2 处的柱线索引。
检测 Y3:
Y3=Y1+(X3-X1)*D
此处 X3 是点 3 处的柱线索引, Y3 是点 3 处搜寻线的值。
我们得到以下函数:
double y3(double x1,double y1,double x2,double y2,double x3){ return(y1+(x3-x1)*(y2-y1)/(x2-x1)); }
以下参数需要传递给函数:
x1 — 点 1 处的柱线索引;
y1 — 点 1 处的数值;
x2 — 点 2 处的柱线索引;
y2 — 点 2 处的数值。
问题 #2. 两条线由两个 x-y 点设定。我们需要找到它们交汇点的 x 坐标 (图例. 3)。在此可能会出现以下问题: 为什么我们选择了 x 坐标? 在任何情况下, 获得 x 坐标之后, 将计算点 3 的 y 坐标 (使用其中一条线的方程)。因此, 我们可以先获得点 3 的 y 坐标, 然后使用方程找到 x 值。
图例. 3. 两条给定直线。我们需要找到它们的交叉点
首先, 使用两点的坐标, 得到 y=a+b*x 形式的线方程。
我们来进行预计算。直线斜率值 (x 轴的每一单位与 y 轴每一单位的比值):
D1=(Y12-Y11)/(X12-X11)
此处, D1 是第一条线的期望斜率值 (每根柱线的直线值变化), X11 是第一条线点1 处的柱线索引, X12 是第一条线点 2 处的柱线索引, Y11 是第一条线点 1 处的值, Y12 是第一条线点 2 处值。
第二条线的斜率:
D2=(Y22-Y21)/(X22-X21)
此处, D2 是第二条线的期望斜率值 (每根柱线的直线值变化), X21 是第二条线点1 处的柱线索引, X22 是第二条线点 2 处的柱线索引, Y21 是第二条线点 1 处的值, Y22 是第二条线点 2 处值。
此处是直线方程。线 1 方程:
Y3=Y11+D1*(X3-X11)
此处 Y3 是交汇点 (点 3) 处的直线值, X3 是点 3 处的柱线索引。
线 2 方程:
Y3=Y21+D2*(X3-X21)
在交点处, 线的值相等。所以我们将线 1 方程与线 2 方程划等号:
Y11+D1*(X3-X11)=Y21+D2*(X3-X21);
使用得到的表达式, 我们发现 X3。作为结果, 我们得到用于检测交点 X 坐标的 TwoLinesCrossX() 函数:
double TwoLinesCrossX(double x11,double y11,double x12,double y12,double x21,double y21,double x22,double y22){ double k2=(y22-y21)/(x22-x21); double k1=(y12-y11)/(x12-x11); return((y11-y21-k1*x11+k2*x21)/(k2-k1)); }
以下参数需要传递给函数:
x11, 第一条线点 1 处的柱线索引
y11, 第一条线点 1 处的值
x12, 第一条线点 2 处的柱线索引
y12, 第一条线点 2 处的值
x21, 第二条线点 1 处的柱线索引
y21, 第二条线点 1 处的值
x22, 第二条线点 2 处的柱线索引
y22, 第二条线点 2 处的值
一旦定义了线-线交点处的 x 坐标, 可使用一条线的两点的坐标, 以及解决问题 #1 时获得的函数 y3() 来求解 y 坐标。
如果我们需要首先获得 y 坐标, 则应该转换直线方程, 以便通过 y 来表示 x 坐标。 以下是一条线的方程:
X3=X11+(Y3-Y11)/D1
第二条线的方程:
X3=X21+(Y3-Y21)/D2
将两个表达式划等号:
X11+(Y3-Y11)/D1=X21+(Y3-Y21)/D2
我们基于以上方程找到 Y3。因此, 我们获得了 TwoLinesCrossY() 函数:
double TwoLinesCrossY(double x11,double y11,double x12,double y12,double x21,double y21,double x22,double y22){ double k2=(x22-x21)/(y22-y21); double k1=(x12-x11)/(y12-y11); return((x11-x21-k1*y11+k2*y21)/(k2-k1)); }
函数参数与 TwoLinesCrossX() 的相同。
检测波形
现在我们可以轻松访问所有之字折线峰值和辅助几何函数, 我们可以继续检测沃尔夫波形。我们需要 "捕捉" 最后一个之字折线的线段穿过直线 1-3 (见图例.1) 的时刻, 即点 5。因此, 当每次出现新的之字折线极值时我们要检查沃尔夫波形条件 (方向改变, 以及最后的线段延伸时)。在上面的 OnCalculate() 函数代码中, 所有应检查条件的地方均有详细的注释。从它们那里会调用 CheckDn() 和 CheckUp() 函数。我们来详细研究 CheckUp() 函数:
void CheckUp(int rates_total,const double & low[],const datetime & time[],int i){ if(CurCount<5 || CurDir!=-1){ // 如果没有足够的峰值, 或之字折线没有指向下方, 无需检查 return; } // 用峰值数据准备 short 变量 // 峰值数据变量 double v1=PeackTrough[CurCount-5].Val; double v2=PeackTrough[CurCount-4].Val; double v3=PeackTrough[CurCount-3].Val; double v4=PeackTrough[CurCount-2].Val; double v5=PeackTrough[CurCount-1].Val; // 峰值柱线变量 int i1=PeackTrough[CurCount-5].Bar; int i2=PeackTrough[CurCount-4].Bar; int i3=PeackTrough[CurCount-3].Bar; int i4=PeackTrough[CurCount-2].Bar; int i5=PeackTrough[CurCount-1].Bar; if(CurLastBuySig!=i4){ // 如果在此之字折线结构中未检测到波形 double d1=K1*(v2-v1); // 相对于峰值 1 的缩进峰值 3 的最小值 if(v3<v1-d1){ // 峰值 3 明显低于峰值 1 if(v4>v1+d1){ // 线 1-4 指而向上 double d2=K2*(v2-v3); // 相对于峰值 2 的缩进峰值 4 的最小值 if(v4<v2-d2){ // 峰值 4 明显低于峰值 2 double v5l=y3(i1,v1,i3,v3,i); // 点 5 处的值 if(v5<v5l){ // 之字折线的最后线段与线 1-3 交叉 double v4x=y3(i1,v1,i3,v3,i4); // 点 4' 处的值 double v2x=y3(i1,v1,i3,v3,i2); // 点 2' 处的值 double h4=v4-v4x; // 线 4-4' 高 double h2=v2-v2x; // 线 2-2' 高 if(h2-h4>K3*h2){ // 线 1-3 和 2-4 相遇 double tb=TwoLinesCrossX(i1,v1,i3,v3,i2,v2,i4,v4); // 线 1-3 与 2-4 交汇处的柱线 double tv=y3(i1,v1,i4,v4,tb); // 线 1-3 与 2-4 交汇点处的值 UpArrowBuffer[i]=low[i]; // 显示向上箭头 UpDotBuffer[i]=tv; // 显示目标价位的点 CurLastBuySig=i4; // 记住, 在此之字折线配置中已发现形状 if(_DrawW*es){ // 绘制结构 DrawObjects(BuyColor,BuyTargetColor,v1,v2,v3,v4,v5l,i1,i2,i3,i4,i5,time,i,tb,tv,rates_total); } } } } } } } }
为了检测一个波形, 我们至少需要 5 个之字折线峰值。附加条件: 为了检测稍后转而向上的波形, 折线应指向下方:
if(CurCount<5 || CurDir!=-1){ // 如果没有足够的峰值, 或之字折线没有指向下方, 无需检查 return; }
若要获得峰值数据, 我们可以直接对 PeakTrough 数组进行寻址, 但这样很不方便。使用简短名称的辅助变量更为简单:
// 峰值数据变量double v1=PeackTrough[CurCount-5].Val;double v2=PeackTrough[CurCount-4].Val;double v3=PeackTrough[CurCount-3].Val;double v4=PeackTrough[CurCount-2].Val;double v5=PeackTrough[CurCount-1].Val; // 峰值柱线变量int i1=PeackTrough[CurCount-5].Bar;int i2=PeackTrough[CurCount-4].Bar; int i3=PeackTrough[CurCount-3].Bar;int i4=PeackTrough[CurCount-2].Bar;int i5=PeackTrough[CurCount-1].Bar;
如果已经检测到一个波形, 且已设置了一个箭头, 则不再需要使用相同的之字折线配置。通过检查峰值 4 的索引 (最后形成的峰值) 来识别之字折线配置:
if(CurLastBuySig!=i4){ // 如果在此之字折线结构中未检测到波形
若要保存配置 ID 的值, 我们使用一对类似于 CurCount 和 PreCount 的变量。
现在我们直接进行波形检测。我们计算点 3 相对于点 1 的最小偏移值, 点 2 相对于点 1 的位移:
double d1=K1*(v2-v1); // 相对于峰值 1 的缩进峰值 3 的最小值
然后检查点的位移:
if(v3<v1-d1){ // 峰值 3 明显低于峰值 1 if(v4>v1+d1){ // 线 1-4 指而向上
我们计算相对于点 2 的点 4 缩进的最小值:
double d2=K2*(v2-v3); // 相对于峰值 2 的缩进峰值 4 的最小值
检查点 2 和点 4 的位置:
if(v4<v2-d2){ // 峰值 4 明显低于峰值 2
现在我们来计算位于线 1-3 上的点对应于所计算柱线的值:
double v5l=y3(i1,v1,i3,v3,i); // 点 5 处的值
检查是否触及线 1-3:
if(v5<v5l){ // 之字折线的最后线段与线 1-3 交叉
计算点 4' 和点 2' 的值:
double v4x=y3(i1,v1,i3,v3,i4); // 点 4' 处的值double v2x=y3(i1,v1,i3,v3,i2); // 点 2' 处的值
计算 4-4' 和 2-2' 的高度:
double h4=v4-v4x; // 线 4-4' 高double h2=v2-v2x; // 线 2-2' 高
利用这些高度, 检查线 1-3 和 2-4 是否在右侧相遇:
if(h2-h4>K3*h2){ // 线 1-3 和 2-4 相遇
如果此条件满足, 意味着波形已发现。
定义目标。首先定义目标柱线:
double tb=TwoLinesCrossX(i1,v1,i3,v3,i2,v2,i4,v4); // 线 1-3 与 2-4 交汇处的柱线
请注意, "double" 变量用于计算精度。
目标值:
double tv=y3(i1,v1,i4,v4,tb); // 线 1-3 与 2-4 交汇点处的值
记住图标并 "记住" 之字折线配置的 ID:
UpDotBuffer[i]=tv; // 显示目标价位的点CurLastBuySig=i4; // 记住, 在此之字折线配置中已发现形状
最后, 我们绘制检测目标的波形和结构:
if(_DrawW*es){ // 绘制结构 DrawObjects(BuyColor,BuyTargetColor,v1,v2,v3,v4,v5l,i1,i2,i3,i4,i5,time,i,tb,tv,rates_total); }
绘制波形和构型, 即 DrawObjects() 函数, 将在本文的另一部分中予以研究。
向下的波形 (对于卖出) 可由 CheckDn 函数检测, 除了连接方向略有差异外, 与 CheckUp 相同。函数代码以及与 CheckUp() 函数的差异如下:
void CheckDn(int rates_total,const double & high[],const datetime & time[],int i){ // 没有足够的峰值, 或并非指向上方 if(CurCount<5 || CurDir!=1){ return; } double v1=PeackTrough[CurCount-5].Val; double v2=PeackTrough[CurCount-4].Val; double v3=PeackTrough[CurCount-3].Val; double v4=PeackTrough[CurCount-2].Val; double v5=PeackTrough[CurCount-1].Val; int i1=PeackTrough[CurCount-5].Bar; int i2=PeackTrough[CurCount-4].Bar; int i3=PeackTrough[CurCount-3].Bar; int i4=PeackTrough[CurCount-2].Bar; int i5=PeackTrough[CurCount-1].Bar; if(CurLastSellSig!=i4){ double d1=K1*(v1-v2); // 峰值 v1 高于峰值 v2 if(v3>v1+d1){ // 峰值 v3 高于峰值 v1 if(v4<v1-d1){ // 峰值 v4 低于峰值 v1 double d2=K2*(v3-v2); // 峰值 v3 高于峰值 v2 if(v4>v2+d2){ // 峰值 v4 高于峰值 v2 double v5l=y3(i1,v1,i3,v3,i); if(v5>v5l){ // 之字折线突破线 1-3 向上 double v4x=y3(i1,v1,i3,v3,i4); double v2x=y3(i1,v1,i3,v3,i2); double h4=v4x-v4; // 点 4' 高于点 4 double h2=v2x-v2; // 点 2' 高于点 2 if(h2-h4>K3*h2){ double tb=TwoLinesCrossX(i1,v1,i3,v3,i2,v2,i4,v4); double tv=y3(i1,v1,i4,v4,tb); DnArrowBuffer[i]=high[i]; DnDotBuffer[i]=tv; CurLastSellSig=i4; if(_DrawW*es){ // 用其它颜色绘制 DrawObjects(SellColor, SellTargetColor, v1, v2, v3, v4, v5l, i1, i2, i3, i4, i5, time, i, tb, tv, rates_total); } } } } } } } }
第一个区别是初步检查:
// 没有足够的峰值, 或并非指向上方 if(CurCount<5 || CurDir!=1){ return; }
如果没有足够的峰值, 或之字折线指向下方, 则函数操作应该完毕。
对于指向下方, 峰值和波谷改变位置: 点 1, 3 , 5 , 6 位于上方, 点 2, 4, 7 位于下方, 因此公式中某些变量的位置也会发生变化。检测峰值 1, 3 和 1, 4 之间的最小距离:
double d1=K1*(v1-v2); // 峰值 v1 高于峰值 v2
检查峰值 1, 3 的位置:
if(v3>v1+d1){ // 峰值 v3 高于峰值 v1
检查峰值 1, 4 的位置:
if(v4<v1-d1){ // 峰值 v4 低于峰值 v1
计算峰值 2, 3 之间的最小距离, 并检查它:
double d2=K2*(v3-v2); // 峰值 v3 高于峰值 v2 if(v4>v2+d2){ // 峰值 v4 高于峰值 v2
检查点 5 是否形成 (之字折线突破线 1-3 向上):
if(v5>v5l){ // 之字折线突破线 1-3 向上
计算 2-2' 和 4-4' 高度, 并检查线 1-3 和 2-4 是否在右侧相遇:
double h4=v4x-v4; // 点 4' 高于点 4double h2=v2x-v2; // 点 2' 高于点 2
波形和构型使用不同的颜色绘制:
// 用其它颜色绘制DrawObjects(SellColor, SellTargetColor, v1, v2, v3, v4, v5l, i1, i2, i3, i4, i5, time, i, tb, tv, rates_total);
绘制波形和目标
所有波形和构型使用单一算法绘制, 所以使用这样的一个 DrawObjects() 函数。元素指向上方和下方会以不同的颜色绘制。为此, 颜色参数 BuyColor 或 SellColor 被传递给函数。波形和定义目标的构型也以不同的颜色绘制, 所以参数 BuyTargetColor 或 SellTargetColor 也传递给函数。这些变量是指标的外部变量, 您可以设置期望的颜色。除了颜色, 还需要更多一些外部参数。以下是所有波形与对象绘制函数所需的附加参数:
input bool DrawW*es = true; // 启用波形和对象绘制input color BuyColor = clrAqua; // 买入波形颜色input color SellColor = clrRed; // 卖出波形颜色input int W*esWidth = 2; // 波形宽度input bool DrawTarget = true; // 附加的启用/禁用构型input int TargetWidth = 1; // 对象宽度input color BuyTargetColor = clrRoyalBlue; // 买入对象颜色input color SellTargetColor = clrPaleVioletRed; // 卖出对象颜色
颜色传递之后, 所有峰值柱线的值和索引变量传递给函数。峰值 5 是例外, 为此, 传递已计算的线 1-3 的值, 替代之字折线尾端的值。所有之字折线极点的坐标以柱线索引给出, 而图形对象需要时间, 所以将指向 "time" 数组的指针传递给函数。已计算柱线的索引 — i, 目标柱线索引 — tb, 目标值 — tv, 以及图表之上的柱线总数 — rates_total 一并传递给函数。
我们已经注意到, 在本文的开头, 只有之字折线采用最高价/最低价 (SrcSelect 设置为 Src_HighLow) 或收盘价 (SrcSelect 设置为 Src_Close) 计算时, 才应绘制波形和对象。所以, 这取决于 SrcSelect 变量, 在 OnInit() 函数里应强制禁用绘图 (DrawW*es 变量)。为此目的, 我们声明使用一个附加变量, 替代 DrawW*es:
bool _DrawW*es;
接下来, 在 OnInit() 函数中, 我们设置 DrawW*es 变量的值, 或将其设置为 false 来禁用它。此外, 为目标绘制缓冲区设置一个不可见的颜色:
if(SrcSelect==Src_HighLow || SrcSelect==Src_Close){ _DrawW*es=DrawW*es; }else{ _DrawW*es=false; PlotIndexSetInteger(2,PLOT_LINE_COLOR,clrNONE); PlotIndexSetInteger(3,PLOT_LINE_COLOR,clrNONE); }
我们继续进入 DrawObjects() 函数。首先, 我们提供了完整的函数代码, 然后我们会详细研究它:
void DrawObjects( color col, color tcol, double v1, double v2, double v3, double v4, double v5, int i1, int i2, int i3, int i4, int i5, const datetime & time[], int i, double target_bar, double target_value, int rates_total){ // 用于图形对象名称的前缀 string prefix=MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString(time[i])+"_"; // 绘制波形 fObjTrend(prefix+"12",time[i1],v1,time[i2],v2,col,W*esWidth); fObjTrend(prefix+"23",time[i2],v2,time[i3],v3,col,W*esWidth); fObjTrend(prefix+"34",time[i3],v3,time[i4],v4,col,W*esWidth); fObjTrend(prefix+"45",time[i4],v4,time[i5],v5,col,W*esWidth); // 绘制构型 if(DrawTarget){ datetime TargetTime; // 获取整数型的目标柱线索引 int tbc=(int)MathCeil(target_bar); if(tbc<rates_total){ // 目标位于图表上存在的柱线之内 TargetTime=time[tbc]; } else{ // 目标位于未来 TargetTime=time[rates_total-1]+(tbc-rates_total+1)*PeriodSeconds(); } // 计算目标所在柱线处的值 double tv13=y3(i1,v1,i3,v3,tbc); double tv24=y3(i2,v2,i4,v4,tbc); double tv14=y3(i1,v1,i4,v4,tbc); // 构型 fObjTrend(prefix+"13",time[i1],v1,TargetTime,tv13,tcol,TargetWidth); fObjTrend(prefix+"24",time[i2],v2,TargetTime,tv24,tcol,TargetWidth); fObjTrend(prefix+"14",time[i1],v1,TargetTime,tv14,tcol,TargetWidth); // 目标价位水平线 fObjTrend(prefix+"67",TargetTime,tv24,TargetTime,tv14,tcol,TargetWidth); // 目标价位垂直线 fObjTrend(prefix+"7h",time[i],target_value,TargetTime,target_value,tcol,TargetWidth); } }
所有绘图使用若干趋势线进行, 为此首先形成名称的公用前缀:
// 用于图形对象名称的前缀 string prefix=MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString(time[i])+"_";
然后绘制波形, 所有峰值的坐标传递给函数:
// 绘制波形 fObjTrend(prefix+"12",time[i1],v1,time[i2],v2,col,W*esWidth); fObjTrend(prefix+"23",time[i2],v2,time[i3],v3,col,W*esWidth); fObjTrend(prefix+"34",time[i3],v3,time[i4],v4,col,W*esWidth); fObjTrend(prefix+"45",time[i4],v4,time[i5],v5,col,W*esWidth);
绘制定义目标的结构。检查是否启用了绘图:
// 绘制构型if(DrawTarget){
如果已启用, 则绘制结构。当在历史数据上显示指标时, 目标柱线最有可能出现在已存在的柱线上, 但如果最近出现的柱线上检测到波形, 目标可能出现在将来, 即最后一根柱线的右侧。因此, 我们需要两个计算目标柱线时间的变体。为此目的我们声明一个变量:
datetime TargetTime;
target_bar 变量有一个分数值, 因此我们将其提升到最接近的整数:
// 获取整数型的目标柱线索引 int tbc=(int)MathCeil(target_bar);
之后我们使用得到的 tbc 变量。在此, 我们可以使用 MathFloor() 函数, 并获得最近的较低的整数。这不会影响最终的结果, 因为构型只有一个提示性的目的。当使用 MathCeil(), 线 1-3 和 2-4 的端点必然在目标柱线附近相交, 且构型看起来更自然。
我们来检测目标抵达的时间。如果目标位于现存的柱线之一, 我们只需要计算目标柱线的索引, 并从 'time' 数组中获取时间。如果目标是在最后一根柱线的右侧, 那么我们要检测目标距离最后一根柱线多少根柱线, 并计算时间:
if(tbc<rates_total){ // 目标位于图表上存在的柱线之内 TargetTime=time[tbc]; }else{ // 目标位于未来 TargetTime=time[rates_total-1]+(tbc-rates_total+1)*PeriodSeconds(); }
我们来计算所有直线 (1-3, 2-4 和 1-4) 在目标柱线处的值:
// 计算目标所在柱线处的值double tv13=y3(i1,v1,i3,v3,tbc); double tv24=y3(i2,v2,i4,v4,tbc); double tv14=y3(i1,v1,i4,v4,tbc);
尽管事实上早前计算的目标值已被传递到函数 (target_value 变量), 它仍然会重新为线 2-4 计算新的构型。这与事实相连, 替代来自 target_bar 变量的确切值, 我们使用来自 tbc 变量的值, 该值比 target_bar 稍大。经过这些计算, 我们确保在确切的 target_bar 坐标上, 直线将在 target_value 价位准确相交。
我们使用计算出值绘制直线:
fObjTrend(prefix+"13",time[i1],v1,TargetTime,tv13,tcol,TargetWidth); fObjTrend(prefix+"24",time[i2],v2,TargetTime,tv24,tcol,TargetWidth); fObjTrend(prefix+"14",time[i1],v1,TargetTime,tv14,tcol,TargetWidth);
使用辅助 fObjTrend() 函数绘制直线:
void fObjTrend( string aObjName, datetime aTime_1, double aPrice_1, datetime aTime_2, double aPrice_2, color aColor = clrRed, color aWidth = 1, bool aRay_1 = false, bool aRay_2 = false, string aText = "", int aWindow = 0, color aStyle = 0, int aChartID = 0, bool aBack = false, bool aSelectable = false, bool aSelected = false, long aTimeFrames = OBJ_ALL_PERIODS ){ ObjectCreate(aChartID,aObjName,OBJ_TREND,aWindow,aTime_1,aPrice_1,aTime_2,aPrice_2); ObjectSetInteger(aChartID,aObjName,OBJPROP_BACK,aBack); ObjectSetInteger(aChartID,aObjName,OBJPROP_COLOR,aColor); ObjectSetInteger(aChartID,aObjName,OBJPROP_SELECTABLE,aSelectable); ObjectSetInteger(aChartID,aObjName,OBJPROP_SELECTED,aSelected); ObjectSetInteger(aChartID,aObjName,OBJPROP_TIMEFRAMES,aTimeFrames); ObjectSetString(aChartID,aObjName,OBJPROP_TEXT,aText); ObjectSetInteger(aChartID,aObjName,OBJPROP_WIDTH,aWidth); ObjectSetInteger(aChartID,aObjName,OBJPROP_STYLE,aStyle); ObjectSetInteger(aChartID,aObjName,OBJPROP_RAY_LEFT,aRay_1); ObjectSetInteger(aChartID,aObjName,OBJPROP_RAY_RIGHT,aRay_2); ObjectMove(aChartID,aObjName,0,aTime_1,aPrice_1); ObjectMove(aChartID,aObjName,1,aTime_2,aPrice_2); }
它是一个通用函数, 也可用于快速创建趋势线并设置其所有参数。函数参数如表 1 所述。最频繁变化的参数在表的开头 (5 个必需参数), 其余的是可选的, 您可以选择不将其传递给函数。这一变体使得函数的使用非常便利。
表 1. 函数 fObjTrend() 的参数
参数 | 目的 |
---|---|
string aObjName | 对象名 |
datetime aTime_1 | 第一个锚点的时间 |
double aPrice_1 | 第一个锚点的价位 |
datetime aTime_2 | 第二个锚点的时间 |
double aPrice_2 | 第二个锚点的价位 |
color aColor | 颜色 |
color aWidth | 宽度 |
bool aRay_1 | 自第一个锚点的延伸线 |
bool aRay_2 | 自第二个锚点的延伸线 |
string aText | 提示文本 |
int aWindow | 子窗口 |
color aStyle | 线型 |
int aChartID | 图表 ID |
bool aBack | 绘制在背景上 |
bool aSelectable | 对象可选 |
bool aSelected | 对象选中 |
long aTimeFrames | 绘制直线的时间帧 |
现在我们需要绘制两条额外的直线: 目标柱线上的垂直线, 和目标价位上的一条水平线:
fObjTrend(prefix+"67",TargetTime,tv24,TargetTime,tv14,tcol,TargetWidth); fObjTrend(prefix+"7h",time[i],target_value,TargetTime,target_value,tcol,TargetWidth);
结果就是, 我们得到了波形和构型的图像:
图例. 4. 沃尔夫波形和检测买入交易目标的构型
删除图形对象
当使用基于收盘价 (SrcSelect = Src_Close) 或基于另一个指标的之字折线时, 形状也许会在柱线形成中不时出现或消失。为此目的, 在主要指标循环开始时清除箭头和点的缓冲区:
UpArrowBuffer[i]=EMPTY_VALUE; DnArrowBuffer[i]=EMPTY_VALUE; UpDotBuffer[i]=EMPTY_VALUE; DnDotBuffer[i]=EMPTY_VALUE;
图形对象删除也应该在循环开始时执行。如果启用了绘制波形和构型, 则在指标循环开始时调用 DeleteObjects() 函数:
if(_DrawW*es){ DeleteObjects(time[i]); }
函数 DeleteObjects() 代码:
void DeleteObjects(datetime time){ string prefix=MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString(time)+"_"; ObjectDelete(0,prefix+"12"); ObjectDelete(0,prefix+"23"); ObjectDelete(0,prefix+"34"); ObjectDelete(0,prefix+"45"); ObjectDelete(0,prefix+"13"); ObjectDelete(0,prefix+"24"); ObjectDelete(0,prefix+"14"); ObjectDelete(0,prefix+"67"); ObjectDelete(0,prefix+"7h"); }
已计算柱线的时间传递给函数。在此函数中, 所有与已计算柱线相对应名称的图形对象均将被删除。
从图表中删除指标时, 我们需要删除由指标创建的所有图形对象。ObjectsDeleteAll() 函数从 DeInit() 函数中调用, 当指标操作完成时, 它会自动执行。指标的名称也被用作所有图形对象的前缀, 作为第二个参数传递给函数。这可确保只有属于指标的图形对象才会被删除:
void OnDeinit(const int reason){ ObjectsDeleteAll(0,MQLInfoString(MQL_PROGRAM_NAME)); ChartRedraw(0); }
警报功能
我们来添加一个警报功能, 通知每个新出现的箭头。功能类似于 "具有图形界面的通用趋势" 中描述的通用趋势指标使用的功能。
警报功能允许您跟踪正在成形的柱线 (适用于最高价-最低价为基准的之字折线) 或已成形的柱线 (适合基于收盘价或其它指标的之字折线) 上出现的箭头。我们来创建一个 用于选择警报类型的枚举:
enum EAlerts{ Alerts_off=0, // 禁用警报 Alerts_Bar0=1, // 形成中柱线 Alerts_Bar1=2 // 完成的柱线};
将变量添加到属性窗口:
input EAlerts Alerts = Alerts_off;
警报功能代码作为单独的 CheckAlerts() 函数提供。图表上的柱线数和时间数组传递到此函数:
void CheckAlerts(int rates_total,const datetime & time[]){ if(Alerts!=Alerts_off){ // 启用警报 static datetime tm0=0; // 一个变量, 最后一次买入警报的柱线时间 static datetime tm1=0; // 一个变量, 最后一次卖出警报的柱线时间 if(tm0==0){ // 第一次函数执行 // 变量初始化 tm0=time[rates_total-1]; tm1=time[rates_total-1]; } string mes=""; // 消息变量 // 有一个向上箭头, 最后一根柱线没有警报 if(UpArrowBuffer[rates_total-Alerts]!=EMPTY_VALUE && tm0!=time[rates_total-1] ){ tm0=time[rates_total-1]; // 记住最后的警报时间 mes=mes+" buy"; // 形成一条消息 } // 有一个向下箭头, 最后一根柱线没有警报 if(DnArrowBuffer[rates_total-Alerts]!=EMPTY_VALUE && tm1!=time[rates_total-1] ){ tm1=time[rates_total-1]; // 记住最后的警报时间 mes=mes+" sell"; // 形成一条消息 } if(mes!=""){ // 有一条消息 // 打开消息窗口 Alert(MQLInfoString(MQL_PROGRAM_NAME)+"("+Symbol()+","+IntegerToString(PeriodSeconds()/60)+"):"+mes); } } }
主循环后之后, 在 OnCalculate() 函数末尾调用 CheckAlerts() 函数。OnCalculate() 函数结束时还调用图表刷新功能, 以便加速波形和构型的绘制:
if(_DrawW*es){ ChartRedraw(0); }
现在指标创建已经完成。它被称为 iWolfeW*es, 并在文章的附件中提供。
专家交易系统
我们已创建了一个相当复杂的指标。我们来尝试确保它不仅可以在静态历史上正常工作, 还可以评估图形分析研究方法的有效性。为此, 我们创建一个简单的专家交易系统。
EA 应能遵照指标的所有信号开仓。这将使我们能够评估其有效性。因此, 它将工作于对冲账户, 且没有持仓数量的限制。
我们在编辑器中创建一个新的专家交易系统, 并将其命名为 eWolfeW*es。从指标中复制外部参数并将其添加到 EA 文件中。下面我们添加额外的参数来确定止损和止盈:
input double StopLoss_K = 1; // 止损比率input bool FixedSLTP = false; // 固定止损和止盈input int StopLoss = 50; // 固定止损值input int TakeProfit = 50; // 固定止盈值
这些参数将允许我们选择两个 SL 和 TP 选项之一。
如果 FixedSLTP=false, 则使用 StopLoss_K 变量。在此情况下, 止盈基于指标值设置 — 在目标点代表的价位上, 止损使用 StopLoss_K 系数按照止盈的比例进行计算。SL 和 TP 定义选项仅适用于基于价格的之字折线: 使用最高价-最低价或收盘价 (SrcSelect 设置为 Src_HighLow 或 Src_Close)。
如果 FixedSLTP=true, 则使用 StopLoss 和 TakeProfit 变量。它可用于基于指标和价格的之字折线。
我们在 OnInit() 函数中检查帐户类型。如果帐户不允许对冲, EA 操作将终止:
if(AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING){ Print("此非对冲账户"); return(INIT_FAILED); }
如果专家交易系统未处于可视测试模式下, 那么我们禁止绘制波形和构型:
bool _DrawW*es;if(MQLInfoInteger(MQL_VISUAL_MODE)){ _DrawW*es=DrawW*es; }else{ _DrawW*es=false; }
当 iWolfeW*es 指标由 iCustom() 函数调用时, 变量 _DrawW*es 将用于替代 DrawW*es。调用指标并检查是否成功加载:
h=iCustom( Symbol(), Period(), "iWolfeW*es", Alerts, SrcSelect, DirSelect, RSIPeriod, RSIPrice, MAPeriod, MAShift, MAMethod, MAPrice, CCIPeriod, CCIPrice, ZZPeriod, K1, K2, K3, _DrawW*es, BuyColor, SellColor, W*esWidth, DrawTarget, TargetWidth, BuyTargetColor, SellTargetColor); if(h==INVALID_HANDLE){ Print("不能加载指标"); return(INIT_FAILED); }
如果指标加载失败, EA 操作应停止。
当使用基于最高价-最低价的指标时, 箭头不会消失, 因此 EA 可以在不完整的当前柱线上操作。在所有其它情况下, EA 应检查第一个完整柱线上的指示箭头。为此目的, 我们使用 EA 的全局变量 "Shift":
int Shift;
根据之字折线类型设置所需的值:
if(SrcSelect==Src_HighLow){ Shift=0; }else{ Shift=1; }
以下是 OnInit() 函数的整体代码:
int OnInit(){ // 检查账户类型 if(AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING){ Print("此非对冲账户"); return(INIT_FAILED); } // 禁止绘制波形和构型 bool _DrawW*es; if(MQLInfoInteger(MQL_VISUAL_MODE)){ _DrawW*es=DrawW*es; } else{ _DrawW*es=false; } // 加载指标 h=iCustom( Symbol(), Period(), "iWolfeW*es", Alerts, SrcSelect, DirSelect, RSIPeriod, RSIPrice, MAPeriod, MAShift, MAMethod, MAPrice, CCIPeriod, CCIPrice, ZZPeriod, K1, K2, K3, _DrawW*es, BuyColor, SellColor, W*esWidth, DrawTarget, TargetWidth, BuyTargetColor, SellTargetColor); // 检查指标是否已成功加载 if(h==INVALID_HANDLE){ Print("不能加载指标"); return(INIT_FAILED); } // 定义 EA 应在哪根柱线上检查指示箭头 if(SrcSelect==Src_HighLow){ Shift=0; } else{ Shift=1; } return(INIT_SUCCEEDED); }
我们来转入 OnTick() 函数。EA 必须能够与柱线和即时报价操作。我们添加变量, 保存形成中柱线和最后一根已处理柱线的时间 (变量在 OnTick() 函数中声明):
datetime tm[1]; // 形成中柱线的时间static datetime lt; // 最后已处理柱线的时间
获取最后 (形成中) 柱线的时间:
if(CopyTime(Symbol(),Period(),0,1,tm)<=0)return;
检查柱线时间:
if(Shift==0 || tm[0]!=lt){
如果 Shift==0, 则 EA 在每次即时报价时操作。否则, 如果 lt 变量不等于形成中柱线的时间 (每根柱线计算一次)。
声明辅助变量并获取指标值:
double tp,sl; // 用于计算止损和止盈的变量double buf_buy[1]; // 买入箭头double buf_sell[1]; // 卖出箭头double buf_buy_target[1]; // 买入目标double buf_sell_target[1]; // 卖出目标if(CopyBuffer(h,0,Shift,1,buf_buy)<=0)return;if(CopyBuffer(h,1,Shift,1,buf_sell)<=0)return;if(CopyBuffer(h,2,Shift,1,buf_buy_target)<=0)return;if(CopyBuffer(h,3,Shift,1,buf_sell_target)<=0)return;
如果有交易信号, 计算止损和止盈, 并开仓:
// 有一个箭头, 此柱线上没有开仓if(buf_buy[0]!=EMPTY_VALUE && LastBuyTime!=tm[0]){ // 止损和止盈 if(FixedSLTP){ tp=SymbolInfoDouble(Symbol(),SYMBOL_ASK)+_Point*TakeProfit; sl=SymbolInfoDouble(Symbol(),SYMBOL_ASK)-_Point*StopLoss; } else{ tp=NormalizeDouble(buf_buy_target[0],_Digits); double ask=SymbolInfoDouble(Symbol(),SYMBOL_ASK); sl=NormalizeDouble(ask-StopLoss_K*(tp-ask),_Digits); } // 开仓 if(!trade.Buy(0.1,Symbol(),0,sl,tp))return; // "记住" 最后开仓时间 LastBuyTime=tm[0]; }
如果使用固定 SL 和 TP (FixedStopLoss=true), 止盈位的计算是 TakeProfit 变量的值乘以 _Point 加上开仓价 (买入时的采购价)。若要计算止损, 从开仓价里减去 StopLoss 变量的值乘以 _Point。计算之后, 结果值要使用 NormalizeDouble() 函数进行规整化, 小数位要与品种报价的小数点后位数相同 (此数字可使用 _Digits 变量获取)。
如果 SL 和 TP 不是固定的, 我们首先检测止盈的值, 然后计算止损。如果开仓失败, 则 OnTick() 函数将被终止, 下一此即时报价来临时再尝试开仓。只要存在指标信号, 即在一根柱线中, 将始终进行尝试。如果开仓成功, 当前柱线的时间将被分配给 LastBuyTime 变量, 以避免在同一根柱线上重复开仓 (当使用即时报价, 即 Shift = 0) 时。LastBuyTime 是专家交易系统的全局变量。
卖出类似于买入, 仅有几个修正:
// 有一个箭头, 这根柱线尚未开仓if(buf_sell[0]!=EMPTY_VALUE && LastSellTime!=tm[0]){ // 止损和止盈 if(FixedSLTP){ tp=SymbolInfoDouble(Symbol(),SYMBOL_BID)-_Point*TakeProfit; sl=SymbolInfoDouble(Symbol(),SYMBOL_BID)+_Point*StopLoss; } else{ tp=NormalizeDouble(buf_sell_target[0],_Digits); double bid=SymbolInfoDouble(Symbol(),SYMBOL_ASK); sl=NormalizeDouble(bid+StopLoss_K*(bid-tp),_Digits); } // 开仓 if(!trade.Sell(0.1,Symbol(),0,sl,tp))return; // "记住" 最后开仓时间 LastSellTime=tm[0]; }
对于卖出交易, 使用 LastSellTime 而不是 LastBuyTime 变量, 并且使用供给价计算止损/止盈位。
最后, 形成中的柱线时间分配给 lt 变量, 以防止 EA 在同一根柱线上执行任何操作 (如果 EA 设置为在每根柱线上操作, 即Shift = 1)。以下是 OnTick() 函数的整体代码:
void OnTick(){ datetime tm[1]; // 形成中柱线的时间 static datetime lt; // 最后已处理柱线的时间 // 复制时间 if(CopyTime(Symbol(),Period(),0,1,tm)<=0)return; if(Shift==0 || tm[0]!=lt){ // 检查 EA 操作是否为每根柱线 double tp,sl; // 用于计算止损和止盈的变量 double buf_buy[1]; // 买入箭头 double buf_sell[1]; // 卖出箭头 double buf_buy_target[1]; // 买入目标 double buf_sell_target[1]; // 卖出目标 if(CopyBuffer(h,0,Shift,1,buf_buy)<=0)return; if(CopyBuffer(h,1,Shift,1,buf_sell)<=0)return; if(CopyBuffer(h,2,Shift,1,buf_buy_target)<=0)return; if(CopyBuffer(h,3,Shift,1,buf_sell_target)<=0)return; // 有一个箭头, 此柱线上没有开仓 if(buf_buy[0]!=EMPTY_VALUE && LastBuyTime!=tm[0]){ // 止损和止盈 if(FixedSLTP){ tp=SymbolInfoDouble(Symbol(),SYMBOL_ASK)+_Point*TakeProfit; sl=SymbolInfoDouble(Symbol(),SYMBOL_ASK)-_Point*StopLoss; } else{ tp=NormalizeDouble(buf_buy_target[0],_Digits); double ask=SymbolInfoDouble(Symbol(),SYMBOL_ASK); sl=NormalizeDouble(ask-StopLoss_K*(tp-ask),_Digits); } // 开仓 if(!trade.Buy(0.1,Symbol(),0,sl,tp))return; // "记住" 最后开仓时间 LastBuyTime=tm[0]; } // 有一个箭头, 这根柱线尚未开仓 if(buf_sell[0]!=EMPTY_VALUE && LastSellTime!=tm[0]){ // 止损和止盈 if(FixedSLTP){ tp=SymbolInfoDouble(Symbol(),SYMBOL_BID)-_Point*TakeProfit; sl=SymbolInfoDouble(Symbol(),SYMBOL_BID)+_Point*StopLoss; } else{ tp=NormalizeDouble(buf_sell_target[0],_Digits); double bid=SymbolInfoDouble(Symbol(),SYMBOL_ASK); sl=NormalizeDouble(bid+StopLoss_K*(bid-tp),_Digits); } // 开仓 if(!trade.Sell(0.1,Symbol(),0,sl,tp))return; // "记住" 最后开仓时间 LastSellTime=tm[0]; } lt=tm[0]; } }
专家交易系统代码可在附带的 eWolfeW*es 文件中找到。
我们来测试由此产生的专家交易系统。如果您在测试后将指标加载到图表上, 您可以看到 EA 在每个箭头处入场, 无论是否有持仓 (图例, 5)。
图例. 5. 专家交易系统在每个指标箭头处入场
当然, 我们首先对交易指标的有效性感兴趣。使用整个 EURUSD H1 历史数据, 省缺设置, EA 的测试结果如图例. 6 所示。
图例. 6. 在整个 EURUSD H1 历史数据上的 EA 测试结果
在测试区间的开始, 有一个显著的下降, 这可能与初期的历史数据质量较低有关。此后, 大约从 1991 年开始, 稳步增长的时期开始。总的来说, 测试结果是积极的, 即使未经优化和额外的检查。
更多来自比尔·沃尔夫所著书籍的提示
除了波形检测规则, 比尔·沃尔夫提供了一些提示, 他称之为 "心理和技术论调"。最重要的技术论调之一是监测即时报价的交易量的建议: 它在反转点会减少, 这种减少可能表明将要逆转。第二个建议是跟随趋势线。比尔·沃尔夫 发现的波形走势经常发生在趋势突破之后, 即突破趋势线之后。即, 趋势突破后出现的波形更可靠。第三个建议是监测线 1-4, 特别是点 4, 如果发生任何不可预见的事件, 则要退出: 反向波形, 交易量强劲增长, 或快速获利的情况。
结论
专家交易系统测试的积极结果 (即使使用省缺设置) 表明, 本文中讨论的图形分析方法绝对有效, 可能有兴趣进行进一步的调查。
一些读者可能想改善指标。在此刻, 指标外部参数有三个可变系数: K1, K2, K3。K1 用于检查点 3 相对于点 1 的位置, 以及点 4 相对于点 1 的位置。也许, 最好使用单独的系数进行这些检查。另一方面, 参数数量的增加使优化复杂化, 这已非优化而是提高了过度适应的风险。也许系数 K1 和 K2 的组合可能更好。这将令指标设置更加容易, 更易于理解。另一方面, 最好只保留一个系数。指标代码功能划分清晰, 使其更容易修改。每个人均可尝试以不同的方式修改指标。
除了使用指标搜索沃尔夫波形, 还可以在创建其它指标来搜索任何其它之字折线形态时, 将其用作模板。您只需要修改 CheckUp 和 CheckDn 函数的代码。最重要的是, 访问之字折线指标值的问题已经解决了。
我要特别提醒变量 CurCount, PreCount 和 LastTime 的技巧。这不仅是本文所分析的狭义问题的解决方案。在开发指标时, 我们经常需要额外的缓冲区保存中间计算过程中获得的辅助值。在每根柱线上, 将缓冲区前一个元素的值移动到缓冲区当前元素之内; 偶尔会改变这个值。一个元素的值用于计算, 而整个缓冲区正是用于此目的。使用两个变量可以显著减少指标使用的内存容量。
附件
本文中创建的指标文件和专家交易系统如下。文件应放在正确的文件夹中。它们应该保存到终端的相同文件夹中。附件中提供以下文件:
Indicators/iWolfeW*es_Step_1.mq5
Indicators/iWolfeW*es.mq5
Experts/eWolfeW*es.mq5
若要提供上述文件的操作, 您还需要从文章 "通用之字折线" 中下载文件:
Indicators/iUniZigZagSW.mq5
Include/CSorceData.mqh
Include/CZZDirection.mqh>
Include/CZZDraw.mqh