开发回放系统 — 市场模拟(第 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