English Русский Español Deutsch 日本語 Português
preview
构建自动运行的 EA(第 05 部分):手工触发器(II)

构建自动运行的 EA(第 05 部分):手工触发器(II)

MetaTrader 5交易 | 16 三月 2023, 08:55
2 733 0
Daniel Jose
Daniel Jose

概述

在上一篇题为构建自动运行的 EA (第 04 部分):手工触发器 (I) 的文章中,我已经展示了如何经由编程来发送市价订单,并用功能键和鼠标的组合下挂单。

在上一篇文章的末尾,我建议允许手工操作 EA 是合适的,至少在一段时间内。 事实证明,这比预期的要有趣得多,因为最初的想法是发表 3-4 篇文章,展示如何开始开发可以真正自动交易的 EA。 虽然这对程序员来说相当容易,但对于刚开始学习编程的初学者来说可能很困难,因为很少有材料可以清楚地解释如何实际针对某些东西编程。 甚至,局限于一个知识水平,并非每个人都应该擅长所有的事情。

由于许多人可能利用这个社区中发表的文章来开始学习编程,我认为这是一个分享我基于多年 C/C++ 编程经验的机会,并展示如何在 MQL5 中实现一些与 C/C++ 非常相似的东西。 我想展示,编程并不神秘,它都是很实在的。

好吧,为了令我们的 EA 在手工模式下的操作更加舒适,我们需要做一些事情。 这项工作对于程序员来说既简单又容易,因此我们可以直奔主题。 也就是说,我们将创建水平线,指示我们发送到交易服务器的订单限价位置。

当我们用鼠标下订单时,即当我们创建挂单时,这些限价更适合观察。 一旦订单已经在服务器上,指示就由 MetaTrader 5 平台管理。 但在实际发生这种情况之前,我们需要向用户显示最有可能放置订单限价的位置。 这是由我们程序员完成的。 我们从 MetaTrader 5 获得的唯一支持是在图表上使用水平线的可能性。 除此之外,所有工作都必须通过 EA 编程实现。

为此,我们简单地编写将这些指示线放置在图表正确位置的代码。 但我们不想以某种随机的方式做到这一点。 这应该受到相应的控制,因为我们不想破坏我们已经创建的代码,并且我们不想增加工作量,以防将来必须从 EA 中删除 C_Mouse 类和 OnChartEvent 事件处理程序。 这是因为自动 EA 不需要这些东西,但手工 EA 需要它们。 我们要确保这些东西的最低限度可用性。 


创建 C_Terminal 类

为此目的,我们将依手工操作创建一切便利。 我们需要添加指示线,表示将要发送的订单或持仓的可能限价。 在此过程中,我们将删除 C_Orders 和 C_Mouse 类中的重复代码。 故此,我们将有一个新的类:C_Terminal 类,它将帮助我们构建和隔离一些东西,从而令我们可以更舒适地工作。 通过使用此类,我们就能够在将来构建自动和手工 EA,而不会冒着在新 EA 中产生某种灾难性故障的风险。

最大的问题是,在构建新的自动化 EA 时,许多人经常从头开始。 这种方式通常会导致许多错误,因为没有足够的检查。

诚然,将这些类转换为私有库会非常有趣。 但由于我们在模式上的意图不同,我们现在不会考虑它。 或许,我将来会这样做。 我们看看实际上我们要做什么。 我们将从以下内容开始:如往常一样,我们创建一个名为 C_Terminal.mqh 的头文件。 这是最基本的代码,它始终存在于我们将要创建的每个类当中。 它如下所示:

class C_Terminal
{
        private :
        public  :
};

始终在类中初始化代码,以这种方式,您就永远不会忘记某些关键点必须在私密部分中,而其它要点可以放在公开部分之中。 即使您在类里没有任何私密内容,澄清事情总是一个好主意。 主要是因为您可以向其他人展示您的代码。

分隔良好、且编写精良的代码,即易于阅读的代码,肯定会吸引其他人的兴趣,并鼓励他们在您需要帮助解决问题时对其进行分析。 杂乱无章、没有任何组织、没有分栏、且没有解释性注释的代码则变得无趣。 即便这个想法很好,也没有人喜欢花时间组织别人的代码,以便理解您在让它干什么。

这是我的建议。 当然,我自己的代码也并不完美,但是:始终清理您的代码,每当您需要在一个过程中嵌套多行时都使用分栏,这很有帮助。 不仅仅是其他人,主要是您。 有时代码组织得非常糟糕,甚至它的创建者事后也无法弄明白。 其他程序员将如何做到这一点?

那好,我们开始编写代码,先为我们的类添加结构。 初期的代码行如下所示。

class C_Terminal
{
        protected:
//+------------------------------------------------------------------+
                struct stTerminal
                {
                        ENUM_SYMBOL_CHART_MODE ChartMode;
                        int     nDigits;
                        double  VolMinimal,
                                VolStep,
                                PointPerTick,
                                ValuePerPoint,
                                AdjustToTrade;
                };
//+------------------------------------------------------------------+
        private :
        public  :
};

在此,我们有一个新东西 — 保留字 protected。 但它告诉我们什么? 我们通常只使用 private 和 public 声明。 那,这又是什么? 实际上,它介于公开和私密之间。 若要理解正在发生的事情,我们需要了解面向对象编程的一些基本概念。

其中一个概念是继承。 但在我们深入研究继承主题之前,我们以独立个人为例来研究该类。 故此,为了更好地理解这个概念,请将每个类视为一个单独的、独特的、排他性的生物。 现在我们可以继续解释。

有些信息是公开的,不光维护它的个人,任何人都可以从使用它、并获取知识中受益。 这些信息始终放置在代码的公开部分。 而其他信息是个人的私密数据,也就是说,只有此人才能访问。 当该个体不复存在时,这些信息将随他一起消亡;他是唯一可以从中受益的人。 将信息视为个人独门绝技。 这个人不能教授、或传授给其他人,也没有人可以把它从他那里夺走。 这种类型的信息位于代码的私密部分。

但也有一些信息不符合这些概念中的任何一个。 它位于受保护部分,如此个人可能会、也可能不会用到它。 最主要的是,它可以传给氏族后代成员。 为了理解这是如何发生的,我们来深入研究继承的主题。

当我们探讨遗传的话题时,最简单的理解方法是参考血统。 有三种类型的继承:公开继承、私密继承和受保护继承。 在此,我谈及的是继承,而不是氏族中每个成员的个别问题。

在公开继承中,父母的信息、数据和内容被传递给子女、及其所有后代,包括孙辈、及以后的后代,理论上,血统以外的任何人都可以访问这些东西。 请注意该短语,从理论上讲,因为这种转移存在一些细微差别。 稍后我们将更详细地介绍这一点。 我们先关注继承。 至于私密继承,只有第一代才能接触到这些信息,而后代即便他们也是血统当中的一脉,也无法获得这些信息。

最后一件我们要解释的事是受保护的继承。 它创造了一些与私密继承非常相似的东西。 但是我们有一个激进的因素,这让很多人难以理解这些概念:父辈条款。 这是因为,即使在公开继承的情况下,也有一种传递信息的规则。 有些东西在血统之外是无法访问的。 若要理解这一点,请参阅下表,其中我简要总结了这个问题:

父类中的定义 继承类型 从子类访问 由调用子类访问 
private public: 访问被拒绝 不可访问基类数据或过程
public: public: 访问被允许 可以访问基类数据或过程
protected public: 访问被允许 不可访问基类数据或过程
private private 访问被拒绝 不可访问基类数据或过程
public: private 访问被允许 不可访问基类数据或过程
protected private 访问被允许 不可访问基类数据或过程
private protected 访问被拒绝 不可访问基类数据或过程
public: protected 访问被允许  不可访问基类数据或过程
protected protected 访问被允许 不可访问基类数据或过程

表 1)基于信息定义的继承系统

请注意,根据继承期间数据类型定义中所用的代码部分,子类可能有权访问数据,也可能无法访问数据。 但是,任何血统之外的调用都将无法访问,除非父类的数据被宣布为公开,且子类以公开方式继承时才会发生的独特情况。 此外,无法访问氏族之外的任何信息。

如果不理解 表 01 所示的思维逻辑,许多缺乏经验的程序员难以遵循面向对象编程。 这是因为实际上他们不明白事情是如何运作的。 那些关注我的文章,和我的代码的人,应该已经注意到我经常使用面向对象编程。

这是因为它在实现非常复杂的事情时提供了最高级别的安全性,否则这是不可能做到的。 我说的不仅仅是继承。 除此之外,我们还有多态性和封装,但这些都是在另外的时间谈论的主题。 虽然封装是表 01 的一部分,但它值得更详细的解释,但其超出了本文的范畴。

那么,我们继续。 如果您仔细观察,就会注意到上面代码中的结构与 C_Orders 类中的结构相同。 请注意这一点,因为 C_Order 类将丢失此数据的定义,并开始从 C_Terminal 类继承数据。 但现在,我们先留在 C_Terminal 类中。

接下来要添加到 C_Terminal 类的是函数,其对于 C_Mouse 类和 C_Orders 类两者通用。 这些函数将被添加到 C_Terminal 类的受保护部分,因此当 C_Mouse 和 C_Orders 类继承自 C_Terminal 时,这些函数和过程将遵循表 01。 如此,我们添加以下代码:

//+------------------------------------------------------------------+
inline double AdjustPrice(const double value)
                        {
                                return MathRound(value / m_TerminalInfo.PointPerTick) * m_TerminalInfo.PointPerTick;
                        }
//+------------------------------------------------------------------+
inline double FinanceToPoints(const double Finance, const uint Leverage)
                        {
                                double volume = m_TerminalInfo.VolMinimal + (m_TerminalInfo.VolStep * (Leverage - 1));
                                
                                return AdjustPrice(MathAbs(((Finance / volume) / m_TerminalInfo.AdjustToTrade)));
                        };
//+------------------------------------------------------------------+

这些代码在两个类中不再重复。 现在所有代码都独处在 C_Terminal 类中,这有助于其维护、测试和可能的修改。 因此,当我们使用和扩展它时,我们的代码将变得更加可靠和有吸引力。

在 C_Terminal 类内部还有几件事需要注意。 但首先我们看一下类构造函数。 它如下所示:

        public  :
//+------------------------------------------------------------------+
                C_Terminal()
                        {
                                m_TerminalInfo.nDigits          = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
                                m_TerminalInfo.VolMinimal       = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
                                m_TerminalInfo.VolStep          = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
                                m_TerminalInfo.PointPerTick     = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
                                m_TerminalInfo.ValuePerPoint    = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
                                m_TerminalInfo.AdjustToTrade    = m_TerminalInfo.ValuePerPoint / m_TerminalInfo.PointPerTick;
                                m_TerminalInfo.ChartMode        = (ENUM_SYMBOL_CHART_MODE) SymbolInfoInteger(_Symbol, SYMBOL_CHART_MODE);
                        }
//+------------------------------------------------------------------+

请注意,它几乎与 C_Orders 类中存在的相同。 现在,我们可以修改 C_Orders 类代码,如此它就继承了我们在 C_Terminal 类中实现的内容。 但尚有一件重要的事情。 查看包含上述构造函数中初始化结构声明的代码部分。 您可以看到它没有变量。 为什么?

原因是封装。 您不应允许类外部的代码访问和修改内部类变量的内容。 那样做是一个严重的编程错误,尽管编译器不会抱怨,但您永远不应该允许这样做。 绝对记住,类的所有全局变量必须始终在私密部分内声明。 变量声明如下所示。

//+------------------------------------------------------------------+
        private :
                stTerminal m_TerminalInfo;
        public  :
//+------------------------------------------------------------------+

请注意,全局类变量是在私密部分和公开部分之间定义的。 因此,它不适用于任何从 C_Terminal 继承的类,换言之,我们保证信息的封装,与此同时在我们的代码中加入继承。 除了扩展代码功效外,我们还以指数方式为其提高了健壮性级别。

但我们将如何访问上述类中的所需数据呢? 我们需要为父类的变量提供一定程度的访问权限,在本例中是 C_Terminal 类。 是的,我们需要这个。 但我们不应该通过公开或保护这些变量来做到这一点。 这样做是一个编程错误。 您必须添加一些手段,以便派生类可以访问父类的值。 但此处伴随一个危险,这很重要:不能允许任何派生类修改父类的变量。 

为此,我们需要以某种方式将变量转换为常量。 也就是说,父类可以根据需要修改变量的值。 如果任何子类想要更改父类的任何变量,则子类必须调用父类提供的某些过程,以便通知父类中存在的特定变量采用所需值。 这样的过程应该在父类中实现,它将检查子类传递来的数据是否有效。 如果有效,则该过程将在父类中推进由子类提请的更改。

但是绝对不能出现父类不知道的情况下,就任由子元素更改父数据。 我见过很多这样编写的潜在危险代码。 有人说,在父类内部调用过程来检查子类提供的数据会减慢代码速度,或导致程序在执行时崩溃。 但这是一个错误。 如果在父类内抛出不正确的数值,与其产生的成本和风险相比,节省调用数据检查过程所实现的小幅速度增加不值一提。 故此,不要担心这会拖慢代码速度。

现在我们又来到了另一个节点。 您作为程序员,已经或即将成为专业人士,应始终优先考虑将其它类继承的任何过程放在受保护的代码部分中,并且仅将其作为最后的手段,才把过程转移到公开区域。 我们始终优先考虑封装。 只有在确实必要的情况下,我们才会摆脱封装,并允许函数和任何过程的公开调用。但我们从不用于变量,因为它们总是私密的。

若要创建允许子类访问父类数据的过程或函数,我们将用到以下函数:

inline const stTerminal GetTerminalInfos(void) const
                        {
                                return m_TerminalInfo;
                        }

现在,我希望您能密切关注我将要解释的内容,因为它非常重要,且能决定编写精良代码、与应付差事的代码之间的区别。

贯穿整篇文章,我曾说过,我们必须以某种方式允许类外部的代码,访问类内部声明并使用的变量。 我说过,理想的情况是,变量在声明它的类中若有必要可以进行修改。 但是在类之外,变量应该被视为常量,即它的值不能被改变。

尽管极端简单,但上面的代码确实做到了这一点。 换言之,您可以保证在 C_Terminal 类内部我们有一个可访问的变量,且其值可以更改,但在类外部,同一变量将被视为常量。 它是如何完成的,为什么我们这里有两个保留字 const

我们来逐一研究:第一个保留字 const 通知编译器返回变量 m_TerminalInfo 应在调用方函数中视为常量。如果调用方尝试修改返回变量中所提供的任何结构成员的数值,编译器将生成错误,并阻断代码继续编译。 第二个保留字 const 通知编译器,如果出于任何原因在此处修改了某个值,编译器应返回错误。 因此,即使您想这样做,也无法修改该函数内部的任何数据,因为它的存在只是为了返回值。

一些程序员有时会犯这种错误:他们修改了函数或过程内部的变量值,而这些变量应该仅用于外部访问,而不是用于某种分解。 遵照上面的代码原理,可以避免这种类型的错误。

而我们尚未完成 C_Terminal 基类,但我们可以删除代码中的重复部分,从而令 C_Mouse 类拥有与 C_Orders 类相同的代码类型。 但由于修改 C_Mouse 类要更容易,我们来看看它继承了 C_Terminal 类后的样子。 这可从以下代码中看到:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_Terminal.mqh"
//+------------------------------------------------------------------+
#define def_MouseName "MOUSE_H"
//+------------------------------------------------------------------+
#define def_BtnLeftClick(A)     ((A & 0x01) == 0x01)
#define def_SHIFT_Press(A)      ((A & 0x04) == 0x04)
#define def_CTRL_Press(A)       ((A & 0x08) == 0x08)
//+------------------------------------------------------------------+
class C_Mouse : private C_Terminal
{
// Inner class code ....
};

在此处,我们包含 C_Terminal 类的头文件。 请注意,此处的文件名用双引号括起来。 这样会告诉编译器 C_Terminal.mqh 文件与 C_Mouse.mqh 文件位于同一目录中。 以这种方式,如果您需要将两个文件移动到另一处,编译器将始终能够找到正确的文件,因为它知道它们位于同一目录中。

现在,遵循始终以尽可能少的访问权限开始操作的念头,我们令 C_Mouse 类作为 C_Terminal 类的私密子类。 现在,您可以从 C_Mouse 类中删除 AdjustPrice 函数,以及 C_Mouse 类中存在的 PointPerTick 函数,因为现在您将调用来自 C_Terminal 类中的过程。 由于该类是私密继承的,并且 AdjustPrice 函数位于代码的受保护部分内,因此您从 C_Terminal 得到的结果将如表 01。 故此,在 C_Mouse 类之外已不可能像之前那样调用 AdjustPrice 过程。

不过,C_Mouse 类中的所有这些修改都是暂时的。 我们还会进行更多修改,添加手工运行 EA 时所需的限价线。 但我们稍后才会这样做。 我们看看如何在 C_Orders 类中进行更深刻的修改。 这些修改需要单独的主题。 那好,我们继续讨论它。


从 C_Terminal 继承后修改 C_Orders 类

我们开始修改,如同在 C_Mouse 类中那样。 那么直接来到区别,您可以在下面的代码中看到。

#include "C_Terminal.mqh"
//+------------------------------------------------------------------+
class C_Orders : private C_Terminal
{
        private :
//+------------------------------------------------------------------+
                MqlTradeRequest m_TradeRequest;
                ulong           m_MagicNumber;
                struct st00
                {
                        int     nDigits;
                        double  VolMinimal,
                                VolStep,
                                PointPerTick,
                                ValuePerPoint,
                                AdjustToTrade;
                        bool    PlotLast;
                        ulong   MagicNumber;
                }m_Infos;
//+------------------------------------------------------------------+

整个原理与 C_Mouse 类几乎相同,但仍存在一些差异。 首先,我们删除 C_Orders 类结构,即高亮所示的行。 但是我们需要结构内部的某些数据,因此我们将它设为私密,但只是一个常规变量

由于我们删除了高亮显示的部分,因此您也许会认为编写新代码需要花费大量精力。 实际上,这将是相当少的工作。 我们立即转到 C_Orders 类的构造函数。 修改实际上将从这里开始。 下面是新的类构造函数。

                C_Orders(const ulong magic)
                        :C_Terminal(), m_MagicNumber(magic)
                        {
                                m_Infos.MagicNumber     = magic;
                                m_Infos.nDigits         = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
                                m_Infos.VolMinimal      = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
                                m_Infos.VolStep         = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
                                m_Infos.PointPerTick    = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
                                m_Infos.ValuePerPoint   = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
                                m_Infos.AdjustToTrade   = m_Infos.ValuePerPoint / m_Infos.PointPerTick;
                                m_Infos.PlotLast        = (SymbolInfoInteger(_Symbol, SYMBOL_CHART_MODE) == SYMBOL_CHART_MODE_LAST);
                        };

如您所见,构造函数的所有内部内容都已被删除,但此处我们强制调用 C_Terminal 类构造函数。这样做是为了确保在任何其它内容之前调用它。 通常,编译器会为我们执行此操作,但我们会显式实现它,同时初始化一个变量,在代码中另一个位置表示魔幻数字。 

通常这是在构造函数中完成的,因为我们希望在执行任何代码之前就定义变量值 — 这将令编译器能够生成足够的代码。 但如果数值是常量,就像往常一样,我们这样做可以节省一些 C_Orders 类初始化的时间。 不过,请记住以下细节:只有当数值是常量时,您才会得到一些益处,否则编译器将生成一段不会给我们带来任何实际好处的代码。

接下来要做的是从 C_Orders 类中删除 AdjustPriceFinanceToPoints 函数,但由于可以直接完成此操作,故我就不在此处显示它了。 从现在开始,这些调用将使用 C_Terminal 类中的代码。

我们来看一下代码部分之一,即将用到的 C_Terminal 类中声明的变量。 这是为了理解如何访问父类的变量。 请看下面的代码:

inline void CommonData(const ENUM_ORDER_TYPE type, const double Price, const double FinanceStop, const double FinanceTake, const uint Leverage, const bool IsDayTrade)
                        {
                                double Desloc;
                                
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.magic            = m_Infos.MagicNumber;
                                m_TradeRequest.magic            = m_MagicNumber;
                                m_TradeRequest.symbol           = _Symbol;
                                m_TradeRequest.volume           = NormalizeDouble(m_Infos.VolMinimal + (m_Infos.VolStep * (Leverage - 1)), m_Infos.nDigits);
                                m_TradeRequest.volume           = NormalizeDouble(GetTerminalInfos().VolMinimal + (GetTerminalInfos().VolStep * (Leverage - 1)), GetTerminalInfos().nDigits);
                                m_TradeRequest.price            = NormalizeDouble(Price, m_Infos.nDigits);
                                m_TradeRequest.price            = NormalizeDouble(Price, GetTerminalInfos().nDigits);
                                Desloc = FinanceToPoints(FinanceStop, Leverage);
                                m_TradeRequest.sl               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? -1 : 1)), m_Infos.nDigits);
                                m_TradeRequest.sl               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? -1 : 1)), GetTerminalInfos().nDigits);
                                Desloc = FinanceToPoints(FinanceTake, Leverage);
                                m_TradeRequest.tp               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? 1 : -1)), m_Infos.nDigits);
                                m_TradeRequest.tp               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? 1 : -1)), GetTerminalInfos().nDigits);
                                m_TradeRequest.type_time        = (IsDayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC);
                                m_TradeRequest.stoplimit        = 0;
                                m_TradeRequest.expiration       = 0;
                                m_TradeRequest.type_filling     = ORDER_FILLING_RETURN;
                                m_TradeRequest.deviation        = 1000;
                                m_TradeRequest.comment          = "Order Generated by Experts Advisor.";
                        }

高亮显示的部分已被删除,并已由其它高亮显示的代码所替换。 现在,请注意以黄色高亮显示的代码。它包含许多人可能从未见过的东西。 请注意,这些高亮显示的黄色代码中有一个函数,它被视作一个结构。但这是多么疯狂的事情! 😵😱

冷静下来,我亲爱的读者。 不用担心。 这并不疯狂。 它只是以一种比您以前见过的更奇特的方式来进行编程。 若要明白为什么允许这样做,以及为什么它有效,我们来单独查看该函数:

GetTerminalInfos().nDigits

现在,我希望您返回到 C_Terminal 类代码,看看这个函数是如何声明的。 它具体如下:

inline const stTerminal GetTerminalInfos(void) const
                        {
                                return m_TerminalInfo;
                        }

请注意,GetTerminalInfos 函数返回的结构如以下代码所示:

                struct stTerminal
                {
                        ENUM_SYMBOL_CHART_MODE ChartMode;
                        int     nDigits;
                        double  VolMinimal,
                                VolStep,
                                PointPerTick,
                                ValuePerPoint,
                                AdjustToTrade;
                };

对于编译器来说,我们由 GetTerminalInfos().nDigits 代码所得等价于说 GetTerminalInfos() 不是一个函数,而是一个变量 😲。 你糊涂了吗? 好吧,事情变得更加有趣,因为对于编译器来说,GetTerminalInfos().nDigits代码将等效于以下代码:

stTerminal info;
int value = info.nDigits;

value = 10;
info.nDigits = value;


也就是说,您不仅可以读取值,还可以写入值。 因此,如果您不小心编写了以下片段:

GetTerminalInfos().nDigits = 10;

编译器将理解为,数值 10 必须放在函数 GetTerminalInfos() 引用的变量当中。这将是一个问题,因为引用的变量位于 C_Terminal 类中,并且该变量是在代码的私密部分中声明。 这意味着它不能被先前进行的调用修改。 但由于 GetTerminalInfos() 函数也是受保护(尽管它也可以是公开的,但结果是相同的),因此声明为私密的变量与引用它的函数具有相同的访问级别。

您有没有意识到这些事情有多危险? 也就是说,即使您将变量声明为私密,但错误地为引用它的函数或过程编写了代码,那么您或其他人也可能会无意中更改其值。 而它打破了封装的整个概念。

但由于在函数声明期间,它是以关键字 const 启动的,因此这会更改所有内容,因为现在编译器将看到不同的 GetTerminalInfo() 函数。 若要理解这一点,只需尝试在 C_Orders 类中的任何位置使用以下代码:

GetTerminalInfos().nDigits = 10;

如果尝试执行此操作,编译器将引发错误。 因为编译器将 GetTerminalInfos().nDigits,或结构内 GetTerminalInfos() 引用的任何其它事物视为常量,且常量值无法更改。 这被视为错误。

现在您明白如何使用变量引用常量数据了吗? 如此,对于 C_Terminal 类,GetTerminalInfos() 函数引用的结构是一个变量,但对于任何其它代码,它是一个常量 😁。

现在我已经解释了这部分,我们继续进行其它修改。 我想现在您可以明白正在发生的事情,以及由 C_Orders 引用的数据来自哪里。 下一个要修改的函数如下面所见:

                ulong CreateOrder(const ENUM_ORDER_TYPE type, const double Price, const double FinanceStop, const double FinanceTake, const uint Leverage, const bool IsDayTrade)
                        {
                                double  bid, ask;
                                
                                bid = SymbolInfoDouble(_Symbol, (m_Infos.PlotLast ? SYMBOL_LAST : SYMBOL_BID));
                                bid = SymbolInfoDouble(_Symbol, (GetTerminalInfos().ChartMode == SYMBOL_CHART_MODE_LAST ? SYMBOL_LAST : SYMBOL_BID));
                                ask = (m_Infos.PlotLast ? bid : SymbolInfoDouble(_Symbol, SYMBOL_ASK));
                                ask = (GetTerminalInfos().ChartMode == SYMBOL_CHART_MODE_LAST ? bid : SymbolInfoDouble(_Symbol, SYMBOL_ASK));
                                CommonData(type, AdjustPrice(Price), FinanceStop, FinanceTake, Leverage, IsDayTrade);
                                m_TradeRequest.action   = TRADE_ACTION_PENDING;
                                m_TradeRequest.type     = (type == ORDER_TYPE_BUY ? (ask >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : 
                                                                                    (bid < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP));                              
                                
                                return (((type == ORDER_TYPE_BUY) || (type == ORDER_TYPE_SELL)) ? ToServer() : 0);
                        };

最后要修改的是:

                bool ModifyPricePoints(const ulong ticket, const double Price, const double PriceStop, const double PriceTake)
                        {
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.symbol   = _Symbol;
                                if (OrderSelect(ticket))
                                {
                                        m_TradeRequest.action   = (Price > 0 ? TRADE_ACTION_MODIFY : TRADE_ACTION_REMOVE);
                                        m_TradeRequest.order    = ticket;
                                        if (Price > 0)
                                        {
                                                m_TradeRequest.price      = NormalizeDouble(AdjustPrice(Price), m_Infos.nDigits);
                                                m_TradeRequest.sl         = NormalizeDouble(AdjustPrice(PriceStop), m_Infos.nDigits);
                                                m_TradeRequest.tp         = NormalizeDouble(AdjustPrice(PriceTake), m_Infos.nDigits);
                                                m_TradeRequest.price      = NormalizeDouble(AdjustPrice(Price), GetTerminalInfos().nDigits);
                                                m_TradeRequest.sl         = NormalizeDouble(AdjustPrice(PriceStop), GetTerminalInfos().nDigits);
                                                m_TradeRequest.tp         = NormalizeDouble(AdjustPrice(PriceTake), GetTerminalInfos().nDigits);
                                                m_TradeRequest.type_time  = (ENUM_ORDER_TYPE_TIME)OrderGetInteger(ORDER_TYPE_TIME) ;
                                                m_TradeRequest.expiration = 0;
                                        }
                                }else if (PositionSelectByTicket(ticket))
                                {
                                        m_TradeRequest.action   = TRADE_ACTION_SLTP;
                                        m_TradeRequest.position = ticket;
                                        m_TradeRequest.tp       = NormalizeDouble(AdjustPrice(PriceTake), m_Infos.nDigits);
                                        m_TradeRequest.sl       = NormalizeDouble(AdjustPrice(PriceStop), m_Infos.nDigits);
                                        m_TradeRequest.tp       = NormalizeDouble(AdjustPrice(PriceTake), GetTerminalInfos().nDigits);
                                        m_TradeRequest.sl       = NormalizeDouble(AdjustPrice(PriceStop), GetTerminalInfos().nDigits);
                                }else return false;
                                ToServer();
                                
                                return (_LastError == ERR_SUCCESS);
                        };

现在我们将完成这一部分。 代码拥有相同的稳定级别性级别,同时我们改进了其健壮性。 由于以前我们有重复的函数,并且可能会冒着它们在一个类中被修改,而在另一个类中保持不变的风险。 如果发生了一些错误,纠正它并不简单,因为我们可能在一个类中修复了它,但它仍然会保留在另一个类中,从而令代码变得不那么健壮和可靠。

始终要考虑这一点:在稳定性和健壮性方面改进代码的一点付出不是工作,它只是一种爱好 😁。

但我们尚未完成本文中要做的事情。 我们想添加限价线(止盈和止损价位),以便在手工下订单时了解这些价位。 这部分仍然缺失。 没有它,我们就无法完成本文,并继续下一篇文章。 不过,我们将在此处创建一个新章节,从而将此主题与我们已经涵盖的内容分开。


创建止盈和止损线

现在,我们来思考以下问题:把创建这些示意线得代码添加在哪个位置最佳? 我们有一个调用点。 它如下所示:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        uint            BtnStatus;
        double  Price;
        static double mem = 0;
        
        (*mouse).DispatchMessage(id, lparam, dparam, sparam);
        (*mouse).GetStatus(Price, BtnStatus);
        if (TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL))
        {
                if (TerminalInfoInteger(TERMINAL_KEYSTATE_UP))  (*manager).ToMarket(ORDER_TYPE_BUY, user03, user02, user01, user04);
                if (TerminalInfoInteger(TERMINAL_KEYSTATE_DOWN))(*manager).ToMarket(ORDER_TYPE_SELL, user03, user02, user01, user04);
        }
        if (def_SHIFT_Press(BtnStatus) != def_CTRL_Press(BtnStatus))
        {
// This point ...
                if (def_BtnLeftClick(BtnStatus) && (mem == 0)) (*manager).CreateOrder(def_SHIFT_Press(BtnStatus) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL, mem = Price, user03, user02, user01, user04);
        }else mem = 0;
}

标记为黄色的部分是我们应该添加调用来显示止盈和止损价位的位置。 但有一个重要的细节:我们应该在哪里为这些示意线编写代码?

最好的替代方案(我想每个人都会同意它)是将此代码添加到 C_Mouse 类之中。 如此当我们移开鼠标时,示意线也会消失。 这就是我们要做的。 我们来打开 C_Mouse 类,创建代表止盈和止损的示意线。

但我会做一些与我们以前看到的略有不同的事情。 我不会把这些行添加到 OnChartEvent 当中,而是添加到 C_Mouse 类中的事件处理程序当中。 这种方式更好,尽管您不得不针对 EA 代码进行一番修改,但我们将其留待以后。 我们打开 C_Mouse.mqh 头文件,并实现我们所需的一切。

我们要做的第一件事是添加一些新的定义,如下所示:

#define def_PrefixNameObject    "MOUSE_"
#define def_MouseLineName       def_PrefixNameObject + "H"
#define def_MouseLineTake       def_PrefixNameObject + "T"
#define def_MouseLineStop       def_PrefixNameObject + "S"
#define def_MouseName           "MOUSE_H"

请注意,旧定义已被删除。 我们以稍微不同的方式做事,尽管编程仍然令人愉快。 为了减少编程工作量,我们以不同的方式修改创建过程:

                void CreateLineH(void)
                void CreateLineH(const string szName, const color cor)
                        {
                                ObjectCreate(m_Infos.Id, def_MouseName, OBJ_HLINE, 0, 0, 0);
                                ObjectSetString(m_Infos.Id, def_MouseName, OBJPROP_TOOLTIP, "\n");
                                ObjectSetInteger(m_Infos.Id, def_MouseName, OBJPROP_BACK, false);
                                ObjectSetInteger(m_Infos.Id, def_MouseName, OBJPROP_COLOR, m_Infos.Cor);
                                ObjectCreate(m_Infos.Id, szName, OBJ_HLINE, 0, 0, 0);
                                ObjectSetString(m_Infos.Id, szName, OBJPROP_TOOLTIP, "\n");
                                ObjectSetInteger(m_Infos.Id, szName, OBJPROP_BACK, false);
                                ObjectSetInteger(m_Infos.Id, szName, OBJPROP_COLOR, cor);
                        }

现在所有示意线都将以独特的方式创建,我们只需要指定名称和颜色。 我不得不再创建 2 个变量来存储颜色,但我认为无需在此展示了。 那好,我们继续讨论构造函数,因为现在它将需要接收比以前更多的数据,您可以在下面看到它:

                C_Mouse(const color corPrice, const color corTake, const color corStop, const double FinanceStop, const double FinanceTake, const uint Leverage)
                        {
                                m_Infos.Id        = ChartID();
                                m_Infos.CorPrice  = corPrice;
                                m_Infos.CorTake   = corTake;
                                m_Infos.CorStop   = corStop;
                                m_Infos.PointsTake= FinanceToPoints(FinanceTake, Leverage);
                                m_Infos.PointsStop= FinanceToPoints(FinanceStop, Leverage);
                                ChartSetInteger(m_Infos.Id, CHART_EVENT_MOUSE_MOVE, true);
                                ChartSetInteger(m_Infos.Id, CHART_EVENT_OBJECT_DELETE, true);
                                CreateLineH(def_MouseLineName, m_Infos.CorPrice);
                        }

正如我已提过的,我不得不创建更多的变量,但与我们可以获得的功能相比,它的代价很小。 注意以下几点:我不会等待 EA 调用,将财务值转换为点数。 这将节省我们以后的时间,因为访问变量比调用函数快得多。 但是析构函数呢? 事实上,这并不难。 我们需要做的就是修改负责删除对象的函数类型。 参见如下:

                ~C_Mouse()
                        {
                                ChartSetInteger(m_Infos.Id, CHART_EVENT_OBJECT_DELETE, false);
                                ObjectsDeleteAll(m_Infos.Id, def_PrefixNameObject);
                                ObjectDelete(m_Infos.Id, def_MouseName);
                        }

此函数可以把名称以某种特定方式开头的所有对象删除。在许多情况下,它非常实用,且用途广泛。 此处是需要修改的最后一个过程。 我们来看看如何在下面的代码中实现限价示意线:

                void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
                        {
                                int w;
                                datetime dt;
                                static bool bView = false;
                                
                                switch (id)
                                        {
                                                case CHARTEVENT_OBJECT_DELETE:
                                                        if (sparam == def_MouseName) CreateLineH();
                                                        if (sparam == def_MouseLineName) CreateLineH(def_MouseLineName, m_Infos.CorPrice);
                                                        break;
                                                case CHARTEVENT_MOUSE_MOVE:
                                                        ChartXYToTimePrice(m_Infos.Id, (int)lparam, (int)dparam, w, dt, m_Infos.Price);
                                                        ObjectMove(m_Infos.Id, def_MouseName, 0, 0, m_Infos.Price = AdjustPrice(m_Infos.Price));
                                                        ObjectMove(m_Infos.Id, def_MouseLineName, 0, 0, m_Infos.Price = AdjustPrice(m_Infos.Price));
                                                        m_Infos.BtnStatus = (uint)sparam;
                                                        if (def_CTRL_Press(m_Infos.BtnStatus) != def_SHIFT_Press(m_Infos.BtnStatus))
                                                        {
								if (!bView)
								{
									if (m_Infos.PointsTake > 0) CreateLineH(def_MouseLineTake, m_Infos.CorTake);
									if (m_Infos.PointsStop > 0) CreateLineH(def_MouseLineStop, m_Infos.CorStop);
									bView = true;
								}
								if (m_Infos.PointsTake > 0) ObjectMove(m_Infos.Id, def_MouseLineTake, 0, 0, m_Infos.Price + (m_Infos.PointsTake * (def_SHIFT_Press(m_Infos.BtnStatus) ? 1 : -1)));
								if (m_Infos.PointsStop > 0) ObjectMove(m_Infos.Id, def_MouseLineStop, 0, 0, m_Infos.Price + (m_Infos.PointsStop * (def_SHIFT_Press(m_Infos.BtnStatus) ? -1 : 1)));
                                                        }else if (bView)
                                                        {
                                                                ObjectsDeleteAll(m_Infos.Id, def_PrefixNameObject);
                                                                bView = false;
                                                        }
                                                        ChartRedraw();
                                                        break;
                                        }
                        }

首先,我必须删除两行旧代码,然后添加两行更新代码。 但是当我们要处理鼠标移动事件时,重要的细节就来了。 在此,我们添加了一些新行。 我们要做的第一件事是检查是否按下了 SHIFT 或 CTRL 键,但不是同时按下,若如此,则转到下一步。

现在,如果结果为 False,则检查图表上是否显示限价线若是的话,则删除所有鼠标示意线。 这不是问题,因为 MetaTrader 5 会立即生成一个事件来通知对象已从屏幕中删除。 当调用屏幕事件处理程序时,系统将引导您将价格线放回到图表上

但让我们回到按住 SHIFT 或 CTRL 键时将显示限价线的那一刻。 在这种情况下,我们检查屏幕上是否已经存在任何示意线。 然后,若没有,则在值大于零时创建它们,因为我们不需要图表上的奇怪元素我们将其标记为已完成,以免在每次调用时都尝试重新创建这些对象。然后,我们将根据价格线的所在将它们定位到对应位置


结束语

我们已经创建了 EA 系统来手工操作。 我们已为迈出下一步做好准备,我们将在下一篇文章中继续研究。 我们将添加一个触发器,如此系统便可以自动执行某些操作。 一旦这些完成后,我将向您展示如何将这种手工交易 EA 转换为全自动 EA。 下一篇文章我们将讨论 EA 如何自动化操作,并从交易中排除人为决策。


本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/11237

附加的文件 |
构建自动运行的 EA(第 06 部分):账户类型(I) 构建自动运行的 EA(第 06 部分):账户类型(I)
今天,我们将看到如何创建一个在自动模式下简单安全地工作的智能系统。 当前状态下,我们的 EA 已能在任何状况下工作,但尚未准备好自动化。 我们仍然需要在几点上努力。
构建自动运行的 EA(第 04 部分):手工触发器(I) 构建自动运行的 EA(第 04 部分):手工触发器(I)
今天,我们将看到如何创建一个在自动模式下简单安全地工作的智能系统。
神经网络变得轻松(第三十三部分):分布式 Q-学习中的分位数回归 神经网络变得轻松(第三十三部分):分布式 Q-学习中的分位数回归
我们继续研究分布式 Q-学习。 今天我们将从另一个角度来看待这种方式。 我们将研究使用分位数回归来解决价格预测任务的可能性。
构建自动运行的 EA(第 03 部分):新函数 构建自动运行的 EA(第 03 部分):新函数
今天,我们将看到如何创建一个在自动模式下简单安全地工作的智能系统。 在上一篇文章中,我们已启动开发一个在自动化 EA 中使用的订单系统。 然而,我们只创建了一个必要的函数。