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

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

MetaTrader 5Testador | 8 agosto 2023, 10:42
1 093 5
Daniel Jose
Daniel Jose

Introdução

No artigo anterior Desenvolvendo um sistema de Replay - Simulação de mercado (Parte 22): FOREX (III), fizemos algumas modificações no sistema, a fim de conseguir fazer com que o simulador consiga gerar informações baseadas no BID, e não apenas no LAST. Mas estas mudanças não me deixaram satisfeito, e o motivo é simples: Estamos duplicando código. O fato de que esta duplicação esteja acontecendo não me deixa nada confortável.

Lá neste mesmo artigo. Existe um momento, em que deixo claro esta insatisfação:

"... Não me pergunte o porque. Mas por algum estranho motivo, que eu pessoalmente não faço a menor ideia do por que. Temos que adicionar esta linha aqui. Caso não façamos isto, o valor informado no volume de tickets será incorreto. Apesar disto, você deve notar que existe uma condicional na função. Esta evita problemas no caso de você usar o sistema de posicionamento rápido, evitando que apareça uma barra estranha, que estará fora do tempo, no gráfico do sistema. Mas fora isto, que é um motivo extremamente estranho, todo o restante funciona conforme o esperado. Então o novo calculo será este daqui, e com isto estaremos contabilizando os tickets tanto quando estamos trabalhando com um ativo de plotagem BID quanto um ativo que utiliza a plotagem LAST..."

Mas como o código já estava fechado para o artigo, e o mesmo também já estava quase pronto. Deixei a coisa como estava. Mas aquilo estava me incomodando e muito. Já que não faz sentido o código funcionar em determinadas situações e não funcionar em outras. E mesmo fazendo um debug do código, tentando encontrar o motivo da falha, não estava de fato conseguindo encontrar o motivo. Mas ao ignorar o código por uns momentos e observar o fluxograma do sistema, ( e sim você deve sempre tentar fazer uso de um fluxograma para agilizar a codificação ), notei que poderia fazer algumas mudanças de forma a evitar que o código fosse duplicado. E para piorar a coisa toda, o código estava mesmo sendo duplicado. Por conta disto existia a tal falha, que eu pessoalmente não conseguia resolver. Mas existe uma solução, e vamos começar este artigo resolvendo este problema. Já que o fato de ele existir pode vim a inviabilizar uma correta codificação do simulador, para que este seja capaz de lidar com os dados de mercado do tipo encontrados no FOREX.


Resolvendo a questão do volume de tickets

Aqui neste tópico, especificamente, irei mostrar como foi resolvido o problema que gerava uma falha no volume de tickets. Para começar o código de leitura dos tickets precisou ser modificado. Ficando conforme é visto abaixo:

datetime LoadTicks(const string szFileNameCSV, const bool ToReplay = true)
    {
        int      MemNRates,
                 MemNTicks;
        datetime dtRet = TimeCurrent();
        MqlRates RatesLocal[],
                 rate;
        bool     bNew;
        
        MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate);
        MemNTicks = m_Ticks.nTicks;
        if (!Open(szFileNameCSV)) return 0;
        if (!ReadAllsTicks(ToReplay)) return 0;         
        rate.time = 0;
        for (int c0 = MemNTicks; c0 < m_Ticks.nTicks; c0++)
        {
            if (!BuildBar1Min(c0, rate, bNew)) continue;
            if (bNew) ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
            m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate;
        }
        if (!ToReplay)
        {
            ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates));
            ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0);
            CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates));
            dtRet = m_Ticks.Rate[m_Ticks.nRate].time;
            m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates);
            m_Ticks.nTicks = MemNTicks;
            ArrayFree(RatesLocal);
        }else
        {
            CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TRADE_CALC_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CALC_MODE_EXCH_STOCKS : SYMBOL_CALC_MODE_FOREX);
            CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_CHART_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CHART_MODE_LAST : SYMBOL_CHART_MODE_BID);
        }
        m_Ticks.bTickReal = true;
        
        return dtRet;
    };

Este código aqui fazia parte do código que converte os tickets em barras de 1 minuto. Mas agora iremos utilizar um código diferente. O motivo é que agora esta chamada, irá servir para mais de um proposito, e o trabalho que ela faz, será também usado para criar as barras do replay. Com isto o código de criação das barras, deixará de estar duplicado nas classes.

Vamos então, dar uma olhada no código de conversão:

inline bool BuildBar1Min(const int iArg, MqlRates &rate, bool &bNew)
inline void BuiderBar1Min(const int iFirst)
   {
      MqlRates rate;
      double   dClose = 0;
      bool     bNew;
                                
      rate.time = 0;
      for (int c0 = iFirst; c0 < m_Ticks.nTicks; c0++)
      {
         switch (m_Ticks.ModePlot)
         {
            case PRICE_EXCHANGE:
               if (m_Ticks.Info[c0].last == 0.0) continue;
               if (m_Ticks.Info[iArg].last == 0.0) return false;
               dClose = m_Ticks.Info[c0].last;
               break;
            case PRICE_FOREX:
               dClose = (m_Ticks.Info[c0].bid > 0.0 ? m_Ticks.Info[c0].bid : dClose);
               if ((dClose == 0.0) || (m_Ticks.Info[c0].bid == 0.0)) continue;
               if ((dClose == 0.0) || (m_Ticks.Info[iArg].bid == 0.0)) return false;
               break;
         }
         if (bNew = (rate.time != macroRemoveSec(m_Ticks.Info[c0].time)))
         {
            ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
            rate.time = macroRemoveSec(m_Ticks.Info[c0].time);
            rate.real_volume = 0;
            rate.tick_volume = (m_Ticks.ModePlot == PRICE_FOREX ? 1 : 0);
            rate.open = rate.low = rate.high = rate.close = dClose;
         }else
         {
            rate.close = dClose;
            rate.high = (rate.close > rate.high ? rate.close : rate.high);
            rate.low = (rate.close < rate.low ? rate.close : rate.low);
            rate.real_volume += (long) m_Ticks.Info[c0].volume_real;
            rate.tick_volume++;
         }
         m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate;
      }
      return true;                    
   }

Todos os prontos riscados no código foram retirados. Já que isto iria atrapalhar a correta criação que será usada pela classe C_Replay. Mas em compensação, tive que adicionar estes pontos, para informar ao chamador o que aconteceu na conversão.

Agora tem um detalhe: Esta função originalmente era privada a classe C_FileTicks. Mas teve seu nível de acesso modificado para que fosse possível utilizar ela na classe C_Replay. No entanto, não quero que ela saia muito destes limites. Por conta disto ela não será publica e sim do tipo protegida. Desta forma conseguimos limitar o acesso ao nível máximo permitido a classe C_Replay. Lembre-se de que o ultimo nível é a classe C_Replay. Então somente os procedimentos e funções que estiverem sendo  declaradas na classe C_Replay, como sendo publicas, é que poderão ser acessadas fora da classe. A construção interna do sistema será totalmente oculta dentro desta classe C_Replay.

Veja agora, como ficou a nova rotina de criação das barras.

inline void CreateBarInReplay(const bool bViewTicks)
   {
#define def_Rate m_MountBar.Rate[0]

      bool    bNew;
      double  dSpread;
      int     iRand = rand();
                                
      if (BuildBar1Min(m_ReplayCount, def_Rate, bNew))
      {
         m_Infos.tick[0] = m_Ticks.Info[m_ReplayCount];
         if ((!m_Ticks.bTickReal) && (m_Ticks.ModePlot == PRICE_EXCHANGE))
         {                                               
            dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 );
            if (m_Infos.tick[0].last > m_Infos.tick[0].ask)
            {
               m_Infos.tick[0].ask = m_Infos.tick[0].last;
               m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread;
            }else   if (m_Infos.tick[0].last < m_Infos.tick[0].bid)
            {
               m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread;
               m_Infos.tick[0].bid = m_Infos.tick[0].last;
            }
         }
         if (bViewTicks) CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
         CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate);
      }
      m_ReplayCount++;
#undef def_Rate
   }

A criação, agora, é efetuada no mesmo ponto que fazemos a conversão dos tickets em barras. Então se algo vim a dar errado durante a conversão, iremos logo notar o erro. Pois o mesmo código que lança as barras de 1 minuto no gráfico, quando fazemos um avanço rápido, também é utilizando pelo sistema de posicionamento, e também é usado para lançar as barras durante o avanço normal. Ou seja, agora o código responsável por tal tarefa, não esta mais sendo duplicado em ponto algum. Desta forma, já temos um sistema bem mais adequado, tanto para manutenção, quanto para melhorias. Mas também quero que notem uma coisa, que tem igual importância, já que foi adicionado ao código acima. A emulação dos preços BID e ASK somente irão ocorrer, se estivermos em um sistema simulado e que os dados simulados, sejam de um mercado similar ao de bolsa. Ou seja, caso a plotagem seja baseada em BID, esta emulação não será mais executada. Isto é importante para o que iremos começar a projetar, já no próximo tópic.


Iniciando a simulação de plotagem BID ( Modo FOREX )

A partir de agora iremos focar apenas e somente na classe C_Simulation. De forma a fazer a simulação dos dados, que não estão sendo cobertos na atual implementação do sistema. Mas antes, precisamos fazer uma pequena coisa:

bool BarsToTicks(const string szFileNameCSV)
   {
      C_FileBars *pFileBars;
      int         iMem = m_Ticks.nTicks,
                  iRet;
      MqlRates    rate[1];
      MqlTick     local[];
                                
      pFileBars = new C_FileBars(szFileNameCSV);
      ArrayResize(local, def_MaxSizeArray);
      Print("Convertendo barras em ticks. Aguarde...");
      while ((*pFileBars).ReadBar(rate) && (!_StopFlag))
      {
         ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary);
         m_Ticks.Rate[++m_Ticks.nRate] = rate[0];
         if ((iRet = Simulation(rate[0], local)) < 0)
         {
            ArrayFree(local);
            delete pFileBars;
            return false;
         }
         for (int c0 = 0; c0 <= iRet; c0++)
         {
            ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
            m_Ticks.Info[m_Ticks.nTicks++] = local[c0];
         }
      }
      ArrayFree(local);
      delete pFileBars;
      m_Ticks.bTickReal = false;
                                
      return ((!_StopFlag) && (iMem != m_Ticks.nTicks));
   }

Caso venha a acontecer alguma coisa de errado, e queremos finalizar totalmente o sistema. Precisamos de uma forma de falar para as demais classes, que a simulação falhou. A forma mais simples de fazer isto, é assim. Apesar de tudo, não gosto muito da forma como esta rotina foi criada. Apesar de tal procedimento funcionar. Nele esta faltando coisas, que precisamos informar para a classe C_Simulation. Então depois de fazer algumas analises no código, foi decidido, trocar a forma como o procedimento irá funciona. Isto para evitar duplicar código novamente. Então esqueçam a rotina acima, ela funciona. Mas iremos de fato usar a rotina abaixo:

int SetSymbolInfos(void)
   {
      int iRet;
                                
      CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, iRet = (m_Ticks.ModePlot == PRICE_EXCHANGE ? 4 : 5));
      CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TRADE_CALC_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CALC_MODE_EXCH_STOCKS : SYMBOL_CALC_MODE_FOREX);
      CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_CHART_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CHART_MODE_LAST : SYMBOL_CHART_MODE_BID);
                                
      return iRet;
   }
//+------------------------------------------------------------------+
   public  :
//+------------------------------------------------------------------+
      bool BarsToTicks(const string szFileNameCSV)
      {
         C_FileBars      *pFileBars;
         C_Simulation    *pSimulator = NULL;
         int             iMem = m_Ticks.nTicks,
                         iRet = -1;
         MqlRates        rate[1];
         MqlTick         local[];
         bool            bInit = false;
                                
         pFileBars = new C_FileBars(szFileNameCSV);
         ArrayResize(local, def_MaxSizeArray);
         Print("Convertendo barras em ticks. Aguarde...");
         while ((*pFileBars).ReadBar(rate) && (!_StopFlag))
         {
            if (!bInit)
            {
               m_Ticks.ModePlot = (rate[0].real_volume > 0 ? PRICE_EXCHANGE : PRICE_FOREX);
               pSimulator = new C_Simulation(SetSymbolInfos());
               bInit = true;
            }
            ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary);
            m_Ticks.Rate[++m_Ticks.nRate] = rate[0];
            if (pSimulator == NULL) iRet = -1; else iRet = (*pSimulator).Simulation(rate[0], local);
            if (iRet < 0) break;
            for (int c0 = 0; c0 <= iRet; c0++)
            {
               ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
               m_Ticks.Info[m_Ticks.nTicks++] = local[c0];
            }
         }
         ArrayFree(local);
         delete pFileBars;
         delete pSimulator;
         m_Ticks.bTickReal = false;
                                
         return ((!_StopFlag) && (iMem != m_Ticks.nTicks) && (iRet > 0));
      }

Esta segunda versão, é consideravelmente muito mais eficiente, no que tange o que precisamos fazer. Isto além de evitar duplicar código. Mas principalmente pelo fato de que ao fazermos uso dela, teremos os seguintes aspectos:

  • Remoção da herança da classe C_Simulation. Com isto o sistema irá ficar ainda mais livre;
  • Inicialização dos dados do ativo, que antes eram somente feitos quando usavamos ticks reais;
  • Largura adequada de dígitos a serem utilizados na plotagem;
  • Uso da classe C_Simulation como sendo um ponteiro. Ou seja melhor aproveitamento da memória do sistema, já que uma vez que a classe tenha feito o seu trabalho, a memória que ela ocupava será liberada;
  • Garantia de apenas 1 ponto de entrada e 1 ponto de saída de dentro da função;
Com isto, algumas coisas irão mudar frente ao que esta presente no código do artigo anterior. Mas vamos continuar neste momento, a implementar a classe C_Simulation. O grande detalhe para o desenvolvimento da classe C_Simulation, é que poderemos ter qualquer quantidade de tickets no sistema. Mas apesar de isto não ser de fato um problema, pelo menos neste momento. A coisa realmente pega, é pelo fato de que em muitos dos casos, o range que teremos que cobrir somente entre a máxima e a mínima, já será muito maior do que o numero de tickets informados, ou plausíveis de serem criados. Isto sem contar com a perna que se inicia no preço de abertura e se dirige até um dos extremos. E a perna que inicia em um dos extremos e vai até o fechamento. Se fizermos esta contabilidade tentando usar o RANDOM WALK, em uma quantidade enorme de casos isto não será possível. Desta forma, teremos que abrir mão do random walk, criando em artigos anteriores e desenvolver um novo método para criar dos tickets. Eu disse que a questão do FOREX não era nada simples.

O problema deste tipo de abordagem, é que muitas das vezes teremos que criar, e fazer com que 2 métodos diferentes, funcionem de forma o mais harmoniosa quanto for possível. O pior de tudo, e sendo assim o grande detalhe, é que em alguns casos o random walk, tem uma modelagem bem mais similar ao que realmente ocorre em um ativo real. Mas quando estamos lidando com um volume baixo de negociação ( menos de 500 negócios em 1 minuto ), o random walk é totalmente inadequado. Neste tipo de cenário, iremos usar uma abordagem um pouco mais exótica, de forma a tentar cobrir todos os casos possíveis. Assim, a primeira coisa que iremos fazer, e isto, visto que precisamos inicializar a classe. É definir o constructor da classe, e este código pode ser visto abaixo:

C_Simulation(const int nDigits)
   {
      m_NDigits       = nDigits;
      m_IsPriceBID    = (SymbolInfoInteger(def_SymbolReplay, SYMBOL_CHART_MODE) == SYMBOL_CHART_MODE_BID);
      m_TickSize      = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE);
   }

Aqui simplesmente estamos iniciando os dados privativos da classe. Isto para não precisar forçar outras formas de buscar estes mesmos dados, em outros locais. Então veja que devemos sempre ter todas as coisas, devidamente configuradas no arquivo de configuração do ativo que irá ser simulado ou como será feito o replay. Caso contrário você acabara gerando erros estranhos no sistema.

Agora podemos começar a fazer a coisa andar. Já que fizemos a inicialização básica da classe. Vamos partir para os problemas as serem resolvidos. O primeiro ponto, é gerar uma valor aleatório de tempo. Mas este deverá ser capaz de comportar todos os tickets que deverão ser gerados nas barras de 1 minuto. Está na verdade, é a parte mais fácil da implementação. Mas antes de começar de fato a sair criando funções, precisamos primeiro criar um tipo especial de procedimento. Este pode ser visto logo abaixo:

template < typename T >
inline T RandomLimit(const T Limit01, const T Limit02)
   {
      T a = (Limit01 > Limit02 ? Limit01 - Limit02 : Limit02 - Limit01);
      return (Limit01 >= Limit02 ? Limit02 : Limit01) + ((T)(((rand() & 32767) / 32737.0) * a));
   }

E o que este procedimento acima faz de fato ?!?! Bem, você deve estar assustado olhando esta função acima, sem entender de fato o que esta acontecendo. Pois bem, vou tentar explicar, da maneira o mais simples quanto for possível, o que esta função de fato esta fazendo. E por que ela tem esta aparência tão estranha.

No código que iremos criar, precisamos de um tipo de função, ou procedimento, que seja capaz de gerar um valor aleatório entre dois extremos. No entanto, em alguns momentos, iremos precisar que este valor seja gerado com dados do tipo Double. Enquanto em outros momentos, precisaremos de valores sejam do tipo inteiro. Seria bastante dispendioso criar dois procedimentos, praticamente idênticos, apenas para fazer o mesmo tipo de fatoração. Então para evitar fazer isto, forçamos, ou melhor dizendo. Informamos ao compilador, para utilizar uma mesma fatoração e sobrecarregar ela, de maneira que podemos utilizar no código, uma mesma função. Mas em termos de executável, teremos de fato duas funções diferentes. Para conseguir fazer isto, usamos esta declaração daqui. Com isto definimos um tipo, que no caso está como sendo a letra T. Esta deverá ser repetida em todos os pontos que precisarmos que o compilador ajuste o tipo para nos. Por conta disto, você deve tomar cuidado, em não misturar as coisas. Deve deixar o compilador fazer os ajustes, de forma a não ter problemas de conversão de tipo.

Desta maneira, iremos sempre efetuar o mesmo cálculo. Mas este será ajustado em termos do tipo da variável usada. Isto pelo compilador, pois ele é que irá decidir qual o tipo correto. Assim, poderemos gerar um número pseudo aleatório, a cada chamada. Independentemente do tipo que estaremos usando. Mas notem que o tipo de ambos limites tem que ser iguais. Ou seja não adianta querer misturar double com inteiros, ou inteiros longos com inteiros curtos. Pois isto não irá funcionar. Esta é a única limitação deste tipo de abordagem onde utilizamos sobrecarga de tipos.

Mas a coisa ainda não acabou. O fato de ter feito a criação desta função acima, é justamente para evitar a geração de macros dentro do código da classe C_Simulation. Então agora vamos para o próximo passo. Gerar o sistema de temporização da simulação. Esta geração pode ser visto no código abaixo:

inline void Simulation_Time(int imax, const MqlRates &rate, MqlTick &tick[])
   {
      for (int c0 = 0, iPos, v0 = (int)(60000 / rate.tick_volume), v1 = 0, v2 = v0; c0 <= imax; c0++, v1 = v2, v2 += v0)
      {
         iPos = RandomLimit(v1, v2);
         tick[c0].time = rate.time + (iPos / 1000);
         tick[c0].time_msc = iPos % 1000;
      }
   }

Aqui estamos simulando o tempo, de forma que ele fique levemente randômico. É verdade que parece bastante confuso a primeira vista. Mas acredite o tempo aqui é randômico, apesar de ele não está ainda seguindo a mesma lógica que é esperada pela classe C_Replay. Isto por conta que o valor em milissegundos, não estar corretamente ajustado. Este ajuste será feito em outro ponto. Aqui queremos apenas que o tempo seja criado de forma aleatória, mas dentro do limite da barra de 1 minuto. E como estou fazendo isto ?!?! Bem, primeiramente dividimos o tempo de 60 segundos que na verdade são 60.000 milissegundos pelo numero de tickets que deverão ser gerados. Este valor é importante para nos, já que ele irá dizer qual é o range limite que iremos utilizar. Feito isto fazemos algumas atribuições simples a cada interação do laço. Agora o segredo para gerar um temporizador aleatório, esta nestas três linhas dentro do laço. Nesta primeira linha pedimos para o compilador gerar uma chamada em que utilizaremos dados do tipo inteiro, e esta chamada irá retornar um valor dentro do range que esta sendo especificado. Agora fazemos dois cálculos bem simples. Primeiro ajustamos o valor gerado com o tempo da barra de um minuto, e depois utilizamos este mesmo valor que foi gerado para ajustar o tempo em milissegundos. Desta forma, cada um dos tickets terá um valor completamente aleatório no que tange o tempo. Lembre-se estamos apenas ajustando o tempo neste primeiro momento. Este ajuste tem como objetivo, não deixar a coisa muito previsivel.

Legal. Agora vamos fazer a simulação dos preços. Lembrando mais uma vez, que irei focar apenas e somente na questão do sistema de plotagem BID. Depois irei unir o sistema de simulação, de forma que teremos uma maneira, bem mais geral de fazer esta simulação, a fim de cobrir tanto o BID quanto o LAST. Mas aqui primeiramente vou focar no BID. Para fazer esta simulação, neste primeiro momento, vamos deixar o spread sempre na mesma distância. Isto para que não compliquemos de forma desnecessária o código, antes de realmente verificar se ele de fato funciona. E esta primeira simulação, é feita com o uso de diversas rotinas bem curtas. Estaremos fazendo uso de rotinas curtas, para que a coisa seja o mais modular possível. Mas depois você entendera o motivo disto.

Então, vamos ver a primeira das chamadas a serem executadas para gerar a simulação BID:

inline void Simulation_BID(int imax, const MqlRates &rate, MqlTick &tick[])
   {
      bool    bHigh  = (rate.open == rate.high) || (rate.close == rate.high), 
      bLow = (rate.open == rate.low) || (rate.close == rate.low);
                                                        
      Mount_BID(0, rate.open, rate.spread, tick);     
      for (int c0 = 1; c0 < imax; c0++)
      {
         Mount_BID(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), rate.spread, tick); 
         bHigh = (rate.high == tick[c0].bid) || bHigh;
         bLow = (rate.low == tick[c0].bid) || bLow;
      }
      if (!bLow) Mount_BID(Unique(imax, rate.high, tick), rate.low, rate.spread, tick);
      if (!bHigh) Mount_BID(Unique(imax, rate.low, tick), rate.high, rate.spread, tick);
      Mount_BID(imax, rate.close, rate.spread, tick);
   }

Veja que esta rotina acima é bastante simples de ser compreendida. Se bem que a única parte aparentemente mais complicada, seja a montagem randomizada do valor de BID. Mas ainda assim é relativamente simples. Pois iremos gerar valores pseudo aleatórios dentro de um range, entre o valor máximo e o mínimo da barra. Mas notem que normalizo o valor. Isto por que, normalmente o valor gerado estará fora da faixa do preço. Então temos que fazer a normalização. Mas todo o restante da rotina acredito não ser problema para você de fato conseguir entender.

Se você observar com calma verá que temos duas funções, bastante referenciadas na parte que da simulação: MOUNT_BID e UNIQUE. Cada uma delas serve para um proposito. Mas vamos começar vendo a função Unique, que tem seu código logo abaixo:

inline int Unique(const int imax, const double price, const MqlTick &tick[])
   {
      int iPos = 1;
                                
      do
      {
         iPos = (imax > 20 ? RandomLimit(1, imax - 1) : iPos + 1);
      }while ((m_IsPriceBID ? tick[iPos].bid : tick[iPos].last) == price);
                                
      return iPos;
   }

Esta função serve para evitar que o valor de um dos limites, ou qualquer outro preço, seja por ventura removido durante a geração da posição aleatória. No entanto no momento, vamos usa-la exclusivamente nos limites. Notem que podemos tanto usar o valor de da modelagem BID, quanto o valor da modelagem LAST. Mas no momento, iremos utilizar esta função apenas no BID. Este é o único proposito desta função: Garantir que não sobrescrevamos um valor de limite.

Agora vamos dar uma olhada na função Mount_BID, que tem seu código visto a seguir:

inline void Mount_BID(const int iPos, const double price, const int spread, MqlTick &tick[])
   {
      tick[iPos].bid = price;
      tick[iPos].ask = NormalizeDouble(price + (m_TickSize * spread), m_NDigits);
   }

Apesar de neste primeiro momento, este código, ser bastante simples, beirando a pura beleza da programação. Ele facilita muito a nossa vida. Já que evitamos ficar repetindo código em diversos pontos. Mas principalmente evita que esqueçamos de normalizar o valor que deve ser colocado na posição de preço ask. Se esta normalização não for feita, teremos problemas depois, quando formos fazer uso deste valor ASK. Uma coisa importante que você deve se atentar aqui, é que o valor do preço ask, sempre será deslocado do valor do spread. Mas no momento este deslocamento é sempre constante. No entanto, isto se deve ao fato de que esta é a primeira implementação, e se eu coloca-se agora o sistema responsável pela randomização, a coisa iria ficar muito confusa de você entender, por que e como, randomizar o valor do spread.

Este valor do spread indicado aqui, na verdade é o valor que é indicado na barra especifica de 1 minuto. Cada uma das barras poderá ter um spread diferente. Mas aqui temos uma outra coisa, que você também deve entender. Se você estiver fazendo uma simulação na tentativa de ter um sistema próximo do que poderia, ter ocorrido no mercado real. Ou seja os dados contidos em um arquivo de tickets reais, irá notar que este spread que deverá ser usado, é o menor dos valores presentes durante a formação da barra de 1 minuto. Mas caso você esteja fazendo uma simulação aleatória, onde os dados poderão ou não se parecer com os que possivelmente tenham ocorrido no mercado real, este spread pode ser qualquer valor. No entanto, aqui irei permanecer fiel a esta questão de tentar montar o que poderia ter ocorrido no mercado. Assim o valor de spread sempre será o informado no arquivo de barras.

Existe mais uma rotina necessária para o sistema. Esta será responsável por ajustar o tempo de forma que a classe C_Replay, tenha os valores corretos de temporização. Este código pode ser visto abaixo:

inline void CorretTime(int imax, MqlTick &tick[])
   {
      for (int c0 = 0; c0 <= imax; c0++)
         tick[c0].time_msc += (tick[c0].time * 1000);
   }

O que este procedimento acima faz, é apenas corrigir de maneira adequada o tempo dado em milissegundos. Se você observar, irá ver que o calculo é o mesmo usado na rotina que carrega os tickets reais de um arquivo. O motivo de ter feito a coisa de forma tão modular, se deve ao fato de que pode ser interessante você fazer logs de cada uma das funções executadas. Se o código estivesse todo unido, seria mais complicado criar tais logs. Mas desta forma, você pode criar os logs e estudar os mesmo, e assim verificar o que deve ou não ser melhorado, de maneira tal a conseguir atender aos seu desejos particulares.

Um detalhe importante: Neste primeiro momento, irei bloquear o uso do sistema baseado na plotagem LAST. Isto por que vamos modificar ele em alguns pontos de forma a que ele seja capaz de trabalhar com ativos, ou momentos com uma liquidez baixa. Atualmente isto não é possível ser feito, mas irei corrigir isto depois. Assim não se preocupe caso você tente executar uma simulação baseada em plotagem LAST e o sistema não permitir. Isto será corrigido depois.

Para garantir isto. Usaremos um artificio de programação. Algo de extrema complexidade e muito elaborado. Veja no fragmento abaixo do que se trata:

inline int Simulation(const MqlRates &rate, MqlTick &tick[])
   {
      int imax;
                        
      imax = (int) rate.tick_volume - 1;
      Simulation_Time(imax, rate, tick);
      if (m_IsPriceBID) Simulation_BID(imax, rate, tick); else return -1;
      CorretTime(imax, tick);

      return imax;
   }

Agora atenção: Toda vez que o sistema for utilizar o modo de plotagem LAST, o sistema irá informa um erro. Mas você não deve se desesperar, como foi dito a pouco. Isto é por conta que a simulação baseado no modo LAST ( usado em Bolsa ), irá passar por melhorias. Desta forma, foi necessário adicionar este artificio extremamente complexo e elaborado. Se for tentado executar a simulação baseada em LAST, retornamos um valor NEGATIVO. Isto é ou não uma forma extremante elaborada de fazer as coisas ?!?! ( RISADAS ).

Mas antes de fechar este artigo, vamos dar uma melhorada na questão sobre a simulação de plotagem BID. Assim iremos ter uma forma um pouco melhor, pelo menos no que diz respeito a parte da randomização, nos resultados. Basicamente precisamos mexer em um único ponto de forma a ter um valor de spread randomizado. Você pode escolher fazer isto na função Mount_Bid ou na função Simulation_Bid. De certa forma isto não importa muito, mas para garantir que teremos um valor de spread mínimo conforme é informado no arquivo de barras de 1 minuto. Iremos fazer a modificação na função vista abaixo:

inline void Simulation_BID(int imax, const MqlRates &rate, MqlTick &tick[])
   {
      bool    bHigh  = (rate.open == rate.high) || (rate.close == rate.high), 
      bLow = (rate.open == rate.low) || (rate.close == rate.low);

      Mount_BID(0, rate.open, rate.spread, tick);     
      for (int c0 = 1; c0 < imax; c0++)
      {
         Mount_BID(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), (rate.spread + RandomLimit((int)(rate.spread | (imax & 0xF)), 0)), tick);
         bHigh = (rate.high == tick[c0].bid) || bHigh;
         bLow = (rate.low == tick[c0].bid) || bLow;
      }
      if (!bLow) Mount_BID(Unique(imax, rate.high, tick), rate.low, rate.spread, tick);
      if (!bHigh) Mount_BID(Unique(imax, rate.low, tick), rate.high, rate.spread, tick);
      Mount_BID(imax, rate.close, rate.spread, tick);
   }

Aqui estaremos garantindo uma randomização do valor do spread, é bem verdade que esta randomização é apenas demonstrativa. Você pode fazer algo um pouco diferente em termos de limites se assim desejar. Bastará apenas ajustar um pouco as coisas. Agora você deve entender que estou utilizando esta randomização aqui bem estranha para alguns, mas na verdade, o que estou fazendo é garantindo que o maior valor possível possa ser utilizado para randomizar o spread. Este valor é baseado em um cálculo onde unimos bit a bit o valor do spread com um valor que pode variar de 1 até 16, já que estaremos utilizando apenas uma parte de todos os bits. Mas você deve notar o seguinte: Caso o spread seja zero, e em alguns momentos ele de fato será zero, teremos ainda assim um valor que no mínimo será de 3 já que os valores 1 e 2. De fato não criarão um spread randomizado, isto por que o valor 1 indica apenas abertura igual ao fechamento, o valor 2 indica que a abertura pode ser igual ou diferente do fechamento. Mas neste caso, do valor 2 que irá realmente montar o valor é esta função daqui. Em todos os outros casos teremos a criação de uma randomização no spread.

Desta forma acredito que fica claro o motivo de eu não ter colocado a randomização na rotina Mount_Bid. Pois se isto fosse feito, em alguns momentos o spread mínimo informado pelo arquivo de barras não seria de fato o valor correto, mas como eu disse você é livre para experimentar e adaptar o sistema ao seu gosto e estilo.


Conclusão

Neste artigo, resolvemos os problemas envolvidos com a duplicação de código. Acredito que tenha ficado claro, e perfeitamente compreendido, os problemas envolvidos quando fazemos uso de um código duplicado. Em projetos muito grandes, sempre temos que tomar cuidado com isto. Código duplicados. Mesmo este código daqui, que não é assim tão grande, pode ter sérios problemas por conta destes descuido.

Um último detalhe, que merece também ser mencionado, é o fato de que em um arquivo de tickets reais, existem pontos onde de fato teremos algum tipo de movimentação " falsa ". Mas aqui isto não acontece, estes movimentos " falsos " são quando acontecem variações apenas em um dos preços, ou no BID ou no ASK. Mas por simplicidade e até mesmo para não complicar inutilmente o código deixei tais colocações de fora. Já que ao meu ver não faz muito sentido para um sistema de replay que esteja simulando um mercado, você ter de fato tais movimentos. Isto não traria de fato nenhuma melhoria em termos de operacional. Já que para cada mudança de BID sem a presença do ASK teríamos que fazer um ASK sem a presença do BID. Isto para manter o sistema dentro de equilíbrio exigido pelo mercado real.

Basicamente assim fechamos a questão da simulação do BID, pelo menos nesta primeira tentativa de faze-lo. Pode ser que no futuro eu venha a fazer mudanças neste sistema, de forma que ele funcione de uma outra maneira. Mas pelo menos ao usar ele com dados vindos do mercado de FOREX, notei que ele funciona de forma bastante adequada, mas para outros mercados pode ser que ele não seja o suficiente.

No arquivo em anexo, você terá acesso ao sistema no atual estágio de desenvolvimento. Mas como falei durante este artigo, você não deverá tentar fazer simulações com os ativos da bolsa, apenas com o do forex. apesar de poder fazer replay com todos eles, a simulação estará desabilitada para os ativos de bolsa. No próximo artigo iremos corrigir isto, melhorando o sistema de simulação da bolsa de forma que ele consiga trabalhar em momentos de baixa liquidez. E assim fechar esta questão sobre simulação. Então nos vemos no próximo artigo.

Arquivos anexados |
Market_Replay_7vx23.zip (14388.45 KB)
Últimos Comentários | Ir para discussão (5)
Philip Tweens
Philip Tweens | 17 ago 2023 em 19:39

Olá caro Daniel,

Parabéns por este grande sistema que você projetou.

Encontrei alguns problemas ao testar seu sistema e preciso da sua ajuda.

A primeira coisa é que salvei os dados do tick por um mês e coloquei para reprodução. No entanto, no TimeFrame de 1 minuto para o replay em um mês com a mudança do pino no controle deslizante, o número de velas mostradas não corresponde à posição do pino no controle deslizante, mostrei isso no vídeo em anexo. Levei o pino até o final, mas as velas só se repetem por um pequeno número de barras de 1 minuto (cerca de 20 barras).

A segunda coisa é que eu preciso mudar esse sistema de forma que a forma como as barras se movem seja como o Strategy Tester, ou seja, a posição do pino representa a velocidade de exibição das barras, ou eu tenho a possibilidade de mover barra a barra como o site TradingView. O seu sistema está de tal forma que pode ser alterado desta forma????

Agradeceria se pudesse me orientar.

Respeitosamente,

Daniel Jose
Daniel Jose | 17 ago 2023 em 21:32
Philip Tweens #:

Olá caro Daniel,

Parabéns por este grande sistema que você projetou.

Encontrei alguns problemas ao testar seu sistema e preciso da sua ajuda.

A primeira coisa é que salvei os dados do tick por um mês e coloquei para reprodução. No entanto, no TimeFrame de 1 minuto para o replay em um mês com a mudança do pino no controle deslizante, o número de velas mostradas não corresponde à posição do pino no controle deslizante, mostrei isso no vídeo em anexo. Levei o pino até o final, mas as velas só se repetem por um pequeno número de barras de 1 minuto (cerca de 20 barras).

A segunda coisa é que eu preciso mudar esse sistema de forma que a forma como as barras se movem seja como o Strategy Tester, ou seja, a posição do pino representa a velocidade de exibição das barras, ou eu tenho a possibilidade de mover barra a barra como o site TradingView. O seu sistema está de tal forma que pode ser alterado desta forma????

Agradeceria se pudesse me orientar.

Respeitosamente,

Ok. Vamos por partes como diria JACK ...😁👍

Você talvez esteja muito confuso sobre esta aplicação, ou melhor, talvez esteja esperando que esta aplicação venha a servir para algo, na qual ela de fato não foi pensada para ser utilizada em principio. Não estou dizendo que ela não possa ser usada para algo em particular, como por exemplo um testador de estratégia. Mas este não foi o objetivo inicial para que ela viesse a ser implementada. 

Sobre a primeira questão: Você talvez não tenha compreendido de fato como o replay / simulação acontecerá. Esqueça por um momento o controle deslizante. Quando você der play no sistema, ele irá buscar dos dados que foram carregados, seja como ticks ou barras e os irá apresentar como barras no gráfico, tendo como base o tempo de 1 minuto. Isto independente do tempo gráfico que você deseja usar. Por isto os dados, que devem estar no arquivo, deve ser pensados como barras de 1 minuto. Você não deve olhar os dados do arquivo, como sendo dados individuais. Pois esta aplicação não os vê desta maneira. Ela sempre interpretará as barras mesmo barras de duas horas como sendo barras de 1 minuto. Sempre.

Caso você esteja utilizando barras, a aplicação irá automaticamente notar isto, e criará uma simulação de forma que cada uma das barras tenha aproximadamente 1 minuto de duração. Criando para isto quantos ticks forem necessários para que os valores sejam corretamente plotados no gráfico. Se os dados presentes no arquivo, forem ticks, o sistema irá lançar cada um dos ticks no intervalo, aproximado, que estiver definido entre eles, Veja os artigos anteriores para compreender isto. Tal intervalo pode variar de poucos milisegundos até várias horas. Mas ao fazer isto, qualquer coisa que estiver no intervalo será tratado como sendo leilão ou uma suspensão de negociação. Assim, se você usar dados com intervalo superior a um dia, ou seja 24 horas, a aplicação muito provavelmente não irá conseguir reconhecer as barras de forma adequada. Isto caso você faça uso do controle deslizante para procurar um novo ponto de estudo. Por isto, deve-se evitar usar dados com um tempo maior que um dia.

Lembre-se a aplicação foi pensada para ser usada em um tempo equivalente a um tempo real. Ou seja períodos curtos. Para entrar com períodos longos no estudo. No caso de precisar usar alguma média ou indicador que necessite de muitas barras terem sido plotadas. Você NÃO DEVE usar os dados no replay ou simulador. Você os deve colocar como sendo barras prévias. Este é o primeiro ponto que você deve procurar entender.

Já sobre a segunda questão: Você imagina que o controle deslizante irá buscar um ponto específico. De fato ele faz isto, mas não da forma como você esteja querendo ou imaginando. Para entender melhor, veja os artigos anteriores, onde o controle foi implementado. Lá você verá em detalhes como de fato ele faz para buscar uma determinada posição. Mas nesta mesma questão, você está confundindo o uso do controle. Já que você também levanta a ideia de que ele talvez serva para modificar a velocidade na plotagem das barras. Isto de fato não acontece, de forma alguma. A tal plotagem que você nota, ao arrastar o controle e logo depois pressionar o botão de play, em isto em uma velocidade maior. É na verdade uma ilusão criada pela aplicação. Para mostrar como as barras foram criadas até o ponto em que você indicou para a simulação ou replay ter inicio, a fim de você poder executar o seu estudo.

Minha sugestão é: Leia com calma os artigos anteriores, e na dúvida pode postar como comentário. Pois ficará bem mais fácil de você entender o que de fato está acontecendo e como você pode usar a aplicação tendo uma boa experiência de uso. Qualquer dúvida pode perguntar nos comentários ... 😁👍

Philip Tweens
Philip Tweens | 18 ago 2023 em 06:15
daniel jose # :

Okay. Let's break it down as JACK would say...😁👍

Perhaps you are very confused about this application, or rather, perhaps you are hoping that this application will come to serve something, which in fact it was not intended to be used in principle. I'm not saying that it can't be used for something in particular, like for example a strategy tester. But this was not the initial objective for it to be implemented. 

About the first question: You might not have really understood how the replay / simulation will happen. Forget about the slider for a moment. When you press play on the system, it will fetch the data that has been loaded, either as ticks or bars, and will display them as bars on the chart, based on a time of 1 minute. This is independent of the timeframe you want to use. For this reason, the data, which must be in the file, must be thought of as bars of 1 minute. You should not look at file data as individual data. Because this application doesn't see them that way. It will always interpret bars even two hour bars as being 1 minute bars. Always .

If you are using bars, the application will automatically notice this, and will create a simulation so that each of the bars is approximately 1 minute long. Creating as many ticks as necessary for the values ​​​​to be correctly plotted on the chart. If the data present in the file are ticks, the system will record each of the ticks in the approximate interval that is defined between them. See previous articles to understand this. Such an interval can vary from a few milliseconds to several hours. But by doing this, anything that is in range will be treated as either an auction or a trading hold. Thus, if you use data with an interval of more than one day, that is, 24 hours, the application most likely will not be able to recognize the bars properly. This is if you use the slider to look for a new study point. Therefore, one should avoid using data with a time greater than one day.

Remember the application was thought to be used in a time equivalent to real time. In other words, short periods. To enter with long periods in the study. In case you need to use some average or indicator that needs many bars to be plotted. You MUST NOT use the data in the replay or simulator. You must place them as prebars. This is the first point you should try to understand.

Now about the second question: You imagine that the slider will look for a specific point. Indeed it does, but not in the way you might want or imagine. To better understand, see the previous articles, where the control was implemented. There you will see in detail how he actually does to seek a certain position. But in this same question, you are confusing the use of the control. Since you also raise the idea that it maybe serves to modify the speed in plotting the bars. This actually doesn't happen at all. Such a plot that you notice, when dragging the control and then pressing the play button, in this at a higher speed. It is actually an illusion created by the application. To show how the bars were created up to the point where you indicated that the simulation or replay should start,

My suggestion is: Read the previous articles calmly, and if in doubt, post them as a comment. Because it will be much easier for you to understand what is actually happening and how you can use the application having a good user experience. Any questions you can ask in the comments... 😁👍

Acho que você não entendeu o que eu quis dizer e provavelmente me expressei mal.
Eu entendo a função do controle deslizante. Coloquei os dados no Replay por um mês (cerca de 20 dias). No entanto, aproximei o pino do final do controle deslizante, mas apenas algumas barras foram desenhadas no primeiro dia, quando deveria ter passado pelo menos 15 dias antes de atingir o ponto desejado. Eu entendi errado??? Imagino que seja por causa do que você disse para não usar mais do que um dia de dados.
Em relação à velocidade de exibição das barras, queria que você me orientasse a alterar o sistema dessa maneira.
Obrigado pela sua resposta.
Daniel Jose
Daniel Jose | 18 ago 2023 em 10:31
Philip Tweens #:
Acho que você não entendeu o que eu quis dizer e provavelmente me expressei mal.
Eu entendo a função do controle deslizante. Coloquei os dados no Replay por um mês (cerca de 20 dias). No entanto, aproximei o pino do final do controle deslizante, mas apenas algumas barras foram desenhadas no primeiro dia, quando deveria ter passado pelo menos 15 dias antes de atingir o ponto desejado. Eu entendi errado??? Imagino que seja por causa do que você disse para não usar mais do que um dia de dados.
Em relação à velocidade de exibição das barras, queria que você me orientasse a alterar o sistema dessa maneira.
Obrigado pela sua resposta.

Para modificar a velocidade é muito simples. Basta você ir na classe C_Replay, procurar a função LoopEventOnTime. Nela existe uma chamada Sleep. É neste ponto em quem controlamos a velocidade de plotagem, quando estamos no modo play. Mas acredito que isto tenha sido adequadamente explicado nos artigos anteriores.

Philip Tweens
Philip Tweens | 18 ago 2023 em 13:30
Daniel Jose #:

Para modificar a velocidade é muito simples. Basta você ir na classe C_Replay, procurar a função LoopEventOnTime. Nela existe uma chamada Sleep. É neste ponto em quem controlamos a velocidade de plotagem, quando estamos no modo play. Mas acredito que isto tenha sido adequadamente explicado nos artigos anteriores.

Não, quero alterar a maneira como o controle deslizante se comporta. Em vez da posição do pino ser igual à posição de um ponto específico, ele representa a velocidade com que as barras são exibidas para o replay em tempo longo. Semelhante ao que vemos no testador de estratégia.
Obrigado.
Negociação automatizada em grade usando ordens pendentes de stop na Bolsa de Moscou (MOEX) Negociação automatizada em grade usando ordens pendentes de stop na Bolsa de Moscou (MOEX)
Uso da abordagem de negociação em grade com ordens pendentes de stop em um Expert Advisor usando a linguagem de estratégias de negociação MQL5 para o MetaTrader 5 na Bolsa de Valores de Moscou (MOEX). Ao negociar no mercado, uma das estratégias mais simples é uma grade de ordens projetada para "capturar" o preço de mercado.
Matrizes e vetores em MQL5: funções de ativação Matrizes e vetores em MQL5: funções de ativação
Neste artigo, descrevemos apenas um aspecto do aprendizado de máquina, em particular as funções de ativação. Em redes neurais artificiais, a função de ativação de neurônio calcula o valor de um sinal de saída com base nos valores de um sinal de entrada ou de um conjunto de sinais de entrada. Vamos mergulhar nos detalhes internos do processo.
Algoritmo de recompra: modelo matemático para aumentar a eficiência Algoritmo de recompra: modelo matemático para aumentar a eficiência
Neste artigo, usaremos o algoritmo de recompra como um guia para um entendimento mais profundo da eficiência dos sistemas de negociação e começaremos a trabalhar com os princípios gerais de aumentar a eficiência de negociação usando matemática e lógica, bem como aplicar os métodos mais inovadores para aumentar a eficiência no contexto de usar qualquer sistema de negociação.
Encapsulando modelos ONNX em classes Encapsulando modelos ONNX em classes
A programação orientada a objetos permite criar códigos mais compactos, fáceis de ler e modificar. Apresentamos um exemplo para três modelos ONNX.