风险管理(第五部分):将风险管理系统集成到 EA 中·综合运用
(3/3)· 从指标优化到 OnTradeTransaction 落地,用四次回测看清动态风控何时真正起作用
◍ EA 初始化时把订单区块指标接进图表
MT5 的 OnInit 是 EA 启动入口,订单区块策略在这里要把指标句柄、EMA 和风控对象一次性备齐。先按指标要求的顺序塞满 MqlParam 数组,再交给 IndicatorCreate 拿句柄,少一步后面 OnTick 都跑不起来。
下面这段代码里 param 数组固定长度为 17,索引 0 是自定义指标路径 ::Indicators\Order_Block_Indicador_New_Part_2,索引 3~4 分别是订单块搜索范围和宽度(Rango_universal_busqueda / Witdth_order_block),索引 12~13 是 ATR 倍数,索引 15~16 是点数化的止盈止损。指标句柄和 30 周期 EMA 句柄任一为 INVALID_HANDLE 就直接 INIT_FAILED 退出,避免半残状态挂实盘。
调试阶段用 ChartIndicatorAdd(0,0,...) 把两个指标强丢到当前图表,肉眼核对区块画得对不对比看日志快得多;正式跑可注释掉,但初始化时 EMA 周期写死 30、偏移 0、PRICE_CLOSE 是策略默认参考轴。
风控对象 risk 在初始化末尾用 SetPorcentages 吃进三类百分比参数(MDL / MWL / GMLPO),外汇和贵金属杠杆高,仓位百分比算错可能单笔回撤失控,建议在策略测试器先用 0.1 手以下验证手数输出。
class=class="str">"cmt">//--- class="type">MqlParam param[class="num">17]; param[class="num">0].type = TYPE_STRING; param[class="num">0].string_value = "::Indicators\\Order_Block_Indicador_New_Part_2"; param[class="num">1].type = TYPE_STRING; param[class="num">1].string_value = "--- Order Block Indicator settings ---"; param[class="num">2].type = TYPE_STRING; param[class="num">2].string_value = "-- Order Block --"; param[class="num">3].type = TYPE_INT; param[class="num">3].integer_value = Rango_universal_busqueda; param[class="num">4].type = TYPE_INT; param[class="num">4].integer_value = Witdth_order_block; param[class="num">5].type = TYPE_BOOL; param[class="num">5].integer_value = Back_order_block; param[class="num">6].type = TYPE_BOOL; param[class="num">6].integer_value = Fill_order_block; param[class="num">7].type = TYPE_COLOR; param[class="num">7].integer_value = Color_Order_Block_Bajista; param[class="num">8].type = TYPE_COLOR; param[class="num">8].integer_value = Color_Order_Block_Alcista; param[class="num">9].type = TYPE_STRING; param[class="num">9].string_value = "-- Strategy --"; param[class="num">10].type = TYPE_INT; param[class="num">10].integer_value = tp_sl_style; param[class="num">11].type = TYPE_STRING; param[class="num">11].string_value = "- ATR"; param[class="num">12].type = TYPE_DOUBLE; param[class="num">12].double_value = Atr_Multiplier_1; param[class="num">13].type = TYPE_DOUBLE; param[class="num">13].double_value = Atr_Multiplier_2; param[class="num">14].type = TYPE_STRING; param[class="num">14].string_value = "- POINT"; param[class="num">15].type = TYPE_INT; param[class="num">15].integer_value = TP_POINT; param[class="num">16].type = TYPE_INT; param[class="num">16].integer_value = SL_POINT; class=class="str">"cmt">//--- order_block_indicator_handle = IndicatorCreate(_Symbol, timeframe_order_block, IND_CUSTOM, ArraySize(param), param); hanlde_ma = iMA(_Symbol, timeframe_order_block, class="num">30, class="num">0, MODE_EMA, PRICE_CLOSE); trade.SetExpertMagicNumber(Magic); if(order_block_indicator_handle == INVALID_HANDLE) { Print("The order blocks indicator is not available last error: ", _LastError); class="kw">return INIT_FAILED; } if(hanlde_ma == INVALID_HANDLE) { Print("The ema indicator is not available latest error: ", _LastError); class="kw">return INIT_FAILED; } ChartIndicatorAdd(class="num">0, class="num">0, order_block_indicator_handle); ChartIndicatorAdd(class="num">0, class="num">0, hanlde_ma); class=class="str">"cmt">//--- risk.SetPorcentages(percentage_or_money_mdl_input, percentage_or_money_mwl_input, percentage_or_money_gmlpo_input
「动态风险分级的阈值串组装」
动态 GMLPO 模式下,风险不是一成不变的百分比,而是随账户回撤档位切换。代码里用 SetDynamicGMLPOUsingFixedParameters 把 4 个激活阈值和 4 个对应风险值拼成逗号分隔的字符串,交给 risk 对象后续按档位调用。 固定参数模式(DYNAMIC_GMLPO_FIXED_PARAMETERS)下,你只需在输入变量里填 inp_balance_percentage_to_activate_the_risk_1~4 与 inp_percentage_to_be_modified_1~4,函数内部用 DoubleToString 转成文本并拼成 percentages_to_activate 与 risks_to_be_applied 两个串,例如 "2.0,5.0,10.0,20.0" 对应 "1.0,0.8,0.5,0.3"。 如果是 FULL_CUSTOM 模式,则直接把外部传进来的 str_percentages_to_be_reviewed 和 str_percentages_to_apply 喂给 SetDynamicGMLPO,不再走固定参数封装。 末尾的 ArraySetAsSeries(tp1,true) 等四行把止盈止损数组设为时间序列方向,使下标 0 对应最新 K 线,回测或实盘读取挂单位置时才不会错位。外汇与贵金属波动剧烈,这种按余额回撤动态降风险的做法只能降低概率性爆仓风险,不保证免亏。
risk.SetEnums(mode_calculation_mdl, mode_calculation_mwl, mode_calculation_gmlpo, mode_calculation_ml, mode_calculation_mdp); risk.SetApplieds(applied_percentages_mdl, applied_percentages_wl, applied_percentages_gmlpo, applied_percentages_ml, applied_percentages_mdp); if(mode_gmlpo == DYNAMIC_GMLPO_FIXED_PARAMETERS) { class="type">class="kw">string percentages_to_activate, risks_to_be_applied; SetDynamicGMLPOUsingFixedParameters(inp_balance_percentage_to_activate_the_risk_1, inp_balance_percentage_to_activate_the_risk_2, inp_balance_percentage_to_activate_the_risk_3 , inp_balance_percentage_to_activate_the_risk_4, inp_percentage_to_be_modified_1, inp_percentage_to_be_modified_2, inp_percentage_to_be_modified_3, inp_percentage_to_be_modified_4 , percentages_to_activate, risks_to_be_applied); risk.SetDynamicGMLPO(percentages_to_activate, risks_to_be_applied, revision_type_gmlpo); } else if(mode_gmlpo == DYNAMIC_GMLPO_FULL_CUSTOM) risk.SetDynamicGMLPO(str_percentages_to_be_reviewed, str_percentages_to_apply, revision_type_gmlpo); class="type">void SetDynamicGMLPOUsingFixedParameters( class="type">class="kw">double _balance_percentage_to_activate_the_risk_1, class="type">class="kw">double _balance_percentage_to_activate_the_risk_2, class="type">class="kw">double _balance_percentage_to_activate_the_risk_3, class="type">class="kw">double _balance_percentage_to_activate_the_risk_4, class="type">class="kw">double _percentage_to_be_modified_1, class="type">class="kw">double _percentage_to_be_modified_2, class="type">class="kw">double _percentage_to_be_modified_3, class="type">class="kw">double _percentage_to_be_modified_4, class="type">class="kw">string &percentages_to_activate, class="type">class="kw">string &risks_to_be_applied) { percentages_to_activate = DoubleToString(_balance_percentage_to_activate_the_risk_1) + "," + DoubleToString(_balance_percentage_to_activate_the_risk_2) + "," + DoubleToString(_balance_percentage_to_activate_the_risk_3) + "," + DoubleToString(_balance_percentage_to_activate_the_risk_4); risks_to_be_applied = DoubleToString(_percentage_to_be_modified_1) + "," + DoubleToString(_percentage_to_be_modified_2) + "," + DoubleToString(_percentage_to_be_modified_3) + "," + DoubleToString(_percentage_to_be_modified_4); } class=class="str">"cmt">//--- ArraySetAsSeries(tp1, true); ArraySetAsSeries(tp2, true); ArraySetAsSeries(sl1, true); ArraySetAsSeries(sl2, true);
EA 主循环里的新K线判定与风控闸口
在 MT5 的 EA 里,OnTick 是价格每次跳动都会触发的入口,而 OnTradeTransaction 专门接交易回执。把每日、每周的边界判断放在 OnTick 最前面,可以避免在跨日或跨周的第一根 Tick 上重复跑逻辑——代码用 iTime(_Symbol, PERIOD_D1, 0) 和 PERIOD_W1 的返回值跟上次记录的 TiempoBarraApertua 比,不等就说明新周期开始了。 新的一天触发时,opera 被重置为 true 放开交易,同时调 risk.OnNewDay() 重算当日时段;HoraYMinutoADatetime 把参数里的小时分钟拼成 datetime,用来框定 start_sesion 与 end_sesion。外汇和贵金属杠杆高,这种时段闸口能挡掉流动性差的凌晨滑点,但并不能消除跳空风险。 订单区块指标所在周期每出一根新 K 线,就用 CopyBuffer 把指标句柄 2~5 号的 tp1/tp2/sl1/sl2 各拉 5 根。只有当四个值都大于 0,且 risk.GetPositionsTotal()==0(当前无持仓)时才下单;tp2>sl2 走多、sl2>tp2 走空,手数按动态或固定参数给。 OnTick 尾巴上还要再过一道盈亏限额:超了就平掉 EA 开的全部仓并把 opera 置 false,相当于当日熔断。OnTradeTransaction 则把开平改事件转给 risk 类处理,保证仓位变动后风控状态同步。 别把正态当圣经 CopyBuffer 拉 5 根却只判 [0],意味着你只信最新信号;若指标在 M15 重绘,历史 4 根只是摆设,回测漂亮实盘可能掉链子。开 MT5 把指标周期调到和你订单区块一致,用专家日志打印 tp1[0]~sl2[0] 四值,确认不重绘再上。
class=class="str">"cmt">//--- if(TiempoBarraApertua != iTime(_Symbol, PERIOD_D1, class="num">0)) { opera = true; risk.OnNewDay(); start_sesion = HoraYMinutoADatetime(hora_inicio,min_inicio); end_sesion = HoraYMinutoADatetime(hora_fin,min_fin); if(TiempoBarraApertua_1 != iTime(_Symbol, PERIOD_W1, class="num">0)) { risk.OnNewWeek(); TiempoBarraApertua_1 = iTime(_Symbol, PERIOD_W1, class="num">0); } TiempoBarraApertua = iTime(_Symbol, PERIOD_D1, class="num">0); } class="type">class="kw">datetime HoraYMinutoADatetime(class="type">int hora, class="type">int minuto) { class="type">MqlDateTime tm; TimeCurrent(tm); class=class="str">"cmt">// Asigna la hora y el minuto deseado tm.hour = hora; tm.min = minuto; tm.sec = class="num">0; class=class="str">"cmt">// Puedes ajustar los segundos si es necesario class="kw">return StructToTime(tm);; } if(prev_vela != iTime(_Symbol, timeframe_order_block, class="num">0)) { CopyBuffer(order_block_indicator_handle, class="num">2, class="num">0, class="num">5, tp1); CopyBuffer(order_block_indicator_handle, class="num">3, class="num">0, class="num">5, tp2); CopyBuffer(order_block_indicator_handle, class="num">4, class="num">0, class="num">5, sl1); CopyBuffer(order_block_indicator_handle, class="num">5, class="num">0, class="num">5, sl2); if(tp1[class="num">0] > class="num">0 && tp2[class="num">0] > class="num">0 && sl1[class="num">0] > class="num">0 && sl2[class="num">0] > class="num">0) { if(tp2[class="num">0] > sl2[class="num">0] && risk.GetPositionsTotal() == class="num">0) class=class="str">"cmt">//compras { class="type">class="kw">double ASK = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits); risk.SetStopLoss(ASK - sl1[class="num">0]); class="type">class="kw">double lot = (Lote_Type == Dinamico ? risk.GetLote(ORDER_TYPE_BUY) : lote); if(lot > class="num">0.0) trade.Buy(lot, _Symbol, ASK, sl1[class="num">0], tp1[class="num">0], "Order Block EA Buy"); } else if(sl2[class="num">0] > tp2[class="num">0] && risk.GetPositionsTotal() == class="num">0) class=class="str">"cmt">//venta {
◍ 出场与风控熔断的实盘代码落点
这段逻辑跑在 EA 的 OnTick 尾部,负责把订单块卖单发出去之后,立刻接管账户级风控。外汇与贵金属杠杆高,净值回撤触发硬性平仓时,程序必须比人快。 double BID 先取当前卖价并按品种精度归一化,risk.SetStopLoss 用信号位 sl1[0] 减去 BID 算出绝对止损距离;若 Lote_Type 选动态,risk.GetLote(ORDER_TYPE_SELL) 按风险敞口反推手数,lot 大于 0 才允许 trade.Sell 进场,挂单备注写死 "Order Block EA Sell" 方便在 MT5 历史里筛。 下面几个 IsSuperated 判断是熔断核心:ML 对应总亏损上限,propfirm_ftmo 模式下直接 Print 并 ExpertRemove 退出评测;其他模式则平掉所有仓位、打印 "Maximum loss exceeded now" 并把 opera 置 false 停止新单。MDL 管每日亏损、MDP 管每日盈利达标、MWL 管每周亏损,越线统统 CloseAllPositions 并停阀。 最后 OnTradeTransaction 只做一件事:把每笔成交事件喂给 risk.OnTradeTransactionEvent,让风控对象自己维护持仓与权益快照。复制进 MT5 把 sl1/tp1 数组接上你的订单块识别,就能在策略测试器里看熔断是否按预期掐断。
class="type">class="kw">double BID = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits); risk.SetStopLoss(sl1[class="num">0] - BID); class="type">class="kw">double lot = (Lote_Type == Dinamico ? risk.GetLote(ORDER_TYPE_SELL) : lote); if(lot > class="num">0.0) trade.Sell(lot, _Symbol, BID, sl1[class="num">0], tp1[class="num">0], "Order Block EA Sell"); } } prev_vela = iTime(_Symbol, timeframe_order_block, class="num">0); } risk.OnTickEvent(); if(risk.ML_IsSuperated(CLOSE_POSITION_AND_EQUITY) == true) { if(risk_mode == propfirm_ftmo) { Print("The expert advisor lost the funding test"); ExpertRemove(); } else { risk.CloseAllPositions(); Print("Maximum loss exceeded now"); opera = false; } } if(risk.MDL_IsSuperated(CLOSE_POSITION_AND_EQUITY) == true) { risk.CloseAllPositions(); Print("Maximum daily loss exceeded now"); opera = false; } if(risk.MDP_IsSuperated(CLOSE_POSITION_AND_EQUITY) == true) { risk.CloseAllPositions(); Print("Excellent Maximum daily profit achieved"); opera = false; } if(risk.MWL_IsSuperated(CLOSE_POSITION_AND_EQUITY) == true) { risk.CloseAllPositions(); Print("The maximum weekly loss has been exceeded"); opera = false; extra = false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TradeTransaction function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTradeTransaction(const MqlTradeTransaction& trans, const class="type">MqlTradeRequest& request, const class="type">MqlTradeResult& result) { risk.OnTradeTransactionEvent(trans); }
「EA 卸载前把图表和内存都清干净」
OnDeinit 在 EA 从图表移除或终端断连前立即触发,是最后一道收尾关口。若不清场,运行时挂上去的 EMA、订单区块指标会残留在图表「指标」选项卡里,自定义指标的描述符也可能赖在内存中,长期加载多个 EA 后 MT5 内存占用会悄悄爬升。 删指标不能硬猜名字。主窗口指标总数由 ChartIndicatorsTotal(chart_id, 0) 拿到,逐个用 ChartIndicatorName(chart_id, 0, i) 取全名,再用 StringFind(indicator_name, "MA") 模糊匹配——返回非负索引即命中,可交给 ChartIndicatorDelete 按名删除。 光删图表对象不够。通过 handle 访问数据的指标必须调 IndicatorRelease(),否则 EA 停止后 handle_ma、order_block_indicator_handle 这类描述符指向的缓冲区不会释放。外汇与贵金属交易杠杆高、回撤快,EA 异常退出本就频繁,养成卸载即释放的习惯能少踩不少坑。 下面这段代码可直接塞进你的 EA 模板:GetMovingAverageIndex 遍历主窗口找含 "MA" 的指标下标,OnDeinit 里先删两个图表指标,再对两个 handle 做释放判断。注意原示例把 handle_ma 拼成了 hanlde_ma,复制时改回正确拼写才能编译过。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- ChartIndicatorDelete(class="num">0, class="num">0, ChartIndicatorName(class="num">0, class="num">0, GetMovingAverageIndex())); ChartIndicatorDelete(class="num">0, class="num">0, "Order Block Indicator"); if(hanlde_ma != INVALID_HANDLE) IndicatorRelease(hanlde_ma); if(order_block_indicator_handle != INVALID_HANDLE) IndicatorRelease(order_block_indicator_handle); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Extra Functions | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int GetMovingAverageIndex(class="type">long chart_id = class="num">0) { class="type">int total_indicators = ChartIndicatorsTotal(chart_id, class="num">0); for(class="type">int i = class="num">0; i < total_indicators; i++) { class="type">class="kw">string indicator_name = ChartIndicatorName(chart_id, class="num">0, i); if(StringFind(indicator_name, "MA") >= class="num">0) class="kw">return i; } class="kw">return -class="num">1; } class=class="str">"cmt">//+------------------------------------------------------------------+
四种回测下的风控差异
拿 Gold 在 MT5 里跑了一组对照回测,测试窗口固定为 2024.01.01 至 2025.03.28,杠杆 1:100,初始资金 10000 美元,止损止盈全部由 ATR 计算。第一遍用未优化的 M5 实点模型,仅作基线;第二遍放开交易时间、单笔风险、ATR 倍数等参数做优化,时间周期落到 M3,并加入每日最大盈利与最大亏损限额。 去掉双限额后跑出的回测 3 显示前期曲线冲得快,但随后受连败与突发波动拖累走低;而带限额的回测 2 是平稳上行、无明显尖峰。这说明盈利上限能压住过度交易,日亏上限能把连败期的坑控住。 回测 4 在回测 3 参数基础上加了动态风险:账户收益转负时单笔风险骤降。结果很直接——回测 3 在连续盈利段把余额推高 10% 却又跌回约 8600 美元;回测 4 始终贴着 10000 美元走,最低余额守在 9086 美元附近,但填坑用了更多笔数和更长时间。 对自营账户来说,动态风险更合算,它显著限制 EA 可能砸出的深坑;普通账户若追求快速回血会觉得它拖沓。黄金外汇类策略本身高风险,限额与动态风险只是把破产概率往下压,不保证不亏。
◍ 一点提醒
这套风险管理框架在四次回测里跑出了明显分叉:用 Set 1% Risk 和 Set Order Block EA(带每日最大亏损/盈利限制)的曲线,比 Set No Dynamic Risk(无限制)更抗极端回撤;而 Set Dynamic Risk 在去掉每日盈亏闸后,浮盈回吐幅度显著放大。 文件层面,Risk_Management.mqh 里的 CRiskManagement 类是所有损益函数的载体,Order Block EA MT5.mq5 负责把指标信号接进执行层,三个 .set 文件直接决定闸口开关。 外汇与贵金属自带高杠杆高风险,上述限制逻辑只是降低破产概率,不承诺任何收益。开 MT5 把四个 set 分别丢进同周期回测,对比权益曲线拐点,比看结论更直观。