简介
很多流行的交易策略是基于对不同图形化模式的使用: 头肩,双顶/双底,等等。有些策略还会分析图表上极值点的背离。当自动化这样的交易系统时,就会有需要找到、处理和解释图表上的峰谷值,现有的工具不能使我们总是根据建立的标准来找到极值点,本文展示了根据价格变化,在价格图表上找到和处理极值点的有效算法和程序方案。
1. 已有的搜索极值点的工具
1.1. 分形和类似的工具分形(Fractals)是用于找到极值点的流行工具,它们可以在5个柱的序列中找到价格的高点和低点(图 1)。极值点在价格变化强弱的情况下都可以定义,如果正确选择了时段,分形可能会显示很好的结果,尽管它们被市场条件的影响很大。
图 1. 使用分形的结果: 当存在趋势时,极值点的相对距离大小是从 140 到 420 个点值(pips) (a), 在平盘时期,相对的大小不大于 50 个点值 (b)
在第二种情况下,极值点的相对大小 (从一个极值点到另一个的价格变化) 可能只有几个点值,这样较小的峰谷值通常在人工交易时不做考虑。在时段之间切换并不会改变这些较小的极值点 — 它们在长期平盘时依然出现。
也可能会出现相反的情况: 没有找到所有的极值点。如果出现了剧烈的市场波动,在短期之内出现了很多峰谷值,它们也不能被发现,分形只能在当前时段由5个柱定义的时间段之内侦测到两个极值点。所以,我们无法推荐在自动交易中使用分形指标来侦测所有或者主要关键的极值点。
在文章"Thomas DeMark 对技术分析的贡献" 一文中指出了分形指标的同样缺点。如果我们选择了一个大的范围来搜索极值点,它们中的很多都会被忽略掉。如果范围太小,又会找到一些微小的极值点。在任何情况下,当处理结果时,我们或者必须总是人工优化参数来消除那些微小的高价和低价,或者开发一个特别的算法来做这件事。
1.2. 当搜索极值点时使用移动平均
使用平均线,例如移动平均,作为自动化搜索极值点的基础看起来是可行的。搜索是在指定数量的柱上进行的,看价格偏离平均线是否大于预先定义的距离点数。该工具可以排除掉微小的峰谷值,看起来比分形更好。但是,它还是没有解决在高低价格距离很近时侦测的问题 (图 2, a).
图 2. 当搜索极值点时使用移动平均: 两个极值点定义为一个 (a), 距离移动平均很近的极值点就被忽略了 (b)
我们可以一起使用移动平均和分形,移动平均用于排除掉微小的极值点,而分形用于在指定的区段中进行搜索。但是,这种方法还是不能解决所有的问题,我们还是需要不断选择最佳的范围参数,否则,两个很近的极值点将只能发现一个 (图 2, a).
使用这种方法还有另一个问题,在强烈波动中,移动平均根据时段可能会忽略掉信号,在这种情况下(图 2, b), 接近两个峰值的谷值和接近移动平均的部分没有被侦测出来,这样的情形在市场上很罕见,但是它们确实提出了正确选择移动平均范围的问题。
所以,这种搜索极值点的方法以及它们上面所述的修改方案都是有缺点的,需要进一步的编程方案。让我们详细探讨当搜索极值点时出现的问题,以及来解决它们的算法。
2. 搜索极值点时遇到的问题和乱局
2.1. 选择用于搜索峰值和谷值的变化范围已有的策略和技巧可能显式或者隐式地使用了极值点。寻找极值点常常是一项必须的任务:不同的人可能会在同一张图表上找到不同的峰值和谷值。让我们看看一个著名的图形模式 – 双顶。
图 3. 双顶模式
两个图表 (图 3) 中包含了相同的模式,但是,我们可能会根据极值点范围的不同侦测到或者未能找到它。在第一个图表上,第一个峰值之后是底部,其后又是第二个峰值。对应地,如果峰值之间没有底部,我们就不能侦测到双顶模式了,该模式会被定义为一个普通的极值点。当底部不明显时,也会发生同样的事情,它会影响到双顶模式而使它难以侦测。当然,在第一个图表上侦测模式与第二个图表相比要容易,而它们之间的仅有区别就是它们相邻极值点的区别。
让我们讨论另一个例子: 有些策略会在一系列极值点(包括高点和低点)的位置高于前面的点时定义向上的趋势,下行趋势的定义也类似。在图4中, 我们可以使用极值点来定义趋势的方向。
图 4. 相同图表上价格的相反方向走势: 向上趋势 (a), 向下趋势 (b)
同一个图表上同时包含了向上趋势和向下趋势,在第一种情况中(图 4, a), 极值点 1, 2, 3 和 4 明显显示了牛市趋势,然而,如果我们使用极值点 2, 5, 6 和 3 (图 4, b), 我们将看到一个熊势趋势。所以,有可能使用不同的极值点来取得其中一个可能的结果,考虑到这个,我们可以得出结论,变化范围对极值点的位置有最大影响。
2.2. 有效分离临近的顶部或者底部当定义极值点时还会产生另一个问题,为了有效定义和分离两个或者更多的顶部,它们之间应该有底部,这在第一个例子(寻找顶部和底部)和第二个例子都是对的,尽管这里的例子更有趣一些,根据所描述的策略,我们可以在下面的图表中(图 5, 6) 在找到极值点后侦测到趋势。
图 5. 在长线投资中侦测顶部和底部
图 6. 侦测小的顶部和底部
如果没有底部把顶部分开(或者相反), 策略就无法根据指定的标准来工作,即使可以在图表上看到向上的趋势。让我们探讨一个典型例子,在向上的趋势中,每个顶部都比之前的一个更高,如果它们之间没有底部或者不能清楚看到,就只有最高的顶点定义为一个极值点,如果相对平均线(例如移动平均线)来定义极值点, 还是需要有分离相邻两个顶部或者底部的任务,为了分出两个顶部,我们应该使用它们之间的一个极值点。
所以,我们可以在显式或者隐式使用极值点的所有策略中使用以下假定: 无论是向前(向将来)还是向后,价格都会从顶部移动到底部,再由底部移动到顶部。如果我们不使用这个假定,那么根据观察的角度,价格图表上的两个顶部:
- 或者被侦测出来,
- 或者只有最高的顶部被侦测到,
- 或者它们中的哪个都没有侦测到。
对于底部也是同样。这个假定使我们可以使用选定的变化范围来开发准确的搜索极值点的算法。
2.3. 定义第一个极值点第三个问题也是与价格变化相关的,并且发生于定义第一个极值点的时候。对于任何交易技巧或者策略,最近的极值点比更早的极值点更加重要,我们已经发现,定义一个极值点就会影响邻近顶部和底部的位置,所以,如果我们在离当前时间一定距离选择一个极值点,取得的结果比距离更远的历史数据影响更大,并且被最近价格波动的影响会最小。这个问题在使用之字转向指标(ZigZag)的时候也会出现,最近极值点的位置不很依赖于最近的价格波动。
然而,这种情形在从图表末端搜索极值点时就完全不同了,在这种情况下,我们应该首先在距离图表末端最近的地方找到一个顶部或者底部,然后所有其他的极值点就能清晰定义了。根据使用的策略和选择的变化范围,可以使用三个选项:
- 找到最近的顶部,
- 找到最近的底部,
- 找到最近的极值点(顶部或者底部).
让我们讨论找到最近的极值点,在选择了某个变化范围之后,我们就能准确定义最近的第一个极值点了。然而,这会出现一定的延迟,可能对策略的运行有负面的影响。为了 "看到" 一个极值点,我们需要根据相对那个点的变化范围定义价格的改变,价格的变化需要花费一些时间,所以就有了延迟。我们也可以使用最后的已知价格作为一个极值点,尽管不太可能它真的会变成顶部或者底部。
在这种情况下,看起来使用另外的比例作为变化范围的一部分来寻找其它的极值点比较合理,例如,让我们选择 0.5 的数值,
选择的另外的比例值定义了从当前值到最近底部的最小价格 (对于最近的顶部就是最高价格),这使我们可以把这个底部 (顶部) 定义为一个极值点,如果当前的价格和最近顶部(底部)的差距小于指定的数值,这样的极值点就不成立。在这种情况下,侦测到的第一个极值点可能就是顶部或者底部。同时,我们也解决了过早检测到极值点的问题,以及随后对它们的分析和(如有必要)开展交易。
让我们探讨一个例子,变化范围设为140个点值(pips),将要使用一个另外的比例来侦测极值点。在第一个例子中,它等于 0.9 (图 7, a), 在第二个例子中, 它是 0.7 (图 7, b)。在此,另外的比例值定义了使我们侦测第一个极值点的最小的价格变化点数,在第一个例子中,变化是 126 个点值,而在第二个例子中,它是 98 个点值,在两个例子中使用的是同一张图表。垂直线指出了进行计算时当前的时段。时段内侦测到的极值点以点状显示。
图 7. 额外比例在定义极值点中的影响: 对于值等于 0.9 (126 个点值), 第一个极值点是在变化 205 个点值找到的 (a), 对于值等于 0.7 (98 个点值), 第一个极值点是在变化120个点值找到的, 而其余的两个点是根据指定范围找到的 (b)
对于第一种情况,选择的额外比例值定义的第一个底部范围是205个点值, 而最小的价格变化是点值,对于第二种情况,如果额外的比例值等于0.7 (98个点值), 第一个底部定义在距离当前价格120个点值的位置。随后的两个极值点事根据指定的变化范围等于140个点值来侦测到的。相应地,第一个底部和随后的顶部的差距略微超过140个点值。第二个底部也是根据价格相对侦测到的顶部超过140个点值来定义的。
我们可以看到,额外比例会明显影响第一个侦测到的极值点的位置,它也可能会影响到它的类型。对于不同的数值(从 0 到 1), 在相同的图表上可能侦测到顶部或者底部,第二个例子中侦测到的前两个极值点 (图 7 b), 就没有在第一个例子中侦测到。
对于更低的比例值,第一个极值点会更快找到。在第二个例子中 (图 7 b), 是用额外比例等于0.4, 第一个侦测到的极值点可以提前5个柱定义 (在当前的时段下是提前5分钟).
3. 搜索极值点任务的算法方案和它们的实现
让我们从选择价格范围来构建极值点开始,显然,柱的大小和极值点的参数非常依赖于时段而有很大变化,存在以及没有顶部/底部也会被趋势、一天中的时间以及其它一些因素所影响。已有的指标,例如分形和类似的工具,使我们可以在任何时段中不论趋势存在与否而找到极值点。如果我们在搜索顶部和底部值的时候使用了移动平均,极值点相对移动平均也许只差两点,也可能差100点。我们在日内交易中应该注意两个点的极值吗?也许不应该。对于一个长线投资,我们同样不会关心小于20个点的极值,不论时段如何,
这就是为什么我们需要“变化范围(variation range)”一词,意思是最小值。移动平均可以作为参考点,使得我们可以定义极值点的距离以限制它的最小值。然而,移动平均的周期数会明显地影响所侦测的顶部和底部的位置,使得难以选择某个周期数作为参考。
所以,让我们现在假定价格从顶部跌到底部然后再回来,这个变化范围就用来定义两个相邻极值点的最小价格变化 - 顶部和底部的距离。如果一些极值点已经被定义,其邻近点的距离应该不小于指定的变化范围,这使得我们可以不管时段和趋势来定义极值点。该工具对于日内交易和长线投资都非常适合,
让我们探讨它的运行算法。首先,让我们使用相同的图表显式地定义极值点,只是在第一个图表上,变化范围是60个点值 (图 8), 而在第二个图表上是30个点值(图 9)。让我们假定第一个极值点已经被侦测到 (点 1) 儿我们正搜索前面的极值点。
图 8. 使用60个点值的变化范围
图 9. 使用30个点值的变化范围
对极值点的搜索是从图表的末端开始进行的 (从点1)。在第一种情况下,在显示的范围内找到了4个极值点,在第二种情况下,在同样的时间间隔内找到了10个极值点。当在图表的指定部分加大变化范围时,极值点根本没有侦测出来,所以,在选择极值点的搜索范围时我们应该现实些,要考虑到市场的波动和时段。这里,范围是进行搜索的柱的数量。
记住以上我们所说过的,让我们介绍搜索极值点的迭代算法。为什么要迭代?第一个顶部后面总应该有个底部,然后是第二个顶部,等等。如果没有找到第二个顶部 (图表没有向上方移动), 就重新定义底部的位置,然后再转向时间序列中更远的地方。第一个顶部的位置 (其它极值点也一样) 也可以使用同样的方法修改。我们也应该去掉同一个柱被定义为同时是顶部和底部的情况。
当然,这种方法需要大量的计算,我建议在搜索几个极值点的时候使用它,点的数量越少,程序运行就越快,计算速度也受到搜索范围的影响。这种搜索是要验证的,因为它使您在最近的价格波动中找到影响最大的某些顶部和底部,如果您需要找到多个极值点,我推荐使用之字转向指标(ZigZag)。
3.2指标的实现下面展示的迭代算法代码使用了少量的迭代以获得更好的效率,这种简化不会造成极值点侦测质量的很大损失,主要的输入参数 — 搜索极值点的点数范围和变化范围。
input double delta_points=160; // 定义的最小的顶部和底部距离点数的变化范围
input double first_extrem=0.9; // 额外的用语搜索第一个极值的比例
input double reload_time=5; // 时间间隔,重新计算指标值的秒数
程序体重包含了三个嵌套的循环,用于定义四个极值点。在程序的这个部分中只包含定义第一个底部和相关极值点,定义第一个顶部和相关极值点是使用类似方法实现的。
datetime Time[];
ArraySetAsSeries(Low,true);
int copied1=CopyLow(Symbol(),0,0,bars+2,Low);
ArraySetAsSeries(High,true);
int copied2=CopyHigh(Symbol(),0,0,bars+2,High);
ArraySetAsSeries(Time,true);
int copied3=CopyTime(Symbol(),0,0,bars+2,Time);
double delta=delta_points*Point(); // 极值点之间差距的变化范围
int j,k,l;
int j2,k2,l2;
double j1,k1,l1;
int min[6]; // 定义底部的数组,数值对应着侦测出的极值点的柱的索引
int max[6]; // 定义顶部的数组,数值对应着侦测出的极值点的柱的索引
int mag1=bars;
int mag2=bars;
int mag3=bars;
int mag4=bars;
j1=SymbolInfoDouble(Symbol(),SYMBOL_BID)+(1-first_extrem)*delta_points*Point();
// 当搜索第一个极值点时,另外的比例变量定义了最小价格,第一个底部就是位于低于它的位置
j2=0; // 在第一个迭代中,搜索是从最新的历史柱上开始进行的
for(j=0;j<=15;j++) // 循环定义第一个底部 - min[1]
{
min[1]=minimum(j2,bars,j1);
//在指定的时间段中定义最近的底部
j2=min[1]+1; // 在下一次迭代中,搜索是从已经侦测出的底部 min[1] 开始进行的
j1=Low[min[1]]+delta;
//在随后的迭代中找到的底部的最低价应该低于当前迭代中找到的底部的最低价
k1=Low[min[1]];
//当搜索下一个极值点时找到的最高价应该低于底部的最低价,这样才应该是顶部。
k2=min[1]; //搜索位于底部之后的顶部,从侦测的底部 min[1]开始进行
for(k=0;k<=12;k++) // 循环定义第一个顶部 - max[1]
{
max[1]=maximum(k2,bars,k1);
//--- 定义指定时间段内最近的顶部
k1=High[max[1]]-delta;
//下一个迭代的最高价应该超过当前迭代侦测出的顶部的最高价
k2=max[1]+1; // 在下一次迭代中,搜索时从已经侦测到的顶部 max[1] 开始进行的
l1=High[max[1]];
//当搜索下一个底部最低价时使用的极值点的最高价,底部应该低于这个价格
l2=max[1]; // 搜索顶部之后的底部,应该从已侦测的顶部 max[1] 开始进行
for(l=0;l<=10;l++) // 循环定义第二个底部 - min[2] 以及第二个顶部 max[2]
{
min[2]=minimum(l2,bars,l1);
//---在指定的时间段内定义最近的底部
l1=Low[min[2]]+delta;
//用于在随后迭代中侦测的底部的最低价应该低于当前迭代中底部的最低价
l2=min[2]+1; // 在下一次迭代中,搜索是从已经侦测到的底部 min[2] 开始进行的
max[2]=maximum(min[2],bars,Low[min[2]]);
//在指定的时间间隔内定义最近的顶部
if(max[1]>min[1] && min[1]>0 && min[2]>max[1] && min[2]<max[2] && max[2]<mag4)
//排除掉冲突的极值和特殊状况
{
mag1=min[1]; //在每次迭代中,侦测到的极值位置在条件符合时要保存
mag2=max[1];
mag3=min[2];
mag4=max[2];
}
}
}
}
min[1]=mag1; // 定义了极值点,否则所有变量设为 'bars' 的数值
max[1]=mag2;
min[2]=mag3;
max[2]=mag4;
在最近的柱中搜索低于指定数值的最低价(或者超过指定值的最高价)是非常简单的人物,把它放到一个独立的函数中。
//本函数在指定的时间段内定义最近的底部. 底部的位置低于 price0 一定的距离,大于变化范围
{
double High[],Low[];
ArraySetAsSeries(Low,true);
int copied4=CopyLow(Symbol(),0,0,bars+2,Low);
int i,e;
e=bars;
double pr=price0-delta_points*Point(); // 价格应该低于算上变化范围的底部之下
for(i=a;i<=b;i++) // 在由a和b参数指定的范围内搜索底部
{
if(Low[i]<pr && Low[i]<Low[i+1]) // 定义最近的底部,之后价格开始上升
{
e=i;
break;
}
}
return(e);
}
int maximum(int a,int b,double price1)
//--- 本函数在指定的时间段内定义最近的顶部. 顶部要高于 price1 比变化范围大的距离
{
double High[],Low[];
ArraySetAsSeries(High,true);
int copied5=CopyHigh(Symbol(),0,0,bars+2,High);
int i,e;
e=bars;
double pr1=price1+delta_points*Point(); // 价格应该高于顶部加上变化范围的距离
for(i=a;i<=b;i++) // 在由参数a和b指定的范围内搜索顶部
{
if(High[i]>pr1 && High[i]>High[i+1]) // 定义最近的顶部,之后价格开始下跌
{
e=i;
break;
}
}
return(e);
}
寻找极值点的任务解决了,但是只是第一次尝试。我们应该考虑到,使用这种算法找到的顶部(位于两个底部之间)可能不是在指定算法中找到的最高点,因为搜索是从图表的末端开始的,顶部和底部的位置应该从后面开始算第一个,第二个,第三个以及后续的极值点。顶部和底部位置的验证和修改是在独立的函数中完成的。极值点位置修正的实现看起来如下:
min[1]=check_min(min[1],max[1]); // 验证和修改在指定时间段中的第一个底部的位置
max[1]=check_max(max[1],min[2]); // 验证和修改在指定时间段中的第一个顶部的位置
min[2]=check_min(min[2],max[2]); // 验证和修改在指定时间段中的第二个底部的位置
int check_min(int a,int b)
// 用于验证和修改在指定时间段内的底部位置
{
double High[],Low[];
ArraySetAsSeries(Low,true);
int copied6=CopyLow(Symbol(),0,0,bars+1,Low);
int i,c;
c=a;
for(i=a+1;i<b;i++) // 当从底部开始搜索时,所有由范围指定的柱都要验证
{
if(Low[i]<Low[a] && Low[i]<Low[c]) // 如果找到了更低的底部
c=i; // 重新定义底部的位置
}
return(c);
}
//--- 本函数在指定的时间间隔内验证和修改顶部的位置
{
double High[],Low[];
ArraySetAsSeries(High,true);
int copied7=CopyHigh(Symbol(),0,0,bars+1,High);
int i,d;
d=a;
for(i=(a+1);i<b;i++) // 当搜索顶部时,范围内的所有柱都要验证
{
if(High[i]>High[a] && High[i]>High[d]) // 如果找到了更高的顶部
d=i; // 顶部位置被重新定义
}
return(d);
}
如果找到了4个极值点,我们只需要验证前三个点的位置。验证和修改函数在当前极值点的范围内工作,使用它自己的位置和后面极值点的位置。在验证之后,我们可以确定找到对应着设置标准的极值点了。
在那以后,从图表的末尾搜索第一个顶部,并把第一个顶部和底部作比较,进行计算之后,我们就得到第一个极值点的位置以及和图表末端最接近的相关极值点。
让我们再次探讨找到第一个极值点的过程,我已经介绍了额外搜索比例 — 变化范围的分数部分,例如 0.7。同时,它的数值更大 (0.8…0.9) 可以使我们更准确地定义第一个极值点,会有少许延迟,而它的数值越小 (0.1…0.25) 则会尽量减少延迟,但是准确度会明显下降。所以,应该根据使用的策略来选择额外比例的值。
侦测到的顶部和底部使用箭头来显示,箭头显示了极值点的坐标 (时间序列和侦测到的顶部/底部的最高价/最低价)。因为这需要许多计算,程序有功能设置输入参数来设置时间间隔来重新计算指标值。如果没有找到顶部和底部,指标会生成相应的消息。极值点图形显示的实现看起来如下:
if(min[1]<Max[1]) // 如果底部距离更近,显示它的位置以及相关极值的数值
{
ObjectDelete(0,"id_1"); // 删除前一阶段生成的标签
ObjectDelete(0,"id_2");
ObjectDelete(0,"id_3");
ObjectDelete(0,"id_4");
ObjectDelete(0,"id_5");
ObjectDelete(0,"id_6");
ObjectCreate(0,"id_1",OBJ_ARROW_UP,0,Time[min[1]],Low[min[1]]); // 突出显示第一个底部
ObjectSetInteger(0,"id_1",OBJPROP_ANCHOR,ANCHOR_TOP);
//--- 对于第一个侦测到的底部,根据它在时间序列的位置和最低价进行绑定
ObjectCreate(0,"id_2",OBJ_ARROW_DOWN,0,Time[max[1]],High[max[1]]); // 突出显示第一个顶部
ObjectSetInteger(0,"id_2",OBJPROP_ANCHOR,ANCHOR_BOTTOM);
//--- 对于侦测到的顶部,根据它在时间序列的位置和最高价进行绑定
ObjectCreate(0,"id_3",OBJ_ARROW_UP,0,Time[min[2]],Low[min[2]]); // 突出显示第二个底部
ObjectSetInteger(0,"id_3",OBJPROP_ANCHOR,ANCHOR_TOP);
//--- 对于第二个侦测到的底部,根据它在时间序列中的位置和最低价进行绑定
}
if(min[1]>Max[1]) // 如果顶部位置更近,显示它的位置和相关极值的数值
{
ObjectDelete(0,"id_1"); // 删除前一阶段的标签
ObjectDelete(0,"id_2");
ObjectDelete(0,"id_3");
ObjectDelete(0,"id_4");
ObjectDelete(0,"id_5");
ObjectDelete(0,"id_6");
ObjectCreate(0,"id_4",OBJ_ARROW_DOWN,0,Time[Max[1]],High[Max[1]]); // 定义第一个顶部
ObjectSetInteger(0,"id_4",OBJPROP_ANCHOR,ANCHOR_BOTTOM);
//对于第一个侦测到的顶部,根据它在时间序列的位置和最高价绑定
ObjectCreate(0,"id_5",OBJ_ARROW_UP,0,Time[Min[1]],Low[Min[1]]); // 突出显示第一个底部
ObjectSetInteger(0,"id_5",OBJPROP_ANCHOR,ANCHOR_TOP);
//对于侦测到的底部,根据它在时间序列的位置和最低价做绑定
ObjectCreate(0,"id_6",OBJ_ARROW_DOWN,0,Time[Max[2]],High[Max[2]]); // 定义第二个顶部
ObjectSetInteger(0,"id_6",OBJPROP_ANCHOR,ANCHOR_BOTTOM);
//对于第二个侦测到的顶部,根据它在时间序列中的位置和最高价进行绑定
}
if(min[1]==Max[1]) Alert("在指定的范围, ",bars," 没有找到极值点");
// 如果没有找到极值点,显示对应消息
当删除指标时,定义顶部和底部的对象也会删除。
提供的算法已经用于开发自定义指标来搜索极值点并且在图表上突出显示它们 (图 10).
图 10. 指标的运行结果: 变化范围为 120 点值 (a), 变化范围为 160 点值 (b)
取得的结果是根据变化范围定义的,对于 120 点值 或者更少的数值 (图10, a), 极值点相互之间很接近,而范围的大小不是很关键了,对于 160 点值 和更多的 (图 10, b), 极值点之间的距离就比较远了。这在选择搜索范围的时候应该注意。对于平盘市场,最优选择的范围使我们可以在较小价格变化时自动找到顶部和底部,并且去除(跳过)时间间隔非常大的极值点。
3.3实现 MACD 柱形图与价格背离策略的 EA 交易提供的算法可以用于实现各种策略,scale_factor 指标的运行结果很适合用于构造图形模式,例如头肩,双底,等等。它们可以用于使用图表上顶部和底部的价格差异的策略的指标。其中EA交易的例子是遵从图表上 MACD 柱形图背离的策略,该策略在文章中有详细描述 (参见 Alexander Elder 的 "交易生活")。
根据这个策略,如果价格上升形成新的顶部,高于前一格顶部,但是 MACD 的顶部低于前一个,我们就有了卖出信号。
如果价格下跌形成新的底部,低于前一个底部,但是 MACD 的底部高于前一个,我们就有了买入信号。signal.
EA 准确实现了算法,根据变化范围侦测到了顶部和底部,集中于图表上的最近变化。
传入的参数 — 用于搜索极值点的范围和变化范围。还有必要来设置在价格上涨时最近两个顶部的最小价格差异(对于价格下跌就是最近两个底部),MACD柱形图在极值点的最小背离。每次交易的风险以及额外比例是在存款货币中设置的。guard_points 参数定义了止损的偏移,如果是买入仓位就是距离最近底部的偏移,相应地,对于卖出仓位就是顶部上方的偏移。还可以选择在进行交易时显示侦测到的极值点的参数 (show_info=1).
input double delta_points=160; // 定义的最小的顶部和底部距离点数的变化范围
input double first_extrem=0.9; // 额外的用语搜索第一个极值的比例
input int orderr_size=10; // 每个交易的风险
input double macd_t=0.00002; // MACD 柱形图偏移最小值
input double trend=100; // 最近的两个顶部/底部的最小价格偏差
input double guard_points=30; // 止损的偏移
input int time=0; // 延迟时间的秒数
input int show_info=0; // 显示有关极值点的数据
计算可以在每次订单时刻时计算,对于交易也是一样,该策略即使是有时间的延迟也能工作得很好。在定义了主要的参数后,我们应该转到极值点的搜索了,程序的第一部分让我们在第一个极值点是底部的时候搜索极值点,随后,它们的状态会被验证。第二部分的代码使我们在图表末端首先出现顶部的时候搜索极值点,下一步是验证顶部和底部的参数。
{
Sleep(1000*time); // 引入时间延迟
double High[],Low[];
ArraySetAsSeries(Low,true);
int copied1=CopyLow(Symbol(),0,0,bars+2,Low);
ArraySetAsSeries(High,true);
int copied2=CopyHigh(Symbol(),0,0,bars+2,High);
ArraySetAsSeries(Time,true);
int copied3=CopyTime(Symbol(),0,0,bars+2,Time);
MqlTick last_tick;
double Bid=last_tick.bid;
double Ask=last_tick.ask;
double delta=delta_points*Point(); // 变化范围的绝对值
double trendd=trend*Point(); // 最近的两个顶部/底部偏差的绝对值
double guard=guard_points*Point(); // 止损偏移的绝对值
int j,k,l;
int j2,k2,l2;
double j1,k1,l1;
int min[6]; // 如果第一个侦测到的极值是底部,定义底部的数组,该数值对应着侦测到的极值点的柱的索引
int max[6]; // 如果第一个侦测到的极值是底部,定义顶部的数组,该数值对应着侦测到的极值点的柱的索引
int Min[6]; // 如果第一个侦测到的极值是顶部,定义底部的数组,该数值对应着侦测到的极值点的柱的索引
int Max[6]; // 如果第一个侦测到的极值是底部,定义顶部的数组,该数值对应着侦测到的极值点的柱的索引
int mag1=bars;
int mag2=bars;
int mag3=bars;
int mag4=bars;
j1=SymbolInfoDouble(Symbol(),SYMBOL_BID)+(1-first_extrem)*delta_points*Point();
// 当搜索第一个极值点时,额外比例定义了第一个底部的最小价格位置
j2=0; // 在第一次迭代中,搜索是从最后面的历史柱开始进行的
for(j=0;j<=15;j++) // 循环定义第一个底部 - min[1]
{
min[1]=minimum(j2,bars,j1);
//在指定的时间间隔中定义最近的底部
j2=min[1]+1; //在下一次迭代中,搜索时从已经侦测到的底部 min[1] 开始进行的
j1=Low[min[1]]+delta;
//--- 随后的迭代中所侦测的底部的最低价应该比当前迭代中找到的底部最低价更低
k1=Low[min[1]];
//搜索到的底部的最低价,下面搜索的顶部的极值点的最高价格应该在此之上
k2=min[1]; // 搜索底部之后的顶部,从侦测到的底部 min[1] 开始进行
for(k=0;k<=12;k++) // 循环定义第一个顶部 - max[1]
{
max[1]=maximum(k2,bars,k1);
//--- 在指定的时间间隔内定义最近的顶部
k1=High[max[1]]-delta;
//--- 下次迭代的最高价应该高于当前迭代中侦测到的顶部的最高价
k2=max[1]+1; // 在下一次迭代中,搜索是从已经侦测到的顶部 max[1] 开始的
l1=High[max[1]];
//--- 极值点的最高价,当搜索下个底部的最低价时,位置应该低于这个值
l2=max[1]; // 搜索底部应该是从已经侦测到的顶部 max[1] 开始的
for(l=0;l<=10;l++) // 循环定义第二个底部 - min[2] 以及第二个顶部 max[2]
{
min[2]=minimum(l2,bars,l1);
//--- 在指定时间段之内定义最近的底部
l1=Low[min[2]]+delta;
//在随后的迭代中用于搜索底部的最低价应该比当前迭代中找到的底部的最低价要低
l2=min[2]+1; //在下一次迭代中,搜索是从已经侦测到的底部 min[2] 开始的
max[2]=maximum(min[2],bars,Low[min[2]]);
//在指定的时间间隔内定义最近的顶部
if(max[1]>min[1] && min[1]>0 && min[2]>max[1] && min[2]<max[2] && max[2]<mag4)
//--- 排除掉冲突的极值和特殊状况
{
mag1=min[1]; // 在每次迭代中,所侦测到的极值在条件满足时要保存位置
mag2=max[1];
mag3=min[2];
mag4=max[2];
}
}
}
}
//--- 定义了极值点,否则对所有变量赋值为 'bars' 的值
min[1]=mag1;
max[1]=mag2;
min[2]=mag3;
max[2]=mag4;
//--- 在指定的时间间隔中验证和修改极值点的位置
max[1]=check_max(max[1],min[2]);
min[2]=check_min(min[2],max[2]);
//---------------------------------------------------------------------------------------------------------------
mag1=bars;
mag2=bars;
mag3=bars;
mag4=bars;
j1=SymbolInfoDouble(Symbol(),SYMBOL_BID)-(1-first_extrem)*delta_points*Point();
// 当搜索第一个极值点时,额外比例定义了最高价格,第一个顶部应该高于它
j2=0; // 在第一次迭代中,搜索是从最后的历史柱开始的
for(j=0;j<=15;j++) // 循环定义第一个顶部 - Max[1]
{
Max[1]=maximum(j2,bars,j1);
//在指定的时间间隔中定义最近的顶部
j1=High[Max[1]]-delta;
//下次迭代的最高价应该高于当前迭代中所侦测到的顶部的最高价
j2=Max[1]+1; // 在下一次迭代中,搜索是从已经侦测到的顶部 Max[1] 开始进行的
k1=High[Max[1]];
//极值点的最高价,当搜索下一个底部时,找到的底部的最低价应该低于这个价格
k2=Max[1]; // 搜索底部应该是从已经侦测到的顶部 max[1] 开始的
for(k=0;k<=12;k++) //循环定义第一个底部 - Min[1]
{
Min[1]=minimum(k2,bars,k1);
//--- 在指定的时间间隔中定义最近的底部
k1=Low[Min[1]]+delta;
//在随后的迭代中侦测到的底部的最低价应该低于当前迭代中找到的底部的最低价
k2=Min[1]+1; // 在下一次迭代中,搜索是从已经侦测到的底部 min[1] 开始的
l1=Low[Min[1]];
//---底部的最低价,当搜索下一个极值点时,顶部的最高价应该高于它
l2=Min[1]; // 对顶部的搜索应该是从已经侦测到的底部 min[1] 开始的
for(l=0;l<=10;l++)//循环定义第二个顶部 - Max[2] 以及第二个底部 Min[2]
{
Max[2]=maximum(l2,bars,l1);
//在指定的时间间隔内定义最近的顶部
l1=High[Max[2]]-delta;
//下次迭代的最高价应该高于当前迭代中侦测到的顶部的最高价
l2=Max[2]+1; //在下一次迭代中,搜索是从已经侦测到的顶部 Max[2] 开始的
Min[2]=minimum(Max[2],bars,High[Max[2]]);
//---在指定的时间段内定义最近的底部
if(Max[2]>Min[1] && Min[1]>Max[1] && Max[1]>0 && Max[2]<Min[2] && Min[2]<bars)
//--- 排除掉冲突的极值和特殊情况
{
mag1=Max[1]; // 在每次迭代中,如果条件满足,侦测到的极值保存的位置
mag2=Min[1];
mag3=Max[2];
mag4=Min[2];
}
}
}
}
Max[1]=mag1; // 定义了极值点,否则所有变量赋值为 'bars' 的数值
Min[1]=mag2;
Max[2]=mag3;
Min[2]=mag4;
Max[1]=check_max(Max[1],Min[1]); // 在指定的时间段内验证和修改极值点的位置
Min[1]=check_min(Min[1],Max[2]);
Max[2]=check_max(Max[2],Min[2]);
并且,我们可以使用第一个传入的顶部或者第一个侦测到的底部,但是,看起来更合理的是使用最近的极值点,以及基于它获得的顶部和底部。
对于这两个例子,都根据极值点的位置计算了手数的大小以及指标值,验证了修改侦测极值点的条件以及是否没有建立仓位。
如果极值点的价格和MACD柱形图有背离,并且不小于在输入参数中设置的值,就建立相应的仓位。背离应该是反方向的。
//当买入时计算手数
double lot_sell=NormalizeDouble(0.1*orderr_size/(NormalizeDouble(((High[Max[1]]-SymbolInfoDouble(Symbol(),SYMBOL_ASK)+guard)*10000),0)+0.00001),2);
//--- 当卖出时计算手数
int index_handle=iMACD(NULL,PERIOD_CURRENT,12,26,9,PRICE_MEDIAN);
double MACD_all[];
ArraySetAsSeries(MACD_all,true);
int copied4=CopyBuffer(index_handle,0,0,bars+2,MACD_all);
double index_min1=MACD_all[min[1]];
double index_min2=MACD_all[min[2]];
//---如果第一个极值点是底部,根据极值点计算指标值
double index_Max1=MACD_all[Max[1]];
double index_Max2=MACD_all[Max[2]];
//如果第一个极值点是顶部,根据极值点计算指标值
bool flag_1=(min[2]<bars && min[2]!=0 && max[1]<bars && max[1]!=0 && max[2]<bars && max[2]!=0); //Check the condition of the correct extreme point condition
bool flag_2=(Min[1]<bars && Min[1]!=0 && Max[2]<bars && Max[2]!=0 && Min[2]<bars && Min[2]!=0);
bool trend_down=(Low[min[1]]<(Low[min[2]]-trendd));
bool trend_up=(High[Max[1]]>(High[Max[2]]+trendd));
//---极值点的价格差异点数应该小于设置的数值
openedorder=PositionSelect(Symbol()); //验证不存在已建仓位的条件
if(min[1]<Max[1] && trend_down && flag_1 && !openedorder && (index_min1>(index_min2+macd_t)))
//如果第一个极值点是底部,建立买入交易
//极值点之间 MACD 的差值不能小于输入参数中 macd_t 的设置
// 如果价格反向移动就进行交易,并基于极值点计算指标值
{
if(show_info==1) Alert("对于最新的",bars," 个柱,最近的底部和极值点的距离柱数",min[1]," ",max[1]," ",min[2]);
//--- 显示极值点的数据
MqlTradeResult result={0};
MqlTradeRequest request={0};
request.action=TRADE_ACTION_DEAL;
request.magic=123456;
request.symbol=_Symbol;
request.volume=lot_buy;
request.price=SymbolInfoDouble(Symbol(),SYMBOL_ASK);
request.sl=Low[min[1]]-guard;
request.tp=MathAbs(2*SymbolInfoDouble(Symbol(),SYMBOL_BID)-Low[min[1]])+guard;
request.type=ORDER_TYPE_BUY;
request.deviation=50;
request.type_filling=ORDER_FILLING_FOK;
OrderSend(request,result);
}
if(min[1]>Max[1] && trend_up && flag_2 && !openedorder && (index_Max1<(index_Max2-macd_t)))
//如果第一个极值点是顶部,建立卖出交易
//极值点的 MACD 值的差距不能小于输入参数中的 macd_t 设置值
// 如果价格反向移动,就进行交易,并且基于极值点计算指标值
{
if(show_info==1) Alert("对于最新的",bars,"个柱,最近的顶部和极值点之间距离柱数",Max[1]," ",Min[1]," ",Max[2]);
//---显示极值点数据
MqlTradeResult result={0};
MqlTradeRequest request={0};
request.action=TRADE_ACTION_DEAL;
request.magic=123456;
request.symbol=_Symbol;
request.volume=lot_sell;
request.price=SymbolInfoDouble(Symbol(),SYMBOL_BID);
request.sl=High[Max[1]]+guard;
request.tp=MathAbs(High[Max[1]]-2*(High[Max[1]]-SymbolInfoDouble(Symbol(),SYMBOL_ASK)))-guard;
request.type=ORDER_TYPE_SELL;
request.deviation=50;
request.type_filling=ORDER_FILLING_FOK;
OrderSend(request,result);
}
当建立卖出仓位时,止损是根据最近的顶部位置设置的,而当建立买入仓位时,止损是根据最近的底部设置的,这使我们可以在价格强烈波动和平盘市场的时候都能够建立合理的目标。在两种情况下,获利是根据止损距离当前价格的值而对称设置的。在日内交易中,选择的是较小的变化范围,而在长线投资中,建议把变化范围设置到几倍大。
让我们探讨下面 EA 运作的实例 (图 11). 主要使用的参数: 变化范围 — 160 点值, 最小 MACD 柱形图差距 – 0,0004; 两个最近的顶部/底部的最小价格差距 – 120 点值 以及额外比例 – 0.9.
图 11. EA 的运行结果
首先,EA搜索最近的三个极值点,在决心买入的时候,EA侦测到了一个顶部和两个底部(使用箭头做了标记),和指标不同,EA没有突出显示这些极值点。但是,我们可以在开始交易时通过把 show_info 设为1来取得极值点的位置数据。
两个最近的底部的价格差距为148个点值,超过了指定的数值。MACD 柱形图在极值点的差距是 0.00062,也超过了指定的数值,考虑到最近的两个底部有反向的价格移动和指标的移动,在根据额外比例 (150 点值)定义的点位建立买入仓位,如果使用较小的额外比例,仓位可能会更早建立,而利润可能会更早得到。
下面是 EA 测试结果 (图 12). 在测试中,我们发现 macd_t 和 trend 参数的值对获利能力的影响最大,这些参数的值越大,获利交易的百分比就越大,然而,获利可能增加的同时也会导致交易总数的下降,
例如,如果 macd_t = 0.0006 并且 trend=160 (图 12), 56% 的交易是获利的,总交易是44个,如果 macd_t = 0.0004 并且 trend=120, 进行了 84 个交易,而它们中的 51% 是获利的。
图 12. EA 的测试结果
当优化策略时,正确设置 macd_t 和 trend 参数值是很关键的,变化范围和另外的数值也会影响交易参数。变化范围定义了侦测到的极值点的数量和交易的数量,另外的参数定义了当建立仓位时获利和止损的数值。
本策略,以及其它一些策略,只有在使用了以上提出的工具时才可能尽可能正确地工作,否则,可能会遇到收到的信号是使用5个点或者更小的极值点,而指定的获利和止损却是距离当前价格200个点。在这种情况下,极值点的重要性很低。在这些和其他许多情况下,传统的乖哦那句或者定义了过多微小的极值点,或者干脆没有侦测到顶部或者底部。另外,这些工具还经常在时间序列结束前定义极值点中有问题。
结论
本文中描述的算法和方案可以使得可以根据价格变化在价格图表上正确定义极值点,取得的结果在定义图形模式和使用图形模式及指标来实现交易策略都是适合的。我们所开发的工具比其他一些著名的方案有更多优势,不论市场状态(有趋势或者平盘),都可以定义出关键的极值点。只有超过预先定义数值的极值点才会被侦测出来,其他的峰值和谷值就忽略掉了。从图表的末端开始搜索极值点使我们可以根据最近的价格波动来取得结果,这些结果不会被所选择的时段很大影响,只是由指定的价格变化来定义。