English Русский Español Deutsch 日本語 Português
preview
算法交易中的风险管理器

算法交易中的风险管理器

MetaTrader 5交易 | 15 十一月 2024, 10:03
710 0
Aleksandr Seredin
Aleksandr Seredin

内容


引言

在本文中,我们将开发一个风险管理器类来控制算法交易中的风险。本文的目的是将控制风险的原则应用于算法交易,并在一个单独的类中实现它们,以便每个人都可以验证风险标准化方法在日内交易和金融市场投资中的有效性。本文将使用和补充上一篇文章"《手动交易风险管理器》"中所总结的内容。在之前的文章中,我们了解到风险控制可以显著提升即使是盈利策略的交易结果,并能在短时间内保护投资者免受大幅回撤的影响。

根据上一篇文章的读者反馈,本文将还阐述选择实施方法的标准,以使文章对初学者来说更加易懂。我们还将在代码注释中阐述相关交易的定义。相应地,经验丰富的开发者可以利用所提供的材料,将代码修改成适合他们自身程序架构的代码。

本文中使用的其他概念和定义:

高低价(High\low):在某一时间段内,由一根柱状图或K线图所表示的价格最高点或最低点。

止损(Stop Loss):是指在出现亏损时退出持仓的限价。如果我们持仓的价格走势与我们预期的方向相反,那么在损失超过我们设定的数值之前,我们通过平仓来限制持仓的亏损。这些数值是在开仓时计算得出的。

止盈(Take Profit):是指带着利润平仓的限价。此价格旨在平仓并锁定已获得的利润。它通常被设定为获取预期的利润或交易对象日常波动范围尽头的区域。也就是说,当明确交易对象在特定时间段内没有进一步的价格变动潜力,并且随后更可能出现反向运动时。

技术形态止损:是根据技术分析设定的止损值,例如,根据所使用的交易策略,它可以是基于K线的高低点、突破、分型等。此方法的主要特征是,我们根据特定的信息在图表上设置止损位。在这种情况下,入场点可能会改变,但止损值可能保持不变。在这种情况下,我们假设如果价格达到止损值,那么技术形态将被视为已破位,并且交易品种的走势因此将不再延续。 

算数止损:是根据交易对象在特定时间段内的波动的某个计算值来设定的止损。它的不同之处在于,它与图表上的任何形态都不相关。采用这种方法时,找到交易的入场点尤为重要,而不是止损位在图表形态上的位置。

Getter:是一个类方法,用于访问类或结构体中受保护字段的值。它用于将类的值封装在类开发者实现的逻辑内,而后续使用时无法更改其功能或具有指定访问级别的受保护字段的值。

滑点(Slippage):发生在经纪商以与用户最初请求价格不同的价格开仓时。这种情况在按市价交易时可能发生。例如,在发送订单时,开仓量是基于存款货币中为该交易设定的风险和以点数计算的算数止损来计算的。然后,当经纪商开仓时,可能会出现仓单是在与以点数计算的止损不同的价格开立的。例如,在市场不稳定时,它可能变为150而不是100。应监控此类仓单的开立情况,如果已开立订单的风险(根据风险管理者的参数)远大于预期,则应提前平仓以避免更高损失。

日内交易:是一种交易风格,根据这种风格,交易操作仅在一天内完成。根据这种方法,未平仓头寸不会过夜,即不会转移到下一个交易日。这种交易方式无需考虑与早盘缺口、持仓转移额外费用、次日趋势变化等相关的风险。通常,如果未平仓头寸转移到下一个交易日,这种交易风格可以被认为是中期交易。

持仓交易:是一种交易风格,根据这种风格,交易者在账户上持有一种交易品种的头寸,而不增加或减少其持仓量,也不执行额外的入场交易。在这种方法中,交易者在收到对应品种的开仓信号后,立即根据信号计算全部风险并进行处理。在之前的头寸完全平仓之前,不会考虑其他开仓信号。 

交易品种动量:是指在给定时间框架内,交易品种的单向、无回撤的运动。动量的起点将是同方向运动的起点,且方向不发生变化。如果价格回到动量的起点,这通常被称为重启。交易品种无回撤运动的点数值取决于多种因素,包括当前市场波动性、重要新闻发布或交易品种本身的重要价格水平。


算法交易的子类

我们在之前的文章中编写的 RiskManagerBase 基类包含了所有必要的功能,为日内活跃交易期间提供更安全的风险控制逻辑。为了避免重复所有这些功能,我们将使用面向对象MQL5编程中最重要的原则之一——继承。这种方法将使我们能够使用之前编写的代码,并在我们的类中补充所需的功能嵌入到任何交易算法中。

项目架构将基于以下原则构建:

  • 避免编写重复代码来节省时间
  • 遵守SOLID编程原则
  • 便于多个开发团队在无架构环境下开展工作
  • 能将我们的项目扩展到任何交易策略

第一点,如前所述,旨在显著节省开发时间。我们将使用继承功能来保留之前创建的与限定和事件相关的操作逻辑,从而避免浪费时间复制和测试新代码。

第二点涉及编程中构建类的基本原则。首先,我们将使用“开闭原则”:我们的类将在不丢失基本的风险控制原则和方法的前提下进行扩展。我们添加的每个独立方法都确保遵循单一职责原则,以便轻松开发和逻辑上理解代码。这引出了下一点中描述的原则。

第三点是,我们要确保其他人能够理解代码逻辑。将每个类分别放在不同的文件中,会使得开发团队能够更方便地同时进行工作。

同时,我们不会使用final修饰符来限制从RiskManagerAlgo类进行继承,以便通过进一步的继承来实现其后续改进。这将使我们的子类能够灵活地适应几乎任何交易系统。

基于上述原则,我们的类将如下所示:

//+------------------------------------------------------------------+
//|                       RiskManagerAlgo                            |
//+------------------------------------------------------------------+
class RiskManagerAlgo : public RiskManagerBase
  {
protected:
   CSymbolInfo       r_symbol;                     // instance
   double            slippfits;                    // allowable slippage per trade
   double            spreadfits;                   // allowable spread relative to the opened stop level
   double            riskPerDeal;                  // risk per trade in the deposit currency

public:
                     RiskManagerAlgo(void);        // constructor
                    ~RiskManagerAlgo(void);        // destructor

   //---getters
   bool              GetRiskTradePermission() {return RiskTradePermission;};


   //---interface implementation
   virtual bool      SlippageCheck() override;  // checking the slippage for an open order
   virtual bool      SpreadMonitor(int intSL) override;           // spread control
  };
//+------------------------------------------------------------------+

除了现有的RiskManagerBase基类中的字段和方法外,在我们的RiskManagerAlgo子类中,我们还提供了以下组件,以为EA提供额外的功能。首先,我们需要一个getter方法来从基类RiskManagerBase中获取派生类RiskTradePermission中protected区块字段的数据。这个方法将是从风险管理器的订单条件区块,通过计算获取是否允许开仓的主要方式。工作原理相当简单:如果此变量包含'true',则EA可以继续根据其交易策略的信号下单;如果它是'false',则即使交易策略显示了新的入场点,也无法下单。

我们还将提供一个MetaTrader 5终端中标准的CSymbolInfo类实例,用于处理交易品种字段的相关事务。CSymbolInfo类提供了对交易品种属性的便捷访问,这还将使我们能够简化EA的代码,使其更加直观,从而更方便地理解和维护类的功能。

接下来,我们将在类中为滑点和点差控制条件增加一些额外功能。'slippfits'字段将存储用户定义的滑点条件的控制状态,而点差大小条件将存储在'spreadfits'变量中。第三个必需的变量将包含以存款货币计价的每笔交易的风险大小。值得注意的是,我们专门声明了一个单独的变量来控制订单的滑点。通常,在日内交易中,交易系统会发出许多信号。因此,没有必要将全天的风险大小限制在一笔交易上。这意味着在交易之前,交易者会提前知道他们将处理哪些交易品种的哪些信号,并考虑到重新入场的次数,将每笔交易的风险视为与每日风险相等。

根据这一点,所有仓单的所有风险之和不应超过当天的风险。如果每天只有一笔交易,那么这个交易可能代表了全部的风险。然而,这种情况很少见,因为通常会有更多的交易。让我们在全局范围内声明以下代码。为了方便,我们用group关键字将其预先“包装”在一个命名块中。

input group "RiskManagerAlgoClass"
input double inp_slippfits    = 2.0;  // inp_slippfits - allowable slippage per open deal
input double inp_spreadfits   = 2.0;  // inp_spreadfits - allowable spread relative to the stop level to open
input double inp_risk_per_deal   = 100;  // inp_risk_per_deal - risk per trade in the deposit currency

此将允许您根据指定的条件灵活地配置,来对未平仓头寸进行监控。

在RiskManagerAlgo类的public部分,我们声明了要重写的接口虚拟函数,如下所示:

//--- implementation of the interface
   virtual bool      SlippageCheck() override;  // checking the slippage for an open order
   virtual bool      SpreadMonitor(int intSL) override;           // spread control

在这里,我们使用了virtual关键字,它是一个函数说明符,提供了一种在运行时动态选择RiskManagerBase基类和RiskManagerAlgo派生类函数之间适当成员的机制。它们的共同父类将是我们的纯虚函数接口。

在RiskManagerAlgo子类的构造函数中,我们将用户输入的参数复制到类实例的相应字段中来执行初始化:

//+------------------------------------------------------------------+
//|                        RiskManagerAlgo                           |
//+------------------------------------------------------------------+
RiskManagerAlgo::RiskManagerAlgo(void)
  {
   slippfits   = inp_slippfits;           // copy slippage condition
   spreadfits  = inp_spreadfits;          // copy spread condition
   riskPerDeal  = inp_risk_per_deal;      // copy risk per trade condition
  }

这里需要指出的是,有时直接初始化类字段可能更为实用。然而,在这种情况下,使用复制进行初始化并不会产生太大差异,因此为了方便起见,我们将继续采用复制初始化的方式。相应地,您可以使用以下代码:

//+------------------------------------------------------------------+
//|                        RiskManagerAlgo                           |
//+------------------------------------------------------------------+
RiskManagerAlgo::RiskManagerAlgo(void):slippfits(inp_slippfits),
                                       spreadfits(inp_spreadfits),
                                       rispPerDeal(inp_risk_per_deal)
  {

  }

在类的析构函数中,我们不需要“手动”清理内存,因此我们将函数体留空:

//+------------------------------------------------------------------+
//|                         ~RiskManagerAlgo                         |
//+------------------------------------------------------------------+
RiskManagerAlgo::~RiskManagerAlgo(void)
  {

  }

既然所有必要的函数都已经在RiskManagerAlgo类中声明了,接下来我们就来选择一种方法来实现处理仓单近距离止损的接口。


处理近距离止损的接口

MQL5编程语言使得我们能够灵活开发并使用已有实现中所需功能。其中一些功能是从C++移植过来的,而另一些功能则为了更方便开发而进行了补充和扩展。为了实现与控制仓单近距离止损相关的功能,我们需要一个通用对象,这个对象不仅可以作为我们风险管理类中的父类进行继承,还可以作为其他交易策略架构中的父类进行继承。

为了声明一个用于实现和连接特定功能而创建的泛型数据类型,我们可以使用C++中的抽象类,也可以使用一个独立的数据类型,如接口。 

抽象类接口都是为了创建泛化实体,基于这些实体可以进一步创建更具体的派生类。在我们的案例中,这是一个用于处理带有近距离止损的头寸的类。抽象类是一个只能作为某些子类的基类使用的类,因此无法创建抽象类的具体对象。如果我们需要使用这个泛化实体,我们类的代码将如下所示:

//+------------------------------------------------------------------+
//|                         CShortStopLoss                           |
//+------------------------------------------------------------------+
class CShortStopLoss
  {
public:
                     CShortStopLoss(void) {};         // the class will be abstract event if at least one function in it is virtual
   virtual          ~CShortStopLoss(void) {};         // the same applies to the destructor

   virtual bool      SlippageCheck()         = NULL;  // checking slippage for the open order
   virtual bool      SpreadMonitor(int intSL)= NULL;  // spread control
  };

接口是 MQL5 编程语言中用于泛化实体的一个特殊数据类型。其表示法更为紧凑和简单,功能上没有差异,因此我们将使用这种类型,。实际上,接口 也是一种类,但它不能包含成员/字段,也不能有构造函数和/或析构函数。接口中声明的所有方法都是纯虚方法,即使没有明确的定义,这也使得其使用更加优雅和紧凑。通过接口这样的泛型实体来实现的话,看起来会像这样:

interface IShortStopLoss
  {
   virtual bool   SlippageCheck();           // checking the slippage for an open order
   virtual bool   SpreadMonitor(int intSL);  // spread control
  };

既然我们已经决定了要使用的泛型实体类型,接下来就让我们为子类实现接口中已经声明的所有必要方法的功能。


开仓的滑点控制

首先,为了实现SlippageCheck(void)方法,我们需要更新图表中交易品种的数据。我们将使用类CSymbolInfo实例的Refresh()方法来完成这一操作。该方法将更新描述交易品种的所有字段,以便进行后续操作:

   r_symbol.Refresh();                                                  // update symbol data

请注意,Refresh()方法会更新CSymbolInfo类中的所有字段数据,这与该类中类似的RefreshRates(void)方法不同,后者仅更新指定交易品种的当前价格数据。在此实现中,Refresh()方法将在每个tick被调用,以确保我们的EA在每次迭代时都使用最新的信息。

在Refresh()方法的变量作用域内,当我们遍历所有已开仓位以计算开仓时可能的滑点时,我们需要动态变量来存储已开仓位属性的数据。已开仓位的信息将以以下形式存储:

   double PriceClose = 0,                                               // close price for the order
          PriceStopLoss = 0,                                            // stop loss price for the order
          PriceOpen = 0,                                                // open price for the order
          LotsOrder = 0,                                                // order lot volume
          ProfitCur = 0;                                                // current order profit

   ulong  Ticket = 0;                                                   // order ticket
   string Symbl;                                                        // symbol

为了获取亏损情况下的tick值数据,我们将使用在RiskManagerAlgo类中声明的CSymbolInfo类实例的TickValueLoss()方法。从该方法获得的值表示当价格以一个最小点变化时,对于一个标准手数来说,账户余额将变化多少。稍后我们将使用这个值来计算在实际开仓价格下的潜在亏损。这里我们使用“潜在”这个词,因为这个方法将在每个tick上工作,并且在开仓后立即工作。这意味着在接收到下一个tick时,我们将能够立即检查在一笔交易中我们可能会亏损多少,尽管此时价格仍然更接近开仓价格而不是止损价格。 

   double lot_cost = r_symbol.TickValueLoss();                          // get tick value
   bool ticket_sc = 0;                                                  // variable for successful closing

在这里,我们还将声明一个变量,该变量对于检查是否需有必要执行平仓,如果计算结果显示由于滑点的原因需要平仓的话。这是一个bool类型的变量,名为ticket_sc。

现在我们可以继续在我们的滑点控制方法框架内遍历所有已开仓位。我们将通过组织一个for循环来遍历已开仓位,循环次数由已开位单数量来限制。为了获取已开仓位数量的值,我们将使用终端中预定义的函数PositionsTotal()。我们将使用标准终端类CPositionInfo的SelectByIndex()方法按索引选择仓位。

r_position.SelectByIndex(i)

一旦选定了一个仓位,我们就可以开始使用同样的标准终端类 CPositionInfo 来查询该仓位的属性。但是首先,我们需要检查所选仓位是否对应于当前正在运行此EA的交易品种。这可以通过在循环中使用以下代码来完成:

         Symbl = r_position.Symbol();                                   // get the symbol
         if(Symbl==Symbol())                                            // check if it's the right symbol

只有在确保通过索引选中的仓单属于我们当前图表的品种之后,我们才能继续查询其他属性来检查开仓情况。接下来,对仓位属性的进一步查询也将使用标准终端类 CPositionInfo 的实例,具体如下所示:

            PriceStopLoss = r_position.StopLoss();                      // remember its stop loss
            PriceOpen = r_position.PriceOpen();                         // remember its open price
            ProfitCur = r_position.Profit();                            // remember financial result
            LotsOrder = r_position.Volume();                            // remember order lot volume
            Ticket = r_position.Ticket();

请注意,不仅可以通过检查交易对象(symbol),还可以通过所选仓位MqlTradeRequest结构中的magic编号进行检查。这种方法通常用于在同一个账户内区分由不同策略执行的交易操作。我们不会使用这种方法,因为我认为从便于分析和利用计算资源的角度来看,为不同的策略使用单独的账户会更好。如果您使用其他方法,请在本文的评论中分享。现在,让我们继续实现我们的方法。

我们的方法包括平仓买入仓位是通过以Bid价格卖出而平仓的,卖出仓位则是通过以Ask价格买入而平仓的。因此,我们需要根据所选开仓类型的不同来实现获取平仓价格的逻辑:

            int dir = r_position.Type();                                // define order type

            if(dir == POSITION_TYPE_BUY)                                // if it is Buy
              {
               PriceClose = r_symbol.Bid();                             // close at Bid
              }
            if(dir == POSITION_TYPE_SELL)                               // if it is Sell
              {
               PriceClose = r_symbol.Ask();                             // close at Ask
              }

在这里,我们不会考虑部分平仓的逻辑,尽管交易平台在技术上允许这样做(前提是您的经纪商也允许)。然而,这种类型的平仓并不包含在我们的方法实现逻辑中。

在确认所选仓位符合要求并获取所有必要特征后,我们就可以继续计算其实际风险了。首先,我们需要根据实际的开盘价格来计算出止损值的大小(以最小点数为单位),以便之后将其与最初预期的值进行比较。

我们计算的方法是求出开盘价与止损价之间的绝对差值。为此,我们将使用终端中预定义的函数MathAbs()。为了从分数价格值中获得以点为单位的整数值,我们将MathAbs()函数得到的结果除以分数值中表示一点(一个point)的值。为了找到它,我们使用标准终端类CPositionInfo实例的Point()方法。

int curr_sl_ord = (int) NormalizeDouble(MathAbs(PriceStopLoss-PriceOpen)/r_symbol.Point(),0); // check the resulting stop

现在,为了得到所选仓位的实际潜在损失值,我们只需要将得到的以点数为单位的止损值乘以以手数表示的仓位大小和交易品种上一个tick的价值。具体操作如下:

double potentionLossOnDeal = NormalizeDouble(curr_sl_ord * lot_cost * LotsOrder,2); // calculate risk upon reaching the stop level

现在,让我们来检查得到的值是否在用户为此交易输入的风险偏差范围内。这个值由'slippfits'变量设定。如果得到的值超出了这个范围,那么我们就平掉所选的仓位:

             if(
                  potentionLossOnDeal>NormalizeDouble(riskPerDeal*slippfits,0) &&   // if the resulting stop exceeds risk per trade given the threshold value
                  ProfitCur<0                                                  &&   // and the order is at a loss
                  PriceStopLoss != 0                                                // if stop loss is not set, don't touch
               )

在这一系列条件中,我们又增加了两个检查项,它们包含了处理交易的以下逻辑。

首先,“ProfitCur”条件确保只有在某仓位的亏损区域内才会处理滑点。这是由于交易策略的以下条件所决定的。由于滑点通常发生在市场波动较高的时候,订单会以向盈利方向滑点的形式开仓,从而增加了止损点数并减少了止盈点数。这会降低交易的预期风险/回报,并增加潜在亏损,但同时也会增加止盈的概率,因为导致我们开仓“滑点”的快速波动很可能会在那一刻继续。这个条件意味着,我们只有在快速波动导致的滑点达到止盈之前停止,并当价格回到亏损区域时,才会平掉仓位。

第二个条件“PriceStopLoss != 0”是为了实现以下逻辑:如果交易者没有设置止损,我们则不会平掉这个仓位,因为这个仓位的风险是未限定的。这意味着,在开仓时,你明白如果价格走势与你的预期相反,这个仓位可能会囊括你当天的全部风险。这暗示了非常高的风险,因为对于你计划当天交易的所有品种,可能没有足够的止损额,而这些品种本可能表现良好并带来利润。一个没有设置止损的仓位可能会使这些交易变得不可能。是否要包含这个条件,应该根据你的个人交易策略来决定。在我们的实现中,我们不会同时交易多个品种,所以我们不会删除没有设置止损水平的仓位。

如果识别仓位滑点的所有条件都满足,我们将使用在基类 RiskManagerBase 中声明的标准 CTrade 类的 PositionClose() 方法来平掉该仓位。作为输入参数,我们传递之前保存的待平仓位的订单号。调用平仓函数的结果保存在 ticket_sc 变量中,以便控制订单的执行。

ticket_sc = r_trade.PositionClose(Ticket);                        // close order

整个方法的代码如下:

//+------------------------------------------------------------------+
//|                         SlippageCheck                            |
//+------------------------------------------------------------------+
bool RiskManagerAlgo::SlippageCheck(void) override
  {
   r_symbol.Refresh();                                                  // update symbol data

   double PriceClose = 0,                                               // close price for the order
          PriceStopLoss = 0,                                            // stop loss price for the order
          PriceOpen = 0,                                                // open price for the order
          LotsOrder = 0,                                                // order lot volume
          ProfitCur = 0;                                                // current order profit

   ulong  Ticket = 0;                                                   // order ticket
   string Symbl;                                                        // symbol
   double lot_cost = r_symbol.TickValueLoss();                          // get tick value
   bool ticket_sc = 0;                                                  // variable for successful closing

   for(int i = PositionsTotal(); i>=0; i--)                             // start loop through orders
     {
      if(r_position.SelectByIndex(i))
        {
         Symbl = r_position.Symbol();                                   // get the symbol
         if(Symbl==Symbol())                                            // check if it's the right symbol
           {
            PriceStopLoss = r_position.StopLoss();                      // remember its stop loss
            PriceOpen = r_position.PriceOpen();                         // remember its open price
            ProfitCur = r_position.Profit();                            // remember financial result
            LotsOrder = r_position.Volume();                            // remember order lot volume
            Ticket = r_position.Ticket();

            int dir = r_position.Type();                                // define order type

            if(dir == POSITION_TYPE_BUY)                                // if it is Buy
              {
               PriceClose = r_symbol.Bid();                             // close at Bid
              }
            if(dir == POSITION_TYPE_SELL)                               // if it is Sell
              {
               PriceClose = r_symbol.Ask();                             // close at Ask
              }

            if(dir == POSITION_TYPE_BUY || dir == POSITION_TYPE_SELL)
              {
               int curr_sl_ord = (int) NormalizeDouble(MathAbs(PriceStopLoss-PriceOpen)/r_symbol.Point(),0); // check the resulting stop

               double potentionLossOnDeal = NormalizeDouble(curr_sl_ord * lot_cost * LotsOrder,2); // calculate risk upon reaching the stop level

               if(
                  potentionLossOnDeal>NormalizeDouble(riskPerDeal*slippfits,0) &&   // if the resulting stop exceeds risk per trade given the threshold value
                  ProfitCur<0                                                  &&   // and the order is at a loss
                  PriceStopLoss != 0                                                // if stop loss is not set, don't touch
               )
                 {
                  ticket_sc = r_trade.PositionClose(Ticket);                        // close order

                  Print(__FUNCTION__+", RISKPERDEAL: "+DoubleToString(riskPerDeal));                  //
                  Print(__FUNCTION__+", slippfits: "+DoubleToString(slippfits));                      //
                  Print(__FUNCTION__+", potentionLossOnDeal: "+DoubleToString(potentionLossOnDeal));  //
                  Print(__FUNCTION__+", LotsOrder: "+DoubleToString(LotsOrder));                      //
                  Print(__FUNCTION__+", curr_sl_ord: "+IntegerToString(curr_sl_ord));                 //

                  if(!ticket_sc)
                    {
                     Print(__FUNCTION__+", Error Closing Orders №"+IntegerToString(ticket_sc)+" on slippage. Error №"+IntegerToString(GetLastError())); // output to log
                    }
                  else
                    {
                     Print(__FUNCTION__+", Orders №"+IntegerToString(ticket_sc)+" closed by slippage."); // output to log
                    }
                  continue;
                 }
              }
           }
        }
     }
   return(ticket_sc);
  }
//+------------------------------------------------------------------+

这就完成了滑点控制方法的重写。接下来,让我们继续描述在开新仓之前控制点差大小的方法。


开仓的点差控制

在我们实现的 SpreadMonitor() 方法中,点差控制包括在开仓前立即对当前点差与作为整数参数传递给该方法的算数或技术形态止损进行初步比较。如果当前点差大小在用户可接受的范围内,该函数将返回 true。否则,如果点差大小超出此范围,该方法将返回 false。

函数的结果将被存储在一个布尔型(bool)变量中,该变量默认初始化为 true:

   bool SpreadAllowed = true;

我们将使用CSymbolInfo类的Spread()方法来获取指定交易品种当前的点差值:

   int SpreadCurrent = r_symbol.Spread();

下面是用于检查的逻辑判断语句

if(SpreadCurrent>intSL*spreadfits)

这意味着,如果当前交易品种的点差大于所需止损金额与用户指定系数的乘积,该方法应返回false,并且这将阻止以当前点差大小开设仓位,直到下一个价格变动(tick)为止。以下是该方法的描述:

//+------------------------------------------------------------------+
//|                          SpreadMonitor                           |
//+------------------------------------------------------------------+
bool RiskManagerAlgo::SpreadMonitor(int intSL)
  {
//--- spread control
   bool SpreadAllowed = true;                                           // allow spread trading and check ratio further
   int SpreadCurrent = r_symbol.Spread();                               // current spread values

   if(SpreadCurrent>intSL*spreadfits)                                   // if the current spread is greater than the stop and the coefficient
     {
      SpreadAllowed = false;                                            // prohibit trading
      Print(__FUNCTION__+IntegerToString(__LINE__)+
            ". 点差太大了!Spread:"+
            IntegerToString(SpreadCurrent)+", SL:"+IntegerToString(intSL));// notify
     }
   return SpreadAllowed;                                                // return result
  }
//+------------------------------------------------------------------+

在使用这种方法时,你需要考虑到,如果点差条件非常严格,EA将不会开立仓位,并且相关信息会不断记录在EA的日志中。通常,会使用至少为2的系数值,这意味着如果点差是止损距离的一半,那么你要么需要等待点差变小,要么要拒绝以如此短的止损距离入场,因为止损水平越接近点差大小,从这个仓位中遭受损失的可能性就越大。


接口实现

接口将成为我们基类的首个父类,因为MQL5不支持多重继承。但在此情况下,这对我们来说并无限制,因为我们能够为我们的项目实现一个连贯的继承体系。 

为此,我们需要让我们的基类RiskManagerBase继承之前描述的IShortStopLoss接口:

//+------------------------------------------------------------------+
//|                        RiskManagerBase                           |
//+------------------------------------------------------------------+
class RiskManagerBase:IShortStopLoss                        // the purpose of the class is to control risk in terminal

这将使我们能够将所需的功能转移到子类RiskManagerAlgo中。在这种情况下,继承的访问级别并不重要,因为我们的接口只包含纯虚函数,不包含字段、构造函数或析构函数。

我们自定义的RiskManagerAlgo类的最终继承结构如图1所示,该结构通过封装公共方法来提供完整的功能。

图例 1. RiskManagerAlgo类的继承结构

图例 1. RiskManagerAlgo类的继承结构

现在,在组装我们的算法之前,我们只需要实现决策工具,来测试所述的风险控制计算功能。


交易模块的实现

在之前的文章《手动交易的风险管理器》中,交易模块包含一个相当简单的TradeModel实体,用于处理从分形接收到的输入数据。由于本文与上一篇不同,是关于算法交易的,因此让我们也基于分形图来构建一个算法决策工具。它的逻辑将保持不变,但现在我们将所有内容都通过代码实现,而不是手动生成信号。这样做的额外好处是,我们可以测试在更长历史数据的时间段内会发生什么,因为我们不需要手动生成必要的输入数据。

让我们声明CFractalsSignal类,该类将负责从分形图上接收信号。逻辑仍然相同:如果价格突破日线图上方部分的分形,则EA买入;如果当前价格突破日线图下方部分的分形,则出现卖出信号。交易将在日内进行,即它们当天开仓,于交易日结束时关闭。

我们的CFractalsSignal类将包含一个字段,用于存储有关我们分析分形突破所使用的时间框架的信息。因此,为了使用的方便,我们可以区分分析分形所使用的时间框架和EA运行的时间框架。让我们声明一个枚举变量ENUM_TIMEFRAMES

ENUM_TIMEFRAMES   TF;                     // timeframe used

接下来,我们声明一个指向标准终端类的变量指针,用于与技术指标CiFractals进行交互,该类已经方便地实现了我们所需的所有功能,因此我们无需重复编写这些功能:

   CiFractals        *cFractals;             // fractals

我们还需要存储有关信号以及EA如何处理这些信号的数据。我们将使用在上一篇文章中使用过的TradeInputs自定义结构。然而,上次我们是手动生成的,而现在CFractalsSignal类将为我们生成它:

//+------------------------------------------------------------------+
//|                         TradeInputs                              |
//+------------------------------------------------------------------+

struct TradeInputs
  {
   string             symbol;                                           // symbol
   ENUM_POSITION_TYPE direction;                                        // direction
   double             price;                                            // price
   datetime           tradedate;                                        // date
   bool               done;                                             // trigger flag
  };

我们分别为买入和卖出信号声明了我们结构的内部变量,以便能够同时考虑它们,因为我们无法提前知道哪个价格会首先达到。

   TradeInputs       fract_Up, fract_Dn;     // current signal

我们只需要声明一些变量,用于存储从CiFractals类接收到的当前值,以便获取日线图上新形成的分形的数据。

为了提供必要的功能,我们需要在CFractalsSignal类的公共域中定义几个方法,这些方法将负责监控最新的当前分形价格突破情况,给出开仓信号,并监控这些信号处理成功与否。

用于控制类数据状态更新的方法是Process()。它不返回任何值,也不接受任何参数,而只是在每个到来的价格变动(tick)时执行数据状态更新。接收买入和卖出信号的方法将分别命名为BuySignal()和SellSignal()。它们不接受任何参数,但如果需要在相应方向上开仓,将返回一个bool值(布尔值)。在检查经纪商服务器关于成功开仓的响应后,需调用BuyDone()和SellDone()方法。我们的类描述如下:

//+------------------------------------------------------------------+
//|                       CFractalsSignal                            |
//+------------------------------------------------------------------+
class CFractalsSignal
  {
protected:
   ENUM_TIMEFRAMES   TF;                     // timeframe used
   CiFractals        *cFractals;             // fractals

   TradeInputs       fract_Up, fract_Dn;     // current signal

   double            FrUp;                   // upper fractals
   double            FrDn;                   // lower fractals

public:
                     CFractalsSignal(void);  // constructor
                    ~CFractalsSignal(void);  // destructor

   void              Process();              // method to start updates

   bool              BuySignal();            // buy signal
   bool              SellSignal();           // sell signal

   void              BuyDone();              // buy done
   void              SellDone();             // sell done
  };

在类的构造函数中,我们需要将TF时间框架字段初始化为日线图间隔PERIOD_D1,因为日线图波动足够大,可以为我们提供达到盈利目标所需的波动,同时它们比在周线图和月线图上出现得更频繁。在这里,测试更段时间框架的机会留给每个读者,我们将重点关注日线图。我们还将创建类的实例对象来处理类中的分形指标,并按照以下顺序以默认值初始化所有必要的字段:

//+------------------------------------------------------------------+
//|                        CFractalsSignal                           |
//+------------------------------------------------------------------+
CFractalsSignal::CFractalsSignal(void)
  {
   TF  =  PERIOD_D1;                                                    // timeframe used

//--- fractal class
   cFractals=new CiFractals();                                          // created fractal instance
   if(CheckPointer(cFractals)==POINTER_INVALID ||                       // if instance not created OR
      !cFractals.Create(Symbol(),TF))                                   // variant not created
      Print("INIT_FAILED");                                             // don't proceed
   cFractals.BufferResize(4);                                           // resize fractal buffer
   cFractals.Refresh();                                                 // update

//---
   FrUp = EMPTY_VALUE;                                                  // leveled upper at start
   FrDn = EMPTY_VALUE;                                                  // leveled lower at start

   fract_Up.done  = true;                                               //
   fract_Up.price = EMPTY_VALUE;                                        //

   fract_Dn.done  = true;                                               //
   fract_Dn.price = EMPTY_VALUE;                                        //
  }

在析构函数中,清除构造函数中创建的分形指标对象的内存:

//+------------------------------------------------------------------+
//|                        ~CFractalsSignal                          |
//+------------------------------------------------------------------+
CFractalsSignal::~CFractalsSignal(void)
  {
//---
   if(CheckPointer(cFractals)!=POINTER_INVALID)                         // if instance was created,
      delete cFractals;                                                 // delete
  }

在数据更新方法中,我们只需要调用CiFractals类实例的Refresh()方法,以刷新从父类CIndicator继承的分形价格数据。

//+------------------------------------------------------------------+
//|                         Process                                  |
//+------------------------------------------------------------------+
void CFractalsSignal::Process(void)
  {
//---
   cFractals.Refresh();                                                 // update fractals
  }

在这里,我想指出,这种方法有可能进一步的优化,因为我们并不需要在每个tick都更新这些数据,因为分形突破的点位来自于日线图。我们可以额外实现一个事件方法,用于在日线图上出现新柱线时触发,并仅在该事件被触发时更新这些数据。但我们将保持当前的实现方式,因为它不会对系统造成很大的额外负担,而且实现额外功能将需要额外的成本,而性能的提升却相当有限。

在根据买入信号开仓的BuySignal(void)方法中,我们首先请求当前Ask价格:

   double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);                  // Buy price

接下来,我们通过CiFractals类实例的Upper()方法请求上分形的当前值,并将所需柱状的索引作为参数传递:

   FrUp=cFractals.Upper(3);                                             // request the current value

我们将值‘3’传递给这个方法,因为我们只使用完全形成的分形突破。由于在时间序列中,缓冲区计数是从当前(0)到较早的向上方向进行,因此在日线图上,值“3”意味着前天的前一天。这将排除在日线图上某一时刻形成的分形突破,然后在同一天价格达到新的高点/低点,分形突破消失的情况。

现在,如果日线图上当前突破的价格发生变化,我们对更新当前分形突破进行逻辑检查。我们将上面在FrUp变量中更新的分形指标的当前值,与存储在自定义结构TradeInputs的price字段中的上一个上分形的最新当前值进行比较。为了使'price'字段始终存储当前价格的值,并且在指标没有返回数据(如果没有检测到突破)时不会重置,我们将添加另一个检查,以检查指标值FrUp是否不等于EMPTY_VALUE(空值)。这两个条件的组合将使我们只能更新最后一个分形的非空价格值(排除对应于指标中EMPTY_VALUE的零值),并且不会用这个空值重写这个变量。该效验代码如下:

   if(FrUp != fract_Up.price           &&                               // if the data has been updated
      FrUp != EMPTY_VALUE)                                              // skip empty value

在该方法的末尾,我们用以下逻辑来检查是否收到买入信号:

   if(fract_Up.price != EMPTY_VALUE    &&                               // skip zero values
      ask            >= fract_Up.price &&                               // if the buy price is greater than or equal to the fractal
      fract_Up.done  == false)                                          // the signal has not been processed yet
     {
      return true;                                                      // generate a signal to process
     }

在这个代码块中,我们首先检查最后一个当前分形fract_Up的变量是否为零——这是在类的构造函数中首次初始化此变量后,EA初次启动时进行的检查。下一个条件检查当前的市场买入价格是否突破了分形的当前值:ask >= fract_Up.price。 这可以视为该方法的主要逻辑条件。最后,我们需要检查这个分形值是否已经被用以生成了这个信号。这里的重点是,分形突破的信号来自日线图,如果当前的市场买入价格已经达到了所需的值,我们必须每天处理一次这个信号,因为我们的交易虽然是日内交易,但属于持仓交易,不增加仓位或同时开立额外的仓位。如果这三个条件都满足,我们的方法将返回true,以便我们的EA处理这个信号。

完整实现上述逻辑的方法如下所示:

//+------------------------------------------------------------------+
//|                         BuySignal                                |
//+------------------------------------------------------------------+
bool CFractalsSignal::BuySignal(void)
  {
   double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);                  // Buy price

//--- check fractals update
   FrUp=cFractals.Upper(3);                                             // request the current value

   if(FrUp != fract_Up.price           &&                               // if the data has been updated
      FrUp != EMPTY_VALUE)                                              // skip empty value
     {
      fract_Up.price = FrUp;                                            // process the new fractal
      fract_Up.done = false;                                            // not processed
     }

//--- check the signal
   if(fract_Up.price != EMPTY_VALUE    &&                               // skip zero values
      ask            >= fract_Up.price &&                               // if the buy price is greater than or equal to the fractal
      fract_Up.done  == false)                                          // the signal has not been processed yet
     {
      return true;                                                      // generate a signal to process
     }

   return false;                                                        // otherwise false
  }

如上所述,获取买入信号的方法应与监控经纪商服务器处理该信号的方法协同工作。当买入信号被处理时调用的方法非常简洁:

//+------------------------------------------------------------------+
//|                         BuyDone                                  |
//+------------------------------------------------------------------+
void CFractalsSignal::BuyDone(void)
  {
   fract_Up.done = true;                                                // processed
  }

逻辑非常简单:在调用这个公共方法时,我们将在fract_Up结构体实例的对应最后一个信号的'done'字段中设置一个表示信号成功处理的标志。因此,这个方法只有在经纪商服务器成功开立订单的检查通过后,才会在EA代码中调用。

卖出方法的逻辑是类似的。唯一的区别是,我们将请求Bid价格,而不是Ask价格。相应地,对于卖出,当前价格的条件将是“小于分形突破”。

下面是卖出信号的相应方法:

//+------------------------------------------------------------------+
//|                         SellSignal                               |
//+------------------------------------------------------------------+
bool CFractalsSignal::SellSignal(void)
  {
   double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);                  // bid price

//--- check fractals update
   FrDn=cFractals.Lower(3);                                             // request the current value

   if(FrDn != EMPTY_VALUE        &&                                     // skip empty value
      FrDn != fract_Dn.price)                                           // if the data has been updated
     {
      fract_Dn.price = FrDn;                                            // process the new fractal
      fract_Dn.done = false;                                            // not processed
     }

//--- check the signal
   if(fract_Dn.price != EMPTY_VALUE    &&                               // skip empty value
      bid            <= fract_Dn.price &&                               // if the ask price is less than or equal to the fractal AND
      fract_Dn.done  == false)                                          // signal has not been processed
     {
      return true;                                                      // generate a signal to process
     }

   return false;                                                        // otherwise false
  }

处理卖出信号的逻辑类似。在这种情况下,“done”字段将根据fract_Dn结构体实例来填写,该实例负责记录卖出的最后一个当前分形:

//+------------------------------------------------------------------+
//|                        SellDone                                  |
//+------------------------------------------------------------------+
void CFractalsSignal::SellDone(void)
  {
   fract_Dn.done = true;                                                // processed
  }
//+------------------------------------------------------------------+

至此我们完成了根据每日分形突破生成交易信号的方法实现,接下来我们可以进行项目的总体集成了。


集成并测试项目

我们将开始集成项目,通过结合上面描述的所有文件,并在项目主文件的开头添加必要的代码。为此,我们使用#include预处理命令。<RiskManagerAlgo.mqh>、<TradeModel.mqh>和<CFractalsSignal.mqh>是我们之前章节中提到的自定义类文件。<Indicators\BillWilliams.mqh>和<Trade\Trade.mqh>分别是用于处理分形和交易操作的标准终端类文件。

#include <RiskManagerAlgo.mqh>
#include <Indicators\BillWilliams.mqh>
#include <Trade\Trade.mqh>
#include <TradeModel.mqh>
#include <CFractalsSignal.mqh>

为了设置滑点控制方法,我们引入了另一个类型为int输入参数变量。用户将指定交易品种可接受的最小点数停止值:

input group "RiskManagerAlgoExpert"
input int inp_sl_in_int       = 2000;  // inp_sl_in_int - a stop loss level for a separate trade

在更详细的全自动集成或实现中,当使用这种滑点控制方法时,最好不要通过用户输入参数来传递这个参数,而是从负责设置技术止损或算数止损的类返回止损值,或者从处理波动率的类返回经计算的止损值。在这个实现中,我们将为用户留下一个可能性,让他们根据自己的具体策略更改此设置。

现在我们声明指向风险管理器、仓位和分形类的必要指针:

RiskManagerAlgo *RMA;                                                   // risk manager
CTrade          *cTrade;                                                // trade
CFractalsSignal *cFract;                                                // fractal

我们将在EA的初始化事件处理函数OnInit()中初始化这些指针:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   RMA = new RiskManagerAlgo();                                         // algorithmic risk manager

//---
   cFract =new CFractalsSignal();                                       // fractal signal

//--- trade class
   cTrade=new CTrade();                                                 // create trade instance
   if(CheckPointer(cTrade)==POINTER_INVALID)                            // if instance not created,
     {
      Print(__FUNCTION__+IntegerToString(__LINE__)+" Error creating object!");   // notify
     }
   cTrade.SetTypeFillingBySymbol(Symbol());                             // fill type for the symbol
   cTrade.SetDeviationInPoints(1000);                                   // deviation
   cTrade.SetExpertMagicNumber(123);                                    // magic number
   cTrade.SetAsyncMode(false);                                          // asynchronous method

//---
   return(INIT_SUCCEEDED);
  }

在设置CTrade对象时,我们在SetTypeFillingBySymbol()方法的参数中指定了EA当前正在运行的交易品种。EA当前正在运行的交易品种是通过预定义的Symbol()方法返回的。在SetDeviationInPoints()方法中,对于请求价格的最大可接受偏差,我们指定了一个稍大的值。由于这个参数对我们的研究来说不是那么重要,因此我们不会将其实现为输入参数,而是将其为作为硬编码(写死在程序中)。我们还为由此EA开仓的所有的头寸设置一个magic编码。

在析构函数中,早指针有效的情况下,我们通过指针实现对象的删除和清除内存:

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   if(CheckPointer(cTrade)!=POINTER_INVALID)                            // if there is an instance,
     {
      delete cTrade;                                                    // delete
     }

//---
   if(CheckPointer(cFract)!=POINTER_INVALID)                            // if an instance is found,
     {
      delete cFract;                                                    // delete
     }

//---
   if(CheckPointer(RMA)!=POINTER_INVALID)                               // if an instance is found,
     {
      delete RMA;                                                       // delete
     }
  }

现在让我们来描述EA的主体部分,其位于OnTick()事件函数中。首先,我们需要从子类实例中运行基类RiskManagerBase的主事件监控方法ContoMonitor(),而我们在子类中并没有重写这个方法,具体做法如下:

   RMA.ContoMonitor();                                                  // run the risk manager

接下来,我们调用滑点控制方法SlippageCheck()。如前所述,该方法在每个新的报价(tick)到来时被调用,以检查实际持仓风险是否与计划风险(相对于设定的止损)相符:

   RMA.SlippageCheck();                                                 // check slippage

应该注意的是,由于我们的分形决策工具不需要深入实现,而更多地是为了演示风险管理器的功能,因此它不会设置止损,而只是会在交易日结束时平仓。因此,这个方法将允许所有传递给它的交易通过。为了使这个方法在您的实现中能够完全工作,您只有在设置了非零的止损值时,才能向经纪商的服务器发送订单。

接下来,我们需要通过自定义的CFractalsSignal类使用公共的Process()方法来更新分形突破指标数据:

   cFract.Process();                                                    // start the fractal process

现在,所有类的全部事件处理方法都已经被包含在代码中了,我们来看监控下单信号出现的代码块。同我们的CFractalsSignal交易决策工具类的相应方法一样,买入和卖出信号的检查将被分开进行。首先,让我们使用以下两个条件来描述买入信号的检查:

   if(cFract.BuySignal() &&
      RMA.SpreadMonitor(inp_sl_in_int))                                 // if there is a buy signal

首先,我们通过CFractalsSignal类实例的BuySignal()方法来检查是否存在买入信号。如果它给出了这个信号,那么我们会检查风险管理器是否通过SpreadMonitor()方法确认点差符合用户允许的值。作为SpreadMonitor()方法的唯一参数,我们传入用户输入参数inp_sl_in_int。

如果上述两个条件都满足,我们将按照以下简化的逻辑结构进行下单:

      if(cTrade.Buy(0.1))                                               // if Buy executed,
        {
         cFract.BuyDone();                                              // the signal has been processed
         Print("Buy has been done");                                    // notify
        }
      else                                                              // if Buy not executed,
        {
         Print("Error: buy");                                           // notify
        }

我们使用CTrade类实例的Buy()方法来下单,并在参数中传递一个等于0.1的标准手数值。为了更客观地评估风险管理器的运行情况,我们不会更改这个数值,以便“平滑”掉交易量参数的统计数据。这意味着在我们EA的运行统计中,所有输入都将具有相同的权重。

如果Buy()方法执行正确,即收到了经纪商的成功响应并且头寸已经开立,我们会立即调用BuyDone()方法来通知我们的CFractalsSignal类,信号已成功处理,并且在此价格下不再需要其他信号。如果无法买入,我们会在日志中通知EA,并且不调用成功信号处理的方法,以便允许重新尝试开立交易。

对于卖出订单,我们将实现类似的逻辑,按照代码顺序调用与卖出对应的方法。

我们将直接使用上一篇文章中提供的在交易日结束时平仓的代码块,不做任何更改:

   MqlDateTime time_curr;                                               // current time structure
   TimeCurrent(time_curr);                                              // request current time

   if(time_curr.hour >= 23)                                             // if end of day
     {
      RMA.AllOrdersClose();                                             // close all positions
     }

这段代码的主要任务是在根据服务器最后已知时间确定的23:00关闭所有持仓,因为我们在这里实现的是日内交易逻辑,不包含隔夜持仓。该代码的逻辑在之前的文章中已有更详细的描述。如果您希望持仓过夜,可以简单地在EA代码中注释掉这个代码块,甚至将其删除。

我们还需要通过预定义的终端函数Comment()在屏幕上显示风险管理器数据的当前状态,将风险管理类的Message()方法传递给它:

   Comment(RMA.Message());                                              // display the data state in a comment
下面是处理新报价(tick)的代码:
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   RMA.ContoMonitor();                                                  // run the risk manager

   RMA.SlippageCheck();                                                 // check slippage

   cFract.Process();                                                    // start the fractal process

   if(cFract.BuySignal() &&
      RMA.SpreadMonitor(inp_sl_in_int))                                 // if there is a buy signal
     {
      if(cTrade.Buy(0.1))                                               // if Buy executed,
        {
         cFract.BuyDone();                                              // the signal has been processed
         Print("Buy has been done");                                    // notify
        }
      else                                                              // if Buy not executed,
        {
         Print("Error: buy");                                           // notify
        }
     }

   if(cFract.SellSignal())                                              // if there is a sell signal
     {
      if(cTrade.Sell(0.1))                                              // if sell executed,
        {
         cFract.SellDone();                                             // the signal has been processed
         Print("Sell has been done");                                   // notify
        }
      else                                                              // if sell failed,
        {
         Print("Error: sell");                                          // notify
        }
     }

   MqlDateTime time_curr;                                               // current time structure
   TimeCurrent(time_curr);                                              // request current time

   if(time_curr.hour >= 23)                                             // if end of day
     {
      RMA.AllOrdersClose();                                             // close all positions
     }

   Comment(RMA.Message());                                              // display the data state in a comment
  }
//+------------------------------------------------------------------+

现在我们可以编译整个项目并在历史数据上测试它。为了测试示例,让我们选择USDJPY货币对,并在2023年期间使用以下输入参数进行测试(参见表格 2):

# 设置
 1  EA  RiskManagerAlgo.ex5
 2  品种  USDJPY
 3  图表时间周期  M15
 4  时间范围  2023.01.01 - 2023.12.31
 5  前向测试  NO
 6  延迟  没有延迟,完美执行
 7  模拟方式  每一个Tick
 8  初始存款  USD 10,000
 9  杠杆比率  1:100
 10  优化  慢速完成算法 

表格 1. RiskManagerAlgo EA的策略测试器设置

在策略测试器的优化设置中,我们基于最小步长的原则来设置参数,以减少训练时间,同时能够追踪之前文章中讨论的依赖关系:即风险管理器是否能够改善甚至是盈利策略的交易结果。优化所需的输入参数如表2所示:

#
参数名称 开始 步长 停止标志
 1  inp_riskperday  0.1  0.5  1
 2  inp_riskperweek  0.5  0.5  3
 3  inp_riskpermonth  2  1  8
 4  inp_plandayprofit  0.1  0.5  3
 5  dayProfitControl  flase  -  true

表格 2. RiskManagerAlgo EA策略优化器参数

优化参数不包括那些不直接依赖于策略有效性且不影响测试器中建模的参数。例如,inp_slippfits将主要取决于经纪商订单执行的质量,而不是我们的入场点。inp_spreadfits则直接取决于点差大小,点差受多种因素影响,包括但不限于经纪商账户类型、重要新闻发布的时间等。每个人都可以根据自己交易的经纪公司独立优化这些参数。

优化结果如图2和图3所示。

图例 2. RiskManagerAlgo EA优化结果

图例 2. RiskManagerAlgo EA优化结果

优化结果的图表显示,大部分结果都处于正数学期望的区域内。这与策略的逻辑有关,当大量市场参与者的头寸集中在强烈的分形突破时,相应地,价格对这些突破的测试会引发市场活动的增加,从而为交易品种提供动力。

为了验证在交易分形水平时存在动量的论点,你可以将给定集合中的最佳和最差迭代与上述指定的参数进行比较。我们的风险管理器允许我们将这种动量与进入头寸的风险标准化。为了更好地理解风险管理器在将风险相对于市场动量标准化方面的作用,让我们来看看图3中每日风险和计划每日利润参数之间的关系。

图例 3. 每日风险vs每日计划利润图表

图例 3. 每日风险vs每日计划利润图表

图3显示了每日风险参数值的一个断点,在这个点上,分形突破策略的有效性首先随着该值的增加而增加,然后开始减少。这将是我们的模型针对这两个参数的极值点(函数断点)。当每日风险参数值的增加开始减少利润而不是增加利润时,模型中这一时刻的存在本身就证明,相对于我们纳入模型的风险而言,市场动能变得较小。风险成本相对于预期利润明显过高。这一断点在图上清晰可见,无需进行额外的数学计算,如函数导数。

现在,为了真正确定是否存在市场动量,让我们分别查看EA优化结果中最佳和最差迭代的参数,以评估风险与回报比率。显然,如果入场时的预期利润是计划风险的几倍,那么动量就发生了:即交易对象在一个方向上单向、无回撤地移动。如果我们的风险值与回报相等或更大,那么就不存在动量。

优化器向我们展示每日风险值的断点很明显是最优点,其参数是如表3中所示:

#
参数名称 参数值
 1  inp_riskperday  0.6
 2  inp_riskperweek  3
 3  inp_riskpermonth  8
 4  inp_plandayprofit  3.1
 5  dayProfitControl  true
 6  inp_slippfits  2
 7  inp_spreadfits  2
 8  inp_risk_per_deal  100
 9  inp_sl_in_int  2000

表格 3. RiskManagerAlgo EA策略优化器的最佳参数

我们看到,计划盈利为3.1,是达到该盈利所需风险成本0.6的五倍。换句话说,我们冒着0.6%存款损失的风险,赚取3.1%的收益。这明确表明,在分形突破的日线形态上存在着价格动量,给出了一个正的数学期望。

最佳迭代的图表如图4所示。

图例 4. 策略优化器最佳迭代实例的图表

图例 4. 策略优化器最佳迭代实例的图表

存款增长图表显示,使用风险控制的策略形成了一个相当平滑的图表,其中每一次新的回撤都不会改写之前回撤的最小值。这表明了使用风险标准化和我们的风险管理器对投资安全性的效果。现在,为了最终确认动量存在以及相对于动量需要标准化风险的论点,让我们来看看优化器运行最糟糕的结果。

可以使用表4中的数据来估算具有以下参数的最糟糕的风险回报。

#
参数名称
参数值
 1  inp_riskperday  1.1
 2  inp_riskperweek  0.5
 3  inp_riskpermonth  2
 4  inp_plandayprofit  0.1
 5  dayProfitControl  true
 6  inp_slippfits  2
 7  inp_spreadfits  2
 8  inp_risk_per_deal  100
 9  inp_sl_in_int  2000

表 4. RiskManagerAlgo EA策略优化器中最糟糕实例的参数

表4中的数据显示,最差的迭代正好位于图3所在的平面,即我们没有针对动量对风险进行标准化的平面。也就是说,我们处于一个区域,在这个区域中,最大风险并不能带来必要的回报,我们没有将潜在的风险最大化利用,同时却在这些交易上损失了大量的本金。

最糟迭代实例的图表如图5所示:

图 5. 策略优化器最糟迭代实例图

图 5. 策略优化器最糟迭代实例图

根据图5,可以清楚地看出,风险与潜在收益之间的不平衡可能导致账户在余额和资金方面出现大幅回撤。基于本文本章所呈现的优化结果,我们可以得出结论,使用风险管理器来控制风险是必需的。相对于交易策略的能力,选择逻辑上合理的风险是至关重要的。现在让我们对全文做一下总结。


结论

基于文章中呈现的资料、模型、论据和算法,可以得出以下结论。仅仅找到一个盈利的投资策略或算法是不够的。即使在这种情况下,由于不合理的成本风险控制,你也可能会亏钱。即使有了盈利的策略,在金融市场中高效且安全运营的关键在于遵守风险管理。高效且安全地长期稳定工作的先决条件是根据所用策略的能力对风险进行标准化。我还强烈建议不要在没有启用风险管理器和没有在每个开仓头寸上设置止损的情况下进行真实账户交易。 

显然,金融市场上的所有风险并非都能被控制和最小化,但它们应该始终与预期收益进行权衡。你始终可以借助风险管理器来控制标准化风险。在这篇文章中,就像之前的文章一样,我强烈建议应用资金和风险管理原则。

如果你在不进行风险控制的情况下进行非系统性交易,你可能会把任何盈利的策略变成亏损的策略。另一方面,如果应用了适当的风险管理,有时也可以将亏损的策略转变为盈利的策略。如果本文中的材料能帮助至少一个人保住存款,我就认为这项工作没有白费。

我期待您的阅读反馈,请在本文下方留言。祝您交易盈利!


本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/14634

附加的文件 |
IShortStopLoss.mqh (1.21 KB)
RiskManagerAlgo.mq5 (10.53 KB)
RiskManagerAlgo.mqh (16.59 KB)
RiskManagerBase.mqh (61.49 KB)
TradeModel.mqh (12.99 KB)
CFractalsSignal.mqh (13.93 KB)
群体优化算法:抵抗陷入局部极值(第二部分) 群体优化算法:抵抗陷入局部极值(第二部分)
我们将继续我们的实验,它的目标是研究群体优化算法在群体多样性较低时有效摆脱局部最小值并达到全局最大值的能力。提供了研究的结果。
群体优化算法:抵抗陷入局部极值(第一部分) 群体优化算法:抵抗陷入局部极值(第一部分)
本文介绍了一个独特的实验,旨在研究群体优化算法在群体多样性较低时有效逃脱局部最小值并达到全局最大值的能力。朝着这个方向努力将进一步了解哪些特定算法可以使用用户设置的坐标作为起点成功地继续搜索,以及哪些因素会影响它们的成功。
您应当知道的 MQL5 向导技术(第 12 部分):牛顿多项式 您应当知道的 MQL5 向导技术(第 12 部分):牛顿多项式
牛顿多项式,其依据一组少量点创建二次方程,是一种古老但有趣的时间序列观察方式。在本文中,我们尝试探讨这种方式在哪些方面对交易者有用,并解决其局限性。
神经网络变得简单(第 76 部分):配合多未来变换器探索不同的交互形态 神经网络变得简单(第 76 部分):配合多未来变换器探索不同的交互形态
本文继续探讨预测即将到来的价格走势的主题。我邀请您领略多未来变换器架构。其主要思路是把未来的多模态分布分解为若干个单模态分布,这样就可以有效地模拟场景中个体之间互动的各种模态。