在一张图表上的多个指标(第 03 部分):为用户开发定义

6 五月 2022, 13:33
Daniel Jose
0
961

概述

“一张图表上的多个指标”的前一篇文章中,我们研究了允许在图表子窗口中加载多个指标的基本代码。 但其所代表的只是一个更大系统的起点。 基于这个模型还可以做一些不同的事情。 但我们应该循序渐进,因为这些文章的目标之一是鼓励您学习如何编程,从而您可以根据自己的想法自行设计系统。 在本文中,我们将扩展该功能。 对于那些已经喜欢该系统之所能,但希望其能做得更多的人来说,这也许很有趣。


计划

通常,当我们开始实现一个新系统时,我们并不真正知道我们如何对其进行改进;故此,我们应该始终启动一个新项目,并着眼于未来进行改进。 这对那些刚开始的人来说非常重要:持续地规划一些事情,想象未来的扩展和改进。

核心代码根本没有改变,这在某种意义上是意料之中的。 但是对象类的代码已经发生了巨大的变化。 随着代码重用性变得越加重要(这是面向对象编程的基本思想之一:始终重用,仅在必要时创建新代码),我们进行了这些修改,以实现新功能,并能够以更灵活的方式创建新的改进。 如此,我们来看看新的对象类。 我会高亮示意这些变化,令其更容易理解。

我们从类的私密变量新定义开始。

struct st
{
        string  szObjName,
                szSymbol;
        int     width;
}m_Info[def_MaxTemplates];
int             m_IdSubWin,
                m_Counter,
                m_CPre,
                m_Aggregate;
long            m_Id,
                m_handle;
ENUM_TIMEFRAMES m_Period;

请注意,所变量数量显著增加。 这是因为我们需要更多数据来正确管理新功能。 现在我们的变量系统有了一个结构。 这种结构非常适合将相关变量组合在一起 — 它们确保我们在处理数据时能够快速、轻松地访问。

void SetBase(const string szSymbol, int iScale, int iSize)
{
#define macro_SetInteger(A, B) ObjectSetInteger(m_Id, m_Info[m_Counter].szObjName, A, B)
        if (m_IdSubWin < 0)
        {
                m_Id = ChartID();
                m_IdSubWin = (int)ChartGetInteger(m_Id, CHART_WINDOWS_TOTAL) - 1;
                m_Aggregate = 0;
        }
        m_Info[m_Counter].szObjName = __FILE__ + (string) MathRand() + (string) ObjectsTotal(m_Id, -1, OBJ_CHART);
        ObjectCreate(m_Id, m_Info[m_Counter].szObjName, OBJ_CHART, m_IdSubWin, 0, 0);
        ObjectSetString(m_Id, m_Info[m_Counter].szObjName, OBJPROP_SYMBOL, (m_Info[m_Counter].szSymbol = szSymbol));

// ....

        macro_SetInteger(OBJPROP_PERIOD, m_Period);
        m_handle = ObjectGetInteger(m_Id, m_Info[m_Counter].szObjName, OBJPROP_CHART_ID);
        m_Aggregate += iSize;
        m_Info[m_Counter].width = iSize;
        m_CPre += (iSize > 0 ? 1 : 0);
        m_Counter++;
#undef macro_SetInteger
};


我们很快将看到的主要变化是,我们正在使用一个结构来存储资产名称、对象名称及其宽度。 现在我们还可以指定指标在子窗口中的宽度。 我们来做些注释,方便在类的其它部分使用它们。 下面是变化最大的函数。

void Decode(string &szArg, int &iScale, int &iSize)
{
#define def_ScaleDefault 4
#define macro_GetData(A)                \
        b0 = false;                     \
        for (c0++; (c0 < max) && (szArg[c0] == ' '); c0++);     \
        for (i0 = 0, i1 = c0; (c0 < max) && (szArg[c0] != A); i0 = (szArg[c0] != ' ' ? c0 - i1 + 1 : i0), c0++);        \
        if (szArg[c0] == A) sz1 = StringSubstr(szArg, i1, i0); else sz1 = "";
                                                                
        string sz1;
        int i0, i1, c1 = StringLen(szArg);
        bool b0 = true;
        StringToUpper(szArg);
        iScale = def_ScaleDefault;
        m_Period = _Period;
        for (int c0 = 0, max = StringLen(szArg); c0 < max; c0++) switch (szArg[c0])
        {
                case ':':
                        b0 = false;
                        for (; (c0 < max) && ((szArg[c0] < '0') || (szArg[c0] > '9')); c0++);
                        iScale = (int)(szArg[c0] - '0');
                        iScale = ((iScale > 5) || (iScale < 0) ? def_ScaleDefault : iScale);
                        break;
                case ' ':
                        break;
                case '<':
                        macro_GetData('>');
                        if (sz1 == "1M") m_Period = PERIOD_M1; else

//....

                        if (sz1 == "1MES") m_Period = PERIOD_MN1;
                        break;
                case '[':
                        macro_GetData(']');
                        iSize = (int) StringToInteger(sz1);
                        break;
                default:
                        c1 = (b0 ? c0 : c1);
                        break;
        }
        szArg = StringSubstr(szArg, 0, c1 + 1);
#undef macro_GetData
#undef def_ScaleDefault
}


绿色示意代码的新增内容。 黄色示意源代码中已经存在,但由于实际原因被移动的行。 现在我们来看看所有这些加入代码做了什么,更重要的是,它们针对原始系统的功能方面做了哪些改进。 实际上,我们正在打造一个基础,让用户能够定制一些特殊的东西。 我们尝试往向现有语法中添加新规则来实现这一点(请参见下表):

分隔符 功能 示例  结果 
< > 指定要使用的图形周期  < 15m > 将指标周期固定为 15 分钟。 原始图表可以使用不同的时间帧,但指标仅显示 15 分钟时间帧的数据。
 [ ] 指定指标宽度  [ 350 ] 指标宽度固定为 350 像素。 

固定图表周期的分隔符 — 仅在指标窗口中,且不会影响用户所作的任何更改。 所有其它指标和主图表将依据用户选择的新图表周期进行更新,但固定指标不会跟随新图表周期。 在某些情况下,这可能很有趣,如下图所示。

         

我们需要在交易屏幕上显示同一资产不同时期的图表,而这大大方便了各种类型的设置。 现在,在很特别的情况下,一个固定图表宽度的分隔符会令这些设置变得更容易。 它还有一个很大的用途,我们会在另一篇文章中探讨,但现在您可以用它来控制指标的宽度。

您可以利用所有分隔符的组合,也可只用指标中真正需要的分隔符,因为这里并无具体的规则。 唯一的规则是,指标名称应该放在其它所有内容之前。 我们回到代码解释。 看下面几行:

#define macro_GetData(A)                \
        b0 = false;                     \
        for (c0++; (c0 < max) && (szArg[c0] == ' '); c0++);     \
        for (i0 = 0, i1 = c0; (c0 < max) && (szArg[c0] != A); i0 = (szArg[c0] != ' ' ? c0 - i1 + 1 : i0), c0++);        \
        if (szArg[c0] == A) sz1 = StringSubstr(szArg, i1, i0); else sz1 = "";
           
//....                                                     
                case '<':
                        macro_GetData('>');


请注意,我们正在定义一些对许多人来说可能感觉很奇怪的东西。 但其名称在此很有用:macro_GetData(A),因其是个宏定义,故会创建一段代码。 当编译器在代码中发现这个宏定义时,编译器将用宏定义代码替换声明。 如果我们要在多个地方重复某个代码片段,但期望在一个声明和另一个声明之间进行最小的修改,这种方式将非常有用。 在上一个示例中,绿色的行会被替换,编译器生成的代码如下所示:

case '<':
	b0 = false;
        for (c0++; (c0 < max) && (szArg[c0] == ' '); c0++);
        for (i0 = 0, i1 = c0; (c0 < max) && (szArg[c0] != '>'); i0 = (szArg[c0] != ' ' ? c0 - i1 + 1 : i0), c0++);
        if (szArg[c0] == A) sz1 = StringSubstr(szArg, i1, i0); else sz1 = "";
//....

这种东西是最真实的哲学表达:尽可能多地重复使用,尽可能少地写新东西。 现在,我们看看如果我们想让语法更清晰,可以如何进行修改。 这是一个小细节,但可依据您的个人风格量身定制,如此可能会更好。 请看下面一行:

//.....

if (sz1 == "1M") m_Period = PERIOD_M1; else

//.....

高亮示意的信息是一个重要的细节。 根据我所用的模型,如果用户想要使用 1 分钟时间帧,那么用户应该使用以下语法来表达:MIN_1。 如果您想拥有一个独立的风格,您可以用自己的方式来指定。 但是字母应该是大写的,不含空格。 例如,所选部分可以替换为 "1MIN","MIN_1","1_MINUTE" 或者更详细的东西,比如: "LOCK_IN_1_MIN" 或以您的语言 - 只要单词之间没有空格,就行。 实际上,这个空格限制是可以取消的,但在我看来,这样做并无实际必要。 看看通晓编程有多好 — 您可以用自己的独特风格来使用。 我修改的下一个代码片段是默认的析构函数。

~C_TemplateChart() 
{
        for (char c0 = 0; c0 < m_Counter; c0++)
        {
                ObjectDelete(m_Id, m_Info[c0].szObjName);
                SymbolSelect(m_Info[c0].szSymbol, false);
        }
}

针对以下情况添加了高亮示意的代码行:如果资产并非在单独的窗口中打开,则它不应再出现在市场观察之中。 这样可以防止使用未出现的资产,占用空间并污染窗户。 现在我们看看在上一篇文章中承诺要解释的内容。 这在最初的代码中并不存在,但将来它将成为代码的一部分。

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

如果所有观察到的资产套用相同的设置,则高亮示意的行使用默认设置文件。 若事情没有按预期进行,且您并未真正理解应该如何采取实际行动,那么这件事是不会做好的。 但如果设置完全相同,为什么不使用它们呢? 请注意,如果找不到指定资产的设置文件,则可以理解为您希望使用 MetaTrader 5 默认的设置,这些设置在 DEFAULT.TPL 文件中定义,其位于 Profiles\Template 目录当中。 但首先我们需要了解一件重要的事情。 为什么我没有在 ChartApplyTemplate 函数中指定一个目录? 这是因为 MetaTrader 5 遵照一定的逻辑执行搜索。 了解这种逻辑是如何运作的,有助于您以更有趣、压力更小的方式理解状况。

想象一下下面的场景,我们用下面一行替换高亮示意的行:

if (!ChartApplyTemplate(m_handle, "MyTemplates\\" + szTemplate + ".tpl")) if (type == SYMBOL) ChartApplyTemplate(m_handle, ""MyTemplates\\Default.tpl");

MetaTrader 5 首先在自定义指标可执行文件所在目录的 MYTEMPLATES 子目录中搜索设置文件,也就是说,如果所创建的多个指标的可执行文件处于同一文件夹中,且含有 MYTEMPLATES 文件夹,MetaTrader 5 则在该文件夹中搜索设置文件。 不过,若该处没有发现,则它将在 MQL5\Profiles\Templates\MyTemplates 目录中搜索相同文件,这就是为什么我以前没有展示这个。 但它不仅如此。 同一代码段中还有另一个细节:

if (!ChartApplyTemplate(m_handle, "\\MyTemplates\\" + szTemplate + ".tpl")) if (type == SYMBOL) ChartApplyTemplate(m_handle, ""\\MyTemplates\\Default.tpl");

一个小细节改变了一切:现在 MetaTrader 5 首先尝试在 MQL5 MyTemplates 目录中查找该文件,如果未找到该文件,它则遵循上述步骤。 您可以在 ChartApplyTemplate 文档中找到这些信息,因为我不想让那些不知道 MetaTrader 5 如何工作的人感到困惑。 但是现在您已经足够了解搜索是如何执行的,故您可以创建变体,因为您知道文件放在何处。

在我们的类里,下一个函数发生了重大变化,如下所示:

void Resize(void)
{
#define macro_SetInteger(A, B) ObjectSetInteger(m_Id, m_Info[c0].szObjName, A, B)
        int x0 = 0, x1, y = (int)(ChartGetInteger(m_Id, CHART_HEIGHT_IN_PIXELS, m_IdSubWin));
        x1 = (int)((ChartGetInteger(m_Id, CHART_WIDTH_IN_PIXELS, m_IdSubWin) - m_Aggregate) / (m_Counter > 0 ? (m_CPre == m_Counter ? m_Counter : (m_Counter - m_CPre)) : 1));
        for (char c0 = 0; c0 < m_Counter; x0 += (m_Info[c0].width > 0 ? m_Info[c0].width : x1), c0++)
        {
                macro_SetInteger(OBJPROP_XDISTANCE, x0);
                macro_SetInteger(OBJPROP_XSIZE, (m_Info[c0].width > 0 ? m_Info[c0].width : x1));
                macro_SetInteger(OBJPROP_YSIZE, y);
        }
        ChartRedraw();
#undef macro_SetInteger
}

所选行示意此函数中最重要的计算。 它们会按照用户想要的方式调整窗口,但已设置为固定大小的窗口将创建一个自由区域,其余的会有自己定义的尺寸。 但如果主窗口的宽度减小,蓝色的计算代码将不允许减小固定尺寸窗口的宽度,这可能会导致问题。 但我们暂时不谈这个问题,因为固定宽度的窗户还有其它好处,在未来会进行更好的探索。 在我们的类中最后一个变化如下:

void AddThese(const eTypeChart type, string szArg)
{
        string szLoc;
        int i0, iSize;
//....
        Decode(szLoc, i0, iSize);
        AddTemplate(type, szLoc, i0, iSize);
//....
}

唯一的变化以高亮示意,所以没有什么特别的。


结束语

我希望这篇文章和其它文章一样,能展示结构化编程是多么有趣。 配合一些调整和耐心,我们可以为系统添加很多功能。 代码重用将令维护更加容易,因为若是复用完善的代码越多,包含错误的可能性就越小。 在下一篇文章中,我们将把这个系统引入对许多人来说更有趣的其它地方,并尝试了解编程的更多细节。 期待再见...



由MetaQuotes Software Corp. 译自葡萄牙文。
原文: https://www.mql5.com/pt/articles/10239

来自专业程序员的提示(第三部分):日志。 连接到 Seq 日志收集和分析系统 来自专业程序员的提示(第三部分):日志。 连接到 Seq 日志收集和分析系统
Logger 类的实现能够统一和结构化打印到智能系统栏的日志消息。 连接到 Seq 日志收集和分析系统。 在线监视日志消息。
在一张图表上的多个指标(第 02 部分):首次实验 在一张图表上的多个指标(第 02 部分):首次实验
在前一篇文章“在一张图表上的多个指标”中,我介绍了如何在一张图表上加载多个指标的概念和基本知识。 在本文中,我将提供源代码,并对其进行详解。
DoEasy 函数库中的图形(第九十八部分):移动扩展的标准图形对象的轴点 DoEasy 函数库中的图形(第九十八部分):移动扩展的标准图形对象的轴点
在本文中,我将继续扩展的标准图形对象的开发,创建移动复合图形对象轴点的功能,通过控制点来管理图形对象轴点坐标。
您能用移动平均线做什么呢 您能用移动平均线做什么呢
本文研究了若干种移动平均指标的应用方法。 每种方法涉及到的曲线分析,都配有思想实现的可视化指标。 在大多数情况下,这里展示的所有思路均属于它们尊敬的作者。 我唯一的任务就是把它们归纳起来,令您看到其主要作用,并希望做出更合理的交易决策。 MQL5 熟练程度 — 基本。