指标创建的高级方式:IndicatorCreate
在创建指标时,无论是使用iCustom函数还是任意内置指标函数,都需要在编码阶段掌握参数列表的信息。但在实际应用中,常常需要编写足够灵活的程序,以便在指标之间进行替换。
例如,在 测试程序中优化 EA 交易时,不仅需选择移动平均线的周期,还需选择其计算算法,这些都是合理的。当然,如果我们基于单一指标iMA构建算法,你可以在其方法设置中提供指定 ENUM_MA_METHOD 的能力。但有人可能希望能在双指数移动平均线、三指数移动平均线和分形移动平均线之间进行切换,从而扩大选择范围。初看来,这可以通过 switch分别调用 DEMA、iTEMA 和iFrAMA 来实现。但如何将自定义指标纳入该列表?
尽管在iCustom调用中可以轻松替换指标名称,但参数列表可能存在显著差异。通常情况下,EA 交易可能需要基于任意技术指标组合生成信号,而这些指标并非预先已知,且不仅限于移动平均线。
针对此类情况,MQL5 提供了一种通用方法,即通过 IndicatorCreate函数创建任意技术指标。
int IndicatorCreate(const string symbol, ENUM_TIMEFRAMES timeframe, ENUM_INDICATOR indicator, int count = 0, const MqlParam ¶meters[] = NULL)
该函数为指定交易品种和时间范围创建指标实例。指标类型通过indicator参数设置。其类型为 ENUM_INDICATOR 枚举(详见下文),其中包含所有 内置指标的标识符以及用于 iCustom的选项。指标参数的数量及其描述分别通过 count参数和 MqlParam 结构体数组(见下文)进行传递。
该数组的每个元素描述所创建指标的相应输入参数,因此元素的内容和顺序必须与以下内容相对应:对于内置指标函数,需与原型相对应,如果是自定义指标,则需与其源代码中的输入变量描述符相对应。
违反此规则可能导致程序执行阶段出错(见下文示例),且无法创建句柄。在最坏的情况下,传递的参数将被错误解释,指标表现异常,但由于缺少报错,这类问题难以察觉。例外情况是当传递空数组或完全不传递时(因为参数 count和parameters 为可选参数):此时指标将使用默认设置创建。此外,对于自定义指标,你还可以从参数列表末尾省略任意数量的参数。
MqlParam结构体专门用于以下场景:通过 IndicatorCreate 创建指标时传递输入参数;通过 IndicatorParameters 获取(在图表上执行)第三方指标的参数信息。
struct MqlParam
|
参数的实际值必须根据第一个 type字段的值,在 integer_value、double_value、string_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
|
如果你使用 iEnvelopes函数,OnInit 处理程序可能如下所示。
int OnInit()
|
缓冲区绑定方式保持不变,但现在我们将采用另一种方式创建句柄。我们来描述 MqlParam数组、填充该数组并调用 IndicatorCreate 函数。
int OnInit()
|
收到句柄后,我们将在 OnCalculate中使用它来填充其两个缓冲区。
int OnCalculate(const int rates_total,
|
我们来验证创建的 UseEnvelopesParams1指标在图表上的显示效果。
UseEnvelopesParams1 指标
上述方法虽然标准,但并非填充特性的最佳方法。由于许多项目中可能需要调用IndicatorCreate,因此有必要简化调用代码的流程,这是合理的。为此,我们将开发一个名为 MqlParamBuilder的类(请参阅文件MqlParamBuilder.mqh)。它的任务是,通过特定方法接收参数值,判断参数类型,并向数组中添加合适的元素(正确填充的结构体)。
MQL5 并不完全支持运行时类型信息 (RTTI) 概念。通过该机制,程序可以在运行时查询其组成部分的描述性元数据,包括变量、结构、类、函数等。MQL5 中可归为 RTTI 的几个内置功能是运算符 typename 和 offsetof。由于 typename运算符可以字符串形式返回类型名称,我们将基于字符串构建类型自动检测器(请参阅文件 RTTI.mqh)。
template<typename T>
|
模板函数 rtti使用typename 获取模板类型参数名称的字符串,然后将其与一个数组的元素进行比较,该数组包含 ENUM_DATATYPE 枚举中的所有内置类型。在该数组中,名称的枚举顺序与枚举元素的值相对应,因此当找到匹配的字符串时,只需将索引转换为 (ENUM_DATATYPE) 类型并返回给调用代码即可。例如,调用 rtti(1.0)或 rtti<double> () 将返回 TYPE_DOUBLE 值。
利用该工具,我们可以回到MqlParamBuilder的开发。在该类中,我们将定义 MqlParam结构体数组以及 n 变量,该变量将包含最后一个待填充元素的索引。
class MqlParamBuilder
|
我们将用于向参数列表添加下一个值的公共方法设计为模板方法。此外,我们将其实现为运算符 '<<' 的重载,并返回指向 "builder" 对象本身的指针。这将允许在一行中向数组写入多个值,例如,像这样:builder << WorkPeriod << PriceType << SmoothingMode。
正是在该方法中,我们将增大数组的大小,获取待填充的有效索引 n,并且立即重置第 n- 个结构。
...
|
在省略号的位置,后续将是主要工作部分,即填充结构体的各个字段。可以假定我们将使用自制的rtti直接确定参数类型。但你应注意一个细微之处。如果我们编写指令array[n].type = rtti(v),则对于枚举,它将无法正确处理。每个枚举都是独立类型,拥有自己的名称,尽管它们的存储方式和整数相同。对于枚举类型,rtti函数将返回 0,因此需要显式地将其替换为 TYPE_INT。
...
|
现在,我们只需将值v 值放入结构体的三个字段之一: integer_value 类型为 long的字段(注意,long 是长整数,因此该字段得名)、类型为 double 的 double_value 字段,或类型为 string 的string_value 字段。同时,内置类型的数量更多,因此我们假定所有整数类型(包括 int、short、char、color、datetime 和枚举)必须归入 integer_value 字段,float 值必须归入 double_value 字段,而 string_value 字段只有一种明确解释:它始终为 string。
为完成该任务,我们将实现多个重载的 assign方法:三个针对特定类型 float、double 和 string 的方法,以及一个适用于其他所有类型的方法。
class MqlParamBuilder
|
这就完成了填充结构体的过程,接下来的问题是将生成的数组传递给调用代码。该操作被分配给一个重载了运算符 '>>’ 的公共方法,该方法有一个参数:对接收数组 MqlParam的引用。
// export the inner array to the outside
|
现在一切准备就绪,我们可以处理修改后的指标UseEnvelopesParams2.mq5的源代码。与第一版相比,改动仅涉及 OnInit处理程序中对MqlParam 数组的填充。在其中,我们描述 "builder" 对象,并通过 '<<' and return the finished array via '>>' 将参数发送给它。整个过程只需一行代码。
int OnInit()
|
为了进行控制,我们将数组输出至日志(上方显示默认值的结果)
如果数组未完整填充,调用IndicatorCreate将返回错误。例如,当使用 Envelopes指标时,如果只传入 5 个必需参数中的 3 个,将返回错误代码 4002,并得到一个无效句柄。
Handle = PRTF(IndicatorCreate(_Symbol, _Period, IND_ENVELOPES, 3, params));
|
不过,如果数组长度超过指标规范不会被视为错误:超出的值将被直接忽略。
需要注意的是,当值类型与预期参数类型不一致时,系统将执行隐式类型转换,这通常不会触发明显的错误,但生成的指标可能无法按预期运行。例如,如果我们向指标发送的不是 Deviation而是字符串,该字符串将被解释为数字 0,导致“包络线”失效:两条线将与中线对齐,而中线的偏移量是由Deviation(以百分比表示)的大小决定的。同样,如果在需要整数的参数中传递含小数部分的实数,该数值将被四舍五入。
当然,我们会保留正确版本的 IndicatorCreate调用,从而获得有效的指标,就像第一版一样。
...
|
从运行表现来看,新指标与之前的版本没有区别。