English Русский Español Deutsch 日本語 Português
preview
风险管理(第三部分):构建风险管理主类

风险管理(第三部分):构建风险管理主类

MetaTrader 5示例 |
208 1
Niquel Mendoza
Niquel Mendoza


概述

我们继续我们的系列文章,我们在其中开发了一个风险管理系统。在上一篇文章中,我们运用第一部分学到的概念创建了界面。现在,在本系列的第三部分中,我们将重点完善风险管理类的核心结构。

在本文中,我们将创建一个类,允许我们为亏损和利润赋值,为利润计算和跟踪奠定基础。这是朝着建立一个稳健和功能强大的风险管理系统迈出的关键一步。

风险管理设计与规划

以下框架概述了我们系统中设计和规划风险管理的结构化计划:

  1. 定义、结构和枚举
    第一步是定义所需的结构和枚举。这些对于存储关键信息(如累计亏损和利润)以及促进系统内的数据处理至关重要。

  2. 创建 CRiskManagement 类
    接下来,我们将开发负责风险管理的主类 CRiskManagement。这个中心类将整合所有与风险控制相关的计算和流程,确保有组织和高效的实现。

  3. 基本函数:值的赋值和检索
    然后我们将实现赋值和检索函数,使我们能够更新和查询损益。在这个阶段,我们还将定义类构造函数和析构函数,以便正确管理内存和数据初始化。

  4. 事件驱动函数
    最后,我们将开发在关键时刻触发的函数,例如新的一天或一周的开始。这些函数将有助于重新计算利润、调整风险和重新初始化某些时期的累积变量,确保随着时间的推移进行适当的绩效跟踪。

有了这项计划,我们将从头开始建设。


常量、枚举和结构体的定义

在实现代码之前,我们将把所有风险管理逻辑组织到一个名为 Risk_Management.mqh 的单独文件中(我们创建的基础文件)。这使代码更加结构化和模块化。

遵循我们的风险管理框架,第一步是定义所需的结构、枚举和常量。这些元素对于多种函数至关重要,尤其是那些负责计算和控制每日、每周或单笔交易最大亏损等数值的函数。

常量定义(#define)

为了便于风险管理和提高代码可读性,我们将定义几个关键常数:

1.NOT_MAGIC_NUMBER

该常量将用作类构造函数中的初始化值。其目的是表明不会使用特定的幻数;相反,风险管理将适用于所有交易,而不限制于特定的标识符。在真实交易环境中对整个账户实施风险管理时,这非常有用。

#define NOT_MAGIC_NUMBER 0 //Not Magic Number 

2.平仓标志

为了使平仓函数更加动态,我们将使用标志而不是创建多个单独的函数来关闭盈利、亏损或两者兼有的交易。这些标志可以简化代码并提高代码的重用性。

#define FLAG_CLOSE_ALL_PROFIT 2 //Flag indicating to close only operations with profit
#define FLAG_CLOSE_ALL_LOSS   4 //Flag indicating to close only operations without profit

枚举

1.计算方法:固定金额或百分比

我们需要一个枚举来确定该策略中最大损失和利润的计算方法。主要有两种方法:

  • 固定的(资金):用户设定一个具体的金额来定义最大亏损或盈利。无论账户余额如何,该值保持不变。

    • 示例:系统运行期间,每日最大亏损金额将保持在 1000 美元不变。
  • 动态的(百分比):用户可以选择一个百分比,该百分比应用于账户中的参考参数,例如余额或净值,而不是一个固定值。

    • 示例:如果限额为余额的 2%,则最大亏损会随账户余额而变化。如果余额增加,亏损限额也相应增加;如果余额减少,限额也相应减少。

这在代码中用以下枚举表示。

enum ENUM_RISK_CALCULATION_MODE //enumeration to define the types of calculation of the value of maximum profits and losses
 {
  money, //Money
  percentage //Percentage %
 };

2.百分比应用:风险计算的参考参数

如果计算方法是基于百分比的,我们必须定义该百分比将应用于哪个账户值。我们考虑以下四个主要选项:

  • 余额 - 应用于账户总余额。
  • 净利润 - 基于账户创建以来的累计利润。
  • 可用保证金 - 根据可用于开设新交易的资金计算得出。
  • 净额 - 代表根据未平仓头寸的盈亏进行调整后的余额。
这在代码中用以下枚举表示。
enum ENUM_APPLIED_PERCENTAGES //enumeration to define the value to which the percentages will be applied
 {
  Balance, //Balance
  ganancianeta,//Net profit
  free_margin, //Free margin
  equity //Equity
 };

3.风险管理类型:个人账户或自营公司(FTMO)

如前几章所述,该系统既可应用于个人账户,也可应用于自营公司,特别是 FTMO。FTMO 采用更动态的方法来计算每日最大亏损。

稍后,在分析结构时,我们将详细解释 FTMO 如何处理此计算。目前,我们将创建一个枚举,允许用户选择风险管理的账户类型。

enum ENUM_MODE_RISK_MANAGEMENT //enumeration to define the type of risk management
 {
  propfirm_ftmo, //Prop Firm FTMO
  personal_account // Personal Account
 };

4.选择手数类型

并非所有交易者都喜欢使用动态手数进行交易。我们将允许用户选择手数类型。这种选择不会影响风险管理类的核心逻辑,但对于开发 EA 来说很重要,因为用户应该能够使用固定手数或动态手数进行交易。

我们将创建以下枚举:

enum ENUM_LOTE_TYPE //lot type
  {
   Dinamico,//Dynamic
   Fijo//Fixed
  };

开仓时,手数选择可以按以下方式设置:

 trade.Sell( (Lote_Type == Dinamico ? risk.GetLote(ORDER_TYPE_SELL,GET_LOT_BY_ONLY_RISK_PER_OPERATION) : lote), _Symbol,tick.bid,sl,tp,"EA Sell");

在这段代码中,如果用户选择动态手数(Dinamico),GetLote 函数将根据每笔交易的风险计算手数大小。如果用户选择固定手数(Fijo),EA 将使用分配给手数变量(lote)的默认值。

稍后,我们将创建 GetLot 函数来正确获取动态手数。

5.动态手数计算方法

如果用户选择动态手数,我们需要定义其大小的计算方式。我们将创建一个枚举来指定计算是仅基于每次交易的风险,还是根据止损进行调整。

  • 仅基于每笔交易风险的计算:
    手数大小完全根据单笔交易的风险百分比来确定。在这种情况下,无需指定止损的具体值。

  • 基于风险和止损的计算
    手数大小是根据每笔交易的风险计算的,并根据止损进行调整。这一点很重要,因为如果您在不考虑止损的情况下设定固定的风险百分比,您可能会收到不合适的交易手数。

    示例:
    假设我们想冒险投入 1000 美元账户中的 1%,这相当于最大风险为 10 美元。我们的策略在 EUR/USD(5 位小数经纪商)上采用 1:2 的风险回报比和 100 点止损,简单的计算可能会得出很多 0.02 的结果。

    • 如果交易触发止损,实际损失将是 2 美元而不是 10 美元。
    • 如果交易触发止盈,实际收益将为 4 美元而不是 20 美元。

    为了解决这个问题,我们调整手数,使得在给定止损的情况下,最大亏损恰好为账户的 1%。这样可以确保,如果交易达到 1:2 的止盈比例,则利润为 2%。

    GetIdealLot 函数(在前一篇文章中介绍过)会自动调整手数。

void GetIdealLot(double& nlot, double glot, double max_risk_per_operation, double& new_risk_per_operation, long StopLoss)

它会调整交易手数,使每次交易的风险保持在规定的限度内。

为了表示这两种计算方法,我们定义以下枚举:
enum ENUM_GET_LOT
 {
  GET_LOT_BY_ONLY_RISK_PER_OPERATION, //Obtain the lot for the risk per operation
  GET_LOT_BY_STOPLOSS_AND_RISK_PER_OPERATION //Obtain and adjust the lot through the risk per operation and stop loss respectively.
 };

结构:

结构在我们的风险管理体系中发挥着关键作用。如前所述,亏损和利润的计算方式可以是直接计算(固定金额)或动态计算(百分比)的。如果采用百分比法,则以账户中的参考值为准。

我们将创建一个包含所有必要信息的结构,用于定义亏损或盈利。

  • value - 表示亏损或盈利金额。
  • assigned_percentage - 如果计算是动态的,则表示分配的百分比。
  • mode_calculation_risk - 定义每笔交易的风险计算方法(直接或动态)。
  • percentage_applied_to - 枚举,指定在使用动态方法时,百分比适用于哪个帐户指标。

这在代码中表示为:

struct Loss_Profit
 {
  double             value; //value
  double             assigned_percentage; //percentage to apply
  ENUM_RISK_CALCULATION_MODE mode_calculation_risk; //risk calculation method
  ENUM_APPLIED_PERCENTAGES percentage_applied_to; //percentage applied to
 };

使用这种结构,我们可以确定以下几种盈亏方案:

名称  描述 重置
每日最大亏损 (MDL) 单日允许的最大亏损。 每日重置为 0。
最大亏损(ML) 账户在其存续期间可能达到的最大总亏损。 不重置。
单笔交易最大亏损 (GMLPO) 与用户定义的单笔交易风险类似;用于估计合适的交易手数。 每次交易平仓或新一天开始时重新计算。
每周最大亏损 (MWL) 一周内允许的最大亏损。这是可选的,因为对于自营公司来说并非必需。 每周重置。
单笔交易净最大亏损 (NMLPO) 仅在调用根据每笔交易风险和止损计算手数的函数时生成。 每次调用基于风险和止损的手数检索函数时都会重新计算。
每日最高利润 机器人或账户一天内可获得的最大利润。  每日重置。

FTMO 中每日最大亏损的特点

当将此风险管理系统应用于普通账户和自营公司(如 FTMO)时,会出现一些关键差异。在 FTMO 中,每日最大亏损额是动态变化的,因为如果在一天内产生盈利,亏损额可能会增加。

例如,在初始账户余额为 20 万美元的 FTMO 挑战赛中,每日最大损失限额为 1 万美元。如果您在已平仓交易中损失了 8000 美元,那么您当天的账户损失不得超过 2000 美元。你的未平仓浮动亏损额也不能超过 -2000 美元。该限额包含佣金和库存费费用。

反之亦然,如果你一天盈利 5000 美元,那么你可以承受损失 15000 美元,但不能损失更多。再次提醒,您的每日最大亏损额也包括您未平仓的交易。例如,如果你在一天内平仓亏损了 6000 美元,然后你又开了一笔新交易,这笔交易一开始亏损了大约 5700 美元,但最终却盈利了,很遗憾,已经太晚了。一瞬间,你的当日权益损失为 -11700 美元,超过了允许的 10000 美元损失。

FTMO每日最大亏损

正如 FTMO 的指导方针中直接指出的那样,每日最大亏损额会根据当天获得的盈利情况而有所不同。

例如,如果一天开始时您允许的最大亏损为 10000 美元,而您当天获得了利润,则该利润将添加到最初的 10000 美元中,从而增加您的允许亏损范围。因此,随着利润的积累,您可以承担的风险金额也会相应调整。


风险管理关键变量的声明

在本节中,我们将定义 CRiskManagement 类,该类负责管理交易风险。

//+------------------------------------------------------------------+
//|                  Class CRisk Management                          |
//+------------------------------------------------------------------+
class CRiskManagemet final

包含库

在定义类之前,我们将引入 CTrade.mqh 库,以便能够管理交易,例如平仓:

#include <Trade/Trade.mqh>

1.指向 CTrade 的指针

第一个变量是指向 CTrade 类的指针,这将允许我们与未平仓头寸进行交互:

private:
  //--- CTrade class pointer to be able to work with open positions
  CTrade             *trade;

2.关键变量

我们定义了三个主要变量:

  • account_profit(double)- 存储自1971.01.01以来的账户净利润(datetime 类型的最小日期);
  • 止损(long)- 维持用于计算手数大小的止损点数水平;
  • batch(double)- 存储上次计算手数的内部变量。

代码:

  //-- Main variables
  double             account_profit;
  long               StopLoss;
  double             lote;

3.通用变量

  1. magic_number(ulong)- 存储用于管理与给定交易集相关的风险的幻数。它也可以等于 0,这样就可以用于个人账户。
  2. mode_risk_management (ENUM_MODE_RISK_MANAGEMENT) - 定义将要应用的风险管理类型:适用于普通账户或自营公司(例如 FTMO)。

代码:

  //--- General variables
  ENUM_MODE_RISK_MANAGEMENT mode_risk_managemet;
  ulong              magic_number;

4.自营公司账户的具体变量(FTMO)

在自营会计公司账户中,风险管理规则适用于期初余额,而不是期末余额。例如,每日最大损失和最大总损失通常按期初余额的百分比计算(通常为 10%)。

为了实现这一功能,我们将添加一个变量来存储初始余额,该变量需要用户手动设置:

  //--- Variables to work with anchoring tests
  double             account_balance_propfirm;

5.下一笔交易预期最大亏损变量(NMLPO)

该变量将存储下一次交易的预计最大亏损。虽然不是强制性的,但可以用来预测如果机器人仓位达到止损水平将会亏损多少。

它只会通过 GetLote() 调用进行计算。

//--- Variables to store the values ​​of the maximum losses
  double             nmlpo;

6.损益管理变量

先前定义的 Loss_Profit 结构将用于存储损失金额、计算方法以及百分比的应用方式。

让我们定义五个关键变量:

  //--- variables that store percentages and enumeration, which will be used for the subsequent calculation of losses
  Loss_Profit        mdl, mwl, ml, gmlpo, mdp;

其中:

  • mdl - 每日最大亏损;
  • mwl - 每周最大亏损;
  • ml - 最大总亏损;
  • gmlpo - 每笔交易的最大亏损;
  • mdp - 每日最大利润。

7.用于利润的变量(日利润、周利润和总利润)

为了监控机器人/ EA 交易的绩效,我们将添加变量来存储盈利数据,以便在达到亏损限额或采取安全措施时发出警报。

我们需要:

  • 利润计算所依据的参考时间
  • 用于存储利润的变量。

代码:

  //--- Variables to store the profit and the time from which they will be obtained
  double             daily_profit, weekly_profit, gross_profit;
  datetime           last_weekly_time, last_day_time, init_time;

其中:

  • daily_profit - 当日获得的利润,
  • weekly_profit - 一周内获得的利润,
  • gross_profit - 自风险管理开始以来的总利润
  • last_weekly_time - 用于计算每周利润的最后记录日期
  • last_day_time - 用于计算每日利润的最后记录日期
  • init_time - 风险管理的开始日期。



创建构造函数、析构函数和初始化方法

现在我们已经定义了关键变量,接下来我们将实现 CRiskManagement 类的主要函数。 

构造函数 

构造函数将负责初始化关键变量,例如幻数、风险管理模式和自有公司账户余额(如果适用)。

构造函数声明如下:

CRiskManagemet(ulong magic_number_ = NOT_MAGIC_NUMBER,ENUM_MODE_RISK_MANAGEMENT mode_risk_management_ = personal_account, double account_propfirm_balance=0);

在构造函数实现中,我们给变量赋值,初始化 CTrade 对象,并计算从 1972 年 1 月 1 日开始的账户净利润。我们还设置了适当的时间戳来跟踪利润。

CRiskManagemet::CRiskManagemet(ulong magic_number_ =  NOT_MAGIC_NUMBER,ENUM_MODE_RISK_MANAGEMENT mode_risk_management_ = personal_account, double account_propfirm_balance =0)
 {
  if(magic_number_ == NOT_MAGIC_NUMBER)
   {
    Print("| Warning | No magic number has been chosen, taking into account all the magic numbers and the user's trades");
   }
//---
  this.account_balance_propfirm = account_propfirm_balance ;
  trade = new CTrade();
  this.account_profit = GetNetProfitSince(true,this.magic_number,D'1972.01.01 00:00');
  this.magic_number = magic_number_;
  this.mode_risk_managemet = mode_risk_management_;
//---
  this.last_day_time = iTime(_Symbol,PERIOD_D1,0);
  this.last_weekly_time = iTime(_Symbol,PERIOD_W1,0);
  this.init_time =magic_number_ != NOT_MAGIC_NUMBER ? TimeCurrent() : D'1972.01.01 00:00';
 }

请注意:如果用户未指定有效交易数量,则会显示警告,提示系统将考虑用户已开立的所有交易。

析构函数

当 CRiskManagement 实例被删除时,类析构函数将负责释放分配给 CTrade 对象的内存。

CRiskManagemet::~CRiskManagemet()
 {
  delete trade;
 }

初始化方法 

为了简化风险管理设置和更新,我们将创建一些特殊函数,使您能够:

  • 设置止损位,
  • 设置盈亏参数。

1.设置亏损结构值的函数

我们定义了一些方法,使我们能够为损益计算的关键变量赋值。

  //--- Functions to assign values ​​to variables for subsequent calculation of losses
  void               SetPorcentages(double percentage_or_money_mdl, double percentage_or_money_mwl,double percentage_or_money_gmlpo, double percentage_or_money_ml, double percentage_or_money_mdp_);    

  void               SetEnums(ENUM_RISK_CALCULATION_MODE mode_mdl_, ENUM_RISK_CALCULATION_MODE mode_mwl_, ENUM_RISK_CALCULATION_MODE mode_gmlpo_, ENUM_RISK_CALCULATION_MODE mode_ml_,
                             ENUM_RISK_CALCULATION_MODE mode_mdp_);  

  void               SetApplieds(ENUM_APPLIED_PERCENTAGES applied_mdl_,  ENUM_APPLIED_PERCENTAGES  applied_mwl_, ENUM_APPLIED_PERCENTAGES applied_gmlpo_,  ENUM_APPLIED_PERCENTAGES  applied_ml_,
                                ENUM_APPLIED_PERCENTAGES applied_mdp_); 

 2.为亏损结构分配百分比

以下方法为相应的结构赋予百分比或金额值。

void CRiskManagemet::SetPorcentages(double percentage_or_money_mdl,double percentage_or_money_mwl,double percentage_or_money_gmlpo,double percentage_or_money_ml,double percentage_or_money_mdp_)
 {
  this.gmlpo.assigned_percentage = percentage_or_money_gmlpo;
  this.mdl.assigned_percentage = percentage_or_money_mdl;
  this.ml.assigned_percentage = percentage_or_money_ml;
  this.mdp.assigned_percentage = percentage_or_money_mdp_;
  this.mwl.assigned_percentage = percentage_or_money_mwl;
 }

3.风险计算模式初始化

该方法为每个结构设置计算模式。如果用户选择以金额为基础的计算模式,则直接分配相应的值。

void CRiskManagemet::SetEnums(ENUM_RISK_CALCULATION_MODE mode_mdl_,ENUM_RISK_CALCULATION_MODE mode_mwl_,ENUM_RISK_CALCULATION_MODE mode_gmlpo_,ENUM_RISK_CALCULATION_MODE mode_ml_,ENUM_RISK_CALCULATION_MODE mode_mdp_)
 {
  this.gmlpo.mode_calculation_risk = mode_gmlpo_;
  this.mdl.mode_calculation_risk  = mode_mdl_;
  this.mdp.mode_calculation_risk  = mode_mdp_;
  this.ml.mode_calculation_risk  = mode_ml_;
  this.mwl.mode_calculation_risk  = mode_mwl_;
//-- If the money mode has been chosen, assign the variable that stores the money or percentage to the corresponding variables.
  this.gmlpo.value = this.gmlpo.mode_calculation_risk  == money ? this.gmlpo.value : 0;
  this.mdp.value  = this.mdp.mode_calculation_risk  == money ? this.mdp.value : 0;
  this.mdl.value  = this.mdl.mode_calculation_risk  == money ? this.mdl.value : 0;
  this.ml.value  = this.ml.mode_calculation_risk  == money ? this.ml.value : 0;
  this.mwl.value  = this.mwl.mode_calculation_risk  == money ? this.mwl.value : 0;
 }

4.为帐户分配参数

此方法决定如何将配置的百分比应用于帐户。

void CRiskManagemet::SetApplieds(ENUM_APPLIED_PERCENTAGES applied_mdl_,ENUM_APPLIED_PERCENTAGES applied_mwl_,ENUM_APPLIED_PERCENTAGES applied_gmlpo_,ENUM_APPLIED_PERCENTAGES applied_ml_,ENUM_APPLIED_PERCENTAGES applied_mdp_)
 {
  this.gmlpo.percentage_applied_to = applied_gmlpo_;
  this.mdl.percentage_applied_to  = applied_mdl_;
  this.mdp.percentage_applied_to  = applied_mdp_;
  this.mwl.percentage_applied_to  = applied_mwl_;
  this.ml.percentage_applied_to  = applied_ml_;
 }

5.止损设置函数

为了设置止损,我们将使用两种简单的方法来为止损变量赋值:

  • 方法 1:用户直接设置止损点数。
  • 方法 2:用户定义止损点与入场点之间的距离,然后将其转换为点数(使用我们在文章第一部分中编写的函数)。 

  //--- Function to set the "StopLoss" variable, in points or distance
  inline void        SetStopLoss(double dist_open_sl) {     this.StopLoss = DistanceToPoint(dist_open_sl);  }
  inline void        SetStopLoss(long _sl_point_)     {     this.StopLoss = _sl_point_;                     }



损益赋值方法

在本节中,我们将开发在风险管理系统中为损益赋值所需的方法。

1.通用赋值函数

为了确保灵活和适应性的风险管理,我们将创建一个通用函数,根据应用于各种账户指标的百分比计算适当的值。

函数定义:

我们将使用的主要函数如下:

  //--- General function to assign values ​​to loss variables
  double             GetValorWithApplied(const ENUM_APPLIED_PERCENTAGES applied,const double percentage_);

该函数将接受两个关键参数:

  1. applied - 定义百分比将应用于哪个账户指标(余额、可用保证金、净值等)。
  2. percentage_ - 要应用于所选指标的百分比。
根据这些值,该函数将计算相应的金额。
double CRiskManagemet::GetValorWithApplied(const ENUM_APPLIED_PERCENTAGES applied,const double percentage_)
 {
  if(this.mode_risk_managemet == propfirm_ftmo && percentage_ != this.mdp.assigned_percentage && percentage_ != this.gmlpo.assigned_percentage)
    return this.account_balance_propfirm * (percentage_/100.0);
  switch(applied)
   {
    case Balance:
      return NormalizeDouble((percentage_/100.0) * AccountInfoDouble(ACCOUNT_BALANCE),2);
    case ganancianeta:
     {
      if(this.account_profit <= 0)
       {
        PrintFormat("The total profit of the account which is %+.2f is invalid or negative",this.account_profit);
        return 0;
       }
      else
        return NormalizeDouble((percentage_/100.0) * this.account_profit,2);
     }
    case free_margin:
     {
      if(AccountInfoDouble(ACCOUNT_MARGIN_FREE) <= 0)
       {
        PrintFormat("free margin of %+.2f is invalid",AccountInfoDouble(ACCOUNT_MARGIN_FREE));
        return 0;
       }
      else
        return NormalizeDouble((percentage_/100.0) * AccountInfoDouble(ACCOUNT_MARGIN_FREE),2);
     }
    case equity:
      return NormalizeDouble((percentage_/100.0) * AccountInfoDouble(ACCOUNT_EQUITY),2);
    default:
      Print("Critical Error | It was not found that: ", EnumToString(applied), " be part of the allowed enumeration");
   }
  return 0;
 }
代码解释
    • 自营公司账户:
      • 如果账户属于自营公司(例如 FTMO),并且所应用的百分比不是指每日最大损失 (mdp.assigned_percentage) 或每次交易的最大损失 (gmlpo.assigned_percentage),则计算基于自营公司的账户余额。
    • 基于多种账户指标的计算:
      • Balance - 占账户总余额的百分比。
      • ganancianeta - 应用于账户净利润的百分比。如果利润为负数或零,则会显示错误信息。
      • free_margin - 应用于可用保证金的百分比。如果可用保证金为负数,则该函数返回 0。
      • equity - 应用于账户当前权益的百分比。
    • 错误处理:
      • 如果向应用的函数传递无效指标,该函数将打印错误消息并返回 0。

    2.创建用于配置损益变量的函数

    为了有效地管理损益,我们将创建一组函数,使我们能够以结构化的方式分配值。我们将总共定义六个函数:五个专门用于配置主要亏损参数,一个用于根据止损计算理想的手数大小。

    1.赋值函数

    盈亏值的确定方法基于以下逻辑:

    1. 如果计算方式为金额,则保留先前赋值。
    2. 如果计算不是以金额模式进行,则使用 GetValorWithApplied 函数根据指定的百分比和相应的应用程序参数来确定值。
    3. 如果指定的百分比为 0,则亏损被视为未使用,因此,其值被赋予 0。

    2.赋值函数

    以下函数针对每种亏损类型实现了上述逻辑:

      //--- Functions to assign values ​​to internal variables
      void               SetMDL()   {this.mdl.value = this.mdl.mode_calculation_risk == money ? this.mdl.value           : (this.mdl.assigned_percentage > 0 ? GetValorWithApplied(this.mdl.percentage_applied_to,mdl.assigned_percentage) : 0);          }
      void               SetMWL()   {this.mwl.value  = this.mwl.mode_calculation_risk  == money ? this.mwl.value         : (this.mwl.assigned_percentage > 0 ? GetValorWithApplied(this.mwl.percentage_applied_to,mwl.assigned_percentage) : 0);          }
      void               SetML()    {this.ml.value  = this.ml.mode_calculation_risk == money ? this.ml.value             : (this.ml.assigned_percentage > 0 ? GetValorWithApplied(this.ml.percentage_applied_to,ml.assigned_percentage): 0);              }
      void               SetGMLPO() {this.gmlpo.value  = this.gmlpo.mode_calculation_risk  == money ? this.gmlpo.value   : (this.gmlpo.assigned_percentage  > 0 ? GetValorWithApplied(this.gmlpo.percentage_applied_to,gmlpo.assigned_percentage) : 0);  }
      void               SetMDP()   {this.mdp.value  = this.mdp.mode_calculation_risk == money ? this.mdp.value          : (this.mdp.assigned_percentage > 0 ? GetValorWithApplied(this.mdp.percentage_applied_to,mdp.assigned_percentage) : 0);          }
      void               SetNMPLO(double& TLB_new, double tlb) { GetIdealLot(TLB_new,tlb,this.gmlpo.value,this.nmlpo,this.StopLoss); }


    根据单笔交易风险计算手数和止损位

    在本节中,我们将实现两个基本函数,以准确计算手数和止损,确保每笔交易都符合我们愿意承担的风险水平。

    1.计算手数大小的函数

    为了计算手数大小,我们将使用第一篇文章中开发的函数,以确保手数符合我们预定义的风险规则。

    用于获取合适手数大小的函数是:

      //--- Get the lot
      double             GetLote(const ENUM_ORDER_TYPE order_type, const ENUM_GET_LOT mode_get_lot);
    其参数:
    1. order_type - 指定订单类型(买入、卖出、止损、限价、止损限价等)。
    2. mode_get_lot - 指示手数计算方法(在前面的枚举部分中已解释)。

    该函数的目的

    该函数使用两种不同的方法帮助计算理想手数大小:

    • 根据止损位和每笔交易的最大风险,
    • 直接基于每笔交易的最大风险。

    现在,让我们看看代码中是如何实现这个计算的。

    //+-----------------------------------------------------------------------------------------------+
    //| Function to obtain the ideal lot based on the maximum loss per operation and the stop loss    |
    //+-----------------------------------------------------------------------------------------------+
    double CRiskManagemet::GetLote(const ENUM_ORDER_TYPE order_type, const ENUM_GET_LOT mode_get_lot)
     {
      if(mode_get_lot == GET_LOT_BY_STOPLOSS_AND_RISK_PER_OPERATION)
       {
        double MaxLote = GetMaxLote(order_type);
        SetNMPLO(this.lote,MaxLote);
        PrintFormat("Maximum loss in case the next operation fails %.2f ", this.nmlpo);
       }
      else
       {
        this.lote = GetLotByRiskPerOperation(this.gmlpo.value,order_type);
       }
      return this.lote;
     }

    模式 1.基于止损和最大单笔交易风险的交易手数计算

    当用户选择 GET_LOT_BY_STOPLOSS_AND_RISK_PER_OPERATION 时,系统会根据每笔交易的风险计算手数,然后根据止损进行调整,以确保如果交易导致亏损,实际亏损与定义的风险相符。

    1. 使用 GetMaxLote(order_type) 函数获取允许的最大手数。
    2. 使用 SetNMPLO(this.lot, MaxLot) 调整手数大小,以防止超出账户限制。
    3. 屏幕上会打印一条信息,指出如果交易达到止损位,预计的最大亏损是多少。

    模式 2.基于单笔交易最大风险的交易手数计算

    当用户选择不同的计算模式(例如,GET_LOT_BY_RISK_PER_OPERATION)时,系统会以更直接的方式调整手数大小:

    1. GetLotByRiskPerOperation(this.gmlpo.value, order_type) 函数根据我们愿意承担的最大交易风险来确定手数大小。
    2. 这种方法更简单,对于不依赖固定止损,而是更动态地调整风险的交易者来说可能很有用。

    2.计算止损的函数

    由于我们在前面的章节中已经开发了辅助函数,因此该函数将相对简单,因为我们将重用这些函数来有效地计算止损。

    帮助我们获得理想止损位的函数是:

    long GetSL(const ENUM_ORDER_TYPE type, double DEVIATION = 100, double STOP_LIMIT = 50);

    其参数:
    1. type - 指定订单类型:买入、卖出、止损、限价、止损限价等。
    2. DEVIATION(可选 - 表示允许的执行偏差,默认值为 100 个点)。
    3. STOP_LIMIT(可选)- 表示 STOP_LIMIT 类型订单的点数距离。

    这些参数确保止损计算是动态的,能够适应不同的市场状况。

    函数实现:

    //+----------------------------------------------------------------------------------+
    //| Get the ideal stop loss based on a specified lot and the maximum loss per trade  |
    //+----------------------------------------------------------------------------------+
    long CRiskManagemet::GetSL(const ENUM_ORDER_TYPE type, double DEVIATION = 100, double STOP_LIMIT = 50)
     {
      double lot;
      return CalculateSL(type,this.gmlpo.value,lot,DEVIATION,STOP_LIMIT);
     }

    • 该函数的工作原理

    1. 它定义了变量 lot,该变量将存储止损计算得出的交易量。
    2. 它调用 CalculateSL() 函数,并传递以下参数:
      • 订单类型(type),以便正确计算买入或卖出的止损价;
      • 最大单笔交易风险(this.gmlpo.value),以便止损与风险管理规则一致;
      • lot - 将根据所使用的手数大小进行更新;
      • DEVIATION - 允许的偏差,为订单执行提供一定的灵活性;
      • STOP_LIMIT - 表示 STOP_LIMIT 类型订单的点数距离。
    3. 最后,该函数以点数为单位返回止损值。


    用于获取损益值的函数

    本节介绍交易系统中负责获取关键损益指标的函数。这些函数被声明为内联函数和常量函数,以确保效率并防止对类变量进行意外修改。

    1.获取最大亏损和利润的函数


    以下函数返回存储在变量中的值,这些变量跟踪不同时期达到的最大利润和亏损:

      inline double      GetML()    const { return this.ml.value;    }
      inline double      GetMWL()   const { return this.mwl.value;   }
      inline double      GetMDL()   const { return this.mdl.value;   }
      inline double      GetGMLPO() const { return this.gmlpo.value; }
      inline double      GetNMLPO() const { return this.nmlpo; }
      inline double      GetMDP()   const { return this.mdp.value;   }

    每个函数都提供不同分析层次上与交易绩效相关的数据。

    2.获取每日、每周和总利润的函数


    这些函数可以返回不同时间段的利润,从而可以对系统进行绩效评估。

      //--- Obtain only profits:
      inline double      GetGrossProfit()  const { return this.gross_profit;   }
      inline double      GetWeeklyProfit() const { return this.weekly_profit;  }
      inline double      GetDailyProfit()  const { return this.daily_profit;   }

    3.根据幻数和标志位平仓的函数


    此函数旨在平掉所有符合特定条件的未平仓位。它基于一个标志系统,允许指定要平仓的交易:

    • 只在获利的,
    • 只在亏损的,
    • 两种类型(所有交易)。

    void CloseAllPositions(int flags = FLAG_CLOSE_ALL_LOSS | FLAG_CLOSE_ALL_PROFIT);

    • flags - 一个整数,表示平仓的条件。可以使用按位或运算符(|)组合多个标志。
    1.标志说明
    #define FLAG_CLOSE_ALL_PROFIT 2 //Flag indicating to close only operations with profit
    #define FLAG_CLOSE_ALL_LOSS   4 //Flag indicating to close only operations without profit

    这些标志被定义为 2 的幂。

    每个标志位代表一个值,该值可以单独激活,也可以组合激活:

     标志   数值(十进制) 值(二进制)
    用途
    FLAG_CLOSE_ALL_PROFIT   2  00000010
    只平掉盈利的交易。
    FLAG_CLOSE_ALL_LOSS     4  00000100 只平掉亏损的交易。

    2.平仓函数

    该函数遍历所有未平仓位,并根据激活标志将其平仓。

    3.遍历所有未平仓位

    for(int i = PositionsTotal() - 1; i >= 0; i--)

    • PositionsTotal() 返回未平仓头寸的总数。
    • 该循环按相反的顺序(从最后一个到第一个)遍历仓位,以避免实时平仓时出错。

    4.获取位置标识符(单号)

    ulong position_ticket = PositionGetTicket(i);

    • 每个仓位都有一个唯一的单号标识。

    5.仓位选择

    if (!PositionSelectByTicket(position_ticket))
    
        continue;

    • PositionSelectByTicket(position_ticket) 尝试根据收到的单号选择仓位。
    • 如果选择失败,“继续”移动到下一个仓位。

    6.检查幻数

    ulong magic = PositionGetInteger(POSITION_MAGIC);
    
    if (magic != this.magic_number && this.magic_number != NOT_MAGIC_NUMBER)
    
        continue;

    • 获取仓位的幻数。
    • 如果仓位的幻数与预期的幻数不同,并且不允许对任何订单进行操作(NOT_MAGIC_NUMBER),则忽略该仓位。

    7.获取持仓利润

    double profit = PositionGetDouble(POSITION_PROFIT);

    • POSITION_PROFIT 返回当前交易的利润。该值可以是正数(盈利)或负数(亏损)。

    8.检查标志并平仓

    if ((flags & FLAG_CLOSE_ALL_PROFIT) != 0 && profit > 0) {  
    
        trade.PositionClose(position_ticket);  
    }  
    
    else if ((flags & FLAG_CLOSE_ALL_LOSS) != 0 && profit < 0) {  
    
        trade.PositionClose(position_ticket);  
    } 

    • & 运算符用于检查某个标志是否已启用。
    • 如果 FLAG_CLOSE_ALL_PROFIT 处于激活状态且仓位盈利,则平仓。
    • 如果 FLAG_CLOSE_ALL_LOSS 处于激活状态且仓位正在亏损,则平仓。
    9.完整代码
    void CRiskManagemet::CloseAllPositions(int flags = FLAG_CLOSE_ALL_LOSS | FLAG_CLOSE_ALL_PROFIT)
     {
      for(int i = PositionsTotal() - 1; i >= 0; i--)
       {
        ulong position_ticket = PositionGetTicket(i);
        if(!PositionSelectByTicket(position_ticket))
          continue; // If you don't select the position, continue
        double profit = PositionGetDouble(POSITION_PROFIT);
        // Check flags before closing the position
        if((flags & FLAG_CLOSE_ALL_PROFIT) != 0 && profit > 0) // Close only profit positions
         {
          trade.PositionClose(position_ticket);
         }
        else
          if((flags & FLAG_CLOSE_ALL_LOSS) != 0 && profit < 0) // Close only losing positions
           {
            trade.PositionClose(position_ticket);
           }
       }
     }
    


    新一天和新一周的规划事件

    为了结束本文,我们将实现在特定事件期间执行的最终函数。具体来说,我们将创建两个关键函数:

    1. 每日事件 - 在每个新一天开始时执行。
    2. 每周事件 - 在每周开始时执行。

    这些事件有助于以结构化的方式管理损失和利润,确保价值得到正确更新。

    每日事件

    每天,都有必要重新计算和设置所有最大损益的值,并将累计的每日利润重置为 0。我们还将把损益值打印到日志中以进行监控。
    //+------------------------------------------------------------------+
    //| Function that runs every new day                                 |
    //+------------------------------------------------------------------+
    void CRiskManagemet::OnNewDay(void)
     {
      SetMWL();
      SetMDL();
      SetML();
      SetMDP();
      SetGMLPO();
      this.daily_profit = 0;
      this.last_day_time = iTime(_Symbol,PERIOD_D1,0);
      Print(" New day ");
      Print(StringFormat("%-6s| %s", "Losses", "Loss"));
      Print(StringFormat("%-6s| %.2f", "MDP", this.mdp.value));
      Print(StringFormat("%-6s| %.2f", "MWL", this.mwl.value));
      Print(StringFormat("%-6s| %.2f", "ML", this.ml.value));
      Print(StringFormat("%-6s| %.2f", "MDl", this.mdl.value));
      Print(StringFormat("%-6s| %.2f", "GMLPO", this.gmlpo.value));
     }

    • 调用设置损益值所需的所有函数。
    • 每日利润变量将被重置为 0,以开始新的一天,不继承之前的盈亏。
    • 使用 iTime(_Symbol, PERIOD_D1, 0) 存储最后记录日期的时间戳。
    • 亏损和盈利数值会打印到日志中,以监控系统行为。

    每周事件

    每周事件负责记录新一周的开始,并将上周累计利润重置为 0。
    //+------------------------------------------------------------------+
    //| Function that runs every new week                                |
    //+------------------------------------------------------------------+
    void CRiskManagemet::OnNewWeek(void)
     {
      this.last_weekly_time = iTime(_Symbol,PERIOD_W1,0);
      this.weekly_profit = 0;
     }

    • last_weekly_time 变量会更新为新一周的开始时间。
    • 为防止累积上周的盈亏,将 weekly_profit 变量重置为 0。

    检测新事件(日/周/自定义周期)

    为了在 EA 交易中自动执行这些函数,我们可以使用日期时间变量检查时间段是否发生了变化。这样我们就可以检测新的一天、一周或任何其他时间段(H1、H12 等)。

    新日期检测示例:

    datetime prev_time = 0;
    
    void OnTick()
    {
      if(prev_time != iTime(_Symbol, PERIOD_D1, 0))
      {
        Print("New day detected");
        prev_time = iTime(_Symbol, PERIOD_D1, 0);
        OnNewDay(); // Call the corresponding function
      }
    }
    

    新一周检测示例:

    datetime prev_week_time = 0;
    
    void OnTick()
    {
      if(prev_week_time != iTime(_Symbol, PERIOD_W1, 0))
      {
        Print("New week detected")
       //Call the corresponding function
        prev_week_time = iTime(_Symbol, PERIOD_W1, 0);
      }
    }

    解释:

    • 定义了一个日期时间变量来存储之前记录的时间。
    • 在每个分时报价(OnTick)上,将当前时间与存储在 prev_time 或 prev_week_time 中的值进行比较。
    • 如果该值发生了变化,则表示新的一天或新的一周已经开始,此时将调用 OnNewDay() 或 OnNewWeek() 函数。
    • prev_time 或 prev_week_time 变量将更新为新值。


    结论

    在本文中,我们开发了 CRiskManagement 风险管理类的第一部分。在这个初始阶段,我们的主要重点是为最大的亏损和利润赋值,从而为结构化风险控制奠定基础。

    虽然该类尚未准备好在实时交易环境中使用,但我们已经可以看到这些值在系统中是如何分配和管理的。

    在下一篇文章中,我们将通过添加关键函数来完成这个类的实现,这些函数将允许我们:

    • 检查是否已超过最大亏损限额,并执行相应的措施;
    • 增加新的事件以进一步改进风险管理;
    • 创建特定函数,例如自动更新自营公司类型账户(如 FTMO)的最大每日损失。

    通过这些改进,CRiskManagement 类将能够有效地管理风险限额并适应不同的市场条件。

    本文中使用/改进的文件:

    文件名称 类型 描述 
     Risk_Management.mqh   .mqh(包含文件) 包含常用函数和 CRiskManagement 类实现的主文件,该类负责系统中的风险管理。本文件定义、开发和扩展了与损益管理相关的所有函数。


    本文由MetaQuotes Ltd译自西班牙语
    原文地址: https://www.mql5.com/es/articles/17249

    附加的文件 |
    最近评论 | 前往讨论 (1)
    Pedro-2006
    Pedro-2006 | 27 1月 2026 在 17:10
    我想学习如何交易
    您应当知道的 MQL5 向导技术(第 59 部分):配以移动平均和随机振荡器形态的强化学习(DDPG) 您应当知道的 MQL5 向导技术(第 59 部分):配以移动平均和随机振荡器形态的强化学习(DDPG)
    我们继续上一篇文章中有关配以 MA 和随机振荡器指标的 DDPG 话题,探讨实现 DDPG 时其他关键的强化学习类。尽管我们大多用 Python 编码,但最终产品是把训练好的网络导出为 ONNX 格式,我们会将它集成到由向导汇编的 MQL5 智能系统中作为资源。
    克服机器学习的局限性(第二部分):缺乏可重复性 克服机器学习的局限性(第二部分):缺乏可重复性
    本文探讨了即便使用相同的策略和金融标的,不同经纪商的交易结果为何仍会存在显著差异,原因在于定价的分散化以及数据差异。本文有助于MQL5开发者理解为何他们的产品在MQL5市场上的评价褒贬不一,并敦促开发者针对特定经纪商调整方法,以确保结果透明且可重复。如果这一做法能被广泛地采用,将有望成为我们社区重要的特定领域最佳实践。
    市场模拟(第 10 部分):套接字(四) 市场模拟(第 10 部分):套接字(四)
    在这篇文章中,我们将以一种非常有趣的方式,看看你需要做什么才能开始使用 Excel 来管理 MetaTrader 5。为此,我们将使用 Excel 加载项来避免使用内置的 VBA。如果您不知道什么是加载项,请阅读本文,学习如何直接在 Excel 中使用 Python 进行编程。
    使用MQL5经济日历进行交易(第十部分):可拖动仪表盘与交互式悬停效果,实现流畅的新闻导航 使用MQL5经济日历进行交易(第十部分):可拖动仪表盘与交互式悬停效果,实现流畅的新闻导航
    在本文中,我们对MQL5经济日历进行了功能增强,引入了可拖动仪表盘,使用户能够重新定位界面,以获得更好的图表可视性。我们为按钮实现了悬停效果,以提高交互性,并确保通过动态定位的滚动条实现流畅的导航。