MQL5自优化智能交易系统(第八部分):多策略分析(3)—— 加权投票机制
现在,我们为多策略EA加入最后一个组件:威廉姆斯百分比反转(WPR)策略。与本系列前几篇文章一样,我们首先手动硬编码实现该策略,以此作为性能基准,再与后续为交易程序编写的策略类进行对比。然而,为了避免让一直追随的读者感到枯燥,我们将省略用于验证策略类有效性的测试结果。目前读者只需了解:我们已完成充分测试,确保今天展示的策略类完整可靠。
在构建策略集成系统时,很自然会遇到一个问题:如何证明我们所选用的所有策略都是必不可少的?如何确认只保留其中少数几个策略,效果不会更好?我们又该如何验证这些猜想?
幸运的是,只要合理设定优化目标,遗传算法优化器就能帮我们轻松解答这些难题。
为此,我们让所有策略以“民主投票”的方式协同工作。每个策略各持一票,而这一票的权重则作为可调参数,交由遗传优化器自动调整。如果优化器判定某项策略对整体收益没有正向贡献,就会把它的投票权重降至接近0。反之,对于能有效盈利的策略,则会提高其权重。
因此,我们将这套机制称为加权投票:首先给所有策略分配均匀的初始权重,作为性能基准。在本例中,我们将每个策略的初始投票权重设置为0.5(取值范围为0~1)。
在此基础上,我们让遗传优化器自动调整这些权重,以最大化收益,并判断三种策略是否真正都有价值。
结果显示,该过程会返回大量不同的参数配置,每种配置都印证了一点:策略的有效性会随着具体参数的设置而变化。在每套独立的配置中,各策略的权重都会有所不同。可能在某套参数下,只有一个策略有效;而在另一套配置中,三个策略全部贡献正向收益。
这使得“三种策略是否都是必需的?”成了一个颇具挑战性且极难回答的问题。我们的研究表明,答案高度依赖于程序最初采用的参数配置。让我们展开具体实践。
MQL5入门指南
到本文结束时,我们的交易策略继承结构树如下图1所示。我们将拥有3个独立的交易策略:
- 相对强弱指数(RSI)动量策略
- 移动平均线(MA)交叉策略
- 威廉姆斯百分比反转(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
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
新手在交易中的10个基本错误
构建动态多品种EA(第三部分):均值回归与动量策略