
从头开始开发智能交易系统(第 30 部分):CHART TRADE 当作指标?
概述
在上一篇文章从头开始开发智能交易系统(第 29 部分)中,我们已经从 EA 中删除了 Chart Trade。 之前,我们曾对其它一些思路做过同样的事情,比如 Volume At Price 和 Times & Trade,从而提高 EA 的性能和可靠性。 从 EA 中删除 Chart Trade 之后,我们只剩下最基本的订单系统。 虽然这对某些用户来说似乎还不够,但 EA 实际上可以完成所有工作。 但有些人喜欢按市价入场、离场交易,而不喜欢放置挂单,他们宁可等待价格达到理想价位才入场或离场交易。
当我们用 MetaTrader 5 平台来处理我们正在交易的资产时(我提到这一点是因为我们可用到交叉订单系统,我们曾在本系列的第 11 部分中进行过讨论),我们可按下 “快速交易” 按钮放置市价单。 它们已在左上角提供。 它们看起来像这样:
这些按钮的工作方式与基本的 Chart Trading 类似,但它们不能用于交叉订单系统,因为它们根本不可见。 那么,在这种情况下,我们将不得不回到我们的 Chart Trade。 但它不再于 EA 内部使用,也不会成为 EA 代码的一部分。 从现在开始,Chart Trade 只是一个指标。
为什么我们需要针对 Chart Trading 如此处置? 原因在于 EA 应该只负责交易系统,并且所有不属于该系统的部分都应该以某种方式从 EA 代码中剔除。 也许现在看来这似乎毫无意义,但很快就会变得更加清晰,因为我已经在准备本文的续篇,我将解释这种现象的确切原因。
甚至可以把 Chart Trading 用作脚本,但这有一个缺点:每次我们更改图表时间帧时,脚本都会关闭,因此我们必须再次手动运行它。
如果我们将其当作指标,就不是这种情况了。 原因之一是 Chart Trade 不会影响指标的执行线程,如此 EA 就不受束缚,可专注于其代码,即仅在意订单和仓位管理。
尽管我们不会拥有原始版本的所有控制和信息,但这个 Chart Trade 作为一个指标要简单得多,且仍然有效。 MetaTrader 5 系统功能强大,但操作简单。 不过,它未对我们提供访问 Chart Trade 某些信息的能力。
那么,我们就开始吧 — 这个话题将会非常有趣。
2.0. 开发 Chart Trade 指标
为了构建这个指标,我们必须要做很多修改。 我们将实现这些修改,以便把 Chart Trade 带回到图表,且无需令其成为 EA 代码的一部分。
乍一看,显示于下的代码似乎至少足以编译 Chart Trade。 但是不行,因为它与我们必须关闭的若干件事情有着内在的联系。 我们不会完全删除它,因为我们可能希望有一天将其带回到 EA 之中。
#property copyright "Daniel Jose" #property indicator_chart_window #property indicator_plots 0 //+------------------------------------------------------------------+ #include <NanoEA-SIMD\SubWindow\C_TemplateChart.mqh> //+------------------------------------------------------------------+ C_TemplateChart Chart; C_Terminal Terminal; //+------------------------------------------------------------------+ int OnInit() { Chart.AddThese("IDE(,,170, 240)"); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) { return rates_total; } //+------------------------------------------------------------------+
如果您尝试编译此代码,会遇到很多错误,但我们会逐一修复它们,从而保持代码与 EA 兼容,同时对其进行调整,以便当作指标。
我们需要做的第一件事是隔离创建或管理子窗口的系统。 由于 Chart Trade 不会用到这些子窗口,因此无需将此代码嵌入到指标当中。 这相当容易做到。 我们需要编辑 C_Chart_IDE.mqh,如下所示:
#ifdef def_INTEGRATION_CHART_TRADER #include <NanoEA-SIMD\SubWindow\C_SubWindow.mqh> #include <NanoEA-SIMD\Trade\Control\C_IndicatorTradeView.mqh> #else #include <NanoEA-SIMD\SubWindow\C_ChartFloating.mqh> #include <NanoEA-SIMD\Auxiliar\C_Terminal.mqh> #endif //+------------------------------------------------------------------+ #ifdef def_INTEGRATION_CHART_TRADER class C_Chart_IDE : public C_SubWindow #else class C_Chart_IDE : public C_ChartFloating #endif
因此,我们将系统与子窗口彻底隔离,同时避免 Chart Trade 和 EA 的订单显示系统之间的任何连接。 请注意,这个控制将由 def_INTEGRATION_CHART_TRADER 定义,但由于此定义仅在 EA 中使用,因此其中的任何内容都不会编译到指标之中。
由于指标不会用到子窗口,我们还应绕过一些内容。 其中之一如下所示:
bool Create(bool bFloat) { m_CountObject = 0; if ((m_fp = FileOpen("Chart Trade\\IDE.tpl", FILE_BIN | FILE_READ)) == INVALID_HANDLE) return false; FileReadInteger(m_fp, SHORT_VALUE); for (m_CountObject = eRESULT; m_CountObject <= eEDIT_STOP; m_CountObject++) m_ArrObject[m_CountObject].szName = ""; m_SubWindow = ((m_IsFloating = bFloat) ? 0 : GetIdSubWinEA()); m_szLine = ""; while (m_szLine != "</chart>") // ... Rest of the code...
GetIdSubWinEA 函数返回指标所在窗口的编号。 此调用在多个不同的点执行。 对此有两种解决方案。 第一个方案是在调用发生的每一处,都针对上述函数进行以下修改。
bool Create(bool bFloat) { m_CountObject = 0; if ((m_fp = FileOpen("Chart Trade\\IDE.tpl", FILE_BIN | FILE_READ)) == INVALID_HANDLE) return false; FileReadInteger(m_fp, SHORT_VALUE); for (m_CountObject = eRESULT; m_CountObject <= eEDIT_STOP; m_CountObject++) m_ArrObject[m_CountObject].szName = ""; #ifdef def_INTEGRATION_CHART_TRADER m_SubWindow = ((m_IsFloating = bFloat) ? 0 : GetIdSubWinEA()); #else m_SubWindow = 0; #endif // ... The rest of the code...
这确实能解决问题,但我们将不得不进行如此多的修改,以至于一段时间后代码将变得难以理解,因为这些条件编译指令太多了。 我有一个更简单,但同样有效的方案:为了经由定义“模拟”此调用和任何其它调用,我们可以简单地将以下代码片段添加到系统代码之中。
#ifndef def_INTEGRATION_CHART_TRADER #define GetIdSubWinEA() 0 #define ExistSubWin() false #endif
此代码将解决调用问题。 这足以(包含少量的细节)令指标可编译。
第二大问题则与鼠标事件中涉及的函数有关,不是鼠标本身,而是 C_Mouse 类。
需要明白的是,当 Chart Trade 作为 EA 的一部分时,它是可以访问鼠标位置(以及其它内容)的;但如果我们只是将 C_Mouse 类添加到指标中,那么我们就会在指标和 EA 之间产生利益冲突。 我现在不会展示如何解决冲突。 我们将采取一种不同的方向,但只是暂时的,直到一切都得到解决,且直到我们至少获得原始 Chart Trade 的一些功能。
为此目的,我们需要将一些东西从 C_Mouse 类移到我们的新指标。 但别担心,这只是小事一桩。 第一处修改发生在 C_TemplateChart 类中 — 我们将在其 DispatchMessage 函数中进行以下修改:
void DispatchMessage(int id, long lparam, double dparam, string sparam) { datetime dt; double p; C_Chart_IDE::DispatchMessage(id, lparam, dparam, sparam); switch (id) { case CHARTEVENT_MOUSE_MOVE: #ifdef def_INTEGRATION_CHART_TRADER Mouse.GetPositionDP(dt, p); #else { int w; ChartXYToTimePrice(Terminal.Get_ID(), (int)lparam, (int)dparam, w, dt, p); } #endif for (int c0 = 0; c0 < m_Counter; c0++) if (m_Info[c0].szVLine != "") // ... Rest of the code...
通过添加高亮显示的部分,我们就已获得了功能,就好像 C_Mouse 类仍然存在一样。 下一处修改将在 C_ChartFloating 类中,我们制作了一些类似于我们之前的东西。 但是代码有点不同:
void DispatchMessage(int id, long lparam, double dparam, string sparam) { int mx, my; datetime dt; double p; static int six = -1, siy = -1, sic = -1; switch (id) { case CHARTEVENT_MOUSE_MOVE: #ifdef def_INTEGRATION_CHART_TRADER Mouse.GetPositionXY(mx, my); if ((Mouse.GetButtonStatus() & 0x01) == 1) #else mx = (int)lparam; my = (int)dparam; if (((uint)sparam & 0x01) == 1) #endif { if (sic == -1) for (int c0 = m_MaxCounter - 1; (sic < 0) && (c0 >= 0); c0--) // ... Rest of the code...
现在,我们可以编译指标,之后我们得到以下视频中显示的结果:
尽管 EA 尚未完全发挥作用,但我们需要做出以下决定:Chart Trade 既拥有与 EA 相同的功能,或可将它们减少,提供介于 MetaTrader 5 以前和当前之间所提供的功能。 鉴于我的立场非常激进,我将保留 Chart Trade 的功能,与其在 EA 中几乎相同。 如果您愿意,您可以减少它们。
做出这个决定后,我们就可以移到下一个主题,因为 Chart Trade 刚刚成为一个指标。
2.1. 如何让 Chart Trade 指标发挥作用
现在事情会变得更加困难。 因此,我们来看看如何让该指标发挥作用,以便我们可以发送订单、平仓、甚至报告业务结果。 事实上,这并不像乍一看那么困难,因为 MetaTrader 5 平台提供了一条可以遵循的途径,故此能以最小代价完成这一点。
在此,我们所作所为与第 16 部分中的相同,其中我们利用 MetaTrader 5 的一些功能创建了一个内部客户端-服务器系统(平台内部),以便在不同进程之间传输数据。 我们会在此做类似的事情,只是模拟会略有不同,因为我们需要双向通信,且它必须对用户保持不可见。
我们将要采用的方法并非唯一可能的方法。 还有一些其它方式,例如调用 DLL 来启用传输。 然而,我们将用 MetaTrader 5 的变量,因为它们是迄今为止最容易导航、维护和根据需要修改的变量。
现在我们已决定走这条路,还有第二个重要决定:谁将作为服务器?谁将作为客户端? 这一决定将影响系统的实际实现方式。 如何,我们已有了我们的消息传递协议,其实现如下所示:
#property copyright "Daniel Jose" //+------------------------------------------------------------------+ #define def_GlobalVariableLeverage (_Symbol + "_Leverage") #define def_GlobalVariableTake (_Symbol + "_Take") #define def_GlobalVariableStop (_Symbol + "_Stop") #define def_GlobalVariableResult (_Symbol + "_Result") #define def_GlobalVariableButton (_Symbol + "_ButtonState") //+------------------------------------------------------------------+ #define def_ButtonDTSelect 0x01 #define def_ButtonSWSelect 0x02 #define def_ButtonBuyMarket 0x04 #define def_ButtonSellMarket 0x08 #define def_ButtonClosePosition 0x10 //+------------------------------------------------------------------+
不过如此。 我们实际上不需要明确谁将成为客户端,谁将成为服务器,因为我们已经定义了消息协议。 协议必须从一开始就定义好,因为这将令其它一切的开发变得更加容易。
请注意,我们针对嵌有 Chart Trade 的 EA使用 5 个变量对应每种资产。 变量名称将取决于所链接资产设置,以便我们可以同时将设置用于多种资产。
这里遇到一个重要的问题:谁来做服务器的工作? 那么它就要负责创建此类变量。 就个人而言,我发现把 EA 当作服务器,并将 Chart Trade 当作客户端更实用。 这个思路在于始终将 EA 置于图表上,而 Chart Trade 则在需要的特定时间出现。 故此,EA 将负责创建 5 个变量中的 4 个,因为其中一个将负责通知按下了哪个按钮。 所以,这应由 Chart Trade 责任。
基于所有这些,数据流如下所示:
- EA 将创建全局变量,其中包含指示杠杆、止盈和止损的初始值。 最终,它还将创建通知当天结果的变量,以便 Chart Trade 能够向用户显示它,而无需在其它地方查看此信息。
- Chart Trade 将创建一个变量,代表按下的按钮值。 此变量将告诉 EA 该如何做,例如平仓,或执行市价买入或卖出。 此变量仅在此期间存在,并在 EA 完成请求后即刻不复存在。
流程很简单,但它保证 Chart Trade 可以与 EA 交互,包括系统发送订单和平仓的能力,就像以往所做的一样。
为了确保 Chart Trade 仅在 EA 存在时期显现在图表上,我们需要加入一些检查。 如果 EA 并不用来发送订单,则在图表上挂载 Chart Trade 毫无意义。 这些检查包括以下两个时刻:
- 第一个是在指标初始化时完成的
- 第二个是在图表上发生任何事件时完成的
我们在代码中可以看到这一点,如此您就更容易理解该过程。 在初始化期间,将执行以下代码:
#define def_SHORTNAME "CHART TRADE" //+------------------------------------------------------------------+ int OnInit() { long lparam = 0; double dparam = 0.0; string sparam = ""; IndicatorSetString(INDICATOR_SHORTNAME, def_SHORTNAME); if(!GlobalVariableGet(def_GlobalVariableLeverage, dparam)) return INIT_FAILED; Terminal.Init(); Chart.AddThese("IDE(,,170, 215)"); Chart.InitilizeChartTrade(dparam * Terminal.GetVolumeMinimal(), GlobalVariableGet(def_GlobalVariableTake), GlobalVariableGet(def_GlobalVariableStop), true); OnChartEvent(CHARTEVENT_OBJECT_ENDEDIT, lparam, dparam, sparam); return INIT_SUCCEEDED; }
高亮显示的行将评估其中一个全局变量,在本例中为指示杠杆水平的变量。 如果此变量缺失,初始化将失败。 请记住,EA 负责创建此变量,而非 Chart Trade,因此该指标自然知晓图表上是否存在 EA,当 EA 完成其工作时,它将从 MetaTrader 5 中删除此变量,也会强制删除图表交易。 这是在第二点完成的,其处我们检查相同的条件 — 全局杠杆变量是否可用。
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if (!GlobalVariableCheck(def_GlobalVariableLeverage)) OnDeinit(REASON_INITFAILED); Chart.DispatchMessage(id, lparam, dparam, sparam); }
上述指示的位置在实现时伴随相对高的频率。 尽管将 OnTime 事件系统放置在指标中很诱人,但这并不明智:所有指标都共享相同的操作线程,如果操作不小心,则放在一个指标中的 OnTime 事件会影响所有其它指标。
如果您认为上述检查会干扰平台的总体性能,您可将其放置在 OnTime 事件之中,但请注意后果。
无论谁触发从图表中删除 Chart Trade 指标,它都会在同一处发生,如下所示:
void OnDeinit(const int reason) { if (reason == REASON_INITFAILED) { Print("Unable to use Chart Trade. The EA is not on the chart of this asset..."); if (!ChartIndicatorDelete(0, 0, def_SHORTNAME)) Print("Unable to delete Chart Trade from the chart."); } }
所选行将从图表中删除指标。 若是失败的情况,则显示相关消息。 这些消息可以在工具箱中看到,如下所示:
按此方式,您必须始终留意此窗口中显现的任何信息。
在我们继续深入分析C_Chart_IDE 类中所做的更改之前,还缺少最后一个函数。 此函数如下:
int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) { double value; if (GlobalVariableGet(def_GlobalVariableResult, value)) { GlobalVariableDel(def_GlobalVariableResult); Chart.DispatchMessage(CHARTEVENT_CHART_CHANGE, 0, value, C_Chart_IDE::szMsgIDE[C_Chart_IDE::eROOF_DIARY]); } return rates_total; }
此函数的作用是观察全局变量。 因此,在平仓时,EA 会每次创建一个全局变量,其名称在 def_GlobalVariableResult 中定义。 一旦创建此变量,且其名称与 Chart Trade 所观测的名称匹配,则该变量的值将被捕获,然后该变量将被立即删除。 这样做是为了避免处理完成之前,EA 发送新的更新,如此会导致后一次更新错失的状况。 然而,由于已在删除之前捕获该值,我们就可将其发送到负责处理消息的 Chart Trade 类,如此这般,传递给 EA 的值就能尽快显示在 Chart Trade 中。
至此,我们完成了 Chart Trade 指标功能的初始部分。 第二部分涉及到按钮。 我们也需要令它们发挥作用。 这可在处理消息的 C_Chart_IDE 函数中轻松完成:
// ... Previous code ... case CHARTEVENT_OBJECT_CLICK: if (StringSubstr(sparam, 0, StringLen(def_HeaderMSG)) != def_HeaderMSG) { Resize(-1); return; } sparam = StringSubstr(sparam, 9, StringLen(sparam)); StringToUpper(sparam); #ifdef def_INTEGRATION_CHART_TRADER if ((sparam == szMsgIDE[eBTN_SELL]) || (sparam == szMsgIDE[eBTN_BUY])) TradeView.ExecuteOrderInMarket(m_BaseFinance.Leverange, m_BaseFinance.FinanceTake, m_BaseFinance.FinanceStop, sparam == szMsgIDE[eBTN_BUY], m_BaseFinance.IsDayTrade); if (sparam == szMsgIDE[eBTN_CANCEL]) { TradeView.CloseAllsPosition(); ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eBTN_CANCEL].szName, OBJPROP_STATE, false); } #else { union u00 { double Value; ulong c; }u_local; u_local.c = 0; if (sparam == szMsgIDE[eBTN_BUY]) u_local.c = (m_BaseFinance.IsDayTrade ? def_ButtonDTSelect : def_ButtonSWSelect) + def_ButtonBuyMarket; else if (sparam == szMsgIDE[eBTN_SELL]) u_local.c = (m_BaseFinance.IsDayTrade ? def_ButtonDTSelect : def_ButtonSWSelect) + def_ButtonSellMarket; else if (sparam == szMsgIDE[eBTN_CANCEL]) { u_local.c = def_ButtonClosePosition; ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eBTN_CANCEL].szName, OBJPROP_STATE, false); } if (u_local.Value > 0) GlobalVariableSet(def_GlobalVariableButton, u_local.Value); } #endif if (sparam == szMsgIDE[eCHECK_DAYTRADE]) InitilizeChartTrade(0, 0, 0, m_BaseFinance.IsDayTrade ? false : true); break; //... Rest of the code...
请注意,该片段包含两个代码。 当 Chart Trade 嵌入到 EA 中时,使用蓝色代码。 当 Chart Trade 作为指标存在时,使用绿色的。
我们对绿色代码感兴趣。 它将创建一个全局变量,并设置按钮状态。 因此,如果交易者平仓,则与平仓按钮对应的值会被赋值到变量之中。 但如果您提交一笔市价单,此值将有所不同,现在它可以是其它两个值的组合:一个指示订单是买入还是卖出;而另一个则指示您是要进行日内交易,亦或长线交易。 这就是 Chart Trade 将告诉 EA 的全部内容。
重要提示:这个系统是自我排斥的,即如果您点击卖出按钮,然后在 EA 做出任何响应之前又点击买入按钮,EA 实际上最终会买入,因为指示卖出的标志将被新的买入标志替换而丢失。 甚至,如果您在已有持仓的情况下请求卖出或买入,且若您在 EA 进行相关交易之前按取消,则该笔持仓将被平仓。
现在,我们可以转到 EA 代码,看看它在新模型中是如何工作的。
2.2. 修改 EA 代码接收来自 Chart Trade 的消息
这部分相当简单,在于我们所要做的就是调整一些小细节。 不过,请记住以下几点:加载 EA 和 Chart Trade 之后,不要更改 EA 中包含的全局变量或数据。 为此使用订单系统或 Chart Trade 本身,否则您可能会遇到问题。 有些问题有解决方案,有些则没有。 因此,以防万一,您最好采用已有的工具,不要试图令您的生活复杂化。
在上一篇文章(第 29 部分)中,在推动删除 Chart Trade 的同时,我们亦进行了一些修改,其中部分修改将被撤回。 我们不需要修改与此问题相关的任何其它内容。 但是,如早前所述,有些事情具备可修复性,而有些则不能;因此在本文的下一个主题中,我们将剔除 Chart Trade 和智能系统关系之间的一些小问题。
我们首先看一下我们必须在 EA 中撤回并激活的内容,如此即可在 EA 和 Chart Trade 之间存在一定程度的通信。
首先,我们修改以下内容:
input int user20 = 1; //Leverage input double user21 = 100; //Take Profit input double user22 = 81.74; //Stop Loss input bool EA_user23 = true; //Day Trade ?
指示 EA 是否优先开立多头或空头交易的标志保持不变。 这必须在 Chart Trade,或您放置在图表上的挂单中完成。 我已经在之前的文章中向您展示了如何执行此操作,那么我们继续编码工作。 我们在 OnInit 事件中添加以下修改:
int OnInit() { if (!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) { Sound.PlayAlert(C_Sounds::TRADE_ALLOWED); return INIT_FAILED; } Terminal.Init(); #ifdef def_INTEGRATION_TAPE_READING VolumeAtPrice.Init(user32, user33, user30, user31); TimesAndTrade.Init(user41); EventSetTimer(1); #endif Mouse.Init(user50, user51, user52); #ifdef def_INTEGRATION_CHART_TRADER static string memSzUser01 = ""; if (memSzUser01 != user01) { Chart.ClearTemplateChart(); Chart.AddThese(memSzUser01 = user01); } Chart.InitilizeChartTrade(EA_user20 * Terminal.GetVolumeMinimal(), EA_user21, EA_user22, EA_user23); TradeView.Initilize(); OnTrade(); #else GlobalVariableTemp(def_GlobalVariableLeverage); GlobalVariableTemp(def_GlobalVariableTake); GlobalVariableTemp(def_GlobalVariableStop); GlobalVariableTemp(def_GlobalVariableResult); GlobalVariableSet(def_GlobalVariableLeverage, user20 * Terminal.GetVolumeMinimal()); GlobalVariableSet(def_GlobalVariableTake, user21); GlobalVariableSet(def_GlobalVariableStop, user22); TradeView.Initilize(); GlobalVariableSet(def_GlobalVariableResult, TradeView.GetFinanceRoof()); #endif return INIT_SUCCEEDED; }
如您所见,我们在此处加入通信要用到的全局变量。 正如我们已提到的,EA 必须始终要在 Chart Trade 之前启动,否则我们就无法初始化指标。 请注意,Chart Trade 所采用初始值是在 EA 中指定。 即使之前有交易,初始化也将完成:累积值也会再次转移到 Chart Trade。
注意一个重要的细节:创建的变量是临时类型的,因为我们不希望在 EA 数据转储的情况下保存这些变量,因为它们在一段时间后就毫无用处。 甚至若发生某些事情,且平台关闭,这些变量也将不复存在。
另一个要实现的补充如下所示:
void OnDeinit(const int reason) { Mouse.Destroy(); TradeView.Finish(); #ifndef def_INTEGRATION_CHART_TRADER GlobalVariableDel(def_GlobalVariableLeverage); GlobalVariableDel(def_GlobalVariableTake); GlobalVariableDel(def_GlobalVariableStop); GlobalVariableDel(def_GlobalVariableResult); GlobalVariableDel(def_GlobalVariableButton); #endif #ifdef def_INTEGRATION_TAPE_READING EventKillTimer(); #endif }
即使变量是临时的,我们仍然要求 EA 强制删除它们。 这将确保 Chart Trade 不再保留在图表上。 此 OnDeinit 事件存在一个小问题,但我们将在下一个主题中再去全方位解决它。 现在,我们看看另一个相当有趣的观点。 这可以通过两种不同的方式来完成,而结果几乎雷同。 我说几乎雷同,因为还会有一些微小的差异可能会有所区别。 (对不起,双关语)
Chart Trade 有一些按钮,它们经由全局变量向 EA 发送消息。 以下是 EA 内部的函数,可充分响应这些点击:
inline void ChartTrade_ClickButton(void) { union u00 { double Value; ulong c; }u_local; if (GlobalVariableGet(def_GlobalVariableButton, u_local.Value)) { GlobalVariableDel(def_GlobalVariableButton); if (u_local.c == def_ButtonClosePosition) TradeView.CloseAllsPosition(); else TradeView.ExecuteOrderInMarket(GlobalVariableGet(def_GlobalVariableLeverage), GlobalVariableGet(def_GlobalVariableTake), GlobalVariableGet(def_GlobalVariableStop), ((u_local.c & def_ButtonBuyMarket) == def_ButtonBuyMarket), ((u_local.c & def_ButtonDTSelect) == def_ButtonDTSelect)); TradeView.Initilize(); } }
该函数被声明为内联,即编译器必须将其放置在声明它的位置。 这可令其运行尽可能地迅捷。
一个非常重要的细节:我们把这个函数放在哪里? 我们来思考一下这个问题。 我们在这里有一个检查,所以它不会一直运行。 所以,我们有两种可能性。 第一个是将函数放在 OnTick 事件当中,第二个 — 放在 OnTime 事件当中。 选择必须基于一定的逻辑,否则可能会出现问题。
那么,我们来思考:如果我们将此函数放在 OnTick 事件当中,那么从交易服务器进入平台的每次跳价时它都会执行。 这似乎是一个很好的解决方案,因为该函数不会多次执行,而只会在特定时间执行。 但这会产生一个问题:如果我们所交易资产的波动性非常低,则 OnTick 事件的触发频率也会非常低,而这意味着我们可能会在点击 Chart Trade 时遇到事件触发的问题。
此处是第二个选项。 将函数置于 OnTime,能确保函数会被执行,因为 OnTime 事件能按设定的频率触发。 但当我们这样做时,我们必须记住,除了观察全局变量之外,我们不应再将 OnTime 事件用于处理其它任何事务。
在这个阶段,我们做出了一个非常好的选择,在于 EA 只会关注市场。 在接下来的几篇文章中,您将看到这一点会变得越来越明显。 然后,最好在 OnTime 事件中放置一个函数。 但现在的问题是:OnTime 事件每秒只触发一次,不是吗?
实际上,大多数情况下它每秒触发一次,但我们可以调用 EventSetMillisecondTimer 函数设置更短的时间片。 因此,我们可以在不到 1 秒的时间内触发事件。 我相信在大多数情况下,500 毫秒就足够了,因此我们将在 EA 的 OnInit 事件中有如下行:
#else GlobalVariableTemp(def_GlobalVariableLeverage); GlobalVariableTemp(def_GlobalVariableTake); GlobalVariableTemp(def_GlobalVariableStop); GlobalVariableTemp(def_GlobalVariableResult); GlobalVariableSet(def_GlobalVariableLeverage, user20 * Terminal.GetVolumeMinimal()); GlobalVariableSet(def_GlobalVariableTake, user21); GlobalVariableSet(def_GlobalVariableStop, user22); TradeView.Initilize(); GlobalVariableSet(def_GlobalVariableResult, TradeView.GetFinanceRoof()); EventSetMillisecondTimer(500); #endif
不要忘记在 OnDeinit 函数中关闭此事件,为此我们只需在事件中添加以下行:
void OnDeinit(const int reason) { EventKillTimer();
现在,我们看看 OnTime 事件是什么样。 这是一段超级简单的代码,只有高亮显示的行才会实际编译。
void OnTimer() { #ifndef def_INTEGRATION_CHART_TRADER ChartTrade_ClickButton(); #endif #ifdef def_INTEGRATION_TAPE_READING VolumeAtPrice.Update(); TimesAndTrade.Connect(); #endif }
全部就这些吗? 不,还有其它一个小问题。 我们还记得,我们已经修改了 EA 的初始化变量,并希望每次在图表上放置新的挂单时,能从 Chart Trade 中提取出这些数据。 为此目的,我们需要在 C_IndicatorTradeView 类代码中添加一个小细节。 它如下所示:
case CHARTEVENT_MOUSE_MOVE: Mouse.GetPositionDP(dt, price); mKeys = Mouse.GetButtonStatus(); bEClick = (mKeys & 0x01) == 0x01; //Left mouse button click bKeyBuy = (mKeys & 0x04) == 0x04; //SHIFT pressed bKeySell = (mKeys & 0x08) == 0x08; //CTRL pressed if (bKeyBuy != bKeySell) { if (!bMounting) { #ifdef def_INTEGRATION_CHART_TRADER m_Selection.bIsDayTrade = Chart.GetBaseFinance(m_Selection.vol, valueTp, valueSl); #else m_Selection.vol = GlobalVariableGet(def_GlobalVariableLeverage) * Terminal.GetVolumeMinimal(); valueTp = GlobalVariableGet(def_GlobalVariableTake); valueSl = GlobalVariableGet(def_GlobalVariableStop); m_Selection.bIsDayTrade = EA_user23; #endif
高亮显示的代码现在捕获全局变量中的值,如此这般,Chart Trade 中的任何内容都能放入订单系统之中。 唯一的细节是所有挂单都将遵循 EA 指定的时间,但这可以直接在图表上更改。 有关更多详细信息,请参阅从头开始开发智能交易系统(第 27 部分),其中我展示了如何直接在图表上修改挂单,而无需经由 Chart Trade。
语音系统也有小改动。 这些语音已加入 C_Router 类之中,确保若经由 Chart Trade 操作,我们能收到有关开仓和平仓的通知。 如果您要删除或更改语音系统中的某些内容,则应记住还有其它要点需要考虑。
在我们完工之前,我们记住 OnDeinit 事件还有一个我们要修复的问题。 那么,我们就转到下一个主题,并解决此问题。
2.3. 防止 Chart Trade 过早退出图表
当我们做任何事情时,比如改变图表时间帧,或 EA 参数(可以是任何东西),MetaTrader 5 都会生成一个 OnDeinit 事件。 触发此事件,意味着需要重新分析事物,从而确保一切能继续正常工作。
在大多数情况下,触发此事件不会导致任何问题。 但由于我们创建了一个客户端-服务器系统,并用全局变量来查询服务器(EA)是否已停止运行,因此我们必须明白如何解决某些情况。
处理原始 OnDeinit 事件的函数如下所示:
void OnDeinit(const int reason) { EventKillTimer(); Mouse.Destroy(); TradeView.Finish(); #ifndef def_INTEGRATION_CHART_TRADER GlobalVariableDel(def_GlobalVariableLeverage); GlobalVariableDel(def_GlobalVariableTake); GlobalVariableDel(def_GlobalVariableStop); GlobalVariableDel(def_GlobalVariableResult); GlobalVariableDel(def_GlobalVariableButton); #endif #ifdef def_INTEGRATION_TAPE_READING EventKillTimer(); #endif }
高亮显示的行将删除全局变量,这正是我们在 Chart Trade 指标中用来检查 EA 是否在图表上的变量。 此检查在以下代码中执行:
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if (!GlobalVariableCheck(def_GlobalVariableLeverage)) OnDeinit(REASON_INITFAILED); Chart.DispatchMessage(id, lparam, dparam, sparam); }
也就是说,每当 EA 中发生某些事情,并激活 OnDeinit 事件时,此变量都将被删除。 在 EA 重新创建变量之前,指标也许已经从图表中删除,这可真的很奇怪。 在某些时候它会发生,而在其它时刻则不会。 故此,我们必须以某种方式确保一切按预期工作。
我在文档中找到了解决此问题的方法。 这可以在逆初始化原因的代码章节看到。 查看这些代码,我们可以配置 OnDeinit 事件处理函数,如此即可从图表中删除 EA 之前不会删除变量。
那么,解决方案和新的处理代码将如下所示:
void OnDeinit(const int reason) { EventKillTimer(); Mouse.Destroy(); TradeView.Finish(); #ifndef def_INTEGRATION_CHART_TRADER switch (reason) { case REASON_CHARTCHANGE: break; default: GlobalVariableDel(def_GlobalVariableLeverage); GlobalVariableDel(def_GlobalVariableTake); GlobalVariableDel(def_GlobalVariableStop); GlobalVariableDel(def_GlobalVariableResult); GlobalVariableDel(def_GlobalVariableButton); }; #endif #ifdef def_INTEGRATION_TAPE_READING EventKillTimer(); #endif }
现在,如果我们只更改图表时间帧或绘图方式,我们就不会遇到有关 Chart Trade 指标从图表中消失的不便。 在任何其它情形下,它也许会被从图表中删除,因为我们无法真正保证一切都会顺利进行。 有因于此,一旦加载了 EA 和 Chart Trade,修改 EA 参数将不再有意义。
结束语
看看点滴的创造力能做什么? 有时我们必须解决看似无法解决的问题。 但是通过研究文档,我们可以发现解决方案,因此必须始终检查文档,并理解它,如此就能够将想法付诸实践。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10653


