在 MQL5 中寻找趋势的几种方法

Dmitriy Skub | 8 十月, 2013

简介

所有交易员都知道一个规则:”趋势是您的好朋友,要跟着它走。“但是到底什么是趋势,这却是仁者见仁智者见智的问题。几乎每位交易员都听说过一些可怕的故事,这些故事的中心思想就是:逆趋势而行者,亡。

任何一位交易人都有可能面临准确把握趋势的好机会。也许这就是每个人都想找到的万能圣杯。在本文中,我们将讨论几个判断趋势的方法。更准确地说,是如何通过 MQL5 方法,制定几个经典的趋势判断程序。


1. 什么是趋势,为什么要知道趋势

首先,让我们来介绍一下趋势的广义概念。

趋势,是市场价格变化的长期倾向(方向)。趋势的这个广义概念会产生几个结果:

让我们用图形表示这个概念:

图 1. 趋势分析

图 1. 趋势分析

让我们来看这个图,您会发现从 2005 年末至 2006 年 5 月,整体的趋势为上涨(图中绿色箭头)。但是,当我们把实现缩短到图中更短的时间周期,就会发现 2006 年 2 月的趋势明显为下降(图中红色箭头),而几乎整个 1 月的价格都处于盘整期(黄色箭头)。

因此,在您判断趋势之前,首先要决定您想要选择的时间周期。对于交易而言,时间周期最重要的作用,就是决定在市场中从建仓到平仓的整个持仓时间。此外,还会决定您的保护性止损和预计平仓操作,以及交易操作频率。

本文的目的,就是帮助交易新手娴熟运用 Meta Trader 5 平台的趋势判断工具。本文还为您提供关于编写简单指标的基础知识,让这个程序自动运行。其终极目标就是编写简单的 EA 交易,使用这些指标进行自动交易。  

我们将以欧元美元日线图(1 天时间周期)为例,这是 Forex 市场中流动性最强的工具。该时间周期的持仓时间可以从几天到几个月不等。相应地,目标就是达到几百甚至几千的点位,在几百个点位之外的地方设置保护性止损。

广义而言,以下所有描述都可以用于任何一个时间周期。但要记住的是,图表时间周期越小,市场噪音对交易的影响就越大。所谓市场噪音是影响市场波动的新闻、大交易者的市场操作和其他因素。

理论上而言,趋势的时间越长,其变化的可能性就越小,那么,当您进行趋势交易的时候,赚钱的机会就比赔钱的机会更大。现在,您必须理解如何判断价格图中的趋势。我们将在本文中讨论这个问题。


2. 如何判断趋势

以下是判断趋势的一些已知方法:

  1. 移动平均线
  2. 峰谷分析
  3. 平均趋向指标 (ADX)
  4. NRTR 指标
  5. Heiken Ashi 烛图的颜色

我们接下来将对所有这些方法的优势和劣势进行分析,然后与历史同期表现进行比较。

2.1. 使用移动平均线判断趋势

也许,这是判断趋势及其方向的最简单办法 - 使用移动平均线。移动平均线是技术分析的第一个工具,目前有很多应用中的变体,是大部分指标的基础。交易人即使用一条移动平均线,也使用由几条移动平均线组成的“扇形线”。

让我们先介绍单一移动平均线的简单原则:

在这种情况下,当价格在移动平均线附近上下浮动时(所谓的“震荡的”),我们将使用柱的收盘价,减少“错误的”趋势变化的次数。

让我们用图形解释这个方法:

图 2. 使用移动平均线判断趋势

图 2. 使用移动平均线判断趋势

在这里,我们使用 EURUSD D1 日线图和简单的 200 天移动平均线图,以收盘价为基础(图中的红线)。在图形的底部,您可以看到特别开发的趋势指标 - MATrendDetector。趋势方向由指标柱形图与零轴相对的位置所显示。+1 对应上涨趋势。-1 下跌趋势。我们将对它和本文中使用的其他指标进行讨论。

您可以看到,当柱的收盘价高于/低于移动平均线,价格接下来经常都会向反方向移动,也就是说,这个方法会给出很多错误信号。这就是为什么 EA 交易和各种指标很少使用这种方式,它被认为是一个非常“粗糙“的趋势过滤器。

2.2. 使用三条移动平均线判断趋势

那么,如何改善移动平均线的趋势判断准确度呢?比如,您可以使用两根或更多不同时间周期的指标平均线。那么,无论是多少根不同时间周期的移动平均线(超过一根),判断趋势的规则都如下所示:

我们使用的术语是:

这种“平均线正确顺序”又被称为平均线扇形图向上/向下开口,因为它看起来确实如其所述。

让我们用图形解释这个方法:

图 3. 使用多条移动平均线判断趋势

图 3. 使用多条移动平均线判断趋势

我们将使用 EURUSD D1 日线图和简单的 200 天移动平均线(粗红线)、50 天(中粗黄线)和 21 天(紫色细线),以收盘价为基准。

在图标的下方是特别开发的趋势指标 - FanTrendDetector。趋势方向由指标柱形图与零轴相对的位置所显示。+1 对应上涨趋势。-1 下跌趋势。如果柱形图的值等于零,就意味着无法判断趋势。还有用于比较的 MATrendDetector 指标。

很显然,这样做可以减少趋势改变警告的数量。但是会增加趋势判断延后的可能性。这是可以理解的,所有移动平均数排列成“正确”顺序,这需要些时间。至于它的优势和劣势?这要看使用这些方法的交易系统而定。

在这个例子中,移动平均线的周期值只是随意选择,但这的确是交易人以及作者本人经常使用的方式。通过选择一系列的移动平均线和它们的数值,您就能为某一货币对改善这种趋势判断方法的特征。

2.3. 使用峰谷指标的高点和低点判断趋势

现在,让我们采用典型的技术分析方法来判断趋势。也就是说,我们会使用以下的 Charles Dow 规则:

我们将通过峰谷指标的顶/底部来寻找局部高点/低点。

让我们用图形解释这个方法:

图 4. 利用峰谷指标判断趋势

图 4. 利用峰谷指标判断趋势

我们将使用的 EURUSD D1 图和峰谷指标参数为:ExtDepth = 5ExtDeviation = 5ExtBackstep = 3

在图标的下方是特别开发的趋势指标 - ZigZagTrendDetector

这种趋势判断方法存在的主要问题是:在实时情况中无法判断极点是否已经形成。当您查看历史图形时可以很容易找到极点,您知道它在什么地方形成。但是在价格实时变化的过程中,已经形成的极点可能会忽然消失,或者忽然又出现。想要了解这一点,可以看看在 EA 交易的模拟测试模式中的峰谷线走向。

由于存在这个缺点,这种方法在实际交易中没有太大用处。但对历史数据的技术分析很有帮助,可以以此找出规律,分析不同交易系统的质量。

2.4. 使用 ADX 指标判断趋势

以下要讨论的是使用 ADX (平均趋向)指标判断趋势的方法。这种指标不仅用于判断趋势走向,还可评估趋势强度。这是 ADX 指标的一个重要价值。趋势强度由 ADX 主线来决定 - 如果值大约 20(普遍接受的水平,但是目前不是最佳的选择),那么趋势就足够强。

趋势方向由 +DI 和 -DI 线之间的相互关系决定。这个指标以指数平均法对这三条线进行柔化,因此对趋势变化的反应会相对滞后。

让我们用图表来解释这种趋势判断方法:

本例并未使用 ADX 趋势线来判断趋势。需要减少这种指标的错误信号数量。如果趋势比较弱(ADX 小于 20),最好等到趋势变强的时候再进行趋势交易。

让我们用图形解释这个方法:

图 5. 使用 ADX 指标判断趋势

图 5. 使用 ADX 指标判断趋势

我们在这里使用的 EURUSD D1 图和 ADX 指标参数为:PeriodADX = 21(蓝色粗线 - ADX 趋势强度值,细绿线 - +DI 值,细红线 - -DI 值)。

在图下方是特别开发的趋势指标 - ADXTrendDetector。为了便于对照,ADXTrendDetector 上方图表(深红色)中未启动趋势强度过滤器,(ADXTrendLevel = 0),下方图表(蓝色)则启用了这个功能(ADXTrendLevel = 20)。

请注意,当我们打开趋势强度过滤器,趋势走向判断中所谓的“反弹”部分已经被去掉。这种过滤器比较适合在实际交易中应用。根据货币对走向的性质,巧妙地根据市场当前情况选择外部参数(平仓/幅度/趋势),可以更好地改善指标的质量。

通常,这个指标会带来一个好机会,构建一个作为输入过滤器的趋势追踪交易系统。

2.5. 使用 NRTR 指标判断趋势

以下趋势判断方法使用了NRTR(趋势转折)指标。这种指标始终都与目标价极点(上升趋势中的较低价和下跌趋势中的较高价格),保持着不变的距离。这种指标的主要理念是:应该忽视主要趋势中一些小的纠正移动,当移动相对于主趋势超过了某个程度,就是趋势改变的信号。

这个理念中可以得出判断趋势方向的原则:

为了减少错误趋势反转对价格波动所产生的影响,我们会使用收盘价来检测 NRTR 线的位置。

让我们用图形解释这个方法:

图 6. 使用 NRTR 指标判断趋势

图 6. 使用 NRTR 指标判断趋势

蓝色大点对应上涨趋势,红色大点对应下跌趋势。在图表下方显示了我们的趋势指标NRTRTrendDetector

2.6. 使用三值 Heiken Ashi 烛图判断趋势

判断趋势另外一个常用方法就是 Heiken Ashi 烛图。Heiken Ashi 烛图是经过改进的日本烛图。它们的值与之前的烛柱进行部分平均。

让我们用图形解释这个方法:

图 7. 以 Heiken Ashi 烛图颜色判断趋势

图 7. 以Heiken Ashi 烛图颜色判断趋势

如您所见,当价格在单向通道中波动时,这种方法仍然不能避免出现错误信号。但更大的问题是,这个质变不仅能改写上一个柱,还会改写倒数第二个柱,也就是说,我进行输入的信号可能在下一个柱出现反转。这是因为当柱体的颜色确定后,就会分析两个柱,所以建议将这种方法与其他辅助信号结合使用。


3. 趋势指标

现在,让我们来创建趋势指标。

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 烛图当前的价格值保存在 openclose 中间变量中。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 烛图的两个价格,开盘价和收盘价,这两个价格将决定其方向。


4. 在 EA 交易中使用趋势判断指标的举例

让我们创建一个使用不同指标的 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 交易测试结果

图 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. 横向移动部分

图 9. 横向移动部分

在这个图表上还有 SMA200 移动平均线。

现在,让我们看看,如果以相同的间隔和默认参数,用几条移动平均线构成的指标来构建 EA 交易,在哪些地方会更”先进“?

图 10. 使用 FanTrendDetector 指标的 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 结果

图 11. 平移部分的 FanTrendExpert 结果

图 9相比较 - 很显然,趋势改变的错误警告数量减少了。但是成交数量也减少了一半,这是合乎逻辑的结果。在分析两个 EA 交易的平衡/市值曲线时,您可以看到,在获得最大收益的方面,很多成交的收盘都低于优化值。因此,对 EA 交易进行的下一个升级,就是改善成交收盘的计算式。但这不属于本文讨论的范围。读者可以自己来完成。


5. EA 交易测试结果

让我们来测试所有的 EA。下面提供了从 1993 到 2010 年,EURUSD 货币对的历史波动区间,时间周期为 D1。

图12. MATrendExpert 测试

图 12. MATrendExpert 测试

图 13. FanTrendExpert 测试

图 13. FanTrendExpert 测试

图 14. ADXTrendExpert 测试 (ADXTrendLevel = 0)

图 14. ADXTrendExpert 测试 (ADXTrendLevel = 0)

图 15. ADXTrendExpert 测试 (ADXTrendLevel = 20)

图15. ADXTrendExpert 测试 (ADXTrendLevel = 20)

图 16.NRTRTrendExpert 测试

图 16. RTRTrendExpert 测试

图 17.Heiken Ashi 测试

图 17. Heiken Ashi 测试

让我们来看看这些测试结果。

领先两个最常用的 EA,一个是移动平均线,一个是扇形移动平均线。的确,这些 EA 只是简单地使用了上一个时间区间的价格平滑系列,但最接近跟随趋势(以及价格)规则。由于我们使用了比较“重“的 200 日移动平均线,市场波动的影响似乎消失了。

这些 EA 的成交数量低,但这并非是它们的缺点,因为仓位保留可能会持续几个月的时间,它所跟随的是 200 天的趋势。注意看 MATrendExpert 如何在三个趋势区间中交替:盈利、平移(在 EA 中)和资金损失,很有意思。

ADX 指标的趋势判断方法也呈现出很好的结果。PeriodADX 稍作改变,其值变成了 17,在整个历史过程中给出了更加一致的结果。区域强度的过滤效应并不显著。也许,您需要调整 ADXTrendLevel 参数,或者根据目前的市场波动,对其进行更动态的设置。有几个亏损周期,因此,需要采用其他的措施对平衡进行补偿。

无论是在测试的整个范围,还是在随机选择的长线间隔,NRTR 指标实际显示都是零利润。从某种程度而言,这表示这是一种非常稳定的趋势判断方法。也许,调整参数会让这个 EA 交易实现获利,比如进行必要的优化。

以 Heiken Ashi 为基础创建的 EA 交易明显没有盈利。尽管它的历史数据看起来还不错,也许是因为进行实时重新绘制的缘故。其测试结果非常不理想。也许,使用这个指标的平滑版本会得到更好的结果 - 平滑 Heiken Ashi,不会出现过于强烈的重新绘制倾向。

的确,如果一个系统中的用动态止损位建仓,创建一个目标水平,那么所有 EA 交易都会因之受益。此外,拥有一个资金管理系统也是个好办法,不仅可以最小化损失,还可以提高长期间隔的利润。


总结

由此可见,编写判断趋势的代码并不是那么困难。重要的是要亲自去做,还要有一个非常合理的想法,探索一些市场规则。如果您在这些规则方面的基础越深,您在根据这些规则建立交易系统时就会越自信。而这个交易系统也会保持很长的时间。