
开发回放系统 — 市场模拟(第 01 部分):首次实验(I)
概述
在撰写“从头开始开发智能系统”系列文章时,我遇到了一些时刻,令我意识到比之已完成的 MQL5 编程部分,可以做得更多。 其中一个时刻是我开发了一个图形的 Times & Trade 系统。 在那篇文章中,我想知道是否有可能超越以前构建的东西。
萌新交易者最常见的抱怨之一是 MetaTrader 5 平台缺乏某些功能。 在这些功能中,有一个,在我看来是有意义的:市场模拟或回放系统。 对于新的市场参与者来说,拥有某种机制或工具,令他们能够测试、验证、甚至研究资产,这将是一件好事。 其中一个工具是回放和模拟系统。
MetaTrader 5 在标准安装包中不包含此功能。 由此,依每个用户决定如何进行此类研究。 不过,在 MetaTrader 5 中,您可以找到许多任务的解决方案,因为该平台非常实用。 但为了能够真正充分发挥它的潜力,您需要有良好的编程经验。 我不光是指 MQL5 编程,而是一般的编程。
如果您在这方面没有太多经验,则您只能卡在基础程度。 您因此缺乏更充足的手段或更好的方法,难以在市场上大展身手(就成为杰出的交易者而言)。 因此,除非您有优良的编程知识水平,否则您将无法真正使用 MetaTrader 5 提供的所有内容。 即使是有经验的程序员,也可能缺乏为 MetaTrader 5 创建某些类型程序或应用的兴趣。
事实上,只有少数人愿意为初学者创建可行的系统。 甚至还有一些免费的建议来创建市场回放系统。 但在我看来,这些并没有真正利用 MQL5 提供的功能。 它们通常需要使用具有封闭代码的外部 DLL。 我认为这不是一个好主意。 更重要的是,您并不真正知道此类 DLL 的来源,或其中存在的内容,这令整个系统面临风险。
我不知道这个系列将包括多少篇文章,但它将是关于开发一套有效的回放系统。 我将向您展示如何创建代码来实现此回放。 但这并非全部。 我们还将开发一个系统,令我们能够模拟任何市场情形,无论它多么奇怪或罕见。
一个奇怪的事实是,许多人在谈论交易量化时,实际上并没有真正意识到他们在谈论什么,因为没有实际的途径进行涉及此类事情的研究。 但是,如果您了解我将在本系列中描述的概念,您就能够将 MetaTrader 5 转换为定量分析系统。 因此,可能性将远远超出我在这里实际揭示的范围。
为了不至于过于重复和累人,我会把系统当作回访来对待。 虽然正确的术语是回放/模拟,因为除了分析过去的走势外,我们还可以开发自己的走势来研究它们。 因此,不要将这个系统仅仅视为市场回放,而是将其视为市场模拟器,甚至是市场“游戏”,因为它也将涉及大量游戏编程。 在某些时候,这种在游戏中大量涉及的编程类型将是必要的。 但我们将在开发和增强系统的同时逐步看到这一点。
规划
首先,我们需要明白我们正在应对什么。 这也许看起来很奇怪,但您真的知道当您使用回放/模拟系统时自己想要实现什么吗?
在创建市场回放时,存在一些非常棘手的问题。 其中之一,也许是主要的那个,是资产的生存周期和有关它们的信息。 如果您不明白这一点,请务必了解以下内容:交易系统逐笔记录所有资产每笔已执行交易的所有逐次跳价信息。 但是您知道它们代表多少数据吗? 您有没有想过组织和排序所有资产需要多长时间?
好吧,一些典型的资产在其日常变动中可能包含大约 80 MB 的数据量。 在某些情况下,它也许多一点或少一点。 这仅是单一资产的一天。 现在考虑必须将相同的资产存储 1 个月、1 年、10 年...... 或者谁知道,永远。 想想如此大量数据需要存储,然后再从其中检索。 因为如果您只是将它们保存在磁盘上,很快您就找不到任何东西。 有一句话可以很好地说明这一点:
空间越大,混乱越大。.
为了令事情变得更容易,一段时间后,数据被压缩成 1 分钟柱线,其中包含最少的必要信息,以便我们可以进行某种研究。 但是当该柱线实际创建时,构建它的跳价就会消失,并且不再可访问。 在那之后,就不再可能进行真正的市场回放。 从这一刻起,我们所拥有的,只是一个模拟器。 由于无法再访问真实的走势,我们就不得不创建某种方式,基于一些合理的市场走势来模拟它。
为了理解,请参见以下图例:
上面的序列示意数据如何随时间丢失。 左图显示了实际的跳价数值。 当数据被压缩时,我们在中心得到图像。 基于它,我们将无法获得左侧数值。 这样做是不可能的。 但我们可以创建类似于右侧图像的东西,我们将根据有关市场通常如何移动的知识来模拟市场走势。 不过,它看起来与原始图像完全不同。
使用回放时请记住这一点。 如果您没有原始数据,那么您就无法进行真实的研究。 您只能进行一些统计研究,其也许接近实际走势,但也可能离之甚远。 永远记住这一点。 在整个系列中,我们将探索更多如何执行此操作。 但这会一点一点地发生。
据此,我们继续真正具有挑战性的部分:实现回放系统。
实现
这部分虽然看起来很简单,但却相当复杂,因为软件部分会涉及硬件限制,和其它方面的问题。 故此,我们必须尝试创造一些东西,至少是最基本、最实用和可接受的。 如果基础太薄弱,尝试做更复杂的事情不会有任何好处。
奇怪的是,我们的主要和最大的问题是时间。 时间是一个需要克服的大问题,甚至是巨大的问题。
在附件中,我将始终(在第一阶段)保留所有过去任何时期任何资产的至少 2 个真实跳价集。 由于数据会丢失且无法下载,因此无法再获取此数据。 这将有助于我们研究每一个细节。 但是,您也可以创建自己的真实跳价基准。
创建您自己的数据库
幸运的是,MetaTrader 5 提供了一些方法,能做到这一点。 这很简单,但您必须稳步地做到这一点,否则数值可能会丢失,并且将无法再完成此任务。为此,请打开 MetaTrader 5,并按默认快捷键:CTRL+U。 这将打开一个屏幕。 在此处指定资产,以及收集数据的开始和结束日期,点击按钮请求数据,然后等待几分钟。 服务器将返回您需要的所有数据。 之后,只需将此数据导出,并精心存储即可,因为它非常有价值。
下面是您所捕获的屏幕。
虽然您可以创建一个程序来做到这一点,但我认为最好手动完成。 有些事情我们不能盲目相信。 我们必须亲眼看到正在发生的事情,否则我们将对自己正在使用的东西缺乏相应的信心。
相信我,这是我们将要学习创建的整个系统中最简单的部分。 从这一点开始,事情变得更加复杂。
首次回放测试
有些人可能认为这将是一项简单的任务,但我们很快就会反驳这个想法。 其他人可能想知道:为什么我们不使用 MetaTrader 5 策略测试器进行回放? 原因是它不允许我们如同在市场上进行交易一样回放。 通过测试器回放存在局限性和困难,因此,我们将无法完全沉浸在回放中,就好像我们实际上是在交易市场一样。
我们将面临巨大的挑战,但我们必须为这一漫长的旅程迈出第一步。 我们从一个非常简单的实现开始。 为此,我们需要 OnTime 事件,它将生成数据流以便创建柱线(烛条)。 此事件是为 EA 和指标提供的,但在这种情况下我们不应该使用指标,因为如果发生故障,它将比回放系统更危险。 我们将按如下方式启动代码:
#property copyright "Daniel Jose" #property icon "Resources\\App.ico" #property description "Expert Advisor - Market Replay" //+------------------------------------------------------------------+ int OnInit() { EventSetTimer(60); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); } //+------------------------------------------------------------------+ void OnTick() { } //+------------------------------------------------------------------+ void OnTimer() { } //+------------------------------------------------------------------+
不过,高亮显示的代码不适合我们的目的,因为在这种情况下,我们可用的最小周期是 1 秒,这是一个长时间,很长的时间。 由于市场事件发生的时间帧要小得多,我们需要降至毫秒,为此我们将被迫用到另一个函数:EventSetMillisecondTimer。 但是我们这里有一个问题。
EventSetMillisecondTimer 函数的限制
我们看一下文档:
“在实时模式下工作时,由于硬件限制,计时器事件在 1-10 毫秒内生成不会超过 16 次。”
这也许不是问题,但我们需要运行各种检查来验证实际发生的情况。 因此,我们来创建一些简单的代码来验证结果。
我们从下面的 EA 代码开始:
#property copyright "Daniel Jose" #property icon "Resources\\App.ico" #property description "Expert Advisor - Market Replay" //+------------------------------------------------------------------+ #include "Include\\C_Replay.mqh" //+------------------------------------------------------------------+ input string user01 = "WINZ21_202110220900_202110221759"; //Tick archive //+------------------------------------------------------------------+ C_Replay Replay; //+------------------------------------------------------------------+ int OnInit() { Replay.CreateSymbolReplay(user01); EventSetMillisecondTimer(20); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); } //+------------------------------------------------------------------+ void OnTick() {} //+------------------------------------------------------------------+ void OnTimer() { Replay.Event_OnTime(); } //+------------------------------------------------------------------+
请注意,我们的 OnTime 事件大约每 20 毫秒发生一次,如 EA 代码中高亮显示的行所示。 您可能认为这太快了,但真的是这样吗? 一起来看看吧。 请记住,文档说我们不能低于 10 到 16 毫秒。 因此,将该值设置为 1 毫秒是没有意义的,因为在此期间不会生成事件。
请注意,在 EA 代码中,我们只有两个外部链接。 现在我们来看看类中实现的这些代码。
#property copyright "Daniel Jose" //+------------------------------------------------------------------+ #define def_MaxSizeArray 134217727 // 128 Mbytes of positions #define def_SymbolReplay "Replay" //+------------------------------------------------------------------+ class C_Replay { };
请务必注意,该类有一条 128 MB 的定义,如上面高亮显示的位置所示。 这意味着包含所有跳价数据的文件不得超过此大小。 如果需要,您可以增加此大小,但就个人而言,我对此值没有异议。
下一行指定回放资产的名称。 我非常有创意地将资产命名为 REPLAY,不是吗? 😂 好吧,我们继续研究该类。 下一段要讨论的代码如下所示:
void CreateSymbolReplay(const string FileTicksCSV) { SymbolSelect(def_SymbolReplay, false); CustomSymbolDelete(def_SymbolReplay); CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\Replay\\%s", def_SymbolReplay), _Symbol); CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX); CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX); SymbolSelect(def_SymbolReplay, true); m_IdReplay = ChartOpen(def_SymbolReplay, PERIOD_M1); LoadFile(FileTicksCSV); Print("Running speed test."); }
高亮显示的两行会做一些非常有趣的事情。 对于那些不知道的人,CustomSymbolCreate 函数会创建一个自定义品种。 在这种情况下,我们可以调整一些内容,但由于这只是一个测试,我暂时不会讨论它。 ChartOpen 打开我们自定义品种的图表,在本例中将是 REPLAY。 一切都非常好,但我们需要从文件中加载回放,这是通过以下函数完成的。
#define macroRemoveSec(A) (A - (A % 60)) void LoadFile(const string szFileName) { int file; string szInfo; double last; long vol; uchar flag; if ((file = FileOpen("Market Replay\\Ticks\\" + szFileName + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE) { ArrayResize(m_ArrayInfoTicks, def_MaxSizeArray); m_ArrayCount = 0; last = 0; vol = 0; for (int c0 = 0; c0 < 7; c0++) FileReadString(file); Print("Loading data for Replay.\nPlease wait ...."); while ((!FileIsEnding(file)) && (m_ArrayCount < def_MaxSizeArray)) { szInfo = FileReadString(file); szInfo += " " + FileReadString(file); m_ArrayInfoTicks[m_ArrayCount].dt = macroRemoveSec(StringToTime(StringSubstr(szInfo, 0, 19))); m_ArrayInfoTicks[m_ArrayCount].milisec = (int)StringToInteger(StringSubstr(szInfo, 20, 3)); m_ArrayInfoTicks[m_ArrayCount].Bid = StringToDouble(FileReadString(file)); m_ArrayInfoTicks[m_ArrayCount].Ask = StringToDouble(FileReadString(file)); m_ArrayInfoTicks[m_ArrayCount].Last = StringToDouble(FileReadString(file)); m_ArrayInfoTicks[m_ArrayCount].Vol = StringToInteger(FileReadString(file)); flag = m_ArrayInfoTicks[m_ArrayCount].flag = (uchar)StringToInteger(FileReadString(file)); if (((flag & TICK_FLAG_ASK) == TICK_FLAG_ASK) || ((flag & TICK_FLAG_BID) == TICK_FLAG_BID)) continue; m_ArrayCount++; } FileClose(file); Print("Loading completed.\nReplay started."); return; } Print("Failed to access tick data file."); ExpertRemove(); }; #undef macroRemoveSec
该函数将逐行加载所有跳价数据。 如果文件不存在,或无法访问,ExpertRemove 将关闭 EA。
所有数据都临时存储在内存当中,以便加快进一步的处理速度。 这是因为您可能正在使用磁盘驱动器,故会比系统内存慢很多。 故此,从一开始就确保所有数据都存在是值得的。
但是上面的代码中有一些相当有趣的东西:FileReadString 函数。 它会读取数据,直到找到一些分隔符。 有趣的是,当我们查看 MetaTrader 5 生成的以 CSV 格式保存的跳价文件的二进制数据时,如本文开头所述,我们得到以下结果。
黄色区域是文件头,它向我们显示了随后的内部结构的组织。 绿色区域表示第一个数据行。 现在我们来看一下这种格式中存在的蓝点(它们是分隔符)。 0D 和 0A 表示换行符,09 表示制表符(TAB 键)。 当我们调用 FileReadString 函数时,我们不需要积累数据来测试它。 该函数将自行执行此操作。 我们所要做的就是将数据转换为所需的类型。 我们看看下一个代码部分。
if (((flag & TICK_FLAG_ASK) == TICK_FLAG_ASK) || ((flag & TICK_FLAG_BID) == TICK_FLAG_BID)) continue;
此代码可防止不必要的数据出现在我们的数据矩阵中,但为什么要过滤掉这些数值? 因为它们不适合回放。 如果您打算用到这些值,您可先放过它们,但稍后在创建柱线时必须过滤它们。 所以,我更喜欢在这里过滤它们。
下面我们展示了测试系统中的最后一个例程:
#define macroGetMin(A) (int)((A - (A - ((A % 3600) - (A % 60)))) / 60) void Event_OnTime(void) { bool isNew; static datetime _dt = 0; if (m_ReplayCount >= m_ArrayCount) return; 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; _dt = TimeLocal(); } isNew = m_dt != m_ArrayInfoTicks[m_ReplayCount].dt; m_dt = (isNew ? m_ArrayInfoTicks[m_ReplayCount].dt : m_dt); 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); m_Rate[0].time = m_dt; CustomRatesUpdate(def_SymbolReplay, m_Rate, 1); m_ReplayCount++; if ((macroGetMin(m_dt) == 1) && (_dt > 0)) { Print(TimeToString(_dt, TIME_DATE | TIME_SECONDS), " ---- ", TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS)); _dt = 0; } }; #undef macroGetMin
此段代码将创建周期为 1 分钟的柱线,这是平台创建任何其它周期图表的最低要求。 高亮显示的部分并非代码本身的一部分,但对于分析 1 分钟柱线很有用。 我们需要检查它是否真的在此时间帧内创建。 因为如果创建时间超过 1 分钟,我们就不得不对此做点什么。 如果在不到一分钟的时间内创建它,这可能表明该系统立即可用。
执行此系统后,我们会得到以下视频中显示的结果:
有些人觉得时间比预期的要长得多,但我们可以对代码进行一些改进,也许这会有所作为。
改进代码
尽管存在巨大的延迟,但我们也许能够稍微改进一下,并帮助系统的性能更接近预期。 但我真的不相信奇迹。 我们知道 EventSetMillisecondTimer 函数的局限性,问题不是由 MQL5 引起的,而是硬件限制。 不过,我们来看看是否可以帮助系统。
如果您看一下数据,您会发现有若干时刻系统就是不动,价格保持不变,或者发生了预订吸收,故价格不会变动。 这可以在下图中看到。
请注意,我们有两个不同的条件:在时间和价格当中其一,并没有变化。 这并不能说数据不正确,但它告诉我们毫秒级测量经历的时间不足以产生价格变化。 我们还有另一种类型的事件,价格没有移动,但时间只移动了 1 毫秒。 在这两种情况下,当我们合并信息时,柱线创建时间的差距可能是 1 分钟。 这将避免额外调用创建函数,从长远来看,节省的每一纳秒都会产生很大的不同。 所有事情积累,一点一点地达成了很多。
为了检查是否会有差别,我们需要检查生成的信息量。 这是一个统计问题,所以并不笃定。 一个小错误是可以接受的。 但是在视频中可以看到的时间,对于接近现实的模拟来说是完全不能接受的。
为了验证这一点,我们对代码进行了第一次修改:
#define macroRemoveSec(A) (A - (A % 60)) void LoadFile(const string szFileName) { // ... Internal code ... FileClose(file); Print("Loading completed.\n", m_ArrayCount, " movement positions were generated.\nStarting Replay."); return; } Print("Failed to access tick data file."); ExpertRemove(); }; #undef macroRemoveSec
指定的附加部分会为我们做到这些。 现在我们来看一下第一次启动后会发生什么。 所有这些均如下图所见:
现在我们有一些参数可以让我们检查修改是否有帮助。 如果我们使用它,我们将花费大概 3 分钟来生成 1 分钟的数据。 换言之,这个系统远不能被接受。
故此,我们将对代码进行小的改进:
#define macroRemoveSec(A) (A - (A % 60)) void LoadFile(const string szFileName) { int file; string szInfo; double last; long vol; uchar flag; if ((file = FileOpen("Market Replay\\Ticks\\" + szFileName + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE) { ArrayResize(m_ArrayInfoTicks, def_MaxSizeArray); m_ArrayCount = 0; last = 0; vol = 0; for (int c0 = 0; c0 < 7; c0++) FileReadString(file); Print("Loading data to Replay.\nPlease wait ...."); while ((!FileIsEnding(file)) && (m_ArrayCount < def_MaxSizeArray)) { szInfo = FileReadString(file); szInfo += " " + FileReadString(file); m_ArrayInfoTicks[m_ArrayCount].dt = macroRemoveSec(StringToTime(StringSubstr(szInfo, 0, 19))); m_ArrayInfoTicks[m_ArrayCount].milisec = (int)StringToInteger(StringSubstr(szInfo, 20, 3)); m_ArrayInfoTicks[m_ArrayCount].Bid = StringToDouble(FileReadString(file)); m_ArrayInfoTicks[m_ArrayCount].Ask = StringToDouble(FileReadString(file)); m_ArrayInfoTicks[m_ArrayCount].Last = StringToDouble(FileReadString(file)); m_ArrayInfoTicks[m_ArrayCount].Vol = vol + StringToInteger(FileReadString(file)); flag = m_ArrayInfoTicks[m_ArrayCount].flag = (uchar)StringToInteger(FileReadString(file)); if (((flag & TICK_FLAG_ASK) == TICK_FLAG_ASK) || ((flag & TICK_FLAG_BID) == TICK_FLAG_BID)) continue; if (m_ArrayInfoTicks[m_ArrayCount].Last != last) { last = m_ArrayInfoTicks[m_ArrayCount].Last; vol = 0; m_ArrayCount++; }else vol += m_ArrayInfoTicks[m_ArrayCount].Vol; } FileClose(file); Print("Loading complete.\n", m_ArrayCount, " movement positions were generated.\nStarting Replay."); return; } Print("Failed to access tick data file."); ExpertRemove(); }; #undef macroRemoveSec
添加高亮显示的粗体行可大大改善结果,如下图所示:
在此,我们提高了系统性能。 这可能看起来不多,但它仍然表明早期的变化起着决定性的作用。 我们已经达到了生成 1 分钟的柱线花费大约 2 分 29 秒的时间。 换言之,该系统总体上有所改善,但尽管这听起来令人鼓舞,我们还是遇到一个令事情复杂化的问题。 我们无法减少 EventSetMillisecondTimer 函数生成事件之间的时间,这令我们思考不同的方法。
不过,针对系统略微进行了改进,如下所示:
void Event_OnTime(void) { bool isNew; static datetime _dt = 0; if (m_ReplayCount >= m_ArrayCount) return; 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(); } // ... Internal code .... }
高亮显示行的作用可以在图表上看到。 没有它们,第一根柱线总是被切断,故此难以正常阅读。 但是当我们添加这两行时,视觉表示变得更加美好,令我们能够正确看到生成的柱线图。 这从第一根柱线开始发生。 这是一件非常简单的事情,但最终会有很大的不同。
但是,我们回到我们最初的问题,即尝试创建一个合适的系统来呈现和创建柱线。 即使有可能降低时间,我们也不会有一个合适的系统。 事实上,我们将不得不改变方式。 这就是为什么 EA 不是创建回放系统的最佳方式,但即便如此,我还想展示另一件可能对您来说有趣的事情。 如果我们使用尽可能短的时间生成 OnTime 事件,我们创建 1 分钟柱线时实际上可以减少或改进多少? 如果数值在 1 分钟内没有变化时,我们进一步将数据压缩到跳价范围? 会有什么不同吗?
走向极值
为此,我们需要对代码进行最后一次修改。 它如下所示:
#define macroRemoveSec(A) (A - (A % 60)) #define macroGetMin(A) (int)((A - (A - ((A % 3600) - (A % 60)))) / 60) void LoadFile(const string szFileName) { int file; string szInfo; double last; long vol; uchar flag; datetime mem_dt = 0; if ((file = FileOpen("Market Replay\\Ticks\\" + szFileName + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE) { ArrayResize(m_ArrayInfoTicks, def_MaxSizeArray); m_ArrayCount = 0; last = 0; vol = 0; for (int c0 = 0; c0 < 7; c0++) FileReadString(file); Print("Loading data to Replay.\nPlease wait ...."); while ((!FileIsEnding(file)) && (m_ArrayCount < def_MaxSizeArray)) { szInfo = FileReadString(file); szInfo += " " + FileReadString(file); m_ArrayInfoTicks[m_ArrayCount].dt = macroRemoveSec(StringToTime(StringSubstr(szInfo, 0, 19))); m_ArrayInfoTicks[m_ArrayCount].milisec = (int)StringToInteger(StringSubstr(szInfo, 20, 3)); m_ArrayInfoTicks[m_ArrayCount].Bid = StringToDouble(FileReadString(file)); m_ArrayInfoTicks[m_ArrayCount].Ask = StringToDouble(FileReadString(file)); m_ArrayInfoTicks[m_ArrayCount].Last = StringToDouble(FileReadString(file)); m_ArrayInfoTicks[m_ArrayCount].Vol = vol + StringToInteger(FileReadString(file)); flag = m_ArrayInfoTicks[m_ArrayCount].flag = (uchar)StringToInteger(FileReadString(file)); if (((flag & TICK_FLAG_ASK) == TICK_FLAG_ASK) || ((flag & TICK_FLAG_BID) == TICK_FLAG_BID)) continue; if ((mem_dt == macroGetMin(m_ArrayInfoTicks[m_ArrayCount].dt)) && (last == m_ArrayInfoTicks[m_ArrayCount].Last)) vol += m_ArrayInfoTicks[m_ArrayCount].Vol; else { mem_dt = macroGetMin(m_ArrayInfoTicks[m_ArrayCount].dt); last = m_ArrayInfoTicks[m_ArrayCount].Last; vol = 0; m_ArrayCount++; } } FileClose(file); Print("Upload completed.\n", m_ArrayCount, " movement positions were generated.\nStarting Replay."); return; } Print("Failed to access tick data file."); ExpertRemove(); }; #undef macroRemoveSec #undef macroGetMin
高亮显示的代码修复了一个以前就存在,但我们没有注意到的小问题。 当价格保持不变,但从一根柱线过渡到另一根柱线时,创建新柱线需要一些时间。 然而,真正的问题是开盘价与原始图表上显示的价格不同。 好了,这已得到纠正。 现在,如果所有其它参数都相同,或以毫秒为单位时有微小的差异,我们将只有一个保存的位置。
之后,我们可以取 EventSetMillisecondTimer 为 20 来测试系统。 结果如下:
在本例中,对于 20 毫秒事件的结果是 34 分 20 秒...... 然后,我们将 EventSetMillisecondTimer 的值更改为 10(这是文档中指定的最小值)。 此为结果:
在本例中,1 毫秒事件的结果为 56 分 10 秒。 结果有所改善,但仍然远远不能满足我们的需要。 现在没有办法使用本文中采用的方法进一步减少时间事件,因为文档本身告诉我们这是不可能的,或者我们将没有足够的稳定性来采取下一步。
结束语
在本文中,我为那些想要创建回放/模拟系统的人介绍了基本原理。 这些原理是整个系统的基础,但对于那些没有编程经验的人来说,了解 MetaTrader 5 平台的工作原理可能是一项艰巨的任务。 看到这些原理如何在实践中应用,可以转化为开始学习编程的巨大动力。 因为只有当我们看到它们是如何工作的,事情才会变得有趣;只看代码是没有动力的。
一旦您意识到可以做什么,并明白它是如何工作的,一切都会改变。 它就像一扇神奇的门正在打开,揭示了一个充满可能性的全新未知世界。 您将在本系列中看到这是如何发生的。 我将在创建文章时开发此系统,因此请耐心等待。 即使看起来没有进展,也总有进步。 知识永远不会嫌太多。 也许它会让我们不那么快乐,但它永远不会伤害拥有者。
附件包含我们在此处讨论的两个版本。 您还能找到两个真实的跳价文件,以便您可以进行实验,并查看系统在您自己的硬件上的行为。 结果不会与我展示的有太大区别,但是看看计算机如何处理某些问题,并以非常有创意的方式解决这些问题可能会非常有趣。
在下一篇文章中,我们将进行一些修改,以便尝试实现更合适的系统。 我们还将研究另一个相当有趣的解决方案,对于那些刚刚开始编程世界之旅的人来说,它也很实用。 所以,工作才刚刚开始。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10543
注意: 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.



例如,用红色和蓝色标出的时间是相同的,而最后的价格可能相同,也可能不相同。
可以压缩这些刻度线,大大减少构建分钟柱状图所需的时间。
但是,你在第 53 行删除了秒。
这使得你无法准确执行压缩。毫秒与秒之间没有绑定,它已被移除。
当然,毫秒值移动到下一秒,甚至在下一个刻度中继续移动的可能性很低。但它确实存在。
只有在从分钟条 移动 到下一个分钟条时,才能保证 100% 的准确性 - 毫秒与分钟之间存在绑定。
嗯,"未来"--对我来说--我还没读下去。我想,对你来说,这已经是 "过去 "了......)))
如果是这样,那么我同意可以去掉秒针--巧合的可能性极低。)
共设立了 1153 809 个流动职位。
删除的刻度 = 1066231
检查 执行速度 .2023.12.02 01:52:21 ---- 2023.12.02 01:53:17
建立第一根蜡烛的时间:00:00:56 秒。)
我们赢了 56 秒!
刚好一半。感谢您的出色工作!
我有一个问题。有没有可能在重播中增加倒带选项?我的意思是回到之前的蜡烛,然后重新播放。