一张图表上多个指标(第 04 部分):晋升为一款智能交易系统

在我之前的文章里,我已经解释了如何创建拥有多个子窗口的指标,在使用自定义指标时如此这般会变得很有趣。 这很容易做到。 但当我们尝试在智能交易系统中实现相同的功能时,事情会越加变得复杂,因为在自定义指标中我们没有可用的工具。 在这一点上,编程变得至关重要:能够编写正确的代码来创建子窗口至关重要。 尽管这项任务并非那么容易,但知道如何在 EA 中设置子窗口并不需要很多编码,只需通晓 MQL5 的工作原理。

计划

我们已经有了自定义指标,也就是说,我们的对象类已经功能齐备,而且由于这是一个对象类,我们可以轻松地将其转换到其它模型。 然而,在我们的 EA 中简单地声明并尝试使用这个类,并不能让事情如同我们在自定义指标中一样工作,原因是我们的 EA 中未提供子窗口功能。 但又冒出了这样一个想法:“如果我们用一个已经编译过,且可工作的自定义指标,然后用 iCustom 命令从 EA 调用它,会怎么样? 好吧,这也许是可行的,因为这样不需要子窗口,命令如下所示:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
input string user01 = "";                //Used indicators
input string user02 = "";                //Assets to follow
//+------------------------------------------------------------------+
int OnInit()
{
        int m_handleSub;

//... Expert Advisor code ...

        if ((m_handleSub = iCustom(NULL, 0, "Chart In SubWindows\\Chart In SubWindow.ex5", user01, user02)) == INVALID_HANDLE) return INIT_FAILED;
        if (!ChartIndicatorAdd(ChartID(), 0, m_handleSub)) return INIT_FAILED;
//... Expert Advisor code ...

        ChartRedraw();
        
        return(INIT_SUCCEEDED);
}
//...The rest of the Expert Advisor code ...

这个简单的代码片段能够加载我们的自定义指标,然而它还不能正常工作,因为我们没有子窗口。 在这种情况下,当代码在 EA 里执行时,EA 将直接在主窗口中应用我们的指标,这意味着图表会被指标加载的模板掩盖,这绝对不是我们想要的。

因此,我们真正的主要问题是创建一个可用的子窗口,以便我们可以使用已经功能齐备的指标。 但为何我们要为随后启动的指标创建一个子窗口呢? 这没有意义,最好直接往 EA 里添加功能,从而克服可能出现的任何限制。

有基于此,我们需要执行若干个任务:

任务目的
1 => 创建一个通用指标。它允许在不污染图表的情况下创建和使用 iCustom 命令。
2 => 在 EA 里以某种方式包含该指标。 这令您能够毫无问题地将其转换为具有完整功能的智能交易系统。
3 => 针对子窗口生成通用对象类 允许经由 EA 添加子窗口
4 => 获取已绑定到 window 类的 C_TemplateChart 类。这允许我们管理子窗口的内容,而无需更改功能齐备的代码中的任何内容。

虽然这看起来很难,但困难很简单就解决了。 那好,我们来逐点处理。

实现:创建通用指标

这一部分可以通过创建一个完全干净、但功能齐全的自定义指标代码来解决。 本例中的代码如下所示:

#property copyright "Daniel Jose"
#property version   "1.00"
#property description "This file only enables support of indicators in SubWin."
#property indicator_chart_window
#property indicator_plots 0
//+------------------------------------------------------------------+
int OnInit()
{
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
{
        return rates_total;
}
//+------------------------------------------------------------------+

只有这些,没别的。 我们将此文件另存为 SubSupport.mq5。 但其所在与其它指标不同 — 我们将其移到智能交易系统的 RESOURCE 目录。 因此,文件结构如下图所示:

这有一个很好的理由,但我们暂时把它搁置一旁。 现在我们进入下一个任务。

实现:在 EA 里包含通用指标

为此,我们需要在 EA 的顶部添加以下代码。

//+------------------------------------------------------------------+
#define def_Resource "Resources\\SubSupport.ex5"
//+------------------------------------------------------------------+
#resource def_Resource
//+------------------------------------------------------------------+

这将把通用指标的编译代码包含到我们的 EA 当中。 这步一旦完成,即可把通用指标的 .ex5 文件删除,因为不再需要它。 现在,您应该注意这样一个事实:如果在编译 EA 代码时未找到 SubSupport.ex5 文件,编译器将自动编译通用指标的代码 SubSupport. mq5,并将这个新编译的可执行文件添加到我们的智能交易系统之中。 那么,如果您曾经编辑过 SubSupport.mq5 文件,并且需要将更改添加到智能交易系统当中,您应该先删除已有的 SubSupport.ex5;否则,新的更改不会被添加。

这个细节很重要:有时候您真的需要知道如何将新实现的修改添加到资源当中。

好了,通用指标现在是智能交易系统的一部分了,如此我们进入下一个任务。

实现 : 创建一个子窗口对象类

这一部分也很简单。 在此,我们需要在编码之前定义一些要点,即:在这个类中,我们真正需要什么特性? 最初,我决定如下使用:

函数说明
Init允许经由 EA 添加子窗口
Close允许经由 EA 添加子窗口

这些函数不会被测试,所以我假设它们在 EA 的生存期内只会被调用一次。 但随着我们的 EA 增长,故考虑让它在未来更加实用是个好主意。 因此,我们来创建一个名为 C_Terminal 的新对象类 — 这个类将支持一些与图形终端相关的东西。 稍后我们将学习更多有关它的内容。 我们看看最后一项任务,因为部分解决方案无法实现。

实现: C_TemplateChart 类继承

当我决定运用 OOP(面向对象编程)创建一些新东西时(我这么做是因为我已经知道运用这种方式有很大的优势,包括安全性和继承性)。 还有多态性,我们将在稍后创建交叉订单系统时用到它。 在这种特殊情况下,我们将用到 OOP 的一项优势 — 继承。 C_TemplateChart 已经是一个功能齐全的类。 看到这一点,您就不想再为所有内容重新编程,或冒着风险往类中添加代码,因为这会阻碍该类在其它地方使用。 解决方案是运用继承,它允许添加新代码或函数,而不必改变原始代码。

运用继承有很多优势,包括以下几点:已经测试过的代码仍然保留测试过的状态;复杂度会随着代码量的增长而增加;只有新功能真正需要测试;只是继承不会改变原类,故可提供稳定性。 换言之,事情会以最小的代价得以改善,但却会带来最大的安全性。 为了理解这一点,我们来看下面的示意图。

祖父类是最基本类,在其中我们拥有的数据操作级别最低,但当父类从祖父类继承某些内容时,祖父类中声明为公开的所有内容都可以由父类查看和使用。 我们还可以向父类添加新的内容,这不会影响其继承和支持的内容。 如果父类已经完成并可工作,且我们希望在不更改类中任何内容的情况下对其进行扩展,那么我们应创建一个子类,它将拥有前类的所有功能。 我们还可以改变工作方式,这是关于继承的有趣之处,因为这些更改不会影响其它类。 然而,这与允许多重继承的 C++ 不同,此处有一个限制。 如果子类可以同时从父类和祖父类继承函数,那么在 MQL5 中这是不可能的。 但您仍然从遗产中受益。 多重继承的例子如下所示:

好的,但是在 MQL5 中如何实现呢? 如何声明继承,以便我们可以利用它的优势? 理解这一点最准确的方法是阅读面向对象编程(OOP)的内容,但在这里我们直奔主题。 继承将使用以下行完成:

#include "C_TemplateChart.mqh"
//+------------------------------------------------------------------+
class C_SubWindow : public C_TemplateChart
{
// ... Class code
};

可见 C_SubWindow 类公开继承自 C_TemplateChart 类,那么现在我们可以使用 C_SubWindow 类来访问 C_TemplateChart 类的功能。

在上面的代码片段中,我强调了一件事。 请注意,它通常在引号(")中,而非尖括号(< > )里。 那我为什么要这么做? 与 C++ 语言一样,MQL5 也有一些非常有趣的东西,但有些东西会令那些刚刚开始学习编程艺术的人感到困惑。 当我们将头文件放在尖括号(<>)之间时,我们指示的是一个绝对路径 — 在这种情况下,编译器将完全遵循我们指定的路径。 但是当我们使用引号时(就像我们这次所做的那样),编译器将采用相对路径,或者更清楚地说,它将首先从工作文件所在的当前目录开始。 这可能看起来很奇怪,但有时我们会用到相同名称但不同内容的文件,且它们位于不同的目录中,若我们仍然希望引用当前目录,那么我们就可以用引号来表示。

我们在前面计划使用的两个函数 INIT 和 CLOSE 如下所示:

//+------------------------------------------------------------------+
bool Init(void)
{
        if (m_handleSub != INVALID_HANDLE) return true;
        if ((m_handleSub = iCustom(NULL, 0, "::" + def_Resource)) == INVALID_HANDLE) return false;
        m_IdSub = (int) ChartGetInteger(Terminal.Get_ID(), CHART_WINDOWS_TOTAL);
        if (!ChartIndicatorAdd(Terminal.Get_ID(), m_IdSub, m_handleSub)) return false;
                
        return true;
}
//+------------------------------------------------------------------+
void Close(void)
{
        ClearTemplateChart();
        if (m_handleSub == INVALID_HANDLE) return;
        IndicatorRelease(m_IdSub);
        ChartIndicatorDelete(Terminal.Get_ID(), m_IdSub, ChartIndicatorName(Terminal.Get_ID(), m_IdSub, 0));
        ChartRedraw();
        m_handleSub = INVALID_HANDLE;
}
//+------------------------------------------------------------------+

看,代码非常简短。 但有些事情我们必须小心,注意高亮显示的部分。 在添加此部件时,您必须小心不要出错,因为如果不保持原样,则我们将要添加到 EA 中的 SubSupport.ex5 可执行文件在 EA 内部不可见 — 而在 EA 外部则可见。 相关的更多详情,请阅读参考资料。 但基本上,如果您使用 ( :: ),这表明 EA 应该使用其内部提供的内部资源。 但如果我们只指示资源的名称,EA 将在 MQL5 目录中搜索它,如果指定位置不存在该文件,即使该文件被添加为 EA 资源,该函数也将失败。

然后,一旦加载资源后,我们检查存在的子窗口的数量,并向该子窗口添加一个指标。

代码的实际操作,如下所见:

input string user01 = "";               //Used indicators
input string user02 = "";               //Assets to follows
//+------------------------------------------------------------------+
int OnInit()
{
        int m_handleSub;

//...   

        if ((m_handleSub = iCustom(NULL, 0, "Chart In SubWindows\\Chart In SubWindow.ex5", user01, user02)) == INVALID_HANDLE) return INIT_FAILED;
        if (!ChartIndicatorAdd(ChartID(), (int) ChartGetInteger(ChartID(), CHART_WINDOWS_TOTAL), m_handleSub)) return INIT_FAILED;

//...

        ChartRedraw();
        
   return(INIT_SUCCEEDED);
}
//...The rest of the Expert Advisor code ...