开发回放系统 — 市场模拟(第 02 部分):首次实验(II)
概述
在上一篇文章“开发回放系统 — 市场模拟(第 01 部分):首次实验(I)”中,当我们尝试创建一个执行时间较短的事件系统,从而生成足够的市场模拟时,我们看出了一些局限性。 很明显,这种方式不可能少于 10 毫秒。 在许多情况下,这个时间相当短。 然而,如果您研究文章附带的文件,您会发现 10 毫秒还不是一个足够短的时间段。 有没有其它方法可以让我们达到 1 或 2 毫秒的期望时间?
考虑在数毫秒时间范围内处理相关的任何事情之前,重要的是要提醒每个人这不是一件容易的任务。 事实上,操作系统本身提供的计时器无法达到这个级别。 因此,这是一个巨大的、但并非高不可攀的问题。 在本文中,我将尝试回答这个问题,并展示如何超越操作系统的时间限制设置来解决它。 我知道很多人认为现代处理器每秒可以执行数十亿次计算。 不过,当处理器执行计算时,这是一回事;而计算机内部的所有进程能否协同完成所需的任务则是另一回事。 请注意,我们只尝试使用 MQL5 来实现这一点,而不使用任何外部代码或 DLL。 我们只使用纯 MQL5。
规划
为了验证这一点,我们必须对方法进行一些修改。 如果这个过程成功了,我们就不必再困惑于回放创建系统了。 我们就能专注于其它问题,并帮助我们使用真实跳价数值或模拟值进行研究或训练。 拼装 1 分钟柱线的方式保持不变。 这将是本文的主要焦点。
我们将最大程度使用一种通用方式,而我发现的最好的方式就是使用类似客户端-服务器的系统。 我已经在之前的文章“从头开始开发智能系统(第 16 部分):访问 Web 上的数据(II)”中解释过相关技术。在那篇文章中,我展示了在 MetaTrader 5 中传输信息的三种途径。 此处,我们将采用这些途径之一,即服务。 因此,市场回放将成为 MetaTrader 5 的一个服务。
您也许会认为我将从头开始创建所有内容。 但我为什么要做这样的事情呢? 基本上,系统已经在运行,然并未达到期望的 1 分钟时间。 您也许会问:“您认为将系统更改为服务可以解决此问题吗?“ 事实上,简单地用服务替换系统并不能解决我们的问题。 但是,如果我们从一开始就将 1 分钟柱线的创建与 EA 系统的其余部分隔离开来,那么我们以后的工作就会减少,因为 EA 本身会导致柱线构建的执行略有延迟。 我稍后会解释其中的原因。
您现在明白我们为什么要使用服务了吗? 它比上面讨论的其它方法更实用。 我们能够如我在有关如何在 EA 和服务之间交换消息的文章中解释的方式来控制它:从头开始开发交易 EA(第 17 部分):访问网络上的数据(III)。 但在这里我们不在意如何生成此控制,我们只希望服务能生成放置在图表上的柱线。 为了令事情更有趣,我们将以更具创造性的方式使用该平台,而不仅仅是使用 EA 和服务。
提醒一下,在最后一次尝试减少时间时,我们得到了以下结果:
这是我们得到的最佳时间。 在此,我们马上就要打碎这个时间。然而,我不希望您完全依附于这些值或此处显示的测试。 这一系列与创建回放/模拟器系统相关的文章已经处于更高级的阶段,我多次更改了一些概念,以便系统能真实地按预期工作。 即使此时一切似乎都足够了,但在内心深处,我犯了一些与计时测试相关的错误。 这种错误或误解,在一个早期的系统中,并不容易被注意到。 随着本系列文章的发展,您会注意到这个与时间相关的问题要复杂得多,它涉及的不仅仅是让 CPU 和 MetaTrader 5 平台在图表上提供一定数量的数据,如此您就可以沉浸在回放/模拟器系统中。
所以您不要从字面上理解在这里看到的一切。 追随本系列文章,因为我们在这里要做的事情并不简单或容易做到。
实现
我们从创建系统的基础开始。 这些包括:
- 创建 1 分钟柱线的服务
- 用于启动服务的脚本
- 用于模拟的 EA(这将在后面讨论)
定义行情回放服务
为了正确操控该服务,我们需要更新我们的 C_Replay 类。 但是这些变化非常小,所以我们不会深入到细节。 基本上,这些是返回代码。 不过,有一点值得单独注意,因为它实现了其他一些东西。 代码如下:
#define macroGetMin(A) (int)((A - (A - ((A % 3600) - (A % 60)))) / 60) int Event_OnTime(void) { bool isNew; int mili; static datetime _dt = 0; if (m_ReplayCount >= m_ArrayCount) return -1; if (m_dt == 0) { m_Rate[0].close = m_Rate[0].open = m_Rate[0].high = m_Rate[0].low = m_ArrayInfoTicks[m_ReplayCount].Last; m_Rate[0].tick_volume = 0; m_Rate[0].time = m_ArrayInfoTicks[m_ReplayCount].dt - 60; CustomRatesUpdate(def_SymbolReplay, m_Rate, 1); _dt = TimeLocal(); } isNew = m_dt != m_ArrayInfoTicks[m_ReplayCount].dt; m_dt = (isNew ? m_ArrayInfoTicks[m_ReplayCount].dt : m_dt); mili = m_ArrayInfoTicks[m_ReplayCount].milisec; while (mili == m_ArrayInfoTicks[m_ReplayCount].milisec) { m_Rate[0].close = m_ArrayInfoTicks[m_ReplayCount].Last; m_Rate[0].open = (isNew ? m_Rate[0].close : m_Rate[0].open); m_Rate[0].high = (isNew || (m_Rate[0].close > m_Rate[0].high) ? m_Rate[0].close : m_Rate[0].high); m_Rate[0].low = (isNew || (m_Rate[0].close < m_Rate[0].low) ? m_Rate[0].close : m_Rate[0].low); m_Rate[0].tick_volume = (isNew ? m_ArrayInfoTicks[m_ReplayCount].Vol : m_Rate[0].tick_volume + m_ArrayInfoTicks[m_ReplayCount].Vol); isNew = false; m_ReplayCount++; } m_Rate[0].time = m_dt; CustomRatesUpdate(def_SymbolReplay, m_Rate, 1); mili = (m_ArrayInfoTicks[m_ReplayCount].milisec < mili ? m_ArrayInfoTicks[m_ReplayCount].milisec + (1000 - mili) : m_ArrayInfoTicks[m_ReplayCount].milisec - mili); if ((macroGetMin(m_dt) == 1) && (_dt > 0)) { Print("Elapsed time: ", TimeToString(TimeLocal() - _dt, TIME_SECONDS)); _dt = 0; } return (mili < 0 ? 0 : mili); }; #undef macroGetMin
高亮显示的部分已添加到 C_Replay 类的源代码之中。 我们要做的是定义延迟时间,也就是说,我们将明确地采用在该行中获得的值,但以毫秒为单位。 不要忘记,这个时间并非准确,因为它还取决于一些变量。 不过,我们将尝试将其维持在尽可能接近 1 毫秒。
考虑到这些更改,我们来查看下面的服务代码:
#property service #property copyright "Daniel Jose" #property version "1.00" //+------------------------------------------------------------------+ #include <Market Replay\C_Replay.mqh> //+------------------------------------------------------------------+ input string user01 = "WINZ21_202110220900_202110221759"; //File with ticks //+------------------------------------------------------------------+ C_Replay Replay; //+------------------------------------------------------------------+ void OnStart() { ulong t1; int delay = 3; if (!Replay.CreateSymbolReplay(user01)) return; Print("Waiting for permission to start replay ..."); GlobalVariableTemp(def_GlobalVariable01); while (!GlobalVariableCheck(def_SymbolReplay)) Sleep(750); Print("Replay service started ..."); t1 = GetTickCount64(); while (GlobalVariableCheck(def_SymbolReplay)) { if ((GetTickCount64() - t1) >= (uint)(delay)) { if ((delay = Replay.Event_OnTime()) < 0) break; t1 = GetTickCount64(); } } GlobalVariableDel(def_GlobalVariable01); Print("Replay service finished ..."); } //+------------------------------------------------------------------+
上面的代码负责创建柱线。 把这段代码放在此处,我们令回放系统独立运行:MetaTrader 5 平台的操作几乎不会影响或受其影响。 故此,我们能操控其它与控制系统、回放分析和模拟相关的事情。 但这将在以后完成。
现在遇到了一件有趣的事情:请注意,高亮显示的部分给出了 GetTickCount64 函数。这将提供一个系统,等同于我们在上一篇文章中看到的系统,但有一个优点:这里的分辨率将下沉到 1 毫秒的时间。 这个精度并不准确,它只是近似,但近似水平非常接近真实的行情走势。 这不取决于您使用的硬件。 毕竟,您甚至可以创建一个可以保证更高精度的循环,但这将是非常费力的,因为这次它将取决于所使用的硬件。
接下来要做的是以下脚本。 此为它的完整代码:
#property copyright "Daniel Jose" #property version "1.00" //+------------------------------------------------------------------+ #include <Market Replay\C_Replay.mqh> //+------------------------------------------------------------------+ C_Replay Replay; //+------------------------------------------------------------------+ void OnStart() { Print("Waiting for the Replay System ..."); while((!GlobalVariableCheck(def_GlobalVariable01)) && (!IsStopped())) Sleep(500); if (IsStopped()) return; Replay.ViewReplay(); GlobalVariableTemp(def_SymbolReplay); while ((!IsStopped()) && (GlobalVariableCheck(def_GlobalVariable01))) Sleep(500); GlobalVariableDel(def_SymbolReplay); Print("Replay Script finished..."); Replay.CloseReplay(); } //+------------------------------------------------------------------+
如您所见,这两段代码都非常简单。 不过,它们通过平台支持的全局变量相互通信。 故此,我们得到以下逻辑概念:
这些逻辑概念将由平台本身维护。 如果脚本关闭,服务将停止。 如果服务停止,那么我们执行回放系统的品种就会停止接收数据。 这令得它超级简单,且高度可持续。 任何改进(平台和硬件)都会自动反映在整体性能中。 这并非什么奇迹 — 这一切都是由于服务进程执行的每个操作期间发生的微小延迟而达成的。 只有这样才能真正影响系统,我们不需要担心将来要开发的脚本或 EA。 任何改进只会影响服务。
为了省去测试系统的麻烦,您可以在下图中预览结果。 因此,亲爱的读者,您不必等待整整一分钟即可在图表上看到结果。
如您所见,结果非常接近理想。 超额的 9 秒可以利用系统设置轻易消除。 理想情况下,时间应该小于 1 分钟,这将令调整事宜变得更加容易,因为我们只需要向系统添加延迟。 增加延迟比减少延迟更容易。 但如果您认为系统时间不能减少,那么我们就要仔细查看。
有一处会在系统中产生延迟,即在服务之中。 下面代码中高亮显示的就是实际生成延迟的这一处。 但如果我们把这一行注释掉呢? 系统会发生什么变化?
t1 = GetTickCount64(); while (GlobalVariableCheck(def_SymbolReplay)) { // ... COMMENT ... if ((GetTickCount64() - t1) >= (uint)(delay)) { if ((delay = Replay.Event_OnTime()) < 0) break; t1 = GetTickCount64(); } } GlobalVariableDel(def_GlobalVariable01);
突出显示的行将不再执行。 在这种情况下,我将为您节省在本地测试系统时每次等待的一分钟。 执行结果在下面的视频中展示。 您可以完整观看它,或跳转到显示最终结果的部分。 您可随意做出选择。
也就是说,最大的挑战是正确生成延迟。 但是创建 1 分钟柱线的微小时间偏差并不是真正的问题。 因为即使在实盘账户上,我们也没有确切的时间,因为信息传输亦存在延迟。 这种延迟非常小,但它仍然存在。
最大速度。 真的吗?
在此,我们将进行最后一次尝试,令系统在低于 1 分钟的时间内运行。
当您查看毫秒值时,您会注意到有时我们在一行和另一行之间的差异仅为 1 毫秒。 但我们将在同一秒内处理所有内容。 因此,我们可以对代码进行一些小的修改。 我们将在其中添加一个循环,而这可能会对整个系统产生非常大的影响。
修改如下所示:
#define macroGetMin(A) (int)((A - (A - ((A % 3600) - (A % 60)))) / 60) inline int Event_OnTime(void) { bool isNew; int mili; static datetime _dt = 0; if (m_ReplayCount >= m_ArrayCount) return -1; if (m_dt == 0) { m_Rate[0].close = m_Rate[0].open = m_Rate[0].high = m_Rate[0].low = m_ArrayInfoTicks[m_ReplayCount].Last; m_Rate[0].tick_volume = 0; m_Rate[0].time = m_ArrayInfoTicks[m_ReplayCount].dt - 60; CustomRatesUpdate(def_SymbolReplay, m_Rate, 1); _dt = TimeLocal(); } isNew = m_dt != m_ArrayInfoTicks[m_ReplayCount].dt; m_dt = (isNew ? m_ArrayInfoTicks[m_ReplayCount].dt : m_dt); mili = m_ArrayInfoTicks[m_ReplayCount].milisec; do { while (mili == m_ArrayInfoTicks[m_ReplayCount].milisec) { m_Rate[0].close = m_ArrayInfoTicks[m_ReplayCount].Last; m_Rate[0].open = (isNew ? m_Rate[0].close : m_Rate[0].open); m_Rate[0].high = (isNew || (m_Rate[0].close > m_Rate[0].high) ? m_Rate[0].close : m_Rate[0].high); m_Rate[0].low = (isNew || (m_Rate[0].close < m_Rate[0].low) ? m_Rate[0].close : m_Rate[0].low); m_Rate[0].tick_volume = (isNew ? m_ArrayInfoTicks[m_ReplayCount].Vol : m_Rate[0].tick_volume + m_ArrayInfoTicks[m_ReplayCount].Vol); isNew = false; m_ReplayCount++; } mili++; }while (mili == m_ArrayInfoTicks[m_ReplayCount].milisec); m_Rate[0].time = m_dt; CustomRatesUpdate(def_SymbolReplay, m_Rate, 1); mili = (m_ArrayInfoTicks[m_ReplayCount].milisec < mili ? m_ArrayInfoTicks[m_ReplayCount].milisec + (1000 - mili) : m_ArrayInfoTicks[m_ReplayCount].milisec - mili); if ((macroGetMin(m_dt) == 1) && (_dt > 0)) { Print("Elapsed time: ", TimeToString(TimeLocal() - _dt, TIME_SECONDS)); _dt = 0; } return (mili < 0 ? 0 : mili); }; #undef macroGetMin
如果您注意到,我们现在得到一个外循环来做这个 1 毫秒的测试。 由于很难在系统内进行正确的调整,如此我们利用这一毫秒的优势,也许最好将其从播放中取出。
我们只做了一处修改。 您可以在下面的视频中看到结果。
对于那些想要一切都更快的人,看看结果:
我认为这就足够了。 我们已在下面创建了一个 1 分钟柱线。 我们可以进行调整以达到最佳时间,从而增加系统的延迟。 但我们不会这样做,因为我们的想法是建立一个允许我们进行模拟研究的系统。 任何接近 1 分钟的东西对于训练和练习都很不错。 它不一定是确切的东西。
结束语
现在我们已经掌握了正在创建的回放系统的基础知识,我们能够移到下一处。 看到所有问题都已利用 MQL5 语言中的设置和函数来解决了,这证明它实际上可以做的比许多人想象的要多得多。
但请注意,我们的工作才刚刚开始。 还有很多工作要做。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10551