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

开发回放系统 — 市场模拟(第 06 部分):首次改进(I)

MetaTrader 5测试者 | 16 十月 2023, 11:13
604 0
Daniel Jose
Daniel Jose

概述

本系列文章中,介绍了我们的市场回放系统及其创建过程,从而向读者展示事情在采取更明确的形式之前是如何真正开始的。 作为该过程的结果,我们可以缓慢而谨慎地移动,这令我们能够创建、删除、添加和更改许多内容。 原因在于市场回放系统是与文章发表过程同步创建的。 故此,在文章发表之前,会进行一些测试,从而确保在建模和调整阶段,系统稳定且功能正常。

现在,我们下沉到正题。 在上一篇文章开发回放系统 — 市场模拟(第 05 部分):加入预览中,我们创建了一个加载预览面板的系统。 虽然这个系统能工作,我们仍有一些问题。 最紧迫的问题是,为了获得更多可用的数据库,有必要为已经存在的柱线创建一个文件,其内通常包含若干天的记录,并且不允许插入数据。

当我们创建一个预览文件时,例如,包含一周数据,即从星期一到星期五,我们不能用相同的数据库进行回放,例如星期四。 这理应创建一个新的数据库。 若您细思,这很糟糕。

除了这种不便之外,我们还有其它问题,例如完全缺乏适当的测试来确保我们正在使用正确的数据库。 有因于此,我们也许会意外地使用柱线文件,如同它们来自所执行交易的跳价数据,反之亦然。 这会对我们的系统造成严重干扰,阻碍其无法正常运行。 作为本文的一部分,我们还将进行其它几处小修改。 我们进入正题。

请记住,我们将通过每个要点来理解正在发生什么。


实现改进

我们需要实现的第一处修改是往服务文件添加两个新行。 它们如下所示:

#define def_Dependence  "\\Indicators\\Market Replay.ex5"
#resource def_Dependence

为什么需要您这样做? 对于系统是由模块组成的简单事实,我们在使用回放系统时必须以某种方式确保它们的存在。 最重要的模块也正是这个指标,因为它负责对将要完成的事情进行某些控制。

我承认,这不是最好的方法。 也许将来那些开发 MetaTrader 5 平台和 MQL5 语言的人会进行一些补充,比如编译指令,如此我们就可以真正确保文件被编译,或应该存在。 但在没有任何其它解决方案的情况下,我们将按这种方式来做。

好的,现在我们正在设置一个指令,指示服务依赖于其它内容。 然后,我们同样把资源添加到服务之中。 一个重点是:终端不一定允许我们将某些元素转换为资源。 这种情况很特殊。

这两行简单指令可确保在编译回放服务时,于模板中存在的、供回放所用的指标能被真正地编译。 您也许会忘记这样做,那么当模板加载指标却找不到时,将出现故障,只有当您意识到图表上找不到用于控制服务的指标时,您才会注意到这一点。

为了避免这种情况,我们已经确保指标与服务一起编译。 不过,如果您使用自定义模板,然后手动添加控制指标,我们可以删除服务代码上方的这两行。 此类指标缺失不会影响代码或服务的操作。

注意:即使我们强制编译,也只在指标的可执行文件不存在时才会发生。 如果指标被修改,仅编译服务不会导致指标的构建。

有人可能会说,这可用 MetaEditor 的项目模式来解决。 然而,这种模式不允许我们以与 C/C++ 等语言相同的方式工作,在那里我们用 MAKE 文件来控制编译。 您甚至可以通过批处理文件来完成它,但这会迫使我们退出 MetaEditor,目的只是为了编译代码。

继续,我们现在有两个新行:

input string            user00 = "Config.txt";  //Replay configuration file
input ENUM_TIMEFRAMES   user01 = PERIOD_M5;     //Initial timeframe

此处的一些东西对我们的回放服务真的非常有用:这一行实际上是包含的是回放品种设置的文件名称。 目前,这些设置包括用于生成以前柱线的文件是哪一个,以及哪些文件将用于存储交易的即刻报价。

现在,您可以同时使用若干个文件。 还有,我知道许多交易者喜欢在进行市场交易时使用特定的时间帧,同时喜欢在全屏模式下查看图表。 如此,这一行允许您设置从一开始就应使用的时间帧。 这非常简单,也更方便,因为您可以保存所有这些设置以供将来使用。 我们可以在这里添加更多内容,但就目前而言,这样已很好了。

现在我们需要再明白几点,即从下面的代码中所见:

void OnStart()
{
        ulong t1;
        int delay = 3;
        long id = 0;
        u_Interprocess Info;
        bool bTest = false;
        
        Replay.InitSymbolReplay();
        if (!Replay.SetSymbolReplay(user00))
        {
                Finish();
                return;
        }
        Print("Wait for permission from [Market Replay] indicator to start replay...");
        id = Replay.ViewReplay(user01);
        while ((!GlobalVariableCheck(def_GlobalVariableReplay)) && (!_StopFlag) && (ChartSymbol(id) != "")) Sleep(750);
        if ((_StopFlag) || (ChartSymbol(id) == ""))
        {
                Finish();
                return;
        }
        Print("Permission received. Now you can use the replay service...");
        t1 = GetTickCount64();
        while ((ChartSymbol(id) != "") && (GlobalVariableGet(def_GlobalVariableReplay, Info.Value)) && (!_StopFlag))
        {
                if (!Info.s_Infos.isPlay)
                {
                        if (!bTest) bTest = true;
                }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();
                        }
                }
        }
        Finish();
}

最初,我们只是在等待一切准备就绪。 但从现在开始,我们将确保一切工作正常。 这是通过测试完成的。 我们测试一下执行回放的文件是否确实适合此操作。 我们检查读取文件中所包含数据的函数的返回。 如果发生故障,MetaTrader 5 工具箱中将显示一条消息,通知所发生的情况,而回放服务将关闭,因为我们没有足够的数据来可供它使用。

如果数据已正确加载,我们将在工具箱中看到相应的消息,我们就可以继续。 然后,我们打开回放品种图表,并等待权限继续执行后续步骤。 不过,可能会在等待期间发生用户关闭服务或关闭回放品种图表的情况。 如果发生这种情况,我们必须停止回放服务。 如果一切工作正常,我们将进入回放循环,但同时我们要确保用户不会关闭图表或终止服务。 因为如果发生这种情况,那么回放也必须关闭。

请注意,现在不仅要想象一切工作正常,而且我们也应确保实际上一切工作如常。 似乎很奇怪,在系统的早期版本中并未进行这种类型的测试,但这还有其它问题,因此每次回放关闭或因某种原因终止时,都会有一些东西遗留在幕后。 但现在,我们将确保一切正常,且没有不必要的元素。

最终,我们在服务文件中仍有以下代码:

void Finish(void)
{
        Replay.CloseReplay();
        Print("Replay service completed...");
}

这不难理解。 在此,我们只需完成回放,并通过工具箱通知用户。 以这种方式,我们可以转到实现 C_Replay 类的文件,其中我们还将执行其它检查,从而确保一切工作正常。

C_Replay 类并无重大变化。 我们以这种方式构建它,以便回放尽可能地稳定可靠。 因此,我们将逐步进行修改,从而避免破坏迄今为止所做的所有工作。

首先映入眼帘的是下面显示的代码行:

#define def_STR_FilesBar        "[BARS]"
#define def_STR_FilesTicks      "[TICKS]"
#define def_Header_Bar          "<DATE><TIME><OPEN><HIGH><LOW><CLOSE><TICKVOL><VOL><SPREAD>"
#define def_Header_Ticks        "<DATE><TIME><BID><ASK><LAST><VOLUME><FLAGS>"

尽管它们看起来微不足道,但这 4 行非常有趣,因为它们执行了所需的测试。 这两个定义会在配置文件中用到,我们将很快看到它。 它包含文件第一行的准确数据,其中包含将用作先前柱线的数据。 此定义包含文件第一行的确切内容,即交易即时报价。

但请等一下。 这些定义与文件头中的定义不完全相同:没有制表符。 是的,实际上,原始文件中没有制表符。 不过,有一个与读取数据的方式相关的小细节。

但在详述之前,我们看一下回放服务配置文件在当前开发阶段的样子。 文件示例如下所示:

[Bars]
WIN$N_M1_202108020900_202108021754
WIN$N_M1_202108030900_202108031754
WIN$N_M1_202108040900_202108041754

[Ticks]
WINQ21_202108050900_202108051759
WINQ21_202108060900_202108061759

定义为 [Bars] 的行可以是这样的类型,因为我没有将系统设置为区分大小写,表明所有后续行都将作为以前的柱线。 如此,我们按指定的顺序加载了三个不同的内容。 请小心,因为如果您以错误的顺序放置它们,您将无法获得所需的回放。 这些文件中出现的所有柱线,将逐个添加到品种之中,以供回放所用。 无论文件或柱线的数量如何,所有文件都将添加到以前的柱线,直到某些指示改变它。

在 [Ticks] 行的情况下,这将告诉回放服务所有后续行应当/或是包含回放的交易即时报价。 与柱线一样,相同的警告也适用于此处:请小心在文件里按正确的顺序放置。 否则,回放将与预期不同;所有文件始终从头到尾读取。 通过这种方式,我们可以将柱线与即时报价结合起来。

但是,目前存在略微的限制。 也许这并不是一个真正的限制,因为添加交易即时报价,回放它们,然后观察更多的柱线出现是没有意义的,这将用在另一个回放调用当中。 如果您将即时报价放在柱线之前,这对回放系统没有任何影响。 已交易柱线总是先来的,然后才会有交易即时报价。

重点: 在上面的例子中,我没有考虑可能如此做的事实。 如果我们打算以更好的方式组织事物,您可以使用目录树来分隔事物,并按更合适的方式进行组织。 这可以在不对类代码进行额外修改的情况下完成。 我们需要做的就是仔细遵循类文件中存在的结构中的某个逻辑。 为了令事情更清晰,我们来看一个如何使用目录树,按品种、月份或年份分隔事物的示例。


为了理解它,我们看下面的图片:

                   

请注意,我们将 MARKET REPLAY 目录指定为根目录,这是基础,因为我们后续操作都必须在此目录当中。 我们在组织事物时可以将事物划分为品种、年份和月份,其中包含的每个月文件将与该特定月份发生的事情相对应。 以这样的方式创建额系统允许我们使用如上所示的结构,而无需对代码进行任何更改。 只需在配置文件中通知即可访问特定数据。

此文件可以具有任何名称,只有其内容需是结构化的。 所以这个名字您可自由选择。

太好了。 因此,如果您需要调用一个文件,假设迷你美元的即时报价文件,对于 2020 年 6 月 16 日,您可以在配置文件中使用以下行:

[Ticks]
Mini_Dolar_Futuro\2020\06-Junho\WDO_16062020

而这会告诉系统准确读取此文件。 当然,这只是如何组织工作的一个示例。

但要了解为什么会发生这种情况并成为可能,我们必须查看负责读取此配置文件的函数。 我们来看看它。

bool SetSymbolReplay(const string szFileConfig)
{
        int     file;
        string  szInfo;
        bool    isBars = true;
                        
        if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
        {
                MessageBox("Failed to read the configuration file.", "Market Replay", MB_OK);
                return false;
        }
        Print("Loading data for replay.\nPlease wait....");
        while ((!FileIsEnding(file)) && (!_StopFlag))
        {
                szInfo = FileReadString(file);
                StringToUpper(szInfo);
                if (szInfo == def_STR_FilesBar) isBars = true; else
                if (szInfo == def_STR_FilesTicks) isBars = false; else
                if (szInfo != "") if (!(isBars ? LoadPrevBars(szInfo) : LoadTicksReplay(szInfo)))
                {
			if (!_StopFlag)
	                        MessageBox(StringFormat("File %s of %s\ncould not be loaded.", szInfo, (isBars ? def_STR_FilesBar : def_STR_FilesTicks), "Market Replay", MB_OK));
                        FileClose(file);
                        return false;
                }
        }
        FileClose(file);
        return (!_StopFlag);
}

我们首先尝试读取配置文件,该文件必须位于特定位置。 此位置无法更改,至少在系统编译后不能更改。 如果无法打开该文件,将出现错误消息,并关闭该函数。 如果文件能被打开,那么我们就开始读取它。 不过,请注意,我们需不断检查 MetaTrader 5 用户是否要求停止回放服务。

如果发生这种情况,即如果用户停止服务,该函将被关闭,就好像它失败了一样。 我们逐行读取,并将所有读取的字符转换为相应的大写字母,这让分析更容易。 然后,我们解析并调用相应的函数,从配置脚本中指定的文件中读取数据。 如果在读取这些文件时因任何一个原因导致失败,用户将看到一条出错消息,并且函数将失败。 读取整个配置文件后,该函数将直接退出。 如若用户要求停止它,我们也将收到一个响应,指示一切正常。

现在我们已经见识到如何读取配置文件,我们来看一下负责读取数据的函数,然后我们将了解为什么会出现说请求的文件不合适的警告。 那就是,如果您尝试使用包含柱线的文件,替包含交易即时报价的文件,反之亦然,系统都会报告错误。 我们看看这是如何发生的。

我们从最简单的函数开始,它负责读取和加载柱线。 负责此操作的代码如下所示:

bool LoadPrevBars(const string szFileNameCSV)
{
        int     file,
                iAdjust = 0;
        datetime dt = 0;
        MqlRates Rate[1];
        string  szInfo = "";
                                
        if ((file = FileOpen("Market Replay\\Bars\\" + szFileNameCSV + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE)
        {
                for (int c0 = 0; c0 < 9; c0++) szInfo += FileReadString(file);
                if (szInfo != def_Header_Bar)
                {
                        Print("Файл ", szFileNameCSV, ".csv это не файл предыдущих баров.");
                        return false;
                }
                Print("Loading bars for the replay. Please wait....");
                while ((!FileIsEnding(file)) && (!_StopFlag))
                {
                        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("Could not access the bars data file.");
                m_dtPrevLoading = 0;                                    
                return false;
        }
        return (!_StopFlag);
}

乍一看,此代码与上一篇文章“开发回放系统(第 05 部分)”中提供的代码并无太大出入。 但缺失存在差异,并且在一般和结构方面都相当明显。

第一处区别是我们现在从正在读取的文件中拦截标头数据。 将此标头与柱线读取函数确定和预期的数值进行比较。 如果此标头与预期不同,则将引发错误,并退出函数。 但如果这是预期的标头,那么我们将结束循环。

以前,此循环的退出仅由正在读取的文件已达末尾来控制。 如果用户因任何原因停止服务,文件不会被关闭。 我们现在已经修复了这个问题,如此若用户在读取其中一个文件时退出回放系统,循环将被关闭,并且将引发一个错误,指示系统已失败。 但这只是一种形式,令一切更顺利、结局不那么生硬。

函数的其余部分继续以相同的方式运行,因为没有修改任何读取数据的方式。

现在我们来看一下读取交易即时报价的函数,它已进行过修改,变得比柱线读取函数更有趣。 其代码如下所示:

#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)
                        {
                                ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
                                old = m_Ticks.nTicks;
                                for (int c0 = 0; c0 < 7; c0++) szInfo += FileReadString(file);
                                if (szInfo != def_Header_Ticks)
                                {
                                        Print("File ", szFileNameCSV, ".csv is not a file with traded ticks.");
                                        return false;
                                }
                                Print("Loading replay ticks. Please wait...");
                                while ((!FileIsEnding(file)) && (m_Ticks.nTicks < (INT_MAX - 2)) && (!_StopFlag))
                                {
                                        ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), 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);
                                        }
                                }
                                if ((!FileIsEnding(file))&& (!_StopFlag))
                                {
                                        Print("Too much data in the tick file.\nCannot continue....");
                                        return false;
                                }
                        }else
                        {
                                Print("Tick file ", szFileNameCSV,".csv not found...");
                                return false;
                        }
                        return (!_StopFlag);
                };
#undef macroRemoveSec

在上一篇文章之前,这个读取交易即时报价的函数有一个限制,其表达如下定义行所示:

#define def_MaxSizeArray        134217727 // 128 Mbytes of positions

这行仍然存在,但我们已经解除了限制,至少部分删除了。 因为创建一个可以处理多个交易即时报价数据库的回放系统可能有大用。 以这种方式,我们就可以向系统添加 2 天或更长时间的数据,并进行一些大型的重复测试。 此外,在非常特殊的情况下,我们可能有一个超过 128MB 的仓位文件可供操控。 这种情况很罕见,但可能会发生。 从现在开始,我们可以为上面的定义设置较小的数值,来优化内存用量。

但请等一下。 我说“少”了吗? 是的。 如果您查看新定义,您将看到以下代码:

#define def_MaxSizeArray        16777216 // 16 Mbytes of positions

您也许正在想:你疯了,这会破坏系统...... 但它不会。 如果您查看交易即时报价的正常读数,您能看到有趣的两行,在以前不存在。 它们的责任是允许我们读取和存储最多 2^32 条仓位数据。 在循环开始时,会进行检查来确保。 

为了不丢失第一条数据,我们减去 2,如此就不会因某种原因而测试失败。 我们可以添加一个额外的外部循环来增加内存容量,但我个人认为没有理由这样做。 如果 2GB 的仓位数据还不够,那么我不知道需要多少才够用。 现在我们明白了如何通过这两行来减小定义值,提供更好的优化。 我们仔细看看负责这项的代码片段。

// ... Previous code ....

if ((file = FileOpen("Market Replay\\Ticks\\" + szFileNameCSV + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE)
{
        ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
        old = m_Ticks.nTicks;
        for (int c0 = 0; c0 < 7; c0++) szInfo += FileReadString(file);
        if (szInfo != def_Header_Ticks)
        {
                Print("File ", szFileNameCSV, ".csv is not a file with traded ticks.");
                return false;
        }
        Print("Loading replay ticks. Please wait...");
        while ((!FileIsEnding(file)) && (m_Ticks.nTicks < (INT_MAX - 2)) && (!_StopFlag))
        {
                ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
                szInfo = FileReadString(file) + " " + FileReadString(file);

// ... The rest of the code...

我们第一次分配内存时,我们将按指定大小加上保留值整体分配。 这个保留值将是我们的保障。 然后,当我们进入读取循环时,我们将有一系列的重新定位,但仅在实际需要时才进行。

现在请注意,在第二次分配中,我们将使用已读取的即时报价计数器的当前值加 1。 在测试系统时,我注意到执行此调用时它的值为 0,这导致了运行时错误。 您也许想这太疯狂了,因为以前为内存分配了更高的数值。 事情是这样的,ArrayResize 函数的文档告诉我们,我们将重新定义数组的大小。

当我们第二次调用时,该函数会将数组重置为零。 变量的当前值即是首次调用函数时的数值,我们没有增加它。 我不会在这里解释其中的原因,但是在 MQL5 中使用动态分配时应该小心。 因为它可能会发生,如此这般,即时您的代码看起来正确,但系统以错误的方式解释它。

这里有另一个需要注意的小细节:为什么我在测试中采用 INT_MAX 而不是 UINT_MAX? 实际上,理想的选择是采用 UINT_MAX,这将为我们提供 4GB 的分配空间,但是 ArrayResize 函数适用于 INT 系统,即带符号整数。

即使我们想分配 4GB(使用 32 位长类型可以实现),由于符号的原因,我们总是会丢失一半的数据长度。 因此,我们将使用 31 位,这保证了我们使用 ArrayResize 函数分配 2GB 的可能空间。 我们可以尝试通过使用共享方案来绕过这个限制,该方案将保证分配 4GB 甚至更多,但我认为没有理由这样做。 2GB 的数据就足够了。

解释之后,我们回到代码。 我们还没有看到读取交易即时报价的函数。 为了检查文件中的数据是否真的是交易即时报价,我们读取并保存文件头中的值。 在此之后,我们可以检查标头是否与系统期望在交易即时报价文件中找到的标头匹配。 否则,系统将生成错误,然后关闭。

就像读取标头的情况下一样,我们检查用户是否已请求关闭系统。 这提供了更平滑、更干净的输出。 因为如果用户关闭或终止回放服务,我们不希望显示一些错误消息导致混淆。

除了我们在这里执行的所有这些测试之外,我们还有一些测试要执行。 事实上,不需要这些操作,但我不想把一切都留给平台处理。 我想确保一些东西真实实施,因此我们的代码中会有一个新行:

void CloseReplay(void)
{
        ArrayFree(m_Ticks.Info);
        ChartClose(m_IdReplay);
        SymbolSelect(def_SymbolReplay, false);
        CustomSymbolDelete(def_SymbolReplay);
        GlobalVariableDel(def_GlobalVariableReplay);
}

您也许会认为此调用根本不重要。 然而,最好的编程实践之一是清理我们创建的所有内容,或者显式回收我们分配的所有内存。 这正是我们现在正在做的。 我们保证加载即时报价时分配的内存将返给操作系统。 这通常是在我们关闭平台,或结束图表上的程序时完成的。 但是,确保完成这项工作是件好事,即使平台已经为我们做了这件事。 我们必须确定这一点。

如果发生故障,并且资源未返回到操作系统,则当我们尝试再次使用该资源时,可能会发生它不可用这样的情况。 这不是由于平台或操作系统中的错误,而是由于编程时的健忘。


结束语

在下面的视频中,您可以看到系统在当前开发阶段的操作方式。 请注意,之前的柱线将于 8 月 4 日结束。 回放的第一天从 8 月 5 日的第一次跳价开始。 不过,您可以提前回放到 8 月 6 日,然后返回到 8 月 5 日开始。 这在以前版本的回放系统中是不可能的,但现在我们有这个机会。

如果仔细观察,您能看到系统中存在一个1错误。 我们将在下一篇文章中修复此错误,我们将进一步改进我们的市场回放,令其用起来更加稳定和直观。



附件包括视频中使用的源代码和文件。 借助它们来更好地理解和练习配置文件的创建。 从现在开始研究这个阶段很重要,因为配置文件会随着时间的推移而发生积极的变化,从而增加其功能。 因此,我们现在需要从一开始就理解这一点。


本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10768

附加的文件 |
Market_Replay.zip (13057.37 KB)
在莫斯科交易所(MOEX)里使用破位挂单的自动兑换网格交易 在莫斯科交易所(MOEX)里使用破位挂单的自动兑换网格交易
本文探讨在莫斯科交易所(MOEX)里基于破位挂单的网格交易方法如何在 MQL5 智能系统中实现。 在市场上进行交易时,最简单的策略之一是设计“捕捉”市场价格的订单网格。
神经网络变得轻松(第三十七部分):分散关注度 神经网络变得轻松(第三十七部分):分散关注度
在上一篇文章中,我们讨论了在其架构中使用关注度机制的关系模型。 这些模型的具体特征之一是计算资源的密集功用。 在本文中,我们将研究于自我关注度模块内减少计算操作数量的机制之一。 这将提高模型的常规性能。
首次启动MetaTrader VPS:分步说明 首次启动MetaTrader VPS:分步说明
使用EA交易或订阅信号的每个交易者几乎都会认识到,需要为自己的交易平台租用一个可靠的24/7全天候主机服务器。出于多种原因,我们建议使用MetaTrader VPS。您可以通过MQL5.community账户方便地支付服务费用和管理订阅。
复购算法:提高效率的数学模型 复购算法:提高效率的数学模型
在本文中,我们将使用复购算法来更深入地了解交易系统的效率,并开始研究使用数学和逻辑提高交易效率的一般原则,以及在使用任意交易系统方面应用更能提高效率的非标准方法。