
开发回放系统(第33部分):订单系统(二)
概述
在上一篇文章开发回放系统(第32部分):订单系统(I)中,我们开始开发要在 EA 交易中使用的订单系统。我们开发了一个基本系统类,其中只包含我们真正需要开始的那些函数。
人们可能会认为,这些功能本质上是非常基本的,不能完全涵盖订单系统,这并不是完全错误的。这是真的,但在这个早期阶段,我们将开发一个不会直接用于回放/模拟服务的系统。我们将开发一个可以与真实交易服务器一起工作的系统,既可以在模拟账户上工作,也可以在真实账户上工作。我们将广泛使用MetaTrader 5平台,该平台将从一开始就为我们提供所有必要的支持。当平台与服务器通信时,使系统从一开始就完美地运行是非常重要的。因为当只使用回放/模拟器时,我们无法再将 EA 交易系统适应我们的回放/模拟器。我们必须确保回放/模拟器与EA正在做的事情相匹配。这就是为什么我们需要首先在真正的服务器上运行EA。
这一要求减少了系统中的方法和函数的数量,因此也减少了C_Orders类的方法和函数的数量。然而,我们仍然记得,我们正在创建一个大型系统。毫无疑问,C_Orders类将无法满足我们的所有需求。我们只需要让 MetaTrader 5 平台为我们提供使用交易服务器所需的最大支持。MetaTrader 5 最大限度支持的本质是避免在可以避免创建的地方创建东西。我们不应该试图重新发明轮子,而是最大限度地使用平台,以便在使用回放/模拟器系统时投入最少的精力。
但在继续使用订单系统之前,我想向您展示如何解决一个问题。尽管这其实不是问题,而是MetaTrader 5中存在的不便。
尝试简化事情
如果你经常使用 MetaTrader 5,那么你可能已经注意到一件事,一方面令人讨厌,另一方面,委婉地说,令人沮丧。我的意思是,最引人注目的是那些从其他平台切换到 MetaTrader 5 的人,他们在遇到这种操作后,感到恼火或不愿意在某些方面使用它,这些方面主要涉及图表分析。
问题是,当我们对资产图表进行某些分析时,有时在更改或删除用于分析的图形对象时可能会出现困难。您需要了解如何在 MetaTrader 5 中进行配置,才能真正了解如何处理问题。让我们假设您刚刚在计算机上安装了该平台,并且从未使用过它。所以,这是你第一次接触这个平台。我想强调一件事:我说的是某人第一次使用 MetaTrader 5。
这里有一个有趣的问题。我们可以双击旧对象来访问它们,只要没有禁用标志的选择(复选框,见图01),我们就可以实际访问该对象。
图01-复选框已激活。
然而,这并不是重点。问题是,许多来自其他平台的用户习惯于只点击一个对象。因此,对象被选中,并且可以从图表中更改或删除。MetaTrader 5 需要双击并不完全明显,在这种情况下需要一些时间来了解如何进行。
现在我们来谈谈细节。在MetaTrader 5中,可以改变这种行为。要访问设置,请按CTRL+O,这将打开 MetaTrader 5 选项窗口。在此窗口中,转到图表选项卡并启用:“单击鼠标选择对象”。通过这样做,您可以像在其他平台一样在 MetaTrader 5 中使用选择。但即使在这种情况下,通过这种小的调整,我们仍然会缺少一些功能。我在这里的目的是展示如何以不同的方式和新的可能性来做这件事。
作为一名程序员,我发现自己的处境是,我可以以某种方式确保切换到 MetaTrader 5 的用户在使用该平台时获得正确而愉快的体验。这将以一种你只需要按下一次的方式来实现。然而,如果用户没有足够的编程经验,解决方案可能就不那么明显了。此外,有些人可能认为这样的工作没有必要。然而,这将扩展我们的能力。
为了解决这个问题,我们将返回 C_Terminal 类并向其添加几行代码。这将足以为新用户提供更轻松的体验。让我们看看应该添加什么。请参阅以下代码:
virtual void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) { static string st_str = ""; switch (id) { case CHARTEVENT_CHART_CHANGE: m_Infos.Width = (int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS); m_Infos.Height = (int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS); break; case CHARTEVENT_OBJECT_CLICK: if (st_str != sparam) ObjectSetInteger(m_Infos.ID, st_str, OBJPROP_SELECTED, false); if (ObjectGetInteger(m_Infos.ID, sparam, OBJPROP_SELECTABLE) == true) ObjectSetInteger(m_Infos.ID, st_str = sparam, OBJPROP_SELECTED, true); break; case CHARTEVENT_OBJECT_CREATE: if (st_str != sparam) ObjectSetInteger(m_Infos.ID, st_str, OBJPROP_SELECTED, false); st_str = sparam; break; } }
此方法已经是 C_Terminal 类的原始代码的一部分。但它还有一些与两个新事件有关的附加代码,它们来自 MetaTrader 5 平台。上面的代码中有一个细节。但首先我可以解释代码,然后再解释细节。我们声明一个静态局部变量,用于存储我们正在检查的对象的名称。此变量初始化为空字符串。如果我们希望每件事都做得正确,这一点非常重要。当我们点击图表上的一个对象时,它的名称会传递给我们的程序。此外,平台还传递CHARTEVENT_OJECT_CLICK事件,对象名称位于sparam变量中。这些小细节将有助于我们下一步的工作。现在,让我们检查一下本地变量中的名称是否与 MetaTrader 5 报告的名称匹配。在第一次执行的情况下,它们将有所不同;如果它们是一样的,那么什么都不会发生。但如果它们不同,那么平台关注的对象将成为焦点。这是在我们检查的行中完成的,如果需要,可以从对象中移除焦点。
现在请注意以下一点:当对象属性被更改为如图01所示时,我们必须理解用户有意识地说这个对象不应该在无意中受到关注。要么是因为对象不应该被修改,要么是因为它会指向某个东西,我们不想改变它的位置或类似的事情。因此,当我们将复选框标记为图01中的复选框时,我们必须理解该对象将被忽略。这正是这种检查的作用。它检查是否应该忽略给定的对象。如果没有此复选框,用户在对象属性中标记的复选框(如图01所示)将被忽略,对象将可以访问。因此,为了使对象接收这个焦点,我们使用这个特定的代码。请注意,在这段代码中,我们存储对象的名称,这样当我们失去焦点时,我们的程序就会知道对象是什么。当MetaTrader 5触发CHARTEVENT_OJECT_CREATE事件时,也会发生类似的情况。然而,这里有一个细节。与CHARTEVENT_OJECT_CLICK事件不同,只有当我们告诉 MetaTrader 5 我们想了解对象创建的情况时,才会在我们的程序中发生此对象创建事件。
注意:在任何情况下,MetaTrader 5 都将始终触发事件。但其中一些只有在我们通知平台的情况下才会指向我们的代码。
为了告诉 MetaTrader 5,我们希望在图表上创建对象时得到通知,我们的处理方式与删除对象的处理方式大致相同。这是代码:
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_EVENT_OBJECT_CREATE, 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.ValuePerPoint / m_Infos.PointPerTick; m_Infos.ChartMode = (ENUM_SYMBOL_CHART_MODE) SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_CHART_MODE); ResetLastError(); }
这些代码告诉 MetaTrader 5,从现在起,我们希望在图表上创建新对象时收到通知。同样,当我们不想再收到通知时,我们需要告诉 MetaTrader 5。这是使用以下代码完成的:
~C_Terminal() { ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, m_Mem.Show_Date); ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, m_Mem.Show_Descr); ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, false); ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_CREATE, 0, false); }
一旦这些代码被执行,MetaTrader 5 将不再通知我们的代码有关对象创建事件。这类事情非常重要和有趣,因为我们有时想知道用户何时从图表中添加或删除对象。尝试“手动”完成这项工作相当困难。然而,使用 MetaTrader 5,这很容易做到。你可能已经注意到,我们可以处理平台,使其按照我们期望的方式运行。这种行为以及我们能够做到的事实意味着我们可以帮助新用户获得更好的使用平台的体验,这是非常有动力的。这都是一个适应的问题,但如果你作为一名程序员能够帮助“非程序员”,适应会更快。
现在让我们回到本文中实际实现的内容。在我们开始做其他事情之前,我们还有很多事情要做。
扩展订单系统
这个主题的标题可能有点夸张,但我想给我们的订单系统带来一些东西,这在另一系列文章中有所展示。所以,你可以使用我们在那里获得的相同概念和知识。该系列的最新文章可在以下链接中获得:创建一个自动工作的EA(第15部分):自动化(七)。在那篇文章中,我们测试了如何将手动构建和管理的 EA 转换为自动 EA。我现在提出这个话题是因为你可能有兴趣在回放/模拟器中观察 EA 的操作,以便能够对其进行一些调整。请记住,为了测试EA的功能,MetaTrader 5 平台提供了一个出色的策略测试器。在这里,我们不是在与测试器并行地创建一些东西。这个想法是即使在市场关闭的时候也能练习一些技巧。
现在让我们介绍一下创建自动化EA系列中涵盖的概念和想法。我们将从计时器类开始。
重要提示:在第一阶段,随着我们使用 EA 并将其开发为更专注于方便访问交易服务器,我们将快速概述我们将从该系列文章中导入的功能。要获得更详细和深入的解释,请阅读本系列,因为它可能对某些问题有很大帮助。
用 C_ControlOfTime 控制时间
C_ControlOfTime 是一个非常有趣的类。尽管代码相当紧凑,但它允许我们以一种相当简单的方式,根据我们可以交易的时间,制作具有一定控制水平的 EA。许多交易员经常担心第一次市场波动,因此他们过早地进入或退出头寸。在我们不需要情绪的地方,根据情绪做出决定,我们往往会遭受损失。但如果我们坚持这个计划,我们就不会在那一刻进行交易。启用此控件的最简单方法之一是使用时间表。在创建自动化EA的系列中,我展示了如何实现这样一个调度器。所以,让我们将其导入到我们的新EA中。我们将创造同样的行为,这是非常好的,因为没有人想在他们认为合适且对市场交易更安全的时区之外进行交易。因此,我们将让EA帮助我们做到这一点。
C_ControlOfTime 类的启动方式如下:
#property copyright "Daniel Jose" //+------------------------------------------------------------------+ #include "C_Orders.mqh" //+------------------------------------------------------------------+ class C_ControlOfTime : protected C_Orders { private : struct st_00 { datetime Init, End; }m_InfoCtrl[SATURDAY + 1]; //+------------------------------------------------------------------+
在这里,我们将在一个变量中存储允许操作的时间。也许,这个代码中最奇怪的部分是SATURDAY定义的使用。对于那些没有阅读过创建自动化 EA 系列文章的人,我将简要解释这个定义的含义。通过使用这个定义,我们的目标是说我们的矩阵将覆盖一周中的所有日子。但有一个重要的细节:我们需要在数值上增加一。这对于确保矩阵具有正确的长度是必要的,因为它从零开始。但是我们不能有一个零元素的矩阵。我们应该总是从一个大于零的值开始,这就是我们加一的原因。如果我们不这样做,我们最终会得到一个由6个元素组成的矩阵,而不是7个。还有一件事:您可能注意到 C_ControlOfTime 类继承自 C_Orders 类。因此,我们扩展了 C_Orders 类的功能。然而,正如您将在随后的解释中看到的那样,此扩展实际上不会转移到 EA。从关于自动化 EA 的一系列文章中可以更好地理解其原因。
代码中的下一个内容是类构造函数。其代码如下:
C_ControlOfTime(C_Terminal *arg, const ulong magic) :C_Orders(arg, magic) { for (ENUM_DAY_OF_WEEK c0 = SUNDAY; c0 <= SATURDAY; c0++) ZeroMemory(m_InfoCtrl[c0]); }
在这里,你需要了解两件简单的事情。首先,我们在执行 C_ControlOfTime 构造函数中的代码之前初始化 C_Orders 类。第二个可能更奇怪的特征是存在一个循环。有趣的是它的运作方式。请注意,在这个循环中,我们使用的枚举以SUNDAY开始,变量将增加到SATURDAY。有可能吗?是的,我们可以做到,但我们必须小心。主要的问题是SUNDAY的值应该低于SATURDAY。如果不是这样,那么我们在访问矩阵数据时会遇到问题。幸运的是,枚举从 SUNDAY 开始,一天一天地进行到 SATURDAY,所以我们得到了一周中的几天。
使用这种编程模式的主要优点是它提高了代码的级别,使MQL5语言非常接近自然语言。在一系列关于自动化 EA 的文章中详细介绍了这种方法。好吧,在这里我只展示了几件事。阅读前面的文章,您将了解如何使代码更加可读。这种做法使您的代码即使对于那些没有足够编程知识的人来说也是可以理解的。请记住:您不是为机器编程,而是为其他程序员编程。
现在让我们看看另外两个方法。
virtual void SetInfoCtrl(const ENUM_DAY_OF_WEEK index, const string szArg) final { string szRes[], sz1[]; bool bLocal; if (_LastError != ERR_SUCCESS) return; if ((index > SATURDAY) || (index < SUNDAY)) return; if (bLocal = (StringSplit(szArg, '-', szRes) == 2)) { m_InfoCtrl[index].Init = (StringToTime(szRes[0]) % 86400); m_InfoCtrl[index].End = (StringToTime(szRes[1]) % 86400); bLocal = (m_InfoCtrl[index].Init <= m_InfoCtrl[index].End); for (char c0 = 0; (c0 <= 1) && (bLocal); c0++) if (bLocal = (StringSplit(szRes[0], ':', sz1) == 2)) bLocal = (StringToInteger(sz1[0]) <= 23) && (StringToInteger(sz1[1]) <= 59); if (_LastError == ERR_WRONG_STRING_DATE) ResetLastError(); } if ((_LastError != ERR_SUCCESS) || (!bLocal)) { Print("Error in the declaration of the time of day: ", EnumToString(index)); ExpertRemove(); } }
这一部分在创建一个自动工作的EA(第10部分):自动化(二)一文中进行了描述和讨论,我在文中详细解释了它的工作原理。如果你需要更多关于这方面的细节,或者如果你不理解代码,请阅读上面提到的文章,因为这将非常有帮助。下一个方法(如下所示)在同一篇文章中进行了描述。事实上,这里所做的与 C_ControlOfTime 类相关的所有工作都可以在上面提到的文章中看到。唯一的区别在于构造函数。这是因为我们下一步要做的。
virtual const bool CtrlTimeIsPassed(void) final { datetime dt; MqlDateTime mdt; TimeCurrent(mdt); dt = (mdt.hour * 3600) + (mdt.min * 60); return ((m_InfoCtrl[mdt.day_of_week].Init <= dt) && (m_InfoCtrl[mdt.day_of_week].End >= dt)); }
有了这些解释,如果您已经阅读了上述文章,您可以理解 EA 会获得一些额外的参数,以便用户可以配置 C_ControlOfTime 类。但我们需要将这个功能引入一个新的类。正如在一系列关于自动 EA 的文章中一样,我们将在这里使用一个代理类。这是下一节的主题。
C_Manager: 管理类
C_Manager 实际上是一个非常有趣的类,因为它将与 EA 代码相关的大部分复杂性与 EA 本身隔离开来。也就是说,EA 只有几行代码,类将管理和操作所有内容。此代码将与我们在一系列自动化 EA 中看到的代码不同。我们将从一些基本的东西开始。主要的问题是 EA 并不能在所有情况下都有效。最初,我们将专注于使 EA 操作尽可能安全。因为以后我们还需要让它在回放/模拟器系统中运行。这是系统中最困难的部分。
为了防止事情变得极其复杂,我们将首先使 EA 与 MetaTrader 5 平台中的可用内容一起工作。这意味着订单系统并没有那么广泛。例如,它不能在交叉订单模式中使用,也就是说,在这个早期阶段,我们不能使用订单系统发送EA正在处理的资产以外的资产的订单。
现在让我们来看一下 C_Manager 类当前开发状态下的代码。代码从以下几行开始:
#property copyright "Daniel Jose" //+------------------------------------------------------------------+ #include "C_ControlOfTime.mqh" #include "..\System EA\Auxiliar\Study\C_Study.mqh" //+------------------------------------------------------------------+ #define def_Prefix "Manager" #define def_LINE_PRICE def_Prefix + "_PRICE" #define def_LINE_TAKE def_Prefix + "_TAKE" #define def_LINE_STOP def_Prefix + "_STOP" //+------------------------------------------------------------------+ #define def_AcessTerminal (*Terminal) #define def_InfoTerminal def_AcessTerminal.GetInfoTerminal() #define def_AcessMouse (*Study) #define def_InfoMouse def_AcessMouse.GetInfoMouse() //+------------------------------------------------------------------+
在这里,我们提供了一些声明,使进一步的编码更加容易。考虑到这个系统相当复杂,这些定义很可能在未来会发生一些变化。
让我们看看类从哪里开始,以下是它的代码:
class C_Manager : public C_ControlOfTime { private : struct st00 { double FinanceStop, FinanceTake; uint Leverage; bool IsDayTrade, AccountHedging; }m_Infos; struct st01 { color corPrice, corTake, corStop; bool bCreate; }m_Objects; //+------------------------------------------------------------------+ C_Terminal *Terminal; C_Study *Study;
在这里我们声明类的私有全局变量。为了更好地组织,我将元素划分为结构。有了这些结构,以后将更容易修改和删除部分代码。这肯定会发生在未来的文章中。目前,我们将尝试使系统以最简单的形式工作,充分利用 MetaTrader 5 的功能。我重复这一点,因为你不应该把这个系统作为最终系统。它仍处于发展阶段。
让我们来看看 C_Manager 类中的第一个函数。其代码如下:
bool CreateOrder(const ENUM_ORDER_TYPE type, const double Price) { ulong tmp; if (!CtrlTimeIsPassed()) return false; tmp = C_Orders::CreateOrder(type, Price, m_Infos.FinanceStop, m_Infos.FinanceTake, m_Infos.Leverage, m_Infos.IsDayTrade); return tmp > 0; }
该函数将一个挂单发送到交易服务器。但是,请注意,只有在时间跟踪系统允许发送订单的情况下,才会发送订单。如果时间超过了计划的交易时间,订单将不会发送。这里我们只需要说明下订单的价格。不管我们是买入还是卖出,其他的都是根据全局类变量填写的。
下面是另一个函数,它对类是私有的:
bool ToMarket(const ENUM_ORDER_TYPE type) { ulong tmp; if (!CtrlTimeIsPassed()) return false; tmp = C_Orders::ToMarket(type, m_Infos.FinanceStop, m_Infos.FinanceTake, m_Infos.Leverage, m_Infos.IsDayTrade); return tmp > 0; }
在这里,我们发送了一个请求,要求以市场价格执行,即可用的最佳价格。在这种情况下,我们所需要表明的就是我们是买入还是卖出。其他一切都是基于全局类变量中的信息完成的。你可能在想:如何初始化这些全局变量?这应该在EA代码中完成吗?答案是否。如果没有对类中存在的全局变量所属的类的适当关心和了解,则不应以任何方式访问它们。
我们有一些资源来初始化这些变量。现在,我们将使用最简单的方法-构造函数。毫无疑问,使用构造函数初始化类变量是可用的最佳方式。然而,出于实用性的原因,我们稍后将对此进行更改。现在,我们可以在构造函数中实现我们需要的东西。其代码如下:
C_Manager(C_Terminal *arg1, C_Study *arg2, color cPrice, color cStop, color cTake, const ulong magic, const double FinanceStop, const double FinanceTake, uint Leverage, bool IsDayTrade) :C_ControlOfTime(arg1, magic) { string szInfo = "HEDGING"; Terminal = arg1; Study = arg2; if (CheckPointer(Terminal) == POINTER_INVALID) SetUserError(C_Terminal::ERR_PointerInvalid); if (CheckPointer(Study) == POINTER_INVALID) SetUserError(C_Terminal::ERR_PointerInvalid); if (_LastError != ERR_SUCCESS) return; m_Infos.FinanceStop = FinanceStop; m_Infos.FinanceTake = FinanceTake; m_Infos.Leverage = Leverage; m_Infos.IsDayTrade = IsDayTrade; m_Infos.AccountHedging = false; m_Objects.corPrice = cPrice; m_Objects.corStop = cStop; m_Objects.corTake = cTake; m_Objects.bCreate = false; switch ((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE)) { case ACCOUNT_MARGIN_MODE_RETAIL_HEDGING: m_Infos.AccountHedging = true; break; case ACCOUNT_MARGIN_MODE_RETAIL_NETTING: szInfo = "NETTING"; break; case ACCOUNT_MARGIN_MODE_EXCHANGE : szInfo = "EXCHANGE"; break; } Print("Detected Account ", szInfo); }
尽管这段代码看起来很长,但考虑到我们将初始化类中存在的所有全局变量,它实际上非常简单。所有这些初始化实际上都是基于 EA 代码提供的参数完成的。因此,在这里我们初始化系统并检查 EA 使用的帐户类型。这在以后会很重要。现在它只是为我们了解账户类型提供了分析信息。该信息在 MetaTrader 5 平台工具栏中使用此行创建的消息提供。
我们还有一个析构函数:
~C_Manager()
{
ObjectsDeleteAll(def_InfoTerminal.ID, def_Prefix);
}
尽管代码中包含这一行,但它几乎不会执行任何操作。好吧,我们不能指望一切顺利。因此,如果出现任何问题,析构函数将删除该类创建的对象。
结论
您可能已经注意到这些知识是可重复使用的。我们不会凭空创造一些东西。我们总是重复使用和改进事物。因此,它们会随着时间的推移而改进。但是 C_Manager 类仍然有一个方法需要解释。作为 EA 代码,它处于当前的开发阶段。但为了给出更充分的解释,最重要的是,不要让这篇文章读起来冗长乏味,我将在下一篇文章中专门介绍一个关于方法和 EA 代码的故事。
由于代码尚未完全解释,并且我不希望您在没有足够知识的情况下使用任何内容,因此本文将不包含任何代码。抱歉了,但我认为还是这样比较好。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/11482

