English Русский Español Deutsch 日本語 Português
preview
开发回放系统 — 市场模拟(第 01 部分):首次实验(I)

开发回放系统 — 市场模拟(第 01 部分):首次实验(I)

MetaTrader 5测试者 | 28 七月 2023, 14:37
1 221 0
Daniel Jose
Daniel Jose

概述

在撰写“从头开始开发智能系统”系列文章时,我遇到了一些时刻,令我意识到比之已完成的 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

附加的文件 |
Replay.zip (10910.23 KB)
学习如何基于比尔·威廉姆斯(Bill Williams)的 MFI 设计交易系统 学习如何基于比尔·威廉姆斯(Bill Williams)的 MFI 设计交易系统
这是该系列中的一篇新文章,我们将学习如何根据流行的技术指标设计交易系统。 这次我们将涵盖比尔·威廉姆斯(Bill Williams)的市场促进指数(BW MFI)。
MQL5 — 您也可以成为该语言的大师 MQL5 — 您也可以成为该语言的大师
本文将是一次自我访谈,我将告诉您我是如何迈出 MQL5 语言的第一步的。 我将向您展示如何成为一名出色的 MQL5 程序员。 我将为您解释实现这一壮举的必要基础。 唯一的先决条件是愿意学习。
种群优化算法:猴子算法(MA) 种群优化算法:猴子算法(MA)
在本文中,我将研究猴子优化算法(MA)。 这些动物克服困难障碍,并到达最难以接近的树顶的能力构成了 MA 算法思想的基础。
将 ML 模型与策略测试器集成(第 3 部分):CSV(II)文件管理 将 ML 模型与策略测试器集成(第 3 部分):CSV(II)文件管理
这篇资料提供了以 MQL5 创建类,从而高效管理 CSV 文件的完整指南。 我们将看到打开、写入、读取、和转换数据等方法的实现。 我们还将研究如何使用它们来存储和访问信息。 此外,我们将讨论使用该类的限制和最重要的方面。 本文对于那些想要学习如何在 MQL5 中处理 CSV 文件的人来说是一个宝贵的资源。