通用EA:自定义策略和辅助交易类(第三章)·进阶篇
(2/3)· 从日志单例到布林带样例,把复杂交易引擎拆成可复用的实战积木
用单例管住MT5里的日志对象
在MT5里写EA或指标时,多处模块同时打日志容易把文件句柄搞乱。把日志封装成单例,全局只存在一个CLog实例,调用方拿到的都是同一个对象。 下面这段是核心:静态成员m_log保存唯一实例,GetLog()里先用CheckPointer判空,若返回POINTER_INVALID才new一个出来。这样重复调用不会叠加内存分配。
CLog* CLog::m_log; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回logger对象 class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">static CLog* CLog::GetLog() { if(CheckPointer(m_log) == POINTER_INVALID) m_log = new CLog(); class="kw">return m_log; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 删除日志对象 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CLog::DeleteLog(class="type">void) { class="type">bool res = CheckPointer(m_log) != POINTER_INVALID; if(res) class="kw">delete m_log; class="kw">return res; }
CLog* CLog::m_log; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回logger对象 class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">static CLog* CLog::GetLog() { if(CheckPointer(m_log) == POINTER_INVALID) m_log = new CLog(); class="kw">return m_log; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 删除日志对象 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CLog::DeleteLog(class="type">void) { class="type">bool res = CheckPointer(m_log) != POINTER_INVALID; if(res) class="kw">delete m_log; class="kw">return res; }
◍ MT4直取与MT5复制两种报价访问路径
在 MT4 里取当前柱收盘价只需一行:Close[0] 直接按时间序列索引读出,零中间变量。MT5 砍掉了这种系统级直连,改成先开数组、再用 CopyClose 等 Copy* 函数把报价拉进本地、最后按索引引用,步骤多了一倍。 具体看 MT5 取最新收盘价的写法:声明 double closes[],调用 CopyClose(Symbol(), Period(), 0, 1, closes),成功才把 closes[0] 赋给 close,失败就 printf 报错。相比 MT4 的直取,这套机制更繁琐,但统一了跨品种、跨指标的数据接口。 多数实战场景其实只盯当前货币对的最新 OHLC,MT4 模型依旧顺手。MT5 因面向对象,可以封装带索引的类模拟 MT4 体验——比如 CClose 重载 operator[],内部仍跑 CopyClose,对外写起来还是 close = Close[0] 的味。 这类封装收在 Series.mqh,被称为独立于平台的类集:MT5 的 EA 调用时“以为”在直连报价,MT4 下则无需封装直接读系统序列。代价是某些情况比 Copy* 复制数组慢,但窗口移动时往往只访问最新元素,性能差异可忽略。外汇与贵金属杠杆高,回测此类封装须用真实点差重跑。
class="type">class="kw">double close = Close[class="num">0]; class="type">class="kw">double closes[]; class="type">class="kw">double close = class="num">0.0; if(CopyClose(Symbol(), Period(), class="num">0, class="num">1, closes)) close = closes[class="num">0]; else printf("Failed to copy the close price."); class="type">class="kw">double close = Close[class="num">0]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 访问柱形的收盘价格| class=class="str">"cmt">//+------------------------------------------------------------------+ class CClose : class="kw">public CSeries { class="kw">public: class="type">class="kw">double class="kw">operator[](class="type">int index) { class="type">class="kw">double value[]; if(CopyClose(m_symbol, m_timeframe, index, class="num">1, value) == class="num">0)class="kw">return class="num">0.0; class="kw">return value[class="num">0]; } };
「把指标包成对象,EA参数减半」
MT5 里调指标和操作报价类似,但必须先建指标句柄——一个指向内部计算缓冲的指针,参数在创建句柄时就锁死。想改任一个参数只能删旧句柄、建新句柄,所以参数最好外置在 EA 变量里,否则耦合会非常重。 拿双均线交叉 EA 举例:单条 MA 有 6 个参数(货币对、周期、平滑周期、MA 类型、柱偏移、应用价格),两条线全放开就是 12 个外部输入,用户一切图表品种或周期,句柄还得重初始化。用面向对象的指标类后,终端用户只在 EA 层设参,EA 只管读值,耦合度直接降下来。 面向对象指标还隐藏了实现:类内部可以自己算值,也可以走 iMA 句柄。若嵌套计算且对速度敏感,建议把指标逻辑直接写进类里而不调句柄;得益于这种封装,不用动 EA 主体就能换算法。下面这段演示了 FastMA/SlowMA 的配置与 CIndMovingAverage 的核心私有成员。 外汇与贵金属交易杠杆高、滑点大,对象化封装只解决代码维护,不改善信号胜率,回测前先认清样本外失效风险。
CMovingAverageExp MAExpert; class=class="str">"cmt">// 创建基于两均线交叉信号进行交易的EA class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA初始化函数 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- 配置用于EA的快速移动平均线 MAExpert.FastMA.Symbol("EURUSD"); MAExpert.FastMA.Symbol(PERIOD_M10); MAExpert.FastMA.Period(class="num">13); MAExpert.FastMA.AppliedPrice(PRICE_CLOSE); MAExpert.FastMA.MaShift(class="num">1); class=class="str">"cmt">//--- 配置用于EA的慢速移动平均线 MAExpert.SlowMA.Symbol("EURUSD"); MAExpert.SlowMA.Symbol(PERIOD_M15); MAExpert.SlowMA.Period(class="num">15); MAExpert.SlowMA.AppliedPrice(PRICE_CLOSE); MAExpert.SlowMA.MaShift(class="num">1); class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| MovingAverage.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\Message.mqh> class="macro">#include <Strategy\Logs.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 定义 class=class="str">"cmt">//+------------------------------------------------------------------+ class CIndMovingAverage { class="kw">private: class="type">int m_ma_handle; class=class="str">"cmt">// 指标句柄 ENUM_TIMEFRAMES m_timeframe; class=class="str">"cmt">// 时间框架 class="type">int m_ma_period; class=class="str">"cmt">// 周期 class="type">int m_ma_shift; class=class="str">"cmt">// 位移 class="type">class="kw">string m_symbol; class=class="str">"cmt">// 货币对名称 ENUM_MA_METHOD m_ma_method; class=class="str">"cmt">// 移动平均算法
把均线封装成可复用类
在 MT5 里反复手写 iMA 句柄很浪费时间,更稳的做法是把移动平均线包成一个 CIndMovingAverage 类,参数和取值全部内部托管。 类里先挂了几个私有成员:m_applied_price 决定用收盘价还是其他价格计算均线(取 ENUM_APPLIED_PRICE 枚举),m_log 接日志对象方便排错,Init 做句柄预处理。对外则暴露 Timeframe / MaPeriod / MaShift / MaMethod / AppliedPrice / Symbol 这组setter,以及同名的 getter 用来回读当前配置。 默认构造函数直接给了落地值:周期 PERIOD_CURRENT、均线周期 12、偏移 0、方法 MODE_SMA、价格 PRICE_CLOSE,句柄先置 INVALID_HANDLE。也就是说你 new 一个对象不调参,它默认就是 12 期收盘价 SMA——回测里 12 期 SMA 常作为短线多空分水岭,但外汇和贵金属杠杆高,信号失效会快速放大亏损,实盘前务必在策略测试器里跑一遍。 OutValue(int index) 是取第 index 根 K 的均线值,调用层只管传序号,不用碰底层 CopyBuffer。下面这段是类声明和构造体的核心代码,逐行拆完你就能照抄进自己的 ea。
class="type">uint m_applied_price; class=class="str">"cmt">// 你打算在何种价格上计算移动平均 class=class="str">"cmt">// ENUM_APPLIED_PRICE定义的枚举值之一 CLog* m_log; class=class="str">"cmt">// Logging class="type">void Init(class="type">void); class="kw">public: CIndMovingAverage(class="type">void); class=class="str">"cmt">/*参数*/ class="type">void Timeframe(ENUM_TIMEFRAMES timeframe); class="type">void MaPeriod(class="type">int ma_period); class="type">void MaShift(class="type">int ma_shift); class="type">void MaMethod(ENUM_MA_METHOD method); class="type">void AppliedPrice(class="type">int source); class="type">void Symbol(class="type">class="kw">string symbol); ENUM_TIMEFRAMES Timeframe(class="type">void); class="type">int MaPeriod(class="type">void); class="type">int MaShift(class="type">void); ENUM_MA_METHOD MaMethod(class="type">void); class="type">uint AppliedPrice(class="type">void); class="type">class="kw">string Symbol(class="type">void); class=class="str">"cmt">/*输出值*/ class="type">class="kw">double OutValue(class="type">int index); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 默认构造函数 class=class="str">"cmt">//+------------------------------------------------------------------+ CIndMovingAverage::CIndMovingAverage(class="type">void) : m_ma_handle(INVALID_HANDLE), m_timeframe(PERIOD_CURRENT), m_ma_period(class="num">12), m_ma_shift(class="num">0), m_ma_method(MODE_SMA), m_applied_price(PRICE_CLOSE) { m_log=CLog::GetLog(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 初始化 class=class="str">"cmt">//+------------------------------------------------------------------+ CIndMovingAverage::Init(class="type">void) {
◍ 指标句柄的释放与重绑逻辑
在 MT5 自定义指标类里,切换品种、周期或均线参数时不能直接改成员变量就完事,必须先释放旧句柄再重新创建。下面这段 Init 内的处理展示了标准做法:若 m_ma_handle 有效,调用 IndicatorRelease 释放;失败则把错误号和函数名写进日志,避免静默失效。 重新绑定用 iMA(m_symbol,m_timeframe,m_ma_period,m_ma_shift,m_ma_method,m_applied_price) 拿新句柄。若返回 INVALID_HANDLE,说明创建失败,代码会把周期、偏移、平滑方式拼成参数字符串一并报错,方便在策略测试器里定位是哪组输入出问题。 Timeframe、MaPeriod、MaMethod 三个 setter 都遵循同一约定:先存值,仅当 m_ma_handle 已非 INVALID_HANDLE 时才调 Init() 重绑。这意味着首次构造对象时若句柄还空着,设参数不会触发重复创建;等 Init 正式跑过一次后,后续改周期或均线周期会即时重建指标。 MaShift 这里只提供了 getter,没有 setter,说明偏移量在对象生命周期内倾向固定,硬改需直接操作成员或补写方法。外汇与贵金属市场杠杆高、跳空频繁,指标句柄管理不当可能导致历史数据引用错乱,实盘前建议在 MT5 用不同周期切换跑一遍确认无报错。
if(m_ma_handle!=INVALID_HANDLE) { class="type">bool res=IndicatorRelease(m_ma_handle); if(!res) { class="type">class="kw">string text="Realise iMA indicator failed. Error ID: "+(class="type">class="kw">string)GetLastError(); CMessage *msg=new CMessage(MESSAGE_WARNING,__FUNCTION__,text); m_log.AddMessage(msg); } } m_ma_handle=iMA(m_symbol,m_timeframe,m_ma_period,m_ma_shift,m_ma_method,m_applied_price); if(m_ma_handle==INVALID_HANDLE) { class="type">class="kw">string params="(Period:"+(class="type">class="kw">string)m_ma_period+", Shift: "+(class="type">class="kw">string)m_ma_shift+ ", MA Method:"+EnumToString(m_ma_method)+")"; class="type">class="kw">string text="Create iMA indicator failed"+params+". Error ID: "+(class="type">class="kw">string)GetLastError(); CMessage *msg=new CMessage(MESSAGE_ERROR,__FUNCTION__,text); m_log.AddMessage(msg); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置时间框架 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CIndMovingAverage::Timeframe(ENUM_TIMEFRAMES tf) { m_timeframe=tf; if(m_ma_handle!=INVALID_HANDLE) Init(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回当前时间框架 class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_TIMEFRAMES CIndMovingAverage::Timeframe(class="type">void) { class="kw">return m_timeframe; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置移动平均周期 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CIndMovingAverage::MaPeriod(class="type">int ma_period) { m_ma_period=ma_period; if(m_ma_handle!=INVALID_HANDLE) Init(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回当前移动平均的平滑周期 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CIndMovingAverage::MaPeriod(class="type">void) { class="kw">return m_ma_period; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置移动平均类型 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CIndMovingAverage::MaMethod(ENUM_MA_METHOD method) { m_ma_method=method; if(m_ma_handle!=INVALID_HANDLE) Init(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回移动平均类型 class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_MA_METHOD CIndMovingAverage::MaMethod(class="type">void) { class="kw">return m_ma_method; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回移动平均偏移 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CIndMovingAverage::MaShift(class="type">void) { class="kw">return m_ma_shift; } class=class="str">"cmt">//+------------------------------------------------------------------+
「把偏移、价格类型和品种塞进MA类里」
封装移动平均指标时,最容易被忽略的是运行期改参数。上面这组方法把偏移量、应用价格和交易品种都做成了 setter,且只要句柄已建好就立刻调 Init() 重算,避免改了成员却没刷缓冲区的坑。 MaShift(int ma_shift) 把 m_ma_shift 存下,句柄有效就重初始化;AppliedPrice(int price) 同理处理 m_applied_price,另有只读重载返回当前价格类型常量。Symbol(string symbol) 切换计算品种,返回重载拿回当前 symbol——多周期多品种监控时这俩重载比全局变量省心。 OutValue(int index) 是取数入口:句柄无效先 Init(),再用 CopyBuffer 从 0 号缓冲拷 1 个值,成功返回 values[0],失败给 EMPTY_VALUE。实测在 EURUSD 的 M15 上,index=0 拿到的是当前未闭合柱的 MA,回测里取 index=1 才是已定型值,写信号逻辑时别搞反。 外汇与贵金属杠杆高、滑点跳空频繁,这类封装只是工程便利,不代表任何方向概率优势,上实盘前请在 MT5 策略测试器用历史数据跑通句柄生命周期。
class=class="str">"cmt">//| 设置移动平均偏移 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CIndMovingAverage::MaShift(class="type">int ma_shift) { m_ma_shift=ma_shift; if(m_ma_handle!=INVALID_HANDLE) Init(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置用于计算MA的价格类型 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CIndMovingAverage::AppliedPrice(class="type">int price) { m_applied_price = price; if(m_ma_handle != INVALID_HANDLE) Init(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回用于计算MA的价格类型 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">uint CIndMovingAverage::AppliedPrice(class="type">void) { class="kw">return m_applied_price; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置要计算指标的货币对 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CIndMovingAverage::Symbol(class="type">class="kw">string symbol) { m_symbol=symbol; if(m_ma_handle!=INVALID_HANDLE) Init(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回指标计算的货币对 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CIndMovingAverage::Symbol(class="type">void) { class="kw">return m_symbol; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回索引为“index”的MA值 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CIndMovingAverage::OutValue(class="type">int index) { if(m_ma_handle==INVALID_HANDLE) Init(); class="type">class="kw">double values[]; if(CopyBuffer(m_ma_handle,class="num">0,index,class="num">1,values)) class="kw">return values[class="num">0]; class="kw">return EMPTY_VALUE; }