English Русский Español Deutsch 日本語 Português
preview
开发回放系统 — 市场模拟(第 09 部分):自定义事件

开发回放系统 — 市场模拟(第 09 部分):自定义事件

MetaTrader 5示例 | 21 十一月 2023, 17:25
715 0
Daniel Jose
Daniel Jose

概述

在上一篇文章 开发回放系统 — 市场模拟(第 08 部分):锁定指标 中,我向您展示了如何锁定控制指标。 尽管我们已经成功地达成了该目标,但仍有一些方面需要定位。 如果您仔细观察了,您大概会注意到,每次我们更改回放/模拟的起点时,我们都会闪现正在构建的交易柱线。 在某种程度上,这并不是真正的问题,甚至也许对某些人来说很有趣,而对其他人来说则不那么有趣。 现在我们将尝试取悦希腊人和特洛伊人(译者按:指性格对立的两类人)。 我们看看如何实现回放/模拟器服务,令其为您完美操作。 换言之,您将能够看到正在构造的柱线,或看不到。


取悦希腊人和特洛伊人

第一步是向服务文件添加新的变量或参数:

input string            user00 = "Config.txt";  //Replay configuration file
input ENUM_TIMEFRAMES   user01 = PERIOD_M5;     //Starting time
input bool              user02 = true;          //Visualize construction of bars


我们允许用户做出决定的过程,从此处开始。 正如我们之前所说,有些人喜欢观看柱线的构建,而另一些人则不在乎。

这一步完成后,我们会在下一步中将这个参数传递给 C_Replay 类:

while ((ChartSymbol(id) != "") && (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value)) && (!_StopFlag))
{
        if (!Info.s_Infos.isPlay)
        {
                if (!bTest) bTest = true;
        }else
        {
                if (bTest)
                {
                        delay = ((delay = Replay.AdjustPositionReplay(user02)) >= 0 ? 3 : delay);
                        bTest = false;
                        t1 = GetTickCount64();
                }else if ((GetTickCount64() - t1) >= (uint)(delay))
                {
                        if ((delay = Replay.Event_OnTime()) < 0) break;
                        t1 = GetTickCount64();
                }
        }
}


现在我们可以处置 C_Replay 类,并于其上开始工作。 虽然这项任务表面上看起来很简单,但它意味着险阻和挑战。 到目前为止,市场回放数据基于交易的跳价,图表绘制采用 1 分钟柱线。 因此,这不仅仅是添加或删除柱线。 我们必须遵照统一的形式对待各种元素。 这并非一件容易的任务,不是吗? 不过,我喜欢解决问题,这个问题似乎很有趣。

第一步是在我们读取交易跳价文件的同时创建一分钟柱线,但还需要考虑另一个方面。 我们应该非常小心。 以下是我们将如何迎接这一挑战。 从一开始,我们将在系统中引入一组新的变量。

struct st00
{
        MqlTick  Info[];
        MqlRates Rate[];
        int      nTicks,
                 nRate;
}m_Ticks;


这个结构将包含 1 分钟柱线,我们将在读取跳价文件的同时绘制这些柱线。 查看到目前为止的代码,我们会注意到 C_Replay 类中存在的 Event_OnTime 函数,它能基于交易跳价的数值绘制一分钟柱线。 不过,我们无法调用此函数来为我们执行此任务。 事实上,我们可以谨慎地做到这一点:在过程结束时,我们可以删除在回放服务中创建的所有柱线。 以这种方式,系统就能待用了。 然而,Event_OnTime 的操作结果在每次调用时都略有延迟,而与交易跳价关联的调用次数通常相对较大。 我们将不得不采取稍微不同的方式。

正如我们曾提到的,我们被迫寻找一种稍微不同的方式。 如此,我们得到以下函数:

inline bool BuiderBar1Min(MqlRates &rate, const MqlTick &tick)
                {
                        if (rate.time != macroRemoveSec(tick.time))
                        {
                                rate.real_volume = (long) tick.volume_real;
                                rate.tick_volume = 0;
                                rate.time = macroRemoveSec(tick.time);
                                rate.open = rate.low = rate.high = rate.close = tick.last;
                
                                return true;
                        }
                        rate.close = tick.last;
                        rate.high = (rate.close > rate.high ? rate.close : rate.high);
                        rate.low = (rate.close < rate.low ? rate.close : rate.low);
                        rate.real_volume += (long) tick.volume_real;
        
                        return false;
                }


我们在这里所做的,本质上与 Event_OnTime 应做的事情相同。 不过,我们将逐个跳价执行此操作。 此处是对正在发生的事情的简要解释:当跳价指示的时间与柱线记录的时间不同时,我们就会有一个初始柱线结构。 我们将返回 “true”,通知调用者需要创建一根新柱线,并允许它进行任何必要的更改。 在随后调用时,我们会相应地调整这些数值。 在本例中,我们将返回 “false”,指示无需创建新柱线。 该函数本身非常简单,但调用时您需要格外小心。

首先,确保正确地初始化数组。 我们来看看这是在哪里完成的。

bool SetSymbolReplay(const string szFileConfig)
{
        int     file;
        string  szInfo;
        bool    isBars = true;
                                
        if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
        {
                MessageBox("Failed to load the\nconfiguration file.", "Market Replay", MB_OK);
                return false;
        }
        Print("Loading data for replay. Please wait....");
        ArrayResize(m_Ticks.Rate, 540);
        m_Ticks.nRate = -1;
        m_Ticks.Rate[0].time = 0;
        while ((!FileIsEnding(file)) && (!_StopFlag))
        {
                szInfo = FileReadString(file);
                StringToUpper(szInfo);
                if (szInfo == def_STR_FilesBar) isBars = true; else
                if (szInfo == def_STR_FilesTicks) isBars = false; else
                if (szInfo != "") if (!(isBars ? LoadPrevBars(szInfo) : LoadTicksReplay(szInfo)))
                {
                        if (!_StopFlag)
                                MessageBox(StringFormat("File %s from%s\ncould not be loaded.", szInfo, (isBars ? def_STR_FilesBar : def_STR_FilesTicks), "Market Replay", MB_OK));
                        FileClose(file);
                        return false;
                }
        }
        FileClose(file);
        return (!_StopFlag);
}


如果没有提前正确完成此操作,您将无法正确调用柱线创建函数。 那么下一个问题浮现:为什么我们要在第一个数组的索引处指定 -1 值? 起始值不是应该从 0 开始吗? 对的,它是 0,但我们在第一次调用时从 -1 开始,这始终是真的。 如果它从 0 开始,我们就不得不在调用构建柱线后立即运行额外的测试。 不过,当设置为 -1 时,这个额外检查就变得没必要。 注意的重点是,我们用 540 个位置初始化数组,这对应于巴西证券交易所(B3)典型交易日内正常所含的 1 分钟柱线数量。

一旦此步骤完成后,我们可以继续读取交易跳价。

bool LoadTicksReplay(const string szFileNameCSV)
{
        int     file,
                old;
        string  szInfo = "";
        MqlTick tick;
        MqlRates rate;
                                
        if ((file = FileOpen("Market Replay\\Ticks\\" + szFileNameCSV + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE)
        {
                ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
                ArrayResize(m_Ticks.Rate, 540, 540);
                old = m_Ticks.nTicks;
                for (int c0 = 0; c0 < 7; c0++) szInfo += FileReadString(file);
                if (szInfo != def_Header_Ticks)
                {
                        Print("File ", szFileNameCSV, ".csv is not a file a traded ticks.");
                        return false;
                }
                Print("Loading replay ticks. Please wait...");
                while ((!FileIsEnding(file)) && (m_Ticks.nTicks < (INT_MAX - 2)) && (!_StopFlag))
                {
                        ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
                        szInfo = FileReadString(file) + " " + FileReadString(file);
                        tick.time = macroRemoveSec(StringToTime(StringSubstr(szInfo, 0, 19)));
                        tick.time_msc = (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                        tick.bid = StringToDouble(FileReadString(file));
                        tick.ask = StringToDouble(FileReadString(file));
                        tick.last = StringToDouble(FileReadString(file));
                        tick.volume_real = StringToDouble(FileReadString(file));
                        tick.flags = (uchar)StringToInteger(FileReadString(file));
                        if ((m_Ticks.Info[old].last == tick.last) && (m_Ticks.Info[old].time == tick.time) && (m_Ticks.Info[old].time_msc == tick.time_msc))
                                m_Ticks.Info[old].volume_real += tick.volume_real;
                        else
                        {                                                       
                                m_Ticks.Info[m_Ticks.nTicks] = tick;
                                if (tick.volume_real > 0.0)
                                {
                                        m_Ticks.nRate += (BuiderBar1Min(rate, tick) ? 1 : 0);
                                        rate.spread = m_Ticks.nTicks;
                                        m_Ticks.Rate[m_Ticks.nRate] = rate;
                                        m_Ticks.nTicks++;
                                }
                                old = (m_Ticks.nTicks > 0 ? m_Ticks.nTicks - 1 : old);
                        }
                }
                if ((!FileIsEnding(file)) && (!_StopFlag))
                {
                        Print("Too many data in the tick file.\nCannot continue...");
                        return false;
                }
        }else
        {
                Print("Tick file ", szFileNameCSV,".csv not found...");
                return false;
        }
        return (!_StopFlag);
};


这里有一个重要的细节:如果分钟柱线的数量大于此处指定的数量,则需要调整初始值和保留值。 这个数值适用于 9:00 至 18:00 的交易时段,对应 540 分钟,但如果这个时段太长,则您需要提前增加。 不过,注意的重点是,要考虑的时间应该是交易时段的开盘和收盘时间。 这是指交易的跳价文件,而不是柱线文件。 这是因为柱线是基于跳价文件生成的,如果该时段在特定文件中不同,则在执行期间(运行时)可能会出现问题。 我们之所以用到这个值,是因为 B3 交易所的交易时段正常为 540 分钟。

现在我们可以继续展示交易跳价文件。 以这种方式,我们将一次捕获一个跳价,并构建 1-分钟柱线。 不过,注意以下重点:只当有一些交易量时才会生成柱线;否则,跳价只代表对资产的出价(BID)或要价(ASK)进行一些调整,因此不予考虑。 注意:我们将在不久的将来处理这种情况,因为我们打算让系统能适配外汇市场。 但现在,我们跳过它。

由于我们在回放/模拟中没有用到点差值,因此它将用于更有意义的用途。 但请注意,这不是点差值。 因此,如果某些指标需要正确的点差值,那么您就需要使用不同的方式。 存储点差的这个变量可用于存储计数器的位置值。 在不久的将来,这将被证明是非常有用的。

现在一切都正确设置了,我们可以保存 1 分钟的柱线数据,并继续下一步:这是由于读取系统无需其它更改。 因此,没有必要对于读取顺序作进一步评论。

我们现在看一下主函数。

int AdjustPositionReplay(const bool bViewBuider)
{
        u_Interprocess Info;
        MqlRates       Rate[1];
        int            iPos = (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks);
        datetime       dt_Local;
                                
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        if (Info.s_Infos.iPosShift == iPos) return 0;
        iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / def_MaxPosSlider));
        if (iPos < m_ReplayCount)
        {
                dt_Local = m_dtPrevLoading;
                m_ReplayCount = 0;
                if (!bViewBuider) for (int c0 = 1; (c0 < m_Ticks.nRate) && (m_Ticks.Rate[c0 - 1].spread < iPos); c0++)
                {
                        dt_Local = m_Ticks.Rate[c0].time;
                        m_ReplayCount = m_Ticks.Rate[c0 - 1].spread;
                }
                CustomRatesDelete(def_SymbolReplay, dt_Local, LONG_MAX);
                if (m_dtPrevLoading == 0)
                {
                        Rate[0].close = Rate[0].open = Rate[0].high = Rate[0].low = m_Ticks.Info[m_ReplayCount].last;
                        Rate[0].tick_volume = 0;
                        Rate[0].time = m_Ticks.Info[m_ReplayCount].time - 60;
                        CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                }
        }
        for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag); m_ReplayCount++) Event_OnTime();
        return Event_OnTime();
}

此函数尚未彻底完工,将继续更改。 然而,为了避免在将来的解释中混淆,我将概括本文开发过程中添加或删除的内容。 以这种方式,读者就能更好地了解正在发生的事情,如果您想更改,也会更容易。 您只需要回到这些文章,并评审所讨论的每个要点实际的走向。 请记住,之前文章中已经涵盖的所有内容,此篇均未再提及。

第一步是声明一个局部变量,用来设置函数内部的时间位置。 依此设置,如果我们继续前进,然后决定后退一点,我们就不必从最开头开始回放。 我们很快就会碰到这一点。 在进行一些计算,从而判定当前位置是向前移动还是向后移动之后,我们寻找要采取的第一个行动。 如果位置需要后退,这两行代码会在动作开始时初始化回放/模拟。 不过,这也许并非必要。 如果您或用户表示并不想在创建柱线时观察它们的形成,我们就进入一个短循环,来检查读取交易跳价时记录的所有 1 分钟柱线的内容。 

现在仍有一个问题此刻似乎不是很清楚。 当将交易跳价转换为 1 分钟柱线时,我们得到计数器的相对位置,同时获得新柱线的开盘时间。 此信息是有用且必要的,可令我们清除指定时间之后出现的所有柱线。 计数器值不太可能与用户请求的新相对定位值相同。 因此,系统会进行小幅调整,从而匹配位置,但这种调整非常快。 因此,柱线的创建几乎是不可见的。

不过,如前所述,此函数尚未完工。 仅当用户从当前计数器位置回退时,才会用到所讲述的操作。 如果用户从计数器位置前进,我们仍旧会有柱线创建效果。 由于我们想取悦所有人,包括希腊人和特洛伊人,我们必须纠正这个小故障,如此就不会提前看到柱的创建。 它不是很复杂。 我们将上面没有超前的系统代码,与包含超前的以下代码进行比较:

int AdjustPositionReplay(const bool bViewBuider)
{
#define macroSearchPosition     {                                                                                               \
                dt_Local = m_dtPrevLoading; m_ReplayCount = count = 0;                                                          \
                if (!bViewBuider) for (count = 1; (count < m_Ticks.nRate) && (m_Ticks.Rate[count - 1].spread < iPos); count++)  \
                        { dt_Local = m_Ticks.Rate[count].time;  m_ReplayCount = m_Ticks.Rate[count - 1].spread; }               \
                                }

        u_Interprocess  Info;
        MqlRates        Rate[def_BarsDiary];
        int             iPos = (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks),
                        count;
        datetime        dt_Local;
                                
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        if (Info.s_Infos.iPosShift == iPos) return 0;
        iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1)));
        if (iPos < m_ReplayCount)
        {
                macroSearchPosition;
                CustomRatesDelete(def_SymbolReplay, dt_Local, LONG_MAX);
                if (m_dtPrevLoading == 0)
                {
                        Rate[0].close = Rate[0].open = Rate[0].high = Rate[0].low = m_Ticks.Info[m_ReplayCount].last;
                        Rate[0].tick_volume = 0;
                        Rate[0].time = m_Ticks.Info[m_ReplayCount].time - 60;
                        CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                }
        }if ((iPos > m_ReplayCount) && (!bViewBuider))
        {
                macroSearchPosition;                    
                CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, count);
        }
        for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag); m_ReplayCount++) Event_OnTime();
        return Event_OnTime();
}


现在,您看出区别了吗? 如果您正在考虑宏,请忘记它,因为它的作用只是为了让我们不必在两个不同的地方重复相同的代码。 事实上,没有实质区别。 也许唯一不同的是该行会加入额外的柱线。 如果您应用回放系统,您会注意到超前和回退的位置点不太可能与一根柱线的收盘价和下一根柱线的开盘点重合。 这是因为这行总会有相对应的余数。 不过,由于此设置的速度,您不太可能注意到这种细化。


向用户发出警报

我们的回放系统现处的时刻,我们应当开始加入一些以前不必要的附属物。 这些附属物之一是当系统中没有更多数据来模拟或继续回放时通知用户。 如果没有此警告,用户可能会认为系统只是崩溃了,或发生了一些异常状况。 为了防止这种推测,我们从添加一些额外的信息开始。 第一步是没有更多数据可供使用的警告。 为了搞清如何做到这一点,我们看看以下代码:

void OnStart()
{
        ulong t1;
        int delay = 3;
        long id = 0;
        u_Interprocess Info;
        bool bTest = false;
        
        Replay.InitSymbolReplay();
        if (!Replay.SetSymbolReplay(user00))
        {
                Finish();
                return;
        }
        Print("Wait for permission from [Market Replay] indicator to start replay ...");
        id = Replay.ViewReplay(user01);
        while ((!GlobalVariableCheck(def_GlobalVariableReplay)) && (!_StopFlag) && (ChartSymbol(id) != "")) Sleep(750);
        if ((_StopFlag) || (ChartSymbol(id) == ""))
        {
                Finish();
                return;
        }
        Print("Permission granted. The replay service can now be used...");
        t1 = GetTickCount64();
        while ((ChartSymbol(id) != "") && (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value)) && (!_StopFlag))
        {
                if (!Info.s_Infos.isPlay)
                {
                        if (!bTest) bTest = true;
                }else
                {
                        if (bTest)
                        {
                                if ((delay = Replay.AdjustPositionReplay(user02)) < 0) AlertToUser(); else
                                {
                                        delay = (delay >= 0 ? 3 : delay);
                                        bTest = false;
                                        t1 = GetTickCount64();
                                }                               
                        }else if ((GetTickCount64() - t1) >= (uint)(delay))
                        {
                                if ((delay = Replay.Event_OnTime()) < 0) AlertToUser();
                                t1 = GetTickCount64();
                        }
                }
        }
        Finish();
}
//+------------------------------------------------------------------+
void AlertToUser(void)
{
        u_Interprocess Info;
        
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        Info.s_Infos.isPlay = false;
        GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
        MessageBox("No more data to use in replay-simulation", "Service Replay", MB_OK);
}
//+------------------------------------------------------------------+
void Finish(void)
{
        Replay.CloseReplay();
        Print("The replay service completed...");
}
//+------------------------------------------------------------------+


在两种情形下,您可以创建此类警告。 第一种发生在正常的回放执行期间,这是最常见的情形。 但是,还有另一种选项:当用户将位置调整到滚动条的末尾时。

int AdjustPositionReplay(const bool bViewBuider)
{

// ... Code ...

        iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1)));

// ...  Rest of the code ...


无论如何,答案始终是一样的。 我们获取全局终端变量中包含的值,并用它来指示我们处于暂停模式。 然后我们再次记录它,并显示一个窗口,报告发生的事情。 基本上这就是我们要做的,但它非常实用。 以这种方式,可怜的用户就会发现发生了什么。


添加 “请稍候” 警告

现在,我们的回放系统已能让用户指明他是否想看到柱线的构建过程,如果他真的想监控柱线的构建过程,就会有一个小问题。 这就是本话题的由来。

当我们想在等待回放服务到达正确位置时观看柱线构建,我们得到的印象是进度可以随时停止或开始。 这是因为我们有播放和暂停按钮。 不过,在回放服务到达正确位置并释放系统之前,我们实际上无法执行任何操作。 正是在这些情形下,我们可能会有点困惑,因为我们不太确定到底发生了什么。 但如果您将这个呈现的按钮替换为另一个指示需要等待的按钮,情况就会发生变化。 对否?

然而,仅仅添加一个按钮还不够。 我们需要执行一些额外的步骤,能够让服务告诉控制指标应该显示什么,不应该显示什么。 首先,我们往 InterProcess.mqh 头文件里添加一个新变量。

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#define def_GlobalVariableReplay        "Replay Infos"
#define def_GlobalVariableIdGraphics    "Replay ID"
#define def_SymbolReplay                "RePlay"
#define def_MaxPosSlider                400
#define def_ShortName                   "Market Replay"
//+------------------------------------------------------------------+
union u_Interprocess
{
        union u_0
        {
                double  df_Value;       // The value of the terminal's global variable...
                ulong   IdGraphic;      // Contains the asset chart ID....
        }u_Value;
        struct st_0
        {
                bool    isPlay;         // Specifies if we are in the play or pause mode ...
                bool    isWait;         // Asks the user to wait...
                ushort  iPosShift;      // A value between 0 and 400 ...
        }s_Infos;
};
//+------------------------------------------------------------------+


该数值将在服务和指标之间传输,且优先级高于其它控件。 因此,如果需要显示,控制指标就无法执行任何其它操作。 我们已定义好变量,现在我们需要进入回放服务,并添加与控制指标进行通信的必要代码。 为此,我们需要往 C_Replay 类里添加一些代码。 这不是很困难。

int AdjustPositionReplay(const bool bViewBuider)
{
#define macroSearchPosition     {                                                                                               \
                dt_Local = m_dtPrevLoading; m_ReplayCount = count = 0;                                                          \
                if (!bViewBuider) for (count = 1; (count < m_Ticks.nRate) && (m_Ticks.Rate[count - 1].spread < iPos); count++)  \
                        { dt_Local = m_Ticks.Rate[count].time;  m_ReplayCount = m_Ticks.Rate[count - 1].spread; }               \
                                }

        u_Interprocess  Info;
        MqlRates        Rate[def_BarsDiary];
        int             iPos = (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks),
                        count;
        datetime        dt_Local;
                                
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        if (Info.s_Infos.iPosShift == iPos) return 0;
        iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1)));
        if (iPos < m_ReplayCount)
        {
                macroSearchPosition;
                CustomRatesDelete(def_SymbolReplay, dt_Local, LONG_MAX);
                if (m_dtPrevLoading == 0)
                {
                        Rate[0].close = Rate[0].open = Rate[0].high = Rate[0].low = m_Ticks.Info[m_ReplayCount].last;
                        Rate[0].tick_volume = 0;
                        Rate[0].time = m_Ticks.Info[m_ReplayCount].time - 60;
                        CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                }
        }if ((iPos > m_ReplayCount) && (!bViewBuider))
        {
                macroSearchPosition;                    
                CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, count);
        }
        if (bViewBuider)
        {
                Info.s_Infos.isWait = true;
                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
        }
        for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag); m_ReplayCount++) Event_OnTime();
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        Info.s_Infos.isWait = false;
        GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
        return Event_OnTime();
}


通常不会达到这个点,只有在真正需要做某事的那一刻才会发生。 如果用户想要可视化图表上显示的柱线,我们将发送一个信号,如此指标就会显示该服务将在一段时间内不可用。 我们将其记录在全局终端变量当中,如此指标就可以解读该数值。 然后,该服务将执行它原本该做的任务。 此后不久,我们无条件地彻底释放指标。

在此之后,我们能移到控制指标代码,分析正在发生的事情。 有些人也许认为需要大量的代码才能让事情于此工作。 不过,正如您所看到的,我将用最少的代码完成所有工作。 为了将事情简化,抽象一点怎么样? 为此,我们首先将以下行添加到 C_Control.mqh 头文件之中。

enum EventCustom {Ev_WAIT_ON, Ev_WAIT_OFF};


实际上,我们正在添加一个额外的抽象层来简化接下来我们要做的事情。 不要忘记我们将用到的图像,它是在以下代码片段中添加的:

#define def_ButtonPlay  "Images\\Market Replay\\Play.bmp"
#define def_ButtonPause "Images\\Market Replay\\Pause.bmp"
#define def_ButtonLeft  "Images\\Market Replay\\Left.bmp"
#define def_ButtonRight "Images\\Market Replay\\Right.bmp"
#define def_ButtonPin   "Images\\Market Replay\\Pin.bmp"
#define def_ButtonWait  "Images\\Market Replay\\Wait.bmp"
#resource "\\" + def_ButtonPlay
#resource "\\" + def_ButtonPause
#resource "\\" + def_ButtonLeft
#resource "\\" + def_ButtonRight
#resource "\\" + def_ButtonPin
#resource "\\" + def_ButtonWait


在此使用图像确实简化了事情。 请记住,我们只想为用户指示服务正在运行,并且在此操作期间它将无法响应任何其它请求。

接下来,在类文件中,我们添加一个内部私密变量来控制内部操作。 

class C_Controls
{
        private :
//+------------------------------------------------------------------+
                string  m_szBtnPlay;
                long    m_id;
                bool    m_bWait;
                struct st_00
                {
                        string  szBtnLeft,
                                szBtnRight,
                                szBtnPin,
                                szBarSlider;
                        int     posPinSlider,
                                posY;
                }m_Slider;
//+------------------------------------------------------------------+


添加此变量,我们就已经有了回放/模拟服务状态的概念。 不过,需要在相应的地方初始化它,最好的选项是类构造函数。

C_Controls() : m_id(0), m_bWait(false)
        {
                m_szBtnPlay             = NULL;
                m_Slider.szBarSlider    = NULL;
                m_Slider.szBtnPin       = NULL;
                m_Slider.szBtnLeft      = NULL;
                m_Slider.szBtnRight     = NULL;
        }


请注意,我们需要用 “false” 初始化它,因为回放/模拟服务将始终自由启动,并能够响应任何命令。 即使初始化在此处发生,我们也会在其它调用中关注正确的状态。 但这足以满足我们此刻的目的。

现在我们需要分析以下内容:我们真正想要锁定哪个事件? 每次我们向前或向后拖动回放位置时,我们都会看到按钮从 “播放” 变为 “暂停”,我们打算阻止用户访问该按钮。 只需单击一下,控制指标就会向回放/模拟服务请求操作。 尽管服务在忙于回放/模拟的准备阶段时不会响应。

如果您查看代码,您会发现系统总是对事件做出反应;换句话说,这是基于事件的系统。 这就是我们创建 EventCustom 枚举来支持基于事件的系统的原因。 我们不打算改变这一点。 事实上,我们甚至不应该考虑进行这样的更改,因为它会迫使我们采用比基于事件更复杂的方式。 不过,简单地添加标示事件存在的枚举并不是解决方案。 我们看看还需要做什么。 我们将更改 DispatchMessage 过程,从而在服务繁忙时,按播放/暂停按钮不会生成事件。 这可以通过添加以下检查轻松实现:

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
        {
                u_Interprocess Info;
                static int six = -1, sps;
                int x, y, px1, px2;
                                
                switch (id)
                {

// ... Internal code ...

                        case CHARTEVENT_OBJECT_CLICK:
                                if (m_bWait) break;
                                if (sparam == m_szBtnPlay)
                                {
                                        Info.s_Infos.isPlay = (bool) ObjectGetInteger(m_id, m_szBtnPlay, OBJPROP_STATE);
                                        if (!Info.s_Infos.isPlay) CreteCtrlSlider(); else
                                        {
                                                RemoveCtrlSlider();
                                                m_Slider.szBtnPin = NULL;
                                        }
                                        Info.s_Infos.iPosShift = (ushort) m_Slider.posPinSlider;
                                        GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                        ChartRedraw();
                                }else   if (sparam == m_Slider.szBtnLeft) PositionPinSlider(m_Slider.posPinSlider - 1);
                                else if (sparam == m_Slider.szBtnRight) PositionPinSlider(m_Slider.posPinSlider + 1);
                                break;

// ... The rest of the code ....

通过添加这段测试行,我们可以防止指标在繁忙时向服务发送请求。 不过,这仍然不能彻底解决我们的问题,因为单击 “播放/暂停” 按钮时,若它没有任何变化,用户也许不喜欢这样。 我们必须采取其它行动。 此外,我们仍然无法为正在测试的变量设置正确的数值。

这部分也许看起来有点令人困惑,但我们真正要做的就是更改 m_bWait 变量的值,并检查它。 这将令我们能够判定应该绘制哪些图像。 目标是在服务繁忙时将 “播放/暂停” 按钮替换为其它图像,并在服务禁用时将其恢复为传统的 “播放/暂停” 按钮。 我们将采用一个简单的方式:

void CreateBtnPlayPause(bool state)
{
        m_szBtnPlay = def_PrefixObjectName + "Play";
        CreateObjectBitMap(5, 25, m_szBtnPlay, (m_bWait ? def_ButtonWait : def_ButtonPause), (m_bWait ? def_ButtonWait : def_ButtonPlay));
        ObjectSetInteger(m_id, m_szBtnPlay, OBJPROP_STATE, state);
}


请注意,我们只是简单地检查变量。 根据其值,我们用 “播放/暂停” 按钮,或表示等待信号的按钮。 但如何操控这个按钮? 它会持续从终端读取全局变量的值吗? 这里有类似的东西。 请记住:每次服务往市场回放资产里添加新记录时,这都会在指标中反映出来。 因此,MetaTrader 5 将生成一个事件,该事件将启动 OnCalculate 函数。 这就是我们大展身手的地方,但我们不会持续监控指标。 我们将以更优雅的方式做到这一点。 为了了解该流程,请查看下图,该图显示了代码中的调用流程:

正是这一系列动作,管理控制指标的按钮执行正确操作。 CreateBtnPlayPause 过程之前已经介绍过,故我认为它是不言自明的。 我们现在要看一下这张示意图的其它重点。 OnCalculate 过程包含一个更困难的逻辑,需要明白 DispatchMessage 中执行的步骤。 

故此,我们移到处理自定义事件的基础代码。 我们查看以下代码:

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        u_Interprocess Info;
        static int six = -1, sps;
        int x, y, px1, px2;
                                
        switch (id)
        {
                case (CHARTEVENT_CUSTOM + Ev_WAIT_ON):
                        m_bWait = true;
                        CreateBtnPlayPause(true);
                        break;
                case (CHARTEVENT_CUSTOM + Ev_WAIT_OFF):
                        m_bWait = false;
                        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                        CreateBtnPlayPause(Info.s_Infos.isPlay);
                        break;

// ... The rest of the code ...

当控制指标的 OnChartEvent 调用 DispatchMessage 时,数据将被传递,使之能够处理 MetaTrader 5 平台提供的事件消息,以及我们的代码在特定点触发的自定义事件。 我们稍后将讨论自定义事件。 如果用到的是 Ev_WAIT_ON 自定义事件,该函数将查找匹配的代码。 这将告诉我们服务繁忙,导致变量 m_bWait 为 true。 接下来,我们调用 “播放/暂停” 按钮的创建,该按钮实际上会绘制一个指示繁忙状态的图像。 当触发 Ev_WAIT_OFF 自定义事件时,我们希望指示服务的当前状态,即它处于播放模式,亦或暂停模式。 因此,m_bWait 变量将接收一个值,该值指示服务可接受请求。 我们还需要从全局终端变量中获取数据,该变量将包含服务的当前状态。 接下来,我们调用一个函数创建一个 “播放/暂停” 按钮,以便用户能与系统交互。

这种方式非常直观,我想每个人都可以理解这个思路。 最大的问题是:这些事件将如何触发? 我们会有极其复杂和难以理解的代码吗? 不会的,在 MQL5 中触发事件的途径非常简单,分析和处理上述自定义事件的方式也非常简单。 在上面的代码中,您可以看到如何处理两个自定义事件。 现在我们来看看如何触发这些事件。 当我们触发自定义事件时,我们实际上是调用 OnChartEvent 函数。 当事件发生时,始终调用该函数,无论是自定义事件还是源自 MetaTrader 5 的事件。 调用的函数都始终相同。 现在在命令行上查看此函数的代码:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        Control.DispatchMessage(id, lparam, dparam, sparam);
}


也就是,当击发一个事件时,其处理将委派给 C_Control 类,并执行 DispatchMessage 函数。 您注意到这一切是如何运作的吗? 如果 DispatchMessage 函数中包含的代码位于事件处理函数内部,则结果将是相同的。 不过,请注意,OnChartEvent 函数需要 4 个参数,而触发自定义事件的函数将用到更多参数。 实际上,触发自定义事件用到 5 个参数。 以这种方式,我们可以区分自定义事件,和来自 MetaTrader 5 的事件。 如果您留意,您会注意到选择时所用的数值,是 EventCustom 枚举中标示的数值个数,与其它数据 CHARTEVENT_CUSTOM 个数的总和。 这样我们就可以得到正确的数值。 

但是,这个数值是如何创造的呢? 如何使用 MQL5 生成自定义事件? 若要明白这一点,查看控制指标的主要代码:OnCalculate 函数。 它如下所示:

int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
{
        static bool bWait = false;
        u_Interprocess Info;
        
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        if (!bWait)
        {
                if (Info.s_Infos.isWait)
                {
                        EventChartCustom(m_id, Ev_WAIT_ON, 0, 0, "");
                        bWait = true;
                }
        }else if (!Info.s_Infos.isWait)
        {
                EventChartCustom(m_id, Ev_WAIT_OFF, 0, Info.u_Value.df_Value, "");
                bWait = false;
        }
        
        return rates_total;
}


我们来搞明白上面的代码是如何工作的。 首先要注意的是,此代码是由 MetaTrader 5 调用的事件处理程序。 也就是说,每当资产价格发生变化,或资产收到新的交易跳价时,MetaTrader 5 都会自动调用 OnCalculate 函数。 因此,我们不需要在指标内部设置定时器。 事实上,您应该避免(尽可能)在指标中使用定时器,因为它们不仅会影响相关指标,还会影响所有其它指标。 因此,我们打算利用 MetaTrader 5 平台的调用来检查服务的情况。 请注意,该服务会将输入数据发送到回放/模拟资源,故此会间接调用 OnCalculate 函数。


结束语

我希望您能获得大致的思路,因为它是其它一切的基础。 因此,每次我们调用 OnCalculate 时,我们都会在终端全局变量中写入存在的值,并检查局部静态变量是否为 true。 如果它的值不为 true,我们将检查服务是否繁忙。 如果满足此条件,我们将创建一个特殊事件来报告此情况。 在此之后,我们将立即更改局部静态变量的值,标明控制指标知道回放/模拟服务繁忙。 如此,下次我们调用 OnCalculate 时,我们会检查回放/模拟服务是否可以执行自由活动。 一旦发生这种情况,我们就会触发一个特殊事件,表明服务已准备好接收控制指标请求。 只要局部静态变量为 true,循环就会一直重复。

现在请注意,我们正在使用一些常用的东西来触发自定义事件,即 EventChartCustom 函数。 在此,我们仅受当前图表和控制指标的限制。 不过,我们可以触发任何图表、指标、甚至智能系统的事件。 为此,您需要正确填写 EventChartCustom 函数的参数。 如果我们这样做,那么其它一切都将委派给 MetaTrader 5 平台,我们只需要在指标或智能系中处理自定义事件。 这是一个鲜为人知的方面,据我所知,人们有时认为 MetaTrader 5 平台无法执行某些操作。 

在下面的视频中,我将演示当前开发阶段的系统。 我希望您喜欢这一系列文章,并希望它们能帮助您更好地研究 MetaTrader 5 平台,以及 MQL5 语言提供的能力。



本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10919

附加的文件 |
Market_Replay.zip (13060.83 KB)
神经网络变得轻松(第四十部分):在大数据上运用 Go-Explore 神经网络变得轻松(第四十部分):在大数据上运用 Go-Explore
本文讨论 Go-Explore 算法覆盖长周期训练的运用,因为随着训练时间的增加,随机动作选择策略也许不会导致可盈利验算。
神经网络变得轻松(第三十九部分):Go-Explore,一种不同的探索方式 神经网络变得轻松(第三十九部分):Go-Explore,一种不同的探索方式
我们继续在强化学习模型中研究环境。 在本文中,我们将见识到另一种算法 — Go-Explore,它允许您在模型训练阶段有效地探索环境。
开发回放系统 — 市场模拟(第 10 部分):仅用真实数据回放 开发回放系统 — 市场模拟(第 10 部分):仅用真实数据回放
在此,我们将查看如何在回放系统中使用更可靠的数据(交易跳价),而不必担心它是否被调整。
如何用 MQL5 创建自定义真实强度指数指标 如何用 MQL5 创建自定义真实强度指数指标
这是一篇关于如何创建自定义指标的新文章。 这一次,我们将与真实强度指数(TSI)共事,并基于它创建一个智能系统。