使用 IndicatorCreate 灵活创建指标

在了解了创建指标的新方法后,我们来看看更贴近实际的任务。IndicatorCreate通常用于被调用指标预先未知的情况。例如,在编写能够基于用户配置的任意信号进行交易的通用 EA 交易时,就会有此类需求。甚至连指标名称都可以由用户设置。

我们尚未准备好开发 EA 交易,因此将通过封装指标 UseDemoAll.mq5的示例来研究这项技术,该指标能够显示任何其他指标的数据。

整个过程应如下所示:当我们在图表上运行UseDemoAll时,特性对话框中会显示一个列表,我们需要从中选择一个内置指标或自定义指标,如果选择后者,还需在输入字段中额外指定该指标名称。在另一个字符串参数中,我们可以输入用逗号分隔的参数列表。参数类型将根据其拼写自动确定。例如,带小数点的数字 (10.0) 会被视为双精度浮点数,不带小数点的数字 (15) 视为整数,用引号括起来的内容("text")则视为字符串。

这些只是UseDemoAll的基础设置,并非全部可能的设置。我们稍后会考虑其他设置。

让我们将 ENUM_INDICATOR 枚举作为解决方案的基础:它已包含所有指标类型的元素,包括自定义指标 (IND_CUSTOM)。但坦白说,由于几个原因,这个枚举的原生形式不适合直接使用。首先,无法从中获取特定指标的元数据,例如自变量的数量和类型、缓冲区数量以及指标显示窗口(主窗口还是子窗口)。这些信息对于正确创建和可视化指标至关重要。其次,如果我们定义一个 ENUM_INDICATOR 类型的输入变量,以便用户可以选择所需指标,则在特性对话框中,该变量将通过下拉列表呈现,其中选项仅包含元素名称。实际上,最好能在该列表中为用户提供提示(至少是关于参数的提示)。因此,我们将描述自己的枚举IndicatorType。回想一下,MQL5 允许为每个元素在右侧指定注释,该注释将在界面中显示。

IndicatorType枚举的每个元素中,我们不仅要编码来自 ENUM_INDICATOR 的相应标识符 (ID),还要编码参数数量 (B)、缓冲区数量 (B) 和工作窗口编号 (W)。为此,我们开发了以下宏。

#define MAKE_IND(P,B,W,ID) (int)((W << 24) | ((B & 0xFF) << 16) | ((P & 0xFF) << 8) | (ID & 0xFF))
#define IND_PARAMS(X)   ((X >> 8) & 0xFF)
#define IND_BUFFERS(X)  ((X >> 16) & 0xFF)
#define IND_WINDOW(X)   ((uchar)(X >> 24))
#define IND_ID(X)       ((ENUM_INDICATOR)(X & 0xFF))

MAKE_IND 宏将上述所有特征作为参数,并将它们打包到单一 4 字节整数的不同字节中,从而为新枚举的元素形成唯一代码。其余 4 个宏则允许执行反向操作,即使用该代码计算指标的所有特征。

此处,我们不会提供完整的IndicatorType枚举,只提供其中一部分。完整的源代码可在 AutoIndicator.mqh文件中找到。

enum IndicatorType
{
   iCustom_ = MAKE_IND(000IND_CUSTOM), // {iCustom}(...)[?]
   
   iAC_ = MAKE_IND(011IND_AC), // iAC( )[1]*
   iAD_volume = MAKE_IND(111IND_AD), // iAD(volume)[1]*
   iADX_period = MAKE_IND(131IND_ADX), // iADX(period)[3]*
   iADXWilder_period = MAKE_IND(131IND_ADXW), // iADXWilder(period)[3]*
   ...
   iMomentum_period_price = MAKE_IND(211IND_MOMENTUM), // iMomentum(period,price)[1]*
   iMFI_period_volume = MAKE_IND(211IND_MFI), // iMFI(period,volume)[1]*
   iMA_period_shift_method_price = MAKE_IND(410IND_MA), // iMA(period,shift,method,price)[1]
   iMACD_fast_slow_signal_price = MAKE_IND(421IND_MACD), // iMACD(fast,slow,signal,price)[2]*
   ...
   iTEMA_period_shift_price = MAKE_IND(310IND_TEMA), // iTEMA(period,shift,price)[1]
   iVolumes_volume = MAKE_IND(111IND_VOLUMES), // iVolumes(volume)[1]*
   iWPR_period = MAKE_IND(111IND_WPR// iWPR(period)[1]*
};

注释将成为对用户可见的下拉列表元素,其中显示带命名参数的原型、方括号内的缓冲区数量,以及显示在独立窗口中的指标的星标标记。这些标识符本身也具有信息性,因为它们会被 EnumToString 函数转换为文本,该函数用于向日志输出消息。

参数列表尤为重要,因为用户需要将相应的逗号分隔值输入到为此预留的输入变量中。我们本可以同时显示参数类型,但为简洁起见,决定只保留具有含义的名称,而类型也可从名称中推断出来。例如,periodfastslow 是表示周期(柱线数量)的整数,method 是均线方法 ENUM_MA_METHOD,price 是价格类型 ENUM_APPLIED_PRICE,volume 是成交量类型 ENUM_APPLIED_VOLUME。

为方便用户使用(无需记忆枚举元素的数值),程序将支持所有枚举的名称。特别是,标识符 sma对应 MODE_SMA,ema 对应 MODE_EMA,依此类推。close价会转换为 PRICE_CLOSE,open 会转换为 PRICE_OPEN,其他价格类型也遵循相同规则,即均根据枚举元素标识符中最后一个单词(下划线之后)。例如,对于 iMA 指标的参数列表 (iMA_period_shift_method_price),可以输入以下值:11,0,sma,close。标识符无需加引号。但如有必要,也可以传递包含相同文本的字符串,例如列表 1.5,"close"包含实数 1.5 和字符串 "close"。

指标类型、参数字符串列表以及可选的名称(如果指标是自定义指标)是 AutoIndicator类构造函数的主要数据。

class AutoIndicator
{
protected:
   IndicatorTypetype;       // selected indicator type
   string symbols;          // working symbol (optional)
   ENUM_TIMEFRAMES tf;      // working timeframe (optional)
   MqlParamBuilder builder// "builder" of the parameter array
   int handle;              // indicator handle
   string name;             // custom indicator name
   ...
public:
   AutoIndicator(const IndicatorType tconst string customconst string parameters,
      const string s = NULLconst ENUM_TIMEFRAMES p = 0):
      type(t), name(custom), symbol(s), tf(p), handle(INVALID_HANDLE)
   {
      PrintFormat("Initializing %s(%s) %s, %s",
         (type == iCustom_ ? name : EnumToString(type)), parameters,
         (symbol == NULL ? _Symbol : symbol), EnumToString(tf == 0 ? _Period : tf));
      // split the string into an array of parameters (formed inside the builder)
      parseParameters(parameters);
      // create and store the handle
      handle = create();
   }
   
   int getHandle() const
   {
      return handle;
   }
};

此处及下文省略了部分涉及校验输入数据正确性的代码片段完整源代码详见本书随附资料。

分析参数字符串的过程由parseParameters方法处理。它实现了上述方案,可识别值的类型并将其传递给 MqlParamBuilder对象,我们在之前的示例中已见过该对象。

   int parseParameters(const string &list)
   {
      string sparams[];
      const int n = StringSplit(list, ',', sparams);
      
      for(int i = 0i < ni++)
      {
         // normalization of the string (remove spaces, convert to lower case)
         StringTrimLeft(sparams[i]);
         StringTrimRight(sparams[i]);
         StringToLower(sparams[i]);
   
         if(StringGetCharacter(sparams[i], 0) == '"'
         && StringGetCharacter(sparams[i], StringLen(sparams[i]) - 1) == '"')
         {
            // everything inside quotes is taken as a string
            builder << StringSubstr(sparams[i], 1StringLen(sparams[i]) - 2);
         }
         else
         {
            string part[];
            int p = StringSplit(sparams[i], '.', part);
            if(p == 2// double/float
            {
               builder << StringToDouble(sparams[i]);
            }
            else if(p == 3// datetime
            {
               builder << StringToTime(sparams[i]);
            }
            else if(sparams[i] == "true")
            {
               builder << true;
            }
            else if(sparams[i] == "false")
            {
               builder << false;
            }
            else // int
            {
               int x = lookUpLiterals(sparams[i]);
               if(x == -1)
               {
                  x = (int)StringToInteger(sparams[i]);
               }
               builder << x;
            }
         }
      }
      
      return n;
   }

辅助函数 lookUpLiterals可将标识符转换为标准枚举常量。

   int lookUpLiterals(const string &s)
   {
      if(s == "sma"return MODE_SMA;
      else if(s == "ema"return MODE_EMA;
      else if(s == "smma"return MODE_SMMA;
      else if(s == "lwma"return MODE_LWMA;
      
      else if(s == "close"return PRICE_CLOSE;
      else if(s == "open"return PRICE_OPEN;
      else if(s == "high"return PRICE_HIGH;
      else if(s == "low"return PRICE_LOW;
      else if(s == "median"return PRICE_MEDIAN;
      else if(s == "typical"return PRICE_TYPICAL;
      else if(s == "weighted"return PRICE_WEIGHTED;
   
      else if(s == "lowhigh"return STO_LOWHIGH;
      else if(s == "closeclose"return STO_CLOSECLOSE;
   
      else if(s == "tick"return VOLUME_TICK;
      else if(s == "real"return VOLUME_REAL;
      
      return -1;
   }

在识别参数并将其保存到对象的内部数组 MqlParamBuilder后,将调用create 方法。其目的是将参数复制到本地数组,补充自定义指标的名称(如有),并调用 IndicatorCreate函数。

   int create()
   {
      MqlParam p[];
      // fill 'p' array with parameters collected by 'builder' object
      builder >> p;
      
      if(type == iCustom_)
      {
         // insert the name of the custom indicator at the very beginning
         ArraySetAsSeries(ptrue);
         const int n = ArraySize(p);
         ArrayResize(pn + 1);
         p[n].type = TYPE_STRING;
         p[n].string_value = name;
         ArraySetAsSeries(pfalse);
      }
      
      return IndicatorCreate(symboltfIND_ID(type), ArraySize(p), p);
   }

该方法将返回所获得的句柄。

特别值得注意的是,如何将包含自定义指标名称的额外字符串参数插入到数组的最起始位置。首先,将数组的索引顺序指定为“时间序列模式”(请参阅 ArraySetAsSeries),结果,最后一个元素(根据内存物理存储位置)的索引变为 0,且元素从右向左计数。随后,数组大小增大,并将指标名称写入新增元素中。由于采用反向索引,新增元素并非在现有元素的右侧,而是在现有元素的左侧。最后,将数组恢复为常规索引顺序,此时索引 0 位置存储的正是刚刚作为最后一个元素添加的字符串。

AutoIndicator类可以选择性地根据枚举元素名称形成内置指标的缩写名称。

   ...
   string getName() const
   {
      if(type != iCustom_)
      {
         const string s = EnumToString(type);
         const int p = StringFind(s"_");
         if(p > 0return StringSubstr(s0p);
         return s;
      }
      return name;
   }
};

现在,一切准备就绪,可以直接查看源代码UseDemoAll.mq5。不过,让我们先从略微简化版 UseDemoAllSimple.mq5开始。

首先,我们定义指标缓冲区的数量。由于内置指标中缓冲区最大数量为 5(对于Ichimoku),因此我们以此为上限。我们会将注册这些数组为缓冲区的工作,交给我们已经熟悉的 BufferArray类来完成(请参阅 多货币和多时间范围指标章节,示例 IndUnityPercent)。

#define BUF_NUM 5
   
#property indicator_chart_window
#property indicator_buffers BUF_NUM
#property indicator_plots   BUF_NUM
   
#include <MQL5Book/IndBufArray.mqh>
 
BufferArray buffers(5);

重要的是要记住,指标可以设计为在主窗口中显示或在独立窗口中显示。MQL5 不允许同时使用这两种模式。然而,由于我们无法预知用户将选择哪种指标,因此需要想出某种“变通方法”。目前,我们先将指标放置在主窗口中,稍后再处理独立窗口的问题。

从纯技术角度而言,将具有indicator_separate_window特性的指标缓冲区数据复制到主窗口显示的缓冲区中,不存在任何障碍。但需要注意的是,这类指标的数值范围往往与价格刻度不匹配,因此,即便数值仍会输出到Data window,也很可能无法在图表上看到它们(线条将超出可见区域,位于顶部或底部某处)。

我们将通过输入变量来选择指标类型、自定义指标名称以及参数列表。我们还会添加用于渲染类型和线宽的变量。由于缓冲区将根据源指标的缓冲区数量动态连接,因此我们不会使用指令静态描述缓冲区央视,而是在 OnInit中通过调用内置的 Plot 函数来完成。

input IndicatorType IndicatorSelector = iMA_period_shift_method_price// Built-in Indicator Selector
input string IndicatorCustom = ""// Custom Indicator Name
input string IndicatorParameters = "11,0,sma,close"// Indicator Parameters (comma,separated,list)
input ENUM_DRAW_TYPE DrawType = DRAW_LINE// Drawing Type
input int DrawLineWidth = 1// Drawing Line Width

我们来定义一个全局变量用于存储指标描述符。

int Handle;

OnInit处理程序中,我们使用前面介绍的 AutoIndicator 类来解析输入数据、准备 MqlParam 数组并基于此获取句柄。

#include <MQL5Book/AutoIndicator.mqh>
   
int OnInit()
{
   AutoIndicator indicator(IndicatorSelectorIndicatorCustomIndicatorParameters);
   Handle = indicator.getHandle();
   if(Handle == INVALID_HANDLE)
   {
      Alert(StringFormat("Can't create indicator: %s",
         _LastError ? E2S(_LastError) : "The name or number of parameters is incorrect"));
      return INIT_FAILED;
   }
   ...

要自定义绘图,我们需要定义一组颜色并从AutoIndicator对象中获取指标的简称。我们还会使用 IND_BUFFERS 宏计算内置指标所使用的n个缓冲区数量;而对于任何自定义指标(预先未知),如果没有更好解决方案,我们将包括其所有缓冲区。此外,在数据复制过程中,不必要的 CopyBuffer调用将直接返回错误,这类数组可填充为空值。

   ...
   static color defColors[BUF_NUM] = {clrBlueclrGreenclrRedclrCyanclrMagenta};
   const string s = indicator.getName();
   const int n = (IndicatorSelector != iCustom_) ? IND_BUFFERS(IndicatorSelector) : BUF_NUM;
   ...

在循环中,我们将设置图表特性,并考虑上限值 n:超出该值的缓冲区将被隐藏。

   for(int i = 0i < BUF_NUM; ++i)
   {
      PlotIndexSetString(iPLOT_LABELs + "[" + (string)i + "]");
      PlotIndexSetInteger(iPLOT_DRAW_TYPEi < n ? DrawType : DRAW_NONE);
      PlotIndexSetInteger(iPLOT_LINE_WIDTHDrawLineWidth);
      PlotIndexSetInteger(iPLOT_LINE_COLORdefColors[i]);
      PlotIndexSetInteger(iPLOT_SHOW_DATAi < n);
   }
   
   Comment("DemoAll: ", (IndicatorSelector == iCustom_ ? IndicatorCustom : s),
      "("IndicatorParameters")");
   
   return INIT_SUCCEEDED;
}

图表左上角的注释将显示带参数的指标名称。

OnCalculate处理程序中,当句柄准备就绪后,我们会将其读取到自己的数组中。

int OnCalculate(ON_CALCULATE_STD_SHORT_PARAM_LIST)
{
   if(BarsCalculated(Handle) != rates_total)
   {
      return prev_calculated;
   }
   
   const int m = (IndicatorSelector != iCustom_) ? IND_BUFFERS(IndicatorSelector) : BUF_NUM;
   for(int k = 0k < m; ++k)
   {
      // fill our buffers with data form the indicator with the 'Handle' handle
      const int n = buffers[k].copy(Handle,
         k0rates_total - prev_calculated + 1);
         
      // in case of error clean the buffer
      if(n < 0)
      {
         buffers[k].empty(EMPTY_VALUEprev_calculatedrates_total - prev_calculated);
      }
   }
   
   return rates_total;
}

上述实现是简化版,与原始文件UseDemoAllSimple.mq5保持一致。我们将在后续处理其扩展功能,但现在先检查当前版本的运行情况。下图展示了该指标的两个实例:蓝色线条使用默认设置(iMA_period_shift_method_price,选项为 "11,0,sma,close"),红色线条为 iRSI_period_price,参数为 "11 close"。

包含 iMA 和 iRSI 读数的 UseDemoAllSimple 指标的两个实例

包含 iMA 和 iRSI 读数的 UseDemoAllSimple 指标的两个实例

为了演示,特意选择了 USDRUB 图表,因为此处的报价值与 RSI 指标的范围基本吻合(RSI 指标本应显示在独立窗口中)。在大多数其他交易品种图表上,我们可能看不到 RSI 指标的显示。如果你只关心通过编程方式访问值,则这不算什么大问题,但如果有可视化需求,这就是一个需要解决的问题。

因此,你应以某种方式为子窗口专用的指标提供独立显示。从根本上讲,MQL 开发者社区中存在一个常见需求:支持在主窗口和子窗口中同时显示图形。我们将介绍其中一种解决方案,但为此你需要先了解一些新功能。