English Русский Deutsch 日本語
preview
MQL5自优化智能交易系统(第八部分):多策略分析(3)—— 加权投票机制

MQL5自优化智能交易系统(第八部分):多策略分析(3)—— 加权投票机制

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

现在,我们为多策略EA加入最后一个组件:威廉姆斯百分比反转(WPR)策略。与本系列前几篇文章一样,我们首先手动硬编码实现该策略,以此作为性能基准,再与后续为交易程序编写的策略类进行对比。然而,为了避免让一直追随的读者感到枯燥,我们将省略用于验证策略类有效性的测试结果。目前读者只需了解:我们已完成充分测试,确保今天展示的策略类完整可靠。

在构建策略集成系统时,很自然会遇到一个问题:如何证明我们所选用的所有策略都是必不可少的?如何确认只保留其中少数几个策略,效果不会更好?我们又该如何验证这些猜想?

幸运的是,只要合理设定优化目标,遗传算法优化器就能帮我们轻松解答这些难题。 

为此,我们让所有策略以“民主投票”的方式协同工作。每个策略各持一票,而这一票的权重则作为可调参数,交由遗传优化器自动调整。如果优化器判定某项策略对整体收益没有正向贡献,就会把它的投票权重降至接近0。反之,对于能有效盈利的策略,则会提高其权重。

因此,我们将这套机制称为加权投票:首先给所有策略分配均匀的初始权重,作为性能基准。在本例中,我们将每个策略的初始投票权重设置为0.5(取值范围为0~1)。 

在此基础上,我们让遗传优化器自动调整这些权重,以最大化收益,并判断三种策略是否真正都有价值。

结果显示,该过程会返回大量不同的参数配置,每种配置都印证了一点:策略的有效性会随着具体参数的设置而变化。在每套独立的配置中,各策略的权重都会有所不同。可能在某套参数下,只有一个策略有效;而在另一套配置中,三个策略全部贡献正向收益。 

这使得“三种策略是否都是必需的?”成了一个颇具挑战性且极难回答的问题。我们的研究表明,答案高度依赖于程序最初采用的参数配置。让我们展开具体实践。


MQL5入门指南

到本文结束时,我们的交易策略继承结构树如下图1所示。我们将拥有3个独立的交易策略:

  1. 相对强弱指数(RSI)动量策略
  2. 移动平均线(MA)交叉策略
  3. 威廉姆斯百分比反转(WPR)策略

每个策略都具备通用功能,例如发出做多或做空信号的能力。这三个策略继承自同一个父类。这一点至关重要,能确保我们所有策略类保持统一的调用接口。

在今天的讨论中,我们将重点实现图1里三个策略中的最后一个:威廉姆斯百分比反转策略。在此之后,我们的遗传优化器将自动调整分配给每个策略的权重,确保盈利能力最弱的策略不会降低整个交易应用程序的整体表现。

图 1:当前交易策略共用的继承树结构

此外,图2中我们提供了可视化辅助说明,用于阐释本策略背后的核心理念。需要注意的是,图 2所示的示例中,所有投票权重的总和不等于1。尽管使用标准化约束是很常见的做法,但我们本次选择不做此限制。未来,我们可能会研究这种变体方案,因为它需要使用与本次实现略有不同的算法。

目前,我们仅允许遗传优化器为三个策略各自分配0~1之间(包含0和1)的权重值。如果遗传优化器无需考虑盈利能力最差的策略,那么就能更容易生成高收益策略组合。这正是本篇内容的核心目的:我们要证明,遗传优化器不仅能调优策略的重要参数,还能自动“精简”交易策略树。 

图 2:遗传优化器为各策略分配权重的可视化示意图

实现本策略的第一步是加载依赖项。第一个依赖项,与我们之前编写威廉姆斯百分比反转策略时一样,需要加载我们预先创建的单缓冲区威廉姆斯百分比指标类。随后,加载父类策略,该类同样是我们在之前探讨中开发完成的。

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

//+------------------------------------------------------------------+
//| Dependencies                                                     |
//+------------------------------------------------------------------+
#include <VolatilityDoctor\Indicators\WPR.mqh>
#include <VolatilityDoctor\Strategies\Parent\Strategy.mqh>

依赖项加载完成后,我们就可以开始定义类的成员。第一个成员是指向我们即将使用的威廉姆斯百分比反转指标的指针。将该成员设置为私有成员,也是本类中唯一的私有成员。其余成员均为公有成员。具体而言,包括构造函数、析构函数,以及从父类继承而来的虚方法。

class WPRReversal : public Strategy
  {
private:
                     //--- The instance of the RSI used in this strategy
                     WPR *my_wpr;

public:
                     //--- Class constructor 
                     WPRReversal(string user_symbol,ENUM_TIMEFRAMES user_timeframe,int user_period);
                     
                     //--- Class destructor
                    ~WPRReversal();
                    
                    //--- Class overrides
                    virtual bool Update(void);
                    virtual bool BuySignal(void);
                    virtual bool SellSignal(void);
  };

我们首先重写update方法。update方法的作用很简单:调用set_indicator_values方法 —— 该方法在我们所有的指标类中都已实现。该方法读取交易终端中的威廉姆斯百分比反转指标值,并填充到我们的指标缓冲区中。在返回之前,它会执行一次预防性检查,确保数据计数不为0。

//+------------------------------------------------------------------+
//| Our strategy update method                                       |
//+------------------------------------------------------------------+
bool WPRReversal::Update(void)
   {
      //--- Set the indicator value
      my_wpr.SetIndicatorValues(Strategy::GetIndicatorBufferSize(),true);
      
      //--- Check readings are valid
      if(my_wpr.GetCurrentReading() != 0) return(true);
      
      //--- Something went wrong
      return(false);
   }  

接下来,我们定义两个用于生成买入、卖出入场信号的方法。当各自对应的交易条件满足时,这两个方法会直接返回true。

//+------------------------------------------------------------------+
//| Check for our buy signal                                         |
//+------------------------------------------------------------------+
bool WPRReversal::BuySignal(void)
   {
      //--- Buy signals when the RSI is above 50
      return(my_wpr.GetCurrentReading()>50);
   }

//+------------------------------------------------------------------+
//| Check for our sell signal                                        |
//+------------------------------------------------------------------+
bool WPRReversal::SellSignal(void)
   {
      //--- Sell signals when the RSI is below 50
      return(my_wpr.GetCurrentReading()<50);
   }

最后,我们定义带参数的构造函数,它接收交易品种、时间周期和计算周期,用于初始化威廉姆斯百分比反转指标。而析构函数则负责直接删除我们为 WPR 类对象新实例创建的指针,释放资源。

//+------------------------------------------------------------------+
//| Our class constructor                                            |
//+------------------------------------------------------------------+
WPRReversal::WPRReversal(string user_symbol,ENUM_TIMEFRAMES user_timeframe,int user_period)
  {
   my_wpr = new WPR(user_symbol,user_timeframe,user_period);
   Print("WPRReversal Strategy Loaded.");
  }
  
//+------------------------------------------------------------------+
//| Our class destructor                                             |
//+------------------------------------------------------------------+
WPRReversal::~WPRReversal()
  {
   delete my_wpr;
  }
//+------------------------------------------------------------------+


构建EA

现在,我们将开始定义适用于当前框架的EA。EA的第一部分是系统常量,为了保证可以复现测试结果,我们将这些常量设置为固定值。简单参数 —— 例如移动平均偏移量、移动平均类型 —— 都必须固定。再将这些参数设置为便于记忆的数值,比如偏移量设置为0。

//+------------------------------------------------------------------+
//|                                                   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                                                 |
//+------------------------------------------------------------------+
//--- Fix any parameters that can afford to remain fixed
#define MA_SHIFT         0
#define MA_TYPE          MODE_EMA
#define RSI_PRICE        PRICE_CLOSE

此外,我们还必须接收特定的用户输入参数。回顾我们之前的类比:这些输入参数本质上是供遗传优化器调整的配置项。前三组输入参数对读者来说应该非常熟悉。这些只是我们技术指标所使用的计算周期。

在本文中,我们重点关注最后一组输入参数:全局策略参数组。我们今天设置的策略权重就存储在此处。像持仓周期、策略时间周期这类设置,老读者应该已经很熟悉了。然而,新读者需要了解的是,持仓周期指的是我们设定一个固定的时间,到期后便会执行平仓。显然,这个持仓周期与策略的时间框架高度相关。例如,在10分钟(M10)时间框架下,将持仓周期设置为5,就意味着我们会持有仓位50分钟后平仓。

//+------------------------------------------------------------------+
//| User Inputs                                                      |
//+------------------------------------------------------------------+
input   group          "Moving Average Strategy Parameters"
input   int             MA_PERIOD                       =        10;//Moving Average Period

input   group          "RSI Strategy Parameters"
input   int             RSI_PERIOD                      =         15;//RSI Period

input   group          "WPR Strategy Parameters"
input   int             WPR_PERIOD                      =         30;//WPR Period

input   group          "Global Strategy Parameters"
input   ENUM_TIMEFRAMES STRATEGY_TIME_FRAME             = PERIOD_D1;//Strategy Timeframe
input   int             HOLDING_PERIOD                  =         5;//Position Maturity Period
input   double          weight_1                        =       0.5;//Strategy 1 vote weight
input   double          weight_2                        =       0.5;//Strategy 2 vote weight
input   double          weight_3                        =       0.5;//Strategy 3 vote weight

接下来是我们的交易应用程序所需的依赖项。第一个依赖项是交易库,它作为我们的基础依赖,主要用于协助进行仓位管理。之后,是其他自定义依赖项,例如TimeInfo和TradeInfo,TimeInfo用于判断何时可以对市场信息执行操作,TradeInfo则分别提供最小交易手数、卖出价、最小交易价值等信息的访问接口。

剩余三个依赖项,均来自本系列教程中我们共同开发的策略类。这些内容您应该已经很熟悉了,如果您是新读者,那么最后一个依赖项您应该认识 —— 因为正是我们今天一起构建的。

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

我们还需要定义一些全局变量,例如用于管理自定义对象的指针,以及一个计时器 —— 用于追踪持仓距离到期平仓的剩余时间。

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

//--- Custom Types
CTrade               Trade;
Time                 *TradeTime;
TradeInfo            *TradeInformation;
RSIMidPoint          *RSIMid;
OpenCloseMACrossover *MACross;
WPRReversal          *WPRR;

//--- System Types
int                  position_timer;

首次初始化应用程序时,我们会为自定义类创建新的实例,包括各类策略类以及TradeInfo类。 

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Create dynamic instances of our custom types
   TradeTime        = new Time(Symbol(),STRATEGY_TIME_FRAME);
   TradeInformation = new TradeInfo(Symbol(),STRATEGY_TIME_FRAME);
   MACross          = new OpenCloseMACrossover(Symbol(),STRATEGY_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_TYPE);
   RSIMid           = new RSIMidPoint(Symbol(),STRATEGY_TIME_FRAME,RSI_PERIOD,RSI_PRICE);
   WPRR             = new WPRReversal(Symbol(),STRATEGY_TIME_FRAME,WPR_PERIOD);
//--- 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;
   delete RSIMid;
  }
//--- End of Deinit Scope

每当收到新的价格数据时,我们会首先检查是否已形成新的K线。如果确认产生新K线,我们就会更新各策略中的参数与指标数值。最后,如果当前无持仓,我们将重置持仓计时器,并检查交易信号条件。反之,如果已有持仓,我们会持续监控持仓距离到期的时间,并逐步准备平仓收尾。

//+------------------------------------------------------------------+
//| 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方法。 

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

check_signal方法的设计逻辑非常巧妙。简单来说,我们首先将总投票数初始化为0。在整个流程结束时,如果总投票数为正数,就执行买入操作;反之,如果总投票数为负数,则执行卖出操作。之后,我们逐一检查每个策略产生的信号:如果某个策略发出做多信号,就将该策略的权重加到总票数中。如果产生做空信号,就从总票数中减去该策略的权重。每个策略仅有一次投票机会。最后,按照上述规则对总票数进行判断,得出最终交易信号。

//+------------------------------------------------------------------+
//| Check for a trading signal using our cross-over strategy         |
//+------------------------------------------------------------------+
void CheckSignal(void)
  {
   double vote = 0;

   if(MACross.BuySignal())
      vote += weight_1;
   else
      if(MACross.SellSignal())
         vote -= weight_1;

   if(RSIMid.BuySignal())
      vote += weight_2;
   else
      if(RSIMid.SellSignal())
         vote -= weight_2;

   if(WPRR.BuySignal())
      vote += weight_3;
   else
      if(WPRR.SellSignal())
         vote -= weight_3;

//--- Long positions when the close moving average is above the open
   if(vote > 0)
     {
      Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,"");
      return;
     }

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

与其它应用程序一样,我们最后必须统一所有已定义的系统常量。

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef MA_SHIFT
#undef RSI_PRICE
#undef MA_TYPE
//+------------------------------------------------------------------+

现在,我们已经准备好对交易策略进行测试与优化。首先选择刚刚我们一起编写的EA。之后,指定用于测试应用程序的交易品种。如前所述,我们全程采用日线(D1)时间框架上的欧元兑美元(EURUSD) 货币对。通过自定义时间段选择测试日期,我们的测试区间为2023年2月至2025年5月。

图 3:用于遗传优化的参数设置与时间段

前半段数据用于回溯测试,后半段数据用于前瞻性测试。前瞻性测试的目的,是区分哪些策略具备稳定性,哪些策略可能过拟合回测数据。为了尽可能真实地模拟市场行情,我们始终采用随机延迟,并且模型应基于真实的价格变动(tick)数据进行计算。

图 4:设置遗传优化器的搜索范围,指定其需要在哪些数值区间内搜索策略参数

最后,对于优化部分,我们选择快速遗传算法。我们已尽力控制和限制策略中的参数总数这个特定的维度。但是正如我们所见,即便采用适中的设置,优化策略所需的总迭代步数依然大幅增加。几乎是瞬间就激增到了极大的量级。因此,我认为有必要将部分计算任务托管到MQL5云端进行。如需跟随操作,您必须先登录自己的MQL5账户,并且账户余额为正。 

图 5:通过MetaTrader 5终端登录您的MQL5用户账户

只需启动优化流程,然后右键单击本机可用的内核数,并选择使用MQL5云网络。所有这些操作都在策略测试器的Agents选项卡中完成。

图 6:启用MQL5云网络以加速回测

启用MQL5云网络后,本机上的部分计算任务将会被转移到云端执行。这样有助于加快优化过程,只要网络安全且稳定,我们就能更快地获得结果。

图 7:连接到您附近任一可用的数据中心

基于可获取的历史数据对遗传优化器进行测试后得出优化结果。这样能够评估策略的表现,并相应地调整参数以提升效果。然而,遗传优化器无法获取前瞻测试的结果 —— 这些结果反映的是所选策略参数在样本外数据上的表现。

由回测结果可见,盈利水平与我们之前版本交易应用程序的表现基本一致。观察表现最优的策略可以发现,各个子策略分配到的权重均落在0.4到0.8区间内。这些最佳的权重数值彼此十分接近,说明回测中表现最优的配置是同时启用了全部策略。

图 8:遗传优化过程的回测结果

然而,查看前瞻测试结果时我们发现,表现最优的策略大多只依赖其中两项策略。事实上,排名靠前的策略给策略三分配的权重都极低,有些甚至直接设置为0。

令人沮丧的是,在前瞻测试中表现优异的策略,只有少数在回测中也能实现盈利。不过,在两项测试均表现良好的策略中,我们再次看到策略三的权重很小,即便在找到的最稳定的配置里也是如此。

这样就促使我们产生了一个想法:舍弃策略三,因为它对前瞻测试中表现顶尖的策略并未做出实质性的贡献。然而,这一结论是基于找到的盈利最高的配置得出的,而这种方式并不总是可取,因为这样可能会导致我们过拟合现有的数据。

不过,纵观所有在两项测试中均实现盈利的策略,通常会发现:在大多数情况下,三个策略的权重都相对接近。只有一个突出的案例例外,策略三的权重最小,却取得了最优表现。在这种不确定性下做出决策是很困难的。然而,这正是我们所面临挑战的本质。

因此,为了最大程度地逼近最优表现,我们得出结论:策略三可能并不重要,后续将只保留前两个策略继续测试。

图 9:遗传优化过程的前瞻结果表明,策略三对我们的交易盈利或许并不重要


结论

正如您所见,在多策略集成模型中确定最优策略数量,是一项极具挑战性的工作。我们往往从一开始无法确定,究竟需要1个、5个还是10个不同的策略。

然而,核心结论是:遗传优化器能够轻松地帮我们解决这类难题。值得一提的是,遗传优化器远比开发者常用来解决量化编程难题的ChatGPT等大语言模型更强大。

正如我们在姊妹篇系列文章《克服人工智能的局限》中所提及的,领域认知算法本质上比通用算法更有价值。ChatGPT等大语言模型属于通用算法,而MetaTrader 5内置的遗传优化器则是领域认知算法,这使其在解决同类问题时,远优于直接向ChatGPT提问的处理方式。

我们还演示了如何使用MQL5云网络来加速回测与优化过程。事实上,如果没有MQL5云,本文相关的测试很可能无法按时完成。其上手简单,费用也十分亲民。

不仅如此,您可以获得24小时不间断的云计算服务,并配有多个冗余数据中心。即便因网络问题短暂断开,系统也几乎能始终保持连接。总而言之,MQL5云网络与遗传优化器,是当代算法交易开发者不可或缺的工具。

本次实验结果,将指导我们在后续为交易策略构建统计模型时的决策方向。

在接下来的探讨中,我们会进一步验证:仅使用前两个策略,或许就足以构建一个二分类任务 —— 判断策略一和策略二哪个盈利更多。之后,我们会将这套统计模型的表现,与我们最初一起开发的基础策略进行对比。

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

附加的文件 |
MSA_Test_3.mq5 (6.82 KB)
WPRReversal.mqh (3.44 KB)
交易策略 交易策略
各种交易策略的分类都是任意的,下面这种分类强调从交易的基本概念上分类。
MQL5 中的奇异谱分析(SSA) MQL5 中的奇异谱分析(SSA)
本文专为不熟悉奇异谱分析概念、希望充分理解并运用 MQL5 内置相关工具的读者编写。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
构建动态多品种EA(第三部分):均值回归与动量策略 构建动态多品种EA(第三部分):均值回归与动量策略
在本文中,我们将继续讲解构建动态多品种智能交易系统(EA)的第三部分内容,重点聚焦于均值回归策略与动量交易策略的融合。我们将详细拆解如何检测价格对均值的偏离(通过Z-分数)并据此执行交易,以及如何在多个外汇对上测算动量,以此确定交易方向。