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

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

MetaTrader 5交易 | 14 三月 2023, 13:50
2 188 0
Daniel Jose
Daniel Jose

概述

在上一篇文章“构建自动运行的 EA(第 03 部分):新函数”中,我们完成了对订单系统的全覆盖。 如果您还没有阅读它,或还未完全理解它的内容,我建议您回到那篇文章。 于此,我们将不再讨论订单系统。 我们将继续讨论其它事情,特别是触发器

触发器系统大概是最困难的部分。 它会产生混淆、怀疑和问题,因为没有 100% 无错的方法。 所有方法都有自己的缺点和问题。 其中一些错误的概率较高,而另一些则具有较低的概率。 但请始终记住以下规则:不要过于依赖触发器,无论它们是什么类型,因为它们可能会在您最意想不到的时候失败。 如果触发器系统出现故障,您可能会错过一个好机会、或遭遇巨亏,因此您应该始终非常对待触发器。

但我们来看一下这个系统的一些细节。 首先,系统的目的是什么? 触发系统有若干种用途,包括指示何时实现盈亏平衡、或何时触发尾随止损移动。 这些是最简单的例子。 触发器系统还可以指示 EA 何时应开仓、或何时应平仓。

所有这些事件都是完全自动发生的:交易者不需要做任何事情。 我们只是指示 EA 哪个触发器应该激活相应事件。 然后不需要人工参与,除非 EA 进入死循环,因为此刻交易者将不得不立即停用它。

为了更好地理解这些思路和概念,我们需要编写一个手动 EA。 但在此,我们需做一些与我们常用的手动 EA 完全不同的事情。 我们在本系列中用作示例的 EA 之中,我们将添加下挂单,或发送订单开立市价持仓的可能性。 由于 EA 是出于演示和教学目的,我建议所有考虑使用它的人都在模拟账户上这样做。 不要在真实账户上使用该 EA,因为它有卡住、或以疯狂的方式运行的风险。


如何发送订单并开立市价仓位

以我的观点,在图表上进行交易的最佳方式是将鼠标与键盘结合使用。 这对于下挂单很不错,而市价单只能使用键盘下单。 但唯一的问题是,如何实现才不会导致复杂到难以解释的代码,因为通过鼠标和键盘提交订单的系统可能非常难以开发。

考虑片刻之后,我决定在文章中采用一个理想系统,十分简单。 故此,不要在真实账户上使用此系统。 它只是为了帮助我解释一些有关触发器的事情,以便您可以了解触发器是如何实现的,以及它应该如何进行处理。

该代码是您可以构建的最简单代码,但对于本系列文章的其余部分来说,它仍然足够了。 那么,我们就来看看代码是如何实现的。

首先,我添加了一个名为 C_Mouse.mqh 的头文件。 文件中的代码按如下方式开始:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#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)
//+------------------------------------------------------------------+

为我们正在创建的图形对象设置名称 — 水平线,来指示图表上鼠标指针所处位置的价格。 我们还创建了三个定义,以便更轻松地访问鼠标事件将生成的某些信息。这是测试单击和按键事件的方式。 稍后我们将在 EA 中用到这些访问定义,而对象名称的定义将在文件末尾通过以下代码行完成:

//+------------------------------------------------------------------+
#undef def_MouseName
//+------------------------------------------------------------------+

将这一行添加到头文件的末尾,可确保此定义不会泄漏、或出现在代码中的其它任何位置。 如果您尝试在此头文件以外的其它位置使用此定义,且不重新声明它,编译器将返回相关警告。

现在我们开始编写负责鼠标操作的类代码。 它开始如下:

class C_Mouse
{
        private :
                struct st00
                {
                        long    Id;
                        color   Cor;
                        double  PointPerTick,
                                Price;
                        uint    BtnStatus;
                }m_Infos;
//+------------------------------------------------------------------+
                void CreateLineH(void)
                        {
                                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);
                        }
//+------------------------------------------------------------------+
inline double AdjustPrice(const double value)
                        {
                                return MathRound(value / m_Infos.PointPerTick) * m_Infos.PointPerTick;
                        }
//+------------------------------------------------------------------+

不要被这段代码吓到,这部分只是一个小例子。我们所做的只是声明了一个数据结构,它将包含类中的一些全局变量,但它们对于该类则是私密的。 此后,我们调用在 MetaTrader 5 平台中创建图表对象的过程。 这个对象是一条水平线,我们将设置它的一些属性并继续

至于这个函数,您在上一篇文章中已经见过了。 如果没见过,请先阅读本系列的前几篇文章,因为它们对于我们将要做的事情也很重要。

现在,我们来看一下将在 C_Mouse 类中公开的过程。 我们从类构造函数开始:

                C_Mouse(const color cor)
                        {
                                m_Infos.Id = ChartID();
                                m_Infos.Cor = cor;
                                m_Infos.PointPerTick = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
                                ChartSetInteger(m_Infos.Id, CHART_EVENT_MOUSE_MOVE, true);
                                ChartSetInteger(m_Infos.Id, CHART_EVENT_OBJECT_DELETE, true);
                                CreateLineH();
                        }

它有两行,如果您不是很熟悉 MQL5,它们对您来说可能看起来很奇怪。 那好,我就简要解释一下它们。 这一行告诉 MetaTrader 5 平台,我们的代码,想要 EA 接收鼠标事件。 鼠标还会生成另一种类型的事件,该事件在鼠标滚动时发生。 但我们不会用到它,因为这个事件类型如果足够了。

这一行通知 MetaTrader 5,我们想知道任何对象已从图表中删除。如果发生这种情况,这将生成一个事件,通知我们哪个元素已从图表中删除。 当我们想要保存某些关键元素,从而完美执行任务时,这种类型的事件可能很有用。 然后,如果用户意外删除了任何关键元素,MetaTrader 5 会通知代码(在本例中为 EA)该元素已被删除,以便我们可以重新创建它。 现在我们来看一下的类析构函数代码:

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

我们在此代码中做两件事。 首先,我们通知 MetaTrader 5,如果从图表中删除任何元素,我们将不再希望收到通知。在我们继续之前这样做很重要,就像我们尝试删除一个元素一样,MetaTrader 5 将生成一个事件,通知某些内容已从图表中删除。 这正是我们接下来要做的:删除我们创建的显示鼠标指针所在价格范围的行

接下来我们需要做的是创建一种读取类内数据的方法,以便我们知道鼠标位置,及其按钮的状态。 为此,我们将用到以下函数

const void GetStatus(double &Price, uint &BtnStatus) const
                        {
                                Price = m_Infos.Price;
                                BtnStatus = m_Infos.BtnStatus;
                        }

我想针对缺少面向对象编程经验的人解释一些很平常的事情。 使用类时,我们不得允许类外的任何代码直接访问类变量或过程,并且必须始终检查将进入类的内容。

允许访问类中的变量或过程,获取未经正确检查的数值是一个严重的错误,因为从长远来看,这会导致问题。 在某些时候,您能够在代码中修改类中的关键变量的数值,那么在使用它时,代码漏洞可能会面临风险。 此外,以后应对这种情况,并解决问题非常复杂。

所以,若要成为一名优秀的程序员,请始终遵循这种做法:如果您要读取类的内容,则允许它可读,而不是修改它;这种情况经常发生,尤其是在使用指针时。 您请求读取变量,并返回指针。 此时,您的代码处于危险之中,因为当您使用指针时,您知道在内存中写入的位置,这是非常危险的。 参见如何创建游戏作弊(在电子游戏中作弊的小程序)。 您简单地依据指针写入内存位置。

使用指针时要非常小心。 如有疑问,始终倾向于使用函数,而非过程。 两者之间的区别在于函数通常返回一个值,但仍然注意不要返回指针。

但如果您别无选择,则遵循我上面所示的相同操作。 这也许看起来过于小心,但当开始用保留字 const 声明过程或函数时,可以保证调用方永远不会更改变量中的值,因为编译器会将任何此类数值视为常量且无法更改。

使用保留字 const 完成过程或函数的声明,可保证您作为程序员不会意外、或不知不觉地更改过程或函数当中的任何值。 这可能是现有最好的编程实践,尽管对许多人来说可能看起来很奇怪。 它避免了一些编程错误,特别是当我们在 OOP(面向对象编程)中执行此操作时,但我们稍后会通过一个更清晰的例子回到这个问题,该示例将帮助您理解使用保留字 const 完成声明的问题。

我们所需的最后一个过程如下所示:

                void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
                        {
                                int w;
                                datetime dt;
                                
                                switch (id)
                                        {
                                                case CHARTEVENT_OBJECT_DELETE:
                                                        if (sparam == def_MouseName) CreateLineH();
                                                        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));
                                                        m_Infos.BtnStatus = (uint)sparam;
                                                        ChartRedraw();
                                                        break;
                                        }
                        }

这非常有趣,因为许多程序员都希望将代码放在 OnChartEvent 消息处理事件当中。 但我喜欢在对象类中处理事件。 这往往有助于不会忘记处理特定内容,或以特殊方式处理它,而这通常会令您检查代码是否正常工作。 通过在类中进行这种处理,从长远来看,我们更有信心。 因为当我们用到该类时,我们知道该类的消息处理系统正在按预期工作。 这大大加快了新程序的创建速度。 这也是重用的路径:一次编程,始终使用。

我们来看看这个消息传递系统中会发生什么。 在此,我们准确地复制了 OnChartEvent 事件处理程序将接收的数据,这是故意这样做的,因为我们希望在类中处理这些事件。

一旦定义完毕,我们来判定处理哪些事件。 首先是对象删除事件,之所以生成该事件,是因为我们之前告诉 MetaTrader 5,我们希望在从图表中删除任何对象时得到通知。 如果删除的对象是鼠标指示线,则将立即重新创建该对象

在类中,我们要处理的下一个事件是鼠标移动事件。 同样,生成此事件是由于我们已经告诉 MetaTrader 5,我们想知道鼠标发生了什么。 在此,我们简单地MetaTrader 5 提供的数值转换为时间和价格值。 之后,我们将水平线对象定位在价格位置,令其位于正确的点位。 请注意,点位它不是在屏幕坐标系中,而是在价格图表当中。 从那里我们保存按钮的值,并强制立即重绘图表

重要的是,此图表重绘在类事件处理程序,或 OnChartEvent 事件处理程序中进行。 如果不这样做,您可能会觉得代码令平台变得迟缓,因为价格线会奇怪地移动。

我们的 C_Mouse 类至此完成,现在我们可以直接在图表上初始挂单。 现在,我们回到我们的 EA 代码,并添加 C_Mouse 类,以便能够生成事件,并发送挂单。 此处就是它如何完成的:

#include <Generic Auto Trader\C_Orders.mqh>
#include <Generic Auto Trader\C_Mouse.mqh>
//+------------------------------------------------------------------+
C_Orders *manager;
C_Mouse *mouse;
//+------------------------------------------------------------------+
input int       user01   = 1;           //Lot increase
input double    user02   = 100;         //Take Profit ( FINANCEIRO )
input double    user03   = 75;          //Stop Loss ( FINANCEIRO )
input bool      user04   = true;        //Day Trade ?
input color     user05  = clrBlack;     //Color Mouse
//+------------------------------------------------------------------+
#define def_MAGIC_NUMBER 987654321
//+------------------------------------------------------------------+
int OnInit()
{
        manager = new C_Orders(def_MAGIC_NUMBER);
        mouse = new C_Mouse(user05);
        
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
        delete mouse;
        delete manager;
}
//+------------------------------------------------------------------+
void OnTick()
{
}
//+------------------------------------------------------------------+

上面的片段讲述了我们将如何生产东西的步骤。 一切都如这种方式完成,即可以轻松删除 C_Mouse 类。 由于对于自动化 EA 它没有用,但现在我们需要一种方式来测试 EA,而最好的方法是使用我们刚刚创建的 C_Mouse 类,而无需添加触发器。 故此,我们包含此类,如此编译器可以为我们添加代码我们定义水平价格线的颜色

我们初始化 C_Order 类,如此所有订单对于此 EA 都具有相同的魔幻数字。 请注意,您应该始终为每个 EA 设置不同的魔幻数字。 原因在于,如果您要在同一时间在同一资产上使用多个 EA,需要将它们分开,而每个 EA 管理自己的订单的方法就是依据这个魔幻数字。 如果两个 EA 拥有相同的魔幻数字,您就无法将它们区别开,其中一个可能会干扰另一个的订单。 因此,每个 EA 都必须有自己的唯一魔幻数字

之后,我们初始化 C_Mouse 类,如此 EA 准备好工作。 不要忘记在 EA 结束时调用析构函数。 不过,如果您自己忘记了,编译器通常会为您完成。 尽管如此,在代码中执行此操作是一种很好的习惯,这样每个人都知道在哪里停止引用类。

虽然我说 EA 已经准备好工作,但尚缺少一件事:图表事件处理系统。 它将具有以下形式:

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 (def_SHIFT_Press(BtnStatus) != def_CTRL_Press(BtnStatus))
        {
                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;
}

此处,非常简单。 我不想将我们正在构建的自动运行的 EA 再变回手动 EA。 出于这个原因,EA 不会创建定义止盈和止损的限价指示线。 但我们来看看这段代码是如何工作的。 这里仍然缺少一些东西。

当 MetaTrader 5 注意到图表上发生了一个事件时,无论它是什么,它都会调用上述代码。 一旦激活此段代码,并接收要运行的处理器后,它将调用来自 C_Mouse 类的消息处理过程。以这种方式,链接到鼠标的事件就能得到正确处理,且始终以相同的方式。

C_Mouse 类消息处理程序返回时,我们将捕获鼠标状态,以便能够在此函数中的此处使用它。

现在,我们使用 C_Mouse.mqh 头文件中的定义检查 ShiftCtrl 键的状态。 它们作为我们是在提交买入还是卖出订单的指示。 出于此原因,它们必须具有不同的值。 如果发生这种情况,我们将进行新的测试。

但是这次,我们检查是否单击了鼠标左键如果属实,且记忆价格为零,则取用户指定的数据向服务器发送挂单。 买入或卖出由 ShiftCtrl 键判定:Shift 表示买入,而 Ctrl 表示卖出。 因此,我们可以根据需要下订单来测试系统,但如上所述,这里仍然缺少一些东西。

在上一篇文章中,我为您提供了一个实际任务:尝试生成一段个代码,按市价进行交易。 如果您无法实现它,没事。 但如果您可以自己处理任务,而不用查看我的解决方案,那就更好了。 解决方案就在下面,在更新的 OnChartEvent 处理程序代码之中。

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))
        {
                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;
}

这是一种将市价单发送到交易服务器的方式。 我们已有以下行为:当按下 CTRL 时,这里的条件为 true,现在我们就可以检查第二个条件。 第二个条件指示我们是按市价买入,还是以市价卖出。 为此,我们将使用箭头键。 如果按下 CTRL + 向上箭头,我们将执行市价买入操作,参数则按照用户交互区域中的指定数值。 如果按下 CTRL + 向下箭头,则执行市价卖出操作,同样基于指定参数。

这些指导如图例 01 所示:

图例 1

图例 01. EA 设定。


结束语

我刚才解释的事件系统及其实现方式就是一种触发机制。 但在这种情况下,该机制是手动的,即它需要交易者的人工干预:开仓或挂单的订单则是通过 EA 用户与 EA 本身的交互来执行的。 在这种情况下,没有自动机制可以在订单簿中下订单或开仓,这完全取决于交易者。

但是一旦订单被放入订单簿中,系统将不再依赖于交易者,一旦价格达到订单中指定的点位,订单就会变为持仓,如果随后达到其中一个限价,该笔持仓将被平仓。 不过,您需要当心,因为可能会出现高波动的时刻,这可能导致价格跳跃。 在这种情况下,该笔持仓将保持状态,直到经纪商强制平仓,或者在日内交易的情况下,由于停市而导致的时间限制到期。 故此,您应该始终小心持仓。

附件文件包含当前形式的代码。 在下一篇文章中,我们将修改此代码,令其更容易在手动模式下使用 EA,因为我们需要进行一些变化。 然而,这些变化将会很有趣。


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

附加的文件 |
构建自动运行的 EA(第 05 部分):手工触发器(II) 构建自动运行的 EA(第 05 部分):手工触发器(II)
今天,我们将看到如何创建一个在自动模式下简单安全地工作的智能系统。 在上一篇文章的末尾,我建议允许手工操作 EA 是合适的,至少在一段时间内。
构建自动运行的 EA(第 03 部分):新函数 构建自动运行的 EA(第 03 部分):新函数
今天,我们将看到如何创建一个在自动模式下简单安全地工作的智能系统。 在上一篇文章中,我们已启动开发一个在自动化 EA 中使用的订单系统。 然而,我们只创建了一个必要的函数。
构建自动运行的 EA(第 06 部分):账户类型(I) 构建自动运行的 EA(第 06 部分):账户类型(I)
今天,我们将看到如何创建一个在自动模式下简单安全地工作的智能系统。 当前状态下,我们的 EA 已能在任何状况下工作,但尚未准备好自动化。 我们仍然需要在几点上努力。
构建自动运行的 EA(第 02 部分):开始编码 构建自动运行的 EA(第 02 部分):开始编码
今天,我们将看到如何创建一个在自动模式下简单安全地工作的智能系统。 在上一篇文章中,我们讨论了任何人在继续创建自动交易的智能系统之前需要了解的第一步。 我们首先研究了概念和结构。