English Русский Español Deutsch 日本語 Português
preview
开发回放系统 — 市场模拟(第 23 部分):外汇(IV)

开发回放系统 — 市场模拟(第 23 部分):外汇(IV)

MetaTrader 5测试者 | 11 四月 2024, 13:36
506 0
Daniel Jose
Daniel Jose

概述

在上一篇文章《开发回放系统 — 市场模拟(第 22 部分):外汇(III)》中,我们对系统进行了一些更改,从而令模拟器能够根据出价(Bid)生成信息,而不仅仅是基于最后成交价。但这些修改不如我意,原因很简单:我们正在重复代码,这根本不符合我的风格。

在那篇文章当中,我明确表达了我的不满:

"... 不要问我为什么。但出于我个人不知道的一些奇怪原因,我们只得在此加上这一行。如果您不加上它,则跳价交易量中所示的数值就不对。注意函数中有一个条件。这可避免快速定位系统在使用当中出现问题,并防止在系统图表上出现不合时宜的奇怪柱线。虽然这是一个非常奇怪的原因,但其它一切都按预期工作。这将是一种新的计算,其中我们将按相同的方式计数跳价 — 无论是采用基于出价的资产,亦或采用基于最后成交价的金融产品。

不过,鉴于文章的代码都是现成的,文章也快完结了,故我把一切都保持原样,但这真的让我很烦恼。代码在某些情况下起作用,而在其它情况下无效是没有意义的。即使调试代码,并试图找到错误的原因,我也没能找到。但是在暂时搁置代码,并查看系统流程图(是的,您应该始终尝试使用流程图来加快编码速度)之后,我注意到我可以修改一些代码以避免重复。更糟糕的是,代码其实是重复的。这导致了一个我无法解决的问题。但有一个解决方案,我们将从这个问题的解决方案开始本文,因为其存在令我们无法正确编写模拟器代码来处理外汇市场数据。


解决跳价交易量问题

在本主题中,我将展示如何解决导致跳价交易量失败的问题。首先,我必须修改跳价读取代码,如下所示:

datetime LoadTicks(const string szFileNameCSV, const bool ToReplay = true)
    {
        int      MemNRates,
                 MemNTicks;
        datetime dtRet = TimeCurrent();
        MqlRates RatesLocal[],
                 rate;
        bool     bNew;
        
        MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate);
        MemNTicks = m_Ticks.nTicks;
        if (!Open(szFileNameCSV)) return 0;
        if (!ReadAllsTicks(ToReplay)) return 0;         
        rate.time = 0;
        for (int c0 = MemNTicks; c0 < m_Ticks.nTicks; c0++)
        {
            if (!BuildBar1Min(c0, rate, bNew)) continue;
            if (bNew) ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
            m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate;
        }
        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);
        }else
        {
            CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TRADE_CALC_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CALC_MODE_EXCH_STOCKS : SYMBOL_CALC_MODE_FOREX);
            CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_CHART_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CHART_MODE_LAST : SYMBOL_CHART_MODE_BID);
        }
        m_Ticks.bTickReal = true;
        
        return dtRet;
    };

以前,该段代码是将跳价转换为 1-分钟柱线代码的一部分,但现在我们将使用不同的代码。原因是现在该调用将服务于多个目的,它所做的工作也用于创建重复柱线。这将避免在创建柱线图的类中重复代码。

我们看一下转换代码:

inline bool BuildBar1Min(const int iArg, MqlRates &rate, bool &bNew)
inline void BuiderBar1Min(const int iFirst)
   {
      MqlRates rate;
      double   dClose = 0;
      bool     bNew;
                                
      rate.time = 0;
      for (int c0 = iFirst; c0 < m_Ticks.nTicks; c0++)
      {
         switch (m_Ticks.ModePlot)
         {
            case PRICE_EXCHANGE:
               if (m_Ticks.Info[c0].last == 0.0) continue;
               if (m_Ticks.Info[iArg].last == 0.0) return false;
               dClose = m_Ticks.Info[c0].last;
               break;
            case PRICE_FOREX:
               dClose = (m_Ticks.Info[c0].bid > 0.0 ? m_Ticks.Info[c0].bid : dClose);
               if ((dClose == 0.0) || (m_Ticks.Info[c0].bid == 0.0)) continue;
               if ((dClose == 0.0) || (m_Ticks.Info[iArg].bid == 0.0)) return false;
               break;
         }
         if (bNew = (rate.time != macroRemoveSec(m_Ticks.Info[c0].time)))
         {
            ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
            rate.time = macroRemoveSec(m_Ticks.Info[c0].time);
            rate.real_volume = 0;
            rate.tick_volume = (m_Ticks.ModePlot == PRICE_FOREX ? 1 : 0);
            rate.open = rate.low = rate.high = rate.close = dClose;
         }else
         {
            rate.close = dClose;
            rate.high = (rate.close > rate.high ? rate.close : rate.high);
            rate.low = (rate.close < rate.low ? rate.close : rate.low);
            rate.real_volume += (long) m_Ticks.Info[c0].volume_real;
            rate.tick_volume++;
         }
         m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate;
      }
      return true;                    
   }

删除了代码中所有划掉的元素,在于它们阻止了正确创建 C_Replay 类中所用的元素。但另一方面,我必须添加这些要点,以便告知调用者转换中发生了什么。

注意,最初该函数在 C_FileTicks 类中是私密的。我修改了它的访问级别,如此它可在 C_Replay 类中使用。尽管如此,我不希望它超出这些限制太远,所以它不会是公开的,而是受保护的。如此这般,我们可以将访问限制在 C_Replay 类允许的最大级别。如您所记,最高级别是 C_Replay 类。因此,只有在 C_Replay 类中声明为公开的过程和函数才能在类外部访问。系统的内部设计必须完全隐藏在这个 C_Replay 类之中。

现在我们看一下新的柱线创建函数。

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

      bool    bNew;
      double  dSpread;
      int     iRand = rand();
                                
      if (BuildBar1Min(m_ReplayCount, def_Rate, bNew))
      {
         m_Infos.tick[0] = m_Ticks.Info[m_ReplayCount];
         if ((!m_Ticks.bTickReal) && (m_Ticks.ModePlot == PRICE_EXCHANGE))
         {                                               
            dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 );
            if (m_Infos.tick[0].last > m_Infos.tick[0].ask)
            {
               m_Infos.tick[0].ask = m_Infos.tick[0].last;
               m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread;
            }else   if (m_Infos.tick[0].last < m_Infos.tick[0].bid)
            {
               m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread;
               m_Infos.tick[0].bid = m_Infos.tick[0].last;
            }
         }
         if (bViewTicks) CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
         CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate);
      }
      m_ReplayCount++;
#undef def_Rate
   }

现在,创建发生在我们将跳价转换为柱线的同一点。以这种方式,如果在转换过程中出现问题,我们就能立即注意到错误。这是因为在快进期间,在图表上放置 1-分钟柱线的代码,也同样在正常表现期间用于定位系统放置柱线。换言之,负责此任务的代码不会在其它任何地方重复。如此这般,我们获得的系统就能更好的维护和改进。但我也希望您注意到我们在上面代码中添加的一些重要内容。只有当我们在模拟系统中模拟股票市场类型的数据时,才会发生模拟出价(Bid)和要价(Ask)。也就是说,如果绘图基于出价(Bid),则模拟不再执行。这对于我们在下一个主题中开始设计非常重要。


我们开始基于出价(外汇模式)的模拟演示。

在以下内容中,我们将专门研究 C_Simulation 类。我们将这样做,以便对当前系统实现未涵盖的数据进行建模。但我们先要做一件小事:

bool BarsToTicks(const string szFileNameCSV)
   {
      C_FileBars *pFileBars;
      int         iMem = m_Ticks.nTicks,
                  iRet;
      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))
      {
         ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary);
         m_Ticks.Rate[++m_Ticks.nRate] = rate[0];
         if ((iRet = Simulation(rate[0], local)) < 0)
         {
            ArrayFree(local);
            delete pFileBars;
            return false;
         }
         for (int c0 = 0; c0 <= iRet; c0++)
         {
            ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
            m_Ticks.Info[m_Ticks.nTicks++] = local[c0];
         }
      }
      ArrayFree(local);
      delete pFileBars;
      m_Ticks.bTickReal = false;
                                
      return ((!_StopFlag) && (iMem != m_Ticks.nTicks));
   }

如果事态走入歧途,我们想彻底关闭系统,我们将需要一种方式来告知其它类本次模拟失败。这样做是最简单方式。不过,我真心不太喜欢我们按这样的方式创建函数。虽然它有效,但它缺少一些我们需要告诉 C_Simulation 类的东西。在分析了代码后,我决定改变函数的工作方式。它需要加以修改,从而避免代码重复。那就忘记以前的函数。虽然它有效,但我们实际上使用以下这个:

int SetSymbolInfos(void)
   {
      int iRet;
                                
      CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, iRet = (m_Ticks.ModePlot == PRICE_EXCHANGE ? 4 : 5));
      CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TRADE_CALC_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CALC_MODE_EXCH_STOCKS : SYMBOL_CALC_MODE_FOREX);
      CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_CHART_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CHART_MODE_LAST : SYMBOL_CHART_MODE_BID);
                                
      return iRet;
   }
//+------------------------------------------------------------------+
   public  :
//+------------------------------------------------------------------+
      bool BarsToTicks(const string szFileNameCSV)
      {
         C_FileBars      *pFileBars;
         C_Simulation    *pSimulator = NULL;
         int             iMem = m_Ticks.nTicks,
                         iRet = -1;
         MqlRates        rate[1];
         MqlTick         local[];
         bool            bInit = false;
                                
         pFileBars = new C_FileBars(szFileNameCSV);
         ArrayResize(local, def_MaxSizeArray);
         Print("Converting bars to ticks. Please wait...");
         while ((*pFileBars).ReadBar(rate) && (!_StopFlag))
         {
            if (!bInit)
            {
               m_Ticks.ModePlot = (rate[0].real_volume > 0 ? PRICE_EXCHANGE : PRICE_FOREX);
               pSimulator = new C_Simulation(SetSymbolInfos());
               bInit = true;
            }
            ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary);
            m_Ticks.Rate[++m_Ticks.nRate] = rate[0];
            if (pSimulator == NULL) iRet = -1; else iRet = (*pSimulator).Simulation(rate[0], local);
            if (iRet < 0) break;
            for (int c0 = 0; c0 <= iRet; c0++)
            {
               ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
               m_Ticks.Info[m_Ticks.nTicks++] = local[c0];
            }
         }
         ArrayFree(local);
         delete pFileBars;
         delete pSimulator;
         m_Ticks.bTickReal = false;
                                
         return ((!_StopFlag) && (iMem != m_Ticks.nTicks) && (iRet > 0));
      }

从我们的目标视角来看,第二种选项更加有效。此外,我们避免了重复代码,主要是因为它能为我们带来以下优点:

  • 剔除 C_Simulation 类的继承。这将令系统更加灵活。
  • 资产数据的初始化,以前仅在使用真实跳价时才会执行。
  • 图形显示中使用与品种对应的宽度。
  • 使用 C_Simulation 类作为指针。这样的话,能更有效地利用系统内存,因为在类完成其工作后,它占用的内存将被释放。
  • 保证函数只有一个入口点和一个出口点。
与上一篇文章相比,有些事情已发生了变化。但我们继续实现 C_Simulation 类。开发 C_Simulation 类的主要细节是,我们可以在系统中拥有任意数量的跳价。虽然这不是大问题(至少目前如此),但困难是在许多情况下,高点和低点之间我们必须覆盖的范围已经远远大于已报告或可以创建的跳价数量。这并未计入若干情况,即从开盘价开始并走向极端值之一的部分,以及从极端值之一开始并走至收盘价的部分。如果我们使用随机游走来实现此计算,那么在很多情况下,这是不可能的。因此,我们只得剔除我们在前几篇文章中创建的随机游走,并开发一种生成跳价的新方法。正如我所说,外汇的问题并不那么明显。

这种方式的问题在于,您必须经常创建并令两种不同的方法尽可能和谐地工作。最糟糕的部分是:在某些情况下,随机游走模拟更接近真实资产中发生的情况。但当我们与低交易量(每分钟少于 500 笔交易)打交道时,随机游走是完全不合适的。在这种情形下,我们可采用一种更奇特的方式来涵盖所有可能的情况。我们要做的第一件事(因为我们需要初始化类)是为类定义一个构造函数,其代码如下所示:

C_Simulation(const int nDigits)
   {
      m_NDigits       = nDigits;
      m_IsPriceBID    = (SymbolInfoInteger(def_SymbolReplay, SYMBOL_CHART_MODE) == SYMBOL_CHART_MODE_BID);
      m_TickSize      = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE);
   }

于此,我们简单地初始化类的私密数据,以免到其它地方查找它。因此,请确保在模拟资产的配置文件中所有设置正确,包括绘图类型。否则,系统中也许会出现奇怪的错误。

现在我们可以开始向前推进了,因为我们已经对类进行了一些基本的初始化。我们开始查看需要解决的问题。首先,我们需要生成一个随机时间值,但这个时间必须能够处理将在 1-分钟柱线上生成的所有跳价。这实际上是最简单的部分实现。但在我们开始创建函数之前,我们需要创建一个特殊类型的过程,如下所示:

template < typename T >
inline T RandomLimit(const T Limit01, const T Limit02)
   {
      T a = (Limit01 > Limit02 ? Limit01 - Limit02 : Limit02 - Limit01);
      return (Limit01 >= Limit02 ? Limit02 : Limit01) + ((T)(((rand() & 32767) / 32737.0) * a));
   }

这个过程到底给我们带来什么?在不了解发生了什么的情况下看到该函数可能会令人惊讶。故此,我将尝试尽可能简单地解释这个函数的实际作用,以及为什么它看起来如此奇怪。

在新代码中,我们需要一种能够在两个极端值之间生成随机值的函数。在某些情况下,我们需要形成该数值作为双精度数据类型,而在其它情况下,我们需要整数值。创建两个几乎相同的过程来执行相同类型的因式分解需要考虑到工作量。为了避免这种情况,我们强制,或者更确切地说,告诉编译器我们需要使用相同的因式分解并重载它,以便在代码中我们可以调用相同的函数,但在可执行形式中,我们实际上会有两个不同的函数。我们为此目特意这样声明 — 它定义了类型,在本例中为字母 T。无论在哪里需要编译器设置类型,都需要重复此操作。因此,您应该注意不要混淆任何东西。编译器进行调整,从而避免强制转换问题。

因此,我们就能始终执行相同的计算,但它会依据所用的变量类型进行调整。编译器会做这件事,因为它将决定哪种类型是正确的。如此这般,无论使用哪种类型,我们都可以在每次调用中生成一个伪随机数,但请注意,两个边界的类型应该是相同的。换句话说,不能将双精度与整数混合使用,也不能将长整数与短整数混合使用。这样行不通。当我们使用类型重载时,这是该方式的唯一限制。

但我们尚未完工。我们创建了上述函数,以避免在 C_Simulation 类的代码中生成宏。现在我们转入下一步 — 生成模拟计时系统。在下面的代码中可以看到:

inline void Simulation_Time(int imax, const MqlRates &rate, MqlTick &tick[])
   {
      for (int c0 = 0, iPos, v0 = (int)(60000 / rate.tick_volume), v1 = 0, v2 = v0; c0 <= imax; c0++, v1 = v2, v2 += v0)
      {
         iPos = RandomLimit(v1, v2);
         tick[c0].time = rate.time + (iPos / 1000);
         tick[c0].time_msc = iPos % 1000;
      }
   }

在此,我们模拟的时间要略微随机。第一眼,这也许看起来很令人困惑。但相信我,这里的时间是随机的,尽管它仍然不符合 C_Replay 类所期望的逻辑。这是因为以毫秒为单位的值设置的不正确。此调整将在稍后进行。此处,我们只希望时间是随机生成的,但限于 1-分钟的柱线之内。但我们如何能做到这一点呢?首先,我们将 60 秒的时间(实际上是 60,000 毫秒)除以需要生成的跳价数量。这个值对我们很重要,因为它会告诉我们将用到的极限范围。之后,在循环的每次迭代中,我们将执行几个简单的赋值。现在,生成随机计时器的秘诀在于循环中的这三行。在第一行中,我们要求编译器生成一个调用,我们将在其中使用整数型数据。此调用将返回指定范围内的值。然后,我们将执行两个非常简单的计算。我们首先将生成的值拟合到分钟柱线时间,然后使用相同的生成值来拟合以毫秒为单位的时间。因此,每次跳价都拥有完全随机的时间值。记住,在这个早期阶段,我们只是在调整时间。此设置的目的是避免过度的可预测性。

继续,我们模拟价格。我再次提醒您,我们将只关注基于出价的绘图系统。然后,我们将链接模拟系统,以便我们有一个更通用的方式来进行此类模拟,涵盖出价和最后成交价。此处,我们重点关注出价。为了在第一步中创建模拟,我们要始终把点差保持在相同的距离。在测试代码是否真的有效之前,我们不会令代码复杂化。第一次模拟是使用几个相当短的函数执行的。我们将使用简短的函数来令一切尽可能模块化。稍后您会看到原因。

现在,我们看看第一个将进行的调用,其是创建基于出价的模拟:

inline void Simulation_BID(int imax, const MqlRates &rate, MqlTick &tick[])
   {
      bool    bHigh  = (rate.open == rate.high) || (rate.close == rate.high), 
      bLow = (rate.open == rate.low) || (rate.close == rate.low);
                                                        
      Mount_BID(0, rate.open, rate.spread, tick);     
      for (int c0 = 1; c0 < imax; c0++)
      {
         Mount_BID(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), rate.spread, tick); 
         bHigh = (rate.high == tick[c0].bid) || bHigh;
         bLow = (rate.low == tick[c0].bid) || bLow;
      }
      if (!bLow) Mount_BID(Unique(imax, rate.high, tick), rate.low, rate.spread, tick);
      if (!bHigh) Mount_BID(Unique(imax, rate.low, tick), rate.high, rate.spread, tick);
      Mount_BID(imax, rate.close, rate.spread, tick);
   }

上面的函数理解起来很简单。虽然,似乎最困难的部分是出价值的随机构造。但即使在这种情况下,一切仍很简单。我们将在柱线的最大值和最小值之间的范围内生成伪随机值。但注意,我正在常规化该值。这是因为产生的数值通常超出价格范围。这就是为什么我们需要将其常规化。但我认为函数的其余部分理应很清楚。

如果您仔细观察,就会发现我们在建模部分有两个经常提到的函数:MOUNT_BID 和 UNIQUE。它们中的每一个都有特定的目的。我们从 Unique 开始。其代码如下所示:

inline int Unique(const int imax, const double price, const MqlTick &tick[])
   {
      int iPos = 1;
                                
      do
      {
         iPos = (imax > 20 ? RandomLimit(1, imax - 1) : iPos + 1);
      }while ((m_IsPriceBID ? tick[iPos].bid : tick[iPos].last) == price);
                                
      return iPos;
   }

此函数可防止在生成随机位置时删除限定值之一,或任何其它价格值。目前,我们将仅将其用于限制。请注意,我们即可以使用模拟的出价,亦或模拟的最后成交价。现在我们只用出价工作。这是该函数的唯一目的:确保我们不会覆盖极限值。

现在我们看看 Mount_BID 函数,其代码如下:

inline void Mount_BID(const int iPos, const double price, const int spread, MqlTick &tick[])
   {
      tick[iPos].bid = price;
      tick[iPos].ask = NormalizeDouble(price + (m_TickSize * spread), m_NDigits);
   }

虽然在这个早期阶段,这段代码非常简单,并没有触及纯编程的美妙之处,但它让我们的生活变得轻松多了。它允许您避免在多个地方重复代码,最重要的是,可以帮助您记住常规化数值应放置在要价位置。如果不执行该常规化,则在进一步使用该要价值时就会出问题。要价值始终与点差值有偏移量。不过,就目前而言,该偏移量始终是恒定的。这是因为这是第一个实现,如果我们现在实现随机化系统,那么它会完全不清楚为什么、以及如何令点差值变得随意。

此处显示的点差值实际上是在特定 1-分钟柱线上显示的值。每根柱线也许点差不同,但有些东西您还需要了解。如果您正在运行模拟系统,以便获得类似于真实市场中发生的情况(即真实跳价文件中包含的数据),那么您会注意到所用的点差是 1-分钟柱线形成过程中所有数值中的较小值。但如果您正在运行随机模拟,其中数据可能与真实市场中发生的情况相似,也可能不同,那么该点差可能是任何数值。此处,我们将坚持构造市场中可能发生的事情的思路。因此,点差值将始终是柱线文件中指定的点差值。

系统还需要一个函数才能工作。它应该负责设置计时,以便 C_Replay 类具有正确的计时值。这段代码如下所见:

inline void CorretTime(int imax, MqlTick &tick[])
   {
      for (int c0 = 0; c0 <= imax; c0++)
         tick[c0].time_msc += (tick[c0].time * 1000);
   }

此函数只是相应地调整指定的时间(以毫秒为单位)。如果仔细观察,您可以发现计算与从文件加载实际跳价的函数中所用的计算相同。这种模块化方法的原因是,保留所执行的每个函数的记录可能很有趣。如果所有代码都是相互关联的,那么创建此类记录将更加困难。不过,通过这种方式,可以创建记录,并对其进行研究,从而检查哪些内容应该或不应该改进,从而满足特定需求。

重要提示:在这个早期阶段,我将阻止使用基于最后成交价系统。我们将在某些地方对其进行修改,令其在低流动性期间操控资产。目前这是不可能的,但我们稍后会修复它。如果您现在尝试基于最后成交价运行模拟,系统将不允许您这样做。我们稍后会修复它。

为了确保这一点,我们将用到其中一种编程技术。这将是非常复杂、且管理良好的事情。参阅下面的代码:

inline int Simulation(const MqlRates &rate, MqlTick &tick[])
   {
      int imax;
                        
      imax = (int) rate.tick_volume - 1;
      Simulation_Time(imax, rate, tick);
      if (m_IsPriceBID) Simulation_BID(imax, rate, tick); else return -1;
      CorretTime(imax, tick);

      return imax;
   }

每次系统采用最后成交价绘图模式时,它都会抛出一个错误。这是因为我们需要改进基于最后成交价的模拟。因此,我不得不添加这个复杂、且精密的技巧。如果尝试运行基于最后成交价的模拟,您则将得到负值。这不是一个复杂的方法吗?

但在本文完结之前,我们将再次讨论出价绘图建模的问题。结果就是,我们将有一个略微改进的随机化方式。基本上,我们需要改变一个时刻,如此其就得到随机点差值。这可以在 Mount_Bid 或 Simulation_Bid 函数中完成。在某些方面,这没什么大不了的,但为了确保 1-分钟柱线文件中指定的最小点差值,我们将对如下所示的函数进行修改:

inline void Simulation_BID(int imax, const MqlRates &rate, MqlTick &tick[])
   {
      bool    bHigh  = (rate.open == rate.high) || (rate.close == rate.high), 
      bLow = (rate.open == rate.low) || (rate.close == rate.low);

      Mount_BID(0, rate.open, rate.spread, tick);     
      for (int c0 = 1; c0 < imax; c0++)
      {
         Mount_BID(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), (rate.spread + RandomLimit((int)(rate.spread | (imax & 0xF)), 0)), tick);
         bHigh = (rate.high == tick[c0].bid) || bHigh;
         bLow = (rate.low == tick[c0].bid) || bLow;
      }
      if (!bLow) Mount_BID(Unique(imax, rate.high, tick), rate.low, rate.spread, tick);
      if (!bHigh) Mount_BID(Unique(imax, rate.low, tick), rate.high, rate.spread, tick);
      Mount_BID(imax, rate.close, rate.spread, tick);
   }

在此,我们提供点差值的随机化,不过,这种随机化仅用于演示目的。如果您愿意,您可以在限制条款做一些不同的事情。我们只需要稍微调整一下。现在您应该明白我正在使用的这种随机化,这对某些人来说似乎有点奇怪,但这就是我实际正在做的:我确保可以使用最大可能的值来随机化点差。该值基于一种计算,其中我们按比特位将点差值与范围从 1 到 16 的值组合在一起,因为我们只用到所有比特位的一部分。请注意,如果点差为零(在某些时候它实际上为零),我们仍将得到一个至少为 3 的值,因为值 1 和 2 实际上不会创建点差的随机化。这是因为值 1 仅表示开盘价等于收盘价,而值 2 表示开盘价可以等于或不同于收盘价。但在这种情况下,真正创造数值的是值 2。在所有其它情况下,我们将在点差中创建随机化。

我希望现在很清楚为什么我没有将随机化放在 Mount_Bid 函数当中。如果我这样做,在某些点上,柱线文件报告的最小点差将不是真实的。但是,正如我已经说过的,您可以自由地试验,并根据自己的品味和风格调整系统。


结束语

在本文中,我们解决了与代码重复相关的问题。我认为现在很清楚使用重复代码时会出现什么问题。在非常庞大的项目中,您始终需要当心这一点。即使代码不是那么庞大,也会因为这种粗心大意而出现严重的问题。

最后一个值得一提的细节是,在真实的跳价文件中,有时我们实际上有某种“虚假”走势。但这不会在这里发生;当只有一个价格(出价或要价)发生变化时,就会发生这种“虚假”走势。不过,为了简单起见,我对这种状况未予关注。在我看来,这对于模拟市场的系统没有多大意义。这不会带来操作上的改进。对于每次出价变化而没有要价,我们只得令要价也没有出价。这对于维持真实市场所需的平衡是必要的。

这就解决了基于出价的建模问题,至少在首次尝试时如此。将来,我也许会对这个系统进行修改,令其以不同的方式工作。但是当它与外汇数据一起使用时,我注意到它的效果很好,尽管它可能不足以满足其它市场的需求。

附件将令您能够访问处于当前开发状态的系统。不过,正如我在本文中曾说过的,您不应该尝试用股票市场资产进行建模,您只能用外汇金融产品进行建模。虽然您可以回放任何金融产品,但禁用了证交所交易的资产模拟。在下一篇文章中,我们将通过改进股票市场回放系统来解决这个问题,如此它就可在低流动性环境中工作。我们对模拟的研究到此结束。下一篇文章再与您相见。

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

附加的文件 |
Market_Replay_7vx23.zip (14388.45 KB)
神经网络变得轻松(第五十三部分):奖励分解 神经网络变得轻松(第五十三部分):奖励分解
我们已经不止一次地讨论过正确选择奖励函数的重要性,我们通过为单独动作添加奖励或惩罚来刺激代理者的预期行为。但是关于由代理者解密我们的信号的问题仍旧悬而未决。在本文中,我们将探讨将单独信号传输至已训练代理者时的奖励分解。
利用 MQL5 的交互式 GUI 改进您的交易图表(第 III 部分):简易可移动交易 GUI 利用 MQL5 的交互式 GUI 改进您的交易图表(第 III 部分):简易可移动交易 GUI
加入我们的《利用 MQL5 的交互式 GUI 改进您的交易图表》系列的第 III 部分,我们将探索将交互式 GUI 集成到 MQL5 中的可移动交易仪表板之中。本文建立在第 I 部分和第 II 部分的基础上,指导读者将静态交易仪表板转换为动态、可移动的。
如何利用 MQL5 创建简单的多币种智能交易系统(第 1 部分):基于 ADX 指标的信号,并结合抛物线 SAR 如何利用 MQL5 创建简单的多币种智能交易系统(第 1 部分):基于 ADX 指标的信号,并结合抛物线 SAR
本文中的多币种智能交易系统是交易机器人,它只能在单一品种图表中运营,但可交易(开单、平单和管理订单)超过一个品种对。
神经网络变得轻松(第五十二部分):研究乐观情绪和分布校正 神经网络变得轻松(第五十二部分):研究乐观情绪和分布校正
由于模型是基于经验复现缓冲区进行训练,故当前的扮演者政策会越来越远离存储的样本,这会降低整个模型的训练效率。在本文中,我们将查看一些能在强化学习算法中提升样本使用效率的算法。