市场模拟(第四部分):创建 C_Orders 类(一)
概述
在上一篇文章,市场模拟(第三部分):性能问题中,为了解决我们遇到的一些问题(至少是暂时的),我们对类进行了一些调整。这些问题正在降低系统的整体性能。虽然我们暂时解决了这些问题,但目前正面临着一个真正棘手的难题。这不是第一部分,因为我已经在前面的文章中谈到了这个话题。然而,我们现在处理事情的方式略有不同。因此,我们处理这个问题的方式也会有所不同。
我知道你们很多人都迫不及待地想看到回放/模拟器最终执行订单。但是,在此之前,我们需要确保订单系统完全正常运行。这对于与真实交易服务器通信至关重要,无论是通过模拟账户还是真实账户。总之,所涉及的应用程序 —— 即 Chart Trade 指标、鼠标指标和 EA 交易系统 —— 必须完美协调地工作,以确保与真实交易服务器的顺畅通信。
除了上述应用程序之外,我们还需要创建其他组件。然而,这些问题现在可以暂时搁置,因为它们的发展(无论是在概念阶段还是在实施过程中)都取决于几个因素,这些因素将在本发展阶段得到解决。
在本文中,我将开始解释我们将如何与交易服务器通信。你们中的许多人可能已经知道如何很好地做到这一点。在这种情况下,我要求大家有点耐心,因为我们会慎重行事,而不是匆忙行事。至关重要的是,你要充分了解实际发生的事情。与许多典型的编程方法不同,该系统将构建在模块中,每个模块负责一个非常具体的任务。如果一个模块发生故障,整个系统也会发生故障,因为没有备份系统以其他方式确保功能。
理解概念
如果你一直在关注这个系列文章,你可能已经注意到在开发回放系统(第 78 部分):新 Chart Trade(五)中,在那里我开始展示交互是如何发生的,EA 交易实际上并不知道订单来自哪里。然而,同样的 EA 交易系统却知道如何解读接收到的消息。尽管当时使用的方法不允许跨订单系统,但我们在后续文章中解决了这个问题。在市场模拟(第二部分):跨期订单(二)中,我演示了消息传递系统将如何构建以与 EA 交易系统进行通信。
整个机制只是更大系统的一部分。因此,理解这种消息传递机制对于理解我们将在本文中开始探索的内容至关重要。不要低估或忽视前几篇文章中解释的内容。最重要的是,在看到它的实际应用之前,不要以为你完全理解它。
如果您已经掌握了前面介绍的 EA 交易系统代码,那么您应该很容易理解这里要编程的内容。不过,不要仅仅通过查看代码来假设知识。您应该了解每个细节的功能,以便充分了解系统作为一个整体是如何工作的。
发起市价单
由于有许多概念需要解释,我不会在下面的代码中立即显示整个类。同样,我也不会在本文中添加大量代码。本部分内容需要仔细理解。因为我们将看到的这段代码实际上是用来处理资金的。您自己的钱,亲爱的读者。了解每个部分的工作原理将使您对所提供的代码感到自信和安全。我不希望你仅仅因为不理解代码就修改它。我希望只有当您需要添加当前不存在的功能时,才进行任何修改,而不是因为您习惯使用特定的编码风格。仔细研究代码,因为它也将在我们实现订单模拟时使用。
为了尽可能简化解释,让我们首先来考察负责向服务器发送订单的初始类 —— 无论是我们现在正在处理的真实服务器,还是我们稍后将看到的模拟服务器。在任何情况下,代码开始如下所示。第一段代码片段如下。
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #include "..\Defines.mqh" 005. //+------------------------------------------------------------------+ 006. class C_Orders 007. { 008. protected: 009. //+------------------------------------------------------------------+ 010. inline const ulong GetMagicNumber(void) const {return m_MagicNumber;} 011. //+------------------------------------------------------------------+ 012. private : 013. //+------------------------------------------------------------------+ 014. MqlTradeRequest m_TradeRequest; 015. ulong m_MagicNumber; 016. bool m_bTrash; 017. //+------------------------------------------------------------------+ 018. struct stChartTrade 019. { 020. struct stEvent 021. { 022. EnumEvents ev; 023. string szSymbol, 024. szContract; 025. bool IsDayTrade; 026. ushort Leverange; 027. double PointsTake, 028. PointsStop; 029. }Data; 030. //--- 031. bool Decode(const EnumEvents ev, const string sparam) 032. { 033. string Res[]; 034. 035. if (StringSplit(sparam, '?', Res) != 7) return false; 036. stEvent loc = {(EnumEvents) StringToInteger(Res[0]), Res[1], Res[2], (bool)(Res[3] == "D"), (ushort) StringToInteger(Res[4]), StringToDouble(Res[5]), StringToDouble(Res[6])}; 037. if ((ev == loc.ev) && (loc.szSymbol == _Symbol)) Data = loc; 038. 039. return true; 040. } 041. //--- 042. }m_ChartTrade; 043. //+------------------------------------------------------------------+ 044. ulong SendToPhysicalServer(void) 045. { 046. MqlTradeCheckResult TradeCheck; 047. MqlTradeResult TradeResult; 048. 049. ZeroMemory(TradeCheck); 050. ZeroMemory(TradeResult); 051. if (!OrderCheck(m_TradeRequest, TradeCheck)) 052. { 053. PrintFormat("Order System - Check Error: %d", GetLastError()); 054. return 0; 055. } 056. m_bTrash = OrderSend(m_TradeRequest, TradeResult); 057. if (TradeResult.retcode != TRADE_RETCODE_DONE) 058. { 059. PrintFormat("Order System - Send Error: %d", TradeResult.retcode); 060. return 0; 061. }; 062. 063. return TradeResult.order; 064. } 065. //+------------------------------------------------------------------+ 066. ulong ToMarket(const ENUM_ORDER_TYPE type) 067. { 068. double price = SymbolInfoDouble(m_ChartTrade.Data.szContract, (type == ORDER_TYPE_BUY ? SYMBOL_ASK : SYMBOL_BID)); 069. double vol = SymbolInfoDouble(m_ChartTrade.Data.szContract, SYMBOL_VOLUME_STEP); 070. uchar nDigit = (uchar)SymbolInfoInteger(m_ChartTrade.Data.szContract, SYMBOL_DIGITS); 071. 072. ZeroMemory(m_TradeRequest); 073. m_TradeRequest.magic = m_MagicNumber; 074. m_TradeRequest.symbol = m_ChartTrade.Data.szContract; 075. m_TradeRequest.price = NormalizeDouble(price, nDigit); 076. m_TradeRequest.action = TRADE_ACTION_DEAL; 077. m_TradeRequest.sl = NormalizeDouble(m_ChartTrade.Data.PointsStop == 0 ? 0 : price + (m_ChartTrade.Data.PointsStop * (type == ORDER_TYPE_BUY ? -1 : 1)), nDigit); 078. m_TradeRequest.tp = NormalizeDouble(m_ChartTrade.Data.PointsTake == 0 ? 0 : price + (m_ChartTrade.Data.PointsTake * (type == ORDER_TYPE_BUY ? 1 : -1)), nDigit); 079. m_TradeRequest.volume = NormalizeDouble(vol + (vol * (m_ChartTrade.Data.Leverange - 1)), nDigit); 080. m_TradeRequest.type = type; 081. m_TradeRequest.type_time = (m_ChartTrade.Data.IsDayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC); 082. m_TradeRequest.stoplimit = 0; 083. m_TradeRequest.expiration = 0; 084. m_TradeRequest.type_filling = ORDER_FILLING_RETURN; 085. m_TradeRequest.deviation = 1000; 086. m_TradeRequest.comment = "Order Generated by Experts Advisor."; 087. 088. MqlTradeRequest TradeRequest[1]; 089. 090. TradeRequest[0] = m_TradeRequest; 091. ArrayPrint(TradeRequest); 092. 093. return (((type == ORDER_TYPE_BUY) || (type == ORDER_TYPE_SELL)) ? SendToPhysicalServer() : 0); 094. }; 095. //+------------------------------------------------------------------+ 096. public : 097. //+------------------------------------------------------------------+ 098. C_Orders(const ulong magic) 099. :m_MagicNumber(magic) 100. { 101. } 102. //+------------------------------------------------------------------+ 103. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 104. { 105. switch (id) 106. { 107. case CHARTEVENT_CUSTOM + evChartTradeBuy : 108. case CHARTEVENT_CUSTOM + evChartTradeSell : 109. case CHARTEVENT_CUSTOM + evChartTradeCloseAll: 110. if (m_ChartTrade.Decode((EnumEvents)(id - CHARTEVENT_CUSTOM), sparam)) switch (m_ChartTrade.Data.ev) 111. { 112. case evChartTradeBuy: 113. ToMarket(ORDER_TYPE_BUY); 114. break; 115. case evChartTradeSell: 116. ToMarket(ORDER_TYPE_SELL); 117. break; 118. case evChartTradeCloseAll: 119. break; 120. } 121. break; 122. } 123. } 124. //+------------------------------------------------------------------+ 125. }; 126. //+------------------------------------------------------------------+
C_Replay.mqh 文件的源代码
很好,让我们了解一下这段代码的实际功能。虽然它看起来很复杂,但它非常简单,只执行一个功能:它在用户与 Chart Trade 指标交互时发送市场买入或卖出订单。但它是如何做到这一点的?你可能认为我们会以某种方式修改 Chart Trade 指标。如果你是这么想的,那么你还没有完全理解这个系统是如何运作的。指标无法向交易服务器发送指令。它们是用于在图表上显示信息的。以 Chart Trade 指标为例,它使用户能够与系统的其他部分进行交互。
在介绍实际会使用此头文件的 EA 交易系统之前,让我们先回顾一下已经解释过的内容。这样可以更容易地理解头文件。
在开发回放系统(第 78 部分):新 Chart Trade(五)中,我们解释了 EA 交易系统应该拦截哪些消息。您可能已经注意到,我们使用 OnChartEvent 过程来捕获这些消息。在这个过程中,我们调用了一个类。该类翻译了接收到的消息,并将其打印到终端进行分析。这是容易的部分,因为我们不需要使用这些数据与交易服务器通信。
查看 EA 交易系统的源代码时,你会发现 OnChartEvent 过程中调用了 DispatchMessage。这个调用实际上会调用头文件。具体来说是第 103 行。但在此之前,让我们从头开始 —— 第 98 行的类构造函数。
构造函数非常简单。被调用时,它会接收一个参数。该参数将用于通过幻数来识别类。请注意这个细节:我们识别的不是 EA 交易本身,而是它的类。为什么要区分这一点?有时,使用类似但独立的类来执行相同类型的任务是有用的。目前,这似乎没有必要,但随着我们的继续,这一点会变得清晰起来。这个特殊的类不使用某些元素,稍后将对此进行解释。
例如,当没有订单只剩下持仓时,您可能需要以特定方式处理它们。使用多个 EA 交易系统容易出错。此外,每个图表只能运行一个 EA 交易系统。(我并不是说你不能对同一品种使用多个 EA 交易系统,而是说同一张图表上不能有多个 EA 交易。)
为同一交易品种打开多个图表,仅仅为了运行不同的 EA 交易系统,管理起来很困难。有些人能理解,但我却觉得非常困惑。然而,将略有不同的类放在同一个 EA 交易系统中,使它们能够和谐地工作,是完全可以实现的。为了方便起见,构造函数在创建类时会为其分配一个幻数。这个数之后会用于订单和仓位处理。我们稍后会看到这一点。
幻数在第 99 行初始化,存储该幻数的变量在第 15 行声明。请注意,第 12 行包含一个 private 子句,它将类中从第 12 行到第 96 行的所有内容封装起来,包括在第 14 行到第 16 行之间声明的变量。
在继续之前,请注意第 10 行。它包含一个函数,该函数返回为此类定义的幻数。由于这一行位于第 8 行(受保护的子句)和第 12 行(私有子句)之间,因此该函数不能在继承系统之外使用。换句话说,尝试在继承自 C_Orders 的类之外访问它会导致编译器错误。
此函数目前仅用于将来用途,其目的仅在于返回幻数。我们暂时不会把重点放在这件事上。目前只需知道第 10 行的这个函数不能在继承系统之外访问,它仅用于返回该类的幻数即可。
为了使其余代码更容易理解,我们将把它分成几个主题。但是,每个主题都将引用相同的代码。
结构中的过程?
在开发回放系统(第 78 部分):新 Chart Trade(五)中,我使用一个类来实现翻译系统。这里,我使用了一个结构。我们可以这样做吗?是的,因为类本质上是更复杂的结构。由于我们需要的东西可以更简单地建模,我选择使用结构。这可以在第 18 行和第 42 行之间看到。
注意:第 18 行声明了结构体。如果这是一个类,那么从第 18 行到第 42 行的所有内容都将是私有的。这需要通过一项 public 语句来调整访问权限。这没问题。但为什么要在一个类中声明一个类呢?这通常只会让代码更加混乱。
我们需要的数据来自消息,而消息可以看作是一组变量。为什么不把消息看作是一个大型变量集呢?这有助于理解。是这样吗?
消息结构位于第 20 行和第 29 行之间。虽然整体数据结构跨越了第 18 行至第 42 行,但消息本身被认为是第 20 行至第 29 行之间的子集。如前文所述,消息是一个字符串。如果我们仅仅根据结构长度来解析字符串,很容易误解其含义。因此,我们需要一些额外的代码来解码这条消息。
第 31 到 40 行包含主结构中的一个函数,用于处理此问题。但它不属于消息结构的一部分 —— 它将逻辑分离出来,从而降低了出错的风险。为什么呢?因为各个元素是独立存在的。我们可以把所有内容整合在一起:消息结构、函数和过程。但这样做,我们在实施过程中可能会遇到问题。
请大家集中注意力,因为这有点令人困惑。如果你能理解这一点,你就会明白为什么很多程序员即使可以使用结构体也会避免使用。当一些程序员尝试使用结构体时,他们最终会将自己的程序变成定时炸弹,尤其是当代码是用传统的 C 语言编写时。
请仔细阅读第 36 行。这一行代表着一种真正的危险,那就是所有东西都可能混在一起。这一行代码将我们需要的所有值填充到数据结构中。如果这是一个函数或过程,那么执行第 35 行时会发生什么?更糟糕的是,如果您调用内存中在第 36 行被覆盖的函数或过程会发生什么?覆盖数据可能会导致严重的后果,这也是传统 C 强调使用类的重要性的一个关键原因。但如果你了解事情的来龙去脉,你就会明白为什么会创建这些类。
我们不会详细说明可能发生的事情。但别忘了,一个真正邪恶的程序员能做到你想象不到的事情。不过,我们还是回到代码上来吧。第 31 行的函数执行与之前相同的任务。但请看第 110 行,函数被调用的地方。请注意,我们不会直接用函数名调用函数,而是需要一个额外的引用。这是一个对包含表示我们结构数据的变量的引用。虽然这听起来可能令人困惑,但其实并非如此。暂时忘记我们正在使用一个结构,把它看作是调用一个类方法。逻辑是一样的。
有些人可能会疑惑为什么 DECODE 不是 C_Orders 的一部分。如果真是这样,就没必要调用第 110 行了。但是 DECODE 是独立存在的,用于解码包含我们数据的消息。因此,在 C_Orders 类中声明它是不合理的。始终从逻辑的角度而不是便利的角度考虑将元素分离。如果 DECODE 函数是在 C_Orders 类中声明的,那么将来如果我们想使用与名称相似的过程,但目标数据结构不同,我们就必须创建一个完全不同的名称来避免冲突。更糟糕的是,随着时间的推移,当我们更改代码时,总是会变得难以理解各个部分的含义。因此,从逻辑上分离职责可以防止将来出现命名冲突,并减少潜在的错误。
与服务器通信
对于新程序员来说,这部分内容常常令人困惑。如果你觉得这一部分令人困惑,这意味着你还没有完全理解你实际需要做的一切。与查看图表的交易者不同,程序员必须将编程过程视为正确填写表单。如果表单有错误,服务器(就像一位一丝不苟的雇主)会拒绝该表单。
服务器不会理解错误,它只会拒绝不合理的请求。但是,它会告诉我们拒绝的原因。完善的通信流程能够确保顺畅的互动。
第 44 到 64 行包含处理服务器通信的函数。尽管它很简单,但它包括所有必要的步骤。注意要遵循的步骤的逻辑顺序。我们不能再像以前那样做事了。必须遵循以下顺序:首先,我们清除回应内存。这是在第 49 到 50 行完成的。接下来,我们验证请求。也就是说,我们在提交之前会检查数据,此检查在第 51 行执行。如果出现错误,系统会返回相应的错误代码,以便我们修复错误并重试。如果一切正常,则请求将发送到服务器(第 56 行)。
服务器的响应存储在占位符变量中,以避免编译器警告。原因是我们并不关心函数本身的回应,我们真正关心的是回复结构的内容。第 57 行检查结果是否与 TRADE_RETCODE_DONE 不同。如果是这样,则发生了错误。相关错误代码会打印在终端上,这是在第 59 行完成的。
如果发生错误,第 44 行的函数将返回 0。否则,它将返回服务器的响应单号,其中标识了订单或仓位。我们稍后会在开始操作这些仓位/订单时讨论这个问题。目前,我们只发送市价单请求。
这样的请求是在另一个函数中完成的,该函数出现在第 66 行。该函数接受一个参数,指示买入或卖出。当 Chart Trade 消息指示进行市场操作时,它会在第 113 行和第 116 行被调用。但是服务器如何知道止损、止盈、杠杆或交易品种的详细信息呢?或者我们是否使用了跨期订单?好吧,这就引出了下一步。
填写请求表单
如果你查看第 44 行的服务器通信函数,你会注意到在第 51 行和第 56 行,我们使用了一个未在该函数中声明的结构。该结构在 C_Orders 的第 14 行声明。这是一个私有的、全局到类中的结构。也就是说,类的整个主体都可以访问这个变量,但类外部的任何代码都无法访问它。
因此,第 44 行函数中使用的这个变量必须正确赋值。该处理在第 66 到 94 行进行的。正确的结构填充可确保服务器执行所需的请求:无论是市场买入还是卖出。稍后会介绍挂单,以及如何修改止损和止盈水平。
要填充该结构,我们需要几条数据。部分数据来自 Chart Trade,部分来自 MetaTrader 5。无论如何,应该遵循一定的逻辑顺序。首先,获取当前价格(第 68 行)。注意这一行的每一个细节,因为每一个都很重要。
我们还需要知道交易量是多少。很多人在这个阶段都会遇到困难,因为服务器期望的交易量是另一个数字的倍数。交易图表上显示的成交量并非实际交易量,而是杠杆水平。这两个概念虽然不同,但又相互关联。杠杆率会乘以最小交易量。因此,不要混淆这些概念。服务器还需要提供工具的十进制位数。该信息可在第 70 行找到。
现在我们拥有了最重要的东西,我们可以开始填充结构了。首先,我们需要清理结构,这在第 72 行完成。接下来的每一行都具体说明了服务器应该执行什么操作。73 到 86 行所示的请求填写方式,其中每个字段都已正确填写,适用于股票、场外交易市场和外汇市场。需要注意的是,这些字段各自都有其含义。对任何字段的误解都可能导致经济损失。关于挂单的详细说明将在后续文章中提供。
最后,填充后的结构体被打印到终端上,我们可以在终端上进行检查。这在第 88 到 91 行中有所体现。虽然有多种方法可以实现这一点,但为了简单起见,我们调用 MQL5 标准库中的 ArrayPrint。然后该函数返回请求响应(第 93 行)。
最后的探讨
虽然本文仅涵盖了部分代码 —— 具体而言,是发送市价单 —— 但该头文件与 Chart Trade 指标一起,已经可以执行市价买卖。可能还有一些问题需要解答,特别是关于 MqlTradeRequest 结构的问题。别担心,挂单会解释清楚这一切。下一篇文章中,我们将继续探讨 EA 交易的源代码。
| 文件 | 描述 |
|---|---|
| Experts\Expert Advisor.mq5 | 演示 Chart Trade 和 EA 交易之间的交互(交互需要鼠标研究) |
| Indicators\Chart Trade.mq5 | 创建用于配置要发送的订单的窗口(需要 Mouse Study 才能进行交互) |
| Indicators\Market Replay.mq5 | 创建用于与回放/模拟器服务交互的控件(交互需要 Mouse Study) |
| Indicators\Mouse Study.mq5 | 实现图形控件和用户之间的交互(操作回放模拟器和实时市场交易所需) |
| Services\Market Replay.mq5 | 创建并维护市场回放和模拟服务(整个系统的主文件) |
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/12589
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
交易中的神经网络:配备注意力机制(MASAAT)的智代融汇(终章)
探索达瓦斯箱体突破策略中的高级机器学习技术
从新手到专家:支撑与阻力强度指标(SRSI)