风险管理(第四部分):完善关键类方法(基础篇)
📘

风险管理(第四部分):完善关键类方法(基础篇)

第 1/3 篇

「给风险类补上持仓与超限处理」

在 MT5 的风险管理框架里,第四部分的重点是给关键类补齐方法,而不是另起炉灶。原文给出的落点是:新增枚举与结构、改写构造函数和主函数、加入类变量,让持仓管理和超限处理能跑在同一个对象里。 实际开 MT5 验证时,最该看的是「管理持仓」和「超限处理方法」两块。前者决定当前订单状态怎么被类内部读取,后者决定账户风险越过阈值后是平仓、减仓还是拒绝新单——这一步直接决定你贵金属仓位会不会在跳空时被强平。外汇与贵金属为高杠杆品种,参数没调好可能瞬间放大回撤。 事件处理接在持仓管理后面,用动态风险函数把仓位和净值波动绑起来。也就是说,风险不是写死的 2% 止损,而是随余额和波动重算。读者可以复制下面这段骨架,把 maxRiskPercent 从 2.0 改成 1.0 看成交行为变化。

MQL5 / C++
class RiskManager {
   class="type">class="kw">double maxRiskPercent; class=class="str">"cmt">// 单笔最大风险百分比
   class="type">class="kw">double equity;         class=class="str">"cmt">// 当前净值
class="kw">public:
   RiskManager(class="type">class="kw">double risk) : maxRiskPercent(risk), equity(class="num">0) {}
   class="type">void UpdateEquity(class="type">class="kw">double eq) { equity = eq; }
   class="type">bool IsOverLimit(class="type">class="kw">double plannedRisk) {
      class="kw">return plannedRisk > equity * maxRiskPercent / class="num">100.0;
   }
};

◍ 专用类的变量与限额检查方法

在风险管理系列的前一篇基础上,这一节先把专用类里几个核心变量和初始化方法讲清楚,方便后续直接套用。重点要落地的,是判断账户是否触及预设最大亏损或最大利润限额的那组方法。 除了静态限额,本节还引入两种按单笔交易动态调节风险敞口的思路:一种随浮盈回撤缩放手数,一种按实时权益占比重算可亏额度。外汇与贵金属杠杆高,动态风控若参数过激,可能放大连亏期的回撤。 读者可在 MT5 新建 EA 后,先只挂这一节的限额检查函数,用历史品种跑一遍,确认触发平仓的逻辑与预期一致再叠加动态方法。

这一篇先动哪些底层模块

本篇的改造从最底层的函数与类生命周期入手:先重写若干工具函数的实现,并调整类的构造与析构逻辑,避免历史版本里重复分配内存的冗余开销。 随后会引入新的结构体、枚举与核心常量,把原本散落在各处的魔法数字收拢到统一命名空间,方便后续调参时一眼定位。 开仓管理函数会补上一条硬约束——当浮盈或浮亏突破预设阈值时触发检测机制,这部分直接决定 EA 是否继续加仓。 动态单笔风险的计算方法也会在本篇落地,意味着每笔订单的止损距离不再写死,而是随账户净值波动重算。 在 MT5 里打开对应 EA 的源文件,先搜构造函数里 new 的调用次数,就能验证我们说的冗余是否真实存在。

「动态风险与余额检查的数据结构怎么搭」

把单笔交易风险做成随账户余额浮动,而不是写死一个百分比,对账户保护有直接意义。以初始余额 10000 美元为例,若余额跌破 9700(降 3%)风险切到 0.7%,跌破 9500(降 5%)切到 0.5%,跌破 9300(降 7%)切到 0.25%——这些阈值和初始余额都能在 EA 输入里改,外汇和贵金属杠杆高,这种下调敞口的做法倾向降低爆仓概率,但连亏后恢复速度会偏慢。 管理这套逻辑最初用两个独立 double 数组存「触发余额」和「新风险值」,但必须排序。因为检查用无循环法:靠计数器跟踪风险档位,若阈值乱序(比如 9700、9300、9500 错排),余额从 10000 降到 9700 计数器 +1,再到 9300 又 +1,直接跳过 9500 对应的 5% 档,映射就乱了。所以要用 ArraySort() 对 balance_to_activate_the_risk[] 升序排,CHashMap 能保住键值对的映射,避免排序后张冠李戴。 余额检查时刻也分两种:REVISION_ON_TICK 每个分时都查,准但净值在 9701/9699 间晃会反复触发,吃资源;REVISION_ON_CLOSE_POSITION 只在平仓查,省资源但可能漏掉中间大波动。动态风险模式用枚举给了三条路:FULL_CUSTOM 全自定义但没法用优化器跑字符串,FIXED_PARAMETERS 限四次改动可直接填数字参数方便优化,NO_DYNAMIC_GMLPO 全关掉。 持仓跟踪上,Positions 结构只存 ticket 和 ENUM_POSITION_TYPE,靠幻数区分 EA 开的还是手动开的;超限判定枚举分 EQUITY(只看实时净值)、CLOSED_POSITION(只看已平盈亏)、CLOSED_POSITION_AND_EQUITY(两者都算)三档。前缀常量和订单类型标志用 #define 写死,后面函数调用更干净。

MQL5 / C++
class="kw">struct Dynamic_gmlpo
{
   class="type">class="kw">double balance_to_activate_the_risk[];
   class="type">class="kw">double risk_to_be_adjusted[];
};
class=class="str">"cmt">//---
enum ENUM_REVISION_TYPE
{
  REVISION_ON_CLOSE_POSITION, class=class="str">"cmt">//Check GMLPO only when closing positions
  REVISION_ON_TICK class=class="str">"cmt">//Check GMLPO on all ticks
};
class=class="str">"cmt">//---
enum ENUM_OF_DYNAMIC_MODES_OF_GMLPO
{
  DYNAMIC_GMLPO_FULL_CUSTOM, class=class="str">"cmt">//Customisable dynamic risk per operation
  DYNAMIC_GMLPO_FIXED_PARAMETERS,class=class="str">"cmt">//Risk per operation with fixed parameters
  NO_DYNAMIC_GMLPO class=class="str">"cmt">//No dynamic risk for risk per operation
};
class="kw">struct Positions
{
  class="type">ulong                ticket; class=class="str">"cmt">//position ticket
  class="type">ENUM_POSITION_TYPE type; class=class="str">"cmt">//position type
};
class=class="str">"cmt">//--- Mode to check if a maximum loss or gain has been exceeded
enum MODE_SUPERATE
{

◍ 用位标志区分持仓与订单类型

在 MT5 风控类 EA 里,经常要按持仓方向或未成交订单类型做分支处理。直接写一堆 if-else 可读性差,用宏定义把类型映射成 2 的幂次标志位是更干净的做法。 上面这段预处理指令把持仓买、卖分别定义为 2 和 4,挂单则从 BUY_LIMIT 的 4 一路排到 CLOSE_BY 的 256,每个值都是独立的二进制位。这样用按位或就能同时标记多种订单类型,用按位与做存在性判断。 EA_NAME 被定义成 "CRiksManagement | " 作为日志前缀,便于在多 EA 同时跑的终端里快速分辨输出来源。外汇和贵金属杠杆高,这类标志位逻辑出错可能导致误平或漏平,上线前建议在策略测试器用历史数据跑一遍边界情形。

MQL5 / C++
  EQUITY, class=class="str">"cmt">//Only Equity
  CLOSE_POSITION, class=class="str">"cmt">//Only for closed positions
  CLOSE_POSITION_AND_EQUITYclass=class="str">"cmt">//Closed positions and equity
};
class="macro">#define EA_NAME "CRiksManagement | " class=class="str">"cmt">//Prefix
class=class="str">"cmt">//--- positions
class="macro">#define  FLAG_POSITION_BUY class="num">2
class="macro">#define  FLAG_POSITION_SELL class="num">4
class=class="str">"cmt">//--- orders
class="macro">#define FLAG_ORDER_TYPE_BUY          class="num">1
class="macro">#define FLAG_ORDER_TYPE_SELL         class="num">2
class="macro">#define FLAG_ORDER_TYPE_BUY_LIMIT    class="num">4
class="macro">#define FLAG_ORDER_TYPE_SELL_LIMIT   class="num">8
class="macro">#define FLAG_ORDER_TYPE_BUY_STOP     class="num">16
class="macro">#define FLAG_ORDER_TYPE_SELL_STOP    class="num">32
class="macro">#define FLAG_ORDER_TYPE_BUY_STOP_LIMIT class="num">64
class="macro">#define FLAG_ORDER_TYPE_SELL_STOP_LIMIT class="num">128
class="macro">#define FLAG_ORDER_TYPE_CLOSE_BY     class="num">256

把风险类的构造与析构打磨稳

给 CRiskManagement 类动刀,最先改的是构造函数。现在把用到的手数类型(type_get_lot)和自营账户初始余额参数(account_propfirm_balance)显式写进参数表,跑 prop firm 账户时这套定义能直接派上用场;EA_NAME 也会持续打在主类生成的注释里,终端日志里一眼就能认出是哪段逻辑吐的。 构造函数里新塞了几个关键变量:curr_profit 存当前利润做持续监控;ActivateDynamicRiskPerOperation 是动态风险开关的布尔量;mdp_is_strict 控制是否严格盯 mdp;type_get_lot 记手数类别。末尾还挂了一个按幻数收未平仓位、或全收进风险管理数组的循环。 析构函数这边重点在动态内存。用 CheckPointer() 验 CTrade 指针,只在确属动态指针时才 delete,避免误释放崩 EA;数组则靠 ArrayFree() 清,内存不漏,EA 跑起来更稳。 总体加固里有一条很实用:取手数和止损前先查 gmlpo.assigned_percentage,若 ≤0 直接 Print 报错并回安全值,EA 不会因为错设风险比例直接挂掉。SetEnums() 也加了校验,必须在 OnInit() 里跑,防止固定金额模式被赋成负的或错的。 下方构造函数片段里,magic_number_ 为 NOT_MAGIC_NUMBER 时会提示未选幻数并统管全部用户单;循环从 PositionsTotal()-1 倒序遍历,按幻数匹配后塞进 open_positions,实盘开 MT5 挂个 EA 就能看数组怎么长出来。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
CRiskManagemet::CRiskManagemet(class="type">bool mdp_strict_, ENUM_GET_LOT type_get_lot_, 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(EA_NAME, " (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.mdp_is_strict = mdp_strict_;
   this.type_get_lot = type_get_lot_;
class=class="str">"cmt">//---
   this.account_balance_propfirm = account_propfirm_balance ;
   trade = new CTrade();
   trade.SetExpertMagicNumber(this.magic_number);
   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_;
   this.ActivateDynamicRiskPerOperation = false;
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;;
class=class="str">"cmt">//---
   this.positions_open = false;
   this.curr_profit = class="num">0;
   UpdateProfit();

class=class="str">"cmt">//---
    for(class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--)
    {
      class="type">ulong position_ticket = PositionGetTicket(i);
      if(!PositionSelectByTicket(position_ticket))
        class="kw">continue;

      class="type">ulong position_magic = PositionGetInteger(POSITION_MAGIC);
      class="type">ENUM_POSITION_TYPE type = (class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      if(position_magic == magic_number_ || magic_number_ == NOT_MAGIC_NUMBER)
       {
         this.positions_open = true;
         Positions new_pos;
         new_pos.type = (class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
         new_pos.ticket = position_ticket;
         ExtraFunctions::AddArrayNoVerification(open_positions, new_pos);
       }
    }  
}

常见问题

用位标志(bit flags)标记持仓与订单类型,检查时按位与运算即可分流,避免用多个布尔变量堆逻辑。
建议用轻量结构体挂到风险类内部,实时更新余额、保证金、自由保证金字段,别用全局变量散落各处。
小布可读取你品种页的持仓与限额设置,对照你的类方法提示超限处理的遗漏点,省去手动逐行核对。
构造里初始化限额与标志位、析构里释放资源,能防止重复开仓时状态污染,是后续扩展动态风控的底座。
优先看单笔上限、总持仓上限、保证金占用比三个字段,写个内部 CheckLimits() 集中判断,别散在交易函数里。