什么是指标?指标是我们希望以便利方式在荧幕上显示的一组计算值。这一组值在程序中以数组表示。因此,创建指标意即编写用于处理数组(价格数组)的算法并将处理结果记录在其他数组(指标值)中。
尽管已有许多已成经典的现成指标,但创建自己的指标的必要性始终存在。我们把使用我们自己的算法创建的这类指标称为自定义指标。本文将探讨如何创建简单的自定义指标。
指标可以表现为带有颜色的线条或区域,或作为指向输入头寸的有利时刻的特殊标签显示。同时,这些类型还可以相互结合,从而提供了更多的指标类型。我们将采用 William Blau 开发的众所周知的“真实强弱指数”作为指标创建的示例。
TSI 指标基于双重平滑动量来确定趋势及超卖/超买区域。指标的数学诠释请见 动量、方向和背离,William Blau。在这里,我们仅涉及计算公式。
TSI(CLOSE,r,s) =100*EMA(EMA(mtm,r),s) / EMA(EMA(|mtm|,r),s)
其中:
从上述公式我们可以得知,有三个参数影响指标计算。它们是时间周期 r 和时间周期 s,以及用于计算的价格类型。在前例中,我们使用收盘价。
我们将 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 向导”创建指标头文件,其内规定了指标属性,即:
所有准备工作就绪,现在我们可以着手改进和完善我们的代码。
函数 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 属性标记的指标数组中。从上述公式可以得知,我们还需要以下附加数组:
我们总共还需要添加 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 个函数。第一组函数接收数组并在指定位置返回移动平均线的值:
这些函数用于获取平均线的值,一个数组一次,且没有针对多重调用进行优化。若需要在循环中使用该组的函数(以计算平均线的值并进一步将每个计算值写入数组),则需要组织一个最优算法。
第二组函数用于将基于初始值数组的移动平均线的值填入接收数组:
所有指定的函数除数组 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() 函数的值,则无需进一步的改进。但如果是直接使用,例如在图表上绘制,我们推荐以下额外设置:
这些设置可使用来自自定义指标组中的函数在 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 中编写任何指标的基本要点:
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...