所有交易员都知道一个规则:”趋势是您的好朋友,要跟着它走。“但是到底什么是趋势,这却是仁者见仁智者见智的问题。几乎每位交易员都听说过一些可怕的故事,这些故事的中心思想就是:逆趋势而行者,亡。
任何一位交易人都有可能面临准确把握趋势的好机会。也许这就是每个人都想找到的万能圣杯。在本文中,我们将讨论几个判断趋势的方法。更准确地说,是如何通过 MQL5 方法,制定几个经典的趋势判断程序。
首先,让我们来介绍一下趋势的广义概念。
趋势,是市场价格变化的长期倾向(方向)。趋势的这个广义概念会产生几个结果:
让我们用图形表示这个概念:
图 1. 趋势分析
让我们来看这个图,您会发现从 2005 年末至 2006 年 5 月,整体的趋势为上涨(图中绿色箭头)。但是,当我们把实现缩短到图中更短的时间周期,就会发现 2006 年 2 月的趋势明显为下降(图中红色箭头),而几乎整个 1 月的价格都处于盘整期(黄色箭头)。
因此,在您判断趋势之前,首先要决定您想要选择的时间周期。对于交易而言,时间周期最重要的作用,就是决定在市场中从建仓到平仓的整个持仓时间。此外,还会决定您的保护性止损和预计平仓操作,以及交易操作频率。
本文的目的,就是帮助交易新手娴熟运用 Meta Trader 5 平台的趋势判断工具。本文还为您提供关于编写简单指标的基础知识,让这个程序自动运行。其终极目标就是编写简单的 EA 交易,使用这些指标进行自动交易。
我们将以欧元美元日线图(1 天时间周期)为例,这是 Forex 市场中流动性最强的工具。该时间周期的持仓时间可以从几天到几个月不等。相应地,目标就是达到几百甚至几千的点位,在几百个点位之外的地方设置保护性止损。
广义而言,以下所有描述都可以用于任何一个时间周期。但要记住的是,图表时间周期越小,市场噪音对交易的影响就越大。所谓市场噪音是影响市场波动的新闻、大交易者的市场操作和其他因素。
理论上而言,趋势的时间越长,其变化的可能性就越小,那么,当您进行趋势交易的时候,赚钱的机会就比赔钱的机会更大。现在,您必须理解如何判断价格图中的趋势。我们将在本文中讨论这个问题。
以下是判断趋势的一些已知方法:
我们接下来将对所有这些方法的优势和劣势进行分析,然后与历史同期表现进行比较。
2.1. 使用移动平均线判断趋势
也许,这是判断趋势及其方向的最简单办法 - 使用移动平均线。移动平均线是技术分析的第一个工具,目前有很多应用中的变体,是大部分指标的基础。交易人即使用一条移动平均线,也使用由几条移动平均线组成的“扇形线”。
让我们先介绍单一移动平均线的简单原则:在这种情况下,当价格在移动平均线附近上下浮动时(所谓的“震荡的”),我们将使用柱的收盘价,减少“错误的”趋势变化的次数。
让我们用图形解释这个方法:
图 2. 使用移动平均线判断趋势
在这里,我们使用 EURUSD D1 日线图和简单的 200 天移动平均线图,以收盘价为基础(图中的红线)。在图形的底部,您可以看到特别开发的趋势指标 - MATrendDetector。趋势方向由指标柱形图与零轴相对的位置所显示。+1 对应上涨趋势。-1 下跌趋势。我们将对它和本文中使用的其他指标进行讨论。
您可以看到,当柱的收盘价高于/低于移动平均线,价格接下来经常都会向反方向移动,也就是说,这个方法会给出很多错误信号。这就是为什么 EA 交易和各种指标很少使用这种方式,它被认为是一个非常“粗糙“的趋势过滤器。
2.2. 使用三条移动平均线判断趋势
那么,如何改善移动平均线的趋势判断准确度呢?比如,您可以使用两根或更多不同时间周期的指标平均线。那么,无论是多少根不同时间周期的移动平均线(超过一根),判断趋势的规则都如下所示:
我们使用的术语是:
这种“平均线正确顺序”又被称为平均线扇形图向上/向下开口,因为它看起来确实如其所述。
让我们用图形解释这个方法:
图 3. 使用多条移动平均线判断趋势
我们将使用 EURUSD D1 日线图和简单的 200 天移动平均线(粗红线)、50 天(中粗黄线)和 21 天(紫色细线),以收盘价为基准。
在图标的下方是特别开发的趋势指标 - FanTrendDetector。趋势方向由指标柱形图与零轴相对的位置所显示。+1 对应上涨趋势。-1 下跌趋势。如果柱形图的值等于零,就意味着无法判断趋势。还有用于比较的 MATrendDetector 指标。
很显然,这样做可以减少趋势改变警告的数量。但是会增加趋势判断延后的可能性。这是可以理解的,所有移动平均数排列成“正确”顺序,这需要些时间。至于它的优势和劣势?这要看使用这些方法的交易系统而定。
在这个例子中,移动平均线的周期值只是随意选择,但这的确是交易人以及作者本人经常使用的方式。通过选择一系列的移动平均线和它们的数值,您就能为某一货币对改善这种趋势判断方法的特征。
2.3. 使用峰谷指标的高点和低点判断趋势
现在,让我们采用典型的技术分析方法来判断趋势。也就是说,我们会使用以下的 Charles Dow 规则:
我们将通过峰谷指标的顶/底部来寻找局部高点/低点。
让我们用图形解释这个方法:
图 4. 利用峰谷指标判断趋势
我们将使用的 EURUSD D1 图和峰谷指标参数为:ExtDepth = 5,ExtDeviation = 5,ExtBackstep = 3。
在图标的下方是特别开发的趋势指标 - ZigZagTrendDetector。
这种趋势判断方法存在的主要问题是:在实时情况中无法判断极点是否已经形成。当您查看历史图形时可以很容易找到极点,您知道它在什么地方形成。但是在价格实时变化的过程中,已经形成的极点可能会忽然消失,或者忽然又出现。想要了解这一点,可以看看在 EA 交易的模拟测试模式中的峰谷线走向。
由于存在这个缺点,这种方法在实际交易中没有太大用处。但对历史数据的技术分析很有帮助,可以以此找出规律,分析不同交易系统的质量。
2.4. 使用 ADX 指标判断趋势
以下要讨论的是使用 ADX (平均趋向)指标判断趋势的方法。这种指标不仅用于判断趋势走向,还可评估趋势强度。这是 ADX 指标的一个重要价值。趋势强度由 ADX 主线来决定 - 如果值大约 20(普遍接受的水平,但是目前不是最佳的选择),那么趋势就足够强。
趋势方向由 +DI 和 -DI 线之间的相互关系决定。这个指标以指数平均法对这三条线进行柔化,因此对趋势变化的反应会相对滞后。
让我们用图表来解释这种趋势判断方法:
本例并未使用 ADX 趋势线来判断趋势。需要减少这种指标的错误信号数量。如果趋势比较弱(ADX 小于 20),最好等到趋势变强的时候再进行趋势交易。
让我们用图形解释这个方法:
图 5. 使用 ADX 指标判断趋势
我们在这里使用的 EURUSD D1 图和 ADX 指标参数为:PeriodADX = 21(蓝色粗线 - ADX 趋势强度值,细绿线 - +DI 值,细红线 - -DI 值)。
在图下方是特别开发的趋势指标 - ADXTrendDetector。为了便于对照,ADXTrendDetector 上方图表(深红色)中未启动趋势强度过滤器,(ADXTrendLevel = 0),下方图表(蓝色)则启用了这个功能(ADXTrendLevel = 20)。
请注意,当我们打开趋势强度过滤器,趋势走向判断中所谓的“反弹”部分已经被去掉。这种过滤器比较适合在实际交易中应用。根据货币对走向的性质,巧妙地根据市场当前情况选择外部参数(平仓/幅度/趋势),可以更好地改善指标的质量。
通常,这个指标会带来一个好机会,构建一个作为输入过滤器的趋势追踪交易系统。
2.5. 使用 NRTR 指标判断趋势
以下趋势判断方法使用了NRTR(趋势转折)指标。这种指标始终都与目标价极点(上升趋势中的较低价和下跌趋势中的较高价格),保持着不变的距离。这种指标的主要理念是:应该忽视主要趋势中一些小的纠正移动,当移动相对于主趋势超过了某个程度,就是趋势改变的信号。
这个理念中可以得出判断趋势方向的原则:
为了减少错误趋势反转对价格波动所产生的影响,我们会使用收盘价来检测 NRTR 线的位置。
让我们用图形解释这个方法:
图 6. 使用 NRTR 指标判断趋势
蓝色大点对应上涨趋势,红色大点对应下跌趋势。在图表下方显示了我们的趋势指标NRTRTrendDetector。
2.6. 使用三值 Heiken Ashi 烛图判断趋势
判断趋势另外一个常用方法就是 Heiken Ashi 烛图。Heiken Ashi 烛图是经过改进的日本烛图。它们的值与之前的烛柱进行部分平均。
让我们用图形解释这个方法:
图 7. 以Heiken Ashi 烛图颜色判断趋势
如您所见,当价格在单向通道中波动时,这种方法仍然不能避免出现错误信号。但更大的问题是,这个质变不仅能改写上一个柱,还会改写倒数第二个柱,也就是说,我进行输入的信号可能在下一个柱出现反转。这是因为当柱体的颜色确定后,就会分析两个柱,所以建议将这种方法与其他辅助信号结合使用。
现在,让我们来创建趋势指标。
3.1. 根据移动平均线创建的趋势指标
根据移动平均线可以设计出最简单的指标,也是判断趋势最简单的方法。首先让我们来看看它都包括哪些部分。指标的全部源代码都在本文所附的 MATrendDetector.MQ5 文件之中。
在指标程序之初就是线,与库连接,计算不同的移动平均值。安装客户端就可以随时使用这个库。线如下所示:
#include <MovingAverages.mqh>
我们将使用其中的一个函数,计算一个简单的移动平均指标:
double SimpleMA(const int position, const int period, const double &price[])
下面是如何定义输入参数:
函数返回移动平均线已计算出的值。
文本的下一部分中,包括在屏幕上显示指标的初始设置:
//--------------------------------------------------------------------- #property indicator_separate_window //--------------------------------------------------------------------- #property indicator_applied_price PRICE_CLOSE #property indicator_minimum -1.4 #property indicator_maximum +1.4 //--------------------------------------------------------------------- #property indicator_buffers 1 #property indicator_plots 1 //--------------------------------------------------------------------- #property indicator_type1 DRAW_HISTOGRAM #property indicator_color1 Black #property indicator_width1 2 //---------------------------------------------------------------------设置以下参数:
最后两个参数可以让您设置一个固定的标尺显示指标图表。之所以能过这样做,是因为我们知道我们指标的最大值和最小值 - 包括从 -1 到 +1。这样做是为了让图表更好看,而不是为了在窗口中重叠窗口边框和指标名称。
接下来是输入指标的外部参数,当我们在图中设置指标、以及之后指标生效的过程中,可以对其进行修改:
input int MAPeriod = 200;
这里只有一个参数 - 移动平均线周期值。
指标下一个重要部分就是函数,处理指标在图中生效时的各种事件。
第一个是初始化函数 - OnInit()。加载指数后立刻调用这个函数。在我们的指数中,它如下所示:
void OnInit() { SetIndexBuffer( 0, TrendBuffer, INDICATOR_DATA ); PlotIndexSetInteger( 0, PLOT_DRAW_BEGIN, MAPeriod ); }
SetIndexBuffer() 指数将之前的数组声明绑定,我们将用一个指标缓冲区,把趋势值 TrendBuffer[] 存储在其中。我们只有一个指标缓冲区,其指标等于零。
PlotIndexSetInteger() 函数设置了初始柱的数量,但不会在指标窗口中将其画出。
如果柱的数量少于其周期,就很难用数学方法计算出一个简单的移动平均线,让我们确定柱的数量,将其设置为等于移动平均线周期。
下一个函数是 OnCalculate(),它处理的是重新计算一个指标需求的事件:
int OnCalculate(const int _rates_total, const int _prev_calculated, const int _begin, const double& _price[ ] ) { int start, i; // 如果屏幕上的柱数少于平均周期,计算无法进行: if( _rates_total < MAPeriod ) { return( 0 ); } // 确定指标缓冲区计算的初始柱: if( _prev_calculated == 0 ) { start = MAPeriod; } else { start = _prev_calculated - 1; } // 循环计算指标缓冲区数值: for( i = start; i < _rates_total; i++ ) { TrendBuffer[ i ] = TrendDetector( i, _price ); } return( _rates_total ); }在指标初始化之后会第一次调用这个函数,每当价格数据改变一次都会调用一次。比如,当已计算的信号有了一个新跳动。让我们来具体讨论它。
首先,让我们来看看在图上是否有足够的柱数量 - 如果少于运动平均线的周期,这没有可以计算的内容,这个函数以 return 返回运算符结束。如果柱数量足够进行计算,确定初始柱,从这里开始计算指标。这样做,是为了在每次价格跳动时,不要重新计算所有指标值。
我们在这里使用的是终端提供的机制。每次当您调用一个处理函数,检查 _prev_calculated 函数自变量值 - 即柱数量,这个值在之前调用的 OnCalculate() 函数中进行处理。如果值的数量为零,则重新计算所有指标值。否则,就用 _prev_calculated - 1. 索引只重新计算最后一个柱。
计算指标缓冲区值的循环由 for 运算符执行 - 为了每一个已重新计算的指标缓冲区值,我们调用其主体中的趋势判断函数 TrendDetector。因此,只覆写这个函数,我们就能为计算趋势方向实施不同的计算式。在本例中,指标其他部分事实上并没有改变(外界参数可能正在改变)。
现在,让我们来讨论一下趋势判断函数自身 - TrendDetector。
int TrendDetector(int _shift, const double& _price[]) { double current_ma; int trend_direction = 0; current_ma = SimpleMA(_shift, MAPeriod, _price); if(_price[_shift] > current_ma) { trend_direction = 1; } else if(_price[_shift] < current_ma) { trend_direction = -1; } return(trend_direction); }这个函数执行以下任务:
如果函数返回零,它意味着不可能判断趋势。
指标设置的结果见 图 2 和 图 3。
3.2. 根据移动平均“扇形图”创建趋势指标
现在让我们看看,如何在这个指标的基础上创建一个比较复杂的指标,用移动平均线“扇形图”判断趋势。
指标的全部源代码都在本文所附的 FanTrendDetector.MQ5 文件之中。
这个指标与之前指标的差别为:
input int MA1Period = 200; // 长期移动平均的周期数 input int MA2Period = 50; // 中期移动平均的周期数 input int MA3Period = 21; // 短期移动平均的周期数
int TrendDetector(int _shift, const double& _price[]) { double current_ma1, current_ma2, current_ma3; int trend_direction = 0; current_ma1 = SimpleMA(_shift, MA1Period, _price); current_ma2 = SimpleMA(_shift, MA2Period, _price); current_ma3 = SimpleMA(_shift, MA3Period, _price); if(current_ma3 > current_ma2 && current_ma2 > current_ma1) { trend_direction = 1; } else if(current_ma3 < current_ma2 && current_ma2 < current_ma1) { trend_direction = -1; } return(trend_direction); }这个函数所检查的是,通过使用 if...else 运算符和它们的顺序,对这三者之间进行比较,检查移动平均指标是否按照正确的顺序排列。如果这些平均线以增加的顺序排列,则返回 1 - 上升。如果这些平均线以减少的顺序排列,则返回 -1 - 下降。在这两种情况下,检查 if 代码块,如果为 false,则返回零(无法判断趋势)。函数有两个输入自变量 - 分析柱缓冲区以及带有价格系列缓冲区自身的位移。
指标其他部分与上一个指标相同。
3.3. 根据峰谷指标创建的趋势指标
现在,让我们来看看这个指标,使用峰谷区间寻找极点,根据 Charles Dow 规则判断趋势方向。指标的全部源代码都在本文所附的 ZigZagTrendDetector.MQ5 文件中。
外部变量和将峰谷外部指标的参数值一起进行赋给:
//--------------------------------------------------------------------- // 外部参数: //--------------------------------------------------------------------- input int ExtDepth = 5; input int ExtDeviation = 5; input int ExtBackstep = 3; //---------------------------------------------------------------------这个指标的一个重要不同之处就是指标缓冲区的数量。这里除了显示缓冲区,我们还要再使用两个计算缓冲区。因此,我们在指标代码中修改了相应的设置:
#property indicator_buffers 3
添加两个缓冲区。它们将储存从外部指标 ZigZag 处获得的极点:
double ZigZagHighs[]; // zigzag 的上方拐点 double ZigZagLows[]; // zigzag 的下方拐点
还需要修改指标初始化事件处理程序,将这两个新增缓冲区设置为计算缓冲区:
// 储存 zigzag 拐点的缓冲区 SetIndexBuffer(1, ZigZagHighs, INDICATOR_CALCULATIONS); SetIndexBuffer(2, ZigZagLows, INDICATOR_CALCULATIONS);
在 OnCalculate 函数的计算代码中,还必须在我们的缓冲区中提供读取峰谷区间。如下所示操作:
// 把zigzag的上方和下方拐点复制到缓冲区: CopyBuffer(indicator_handle, 1, 0, _rates_total - _prev_calculated, ZigZagHighs); CopyBuffer(indicator_handle, 2, 0, _rates_total - _prev_calculated, ZigZagLows); // 循环计算指标缓冲区值: for(i = start; i < _rates_total; i++) { TrendBuffer[i] = TrendDetector(i); }
TrendDetector 函数看起来如下所示:
//--------------------------------------------------------------------- // 确定当前趋势方向: //--------------------------------------------------------------------- // 返回值: // -1 - 下跌趋势 // +1 - 上涨趋势 // 0 - 趋势没有确定 //--------------------------------------------------------------------- double ZigZagExtHigh[2]; double ZigZagExtLow[2]; //--------------------------------------------------------------------- int TrendDetector(int _shift) { int trend_direction = 0; // 寻找zigzag的最后四个拐点: int ext_high_count = 0; int ext_low_count = 0; for(int i = _shift; i >= 0; i--) { if(ZigZagHighs[i] > 0.1) { if(ext_high_count < 2) { ZigZagExtHigh[ext_high_count] = ZigZagHighs[i]; ext_high_count++; } } else if(ZigZagLows[i] > 0.1) { if(ext_low_count < 2) { ZigZagExtLow[ext_low_count] = ZigZagLows[i]; ext_low_count++; } } // 如果找到两对极值,退出循环: if(ext_low_count == 2 && ext_high_count == 2) { break; } } // 如果所需的极值数没有找到,趋势无法确定: if(ext_low_count != 2 || ext_high_count != 2) { return(trend_direction); } // 实施指标趋势方向条件检查: if(ZigZagExtHigh[0] > ZigZagExtHigh[1] && ZigZagExtLow[0] > ZigZagExtLow[1]) { trend_direction = 1; } else if(ZigZagExtHigh[0] < ZigZagExtHigh[1] && ZigZagExtLow[0] < ZigZagExtLow[1]) { trend_direction = -1; } return(trend_direction); }
在这里,我们搜索前 4 个峰谷极点。注意,搜索将在历史数据中进行。所以,在 for 循环中的索引会随着每次搜索重复降至零。如果找到极点,根据 Dow 规则对这些极点进行对比,实现趋势定义的一致性。有两个可能的极点位置,上升极点和下降极点。用 if...else 运算符对这些变量进行检查。
3.4. 根据 ADX 指标创建的趋势指标
让我们来看看 ADXTrendDetector 趋势指标,它使用的是 ADX 指标。指标的全部源代码都在本文所附的 ADXTrendDetector.MQ5 文件中。将外部 ADX 指标的参数值赋给外部变量:
//--------------------------------------------------------------------- // 外部参数 //--------------------------------------------------------------------- input int PeriodADX = 14; input int ADXTrendLevel = 20;
TrendDetector 函数的形式为:
//--------------------------------------------------------------------- // 确定当前趋势方向: //--------------------------------------------------------------------- // 返回值: // -1 - 下跌趋势 // +1 - 上涨趋势 // 0 - 趋势没有确定 //--------------------------------------------------------------------- int TrendDetector(int _shift) { int trend_direction = 0; double ADXBuffer[ 1 ]; double PlusDIBuffer[ 1 ]; double MinusDIBuffer[ 1 ]; // 把 ADX 指标值复制到缓冲区: CopyBuffer(indicator_handle, 0, _shift, 1, ADXBuffer); CopyBuffer(indicator_handle, 1, _shift, 1, PlusDIBuffer); CopyBuffer(indicator_handle, 2, _shift, 1, MinusDIBuffer); // 如果考虑 ADX 值 (趋势强度): if(ADXTrendLevel > 0) { if(ADXBuffer[0] < ADXTrendLevel) { return(trend_direction); } } // 检查 +DI 和 -DI 的相对位置: if(PlusDIBuffer[0] > MinusDIBuffer[0]) { trend_direction = 1; } else if(PlusDIBuffer[0] < MinusDIBuffer[0]) { trend_direction = -1; } return( trend_direction ); }
使用 CopyBuffer(),为计算柱数量,从外部指标 ADX 处获得所需的缓冲区指标值,由 _shift 自变量提供。下一步,分析这几条 +DI 和 -DI 线的相对位置。在必要的情况下,考虑趋势强度 - 如果比定义值小,就未判断出趋势。
3.5. 根据 NTRT 指标创建的趋势指标
NRTRTrendDetector 趋势指标的结构以 NRTR 为基础,与之前的指标比较类似。指标的全部源代码都在本文所附的 NRTRTrendDetector.MQ5 文件中。
//--------------------------------------------------------------------- // 外部参数: //--------------------------------------------------------------------- input int ATRPeriod = 40; // ATR 周期数, 以柱为单位 input double Koeff = 2.0; // ATR 数值改变系数 //---------------------------------------------------------------------
第二个差别 - 在于判断趋势方向的 TrendDetector 函数:
//--------------------------------------------------------------------- // 确定当前趋势方向: //--------------------------------------------------------------------- // 返回值: // -1 - 下跌趋势 // +1 - 上涨趋势 // 0 - 趋势没有确定 //--------------------------------------------------------------------- int TrendDetector(int _shift) { int trend_direction = 0; double Support[1]; double Resistance[1]; // 把 NRTR 指标值复制到缓冲区: CopyBuffer(indicator_handle, 0, _shift, 1, Support); CopyBuffer(indicator_handle, 1, _shift, 1, Resistance); // 检查指标线的值: if(Support[0] > 0.0 && Resistance[0] == 0.0) { trend_direction = 1; } else if(Resistance[0] > 0.0 && Support[0] == 0.0) { trend_direction = -1; } return( trend_direction ); }
在这里,我们用索引 0 和 1 读取外部指标 NRTR 两个缓冲区的值。上升时,Support 缓冲区中的值不等于零,下跌时,Resistance 缓冲区中的值不等于零。
3.6. 根据 Heiken Ashi 烛图创建趋势指标
现在,让我们使用 Heiken Ashi 烛图来设置趋势指标。
在本例中,我们不会调用外部指标,但会计算烛柱。这将改善指标性能,让 CPU 可以进行更重要的任务。指标的全部源代码都在本文所附的 HeikenAshiTrendDetector.MQ5 文件之中。
由于 Heiken Ashi 指标不能假定外部参数,我们可以移除 input 运算符的代码块。然后在运算符再计算事件的处理程序中进行主要的修改。在这里,我们将使用一个处理程序的替换变量,访问当前图表的所有价格数组。
现在,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& TickVolume[], const long& Volume[], const int& Spread[]) { int start, i; double open, close, ha_open, ha_close; // 确定指标缓冲区计算的初始柱: if(_prev_calculated == 0) { open = Open[0]; close = Close[0]; start = 1; } else { start = _prev_calculated - 1; } // 循环计算指标缓冲区值: for(i = start; i < _rates_total; i++) { // Heiken Ashi 蜡烛柱开盘价: ha_open = (open + close) / 2.0; // Heiken Ashi 蜡烛柱收盘价: ha_close = (Open[i] + High[i] + Low[i] + Close[i]) / 4.0; TrendBuffer[i] = TrendDetector(ha_open, ha_close); open = ha_open; close = ha_close; } return(_rates_total); }当我们决定 Heiken Ashi 烛柱颜色时,只需要两个价格 - 开盘价和收盘价,然后只计算这两个值。
当我们通过 TrendDetector 函数调用判断趋势方向后,将 Heiken Ashi 烛图当前的价格值保存在 open 和 close 中间变量中。TrendDetector 函数看起来很简单。您可以将其插入 OnCalculate,但这样做的目的,是为了在计算式有了进一步发展的情况下,可以让这个函数获得更多的功能和复杂性。这个函数如下:
int TrendDetector(double _open, double _close) { int trend_direction = 0; if(_close > _open) // 如果蜡烛柱增长,说明是上涨趋势 { trend_direction = 1; } else if(_close < _open) // 如果蜡烛柱下降,说明是下跌趋势 { trend_direction = -1; } return(trend_direction); }函数的自变量是 Heiken Ashi 烛图的两个价格,开盘价和收盘价,这两个价格将决定其方向。
让我们创建一个使用不同指标的 EA 交易。对比使用不同趋势判断方式创建的 EA交易,是一件很有趣的事情。首先,用默认参数检查结果,然后试着对其进行调整,找到最佳参数。
在本例中,创建 EA 交易的目的是对比各趋势判断方法的准确性和速度。因此,让我们先简要介绍一下创建所有 EA 交易的总原则:
我们所创建的所有趋势指标都包含零索引指标缓冲区,在其中储存了趋势方向所需要的数据。我们将在 EA 交易中使用它获得一个开/平仓的信号。
由于我们需要交易函数,所以我们在其中纳入了相应的库,与 MetaTrader 5 一起安装。这个库包含了 CTrade 课程和处理仓位和订单的几种方法。用交易函数简化日常工作量。库包含在以下线中:
#include <Trade\Trade.mqh>
我们将使用它的两种方法:建仓和平仓。第一个方法可以让您进行指定方向和交易量的建仓:PositionOpen(const string symbol, ENUM_ORDER_TYPE order_type, double volume, double price, double sl, double tp, const string comment )输入自变量如下显示:
第二个方法允许您平一个仓位:
PositionClose( const string symbol, ulong deviation )输入自变量如下显示:
让我们了来看看使用 MATrendDetector 指标的 EA 交易结构。EA 交易的全部源代码见本文所附的 MATrendExpert.MQ5 文件。第一个主要的 EA 交易代码块,是设置外部参数的代码块。
input double Lots = 0.1; input int MAPeriod = 200;
EA 交易的 Lots 参数 - 手数的大小,在建仓时使用。为了获得不同趋势判断方法的比较结果,我们使用了不需要资金管理的固定手数。趋势指标使用的所有其他外部参数,如上文所述。列表和目标与相应的指标完全一致。
EA 交易的第二个重要代码块 - EA 交易初始化的事件处理程序。
//--------------------------------------------------------------------- // 初始化事件处理函数: //--------------------------------------------------------------------- int OnInit() { // 创建外部指标句柄用于将来引用: ResetLastError(); indicator_handle = iCustom(Symbol(), PERIOD_CURRENT, "Examples\\MATrendDetector", MAPeriod); // 如果初始化没有成功,返回非零值: if(indicator_handle == INVALID_HANDLE) { Print("MATrendDetector 初始化错误, 代码 = ", GetLastError()); return(-1); } return(0); }这里的创建处理是为了调用趋势指标,如果创建成功,就返回零代码。如果创建指标处理失败(比如,指标不适用于 EX5 格式),我们将这个信息打印出来,然后返回非零代码。在本例中,EA 交易停止了进一步的工作,与日志中相应的信息一起,从终端卸载。
EA 交易的第二个代码块 - EA 交易去初始化时间处理程序。
//--------------------------------------------------------------------- // 指标去初始化事件处理函数: //--------------------------------------------------------------------- void OnDeinit(const int _reason) { // 删除指标句柄: if(indicator_handle != INVALID_HANDLE) { IndicatorRelease(indicator_handle); } }在这里删除了指标处理,释放其分配的存储。
去初始化 EA 交易不需要任何其他操作。
接下来就是 EA 交易的主代码块 - 当前符号新跳动的事件处理程序。
//--------------------------------------------------------------------- // 当前交易品种新订单事件处理函数: //--------------------------------------------------------------------- int current_signal = 0; int prev_signal = 0; bool is_first_signal = true; //--------------------------------------------------------------------- void OnTick() { // 等待一个新柱的开始: if(CheckNewBar() != 1) { return; } // 获得建仓/平仓信号: current_signal = GetSignal(); if(is_first_signal == true) { prev_signal = current_signal; is_first_signal = false; } // 选择当前交易品种仓位: if(PositionSelect(Symbol()) == true) { // 检查我们是否需要平掉一个反向仓位: if(CheckPositionClose(current_signal) == 1) { return; } } // 检查是否有买入信号: if(CheckBuySignal(current_signal, prev_signal) == 1) { CTrade trade; trade.PositionOpen(Symbol(), ORDER_TYPE_BUY, Lots, SymbolInfoDouble(Symbol(), SYMBOL_ASK ), 0, 0); } // 检查是否有卖出信号: if(CheckSellSignal(current_signal, prev_signal) == 1) { CTrade trade; trade.PositionOpen(Symbol(), ORDER_TYPE_SELL, Lots, SymbolInfoDouble(Symbol(), SYMBOL_BID ), 0, 0); } // 保存当前信号: prev_signal = current_signal; }让我们来看看 EA 交易所使用的辅助函数。
首先,为了在图表上开盘一个新柱,我们的EA必须检查信号。这就需要使用 CheckNewBar 函数:
//--------------------------------------------------------------------- // 返回新柱的标志: //--------------------------------------------------------------------- // - 如果返回1,说明有一个新柱 //--------------------------------------------------------------------- int CheckNewBar() { MqlRates current_rates[1]; ResetLastError(); if(CopyRates(Symbol(), Period(), 0, 1, current_rates)!= 1) { Print("CopyRates 复制错误, 代码 = ", GetLastError()); return(0); } if(current_rates[0].tick_volume>1) { return(0); } return(1); }新柱的存在由跳动量的值来决定。在开盘一个新柱时,其量在初始化时等于零(因为没有报价)。新跳动点的出现,大小就变成等于 1。
在这个函数中,我们将创建 current_rates[] 数组,这是 MqlRates 结构数组,包括一个元素,将当前价格和交易量的信息复制其中,然后检查跳动量的值。
在用当前符号设置的新跳动时间处理程序中,我们将按照以下的方式使用这个函数:
// 等待一个新柱的开始: if(CheckNewBar()!= 1) { return; }因此,新柱开盘,您就能获得一个关于当前趋势方向的信号。如下所示操作:
// 获得建仓/平仓信号: current_signal = GetSignal(); if(is_first_signal == true) { prev_signal = current_signal; is_first_signal = false; }
由于我们需要跟踪趋势中的变化,就需要记住之前柱的趋势值。在上述的一段代码中,我们将使用 prev_signal 变量。此外,我们还应该使用标志,标出这是第一个信号(之前没有信号)。这是 is_first_signal 变量。如果标志的值为 true,我们就用初始值对 prev_signal 变量进行初始化。
在这里,我们使用 GetSignal 函数,返回从我们的指标中获得的当前趋势方向。如下图所示:
//--------------------------------------------------------------------- // 取得建仓/平仓信号: //--------------------------------------------------------------------- int GetSignal() { double trend_direction[1]; // 从趋势指标获得信号: ResetLastError(); if(CopyBuffer(indicator_handle, 0, 0, 1, trend_direction) != 1) { Print("CopyBuffer 复制错误, 代码 = ", GetLastError()); return(0); } return((int)trend_direction[0]); }趋势指标数据从零缓冲区复制到我们的 trend_direction 数组,其中包括一个元素。数组元素值从这个函数处返回。此外,为了避免出现编译器警告,从 double 类型转到 int 类型。
在建一个新仓位之前,您应该检查是否有必要平掉之前开设的对立仓位。还要检查在同样的方向中是否已经建了一个仓位。用以下这部分代码进行这个设置:
// 选择当前交易品种仓位: if(PositionSelect(Symbol()) == true) { // 检查我们是否需要平掉一个反向仓位: if(CheckPositionClose(current_signal) == 1) { return; } }为了能够访问仓位,首先必须对其进行选择,使用当前符号的 PositionSelect() 函数进行这一操作。如果该函数返回为 true,那么仓位就是存在,已经对其进行成功选择,您就可以对其进行处理。
对立仓位的平仓,需要使用 CheckPositionClose 函数:
//--------------------------------------------------------------------- // 检查我们是否需要平仓: //--------------------------------------------------------------------- // 返回值: // 0 - 没有开启的仓位 // 1 - 已经有了信号同方向上的仓位 //--------------------------------------------------------------------- int CheckPositionClose(int _signal) { long position_type = PositionGetInteger(POSITION_TYPE); if(_signal == 1) { // 如果已经有了买入仓位,则返回: if(position_type == (long)POSITION_TYPE_BUY) { return(1); } } if(_signal==-1) { // 如果已经有了卖出仓位,则返回: if( position_type == ( long )POSITION_TYPE_SELL ) { return(1); } } // 平仓: CTrade trade; trade.PositionClose(Symbol(), 10); return(0); }首先,检查在趋势方向中是否已经建仓。如果已建仓,函数返回 1,当前仓位未平。如果在对立趋势方向中已建仓,那么您必须将其平掉。采用上述的 PositionClose 方法进行设置。由于没有已建仓位,将返回零。
当完成了现有仓位所有必须的检查和操作,就需要检查是否存在新信号。用以下这部分代码进行这个设置:
// 检查是否有买入信号: if(CheckBuySignal(current_signal, prev_signal)==1) { CTrade trade; trade.PositionOpen(Symbol(), ORDER_TYPE_BUY, Lots, SymbolInfoDouble(Symbol(), SYMBOL_ASK), 0, 0); }如果有买入信号,就用当前价格的 SYMBOL_ASK 建指定交易量的买入持仓。由于所有的仓位都用对立信号平仓,就不再使用获利和止损。EA交易”始终身处市场中“。
在实际交易中,建议使用防御性的止损,以防止出现未预料的情况,比如与 DC 服务器失去联系以及其他不可抗力情况。
卖出信号的所有设置都相同:
// 检查是否有卖出信号: if(CheckSellSignal(current_signal, prev_signal) == 1) { CTrade trade; trade.PositionOpen(Symbol(), ORDER_TYPE_SELL, Lots, SymbolInfoDouble(Symbol(), SYMBOL_BID), 0, 0); }唯一不同的就是卖出价格 - SYMBOL_BID。
由函数对信号的存在进行检查,CheckBuySignal 函数 - 买入,CheckSellSignal函数 - 卖出。这两个函数非常简单明了:
//--------------------------------------------------------------------- // 检查信号是否改为买入: //--------------------------------------------------------------------- // 返回值: // 0 - 无信号 // 1 - 有买入信号 //--------------------------------------------------------------------- int CheckBuySignal(int _curr_signal, int _prev_signal) { // 检查信号是否改为买入: if((_curr_signal==1 && _prev_signal==0) || (_curr_signal==1 && _prev_signal==-1)) { return(1); } return(0); } //--------------------------------------------------------------------- // 检查是否有卖出信号: //--------------------------------------------------------------------- // 返回值: // 0 - 无信号 // 1 - 有卖出信号 //--------------------------------------------------------------------- int CheckSellSignal(int _curr_signal, int _prev_signal) { // 检查信号是否改为卖出: if((_curr_signal==-1 && _prev_signal==0) || (_curr_signal==-1 && _prev_signal==1)) { return(1); } return(0); }
我们在这里检查的,是趋势是否已经改变成相反方向,或者趋势方向是否已经出现。如果满足了任何一个条件,函数就会返回信号存在。
总体而言,EA 交易的这种方案提供了一个非常广义的结构,可以轻松升级或拓展,满足更复杂的计算法。
其他 EA 交易的构建方式完全相同。重大差异值存在于外部参数的代码块之中 - 它们必须与已经使用过的趋势指标相对应,必须在创建指标处理的时候作为自变量进行传递。
让我们来看看根据历史数据创建的第一个 EA 交易结果。我们会使用 EURUSD 的历史数据,日柱的区间是 01.04.2004 至 06.08.2010。 在策略测试程序中用默认参数运行 EA 交易,然后就会得出以下结果:
图 8. 使用 MATrendDetector 指标的 EA 交易测试结果
策略测试报告
|
||||||||||||
MetaQuotes-演示 (Build 302)
|
||||||||||||
|
||||||||||||
设置
|
||||||||||||
EA: | MATrendExpert | |||||||||||
交易品种: | EURUSD | |||||||||||
周期: | 每日 (2004.04.01 - 2010.08.06) | |||||||||||
输入: | 手数=0.100000 | |||||||||||
|
MA 周期=200 | |||||||||||
经纪: | MetaQuotes Software Corp. | |||||||||||
货币: | USD | |||||||||||
初始存入: | 10 000.00 | |||||||||||
|
||||||||||||
结果
|
||||||||||||
柱: | 1649 | 订单号: | 8462551 | |||||||||
总净利润: | 3 624.59 | 毛利: | 7 029.16 | 净损失: | -3 404.57 | |||||||
获利系数: | 2.06 | 预计获利: | 92.94 | |||||||||
回收系数: | 1.21 | 夏普比率: | 0.14 | |||||||||
|
||||||||||||
平衡亏损: | ||||||||||||
平衡亏损绝对值: | 2 822.83 | 平衡亏损最大值: | 2 822.83 (28.23%) | 平衡亏损相对值: | 28.23% (2 822.83) | |||||||
市值亏损: | ||||||||||||
市值亏损绝对值: | 2 903.68 | 市值亏损最大值: | 2 989.93 (29.64%) | 市值亏损相对值: | 29.64% (2 989.93) | |||||||
|
||||||||||||
总交易: | 39 | 短线交易(获利%): | 20 (20.00%) | 长线交易(获利%): | 19 (15.79%) | |||||||
总成交: | 78 | 盈利交易(总交易的%): | 7 (17.95%) | 亏损交易(总交易的%): | 32 (82.05%) | |||||||
|
最大获利交易: | 3 184.14 | 最大亏损交易(总交易的%): | -226.65 | ||||||||
|
平均获利交易: | 1 004.17 | 平均亏损交易(总交易的%): | -106.39 | ||||||||
|
最大连续盈利 ($): | 4 (5 892.18) | 最大连续亏损 ($): | 27 (-2 822.83) | ||||||||
|
最大连续盈利(次数): | 5 892.18 (4) | 最大连续盈利(次数): | -2 822.83 (27) | ||||||||
|
平均连续盈利: | 2 | 平均连续亏损: | 8 | ||||||||
|
||||||||||||
|
---|
总体而言还不错,除了从测试最初到 22.09.2004 的这一部分。无法保证这部分是否会在未来重复。当您看到这一段时期的图表,就会看到在一个有限的范围内,存在明显的横向移动。在这些情况下,我们简单的趋势专家就不是那么胜任了。将交易放置在这段时期之中,就得到了以下的图像:
图 9. 横向移动部分
在这个图表上还有 SMA200 移动平均线。
现在,让我们看看,如果以相同的间隔和默认参数,用几条移动平均线构成的指标来构建 EA 交易,在哪些地方会更”先进“?
图 10. 使用 FanTrendDetector 指标的 EA 交易测试结果
策略测试报告
|
||||||||||||
MetaQuotes-演示 (Build 302)
|
||||||||||||
|
||||||||||||
设置
|
||||||||||||
EA: | FanTrendExpert | |||||||||||
交易品种: | EURUSD | |||||||||||
周期: | 每日 (2004.04.01 - 2010.08.06) | |||||||||||
输入: | 手数=0.100000 | |||||||||||
|
MA1 周期=200 | |||||||||||
|
MA2 周期=50 | |||||||||||
|
MA3 周期=21 | |||||||||||
经纪: | MetaQuotes Software Corp. | |||||||||||
货币: | USD | |||||||||||
初始存入: | 10 000.00 | |||||||||||
|
||||||||||||
结果
|
||||||||||||
柱: | 1649 | 订单号: | 8462551 | |||||||||
总净利润: | 2 839.63 | 毛利: | 5 242.93 | 净损失: | -2 403.30 | |||||||
获利系数: | 2.18 | 预计获利: | 149.45 | |||||||||
回收系数: | 1.06 | 夏普比率: | 0.32 | |||||||||
|
||||||||||||
平衡亏损: | ||||||||||||
平衡亏损绝对值: | 105.20 | 平衡亏损最大值: | 1 473.65 (11.73%) | 平衡亏损相对值: | 11.73% (1 473.65) | |||||||
市值亏损: | ||||||||||||
市值亏损绝对值: | 207.05 | 市值亏损最大值: | 2 671.98 (19.78%) | 市值亏损相对值: | 19.78% (2 671.98) | |||||||
|
||||||||||||
总交易: | 19 | 短线交易(获利%) | 8 (50.00%) | 长线交易(获利%) | 11 (63.64%) | |||||||
总成交: | 38 | 盈利交易(总交易的%): | 11 (57.89%) | 亏损交易(总交易的%): | 8 (42.11%) | |||||||
|
最大获利交易: | 1 128.30 | 最大亏损交易(总交易的%): | -830.20 | ||||||||
|
平均获利交易: | 476.63 | 平均亏损交易(总交易的%): | -300.41 | ||||||||
|
最大连续盈利 ($): | 2 (1 747.78) | 最大连续亏损($): | 2 (-105.20) | ||||||||
|
最大连续盈利(次数): | 1 747.78 (2) | 最大连续盈利(次数): | -830.20 (1) | ||||||||
|
平均连续盈利: | 2 | 平均连续亏损: | 1 | ||||||||
|
好多了!看看在之前 EA 交易结果中出现问题的那部分,图像如下:
图 11. 平移部分的 FanTrendExpert 结果
与 图 9相比较 - 很显然,趋势改变的错误警告数量减少了。但是成交数量也减少了一半,这是合乎逻辑的结果。在分析两个 EA 交易的平衡/市值曲线时,您可以看到,在获得最大收益的方面,很多成交的收盘都低于优化值。因此,对 EA 交易进行的下一个升级,就是改善成交收盘的计算式。但这不属于本文讨论的范围。读者可以自己来完成。
让我们来测试所有的 EA。下面提供了从 1993 到 2010 年,EURUSD 货币对的历史波动区间,时间周期为 D1。
图 12. MATrendExpert 测试
图 13. FanTrendExpert 测试
图 14. ADXTrendExpert 测试 (ADXTrendLevel = 0)
图15. ADXTrendExpert 测试 (ADXTrendLevel = 20)
图 16. RTRTrendExpert 测试
图 17. Heiken Ashi 测试
让我们来看看这些测试结果。
领先两个最常用的 EA,一个是移动平均线,一个是扇形移动平均线。的确,这些 EA 只是简单地使用了上一个时间区间的价格平滑系列,但最接近跟随趋势(以及价格)规则。由于我们使用了比较“重“的 200 日移动平均线,市场波动的影响似乎消失了。
这些 EA 的成交数量低,但这并非是它们的缺点,因为仓位保留可能会持续几个月的时间,它所跟随的是 200 天的趋势。注意看 MATrendExpert 如何在三个趋势区间中交替:盈利、平移(在 EA 中)和资金损失,很有意思。
ADX 指标的趋势判断方法也呈现出很好的结果。PeriodADX 稍作改变,其值变成了 17,在整个历史过程中给出了更加一致的结果。区域强度的过滤效应并不显著。也许,您需要调整 ADXTrendLevel 参数,或者根据目前的市场波动,对其进行更动态的设置。有几个亏损周期,因此,需要采用其他的措施对平衡进行补偿。
无论是在测试的整个范围,还是在随机选择的长线间隔,NRTR 指标实际显示都是零利润。从某种程度而言,这表示这是一种非常稳定的趋势判断方法。也许,调整参数会让这个 EA 交易实现获利,比如进行必要的优化。
以 Heiken Ashi 为基础创建的 EA 交易明显没有盈利。尽管它的历史数据看起来还不错,也许是因为进行实时重新绘制的缘故。其测试结果非常不理想。也许,使用这个指标的平滑版本会得到更好的结果 - 平滑 Heiken Ashi,不会出现过于强烈的重新绘制倾向。
的确,如果一个系统中的用动态止损位建仓,创建一个目标水平,那么所有 EA 交易都会因之受益。此外,拥有一个资金管理系统也是个好办法,不仅可以最小化损失,还可以提高长期间隔的利润。
由此可见,编写判断趋势的代码并不是那么困难。重要的是要亲自去做,还要有一个非常合理的想法,探索一些市场规则。如果您在这些规则方面的基础越深,您在根据这些规则建立交易系统时就会越自信。而这个交易系统也会保持很长的时间。
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...
移动端课程