English Русский Español Deutsch 日本語 Português
preview
开发回放系统 — 市场模拟(第 25 部分):为下一步做准备

开发回放系统 — 市场模拟(第 25 部分):为下一步做准备

MetaTrader 5测试者 | 2 五月 2024, 09:33
274 0
Daniel Jose
Daniel Jose

概述

在上一篇文章《开发回放系统 — 市场模拟(第 24 部分):外汇(V)》中,我演示了如何把初看似乎不同的两个宇宙和谐地糅合。一只手上有个市场是基于出价的图表,另一只手上则是一个基于最后成交价的市场。我们的目的是创建一种方法来模拟、或更确切地说生成可能的价格走势,即通过计数柱线,完美表达 1-分钟图表时序。这是一个非常有趣的挑战。呈现的解决方案虽然有效,但并非达成这一具体目标的唯一途径。不过,鉴于该方案被证明是有效的,我认为此阶段已完成。直到它无法求解特定模型。在这种情况下,我们要再次改进所提议的解决方案,如此它便可以涵盖未求解的模型。

某些调整我们尚未施行。尽管实际上这不需要真的进行更改,但剔除一些也许会严重干扰我们的元素,并实现我们仍然期待的功能。其中一个层面是时间回溯能力。利用控件回溯时间的问题很麻烦,必须从系统中删除。从长远来看,该功能被证明是不切实际的,尽管它尚未引发问题,但我们以后实现新功能时肯定会引发问题。您可能会发现利用控件回溯时间的想法很有趣。我实际上同意这是一个有趣的概念,但实践中它并无大用。在许多状况下,处理时间回溯能力导致的问题会令人头脑发胀。

修复此功能并不是特别困难,只是有点乏味,因为它需要添加控件指示的测试和检查。我还在考虑从系统中删除另一些元素。我将在本文中做出这个决定。控件指示中的这种变化之外,我还重点关注其它一些需要改进的问题,以便令服务的作用更有效。我邀请您关注本文中讲述的发展,这将带来非常丰富的信息。今天,我们将涵盖许多有趣的概念,这些肯定会有益于您学习编程和系统开发。我们从本文的第一个主题开始。


限制控件指示器的使用

我们从给控件指示器施加限制开始,如此用户就无法回溯时间。我所说的“回溯时间”是指在取得一定进展后,它不再可能利用控件指示器返回到以前的位置。若要撤消这些动作,您需要关闭回放/模拟服务,并重启该过程从头开始。我知道这个限制也许看起来令人生畏,但相信我,这种方式能防止试图使用回溯功能时也许会出现的诸多问题。

实现该限制并不困难,但确实需要一些努力,因为您需要往系统里添加特定的测试。必须谨慎使用这些测试,以免与指示器的其它函数发生冲突,令其有效工作。我们将此任务分解为若干步骤,以便更轻松地以有效的方式实现更改。


入门:打开和关闭优调按钮

在这个阶段,任务相对简单。它涉及启用或禁用访问位于控件面板末端的优调按钮。这些按钮如下图所示:

图例 01

图例 01:优调按钮

这些按钮可以更轻松地优调所需的推进速度。据其我们可以非常精准地向前或向后移动一段时间,这非常实用。不过,为了防止用户时间回溯,隐藏或显示这些按钮很重要,因为它们的存在是必要的。为了更好地理解这一步,请思考一下:如果系统没有推进任何单项,为什么要保持左侧的按钮处于激活状态?或者为什么当系统达到其最大推进限制时,我们需要右侧的按钮?如此,我们有了最后生成的凭证。如此,为什么我们需要右侧的按钮呢?如此,没有必要保留它吗?因此,该阶段的目的是告知用户不能向前或向后移动超过既定的限制。

这项任务既轻松又简单,因为主要是检查限制。如果我们达到极限,且走势无法更进一步,我们应该禁用按钮显示。不过,我将采用稍微不同的方式,我认为这会令结果更有趣。首先,我们不需要编写大量代码,只需少量修改即可。第一步涉及包括位图作为控件指示器资源,可在禁用按钮时代表按钮。具体操作如下:

#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_ButtonLeftBlock     "Images\\Market Replay\\Left_Block.bmp"
#define def_ButtonRight         "Images\\Market Replay\\Right.bmp"
#define def_ButtonRightBlock    "Images\\Market Replay\\Right_Block.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_ButtonLeftBlock
#resource "\\" + def_ButtonRight
#resource "\\" + def_ButtonRightBlock
#resource "\\" + def_ButtonPin
#resource "\\" + def_ButtonWait

这些行在控件指示器内添加位图,在极限处符号化禁用的按钮。这令界面更具吸引力,允许您如愿给用户创建外观更一致的按钮。随意进行更改。一旦此步完成后,我们需要引用这些数值。代码差不多已就绪,我们只需要引用这些资源。这是在下面的代码中完成的:

void CreteCtrlSlider(void)
   {
      u_Interprocess Info;
                                
      m_Slider.szBarSlider = def_NameObjectsSlider + " Bar";
      m_Slider.szBtnLeft   = def_NameObjectsSlider + " BtnL";
      m_Slider.szBtnRight  = def_NameObjectsSlider + " BtnR";
      m_Slider.szBtnPin    = def_NameObjectsSlider + " BtnP";
      m_Slider.posY = 40;
      CreteBarSlider(82, 436);
      CreateObjectBitMap(52, 25, m_Slider.szBtnLeft, def_ButtonLeft, def_ButtonLeftBlock);
      CreateObjectBitMap(516, 25, m_Slider.szBtnRight, def_ButtonRight, def_ButtonRightBlock);
      CreateObjectBitMap(def_MinPosXPin, m_Slider.posY, m_Slider.szBtnPin, def_ButtonPin);
      ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_ANCHOR, ANCHOR_CENTER);
      if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.u_Value.df_Value = 0;
      PositionPinSlider(Info.s_Infos.iPosShift);

插入这些引用允许负责按钮的对象按其方式处理它们,以便达成期望的结果。请注意,到目前为止,除了资源链接之外,我还没有添加任何其它内容,系统现在可以执行预期的函数。不过,为了在达到调整限制时更改按钮,我们需要添加更多代码。但不用担心,这是一项非常简单、且直接了当的任务。所需代码如下所示:

inline void PositionPinSlider(int p, const int minimal = 0)
   {
      m_Slider.posPinSlider = (p < 0 ? 0 : (p > def_MaxPosSlider ? def_MaxPosSlider : p));
      ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_XDISTANCE, m_Slider.posPinSlider + def_MinPosXPin);
      ObjectSetInteger(m_id, m_Slider.szBtnLeft, OBJPROP_STATE, m_Slider.posPinSlider != minimal);
      ObjectSetInteger(m_id, m_Slider.szBtnRight, OBJPROP_STATE, m_Slider.posPinSlider < def_MaxPosSlider);
      ChartRedraw();
   }

我在调用中引入了一个新参数,但由于我们最初将在标准模式下使用该系统,因此开始时该参数为零值。这意味着此刻不需要更改。之后,我们可以开始测试某些状况下的极限。若要启用或禁用左侧的控件按钮,我们将调用一个计算。若要关闭右上角的控件按钮,我们要应用另一个计算。在右键单击的情况下,计算将仅考虑滑块是否已达到上限。不过,左侧按钮的作用会有所不同,最初仅基于零值。编译控件指示器代码,并运行回放/模拟服务之后,我们将看到以下动画中演示的行为:

动画 01

动画 01:系统按钮开/关演示

该解决方案非常易于理解和实现,是我们真正需要开发内容的一个良好起点。现在,我们面临着一项稍微困难的任务,然而对于用户明了正在发生的事情是必要的。我们将在下一个主题中详细研究这个问题。


通知用户有关限制更改的信息

我们可令该过程变得非常简单,即当滑块达到回放/模拟服务指定的最小点时,简单地打开和关闭左侧按钮。不过,这也许会令用户感到困惑,用户将无法控件后移,即移到零点。为了更好地理解,请观看下面的动画:

动画 02

动画 02:为什么我到不了零?

动画 02 清晰地展示出当滑块未达到零时用户也许会经历的混乱,即使左侧按钮指示不可移动。这种状况表明,目前的指示还不够明确。因此,我们需要改进有关现有限制或极限的通知,因此滑块无法移动到某个点之外。现在,在详细讲述如何实现该指示之前,您也许想知道使用什么方法在到达零点之前锁定控件。好奇心是伟大的!我没有求助于任何复杂的软件技巧;我只是识别到一个停止点。但在何处?位置如下图所示:

inline void PositionPinSlider(int p, const int minimal = 0)
   {
      m_Slider.posPinSlider = (p < minimal ? minimal : (p > def_MaxPosSlider ? def_MaxPosSlider : p));
      ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_XDISTANCE, m_Slider.posPinSlider + def_MinPosXPin);
      ObjectSetInteger(m_id, m_Slider.szBtnLeft, OBJPROP_STATE, m_Slider.posPinSlider != minimal);
      ObjectSetInteger(m_id, m_Slider.szBtnRight, OBJPROP_STATE, m_Slider.posPinSlider < def_MaxPosSlider);
      ChartRedraw();
   }

您一定想知道,“此处干了啥?” 别担心,有一个微妙但重要的细节需要注意:变量 minimal 置为零。如果我们将这个值更改为 100 或 80,会发生什么?此时检查该值则会禁用左上角的按钮。不过,如果用户单击左侧、或把滑块向左拖动,这不会阻止系统递减该数值。这是正确的。不过,现在我将滑块设置为由 minimal 变量精确定义的位置。现在清楚了吗?无论用户尝试移动滑块、或按左侧按钮多少次,指定的点都不会落在所设置的最小值之下。

挺有趣,是不是?判定最小可能值是回放/模拟服务的任务,它会随着回放或模拟的进度而自动调整该值。然而,如果服务未更改可用的最小值,则用户可以更改此点。这也许看起来很复杂,但它比您想象的要容易。我们稍后会深入这个话题。现在,我们关注动画 02 提出的问题,它展示的是缺乏向用户明确指示左侧限制。有若干种方式可以做到这一点,有些从美学上看似很奇怪,而另一些则有点诡异。我们可以选择一个过渡方案。创建一面墙壁通知怎么样?在我看来,这是一个聪明的选择,因为它可以提供有趣的视角。如果您是一位图形艺术家,成果也许比此处显示的更佳。我们将用到以下代码:

inline void CreteBarSlider(int x, int size)
   {
      ObjectCreate(m_id, m_Slider.szBarSlider, OBJ_RECTANGLE_LABEL, 0, 0, 0);
      ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_XDISTANCE, x);
      ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_YDISTANCE, m_Slider.posY - 4);
      ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_XSIZE, size);
      ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_YSIZE, 9);
      ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_BGCOLOR, clrLightSkyBlue);
      ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_BORDER_COLOR, clrBlack);
      ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_WIDTH, 3);
      ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_BORDER_TYPE, BORDER_FLAT);
//---
      ObjectCreate(m_id, m_Slider.szBarSliderBlock, OBJ_RECTANGLE_LABEL, 0, 0, 0);
      ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_XDISTANCE, x);
      ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_YDISTANCE, m_Slider.posY - 9);
      ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_YSIZE, 19);
      ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_BGCOLOR, clrRosyBrown);
      ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_BORDER_TYPE, BORDER_RAISED);
   }

绿色高亮显示的行示意创建此下限指示的代码。是的,我们为此用到一个对象。如果您愿意,可以使用位图来得到更具视觉吸引力的成果。我想保持代码简单,出于许多读者的编程知识大抵有限。以这种方式,更易于访问的代码可以更轻松地理解所有内容如何实现。添加位图、甚至纹理图案很容易,结果可能非常有趣,尤其是在使用 DirectX 编程时。是的,MQL5 允许这样做。但我们会把它留到其它时间。现在,我们保持简单而实用。成果展示在下面的动画 03 当中:

动画 03

动画 03:现在我们有了左侧极限。

左限位指示器面板的引入令用户更容易理解为什么他们无法回溯回放/模拟。不过,您也许已经注意到,上面的代码没有明确如何调整左限栏的大小。定义此尺寸的代码如下所示:

inline void PositionPinSlider(int p, const int minimal = 0)
   {
      m_Slider.posPinSlider = (p < minimal ? minimal : (p > def_MaxPosSlider ? def_MaxPosSlider : p));
      ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_XDISTANCE, m_Slider.posPinSlider + def_MinPosXPin);
      ObjectSetInteger(m_id, m_Slider.szBtnLeft, OBJPROP_STATE, m_Slider.posPinSlider != minimal);
      ObjectSetInteger(m_id, m_Slider.szBtnRight, OBJPROP_STATE, m_Slider.posPinSlider < def_MaxPosSlider);
      ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_XSIZE, minimal + 2);
      ChartRedraw();
   }

指示栏的尺寸由 minimal 变量决定。当回放/建模服务更新其数据时,柱线将相应地调整。现在,下一步是确保回放/模拟服务正确更新该限制。下一个主题将专注这个话题。


与回放/模拟服务交谈

现在系统的核心已经设置为不允许用户在控件提示出现时回溯时间,我们需要回放/模拟服务来告诉控件指示器用户在什么时候不能再时间回溯。比之我们已经完成的任务,这项任务相对容易。最主要的是检查暂停时回放/模拟服务的当前位置。这部分很简单。我们看看如何实现必要的功能。最初,您需要对代码进行一些小的修改,现在如下所示:

class C_Controls
{
   private :
//+------------------------------------------------------------------+
      string  m_szBtnPlay;
      long    m_id;
      bool    m_bWait;
      struct st_00
      {
         string  szBtnLeft,
                 szBtnRight,
                 szBtnPin,
                 szBarSlider,
                 szBarSliderBlock;
         int     posPinSlider,
                 posY,
                 Minimal;
      }m_Slider;
//+------------------------------------------------------------------+
      void CreteCtrlSlider(void)
         {
            u_Interprocess Info;
                                
            m_Slider.szBarSlider      = def_NameObjectsSlider + " Bar";
            m_Slider.szBarSliderBlock = def_NameObjectsSlider + " Bar Block";
            m_Slider.szBtnLeft        = def_NameObjectsSlider + " BtnL";
            m_Slider.szBtnRight       = def_NameObjectsSlider + " BtnR";
            m_Slider.szBtnPin         = def_NameObjectsSlider + " BtnP";
            m_Slider.posY = 40;
            CreteBarSlider(82, 436);
            CreateObjectBitMap(52, 25, m_Slider.szBtnLeft, def_ButtonLeft, def_ButtonLeftBlock);
            CreateObjectBitMap(516, 25, m_Slider.szBtnRight, def_ButtonRight, def_ButtonRightBlock);
            CreateObjectBitMap(def_MinPosXPin, m_Slider.posY, m_Slider.szBtnPin, def_ButtonPin);
            ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_ANCHOR, ANCHOR_CENTER);
            if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.u_Value.df_Value = 0;
            m_Slider.Minimal = Info.s_Infos.iPosShift;
            PositionPinSlider(Info.s_Infos.iPosShift);
         }
//+------------------------------------------------------------------+
inline void PositionPinSlider(int p, const int minimal = 0)
         {
            m_Slider.posPinSlider = (p < minimal ? minimal : (p > def_MaxPosSlider ? def_MaxPosSlider : p));
            m_Slider.posPinSlider = (p < m_Slider.Minimal ? m_Slider.Minimal : (p > def_MaxPosSlider ? def_MaxPosSlider : p));
            ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_XDISTANCE, m_Slider.posPinSlider + def_MinPosXPin);
            ObjectSetInteger(m_id, m_Slider.szBtnLeft, OBJPROP_STATE, m_Slider.posPinSlider != minimal);
            ObjectSetInteger(m_id, m_Slider.szBtnLeft, OBJPROP_STATE, m_Slider.posPinSlider != m_Slider.Minimal);
            ObjectSetInteger(m_id, m_Slider.szBtnRight, OBJPROP_STATE, m_Slider.posPinSlider < def_MaxPosSlider);
            ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_XSIZE, minimal + 2);
            ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_XSIZE, m_Slider.Minimal + 2);
            ChartRedraw();
         }
//+------------------------------------------------------------------+

您会注意到代码经过了一些简单的调整,这足以确保正确创建和配置限制栏,以及控件按钮。为此,我们必须从函数调用中移动变量,并将其放置在结构之中。它将在代码中的某个位置被初始化,以便将来在适当的位置访问它。我为什么选择这种方式?这样做是为了避免在代码中的其它位置进行调整。每次挂起回放/模拟服务时,都会调用 CreateCtrlSlider 函数。即使某些对象已被销毁,仍然会调用该函数,这将简化整个创建逻辑。

现在我们已经解决了控件指示器问题,是时候专注于回放/模拟服务代码,并进行一些修改了。虽然其中许多修改本质上更具美感,但重要的是在解决更复杂的问题之前确保系统平稳运行。


解决回放/模拟服务中的美化问题

我们第一个需要解决的问题不仅仅是美化问题,而且是技术问题。当要求回放/模拟服务在重播开始之前移动到未来位置时,就会发生这种情况。换言之,如果您刚刚开启服务,不先播放,而是决定将图表向前移动一段位置,然后再启动播放,那么图表的正确显示就会出问题。若要修复该问题,您需要强制系统执行“虚假播放”,然后移动到滑块指示的位置。所需的代码修改如下所示:

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) && (m_Ticks.ModePlot == PRICE_EXCHANGE))
         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)
      {
      CreateBarInReplay(true);
      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++);
         nCount = CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, nCount);
      }
      for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag);) CreateBarInReplay(false);
      CustomTicksAdd(def_SymbolReplay, m_Ticks.Info, m_ReplayCount);
      Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
      Info.s_Infos.isWait = false;
      GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
   }

该段代码调用对于创建所需的“虚假播放”至关重要。如果没有此调用,将出现图表绘制错误。此外,我在代码中额外加了一行,将缺失的跳价添加到市场观察窗口之中,从而提供更真实和有趣的回放。代码当中的其它修改,可从划掉的行中可以看出。如果我们处于相同的走势位置,这些检查也会阻止我们进入系统。这就是我们决定不允许用户回溯时间的直接后果,因此与此功能相关的代码就可删除了。

现在我们已修复了这个缺陷,我们专注美化问题已经很长时间了,但我们现在有机会通过使用户经验回放/模拟服务更加愉快来解决它。当选择文件来表示图表上的之前柱线时,会发生这类美化问题。通过回放/模拟服务打开图表时,最初不会显示价格线。虽然这不会影响系统的功能,但从美学的角度来看,观察没有价格线的图表是不太方便的。若要修复或应对此部分,有必要进行一些修改。这些修改的第一处如下所示:

bool LoopEventOnTime(const bool bViewBuider)
   {
      u_Interprocess Info;
      int iPos, iTest;
                                
      if (!m_Infos.bInit) ViewInfos();
      if (!m_Infos.bInit)
      {
         ChartSetInteger(m_IdReplay, CHART_SHOW_ASK_LINE, m_Ticks.ModePlot == PRICE_FOREX);
         ChartSetInteger(m_IdReplay, CHART_SHOW_BID_LINE, m_Ticks.ModePlot == PRICE_FOREX);
         ChartSetInteger(m_IdReplay, CHART_SHOW_LAST_LINE, m_Ticks.ModePlot == PRICE_EXCHANGE);
         m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE);
         m_MountBar.Rate[0].time = 0;
         m_Infos.bInit = true;
         ChartRedraw(m_IdReplay);
      }
      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);
      iPos = 0;
      while ((m_ReplayCount < m_Ticks.nTicks) && (!_StopFlag))
      {
         iPos += (int)(m_ReplayCount < (m_Ticks.nTicks - 1) ? m_Ticks.Info[m_ReplayCount + 1].time_msc - m_Ticks.Info[m_ReplayCount].time_msc : 0);
         CreateBarInReplay(true);
         while ((iPos > 200) && (!_StopFlag))
         {
            if (ChartSymbol(m_IdReplay) == "") return false;
            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(195);
            iPos -= 200;
         }
      }                               
      return (m_ReplayCount == m_Ticks.nTicks);
   }                               

我们删除代码中划掉的部分,并加入高亮显示的新行。我们可以在此处包含调用的代码,但将来该段代码很可能会移至另一个函数。因此,为了便于将来的可移植性,我更愿意在其它地方集合所需的代码。

为了解决回放/模拟服务打开图表时不立即显示价格线的美化问题,需要以下代码:

void ViewInfos(void)
   {
      MqlRates Rate[1];
                                
      ChartSetInteger(m_IdReplay, CHART_SHOW_ASK_LINE, m_Ticks.ModePlot == PRICE_FOREX);
      ChartSetInteger(m_IdReplay, CHART_SHOW_BID_LINE, m_Ticks.ModePlot == PRICE_FOREX);
      ChartSetInteger(m_IdReplay, CHART_SHOW_LAST_LINE, m_Ticks.ModePlot == PRICE_EXCHANGE);
      m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE);
      m_MountBar.Rate[0].time = 0;
      m_Infos.bInit = true;
      CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, Rate);
      if ((m_ReplayCount == 0) && (m_Ticks.ModePlot == PRICE_EXCHANGE))

         for (; m_Ticks.Info[m_ReplayCount].volume_real == 0; m_ReplayCount++);
      if (Rate[0].close > 0)
      {
         if (m_Ticks.ModePlot == PRICE_EXCHANGE) m_Infos.tick[0].last = Rate[0].close; else
         {
            m_Infos.tick[0].bid = Rate[0].close;
            m_Infos.tick[0].ask = Rate[0].close + (Rate[0].spread * m_Infos.PointsPerTick);
         }                                       
         m_Infos.tick[0].time = Rate[0].time;
         m_Infos.tick[0].time_msc = Rate[0].time * 1000;
      }else
         m_Infos.tick[0] = m_Ticks.Info[m_ReplayCount];
      CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
      ChartRedraw(m_IdReplay);
   }

这些代码行在之前的函数中被划掉了。但应注意所需的那些额外行。我们所做的是调用处理指式器的通用函数来识别回放/模拟服务放置在图表上的最后一根柱线。如果我们设法捕获柱线,也就是说,如果收盘价大于零,我们将根据所采用的构造模式设置一个特殊的跳价。如果收盘价为零,我们就用加载或模拟的跳价列表中的第一个有效跳价。负责搜索有效跳价的函数恰好就在上述两行里。该函数在配合“最后成交价绘制模式工作时特别实用,因为在出价模式下,第一次跳价已生效。最终,这个特别创建的跳价将显示在市场观察窗口当中,导致服务命令 MetaTrader 5 平台打开图表时,价格线立即出现在图表上。

我们需要另一处修改,因为我们在回放/建模方面还存在问题,尽管有些不可靠,但在呈现数据集之前没有指向任何柱线。在这种情况下,可以第一根柱线的构造会被截断。为了最终解决这个问题,我们需要指定柱线,作为稍后呈现整个集合的预览。该段修改代码将允许系统在具有不同时间间隔的图表上正常工作:从一分钟到一天,甚至一周。好吧,我猜一个月就太多了。

inline void FirstBarNULL(void)
   {
      MqlRates rate[1];
      int c0 = 0;
                                
      for(; (m_Ticks.ModePlot == PRICE_EXCHANGE) && (m_Ticks.Info[c0].volume_real == 0); c0++);
      rate[0].close = (m_Ticks.ModePlot == PRICE_EXCHANGE ? m_Ticks.Info[c0].last : m_Ticks.Info[c0].bid);
      rate[0].open = rate[0].high = rate[0].low = rate[0].close;
      rate[0].tick_volume = 0;
      rate[0].real_volume = 0;
      rate[0].time = macroRemoveSec(m_Ticks.Info[c0].time) - 86400;
      CustomRatesUpdate(def_SymbolReplay, rate);
      m_ReplayCount = 0;
   }

第一步是找到一个有效的跳价,特别是如果图表系统使用最后成交价。一旦该操作完成后,我们将用跳价系列的第一个有效价格创建前一根柱线,其将用于回放或模拟。这里的重点是时间位置的指示,通过减去一天的值(以分钟为单位)来校正。这可确保前一根柱线正确显示在图表上,且即使在日线图上也完全可见。该系统对外汇市场数据和股票市场都有效。


结束语

使用所提供的附件,您可以测试回放/模拟服务的当前实现。基本系统已经准备就绪,但鉴于我们尚未用到某些函数,因此需要进行额外的修改及调整,从而令系统适配更有效的训练模式。在这一点上,我们认为回放/模拟系统是完整的。在接下来的几篇文章中,我们将视察进一步改进它的方式,为该系统的发展开启一个新阶段。

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

附加的文件 |
Files_-_BOLSA.zip (1358.24 KB)
Files_-_FOREX.zip (3743.96 KB)
Files_-_FUTUROS.zip (11397.51 KB)
为 MetaTrader 5 开发MQTT客户端:TDD方法——第3部分 为 MetaTrader 5 开发MQTT客户端:TDD方法——第3部分
本文是一系列文章的第三部分,介绍了我们为MQTT协议开发本机MQL5客户端的步骤。在这一部分中,我们详细描述了如何使用测试驱动开发来实现CONNECT/CONNACK数据包交换的操作行为部分。在这一步骤结束时,我们的客户端必须能够在处理连接尝试可能产生的任何服务器结果时表现得正常。
MQL5中的范畴论(第21部分):使用LDA的自然变换 MQL5中的范畴论(第21部分):使用LDA的自然变换
这篇文章是我们系列的第21篇,继续研究自然变换以及如何使用线性判别分析(linear discriminant analysis,LDA)来实现它们。我们以信号类格式展示了它的应用程序,就像在前一篇文章中一样。
MQL5中的范畴论(第22部分):对移动平均的不同看法 MQL5中的范畴论(第22部分):对移动平均的不同看法
在本文中,我们尝试通过只关注一个指标来简化对这些系列中所涵盖概念的说明,这是最常见的,可能也是最容易理解的。它就是移动平均。在这样做的时候,我们会探讨垂直自然变换的意义和可能的应用。
开发回放系统 — 市场模拟(第 24 部分):外汇(V) 开发回放系统 — 市场模拟(第 24 部分):外汇(V)
今天,我们将去除阻止基于最后成交价进行模拟的限制,并将专门针对这类模拟引入一个新的切入点。整个操作机制将基于外汇市场的原则。该过程的主要区别在于出价(Bid)和最后成交价(Last)模拟的分离。不过,重点要注意,用于随机化时间,并将其调整为与 C_Replay 类兼容的方法在两类模拟中保持雷同。这很好,因为一种模式的变化会导致另一种模式的自动改进,尤其遇到处理跳价之间的时间。