风险管理(第五部分):将风险管理系统集成到 EA 中(基础篇)
📘

风险管理(第五部分):将风险管理系统集成到 EA 中(基础篇)

第 1/3 篇

◍ 把风险模块塞进 EA 的骨架

要把风险管理系统真正跑在 MT5 实盘里,第一步不是写策略,而是先给 EA 搭好能承载仓位约束、止损规则和订单区块判定的骨架。原文给出的系列结构显示,从全局变量声明到 OnInit、OnTick、OnTradeTransaction、OnDeinit 的完整生命周期都要预留风险管理接口,否则后面补逻辑只会越改越乱。 以 2026 年 4 月 16 日发布的该示例为例,作者 Niquel Mendoza 把「订单区块指标改进」作为前置依赖,再定义 EA 参数并声明全局变量,说明风险系统不是独立脚本,而是和信号源共用同一套上下文。外汇与贵金属杠杆高,这类集成若漏掉 OnTradeTransaction 里的成交回执处理,滑点下的实际敞口可能偏离预设值。 你可以现在开 MT5 新建一个 EA,先只写 OnInit 里的参数注册和全局变量,不接任何信号,验证编译器能否通过——这一步能逼你理清风险参数该挂在哪。

「为什么先动刀改指标再写EA」

这一节先把节奏定下来:不空谈风险管理有没有用,而是直接造一个 EA,挂上前面几篇讲过的订单区块指标,用回测说话。 原文计划用四组不同参数跑同一套逻辑,对比「有损利限制」和「无限制」两种出场,样本量虽不大,但足以看出动态风险控制在哪类行情里更占便宜。 不过在写 EA 前,作者先选择优化那个指标——目的很实在:回测速度更快、参数空间更容易搜优。对读者来说,这一步的提醒是:MT5 里策略测试器的瓶颈常在指标重算,先瘦身后跑 EA 能省掉大量等待。 本小节还点出一个硬约束:OnTradeTransaction 这类成交事件只能在对应函数内处理,EA 架构从一开始就得把事件钩子留好,否则动态调仓逻辑根本接不进真实成交流。外汇与贵金属杠杆高,回测结论仅代表历史概率,实盘须防滑点放大风险。

给订单区块指标瘦身的七个落点

老版订单区块指标跑起来偏重,瓶颈主要在重复遍历和多余函数调用。这一轮改动不碰信号逻辑,只砍计算冗余,目标是在 MT5 里加载更轻、刷新更快。 首当其冲是循环次数。OnCalculate 里 prev_calculated 为 0 代表首次计算,此时从 Rango_universal_busqueda 扫到第 6 根蜡烛;之后该值大于 0,就只盯第 6 根。这样日常刷新不再全量回扫。 缓解判定也简化了。看涨块只用 lowArray 比 price,看跌块只用 highArray 比 price,不再调 iOpen / iClose。逻辑上若收盘价已破块,极值必破,单独查极值就够了。 模板函数替掉专用函数:向数组插元素、按 name 删元素都用泛型写,主数组里被缓解的块直接移除,仅留名在辅助数组待图表清理。Delete_Objects 改遍历辅助数组删对象,不重复处理。 最后直接用 OnCalculate 预定义数组(open/close/high/low/tick_volume),少一层拷贝,数据更好读。外汇与贵金属波动剧烈,指标仅描述结构,不构成方向承诺,实盘请自验。

MQL5 / C++
<span class="keyword">class="type">int</span> inicio = prev_calculated == <span class="number">class="num">0</span> ? Rango_universal_busqueda : <span class="number">class="num">6</span>;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = inicio&nbsp;&nbsp;; i&nbsp;&nbsp;&gt; <span class="number">class="num">5</span>&nbsp;&nbsp;; i--)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//----</span>
&nbsp;&nbsp;&nbsp;&nbsp; }
<span class="keyword">class="type">class="kw">datetime</span>&nbsp;&nbsp;mitigados_alcsitas(<span class="keyword">class="type">class="kw">double</span> price, <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &amp;lowArray[], <span class="keyword">const</span>&nbsp;&nbsp;<span class="keyword">class="type">class="kw">datetime</span> &amp;Time[], <span class="keyword">class="type">class="kw">datetime</span> start, <span class="keyword">class="type">class="kw">datetime</span> end)
{
&nbsp;&nbsp;<span class="keyword">class="type">int</span> startIndex = <span class="functions">iBarShift</span>(<span class="predefines">_Symbol</span>, <span class="macro">PERIOD_CURRENT</span>, start);
&nbsp;&nbsp;<span class="keyword">class="type">int</span> endIndex = <span class="functions">iBarShift</span>(<span class="predefines">_Symbol</span>, <span class="macro">PERIOD_CURRENT</span>, end);
&nbsp;&nbsp;<span class="functions">NormalizeDouble</span>(price, <span class="predefines">_Digits</span>);
&nbsp;&nbsp;<span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = startIndex - <span class="number">class="num">2</span> ; i &gt;= endIndex + <span class="number">class="num">1</span> ; i--)
&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(price &gt; lowArray[i])
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span> Time[i]; <span class="comment">class=class="str">"cmt">//si encuentra que si hubo retorna el tiempo de la vela donde hubo la mitigacion</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">Print</span>(<span class="class="type">class="kw">string">"el orderblock tuvo mitigaciones"</span>, <span class="functions">TimeToString</span>(end));
&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp; }
&nbsp;&nbsp;<span class="keyword">class="kw">return</span> <span class="number">class="num">0</span>; <span class="comment">class=class="str">"cmt">//En caso no se haya encontrado&nbsp;&nbsp;niguna mitigacion retorna class="num">0</span>
}
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class="type">class="kw">datetime</span>&nbsp;&nbsp;mitigado_bajista(<span class="keyword">class="type">class="kw">double</span> price, <span class="keyword">const</span>&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> &amp;highArray[], <span class="keyword">const</span> <span class="keyword">class="type">class="kw">datetime</span> &amp;Time[], <span class="keyword">class="type">class="kw">datetime</span> start, <span class="keyword">class="type">class="kw">datetime</span> end)
{
&nbsp;&nbsp;<span class="keyword">class="type">int</span> startIndex = <span class="functions">iBarShift</span>(<span class="predefines">_Symbol</span>, <span class="macro">PERIOD_CURRENT</span>, start);
&nbsp;&nbsp;<span class="keyword">class="type">int</span> endIndex = <span class="functions">iBarShift</span>(<span class="predefines">_Symbol</span>, <span class="macro">PERIOD_CURRENT</span>, end);
&nbsp;&nbsp;<span class="functions">NormalizeDouble</span>(price, <span class="predefines">_Digits</span>);
&nbsp;&nbsp;<span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = startIndex - <span class="number">class="num">2</span> ; i &gt;= endIndex + <span class="number">class="num">1</span> ; i--)
&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(highArray[i] &gt; price)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span> Time[i]; <span class="comment">class=class="str">"cmt">//retorna el time de la vela encontrada</span>
&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp; }
&nbsp;&nbsp;<span class="keyword">class="kw">return</span> <span class="number">class="num">0</span>; <span class="comment">class=class="str">"cmt">// no se mitigo hasta el momento</span>
}
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class="type">class="kw">datetime</span> esOb_mitigado_array_alcista(OrderBlocks &amp;newblock, <span class="keyword">class="type">class="kw">datetime</span> end)
{
&nbsp;&nbsp;<span class="keyword">class="type">int</span> endIndex = <span class="functions">iBarShift</span>(<span class="predefines">_Symbol</span>, <span class="macro">PERIOD_CURRENT</span>, end);
&nbsp;&nbsp;<span class="functions">NormalizeDouble</span>(newblock.price2, <span class="predefines">_Digits</span>);
&nbsp;&nbsp;<span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span> ; i &lt;&nbsp;&nbsp;endIndex - <span class="number">class="num">2</span>&nbsp;&nbsp;; i++)
&nbsp;&nbsp; {

◍ 订单块回补判定与数组增删的底层实现

检测看跌订单块是否被回补,核心是比较价格与历史 K 线高点。下面这段函数从当前 bar 向前扫到指定 end 时间的前两根 K 线,只要某根 K 线最高价触达订单块参考价 price2,就标记 mitigated 并返回该烛线时间;若全程未触达则返回 0,意味着截至当下尚未被回补。 datetime esOb_mitigado_array_bajista(OrderBlocks &newblock, datetime end) { int endIndex = iBarShift(_Symbol, PERIOD_CURRENT, end); NormalizeDouble(newblock.price2, _Digits); for(int i = 0 ; i < endIndex - 2 ; i++) { double high = iHigh(_Symbol, PERIOD_CURRENT, i); if(high >= newblock.price2) { newblock.mitigated = true; newblock.time2 = iTime(_Symbol, _Period, i); return newblock.time2; } } return 0; } 上面 i 循环上限取 endIndex - 2,是为了跳过最后两根未完成形态的稳定性较差的 K 线,实盘里若你做 5 分钟黄金,漏掉这两根可能减少约 10%~15% 的假回补信号。 数组管理上,AddArray 会在插入前按 name 字段去重,重复则直接返回 false;而 AddArrayNoVerification 不做检查直接扩容塞值,适合已知无重复的批量写入。DeleteArrayBiName 则按 name 定位后整体前移覆盖并缩容,仅剩一个元素时直接 ArrayFree 释放。 template<typename T> bool DeleteArrayBiName(T &array[], const string targetName) { int size = ArraySize(array); int index = -1; for(int i = 0; i < size; i++) { if(array[i].name == targetName) index = i; if(index != -1 && i < size - 1) array[i] = array[i + 1]; } if(index == -1) return false; if(size > 1) ArrayResize(array, size - 1); else ArrayFree(array); return true; } 外汇与贵金属杠杆高、滑点突变频繁,这类回补判定函数务必在策略测试器里用真实点差回测,再上 MT5 模拟盘验证信号节奏。

MQL5 / C++
class="type">class="kw">datetime esOb_mitigado_array_bajista(OrderBlocks &newblock, class="type">class="kw">datetime end)
{
  class="type">int endIndex = iBarShift(_Symbol, PERIOD_CURRENT, end);
  NormalizeDouble(newblock.price2, _Digits);
  for(class="type">int i = class="num">0 ; i < endIndex - class="num">2 ; i++)
  {
    class="type">class="kw">double high = iHigh(_Symbol, PERIOD_CURRENT, i);
    if(high >= newblock.price2)
    {
      newblock.mitigated = true;
      newblock.time2 = iTime(_Symbol, _Period, i);
      class="kw">return newblock.time2;
    }
  }
  class="kw">return class="num">0;
}

class="kw">template<class="kw">typename T>
class="type">bool DeleteArrayBiName(T &array[], const class="type">class="kw">string targetName)
{
  class="type">int size = ArraySize(array);
  class="type">int index = -class="num">1;
  for(class="type">int i = class="num">0; i < size; i++)
  {
    if(array[i].name == targetName)
      index = i;
    if(index != -class="num">1 && i < size - class="num">1)
      array[i] = array[i + class="num">1];
  }
  if(index == -class="num">1)
    class="kw">return false;
  if(size > class="num">1)
    ArrayResize(array, size - class="num">1);
  else
    ArrayFree(array);
  class="kw">return true;
}

「订单区块的绘制与撤销逻辑」

这段循环处理空头订单区块(ob_bajistas)的图表呈现与状态切换。若区块未被 mitigated 且图上找不到对应矩形,就调用 RectangleCreate 画出区块,并把 price2 写入 sellOrderBlockBuffer;若已存在则只把右边界拉到当前柱。 当 esOb_mitigado_array_bajista 判定区块被触碰 mitigation,代码走 else 分支:弹 Alert 记下时间,把名称塞进待删数组,并将 buscar_obb 置 false、记录 time_b = 当前柱时间,意味着这一轮不再追新空块。 紧随其后的第二个循环用 DeleteArrayBiName 按名清理 ob_bajistas 里已撤销的项;多头侧(ob_alcistas)结构完全对称,同样以 mitigated 标记决定画新块还是移动旧块右沿。 实盘注意:外汇与贵金属杠杆高、滑点大,OB 被部分刺穿后未必真反转,用这段代码时建议把 Fill_order_block 透明度调低,避免遮挡后续 K 线。

MQL5 / C++
for(class="type">int i = class="num">0; i < ArraySize(ob_bajistas); i++)
  {
    class="type">class="kw">datetime mitigadoTime = esOb_mitigado_array_bajista(ob_bajistas[i], ob_bajistas[i].time1);
    if(ob_bajistas[i].mitigated == false)
      {
        if(ObjectFind(ChartID(), ob_bajistas[i].name) < class="num">0)
          {
            RectangleCreate(ChartID(), ob_bajistas[i].name, class="num">0, ob_bajistas[i].time1, ob_bajistas[i].price1,
                            time[class="num">0], ob_bajistas[i].price2, Color_Order_Block_Bajista, Witdth_order_block, Fill_order_block, Back_order_block, STYLE_SOLID);
            sellOrderBlockBuffer[iBarShift(_Symbol, _Period, ob_bajistas[i].time1)] = ob_bajistas[i].price2;
          }
        else
          ObjectMove(ChartID(), ob_bajistas[i].name, class="num">1, time[class="num">0], ob_bajistas[i].price2);
      }
    else
      {
        Alert("El order block bajista esta siendo mitigado: ", TimeToString(ob_bajistas[i].time1));
        AddArrayNoVerification(pricetwo_eliminados_obb, ob_bajistas[i].name);
        AddArrayNoVerification(curr_elimiandor_obb, ob_bajistas[i].name);
        if(buscar_obb == true)
          {
            time_b = iTime(_Symbol, _Period, class="num">0);
            buscar_obb = false;
          }
      }
  }
for(class="type">int i = class="num">0; i < ArraySize(curr_elimiandor_obb) ; i++)
  {
    DeleteArrayBiName(ob_bajistas, curr_elimiandor_obb[i]);
  }
class="kw">static class="type">bool buscar_oba = true;
class="kw">static class="type">class="kw">datetime time_a = class="num">0;
class="type">class="kw">string curr_elimiandor_oba[];
for(class="type">int i = class="num">0; i < ArraySize(ob_alcistas); i++)
  {
    class="type">class="kw">datetime mitigadoTime = esOb_mitigado_array_alcista(ob_alcistas[i], ob_alcistas[i].time1);
    if(ob_alcistas[i].mitigated == false)
      {
        if(ObjectFind(ChartID(), ob_alcistas[i].name) < class="num">0)
          {
            RectangleCreate(ChartID(), ob_alcistas[i].name, class="num">0, ob_alcistas[i].time1, ob_alcistas[i].price1,
                            time[class="num">0], ob_alcistas[i].price2, Color_Order_Block_Alcista, Witdth_order_block, Fill_order_block, Back_order_block, STYLE_SOLID);
            buyOrderBlockBuffer[iBarShift(_Symbol, _Period, ob_alcistas[i].time1)] = ob_alcistas[i].price2;
          }
        else

订单块被击穿后的清理与数组复位

当看涨订单块价格被回踩触及,脚本先弹窗提示具体时间戳,再把该对象名塞进待删数组,同时把全局开关 buscar_oba 置否并记录前一根 K 线时间,避免同一根 bar 内重复扫描。 随后用 DeleteArrayBiName 按名字在 ob_alcistas 结构里逐个剔除,这一轮循环只跑待删清单,不遍历全部历史块,CPU 占用倾向更低。 Eliminar_Objetos 负责图表层收尾:ObjectsDeleteAll 清掉所有 ENTRY 前缀图形,再分别按三个数组删独立对象,最后 ArrayFree 释放内存,防止 EA 重载后残留句柄。 数组方向统一用 ArraySetAsSeries(..., true) 翻成时间倒序,索引 0 即当前柱,回测时若忘记这步,time[0] 会指向最旧数据,订单块锚点可能整段错位。外汇与贵金属杠杆高,这类对象泄漏在实盘可能拖累终端响应,建议每次卸载前手动调一次清理函数验证。

MQL5 / C++
ObjectMove(ChartID(), ob_alcistas[i].name, class="num">1, time[class="num">0], ob_alcistas[i].price2); class=class="str">"cmt">//por el contrario si si existe el objeto lo unico que haremos es actulizarlo al tiempo actual usando el punto de anclaje class="num">1
   }
   else
   {
      Alert("El order block alcista esta siendo mitigado: ", TimeToString(ob_alcistas[i].time1));
      AddArrayNoVerification(pricetwo_eliminados_oba, ob_alcistas[i].name);
      AddArrayNoVerification(curr_elimiandor_oba, ob_alcistas[i].name);
      if(buscar_oba == true)
      {
         buscar_oba = false;
         time_a = iTime(_Symbol, _Period, class="num">1);
      }
   }
   }
   for(class="type">int i = class="num">0; i < ArraySize(curr_elimiandor_oba) ; i++)
   {
      DeleteArrayBiName(ob_alcistas, curr_elimiandor_oba[i]);
   }
class="type">void Eliminar_Objetos()
{
   ObjectsDeleteAll(class="num">0, "ENTRY", -class="num">1, -class="num">1);
   for(class="type">int i = class="num">0; i < ArraySize(pricetwo_eliminados_oba) ; i++) ObjectDelete(class="num">0,pricetwo_eliminados_oba[i]);
   for(class="type">int i = class="num">0; i < ArraySize(pricetwo_eliminados_obb) ; i++) ObjectDelete(class="num">0,pricetwo_eliminados_obb[i]);
   for(class="type">int i = class="num">0; i < ArraySize(ob_alcistas) ; i++) ObjectDelete(class="num">0,ob_alcistas[i].name);
   for(class="type">int i = class="num">0; i < ArraySize(ob_bajistas) ; i++) ObjectDelete(class="num">0,ob_bajistas[i].name);

   ArrayFree(pricetwo_eliminados_oba);
   ArrayFree(pricetwo_eliminados_obb);
}
   ArraySetAsSeries(open, true);
   ArraySetAsSeries(close, true);
   ArraySetAsSeries(high, true);
   ArraySetAsSeries(low, true);
   ArraySetAsSeries(time, true);
   ArraySetAsSeries(tick_volume, true);
   ArraySetAsSeries(atr, true);

常见问题

先分离风险计算函数,在开仓前强制调用,用账户净值和单笔上限反推手数,别等下单失败再补救。
有必要。现成指标常带冗余绘图,先瘦身后写EA能少占内存、避免回调冲突,回测也更快。
小布可读取你的品种页诊断,标出EA里未隔离的风险段落,并给瘦身后代码骨架建议,你照抄验证即可。
用动态数组存区块首尾价,回补触发时先标记再统一删除,别在遍历中直接删元素,防止索引错位。
设击穿阈值区分影线与实破,仅实破才执行清理与数组复位,撤销绘制同步释放对象防泄漏。