
开发回放系统(第32部分):订单系统(一)
概述
在上一篇文章开发回放系统(第31部分):EA交易项目—C_Mouse类(五)中,我们已经开发了在回放/模拟系统中使用鼠标的基本部分。我们没有显示鼠标滚轮的问题,因为我最初没有看到使用此功能的必要性。现在我们可以开始处理另一部分,这无疑要困难得多。毫无疑问,我们必须在代码和其他相关内容中实现的是整个回放/建模系统中最困难的事情。没有这一部分,就不可能进行任何实用和简单的分析。我们谈论的是订单系统。
在我们迄今为止开发的所有东西中,正如你可能会注意到并最终同意的那样,这个系统是最复杂的。现在我们需要做一些非常简单的事情:让我们的系统模拟交易服务器的操作。准确实现交易服务器操作方式似乎是一件轻而易举的事情。至少说起来是这样。但我们需要这样做,以便对回放/模拟系统的用户来说,一切都是无缝和透明的。在使用该系统时,用户无法区分真实系统和模拟系统。但这并不是最难的部分。最困难的部分是允许EA在任何情况下都是相同的。
说EA必须是相同的意味着不要编译它以供测试人员使用,然后再编译它以用于实际市场。从编程的角度来看,这将更简单、更容易,但会给用户带来问题,需要不断重新编译EA。因此,为了确保最佳的用户体验,我们需要创建一个只需要一次编译的EA。一旦完成,它就可以在实际市场和测试器中使用。
在真实市场中使用EA的关键部分,即使是在演示帐户上,也是最容易实现的。所以,我们从那里开始。请注意,我们将从一个非常基本的模型开始。我们将逐步增加EA的复杂性,并增强其功能,最终创建可用于回放/模拟以及演示或实时帐户的EA。我们要做的第一件事是借用之前在社区中发布的其他文章中解释的大部分代码。这些文章属于我,所以我认为使用这些信息没有问题。我们将进行一些更改,使系统变得灵活、模块化、可靠和强健。否则,我们可能会在某个时候陷入死胡同,这将使该系统在各个方面无法进一步发展。
这仅仅是一个开始,因为任务非常复杂。首先,我们需要了解类继承系统目前是如何在EA中实现的。我们之前在本系列的其他文章中看到过这一点。当前的继承图如图01所示:
图01:当前EA类继承图
尽管这张图对迄今为止所做的工作非常有效,但它仍然远远不是我们真正需要的。这并不是因为很难将 C_Orders 类添加到此继承方案中。实际上,这可以通过从 C_Study 类派生 C_Orders 类来实现。但我不想这么做。原因在于一个非常实际的问题,而大多数使用面向对象编程的程序员有时会忽略这个问题。这个问题被称为封装,也就是说,只知道你需要做什么来完成你的工作。当我们创建类的层次结构时,我们不应该让一些类知道比他们真正需要知道的更多的信息。我们应该始终支持这样的编程,即每个类只知道执行任务真正需要知道什么。因此,在将 C_Orders 类添加到图01所示的图中的同时维护封装实际上是不可能的。因此,这个问题的最佳解决方案是从继承块中删除 C_Terminal 类,如图01所示,并将其作为参数或参数传递到同一块中,这可以用更合适的方式使用。因此,EA代码将对谁接收哪些信息进行控制,这将有助于维护信息封装。
因此,我们将在本文中使用的新类关系图如图02所示。
图02:新的继承图
在新的图表中,只有在EA代码允许的情况下,才能访问各个类。正如您可能已经猜到的,我们将不得不对现有代码进行一些小的更改,但这些变化不会对整个系统产生太大影响。我们可以快速探讨这些变化,看看有什么新的变化。
准备基础
要做的第一件事是在 C_Terminal 类中创建一个枚举。它在这里:
class C_Terminal { protected: enum eErrUser {ERR_Unknown, ERR_PointerInvalid}; // ... Internal code ... };
此枚举将允许我们使用_LastError变量在系统中由于某种原因发生错误时通知我们。现在我们只定义这两种类型的错误。
此时,我们将修改 C_Mouse 类。我不会详细介绍,因为这些变化不会影响类的运作。它们只是引导消息流,与使用继承系统时略有不同。更改如下所示:
#define def_AcessTerminal (*Terminal) #define def_InfoTerminal def_AcessTerminal.GetInfoTerminal() //+------------------------------------------------------------------+ class C_Mouse : public C_Terminal { protected: //+------------------------------------------------------------------+ // ... Internal fragment .... //+------------------------------------------------------------------+ private : //+------------------------------------------------------------------+ // ... Internal fragment ... C_Terminal *Terminal; //+------------------------------------------------------------------+
为了避免在任何时候都重复代码,我们添加了两个新的定义。这允许扩展的配置选项。此外,还添加了一个专用全局变量,以允许正确访问 C_Terminal 类。此外,从上面的代码中可以看出,我们将不再使用 C_Terminal 类继承。
由于我们不使用继承,还有两个修改需要讨论。第一个是在 C_Mouse 类的构造函数中:
C_Mouse(C_Terminal *arg, color corH, color corP, color corN) :C_Terminal() { Terminal = arg; if (CheckPointer(Terminal) == POINTER_INVALID) SetUserError(C_Terminal::ERR_PointerInvalid); if (_LastError != ERR_SUCCESS) return; ZeroMemory(m_Info); m_Info.corLineH = corH; m_Info.corTrendP = corP; m_Info.corTrendN = corN; m_Info.Study = eStudyNull; m_Mem.CrossHair = (bool)ChartGetInteger(def_InfoTerminal.ID, CHART_CROSSHAIR_TOOL); ChartSetInteger(def_InfoTerminal.ID, CHART_EVENT_MOUSE_MOVE, true); ChartSetInteger(def_InfoTerminal.ID, CHART_CROSSHAIR_TOOL, false); def_AcessTerminal.CreateObjectGraphics(def_NameObjectLineH, OBJ_HLINE, m_Info.corLineH); }
在这里,我们从 C_Mouse 类构造函数中移除对 C_Terminal 类构造函数的调用。现在我们需要获得一个新的参数来初始化指向类的指针。出于安全原因,由于我们不希望代码在不合适的情况下中断,我们将运行一个测试来验证允许我们使用 C_Terminal 类的指针是否已正确初始化。
为此,我们使用CheckPointer函数,但与构造函数一样,它不允许返回错误信息。我们将使用 C_Terminal 类中存在的枚举中的预定义值来指示错误条件。但是,由于我们无法直接更改_LastError变量的值,因此需要使用调用SetUserError。之后,我们可以检查_LastError以了解发生了什么。
但是,如果 C_Terminal 类没有正确初始化,我们需要小心:C_Mouse 类构造函数将返回而不执行任何操作,因为它将无法使用尚未初始化的 C_Terminal 类。
另一个修改与以下函数有关:
virtual void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) { int w = 0; static double memPrice = 0; C_Terminal::DispatchMessage(id, lparam, dparam, sparam); switch (id) { //....
指定的代码必须添加到EA中,以处理 MetaTrader 5平台报告的事件。正如您所看到的,如果不这样做,某些事件将出现问题,这可能导致元素在图表上的位置发生冲突。现在,我们将从这个位置删除代码。我们甚至可以允许 C_Mouse 类调用 C_Terminal 类消息传递系统。但由于我们没有使用继承,这将给代码留下一个相当不寻常的依赖关系。
就像我们在 C_Mouse 类中所做的那样,我们将在 C_Study 类中进行。请注意类构造函数,如下所示:
C_Study(C_Terminal *arg, color corH, color corP, color corN) :C_Mouse(arg, corH, corP, corN) { Terminal = arg; if (CheckPointer(Terminal) == POINTER_INVALID) SetUserError(C_Terminal::ERR_PointerInvalid); if (_LastError != ERR_SUCCESS) return; ZeroMemory(m_Info); m_Info.Status = eCloseMarket; m_Info.Rate.close = iClose(def_InfoTerminal.szSymbol, PERIOD_D1, ((def_InfoTerminal.szSymbol == def_SymbolReplay) || (macroGetDate(TimeCurrent()) != macroGetDate(iTime(def_InfoTerminal.szSymbol, PERIOD_D1, 0))) ? 0 : 1)); m_Info.corP = corP; m_Info.corN = corN; CreateObjectInfo(2, 110, def_ExpansionBtn1, clrPaleTurquoise); CreateObjectInfo(2, 53, def_ExpansionBtn2); CreateObjectInfo(58, 53, def_ExpansionBtn3); }
我们获取一个指向 C_Terminal 类指针的参数,并将其传递给 C_Mouse 类。由于我们继承了它,我们必须正确地初始化它,但无论哪种方式,我们都将执行与在 C_Mouse 类构造函数中相同的检查,以确保使用正确的指针。现在我们必须注意一件事:如果您注意到,在 C_Mouse 和 C_Study 中的构造函数中,我们都会检查 _LastError 的值,以了解是否有什么不符合预期。但是,根据所使用的资产,C_Terminal 类可能需要初始化其名称,以便EA知道当前图表上的资产。
如果偶然发生这种情况,_LastError 变量将包含值 4301(ERR_MARKET_UNKNOWN_SYMBOL),这表示未正确检测到资产。但这不是真的,因为 C_Terminal 类在其当前编程状态下可以访问所需的资产。为了避免由于此错误而从图表中删除EA,您需要对 C_Terminal 类的构造函数进行小的更改。它在这里:
C_Terminal() { m_Infos.ID = ChartID(); CurrentSymbol(); m_Mem.Show_Descr = ChartGetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR); m_Mem.Show_Date = ChartGetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE); ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, false); ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, true); ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, false); m_Infos.nDigits = (int) SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_DIGITS); m_Infos.Width = (int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS); m_Infos.Height = (int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS); m_Infos.PointPerTick = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_SIZE); m_Infos.ValuePerPoint = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_VALUE); m_Infos.VolumeMinimal = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_VOLUME_STEP); m_Infos.AdjustToTrade = m_Infos.PointPerTick / m_Infos.ValuePerPoint; ResetLastError(); }
通过添加此代码,我们将表明没有初始错误。因此,构造函数系统将用于初始化EA代码中的类。我们将不需要实际添加这一行代码,因为在某些情况下,我们可能会忘记进行此添加,或者更糟的是,在错误的时间进行添加,这将使代码完全不稳定,使用起来不安全。
C_Orders 类
到目前为止,我们所看到的将有助于我们为下一步行动奠定基础。我们仍然需要对 C_Terminal 类进行更多的更改。我们将在本文后面进行一些更改。让我们继续创建 C_Orders 类,该类将启用与交易服务器的交互。在这种情况下,它将是一个真正的服务器,由代理提供访问权限。但是你可以使用一个模拟帐户来测试系统。实际上,直接在真实帐户上使用该系统是不可取的。
此类的代码如下所示:
#property copyright "Daniel Jose" //+------------------------------------------------------------------+ #include "..\C_Terminal.mqh" //+------------------------------------------------------------------+ #define def_AcessTerminal (*Terminal) #define def_InfoTerminal def_AcessTerminal.GetInfoTerminal() //+------------------------------------------------------------------+ class C_Orders {
在这里,为了便于编码,我们将定义几个访问 C_Terminal 类的内容。现在,这些定义将不位于类文件的末尾,而是位于类代码内部。这将是访问 C_Terminal 类的方法。现在,如果我们在未来进行任何更改,我们将不必更改类代码,我们只需要更改此定义。请注意,该类没有继承任何内容。记住这一点很重要,这样在编程这个类和编写稍后出现的其他类时就不会感到困惑。
接下来,我们声明第一个全局类变量和内部类变量。它在这里:
private : //+------------------------------------------------------------------+ MqlTradeRequest m_TradeRequest; ulong m_MagicNumber; C_Terminal *Terminal;
请注意,这些全局变量被声明为私有的,即不能在类代码之外访问它们。请注意如何声明将提供对 C_Terminal 类访问权限的变量。它实际上被声明为指针,尽管MQL5中指针的使用与C/C++中的不同。
ulong ToServer(void) { MqlTradeCheckResult TradeCheck; MqlTradeResult TradeResult; bool bTmp; ResetLastError(); ZeroMemory(TradeCheck); ZeroMemory(TradeResult); bTmp = OrderCheck(m_TradeRequest, TradeCheck); if (_LastError == ERR_SUCCESS) bTmp = OrderSend(m_TradeRequest, TradeResult); if (_LastError != ERR_SUCCESS) PrintFormat("Order System - Error Number: %d", _LastError); return (_LastError == ERR_SUCCESS ? TradeResult.order : 0); }
上述函数将是私有的,用于“集中”调用。我决定集中呼叫,因为将来调整系统会更容易。这对于能够在真实服务器和模拟服务器上使用相同的图表是必要的。前面的函数以及我们在文章中讨论的其他函数已被删除:创建一个自动工作的EA(第15部分):自动化(VII)。这篇文章解释了如何从手动EA创建一个自动化EA。我们将使用本文中的一些函数来加快我们在这里的工作。这样,如果我们想使用相同的概念,我们可以使用重放/模拟系统测试自动EA,而无需使用 MetaTrader 5 策略测试器。
基本上,上面的函数将检查代理服务器上的一些内容。如果一切正常,它将向交易服务器发送一个请求,以填写用户或EA的请求(因为我们可以在自动模式下工作)。
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_MagicNumber; m_TradeRequest.symbol = def_InfoTerminal.szSymbol; m_TradeRequest.volume = NormalizeDouble(def_InfoTerminal.VolumeMinimal + (def_InfoTerminal.VolumeMinimal * (Leverage - 1)), def_InfoTerminal.nDigits); m_TradeRequest.price = NormalizeDouble(Price, def_InfoTerminal.nDigits); Desloc = def_AcessTerminal.FinanceToPoints(FinanceStop, Leverage); m_TradeRequest.sl = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? -1 : 1)), def_InfoTerminal.nDigits); Desloc = def_AcessTerminal.FinanceToPoints(FinanceTake, Leverage); m_TradeRequest.tp = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? 1 : -1)), def_InfoTerminal.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."; }
上述函数也是从同一系列文章中导入的。然而,在这里,我们不得不使其适应正在实施的新系统。工作原理与自动化系列基本相同。但对于那些没有读过该系列的人,让我们快速看看这个函数。首先,它有一个代码,可以将金融价值转换为点数。这样做是为了让我们作为用户不必担心为给定的杠杆设置点数。因此,我们不必比我们想要的更多地处理财务问题。手动执行此操作可能会导致错误和失败,但使用此函数,可以很容易地转换数值。无论资产是什么,该函数都能工作。无论您使用何种资产类型,转换都将始终正确有效地完成。
现在让我们来看 C_Terminal 类中的下一个函数。其代码如下所示:
inline double FinanceToPoints(const double Finance, const uint Leverage) { double volume = m_Infos.VolumeMinimal + (m_Infos.VolumeMinimal * (Leverage - 1)); return AdjustPrice(MathAbs(((Finance / volume) / m_Infos.AdjustToTrade))); };
该函数的主要秘密在于计算的值,如下面的片段所示:
m_Infos.PointPerTick = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_SIZE); m_Infos.ValuePerPoint = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_VALUE); m_Infos.VolumeMinimal = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_VOLUME_STEP); m_Infos.AdjustToTrade = m_Infos.ValuePerPoint / m_Infos.PointPerTick;
FinanceToPoints 中使用的所有上述值都取决于我们管理和用于交易的资产。因此,当 FinanceToPoints 进行转换时,它实际上会适应我们在图表上使用的资产。因此,EA 并不关心它是在什么资产和什么市场上推出的。同样,它可以与任何用户配合使用。既然我们已经看到了类的私有部分,让我们来看看公共部分。我们将从构造函数开始:
C_Orders(C_Terminal *arg, const ulong magic) :m_MagicNumber(magic) { if (CheckPointer(Terminal = arg) == POINTER_INVALID) SetUserError(C_Terminal::ERR_PointerInvalid); }
以一种简单有效的方式,我们将确保类构造函数可以访问 C_Terminal 类。请注意这是如何实际发生的:当EA为类创建要使用的 C_Terminal 对象时,它还会创建一个对象,该对象将传递给所有其他需要该对象的类。这种情况如下:类接收EA创建的指针,以便访问已经初始化的类。然后,我们将此值保存在私有全局变量中,以便在需要时访问 C_Terminal 类的任何数据或函数。事实上,如果这样一个对象,在本例中是一个类,没有指向有用的东西,就会被报告为错误。由于构造函数不能返回值,我们使用此方法为 _LastError 变量设置适当的值。这将使我们看到原因。
现在,让我们继续讨论在这个开发阶段出现在类中的最后两个函数。第一个如下所示:
ulong ToMarket(const ENUM_ORDER_TYPE type, const double FinanceStop, const double FinanceTake, const uint Leverage, const bool IsDayTrade) { CommonData(type, SymbolInfoDouble(def_InfoTerminal.szSymbol, (type == ORDER_TYPE_BUY ? SYMBOL_ASK : SYMBOL_BID)), FinanceStop, FinanceTake, Leverage, IsDayTrade); m_TradeRequest.action = TRADE_ACTION_DEAL; m_TradeRequest.type = type; return (((type == ORDER_TYPE_BUY) || (type == ORDER_TYPE_SELL)) ? ToServer() : 0); };
此函数负责发送以市场价格执行的请求。在这里,我们实际上使用了我们之前考虑过的整个代码。这是一个很好的重用案例。随着时间的推移,这种重用可以提高安全性和性能。改进重用系统的任何部分都可以改进整个代码。请注意以下有关上述代码的详细信息:
- 首先,我们将止损水平(获利和止损)表示为金融价值,而不是点。
- 其次,我们将告诉服务器立即执行订单,以执行订单时的最佳价格执行。
- 第三,尽管我们可以访问更多的订单类型,但在这里我们只能使用这两种类型,表明我们是想购买还是出售。如果没有此指示,订单将不会发送。
这些细节非常重要,我们应该注意到它们,否则我们将无法使用这个系统。如果你不知道或忽视这些点,你在下一个发展阶段会有很多头痛和疑虑。
下面是 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(def_InfoTerminal.szSymbol, (def_InfoTerminal.ChartMode == SYMBOL_CHART_MODE_LAST ? SYMBOL_LAST : SYMBOL_BID)); ask = (def_InfoTerminal.ChartMode == SYMBOL_CHART_MODE_LAST ? bid : SymbolInfoDouble(def_InfoTerminal.szSymbol, SYMBOL_ASK)); CommonData(type, def_AcessTerminal.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); };
以下是一些与市场执行功能非常相似的东西。例如,止损水平是以金融价值设定的,我们必须仅使用这两个价值中的一个来指示我们是买入还是卖出。然而,EA 中存在的绝大多数代码都有一些不同之处。通常,当我们创建 EA 交易时,它的目标是在非常特定的市场类型上使用,无论是外汇市场还是证券交易所。由于 MetaTrader 5支持这两种类型的市场,我们需要做一些标准化工作,让我们的生活更轻松。在外汇和证券交易所工作不是一回事吗?从用户的角度是,但从编程的角度否。如果你仔细观察,你可以看到我们正在检查当前使用的图表类型。基于此,我们可以得出系统是使用最后价格(Last)还是使用出价(Bid)和要价(Ask)的结论。知道这一点很重要,不是为了下订单,而是为了知道要使用什么类型的订单。稍后我们将需要将此类订单实现到系统中,以模拟交易服务器的操作。但在这个阶段,我们需要知道的是,订单的类型与执行价格一样重要。如果我们把价格放在正确的位置,但订单类型错误,那么就会出现问题,因为订单将在与您预期的服务器执行时间不同的时间执行。
MetaTrader 5 的新手用户在填写待处理订单时经常会出错。在任何类型的市场中都不会,因为随着时间的推移,用户会习惯市场,不会那么容易出错。然而,当我们从一个市场转移到另一个市场时,事情会变得更加复杂。如果图表系统基于BID-ASK,则设置订单类型的方法与基于LAST的图表系统不同。这些差异是微妙的,但它们是存在的,并导致订单不是挂单,而是以市场价格执行的。
结论
尽管讨论了这些材料,但本文不会附带任何代码,原因是我们不是在实现订单系统,而是简单地创建一个BASIC类来实现这样的系统。您可能已经注意到,与此处显示的 C_Orders 类代码相比,缺少了几个函数和方法。我的意思是,与前几篇文章中讨论的代码相比,我们讨论了订单系统。
之所以会发生这种情况,是因为我决定将订单系统分为几个部分,有些更大,有些更小。这将帮助我清楚而简单地解释系统将如何与回放/模拟服务集成。相信我,这不是最容易的任务,相反,它相当复杂,包括许多你可能还不熟悉的概念。因此,我必须逐步给出解释,这样文章才可以理解,内容才不会完全混乱。
在下一篇文章中,我们将研究如何让这个订单系统开始与交易服务器交互。至少在物理层面上,这样我们就可以在演示或真实账户上使用EA。在那里,我们将开始了解订单类型是如何工作的,以便我们可以从模拟系统开始。如果我们做相反的事情,或者把模拟系统和真实系统放在一起,结果将是完全混乱。下一篇文章见!
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/11393
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.



