
开发回放系统 — 市场模拟(第 12 部分):模拟器的诞生(II)
概述
在上一篇文章开发回放系统 — 市场模拟(第 11 部分):模拟器的诞生(I) 中,我们打造了我们的回放/模拟系统,能够利用 1-分钟柱线来模拟可能的市场走势。 虽然,也许在参阅了这些材料之后,您注意到这些走势与真实市场的走势并不那么相似。 在那篇文章中,我展示了需要改变的要点,从而令系统更接近您看到的真实市场。 然而,不管您用简单方法进行多少次尝试和实验,您都无法创造出任何与可能的市场走势相似的东西。
开始实现
为了完成所有必要的工作,并往系统里增加一些复杂性,我们即将采用随机数生成。 这会令事情可预测性更低,且回放/模拟系统更有趣。 按照 MQL5 文档中给出的生成随机数的提示,我们需要执行若干个步骤,第一眼会觉得十分简单。 没有理由担心,这确实很简单。 以下就是我们最初添加到代码中的内容:
void InitSymbolReplay(void) { Print("************** Market Replay Service **************"); srand(GetTickCount()); GlobalVariableDel(def_GlobalVariableReplay); SymbolSelect(def_SymbolReplay, false); CustomSymbolDelete(def_SymbolReplay); CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay), _Symbol); CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX); CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX); SymbolSelect(def_SymbolReplay, true); }
在此,我们严格遵循文档中的提示。 您可以通过查看 srand 函数来验证这一点,该函数初始化伪随机数的生成。 正如文档本身所释,如果我们在调用中使用固定值,例如:
srand(5);
我们将始终得到相同的数字序列。 若按这种方式,我们就停用了随机生成,并得到了一个“可预测”的序列。 请注意,我把“可预测”这个词放在引号里,因为序列始终相同。 不过,在整个生成循环完成之前,我们都无法确切知道下一个值会是什么。 在某些方面,如果我们想创建一个数值序列始终相同的模拟,这可能会很有趣。 另一方面,这种方式令完成它变得非常容易,故不可能经由该系统获得良好的学习体验。
如果您用测试器创建自定义算例,则创建大量不同的文件并无意义。 我们只需创建一个文件,并用它来引入所有的随机性。 出于这个原因,我不会在调用 srand 时指定固定值。 由机遇来负责接手。 不过,这留待每个人自行决定。
我们实验一种更复杂的任务执行方式。
我们要做的第一件事是摒除一个事实,即我们从搜素最小值开始。 知道这一点,一切就简单得多。 我们只需等待新柱线开立,并执行卖出操作。 如果超出开盘,我们将执行买入。 但这不是训练,这是作弊。
注意:一些智能系统能够分析和注意到这样的事情,这发生在策略测试器当中。 智能系统能注意到这会令所执行的任何模拟无效。
为此,我们将不得不使情况复杂化。 我们打算采用一种非常简单、但非常有效的方法。 我们来看看下面的代码。
inline int SimuleBarToTicks(const MqlRates &rate, MqlTick &tick[]) { int t0 = 0; long v0, v1, v2, msc; bool b1 = ((rand() & 1) == 1); double p0, p1; m_Ticks.Rate[++m_Ticks.nRate] = rate; p0 = (b1 ? rate.low : rate.high); p1 = (b1 ? rate.high : rate.low); Pivot(rate.open, p0, t0, tick); Pivot(p0, p1, t0, tick); Pivot(p1, rate.close, t0, tick, true); v0 = (long)(rate.real_volume / (t0 + 1)); v1 = 0; msc = 5; v2 = ((60000 - msc) / (t0 + 1)); for (int c0 = 0; c0 <= t0; c0++, v1 += v0) { tick[c0].volume_real = (v0 * 1.0); tick[c0].time = rate.time + (datetime)(msc / 1000); tick[c0].time_msc = msc % 1000; msc += v2; } tick[t0].volume_real = ((rate.real_volume - v1) * 1.0); return t0; }
请不要害怕上述函数在做什么,因为一切都和以前一样。 唯一的变化是,现在我们不知道柱线是否会开始搜素最小值或最大值。 第一步是检查随机生成的数值是偶数还是奇数。 一旦我们知道了这一点,我们简单地交换将创建的轴点值。 但请记住,轴点仍将以相同的方式创建。 我们唯一不知道的是,柱线是否会因它已经达到最小值而上升,亦或因它已经达到最大值而下降。
这是个开始。 在移到下一步之前,我们需要进行另一项变更。 变更是什么? 在以前的版本中,柱线的开盘和收盘之间通常有 9 个分段,但只需一点代码,我们就可以将这 9 个分段变成 11 个分段。 但如何做到呢? 查看下面的代码:
#define def_NPASS 3 inline int SimuleBarToTicks(const MqlRates &rate, MqlTick &tick[]) { int t0 = 0; long v0, v1, v2, msc; bool b1 = ((rand() & 1) == 1); double p0, p1, p2; m_Ticks.Rate[++m_Ticks.nRate] = rate; p0 = (b1 ? rate.low : rate.high); p1 = (b1 ? rate.high : rate.low); p2 = floor((rate.high - rate.low) / def_NPASS); Pivot(rate.open, p0, t0, tick); for (int c0 = 1; c0 < def_NPASS; c0++, p0 = (b1 ? p0 + p2 : p0 - p2)) Pivot(p0, (b1 ? p0 + p2 : p0 - p2), t0, tick); Pivot(p0, p1, t0, tick); Pivot(p1, rate.close, t0, tick, true); v0 = (long)(rate.real_volume / (t0 + 1)); v1 = 0; msc = 5; v2 = ((60000 - msc) / (t0 + 1)); for (int c0 = 0; c0 <= t0; c0++, v1 += v0) { tick[c0].volume_real = (v0 * 1.0); tick[c0].time = rate.time + (datetime)(msc / 1000); tick[c0].time_msc = msc % 1000; msc += v2; } tick[t0].volume_real = ((rate.real_volume - v1) * 1.0); return t0; } #undef def_NPASS
您也许认为它们是一样的,但实际上有很大的不同。 虽然我们只加了一个变量来表示中间点,而一旦我们找到这个点,我们就能再多加两个分段。 注意,若要添加这两个分段,我们要继续执行几乎相同的代码。 请注意,我们在创建模拟来形成柱线时,引入的复杂性会迅速增加,且其与代码增加的速率不同。 我们应该注意的一个小细节是,定义不应设置为零。 如果发生这种情况,我们将得到除零错误。 在这种情况下,我们应当取定义的最小值 1。 但如果您定义了从 1 到最大值之间的任意值,则您可以加入更多分段。 由于我们通常没有足够宽的走位来创建更多分段,因此值 3 就很好了。
若要了解此处发生的情况,请参阅下图。
添加新分段之前
尽管一切工作良好,但当我们所用版本允许振幅划分为范围时,我们会遇到以下场景:
更改后,我们开始将柱线范围除以 3
请注意复杂度是如何略微提高的。 不过,我没有注意到将其切分 3 个以上分段有啥巨大优势。 因此,虽然事情已经变得非常有趣,但系统并没有产生应有的复杂度。 那么,我们必须采取不同的方式。 这不会导致代码变得更加复杂。 该思路是在代码不过于复杂的情况下达成复杂度的指数级增长。
为了达成这一目标,我们将采取完全不同的方式。 但首先,我们研究一些理当解释的事情。 以这种途径,我们可以真正明白为什么我们要改变解决问题的方式。
如果您关注到上一步中所做的更改,您可能已经意识到最终代码中有一些有趣的东西。 某些时刻,我们能全权控制柱线整个主体,并能够做任何我们想做的事情。 与其它时间不同,我们有一个相对方向的走势,从开盘到高点或低点。 当我们需要在柱线的整个主体上操作时,我们在其内做的工作很少。 无论我们多么努力,我们总是陷入同样的境地,但如果您仔细观察,您会注意到我们总能在两个数值上操作。 这些就是起点和终点。 为什么要关注这一时刻? 试想:我们有 6 万毫秒的时间来创建一根 1-分钟柱线,如果我们在柱线的开头留出 5 毫秒的余量,我们仍然会有很多时间。 如果我们做一些简单的计算,我们会注意到我们浪费了很多时间,而这可令柱线模拟更加复杂。
我们可以拿出一个可能的解决方案:如果我们留出 1 秒钟让价格从开盘点离开,并走向高点或低点,再留出 1 秒钟让价格从那里移动到收盘点,我们将有 58 秒的时间来创建所需的复杂度。 不过,请注意最后一秒所说的话:“价格从它所在的位置移动到收盘点”,重点是要准确认识和理解所说的内容。 大多数时间发生了什么无所谓,我们应当始终为价格最终达到收盘点预留一段时间。
您会注意到在较长时间内发生的走势,时间刚刚超过 33 毫秒、或 30 赫兹。 如果我们将每次跳价的最大持续时间设置为 30 毫秒,您会发现其走势将与资产的走势非常相似。 一个重要的细节:这种感知是非常相对的,因为有人发现某种资产因其高波动性,故变化非常快,导致难于交易。
出于这个原因,不应该真的认为回放/模拟系统是优良的训练。 除非我们实际使用包含真实跳价的文件。 当您模拟此类跳价时,也许会有一种虚假的印象,即所有价格范围都能看到。 目前,该系统模拟 1-分钟柱线的方式不允许产生跳空缺口,尽管在真实市场中,这些跳空缺口在非常特殊的时间会实际发生。 这些都是极其危险的开仓或平仓交易时刻,因为订单在所期望价格之外执行的可能性非常高,并且由于波动性可能非常高的事实,因此错过下单的可能性也很大,这令一切想象中的完美运行都处于意想不到的境地。
我想您可能会认为我应当采用一种始终生成最小数量跳价的方法,但我暂时不会采用这种方式。 不过,您必须记住这一点:通过模拟绝不可能重现实际的市场走势。 我们所能做的就是估计可能的走势。 但在我们继续之前,我们需要专注于解决一些具体问题。 我们将从一个稍微高级的主题开始,但它将作为我们模拟器的基础。 但在我们继续之前,我们需要专注于解决一些具体问题。
如果没有跳价,为何服务会处于激活状态?
尽管我们必须解决所有复杂度,但在继续朝着所有事情都向现实接近之前,我们必须解决一些确实需要解决,然而已经推迟了一段时间的个别问题。 这些问题中的第一个就是,当我们在不加载预览柱线的情况下启动系统时,我们无法访问控制指标。 这个错误在系统中已经存在了一段时间,但由于预览柱线以前总是存在,故我把这个系统需求推迟修复。 现在我们就来解决这个问题。 为此,我们必须在系统中的一个非常特殊点上进行一些小的添加。 我们这样做是为了尽可能简化任务,请参阅下面我们的所做:
bool SetSymbolReplay(const string szFileConfig) { #define macroERROR(MSG) { FileClose(file); MessageBox((MSG != "" ? MSG : StringFormat("Error occurred in line %d", iLine)), "Market Replay", MB_OK); return false; } int file, iLine; string szInfo; char iStage; bool bBarPrev; MqlRates rate[1]; if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) { MessageBox("Failed to open the\nconfiguration file.", "Market Replay", MB_OK); return false; } Print("Loading data for replay. Please wait...."); ArrayResize(m_Ticks.Rate, def_BarsDiary); m_Ticks.nRate = -1; m_Ticks.Rate[0].time = 0; iStage = 0; iLine = 1; bBarPrev = false; while ((!FileIsEnding(file)) && (!_StopFlag)) { switch (GetDefinition(FileReadString(file), szInfo)) { case Transcription_DEFINE: if (szInfo == def_STR_FilesBar) iStage = 1; else if (szInfo == def_STR_FilesTicks) iStage = 2; else if (szInfo == def_STR_TicksToBars) iStage = 3; else if (szInfo == def_STR_BarsToTicks) iStage = 4; else if (szInfo == def_STR_ConfigSymbol) iStage = 5; else macroERROR(StringFormat("%s is not recognized in the system\nin line %d.", szInfo, iLine)); break; case Transcription_INFO: if (szInfo != "") switch (iStage) { case 0: macroERROR(StringFormat("Couldn't recognize command in line %d\nof configuration file.", iLine)); break; case 1: if (!LoadPrevBars(szInfo)) macroERROR(""); bBarPrev = true; break; case 2: if (!LoadTicksReplay(szInfo)) macroERROR(""); break; case 3: if (!LoadTicksReplay(szInfo, false)) macroERROR(""); bBarPrev = true; break; case 4: if (!LoadBarsToTicksReplay(szInfo)) macroERROR(""); break; case 5: if (!Configs(szInfo)) macroERROR(""); break; } break; }; iLine++; } FileClose(file); if (m_Ticks.nTicks <= 0) { MessageBox("No ticks to be used.\nClose the service...", "Market Replay", MB_OK); return false; } if (!bBarPrev) { rate[0].close = rate[0].open = rate[0].high = rate[0].low = m_Ticks.Info[0].last; rate[0].tick_volume = 0; rate[0].real_volume = 0; rate[0].time = m_Ticks.Info[0].time - 60; CustomRatesUpdate(def_SymbolReplay, rate, 1); } return (!_StopFlag); #undef macroERROR }
首先,我们将定义两个新变量供局部使用。 然后,我们把它们初始化为 false 值,表示我们没有加载预览柱线。 现在,如果在任何时候加载了以前的任何一根柱线,该变量将指示 true 值。 以这种方式,系统就会知道我们已经加载了以前的柱线,从而解决了第一个问题的一部分。 但我们仍然需要检查是否加载了任何文件,来生成所用的跳价。 如果没有跳价,则启动服务没有意义。 因此,该服务将会停止。 现在,如果有跳价,我们检查是否加载了以前的某种类型柱线。 如果没有发生这种情况,我们初始化一根空柱线。 缺了这样的初始化,我们将无法访问控制指标,即使该服务仍可供使用。
不过,通过进行上述更正,一切都会得到解决。
实现跳价交易
清单中要调整的下一件事是指示交易跳价交易量的系统。 许多人喜欢在图表上有一个成交量指标,到目前为止,实际上仅实现了真实成交量。 也就是说,交易量是已执行合约的数量。 不过,跳价的交易量同样重要。 您知道两者有什么区别吗? 请看下图:
您可以在其中看到两个交易量数值。 一个是跳价的交易量,另一个是交易量(在本例中为实际交易量)。 但看这张图片,您能告诉我实际交易量和跳价交易量之间的区别吗? 如果您不知道其中的区别,现在就是找出最终答案的时候了。
VOLUME 或 REAL VOLUME 本质上是在给定时间点所交易的合约数量。 它始终是倍数值,具体取决于资产。 例如,某些资产进行交易时不允许小于 5 份,而其它资产则接受分数值。 不要试图理解为什么这是可能的,只需知道您可以按分数值交易即可。 这个值很容易理解,也许就是很多人用它的原因。 现在,如果我们将 REAL VOLUME 的值乘以每笔合约的最小份数值,我们会得到另一个称为 FINANCIAL VOLUME 的值。 MetaTrader 5 不直接提供该数值,但如您所见,它很容易获得。 因此,交易服务器明白它不需要向交易终端报告这个 FINANCIAL VOLUME。 程序员或平台用户必须自行实现指定的计算。
现在,TICK VOLUME 是一个完全不同的交易量。 它只在柱形内容中提供,出于一个很简单的原因:我们不能仅仅通过查看实际交易量就说交易期间发生了什么。 我们需要更多信息 — 跳价的交易量。 但是,为何当我们查询柱线时,跳价交易量可用,而当我们查询跳价时,它就不可用呢? 当我们请求跳价时会出现什么样的交易量? 如果您从未注意到这一点(或尚未看到),可以查看下图:
同样,VOLUME 字段中指定的值并不代表跳价的交易量。 此值为 REAL VOLUME。 不过,如果在请求跳价时没有报告该值,我们如何找出跳价交易量? 它仅在我们查询柱线时出现。 关键是,就像服务器明白它不需要提供 FINANCIAL VOLUME 一样,它也明白通过提供交易跳价,我们就能够计算 TICK VOLUME。 与请求柱线时发生的情况不同,我们无法访问正在交易的实际跳价交易量。
还没明白? 有了实际交易的跳价数据,我们就可以计算出跳价的交易量。 但如何做到呢? 是不是有某种神秘的公式? 因为每次我尝试时,我都无法获得匹配的数值。 冷静,我亲爱的读者。 没有神奇的公式。 关键是您可能不太了解 TICK VOLUME 到底是什么。 在这篇文章和之前的文章中,我们曾用到据分钟柱线内进行走势建模的方法。 尽管此举会导致所有价格受到影响,但我们创建的实际跳价交易量远低于 1-分钟柱线上报告的报价量。
但为什么呢? 不用担心。 您将在下一篇文章中就会更好地理解这一点,其中我们将有实际模型,对应相同的跳价交易量。 曾提过这个,我想您明白什么是跳价交易量。 跳价交易量是给定柱线内实际发生的交易数量。 我们的平均交易量约为 150 次。 事实上,平均值通常在 12,890 次左右。
不过,您可能会想:那么我该如何计算这个跳价交易量呢? 这很容易做到。 我们看看我们的系统是否可以执行此计算。 因为要理解这一点,您真的需要看看计算动作。
目前,由于不同的原因,此计算是在两个地方进行的。 第一处如下所示:
inline bool BuiderBar1Min(MqlRates &rate, const MqlTick &tick) { if (rate.time != macroRemoveSec(tick.time)) { rate.real_volume = 0; rate.tick_volume = 0; rate.time = macroRemoveSec(tick.time); rate.open = rate.low = rate.high = rate.close = tick.last; return true; } rate.close = tick.last; rate.high = (rate.close > rate.high ? rate.close : rate.high); rate.low = (rate.close < rate.low ? rate.close : rate.low); rate.real_volume += (long) tick.volume_real; rate.tick_volume += (tick.last > 0 ? 1 : 0); return false; }
在这个阶段,我们计算柱线中将出现的跳价的交易量。 第二处如下:
inline int Event_OnTime(void) { bool bNew; int mili, iPos; u_Interprocess Info; static MqlRates Rate[1]; static datetime _dt = 0; datetime tmpDT = macroRemoveSec(m_Ticks.Info[m_ReplayCount].time); if (m_ReplayCount >= m_Ticks.nTicks) return -1; if (bNew = (_dt != tmpDT)) { _dt = tmpDT; Rate[0].real_volume = 0; Rate[0].tick_volume = 0; } mili = (int) m_Ticks.Info[m_ReplayCount].time_msc; do { while (mili == m_Ticks.Info[m_ReplayCount].time_msc) { Rate[0].close = m_Ticks.Info[m_ReplayCount].last; Rate[0].open = (bNew ? Rate[0].close : Rate[0].open); Rate[0].high = (bNew || (Rate[0].close > Rate[0].high) ? Rate[0].close : Rate[0].high); Rate[0].low = (bNew || (Rate[0].close < Rate[0].low) ? Rate[0].close : Rate[0].low); Rate[0].real_volume += (long) m_Ticks.Info[m_ReplayCount].volume_real; Rate[0].tick_volume += (m_Ticks.Info[m_ReplayCount].volume_real > 0 ? 1 : 0); bNew = false; m_ReplayCount++; } mili++; }while (mili == m_Ticks.Info[m_ReplayCount].time_msc); Rate[0].time = _dt; CustomRatesUpdate(def_SymbolReplay, Rate, 1); iPos = (int)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks); GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value); if (Info.s_Infos.iPosShift != iPos) { Info.s_Infos.iPosShift = (ushort) iPos; GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value); } return (int)(m_Ticks.Info[m_ReplayCount].time_msc < mili ? m_Ticks.Info[m_ReplayCount].time_msc + (1000 - mili) : m_Ticks.Info[m_ReplayCount].time_msc - mili); }
在这个阶段,我们做同样的事情,即我们计算跳价交易量。 真是这样吗? 对的,就是这样。 跳价交易量的计算,实际上仅包括指示所执行交易的跳价。 也就是说,每次操作一个跳价。 这意味着带有 BID 或 ASK 激活标志的跳价不参与计算,只有那些带有 SELL 或 BUY 标志的跳价才会被计算在内。 但由于这些标志仅在价格值或实际交易量大于零时才会激活,因此我们不会检查这些标志,因为这不是必需的。
因此,从现在开始,回放/模拟系统将带有跳价交易量。 但有一个细节:现在,当使用柱线模拟跳价时,该交易量与柱线文件中指定的交易量始终不同。 我们将在下一篇文章中修复此问题。 这需要一篇单独的文章,如此我才可以冷静地解释我们将要做什么。
设置参考点
下一个需要解决的问题(尽管这不是一个真正的问题)是令系统知道每个定位单元代表什么。 问题在于,到目前为止,该系统一直采用一种非常不恰当的方式来执行用户指定的定位。 然后,当可以用到多个文件来获取跳价数据时,对于以前的系统来说,这种状况将变得完全不可接受。 由此,我们在控制指标中的定位,与回放产生的定位之间进行转换时遇到了问题。
若要解决此问题,您需要在加载系统中删除某一行。
bool LoadTicksReplay(const string szFileNameCSV, const bool ToReplay = true) { int file, old, MemNRates, MemNTicks; string szInfo = ""; MqlTick tick; MqlRates rate, RatesLocal[]; MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate); MemNTicks = m_Ticks.nTicks; if ((file = FileOpen("Market Replay\\Ticks\\" + szFileNameCSV + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE) { ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray); ArrayResize(m_Ticks.Rate, def_BarsDiary, def_BarsDiary); old = m_Ticks.nTicks; for (int c0 = 0; c0 < 7; c0++) szInfo += FileReadString(file); if (szInfo != def_Header_Ticks) { Print("File ", szFileNameCSV, ".csv is not a traded tick file."); return false; } Print("Loading data for replay. Please wait..."); while ((!FileIsEnding(file)) && (m_Ticks.nTicks < (INT_MAX - 2)) && (!_StopFlag)) { ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray); szInfo = FileReadString(file) + " " + FileReadString(file); tick.time = StringToTime(StringSubstr(szInfo, 0, 19)); tick.time_msc = (int)StringToInteger(StringSubstr(szInfo, 20, 3)); tick.bid = StringToDouble(FileReadString(file)); tick.ask = StringToDouble(FileReadString(file)); tick.last = StringToDouble(FileReadString(file)); tick.volume_real = StringToDouble(FileReadString(file)); tick.flags = (uchar)StringToInteger(FileReadString(file)); if ((m_Ticks.Info[old].last == tick.last) && (m_Ticks.Info[old].time == tick.time) && (m_Ticks.Info[old].time_msc == tick.time_msc)) m_Ticks.Info[old].volume_real += tick.volume_real; else { m_Ticks.Info[m_Ticks.nTicks] = tick; if (tick.volume_real > 0.0) { m_Ticks.nRate += (BuiderBar1Min(rate, tick) ? 1 : 0); rate.spread = (ToReplay ? m_Ticks.nTicks : 0); m_Ticks.Rate[m_Ticks.nRate] = rate; m_Ticks.nTicks++; } old = (m_Ticks.nTicks > 0 ? m_Ticks.nTicks - 1 : old); } } if ((!FileIsEnding(file)) && (!_StopFlag)) { Print("Too much data in the tick file.\nCannot continue..."); FileClose(file); return false; } FileClose(file); }else { Print("Tick file ", szFileNameCSV,".csv not found..."); return false; } if ((!ToReplay) && (!_StopFlag)) { ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates)); ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0); CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates)); m_dtPrevLoading = m_Ticks.Rate[m_Ticks.nRate].time; m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates); m_Ticks.nTicks = MemNTicks; ArrayFree(RatesLocal); } return (!_StopFlag); };
抛出此异常将释放 “spread” 变量,该变量可以在另外的时间进行相应的调整。 在本文中我们不会这样做,因为目前还没有这样的需要。 若一旦这样做了,我们将不得不修复负责变换的系统。 因为从现在开始,定位控制系统将始终指示一个无效点。 更准确地说,它是与用户所想的不同点。
为了正确执行转换,我们需要修改一个非常特殊的过程。 这个就是:
long AdjustPositionReplay(const bool bViewBuider) { u_Interprocess Info; MqlRates Rate[def_BarsDiary]; int iPos, nCount; Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); if (Info.s_Infos.iPosShift == (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks)) return 0; 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); if ((m_dtPrevLoading == 0) && (iPos == 0)) { m_ReplayCount = 0; Rate[m_ReplayCount].close = Rate[m_ReplayCount].open = Rate[m_ReplayCount].high = Rate[m_ReplayCount].low = m_Ticks.Info[iPos].last; Rate[m_ReplayCount].tick_volume = Rate[m_ReplayCount].real_volume = 0; CustomRatesUpdate(def_SymbolReplay, Rate, 1); }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);) Event_OnTime(); Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); Info.s_Infos.isWait = false; GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value); return Event_OnTime(); }
上面介绍的转换与之前文章中所示的版本有很大不同。 这是因为它实际上将用户配置的百分比值转换为控制指标和定位系统,故此票据的组织方式无关紧要。 该过程将搜索正确的点,并从该点开始显示在跳价中找到的数据。
为了正确地做到这一点,我们将首先计算,判定期望位置的百分比。 这个定位非常重要。 如果该值较低,则意味着我们必须回到某个点。 然后,我们删除信息,直到我们接近该点。 通常,总是会有一些额外的数据被删除,但这是该过程的一部分,我们稍后将返回这些数据。 我们也许确实要回到数据序列的开头。 但如果不是,我们会将计数器重置回接近百分比值的点。 这特殊的一行修复了总是退回比我们实际想到的位置更远的问题。 没有它,预览栏线就不正确。 后向系统比前向系统更复杂。 对于前向,我们只需检查用户是否想看到正在创建的柱线。 如果需要,它们将被显示;否则,系统将跳转到百分比值指示的点。 在大多数情况下,我们需要在百分比值和实际位置之间进行微调。 不过,事情完成得很快:如果实际值实际上接近百分比值,则转换实际上是即时的。 但如果该值存在一段距离,则会出现一个小动画,显示柱线是如何构建的。
本文的最终思索
尽管该系统看起来更加用户友好,但在柱线结构显示模式下运行时,您可能会注意到一些奇怪的事情。 这些不寻常的事情可以在下面的视频中看到。 不过,由于它们需要对代码中的某些地方进行修改,并且我不想让您认为这些东西是凭空出现的,因此我决定留下“漏洞”。 但也许主要原因是,在下一篇文章中,我将展示如何令系统更适合作为模拟器。 我不希望任何人来质疑我,为什么把已编好的模拟器放在下一篇文章中展示。
现在观看视频。 请搞明白我所知晓的正在发生的事情。
本文用到的文件可以在附件中找到。 您还将得到一个额外文件,显示同一天交易的 1-分钟柱线和跳价。 运行这两种配置,并检查结果,但首先您需要了解图表上发生的情况。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10987

