开发多币种 EA 交易(第 24 部分):添加新策略(一)·进阶篇
(2/3)· 当 SimpleVolumes 不再是唯一标的,库与工作区如何拆分才不拖垮编译与优化流水线
纯阴纯阳序列与虚拟买单的开仓逻辑
信号判定的核心是先扫一遍最近 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 报错。外汇与贵金属品种波动剧烈,这套虚拟下单仅用于回测验证逻辑,实盘需自担高风险。
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 核对数值。
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 验证加载是否干净。
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 两个分支能否正常出对象。
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 指针调静态构造。全程不再出现任何子类字面名,库和策略文件彻底分离。
<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>: <span class="keyword">class="type">class="kw">string</span> m_className; <span class="comment">class=class="str">"cmt">// Class name</span> TCreateFunc m_creator; <span class="comment">class=class="str">"cmt">// Static constructor for the class</span> <span class="comment">class=class="str">"cmt">// Creator constructor</span> CFactorableCreator(<span class="keyword">class="type">class="kw">string</span> p_className, TCreateFunc p_creator); <span class="comment">class=class="str">"cmt">// Static array of all created creator objects</span> <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) : m_className(p_className), m_creator(p_creator) { <span class="comment">class=class="str">"cmt">// Add the current creator object to the class="kw">static array</span> APPEND(creators, &<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>: <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(你的类名),编译后看全局对象是否进了对的数组——能跑通,说明你的策略已经被工厂接管了。
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