风险管理(第三部分):构建风险管理主类·进阶篇
🛡️

风险管理(第三部分):构建风险管理主类·进阶篇

(2/3)· 从界面到内核,CRiskManagement 类如何把亏损利润赋值与手数计算收拢成可复用结构

含代码示例偏理论 第 2/3 篇
不少人在 MQL5 里把风控变量散落在 EA 主文件,结果新一周重置漏写、单笔风险算错却查不到源头。把风控逻辑塞进一个类之前,先想清楚结构和枚举怎么定,否则后期重构成本比从头写还高。

「类的构造、析构与参数注入」

把风险管理逻辑封装成类,第一道关就是构造时把状态立住。构造函数吃三个参数:魔术号、风控模式、以及自营账户余额(个人户可填0)。若魔术号留空,终端会打印一条警告,提示后续统计将纳入全部魔术号下的历史单——这点对多策略同跑的账户尤其要留心,可能把不相关的浮盈浮亏也算进净值基线。 构造体里直接 new 一个 CTrade 实例,并调用 GetNetProfitSince 拉取从 1972.01.01 起的账户净利润,相当于把‘远古净值’当锚点。同时用 iTime 抓日线和周线最新 K 线时间,init_time 在有魔术号时取当前时间,否则也回退到 1972 年。析构函数只做一件事:delete trade,避免 CTrade 对象残留在内存。 参数注入拆成三组 setter:SetPorcentages 灌亏损结构的百分比或金额,SetEnums 指定每个结构是‘按比例’还是‘按金额’算,SetApplieds 决定百分比挂在账户哪一层。实际写 EA 时,建议在 OnInit 里依次调这三个方法,否则结构里的 assigned_percentage 全是 0,风控计算会直接失效。外汇与贵金属杠杆高,这类自写风控若参数错配,可能放大回撤。

MQL5 / C++
CRiskManagemet(class="type">ulong magic_number_ = NOT_MAGIC_NUMBER,ENUM_MODE_RISK_MANAGEMENT mode_risk_management_ = personal_account, class="type">class="kw">double account_propfirm_balance=class="num">0);
CRiskManagemet::CRiskManagemet(class="type">ulong magic_number_ =  NOT_MAGIC_NUMBER,ENUM_MODE_RISK_MANAGEMENT mode_risk_management_ = personal_account, class="type">class="kw">double account_propfirm_balance =class="num">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&class="macro">#x27;s trades");
   }
class=class="str">"cmt">//---
   this.account_balance_propfirm = account_propfirm_balance ;
   trade = new CTrade();
   this.account_profit = GetNetProfitSince(true,this.magic_number,D&class="macro">#x27;class="num">1972.01.class="num">01 class="num">00:class="num">00&class="macro">#x27;);
   this.magic_number = magic_number_;
   this.mode_risk_managemet = mode_risk_management_;
class=class="str">"cmt">//---
   this.last_day_time = iTime(_Symbol,PERIOD_D1,class="num">0);
   this.last_weekly_time = iTime(_Symbol,PERIOD_W1,class="num">0);
   this.init_time =magic_number_ != NOT_MAGIC_NUMBER ? TimeCurrent() : D&class="macro">#x27;class="num">1972.01.class="num">01 class="num">00:class="num">00&class="macro">#x27;;
}
CRiskManagemet::~CRiskManagemet()
{
   class="kw">delete trade;
}
  class=class="str">"cmt">//--- Functions to assign values ​​to variables for subsequent calculation of losses
  class="type">void                SetPorcentages(class="type">class="kw">double percentage_or_money_mdl, class="type">class="kw">double percentage_or_money_mwl,class="type">class="kw">double percentage_or_money_gmlpo, class="type">class="kw">double percentage_or_money_ml, class="type">class="kw">double percentage_or_money_mdp_);  
  class="type">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_);  
  class="type">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_);
class="type">void CRiskManagemet::SetPorcentages(class="type">class="kw">double percentage_or_money_mdl,class="type">class="kw">double percentage_or_money_mwl,class="type">class="kw">double percentage_or_money_gmlpo,class="type">class="kw">double percentage_or_money_ml,class="type">class="kw">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;
}
class="type">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_;

风险计算模式切换时的赋值陷阱

在 EA 风控模块里,money 与 percentage 两种仓位计算模式共用一套变量接口。上面这段逻辑只在模式等于 money 时保留各自 value,否则强制写 0,避免百分比分支误读金额字段。 具体看 gmlpo、mdp、mdl、ml、mwl 五个对象:每一行都用三元判断 mode_calculation_risk == money,命中才留原值,未命中归零。若你改代码时漏掉某一行,会在百分比模式下带出上一轮的金额残留值,下单手数可能偏离预期。 SetApplieds 函数则把五个百分比应用标的(如账户净值、保证金等枚举)一次性注入对应对象,和前面的 money 分支互不干扰。 StopLoss 的两个重载值得注意:传 double 会走 DistanceToPoint 转成点,传 long 直接当点数存。外汇与贵金属杠杆高,SL 点数设错会瞬间放大回撤风险,建议开 MT5 用脚本打印两种入参的实际 StopLoss 值核对。

MQL5 / C++
class=class="str">"cmt">//-- 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 : class="num">0;
  this.mdp.value  = this.mdp.mode_calculation_risk == money ? this.mdp.value : class="num">0;
  this.mdl.value  = this.mdl.mode_calculation_risk == money ? this.mdl.value : class="num">0;
  this.ml.value   = this.ml.mode_calculation_risk == money ? this.ml.value : class="num">0;
  this.mwl.value  = this.mwl.mode_calculation_risk == money ? this.mwl.value : class="num">0;
}
class="type">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_;
}
  class=class="str">"cmt">//--- Function to set the "StopLoss" variable, in points or distance
  class="kw">inline class="type">void        SetStopLoss(class="type">class="kw">double dist_open_sl) {   this.StopLoss = DistanceToPoint(dist_open_sl);  }
  class="kw">inline class="type">void        SetStopLoss(class="type">long _sl_point_)      {   this.StopLoss = _sl_point_;                }

◍ 按账户指标百分比折算损益金额

做风控系统最头疼的是损益口径不统一:有人看余额,有人看净值,自营账户又另算一套。下面这个函数把「百分比 + 应用标的」直接映射成金额,MT5 里挂 EA 后改个枚举就能切换口径。 自营账户(如 FTMO)有个坑:只要传进来的百分比既不是每日最大亏损(mdp.assigned_percentage)也不是单笔最大亏损(gmlpo.assigned_percentage),金额就按自营方给的账户余额算,而不是终端里的 ACCOUNT_BALANCE。这点不分开,回测和实盘会差出一倍权益。 普通账户走 switch 分支:Balance 取总余额百分比;ganancianeta 取净利润百分比,利润≤0 直接 PrintFormat 报错并返回 0;free_margin 取可用保证金百分比,保证金≤0 返回 0 防穿仓;equity 取当前权益百分比。所有结果用 NormalizeDouble(...,2) 保留两位小数。传了非法枚举则打印错误并返回 0。 别把正态当圣经 净利润为负时函数直接归零而不是给负值限额,这意味着浮亏阶段系统会「跳过」该维度风控。真要管回撤,得在调用层补一道权益曲线判断,别只依赖这一个分支。

MQL5 / C++
  class=class="str">"cmt">//--- General function to assign values ​​to loss variables
  class="type">class="kw">double                GetValorWithApplied(const ENUM_APPLIED_PERCENTAGES applied,const class="type">class="kw">double percentage_);
class="type">class="kw">double CRiskManagemet::GetValorWithApplied(const ENUM_APPLIED_PERCENTAGES applied,const class="type">class="kw">double percentage_)
{
  if(this.mode_risk_managemet == propfirm_ftmo && percentage_ != this.mdp.assigned_percentage && percentage_ != this.gmlpo.assigned_percentage)
    class="kw">return this.account_balance_propfirm * (percentage_/class="num">100.0);
  class="kw">switch(applied)
   {
    case Balance:
      class="kw">return NormalizeDouble((percentage_/class="num">100.0) * AccountInfoDouble(ACCOUNT_BALANCE),class="num">2);
    case ganancianeta:
     {
      if(this.account_profit <= class="num">0)
       {
        PrintFormat("The total profit of the account which is %+.2f is invalid or negative",this.account_profit);
        class="kw">return class="num">0;
       }
      else
        class="kw">return NormalizeDouble((percentage_/class="num">100.0) * this.account_profit,class="num">2);
     }
    case free_margin:
     {
      if(AccountInfoDouble(ACCOUNT_MARGIN_FREE) <= class="num">0)
       {
        PrintFormat("free margin of %+.2f is invalid",AccountInfoDouble(ACCOUNT_MARGIN_FREE));
        class="kw">return class="num">0;
       }
      else
        class="kw">return NormalizeDouble((percentage_/class="num">100.0) * AccountInfoDouble(ACCOUNT_MARGIN_FREE),class="num">2);
     }
    case equity:
      class="kw">return NormalizeDouble((percentage_/class="num">100.0) * AccountInfoDouble(ACCOUNT_EQUITY),class="num">2);
    class="kw">default:
      Print("Critical Error | It was not found that: ", EnumToString(applied), " be part of the allowed enumeration");
   }
  class="kw">return class="num">0;
}
  class=class="str">"cmt">//--- Functions to assign values ​​to internal variables

「仓位参数按风险模式分流赋值」

这段类方法集中处理了六种仓位相关变量的取值逻辑:最大持仓手数(mdl)、最大挂单手数(mwl)、马丁手数(ml)、全局马丁挂单(mglpo)、最大加仓间距手数(mdp)以及新市价挂单(nmplo)。前五个方法结构完全一致,都在 money 模式时直接沿用已设 value,否则看 assigned_percentage 是否大于 0——大于 0 就调 GetValorWithApplied 按百分比折算,否则归 0。 SetNMPLO 略有不同,它接收 TLB_new 与 tlb 两个引用/值参数,内部调用 GetIdealLot 并传入 gmlpo.value、nmlpo 对象与 StopLoss,用来在动态仓位模型里推算下一笔市价挂单的理想手数。

MQL5 / C++
class="type">void SetMDL() {this.mdl.value = this.mdl.mode_calculation_risk == money ? this.mdl.value : (this.mdl.assigned_percentage > class="num">0 ? GetValorWithApplied(this.mdl.percentage_applied_to,mdl.assigned_percentage) : class="num">0);}
class="type">void SetMWL() {this.mwl.value = this.mwl.mode_calculation_risk == money ? this.mwl.value : (this.mwl.assigned_percentage > class="num">0 ? GetValorWithApplied(this.mwl.percentage_applied_to,mwl.assigned_percentage) : class="num">0);}
class="type">void SetML() {this.ml.value = this.ml.mode_calculation_risk == money ? this.ml.value : (this.ml.assigned_percentage > class="num">0 ? GetValorWithApplied(this.ml.percentage_applied_to,ml.assigned_percentage): class="num">0);}
class="type">void SetGMLPO() {this.gmlpo.value = this.gmlpo.mode_calculation_risk == money ? this.gmlpo.value : (this.gmlpo.assigned_percentage > class="num">0 ? GetValorWithApplied(this.gmlpo.percentage_applied_to,gmlpo.assigned_percentage) : class="num">0);}
class="type">void SetMDP() {this.mdp.value = this.mdp.mode_calculation_risk == money ? this.mdp.value : (this.mdp.assigned_percentage > class="num">0 ? GetValorWithApplied(this.mdp.percentage_applied_to,mdp.assigned_percentage) : class="num">0);}
class="type">void SetNMPLO(class="type">class="kw">double& TLB_new, class="type">class="kw">double tlb) { GetIdealLot(TLB_new,tlb,this.gmlpo.value,this.nmlpo,this.StopLoss); }
逐行拆解: 第1行 SetMDL — 若风险计算模式为 money,最大持仓手数保持原值;否则当 assigned_percentage>0 时按所应用基数折算,否则置 0。 第2行 SetMWL — 同理处理最大挂单手数 mwl。 第3行 SetML — 同理处理马丁手数 ml。 第4行 SetGMLPO — 同理处理全局马丁挂单手数 gmlpo。 第5行 SetMDP — 同理处理最大加仓间距手数 mdp。 第6行 SetNMPLO — 通过引用传回 TLB_new,用 tlb 与全局马丁挂单值、nmlpo、止损点差联合推算新挂单手数。 开 MT5 把这段代码塞进你的 EA 类里,把 mode_calculation_risk 切到 percentage 模式,观察 assigned_percentage 从 0 调到 2 时 mdl.value 是否按基数联动变化;外汇与贵金属杠杆高,仓位折算逻辑出错可能迅速放大回撤。

MQL5 / C++
class="type">void SetMDL() {this.mdl.value = this.mdl.mode_calculation_risk == money ? this.mdl.value : (this.mdl.assigned_percentage > class="num">0 ? GetValorWithApplied(this.mdl.percentage_applied_to,mdl.assigned_percentage) : class="num">0);}
class="type">void SetMWL() {this.mwl.value = this.mwl.mode_calculation_risk == money ? this.mwl.value : (this.mwl.assigned_percentage > class="num">0 ? GetValorWithApplied(this.mwl.percentage_applied_to,mwl.assigned_percentage) : class="num">0);}
class="type">void SetML() {this.ml.value = this.ml.mode_calculation_risk == money ? this.ml.value : (this.ml.assigned_percentage > class="num">0 ? GetValorWithApplied(this.ml.percentage_applied_to,ml.assigned_percentage): class="num">0);}
class="type">void SetGMLPO() {this.gmlpo.value = this.gmlpo.mode_calculation_risk == money ? this.gmlpo.value : (this.gmlpo.assigned_percentage > class="num">0 ? GetValorWithApplied(this.gmlpo.percentage_applied_to,gmlpo.assigned_percentage) : class="num">0);}
class="type">void SetMDP() {this.mdp.value = this.mdp.mode_calculation_risk == money ? this.mdp.value : (this.mdp.assigned_percentage > class="num">0 ? GetValorWithApplied(this.mdp.percentage_applied_to,mdp.assigned_percentage) : class="num">0);}
class="type">void SetNMPLO(class="type">class="kw">double& TLB_new, class="type">class="kw">double tlb) { GetIdealLot(TLB_new,tlb,this.gmlpo.value,this.nmlpo,this.StopLoss); }

把单笔风险翻译成手数和止损

风险管理落到执行层,核心就是两个函数:一个算手数,一个算止损。两者都吃订单类型与风险上限,保证任何一笔单子亏损触止损时,实际回撤不超出你设的阈值。外汇与贵金属杠杆高,错把手数算大一级,爆仓概率会显著跳升。 手数计算分两条路径。选 GET_LOT_BY_STOPLOSS_AND_RISK_PER_OPERATION 时,先抓账户允许最大手数 GetMaxLote,再用 SetNMPLO 夹紧到上限内,并打印预计最大亏损 this.nmlpo;选另一模式则直接 GetLotByRiskPerOperation 按每笔风险给手数,不依赖固定止损,适合动态调仓的人。 止损函数反而轻——它复用前面写好的 CalculateSL,传入订单类型、最大风险值、手数引用、偏差 100 点、STOP_LIMIT 50 点,返回点数止损。偏差和距离参数让止损能随流动性和单类订单微调。 代码里 GetLote 的两种分支和 GetSL 的透传写法,直接拷进你的 CRiskManagemet 类就能编译。开 MT5 把 this.gmlpo.value 设成账户净值 1%,跑一遍 PrintFormat 看 nmlpo 输出,就能验证手数有没有被夹紧。

MQL5 / C++
  class=class="str">"cmt">//--- Get the lot
  class="type">class="kw">double           GetLote(const ENUM_ORDER_TYPE order_type, const ENUM_GET_LOT mode_get_lot);
class=class="str">"cmt">//+-----------------------------------------------------------------------------------------------+
class=class="str">"cmt">//| Function to obtain the ideal lot based on the maximum loss per operation and the stop loss      |
class=class="str">"cmt">//+-----------------------------------------------------------------------------------------------+
class="type">class="kw">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)
   {
    class="type">class="kw">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);
   }
  class="kw">return this.lote;
 }
class="type">long GetSL(const ENUM_ORDER_TYPE type, class="type">class="kw">double DEVIATION = class="num">100, class="type">class="kw">double STOP_LIMIT = class="num">50);
class=class="str">"cmt">//+----------------------------------------------------------------------------------+
class=class="str">"cmt">//| Get the ideal stop loss based on a specified lot and the maximum loss per trade   |
class=class="str">"cmt">//+----------------------------------------------------------------------------------+
class="type">long CRiskManagemet::GetSL(const ENUM_ORDER_TYPE type, class="type">class="kw">double DEVIATION = class="num">100, class="type">class="kw">double STOP_LIMIT = class="num">50)
 {
  class="type">class="kw">double lot;
  class="kw">return CalculateSL(type,this.gmlpo.value,lot,DEVIATION,STOP_LIMIT);
 }
交给小布盯盘看盘口
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到账户级风险暴露与日周盈亏边界,你写类时对照真实盘口数据调参更稳。

常见问题

它作为构造函数初始化值,表示不限定特定幻数,风控覆盖账户全部交易,适合全账户层级的风险管理而非单策略隔离。
目前小布盯盘展示的是账户实际风险暴露与盈亏边界,不会解析你的私有 mqh 文件;但你可参照其界面数值回校准类内常量,减少实盘偏差。
标志位组合可动态控制平盈、平亏或全平,减少函数数量、提升代码复用率,也降低后续维护时漏改某一函数的概率。
实盘仓位随净值波动时倾向用百分比以锁定风险比例;定额适合资金规模固定且想绝对控制最大回撤的场景,两者可在初始化时切换。
累计亏损利润变量不会重置,可能让当日风控阈值失效,系统误判可用风险额度,进而开出超规划手数。