English Русский Español Deutsch 日本語 Português
preview
开发回放系统 — 市场模拟(第 21 部分):外汇(II)

开发回放系统 — 市场模拟(第 21 部分):外汇(II)

MetaTrader 5测试者 | 28 三月 2024, 09:40
313 0
Daniel Jose
Daniel Jose

概述

在上一篇文章“开发回放系统 — 市场模拟(第 20 部分):外汇(I)”中,我们开始装配,或者更确切地说,调整回放/模拟系统。这样做是为了允许使用市场数据,例如外汇,以便至少能够进行市场回放。

为了开发这种潜能,有必要对系统进行一些修改和调整,但在本文中,我们不仅考虑到外汇市场。可以看出,我们已经能够涵盖广泛的可能市场类型,因为我们现在不仅可以看到最后的交易价格,还可以使用基于出价的显示系统,这在某些特定类型的资产或市场里很常见。

但这并非全部。我们还实现了一种方式,来防止回放/模拟系统在显示资产跳价数据时,在一段期间内没有交易的情况下被“锁定”。系统处于定时模式,并在一定时间后释放,从而能再次使用。我们目前没有遇到这样的问题,尽管新的挑战和问题仍然存在,等待解决,并因此进一步改进我们的回放/模拟系统。


解决配置文件的问题

我们从更加用户友好的配置文件开始。您也许已经注意到,它变得有些更加难以操控。我不希望用户在尝试设置回放/模拟时感到不安,如果图表完全留空,并且无法访问控制指标,那启动服务时就没有消息指示出错。

因此,在尝试使用回放/模拟服务时,可能会发生所示的类似情况,如图例 01:

图例 01

图例 01:启动顺序失败的结果


此错误不是由于误用或误解了系统如何操作而造成的。之所以出现是因为在自定义资产中放置的所有数据都已被彻底删除。这可能是由于资产配置的更改,也可能是由于资产中存在的数据被破坏而发生的。

但无论如何,如果我们在加载 1-分钟柱线后加载跳价数据,这种失败就会更频繁地发生。在这种情况下,系统会明白它需要更改其显示价格的方式,并经此更改,所有柱线将被删除,如上一篇文章所示。

为了解决这个问题,我们必须在加载以前的柱线之前首先声明加载跳价。这解决了问题,但同时迫使用户遵循配置文件中的某些结构,就个人而言,这对我来说没有多大意义。原因是,通过设计一个负责分析和执行配置文件中内容的程序,我们可以允许用户按任何顺序声明他需要的元素,只要遵循某些语法即可。

基本上,只有一种途径可以解决这个问题。我们创建这条路径的方式令我们产生细微的变化,但基本上它总是相同的。但这是从编程的角度来看的。简而言之,我们需要读取整个配置文件,然后按照它们应该可用的顺序访问必要的元素,如此一切就能完美和谐地工作。

此技术的一种变体是使用 FileSeekFileTell 函数,以便能够按所需的顺序读取。我们可以只访问一次文件,然后直接在内存中访问所需的元素,或者我们可以逐片读取文件,然后执行与内存中相同的工作。但还有另一种选项。

个人而言,我更喜欢将其完全加载到内存中,然后分批读取它,如此我就拥有加载它所需的特定序列,而不会得到如图例 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

附加的文件 |
Market_Replay_ev221.zip (14387.15 KB)
掌握ONNX:MQL5交易者的游戏规则改变者 掌握ONNX:MQL5交易者的游戏规则改变者
深入ONNX的世界,这是一种用于交换机器学习模型的强大的开放标准格式。了解利用ONNX如何彻底改变MQL5中的算法交易,使交易员能够无缝集成尖端的人工智能模型,并将其策略提升到新的高度。揭开跨平台兼容性的秘密,学习如何在您的MQL5交易活动中释放ONNX的全部潜力。通过这篇掌握ONNX的全面指南提升您的交易游戏
从外汇市场的季节性获益 从外汇市场的季节性获益
我们都熟悉季节性的概念,例如,我们都习惯于冬季新鲜蔬菜价格的上涨或严重霜冻期间燃料价格的上涨,但很少有人知道外汇市场也存在类似的模式。
将您自己的LLM集成到EA中(第1部分):硬件和环境部署 将您自己的LLM集成到EA中(第1部分):硬件和环境部署
随着人工智能的快速发展,大型语言模型(LLM)成为人工智能的重要组成部分,因此我们应该思考如何将强大的语言模型集成到我们的算法交易中。对大多数人来说,很难根据他们的需求对这些强大的模型进行微调,在本地部署,然后将其应用于算法交易。本系列文章将采取循序渐进的方法来实现这一目标。
神经网络变得轻松(第五十部分):软性扮演者-评价者(模型优化) 神经网络变得轻松(第五十部分):软性扮演者-评价者(模型优化)
在上一篇文章中,我们实现了软性扮演者-评论者算法,但未能训练出一个可盈利的模型。在此,我们将优化先前创建的模型,以期获得所需的结果。