
开发回放系统(第 34 部分):订单系统 (三)
概述
在上一篇文章《开发回放系统》(第 33 部分)中:订单系统 (二) ,我解释了我们将如何构建订单系统。在那篇文章中,我们查阅了大部分代码,并触及了我们必须解决的复杂性和问题。虽然代码看似简单易用,但事实远非如此。因此,在这篇文章中,我们将深入探讨我们在实现方面的实际情况和仍然需要做的事情。我们还需要讨论和解释 C_Manager 类中的最后一个方法,并对 EA 代码进行注释。就 EA 代码而言,我们将主要关注修改过的部分。这样,您就无需在 MetaTrader 5 平台上进行测试,就能知道它将如何表现。你可以尽情测试,因为代码将包含在本文的附录中。
许多人可能会觉得没有必要按照展示的那样对系统进行测试,希望在实际实验之前能有一个更兼容的系统。这不是一个好主意,因为如果你现在不了解系统是如何运行的,那么你在了解它将来如何运行时就会遇到很大的问题。更糟糕的是,如果由于某种原因没有在这里展示,而您又对开发不感兴趣,那么您就无法对其进行调整,使其生成您希望看到的内容。随着系统的逐步发展,你可以更加关注一些细节,测试其他细节,或者等到系统成熟到你真正觉得是时候把它放到平台上并分析它的表现时再进行测试。
我知道,很多人喜欢拿起一个更复杂的系统并使用它,而其他人则喜欢看着它成长和发展。好吧,我们先来看看以前没有提到的一个方法。由于这是一个相当大的话题,我们将用一个单独的章节来讨论。
C_Manager 类的主要函数:DispatchMessage
这个函数无疑是我们创建的整个类的核心。它处理从 MetaTrader 5 平台生成并发送到我们程序的事件,这些事件是我们希望平台向我们发送的。例如,CHARTEVENT_MOUSE_MOVE。此外,还有其他发送的事件,我们的程序可能会忽略它们,因为它们对我们正在创建的项目并无太大帮助。CHARTEVENT_OBJECT_CLICK 就是一个例子。
所有的事件处理都集中在类中,这使得在模块中运行项目变得更加容易。虽然这看起来似乎很费事,但你很快就会发现,这样可以更方便地将代码从一个项目转移到另一个项目,从而加快新项目的开发。
这里有两个要点:
- 首先,通过将事件处理集中在一个地方,以及使代码更容易在项目之间移植和移动,我们减少了需要放在主代码(这里是 EA)中的代码量。这使得调试变得更加容易,因为它减少了在重用类和处理事件时可能出现的错误数量。
- 第二个要点则要复杂一些。这关系到每个程序如何运行。某些类型的代码应该按照一定的顺序执行。很多时候,人们编写的代码必须按照特定的顺序执行,否则就无法运行或产生错误的结果。
有了这两点,我们就可以查看该方法的代码,找出并理解执行该方法的原因:
void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) { static double price = 0; bool bBuy, bSell; def_AcessTerminal.DispatchMessage(id, lparam, dparam, sparam); def_AcessMouse.DispatchMessage(id, lparam, dparam, sparam); switch (id) { case CHARTEVENT_KEYDOWN: if (TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL)) { if (TerminalInfoInteger(TERMINAL_KEYSTATE_UP)) ToMarket(ORDER_TYPE_BUY); if (TerminalInfoInteger(TERMINAL_KEYSTATE_DOWN))ToMarket(ORDER_TYPE_SELL); } break; case CHARTEVENT_MOUSE_MOVE: bBuy = def_AcessMouse.CheckClick(C_Mouse::eSHIFT_Press); bSell = def_AcessMouse.CheckClick(C_Mouse::eCTRL_Press); if (bBuy != bSell) { if (!m_Objects.bCreate) { def_AcessTerminal.CreateObjectGraphics(def_LINE_PRICE, OBJ_HLINE, m_Objects.corPrice, 0); def_AcessTerminal.CreateObjectGraphics(def_LINE_STOP, OBJ_HLINE, m_Objects.corStop, 0); def_AcessTerminal.CreateObjectGraphics(def_LINE_TAKE, OBJ_HLINE, m_Objects.corTake, 0); EventChartCustom(def_InfoTerminal.ID, C_Mouse::ev_HideMouse, 0, 0, ""); m_Objects.bCreate = true; } ObjectMove(def_InfoTerminal.ID, def_LINE_PRICE, 0, 0, def_InfoMouse.Position.Price); ObjectMove(def_InfoTerminal.ID, def_LINE_TAKE, 0, 0, def_InfoMouse.Position.Price + (Terminal.FinanceToPoints(m_Infos.FinanceTake, m_Infos.Leverage) * (bBuy ? 1 : -1))); ObjectMove(def_InfoTerminal.ID, def_LINE_STOP, 0, 0, def_InfoMouse.Position.Price + (Terminal.FinanceToPoints(m_Infos.FinanceStop, m_Infos.Leverage) * (bSell ? 1 : -1))); if ((def_AcessMouse.CheckClick(C_Mouse::eClickLeft)) && (price == 0)) CreateOrder((bBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL), price = def_InfoMouse.Position.Price); }else if (m_Objects.bCreate) { EventChartCustom(def_InfoTerminal.ID, C_Mouse::ev_ShowMouse, 0, 0, ""); ObjectsDeleteAll(def_InfoTerminal.ID, def_Prefix); m_Objects.bCreate = false; price = 0; } break; } }
不要害怕上面的代码,虽然这些代码乍一看似乎很复杂,但你真的不应该害怕它。我们所做的一切都很简单,也相对普通。代码的外观给人的第一印象是非常复杂和难以理解,所以,让我们一步一步来。首先,让我们来看看第一部分调用。为了更好地解释它们,我将把它们分成几个部分。我相信这样的解释会更容易理解。我们将集中讨论几个要点,这样您就不必滚动页面就能找到我们要讨论的内容。
def_AcessTerminal.DispatchMessage(id, lparam, dparam, sparam); def_AcessMouse.DispatchMessage(id, lparam, dparam, sparam);
还记得我们提到过,在某些情况下,我们需要所有事情都按照一定的顺序发生吗?上面这两行正是关于这一点的。它们将防止您忘记或(更糟糕的是)在 EA 代码中将事件处理放在错误的顺序中。在编写代码的现阶段,这并不会产生任何影响,但我们在 EA 中放置的代码越少,将来的效果就会越好。错过某个事件,我们可能会导致整个项目以完全出乎意料的方式运行,或者不是我们想要的方式运行。
这一切都很清楚。现在,我们可以从CHARTEVENT_KEYDOWN事件开始。它将处理您按下某个键时发生的触发行为。
case CHARTEVENT_KEYDOWN: if (TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL)) { if (TerminalInfoInteger(TERMINAL_KEYSTATE_UP)) ToMarket(ORDER_TYPE_BUY); if (TerminalInfoInteger(TERMINAL_KEYSTATE_DOWN))ToMarket(ORDER_TYPE_SELL); } break;
这里的情况似乎有点令人困惑:根据文档,lparam变量将包含按下的键的代码。这种情况确实存在,但问题是我们需要用稍微不同的方式来做事。按键被按下时,操作系统会生成一个特定事件。如果 MetaTrader 5 从操作系统接得到焦点,它将传递生成的事件。由于我们的代码为按键操作提供了一个处理程序,因此可以将按键处理与其他类型的事件隔离开来。
请注意:上面显示的代码实际上并不需要放在 CHARTEVENT_KEYDOWN 事件中,可以把它放在这个事件之外。不过,通过将其放在 CHARTEVENT_KEYDOWN 中,我们可以避免出现一些尴尬的情况。当我们对关键条件(即 CHARTEVENT_KEYDOWN 事件)进行分析的同时,平台提醒我们由于某种原因触发了其他类型的事件,这时就会出现这种情况。
想想在触发 CHARTEVENT_CHART_CHANGE 等事件时如何处理键盘状态,实际上,当图表上发生某些变化时,该事件就会被激活。与此同时,我们的程序还会检查键盘的状态。这些东西没有实际意义,而且需要大量时间来实现。这就是我在 CHARTEVENT_KEYDOWN 事件中隔离键盘状态解析的原因。
让我们回到代码上来。你会注意到,我正在使用 TerminalInfoInteger 函数来识别和隔离特定的键盘代码。如果不这样做,我们就必须额外检查 CTRL 键是否与另一个键同时按下,在这种情况下是向上箭头键(UP ARROW)还是向下箭头键(DOWN ARROW)。这正是我们所做的。我们需要一个键盘快捷键,这样我们的程序(在本例中为 EA)就知道在编程方面该做什么。如果您按下 CTRL + UP ARROW 组合键,EA 就会明白我们想按市价买入。如果按下 CTRL + DOWN ARROW 组合键,EA 将按市价卖出。请注意,尽管lparam变量单独指定了键值,但这并不能帮助我们处理键盘快捷键。但是,如果按现在的方式操作,只按其中组合中的一个键,EA 将不会收到任何按市场价格进行交易的指令。
如果您认为这种组合键与您正在使用的产品有冲突,只需更改即可。但请注意,代码应保留在 CHARTEVENT_KEYDOWN 事件中,以利用只有在 MetaTrader 5 触发按键事件时才会执行代码这一特点,从而避免执行不必要的代码。另外,lparam 变量中显示的按键代码是根据不同地区的表格显示的,这使得事情变得更加复杂。我在这里展示的方法实际上不会使用这样的表格。
现在让我们看看下一个事件处理程序 CHARTEVENT_MOUSE_MOVE。为了便于解释,我将按照在类代码中出现的顺序,把它分成几个小部分。
case CHARTEVENT_MOUSE_MOVE: bBuy = def_AcessMouse.CheckClick(C_Mouse::eSHIFT_Press); bSell = def_AcessMouse.CheckClick(C_Mouse::eCTRL_Press);
请注意一点,在这里,我们使用 C_Study 类访问 C_Mouse 类。不要忘记这一点,注意与上面讨论的 CHARTEVENT_KEYDOWN 事件处理程序不同的是,这里我们捕捉的是按钮的状态。现在指的是鼠标。你不觉得困扰吗?事实上,这些按钮属于鼠标,而不是字母数字键盘。为什么呢?你想把我弄糊涂吗?不是这样的,我亲爱的读者。事实上,我们可以在字母数字键盘上按 SHIFT 和 CTRL 键,而且还能在 C_Mouse 类中完成操作,这是因为它并不完全是这样工作的。这些 SHIFT 和 CTRL 键实际上属于鼠标。但不是普通的鼠标。我说的是一种非常特殊的鼠标,与图 01 中所示的差不多:
图 01:
这种鼠标的机身上有额外的按钮。对于操作系统来说,以及对于平台和我们的程序来说,我们所说的 SHIFT 和 CTRL 键实际上是鼠标的一部分。不过,由于鼠标可能没有这些额外的按钮,操作系统允许使用键盘,以使得平台和程序将确保以正确的方式解释代码。因此,不应将 CHARTEVENT_KEYDOWN 事件中的 SHIFT 和 CTRL 键与 CHARTEVENT_MOUSE_MOVE 事件中使用的 SHIFT 和 CTRL 键混淆。
现在我们知道了 SHIFT 和 CTRL 键的状态,就可以查看事件代码的其余部分了。这可以从下面的片段中判断出来。
if (bBuy != bSell) { if (!m_Objects.bCreate) { def_AcessTerminal.CreateObjectGraphics(def_LINE_PRICE, OBJ_HLINE, m_Objects.corPrice, 0); def_AcessTerminal.CreateObjectGraphics(def_LINE_STOP, OBJ_HLINE, m_Objects.corStop, 0); def_AcessTerminal.CreateObjectGraphics(def_LINE_TAKE, OBJ_HLINE, m_Objects.corTake, 0); EventChartCustom(def_InfoTerminal.ID, C_Mouse::ev_HideMouse, 0, 0, ""); m_Objects.bCreate = true; } ObjectMove(def_InfoTerminal.ID, def_LINE_PRICE, 0, 0, def_InfoMouse.Position.Price); ObjectMove(def_InfoTerminal.ID, def_LINE_TAKE, 0, 0, def_InfoMouse.Position.Price + (Terminal.FinanceToPoints(m_Infos.FinanceTake, m_Infos.Leverage) * (bBuy ? 1 : -1))); ObjectMove(def_InfoTerminal.ID, def_LINE_STOP, 0, 0, def_InfoMouse.Position.Price + (Terminal.FinanceToPoints(m_Infos.FinanceStop, m_Infos.Leverage) * (bSell ? 1 : -1))); if ((def_AcessMouse.CheckClick(C_Mouse::eClickLeft)) && (price == 0)) CreateOrder((bBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL), price = def_InfoMouse.Position.Price); }else if (m_Objects.bCreate) { EventChartCustom(def_InfoTerminal.ID, C_Mouse::ev_ShowMouse, 0, 0, ""); ObjectsDeleteAll(def_InfoTerminal.ID, def_Prefix); m_Objects.bCreate = false; price = 0; }
虽然这段代码看起来很复杂,但它实际上只做了三件相当简单的事情。它们可以分离成单独的函数或类中的方法。但现在,它们将保留在这里,至少在调用方面,让事情变得简单一些。首先要做的是通过 EA 通知平台我们要下挂单。但在此之前,我们需要确定限价水平和订单的位置。为此,我们要使用此时在代码中创建的三个对象。请注意,我们还向 C_Mouse 类系统发送了一个事件,告诉 C_Mouse 类在接下来的时间里应该隐藏鼠标。这是第一阶段。
一旦在图表上找到所需的对象,我们就可以按照特定的方式移动它们。为此,我们使用了这组函数。但是,如果用户通过创建的对象告诉我们,提交订单的理想时刻是图表上显示的时刻,我们执行请求来设置挂单。注意检查是如何进行的,这样我们就能知道鼠标和图表上发生了什么。
至于第三点,也是最后一点,事件的发展如下。首先,向 C_Mouse 类系统发送一个事件,使鼠标在图表上重新可见。之后,我们将立即删除已创建的对象。
现在,这段代码中有一些重要内容。在您编写自己的代码时,应始终注意这一点。如果您刚刚开始编程,可能还没有注意到上面代码中一个非常有趣同时也很危险的地方。如果操作不当,就会有危险。我说的是 RECURSION(递归)。在上面的代码中,我们使用了递归。如果规划不当,我们将陷入死循环。系统一旦进入使用递归的代码部分,就可能再也无法离开。
要了解这种递归是如何发生的,请看下图 02:
图 02:内部消息流。
图 02 中的绿色箭头正是递归发生的位置。但在代码中是如何发生的呢?要了解这一点,请看下面所显示的 C_Manager 类中 DispatchMessage 方法的代码。
void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) { // ... def_AcessMouse.DispatchMessage(id, lparam, dparam, sparam); switch (id) { // ... case CHARTEVENT_MOUSE_MOVE: // ... if (bBuy != bSell) { if (!m_Objects.bCreate) { // ... EventChartCustom(def_InfoTerminal.ID, C_Mouse::ev_HideMouse, 0, 0, ""); } // ... }else if (m_Objects.bCreate) { EventChartCustom(def_InfoTerminal.ID, C_Mouse::ev_ShowMouse, 0, 0, ""); // ... } break; } }
图 02 中的递归可能不是很清楚。如果只看上面的代码,可能也不够清楚。但是,如果将图 02 与上面的代码结合起来,就可以看到递归是如何工作的。如果规划不当,我们将会面临严重问题。让我们开始解释,以便让新程序员了解递归的威力和危险。当我们需要做一件非常复杂的事情时,无论它是什么,我们都需要把这个过程分解成更小或更简单的任务。这些任务可以重复一定的次数,最终得到更复杂的东西,同时遵循一个简单的概念。这就是递归的力量。然而,它并不仅仅用于这种场景。虽然它最常与此相关,但我们也可以使用递归来解决其他问题。
上述片段就是其中之一。要理解其中的解释,请再看一次图 02。当用户产生一个事件,MetaTrader 5 平台就会调用 OnChartEvent 函数。在这种情况下,该函数会调用 C_Manager 类中的 DispatchMessage 方法。此时,我们调用 C_Mouse 类中通过继承而存在的事件处理程序,特别是该类中的 DispatchMessage 方法。方法返回后,程序将从原来的位置继续执行。我们进入一个事件处理函数来检查用户是否要创建或删除辅助线。因此,在某个时刻,我们需要调用 EventChartCustom。此时,递归启动。
实际上,MetaTrader 5 会对 OnChartEvent 函数进行新的调用,因此会再次执行该函数,并调用 C_Manager 类中的 DispatchMessage 方法。这反过来又通过继承调用 C_Mouse 类来执行一个自定义方法,该方法将根据情况导致鼠标光标出现或消失。但是,由于递归的原因,代码的返回结果并不像很多人想象的那样。事实上,它会返回,但这将再次触发 C_Manager 类 DispatchMessage 方法中代码的执行。危险就在这里:如果你将调用放在 C_Mouse 类中处理的自定义事件出现在 C_Manager 类中,它也会在 C_Manager 类中被处理。如果我们不小心在 C_Mouse 类中使用 EventChartCustom 函数再次处理该事件,我们就会发现自己陷入了一个无休止的循环。
但是,由于 C_Manager 类没有这样的事件,而是由 C_Mouse 类处理,因此我们将回到 OnChartEvent 函数,该函数将被完全执行。执行 OnChartEvent 函数后,将返回到调用 EventChartCustom 的位置。就像上面代码显示得那样。这将导致执行 C_Manager 类中 DispatchMessage 方法的所有剩余代码。完成后,我们将返回到 OnChartEvent 函数,在那里它将被完全执行,从而释放平台以执行其他类型的事件。
因此,在调用 EventChartCustom 时,由于递归的原因,执行的代码量至少是 OnChartEvent 函数的两倍。这样做似乎效率不高,但关键是代码简单,不会对平台的整体性能造成很大影响。不过,好在我们总能意识到真正发生了什么。与更多的模块化代码相比,这种情况下递归的成本很低。但在某些情况下,这些成本可能无法弥补,并可能使代码运行速度过慢。在这种情况下,我们将不得不采取一些其他行动,但目前我们的情况并非如此。
我想我已经详细解释了 C_Manager 类中使用的 DispatchMessage 方法。虽然这看起来相当复杂,但事实上我们离真正的复杂还很远,因为系统还不知道如何与跨订单模型协同工作。为此,需要对 DispatchMessage 方法进行重大修改。不过,我们将把这个问题留待将来解决。
现在让我们来看看 EA 代码中的进一步变化。
分析 EA 交易中的更新内容
虽然 EA 现在可以在设置的交易时间内下单和交易,但其代码并未发生任何重大变化。有一部分代码值得特别注意,我会解释那里发生了什么。这一点很重要,涉及到用户交互和 OnInit 事件中的代码。让我们从用户交互开始。它在下面的代码中显示:
input group "Mouse"; input color user00 = clrBlack; //Price Line input color user01 = clrPaleGreen; //Positive Study input color user02 = clrLightCoral; //Negative Study input group "Trade"; input uint user10 = 1; //Leverage input double user11 = 100; //Take Profit ( Finance ) input double user12 = 75; //Stop Loss ( Finance ) input bool user13 = true; //Is Day Trade //+------------------------------------------------------------------+ input group "Control of Time" input string user20 = "00:00 - 00:00"; //Sunday input string user21 = "09:05 - 17:35"; //Monday input string user22 = "10:05 - 16:50"; //Tuesday input string user23 = "09:45 - 13:38"; //Wednesday input string user24 = "11:07 - 15:00"; //Thursday input string user25 = "12:55 - 18:25"; //Friday input string user26 = "00:00 - 00:00"; //Saturday
该代码负责用户交互。在这里,我们有两组新的信息,用户可以对其进行访问和配置。在第一组中,用户选择交易操作的执行方式 - 下市价单或挂单。设置非常简单,代表杠杆率的数值(user10)应代表您使用最小交易量交易资产的倍数。在外汇交易中,您很可能会使用 100 或类似的数值来找到一个合适的保证金比率。否则,您将使用分值的订单,这将使限价线远离您期望的位置。如果您在交易所进行交易,您必须报告要使用的股票数量。否则,请指定手数。对于期货交易,请指定合约数量。所以,这一切都很简单明了。至于止盈(user11)和止损(user12),您不应使用点数,而应说明将使用的金融资产价格值。代码必须对该值进行相应调整,以反映资产价格的正确位置。最后一个变量(user13)仅用于表示我们是在做多还是做空。
重要提示:由于经纪商可能有非常具体的交易条件,因此应谨慎测试这一机制。请事先与您的经纪商核实。
现在,在第二组中,在正确设置之前需要检查一些事项。这并不是因为它们很复杂或难以理解,而是因为您应该明白,这些变量将决定 EA 何时允许我们发送订单或设置挂单。管理、终止甚至修改订单的问题将不再依赖于 EA,MetaTrader 5 平台将负责这项工作,至少目前如此。
然后,您可以设置一个 1 小时的窗口,允许 EA 使用其拥有的资源工作。这种配置以一周为单位,而不是特定的一天或特殊日期。
要理解这一点,请看下面的 OnInit 代码:
int OnInit() { string szInfo; terminal = new C_Terminal(); study = new C_Study(terminal, user00, user01, user02); manager = new C_Manager(terminal, study, user00, user02, user01, def_MagicNumber, user12, user11, user10, user13); if (_LastError != ERR_SUCCESS) return INIT_FAILED; for (ENUM_DAY_OF_WEEK c0 = SUNDAY; c0 <= SATURDAY; c0++) { switch (c0) { case SUNDAY : szInfo = user20; break; case MONDAY : szInfo = user21; break; case TUESDAY : szInfo = user22; break; case WEDNESDAY : szInfo = user23; break; case THURSDAY : szInfo = user24; break; case FRIDAY : szInfo = user25; break; case SATURDAY : szInfo = user26; break; } (*manager).SetInfoCtrl(c0, szInfo); } MarketBookAdd(def_InfoTerminal.szSymbol); OnBookEvent(def_InfoTerminal.szSymbol); EventSetMillisecondTimer(500); return INIT_SUCCEEDED; }
请注意上面的 OnInit 代码,它代表了 EA 在一周内应如何运行的全貌,而不仅仅是某一天。在整个一周内,有些资产或市场(这里指的是外汇市场)的交易几乎是不间断的,一天中的任何时候都不会停止。如果我们需要为可以全天 24 小时运行的 EA 配置单独的交易时间表,我们就会在换日期间遇到问题。也就是说,只要时间一到 23:59:59,我们就需要停止 EA,并将其设置到后一秒,以找出新的交易时间段。但如果使用上述方法,EA 就可以全年 52 周、每周 7 天、每天 24 小时运行,而不会迷失方向或不知道该使用哪个时间安排。我知道,光看这段代码,很多人可能无法理解这实际是如何发生的。因此,您应该对 EA 进行测试,以了解该系统的工作原理。但这种系统不是全新的,我们在之前的一篇文章中已经讨论过这个问题:创建自动运行的 EA(第 10 部分):自动化 (二).
结论
尽管该系统看起来相当稳定和通用,但它开始出现一个错误。这很奇怪,也很令人费解,因为这个错误的出现毫无道理。原因是错误出现在一个未作任何修改的地方。总之,我们将在下一篇文章中讨论这个错误。附件中包含当前开发阶段的全部代码,您可以对其进行详细研究和分析。在前一篇文章中,我没有附上任何代码。
请注意,在上一篇文章中,我谈到了单击选择对象的系统,而不是平台标准的双击模式。最好的办法是在自己的平台上测试该系统,并在看到其运行结果后得出自己的结论。因此,请下载并运行它,看看系统是如何运作的。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/11484


