English Русский Deutsch 日本語
preview
构建自优化型MQL5智能交易系统(EA)(第3部分):动态趋势跟踪与均值回归策略

构建自优化型MQL5智能交易系统(EA)(第3部分):动态趋势跟踪与均值回归策略

MetaTrader 5示例 |
37 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

基于移动平均线的算法交易策略因其能让交易程序与长期市场趋势保持一致,从众多策略中脱颖而出。遗憾的是,当市场处于无明确长期趋势的盘整阶段时,原本可靠的趋势策略反而会引发问题。理解市场如何在“趋势”与“震荡”两种状态间切换,将显著提升我们使用均线策略的效能。

通常,交易者会先判断当前市场属于震荡还是趋势,再决定适用策略。然而,关于市场如何在这两种机制间切换的文献甚少。与其把市场看作“非此即彼”的静态环境。我们更希望将金融市场视为一种动态环境——市场持续在两种可能状态(趋势行情与震荡行情)之间交替切换,而不会长期固定于某一状态。我们今天的目标是构建一套单一动态交易策略,使其能够自主识别市场底层状态何时从趋势行情转变为震荡行情,或反之。

这一单一动态策略有望取代传统范式——即针对每种市场条件单独设计一套策略。所提出的策略基于滚动计算交易通道。其核心逻辑是:市场趋势行情与震荡行情之间存在明确边界。通过监测价格相对于该边界的位置,可实现更精准的交易决策。通道的上下轨通过以下方式计算:上轨 = 移动平均线均值 + ATR(平均真实波幅)的倍数;下轨 = 移动平均线均值 - ATR的倍数。我们会在本文的后续章节详细展开讨论。

需要说明的是,该通道每日动态更新,仅使用MetaTrader 5内置的标准技术指标。我们最初采用简单的趋势跟踪策略——当价格收盘价上穿100周期均线时做多,下穿时做空。开仓后,系统使用固定的止损和止盈进行管理。在EURUSD M1数据的4年回测中,该策略仅有52%的交易盈利。引入动态通道规则后,同一周期、同段行情,盈利交易占比跃升至86%,且未使用任何过拟合或AI技术。

这表明:精准估计市场状态边界的投入,能带来显著地回报。通过放弃对市场的刻板分类(如“非趋势即震荡”),转而追踪市场自然节奏,我们的交易系统实现了胜率的大幅跃升。此外,由于代码结构简洁,读者可轻松地基于自身对市场的理解扩展策略模板。 


交易策略概述

如前文所述,基于移动平均线的策略因能贴合长期市场趋势而广受欢迎。下图1提供了一个典型案例。该图为EURUSD日线图,展示2016年11月下旬至2018年4月的强劲牛市行情。无论以何种标准衡量,都让人印象深刻。由图可见,移动平均线清晰确认了价格的长期上行趋势。 

通常而言,当价格收盘价上穿均线时,可建立多头头寸;当价格收盘价下穿均线时,可建立空头头寸。 

趋势状态

图1:移动平均线趋势跟踪系统实践案例

图1所示案例体现了移动平均线策略在趋势市场中的核心优势——当市场呈现明确趋势时,该策略能稳定捕捉趋势收益。然而,若市场缺乏长期趋势(如图2所示的震荡行情),此类简单趋势跟踪策略将失效。 

震荡市场状态

图2:趋势跟踪系统长期亏损期案例

本文提出的策略可同时高效应对趋势与震荡两种市场状态,无需额外指标或复杂策略切换逻辑,即可实现动态自适应调整。接下来,我们将探讨如何将传统移动平均线策略升级为动态自调节系统。


MQL5入门指南

我们的交易系统由多个模块组成:

系统模块 设计目的
系统常量 这些常量对终端用户不可见,旨在确保回测过程中的系统行为的一致性,避免因主观偏差或意外修改导致交易逻辑失效。
库文件 我们仅引入了交易库,用于开仓与持仓管理。
全局变量 此类变量用于存储指标值、价格水平等数据,供系统各模块共享调用。
系统事件处理器 如OnTick()等函数是系统级事件触发器,以结构化的方式执行交易任务。
自定义函数 针对移动平均线趋势跟踪策略需求开发的专用函数。

我们首先定义系统常量。请注意,后续在所有的版本迭代中均保持这些参数不变。

//+------------------------------------------------------------------+
//|                              Dynamic Moving Average Strategy.mq5 |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
#define MA_PRICE    PRICE_CLOSE           //Moving average applied price
#define MA_MODE    MODE_EMA               //Moving average type
#define MA_SHIFT   0                      //Moving average shift
#define ATR_PERIOD 14                     //Period for our ATR
#define TF_1       PERIOD_D1              //Hihger order timeframe
#define TRADING_MA_PERIOD  100            //Moving average period
#define SL_WIDTH 1.5                      //How wide should the stop loss be?
#define CURRENT_VOL 0.1                   //Trading volume

接下来,我们将引入MQL5 API中最常用的库之一——交易库(Trade Library)。它是实现持仓高效管理的必需工具。

//+------------------------------------------------------------------+
//| Libraries                                                        |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
CTrade Trade;

我们还需要创建全局变量,用于存储市场价格数据和技术指标计算结果。

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
int    ma_handler,trading_ma_handler,atr_handler;
double ma[],atr[],trading_ma[];
double ask,bid;

我们的MQL5应用程序主体由事件处理器构成。 这些处理器会在终端发生新事件时被调用,例如收到新报价或是应用程序从图表被移除时。事件触发来源分为两类:终端用户操作和交易服务器推送。OnInit()处理器在用户启动交易应用时触发, 它会调用我们专为初始化全局变量设计的函数。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   setup();
//---
   return(INIT_SUCCEEDED);
  }

当终端用户从图表中移除交易应用时,OnDeinit()处理器会被触发执行。我们将利用该处理器释放系统内存资源,避免资源被持续占用。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
IndicatorRelease(atr_handler);
IndicatorRelease(ma_handler);
IndicatorRelease(trading_ma_handler);
  }

OnTick()处理器由交易服务器触发,而非终端用户。每当接收到最新价格数据时,该处理器即被调用执行。我们将通过调用专用函数来更新技术指标计算结果和存储最新价格数据,如果当前无持仓,系统将寻求开仓机会。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   update();
   if(PositionsTotal() == 0)
      find_setup();
  }
//+------------------------------------------------------------------+

我们基于趋势跟踪策略的开仓规则简单易懂。当价格运行于100周期移动平均线上方时,会通过固定止损开立多头头寸。反之,如果价格跌破移动平均线,则开立空头头寸。

//+------------------------------------------------------------------+
//| Find setup                                                       |
//+------------------------------------------------------------------+
void find_setup(void)
  {
//Buy on rallies
   if(iClose(Symbol(),PERIOD_CURRENT,0) > trading_ma[0])
      Trade.Buy(CURRENT_VOL,Symbol(),ask,(bid - (atr[0] * SL_WIDTH)),(bid + (atr[0] * SL_WIDTH)),"");

   if(iClose(Symbol(),PERIOD_CURRENT,0) < trading_ma[0])
      Trade.Sell(CURRENT_VOL,Symbol(),bid,(ask + (atr[0] * SL_WIDTH)),(ask - (atr[0] * SL_WIDTH)),"");
  }

调用OnInit()处理器函数,初始化技术指标。

//+------------------------------------------------------------------+
//| Setup our global variables                                       |
//+------------------------------------------------------------------+
void setup(void)
  {
   atr_handler =  iATR(Symbol(),TF_1,ATR_PERIOD);
   trading_ma_handler  =  iMA(Symbol(),PERIOD_CURRENT,TRADING_MA_PERIOD,MA_SHIFT,MA_MODE,MA_PRICE);
  }

OnTick()处理器将调用我们的Update()函数,用于更新全局变量。

//+------------------------------------------------------------------+
//| Update                                                           |
//+------------------------------------------------------------------+
void update(void)
  {
   static datetime time_stamp;
   datetime current_time = iTime(Symbol(),PERIOD_CURRENT,0);
   ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
   bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);

   if(time_stamp != current_time)
     {
      time_stamp = current_time;
      CopyBuffer(atr_handler,0,0,1,atr);
      CopyBuffer(trading_ma_handler,0,0,1,trading_ma);
     }
  }
//+------------------------------------------------------------------+

这是我们交易应用当前的实现状态。

//+------------------------------------------------------------------+
//|                              Dynamic Moving Average Strategy.mq5 |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
#define MA_PRICE    PRICE_CLOSE           //Moving average applied price
#define MA_MODE    MODE_EMA               //Moving average type
#define MA_SHIFT   0                      //Moving average shift
#define ATR_PERIOD 14                     //Period for our ATR
#define TF_1       PERIOD_D1              //Hihger order timeframe
#define TRADING_MA_PERIOD  100            //Moving average period
#define SL_WIDTH 1.5                      //How wide should the stop loss be?
#define CURRENT_VOL 0.1                   //Trading volume

//+------------------------------------------------------------------+
//| Libraries                                                        |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
CTrade Trade;

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
int    ma_handler,trading_ma_handler,atr_handler;
double ma[],atr[],trading_ma[];
double ask,bid;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   setup();
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
IndicatorRelease(atr_handler);
IndicatorRelease(ma_handler);
IndicatorRelease(trading_ma_handler);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   update();
   if(PositionsTotal() == 0)
      find_setup();
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Find setup                                                       |
//+------------------------------------------------------------------+
void find_setup(void)
  {
//Buy on rallies
   if(iClose(Symbol(),PERIOD_CURRENT,0) > trading_ma[0])
      Trade.Buy(CURRENT_VOL,Symbol(),ask,(bid - (atr[0] * SL_WIDTH)),(bid + (atr[0] * SL_WIDTH)),"");

   if(iClose(Symbol(),PERIOD_CURRENT,0) < trading_ma[0])
      Trade.Sell(CURRENT_VOL,Symbol(),bid,(ask + (atr[0] * SL_WIDTH)),(ask - (atr[0] * SL_WIDTH)));
  }

//+------------------------------------------------------------------+
//| Setup our global variables                                       |
//+------------------------------------------------------------------+
void setup(void)
  {
   atr_handler =  iATR(Symbol(),TF_1,ATR_PERIOD);
   trading_ma_handler  =  iMA(Symbol(),PERIOD_CURRENT,TRADING_MA_PERIOD,MA_SHIFT,MA_MODE,MA_PRICE);
  }

//+------------------------------------------------------------------+
//| Update                                                           |
//+------------------------------------------------------------------+
void update(void)
  {
   static datetime time_stamp;
   datetime current_time = iTime(Symbol(),PERIOD_CURRENT,0);
   ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
   bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);

   if(time_stamp != current_time)
     {
      time_stamp = current_time;
      CopyBuffer(atr_handler,0,0,1,atr);
      CopyBuffer(trading_ma_handler,0,0,1,trading_ma);
     }
  }
//+------------------------------------------------------------------+


建立基准测试

让我们通过M1时间框架的历史数据,检验趋势跟踪策略在2020年1月1日至2025年1月6日期间的表现。我们将使用EURUSD货币对进行回测。请注意,本次回测未对策略参数进行优化,因此回测的"Forward"模式设置为"No"(禁用)。 

图3:回测日期范围

为了更贴近真实交易环境,我们将回测模型设置为基于真实的Tick数据,并启用随机延迟功能,以模拟系统在压力条件下的表现。再次提醒,实际交易中的网络延迟存在波动,因此我们需确保回测条件尽可能接近生产环境。

图4:回测情况分析

这是该交易策略在5年回测期内生成的资金曲线。需要重点关注账户峰值资金与最终资金的差异。结果明确显示:策略表现不稳定,盈利与亏损的波动幅度相近。盈利周期短暂,且往往伴随同等时长的持续回撤期。长回撤期可能对应市场无明显趋势的阶段,而当前简单的策略无法有效应对此类市场行情。

图5:初始策略版本生成的资金曲线

当我们详细分析回测结果时,发现该策略在5年周期内整体盈利,这激励我们进一步优化策略。然而,盈利交易与亏损交易的比例接近50/50。这是不可取的。我们希望过滤掉亏损交易,以提高盈利交易占比。当前策略的平均连续盈利次数为2次,平均连续亏损次数也为2次,这印证了我们的判断:该系统盈利与亏损的概率几乎均等。如果无人监控,不可用于实盘交易。

图6:交易回测详细数据汇总


改进方案概述

初始回测结果促使我们深入探讨改进策略的设计逻辑。已知该策略在趋势市场中盈利,但在无趋势市场中亏损。那么,如何让系统自主判断市场处于趋势状态还是震荡状态?

理解市场如何在这两种模式间切换,将帮助系统更高效地利用移动平均线指标。当系统判定市场可能处于震荡区间时,将采取逆移动平均线趋势的交易方向。反之,则采取顺移动平均线趋势的交易方向。

换句话说,如果算法检测到交易品种处于震荡模式,当价格上穿移动平均线时,要卖出而非买入。然而,如果算法检测到趋势模式,当价格上穿移动平均线时,要买入。本质上来说,对同一市场事件的解读与操作,将完全取决于系统对市场模式的判定。

因此,核心问题转化为:如何识别当前市场模式?我们的解决方案是将价格区间划分为4个离散区域。该策略的基础逻辑如下:

  1. 趋势模式:市场仅在区域1或区域4中可能形成真实趋势。
  2. 震荡模式:市场仅在区域2或区域3中可能维持区间震荡。
4个区域的划分示意图如下图7所示:

图7:基于区域划分的交易系统示意图

我们按顺序逐区观察,先从区域1开始。当价格位于区域1时,视为看涨情绪,仅在收盘价高于100周期均线时寻找寻找买入机会。止盈和止损宽度要与初始回测保持一致。请注意,区域1内我们只会寻找长期机会来做多。而不建立空头头寸。

图8:区域1的市场意义解析

若价格从区域1跌入区域2,则市场情绪将发生转变。此时我们不再认为,只要价格仍处于区域2,市场就会形成任何真实趋势。相反,我们判断在区域2内,价格倾向于围绕区域2与区域3的分界中轴(mid-band)波动。因此,在区域2内,我们仅寻找做空机会,因为预期价格将回落至该中轴位置。

若价格在区域2内上穿100周期移动平均线,我们将执行卖出操作,止损设置沿用初始交易策略的规则。但是止盈位需要调整。将止盈设于区域2与区域3的分界中轴,因为我们推测,只要价格仍处于区域2,会趋于稳定在这一水平。请注意,在区域2内,禁止建立任何多头头寸。每个区域仅允许一种交易方向。

图9:解析我们的交易策略如何随已定义的4个市场区域转换而动态调整

我希望此时读者已能隐约察觉其中的规律,剩余的规则应能凭直觉理解。为验证共识,让我们做个趣味问答。若价格跌入区域3,您认为我们应采取何种交易方向?我希望您在心里说的是做多。

那么止盈位应设于何处?我相信读者已能凭直觉判断——无论处于区域2或区域3,止盈均设于两区域分界的中轴线。

换句话说,仅当价格收盘价跌破100周期移动平均线时,执行做多操作。我们将止盈位设于区域2-3分界中轴,而止损位将沿用初始策略的止损幅度。在区域3内不建立任何空头头寸。

图10:区域2与区域3被视为均值回归区域

最后,当价格进入区域4时,我们仅寻找做空机会。一旦价格持续位于100周期移动平均线下方,则执行做空操作。止盈和止损幅度与基准策略保持一致。在区域4内,无论价格如何波动,均不建立任何多头头寸

图11:区域4被视为空头趋势形成区因此,所有交易方向强制统一为做空,彻底排除多头干扰

要实施4区域策略,我们需对原始的趋势跟踪策略进行系统性调整。

调整建议 设计目的
新系统变量 我们需要引入新的系统变量,以便控制即将构建的通道参数。
用户输入参数设置 尽管用户无法完全控制通道的所有属性,但部分参数(如通道宽度、计算通道所用的K线数量)将开放给终端用户自定义。
新增全局变量 需创建与通道相关的全局变量,以优化交易应用的逻辑判断能力。
自定义函数调整 我们将在交易应用中新增函数,并修改部分现有函数,以实现既定的交易策略逻辑。

通道的具体计算方法将在后续章节详述。


MQL5实现方案

首先需设定用于通道计算的系统常量。计算通道前,必须先在高于目标周期的时间框架上应用移动平均线。例如,我们想在M1周期交易,于是把20周期移动平均线放在日线上,再用它计算通道。避免基于相同周期计算导致通道波动过大,从而影响策略的稳定性。

#define MA_PERIOD  20                     //Moving Average period for calculating the mid point of our range zone

接着,我们需要定义全局变量,持续跟踪通道位置。具体而言,我们需要明确区域2上边界和区域3下边界的位置。此外,需精准定位区域2与区域3的分界中轴,作为两区域的过渡基准。区域1从上边界开始,向上无上限。同样,区域4始于区域3的终点,向下无下限。

double range_zone_mid,range_zone_ub,range_zone_lb;
int    active_zone;

接下来,我们定义允许终端用户修改的通道参数。例如历史数据计算周期,以及用户期望的通道宽度。由于通道宽度与账户风险直接相关,因此允许用户调整通道宽度至关重要。在本示例中,我们将通道宽度固定为两倍ATR。多数品种单日波动幅度接近1倍ATR,避免通道因短期价格波动而频繁变动。

//+------------------------------------------------------------------+
//| User inputs                                                      |
//+------------------------------------------------------------------+
input group "Technical Analysis"
input double atr_multiple = 
2 ;            //ATR Multiple
input int    bars_used = 30;              //How Many Bars should we use to calculate the channel?

我们需要开发一个函数,用于实时判断当前价格所处的区域。区域划分的逻辑已在前文中详细说明,此处不再赘述。

//+------------------------------------------------------------------+
//| Get our current active zone                                      |
//+------------------------------------------------------------------+
void get_active_zone(void)
  {
   if(iClose(Symbol(),PERIOD_CURRENT,0) > range_zone_ub)
     {
      active_zone = 1;
      return;
     }

   if((iClose(Symbol(),PERIOD_CURRENT,0) < range_zone_ub) && (iClose(Symbol(),PERIOD_CURRENT,0) > range_zone_mid))
     {
      active_zone = 2;
      return;
     }

   if((iClose(Symbol(),PERIOD_CURRENT,0) < range_zone_mid) && (iClose(Symbol(),PERIOD_CURRENT,0) > range_zone_lb))
     {
      active_zone = 3;
      return;
     }

   if(iClose(Symbol(),PERIOD_CURRENT,0) < range_zone_lb)
     {
      active_zone = 4;
      return;
     }
  }
//+------------------------------------------------------------------+

我们还需要更新设置函数。请忽略函数中未修改的部分。我们本次更新仅引入1个新的技术指标。我们应用的第一项技术指标是M1时间框架下的100周期移动平均线。新增指标是日线时间框架下的20周期移动平均线。

//+------------------------------------------------------------------+
//| Setup our global variables                                       |
//+------------------------------------------------------------------+
void setup(void)
  {
   //We have omitted parts of the code that have not changed
   ma_handler  =  iMA(Symbol(),TF_1,MA_PERIOD,MA_SHIFT,MA_MODE,MA_PRICE);
  }

接下来,我们需要对更新函数进行调整。为突出核心修改,我们已省略未变动的函数部分,仅聚焦函数新增的逻辑。我们首先初始化一个全零向量。随后,利用新向量,从我们在上一步于日线周期应用的移动平均线中,复制用户指定数量的K线数据用于计算。

直接取20周期日线移动平均线的平均值,作为区域2与区域3的中轴基准。通过在中轴基础上加上ATR的倍数计算区域2上限;通过在中轴基础上减去ATR的倍数计算区域3下限。

//+------------------------------------------------------------------+
//| Update                                                           |
//+------------------------------------------------------------------+
void update(void)
  {
   static datetime time_stamp;
   datetime current_time = iTime(Symbol(),PERIOD_CURRENT,0);
   ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
   bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);

   if(time_stamp != current_time)
     {
      //Omitted parts of the function that remained unchanged
      vector ma_average = vector::Zeros(1);
      ma_average.CopyIndicatorBuffer(ma_handler,0,1,bars_used);
      range_zone_mid = ma_average.Mean();
      range_zone_ub = (range_zone_mid + (atr[0] * atr_multiple));
      range_zone_lb = (range_zone_mid - (atr[0] * atr_multiple));
      get_active_zone();
      Comment("Zone: ",active_zone);
      ObjectDelete(0,"RANGE HIGH");
      ObjectDelete(0,"RANGE LOW");
      ObjectDelete(0,"RANGE MID");
      ObjectCreate(0,"RANGE MID",OBJ_HLINE,0,0,range_zone_mid);
      ObjectCreate(0,"RANGE LOW",OBJ_HLINE,0,0,range_zone_lb);
      ObjectCreate(0,"RANGE HIGH",OBJ_HLINE,0,0,range_zone_ub);
     }
  }

最后一项修改涉及交易信号生成逻辑的优化。由于此前已详细讨论过仓位布局的交易逻辑,本段代码逻辑对读者而言应该较为直观。核心思路就是我们只在区域1与区域4内顺势交易。意思是,当价格收盘价上穿M1时间框架的100周期移动平均线时,将做多。否则,当价格处于区域2或区域3时,我们将逆势交易,意味着价格在区域2内的收盘价上穿100周期移动平均线,则做空(与区域1操作相反)。

//+------------------------------------------------------------------+
//| Find setup                                                       |
//+------------------------------------------------------------------+
void find_setup(void)
  {

// Follow the trend
   if(active_zone == 1)
     {
      //Buy on rallies
      if(iClose(Symbol(),PERIOD_CURRENT,0) > trading_ma[0])
         Trade.Buy(CURRENT_VOL,Symbol(),ask,(bid - (atr[0] * 1.5)),(bid + (atr[0] * SL_WIDTH)),"");
     }

// Go against the trend
   if(active_zone == 2)
     {
      //Sell on rallies
      if(iClose(Symbol(),PERIOD_CURRENT,0) > trading_ma[0])
         Trade.Sell(CURRENT_VOL,Symbol(),bid,(ask + (atr[0] * 1.5)),range_zone_mid);
     }

// Go against the trend
   if(active_zone == 3)
     {
      //Buy the dip
      if(iClose(Symbol(),PERIOD_CURRENT,0) < trading_ma[0])
         Trade.Buy(CURRENT_VOL,Symbol(),ask,(bid - (atr[0] * 1.5)),range_zone_mid,"");
     }

// Follow the trend
   if(active_zone == 4)
     {
      //Sell the dip
      if(iClose(Symbol(),PERIOD_CURRENT,0) < trading_ma[0])
         Trade.Sell(CURRENT_VOL,Symbol(),bid,(ask + (atr[0] * atr_multiple)),(ask - (atr[0] * SL_WIDTH)));
     }
  }

综合以上所有修改,这就是我们优化交易策略后的最终版本。

//+------------------------------------------------------------------+
//|                              Dynamic Moving Average Strategy.mq5 |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
#define MA_PRICE    PRICE_CLOSE           //Moving average shift
#define MA_MODE    MODE_EMA               //Moving average shift
#define MA_SHIFT   0                      //Moving average shift
#define ATR_PERIOD 14                     //Period for our ATR
#define TF_1       PERIOD_D1              //Hihger order timeframe
#define MA_PERIOD  20                     //Moving Average period for calculating the mid point of our range zone
#define TRADING_MA_PERIOD  100            //Moving average period
#define SL_WIDTH   1.5                    //How wide should the stop loss be?
#define CURRENT_VOL 0.1                   //Trading volume

//+------------------------------------------------------------------+
//| Libraries                                                        |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
CTrade Trade;

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
int    ma_handler,trading_ma_handler,atr_handler;
double ma[],atr[],trading_ma[];
double range_zone_mid,range_zone_ub,range_zone_lb;
double ask,bid;
int    active_zone;

//+------------------------------------------------------------------+
//| User inputs                                                      |
//+------------------------------------------------------------------+
input group "Technical Analysis"
input double atr_multiple = 1;            //ATR Multiple
input int    bars_used = 30;              //How Many Bars should we use to calculate the channel?

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   setup();
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   IndicatorRelease(ma_handler);
   IndicatorRelease(atr_handler);
   IndicatorRelease(trading_ma_handler);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   update();
   if(PositionsTotal() == 0)
      find_setup();
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Find setup                                                       |
//+------------------------------------------------------------------+
void find_setup(void)
  {

// Follow the trend
   if(active_zone == 1)
     {
      //Buy on rallies
      if(iClose(Symbol(),PERIOD_CURRENT,0) > trading_ma[0])
         Trade.Buy(CURRENT_VOL,Symbol(),ask,(bid - (atr[0] * 1.5)),(bid + (atr[0] * SL_WIDTH)),"");
     }

// Go against the trend
   if(active_zone == 2)
     {
      //Sell on rallies
      if(iClose(Symbol(),PERIOD_CURRENT,0) > trading_ma[0])
         Trade.Sell(CURRENT_VOL,Symbol(),bid,(ask + (atr[0] * 1.5)),range_zone_mid);
     }

// Go against the trend
   if(active_zone == 3)
     {
      //Buy the dip
      if(iClose(Symbol(),PERIOD_CURRENT,0) < trading_ma[0])
         Trade.Buy(CURRENT_VOL,Symbol(),ask,(bid - (atr[0] * 1.5)),range_zone_mid,"");
     }

// Follow the trend
   if(active_zone == 4)
     {
      //Sell the dip
      if(iClose(Symbol(),PERIOD_CURRENT,0) < trading_ma[0])
         Trade.Sell(CURRENT_VOL,Symbol(),bid,(ask + (atr[0] * atr_multiple)),(ask - (atr[0] * SL_WIDTH)));
     }
  }

//+------------------------------------------------------------------+
//| Setup our global variables                                       |
//+------------------------------------------------------------------+
void setup(void)
  {
   atr_handler =  iATR(Symbol(),TF_1,ATR_PERIOD);
   trading_ma_handler  =  iMA(Symbol(),PERIOD_CURRENT,TRADING_MA_PERIOD,MA_SHIFT,MA_MODE,MA_PRICE);
   ma_handler  =  iMA(Symbol(),TF_1,MA_PERIOD,MA_SHIFT,MA_MODE,MA_PRICE);
  }

//+------------------------------------------------------------------+
//| Update                                                           |
//+------------------------------------------------------------------+
void update(void)
  {
   static datetime time_stamp;
   datetime current_time = iTime(Symbol(),PERIOD_CURRENT,0);
   ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
   bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);

   if(time_stamp != current_time)
     {
      time_stamp = current_time;
      CopyBuffer(ma_handler,0,0,1,ma);
      CopyBuffer(atr_handler,0,0,1,atr);
      CopyBuffer(trading_ma_handler,0,0,1,trading_ma);
      vector ma_average = vector::Zeros(1);
      ma_average.CopyIndicatorBuffer(ma_handler,0,1,bars_used);
      range_zone_mid = ma_average.Mean();
      range_zone_ub = (range_zone_mid + (atr[0] * atr_multiple));
      range_zone_lb = (range_zone_mid - (atr[0] * atr_multiple));
      get_active_zone();
      Comment("Zone: ",active_zone);
      ObjectDelete(0,"RANGE HIGH");
      ObjectDelete(0,"RANGE LOW");
      ObjectDelete(0,"RANGE MID");
      ObjectCreate(0,"RANGE MID",OBJ_HLINE,0,0,range_zone_mid);
      ObjectCreate(0,"RANGE LOW",OBJ_HLINE,0,0,range_zone_lb);
      ObjectCreate(0,"RANGE HIGH",OBJ_HLINE,0,0,range_zone_ub);
     }
  }

//+------------------------------------------------------------------+
//| Get our current active zone                                      |
//+------------------------------------------------------------------+
void get_active_zone(void)
  {
   if(iClose(Symbol(),PERIOD_CURRENT,0) > range_zone_ub)
     {
      active_zone = 1;
      return;
     }

   if((iClose(Symbol(),PERIOD_CURRENT,0) < range_zone_ub) && (iClose(Symbol(),PERIOD_CURRENT,0) > range_zone_mid))
     {
      active_zone = 2;
      return;
     }

   if((iClose(Symbol(),PERIOD_CURRENT,0) < range_zone_mid) && (iClose(Symbol(),PERIOD_CURRENT,0) > range_zone_lb))
     {
      active_zone = 3;
      return;
     }

   if(iClose(Symbol(),PERIOD_CURRENT,0) < range_zone_lb)
     {
      active_zone = 4;
      return;
     }
  }
//+------------------------------------------------------------------+

让我们验证这些调整是否能实现预期的控制效果。我们将固定回测的时间周期。

图12:回测周期与初始测试周期保持一致

同时,我们将确保回测条件与先前的测试完全相同。

图13:验证两种策略在相同条件下进行测试

优化后的策略包含两个可调节的输入参数。其一是价格通道的波动范围,其二是用于通道计算的K线数量。 

图14:终端用户可调节的控制参数

尽管优化后的新策略的资金曲线与其他策略一样存在回撤期,但其显著特点是亏损后恢复速度极快。与初始策略长期陷入亏损泥潭不同。新策略在单笔亏损后能迅速重新与市场趋势对齐。这体现在每次亏损期后都会伴随一段持续盈利的交易阶段。

图15:新交易策略生成的资金曲线

通过详细分析新策略的交易数据,我们发现盈利率从52%提升至86%,而亏损率从47%降至13%。平均单笔盈利小于平均亏损,因采用固定止损/止盈机制,此问题可通过动态止损策略优化:亏损时保持止损固定,盈利时启用追踪止损。另外,连续盈利次数从2次增至9次,连续亏损次数从2次降至1次。初始策略的总交易次数为300笔。而我们的新策略总交易次数为1301笔。新策略交易频率更高且盈利率显著提升。 

图16:新策略性能的详细数据汇总


结论

在今天的讨论中,我们以一个基础的趋势跟踪策略为起点,通过引入MetaTrader 5终端中的历史数据,引导其逐步实现更合理的交易决策。理论上讲,这一策略完全可通过手工计算实现,但幸运的是,MQL5 API为我们大幅简化了整个流程。从利用向量函数快速计算统计指标,到实现最优交易执行,MetaTrader 5应用已在后台默默完成了大量复杂工作。在后续文章中,我们将进一步探索如何提升策略的盈利交易占比,并加强对亏损交易规模的控制。 

附件文件 描述
基准移动平均线策略 原趋势跟踪策略的初始实现版本
动态移动平均线策略 我们新提出的动态优化策略

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/16856

交易策略 交易策略
各种交易策略的分类都是任意的,下面这种分类强调从交易的基本概念上分类。
让新闻交易轻松上手(第六部分):执行交易(3) 让新闻交易轻松上手(第六部分):执行交易(3)
在本文中,将实现基于新闻事件ID对单个新闻事件进行新闻筛选。此外,还将对先前的SQL查询进行改进,以提供更多信息或减少查询运行时间。另外,还将使前几篇文章中构建的代码具备实际功能。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
交易中的多项式模型 交易中的多项式模型
本文将介绍正交多项式。正交多项式的应用,可以成为更准确、更有效地分析市场信息的基础,从而帮助交易者做出更明智的决策。