Skip to content

在MQL程序中使用现成指标

在上一章中,我们学习了如何开发自定义指标。用户可以将这些指标放置在图表上,并使用它们进行手动技术分析。但这并非使用指标的唯一方式。MQL5允许你以编程方式创建指标实例并获取其计算数据。既可以在其他指标中这么做,将几个简单指标组合成更复杂的指标,也能在根据指标信号执行自动或半自动交易的智能交易系统中实现。

只要了解指标参数,以及计算数据在其公共缓冲区中的位置和含义,就能够构建这些新应用的时间序列并访问它们。

在本章中,我们将学习创建和删除指标以及读取其缓冲区的函数。这不仅适用于用MQL5编写的自定义指标,还适用于大量内置指标。

以编程方式与指标进行交互的一般原则包括以下几个步骤:

  1. 创建指标描述符:这是系统响应特定函数调用(iCustom或IndicatorCreate)而发出的唯一识别号,MQL代码通过它来指定所需指标的名称和参数。
  2. 从指标缓冲区读取数据:使用CopyBuffer函数,根据描述符指定的缓冲区来读取数据。
  3. 释放句柄:如果不再需要该指标,使用IndicatorRelease函数释放句柄。

创建和释放描述符通常分别在程序初始化和反初始化期间进行,而缓冲区则会根据需要反复读取和分析,例如在新报价到来时。

在除特殊情况之外的所有情形下,若需要在程序执行期间动态更改指标设置,建议在OnInit函数或全局对象类的构造函数中一次性获取指标描述符。

所有创建指标的函数至少有两个参数:交易品种和时间周期。你可以传入NULL来替代交易品种,这意味着当前交易品种。值为0则对应当前时间周期。你也可以选择使用内置变量_Symbol和_Period。如有必要,还能设置与图表无关的任意交易品种和时间周期。这样,尤其可以实现多资产和多时间周期指标。

在创建指标实例后,你不能立即访问其数据,因为缓冲区的计算需要一定时间。在读取数据之前,你应该使用BarsCalculated函数检查数据是否准备就绪(该函数也接受描述符参数,并返回已计算的K线数量)。否则,你将收到错误信息而非数据。虽然这并非致命错误,不会导致程序停止或卸载,但缺少数据会使程序失去作用。

在本章后续内容中,为简洁起见,我们将创建指标实例并获取其描述符简称为“创建指标”。需要将其与前一章中提到的“创建自定义指标”(即编写指标源代码)区分开来。

指标所有者的句柄和计数器

以编程方式使用指标需要操作句柄。这一点可以与文件描述符作类比(请参阅“打开和关闭文件”部分):在那里我们使用FileOpen函数告知系统文件名和打开模式,之后描述符就成为了使用所有其他文件函数的“通行证”。

指标描述符系统有几个作用。

它允许提前告知终端要启动哪个指标以及计算哪个时间序列。由于下载初始历史数据和计算指标(至少在首次请求时)需要一些时间,同时还需要分配资源(内存、图形处理等),所以指标创建的时间点和准备就绪的时间点是不同的。描述符就是它们之间的纽带。它类似于指向终端内部对象的一种链接,这个内部对象存储着我们在创建指标时设置的一组属性以及指标的当前状态。

当然,为了使用描述符,终端需要维护一个包含所有请求指标及其属性的特定表格。然而,终端不会提供这个总表格中的实际编号信息:相反,每个程序都会形成自己请求的指标的私有列表。这个列表中的条目指向总表格中的元素,而描述符只是该列表中的一个编号。

因此,在不同的程序中,相同的描述符背后可能是完全不同的指标。所以,在程序之间传递描述符的值是没有意义的。

描述符是终端资源管理系统的一部分,因为在可能的情况下,它可以避免具有相同特征的指标实例的重复。换句话说,所有通过编程方式、手动方式或从tpl模板创建的内置指标和自定义指标都会被缓存。

在创建新的指标实例之前,终端会检查缓存中是否存在相同的指标。在检查是否存在副本时,适用以下标准:

  1. 交易品种和时间周期匹配。
  2. 参数匹配。

对于自定义指标,还必须满足以下条件:

  1. 磁盘上的路径(以字符串形式,无需规范化为绝对形式)匹配。
  2. 指标运行所在的图表匹配(当从MQL程序创建指标时,新创建的指标会继承创建它的程序所在的图表)。

内置指标是按交易品种进行缓存的,因此它们的实例可以在不同的图表(相同的交易品种/时间周期)上单独使用。

请注意,不能手动在同一个图表上创建两个相同的指标。不同的程序实例可以请求相同的指标,在这种情况下,只会创建一个指标副本,并提供给两个程序使用。

对于每个唯一的条件组合,终端都会保留一个计数器:在首次请求创建特定指标后,其计数器值为1,在后续请求时,计数器值会增加1(不会创建指标副本)。当释放一个指标时,其计数器值会减1。只有当计数器值归零,即所有使用该指标的程序都明确表示不再使用它时,该指标才会被卸载。

需要注意的是,在同一个MQL程序中多次使用相同参数(包括交易品种/时间周期)调用指标创建函数,并不会导致引用计数器多次增加 —— 计数器只会增加一次。因此,对于每个句柄值,调用一次释放函数(IndicatorRelease)就足够了。所有后续调用都是多余的,并且会返回错误,因为已经没有需要释放的内容了。

除了在MQL5中使用iCustom和IndicatorCreate函数创建指标外,还可以获取第三方(已经存在)指标的句柄。这可以通过使用ChartIndicatorGet函数来实现,我们将在关于图表的章节中学习该函数。这里需要重点注意的是,以这种方式获取句柄也会增加其引用计数,并且在不释放该句柄的情况下会阻止指标被卸载。

如果一个程序创建了从属指标,当这个程序被卸载时,即使没有调用IndicatorRelease函数,这些从属指标的句柄也会自动释放(计数器减1)。

简单创建指标实例的方法:iCustom

MQL5 提供了两个用于从程序中创建指标实例的函数:iCustomIndicatorCreate。第一个函数需要传递一个参数列表,这些参数必须在程序编译时就已知。第二个函数允许在程序执行期间动态地形成一个包含被调用指标参数的数组。这种高级模式将在“高级创建指标的方法:IndicatorCreate”部分进行讨论。

plaintext
int iCustom(const string symbol, ENUM_TIMEFRAMES timeframe, const string pathname, ...)

该函数为指定的交易品种和时间周期创建一个指标。symbol 参数中使用 NULL 可表示当前图表的交易品种,而 timeframe 参数中使用 0 则设置为当前周期。

pathname 参数中,需指定指标名称(即不带扩展名的 .ex5 文件名称),还可以选择指定路径。下面会详细介绍路径相关内容。

pathname 所引用的指标必须已经编译。

该函数返回一个指标句柄,若出错则返回 INVALID_HANDLE。这个句柄在调用本章描述的其他函数以及指标程序控制组中的函数时会用到。句柄是一个整数,在调用程序中唯一标识所创建的指标实例。

iCustom 函数原型中的省略号表示指标的实际参数列表。它们的类型和顺序必须与指标代码中的形式参数相对应。不过,允许从参数列表末尾开始省略值。对于调用代码中未指定的这些参数,所创建的指标将使用相应输入的默认值。

例如,如果指标接受两个输入变量:周期(input int WorkPeriod = 14)和价格类型(input ENUM_APPLIED_PRICE WorkPrice = PRICE_CLOSE),那么可以不同程度详细地调用 iCustom

  • iCustom(_Symbol, _Period, 21, PRICE_TYPICAL):为整个参数列表设置值。
  • iCustom(_Symbol, _Period, 21):设置第一个参数,第二个参数省略,将使用默认值 PRICE_CLOSE
  • iCustom(_Symbol, _Period):两个参数都省略,将分别使用默认值 14PRICE_CLOSE

不能省略参数列表开头或中间的参数。

如果要创建的指标具有简短形式的 OnCalculate 函数,那么除了指标内部描述的输入变量列表之外,最后一个额外参数可以是用于构建指标的价格类型。这就如同指标属性对话框中的“应用于”下拉列表。此外,在这个额外参数中,还可以传递一个之前创建的另一个指标的句柄(见下面的示例)。在这种情况下,新创建的指标将使用指定句柄的第一个指标缓冲区进行计算。换句话说,程序员可以设置一个指标基于另一个指标进行计算。

MQL5 没有提供编程手段来确定特定的第三方指标是使用简短形式还是长形式的 OnCalculate 函数实现的,也就是说,无法确定通过 iCustom 创建时是否允许传递额外的句柄。此外,如果由额外句柄标识的指标有多个缓冲区,MQL5 也不允许选择缓冲区编号。

现在回到 pathname 参数。

路径是一个包含至少一个反斜杠 (\) 或正斜杠 (/) 的字符串,这些字符在文件系统中用作文件夹和文件层次结构的分隔符。可以使用正斜杠或反斜杠,但反斜杠需要“转义”,即必须写两次。这是因为反斜杠是一个控制字符,可形成许多转义码,如制表符 (\t)、换行符 (\n) 等(见“字符类型”部分)。

如果路径以斜杠开头,则称为绝对路径,其根文件夹是所有 MQL5 源代码的目录。例如,在 pathname 参数中指定字符串 "/MyIndicator" 将搜索文件 MQL5/MyIndicator.ex5,而更长的路径 "/Exercise/MyIndicator" 目录将指向 MQL5/Exercise/MyIndicator.ex5

如果 pathname 参数包含一个或多个斜杠,但不以斜杠开头,则该路径称为相对路径,因为它被认为是相对于两个预定义位置之一。首先,会相对于调用 MQL 程序所在的文件夹搜索指标文件。如果在那里找不到,搜索将继续在通用指标文件夹 MQL5/Indicators 内进行。

在包含斜杠的字符串中,最右边斜杠右侧的部分被视为文件名,而之前的部分描述文件夹层次结构。例如,路径 "Folder/SubFolder/Filename" 对应两个子文件夹:Folder 内的 SubFolder,以及 SubFolder 内的 Filename 文件。

最简单的情况是 pathname 不包含斜杠。这样它只指定文件名。同样会在上述两个搜索起始点的上下文中进行搜索。

例如,MyExpert.ex5 智能交易系统位于文件夹 MQL5/Experts/Examples 中,其中包含对 iCustom(_Symbol, _Period, "MyIndicator") 的调用。这里相对路径为空,仅存在文件名。因此,指标搜索从文件夹 MQL5/Experts/Examples/ 开始,查找名为 MyIndicator 的文件,即 MQL5/Experts/Examples/MyIndicator.ex5。如果在该目录中未找到此指标,搜索将继续在指标的根文件夹中进行,即通过连接路径和名称 MQL5/Indicators/MyIndicator.ex5

如果在这两个位置都未找到指标,函数将返回 INVALID_HANDLE,并将错误代码 4802ERR_INDICATOR_CANNOT_CREATE)设置给 _LastError

更复杂的情况是,如果 pathname 不仅包含名称,还包含目录,例如 "TradeSignals/MyIndicator"。然后将指定的路径添加到调用程序的文件夹中,得到以下搜索目标:MQL5/Experts/Examples/TradeSignals/MyIndicator.ex5。如果失败,将相同的路径添加到 MQL5/Indicators 中,即搜索文件 MQL5/Indicators/TradeSignals/MyIndicator.ex5。请注意,如果使用反斜杠作为分隔符,不要忘记写两次,例如 iCustom(_Symbol, _Period, "TradeSignals\\MyIndicator")

要释放不再使用的指标所占用的计算机内存,可以使用 IndicatorRelease 函数,并将该指标的句柄作为参数传递给它。

特别需要注意测试使用指标的程序。如果 iCustom 调用中的 pathname 参数被指定为常量字符串,那么编译器会自动检测相应的所需指标,并将其与被测试的程序一起传递给测试器。否则,如果该参数是在表达式中计算得出的,或者是从外部获取的(例如通过用户输入),则必须在源代码中指定属性 #property tester_indicator

plaintext
#property tester_indicator "indicator_name.ex5"

这意味着在程序中只能测试之前已知的自定义指标。

考虑一个新指标 UseWPR1.mq5 的示例,它在其 OnInit 处理程序内部将创建上一章讨论的 IndWPR 指标的句柄(不要忘记编译 IndWPR,因为 iCustom 会加载 .ex5 文件)。在 UseWPR1 中获得的句柄目前暂未使用,因为我们只是研究这种可能性并检查是否成功。因此,在新指标中不需要缓冲区。

plaintext
#property indicator_separate_window
#property indicator_buffers 0
#property indicator_plots   0

该指标将创建一个空的子窗口,但目前不会在其中显示任何内容。这是正常行为。

让我们检查几种获取描述符的选项,使用不同的 pathname 值:

  1. 以斜杠开头的绝对路径,因此包含整个文件夹层次结构(从 MQL5 开始)以及第 5 章指标的示例,即 "/Indicators/MQL5Book/p5/IndWPR"
  2. 仅使用名称 "IndWPR",在调用指标 UseWPR1.mq5 所在的同一文件夹中进行搜索(两个指标都位于同一文件夹中)。
  3. 相对于标准目录 MQL5/Indicators 的指标示例的文件夹层次结构路径,即 "MQL5Book/p5/IndWPR"(注意开头没有斜杠)。
  4. 与第 2 点相同,仅使用名称,但针对不存在的指标 "IndWPR NonExistent"
  5. 与第 1 点相同的绝对路径,但使用未转义的反斜杠,即 "\Indicators\MQL5Book\p5\IndWPR"
  6. 完全复制第 2 点。
plaintext
int OnInit()
{
   int handle1 = PRTF(iCustom(_Symbol, _Period, "/Indicators/MQL5Book/p5/IndWPR"));
   int handle2 = PRTF(iCustom(_Symbol, _Period, "IndWPR"));
   int handle3 = PRTF(iCustom(_Symbol, _Period, "MQL5Book/p5/IndWPR"));
   int handle4 = PRTF(iCustom(_Symbol, _Period, "IndWPR NonExistent"));
   int handle5 = PRTF(iCustom(_Symbol, _Period, "\Indicators\MQL5Book\p5\IndWPR"));
   int handle6 = PRTF(iCustom(_Symbol, _Period, "IndWPR"));
   return INIT_SUCCEEDED;
}

由于句柄变量未被使用,它们被声明为局部变量。特别说明一下,虽然局部句柄变量在 OnInit 退出时会被删除,但这不会影响句柄本身:只要“父”指标 UseWPR 仍在执行,这些句柄就会继续存在。我们只是在代码中丢失了这些句柄的值,但这不是问题,因为它们在这里没有被使用。在我们后面将考虑的实际指标示例中,句柄当然会被存储(通常存储在全局变量中)并使用。

也不用担心资源泄漏问题:当从图表中删除 UseWPR 指标时,终端会自动清除它创建的所有句柄。使用 IndicatorRelease 删除指标实例的原理和显式释放句柄的必要性将在相关部分详细描述。

上述 OnInit 代码会生成以下日志条目:

plaintext
iCustom(_Symbol,_Period,/Indicators/MQL5Book/p5/IndWPR)=10 / ok
iCustom(_Symbol,_Period,IndWPR)=11 / ok
iCustom(_Symbol,_Period,MQL5Book/p5/IndWPR)=12 / ok
cannot load custom indicator 'IndWPR NonExistent' [4802]
iCustom(_Symbol,_Period,IndWPR NonExistent)=-1 / INDICATOR_CANNOT_CREATE(4802)
iCustom(_Symbol,_Period,\Indicators\MQL5Book\p5\IndWPR)=13 / ok
iCustom(_Symbol,_Period,IndWPR)=11 / ok

正如我们所见,除了第 4 种情况(调用的指标不存在)之外,在所有情况下都获得了有意义的句柄 10111213。句柄的值为 -1INVALID_HANDLE)。

还需注意,第 5 行在编译时会生成几个“未识别的字符转义序列”警告。这是因为我们没有对反斜杠进行转义。而且我们很幸运该指令执行成功了,因为如果任何文件夹或文件的名称以支持的转义序列中的某个字母开头,那么该序列的解释将破坏对名称的预期读取。例如,如果我们在同一文件夹中有一个名为 "test" 的指标,并尝试通过路径 "MQL5Book\p5\test" 创建它,我们将得到 INVALID_HANDLE 和错误 4802。这是因为 \t 是制表符,所以终端会查找 "MQL5Book\p5<nbsp> est"。正确的写法应该是 "MQL5Book\\p5\\test"。因此,使用正斜杠会更方便。

同样重要的是要注意,尽管所有成功的变体都指向同一个指标 MQL5/Indicators/MQL5Book/p5/IndWPR.ex5,并且实际上路径 1、2、3 和 5 是等效的,但终端将它们视为不同的字符串,这就是为什么我们得到不同的描述符值。只有选项 6 完全复制了选项 2,返回了相同的描述符 11

为什么句柄编号从 10 开始?较小的值是为系统保留的。如前所述,对于具有简短形式 OnCalculate 函数的指标,最后一个参数可用于传递价格类型或另一个指标的句柄,该指标的缓冲区将用于计算新创建的实例。由于 ENUM_APPLIED_PRICE 枚举的元素有自己的常量值,它们占据了小于 10 的范围。更多详细信息请参阅“定义指标的数据源”。

在下一个 UseWPR2.mq5 示例中,我们将实现一个指标,它将创建 IndWPR 的一个实例,并使用句柄检查其计算进度。但为此,你需要熟悉新函数 BarsCalculated

计算柱线数量检查:BarsCalculated

当我们通过调用 iCustom 或本章后续会介绍的其他函数来创建第三方指标时,计算需要花费一些时间。我们知道,指标数据准备就绪的主要衡量标准是已计算的柱线数量,这一数量由指标的 OnCalculate 函数返回。有了指标句柄,我们就能得知这个数量。

plaintext
int BarsCalculated(int handle)

该函数会返回由 handle 指定的指标中已计算数据的柱线数量。若出现错误,则返回 -1。

在数据尚未计算完成时,结果为 0。之后,应将这个数值与时间序列的大小(例如,若调用指标在其自身的 OnCalculate 函数上下文中检查 BarsCalculated,则与 rates_total 进行比较)进行对比,以分析该指标对新柱线的处理情况。

UseWPR2.mq5 指标中,我们将尝试创建 IndWPR 指标,同时在输入参数中更改威廉指标(WPR)的周期。

plaintext
input int WPRPeriod = 0;

其默认值为 0,这是一个无效值。故意设置这个值是为了演示异常情况。回顾一下,在原始的 IndWPR.mq5 代码中,OnInitOnCalculate 函数里都有相应的检查。

plaintext
// IndWPR.mq5
void OnInit()
{
   if(WPRPeriod < 1)
   {
      Alert(StringFormat("Incorrect Period value (%d). Should be 1 or larger",
         WPRPeriod));
   }
   ...
}

int OnCalculate(ON_CALCULATE_STD_FULL_PARAM_LIST)
{
   if(rates_total < WPRPeriod || WPRPeriod < 1) return 0;
   ...
}

因此,当周期为 0 时,我们应该会收到一条错误消息,并且 BarsCalculated 应该始终返回 0。在我们输入一个正的周期值后,辅助指标应该会开始正常计算(考虑到威廉指标计算的简易性,几乎会立即开始计算),并且 BarsCalculated 应该返回柱线的总数。

现在,让我们来看一下 UseWPR2.mq5 中创建句柄的源代码。

plaintext
// UseWPR2.mq5
int handle; // 全局变量句柄

int OnInit()
{
   // 传递名称和参数
   handle = PRTF(iCustom(_Symbol, _Period, "IndWPR", WPRPeriod));
   // 此处接下来的检查并无实际用处,因为你必须等待
   // 指标加载、运行并完成计算
   // (此处仅用于演示目的)
   PRTF(BarsCalculated(handle));
   // 初始化是否成功取决于描述符
   return handle == INVALID_HANDLE ? INIT_FAILED : INIT_SUCCEEDED;
}

OnCalculate 函数中,我们只需记录 BarsCalculatedrates_total 的值。

plaintext
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &data[])
{
   // 等待从属指标在所有柱线上完成计算
   if(PRTF(BarsCalculated(handle)) != PRTF(rates_total))
   {
      return prev_calculated;
   }
   
   // ... 此处通常是使用句柄进行的后续操作
   
   return rates_total;
}

编译并运行 UseWPR2,首先使用参数 0,然后使用某个有效数值,例如 21。以下是周期为 0 时的日志记录。

plaintext
iCustom(_Symbol,_Period,IndWPR,WPRPeriod)=10 / ok
BarsCalculated(handle)=-1 / INDICATOR_DATA_NOT_FOUND(4806)
Alert: Incorrect Period value (0). Should be 1 or larger
BarsCalculated(handle)=0 / ok
rates_total=20000 / ok
...

在创建句柄后,数据尚未可用,因此会显示 INDICATOR_DATA_NOT_FOUND(4806) 错误,并且 BarsCalculated 的结果等于 -1。随后会有一条关于输入参数不正确的通知,这证实了 IndWPR 指标已成功加载并启动。在后续部分,我们得到的 BarsCalculated 值等于 0。

为了让指标进行计算,我们需要输入正确的输入参数。在这种情况下,BarsCalculated 等于 rates_total

plaintext
iCustom(_Symbol,_Period,IndWPR,WPRPeriod)=10 / ok
BarsCalculated(handle)=-1 / INDICATOR_DATA_NOT_FOUND(4806)
BarsCalculated(handle)=20000 / ok
rates_total=20000 / ok
...

在我们掌握了检查从属指标是否准备就绪的方法后,就可以开始读取其数据了。让我们在下一个示例 UseWPR3.mq5 中进行这一操作,在那里我们将了解 CopyBuffer 函数。

从指标中获取时间序列数据:CopyBuffer

一个MQL程序可以通过指标的句柄从其公共缓冲区中读取数据。回想一下,在自定义指标中,这些缓冲区是在源代码中通过SetIndexBuffer函数调用指定的数组。

MQL5 API提供了用于读取缓冲区的CopyBuffer函数,该函数有三种形式:

int CopyBuffer(int handle, int buffer, int offset, int count, double &array[])
int CopyBuffer(int handle, int buffer, datetime start, int count, double &array[])
int CopyBuffer(int handle, int buffer, datetime start, datetime stop, double &array[])

handle参数指定从iCustom或其他函数调用中获得的句柄(更多详细信息,请参阅有关IndicatorCreate和内置指标的部分)。buffer参数设置要从中请求数据的指标缓冲区的索引,编号从0开始。

请求的时间序列的接收元素会进入通过引用设置的数组中。

该函数的三种变体的区别在于指定时间戳范围(起始/结束)或获取数据的柱线数量(偏移量)和数量(计数)的方式。使用这些参数的基本原理与我们在“用于获取报价数组的复制函数概述”中所学的完全一致。特别是,偏移量和计数中复制数据的元素是从当前向过去计数的,也就是说,起始位置等于0表示当前柱线。接收数组中的元素在物理上是按从过去到现在的顺序排列的(但是,在逻辑层面上可以使用ArraySetAsSeries来反转这种寻址方式)。

CopyBuffer是用于读取内置时间序列的函数(如CopyOpen、CopyClose等)的类似函数。主要区别在于,带报价的时间序列是由终端本身生成的,而指标缓冲区中的时间序列是由自定义或内置指标计算得出的。此外,对于指标,我们会在诸如iCustom这样的句柄创建函数中提前设置特定的交易品种和时间框架对,以定义和识别时间序列,而在CopyBuffer中,这些信息是通过句柄间接传递的。

当复制未知数量的数据作为目标数组时,最好使用动态数组。在这种情况下,CopyBuffer函数将根据复制数据的大小来分配接收数组的大小。如果需要重复复制已知数量的数据,那么最好在静态分配的缓冲区(带有static修饰符的局部缓冲区或全局上下文中的固定大小缓冲区)中进行,以避免重复的内存分配。

如果接收数组是一个指标缓冲区(之前通过SetIndexBufer函数在系统中注册的数组),那么时间序列和接收缓冲区中的索引是相同的(前提是请求的是相同的交易品种/时间框架对)。在这种情况下,很容易实现对接收缓冲区的部分填充(特别是,这用于更新最后几根柱线,见下面的示例)。如果请求的时间序列的交易品种或时间框架与当前图表的交易品种和/或时间框架不匹配,该函数返回的元素数量将不会超过源和目标中最小的柱线数量。

如果作为数组参数传递的是一个普通数组(不是缓冲区),那么该函数将从第一个元素开始填充它,对于动态数组是全部填充,对于静态数组(如果大小有剩余)则是部分填充。因此,如果需要将指标值部分复制到另一个数组的任意位置,那么为此需要使用一个中间数组,将所需数量的元素复制到该数组中,然后再从那里将它们传输到最终目标。

该函数返回复制的元素数量,如果发生错误(包括暂时没有准备好的数据)则返回-1。

由于指标通常直接或间接依赖于价格时间序列,其计算不会早于报价同步的时间开始。因此,应该考虑终端中时间序列组织和存储的技术特点,并做好请求的数据不会立即出现的准备。特别是,我们可能会收到0个或少于请求数量的数据。所有这些情况都应该根据具体情况进行处理,例如等待构建或向用户报告问题。

如果请求的时间序列尚未构建,或者需要从服务器下载,那么该函数的行为将根据调用它的MQL程序的类型而有所不同。

当从指标请求尚未准备好的数据时,该函数将立即返回-1,但会启动加载和构建时间序列的过程。

当从智能交易系统或脚本请求数据时,如果数据可以从本地历史记录中构建,则会启动从服务器的下载和/或开始构建所需的时间序列。该函数将返回在为同步执行该函数分配的超时时间(45秒)内准备好的数据量(调用代码会等待该函数完成)。

请注意,CopyBuffer函数可以从缓冲区中读取数据,而不管其操作模式是INDICATOR_DATA、INDICATOR_COLOR_INDEX还是INDICATOR_CALCULATIONS,后两者对用户是隐藏的。

同样重要的是要注意,可以在被调用的指标中使用属性PLOT_SHIFT设置时间序列的偏移量,这会影响使用CopyBuffer读取数据的偏移量。例如,如果指标线向未来偏移了N根柱线,那么在CopyBuffer(第一种形式)的参数中,必须给出等于(-N)的偏移量,也就是带负号的,因为当前时间序列柱线的索引为0,而未来带偏移的柱线的索引每根柱线减少1。特别是,在Gator指标中会出现这种情况,因为它的零图表向前偏移了TeethShift参数的值,而第一个图表偏移了LipsShift参数的值。应该根据其中较大的值进行修正。我们将在“从有偏移的图表中读取数据”部分看到一个示例。

MQL5没有提供编程工具来查找第三方指标的PLOT_SHIFT属性。因此,如果有必要,你将不得不通过输入变量向用户请求此信息。

我们将在关于智能交易系统的章节中从智能交易系统代码中使用CopyBuffer,但目前我们只限于指标方面的使用。

让我们继续开发一个辅助指标IndWPR的示例。这次在版本UseWPR3.mq5中,我们将提供一个指标缓冲区,并使用CopyBuffer用来自IndWPR的数据填充它。为此,我们将应用带有缓冲区数量和渲染设置的指令。

#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1

#property indicator_type1   DRAW_LINE
#property indicator_color1  clrBlue
#property indicator_width1  1
#property indicator_label1  "WPR"

在全局上下文中,我们描述了带有WPR周期的输入参数、一个用于缓冲区的数组以及一个带有描述符的变量。

input int WPRPeriod = 14;

double WPRBuffer[];

int handle;

OnInit处理程序实际上没有变化:只是添加了SetIndexBuffer调用。

int OnInit()
{
   SetIndexBuffer(0, WPRBuffer);
   handle = iCustom(_Symbol, _Period, "IndWPR", WPRPeriod);
   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, WPRBuffer);
   // 如果没有错误,我们的数据对于所有柱线rates_total都已准备好
   return n > -1 ? rates_total : 0;
}

通过编译和运行UseWPR3,我们实际上将得到原始WPR的一个副本,但不包括水平调整、数字精度和标题。这对于测试该机制已经足够了,但通常基于一个或多个辅助指标的新指标会提供一些自己的想法和对数据的转换。因此,我们将开发另一个指标,该指标生成买入和卖出交易信号(从交易的角度来看,它们不应被视为一个模型,因为这只是一个编程任务)。该指标的思路如下图所示。

指标IndWPR、IndTripleEMA、IndFractals

指标IndWPR、IndTripleEMA、IndFractals

我们分别将WPR从超买和超卖区域的退出作为卖出和买入的建议。为了使信号不响应随机波动,我们对WPR应用三重移动平均线,并检查其值是否穿过上下区域的边界。

作为这些信号的过滤器,我们将检查在此之前的最后一个分形是什么:顶部的分形意味着价格向下反转并确认卖出,而底部的分形意味着向上反转,因此支持买入。分形会在滞后一定数量的柱线后出现,该数量等于分形的阶数。

新指标在文件UseWPRFractals.mq5中可用。

我们需要三个缓冲区:两个信号缓冲区和一个用于过滤器的缓冲区。我们本可以以INDICATOR_CALCULATIONS模式发出后者。相反,我们将使其成为标准的INDICATOR_DATA模式,但使用DRAW_NONE样式——这样它不会在图表上造成干扰,但其值将在数据窗口中可见。

信号将显示在主图表上(默认在收盘价处),所以我们使用指令indicator_chart_window。我们仍然可以调用在单独窗口中绘制的WPR类型的指标,因为所有从属指标都可以在不进行可视化的情况下进行计算。如果需要,我们可以绘制它们,但我们将在关于图表的章节中讨论这一点(请参阅ChartIndicatorAdd)。

#property indicator_chart_window
#property indicator_buffers 3
#property indicator_plots   3
// 缓冲区绘制设置
#property indicator_type1   DRAW_ARROW
#property indicator_color1  clrRed
#property indicator_width1  1
#property indicator_label1  "Sell"
#property indicator_type2   DRAW_ARROW
#property indicator_color2  clrBlue
#property indicator_width2  1
#property indicator_label2  "Buy"
#property indicator_type3   DRAW_NONE
#property indicator_color3  clrGreen
#property indicator_width3  1
#property indicator_label3  "Filter"

在输入变量中,我们将提供指定WPR周期、平均(平滑)周期和分形阶数的能力。这些是从属指标的参数。此外,我们引入偏移变量,该变量表示将分析信号的柱线编号。值0(默认值)表示当前柱线并以tick模式进行分析(注意:最后一根柱线上的信号可能会重绘;一些交易者不喜欢这样)。如果我们将偏移量设置为1,我们将分析已经形成的柱线,并且这样的信号不会改变。

input int PeriodWPR = 11;
input int PeriodEMA = 5;
input int FractalOrder = 1;
input int Offset = 0;
input double Threshold = 0.2;

Threshold变量将超买和超卖区域的大小定义为±1.0的分数(在每个方向上)。例如,如果你遵循经典的WPR设置,在从0到-100的刻度上,水平为-20和-80,那么Threshold应该等于0.4。

为指标缓冲区提供以下数组。

double UpBuffer[];   // 上信号表示超买,即卖出
double DownBuffer[]; // 下信号表示超卖,即买入
double filter[];     // 分形过滤器方向 +1(向上/买入),-1(向下/卖出)

指标句柄将保存在全局变量中。

int handleWPR, handleEMA3, handleFractals;

我们将像往常一样在OnInit中执行所有设置。由于CopyBuffer函数使用从当前到过去的索引,为了统一读取数据,我们为所有数组设置“series”标志(ArraySetAsSeries)。

int OnInit()
{
   // 绑定缓冲区
   SetIndexBuffer(0, UpBuffer);
   SetIndexBuffer(1, DownBuffer);
   SetIndexBuffer(2, Filter, INDICATOR_DATA); // 版本:INDICATOR_CALCULATIONS
   ArraySetAsSeries(UpBuffer, true);
   ArraySetAsSeries(DownBuffer, true);
   ArraySetAsSeries(Filter, true);

   // 箭头信号
   PlotIndexSetInteger(0, PLOT_ARROW, 234);
   PlotIndexSetInteger(1, PLOT_ARROW, 233);

   // 从属指标
   handleWPR = iCustom(_Symbol, _Period, "IndWPR", PeriodWPR);
   handleEMA3 = iCustom(_Symbol, _Period, "IndTripleEMA", PeriodEMA, 0, handleWPR);
   handleFractals = iCustom(_Symbol, _Period, "IndFractals", FractalOrder);
   if(handleWPR == INVALID_HANDLE
   || handleEMA3 == INVALID_HANDLE
   || handleFractals == INVALID_HANDLE)
   {
      return INIT_FAILED;
   }

   return INIT_SUCCEEDED;
}

在iCustom调用中,应该注意handleEMA3是如何创建的。由于这个平均值是要基于WPR进行计算的,我们将handleWPR(在前一个iCustom调用中获得)作为最后一个参数传递,在指标IndTripleEMA的实际参数之后。这样做时,我们必须指定IndTripleEMA的完整输入参数列表(其中的参数是int InpPeriodEMA和BEGIN_POLICY InpHandleBegin;我们使用第二个参数来研究跳过初始柱线的情况,现在不需要它,但我们必须传递它,所以我们就将其设置为0)。如果我们在调用中因为在当前应用上下文中无关紧要而省略了第二个参数,那么传递的handleWPR句柄将在被调用的指标中被解释为InpHandleBegin。结果,IndTripleEMA将应用于常规收盘价。

当我们不需要传递额外的句柄时,iCustom调用的语法允许我们省略任意数量的最后参数,此时它们将从源代码中获取默认值。

在OnCalculate处理程序中,我们等待WPR指标和分形准备好,然后使用辅助函数MarkSignals为整个历史或最后一根柱线计算信号。

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &data[])
{
   if(BarsCalculated(handleEMA3) != rates_total
   || BarsCalculated(handleFractals) != rates_total)
   {
      return prev_calculated;
   }

   ArraySetAsSeries(data, true);

   if(prev_calculated == 0) // 首次启动
   {
      ArrayInitialize(UpBuffer, EMPTY_VALUE);
      ArrayInitialize(DownBuffer, EMPTY_VALUE);
      ArrayInitialize(Filter, 0);

      // 在整个历史中寻找信号
      for(int i = rates_total - FractalOrder - 1; i >= 0; --i)
      {
         MarkSignals(i, Offset, data);
      }
   }
   else // 在线
   {
      for(int i = 0; i < rates_total - prev_calculated; ++i)
      {
         UpBuffer[i] = EMPTY_VALUE;
         DownBuffer[i] = EMPTY_VALUE;
         Filter[i] = 0;
      }

      // 在新柱线或每个tick上寻找信号(如果Offset == 0)
      if(rates_total != prev_calculated
      || Offset == 0)
      {
         MarkSignals(0, Offset, data);
      }
   }

   return rates_total;
}

我们主要感兴趣的是隐藏在MarkSignals中的CopyBuffer函数的使用。平滑后的WPR值将被读取到wpr[2]数组中,分形将被读取到peaks[1]和hollows[1]中。

int MarkSignals(const int bar, const int offset, const double &data[])
{
   double wpr[2];
   double peaks[1], hollows[1];
   ...

然后我们使用三个CopyBuffer调用来填充局部数组。请注意,我们不需要直接读取IndWPR,因为它在IndTripleEMA的计算中被使用。我们通过handleEMA3句柄将数据读取到wpr数组中。同样重要的是,分形指标中有2个缓冲区,因此CopyBuffer函数会分别针对数组peaks和hollows使用不同的索引0和1调用两次。分形数组是在偏移FractalOrder的情况下读取的,因为分形只能在左右两侧有一定数量柱线的柱线上形成。

   if(CopyBuffer(handleEMA3, 0, bar + offset, 2, wpr) == 2
   && CopyBuffer(handleFractals, 0, bar + offset + FractalOrder, 1, peaks) == 1
   && CopyBuffer(handleFractals, 1, bar + offset + FractalOrder, 1, hollows) == 1)
   {
      ...

接下来,我们从缓冲区Filter的前一根柱线获取过滤器的先前方向(在历史开始时为0,但当出现向上或向下的分形时,我们会在那里写入+1或-1,这可以在下面的源代码中看到),并在检测到任何新分形时相应地更改它。

      int filterdirection = (int)Filter[bar + 1];

      // 最后一个分形设置反转走势
      if(peaks[0] != EMPTY_VALUE)
      {
         filterdirection = -1; // 卖出
      }
      if(hollows[0] != EMPTY_VALUE)
      {
         filterdirection = +1; // 买入
      }

      Filter[bar] = filterdirection; // 记住当前方向

最后,我们考虑到Threshold中指定的区域宽度,分析平滑后的WPR从上部或下部区域到中间区域的转换。

      // 将2个WPR值转换到范围[-1,+1]
      const double
        const double old = (wpr[0] + 50) / 50;     // +1.0 -1.0
      const double last = (wpr[1] + 50) / 50;    // +1.0 -1.0

      // 从顶部向下反弹
      if(filterdirection == -1
      && old >= 1.0 - Threshold && last <= 1.0 - Threshold)
      {
         UpBuffer[bar] = data[bar];
         return -1; // 卖出
      }

      // 从底部向上反弹
      if(filterdirection == +1
      && old <= -1.0 + Threshold && last >= -1.0 + Threshold)
      {
         DownBuffer[bar] = data[bar];
         return +1; // 买入
      }
   }
   return 0; // 无信号
}

对多个交易品种和时间框架的支持

到目前为止,在所有的指标示例中,我们创建的描述符都与当前图表上的交易品种和时间框架相同。然而,实际上并没有这样的限制。我们可以在任何交易品种和时间框架上创建辅助指标。当然,在这种情况下,就像我们之前所做的那样,例如通过定时器,我们需要等待第三方时间序列准备就绪。

让我们实现一个多时间框架的威廉指标(WPR)(请参阅文件 UseWPRMTF.mq5),它也可以被设置为在任意一个交易品种(非当前图表上的交易品种)上进行计算。

我们将显示来自 ENUM_TIMEFRAMES 枚举中所有标准时间框架的给定周期的 WPR 值。时间框架的数量是 21 个,所以该指标将始终显示在最后 21 根柱线上。最右边的第 0 根柱线将包含 M1 时间框架的 WPR 值,下一根柱线将包含 M2 时间框架的 WPR 值,以此类推,直到第 20 根柱线包含月线时间框架的 WPR 值。为了便于阅读,我们将用不同颜色为图表曲线上色:分钟时间框架用红色,小时时间框架用绿色,日线及更大时间框架用蓝色。

由于可以在指标中设置工作交易品种,并且可以在同一图表上为不同交易品种创建多个副本,我们将选择 DRAW_ARROW 绘图样式,并提供一个输入参数来指定交易品种。通过这种方式,就可以区分不同交易品种的指标指示。上色需要额外的缓冲区。

plaintext
#property indicator_separate_window
#property indicator_buffers 2
#property indicator_plots   1

#property indicator_type1   DRAW_COLOR_ARROW
#property indicator_color1  clrRed,clrGreen,clrBlue
#property indicator_width1  3
#property indicator_label1  "WPR"

WPR 值被转换到范围 [-1, +1] 内。让我们选择子窗口的刻度范围,使其比该范围略大一些。值为 ±0.6 的水平位对应于 WPR 转换前的标准值 -20-80

plaintext
#property indicator_maximum    +1.2
#property indicator_minimum    -1.2

#property indicator_level1     +0.6
#property indicator_level2     -0.6
#property indicator_levelstyle STYLE_DOT
#property indicator_levelcolor clrSilver
#property indicator_levelwidth 1

在输入变量中:WPR 周期、工作交易品种和显示箭头的代码。当交易品种留空时,将使用当前图表的交易品种。

plaintext
input int WPRPeriod = 14;
input string WorkSymbol = ""; // 交易品种
input int Mark = 0;

为了方便编码,时间框架的集合列在数组 TF 中。

plaintext
#define TFS 21

ENUM_TIMEFRAMES TF[TFS] =
{
   PERIOD_M1,
   PERIOD_M2,
   PERIOD_M3,
  ...
   PERIOD_D1,
   PERIOD_W1,
   PERIOD_MN1,
};

每个时间框架的指标描述符存储在数组 Handle 中。

plaintext
int Handle[TFS];

我们将在 OnInit 函数中配置指标缓冲区并获取句柄。

plaintext
double WPRBuffer[];
double Colors[];

int OnInit()
{
   SetIndexBuffer(0, WPRBuffer);
   SetIndexBuffer(1, Colors, INDICATOR_COLOR_INDEX);
   ArraySetAsSeries(WPRBuffer, true);
   ArraySetAsSeries(Colors, true);
   PlotIndexSetString(0, PLOT_LABEL, _WorkSymbol + " WPR");

   if(Mark != 0)
   {
      PlotIndexSetInteger(0, PLOT_ARROW, Mark);
   }

   for(int i = 0; i < TFS; ++i)
   {
      Handle[i] = iCustom(_WorkSymbol, TF[i], "IndWPR", WPRPeriod);
      if(Handle[i] == INVALID_HANDLE) return INIT_FAILED;
   }

   IndicatorSetInteger(INDICATOR_DIGITS, 2);
   IndicatorSetString(INDICATOR_SHORTNAME,
      "%Rmtf" + "(" + _WorkSymbol + "/" + (string)WPRPeriod + ")");

   return INIT_SUCCEEDED;
}

OnCalculate 函数中的计算按照常规方案进行:等待数据准备就绪、初始化、在新柱线上填充数据。辅助函数 IsDataReadyFillData 对描述符进行直接操作(如下所示)。

plaintext
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &data[])
{
   // 等待从属指标准备就绪
   if(!IsDataReady())
   {
      EventSetTimer(1); // 如果未准备好,推迟计算
      return prev_calculated;
   }
   if(prev_calculated == 0) // 初始化
   {
      ArrayInitialize(WPRBuffer, EMPTY_VALUE);
      ArrayInitialize(Colors, EMPTY_VALUE);
      // 为最后 TFS 根柱线设置固定颜色
      for(int i = 0; i < TFS; ++i)
      {
         Colors[i] = i < 11? 0 : (i < 18? 1 : 2);
      }
   }
   else // 准备新柱线
   {
      for(int i = prev_calculated; i < rates_total; ++i)
      {
         WPRBuffer[i] = EMPTY_VALUE;
         Colors[i] = 0;
      }
   }

   if(prev_calculated != rates_total) // 新柱线
   {
      // 清除移动到 TFS 根柱线左侧最旧柱线上的标签
      WPRBuffer[TFS] = EMPTY_VALUE;
      // 更新柱线颜色
      for(int i = 0; i < TFS; ++i)
      {
         Colors[i] = i < 11? 0 : (i < 18? 1 : 2);
      }
   }

   // 将数据从从属指标复制到我们的缓冲区
   FillData();
   return rates_total;
}

如有必要,我们通过定时器启动重新计算。

plaintext
void OnTimer()
{
   ChartSetSymbolPeriod(0, _Symbol, _Period);
   EventKillTimer();
}

以下是函数 IsDataReadyFillData

plaintext
bool IsDataReady()
{
   for(int i = 0; i < TFS; ++i)
   {
      if(BarsCalculated(Handle[i]) != iBars(_WorkSymbol, TF[i]))
      {
         Print("Waiting for ", _WorkSymbol, " ", EnumToString(TF[i]));
         return false;
      }
   }
   return true;
}

void FillData()
{
   for(int i = 0; i < TFS; ++i)
   {
      double data[1];
      // 获取最后一个实际值(缓冲区 0,索引 0)
      if(CopyBuffer(Handle[i], 0, 0, 1, data) == 1)
      {
         WPRBuffer[i] = (data[0] + 50) / 50;
      }
   }
}

让我们编译该指标并查看它在图表上的显示情况。例如,让我们为欧元兑美元(EURUSD)、美元兑俄罗斯卢布(USDRUB)和黄金(XAUUSD)创建三个副本。

不同工作交易品种的三个多时间框架 WPR 指标实例

不同工作交易品种的三个多时间框架 WPR 指标实例

在第一次计算时,该指标可能需要相当长的时间来为所有时间框架准备时间序列。

在计算部分,完全相同的指标 UseWPRMTFDashboard.mq5 被设计成了交易者常用的仪表盘形式。对于每个交易品种,我们在指标的 Level 参数中设置单独的垂直缩进。在这里,所有时间框架的 WPR 值以一行标记的形式显示,并且这些值用颜色编码。在这个版本中,WPR 值被归一化到范围 [0..1] 内,所以使用相隔几十的水平位刻度(例如,如下图截图中的 20)可以在子窗口中放置多个指标实例而不会重叠(80、100、120 等)。每个副本用于其自己的工作交易品种。此外,由于 Level 大于 1.0,而 WPR 值较小,它们在数据窗口中的值可以分别从小数点的左边和右边清晰可见。

标签刻度的标签由在 OnInit 函数中动态添加的水平位提供。

不同工作交易品种的三个多时间框架 WPR 指标线面板

不同工作交易品种的三个多时间框架 WPR 指标线面板

你可以研究 UseWPRMTFDashboard.mq5 的源代码,并将其与 UseWPRMTF.mq5 进行比较。为了生成颜色色调的调色板,我们使用了文件 ColorMix.mqh

在我们完成对内置指标(包括 iWPR)的学习后,我们可以用内置的 iWPR 指标来替换自定义的 IndWPR 指标。

关于复合指标的有效性和资源占用

上面展示的生成许多辅助指标的方法,在速度和资源消耗方面效率并不高。这主要是一个集成 MQL 程序并在它们之间交换数据的示例。但就像任何技术一样,应该恰当地使用它。

所创建的两个指标中的每一个都会在时间序列的所有柱线上计算 WPR,然后只有最后一个值会被取到调用指标中。我们既浪费了内存,也浪费了处理器时间。

如果辅助指标的源代码可用,或者知道它们的运行原理,最理想的方法是将计算算法放在主指标(或智能交易系统)内部,并将其应用于有限的、最小深度的即时历史数据。

在某些情况下,可以通过在当前时间框架上进行等效计算来避免引用更高的时间框架:例如,对于 14 根日线柱线的价格区间(这需要构建完整的 D1 时间序列),可以在 14 * 24 根 H1 柱线上取一个区间,前提是 24 小时交易并且在 H1 图表上启动该指标。

同时,当在交易系统中使用一个商业指标(没有源代码)时,只能通过开放的编程接口从它那里获取数据。在这种情况下,创建一个句柄,然后通过 CopyBuffer 从指标缓冲区读取数据是唯一可行的选择,但同时也是一种方便、通用的方法。只是应该始终记住,调用 API 函数是一种比在 MQL 程序内部操作自己的数组和调用局部函数更“昂贵”的操作。如果你需要打开许多终端,可能每个终端都有一组这样未优化的 MQL 程序,并且如果你的资源有限,那么性能很可能会下降。

内置指标概述

终端提供了大量常用指标,这些指标也可以通过API使用。因此,您无需在MQL5中实现它们的算法。此类指标是使用类似于iCustom的内置函数创建的。例如,我们之前出于教学目的创建了自己版本的威廉指标(WPR)和三重指数移动平均线(EMA)。然而,相应的指标可以直接通过iWPR和iTEMA函数使用。所有可用的指标如下表所示。

所有内置指标都将工作交易品种的字符串和时间框架作为前两个参数,并且还会返回一个整数,该整数是指标描述符。一般来说,所有函数的原型如下:

int iFunction(const string symbol, ENUM_TIMEFRAMES timeframe, ...)

省略号的位置后面跟着特定指标的具体参数。参数的数量和类型各不相同。有些指标没有参数。

例如,WPR有一个参数,就像我们自制版本中的一样——周期:int iWPR(const string symbol, ENUM_TIMEFRAMES timeframe, int period)。与我们的版本不同,内置的分形指标没有特殊参数:int iFractals(const string symbol, ENUM_TIMEFRAMES period)。在这种情况下,分形的阶数是硬编码的,等于2,也就是说,在极值(顶部或底部)之前和之后,必须分别至少有两根高低价格不太明显的柱线。

允许设置值NULL来代替交易品种。NULL表示当前图表的工作交易品种,并且时间框架参数中的值0对应于当前图表的时间框架,因为它也是ENUM_TIMEFRAMES枚举中的PERIOD_CURRENT值(请参阅“交易品种和时间框架”部分)。

您还应该记住,不同类型的指标具有不同数量的缓冲区。例如,移动平均线或WPR只有一个缓冲区,而分形有两个缓冲区。缓冲区的数量也在表格的单独一列中注明。

函数指标名称选项缓冲区
iAC加速振荡器1*
iAD累积/派发指标ENUM_APPLIED_VOLUME volume1*
iADX平均趋向指数int period3*
iADXWilder威尔德平均趋向指数int period3*
iAlligator鳄鱼指标int jawPeriod, int jawShift, int teethPeriod, int teethShift, int lipsPeriod, int lipsShift, ENUM_MA_METHOD method, ENUM_APPLIED_PRICE price3
iAMA自适应移动平均线int period, int fast, int slow, int shift, ENUM_APPLIED_PRICE price1
iAO很棒振荡器1*
iATR平均真实波幅int period1*
iBands布林带指标int period, int shift, double deviation, ENUM_APPLIED_PRICE price3
iBearsPower熊市力量指标int period1*
iBullsPower牛市力量指标int period1*
iBWMFI比尔·威廉姆斯市场促进指数ENUM_APPLIED_VOLUME volume1*
iCCI商品通道指标int period, ENUM_APPLIED_PRICE price1*
iChaikin蔡金振荡器int fast, int slow, ENUM_MA_METHOD method, ENUM_APPLIED_VOLUME volume1*
iDEMA双指数移动平均线int period, int shift, ENUM_APPLIED_PRICE price1
iDeMarker德马克指标int period1*
iEnvelopes包络线指标int period, int shift, ENUM_MA_METHOD method, ENUM_APPLIED_PRICE price, double deviation2
iForce力量指数int period, ENUM_MA_METHOD method, ENUM_APPLIED_VOLUME volume1*
iFractals分形指标2
iFrAMA分形自适应移动平均线int period, int shift, ENUM_APPLIED_PRICE price1
iGator鳄鱼振荡器int jawPeriod, int jawShift, int teethPeriod, int teethShift, int lipsPeriod, int lipsShift, ENUM_MA_METHOD method, ENUM_APPLIED_PRICE price4*
iIchimoku一目均衡表指标int tenkan, int kijun, int senkou5
iMomentum动量指标int period, ENUM_APPLIED_PRICE price1*
iMFI资金流量指标int period, ENUM_APPLIED_VOLUME volume1*
iMA移动平均线int period, int shift, ENUM_MA_METHOD method, ENUM_APPLIED_PRICE price1
iMACD指数平滑异同移动平均线int fast, int slow, int signal, ENUM_APPLIED_PRICE price2*
iOBV能量潮指标ENUM_APPLIED_VOLUME volume1*
iOsMA振荡器移动平均线(MACD直方图)int fast, int slow, int signal, ENUM_APPLIED_PRICE price1*
iRSI相对强弱指标int period, ENUM_APPLIED_PRICE price1*
iRVI相对活力指标int period1*
iSAR抛物线转向指标double step, double maximum1
iStdDev标准差指标int period, int shift, ENUM_MA_METHOD method, ENUM_APPLIED_PRICE price1*
iStochastic随机指标int Kperiod, int Dperiod, int slowing, ENUM_MA_METHOD method, ENUM_APPLIED_PRICE price2*
iTEMA三重指数移动平均线int period, int shift, ENUM_APPLIED_PRICE price1
iTriX三重指数移动平均线振荡器int period, ENUM_APPLIED_PRICE price1*
iVIDyA可变指数动态平均线int momentum, int smooth, int shift, ENUM_APPLIED_PRICE price1
iVolumes成交量指标ENUM_APPLIED_VOLUME volume1*
iWPR威廉指标int period1*

在最右边一列中,带有自己窗口的指标用星号 * 表示(它们显示在主图表下方)。

最常用的参数是那些定义指标周期的参数(period、fast、slow以及其他变体),以及线条偏移量:当偏移量为正数时,图形向右偏移;当偏移量为负数时,图形向左偏移给定数量的柱线。

许多参数都有应用枚举类型:ENUM_APPLIED_PRICE、ENUM_APPLIED_VOLUME、ENUM_MA_METHOD。我们已经在“枚举”部分了解了ENUM_APPLIED_PRICE。所有可用的类型及其描述如下表所示。

标识符描述
PRICE_CLOSE柱线收盘价1
PRICE_OPEN柱线开盘价2
PRICE_HIGH柱线最高价3
PRICE_LOW柱线最低价4
PRICE_MEDIAN中间价,(最高价+最低价)/25
PRICE_TYPICAL典型价,(最高价+最低价+收盘价)/36
PRICE_WEIGHTED加权平均价,(最高价+最低价+收盘价+收盘价)/47

与成交量相关的指标可以使用tick成交量(实际上,这是一个tick计数器)或实际成交量(通常仅对交易所交易品种可用)进行运算。这两种类型都汇总在ENUM_APPLIED_VOLUME枚举中。

标识符描述
VOLUME_TICKTick成交量0
VOLUME_REAL交易量1

许多技术指标会对时间序列进行平滑(或平均)处理。终端支持四种最常见的平滑方法,在MQL5中使用ENUM_MA_METHOD枚举的元素来指定这些方法。

标识符描述
MODE_SMA简单平均0
MODE_EMA指数平均1
MODE_SMMA平滑平均2
MODE_LWMA线性加权平均3

对于随机指标,我们将在下一部分中考虑其示例,该指标有两种计算选项:按收盘价计算或按最高价/最低价计算。这些值在特殊的ENUM_STO_PRICE枚举中提供。

标识符描述
STO_LOWHIGH按最低价/最高价计算0
STO_CLOSECLOSE按收盘价/收盘价计算1

对于那些有多个缓冲区的指标,其缓冲区的用途和编号如下表所示。

指标常量描述
ADX, ADXW主线0
+DI线1
-DI线2
iAlligator鳄鱼颚线0
鳄鱼齿线1
鳄鱼唇线2
iBands基线0
上轨线1
下轨线2
iEnvelopes, iFractals上轨线0
下轨线1
iGator上直方图0
下直方图2
iIchimoku转换线(Tenkan-sen线)0
基准线(Kijun-sen线)1
先行上线(Senkou Span A线)2
先行下线(Senkou Span B线)3
延迟线(Chikou span线)4
iMACD, iRVI, iStochastic主线0
信号线1

所有指标的计算公式都在MetaTrader 5文档中给出。

有关调用指标函数的完整技术信息,包括源代码示例,可以在MQL5文档中找到。我们将在本书后面的部分中考虑一些示例。

使用内置指标

作为使用内置指标的一个简单入门示例,我们调用 iStochastic 函数。该指标函数的原型如下:

plaintext
int iStochastic(const string symbol, ENUM_TIMEFRAMES timeframe,
  int Kperiod, int Dperiod, int slowing,
  ENUM_MA_METHOD method, ENUM_STO_PRICE price)

可以看到,除了标准参数 symbol(交易品种)和 timeframe(时间周期)外,随机指标还有几个特定参数:

  • Kperiod:计算 %K 线所需的K线数量。
  • Dperiod:%D 线的一次平滑周期。
  • slowing:二次平滑周期(减速)。
  • method:平均(平滑)方法。
  • price:计算随机指标的方法。

我们尝试创建自己的指标 UseStochastic.mq5,它会将随机指标的值复制到自己的缓冲区中。由于随机指标有两个缓冲区,我们也预留两个:即“主”线和“信号”线。

plaintext
#property indicator_separate_window
#property indicator_buffers 2
#property indicator_plots   2
   
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrBlue
#property indicator_width1  1
#property indicator_label1  "St'Main"
   
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrChocolate
#property indicator_width2  1
#property indicator_label2  "St'Signal"
#property indicator_style2  STYLE_DOT

在输入变量中,我们提供所有必需的参数:

plaintext
input int KPeriod = 5;
input int DPeriod = 3;
input int Slowing = 3;
input ENUM_MA_METHOD Method = MODE_SMA;
input ENUM_STO_PRICE StochasticPrice = STO_LOWHIGH;

接下来,我们描述用于指标缓冲区的数组和一个用于描述符的全局变量:

plaintext
double MainBuffer[];
double SignalBuffer[];
   
int Handle;

我们将在 OnInit 函数中进行初始化:

plaintext
int OnInit()
{
   IndicatorSetString(INDICATOR_SHORTNAME,
      StringFormat("Stochastic(%d,%d,%d)", KPeriod, DPeriod, Slowing));
   // 将数组绑定为缓冲区
   SetIndexBuffer(0, MainBuffer);
   SetIndexBuffer(1, SignalBuffer);
   // 获取随机指标的描述符
   Handle = iStochastic(_Symbol, _Period,
      KPeriod, DPeriod, Slowing, Method, StochasticPrice);
   return Handle == INVALID_HANDLE ? INIT_FAILED : INIT_SUCCEEDED;
}

现在,在 OnCalculate 函数中,一旦句柄准备好,我们就需要使用 CopyBuffer 函数读取数据:

plaintext
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &data[])
{
   // 等待随机指标在所有K线上完成计算
   if(BarsCalculated(Handle) != rates_total)
   {
      return prev_calculated;
   }
   
   // 将数据复制到我们的两个缓冲区
   const int n = CopyBuffer(Handle, 0, 0, rates_total - prev_calculated + 1,
      MainBuffer);
   const int m = CopyBuffer(Handle, 1, 0, rates_total - prev_calculated + 1,
      SignalBuffer);
   
   return n > -1 && m > -1 ? rates_total : 0;
}

注意,我们调用了两次 CopyBuffer 函数,分别针对每个缓冲区(第二个参数为 0 和 1)。如果尝试读取一个不存在索引的缓冲区(例如 2),将会产生错误,并且我们将无法获取任何数据。

我们的指标并不是特别有用,因为它没有给原始随机指标添加任何内容,也没有分析其读数。另一方面,我们可以确保标准终端指标的线条和在 MQL5 中创建的线条是一致的(也可以像处理完全自定义指标那样轻松添加级别和精度设置,但那样就很难区分副本和原始指标了)。

标准随机指标和基于 iStochastic 函数的自定义指标

为了演示终端对指标的缓存功能,在 OnInit 函数中添加几行代码:

plaintext
   double array[];
   Print("This is very first copy of iStochastic with such settings=",
      !(CopyBuffer(Handle, 0, 0, 10, array) > 0));

这里我们利用了一个已知特性的技巧:指标创建后,需要一些时间进行计算,在获取句柄后立即从缓冲区读取数据是不可能的。这对于“冷启动”情况是适用的,即当具有指定参数的指标在终端内存的缓存中还不存在时。如果有现成的同类指标,那么我们可以立即访问缓冲区。

编译新指标后,应在同一交易品种和时间周期的两个图表上放置该指标的两个副本。第一次,日志中会显示带有 true 标志的消息(这是第一个副本),第二次(如果有多个图表,后续也是如此)则会显示 false。你也可以先手动将标准的“随机震荡指标”添加到图表中(使用默认设置或后续在 Use Stochastic 中应用的设置),然后运行 Use Stochastic,同样会得到 false

现在,让我们尝试基于标准指标设计一些有创意的东西。下面的指标 UseM1MA.mq5 用于在 M5 及更高时间周期(主要是日内)上计算每根 K 线的平均价格。它会累积每个特定工作(更高)时间周期 K 线时间戳范围内的 M1 K 线价格。这使得我们能够比标准价格类型(收盘价、开盘价、中间价、典型价、加权价等)更准确地估算 K 线的有效价格。此外,我们还考虑了在一定周期内对这些价格进行平均的可能性,但要注意,可能无法得到特别平滑的线条。

该指标将显示在主窗口中,并且包含一个缓冲区。可以使用 3 个参数更改设置:

plaintext
input uint _BarLimit = 100; // BarLimit
input uint BarPeriod = 1;
input ENUM_APPLIED_PRICE M1Price = PRICE_CLOSE;
  • BarLimit:设置用于计算的最近历史 K 线数量。这很重要,因为与分钟级的 M1 时间周期相比,高时间周期图表可能需要大量的 K 线(例如,在 24/7 交易中,一天的 D1 时间周期包含 1440 根 M1 K 线)。这可能会导致额外的数据下载和同步等待。在将此参数设置为 0(表示无限制处理)之前,先尝试使用节省资源的默认设置(工作时间周期的 100 根 K 线)。
  • 不过,即使将 BarLimit 设置为 0,指标可能也不会对更高时间周期的整个可见历史进行计算:如果终端对图表中的 K 线数量有限制,那么这也会影响对 M1 K 线的请求。换句话说,分析深度由允许的最大 M1 K 线数量对应的历史时间决定。
  • BarPeriod:设置进行平均计算的更高时间周期 K 线数量。默认值为 1,这样可以分别查看每根 K 线的有效价格。
  • M1Price 参数:指定用于 M1 K 线计算的价格类型。

在全局上下文中,描述了一个用于缓冲区的数组、一个描述符和一个自动更新标志,我们需要使用这个标志等待“外部”M1 时间周期的时间序列构建完成:

plaintext
double Buffer[];
   
int Handle;
int BarLimit;
bool PendingRefresh;
   
const string MyName = "M1MA (" + StringSubstr(EnumToString(M1Price), 6)
   + "," + (string)BarPeriod + "[" + (string)(PeriodSeconds() / 60) + "])";
const uint P = PeriodSeconds() / 60 * BarPeriod;

此外,这里还形成了指标的名称和平均周期 PPeriodSeconds 函数返回当前时间周期内一根 K 线的秒数,通过它可以计算出当前一根 K 线内包含的 M1 K 线数量:PeriodSeconds() / 60(M1 K 线的持续时间为 60 秒)。

通常的初始化在 OnInit 函数中完成:

plaintext
int OnInit()
{
   IndicatorSetString(INDICATOR_SHORTNAME, MyName);
   IndicatorSetInteger(INDICATOR_DIGITS, _Digits);
   
   SetIndexBuffer(0, Buffer);
   
   Handle = iMA(_Symbol, PERIOD_M1, P, 0, MODE_SMA, M1Price);
   
   return Handle != INVALID_HANDLE ? INIT_SUCCEEDED : INIT_FAILED;
}

为了获取更高时间周期 K 线的平均价格,我们使用简单移动平均线,以 MODE_SMA 模式调用 iMA 函数。

下面的 OnCalculate 函数进行了简化。首次运行或历史数据更改时,我们清空缓冲区并填充 BarLimit 变量(这是必需的,因为输入变量不能编辑,而我们希望将值 0 解释为可用于计算的最大 K 线数量)。在后续调用中,仅从 prev_calculated 开始,且不超过 BarLimit 的最后几根 K 线的缓冲区元素会被清空。

plaintext
int OnCalculate(ON_CALCULATE_STD_FULL_PARAM_LIST)
{
   if(prev_calculated == 0)
   {
      ArrayInitialize(Buffer, EMPTY_VALUE);
      if(_BarLimit == 0
      || _BarLimit > (uint)rates_total)
      {
         BarLimit = rates_total;
      }
      else
      {
         BarLimit = (int)_BarLimit;
      }
   }
   else
   {
      for(int i = fmax(prev_calculated - 1, (int)(rates_total - BarLimit));
         i < rates_total; ++i)
      {
         Buffer[i] = EMPTY_VALUE;
      }
   }

在从创建的 iMA 指标读取数据之前,需要等待数据准备好:为此,我们将 BarsCalculated 与 M1 K 线的数量进行比较。

plaintext
   if(BarsCalculated(Handle) != iBars(_Symbol, PERIOD_M1))
   {
      if(prev_calculated == 0)
      {
         EventSetTimer(1);
         PendingRefresh = true;
      }
      return prev_calculated;
   }
   ...

如果数据未准备好,我们启动一个定时器,以便在一秒后再次尝试读取数据。

接下来,我们进入算法的主要计算部分,因此如果定时器仍在运行,我们必须停止它。如果下一个报价事件比 1 秒来得更快,并且 M1 的 iMA 指标已经计算完成,就可能会出现这种情况。逻辑上,我们只需调用相应的函数 EventKillTimer。然而,它的行为有一个细微差别:它不会清空指标类型 MQL 程序的事件队列,如果定时器事件已经在队列中,那么 OnTimer 处理程序将被调用一次。为了避免图表不必要的更新,我们使用自己的变量 Pending Refresh 来控制这个过程,并在此处将其赋值为 false

plaintext
   ...
   Pending Refresh = false; // 数据已准备好,定时器将闲置
   ...

以下是 OnTimer 处理程序的组织方式:

plaintext
void OnTimer()
{
   EventKillTimer();
   if(PendingRefresh)
   {
      ChartSetSymbolPeriod(0, _Symbol, _Period);
   }
}

让我们回到 OnCalculate 函数,展示主要的工作流程:

plaintext
   for(int i = fmax(prev_calculated - 1, (int)(rates_total - BarLimit));
      i < rates_total; ++i)
   {
      static double result[1];
      
      // 获取与当前时间周期第 i 根 K 线对应的最后一根 M1 K 线
      const datetime dt = time[i] + PeriodSeconds() - 60;
      const int bar = iBarShift(_Symbol, PERIOD_M1, dt);
      
      if(bar > -1)
      {
         // 请求 M1 上的 MA 值
         if(CopyBuffer(Handle, 0, bar, 1, result) == 1)
         {
            Buffer[i] = result[0];
         }
         else
         {
            Print("CopyBuffer failed: ", _LastError);
            return prev_calculated;
         }
      }
   }
   
   return rates_total;
}

该指标的操作通过 EURUSD,H1 图表上的以下图像进行说明。蓝色线条对应默认设置,每个值是通过对 60 根 M1 K 线的收盘价进行平均得到的。橙色线条额外包含了 5 根 H1 K 线的平滑处理,使用的是 M1 的典型价格。

EURUSD,H1 上的两个 UseM1MA 指标实例

本书展示了 UseM1MASimple.mq5 的简化版本。我们没有详细介绍对最后一根(未完成)K 线的平均处理、空 K 线(M1 上没有数据的 K 线)的处理、PLOT_DRAW_BEGIN 属性的正确设置,以及在新 K 线出现时对平均计算短期滞后的控制。完整版本可在 UseM1MA.mq5 文件中找到。

作为基于标准指标构建指标的最后一个示例,让我们分析对 IndUnityPercent.mq5 指标的改进,该指标在“多货币和多时间周期指标”部分有介绍。第一个版本使用 CopyBuffer 函数获取收盘价进行计算。在新的 UseUnityPercentPro.mq5 版本中,我们将这种方法替换为读取 iMA 指标的数据。这将使我们能够实现新的功能:

  • 在给定周期内对价格进行平均。
  • 选择平均方法。
  • 选择用于计算的价格类型。

源代码的更改非常小。我们添加了 3 个新参数和一个用于 iMA 句柄的全局数组:

plaintext
input ENUM_APPLIED_PRICE PriceType = PRICE_CLOSE;
input ENUM_MA_METHOD PriceMethod = MODE_EMA;
input int PricePeriod = 1;
...   
int Handles[];

在辅助函数 InitSymbols 中(该函数从 OnInit 调用,用于解析包含工作交易品种列表的字符串),我们为新数组分配内存(其 SymbolCount 大小由列表确定):

plaintext
string InitSymbols()
{
   SymbolCount = StringSplit(Instruments, ',', Symbols);
   ...
   ArrayResize(Handles, SymbolCount);
   ArrayInitialize(Handles, INVALID_HANDLE);
   ...
   for(int i = 0; i < SymbolCount; i++)
   {
      ...
      Handles[i] = iMA(Symbols[i], PERIOD_CURRENT, PricePeriod, 0,
         PriceMethod, PriceType);
   }
}

在同一函数的末尾,我们将创建所需从属指标的描述符。

在进行主要计算的 Calculate 函数中,我们将以下形式的调用:

plaintext
CopyClose(Symbols[j], _Period, time0, time1, w);

替换为:

plaintext
CopyBuffer(Handles[j], 0, time0, time1, w); // 第 j 个句柄,第 0 个缓冲区

为了清晰起见,我们还在指标的短名称中补充了三个新参数:

plaintext
   IndicatorSetString(INDICATOR_SHORTNAME,
      StringFormat("Unity [%d] %s(%d,%s)", workCurrencies.getSize(),
      StringSubstr(EnumToString(PriceMethod), 5), PricePeriod,
      StringSubstr(EnumToString(PriceType), 6)));

以下是最终的结果:

UseUnityPercentPro 多交易品种指标与主要外汇货币对

这里展示了一个包含 8 种主要外汇货币的篮子(默认设置),对 11 根 K 线进行平均,并基于典型价格进行计算。两条粗线对应当前图表货币的相对价值:欧元用蓝色标记,美元用绿色标记。

创建指标的高级方法:IndicatorCreate

使用 iCustom 函数或构成内置指标集合的那些函数之一来创建指标,需要在编码阶段了解参数列表。然而,在实际应用中,常常需要编写足够灵活的程序,以便能够将一个指标替换为另一个指标。

例如,在测试器中优化智能交易系统(Expert Advisor)时,不仅选择移动平均线的周期是有意义的,而且选择其计算算法也是有必要的。当然,如果我们基于单个指标 iMA 构建算法,你可以在其 method 设置中提供指定 ENUM_MA_METHOD(移动平均方法枚举)的可能性。但有些人可能希望通过在双指数移动平均线、三指数移动平均线和分形移动平均线之间切换来扩大选择范围。乍一看,这可以通过 switch 语句分别调用 DEMAiTEMAiFrAMA 来实现。然而,如何将自定义指标包含在这个列表中呢?

虽然在 iCustom 调用中指标的名称很容易被替换,但参数列表可能会有很大差异。一般来说,一个智能交易系统可能需要基于任意指标的组合来生成信号,而这些指标并不一定是预先知道的,而且不仅仅局限于移动平均线。

对于这种情况,MQL5 有一种通用方法,即使用 IndicatorCreate 函数来创建任意技术指标。

plaintext
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 获取关于(图表上的)第三方指标参数的信息而设计的。

plaintext
struct MqlParam 
{ 
   ENUM_DATATYPE type;          // 输入参数类型
   long          integer_value; // 用于存储整数值的字段
   double        double_value;  // 用于存储双精度或浮点数值的字段
   string        string_value;  // 用于存储字符串类型值的字段
};

参数的实际值必须根据第一个 type 字段的值,设置在 integer_valuedouble_valuestring_value 字段之一中。反过来,type 字段使用 ENUM_DATATYPE 枚举来描述,该枚举包含所有内置 MQL5 类型的标识符。

标识符数据类型
TYPE_BOOLbool
TYPE_CHARchar
TYPE_UCHARuchar
TYPE_SHORTshort
TYPE_USHORTushort
TYPE_COLORcolor
TYPE_INTint
TYPE_UINTuint
TYPE_DATETIMEdatetime
TYPE_LONGlong
TYPE_ULONGulong
TYPE_FLOATfloat
TYPE_DOUBLEdouble
TYPE_STRINGstring

如果任何指标参数具有枚举类型,你应该在 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自定义指标(Custom indicator)
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 值作为指标类型,那么 parameters 数组的第一个元素的 type 字段必须具有 TYPE_STRING 值,并且 string_value 字段必须包含自定义指标的名称(路径)。

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

回顾一下,为了测试创建自定义指标的 MQL 程序(在编译阶段指标名称未知,使用 IndicatorCreate 时也是这种情况),你必须使用指令显式绑定它们:

plaintext
#property tester_indicator "indicator_name.ex5"

这允许测试器将所需的辅助指标发送给测试代理,但将该过程限制为仅预先知道的指标。

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

UseEnvelopesParams1.mq5 的第一个示例创建了一个包络线(Envelopes)指标的嵌入式副本。为此,我们描述了两个缓冲区、两个绘图、用于它们的数组,以及重复 iEnvelopes 参数的输入参数。

plaintext
#property indicator_chart_window
#property indicator_buffers 2
#property indicator_plots   2

// 绘图设置
#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; // 偏差,%

double UpBuffer[];
double DownBuffer[];

int Handle; // 从属指标的句柄

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

plaintext
int OnInit()
{
   SetIndexBuffer(0, UpBuffer);
   SetIndexBuffer(1, DownBuffer);

   Handle = iEnvelopes(WorkPeriod, Shift, Method, Price, Deviation);
   return Handle == INVALID_HANDLE? INIT_FAILED : INIT_SUCCEEDED;
}

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

plaintext
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, _Period, IND_ENVELOPES,
      ArraySize(params), params);
   return Handle == INVALID_HANDLE? INIT_FAILED : INIT_SUCCEEDED;
}

获得句柄后,我们在 OnCalculate 函数中使用它来填充其两个缓冲区。

plaintext
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)的概念。借助 RTTI,程序可以在运行时询问关于其组成部分的描述性元数据,包括变量、结构体、类、函数等。MQL5 中可以归类为 RTTI 的几个内置特性是 typename 运算符和 offsetof 运算符。因为 typename 返回类型名称作为字符串,所以让我们基于字符串构建我们的类型自动检测器(请参阅文件 RTTI.mqh)。

plaintext
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 = 0; i < 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 变量,n 变量将包含要填充的最后一个元素的索引。

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

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

正是在这个方法中,我们增加数组的大小,获取用于填充的工作索引 n,并立即重置这个第 n 个结构体。

plaintext
...
public:
   template<typename T>
   MqlParamBuilder *operator<<(T v)
   {
 // 扩展数组
      n = ArraySize(array);
      ArrayResize(array, n + 1);
      ZeroMemory(array[n]);
      ...
      return &this;
   }

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

plaintext
      ...
      // 定义值的类型
      array[n].type = rtti(v);
      if(array[n].type == 0) array[n].type = TYPE_INT; // 意味着是枚举类型
      ...

现在我们只需要将 v 值放入结构体的三个字段之一:long 类型的 integer_value 字段(注意,long 是长整数,因此该字段以此命名)、double 类型的 `

使用IndicatorCreate灵活创建指标

在了解了创建指标的新方法之后,让我们来处理一个更接近实际的任务。IndicatorCreate通常用于事先不知道被调用指标的情况。例如,在编写能够根据用户配置的任意信号进行交易的通用智能交易系统时,就会产生这样的需求。甚至指标的名称也可以由用户设置。

我们还没有准备好开发智能交易系统,因此我们将通过一个包装指标UseDemoAll.mq5的示例来研究这项技术,该指标能够显示任何其他指标的数据。

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

这些只是UseDemoAll的基本设置,但并非所有可能的设置。我们稍后将讨论其他设置。

让我们以ENUM_INDICATOR枚举为基础来解决这个问题:它已经包含了所有类型指标的元素,包括自定义指标(IND_CUSTOM)。说实话,单纯从这个枚举本身来看,由于几个原因,它并不适用。首先,无法从中获取关于特定指标的元数据,例如参数的数量和类型、缓冲区的数量,以及指标显示在哪个窗口(主窗口或子窗口)。这些信息对于正确创建和可视化指标很重要。其次,如果我们定义一个ENUM_INDICATOR类型的输入变量,以便用户可以选择所需的指标,在属性对话框中,这将表现为一个下拉列表,其中的选项仅包含元素的名称。实际上,希望在这个列表中为用户提供提示(至少是关于参数的提示)。因此,我们将描述自己的枚举IndicatorType。回想一下,MQL5允许为每个元素在右侧指定一个注释,该注释会显示在界面中。

在IndicatorType枚举的每个元素中,我们不仅要对来自ENUM_INDICATOR的相应标识符(ID)进行编码,还要对参数数量(P)、缓冲区数量(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(0, 0, 0, IND_CUSTOM), // {iCustom}(...)[?]
   
   iAC_ = MAKE_IND(0, 1, 1, IND_AC), // iAC( )[1]*
   iAD_volume = MAKE_IND(1, 1, 1, IND_AD), // iAD(volume)[1]*
   iADX_period = MAKE_IND(1, 3, 1, IND_ADX), // iADX(period)[3]*
   iADXWilder_period = MAKE_IND(1, 3, 1, IND_ADXW), // iADXWilder(period)[3]*
   ...
   iMomentum_period_price = MAKE_IND(2, 1, 1, IND_MOMENTUM), // iMomentum(period,price)[1]*
   iMFI_period_volume = MAKE_IND(2, 1, 1, IND_MFI), // iMFI(period,volume)[1]*
   iMA_period_shift_method_price = MAKE_IND(4, 1, 0, IND_MA), // iMA(period,shift,method,price)[1]
   iMACD_fast_slow_signal_price = MAKE_IND(4, 2, 1, IND_MACD), // iMACD(fast,slow,signal,price)[2]*
   ...
   iTEMA_period_shift_price = MAKE_IND(3, 1, 0, IND_TEMA), // iTEMA(period,shift,price)[1]
   iVolumes_volume = MAKE_IND(1, 1, 1, IND_VOLUMES), // iVolumes(volume)[1]*
   iWPR_period = MAKE_IND(1, 1, 1, IND_WPR) // iWPR(period)[1]*
};

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

参数列表尤为重要,因为用户需要在为此保留的输入变量中输入相应的用逗号分隔的值。我们也可以显示参数的类型,但为了简单起见,决定只保留有意义的名称,从这些名称中也可以推断出类型。例如,period、fast、slow是表示周期(柱线数量)的整数,method是平均方法ENUM_MA_METHOD,price是价格类型ENUM_APPLIED_PRICE,volume是成交量类型ENUM_APPLIED_VOLUME。

为了方便用户(无需记住枚举元素的值),程序将支持所有枚举的名称。特别是,sma标识符表示MODE_SMA,ema表示MODE_EMA,依此类推。Price 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;       // 选定的指标类型
   string symbols;          // 工作交易品种(可选)
   ENUM_TIMEFRAMES tf;      // 工作时间框架(可选)
   MqlParamBuilder builder; // 参数数组的“构建器”
   int handle;              // 指标句柄
   string name;             // 自定义指标名称
   ...
public:
   AutoIndicator(const IndicatorType t, const string custom, const string parameters,
      const string s = NULL, const 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));
      // 将字符串拆分为参数数组(在构建器内部形成)
      parseParameters(parameters);
      // 创建并存储句柄
      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 = 0; i < n; i++)
      {
         // 字符串规范化(去除空格,转换为小写)
         StringTrimLeft(sparams[i]);
         StringTrimRight(sparams[i]);
         StringToLower(sparams[i]);
   
         if(StringGetCharacter(sparams[i], 0) == '"'
         && StringGetCharacter(sparams[i], StringLen(sparams[i]) - 1) == '"')
         {
            // 引号内的所有内容都被视为字符串
            builder << StringSubstr(sparams[i], 1, StringLen(sparams[i]) - 2);
         }
         else
         {
            string part[];
            int p = StringSplit(sparams[i], '.', part);
            if(p == 2) // 双精度浮点数/浮点数
            {
               builder << StringToDouble(sparams[i]);
            }
            else if(p == 3) // 日期时间
            {
               builder << StringToTime(sparams[i]);
            }
            else if(sparams[i] == "true")
            {
               builder << true;
            }
            else if(sparams[i] == "false")
            {
               builder << false;
            }
            else // 整数
            {
               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[];
      // 用“builder”对象收集的参数填充“p”数组
      builder >> p;
      
      if(type == iCustom_)
      {
         // 在数组最开头插入自定义指标的名称
         ArraySetAsSeries(p, true);
         const int n = ArraySize(p);
         ArrayResize(p, n + 1);
         p[n].type = TYPE_STRING;
         p[n].string_value = name;
         ArraySetAsSeries(p, false);
      }
      
      return IndicatorCreate(symbol, tf, IND_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 > 0) return StringSubstr(s, 0, p);
         return s;
      }
      return name;
   }
};

现在,一切都准备就绪,可以直接进入UseDemoAll.mq5的源代码了。但让我们从一个稍微简化的版本UseDemoAllSimple.mq5开始。

首先,让我们定义指标缓冲区的数量。由于内置指标中缓冲区的最大数量是五个(对于一目均衡表指标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属性的指标缓冲区中的数据复制到显示在主窗口中的缓冲区中没有障碍。然而,应该记住,此类指标的值范围通常与价格刻度不一致,因此不太可能在图表上看到它们(线条会在可见区域的上方或下方很远的地方),尽管这些值仍然会输出到数据窗口中。

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

input IndicatorType IndicatorSelector = iMA_period_shift_method_price; // 内置指标选择器
input string IndicatorCustom = ""; // 自定义指标名称
input string IndicatorParameters = "11,0,sma,close"; // 指标参数(用逗号分隔的列表)
input ENUM_DRAW_TYPE DrawType = DRAW_LINE; // 绘制类型
input int DrawLineWidth = 1; // 绘制线条宽度

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

int Handle;

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

#include <MQL5Book/AutoIndicator.mqh>
   
int OnInit()
{
   AutoIndicator indicator(IndicatorSelector, IndicatorCustom, IndicatorParameters);
   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] = {clrBlue, clrGreen, clrRed, clrCyan, clrMagenta};
   const string s = indicator.getName();
   const int n = (IndicatorSelector != iCustom_) ? IND_BUFFERS(IndicatorSelector) : BUF_NUM;
   ...

在循环中,我们将设置图表的属性,同时考虑限制n:高于该限制的缓冲区将被隐藏。

   for(int i = 0; i < BUF_NUM; ++i)
   {
      PlotIndexSetString(i, PLOT_LABEL, s + "[" + (string)i + "]");
      PlotIndexSetInteger(i, PLOT_DRAW_TYPE, i < n ? DrawType : DRAW_NONE);
      PlotIndexSetInteger(i, PLOT_LINE_WIDTH, DrawLineWidth);
      PlotIndexSetInteger(i, PLOT_LINE_COLOR, defColors[i]);
      PlotIndexSetInteger(i, PLOT_SHOW_DATA, i < n);
   }
   
   Comment("DemoAll: ", (IndicatorSelector == iCustom_ ? IndicatorCustom : s),
      "(", IndicatorParameters, ")");
   
   return INIT_SUCCEEDED;
}

图表上指标管理函数概述

正如我们已经了解到的,指标是一种 MQL 程序,它将计算部分和可视化部分结合在一起。计算在内部进行,用户无法察觉,但可视化需要与图表关联。这就是为什么指标与图表密切相关,并且 MQL5 API 甚至包含一组用于管理图表上指标的函数。我们将在关于图表的章节中更详细地讨论这些函数,在这里我们仅列出它们。

函数用途
ChartWindowFind返回包含当前指标或具有给定名称指标的子窗口编号
ChartIndicatorAdd将具有指定句柄的指标添加到指定的图表窗口中
ChartIndicatorDelete从指定的图表窗口中删除具有指定名称的指标
ChartIndicatorGet返回指定图表窗口上具有指定短名称的指标句柄
ChartIndicatorName通过指定图表窗口上指标列表中的编号返回指标的短名称
ChartIndicatorsTotal返回附加到指定图表窗口的所有指标的数量

在下一节“在主窗口和辅助窗口中组合信息输出”中,我们将看到一个 UseDemoAll.mq5 的示例,该示例使用了其中一些函数。

主窗口和辅助窗口的输出组合

让我们回到在主窗口和子窗口中显示同一指标图形的问题上,因为在开发示例 UseDemoAllSimple.mq5 时我们遇到过这个问题。专门为单独窗口设计的指标不适合在主图表上进行可视化,而为主窗口设计的指标又没有额外的窗口。有几种替代方法:

  1. 实现父指标:为单独窗口实现一个父指标,在那里显示图表,并在主窗口中使用它以图形对象的形式显示数据。这种方法不太好,因为无法像读取时间序列那样读取对象中的数据,而且大量对象会消耗额外的资源。
  2. 开发虚拟面板:为主窗口开发自己的虚拟面板(类),并在正确的比例尺下,在其中呈现本应显示在子窗口中的时间序列。
  3. 使用多个指标:使用多个指标,至少一个用于主窗口,一个用于子窗口,并通过共享内存(需要 DLL)、资源或数据库在它们之间交换数据。
  4. 重复计算:在主窗口和子窗口的指标中重复进行计算(使用相同的源代码)。

我们将介绍其中一种超出单个 MQL 程序范畴的解决方案:我们需要一个具有 indicator_separate_window 属性的额外指标。实际上,我们已经通过请求句柄创建了它的计算部分,只需要以某种方式将其显示在单独的子窗口中。

在新的(完整)版本的 UseDemoAll.mq5 中,我们将分析在相应的 IndicatorType 枚举元素中请求创建的指标的元数据。请记住,其中对每种内置指标的工作窗口进行了编码。当一个指标需要单独的窗口时,我们将使用专门的 MQL5 函数来创建一个,这些函数我们稍后会学习。

对于自定义指标,无法获取其工作窗口的信息。因此,我们添加 IndicatorCustomSubwindow 输入变量,用户可以在其中指定是否需要子窗口。

plaintext
input bool IndicatorCustomSubwindow = false; // 自定义指标子窗口

OnInit 函数中,我们隐藏用于子窗口的缓冲区。

plaintext
int OnInit()
{
   ...
   const bool subwindow = (IND_WINDOW(IndicatorSelector) > 0)
      || (IndicatorSelector == iCustom_ && IndicatorCustomSubwindow);
   for(int i = 0; i < BUF_NUM; ++i)
   {
      ...
      PlotIndexSetInteger(i, PLOT_DRAW_TYPE,
         i < n && !subwindow ? DrawType : DRAW_NONE);
   }
   ...
}

完成此设置后,我们需要使用几个不仅适用于指标操作,还适用于图表操作的函数。我们将在相应的章节中详细研究它们,而在前一节中已经进行了初步介绍。

其中一个函数 ChartIndicatorAdd 允许将由句柄指定的指标添加到窗口中,不仅可以添加到主窗口,还可以添加到子窗口。我们将在图表相关章节中讨论图表标识符和窗口编号,目前只需知道下一次调用 ChartIndicatorAdd 函数会将带有句柄的指标添加到当前图表的新子窗口中。

plaintext
int handle = ...// 获取指标句柄,通过 iCustom 或 IndicatorCreate

// 设置当前图表(0)
// |
// |     设置窗口编号为当前窗口总数
// |                          |
// |                          | 传递描述符
// |                          |                       |
// v                          v                       v
ChartIndicatorAdd(  0, (int)ChartGetInteger(0, CHART_WINDOWS_TOTAL), handle);

了解到这种可能性后,我们可以考虑调用 ChartIndicatorAdd 函数,并将已准备好的从属指标的句柄传递给它。

我们需要的第二个函数是 ChartIndicatorName。它通过指标的句柄返回其短名称。这个名称对应于指标代码中设置的 INDICATOR_SHORTNAME 属性,可能与文件名不同。在删除或重新配置父指标后,需要这个名称来清理工作,即移除辅助指标及其子窗口。

plaintext
string subTitle = "";

int OnInit()
{
   ...
   if(subwindow)
   {
      // 在子窗口中显示新指标
      const int w = (int)ChartGetInteger(0, CHART_WINDOWS_TOTAL);
      ChartIndicatorAdd(0, w, Handle);
      // 保存名称,以便在 OnDeinit 中移除指标
      subTitle = ChartIndicatorName(0, w, 0);
   }
   ...
}

OnDeinit 处理程序中,我们使用保存的 subTitle 来调用另一个我们稍后会学习的函数 — ChartIndicatorDelete。它会从图表中移除最后一个参数指定名称的指标。

plaintext
void OnDeinit(const int)
{
   Print(__FUNCSIG__, (StringLen(subTitle) > 0 ? " deleting " + subTitle : ""));
   if(StringLen(subTitle) > 0)
   {
      ChartIndicatorDelete(0, (int)ChartGetInteger(0, CHART_WINDOWS_TOTAL) - 1,
         subTitle);
   }
}

这里假设只有我们的指标在图表上运行,并且仅运行一个实例。在更一般的情况下,应该分析所有子窗口以进行正确删除,但这需要图表章节中介绍的更多函数,所以目前我们先采用这种简单的版本。

如果现在运行 UseDemoAll 并从列表中选择带有星号标记的指标(即需要子窗口的指标),例如相对强弱指数(RSI),我们将看到预期的结果:RSI 显示在单独的窗口中。

从有偏移的图表中读取数据

我们的新指标UseDemoAll几乎已经准备就绪。我们只需要再考虑一个要点。

在从属指标中,一些图表可以通过PLOT_SHIFT属性设置偏移量。例如,当偏移量为正数时,时间序列元素会向未来偏移,并显示在索引为0的柱线的右侧。奇怪的是,它们的索引是负数。随着向右移动,数字越来越小:-1,-2,-3,等等。这种寻址方式也会影响CopyBuffer函数。当我们使用CopyBuffer的第一种形式时,偏移参数设置为0表示时间序列中当前时间的元素。但是,如果时间序列本身向右偏移,我们将从编号为N的元素开始获取数据,其中N是源指标中的偏移值。同时,位于我们缓冲区中索引N右侧的元素将不会被数据填充,并且其中会保留“无效数据”。

为了演示这个问题,让我们从一个没有偏移的指标开始:很棒振荡器(Awesome Oscillator)非常符合这个要求。回想一下,UseDemoAll会将所有值复制到它的数组中,尽管由于不同的价格刻度和指标读数,这些值在图表上不可见,但我们可以通过数据窗口进行检查。无论我们在图表上把鼠标光标移动到哪里,数据窗口子窗口中的指标值和UseDemoAll缓冲区中的指标值都会匹配。例如,在下面的图片中,你可以清楚地看到,在16:00的小时柱线上,两个值都等于0.001797。

UseDemoAll缓冲区中的AO指标数据

UseDemoAll缓冲区中的AO指标数据

现在,在UseDemoAll的设置中,我们选择iGator(鳄鱼振荡器)指标。为了简单起见,清空鳄鱼指标参数的字段,这样它将使用默认参数构建。在这种情况下,直方图偏移量是5根柱线(向前),这在图表上可以清楚地看到。

未对未来偏移进行修正的UseDemoAll缓冲区中的鳄鱼指标数据

未对未来偏移进行修正的UseDemoAll缓冲区中的鳄鱼指标数据

黑色垂直线标记的是16:00的小时柱线。然而,数据窗口中的鳄鱼指标值和我们从同一指标读取到的数组中的值是不同的。UseDemoAll用黄色突出显示了包含无效数据的缓冲区。

如果我们检查向过去移动5根柱线的数据,即在11:00(橙色垂直线),我们会发现那里是鳄鱼指标在16:00输出的值。上下直方图的成对正确值分别用绿色和粉色突出显示。

为了解决这个问题,我们必须在UseDemoAll中添加一个输入变量,让用户指定图表偏移量,然后在调用CopyBuffer时对其进行修正。

input int IndicatorShift = 0; // 图表偏移量
...
int OnCalculate(ON_CALCULATE_STD_SHORT_PARAM_LIST)
{
   ...
   for(int k = 0; k < m; ++k)
   {
      const int n = buffers[k].copy(Handle, k,
         -IndicatorShift, rates_total - prev_calculated + 1);
      ...
   }
}

不幸的是,从MQL5中无法找到第三方指标的PLOT_SHIFT属性。

让我们检查一下引入5的偏移量是如何解决鳄鱼指标(使用默认设置)的情况的。

对未来偏移进行调整后UseDemoAll缓冲区中的鳄鱼指标数据

对未来偏移进行调整后UseDemoAll缓冲区中的鳄鱼指标数据

现在,UseDemoAll在16:00柱线上的读数对应于来自鳄鱼指标的、来自虚拟未来5根柱线之后的实际数据(21:00处的淡紫色垂直线)。

你可能会想,为什么在鳄鱼指标窗口中只显示了2个缓冲区,而我们的有4个。关键在于,鳄鱼指标的彩色直方图使用了一个额外的缓冲区进行颜色编码。但只有两种颜色,红色和绿色,我们在数组中看到它们的值为0或1。

删除指标实例:IndicatorRelease

正如本章引言部分所述,终端会为每个创建的指标维护一个引用计数器,只要至少有一个 MQL 程序或图表在使用该指标,它就会保持运行状态。在 MQL 程序中,对指标的需求标志是一个有效的句柄。通常,我们在初始化时请求一个句柄,并在程序运行期间的算法中使用它,直到程序结束。

当程序卸载时,所有创建的唯一句柄会自动释放,即它们的计数器减 1(如果计数器达到零,这些指标也会从内存中卸载)。因此,无需显式释放句柄。

然而,在程序运行过程中,有时子指标会变得不再需要。此时,无用的指标会继续消耗资源。因此,必须使用 IndicatorRelease 显式释放句柄。

plaintext
bool IndicatorRelease(int handle)

该函数会删除指定的指标句柄,如果没有其他程序使用该指标,还会卸载该指标本身。卸载操作会有轻微延迟。

函数返回操作成功(true)或出错(false)的指示。

调用 IndicatorRelease 后,传递给它的句柄将变得无效,尽管变量本身仍保留其先前的值。尝试在其他指标函数(如 CopyBuffer)中使用这样的句柄将失败,并返回错误代码 4807(ERR_INDICATOR_WRONG_HANDLE)。为避免误解,最好在释放句柄后立即将 INVALID_HANDLE 值赋给相应的变量。

但是,如果程序随后为新指标请求句柄,该句柄很可能与先前释放的句柄具有相同的值,但现在将与新指标的数据关联。

在策略测试器中工作时,IndicatorRelease 函数不会执行。

为了演示 IndicatorRelease 的应用,我们准备一个特殊版本的 UseDemoAllLoop.mq5,它将周期性地从列表中循环重新创建一个辅助指标,该列表仅包含主窗口的指标(为了清晰起见)。

plaintext
IndicatorType MainLoop[] =
{
   iCustom_,
   iAlligator_jawP_jawS_teethP_teethS_lipsP_lipsS_method_price,
   iAMA_period_fast_slow_shift_price,
   iBands_period_shift_deviation_price,
   iDEMA_period_shift_price,
   iEnvelopes_period_shift_method_price_deviation,
   iFractals_,
   iFrAMA_period_shift_price,
   iIchimoku_tenkan_kijun_senkou,
   iMA_period_shift_method_price,
   iSAR_step_maximum,
   iTEMA_period_shift_price,
   iVIDyA_momentum_smooth_shift_price,
};
   
const int N = ArraySize(MainLoop);
int Cursor = 0; // MainLoop 数组中的当前位置
      
const string IndicatorCustom = "LifeCycle";

数组的第一个元素包含一个自定义指标 LifeCycle,这是个例外,它来自“不同类型程序启动和停止的特点”部分。虽然这个指标不显示任何线条,但它在这里很合适,因为当调用其 OnInit/OnDeinit 处理程序时,它会在日志中显示消息,这将使我们能够跟踪其生命周期。其他指标的生命周期类似。

在输入变量中,我们只保留渲染设置。DRAW_ARROW 标签的默认输出对于显示不同类型的指标是最优的。

plaintext
input ENUM_DRAW_TYPE DrawType = DRAW_ARROW; // 绘图类型
input int DrawLineWidth = 1; // 绘图线宽

为了“动态”重新创建指标,我们在 OnInit 中启动一个 5 秒的定时器,并将整个先前的初始化(有一些下面描述的修改)移到 OnTimer 处理程序中。

plaintext
int OnInit()
{
   Comment("Wait 5 seconds to start looping through indicator set");
   EventSetTimer(5);
   return INIT_SUCCEEDED;
}
   
IndicatorType IndicatorSelector; // 当前选择的指标类型
   
void OnTimer()
{
   if(Handle != INVALID_HANDLE && ClearHandles)
   {
      IndicatorRelease(Handle);
      /*
      // 句柄仍然是 10,但不再有效
      // 如果我们取消注释该片段,将得到以下错误
      double data[1];
      const int n = CopyBuffer(Handle, 0, 0, 1, data);
      Print("Handle=", Handle, " CopyBuffer=", n, " Error=", _LastError);
      // Handle=10 CopyBuffer=-1 Error=4807 (ERR_INDICATOR_WRONG_HANDLE)
      */
   }
   IndicatorSelector = MainLoop[Cursor];
   Cursor = ++Cursor % N;
   
   // 使用默认参数创建句柄
   // (因为我们在构造函数的第三个参数中传递了空字符串)
   AutoIndicator indicator(IndicatorSelector,
      (IndicatorSelector == iCustom_ ? IndicatorCustom : ""), "");
   Handle = indicator.getHandle();
   if(Handle == INVALID_HANDLE)
   {
      Print(StringFormat("Can't create indicator: %s",
         _LastError ? E2S(_LastError) : "The name or number of parameters is incorrect"));
   }
   else
   {
      Print("Handle=", Handle);
   }
   
   buffers.empty(); // 清空缓冲区,因为将显示新的指标
   ChartSetSymbolPeriod(0,NULL,0); // 请求完全重绘
   ...
   // 图表的进一步设置 - 与之前类似
   ...
   Comment("DemoAll: ", (IndicatorSelector == iCustom_ ? IndicatorCustom : s),
      "(default-params)");
}

主要区别在于,当前创建的指标类型 IndicatorSelector 现在不是由用户设置,而是从 MainLoop 数组中按 Cursor 索引顺序选择。每次调用定时器时,该索引会循环增加,即当到达数组末尾时,我们会跳转到数组开头。

对于所有指标,参数行都是空的。这样做是为了统一它们的初始化。结果,每个指标将使用其自身的默认值创建。

OnTimer 处理程序的开头,我们为前一个句柄调用 IndicatorRelease。然而,我们提供了一个输入变量 ClearHandles 来禁用给定的 if 语句分支,并查看如果不清理句柄会发生什么。

plaintext
input bool ClearHandles = true;

默认情况下,ClearHandles 等于 true,即指标将按预期被删除。

最后,另一个额外的设置是清空缓冲区和请求图表完全重绘的代码行。这两者都是必需的,因为我们替换了提供显示数据的从属指标。

OnCalculate 处理程序没有改变。

让我们使用默认设置运行 UseDemoAllLoop。日志中将出现以下条目(仅显示开头部分):

plaintext
UseDemoAllLoop (EURUSD,H1) Initializing LifeCycle() EURUSD, PERIOD_H1
UseDemoAllLoop (EURUSD,H1) Handle=10
LifeCycle      (EURUSD,H1) Loader::Loader()
LifeCycle      (EURUSD,H1) void OnInit() 0 DEINIT_REASON_PROGRAM
UseDemoAllLoop (EURUSD,H1) Initializing iAlligator_jawP_jawS_teethP_teethS_lipsP_lipsS_method_price() EURUSD, PERIOD_H1
UseDemoAllLoop (EURUSD,H1) iAlligator_jawP_jawS_teethP_teethS_lipsP_lipsS_method_price requires 8 parameters, 0 given
UseDemoAllLoop (EURUSD,H1) Handle=10
LifeCycle      (EURUSD,H1) void OnDeinit(const int) DEINIT_REASON_REMOVE
LifeCycle      (EURUSD,H1) Loader::~Loader()
UseDemoAllLoop (EURUSD,H1) Initializing iAMA_period_fast_slow_shift_price() EURUSD, PERIOD_H1
UseDemoAllLoop (EURUSD,H1) iAMA_period_fast_slow_shift_price requires 5 parameters, 0 given
UseDemoAllLoop (EURUSD,H1) Handle=10
UseDemoAllLoop (EURUSD,H1) Initializing iBands_period_shift_deviation_price() EURUSD, PERIOD_H1
UseDemoAllLoop (EURUSD,H1) iBands_period_shift_deviation_price requires 4 parameters, 0 given
UseDemoAllLoop (EURUSD,H1) Handle=10
...

注意,每次我们都会得到相同的句柄“编号”(10),因为我们在创建新句柄之前释放了旧句柄。

同样重要的是,LifeCycle 指标在我们释放它后不久就被卸载了(假设它没有被单独添加到同一个图表中,因为那样它的引用计数不会重置为零)。

下图显示了我们的指标渲染鳄鱼指标(Alligator)数据的时刻。

UseDemoAllLoop 在鳄鱼指标演示步骤中

如果将 ClearHandles 的值更改为 false,我们将在日志中看到完全不同的情况。句柄编号现在将不断增加,这表明指标仍留在终端中并继续运行,白白消耗资源。特别是,不会收到来自 LifeCycle 指标的反初始化消息。

plaintext
UseDemoAllLoop (EURUSD,H1) Initializing LifeCycle() EURUSD, PERIOD_H1
UseDemoAllLoop (EURUSD,H1) Handle=10
LifeCycle      (EURUSD,H1) Loader::Loader()
LifeCycle      (EURUSD,H1) void OnInit() 0 DEINIT_REASON_PROGRAM
UseDemoAllLoop (EURUSD,H1) Initializing iAlligator_jawP_jawS_teethP_teethS_lipsP_lipsS_method_price() EURUSD, PERIOD_H1
UseDemoAllLoop (EURUSD,H1) iAlligator_jawP_jawS_teethP_teethS_lipsP_lipsS_method_price requires 8 parameters, 0 given
UseDemoAllLoop (EURUSD,H1) Handle=11
UseDemoAllLoop (EURUSD,H1) Initializing iAMA_period_fast_slow_shift_price() EURUSD, PERIOD_H1
UseDemoAllLoop (EURUSD,H1) iAMA_period_fast_slow_shift_price requires 5 parameters, 0 given
UseDemoAllLoop (EURUSD,H1) Handle=12
UseDemoAllLoop (EURUSD,H1) Initializing iBands_period_shift_deviation_price() EURUSD, PERIOD_H1
UseDemoAllLoop (EURUSD,H1) iBands_period_shift_deviation_price requires 4 parameters, 0 given
UseDemoAllLoop (EURUSD,H1) Handle=13
UseDemoAllLoop (EURUSD,H1) Initializing iDEMA_period_shift_price() EURUSD, PERIOD_H1
UseDemoAllLoop (EURUSD,H1) iDEMA_period_shift_price requires 3 parameters, 0 given
UseDemoAllLoop (EURUSD,H1) Handle=14
UseDemoAllLoop (EURUSD,H1) Initializing iEnvelopes_period_shift_method_price_deviation() EURUSD, PERIOD_H1
UseDemoAllLoop (EURUSD,H1) iEnvelopes_period_shift_method_price_deviation requires 5 parameters, 0 given
UseDemoAllLoop (EURUSD,H1) Handle=15
...
UseDemoAllLoop (EURUSD,H1) Initializing iVIDyA_momentum_smooth_shift_price() EURUSD, PERIOD_H1
UseDemoAllLoop (EURUSD,H1) iVIDyA_momentum_smooth_shift_price requires 4 parameters, 0 given
UseDemoAllLoop (EURUSD,H1) Handle=22
UseDemoAllLoop (EURUSD,H1) Initializing LifeCycle() EURUSD, PERIOD_H1
UseDemoAllLoop (EURUSD,H1) Handle=10
UseDemoAllLoop (EURUSD,H1) Initializing iAlligator_jawP_jawS_teethP_teethS_lipsP_lipsS_method_price() EURUSD, PERIOD_H1
UseDemoAllLoop (EURUSD,H1) iAlligator_jawP_jawS_teethP_teethS_lipsP_lipsS_method_price requires 8 parameters, 0 given
UseDemoAllLoop (EURUSD,H1) Handle=11
UseDemoAllLoop (EURUSD,H1) Initializing iAMA_period_fast_slow_shift_price() EURUSD, PERIOD_H1
UseDemoAllLoop (EURUSD,H1) iAMA_period_fast_slow_shift_price requires 5 parameters, 0 given
UseDemoAllLoop (EURUSD,H1) Handle=12
UseDemoAllLoop (EURUSD,H1) Initializing iBands_period_shift_deviation_price() EURUSD, PERIOD_H1
UseDemoAllLoop (EURUSD,H1) iBands_period_shift_deviation_price requires 4 parameters, 0 given
UseDemoAllLoop (EURUSD,H1) Handle=13
UseDemoAllLoop (EURUSD,H1) Initializing iDEMA_period_shift_price() EURUSD, PERIOD_H1
UseDemoAllLoop (EURUSD,H1) iDEMA_period_shift_price requires 3 parameters, 0 given
UseDemoAllLoop (EURUSD,H1) Handle=14
UseDemoAllLoop (EURUSD,H1) void OnDeinit(const int)
...

当指标类型数组循环中的索引到达最后一个元素并从开头重新开始时,终端将开始向我们的代码返回已存在指标的句柄(相同的值:句柄 22 之后又是 10)。

通过句柄获取指标设置

有时候,MQL 程序需要了解正在运行的指标实例的参数。这些指标可以是图表上的第三方指标,或者是从主程序传递到库文件或头文件的句柄所对应的指标。为此,MQL5 提供了 IndicatorParameters 函数。

plaintext
int IndicatorParameters(int handle, ENUM_INDICATOR &type, MqlParam &params[])

该函数通过指定的句柄,返回指标输入参数的数量,以及它们的类型和值。

如果成功,函数会填充传递给它的 params 数组,并将指标类型保存在 type 参数中。

如果出现错误,函数返回 -1。

作为使用此函数的一个示例,我们来改进在“删除指标实例”部分中介绍的指标 UseDemoAllLoop.mq5。我们将新版本命名为 UseDemoAllParams.mq5

你可能还记得,我们在循环中依次创建了列表中的一些内置指标,并将参数列表留空,这导致指标使用了一些未知的默认值。因此,我们在图表的注释中显示了一个通用的原型:只有名称,没有具体的值。

plaintext
// UseDemoAllLoop.mq5
void OnTimer()
{
   ...
   Comment("DemoAll: ", (IndicatorSelector == iCustom_ ? IndicatorCustom : s),
      "(default-params)");
   ...
}

现在,我们有机会根据指标句柄找出其参数,并将它们显示给用户。

plaintext
// UseDemoAllParams.mq5
void OnTimer()
{
   ...   
   // 读取指标默认应用的参数
   ENUM_INDICATOR itype;
   MqlParam defParams[];
   const int p = IndicatorParameters(Handle, itype, defParams);
   ArrayPrint(defParams);
   Comment("DemoAll: ", (IndicatorSelector == iCustom_ ? IndicatorCustom : s),
      "(" + MqlParamStringer::stringify(defParams) + ")");
   ...
}

MqlParam 数组转换为字符串的功能在特殊类 MqlParamStringer 中实现(见文件 MqlParamStringer.mqh)。

plaintext
class MqlParamStringer
{
public:
   static string stringify(const MqlParam &param)
   {
      switch(param.type)
      {
      case TYPE_BOOL:
      case TYPE_CHAR:
      case TYPE_UCHAR:
      case TYPE_SHORT:
      case TYPE_USHORT:
      case TYPE_DATETIME:
      case TYPE_COLOR:
      case TYPE_INT:
      case TYPE_UINT:
      case TYPE_LONG:
      case TYPE_ULONG:
         return IntegerToString(param.integer_value);
      case TYPE_FLOAT:
      case TYPE_DOUBLE:
         return (string)(float)param.double_value;
      case TYPE_STRING:
         return param.string_value;
      }
      return NULL;
   }
   
   static string stringify(const MqlParam &params[])
   {
      string result = "";
      const int p = ArraySize(params);
      for(int i = 0; i < p; ++i)
      {
         result += stringify(params[i]) + (i < p - 1 ? "," : "");
      }
      return result;
   }
};

编译并运行新指标后,你可以确认现在图表左上角会显示正在渲染的指标的具体参数列表。

对于列表中的单个自定义指标(LifeCycle),第一个参数将包含指标的路径和文件名。第二个参数在源代码中被描述为整数。但第三个参数很有趣,因为它隐式描述了“应用于”属性,这是所有具有简短形式 OnCalculate 处理程序的指标所固有的。在这种情况下,默认情况下,指标应用于收盘价(PRICE_CLOSE,值为 1)。

plaintext
Initializing LifeCycle() EURUSD, PERIOD_H1

Handle=10

    [type] [integer_value] [double_value] [string_value]

[0]     14               0        0.00000 "Indicators\MQL5Book\p5\LifeCycle.ex5"

[1]      7               0        0.00000 null

[2]      7               1        0.00000 null

Initializing iAlligator_jawP_jawS_teethP_teethS_lipsP_lipsS_method_price() EURUSD, PERIOD_H1

iAlligator_jawP_jawS_teethP_teethS_lipsP_lipsS_method_price requires 8 parameters, 0 given

Handle=10

    [type] [integer_value] [double_value] [string_value]

[0]      7              13        0.00000 null          

[1]      7               8        0.00000 null          

[2]      7               8        0.00000 null          

[3]      7               5        0.00000 null          

[4]      7               5        0.00000 null          

[5]      7               3        0.00000 null          

[6]      7               2        0.00000 null          

[7]      7               5        0.00000 null          

Initializing iAMA_period_fast_slow_shift_price() EURUSD, PERIOD_H1

iAMA_period_fast_slow_shift_price requires 5 parameters, 0 given

Handle=10

    [type] [integer_value] [double_value] [string_value]

[0]      7               9        0.00000 null          

[1]      7               2        0.00000 null          

[2]      7              30        0.00000 null          

[3]      7               0        0.00000 null          

[4]      7               1        0.00000 null

根据日志,内置指标的设置也符合默认值。

综上所述,IndicatorParameters 函数为我们提供了一种方便的方式来获取运行中指标的参数设置,并且通过 MqlParamStringer 类可以将这些参数以字符串形式显示出来,方便用户查看。这在开发和调试 MQL 程序时非常有用,能够让我们更清楚地了解指标的运行状态和参数配置。

为指标定义数据源

在MQL程序的内置变量中,有一个只能在指标中使用的变量。这就是int类型的_AppliedTo变量,它允许从指标设置对话框中读取“应用于”属性。此外,如果指标是通过调用iCustom函数创建的,并且向该函数传递了第三方指标的句柄,那么_AppliedTo变量将包含这个句柄。

下表描述了_AppliedTo变量的可能取值。

用于计算的数据描述
0该指标使用OnCalculate的完整形式,并且计算数据不是由一个数据数组设置的
1收盘价
2开盘价
3最高价
4最低价
5平均价 = (最高价 + 最低价)/2
6典型价 = (最高价 + 最低价 + 收盘价)/3
7加权价 = (开盘价 + 最高价 + 最低价 + 收盘价)/4
8在该指标之前在图表上启动的指标的数据
9最先在图表上启动的指标的数据
10及以上_AppliedTo中包含的指标句柄对应的数据;在创建该指标时,这个句柄作为最后一个参数传递给了iCustom函数

为了便于分析这些值,本书附带了一个头文件AppliedTo.mqh,其中包含了相关的枚举。