以资源形式连接自定义指标

为便于运算,MQL 程序可能需要一个或多个自定义指标。所有这些都可以作为资源包含在 ex5 可执行文件中,使其易于分发和安装。

包含嵌套指标说明的 #resource 指令格式如下:

#resource "path_indicator_name.ex5"

设置和搜索指定文件的规则,与一般所有 资源 的处理方式相同。

我们已经在 大型智能交易系统示例的UnityMartingale.mq5 最终版本中使用了这一功能。

#resource "\\Indicators\\MQL5Book\\p6\\UnityPercentEvent.ex5"

在该智能交易系统中,传递给 iCustom 函数的不是指标名称,而是该资源:"::Indicators\\MQL5Book\\p6\\UnityPercentEvent.ex5"。

关于 OnInit 函数里的自定义指标创建自身一个或多个实例的情况,需要特别加以考虑(如果这个技术方案本身看起来有点奇怪,我们会在介绍性范例之后提供一个实际应用的例子)。

我们知道,要使用 MQL 程序中的资源,必须以如下形式指定:path_file_name.ex5::resource_name。例如,如果 EmbeddedIndicator.ex5 指标作为资源包含在另一个指标 MainIndicator.mq5(更准确地说,包含在其二进制映像 MainIndicator.ex5)中,那么通过 iCustom 调用时指定的名称就不能再是简短的无路径名称,路径必须包含 MQL5 文件夹中“父”指标的位置。否则,系统将无法找到嵌套指标。

事实上,在正常情况下,指标可以使用 iCustom(_Symbol, _Period, myself,...) 运算符调用自身,其中 myself 是一个字符串,等于 MQLInfoString(MQL_PROGRAM_NAME) 或之前在代码中分配给 INDICATOR_SHORTNAME 特性的名称。但是,当指标作为资源位于另一个 MQL 程序中时,名称就不再指向相应的文件,因为作为资源原型的文件仍保留在进行编译的计算机上,而在用户的计算机上只有 MainIndicator.ex5 文件。这就需要在启动程序时对程序环境进行一些分析。

我们来实际操作一下。

首先,我们来创建一个 NonEmbeddedIndicator.mq5 指标。需要注意的是,它位于 MQL5/Indicators/MQL5Book/p7/SubFolder/ 文件夹中,即相对于本书这章所有指标所在的 p7 文件夹下的 SubFolder 中。这样做的目的是模拟用户计算机上没有编译文件的情况。现在我们来看看它是如何运作的(或者说,演示了这个问题)。

该指标只有一个输入参数 Reference。它的作用是计算自身副本的数量:在首次创建时,参数值为 0,指标将创建参数值为 1 的自身副本。第二个副本在“侦测到”数值 1 之后,就不会再建立新的副本了(否则,如果没有停止复制的边界条件,我们的资源很快就会用尽)。

input int Reference = 0;

handle 变量保留给复制指标的句柄。

int handle = 0;

在处理程序 OnInit 中,为了清晰起见,我们首先显示 MQL 程序的名称和路径。

int OnInit()
{
   const string name = MQLInfoString(MQL_PROGRAM_NAME);
   const string path = MQLInfoString(MQL_PROGRAM_PATH);
   Print(Reference);
   Print("Name: " + name);
   Print("Full path: " + path);
   ...

接下来是适合自启动单独指标的代码(以我们熟悉的文件 NonEmbeddedIndicator.ex5 的形式存在)。

   if(Reference == 0)
   {
      handle = iCustom(_Symbol_Periodname1);
      if(handle == INVALID_HANDLE)
      {
         return INIT_FAILED;
      }
   }
   Print("Success");
   return INIT_SUCCEEDED;
}

我们可以成功地在图表上放置这样一个指标,并在日志中接收以下类型的条目(你将拥有自己的文件系统路径):

0

Name: NonEmbeddedIndicator

Full path: C:\Program Files\MT5East\MQL5\Indicators\MQL5Book\p7\SubFolder\NonEmbeddedIndicator.ex5

Success

1

Name: NonEmbeddedIndicator

Full path: C:\Program Files\MT5East\MQL5\Indicators\MQL5Book\p7\SubFolder\NonEmbeddedIndicator.ex5

Success

只需使用 "NonEmbeddedIndicator" 这一名称,复制就能成功启动。

我们现在暂时先不讨论这个指标,来创建第二个指标 FaultyIndicator.mq5。我们会将第一个指标作为资源包含进这个新指标中(请注意资源相对路径中 subfolder 的指定;这是必要的,因为 FaultyIndicator.mq5 指标位于上一层文件夹:MQL5/Indicators/MQL5Book/p7/)。

// FaultyIndicator.mq5
#resource "SubFolder\\NonEmbeddedIndicator.ex5"
   
int handle;
   
int OnInit()
{
   handle = iCustom(_Symbol_Period"::SubFolder\\NonEmbeddedIndicator.ex5");
   if(handle == INVALID_HANDLE)
   {
      return INIT_FAILED;
   }
   return INIT_SUCCEEDED;
}

如果你尝试运行已编译的 FaultyIndicator.ex5,则会出现错误:

0

Name: NonEmbeddedIndicator

Full path: C:\Program Files\MT5East\MQL5\Indicators\MQL5Book\p7\FaultyIndicator.ex5 »

» ::SubFolder\NonEmbeddedIndicator.ex5

cannot load custom indicator 'NonEmbeddedIndicator' [4802]

当启动一个内嵌指标的副本时,系统会在描述该资源的那个主指标所在的文件夹中搜寻它。但是并没有 NonEmbeddedIndicator.ex5 文件,因为所需资源在 FaultyIndicator.ex5 中。

为了解决这个问题,我们需要修改 NonEmbeddedIndicator.mq5。首先,我们给它取一个更合适的名字,即 EmbeddedIndicator.mq5。在源代码中,我们需要添加一个辅助函数 GetMQL5Path,它可以从已启动的 MQL 程序的常规路径中分离出 MQL5 文件夹内的相对部分(如果指标是从资源启动的,这部分还将包含资源名称)。

// EmbeddedIndicator.mq5
string GetMQL5Path()
{
   static const string MQL5 = "\\MQL5\\";
   static const int length = StringLen(MQL5) - 1;
   static const string path = MQLInfoString(MQL_PROGRAM_PATH);
   const int start = StringFind(pathMQL5);
   if(start != -1)
   {
      return StringSubstr(pathstart + length);
   }
   return path;
}

考虑到新函数,我们将更改 OnInit 处理程序中的 iCustom 调用。

int OnInit()
{
   ...
   const string location = GetMQL5Path();
   Print("Location in MQL5:" + location);
   if(Reference == 0)
   {
      handle = iCustom(_Symbol_Periodlocation1);
      if(handle == INVALID_HANDLE)
      {
         return INIT_FAILED;
      }
   }
   return INIT_SUCCEEDED;
}

我们确保这次修改不会破坏指标的启动。在图表上叠加后,日志中会出现预期的文本行:

0

Name: EmbeddedIndicator

Full path: C:\Program Files\MT5East\MQL5\Indicators\MQL5Book\p7\SubFolder\EmbeddedIndicator.ex5

Location in MQL5:\Indicators\MQL5Book\p7\SubFolder\EmbeddedIndicator.ex5

Success

1

Name: EmbeddedIndicator

Full path: C:\Program Files\MT5East\MQL5\Indicators\MQL5Book\p7\SubFolder\EmbeddedIndicator.ex5

Location in MQL5:\Indicators\MQL5Book\p7\SubFolder\EmbeddedIndicator.ex5

Success

我们在这里添加了 GetMQL5Path 函数接收到的相对路径的调试输出。这一行现在用于 iCustom,并在此模式下运作:创建了一个副本。

现在,我们将该指标作为资源嵌入 MQL5Book/p7 文件夹中的另一个指标,其名称为 MainIndicator.mq5。除了连接的资源外,MainIndicator.mq5FaultyIndicator.mq5 完全相同。

// MainIndicator.mq5
#resource "SubFolder\\EmbeddedIndicator.ex5"
...
int OnInit()
{
   handle = iCustom(_Symbol_Period"::SubFolder\\EmbeddedIndicator.ex5");
   ...
}

我们来编译并运行它。日志中出现的条目会带有一个包含嵌套资源的新相对路径。

0

Name: EmbeddedIndicator

Full path: C:\Program Files\MT5East\MQL5\Indicators\MQL5Book\p7\MainIndicator.ex5 »

» ::SubFolder\EmbeddedIndicator.ex5

Location in MQL5:\Indicators\MQL5Book\p7\MainIndicator.ex5::SubFolder\EmbeddedIndicator.ex5

Success

1

Name: EmbeddedIndicator

Full path: C:\Program Files\MT5East\MQL5\Indicators\MQL5Book\p7\MainIndicator.ex5 »

» ::SubFolder\EmbeddedIndicator.ex5

Location in MQL5:\Indicators\MQL5Book\p7\MainIndicator.ex5::SubFolder\EmbeddedIndicator.ex5

Success

我们可以看到,这次嵌套指标成功地创建了自己的副本,因为它使用了带有相对路径的限定名和资源名 "\\Indicators\\MQL5Book\\p7\\MainIndicator.ex5::SubFolder\\EmbeddedIndicator.ex5"。

在启动该指标的多次实验中,请注意在移除主指标后,嵌套副本不会立即从图表中卸载。因此,只有在等待卸载后才能重新启动:否则,仍在运行的副本将被重复使用,上述初始化行将不会出现在日志中。为了控制卸载,我们在 OnDeinit 处理程序中添加了 Reference 值的打印输出。

我们承诺向大家展示,创建指标的副本并不是什么特别的事情。作为该技术的应用演示,我们使用指标 DeltaPrice.mq5,它可以计算给定阶数的价格增量差。阶数 0 表示无差异(仅检查原始时间序列),1 表示单差异,2 表示双差异,以此类推。

阶数在 Differentiating 输入参数中指定。

input int Differencing = 1;

差值序列将显示在子窗口的单个缓冲区中。

#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1
   
#property indicator_type1 DRAW_LINE
#property indicator_color1 clrDodgerBlue
#property indicator_width1 2
#property indicator_style1 STYLE_SOLID
   
double Buffer[];

OnInit 处理程序中,我们将设置缓冲区并创建相同的指标,在输入参数中传递减 1 的值。

#include <MQL5Book/AppliedTo.mqh// APPLIED_TO_STR macro
 
int handle = 0;
   
int OnInit()
{
   const string label = "DeltaPrice (" + (string)Differencing + "/"
      + APPLIED_TO_STR() + ")";
   IndicatorSetString(INDICATOR_SHORTNAMElabel);
   PlotIndexSetString(0PLOT_LABELlabel);
   
   SetIndexBuffer(0Buffer);
   if(Differencing > 1)
   {
      handle = iCustom(_Symbol_PeriodGetMQL5Path(), Differencing - 1);
      if(handle == INVALID_HANDLE)
      {
         return INIT_FAILED;
      }
   }
   return INIT_SUCCEEDED;
}

为了避免将指标作为资源嵌入时可能出现的问题,我们使用了已证明有效的函数 GetMQL5Path

OnCalculate 函数中,我们执行减去时间序列相邻值的运算。当 Differentiating 等于 1 时,操作数是 price 数组的元素。如果 Differentiating 值较大,我们将读取为前一个阶数创建的指标副本的缓冲区。

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
{
   for(int i = fmax(prev_calculated - 11); i < rates_total; ++i)
   {
      if(Differencing > 1)
      {
         static double value[2];
         CopyBuffer(handle0rates_total - i - 12value);
         Buffer[i] = value[1] - value[0];
      }
      else if(Differencing == 1)
      {
         Buffer[i] = price[i] - price[i - 1];
      }
      else
      {
         Buffer[i] = price[i];
      }
   }
   return rates_total;
}

差分价格的初始类型在指标设置对话框的Apply to下拉列表中设置。默认为Close价。

这就是在图表上,同一个指标的数个副本以不同的差分阶数呈现出来的样子。

不同差分阶数的收盘价差异

不同差分阶数的收盘价差异