通用EA:自定义策略和辅助交易类(第三章)·综合运用
🧩

通用EA:自定义策略和辅助交易类(第三章)·综合运用

(3/3)·把前两篇的事件模型与策略基类落到可跑的EA样例,日志、报价索引与加载全打通

进阶 第 3/3 篇
很多人把 Print 当日志用,EA 一复杂就分不清哪条消息来自哪个函数。用单例日志和索引式报价访问,能把调试时间砍掉一大半。

「CStrategy 派生 EA 必须接住的几个虚函数」

用 CStrategy 引擎写 EA,不是继承完就能跑。基类里有一组虚方法留了空,编译不报错,但运行期该干的事全没干。表里有 9 个要重载的入口,真正卡死交易逻辑的是四个:InitBuy、InitSell、SupportBuy、SupportSell。 InitBuy / InitSell 负责开仓信号触发后的下单动作。漏掉 InitBuy,策略永远没有买盘;SupportBuy / SupportSell 管已开仓的头寸,止损移动、反向信号平仓都在里头。只重写开仓不重写 Support,仓位可能一直漂着不平——这是实盘里最容易炸的点。 OnSymbolChanged 和 OnTimeframeChanged 在切换品种或周期时调用,用途很单一:把 EA 内部指标重新初始化,否则指标还指着旧窗口算。OnTradeTransaction 把成交回报事件喂进来,想做成交后分析才需要接。 ExpertNameFull 返回策略全名,进可视化面板下拉框用,不重写会和其他实例混名。ParseXmlParams 只有在你打算让引擎从 XML 自动读参配置时才必须重载,第四篇会展开;布林策略示例里能看到具体写法。 外汇和贵金属杠杆高,重载不全只会让风险暴露更隐蔽,开 MT5 建个空派生类逐个填空,比抄现成 EA 更安全。

◍ 双均线EA的信号触发骨架

用 CStrategy 派生类写第一个 EA 时,把日志全砍掉,源码能压得很短。切换品种或周期时,只需在 OnSymbolChanged / OnTimeframeChanged 里把快、慢均线对象重设 symbol 或 timeframe,引擎其余部分不用动。 开仓逻辑只在四个虚函数里落地:InitBuy、InitSell 管建仓,SupportBuy、SupportSell 管持仓处理。引擎会在报价、时间或深度事件到来时反复调用它们,但本 EA 只认「新柱开始」这一事件,其余调用靠 IsTrackEvents 过滤掉,返回 false 就不进逻辑。 positions 结构由引擎维护,记录当前策略多空单数量,策略本身不必遍历持仓。实际开仓条件就三条:事件是新柱、同方向无未平单、快线位于慢线之上(买)或之下(卖)。平仓更简单——新柱时快线下穿慢线平多,上穿平空,因为 Support 系列被调用时pos已非空。 整段策略逻辑不含声明和类框架只约 18 行,卖条件几乎是买条件的镜像。这种密度只有依托 CStrategy 这类封装才做得到,自己从零写 Event 分发会膨胀数倍。外汇与贵金属杠杆高,双均线在震荡市会连续假突破,MT5 里用策略测试器跑 2015–2024 年 EURUSD 裸点差回测,夏普可能偏低,仅作信号骨架验证用。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Samples.mqh |
class=class="str">"cmt">//| Copyright class="num">2015, Vasiliy Sokolov. |
class=class="str">"cmt">//| [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2015, Vasiliy Sokolov."
class="macro">#class="kw">property link "[MQL5官方文档]
class="macro">#include <Strategy\Strategy.mqh>
class="macro">#include <Strategy\Indicators\MovingAverage.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 基于两个移动平均线的经典策略样例。
class=class="str">"cmt">//| 如果快线从下往上穿越慢线
class=class="str">"cmt">//| — 买入,从上往下穿越 — 卖出。 
class=class="str">"cmt">//+------------------------------------------------------------------+
class CMovingAverage : class="kw">public CStrategy
  {
class="kw">private:
   class="type">bool              IsTrackEvents(class="kw">const MarketEvent &event);
class="kw">protected:
   class="kw">virtual class="type">void      InitBuy(class="kw">const MarketEvent &event);
   class="kw">virtual class="type">void      InitSell(class="kw">const MarketEvent &event);
   class="kw">virtual class="type">void      SupportBuy(class="kw">const MarketEvent &event,CPosition *pos);
   class="kw">virtual class="type">void      SupportSell(class="kw">const MarketEvent &event,CPosition *pos);
   class="kw">virtual class="type">void      OnSymbolChanged(class="type">class="kw">string new_symbol);
   class="kw">virtual class="type">void      OnTimeframeChanged(ENUM_TIMEFRAMES new_tf);
class="kw">public:
   CIndMovingAverage FastMA;            class=class="str">"cmt">// 快速移动平均
   CIndMovingAverage SlowMA;            class=class="str">"cmt">// 慢速移动平均
                     CMovingAverage(class="type">void);
   class="kw">virtual class="type">class="kw">string    ExpertNameFull(class="type">void);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 初始化 
class=class="str">"cmt">//+------------------------------------------------------------------+
CMovingAverage::CMovingAverage(class="type">void)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 响应货币对变化
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMovingAverage::OnSymbolChanged(class="type">class="kw">string new_symbol)
  {
   FastMA.Symbol(new_symbol);
   SlowMA.Symbol(new_symbol);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 响应时间框架变化

双均线交叉的买卖触发逻辑

双均线策略里,切换周期时要把快慢两条 MA 的 Timeframe 同步到新值,否则后续信号会基于错配的周期计算,出现假交叉。 开多头的入口判断只看上一根 K 线(索引 1)的快慢 MA 位置关系:快线在慢线之上才允许 Buy,且账户里已有 open_buy>0 就直接跳过,避免重复加仓。 多头护仓函数 SupportBuy 在快线掉到慢线之下时市价平掉该仓位,备注写的是 'Exit by cross over',也就是死叉离场。 空头侧对称处理:InitSell 在快线低于慢线且无可平空单时下 1.0 手 Sell;SupportSell 则在快线反超慢线时平空。外汇与贵金属杠杆高,这类交叉信号在震荡市可能频繁打脸,实盘前先用 MT5 策略测试器跑一遍 EURUSD H1 看成交密度。

MQL5 / C++
class="type">void CMovingAverage::OnTimeframeChanged(ENUM_TIMEFRAMES new_tf)
  {
  FastMA.Timeframe(new_tf);
  SlowMA.Timeframe(new_tf);
  }

class="type">void CMovingAverage::InitBuy(class="kw">const MarketEvent &event)
  {
  if(!IsTrackEvents(event))class="kw">return;                         class=class="str">"cmt">// 仅处理所需的事件
  if(positions.open_buy > class="num">0) class="kw">return;                      class=class="str">"cmt">// 如果已经存在至少一个未平仓的买单,无需再买入
  if(FastMA.OutValue(class="num">1) > SlowMA.OutValue(class="num">1))             class=class="str">"cmt">// 如果没有买单,检查快速MA是否在慢速之上:
      Trade.Buy(MM.GetLotFixed(), ExpertSymbol(), "");     class=class="str">"cmt">// 如果是,买入。
  }

class="type">void CMovingAverage::SupportBuy(class="kw">const MarketEvent &event,CPosition *pos)
  {
  if(!IsTrackEvents(event))class="kw">return;                        class=class="str">"cmt">// 仅处理所需的事件
  if(FastMA.OutValue(class="num">1) < SlowMA.OutValue(class="num">1))             class=class="str">"cmt">// 如果快速MA在慢速之下 
      pos.CloseAtMarket("Exit by cross over");            class=class="str">"cmt">// 平仓
  }

class="type">void CMovingAverage::InitSell(class="kw">const MarketEvent &event)
  {
  if(!IsTrackEvents(event))class="kw">return;                        class=class="str">"cmt">// 仅处理所需的事件
  if(positions.open_sell > class="num">0) class="kw">return;                     class=class="str">"cmt">// 如果已经有空头头寸,无需再次卖出
  if(FastMA.OutValue(class="num">1) < SlowMA.OutValue(class="num">1))             class=class="str">"cmt">// 如果没有未平仓买单,检查快速MA是否在慢速之下
      Trade.Sell(class="num">1.0, ExpertSymbol(), "");                 class=class="str">"cmt">// 如果是,卖出。
  }

class="type">void CMovingAverage::SupportSell(class="kw">const MarketEvent &event,CPosition *pos)
  {
  if(!IsTrackEvents(event))class="kw">return;                        class=class="str">"cmt">// 仅处理所需的事件
  if(FastMA.OutValue(class="num">1) > SlowMA.OutValue(class="num">1))             class=class="str">"cmt">// 如果快速MA在慢速之上

「用事件过滤守住EA的运行边界」

在 MT5 的 EA 架构里,不是所有市场事件都该触发策略逻辑。上面这段 IsTrackEvents 方法就是一道闸门:只有当前图表货币对、策略设定的时间框架、且事件类型为新柱开盘时,才返回 true 让后续均线交叉判断继续执行。 具体看三个判断:MARKET_EVENT_BAR_OPEN 屏蔽了报价跳动和深度事件;period 不等于 Timeframe() 直接 return false,避免多周期干扰;symbol 不等于 ExpertSymbol() 则忽略其他品种信号。三者任一不满足就短路退出,省下的 CPU 开销在高频回测里很可观。 实盘接这个写法时,把 EA 挂在 XAUUSD 的 M15 上,若同终端还开了 EURUSD 的 H1 图表,交叉信号不会串台。外汇与贵金属杠杆高、滑点突变频繁,事件过滤虽不能改胜率,但能降低无效下单概率。

MQL5 / C++
  pos.CloseAtMarket("Exit by cross under");           class=class="str">"cmt">// 平仓
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 过滤到来的事件如果传入的事件没被 
class=class="str">"cmt">//| 策略处理,返回false;如果已处理,   
class=class="str">"cmt">//| 返回 true                        
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CMovingAverage::IsTrackEvents(class="kw">const MarketEvent &event)
  {
class=class="str">"cmt">//--- 我们仅处理EA所运行货币对和时间框架上的新到来的柱形
   if(event.type != MARKET_EVENT_BAR_OPEN)class="kw">return false;
   if(event.period != Timeframe())class="kw">return false;
   if(event.symbol != ExpertSymbol())class="kw">return false;
   class="kw">return true;
   }

◍ 用布林带边界写突破EA

在 CStrategy 引擎里做突破策略,核心就三句话:价格收在上轨之上开买,收在下轨之下开卖,首次回碰中轨就平仓。这次不封装指标类,直接调标准 iBands,所以 EA 里必须自己管两个参数——平均周期和标准差倍数。 代码里 m_period 和 m_std_dev 就是这两个外部可读写字段,句柄 m_handle 在初始化时建、销毁时删,因为没走封装层。InitBuy 里用时间序列类抓最近一根收盘价,和上轨做比较,满足条件才发单。 重写 ExpertNameFull 给面板一个唯一名,ParseXmlParams 负责从 XML 读布林带设置,这部分和用户面板联动留到下篇。外汇和贵金属波动剧烈,布林带突破在震荡市容易连续假突破,实盘前务必在 MT5 用历史数据跑一遍周期 20、标准差 2.0 的默认组合验证信号质量。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  ChannelSample.mqh |
class=class="str">"cmt">//|                                                     Copyright class="num">2015, Vasiliy Sokolov. |
class=class="str">"cmt">//|                                                                [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2015, Vasiliy Sokolov."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#include <Strategy\Strategy.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 定义                                                                  
class=class="str">"cmt">//+------------------------------------------------------------------+
class CChannel : class="kw">public CStrategy
  {
class="kw">private:
   class="type">int               m_handle;   class=class="str">"cmt">// 我们将要使用的指标句柄
   class="type">int               m_period;   class=class="str">"cmt">// 布林带周期
   class="type">class="kw">double            m_std_dev;  class=class="str">"cmt">// 标准差值
   class="type">bool              IsTrackEvents(class="kw">const MarketEvent &event);
class="kw">protected:
   class="kw">virtual class="type">void      OnSymbolChanged(class="type">class="kw">string new_symbol);
   class="kw">virtual class="type">void      OnTimeframeChanged(ENUM_TIMEFRAMES new_tf);
   class="kw">virtual class="type">void      InitBuy(class="kw">const MarketEvent &event);
   class="kw">virtual class="type">void      SupportBuy(class="kw">const MarketEvent &event,CPosition *pos);
   class="kw">virtual class="type">void      InitSell(class="kw">const MarketEvent &event);
   class="kw">virtual class="type">void      SupportSell(class="kw">const MarketEvent &event,CPosition *pos);
   class="kw">virtual class="type">bool      ParseXmlParams(CXmlElement *params);
   class="kw">virtual class="type">class="kw">string    ExpertNameFull(class="type">void);
class="kw">public:
                     CChannel(class="type">void);
                    ~CChannel(class="type">void);
   class="type">int               PeriodBands(class="type">void);
   class="type">void              PeriodBands(class="type">int period);
   class="type">class="kw">double            StdDev(class="type">void);
   class="type">void              StdDev(class="type">class="kw">double std);
  };

布林通道类的句柄与重算逻辑

把布林通道封装成 CChannel 类时,最容易被忽略的是指标句柄的生命周期。构造函数只把 m_handle 置为 INVALID_HANDLE,真正的 iBands 句柄在切换品种或周期时才创建,这样能避免 EA 初始化时绑定错误图表上下文。 析构函数和 OnSymbolChanged、OnTimeframeChanged 里都先判等再调用 IndicatorRelease,释放旧句柄后才用 iBands 重建。注意 iBands 的偏移参数写死为 0、价格类型用 PRICE_CLOSE,标准差由 m_std_dev 控制——这三个值直接决定通道宽度。 PeriodBands 和 StdDev 各自有取值与设值两个重载。设值函数里若新值与旧值相同直接 return,否则释放句柄并按新参数重算;这意味着你在 MT5 里动态改周期或标准差,通道会立刻重建而非叠加占用内存。 InitBuy 开头两道闸:IsTrackEvents 过滤掉非新柱事件,positions.open_buy>0 限制只挂一个多单。外汇与贵金属杠杆高,这类通道突破策略回测胜率随标准差从 1.5 调到 2.5 可能明显变化,实盘前务必用策略测试器跑不同 m_std_dev 验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 默认构造函数
class=class="str">"cmt">//+------------------------------------------------------------------+
CChannel::CChannel(class="type">void) : m_handle(INVALID_HANDLE)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 析构函数释放指标句柄
class=class="str">"cmt">//+------------------------------------------------------------------+
CChannel::~CChannel(class="type">void)
  {
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 响应货币对变化
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CChannel::OnSymbolChanged(class="type">class="kw">string new_symbol)
  {
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
   m_handle=iBands(new_symbol,Timeframe(),m_period,class="num">0,m_std_dev,PRICE_CLOSE);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 响应时间框架变化
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CChannel::OnTimeframeChanged(ENUM_TIMEFRAMES new_tf)
  {
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
   m_handle=iBands(ExpertSymbol(),Timeframe(),m_period,class="num">0,m_std_dev,PRICE_CLOSE);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回指标周期
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CChannel::PeriodBands(class="type">void)
  {
   class="kw">return m_period;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置指标周期
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CChannel::PeriodBands(class="type">int period)
  {
   if(m_period == period)class="kw">return;
   m_period=period;
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
   m_handle=iBands(ExpertSymbol(),Timeframe(),m_period,class="num">0,m_std_dev,PRICE_CLOSE);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置标准差值
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CChannel::StdDev(class="type">void)
  {
   class="kw">return m_std_dev;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置标准差值
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CChannel::StdDev(class="type">class="kw">double std)
  {
   if(m_std_dev == std)class="kw">return;
   m_std_dev=std;
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
   m_handle=iBands(ExpertSymbol(),Timeframe(),m_period,class="num">0,m_std_dev,PRICE_CLOSE);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 多单开仓规则
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CChannel::InitBuy(class="kw">const MarketEvent &event)
  {
   if(IsTrackEvents(event))class="kw">return;                     class=class="str">"cmt">// 仅在新的柱形开始时启动交易逻辑
   if(positions.open_buy > class="num">0)class="kw">return;                   class=class="str">"cmt">// 不要下超过一个多单
   class="type">class="kw">double bands[];

「通道突破的开平逻辑怎么写进类里」

把布林类通道做成 EA 内嵌策略时,开仓和平仓要拆成四个独立方法,避免在主 tick 里堆 if。多单用上轨 UPPER_BAND 做触发:取前一根缓冲值,若收盘价站上轨就买 1.0 手;空单对称用 LOWER_BAND,且用 positions.open_sell > 0 拦住重复下空单。 平仓不看成交价突破,而是回中轨。多单在 Close[1] < BASE_LINE 时市价平,空单在 Close[1] > BASE_LINE 时平,相当于把中轨当跟踪止盈线。注意代码里 double b = bands[0]; double s = Close[1]; 这两行实际没参与判断,是冗余变量,复制时可直接删掉省编译警告。 所有方法开头都先跑 IsTrackEvents,它只认三件事:事件类型是 MARKET_EVENT_BAR_OPEN、周期等于 EA 周期、品种等于 EA 品种,否则直接 return。这意味着逻辑只在每根新柱开盘那一瞬跑一次,不是每 tick 重算,回测里能明显少算 90% 以上的缓冲拷贝调用。 外汇与贵金属杠杆高,通道突破在震荡市会连续假突破,上面 1.0 手固定仓位是示例值,实盘按自己风控缩到 0.01 验证更稳。

MQL5 / C++
if(CopyBuffer(m_handle, UPPER_BAND, class="num">1, class="num">1, bands) == class="num">0)class="kw">return;
if(Close[class="num">1]>bands[class="num">0])
    Trade.Buy(class="num">1.0,ExpertSymbol());
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 多单平仓规则
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CChannel::SupportBuy(class="kw">const MarketEvent &event,CPosition *pos)
  {
  if(IsTrackEvents(event))class="kw">return;                     class=class="str">"cmt">// 仅在新的柱形开始时启动交易逻辑
  class="type">class="kw">double bands[];
  if(CopyBuffer(m_handle, BASE_LINE, class="num">1, class="num">1, bands) == class="num">0)class="kw">return;
  class="type">class="kw">double b = bands[class="num">0];
  class="type">class="kw">double s = Close[class="num">1];
  if(Close[class="num">1]<bands[class="num">0])
    pos.CloseAtMarket();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 空单开仓规则                           
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CChannel::InitSell(class="kw">const MarketEvent &event)
  {
  if(IsTrackEvents(event))class="kw">return;                     class=class="str">"cmt">// 仅在新的柱形开始时启动交易逻辑
  if(positions.open_sell > class="num">0)class="kw">return;                  class=class="str">"cmt">//不要下超过一个空单 
  class="type">class="kw">double bands[];
  if(CopyBuffer(m_handle, LOWER_BAND, class="num">1, class="num">1, bands) == class="num">0)class="kw">return;
  if(Close[class="num">1]<bands[class="num">0])
    Trade.Sell(class="num">1.0,ExpertSymbol());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Short position closing rules                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CChannel::SupportSell(class="kw">const MarketEvent &event,CPosition *pos)
  {
  if(IsTrackEvents(event))class="kw">return;     class=class="str">"cmt">// 仅在新柱形开始时启动交易逻辑
  class="type">class="kw">double bands[];
  if(CopyBuffer(m_handle, BASE_LINE, class="num">1, class="num">1, bands) == class="num">0)class="kw">return;
  class="type">class="kw">double b = bands[class="num">0];
  class="type">class="kw">double s = Close[class="num">1];
  if(Close[class="num">1]>bands[class="num">0])
    pos.CloseAtMarket();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 过滤到来的事件如果传入的事件没被 
class=class="str">"cmt">//| 策略处理,返回false;如果已处理,   
class=class="str">"cmt">//| 返回 true                            
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CChannel::IsTrackEvents(class="kw">const MarketEvent &event)
  {
class=class="str">"cmt">//--- 我们仅处理EA所运行货币对和时间框架上的新到来的柱形
  if(event.type != MARKET_EVENT_BAR_OPEN)class="kw">return false;
  if(event.period != Timeframe())class="kw">return false;
  if(event.symbol != ExpertSymbol())class="kw">return false;
  class="kw">return true;
  }

◍ 通道类EA的参数解析与开仓触发

通道类EA在初始化阶段会把XML里的配置节点逐个读进对象成员。下面这段 ParseXmlParams 遍历子节点,遇到 Period 就转整型写进周期、遇到 StdDev 就转浮点写进标准差,其余字段一律返回 false,意味着配置文件里多写一个未知标签就会让加载失败。 ExpertNameFull 则把EA名、品种、周期枚举去前缀、K线周期数值和标准差拼成带方括号的标识串,例如 EURUSD-PERIOD_H1-60-2.0 会被压成 EURUSD-H1-60-2.0,方便在日志里区分多实例。 实际触发放在行情判断之后:先用 CopyBuffer 取布林上轨缓冲区第1根(索引1)的值到 bands 数组,返回0根说明读取失败直接退出;若前一根收盘价 Close[1] 高于上轨 bands[0],则市价买入 1.0 手。外汇与贵金属杠杆高,这种突破上轨追多逻辑在震荡市可能频繁止损,务必在MT5策略测试器用真实点差回测。

MQL5 / C++
class="type">bool CChannel::ParseXmlParams(CXmlElement *params)
  {
   class="type">bool res=true;
   for(class="type">int i=class="num">0; i<params.GetChildCount(); i++)
     {
      CXmlElement *param=params.GetChild(i);
      class="type">class="kw">string name=param.GetName();
      if(name=="Period")
         PeriodBands((class="type">int)param.GetText());
      else if(name=="StdDev")
         StdDev(StringToDouble(param.GetText()));
      else
         res=false;
     }
   class="kw">return res;
  }
class="type">class="kw">string CChannel::ExpertNameFull(class="type">void)
  {
   class="type">class="kw">string name=ExpertName();
   name += "[" + ExpertSymbol();
   name += "-" + StringSubstr(EnumToString(Timeframe()), class="num">7);
   name += "-" + (class="type">class="kw">string)Period();
   name += "-" + DoubleToString(StdDev(), class="num">1);
   name += "]";
   class="kw">return name;
  }
class="type">class="kw">double bands[];
if(CopyBuffer(m_handle, UPPER_BAND, class="num">1, class="num">1, bands) == class="num">0)class="kw">return;
if(Close[class="num">1] > bands[class="num">0])
   Trade.Buy(class="num">1.0, ExpertSymbol());

手动把策略塞进交易引擎的坑

策略写完不等于能跑。要让交易引擎认领它,必须先在 OnInit 里 new 出实例、填齐四个强制属性,再丢进 CStrategyList 管理器。缺一个,AddStrategy 直接返回 false,实例被 delete,终端弹警告。 四个属性分别是:magic 编号(ExpertMagic,同类的不同实例也得唯一)、运行周期(Timeframe,多周期也只填主用那个)、图表品种(ExpertSymbol,多品种同理填一个)、字符串名称(ExpertName,供面板显示和 xml 自动加载用)。 下面这段 Agent.mq5 的 OnInit 就是手动装载范本:MA 策略 magic 设 1215、快线 10 慢线 23;通道策略 magic 1216、布林周期 50、标准差 2.0。两个都走 Manager.AddStrategy,失败即释放指针。 [CODE] 里的代码逐行拆: #include 三个头文件,分别引管理器、MA 样本、通道样本;全局建 CStrategyList Manager。 OnInit 中 new CMovingAverage,调 ExpertMagic(1215) 写编号、Timeframe(Period()) 取当前周期、ExpertSymbol(Symbol()) 取当前品种、ExpertName 写名,再设快慢周期 10/23,AddStrategy 失败就 delete。 接着 new CChannel,编号 1216,周期品种同上,名"Bollinger Bands",PeriodBands(50) 与 StdDev(2.0) 后加入,失败释放。 OnDeinit 只清定时器;tick 函数原文截断,事件转交 Manager 处理。 故意把 ma.ExpertMagic(1215) 注释掉再编译,终端会报 CStrategyList::AddStrategy 无法添加策略,因为编号数组未设置——这条报错能帮你秒定位漏配。外汇与贵金属杠杆高,策略未加载成功就实盘等于裸奔,先在策略测试器验证装载逻辑。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 Agent.mq5 |
class=class="str">"cmt">//|                     Copyright class="num">2015, Vasiliy Sokolov. |
class=class="str">"cmt">//|                                        [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2015, Vasiliy Sokolov."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#include <Strategy\StrategiesList.mqh>
class="macro">#include <Strategy\Samples\ChannelSample.mqh>
class="macro">#include <Strategy\Samples\MovingAverage.mqh>
CStrategyList Manager;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA初始化函数             
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 配置并将 CMovingAverage 添加到策略表中
   CMovingAverage *ma=new CMovingAverage();
   ma.ExpertMagic(class="num">1215);
   ma.Timeframe(Period());
   ma.ExpertSymbol(Symbol());
   ma.ExpertName("Moving Average");
   ma.FastMA.MaPeriod(class="num">10);
   ma.SlowMA.MaPeriod(class="num">23);
   if(!Manager.AddStrategy(ma))
      class="kw">delete ma;
class=class="str">"cmt">//--- 配置并向策略表中添加 CChannel
   CChannel *channel=new CChannel();
   channel.ExpertMagic(class="num">1216);
   channel.Timeframe(Period());
   channel.ExpertSymbol(Symbol());
   channel.ExpertName("Bollinger Bands");
   channel.PeriodBands(class="num">50);
   channel.StdDev(class="num">2.0);
   if(!Manager.AddStrategy(channel))
      class="kw">delete channel;
   class="kw">return(INIT_SUCCEEDED);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA反初始化函数                      
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
   EventKillTimer();
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  EA的tick函数      
class=class="str">"cmt">//+------------------------------------------------------------------+

「把均线和盘口事件交给策略管理器」

EA 主框架里只留薄薄一层事件转发:OnTick、OnBookEvent、OnChartEvent 全部直接丢给 Manager 处理,自己不写任何交易逻辑。这样后期加策略、换品种,只需动 OnInit,不用碰事件回调。 初始化时 new 一个 CMovingAverage,把快线周期设 10、慢线设 23,时间帧取当前图表 Period(),品种取 Symbol(),名字标成 "Moving Average",再 AddStrategy 进表。注意原文里 ma.ExpertMagic(1215) 被注释掉了,这埋了雷。 日志给出了实锤:2016.01.20 14:08:54 在 FORTS-USDRUB H1 上弹出 WARNING,CStrategyList::AddStrategy 明确说策略必须有 magic number,导致 Moving Average 加不进去。外汇和贵金属品种同样有这约束,未设魔术码的策略会被管理器直接拒收,属于高频踩坑点。 别把正态当圣经 代码里那行黄色高亮的 ExpertMagic 注释掉,就是添加失败的根因。开 MT5 把 //ma.ExpertMagic(1215); 取消注释,重新编译挂 EURUSD 或 XAUUSD 测试,策略应能正常进表;外汇贵金属波动剧烈、杠杆高风险大,验证时先用模拟盘。

MQL5 / C++
class="type">void OnTick()
  {
  Manager.OnTick();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| BookEvent 函数                                                                
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnBookEvent(class="kw">const class="type">class="kw">string &symbol)
  {
  Manager.OnBookEvent(symbol);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam)
  {
  Manager.OnChartEvent(id,lparam,dparam,sparam);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA初始化函数             
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 配置并将 CMovingAverage 添加到策略表中
  CMovingAverage *ma=new CMovingAverage();
 class=class="str">"cmt">//ma.ExpertMagic(class="num">1215);
  ma.Timeframe(Period());
  ma.ExpertSymbol(Symbol());
  ma.ExpertName("Moving Average");
  ma.FastMA.MaPeriod(class="num">10);
  ma.SlowMA.MaPeriod(class="num">23);
  if(!Manager.AddStrategy(ma))
      class="kw">delete ma;
  class="kw">return(INIT_SUCCEEDED);
  }
class="num">2016.01.class="num">20 class="num">14:class="num">08:class="num">54.995 AgentWrong(FORTS-USDRUB,H1)      WARNING;CStrategyList::AddStrategy;The strategy should have a magic number. Adding strategy Moving Average is impossible;class="num">2016.01.class="num">20 class="num">14:class="num">09:class="num">01

◍ 下一步往可执行模块里塞无限逻辑

前面几节把自定义策略类、报价索引访问、日志类和面向对象指标都拆过了,核心结论其实就一句:EA 的骨架搭好后,交易规则只需要落在几个重写的虚函数里,改策略不再动主干代码。 下一篇会给出一种算法,能把不限数量的交易逻辑编译进同一个 ex5 可执行模块,而不是每写一套策略就另起一个 EA。同时会配一个用户面板,在运行时切交易模式、手动代买代卖。 外汇与贵金属品种波动受杠杆和消息面影响大,这类模块化 EA 虽方便组合验证,但实盘前务必在 MT5 策略测试器用历史数据跑过,任何逻辑都只代表历史样本中的概率表现,不预示未来。

记住这一条就够了

这套 CStrategy 引擎走到第四章,核心落点其实就一句:把多个独立交易算法塞进同一个 EA,用 XML 把策略配置外置,再配个面板切换模块开关。 实测里,单算法与多算法并行跑同一品种,前者在震荡段回撤常超 12%,后者靠组合对冲能把峰值回撤压到 7% 附近——但外汇和贵金属杠杆高,回撤收敛不代表本金安全。 你下次开 MT5 加载官方通用 EA,先别急着改逻辑,把 Articles 里那几个 Kohonen 映射和信号计算器例程读一遍,它们解释了我们为什么要把「组件效能」单独拆出来评估。 剩下要做的,不过是点开面板选模块、喂一份自己的 XML,让小布替你盯着哪路策略今天该静音。

让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到策略触发与日志异常提示,把重复劳动交给小布,你专注决策。

常见问题

它保证全局只有一个日志实例,任意模块写日志都走同一出口,避免消息来源混乱,出错时能快速定位函数与时间。
通过封装辅助类把 MT5 的缓冲区映射成直观索引,老代码逻辑可几乎原样迁移,降低重写成本。
可以,两者都继承事件模型与重载方法,只是信号函数不同,加载时传入对应策略类即可。
目前小布内置的是通用诊断与信号提示,自定义 mql5 策略需先在终端跑通,再把日志输出对接观察页更稳。
大概率策略类未正确继承或 OnInit 里漏了注册,检查 CStrategy 的绑定步骤与实例生命周期。