开发回放系统 — 市场模拟(第 16 部分):新的类系统
概述
在上一篇文章“开发回放系统 — 市场模拟(第 15 部分):模拟器(V)的诞生 - 随机游走” 中,我们开发了一种获取随机数据的方法。 这样做是为了确保与结果的完全适配。 但即便该系统确有能力遵循一种合理的方式模拟跳价,它仍然缺乏某些信息,不管是来自模拟器,还是来自回放系统本身。 真相在于我们需要实现的一些事情非常复杂,特别是从系统构建的角度。 故此,我们需要进行一些更改,从而至少可获得更佳的模型,且在接下来的步骤中不会迷失方向。
信任我,如果您发现随机游走具有挑战性,那只是因为您尚未见识到、或者不知道需要开发什么,才能令模拟/回放达到最佳体验。 最具挑战性的层面之一是,我们必须至少知晓有关资产模拟或回放的基础知识。 在当前开发阶段,我不费心主动解决一些问题,因为有更实用的途径可以做到这一点。
如果您仔细研究过上一篇文章,您会注意到诸如 EXCEL 这样的程序可用于创建可能的市场场景。 但我们也能采用来自其它资产的信息,并将它们组合起来创建更复杂的模拟。 这令创建一个全自动寻找有关资产自身缺失信息的系统变得困难。 我们还有一个小问题。 因此,在这里我们将剪切和派发 C_Replay.mqh 头文件的内容,从而简化维护和改进。 我不喜欢操控一个超大型的类,这对我来说似乎很不方便。 原因在于,许多情况下代码难以完全优化。 由此,出于易用起见,我们将从两方面更改,即我们把 C_Replay 类的内容分散到若干个类之中,与此同时实现最相关的部分。
以新的类系统实现服务
尽管前几篇文章中讨论的代码通常很少更改,但这个新实现包含了一个新模型。 对于那些没有深厚编程知识的人来说,这个模型更易于扩展,也更易于理解,因为所连接元素的方式更简单。 我们无需阅读冗长且累人的函数。 有些细节也许看似有点奇怪,但怪异的是,它们令代码更加安全、稳定、且结构合理。 其中一个细节是指针的用法与 C++ 或传统 C 语言中的不同。 不过,该行为与这些语言非常相似。
指针的运用令我们能够做到原本不可能的事情。 尽管 C++ 中引入的许多特色在 MQL5 中不可用,但以最简单的形式使用指针的朴素事实令我们能以更灵活、更愉快的方式实现建模,至少在编程和语法方面如此。
故而,当前版本中的新服务文件现在如下所示:
#property service #property icon "\\Images\\Market Replay\\Icon.ico" #property copyright "Daniel Jose" #property version "1.16" #property description "Replay-simulation system for MT5." #property description "It is independent from the Market Replay." #property description "For details see the article:" #property link "https://www.mql5.com/ru/articles/11095" //+------------------------------------------------------------------+ #define def_Dependence "\\Indicators\\Market Replay.ex5" #resource def_Dependence //+------------------------------------------------------------------+ #include <Market Replay\C_Replay.mqh> //+------------------------------------------------------------------+ input string user00 = "Config.txt"; //"Replay" config file. input ENUM_TIMEFRAMES user01 = PERIOD_M1; //Initial timeframe for the chart. input bool user02 = true; //Visual bar construction. input bool user03 = true; //Visualize creation metrics. //+------------------------------------------------------------------+ void OnStart() { C_Replay *pReplay; pReplay = new C_Replay(user00); if (pReplay.ViewReplay(user01)) { Print("Permission received. The replay service can now be used..."); while (pReplay.LoopEventOnTime(user02, user03)); } delete pReplay; } //+------------------------------------------------------------------+
看似没有经历任何重大变化,但实际上一切都完全不同。 对于最终用户而言,它保持不变,并具有与以前版本完全相同的行为。 但对于平台而言,尤其是对于操作系统,生成的可执行文件看起来会有所不同,且具有不同的行为。 原因在于我们所用的类并非当作变量,而是当作指向内存的指针。 这就是为什么整个系统的行为完全不同。 如果您仔细编程,比之仅把类当作变量,类的这种用法更加安全和稳定。 尽管它们展现出相同的行为,但它们在内存使用方面会有所不同。
现在,由于类被当作指针来用,我们就要排除一些东西,并开始用到其它东西。 第一件事就是,该类始终要显式开启和关闭。 这是使用 new 和 delete 操作符完成的。 当我们使用 “new” 操符创建类实例时,我们必须始终调用类的构造函数。 它们从不返回任何值,如此我们就并不能直接检查返回值。 我们只好在另一个时间做这件事。 同样的事情当使用 “delete” 操作符时也会发生,并且将调用类的析构函数。 与类的构造函数一样,析构函数也从不返回任何值。 但与构造函数不同的是,析构函数不接收任何参数。
我们必须始终以这种方式进行:我们用 “new” 操作符创建一个类实例,并用 “delete” 操作符销毁该类实例。 这就是我们仅有的真正要做的工作。 剩下的则由操作系统完成:它在特定的内存区域中为程序分配足够的内存以便运行,令其尽可能安全地贯穿整个存在期。 但对于那些习惯于在 C++/C 中使用指针的人来说,这里存在着危险:我们所说的是注入符。 在 C++ 和 C 中,每当我们引用指针时,我们都会用到非常特殊注入符。 通常我们使用箭头(->)。 对于 C++/C 程序员,这意味着用的是指针。 但我们也可以使用不同的注入符,在我的代码中访问指针时就能见到。
此外,当然,还要用到变量名称,通常以字母 “p” 或 “ptr” 等组合开头(尽管这并非一个严格的规则,故不必强求)。 虽然上面和下面代码中所示的两种注入符 MQL5 都接受,但我个人发现在实际代码里采用正确声明的指针更易于阅读。 因此,在我们的代码中,注入符将如下所示,这要致谢我对 C++/C 语言的了解:
void OnStart() { C_Replay *pReplay; pReplay = new C_Replay(user00); if ((*pReplay).ViewReplay(user01)) { Print("Permission received. The replay service can now be used..."); while ((*pReplay).LoopEventOnTime(user02, user03)); } delete pReplay; }
实际上,还有与编写代码相关的额外工作。 但是作为一个拥有多年经验的 C++/C 程序员,当我查看上面所示的代码时,我很轻易就明白它是引用指针。 由于 MQL5 的理解方式与 C++/C 相同,故我明白注入符这样用没有问题。 每当我们看到含有如上所示注入符的代码时,您不必担心,因为它就只是一个指针。
我们可以继续探索新的类系统。 如果您认为只发生了这些变化,您就太乐观了。 进行这些更改实际上是为了我们要明确保证类实例应在特定时间创建和销毁,这就需要对代码进行更多修改。 构造函数和析构函数不返回任何值。 但我们必须以某种方式知道类实例是否正确创建。
为了了解如何做到这点,您需要查看 C_Replay 类的黑匣子内部。 它位于头文件 C_Replay.mqh 当中。 其内部结构如下图所示:
图例 01 - 回放类连接系统
图例 01 展示了这些类如何相互连接,以便通过回放/模拟服务执行所需的操作。 绿色箭头表示该类已导入,如此该类的某些内部成员就是公开的。 红色箭头表示数据将被导入,但它们在深层类中将不再可见。 C_FilesBars 类是一个松散类。 实际上它不会由任何其它类继承,但其方法会由其它类所用。
若要真正理解这些连接是如何创建的,您需要看看内部将会发生什么。 为此,我们需要查看每个类是如何创建的,以及它们在相应的文件中如何定位。 这些文件始终与其类同名。 这不是必需的,但不失为一种很良性做法,鉴于代码曾经历了一些变化。 但我们不会深耕有关细节,几分钟后我们就会看到发生了哪些变化,以及原因。 对于那些刚开始编程之旅的人来说这些非常实用,因为已有了功能代码,且我们能修改它,并生成不同的东西(我们此刻想要的东西),故学习起来要容易得多,同时保持代码的可操作性。
我们看看该结构是如何实现的。
松散类:C_FileBars
该类非常简单,包含读取文件中存在柱线所需的一切。 它不是一个很大型的类。 其完整代码如下所示:
#property copyright "Daniel Jose" //+------------------------------------------------------------------+ #include "Interprocess.mqh" //+------------------------------------------------------------------+ #define def_BarsDiary 1440 //+------------------------------------------------------------------+ class C_FileBars { private : int m_file; string m_szFileName; //+------------------------------------------------------------------+ inline void CheckFileIsBar(void) { string szInfo = ""; for (int c0 = 0; (c0 < 9) && (!FileIsEnding(m_file)); c0++) szInfo += FileReadString(m_file); if (szInfo != "<DATE><TIME><OPEN><HIGH><LOW><CLOSE><TICKVOL><VOL><SPREAD>") { Print("Файл ", m_szFileName, ".csv не является файлом баров."); FileClose(m_file); m_file = INVALID_HANDLE; } } //+------------------------------------------------------------------+ public : //+------------------------------------------------------------------+ C_FileBars(const string szFileNameCSV) :m_szFileName(szFileNameCSV) { if ((m_file = FileOpen("Market Replay\\Bars\\" + m_szFileName + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) Print("Could not access ", m_szFileName, ".csv with bars."); else CheckFileIsBar(); } //+------------------------------------------------------------------+ ~C_FileBars() { if (m_file != INVALID_HANDLE) FileClose(m_file); } //+------------------------------------------------------------------+ bool ReadBar(MqlRates &rate[]) { if (m_file == INVALID_HANDLE) return false; if (FileIsEnding(m_file)) return false; rate[0].time = StringToTime(FileReadString(m_file) + " " + FileReadString(m_file)); rate[0].open = StringToDouble(FileReadString(m_file)); rate[0].high = StringToDouble(FileReadString(m_file)); rate[0].low = StringToDouble(FileReadString(m_file)); rate[0].close = StringToDouble(FileReadString(m_file)); rate[0].tick_volume = StringToInteger(FileReadString(m_file)); rate[0].real_volume = StringToInteger(FileReadString(m_file)); rate[0].spread = (int) StringToInteger(FileReadString(m_file)); return true; } //+------------------------------------------------------------------+ datetime LoadPreView(const string szFileNameCSV) { int iAdjust = 0; datetime dt = 0; MqlRates Rate[1]; Print("Loading bars for Replay. Please wait...."); while (ReadBar(Rate) && (!_StopFlag)) { iAdjust = ((dt != 0) && (iAdjust == 0) ? (int)(Rate[0].time - dt) : iAdjust); dt = (dt == 0 ? Rate[0].time : dt); CustomRatesUpdate(def_SymbolReplay, Rate, 1); } return ((_StopFlag) || (m_file == INVALID_HANDLE) ? 0 : Rate[0].time + iAdjust); } //+------------------------------------------------------------------+ };
您也许觉得该类没有太多意义,但它包含打开、读取、生成自定义资源、和关闭面板文件所需的一切。 所有这些都是经由非常具体的步骤实现的,故此处所做的修改无关紧要。 如果读者打算读取柱线文件,可以在此处完成。
该类的设计,应始终由操作符 NEW 和 DELETE 管理。 由此,它才能在内存中保留足够长的时间来完成其工作。 我们应该尽量避免不用上述操作符就使用该类的情况。 否则,我们也许会遇到稳定性问题。 这并非真的会发生。 但事实上它在设计时,就是为了经由操作符管理,故并不适合其它方式。
在该类中,我们有 2 个全局性私密变量。 它们在类的构造函数中精确初始化,其中我们会尝试打开指定的文件,并检查是否为柱线文件。 不过,请不要忘记构造函数不返回任何类型的数值,我们根本无法在此处返回任何内容,但我们能指示文件不符合预期,即将其标注为无效文件。 关闭后会变得清晰。 任何读取尝试都会导致错误,并报告相应错误。 返回后,可由调用者处理。 于是乎,该类的运作如同一个已经包含柱线的文件。 或者已包含文件内容的大型对象。 此处的调用者将读取该大型对象的内容。 但是一旦调用析构函数,文件将被关闭,且该类将被调用程序销毁。
这种类型的模拟也许看似不那么安全和稳定,但相信我,它比看上去的更安全、更稳定。 如果可以访问 C++ 中存在的其它一些运算符,事情会变得更加有趣。 当然,为此您需要编写正确代码,否则一切都将是一场彻底的灾难。 但由于 MQL5 并非 C++,我们要研究并发挥这种语言的能力。 这样的话,我们将拥有一个系统,其所用近乎该语言允许我们达成的极限。
深度类:C_FileTicks
我们下一个要查看的是 C_FileTicks 类。 它比 C_FileBars 类复杂得多,因为我们有公开元素、私密元素、和介于两者之间的元素。 它们有一个特殊的名字:PROTECTED。 当涉及到类之间的继承时,术语 “protected(受保护)” 具有特殊的级别。 以 C++ 为例,一切都非常复杂,至少在学习开始时如此。 这是由于 C++ 中存在的一些操作符。 幸运的是,MQL5 以一种更简单的方式解决了这个问题。 故此,就会更容易地理解声明为受保护的元素是如何继承的,以及它们是否可以被访问,当然这取决于继承是如何发生的。 参见下表:
基类中的定义 | 基类继承类型 | 派生类中的访问 | 通过调用派生类访问 |
---|---|---|---|
私密 | 公开 | 禁止访问 | 不可访问基类数据或过程 |
公开 | 公开 | 允许访问 | 能够访问基类数据或过程 |
受保护 | 公开 | 允许访问 | 不可访问基类数据或过程 |
私密 | 私密 | 禁止访问 | 不可访问基类数据或过程 |
公开 | 私密 | 允许访问 | 不可访问基类数据或过程 |
受保护 | 私密 | 允许访问 | 不可访问基类数据或过程 |
私密 | 受保护 | 禁止访问 | 不可访问基类数据或过程 |
公开 | 受保护 | 允许访问 | 不可访问基类数据或过程 |
受保护 | 受保护 | 允许访问 | 不可访问基类数据或过程 |
类元素和函数的访问级别表
在使用继承或访问定义时,仅在一种情况下,我们才能访问类中的数据或过程。 那就是一切都被声明为公开。 在所有其它情况下,在类的继承层面,或多或少是可能的,但访问位于基类内部的任何过程或数据都是不可能的。 这不依赖于访问从句。
重点: 如果您将某些内容声明为 “protected”,并尝试在非类继承的情况下直接访问其数据或过程,那么您会无法访问其数据或过程。 这是因为如果不通过继承,声明为受保护的数据或过程将被视为私密,故无法访问。
这看似很复杂,不是吗? 然而,在实践中,这要容易得多。 不过,我们只能靠多次的尝试举措,才能真正理解这种机制的功用。 但相信我,在 MQL5 中比之 C++ 要容易得多;那边要复杂得多。 原因是,在类继承过程中,对于声明为受保护的数据或过程(在某些情况下甚至是私密的),我们也有办法更改其访问级别。 那真是太疯狂了。 然而,在 MQL5 中,一切运作顺滑。
有基于此,我们能最终看到 C_FileTicks 类,尽管它在理论上更复杂,但代码相对简单。 我们开始查看一下类内部的第一个元素,并从它的声明开始:
#property copyright "Daniel Jose" //+------------------------------------------------------------------+ #include "C_FileBars.mqh" //+------------------------------------------------------------------+ #define def_MaxSizeArray 16777216 // 16 Mbytes of positions //+------------------------------------------------------------------+ #define macroRemoveSec(A) (A - (A % 60)) //+------------------------------------------------------------------+ class C_FileTicks { protected: struct st00 { MqlTick Info[]; MqlRates Rate[]; int nTicks, nRate; }m_Ticks; double m_PointsPerTick;
看到了吧,它很简单,但要当心,因为这个结构和变量按照访问级别表是能被访问的。 一旦操作完成后,我们将为该类提供一定数量的私密过程。 它们不能从类外部访问,并服务于支持公开程序。 其中的两个,如下所示:
public : //+------------------------------------------------------------------+ bool BarsToTicks(const string szFileNameCSV) { C_FileBars *pFileBars; int iMem = m_Ticks.nTicks; MqlRates rate[1]; MqlTick local[]; pFileBars = new C_FileBars(szFileNameCSV); ArrayResize(local, def_MaxSizeArray); Print("Converting bars to ticks. Please wait..."); while ((*pFileBars).ReadBar(rate) && (!_StopFlag)) Simulation(rate[0], local); ArrayFree(local); delete pFileBars; return ((!_StopFlag) && (iMem != m_Ticks.nTicks)); } //+------------------------------------------------------------------+ datetime LoadTicks(const string szFileNameCSV, const bool ToReplay = true) { int MemNRates, MemNTicks; datetime dtRet = TimeCurrent(); MqlRates RatesLocal[]; MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate); MemNTicks = m_Ticks.nTicks; if (!Open(szFileNameCSV)) return 0; if (!ReadAllsTicks()) return 0; if (!ToReplay) { ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates)); ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0); CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates)); dtRet = m_Ticks.Rate[m_Ticks.nRate].time; m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates); m_Ticks.nTicks = MemNTicks; ArrayFree(RatesLocal); } return dtRet; }; //+------------------------------------------------------------------+
所有私密过程都在本系列的前几篇文章中曾解释和分析过。 我们不会在此详述,因为过程与原本的解释几乎没有变化。 不过,关于这两个公开过程,有一些值得我们注意的地方。 这些是在 BarsToTicks 当中。 在上一个主题中,我提到 C_FileBars 类是一个松散类,并无实际继承。 这仍然为真。 但我也提到,它会在某处用到,并应按相当具体的方式访问。
这是其中一个时刻。 首先,我们以一种非常具体的方式声明类。 现在我们调用类的构造函数,参数是我们想要从中获取柱线值的文件名。 记住,此调用不会返回任何值。 我们使用 NEW 操作符,如此平台就会为类实例保留内存空间。 这个空间将包含该类实例,因为 MetaTrader 5 并不实际控制该类实例的所在。 只有操作系统才拥有此信息。
但更便捷的是,我们从 NEW 擦偶哦符中得到一个值,这个值是一个“指针”,我们可用它来直接引用我们的类(注意:“指针”这个词是用引号括起来的,因为它实际上并非一个指针,它只是一个能够引用一个类的变量,就好像该类是另一个变量或常量一样。 将来,我将展示如何用它来创建一个常量类,在该类中,我们只能访问数据,而不能执行计算。 由于它拥有非常特殊的用途,我们将把它留到另外的时间。) 。 一旦我们得到这个“指针”,我们就可基于该类工作,但再次,我们仍然不确定文件是否已打开,并可供读取。 因此,在尝试读取和使用任何数据之前,我们需要执行某种验证。 幸运的是,这不是必需的,因为在尝试读取期间,我们就可以检查调用是否正确完成。 也就是,我们能在此检查数据是否已被读取。
如果由于某种原因读取失败,则 WHILE 循环将关闭。 因此,无需进一步检查。 读取数据的每次尝试也用于检查读取是否成功。 这样的话,我们就能从 C_FileBars 类外部操控,但我们必须显式终止该类,如此就将它占用的内存返回给操作系统。 这是由 DELETE 操作符调用析构函数来完成的。 这可确保该类实例已被正确删除,并且不再可引用。
不这样做的话,也许会导致在我们的程序中数据不一致,甚至是垃圾。 但遵循上述过程,我们就能确切地知道何时、何地、以及如何使用该类。 在若干非常复杂的建模场景下,这能为我们提供助力。
一个类,多个函数:C_ConfigService
这个类非常有趣。 尽管这个类是充当 C_FileTicks 类和 C_Replay 类之间的桥梁,它以某种方式确保一切都保持我们的预期,并且配置系统中的改进或变化仅反映在真正需要可见的地方。 这不是一个非常宽泛或复杂的类,而只是一个含有相当简单代码的过渡类。 这个思路是把与设置回放/模拟服务相关的一切都放在这个类之中。 从本质上讲,其目的是读取配置文件,并将其内容应用于服务,如此它即可按用户的配置工作。
该类应按以下运作:读取和创建用于建模的跳价,生成资产的回放柱线,以及在某些情况下调整回放资产的变量。 由此,资产的行为将与真实资产非常接近。 以下是该类在当前开发阶段的完整代码:
#property copyright "Daniel Jose" //+------------------------------------------------------------------+ #include "C_FileBars.mqh" #include "C_FileTicks.mqh" //+------------------------------------------------------------------+ class C_ConfigService : protected C_FileTicks { protected: //+------------------------------------------------------------------+ datetime m_dtPrevLoading; //+------------------------------------------------------------------+ private : //+------------------------------------------------------------------+ enum eTranscriptionDefine {Transcription_INFO, Transcription_DEFINE}; //+------------------------------------------------------------------+ 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; } //+------------------------------------------------------------------+ inline bool Configs(const string szInfo) { const string szList[] = { "POINTSPERTICK" }; string szRet[]; char cWho; if (StringSplit(szInfo, '=', szRet) == 2) { StringTrimRight(szRet[0]); StringTrimLeft(szRet[1]); for (cWho = 0; cWho < ArraySize(szList); cWho++) if (szList[cWho] == szRet[0]) break; switch (cWho) { case 0: m_PointsPerTick = StringToDouble(szRet[1]); return true; } Print("Variable >>", szRet[0], "<< undefined."); }else Print("Configuration >>", szInfo, "<< invalid."); return false; } //+------------------------------------------------------------------+ inline void FirstBarNULL(void) { MqlRates rate[1]; rate[0].close = rate[0].open = rate[0].high = rate[0].low = m_Ticks.Info[0].last; rate[0].tick_volume = 0; rate[0].real_volume = 0; rate[0].time = m_Ticks.Info[0].time - 60; CustomRatesUpdate(def_SymbolReplay, rate, 1); } //+------------------------------------------------------------------+ inline bool WhatDefine(const string szArg, char &cStage) { const string szList[] = { "[BARS]", "[TICKS]", "[TICKS->BARS]", "[BARS->TICKS]", "[CONFIG]" }; cStage = 1; for (char c0 = 0; c0 < ArraySize(szList); c0++, cStage++) if (szList[c0] == szArg) return true; return false; } //+------------------------------------------------------------------+ public : //+------------------------------------------------------------------+ bool SetSymbolReplay(const string szFileConfig) { int file, iLine; char cError, cStage; string szInfo; bool bBarPrev; C_FileBars *pFileBars; if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) { MessageBox("Failed to open the\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; bBarPrev = false; iLine = 1; cError = cStage = 0; while ((!FileIsEnding(file)) && (!_StopFlag) && (cError == 0)) { switch (GetDefinition(FileReadString(file), szInfo)) { case Transcription_DEFINE: cError = (WhatDefine(szInfo, cStage) ? 0 : 1); break; case Transcription_INFO: if (szInfo != "") switch (cStage) { case 0: cError = 2; break; case 1: pFileBars = new C_FileBars(szInfo); if ((m_dtPrevLoading = (*pFileBars).LoadPreView(szInfo)) == 0) cError = 3; else bBarPrev = true; delete pFileBars; break; case 2: if (LoadTicks(szInfo) == 0) cError = 4; break; case 3: if ((m_dtPrevLoading = LoadTicks(szInfo, false)) == 0) cError = 5; else bBarPrev = true; break; case 4: if (!BarsToTicks(szInfo)) cError = 6; break; case 5: if (!Configs(szInfo)) cError = 7; break; } break; }; iLine += (cError > 0 ? 0 : 1); } FileClose(file); switch(cError) { case 0: if (m_Ticks.nTicks <= 0) { Print("No ticks to use. Closing the service..."); cError = -1; }else if (!bBarPrev) FirstBarNULL(); break; case 1 : Print("Command in line ", iLine, " cannot be recognized by the system..."); break; case 2 : Print("The system did not expect the content of the line ", iLine); break; default : Print("Error in line ", iLine); } return (cError == 0 ? !_StopFlag : false); } //+------------------------------------------------------------------+ };
于此我们就开始使用这个继承表。 所有这些项都继承自 C_FileTicks 类。 因此,我们实际上是在扩展 C_ConfigService 类本身的功能。 但并非仅有这件事,因为若您仔细观察,当我们需要从柱线加载数据之时,有一种非常特殊的状况。 为此,我们需要用到 C_FileBars 类。 那好,我们如 C_FileTicks 类一样使用相同的方法,其中我们需要从柱线文件中加载数据,以便将其转换为跳价。 我们在那边给出的解释在这里也适用。
从某种意义上说,该类负责翻译配置文件中包含的数据。 现在,我们所要做的就是在正确的点上定义事物,从而令它们指向,或者更确切地说,导致正确的状态。 我们将这样做,来确保正确填充数值,或正确加载数据。 这是在两个地方完成的。
第一处,我们指示哪种状态,或者更准确地说,我们正在捕获或调整的关键。 虽然它不是很复杂,我们在此拥有的是:一个事由列表,其将作为我们依据配置文件的后面几行来运作的关键。 在此,您只需注意该列表必须遵循一定的逻辑顺序这一事实。 否则,我们将在数值转换时遇到问题。 为了明白数据元素应该处于什么位置,只需查看 SetSymbolReplay 函数,看看每个数值于此的作用是啥。
第二处,负责解码回放/模拟配置文件中包含的数值,使之作为服务中所用的常量。 此处,我们要执行的操作几乎与以前相同,但这次数组中包含的每个数值指示的都是类中的变量名。 故此,您需要做的就是将变量名添加到配置文件列表当中。 然后,调用时我们加入它在列表中的位置。 我们以这种方式修改所需变量的数值。 如果您不明白我在说什么,别担心。 很快,我将展示一个如何添加新变量的真实示例。 在此之前,我们需要在这个特定点上定义一些额外的事由。
虽然一切看起来都非常美好,但我们还要看看最后的类。
Class C_Replay — 我什么都不懂...... 事由在哪里?!
这是回放/模拟服务实际应对的唯一类。 我们需要把该类当作一个函数库,但这个函数库的唯一功能是促进行为,类似于我们与实物市场或模拟账户互动时所发生的行为。 也就是,我们需要做的就是在该类中专门实现一些东西,如此 MetaTrader 5 平台即可执行所有的回放模拟,就如同它来自一个真实的服务器。
不过,如果您仔细查看类代码,并开始寻找某些内容,您也许会好奇:所要调用的变量、结构和函数在哪里? 我在任何地方都找不到它们! 这种认知可能发生在初期。 这恰恰意味着您对类之间的继承还不是很熟悉。 别担心,平静地研究代码,很快您就会开始明白这种继承是如何运作的。 最好现在就开始,因为很快我会向您展示一些更复杂的东西,而它也许会令您感到困惑。 其中之一是多态性。 这是非常有用的东西,但它也给那些不了解继承如何运作等相关问题的人带来了更多困惑。 因此,我建议您正确研习这段代码。
现在,我们把多态性的话题留到将来。 参阅下面的代码:
#property copyright "Daniel Jose" //+------------------------------------------------------------------+ #include "C_ConfigService.mqh" //+------------------------------------------------------------------+ class C_Replay : private C_ConfigService { private : int m_ReplayCount; long m_IdReplay; struct st01 { MqlRates Rate[1]; bool bNew; datetime memDT; int delay; }m_MountBar; //+------------------------------------------------------------------+ void AdjustPositionToReplay(const bool bViewBuider) { u_Interprocess Info; MqlRates Rate[def_BarsDiary]; int iPos, nCount; Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); if (Info.s_Infos.iPosShift == (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks)) return; iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1))); Rate[0].time = macroRemoveSec(m_Ticks.Info[iPos].time); if (iPos < m_ReplayCount) { CustomRatesDelete(def_SymbolReplay, Rate[0].time, LONG_MAX); if ((m_dtPrevLoading == 0) && (iPos == 0)) { m_ReplayCount = 0; Rate[m_ReplayCount].close = Rate[m_ReplayCount].open = Rate[m_ReplayCount].high = Rate[m_ReplayCount].low = m_Ticks.Info[iPos].last; Rate[m_ReplayCount].tick_volume = Rate[m_ReplayCount].real_volume = 0; CustomRatesUpdate(def_SymbolReplay, Rate, 1); }else { for(Rate[0].time -= 60; (m_ReplayCount > 0) && (Rate[0].time <= macroRemoveSec(m_Ticks.Info[m_ReplayCount].time)); m_ReplayCount--); m_ReplayCount++; } }else if (iPos > m_ReplayCount) { if (bViewBuider) { Info.s_Infos.isWait = true; GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value); }else { for(; Rate[0].time > m_Ticks.Info[m_ReplayCount].time; m_ReplayCount++); for (nCount = 0; m_Ticks.Rate[nCount].time < macroRemoveSec(m_Ticks.Info[iPos].time); nCount++); CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, nCount); } } for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag);) CreateBarInReplay(); Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); Info.s_Infos.isWait = false; GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value); } //+------------------------------------------------------------------+ inline void CreateBarInReplay(const bool bViewMetrics = false) { #define def_Rate m_MountBar.Rate[0] static ulong _mdt = 0; int i; if (m_MountBar.bNew = (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time))) { if (bViewMetrics) { _mdt = (_mdt > 0 ? GetTickCount64() - _mdt : _mdt); i = (int) (_mdt / 1000); Print(TimeToString(m_Ticks.Info[m_ReplayCount].time, TIME_SECONDS), " - Metrica: ", i / 60, ":", i % 60, ".", (_mdt % 1000)); _mdt = GetTickCount64(); } m_MountBar.memDT = macroRemoveSec(m_Ticks.Info[m_ReplayCount].time); def_Rate.real_volume = 0; def_Rate.tick_volume = 0; } def_Rate.close = m_Ticks.Info[m_ReplayCount].last; def_Rate.open = (m_MountBar.bNew ? def_Rate.close : def_Rate.open); def_Rate.high = (m_MountBar.bNew || (def_Rate.close > def_Rate.high) ? def_Rate.close : def_Rate.high); def_Rate.low = (m_MountBar.bNew || (def_Rate.close < def_Rate.low) ? def_Rate.close : def_Rate.low); def_Rate.real_volume += (long) m_Ticks.Info[m_ReplayCount].volume_real; def_Rate.tick_volume += (m_Ticks.Info[m_ReplayCount].volume_real > 0 ? 1 : 0); def_Rate.time = m_MountBar.memDT; m_MountBar.bNew = false; CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate, 1); m_ReplayCount++; #undef def_Rate } //+------------------------------------------------------------------+ public : //+------------------------------------------------------------------+ C_Replay(const string szFileConfig) { m_ReplayCount = 0; m_dtPrevLoading = 0; m_Ticks.nTicks = 0; m_PointsPerTick = 0; Print("************** Market Replay Service **************"); srand(GetTickCount()); GlobalVariableDel(def_GlobalVariableReplay); SymbolSelect(def_SymbolReplay, false); CustomSymbolDelete(def_SymbolReplay); CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay), _Symbol); CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX); CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX); SymbolSelect(def_SymbolReplay, true); m_IdReplay = (SetSymbolReplay(szFileConfig) ? 0 : -1); } //+------------------------------------------------------------------+ ~C_Replay() { 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); Print("Replay service completed..."); } //+------------------------------------------------------------------+ bool ViewReplay(ENUM_TIMEFRAMES arg1) { u_Interprocess info; if (m_IdReplay == -1) return false; if ((m_IdReplay = ChartFirst()) > 0) do { if (ChartSymbol(m_IdReplay) == def_SymbolReplay) { ChartClose(m_IdReplay); ChartRedraw(); } }while ((m_IdReplay = ChartNext(m_IdReplay)) > 0); Print("Wait for permission from [Market Replay] indicator to start replay ..."); info.u_Value.IdGraphic = m_IdReplay = ChartOpen(def_SymbolReplay, arg1); ChartApplyTemplate(m_IdReplay, "Market Replay.tpl"); ChartRedraw(m_IdReplay); GlobalVariableDel(def_GlobalVariableIdGraphics); GlobalVariableTemp(def_GlobalVariableIdGraphics); GlobalVariableSet(def_GlobalVariableIdGraphics, info.u_Value.df_Value); while ((!GlobalVariableCheck(def_GlobalVariableReplay)) && (!_StopFlag) && (ChartSymbol(m_IdReplay) != "")) Sleep(750); return ((!_StopFlag) && (ChartSymbol(m_IdReplay) != "")); } //+------------------------------------------------------------------+ bool LoopEventOnTime(const bool bViewBuider, const bool bViewMetrics) { u_Interprocess Info; int iPos, iTest; iTest = 0; while ((iTest == 0) && (!_StopFlag)) { iTest = (ChartSymbol(m_IdReplay) != "" ? iTest : -1); iTest = (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value) ? iTest : -1); iTest = (iTest == 0 ? (Info.s_Infos.isPlay ? 1 : iTest) : iTest); if (iTest == 0) Sleep(100); } if ((iTest < 0) || (_StopFlag)) return false; AdjustPositionToReplay(bViewBuider); m_MountBar.delay = 0; while ((m_ReplayCount < m_Ticks.nTicks) && (!_StopFlag)) { CreateBarInReplay(bViewMetrics); iPos = (int)(m_ReplayCount < m_Ticks.nTicks ? m_Ticks.Info[m_ReplayCount].time_msc - m_Ticks.Info[m_ReplayCount - 1].time_msc : 0); m_MountBar.delay += (iPos < 0 ? iPos + 1000 : iPos); if (m_MountBar.delay > 400) { if (ChartSymbol(m_IdReplay) == "") break; GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value); if (!Info.s_Infos.isPlay) return true; Info.s_Infos.iPosShift = (ushort)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks); GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value); Sleep(m_MountBar.delay - 20); m_MountBar.delay = 0; } } return (m_ReplayCount == m_Ticks.nTicks); } //+------------------------------------------------------------------+ }; //+------------------------------------------------------------------+ #undef macroRemoveSec #undef def_SymbolReplay //+------------------------------------------------------------------+
如您所见,这并没有什么不同寻常的。 不过,它比以前的版本庞大得多。 然而,我们设法做到了我们之前已实现的一切。 但我想强调的是,提到的所有关键点实际上都并非 C_Replay 类的一部分。 这些关键点均是继承的。 发生这种情况是因为我不希望从 C_Replay 类外部访问它们。 为了达成这一点,我们继承的东西都是私密的。 以这种方式,我们保证了继承信息的完整性。 这个类实际上只有两个函数可从外部访问,因为构造函数和析构函数并未被考虑在内。
但在我们讨论类的构造函数和析构函数之前,我们看看可以从类外部访问的两个函数。 在某个时刻,我曾决定只保留二者之一,但出于实际原因,我保留了这两个函数。 这是更简单的方式。 在之前的文章中,我们曾研究过 LoopEventOnTime 函数。 而且鉴于它在此处没有经过任何修改,故没有必要进行额外的解释。 我们可以跳过它,专注于经过修改的那个:ViewReplay。
ViewReplay 函数只有一处修改,即检查。 在此,我们检查类的构造函数是否能够成功初始化。 如果失败,该函数将返回一个数值,该值应导致回放服务终止。 与之前文章中的内容相比,这是唯一的修改。
后记
所有给出的修改,我建议您研究这些新素材,并将随附的代码与之前文章中讲述的其它代码进行比较。 在下一篇文章中,我们将开始一个完全不同、然相当有趣的话题。 期待与您再见!
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/11095