![开发回放系统 — 市场模拟(第 10 部分):仅用真实数据回放](https://c.mql5.com/i/articles/overlay.png)
开发回放系统 — 市场模拟(第 10 部分):仅用真实数据回放
概述
在上一篇文章 开发回放系统 — 市场模拟(第 09 部分):自定义事件 中,我们讲述了如何触发自定义事件。 从指标与服务的交互角度来看,我们还实现了一个非常有趣的系统。 在本文中,我将强调维护交易跳价的重要性。 如果您尚未尝试过这种实践,您应当认真考虑每天做。 记录您需要详细研究资产的所有交易价值。
寻找奇迹般恢复丢失信息的方案是没有意义的。 一旦信息丢失,无论采用什么方法,都不可能再找回它。 如果您的真正目标是深入挖掘并了解特定资产,就不要将其往后推迟。 尽快开始将交易跳价存储在安全的地方,因为它们是无价的。 随着时间的流逝,您将明白柱线数据也许与交易跳价的数值不一致。 这种差异可能令其难以理解,尤其是对于那些不太熟悉金融市场的人来说。
有时某些事件会影响这些值,例如,因为 1-分钟柱线不能反映真实操作。 还有不同的事件,如收益公告、资产合并或拆分、资产到期(在期货的情况下)、债券发行、等等。 所有这些状况的示例都可能扭曲 1-分钟柱线的当前数值,令它们与当时或周期内的现实交易不一致。
这是由于当这些事件发生时,资产的价格会以某种方式进行调整。 无论我们多么努力尝试,我们都无法调整 1-分钟柱线,令其等同于交易跳价。 这就是为什么跳价是无价的。
注意:也许有人会说,如果我们把差值相加,则数值会等同。 然而,这不是问题。 困境在于,如果市场感知价格在一定范围内,它就会以某种方式做出反应。 如果我们感知它处于其它状况,那么反应就不同了。
因此,我们必须在回放/模拟系统中实现适配,以便使用独有的交易跳价文件。 尽管我们从本系列开始就这样做了,但除了创建回放之外,我们实际上仍然未用到在交易跳价中包含的数据。 不过,状况很快就会改变。 现在,我们也将能使用这些文件,以及它们包含的数据来创建预览柱线。 这将取代当前的方法,在现有方法中,我们只用到含有 1-分钟预览柱线数据的文件。
代码开发
大部分代码已经实现,所以我们不需要在这方面做太多工作。 不过,我们需要思考在回放/模拟期间需用到的配置文件。 我们开始使用这个文件时,很明显它对我们来说至关重要。 问题(尽管不是真正的问题)在于这个文件有两个结构:一个结构列出创建回放/模拟所需的交易跳价,另一个结构指示作为走势预览的柱线,其不会包含在模拟中,而只会被添加到资产之中。
这令您可以正确地分离和纠正信息,但现在我们面临一个小问题:如果跳价文件包含在柱线结构当中,系统将发出错误警告。 我们希望保持这种方式,如此创建回放/模拟就不会有风险,因为在创建配置文件时,会因拼写错误而导致事态失控。 如果我们尝试添加一个 1-分钟的柱线文件,就如同它是交易跳价文件,也会发生同样的事情:系统简单地将其视为错误。
那么,我们如何才能解决这个困境的一部分,并把跳价文件作为预览栏线,从而系统不会感知其错误呢? 于此,我将提供若干种可能的操作方案之一。 第一步是往 C_Replay.mqh 头文件里添加新定义。
#define def_STR_FilesBar "[BARS]" #define def_STR_FilesTicks "[TICKS]" #define def_STR_TicksToBars "[TICKS->BARS]" // ... The rest of the code...
此定义将有助于判定何时应将一个或多个跳价文件当作柱线文件对待。 这将使后续步骤更容易。 不过,还有一个额外的细节:我们不会把自己局限于添加此定义。 如同其它定义一样,我们还允许用户更改它,但会以更优雅的方式。 这样做的潜在好处是一切更清晰,令用户更易于理解。 无论如何,对于系统来说并无太大的变化,它将继续正确解释所有内容。
故此,如果用户决定在配置文件中输入以下定义:
[ TICKS -> BARS ]
这应该被理解为一个有效的定义。 与此同时,我们对参数进行了一点扩展,令用户更容易理解。 原因是有些人不喜欢对数据进行分组,而是按逻辑将其分开,这是完全可以接受的,我们能允许。 为了提供这种灵活性,允许用户稍微“更改”所提供的定义,我们需要在服务代码中添加一些细节。 这将令代码更清晰,并允许我们在未来尽可能轻松地扩展功能。 为此,我们将创建一个枚举器。 不过,下面可以看到一个细节:
class C_Replay { private : enum eTranscriptionDefine {Transcription_FAILED, Transcription_INFO, Transcription_DEFINE}; int m_ReplayCount; datetime m_dtPrevLoading; // ... The rest of the code...
此枚举器是 C_Replay 类私用的,这意味着从类外部无法访问它。 而当我们把它放在私密声明模块中来启用这一点时,我们也获得了轻松增加配置文件复杂性的能力。 不过,我们将在本系列的后续文章中再讲解有关详细信息,对于本文来说它离题太远了。
之后,我们可以专注于下一个要实现的项目。 这个项目实际上是一个函数,它允许我们定义配置文件中所包含数据的分类。 我们来看看这个过程是如何工作的。 我们将在此用到以下代码:
inline eTranscriptionDefine GetDefinition(const string &In, string &Out) { string szInfo; szInfo = In; Out = ""; StringToUpper(szInfo); StringTrimLeft(szInfo); StringTrimRight(szInfo); if (StringSubstr(szInfo, 0, 1) != "[") { Out = szInfo; return Transcription_INFO; } for (int c0 = 0; c0 < StringLen(szInfo); c0++) if (StringGetCharacter(szInfo, c0) > ' ') StringAdd(Out, StringSubstr(szInfo, c0, 1)); return Transcription_DEFINE; }
我们拟将从配置文件中初步转换数据的整个过程集中于此。 无论后续工作走向如何,上述函数都会进行所有初步检查,确保接收到的数据与某个范式匹配。 在此之前要做的第一件事是将所有字符转换为大写。 此后,我们删除所有后续步骤不需要的元素。 一旦完成,我们检查序列的第一个字符,如果它与 “[” 不同,我们将得到一个指示,表明该序列代表一些信息,而非定义。
在这种特殊情况下,我们简单地返回先前所执行进程的结果。 如果不是这种情况,那么我们就开始审阅这些定义。 即便它们采用不同的格式,内容也也许是适当和正确的。 读取时,我们必须忽略任何值小于空格的字符。 因此,即使我们写:[ B A R S ],系统也会解释为 [BARS]。 在这种情况下,输入的编写方式可能会略有不同,但只要内容符合预期,我们就会得到相应的结果。
我们现在得到一个新系统,可基于配置文件的内容进行读取和配置。
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 open\nconfiguration file.", "Market Replay", MB_OK); return false; } Print("Loading data for replay. Please wait...."); ArrayResize(m_Ticks.Rate, def_BarsDiary); m_Ticks.nRate = -1; m_Ticks.Rate[0].time = 0; while ((!FileIsEnding(file)) && (!_StopFlag)) switch (GetDefinition(FileReadString(file), szInfo)) { case Transcription_DEFINE: if (szInfo == def_STR_FilesBar) isBars = true; else if (szInfo == def_STR_FilesTicks) isBars = false; break; case Transcription_INFO: if (szInfo != "") if (!(isBars ? LoadPrevBars(szInfo) : LoadTicksReplay(szInfo))) { if (!_StopFlag) MessageBox(StringFormat("File %s from %s\ncould not be loaded.", szInfo, (isBars ? def_STR_FilesBar : def_STR_FilesTicks), "Market Replay", MB_OK)); FileClose(file); return false; } break; } FileClose(file); return (!_StopFlag); }
不过,您不要太兴奋。 这里唯一的变化是以前的结构。 行动的方法保持不变。 我们仍然不能把包含交易跳价的文件当作 1-分钟预览柱线。 我们需要设置上面的代码。
如果我们抵近查看,我们会发现我们已经获得了以前不可能做到的能力。 想一想:数据被发送到一个过程,其中集中了从配置文件中读取数据的所有预处理。 上面代码中所用的数据已经是 “干净” 的了。 这是否意味着我们可以在配置文件中包含注释? 答案是 肯定的。 现在我们可以这样做。 我们只需要确定注释的格式:它是否包含一行或多行。 我们从一行注释开始。 该过程简单明了:
inline eTranscriptionDefine GetDefinition(const string &In, string &Out) { string szInfo; szInfo = In; Out = ""; StringToUpper(szInfo); StringTrimLeft(szInfo); StringTrimRight(szInfo); if (StringSubstr(szInfo, 0, 1) == "#") return Transcription_INFO; if (StringSubstr(szInfo, 0, 1) != "[") { Out = szInfo; return Transcription_INFO; } for (int c0 = 0; c0 < StringLen(szInfo); c0++) if (StringGetCharacter(szInfo, c0) > ' ') StringAdd(Out, StringSubstr(szInfo, c0, 1)); return Transcription_DEFINE; }
如果指定的字符位于行的开头,则该字符将被视为注释,并且将忽略其余全部内容。 如此这般,现在我们可以在配置文件中插入注释。 很有趣,不是吗? 简单地添加代码行,我们现在就可支持注释。 不过,我们回到主要问题。 我们的代码尚未将交易文件当作 1-分钟柱线对待。 为此,我们需要进行一些更改。 在进行这样的更改之前,我们需要确保系统能继续像以前一样工作,但又具有一些新功能。 如此,我们得到以下代码:
bool SetSymbolReplay(const string szFileConfig) { #define macroERROR(MSG) { FileClose(file); if (MSG != "") MessageBox(MSG, "Market Replay", MB_OK); return false; } int file, iLine; string szInfo; char iStage; if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) { MessageBox("Failed to open\nconfiguration file.", "Market Replay", MB_OK); return false; } Print("Loading data for replay. Please wait...."); ArrayResize(m_Ticks.Rate, def_BarsDiary); m_Ticks.nRate = -1; m_Ticks.Rate[0].time = 0; iStage = 0; iLine = 1; while ((!FileIsEnding(file)) && (!_StopFlag)) { switch (GetDefinition(FileReadString(file), szInfo)) { case Transcription_DEFINE: if (szInfo == def_STR_FilesBar) iStage = 1; else if (szInfo == def_STR_FilesTicks) iStage = 2; else if (szInfo == def_STR_TicksToBars) iStage = 3; else macroERROR(StringFormat("%s is not recognized in system\nin line %d.", szInfo, iLine)); break; case Transcription_INFO: if (szInfo != "") switch (iStage) { case 0: macroERROR(StringFormat("Couldn't recognize command in line %d\nof the configuration file.", iLine)); break; case 1: if (!LoadPrevBars(szInfo)) macroERROR(StringFormat("This line is declared in line %d", iLine)); break; case 2: if (!LoadTicksReplay(szInfo)) macroERROR(StringFormat("This line is declared in line %d", iLine)); break; case 3: break; } break; }; iLine++; } FileClose(file); return (!_StopFlag); #undef macroERROR }
当我们调用此过程时,第一个动作是初始化一些参数供我们所用。 我们正在计算行数 — 这对于系统准确报告错误发生的位置是必要的。 出于这个原因,我们定义了一个宏,它产生一条与各种状况对应的常规错误消息。 现在我们的系统将按步骤工作。 因此,我们需在以下几行中明确定义将要处理的事情。 跳过此步骤将被视为出错。 在这种情况下,第一个错误将始终等于零,因为在初始化过程时,我们表明我们处于分析的零阶段。
尽管这种结构看起来很复杂,但它令我们能够针对期待的任何方面进行快速、有效地扩展。 添加的内容对于以前的代码影响甚微。 这种方式允许我们遵照内部格式处理配置文件,如下所示:
#First set initial bars, I think 3 is enough .... [Bars] WIN$N_M1_202108020900_202108021754 WIN$N_M1_202108030900_202108031754 WIN$N_M1_202108040900_202108041754 #I have a tick file but I will use it for pre-bars ... thus we have 4 files with bars [ Ticks -> Bars] WINQ21_202108050900_202108051759 #Now use the file of traded ticks to run replay ... [Ticks] WINQ21_202108060900_202108061759 #End of the configuration file...
现在,您可以厘清正在发生的事情,并采用更有效的格式。 但我们尚未实现我们想要的。 我正在展示的只是如何对代码进行小改动,令事情变得更有趣。 这些变化并不像您想象的那么困难。
我们继续。 我们正在进行必要的修改,以便能用交易跳价文件转化为预览柱线。 在您开始遇到一些复杂的代码之前,我会让您知晓,必要的代码已经在回放/模拟服务中准备好了。 它只是隐藏在复杂性之中。 现在我们需要提取这些代码,并贴出来。 这项工作必须非常谨慎地进行,因为任何错误都可能危及整个现有系统。 为了理解这一点,我们看看上一篇文章中提到的以下代码:
bool LoadTicksReplay(const string szFileNameCSV) { int file, old; string szInfo = ""; MqlTick tick; MqlRates rate; if ((file = FileOpen("Market Replay\\Ticks\\" + szFileNameCSV + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE) { ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray); ArrayResize(m_Ticks.Rate, def_BarsDiary, def_BarsDiary); 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 ticks for replay. 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; if (tick.volume_real > 0.0) { m_Ticks.nRate += (BuiderBar1Min(rate, tick) ? 1 : 0); rate.spread = m_Ticks.nTicks; m_Ticks.Rate[m_Ticks.nRate] = rate; m_Ticks.nTicks++; } 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); };
虽然这段代码旨在读取和保存交易跳价,为之后的回放/模拟所用,但造就了一重点。 在一些时刻,我们打算仅用交易跳价数据来创建等价的 1-分钟柱线。
现在我们来想一想:这不正是我们想要做的吗? 我们本打算读取一个交易跳价文件,并创建一根 1-分钟的柱线,然后将其保存在回放/模拟对应的资产之中。 不过,我们不是将其保存为交易跳价,而是将其数据解释为预览柱线。 因此,如果我们对上述代码进行小幅更改,取代简单地存储这些跳价,那么我们就可以在读取交易跳价同时创建柱线,并插入到资产之中,如此就可在回放/模拟中作为预览柱线进行分析。
在这项任务中需考虑一个重要的细节。 在讨论实现时,我们将涵盖这一点,因为但凡您看到实际构建和操作的代码时,您才会理解它。 我们先来看一下负责将交易跳价转化为柱线的代码,如下所示:
bool SetSymbolReplay(const string szFileConfig) { #define macroERROR(MSG) { FileClose(file); MessageBox((MSG != "" ? MSG : StringFormat("Error occurred in line %d", iLine)), "Market Replay", MB_OK); return false; } int file, iLine; string szInfo; char iStage; if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) { MessageBox("Failed to open\nconfiguration file.", "Market Replay", MB_OK); return false; } Print("Loading data for replay. Please wait...."); ArrayResize(m_Ticks.Rate, def_BarsDiary); m_Ticks.nRate = -1; m_Ticks.Rate[0].time = 0; iStage = 0; iLine = 1; while ((!FileIsEnding(file)) && (!_StopFlag)) { switch (GetDefinition(FileReadString(file), szInfo)) { case Transcription_DEFINE: if (szInfo == def_STR_FilesBar) iStage = 1; else if (szInfo == def_STR_FilesTicks) iStage = 2; else if (szInfo == def_STR_TicksToBars) iStage = 3; else macroERROR(StringFormat("%s is not recognized in system\nin line %d.", szInfo, iLine)); break; case Transcription_INFO: if (szInfo != "") switch (iStage) { case 0: macroERROR(StringFormat("Couldn't recognize command in line %d\nof the configuration file.", iLine)); break; case 1: if (!LoadPrevBars(szInfo)) macroERROR(""); break; case 2: if (!LoadTicksReplay(szInfo)) macroERROR(""); break; case 3: if (!LoadTicksReplay(szInfo, false)) macroERROR(""); break; } break; }; iLine++; } FileClose(file); return (!_StopFlag); #undef macroERROR }
虽然我们对错误报告系统进行了一些小的修正,以便标准化报告,但这并非我们此刻的目标。 我们真正感兴趣的是,据交易跳价生成 1-分钟柱线的调用。 注意,该调用与我们之前的调用雷同,只是增加了一个参数。 这个简单的额外参数细节是关键,它令我们不必重写整个函数,如下面的代码所示。
所有必要的逻辑都已经存在于原始的回放/模拟服务功能之中,处理来自交易跳价文件的数据,并将其转化为 1-分钟柱线。 我们真正需要做的是,初步调配这些柱线,且不影响整体性能。 因为若无必要的修改,在回放/模拟服务进行时可能会出错。 如此,我们来看看针对读取交易跳价的代码所做的修改。 由此,系统就能将这些跳价化为柱线。
bool LoadTicksReplay(const string szFileNameCSV, const bool ToReplay = true) { int file, old, MemNRates, MemNTicks; string szInfo = ""; MqlTick tick; MqlRates rate, RatesLocal[]; MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate); MemNTicks = m_Ticks.nTicks; if ((file = FileOpen("Market Replay\\Ticks\\" + szFileNameCSV + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE) { ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray); ArrayResize(m_Ticks.Rate, def_BarsDiary, def_BarsDiary); 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 ticks for replay. 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; if (tick.volume_real > 0.0) { m_Ticks.nRate += (BuiderBar1Min(rate, tick) ? 1 : 0); rate.spread = (ToReplay ? m_Ticks.nTicks : 0); m_Ticks.Rate[m_Ticks.nRate] = rate; m_Ticks.nTicks++; } old = (m_Ticks.nTicks > 0 ? m_Ticks.nTicks - 1 : old); } } if ((!FileIsEnding(file)) && (!_StopFlag)) { Print("Too much data in the tick file.\nCannot continue..."); FileClose(file); return false; } FileClose(file); }else { Print("Tick file ", szFileNameCSV,".csv not found..."); return false; } if ((!ToReplay) && (!_StopFlag)) { ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates)); ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0); CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates)); m_dtPrevLoading = m_Ticks.Rate[m_Ticks.nRate].time; m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates); m_Ticks.nTicks = MemNTicks; ArrayFree(RatesLocal); } return (!_StopFlag); };
首先,我们需要添加一些额外的变量。 这些局部变量临时存储重要信息,如此您在以后必要时,可将系统恢复到其原始状态。 注意,所有代码都与原始代码雷同。 我们可以在它们出现时添加 1-分钟柱线,但我采用了不同的方式。 当然,我必须在一个特定的地方进行更改,正是我们评估数据是否被转化为柱线的位置。 如果数据已转化为柱线,我们必须确保一致,且合乎逻辑表示。
此外,我们确保在不干扰原始程序的情况下执行读取。 读取交易跳价,并转化为 1-分钟柱线,那里没有变化。 不过,一旦文件被完全读取,且一切都按预期进行,我们就会进入一个新的阶段。 这才是真正的转化发生之处。 如果已分配跳价文件转化为预览栏线系统,并且用户在读取时没有请求终止回放/模拟服务,那么我们就发现了真实状态。 然后,我们采取具体措施来确保数据被正确使用,并将系统恢复到其原始状态。 由此,我们避免了回放阶段的问题和异常情况。
这里的第一步是分配内存,来临时存储 1-分钟柱线。 一旦此操作完成,我们就在读取文件至这个临时空间的同时,移到柱线构建。 此操作对于下一步至关重要,下一步是将柱线插入回放/模拟的资产中,并确保其成功。 如果没有前面的操作,将很难在资产中正确校正 1-分钟柱线位置,如此它们就能解释成预览柱线。 注意,如果我们选择了在创建时添加柱线的直接方法,则不需要所有这些逻辑。 不过,对于所选择的方法,我们首先读取文件,然后保存柱线,我们需要这些操作来确保操作无误。
以这种方式派发和传输数据,该过程得到了简化,因为不需要创建传输循环。 转换完成后,我们将校正柱线极限位置的值,并恢复在程序开始时保存的值。 以这种方式,系统最终就像什么都没有改变一样工作。 对于系统来说,柱线的读取似乎是直接从 1-分钟柱线文件中完成的,而非来自跳价图表。 柱线将按预期显示,我们最终应当释放一些临时内存空间。
至此,我们以最小的努力克服了一个严重的问题。 然而,所提出的解决方案并非唯一可能,尽管它似乎需要修改的源代码最少。
删除所有回放图形
在系统关闭时刻,系统中有一个有趣的细节。 通常,当我们关闭带有控制指标的图表时,就会发生这种情况。 实际上,这不是一个问题,而只是一个小小的不便。 许多人喜欢同时打开同一资产的多个图表。 这是正常且可以理解的,且在某些状况下可能很有用。 不过,考虑以下情况:当回放/模拟服务尝试删除所用资产的任何痕迹时,它会失败。 原因很简单:回放资产的另一个图表处于打开状态。
因此,系统无法删除这些痕迹。 市场观察窗口中剩下一种资产是没有意义的。 虽然可以手动删除它,但这不是我们想要的。 回放/模拟服务应完全自动删除任何痕迹。 为了修复它,我们需要对代码进行一些更改。 为了真正理解将要添加的内容,我们看看下面的源代码:
void CloseReplay(void) { ArrayFree(m_Ticks.Info); ArrayFree(m_Ticks.Rate); ChartClose(m_IdReplay); SymbolSelect(def_SymbolReplay, false); CustomSymbolDelete(def_SymbolReplay); GlobalVariableDel(def_GlobalVariableReplay); GlobalVariableDel(def_GlobalVariableIdGraphics); }
注意这段代码的行为:调用它时,只会关闭由服务打开的图表。 如果没有其它打开的图表引用回放资产,您可以将其从市场观察窗口中删除。 然而,如前所述,用户可能打开了市场回放资产的其它图表。 在这种情形下,当我们试图从市场观察窗口中删除资产时,我们失败了。 为了解决这个问题,我们需要改变服务的终止方式。 我们需要调整相应的代码行,来获得更复杂,但也更有效的解决方案。 相关代码如下图所示:
void CloseReplay(void) { ArrayFree(m_Ticks.Info); ArrayFree(m_Ticks.Rate); m_IdReplay = ChartFirst(); do { if (ChartSymbol(m_IdReplay) == def_SymbolReplay) ChartClose(m_IdReplay); }while ((m_IdReplay = ChartNext(m_IdReplay)) > 0); for (int c0 = 0; (c0 < 2) && (!SymbolSelect(def_SymbolReplay, false)); c0++); CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX); CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX); CustomSymbolDelete(def_SymbolReplay); GlobalVariableDel(def_GlobalVariableReplay); GlobalVariableDel(def_GlobalVariableIdGraphics); }
这段代码可能看起来很奇怪,但函数非常简单。 首先,我们捕获第一个图表的 ID。 需要注意的是,它无需第一个打开。 接下来,我们开始一个循环。 在此循环中,我们执行一些检查,判定图表所引用的资产。 如果是回放资产,则关闭图表。 为了判定循环是否完成,我们会要求平台告诉我们列表中下一个图表的 ID。 如果列表中没有其它图表,则返回小于零的值,并结束循环。 否则,循环将再次执行。 这可确保与我们回放资产相同的窗口都被关闭,无论其数量。 然后,我们尝试两次从市场观察窗口中删除回放资产。 之所以要尝试两次,是因为若只有我们打开的回放/模拟服务窗口时,一次尝试就足以删除资产。 但如果用户打开了其它窗口,或许就需要第二次尝试。
通常一次尝试就足够了。 如果我们无法从“市场报价”窗口中删除资产,我们将无法删除自定义交易品种。 但无论如何,我们将删除用户资源中存在的任何内容,因为它除了回放/模拟服务之外没有任何用处。 什么都不应该留在那里。 即使我们不能完全删除自定义资产,也不会有什么大问题,因为里面不会有任何内容。 不过,该过程的目标就是将其从平台中彻底删除。
结束语
在下面的视频中,您可以看到本文讲解的工作结果。 虽然有些东西也许还不可见,但观看视频会令您清楚地了解所有这些文章中回放/模拟系统的实现进度。 只需观看视频,并比较从一开始到现在的变化。
在下一篇文章中,我们将继续开发该系统,因为还有一些真正必要的功能需要实现。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10932