English Русский Español Deutsch 日本語 Português
preview
您应当知道的 MQL5 向导技术(第 17 部分):多币种交易

您应当知道的 MQL5 向导技术(第 17 部分):多币种交易

MetaTrader 5示例 | 21 一月 2025, 08:42
680 0
Stephen Njuki
Stephen Njuki

前言

本文是系列延续,MQL5 向导如何成为交易者原型思路快速测试的理想选择。对于许多开发智能系统和交易系统的人士来说,不仅需要持续学习、并紧跟机器学习的趋势,而且常见的交易和风险管理也很重要。因此,在这些系列中,我们要研究 MQL5 集成开发环境在这方面如何起作用,它不仅可以节省时间,还可以最低化编码错误。

我曾打算在这个篇幅里审视神经架构搜索(NAS),但意识到还有一些我尚未涵盖的基本原则值得研究,其首位是经由向导组装的智能系统如何交易多种证券。故此,我们将从新的交易设置中绕点道,并在本文研究一些基础知识。下一篇文章再研究 NAS。


概述

与单一货币交易不同,多币种交易降低了风险聚拢性。因为每个账户都设定了杠杆级别,因此可用保证金额度明确,当面对您必须交易多个品种的状况时,则要分配的可用保证金额度必须针对所有可用货币拆分。如果这些货币没有相关性、或呈负相关性,那就可以大大降低过度依赖其中一种货币的风险。

此外,如果一款交易系统不是如前所述同时跨多种货币开仓,将风险最小化;而是按顺序进行,其中每个货币对都在不同的时间考虑,那么就可利用跨市场机会。这将涉及到,如果持仓正在缩水,则取与持仓逆相关的货币,逐次开仓;如果持仓有浮盈,则相反。当选择跨市场机会时,除了参考相关性外,任何货币对中的配对货币即可作为保证金、或盈利,这令该过程特别有趣。

每个货币对的保证金盈利配对,也体现出多货币交易的优势,即对冲。与套利接近,但这种做法不仅遭到大多数经纪商所反对,而且在实践中也难以实施,多货币对进行对冲,基于它们的保证金或盈利货币,能由挂单进行管控,尤其在交易者可能希望在高影响度新闻事件、甚至持仓过周末的状况下。

然而,除了这些策略之外,多币种交易还允许交易者或智能系统查看并利用许多在市场观察中可交易对的特定货币趋势。那么举个例子,如果某个特定货币赚取有利的货币利息,您可以买入并持有一定数量的货币对,其在无担保利息套利下具有更高的利率,则赚的钱不仅来自任何价格变化,还来自利息。若智能系统是为多货种交易构建的,才能分析多个货币对、并下单,行之有效地利用这些跨货币设置的优势,而我们看到由向导组装的智能系统并不具备这些。

故此,在本文中,我们将寻求构建模板,可修改 MQL5 向导中所用的类,从而可在大致 2 种场景下组装智能交易系统。首先,我们将寻求得到一个款由向导组装的智能系统,它可以简单地分析多个货币对,且能并行开仓,以减轻风险敞口为目标。该方式可能需要进行更多的定制,因我们将修改向导组装时所用的主文件。最后,在第二种方时中,我们将研究向导组装智能系统,即参考相关货币对的关联性,并可以基于独立分析按顺序开仓。


MQL5 向导模板(选项-1):

由向导组装的智能系统有 3 个主要的锚点类,每个类都处于一个单独的文件里。这些是 'ExpertBase'、'ExpertTrade' 和 'Expert'。除了这 3 个锚点类之外,还有其它 3 个辅助类,即 'ExpertSignal'、'ExpertMoney' 和 'ExpertTrailling',在构建智能信号、资金管理、或尾随类时,分别自它们继承而来。在所有这些类中,'ExpertBase' 类定义了品种类对象,访问交易品种市场信息时默认采用('m_symbol')。如上所述,默认情况下,对于所有由向导组装的智能系统,只能交易一个品种,故在 'ExpertBase' 内初始化的品种类意在仅处理一个品种。

为其应对多品种,一个可能的解决方案是将该类的实例转换到一个数组。虽然这是可能的,但访问该品种类实例的类要调用许多函数来做这件事,且为了连贯性始终需要一个中间数组,这意味着只得对代码进行大量可行度存疑的修改。故此,对于经由向导组装多货种 EA,这并非好的解决方案。

我们将研究更可行、且类似的一种方式,是将伞状 'Expert' 类的实例设置为一个数组,其大小与我们将要测试的交易品种数量相匹配。始终在向导组装的智能交易系统的 '*.mq5' 文件中声明该类的一个实例,简单地将其转换为数组,实质上标记出所有主要下游定制,我们都必须要做。

//+------------------------------------------------------------------+
//| Global expert object                                             |
//+------------------------------------------------------------------+
CExpert ExtExpert[__FOLIO];
CExpertSignal *signals[__FOLIO];

为将智能类实例转换为数组,需要我们预定义一个可交易的货币对数组。我们所做如下代码:

//+------------------------------------------------------------------+
//| 'Commodity' Currency folio                                       |
//+------------------------------------------------------------------+
#define                          __FOLIO 9
string                           __F[__FOLIO] 

                                 = 
         
                                 {
                                 "AUDNZD","AUDUSD","AUDCAD","AUDJPY",
                                 "NZDUSD","NZDCAD","NZDJPY",
                                 "USDCAD",
                                 "CADJPY"
                                 };
input string                     __prefix="";
input string                     __suffix="";


上面的代码被修改为 'Expert' 类的自定义版本。由于我们正在修改这个类,那么我们必须按新名称保存一个它的实例,以便维持典型组装的默认设置。我们所用的名称是 'ExpertFolio.mqh',原名是 'Expert.mqh'。除了更改标题中的名称、及修改代码以外,我们还需要修改 'Init' 函数的清单,从而更包容与智能系统所附图表品种不匹配的品种。我们所做如下代码:

//+------------------------------------------------------------------+
//| Initialization and checking for input parameters                 |
//+------------------------------------------------------------------+
bool CExpert::Init(string symbol,ENUM_TIMEFRAMES period,bool every_tick,ulong magic)
  {
//--- returns false if the EA is initialized on a timeframe different from the current one
   if(period!=::Period())
     {
      PrintFormat(__FUNCTION__+": wrong timeframe (must be: %s)",EnumToString(period));
      return(false);
     }
     
   if(m_on_timer_process && !EventSetTimer(PeriodSeconds(period)))
      {
      PrintFormat(__FUNCTION__+": cannot set timer at: ",EnumToString(period));
      return(false);
      }
//--- initialize common information
   if(m_symbol==NULL)
     {
      if((m_symbol=new CSymbolInfo)==NULL)
         return(false);
     }
   if(!m_symbol.Name(symbol))
      return(false);
   
....
....


//--- ok
   return(true);
  }

上述修改都是针对智能交易类文件的副本进行,然后我们继续按照上的示意重命名,或者我们可以创建一个继承自 'Expert.mqh' 的新智能交易类,然后在这个新类中添加一个覆盖的 'Init' 函数。这个新类和文件将在智能系统主文件中引用。以下是其清单:

#include "Expert.mqh"
//+------------------------------------------------------------------+
//| Class CExfolio.                                                  |
//| Purpose: Base class expert advisor.                              |
//| Derives from class CExpertBase.                                  |
//+------------------------------------------------------------------+
class CExfolio : public CExpert
  {
protected:

   
public:

                     CExfolio(void);
                    ~CExfolio(void);
                    
   //--- initialization
   virtual bool      Init(string symbol,ENUM_TIMEFRAMES period,bool every_tick,ulong magic=0) override;
   
   //...

  };

如上面的代码中所见,在接口声明的末尾添加了覆盖命令,但是由于 Init 函数默认情况下不是'虚拟的(virtual)',因此原始智能系统类中的 'Init' 函数需要通过添加来修改。故此,那一行只能看起来是这样的:

//+------------------------------------------------------------------+
//| Initialization and checking for input parameters                 |
//+------------------------------------------------------------------+
bool CExfolio::Init(string symbol,ENUM_TIMEFRAMES period,bool every_tick,ulong magic)
  {
//--- returns false if the EA is initialized on a symbol/timeframe different from the current one
      
      bool _init=true;
      
      if(!CExpert::Init(symbol,period,every_tick,magic))
      {
         _init=false;
         //
         if(symbol!=_Symbol)
         {
            if(Reinit(symbol,period,every_tick,magic)){ _init=true; }
         }
      }
      
      CExpert::OnTimerProcess(true);
      
      if(CExpert::m_on_timer_process && !EventSetTimer(PeriodSeconds(_Period)))
      {
         printf(__FUNCTION__+": cannot set timer at: ",EnumToString(period));
         _init=false;
      }
      
//--- ok
      return(_init);
  }

此处的主要变化确保如若图表品种与投资组合品种不匹配,初始化不会失败。这肯定是一个“更整洁”的选项,因为我们创建的类文件要小得多,意即我们的覆盖度比第一种情况更少,不过我们需要修改内置的智能系统文件,这意味着对于每次终端更新,我们都必须将虚拟函数修饰符添加到 init 函数之中。

除了这些智能交易类的变化之外,由向导生成的智能系统文件还需要修改 OnInit、OnTick、OnTimer、OnDeInit 和 Trade 函数。要加入的是一个 for 循环,其迭代遍历自定义智能系统类中预先声明的所有货币。它们的声明伴有前缀和后缀字符串输入参数,从而确保它们的名字格式圆满,并出现在市场观察当中。此处高亮显示了访问可用品种的正确方式,而不是明确命名品种,并管理名称前缀/后缀,但即便如此,也需要有一份您感兴趣的品种预定义列表,以便从市场观察中很长的可用品种列表里正确筛选。

在自定义智能系统类中选择的品种曾被当作“商品”,尤其是在过去的冷战时代,因为它们的动量会受到商品价格的过度影响。故此,我们的列表选用 AUD(澳元)、NZD(新西兰元)、和 CAD(加元)作为主要商品。USD(美元)和 JPY(日元)的加入是为了波动性敞口,但它们并非“商品”组的成员。

向导中智能系统可用的信号类可以是任何内置类。这是因为对于首次实现,品种是并行执行的,可将风险敞口最小化。任何人的交易决策都不受其它品种发生情况的影响,故所选信号的设置将应用于所有品种。从测试来看,这意味着我们的测试运行在前向漫游、或交叉验证中将有更佳机会表现出色,因为不同的品种共享相同的设置,而曲线拟合更低。我们选用需要相对较少输入参数的 RSI 信号类,即:指标周期,和应用价格。


MQL5 向导模板(选项-2):

对于多货种交易的第二次实现,我们将寻求修改由向导组装好的智能系统文件,添加一个 'Select' 函数。该函数依赖交易投资组合中品种间的相关性,寻求仅交易多个品种(如果存在这些机会)从跨行情机会中获利。它本质上是我们在上面第一个选项中所研究方式的过滤器。

为了达成这一点,首先我们应当启用定时交易,令人惊讶的是,默认没有开启。我们在 'OnInit' 函数中用一行来做此事,如下所示:

//
ExtExpert[f].OnTimerProcess(true);

有了该设置,我们就可以专注于 'Select' 函数,其功能简单地分两个主要部分。第一部分处理无开仓的状况,需要在投资组合中的所有交易品种中选择一个相关性确认信号。这个信号是作为确认,因为请记住,我们仍然依赖信号类 RSI 信号作为所有品种的主要信号。故此,在我们的例子中,将通过每个品种收盘价缓冲区的自动关联来获得确认。从投资组合中选择具有最大正数值的品种。

我们使用最大的正数值,而非简单的幅度,因为我们的目标是趋势商品,或投资组合中趋势最强的品种。一旦我们得到这个,我们在收盘价缓冲区里查看最新值和最后一个值之间的价格变化,来判定它的趋势方向。正面变化是看涨,而负面变化是看跌。这个处理如下所列:

//+------------------------------------------------------------------+
//| Symbol Selector via Correlation                                  |
//+------------------------------------------------------------------+
bool  Select(double Direction, int &Index, ENUM_POSITION_TYPE &Type)
{  if(PositionsTotal() == 0)
   {  double _max = 0.0;
      int _index = -1;
      Type = INVALID_HANDLE;
      for(int f = 0; f < __FOLIO; f++)
      {  vector _v_0, _v_1;
         _v_0.CopyRates(__F[f], Period(), 8, 0, 30);
         _v_1.CopyRates(__F[f], Period(), 8, 30, 30);
         double _corr = _v_0.CorrCoef(_v_1);
         if(_max < _corr && ((Direction > 0.0 && _v_0[0] > _v_0[29]) || (Direction < 0.0 && _v_0[0] < _v_0[29])))
         {  _max = _corr;
            _index = f;
            if(_v_0[0] > _v_0[29])
            {  Type = POSITION_TYPE_BUY;
            }
            else if(_v_0[0] < _v_0[29])
            {  Type = POSITION_TYPE_SELL;
            }
         }
      }
      Index = _index;
      return(true);
   }
   else if(PositionsTotal() == 1)
   {  

//...
//...

   }
   return(false);
}


故此,上面的清单是 select 函数的'默认'部分,它处理尚未开仓的状况。一旦开仓,我们将根据开仓的表现继续寻找跨行情机会。如果初始持仓正在缩水,那么我们筛选剩余的投资组合品种,并尝试发现具有“逆”相关性效果的品种。“逆”是指我们只对幅度感兴趣,若与持仓相比,其与当前开仓品种的相关度最高,我们就会跟随该品种的趋势。处理代码如下:

//+------------------------------------------------------------------+
//| Symbol Selector via Correlation                                  |
//+------------------------------------------------------------------+
bool  Select(double Direction, int &Index, ENUM_POSITION_TYPE &Type)
{  if(PositionsTotal() == 0)
   {  
//...
//...

   }
   else if(PositionsTotal() == 1)
   {  ulong _ticket = PositionGetTicket(0);
      if(PositionSelectByTicket(_ticket))
      {  double _float = PositionGetDouble(POSITION_PROFIT);
         ENUM_POSITION_TYPE _type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
         int _index = ArrayBsearch(__F, PositionGetString(POSITION_SYMBOL));
         double _max = 0.0;
         Type = INVALID_HANDLE;
         for(int f = 0; f < __FOLIO; f++)
         {  if(f == _index)
            {  continue;
            }
            else
            {  vector _v_0, _v_1;
               _v_0.CopyRates(__F[_index], Period(), 8, 0, 30);
               _v_1.CopyRates(__F[f], Period(), 8, 0, 30);
               double _corr = fabs(_v_0.CorrCoef(_v_1));
               if(_float < 0.0 && _max < _corr)
               {  _max = _corr;
                  Index = f;
                  if(_v_1[0] > _v_1[29])
                  {  Type = POSITION_TYPE_BUY;
                  }
                  else  if(_v_1[0] < _v_1[29])
                  {  Type = POSITION_TYPE_SELL;
                  }
               }
            }
         }
      }
      return(true);
   }
   return(false);
}

为了在向导组装智能系统的环境下使用 'Select' 函数,我们只得对智能系统类进行更多修改。这就是为什么要创建智能系统类的副本实例,并将其重命名,尽管看似很蠢笨,但更务实。我们需要做的第一个修改是令函数 'Refresh' 可公开访问。这意味着将其从类接口的 'Protected' 部分移动到 'Public' 部分。类似地,函数 'Process' 也应被授予公开访问权限。该修改如下所示:

//+------------------------------------------------------------------+
//| Class CExpert.                                                   |
//| Purpose: Base class expert advisor.                              |
//| Derives from class CExpertBase.                                  |
//+------------------------------------------------------------------+
class CExpert : public CExpertBase
  {
protected:
   
   //...
   //...

public:
                     CExpert(void);
                    ~CExpert(void);
   
   //...

   //--- refreshing 
   virtual bool      Refresh(void);
   //--- processing (main method)
   virtual bool      Processing(void);
   
protected:
   
   //...
   //...

  };


这两个关键函数需要移至公开状态,因为我们必须在替换后的智能系统里手工调用它们。典型情况,它们由 'OnTick' 或 'OnTimer' 函数从内部调用,具体取决于启用两者中的哪一个。当调用它们时,它们的所作所为会调用品种的全部信号、尾随、和资金管理处理。在我们的例子中,我们仅欲处理某些品种,这取决于 a)它们的相对关联性,以及 b)我们是否已有一笔持仓。这显然需要我们'指挥'它们如何、以及何时被调用。我们将在 'OnTimer' 函数中如此行事,所示如下:

//+------------------------------------------------------------------+
//| "Timer" event handler function                                   |
//+------------------------------------------------------------------+
void OnTimer()
{  for(int f = 0; f < __FOLIO; f++)
   {  ExtExpert[f].Refresh();
   }
   for(int f = 0; f < __FOLIO; f++)
   {  int _index = -1;
      ENUM_POSITION_TYPE _type = INVALID_HANDLE;
      ExtExpert[f].Magic(f);
      if(Select(signals[f].Direction(), _index, _type))
      {  ExtExpert[f].OnTimer();
         ExtExpert[f].Processing();
      }
   }
}


在选择进行交易的品种时,我们为其赋予在源投资组合数组中的索引,并将其作为魔幻数字来跟踪它。这意味着在 'OnTick'、'OnTrade' 和 'OnTimer' 的所有默认处理函数中的每个 for 循环中,我们需要在处理之前更新跟踪类所用的魔幻数字。此外,由于该测试用到了自定义品种,出于数据频质原因,投资组合数组中品种的名称需要包含其后缀(或前缀,如果已用的话)。出于某种原因,如果您在初始化时添加了前缀和后缀,即使所有数据都存在于测试计算机上,策略测试器也无法同步自定义品种。

还有,我们希望在 'OnTimer' 里交易,而非默认的 'OnTick',理论上应该通过调整 'OnProcessTimer' 和 'OnProcessTick' 参数来轻松设置,不过修改它们要么会导致没有交易,要么就在逐笔报价时进行交易。这意味着需要在智能交易类的 OnTick 函数和 OnTimer 函数中里进行更具侵入性的修改,如此这般,在 'OnTick' 中禁用了 'OnProcess' 函数,而上面公开的 'OnProcess' 函数现在从智能交易的 'OnTimer' 函数中独立调用,如上面的 'OnTimer' 代码所示。由于未知原因,这同样是必要的,在撰写本文时,“ExpertFolio” 类中 “OnTimer” 中的 “Process” 函数无法执行。我们能够刷新所有品种的价格和指标值,但 'Process' 函数需要独立调用。而且需要声明计时器,并像普通的智能系统一样手工终止它。


测试和优化

我们依据 2023 年至 2024 年的 H4 时间帧对“商品” AUDJPY 执行了测试运行。我们的测试智能系统未使用止损,因为在上面概述的第二个选项中,亏损持仓由跨行情机会管控。我们只针对 RSI 周期进行优化,因为它是信号类指标,以及开仓和平仓阈值,限价单的过期、及限价单的入场空隙。所用资金管理是最小规模的固定手数。两种选项的测试报告和净值曲线如下所示:

r1

c1


r2

c2

如上面的报告所见,正如人们所期望的那样,与单一货币设置的情况相比,交易下单多得多。有趣的是,执行这些多笔交易的设置,而回撤管理也达到合理的极限,这指明其扩展应用潜力。此外,正如预期的那样,第二个选项的交易下单比第一个选项较少,因为 a)入场信号没有二级过滤器,并且 b)第二个选项中运用了跨行情(对冲)规则,把回撤和管理风险最小化。

除了跨行情机会外,还可以如介绍中所述探索未发现的利息套利机会,在这种情况下,需要从 mql5 财经日历中提取财经新闻数据。在策略测试器使用这些值仍然受限,但最近此处发布了一篇关于在使用 MQL5 IDE 的同时将财经新闻数据存储在数据库中的有趣文章,如果您愿意探索的话,那么值得一阅。


结束语

总而言之,我们探讨了如何将多货种交易引入由 MQL5 向导组装的智能交易系统。这些在无向导的情况下可以轻松编码,但 MQL5 向导不仅允许以更少的重复次数快速开发 EA,还允许通过加权系统并发测试多个思路。如常,新读者可以参考此处此处的指南,关乎如何使用向导。

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

附加的文件 |
ExpertFolio_1.mqh (121.5 KB)
ExpertFolio_2.mqh (121.49 KB)
opt_17_1.mq5 (7.04 KB)
opt_17_2.mq5 (10.18 KB)
人工电场算法(AEFA) 人工电场算法(AEFA)
本文介绍了一种受库仑静电力定律启发的人工电场算法(AEFA)。该算法通过模拟电学现象,利用带电粒子及其相互作用来解决复杂的优化问题。与其他基于自然法则的算法相比,AEFA具有独特性质。
化学反应优化(CRO)算法(第一部分):在优化中处理化学 化学反应优化(CRO)算法(第一部分):在优化中处理化学
在本文的第一部分中,我们将深入化学反应的世界并发现一种新的优化方法!化学反应优化 (CRO,Chemical reaction optimization) 利用热力学定律得出的原理来实现有效的结果。我们将揭示分解、合成和其他化学过程的秘密,这些秘密成为了这种创新方法的基础。
化学反应优化 (CRO) 算法(第二部分):汇编和结果 化学反应优化 (CRO) 算法(第二部分):汇编和结果
在第二部分中,我们将把化学运算符整合到一个算法中,并对其结果进行详细分析。让我们来看看化学反应优化 (CRO) 方法是如何解决测试函数的复杂问题的。
利用季节性因素进行外汇价差交易 利用季节性因素进行外汇价差交易
本文探讨了在外汇价差交易中利用季节性因素生成并提供报告数据的可能性。