从头开始开发智能交易系统(第 8 部分):概念上的飞跃

Daniel Jose | 24 六月, 2022

概述

有时,在开发一些项目时,我们也许会发现新的思路和新的可能特性,这些特性能够派上用场,并为我们正在创建的系统提供极大的改进。 但问题出来了:实现新功能的最简单途径是什么?

问题在于,有时我们不得不忘记已经开发的一切,并从头重新开始。 这令人十分沮丧。 随着时间的推移,在经历了 20 多年的 C++ 编程之后,我提炼出了某种思路。 我们开发出一些概念,帮助我们计划事物,并可以最小的代价进行修改;但有时事物会发生变化,变得比我们最初估计的要复杂得多。

直至目前,我们一直在以这种途径构建 EA,即它可以接收新代码,而不必丢失当前功能:我们只是简单地创建和添加新类。 而现在,我们需要后撤一步,然后向前迈出两步。 后撤一步将允许我们引入新的功能。 该功能是一个窗口类,包含一些基于模板的信息;这是本文的第一部分。 我们将彻底修改代码,同时保留目前所有的功能;而在第二部分中,我们将处理 IDE。


计划

我们的智能交易系统目前是在对象类中构造的。 正如下图所见。

该系统目前运行良好,非常稳定。 但现在,我们必须如下所示重构 EA。 您也许已注意到还有一个额外的类,而 C_TemplateChart 和 C_SubWindow 的位置业已更改。


此种重构的目的是什么? 问题是,以浮动窗口的实现方式不适合包含资产数据的窗口,因此有必要修改该类。 然而,这种变化不仅在结构项上更具美感,当然还需要对代码进行极端的修改,故此它将与以前的代码有很大的区别。

那么,我们就开始工作吧。


实际的实现

1. 智能交易系统内部代码的变更

第一个重大变化是从 EA 初始化文件开始。 参见以下代码:

input group "Window Indicators"
input string                    user01 = "";                    //Subwindow indicators
input group "WallPaper"
input string                    user10 = "Wallpaper_01";        //Used BitMap
input char                      user11 = 60;                    //Transparency (from 0 to 100)
input C_WallPaper::eTypeImage   user12 = C_WallPaper::IMAGEM;   //Chart background type
input group "Chart Trader"
input int                       user20   = 1;                   //Leverage factor
input int                       user21   = 100;                 //Take Profit (financial)
input int                       user22   = 75;                  //Stop Loss (financial)
input color                     user23   = clrBlue;             //Price line color
input color                     user24   = clrForestGreen;      //Take Profit line color
input color                     user25   = clrFireBrick;        //Stop line color
input bool                      user26   = true;                //Day Trade?
input group "Volume At Price"
input color                     user30  = clrBlack;             //Bar color
input char                      user31  = 20;                   //Transparency (from 0 to 100 )
//+------------------------------------------------------------------+
C_TemplateChart Chart;
C_WallPaper     WallPaper;
C_VolumeAtPrice VolumeAtPrice;
//+------------------------------------------------------------------+
int OnInit()
{
        static string   memSzUser01 = "";
        
        Terminal.Init();
        WallPaper.Init(user10, user12, user11);
        if (memSzUser01 != user01)
        {
                Chart.ClearTemplateChart();
                Chart.AddThese(memSzUser01 = user01);
        }
        Chart.InitilizeChartTrade(user20, user21, user22, user23, user24, user25, user26);
        VolumeAtPrice.Init(user24, user25, user30, user31);
        
   OnTrade();
        EventSetTimer(1);
   
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+


注意,现在只有一个变量来指示将加载哪些模板。 除了高亮显示的部分外,代码的其余部分似乎保持原样。 也许还未彻底清楚它在这里做了什么,也不清楚为什么这段高亮显示的代码被放在 EA 的初始化部分。 当我们把 EA 加载到图表上时,它会创建一些东西,在正常操作中还可以修改它们。 以前,所添加的高亮显示代码是没有意义的,因为一切都是为了协同工作,修改不会影响 EA 行为或外观。 但是,当我们添加浮动指标时,会发生一些恼人的事情:每次我们变更时间帧时,EA 就会重新启动,窗口也会回归其原始状态。 如果您不去销毁它们,那么废品会堆积在图表上;如果您销毁它们不正确,则它们会在原来的地方重建,这也是一个极大的麻烦。 如果用户没有更改所需的模板,高亮显示的代码会防止浮动窗口被错误地销毁,但如果模板发生了变更,则 EA 将重新正常启动。 这非常简单,但极具效率。

接下来要注意的是有关内部消息传递系统。 以前它有一个附加变量,不过我们把它删除了,因此代码现在如下所示:

void OnTick()
{
        Chart.DispatchMessage(CHARTEVENT_CHART_CHANGE, 0, NanoEA.CheckPosition(), C_Chart_IDE::szMsgIDE[C_Chart_IDE::eRESULT]);
}


系统现在能更有效地运用 MQL5 消息交换系统,而消息发送与 OnChartEvent 函数本身非常相似。 这允许在没有任何压力的情况下将参数传递给对象类,从而每个类都能以最合适的方式处理 MetaTrader 5 系统生成的事件消息。 通过这种途径,我们进一步把每个对象类隔离开,因此 EA 可以针对每种类型的用户采用更加多样化的形式,且所需代价最少。


2. 子窗口变更的支持代码

直到此刻,子窗口代码都非常简单,但它有一个问题:出于这样或那样的原因,EA 无法删除已创建子窗口。 重新打开智能交易系统时,它会创建一个新的子窗口,导致系统失去对其的控制。 令人惊讶的是,修复这个问题极其容易。 首先,查看下面显示的支持文件片段:

int OnInit()
{
        IndicatorSetString(INDICATOR_SHORTNAME, "SubWinSupport");
        
        return INIT_SUCCEEDED;
}

高亮显示的行将为支持文件创建别名。 EA 能看到此 EA 的别名 ,并检查子窗口系统是否已加载。 由于 EA 只检查别名,因此该操作的执行与文件名无关。 稍后将使用相同的代码类型来支持 EA 中的其它内容。 在此我不会详细介绍,但稍后,我会在另一篇文章中,解释如何运用高亮显示的代码。

现在,我们看看加载和创建子窗口的代码。 如下所示:

void Init(void)
{
        int i0;
        if ((i0 = ChartWindowFind(Terminal.Get_ID(), def_Indicador)) == -1)
                ChartIndicatorAdd(Terminal.Get_ID(), i0 = (int)ChartGetInteger(Terminal.Get_ID(), CHART_WINDOWS_TOTAL), iCustom(NULL, 0, "::" + def_Resource));
        m_IdSubWinEA = i0;
}


如您所见,这有多简单,但此代码不是公开的,而是由另一个公开代码访问:

inline int GetIdSubWinEA(void)
{
        if (m_IdSubWinEA < 0) Init();
        return m_IdSubWinEA;
}


但为什么要以这种方式实现呢? 也许会发生这样的情况,即 EA 在子窗口中不用任何指标,当系统意识到这一点时,它会从图表中删除子窗口,并仅在必要时才创建它。 但这个决定不是由 EA 代码做出的,而是出自 C_TemplateChart 类。


3. 新的 C_TemplateChart 类

查看以下动画:

注意,我们现在有一条垂直线,提示我们正在分析的位置。 这些线相互独立。 以前没有提供这些数据,因此很难根据图表来分析指标的某些点。 这是 C_TemplateChart 类中包含的改进之一。 我们来看看类中的代码,以便了解进一步的变化,因为它们是实质性的。

我们看看以下声明变量的代码:

class C_TemplateChart : public C_SubWindow
{
#define def_MaxTemplates                8
#define def_NameTemplateRAD     "IDE"
//+------------------------------------------------------------------+
        private :
//+------------------------------------------------------------------+
        enum eParameter {TEMPLATE = 0, PERIOD, SCALE, WIDTH, HEIGHT};
//+------------------------------------------------------------------+
                struct st
                {
                        string          szObjName,
                                        szSymbol,
                                        szTemplate,
                                        szVLine;
                        int             width,
                                        scale;
                        ENUM_TIMEFRAMES timeframe;
                        long            handle;
                }m_Info[def_MaxTemplates];
                int     m_Counter,
                        m_CPre,
                        m_Aggregate;
                struct st00
                {
                        int     counter;
                        string  Param[HEIGHT + 1];
                }m_Params;


首先要注意的是,C_TemplateChart 类将扩展 C_SubWindow 类。 这部分代码似乎并无特别之处,但请注意高亮显示的部分:它指向一个内部数据分析系统,通过该系统可以相应地创建和呈现用户请求的指标。 现在,针对描述用户将如何指示事物的系统开始标准化。 即使它看起来令人困惑,但随着时间的推移,它会变得越来越清晰。 为了解释新格式,有必要分析以下片段,该片段负责分析用户的请求:

int GetCommand(int iArg, const string szArg)
{
        for (int c0 = TEMPLATE; c0 <= HEIGHT; c0++) m_Params.Param[c0] = "";
        m_Params.counter = 0;
        for (int c1 = iArg, c2 = 0; szArg[iArg] != 0x00; iArg++) switch (szArg[iArg])
        {
                case ')':
                case ';':
                        m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2);
                        for (; (szArg[iArg] != 0x00) && (szArg[iArg] != ';'); iArg++);
                        return iArg + 1;
                case ' ':
                        c2 += (c1 == iArg ? 0 : 1);
                        c1 = (c1 == iArg ? iArg + 1 : c1);
                        break;
                case '(':
                case ',':
                        c2 = (m_Params.counter == SCALE ? (c2 >= 1 ? 1 : c2) : c2);
                        m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2);
                        c2 = 0;
                        c1 = iArg + 1;
                        break;
                default:
                        c2++;
                        break;
        }
        return -1;
}


首先,清理结构中以前的所有数据。 然后开始逐个分析并接收参数(如果有)。 它们不是必需的,但它们指明了如何向用户呈现内容。 该系统是自扩展的。 故此,如果您打算添加更多信息,您只需在 eParameter 枚举中指定即可。 目前,系统有五个参数,它们的指定顺序应与 eParameters 枚举中所示的顺序相同。 该枚举在变量声明部分高亮显示。 参数的正确顺序及其解释如下所示。

参数 结果
1. TEMPLATE 或 ASSET 指定是要查看模板亦或资源
2. PERIOD 如果已指定,将按照指定的固定时间段设置指标,就像以前一样
3. SCALE 如果已指定,指标将绑定到固定标尺。
4. WIDTH 如果已指定,按照参数设置指标在窗口中的宽度。
5. HEIGHT 这个新参数将在本文后面讨论 — 它表示使用浮动窗口。

目前唯一不能从参数中获益的结构是 IDE,但这会得到修复,在下一篇文章中,我将展示如何从 IDE 中获益。 本文的重点是关注其它系统。

现在假设出于某种原因,我们希望允许用户控制指标垂直线的颜色。 参数分析代码无需进行任何更改 — 简单地按以下方式进行修改:

class C_TemplateChart : public C_SubWindow
{
#define def_MaxTemplates        8
#define def_NameTemplateRAD     "IDE"
//+------------------------------------------------------------------+
        private :
//+------------------------------------------------------------------+
        enum eParameter {TEMPLATE = 0, COLOR_VLINE, PERIOD, SCALE, WIDTH, HEIGHT};
//+------------------------------------------------------------------+

// ... Internal code ....

                struct st00
                {
                        int     counter;
                        string  Param[HEIGHT + 1];
                }m_Params;


系统将自动识别在一次调用中的六个参数。 现在又出现了另一个问题。 虽然上面的 GetCommand 代码工作正常,但其中存在一个漏洞。 当我们创建自用系统时,我们往往不会注意到这个漏洞。 但若把该系统提供给其他人使用时,错误就会变得很明显,对于如何解决该问题,也许会令一些经验不足的程序员束手无策。 这就是为什么面向对象编程受到高度重视的原因 — 它允许为最感兴趣的程序创建最合用的模型。 OOP 的先决条件之一是确保数据和类变量的正确初始化。 为了保证这一点,必须测试所有内容。 虽然 GetCommand 代码看起来正确,但实际上它包含一个漏洞 — 它没有检查最大参数限制。 如果原始模型只接受五个参数,那么如果用户设置了六个参数会发生什么? 这是需要避免的:我们不应该假设一切都会成功,而应该保证一切都会成功。 因此,需要按如下所示调整代码(调整高亮显示部分)。

int GetCommand(int iArg, const string szArg)
{
        for (int c0 = TEMPLATE; c0 <= HEIGHT; c0++) m_Params.Param[c0] = "";
        m_Params.counter = 0;
        for (int c1 = iArg, c2 = 0; szArg[iArg] != 0x00; iArg++) switch (szArg[iArg])
        {
                case ')':
                case ';':
                        m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2);
                        for (; (szArg[iArg] != 0x00) && (szArg[iArg] != ';'); iArg++);
                        return iArg + 1;
                case ' ':
                        c2 += (c1 == iArg ? 0 : 1);
                        c1 = (c1 == iArg ? iArg + 1 : c1);
                        break;
                case '(':
                case ',':
                        if (m_Params.counter == HEIGHT) return StringLen(szArg) + 1;
                        c2 = (m_Params.counter == SCALE ? (c2 >= 1 ? 1 : c2) : c2);
                        m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2);
                        c2 = 0;
                        c1 = iArg + 1;
                        break;
                default:
                        c2++;
                        break;
        }
        return -1;
}

通过简单地添加一行,我们就能防止系统发生意外结果,因为如果期望的最后一个参数是 HEIGHT,但实际上却不是最后一个,这在逻辑上意味着有问题,因此系统会忽略随后声明的所有内容,来避免出现更多问题。

如果您不了解系统是如何识别模式和参数,请使用以下语法:

Parameter_00 (Parameter_01, Parameter_02, Parameter_03, Parameter_04)

其中,Parameter_00 指定要所用的模板,其余的则用逗号(,)分隔,且指示的数值均应在 eParameter 枚举中定义。 如果我们只想更 Parameter_03,,那么可以将其余的保留为空,如下图所示。 在该示意图中,我展示的系统会按照用户想要的方式工作。

       

请注意,我们有一个标准化的指示,它与函数调用非常相似,但这似乎令人困惑。 下面是实际发生的情况:我们指定 RSI 模板,然后不指定周期或标尺。 这些值保留为空,如此系统即可理解,它必须遵循主图表。 但我们指定了宽度和高度,因此系统理解这应该显示在浮动窗口中,故 RSI 指标显示在浮动窗口中。 在 ADX 模板中,我们仅指示宽度,如此系统就按子窗口中定义的宽度来显示它。 Stoch 指标将占用剩余的整个子窗口,与 ADX 共享空间。 但如果用户想要更改某些内容,这并不困难,看看当我们指定 ADX 的高度时会发生什么。

系统将立即改变 ADX 的显示方式 — 将其置于浮动窗口当中,同时将整个子窗口留给 Stoch。 每个浮动窗口彼此完全独立。 但它们未超出我们所能看到的范围,请看下一张图片。

请注意,子窗口已被删除,因为不再需要它。 但是,哪个函数负责所有这些? 如下所示 — 只需稍作修改即可完成许多有趣的事情:

void AddTemplate(void)
{
        ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT;
        string sz0 = m_Params.Param[PERIOD];
        int w, h, i;
        bool bIsSymbol;

        if (sz0 == "1M") timeframe = PERIOD_M1; else
        if (sz0 == "2M") timeframe = PERIOD_M2; else
        if (sz0 == "3M") timeframe = PERIOD_M3; else
        if (sz0 == "4M") timeframe = PERIOD_M4; else
        if (sz0 == "5M") timeframe = PERIOD_M5; else
        if (sz0 == "6M") timeframe = PERIOD_M6; else
        if (sz0 == "10M") timeframe = PERIOD_M10; else
        if (sz0 == "12M") timeframe = PERIOD_M12; else
        if (sz0 == "15M") timeframe = PERIOD_M15; else
        if (sz0 == "20M") timeframe = PERIOD_M20; else
        if (sz0 == "30M") timeframe = PERIOD_M30; else
        if (sz0 == "1H") timeframe = PERIOD_H1; else
        if (sz0 == "2H") timeframe = PERIOD_H2; else
        if (sz0 == "3H") timeframe = PERIOD_H3; else
        if (sz0 == "4H") timeframe = PERIOD_H4; else
        if (sz0 == "6H") timeframe = PERIOD_H6; else
        if (sz0 == "8H") timeframe = PERIOD_H8; else
        if (sz0 == "12H") timeframe = PERIOD_H12; else
        if (sz0 == "1D") timeframe = PERIOD_D1; else
        if (sz0 == "1S") timeframe = PERIOD_W1; else
        if (sz0 == "1MES") timeframe = PERIOD_MN1;
        if ((m_Counter >= def_MaxTemplates) || (m_Params.Param[TEMPLATE] == "")) return;
        bIsSymbol = SymbolSelect(m_Params.Param[TEMPLATE], true);
        w = (m_Params.Param[WIDTH] != "" ? (int)StringToInteger(m_Params.Param[WIDTH]) : 0);
        h = (m_Params.Param[HEIGHT] != "" ? (int)StringToInteger(m_Params.Param[HEIGHT]) : 0);
        i = (m_Params.Param[SCALE] != "" ? (int)StringToInteger(m_Params.Param[SCALE]) : -1);
        i = (i > 5 || i < 0 ? -1 : i);
        if ((w > 0) && (h > 0)) AddIndicator(m_Params.Param[TEMPLATE], 0, -1, w, h, timeframe, i); else
        {
                SetBase(m_Params.Param[TEMPLATE], (bIsSymbol ? m_Params.Param[TEMPLATE] : _Symbol), timeframe, i, w);
                if (!ChartApplyTemplate(m_Info[m_Counter - 1].handle, m_Params.Param[TEMPLATE] + ".tpl")) if (bIsSymbol) ChartApplyTemplate(m_Info[m_Counter - 1].handle, "Default.tpl");
                if (m_Params.Param[TEMPLATE] == def_NameTemplateRAD)
                {
                        C_Chart_IDE::Create(GetIdSubWinEA());
                        m_Info[m_Counter - 1].szVLine = "";
                }else
                {
                        m_Info[m_Counter - 1].szVLine = (string)ObjectsTotal(Terminal.Get_ID(), -1, -1) + (string)MathRand();
                        ObjectCreate(m_Info[m_Counter - 1].handle, m_Info[m_Counter - 1].szVLine, OBJ_VLINE, 0, 0, 0);
                        ObjectSetInteger(m_Info[m_Counter - 1].handle, m_Info[m_Counter - 1].szVLine, OBJPROP_COLOR, clrBlack);
                }
                ChartRedraw(m_Info[m_Counter - 1].handle);
        }
}

高亮显示的部分示意数据在屏幕上的显示方式选择。 该代码与之前的已有代码没有太大区别。 但这些测试保证了系统将按照用户想要的方式运行。 到目前为止,所有这些都不需要在新模型中重新构造代码,但当我们查看负责调整子窗口大小的函数时,情况会发生变化。

void Resize(void)
{
#define macro_SetInteger(A, B) ObjectSetInteger(Terminal.Get_ID(), m_Info[c0].szObjName, A, B)
        int x0, x1, y;
        if (!ExistSubWin()) return;
        x0 = 0;
        y = (int)(ChartGetInteger(Terminal.Get_ID(), CHART_HEIGHT_IN_PIXELS, GetIdSubWinEA()));
        x1 = (int)((Terminal.GetWidth() - 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);
                if (m_Info[c0].szTemplate == "IDE") C_Chart_IDE::Resize(x0);
        }
        ChartRedraw();
#undef macro_SetInteger
}

高亮显示的行可防止系统在老旧的基础上构建。 此行将检查是否有 EA 打开和维护的子窗口,如果没有,则返回,函数不执行其它操作。 但如果存在这样一个子窗口,则应根据需要调整其中所有内容的大小,并且正是因为进行了此测试,系统才得以彻底改造。

下面是另一个经过修改的函数:

void DispatchMessage(int id, long lparam, double dparam, string sparam)
{
        int mx, my;
        datetime dt;
        double p;

        C_Chart_IDE::DispatchMessage(id, lparam, dparam, sparam);
        switch (id)
        {
                case CHARTEVENT_MOUSE_MOVE:
                        mx = (int)lparam;
                        my = (int)dparam;
                        ChartXYToTimePrice(Terminal.Get_ID(), mx, my, my, dt, p);
                        for (int c0 = 0; c0 < m_Counter; c0++)  if (m_Info[c0].szVLine != "")
                        {
                                ObjectMove(m_Info[c0].handle, m_Info[c0].szVLine, 0, dt, 0);
                                ChartRedraw(m_Info[c0].handle);
                        }
                        break;
                case CHARTEVENT_CHART_CHANGE:
                        Resize();
                        for (int c0 = 0; c0 < m_Counter; c0++)
                        {
                                ObjectSetInteger(Terminal.Get_ID(), m_Info[c0].szObjName, OBJPROP_PERIOD, m_Info[c0].timeframe);
                                ObjectSetInteger(Terminal.Get_ID(), m_Info[c0].szObjName, OBJPROP_CHART_SCALE,(m_Info[c0].scale < 0 ? ChartGetInteger(Terminal.Get_ID(),CHART_SCALE):m_Info[c0].scale));
                        }
                        break;
        }
}


高亮显示的部分确实值得特别注意。 它的作用是什么? 此代码将在正确的位置和正确的模板中显示垂直线。 代码的其余部分将在图表变更时简单地维护和调整模板。

在对象类中进行此操作,比之在 EA 的 OnChartEvent 系统中有更多好处。 主要是,每个类都可以处理 MetaTrader 5 发送给 EA 的事件。 我们可令每个类完成它的工作,替代将所有内容集中在一个函数中;如此,如果我们不想在 EA 中使用该类,我们就可简单地删除它,而不会对其余代码产生任何副作用。

这样的编程很优美,是不是呢? 我热爱编程...

在继续讨论本文的下个论点之前,我们将简要评论参数 1 和 2 中可以使用的值。 参数 1 可赋予以下数值: 1M, 2M, 3M, 4M, 5M, 6M, 10M, 12M, 15M, 20M, 30M, 1H, 2H, 3H, 4H, 6H, 8H, 12H, 1D, 1S, 1MONTH。 这些值不是随机的 — 它们来自 ENUM_TIMEFRAME 枚举,并且完全重复您使用常规图表时的操作。 参数 2 可以取 0 到 5 之间的值,其中 0 是最远的标尺,5 是最靠近的标尺。 有关更多详细信息,请参阅 CHART_SCALE


3.4 支持浮动窗口

现在我们来了解浮动窗口是如何创建和维护的,因为如果不了解这一点,就不可能真正运用系统。 负责此操作的对象类名为 C_ChartFloating。 有人可能会想:为什么不用 MQL5 标准库中的 Control 类? 原因很简单。 控件类允许我们创建和维护一个窗口,该窗口的功能与机器上的操作系统非常相似,但对于我们的目的而言,它过于夸张了。 我们需要更简约的东西。 使用控件类做我们想做的事情就像使用火箭筒杀死苍蝇一样,因此我们使用 C_ChartFloating 类 — 它包含支持浮动窗口所需的最少元素,同时允许我们控制它们。

这个类本身不需要太多解释,因为我们只需要创建 4 个图形对象。 但在内部功能中,有两个地方值得特别注意。 我们从创建窗口的函数开始:

bool AddIndicator(string sz0, int x = 0, int y = -1, int w = 300, int h = 200, ENUM_TIMEFRAMES TimeFrame = PERIOD_CURRENT, int Scale = -1)
{
        m_LimitX = (int)ChartGetInteger(Terminal.Get_ID(), CHART_WIDTH_IN_PIXELS);
        m_LimitY = (int)ChartGetInteger(Terminal.Get_ID(), CHART_HEIGHT_IN_PIXELS);
        if (m_MaxCounter >= def_MaxFloating) return false;
        y = (y < 0 ? m_MaxCounter * def_SizeBarCaption : y);
        CreateBarTitle();
        CreateCaption(sz0);
        CreateBtnMaxMin();
        CreateRegion(TimeFrame, Scale);
        m_Win[m_MaxCounter].handle = ObjectGetInteger(Terminal.Get_ID(), m_Win[m_MaxCounter].szRegionChart, OBJPROP_CHART_ID);
        ChartApplyTemplate(m_Win[m_MaxCounter].handle, sz0 + ".tpl");   
        m_Win[m_MaxCounter].szVLine = (string)ObjectsTotal(Terminal.Get_ID(), -1, -1) + (string)MathRand();
        ObjectCreate(m_Win[m_MaxCounter].handle, m_Win[m_MaxCounter].szVLine, OBJ_VLINE, 0, 0, 0);
        ObjectSetInteger(m_Win[m_MaxCounter].handle, m_Win[m_MaxCounter].szVLine, OBJPROP_COLOR, clrBlack);
        m_Win[m_MaxCounter].PosX = -1;
        m_Win[m_MaxCounter].PosY = -1;
        m_Win[m_MaxCounter].PosX_Minimized = m_Win[m_MaxCounter].PosX_Maximized = x;
        m_Win[m_MaxCounter].PosY_Minimized = m_Win[m_MaxCounter].PosY_Maximized = y;
        SetDimension(w, h, true, m_MaxCounter);
        SetPosition(x, y, m_MaxCounter);
        ChartRedraw(m_Win[m_MaxCounter].handle);
        m_MaxCounter++;
        return true;
}

该代码创建了所有必要的支持,如此您就可以在 CHART 对象上应用模板。 它是在高亮显示的代码部分实现的,请注意,调用此函数所需的真正唯一参数是模板名称。 所有其它值都已预先初始化,但没有任何东西阻止您指定需要哪些值。 对于创建的每个新窗口,下一个窗口将略微偏移,如此在默认情况下就不会与其它窗口重叠。 如下行所示:

y = (y < 0 ? m_MaxCounter * def_SizeBarCaption : y);


该类中下一个有趣的函数可处理消息。 此为其代码:

void DispatchMessage(int id, long lparam, double dparam, string sparam)
{
        int mx, my;
        datetime dt;
        double p;
        static int six = -1, siy = -1, sic = -1;
                                
        switch (id)
        {
                case CHARTEVENT_MOUSE_MOVE:
                        mx = (int)lparam;
                        my = (int)dparam;
                        if ((((int)sparam) & 1) == 1)
                        {
                                if (sic == -1)  for (int c0 = m_MaxCounter - 1; (sic < 0) && (c0 >= 0); c0--)
                                        sic = (((mx > m_Win[c0].PosX) && (mx < (m_Win[c0].PosX + m_Win[c0].Width)) && (my > m_Win[c0].PosY) && (my < (m_Win[c0].PosY + def_SizeBarCaption))) ? c0 : -1);
                                if (sic >= 0)
                                {
                                        if (six < 0) ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, false);
                                        six = (six < 0 ? mx - m_Win[sic].PosX : six);
                                        siy = (siy < 0 ? my - m_Win[sic].PosY : siy);
                                        SetPosition(mx - six, my - siy, sic);
                                }
                        }else
                        {
                                if (six > 0) ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, true);
                                six = siy = sic = -1;
                        }
                        ChartXYToTimePrice(Terminal.Get_ID(), mx, my, my, dt, p);
                        for (int c0 = 0; c0 < m_MaxCounter; c0++)
                                ObjectMove(m_Win[c0].handle, m_Win[c0].szVLine, 0, dt, 0);
                        break;
                case CHARTEVENT_OBJECT_CLICK:
                        for (int c0 = 0; c0 < m_MaxCounter; c0++) if (sparam == m_Win[c0].szBtnMaxMin)
                        {
                                SwapMaxMin((bool)ObjectGetInteger(Terminal.Get_ID(), m_Win[c0].szBtnMaxMin, OBJPROP_STATE), c0);
                                break;
                        }
                        break;
                case CHARTEVENT_CHART_CHANGE:
                        for(int c0 = 0; c0 < m_MaxCounter; c0++)
                        {
                               ObjectSetInteger(Terminal.Get_ID(), m_Win[c0].szRegionChart, OBJPROP_PERIOD, m_Win[c0].TimeFrame);
                               ObjectSetInteger(Terminal.Get_ID(), m_Win[c0].szRegionChart, OBJPROP_CHART_SCALE,(m_Win[c0].Scale < 0 ? ChartGetInteger(Terminal.Get_ID(),CHART_SCALE):m_Win[c0].Scale));
                        }
                        m_LimitX = (int)ChartGetInteger(Terminal.Get_ID(), CHART_WIDTH_IN_PIXELS);
                        m_LimitY = (int)ChartGetInteger(Terminal.Get_ID(), CHART_HEIGHT_IN_PIXELS);
                        break;
        }
        for (int c0 = 0; c0 < m_MaxCounter; c0++)
                ChartRedraw(m_Win[c0].handle);
}


此函数实现了 C_ChartFloating 类支持的所有事件处理。 无论有多少个窗口,它都将以相同的方式处理。 如果我们在智能交易系统的 OnChartEvent 函数中执行此操作,该函数将非常复杂,并且不太稳定。 通过在对象类中实现这里的功能,我们就能保证代码的完整性。 因此,如果我们不需要用到浮动窗口,那么我们所需要做的就是从类和可以访问文件的地方删除文件。 这样的实现令您的代码更快捷、更容易。

上述代码中还有一个有趣的部分。 它已被高亮显示,其内部代码如下:

void SwapMaxMin(const bool IsMax, const int c0)
{
        m_Win[c0].IsMaximized = IsMax;
        SetDimension((m_Win[c0].IsMaximized ? m_Win[c0].MaxWidth : 100), (m_Win[c0].IsMaximized ? m_Win[c0].MaxHeight : 0), false, c0);
        SetPosition((m_Win[c0].IsMaximized ? m_Win[c0].PosX_Maximized : m_Win[c0].PosX_Minimized), (m_Win[c0].IsMaximized ? m_Win[c0].PosY_Maximized : m_Win[c0].PosY_Minimized), c0);
}


以上代码起什么作用? 看起来太困惑了? 为了理解,我们仔细看看下面的动画。


当创建浮动窗口时,它有一个初始锚点,可由程序员或类本身的定位系统指定。 该定位点对于最大化窗口和最小化窗口都是相同的。 这些值不是固定的,即用户可以轻松更改这些点。

假设您需要空白图表上的某个位置,那么您可以将最大化窗口移到易于快速阅读的位置,然后最小化同一窗口,并将其移到另一个位置,例如屏幕的一角。 系统记住它,当您最大化窗口时,它将跳到最小化之前的最后那个锚点。 当窗口最小化时,相反的情况同样适用。


结束语

目前全部内容就是这些了。 在下一篇文章中,我们将把此功能扩展到 IDE 支持类。