
开发回放系统 — 市场模拟(第 23 部分):外汇(IV)
概述
在上一篇文章《开发回放系统 — 市场模拟(第 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 类作为指针。这样的话,能更有效地利用系统内存,因为在类完成其工作后,它占用的内存将被释放。
- 保证函数只有一个入口点和一个出口点。
这种方式的问题在于,您必须经常创建并令两种不同的方法尽可能和谐地工作。最糟糕的部分是:在某些情况下,随机游走模拟更接近真实资产中发生的情况。但当我们与低交易量(每分钟少于 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
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.




你好,亲爱的丹尼尔、
祝贺你设计了这个伟大的系统。
我在测试您的系统时遇到了一些问题,需要您的帮助。
首先,我保存了一个月的勾选数据并将其用于重放。但是,在重播一个月的 1 分钟时间框架中,随着滑块上销轴的变化,显示的蜡烛数量与滑块上销轴的位置不一致,我已在所附视频中展示了这一点。我将针一直拉到最后,但蜡烛图只重复了少量的 1 分钟柱状图(约 20 个柱状图)。
第二件事是,我需要改变这个系统,使柱状图的移动方式与 Strategy Tester 相似,即插针的位置代表柱状图的显示速度,或者我可以像 TradingView 网站那样逐柱移动。您的系统可以像这样改变????。
如蒙指教,不胜感激。
此致敬礼、
第二件事是,我需要改变这个系统,使柱状图的移动方式与 Strategy Tester 相似,即针的位置代表柱状图的显示速度,或者我也可以像 TradingView 网站那样逐柱移动。您的系统是可以这样改变的????
如果您能提供指导,我将不胜感激。
此致敬礼、
好了,让我们分头行动,就像 JACK 说的那样......😁👍
您可能对这个应用程序感到非常困惑,或者说,您可能期望这个应用程序用于一些它本来不打算用于的用途。我并不是说它不能用于某些特殊用途,比如策略测试仪。但这并不是实施它的初衷。
关于第一个问题:你可能没有真正理解重放/模拟是如何进行的。先别管滑块了。当您播放系统时,它将检索已加载的数据(以刻度线或条形图的形式),并以 1 分钟的时间为基础,在图表上以条形图的形式显示。这与您要使用的图表时间无关。因此,文件中的数据应视为 1 分钟的条形图。不应将文件中的数据视为单个数据。本应用程序不会这么看。它总是将条形图,甚至是两小时的条形图,理解为 1 分钟的条形图。始终如此。
如果您使用的是条形图,程序会自动注意到这一点,并创建模拟,使每个条形图的长度大约为 1 分钟。根据需要创建尽可能多的刻度线,以便在图形上正确绘制数值。如果文件中的数据是刻度线,系统将按照刻度线之间定义的大致间隔启动每个刻度线。这个间隔可以从几毫秒到几个小时不等。但这样做,间隔内的任何情况都将被视为拍卖或交易停止。因此,如果使用的数据间隔超过一天或 24 小时,应用程序很可能无法正确识别条形图。如果使用滑块搜索新的研究点,也会出现这种情况。因此,应避免使用时间跨度超过一天的数据。
请记住,该程序的设计使用时间相当于实时时间。换句话说,就是短时间。要在研究中输入长时段。如果需要使用需要绘制多条柱状图的平均线或指标。不得 在重放或模拟器中使用这些数据。您应该将它们作为前一个条形图使用。这是您应该努力理解的第一点。
至于第二个问题:您认为滑块会搜索特定点。的确如此,但并不是你想要或想象的那样。为了更好地理解这一点,请看一下之前实现滑块的文章。在那里,你将详细了解它是如何搜索特定位置的。但在这个问题中,你混淆了控件的用途。因为你还提出了一个想法,认为它可以用来修改绘制条形图的速度。事实并非如此。当你拖动控制器并按下播放按钮时,你所看到的绘制是以较快的速度进行的。这实际上是应用程序造成的错觉。它的目的是显示在您指示开始模拟或回放之前,条形图是如何绘制的,以便您进行研究。
我的建议是:仔细阅读前面的文章,如果有任何问题,可以发表评论。这将使您更容易理解到底发生了什么,以及如何使用应用程序才能获得良好的用户体验。如果您有任何问题,可以在评论中提出 ...😁👍
好吧,就像 JACK 说的那样......😁👍
也许你对这一应用感到非常困惑,或者说,也许你希望这一应用能为某些事情服务,而事实上,它原则上并不打算用于这些事情。我并不是说它不能用于某些特殊用途,比如策略测试仪。但这并不是它的最初目的。
关于第一个问题:您可能没有真正理解重放/模拟是如何进行的。先别管滑块。当您在系统上按下播放键时,它将获取已加载的数据(以刻度线或条形图的形式),并以 1 分钟的时间为基础,在图表上以条形图的形式显示。这与您要使用的时间框架无关。因此,必须将文件中的数据视为 1 分钟的条形图。不应将文件数据视为单个数据。因为本程序不会这样看。它总是将条形数据(即使是两小时条形数据)理解为 1 分钟条形数据。 总是 .
如果您使用的是条形图,程序会自动注意到这一点,并创建模拟,使每个条形图的长度约为 1 分钟。创建尽可能多的刻度线,以便在图表上正确绘制数值。如果文件中的数据是刻度线,系统将记录每个刻度线之间定义的大致间隔。请参阅之前的文章了解这一点。这样的间隔可以从几毫秒到几个小时不等。但这样做后,任何在区间内的数据都将被视为拍卖或交易持有。因此,如果您使用的数据间隔超过一天,即 24 小时,应用程序很可能无法正确识别条形图。如果使用滑块寻找新的研究点,就会出现这种情况。因此,应避免使用时间超过一天的数据。
请记住,该应用程序的使用时间相当于实时时间。换句话说,时间要短。要在研究中输入长周期的数据。如果您需要使用某些需要绘制多条柱状图的平均值或指标。 切勿 在重播或模拟器中使用这些数据。您必须将它们作为前置条形图。这是您需要了解的第一点。
关于第二个问题:您想象滑块会寻找一个特定的点。确实如此,但并不是你想要或想象的那样。为了更好地理解,请参阅之前的文章,那里有控件的实现过程。在那里您将详细了解滑块是如何寻找特定位置的。但在同一个问题中,你混淆了控制的使用。因为你还提出了一个想法,即它可能用于修改绘制柱形图的速度。实际上这根本不会发生。当您拖动控件并按下播放按钮时,您会发现绘制的速度更快。这实际上是应用程序造成的错觉。为了显示柱状图是如何绘制的,请参阅 "开始模拟或重放"、
我的建议是:静下心来阅读前面的文章,如果有疑问,可以发表评论。因为这样会更容易理解实际发生的情况,以及如何使用应用程序才能获得良好的用户体验。有任何问题都可以在评论中提出...😁👍
更改速度非常简单。只需进入C_Replay 类,查找LoopEventOnTime 函数。那里有一个Sleep 调用。这就是我们在播放模式下控制绘图速度的地方。但我相信这在之前的文章中已经有了充分的解释。
改变速度非常简单。只需进入C_Replay 类,查找LoopEventOnTime 函数。那里有一个Sleep 调用。这就是我们在播放模式下控制绘图速度的地方。但我相信这在之前的文章中已经解释得很清楚了。