开发回放系统 — 市场模拟(第 04 部分):调整设置(II)
概述
在上一篇文章“开发回放系统 — 市场模拟(第 03 部分):调整设置(I)”中,我们创建了一个可以轻松管理市场回放服务的 EA。 到目前为止,我们已经设法实现了重要的一点:暂停和播放系统。 我们尚未创建任何类型的控制机制,来允许您选择所需的回放起始位置。 也就是说,尚不可能从周期中间,或从另一个特定时间点开始回放。 我们总是要从数据的开头开始,这对于想要进行训练的人来说是不切实际的。
这一次,我们将实现以最简单的方式选择回放起点的功能。 针对一些喜欢这个系统,并希望在他们自己的 EA 中使用它的朋友,我们还要遵照他们的需求对策略进行小幅修改。 故此,我们将对系统进行相应的修改,从而允许这样做。
现在,我们就以此方式来演示如何实际创建新应用程序。 许多人认为这些会凭空而来,从想法诞生的那一刻到系统和代码的完全稳定,最后应用程序能够完全按照我们的期望去做,却对整个过程如何发生没有正确的概念。
将 EA 换成指标
这样的修改相当容易实现。 之后,我们就能够用我们自己的 EA 来研究如何利用市场回放服务,或在现实市场上进行交易。 例如,我们就能够使用我在之前文章中展示的 EA。 参阅“从头开始开发交易 EA”系列中的更多内容。 虽然并非旨在 100% 自动化,但它可以适应在回放服务中使用。 但是,我们把它留待将来。 此外,我们还将能够使用“创建自动运行的 EA(第 01 部分):概念和结构”系列中的一些 EA,其中我们研究过如何创建在全自动模式下工作的智能系统。
不过,我们目前的焦点不是 EA(我们将在未来探讨这一点),而是其它东西。
完整的指标代码可以在下面看到。 它完全包括 EA 中已经存在的功能,同时将其实现为指标:
#property copyright "Daniel Jose" #property indicator_chart_window #property indicator_plots 0 //+------------------------------------------------------------------+ #include <Market Replay\C_Controls.mqh> //+------------------------------------------------------------------+ C_Controls Control; //+------------------------------------------------------------------+ int OnInit() { IndicatorSetString(INDICATOR_SHORTNAME, "Market Replay"); Control.Init(); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) { return rates_total; } //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { Control.DispatchMessage(id, lparam, dparam, sparam); } //+------------------------------------------------------------------+
唯一的区别是增加了一个短名,其最好包含在指标中。 这部分在上面的代码中都以高亮显示。 通过这样做,我们获得了额外的优势:我们可以使用任何 EA 基于回放服务进行练习和训练。 在任何人提出可能的问题之前,我会回答它:市场回放并非策略测试器。 它是针对那些想要练习读取市场,改善预测资产走势来达成稳定盈利的人。 市场回放并不能取代出色的 MetaTrader 5 策略测试器。 不过,策略测试器并不适合实施市场回放。
虽然乍一看转换似乎没有副作用,但这并不完全如实。 当运行回放系统,并由指标替 EA 控制交易完成时,您会注意到失败。 当图表时间帧变更时,指标将从图表中删除,然后重新启动。 这种删除并重新启动它的操作,令指示我们处于暂停或播放模式的按钮状态与回放系统的实际状态不一致。 为了修复此问题,我们需要进行一些小的调整。 故此,我们将有以下指标启动代码:
int OnInit() { u_Interprocess Info; IndicatorSetString(INDICATOR_SHORTNAME, "Market Replay"); if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.Value = 0; Control.Init(Info.s_Infos.isPlay); return INIT_SUCCEEDED; }
代码中添加的高亮显示内容可确保回放服务的状态与我们在图表上看到的按钮匹配。 控制代码的修改非常简单,不需要特别留意。
现在,以前使用 EA 的模板文件现在将切换到使用指标。 这让我们可以完全自由地在未来进行其它修改。
位置控制实现
在此,我们将实现一个控制功能,来指示我们要进入重播文件中的位置,便以开始我们的市场研究。 但这不会是一个精准的所在。 起始位置将是近似值。 这并非因为不可能做这样的事情。 与之对,指出确切的所在会容易得多。 然而,在与那些在市场上有更多经验的人谈论并交流经验时,我们发现了一个共识。 理想的选择不是跳转到我们所期望的特定走势的确切之处,而是在接近所需走势的所在开始回放。 换言之,在采取行动之前,您需要明白正在发生的事情。
这个思路对我来说似乎很好,所以我决定:市场回放不应该跳到一个特定的位置点。 尽管那样更容易实现,但您需要转到最近的位置点。 哪个位置点最接近,取决于每天执行的交易数量。 我们执行的交易越多,就越难达到确切的位置。
故此,我们将访问附近的位置点,来搞清实际发生的情况,以便创建实际的交易模拟。 再次:我们不是在创建策略测试器。但通过这样做,随着时间的推移,您将从中学会判定何时走势更安全,或何时风险过高,且您不应该入场交易。
此步骤中的所有工作都将在 C_Control 类中完成。 所以,现在我们开工吧!
我们要做的第一件事是厘定一些定义。
#define def_ButtonLeft "Images\\Market Replay\\Left.bmp" #define def_ButtonRight "Images\\Market Replay\\Right.bmp" #define def_ButtonPin "Images\\Market Replay\\Pin.bmp"
现在我们需要创建一组变量来存储位置系统数据。 它们的实现方式如下:
struct st_00 { string szBtnLeft, szBtnRight, szBtnPin, szBarSlider; int posPinSlider, posY; }m_Slider;
这正是您刚刚注意到的。 我们将使用滑块指向回放系统理应启动的大致位置。 我们现在有一个通用函数,用于创建播放/暂停按钮和滑块按钮。 此函数如下所示。 我认为理解它不会有任何困难,因为它非常简单。
inline void CreateObjectBitMap(int x, int y, string szName, string Resource1, string Resource2 = NULL) { ObjectCreate(m_id, szName, OBJ_BITMAP_LABEL, 0, 0, 0); ObjectSetInteger(m_id, szName, OBJPROP_XDISTANCE, x); ObjectSetInteger(m_id, szName, OBJPROP_YDISTANCE, y); ObjectSetString(m_id, szName, OBJPROP_BMPFILE, 0, "::" + Resource1); ObjectSetString(m_id, szName, OBJPROP_BMPFILE, 1, "::" + (Resource2 == NULL ? Resource1 : Resource2)); }
好吧,现在每个按钮都将调用此函数来创建。 事情因此变得容易很多,并增加了代码重用度,从而令事情更加稳定和快速。 接下来要创建的是一个函数,该函数将表示要在滑块中使用的通道。 它由以下函数创建:
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); }
此处最奇怪的是控制通道边界的表示。 您可以根据需要自定义此设置,以及通道的宽度,该宽度在 OBJPROP_YSIZE 属性中设置。 但在更改此属性的数值时,请不要忘记调整该数值,减去 m_Slider.posY,以便通道位于按钮之间。
创建播放/暂停按钮的函数现在如下所示:
void CreateBtnPlayPause(long id, bool state) { m_szBtnPlay = def_PrefixObjectName + "Play"; CreateObjectBitMap(5, 25, m_szBtnPlay, def_ButtonPause, def_ButtonPlay); ObjectSetInteger(id, m_szBtnPlay, OBJPROP_STATE, state); }
容易得多,不是吗? 现在我们来看一下创建滑块的函数。 它如下所示:
void CreteCtrlSlider(void) { u_Interprocess Info; m_Slider.szBarSlider = def_PrefixObjectName + "Slider Bar"; m_Slider.szBtnLeft = def_PrefixObjectName + "Slider BtnL"; m_Slider.szBtnRight = def_PrefixObjectName + "Slider BtnR"; m_Slider.szBtnPin = def_PrefixObjectName + "Slider BtnP"; m_Slider.posY = 40; CreteBarSlider(82, 436); CreateObjectBitMap(52, 25, m_Slider.szBtnLeft, def_ButtonLeft); CreateObjectBitMap(516, 25, m_Slider.szBtnRight, def_ButtonRight); 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.Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.Value = 0; PositionPinSlider(Info.s_Infos.iPosShift); }
仔细查看控件的名称,这非常重要。 当回放系统处于重播状态时,这些控件将不可用。 每次我们处于暂停状态时,都会调用该函数,从而创建滑块。 请注意因为这一点,我们将捕获终端全局变量的数值,以便正确识别和定位滑块。
因此,我建议您不要手工对终端的全局变量执行任何操作。 请注意另一个重要细节,即顶针。 与按钮不同,设计它时会在中心有一个锚点,便于查找。 这里我们还有另一个函数调用:
inline void PositionPinSlider(int p) { 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); ChartRedraw(); }
它将滑块放置在某个区域中,并确保它驻留在上面的受限设置内。
您可以想象,我们仍然需要对系统进行小的调整。 每次图表时间帧变更时,指标都会重置,导致我们失去在回放系统中存在的实际当前位置点。 避免这种情况的途径之一是在初始化函数里进行一些扩充。 取这些变更的好处,我们还将添加一些其它内容。 我们来看看初始化函数现在是什么样子的:
void Init(const bool state = false) { if (m_szBtnPlay != NULL) return; m_id = ChartID(); ChartSetInteger(m_id, CHART_EVENT_MOUSE_MOVE, true); CreateBtnPlayPause(m_id, state); GlobalVariableTemp(def_GlobalVariableReplay); if (!state) CreteCtrlSlider(); ChartRedraw(); }
现在我们还要添加代码,以便将鼠标移动事件转发到指标。 如果不加这些,鼠标事件就会丢失,并且不会由 MetaTrader 5 传递给指标。 为了在不需要时隐藏滑块,我们添加了一些检查。 如果检查到确认应显示滑块,那么它就被显示在屏幕上。
到目前为止,我们已经见识到了一切,您可能想知道:事件处理现在如何执行? 我们会有一些超级复杂的额外代码吗? 好吧,处理鼠标事件的方式没有太大变化。 添加拖动事件并不是很复杂。 您真正要做的就是管控一些限制,以免事情失控。 而实现本身非常简单。
我们来看一下处理所有这些事件的函数的代码:DispatchMessage。 为了便于解释,我们分部分查看代码。 第一部分负责处理对象单击事件。 请看下面的代码:
void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) { u_Interprocess Info; //... other local variables .... switch (id) { case CHARTEVENT_OBJECT_CLICK: if (sparam == m_szBtnPlay) { Info.s_Infos.isPlay = (bool) ObjectGetInteger(m_id, m_szBtnPlay, OBJPROP_STATE); if (!Info.s_Infos.isPlay) CreteCtrlSlider(); else { ObjectsDeleteAll(m_id, def_PrefixObjectName + "Slider"); m_Slider.szBtnPin = NULL; } Info.s_Infos.iPosShift = m_Slider.posPinSlider; GlobalVariableSet(def_GlobalVariableReplay, Info.Value); ChartRedraw(); }else if (sparam == m_Slider.szBtnLeft) PositionPinSlider(m_Slider.posPinSlider - 1); else if (sparam == m_Slider.szBtnRight) PositionPinSlider(m_Slider.posPinSlider + 1); break; // ... The rest of the code...
当我们按下播放/暂停按钮时,我们需要执行几个操作。 其中之一是如果我们处于暂停状态,则需创建一个滑块。 如果我们退出暂停状态,并进入播放状态,则必须从图表中隐藏控件,从而我们能不再误碰它们。 滑块的当前值应发送到终端的全局变量。 因此,回放服务可以访问我们想要放置回放系统的位置对应的百分比位置。
除了与播放/暂停按钮相关的这些问题外,我们还需要应对单击滚动条的逐点移动按钮时发生的事件。 如果我们单击滚动条的左按钮,滑块的当前值应该减少 1。 与此类似,如果我们按下滚动条的右按钮,控件当前值加 1,直到最大设置限制。
这很简单。 至少在这一部分中,应对对象点击消息并不难。 然而,现在拖动滑块存在一个稍微复杂的问题。 为了搞清这一点,我们来看一下处理鼠标移动事件的代码。 它如下所示:
void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) { u_Interprocess Info; static int six = -1, sps; int x, y, px1, px2; switch (id) { // ... Object click EVENT ... case CHARTEVENT_MOUSE_MOVE: x = (int)lparam; y = (int)dparam; px1 = m_Slider.posPinSlider + def_MinPosXPin - 14; px2 = m_Slider.posPinSlider + def_MinPosXPin + 14; if ((((uint)sparam & 0x01) == 1) && (m_Slider.szBtnPin != NULL)) { if ((y >= (m_Slider.posY - 14)) && (y <= (m_Slider.posY + 14)) && (x >= px1) && (x <= px2) && (six == -1)) { six = x; sps = m_Slider.posPinSlider; ChartSetInteger(m_id, CHART_MOUSE_SCROLL, false); } if (six > 0) PositionPinSlider(sps + x - six); }else if (six > 0) { six = -1; ChartSetInteger(m_id, CHART_MOUSE_SCROLL, true); } break; } }
它看起来有点复杂,但实际上就像处理对象单击一样简单。 唯一的区别是,现在我们将不得不用到更多的变量,其中一些必须是静态的,这样数值就不会在调用之间丢失。 当鼠标移动时,MetaTrader 5 会向我们的系统发送一条消息。 我们应该依据此消息来找出发生了什么,并找出鼠标光标的位置、按下的按钮、或其它一些信息。 所有这些信息都来自 MetaTrader 5 发送到我们应用程序的消息。
当按下左键时,有一些事情要做。 但是为了确保滑块在屏幕上,并且我们不会收到误报,我们提供了一个额外的测试来确保我们正在做的事情的完整性。
如果测试表明事件有效,我们运行另一个测试来检查我们是否正在单击滑块,即位于属于滑块的区域。 与此同时,我们检查此位置是否仍然有效,因为可能会发生已经点击,但该位置无效的情况。 若是这种情况下,我们应该忽略它。如果此检查成功,我们将保存单击位置和控件值。 我们还需要锁定图表拖动。 这对于下一步是必需的,我们将根据控件中存在的先前值计算滑块的位置。在进行任何计算之前保存此数据非常重要,因为它可以更轻松地设置和理解在这种情况下如何进行。 但此处完成的方式很容易实现,因为计算实际上是偏差计算。
松开左键后,情况将恢复到原始模式。 也就是说,图形将再次可拖动,用于存储鼠标位置的静态变量的数值则代表没有任何位置正在分析。 相同的方法可用于在图表上拖放任何内容,这是另一个很大的优势。 所有这些都是通过单击和拖动来完成的。 然后,您需要做的就是分析可以接收点击的区域在哪里。 微调这一点,其余的将由代码完成。 它的代码如上面所示。
完成此操作后,我们已经在控件中含有所需的行为。 但我们尚未完工。 我们必须强制服务采用我们在滑块中指定的值。 我们将在下一主题中实现这一点。
调整 C_Replay 类
事情永远不会如某些人想象的完全一样。 因为我们仅仅创建了一个滑块,并在控件类(C_Control)中设置了一些东西,这并不意味着一切都完美无缺了。 我们需要对实际构建回放的类进行一些调整。
这些调整不是很复杂。 事实上,这些只是少量,而且它们在非常具体的点上。 不过,请务必注意,对一个类所做的任何修改都会影响其它类。 但是您不一定必须在其它点上进行任何修改。 我宁愿永不触及不必要的点,并且始终尽可能地把封装提升到最大级别,从而隐藏整个系统的复杂性。
我们直指要点。 首先要做的是设置 Event_OnTime 函数。 它负责把交易跳价添加到复现资产。 基本上,我们应给该函数添加一点东西。 查看下面的代码:
#define macroGetMin(A) (int)((A - (A - ((A % 3600) - (A % 60)))) / 60) inline int Event_OnTime(void) { bool isNew; int mili, test; static datetime _dt = 0; u_Interprocess Info; if (m_ReplayCount >= m_ArrayCount) return -1; if (m_dt == 0) { m_Rate[0].close = m_Rate[0].open = m_Rate[0].high = m_Rate[0].low = m_ArrayInfoTicks[m_ReplayCount].Last; m_Rate[0].tick_volume = 0; m_Rate[0].time = m_ArrayInfoTicks[m_ReplayCount].dt - 60; CustomRatesUpdate(def_SymbolReplay, m_Rate, 1); _dt = TimeLocal(); } isNew = m_dt != m_ArrayInfoTicks[m_ReplayCount].dt; m_dt = (isNew ? m_ArrayInfoTicks[m_ReplayCount].dt : m_dt); mili = m_ArrayInfoTicks[m_ReplayCount].milisec; do { while (mili == m_ArrayInfoTicks[m_ReplayCount].milisec) { m_Rate[0].close = m_ArrayInfoTicks[m_ReplayCount].Last; m_Rate[0].open = (isNew ? m_Rate[0].close : m_Rate[0].open); m_Rate[0].high = (isNew || (m_Rate[0].close > m_Rate[0].high) ? m_Rate[0].close : m_Rate[0].high); m_Rate[0].low = (isNew || (m_Rate[0].close < m_Rate[0].low) ? m_Rate[0].close : m_Rate[0].low); m_Rate[0].tick_volume = (isNew ? m_ArrayInfoTicks[m_ReplayCount].Vol : m_Rate[0].tick_volume + m_ArrayInfoTicks[m_ReplayCount].Vol); isNew = false; m_ReplayCount++; } mili++; }while (mili == m_ArrayInfoTicks[m_ReplayCount].milisec); m_Rate[0].time = m_dt; CustomRatesUpdate(def_SymbolReplay, m_Rate, 1); mili = (m_ArrayInfoTicks[m_ReplayCount].milisec < mili ? m_ArrayInfoTicks[m_ReplayCount].milisec + (1000 - mili) : m_ArrayInfoTicks[m_ReplayCount].milisec - mili); test = (int)((m_ReplayCount * def_MaxPosSlider) / m_ArrayCount); GlobalVariableGet(def_GlobalVariableReplay, Info.Value); if (Info.s_Infos.iPosShift != test) { Info.s_Infos.iPosShift = test; GlobalVariableSet(def_GlobalVariableReplay, Info.Value); } return (mili < 0 ? 0 : mili); }; #undef macroGetMin
在这个函数中,我们构建 1 分钟柱线。 请注意,我们在这一部分添加了一个变量:该变量在上面的代码中不存在。 现在,我们将在终端的全局变量中存储表示相对百分比位置。 由此,我们需要这个变量来解码存储在终端变量中的内部内容。 一旦交易的跳价被添加到 1 分钟柱线,我们需要知道当前回放位置的百分比是多少。 这是在这个计算中完成的,我们根据保存的跳价总数找出其相对位置。
然后将该数值与存储在终端全局变量中的数值进行比较。 如果它们不同,我们会更新该值,以便系统在停止时指示正确的相对位置。 以这种方式,您就不必进行额外的计算,或遇到不必要的问题。
第一阶段到此结束。 不过,我们还有一个问题需要解决。 暂停期间调整数值后,如何将回放系统定位在所期望的相对位置?
这个问题有点复杂。 这是因为我们可以同时进行加法(更容易解决)和减法(稍微复杂一些)。 这种减法不是大问题,至少在这个开发阶段是这样。 但在下一阶段它就会是一个打问题,我们将在本系列的下一篇文章中看到。 但首先要做的是在 C_Replay 类中添加一个额外的函数,用于从回放系统中加上或减去柱线。 我们来看看这个函数的准备:
int AdjustPositionReplay() { u_Interprocess Info; int test = (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_ArrayCount); Info.Value = GlobalVariableGet(def_GlobalVariableReplay); if (Info.s_Infos.iPosShift == test) return 0; test = (int)(m_ArrayCount * ((Info.s_Infos.iPosShift * 1.0) / def_MaxPosSlider)); if (test < m_ReplayCount) { CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX); CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX); m_ReplayCount = 0; m_Rate[0].close = m_Rate[0].open = m_Rate[0].high = m_Rate[0].low = m_ArrayInfoTicks[m_ReplayCount].Last; m_Rate[0].tick_volume = 0; m_Rate[0].time = m_ArrayInfoTicks[m_ReplayCount].dt - 60; CustomRatesUpdate(def_SymbolReplay, m_Rate, 1); }; for (test = (test > 0 ? test - 1 : 0); m_ReplayCount < test; m_ReplayCount++) Event_OnTime(); return Event_OnTime(); }
在上面的代码中,我们看到一段代码,它是这个定制系统的基础。 我们来搞明白这个基本系统中会发生什么。 首先,我们生成当前位置的百分比值。 然后将该数值与终端的全局变量中的数值进行比较。 控制系统将该数值记录在全局变量当中。 如果数值相等(不是绝对值,而是百分比值),则函数将退出,因为我们正处于正确的百分点,或者用户在系统暂停期间未更改位置。
但是,如果数值不同,则根据终端全局变量中指定的百分比值生成绝对值。 也就是说,现在我们将有一个绝对位置点,回放系统应该从该位置点开始。 由于多种原因,该值不太可能等于交易跳价的计数器。 如果它小于回放计数器的当前值,则将删除当前资源中存在的所有数据。
这很棘手,但在这个开发阶段不会如此。 它将在下一步中完成。 目前,没有理由过度关注。 现在我们可以针对这两种情况做一些共同的事情:添加新值,直到回放计数器的位置等于绝对位置减 1。 这个减 1 的原因,是允许该函数返回一个值,该值稍后将用作延迟。 这是由 Event_OnTime 函数达成的。
至于这种变化类型,它总是伴随着痛苦。 我们来看看系统中需要修改什么。 这些显示在下面的代码当中。 这是唯一被更改的地方:
#property service #property copyright "Daniel Jose" #property version "1.00" //+------------------------------------------------------------------+ #include <Market Replay\C_Replay.mqh> //+------------------------------------------------------------------+ input string user01 = "WINZ21_202110220900_202110221759"; //File with ticks //+------------------------------------------------------------------+ C_Replay Replay; //+------------------------------------------------------------------+ void OnStart() { ulong t1; int delay = 3; long id; u_Interprocess Info; bool bTest = false; if (!Replay.CreateSymbolReplay(user01)) return; id = Replay.ViewReplay(); Print("Wait for permission to start replay ..."); while (!GlobalVariableCheck(def_GlobalVariableReplay)) Sleep(750); Print("Replay system started ..."); t1 = GetTickCount64(); while ((ChartSymbol(id) != "") && (GlobalVariableGet(def_GlobalVariableReplay, Info.Value))) { if (!Info.s_Infos.isPlay) { if (!bTest) bTest = (Replay.Event_OnTime() > 0); }else { if (bTest) { delay = ((delay = Replay.AdjustPositionReplay()) == 0 ? 3 : delay); bTest = false; t1 = GetTickCount64(); }else if ((GetTickCount64() - t1) >= (uint)(delay)) { if ((delay = Replay.Event_OnTime()) < 0) break; t1 = GetTickCount64(); } } } Replay.CloseReplay(); Print("Replay system stopped ..."); } //+------------------------------------------------------------------+
当我们处于暂停模式时,我们将运行此测试,以便查看我们是否正在更改服务的状态。 若这种情况发生,我们将要求 C_Replay 类执行新的定位,不过该定位可能会执行,也可能不会执行。
如果执行,我们将获得下一个延迟的数值,系统调整并定位后会用到该数值。 如有必要,我们自然会继续回放剩余的时间,直到我们退出回放状态,并进入暂停状态。 然后整个过程将再次重复。
结束语
该视频显示了整个系统的运行情况,您可以亲眼看到一切是如何发生的。 不过,重要的是要注意,您需要等到所有事情稳定后才能使用回放系统。 当将位置移动到所期望位置点时,似乎难以执行移动。
这种状况会在将来得到纠正。 但我们现在可以接受它,因为我们还有很多事情要理清。
在附件中,我包含了两个真实的市场跳价文件,如此您就可以基于不同交易跳价数量的日子里尝试移动和定位系统。 如此,您就可看到系统的百分比工作原理。 对于那些想要研究市场特定时刻的人来说,这会令事情变得复杂,但这恰恰正是我们的本意,如本文开头所述。
通过我们在这里构建的这个回放系统,您能真正学习如何分析市场。 没有确切的地方您会说,“这里...这是我应该入场的地方"。因为它可能会发生,以至于您观察到的走势实际上发生在几根柱线之外。 因此,您必须学习如何分析市场,否则您也许就不喜欢本系列文章中介绍的回放系统。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10714