MQL5自动化交易策略(第十九部分):包络线趋势反弹剥头皮交易交易执行与风险管理(下篇)·进阶篇
⚙️

MQL5自动化交易策略(第十九部分):包络线趋势反弹剥头皮交易交易执行与风险管理(下篇)·进阶篇

(2/3)· 接上篇架构,本篇落地 MQL5 交易执行与动态仓位,让 EA 真正下单且控亏

新手友好 第 2/3 篇

接上篇,我们继续深挖包络线趋势反弹剥头皮系统——第十八部分只搭了信号骨架,这一篇要把它接上交易执行与风险管理的血肉。很多自建 EA 卡在「能看不能做」,根源就在订单触发和仓位控制没写透。下面直接进架构与实现。

「信号容器的内存与索引管理」

在 MT5 里做多信号策略组合,先得有一个能装信号的容器。下面这段类实现用动态数组 _tradeSignals 存指针,并用 _size 记录实际信号数、_pointer 做游标,初始构造时两者分别置 -1 和 0。 Add 方法每次把 _size 加 1,再用 ArrayResize(_tradeSignals, _size, 8) 扩容——末尾的 8 是预留内存增量,连续加信号时能少几次重分配。信号指针塞进 _tradeSignals[_size-1],即末位。 Remove 会先校验 index 在 [0, _size) 内,把待删指针暂存,随后把后方元素整体前移一位覆盖,再 ArrayResize 缩容并 _size 减 1;越界调用直接返回 NULL 不报错。析构里跑一遍 delete 释放每个信号对象,避免 EA 退出时漏内存。 外汇与贵金属杠杆高、滑点跳空频繁,这类容器在实盘重载或定时器里反复增删时,若忘了析构释放,跑几天可能把终端内存吃满。开 MT5 把这段粘进类里,故意 Add 100 个假信号再 Remove 中间 50 个,用 ArraySize 打印验证预留逻辑是否如预期。

MQL5 / C++
class="type">int _size;                                                                       class=class="str">"cmt">//--- Track number of signals
class="kw">public:
  class=class="str">"cmt">//--- Initialize empty signal collection
  class="type">void TradeSignalCollection() {
    _pointer = -class="num">1;                                                                 class=class="str">"cmt">//--- Set initial pointer to -class="num">1
    _size = class="num">0;                                                                     class=class="str">"cmt">//--- Set initial size to class="num">0
  }
  class=class="str">"cmt">//--- Destructor to clean up signals
  class="type">void ~TradeSignalCollection() {
    for (class="type">int i = class="num">0; i < ArraySize(_tradeSignals); i++) { class=class="str">"cmt">//--- Iterate signals
      class="kw">delete(_tradeSignals[i]);                                                     class=class="str">"cmt">//--- Delete each signal
    }
  }
  class=class="str">"cmt">//--- Add signal to collection
  class="type">void Add(IAdvisorStrategyExpression* item) {
    _size = _size + class="num">1;                                                             class=class="str">"cmt">//--- Increment size
    ArrayResize(_tradeSignals, _size, class="num">8);                                          class=class="str">"cmt">//--- Resize array with reserve
    _tradeSignals[(_size - class="num">1)] = item;                                             class=class="str">"cmt">//--- Store signal at last index
  }
  class=class="str">"cmt">//--- Remove signal at index
  IAdvisorStrategyExpression* Remove(class="type">int index) {
    IAdvisorStrategyExpression* removed = NULL;                                   class=class="str">"cmt">//--- Initialize removed signal as null
    if (index >= class="num">0 && index < _size) {                                            class=class="str">"cmt">//--- Check valid index
      removed = _tradeSignals[index];                                             class=class="str">"cmt">//--- Store signal to remove
      for (class="type">int i = index; i < (_size - class="num">1); i++) {                                 class=class="str">"cmt">//--- Shift signals left
        _tradeSignals[i] = _tradeSignals[i + class="num">1];                                   class=class="str">"cmt">//--- Move signal
      }
      ArrayResize(_tradeSignals, ArraySize(_tradeSignals) - class="num">1, class="num">8);                 class=class="str">"cmt">//--- Reduce array size
      _size = _size - class="num">1;                                                           class=class="str">"cmt">//--- Decrement size
    }
    class="kw">return removed;                                                               class=class="str">"cmt">//--- Return removed signal or null
  }
  class=class="str">"cmt">//--- Retrieve signal at index
  IAdvisorStrategyExpression* Get(class="type">int index) {

信号容器的越界与游标控制

把策略信号塞进一个定长数组后,最容易被忽视的是下标越界和游标状态。下面这段实现用 _size 约束访问边界,越界直接返回 NULL,而不是抛异常或读脏内存。 取单条信号时先判 index >= 0 && index < _size,成立才返回 _tradeSignals[index],否则返回 NULL。这套写法在 EA 回测里能避免历史信号数组长度变动导致的数组越界报错(MT5 默认数组越界会写日志并中断当前 tick)。 游标类方法才是实际遍历用的:Rewind()_pointer 设为 -1;Next() 先自增再判断是否等于 _size,到尾了就重置并返回 NULL;Prev() 自减后若落到 -1 也返回 NULL。HasNext()_pointer < (_size - 1) 判断,注意它不移动指针,只探路。 外汇与贵金属杠杆高、滑点跳空频繁,这类容器若在实盘重算信号时复用旧 _size,可能漏掉最新 K 线信号或重复触发,建议在 OnTick 里先 Count() 核对长度再遍历。

MQL5 / C++
if (index >= class="num">0 && index < _size) {              class=class="str">"cmt">//--- Check valid index
     class="kw">return _tradeSignals[index];                 class=class="str">"cmt">//--- Return signal
   }
   class="kw">return NULL;                                   class=class="str">"cmt">//--- Return null for invalid index
  }
  class=class="str">"cmt">//--- Retrieve number of signals
  class="type">int Count() {
    class="kw">return _size;                                 class=class="str">"cmt">//--- Return current size
  }
  class=class="str">"cmt">//--- Reset iterator to start
  class="type">void Rewind() {
    _pointer = -class="num">1;                                class=class="str">"cmt">//--- Set pointer to -class="num">1
  }
  class=class="str">"cmt">//--- Move to next signal
  IAdvisorStrategyExpression* Next() {
    _pointer++;                                   class=class="str">"cmt">//--- Increment pointer
    if (_pointer == _size) {                      class=class="str">"cmt">//--- Check if at end
      Rewind();                                   class=class="str">"cmt">//--- Reset pointer
      class="kw">return NULL;                                class=class="str">"cmt">//--- Return null
    }
    class="kw">return Current();                             class=class="str">"cmt">//--- Return current signal
  }
  class=class="str">"cmt">//--- Move to previous signal
  IAdvisorStrategyExpression* Prev() {
    _pointer--;                                   class=class="str">"cmt">//--- Decrement pointer
    if (_pointer == -class="num">1) {                         class=class="str">"cmt">//--- Check if before start
      class="kw">return NULL;                                class=class="str">"cmt">//--- Return null
    }
    class="kw">return Current();                             class=class="str">"cmt">//--- Return current signal
  }
  class=class="str">"cmt">//--- Check if more signals exist
  class="type">bool HasNext() {
    class="kw">return (_pointer < (_size - class="num">1));              class=class="str">"cmt">//--- Return true if pointer is before end
  }

◍ 信号集合与策略类的内存边界

把开仓和平仓信号按买卖方向拆成四个独立集合,是这套策略框架避免互相污染的关键。AdvisorStrategy 在构造时一次性 new 出 _openBuySignals、_openSellSignals、_closeBuySignals、_closeSellSignals,析构里逐个 delete,MT5 上跑长周期 EA 时不会漏掉释放。 EvaluateASLevel 用 level>0 && level<=signals.Count() 做越界保护,调用方传 1 基的层级,内部立刻减 1 转成 0 基索引再 Evaluate()。若层级非法直接返 false,这意味着你外面拼多层级信号时,错层不会崩只是静默失效。 _current 信号靠 TradeSignalCollection 里的 _pointer 游标定位,Current() 返回指针位置那一条,Key() 把游标下标暴露出来。实盘里想看「现在命中第几层」直接读 Key(),不用自己维护计数。 外汇与贵金属杠杆高,这类信号集合若在非主图指标里频繁 new/delete,要留意 MT5 对对象生命周期的线程限制,建议开策略测试器用 2023 全年 EURUSD M15 跑一遍确认无句柄泄漏。

MQL5 / C++
  class=class="str">"cmt">//--- Retrieve current signal
  IAdvisorStrategyExpression* Current() {
    class="kw">return _tradeSignals[_pointer];              class=class="str">"cmt">//--- Return signal at pointer
  }
  class=class="str">"cmt">//--- Retrieve current iterator index
  class="type">int Key() {
    class="kw">return _pointer;                             class=class="str">"cmt">//--- Return current pointer
  }
};
class=class="str">"cmt">//--- Define class for managing trading signals
class AdvisorStrategy {
class="kw">private:
  TradeSignalCollection* _openBuySignals;        class=class="str">"cmt">//--- Store open Buy signals
  TradeSignalCollection* _openSellSignals;       class=class="str">"cmt">//--- Store open Sell signals
  TradeSignalCollection* _closeBuySignals;       class=class="str">"cmt">//--- Store close Buy signals
  TradeSignalCollection* _closeSellSignals;      class=class="str">"cmt">//--- Store close Sell signals
  class=class="str">"cmt">//--- Evaluate signal at specified level
  class="type">bool EvaluateASLevel(TradeSignalCollection* signals, class="type">int level) {
    if (level > class="num">0 && level <= signals.Count()) { class=class="str">"cmt">//--- Check valid level
      class="kw">return signals.Get(level - class="num">1).Evaluate();  class=class="str">"cmt">//--- Evaluate signal
    }
    class="kw">return false;                                class=class="str">"cmt">//--- Return false for invalid level
  }
class="kw">public:
  class=class="str">"cmt">//--- Initialize strategy
  class="type">void AdvisorStrategy() {
    _openBuySignals = new TradeSignalCollection();    class=class="str">"cmt">//--- Create open Buy signals collection
    _openSellSignals = new TradeSignalCollection();   class=class="str">"cmt">//--- Create open Sell signals collection
    _closeBuySignals = new TradeSignalCollection();   class=class="str">"cmt">//--- Create close Buy signals collection
    _closeSellSignals = new TradeSignalCollection();  class=class="str">"cmt">//--- Create close Sell signals collection
  }
  class=class="str">"cmt">//--- Destructor to clean up signals
  class="type">void ~AdvisorStrategy() {
    class="kw">delete(_openBuySignals);                     class=class="str">"cmt">//--- Delete open Buy signals
    class="kw">delete(_openSellSignals);                    class=class="str">"cmt">//--- Delete open Sell signals
    class="kw">delete(_closeBuySignals);                    class=class="str">"cmt">//--- Delete close Buy signals
    class="kw">delete(_closeSellSignals);                   class=class="str">"cmt">//--- Delete close Sell signals
  }
  class=class="str">"cmt">//--- Retrieve trading advice
  class="type">bool GetAdvice(TradeAction tradeAction, class="type">int level) {
    if (tradeAction == OpenBuyAction) {          class=class="str">"cmt">//--- Check open Buy action

「信号分级注册与动作派发的实现细节」

这段逻辑把开仓、平仓四类动作统一走 EvaluateASLevel 接口,靠 tradeAction 枚举分流。若传入的动作不在 OpenBuy/OpenSell/CloseBuy/CloseSell 之内,直接弹 Alert 并 return false,说明策略层没接住非法指令。 注册函数对 level 做了硬约束:以 RegisterOpenBuy 为例,当 level <= _openBuySignals.Count() 时就报“level already set”并退出,意味着层级必须严格递增、不能覆盖已有槽位。四个 Register 函数(开买、开卖、平买、平卖)结构完全对称,只是操作的信号数组不同。 在 MT5 里跑这套,你可以故意传一个重复 level 验证 Alert 是否触发;外汇与贵金属杠杆高,信号层级错乱可能让 EA 在极端波动中漏单或重复开仓,务必先在策略测试器用历史数据过一遍。

MQL5 / C++
  class="kw">return EvaluateASLevel(_openBuySignals, level);   class=class="str">"cmt">//--- Evaluate Buy signal
  } else if (tradeAction == OpenSellAction) {        class=class="str">"cmt">//--- Check open Sell action
    class="kw">return EvaluateASLevel(_openSellSignals, level); class=class="str">"cmt">//--- Evaluate Sell signal
  } else if (tradeAction == CloseBuyAction) {        class=class="str">"cmt">//--- Check close Buy action
    class="kw">return EvaluateASLevel(_closeBuySignals, level); class=class="str">"cmt">//--- Evaluate close Buy signal
  } else if (tradeAction == CloseSellAction) {       class=class="str">"cmt">//--- Check close Sell action
    class="kw">return EvaluateASLevel(_closeSellSignals, level); class=class="str">"cmt">//--- Evaluate close Sell signal
  } else {
    Alert("Unsupported TradeAction in Advisor Strategy. TradeAction: " + DoubleToStr(tradeAction)); class=class="str">"cmt">//--- Log unsupported action
  }
  class="kw">return false;                                      class=class="str">"cmt">//--- Return false for invalid action
  }
  class=class="str">"cmt">//--- Register open Buy signal
  class="type">void RegisterOpenBuy(IAdvisorStrategyExpression* openBuySignal, class="type">int level) {
    if (level <= _openBuySignals.Count()) {          class=class="str">"cmt">//--- Check if level already set
      Alert("Register Open Buy failed: level already set."); class=class="str">"cmt">//--- Log failure
      class="kw">return;                                        class=class="str">"cmt">//--- Exit
    }
    _openBuySignals.Add(openBuySignal);              class=class="str">"cmt">//--- Add signal
  }
  class=class="str">"cmt">//--- Register open Sell signal
  class="type">void RegisterOpenSell(IAdvisorStrategyExpression* openSellSignal, class="type">int level) {
    if (level <= _openSellSignals.Count()) {         class=class="str">"cmt">//--- Check if level already set
      Alert("Register Open Sell failed: level already set."); class=class="str">"cmt">//--- Log failure
      class="kw">return;                                        class=class="str">"cmt">//--- Exit
    }
    _openSellSignals.Add(openSellSignal);            class=class="str">"cmt">//--- Add signal
  }
  class=class="str">"cmt">//--- Register close Buy signal
  class="type">void RegisterCloseBuy(IAdvisorStrategyExpression* closeBuySignal, class="type">int level) {
    if (level <= _closeBuySignals.Count()) {         class=class="str">"cmt">//--- Check if level already set
      Alert("Register Close Buy failed: level already set."); class=class="str">"cmt">//--- Log failure
      class="kw">return;                                        class=class="str">"cmt">//--- Exit
    }

信号容器的越级保护与批量复位

平仓信号的注册函数和平仓一样做了 level 防重保护:RegisterCloseSell 里若 level 小于等于已注册数量,直接 Alert 报错并 return,避免同一优先级被重复写入导致逻辑错乱。 GetNumberOfExpressions 按 TradeAction 枚举分流,分别返回 _openBuySignals、_openSellSignals、_closeBuySignals、_closeSellSignals 四个容器的 Count(),无效动作返回 0。这一步是策略引擎统计某类动作挂了几条表达式的依据。 SetFireOnlyWhenReset 用 Rewind+Next 遍历多空开仓信号容器,对每条表达式调 SetFireOnlyWhenReset(value)。实盘里若设为 true,信号在触发后必须等条件复位才能再触发,可压住外汇与贵金属_high 波动下的重复开仓噪音,但也可能漏掉连续趋势段,需按品种波动率手调。

MQL5 / C++
  _closeBuySignals.Add(closeBuySignal);                      class=class="str">"cmt">//--- Add signal
  }
  class=class="str">"cmt">//--- Register close Sell signal
  class="type">void RegisterCloseSell(IAdvisorStrategyExpression* closeSellSignal, class="type">int level) {
    if (level <= _closeSellSignals.Count()) {                 class=class="str">"cmt">//--- Check if level already set
       Alert("Register Close Sell failed: level already set."); class=class="str">"cmt">//--- Log failure
       class="kw">return;                                                class=class="str">"cmt">//--- Exit
    }
    _closeSellSignals.Add(closeSellSignal);                   class=class="str">"cmt">//--- Add signal
  }
  class=class="str">"cmt">//--- Retrieve number of signals for action
  class="type">int GetNumberOfExpressions(TradeAction tradeAction) {
    if (tradeAction == OpenBuyAction) {                       class=class="str">"cmt">//--- Check open Buy action
       class="kw">return _openBuySignals.Count();                        class=class="str">"cmt">//--- Return Buy signal count
    } else if (tradeAction == OpenSellAction) {               class=class="str">"cmt">//--- Check open Sell action
       class="kw">return _openSellSignals.Count();                       class=class="str">"cmt">//--- Return Sell signal count
    } else if (tradeAction == CloseBuyAction) {               class=class="str">"cmt">//--- Check close Buy action
       class="kw">return _closeBuySignals.Count();                       class=class="str">"cmt">//--- Return close Buy signal count
    } else if (tradeAction == CloseSellAction) {              class=class="str">"cmt">//--- Check close Sell action
       class="kw">return _closeSellSignals.Count();                      class=class="str">"cmt">//--- Return close Sell signal count
    }
    class="kw">return class="num">0;                                                 class=class="str">"cmt">//--- Return class="num">0 for invalid action
  }
  class=class="str">"cmt">//--- Set fire-only-when-reset for all signals
  class="type">void SetFireOnlyWhenReset(class="type">bool value) {
    _openBuySignals.Rewind();                                 class=class="str">"cmt">//--- Reset Buy signals iterator
    class="kw">while (_openBuySignals.Next() != NULL) {                  class=class="str">"cmt">//--- Iterate Buy signals
       _openBuySignals.Current().SetFireOnlyWhenReset(value); class=class="str">"cmt">//--- Set flag
    }
    _openSellSignals.Rewind();                                class=class="str">"cmt">//--- Reset Sell signals iterator
    class="kw">while (_openSellSignals.Next() != NULL) {                 class=class="str">"cmt">//--- Iterate Sell signals
       _openSellSignals.Current().SetFireOnlyWhenReset(value); class=class="str">"cmt">//--- Set flag
    }

◍ 信号触发开关与逐笔重算的衔接

信号容器里挂了开多、开空、平多、平空四组迭代器,每组在设置阶段都能统一写入 FireOnlyWhenReset 标志。上面这段把 _closeBuySignals 和 _closeSellSignals 先 Rewind 归零游标,再 Next 遍历,对当前信号对象调 SetFireOnlyWhenReset(value) 批量打标,逻辑对称且无遗漏。 到了 HandleTick,四组信号每次都先 Rewind 再循环 ResetSignalValue 清空旧值;只有当 GetFireOnlyWhenReset 返回真时才进 Evaluate 重算。这意味着若某信号被标了“仅重置时触发”,它不会在每个 tick 盲目刷值,只在复位节点参与评估,能压住不必要的重绘与误触。 实盘接这套结构时,建议先在策略测试器里把 value 分别传 true / false 各跑一轮 EURUSD 的 M15 回测,观察信号线闪烁频率差异——外汇与贵金属杠杆高,信号抖动会直接放大滑点成本,参数取向需结合自己的点差环境定。

MQL5 / C++
      _closeBuySignals.Rewind();                                           class=class="str">"cmt">//--- Reset close Buy signals iterator
      class="kw">while (_closeBuySignals.Next() != NULL) {                             class=class="str">"cmt">//--- Iterate close Buy signals
         _closeBuySignals.Current().SetFireOnlyWhenReset(value);            class=class="str">"cmt">//--- Set flag
      }
      _closeSellSignals.Rewind();                                           class=class="str">"cmt">//--- Reset close Sell signals iterator
      class="kw">while (_closeSellSignals.Next() != NULL) {                            class=class="str">"cmt">//--- Iterate close Sell signals
         _closeSellSignals.Current().SetFireOnlyWhenReset(value);           class=class="str">"cmt">//--- Set flag
      }
   }
   class=class="str">"cmt">//--- Handle tick event for signals
   class="type">void HandleTick() {
      _openBuySignals.Rewind();                                             class=class="str">"cmt">//--- Reset Buy signals iterator
      class="kw">while (_openBuySignals.Next() != NULL) {                              class=class="str">"cmt">//--- Iterate Buy signals
         _openBuySignals.Current().ResetSignalValue();                      class=class="str">"cmt">//--- Reset signal value
         if (_openBuySignals.Current().GetFireOnlyWhenReset()) {            class=class="str">"cmt">//--- Check fire-only-when-reset
            _openBuySignals.Current().Evaluate();                           class=class="str">"cmt">//--- Evaluate signal
         }
      }
      _openSellSignals.Rewind();                                            class=class="str">"cmt">//--- Reset Sell signals iterator
      class="kw">while (_openSellSignals.Next() != NULL) {                             class=class="str">"cmt">//--- Iterate Sell signals
         _openSellSignals.Current().ResetSignalValue();                     class=class="str">"cmt">//--- Reset signal value
         if (_openSellSignals.Current().GetFireOnlyWhenReset()) {           class=class="str">"cmt">//--- Check fire-only-when-reset
            _openSellSignals.Current().Evaluate();                          class=class="str">"cmt">//--- Evaluate signal
         }
      }
      _closeBuySignals.Rewind();                                            class=class="str">"cmt">//--- Reset close Buy signals iterator
      class="kw">while (_closeBuySignals.Next() != NULL) {                             class=class="str">"cmt">//--- Iterate close Buy signals
         _closeBuySignals.Current().ResetSignalValue();                     class=class="str">"cmt">//--- Reset signal value
         if (_closeBuySignals.Current().GetFireOnlyWhenReset()) {           class=class="str">"cmt">//--- Check fire-only-when-reset
            _closeBuySignals.Current().Evaluate();                          class=class="str">"cmt">//--- Evaluate signal
         }
      }
      _closeSellSignals.Rewind();                                           class=class="str">"cmt">//--- Reset close Sell signals iterator
      class="kw">while (_closeSellSignals.Next() != NULL) {                            class=class="str">"cmt">//--- Iterate close Sell signals
         _closeSellSignals.Current().ResetSignalValue();                    class=class="str">"cmt">//--- Reset signal value
交给小布盯盘看盘口
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到包络线偏离与 RSI 过滤状态,把重复劳动交给小布,你专注决策。

常见问题

它统一了信号评估规范,用 Evaluate 判激活、Get/SetFireOnlyWhenReset 管触发标识、ResetSignalValue 清状态,让不同信号类型可模块化复用。
小布目前提供的是盘口诊断与信号可视化,不代下单;策略执行仍需你在 MQL5 环境部署 EA,但可借小布减少人工盯偏离的耗时。
常见做法是读 AccountInfoDouble 的余额或权益,按预设风险百分比反推手数,并对单笔与总亏损设上限以保全本金。
继承后各信号可互换、易扩展,tick 处理函数只需面向接口编排,降低耦合也方便后续回测替换过滤器。