开发回放系统 — 市场模拟(第 05 部分):加入预览
概述
在之前题为“开发回放系统 - 市场模拟(第 02 部分):首次实验(II)”的文章中,我们设法开发了一套以逼真且易于访问的方式来实现市场回放的系统。 在此方法中,每根 1 分钟柱线均在一分钟内创建。 我们仍然需要做一些小的调整,但稍后会更多。 这一步对于研究模拟市场回放的系统非常重要。
尽管我们设法做到了这一点,但似乎我们的回放系统不足以探索和实践某些系统操作。 按这样方式,我们就无法对以前的数据(无论是数分钟、数小时、还是数天)进行预分析,这就是为什么我们实际上无法获得可行的数据的原因。
当我们提到预分析时,我们的意思是系统自某个点创建柱线时,之前已有柱线的可能性。 没有这些早期的柱线,从任何指标中都不可能获得可靠的信息。
您可能会以这样的方式思考:我们有一个文件,其中包含某一天执行的所有交易跳价。 不过,仅使用此文件的内容,我们将无法从任何指标中获得真正有用的信息。 例如,即使我们使用 3 周期移动平均线(这正是 JOE DI NAPOLI 系统中所采用的),在至少创建了 3 根柱线之前,信号不会生成。 只有在此之后,移动平均线才会显示在图表上。 从实际应用的角度来看,直到今天,这个系统是完全无用的、且无操作性的。
我们想象一下我们想在 5 分钟时间帧内进行研究的情况。 我们需要等待 15 分钟,才能在图表上显示 3 周期移动平均线。 在任何有用的信号出现之前,它尚需花费几分钟。 也就是说,系统需要更新,本文的目的是讨论如何进行此更新。
理解一些细节
实现部分非常简单,可以相对快速地完成。 但在编写哪怕是一行代码之前,我们都需要考虑一件事。
需要考虑的问题是,说来奇怪:我们将在回放中使用哪个图表周期,每个指标生成有用信息所需的最短周期是多少?这些问题非常重要,需要有说服力的答案。 没有标准的模型,这完全取决于我们将如何运用市场回放进行分析。
但如果我们正确回答了这些问题,那么我们将不再仅限于回放。 我们还可以创建一个市场模拟器,这会更有趣。 不过,它遵循与回放相同的操作原则。 回放和模拟器之间唯一真正的区别是创建研究的数据源(TICKS 或 BARS)。
在回放的情况下,数据是真实的,而在模拟器的情况下,数据以某种方式合成,要么基于数学模型,要么纯粹是任意的。 在模拟器的情况下,会出现一些非常有趣的事情。 许多公司甚至经验丰富的交易者使用数学方法创建一种称为压力测试的特殊类型的研究,他们据其模拟市场走势,并预测各种情况下可能的极端走势。 不过,我们会在以后研究这个问题,因为它涉及尚未解释的层面。
但不用担心,将来当这个回放系统处于更高级的阶段时,我将解释如何“创建”此类数据。 但现在,我们将专注于如何使用真实的市场数据,即回放。
为了判定回放中所需的先前的柱线数量,我们需要执行一个相当简单的计算:
其中 N 是所需的柱线数量,P 是我们将采用、或计算平均值所需、或指标开始提供有用信息的最长周期,T 是图表周期中的分钟数量。 为了更清楚起见,我们来看看下面的例子:
示例 1
假设我们将在图表上计算 5 分钟时间帧的 200 周期的移动平均线。 图表上所需的最少柱线数值是多少?
n = ( 200 * 5 ) + 5
这等于 1005,即在回放开始之前,您需要至少有 1005 根 1 分钟柱线才能构建平均 200 根柱线。 这是指图表时间帧使用等于或小于 5 分钟。 如此,平均值才能从回放的最开始即出现。
示例 2
对于研究,我们希望采用 21-周期移动平均线、50-周期移动平均线、设置为 14-周期的 RSI、和日内 VWAP。 我们将在 5 分钟和 15 分钟的时间帧上进行研究。 不过,我们会不时检查 30 分钟时间帧,从而确认入场或离场交易。 在开始回放之前,我们所需的最少柱线数量是多少呢?
对于一个没有经验的人来说,这种情形可能会看似很复杂,但实际上很简单,只是需要注意细节。 我们先弄清楚如何计算最少柱线数量。 首先,我们需要找到将用于移动平均线或指标的最大周期。 在这种情况下,该值为 50,因为 VWAP 是一个与时间无关的指标,我们无需担心它。 这是第一个要点。 现在我们来检查一下时间帧。 在这种情况下,我们应该取 30,因为它是三个可用时间帧当中中最长的。 即使 30 分钟图表只使用一次,也应将其视为最小值。 然后计算将如下所示:
n = ( 50 * 30 ) + 30
这等于 1530,即在开始实际回放之前,您需要至少有 1530 根 1 分钟柱线
虽然这看起来很多,但实际上该值仍然远未达到更极端情况下可能需要的值。 然而,这个思路是,我们所拥有的数据量,应始终比许多人认为的研究或分析所需的数据量大很多。
现在还有另一个问题。 通常在 B3(巴西证券交易所),交易时段(交易周期)分几个阶段进行,某些资产类别的时间窗口不同。 期货市场通常在上午 9:00 开始交易,下午 6:00 结束,但在一年中的某些时候,这段时间延长到下午 6:30,而股票市场的开盘时间为上午 10:00 至下午 6:00。 其它市场和资产有其特定的开盘时间。 故此,在投标之前,有必要检查这些参数。 原因如下。 出于 B3 有一个交易窗口的事实,您通常需要上传若干天的数据才能获得计算出的最少柱线数量。
重要提示:虽然我们正在计算柱线的数量,但不要忘记柱线是依据交易跳价生成的,即所需的数据量会更庞大。 为了避免混淆,我更喜欢使用柱线的数量来简化解释。
因此,我们需要一种不同的计算方式。 它将判定要捕获的最短天数。 我们利用以下公式:
其中 N 是天数,F 是交易窗口关闭时的完整小时,K 是交易窗口开始时的完整小时,M 是此窗口中的分钟数。 为了清楚起见,我们研究以下示例:
美元期货于上午 9:00 开始交易,并于下午 6:30 结束。 那天我们有多少根 1 分钟的柱线呢?
n = (( 18 - 9 ) * 60 ) + 30
这等于 570 根 1 分钟柱线。
通常这个值在 540 根 1 分钟柱线左右,因为市场通常在下午 6:00 关闭交易窗口。 然而,这个数字并不精准,因为在日间,交易可能会因日内举行的拍卖而停止。 这令事情变得更加复杂。 但无论如何,由于期货具有最大的交易窗口,因此每个交易日最多将有 570 根 1 分钟柱线。
知道了这一点,您就可以将所需的柱线数量除以资产将在复制中使用的每日柱线数量。 这个每日数额是平均值。 它将为您提供移动平均线和指标在图表上正确显示所需的最短天数。
实现
我在此的意图是激励您学习构建自己的解决方案。 也许听起来我很啰嗦,但对系统的微小更改可能造成很多的不同。 由于我们将不得不使用编码模型,因此请尝试令代码尽可能快速。 以下是第一处修改:
#property service #property copyright "Daniel Jose" #property version "1.00" //+------------------------------------------------------------------+ #include <Market Replay\C_Replay.mqh> //+------------------------------------------------------------------+ input string user00 = "WIN$N_M1_202108030900_202108051754"; //File with bars ( Prev. ) input string user01 = "WINQ21_202108060900_202108061759"; //File with ticks ( Replay ) //+------------------------------------------------------------------+ C_Replay Replay;
我们添加了一行,该行将负责指定哪个文件包含要用到的先前数据。 我们将来会改变这一点,但现在就让它保持这种方式。 我不希望您在编码过程中感到困惑。 代码将渐次更改,以便您可以从中学习。
我们只能指定一个文件的事实意味着,如果我们需要使用多天,我们将不得不强制 MetaTrader 5 生成一个内含多天的文件。 这听起来很复杂,但它比看起来简单得多。
请注意上图:日期不同。 因此,我们将修复我们需要的所有柱线。 不要忘记,我们总共捕获了 1605 根 1 分钟柱线,这为我们提供了宽广的操作范围。 在计算示例中,它们的数量较少。
但我们为什么要使用 1 分钟柱线呢? 为何我们不能使用 5 或 15 分钟的柱线?
这是因为当我们有 1 分钟柱线时,我们可以采用任意时间帧。 MetaTrader 5 平台将创建不同的时间帧柱线,这非常方便,因为我们不必担心进行任何调整。 现在,如果我们所用的数据库依据 15 分钟时间的柱线,我们就无法以简单的方式来将其应用于其它时间帧。 这是因为除了使用 1 分钟柱线,我们将无法依赖 MetaTrader 5 平台提供的支持。 出于这个原因,我们最好始终使用 1 分钟柱线。
好吧,还可以用另一个时间帧。 但我们不会在此讨论它。 在处理涉及仿真的问题时,我们将会看到这一点。
但即使您不想捕获多天数据,您也可以分别捕获某些天的数据,然后将文件合并在一起。 但不要绝望。 还有另一种解决方案,如此您不必这样做。 但是在本文中,我不会在这里谈论它。 代码中的后续步骤如下所示:
void OnStart() { ulong t1; int delay = 3; long id; u_Interprocess Info; bool bTest = false; Replay.InitSymbolReplay(); Replay.LoadPrevBars(user00); if (!Replay.LoadTicksReplay(user01)) return; Print("Aguardando permissão para iniciar replay ...");
我们声明了一些服务将用到的变量。 之后,我们创建一个调用来判定要在回放中用到的品种。 重要的是,这实际上是第一个调用,因为其它所有操作都将直接在所创建品种内完成。
现在我们正在加载以前的柱线。 请注意,这里是一个逻辑序列,但是加载以前柱线的这个功能有一些有趣的细节,我们已经注意到了。 现在,我们继续,并看看系统是如何启动的。
加载之前的数值后,如果它们存在,我们加载已交易的跳价。 这是我们真正准备回放的时刻。 如果一切正常,则应在终端中打印一条消息,说明系统处于待机状态。
id = Replay.ViewReplay(); while (!GlobalVariableCheck(def_GlobalVariableReplay)) Sleep(750); Print("Permission granted. Replay service can now be used..."); t1 = GetTickCount64();
系统现在将处于此状态,等待回放模板加载。 还应打开并显示回放资产图表。 发生这种情况时,等待循环结束,
因为在这种情况下,将创建一个全局终端变量,在终端服务和控制指标之间提供链接。 我们早前已创建并讨论过这个指标。 那么, 您可以在文章“开发回放系统 — 市场模拟(第 03 部分):调整设置(I)”和“开发回放系统 — 市场模拟(第 04 部分):调整设置(II)”一文中研究它。
一旦控制指标加载,将显示一条消息。 用户现在可以在系统中按“播放”。 最后,我们捕获机器跳价的数量,以便创建一种方式来检查时间经历了多久。
之后,我们进入在前面的文章中已经详细描述的循环。 然而,这只是实现的第一部分。 我们还有其它值得特别注意的细节。
新 C_Replay 类
说来奇怪,我不得不进行一些内部更改,履行回放类的维护责任。 我将逐步详细介绍新的更改,以便您可以了解它们是什么,以及为什么它们要这样做。 我们从变量开始。 它们现在不一样了。
int m_ReplayCount; datetime m_dtPrevLoading; long m_IdReplay; struct st00 { MqlTick Info[]; int nTicks; }m_Ticks;
这组新的变量将足以满足我们的工作。 如果您看上面的代码,您会发现集合现在不同了。 结果就是,其它组件也发生了变化。 我们现在有一个新的回放品种初始化函数。
void InitSymbolReplay(void) { SymbolSelect(def_SymbolReplay, false); CustomSymbolDelete(def_SymbolReplay); CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay), _Symbol); SymbolSelect(def_SymbolReplay, true); }
我们首先从市场观察窗口中删除回放品种(如果存在)。 仅当它与打开图表的品种不同时,才会发生这种情况。
然后我们将其删除,并将其创建为自定义品种。 但为什么要做这一切呢? 稍后您就会明白原因。 创建自定义品种后,将其放置在市场观察窗口当中,以便我们可以控制它,并将其放置在图表上。 这个初始化函数将来会有变化,但现在它足以满足我们的需求。 请记住,在进行任何修改之前,必须先令系统正常工作。
下一个发生变化的函数是交易跳价的加载。
#define macroRemoveSec(A) (A - (A % 60)) bool LoadTicksReplay(const string szFileNameCSV) { int file, old; string szInfo; MqlTick tick; if ((file = FileOpen("Market Replay\\Ticks\\" + szFileNameCSV + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE) { Print("Loading replay ticks. Wait..."); ArrayResize(m_Ticks.Info, def_MaxSizeArray); old = m_Ticks.nTicks = 0; for (int c0 = 0; c0 < 7; c0++) FileReadString(file); while ((!FileIsEnding(file)) && (m_Ticks.nTicks < def_MaxSizeArray)) { szInfo = FileReadString(file) + " " + FileReadString(file); tick.time = macroRemoveSec(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; m_Ticks.nTicks += (tick.volume_real > 0.0 ? 1 : 0); old = (m_Ticks.nTicks > 0 ? m_Ticks.nTicks - 1 : old); } } }else { Print("Tick file ", szFileNameCSV,".csv not found..."); return false; } return true; }; #undef macroRemoveSec
首先,我们尝试打开一个内含已完成交易跳价的文件。 注意在此阶段,我们还没有任何检查。 因此,指定正确的文件名时要小心。
该文件必须位于 MQL5 目录中的指定文件夹之中。 但是,您不必指定文件扩展名,因为默认值为 CSV。如果找到该文件,我们将继续加载。 如果未找到该文件,回放服务将不会启动,并且消息框中会显示一条消息,指示失败的原因。
我们看看如果找到交易的跳价文件会发生什么。 在这种情况下,我们要做的第一件事是跳过文件第一行的内容,现在不需要。 之后,我们进入循环,以便读取信息。 我们要做的第一件事是捕获已交易跳价的日期和时间,然后将数据保存到临时位置。 然后,我们以正确的顺序捕获每个数值。 注意,我们不需要指定任何东西,拥有 CSV 文件格式就足够了 — MQL5 应该正确执行读取。
现在我们检查以下条件:如果最后一笔交易的价格和时间等于当前交易的价格和时间,则当前交易量将添加到前一笔交易之中。 故此,为了做到这一点,两个检查都必须为真。
如果您注意到,我其实不关心标志问题,这意味着它是买入还是卖出都无所谓。 在其它条件相同的情况下,这不会影响回放,至少目前是这样,因为价格并未移动。 现在,如果检查条件失败,我们则有一个指示,表明我们正在读取不同的跳价。 在这种情况下,我们将它添加到我们的跳价矩阵当中。 一个重要的细节:如果跳价是买入价或卖出价调整,则其中就没有交易量。 故此,我们不会添加新仓位,当读取新的跳价时,该仓位将被覆盖。 但如果有一些交易量,持仓就会增加。
我们继续此循环,直到文件结束,或达到跳价限制。 但要小心,因为我们还没有运行任何测试。 由于系统仍然非常简单,用到的数据量也不大,因此可以忽略一些小缺陷。 但是随着时间的推移,该函数中的检查量会增加,以便避免迄今为止还未给我们带来太多麻烦的问题。
下一个函数的代码如下所示。
bool LoadPrevBars(const string szFileNameCSV) { int file, iAdjust = 0; datetime dt = 0; MqlRates Rate[1]; if ((file = FileOpen("Market Replay\\Bars\\" + szFileNameCSV + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE) { for (int c0 = 0; c0 < 9; c0++) FileReadString(file); Print("Loading preview bars to Replay. Wait ...."); while (!FileIsEnding(file)) { Rate[0].time = StringToTime(FileReadString(file) + " " + FileReadString(file)); Rate[0].open = StringToDouble(FileReadString(file)); Rate[0].high = StringToDouble(FileReadString(file)); Rate[0].low = StringToDouble(FileReadString(file)); Rate[0].close = StringToDouble(FileReadString(file)); Rate[0].tick_volume = StringToInteger(FileReadString(file)); Rate[0].real_volume = StringToInteger(FileReadString(file)); Rate[0].spread = (int) StringToInteger(FileReadString(file)); iAdjust = ((dt != 0) && (iAdjust == 0) ? (int)(Rate[0].time - dt) : iAdjust); dt = (dt == 0 ? Rate[0].time : dt); CustomRatesUpdate(def_SymbolReplay, Rate, 1); } m_dtPrevLoading = Rate[0].time + iAdjust; FileClose(file); }else { Print("Failed to access the previous bars data file."); m_dtPrevLoading = 0; return false; } return true; }
上面的代码将给定文件拥有,或所需的所有以前柱线上传到回放之中。 但是,与需要保存信息以供以后使用的上一个函数不同,这里我们将以不同的方式进行操作。 我们读取信息,并立即将其添加到品种之中。 遵照说明了解其工作原理。
首先,我们将尝试打开一个含有柱线的文件。 请注意,我们有配合跳价的相同命令。 不过,位置不同,文件内容也不同,但访问信息的方式将完全相同。 如果我们成功打开文件,我们将首先跳过第一行,因为目前我们对其不感兴趣。
这次我们进入循环,并只会在文件的最后一行才结束。 此文件中的数据大小或数量没有限制。 结果就是,将读取所有数据。 读取的数据遵循 OHCL 模型,因此我们将其放置在临时结构当中。 现在,请注意以下几点:我们如何知道回放在哪个点开始,以及前一根柱线在哪里结束?
在此函数之外,它会很复杂。 但在此,我们有一个确切的指示,表明前一根柱线在哪里结束,回放在哪里开始。 正是在这个精确点,我们存储了这个临时位置,以供以后使用。 现在,请注意,对于我们读取的每一行柱线,我们立即更新回放品种中包含的柱线。 以这种方式,我们以后就不会有任何额外的工作。 故此,当读取前一根柱线后,我们将能够添加我们的指标。
我们继续下一个函数,它非常简单但却非常重要。
long ViewReplay(void) { m_IdReplay = ChartOpen(def_SymbolReplay, PERIOD_M1); ChartApplyTemplate(m_IdReplay, "Market Replay.tpl"); ChartRedraw(m_IdReplay); return m_IdReplay; }
它在标准的 1 分钟时间帧内打开回放品种的图表。 您可以采用不同的标准时间帧来避免持续更改它。
之后,我们加载模板,其中应包含回放指标。 如果指标不在模板当中,我们就无法播放该服务。 好吧,可以使用不同的模板,但在这种情况下,我们将不得不手动将回放指标添加到交易品种图表之中,以便开始回放。 如果这样适合您手动执行此操作,那很好,您可以自由地执行此操作。 然而,通过使用含有指标的模板,我们就可以立即访问系统,因为我们将强制更新图表,并返回图表索引,以便检查它是否可用。
由于我们正在启动系统,因此我们还需要将其关闭,为此我们还有另一个函数。
void CloseReplay(void) { ChartClose(m_IdReplay); SymbolSelect(def_SymbolReplay, false); CustomSymbolDelete(def_SymbolReplay); GlobalVariableDel(def_GlobalVariableReplay); }
这里的关键点是执行操作的顺序非常重要。 如果我们改变了顺序,事情可能不会按预期进行。 因此,我们首先关闭品种图表,将其从市场观察窗口中删除,从自定义品种列表中删除,最后从终端中删除全局变量。 之后,回放服务将关闭。
以下函数也进行了一些修改。
inline int Event_OnTime(void) { bool bNew; int mili, iPos; u_Interprocess Info; static MqlRates Rate[1]; static datetime _dt = 0; if (m_ReplayCount >= m_Ticks.nTicks) return -1; if (bNew = (_dt != m_Ticks.Info[m_ReplayCount].time)) { _dt = m_Ticks.Info[m_ReplayCount].time; 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; bNew = false; m_ReplayCount++; } mili++; }while (mili == m_Ticks.Info[m_ReplayCount].time_msc); Rate[0].time = m_Ticks.Info[m_ReplayCount].time; CustomRatesUpdate(def_SymbolReplay, Rate, 1); iPos = (int)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks); GlobalVariableGet(def_GlobalVariableReplay, Info.Value); if (Info.s_Infos.iPosShift != iPos) { Info.s_Infos.iPosShift = iPos; GlobalVariableSet(def_GlobalVariableReplay, Info.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); }
上面的函数创建一个 1 分钟柱线,并将其添加到回放品种当中。 但在此,我们应该注意一些事情:为什么回放显示这些内容,而不是其它?
您可能已经注意到,跳价的交易量始终为零。 为什么? 若要理解这一点,您需要检查“真实和生成的跳价”的文档。 如果读完,但不理解描述,或不理解为什么跳价的交易量始终为零,那说明您忽略了一个重要事实。 因此,我们来尝试理解为什么此值始终为零。
当我们读取交易跳价的数据时,我们读取真实的跳价,即所显示交易量是真实的。 例如,如果订单的交易量等于 10,那么这意味着交易的实际交易量为 10 个最低要求手数,而不会使用 10 个跳价。 不可能产生 1 个交易跳价的交易量。 但是,有一种情况我们确实有跳价的交易量,那就是当订单开立和平仓时,导致交易量为 2 个跳价。 然而,在实践中,我们的最小跳价交易量是 3。 由于我们用到真实的交易跳价,我们需要对呈现的数值进行调整。 到此刻,我还没有将此计算添加到回放系统之中,因此实际上只会显示交易的真实数值。
对此要小心,因为尽管看起来相同,但真实交易量显示实际发生了多少笔交易,而跳价交易量显示发生了多少次变动。
我知道这似乎非常令人困惑且难以理解,但是在下面的文章中,当我们进入外汇市场,并讨论如何在这个市场中进行回放和模拟时,一切都会变得更加清晰。 故此,您现在不必纠结完全理解一切 — 随着时间的推移,您会理解这一点。
本文将讨论的最后一个函数如下所示:
int AdjustPositionReplay() { u_Interprocess Info; MqlRates Rate[1]; int iPos = (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks); Info.Value = GlobalVariableGet(def_GlobalVariableReplay); if (Info.s_Infos.iPosShift == iPos) return 0; iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / def_MaxPosSlider)); if (iPos < m_ReplayCount) { CustomRatesDelete(def_SymbolReplay, m_dtPrevLoading, LONG_MAX); m_ReplayCount = 0; if (m_dtPrevLoading == 0) { Rate[0].close = Rate[0].open = Rate[0].high = Rate[0].low = m_Ticks.Info[m_ReplayCount].last; Rate[0].tick_volume = 0; Rate[0].time = m_Ticks.Info[m_ReplayCount].time - 60; CustomRatesUpdate(def_SymbolReplay, Rate, 1); } }; for (iPos = (iPos > 0 ? iPos - 1 : 0); m_ReplayCount < iPos; m_ReplayCount++) Event_OnTime(); return Event_OnTime(); }
在此函数中,只有一件事与以前的不同,这一件事将令我们能够开始插入交易的跳价。 因此,高于此点的所有内容都被视为真实交易跳价的回放,低于此点的是先前柱线的值。 如果我们此时只通知一个等于零的值,则回放品种中记录的所有信息都将被删除,并且我们必须重新读取 1 分钟数据柱线才能获得前一根柱线的值。 这是多余的,而且要困难得多。 因此,我们需要做的就是指出阈值的点,MetaTrader 5 将删除由真实跳价创建的柱线,令我们的生计更加轻松。
结束语
函数中的其它所有内容已在文章开发回放系统 — 市场模拟(第 04 部分):调整设置(II) 中有所讲述,因此我们不会在这里赘述。
您可以在下面的视频中看到系统的运行情况。 它演示了如何将不同的指标添加到回放系统当中。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10704