当开发者创建 EA 交易来从指标接收信号时,他们总是要决定,是使用对指标的引用还是把指标代码迁移到 EA 中呢?原因可能有多种,开发者可能想把使用的指标和整个策略保密,以单个文件的方式发布 EA,在不使用指标的所有信号/缓冲区时减少操作的数量,等等。当然,我不是第一个,相信也不是最后一个问这个问题的人,Nikolay Kositsin 已经探讨了在 MetaTrader 4 中的类似问题,让我们看看,在 MetaTrader 5 平台上会有什么收获。
在我们开始工作之前,让我们思考一下指标和EA交易在工作中的差别。例如有一个空白的指标模板。
//+------------------------------------------------------------------+ //| Blanc.mq5 | //| Copyright 2018, DNG® | //| http://www.mql5.com/ru/users/dng | //+------------------------------------------------------------------+ #property copyright "Copyright 2018, DNG®" #property link "http://www.mql5.com/ru/users/dng" #property version "1.00" #property indicator_chart_window #property indicator_buffers 1 #property indicator_plots 1 //--- 绘图缓冲区 #property indicator_label1 "Buffer" #property indicator_type1 DRAW_LINE #property indicator_color1 clrRed #property indicator_style1 STYLE_SOLID #property indicator_width1 1 //--- 指标缓冲区 double BufferBuffer[]; //+------------------------------------------------------------------+ //| 自定义指标初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- 指标缓冲区映射 SetIndexBuffer(0,BufferBuffer,INDICATOR_DATA); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| 自定义指标迭代函数 | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- //--- 返回 prev_calculated 的值用于下一次调用 return(rates_total); } //+------------------------------------------------------------------+
在指标代码的开始,会声明缓冲区数组,用于与其他程序交换数据。这些数组都是时间序列,它们的元素与价格柱相关,这种关联是由终端直接支持的。指标在这些数组中保存着计算的结果,而不必关心改变它们的大小以及在新的烛形出现时移动数据。在 EA 交易中没有这样的数组,所以,如果您把指标代码迁移到 EA 交易,您将需要自己创建它们。除了计算部分本身,您还需要在数组元素和价格图表柱之间建立连接。另一方面,还需要可以不必在整个历史中进行计算(这可能会在指标中出现),只需要重新计算数据深度就够了,
所以,您需要在 EA 交易中创建指标缓冲区。请记住,指标可能会不仅有用于在图表上显示信息的缓冲区,还有些辅助缓冲区用于中间计算,它们也应当被创建出来。如果在 EA 策略中没有指标线颜色改变的特性,就可以忽略绘制色彩缓冲区。
指标和EA交易另一点架构上的区别是分时处理函数,与 MetaTrader 4 不同, MetaTrader 5 分离了指标和EA交易的分时处理函数,当一个新的分时来到时,指标会调用 OnCalculate 函数,在参数中,有图表上柱的总数,前一次调用时的柱数,以及需要计算指标的时段。在 EA 交易中,新的分时在 OnTick 函数中处理,而它没有参数。所以,我们将必须建立对时间序列的访问,并确保能够跟踪图表上的变化。
在 EA 交易的策略中,经常使用不同参数的单个指标,所以使用OOP(面向对象的编程)特性,从CIndicator 类来封装我们的指标是合理的。
让我们总结一下,下面是我们为了把指标计算迁移到 EA 交易中所需要做的事。
所有的工作都可以可视化地总结如下。
另外,说到指标的时候,我的意思将是在 EA 代码中创建的指标的副本。
让我们使用 CArrayDouble 类来创建指标缓冲区,再在它的基础上创建一个新的 CArrayBuffer 类。
class CArrayBuffer : public CArrayDouble { public: CArrayBuffer(void); ~CArrayBuffer(void); //--- int CopyBuffer(const int start, const int count, double &double_array[]); int Initilize(void); virtual bool Shift(const int shift); };
创建 CopyBuffer 方法,这样接收数据就和使用指标的标准引用类似了。另外,加上两个实用方法:Initilize — 用于清空缓冲区数据,以及 Shift — 用于当新的烛形出现时在缓冲区内移动数据。函数的代码在附件中提供。
下一步是在 CIndicator 积累中创建指标的“骨架”。
class CIndicator { private: //--- datetime m_last_load; public: CIndicator(void); ~CIndicator(void); virtual bool Create(const string symbol=NULL, const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT, const ENUM_APPLIED_PRICE price=PRICE_CLOSE); //--- 设置指标的主要配置 virtual bool SetBufferSize(const int bars); //--- 取得指标数据 virtual int CopyBuffer(const uint buffer_num,const uint start, const uint count, double &double_array[]); virtual double GetData(const uint buffer_num,const uint shift); protected: double m_source_data[]; CArrayBuffer ar_IndBuffers[]; int m_buffers; int m_history_len; int m_data_len; //--- string m_Symbol; ENUM_TIMEFRAMES m_Timeframe; ENUM_APPLIED_PRICE m_Price; //--- 设置指标的主要配置 virtual bool SetHistoryLen(const int bars=-1); //--- virtual bool LoadHistory(void); virtual bool Calculate() { return true; } };
这个类包含六个公有方法:
类成员的主要部分声明在 'protected' 区域,这里我们声明了:
所有的方法都创建为虚方法,这样它们可以根据特定指标的需要进行调整,在类的构造函数中初始化变量和释放数组.
CIndicator::CIndicator() : m_buffers(0), m_Symbol(_Symbol), m_Timeframe(PERIOD_CURRENT), m_Price(PRICE_CLOSE), m_last_load(0) { m_data_len=m_history_len = Bars(m_Symbol,m_Timeframe)-1; ArrayFree(ar_IndBuffers); ArrayFree(m_source_data); }
在类的初始化函数中,首先检查是否使用了特定的交易品种,为此,要检查它是否在市场报价中处于活动状态,如果不是,就要尝试选择它。如果交易品种无法使用,函数就返回 'false',如果检查成功,就把交易品种、时段和计算价格的类型保存到相应的变量中。
bool CIndicator::Create(const string symbol=NULL,const ENUM_TIMEFRAMES timeframe=0,const ENUM_APPLIED_PRICE price=1) { m_Symbol=(symbol==NULL ? _Symbol : symbol); if(!SymbolInfoInteger(m_Symbol,SYMBOL_SELECT)) if(!SymbolSelect(m_Symbol,true)) return false; //--- m_Timeframe=timeframe; m_Price=price; //--- return true; }
设置指标缓冲区大小的方法只有一个参数 — 即大小本身。在这种情况下,如果我们想使用全部可用的历史,我们只要向这个函数传入一个等于或者小于0的数字就可以了。在函数中,我们首先把传入参数的数值保存到对应的变量中,然后,我们会检查这个时段的历史数据是否足够来获得指标的历史,如果初始值不够,我们下载的数据量就要增加,在函数的末尾,要清空所有的指标缓冲区并改变它们的大小。
bool CIndicator::SetBufferSize(const int bars) { if(bars>0) m_data_len = bars; else m_data_len = Bars(m_Symbol,m_Timeframe); //--- if(m_data_len<=0) { for(int i=0;i<m_buffers;i++) ar_IndBuffers[i].Shutdown(); return false; } //--- if(m_history_len<m_data_len) if(!SetHistoryLen(m_data_len)) return false; //--- for(int i=0;i<m_buffers;i++) { ar_IndBuffers[i].Shutdown(); if(!ar_IndBuffers[i].Resize(m_data_len)) return false; } //--- return true; }
如需取得某时段的数据,就要使用 LoadHistory 函数,它没有参数,它是把之前函数中保存的数据用作初始值,
当前的指标值经常在烛形形成的过程中有改变,这可能会引起错误信号,所以,很多指标策略都使用已经关闭的烛形的数据。对于这种逻辑,只需要在有新柱形成的时候,根据指标的需要来载入历史数据就足够了。所以,在函数的开始,我们设置对开启新柱的检查,如果没有开启新柱,而数据已经载入了,我们就退出函数。如果我们需要载入数据,就转到下一个功能块。如果只需要一个时段来进行指标的计算,就把所有所需数据下载到我们的初始数据数组中。当指标使用中间价格、典型价格或者加权平均价格的时候,我们首先要把历史数据下载到 MqlRates 结构的数组中,然后再在循环中进行所需的价格计算。计算结果保存在初始数据数组中以备将来使用。
bool CIndicator::LoadHistory(void) { datetime cur_date=(datetime)SeriesInfoInteger(m_Symbol,m_Timeframe,SERIES_LASTBAR_DATE); if(m_last_load>=cur_date && ArraySize(m_source_data)>=m_history_len) return true; //--- MqlRates rates[]; int total=0,i; switch(m_Price) { case PRICE_CLOSE: total=CopyClose(m_Symbol,m_Timeframe,1,m_history_len,m_source_data); break; case PRICE_OPEN: total=CopyOpen(m_Symbol,m_Timeframe,1,m_history_len,m_source_data); case PRICE_HIGH: total=CopyHigh(m_Symbol,m_Timeframe,1,m_history_len,m_source_data); case PRICE_LOW: total=CopyLow(m_Symbol,m_Timeframe,1,m_history_len,m_source_data); case PRICE_MEDIAN: total=CopyRates(m_Symbol,m_Timeframe,1,m_history_len,rates); if(total!=ArraySize(m_source_data)) total=ArrayResize(m_source_data,total); for(i=0;i<total;i++) m_source_data[i]=(rates[i].high+rates[i].low)/2; break; case PRICE_TYPICAL: total=CopyRates(m_Symbol,m_Timeframe,1,m_history_len,rates); if(total!=ArraySize(m_source_data)) total=ArrayResize(m_source_data,total); for(i=0;i<total;i++) m_source_data[i]=(rates[i].high+rates[i].low+rates[i].close)/3; break; case PRICE_WEIGHTED: total=CopyRates(m_Symbol,m_Timeframe,1,m_history_len,rates); if(total!=ArraySize(m_source_data)) total=ArrayResize(m_source_data,total); for(i=0;i<total;i++) m_source_data[i]=(rates[i].high+rates[i].low+2*rates[i].close)/4; break; } //--- if(total<=0) return false; //--- m_last_load=cur_date; return (total>0); }
如果在函数的执行过程中没有发现数据,函数就返回 'false'。
用于取得指标数据的方法和访问指标缓冲区的标准方法类似,为了做到这一点,要创建 CopyBuffer 函数,在它的参数中,包括缓冲区编号,复制数据起始位置,所需元素的数量以及用于接收数据的数组。在执行之后,函数返回所复制元素的数量。
int CIndicator::CopyBuffer(const uint buffer_num,const uint start,const uint count,double &double_array[]) { if(!Calculate()) return -1; //--- if((int)buffer_num>=m_buffers) { ArrayFree(double_array); return -1; } //--- return ar_IndBuffers[buffer_num].CopyBuffer(start,count,double_array); }
为了使用户能够一直获得实际数据,在函数的开始会调用指标重计算函数 (在这个类中,我们只是声明虚函数,而计算是在指标实际最终类中直接进行的). 在重新计算了指标值之后,检查是否有指定的缓冲区,如果缓冲区编号指定得不正确,就清空接收数组,并使用 "-1" 作为结果退出函数。如果缓冲区编号被成功确认,就调用对应缓冲区数组的 CopyBuffer 方法。
访问目标数据的函数的运行方式类似,
完整的类代码和所有的函数都在附件中提供。
为了演示方法,我选择了移动平均指标 (MA)。这个技术分析指标不仅是交易者中最常用的,也广泛用于开发其它指标,包括 MACD,鳄鱼指标,等等。另外,在标准发布包中有一个МА 实例, 我们可以通过 iCustom 函数来从中读取数据,来与在 EA 中进行数据计算进行速度上的比较。
我们将在 CMA 类中计算 MA。这个类有四个公有方法: 构造函数,析构函数,初始化方法 (Create) 以及用于设置指标历史深度的方法 (我们要重写)。这个类从它的父类中继承了访问指标数据的方法。
class CMA : public CIndicator { private: int m_Period; int m_Shift; ENUM_MA_METHOD m_Method; datetime m_last_calculate; public: CMA(); ~CMA(); bool Create(const string symbol, const ENUM_TIMEFRAMES timeframe, const int ma_period, const int ma_shift, const ENUM_MA_METHOD ma_method, const ENUM_APPLIED_PRICE price=PRICE_CLOSE); virtual bool SetBufferSize(const int bars); protected: virtual bool Calculate(); virtual double CalculateSMA(const int shift); virtual double CalculateEMA(const int shift); virtual double CalculateLWMA(const int shift); virtual double CalculateSMMA(const int shift); };
在这个阶段,您可以在上面看到类的头部和用于直接进行指标计算的元素,这些自定义变量是用于保存指标周期数、偏移和计算方法的。在 'protected' 区块,我们会重写 Calculate 虚函数用于指标计算,根据指定的指标计算方法,它会调用 CalculateSMA, CalculateEMA, CalculateLWMA 或者 CalculateSMMA 子函数。
在类的构造函数中,我们初始化变量,指定指标缓冲区的数量以及创建指标缓冲区。
CMA::CMA() : m_Period(25), m_Shift(0), m_Method(MODE_SMA) { m_buffers=1; ArrayResize(ar_IndBuffers,1); }
指定在类的初始化函数参数中的所需的交易品种、时段和用于指标计算的参数,在函数内部,我们首先调用父类的初始化函数,然后,检查指定的平均周期数是否有效(它应当为正),在那以后,把指标参数保存到对应的类变量中,并设置用于指标缓冲区和载入时间序列的历史深度,如果有错误发生,函数就返回 'false',在成功初始化之后,它返回 'true'。
bool CMA::Create(const string symbol,const ENUM_TIMEFRAMES timeframe,const int ma_period,const int ma_shift,const ENUM_MA_METHOD ma_method,const ENUM_APPLIED_PRICE price=1) { if(!CIndicator::Create(symbol,timeframe,price)) return false; //--- if(ma_period<=0) return false; //--- m_Period=ma_period; m_Shift=ma_shift; m_Method=ma_method; //--- if(!SetBufferSize(ma_period)) return false; if(!SetHistoryLen(2*ma_period+(m_Shift>0 ? m_Shift : 0))) return false; //--- return true; }
Calculate 函数会直接计算指标。当创建父类的时候,我们决定当开启新的烛形时会载入时间序列的历史数据,所以,我们将会以同样的频率来重新计算指标数据。为此,我们要在函数的开始检查是否有新的烛形开启,如果已经在当前柱上进行过计算,就以'true'的结果退出函数,
如果开启了新柱,就调用载入时间序列的函数。如果成功下载了历史数据,就检查在前一次指标重新计算后生成烛形的数量,如果新烛形的数量大于指标缓冲区的大小,就要再次初始化,如果新的烛形数量更少,就把缓冲区中的数据移动所出现的柱数,然后,只重新计算新的元素。
现在,进行循环来重新计算指标缓冲区中的新元素。请注意:如果重新计算的元素超过了当前指标缓冲区的大小 (这在第一次开始计算或者在有连接中断后新的柱数超过了缓冲区大小的时候是可能的),数据会通过 Add 方法加到缓冲区中。如果重新计算的元素在已有缓冲区大小之内,元素的数值会使用 Update 方法更新。指标值的计算是在对应的平均方法的子函数中进行的,计算逻辑是来自于MetaTrader 5 标准分发包中包含的 Custom Moving Average.mq5 指标。
在成功重新计算了指标缓冲区之后,把最后重新计算的时间保存下来,然后以'true'的结果退出函数。
bool CMA::Calculate(void) { datetime cur_date=(datetime)SeriesInfoInteger(m_Symbol,m_Timeframe,SERIES_LASTBAR_DATE); if(m_last_calculate==cur_date && ArraySize(m_source_data)==m_history_len) return true; //--- if(!LoadHistory()) return false; //--- int shift=Bars(m_Symbol,m_Timeframe,m_last_calculate,cur_date)-1; if(shift>m_data_len) { ar_IndBuffers[0].Initilize(); shift=m_data_len; } else ar_IndBuffers[0].Shift(shift); //--- for(int i=(m_data_len-shift);i<m_data_len;i++) { int data_total=ar_IndBuffers[0].Total(); switch(m_Method) { case MODE_SMA: if(i>=data_total) ar_IndBuffers[0].Add(CalculateSMA(i+m_Shift)); else ar_IndBuffers[0].Update(i,CalculateSMA(i+m_Shift)); break; case MODE_EMA: if(i>=data_total) ar_IndBuffers[0].Add(CalculateEMA(i+m_Shift)); else ar_IndBuffers[0].Update(i,CalculateEMA(i+m_Shift)); break; case MODE_SMMA: if(i>=data_total) ar_IndBuffers[0].Add(CalculateSMMA(i+m_Shift)); else ar_IndBuffers[0].Update(i,CalculateSMMA(i+m_Shift)); break; case MODE_LWMA: if(i>=data_total) ar_IndBuffers[0].Add(CalculateLWMA(i+m_Shift)); else ar_IndBuffers[0].Update(i,CalculateLWMA(i+m_Shift)); break; } } //--- m_last_calculate=cur_date; m_data_len=ar_IndBuffers[0].Total(); //--- return true; }
另外,我们还将重写父类中的虚函数,即设置所需指标缓冲区大小的函数。需要这个是因为要验证指标缓冲区深度和时间序列历史数据深度之间的对应。在父类中,我们指定了时间序列中元素的数量应当不小于指标缓冲区中元素的数量,为了计算 MA,时间序列中元素的数量应当大于指标缓冲区的大小至少平均周期数。
当我准备这篇文章时,我的目标之一是比较在 EA 内处理数据的速度和从指标中读取数据的速度。所以,我决定不要创建完整功能的交易机器人来演示类的工作,而是提供了一个空白的 EA ,您可以加上您自己的用来处理指标信号的逻辑。
让我们创建一个新的 Test_Class.mq5 EA 文件,它的输入参数和所使用指标的参数类似。
input int MA_Period = 25; input int MA_Shift = 0; input ENUM_MA_METHOD MA_Method = MODE_SMA; input ENUM_APPLIED_PRICE MA_Price = PRICE_CLOSE;
声明一个指标类的实例,以及用于在全局范围内接收指标数据的数组。
CMA *MA;
double c_data[];
在 OnInit 函数中,我们应当初始化指标类的实例,并把初始数据传给它。
int OnInit() { //--- MA=new CMA; if(CheckPointer(MA)==POINTER_INVALID) return INIT_FAILED; //--- if(!MA.Create(_Symbol,PERIOD_CURRENT,MA_Period,MA_Shift,MA_Method,MA_Price)) return INIT_FAILED; MA.SetBufferSize(3); //--- return(INIT_SUCCEEDED); }
当 EA 工作完成的时候,在 OnDeinit 函数中清除内存并删除类的实例。
void OnDeinit(const int reason) { //--- if(CheckPointer(MA)!=POINTER_INVALID) delete MA; }
现在,这个类已经可以工作了,我们只需在 OnTick 函数中加上接收指标数据就可以了。在函数的开始,检查是否有新的柱形开启,然后调用类的 CopyBuffer 方法,再后面是您自己的用于处理信号和进行交易的代码。
void OnTick() { //--- static datetime last_bar=0; datetime cur_date=(datetime)SeriesInfoInteger(_Symbol,PERIOD_CURRENT,SERIES_LASTBAR_DATE); if(last_bar==cur_date) return; last_bar=cur_date; //--- if(!MA.CopyBuffer(MAIN_LINE,0,3,c_data)) return; //--- // 在这里加上您用于处理信号和交易操作的代码 //--- return; }
所有程序和类的完整代码都在附件中。
另一个重要的问题是,指标代码的迁移对 EA 的运行有什么影响?为了回答它,我们应当进行一些实验。
我已经提到过,我选择 MA 指标不是偶然的,现在我们可以检查使用三种方法来取得相同数据的速度:
我首先想到的是使用 MetaEditor 的分析功能,为此,让我们创建一个不进行交易的 EA,它将同时所有三个来源接收数据,我想,在这里没有必要全部描述 EA 的操作,它的代码都在附件中有提供。为了保持实验的完整性,所有三个来源都只在新的烛形开启时进行访问,
分析是在策略测试器中,以M15时段使用了15个月来进行的,实验取得了下面的数据。
函数 | 平均执行时间,毫秒 | 占总时间的比例 |
---|---|---|
OnTick | 99.14% | |
检查是否有新柱开启 | 0.528 | 67.23% |
内部计算 |
21.524 | 2.36% |
包含 CopyClose | 1.729 | 0.19% |
iMA | 2.231 | 0.24% |
iCustom | 0.748 | 0.08% |
OnInit | 241439 | 0.86% |
接收 iCustom 句柄 | 235676 | 0.84% |
我们首先注意到的是通过 iCustom 函数取得指标句柄花费了很长时间,它所花的时间比初始化指标类和通过iMA函数取得指标句柄多了超过10倍的时间。同时,从使用 iCustom 函数初始化的指标接收数据要比从 iMA 指标取得数据快3倍,比在类中计算指标值快30倍。
让我们详细探讨在我们的指标类中不同函数的执行时间,请注意,使用 CopyClose 函数取得历史数据的时间和获得指标数据的时间差不多,这是否意味着指标的计算几乎没有花费时间呢?现实证明不是这样。MetaTrader 5 的架构对指标值的访问是异步的,换句话说,当指标句柄接收到时,它就被附加到图表上,然后,指标所进行的计算就在 EA 流程之外。它们只是在数据传输的时候有互操作,和接收时间序列数据类似。所以,用于进行这些操作的时间是差不多的。
让我们总结一下在这个实验中我们的收获: 我们证明了,使用 MetaEditor 的分析功能来估算 EA 中指标的计算时间是有缺陷的。
创建四个独立的 EA。
随后,在策略测试器中进行它们的优化,进行11次通过再比较一个通过的平均时间。
测试结果显示,当在 EA 内部进行计算的时候可以节约时间,而花费时间最多的任务是从自定义指标中取的数据。
请注意,在实验中 MA 是根据收盘价进行计算的,这样的指标计算是非常简单的,如果有更加复杂的计算,情况会有什么改变吗?让我们通过另一个实验来找到答案。
这个实验重复了前一个实验,但是为了增加计算负担,指标使用的是加权平均价格的线性加权平均来计算的。
我们可以看到,所有获得数据的方法都花费了更多的时间,单个通过所花费的时间也都按比例增加,确认了前一个实验的结果。
本文描述了把指标计算迁移到 EA 交易的方法,使用 OOP 使得对最终指标数据的访问与从指标缓冲区中取得数据的标准方法可以尽可能地接近,在 EA 交易的源代码需要改写时对源代码的影响最小。
根据所进行的实验,这样的方法也可以在测试和对 EA 的优化中节约时间。但是,当 EA 交易在实时工作时,这个有点可能会无法凸显,因为 MetaTrader 5 的架构是多线程的。
# |
名称 |
类型 |
描述 |
---|---|---|---|
1 | Indicarot.mqh | 类库 | 用于迁移指标的基类 |
2 | MA.mqh | 类库 | 用于在 EA 内计算 MA 指标的类 |
3 | Test.mq5 | EA | 用于进行实验1的 EA |
4 | Test_Class.mq5 | EA | 在交易机器人内部进行指标计算的 EA (实验 2 和 3) |
5 | Test_iMA.mq5 | EA | 通过 iMA 来取得指标数据的 EA |
6 | Test_iCustom.mq5 | EA | 通过 iCustom 来取得指标数据的 EA |
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...