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

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

MetaTrader 5Testador | 31 julho 2023, 14:58
347 0
Daniel Jose
Daniel Jose

Introdução

No artigo anterior Desenvolvendo um sistema de Replay - Simulação de mercado (Parte 21): FOREX (II) praticamente ficamos apenas resolvendo os problemas do sistema. Isto no que tange e diz respeito ao arquivo que configura o replay / simulação. Mas por conta que ele já continha bastante coisas a serem assimiladas por quem esteja estudando e acompanhando os artigos do sistema, afim de aprender a também construir seus próprios programas. Foi decidido encerrar o artigo, quando o sistema de configuração já estava em um estágio que nos permite trabalhar tranquilamente por um bom tempo.

Mas se você vem testando o conteúdo presente nos anexo, deve ter notado que apesar do sistema de replay / simulação, estar trabalhando relativamente bem. De forma bastante estável no que diz respeito ao mercado de bolsa, e isto já a algum tempo, o mesmo não pode ser dito com relação ao mercado similar ao forex. E aqui não estou me referindo apenas e somente ao forex em si. Mas todo e qualquer ativo que segue os mesmos conceitos de plotagem de preços feitos no forex, ou seja o uso do valor de BID como valor principal.

Para quem ainda não entendeu a diferença entre o mercado de bolsa e o de forex, apesar de este já ser o terceiro artigo em que estou abordando isto. Devo deixar claro, que a grande diferença, é o fato de que no forex não existe, ou melhor, não nos é informado algumas coisas a respeito do que aconteceu de fato na negociação. Apesar de isto parecer relacionado apenas ao forex, não estou me referindo apenas a ele. Mas por conta que nele temos um método próprio de se obter algumas das informações e o modelo de operar é totalmente diferente do modelo de bolsa. Acho mais simples fazer este distinção desta maneira. Mas tudo que estiver relacionado nestes artigos, ao forex, você deverá entender, como estando relacionado a qualquer tipo de mercado, onde a forma de plotagem é via o valor BID. Diferente do que acontece no mercado de bolsa, onde o valor é o LAST.

Então ao conseguir cobrir o mercado de forex, da mesma forma que o mercado de bolsa, já vem sendo coberto pelo sistema de replay / simulação. Teremos a capacidade de fazer um replay ou simulação de qualquer tipo de mercado, independentemente de qual ele seja. E uma das coisas que ainda estão nos dando problemas no sistema, é o fato de que se você desligar a visualização da criação das barras, que estão sendo criadas pelo replay ou simulador, o gráfico irá ficar errado. Já enfrentamos este problema lá atrás, ao fazer a mesma coisa quando ainda estamos desenvolvendo o sistema de forma a cobrir o mercado de bolsa. Mas já que no forex a plotagem é diferente, e o preço segue o BID, o sistema atual não consegue lidar com este problema de maneira adequada. E ao tentar deslocar a posição de estudo para um outro ponto, quando o sistema de visualização das barras esta desligado, todos os indicadores ficarão incorretos, já que não haverá nenhuma barra entre a antiga posição e a nova. Então vamos começar este artigo corrigindo este problema.


Corrigindo o sistema de posicionamento rápido

Para corrigir este problema teremos que fazer algumas mudanças no código, não por que ele esteja errado, mas por que ele não consegue trabalhar com a plotagem BID. Para ser mais preciso na explicação, o fato é que durante a leitura dos tickets e a conversão deles, ainda na classe C_FileTicks, em barras de 1 minuto, para serem depois usados durante o processo de posicionamento rápido. Não está sendo adequado. Já que não podemos plotar valores de preço baseados em BID. Para entender o motivo disto, veja o código responsável por fazer esta conversão. Este é visto logo abaixo:

inline bool ReadAllsTicks(const bool ToReplay)
    {
#define def_LIMIT (INT_MAX - 2)
#define def_Ticks m_Ticks.Info[m_Ticks.nTicks]

        string   szInfo;
        MqlRates rate;

        Print("Carregando ticks de replay. Aguarde...");
        ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
        m_Ticks.ModePlot = PRICE_FOREX;
        while ((!FileIsEnding(m_File)) && (m_Ticks.nTicks < def_LIMIT) && (!_StopFlag))
        {
            ArrayResize(m_Ticks.Info, m_Ticks.nTicks + 1, def_MaxSizeArray);
            szInfo = FileReadString(m_File) + " " + FileReadString(m_File);
            def_Ticks.time = StringToTime(StringSubstr(szInfo, 0, 19));
            def_Ticks.time_msc = (def_Ticks.time * 1000) + (int)StringToInteger(StringSubstr(szInfo, 20, 3));
            def_Ticks.bid = StringToDouble(FileReadString(m_File));
            def_Ticks.ask = StringToDouble(FileReadString(m_File));
            def_Ticks.last = StringToDouble(FileReadString(m_File));
            def_Ticks.volume_real = StringToDouble(FileReadString(m_File));
            def_Ticks.flags = (uchar)StringToInteger(FileReadString(m_File));
            m_Ticks.ModePlot = (def_Ticks.volume_real > 0.0 ? PRICE_EXCHANGE : m_Ticks.ModePlot);
            if (def_Ticks.volume_real > 0.0)
            {
                ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
                m_Ticks.nRate += (BuiderBar1Min(rate, def_Ticks) ? 1 : 0);
                m_Ticks.Rate[m_Ticks.nRate] = rate;
            }
            m_Ticks.nTicks++;
        }
        FileClose(m_File);
        if (m_Ticks.nTicks == def_LIMIT)
        {
            Print("Excesso de dados no arquivo de tick.\nNão é possivel continuar...");
            return false;
        }
        return (!_StopFlag);
#undef def_Ticks
#undef def_LIMIT
    }

Observem que para a rotina que faz a criação das barras ser chamada é preciso que exista um volume negociado. Este volume somente acontece quando o valor do último preço for modificado, no caso da modificação do valor de BID, este volume será sempre zero, ou seja a rotina não é chamada. Então a primeira coisa a ser feita é retirar este código daqui, isto por conta que não sabemos de fato se iremos trabalhar, durante a leitura dos tickets, com uma modelagem BID ou LAST. Desta forma esta rotina acima será modificada ficando conforme mostrado abaixo:

inline bool ReadAllsTicks(const bool ToReplay)
    {
#define def_LIMIT (INT_MAX - 2)
#define def_Ticks m_Ticks.Info[m_Ticks.nTicks]

        string   szInfo;
            
        Print("Carregando ticks de replay. Aguarde...");
        ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
        m_Ticks.ModePlot = PRICE_FOREX;
        while ((!FileIsEnding(m_File)) && (m_Ticks.nTicks < def_LIMIT) && (!_StopFlag))
        {
            ArrayResize(m_Ticks.Info, m_Ticks.nTicks + 1, def_MaxSizeArray);
            szInfo = FileReadString(m_File) + " " + FileReadString(m_File);
            def_Ticks.time = StringToTime(StringSubstr(szInfo, 0, 19));
            def_Ticks.time_msc = (def_Ticks.time * 1000) + (int)StringToInteger(StringSubstr(szInfo, 20, 3));
            def_Ticks.bid = StringToDouble(FileReadString(m_File));
            def_Ticks.ask = StringToDouble(FileReadString(m_File));
            def_Ticks.last = StringToDouble(FileReadString(m_File));
            def_Ticks.volume_real = StringToDouble(FileReadString(m_File));
            def_Ticks.flags = (uchar)StringToInteger(FileReadString(m_File));
            m_Ticks.ModePlot = (def_Ticks.volume_real > 0.0 ? PRICE_EXCHANGE : m_Ticks.ModePlot);
            m_Ticks.nTicks++;
        }
        FileClose(m_File);
        if (m_Ticks.nTicks == def_LIMIT)
        {
            Print("Excesso de dados no arquivo de tick.\nNão é possivel continuar...");
            return false;
        }
        return (!_StopFlag);
#undef def_Ticks
#undef def_LIMIT
    }

E por que estou fazendo isto ?!?! O motivo é que apenas e somente no final da leitura total e completa do arquivo, é que saberemos se a plotagem será feita usando o BID ou LAST presente nos tickets. E quem garante isto é esta linha em particular. Mas ela somente será válida quando a leitura do arquivo for finalizada. Bem, mas então quando iremos invocar esta conversão de tickets em barras para ser utilizadas durante a fase de plotagem ?!?! Pelo menos por enquanto, a ideia não será esta. Assim a rotina que fará isto, é vista logo a seguir:

datetime LoadTicks(const string szFileNameCSV, const bool ToReplay = true)
    {
        int      MemNRates,
                 MemNTicks;
        datetime dtRet = TimeCurrent();
        MqlRates RatesLocal[];
        
        MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate);
        MemNTicks = m_Ticks.nTicks;
        if (!Open(szFileNameCSV)) return 0;
        if (!ReadAllsTicks(ToReplay)) return 0;
        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;
    };

Não podemos invocar a rotina de conversão dos tickets em qualquer lugar nesta rotina acima. Isto por que em alguns momentos iremos precisar que os tickets tenham sido convertidos em barras, e o motivo disto é que estaremos utilizando um arquivo de tickets reais como sendo um arquivo de barras previas de 1 minuto. Isto pode ser notado ao observar a presença da rotina CustomRatesUpdate no código acima. Por conta disto temos que chamar o procedimento de conversão antes que esta chamada a CustomRatesUpdate de fato aconteça. Mas se você olhar a rotina de conversão verá que ela não é adequada para ser usada no código acima. A rotina original de conversão pode ser vista logo abaixo:

inline bool BuiderBar1Min(MqlRates &rate, const MqlTick &tick)
    {
        if (rate.time != macroRemoveSec(tick.time))
        {
            rate.real_volume = 0;
            rate.tick_volume = 0;
            rate.time = macroRemoveSec(tick.time);
            rate.open = rate.low = rate.high = rate.close = tick.last;
        
            return true;
        }
        rate.close = tick.last;
        rate.high = (rate.close > rate.high ? rate.close : rate.high);
        rate.low = (rate.close < rate.low ? rate.close : rate.low);
        rate.real_volume += (long) tick.volume_real;
        rate.tick_volume += (tick.last > 0 ? 1 : 0);

        return false;
    }

Vejam como ela é totalmente inadequada de ser utilizada onde precisamos. Então teremos que criar um outro procedimento para fazer a conversão. Mas além disto temos um outro problema. Como saber onde deverá começar a conversão! Lembre-se que a chamada pode acontecer em diversos momentos diferentes, e em cada um deles podemos ter necessidades diferentes. Pois em uma das chamadas podemos de fato estar carregando tickets a serem utilizados no replay. Enquanto em outra chamada poderemos estar carregando tickets que deverão ser descartados logo depois, por serem usados como barras previas, Então vejam que a coisa em si tem que ser bem pensada, senão você acabará entrando em um beco sem saída.

Mas tudo isto será muito mais simples e fácil de ser feito, se você analisar as mudanças que terão que ser executadas e as implementar seguindo exatamente o plano traçado. Desta forma começaremos modificado o procedimento chamador, depois iremos nos preocupar com a rotina de conversão. O novo procedimento chamador, pode ser vista logo abaixo:

datetime LoadTicks(const string szFileNameCSV, const bool ToReplay = true)
    {
        int      MemNRates,
                 MemNTicks;
        datetime dtRet = TimeCurrent();
        MqlRates RatesLocal[];
        
        MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate);
        MemNTicks = m_Ticks.nTicks;
        if (!Open(szFileNameCSV)) return 0;
        if (!ReadAllsTicks(ToReplay)) return 0;
        BuiderBar1Min(MemNTicks);
        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;
    };

Vejam como resolvemos a primeira parte do nosso problema. Ao colocar esta linha, estaremos garantindo que teremos a conversão dos tickets em barras de 1 minuto antes da possível chamada a CustomRatesUpdate. Ao mesmo tempo que informamos a rotina de conversão onde o processo de conversão deverá ser iniciado. O fato de colocar a chamada da rotina de conversão aqui, e não na rotina de leitura, é justamente o fato de querer evitar adicionar uma variável aparentemente desnecessária na rotina de leitura. Sendo que aqui já temos acesso a variável que precisamos, nos informando o range que deveremos trabalhar. Então, está certo, podemos desta forma passar para a implementação da rotina que irá converter os tickets em barras de 1 minuto. Devemos fazer isto, de uma maneira que ela funcione tanto para um mercado do tipo FOREX, quanto para um mercado do tipo BOLSA. Parece complicado, não é mesmo ??? Pois novamente se você não planejar o que irá fazer, poderá entrar em um loop de codificação que vai acabar fazendo você desistir de tentar implementar o código adequado.

Aqui quero não lhes dar uma solução. Quero que aprendam a pensar de forma a conseguir chegar em uma solução, que seja adequada ao que você estiver de fato querendo fazer. Então vamos pensar o seguinte: O procedimento original de conversão já estava conseguindo transformar um preço LAST em uma barra de 1 minuto. Pois bem, o que precisamos é adicionar um laço naquele procedimento, de forma a ler todos os tickets que foram lidos no arquivo. O ponto inicial deste laço nos é informado pelo chamador, e o ponto final será o último ticket lido. Até ai tudo bem. Mas precisamos também caso o sistema detecte que estamos utilizando uma plotagem baseadas no valor do BID, que este fique no lugar do preço LAST, que é originalmente usado. Desta forma iremos conseguir converter sem grande trabalho o valor BID em uma barra de 1 minuto. Legal não é mesmo. E esta é a ideia a ser implementada. E o resultado da implementação, é o procedimento visto logo a seguir:

inline void BuiderBar1Min(const int iFirst)
    {
        MqlRates rate;
        double   dClose = 0;
        
        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;
                    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) continue;
                    break;
            }
            if (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 = 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 += (rate.tick_volume == 0 ? 1 : 0))] = rate;
        }
    }

A rotina original esta toda em verde, para que você consiga notar onde ela esta presente. Já estes códigos são a parte nova da rotina. Notem que esta parte é responsável apenas por fazer a troca do LAST pelo BID, ou vice-versa de forma que o preço de fechamento seja adequado para poder produzir a barra de 1 minuto. E o laço do qual eu mencionei na explicação do que deveria ser feito é visto neste ponto. Apesar desta rotina acima resolver grande parte dos nossos problemas. Ela não esta resolvendo o problema do volume de tickets. Isto no caso de estarmos usando uma plotagem do tipo BID. Para resolver este problema, teremos que modificar ligeiramente a rotina acima, de forma que o código final ficará conforme mostrado abaixo:

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

Neste código que agora sim, resolve o problema do volume de tickets. Tivemos que adicionar uma nova variável. Esta tem o seu valor definido neste local, onde testamos se estaremos adicionando ou não uma nova barra. Mas ela somente é usada neste ponto, de forma que saberemos se estaremos ou não adicionando uma nova barra ao sistema.

Agora um detalhe: Quando usamos o modo de plotagem por LAST, para que o volume de tickets seja correto, devemos iniciar o contador em zero. Mas quando usamos a plotagem por BID devemos iniciar o valor em um. Caso contrário teremos dados incorretos no volume de tickets, e isto é conseguido ao se executar este código.

Detalhes, detalhes. Mas se você não tomar os devidos cuidados, eles irão acabar por te matar.


Corrigindo o volume de tickets

Você pode estar achando que o sistema já não tem mais nada de errado. Mas existem falhas ainda para resolver, e entre ela, esta a questão do volume. No tópico anterior, corrigimos o volume de tickets, a ser informado pelo sistema no caso de fazermos uma mudança rápida da posição que estaremos analisando. Mas no caso de você executar o replay ou simulação, sem mudar rapidamente a posição, a informação sobre o volume irá ficar errada.

Não que o código esteja com defeito, pelo contrário. Caso você esteja usando o sistema para fazer um replay ou simulação, de dados de um ativo que utiliza o valor LAST como sendo o preço de plotagem, o volume de tickets informados será correto. Mas no caso de usar o preço BID, como acontece no FOREX, este volume estará incorreto. Então precisamos resolver este problema, de forma que o volume informado seja o correto. Para entender onde está o problema, veja o código responsável por fazer tal contabilidade:

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

        bool    bNew;
        MqlTick tick[1];
        static double PointsPerTick = 0.0;

        if (bNew = (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time)))
        {
            PointsPerTick = (PointsPerTick == 0.0 ? SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) : PointsPerTick);            
            if (bViewMetrics) Metrics();
            m_MountBar.memDT = (datetime) macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
            def_Rate.real_volume = 0;
            def_Rate.tick_volume = 0;
        }
        def_Rate.close = (m_Ticks.ModePlot == PRICE_EXCHANGE ? (m_Ticks.Info[m_ReplayCount].volume_real > 0.0 ? m_Ticks.Info[m_ReplayCount].last : def_Rate.close) :
               (m_Ticks.Info[m_ReplayCount].bid > 0.0 ? m_Ticks.Info[m_ReplayCount].bid : def_Rate.close));
        def_Rate.open = (bNew ? def_Rate.close : def_Rate.open);
        def_Rate.high = (bNew || (def_Rate.close > def_Rate.high) ? def_Rate.close : def_Rate.high);
        def_Rate.low = (bNew || (def_Rate.close < def_Rate.low) ? def_Rate.close : def_Rate.low);
        def_Rate.real_volume += (long) m_Ticks.Info[m_ReplayCount].volume_real;
        def_Rate.tick_volume += (m_Ticks.Info[m_ReplayCount].volume_real > 0 ? 1 : 0);
        def_Rate.time = m_MountBar.memDT;
        CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate);
        if (bViewTicks)
        {
            tick = m_Ticks.Info[m_ReplayCount];
            if (!m_Ticks.bTickReal)
            {
                static double BID, ASK;
                double dSpread;
                int    iRand = rand();
    
                dSpread = PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? PointsPerTick : 0 ) : 0 );
                if (tick[0].last > ASK)
                {
                    ASK = tick[0].ask = tick[0].last;
                    BID = tick[0].bid = tick[0].last - dSpread;
                }
                if (tick[0].last < BID)
                {
                    ASK = tick[0].ask = tick[0].last + dSpread;
                    BID = tick[0].bid = tick[0].last;
                }
            }
            CustomTicksAdd(def_SymbolReplay, tick); 
        }
        m_ReplayCount++;
        
#undef def_Rate
    }

No caso de trabalhar com um ativo cujo preço de plotagem é baseado no BID, este calculo aqui não terá nenhum tipo de efeito, tanto que no FOREX este tipo de volume simplesmente não existe. No entanto, este calculo aqui, estará produzindo valores incorretos para um sistema de plotagem baseados no BID. Isto por conta que nestes casos, não existe no ticket a informação do volume negociado. Então o valor do volume de tickets será sempre de zero.

Mas no calculo usado para gerar o volume de tickets, durante a fase em que estamos criando as barras de 1 minuto, para serem utilizadas, em caso de um deslocamento rápido, este calculo já irá informar o valor correto. Então temos que corrigir justamente o procedimento visto acima. Este ficará conforme mostrado abaixo:

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

        bool bNew;
        MqlTick tick[1];
        static double PointsPerTick = 0.0;

        if (bNew = (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time)))
        {
            PointsPerTick = (PointsPerTick == 0.0 ? SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) : PointsPerTick);            
            m_MountBar.memDT = (datetime) macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
            if (m_Ticks.ModePlot == PRICE_FOREX) CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate, (def_Rate.time < m_MountBar.memDT ? 1 : 0));
            def_Rate.real_volume = 0;
            def_Rate.tick_volume = 0;
        }
        def_Rate.close = (m_Ticks.ModePlot == PRICE_EXCHANGE ? (m_Ticks.Info[m_ReplayCount].volume_real > 0.0 ? m_Ticks.Info[m_ReplayCount].last : def_Rate.close) :
                                                               (m_Ticks.Info[m_ReplayCount].bid > 0.0 ? m_Ticks.Info[m_ReplayCount].bid : def_Rate.close));
        def_Rate.open = (bNew ? def_Rate.close : def_Rate.open);
        def_Rate.high = (bNew || (def_Rate.close > def_Rate.high) ? def_Rate.close : def_Rate.high);
        def_Rate.low = (bNew || (def_Rate.close < def_Rate.low) ? def_Rate.close : def_Rate.low);
        def_Rate.real_volume += (long) m_Ticks.Info[m_ReplayCount].volume_real;
        def_Rate.tick_volume += ((m_Ticks.ModePlot == PRICE_FOREX) && (m_Ticks.Info[m_ReplayCount].bid > 0.0) ? 1 : (m_Ticks.Info[m_ReplayCount].volume_real > 0 ? 1 : 0));
        def_Rate.time = m_MountBar.memDT;
        CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate);
        if (bViewTicks)
        {
            tick = m_Ticks.Info[m_ReplayCount];
            if (!m_Ticks.bTickReal)
            {
                static double BID, ASK;
                double dSpread;
                int    iRand = rand();
    
                dSpread = PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? PointsPerTick : 0 ) : 0 );
                if (tick[0].last > ASK)
                {
                    ASK = tick[0].ask = tick[0].last;
                    BID = tick[0].bid = tick[0].last - dSpread;
                }
                if (tick[0].last < BID)
                {
                    ASK = tick[0].ask = tick[0].last + dSpread;
                    BID = tick[0].bid = tick[0].last;
                }
            }
            CustomTicksAdd(def_SymbolReplay, tick); 
        }
        m_ReplayCount++;
        
#undef def_Rate
    }

Não me pergunte o porque, mas por algum estranho motivo, que eu pessoalmente não faço a menor ideia para de fato poder lhe explicar. 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.

Este calculo é bastante simples, como você podem ter notado. Mas a parte realmente curiosa, é o fato de termos que enviar os valores do RATE para dentro do ativo customizado uma segunda vez. Assim que a barra fecha, não consegui de fato entender o motivo disto. Tanto que este envio só se torna necessário no caso de uma plotagem do tipo BID, algo bastante curioso diga-se por passagem.

Existe uma outra coisa que você deve ter notado na rotina acima. Agora não existirá mais o sistema de métricas. De certa forma eu já cogitava remover este sistema quando os tickets foram adicionados na janela de observação de mercado. Isto por que podemos ter ali uma boa medida do tempo que esta sendo necessário para gerar cada uma das barras. Por conta disto o código de métricas foi removido.


Preparando o terreno para o próximo desafio

Com todas estas mudanças implementadas até aqui, podemos partir para o real e verdadeiro desafio: Criar uma forma de simular os tickets do mercado de FOREX com base apenas e somente no conteúdo presente em arquivos de barras de 1 minuto. E acreditem o desafio será bastante grande. Mas ao mesmo tempo bastante interessante e empolgante de ser enfrentado e superado. Para facilitar, já que a coisa é bem mais complexa do que possa parecer. Vamos dividir a classe C_FileTicks em 2 classes. Mas isto é apenas para facilitar as coisas. Esta divisão não precisaria de fato acontecer. Mas já que precisaremos lidar com algumas coisas bem chatinhas de serem processadas, e eu não gosto de ter uma classe contendo mais de 1000 linhas. Então vamos dividir C_FileTicks em duas classes.

Nesta divisão, o que irá acontecer é que iremos retirar da classe C_FileTicks a parte responsável por fazer a simulação dos tickets. E assim nasce a classe C_Simulation. Que será a responsável por fazer esta criação, transformando as barras de 1 minuto em tickets, de forma que isto poderá ser apresentado e trabalhado sem mais problemas, já que a tal classe C_Simulation será invisível para o sistema de replay. Para o serviço de replay, os dados estarão sempre vindo de um arquivo de tickets reais. Sendo que na verdade, poderão estar de fato vindo de uma simulação. Mesmo que você tente acessar a classe C_Simulation, de dentro da classe C_Replay, ela não será acessível. Desta forma a coisa irá funcionar conforme esperamos que ela funcione. Já que a classe C_Replay, irá conseguir ver apenas a classe C_FileTicks e esta faz justamente isto: Carrega dos tickets reais presentes em um arquivo para que a classe C_Replay, possa apresentar eles no terminal gráfico do MetaTrader 5.

Então a nova declaração da classe C_FileTicks, fica conforme mostrado abaixo:

#include "C_FileBars.mqh"
#include "C_Simulation.mqh"
//+------------------------------------------------------------------+
#define macroRemoveSec(A) (A - (A % 60))
//+------------------------------------------------------------------+
class C_FileTicks : private C_Simulation
{

// ... Código interno da classe 

};

O fato de fazermos isto, faz com que a classe C_FileTicks herde de forma privada a classe C_Simulation. Desta maneira cumpri exatamente aquilo que descrevi acima.

Mas temos que fazer algumas pequenas mudanças dentro do código da classe C_FileTicks. Isto por que estaremos herdando a classe C_Simulation. Mas não quero enviar para dentro da classe C_Simulation os dados que estão sendo declarados como sendo do tipo protected. Isto para que a classe C_Simulation permaneça oculta para o resto do sistema. Mas temos que ainda assim permitir que o trabalho feito pela classe possa ainda ser utilizado pelas demais classes. Então precisamos modificar o seguinte código adicionando as seguintes linhas:

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];
            iRet = Simulation(rate[0], local);
            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));
    }

Estas linhas destacadas, faziam parte do código que era executado pelo procedimento que cria a simulação. Para manter as coisas no seu devido lugar, precisamos que agora a rotina que gera a simulação retorne um valor. Este valor é utilizado para que a classe C_FileTicks, saiba quantos tickets devem ser armazenados no array, de forma que depois venham a ser utilizados pela classe C_Replay.

Agora poderemos focar totalmente nossa atenção a classe C_Simulation, e construir um sistema que seja capaz de efetuar qualquer nível de simulação, baseando-se apenas e somente nos dados contidos em um arquivo de barras de 1 minuto. Mas como você já deve estar pensando, isto será tema para o próximo tópico deste artigo.


A Classe C_Simulation

Muito bem, agora que já separamos as coisas, e para qualquer efeito estaremos trabalhando com qualquer tipo de mercado. Precisamos definir um pequeno detalhe. Porém este é bastante importante: Que tipo de simulação de fato iremos criar ?!?! Queremos que os tickets criados, sejam do tipo encontrados em um mercado de Bolsa, ou em um mercado de Forex ?!?!  Pois bem, esta questão parece gerar confusão e nos força a ter dois tipos de modos a serem simulados. Mas você deve pensar no seguinte: Para você que esta programando o sistema, esta questão é relativamente simples e meramente burocrática. Mas para o usuário, isto é bastante confuso e muitas das vezes, ele não quer de fato saber, se estará simulando um banco de dados vindo de um mercado parecido com o forex ou parecido com a bolsa. Ele simplesmente irá querer que o sistema faça o que deve ser feito, ou seja simular os dados.

Mas ao se trabalhar com informações presentes em arquivos de barras de 1 minuto, temos a impressão, a primeira vista, de que não tem como saber, se os dados presentes naquele arquivo especifico, veio de um mercado similar ao forex ou a um mercado similar ao da bolsa. Isto no primeiro momento. Mas ao observar com calma tais arquivos e comparar os dados presentes neles, você logo acabara notando um certo padrão. Por conta deste padrão, é possível definir de forma muito clara e eficaz o modo de plotagem a ser usado. Seja ele do tipo BID ou do tipo LAST. Isto apenas observando o conteúdo do arquivo de barras.

Por conta disto, você como programador, deverá sempre assumir este trabalho de fazer, com que o programa se adeque a um modelo ou outro de plotagem. De forma que para o usuário a questão não precisará de estudos sobre o tipo de modelagem presentes no arquivo de barras de 1 minuto. Pois o programador irá, e deverá resolver este problema para ele, o usuário. Se você não esta entendendo do que estou falando, não se preocupem. Sei que parece estranho o fato de eu estar falando disto. Mas o fato de entender como a base de dados funciona para esta questão especifica, é que me fez dividir o código do sistema de simulação. Isto para que ele fosse melhor implementado. Então antes de entrar de fato na classe C_Simulation, vamos entender como as coisas funcionam de forma a diferenciar um sistema de plotagem do tipo BID de um que usa o LAST.

Acredito que você tenha entendido como o sistema consegue saber se iremos fazer o replay de dados vindos do forex ou da bolsa. Se isto não ficou claro, recomendo você a reler os artigos anteriores até que consiga de fato entender como o sistema consegue fazer esta diferenciação. Mas se você já entendeu a parte sobre o replay, vamos a grande questão: Como o sistema pode saber se estará trabalhando com dados de um mercado similar ao de Forex ou um mercado similar ao de Bolsa. Isto quando a única informação é um arquivo de barras de 1 minuto ?!?! 

A forma de se saber, se deveremos utilizar o simulador, de forma a criar os tickets de forma que a informação seja similar ao mercado de Forex ou mercado de Bolsa é: O volume !!! Mas como ?!?! Como assim o volume ?!?! É isto mesmo. A informação que diz se o arquivo de barras pertence a um mercado similar ao de Forex ( assim teremos e iremos usar o valor de BID como preço negociado ), ou ao mercado de Bolsa ( onde iremos utilizar o LAST como sendo o preço negociado ), é justamente o volume.

Se você não entendeu. Veja as figuras logo abaixo onde é possível ver trechos de um arquivo de barras de 1 minuto.


Forex

Figura 02 - Arquivo de um ativo de Forex


Bolsa

Figura 03 - Arquivo de um ativo de Bolsa

Apesar de não termos informações sobre se estamos utilizando valores de BID ou valores do LAST, para servirem de referencia ao preço, que esta de fato sendo negociado. Isto por que as figuras acima são figura de arquivos de barras de 1 minuto. Você logo nota que a única coisa ali que esta diferente, é o valor de volume. Por conta disto que este valor esta sendo destacado nas figuras. Isto para que você note onde de fato está a diferença.


Considerações finais deste artigo

Agora vamos entender uma coisa: Desde o artigo Desenvolvendo um sistema de Replay - Simulação de mercado (Parte 11): Nascimento do SIMULADOR (I), onde começamos a desenhar um sistema de simulação. Temos utilizado alguns dos recursos presentes nas barras de 1 minuto de forma a conseguir simular um random walk. Este seria um provável movimento de mercado dentro daquela barra de 1 minuto, informado pelo arquivo. No entanto, em momento algum da construção, de tal mecanismo, e isto até o artigo Desenvolvendo um sistema de Replay - Simulação de mercado (Parte 15): Nascimento do SIMULADOR (V) - RANDOM WALK onde o random walk foi de fato construído. Não foi necessário, ou melhor dizendo, eu não achei que seria necessário, tratar casos em que o volume fosse zero ou o volume de tickets fosse muito baixo. Mas por conta que existem mercados, e ativos, que tem estes valores bem peculiares, torna-se necessário cobrir tais casos. Se isto não for feito, o serviço irá travar ou irá bloquear em algum momento, lhe deixando na mão quando você desejar executar algum tipo de simulação. Lembre-se: O problema é no caso da simulação. No caso do replay as coisas funcionarão normalmente. Desde que esteja tudo em perfeita harmonia. Já que não será necessário trabalhar com situações exóticas, como o que iremos fazer a partir de agora, onde teremos que criar, ou melhor dizendo simular possíveis movimentos de mercado.

Bem, por enquanto vou deixar você assimilar o que aconteceu até aqui. Pois a parte da criação do simulador para um mercado similar ao FOREX, onde usaremos valores de BID como forma de plotagem, merece ser vista com calma. Já que a quantidade de coisas a serem explicadas para o entendimento do sistema será bastante diferente do que foi visto até aqui. O motivo é que teremos de reformular algumas partes do cálculo de simulação. Então nos vemos no próximo artigo desta sequencia.



Arquivos anexados |
Market_Replay_7vc22.zip (14387.78 KB)
Redes neurais de maneira fácil (Parte 38): Exploração auto-supervisionada via desacordo (Self-Supervised Exploration via Disagreement) Redes neurais de maneira fácil (Parte 38): Exploração auto-supervisionada via desacordo (Self-Supervised Exploration via Disagreement)
Um dos principais desafios do aprendizado por reforço é a exploração do ambiente. Anteriormente, já nos iniciamos no método de exploração baseado na curiosidade interna. E hoje proponho considerar outro algoritmo, o de exploração por desacordo.
Aprenda algumas lições com as Empresas de Prop Trading (Parte 1) — Uma introdução Aprenda algumas lições com as Empresas de Prop Trading (Parte 1) — Uma introdução
Neste artigo introdutório, discutirei algumas lições que podem ser aprendidas com os testes que as empresas de prop trading empregam. Isso é especialmente relevante para iniciantes e para aqueles que estão lutando para encontrar seu lugar no mundo do trading. O próximo artigo abordará a implementação do código.
Ciência de Dados e Aprendizado de Máquina (Parte 14): aplicando mapas de Kohonen nos mercados Ciência de Dados e Aprendizado de Máquina (Parte 14): aplicando mapas de Kohonen nos mercados
Deseja descobrir uma nova metodologia de negociação que facilite a orientação em mercados complexos e voláteis? Explore os mapas de Kohonen - uma versão inovadora de redes neurais artificiais, capazes de identificar regularidades e tendências ocultas nos dados do mercado. Neste texto, analisaremos a funcionalidade dos mapas de Kohonen e a forma de utilizá-los na elaboração de estratégias de negociação eficazes. Estou convencido de que esta abordagem inédita será do interesse de traders novatos e experientes.
Experimentos com redes neurais (Parte 5): Normalização de parâmetros de entrada para alimentar a rede neural Experimentos com redes neurais (Parte 5): Normalização de parâmetros de entrada para alimentar a rede neural
As redes neurais são tudo para nós. E vamos verificar na prática se é assim, indagando se MetaTrader 5 é uma ferramenta autossuficiente para implementar redes neurais na negociação. A explicação vai ser simples.