English Русский Español Deutsch 日本語 Português
preview
开发回放系统 — 市场模拟(第 18 部分):跳价和更多跳价(II)

开发回放系统 — 市场模拟(第 18 部分):跳价和更多跳价(II)

MetaTrader 5测试者 | 28 二月 2024, 09:01
385 0
Daniel Jose
Daniel Jose

概述

在上一篇文章“开发回放系统 — 市场模拟(第 17 部分):跳价和跳价(I)”中,我们添加了在市场观察系统中显示跳价图表的功能。这是一项非常积极的发展,但在那篇文章中,我提到我们的系统还有一些缺陷。因此,我决定禁用该服务的某些功能,直到缺陷得以纠正。现在,我们将修复跳价图开始显示时出现的许多错误。

就我来说,最引人注目的,也许是最烦人的错误之一是与创建 1-分钟柱线所需的模拟时间有关。那些一直在关注和测试回放/模拟服务的人可能已经注意到,时序远非理想。当资产具有一定的流动性时,这一点变得更加明显,这可能会导致我们在几秒钟内失去真实交易。从开始起,我就试图顾及这个问题,并尝试能让回放/模拟服务的体验类似于交易真实资产。

显然,目前的衡量度与创建 1-分钟柱线的理想时间相距甚远。这是我们要率先解决的一件事。解决同步问题并不困难。也许这看起来很难,但实际上却很简单。在上一篇文章中,我们没有进行所需的调整,因为它的目的是解释如何把图表上创建 1-分钟柱线的跳价数据转移至市场观察窗口。

如果我们决定修复计时器,对于那些想知道存储在文件中的实际跳价数据如何在市场观察窗口中应用的人来说,将很难理解。故此,只把精力集中到如何在市场观察窗口中启用跳价,至于这个过程如何行进,我认为现在很清楚了。一个重要的细节是,我没有发现任何其它关于如何做到这一点的参考资料。唯一的参考是文档本身,在搜索时,我甚至发现在社区论坛上也有许多想知道如何做到这一点的人士。然而,他们没有找到一个真正有助于理解这个过程应是什么样的答案。因此,上一篇文章似乎以一个奇怪的注释结束,似乎像它不清楚如何解决那里提到的问题。

但在此,我们将真正应对它,尽管还不彻底,因为有些问题更难解释。尽管实现这些往往相对简单。为了解释一些彼此完全不同,但又以某种方式相关的观点,并在一篇文章中做到所有事情,会令其极易混肴。非但解释,理解整个过程也许都会进一步复杂化。

我对每篇文章的观念是解释和鼓励人们学习和深入探索 MetaTrader 5 平台和 MQL5 语言。这远远超出了在某处分发的代码中能够看到的内容。我真的希望你们每个人都有创造力和动力,去探索以前从未走过的道路,而非总是做同样的事,就好像所有人在 MQL5 或 MetaTrader 5 帮助下做到的那些事之外,没有带来任何益处。但我们回到我们的文章。


实现 1-分钟柱线创建时间校正

我们从计时器开始。为了解决它,我们要修改整段代码中的一处小细节。该修改如下面的代码所示:

inline bool ReadAllsTicks(const bool ToReplay)
                        {
#define def_LIMIT (INT_MAX - 2)
#define def_Ticks m_Ticks.Info[m_Ticks.nTicks]

                                string   szInfo;
                                MqlRates rate;
                                
                                Print("Loading ticks for replay. Please wait...");
                                ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
                                while ((!FileIsEnding(m_File)) && (m_Ticks.nTicks < def_LIMIT) && (!_StopFlag))
                                {
                                        ArrayResize(m_Ticks.Info, m_Ticks.nTicks + 1, def_MaxSizeArray);
                                        szInfo = FileReadString(m_File) + " " + FileReadString(m_File);
                                        def_Ticks.time = StringToTime(StringSubstr(szInfo, 0, 19));
                                        def_Ticks.time_msc = (def_Ticks.time * 1000) + (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                                        def_Ticks.time_msc = (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                                        def_Ticks.bid = StringToDouble(FileReadString(m_File));
                                        def_Ticks.ask = StringToDouble(FileReadString(m_File));
                                        def_Ticks.last = StringToDouble(FileReadString(m_File));
                                        def_Ticks.volume_real = StringToDouble(FileReadString(m_File));
                                        def_Ticks.flags = (uchar)StringToInteger(FileReadString(m_File));                                       
                                        if (def_Ticks.volume_real > 0.0)
                                        {
                                                ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
                                                m_Ticks.nRate += (BuiderBar1Min(rate, def_Ticks) ? 1 : 0);
                                                m_Ticks.Rate[m_Ticks.nRate] = rate;
                                        }
                                        m_Ticks.nTicks++;
                                }
                                FileClose(m_File);
                                if (m_Ticks.nTicks == def_LIMIT)
                                {
                                        Print("Too much data in the tick file.\nCannot continue...");
                                        return false;
                                }
                                return (!_StopFlag);
#undef def_Ticks
#undef def_LIMIT
                        }

一切准备就绪。现在计时器能更正确地工作。您也许会想:但这能怎样呢?我不明白🤔。简单地删除一行(它已被删除),并用一些计算替换它,就已完全解决计时器问题,但还有更多。我们也可以删除时间值,将其保留为零。

在向市场观察图表添加跳价时,这将为我们节省几个机器周期。但是(这个“但是”真的让我惊叹),在创建 1-分钟柱线时,我们必须执行额外的计算,然后才可在 MetaTrader 5 中绘制。结果就是,我们将不得不花费几个机器周期来进行计算。我们以这种方式提供的成本更低。

据该更改,我们可以立即实现另一处修改:

inline void ViewTick(void)
                        {
                                MqlTick tick[1];

                                tick[0] = m_Ticks.Info[m_ReplayCount];
                                tick[0].time_msc = (m_Ticks.Info[m_ReplayCount].time * 1000) + m_Ticks.Info[m_ReplayCount].time_msc;
                                CustomTicksAdd(def_SymbolReplay, tick);
                        }

我们不再需要以前的远程计算,因为它是在我们从文件加载真实跳价之刻执行的。如果我们在上一篇文章中就这样做的话,许多人就不会理解为什么跳价会出现在市场观察图表上。但正如我曾说的,在我看来,现在这一点变得更加清晰。更平滑的变化这一简单事实让所有人都能更容易理解它们。

现在遇到一个也许令人烦心的问题:

若资产的流动性很低,交易可能若干秒才会发生,那么服务是否有可能冻结?由于它实际上并未完全停止,那么能否避免它关闭?这种情况会不会因为计时器处于待机状态的几秒钟而发生?

这是个好问题。我们看看为什么这不会发生。

                bool LoopEventOnTime(const bool bViewBuider, const bool bViewMetrics)
                        {

                                u_Interprocess Info;
                                int iPos, iTest;
                                
                                iTest = 0;
                                while ((iTest == 0) && (!_StopFlag))
                                {
                                        iTest = (ChartSymbol(m_IdReplay) != "" ? iTest : -1);
                                        iTest = (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value) ? iTest : -1);
                                        iTest = (iTest == 0 ? (Info.s_Infos.isPlay ? 1 : iTest) : iTest);
                                        if (iTest == 0) Sleep(100);
                                }
                                if ((iTest < 0) || (_StopFlag)) return false;
                                AdjustPositionToReplay(bViewBuider);
                                m_MountBar.delay = 0;
                                while ((m_ReplayCount < m_Ticks.nTicks) && (!_StopFlag))
                                {
                                        CreateBarInReplay(bViewMetrics);
                                        iPos = (int)(m_ReplayCount < m_Ticks.nTicks ? m_Ticks.Info[m_ReplayCount].time_msc - m_Ticks.Info[m_ReplayCount - 1].time_msc : 0);
                                        m_MountBar.delay += (iPos < 0 ? iPos + 1000 : iPos);
                                        if (m_MountBar.delay > 400)
                                        {
                                                if (ChartSymbol(m_IdReplay) == "") break;
                                                GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                                if (!Info.s_Infos.isPlay) return true;
                                                Info.s_Infos.iPosShift = (ushort)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks);
                                                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                                Sleep(m_MountBar.delay - 20);
                                                m_MountBar.delay = 0;
                                        }
                                }                               
                                return (m_ReplayCount == m_Ticks.nTicks);
                        }

真正的问题是,当达到 Sleep 函数时,服务会保持“停止”一段时间,但没有任何情况能终止服务。事实上,当调用更改 Stop 标志的状态时,可以使用 STOP 请求停止它。我怎么知道这个?这是 Sleep 函数文档中写明的内容。下面是一个摘要,它把事情说清楚了。

注意

Sleep() 函数不能从自定义指标调用,因为指标是在界面线程中执行的,不应该减慢它的速度。该函数内置每 0.1 秒检查一次 EA 停止标志状态

因此,我们不需要经常检查服务是否已停止。MetaTrader 5 的实现将为我们做到这一点。这太棒了。这为我们节省了大量创建方法以便维护功能的相关工作,同时保持与用户的交互性。


在快速导航系统中实现修复

现在我们将用导航系统解决问题,将所有内容恢复到原始状态。这有一个小缺点,即我们无法仅使用 MQL5 来解决。由于我们在此阶段不会强制用到 DLL,因此需要在 MetaTrader 5 平台中用到一个小细节。我们这样做是为了保持事情的正确性。事实上,需要做的事情很简单,在某些方面甚至是傻瓜式。不过,为了明白将要做什么,您需要注意我的解释。因为一开始看起来几乎是直觉,但除非您非常专心,否则您也许无法真正理解它。

但无论如何,首先我们看看代码是如何编写的。

#property service
#property icon "\\Images\\Market Replay\\Icon.ico"
#property copyright "Daniel Jose"
#property version   "1.18"
#property description "Replay-simulation system for MT5."
#property description "It is independent from the Market Replay."
#property description "For details see the article:"
#property link "https://www.mql5.com/en/articles/11113"
//+------------------------------------------------------------------+
#define def_Dependence  "\\Indicators\\Market Replay.ex5"
#resource def_Dependence
//+------------------------------------------------------------------+
#include <Market Replay\C_Replay.mqh>
//+------------------------------------------------------------------+
input string            user00 = "Mini Dolar.txt";      //"Replay" config file.
input ENUM_TIMEFRAMES   user01 = PERIOD_M1;             //Initial timeframe for the chart.
input bool              user02 = true;                  //Visual bar construction.
input bool              user03 = true;                  //Visualize creation metrics
//+------------------------------------------------------------------+
void OnStart()
{
        C_Replay        *pReplay;

        pReplay = new C_Replay(user00);
        if ((*pReplay).ViewReplay(user01))
        {
                Print("Permission received. The replay service can now be used...");
                while ((*pReplay).LoopEventOnTime(user02, user03));
        }
        delete pReplay;
}
//+------------------------------------------------------------------+

现在,我们又拥有了一个能够启用或禁用柱线构造可视化的系统。但决定权由用户掌控。如果您愿意,可以关闭可视化效果 — 从编码的角度来看,它不会产生任何区别。原因在于,如果我们想将市场观察配合跳价一起使用,我们仍然需要以某种方式在 MetaTrader 5 中做一些事情。对于确保图表具有足够的数值这是必要的。但对于常规图表和价格线,不需要任何更改或干预,因为它们将正确配置。(当我这样做时,我就是这么想的,但您稍后会发现我错了。有一个错误我真的不知道如何修复,但会在另一篇文章中看到它)。

为此,我必须对 C_Replay 类做一些更改。第一个变化涉及柱线创建例程。请看下面的代码:

inline void CreateBarInReplay(const bool bViewMetrics, const bool bViewTicks)
                        {
#define def_Rate m_MountBar.Rate[0]

                                bool bNew;
                                MqlTick tick[1];

                                if (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time))
                                {                               
                                        if (bViewMetrics) Metrics();
                                        m_MountBar.memDT = (datetime) macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
                                        def_Rate.real_volume = 0;
                                        def_Rate.tick_volume = 0;
                                }
                                bNew = (def_Rate.tick_volume == 0);
                                def_Rate.close = (m_Ticks.Info[m_ReplayCount].volume_real > 0.0 ? m_Ticks.Info[m_ReplayCount].last : def_Rate.close);
                                def_Rate.open = (bNew ? def_Rate.close : def_Rate.open);
                                def_Rate.high = (bNew || (def_Rate.close > def_Rate.high) ? def_Rate.close : def_Rate.high);
                                def_Rate.low = (bNew || (def_Rate.close < def_Rate.low) ? def_Rate.close : def_Rate.low);
                                def_Rate.real_volume += (long) m_Ticks.Info[m_ReplayCount].volume_real;
                                def_Rate.tick_volume += (m_Ticks.Info[m_ReplayCount].volume_real > 0 ? 1 : 0);
                                def_Rate.time = m_MountBar.memDT;
                                CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate);
                                tick = m_Ticks.Info[m_ReplayCount];
                                if (bViewTicks) CustomTicksAdd(def_SymbolReplay, tick);
                                m_ReplayCount++;
                                
#undef def_Rate
                        }

该例程需要添加新参数。该参数将启用或禁用将跳价发送到市场观察窗口。为什么要这样做?这是因为不可能同时更新市场观察系统里的跳价和柱线图。当我们将服务配置为随时启动时,就会发生这种情况。但在正常使用的情况下,我们可以毫无问题地将跳价发送到市场观察窗口和柱线图,这很奇怪。

那么您也许会想:所以我们并不能真正改变市场观察窗口的内容。我们可以,但并非在所有情况下。我们真正能做的就是删除旧的跳价。但不可能毫无困难,至少 MetaTrader 5 平台的开发人员解决在市场观察窗口中使用自定义品种的相关问题之前是这样。这是因为在自定义交易品种中已有的跳价不会从我们可以看到此类自定义跳价的窗口中消失。奇怪的很,当我们回到以前的任何位置,它们仍然在那里,这令我们很难理解。

无论如何,您可以在下面看到负责管理位置系统的函数:

                void AdjustPositionToReplay(const bool bViewBuider)
                        {
                                u_Interprocess Info;
                                MqlRates       Rate[def_BarsDiary];
                                int            iPos, nCount;
                                
                                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                                if (m_ReplayCount == 0)
                                        for (; m_Ticks.Info[m_ReplayCount].volume_real == 0; m_ReplayCount++);
                                if (Info.s_Infos.iPosShift == (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks)) return;
                                iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1)));
                                Rate[0].time = macroRemoveSec(m_Ticks.Info[iPos].time);
                                if (iPos < m_ReplayCount)
                                {
                                        CustomRatesDelete(def_SymbolReplay, Rate[0].time, LONG_MAX);
                                        CustomTicksDelete(def_SymbolReplay, m_Ticks.Info[iPos].time_msc, LONG_MAX);
                                        if ((m_dtPrevLoading == 0) && (iPos == 0)) FirstBarNULL(); else
                                        {
                                                for(Rate[0].time -= 60; (m_ReplayCount > 0) && (Rate[0].time <= macroRemoveSec(m_Ticks.Info[m_ReplayCount].time)); m_ReplayCount--);
                                                m_ReplayCount++;
                                        }
                                }else if (iPos > m_ReplayCount)
                                {
                                        if (bViewBuider)
                                        {
                                                Info.s_Infos.isWait = true;
                                                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                        }else
                                        {
                                                for(; Rate[0].time > (m_Ticks.Info[m_ReplayCount].time); m_ReplayCount++);
                                                for (nCount = 0; m_Ticks.Rate[nCount].time < macroRemoveSec(m_Ticks.Info[iPos].time); nCount++);
                                                CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, nCount);
                                        }
                                }
                                for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag);) CreateBarInReplay(false, false);
                                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                                Info.s_Infos.isWait = false;
                                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                        }

与以前的版本相比,这里唯一的变化:一个简单的函数从市场观察窗口的跳价图上删除特定点的跳价。为什么我们以前没有看到这种实现?因为我仍在尝试在两个图表(含柱线图和跳价图)上实现动态数据更新。但我无法做到更新系统却不会引发新的错误和问题。在某些时候,我简单地决定仅更新柱线图表,故此这个函数现在有 2 个参数。 

现在,该系统与实现跳价图表显示和市场观察窗口之前几乎相同,在结束本文之前,我还要再展示一件事。但我希望您明白当我们依据真实数据时,回放/建模是如何工作的。现在,我们只有在数据模拟完成后才将跳价添加到市场观察窗口。这正是下一章节的主题。


在市场观察中使用模拟数据

由于我不打算将市场观察窗口中关于跳价的主题延展到另一篇文章中,我们看看如何做到这一点,或者更准确地说,参阅我针对此类状况的建议。至于 1-分钟柱线的跳价建模,这里的问题比迄今为止已做过的要简单得多。如果您明白了之前的一切,那么您理解这一点也没有问题。

与使用真实交易数据时发生的不同,当使用模拟数据时,我们最初缺乏某些类型的信息。创建这些数据并非不可能,而是必须精心完成。我说的信息是当最后一个价格超过出价和要价之间的区域时。如果您仔细观察回放,在这种情况发生的那一刻,您会发现突破这段被出价和要价限定的区域总是非常快速和罕见。事实上,根据我在市场里的经验,这些事情发生在价格波动飙发时。然而,正如我所说,这些都是罕见的事件。

注意:因此,永远不要相信您可以且能始终在点差内操作。有时,系统可以超越点差。了解这一点很重要,因为在开发订单系统时,这些信息与正确理解它至关重要。 

重要事实:价格由出价和要价组成,但这并不意味着系统存在缺口或崩溃。我们根本没有及时收到交易服务器更新的出价和要价数值。但如果您遵照订单簿,您会发现事情与大多数人想象的有些不同。因此,您需要对整个交易系统有丰富的经验才能真正知晓存在的问题。

知道了这一点,您甚至可以考虑将此类走势整合到您的模拟系统当中。这将令情况更加真实。但请记住,必须谨慎对待此类事情。理想情况下,您应该非常了解该资产的此类走势模拟。只有按这种方式,才能将您带入正在发生的事情,更接近真实市场中实际发生的事情。为了明白如何在模拟器中启用这种走势类型,首先我们来看看应该如何实现模拟器,以便价格始终保持在出价到要价范围内。

首先,我们需要添加一个新变量。

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

我们将用它来搞清跳价是基于真实数据还是模拟数据。这种差异至关重要,因为我们不会真的模拟出价和要价走势。我们将根据模拟器生成的最后交易价格值来建立这些限定。但主要原因是出价和要价不参与报告的交易量。为了保持模拟函数的简单性,我们将进行此设置,以便在其它地方生成出价和要价。

一旦我们有了新变量,我们就需要正确初始化它。有两个地方将对其进行初始化。第一处是当我们表明我们正用到模拟跳价时。

                bool BarsToTicks(const string szFileNameCSV)
                        {
                                C_FileBars      *pFileBars;
                                int             iMem = m_Ticks.nTicks;
                                MqlRates        rate[1];
                                MqlTick         local[];
                                
                                pFileBars = new C_FileBars(szFileNameCSV);
                                ArrayResize(local, def_MaxSizeArray);
                                Print("Converting bars to ticks. Please wait...");
                                while ((*pFileBars).ReadBar(rate) && (!_StopFlag)) Simulation(rate[0], local);
                                ArrayFree(local);
                                delete pFileBars;
                                m_Ticks.bTickReal = false;
                                
                                return ((!_StopFlag) && (iMem != m_Ticks.nTicks));
                        }

另一处我们将初始化该变量的地方是当我们指示我们正用到真实跳价时。

                datetime LoadTicks(const string szFileNameCSV, const bool ToReplay = true)
                        {
                                int      MemNRates,
                                         MemNTicks;
                                datetime dtRet = TimeCurrent();
                                MqlRates RatesLocal[];
                                
                                MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate);
                                MemNTicks = m_Ticks.nTicks;
                                if (!Open(szFileNameCSV)) return 0;
                                if (!ReadAllsTicks(ToReplay)) return 0;
                                if (!ToReplay)
                                {
                                        ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates));
                                        ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0);
                                        CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates));
                                        dtRet = m_Ticks.Rate[m_Ticks.nRate].time;
                                        m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates);
                                        m_Ticks.nTicks = MemNTicks;
                                        ArrayFree(RatesLocal);
                                }
                                m_Ticks.bTickReal = true;
                                                                        
                                return dtRet;
                        };

现在我们知道是在处理真实、亦或模拟跳价,我们可以开始工作了。但在我们转入 C_Replay 类,并开始配置任何内容之前,我们需要对模拟器本身进行一些小的修改。当我们加载真实跳价时,我们会调整时间,如此毫秒字段中的数值校正为代表特定的时间点。但模拟器尚未进行此调整。因此,如果我们在修改 C_Replay 类之后尝试运行系统,我们无法真正了解模拟数据。这是因为毫秒字段中表示的时间不正确。

为了解决此问题,我们将进行以下修改:

inline void Simulation(const MqlRates &rate, MqlTick &tick[])
                        {
#define macroRandomLimits(A, B) (int)(MathMin(A, B) + (((rand() & 32767) / 32767.0) * MathAbs(B - A)))

                                long     il0, max, i0, i1;
                                bool     b1 = ((rand() & 1) == 1);
                                double   v0, v1;
                                MqlRates rLocal;
                                
                                ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary);
                                m_Ticks.Rate[++m_Ticks.nRate] = rate;
                                max = rate.tick_volume - 1;     
                                v0 = 4.0;
                                v1 = (60000 - v0) / (max + 1.0);
                                for (int c0 = 0; c0 <= max; c0++, v0 += v1)
                                {
                                        tick[c0].last = 0;
                                        tick[c0].flags = 0;
                                        il0 = (long)v0;
                                        tick[c0].time = rate.time + (datetime) (il0 / 1000);
                                        tick[c0].time_msc = (tick[c0].time * 1000) + (il0 % 1000);
                                        tick[c0].time_msc = il0 % 1000;
                                        tick[c0].volume_real = 1.0;
                                }
                                tick[0].last = rate.open;
                                tick[max].last = rate.close;
                                for (int c0 = (int)(rate.real_volume - rate.tick_volume); c0 > 0; c0--)
                                        tick[macroRandomLimits(0, max)].volume_real += 1.0;                                     
                                i0 = (long)(MathMin(max / 3.0, max * 0.2));
                                i1 = max - i0;
                                rLocal = rate;  
                                rLocal.open = rate.open;
                                rLocal.close = (b1 ? rate.high : rate.low);
                                i0 = RandomWalk(1, i0, rLocal, tick, 0);
                                rLocal.open = tick[i0].last;
                                rLocal.close = (b1 ? rate.low : rate.high);
                                RandomWalk(i0, i1, rLocal, tick, 1);
                                rLocal.open = tick[i1].last;
                                rLocal.close = rate.close;
                                RandomWalk(i1, max, rLocal, tick, 2);
                                for (int c0 = 0; c0 <= max; c0++)
                                {
                                        ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
                                        m_Ticks.Info[m_Ticks.nTicks++] = tick[c0];
                                }
#undef macroRandomLimits
                        }

我们删除了一些代码,并将其替换为推荐的代码。以这种方式,以毫秒为单位的时间就能与 C_Replay 类的期望兼容。现在我们可以进入并修改它,以便显示模拟内容。

在 C_Replay 类中,我们将重点针对以下代码中所示的单个函数进行修改:

inline void CreateBarInReplay(const bool bViewMetrics, const bool bViewTicks)
                        {
#define def_Rate m_MountBar.Rate[0]

                                bool bNew;
                                MqlTick tick[1];

                                if (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time))
                                {                               
                                        if (bViewMetrics) Metrics();
                                        m_MountBar.memDT = (datetime) macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
                                        def_Rate.real_volume = 0;
                                        def_Rate.tick_volume = 0;
                                }
                                bNew = (def_Rate.tick_volume == 0);
                                def_Rate.close = (m_Ticks.Info[m_ReplayCount].volume_real > 0.0 ? m_Ticks.Info[m_ReplayCount].last : def_Rate.close);
                                def_Rate.open = (bNew ? def_Rate.close : def_Rate.open);
                                def_Rate.high = (bNew || (def_Rate.close > def_Rate.high) ? def_Rate.close : def_Rate.high);
                                def_Rate.low = (bNew || (def_Rate.close < def_Rate.low) ? def_Rate.close : def_Rate.low);
                                def_Rate.real_volume += (long) m_Ticks.Info[m_ReplayCount].volume_real;
                                def_Rate.tick_volume += (m_Ticks.Info[m_ReplayCount].volume_real > 0 ? 1 : 0);
                                def_Rate.time = m_MountBar.memDT;
                                CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate);
                                if (bViewTicks)
                                {
                                        tick = m_Ticks.Info[m_ReplayCount];
                                        if (!m_Ticks.bTickReal)
                                        {
                                                tick[0].bid = tick[0].last - m_PointsPerTick;
                                                tick[0].ask = tick[0].last + m_PointsPerTick;
                                        }
                                        CustomTicksAdd(def_SymbolReplay, tick); 
                                }
                                m_ReplayCount++;
                                
#undef def_Rate
                        }

此处,我们有一个非常简单的更改。该更改仅创建和显示出价和要价值。但请记住,创建时基于最后一笔交易价格的数值,且只当我们与模拟数值打交道时才会进行。运行该段代码,我们将获得由随机游走系统生成的内部图形表达,就像我们使用 EXCEL 时所做的那样。在文章“开发回放系统 — 市场模拟(第 15 部分):模拟器的诞生(V)- 随机游走”中,我提到还有其它途径也可以做到同样的可视化,我指的就是这个模型。但那时还未到说如何做的时候。现在是时候了。

如果这些出和要价值尚未创建,我们只好根据上次模拟价格显示数值。这对您来说可能已经足够了,但有些人真的很喜欢看出价和要价值。不过,以这种方式使用数据并不完全合适。事实上操作是在出价和要价间精确运作的,没有真正接触它们,这表明直接操作是在市场上进行的。因此,交易是在没有订单簿的情况下执行的。在这种情况下,价格不应该移动,尽管它会如此,正如我们将在模拟器中看到的那样。因此,我们只需要修复以绿色高亮显示的部分。这样做是为了令走势至少与人们的实际期望一致。

查看高亮显示的段落,和如下所示的修改:

                                if (bViewTicks)
                                {
                                        tick = m_Ticks.Info[m_ReplayCount];
                                        if (!m_Ticks.bTickReal)
                                        {
                                                static double BID, ASK;
                                                
                                                if (tick[0].last > ASK)
                                                {
                                                        ASK = tick[0].ask = tick[0].last;
                                                        BID = tick[0].bid = tick[0].last - m_PointsPerTick;
                                                }
                                                if (tick[0].last < BID)
                                                {
                                                        ASK = tick[0].ask = tick[0].last + m_PointsPerTick;
                                                        BID = tick[0].bid = tick[0].last;
                                                }
                                                tick[0].ask = tick[0].last + m_PointsPerTick;
                                                tick[0].bid = tick[0].last - m_PointsPerTick;
                                        }
                                        CustomTicksAdd(def_SymbolReplay, tick); 
                                }

我们删除了一些代码,并增加了一些关键点。应该注意的是,出价和要价值是静态的,这是必要的,如此我们才能构建一个小指标。有些东西非常简单,但足以引发一场革命。由于在系统启动时,这些值很可能为零,因此我们要首先进行一次调用,其重点将首先放在要价上,并且我们有一条相当狭窄的通道只有一次跳价。然后,直到最后一笔交易价格离开这个通道,它将一直存在。

这很简单,但很实用。请注意,不应允许出价与要价值发生冲突(这是在交易所市场,而在外汇市场中则是另一回事,但我们稍后会看到这一点)。导致冲突的是最后执行的交易价格。如果我们对上面所示的代码进行小的修改会怎样?非常微妙的东西。如果出价或要价发生变化,而最后一笔成交价格没有真正变化,会发生什么情况?

我们按如下方式修改代码:

                                if (bViewTicks)
                                {
                                        tick = m_Ticks.Info[m_ReplayCount];
                                        if (!m_Ticks.bTickReal)
                                        {
                                                static double BID, ASK;
                                                
                                                if (tick[0].last > ASK)
                                                {
                                                        ASK = tick[0].ask = tick[0].last;
                                                        BID = tick[0].bid = tick[0].last - (m_PointsPerTick * ((rand() & 1) == 1 ? 2 : 1));
                                                }
                                                if (tick[0].last < BID)
                                                {
                                                        ASK = tick[0].ask = tick[0].last + (m_PointsPerTick * ((rand() & 1) == 1 ? 2 : 1));
                                                        BID = tick[0].bid = tick[0].last;
                                                }
                                        }
                                        CustomTicksAdd(def_SymbolReplay, tick); 
                                }

看看这样会多有趣。通过向系统添加一定程度的随机性,我们已能够包含直接订单。也就是说,在不改变出价或要价的情况下执行的订单。在真实市场中,此类订单并不经常发生,也不会以系统显示它们的形式出现。但如果我们忽略这一事实,我们已经有了一个优秀的系统,其中出价和要价之间的价差有时会很小。换言之,模拟器实际上适应了真实市场中更常见的状况。不过,我们应该提防过多的直接订单。为了避免这种泛滥,我们可降低事情的随机性。

为此,我们需要实现最后一处修改。它如下所示:

                                if (bViewTicks)
                                {
                                        tick = m_Ticks.Info[m_ReplayCount];
                                        if (!m_Ticks.bTickReal)
                                        {
                                                static double BID, ASK;
                                                double  dSpread;
                                                int     iRand = rand();
                                                
                                                dSpread = m_PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_PointsPerTick : 0 ) : 0 );
                                                if (tick[0].last > ASK)
                                                {
                                                        ASK = tick[0].ask = tick[0].last;
                                                        BID = tick[0].bid = tick[0].last - dSpread;
                                                        BID = tick[0].bid = tick[0].last - (m_PointsPerTick * ((rand() & 1) == 1 ? 2 : 1));
                                                }
                                                if (tick[0].last < BID)
                                                {
                                                        ASK = tick[0].ask = tick[0].last + (m_PointsPerTick * ((rand() & 1) == 1 ? 2 : 1));
                                                        ASK = tick[0].ask = tick[0].last + dSpread;
                                                        BID = tick[0].bid = tick[0].last;
                                                }
                                        }
                                        CustomTicksAdd(def_SymbolReplay, tick); 
                                }

此处,我们控制生成的随机复杂度。这样做是为了将一切偶然性都保持在一定程度的范围内。我们不时会得到直接订单。但这将以更可控的数量进行。为此,我们将在此处简单地调整这些数值。通过调整它们,我们创建了一个小窗口,其中点差可能略大于最小可能值。结果就是,我们将不时与建模系统生成的直接订单打交道,这在以前的文章中、或直到现在都是不可能的。


结束语

在本文中,我向您展示了如何利用市场观察实现在图表上设置和创建跳价的系统。我们在上一篇文章中就开始这样做了。有因于此,我们创建了一个能够模拟直接订单的模拟系统。这并非我们最初目标的一部分。我们距离在某些类型的交易系统中完全可用还有很长的路要走。但我们今天所做的只是一个开始。

在下一篇文章中,我们将继续有关创建市场回放/模拟系统的系列文章。附件包含 4 个不同的资源,用来测试和检查系统操作。请记住,我将提供真实的跳价数据和 1-分钟柱线,如此您就可以看到模拟值和真实值之间的差异。这将令您能够开始更深入地分析事物。为了理解此处解释的所有内容,您需要在两种模式下运行回放/模拟服务。首先在运行模拟时查看自定义品种,然后通过回放查看它的内容。但要注意跳价窗口,而不是图表本身。您会看到差异非常明显。至少有关市场观察窗口的内容如此。


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

附加的文件 |
Market_Replay_rvt_18.zip (12899.62 KB)
制作仪表板以显示指标和EA中的数据 制作仪表板以显示指标和EA中的数据
在本文中,我们将创建一个用于指标和EA的仪表板类。这是一个小系列文章中的介绍性文章,其中包含模板以在EA交易中包含和使用标准指标。我将首先创建一个类似于MetaTrader 5数据窗口的面板。
MetaTrader 5中的蒙特卡罗置换测试 MetaTrader 5中的蒙特卡罗置换测试
在本文中,我们将了解如何仅使用 Metatrader 5在任何 EA 交易上基于修改的分时数据进行置换测试。
了解使用MQL5下单 了解使用MQL5下单
在创建任何交易系统时,我们都需要有效地处理一项任务。这项任务是下单,或者让创建的交易系统自动处理订单,因为它在任何交易系统中都至关重要。因此,您将在本文中找到您需要了解的关于这项任务的大多数主题,以有效地创建您的交易系统。
软件开发和 MQL5 中的设计范式(第一部分):创建范式 软件开发和 MQL5 中的设计范式(第一部分):创建范式
有一些方法可以用来解决许多重复性的问题。一旦明白如何运用这些方法,就可助您有效地创建软件,并贯彻 DRY(不要重复自己)的概念。在这种境况下,设计范式的主题就非常好用,因为它们为恰当描述过,且重复的问题提供了解决方案。