本文将探讨如何开发多交易品种波动指标。对于 MQL5 开发新手来说,开发多交易品种指标可能有些难度,本文将帮他们理清开发过程。开发多交易品种指标的主要问题:其它交易品种的数据与当前交易品种之间的同步;缺乏某些指标数据;以及确定既定时间表“真实”柱的起点。本文将密切关注以上所有问题。
我们将获得根据句柄已计算出的每个交易品种的平均真实波幅(ATR)指标的值。为达到说明目的,我们将列举六个交易品种,可在指标的外部参数中设置这些交易品种的名称。将检查输入的名称是否正确。如果总表中没有提供参数中指定的某个交易品种,则不会对该交易品种做出计算。所有可用交易品种都会被添加到市场报价窗口,除非这些交易品种已经存在于该窗口中。
在上一篇文章“MQL5 细则手册:指标子窗口控件 - 滚动条”中,我们已经探讨了画布功能,你可以在画布上打印文本甚至绘图。这次,我们不是用画布绘图,而是用它来显示当前的程序进程,让用户了解既定时间点的进度情况。
让我们开始程序的开发。用 MQL5 Wizard 创建一个自定义指标模板。经过几次修改后,你会获得以下源代码:
//+------------------------------------------------------------------+ //| MultiSymbolATR.mq5 | //| Copyright 2010, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ //--- Indicator properties #property copyright "Copyright 2010, MetaQuotes Software Corp." #property link "http://www.mql5.com" #property version "1.00" #property indicator_separate_window // Indicator is in a separate subwindow #property indicator_minimum 0 // Minimum value of the indicator #property indicator_buffers 6 // Number of buffers for indicator calculation #property indicator_plots 6 // Number of plotting series //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Initialization completed successfully return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Deinitialization | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, // Size of input time series const int prev_calculated, // Bars processed at the previous call const datetime &time[], // Opening time const double &open[], // Open prices const double &high[], // High prices const double &low[], // Low prices const double &close[], // Close prices const long &tick_volume[], // Tick volumes const long &volume[], // Real volumes const int &spread[]) // Spread { //--- Return the size of the data array of the current symbol return(rates_total); } //+------------------------------------------------------------------+ //| Timer function | //+------------------------------------------------------------------+ void OnTimer() { } //+------------------------------------------------------------------+
为了实现我们的想法,我们要在模板中填入所有必要信息。本文后面将解释为何需要计时器。我们在开始处添加一个常数,就添加到特定的指标属性后面:
//--- Constants #define RESET 0 // Returning the indicator recalculation command to the terminal #define LEVELS_COUNT 6 // Number of levels #define SYMBOLS_COUNT 6 // Number of symbols
常数LEVELS_COUNT包含用“水平线”图形对象表示的层数的数值 (OBJ_HLINE)。可在指标的外部参数中指定这些水平面的数值。
我们在项目中加入一个文件,这个文件包含了使用自定义图形的类:
//--- Include the class for working with the canvas #include <Canvas\Canvas.mqh>
在外部参数中,我们会指定iATR平均周期,应显示哪些交易品种的波幅以及水平数值。将这些交易品种从 2 开始编号,因为图表中附有指标的交易品种被视为第一个交易品种。
//--- External parameters input int IndicatorPeriod=14; // Averaging period sinput string dlm01=""; //- - - - - - - - - - - - - - - - - - - - - - - - - - - input string Symbol02 ="GBPUSD"; // Symbol 2 input string Symbol03 ="AUDUSD"; // Symbol 3 input string Symbol04 ="NZDUSD"; // Symbol 4 input string Symbol05 ="USDCAD"; // Symbol 5 input string Symbol06 ="USDCHF"; // Symbol 6 sinput string dlm02=""; //- - - - - - - - - - - - - - - - - - - - - - - - - - - input int Level01 =10; // Level 1 input int Level02 =50; // Level 2 input int Level03 =100; // Level 3 input int Level04 =200; // Level 4 input int Level05 =400; // Level 5 input int Level06 =600; // Level 6
在这个代码中,我们要进一步创建将在后面用到的所有全局变量和数组。我们在下面的代码中提供了这里所说的所有全局变量和数组并配有详细注释:
//--- Global variables and arrays CCanvas canvas; // Loading the class //--- Variables/arrays for copying data from OnCalculate() int OC_rates_total =0; // Size of input time series int OC_prev_calculated =0; // Bars processed at the previous call datetime OC_time[]; // Opening time double OC_open[]; // Open prices double OC_high[]; // High prices double OC_low[]; // Low prices double OC_close[]; // Close prices long OC_tick_volume[]; // Tick volumes long OC_volume[]; // Real volumes int OC_spread[]; // Spread //--- Structure of buffers for drawing indicator values struct buffers {double data[];}; buffers atr_buffers[SYMBOLS_COUNT]; //--- Structure of time arrays for data preparation struct temp_time {datetime time[];}; temp_time tmp_symbol_time[SYMBOLS_COUNT]; //--- Structure of arrays of the ATR indicator values for data preparation struct temp_atr {double value[];}; temp_atr tmp_atr_values[SYMBOLS_COUNT]; //--- For the purpose of storing and checking the time of the first bar in the terminal datetime series_first_date[SYMBOLS_COUNT]; datetime series_first_date_last[SYMBOLS_COUNT]; //--- Time of the bar from which we will start drawing datetime limit_time[SYMBOLS_COUNT]; //--- Indicator levels int indicator_levels[LEVELS_COUNT]; //--- Symbol names string symbol_names[SYMBOLS_COUNT]; //--- Symbol handles int symbol_handles[SYMBOLS_COUNT]; //--- Colors of indicator lines color line_colors[SYMBOLS_COUNT]={clrRed,clrDodgerBlue,clrLimeGreen,clrGold,clrAqua,clrMagenta}; //--- String representing the lack of the symbol string empty_symbol="EMPTY"; //--- Indicator subwindow properties int subwindow_number =WRONG_VALUE; // Subwindow number int chart_width =0; // Chart width int subwindow_height =0; // Subwindow height int last_chart_width =0; // Last saved chart width int last_subwindow_height =0; // Last saved subwindow height int subwindow_center_x =0; // Horizontal center of the subwindow int subwindow_center_y =0; // Vertical center of the subwindow string subwindow_shortname ="MS_ATR"; // Short name of the indicator string prefix =subwindow_shortname+"_"; // Prefix for objects //--- Canvas properties string canvas_name =prefix+"canvas"; // Canvas name color canvas_background =clrBlack; // Canvas background color uchar canvas_opacity =190; // Opacity int font_size =16; // Font size string font_name ="Calibri"; // Font ENUM_COLOR_FORMAT clr_format =COLOR_FORMAT_ARGB_RAW; // Color components should be correctly set by the user //--- Canvas messages string msg_invalid_handle ="Invalid indicator handle! Please wait..."; string msg_prepare_data ="Preparing data! Please wait..."; string msg_not_synchronized ="Unsynchronized data! Please wait..."; string msg_load_data =""; string msg_sync_update =""; string msg_last =""; //--- Maximum number of bars specified in the terminal settings int terminal_max_bars=0;
在图表中加载指标时,OnInit() 函数会执行以下动作:
如果分别用单独的函数执行这些动作,则操作起来会更加容易。结果, OnInit() 函数的源代码将非常易于理解,如下所示:
//+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Check input parameters for correctness if(!CheckInputParameters()) return(INIT_PARAMETERS_INCORRECT); //--- Set the timer at 1-second intervals EventSetTimer(1); //--- Set the font to be displayed on the canvas canvas.FontSet(font_name,font_size,FW_NORMAL); //--- Initialization of arrays InitArrays(); //--- Initialize the array of symbols InitSymbolNames(); //--- Initialize the array of levels InitLevels(); //--- Get indicator handles GetIndicatorHandles(); //--- Set indicator properties SetIndicatorProperties(); //--- Get the number of bars specified in the terminal settings terminal_max_bars=TerminalInfoInteger(TERMINAL_MAXBARS); //--- Clear the comment Comment(""); //--- Refresh the chart ChartRedraw(); //--- Initialization completed successfully return(INIT_SUCCEEDED); }
下面我们进一步了解上述代码中使用的自定义函数。在 CheckInputParameters()函数中,我们要检查外部参数是否正确。在这个例子中,我们只需要检查一个参数,即 ATR 指标周期。我们将限值设为 500。也就是说,如果你设置的周期数值高于限值,指标会停止运行,并将程序终止运行的原因打印到日志和图标注释中。以下是 CheckInputParameters() 函数代码。
//+------------------------------------------------------------------+ //| Checking input parameters for correctness | //+------------------------------------------------------------------+ bool CheckInputParameters() { if(IndicatorPeriod>500) { Comment("Decrease the indicator period! Indicator Period: ",IndicatorPeriod,"; Limit: 500;"); printf("Decrease the indicator period! Indicator Period: %d; Limit: %d;",IndicatorPeriod,500); return(false); } //--- return(true); }
然后我们来看三个数组初始化函数:InitArrays(), InitSymbolNames() 和 InitLevels()。下面分别提供了它们的源代码:
//+------------------------------------------------------------------+ //| First initialization of arrays | //+------------------------------------------------------------------+ void InitArrays() { ArrayInitialize(limit_time,NULL); ArrayInitialize(series_first_date,NULL); ArrayInitialize(series_first_date_last,NULL); ArrayInitialize(symbol_handles,INVALID_HANDLE); //--- for(int s=0; s<SYMBOLS_COUNT; s++) ArrayInitialize(atr_buffers[s].data,EMPTY_VALUE); } //+------------------------------------------------------------------+ //| Initializing array of symbols | //+------------------------------------------------------------------+ void InitSymbolNames() { symbol_names[0]=AddSymbolToMarketWatch(_Symbol); symbol_names[1]=AddSymbolToMarketWatch(Symbol02); symbol_names[2]=AddSymbolToMarketWatch(Symbol03); symbol_names[3]=AddSymbolToMarketWatch(Symbol04); symbol_names[4]=AddSymbolToMarketWatch(Symbol05); symbol_names[5]=AddSymbolToMarketWatch(Symbol06); } //+------------------------------------------------------------------+ //| Initializing array of levels | //+------------------------------------------------------------------+ void InitLevels() { indicator_levels[0]=Level01; indicator_levels[1]=Level02; indicator_levels[2]=Level03; indicator_levels[3]=Level04; indicator_levels[4]=Level05; indicator_levels[5]=Level06; }
在 InitSymbolNames() 函数中,我们使用了另一个自定义函数 - AddSymbolToMarketWatch()。它接收交易品种名称,如果这个交易品种位于总表中,则会将这个交易品种添加到市场报价窗口,且该函数会返回带有交易品种名称的字符串。如果这个交易品种不可用,则该函数会返回“EMPTY”(空)字符串,且在其它函数运行检查时不会再在交易品种数组中对这一元素进行任何操作。
//+------------------------------------------------------------------+ //| Adding the specified symbol to the Market Watch window | //+------------------------------------------------------------------+ string AddSymbolToMarketWatch(string symbol) { int total=0; // Number of symbols string name=""; // Symbol name //--- If an empty string is passed, return the empty string if(symbol=="") return(empty_symbol); //--- Total symbols on the server total=SymbolsTotal(false); //--- Iterate over the entire list of symbols for(int i=0;i<total;i++) { //--- Symbol name on the server name=SymbolName(i,false); //--- If this symbol is available, if(name==symbol) { //--- add it to the Market Watch window and SymbolSelect(name,true); //--- return its name return(name); } } //--- If this symbol is not available, return the string representing the lack of the symbol return(empty_symbol); }
GetIndicatorHandles() 是在指标初始化时调用的另一函数。它尝试为每个指定交易品种获取 ATR 指标句柄。如果没能为某个交易品种获得句柄,该函数会返回 false,但在 OnInit() 中不会对其进行任何处理,因为它会在程序的其它部分检查句柄的可用性。
//+------------------------------------------------------------------+ //| Getting indicator handles | //+------------------------------------------------------------------+ bool GetIndicatorHandles() { //--- An indication of all handles being valid bool valid_handles=true; //--- Iterate over all symbols in a loop and ... for(int s=0; s<SYMBOLS_COUNT; s++) { //--- If the symbol is available if(symbol_names[s]!=empty_symbol) { // And if the handle of the current symbol is invalid if(symbol_handles[s]==INVALID_HANDLE) { //--- Get it symbol_handles[s]=iATR(symbol_names[s],Period(),IndicatorPeriod); //--- If the handle could not be obtained, try again next time if(symbol_handles[s]==INVALID_HANDLE) valid_handles=false; } } } //--- Print the relevant message if the handle for one of the symbols could not be obtained if(!valid_handles) { msg_last=msg_invalid_handle; ShowCanvasMessage(msg_invalid_handle); } //--- return(valid_handles); }
稍后会讨论 ShowCanvasMessage() 函数以及使用画布所需的其它函数。
SetIndicatorProperties() 函数设置了指标属性。由于各个标图序列的属性相似,因此用循环来设置更加方便:
//+------------------------------------------------------------------+ //| Setting indicator properties | //+------------------------------------------------------------------+ void SetIndicatorProperties() { //--- Set the short name IndicatorSetString(INDICATOR_SHORTNAME,subwindow_shortname); //--- Set the number of decimal places IndicatorSetInteger(INDICATOR_DIGITS,_Digits); //--- Define buffers for drawing for(int s=0; s<SYMBOLS_COUNT; s++) SetIndexBuffer(s,atr_buffers[s].data,INDICATOR_DATA); //--- Set labels for the current symbol for(int s=0; s<SYMBOLS_COUNT; s++) PlotIndexSetString(s,PLOT_LABEL,"ATR ("+IntegerToString(s)+", "+symbol_names[s]+")"); //--- Set the plotting type: lines for(int s=0; s<SYMBOLS_COUNT; s++) PlotIndexSetInteger(s,PLOT_DRAW_TYPE,DRAW_LINE); //--- Set the line width for(int s=0; s<SYMBOLS_COUNT; s++) PlotIndexSetInteger(s,PLOT_LINE_WIDTH,1); //--- Set the line color for(int s=0; s<SYMBOLS_COUNT; s++) PlotIndexSetInteger(s,PLOT_LINE_COLOR,line_colors[s]); //--- Empty value for plotting where nothing will be drawn for(int s=0; s<SYMBOLS_COUNT; s++) PlotIndexSetDouble(s,PLOT_EMPTY_VALUE,EMPTY_VALUE); }
成功对程序进行初始化后,我们需要首次调用 OnCalculate() 函数。首次调用函数时,prev_calculated 变量的数值为零。加载更更多历史记录或填补历史空白时,终端也会将该变量归零。在这种情况下,会完全重新计算指标缓冲区。如果参数值不为零,即之前由同一函数返回的结果也就是输入时间序列的大小不为零,则只需更新缓冲区的最后几个数值即可。
第一次尝试时,你也许无法做到准确完成所有计算。这时,我们可以用包含零值的 RESET(重置)常数来返回。下一次调用 OnCalculate() 时(如在下次价格变动时),prev_calculated 参数会包含零值,也就是说,在图表中显示指标的标图序列前,我们需要多次尝试以完成所有必要计算。
然而当市场关闭后图表仍然为空,且不存在新的价格变动或后续的不成功计算。这时,你可以尝试以一种简单的方式发出命令再次做出尝试,那就是手动更改图表的时间表。但我们会用一种不同的方式。这就解释了我们为什么要在一开始的时候便将计时器,OnTimer() 函数添加到程序模板中,并在OnInit() 函数中将时间间隔设为 1 秒。
计时器每秒便会检查一次 OnCalculate() 函数是否已归零。为达到这一目的,我们会编写一个 CopyDataOnCalculate() 函数,它会将 OnCalculate() 的所有参数复制到使用相应名称的全局变量以及前缀为 OC_的数组中。
//+------------------------------------------------------------------+ //| Copying data from OnCalculate | //+------------------------------------------------------------------+ void CopyDataOnCalculate(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[]) { OC_rates_total=rates_total; OC_prev_calculated=prev_calculated; ArrayCopy(OC_time,time); ArrayCopy(OC_open,open); ArrayCopy(OC_high,high); ArrayCopy(OC_low,low); ArrayCopy(OC_close,close); ArrayCopy(OC_tick_volume,tick_volume); ArrayCopy(OC_volume,volume); ArrayCopy(OC_spread,spread); }
应在 OnCalculate() 函数体的最开始调用这个函数。另外,我们还应该在最开始添加另一个自定义函数,ResizeCalculatedArrays(),这个函数可以在将用于数据准备的数组放入指标缓冲区之前设置数组的大小。这些数组的大小应与输入时间序列的大小相等。
//+------------------------------------------------------------------+ //| Resizing the size of arrays to the size of the main array | //+------------------------------------------------------------------+ void ResizeCalculatedArrays() { for(int s=0; s<SYMBOLS_COUNT; s++) { ArrayResize(tmp_symbol_time[s].time,OC_rates_total); ArrayResize(tmp_atr_values[s].value,OC_rates_total); } }
另外,我们会创建一个 ZeroCalculatedArrays() 函数,在将用于数据准备的数组输出到图表之前将数组初始化为零。
//+------------------------------------------------------------------+ //| Zeroing out arrays for data preparation | //+------------------------------------------------------------------+ void ZeroCalculatedArrays() { for(int s=0; s<SYMBOLS_COUNT; s++) { ArrayInitialize(tmp_symbol_time[s].time,NULL); ArrayInitialize(tmp_atr_values[s].value,EMPTY_VALUE); } }
将指标缓冲区初步归零时,也需要这个函数。我们称其为 ZeroIndicatorBuffers()。
//+------------------------------------------------------------------+ //| Zeroing out indicator buffers | //+------------------------------------------------------------------+ void ZeroIndicatorBuffers() { for(int s=0; s<SYMBOLS_COUNT; s++) ArrayInitialize(atr_buffers[s].data,EMPTY_VALUE); }
OnCalculate() 函数的当前代码显示如下。我还对后面需要填写的一些主要运算给出了注释(以下注释和圆点)。
//+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, // Size of input time series const int prev_calculated, // Bars processed at the previous call const datetime &time[], // Opening time const double &open[], // Open prices const double &high[], // High prices const double &low[], // Low prices const double &close[], // Close prices const long &tick_volume[], // Tick volumes const long &volume[], // Real volumes const int &spread[]) // Spread { //--- For the purpose of determining the bar from which the calculation shall be made int limit=0; //--- Make a copy of the OnCalculate() parameters CopyDataOnCalculate(rates_total,prev_calculated, time,open,high,low,close, tick_volume,volume,spread); //--- Set the size to arrays for data preparation ResizeCalculatedArrays(); //--- If this is the first calculation or if a deeper history has been loaded or gaps in the history have been filled if(prev_calculated==0) { //--- Zero out arrays for data preparation ZeroCalculatedArrays(); //--- Zero out indicator buffers ZeroIndicatorBuffers(); //--- Other checks // ... //--- If you reached this point, it means that OnCalculate() will return non-zero value and this needs to be saved OC_prev_calculated=rates_total; } //--- If only the last values need to be recalculated else limit=prev_calculated-1; //--- Prepare data for drawing // ... //--- Fill arrays with data for drawing // ... //--- Return the size of the data array of the current symbol return(rates_total); }
现在,OnTimer() 函数代码如下所示:
//+------------------------------------------------------------------+ //| Timer function | //+------------------------------------------------------------------+ void OnTimer() { //--- If for some reason calculations have not been completed or // a deeper history has been loaded or // gaps in the history have been filled, // then make another attempt without waiting for the new tick if(OC_prev_calculated==0) { OnCalculate(OC_rates_total,OC_prev_calculated, OC_time,OC_open,OC_high,OC_low,OC_close, OC_tick_volume,OC_volume,OC_spread); } }
现在,我们来看当 prev_calculated 变量等于零时还会使用哪些函数。这些函数会:
另外,我们还将确认每个交易品种的首个“真实”柱。我们创造这个简洁的术语是为了方便今后使用。它的意思是,MetaTrader 5 中的所有时间表都是用分钟数据建立的。但是,若服务器能提供自1993 年以来的每日数据,却只能提供 2000 年以来的分钟数据,然后若我们选择小时图时间表,则会自有分钟数据的当日建立柱,也就是从 2000 年起建立柱。2000 年之前的所有东西都会以每日数据或最接近当前时间表数据的形式呈现。因此,为避免混淆,不得显示与当前时间表无关的指标数据。这就是我们为什么要找出当前时间表的第一个“真实”柱,并用与交易品种指标缓冲区颜色相同的垂直线标记这个“真实”柱。
在开发“EA 交易”时,找出“真实”柱同样非常重要,因为如果在某个时间表内对参数进行了优化,则来自其它时间表的数据也将不再适用。
执行上述检查前,我们会在指标子窗口中添加画布。因此,我们首先要编写管理画布需要的所有函数。将画布添加到子窗口前,我们需要确定画布的大小以及文本消息将显示于画布的坐标。为此,我们来编写 GetSubwindowGeometry() 函数:
//+------------------------------------------------------------------+ //| Getting geometry of the indicator subwindow | //+------------------------------------------------------------------+ void GetSubwindowGeometry() { //--- Get the indicator subwindow number subwindow_number=ChartWindowFind(0,subwindow_shortname); //--- Get the subwindow width and height chart_width=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS); subwindow_height=(int)ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS,subwindow_number); //--- Calculate the center of the subwindow subwindow_center_x=chart_width/2; subwindow_center_y=subwindow_height/2; }
获得子窗口属性后,你可以添加画布。画布背景为 100% 透明(不透明度为 0),仅当加载并生成数据从而让用户了解当前进度时才会显露画布。可见时,背景的不透明度为 190。你可以在 0 到 255 之间任意设置不透明度值。更多信息,请参考位于 Help(帮助)选项下的 ColorToARGB() 函数说明。
下面我们编写一个 SetCanvas() 函数来设置画布:
//+------------------------------------------------------------------+ //| Setting canvas | //+------------------------------------------------------------------+ void SetCanvas() { //--- If there is no canvas, set it if(ObjectFind(0,canvas_name)<0) { //--- Create the canvas canvas.CreateBitmapLabel(0,subwindow_number,canvas_name,0,0,chart_width,subwindow_height,clr_format); //--- Make the canvas completely transparent canvas.Erase(ColorToARGB(canvas_background,0)); //--- Redraw the canvas canvas.Update(); } }
我们还需要一个函数,用来检查是否重新调整了指标子窗口的大小。如果调整了指标子窗口大小,则会自动调整画布大小以适应新的子窗口大小。下面我们来调用 OnSubwindowChange()函数:
//+------------------------------------------------------------------+ //| Checking the subwindow size | //+------------------------------------------------------------------+ void OnSubwindowChange() { //--- Get subwindow properties GetSubwindowGeometry(); //--- If the subwindow size has not changed, exit if(!SubwindowSizeChanged()) return; //--- If the subwindow height is less than one pixel or if the center has been calculated incorrectly, exit if(subwindow_height<1 || subwindow_center_y<1) return; //--- Set the new canvas size ResizeCanvas(); //--- Show the last message ShowCanvasMessage(msg_last); }
我们会在下面探讨上述代码中强调的几个函数。请注意在重新调整子窗口大小前运行的检查类型。如果任何属性发生错误,则该函数会停止运算。
SubwindowSizeChanged() 函数代码如下:
//+------------------------------------------------------------------+ //| Checking if the subwindow has been resized | //+------------------------------------------------------------------+ bool SubwindowSizeChanged() { //--- If the subwindow size has not changed, exit if(last_chart_width==chart_width && last_subwindow_height==subwindow_height) return(false); //--- If the size has changed, save it else { last_chart_width=chart_width; last_subwindow_height=subwindow_height; } //--- return(true); }
ResizeCanvas() 函数代码如下:
//+------------------------------------------------------------------+ //| Resizing canvas | //+------------------------------------------------------------------+ void ResizeCanvas() { //--- If the canvas has already been added to the indicator subwindow, set the new size if(ObjectFind(0,canvas_name)==subwindow_number) canvas.Resize(chart_width,subwindow_height); }
最后,以下是 ShowCanvasMessage() 函数代码,我们在之前获取指标句柄时使用过这个代码:
//+------------------------------------------------------------------+ //| Displaying message on the canvas | //+------------------------------------------------------------------+ void ShowCanvasMessage(string message_text) { GetSubwindowGeometry(); //--- If the canvas has already been added to the indicator subwindow if(ObjectFind(0,canvas_name)==subwindow_number) { //--- If the string passed is not empty and correct coordinates have been obtained, display the message if(message_text!="" && subwindow_center_x>0 && subwindow_center_y>0) { canvas.Erase(ColorToARGB(canvas_background,canvas_opacity)); canvas.TextOut(subwindow_center_x,subwindow_center_y,message_text,ColorToARGB(clrRed),TA_CENTER|TA_VCENTER); canvas.Update(); } } }
可用消失效果删除画布。删除画布前,必须在循环中将不透明度从当前值逐步减小到零,同时每个迭代刷新一次画布。
DeleteCanvas() 函数代码如下:
//+------------------------------------------------------------------+ //| Deleting canvas | //+------------------------------------------------------------------+ void DeleteCanvas() { //--- Delete the canvas if it exists if(ObjectFind(0,canvas_name)>0) { //--- Before deleting, implement the disappearing effect for(int i=canvas_opacity; i>0; i-=5) { canvas.Erase(ColorToARGB(canvas_background,(uchar)i)); canvas.Update(); } //--- Delete the graphical resource canvas.Destroy(); } }
接下来,在将检查数据完备所需函数放入指标缓冲区并在图表中显示这些函数前,检查该等函数。我们从 LoadAndFormData() 函数开始。我们用它来比较当前交易品种数组与用于其它交易品种的数据的大小。如有必要,从服务器加载数据。我们提供了函数代码和详细注释供你参考。
//+------------------------------------------------------------------+ //| Loading and generating the necessary/available amount of data | //+------------------------------------------------------------------+ void LoadAndFormData() { int bars_count=100; // Number of loaded bars //--- for(int s=0; s<SYMBOLS_COUNT; s++) { int attempts =0; // Counter of data copying attempts int array_size =0; // Array size datetime firstdate_server =NULL; // Time of the first bar on the server datetime firstdate_terminal=NULL; // Time of the first bar in the terminal base //--- Get the first date by the symbol/time frame in the terminal base SeriesInfoInteger(symbol_names[s],Period(),SERIES_FIRSTDATE,firstdate_terminal); //--- Get the first date of the symbol/time frame on the server SeriesInfoInteger(symbol_names[s],Period(),SERIES_SERVER_FIRSTDATE,firstdate_server); //--- Print the message msg_last=msg_load_data="Loading and generating data: "+ symbol_names[s]+"("+(string)(s+1)+"/"+(string)SYMBOLS_COUNT+") ... "; ShowCanvasMessage(msg_load_data); //--- Load/generate data. // If the array size is smaller than the maximum number of bars in the terminal, and if // the number of bars between the first date of the series in the terminal and the first date of the series on the server is more than specified while(array_size<OC_rates_total && firstdate_terminal-firstdate_server>PeriodSeconds()*bars_count) { datetime copied_time[]; //--- Get the first date by the symbol/time frame in the terminal base SeriesInfoInteger(symbol_names[s],Period(),SERIES_FIRSTDATE,firstdate_terminal); //--- Load/copy the specified number of bars if(CopyTime(symbol_names[s],Period(),0,array_size+bars_count,copied_time)!=-1) { //--- If the time of the first bar in the array, excluding the number of the bars being loaded, is earlier // than the time of the first bar in the chart, terminate the loop if(copied_time[0]-PeriodSeconds()*bars_count<OC_time[0]) break; //--- If the array size hasn't increased, increase the counter if(ArraySize(copied_time)==array_size) attempts++; //--- Otherwise get the current size of the array else array_size=ArraySize(copied_time); //--- If the array size hasn't increased over 100 attempts, terminate the loop if(attempts==100) { attempts=0; break; } } //--- Check the subwindow size once every 2000 bars // and if the size has changed, adjust the canvas size to it if(!(array_size%2000)) OnSubwindowChange(); } } }
尝试加载需要的数据量后,我们再一次检查指标句柄。为此,我们使用上述 GetIndicatorHandles() 函数。
句柄检查完毕后,程序会用 CheckAvailableData() 函数检查特定交易品种数据的可用性以及每个交易品种的指标数值。下面请仔细观察整个检查过程:
//+------------------------------------------------------------------+ //| Checking the amount of available data for all symbols | //+------------------------------------------------------------------+ bool CheckAvailableData() { for(int s=0; s<SYMBOLS_COUNT; s++) { //--- If this symbol is available if(symbol_names[s]!=empty_symbol) { double data[]; // Array for checking the amount of indicator data datetime time[]; // Array for checking the number of bars int calculated_values =0; // Amount of indicator data int available_bars =0; // Number of bars of the current period datetime firstdate_terminal=NULL; // First date of the current time frame data available in the terminal //--- Get the number of calculated values of the indicator calculated_values=BarsCalculated(symbol_handles[s]); //--- Get the first date of the current time frame data in the terminal firstdate_terminal=(datetime)SeriesInfoInteger(symbol_names[s],Period(),SERIES_TERMINAL_FIRSTDATE); //--- Get the number of available bars from the date specified available_bars=Bars(symbol_names[s],Period(),firstdate_terminal,TimeCurrent()); //--- Check the readiness of bar data: 5 attempts to get values for(int i=0; i<5; i++) { //--- Copy the specified amount of data if(CopyTime(symbol_names[s],Period(),0,available_bars,time)!=-1) { //--- If the required amount has been copied, terminate the loop if(ArraySize(time)>=available_bars) break; } } //--- Check the readiness of indicator data: 5 attempts to get values for(int i=0; i<5; i++) { //--- Copy the specified amount of data if(CopyBuffer(symbol_handles[s],0,0,calculated_values,data)!=-1) { //--- If the required amount has been copied, terminate the loop if(ArraySize(data)>=calculated_values) break; } } //--- If the amount of data copied is not sufficient, one more attempt is required if(ArraySize(time)<available_bars || ArraySize(data)<calculated_values) { msg_last=msg_prepare_data; ShowCanvasMessage(msg_prepare_data); OC_prev_calculated=0; return(false); } } } //--- return(true); }
在为所有交易品种备齐数据前,CheckAvailableData() 函数不会允许进行下一步计算。所有检查函数都以类似的方式运行。
要在加载更多历史报价的过程中实施监控,我们需要下一个函数。我们称其为 CheckEventLoadHistory()。如果加载了更多数据,则应完全重新计算该指标。该函数的源代码如下:
//+------------------------------------------------------------------+ //| Checking the event of loading a deeper history | //+------------------------------------------------------------------+ bool CheckLoadedHistory() { bool loaded=false; //--- for(int s=0; s<SYMBOLS_COUNT; s++) { //--- If this symbol is available if(symbol_names[s]!=empty_symbol) { //--- If the series need to be updated if(OC_prev_calculated==0) { //--- Get the first date by the symbol/time frame series_first_date[s]=(datetime)SeriesInfoInteger(symbol_names[s],Period(),SERIES_FIRSTDATE); //--- If this is the first time (no value is available), then if(series_first_date_last[s]==NULL) //--- Store the first date by the symbol/time frame for further comparison // in order to determine if a deeper history has been loaded series_first_date_last[s]=series_first_date[s]; } else { //--- Get the first date by the symbol/time frame series_first_date[s]=(datetime)SeriesInfoInteger(symbol_names[s],Period(),SERIES_FIRSTDATE); //--- If the dates are different, i.e. the date in the memory is later than the one we have just obtained, // this means that a deeper history has been loaded if(series_first_date_last[s]>series_first_date[s]) { //--- Print the relevant message to the log Print("(",symbol_names[s],",",TimeframeToString(Period()), ") > A deeper history has been loaded/generated: ", series_first_date_last[s]," > ",series_first_date[s]); //--- Store the date series_first_date_last[s]=series_first_date[s]; loaded=true; } } } } //--- If a deeper history has been loaded/generated, then // send the command to refresh the plotting series of the indicator if(loaded) return(false); //--- return(true); }
下面我们再编写一个函数来检查终端与服务器间的数据同步。仅当与服务器建立连接后才能运行此项检查。以下是 CheckSymbolIsSynchronized() 函数代码:
//+------------------------------------------------------------------+ //| Checking synchronization by symbol/time frame | //+------------------------------------------------------------------+ bool CheckSymbolIsSynchronized() { //--- If the connection to the server is established, check the data synchronization if(TerminalInfoInteger(TERMINAL_CONNECTED)) { for(int s=0; s<SYMBOLS_COUNT; s++) { //--- If the symbol is available if(symbol_names[s]!=empty_symbol) { //--- If the data are not synchronized, print the relevant message and try again if(!SeriesInfoInteger(symbol_names[s],Period(),SERIES_SYNCHRONIZED)) { msg_last=msg_not_synchronized; ShowCanvasMessage(msg_not_synchronized); return(false); } } } } //--- return(true); }
“MQL5 细则手册”系列的前几篇文章提供了用来将时间表转换到字符串的函数:
//+------------------------------------------------------------------+ //| Converting time frame to a string | //+------------------------------------------------------------------+ string TimeframeToString(ENUM_TIMEFRAMES timeframe) { string str=""; //--- If the value passed is incorrect, take the current chart time frame if(timeframe==WRONG_VALUE || timeframe== NULL) timeframe= Period(); switch(timeframe) { case PERIOD_M1 : str="M1"; break; case PERIOD_M2 : str="M2"; break; case PERIOD_M3 : str="M3"; break; case PERIOD_M4 : str="M4"; break; case PERIOD_M5 : str="M5"; break; case PERIOD_M6 : str="M6"; break; case PERIOD_M10 : str="M10"; break; case PERIOD_M12 : str="M12"; break; case PERIOD_M15 : str="M15"; break; case PERIOD_M20 : str="M20"; break; case PERIOD_M30 : str="M30"; break; case PERIOD_H1 : str="H1"; break; case PERIOD_H2 : str="H2"; break; case PERIOD_H3 : str="H3"; break; case PERIOD_H4 : str="H4"; break; case PERIOD_H6 : str="H6"; break; case PERIOD_H8 : str="H8"; break; case PERIOD_H12 : str="H12"; break; case PERIOD_D1 : str="D1"; break; case PERIOD_W1 : str="W1"; break; case PERIOD_MN1 : str="MN1"; break; } //--- return(str); }
最后,我们必须用垂直线在图表中做出标记来找出并保存每个交易品种的首个真实柱。为此,我们来编写一个 DetermineFirstTrueBar() 函数以及一个 GetFirstTrueBarTime() 辅助函数,这个辅助函数会返回首个真实柱的时间。
//+-----------------------------------------------------------------------+ //| Determining the time of the first true bar for the purpose of drawing | //+-----------------------------------------------------------------------+ bool DetermineFirstTrueBar() { for(int s=0; s<SYMBOLS_COUNT; s++) { datetime time[]; // Bar time array int available_bars=0; // Number of bars //--- If this symbol is not available, move to the next one if(symbol_names[s]==empty_symbol) continue; //--- Get the total number of bars for the symbol available_bars=Bars(symbol_names[s],Period()); //--- Copy the bar time array. If this action failed, try again. if(CopyTime(symbol_names[s],Period(),0,available_bars,time)<available_bars) return(false); //--- Get the time of the first true bar corresponding to the current time frame limit_time[s]=GetFirstTrueBarTime(time); //--- Place a vertical line on the true bar CreateVerticalLine(0,0,limit_time[s],prefix+symbol_names[s]+": begin time series", 2,STYLE_SOLID,line_colors[s],false,TimeToString(limit_time[s]),"\n"); } //--- return(true); } //+-----------------------------------------------------------------------+ //| Returning the time of the first true bar of the current time frame | //+-----------------------------------------------------------------------+ datetime GetFirstTrueBarTime(datetime &time[]) { datetime true_period =NULL; // Time of the first true bar int array_size =0; // Array size //--- Get the array size array_size=ArraySize(time); ArraySetAsSeries(time,false); //--- Check each bar one by one for(int i=1; i<array_size; i++) { //--- If the bar corresponds to the current time frame if(time[i]-time[i-1]==PeriodSeconds()) { //--- Save it and terminate the loop true_period=time[i]; break; } } //--- Return the time of the first true bar return(true_period); }
通过 CreateVerticalLine() 函数,可用垂直线在图表中标记首个真实柱的时间:
//+------------------------------------------------------------------+ //| Creating a vertical line at the specified time point | //+------------------------------------------------------------------+ void CreateVerticalLine(long chart_id, // chart id int window_number, // window number datetime time, // time string object_name, // object name int line_width, // line width ENUM_LINE_STYLE line_style, // line style color line_color, // line color bool selectable, // cannot select the object if FALSE string description_text, // text of the description string tooltip) // no tooltip if "\n" { //--- If the object has been created successfully if(ObjectCreate(chart_id,object_name,OBJ_VLINE,window_number,time,0)) { //--- set its properties ObjectSetInteger(chart_id,object_name,OBJPROP_TIME,time); ObjectSetInteger(chart_id,object_name,OBJPROP_SELECTABLE,selectable); ObjectSetInteger(chart_id,object_name,OBJPROP_STYLE,line_style); ObjectSetInteger(chart_id,object_name,OBJPROP_WIDTH,line_width); ObjectSetInteger(chart_id,object_name,OBJPROP_COLOR,line_color); ObjectSetString(chart_id,object_name,OBJPROP_TEXT,description_text); ObjectSetString(chart_id,object_name,OBJPROP_TOOLTIP,tooltip); } }
我们已经准备好各个检查函数。结果,OnCalculate() 函数代码在 prev_calculated 变量等于零时显示的部分现在应如下所示:
//--- If this is the first calculation or if a deeper history has been loaded or gaps in the history have been filled if(prev_calculated==0) { //--- Zero out arrays for data preparation ZeroCalculatedArrays(); //--- Zero out indicator buffers ZeroIndicatorBuffers(); //--- Get subwindow properties GetSubwindowGeometry(); //--- Add the canvas SetCanvas(); //--- Load and generate the necessary/available amount of data LoadAndFormData(); //--- If there is an invalid handle, try to get it again if(!GetIndicatorHandles()) return(RESET); //--- Check the amount of data available for all symbols if(!CheckAvailableData()) return(RESET); //--- Check if a deeper history has been loaded if(!CheckLoadedHistory()) return(RESET); //--- Check synchronization by symbol/time frame at the current moment if(!CheckSymbolIsSynchronized()) return(RESET); //--- For each symbol, determine the bar from which we should start drawing if(!DetermineFirstTrueBar()) return(RESET); //--- If you reached this point, it means that OnCalculate() will return non-zero value and this needs to be saved OC_prev_calculated=rates_total; }
现在,每次检查失败后,程序都会返回,在下一价格变动或计时器事件时重试。在计时器中加载更多历史记录时,我们还要运行 OnCalculate() 函数之外的检查:
//+------------------------------------------------------------------+ //| Timer function | //+------------------------------------------------------------------+ void OnTimer() { //--- If a deeper history has been loaded if(!CheckLoadedHistory()) OC_prev_calculated=0; //--- If for some reason calculations have not been completed or // a deeper history has been loaded or // gaps in the history have been filled, // then make another attempt without waiting for the new tick if(OC_prev_calculated==0) { OnCalculate(OC_rates_total,OC_prev_calculated, OC_time,OC_open,OC_high,OC_low,OC_close, OC_tick_volume,OC_volume,OC_spread); } }
现在,我们只需编写放入 OnCalculate() 函数的两个主循环:
第一个循环的代码如下:
//--- Prepare data for drawing for(int s=0; s<SYMBOLS_COUNT; s++) { //--- If the symbol is available if(symbol_names[s]!=empty_symbol) { double percent=0.0; // For the purpose of calculating the progress percentage msg_last=msg_sync_update="Preparing data ("+IntegerToString(rates_total)+" bars) : "+ symbol_names[s]+"("+(string)(s+1)+"/"+(string)(SYMBOLS_COUNT)+") - 00% ... "; //--- Print the message ShowCanvasMessage(msg_sync_update); //--- Control every value of the array for(int i=limit; i<rates_total; i++) { PrepareData(i,s,time); //--- Refresh the message once every 1000 bars if(i%1000==0) { //--- Progress percentage ProgressPercentage(i,s,percent); //--- Print the message ShowCanvasMessage(msg_sync_update); } //--- Check the subwindow size once every 2000 bars // and if the size has changed, adjust the canvas size to it if(i%2000==0) OnSubwindowChange(); } } }
以上代码强调了用来复制和保存数值的 PrepareData() 主函数。还有一个尚未被考虑的新函数 - ProgressPercentage()。它会计算当前运算的进度百分比,从而告诉我们还需多长时间完成运算。
PrepareData() 函数代码如下:
//+------------------------------------------------------------------+ //| Preparing data before drawing | //+------------------------------------------------------------------+ void PrepareData(int bar_index,int symbol_number,datetime const &time[]) { int attempts=100; // Number of copying attempts //--- Time of the bar of the specified symbol and time frame datetime symbol_time[]; //--- Array for copying indicator values double atr_values[]; //--- If within the area of the current time frame bars if(time[bar_index]>=limit_time[symbol_number]) { //--- Copy the time for(int i=0; i<attempts; i++) { if(CopyTime(symbol_names[symbol_number],0,time[bar_index],1,symbol_time)==1) { tmp_symbol_time[symbol_number].time[bar_index]=symbol_time[0]; break; } } //--- Copy the indicator value for(int i=0; i<attempts; i++) { if(CopyBuffer(symbol_handles[symbol_number],0,time[bar_index],1,atr_values)==1) { tmp_atr_values[symbol_number].value[bar_index]=atr_values[0]; break; } } } //--- If outside the area of the current time frame bars, set an empty value else tmp_atr_values[symbol_number].value[bar_index]=EMPTY_VALUE; }
ProgressPercentage() 函数代码如下:
//+------------------------------------------------------------------+ //| Calculating progress percentage | //+------------------------------------------------------------------+ void ProgressPercentage(int bar_index,int symbol_number,double &percent) { string message_text=""; percent=(double(bar_index)/OC_rates_total)*100; //--- if(percent<=9.99) message_text="0"+DoubleToString(percent,0); else if(percent<99) message_text=DoubleToString(percent,0); else message_text="100"; //--- msg_last=msg_sync_update="Preparing data ("+(string)OC_rates_total+" bars) : "+ symbol_names[symbol_number]+ "("+(string)(symbol_number+1)+"/"+(string)SYMBOLS_COUNT+") - "+message_text+"% ... "; }
在 OnCalculate() 函数的第二个主循环中填充指标缓冲区:
//--- Fill indicator buffers for(int s=0; s<SYMBOLS_COUNT; s++) { //--- If the specified symbol does not exist, zero out the buffer if(symbol_names[s]==empty_symbol) ArrayInitialize(atr_buffers[s].data,EMPTY_VALUE); else { //--- Generate a message msg_last=msg_sync_update="Updating indicator data: "+ symbol_names[s]+"("+(string)(s+1)+"/"+(string)SYMBOLS_COUNT+") ... "; //--- Print the message ShowCanvasMessage(msg_sync_update); //--- Fill indicator buffers with values for(int i=limit; i<rates_total; i++) { FillIndicatorBuffers(i,s,time); //--- Check the subwindow size once every 2000 bars // and if the size has changed, adjust the canvas size to it if(i%2000==0) OnSubwindowChange(); } } }
上述代码中强调的字符串包括 FillIndicatorBuffers() 函数。在图表中显示指标的标图序列前执行最终运算的地方:
//+------------------------------------------------------------------+ //| Filling indicator buffers | //+------------------------------------------------------------------+ void FillIndicatorBuffers(int bar_index,int symbol_number,datetime const &time[]) { //--- For the purpose of checking the obtained indicator value bool check_value=false; //--- Counter of the current time frame bars static int bars_count=0; //--- Zero out the counter of the current time frame bars at the beginning of the symbol's time series if(bar_index==0) bars_count=0; //--- If within the area of current time frame bars and the counter is smaller // than the specified indicator period, increase the counter if(bars_count<IndicatorPeriod && time[bar_index]>=limit_time[symbol_number]) bars_count++; //--- If within the indicator area and the time of the current symbol coincides with the time of the specified symbol if(bars_count>=IndicatorPeriod && time[bar_index]==tmp_symbol_time[symbol_number].time[bar_index]) { //--- If the value obtained is not empty if(tmp_atr_values[symbol_number].value[bar_index]!=EMPTY_VALUE) { check_value=true; atr_buffers[symbol_number].data[bar_index]=tmp_atr_values[symbol_number].value[bar_index]; } } //--- Set an empty value in case of failure to set a higher value if(!check_value) atr_buffers[symbol_number].data[bar_index]=EMPTY_VALUE; }
在 OnCalculate() 函数的结尾,我们必须删除画布,设置水平,将信息变量归零,并刷新图表。最后,会返回 rates_total 数组大小,之后每次产生价格变动或出现计时器事件时只需在 OnCalculate()中重新计算最后一个数值。
以下是要插入第二个主循环和该函数的返回值之间的代码的字符串:
//--- Delete the canvas DeleteCanvas(); //--- Set indicator levels SetIndicatorLevels(); //--- Zero out variables msg_last=""; msg_sync_update=""; //--- Refresh the chart ChartRedraw();
用于设置水平位置的 SetIndicatorLevels() 函数的代码如下:
//+------------------------------------------------------------------------+ //| Setting indicator levels | //+------------------------------------------------------------------------+ void SetIndicatorLevels() { //--- Get the indicator subwindow number subwindow_number=ChartWindowFind(0,subwindow_shortname); //--- Set levels for(int i=0; i<LEVELS_COUNT; i++) CreateHorizontalLine(0,subwindow_number, prefix+"level_0"+(string)(i+1)+"", CorrectValueBySymbolDigits(indicator_levels[i]*_Point), 1,STYLE_DOT,clrLightSteelBlue,false,false,false,"\n"); } //+------------------------------------------------------------------------+ //| Adjusting the value based on the number of digits in the price (double)| //+------------------------------------------------------------------------+ double CorrectValueBySymbolDigits(double value) { return(_Digits==3 || _Digits==5) ? value*=10 : value; }
CreateHorizontalLine() 函数用于设置水平位置以及特定属性,其代码如下:
//+------------------------------------------------------------------+ //| Creating a horizontal line at the price level specified | //+------------------------------------------------------------------+ void CreateHorizontalLine(long chart_id, // chart id int window_number, // window number string object_name, // object name double price, // price level int line_width, // line width ENUM_LINE_STYLE line_style, // line style color line_color, // line color bool selectable, // cannot select the object if FALSE bool selected, // line is selected bool back, // background position string tooltip) // no tooltip if "\n" { //--- If the object has been created successfully if(ObjectCreate(chart_id,object_name,OBJ_HLINE,window_number,0,price)) { //--- set its properties ObjectSetInteger(chart_id,object_name,OBJPROP_SELECTABLE,selectable); ObjectSetInteger(chart_id,object_name,OBJPROP_SELECTED,selected); ObjectSetInteger(chart_id,object_name,OBJPROP_BACK,back); ObjectSetInteger(chart_id,object_name,OBJPROP_STYLE,line_style); ObjectSetInteger(chart_id,object_name,OBJPROP_WIDTH,line_width); ObjectSetInteger(chart_id,object_name,OBJPROP_COLOR,line_color); ObjectSetString(chart_id,object_name,OBJPROP_TOOLTIP,tooltip); } }
用于删除图形对象的函数:
//+------------------------------------------------------------------+ //| Deleting levels | //+------------------------------------------------------------------+ void DeleteLevels() { for(int i=0; i<LEVELS_COUNT; i++) DeleteObjectByName(prefix+"level_0"+(string)(i+1)+""); } //+------------------------------------------------------------------+ //| Deleting vertical lines of the beginning of the series | //+------------------------------------------------------------------+ void DeleteVerticalLines() { for(int s=0; s<SYMBOLS_COUNT; s++) DeleteObjectByName(prefix+symbol_names[s]+": begin time series"); } //+------------------------------------------------------------------+ //| Deleting the object by name | //+------------------------------------------------------------------+ void DeleteObjectByName(string object_name) { //--- If such object exists if(ObjectFind(0,object_name)>=0) { //--- If an error occurred when deleting, print the relevant message if(!ObjectDelete(0,object_name)) Print("Error ("+IntegerToString(GetLastError())+") when deleting the object!"); } }
应将以下代码添加到 OnDeinit() 函数:
现在一切准备就绪,可进行全面测试。可在终端设置中用 Charts(图表) 选项卡设置可在一个窗口中最多显示几个柱。指标的运行速度取决于窗口中的柱数。
图1 在终端设置中设置最多可显示几个柱
将指标加载到图表中时,你可以看到所有交易品种的数据准备进度:
图2 数据准备期间画布上显示的信息
以下屏幕截图显示的是 20 分钟时间表上的指标:
图3 20 分钟时间表上的多交易品种 ATR 指标
图中用垂直线标记出“真实”柱的起始位置。以下屏幕截图显示,NZDUSD(新西兰元兑美元) (黄线)的真实柱从 2000 年开始(MetaQuotes-Demo 服务器),而其它货币对真实柱从 1999 年年初开始,因此只显示了一行(所有真实柱都位于同一日期)。我们还注意到,1999 年之前的周期分隔符之间的间距更小,如果分析各柱的时间,你会看到这些是每日柱。
图4 垂直线标记了每个交易品种真实柱的起点
本文到此结束。附上文中提到的源代码,并提供下载。在以后的文章中,我们会尝试实现一个交易系统,用它来分析波幅并查看结果如何。
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...