
探索新功能:MQL5 的自定义指标
简介
我们总算获得机会尝试这一全新的交易终端 - MetaTrader 5。毫无疑问,新版本值得关注,并且相比之前的版本具备很多新的功能。现将此平台众多优点中较为重要的几点列示如下:
- 从根本改进的语言使我们利用结构化编程的众多优点的同时,现在还可以使用面向对象编程。
- 相比 MetaTrader 4,代码执行的速度要快捷得多。
- 显示必要信息的发展潜力获得本质提高。
在这里,我无法一一列举新终端和新编程语言的所有新的可能性和功能。这数不胜数,而且部分创新点也需要以单独的章节来进行详细探讨。同样,本章节也不会出现以面向对象编程语言写就的代码。这部分内容十分重要,不能简单地附于其他内容之后作为补充优点呈现给开发人员。
在本文中,我们将会探讨指标,以及相比 MQL4 指标的结构、图形、类型和编程细节。
本文浅显易懂,并且里面提到的内容可直接使用随附的文件在终端中加以检验。
希望无论是初学者还是经验丰富的开发人员均能从本文中获益,也许部分读者还能在此有自己新的发现。
总体结构
相较 MQL4,新版本中指标的总体结构并无不同。
和之前一样,指标具有三大函数,分别用于初始化、数据处理以及指标的去初始化。
许多指标参数可由属性定义(#property关键字),这和之前是一样的。大部分属性专门用于指标。属性及输入参数和之前一样,在全局上下文中定义。
例如,让我们考虑 RSI 指标自定义着色的实施。此处给出的仅仅是删节版,完整的代码可见文件 Color_RSI.mq5。
让我们来看一看给出的这部分代码。
//--- 数据属性部分 #property copyright "TheXpert" #property link "theforexpert@gmail.com" #property version "1.00" //--- 指标的描述一共不能超过511个字符 //--- 包括换行符在内 #property description " " #property description "根据彩色相对强弱指数指标(RSI)实例" #property description "的指标创建演示。"
以上代码指定的属性在指标信息面板中显示(属性的 "Common"(通用)选项卡)。如下图所示:
//--- 指标属性 #property indicator_separate_window // 指标在单独子窗口中显示 #property indicator_buffers 2 // 使用缓冲区的数量 #property indicator_plots 1 // 显示缓冲区的数量 //--- 绘图1 #property indicator_color1 clrDarkSalmon, clrDeepSkyBlue // 使用两种颜色 #property indicator_type1 DRAW_COLOR_LINE // 显示样式
这些属性是指标属性。其他属性的说明可在“帮助”中找到。
//---- 缓冲区 double Values[]; // 数值缓冲区 double ValuesPainting[]; // 颜色索引缓冲区 //--- 指标输入参数 input string _1 = "RSI parameters"; input int RSIPeriod = 5; input int SmoothPeriod = 5; input ENUM_APPLIED_PRICE AppliedPrice = PRICE_CLOSE; input string _2 = "Color settings"; input color Down = clrDarkSalmon; input color Up = clrDeepSkyBlue; //---存放指标句柄的变量 int RSIHandle;
这些是指标输入参数和全局变量(请勿与客户终端的全局变量相混淆)。指标输入参数通过input标识符指定。
现在为输入参数设置枚举变得可能,有时这对避免不正确的参数选择十分有用。
例如,AppliedPrice 参数将在带有可能有效值的下拉列表中显示。
因此,所有的枚举,包括用户定义的在内,将显示于同一下拉列表中。例如,以下参数
//... enum DayOfWeek { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }; input DayOfWeek Day; //...
将会如下显示:
int OnInit() { //--- 绑定缓冲区 //--- 用于做显示缓冲区的数值 SetIndexBuffer(0,Values,INDICATOR_DATA); //--- 于做保存颜色缓冲区的数值 SetIndexBuffer(1,ValuesPainting,INDICATOR_COLOR_INDEX); //--- 设置绘图缓冲区起点 PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,RSIPeriod); //--- 设置指标名称 IndicatorSetString(INDICATOR_SHORTNAME,"Color RSI("+string(RSIPeriod)+")"); //--- 设置绘图的空值 PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,EMPTY_VALUE); //--- 设置绘图颜色 PlotIndexSetInteger(0,PLOT_LINE_COLOR,0,Down); PlotIndexSetInteger(0,PLOT_LINE_COLOR,1,Up); //--- 获取指标句柄 RSIHandle=iRSI(NULL,0,RSIPeriod,AppliedPrice); //--- 设置缓冲区索引方向 ArraySetAsSeries(Values,true); ArraySetAsSeries(ValuesPainting,true); //--- 成功执行 return(0); }
OnInit 是指标初始化函数。在此,我们配置了指标缓冲区及其属性,并定义了无法在属性中定义或必须动态设置的指标变量。同样,还有原始数据初始化,包括指标所需的句柄分配。
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[]) { //--- 用于计算的柱数 int toCount=(int)MathMin(rates_total,rates_total-prev_calculated+1); //--- 尝试复制iRSI指标数据 if(CopyBuffer(RSIHandle,0,0,toCount,Values)==-1) { Print("数据复制错误, 错误码为",GetLastError()); //--- 返回重新计算指标数据 return(0); } //--- 计算颜色部分 for(int i=toCount-2;i>=0;--i) { //--- 计算第一条线的颜色 if(Values[i+1]!=EMPTY_VALUE && Values[i]>Values[i+1]) ValuesPainting[i]=1; else ValuesPainting[i]=0; } //--- 返回值为下一次调用的 prev_calculated 值 return(rates_total); }
OnCalculate 是数据计算函数。此函数可以有两种形式。此处是函数的标准形式。详细内容如下。
函数:
//--- 此函数不是必需的 /* void OnDeinit() { } */
OnDeinit 是指标去初始化函数。通常,该函数是释放资源所必需的,例如,文件句柄。在其他情况下,此函数不是必需的。
指标的两个概念
其一是标准,与我们过去在 MQL4 中使用的并无二致,不过在形式上稍有改变。函数 OnCalculate 用于取代函数 Start。
其标准形式如下所示:
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[]) { return rates_total; }
为了减少用于数据复制的代码量,图表数据作为数组直接传递给函数的参数。此外,可用柱的数量作为函数的第一个参数传递,最后一次调用后处理的柱的数量或 0(零)作为第二个参数传递。
0(零)值可以在第一次指标调用以及加载新的或缺失数据时传递。此参数是 IndicatorCounted() 的替代(替换项或等效项 - 这取决于您),后者对于许多开发人员而言都不太方便。
第二个概念是 i<…>OnArray 的替代和扩展 - 与 MQL4 的函数类似。终端示例中有一个此类型的指标 - Custom Moving Average(自定义移动平均线 )。此类型的指标,包括自定义指标,用于基于用户选择的数据处理。
这一类型指标的数据处理函数如下所示:
int OnCalculate (const int rates_total, // price[] 数组的大小 const int prev_calculated, // 上一次调用计算的柱数 const int begin, // 数据计算起点 const double& price[] // 用于计算的数据数组 ) { return rates_total; }
函数的最后一个参数为用户选择用于处理的数据。若要应用包含大量缓冲区的指标,第一个指标缓冲区将用于数据处理。
First Indicator's Data(首个指标数据)表示指标将会应用至首个附于选定图表窗口的指标。
Previous Indicator's Data(上一指标数据)表示指标将会应用至最后附于选定图表窗口的指标。
这些指标可用于构成整个堆栈。例如,使用Custom Moving Average(自定义移动平均线)指标,通过将第一个指标指派给必要数据、第二个指派给第一个且第三个指派给第二个,从而有可能获得三倍的平滑度:
有许多标准指标均实施此特定概念。因此,在看到函数参数 applied_price_or_handle 的提示时:
它表示该指标以其可在用户数据上计算的方式实施 - 这些数据的句柄必须作为参数 applied_price_or_handle 传递。
使用同样的方式,直接在指标代码中组织数据处理是可能的:
{ //... RSIHandle = iRSI(NULL, 0, RSIPeriod, AppliedPrice); SmoothHandle = iMA(NULL, 0, SmoothPeriod, 0, MODE_EMA, RSIHandle); //... }
此概念的另一新应用是,编写通用服务指标的能力。此类指标的示例请见文件 Direction_Brush.mq5。
结果在上面的图表中给出。此例中的方向着色作为独立的实体分离,并在另一指标中实施。
诚然,由于它们仅对指标的零缓冲区可用,因而具有有限的通用性。然而,我认为此类指标可能会非常有用。
从另一个角度来说,您写下一个自定义指标时,您需要将其考虑在内,因为零缓冲区中的主要信息处理将允许避免在一个指标中的多功能机的实施。许多其他的操作可在外部服务指标中制定和执行。而您所要做的只不过是将服务指标及所需的功能附加至您的自定义中。
它的应用范围并不只是您的表面所见:
- 指标特征着色(顶端、方向、级别、细分段等),包括市场买卖情况可视化;
- 不同条件下的不同信号;
- 收集和显示统计数据 - 例如,数据分布;
- 构建可用于某个缓冲区计算的通用指标 - 例如,Moving Averages(移动平均线)、Zigzag(波浪)。
上面提到的功能并未囊括概念实施的全部应用。我认为在未来还将发现许多其他有效的实施。
访问数据
在 MQL5 中,数据访问原则发生了变化。现在数据访问直接在数组中进行,相应地,计算速度得到明显提高。现在,无需为每个值创建一个数组然后调用 iCustom 函数。取而代之的是,通过调用一个函数获得必要的数据计数,然后直接使用在指定局部数组中复制的所需数据成为可能。
数据复制通过使用系统函数 CopyBuffer 实现。您可以在“帮助”中找到该函数的说明。
指标和静态(具有预定义的大小)数组的最大复制数据计数取决于数组的大小。如果复制的数据超过动态数组的大小,数组大小可相应变化。
此外,用于访问历史数据的特殊函数列示如下:
函数 | 说明 |
---|---|
CopyBuffer | 获取相关指标的指定缓冲区的必要数量的数据。 |
CopyRates | 获取指定交易品种周期内 MqlRates 结构的指定数量的历史数据至 rates_array 数组。 |
CopyTime | 函数获取指定交易品种周期对的柱开盘时间的指定数量的历史数据至 time_array。 |
CopyOpen | 函数获取选定交易品种周期对的柱开盘价格的指定数量的历史数据至 open_array。 |
CopyHigh | 函数获取选定交易品种周期对的柱最高价格的指定数量的历史数据至 high_array。 |
CopyLow | 函数获取选定交易品种周期对的柱最低价格的指定数量的历史数据至 low_array。 |
CopyClose | 函数获取选定交易品种周期对的柱收盘价格的指定数量的历史数据至 close_array。 |
CopyTickVolume | 函数获取选定交易品种周期对的订单号交易量的指定数量的历史数据至 volume_array。 |
CopyRealVolume | 函数获取选定交易品种周期对的交易量的指定数量的历史数据至 volume_array。 |
CopySpread | 函数获取选定交易品种周期对的点差值的指定数量的历史数据至 spread_array。 |
详细内容可在“帮助”中找到。
此数据仅以一种指标形式传递,其他数据有其自己的形式。
由于历史数据数组的类型不必是二维的,建议仅使用动态非指标缓冲区用于存储。
尚有一处未加证明的细节 - 如果复制的数据计数等于 0(零),函数 CopyBuffer 将生成代码为 4003 的错误,因此复制的数据计数不应小于 1(一)个元素。
指标缓冲区
缓冲区计数无限制。
现在您无需考虑如何正确容纳信息、高效执行中间计算以及创建群集指标。
但我们不要忘记,缓冲区存储需要内存才能够实现。因此,若指定约 1,000,000 柱的终端历史数据深度并将该“密集”群集指标附于分钟图表,则终端消耗数以 Gb 计的内存也就不足为奇了。
缓冲区也有了一些本质改变。所使用的缓冲区数量在属性中指定。
#property indicator_buffers 2 // 使用的缓冲区数量
该值应与缓冲区总计数相对应。
显示的缓冲区数量由属性定义:
#property indicator_plots 1 // 显示缓冲区数量
此处附上部分细节。大部分图形样式仅需要一个 INDICATOR_DATA 缓冲区用于制图。然而,有部分样式需要多个指标缓冲区用于制图。
我们指的是以下这些图形样式:
- DRAW_HISTOGRAM2 - 需要两个指标缓冲区 (HistogramSample.mq5)
- DRAW_FILLING - 需要两个指标缓冲区 (CrossMa.mq5)
- DRAW_CANDLES - 需要四个指标缓冲区 (CandleSample.mq5)
- DRAW_BARS - 需要四个指标缓冲区 (BarsSample.mq5)
以上除 DRAW_FILLING 样式(无法着色)之外的所有类型均具有有色模拟。
现在可将所有指标分为 3 类:
-
INDICATOR_DATA - 缓冲区,其数据在图表上显示。这些缓冲区用于制图及配合 iCustom 使用。需要首先对其进行注册。在任意订单(错误)的情况下,代码将会成功汇编并在应用至图表时进行制图,然而很有可能是不正确的。
-
INDICATOR_COLOR_INDEX - 用于存储颜色的缓冲区。这些缓冲区对于存储具有特别颜色类型 (#property indicator_typeN) 之一的 INDICATOR_DATA 型色彩缓冲区索引而言是必需的。此类缓冲区(我们称其为颜色缓冲区)应在使用其的主缓冲区后注册。
-
INDICATOR_CALCULATIONS - 此类缓冲区用于存储辅助计算的结果。它们不在图表上显示。
int OnInit() { // ... SetIndexBuffer(0, V2, INDICATOR_DATA); SetIndexBuffer(1, V2C,INDICATOR_COLOR_INDEX); SetIndexBuffer(2, V4, INDICATOR_DATA); SetIndexBuffer(3, V4C,INDICATOR_COLOR_INDEX); SetIndexBuffer(4, V1, INDICATOR_CALCULATIONS); SetIndexBuffer(5, V3, INDICATOR_CALCULATIONS); // ... return 0; }
通过 iCustom 引用指标时还有如下功能。
- 制图缓冲区(在图表上显示)可用于读取。缓冲区编号(索引)必须和缓冲区所注册的相匹配。
- 用于颜色存储的缓冲区可能可用于读取,但不总是这样。例如,在上述代码中,缓冲区 V2C 可读取并获取必要的值,而缓冲区 V4C 不可用。
- 应于中间计算的缓冲区不可用,这和 MQL4 中是一样的。若想要保证对外部数据缓冲区的访问,将其声明为 INDICATOR_DATA。
若对不可访问的缓冲区发出访问请求,将生成错误 4806 ("The requested data was not found" (找不到请求的数据))。
接下来我们详细讨论颜色缓冲区。
在 MQL4 中,必须为每种颜色创建一个单独的缓冲区,而在新版中使用颜色样式可为一个缓冲区指定多达 63 种颜色。在某些情况下,这可以最优化使用的缓冲区数量,从而节省内存的使用。这也同样为编写指标、尤其是市场买卖情况可视化的使用打开了一扇新的窗口。
此外,对比 MQL4,该创新在某些情形下极大简化了多种颜色应用的逻辑。最明显的例子便是,通过颜色来区分趋势。在 MQL4 中,非常经济的实施情况下(正确情形下)也需要三 (3) 个缓冲区并且无复杂编程。
现在比以往要容易得多。以下为代码示例,完整的实施过程可参见文件 Color_RSI.mq5。
#property indicator_color1 clrDarkSalmon, clrDeepSkyBlue // 使用两种颜色 #property indicator_type1 DRAW_COLOR_LINE // 显示类型 //---- 缓冲区 double Values[]; // 数值缓冲区 double ValuesPainting[]; // 颜色索引缓冲区 //+------------------------------------------------------------------+ //| 自定义指标初始函数 | //+------------------------------------------------------------------+ int OnInit() { //--- 绑定缓冲区 //--- 用于做显示缓冲区的数值 SetIndexBuffer(0,Values,INDICATOR_DATA); //--- 用于做保存颜色缓冲区的数值 SetIndexBuffer(1,ValuesPainting,INDICATOR_COLOR_INDEX); //... return(0); } //+------------------------------------------------------------------+ //| 自定义指标迭代函数 | //+------------------------------------------------------------------+ int OnCalculate(/*...*/) { //--- 用于计算的柱数 int toCount=(int)MathMin(rates_total,rates_total-prev_calculated+1); //--- 尝试复制iRSI指标数据 if(CopyBuffer(RSIHandle,0,0,toCount,Values)==-1) { Print("数据复制错误,错误码",GetLastError()); //--- 返回重新计算指标数据 return(0); } //--- 计算颜色部分 for(int i=toCount-2;i>=0;--i) { //--- 计算第一条线的颜色 if(Values[i+1]!=EMPTY_VALUE && Values[i]>Values[i+1]) ValuesPainting[i]=1; else ValuesPainting[i]=0; } //--- 返回值为下一次调用的 prev_calculated 值 return(rates_total); }
使用更多代码,然后我们获得如下图形:
您应在使用制图的颜色类型时注意到一些细节。
对于颜色缓冲区而言,当使用动态颜色定义方案时,最大颜色计数受 indicator_colorN 属性中定义的颜色计数的限制。示例
#property indicator_color1 clrDarkSalmon, clrDeepSkyBlue // 使用两种颜色
缓冲区的颜色方案将包含最多两种颜色,即使动态设置一个更大的颜色数量也是如此(使用 PlotIndexSetInteger)。
因此,所需的颜色数量应在一行 - 属性定义行 - 中写入。然后颜色数量可以动态改变。我所发现的最短的颜色是“红色”。然而,您可以随时进行以下操作:
替代
#property indicator_color1 clrRed, clrRed, clrRed, clrRed, clrRed, clrRed, clrRed, clrRed, //…
您可按如下所示编写代码:
#define C clrRed #property indicator_color1 C, C, C, C, C, C, C, C, C, C, C, C, C, C, //…
缓冲区的最大颜色数量为 63。当颜色数量大于最大数量时(由属性 indicator_colorN 定义),缓冲区将不会显示。
以下是使用最大颜色数量的市场买卖情况可视化的示例:
总而言之,制图条件获得明显改善,这相当不错。
数组
在通过索引直接引用数组数据时,必须指定数据排序类型 - AsSeries 属性。无法为某些数组类型定义该属性。
无法为多维数组和静态数组设置此标志。对于传递至 OnCalculate 函数的数组类型,可以设置此标志。
通过函数 CopyBuffer 实现的数据复制不依赖于 AsSeries 属性,但其实施因不同的缓冲区而异。
对于指标缓冲区,必须复制可用历史数据的整个深度。这一点需要牢记。
对于动态和静态(具有预定义大小)数组,数据复制从当前向过去执行。
函数 CopyBuffer 更改动态(指标除外)缓冲区的大小至所需大小。
不建议使用静态数组进行数据复制。
一般而言,我建议始终检查复制数据和为数据寻址的方式。最简单和最安全的方式是:
- 为用于存储历史数据的所有缓冲区设置 AsSeries 属性。
- 考虑不同缓冲区的缓冲区结构。
- 总是检查 _LastError 变量的值(最后错误代码)。
此外,强烈建议学习 CopyBuffer 函数以及所有和 AsSeries 属性有关的函数的“帮助”文字。
IndicatorCounted
新版中将不再会有关于 IndicatorCounted 函数必要性的争论,因为我们将该值作为上次函数调用的返回值直接定义。
int OnCalculate(const int rates_total, // 数组大小 const int prev_calculated, // 上一次调用后计算的柱数 //...) { return rates_total; }
在大多数情况下,返回包含当前函数调用的柱计数的 rates_total 值即已足够。
然而,若价格数据自上次 OnCalculate() 调用(例如,加载历史数据或填充历史数据空白处)以来发生变化,则输入参数 prev_calculated
的值将会由客户终端设置为 0(零)。
同样,若 OnCalculate 函数返回零值,则指标值不会在客户终端的 DataWindow 中显示。因此,若想要查看指标和在历史数据加载过程中执行完整的再计算或在连接失败后,返回 1 而不是 0。
新增的另一有用功能是确定为指标计算的柱的数量。这对基于庞大数据执行计算的“EA 交易”而言尤为有用。函数 BarsCalculated 的详细内容可在“帮助”中找到。
此函数还有另一有用应用。如果指标尚未加载,其加载可能需要一些时间 - 指标句柄创建和用于计算的时间。
该时间对于初始化和初次预计算而言是必需的。其取决于计算速度和指标的详细内容。
在此期间,函数 CopyBuffer 的调用生成错误 4806 - "The requested data was not found"(找不到请求的数据)。
函数 BarsCalculated 可用于确定用于复制的指标数据的可用性 :
//--- 计算需要的柱数 int toCount=rates_total-(int)MathMax(prev_calculated-1,0); //--- 尝试复制iWPR指标数据 int copied=CopyBuffer(WPRHandle,0,0,toCount,Values); int err=GetLastError(); //--- 检查复制结果 if(copied==-1) { //--- 如果错误码为4806,说明数据没有被上传 if(err==4806) { //--- 等待数据上传 for(int i=0;i<1000;++i) if(BarsCalculated(WPRHandle)>0) break; //--- 再次尝试复制iWPR指标数据 copied=CopyBuffer(WPRHandle,0,0,rates_total,Values); err=GetLastError(); } } //--- 检查复制结果 if(copied==-1) { Print("读取WPR数值错误,最后错误是 ",err," 柱数 ",rates_total); return(0); } //...
总结
综上所述,我得说本文只是涉及到了部分细节。但我希望基本方面已在此陈述清楚。
如果本文可作为涉及话题的有用参考则再好不过,在此您总是可以查看和找到有关细节的信息。
若文中有任何错误或疏漏,请不吝指出,我会尽快修改和完善本文。
我正在计划列出最新的变更,此外,我希望诸位读者可通过评论不吝赐教。
这正是需仔细阅读该部分的原因。
附录
- Color.mqh - 包含文件,将此文件复制到文件夹 MQL5/Include。此文件对 Toned_WPR 指标是必需的。
- Color.mq5 - 库,将此文件复制到文件夹 MQL5/Libraries。此文件对 Toned_WPR 指标是必需的。
以上文件均为指标。
致谢
再次感谢 Victor Rustamov 先生 (granit77) 阅读原稿,与您的讨论以及您的建议使本文受益良多。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/5
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.




Prival:
应该是这样的吗 还是我做错了什么
你好,谢尔盖。这篇文章是在第一批公开发布的时候写的。从那以后发生了很多变化,也许有些指标需要改进。
一旦有时间,我会尽快修改。
我只定义了一个标记为 INDICATOR_DATA 的缓冲区,并将所有其他缓冲区移至 INDICATOR_CALCULATIONS,这样 CopyBuffer/GetData 就能将正确的复制项返回到目标数组中,从而解决了这个问题。
你好我读了......看了...
在 RSI_Color 中,有额外的几行文字:
既然图表中上升线和下降线的颜色是在输入颜色块中描述的,那么,正如参考书中所说:"在 mql5 程序中不可能用输入修改器更改变量的值,变量是只读的。输入变量的值只能由用户在程序 的属性 窗口中更改"。
我在注释中加入了颜色设置函数 // PlotIndexSetInteger(1,PLOT_LINE_COLOR,0,Down); // PlotIndexSetInteger(1,PLOT_LINE_COLOR,1,Up); 结果确实没有变化
你好我一直在看书。
这篇文章的代码是在 MT5 公开测试版的时候写的,但却能正常工作,这说明 MT5 的指标系统是连贯的、经过深思熟虑的。
但从那时起,许多事情都发生了变化,有些变化甚至是巨大的。如果您了解了一般原则,最好从帮助和近期文章中获取更具体的实际信息。
感谢您的澄清(不过说实话,我并不理解给出的代码是如何打破帮助中的说法的),但我认为这篇文章早已达到了目的,所以不打算做任何修改。