简介
什么是指标?指标是我们希望以便利方式在荧幕上显示的一组计算值。这一组值在程序中以数组表示。因此,创建指标意即编写用于处理数组(价格数组)的算法并将处理结果记录在其他数组(指标值)中。
尽管已有许多已成经典的现成指标,但创建自己的指标的必要性始终存在。我们把使用我们自己的算法创建的这类指标称为自定义指标。本文将探讨如何创建简单的自定义指标。
指标是不同的
指标可以表现为带有颜色的线条或区域,或作为指向输入头寸的有利时刻的特殊标签显示。同时,这些类型还可以相互结合,从而提供了更多的指标类型。我们将采用 William Blau 开发的众所周知的“真实强弱指数”作为指标创建的示例。
真实强弱指数
TSI 指标基于双重平滑动量来确定趋势及超卖/超买区域。指标的数学诠释请见 动量、方向和背离,William Blau。在这里,我们仅涉及计算公式。
TSI(CLOSE,r,s) =100*EMA(EMA(mtm,r),s) / EMA(EMA(|mtm|,r),s)
其中:
- mtm = CLOSEcurrent – CLOSprev,值数组指示当前柱的收盘价格和上一个柱的收盘价格的差值;
- EMA(mtm,r) = 周期长度为 r 的 mtm 值的指数平滑;
- EMA(EMA(mtm,r),s) = 时间周期为 s 的 EMA(mtm,r) 值的指数平滑;
- |mtm| = mtm 绝对值;
- r = 25,
- s = 13。
从上述公式我们可以得知,有三个参数影响指标计算。它们是时间周期 r 和时间周期 s,以及用于计算的价格类型。在前例中,我们使用收盘价。
MQL5 向导
我们将 TSI 以蓝色线条显示 - 在这里,我们需要启动“MQL5 向导”。首先,我们需要指出我们希望创建的程序的类型 - 自定义指标。接来下,我们应设置程序名、r 和 s 参数以及它们的值。
之后,我们需定义指标在单独的窗口中以蓝色线条显示,并为该线条设置 TSI 标签。
在输入所有初始数据后,按 Done(完成)并获得指标的草稿。
//+------------------------------------------------------------------+ //| True Strength Index.mq5 | //| Copyright 2009, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "2009, MetaQuotes Software Corp." #property link "https://www.mql5.com" #property version "1.00" #property indicator_separate_window #property indicator_buffers 1 #property indicator_plots 1 //---- TSI 绘图属性 #property indicator_label1 "TSI" #property indicator_type1 DRAW_LINE #property indicator_color1 Blue #property indicator_style1 STYLE_SOLID #property indicator_width1 1 //--- 输入参数 input int r=25; input int s=13; //--- 指标缓冲区 double TSIBuffer[]; //+------------------------------------------------------------------+ //| 自定义指标初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- 指标缓冲区映射关系 SetIndexBuffer(0,TSIBuffer,INDICATOR_DATA); //--- return(0); } //+------------------------------------------------------------------+ //| 自定义指标迭代函数 | //+------------------------------------------------------------------+ 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[]) { //--- //--- 返回值会作为下一次调用的 prev_calculated 参数调用 return(rates_total); } //+------------------------------------------------------------------+
“MQL5 向导”创建指标头文件,其内规定了指标属性,即:
- 指标在单独窗口中显示;
- 指标缓冲区数量, indicator_buffers=1;
- 绘图数量, indicator_buffers=1;
- 绘图 1 名称,indicator_label1="TSI";
- 首个绘图样式 - 线条,indicator_type1=DRAW_LINE;
- 绘图 1 颜色,indicator_color1=Blue;
- 线条样式,indicator_style1=STYLE_SOLID;
- 绘图 1 线宽,indicator_width1=1。
所有准备工作就绪,现在我们可以着手改进和完善我们的代码。
OnCalculate()
函数 OnCalculate() 是Calculate事件的处理函数,在需要重新计算指标值以及在图表上重新绘制指标时出现。这是新的订单号接收、交易品种历史数据更新等的事件。这就是指标值所有计算的主代码必须位于此函数中的原因。
当然,辅助计算可以通过其他的单独函数实施,但这些函数必须用于 OnCalculate处理函数。
默认情况下,“MQL5 向导”创建 OnCalculate() 的第二种形式,该形式提供对所有时序类型的访问:
- 开盘价、最高价、最低价、收盘价;
- 交易量(实际量和/或跳动量);
- 点差;
- 周期开盘时间。
而对于我们而言,我们只需要一个数据数组,这就是我们要改写调用的 OnCalculate() 函数的第一种形式的原因。
int OnCalculate (const int rates_total, // price[]数组大小; const int prev_calculated, // 上次调用计算后的价格柱的数量 const int begin, // price[]数组开始计算的索引 const double& price[]) // 指标计算的依据数组 { //--- //--- 返回值会作为下一次调用的 prev_calculated 参数调用 return(rates_total); }
这就使我们不仅可以进一步将指标应用于价格数据,同时还可以基于其他指标的值来创建指标。
如果我们在 Parameters(参数)选项卡中选择 Close(收盘)(默认提供),则传递至 OnCalculate() 的 price[] 将包含收盘价。如果我们选择,例如,Typical Price(典型价格),price[] 将在每个时间周期中包含(最高价+最低价+收盘价)/3 的价格。
rates_total 参数指示 price[] 数组的大小;该参数在循环中组织计算时十分有用。price[] 中的元素索引从零开始,方向从过去至未来,即 price[0] 元素包含最旧的值,而 price[rates_total-1] 包含最新的数组值。
组织辅助指标缓冲区
仅有一根线会在图表中显示,即一个指标数组的数据。但在此之前,我们需要组织中间计算。中间数据存储在以 INDICATOR_CALCULATIONS 属性标记的指标数组中。从上述公式可以得知,我们还需要以下附加数组:
- 用于值 mtm - 数组 MTMBuffer[];
- 用于值 |mtm| - 数组 AbsMTMBuffer[];
- 用于 EMA(mtm,r) - 数组 EMA_MTMBuffer[];
- 用于 EMA(EMA(mtm,r),s) - 数组 EMA2_MTMBuffer[];
- 用于 EMA(|mtm|,r) - 数组 EMA_AbsMTMBuffer[];
- 用于 EMA(EMA(|mtm|,r),s) - 数组 EMA2_AbsMTMBuffer[]。
我们总共还需要添加 6 个全局级别的双精度类型数组,并需要将这些数组和指标缓冲区绑定至 OnInit() 函数。切勿忘记标示新的指标缓冲区数量;indicator_buffers 属性必须等于 7(原有的 1 个缓冲区加上添加的 6 个缓冲区)。
#property indicator_buffers 7
现在指标代码如下所示:
#property indicator_separate_window #property indicator_buffers 7 #property indicator_plots 1 //---- TSI 绘图属性 #property indicator_label1 "TSI" #property indicator_type1 DRAW_LINE #property indicator_color1 Blue #property indicator_style1 STYLE_SOLID #property indicator_width1 1 //--- 输入参数 input int r=25; input int s=13; //--- 指标缓冲区 double TSIBuffer[]; double MTMBuffer[]; double AbsMTMBuffer[]; double EMA_MTMBuffer[]; double EMA2_MTMBuffer[]; double EMA_AbsMTMBuffer[]; double EMA2_AbsMTMBuffer[]; //+------------------------------------------------------------------+ //| 自定义指标初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- 指标缓冲区映射关系 SetIndexBuffer(0,TSIBuffer,INDICATOR_DATA); SetIndexBuffer(1,MTMBuffer,INDICATOR_CALCULATIONS); SetIndexBuffer(2,AbsMTMBuffer,INDICATOR_CALCULATIONS); SetIndexBuffer(3,EMA_MTMBuffer,INDICATOR_CALCULATIONS); SetIndexBuffer(4,EMA2_MTMBuffer,INDICATOR_CALCULATIONS); SetIndexBuffer(5,EMA_AbsMTMBuffer,INDICATOR_CALCULATIONS); SetIndexBuffer(6,EMA2_AbsMTMBuffer,INDICATOR_CALCULATIONS); //--- return(0); } //+------------------------------------------------------------------+ //| 自定义指标迭代函数 | //+------------------------------------------------------------------+ int OnCalculate (const int rates_total, // price[]数组大小; const int prev_calculated,// 次调用计算后的价格柱的数量; const int begin, // price[]数组开始计算的索引; const double& price[]) // 指标计算的依据数组; { //--- //--- 返回值会作为下一次调用的 prev_calculated 参数调用 return(rates_total); }
中间计算
组织缓冲区 MTMBuffer[] 和 AbsMTMBuffer[] 的值的计算是非常容易的。在循环中,从 price[1] 至 price[rates_total-1] 逐一遍历值,将差值写入一个数组,差值的绝对值写入第二个数组。
//--- 计算 mtm 和 |mtm| 的数值 for(int i=1;i<rates_total;i++) { MTMBuffer[i]=price[i]-price[i-1]; AbsMTMBuffer[i]=fabs(MTMBuffer[i]); }
下一阶段是计算这些数组的指数平均线。我们有两种方法可用。其一是写入整个算法,设法不犯错误。其二是使用已经调试并严格用于这些目的的现成函数。
MQL5 中没有内置函数用于通过数组值计算移动平均线,但有一个现成的函数库 MovingAverages.mqh 可用,到该库的完整路径为 terminal_directory/MQL5/Include/MovingAverages.mqh,其中 terminal_directory 是 MetaTrader 5 终端的安装目录。该库是一个引用文件;它包含用于计算移动平均线的函数,计算通过使用以下四个经典方法之一在数组上完成:
- 简单移动平均线;
- 指数移动平均线;
- 平滑移动平均线;
- 线性加权移动平均线。
要使用这些函数,应在任何 MQL5 程序的标头码中添加以下代码:
#include <MovingAverages.mqh>
我们需要函数 ExponentialMAOnBuffer(),该函数在值数组上计算指数移动平均线,并将平均值记录在另一数组中。
数组平滑函数
引用文件 MovingAverages.mqh 总共包含八个函数,这八个函数可以划分为相同类型的两组函数,每组 4 个函数。第一组函数接收数组并在指定位置返回移动平均线的值:
- SimpleMA() - 用于计算简单移动平均线的值;
- ExponentialMA() - 用于计算指数移动平均线的值;
- SmoothedMA() - 用于计算平滑移动平均线的值;
- LinearWeightedMA() - 用于计算线性加权移动平均线的值。
这些函数用于获取平均线的值,一个数组一次,且没有针对多重调用进行优化。若需要在循环中使用该组的函数(以计算平均线的值并进一步将每个计算值写入数组),则需要组织一个最优算法。
第二组函数用于将基于初始值数组的移动平均线的值填入接收数组:
- SimpleMAOnBuffer() - 将来自 price[] 数组的简单移动平均线的值填入输出数组 buffer[];
- ExponentialMAOnBuffer() - 将来自 price[] 数组的指数移动平均线的值填入输出数组 buffer[];
- SmoothedMAOnBuffer() - 将来自 price[] 数组的平滑移动平均线的值填入输出数组 buffer[];
- LinearWeightedMAOnBuffer() - 将来自 price[] 数组的线性加权移动平均线的值填入输出数组 buffer[]。
所有指定的函数除数组 buffer[]、price[] 以及 period 平均周期外,均获得 3 个以上的参数,其目的是类同于 OnCalculate() 函数 rates_total、prev_calculated 和 begin 的参数。该组函数可正确处理传递的数组 price[] 和 buffer[],并将索引方向考虑在内(AS_SERIES 标志)。
begin 参数指示源数组的索引,有意义的数据(即需要处理的数据)从此开始。对于 MTMBuffer[] 数组,实际数据从索引 1 开始,因为 MTMBuffer[1]=price[1]-price[0]。MTMBuffer[0] 的值未定义,这就是 begin=1 的原因。
//--- 计算数组的首要移动平均 ExponentialMAOnBuffer(rates_total,prev_calculated, 1, // 索引, 开始从哪个数据开始平滑计算 r, // 指数平均的周期 MTMBuffer, // 计算平均的缓冲区 EMA_MTMBuffer); // 存储计算结果的缓冲区 ExponentialMAOnBuffer(rates_total,prev_calculated, 1,r,AbsMTMBuffer,EMA_AbsMTMBuffer);
取平均线时,应考虑时间周期值,因为在输出数组中,计算值的填入带有迟滞,较大的平均周期则该迟滞也较大。例如,如果 period=10,结果数组中的值将起始于 begin+period-1=begin+10-1。在 buffer[] 的进一步调用中,应将其考虑在内,且处理应从索引 begin+period-1 开始。
因此,我们可以从数组 MTMBuffer[] 和 AbsMTMBuffer 轻松获得次要指数平均线。
//--- 计算数组的次要移动平均 ExponentialMAOnBuffer(rates_total,prev_calculated, r,s,EMA_MTMBuffer,EMA2_MTMBuffer); ExponentialMAOnBuffer(rates_total,prev_calculated, r,s,EMA_AbsMTMBuffer,EMA2_AbsMTMBuffer);
当前的 begin 值为 r,因为 begin=1+r-1(r 是主要指数平均线的时间周期,处理从索引 1 开始)。在输出数组 EMA2_MTMBuffer[] 和 EMA2_AbsMTMBuffer[] 中,计算值起始于索引 r+s-1,因为我们从索引 r 开始处理输入数组,且次要指数平均线的时间周期为 s。
所有预计算就绪,现在我们可以计算将会在图表中绘制的指标缓冲区 TSIBuffer[] 的值。
//--- 现在计算指标的数值 for(int i=r+s-1;i<rates_total;i++) { TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i]; }按 F5 键编译代码,并在 MetaTrader 5 终端中启动代码。运作良好!
此时仍有一些问题待解决。
优化计算
事实上,仅仅编写一个可用的指标是远远不够的。如果我们仔细检查 OnCalculate() 的当前实施情况,就会发现它不是最优的。
int OnCalculate (const int rates_total, // price[]数组大小; const int prev_calculated,// 次调用计算后的价格柱的数量; const int begin,// price[]数组开始计算的索引; const double &price[]) // 指标计算的依据数组; { //--- 计算 mtm 和 |mtm| 的数值 MTMBuffer[0]=0.0; AbsMTMBuffer[0]=0.0; for(int i=1;i<rates_total;i++) { MTMBuffer[i]=price[i]-price[i-1]; AbsMTMBuffer[i]=fabs(MTMBuffer[i]); } //--- 计算数组的首要移动平均 ExponentialMAOnBuffer(rates_total,prev_calculated, 1, // 索引, 开始从哪个数据开始平滑计算 r, // 指数平均的周期 MTMBuffer, // 计算平均的缓冲区 EMA_MTMBuffer); // 存储计算结果的缓冲区 ExponentialMAOnBuffer(rates_total,prev_calculated, 1,r,AbsMTMBuffer,EMA_AbsMTMBuffer); //--- 计算数组的次要移动平均 ExponentialMAOnBuffer(rates_total,prev_calculated, r,s,EMA_MTMBuffer,EMA2_MTMBuffer); ExponentialMAOnBuffer(rates_total,prev_calculated, r,s,EMA_AbsMTMBuffer,EMA2_AbsMTMBuffer); //--- 现在计算指标的数值 for(int i=r+s-1;i<rates_total;i++) { TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i]; } //---返回值会作为下一次调用的 prev_calculated 参数调用 return(rates_total); }
在每个函数的开始,我们在数组 MTMBuffer[] 和 AbsMTMBuffer[] 中计算值。在这种情况下,如果 price[] 的大小达成千上万或甚至百万,不必要的重复计算可能占用 CPU 的所有资源,不论该 CPU 性能如何强劲。
对于组织优化计算,我们使用 prev_calculated 输入参数,其值等于 OnCalculate() 上次调用返回的值。在函数的首次调用中,prev_calculated 的值始终为 0。在此情况下,我们在指标缓冲区中计算所有的值。在下次调用中,我们不必计算整个缓冲区 - 仅需计算最后的值。代码编写如下:
//--- 如果这是第一次调用 if(prev_calculated==0) { //--- 把索引0的数据设为0 MTMBuffer[0]=0.0; AbsMTMBuffer[0]=0.0; } //--- 计算 mtm 和 |mtm| 的数值 int start; if(prev_calculated==0) start=1; // 设置从 MTMBuffer[] 和 AbsMTMBuffer[] 的索引1开始计算填写数据 else start=prev_calculated-1; // 设置从上一次计算的最后一个索引开始计算填写数据 for(int i=start;i<rates_total;i++) { MTMBuffer[i]=price[i]-price[i-1]; AbsMTMBuffer[i]=fabs(MTMBuffer[i]); }
EMA_MTMBuffer[]、EMA_AbsMTMBuffer[]、EMA2_MTMBuffer[] 和 EMA2_AbsMTMBuffer[] 的运算块不需要优化计算,因为 ExponentialMAOnBuffer() 已经是以最优方式编写。我们仅需优化 TSIBuffer[] 数组的值的计算。我们使用用于 MTMBuffer[] 的同样方法。
//--- 现在计算指标的数值 if(prev_calculated==0) start=r+s-1; // 第一次计算的开始位置 for(int i=start;i<rates_total;i++) { TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i]; } //--- 返回值会作为下一次调用的 prev_calculated 参数调用 return(rates_total);
关于优化程序还有最后一点:OnCalculate() 返回 rates_total 的值。这表示 price[] 输入数组的元素数量,该数量用于指标计算。
OnCalculate() 返回值保存在终端内存中,并在下次调用 OnCalculate() 时作为输入参数 prev_calculated 的值传递给函数。
这让我们始终知晓 OnCalculate() 的上次调用中输入数组的大小,并从正确的索引开始指标缓冲区的计算而无需不必要的重复计算。
检查输入数据
要使 OnCalculate() 完美运行,我们还需进行以下操作。我们需要检查在其上计算指标值的 price[] 数组。如果数组的大小 (rates_total) 过小,则无需计算 - 我们需要等待,直至下次调用 OnCalculate() 时有足够的数据。
//--- 如果price[]数组太小 if(rates_total<r+s) return(0); // 不进行计算或者绘图,直接退出 //--- 如果这是第一次调用 if(prev_calculated==0) { //--- 把索引0的数据设为0 MTMBuffer[0]=0.0; AbsMTMBuffer[0]=0.0; }
由于指数平滑相继两次用于计算“真实强弱指数”,则 price[] 的大小须至少等于或大于时间周期 r 和 s 的和;否则执行将终止,OnCalculate() 返回 0。返回零值意味着指标将不会在图表上绘制,因为并未计算指标的值。
设置表示法
若计算正确,则指标可就绪待用。但如果我们从其他 MQL5 程序调用该指标,则其默认使用 Close(收盘)价格建立。我们也可以使用其他默认价格类型 - 从指标的 indicator_applied_price 属性的 ENUM_APPLIED_PRICE 枚举中指定一个值。
例如,若要设置典型价格((最高价+最低价+收盘价)/3)作为价格,则编码如下:
#property indicator_applied_price PRICE_TYPICAL
如果我们仅使用利用 iCustom() 或 IndicatorCreate() 函数的值,则无需进一步的改进。但如果是直接使用,例如在图表上绘制,我们推荐以下额外设置:
- 柱号,起始于绘制的指标;
- TSIBuffer[] 中值的标签,将在 DataWindow 中反映;
- 将鼠标光标停留在指标线上时将会在单独窗口和帮助弹出窗口中显示的指标的短名称。
- 指标值中小数点后显示的位数(这不会影响精确性)。
这些设置可使用来自自定义指标组中的函数在 OnInit()处理函数中调整。添加新的线条并将指标另存为 True_Strength_Index_ver2.mq5。
//+------------------------------------------------------------------+ //| 自定义指标初始函数 | //+------------------------------------------------------------------+ int OnInit() { //--- 指标缓冲区映射关系 SetIndexBuffer(0,TSIBuffer,INDICATOR_DATA); SetIndexBuffer(1,MTMBuffer,INDICATOR_CALCULATIONS); SetIndexBuffer(2,AbsMTMBuffer,INDICATOR_CALCULATIONS); SetIndexBuffer(3,EMA_MTMBuffer,INDICATOR_CALCULATIONS); SetIndexBuffer(4,EMA2_MTMBuffer,INDICATOR_CALCULATIONS); SetIndexBuffer(5,EMA_AbsMTMBuffer,INDICATOR_CALCULATIONS); SetIndexBuffer(6,EMA2_AbsMTMBuffer,INDICATOR_CALCULATIONS); //--- 设定从哪一个柱开始画指标 PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,r+s-1); string shortname; StringConcatenate(shortname,"TSI(",r,",",s,")"); //--- 设置在数据窗口(DataWindow)中的显示标签 PlotIndexSetString(0,PLOT_LABEL,shortname); //--- 设置单独子窗口或者弹出帮助时候的显示名称 IndicatorSetString(INDICATOR_SHORTNAME,shortname); //--- 设置指标数值显示的精确度 IndicatorSetInteger(INDICATOR_DIGITS,2); //--- return(0); }
如果我们启动两种版本的指标,然后将图表滚动至开始处,我们会看到所有的差异。
小结
以创建“真实强弱指数”指标为例,我们可以归纳出在 MQL5 中编写任何指标的基本要点:
- 要创建自己的自定义指标,使用“MQL5 向导”,它将帮助您执行指标设置的基本例行操作。为 OnCalculate() 函数选择必要的变量。
- 如必要,添加更多的数组用于中间计算,并使用 SetIndexBuffer() 函数将这些数组与相应的指标缓冲区绑定。为这些缓冲区指示 INDICATOR_CALCULATIONS 类型。
- 优化 OnCalculate() 中的计算,因为该函数将会在每次价格数据改变时调用。使用现成的已调试函数让代码的编写更加容易,并获得更好的代码可读性。
- 对指标进行额外的视觉调整,使程序对于其他 MQL5 程序和用户而言更易于使用。