风险管理(第四部分):完善关键类方法(基础篇)
「给风险类补上持仓与超限处理」
在 MT5 的风险管理框架里,第四部分的重点是给关键类补齐方法,而不是另起炉灶。原文给出的落点是:新增枚举与结构、改写构造函数和主函数、加入类变量,让持仓管理和超限处理能跑在同一个对象里。
实际开 MT5 验证时,最该看的是「管理持仓」和「超限处理方法」两块。前者决定当前订单状态怎么被类内部读取,后者决定账户风险越过阈值后是平仓、减仓还是拒绝新单——这一步直接决定你贵金属仓位会不会在跳空时被强平。外汇与贵金属为高杠杆品种,参数没调好可能瞬间放大回撤。
事件处理接在持仓管理后面,用动态风险函数把仓位和净值波动绑起来。也就是说,风险不是写死的 2% 止损,而是随余额和波动重算。读者可以复制下面这段骨架,把 maxRiskPercent 从 2.0 改成 1.0 看成交行为变化。
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 写死,后面函数调用更干净。
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 同时跑的终端里快速分辨输出来源。外汇和贵金属杠杆高,这类标志位逻辑出错可能导致误平或漏平,上线前建议在策略测试器用历史数据跑一遍边界情形。
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 就能看数组怎么长出来。
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); } } }