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

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

MetaTrader 5Testador | 29 agosto 2023, 18:07
358 0
Daniel Jose
Daniel Jose

Introdução

No artigo anterior Desenvolvendo um sistema de Replay - Simulação de mercado (Parte 23): FOREX (IV), foi feito um bloqueio parcial do sistema de simulação. O motivo de tal bloqueio, é por que o sistema não conseguiria, lidar com volumes de negócio muito baixos. E ao tentar efetuar a simulação baseada na plotagem LAST, poderia acontecer de ele simplesmente travar tentando gerar a simulação. Isto acontece especialmente em momentos que o volume de negócios indicados na barra de 1 minuto for muito baixo. Para resolver este problema, irei mostrar como modificar a implementação de forma que ela irá seguir os princípios que foram usados na simulação baseada em uma plotagem BID. Este é muito comum no forex, dai o fato de este ser o quinto artigo de forex. Mas aqui não iremos de fato tratar de forex. Vamos tratar do sistema de bolsa.


Iniciando as mudanças na implementação

Para iniciar, vamos adicionar uma estrutura privativa na nossa classe. Isto por que existem dados, ou informações que são comuns em ambos os modos de simulação. Tanto para o LAST quanto para o BID. Estas partes em comum, que na verdade são valores, podem ser colocadas todas juntas. Assim nasce a seguinte estrutura:

struct st00
{
   bool    bHigh, bLow;
   int     iMax;
}m_Marks;

Esta é bem simples como  se pode notar. No entanto, é suficiente para que possamos trabalhar com um código um pouco melhor. Já que os valores comuns estarão em um mesmo local.

Feito isto, podemos fazer a nossa primeira modificação real no código.

inline int Simulation(const MqlRates &rate, MqlTick &tick[])
   {
      m_Marks.iMax = (int) rate.tick_volume - 1;
      m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high);
      m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low);
      Simulation_Time(rate, tick);
      if (m_IsPriceBID) Simulation_BID(rate, tick);
      else Simulation_LAST(rate, tick);
      else return -1;
      CorretTime(tick);

      return m_Marks.iMax;
   }

Aqui estamos retirando o bloqueio de simulação baseada na plotagem LAST, e adicionando um ponto de entrada para este tipo de simulação. Agora prestem atenção ao fato de que todo o funcionamento, irá se basear no sistema do forex. Sendo que a única diferença, aqui nesta rotina, é o fato de que estaremos separando uma simulação BID, de uma LAST. Mas a questão de randomização do tempo e a sua correção para ser utilizado pela classe C_Replay, é a mesma em ambos modos de simulação. Isto é uma coisa boa, já que se modificarmos um dos modos, o outro irá se beneficiar, pelo menos no que rege a parte do tempo entre os tickets. É claro que as mudanças feitas aqui, irão se refletir no código da plotagem BID. Mas é algo bem simples de se entender, então não entrarei em detalhes sobre isto.

Voltemos ao código que nos interessa. Uma vez que temos a chamada para a função de simulação de plotagem LAST, podemos ver os primeiros pontos desta chamada. Ou seja, o esqueleto interno da chamada. E este pode ser visto no fragmento abaixo:

inline void Simulation_LAST(const MqlRates &rate, MqlTick &tick[])
   {
      if (CheckViability_LAST(rate))
      {
      }else
      {
      }
      DistributeVolumeReal(rate, tick);
   }

Aqui estaremos fazendo duas coisas, que será comum dentro do trabalho com a plotagem LAST. A primeira é checar se podemos ou não usar o sistema de random walk, de forma a ter uma modelagem como já foi explicada em artigos anteriores. Quem não viu, dê uma olhada no artigo : Desenvolvendo um sistema de Replay - Simulação de mercado ( Parte 15 ): Nascimento do SIMULADOR ( V ) - RANDOM WALK.  A segunda, será distribuir todo o volume negociado entre os tickets possíveis de serem utilizados. Estas duas coisas serão, e deverão, sempre executadas quando tratamos de uma plotagem LAST.

Antes de ver como este esqueleto irá ser montado. Vamos dar uma olhada nas duas funções comuns a simulação baseadas no LAST. A primeira das funções é vista logo abaixo:

inline bool CheckViability_LAST(const MqlRates &rate)
   {
#define macro_AdjustSafetyFator(A) (A + (A * 1.4));
                                
      double  v0, v1, v2;
                                
      v0 = macro_AdjustSafetyFator(rate.high - rate.low);
      v1 = (rate.open - rate.low);
      v2 = (rate.high - rate.open);
      v0 += macro_AdjustSafetyFator(v1 > v2 ? v1 : v2);
      v1 = (rate.close - rate.low);
      v2 = (rate.high - rate.close);
      v0 += macro_AdjustSafetyFator(v1 > v2 ? v1 : v2);
      return ((int)(v0 / m_TickSize) < rate.tick_volume);
                                
#undef macro_AdjustSafetyFator
   }

Esta função irá verificar se podemos gerar um random walk, viável, dentro do espaço que temos para trabalhar. E como iremos fazer isto ?!?! Bem, a forma de fazer isto, é saber quantos ticks temos disponíveis para serem utilizados. Isto é informado pela barra que iremos trabalhar, e quantos ticks teremos que cobrir, e isto é calculado aqui na função.

O detalhe é que não iremos utilizar exatamente aquele valor que calculamos, para saber qual será a área a ser coberta. Isto por que, se for feito assim, o random walk, irá ter uma aparência errada, tendo um movimento muito previsível. Então para compensar esta diferença, efetuaremos um pequeno ajuste em cada um dos cálculos. Este ajuste é efetuado pela macro. Aqui estou definindo um área 30% maior como sendo um fator de segurança, isto para que o random walk seja criado de uma maneira adequada. Uma outra coisa, que você também deve ficar atento, é sempre utilizar a maior distância para efetivamente efetuar o cálculo. Já que durante a parte de randomização poderemos vim a ter que usar esta distância maior. Então já calculamos a coisa toda, baseando nesta possibilidade.

No final temos como resultado, se será ou não viável usar o random walk, ou se iremos utilizar um método de randomização puro. Mas isto será feito dentro do chamador, e não aqui.

A segunda função é vista a seguir:

inline void DistributeVolumeReal(const MqlRates &rate, MqlTick &tick[])
   {
      for (int c0 = 0; c0 <= m_Marks.iMax; c0++)
         tick[c0].volume_real = 1.0;
      for (int c0 = (int)(rate.real_volume - rate.tick_volume); c0 > 0; c0--)
         tick[RandomLimit(0, m_Marks.iMax)].volume_real += 1.0;                                  
   }

O que estamos fazendo aqui é distribuir de forma randômica, todo o volume que foi negociado na barra de 1 minuto. O primeiro laço faz uma distribuição simples, onde todos os ticks terão certamente pelo menos um volume mínimo inicial. Já o segundo laço faz exatamente a distribuição aleatória do volume restante. Assim ele não ficará concentrado em apenas um único ticket, apesar desta possibilidade existir, ela será bem menor fazendo a distribuição da forma indicada.

Estas partes já estavam presentes na versão apresentada nos artigos, onde o random walk foi implementado. Mas aqui estamos usando elas de uma forma mais modular, e o motivo é tentar reutilizar ao máximo o que será implementado.

Agora vamos pensar um pouco: Existem coisas que ainda são comuns entre uma simulação de BID e uma simulação de LAST. Estas coisas são a existência de um ponto de entrada, um ponto de saída, e a possibilidade de pontos extremos. Então novamente vamos tentar reaproveitar códigos já criados. Para fazer isto, precisaremos fazer uma mudança em um código, mostrado no artigo anterior. Este pode ser visto abaixo:

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

Primeiro vamos eliminar o nome antigo da função e dar um outro nome para ela. Depois adicionamos um teste dentro da função, de forma a saber se estaremos utilizando a plotagem BID ou a plotagem LAST. Agora poderemos utilizar a mesma função para produzir um ticket que poderá estar usando o BID ou o LAST, dependendo é claro, do que foi visto pelo sistema, ao observar o arquivo de barras de 1 minuto.

Como consequência desta mudança, precisaremos fazer mais duas mudanças. Lembre-se: Quero unir de forma simples a simulação de BID e a simulação de LAST, de maneira que apenas e somente as partes realmente exclusivas de cada uma, seja feita pelo método correto. Qualquer outra coisa, irá ser feita de uma maneira comum entre ambas. Assim temos a próxima mudança no código da classe de simulação:

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

Todas as partes riscadas foram retiradas do código, de forma que ele irá continuar a efetuar o seu trabalho, porém se você notar, precisamos destas partes riscadas, mas elas são comuns ao sistema de simulação que irá fazer a plotagem LAST, desta forma este código comum foi colocado na função que pode ser vista logo abaixo:

inline int Simulation(const MqlRates &rate, MqlTick &tick[])
   {
      m_Marks.iMax = (int) rate.tick_volume - 1;
      m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high);
      m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low);
      Simulation_Time(rate, tick);
      MountPrice(0, rate.open, rate.spread, tick);    
      if (m_IsPriceBID) Simulation_BID(rate, tick);
      else Simulation_LAST(rate, tick);
      if (!m_Marks.bLow) MountPrice(Unique(rate.high, tick), rate.low, rate.spread, tick);
      if (!m_Marks.bHigh) MountPrice(Unique(rate.low, tick), rate.high, rate.spread, tick);
      MountPrice(m_Marks.iMax, rate.close, rate.spread, tick);
      CorretTime(tick);

      return m_Marks.iMax;
   }

Opa. Agora sim. Sem fazer absolutamente nada. Apenas fazendo com que o código fosse reutilizado, de uma maneira mais inteligente, temos ambas simulações. Ou seja, agora temos a plotagem BID e a plotagem LAST. Os valores de entrada, de saída e os limites, sendo adicionados, caso não tenham sido colocados pela simulação randomizada. Ou seja com pouco trabalho conseguimos cobrir uma área bem mais ampla. E o nível de complexidade do código praticamente não mudou em nada. Desta forma, se você usar este código, como mostrado até o momento, irá conseguir trabalhar relativamente bem na plotagem BID. E já conseguirá jogar pelo menos a barra no gráfico, no caso da plotagem LAST. É bem verdade, que o valor da mínima, estará errado, isto por conta que ele será zero, mesmo ele sendo definido com o valor correto. Mas isto se deve ao fato, de que nos tickets não inicializados, tendo apenas o tempo definido, todos os valores LAST serão zero. Isto não seria problema, se não fosse o fato de já termos distribuído o volume negociado. Então vamos voltar para a nossa rotina que faz o trabalho de colocar os valores de preço LAST, em cada um dos tickets. Assim poderemos ter uma simulação com dados corretos.

Se você olhar a nossa rotina de simulação do preço LAST continua igual. Mas observando o código responsável por fazer a simulação BID, podemos pensar: Será que não daria para usar este mesmo código, para fazer a simulação do preço LAST, caso o número de ticks, seja menor que o necessário para cobrir uma execução do random walk ?!?! Pois bem, esta mesma questão também me veio a mente. E observando com atenção, precisaremos fazer apenas uma pequena mudança na rotina, que faz a simulação do BID. Mas para que a coisa não fique totalmente confusa, caso venhamos a fazer uma mudança futura no código, precisamos pensar bem como fazer a mudança aqui e agora. Observem que este procedimento que simula o BID, é chamada exatamente pela função vista pouco acima. Mas já que o conceito para simular o LAST será o mesmo, podemos tentar criar uma forma de manter a chamada acima intacta. Enquanto adaptamos o procedimento de simulação da plotagem BID, de forma que ele também consiga simular a plotagem LAST, no caso de não fazermos uso do random walk.

Pois bem, alguns podem me chamar de louco, mas eis como o código de simulação da plotagem BID, deverá ficar para conseguir também simular a plotagem LAST.

inline void Simulation_BID(const MqlRates &rate, MqlTick &tick[])
inline void Random_Price(const MqlRates &rate, MqlTick &tick[])
   {
      for (int c0 = 1; c0 < m_Marks.iMax; c0++)
      {
         MountPrice(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), (rate.spread + RandomLimit((int)(rate.spread | (m_Marks.iMax & 0xF)), 0)), tick);
         m_Marks.bHigh = (rate.high == (m_IsPriceBID ? tick[c0].bid : tick[c0].last)) || m_Marks.bHigh;
         m_Marks.bLow = (rate.low == (m_IsPriceBID ? tick[c0].bid : tick[c0].last)) || m_Marks.bLow;
         m_Marks.bHigh = (rate.high == tick[c0].bid) || m_Marks.bHigh;
         m_Marks.bLow = (rate.low == tick[c0].bid) || m_Marks.bLow;
      }
   }

Para que a coisa não fique confusa, durante as chamadas, tive que trocar o nome da função. Bem este é um pequeno custo frente ao que poderemos ganhar. Mas a coisa que realmente pega é aqui. Justamente estes dois pontos do código. Muita gente torce o nariz para este operador ternário, dizendo que ele é confuso. Mas este é um dos operadores vindos da linguagem C, que ajuda muito. Aqui nestes pontos, iremos verificar o tipo de plotagem que esta sendo feita, e ajustar o preço limite. Note que independentemente do tipo, a randomização é feita da mesma forma. Assim estamos unindo ambos os métodos de forma a criar o sistema adequado de simulação de BID ou de LAST.

Feitas as mudanças no código chamador, você já conseguirá ter uma simulação muito similar ao que pode ser visto e discutido no artigo Desenvolvendo um sistema de Replay - Simulação de mercado ( Parte 13 ): Nascimento do SIMULADOR ( III ). Mas ainda não temos a simulação random walk, sendo implementada pelo sistema. Isto por que, agora o nosso código esta conforme pode ser visto abaixo:

inline void Simulation_LAST(const MqlRates &rate, MqlTick &tick[])
   {
      if (CheckViability_LAST(rate))
      {
      }else Random_Price(rate, tick);
      DistributeVolumeReal(rate, tick);
   }

Ou seja ainda não estamos simulando um típico cenário onde o random walk é viável e plausível. Mas mesmo assim ainda quero fazer com que uma plotagem de preço BID, consiga em algum tipo de cenário utilizar o random walk, que também será utilizado pela plotagem de preço LAST. Será que isto é possível ?!?! Será que ainda podemos deixar a coisa mais interessante e consideravelmente sustentável, a ponto de que um mercado similar ao FOREX possa também usar o método random walk de forma a gerar os movimentos do preço ?!?! Pois a resposta para isto é SIM. De fato, é possível fazer isto. Antes mesmo de implementar o random walk para a plotagem de preço LAST. Para que isto possa ser feito, precisaremos mudar algumas coisas.

inline bool CheckViability(const MqlRates &rate)
   {
#define macro_AdjustSafetyFator(A) (int)(A + ceil(A * 1.7))
                                
      int i0, i1, i2;
                                
      i0 = macro_AdjustSafetyFator((rate.high - rate.low) / m_TickSize);
      i1 = (int)((rate.open - rate.low) / m_TickSize);
      i2 = (int)((rate.high - rate.open) / m_TickSize);
      i0 += macro_AdjustSafetyFator(i1 > i2 ? i1 : i2);
      i0 += macro_AdjustSafetyFator((i1 > i2 ? (rate.high - rate.close) : (rate.close - rate.low) / m_TickSize));

      return (i0 < rate.tick_volume);
                                
#undef macro_AdjustSafetyFator
   }

Este procedimento acima, é o mesmo que antes era utilizado para checar se um movimento random walk, seria ou não viável de ser criado. Mas por conta de alguns detalhe técnicos e fatores de segurança, ela sofreu algumas mudanças, de forma a não gerar falsos indícios, de que o movimento seria ou não viável. O motivo disto, é por este procedimento de checagem, não irá mais servir apenas a questão envolvida com o sistema de plotagem LAST. Ela também servirá para testar a viabilidade do random walk, na plotagem BID. De certa forma, isto seria fácil de fazer. Mas temos que tomar alguns cuidados. Para entender observe a figura 01.

Figura 01

Figura 01 - Cálculo do caminho mais longo possível

O que estamos fazendo na função acima, é justamente o que aparece na figura 01. Ou seja, calcular o caminho mais longo possível,  para gerar a barra de 1 minuto. No entanto, o calculo foi modificado de forma a otimizar esta busca, a fim de que a plotagem BID possa se beneficiar. Apesar do fator de segurança, que era 1.4 subiu para 1.7, o que dificulta muito para alguns ativos usar o random walk. Para saber qual o caminho mais longo, primeiro calculamos o valor da distancia entre o preço de abertura da barra, e os seus extremos, com esta informação em mãos podemos usar o valor maior na primeira parte do cálculo.  Depois usamos o outro valor de forma que o movimento cubra a barra de maneira mostrada na figura 01. No final fazemos um cálculo simples para verificar se é possível ou não usar o random walk.

Você pode estar pensando que teremos que mudar novamente o código da classe. E sim, teremos. Mas agora será de uma maneira mais agradável.

inline int Simulation(const MqlRates &rate, MqlTick &tick[])
   {
      m_Marks.iMax = (int) rate.tick_volume - 1;
      m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high);
      m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low);
      Simulation_Time(rate, tick);
      MountPrice(0, rate.open, rate.spread, tick);
      if (CheckViability(rate))
      {
      }else Random_Price(rate, tick);
      if (!m_IsPriceBID) DistributeVolumeReal(rate, tick);
      if (m_IsPriceBID) Random_Price(rate, tick);
      else Simulation_LAST(rate, tick);
      if (!m_Marks.bLow) MountPrice(Unique(rate.high, tick), rate.low, rate.spread, tick);
      if (!m_Marks.bHigh) MountPrice(Unique(rate.low, tick), rate.high, rate.spread, tick);
      MountPrice(m_Marks.iMax, rate.close, rate.spread, tick);
      CorretTime(tick);

      return m_Marks.iMax;
   }

As partes riscadas foram retiradas do código, e as partes em verde foram adicionadas. Você pode notar que agora existirá casos em que uma barra de 1 minuto, baseadas em um mercado similar ao FOREX, pode ter seu tickets sendo criados de uma forma similar, aos de um mercado similar ao da BOLSA e vice-versa. Fazendo com que o simulador consiga cobrir toda a gama de possíveis movimentos de mercado. Isto independentemente do volume de tickets que aconteceram. É bem verdade, que o código que irá criar o random walk, ainda não está presente no procedimento acima. Então vamos ver como este código será implementado, de forma a conseguir trabalhar com ambos tipos de plotagem. Pois agora a coisa irá ser somente em cima dele.


Implementando o Random Walk para preço BID e preço LAST

No tópico acima, você viu como fui implementando a classe C_Simulation, de maneira que fosse possível, ter um mesmo tipo de tratamento entre uma plotagem BID e uma plotagem LAST. Isto para que fosse possível gerar uma simulação, o mais adequada quanto fosse possível fazer. Conseguimos chegar em um ponto, em que tudo que precisamos, será implementar um procedimento que consiga trabalhar no formato random walk. Isto usando o mínimo de código, mas a questão aqui não será tão complicada. O que de fator iremos fazer será adaptar o que já foi feito em um outro artigo: Desenvolvendo um sistema de Replay - Simulação de mercado ( Parte 15 ): Nascimento do SIMULADOR ( V ) - RANDOM WALK. Por isto, não irei entrar em muitos detalhes aqui. Se você deseja saber mais detalhes de como o random walk, foi de fato implementado e como cheguei a ele. Veja artigo descrito informado. Aqui irei focar em adaptar aquele código a este novo sistema.

inline int RandomWalk(int In, int Out, const double Open, const double Close, double High, double Low, const int Spread, MqlTick &tick[], int iMode)
   {
      double vStep, vNext, price, vH = High, vL = Low;
      char i0 = 0;
                                
      vNext = vStep = (Out - In) / ((High - Low) / m_TickSize);
      for (int c0 = In, c1 = 0, c2 = 0; c0 <= Out; c0++, c1++)
      {
         price = (m_IsPriceBID ? tick[c0 - 1].bid : tick[c0 - 1].last) + (m_TickSize * ((rand() & 1) == 1 ? -1 : 1));
         price = (price > High ? price - m_TickSize : (price < Low ? price + m_TickSize : price));
         MountPrice(c0, price, (Spread + RandomLimit((int)(Spread | (m_Marks.iMax & 0xF)), 0)), tick);
         switch (iMode)
         {
            case 0:
               if (price == Close) return c0; else break;
            case 1:
               i0 |= (price == High ? 0x01 : 0);
               i0 |= (price == Low ? 0x02 : 0);
               vH = (i0 == 3 ? High : vH);
               vL = (i0 ==3 ? Low : vL);
               break;
            default: break;
         }
         if (((int)floor(vNext)) >= c1) continue;
         if ((++c2) <= 3) continue;
         vNext += vStep;
         if (iMode != 2)
         {
            if (Close > vL) vL = (i0 == 3 ? vL : vL + m_TickSize); else vH = (i0 == 3 ? vH : vH - m_TickSize);
         }else
         {
            vL = (((c2 & 1) == 1) ? (Close > vL ? vL + m_TickSize : vL) : (Close < vH ? vL : vL + m_TickSize));
            vH = (((c2 & 1) == 1) ? (Close > vL ? vH : vH - m_TickSize) : (Close < vH ? vH - m_TickSize : vH));
         }
      }
                                
      return Out;
   }

As mudanças feitas visam deixar o código mais simples em termos de estrutura. No entanto, o seu funcionamento permanece igual. Mas você pode notar aqui, que existe este ponto, onde fazemos a leitura do valor prévio para gerar o novo valor. Notem que ele esta se adaptando, ao que estará sendo plotado. Independente do tipo de plotagem que estará sendo usada. Já para a montagem do valor, usamos a nossa velha conhecida função, que foi gerada aqui neste artigo. Então a coisa se desenrola de forma bastante natural. Como foi dito, não irei entrar em detalhes de como este procedimento funciona. Já que ele foi explicado em outro artigo.

Agora vamos ver como ficou finalmente o nosso procedimento. Já na primeira tentativa de terminar esta fase da implementação, onde poderemos testar se a função responsável por gerar as chamadas, para executar a simulação dos tickets, com base nos dados das barras, consegue de fato fazer o trabalho esperado. Isto de maneira a cobrir tanto o sistema de plotagem BID, quanto a plotagem LAST. O código da função é visto logo abaixo na integra:

inline int Simulation(const MqlRates &rate, MqlTick &tick[])
   {
      int     i0, i1;
      bool    b0 = ((rand() & 1) == 1);
                                
      m_Marks.iMax = (int) rate.tick_volume - 1;
      m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high);
      m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low);
      Simulation_Time(rate, tick);
      MountPrice(0, rate.open, rate.spread, tick);
      if (CheckViability(rate))
      {
         i0 = (int)(MathMin(m_Marks.iMax / 3.0, m_Marks.iMax * 0.2));
         i1 = m_Marks.iMax - i0;
         i0 = RandomWalk(1, i0, rate.open, (b0 ? rate.high : rate.low), rate.high, rate.low, rate.spread, tick, 0);
         RandomWalk(i0, i1, (m_IsPriceBID ? tick[i0].bid : tick[i0].last), (b0 ? rate.low : rate.high), rate.high, rate.low, rate.spread, tick, 1);
         RandomWalk(i1, m_Marks.iMax, (m_IsPriceBID ? tick[i1].bid : tick[i1].last), rate.close, rate.high, rate.low, rate.spread, tick, 2);
	 m_Marks.bLow = m_Marks.bHigh = true;
      }else Random_Price(rate, tick);
      if (!m_IsPriceBID) DistributeVolumeReal(rate, tick);
      if (!m_Marks.bLow) MountPrice(Unique(rate.high, tick), rate.low, rate.spread, tick);
      if (!m_Marks.bHigh) MountPrice(Unique(rate.low, tick), rate.high, rate.spread, tick);
      MountPrice(m_Marks.iMax, rate.close, rate.spread, tick);
      CorretTime(tick);

      return m_Marks.iMax;
   }

O trecho em destaque, é responsável por criar o padrão de simulação do random walk na barra. Este mesmo processo já era utilizado antes, só que ele estava completamente embutido no código de geração. Mas aqui ele foi colocado em uma outra posição, de forma que pode ser melhor compreendido e analisado. Mesmo por programadores iniciantes. Se você notar e prestar atenção, irá ver que se o sistema de simulação irá detectar se é possível utilizar o random walk. Se for, ele o fará, se não for possível, ele irá usar um método alternativo. Isto de forma, que de qualquer modo teremos sempre a geração de um possível movimento ou deslocamento dos preços. Seja em um mercado similar ao forex. Seja um mercado similar a bolsa. Não importa. O sistema sempre irá tentar se adaptar de forma a gerar, a melhor simulação possível, tentando cobrir todos os pontos de preço, que ele conseguir, sem se desviar do que esta sendo indicado nas barras.

Você deve ficar atento a um outro detalhe que pode acontecer. Onde uma barra particular, não será capaz de usar o random walk para a simulação. Enquanto uma barra imediatamente subsequente, irá conseguir fazer uso do processo. Então pode acontecer de que em alguns pontos, o preço de desloque de forma mais harmoniosa e suave. Em outros ele sofra grandes deslocamentos de forma bastante brusca. Mas isto não se deve de fato, a um problema no sistema de simulação ou replay. Apenas ao fato de que o preço naquela barra deve ter tido necessidade de se deslocar muito rapidamente, fazendo assim com que o numero de negócios, não tenha sido tão grande, a ponto de que o movimento fosse simulado de uma forma mais suave. Usando para isto a implementação do random walk. A mesma coisa pode acontecer, caso tenhamos uma quantidade de negócios alta a ponto de que o random walk venha a ser usado. Isto não implica, que de fato o preço tenha se deslocado de forma suave. Talvez ele tenha feito isto de forma bastante abruta. Mas como a quantidade de tickets negociados permitiu a utilização do random walk, este foi utilizado na simulação. E isto, não representa de forma alguma, a realidade que esteve presente no mercado naquela barra especifica.

Você pode pensar que esta tudo muito lindo e muito maravilhoso. Que já conseguimos o que queríamos. Mas NÃO. Ainda não conseguimos, apesar do random walk, ser bastante utilizado quando o numero de negócios indicados na barra de 1 minuto for suficientemente grande. Ele não será de fato utilizado quando este numero for levemente abaixo do que seria necessário. E o pior de tudo, é que utilizar uma criação totalmente aleatória para gerar um movimento da barra de 1 minuto, quando a distancia entre o preço máximo e o preço mínimo, é relativamente próximo do valor informado no numero de tickets. Isto deixa o sistema de simulação muito estranho. Nestes casos, devemos voltar a considerar um modelo visto em um outro artigo desta serie, Desenvolvendo um sistema de Replay - Simulação de mercado ( Parte 11 ): Nascimento do SIMULADOR ( I ). Onde criamos um sistema que funcionaria montando um pivô dentro da barra.

A ideia de fazer isto, parece bastante adequada e plausível. Já que a intenção é gerar movimentos possíveis, e admissíveis. E não simplesmente jogar valores de preço de forma completamente arbitrária. O problema de fato, não é mais a questão do tempo, mas sim uma questão do valor indicado no preço. O fato de usar uma rotina que simplesmente gerar valores, sem considerar nenhuma lógica e em momentos, que um negociante mais experiente, veria alguma lógica no movimento do preço, é muito desmotivador. Mas de fato existe uma solução para isto. Ela irá unir de alguma forma, tudo o que foi visto deste a parte 11 desta sequencia, até o devido momento. Apesar de não parecer claro a primeira vista, para aqueles que estão começando a estudar programação. Ela é bastante clara para os com mais experiência na área. Então não iremos criar de fato uma nova rotina ou procedimento de simulação. O que iremos fazer é gerar um movimento um pouco menos suave, em alguns momentos e mais suave em outros. Mas esta escolha deverá ser feita pelo próprio simulador. Você deve entender que contamos apenas e somente com 5 dados para fazer esta escolha, e estes dados são: Preço de abertura; Preço de fechamento; Preço Máximo alcançado pela barra; Preço Mínimo alcançado e o Volume de tickets. Temos e precisamos apenas disto, para fazer a escolha, de quão suave o movimento deverá de fato ser. Mas aqui não irei de fato propor uma solução final, e definitiva. Estarei apenas mostrando um de tantos caminhos, que poderemos tomar a fim de criar e gerar um movimento dentro da barra de 1 minuto.


Usando o Random Walk em vários cenários - Seguindo o caminho do menor esforço

Como foi tido no final do tópico anterior. Temos sempre que tentar criar um método, onde existe algum tipo de lógica no sistema. Usar pura e exclusivamente a randomização não gera bons resultados. Mesmo que estejamos fazendo todo o trabalho dentro de uma barra de 1 minuto e venhamos a utilizar o sistema em um tempo gráfico bem maior, por exemplo 10 ou 15 minutos. O ideal é que os movimentos não sejam constantemente bruscos, e de um extremo a outro logo de inicio. O melhor é que ele se desenhe aos poucos e desta forma, você irá imaginar que ele esta sendo feito de maneira aleatória. Quando na verdade, tudo que esta sendo feito, são cálculos matemáticos simples, de forma a gerar um movimento relativamente complicado, quando observamos ele de forma grosseira. Este é um dos princípios de movimentos estocásticos.

Para tentar fazer, com que as coisas fluam de maneira um pouco mais elaborada e suave. Será preciso que venhamos a remover algumas das funções existentes. E criar algumas regras para poder direcionar o movimento de alguma forma. É importante que você não tente de fato conduzir o movimento, em uma direção. Você deve criar as regras para que o MetaTrader 5, faça a condução da maneira que ele achar melhor. Para fazer isto, precisaremos primeiro mudar o código do procedimento de random walk. O novo código mostrando onde e o que foi mudado, pode ser visto logo abaixo:

inline int RandomWalk(int In, int Out, const double Open, const double Close, double High, double Low, const int Spread, MqlTick &tick[], int iMode, int iDesloc)
   {
      double vStep, vNext, price, vH = High, vL = Low;
      char i0 = 0;
                                
      vNext = vStep = (Out - In) / ((High - Low) / m_TickSize);
      for (int c0 = In, c1 = 0, c2 = 0; c0 <= Out; c0++, c1++)
      {
         price = (m_IsPriceBID ? tick[c0 - 1].bid : tick[c0 - 1].last) + (m_TickSize * ((rand() & 1) == 1 ? -1 : 1));
         price = (price > vH ? price - m_TickSize : (price < vL ? price + m_TickSize : price));                                  
         price = (m_IsPriceBID ? tick[c0 - 1].bid : tick[c0 - 1].last) + (m_TickSize * ((rand() & 1) == 1 ? -iDesloc : iDesloc));
         price = (price > vH ? vH : (price < vL ? vL : price));
         MountPrice(c0, price, (Spread + RandomLimit((int)(Spread | (m_Marks.iMax & 0xF)), 0)), tick);
         switch (iMode)
         {
            case 1:
               i0 |= (price == High ? 0x01 : 0);
               i0 |= (price == Low ? 0x02 : 0);
               vH = (i0 == 3 ? High : vH);
               vL = (i0 ==3 ? Low : vL);
               break;
            case 0:
               if (price == Close) return c0;
            default:
               break;
         }
         if (((int)floor(vNext)) >= c1) continue; else if ((++c2) <= 3) continue;
         vNext += vStep;
         vL = (iMode != 2 ? (Close > vL ? (i0 == 3 ? vL : vL + m_TickSize) : vL) : (((c2 & 1) == 1) ? (Close > vL ? vL + m_TickSize : vL) : (Close < vH ? vL : vL + m_TickSize)));
         vH = (iMode != 2 ? (Close > vL ? vH : (i0 == 3 ? vH : vH - m_TickSize)) : (((c2 & 1) == 1) ? (Close > vL ? vH : vH - m_TickSize) : (Close < vH ? vH - m_TickSize : vH)));
         if (iMode == 2)
         {
            vL = (((c2 & 1) == 1) ? (Close > vL ? vL + m_TickSize : vL) : (Close < vH ? vL : vL + m_TickSize));
            vH = (((c2 & 1) == 1) ? (Close > vL ? vH : vH - m_TickSize) : (Close < vH ? vH - m_TickSize : vH));
         }else
         {
            if (Close > vL) vL = (i0 == 3 ? vL : vL + m_TickSize); else vH = (i0 == 3 ? vH : vH - m_TickSize);
         }                                       
      }
                                
      return Out;
   }

As partes riscadas foram substituídas pelas partes destacadas em verde. Sei que pode não parecer muita diferença. Mas acreditem, agora poderemos fazer muito mais do que era possível antes. Pois anteriormente, o movimento era sempre de ticket em ticket, não existia nenhum tipo de " GAP " entre os tickets. Por conta disto, era exigido um numero muito grande de negócios, para que o sistema de fato conseguisse fazer com que o random walk funciona-se, de forma a ter um movimento o mais suave quanto fosse possível ser gerado. Mas quando existe a possibilidade de se gerar GAP, dentro da barra de 1 minuto, a quantidade de negócios exigidos caem, e caem bastante. Você poderá experimentar isto colocando o sistema para executar testes com diferentes parâmetros de barra de 1 minuto e com volumes baixos e altos. E irá ver que graficamente, o movimento gerado pelo random walk, irá se adaptar conforme a necessidade, de maneira que os 4 valores principais sejam de fato atingidos. Estes valores são: A abertura, o fechamento, a máxima e a mínima da barra. O que acontece ali, entre estes valores será definido pelo random walk. Mas existe um detalhe importante aqui. Este detalhe, reside na função responsável por chamar o random walk. E esta função é vista logo abaixo:

inline int Simulation(const MqlRates &rate, MqlTick &tick[])
   {
      int     i0, i1, i2;
      bool    b0;
                                
      m_Marks.iMax = (int) rate.tick_volume - 1;
      m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high);
      m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low);
      Simulation_Time(rate, tick);
      MountPrice(0, rate.open, rate.spread, tick);
      if (CheckViability(rate))
      if (m_Marks.iMax > 10)
      {
         i0 = (int)(MathMin(m_Marks.iMax / 3.0, m_Marks.iMax * 0.2));
         i1 = m_Marks.iMax - i0;
         i2 = (int)(((rate.high - rate.low) / m_TickSize) / i0);
         i2 = (i2 == 0 ? 1 : i2);
         b0 = (m_Marks.iMax >= 1000 ? ((rand() & 1) == 1) : (rate.high - rate.open) < (rate.open - rate.low));
         i0 = RandomWalk(1, i0, rate.open, (b0 ? rate.high : rate.low), rate.high, rate.low, rate.spread, tick, 0, i2);
         RandomWalk(i0, i1, (m_IsPriceBID ? tick[i0].bid : tick[i0].last), (b0 ? rate.low : rate.high), rate.high, rate.low, rate.spread, tick, 1, i2);
         RandomWalk(i1, m_Marks.iMax, (m_IsPriceBID ? tick[i1].bid : tick[i1].last), rate.close, rate.high, rate.low, rate.spread, tick, 2, i2);
         m_Marks.bHigh = m_Marks.bLow = true;
      }else Random_Price(rate, tick);
      if (!m_IsPriceBID) DistributeVolumeReal(rate, tick);
      if (!m_Marks.bLow) MountPrice(Unique(rate.high, tick), rate.low, rate.spread, tick);
      if (!m_Marks.bHigh) MountPrice(Unique(rate.low, tick), rate.high, rate.spread, tick);
      MountPrice(m_Marks.iMax, rate.close, rate.spread, tick);
      CorretTime(tick);

      return m_Marks.iMax;
   }

Esta função mostra, que agora não mais iremos testar a viabilidade do random walk, da forma como estivemos fazendo durante todo este artigo. Agora o teste será feito inteiramente aqui. E por mais estranho que possa parecer, o sistema irá tentar gerar um random walk, a partir de um volume mínimo de 10 negócios. Se o volume for igual ou menor que este valor, a randomização pura será utilizada. Isto por conta que ela neste tipo de cenário, é mais eficaz do que o random walk. Mas se você prestou atenção na explicação, notou que falei sobre o fato de que o random walk, agora usa e criará, gaps dentro da barra de 1 minuto. Estes gaps, são garantidos por este cálculo aqui. Mas devemos garantir que pelo menos o valor de 1 tick, seja enviado para o random walk. Caso contrário ele não conseguirá sair do lugar.

Como nem tudo são flores. Precisamos de mais um segundo controle, para que a coisa de fato aconteça no random walk. Esta coisa é garantida por este teste daqui. Este é bastante curioso, podendo ter seu valor modificado conforme o necessário. Mas caso o volume de negócios seja superior a 1000 em 1 minuto, podemos deixar o sistema de simulação, escolher um caminho para ser seguido. Este caminho pode ser indo primeiro em direção a máximo ou em direção a mínima. Isto será escolhido internamente e de forma totalmente randomizada. Mas no entanto, se este volume for inferior ao indicado, iremos forçar, por assim disser, que o random walk, inicie indo em uma determinada direção. Seja em direção a máxima, ou em direção a mínima. Mas neste caso, diferente da primeira modalidade, aqui quem irá indicar se a simulação irá iniciar primeiramente na direção da máxima ou da mínima, é o próprio valor indicado na barra de 1 minuto. Isto por conta que se o preço abrir muito perto da máxima, é mais simples para o random walk, seguir primeiramente nesta direção. A mesma coisa se o valor começar perto da mínima. Pode parecer meio confuso e estranho, mas o que estou fazendo aqui, é que usando o caminho do menor esforço, ou seja, é mais simples, ir até o ponto que está mais próximo, do que ir até o que esta mais distante.

Este método do menor esforço, é muito bom para resolver problemas quando a quantidade de saltos que teremos que dar, é bem menor do que a distancia total que precisaremos percorrer. Isto caso tomemos o caminho errado. Por conta justamente deste calculo em sim, é que algumas partes vista neste artigo, não estarão presentes no anexo presente no final deste mesmo artigo. Para que vocês tenham uma noção, de como o sistema conseguiu trabalhar bem ao gerar as simulações. Abaixo temos duas figuras, onde uma representa o gráfico utilizando dados reais presentes no arquivo de tickets. E outra figura pode-se ver o resultado da simulação efetuada, por este sistema onde fazemos o uso da busca pelo menor esforço.

Figura 02

Figura 02 - Gráfico gerado usando dados reais


Figura 03

Figura 03 - Gráfico gerado usando dados do simulador

Apesar de parecer serem os mesmo gráfico, eles não são. Observe com atenção,  a fonte é diferente como destacado, em cada uma das figuras. Você poderá experimentar fazer a mesma coisa, pois os dados usados estão presentes no anexo, e trata-se o ativo EURUSD, ou seja um par de moedas presentes no mercado de FOREX. Com isto consegui de fato mostrar, que é possível usar o mesmo método de simulação usado em uma plotagem LAST, na simulação de uma ativo que utiliza a plotagem BID. Mas para que você também perceba que é possível fazer isto, bastará usar os dados que estou disponibilizando no anexo, e testar você mesmo como o sistema esta funcionando.


Conclusão

Lembre-se de que este, é o último passo antes de começarmos de fato a tornar o nosso sistema de replay / simulador funcional. No próximo artigo, veremos mais alguns poucos ajustes que precisaremos fazer antes de começar a mexer com outros detalhes além do próprio serviço de replay / simulação. Para que vocês possam ver como o sistema funciona, caso estejam na duvida sobre o real funcionamento entre a simulação e o replay.

Uma nota importante sobre os arquivos em anexo: Por conta do tamanho dos arquivos de dados, principalmente sobre os tickets reais, nos ativos futuros. Você terá acesso, a 4 arquivos, cada um ligado de alguma forma a um ativo ou mercado especifico. O arquivo principal, contém o código fonte de todo o sistema até a fase atual de desenvolvimento do mesmo. Todos os arquivos devem ser baixados e colocados no diretório informado pelo editor do MQL5. Desta forma a estrutura irá se manter intacta e o sistema poderá ser usado com os arquivos de demonstração presentes no anexo. Isto da mesma forma que foi feito nos vídeos acima.

Arquivos anexados |
Files_-_BOLSA.zip (1358.24 KB)
Files_-_FOREX.zip (3743.96 KB)
Files_-_FUTUROS.zip (11397.51 KB)
Redes neurais de maneira fácil (Parte 42): Procrastinação do modelo, causas e métodos de resolução Redes neurais de maneira fácil (Parte 42): Procrastinação do modelo, causas e métodos de resolução
A procrastinação de modelos no contexto do aprendizado por reforço pode ser causada por vários motivos, e a solução desse problema requer medidas apropriadas. Este artigo discute algumas das possíveis causas da procrastinação do modelo e métodos para superá-las.
Redes neurais de maneira fácil (Parte 41): Modelos Hierárquicos Redes neurais de maneira fácil (Parte 41): Modelos Hierárquicos
Este artigo descreve modelos hierárquicos de aprendizado que propõem uma abordagem eficaz para resolver tarefas complexas de aprendizado de máquina. Os modelos hierárquicos consistem em vários níveis, cada um responsável por aspectos diferentes da tarefa.
Redes neurais de maneira fácil (Parte 43): Dominando habilidades sem função de recompensa Redes neurais de maneira fácil (Parte 43): Dominando habilidades sem função de recompensa
O problema com o aprendizado por reforço é a necessidade de definir uma função de recompensa, que pode ser complexa ou difícil de formular, porém abordagens baseadas no tipo de ação e na exploração do ambiente que permitem que as habilidades sejam aprendidas sem uma função de recompensa explícita estão sendo exploradas para resolver esse problema.
Redes neurais de maneira fácil (Parte 40): Abordagens para usar Go-Explore em uma grande quantidade de dados Redes neurais de maneira fácil (Parte 40): Abordagens para usar Go-Explore em uma grande quantidade de dados
Neste artigo, discutiremos a aplicação do algoritmo Go-Explore ao longo de um período de treinamento prolongado, uma vez que uma estratégia de seleção aleatória de ações pode não levar a uma passagem lucrativa à medida que o tempo de treinamento aumenta.