开发多币种 EA 交易(第 24 部分):添加新策略(一)·进阶篇
🧩

开发多币种 EA 交易(第 24 部分):添加新策略(一)·进阶篇

(2/3)· 当 SimpleVolumes 不再是唯一标的,库与工作区如何拆分才不拖垮编译与优化流水线

偏理论 第 2/3 篇
直接把新策略源码塞进 Include 库,会让阶段 EA 与最终 EA 的编译边界糊成一团。多数人卡在第二步就放弃接入自动优化,其实缺的只是文件归属与包含指令的清晰划分。

纯阴纯阳序列与虚拟买单的开仓逻辑

信号判定的核心是先扫一遍最近 m_signalSeqLen 根蜡烛:只要出现任意一根阳线(开盘低于收盘),就直接把 signal 置 0 并 break,意味着向上干扰会废掉原信号。 若 signal 仍为 0,则赋值为 -1 作为卖出备用信号,再反向扫一遍,若碰到任意一根阴线(开盘高于收盘)同样清 0 退出。也就是说,只有连续 N 根同向蜡烛才保留信号倾向,夹杂反向 K 线就取消。 开 Buy 时先取 point 与 digits,用 Ask 做进场价,并调用 UpdateLevels() 按 ATR 重算止损止盈距离。SL 设为 price - m_sl*point,TP 设为 price + m_tp*point,均做 NormalizeDouble 精度裁剪。 下方循环遍历 m_maxCountOfOrders 个虚拟仓位,找到第一个未开的就下虚拟多单,传固定手数 m_fixedLot 与计算好的 sl/tp,成功即 break;若全部占用则 PrintFormat 报错。外汇与贵金属品种波动剧烈,这套虚拟下单仅用于回测验证逻辑,实盘需自担高风险。

MQL5 / C++
for(class="type">int i = class="num">0; i < m_signalSeqLen; i++) {
  class=class="str">"cmt">// If at least one upward candle occurs, cancel the signal
  if(rates[i].open < rates[i].close ) {
    signal = class="num">0;
    class="kw">break;
  }
}
if(signal == class="num">0) {
  signal = -class="num">1; class=class="str">"cmt">// otherwise, sell signal
  class=class="str">"cmt">// Loop through all the candles
  for(class="type">int i = class="num">0; i < m_signalSeqLen; i++) {
    class=class="str">"cmt">// If at least one downward candle occurs, cancel the signal
    if(rates[i].open > rates[i].close ) {
      signal = class="num">0;
      class="kw">break;
    }
  }
}
}
class="kw">return signal;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Open BUY order                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CSimpleCandlesStrategy::OpenBuy() {
class=class="str">"cmt">// Retrieve the necessary symbol and price data
  class="type">class="kw">double point = m_symbolInfo.Point();
  class="type">int digits = m_symbolInfo.Digits();
class=class="str">"cmt">// Opening price
  class="type">class="kw">double price = m_symbolInfo.Ask();
class=class="str">"cmt">// Update SL and TP levels by calculating ATR
  UpdateLevels();
class=class="str">"cmt">// StopLoss and TakeProfit levels
  class="type">class="kw">double sl = NormalizeDouble(price - m_sl * point, digits);
  class="type">class="kw">double tp = NormalizeDouble(price + m_tp * point, digits);
  class="type">bool res = false;
  for(class="type">int i = class="num">0; i < m_maxCountOfOrders; i++) {   class=class="str">"cmt">// Iterate through all class="kw">virtual positions
    if(!m_orders[i].IsOpen()) {                    class=class="str">"cmt">// If we find one that is not open, then open it
      class=class="str">"cmt">// Open a class="kw">virtual SELL position
      res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY, m_fixedLot,
                            class="num">0,
                            NormalizeDouble(sl, digits),
                            NormalizeDouble(tp, digits));
      class="kw">break; class=class="str">"cmt">// and exit
    }
  }
  if(!res) {
    PrintFormat(__FUNCTION__" | ERROR opening BUY class="kw">virtual order", class="num">0);
  }
}

◍ 用中值波幅替掉标准ATR定止损

这段策略没有调用 iATR,而是自己写了 ChannelWidth 算通道宽度。它默认取 D1 周期、从第 1 根 bar 开始拷贝 m_periodATR 根日线,用 high-low 当作每根的真实波幅,再排序后只取中间二分之一(即 n/4 到 3n/4)的样本求平均,最后乘 2 除以 Point 转成点数。 中间四分位裁剪是关键:极端大棒和小棒被丢掉,得到的宽度比标准 ATR 更抗异常波动,外汇与贵金属在高波动时段出现长影线时,这种算法给出的止损距离可能更贴合常态波动。 UpdateLevels 里 m_sl 和 m_tp 直接用 stopLevel、takeLevel 乘这个宽度。你在 MT5 里把 m_periodATR 设成 20,就能看到日线中间波幅如何直接放大成具体止损点数,建议先开 EURUSD 的 D1 核对数值。

MQL5 / C++
class="type">void CSimpleCandlesStrategy::UpdateLevels() {
class=class="str">"cmt">// Calculate ATR
  class="type">class="kw">double channelWidth = (m_periodATR > class="num">0 ? ChannelWidth() : class="num">1);
class=class="str">"cmt">// Update SL and TP levels
  m_sl = m_stopLevel * channelWidth;
  m_tp = m_takeLevel * channelWidth;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Calculate the ATR value(non-standard implementation)             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CSimpleCandlesStrategy::ChannelWidth(ENUM_TIMEFRAMES p_tf = PERIOD_D1) {
  class="type">int n = m_periodATR; class=class="str">"cmt">// Number of bars for calculation
  class="type">MqlRates rates[];    class=class="str">"cmt">// Array for quotes
  class=class="str">"cmt">// Copy quotes from the daily(class="kw">default) timeframe
  class="type">int res = CopyRates(m_symbol, p_tf, class="num">1, n, rates);
  class=class="str">"cmt">// If the required amount has been copied
  if(res == n) {
    class="type">class="kw">double tr[];        class=class="str">"cmt">// Array for price ranges
    ArrayResize(tr, n); class=class="str">"cmt">// Change its size

    class="type">class="kw">double s = class="num">0;       class=class="str">"cmt">// Sum for calculating the average
    FOREACH(rates, {
      tr[i] = rates[i].high - rates[i].low; class=class="str">"cmt">// Remember the bar size
    });

    ArraySort(tr); class=class="str">"cmt">// Sort the sizes
    class=class="str">"cmt">// Sum the inner two quarters of the bar sizes
    for(class="type">int i = n / class="num">4; i < n * class="num">3 / class="num">4; i++) {
      s += tr[i];
    }

    class=class="str">"cmt">// Return the average size in points
    class="kw">return class="num">2 * s / n / m_symbolInfo.Point();
  }
  class="kw">return class="num">0.0;
}

「把策略类挂进第一阶段EA的实操坑」

把写好的 SimpleCandles 策略接进 Stage1 EA,核心是在项目文件里做五件事:定义 __NAME__ 常量保证 EA 名唯一、include 策略头文件、接 Advisor 库的 Stage1.mqh 公共部分、列输入参数、写 GetStrategyParams() 把输入转成初始化字符串。 代码里 symbol_ 默认 GBPUSD、period_ 用 H1,信号看连续 5 根同向蜡烛,ATR 周期 30,止损 3750 点、止盈 50 点,最多同开 3 单——这些数字直接决定回测样本与触发频率,上 MT5 改一处就能看到行为变化。 编译能过,但 OnInit() 运行会报 'Constructor not found'。根因在 CVirtualFactory::Create():它从初始化串读类名,遍历 CFactorable 所有子类去匹配。旧做法是在库里硬加条件分支(如 CSimpleVolumesStrategy),但新策略也塞进去就破了库与项目的隔离——库不该知道具体策略。 正确路子是让项目侧自己注册类,库只管通用创建逻辑。外汇与贵金属波动剧烈、点值杠杆风险高,这类架构错配可能在实盘加载时直接让 EA 罢工,建议先在策略测试器里用上述参数跑一遍 GBPUSD H1 验证加载是否干净。

MQL5 / C++
class=class="str">"cmt">// class="num">1. Define a constant with the EA name
class="macro">#define  __NAME__ "SimpleCandles" + MQLInfoString(MQL_PROGRAM_NAME)
class=class="str">"cmt">// class="num">2. Connect the required strategy
class="macro">#include "Strategies/SimpleCandlesStrategy.mqh";
class=class="str">"cmt">// class="num">3. Connect the general part of the first stage EA from the Advisor library
class="macro">#include <antekov/Advisor/Experts/Stage1.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| class="num">4. Strategy inputs                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
sinput class="type">class="kw">string      symbol_                 = "GBPUSD";
sinput ENUM_TIMEFRAMES period_             = PERIOD_H1;
input group "===  Opening signal parameters"
input class="type">int          signalSeqLen_           = class="num">5;      class=class="str">"cmt">// Number of unidirectional candles
input class="type">int          periodATR_              = class="num">30;     class=class="str">"cmt">// ATR period
input group "===  Pending order parameters"
input class="type">class="kw">double       stopLevel_              = class="num">3750;   class=class="str">"cmt">// Stop Loss(in points)
input class="type">class="kw">double       takeLevel_              = class="num">50;     class=class="str">"cmt">// Take Profit(in points)
input group "===  Money management parameters"
input class="type">int          maxCountOfOrders_      = class="num">3;      class=class="str">"cmt">// Maximum number of simultaneously open orders
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| class="num">5. Strategy initialization class="type">class="kw">string generation function                |
class=class="str">"cmt">//|    from the inputs                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string GetStrategyParams() {
   class="kw">return StringFormat(
            "class CSimpleCandlesStrategy(\"%s\",%d,%d,%d,%.3f,%.3f,%d)",
            symbol_, period_,
            signalSeqLen_, periodATR_, stopLevel_, takeLevel_, maxCountOfOrders_
   );
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">2018.01.class="num">01 class="num">00:class="num">00:class="num">00   CVirtualFactory::Create | ERROR: Constructor not found for:

用工厂函数按字符串分流策略对象

在 MT5 里做多策略回测框架时,经常需要从一个初始化字符串反序列化出不同类的实例。上面这段把类名从参数字符串里读出来,再按名字 new 出对应对象,是典型的工厂模式落法。 注意 CSimpleCandlesStrategy 的构造示例:GBPUSD 品种、magic 号 16385、5 分钟周期、30 点止损、2.95 倍 ATR 止盈参数、3 仓分层。这些数字直接决定了回测时英镑兑美元在 5M 图上的触发逻辑,改一个就可能让历史曲线形状完全变掉。 外汇与贵金属杠杆高、滑点跳空频繁,这类工厂创建的对象若状态无效必须报错返回,否则后续持仓管理会静默失控。开 MT5 把这段塞进你的 EA 基类,先验证 CVirtualAdvisor 与 CSimpleCandlesStrategy 两个分支能否正常出对象。

MQL5 / C++
class="num">2018.01.class="num">01 class="num">00:class="num">00:class="num">00   class CSimpleCandlesStrategy("GBPUSD",class="num">16385,class="num">5,class="num">30,class="num">2.95,class="num">3.92,class="num">3)
class=class="str">"cmt">// Create an object from the initialization class="type">class="kw">string
  class="kw">static CFactorable* Create(class="type">class="kw">string p_params) {
      class=class="str">"cmt">// Read the object class name
      class="type">class="kw">string className = CFactorable::ReadClassName(p_params);
      
      class=class="str">"cmt">// Pointer to the object being created
      CFactorable* object = NULL;
      class=class="str">"cmt">// Call the corresponding constructor  depending on the class name
      if(className == "CVirtualAdvisor") {
         object = new CVirtualAdvisor(p_params);
      } else if(className == "CVirtualRiskManager") {
         object = new CVirtualRiskManager(p_params);
      } else if(className == "CVirtualStrategyGroup") {
         object = new CVirtualStrategyGroup(p_params);
      } else if(className == "CSimpleVolumesStrategy") {
         object = new CSimpleVolumesStrategy(p_params);
      } else if(className == "CHistoryStrategy") {
         object = new CHistoryStrategy(p_params);
      }
              
      class=class="str">"cmt">// If the object is not created or is created in the invalid state, report an error
      if(!object) {
         ...
      }
      class="kw">return object;
   }
} else if(className == "CSimpleVolumesStrategy") {
   object = new CSimpleVolumesStrategy(p_params);
}
} else if(className == "CSimpleCandlesStrategy") {
   object = new CSimpleCandlesStrategy(p_params);
}

◍ 用静态构造器把类名和对象创建解耦

MQL5 没有 eval() 之类的运行时字符串执行能力,想在初始化字符串里写『CVirtualAdvisor(...)』就动态建对象,只能靠函数指针自己搭桥。思路是给每个 CFactorable 子类配一个静态 Create(),把常规构造藏进 protected,外部只走静态入口。 为了把『字符串类名』映射到『静态构造指针』,作者做了个 CFactorableCreator 类:它存一对数据——m_className 和 m_creator(函数指针),自身构造时就把 this 塞进静态数组 creators[]。只要在类定义外声明一个全局 creator 对象,MT5 加载模块时就会自动跑构造、自动进数组,不用在 OnInit() 里手写注册。 Advisor 库里三个核心类(CVirtualAdvisor、CVirtualStrategyGroup、CVirtualRiskManager)各加一次宏就行,以后忘掉。但项目工作目录里每新增一个交易策略类,都得补一段全局 creator 对象声明,否则初始化字符串认不出类名。外汇与贵金属自动化加载逻辑出错会导致 EA 不初始化,属高风险操作,上真仓前务必在策略测试器跑通。 最后 CVirtualFactory 被废掉,Create() 逻辑搬进 CFactorable::Create():先解析初始化串取类名,遍历 creators 数组按 m_className 匹配下标 i,命中就通过 m_creator 指针调静态构造。全程不再出现任何子类字面名,库和策略文件彻底分离。

MQL5 / C++
<span class="keyword">class="kw">static</span> CFactorable* Create(<span class="keyword">class="type">class="kw">string</span> p_params)
<span class="comment">class=class="str">"cmt">// Preliminary class definition</span>
<span class="keyword">class</span> CFactorable;
<span class="comment">class=class="str">"cmt">// Type declaration - pointer to the function for creating objects of the CFactorable class</span>
<span class="keyword">typedef</span> CFactorable* (*TCreateFunc)(<span class="keyword">class="type">class="kw">string</span>);
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Class of creators that bind names and class="kw">static                 |</span>
<span class="comment">class=class="str">"cmt">//| constructors of CFactorable descendant classes               |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class</span> CFactorableCreator {
<span class="keyword">class="kw">public</span>:
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_className;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Class name</span>
&nbsp;&nbsp; TCreateFunc&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_creator;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Static constructor for the class</span>
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Creator constructor</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CFactorableCreator(<span class="keyword">class="type">class="kw">string</span> p_className, TCreateFunc p_creator);
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Static array of all created creator objects</span>
&nbsp;&nbsp; <span class="keyword">class="kw">static</span> CFactorableCreator* creators[];
};
<span class="comment">class=class="str">"cmt">// Static array of all created creator objects</span>
CFactorableCreator* CFactorableCreator::creators[];
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Creator constructor                                          |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
CFactorableCreator::CFactorableCreator(<span class="keyword">class="type">class="kw">string</span> p_className, TCreateFunc p_creator) :
&nbsp;&nbsp; m_className(p_className),
&nbsp;&nbsp; m_creator(p_creator) {
<span class="comment">class=class="str">"cmt">// Add the current creator object to the class="kw">static array</span>
&nbsp;&nbsp; APPEND(creators, &amp;<span class="keyword">this</span>);
}
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Class of the EA handling class="kw">virtual positions(orders)           |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class</span> CVirtualAdvisor : <span class="keyword">class="kw">public</span> CAdvisor {

<span class="keyword">class="kw">protected</span>:
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//...</span>

「用宏把策略类塞进工厂数组」

把 EA 和策略做成可动态创建的对象,核心是把构造函数藏起来,只留一个静态 Create 入口。下面这段把 CVirtualAdvisor 的私有构造暴露为工厂方法,再用全局对象注册到 CFactorableCreator 列表,MT5 启动时就能按名字 "CVirtualAdvisor" 调出实例。 STATIC_CONSTRUCTOR 和 REGISTER_FACTORABLE_CLASS 两个宏是偷懒利器:前者在类里展开成 static CFactorable* Create(string p),后者在类外生成 CFactorableCreator CVirtualAdvisorCreator("CVirtualAdvisor", CVirtualAdvisor::Create)。你新建一个 CVirtualStrategy 的子类,只要套这两行宏,不用改主程序就能被框架识别。 CSimpleCandlesStrategy 这个例子挂了 9 个成员变量,从 m_symbol、m_timeframe 到 m_signalSeqLen(单向蜡烛根数)、m_periodATR(ATR 周期),再到 m_stopLevel / m_takeLevel 和 m_maxCountOfOrders。外汇与贵金属杠杆高、滑点跳空频繁,这类虚拟持仓回测若用固定点数止损,实盘可能触发偏差,建议把 stopLevel 改成 ATR 百分比再验证。 开 MT5 新建一个继承自 CVirtualStrategy 的类,粘上 REGISTER_FACTORABLE_CLASS(你的类名),编译后看全局对象是否进了对的数组——能跑通,说明你的策略已经被工厂接管了。

MQL5 / C++
CVirtualAdvisor(class="type">class="kw">string p_param);  class=class="str">"cmt">// Private constructor
class="kw">public:
  class="kw">static CFactorable* Create(class="type">class="kw">string p_params) { class="kw">return new CVirtualAdvisor(p_params) };
  class=class="str">"cmt">//...
};
CFactorableCreator CVirtualAdvisorCreator("CVirtualAdvisor", CVirtualAdvisor::Create);
class=class="str">"cmt">// Declare a class="kw">static constructor inside the class
class="macro">#define STATIC_CONSTRUCTOR(C) class="kw">static CFactorable* Create(class="type">class="kw">string p) { class="kw">return new C(p); }
class=class="str">"cmt">// Add a class="kw">static constructor for the new CFactorable descendant class
class=class="str">"cmt">// to a special array by creating a global object of the CFactorableCreator class 
class="macro">#define REGISTER_FACTORABLE_CLASS(C) CFactorableCreator C#class="macro">#Creator(class="macro">#C, C::Create);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class of the EA handling class="kw">virtual positions(orders)              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CVirtualAdvisor : class="kw">public CAdvisor {
class="kw">protected:
  class=class="str">"cmt">// ...
  CVirtualAdvisor(class="type">class="kw">string p_param);  class=class="str">"cmt">// Constructor
class="kw">public:
  STATIC_CONSTRUCTOR(CVirtualAdvisor);
  class=class="str">"cmt">// ...
};
REGISTER_FACTORABLE_CLASS(CVirtualAdvisor);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Trading strategy class="kw">using unidirectional candlesticks               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CSimpleCandlesStrategy : class="kw">public CVirtualStrategy {
class="kw">protected:
  class="type">class="kw">string        m_symbol;          class=class="str">"cmt">// Symbol(trading instrument)
  ENUM_TIMEFRAMES  m_timeframe;    class=class="str">"cmt">// Chart period(timeframe)
  class=class="str">"cmt">//---  Open signal parameters
  class="type">int           m_signalSeqLen;    class=class="str">"cmt">// Number of unidirectional candles
  class="type">int           m_periodATR;       class=class="str">"cmt">// ATR period
  class=class="str">"cmt">//---  Position parameters
  class="type">class="kw">double        m_stopLevel;       class=class="str">"cmt">// Stop Loss(in points or % ATR)
  class="type">class="kw">double        m_takeLevel;       class=class="str">"cmt">// Take Profit(in points or % ATR)
  class=class="str">"cmt">//---  Money management parameters
  class="type">int           m_maxCountOfOrders;class=class="str">"cmt">// Max number of simultaneously open positions
  CSymbolInfo   *m_symbolInfo;     class=class="str">"cmt">// Object for getting information about the symbol properties
  class="type">class="kw">double        m_tp;              class=class="str">"cmt">// Stop Loss in points
把策略切换交给小布盯盘看板
这些多策略参数与优化状态的诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到当前 EA 实例加载的是哪套参数组。

常见问题

库部分应只保留与具体策略类型无关的支持代码;策略本体放工作区才能在不重编译最终 EA 的前提下做参数热更新。
可以,小布盯盘的品种页支持读取独立 EA 数据库中的参数组,省去手动比对优化结果的重复劳动。
库代码不感知策略类别,必须在项目工作区代码里加一条 include 指令,否则阶段 EA 编译时找不到策略符号。
主要是第一阶段 EA 的输入参数模板,因为新策略的输入项组合和 SimpleVolumes 不同,其余输送逻辑可近似复用。