在一张图表上的多个指标(第 02 部分):首次实验

Daniel Jose | 6 五月, 2022

概述

在前一篇文章“在一张图表上的多个指标”中,我介绍了如何在一张图表上加载多个指标的概念和基本知识,且并在屏幕上塞入太多不同的细节。 那篇文章的唯一目的是表现系统本身,展示如何创建数据库,以及如何利用这些数据库,我之前没有提供系统代码。 在此,我们将开始实现代码,在未来的文章中,我们还将扩展系统功能,令其更加通用和完整,因为系统看起来很有前途,故有进一步改进的巨大可能性。


计划

为了更容易理解该思路,最重要的是令系统可扩展性更强,它被切分为两个独立的文件,而主代码使用 OOP(面向对象编程)原则。 所有这些都是为了确保该系统能够以可持续、安全和稳定地发展。

在第一步当中,我们会用到指标,因此我们为该窗口创建一个指标:

为什么我们要用到指标,而非任何其它文件类型? 原因是,对于指标,我们不需要实现额外的内部逻辑来创建子窗口 — 取而代之,我们能够指令指标这样做,如此节省了我们的时间,并加快了系统开发。 指标头部如下所示:

#property indicator_plots 0
#property indicator_separate_window


仅使用这两行,我们就可以在品种图表上创建一个子窗口(对于那些不知道如何操作的人,请参见下表):

代码 说明
indicador_plots 0 这一行通知编译器我们不会跟踪任何数据类型;它防止编译器显示警告消息。
indicator_separate_window 这一行指示编译器添加必要的逻辑来创建子窗口。

这应该很容易。 对于那些不熟悉编程的人来说,源代码中的一些东西可能看起来很奇怪,但它们只是单单遵循整个编程社区广泛接受且使用的协议。 由于 MetaTrader 5 使用 MQL5 语言,它与 C++ 非常相似,只是略有不同;因此我们可以使用与 C++ 相同的编程方式。 因此,事情变得容易多了。 因此,依靠这一事实的优势,我们可以使用 C 语言指令,如下所示:

 #include <Auxiliary\C_TemplateChart.mqh>

该指令指示编译器包含存在于特定位置的头文件。 完整路路径应该像这样 Includes \ Auxiliary \ C_TemplateChart.mqh!? 是的,完整路径看起来是这样的,但 MQL5 已经知道任何头文件都应该位于 “includes” 目录中,所以我们可以省略第一部分。 如果路径用尖括号括起来,那么它是一条绝对路径;如果用引号括起来,则路径是相对的,即<Auxiliary \ C_TemplateChart. mqh> 区别于 "Auxiliary \ C_TemplateChart.mqh"

继续代码,我们得到以下几行:

input string user01 = "" ;       //Used indicators
input string user02 = "" ;       //Assets to follow


字符串值可在此处输入。 如果您清楚地知道打开指标时要使用哪些命令,可以在此处指定默认值。 例如,您总希望使用线宽为 3 的 RSI 和线宽为 2 的 MACD。 如下为其指定默认值:

input string user01 = "RSI:3;MACD:2" ;  //Used indicators
input string user02 = "" ;              //Assets to follow


该命令以后仍可更改,但默认值会令打开指标更容易,因为它已经以该命令预先配置。 下一行将创建一个别名,通过它我们可以访问包含所有“重量级”代码的对象类,从而允许我们访问其公开函数。

C_TemplateChart SubWin;

我们在自定义指标文件中的代码已经基本就绪,我们只需要再添加 3 行代码就可以让一切正常工作。 当然,我们的对象类不包含错误,但在这个类中,我们在内部将看到这一点。 因此,为了完成指标文件,需添加以下绿色高亮显示的行:

 //+------------------------------------------------------------------+
int OnInit ()
{
         SubWin.AddThese(C_TemplateChart::INDICATOR, user01);
         SubWin.AddThese(C_TemplateChart::SYMBOL, user02);

         return INIT_SUCCEEDED ;
}
//+------------------------------------------------------------------+

//...... other lines are of no interest to us ......

//+------------------------------------------------------------------+
void OnChartEvent ( const int id,
                   const long &lparam,
                   const double &dparam,
                   const string &sparam)
{
         if (id == CHARTEVENT_CHART_CHANGE ) SubWin.Resize();
}
//+------------------------------------------------------------------+


这正是自定义指标将具备的功能。 现在,我们来仔细查看包含对象类的文件中的黑盒。 从现在起,我们应该把注意力集中在两个函数上,但为了让它更简单,我们先看看对象类中存在的函数,了解它们各自的用途。

函数 说明
SetBase 创建显示指标数据所需的对象
decode 解码传递给它的命令
AddTemplate 根据显示的数据类型相应地调整内容
C_Template 默认类构造函数
~ C_Template 类析构函数
Resize 更改子窗口的尺寸
AddThese 该函数负责访问和构造内部对象

就这些。 如您所见,我们在自定义指标当中使用 RESIZE 和 ADDTHESE 函数。 这些是目前仅有的公开函数,这意味着我们不必担心,因为其它一切都隐藏在我们的对象中,确保它们不会在非必要的情况下被修改。 这为我们的最终代码提供了高可靠性。 我们继续编写代码,从以下定义开始:

 #define def_MaxTemplates         6

这一行对我们的对象类非常重要 — 它定义了这个类可以创建的最大指针数量。 若要加更多或减更少,只需更改此数字。 通过这个简单的解决方案,我们可以动态分配内存,并限制指标的数量。 大概,这是唯一您可能想改变的关键点,但我认为 6 是一个适合大多数人和监视器的数字。

下一行是枚举,这会令程序在某些点管理数据更容易:

 enum eTypeChart {INDICATOR, SYMBOL};

这一行位于我们的类内部当中,这个事实能保证它可在不同的类中拥有相同的名称,但其中指定的数据只针对该对象类。 因此,为了正确访问该枚举,请用自定义指标文件里 OnInit 函数提供的形式。 如果省略了类名,将被视为语法错误,代码将无法编译。 下一行是保留字。

 private :


这意味着所有进一步的操作对于这个对象类来说都是私密的,且在类之外不可见。 即,若您不在类的内部,就不可能访问五年任何深入的信息。 这提高了代码的安全性,使得该类所属的特定私密数据不可从外界访问。 后续的行声明一些内部和私密变量,直到我们得到类的第一个真实函数。

 void SetBase( const string szSymbol, int scale)
{
#define macro_SetInteger(A, B) ObjectSetInteger (m_Id, m_szObjName[m_Counter], A, B)

...

         ObjectCreate (m_Id, m_szObjName[m_Counter], OBJ_CHART , m_IdSubWin, 0 , 0 );
         ObjectSetString (m_Id, m_szObjName[m_Counter], OBJPROP_SYMBOL , szSymbol);
        macro_SetInteger( OBJPROP_CHART_SCALE , scale);
...
        macro_SetInteger( OBJPROP_PERIOD , _Period );
        m_handle = ObjectGetInteger (m_Id, m_szObjName[m_Counter], OBJPROP_CHART_ID );
        m_Counter++;
#undef macro_SetInteger
};


我们来更详尽地研究这个 SetBase 代码段。 我们从声明宏替换开始 — 它告诉编译器应该如何解释含有宏名称的简化代码。 即,如果我们必须重复若干次,我们可以使用 C 语言的这个特性来生成更简单的东西。 如果恰巧我们必须改变一些东西,我们只需修改宏替换的内容。 这能大大加快工作速度,并降低代码中出错的可能性,因为只需修改代码中一处,所有其它参数对应更改。

通过这样做,我们创建了一个类型为 CHART 的对象。 这似乎很奇怪。 为什么我们要用可以变化的东西来改变事物? 是的,这是真的。 下一步是声明所用资产。 这里的第一点是:如果在保存图表时不存在资产,那么该对象将链接到当前所用的资产。 如果它指定了图表资产,那么稍后将使用该资产。 重要细节:您可以指定不同的资产,并采用通用设置,我会在下一篇文章中详细解释。 因为我们将在这段代码中实现一些改进,以便能够完成目前不可能完成的事情。 接下来,在 OBJPROP_CHART_SCALE 属性中指定一个信息加密级别。 我们取从 0 到 5之间的数值。 虽然我们可以使用超出此范围的数值,但最好保持该范围。

接下来要注意的是 OBJPROP_PERIOD 属性。 请注意,我们使用的是当前图表周期,如果我们更改它,这个周期也会随之变更。 在未来,我们还将进行一些修改,并允许锁定它。 如果您想尝试,可以使用 MetaTrader 5 定义的时间帧,例如 PERIOD_M10,它表示以固定的 10 分钟时间帧显示数据。 但这将在稍后得以改进。 之后,我们将 sub0indicators 的数字加 1,并销毁宏替换。 也就是说,它将不再具有代表性,必须重新定义才能在其它地方使用。 我是不是忘了什么! 是的,它可能是这段代码中最重要的部分。

m_handle = ObjectGetInteger (m_Id, m_szObjName[m_Counter], OBJPROP_CHART_ID );

这一行捕获了一些可以被视为指针的东西,虽然它本身并不是指针,但它允许我们针对自己创建的 OBJ_CHART 对象进行一些额外的操作。 我们需要在对象内部采用这个值来应用设置。 它们在我们之前创建的设置文件当中。 继续代码,我们将看到以下函数,如下所示:

 void AddTemplate( const eTypeChart type, const string szTemplate, int scale)
{
	if (m_Counter >= def_MaxTemplates) return ;
	if (type == SYMBOL) SymbolSelect (szTemplate, true );
	SetBase((type == INDICATOR ? _Symbol : szTemplate), scale);
	ChartApplyTemplate (m_handle, szTemplate + ".tpl" );
	ChartRedraw (m_handle);
}


首先,我们检查是否可以添加新的指标。 如果可能,则检查它是否是 SYMBOL,如果是,则 SYMBOL 必须出现在市场观察窗口里,这是由该函数保证的。 有基于此,我们创建了一个接收信息的对象。 一旦执行,模板将应用于 OBJ_CHART,这时神奇的事情发生了:我们再次调用该对象,但现在它将根据设置文件中所包含的为 OBJ_CHART 定义的数据。 现在它简单、美丽、易懂。

使用这两个函数可以做很多事情。 但我们至少还需要一个函数 — 其完整代码如下所示:

 void Resize( void )
{
         int x0 = 0 , x1 = ( int )( ChartGetInteger (m_Id, CHART_WIDTH_IN_PIXELS , m_IdSubWin) / (m_Counter > 0 ? m_Counter : 1 ));
         for ( char c0 = 0 ; c0 < m_Counter; c0++, x0 += x1)
        {
                 ObjectSetInteger (m_Id, m_szObjName[c0], OBJPROP_XDISTANCE , x0);
                 ObjectSetInteger (m_Id, m_szObjName[c0], OBJPROP_XSIZE , x1);
                 ObjectSetInteger (m_Id, m_szObjName[c0], OBJPROP_YSIZE , ChartGetInteger (m_Id, CHART_HEIGHT_IN_PIXELS , m_IdSubWin));
        }
         ChartRedraw ();
}

上面的函数完成的操作是把所有内容都放在其应有的位置上,数据始终保持在子窗口区域内,其位置不会添加更多的内容。 因此,我们就完成了所有必要的代码,一切都能完美地工作。 但其它函数呢?! 别担心,其余的例程根本不需要,它们只是支持命令行的解释。 然而,我们来看看一些将来对修改代码的人来说很重要的东西。 它就是出现在我们的对象类代码中的保留字:

 public   :

这个保留词保证从即刻起,代码的所有数据和函数均可由其它部分访问和查看,即使它们不是对象类的一部分。 故我们于此处声明的内容,可由其它对象更改或访问。 事实上,良好的面向对象代码的行为是从不允许直接访问对象内的数据。 在设计精良的代码中,我们只能访问方法。 原因很简单 — 安全。 当我们允许外部代码修改类中的数据时,我们会面临数据与对象预期不匹配的风险,这在尝试解决不一致或缺陷时,会引发很多问题并令人头痛,因为看似一切正常。 因此,我可以给那些多年来一直使用 C++ 编程的人一些建议:永远不要允许外部对象修改或直接访问您创建的类中的内部数据。 提供函数或例程,如此便可以访问数据,但绝不允许直接访问数据,并确保函数和例程支持您所创建类的预期数据。 考虑到这一点,我们继续学习教程的最后两个函数,其中一个是 public (AddThese) ,另一个是 private (Decode)。 您可在下面看到它们的全部内容:

void Decode( string &szArg, int &iScale)
{
#define def_ScaleDefault 4
         StringToUpper (szArg);
        iScale = def_ScaleDefault;
         for ( int c0 = 0 , c1 = 0 , max = StringLen (szArg); c0 < max; c0++) switch (szArg[c0])
        {
                 case ':' :
                         for (; (c0 < max) && ((szArg[c0] < '0' ) || (szArg[c0] > '9' )); c0++);
                        iScale = ( int )(szArg[c0] - '0' );
                        iScale = ((iScale > 5 ) || (iScale < 0 ) ? def_ScaleDefault : iScale);
                        szArg = StringSubstr (szArg, 0 , c1 + 1 );
                         return ;
                 case ' ' :
                         break ;
                 default :
                        c1 = c0;
                         break ;
        }
#undef def_ScaleDefault
}
//+------------------------------------------------------------------+
// ... Codes not related to this part...
//+------------------------------------------------------------------+
void AddThese( const eTypeChart type, string szArg)
{
         string szLoc;
         int i0;
         StringToUpper (szArg);
         StringAdd (szArg, ";" );
         for ( int c0 = 0 , c1 = 0 , c2 = 0 , max = StringLen (szArg); c0 < max; c0++) switch (szArg[c0])
        {
                 case ';' :
                         if (c1 != c2)
                        {
                                szLoc = StringSubstr (szArg, c1, c2 - c1 + 1 );
                                Decode(szLoc, i0);
                                AddTemplate(type, szLoc, i0);
                        }
                        c1 = c2 = (c0 + 1 );
                         break ;
                 case ' ' :
                        c1 = (c1 >= c2 ? c0 + 1 : c1);
                         break ;
                 default :
                        c2 = c0;
                         break ;
        }
}


这两个函数与我上面解释过的完全一样:它强制执行数据完整性验证,防止类的内部数据不一致。 它们接收一个命令行,并按照预定义的语法对其进行解码。 然而,它们并不会反馈收到的命令有错误,因为这不是它们的目的。 其目的是确保不一致的数据不会进入对象,也不会导致难以检测和修复的副作用。

最终结果如下:



结束语

我希望这段代码能激励你! 我对编程感兴趣是因为它既优美又令人兴奋。 虽然有时候,如果我们想达成一些特殊的结果,它会让我们非常头疼。 但大多数时候,这是值得的。 在下一篇文章中,我会告诉您如何让这一切变得更加有趣。 本文附件包含指标的完整代码,可以按照本文和前一篇文章中的简述使用。