DoEasy 函数库中的时间序列(第三十九部分):基于函数库的指标 - 准备数据和时间序列事件

24 七月 2020, 15:01
Artyom Trishkin
0
956

内容


概念

到目前为止,我们所做的一切仅与 EA 和脚本有关。 它与指标无任何关系。 然而,时间序列也能胜任指标中各种计算的数据源。 因此,到研究它们的时候了。

与 EA 不同,指标具有完全不同的架构。 每个指标均在其启动的单个品种的单线程中执行。 这意味着,如果我们在同一品种的多个图表上启动了不同的指标,则它们都运行在同一品种所有图表所属的线程之中。
相应地,如果其中一个指标的体系结构有缺陷,则会拖累整个品种线程的速度。 在这种情况下,在同一线程中操作的所有其他指标会被冻结,并等待“慢速”指标。

为避免在操控指标时因等待历史数据而出现延迟,终端拥有顺序返回所请求数据的功能 — 激活加载历史数据的函数可立即返回函数结果,而无需等待。

利用 Copy 函数请求任何品种的任何时间序列数据,当终端发送历史数据之时,指标和 EA 会表现出不同的行为:

当所请求数据来自指标时,如果所请求的时间序列尚未构建,或者需从服务器下载它们,但加载/构建本身正在初始化,则函数立即返回 -1。

当所请求数据来自 EA 或脚本时,如果终端本地没有相应的数据,或者可以从本地历史数据构建所需的时间序列,但它们还没有准备好,即会启动从服务器下载。 该函数返回超时到期之前准备就绪的数据量,但历史记录下载会继续,且该函数在下一个相似请求期间返回更多数据。

因此,我们可以看到,当从 EA 请求数据时,终端将开始下载数据(若本地尚无已请求的数据,或者它不够用)。 直至超时,该函数将返回等待历史记录下载完成时已存在的历史记录数量 — 终端立即尝试向我们提供所请求的历史记录。 如果本地数据不足,它将尝试下载所需数量的数据。
同时,程序等待数据下载。

在指标的情况下,我们不能等待,因此终端向我们报告它已拥有的数量(或报告什么都没有)。 如果在第一次数据请求期间没有本地历史记录或记录不足,则开始下载。 在此,系统在超时之前不会等待,直至缺失的数据下载完成。
在当前状况下,程序应在下一次即时报价之前退出计算部分。 在新的即时报价来临时,会启动指标的 OnCalculate() 应答程序,期间计算所需数据可能已部分或全部加载。 在此,我们应决定多少数据可足以无缝运行程序算法。

此外,指标不应尝试下载其自身的数据 — 指标启动所在品种和周期的数据。 否则,这种请求可能导致冲突。 终端子系统会为指标下载此类数据。 它在 OnCalculate() 应答程序的 rates_total prev_calculated 变量中为我们提供有关数量和状态的所有数据。

根据这些最低需求,我们要调整一些时间序列类,并在指标中安排加载正确的初始数据,以便进行计算。

在本文中,我们打算调整已经创建的类,安排程序中所有用到的时间序列加载正确的初始数据,并将所有用到的时间序列的所有事件在实时更新期间发送到控制程序所在图表。

改进操控指标的类,创建时间序列事件

首先,我们将新消息添加到 Datas.mqh 文件 — 消息索引

   MSG_LIB_SYS_FAILED_PREPARING_SYMBOLS_ARRAY,        // Failed to prepare array of used symbols. Error 
   MSG_LIB_SYS_FAILED_GET_SYMBOLS_ARRAY,              // Failed to get array of used symbols.
   MSG_LIB_SYS_ERROR_EMPTY_PERIODS_STRING,            // Error. The string of predefined periods is empty and is to be used

...

//--- CBar
   MSG_LIB_TEXT_BAR_FAILED_GET_BAR_DATA,              // Failed to receive bar data
   MSG_LIB_TEXT_BAR_FAILED_DT_STRUCT_WRITE,           // Failed to write time to time structure
   MSG_LIB_TEXT_BAR_FAILED_GET_SERIES_DATA,           // Failed to receive timeseries data

...

   MSG_LIB_TEXT_TS_TEXT_SYMBOL_TERMINAL_FIRSTDATE,    // The very first date in history by a symbol in the client terminal
   MSG_LIB_TEXT_TS_TEXT_CREATED_OK,                   // successfully created
   MSG_LIB_TEXT_TS_TEXT_NOT_CREATED,                  // not created
   MSG_LIB_TEXT_TS_TEXT_IS_SYNC,                      // synchronized
   MSG_LIB_TEXT_TS_TEXT_ATTEMPT,                      // Attempt:
   MSG_LIB_TEXT_TS_TEXT_WAIT_FOR_SYNC,                // Waiting for data synchronization ...

  };
//+------------------------------------------------------------------+

和与新添加的索引相对应的消息文本

   {"Не удалось подготовить массив используемых символов. Ошибка ","Failed to create an array of used symbols. Error "},
   {"Не удалось получить массив используемых символов","Failed to get array of used symbols"},
   {"Ошибка. Строка предопределённых периодов пустая, будет использоваться ","Error. String of predefined periods is empty, the Period will be used: "},

...

   {"Не удалось получить данные бара","Failed to get bar data"},
   {"Не удалось записать время в структуру времени","Failed to write time to datetime structure"},
   {"Не удалось получить данные таймсерии","Failed to get timeseries data"},

...

   {"Самая первая дата в истории по символу в клиентском терминале","Very first date in history of symbol in client terminal"},
   {"создана успешно","created successfully"},
   {"не создана","not created"},
   {"синхронизирована","synchronized"},
   {"Попытка: ","Attempt: "},
   {"Ожидание синхронизации данных ...","Waiting for data synchronization ..."},
   
  };
//+---------------------------------------------------------------------+

在 \MQL5\Include\DoEasy\Objects\BaseObj.mqh 文件里,所有函数库对象的 CBaseObj 基准对象类构造函数中,我修改了 m_available 变量的初始化。 在创建过程中,所有从 CBaseObj 基准对象派生的对象属性都置为在程序中“已用”状态( true )。 以前,在初始化时该状态值初始化为“未使用” false

//--- Constructor
                     CBaseObj() : m_program((ENUM_PROGRAM_TYPE)::MQLInfoInteger(MQL_PROGRAM_TYPE)),
                                  m_global_error(ERR_SUCCESS),
                                  m_log_level(LOG_LEVEL_ERROR_MSG),
                                  m_chart_id_main(::ChartID()),
                                  m_chart_id(::ChartID()),
                                  m_folder_name(DIRECTORY),
                                  m_sound_name(""),
                                  m_name(__FUNCTION__),
                                  m_type(0),
                                  m_use_sound(false),
                                  m_available(true),
                                  m_first_start(true) {}
  };
//+------------------------------------------------------------------+

在 \MQL5\Include\DoEasy\Objects\BaseObj.mqh 里,所有 CBaseObjExt 库对象的扩展基准对象类中,修改了在对象中设置检测到事件标志的方法名称

//--- Set/return the occurred event flag to the object data
   void              SetEventFlag(const bool flag)                   { this.m_is_event=flag;                   }

以前,该方法m名为 SetEvent(),由于 SetEvent 可能意味着任何事件的创建、设置、发送、等等,而不是为事件设置存在的信号标志,故容易引发混淆。

所以,用到该方法的类文件也进行了修改 — 调用的 SetEvent() 方法已被 SetEventFlag() 所取代。 在附件里可找到详细信息。

由于在指标中禁用了交易功能,因此需修改交易对象类。
在跨平台交易对象类所在的文件 \MQL5\Include\DoEasy\Objects\Trade\TradeObj.mqh 里,于所有方法的开头,键入程序类型检查。 如果这是指标或服务,则离开方法并返回 true

//+------------------------------------------------------------------+
//| Open a position                                                  |
//+------------------------------------------------------------------+
bool CTradeObj::OpenPosition(const ENUM_POSITION_TYPE type,
                             const double volume,
                             const double sl=0,
                             const double tp=0,
                             const ulong magic=ULONG_MAX,
                             const string comment=NULL,
                             const ulong deviation=ULONG_MAX,
                             const ENUM_ORDER_TYPE_FILLING type_filling=WRONG_VALUE)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

...

//+------------------------------------------------------------------+
//| Close a position                                                 |
//+------------------------------------------------------------------+
bool CTradeObj::ClosePosition(const ulong ticket,
                              const string comment=NULL,
                              const ulong deviation=ULONG_MAX)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

...

//+------------------------------------------------------------------+
//| Close a position partially                                       |
//+------------------------------------------------------------------+
bool CTradeObj::ClosePositionPartially(const ulong ticket,
                                       const double volume,
                                       const string comment=NULL,
                                       const ulong deviation=ULONG_MAX)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

...

//+------------------------------------------------------------------+
//| Close a position by an opposite one                              |
//+------------------------------------------------------------------+
bool CTradeObj::ClosePositionBy(const ulong ticket,const ulong ticket_by)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

...

//+------------------------------------------------------------------+
//| Modify a position                                                |
//+------------------------------------------------------------------+
bool CTradeObj::ModifyPosition(const ulong ticket,const double sl=WRONG_VALUE,const double tp=WRONG_VALUE)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

...

//+------------------------------------------------------------------+
//| Set an order                                                     |
//+------------------------------------------------------------------+
bool CTradeObj::SetOrder(const ENUM_ORDER_TYPE type,
                         const double volume,
                         const double price,
                         const double sl=0,
                         const double tp=0,
                         const double price_stoplimit=0,
                         const ulong magic=ULONG_MAX,
                         const string comment=NULL,
                         const datetime expiration=0,
                         const ENUM_ORDER_TYPE_TIME type_time=WRONG_VALUE,
                         const ENUM_ORDER_TYPE_FILLING type_filling=WRONG_VALUE)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

...

//+------------------------------------------------------------------+
//| Remove an order                                                  |
//+------------------------------------------------------------------+
bool CTradeObj::DeleteOrder(const ulong ticket)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

...

//+------------------------------------------------------------------+
//| Modify an order                                                  |
//+------------------------------------------------------------------+
bool CTradeObj::ModifyOrder(const ulong ticket,
                            const double price=WRONG_VALUE,
                            const double sl=WRONG_VALUE,
                            const double tp=WRONG_VALUE,
                            const double price_stoplimit=WRONG_VALUE,
                            const datetime expiration=WRONG_VALUE,
                            const ENUM_ORDER_TYPE_TIME type_time=WRONG_VALUE,
                            const ENUM_ORDER_TYPE_FILLING type_filling=WRONG_VALUE)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

在 \MQL5\Include\DoEasy\Trading.mqh 里,函数库主要交易类的所有同名交易方法都经过了同样的修改。

由于被禁用,故在程序里以这种方式退出交易方法不能调用交易函数,并返回方法成功执行,从而防止函数库错误。

现在,我们来研究那些直接影响时间序列对象类的修改。

在柱线对象类中,我略微修改了一下文本显示部分,当创建柱线对象时,类构造函数接收历史数据可能会发生错误。 现在文本显示里还含有所创建对象时间序列的构造器编号品种时间帧
在第一种形式的构造器里,检查数据提取错误,并在单独的模块中将时间写入时间结构

//+------------------------------------------------------------------+
//| Constructor 1                                                    |
//+------------------------------------------------------------------+
CBar::CBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index)
  {
   this.m_type=COLLECTION_SERIES_ID;
   MqlRates rates_array[1];
   this.SetSymbolPeriod(symbol,timeframe,index);
   ::ResetLastError();
//--- If ailed to get the requested data by index and write bar data to the MqlRates array,
//--- display an error message, create and fill the structure with zeros, and write it to the rates_array array
   if(::CopyRates(symbol,timeframe,index,1,rates_array)<1)
     {
      int err_code=::GetLastError();
      ::Print
        (
         DFUN,"(1) ",symbol," ",TimeframeDescription(timeframe)," ",
         CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_GET_BAR_DATA),". ",
         CMessage::Text(MSG_LIB_SYS_ERROR)," ",CMessage::Text(err_code)," ",
         CMessage::Retcode(err_code)
        );
      MqlRates err={0};
      rates_array[0]=err;
     }
   ::ResetLastError();
//--- If failed to set time to the time structure, display the error message
   if(!::TimeToStruct(rates_array[0].time,this.m_dt_struct))
     {
      int err_code=::GetLastError();
      ::Print
        (
         DFUN,"(1) ",symbol," ",TimeframeDescription(timeframe)," ",
         CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_DT_STRUCT_WRITE),". ",
         CMessage::Text(MSG_LIB_SYS_ERROR)," ",CMessage::Text(err_code)," ",
         CMessage::Retcode(err_code)
        );
     }
//--- Set the bar properties
   this.SetProperties(rates_array[0]);
  }
//+------------------------------------------------------------------+
//| Constructor 2                                                    |
//+------------------------------------------------------------------+
CBar::CBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index,const MqlRates &rates)
  {
   this.m_type=COLLECTION_SERIES_ID;
   this.SetSymbolPeriod(symbol,timeframe,index);
   ::ResetLastError();
//--- If failed to set time to the time structure, display the error message,
//--- create and fill the structure with zeros, set the bar properties from this structure and exit
   if(!::TimeToStruct(rates.time,this.m_dt_struct))
     {
      int err_code=::GetLastError();
      ::Print
        (
         DFUN,"(2) ",symbol," ",TimeframeDescription(timeframe)," ",
         CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_DT_STRUCT_WRITE),". ",
         CMessage::Text(MSG_LIB_SYS_ERROR)," ",CMessage::Text(err_code)," ",
         CMessage::Retcode(err_code)
        );
      MqlRates err={0};
      this.SetProperties(err);
      return;
     }
//--- Set the bar properties
   this.SetProperties(rates);
  }
//+------------------------------------------------------------------+

如果柱线对象创建错误,这些操作将为我们提供更多数据。

由于我们需要 OnCalculate() 应答程序提供的时间序列数组,以便请求有关当前品种周期上的柱线数量及其数值的数据,之后我们需要以某种方式将这些数组和数值传递给库类。
为此,在 \MQL5\Include\DoEasy\Defines.mqh 里创建结构。 该结构是为了存储变量,该变量将当前时间序列所有已计算的必要数据传递给函数库时间序列:

//+------------------------------------------------------------------+
//| Structures                                                       |
//+------------------------------------------------------------------+
struct SDataCalculate
  {
   int         rates_total;                                 // size of input time series
   int         prev_calculated;                             // number of handled bars at the previous call
   int         begin;                                       // where significant data start
   double      price;                                       // current array value for calculation
   MqlRates    rates;                                       // Price structure
  } rates_data;
//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Search and sorting data                                          |
//+------------------------------------------------------------------+

正如我们所见,该结构包含需要传递给函数库的所有必要字段,对应指标 OnCalculate() 应答程序的任何实现 。

对于应答程序的第一种形式

int OnCalculate(
   const int        rates_total,       // price[] array size
   const int        prev_calculated,   // number of handled bars at the previous call
   const int        begin,             // index number in the price[] array meaningful data starts from
   const double&    price[]            // array of values for calculation
   );

用到了 rates_total, prev_calculated, beginprice 变量结构。

对于应答程序的第二种形式

int OnCalculate(
   const int        rates_total,       // size of input time series
   const int        prev_calculated,   // number of handled bars at the previous call
   const datetime&  time{},            // Time array
   const double&    open[],            // Open array
   const double&    high[],            // High array
   const double&    low[],             // Low array
   const double&    close[],           // Close array
   const long&      tick_volume[],     // Tick Volume array
   const long&      volume[],          // Real Volume array
   const int&       spread[]           // Spread array
   );

使用了 rates_totalprev_calculated 变量结构,以及 MqlRates 汇率结构存储数组值。

当前的结构实现仅适合将单根柱线数值传递给函数库。

在位于 \MQL5\Include\DoEasy\Objects\Series\Series.mqh 里的 CSeries 类中,将设置服务器日期的标志添加到设置品种和时间帧的方法之中:

//--- Set (1) symbol, (2) timeframe, (3) symbol and timeframe, (4) amount of applied timeseries data
   void              SetSymbol(const string symbol,const bool set_server_date=false);
   void              SetTimeframe(const ENUM_TIMEFRAMES timeframe,const bool set_server_date=false);

默认情况下,该标志为禁用状态。 这可防止在调用方法时设置服务器日期,因为为了调用设置服务器日期的方法首先要检查标志状态

//+------------------------------------------------------------------+
//| Set a symbol                                                     |
//+------------------------------------------------------------------+
void CSeries::SetSymbol(const string symbol,const bool set_server_date=false)
  {
   if(this.m_symbol==symbol)
      return;
   this.m_symbol=(symbol==NULL || symbol==""   ? ::Symbol() : symbol);
   this.m_new_bar_obj.SetSymbol(this.m_symbol);
   if(set_server_date)
      this.SetServerDate();
  }
//+------------------------------------------------------------------+
//| Set a timeframe                                                  |
//+------------------------------------------------------------------+
void CSeries::SetTimeframe(const ENUM_TIMEFRAMES timeframe,const bool set_server_date=false)
  {
   if(this.m_timeframe==timeframe)
      return;
   this.m_timeframe=(timeframe==PERIOD_CURRENT ? (ENUM_TIMEFRAMES)::Period() : timeframe);
   this.m_new_bar_obj.SetPeriod(this.m_timeframe);
   this.m_period_description=TimeframeDescription(this.m_timeframe);
   if(set_server_date)
      this.SetServerDate();
  }
//+------------------------------------------------------------------+

这样做是为了避免同时调用设置品种和时间帧的方法时多次重置服务器日期:

//+------------------------------------------------------------------+
//| Set a symbol and timeframe                                       |
//+------------------------------------------------------------------+
void CSeries::SetSymbolPeriod(const string symbol,const ENUM_TIMEFRAMES timeframe)
  {
   if(this.m_symbol==symbol && this.m_timeframe==timeframe)
      return;
   this.SetSymbol(symbol);
   this.SetTimeframe(timeframe,true);
  }
//+------------------------------------------------------------------+

在此,首先调用设置品种方法(禁用标记)随后是设置时间帧方法(启用标记),从设置时间帧方法里调用设置服务器日期的方法。

刷新时间序列数据的方法现在传递 OnCalculate() 应答程序数据的新结构,替代了原本的全部数组列表:

//--- (1) Create and (2) update the timeseries list
   int               Create(const uint required=0);
   void              Refresh(SDataCalculate &data_calculate);
                            
//--- Create and send the "New bar" event to the control program chart
   void              SendEvent(void);

因此,Refresh() 方法实现现在能访问结构数据而不是原本的数组:

//+------------------------------------------------------------------+
//| Update timeseries list and data                                  |
//+------------------------------------------------------------------+
void CSeries::Refresh(SDataCalculate &data_calculate)
  {
//--- If the timeseries is not used, exit
   if(!this.m_available)
      return;
   MqlRates rates[1];
//--- Set the flag of sorting the list of bars by index
   this.m_list_series.Sort(SORT_BY_BAR_INDEX);
//--- If a new bar is present on a symbol and period,
   if(this.IsNewBarManual(data_calculate.rates.time))
     {
      //--- create a new bar object and add it to the end of the list
      CBar *new_bar=new CBar(this.m_symbol,this.m_timeframe,0);
      if(new_bar==NULL)
         return;
      if(!this.m_list_series.InsertSort(new_bar))
        {
         delete new_bar;
         return;
        }
      //--- Write the very first date by a period symbol at the moment and the new time of opening the last bar by a period symbol 
      this.SetServerDate();
      //--- if the timeseries exceeds the requested number of bars, remove the earliest bar
      if(this.m_list_series.Total()>(int)this.m_required)
         this.m_list_series.Delete(0);
      //--- save the new bar time as the previous one for the subsequent new bar check
      this.SaveNewBarTime(data_calculate.rates.time);
     }
//--- Get the bar object from the list by the terminal timeseries index (zero bar)
   CBar *bar=this.GetBarBySeriesIndex(0);
//--- if the work is performed in an indicator and the timeseries belongs to the current symbol and timeframe,
//--- copy price parameters (passed to the method from the outside) to the bar price structure
   int copied=1;
   if(this.m_program==PROGRAM_INDICATOR && this.m_symbol==::Symbol() && this.m_timeframe==(ENUM_TIMEFRAMES)::Period())
     {
      rates[0].time=data_calculate.rates.time;
      rates[0].open=data_calculate.rates.open;
      rates[0].high=data_calculate.rates.high;
      rates[0].low=data_calculate.rates.low;
      rates[0].close=data_calculate.rates.close;
      rates[0].tick_volume=data_calculate.rates.tick_volume;
      rates[0].real_volume=data_calculate.rates.real_volume;
      rates[0].spread=data_calculate.rates.spread;
     }
//--- otherwise, get data to the bar price structure from the environment
   else
      copied=::CopyRates(this.m_symbol,this.m_timeframe,0,1,rates);
//--- If the prices are obtained, set the new properties from the price structure for the bar object
   if(copied==1)
      bar.SetProperties(rates[0]);
  }
//+------------------------------------------------------------------+

现在可以通过比较两个时间序列对象的虚拟方法来按时间帧搜索时间序列对象列表:

//--- Comparison method to search for identical timeseries objects by timeframe
   virtual int       Compare(const CObject *node,const int mode=0) const 
                       {   
                        const CSeries *compared_obj=node;
                        return(this.Timeframe()>compared_obj.Timeframe() ? 1 : this.Timeframe()<compared_obj.Timeframe() ? -1 : 0);
                       } 
//--- Constructors
                     CSeries(void);
                     CSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const uint required=0);
  };
//+------------------------------------------------------------------+

该方法取两个可比较时间序列对象(当前的一个和传递给该方法的一个)的 “timeframe” 属性进行比对,如果它们相等,则返回零。
CObject 标准库基准对象衍生出的各种对象进行搜索和排序的方法,我们已审阅过相似的逻辑。 该方法在标准库的基准对象中定义为虚拟方法。 所以,它应该在后代对象中实现,若相等则该方法应返回零;若当前对象的属性值大于/小于被比较对象的属性值,则该方法应返回 1/-1。 >

由于首次访问时,若本地已下载历史数据不存在/不足的情况下,函数会直接返回,因此在设置所需数据量方法的最开始添加访问所需历史数据(仅请求当前柱线日期)。 如此就会开始下载所需数据(如果本地不存在):

//+------------------------------------------------------------------+
//| Set the number of required data                                  |
//+------------------------------------------------------------------+
bool CSeries::SetRequiredUsedData(const uint required,const uint rates_total)
  {
   this.m_required=(required<1 ? SERIES_DEFAULT_BARS_COUNT : required);
//--- Launch downloading historical data
   if(this.m_program!=PROGRAM_INDICATOR || (this.m_program==PROGRAM_INDICATOR && (this.m_symbol!=::Symbol() || this.m_timeframe!=::Period())))
     {
      datetime array[1];
      ::CopyTime(this.m_symbol,this.m_timeframe,0,1,array);
     }
//--- Set the number of available timeseries bars


当我们创建存储单个品种所有时间序列列表的对象(CTimeSeries 类)时,我们已做到了这一点,故此该对象始终含有一个列表,其中包含终端内所有可能用到的时间帧完整集合。 时间序列列表立即添加到该列表内。 但是,它们仅在必要时才会创建。 从 ENUM_TIMEFRAMES 枚举中按偏移量 1 取得时间帧列表索引所处相对应的常量索引(在文章中中讲述),执行完毕即可访问其指向的必要时间序列。

这样做是为了加速访问指向列表中的必要时间序列对象。 但事实证明,即时访问指针会伴随测试器的问题 — 可视测试器会创建几乎所有时间帧图表,无论它们在程序里是否实际引用,以及是否为其创建了时间序列列表。

此外,在程序运行期间切换图表周期时我们还有另一个问题 — 之前创建的列表不会重新创建,程序也不会替换为其他对象,而是继续跟踪不存在的对象事件。

虽然在 CTimeSeries 类对象中存储了全部时间帧的时间序列列表,但为避免一些隐藏错误的进一步积累,以及花费很长时间寻找其原因,我决定仅保存那些真正用到的时间序列指针。 换句话说,仅当程序明确指示需要用到、且实际创建了该时间序列对象时,才会将指向图表周期的那个时间序列指针添加到列表中。

打开 \MQL5\Include\DoEasy\Objects\Series\TimeSeries.mqh,并对其进行必要的改进。

现在,单个品种的时间序列类从所有函数库对象的扩展准础对象类派生出的
如此做是为了能够使用 CBaseObjExt 类的事件功能:

//+------------------------------------------------------------------+
//| Symbol timeseries class                                          |
//+------------------------------------------------------------------+
class CTimeSeries : public CBaseObjExt
  {

如今,在类的私密部分简单地声明按时间帧名称返回列表中时间序列索引的方法

//+------------------------------------------------------------------+
class CTimeSeries : public CBaseObjExt
  {
private:
   string            m_symbol;                                             // Timeseries symbol
   CNewTickObj       m_new_tick;                                           // "New tick" object
   CArrayObj         m_list_series;                                        // List of timeseries by timeframes
   datetime          m_server_firstdate;                                   // The very first date in history by a server symbol
   datetime          m_terminal_firstdate;                                 // The very first date in history by a symbol in the client terminal
//--- Return (1) the timeframe index in the list and (2) the timeframe by the list index
   int               IndexTimeframe(const ENUM_TIMEFRAMES timeframe);
   ENUM_TIMEFRAMES   TimeframeByIndex(const uchar index)             const { return TimeframeByEnumIndex(uchar(index+1));                       }
//--- Set the very first date in history by symbol on the server and in the client terminal
   void              SetTerminalServerDate(void)
                       {
                        this.m_server_firstdate=(datetime)::SeriesInfoInteger(this.m_symbol,::Period(),SERIES_SERVER_FIRSTDATE);
                        this.m_terminal_firstdate=(datetime)::SeriesInfoInteger(this.m_symbol,::Period(),SERIES_TERMINAL_FIRSTDATE);
                       }
public:

此刻,在类主体之外实现该方法:

//+------------------------------------------------------------------+
//| Return the timeframe index in the list                           |
//+------------------------------------------------------------------+
int CTimeSeries::IndexTimeframe(const ENUM_TIMEFRAMES timeframe)
  {
   const CSeries *obj=new CSeries(this.m_symbol,timeframe);
   if(obj==NULL)
      return WRONG_VALUE;
   this.m_list_series.Sort();
   int index=this.m_list_series.Search(obj);
   delete obj;
   return index;
  }
//+------------------------------------------------------------------+

该方法接收一个时间帧。 并应返回时间帧的时间序列指针。
接下来,依据所需时间帧创建一个临时时间序列对象
为时间序列对象列表设置列表已排序标志
,然后获取时间帧等于临时对象时间帧所对应的列表中时间序列对象索引。
如果列表中存在这个对象,则接收其索引,否则为 WRONG_VALUE(-1)。
删除临时对象,并返回得到的索引

代替 Create() 和 CreateAll() 方法,声明将指定时间序列添加到列表的方法创建指定时间序列对象的方法,而更新时间序列表的方法现在接收参数结构和 OnCalculate() 数组,替代了完整的数组列表:

//--- (1) Add the specified timeseries list to the list and create (2) the specified timeseries list
   bool              AddSeries(const ENUM_TIMEFRAMES timeframe,const uint required=0);
   bool              CreateSeries(const ENUM_TIMEFRAMES timeframe,const uint required=0);
//--- Update (1) the specified timeseries list and (2) all timeseries lists
   void              Refresh(const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate);
   void              RefreshAll(SDataCalculate &data_calculate);

//--- Compare CTimeSeries objects (by symbol)
   virtual int       Compare(const CObject *node,const int mode=0) const;
//--- Display (1) description and (2) short symbol timeseries description in the journal
   void              Print(const bool created=true);
   void              PrintShort(const bool created=true);
   
//--- Constructors
                     CTimeSeries(void){;}
                     CTimeSeries(const string symbol);
  };
//+------------------------------------------------------------------+

从类构造函数中删除创建时间序列表的循环

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CTimeSeries::CTimeSeries(const string symbol) : m_symbol(symbol)
  {
   this.m_list_series.Clear();
   this.m_list_series.Sort();
   for(int i=0;i<21;i++)
     {
      ENUM_TIMEFRAMES timeframe=this.TimeframeByIndex((uchar)i);
      CSeries *series_obj=new CSeries(this.m_symbol,timeframe);
      this.m_list_series.Add(series_obj);
     }
   this.SetTerminalServerDate();
   this.m_new_tick.SetSymbol(this.m_symbol);
   this.m_new_tick.Refresh();
  }
//+------------------------------------------------------------------+

现在,程序的 OnInit() 应答程序先为用到的时间序列创建数组,然后再创建必要的时间序列。 程序中用到的图表周期数量若有任何变化都会导致 EA 重新初始化或重新创建指标,故导致彻底重建所有用到的时间序列对象列表,和未来的调整。

在为所有用到的时间序列设置历史深度的方法 SetRequiredAllUsedData() ,和返回所有用到的时间序列同步标志的方法 SyncAllData() 里,将循环次数从所有可能的时间帧总数

//+------------------------------------------------------------------+
//| Set the history depth of all applied symbol timeseries           |
//+------------------------------------------------------------------+
bool CTimeSeries::SetRequiredAllUsedData(const uint required=0,const int rates_total=0)
  {
   if(this.m_symbol==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_TS_TEXT_FIRST_SET_SYMBOL));
      return false;
     }
   bool res=true;
   for(int i=0;i<21;i++)
     {
      CSeries *series_obj=this.m_list_series.At(i);
      if(series_obj==NULL)
         continue;
      res &=series_obj.SetRequiredUsedData(required,rates_total);
     }
   return res;
  }
//+------------------------------------------------------------------+

替换为列表里实际的时间序列对象数量:

   int total=this.m_list_series.Total();
   for(int i=0;i<total;i++)

现在,列表由实际创建的时间序列对象组成,且循环次数根据其实际数量执行。

将指定时间序列对象添加到列表的方法实现:

//+------------------------------------------------------------------+
//| Add the specified timeseries list to the list                    |
//+------------------------------------------------------------------+
bool CTimeSeries::AddSeries(const ENUM_TIMEFRAMES timeframe,const uint required=0)
  {
   bool res=false;
   CSeries *series=new CSeries(this.m_symbol,timeframe,required);
   if(series==NULL)
      return res;
   this.m_list_series.Sort();
   if(this.m_list_series.Search(series)==WRONG_VALUE)
      res=this.m_list_series.Add(series);
   if(!res)
      delete series;
   series.SetAvailable(true);
   return res;
  }
//+------------------------------------------------------------------+

该方法接收时间序列图周期,以便将其添加到品种时间序列列表之中

依据传递给方法的时间帧创建一个时间序列对象
为时间序列列表设置列表已排序标志,并在列表中搜索与新创建的对象相等的时间序列对象
如果列表不包含此类对象(搜索返回 -1),则将创建的时间序列对象添加到列表之中
否则,
删除所创建对象,因为该时间序列对象已经在列表中了。
为在程序中用到的时间序列设置标志,并返回将时间序列添加到列表的结果
成功添加将返回 true ,不成功 — false

该函数库为所有库对象的扩展对象提供事件功能,可将发生的事件发送到函数库的各种对象。 在第十六篇文章第十七篇文章里,我们研究过函数库事件处理的原理和逻辑。

简言之,从 CBaseObj 函数库基准对象派生出的每个对象(当前为 CBaseObjExt)都含有列表,该列表记录了单次即时报价或单次计时器迭代内程序一次循环操作期间该对象可能发生的所有事件。
识别任意对象事件时,都会为其设置事件已发生的标志。 接下来,可以在集合类中查看集合对象的列表。 反过来,在列表中检查标志。 如果找到带有启用事件标志的对象,则这些对象的集合类接收所有带有启用事件标志的对象事件的列表,并将所有事件从列表发送到控制程序的图表。
该程序本身拥有处理所有传入事件的功能。 在测试器中,所有事件均在即时报价来临时处理。 除了测试器之外,它们还在 OnChartEvent() 应答程序中进行处理。

在曾研究过单一品种所有时间序列的对象类 CTimeSeries 中,定义其所有时间序列列表事件的最佳位置是更新指定时间序列的 Refresh() 方法,和更新所有品种时间序列的 RefreshAll() 方法。

我们来研究更新时间序列列表的方法实现:

//+------------------------------------------------------------------+
//| Update a specified timeseries list                               |
//+------------------------------------------------------------------+
void CTimeSeries::Refresh(const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate)
  {
//--- Reset the timeseries event flag and clear the list of all timeseries events
   this.m_is_event=false;
   this.m_list_events.Clear();
//--- Get the timeseries from the list by its timeframe
   CSeries *series_obj=this.m_list_series.At(this.IndexTimeframe(timeframe));
   if(series_obj==NULL || series_obj.DataTotal()==0 || !series_obj.IsAvailable())
      return;
//--- Update the timeseries list
   series_obj.Refresh(data_calculate);
//--- If the timeseries object features the New bar event
   if(series_obj.IsNewBar(data_calculate.rates.time))
     {
      //--- send the "New bar" event to the control program chart
      series_obj.SendEvent();
      //--- set the values of the first date in history on the server and in the terminal
      this.SetTerminalServerDate();
      //--- add the "New bar" event to the list of timeseries events
      //--- in case of successful addition, set the event flag for the timeseries
      if(this.EventAdd(SERIES_EVENTS_NEW_BAR,series_obj.Time(0),series_obj.Timeframe(),series_obj.Symbol()))
         this.m_is_event=true;
     }
  }
//+------------------------------------------------------------------+
//| Update all timeseries lists                                      |
//+------------------------------------------------------------------+
void CTimeSeries::RefreshAll(SDataCalculate &data_calculate)
  {
//--- Reset the flags indicating the necessity to set the first date in history on the server and in the terminal
//--- and the timeseries event flag, and clear the list of all timeseries events
   bool upd=false;
   this.m_is_event=false;
   this.m_list_events.Clear();
//--- In the loop by the list of all used timeseries,
   int total=this.m_list_series.Total();
   for(int i=0;i<total;i++)
     {
      //--- get the next timeseries object by the loop index
      CSeries *series_obj=this.m_list_series.At(i);
      if(series_obj==NULL || !series_obj.IsAvailable() || series_obj.DataTotal()==0)
         continue;
      //--- update the timeseries list
      series_obj.Refresh(data_calculate);
      //--- If the timeseries object features the New bar event
      if(series_obj.IsNewBar(data_calculate.rates.time))
        {
         //--- send the "New bar" event to the control program chart,
         series_obj.SendEvent();
         //--- set the flag indicating the necessity to set the first date in history on the server and in the terminal
         upd=true;
         //--- add the "New bar" event to the list of timeseries events
         //--- in case of successful addition, set the event flag for the timeseries
         if(this.EventAdd(SERIES_EVENTS_NEW_BAR,series_obj.Time(0),series_obj.Timeframe(),series_obj.Symbol()))
            this.m_is_event=true;
        }
     }
//--- if the flag indicating the necessity to set the first date in history on the server and in the terminal is enabled,
//--- set the values of the first date in history on the server and in the terminal
   if(upd)
      this.SetTerminalServerDate();
  }
//+------------------------------------------------------------------+

在此,我针对每个方法的代码都进行了注释,故所有内容都应该很清晰。 如果您有任何疑问,请随时在下面的评论中提问。

至此,针对单一品种所有时间序列对象的 CTimeSeries 类完成。

下一个类是品种时间序列对象的 CTimeSeriesCollection 集合类。 它也应拥有事件功能,因为它“负责”从所有对象中获取事件列表,这些对象存储程序用到的每个品种的所有时间序列。

打开 \MQL5\Include\DoEasy\Collections\TimeSeriesCollection.mqh,并自所有库对象的扩展基类派生它:

//+------------------------------------------------------------------+
//| Symbol timeseries collection                                     |
//+------------------------------------------------------------------+
class CTimeSeriesCollection : public CBaseObjExt
  {

在该类的公开部分,声明两个方法:一个返回指定品种所有时间序列的对象,另一个返回指定品种和周期的时间序列对象

public:
//--- Return (1) oneself and (2) the timeseries list
   CTimeSeriesCollection  *GetObject(void)            { return &this;         }
   CArrayObj              *GetList(void)              { return &this.m_list;  }
//--- Return (1) the timeseries object of the specified symbol and (2) the timeseries object of the specified symbol/period
   CTimeSeries            *GetTimeseries(const string symbol);
   CSeries                *GetSeries(const string symbol,const ENUM_TIMEFRAMES timeframe);

我们在类主体之外编写其实现。
该方法返回指定品种的时间序列对象:

//+------------------------------------------------------------------+
//| Return the timeseries object of the specified symbol             |
//+------------------------------------------------------------------+
CTimeSeries *CTimeSeriesCollection::GetTimeseries(const string symbol)
  {
   int index=this.IndexTimeSeries(symbol);
   if(index==WRONG_VALUE)
      return NULL;
   CTimeSeries *timeseries=this.m_list.At(index);
   return timeseries;
  }
//+------------------------------------------------------------------+

在此,我们利用在第三十七篇文章里研究过的 IndexTimeSeries() 方法,获取命名品种时间序列对象的索引。 获取的索引可从列表中获取时间序列对象。。 如果从列表中获取索引或对象失败,则返回 NULL。 否则,我们就获得列表中所请求对象的指针。

该方法返回指定品种/周期的时间序列对象:

//+------------------------------------------------------------------+
//| Return the timeseries object of the specified symbol/period      |
//+------------------------------------------------------------------+
CSeries *CTimeSeriesCollection::GetSeries(const string symbol,const ENUM_TIMEFRAMES timeframe)
  {
   CTimeSeries *timeseries=this.GetTimeseries(symbol);
   if(timeseries==NULL)
      return NULL;
   CSeries *series=timeseries.GetSeries(timeframe);
   return series;
  }
//+-----------------------------------------------------------------------+

在此,我们利用 GetTimeseries() 方法(上在上面研究过)按传递给该方法的品种来获取时间序列对象
从获取的时间序列对象里,按指定的时间帧获取时间序列列表,然后返回获取的时间序列对象指针

时间序列对象的 GetSeries() 方法调用上述的 IndexTimeframe() 方法返回所需时间序列,而 CTimeSeries 时间序列对象的 GetSeries() 方法如下所示:

CSeries *GetSeries(const ENUM_TIMEFRAMES timeframe) { return this.m_list_series.At(this.IndexTimeframe(timeframe)); }

在该类的公开部分,删除三个创建时间序列的方法仅保留按指定品种创建指定时间序列的那一个

//--- Create (1) the specified timeseries of the specified symbol, (2) the specified timeseries of all symbols,
//--- (3) all timeseries of the specified symbol and (4) all timeseries of all symbols
   bool                    CreateSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const uint required=0);
   bool                    CreateSeries(const ENUM_TIMEFRAMES timeframe,const uint required=0);
   bool                    CreateSeries(const string symbol,const uint required=0);
   bool                    CreateSeries(const uint required=0);
//--- Update (1) the specified timeseries of the specified symbol, (2) the specified timeseries of all symbols,
//--- (3) all timeseries of the specified symbol and (4) all timeseries of all symbols and (5) all timeseries except for the current symbol

到目前为止,三个被删除的方法似乎是多余的。 因此,我们声明三个新方法来替代 — 重建指定的时间序列返回空的时间序列,和返回已部分填充的时间序列

//--- (1) Create and (2) re-create a specified timeseries of a specified symbol
   bool                    CreateSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0);
   bool                    ReCreateSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0);
//--- Return (1) an empty, (2) partially filled timeseries
   CSeries                *GetSeriesEmpty(void);
   CSeries                *GetSeriesIncompleted(void);

为什么我们需要重新创建时间序列呢? 在初始化函数库并创建所有品种的所有时间序列时,我们会用函数来启动历史数据下载。 正如我多次提到的那样,如果程序是一个指标,并引用了它启动所在的品种和时间表,则可能会引起冲突。 因此,这种状况应跳过。 一旦完成并进入 OnCalculate() 应答程序后,我们应首先修订所创建时间序列,获取一个空序列(在初始化过程中跳过),然后依据 OnCalculate() 里 rates_total 变量数值重新创建它。

现在,时间序列刷新方法接收数据结构,替代原本从 OnCalculate() 获取的时间序列数组。 声明从时间序列对象获取事件,并将它们添加到该品种时间序列集合内所有对象的事件列表的方法

//--- Update (1) the specified timeseries of the specified symbol, (2) all timeseries of all symbols
   void                    Refresh(const string symbol,const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate);
   void                    Refresh(SDataCalculate &data_calculate);

//--- Get events from the timeseries object and add them to the list
   bool                    SetEvents(CTimeSeries *timeseries);

//--- Display (1) the complete and (2) short collection description in the journal
   void                    Print(const bool created=true);
   void                    PrintShort(const bool created=true);
   
//--- Constructor
                           CTimeSeriesCollection();
  };
//+------------------------------------------------------------------+

返回部分填充时间序列的方法实现:

//+------------------------------------------------------------------+
//|Return the empty (created but not filled with data) timeseries    |
//+------------------------------------------------------------------+
CSeries *CTimeSeriesCollection::GetSeriesEmpty(void)
  {
//--- In the loop by the timeseries object list
   int total_timeseries=this.m_list.Total();
   for(int i=0;i<total_timeseries;i++)
     {
      //--- get the next object of all symbol timeseries by the loop index
      CTimeSeries *timeseries=this.m_list.At(i);
      if(timeseries==NULL || !timeseries.IsAvailable())
         continue;
      //--- get the list of timeseries objects from the object of all symbol timeseries
      CArrayObj *list_series=timeseries.GetListSeries();
      if(list_series==NULL)
         continue;
      //--- in the loop by the symbol timeseries list
      int total_series=list_series.Total();
      for(int j=0;j<total_series;j++)
        {
         //--- get the next timeseries
         CSeries *series=list_series.At(j);
         if(series==NULL || !series.IsAvailable())
            continue;
         //--- if the timeseries has no bar objects,

         //--- return the pointer to the timeseries
         if(series.DataTotal()==0)
            return series;
        }
     }
   return NULL;
  }
//+------------------------------------------------------------------+
//| Return partially filled timeseries                               |
//+------------------------------------------------------------------+
CSeries *CTimeSeriesCollection::GetSeriesIncompleted(void)
  {
//--- In the loop by the timeseries object list
   int total_timeseries=this.m_list.Total();
   for(int i=0;i<total_timeseries;i++)
     {
      //--- get the next object of all symbol timeseries by the loop index
      CTimeSeries *timeseries=this.m_list.At(i);
      if(timeseries==NULL || !timeseries.IsAvailable())
         continue;
      //--- get the list of timeseries objects from the object of all symbol timeseries
      CArrayObj *list_series=timeseries.GetListSeries();
      if(list_series==NULL)
         continue;
      //--- in the loop by the symbol timeseries list
      int total_series=list_series.Total();
      for(int j=0;j<total_series;j++)
        {
         //--- get the next timeseries
         CSeries *series=list_series.At(j);
         if(series==NULL || !series.IsAvailable())
            continue;
         //--- if the timeseries has bar objects,
         //--- but their number is not equal to the requested and available one for the symbol,
         //--- return the pointer to the timeseries
         if(series.DataTotal()>0 && series.AvailableUsedData()!=series.DataTotal())
            return series;
        }
     }
   return NULL;
  }
//+------------------------------------------------------------------+

除了部分填充时间序列验证方法之外,每个方法的代码都相似且带有注释。

该方法返回满足搜索条件的第一个即将到来的时间序列。 这样做是为了在每个后续即时报价来临时(进入 OnCalculate),连续获取所有可能的空或部分填充的时间序列。 这对应于 MetaQuotes 有关正确处理指标中数据不足情况的建议 — 退出应答程序,并在下一次即时报价来临时检查数据是否存在。

按指定品种创建指定时间序列的方法实现:

//+------------------------------------------------------------------+
//| Create the specified timeseries of the specified symbol          |
//+------------------------------------------------------------------+
bool CTimeSeriesCollection::CreateSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0)
  {
   CTimeSeries *timeseries=this.GetTimeseries(symbol);
   if(timeseries==NULL)
      return false;
   if(!timeseries.AddSeries(timeframe,required))
      return false;
   if(!timeseries.SyncData(timeframe,required,rates_total))
      return false;
   return timeseries.CreateSeries(timeframe,required);
  }
//+------------------------------------------------------------------+

该方法将数据添加到单个品种的时间序列对象之中 — 一个指定图表周期的新时间序列。
方法接收品种所需的时间序列周期
获取时间序列对象,并向其添加指定图表周期的新时间序列。
请求品种/周期数据,并为时间序列设置所需的数据量
如果之前的所有操作均成功,则返回创建新时间序列的结果,并向其内添加数据

在前面的文章里,我们已研究了所有这些方法。 在此,我已介绍完毕创建所需品种/周期时间序列的新逻辑。 该逻辑与第三十七篇文章中所述的不同。

按指定品种重新创建指定时间序列的方法实现:

//+------------------------------------------------------------------+
//| Re-create a specified timeseries of a specified symbol           |
//+------------------------------------------------------------------+
bool CTimeSeriesCollection::ReCreateSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0)
  {
   CTimeSeries *timeseries=this.GetTimeseries(symbol);
   if(timeseries==NULL)
      return false;
   if(!timeseries.SyncData(timeframe,rates_total,required))
      return false;
   return timeseries.CreateSeries(timeframe,required);
  }
//+------------------------------------------------------------------+

此处,所有内容完全相同,仅有的一点区别 — 时间序列已创建了,因此跳过了向所有品种时间序列对象里添加新时间序列的步骤。

从时间序列对象接收事件,并将其添加到时间序列收集事件列表中的方法实现:

//+------------------------------------------------------------------+
//| Get events from the timeseries object and add them to the list   |
//+------------------------------------------------------------------+
bool CTimeSeriesCollection::SetEvents(CTimeSeries *timeseries)
  {
//--- Set the flag of successfully adding an event to the list and
//--- get the list of symbol timeseries object events
   bool res=true;
   CArrayObj *list=timeseries.GetListEvents();
   if(list==NULL)
      return false;
//--- In the loop by the obtained list of events,
   int total=list.Total();
   for(int i=0;i<total;i++)
     {
      //--- get the next event by the loop index and
      CEventBaseObj *event=timeseries.GetEvent(i);
      if(event==NULL)
         continue;
      //--- add the result of adding the obtained event to the flag value
      //--- from the symbol timeseries list to the timeseries collection list
      res &=this.EventAdd(event.ID(),event.LParam(),event.DParam(),event.SParam());
     }
//--- Return the result of adding events to the list
   return res;
  }
//+------------------------------------------------------------------+

该方法接收品种时间序列对象的指针。 循环遍历对象事件列表,并把所有事件添加到时间序列集合事件列表之中。

更新指定品种的特定时间序列,并将其事件添加到时间序列集合事件列表的方法实现:

//+------------------------------------------------------------------+
//| Update the specified timeseries of the specified symbol          |
//+------------------------------------------------------------------+
void CTimeSeriesCollection::Refresh(const string symbol,const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate)
  {
//--- Reset the flag of an event in the timeseries collection and clear the event list
   this.m_is_event=false;
   this.m_list_events.Clear();
//--- Get the object of all symbol timeseries by a symbol name
   CTimeSeries *timeseries=this.GetTimeseries(symbol);
   if(timeseries==NULL)
      return;
//--- If there is no new tick on the timeseries object symbol, exit
   if(!timeseries.IsNewTick())
      return;
//--- Update the required object timeseries of all symbol timeseries
   timeseries.Refresh(timeframe,data_calculate);
//--- If the timeseries has the enabled event flag,
//--- get events from symbol timeseries, write them to the collection event list
//--- and set the event flag in the collection
   if(timeseries.IsEvent())
      this.m_is_event=this.SetEvents(timeseries);
  }
//+------------------------------------------------------------------+

更新所有品种的所有时间序列,并将其事件添加到时间序列集合事件列表的方法实现:

//+------------------------------------------------------------------+
//| Update all timeseries of all symbols                             |
//+------------------------------------------------------------------+
void CTimeSeriesCollection::Refresh(SDataCalculate &data_calculate)
  {
//--- Reset the flag of an event in the timeseries collection and clear the event list
   this.m_is_event=false;
   this.m_list_events.Clear();
//--- In the loop by all symbol timeseries objects in the collection,
   int total=this.m_list.Total();
   for(int i=0;i<total;i++)
     {
      //--- get the next symbol timeseries object
      CTimeSeries *timeseries=this.m_list.At(i);
      if(timeseries==NULL)
         continue;
      //--- if there is no new tick on a timeseries symbol, move to the next object in the list
      if(!timeseries.IsNewTick())
         continue;
      //--- Update all symbol timeseries
      timeseries.RefreshAll(data_calculate);
      //--- If the event flag enabled for the symbol timeseries object,
      //--- get events from symbol timeseries, write them to the collection event list
      //--- and set the event flag in the collection
      if(timeseries.IsEvent())
         this.m_is_event=this.SetEvents(timeseries);
     }
  }
//+------------------------------------------------------------------+

所有这些方法都加了详细注释,其逻辑很易于理解。

当前阶段所有时间序列类的改进至此完毕。

现在,我们来改进 CEngine 函数库主对象(\MQL5\Include\DoEasy\Engine.mqh),从而可以操控程序中的时间序列集合。

在类的私密部分,声明暂停对象

class CEngine
  {
private:
   CHistoryCollection   m_history;                       // Collection of historical orders and deals
   CMarketCollection    m_market;                        // Collection of market orders and deals
   CEventsCollection    m_events;                        // Event collection
   CAccountsCollection  m_accounts;                      // Account collection
   CSymbolsCollection   m_symbols;                       // Symbol collection
   CTimeSeriesCollection m_time_series;                  // Timeseries collection
   CResourceCollection  m_resource;                      // Resource list
   CTradingControl      m_trading;                       // Trading management object
   CPause               m_pause;                         // Pause object

在类的公开部分,添加返回时间序列集合中事件存在标志的方法

//--- Return the (1) hedge account, (2) working in the tester, (3) account event, (4) symbol event and (5) trading event flag
   bool                 IsHedge(void)                             const { return this.m_is_hedge;                             }
   bool                 IsTester(void)                            const { return this.m_is_tester;                            }
   bool                 IsAccountsEvent(void)                     const { return this.m_accounts.IsEvent();                   }
   bool                 IsSymbolsEvent(void)                      const { return this.m_symbols.IsEvent();                    }
   bool                 IsTradeEvent(void)                        const { return this.m_events.IsEvent();                     }
   bool                 IsSeriesEvent(void)                       const { return this.m_time_series.IsEvent();                }

该方法返回时间序列集合对象的 IsEvent() 方法的操作结果。

来自指标 OnCalculate() 应答程序的数组数据,现在应发送到处理当前时间序列数据的刷新方法,在 Timer 和 Tick 事件处理方法里添加传递 OnCalculate() 数组数据结构 ,以及声明 Calculate 事件应答的方法

//--- (1) Timer, (2) NewTick event handler and (3) Calculate event handler
   void                 OnTimer(SDataCalculate &data_calculate);
   void                 OnTick(SDataCalculate &data_calculate,const uint required=0);
   int                  OnCalculate(SDataCalculate &data_calculate,const uint required=0);

在该类的同一公开部分,添加返回时间序列事件列表的方法

//--- Return (1) the timeseries collection and (2) the list of timeseries from the timeseries collection and (3) the list of timeseries events
   CTimeSeriesCollection *GetTimeSeriesCollection(void)                       { return &this.m_time_series;                                     }
   CArrayObj           *GetListTimeSeries(void)                               { return this.m_time_series.GetList();                            }
   CArrayObj           *GetListSeriesEvents(void)                             { return this.m_time_series.GetListEvents();                      }

该方法调用 GetListEvents() 时间序列集合方法,返回时间序列集合事件列表的指针。

该类的公开部分提供了四个创建各种时间序列的方法。 我们暂时将不需要的三个方法删除

//--- Create (1) the specified timeseries of the specified symbol, (2) the specified timeseries of all symbols,
//--- (3) all timeseries of the specified symbol and (4) all timeseries of all symbols
   bool                 SeriesCreate(const string symbol,const ENUM_TIMEFRAMES timeframe,const uint required=0)
                          { return this.m_series.CreateSeries(symbol,timeframe,required);          }
   bool                 SeriesCreate(const ENUM_TIMEFRAMES timeframe,const uint required=0)
                          { return this.m_series.CreateSeries(timeframe,required);                 }
   bool                 SeriesCreate(const string symbol,const uint required=0)
                          { return this.m_series.CreateSeries(symbol,required);                    }
   bool                 SeriesCreate(const uint required=0)
                          { return this.m_series.CreateSeries(required);                           }

声明依据所有用到的集合品种创建所有时间序列的方法来替换它们。 另外,编写用于重新创建指定时间序列的方法,并声明时间序列请求与服务器同步的方法

//--- Create (1) the specified timeseries of the specified symbol and (2) all used timeseries of all used symbols
   bool                 SeriesCreate(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0)
                          { return this.m_time_series.CreateSeries(symbol,timeframe,rates_total,required);        }
   bool                 SeriesCreateAll(const string &array_periods[],const int rates_total=0,const uint required=0);
//--- Re-create a specified timeseries of a specified symbol
   bool                 SeriesReCreate(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0)
                          { return this.m_time_series.ReCreateSeries(symbol,timeframe,rates_total,required);      }
//--- Synchronize timeseries data with the server
   void                 SeriesSync(SDataCalculate &data_calculate,const uint required=0);

我们还有四个刷新时间序列集合的方法。
仅保留两个方法 — 第一个方法更新指定时间序列第二个方法更新所有集合时间序列

//--- Update (1) the specified timeseries of the specified symbol, (2) all timeseries of all symbols
   void                 SeriesRefresh(const string symbol,const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate)
                          { this.m_time_series.Refresh(symbol,timeframe,data_calculate);                          }
   void                 SeriesRefresh(SDataCalculate &data_calculate)
                          { this.m_time_series.Refresh(data_calculate);                                           }

向方法传递含有变量的数据结构和 OnCalculate() 数组,替代仅有的 OnCalculate() 数组值。

我们来添加四个新方法 — 返回指定品种的时间序列对象指针指定时间序列对象的方法,以及返回指向部分填充的时间序列指针的方法:

//--- Return (1) the timeseries object of the specified symbol and (2) the timeseries object of the specified symbol/period
   CTimeSeries         *SeriesGetTimeseries(const string symbol)
                          { return this.m_time_series.GetTimeseries(symbol);                                      }
   CSeries             *SeriesGetSeries(const string symbol,const ENUM_TIMEFRAMES timeframe)
                          { return this.m_time_series.GetSeries(symbol,timeframe);                                }
//--- Return (1) an empty, (2) partially filled timeseries
   CSeries             *SeriesGetSeriesEmpty(void)       { return this.m_time_series.GetSeriesEmpty();            }
   CSeries             *SeriesGetSeriesIncompleted(void) { return this.m_time_series.GetSeriesIncompleted();      }

这些方法返回我们上面研究的时间序列集合的同名方法的结果。

将所有必须集合的指针传递到交易类的 TradingOnInit() 方法已重命名为 CollectionOnInit(),因为所有集合类必须执行初始化,这名称更适合它。

在类实体代码的末尾,添加操控暂停对象的方法模块

//--- Set the new (1) pause countdown start time and (2) pause in milliseconds
   void                 PauseSetTimeBegin(const ulong time)             { this.m_pause.SetTimeBegin(time);                    }
   void                 PauseSetWaitingMSC(const ulong pause)           { this.m_pause.SetWaitingMSC(pause);                  }
//--- Return (1) the time passed from the pause countdown start in milliseconds, (2) waiting completion flag
//--- (3) pause countdown start time, (4) pause in milliseconds
   ulong                PausePassed(void)                         const { return this.m_pause.Passed();                       }
   bool                 PauseIsCompleted(void)                    const { return this.m_pause.IsCompleted();                  }
   ulong                PauseTimeBegin(void)                      const { return this.m_pause.TimeBegin();                    }
   ulong                PauseTimeWait(void)                       const { return this.m_pause.TimeWait();                     }
//--- Return the description (1) of the time passed till the countdown starts in milliseconds,
//--- (2) pause countdown start time, (3) pause in milliseconds
   string               PausePassedDescription(void)              const { return this.m_pause.PassedDescription();            }
   string               PauseTimeBeginDescription(void)           const { return this.m_pause.TimeBeginDescription();         }
   string               PauseWaitingMSCDescription(void)          const { return this.m_pause.WaitingMSCDescription();        }
   string               PauseWaitingSECDescription(void)          const { return this.m_pause.WaitingSECDescription();        }
//--- Launch the new pause countdown
   void                 Pause(const ulong pause_msc,const datetime time_start=0)
                          {
                           this.PauseSetWaitingMSC(pause_msc);
                           this.PauseSetTimeBegin(time_start*1000);
                           while(!this.PauseIsCompleted() && !IsStopped()){}
                          }

//--- Constructor/destructor
                        CEngine();
                       ~CEngine();

在第三十篇文章中曾讲述过 Pause 类。 该类插入暂停,替代在指标中不起作用的 Sleep() 函数。

除了已讲述过的 CPause 类方法之外,我们还添加了另一个 Pause() 方法,这可令我们能够启动新的等待暂停,而无需对其参数进行初始化 — 所有参数都传递给该方法,而该方法执行暂停的毫秒值可作为输入传递。 这些方法在程序中很有用,可在指标里管控暂停。

请记住,这个暂停对象会令启动指标所在的主线程延迟,就像 Sleep() 函数一样。
此暂停只有必要时才需在指标中应用。

CEngine 类的计时器已重新编排 — 之前我们要检查每个应答程序操作于何种环境 — 是否在测试器中。 这种不合理的检查在所有集合的每个应答程序里都必须执行。
现在,我们首先检查操作在何处完成 — 不在测试器中,或在测试器中。 然后,在模块(非测试器测试器)内部执行所有集合的处理:

//+------------------------------------------------------------------+
//| CEngine timer                                                    |
//+------------------------------------------------------------------+
void CEngine::OnTimer(SDataCalculate &data_calculate)
  {
//--- If this is not a tester, work with collection events by timer
   if(!this.IsTester())
     {
   //--- Timer of the collections of historical orders and deals, as well as of market orders and positions
      int index=this.CounterIndex(COLLECTION_ORD_COUNTER_ID);
      CTimerCounter* cnt1=this.m_list_counters.At(index);
      if(cnt1!=NULL)
        {
         //--- If unpaused, work with the order, deal and position collections events
         if(cnt1.IsTimeDone())
            this.TradeEventsControl();
        }
   //--- Account collection timer
      index=this.CounterIndex(COLLECTION_ACC_COUNTER_ID);
      CTimerCounter* cnt2=this.m_list_counters.At(index);
      if(cnt2!=NULL)
        {
         //--- If unpaused, work with the account collection events
         if(cnt2.IsTimeDone())
            this.AccountEventsControl();
        }
   //--- Timer 1 of the symbol collection (updating symbol quote data in the collection)
      index=this.CounterIndex(COLLECTION_SYM_COUNTER_ID1);
      CTimerCounter* cnt3=this.m_list_counters.At(index);
      if(cnt3!=NULL)
        {
         //--- If the pause is over, update quote data of all symbols in the collection
         if(cnt3.IsTimeDone())
            this.m_symbols.RefreshRates();
        }
   //--- Timer 2 of the symbol collection (updating all data of all symbols in the collection and tracking symbl and symbol search events in the market watch window)
      index=this.CounterIndex(COLLECTION_SYM_COUNTER_ID2);
      CTimerCounter* cnt4=this.m_list_counters.At(index);
      if(cnt4!=NULL)
        {
         //--- If the pause is over
         if(cnt4.IsTimeDone())
           {
            //--- update data and work with events of all symbols in the collection
            this.SymbolEventsControl();
            //--- When working with the market watch list, check the market watch window events
            if(this.m_symbols.ModeSymbolsList()==SYMBOLS_MODE_MARKET_WATCH)
               this.MarketWatchEventsControl();
           }
        }
   //--- Trading class timer
      index=this.CounterIndex(COLLECTION_REQ_COUNTER_ID);
      CTimerCounter* cnt5=this.m_list_counters.At(index);
      if(cnt5!=NULL)
        {
         //--- If unpaused, work with the list of pending requests
         if(cnt5.IsTimeDone())
            this.m_trading.OnTimer();
        }
   //--- Timeseries collection timer
      index=this.CounterIndex(COLLECTION_TS_COUNTER_ID);
      CTimerCounter* cnt6=this.m_list_counters.At(index);
      if(cnt6!=NULL)
        {
         //--- If unpaused, work with the timeseries list
         if(cnt6.IsTimeDone())
            this.SeriesRefresh(data_calculate);
        }
     }
//--- If this is a tester, work with collection events by tick
   else
     {
      //--- work with events of collections of orders, deals and positions by tick
      this.TradeEventsControl();
      //--- work with events of collections of accounts by tick
      this.AccountEventsControl();
      //--- update quote data of all collection symbols by tick
      this.m_symbols.RefreshRates();
      //--- work with events of all symbols in the collection by tick
      this.SymbolEventsControl();
      //--- work with the list of pending orders by tick
      this.m_trading.OnTimer();
      //--- work with the timeseries list by tick
      this.SeriesRefresh(data_calculate);
     }
  }
//+------------------------------------------------------------------+

应答程序变得更加紧凑,且逻辑更易于理解。 此外,现在可以避免不必要的重复检查。

该方法拿空时间序列数据与服务器同步,并重新创建空时间序列:

//+------------------------------------------------------------------+
//| Synchronize timeseries data with the server                      |
//+------------------------------------------------------------------+
void CEngine::SeriesSync(SDataCalculate &data_calculate,const uint required=0)
  {
//--- If the timeseries data is not calculated, try re-creating the timeseries
//--- Get the pointer to the empty timeseries
   CSeries *series=this.SeriesGetSeriesEmpty();
   if(series!=NULL)
     {
      //--- Display the empty timeseries data as a chart comment and try synchronizing the timeseries with the server data
      ::Comment(series.Header(),": ",CMessage::Text(MSG_LIB_TEXT_TS_TEXT_WAIT_FOR_SYNC));
      ::ChartRedraw(::ChartID());
      //--- if the data has been synchronized
      if(series.SyncData(0,data_calculate.rates_total))
        {
         //--- if managed to re-create the timeseries
         if(this.m_time_series.ReCreateSeries(series.Symbol(),series.Timeframe(),data_calculate.rates_total))
           {
            //--- display the chart comment and the journal entry with the re-created timeseries data
            ::Comment(series.Header(),": OK");
            ::ChartRedraw(::ChartID());
            Print(series.Header()," ",CMessage::Text(MSG_LIB_TEXT_TS_TEXT_CREATED_OK),":");
            series.PrintShort();
           }
        }
     }
//--- Delete all comments
   else
     {
      ::Comment("");
      ::ChartRedraw(::ChartID());
     }
  }
//+------------------------------------------------------------------+

该方法是为所有用到的时间序列正确加载历史数据的基石 —任何品种和图表周期。

该方法从时间序列集合中接收第一个未填充的时间序列,这意味着在一次即时报价之前它尚无任何数据。 立即尝试执行时间序列数据与服务器数据同步。 如果失败,则退出该方法,直到下一次即时报价。 如果数据已同步,则将重新创建时间序列 — 用历史记录中所有可用的(但不超过所请求数量)柱线填充。

该过程在每次即时报价上执行 — 我们得到下一个空时间序列,同步并重建,直到没有空时间序列为止。

NewTick 和 Calculate 事件应答程序的实现:

//+------------------------------------------------------------------+
//| NewTick event handler                                            |
//+------------------------------------------------------------------+
void CEngine::OnTick(SDataCalculate &data_calculate,const uint required=0)
  {
//--- If this is not a EA, exit
   if(this.m_program!=PROGRAM_EXPERT)
      return;
//--- Re-create empty timeseries
   this.SeriesSync(data_calculate,required);
//--- end
  }
//+------------------------------------------------------------------+
//| Calculate event handler                                          |
//+------------------------------------------------------------------+
int CEngine::OnCalculate(SDataCalculate &data_calculate,const uint required=0)
  {
//--- If this is not an indicator, exit
   if(this.m_program!=PROGRAM_INDICATOR)
      return data_calculate.rates_total;
//--- Re-create empty timeseries
   this.SeriesSync(data_calculate,required);
//--- return rates_total
   return data_calculate.rates_total;
  }
//+------------------------------------------------------------------+

在两个方法中都会调用重新创建空时间序列的方法
这些方法本身应从基于函数库的同名程序应答程序里调用。

为所有已用品种创建所有时间序列的方法实现:

//+------------------------------------------------------------------+
//| Create all applied timeseries of all used symbols                |
//+------------------------------------------------------------------+
bool CEngine::SeriesCreateAll(const string &array_periods[],const int rates_total=0,const uint required=0)
  {
//--- Set the flag of successful creation of all timeseries of all symbols
   bool res=true;
//--- Get the list of all used symbols
   CArrayObj* list_symbols=this.GetListAllUsedSymbols();
   if(list_symbols==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_GET_SYMBOLS_ARRAY));
      return false;
     }
   //--- In the loop by the total number of symbols
   for(int i=0;i<list_symbols.Total();i++)
     {
      //--- get the next symbol object
      CSymbol *symbol=list_symbols.At(i);
      if(symbol==NULL)
        {
         ::Print(DFUN,"index ",i,": ",CMessage::Text(MSG_LIB_SYS_ERROR_FAILED_GET_SYM_OBJ));
         continue;
        }
      //--- In the loop by the total number of used timeframes,
      int total_periods=::ArraySize(array_periods);
      for(int j=0;j<total_periods;j++)
        {
         //--- create the timeseries object of the next symbol.
         //--- Add the timeseries creation result to the res variable
         ENUM_TIMEFRAMES timeframe=TimeframeByDescription(array_periods[j]);
         res &=this.SeriesCreate(symbol.Name(),timeframe,rates_total,required);
        }
     }
//--- Return the result of creating all timeseries for all symbols
   return res;
  }
//+------------------------------------------------------------------+

为所有已用品种创建列表之后,将在程序初始化期间调用该方法。
方法接收初始化期间创建的数组。 该数组包含所用图表周期的名称,和创建时间序列的参数 — 当前时间序列柱线的数量(仅用于指标 — rate_total)和创建的时间序列所需的历史深度(默认值是 1000,但不大于该品种的 Bars() 值,且不大于指标的 rates_total)。

当前,这些都是操控时间序列的所有必要改进。


在指标中测试时间序列及其事件

若要测试指标中时间序列集合类的操作,请在终端指标目录 \MQL5\Indicators\Test Easy\ 里创建一个新文件夹。 我们在其中创建一个新的子文件夹 Part39\,并在其中增加一个新的指标 TestDoEasyPart39.mq5

到目前为止,我们并不在意指标绘制缓冲区的数量和类型,因为我们不会在其中绘制任何内容。 然而,我设置了两个 DRAW_LINE 绘图类型的绘制缓冲区,为将来备用。

从上一篇文章里讲述的测试 EA 中获取了为所需品种和时间帧设置的必要指标输入,以及其他一些输入。 此为现在的模样:

//+------------------------------------------------------------------+
//|                                             TestDoEasyPart39.mq5 |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
//--- includes
#include <DoEasy\Engine.mqh>
//--- enums
//--- defines
//--- structures
//--- properties
#property indicator_chart_window
#property indicator_buffers 2
#property indicator_plots   2
//--- plot Label1
#property indicator_label1  "Label1"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrRed
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- plot Label2
#property indicator_label2  "Label2"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrGreen
#property indicator_style2  STYLE_SOLID
#property indicator_width2  1
//--- indicator buffers
double         Buffer1[];
double         Buffer2[];
//--- input variables
sinput   ENUM_SYMBOLS_MODE InpModeUsedSymbols   =  SYMBOLS_MODE_CURRENT;            // Mode of used symbols list
sinput   string            InpUsedSymbols       =  "EURUSD,AUDUSD,EURAUD,EURCAD,EURGBP,EURJPY,EURUSD,GBPUSD,NZDUSD,USDCAD,USDJPY";  // List of used symbols (comma - separator)
sinput   ENUM_TIMEFRAMES_MODE InpModeUsedTFs    =  TIMEFRAMES_MODE_LIST;            // Mode of used timeframes list
sinput   string            InpUsedTFs           =  "M1,M5,M15,M30,H1,H4,D1,W1,MN1"; // List of used timeframes (comma - separator)
sinput   bool              InpUseSounds         =  true; // Use sounds
//--- global variables
CEngine        engine;                          // CEngine library main object
string         prefix;                          // Prefix of graphical object names
bool           testing;                         // Flag of working in the tester
int            used_symbols_mode;               // Mode of working with symbols
string         array_used_symbols[];            // Array of used symbols
string         array_used_periods[];            // Array of used timeframes
//+------------------------------------------------------------------+

在指标的 OnInit() 应答程序中,实现设置指标全局变量,并调用函数库初始化函数:

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,Buffer1,INDICATOR_DATA);
   SetIndexBuffer(1,Buffer2,INDICATOR_DATA);

//--- Set indicator global variables
   prefix=MQLInfoString(MQL_PROGRAM_NAME)+"_";
   testing=engine.IsTester();
   ZeroMemory(rates_data);
   
//--- Initialize DoEasy library
   OnInitDoEasy();

//--- Check and remove remaining indicator graphical objects
   if(IsPresentObectByPrefix(prefix))
      ObjectsDeleteAll(0,prefix);

//--- Check playing a standard sound using macro substitutions
   engine.PlaySoundByDescription(SND_OK);
//--- Wait for 600 milliseconds
   engine.Pause(600);
   engine.PlaySoundByDescription(SND_NEWS);

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

指标的 OnDeinit() 应答函数取自上一篇文章中讲述的测试 EA:

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Remove indicator graphical objects by an object name prefix
   ObjectsDeleteAll(0,prefix);
   Comment("");
  }
//+------------------------------------------------------------------+

我们同时从 EA 中提取 OnTimer() 和 OnChartEvent() 应答程序:

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
//--- Launch the library timer (only not in the tester)
   if(!MQLInfoInteger(MQL_TESTER))
      engine.OnTimer(rates_data);
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- If working in the tester, exit
   if(MQLInfoInteger(MQL_TESTER))
      return;
//--- Handling mouse events
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      //--- Handling pressing the buttons in the panel
      if(StringFind(sparam,"BUTT_")>0)
         PressButtonEvents(sparam);
     }
//--- Handling DoEasy library events
   if(id>CHARTEVENT_CUSTOM-1)
     {
      OnDoEasyEvent(id,lparam,dparam,sparam);
     } 
  }
//+------------------------------------------------------------------+

创建两个函数,以便按指标的 OnCalculate() 第一种第二种 形式填充数组结构和变量数据

//+------------------------------------------------------------------+
//| Copy data from the first OnCalculate() form to the structure     |
//+------------------------------------------------------------------+
void CopyData(SDataCalculate &data_calculate,
              const int rates_total,
              const int prev_calculated,
              const int begin,
              const double &price[])
  {
//--- Get the array indexing flag as in the timeseries. If failed,
//--- set the indexing direction for the array as in the timeseries
   bool as_series_price=ArrayGetAsSeries(price);
   if(!as_series_price)
      ArraySetAsSeries(price,true);
//--- Copy the array zero bar to the OnCalculate() SDataCalculate data structure
   data_calculate.rates_total=rates_total;
   data_calculate.prev_calculated=prev_calculated;
   data_calculate.begin=begin;
   data_calculate.price=price[0];
//--- Return the array's initial indexing direction
   if(!as_series_price)
      ArraySetAsSeries(price,false);
  }
//+------------------------------------------------------------------+
//| Copy data from the second OnCalculate() form to the structure    |
//+------------------------------------------------------------------+
void CopyData(SDataCalculate &data_calculate,
              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[])
  {
//--- Get the array indexing flags as in the timeseries. If failed,
//--- set the indexing direction or the arrays as in the timeseries
   bool as_series_time=ArrayGetAsSeries(time);
   if(!as_series_time)
      ArraySetAsSeries(time,true);
   bool as_series_open=ArrayGetAsSeries(open);
   if(!as_series_open)
      ArraySetAsSeries(open,true);
   bool as_series_high=ArrayGetAsSeries(high);
   if(!as_series_high)
      ArraySetAsSeries(high,true);
   bool as_series_low=ArrayGetAsSeries(low);
   if(!as_series_low)
      ArraySetAsSeries(low,true);
   bool as_series_close=ArrayGetAsSeries(close);
   if(!as_series_close)
      ArraySetAsSeries(close,true);
   bool as_series_tick_volume=ArrayGetAsSeries(tick_volume);
   if(!as_series_tick_volume)
      ArraySetAsSeries(tick_volume,true);
   bool as_series_volume=ArrayGetAsSeries(volume);
   if(!as_series_volume)
      ArraySetAsSeries(volume,true);
   bool as_series_spread=ArrayGetAsSeries(spread);
   if(!as_series_spread)
      ArraySetAsSeries(spread,true);
//--- Copy the arrays' zero bar to the OnCalculate() SDataCalculate data structure
   data_calculate.rates_total=rates_total;
   data_calculate.prev_calculated=prev_calculated;
   data_calculate.rates.time=time[0];
   data_calculate.rates.open=open[0];
   data_calculate.rates.high=high[0];
   data_calculate.rates.low=low[0];
   data_calculate.rates.close=close[0];
   data_calculate.rates.tick_volume=tick_volume[0];
   data_calculate.rates.real_volume=(#ifdef __MQL5__ volume[0] #else 0 #endif);
   data_calculate.rates.spread=(#ifdef __MQL5__ spread[0] #else 0 #endif);
//--- Return the arrays' initial indexing direction
   if(!as_series_time)
      ArraySetAsSeries(time,false);
   if(!as_series_open)
      ArraySetAsSeries(open,false);
   if(!as_series_high)
      ArraySetAsSeries(high,false);
   if(!as_series_low)
      ArraySetAsSeries(low,false);
   if(!as_series_close)
      ArraySetAsSeries(close,false);
   if(!as_series_tick_volume)
      ArraySetAsSeries(tick_volume,false);
   if(!as_series_volume)
      ArraySetAsSeries(volume,false);
   if(!as_series_spread)
      ArraySetAsSeries(spread,false);
  }
//+------------------------------------------------------------------+

从测试 EA 移出处理 DoEasy 函数库事件的函数:

//+------------------------------------------------------------------+
//| Handling DoEasy library events                                   |
//+------------------------------------------------------------------+
void OnDoEasyEvent(const int id,
                   const long &lparam,
                   const double &dparam,
                   const string &sparam)
  {
   int idx=id-CHARTEVENT_CUSTOM;
//--- Retrieve (1) event time milliseconds, (2) reason and (3) source from lparam, as well as (4) set the exact event time
   ushort msc=engine.EventMSC(lparam);
   ushort reason=engine.EventReason(lparam);
   ushort source=engine.EventSource(lparam);
   long time=TimeCurrent()*1000+msc;
   
//--- Handling symbol events
   if(source==COLLECTION_SYMBOLS_ID)
     {
      CSymbol *symbol=engine.GetSymbolObjByName(sparam);
      if(symbol==NULL)
         return;
      //--- Number of decimal places in the event value - in case of a 'long' event, it is 0, otherwise - Digits() of a symbol
      int digits=(idx<SYMBOL_PROP_INTEGER_TOTAL ? 0 : symbol.Digits());
      //--- Event text description
      string id_descr=(idx<SYMBOL_PROP_INTEGER_TOTAL ? symbol.GetPropertyDescription((ENUM_SYMBOL_PROP_INTEGER)idx) : symbol.GetPropertyDescription((ENUM_SYMBOL_PROP_DOUBLE)idx));
      //--- Property change text value
      string value=DoubleToString(dparam,digits);
      
      //--- Check event reasons and display its description in the journal
      if(reason==BASE_EVENT_REASON_INC)
        {
         Print(symbol.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
      if(reason==BASE_EVENT_REASON_DEC)
        {
         Print(symbol.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
      if(reason==BASE_EVENT_REASON_MORE_THEN)
        {
         Print(symbol.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
      if(reason==BASE_EVENT_REASON_LESS_THEN)
        {
         Print(symbol.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
      if(reason==BASE_EVENT_REASON_EQUALS)
        {
         Print(symbol.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
     }   
     
//--- Handling account events
   else if(source==COLLECTION_ACCOUNT_ID)
     {
      CAccount *account=engine.GetAccountCurrent();
      if(account==NULL)
         return;
      //--- Number of decimal places in the event value - in case of a 'long' event, it is 0, otherwise - Digits() of a symbol
      int digits=int(idx<ACCOUNT_PROP_INTEGER_TOTAL ? 0 : account.CurrencyDigits());
      //--- Event text description
      string id_descr=(idx<ACCOUNT_PROP_INTEGER_TOTAL ? account.GetPropertyDescription((ENUM_ACCOUNT_PROP_INTEGER)idx) : account.GetPropertyDescription((ENUM_ACCOUNT_PROP_DOUBLE)idx));
      //--- Property change text value
      string value=DoubleToString(dparam,digits);
      
      //--- Checking event reasons and handling the increase of funds by a specified value,
      
      //--- In case of a property value increase
      if(reason==BASE_EVENT_REASON_INC)
        {
         //--- Display an event in the journal
         Print(account.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
         //--- if this is an equity increase
         if(idx==ACCOUNT_PROP_EQUITY)
           {
            //--- Get the list of all open positions for the current symbol
            CArrayObj* list_positions=engine.GetListMarketPosition();
            list_positions=CSelect::ByOrderProperty(list_positions,ORDER_PROP_SYMBOL,Symbol(),EQUAL);
            //--- Select positions with the profit exceeding zero
            list_positions=CSelect::ByOrderProperty(list_positions,ORDER_PROP_PROFIT_FULL,0,MORE);
            if(list_positions!=NULL)
              {
               //--- Sort the list by profit considering commission and swap
               list_positions.Sort(SORT_BY_ORDER_PROFIT_FULL);
               //--- Get the position index with the highest profit
               int index=CSelect::FindOrderMax(list_positions,ORDER_PROP_PROFIT_FULL);
               if(index>WRONG_VALUE)
                 {
                  COrder* position=list_positions.At(index);
                  if(position!=NULL)
                    {
                     //--- Get a ticket of a position with the highest profit and close the position by a ticket
                     engine.ClosePosition(position.Ticket());
                    }
                 }
              }
           }
        }
      //--- Other events are simply displayed in the journal
      if(reason==BASE_EVENT_REASON_DEC)
        {
         Print(account.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
      if(reason==BASE_EVENT_REASON_MORE_THEN)
        {
         Print(account.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
      if(reason==BASE_EVENT_REASON_LESS_THEN)
        {
         Print(account.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
      if(reason==BASE_EVENT_REASON_EQUALS)
        {
         Print(account.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
     } 
     
//--- Handling market watch window events
   else if(idx>MARKET_WATCH_EVENT_NO_EVENT && idx<SYMBOL_EVENTS_NEXT_CODE)
     {
      //--- Market Watch window event
      string descr=engine.GetMWEventDescription((ENUM_MW_EVENT)idx);
      string name=(idx==MARKET_WATCH_EVENT_SYMBOL_SORT ? "" : ": "+sparam);
      Print(TimeMSCtoString(lparam)," ",descr,name);
     }
     
//--- Handling timeseries events
   else if(idx>SERIES_EVENTS_NO_EVENT && idx<SERIES_EVENTS_NEXT_CODE)
     {
      //--- "New bar" event
      if(idx==SERIES_EVENTS_NEW_BAR)
        {
         Print(TextByLanguage("Новый бар на ","New Bar on "),sparam," ",TimeframeDescription((ENUM_TIMEFRAMES)dparam),": ",TimeToString(lparam));
        }
     }
     
//--- Handling trading events
   else if(idx>TRADE_EVENT_NO_EVENT && idx<TRADE_EVENTS_NEXT_CODE)
     {
      //--- Get the list of trading events
      CArrayObj *list=engine.GetListAllOrdersEvents();
      if(list==NULL)
         return;
      //--- get the event index shift relative to the end of the list
      //--- in the tester, the shift is passed by the lparam parameter to the event handler
      //--- outside the tester, events are sent one by one and handled in OnChartEvent()
      int shift=(testing ? (int)lparam : 0);
      CEvent *event=list.At(list.Total()-1-shift);
      if(event==NULL)
      return;
      //--- Accrue the credit
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_CREDIT)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Additional charges
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_CHARGE)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Correction
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_CORRECTION)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Enumerate bonuses
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_BONUS)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Additional commissions
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_COMISSION)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Daily commission
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_COMISSION_DAILY)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Monthly commission
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_COMISSION_MONTHLY)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Daily agent commission
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_COMISSION_AGENT_DAILY)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Monthly agent commission
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_COMISSION_AGENT_MONTHLY)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Interest rate
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_INTEREST)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Canceled buy deal
      if(event.TypeEvent()==TRADE_EVENT_BUY_CANCELLED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Canceled sell deal
      if(event.TypeEvent()==TRADE_EVENT_SELL_CANCELLED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Dividend operations
      if(event.TypeEvent()==TRADE_EVENT_DIVIDENT)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Accrual of franked dividend
      if(event.TypeEvent()==TRADE_EVENT_DIVIDENT_FRANKED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Tax charges
      if(event.TypeEvent()==TRADE_EVENT_TAX)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Replenishing account balance
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_BALANCE_REFILL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Withdrawing funds from balance
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_BALANCE_WITHDRAWAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      
      //--- Pending order placed
      if(event.TypeEvent()==TRADE_EVENT_PENDING_ORDER_PLASED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Pending order removed
      if(event.TypeEvent()==TRADE_EVENT_PENDING_ORDER_REMOVED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Pending order activated by price
      if(event.TypeEvent()==TRADE_EVENT_PENDING_ORDER_ACTIVATED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Pending order partially activated by price
      if(event.TypeEvent()==TRADE_EVENT_PENDING_ORDER_ACTIVATED_PARTIAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position opened
      if(event.TypeEvent()==TRADE_EVENT_POSITION_OPENED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position opened partially
      if(event.TypeEvent()==TRADE_EVENT_POSITION_OPENED_PARTIAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position closed
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position closed by an opposite one
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_BY_POS)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position closed by StopLoss
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_BY_SL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position closed by TakeProfit
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_BY_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position reversal by a new deal (netting)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_REVERSED_BY_MARKET)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position reversal by activating a pending order (netting)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_REVERSED_BY_PENDING)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position reversal by partial market order execution (netting)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_REVERSED_BY_MARKET_PARTIAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position reversal by activating a pending order (netting)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_REVERSED_BY_PENDING_PARTIAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Added volume to a position by a new deal (netting)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_VOLUME_ADD_BY_MARKET)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Added volume to a position by partial execution of a market order (netting)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_VOLUME_ADD_BY_MARKET_PARTIAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Added volume to a position by activating a pending order (netting)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_VOLUME_ADD_BY_PENDING)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Added volume to a position by partial activation of a pending order (netting)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_VOLUME_ADD_BY_PENDING_PARTIAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position closed partially
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_PARTIAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position partially closed by an opposite one
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_POS)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position closed partially by StopLoss
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_SL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position closed partially by TakeProfit
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- StopLimit order activation
      if(event.TypeEvent()==TRADE_EVENT_TRIGGERED_STOP_LIMIT_ORDER)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Changing order price
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_PRICE)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Changing order and StopLoss price
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_PRICE_SL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Changing order and TakeProfit price
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_PRICE_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Changing order, StopLoss and TakeProfit price
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_PRICE_SL_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Changing order's StopLoss and TakeProfit price
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_SL_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Changing order's StopLoss
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_SL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Changing order's TakeProfit
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Changing position's StopLoss and TakeProfit
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_POSITION_SL_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Changing position StopLoss
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_POSITION_SL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Changing position TakeProfit
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_POSITION_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
     }
  }
//+------------------------------------------------------------------+

来自 EA 的,在测试器中处理函数库事件的函数:

//+------------------------------------------------------------------+
//| Working with events in the tester                                |
//+------------------------------------------------------------------+
void EventsHandling(void)
  {
//--- If a trading event is present
   if(engine.IsTradeEvent())
     {
      //--- Number of trading events occurred simultaneously
      int total=engine.GetTradeEventsTotal();
      for(int i=0;i<total;i++)
        {
         //--- Get the next event from the list of simultaneously occurred events by index
         CEventBaseObj *event=engine.GetTradeEventByIndex(i);
         if(event==NULL)
            continue;
         long   lparam=i;
         double dparam=event.DParam();
         string sparam=event.SParam();
         OnDoEasyEvent(CHARTEVENT_CUSTOM+event.ID(),lparam,dparam,sparam);
        }
     }
//--- If there is an account event
   if(engine.IsAccountsEvent())
     {
      //--- Get the list of all account events occurred simultaneously
      CArrayObj* list=engine.GetListAccountEvents();
      if(list!=NULL)
        {
         //--- Get the next event in a loop
         int total=list.Total();
         for(int i=0;i<total;i++)
           {
            //--- take an event from the list
            CEventBaseObj *event=list.At(i);
            if(event==NULL)
               continue;
            //--- Send an event to the event handler
            long lparam=event.LParam();
            double dparam=event.DParam();
            string sparam=event.SParam();
            OnDoEasyEvent(CHARTEVENT_CUSTOM+event.ID(),lparam,dparam,sparam);
           }
        }
     }
//--- If there is a symbol collection event
   if(engine.IsSymbolsEvent())
     {
      //--- Get the list of all symbol events occurred simultaneously
      CArrayObj* list=engine.GetListSymbolsEvents();
      if(list!=NULL)
        {
         //--- Get the next event in a loop
         int total=list.Total();
         for(int i=0;i<total;i++)
           {
            //--- take an event from the list
            CEventBaseObj *event=list.At(i);
            if(event==NULL)
               continue;
            //--- Send an event to the event handler
            long lparam=event.LParam();
            double dparam=event.DParam();
            string sparam=event.SParam();
            OnDoEasyEvent(CHARTEVENT_CUSTOM+event.ID(),lparam,dparam,sparam);
           }
        }
     }
//--- If there is a timeseries collection event
   if(engine.IsSeriesEvent())
     {
      //--- Get the list of all timeseries events occurred simultaneously
      CArrayObj* list=engine.GetListSeriesEvents();
      if(list!=NULL)
        {
         //--- Get the next event in a loop
         int total=list.Total();
         for(int i=0;i<total;i++)
           {
            //--- take an event from the list
            CEventBaseObj *event=list.At(i);
            if(event==NULL)
               continue;
            //--- Send an event to the event handler
            long lparam=event.LParam();
            double dparam=event.DParam();
            string sparam=event.SParam();
            OnDoEasyEvent(CHARTEVENT_CUSTOM+event.ID(),lparam,dparam,sparam);
           }
        }
     }
  }
//+------------------------------------------------------------------+

我们不需要为交易面板的按钮而重新编排 EA 函数。 不过,无论如何,我们还是要做一些微小的修改,从而够使用指标中的按钮(有两个按钮需要实现):

//+------------------------------------------------------------------+
//| Return the button status                                         |
//+------------------------------------------------------------------+
bool ButtonState(const string name)
  {
   return (bool)ObjectGetInteger(0,name,OBJPROP_STATE);
  }
//+------------------------------------------------------------------+
//| Set the button status                                            |
//+------------------------------------------------------------------+
void ButtonState(const string name,const bool state)
  {
   ObjectSetInteger(0,name,OBJPROP_STATE,state);
//--- Button 1
   if(name=="BUTT_1")
     {
      if(state)
         ObjectSetInteger(0,name,OBJPROP_BGCOLOR,C'220,255,240');
      else
         ObjectSetInteger(0,name,OBJPROP_BGCOLOR,C'240,240,240');
     }
//--- Button 2
   if(name=="BUTT_2")
     {
      if(state)
         ObjectSetInteger(0,name,OBJPROP_BGCOLOR,C'255,220,90');
      else
         ObjectSetInteger(0,name,OBJPROP_BGCOLOR,C'240,240,240');
     }
  }
//+------------------------------------------------------------------+
//| Track the buttons' status                                        |
//+------------------------------------------------------------------+
void PressButtonsControl(void)
  {
   int total=ObjectsTotal(0,0);
   for(int i=0;i<total;i++)
     {
      string obj_name=ObjectName(0,i);
      if(StringFind(obj_name,prefix+"BUTT_")<0)
         continue;
      PressButtonEvents(obj_name);
     }
  }
//+------------------------------------------------------------------+
//| Handle pressing the buttons                                      |
//+------------------------------------------------------------------+
void PressButtonEvents(const string button_name)
  {
   //--- Convert button name into its string ID
   string button=StringSubstr(button_name,StringLen(prefix));
   //--- If the button is pressed
   if(ButtonState(button_name))
     {
      //--- If button 1 is pressed
      if(button=="BUTT_1")
        {

        }
      //--- If button 2 is pressed
      else if(button=="BUTT_2")
        {

        }
      //--- Wait for 1/10 of a second
      engine.Pause(100);
      //--- "Unpress" the button (if this is neither a trailing button, nor the buttons enabling pending requests)
      ButtonState(button_name,false);
      //--- re-draw the chart
      ChartRedraw();
     }
   //--- Not pressed
   else 
     {
      //--- button 1
      if(button=="BUTT_1")
        {
         ButtonState(button_name,false);
        }
      //--- button 2
      if(button=="BUTT_2")
        {
         ButtonState(button_name,false);
        }
      //--- re-draw the chart
      ChartRedraw();
     }
  }
//+------------------------------------------------------------------+

正如我们所见,大多数 EA 函数无需调整即可在指标中使用。 建议把操控函数库的所有必要函数都从 EA 和指标里移至函数库的包含文件。 但这将在稍后完成。 当下,我们需要创建指标的 OnCalculate() 应答函数。

该应答程序由准备函数库数据的基础代码块,以及配合指标一起操作的可选(暂时)代码块组成:

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
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[])
  {
//+------------------------------------------------------------------+
//| OnCalculate code block for working with the library:             |
//+------------------------------------------------------------------+
//--- Pass the current symbol data from OnCalculate() to the price structure
   CopyData(rates_data,rates_total,prev_calculated,time,open,high,low,close,tick_volume,volume,spread);

//--- Handle the Calculate event in the library
   engine.OnCalculate(rates_data);

//--- If working in the tester
   if(MQLInfoInteger(MQL_TESTER)) 
     {
      engine.OnTimer(rates_data);   // Working in the timer
      PressButtonsControl();        // Button pressing control
      EventsHandling();             // Working with events
     }

//+------------------------------------------------------------------+
//| OnCalculate code block for working with the indicator:           |
//+------------------------------------------------------------------+
//--- Arrange resource-saving indicator calculations
//--- Set OnCalculate arrays as timeseries
   ArraySetAsSeries(open,true);
   ArraySetAsSeries(high,true);
   ArraySetAsSeries(low,true);
   ArraySetAsSeries(close,true);
   ArraySetAsSeries(tick_volume,true);
   ArraySetAsSeries(volume,true);
   ArraySetAsSeries(spread,true);

//--- Setting buffer arrays as timeseries
   ArraySetAsSeries(Buffer1,true);
   ArraySetAsSeries(Buffer2,true);

//--- Check for the minimum number of bars for calculation
   if(rates_total<2 || Point()==0) return 0;

//--- Check and calculate the number of calculated bars
   int limit=rates_total-prev_calculated;
   if(limit>1)
     {
      limit=rates_total-1;
      ArrayInitialize(Buffer1,EMPTY_VALUE);
      ArrayInitialize(Buffer2,EMPTY_VALUE);
     }
//--- Prepare data
   for(int i=limit; i>=0 && !IsStopped(); i--)
     {
      // the code for preparing indicator calculation buffers
     }

//--- Calculate the indicator
   for(int i=limit; i>=0 && !IsStopped(); i--)
     {
      Buffer1[i]=high[i];
      Buffer2[i]=low[i];
     }

//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

正如我们所见,与函数库操作相关的所有内容比较适合作为 OnCalculate() 应答程序中的一个小代码模块。 实际上,指标与 EA 之间的区别在于,EA 用 CopyData() 函数,指标用 OnCalculate() 函数来填充当前的价格结构数组,而其他所有内容都与在 EA 中工作完全相同 — 如果是在品种图表上启动指标,函数库在计时器中工作;若指标是在测试器里启动,则每次即时报价来临时于 OnCalculate() 里工作。
在 OnCalculate() 计算部分中,用 high[] 和 low[] 数组数据填充指标缓冲区。

完整的指标代码可在下面的文件中查看。

编译指标并启动,我们已经很长时间没有触及品种图表了(同时在预设置中操控当前品种设定),然后选择操控指定的时间帧列表。 若在长时间未使用的品种图表上启动指标,将会令指标下载缺失的数据,并在日志和图表中发出通知:


在此,我们可以看到在每次即时报价处,每个空时间序列都会被同步并创建。 日志中会显示以下条目:

Account 8550475: Artyom Trishkin (MetaQuotes Software Corp.) 10425.23 USD, 1:100, Hedge, MetaTrader 5 demo
--- Initializing "DoEasy" library ---
Working with the current symbol only: "USDCAD"
Working with the specified timeframe list:
"M1"  "M5"  "M15" "M30" "H1"  "H4"  "D1"  "W1"  "MN1"
USDCAD symbol timeseries: 
- Timeseries "USDCAD" M1: Requested: 1000, Actual: 0, Created: 0, On the server: 0
- Timeseries "USDCAD" M5: Requested: 1000, Actual: 0, Created: 0, On the server: 0
- Timeseries "USDCAD" M15: Requested: 1000, Actual: 0, Created: 0, On the server: 0
- Timeseries "USDCAD" M30: Requested: 1000, Actual: 0, Created: 0, On the server: 0
- Timeseries "USDCAD" H1: Requested: 1000, Actual: 0, Created: 0, On the server: 0
- Timeseries "USDCAD" H4: Requested: 1000, Actual: 0, Created: 0, On the server: 0
- Timeseries "USDCAD" D1: Requested: 1000, Actual: 0, Created: 0, On the server: 0
- Timeseries "USDCAD" W1: Requested: 1000, Actual: 0, Created: 0, On the server: 0
- Timeseries "USDCAD" MN1: Requested: 1000, Actual: 0, Created: 0, On the server: 0
Library initialization time: 00:00:01.406
"USDCAD" M1 timeseries created successfully:
- Timeseries "USDCAD" M1: Requested: 1000, Actual: 1000, Created: 1000, On the server: 5001
"USDCAD" M5 timeseries created successfully:
- Timeseries "USDCAD" M5: Requested: 1000, Actual: 1000, Created: 1000, On the server: 5741
"USDCAD" M15 timeseries created successfully:
- Timeseries "USDCAD" M15: Requested: 1000, Actual: 1000, Created: 1000, On the server: 5247
"USDCAD" M30 timeseries created successfully:
- Timeseries "USDCAD" M30: Requested: 1000, Actual: 1000, Created: 1000, On the server: 5123
"USDCAD" H1 timeseries created successfully:
- Timeseries "USDCAD" H1: Requested: 1000, Actual: 1000, Created: 1000, On the server: 6257
"USDCAD" H4 timeseries created successfully:
- Timeseries "USDCAD" H4: Requested: 1000, Actual: 1000, Created: 1000, On the server: 6232
"USDCAD" D1 timeseries created successfully:
- Timeseries "USDCAD" D1: Requested: 1000, Actual: 1000, Created: 1000, On the server: 5003
"USDCAD" W1 timeseries created successfully:
- Timeseries "USDCAD" W1: Requested: 1000, Actual: 1000, Created: 1000, On the server: 1403
"USDCAD" MN1 timeseries created successfully:
- Timeseries "USDCAD" MN1: Requested: 1000, Actual: 323, Created: 323, On the server: 323
New bar on USDCAD M1: 2020.03.19 12:18
New bar on USDCAD M1: 2020.03.19 12:19
New bar on USDCAD M1: 2020.03.19 12:20
New bar on USDCAD M5: 2020.03.19 12:20

此处我们可以看到,初始化函数库时会创建所有请求的时间序列。 但是,由于缺少数据,因此尚未填充数据。 在首次访问所请求的数据期间,终端既已启动数据下载。 随后,每次即时报价到达时,我们会收到另一个空时间序列对象,将其数据与服务器同步,之后用请求数量的柱线数据填充该时间序列对象。 在 MN1(月线图表)上实际上只有 323 根柱线可用。 所有这些都已添加到时间序列列表之中。

现在,我们以相同的设置在测试器的可视模式下启动指标:


测试器将加载所有使到的时间帧的所有必要历史记录,函数库会通知您创建除当前时间序列以外的所有时间序列。 当前品种和周期的时间序列已在 OnCalculate() 第一次进入时成功创建。 取消测试器暂停后,我们可以看到在测试器中如何触发时间序列的“新柱线”事件。

一切操作符合期望。

下一步是什么?

在下一篇文章中,我们将继续操控指标的时间序列,并用所创建时间序列进行测试,并在图表上显示信息。

以下附件是函数库当前版本的所有文件,以及测试 EA 文件,供您测试和下载。
请您在评论中留下问题和建议。

返回内容目录

该系列中的先前文章:

DoEasy 函数库中的时间序列(第三十五部分):柱线对象和品种时间序列列表
DoEasy 函数库中的时间序列(第三十六部分):所有用到的品种周期的时间序列对象
DoEasy 函数库中的时间序列(第三十七部分):时间序列集合 - 按品种和周期的时间序列数据库
DoEasy 函数库中的时间序列(第三十八部分):时间序列集合 - 实时更新以及从程序访问数据


本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/7724

附加的文件 |
MQL5.zip (3711.64 KB)
MQL4.zip (3711.64 KB)
监视多币种的交易信号(第四部分):增强功能并改善信号搜索系统 监视多币种的交易信号(第四部分):增强功能并改善信号搜索系统

在这一部分中,我们要扩展交易信号搜索和编辑系统,及介绍自定义指标,和加入程序本地化的可能性。 之前我们已创建了一个搜索信号的基本系统,但它是基于一小组指标和一组简单的搜索规则。

MQL 作为 MQL 程序图形界面的标记工具。 第一部分 MQL 作为 MQL 程序图形界面的标记工具。 第一部分

这篇论文提出了一种新的概念,即利用 MQL 结构来描述 MQL 程序的窗口界面。 特殊类将可观察的 MQL 标记转换为 GUI 元素,并允许对其进行管理,为其设置属性,并以统一的方式处理事件。 它还提供了一些运用标准库的对话框和元素标记的示例。

MQL 作为 MQL 程序图形界面的标记工具。 第二部分 MQL 作为 MQL 程序图形界面的标记工具。 第二部分

本篇论文继续验证新概念,即利用 MQL 结构描述 MQL 程序的窗口界面。 基于 MQL 标记自动创建 GUI 提供了缓存和动态生成元素和控制风格,以及事件处理的新方案。 随附的是标准控件库的增强版本。

连续前行优化 (第六部分): 自动优化器的逻辑部分和结构 连续前行优化 (第六部分): 自动优化器的逻辑部分和结构

我们之前曾研究过创建自动前行优化。 这次,我们将继续探究自动优化器工具的内部结构。 本文对于那些希望深入操控所创建项目并进行修改的人士,以及那些希望理解程序逻辑的人士来说都很有用处。 本文包含 UML 示意图,它能揭示项目的内部结构,以及对象之间的关系。 它还阐述了优化开始的过程,但未包含优化器实现过程的讲述。