English Deutsch 日本語
preview
在 MQL5 中构建自优化智能交易系统(第八部分):多策略分析

在 MQL5 中构建自优化智能交易系统(第八部分):多策略分析

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

作为算法交易员,我们面临许多挑战,在本系列文章中已经讨论过这些问题。例如,我们注意到,与预测未来价格水平相比,我们的统计模型更容易预测未来的技术指标值。

我们还研究了交易系统建模的好处,即对其所遵循的策略与应用该策略的市场之间的关系进行建模。

当我们的任务是预测技术指标的未来值,而非直接预测价格这类经典任务时,我们的模型表现始终更佳。直接价格预测难度很大,但通过改变问题的框架,即使使用相同的统计工具,我们也能超越那些局限于经典任务的模型。

今天,我们将基于之前的发现,探索一种新的潜在策略。如果我们创建一个包罗三种不同交易策略的应用程序会怎样?这个应用程序能否学会每次只选择一种策略,并定期切换到最盈利的那一种,而不是同时跟随这三种策略?如果应用程序能够定期更换策略,它能否从其掌握的三种策略中选出最佳的一种?

这样的应用程序可能比固定遵循所有三种策略或其组合的算法更有价值。

为了衡量我们统计模型的价值,我们首先需要一个基准绩效水平,这是我们的模型需要超越的目标。

我们将结合三种独立的交易策略:基于移动平均线交叉延续的策略、相对强弱指数(RSI)动量策略,以及威廉指标百分比范围趋势突破策略。稍后我们将详细解释每一种策略。

本文将介绍 MetaTrader 5 终端中的一些强大工具,重点讲解前向测试。前向测试不同于回测,二者的差异我们将稍后解释。

与简单的回测相比,前向测试能为我们提供更深入的洞见,尤其是当它与优化器结合使用,生成新的策略参数进行测试时更是如此。这使我们能稳定地获得让交易策略盈利的参数设置。MetaTrader 5 包含了这项高级功能,即快速和慢速遗传优化器。

通过将这些强大的策略测试工具与本系列文章所涵盖的面向对象的设计原则相结合,我们将设计、测试并验证一个强有力的竞争者,以此作为我们统计模型需要超越的标杆。


在 MQL5 中实现

本次讨论旨在解决如何最好地将不同策略整合为一个有效策略的问题。硬编码的解决方案并不常见,尤其是在同时使用多种策略时。

将策略进行组合之所以令人兴奋,是因为它需要创造力。但这同时也意味着我们必须最大限度地减少意外的副作用。

交易员经常将不同的策略搭配使用。例如,一种策略可能负责开仓,而另一种策略则决定何时平仓。每种策略只专注于解决问题的一部分。我们希望模仿这种人工方法,同时展示如何利用 MetaTrader 5 策略测试器来寻找合适的策略设置。

为了可靠地组合策略,我们将把每个策略封装在一个类中。必须对每个类进行测试,以证明其有效。我们将使用一个名为 “Parent” 的父类,作为所有策略变体的基类。该类将包含通用功能,如更新参数以及检查买入或卖出信号。

每个继承自父类的子类都将重新定义买入或卖出信号的判定标准。这是通过将共享方法声明为虚函数来实现的,允许每个策略安全地重写这些方法。

我们已经掌握了足够的知识,可以开始构建第一个类:父策略类。

在 MQL5 中,每个类的定义都以关键字 class 开头,后跟类名。按照惯例,文件名应与类名保持一致。

某些类成员将被标记为 virtual(虚函数),以告知编译器这些函数可以被子类重写。这使得每个策略都能安全地定义自己处理这些方法的方式。

//+------------------------------------------------------------------+
//|                                                     Strategy.mqh |
//|                                               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"

class Strategy
  {
private:
                     int  buffer_size;
                     bool buy;
                     bool sell;
public:
                     //--- Class constructors and destructors
                     Strategy(void);
                    ~Strategy(void);
                    
                    //--- Check if we have any valid trading signals from our strategy
                    virtual bool BuySignal(void);
                    virtual bool SellSignal(void);
                    
                    //--- Update the technical indicators in our strategy
                    virtual bool Update(void);
                    
                    //--- Get the size of the technical indicator buffers
                            int  GetIndicatorBufferSize(void);
  };

我们的默认构造函数会为策略的所有实例设置共用的默认值。

//+------------------------------------------------------------------+
//| The only way to create an object of the class                    |
//+------------------------------------------------------------------+
Strategy::Strategy(void)
  {
      //--- Upon initialization, both flags should be false 
      buy = false;
      sell = false;
      buffer_size = 10;
  }

我们需要若干实用方法,即通常所说的 getter(取值方法)和 setter(设值方法)。首先,我们将定义一个方法,用于返回当前为策略中所用指标选定的缓冲区大小。

//+------------------------------------------------------------------+
//| The size of our indicator buffer                                 |
//+------------------------------------------------------------------+
int Strategy::GetIndicatorBufferSize(void)
   {
      int res = buffer_size;
      return(res);
   }

我们还将要求每个策略必须有两个方法,分别用于通知我们是否存在买入或卖出信号。每个继承自基类的子类,都应实现定义其入场规则的逻辑。否则,父类将始终返回 false,并提示开发者需在子类中重写此方法。

//+------------------------------------------------------------------+
//| Check if our strategy is giving us any buy signals               |
//+------------------------------------------------------------------+
bool Strategy::BuySignal(void)
   {
      //--- The user is intended to overwrite the function in the child class
      //--- Otherwise, failing to do so will always return false as a safety feature
      Print("[WARNING] This function has only been implemented in the parent: ",__FUNCSIG__,"\nKindly make the necessary corrections to the child class");
      return(false);
   }
   
//+------------------------------------------------------------------+
//| Check if our strategy is giving us any sell signals              |
//+------------------------------------------------------------------+
bool Strategy::SellSignal(void)
   {
      //--- The user is intended to overwrite the function in the child class
      //--- Otherwise, failing to do so will always return false as a safety feature
      Print("[WARNING] This function has only been implemented in the parent: ",__FUNCSIG__,"\nKindly make the necessary corrections to the child class");
      return(false);
   }

更新策略对象意味着更新任何用于交易的策略参数。

//+------------------------------------------------------------------+
//| Update our strategy parameters                                   |
//+------------------------------------------------------------------+
bool Strategy::Update(void)
   {
      //--- The user is intended to overwrite the function in the child class
      Print("[WARNING] This function has only been implemented in the parent: ",__FUNCSIG__,"\nKindly make the necessary corrections to the child class");
      return(false);
   }

当前我们的类析构函数是空的。

//+------------------------------------------------------------------+
//| The class destructor is currently empty                          |
//+------------------------------------------------------------------+
Strategy::~Strategy(void)
  {
  }
//+------------------------------------------------------------------+

我们将从定义类的主体开始。该类名为 “OpenCloseMACrossover”。这是一个依赖两条周期相同的移动平均线的策略,它们分别应用于开盘价和收盘价数据源。请注意,父类中声明为虚函数的方法,在子类中仍然是虚函数。

当开盘价的移动平均线位于收盘价的移动平均线之上时,产生卖出信号。反之,则为买入信号。其逻辑是,如果平均收盘价高于平均开盘价,则价格行为可被视为看涨,且该方向的强劲趋势可能持续。

//+------------------------------------------------------------------+
//|                                         OpenCloseMACrossover.mqh |
//|                                               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"

#include<VolatilityDoctor\Strategies\Parent\Strategy.mqh>
#include<VolatilityDoctor\Indicators\MA.mqh>

class OpenCloseMACrossover : public Strategy
  {
private:
                     //--- Create 2 moving average instances
                     MA *ma_array[2];

public:
                     //---- Class constructors and destructor
                     OpenCloseMACrossover(string symbol,ENUM_TIMEFRAMES time_frame,int period,int shift,ENUM_MA_METHOD ma_mode);
                     ~OpenCloseMACrossover();
                     
                     //--- Class methods
                     virtual bool Update(void);
                     virtual bool BuySignal(void);
                     virtual bool SellSignal(void);
                    
  };

由于此前已明确了交易策略的各项规则,现在我们只需按部就班地编写校验逻辑代码即可。

//+------------------------------------------------------------------+
//| Check For a Buy Signal                                           |  
//+------------------------------------------------------------------+
bool OpenCloseMACrossover::BuySignal(void)
   {
      //--- Our buy signal is generated if the close moving average is above the open.
      return(ma_array[0].GetCurrentReading()>ma_array[1].GetCurrentReading());
   }

//+------------------------------------------------------------------+
//| Check For a Sell Signal                                          |  
//+------------------------------------------------------------------+
bool OpenCloseMACrossover::SellSignal(void)
   {
      //--- Our sell signal is generated if the open moving average is above the close.
      return(ma_array[0].GetCurrentReading()<ma_array[1].GetCurrentReading());
   }

我们的 Update 方法调用了为 SingleBufferIndicator 类构建的指标更新函数。此方法需要将缓冲区大小作为参数传递。我们在父类中创建了一个方法来返回缓冲区大小。我们在调用中使用双冒号 “::” 语法引用父类:“Strategy::GetIndicatorBufferSize()”。Update 方法在将控制权交还给调用上下文之前,最后会检查更新后的值不为零。

//+------------------------------------------------------------------+
//| Our update method                                                |  
//+------------------------------------------------------------------+
bool OpenCloseMACrossover::Update(void)
   {
      //--- Copy indicator readings 
      //--- We will always get the buffer size from the parent class
      ma_array[0].SetIndicatorValues(Strategy::GetIndicatorBufferSize(),true);
      ma_array[1].SetIndicatorValues(Strategy::GetIndicatorBufferSize(),true);
      
      //--- Make sure neither of the indicator values equal 0
      if((ma_array[0].GetCurrentReading() * ma_array[1].GetCurrentReading()) != 0) return(true);
      
      //--- If one/both indicator values equal 0, something went wrong.
      return(false);
   }

类的构造函数动态创建了两个移动平均指标对象的新实例,并将它们的指针存储在相同类型的指针数组中,该类型即我们自定义的 MA 类型。

//+------------------------------------------------------------------+
//| Our class constructor                                            |
//+------------------------------------------------------------------+
OpenCloseMACrossover::OpenCloseMACrossover(string symbol,ENUM_TIMEFRAMES time_frame,int period,int shift,ENUM_MA_METHOD ma_mode)
  {
      //--- Create two instances of our moving average indiator objects
      ma_array[0] = new MA(symbol,time_frame,period,shift,ma_mode,PRICE_CLOSE);
      ma_array[1] = new MA(symbol,time_frame,period,shift,ma_mode,PRICE_OPEN);
      
      //--- Give feedback
      Print("Strategy class loaded correctly");
  }

类的析构函数删除了我们创建的动态对象,帮助我们管理所消耗的内存。

//+------------------------------------------------------------------+
//| Our class destructor                                             |
//+------------------------------------------------------------------+
OpenCloseMACrossover::~OpenCloseMACrossover()
  {
   //--- Delete the custom objects we made
   delete ma_array[0];
   delete ma_array[1];
   
   //--- Give feedback
   Print("Strategy deinitialized correctly. Goodbye");
  }
//+------------------------------------------------------------------+

现在我们将继续测试我们的第一个策略类。请记住,我们的最终应用程序将运行三个类,即三种不同的策略。因此,作为优秀的开发者,我们必须对每个类进行单独测试,与相同策略的硬编码版本进行对比。如果两种策略在相同的时间段内回测结果相同,则测试通过。这可能会为我们未来节省数小时的调试时间。

我们将首先定义在两次测试中都要保持一致的系统常量。如果两个策略是等价的,它们应该在相同的时间被触发,开仓数量相同,且买入与卖出的比例相同。重复这些系统常量是我们测试中有意为之的一部分,因为这些常量控制着策略参数。

//+------------------------------------------------------------------+
//|                                                   MSA Test 1.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"

//+------------------------------------------------------------------+
//| Define system constants                                          |
//+------------------------------------------------------------------+
#define MA_TYPE        MODE_EMA
#define MA_PERIOD      10
#define MA_TIME_FRAME  PERIOD_D1
#define MA_SHIFT       0
#define HOLDING_PERIOD 5

接着,加载库。

//+------------------------------------------------------------------+
//| Dependencies                                                     |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
#include <VolatilityDoctor\Trade\TradeInfo.mqh>
#include <VolatilityDoctor\Time\Time.mqh>

接着,我们需要定义几个全局变量来控制技术指标,并记录持仓时间。

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+

//--- Custom Types
CTrade    Trade;
TradeInfo *TradeInformation;
Time      *TradeTime;

//--- System Types
double ma_open[],ma_close[];
int    ma_open_handler,ma_close_handler;
intn    position_timer; 

当系统初始化后,我们将加载技术指标并校验他们是否正常运行。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Our technical indicators
   ma_close_handler = iMA(Symbol(),MA_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_TYPE,PRICE_CLOSE);
   ma_open_handler = iMA(Symbol(),MA_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_TYPE,PRICE_OPEN);

//--- Create dynamic instances of our custom types
   TradeTime        = new Time(Symbol(),MA_TIME_FRAME);
   TradeInformation = new TradeInfo(Symbol(),MA_TIME_FRAME);


//--- Safety checks
   if(ma_close_handler == INVALID_HANDLE)
      return(false);
   if(ma_open_handler == INVALID_HANDLE)
      return(false);

//--- Everything was fine
   return(INIT_SUCCEEDED);
  }
//--- End of OnInit Scope

当系统停止使用时,我们将卸载此前加载的技术指标,并清除策略初始化阶段创建的所有动态对象。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Delete the indicators and dynamic objects
   IndicatorRelease(ma_close_handler);
   IndicatorRelease(ma_open_handler);

   delete TradeTime;
   delete TradeInformation;

  }
//--- End of Deinit Scope

交易终端收到最新报价后,系统将先检查是否形成了新的 K 线。若新 K 线已生成,系统会同步更新所有技术指标,随后根据持仓状态执行操作:空仓时触发交易信号校验,持仓时则继续持有,直到触发预设的平仓条件。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Check if a new daily candle has formed
   if(TradeTime.NewCandle())
     {

      //--- Update our technical indicators
      Update();

      //--- If we have no open positions
      if(PositionsTotal() == 0)
        {
         //--- Reset the position timer
         position_timer = 0;

         //--- Check for a trading signal
         CheckSignal();
        }

      //--- Otherwise
      else
        {
         //--- The position has reached maturity
         if(position_timer == HOLDING_PERIOD)
            Trade.PositionClose(Symbol());

         //--- Otherwise keep holding
         else
            position_timer++;
        }
     }
  }
//--- End of OnTick Scope

我们的更新函数会将当前指标的实时读数,从指标缓冲区写入预先创建好的数据存储数组中。

//+------------------------------------------------------------------+
//| Update our technical indicators                                  |
//+------------------------------------------------------------------+
void Update(void)
  {
//--- Call the CopyBuffer method to get updated indicator values
   CopyBuffer(ma_close_handler,0,0,1,ma_close);
   CopyBuffer(ma_open_handler,0,0,1,ma_open);
  }
//--- End of Update Scope

信号校验函数负责识别我们此前定义的交易条件:当收盘价均线位于开盘价均线之上时,即判定为多头行情。

//+------------------------------------------------------------------+
//| Check for a trading signal using our cross-over strategy         |
//+------------------------------------------------------------------+
void CheckSignal(void)
  {
//--- Long positions when the close moving average is above the open
   if(ma_close[0] > ma_open[0])
     {
      Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,"");
      return;
     }

//--- Otherwise short
   else
      if(ma_close[0] < ma_open[0])
        {
         Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,"");
         return;
        }

  }
//--- End of CheckSignal Scope

最后,务必清理并解绑自行定义的系统变量。

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef MA_PERIOD
#undef MA_SHIFT
#undef MA_TIME_FRAME
#undef MA_TYPE
#undef HOLDING_PERIOD
//+------------------------------------------------------------------+

接下来,我们将为该类设定基础性能基准线,以此作为后续优化的参照标准。 

图1:基准测试初始参数配置

接下来需设置回测的时间范围。建议你跟随我们的演示选择日线周期(D1)。

图2:基准测试使用的日期区间

只要确保两套策略参数保持一致,我们就能从自定义类模块中复现硬编码版本策略生成的资金曲线,以此验证类功能的准确性。接下来就可以开始检查我们编写的类是否存在功能缺陷。 

图3:可视化基准策略生成的资金曲线

两组测试的统计数据无需做到完全对齐。毕竟我们的回测机制会模拟随机延迟和系统性噪音。我们只需要确保两者的结果在误差允许范围内高度接近即可,而非要求精确一致。

图4:基于硬编码均线交叉策略生成的回测详细报告

为了保证测试结果具备可对比性,本次类模块测试将直接沿用之前定义的系统常量,不对参数做任何调整。

//+------------------------------------------------------------------+
//|                                                   MSA Test 1.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"

//+------------------------------------------------------------------+
//| Define system constants                                          |
//+------------------------------------------------------------------+
#define MA_TYPE        MODE_EMA
#define MA_PERIOD      10
#define MA_TIME_FRAME  PERIOD_D1
#define MA_SHIFT       0
#define HOLDING_PERIOD 5

接着,加载库。

//+------------------------------------------------------------------+
//| Dependencies                                                     |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
#include <VolatilityDoctor\Time\Time.mqh>
#include <VolatilityDoctor\Trade\TradeInfo.mqh>
#include <VolatilityDoctor\Strategies\OpenCloseMACrossover.mqh>

我们还需要为策略实例创建一个新的全局变量。

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+

//--- Custom Types
CTrade               Trade;
TradeInfo            *TradeInformation;
Time                 *TradeTime;
OpenCloseMACrossover *MACross;

//--- System Types
int    position_timer;

从整体上看,程序主体代码无需做出任何调整。我们正在单独测试类所产生的信号效果。因此,对于实现过第一次测试的读者来说,这部分代码应该会感到非常熟悉。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Create dynamic instances of our custom types
   TradeTime        = new Time(Symbol(),MA_TIME_FRAME);
   TradeInformation = new TradeInfo(Symbol(),MA_TIME_FRAME);
   MACross          = new OpenCloseMACrossover(Symbol(),MA_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_TYPE);

//--- Everything was fine
   return(INIT_SUCCEEDED);
  }
//--- End of OnInit Scope

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Delete the dynamic objects
   delete TradeTime;
   delete TradeInformation;
   delete MACross;
  }
//--- End of Deinit Scope

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Check if a new daily candle has formed
   if(TradeTime.NewCandle())
     {
      //--- Update strategy
      Update();

      //--- If we have no open positions
      if(PositionsTotal() == 0)
        {
         //--- Reset the position timer
         position_timer = 0;

         //--- Check for a trading signal
         CheckSignal();
        }

      //--- Otherwise
      else
        {
         //--- The position has reached maturity
         if(position_timer == HOLDING_PERIOD)
            Trade.PositionClose(Symbol());

         //--- Otherwise keep holding
         else
            position_timer++;
        }
     }
  }
//--- End of OnTick Scope

原有的策略参数更新与交易信号检测接口保持不变,但底层实现逻辑已重构:不再手动编写信号生成逻辑,而是直接调用我们此前封装好的类模块完成计算。

//+------------------------------------------------------------------+
//| Update our technical indicators                                  |
//+------------------------------------------------------------------+
void Update(void)
  {
//--- Update the strategy
   MACross.Update();
  }
//--- End of Update Scope

//+------------------------------------------------------------------+
//| Check for a trading signal using our cross-over strategy         |
//+------------------------------------------------------------------+
void CheckSignal(void)
  {
//--- Long positions when the close moving average is above the open
   if(MACross.BuySignal())
     {
      Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,"");
      return;
     }

//--- Otherwise short
   else
      if(MACross.SellSignal())
        {
         Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,"");
         return;
        }
  }
//--- End of CheckSignal Scope

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef MA_PERIOD
#undef MA_SHIFT
#undef MA_TIME_FRAME
#undef MA_TYPE
#undef HOLDING_PERIOD
//+------------------------------------------------------------------+

现在我们可以启动MQL5均线交叉策略类模块的验证测试。先加载调用了自定义类的EA程序,沿用首次回测的时间区间。

图5:基准策略评估初始时间区间(后续将用于样本外测试)

务必保持测试参数和首次完全一致。

图6:高可信度回测必备设置:勾选真实Ticks + 随机延迟

回测结果显示,类模块版本与硬编码版本的表现几乎完全对齐:两者均完成127笔交易,多空比保持一致,夏普比率也高度接近。

图7:类模块策略回测统计数据与硬编码版本完全匹配

由类模块版本生成的资金曲线与图 3 几乎重合。因此,两套策略的收益表现达到预期一致。

图8:类模块策略资金曲线与硬编码版本完全重合

一旦确认类模块功能无误,我们即可启动参数寻优工作。 

首先需要将大部分系统常量改造为用户可自定义的输入参数。这样才能利用遗传优化算法自动调参。最终仅保留少数核心系统级定义变量。

//+------------------------------------------------------------------+
//|                                                   MSA Test 1.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_SHIFT 0

//+------------------------------------------------------------------+
//| User Inputs                                                      |
//+------------------------------------------------------------------+
input   group          "Strategy Parameters"
input   int             MA_PERIOD      =        10;//Moving Average Period
input   int             HOLDING_PERIOD =         5;//Position Holding Period
input   ENUM_TIMEFRAMES MA_TIME_FRAME  = PERIOD_D1;//Moving Average Time Frame
input   ENUM_MA_METHOD  MA_TYPE        =  MODE_EMA;//Moving Average Type

我们系统的其余部分基本保持不变,因此现在让我们开始讨论 MetaTrader 5 中回测与前向测试的区别。

简而言之,回测就是在历史数据上运行交易策略。我们不应仅仅满足于简单的历史测试,而应对数据进行更深入的利用。通过将数据分割,我们可以使用一部分数据来搜索策略参数,然后用剩余的数据来验证找到的参数。

这就是前向测试的价值所在。我们不仅是在寻找好的参数设置,更是在试图了解这些设置的稳定性。

因此,将“Forward”参数设置为“1/2”,即可使用50%的数据进行训练,用另一半进行测试。

图9:将“Forward”字段设置为1/2,使用一半数据进行训练,另一半进行测试

MetaTrader 5 终端提供了不同的优化策略。我们将选择“快速遗传算法”,因为它对系统要求不高,但仍能提供可靠的结果。

图10:我们需要一个优化器在每次测试后生成新的策略参数

测试开始后,你会观察到类似下图(图11)的结果散点图。该图帮助我们可视化每次训练迭代的性能表现。优化器只能看到前半部分数据的结果,并利用这些结果学习更好的参数,以便在下一次迭代中尝试。

图11:我们设置快速遗传优化器来提升策略的夏普比率。每个点代表一次测试迭代的结果

我们可以右键单击这个结果散点图,并进行多种操作。我们可以更改绘制的坐标轴,甚至将结果图表转换为三维视图。

图12:上下文菜单允许我们更改图表的坐标轴并探索不同的关系

以不同的形式可视化数据,可以帮助我们观察所选择的策略与市场之间发生的现象。例如,看起来当我们选择更高的时间周期时,策略的盈利能力会增强。

图13:通过制作三维条形图,我们可以观察到更高的时间周期有助于获得更好的夏普比率

策略测试器还会提供它测试的每个输入组合所获得的详细结果。注意表格底部有一个面板,将“回测”和“前向”结果分开。 

图14:MetaTrader 5 还为我们提供了它尝试的所有策略参数的详细分析

用鼠标右键单击此表格将加载一个上下文菜单,允许我们执行许多有用的任务,例如决定表格中应包含哪些列,甚至将前向测试结果导出到文件。

图15:在结果表格上加载上下文菜单,显示我们可以将结果导出为XML格式以供进一步研究。

我们可以分别仔细查看回测和前向结果。回测结果显示在下方的图16中。请记住,我们更感兴趣的是前向测试结果,以及它们与回测结果的偏离程度。

图16:我们的遗传优化器获得的回测详细统计结果

前向测试结果与回测结果非常相似。这是潜在稳定性的一个良好指标。否则,如果最佳前向测试结果与回测结果不一致,则策略表现出不稳定的特性。

图17:这些是我们特别感兴趣的结果,即前向结果

我们还会获得两种测试生成的资金曲线。中间的长垂直线标记了回测和前向测试的分隔。

图18:请注意,在训练期间和前向测试中获得的资金曲线都具有正斜率

最后,这些是我们今天的遗传优化器帮助我们找到的最佳策略设置

图19:我们的遗传算法搜索获得的最佳设置


结论

本文展示了MetaTrader 5策略测试器的价值。即使读者不打算设计类,仍然可以将MQL5中面向对象编程(OOP)的原则整合到开发过程中,以此获得实际经验。本文还鼓励读者在创建和测试可靠类时采用良好的开发模式。

最后,通过使用OOP设计原则,读者可以可靠地构建策略,并轻松地测试它们在不同交易品种和时间周期上的最佳输入。请加入我们的下一次讨论,我们将结合RSI和移动平均线策略。 

文件名 文件描述
MSA Test 1 Baseline.mq5 我们的硬编码版本均线交叉策略将作为性能对照的基准,用于验证类模块的实现效果。
MSA Test 1 Class.mq5 测试移动平均策略类的文件。
MSA Test 1.mq5 我们使用这个EA,通过MetaTrader 5策略测试器来搜索好的策略参数。
OpenCloseMACrossover.mqh 实现均线交叉策略的类。
Strategy.mqh 所有策略的基类。
MSA Test 1.ex5 编译后的EA。

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

附加的文件 |
MSA_Test_1.mq5 (5.28 KB)
Strategy.mqh (3.98 KB)
MSA_Test_1.ex5 (44.16 KB)
交易策略 交易策略
各种交易策略的分类都是任意的,下面这种分类强调从交易的基本概念上分类。
使用机器学习开发趋势交易策略 使用机器学习开发趋势交易策略
本研究介绍了一种开发趋势跟踪交易策略的新方法。本节介绍标注训练数据并利用它训练分类器的过程。这个过程获得了可在 MetaTrader 5 上运行的完全可操作的交易系统。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
MQL5自动化交易策略(第十九部分):包络线趋势反弹剥头皮交易——交易执行与风险管理(下篇) MQL5自动化交易策略(第十九部分):包络线趋势反弹剥头皮交易——交易执行与风险管理(下篇)
我们将为MQL5中的包络线趋势反弹剥头皮策略实现交易执行模块与风险管理功能。我们实现了订单触发逻辑,并构建了包含止损设置与头寸规模计算在内的风险控制体系。最终在第十八部分的基础上完成策略回测与参数优化。