
构建自动运行的 EA(第 08 部分):OnTradeTransaction
概述
在之前的文章中:构建自动运行的 EA(第 06 部分):账户类型(I),和构建自动运行的 EA(第 07 部分):账户类型(II),我们专注于设计自动交易 EA 时要小心的重要性。
在我们真正理解自动化的 EA 代码应该如何工作之前,我们需要明白它如何与交易服务器交互。 请参看图例 01:
图例 01. 消息流
图例 01 示意允许 EA 向交易服务器发送订单或请求的消息流。 请注意箭头的方向。
箭头是双向的唯一时刻是 C_Orders 类调用 OrderSend 函数向服务器发送订单,因为此时它依据结构接收来自服务器的响应。 除了此刻之外,所有其它点都是定向的。 但在此,我只展示了提交市价单或在订单簿里下单的过程。 因此,我们有一个非常简单的系统。
在 100% 自动化 EA 的情况下,我们仍然需要一些东西。 对于自动化程度最低的 EA,我们仍然需要添加一些细节。 一切都发生在 EA 和 C_Manager 类之间。 我们不会在 EA 的任何其它部分添加代码。 对了,还有另一件事。 在 100% 的自动化 EA 中,我们将不得不删除 C_Mouse 类(对于 100% 自动化的 EA,它没有用)。 非常重要的是明白什么是消息流,因为没有它,我们就无法继续进一步讨论主题。
加入控制和辅助功能
最大的问题是许多 MQL5 语言用户不会用某些功能来创建 EA,尽管这种语言提供了这些功能。 也许它源自不熟悉或其它原因,但这并不重要。 如果您打算利用 MQL5 提供的所有功能,那么为了提高代码的健壮性和可靠性,您确实需要考虑利用该语言为您提供的一些资源。
我们要做的第一件事是在 C_Manager 类中添加三个新函数。 它们在发布的 EA 里作为提供服务的类,或知道 EA 正在计划做什么。 这些函数中的第一个如下所示:
inline void EraseTicketPendig(const ulong ticket) { m_TicketPending = (ticket == m_TicketPending ? 0 : m_TicketPending); }
当通知的单号等于挂单的单号时,此函数将删除挂单的单号值。 正常情况下这实际上不会发生。 挂单不会被 EA 删除。 删除通常是由交易者或 EA 用户的干预而发生的,这并不可取。 然而,如果 EA 注意到它在订单簿中放置的挂单已被用户删除,那么它必须通知 C_Manager 类,以便在必要时 允许 EA 在订单簿中放置新的挂单。
下一个新函数如下所示:
void PedingToPosition(void) { ResetLastError(); if ((m_bAccountHedging) && (m_Position.Ticket > 0)) SetUserError(ERR_Unknown); else m_Position.Ticket = (m_Position.Ticket == 0 ? m_TicketPending : m_Position.Ticket); m_TicketPending = 0; if (_LastError != ERR_SUCCESS) UpdatePosition(m_Position.Ticket); CheckToleranceLevel(); }
EA 用此代码通知 C_Manager 类,挂单刚刚变为持仓,或刚对持仓进行了一些修改。 请注意,在此函数执行时,C_Manager 类将删除挂单单号,并允许 EA 放置新的挂单。 不过,仅当未发生严重错误时,这种情况才会继续,我们将通过在上一篇文章中讨论的函数进行分析。 但这个函数实际上不能单独工作,它需要另外一个配合,如下所示:
void UpdatePosition(const ulong ticket) { int ret; if ((ticket == 0) || (ticket != m_Position.Ticket)) return; if (PositionSelectByTicket(m_Position.Ticket)) { ret = SetInfoPositions(); m_StaticLeverage += (ret > 0 ? ret : 0); }else ZeroMemory(m_Position); ResetLastError(); }
C_Manager 类中还缺少另外两个函数。 但由于这些是自动函数,我们现在尚未详细涵盖它们。
现在,以更彻底的方式,我们终于有了彼此友好的 C_Manager 类和 EA。 两者都可以工作,并确保它们不会变得激进、或不友好。 因此,EA 和 C_Manager 类之间的消息流如图例 02 所示:
图例 02. 新函数的消息流
此消息流可能看起来太复杂,或完全不起作用,但这确实是到目前为止已经实现的。
查看图例 02,您可能会认为 EA 代码非常复杂。 但它比许多人认为 EA 所需的必要代码要简单得多。 尤其是遇到一款自动化 EA 时。 请记住以下几点:EA 实际上不会生成任何交易。 它只是与交易服务器通信的一种手段或工具。 故此,它实际上只是针对所应用的触发器做出反应。
基于这样的理解,我们于 EA 代码变得自动化之前,先通览一遍当前状态的 EA 代码。 但对于那些尚未见过它的人来说,EA 代码自上一篇文章(即构建自动运行的 EA(第 05 部分):手动触发器(II))发表以来没有发生重大变化。
int OnInit() { manager = new C_Manager(def_MAGIC_NUMBER, user03, user02, user01, user04, user08); mouse = new C_Mouse(user05, user06, user07, user03, user02, user01); (*manager).CheckToleranceLevel(); return INIT_SUCCEEDED; }
实际上,我们只需要添加一个新行。 这样就启动检查是否发生了更严重或更危急的错误。 但问题出现了:如何让 EA 通知C_Manager 类有关订单系统的相关情况? 许多人不知道在这种情况下该怎么做,试图找出如何得知交易服务器上正在做什么。 但其中往往潜伏着危险。
第一件您应该真正了解的事就是,MetaTrader 5 平台和 MQL5 语言不仅仅是普通的工具。 您实际上不必创建一个持续搜索信息的程序。 这是因为系统基于事件而非进程。 在基于事件的编程中,您不必步步分心,您需要的是以不同的方式思考。
为了理解这一点,请考虑以下几点:如果您正在开车,基本上您的本意是到达某个目的地。 但一路上,您将不得不解决一些发生的事情,且这些事显然相互并无关联。 但所有这些事都会影响您的方向,例如,制动、加速、由于发生了不可预见的事情而不得不改变路径。 您知道这些事件可能发生,但您不知道它们什么时候会发生。
这就是基于事件编程的全部意义所在:您可以访问某些事件,这些事件由特定语言针对特定作业而产生。 您所要做的就是创建一些逻辑来解决给定事件引发的问题,从而得到某种有用的结果。
MQL5 针对每种情况类型提供了一些我们可以(以及我们必须)处理的事件。 很多人在尝试理解这背后的逻辑时会迷失方向,但其实这并不复杂。 一旦您理解了这一点,编程就会变得简单得多。 因为语言本身为您提供了处理任何问题的必要手段。
这是第一点:主要利用 MQL5 语言来解决问题。 如果这还不够,则添加特定功能。 您可以利用其它语言,如 C/C++,甚至 Python,但首先尝试利用 MQL5。
第二点:您一定不要试图捕获信息,无论它来自哪里。 只要有可能,您应该简单地使用和响应 MetaTrader 5 平台生成的事件。
第三点:不要针对于您没有真正用处的事件调用函数或尝试编写代码。 选取您确实需要的内容,并尝试始终为正确的作业选取正确的事件。
基于这 3 点,我们有 3 个选项可供选择,如此令 EA 与 C_Manager 类,或任何其它需要接收 MetaTrader 5 平台提供数据的类进行交互。 第一个选项是针对收到的每个新单号使用事件触发器。 此事件将调用 OnTick 函数。 然而,诚心地,我不建议使用此函数。 我们将再次查看原因。
第二个选项是利用定时器触发的 OnTime 函数事件。 不过,此选项不适合我们现在正在做的事情。 这是因为我们必须在每次定时器触发事件时不断检查订单或持仓清单。 这根本没啥效果,且令 EA 成为 MetaTrader 5 平台的累赘。
最后一个选项是利用交易事件来触发 OnTrade 函数。 每当订单系统发生变化时,即有新订单或持仓发生变化时,它就会被激活。 但是 OnTrade 函数在某些情况下不是很合适,而在某些情况下,它可令我们免于执行某些任务,从而令事情变得更加简单。 替代调用 OnTrade,我们选用 OnTradeTransaction。
什么是 OnTradeTransaction,它的用途是什么?
它或许是 MQL5 最复杂的事件处理函数,故此,请参阅本文作为学习它的优秀资源。 我尝试解释并提供尽可能多的信息,即我对运用此函数的理解和学习。
为了令事情更容易解释,至少在这个初始阶段,我们先看一下 EA 中的函数代码:
void OnTradeTransaction(const MqlTradeTransaction &trans, const MqlTradeRequest &request, const MqlTradeResult &result) { switch (trans.type) { case TRADE_TRANSACTION_POSITION: manager.UpdatePosition(trans.position); break; case TRADE_TRANSACTION_ORDER_DELETE: if (trans.order == trans.position) (*manager).PendingToPosition(); else (*manager).UpdatePosition(trans.position); break; case TRADE_TRANSACTION_REQUEST: if ((request.symbol == _Symbol) && (result.retcode == TRADE_RETCODE_DONE) && (request.magic == def_MAGIC_NUMBER)) switch (request.action) { case TRADE_ACTION_DEAL: (*manager).UpdatePosition(request.order); break; case TRADE_ACTION_SLTP: (*manager).UpdatePosition(trans.position); break; case TRADE_ACTION_REMOVE: (*manager).EraseTicketPending(request.order); break; } break; } }
我知道这段代码对于大多人来说似乎很奇怪,尤其是有些人习惯于借助其它方法来查找他们的订单和持仓发生了什么。 但我保证,如果您真的了解 OnTradeTransaction 函数是如何工作的,那么您就会开始在所有 EA 中使用它,因为它确实有很大帮助。
然而,为了解释它是如何工作的,我们将尽可能避免谈论日志文件数据,因为如果您尝试追踪日志文件中找到的文件或范式来查看逻辑,您可能会发疯。 这是因为有时数据不会有任何范式。 原因是此函数是一个事件处理程序。 这些事件来自交易服务器,所以忘记日志文件。 我们专注于处理从交易服务器发送的事件,无论它们出现的顺序如何。
基本上,我们将在这里研究三种结构及其内容。 这些结构由交易服务器填充。 您必须了解,此处执行的任何操作都是处理来自服务器提供的内容。 于此,我们在 EA 中检查的交易常量,都是我们实际上需要的。 您可能需要更多常量,具体取决于要创建的内容。 若要发掘它们是什么,请查看文档交易业务类型。 您将看到 11 个不同的枚举,每个枚举都对应特定的东西。
请注意,在某些时候,我用到了一个引用 MqlTradeTransaction 结构的变量。 这个结构非常复杂,但它本就是相对于服务器看到和理解的内容而言。 不过对我们来说,这取决于我们真正想要检查、分析和知道什么样的事情。我们感兴趣的是这种结构的“类型”字段,因为它启动进一步的系统。 在此代码中,我们处理服务器执行的三种事务类型:TRADE_TRANSACTION_REQUEST、TRADE_TRANSACTION_ORDER_DELETE 和 TRADE_TRANSACTION_POSITION。 这就是在此使用它们的原因。
由于没有示例就很难解释事务类型,因此我们先看一下只有一行的 TRADE_TRANSACTION_POSITION:
case TRADE_TRANSACTION_POSITION: manager.UpdatePosition(trans.position); break;
每当持仓发生某些事情时,就会触发此事件 — 并非任何持仓,而仅是经过某种修改的持仓。 服务器会通知有关它的情报,如果该笔持仓由 EA 观察,我们将其传递给 C_Manager 类,以便对其进行更新。 否则,它将被忽略。 当试图找出实际更改的持仓时,这为我们节省了大量时间。
列表中的下一个是 TRADE_TRANSACTION_ORDER_DELETE。
case TRADE_TRANSACTION_ORDER_DELETE: if (trans.order == trans.position) (*manager).PendingToPosition(); else (*manager).UpdatePosition(trans.position); break;
当订单转换为仓位时,则触发服务器报告订单已删除的事件。 平仓时也会发生同样的事情件,并触发相同的事件。 报告订单转换为持仓的事件,与平仓之间的区别在于服务器提供的标志值。
当订单转换为持仓时,我们会得到持仓单号中指定的值,因此我们会通知 C_Manager 订单变成了持仓。 平仓时,这些值会有所不同。 但也可能发生您的一笔持仓加仓的情况,如此持仓量会发生变化。 在这种情况下,trans.order 和 trans.position 当中的数值会有所不同。 在这种情况下,我们会向 C_Manager 提出更新请求。
在某些情况下,此事件可能会伴随 TRADE_TRANSACTION_POSITION。 但情况并非总是如此。 为了令解释更容易,我们需要分离信息,因为理解此代码非常重要。
我们首先处理 trans.order 等于 trans.position 的情况 — 它们可以是相同的。 所以不要指望它们总是不同的。 当它们相等时,服务器启动 TRADE_TRANSACTION_ORDER_DELETE 枚举,但该枚举不会独自发生,而是伴随着其它枚举。 我们不必处理所有这些,而只需要处理这个特定的问题。 服务器将通知我们订单刚刚变成持仓。 此时,订单将被平单,并取已平单相同的单号开仓。
但也许服务器不会向我们发送 TRADE_TRANSACTION_POSITION 枚举。尽管一开始您可能正在等待此枚举,但服务器根本不会触发它。 但可以肯定的是,它会触发删除。 指示的值将相等。 在这种情况下,我们知道这是订单簿中的订单,并且它变成了持仓,但在市价单的情况下,一切都有所不同。 我们稍后会看到这种情况。
现在,如果 trans.order 与 trans.position 不同,服务器也将触发其它枚举。 但同样,不要指望某个具体的来到。 也许服务器没有触发它,但触发了我正在使用的那个。 在这种情况下,它表明该持仓刚刚由某种原因而被平仓,我们在这里不做分析。 在任何情况下,我们都会收到由 TradeTransaction 事件结构携带的有关信息。 这就是为什么这个事件处理程序如此有趣:您不必出去寻找信息。 事件就在那里,您只需要转到正确的结构,并读取信息即可。 是否清楚以这种方式执行检查?
通常,在未用此事件处理程序的程序中,程序员会创建一个循环来遍历所有持仓或挂单,试图找出哪个被执行或关闭。 这纯粹是浪费时间,因为这会令 EA 忙于完全无用的事情,而这些事情本可以很容易地捕获。 这是因为交易服务器已经为我们完成了所有繁重的工作,通知我们哪笔挂单被关闭,哪笔持仓被开立,或者哪笔持仓被平仓。 而我们却正在这里创建循环来查找此信息。
现在我们来到需要最长时间来解释的部分,但我不会涵盖所有情况。 原因与前一种情况相同:在没有示例的情况下解释所有情况并不轻松。 然而,这里解释的内容将帮助许多人。 为了更方便起见,我们先看看现在将要研究的片段。 它具体如下:
case TRADE_TRANSACTION_REQUEST: if ((request.symbol == _Symbol) && (result.retcode == TRADE_RETCODE_DONE) && (request.magic == def_MAGIC_NUMBER)) switch (request.action) { case TRADE_ACTION_DEAL: (*manager).UpdatePosition(request.order); break; case TRADE_ACTION_SLTP: (*manager).UpdatePosition(trans.position); break; case TRADE_ACTION_REMOVE: (*manager).EraseTicketPending(request.order); break; } break;
这个 TRADE_TRANSACTION_REQUEST 枚举几乎在所有情况下都会被触发。 如果没有,那才奇怪了。 故此,我们在测试方面可以做的很多事情都可以在其中完成。 但由于这是服务器大量触发的枚举,我们需要过滤其中的内容。
通常,服务器在 EA 或平台发出某些请求后触发此枚举。 这是当用户执行与订单系统相关的操作时。 但不要每次都指望这个,因为有时服务器只是触发此枚举。 有时并没有明显的原因,因此我们必须过滤其通告的内容。
首先,我们筛选资产。 您可以为此使用任何结构,但我更喜欢这个。 接下来,我们检查服务器是否接受了请求,为此我们要用到这个测试。 最后,我们检查魔幻数字,以便进一步过滤内容。 现在来到最令人困惑的部分。 因为您不知道如何填写代码的其余部分。
当我们使用 switch 检查操作类型时,我们不会(以后也不会)分析在服务器上执行的操作。 这不是我们实际要做的。 事实上,我们将对 EA 或平台发送到服务器的内容进行精确的检查。 有 6 种类型的动作,都是经典的。 它们位于 ENUM_TRADE_REQUEST_ACTIONS 枚举里。为了简化任务,我们来看如下表格。 它与文档中的相同。 我用它来简化解释,但我的讲述与文档略有不同。
动作类型 | 动作说明 |
---|---|
TRADE_ACTION_DEAL | 下达交易订单,以市场价格执行 |
TRADE_ACTION_PENDING | 在订单簿中下订单,根据指定的参数执行 |
TRADE_ACTION_SLTP | 修改持仓的止损和止盈数值 |
TRADE_ACTION_MODIFY | 更改订单簿中的挂单参数 |
TRADE_ACTION_REMOVE | 删除仍在订单簿中的挂单 |
TRADE_ACTION_CLOSE_BY | 平仓 |
表 01
如果您虽真的追随我们自本系列文章开始以来一直在编程的内容,但没有给予应有的关注,那么在编程时,您必须检查 Action 类型字段在我们的代码中使用的位置。 更不用说 OnTradeTransaction 事件处理程序了,因为它还未算入其中。 这些枚举均已已被使用。 在哪里呢? 在 C_Orders 类当中。
打开源代码,并注意 C_Orders 类中的以下过程:CreateOrder,ToMarket,ModifyPricePoints 和 ClosePosition。 我们将研究除 ClosePosition 之外的每一个,其中我没用到 TRADE_ACTION_CLOSE_BY 枚举。
为什么它在 OnTradeTransaction 事件处理程序中如此重要? 原因在于这些枚举与我们在分析 TRADE_TRANSACTION_REQUEST 枚举引用的操作类型时将看到的枚举相同。 这就是为什么在 OnTradeTransaction 事件处理程序代码中,我们看到 TRADE_ACTION_DEAL 和 TRADE_ACTION_SLTP,以及 TRADE_ACTION_REMOVE — 在 EA 里理应关注它们。
但是其余的呢? 对于创建自动 EA 的目的,其它类型并不重要,因为它们对应其它事情。 如果您想了解如何应用其它类型,请查看文章从头开始开发交易 EA(第 25 部分):提供系统健壮性(II),其中我展示了如何使用其它枚举。
既然我已经解释了这些案例的来源,我们来分解每个案例的作用,从下面看到的开始:
case TRADE_ACTION_DEAL: (*manager).UpdatePosition(request.order); break;
枚举器在执行市价单时调用。 但不光在这种情况下。 还有第二种情况也调用它。 当我谈到 TRADE_TRANSACTION_ORDER_DELETE 时,我提到有一种情况是 trans.order 和 trans.postion 可以相等。 这是服务器触发 TRADE_ACTION_DEAL 的第二种情况。 故此,我们现在可以加入单号作为持仓。 但请注意,如果发生某些事情,例如,另一笔持仓仍未平仓,则会发生错误,导致 EA 终止。 它没有在此展示,但可在 UpdatePosition 代码中看到。
请参阅下面的一段代码:
case TRADE_ACTION_SLTP: (*manager).UpdatePosition(trans.position); break;
此枚举器将在更改限价值(称为止盈和止损)时被触发。 它只是简单地更新止损和止盈值。 这个版本非常简单,有很多方法可以让它更有趣一点,但现在这就足够了。 我们将在代码中使用的最后一个枚举就在下面:
case TRADE_ACTION_REMOVE: (*manager).EraseTicketPending(request.order); break;
此段代码在订单被删除时触发,我们需要通知 C_Manager 类,以便 EA 可以发送挂单。 正常时,挂单不会从订单簿中删除,但也许用户已经这样做了。 如果订单被意外或故意从订单簿中删除,并且 EA 没有通知 C_Manager 类,这将阻止 EA 发送另一笔挂单。
结束语
在本文中,我们探讨了如何利用事件处理系统,更快更好地处理与订单系统相关的问题。 配合这个系统,EA 就能更快地工作,如此它就不必持续不断地搜索所需的数据。 诚然,我们仍在应对的 EA,尚无任何自动化等级,但很快我们将添加自动化,即使是基本的,即令 EA 管理盈亏平衡和尾随停止。 附件提供了我们在上三篇文章中所述代码的完整版本。 我建议您仔细研究此代码,从而理解所有实际工作原理,因为它将在我们的后续工作步骤中为您提供很多帮助。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/11248


Hi, Daniel Jose,
Thank you for dedicating such a wonderful article. I have a problem that confuses me. I wrote a code to open a position.
It can return the correct value when it is run on demo account, but it returns 0 when it is run on a real account, can you help me to advise where is the problem?
Olá, Daniel José,
Obrigado por dedicar um artigo tão maravilhoso. Estou com um problema que me confunde. Eu escrevi um código para abrir uma posição.
Ele pode retornar o valor correto quando é executado em conta demo, mas retorna 0 quando é executado em uma conta real, você pode me ajudar a informar onde está o problema?
Tente verificar o erro que está sendo retornado, adicionando a linha destacada logo abaixo.
Tente verificar o erro que está sendo retornado, adicionando a linha destacada logo abaixo.
No, my purpose is to return a deal ticket
it can return deal ticket of the position on Demo account , but return zero value on Real account, this is for what ?
Não, meu objetivo é devolver um bilhete de acordo
ele pode retornar ticket de negociação da posição na conta Demo, mas retornar valor zero na conta Real, isso é para quê?
A linha que mandei você adicionar NÃO É ... vou repetir: NÃO É para retornar nenhum valor ... ELA SERVE PARA VERIFICAR O ERRO que está dando ao tentar abrir uma posição ... VOCÊ NÃO DEVE USAR O VALOR RETORNADO .. você deve verificar o ERRO na tabela de erros ... Veja a DOCUMENTAÇÃO
Hi, Daniel Jose,
Thank you for dedicating such a wonderful article. I have a problem that confuses me. I wrote a code to open a position.
It can return the correct value when it is run on demo account, but it returns 0 when it is run on a real account, can you help me to advise where is the problem?
this function has open position and return DEAL TICKET :