指标创建的高级方式:IndicatorCreate

在创建指标时,无论是使用iCustom函数还是任意内置指标函数,都需要在编码阶段掌握参数列表的信息。但在实际应用中,常常需要编写足够灵活的程序,以便在指标之间进行替换。

例如,在 测试程序中优化 EA 交易时,不仅需选择移动平均线的周期,还需选择其计算算法,这些都是合理的。当然,如果我们基于单一指标iMA构建算法,你可以在其方法设置中提供指定 ENUM_MA_METHOD 的能力。但有人可能希望能在双指数移动平均线、三指数移动平均线和分形移动平均线之间进行切换,从而扩大选择范围。初看来,这可以通过 switch分别调用 DEMAiTEMAiFrAMA 来实现。但如何将自定义指标纳入该列表?

尽管在iCustom调用中可以轻松替换指标名称,但参数列表可能存在显著差异。通常情况下,EA 交易可能需要基于任意技术指标组合生成信号,而这些指标并非预先已知,且不仅限于移动平均线。

针对此类情况,MQL5 提供了一种通用方法,即通过 IndicatorCreate函数创建任意技术指标。

int IndicatorCreate(const string symbol, ENUM_TIMEFRAMES timeframe, ENUM_INDICATOR indicator, int count = 0, const MqlParam &parameters[] = NULL)

该函数为指定交易品种和时间范围创建指标实例。指标类型通过indicator参数设置。其类型为 ENUM_INDICATOR 枚举(详见下文),其中包含所有 内置指标的标识符以及用于 iCustom的选项。指标参数的数量及其描述分别通过 count参数和 MqlParam 结构体数组(见下文)进行传递。

该数组的每个元素描述所创建指标的相应输入参数,因此元素的内容和顺序必须与以下内容相对应:对于内置指标函数,需与原型相对应,如果是自定义指标,则需与其源代码中的输入变量描述符相对应。

违反此规则可能导致程序执行阶段出错(见下文示例),且无法创建句柄。在最坏的情况下,传递的参数将被错误解释,指标表现异常,但由于缺少报错,这类问题难以察觉。例外情况是当传递空数组或完全不传递时(因为参数 countparameters 为可选参数):此时指标将使用默认设置创建。此外,对于自定义指标,你还可以从参数列表末尾省略任意数量的参数。

MqlParam结构体专门用于以下场景:通过 IndicatorCreate 创建指标时传递输入参数;通过 IndicatorParameters 获取(在图表上执行)第三方指标的参数信息。

struct MqlParam 

   ENUM_DATATYPE type;          // input parameter type
   long          integer_value// field for storing an integer value
   double        double_value;  // field for storing double or float values
   string        string_value;  // field for storing a value of string type
};

参数的实际值必须根据第一个 type字段的值,在 integer_valuedouble_valuestring_value 其中一个字段进行设置。而 type字段使用 ENUM_DATATYPE 枚举定义,该枚举包含所有 MQL5 内置类型的标识符。

标识符

数据类型

TYPE_BOOL

bool

TYPE_CHAR

char

TYPE_UCHAR

uchar

TYPE_SHORT

short

TYPE_USHORT

ushort

TYPE_COLOR

color

TYPE_INT

int

TYPE_UINT

uint

TYPE_DATETIME

datetime

TYPE_LONG

long

TYPE_ULONG

ulong

TYPE_FLOAT

float

TYPE_DOUBLE

double

TYPE_STRING

string

如果指标参数为枚举类型,需在 type字段中使用 TYPE_INT 值进行描述。

第三个参数 IndicatorCreate中用于指定指标类型的 ENUM_INDICATOR 枚举包含以下常量:

标识符

指标

IND_AC

Accelerator Oscillator

IND_AD

Accumulation/Distribution

IND_ADX

Average Directional Index

IND_ADXW

ADX by Welles Wilder

IND_ALLIGATOR

Alligator

IND_AMA

Adaptive Moving Average

IND_AO

Awesome Oscillator

IND_ATR

Average True Range

IND_BANDS

Bollinger Bands®

IND_BEARS

Bears Power

IND_BULLS

Bulls Power

IND_BWMFI

Market Facilitation Index

IND_CCI

Commodity Channel Index

IND_CHAIKIN

Chaikin Oscillator

IND_CUSTOM

自定义指标

IND_DEMA

Double Exponential Moving Average

IND_DEMARKER

DeMarker

IND_ENVELOPES

Envelopes

IND_FORCE

Force Index

IND_FRACTALS

Fractals

IND_FRAMA

Fractal Adaptive Moving Average

IND_GATOR

Gator Oscillator

IND_ICHIMOKU

Ichimoku Kinko Hyo

IND_MA

Moving Average

IND_MACD

MACD

IND_MFI

Money Flow Index

IND_MOMENTUM

Momentum

IND_OBV

On Balance Volume

IND_OSMA

OsMA

IND_RSI

Relative Strength Index

IND_RVI

Relative Vigor Index

IND_SAR

Parabolic SAR

IND_STDDEV

Standard Deviation

IND_STOCHASTIC

Stochastic Oscillator

IND_TEMA

Triple Exponential Moving Average

IND_TRIX

Triple Exponential Moving Averages Oscillator

IND_VIDYA

Variable Index Dynamic Average

IND_VOLUMES

Volumes

IND_WPR

Williams Percent Range

需要注意的是,如果将 IND_CUSTOM 值作为指标类型传递,则参数数组的第一个元素必须包含 type字段且其值为 TYPE_STRING,同时 string_value 字段必须包含自定义指标的名称(路径)。

如果成功,IndicatorCreate函数将返回所创建指标的句柄;如果失败,则返回 INVALID_HANDLE。错误代码将通过 _LastError提供。

需注意,要测试创建自定义指标且指标名称在编译阶段未知的 MQL 程序(使用 IndicatorCreate时也通常如此),必须通过以下指令显式绑定指标:

#property tester_indicator "indicator_name.ex5"

这允许测试程序向测试代理发送所需的辅助指标,但该过程仅限于预先已知的指标。

我们来看看几个示例。我们从一个简单应用开始:使用 IndicatorCreate替代已知函数,然后为了展示新方法的灵活性,我们将创建一个通用包装指标,用于可视化任意内置或自定义指标。

UseEnvelopesParams1.mq5的第一个示例创建了Envelopes 指标的嵌入式副本。为此,我们需要定义两个缓冲区、两个绘图、它们的数组、以及重复 iEnvelopes参数的输入参数。

#property indicator_chart_window
#property indicator_buffers 2
#property indicator_plots   2
   
// drawing settings
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrBlue
#property indicator_width1  1
#property indicator_label1  "Upper"
#property indicator_style1  STYLE_DOT
   
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrRed
#property indicator_width2  1
#property indicator_label2  "Lower"
#property indicator_style2  STYLE_DOT
   
input int WorkPeriod = 14;
input int Shift = 0;
input ENUM_MA_METHOD Method = MODE_EMA;
input ENUM_APPLIED_PRICE Price = PRICE_TYPICAL;
input double Deviation = 0.1// Deviation, %
   
double UpBuffer[];
double DownBuffer[];
   
int Handle// handle of the subordinate indicator

如果你使用 iEnvelopes函数,OnInit 处理程序可能如下所示。

int OnInit()
{
   SetIndexBuffer(0UpBuffer);
   SetIndexBuffer(1DownBuffer);
   
   Handle = iEnvelopes(WorkPeriodShiftMethodPriceDeviation);
   return Handle == INVALID_HANDLE ? INIT_FAILED : INIT_SUCCEEDED;
}

缓冲区绑定方式保持不变,但现在我们将采用另一种方式创建句柄。我们来描述 MqlParam数组、填充该数组并调用 IndicatorCreate 函数。

int OnInit()
{
   ...
   MqlParam params[5] = {};
   params[0].type = TYPE_INT;
   params[0].integer_value = WorkPeriod;
   params[1].type = TYPE_INT;
   params[1].integer_value = Shift;
   params[2].type = TYPE_INT;
   params[2].integer_value = Method;
   params[3].type = TYPE_INT;
   params[3].integer_value = Price;
   params[4].type = TYPE_DOUBLE;
   params[4].double_value = Deviation;
   Handle = IndicatorCreate(_Symbol_PeriodIND_ENVELOPES,
      ArraySize(params), params);
   return Handle == INVALID_HANDLE ? INIT_FAILED : INIT_SUCCEEDED;
}

收到句柄后,我们将在 OnCalculate中使用它来填充其两个缓冲区。

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &data[])
{
   if(BarsCalculated(Handle) != rates_total)
   {
      return prev_calculated;
   }
   
   const int n = CopyBuffer(Handle, 0, 0, rates_total - prev_calculated + 1, UpBuffer);
   const int m = CopyBuffer(Handle, 1, 0, rates_total - prev_calculated + 1, DownBuffer);
      
   return n > -1 && m > -1 ? rates_total : 0;
}

我们来验证创建的 UseEnvelopesParams1指标在图表上的显示效果。

UseEnvelopesParams1 指标

UseEnvelopesParams1 指标

上述方法虽然标准,但并非填充特性的最佳方法。由于许多项目中可能需要调用IndicatorCreate,因此有必要简化调用代码的流程,这是合理的。为此,我们将开发一个名为 MqlParamBuilder的类(请参阅文件MqlParamBuilder.mqh)。它的任务是,通过特定方法接收参数值,判断参数类型,并向数组中添加合适的元素(正确填充的结构体)。

MQL5 并不完全支持运行时类型信息 (RTTI) 概念。通过该机制,程序可以在运行时查询其组成部分的描述性元数据,包括变量、结构、类、函数等。MQL5 中可归为 RTTI 的几个内置功能是运算符 typenameoffsetof。由于 typename运算符可以字符串形式返回类型名称,我们将基于字符串构建类型自动检测器(请参阅文件 RTTI.mqh)。

template<typename T>
ENUM_DATATYPE rtti(T v = (T)NULL)
{
   static string types[] =
   {
      "null",     //               (0)
      "bool",     // 0 TYPE_BOOL=1 (1)
      "char",     // 1 TYPE_CHAR=2 (2)
      "uchar",    // 2 TYPE_UCHAR=3 (3)
      "short",    // 3 TYPE_SHORT=4 (4)
      "ushort",   // 4 TYPE_USHORT=5 (5)
      "color",    // 5 TYPE_COLOR=6 (6)
      "int",      // 6 TYPE_INT=7 (7)
      "uint",     // 7 TYPE_UINT=8 (8)
      "datetime"// 8 TYPE_DATETIME=9 (9)
      "long",     // 9 TYPE_LONG=10 (A)
      "ulong",    // 10 TYPE_ULONG=11 (B)
      "float",    // 11 TYPE_FLOAT=12 (C)
      "double",   // 12 TYPE_DOUBLE=13 (D)
      "string",   // 13 TYPE_STRING=14 (E)
   };
   const string t = typename(T);
   for(int i = 0i < ArraySize(types); ++i)
   {
      if(types[i] == t)
      {
         return (ENUM_DATATYPE)i;
      }
   }
   return (ENUM_DATATYPE)0;
}

模板函数 rtti使用typename 获取模板类型参数名称的字符串,然后将其与一个数组的元素进行比较,该数组包含 ENUM_DATATYPE 枚举中的所有内置类型。在该数组中,名称的枚举顺序与枚举元素的值相对应,因此当找到匹配的字符串时,只需将索引转换为 (ENUM_DATATYPE) 类型并返回给调用代码即可。例如,调用 rtti(1.0)rtti<double> () 将返回 TYPE_DOUBLE 值。

利用该工具,我们可以回到MqlParamBuilder的开发。在该类中,我们将定义 MqlParam结构体数组以及 n 变量,该变量将包含最后一个待填充元素的索引。

class MqlParamBuilder
{
protected:
   MqlParam array[];
   int n;
   ...

我们将用于向参数列表添加下一个值的公共方法设计为模板方法。此外,我们将其实现为运算符 '<<' 的重载,并返回指向 "builder" 对象本身的指针。这将允许在一行中向数组写入多个值,例如,像这样:builder << WorkPeriod << PriceType << SmoothingMode

正是在该方法中,我们将增大数组的大小,获取待填充的有效索引 n,并且立即重置第 n- 个结构。

...
public:
   template<typename T>
   MqlParamBuilder *operator<<(T v)
   {
 // expand the array
      n = ArraySize(array);
      ArrayResize(arrayn + 1);
      ZeroMemory(array[n]);
      ...
      return &this;
   }

在省略号的位置,后续将是主要工作部分,即填充结构体的各个字段。可以假定我们将使用自制的rtti直接确定参数类型。但你应注意一个细微之处。如果我们编写指令array[n].type = rtti(v),则对于枚举,它将无法正确处理。每个枚举都是独立类型,拥有自己的名称,尽管它们的存储方式和整数相同。对于枚举类型,rtti函数将返回 0,因此需要显式地将其替换为 TYPE_INT。

      ...
      // define value type
      array[n].type = rtti(v);
      if(array[n].type == 0array[n].type = TYPE_INT// imply enum
      ...

现在,我们只需将值v 值放入结构体的三个字段之一: integer_value 类型为 long的字段(注意,long 是长整数,因此该字段得名)、类型为 doubledouble_value 字段,或类型为 stringstring_value 字段。同时,内置类型的数量更多,因此我们假定所有整数类型(包括 intshortcharcolordatetime 和枚举)必须归入 integer_value 字段,float 值必须归入 double_value 字段,而 string_value 字段只有一种明确解释:它始终为 string

为完成该任务,我们将实现多个重载的 assign方法:三个针对特定类型 floatdoublestring 的方法,以及一个适用于其他所有类型的方法。

class MqlParamBuilder
{
protected:
   ...
   void assign(const float v)
   {
      array[n].double_value = v;
   }
   
   void assign(const double v)
   {
      array[n].double_value = v;
   }
   
   void assign(const string v)
   {
      array[n].string_value = v;
   }
   
   // here we process int, enum, color, datetime, etc. compatible with long
   template<typename T>
   void assign(const T v)
   {
      array[n].integer_value = v;
   }
   ...

这就完成了填充结构体的过程,接下来的问题是将生成的数组传递给调用代码。该操作被分配给一个重载了运算符 '>>’ 的公共方法,该方法有一个参数:对接收数组 MqlParam的引用。

   // export the inner array to the outside
   void operator>>(MqlParam &params[])
   {
      ArraySwap(arrayparams);
   }

现在一切准备就绪,我们可以处理修改后的指标UseEnvelopesParams2.mq5的源代码。与第一版相比,改动仅涉及 OnInit处理程序中对MqlParam 数组的填充。在其中,我们描述 "builder" 对象,并通过 '<<' and return the finished array via '>>' 将参数发送给它。整个过程只需一行代码。

int OnInit()
{
   ...
   MqlParam params[];
   MqlParamBuilder builder;
   builder << WorkPeriod << Shift << Method << Price << Deviation >> params;
   ArrayPrint(params);
   /*
       [type] [integer_value] [double_value] [string_value]
   [0]      7              14        0.00000 null            <- "INT" period
   [1]      7               0        0.00000 null            <- "INT" shift
   [2]      7               1        0.00000 null            <- "INT" EMA
   [3]      7               6        0.00000 null            <- "INT" TYPICAL
   [4]     13               0        0.10000 null            <- "DOUBLE" deviation
   */

为了进行控制,我们将数组输出至日志(上方显示默认值的结果)

如果数组未完整填充,调用IndicatorCreate将返回错误。例如,当使用 Envelopes指标时,如果只传入 5 个必需参数中的 3 个,将返回错误代码 4002,并得到一个无效句柄。

   Handle = PRTF(IndicatorCreate(_Symbol_PeriodIND_ENVELOPES3params));
   // Error example:
   // indicator Envelopes cannot load [4002]   
   // IndicatorCreate(_Symbol,_Period,IND_ENVELOPES,3,params)=
      -1 / WRONG_INTERNAL_PARAMETER(4002)

不过,如果数组长度超过指标规范不会被视为错误:超出的值将被直接忽略。

需要注意的是,当值类型与预期参数类型不一致时,系统将执行隐式类型转换,这通常不会触发明显的错误,但生成的指标可能无法按预期运行。例如,如果我们向指标发送的不是 Deviation而是字符串,该字符串将被解释为数字 0,导致“包络线”失效:两条线将与中线对齐,而中线的偏移量是由Deviation(以百分比表示)的大小决定的。同样,如果在需要整数的参数中传递含小数部分的实数,该数值将被四舍五入。

当然,我们会保留正确版本的 IndicatorCreate调用,从而获得有效的指标,就像第一版一样。

   ...
   Handle = PRTF(IndicatorCreate(_Symbol_PeriodIND_ENVELOPES,
      ArraySize(params), params));
   // success:
   // IndicatorCreate(_Symbol,_Period,IND_ENVELOPES,ArraySize(params),params)=10 / ok
   return Handle == INVALID_HANDLE ? INIT_FAILED : INIT_SUCCEEDED;
}

从运行表现来看,新指标与之前的版本没有区别。