内容
- 概念
- 改进操控指标的类,创建时间序列事件
- 在指标中测试时间序列及其事件
- 下一步是什么?
概念
到目前为止,我们所做的一切仅与 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, begin 和 price 变量结构。
对于应答程序的第二种形式
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_total 和 prev_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 函数库中的时间序列(第三十八部分):时间序列集合 - 实时更新以及从程序访问数据