English Русский 中文 Español Deutsch 日本語
preview
Desenvolvendo um sistema de Replay - Simulação de mercado ( Parte 21):  FOREX (II)

Desenvolvendo um sistema de Replay - Simulação de mercado ( Parte 21): FOREX (II)

MetaTrader 5Testador | 24 julho 2023, 16:12
344 0
Daniel Jose
Daniel Jose

Introdução

No artigo anterior Desenvolvendo um sistema de Replay - Simulação de mercado (Parte 20): FOREX (I), começamos a montar, ou melhor dizendo, a adaptar o sistema de replay/simulação. Isto de maneira, que fosse possível utilizar dados de mercados, como o do FOREX, de forma a podermos fazer pelo menos um replay deste mercado.

Lá foi preciso fazer diversas mudanças e alterações no sistema, de forma a promover esta capacidade, mas não é apenas o mercado de forex que começamos a cobrir naquele artigo. Lá você pode ver que conseguimos cobrir, uma ampla gama de possíveis tipos de mercados, já que agora podemos além do preço de último negócio. Também utilizar o sistema de plotagem baseados no preço de BID, que são bem comuns em alguns tipos específicos de ativos ou mercados.

Mas não somente isto, também foi implementado uma forma de fazer, com que o sistema de replay / simulação, não ficasse " travado " quando os dados de tickets, indicavam que o ativo não tinha negócios em um dado período. Ficando assim em modo temporizado, até que todo aquele tempo fosse consumido, liberando o sistema para ser novamente utilizado. Agora não temos mais este tipo de problema. Em compensação novos desafios e problemas, ainda se mantiveram ali. Aguardando um momento para serem corrigidos e assim deixar o nosso sistema de replay / simulador ainda melhor.


Resolvendo o problema do arquivo de configuração

Mas vamos começar deixando o arquivo de configuração mais livre. Se você notou, ele ficou meio que chato de ser trabalhado. Não quero deixar o usuário enfurecido, ao tentar configurar um replay ou simulação. E ao iniciar o serviço, não ser reportado nenhum tipo de erro, mas o gráfico ficar totalmente em branco, sem que o indicador de controle possa ser acessado.

Como resultado, numa tentativa de usar o serviço de replay / simulador, você ter algo parecido com o conteúdo visto na figura 01:

Figura 01

Figura 01 : Resultado de uma falha na sequencia de carregamento


Esta falha, não é causada por conta de mal uso, ou não compreensão, de como trabalhar com o sistema. Ela é causada, pelo fato de que qualquer dado colocado no ativo customizado, foi totalmente removido. Seja por conta de alguma mudança na configuração do ativo. Seja por conta de uma destruição dos dados presentes nele.

Mas de qualquer forma, esta falha será mais comum, quando depois de carregarmos as barras previas de 1 minuto, fizermos o carregamento dos tickets negociados. Ao fazer isto, o sistema irá perceber que precisará mudar a forma de plotagem do preço, e ao fazer esta troca, todas as barras previas serão removidas, isto foi mostrado no artigo anterior.

Então para resolver este problema, precisaríamos primeiramente, declarar o carregamento dos tickets, antes de fazer o carregamento das barras previas. Isto resolve o problema, mas ao mesmo tempo força o usuário, a um tipo de modelagem do arquivo de configuração, que ao meu ver não faz muito sentido. O motivo é que, ao desenvolver a programação, responsável por analisar e executar o que esta no arquivo de configuração, podemos permitir ao usuário, declarar as coisas em qualquer ordem. Desde é claro, seja respeitado uma certa sintaxe.

Basicamente, existe apenas um caminho para se resolver o problema. A forma como este caminho é criado, nos permite gerar pequenas variações do mesmo. No entanto, basicamente ele é sempre o mesmo. Isto pensando em termos de programação. O caminho a ser feito, é em suma, fazer a leitura completa do arquivo de configuração, e depois ir acessando as coisas, na ordem que elas precisam ser acessadas, de forma que tudo funcione em perfeita harmonia.

Uma das variações nesta técnica, é usar as funções de FileSeek e FileTell, de forma a poder fazer a leitura, em uma certa sequência. Mas entre acessar o arquivo apenas uma vez, e depois acessar as coisas diretamente na memória, Ou ficar fazendo leituras particionadas do arquivo, a ponto de efetuar o mesmo trabalho que seria feito, se ele já estivesse na memória.

Pessoalmente prefiro carregar ele totalmente, para a memória e depois fazer a leitura particionada, de forma a ter uma certa sequencia, que precisamos ter, para que o carregamento se dê, de forma a não termos como resultado a figura 01. Desta forma esta será a técnica que será utilizada: Irei carregar o arquivo de configuração para a memória, e depois irei fazer o carregamento dos dados, de forma que o ativo possa ser usado no replay ou simulação.

Para criar um meio de que a programação se sobreponha a sequência de comandos, dentro do arquivo de configuração, deveremos primeiramente, criar um conjunto de variáveis. Mas não irei programar a coisa do zero. Vamos aproveitar as rotinas já incluídas no MQL5, para nos ajudar.

Para ser mais preciso, vamos utilizar a biblioteca padrão do MQL5. É verdade que iremos utilizar, apenas uma pequena fração de tudo que existe ali. A grande vantagem em se fazer isto, é que as rotinas presentes ali, já foram testadas de forma bastante exaustivas. E se em algum momento no futuro, elas vierem a sofrer algum tipo de melhoria em sua programação, seja ela qual for, nosso sistema irá absorver estas melhorias automaticamente. Assim fica muito mais simples, programar as coisas, pois precisaremos de menos tempo na fase de testes e otimização.

Então vamos ver do que iremos precisar. Isto é visto no código abaixo:

#include "C_FileBars.mqh"
#include "C_FileTicks.mqh"
//+------------------------------------------------------------------+
#include <Arrays\ArrayString.mqh>
//+------------------------------------------------------------------+
class C_ConfigService : protected C_FileTicks
{
// ... Código interno da classe ....
}

Aqui vamos utilizar esta include, que é fornecida no MQL5. Isto será mais do que o suficiente para nosso proposito.

Feito isto, precisamos de algumas variáveis globais privativas a classe:

    private :
//+------------------------------------------------------------------+
        enum eTranscriptionDefine {Transcription_INFO, Transcription_DEFINE};
        string m_szPath;
        struct st001
        {
            CArrayString *pTicksToReplay, *pBarsToTicks, *pTicksToBars, *pBarsToPrev;
        }m_GlPrivate;

Estas variáveis, que estão dentro de uma estrutura, são na verdade, ponteiro para a classe que iremos utilizar. É importante você notar, que trabalhar com ponteiros, é diferente de trabalhar com variáveis. Ponteiros são estruturas que indicam uma posição na memória, um endereço para ser mais exato. Já as variáveis, contém apenas valores.

Um grande detalhe nesta história, é que por padrão, e para facilitar a vida de muitos programadores, principalmente os que estão iniciando na área de programação. É o fato que o MQL5, na verdade não usa exatamente o mesmo conceito de ponteiros utilizados em C / C++. Quem já programou, ou programa em C / C++, sabe o quanto ponteiros podem ser uteis, e ao mesmo tempo perigosos e confusos. Mas aqui no MQL5, os desenvolvedores tentaram retirar a maior parte da confusão e do perigo que existe em utilizar ponteiros. Mas para fins práticos, você tem que ter em mente, que de fato, o que iremos fazer, é utilizar ponteiros para poder acessar a classe CArrayString.

Se você desejar saber mais sobre os métodos da classe CArrayString. Dê uma olhada na parte da documentação sobre ela, para isto bastará clicar em CArrayString e ver o que já existe ali disponível para o nosso uso. Notem que ela é bem completa e serve perfeitamente para o que iremos fazer.

Bem, já que estamos utilizando ponteiros para acessar as coisas, precisamos antes de tudo, fazer uma pre inicialização dos mesmos. Isto é feito no seguinte fragmento ...

bool SetSymbolReplay(const string szFileConfig)
   {

// ... Código interno ...

      m_GlPrivate.pTicksToReplay = m_GlPrivate.pTicksToBars = m_GlPrivate.pBarsToTicks = m_GlPrivate.pBarsToPrev = NULL;
// ... Restante do código ...

   }

Talvez, o maior dos erros dos programadores, ao utilizar ponteiros, principalmente em C / C++, seja o fato, de que eles tentam utilizar estes ponteiros, antes mesmo de os inicializar. O perigo dos ponteiros, é que eles nunca apontam para uma posição de memória, que podemos utilizar antes de os inicializar. O fato de você tentar ler, mas principalmente escrever, em um região desconhecida da memória, é que você muito provavelmente, estará escrevendo em uma parte critica, ou lendo lixo. Ao fazer isto, todo o sistema costuma quebrar, produzindo travamentos ou coisas do tipo. Então tomem muito cuidado ao usar ponteiros em seus programas.

Tomados estes cuidados, vamos ver como ficou na nossa rotina de configuração do sistema de replay / simulador. Esta pode ser vista integralmente no código abaixo:

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("Falha na abertura do arquivo de configuração [", szFileConfig, "]. Serviço sendo encerrado...");

             return false;
      }

          Print("Carregando dados para replay. Aguarde....");

          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("Não existem ticks para serem usados. Serviço esta sendo encerrado...");
                  cError = -1;
               }else if (!bBarsPrev) FirstBarNULL();
               break;
            case 1  : Print("O comando na linha ", iLine, " não é reconhecido pelo sistema...");    break;
            case 2  : Print("O sistema não esperava o conteudo da linha ", iLine);                  break;
            default : Print("Existe um erro no acesso de um dos arquivos indicados...");
         }
                                              
         return (cError == 0 ? !_StopFlag : false);
#undef macroFileName
      }

As partes riscadas no código acima, foram removidas dele. Isto por que não precisamos mais delas, elas foram colocadas em um outro local. Mas antes de vermos o local onde elas estão, você precisa prestar bastante atenção, ao como o código esta funcionando, nesta etapa daqui.

Se você notar, irá perceber que existe um código bastante repetitivo aqui. Apesar de que estaremos sempre nos referindo a ponteiros diferentes.

Este código, serve para que o operador new, crie uma área de memória, para que a classe possa existir. Ao mesmo tempo o tal, operador inicializa a classe. Como aqui não existe de fato um constructor de inicialização, a classe é criada e inicializada, com um valor padrão, e isto em todos os casos.

Uma vez que a classe foi inicializada, o valor do ponteiro, não será mais NULL, então o ponteiro irá se referir a apenas e somente ao primeiro elemento. Seria como se fosse uma lista, mas aqui estamos nos referindo a Arrays. Agora que já temos o ponteiro voltado para uma posição valida da memória, podemos usar um dos métodos da classe, de forma a adicionar uma cadeia de caracteres a ela.

Observem que isto é feito de maneira praticamente transparente. Não precisamos saber onde e de que forma, a classe está organizando as informações que estamos adicionando. Para nos isto não interessa. O que de fato importa, é que cada vez que chamamos ou invocamos o método, a classe armazene os dados para nos.

Vejam que em momento algum, fizemos qualquer referencia a algum dos arquivos que estará sendo indicado pelo arquivo de configuração. A única coisa que é feita, em tempo real de leitura, é a configuração das informações básicas de funcionamento, mas a leitura dos dados, seja eles de barras ou tickets, é deixada para depois.

Depois que o sistema acabou de ler o arquivo de configuração, passamos para a próxima etapa. Agora que vem a grande questão, da qual você, e apenas você, deve pensar ao adicionar, ou usar este sistema. Então preste muita atenção, ao que irei explicar. Pois isto pode fazer diferença no seu sistema em particular.

Aqui temos 4 procedimentos, e a ordem em que eles aparecem influencia no resultado final. Não se preocupem por enquanto, com o código de cada um deles. Aqui você precisa se preocupar, é com a ordem em que eles aparecem no código. Pois dependendo da ordem que eles surgem, o sistema irá apresentar o gráfico de uma forma, ou de outra. Talvez tenha ficado confuso, mas vamos tentar entender como a coisa irá se dar, e assim saber qual a ordem mais adequada, dependendo do tipo de base de dados, que você estará utilizando.

Na ordem que esta sendo usada no código. O sistema irá fazer o seguinte: Primeiro ele irá ler os tickets de dados reais. Depois irá transformar as barras em tickets, via processo de simulação. A seguir, irá criar as barras previas, seguindo primeiramente a transformação de tickets em barras, e por ultimo irá carregar as barras de 1 minuto.

A questão é que os dados serão lidos nesta ordem, que foi descrita acima. Caso você queira mudar esta ordem, terá que faze-lo mudando a ordem, que estes procedimentos são chamados. Mas você não deve fazer a leitura das barras previas, antes de ler os tickets que serão simulados. Isto por que, a leitura dos tickets a serem usados no replay, ou a leitura de barras, que serão usadas no simulador, fazem com que qualquer coisa, que esteja no gráfico seja deletada. Isto por conta dos detalhes visto no artigo anterior. Mas o fato de que a ordem de uso, ou leitura seja definida justamente no sistema, e não pelo arquivo de configuração. É que permite você declarar as barras previas, antes dos tickets, e ainda assim o sistema conseguirá lidar com eventuais problemas.

Mas nem tudo são flores. Ao se fazer isto, será permitido que o sistema defina a ordem que as coisas irão acontecer, ou melhor dizendo, serão lidas. Mas principalmente, da forma como isto foi feito, onde lemos todos os dados para a memória e depois fazemos a devida interpretação deles, faz com que o sistema, fique com dificuldades em informa a linha exata. Isto no caso, da leitura vim a dar errado. No entanto, ainda assim, teremos a indicação do nome do arquivo do qual não foi possível acessar os dados.

Para sanar este inconveniente, de o sistema não conseguir reportar a linha exata, onde o erro esta. Teremos que fazer uma pequena nova adição ao código, mas é algo bastante simples de ser feito. Lembre-se: Não queremos reduzir a funcionalidade do nosso sistema, queremos aumentar e expandir a coisa, de forma a conseguir cobrir a maior quantidade possível de casos.

Bem, antes de realmente implementar a solução. Quero que você entenda o motivo de estamos fazendo o uso esta solução especifica.

Para resolvermos o problema, que é o de saber em qual linha ocorreu o erro. Temos que durante a fase de carregamento, armazenar tanto o nome o arquivo, quanto a linha onde ele estava sendo declarado. Isto na verdade, é uma solução bem simples de ser feita. O motivo é o de que tudo que será preciso fazer, é seria declarar mais um conjunto de variáveis, que fariam uso da classe CArrayInt. Esta iria armazenar as linhas correspondentes, a cada um dos nomes de arquivos.

Apesar desta solução ser bastante agradável a primeira vista, já que ao faze-la, iriamos fazer uso da biblioteca padrão. Ela é um pouco dispendiosa, nos forçando a adicionar um numero muito maior de variáveis, do que seria necessário se desenvolvêssemos a nossa própria solução. Usando para isto, o mesmo principio das classes usadas, na biblioteca padrão. Só que com a capacidade, de trabalhar com uma quantidade maior de dados ao mesmo tempo.

Você pode pensar que ao fazermos isto, estaríamos trazendo problemas para nossa ossada, ou invés de utilizar uma solução, já testada e implementada, que está disponível na biblioteca padrão. Pessoalmente, até concordaria com esta ideia e afirmação em muitos dos casos. Mas aqui não faz muito sentido, já que não precisamos de todos aqueles métodos disponíveis na biblioteca padrão. Precisamos apenas e somente de 2 deles. Desta forma então, o custo da implementação, compensa o trabalho extra. Assim então nasce, uma nova classe no nosso projeto. A classe C_Array.

O código da classe pode ser visto integralmente logo abaixo:

#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];
        }
//+------------------------------------------------------------------+
};
//+------------------------------------------------------------------+

Notem como ela é super simples e compacta, e no entanto encaixa perfeitamente com o proposito que precisamos, já que usando ela podemos armazenar ao mesmo tempo tanto a linha e o nome do arquivo que contem as informações necessárias para que o replay ou simulação possam acontecer, e estes dados estão todos sendo declarados no arquivo de configuração. Então adicionando esta classe ao nosso projeto, ficamos com o mesmo nível de funcionalidade de antes, ou melhor dizendo, aumentamos o nível de funcionalidade, já que agora podemos também fazer um replay de dados vindos de um mercado similar ao de forex.

Mas com isto algumas mudanças precisam ser feitas no código da classe responsável pela configuração do replay / simulação, e estas mudanças começam no seguinte código:

#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;

Foi preciso adicionar apenas e somente estes pontos no arquivo, mas ao mesmo tempo precisamos ajustar algumas coisas na rotina de configuração. Então a nova rotina ficará conforme mostrado abaixo:

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("Falha na abertura do arquivo de configuração [", szFileConfig, "]. Serviço sendo encerrado...");
            return false;
        }
        Print("Carregando dados para replay. Aguarde....");
        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("Não existem ticks para serem usados. Serviço esta sendo encerrado...");
                    cError = -1;
                }else if (!bBarsPrev) FirstBarNULL();
                break;
            case 1  : Print("O comando na linha ", m_GlPrivate.Line, " não é reconhecido pelo sistema..."); break;
            case 2  : Print("O sistema não esperava o conteudo da linha: ", m_GlPrivate.Line);              break;
            default : Print("Erro no acesso no arquivo indicado na linha: ", m_GlPrivate.Line);
        }
        
        return (cError == 0 ? !_StopFlag : false);
#undef macroFileName

    }

Agora vejam que já podemos informar para o usuário se ocorreu um erro, e em que linha isto aconteceu. Mas o custo para fazer isto é quase nenhum, pois como você pode notar as rotinas praticamente se mantiveram iguais antes, apenas sofreram com a adição de um novo elemento. O que é muito bom, dado a quantidade de coisas que precisariam ser feitas se fossemos usar a biblioteca padrão. Não me entendam mal, eu não estou dizendo para você não fazer uso da biblioteca padrão, apenas estou mostrando que existem momentos que precisamos criar a nossa própria solução, já que nestes momentos os custos compensam o trabalho.

Muito bem, agora já podemos ver as rotinas que complementam estes sistema, que são as quatro rotinas mostradas acima, estas são todas rotinas bastante simples de forma geral, e tem seus código vistos logo abaixo, e já que todas seguem quase um mesmo principio irei explicar todas de uma só vez, para não deixar a coisa toda muito cansativa.

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;
    }
//+------------------------------------------------------------------+

Mas esperem um pouco .... observem com atenção todas estas 4 rotinas acima ... notem que elas tem muito código que esta sendo repetido ... e o que fazemos quando temos muito código repetido ?!?! Tentamos reduzir a rotina a um nível mais básico de forma que a reutilização de código se torna uma coisa mais rotineira ... eu sei que pode parecer burrice ou até falta de atenção da minha parte, mas nesta sequencia de artigos que não apenas mostrar a vocês como de fato programas são construídos do zero, mas como bons programadores vão tornando seus programas melhores reduzindo o trabalho de manutenção ao mesmo tempo que eles vão sendo produzidos de forma extremamente rápida ...

Não é o fato de que alguém tem mais ou menos anos de experiência em programação que conta, mas sim o fato de que você, mesmo estando começando no mundo da programação consiga perceber que muitas das vezes rotinas podem ser melhoradas e códigos reduzidos, facilitando assim o trabalho de desenvolvimento e agilizando a construção das coisas, mesmo que no primeiro momento possa parecer perda de tempo otimizar as coisas de forma a ter um nível maior de reutilização de código, isto no decorrer do desenvolvimento acaba por lhe ajudar, já que no momento que você precisar melhor alguma coisa terá menos coisas a ser replicada em mais de um ponto ... Pense nisto na hora que estiver programando: Será que não daria para reduzir o numero ou a quantidade de código que estou usando para fazer esta tarefa ???

É isto que a rotina abaixo faz, ela substitui todas as 4 rotinas acima, então caso precisemos melhorar o sistema precisaremos mexer em apenas um rotina e não mais em 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;
    }

É muito importante você notar que esta rotina na verdade esta dividida em dois segmentos, apesar de ela esta dentro de um único procedimento. O primeiro segmento é onde incializamos o ponteiro a ser usado para acessar os dados que foram armazenados durante a leitura do arquivo de configuração. Este segmento é bastante simples, creio que ninguém irá ter dificuldades em entende-lo. Feito isto passaremos para o segundo segmento onde iremos ler o conteúdo dos arquivos apontados pelo usuário. Aqui cada um dos ponteiros irá ler o conteúdo armazenados na memória e executar o seu trabalho, no final o ponteiro irá ser destruído liberando assim a memória utilizada.

Mas antes de voltamos a rotina de configuração, quero mostrar que podemos fazer ainda mais neste sistema. Lá no inicio deste tópico eu disse que você deveria compilar o programa pensando no como a leitura iria se dar, mas durante o decorrer do artigo, pensei melhor e decidi que podemos estender as coisas a um nível um pouco mais amplo, de forma que não será preciso ficar recompilando o programa a todo momento.

A ideia aqui é fazer o carregamento primeiro dos dados a serem simulados, para somente depois caso seja indicado, carregar as barras previas. Mas temos um problema: Tanto os ticks a serem utilizados no replay / simulação, quanto as barras previas, podem vim de arquivos do tipo tickets ou do tipo barras, mas precisamos de alguma forma de especificar isto, então a minha proposta é usar uma variável que poderá ser definida pelo usuário, mas ela não irá seguir qualquer padrão, vamos ser bastante específicos para que a coisa não desande e se torne inviável no longo prazo, e para fazer isto iremos utilizar a seguinte tabela:

Valor Modo de leitura
1 Modo Ticket - Ticket
2 Modo Ticket - Bar
3 Modo Bar - Ticket 
4 Modo Bar - Bar 

Tabela 01 - Dados para modelo de leitura interna

O que esta tabela acima diz é como será feita a leitura. Ela parece complicada, mas vamos entender com calma a proposta: Sempre iremos começar lendo os dados a serem usados no replay ou na simulação para somente depois ler o valor a ser usado como prévios no gráfico. Então vamos supor que o usuário informe o valor 1, o sistema irá fazer a leitura da seguinte maneira: Arquivo de tickets reais - Arquivo de barras convertido em tickets - Arquivo de tickets convertido em dados prévios - Arquivo de barras previas ... Este seria o modo 1, mas vamos supor que você deseje modificar por qualquer motivo o sistema, de forma a usar o mesmo banco de dados mas fazendo um outro tipo de analise, então você poderia informa o valor 4 por exemplo, e o sistema irá utilizar o mesmo banco de dados só que o resultado seria levemente diferente pois a leitura seria feita na seguinte ordem: Arquivo de barras convertido em tickets - Arquivo de tickets reais - Arquivo de barras previas - Arquivo de tickets convertido em dados prévios ... se você usar experimentar irá ver que os indicadores, sejam eles médias ou qualquer outro irá gerar leves mudanças entre um modo e outro, mesmo usando a mesma base de dados.

Para implementar isto o custo de programação é mínimo, e por conta disto acredito que vale apena disponibilizar este tipo de recurso ao usuário.

Então vamos ver como implementar este tipo de sistema. Começaremos adicionando uma variável global, porém privativa a classe, de forma a poder trabalhar mais tranquilamente.

class C_ConfigService : protected C_FileTicks
{

       protected:
//+------------------------------------------------------------------+

          datetime m_dtPrevLoading;

          int      m_ReplayCount,
               m_ModelLoading;

Esta variável irá armazenar o modelo para nos, mas mesmo que o usuário não a defina no arquivo de configuração, iremos definir um valor inicial para ela.

C_ConfigService()
   :m_szPath(NULL), m_ModelLoading(1)
   {
   }

Ou seja, iremos sempre começar, e por padrão, utilizar o modo Ticket - Ticket ... Feito isto precisamos permitir que o usuário possa indicar o valor que deverá ser usado. E isto é igualmente bem simples de ser feito, veja no fragmento abaixo:

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("Variável >>", szRet[0], "<< não definida.");
        }else
        Print("Definição de configuração >>", szInfo, "<< invalida.");

        return false;
    }

Aqui definimos o nome que será usado pelo usuário ao manipular a variável, e também permitimos que o usuário defina um valor para se utilizado conforme é visto na tabela 01. Aqui temos um pequeno detalhe: Devemos garantir que o valor esteja dentro dos limites esperados pelo sistema, por conta disto fazemos este pequeno teste, caso o usuário defina um valor diferente do esperado, nenhum tipo de erro será disparado, mas o sistema irá passar a usar o valor padrão.

Com tudo isto feito podemos finalmente ver como a rotina de carregamento e configuração ficou no final de todo este trabalho. Ela pode ser vista logo abaixo:

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("Falha na abertura do arquivo de configuração [", szFileConfig, "]. Serviço sendo encerrado...");
            return false;
        }
        Print("Carregando dados para replay. Aguarde....");
        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("Não existem ticks para serem usados. Serviço esta sendo encerrado...");
                    cError = -1;
                }else if (!bBarsPrev) FirstBarNULL();
                break;
            case 1  : Print("O comando na linha ", m_GlPrivate.Line, " não é reconhecido pelo sistema..."); break;
            case 2  : Print("O sistema não esperava o conteudo da linha: ", m_GlPrivate.Line);              break;
            default : Print("Erro no acesso no arquivo indicado na linha: ", m_GlPrivate.Line);
        }

        return (cError == 0 ? !_StopFlag : false);
#undef macroFileName
    }

O que temos aqui, é um pequeno sistema que nos permite, ou melhor que permite ao usuário, selecionar qual será o modelo a ser usado no carregamento, mas notem que iremos sempre começar carregando primeiramente as coisas que será usadas no replay ou simulação para somente depois carregar os dados que serão usados como barras previas.


Considerações finais

E com isto concluímos esta fase de trabalho no arquivo de configuração, agora e pelo menos por enquanto, o usuário terá como indicar todas as coisas que precisamos neste primeiro momento.

Bem, este artigo consumiu bastante tempo, mas ao meu ver valeu apena, já que agora temos a possibilidade de simplesmente não nos preocupar por um tempo com as questões relacionadas ao arquivo de configuração, já que ele agora estará sempre sendo executado da forma como é esperado, independentemente de como tenha sido construído.

No próximo artigo iremos continuar a adaptar o sistema de replay / simulador de forma a cobrir ainda mais o mercado de forex e seus similares, já que o mercado de Bolsa já se encontra em um estágio bem mais avançado, irei focar no de forex de forma que o sistema consiga lidar com ele da mesma forma que já faz com o mercado de bolsa.

Arquivos anexados |
Market_Replay_ev221.zip (14387.15 KB)
Um exemplo de como montar modelos ONNX em MQL5 Um exemplo de como montar modelos ONNX em MQL5
O ONNX (Open Neural Network Exchange) é um padrão aberto para a representação de modelos de redes neurais. Neste artigo, mostraremos a possibilidade de usar dois modelos ONNX simultaneamente em um Expert Advisor.
Estratégia de negociação no indicador de reconhecimento apurado de velas Doji Estratégia de negociação no indicador de reconhecimento apurado de velas Doji
O indicador baseado em metabarras detecta mais velas do que o clássico baseado em barras únicas. Vamos ver se ele oferece benefícios reais na negociação automatizada.
Como conectar o MetaTrader 5 ao PostgreSQL Como conectar o MetaTrader 5 ao PostgreSQL
Esse artigo descreve quatro métodos de conexão do código MQL5 ao banco de dados Postgres e apresenta um guia passo a passo para configurar um ambiente de desenvolvimento para um deles, a API REST, por meio do Windows Subsystem for Linux (WSL). Além disso, mostra-se um aplicativo de demonstração para a API com o código MQL5 necessário para inserir dados e consultar as respectivas tabelas, bem como um EA de demonstração para usar esses dados.
Redes neurais de maneira fácil (Parte 37): atenção esparsa Redes neurais de maneira fácil (Parte 37): atenção esparsa
No artigo anterior, abordamos modelos relacionais que usavam mecanismos de atenção. Uma das características desses modelos era o aumento do uso de recursos computacionais. O artigo de hoje apresenta um dos mecanismos para reduzir o número de operações computacionais dentro do bloco Self-Attention, o que aumenta o desempenho geral do modelo.