
开发回放系统 — 市场模拟(第 21 部分):外汇(II)
概述
在上一篇文章“开发回放系统 — 市场模拟(第 20 部分):外汇(I)”中,我们开始装配,或者更确切地说,调整回放/模拟系统。这样做是为了允许使用市场数据,例如外汇,以便至少能够进行市场回放。
为了开发这种潜能,有必要对系统进行一些修改和调整,但在本文中,我们不仅考虑到外汇市场。可以看出,我们已经能够涵盖广泛的可能市场类型,因为我们现在不仅可以看到最后的交易价格,还可以使用基于出价的显示系统,这在某些特定类型的资产或市场里很常见。
但这并非全部。我们还实现了一种方式,来防止回放/模拟系统在显示资产跳价数据时,在一段期间内没有交易的情况下被“锁定”。系统处于定时模式,并在一定时间后释放,从而能再次使用。我们目前没有遇到这样的问题,尽管新的挑战和问题仍然存在,等待解决,并因此进一步改进我们的回放/模拟系统。
解决配置文件的问题
我们从更加用户友好的配置文件开始。您也许已经注意到,它变得有些更加难以操控。我不希望用户在尝试设置回放/模拟时感到不安,如果图表完全留空,并且无法访问控制指标,那启动服务时就没有消息指示出错。
因此,在尝试使用回放/模拟服务时,可能会发生所示的类似情况,如图例 01:
图例 01:启动顺序失败的结果
此错误不是由于误用或误解了系统如何操作而造成的。之所以出现是因为在自定义资产中放置的所有数据都已被彻底删除。这可能是由于资产配置的更改,也可能是由于资产中存在的数据被破坏而发生的。
但无论如何,如果我们在加载 1-分钟柱线后加载跳价数据,这种失败就会更频繁地发生。在这种情况下,系统会明白它需要更改其显示价格的方式,并经此更改,所有柱线将被删除,如上一篇文章所示。
为了解决这个问题,我们必须在加载以前的柱线之前首先声明加载跳价。这解决了问题,但同时迫使用户遵循配置文件中的某些结构,就个人而言,这对我来说没有多大意义。原因是,通过设计一个负责分析和执行配置文件中内容的程序,我们可以允许用户按任何顺序声明他需要的元素,只要遵循某些语法即可。
基本上,只有一种途径可以解决这个问题。我们创建这条路径的方式令我们产生细微的变化,但基本上它总是相同的。但这是从编程的角度来看的。简而言之,我们需要读取整个配置文件,然后按照它们应该可用的顺序访问必要的元素,如此一切就能完美和谐地工作。
此技术的一种变体是使用 FileSeek 和 FileTell 函数,以便能够按所需的顺序读取。我们可以只访问一次文件,然后直接在内存中访问所需的元素,或者我们可以逐片读取文件,然后执行与内存中相同的工作。但还有另一种选项。
个人而言,我更喜欢将其完全加载到内存中,然后分批读取它,如此我就拥有加载它所需的特定序列,而不会得到如图例 01 所示结果。故此,我们将使用以下技术:我们把配置文件加载到内存当中,然后加载数据,如此资产即可用于回放或模拟。
为了打造在配置文件中可编程序列交错的能力,我们必须首先创建一组变量。但我不会从头开始编写所有内容。代之,我将利用 MQL5 中已经包含的函数。
更准确地说,我们将使用 MQL5 的标准库。好吧,我们将仅用到该标准库中提供的一小部分。该方式的一大优点是它所包含的功能已经过全面测试。如果在将来的某个时候,您对程序进行了改进,无论它们怎样,系统都会自动接受它们。以这种方式,编程过程就变得容易得多,因为我们在测试和优化阶段需要花费的时间更少。
现在我们看看我们需要什么。如下代码所示:
#include "C_FileBars.mqh" #include "C_FileTicks.mqh" //+------------------------------------------------------------------+ #include <Arrays\ArrayString.mqh> //+------------------------------------------------------------------+ class C_ConfigService : protected C_FileTicks { // ... Internal class code .... }
此处,我们将用到 MQL5 中提供的包含文件。对于我们的目的来说,这已经足够了。
在此之后,我们将需要类的全局私密变量:
private : //+------------------------------------------------------------------+ enum eTranscriptionDefine {Transcription_INFO, Transcription_DEFINE}; string m_szPath; struct st001 { CArrayString *pTicksToReplay, *pBarsToTicks, *pTicksToBars, *pBarsToPrev; }m_GlPrivate;
这些变量位于结构内部,实际上是指向我们将要用到的类的指针。重点要注意的是,操控指针与操控变量不同,因为指针是指向内存中某处(更准确地说是地址)的结构,而变量仅包含值。
整个故事中的一个重要细节是,默认情况下,为了令许多程序员(尤其是那些刚刚开始学习编程的人)的生活更轻松,MQL5 实际上并未采用与 C/C++ 完全相同的指针概念。那些曾用 C/C++ 编程的人都知道指针是多么实用,但指针同时又是危险和令人困惑的。不过,在 MQL5 中,开发人员试图消除使用指针时相关的许多混淆和危险。出于实际目的,应当注意的是,我们实际上将用指针来访问 CArrayString 类。
如果您想了解更多关于 CArrayString 类的方法,请参考其文档。只需单击 CArrayString 即查看已供我们所用的内容。它非常适合我们的目的。
鉴于我们要用指针来访问对象,因此需要先初始化它们,这就是我们在下面的代码中所做的。
bool SetSymbolReplay(const string szFileConfig) { // ... Internal code ... m_GlPrivate.pTicksToReplay = m_GlPrivate.pTicksToBars = m_GlPrivate.pBarsToTicks = m_GlPrivate.pBarsToPrev = NULL; // ... The rest of the code ... }
大概程序员在使用指针时所犯最大错误在于,尤其是在 C/C++ 中,就是他们试图在指针初始化数据之前使用它。指针的危险在于,它们在初始化之前永远不会指向我们所用的内存区域。试图在一个不熟悉的内存区域读取,尤其是写入,很可能意味着我们在危险部分写入或读取错误的信息。在这种情况下,系统往往会崩溃,导致各种问题。因此,在程序中使用指针时要非常小心。
考虑到所有预防措施,我们来看看它是如何进出回放/模拟系统配置函数的。整个函数如下代码所示:
bool SetSymbolReplay(const string szFileConfig) { #define macroFileName ((m_szPath != NULL ? m_szPath + "\\" : "") + szInfo) int file, iLine; char cError, cStage; string szInfo; bool bBarsPrev; if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) { Print("Failed to open the configuration file [", szFileConfig, "]. Closing the service..."); return false; } Print("Loading ticks for replay. Please wait...."); ArrayResize(m_Ticks.Rate, def_BarsDiary); m_Ticks.nRate = -1; m_Ticks.Rate[0].time = 0; iLine = 1; cError = cStage = 0; bBarsPrev = false; m_GlPrivate.pTicksToReplay = m_GlPrivate.pTicksToBars = m_GlPrivate.pBarsToTicks = m_GlPrivate.pBarsToPrev = NULL; 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: if (m_GlPrivate.pBarsToPrev == NULL) m_GlPrivate.pBarsToPrev = new CArrayString(); (*m_GlPrivate.pBarsToPrev).Add(macroFileName); pFileBars = new C_FileBars(macroFileName); if ((m_dtPrevLoading = (*pFileBars).LoadPreView()) == 0) cError = 3; else bBarsPrev = true; delete pFileBars; break; case 2: if (m_GlPrivate.pTicksToReplay == NULL) m_GlPrivate.pTicksToReplay = new CArrayString(); (*m_GlPrivate.pTicksToReplay).Add(macroFileName); if (LoadTicks(macroFileName) == 0) cError = 4; break; case 3: if (m_GlPrivate.pTicksToBars == NULL) m_GlPrivate.pTicksToBars = new CArrayString(); (*m_GlPrivate.pTicksToBars).Add(macroFileName); if ((m_dtPrevLoading = LoadTicks(macroFileName, false)) == 0) cError = 5; else bBarsPrev = true; break; case 4: if (m_GlPrivate.pBarsToTicks == NULL) m_GlPrivate.pBarsToTicks = new CArrayString(); (*m_GlPrivate.pBarsToTicks).Add(macroFileName); if (!BarsToTicks(macroFileName)) cError = 6; break; case 5: if (!Configs(szInfo)) cError = 7; break; } break; }; iLine += (cError > 0 ? 0 : 1); } FileClose(file); Cmd_TicksToReplay(cError); Cmd_BarsToTicks(cError); bBarsPrev = (Cmd_TicksToBars(cError) ? true : bBarsPrev); bBarsPrev = (Cmd_BarsToPrev(cError) ? true : bBarsPrev); switch(cError) { case 0: if (m_Ticks.nTicks <= 0) { Print("No ticks to use. Closing the service..."); cError = -1; }else if (!bBarsPrev) FirstBarNULL(); break; case 1 : Print("Command in line ", iLine, " could not be recognized by the system..."); break; case 2 : Print("The contents of the line are unexpected for the system ", iLine); break; default : Print("Error occurred while accessing one of the specified files..."); } return (cError == 0 ? !_StopFlag : false); #undef macroFileName }
删除了前面代码中划掉的部分,因为我们在这里不再需要它们。它们被放置在另一处。但在新位置查看它们之前,要注意代码在这个阶段是如何工作的。
您可以看到这里有相当一些重复的代码,即使我们总是会访问不同的指针。
该段代码旨在由 new 运算符创建一片内存区域维持类的存在。同时,该运算符初始化类。由于没有初始化构造函数,因此在所有情况下都使用默认值创建和初始化该类。
类经初始化后,指针值将不再为 NULL。指针将仅引用第一个元素,如果我们在此不讨论数组,它看起来像一个列表。现在指针指向内存中的正确位置,我们可以调用类方法之一附加一个字符串。
注意,这几乎是透明的。我们不需要知道我们添加的信息在类中的位置和组织形式。真正重要的是,每次我们调用该方法时,类都会为我们存储数据。
请注意,我们从未引用过配置文件中将列出的文件。实时读取时所要做的唯一事情就是设置基本操作信息。读取数据,无论是直方图还是跳价数据,都将在稍后完成。
一旦系统读取配置文件完毕,我们就转入下一步。目前,您在添加或使用该系统时遇到绝对应该考虑的主要问题。故此,请密切注意我将要解释的内容,因为它可能对您的特定系统很重要。对我们来说,这并不重要。
此处我们有 4 个函数,它们出现的顺序会影响最终结果。现在,不要担心每个单独函数的代码。在此有必要考虑它们在代码中的出现顺序,因为取决于它们的出现顺序,系统将以一种或另一种方式显示图形。这也许令您感到困惑,但我们尝试了解情况将如何发展。我们需要知道哪个顺序最合适,具体取决于我们将要采用的数据库类型。
按照代码中所用的顺序,系统执行以下操作。首先,它读取实际跳价数据。接下来,它使用建模过程将数据转换为跳价。然后它创建柱线,坚持从跳价到柱线的初始转换,最后加载 1-分钟柱线。
数据的读取顺序完全相同。如果我们想改变这个顺序,我们就不得不改变这些函数的调用顺序。不过,在读取我们建模时所需跳价之前,您不应该读取以前的柱线。这是因为事实上,读取回放中所用的跳价,或读取在测试器中所用的柱线,结果都是在图表上删除所有内容。这与上一篇文章中讨论的细节有关。但是,使用或读取的顺序是在系统中判定的,而非在配置文件中确定的,故令在报价之前声明柱线成为可能,同时允许系统应对可能的问题。
然而,事无单纯。这样做的话,我们允许系统判定事件的顺序,或者更确切地说,是读取事件的顺序。然而,当我们把所有数据读取到内存中,然后解释它时,这种包揽所有事情的方式,令系统很难报告读取失败的确切行。我们仍然拥有文件数据的名称,但无法访问。
为了解决系统无法报告发生错误的确切行造成的不便,我们只得针对代码进行一个较小且相当简单的修改。记住:我们不想降低系统的功能。取而代之,我们希望增加和扩展它,以便涵盖尽可能多的情况。
在我们实现解决方案之前,我希望您明白我们做出此决定的原因。
为了解决这个定义问题,在加载阶段,我们必须同时存储文件名称,以及它的声明所在行。这个解决方案其实很简单。我们需要做的就是使用 CArrayInt 类声明另一组变量。该类将存储与每个文件名相对应的字符串。
虽然表面看,这似乎是一个相当不错的解决方案,因为我们将使用标准库,但它迫使我们添加更多的所需变量,如此会比自行开发解决方案麻烦一点。我们将采用标准库中所用类相同的原理,但能够同时处理大量数据。
您也许会认为,通过这种方式,我们令项目复杂化,因为我们可以使用标准库中已测试和实现的解决方案,就个人而言,在许多情况下我都同意这一想法。但在此这并无多大意义,因为我们不需要标准库中提供的所有这些方法。您只需要其中两个。因此,实现成本弥补了额外的劳动力成本。由此,我们的项目中出现了一个新类:C_Array。
其完整代码如下所示:
#property copyright "Daniel Jose" //+------------------------------------------------------------------+ class C_Array { private : //+------------------------------------------------------------------+ string m_Info[]; int m_nLine[]; int m_maxIndex; //+------------------------------------------------------------------+ public : C_Array() :m_maxIndex(0) {}; //+------------------------------------------------------------------+ ~C_Array() { if (m_maxIndex > 0) { ArrayResize(m_nLine, 0); ArrayResize(m_Info, 0); } }; //+------------------------------------------------------------------+ bool Add(const string Info, const int nLine) { m_maxIndex++; ArrayResize(m_Info, m_maxIndex); ArrayResize(m_nLine, m_maxIndex); m_Info[m_maxIndex - 1] = Info; m_nLine[m_maxIndex - 1] = nLine; return true; } //+------------------------------------------------------------------+ string At(const int Index, int &nLine) const { if (Index >= m_maxIndex) { nLine = -1; return ""; } nLine = m_nLine[Index]; return m_Info[Index]; } //+------------------------------------------------------------------+ }; //+------------------------------------------------------------------+
注意它有多么简单和紧凑,但同时它完美契合我们需要的目的。使用这个类,我们能够存储包含回放或模拟所需信息的行和文件名称。所有这些数据都在配置文件中声明。因此,通过将这个类添加到我们的项目中,我们保持了相同级别的功能,或者更确切地说,提升了它,因为现在我们还可以回放来自于类似外汇市场的数据。
需要对负责设置回放/模拟的类代码进行一些修改。这些修改从以下代码开始:
#property copyright "Daniel Jose" //+------------------------------------------------------------------+ #include "C_FileBars.mqh" #include "C_FileTicks.mqh" #include "C_Array.mqh" //+------------------------------------------------------------------+ class C_ConfigService : protected C_FileTicks { protected: //+------------------------------------------------------------------+ datetime m_dtPrevLoading; int m_ReplayCount; //+------------------------------------------------------------------+ inline void FirstBarNULL(void) { MqlRates rate[1]; for(int c0 = 0; m_Ticks.Info[c0].volume_real == 0; c0++) rate[0].close = m_Ticks.Info[c0].last; rate[0].open = rate[0].high = rate[0].low = rate[0].close; rate[0].tick_volume = 0; rate[0].real_volume = 0; rate[0].time = m_Ticks.Info[0].time - 60; CustomRatesUpdate(def_SymbolReplay, rate); m_ReplayCount = 0; } //+------------------------------------------------------------------+ private : //+------------------------------------------------------------------+ enum eTranscriptionDefine {Transcription_INFO, Transcription_DEFINE}; string m_szPath; struct st001 { C_Array *pTicksToReplay, *pBarsToTicks, *pTicksToBars, *pBarsToPrev; int Line; }m_GlPrivate;
只需将这些项目添加到文件之中。另外,我们需要对配置函数进行更正。它看起来像这样:
bool SetSymbolReplay(const string szFileConfig) { #define macroFileName ((m_szPath != NULL ? m_szPath + "\\" : "") + szInfo) int file; char cError, cStage; string szInfo; bool bBarsPrev; if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) { Print("Failed to open configuration file [", szFileConfig, "]. Closing the service..."); return false; } Print("Loading ticks for replay. Please wait...."); ArrayResize(m_Ticks.Rate, def_BarsDiary); m_Ticks.nRate = -1; m_Ticks.Rate[0].time = 0; cError = cStage = 0; bBarsPrev = false; m_GlPrivate.Line = 1; m_GlPrivate.pTicksToReplay = m_GlPrivate.pTicksToBars = m_GlPrivate.pBarsToTicks = m_GlPrivate.pBarsToPrev = NULL; 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: if (m_GlPrivate.pBarsToPrev == NULL) m_GlPrivate.pBarsToPrev = new C_Array(); (*m_GlPrivate.pBarsToPrev).Add(macroFileName, m_GlPrivate.Line); break; case 2: if (m_GlPrivate.pTicksToReplay == NULL) m_GlPrivate.pTicksToReplay = new C_Array(); (*m_GlPrivate.pTicksToReplay).Add(macroFileName, m_GlPrivate.Line); break; case 3: if (m_GlPrivate.pTicksToBars == NULL) m_GlPrivate.pTicksToBars = new C_Array(); (*m_GlPrivate.pTicksToBars).Add(macroFileName, m_GlPrivate.Line); break; case 4: if (m_GlPrivate.pBarsToTicks == NULL) m_GlPrivate.pBarsToTicks = new C_Array(); (*m_GlPrivate.pBarsToTicks).Add(macroFileName, m_GlPrivate.Line); break; case 5: if (!Configs(szInfo)) cError = 7; break; } break; }; m_GlPrivate.Line += (cError > 0 ? 0 : 1); } FileClose(file); Cmd_TicksToReplay(cError); Cmd_BarsToTicks(cError); bBarsPrev = (Cmd_TicksToBars(cError) ? true : bBarsPrev); bBarsPrev = (Cmd_BarsToPrev(cError) ? true : bBarsPrev); switch(cError) { case 0: if (m_Ticks.nTicks <= 0) { Print("No ticks to use. Closing the service..."); cError = -1; }else if (!bBarsPrev) FirstBarNULL(); break; case 1 : Print("Command in line ", m_GlPrivate.Line, " could not be recognized by the system..."); break; case 2 : Print("The contents of the line is unexpected for the system: ", m_GlPrivate.Line); break; default : Print("Access error occurred in the files indicated in line: ", m_GlPrivate.Line); } return (cError == 0 ? !_StopFlag : false); #undef macroFileName }
现在我们可以看到,我们可以告诉用户发生了错误,以及它发生在哪一行。这样做的成本几乎为零,因为如您所见,函数与以前几乎相同,只是添加了一个新元素。考虑到如果我们使用标准库,我们需要做如此多的工作,现在这样很值了。请正确理解我的意思:我并不是说您不该使用标准库,我只是在表明有时您需要创建自己的解决方案,因为在某些情况下,利用标准库的代价重于自研。
现在,我们可以查看添加到系统中的新函数,即上面显示的四个函数。它们都非常简单,代码如下所示,且由于它们都遵循相同的原理工作,我会马上解释它们,如此就不会令过程过于繁琐。
inline void Cmd_TicksToReplay(char &cError) { string szInfo; if (m_GlPrivate.pTicksToReplay != NULL) { for (int c0 = 0; (c0 < INT_MAX) && (cError == 0); c0++) { if ((szInfo = (*m_GlPrivate.pTicksToReplay).At(c0, m_GlPrivate.Line)) == "") break; if (LoadTicks(szInfo) == 0) cError = 4; } delete m_GlPrivate.pTicksToReplay; } } //+------------------------------------------------------------------+ inline void Cmd_BarsToTicks(char &cError) { string szInfo; if (m_GlPrivate.pBarsToTicks != NULL) { for (int c0 = 0; (c0 < INT_MAX) && (cError == 0); c0++) { if ((szInfo = (*m_GlPrivate.pBarsToTicks).At(c0, m_GlPrivate.Line)) == "") break; if (!BarsToTicks(szInfo)) cError = 6; } delete m_GlPrivate.pBarsToTicks; } } //+------------------------------------------------------------------+ inline bool Cmd_TicksToBars(char &cError) { bool bBarsPrev = false; string szInfo; if (m_GlPrivate.pTicksToBars != NULL) { for (int c0 = 0; (c0 < INT_MAX) && (cError == 0); c0++) { if ((szInfo = (*m_GlPrivate.pTicksToBars).At(c0, m_GlPrivate.Line)) == "") break; if ((m_dtPrevLoading = LoadTicks(szInfo, false)) == 0) cError = 5; else bBarsPrev = true; } delete m_GlPrivate.pTicksToBars; } return bBarsPrev; } //+------------------------------------------------------------------+ inline bool Cmd_BarsToPrev(char &cError) { bool bBarsPrev = false; string szInfo; C_FileBars *pFileBars; if (m_GlPrivate.pBarsToPrev != NULL) { for (int c0 = 0; (c0 < INT_MAX) && (cError == 0); c0++) { if ((szInfo = (*m_GlPrivate.pBarsToPrev).At(c0, m_GlPrivate.Line)) == "") break; pFileBars = new C_FileBars(szInfo); if ((m_dtPrevLoading = (*pFileBars).LoadPreView()) == 0) cError = 3; else bBarsPrev = true; delete pFileBars; } delete m_GlPrivate.pBarsToPrev; } return bBarsPrev; } //+------------------------------------------------------------------+
但等等......仔细看看上面的所有四个函数......有很多重复的代码...... 当我们有太多重复的代码时,我们该怎么办?我们尝试将函数降解到更基本的级别,以便实现代码重用。我知道这对我来说可能听起来很笨或粗心,但在这一系列文章中,我不仅展示了如何从头开始创建程序,还展示了优秀的程序员如何通过减少维护工作量来改进他们的程序,并创建它们。
这不是关于您有多少年的编程经验,而事实在于,即使您刚刚开始进入编程领域,也能明白应当通常改进函数,并减少代码,从而令开发更容易、更快捷。出于可重用性而优化代码表面看似乎是在浪费时间。然而,在贯穿整个开发过程中它都能帮助我们,因为当我们需要改进某些东西时,我们在多处需要重复做的事情就更少。在编程时问自己这个问题:我可以减少执行此任务的代码量吗?
这正是下面的函数的作用:它替换了前面的 4 个函数,如此如果我们需要改进系统,我们只需要修改一个函数,而不是四个函数。
inline bool CMD_Array(char &cError, eWhatExec e1) { bool bBarsPrev = false; string szInfo; C_FileBars *pFileBars; C_Array *ptr = NULL; switch (e1) { case eTickReplay: ptr = m_GlPrivate.pTicksToReplay; break; case eTickToBar : ptr = m_GlPrivate.pTicksToBars; break; case eBarToTick : ptr = m_GlPrivate.pBarsToTicks; break; case eBarPrev : ptr = m_GlPrivate.pBarsToPrev; break; } if (ptr != NULL) { for (int c0 = 0; (c0 < INT_MAX) && (cError == 0); c0++) { if ((szInfo = ptr.At(c0, m_GlPrivate.Line)) == "") break; switch (e1) { case eTickReplay: if (LoadTicks(szInfo) == 0) cError = 4; break; case eTickToBar : if ((m_dtPrevLoading = LoadTicks(szInfo, false)) == 0) cError = 5; else bBarsPrev = true; break; case eBarToTick : if (!BarsToTicks(szInfo)) cError = 6; break; case eBarPrev : pFileBars = new C_FileBars(szInfo); if ((m_dtPrevLoading = (*pFileBars).LoadPreView()) == 0) cError = 3; else bBarsPrev = true; delete pFileBars; break; } } delete ptr; } return bBarsPrev; }
重点要注意的是,这个函数实际上分为两个部分,尽管它位于同一个函数之中。在第一段中,我们初始化一个指针,该指针用来访问读取配置文件时存储的数据。这部分非常简单,我认为任何人都不难理解它。此后,我们转入第二部分,我们将在其中读取用户指定文件的内容。此处,每个指针都是用来读取存储在内存中的内容,并完成其工作。最后,指针被销毁,释放所用的内存。
但在我们回到设置函数之前,我想向您展示,用此系统做更多事情。在本主题的开头,我说过您需要编译一个程序,思考读取将如何发生,但是在文章的工作过程中,我思考了有关之处,并决定可以扩大范围,这样您就不需要不断地重新编译程序了。
这个思路是首先加载模拟数据,然后在需要时加载以前的柱线。但我们有一个问题:将在回放/模拟中使用的跳价和之前的柱线都可以来自 “Tick” 或 “Bar” 类型的文件,但我们需要以某种方式指出这一点。故此,我建议利用一个用户可定义变量。我们非常特别地保持事情的简单性,并令事情从长远看可行。为此,我们使用下表格:
值 | 读取模式 |
---|---|
1 | 跳价 - 跳价模式 |
2 | 跳价 - 柱线模式 |
3 | 柱线 - 跳价模式 |
4 | 柱线 - 柱线模式 |
表格 01 - 内部读取模型的数据
上表展示了读取将如何发生。我们总是从读取回放或模拟中所用数据开始,然后读取恢复以前图表的数据值。然后,如果用户报告的值为 1,系统将按如下方式工作:实际跳文件 - 柱线文件转换为跳价 - 跳价文件转换为以前数据 - 以前柱线的文件。这将是选项 1,但假设由于某种原因我们想要修改系统,如此即可使用相同的数据库,但执行不同类型的分析。在这种情况下,您可以报告特定值,例如值 4,系统将使用相同的数据库,但结果会略有不同,因为读取将按以下顺序完成: 柱线文件转换为跳价 - 实际调价文件 - 以前根柱的文件 - 跳价文件转换为以前数据... 如果我们这样尝试,我们将看到指标,如平均或其它,即使使用相同的数据库,从一种模式到另一种模式也会产生微小的变化。
这种实现需要最少的编程工作量,所以我认为提供这样的资源是有意义的。
现在我们来看看如何实现这样的系统。首先,我们在类中添加一个全局私密变量,以便我们可以继续工作。
class C_ConfigService : protected C_FileTicks { protected: //+------------------------------------------------------------------+ datetime m_dtPrevLoading; int m_ReplayCount, m_ModelLoading;
此变量将存储模型。如果用户没有在配置文件中定义它,我们将为其设置一个初始值。
C_ConfigService() :m_szPath(NULL), m_ModelLoading(1) { }
换言之,默认情况下,我们将始终运行并采用跳价 - 跳价模式。此后,您需要给用户指定所用值的机会。这也很简单:
inline bool Configs(const string szInfo) { const string szList[] = { "PATH", "POINTSPERTICK", "VALUEPERPOINTS", "VOLUMEMINIMAL", "LOADMODEL" }; 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_szPath = szRet[1]; return true; case 1: CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, StringToDouble(szRet[1])); return true; case 2: CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, StringToDouble(szRet[1])); return true; case 3: CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, StringToDouble(szRet[1])); return true; case 4: m_ModelLoading = StringInit(szRet[1]); m_ModelLoading = ((m_ModelLoading < 1) && (m_ModelLoading > 4) ? 1 : m_ModelLoading); return true; } Print("Variable >>", szRet[0], "<< undefined."); }else Print("Set-up configuration >>", szInfo, "<< invalid."); return false; }
在此,我们指定用户操控变量时将用的名称,并允许我们定义所用值,如表格 01 所示。这有一个小问题:我们需要确保该值在系统预期的限制范围内。如果用户指定的值与预期值不同,并不会发生错误,但系统会开始采用默认值。
所有这些完成后,我们可以看看最终的加载和设置函数是什么样子的。它如下所示:
bool SetSymbolReplay(const string szFileConfig) { #define macroFileName ((m_szPath != NULL ? m_szPath + "\\" : "") + szInfo) int file; char cError, cStage; string szInfo; bool bBarsPrev; if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) { Print("Failed to open configuration file [", szFileConfig, "]. Closing the service..."); return false; } Print("Loading ticks for replay. Please wait...."); ArrayResize(m_Ticks.Rate, def_BarsDiary); m_Ticks.nRate = -1; m_Ticks.Rate[0].time = 0; cError = cStage = 0; bBarsPrev = false; m_GlPrivate.Line = 1; m_GlPrivate.pTicksToReplay = m_GlPrivate.pTicksToBars = m_GlPrivate.pBarsToTicks = m_GlPrivate.pBarsToPrev = NULL; 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: if (m_GlPrivate.pBarsToPrev == NULL) m_GlPrivate.pBarsToPrev = new C_Array(); (*m_GlPrivate.pBarsToPrev).Add(macroFileName, m_GlPrivate.Line); break; case 2: if (m_GlPrivate.pTicksToReplay == NULL) m_GlPrivate.pTicksToReplay = new C_Array(); (*m_GlPrivate.pTicksToReplay).Add(macroFileName, m_GlPrivate.Line); break; case 3: if (m_GlPrivate.pTicksToBars == NULL) m_GlPrivate.pTicksToBars = new C_Array(); (*m_GlPrivate.pTicksToBars).Add(macroFileName, m_GlPrivate.Line); break; case 4: if (m_GlPrivate.pBarsToTicks == NULL) m_GlPrivate.pBarsToTicks = new C_Array(); (*m_GlPrivate.pBarsToTicks).Add(macroFileName, m_GlPrivate.Line); break; case 5: if (!Configs(szInfo)) cError = 7; break; } break; }; m_GlPrivate.Line += (cError > 0 ? 0 : 1); } FileClose(file); CMD_Array(cError, (m_ModelLoading <= 2 ? eTickReplay : eBarToTick)); CMD_Array(cError, (m_ModelLoading <= 2 ? eBarToTick : eTickReplay)); bBarsPrev = (CMD_Array(cError, ((m_ModelLoading & 1) == 1 ? eTickToBar : eBarPrev)) ? true : bBarsPrev); bBarsPrev = (CMD_Array(cError, ((m_ModelLoading & 1) == 1 ? eBarPrev : eTickToBar)) ? true : bBarsPrev); switch(cError) { case 0: if (m_Ticks.nTicks <= 0) { Print("No ticks to use. Closing the service..."); cError = -1; }else if (!bBarsPrev) FirstBarNULL(); break; case 1 : Print("Command in line ", m_GlPrivate.Line, " could not be recognized by the system..."); break; case 2 : Print("The contents of the line is unexpected for the system: ", m_GlPrivate.Line); break; default : Print("Access error occurred in the files indicated in line: ", m_GlPrivate.Line); } return (cError == 0 ? !_StopFlag : false); #undef macroFileName }
现在我们有了一个小型系统,允许我们选择加载的模型。不过,请记住,我们开始总是从加载将在回放或模拟中所用的内容,然后加载以前柱线的数据。
后记
配置文件的这一阶段工作到此结束。现在(至少对于现在)用户将能够指定这个早期阶段所需的一切。
这篇文章花了相当多的时间,但在我看来,这是值得的,因为现在我们暂时不用担心配置文件的问题了。无论它是如何构建的,它都将始终按预期工作。
在下一篇文章中,我们将继续调整回放/模拟系统,以进一步涵盖外汇和类似市场,因为股票市场已经处于更高级的阶段。我将专注于外汇市场,如此即可令系统能按股票市场一样的方式工作。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/11153

