风险管理(第三部分):构建风险管理主类·进阶篇
(2/3)· 从界面到内核,CRiskManagement 类如何把亏损利润赋值与手数计算收拢成可复用结构
「类的构造、析构与参数注入」
把风险管理逻辑封装成类,第一道关就是构造时把状态立住。构造函数吃三个参数:魔术号、风控模式、以及自营账户余额(个人户可填0)。若魔术号留空,终端会打印一条警告,提示后续统计将纳入全部魔术号下的历史单——这点对多策略同跑的账户尤其要留心,可能把不相关的浮盈浮亏也算进净值基线。 构造体里直接 new 一个 CTrade 实例,并调用 GetNetProfitSince 拉取从 1972.01.01 起的账户净利润,相当于把‘远古净值’当锚点。同时用 iTime 抓日线和周线最新 K 线时间,init_time 在有魔术号时取当前时间,否则也回退到 1972 年。析构函数只做一件事:delete trade,避免 CTrade 对象残留在内存。 参数注入拆成三组 setter:SetPorcentages 灌亏损结构的百分比或金额,SetEnums 指定每个结构是‘按比例’还是‘按金额’算,SetApplieds 决定百分比挂在账户哪一层。实际写 EA 时,建议在 OnInit 里依次调这三个方法,否则结构里的 assigned_percentage 全是 0,风控计算会直接失效。外汇与贵金属杠杆高,这类自写风控若参数错配,可能放大回撤。
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 值核对。
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。 别把正态当圣经 净利润为负时函数直接归零而不是给负值限额,这意味着浮亏阶段系统会「跳过」该维度风控。真要管回撤,得在调用层补一道权益曲线判断,别只依赖这一个分支。
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,用来在动态仓位模型里推算下一笔市价挂单的理想手数。
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); }
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 输出,就能验证手数有没有被夹紧。
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); }