技术指标和数字滤波器

GT788 | 27 五月, 2014

简介

多年以来,代码库积累了大量的指标。其中许多指标都是在其他指标的基础上稍作修改。在对图表上的指标经过若干小时的目视比较后,我们禁不住要问:“有没有可能找到更客观和更有效的比较方式?”事实上这是可能的。我们应该承认,指标就是数字滤波器。让我们转向维基百科。

过滤器(化学),一种设计用于物理阻隔某些物体或物质而让其他物体或物质通过的装置(通常为膜或层)。

您是否同意指标可阻隔一些“不必要”的对象并专注于关键对象?现在,让我们来看看什么是数字滤波器。

在电子学、计算机科学和数学中,数字滤波器是在一个取样、离散时间信号上执行数学操作以减少或加强该信号某些方面的系统。

换言之,数字滤波器是一个处理离散信号的过滤器。我们可以将终端上看到的价格视为离散信号,因为它们的值不是连续记录而是在某段时间上记录的。例如,H1 图表上每一小时记录一次价格值,而 M5 上每 5 分钟记录一次。许多指标可被视为线性滤波器。我们在本文中讨论的正是这种指标类型。

现在,当我们处理数字滤波器时,我们来研究一下理论以定义哪些参数需要比较。


1. 频率和周期

首先我要说明一点,任何曲线可以表示为正弦波的总和。

振动周期是经由相同的位置并在相同方向上的波体的两个连续的通过之间的时间间隔。这个值是频率的倒数。

这一定义通过使用正弦波来理解最为容易。让我们考虑 10 个计数的周期。为简单起见,我们将以柱为单位执行计算。

图 1

 图 1. 采样周期信号

我们可以看到,线条在 10 个计数内完成整个周期,而第 11 个柱是新的周期的第一个点。

什么是正弦波的频率?在定义的表述中,周期是一个与频率成倒数的值。那么,如果周期等于 10(柱),频率将为 1/10=0.1(1/柱)。

在物理学中,周期 (T) 以秒 (s) 计算,而频率 (f) 以赫兹 (Hz) 计算。如果我们使用分钟时间框架,则 T=60*10=600 秒,而 f=1/Т=1/600=0.001667 Hz。赫兹和秒在模拟滤波器中最为常用。在数字滤波器中,通常使用计数(我们使用柱的方式)。如果有需要,将它们与必要的秒数相乘。

您可能想知道,这和正弦波有什么关系?正弦波是说明滤波器进入频率和物理意义所必须的,因为这个概念用于相应的工作中。现在,让我们使用 7 个周期从 10 到 70、步进为 10 个柱的正弦波代替之前的 1 个正弦波。图 2 中上方子窗口中的柱用作目测计数数量的指引。

图 2

 图 2. 具有相同振幅的七个正弦波,周期为 10、20...70 个柱。

标度足够大,但仍然有可能混淆。并且如果我们有更多的正弦波,则更容易混淆。

正弦波的总和如下所示:

图 3

图 3. 图 2 中显示的七个正弦波的总和

频率以下列方式显示:

 图 4

图 4. 正弦波总和的频谱(频率)

7 个计数足够显示 7 个正弦波。请注意颜色,它们与上图相对应。慢正弦波后跟快正弦波。最小频率为 0(常数部分),而最大频率为 0.5(1/柱)。对于周期则情况相反。

 图 5

 图 5. 正弦波总和的频谱(周期)

我们知道,频率等于 1/周期。因此,周期应位于 2 到无穷大的范围内。为什么是 0.5 和 2?一个正弦波至少需要两个计数来描述(参见 Nyquist–Shannon 采样定理)。要恢复模拟(连续)信号,每个正弦波我们需要两个或更多的计数(0.5 接收自 1/2)。

让我们看看下表,避免混淆周期和频率:

周期
  
 100     50
   16
  10
   4
   2
频率
0
 0.01 
 0.02 
0.0625
  0.1 
 0.25 
  0.5 

 

我们已探讨了周期和频率的概念,这些都是一些基础内容。所有进一步信息都与这些术语相关。

 

2. 数字滤波器

所以,我们终于准备好来讨论滤波器。假设我们必须移除周期小于 50 的正弦波。

图 6

 图 6. 正弦波总和中较慢(小频率)的组成部分(周期为 50,60 和 70 个柱)

当我们知道初始组成部分时,所有一切就会相对容易了。但如果我们仅仅知道总和呢?在这种情况下,我们需要一个截止频率为 1/45(1/柱)的低通滤波器 (LPF)。

过滤结果将如下所示:

图 7

 图 7. 使用 LPF 的正弦波总和过滤结果(蓝线)

现在,我们仅留下周期为 10、20 和 30 的正弦波。为此,我们应使用一个截止频率为 1/35(1/柱)的高通滤波器 (HPF)。

图 8

 图 8. 正弦波总和的高频率组成部分(周期为 10、20 和 30 个柱)

图 9

图 9. 使用 HPF 的正弦波总和过滤结果(蓝线)

要留下周期为 30、40 和 50 的正弦波,我们需要一个截止频率为 1/25 和 1/55(1/柱)的带宽滤波器 (BF)。

图 10

 图 10. 周期为 30、40 和 50 个柱的正弦线

 

图 11

图 11. 正弦波总和带宽过滤结果(30-50 个柱)

如果我们希望移除周期为 30、40 和 50 的正弦波,我们需要一个截止频率同样为 1/25 和 1/55(1/柱)的带阻(抑制)滤波器。

图 12

图 12. 周期为 10、20、60 和 70 个柱的正弦波

 

图 13

图 13. 取决于正弦波总和的带阻滤波器操作的结果(30-50 个柱)

我们在下图中总结一下中间结果:

图 14

图 14. 理想滤波器的频率参数:较低的频率 (LPF)、较高的频率 (HPF)、带宽 (BF) 和带阻 (RF)

上文探讨的滤波器均已理想化。实际有较大的不同。

 图 15

图 15. 滤波器中的过渡带

可用频带和衰减频带之间有一个过渡带其斜率以 dB/octave 或 dB/decade 为单位。倍频程是频率随机值与其双倍值之间的部分。十倍频程是频率随机值与其十倍值之间的部分。正式而言,过渡带位于截止频率和衰减频带之间。展望未来,我得说,按频谱的截止频率通常由 3 dB 级别定义。

衰减频带中的带外抑制是以分贝为单位的频率抑制。

差拍振动在可用频带中检测。由于我们处理的是实际滤波器,即可用频带中的扭曲,一些频率通过其振幅变大,而一些则变小。值以分贝为单位。

下表可帮助您将值转化为以 dB 为单位:

dB
振幅比
0.5    
1.06
1
1.12
3
1.41
6 2
10
3.16
20 10
30 31.6
40
100
60  1000

 

例如,如果我们希望得到 60 dB 的结果,我们可以找到 20 和 40 dB 的值并将其相乘。

现在我们了解了滤波器的基本参数,让我们转向本文的实践部分。


3. 搜索内核

我们可以说,数字滤波器可通过其脉冲响应(内核)来完全描述。脉冲响应是滤波器对单个脉冲的响应。滤波器可以是 IIR(无线脉冲响应,例如指数移动平均线,EMA)或 FIR(有限脉冲响应,例如简单移动平均线,SMA)类型。

现在,我们可以把我们的注意力转到 MetaEditor 上。首先,我们来创建单个脉冲。它是一个非常简单的指标,将仅显示等于 1 的一个计数。在 MetaEditor 中,单击 New(新建),然后选择 "Custom Indicator"(自定义指标)并单击 Next(下一步):

图 16

图 16. 在 MQL5 向导中创建自定义指标

指定 "Impulse"(脉冲)为名称:

图 17

图 17. 指标的基本属性

选择事件处理程序:

图 18

 图 18. 指标的事件处理程序

现在,我们应添加指标线并在单独的窗口中显示。一切准备就绪。

 图 19

 图 19. 指标的绘图属性

指标的代码如下:

//+------------------------------------------------------------------+
//|                                                      Impulse.mq5 |
//|                        Copyright 2012, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2012, 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
//--- plot Label1
#property indicator_label1  "Label1"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrRed
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- input parameters

//--- indicator buffers
double         Label1Buffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,Label1Buffer,INDICATOR_DATA);
   ArraySetAsSeries(Label1Buffer,true);
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
              const int prev_calculated,
              const int begin,
              const double &price[])
  {
//---
   ArrayInitialize(Label1Buffer,0.0);
   Label1Buffer[1023]=1.;
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

将下述代码添加至 OnInit() 函数:

ArraySetAsSeries(Label1Buffer,true);

以便索引方向从数组尾部开始。

OnCalculate() 中:

ArrayInitialize(Label1Buffer,0.0);
Label1Buffer[1023]=1.;

我们将所有值归零,并将 1 添加到第 1023 个指标的数组单元。

编译 (F7) 并得到如下结果:

图 20

 图 20. 脉冲指标

现在,如果我们应用一些指标,我们可以看到其高达 1024 个计数的脉冲响应(参见示例)。

当然,查看滤波器的内核是极好的,但更多数据只能从频率范围的表现形式获得。为此,我们需要创建一个频谱分析程序,或使用一个无需太费劲的现成解决方案。我们选择第二种方案,使用在“建立一个频谱分析程序”一文中说明的 SpecAnalyzer 指标。

该指标如下所示:

图 21

图 21. SpecAnalyzer

使用前的一些准备工作是必要的。所有必要步骤在下文中说明。


4. 频谱分析程序适应性

"External Data"(外部数据)按钮允许使用来自 SAInpData 指标的数据。

原始数据包含代表滤波器内核的数组。我们将重制文件,以便其能够将任何图表指标传递至频谱分析程序。修改后的指标提供自动和手动模式。在自动模式中,使用找到的第一个图表指标。在手动模式中,用户可在列表中设置一个子窗口和指标索引。在这种情况下,应手动将脉冲指标添加至图表。此后,必要指标被应用以接收内核。

我们开始吧。我们应按照与脉冲相同的算法创建一个新的指标。添加输入参数:

input bool Automatic=true; // Autosearch
input int  Window=0;       // Subwindow index
input int  Indicator=0;    // Indicator index

如果 Automatic=true,使用自动模式,同时忽略其他输入参数。If Automatic=false,使用带子窗口和指标索引的手动模式。

接下来,我们应在全局层面添加用于存储句柄的整型变量。

int Impulse=0; // single impulse's handle
int Handle=0;  // required indicator's handle
int Kernel=0;  // filter kernel's handle

脉冲指标句柄存储在 Impulse 中。指标的句柄 - 我们希望在频谱分析程序中查看其内核 - 存储在 Handle 中。基于脉冲指标构建的目标指标的句柄,或换言之目标指标的内核,存储在 Kernel 中。

OnInit() 函数:

int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,DataBuffer,INDICATOR_DATA);

   Impulse=iCustom(NULL,0,"SpecAnalyzer\\Impulse");//get the single impulse handle
   if(Impulse==INVALID_HANDLE)
     {
      Alert("Impulse initialization failed"); 
       return(INIT_FAILED);
     }
//---
   return(0);
  }

由于脉冲指标在程序操作期间没有发生变化,指标的句柄应在 OnInit() 函数中接收。还需要检查句柄接收错误。如果失败,"Impulse initialization failed"(脉冲初始化失败)消息显示,指标的操作通过 INIT_FAILED 键中断。

OnDeinit() 函数:

void OnDeinit(const int reason)
  {
//--- delete the indicators
   IndicatorRelease(Impulse);
   IndicatorRelease(Handle);
   IndicatorRelease(Kernel);
  }

用过的指标在 OnDeinit() 函数中删除。

OnCalculate() 函数:

static bool Flag=false;        //error flag
if(Flag) return(rates_total); //exit in case of the flag

标记静态变量添加至函数的开头部分。如果在程序的执行期间发生错误,Flag 等于 true OnCalculate() 函数的所有进一步迭代从开始处被中断。

下面是与手动模式相关的代码块:

   string Name;  //short name of the required indicator
   if(!Automatic)//in case of the manual mode
     {
      if(ChartIndicatorsTotal(0,Window)>0)//if an indicator is present
        {
         Name=ChartIndicatorName(0,Window,Indicator);//search for its name
         Handle=ChartIndicatorGet(0,Window,Name);//search for the handle
        }
      else//otherwise
        {
         Alert("No indicator");
         Flag=true;
         return(rates_total);
        }

      if(Handle==INVALID_HANDLE)//in case of a handle receiving error
        {
         Alert("No indicator");
         Flag=true;
         return(rates_total);
        }

      CopyBuffer(Handle,0,0,1024,DataBuffer);//display the kernel on the chart
      return(rates_total);
     }

如果 Automatic=false,手动模式启动。将检查指标是否存在。如果成功,我们开始搜索名称和句柄,检查句柄是否存在错误,并将数据复制到指标的缓冲区。如果失败,"No indicator"(无指标)消息显示,Flag 切换为 trueOnCalculate() 函数的执行被中断。

自动模式的代码块要有趣得多。它包含在图表上搜索指标和创建内核。

所以,我们来考虑搜索指标。其主要目标是接收句柄。

   if(ChartIndicatorsTotal(0,0)>0)//if the indicator is in the main window
     {
      Name=ChartIndicatorName(0,0,0);//search for its name
      if(Name!="SpecAnalyzer")//if it is not SpecAnalyzer
         Handle=ChartIndicatorGet(0,0,Name);//look for a handle
      else
        {
         Alert("Indicator not found");
         Flag=true;
         return(rates_total);
        }
     }
   else//otherwise
   if(ChartIndicatorsTotal(0,1)>0)//if the indicator is in the first subwindow
     {
      Name=ChartIndicatorName(0,1,0);//search for its name
      if(Name!="SAInpData")//if it is not SAInpData
         Handle=ChartIndicatorGet(0,1,Name);//look for a handle
      else//otherwise
        {
         Alert("Indicator not found");
         Flag=true;
         return(rates_total);
        }
     }

   if(Handle==INVALID_HANDLE)//in case of a handle receiving error
     {
      Alert("No indicator");
      Flag=true;
      return(rates_total);
     }

首先,我们在图表的主要子窗口中搜索指标,确保它不是 SpecAnalyzer。如果在主窗口中未找到指标,我们在下一个子窗口中继续搜索(考虑到此处有可能是 SAInpData)。所有其他操作与手动模式类似。

我们来创建一个指标。我们应接收获得指标的参数,并基于脉冲创建一个类似指标:

   ENUM_INDICATOR indicator_type;//obtained indicator's type
   MqlParam parameters[];      //parameters
   int parameters_cnt=0;      //number of parameters

//--- receive the indicator's type, parameter values and amount
   parameters_cnt=IndicatorParameters(Handle,indicator_type,parameters);
//--- define that a single impulse is to be sent to the indicator's input
   parameters[parameters_cnt-1].integer_value=Impulse;
//--- receive the indicator's handle from the single impulse - filter's kernel
   Kernel=IndicatorCreate(NULL,0,indicator_type,parameters_cnt,parameters);

   if(Kernel==INVALID_HANDLE)//in case of a handle receiving error
     {
      Alert("Kernel initialization failed");
      Flag=true;
      return(rates_total);
     }

   CopyBuffer(Kernel,0,0,1024,DataBuffer);//display the kernel on the chart

indicator_type - 特殊枚举 ENUM_INDICATOR 类型的变量。该变量用于接收指标类型。

parameters[] - MqlParam 类型的数组,用于存储和传递指标参数的特殊结构。

IndicatorParameters 函数允许在图表指标上接收数据。接下来,我们对参数的数组稍作更改。脉冲指标句柄包含在最后一个单元中,其中时间序列名称(收盘价、最低价、句柄等)存储在 integer_value 字段中。然后,我们使用 IndicatorCreate 函数创建一个同样为内核的新指标。现在,我们应检查句柄,并在图表上显示内核。

SpecAnalyzer 指标同样稍有改变。添加了下述输入参数:

input bool Automatic=true; //Autosearch
input int Window=0;        //Subwindow index
input int Indicator=0;    //Indicator index

SAInpData 调用也发生了变化:

ExtHandle=iCustom(NULL,0,"SpecAnalyzer\\SAInpData",Automatoc,Window,Indicator);

可单独使用 SAInpData 以查看脉冲响应。

 

5. 示例

为使其开始工作,将 SpecAnalyzer 文件夹粘贴至 MetaTrader 5\MQL5\Indicators。启动 MetaTrader 5,打开一个新的 EURUSD 图表:

图 22

图 22. 打开一个新的 EURUSD 图表

现在,我们应用必要的指标,例如 MA(16):

图 23

图 23. 将“移动平均线”指标应用至 EURUSD 图表

启动 SpecAnalyzer:

图 24

图 24. 启动 SpecAnalyzer

参数窗口出现:

图 25

图 25. SpecAnalyzer 指标参数

对于自动模式,只需要单击 OK(确定)。在手动模式下,true 应替换为 false,并且应指定必要指标的位置。

所以,我们单击 OK(确定)。在新出现的 Spectrum Analyzer(频谱分析程序)窗口中单击 "External Data"(外部数据):

图 26

图 26. 为 SpecAnalyzer 指标选择输入数据

现在,让我们考虑在手动模式下工作。首先,我们应将脉冲指标添加至图表:

图 27

图 27. 添加脉冲指标

接下来,我们应使用这个指标来生成目标指标。为此,我们应使用鼠标将指标拖动到 Impulse(脉冲)窗口,并在参数的 "Apply to"(应用至)字段中选择之前的指标数据

图 28

图 28. 使用脉冲指标数据生成“移动平均线”指标

应得到以下结果:

图 29

图 29. 在单个脉冲上计算“移动平均线”指标的结果

现在,右键单击以查看指标列表:

图 30

图 30. 列表中的指标

我们的指标位于子窗口 1 中,序列号为 1(请记住索引编排从零开始,而不是 1)。现在,让我们启动 SpecAnalyzer。设置 false,1, 1。单击 "External Data"(外部数据)。

指标的属性可以动态变更。尝试使用指标列表更改周期,并查看频谱分析程序如何反应。

在转向示例前,有必要提及 SpecAnalyzer 指标的一个特性。其标度上的读数不是周期,而是频率网格标记。频谱分析程序处理具有长度达 1024 个读数的内核。它表示频率网格的间距为 1/1024=0,0009765625。因此,标度上的值 128 对应于频率 0.125 或周期 8。

标度
周期
16   64
32   32
64   16
128
8
256
4
384
2.67
512
2

SMA (16)

 图 31

图 31.“简单移动平均线”指标的脉冲响应(FIR 滤波器)

图 32

图 32.“简单移动平均线”指标的频率响应

我们可以看到,这是一个低通滤波器,以低频率为主。衰减频带中的抑制较弱。

  

EMA (16)

图 33

图 33.“指数移动平均线”指标的脉冲响应(IIR 滤波器)

图 34

图 34.“指数移动平均线”指标的频率响应

“指数移动平均线”指标也是低通滤波器。线条相当平缓,但与上一个指标不同的是过渡带更宽。抑制几乎相同。

现在,我们来检查一下通用数字滤波器的结果。

低通滤波器

图 35

图 35. 低通滤波器的脉冲响应(内核)

图 36

图 36. 低通滤波器的频率响应

高通滤波器

 图 37

图 37. 高通滤波器的脉冲响应(内核)

 

 图 38

图 38. 高通滤波器的频率响应

 

带通滤波器

 图 39

图 39.带通滤波器的脉冲响应(内核)

 

图 40

  图 40. 带通滤波器的频率响应


总结

总之,应注意到滤波器参数互相密切关联。对其中一些参数的改进意味着其他参数的劣化。因此,应根据手头的任务选择参数。

例如,如果您希望加强对衰减频带中频率的抑制,您需要牺牲向下曲线的陡度。如果两个参数都要好,则我们需要增大内核的长度,其反过来将影响指标和价格之间的间距或增大可用频带的扭曲。