风险管理(第五部分):将风险管理系统集成到 EA 中·综合运用
🛡️

风险管理(第五部分):将风险管理系统集成到 EA 中·综合运用

(3/3)· 从指标优化到 OnTradeTransaction 落地,用四次回测看清动态风控何时真正起作用

含代码示例实战向 第 3/3 篇
很多交易者把风控只停在手算仓位和硬止损,EA 跑起来却从不管订单区块是否被缓解。本文把前几篇的指标接进机器人,让风控随成交事件自动重算,而不是靠人盯屏幕。

◍ 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 手以下验证手数输出。

MQL5 / C++
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 线,回测或实盘读取挂单位置时才不会错位。外汇与贵金属波动剧烈,这种按余额回撤动态降风险的做法只能降低概率性爆仓风险,不保证免亏。

MQL5 / C++
  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] 四值,确认不重绘再上。

MQL5 / C++
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 数组接上你的订单块识别,就能在策略测试器里看熔断是否按预期掐断。

MQL5 / C++
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,复制时改回正确拼写才能编译过。

MQL5 / C++
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 分别丢进同周期回测,对比权益曲线拐点,比看结论更直观。

把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到订单区块与缓解状态,你专注决策而非手写循环。

常见问题

成交、持仓变更等事件只能在 OnTradeTransaction 捕获,放 OnTick 容易漏掉瞬时变化,导致仓位计算滞后。
小布内置了订单区块与缓解的可视化,但 EA 编译部署仍在 MT5 端完成;它帮你省掉肉眼核对盘口的部分。
prev_calculated 大于 0 后只查最新蜡烛,避免全量重扫,回测中每根 K 线省下的时间会随样本放大。
测试显示趋势流畅时硬限制利润反而截断行情,震荡市里动态止损才更显概率优势,具体看第四组对照。
基础概念见《风险管理(第一部分):订单区块指标·基础篇》,接入前准备见《风险管理(第四部分):参数与事件·进阶篇》。