在前两篇文章中,我们曾研究过函数库运用在指标里的能力。 特别是,我们已针对函数库时间序列实现了历史数据的正确下载,和实时刷新当前数据。 在前一篇文章里,我们已将指标缓冲区设置为在屏幕上显示数据的数据结构。 指标的单个绘制缓冲区会由单个结构定义。 如果要实现多个绘制缓冲区,则每个缓冲区都要由单独结构定义,并且每个缓冲区结构都要置于数组当中。
在本文中,我将继续优调以结构操控指标缓冲区的概念,并创建一个多品种多周期指标,按指定货币对和指定图表周期之一绘制价格蜡烛图表。 此外,我们将逐级发掘理解创建指标缓冲区类的必要性。
该函数库具有消息类,它允许选择函数库显示消息的语言,并轻松添加任意数量的自定义语言,并指定一种语言来显示函数库消息。 当前,我们尚不能选择输入描述的翻译语言。 编译后,所有输入描述均以用户在其程序中编写的输入描述语言显示。
在此,我们没有太多的自由选择来实现为程序的输入描述选择翻译语言的能力 — 要么我们只用一种语言,要么为每种所需的编译语言创建一组相似的输入。
我们选择第二个选项,并创建一个单独的文件,其中包含必要的枚举,其是为两种可能的语言提供的枚举常量 — 俄语和英语。 因此,用户应将枚举常量描述从俄语翻译成所需的语言。 由于在市场服务中发布产品需要英语,因此应始终保留英语。
我们把函数库文件中的数据位置进行一些重组。
从 OnCalculate() 应答程序将当前柱线数据传递到函数库的结构位于 \MQL5\Include\DoEasy\Defines.mqh 之中。
//+------------------------------------------------------------------+ //| Structures | //+------------------------------------------------------------------+ struct SDataCalculate { int rates_total; // size of input timeseries int prev_calculated; // number of handled bars at the previous call int begin; // where significant data starts double price; // current array value for calculation MqlRates rates; // Price structure } rates_data; //+------------------------------------------------------------------+ //| Enumerations | //+------------------------------------------------------------------+
但此结构不适用于预定义变量和静态值。 它更适合于“数据”的定义。 因此,我们将其从 Defines.mqh 中删除,并在 \MQL5\Include\DoEasy\Datas.mqh 当中定义它:
//+------------------------------------------------------------------+ //| Datas.mqh | //| 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" //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "InpDatas.mqh" //+------------------------------------------------------------------+ //| Macro substitutions | //+------------------------------------------------------------------+ #define INPUT_SEPARATOR (",") // Separator in the inputs string #define TOTAL_LANG (2) // Number of used languages //+------------------------------------------------------------------+ //| Structures | //+------------------------------------------------------------------+ struct SDataCalculate { int rates_total; // size of input timeseries int prev_calculated; // number of handled bars at the previous call int begin; // where significant data starts double price; // current array value for calculation MqlRates rates; // Price structure } rates_data; //+------------------------------------------------------------------+ //| Arrays | //+------------------------------------------------------------------+ string ArrayUsedSymbols[]; // Array of used symbols' names ENUM_TIMEFRAMES ArrayUsedTimeframes[]; // Array of used timeframes //+------------------------------------------------------------------+ //| Enumerations | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Data sets | //+------------------------------------------------------------------+
我们曾提及一个单独的文件,其中包含程序输入的枚举。 该文件尚未创建,但已为其设置了包含,以避免再次编辑 Datas.mqh 文件。
我们还在新的数组模块里加入了两个数组 — 这些数组可从基于函数库的程序中获得。 在程序输入中选择的品种和时间帧列表应包含在数组当中。
现在,我们来创建文件 \MQL5\Include\DoEasy\InpDatas.mqh 来存储程序输入的枚举:
//+------------------------------------------------------------------+ //| InpDatas.mqh | //| 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" //+------------------------------------------------------------------+ //| Macro substitutions | //+------------------------------------------------------------------+ //#define COMPILE_EN // Comment out the string for compilation in Russian //+------------------------------------------------------------------+ //| Input enumerations | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| English language inputs | //+------------------------------------------------------------------+ #ifdef COMPILE_EN //+------------------------------------------------------------------+ //| Modes of working with symbols | //+------------------------------------------------------------------+ enum ENUM_SYMBOLS_MODE { SYMBOLS_MODE_CURRENT, // Work only with the current symbol SYMBOLS_MODE_DEFINES, // Work with a given list of symbols SYMBOLS_MODE_MARKET_WATCH, // Working with Symbols from the "Market Watch" window SYMBOLS_MODE_ALL // Work with a complete list of Symbols }; //+------------------------------------------------------------------+ //| Mode of working with timeframes | //+------------------------------------------------------------------+ enum ENUM_TIMEFRAMES_MODE { TIMEFRAMES_MODE_CURRENT, // Work only with the current timeframe TIMEFRAMES_MODE_LIST, // Work with a given list of timeframes TIMEFRAMES_MODE_ALL // Work with a complete list of timeframes }; //+------------------------------------------------------------------+ //| Russian language inputs | //+------------------------------------------------------------------+ #else //+------------------------------------------------------------------+ //| Modes of working with symbols | //+------------------------------------------------------------------+ enum ENUM_SYMBOLS_MODE { SYMBOLS_MODE_CURRENT, // Работа только с текущим символом SYMBOLS_MODE_DEFINES, // Работа с заданным списком символов SYMBOLS_MODE_MARKET_WATCH, // Работа с символами из окна "Обзор рынка" SYMBOLS_MODE_ALL // Работа с полным списком символов }; //+------------------------------------------------------------------+ //| Mode of working with timeframes | //+------------------------------------------------------------------+ enum ENUM_TIMEFRAMES_MODE { TIMEFRAMES_MODE_CURRENT, // Работа только с текущим таймфреймом TIMEFRAMES_MODE_LIST, // Работа с заданным списком таймфреймов TIMEFRAMES_MODE_ALL // Работа с полным списком таймфреймов }; #endif //+------------------------------------------------------------------+
此处的一切都很简单:设置一个宏替换。 如果它不存在,则取用英语描述的枚举进行编译。 如果宏替换不存在(声明的字符串被注释掉),则用常量枚举执行编译,该枚举含有俄语(或用户为枚举常量设置的任意其他语言)描述 。
如有需要,可将新的枚举加入文件之中。
所有品种时间序列对象添加到列表的方法里出现的错误也得以修复,该错误有时会导致访问 \MQL5\Include\DoEasy\Objects\Series\TimeSeriesDE.mqh 中不存在的 CTimeSeries 类指针,从而出错:
//+------------------------------------------------------------------+ //| Add the specified timeseries list to the list | //+------------------------------------------------------------------+ bool CTimeSeriesDE::AddSeries(const ENUM_TIMEFRAMES timeframe,const uint required=0) { bool res=false; CSeriesDE *series=new CSeriesDE(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; } //+------------------------------------------------------------------+
将对象添加到列表时收到错误之后,删除 “series” 对象,并尝试为其设置用途标志。 在这种情况下,由于指向该对象的指针已被删除,因此会出现错误。
若要解决此问题,只需移动之前设置的标志,然后在代码中验证将对象添加到列表的结果:
if(this.m_list_series.Search(series)==WRONG_VALUE) res=this.m_list_series.Add(series); series.SetAvailable(true); if(!res) delete series; return res; } //+------------------------------------------------------------------+
在更新指定时间序列列表和所有时间序列列表的方法当中,并非总是能够为事件列表设置正确的“新柱线”事件(新柱线开立时间)。 有时候,时间等于零。
若要解决它,创建存储时间的新变量。 如果程序类型为“指标” ,且操作是在当前品种和图表周期上进行,则将来自 OnCalculate() 价格结构的时间写入变量,否则从时间序列对象 LastBarDate() 方法的返回值中获取时间。 在将事件添加到所有品种时间序列的所有对象事件的列表中时,使用获得的时间 :
//+------------------------------------------------------------------+ //| Update a specified timeseries list | //+------------------------------------------------------------------+ void CTimeSeriesDE::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 CSeriesDE *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); datetime time= ( this.m_program==PROGRAM_INDICATOR && series_obj.Symbol()==::Symbol() && series_obj.Timeframe()==(ENUM_TIMEFRAMES)::Period() ? data_calculate.rates.time : series_obj.LastBarDate() ); //--- If the timeseries object features the New bar event if(series_obj.IsNewBar(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,time,series_obj.Timeframe(),series_obj.Symbol())) this.m_is_event=true; } } //+------------------------------------------------------------------+ //| Update all timeseries lists | //+------------------------------------------------------------------+ void CTimeSeriesDE::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 CSeriesDE *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); datetime time= ( this.m_program==PROGRAM_INDICATOR && series_obj.Symbol()==::Symbol() && series_obj.Timeframe()==(ENUM_TIMEFRAMES)::Period() ? data_calculate.rates.time : series_obj.LastBarDate() ); //--- If the timeseries object features the New bar event if(series_obj.IsNewBar(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,time,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(); } //+------------------------------------------------------------------+
为了更新所有时间序列,我们需要将刷新当前品种和其余品种时间序列数据的方法调用安置在分离的位置。 任何其他时间序列均在计时器中更新,而在 OnCalculate() 中仅更新当前品种的时间序列。 这样做是为了避免在计时器里过度占用当前品种时间序列,因为在新即时报价到达时,会在 OnCalculate() 中调用更新当前品种时间序列。
若要在计时器中工作,在时间序列集合类 \MQL5\Include\DoEasy\Collections\TimeSeriesCollection.mqh 的文件中声明另一个方法:
//--- Update (1) the specified timeseries of the specified symbol, (2) all timeseries of a specified symbol, //--- (3) all timeseries of all symbols, (4) all timeseries except the current one void Refresh(const string symbol,const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate); void Refresh(const string symbol,SDataCalculate &data_calculate); void Refresh(SDataCalculate &data_calculate); void RefreshAllExceptCurrent(SDataCalculate &data_calculate); //--- Get events from the timeseries object and add them to the list bool SetEvents(CTimeSeriesDE *timeseries);
方法调用更新所有时间序列的方法,除了当前品种(该方法的实现):
//+------------------------------------------------------------------+ //| Update all timeseries except the current one | //+------------------------------------------------------------------+ void CTimeSeriesCollection::RefreshAllExceptCurrent(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 CTimeSeriesDE *timeseries=this.m_list.At(i); if(timeseries==NULL) continue; //--- if the timeseries symbol is equal to the current chart symbol or //--- if there is no new tick on a timeseries symbol, move to the next object in the list if(timeseries.Symbol()==::Symbol() || !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); } } //+------------------------------------------------------------------+
在函数库服务函数 \MQL5\Include\DoEasy\Services\DELib.mqh 文件中添加该函数,以便在第一个指定图表周期的一根柱线内返回第二个指定周期图表的柱线数 :
//+-------------------------------------------------------------------------+ //| Return the number of bars of one period in a single bar of another one | //+-------------------------------------------------------------------------+ int NumberBarsInTimeframe(ENUM_TIMEFRAMES timeframe,ENUM_TIMEFRAMES period=PERIOD_CURRENT) { return PeriodSeconds(timeframe)/PeriodSeconds(period==PERIOD_CURRENT ? (ENUM_TIMEFRAMES)Period() : period); } //+------------------------------------------------------------------+
由于 PeriodSeconds() 函数返回的是周期秒数,因此将较大周期秒数除以较低周期秒数,即可定义较大周期的单根柱线内所含较小周期柱线的数量。 这正是我们在此所做的。
我们能够在程序中设置所用品种的列表。 该列表是在函数库的 \MQL5\Include\DoEasy\Collections\SymbolsCollection.mqh 里的品种集合类的 SetUsedSymbols() 方法中设置的。 如果我们在程序中设置不含当前品种的已用品种列表,则除当前品种外,函数库将按时间序列设置里的品种创建所有时间序列集合。 但我们需要它,因为在屏幕上定位数据时,需要持续不断访问它。 因此,我们需要解决此问题。
在品种集合类的 SetUsedSymbols() 方法里,将当前品种添加到列表中。 如果用户未在程序设置指定的操作品种列表里加入当前品种,它会被添加到列表中。 如果该品种已存在,则同名品种不会被添加://+------------------------------------------------------------------+ //| Set the list of used symbols | //+------------------------------------------------------------------+ bool CSymbolsCollection::SetUsedSymbols(const string &symbol_used_array[]) { ::ArrayResize(this.m_array_symbols,0,1000); ::ArrayCopy(this.m_array_symbols,symbol_used_array); this.m_mode_list=this.TypeSymbolsList(this.m_array_symbols); this.m_list_all_symbols.Clear(); this.m_list_all_symbols.Sort(SORT_BY_SYMBOL_INDEX_MW); //--- Use only the current symbol if(this.m_mode_list==SYMBOLS_MODE_CURRENT) { string name=::Symbol(); ENUM_SYMBOL_STATUS status=this.SymbolStatus(name); return this.CreateNewSymbol(status,name,this.SymbolIndexInMW(name)); } else { bool res=true; //--- Use the pre-defined symbol list if(this.m_mode_list==SYMBOLS_MODE_DEFINES) { int total=::ArraySize(this.m_array_symbols); for(int i=0;i<total;i++) { string name=this.m_array_symbols[i]; ENUM_SYMBOL_STATUS status=this.SymbolStatus(name); bool add=this.CreateNewSymbol(status,name,this.SymbolIndexInMW(name)); res &=add; if(!add) continue; } //--- Create the new current symbol (if it is already in the list, it is not re-created) res &=this.CreateNewSymbol(this.SymbolStatus(NULL),NULL,this.SymbolIndexInMW(NULL)); return res; } //--- Use the full list of the server symbols else if(this.m_mode_list==SYMBOLS_MODE_ALL) { return this.CreateSymbolsList(false); } //--- Use the symbol list from the Market Watch window else if(this.m_mode_list==SYMBOLS_MODE_MARKET_WATCH) { this.MarketWatchEventsControl(false); return true; } } return false; } //+------------------------------------------------------------------+
在 CEngine 库类主对象 \MQL5\Include\DoEasy\Engine.mqh 里,声明三个私密方法:
//--- Set the list of used symbols in the symbol collection and create the collection of symbol timeseries bool SetUsedSymbols(const string &array_symbols[]); private: //--- Write all used symbols and timeframes to the ArrayUsedSymbols and ArrayUsedTimeframes arrays void WriteSymbolsPeriodsToArrays(void); //--- Check the presence of a (1) symbol in the ArrayUsedSymbols array, (2) the presence of a timeframe in the ArrayUsedTimeframes array bool IsExistSymbol(const string symbol); bool IsExistTimeframe(const ENUM_TIMEFRAMES timeframe); public: //--- Create a resource file
需要利用这些方法将品种和时间帧的列表写入 Datas.mqh 文件先前声明的数组里,返回品种存在于品种名称数组中的标志,以及返回时间帧存在于所有时间帧数组里的标志。
返回相应数组中存在品种和时间帧标志的方法实现:
//+------------------------------------------------------------------+ //| Check if a symbol is present in the array | //+------------------------------------------------------------------+ bool CEngine::IsExistSymbol(const string symbol) { int total=::ArraySize(ArrayUsedSymbols); for(int i=0;i<total;i++) if(ArrayUsedSymbols[i]==symbol) return true; return false; } //+------------------------------------------------------------------+ //| Check if a timeframe is present in the array | //+------------------------------------------------------------------+ bool CEngine::IsExistTimeframe(const ENUM_TIMEFRAMES timeframe) { int total=::ArraySize(ArrayUsedTimeframes); for(int i=0;i<total;i++) if(ArrayUsedTimeframes[i]==timeframe) return true; return false; } //+------------------------------------------------------------------+
在相应数组循环中获取下一个数组元素,并将其与传递给该方法的数值进行比较。 如果下一个数组元素的值与传递给该方法的值匹配,则返回 true。 直至整个循环完成后,返回 false — 数组中的元素值和传递给方法的值不匹配。
将用到的品种和时间帧写入数组的方法实现:
//+------------------------------------------------------------------+ //| Write all used symbols and timeframes | //| to the ArrayUsedSymbols and ArrayUsedTimeframes arrays | //+------------------------------------------------------------------+ void CEngine::WriteSymbolsPeriodsToArrays(void) { //--- Get the list of all created timeseries (created by the number of used symbols) CArrayObj *list_timeseries=this.GetListTimeSeries(); if(list_timeseries==NULL) return; //--- Get the total number of created timeseries int total_timeseries=list_timeseries.Total(); if(total_timeseries==0) return; //--- Set the size of the array of used symbols equal to the number of created timeseries, while //--- the size of the array of used timeframes is set equal to the maximum possible number of timeframes in the terminal if(::ArrayResize(ArrayUsedSymbols,total_timeseries,1000)!=total_timeseries || ::ArrayResize(ArrayUsedTimeframes,21,21)!=21) return; //--- Set both arrays to zero ::ZeroMemory(ArrayUsedSymbols); ::ZeroMemory(ArrayUsedTimeframes); //--- Reset the number of added symbols and timeframes to zero and, //--- in a loop by the total number of timeseries, int num_symbols=0,num_periods=0; for(int i=0;i<total_timeseries;i++) { //--- get the next object of all timeseries of a single symbol CTimeSeriesDE *timeseries=list_timeseries.At(i); if(timeseries==NULL || this.IsExistSymbol(timeseries.Symbol())) continue; //--- increase the number of used symbols and (num_symbols variable), and //--- write the timeseries symbol name to the array of used symbols by the num_symbols-1 index num_symbols++; ArrayUsedSymbols[num_symbols-1]=timeseries.Symbol(); //--- Get the list of all its timeseries from the object of all symbol timeseries CArrayObj *list_series=timeseries.GetListSeries(); if(list_series==NULL) continue; //--- In the loop by the total number of symbol timeseries, int total_series=list_series.Total(); for(int j=0;j<total_series;j++) { //--- get the next timeseries object CSeriesDE *series=list_series.At(j); if(series==NULL || this.IsExistTimeframe(series.Timeframe())) continue; //--- increase the number of used timeframes and (num_periods variable), and //--- write the timeseries timeframe value to the array of used timeframes by num_periods-1 index num_periods++; ArrayUsedTimeframes[num_periods-1]=series.Timeframe(); } } //--- Upon the loop completion, change the size of both arrays to match the exact number of added symbols and timeframes ::ArrayResize(ArrayUsedSymbols,num_symbols,1000); ::ArrayResize(ArrayUsedTimeframes,num_periods,21); } //+------------------------------------------------------------------+
该方法查看程序中用到的每个品种的所有已创建时间序列,并用时间序列集合中的数据填充所用品种和时间帧的数组。 所有方法列表代码都加了详细注释,且易于理解。 如果您有任何疑问,请随时在下面的评论中提问。
在时间序列更新方法的模块里,添加受保护的方法,更新除当前品种以外的所有时间序列:
//--- Update (1) the specified timeseries of the specified symbol, (2) all timeseries of a specified symbol, //--- (3) all timeseries of all symbols, (4) all timeseries except the current one void SeriesRefresh(const string symbol,const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate) { this.m_time_series.Refresh(symbol,timeframe,data_calculate); } void SeriesRefresh(const string symbol,SDataCalculate &data_calculate) { this.m_time_series.Refresh(symbol,data_calculate); } void SeriesRefresh(SDataCalculate &data_calculate) { this.m_time_series.Refresh(data_calculate); } protected: void SeriesRefreshAllExceptCurrent(SDataCalculate &data_calculate) { this.m_time_series.GetObject().RefreshAllExceptCurrent(data_calculate); } public: //--- Return (1) the timeseries object of the specified symbol and (2) the timeseries object of the specified symbol/period
我们早前研究过这种方法的必要性。 在此,该方法仅调用了我们上面已研究过的同名的时间序列集合类方法。
在类计时器里处理时间序列集合的模块里,调用此方法,从而更新除当前品种以外的所有时间序列:
//+------------------------------------------------------------------+ //| CEngine timer | //+------------------------------------------------------------------+ void CEngine::OnTimer(SDataCalculate &data_calculate) { // // here I have removed some code not needed for the current example // //--- Timeseries collection timer index=this.CounterIndex(COLLECTION_TS_COUNTER_ID); CTimerCounter* cnt6=this.m_list_counters.At(index); if(cnt6!=NULL) { //--- If the pause is over, work with the timeseries list (update all except the current one) if(cnt6.IsTimeDone()) this.SeriesRefreshAllExceptCurrent(data_calculate); } } //--- If this is a tester, work with collection events by tick else { // // here I have removed some code not needed for the current example // } }
在 Calculate 事件应答程序里(即,在 CEngine 函数库主对象的 OnCalculate() 方法中),如果尚未创建所有时间序列,则返回零,或是 rate_total 若所有用到的时间序列已全部创建:
//+------------------------------------------------------------------+ //| 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 0; //--- Re-create empty timeseries //--- If at least one of the timeseries is not synchronized, return zero if(!this.SeriesSync(data_calculate,required)) return 0; //--- Update the timeseries of the current symbol (not in the tester) and //--- return either 0 (in case there are empty timeseries), or rates_total if(!this.IsTester()) this.SeriesRefresh(NULL,data_calculate); return(this.SeriesGetSeriesEmpty()==NULL ? data_calculate.rates_total : 0); } //+------------------------------------------------------------------+
以前,经由当前柱线价格结构传递给该方法的 rates_total 会立即被返回。 然而,我们需要管控从方法返回的值,以便正确处理时间序列同步。 返回零可启动基于整个历史记录的重新计算,而 rates_total 仅用于计算尚未计算的数据(通常为 0 — 当前柱线计算,或 1 — 在新柱线开立之时,计算前一根和当前柱线)。
在为所有用到的品种创建所有时间序列的方法里,把所有用到的品种和时间序列写入数组:
//+------------------------------------------------------------------+ //| 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); } } //--- Write all used symbols and timeframes to the ArrayUsedSymbols and ArrayUsedTimeframes arrays this.WriteSymbolsPeriodsToArrays(); //--- Return the result of creating all timeseries for all symbols return res; } //+------------------------------------------------------------------+
自程序中的函数库初始化函数调用该方法之后,如有必要,正在调用的方法应准备两个在程序中使用的数组 — 数组保存所有用到的品种,和所有用到的时间帧。 该方法已在上面研究。
库类的改进至此完结。
今天,我们将创建一个测试指标,以便查看函数库如何在多品种多周期模式下与指标一起操作。
该指标提供设置四个品种和所有时间序列的能力。 每个品种和时间帧均可通过按钮选择来操作。 该图表最多显示四个按钮,这些按钮的名称需在设置中指定。 含有可用时间帧的按钮列表,显示在已按下品种按钮的对面。
任何时候都只能有一个品种和一个时间帧按钮可按下。
这令我们能够选择指标里要用的品种以及时间帧,其数据将显示在图表的指标子窗口中。 展望未来,我应该注意到以过程风格操控按钮的实现对我来说非常不方便。 因此,管控按钮状态的代码有很大的改进空间。 无论如何,在多品种多周期指标中测试时间序列操作仍然足够。 毕竟这只是一个测试用指标。
之前的测试指标和当前开发的指标背后的思想,不仅在于测试和检查函数库时间序列在指标里的操作,还要运行指标缓冲区结构的测试。 我们将基于所获得的结构运用知识来安排一组指标缓冲区类。 今天,我将补充缓冲区结构,令其绘制烛条样式的图形缓冲区。
为了创建测试指标,借用上一篇文章中的指标,并将其保存在 \MQL5\Indicators\TestDoEasy\Part41\ 之下,名称为 TestDoEasyPart41.mq5。
首先,指定在单独的窗口中绘制指标,定义所有必要的指标缓冲区,然后再添加一个宏替换,指示所用品种的最大数量 (以及相应地,指标绘制缓冲区的数量):
//+------------------------------------------------------------------+ //| TestDoEasyPart41.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> //--- properties #property indicator_separate_window #property indicator_buffers 21 // 5 arrays (Open[] High[] Low[] Close[] Color[]) * 4 drawn buffers + 1 BufferTime[] calculated buffer #property indicator_plots 4 // 1 candlesticks buffer consisting of 5 arrays (Open[] High[] Low[] Close[] Color[]) * 4 symbols //--- plot Pair1 #property indicator_label1 "Pair 1" #property indicator_type1 DRAW_COLOR_CANDLES #property indicator_color1 clrLimeGreen,clrRed,clrDarkGray //--- plot Pair2 #property indicator_label2 "Pair 2" #property indicator_type2 DRAW_COLOR_CANDLES #property indicator_color2 clrDeepSkyBlue,clrFireBrick,clrDarkGray //--- plot Pair3 #property indicator_label3 "Pair 3" #property indicator_type3 DRAW_COLOR_CANDLES #property indicator_color3 clrMediumPurple,clrDarkSalmon,clrGainsboro //--- plot Pair4 #property indicator_label4 "Pair 4" #property indicator_type4 DRAW_COLOR_CANDLES #property indicator_color4 clrMediumAquamarine,clrMediumVioletRed,clrGainsboro //--- classes //--- enums //--- defines #define PERIODS_TOTAL (21) // Total amount of available chart periods #define SYMBOLS_TOTAL (4) // Maximum number of drawn symbol buffers //--- structures
为什么指标缓冲区的数量等于 21?
答案很简单: DRAW_COLOR_CANDLES 绘图样式意味着有五个与之关联的数组:
指标中用到的最大品种数量等于 4。 相应地,四个绘制缓冲区带有五个关联数组,这意味着 20 个指标缓冲区。 还需要一个缓冲区来存储柱线时间。 时间会被传递给函数。 总计:21 个指标缓冲区,其中四个是绘制。
编写烛条缓冲区结构:
//--- structures struct SDataBuffer // Candlesticks buffer structure { private: ENUM_TIMEFRAMES m_buff_timeframe; // Buffer timeframe string m_buff_symbol; // Buffer symbol int m_buff_open_index; // The index of the indicator buffer related to the Open[] array int m_buff_high_index; // The index of the indicator buffer related to the High[] array int m_buff_low_index; // The index of the indicator buffer related to the Low[] array int m_buff_close_index; // The index of the indicator buffer related to the Close[] array int m_buff_color_index; // The index of the color buffer related to the Color[] array int m_buff_next_index; // The index of the next free indicator buffer bool m_used; // The flag of using the buffer in the indicator bool m_show_data; // The flag of displaying the buffer on the chart before enabling/disabling its display public: double Open[]; // The array assigned as INDICATOR_DATA by the Open indicator buffer double High[]; // The array assigned as INDICATOR_DATA by the High indicator buffer double Low[]; // The array assigned as INDICATOR_DATA by the Low indicator buffer double Close[]; // The array assigned as INDICATOR_DATA by the Close indicator buffer double Color[]; // The array assigned as INDICATOR_COLOR_INDEX by the Color indicator buffer //--- Set indices for the drawn OHLC and Color buffers void SetIndexes(const int index_first) { this.m_buff_open_index=index_first; this.m_buff_high_index=index_first+1; this.m_buff_low_index=index_first+2; this.m_buff_close_index=index_first+3; this.m_buff_color_index=index_first+4; this.m_buff_next_index=index_first+5; } //--- Methods of setting and returning values of the private structure members void SetTimeframe(const ENUM_TIMEFRAMES timeframe) { this.m_buff_timeframe=(timeframe==PERIOD_CURRENT ? ::Period() : timeframe); } void SetSymbol(const string symbol) { this.m_buff_symbol=symbol; } void SetUsed(const bool flag) { this.m_used=flag; } void SetShowDataFlag(const bool flag) { this.m_show_data=flag; } int IndexOpenBuffer(void) const { return this.m_buff_open_index; } int IndexHighBuffer(void) const { return this.m_buff_high_index; } int IndexLowBuffer(void) const { return this.m_buff_low_index; } int IndexCloseBuffer(void) const { return this.m_buff_close_index; } int IndexColorBuffer(void) const { return this.m_buff_color_index; } int IndexNextBuffer(void) const { return this.m_buff_next_index; } ENUM_TIMEFRAMES Timeframe(void) const { return this.m_buff_timeframe; } string Symbol(void) const { return this.m_buff_symbol; } bool IsUsed(void) const { return this.m_used; } bool GetShowDataFlag(void) const { return this.m_show_data; } void Print(void); }; //--- Display structure data to the journal void SDataBuffer::Print(void) { string array[8]; array[0]="Buffer "+this.Symbol()+" "+TimeframeDescription(this.Timeframe())+":"; array[1]=" Open buffer index: "+(string)this.IndexOpenBuffer(); array[2]=" High buffer index: "+(string)this.IndexHighBuffer(); array[3]=" Low buffer index: "+(string)this.IndexLowBuffer(); array[4]=" Close buffer index: "+(string)this.IndexCloseBuffer(); array[5]=" Color buffer index: "+(string)this.IndexColorBuffer(); array[6]=" Next buffer index: "+(string)this.IndexNextBuffer(); array[7]=" Used: "+(string)(bool)this.IsUsed(); for(int i=0;i<ArraySize(array);i++) ::Print(array[i]); } //--- input variables
该结构含有存储缓冲区索引值的变量,它与相应的 OHLC 和 Color 数组相关。 我们总是能够按索引访问必要的缓冲区。 下一个可绑定指标缓冲区结构数组的空闲索引,始终可以利用 IndexNextBuffer() 方法从 m_buff_next_index 变量里获取,返回的索引位于当前结构的颜色缓冲区之后。
根据清单,该结构含有设置和返回私密结构部分中定义的所有数值的所有方法,以及用于在日志中打印所有结构数据的方法:OHLC 和颜色缓冲区索引数据,下一个可绑定的自由索引,缓冲区数据可用于生成数组的标志。 接下来,所有这些循环数据都将从数组传递到日志。
例如,该方法将指标配置中设置的四个绘制缓冲区上的数据发送到日志(AUDUSD 缓冲区显示在图表上):
2020.04.08 21:55:21.528 Buffer EURUSD H1: 2020.04.08 21:55:21.528 Open buffer index: 0 2020.04.08 21:55:21.528 High buffer index: 1 2020.04.08 21:55:21.528 Low buffer index: 2 2020.04.08 21:55:21.528 Close buffer index: 3 2020.04.08 21:55:21.528 Color buffer index: 4 2020.04.08 21:55:21.528 Next buffer index: 5 2020.04.08 21:55:21.528 Used: false 2020.04.08 21:55:21.530 Buffer AUDUSD H1: 2020.04.08 21:55:21.530 Open buffer index: 5 2020.04.08 21:55:21.530 High buffer index: 6 2020.04.08 21:55:21.530 Low buffer index: 7 2020.04.08 21:55:21.530 Close buffer index: 8 2020.04.08 21:55:21.530 Color buffer index: 9 2020.04.08 21:55:21.530 Next buffer index: 10 2020.04.08 21:55:21.530 Used: true 2020.04.08 21:55:21.532 Buffer EURAUD H1: 2020.04.08 21:55:21.532 Open buffer index: 10 2020.04.08 21:55:21.532 High buffer index: 11 2020.04.08 21:55:21.532 Low buffer index: 12 2020.04.08 21:55:21.532 Close buffer index: 13 2020.04.08 21:55:21.532 Color buffer index: 14 2020.04.08 21:55:21.532 Next buffer index: 15 2020.04.08 21:55:21.532 Used: false 2020.04.08 21:55:21.533 Buffer EURGBP H1: 2020.04.08 21:55:21.533 Open buffer index: 15 2020.04.08 21:55:21.533 High buffer index: 16 2020.04.08 21:55:21.533 Low buffer index: 17 2020.04.08 21:55:21.533 Close buffer index: 18 2020.04.08 21:55:21.533 Color buffer index: 19 2020.04.08 21:55:21.533 Next buffer index: 20 2020.04.08 21:55:21.533 Used: false
正如我们所见,每根后续烛条缓冲区的“开盘价”缓冲区索引与上一根烛条缓冲区的“下一个缓冲区索引”索引匹配。 下一个可用缓冲区的索引为 20。 该索引可用于分配下一个索引,例如,计算用指标缓冲区。 对于存储当前图表柱线时间的计算用缓冲区,这正是恰当的操作。
添加指标输入模块:
//--- input variables /*sinput*/ ENUM_SYMBOLS_MODE InpModeUsedSymbols= SYMBOLS_MODE_DEFINES; // Mode of used symbols list sinput string InpUsedSymbols = "EURUSD,AUDUSD,EURAUD,EURGBP,EURCAD,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 uint InpButtShiftX = 0; // Buttons X shift sinput uint InpButtShiftY = 10; // Buttons Y shift sinput bool InpUseSounds = true; // Use sounds //--- indicator buffers
在 Defines.mqh 中供选择品种操作模式的 ENUM_SYMBOLS_MODE 枚举含有两个不必要的模式:Work with Market Watch window symbols" 和 "Work with the full list of symbols":
//+------------------------------------------------------------------+ //| Modes of working with symbols | //+------------------------------------------------------------------+ enum ENUM_SYMBOLS_MODE { SYMBOLS_MODE_CURRENT, // Work with the current symbol only SYMBOLS_MODE_DEFINES, // Work with the specified symbol list SYMBOLS_MODE_MARKET_WATCH, // Work with the Market Watch window symbols SYMBOLS_MODE_ALL // Work with the full symbol list }; //+------------------------------------------------------------------+
...然后为了避免在设置中选择这两种模式,请注释掉 sinput 修饰符,令 InpModeUsedSymbols 变量成为非外部。 因此,在指标里操控品种的模式始终等于 “Work with the specified symbol list”,和输入 InpUsedSymbols 里指定的列表前四个品种。
我们来编写指标缓冲区的定义和全局变量模块:
//--- indicator buffers SDataBuffer Buffers[]; // Array of the indicator buffer data structures assigned to the timeseries double BufferTime[]; // The calculated buffer for storing and passing data from the time[] array //--- global variables CEngine engine; // CEngine library main object string prefix; // Prefix of graphical object names int min_bars; // The minimum number of bars for the indicator calculation int used_symbols_mode; // Mode of working with symbols string array_used_symbols[]; // The array for passing used symbols to the library string array_used_periods[]; // The array for passing used timeframes to the library //+------------------------------------------------------------------+
我们将烛条缓冲区结构数组声明为绘制缓冲区。 这比定义四个相似的缓冲区要方便得多。 此外,按数组里匹配按钮的位置索引访问每个缓冲区要方便得多:如果您需要按第一个按钮选择缓冲区,则选择数组中位于首位的缓冲区,如果您需要选择最后一个按钮分配的缓冲区,则在数组中选择最后一个缓冲区,依此类推。
计算用缓冲区 之一:时间缓冲区 — 它是必需的,把来自指标 OnCalculate() 应答函数的 time[] 预定义数组传递给指标函数,作为柱线时间。
包含全局变量的模块我们已经很熟悉了,我在几乎所有文章中的测试 EA 和指标里都用过的。 所有变量都带有注释,因此这里没有必要再对它们进行全面分析。
我们需要指标计算所需的最小柱线数量,定义这些柱线是否足以计算时间序列,以便在较低时间帧的图表上正确显示较高时间帧的指标数据。
根据所应用的时间周期,计算当前图表所需柱线数量,经由之前研究的函数库服务函数 DELib.mqh 文件中包含的 NumberBarsInTimeframe() 函数执行。
我曾提到过以程序方式编写指标时遇到的困难 — 我不得不创建其他辅助函数来搜索、设置和监视按钮和缓冲区状态。 如果将按钮和缓冲区编写为对象,则访问其属性和模式将大大简化。 但我不打算改变任何事情。 以进程形式引入测试似乎更快。 此外,测试指标不需要临时对象。 之后它们就没用了。
我们来研究一下新开发的辅助函数。
设置指标绘制用缓冲区状态的函数:
//+------------------------------------------------------------------+ //| Set the state for drawn buffers | //+------------------------------------------------------------------+ void SetPlotBufferState(const int buffer_index,const bool state) { //--- Depending on a passed status, define whether data should be displayed in the data window (state==true) or not (state==false) PlotIndexSetInteger(buffer_index,PLOT_SHOW_DATA,state); //--- Create the buffer description consisting of a symbol and timeframe and set the buffer description by its buffer_index index string params=Buffers[buffer_index].Symbol()+" "+TimeframeDescription(Buffers[buffer_index].Timeframe()); string label=params+" Open;"+params+" High;"+params+" Low;"+params+" Close"; PlotIndexSetString(buffer_index,PLOT_LABEL,(state ? label : NULL)); //--- If the buffer is active (drawn), set a short name for the indicator with the displayed symbol and timeframe if(state) IndicatorSetString(INDICATOR_SHORTNAME,engine.Name()+" "+Buffers[buffer_index].Symbol()+" "+TimeframeDescription(Buffers[buffer_index].Timeframe())); } //+------------------------------------------------------------------+
该函数传递缓冲区索引,以便设置状态。 所需状态也通过输入来传递。
当需要显示若干个指标缓冲区相关数组时,应记住这个特征。
例如,如果指标缓冲区需要 2 个数组,则与该缓冲区相关的数组索引应等于 0 和 1。 这些值是利用 SetIndexBuffer() 函数设置的。 把两个数据数组作为一个绘制用缓冲区应用,不会导致访问绘制用缓冲区的理解产生任何问题 — 只需指定索引为 0 的缓冲区即可访问其属性。
但如果我们需要两个数组作为两个或更多的绘制用缓冲区,则可能会有误解,到底要用哪个索引来访问第二、第三和后续的绘制用缓冲区。
我们来研究一个含有三个绘制用缓冲区的示例,每个缓冲区都有两个数组,且绘制缓冲区有其索引编号和数组:似乎三个绘制缓冲区含有六个数组,并且为了访问第二个绘制数组,我们应该访问索引 2(因为第一个缓冲区数组占用了 0 和 1)。 然而,情况并非如此。 若要访问第二个绘制缓冲区,我们需要访问绘制缓冲区的索引,而不是访问为每个绘制缓冲区分配的所有数组,即,按索引 1。
因此,为了利用 SetIndexBuffer() 函数将数组与缓冲区绑定,应指定作为指标缓冲区的所有数组的序列编号。 然而,为了利用 PlotIndexGetInteger() 函数从绘制缓冲区中获取数据,或利用 PlotIndexSetDouble() 为绘制缓冲区设置数据,请用 PlotIndexSetInteger() 和 PlotIndexSetString() 函数为每个绘制缓冲区设置索引,而不是设置数组编号。 在当前示例中,第一个绘制缓冲区的索引为 0,第二个缓冲区的索引为 1,第三个缓冲区的索引为 2。 应该考虑到这一点。
该函数返回设置中指定品种正在使用的标志:
//+------------------------------------------------------------------+ //| Return the flag of using a symbol specified in the settings | //+------------------------------------------------------------------+ bool IsUsedSymbolByInput(const string symbol) { int total=ArraySize(array_used_symbols); for(int i=0;i<total;i++) if(array_used_symbols[i]==symbol) return true; return false; } //+------------------------------------------------------------------+
如果用到品种数组中存在该品种,则该函数返回 true,否则返回 false。 有时,我们也许未在用到品种列表里指定当前品种,但它始终存在 — 它的数据对于执行函数库内部计算是必需的。 该函数返回当前品种未在设置中指定的指示标志,故应将其跳过。
该函数按品种返回绘制缓冲区的索引:
//+------------------------------------------------------------------+ //| Return the structure drawn buffer index by symbol | //+------------------------------------------------------------------+ int IndexBuffer(const string symbol) { int total=ArraySize(Buffers); for(int i=0;i<total;i++) if(Buffers[i].Symbol()==symbol) return i; return WRONG_VALUE; } //+------------------------------------------------------------------+
该函数接收品种名称,返回其缓冲区索引。 在所有缓冲区的循环中,搜索缓冲区品种名称,并在匹配的情况下返回循环索引。 如果没有匹配该品种的缓冲区,则返回 -1。
函数返回下一个指标绘制缓冲区的第一个可分配索引:
//+------------------------------------------------------------------+ //| Return the first free index of the drawn buffer | //+------------------------------------------------------------------+ int FirstFreePlotBufferIndex(void) { int num=WRONG_VALUE,total=ArraySize(Buffers); for(int i=0;i<total;i++) if(Buffers[i].IndexNextBuffer()>num) num=Buffers[i].IndexNextBuffer(); return num; } //+------------------------------------------------------------------+
在缓冲区结构数组中遍历所有绘制缓冲区的循环中,检查下一个空闲缓冲区的值。
若其超过前一个,则记住新值。 直至循环完成后,返回写在 num 变量里的值。
编写函数,执行安装和搜索按钮/缓冲区状态的辅助动作:
//+------------------------------------------------------------------+ //| Return the button status | //+------------------------------------------------------------------+ bool ButtonState(const string name) { return (bool)ObjectGetInteger(0,name,OBJPROP_STATE); } //+------------------------------------------------------------------+ //| Set the button status | //+------------------------------------------------------------------+ void SetButtonState(const string button_name,const bool state) { //--- Set the button status and its color depending on the status ObjectSetInteger(0,button_name,OBJPROP_STATE,state); if(state) ObjectSetInteger(0,button_name,OBJPROP_BGCOLOR,C'220,255,240'); else ObjectSetInteger(0,button_name,OBJPROP_BGCOLOR,C'240,240,240'); //--- If not in the tester, //--- set the status to the terminal global variable if(!engine.IsTester()) GlobalVariableSet((string)ChartID()+"_"+button_name,state); } //+------------------------------------------------------------------+ //| Set the symbol button status | //+------------------------------------------------------------------+ void SetButtonSymbolState(const string button_symbol_name,const bool state) { //--- Set the symbol button status SetButtonState(button_symbol_name,state); //--- Detect wrong names if the timeframe button status is not specified //--- Write the button status to the global variable only if its name contains no "PERIOD_CURRENT" substring if(StringFind(button_symbol_name,"PERIOD_CURRENT")==WRONG_VALUE) GlobalVariableSet((string)ChartID()+"_"+button_symbol_name,state); //--- Set the visibility for all period buttons corresponding to the symbol button SetButtonPeriodVisible(button_symbol_name,state); } //+------------------------------------------------------------------+ //| Set the period button status | //+------------------------------------------------------------------+ void SetButtonPeriodState(const string button_period_name,const bool state) { //--- Set the button status and write it to the terminal global variable SetButtonState(button_period_name,state); GlobalVariableSet((string)ChartID()+"_"+button_period_name,state); } //+------------------------------------------------------------------+ //| Set the "visibility" of period buttons for the symbol button | //+------------------------------------------------------------------+ void SetButtonPeriodVisible(const string button_symbol_name,const bool state_symbol) { //--- In the loop by the amount of used timeframes int total=ArraySize(array_used_periods); for(int j=0;j<total;j++) { //--- create the name of the next period button string butt_name_period=button_symbol_name+"_"+EnumToString(ArrayUsedTimeframes[j]); //--- Set the status and visibility for the period button depending on the symbol button status ObjectSetInteger(0,butt_name_period,OBJPROP_TIMEFRAMES,(engine.IsTester() ? OBJ_ALL_PERIODS : (state_symbol ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS))); } } //+------------------------------------------------------------------+ //| Reset the states of the remaining symbol buttons | //+------------------------------------------------------------------+ void ResetButtonSymbolState(const string button_symbol_name) { //--- In the loop by all chart objects, for(int i=ObjectsTotal(0,0)-1;i>WRONG_VALUE;i--) { //--- get the name of the next object string name=ObjectName(0,i,0); //--- If this is a pressed button, or the object does not belong to the indicator, or this is a period button, move on to the next one if(name==button_symbol_name || StringFind(name,prefix)==WRONG_VALUE || StringFind(name,"_PERIOD_")>0) continue; //--- Reset the symbol button status by object name SetButtonSymbolState(name,false); } } //+------------------------------------------------------------------+ //| Reset the states of the remaining symbol period buttons | //+------------------------------------------------------------------+ void ResetButtonPeriodState(const string button_period_name,const string symbol) { //--- In the loop by all chart objects, for(int i=ObjectsTotal(0,0)-1;i>WRONG_VALUE;i--) { //--- get the name of the next object string name=ObjectName(0,i,0); //--- If this is a pressed button, or the object does not belong to the indicator, or this is not a period button, or the button does not belong to the symbol, move on to the next one if(name==button_period_name || StringFind(name,prefix)==WRONG_VALUE || StringFind(name,"_PERIOD_")==WRONG_VALUE || StringFind(name,symbol)==WRONG_VALUE) continue; //--- Reset the period button status by object name SetButtonPeriodState(name,false); } } //+---------------------------------------------------------------------------+ //| Return the name of the pressed period button corresponding to the symbol | //+---------------------------------------------------------------------------+ string GetNamePressedTimeframe(const string button_symbol_name,const string symbol) { //--- In the loop by all chart objects, for(int i=ObjectsTotal(0,0)-1;i>WRONG_VALUE;i--) { //--- get the name of the next object string name=ObjectName(0,i,0); //--- If this is a pressed button, or the object does not belong to the indicator, or this is not a period button, or the button does not belong to the symbol, move on to the next one if(name==button_symbol_name || StringFind(name,prefix)==WRONG_VALUE || StringFind(name,"_PERIOD_")==WRONG_VALUE || StringFind(name,symbol)==WRONG_VALUE) continue; //--- If the button is pressed, return the name of the pressed button graphic object if(ButtonState(name)) return name; } //--- Return NULL if no symbol period buttons are pressed return NULL; } //+------------------------------------------------------------------+ //| Set the buffer states, 'true' - only for the specified one | //+------------------------------------------------------------------+ void SetAllBuffersState(const string symbol) { int total=ArraySize(Buffers); //--- Get the specified buffer index int index=IndexBuffer(symbol); //--- In a loop by the number of drawn buffers for(int i=0;i<total;i++) { //--- if the loop index is equal to the specified buffer index, //--- set the flag of its usage to 'true', otherwise - to 'false' //--- forcibly set the flag indicating that pressing the button for the buffer has already been handled Buffers[i].SetUsed(i!=index ? false : true); Buffers[i].SetShowDataFlag(false); } } //+------------------------------------------------------------------+
处理按钮按下的函数进行了略微的修改,因为我们只能按下一个品种按钮和一个对应于该品种周期的按钮:
//+------------------------------------------------------------------+ //| Handle pressing the buttons | //+------------------------------------------------------------------+ void PressButtonEvents(const string button_name) { //--- Convert button name into its string ID string button=StringSubstr(button_name,StringLen(prefix)); //--- Get the index of the drawn buffer by timeframe, its symbol and index int index=StringFind(button,"_PERIOD_"); string symbol=StringSubstr(button,5,index-5); int buffer_index=IndexBuffer(symbol); //--- Create the button name for the terminal's global variable string name_gv=(string)ChartID()+"_"+prefix+button; //--- Get the button status (pressed/released). If not in the tester, //--- write the status to the button global variable (1 or 0) bool state=ButtonState(button_name); if(!engine.IsTester()) GlobalVariableSet(name_gv,state); //--- Set the button color depending on its status, //--- write its status to the buffer structure depending on the button status (used/not used) //--- initialize the buffer corresponding to the button timeframe by the buffer index received earlier if(StringFind(button_name,"_PERIOD_")==WRONG_VALUE) { SetButtonSymbolState(button_name,state); ResetButtonSymbolState(button_name); } else { SetButtonPeriodState(button_name,state); ResetButtonPeriodState(button_name,symbol); } //--- Get the timeframe from the pressed symbol timeframe button string pressed_period=GetNamePressedTimeframe(button_name,symbol); ENUM_TIMEFRAMES timeframe= ( StringFind(button,"_PERIOD_")==WRONG_VALUE ? TimeframeByDescription(StringSubstr(pressed_period,StringFind(pressed_period,"_PERIOD_")+8)) : TimeframeByDescription(StringSubstr(button,StringFind(button,"_PERIOD_")+8)) ); //--- Set the states of all buffers, 'true' - for the pressed button symbol buffer, the rest are 'false' SetAllBuffersState(symbol); //--- Set the displayed timeframe for the buffer Buffers[buffer_index].SetTimeframe(timeframe); //--- If the button pressing is not handled yet if(Buffers[buffer_index].GetShowDataFlag()!=state) { //--- Initialize all indicator buffers InitBuffersAll(); //--- If the buffer is active, fill it with historical data if(state) BufferFill(buffer_index); //--- Set the flag indicating that pressing the button has already been handled Buffers[buffer_index].SetShowDataFlag(state); } //--- Here you can add additional handling of button pressing: //--- If the button is pressed if(state) { //--- If M1 button is pressed if(button=="BUTT_M1") { } //--- If button M2 is pressed else if(button=="BUTT_M2") { } //--- // Remaining buttons ... //--- } //--- Not pressed else { //--- M1 button if(button=="BUTT_M1") { } //--- M2 button if(button=="BUTT_M2") { } //--- // Remaining buttons ... //--- } //--- re-draw the chart ChartRedraw(); } //+------------------------------------------------------------------+
创建按钮面板的函数:
//+------------------------------------------------------------------+ //| Create the buttons panel | //+------------------------------------------------------------------+ bool CreateButtons(const int shift_x=20,const int shift_y=0) { int total_symbols=ArraySize(array_used_symbols); int total_periods=ArraySize(ArrayUsedTimeframes); uint ws=48,hs=18,w=26,h=16,shift_h=2,x=InpButtShiftX+1, y=InpButtShiftY+h+1; //--- In the loop by the number of used symbols, for(int i=0;i<SYMBOLS_TOTAL;i++) { //--- create the name of the next symbol button string butt_name_symbol=prefix+"BUTT_"+array_used_symbols[i]; //--- create the next symbol button with a shift calculated as //--- ((button height + 2) * loop index) uint ys=y+(hs+shift_h)*i; if(ButtonCreate(butt_name_symbol,x,ys,ws,hs,array_used_symbols[i],clrGray)) { bool state_symbol=(engine.IsTester() && i==0 ? true : false); //--- If not in the tester, if(!engine.IsTester()) { //--- set the name of the terminal global variable for storing the symbol button status string name_gv_symbol=(string)ChartID()+"_"+butt_name_symbol; //--- if there is no global variable with the symbol name, create it set to 'false', if(!GlobalVariableCheck(name_gv_symbol)) GlobalVariableSet(name_gv_symbol,false); //--- get the symbol button status from the terminal global variable state_symbol=GlobalVariableGet(name_gv_symbol); } //--- Set the status for the symbol button SetButtonState(butt_name_symbol,state_symbol); //--- In the loop by the amount of used timeframes for(int j=0;j<total_periods;j++) { //--- create the name of the next period button string butt_name_period=butt_name_symbol+"_"+EnumToString(ArrayUsedTimeframes[j]); uint yp=ys-(hs-h)/2; //--- create the next period button with a shift calculated as //--- (symbol button width + (period button width + 1) * loop index) if(ButtonCreate(butt_name_period,x+ws+2+(w+1)*j,yp,w,h,TimeframeDescription(ArrayUsedTimeframes[j]),clrGray)) ObjectSetInteger(0,butt_name_period,OBJPROP_TIMEFRAMES,(engine.IsTester() ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS)); else { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_name_period,"\""); return false; } bool state_period=(engine.IsTester() && ArrayUsedTimeframes[j]==Period() ? true : false); //--- If not in the tester, if(!engine.IsTester()) { //--- set the name of the terminal global variable for storing the period button status string name_gv_period=(string)ChartID()+"_"+butt_name_period; //--- if there is no global variable with the period name, create it set to 'false', if(!GlobalVariableCheck(name_gv_period)) GlobalVariableSet(name_gv_period,false); //--- get the period button status from the terminal global variable state_period=GlobalVariableGet(name_gv_period); } //--- Set the status and visibility for the period button depending on the symbol button status SetButtonState(butt_name_period,state_period); ObjectSetInteger(0,butt_name_period,OBJPROP_TIMEFRAMES,(engine.IsTester() ? OBJ_ALL_PERIODS : (state_symbol ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS))); } } else { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_name_symbol,"\""); return false; } } ChartRedraw(0); return true; } //+------------------------------------------------------------------+
初始化指标绘制缓冲区的函数:
//+------------------------------------------------------------------+ //| Initialize the timeseries and the appropriate buffers by index | //+------------------------------------------------------------------+ bool InitBuffer(const int buffer_index) { //--- Leave if the wrong index is passed if(buffer_index==WRONG_VALUE) return false; //--- Initialize drawn OHLC buffers using the "empty" value, while Color is initialized using zero ArrayInitialize(Buffers[buffer_index].Open,EMPTY_VALUE); ArrayInitialize(Buffers[buffer_index].High,EMPTY_VALUE); ArrayInitialize(Buffers[buffer_index].Low,EMPTY_VALUE); ArrayInitialize(Buffers[buffer_index].Close,EMPTY_VALUE); ArrayInitialize(Buffers[buffer_index].Color,0); //--- Set the flag of the buffer display in the data window by index SetPlotBufferState(buffer_index,Buffers[buffer_index].IsUsed()); return true; } //+------------------------------------------------------------------+ //| Initialize all timeseries and the appropriate buffers | //+------------------------------------------------------------------+ void InitBuffersAll(void) { //--- Initialize the next buffer in the loop by the total number of chart periods int total=ArraySize(Buffers); for(int i=0;i<total;i++) InitBuffer(i); } //+------------------------------------------------------------------+
计算所有活动指标缓冲区的单根柱线的函数:
//+------------------------------------------------------------------+ //| Calculating a single bar of all active buffers | //+------------------------------------------------------------------+ void CalculateSeries(const int index,const datetime time) { //--- In the loop by the total number of buffers, get the next buffer int total=ArraySize(Buffers); for(int i=0;i<total;i++) { //--- if the buffer is not used (the symbol button is released), move on to the next one if(!Buffers[i].IsUsed()) { SetBufferData(i,index,NULL); continue; } //--- get the timeseries object by the buffer timeframe CSeriesDE *series=engine.SeriesGetSeries(Buffers[i].Symbol(),(ENUM_TIMEFRAMES)Buffers[i].Timeframe()); // Here we should use the timeframe from the pressed button next to the pressed symbol button //--- if the timeseries is not received //--- or the bar index passed to the function is beyond the total number of bars in the timeseries, move on to the next buffer if(series==NULL || index>series.GetList().Total()-1) continue; //--- get the bar object from the timeseries corresponding to the one passed to the bar time function on the current chart CBar *bar=engine.SeriesGetBarSeriesFirstFromSeriesSecond(NULL,PERIOD_CURRENT,time,Buffers[i].Symbol(),Buffers[i].Timeframe()); if(bar==NULL) continue; //--- get the specified property from the obtained bar and //--- call the function of writing the value to the buffer by i index SetBufferData(i,index,bar); } } //+------------------------------------------------------------------+
将所传递的柱线对象的值写入指定绘制缓冲区的函数:
//+------------------------------------------------------------------+ //| Write data on a single bar to the specified buffer | //+------------------------------------------------------------------+ void SetBufferData(const int buffer_index,const int index,const CBar *bar) { //--- Get the bar index by its time falling within the time limits on the current chart int n=(bar!=NULL ? iBarShift(NULL,PERIOD_CURRENT,bar.Time()) : index); //--- If the passed index on the current chart (index) is less than the calculated time of bar start on another timeframe if(index<n) //--- in the loop from the n bar on the current chart to zero while(n>WRONG_VALUE && !IsStopped()) { //--- fill in the buffer by the n index with the passed values of the bar passed to the function (0 - EMPTY_VALUE) //--- and decrease the n value Buffers[buffer_index].Open[n]=(bar.Open()>0 ? bar.Open() : EMPTY_VALUE); Buffers[buffer_index].High[n]=(bar.High()>0 ? bar.High() : EMPTY_VALUE); Buffers[buffer_index].Low[n]=(bar.Low()>0 ? bar.Low() : EMPTY_VALUE); Buffers[buffer_index].Close[n]=(bar.Close()>0 ? bar.Close() : EMPTY_VALUE); Buffers[buffer_index].Color[n]=(bar.TypeBody()==BAR_BODY_TYPE_BULLISH ? 0 : bar.TypeBody()==BAR_BODY_TYPE_BEARISH ? 1 : 2); n--; } //--- If the passed index on the current chart (index) is not less than the calculated time of bar start on another timeframe //--- Set 'value' for the buffer by the 'index' passed to the function (0 - EMPTY_VALUE) else { //--- If the bar object is passed to the function, fill in the indicator buffers with its data if(bar!=NULL) { Buffers[buffer_index].Open[index]=(bar.Open()>0 ? bar.Open() : EMPTY_VALUE); Buffers[buffer_index].High[index]=(bar.High()>0 ? bar.High() : EMPTY_VALUE); Buffers[buffer_index].Low[index]=(bar.Low()>0 ? bar.Low() : EMPTY_VALUE); Buffers[buffer_index].Close[index]=(bar.Close()>0 ? bar.Close() : EMPTY_VALUE); Buffers[buffer_index].Color[index]=(bar.TypeBody()==BAR_BODY_TYPE_BULLISH ? 0 : bar.TypeBody()==BAR_BODY_TYPE_BEARISH ? 1 : 2); } //--- If NULL, instead of the bar object, is passed to the function, fill in the indicator buffers with the empty value else { Buffers[buffer_index].Open[index]=Buffers[buffer_index].High[index]=Buffers[buffer_index].Low[index]=Buffers[buffer_index].Close[index]=EMPTY_VALUE; Buffers[buffer_index].Color[index]=2; } } } //+------------------------------------------------------------------+
现在,我们来研究指标的 OnInit() 应对程序,在其中创建按钮,并准备好所有指标缓冲区:
//+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Initialize DoEasy library OnInitDoEasy(); //--- Set indicator global variables prefix=engine.Name()+"_"; //--- Get the index of the maximum used timeframe in the array, //--- calculate the number of bars of the current period fitting in the maximum used period //--- Use the obtained value if it exceeds 2, otherwise use 2 int index=ArrayMaximum(ArrayUsedTimeframes); int num_bars=NumberBarsInTimeframe(ArrayUsedTimeframes[index]); min_bars=(index>WRONG_VALUE ? (num_bars>2 ? num_bars : 2) : 2); //--- Check and remove remaining indicator graphical objects if(IsPresentObectByPrefix(prefix)) ObjectsDeleteAll(0,prefix); //--- Create the button panel if(!CreateButtons(InpButtShiftX,InpButtShiftY)) return INIT_FAILED; //--- Check playing a standard sound using macro substitutions engine.PlaySoundByDescription(SND_OK); //--- Wait for 600 milliseconds engine.Pause(600); engine.PlaySoundByDescription(SND_NEWS); //--- indicator buffers mapping //--- In the loop by the total number of all symbols int total_symbols=ArraySize(array_used_symbols); for(int i=0;i<SYMBOLS_TOTAL;i++) { //--- get the next symbol //--- if the loop index is less than the size of used symbols array, the symbol name is taken from the array, //--- otherwise, this is an empty (unused) buffer, and the buffer symbol name is "EMPTY "+loop index string symbol=(i<total_symbols ? array_used_symbols[i] : "EMPTY "+string(i+1)); //--- Increase the size of the buffer structures array, set the buffer symbol, ArrayResize(Buffers,ArraySize(Buffers)+1,SYMBOLS_TOTAL); Buffers[i].SetSymbol(symbol); //--- set the values of all indices for binding the indicator buffers with the structure arrays and //--- specify the next buffer index int index_first=(i==0 ? i : Buffers[i-1].IndexNextBuffer()); Buffers[i].SetIndexes(index_first); //--- Setting the drawn buffer according to the button status //--- Set the symbol button status. The first button is active in the tester bool state_symbol=(engine.IsTester() && i==0 ? true : false); //--- Set the name of the symbol button corresponding to the buffer with the loop index and its timeframe string name_butt_symbol=prefix+"BUTT_"+Buffers[i].Symbol(); string name_butt_period=name_butt_symbol+"_PERIOD_"+TimeframeDescription(Buffers[i].Timeframe()); //--- If not in the tester, while the chart features the button with the specified name, if(!engine.IsTester() && ObjectFind(ChartID(),name_butt_symbol)==0) { //--- set the name of the terminal global variable for storing the button status string name_gv_symbol=(string)ChartID()+"_"+name_butt_symbol; string name_gv_period=(string)ChartID()+"_"+name_butt_period; //--- get the symbol button status from the terminal global variable state_symbol=GlobalVariableGet(name_gv_symbol); } //--- Get the timeframe from pressed symbol timeframe buttons string pressed_period=GetNamePressedTimeframe(name_butt_symbol,symbol); //--- Convert button name into its string ID string button=StringSubstr(name_butt_symbol,StringLen(prefix)); ENUM_TIMEFRAMES timeframe= ( StringFind(button,"_PERIOD_")==WRONG_VALUE ? TimeframeByDescription(StringSubstr(pressed_period,StringFind(pressed_period,"_PERIOD_")+8)) : TimeframeByDescription(StringSubstr(button,StringFind(button,"_PERIOD_")+8)) ); //--- Set the values for all structure fields Buffers[i].SetTimeframe(timeframe); Buffers[i].SetUsed(state_symbol); Buffers[i].SetShowDataFlag(state_symbol); //--- Bind drawn indicator buffers by the buffer index equal to the loop index with the structure bar price arrays (OHLC) SetIndexBuffer(Buffers[i].IndexOpenBuffer(),Buffers[i].Open,INDICATOR_DATA); SetIndexBuffer(Buffers[i].IndexHighBuffer(),Buffers[i].High,INDICATOR_DATA); SetIndexBuffer(Buffers[i].IndexLowBuffer(),Buffers[i].Low,INDICATOR_DATA); SetIndexBuffer(Buffers[i].IndexCloseBuffer(),Buffers[i].Close,INDICATOR_DATA); //--- Bind the color buffer by the buffer index equal to the loop index with the corresponding structure arrays SetIndexBuffer(Buffers[i].IndexColorBuffer(),Buffers[i].Color,INDICATOR_COLOR_INDEX); //--- set the "empty value" for all structure buffers, PlotIndexSetDouble(Buffers[i].IndexOpenBuffer(),PLOT_EMPTY_VALUE,EMPTY_VALUE); PlotIndexSetDouble(Buffers[i].IndexHighBuffer(),PLOT_EMPTY_VALUE,EMPTY_VALUE); PlotIndexSetDouble(Buffers[i].IndexLowBuffer(),PLOT_EMPTY_VALUE,EMPTY_VALUE); PlotIndexSetDouble(Buffers[i].IndexCloseBuffer(),PLOT_EMPTY_VALUE,EMPTY_VALUE); PlotIndexSetDouble(Buffers[i].IndexColorBuffer(),PLOT_EMPTY_VALUE,0); //--- set the drawing type PlotIndexSetInteger(i,PLOT_DRAW_TYPE,DRAW_COLOR_CANDLES); //--- Depending on the button status, set the graphical series name //--- and specify whether the buffer data in the data window is displayed or not SetPlotBufferState(i,state_symbol); //--- set the direction of indexing of all structure buffers as in the timeseries ArraySetAsSeries(Buffers[i].Open,true); ArraySetAsSeries(Buffers[i].High,true); ArraySetAsSeries(Buffers[i].Low,true); ArraySetAsSeries(Buffers[i].Close,true); ArraySetAsSeries(Buffers[i].Color,true); //--- Print data on the next buffer in the journal //Buffers[i].Print(); } //--- Bind the calculated indicator buffer by the FirstFreePlotBufferIndex() buffer index with the BufferTime[] array of the indicator //--- set the direction of indexing the BufferTime[] calculated buffer as in the timeseries int buffer_temp_index=FirstFreePlotBufferIndex(); SetIndexBuffer(buffer_temp_index,BufferTime,INDICATOR_CALCULATIONS); ArraySetAsSeries(BufferTime,true); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
我们来查看一下指标的 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); //--- Check for the minimum number of bars for calculation if(rates_total<min_bars || Point()==0) return 0; //--- Handle the Calculate event in the library //--- If the OnCalculate() method of the library returns zero, not all timeseries are ready - leave till the next tick if(engine.0) return 0; //--- 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: | //+------------------------------------------------------------------+ //--- Set OnCalculate arrays as timeseries ArraySetAsSeries(open,true); ArraySetAsSeries(high,true); ArraySetAsSeries(low,true); ArraySetAsSeries(close,true); ArraySetAsSeries(time,true); ArraySetAsSeries(tick_volume,true); ArraySetAsSeries(volume,true); ArraySetAsSeries(spread,true); //--- Check and calculate the number of calculated bars //--- If limit = 0, there are no new bars - calculate the current one //--- If limit = 1, a new bar has appeared - calculate the first and the current ones //--- If limit > 1, there are changes in history - the full recalculation of all data int limit=rates_total-prev_calculated; //--- Recalculate the entire history if(limit>1) { limit=rates_total-1; InitBuffersAll(); } //--- Prepare data //--- Calculate the indicator for(int i=limit; i>WRONG_VALUE && !IsStopped(); i--) { BufferTime[i]=(double)time[i]; CalculateSeries(i,time[i]); } //--- return value of prev_calculated for next call return(rates_total); } //+------------------------------------------------------------------+
我尝试在所有提供的函数清单里加上注释,讲解我们在每个函数中所做的一切。
我希望它们很容易理解。 如果您有任何疑问,请随时在下面的评论中提问。
指标从其先前版本继承了其余功能。 它们绝大部分基本没变。
完整的指标代码可在下面的文件中查看。
编译指标,并在 EURUSD M15 图表上启动它:
我们可以看到含有前四个品种的四个按钮。 选择周期的按钮也会随之显示,直到按下任何一个按钮。 按下某个品种按钮后,便会立即打开周期选择按钮的列表。 选择周期后,所选品种和周期的蜡烛将显示在图表上。 现在,所选按钮的状态将写入终端全局变量。 重新启动指标,或按下另一个品种按钮,然后返回到之前的指标之后,将为其显示周期按钮,并附带先前已选用的周期按钮。
我们已验证了构造指标缓冲区时在结构里存储指标缓冲区的概念。 然而,从指标上操控它们仍不是很方便。 因此,从下一篇文章开始,我将开发指标缓冲区的类,从而令指标开发更加容易和方便。
在过去的两篇文章中,我们熟悉了运用函数库时间序列来轻松开发多品种多周期指标的方法。
从下一篇文章开始,我将开发指标缓冲区类。
以下附件是函数库当前版本的所有文件,以及测试 EA 文件,供您测试和下载。
将您的问题、评论和建议留在评论中。
请记住,此处我已经为 MetaTrader 5 开发了 MQL5 测试指标。
附件仅适用于 MetaTrader 5。 当前函数库版本尚未在 MetaTrader 4 里进行测试。
在MT4 里不支持当前的缓冲区绘图类型(DRAW_COLOR_CANDLES)。 不过,在创建指标缓冲区类时,我将尝试在 MetaTrader 4 中实现一些 MQL5 功能。
返回内容目录
该系列中的先前文章:
DoEasy 函数库中的时间序列(第三十五部分):柱线对象和品种时间序列列表
DoEasy 函数库中的时间序列(第三十六部分):所有用到的品种周期的时间序列对象
DoEasy 函数库中的时间序列(第三十七部分):时间序列集合 - 按品种和周期的时间序列数据库
DoEasy 函数库中的时间序列(第三十八部分):时间序列集合 - 实时更新以及从程序访问数据
DoEasy 函数库中的时间序列(第三十九部分):基于函数库的指标 - 准备数据和时间序列事件
DoEasy 函数库中的时间序列(第四十部分):基于函数库的指标 - 实时刷新数据
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...
移动端课程