风险管理(第五部分):将风险管理系统集成到 EA 中(基础篇)
◍ 把风险模块塞进 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),少一层拷贝,数据更好读。外汇与贵金属波动剧烈,指标仅描述结构,不构成方向承诺,实盘请自验。
<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>; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = inicio ; i > <span class="number">class="num">5</span> ; i--) { <span class="comment">class=class="str">"cmt">//----</span> } <span class="keyword">class="type">class="kw">datetime</span> 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> &lowArray[], <span class="keyword">const</span> <span class="keyword">class="type">class="kw">datetime</span> &Time[], <span class="keyword">class="type">class="kw">datetime</span> start, <span class="keyword">class="type">class="kw">datetime</span> end) { <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); <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); <span class="functions">NormalizeDouble</span>(price, <span class="predefines">_Digits</span>); <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = startIndex - <span class="number">class="num">2</span> ; i >= endIndex + <span class="number">class="num">1</span> ; i--) { <span class="keyword">if</span>(price > lowArray[i]) { <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> <span class="functions">Print</span>(<span class="class="type">class="kw">string">"el orderblock tuvo mitigaciones"</span>, <span class="functions">TimeToString</span>(end)); } } <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 niguna mitigacion retorna class="num">0</span> } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">class="kw">datetime</span> mitigado_bajista(<span class="keyword">class="type">class="kw">double</span> price, <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &highArray[], <span class="keyword">const</span> <span class="keyword">class="type">class="kw">datetime</span> &Time[], <span class="keyword">class="type">class="kw">datetime</span> start, <span class="keyword">class="type">class="kw">datetime</span> end) { <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); <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); <span class="functions">NormalizeDouble</span>(price, <span class="predefines">_Digits</span>); <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = startIndex - <span class="number">class="num">2</span> ; i >= endIndex + <span class="number">class="num">1</span> ; i--) { <span class="keyword">if</span>(highArray[i] > price) { <span class="keyword">class="kw">return</span> Time[i]; <span class="comment">class=class="str">"cmt">//retorna el time de la vela encontrada</span> } } <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 &newblock, <span class="keyword">class="type">class="kw">datetime</span> end) { <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); <span class="functions">NormalizeDouble</span>(newblock.price2, <span class="predefines">_Digits</span>); <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span> ; i < endIndex - <span class="number">class="num">2</span> ; i++) {
◍ 订单块回补判定与数组增删的底层实现
检测看跌订单块是否被回补,核心是比较价格与历史 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 模拟盘验证信号节奏。
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 线。
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] 会指向最旧数据,订单块锚点可能整段错位。外汇与贵金属杠杆高,这类对象泄漏在实盘可能拖累终端响应,建议每次卸载前手动调一次清理函数验证。
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);