English Русский Español Deutsch 日本語 Português
preview
从头开始开发智能交易系统(第 30 部分):CHART TRADE 当作指标?

从头开始开发智能交易系统(第 30 部分):CHART TRADE 当作指标?

MetaTrader 5示例 | 10 一月 2023, 10:02
827 0
Daniel Jose
Daniel Jose

概述

在上一篇文章从头开始开发智能交易系统(第 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 责任。

基于所有这些,数据流如下所示:

  1. EA 将创建全局变量,其中包含指示杠杆、止盈和止损的初始值。 最终,它还将创建通知当天结果的变量,以便 Chart Trade 能够向用户显示它,而无需在其它地方查看此信息。
  2. 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

附加的文件 |
EA_-_p_Parte_30_u.zip (14532.23 KB)
从头开始开发智能交易系统(第 31 部分):面向未来((IV) 从头开始开发智能交易系统(第 31 部分):面向未来((IV)
我们继续从 EA 中删除单独的部件。 这是本系列中的最后一篇文章。 并且最后要移除的是声音系统。 如果您之前没有关注过这些文章系列,可能会有点困惑。
从头开始开发智能交易系统(第 29 部分):谈话平台 从头开始开发智能交易系统(第 29 部分):谈话平台
在本文中,我们将学习如何让 MetaTrader 5 平台谈话。 我们如何才能让 EA 更有趣呢? 金融市场交易往往过于无聊和单调,但我们能够令这项工作少些无趣。 请注意,对于那些经历过上瘾等问题的人来说,这个项目可能是危险的。 然而,在一般情况下,它只会让事情聊胜于无。
学习如何基于相对活力(Vigor)指数设计交易系统 学习如何基于相对活力(Vigor)指数设计交易系统
我们系列中的新篇章,介绍如何基于最流行的技术指标设计交易系统。 在本文中,我们将学习如何基于相对活力(Vigor)指数指标来做到这一点。
DoEasy. 控件 (第 17 部分): 裁剪对象不可见部分、辅助箭头按钮 WinForms 对象 DoEasy. 控件 (第 17 部分): 裁剪对象不可见部分、辅助箭头按钮 WinForms 对象
在本文中,我将创建一种功能,可隐藏超出其容器之外的对象部分。 此外,我亦将创建辅助箭头按钮对象,作为其它 WinForms 对象的一部分。