English Русский Español Deutsch 日本語 Português
preview
开发多币种 EA 交易(第 12 部分):开发自营交易级别风险管理器

开发多币种 EA 交易(第 12 部分):开发自营交易级别风险管理器

MetaTrader 5交易 | 13 一月 2025, 10:03
289 0
Yuriy Bykov
Yuriy Bykov

概述

在整个系列中,我们多次谈到了风险控制这一主题。引入了规范化交易策略的概念,其参数可确保在测试期间达到 10%的回撤水平。然而,以这种方式对交易策略实例以及交易策略组进行归一化处理,只能提供一个历史时期内的给定回撤率。我们无法确定,在前向时期开始测试规范化策略组或在交易账户上启动测试时,是否能观察到指定的回撤水平。

最近,人工交易的风险管理算法交易的风险管理两篇文章对风险管理这一主题进行了探讨。在这些文章中,作者提出了一种编程式实现方法,可控制各种交易参数是否符合预设指标。例如,如果超过了一天、一周或一个月的设定损失水平,交易就会暂停。

从自营公司吸取一些教训一文也很有意思。作者研究了自营交易公司对希望获得管理资本的交易者提出的交易要求。尽管在各种专门用于交易的资源上可以找到对此类公司活动的模糊态度,但使用明确的风险管理规则是成功交易的最重要组成部分之一。因此,以自营交易公司使用的风险控制模型为基础,利用已经积累的经验,实施我们自己的风险管理器是合理的。


模式和概念

对于风险管理器来说,以下概念会对我们有所帮助:

  • 基础余额 - 可用来计算其余参数值的初始账户余额(或部分账户余额)。在这里,我们将使用 10,000 这个值。
  • 每日基础余额 - 当前每日时段开始时的交易账户余额。为简单起见,我们假设日线周期的开始时间与终端中 D1 时间框架出现新柱形的时间相吻合。
  • 每日基础资金是指交易账户在当前每日时段开始时的资金量。
  • 每日水平是每日基础余额和资金的最大值。该值在每日时段开始时确定,并在下一个每日时段开始时保持不变。
  • 最大每日亏损 - 账户资金每日水平的向下偏差量,在该水平下,应在当前每日时段停止交易。 交易将在下一个每日时段恢复。止损可以理解为旨在缩小未平仓头寸规模直至完全平仓的各种行动。首先,我们将使用这个简单的模型:当达到每日最大损失时,所有未平仓的市场仓位都将被关闭。 
  • 最大总损失 - 账户资金与基础余额值的向下偏差,在此情况下交易完全停止(在接下来的时间内不会恢复)。当达到这一水平时,所有未关闭的仓位都会关闭。

我们将限制自己只在两个级别停止交易:每日和总计。也可以用类似的方法添加每周或每月级别。但由于自营交易公司没有这些功能,因此我们不会将风险管理器的首次实现复杂化。如有必要,可在以后添加。

不同的自营交易公司计算每日最大损失和总损失的方法可能略有不同。因此,我们将在风险管理器中提供三种可能的方法来设定计算最大损失的数值:

  • 固定存款货币。在这里,我们直接在参数中传递以交易账户货币单位表示的损失值。我们将其设置为正数。
  • 占基础余额的百分比。在这种情况下,该值被视为既定基础余额的一个百分比。由于我们模型中的基础余额是一个常数值(直到账户和 EA 以手动设置的不同基础余额值重新启动),因此用这种方法计算出的最大损失也将是一个常数值。我们可以将这种情况简化为第一种情况,但由于通常情况下是以最大损失的百分比来表示的,所以我们将其作为一个单独的情况。
  • 占每日水平的百分比。在此选项中,在每天开始时,我们会重新计算最大损失额,它等于刚刚计算出的每日水平的一个指定百分比。随着余额或资金的增加,最大损失额也会增加。这种方法主要用于计算每天的最大损失。相对于基础余额而言,最大总损失通常是固定的。

让我们一如既往地遵循 "最少行动" 原则,开始实现我们的风险管理类。让我们首先进行最起码的必要实现,并为必要时的进一步复杂化做好准备。


CVirtualRiskManager

该类的发展经历了几个阶段。起初,它是一个完全静态的对象,因此可以在所有对象中自由使用。然后我们想到,我们还可以优化风险管理器参数,如果能够将它们保存为初始化字符串,那就太好了。为此,该类成为CFactorable 类的后代。以单例模式实现是为了确保在不同类的对象中使用风险管理器的能力。但后来发现,只有 CVirtualAdvisor EA 类才需要风险管理器。因此,我们从风险管理器类中删除了单例模式的实现。

首先,让我们为可能的风险管理器状态和可能的限额计算方法创建枚举:

// Possible risk manager states
enum ENUM_RM_STATE {
   RM_STATE_OK,            // Limits are not exceeded 
   RM_STATE_DAILY_LOSS,    // Daily limit is exceeded
   RM_STATE_OVERALL_LOSS   // Overall limit is exceeded
};


// Possible methods for calculating limits
enum ENUM_RM_CALC_LIMIT {
   RM_CALC_LIMIT_FIXED,          // Fixed (USD)
   RM_CALC_LIMIT_FIXED_PERCENT,  // Fixed (% from Base Balance)
   RM_CALC_LIMIT_PERCENT         // Relative (% from Daily Level)
};


在风险管理器类的描述中,我们将使用几个属性来存储通过初始化字符串传递给构造函数的输入参数。我们还将添加用于存储各种计算特征(当前余额、资金、利润等)的属性。让我们在受保护部分声明一些辅助方法。在公有部分,我们基本上只有一个构造函数和一个处理每个分时报价的方法。我们现在只提及保存/加载方法和字符串转换操作符,稍后再编写实现方法。

然后类描述看起来像这样:

//+------------------------------------------------------------------+
//| Risk management class (risk manager)                             |
//+------------------------------------------------------------------+
class CVirtualRiskManager : public CFactorable {
protected:
// Main constructor parameters
   bool              m_isActive;             // Is the risk manager active?

   double            m_baseBalance;          // Base balance

   ENUM_RM_CALC_LIMIT m_calcDailyLossLimit;  // Method of calculating the maximum daily loss
   double            m_maxDailyLossLimit;    // Parameter of calculating the maximum daily loss

   ENUM_RM_CALC_LIMIT m_calcOverallLossLimit;// Method of calculating the total daily loss
   double            m_maxOverallLossLimit;  // Parameter of calculating the maximum total loss

// Current state
   ENUM_RM_STATE     m_state;

// Updated values
   double            m_balance;              // Current balance
   double            m_equity;               // Current equity
   double            m_profit;               // Current profit
   double            m_dailyProfit;          // Daily profit
   double            m_overallProfit;        // Total profit
   double            m_baseDailyBalance;     // Daily basic balance
   double            m_baseDailyEquity;      // Daily base balance
   double            m_baseDailyLevel;       // Daily base level
   double            m_virtualProfit;        // Profit of open virtual positions

// Managing the size of open positions
   double            m_prevDepoPart;         // Used part of the total balance

// Protected methods
   double            DailyLoss();            // Maximum daily loss
   double            OverallLoss();          // Maximum total loss

   void              UpdateProfit();         // Update current profit values
   void              UpdateBaseLevels();     // Updating daily base levels

   void              CheckLimits();          // Check for excess of permissible losses
   void              CheckDailyLimit();      // Check for excess of the permissible daily loss
   void              CheckOverallLimit();    // Check for excess of the permissible total loss

   double            VirtualProfit();        // Determine the real size of the virtual position

public:
                     CVirtualRiskManager(string p_params);     // Constructor

   virtual void      Tick();                 // Tick processing in risk manager 

   virtual bool      Load(const int f);      // Load status
   virtual bool      Save(const int f);      // Save status

   virtual string    operator~() override;   // Convert object to string
};


风险管理器对象的构造函数希望初始化字符串包含六个数值,这些数值在转换为适当的数据类型后,将分配给对象的主要属性。此外,在创建时,我们将状态设置为正常(未超出限制)。如果在一天中的某个时间段重新启动 EA 时重新创建了对象,则在加载保存的信息时,应将状态更正为上次保存时的状态。这同样适用于设置账户余额中分配用于交易的份额 - 构造函数中设置的值可以在加载已保存的风险管理信息时预先定义。

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualRiskManager::CVirtualRiskManager(string p_params) {
// Save the initialization string
   m_params = p_params;

// Read the initialization string and set the property values
   m_isActive = (bool) ReadLong(p_params);
   m_baseBalance = ReadDouble(p_params);
   m_calcDailyLossLimit = (ENUM_RM_CALC_LIMIT) ReadLong(p_params);
   m_maxDailyLossLimit = ReadDouble(p_params);
   m_calcOverallLossLimit = (ENUM_RM_CALC_LIMIT) ReadLong(p_params);
   m_maxOverallLossLimit = ReadDouble(p_params);

// Set the state: Limits are not exceeded
   m_state = RM_STATE_OK;

// Remember the share of the account balance allocated for trading
   m_prevDepoPart = CMoney::DepoPart();

// Update base daily levels
   UpdateBaseLevels();

// Adjust the base balance if it is not set
   if(m_baseBalance == 0) {
      m_baseBalance = m_balance;
   }
}


风险管理器将在事件处理程序中对每个分时报价执行主要工作。这将涉及检查风险管理器的活动,如果活动,必要时更新当前利润值和基础每日水平,以及检查是否超过损失限额:

//+------------------------------------------------------------------+
//| Tick processing in the risk manager                              |
//+------------------------------------------------------------------+
void CVirtualRiskManager::Tick() {
// If the risk manager is inactive, exit
   if(!m_isActive) {
      return;
   }

// Update the current profit values
   UpdateProfit();

// If a new daily period has begun, then we update the base daily levels
   if(IsNewBar(Symbol(), PERIOD_D1)) {
      UpdateBaseLevels();
   }

// Check for exceeding loss limits
   CheckLimits();
}


我们还想单独指出一点,由于开发了涉及虚拟仓位的结构(交易量的接收者会将虚拟仓位转化为真实市场仓位)和资本管理模块(允许我们在虚拟仓位和真实仓位的规模之间设置所需的比例系数),我们可以非常容易地实现市场仓位的安全平仓,而不会违反工作策略的交易逻辑。为此,只需将资本管理模块中的缩放系数设置为 0:

CMoney::DepoPart(0);               // Set the used portion of the total balance to 0


如果在此之前,我们在 m_prevDepoPart 属性中记住了之前的比率,那么在新的一天到来且每日限额更新后,我们只需将该比率恢复为之前的值,即可恢复之前关闭的真实仓位: 

CMoney::DepoPart(m_prevDepoPart);  // Return the used portion of the total balance


当然,与此同时,我们也无法预先知道仓位会以更差还是更好的价格重新开启。但我们可以肯定的是,增加风险管理器并不会影响所有交易策略的整体表现。

现在,让我们来看看风险管理器类的其余方法。

UpdateProfits() 方法中,我们更新余额、资金和利润的当前值,并将当前资金与每日水平之差作为每日利润进行计算。需要注意的是,该值并不总是与当前利润一致。如果某些交易在新的每日时段开始后已经关闭,则会出现差异。我们以当前资金与基础余额之间的差额来计算总损失。

//+------------------------------------------------------------------+
//| Updating current profit values                                   |
//+------------------------------------------------------------------+
void CVirtualRiskManager::UpdateProfit() {
   m_equity = AccountInfoDouble(ACCOUNT_EQUITY);
   m_balance = AccountInfoDouble(ACCOUNT_BALANCE);
   m_profit = m_equity - m_balance;
   m_dailyProfit = m_equity - m_baseDailyLevel;
   m_overallProfit = m_equity - m_baseBalance;
   m_virtualProfit = VirtualProfit();

   if(IsNewBar(Symbol(), PERIOD_H1) && PositionsTotal() > 0) {
      PrintFormat(__FUNCTION__" | VirtualProfit = %.2f | Profit = %.2f | Daily Profit = %.2f",
                  m_virtualProfit, m_profit, m_dailyProfit);
   }
}

在这个方法中,我们还计算所谓的当前虚拟利润。它是根据开启的虚拟仓位计算出来的。如果我们在触发风险管理限制时让虚拟仓位处于未平仓状态,那么即使在没有真实未平仓仓位的情况下,我们也可以随时估算出如果风险管理器关闭的真实仓位保持未平仓状态下的大致利润。遗憾的是,这一计算参数并不能给出完全准确的结果(有几个百分点的误差)。不过,它还是有用的。

VirtualProfit() 方法计算当前的虚拟利润。在这个方法中,我们会得到一个指向虚拟交易量接收器对象的指针,因为我们需要从中找出虚拟仓位的总数,并能够访问每个虚拟仓位。然后,我们循环查看所有虚拟仓位,要求资金管理模块计算每个仓位的虚拟利润,并根据当前的交易资金进行缩放:

//+------------------------------------------------------------------+
//| Determine the profit of open virtual positions                   |
//+------------------------------------------------------------------+
double CVirtualRiskManager::VirtualProfit() {
   // Access the receiver object
   CVirtualReceiver *m_receiver = CVirtualReceiver::Instance();
   
   double profit = 0;
   
   // Find the profit sum for all virtual positions
   FORI(m_receiver.OrdersTotal(), profit += CMoney::Profit(m_receiver.Order(i)));
   
   return profit;
}

在这个方法中,我们使用了一个新的宏 FORI,下面将对此进行讨论。

当新的每日周期开始时,我们将重新计算每日基础余额、资金和水平。我们还将检查,如果前一天达到了每日损失限额,那么我们就需要恢复交易,并根据未关闭虚拟仓位重开真实仓位。UpdateBaseLevels() 方法会处理这个问题:

//+------------------------------------------------------------------+
//| Update daily base levels                                         |
//+------------------------------------------------------------------+
void CVirtualRiskManager::UpdateBaseLevels() {
// Update balance, funds and base daily level
   m_baseDailyBalance = m_balance;
   m_baseDailyEquity = m_equity;
   m_baseDailyLevel = MathMax(m_baseDailyBalance, m_baseDailyEquity);

   PrintFormat(__FUNCTION__" | DAILY UPDATE: Balance = %.2f | Equity = %.2f | Level = %.2f",
               m_baseDailyBalance, m_baseDailyEquity, m_baseDailyLevel);

// If the daily loss level was reached earlier, then
   if(m_state == RM_STATE_DAILY_LOSS) {
      // Restore the status to normal:
      CMoney::DepoPart(m_prevDepoPart);         // Return the used portion of the total balance
      m_state = RM_STATE_OK;                    // Set the risk manager to normal
      CVirtualReceiver::Instance().Changed();   // Notify the recipient about changes

      PrintFormat(__FUNCTION__" | VirtualProfit = %.2f | Profit = %.2f | Daily Profit = %.2f",
                  m_virtualProfit, m_profit, m_dailyProfit);
      PrintFormat(__FUNCTION__" | RESTORE: depoPart = %.2f",
                  m_prevDepoPart);
   }
}


根据参数中规定的方法计算最大损失,我们有两种方法:DailyLoss()OverallLoss()。它们的实现方法非常相似,唯一的区别在于计算所用的数值参数和方法参数:

//+------------------------------------------------------------------+
//| Maximum daily loss                                               |
//+------------------------------------------------------------------+
double CVirtualRiskManager::DailyLoss() {
   if(m_calcDailyLossLimit == RM_CALC_LIMIT_FIXED) {
      // To get a fixed value, just return it 
      return m_maxDailyLossLimit;
   } else if(m_calcDailyLossLimit == RM_CALC_LIMIT_FIXED_PERCENT) {
      // To get a given percentage of the base balance, calculate it 
      return m_baseBalance * m_maxDailyLossLimit / 100;
   } else { // if(m_calcDailyLossLimit == RM_CALC_LIMIT_PERCENT)
      // To get a specified percentage of the daily level, calculate it
      return m_baseDailyLevel * m_maxDailyLossLimit / 100;
   }
}

//+------------------------------------------------------------------+
//| Maximum total loss                                               |
//+------------------------------------------------------------------+
double CVirtualRiskManager::OverallLoss() {
   if(m_calcOverallLossLimit == RM_CALC_LIMIT_FIXED) {
      // To get a fixed value, just return it 
      return m_maxOverallLossLimit;
   } else if(m_calcOverallLossLimit == RM_CALC_LIMIT_FIXED_PERCENT) {
      // To get a given percentage of the base balance, calculate it 
      return m_baseBalance * m_maxOverallLossLimit / 100;
   } else { // if(m_calcDailyLossLimit == RM_CALC_LIMIT_PERCENT)
      // To get a specified percentage of the daily level, calculate it
      return m_baseDailyLevel * m_maxOverallLossLimit / 100;
   }
}


检查限额的 CheckLimits() 方法只需调用两个辅助方法来检查每日亏损和总亏损:

//+------------------------------------------------------------------+
//| Check loss limits                                                |
//+------------------------------------------------------------------+
void CVirtualRiskManager::CheckLimits() {
   CheckDailyLimit();      // Check daily limit
   CheckOverallLimit();    // Check total limit
}


每日亏损检查方法使用 DailyLoss() 方法获取允许的最大每日亏损限额,并将其与当前的每日利润进行比较。当超过限额时,风险管理器会切换到 "超过每日限额" 状态,并通过将已用交易余额的大小设为 0 来关闭未结仓位:

//+------------------------------------------------------------------+
//| Check daily loss limit                                           |
//+------------------------------------------------------------------+
void CVirtualRiskManager::CheckDailyLimit() {
// If daily loss is reached and positions are still open
   if(m_dailyProfit < -DailyLoss() && CMoney::DepoPart() > 0) {
   // Switch the risk manager to the achieved daily loss state:
      m_prevDepoPart = CMoney::DepoPart();   // Save the previous value of the used part of the total balance
      CMoney::DepoPart(0);                   // Set the used portion of the total balance to 0
      m_state = RM_STATE_DAILY_LOSS;         // Set the risk manager to the achieved daily loss state
      CVirtualReceiver::Instance().Changed();// Notify the recipient about changes

      PrintFormat(__FUNCTION__" | VirtualProfit = %.2f | Profit = %.2f | Daily Profit = %.2f",
                  m_virtualProfit, m_profit, m_dailyProfit);
      PrintFormat(__FUNCTION__" | RESET: depoPart = %.2f",
                  CMoney::DepoPart());
   }
}


总损失测试方法的原理与此类似,唯一的区别在于它是将总利润与可接受的总损失进行比较。如果超过了总限额,风险管理器就会切换到 "超过总限额" 状态。

将获得的代码保存到当前文件夹的 VirtualRiskManager.mqh 文件中。

现在,让我们来看看为了使用新的风险管理器类,我们需要对之前创建的项目文件进行哪些更改和添加。


实用的宏

在处理数组的有用的宏列表中,我添加了一个新宏 FORI(N, D)。它用 i 变量安排了一个循环,执行 ND 表达式:

// Useful macros for array operations
#ifndef __MACROS_INCLUDE__
#define APPEND(A, V)    A[ArrayResize(A, ArraySize(A) + 1) - 1] = V;
#define FIND(A, V, I)   { for(I=ArraySize(A)-1;I>=0;I--) { if(A[I]==V) break; } }
#define ADD(A, V)       { int i; FIND(A, V, i) if(i==-1) { APPEND(A, V) } }
#define FOREACH(A, D)   { for(int i=0, im=ArraySize(A);i<im;i++) {D;} }
#define FORI(N, D)      { for(int i=0; i<N;i++) {D;} }
#define REMOVE_AT(A, I) { int s=ArraySize(A);for(int i=I;i<s-1;i++) { A[i]=A[i+1]; } ArrayResize(A, s-1);}
#define REMOVE(A, V)    { int i; FIND(A, V, i) if(i>=0) REMOVE_AT(A, i) }

#define __MACROS_INCLUDE__
#endif

将更改保存到当前文件夹的 Macros.mqh 文件中。


CMoney 资金管理类

在本类中,我们将添加一种计算虚拟仓位利润的方法,并将其交易量的缩放因子考虑在内。实际上,我们在 Volume() 方法中执行了类似的操作,以确定虚拟仓位的计算大小:根据当前可用于交易的余额大小和虚拟仓位交易量对应的余额大小的信息,我们找到一个缩放因子,等于这些余额的比率。然后将该系数乘以虚拟仓位交易量,得出计算交易量,即交易账户中将开立的交易量。

因此,让我们首先从 Volume() 方法中删除查找缩放因子的代码部分,将其转换为单独的 Coeff() 方法:

//+------------------------------------------------------------------+
//| Calculate the virtual position volume scaling factor             |
//+------------------------------------------------------------------+
double CMoney::Coeff(CVirtualOrder *p_order) {
   // Request the normalized strategy balance for the virtual position
   double fittedBalance = p_order.FittedBalance();

   // If it is 0, then the scaling factor is 1
   if(fittedBalance == 0.0) {
      return 1;
   }

   // Otherwise, find the value of the total balance for trading
   double totalBalance = s_fixedBalance > 0 ? s_fixedBalance : AccountInfoDouble(ACCOUNT_BALANCE);

   // Return the volume scaling factor
   return totalBalance * s_depoPart / fittedBalance;
}


在此之后,Volume()Profit() 方法的实现就变得非常相似:我们从虚拟仓位中获取所需的值 (交易量或利润),然后乘以由此产生的缩放因子:

//+------------------------------------------------------------------+
//| Determine the calculated size of the virtual position            |
//+------------------------------------------------------------------+
double CMoney::Volume(CVirtualOrder *p_order) {
   return p_order.Volume() * Coeff(p_order);
}

//+------------------------------------------------------------------+
//| Determining the calculated profit of a virtual position          |
//+------------------------------------------------------------------+
double CMoney::Profit(CVirtualOrder *p_order) {
   return p_order.Profit() * Coeff(p_order);
}


当然,我们需要在类描述中添加新的方法:

//+------------------------------------------------------------------+
//| Basic money management class                                     |
//+------------------------------------------------------------------+
class CMoney {
   ...
   
   // Calculate the scaling factor of the virtual position volume
   static double     Coeff(CVirtualOrder *p_order);

public:
   CMoney() = delete;                  // Disable the constructor
   
   // Determine the calculated size of the virtual position
   static double     Volume(CVirtualOrder *p_order);
   
   // Determine the calculated profit of a virtual position  
   static double     Profit(CVirtualOrder *p_order);  

   ...
};

保存对当前文件夹中 Money.mqh 文件所做的更改。


СVirtualFactory

由于我们创建的风险管理器类是 CFactorable 类的后代,因此为了确保创建该类的可能性,有必要扩展由 CVirtualFactory 创建的对象的组成。在 Create() 静态方法中,添加一个代码块,负责创建 CVirtualRiskManager 类对象:

//+------------------------------------------------------------------+
//| Object factory class                                             |
//+------------------------------------------------------------------+
class CVirtualFactory {
public:
   // Create an object from the initialization string
   static CFactorable* Create(string p_params) {
      // Read the object class name
      string className = CFactorable::ReadClassName(p_params);
      
      // Pointer to the object being created
      CFactorable* object = NULL;

      // Call the corresponding constructor  depending on the class name
      if(className == "CVirtualAdvisor") {
         object = new CVirtualAdvisor(p_params);
      } else if(className == "CVirtualRiskManager") {
         object = new CVirtualRiskManager(p_params);
      } else if(className == "CVirtualStrategyGroup") {
         object = new CVirtualStrategyGroup(p_params);
      } else if(className == "CSimpleVolumesStrategy") {
         object = new CSimpleVolumesStrategy(p_params);
      }
      
      ...

      return object;
   }
};

将获得的代码保存到当前文件夹的 VirtualFactory.mqh 文件中。


CVirtualAdvisor

我们需要对 CVirtualAdvisor EA 类进行更多重大修改。由于我们已决定只在本类中使用风险管理器对象,因此我们将在类描述中添加相应的属性:

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   CVirtualReceiver     *m_receiver;      // Receiver object that brings positions to the market
   CVirtualInterface    *m_interface;     // Interface object to show the status to the user
   CVirtualRiskManager  *m_riskManager;   // Risk manager object

   ...
};


我们还同意,风险管理器初始化字符串将嵌入 EA 初始化字符串,紧接在策略组初始化字符串之后。另外,让我们在构造函数中把这个初始化字符串读入 riskManagerParams 变量,然后再从中创建风险管理器:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(string p_params) {
// Save the initialization string
   m_params = p_params;

// Read the initialization string of the strategy group object
   string groupParams = ReadObject(p_params);

// Read the initialization string of the risk manager object
   string riskManagerParams = ReadObject(p_params);

// Read the magic number
   ulong p_magic = ReadLong(p_params);

// Read the EA name
   string p_name = ReadString(p_params);

// Read the work flag only at the bar opening
   m_useOnlyNewBar = (bool) ReadLong(p_params);

// If there are no read errors,
   if(IsValid()) {
      ...
      
      // Create the risk manager object 
      m_riskManager = NEW(riskManagerParams);
   }
}


既然我们在构造函数中创建了一个对象,那么也应该在析构函数中删除它:

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CVirtualAdvisor::~CVirtualAdvisor() {
   if(!!m_receiver)     delete m_receiver;      // Remove the recipient
   if(!!m_interface)    delete m_interface;     // Remove the interface
   if(!!m_riskManager)  delete m_riskManager;   // Remove risk manager
   DestroyNewBar();           // Remove the new bar tracking objects 
}


最重要的是从相关的 EA 处理程序中调用风险管理器的 Tick() 处理函数。请注意,风险管理器处理函数是在调整市场交易量之前启动的,因此,如果超出了损失限额,或者相反,限额被更新,那么接收者就可以在处理同一分时报价时调整市场仓位的未平仓交易量:

//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CVirtualAdvisor::Tick(void) {
// Define a new bar for all required symbols and timeframes
   bool isNewBar = UpdateNewBar();

// If there is no new bar anywhere, and we only work on new bars, then exit
   if(!isNewBar && m_useOnlyNewBar) {
      return;
   }

// Receiver handles virtual positions
   m_receiver.Tick();

// Start handling in strategies
   CAdvisor::Tick();

// Risk manager handles virtual positions
   m_riskManager.Tick();

// Adjusting market volumes
   m_receiver.Correct();

// Save status
   Save();

// Render the interface
   m_interface.Redraw();
}

保存对当前文件夹中 VirtualAdvisor.mqh 文件所做的更改。


SimpleVolumesExpertSingle EA

要测试风险管理器,只需在 EA 中添加指定参数的功能,并生成所需的初始化字符串。现在,让我们把风险管理器的所有六个参数都移动到独立的 EA 输入参数中:

input group "===  Risk management"
input bool        rmIsActive_             = true;
input double      rmStartBaseBalance_     = 10000;
input ENUM_RM_CALC_LIMIT 
                  rmCalcDailyLossLimit_   = RM_CALC_LIMIT_FIXED;
input double      rmMaxDailyLossLimit_    = 200;
input ENUM_RM_CALC_LIMIT 
                  rmCalcOverallLossLimit_ = RM_CALC_LIMIT_FIXED;
input double      rmMaxOverallLossLimit_  = 500;


OnInit() 函数中,有必要添加风险管理器初始化字符串的创建,并将其嵌入到 EA 初始化字符串中。同时,我们将稍微重写为一个策略和包含这一个策略的组创建初始化字符串的代码,将单个对象的初始化字符串分离到不同的变量中:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   CMoney::FixedBalance(fixedBalance_);
   CMoney::DepoPart(1.0);

// Prepare the initialization string for a single strategy instance
   string strategyParams = StringFormat(
                              "class CSimpleVolumesStrategy(\"%s\",%d,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%d)",
                              Symbol(), Period(),
                              signalPeriod_, signalDeviation_, signaAddlDeviation_,
                              openDistance_, stopLevel_, takeLevel_, ordersExpiration_,
                              maxCountOfOrders_
                           );

// Prepare the initialization string for a group with one strategy instance
   string groupParams = StringFormat(
                           "class CVirtualStrategyGroup(\n"
                           "       [\n"
                           "        %s\n"
                           "       ],%f\n"
                           "    )",
                           strategyParams, scale_
                        );

// Prepare the initialization string for the risk manager
   string riskManagerParams = StringFormat(
                                 "class CVirtualRiskManager(\n"
                                 "       %d,%.2f,%d,%.2f,%d,%.2f"
                                 "    )",
                                 rmIsActive_, rmStartBaseBalance_,
                                 rmCalcDailyLossLimit_, rmMaxDailyLossLimit_,
                                 rmCalcOverallLossLimit_, rmMaxOverallLossLimit_
                              );

// Prepare the initialization string for an EA with a group of a single strategy and the risk manager
   string expertParams = StringFormat(
                            "class CVirtualAdvisor(\n"
                            "    %s,\n"
                            "    %s,\n"
                            "    %d,%s,%d\n"
                            ")",
                            groupParams,
                            riskManagerParams,
                            magic_, "SimpleVolumesSingle", true
                         );

   PrintFormat(__FUNCTION__" | Expert Params:\n%s", expertParams);

// Create an EA handling virtual positions
   expert = NEW(expertParams);

   if(!expert) return INIT_FAILED;

   return(INIT_SUCCEEDED);
}

将获得的代码保存到当前文件夹下的 SimpleVolumesExpertSingle.mq5 文件中。现在,测试风险管理器操作的一切准备就绪。


测试

让我们使用前几个开发阶段优化过程中获得的一个交易策略实例的参数。我们称这种交易策略为模型策略。模型策略参数如图 1 所示。

图 1.模型策略参数


让我们在 2021-2022 年期间,使用这些参数和关闭的风险管理器,运行一个单一的测试通过。我们得到以下结果:

图 2.没有风险管理器的模型策略结果


从图中可以看出,在选定的时间段内,资金出现了几次明显的回撤。其中最大的一次发生在 2021 年 10 月底(约 380 美元)和 2022 年 6 月(约 840 美元)。

现在,让我们打开风险管理器,将每日最大损失限额设为 150 美元,最大总损失设为 450 美元。我们得到以下结果:


图 3。无风险管理器的模型策略结果(最大损失:150 美元和 450 美元)


图表显示,2021 年 10 月,风险管理器关闭了两次亏损的市场仓位,但虚拟仓位仍未关闭。因此,第二天一到,市场仓位又被打开了。不幸的是,重新开放的价格并不优惠,因此余额和基金的总回撤额略高于禁用风险管理器的净值回撤。同样明显的是,在平仓之后,该策略并没有获得微薄的利润(与没有风险管理器的情况一样),反而出现了一些亏损。

2022 年 6 月,风险管理器已被触发七次,在每日损失达到 150 美元时关闭了市场头寸。结果又是以较低的价格重新开启,这一系列交易造成了损失。但是,如果这样一个 EA 在一家自营交易公司的模拟账户上运行,并设定了每日最大损失和总损失这样的参数,那么如果没有风险管理器,该账户就会因违反交易规则而被停止,而如果有了风险管理器,该账户就会继续运行,并因此获得略少的利润。

尽管我将总损失设置为 450 美元,而 6 月份的总余额回撤超过了 1000 美元,但由于总损失是根据基础余额计算的,因此并未达到最大损失总额。换句话说,如果资金低于(10,000 - 450)= 9550 美元,就实现了这一目标。但由于之前积累的利润,这一时期的资金额肯定不会低于 10 000 美元。因此,EA 继续开展工作,同时开启市场仓位。

现在让我们模拟一下触发总亏损的情况。为此,我们将提高仓位大小的缩放系数,以便在 2021 年 10 月尚未超过最大亏损总额,而在 2022 年 6 月超过最大亏损总额。让我们设置 scale_ = 50,看看结果如何:

图 4.无风险管理器的模型策略结果(最大损失:150 美元和 450 美元),scale_ = 50


我们可以看到,交易将于 2022 年 6 月结束。在随后的一段时间里,EA 没有开立一个仓位。这是因为达到了总亏损限额(9550 美元)。我们还可以注意到,现在不仅在 2021 年 10 月,而且在其他几个时期,达到每日亏损额的情况也更频繁地出现。

因此,我们的两个限制工具都能正常工作。

风险管理器即使在自营交易公司之外也能发挥作用。举例说明,让我们尝试优化模型策略的风险管理器参数,努力扩大开仓规模,但不超过 10%的允许回撤率。为此,我们将在风险管理器参数中设置最大总损失等于每日水平的 10%。我们还将了解优化期间的最大日亏损额,也是按日亏损额的百分比计算的。


图 5.利用风险管理器优化模型策略的结果


结果显示,使用风险管理器后,一年的标准化利润几乎增加了一倍半:从 1560 美元增至 2276 美元(结果列)。以下是最佳通过的单独显示效果:

图 6.无风险管理器的模型策略结果(最大损失:7.6% 和 10%, scale_ = 88)


请注意,在整个测试期间,EA 都在继续开启交易。这意味着 10%的总限额从未被触发。显然,将风险管理器应用于单个交易策略实例并没有特别的意义,因为我们并不打算在真实账户中逐一启动这些策略。不过,对一个实例有效的方法对有许多实例的 EA 也应同样有效。因此,即使是这样粗略的结果,我们也可以说,风险管理器肯定是有用的。


    结论

    因此,我们现在有了一个基本的交易风险管理器,它可以让我们遵守指定的每日最大亏损和总亏损水平。它还不支持在重启 EA 时保存和加载状态,因此我不建议在真实账户上使用它。但这种修改并没有带来任何特别的困难。我稍后再谈。

    同时,还可以尝试增加在不同时段限制交易的功能,从禁止在一周中某些天的某些时段进行交易,到禁止在重要经济新闻发布期间开立新仓位。风险管理器的其他可能发展领域包括:更平稳地改变仓位大小(例如,当超过限额的一半时,减少两倍),以及更 "智能" 地恢复交易量(例如,只有当损失超过仓位减少水平时)。

    我会推迟到以后再说。现在,我将继续自动优化 EA。第一阶段已在前一篇文章中实现。现在该进入第二阶段了。

    感谢您的关注!期待很快与您见面!


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

    附加的文件 |
    Macros.mqh (2.4 KB)
    Money.mqh (6.52 KB)
    VirtualFactory.mqh (4.46 KB)
    VirtualAdvisor.mqh (23.02 KB)
    跨邻域搜索(ANS) 跨邻域搜索(ANS)
    本文揭示了跨邻域搜索(ANS)算法的潜力,作为重要的一步,旨在开发灵活且智能的优化方法,使其能够在搜索空间中考虑问题的具体特性和环境的动态变化。
    您应当知道的 MQL5 向导技术(第 16 部分):配合本征向量进行主成分分析 您应当知道的 MQL5 向导技术(第 16 部分):配合本征向量进行主成分分析
    本文所见的主成分分析,是数据分析中的一种降维技术,文中还有如何配合本征值和向量来实现它。一如既往,我们瞄向的是开发一个可在 MQL5 向导中使用的原型专业信号类。
    构建K线趋势约束模型(第五部分):通知系统(第三部分) 构建K线趋势约束模型(第五部分):通知系统(第三部分)
    本系列文章的这一部分专门介绍如何将WhatsApp与MetaTrader 5集成以实现通知功能。我们包含一张流程图以简化理解,并将讨论在集成过程中安全措施的重要性。指标的主要目的是通过自动化的简化分析过程,并且它们应包含通知方法,以便在满足特定条件时向用户发出警报。欲了解更多信息,请阅读本文。
    开发Python交易机器人(第三部分):实现基于模型的交易算法 开发Python交易机器人(第三部分):实现基于模型的交易算法
    让我们继续阅读关于使用Python和MQL5开发交易机器人系列的文章。在本文中,我们将用Python中创建一个交易算法。