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

Desenvolvendo um sistema de Replay - Simulação de mercado (Parte 09): Eventos Customizados

MetaTrader 5Exemplos | 8 maio 2023, 10:08
392 0
Daniel Jose
Daniel Jose

Introdução

No artigo anterior, Desenvolvendo um sistema de Replay - Simulação de mercado (Parte 08): Travando o Indicador, mostrei como fazer para travar o indicador de controle. Apesar de termos tido sucesso em fazer tal coisa, ainda temos algumas coisas para serem resolvidas. Se você reparou bem, irá notar que toda a vez que você muda o ponto onde o replay/simulação irá se iniciar, temos uma rápida apresentação das barras de negociação sendo construídas. Isto de certa forma, não é algo realmente problemático, pode até ser interessante para alguns, e nem tanto para outros. Para tentar agradar a gregos e troianos. Vamos ver como implementar o serviço de replay/simulador, de forma que ele fique, como mais lhe agradar. Ou seja, você poderá ver as barras sendo construídas, ou não.


No caminho para agradar a Gregos e Troianos

A primeiríssima coisa a ser feita, é adicionar uma nova variável, ou parâmetro no arquivo de serviço:

input string            user00 = "Config.txt";  //Arquivo de configuração do Replay.
input ENUM_TIMEFRAMES   user01 = PERIOD_M5;     //Tempo gráfico inicial.
input bool              user02 = true;          //Visualizar a construção das barras.

Ao fazer isto, estamos começando a deixar a coisa, de forma que o usuário posso fazer a escolha. Como foi dito, tem pessoas que gostam de ver as barras sendo construídas, enquanto para outros, isto não faz a menor diferença.

Uma vez feito isto, iremos passar este parâmetro para a classe C_Replay, no seguinte ponto:

while ((ChartSymbol(id) != "") && (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value)) && (!_StopFlag))
{
        if (!Info.s_Infos.isPlay)
        {
                if (!bTest) bTest = true;
        }else
        {
                if (bTest)
                {
                        delay = ((delay = Replay.AdjustPositionReplay(user02)) >= 0 ? 3 : delay);
                        bTest = false;
                        t1 = GetTickCount64();
                }else if ((GetTickCount64() - t1) >= (uint)(delay))
                {
                        if ((delay = Replay.Event_OnTime()) < 0) break;
                        t1 = GetTickCount64();
                }
        }
}

Agora já podemos ir para dentro da classe C_Replay, para começar a trabalhar nela. Apesar de parecer se uma tarefa de fácil execução, ela estará cheia de percalços e desafios. Até o momento a única coisa utilizada, como dados de replay de mercado, eram os ticks negociados, e o gráfico, é construído sobre barras de 1 minuto. Então não é simplesmente remover ou adicionar barras. Temos que trabalhar com coisas diferentes, de forma a faze-las parecer iguais. É ou não é um desafio ?!?! Mas gosto de desafios, e este será bastante interessante de ser resolvido.

A primeira coisa a ser feita, é no momento que estivermos lendo o arquivo de ticks negociados, deveremos ao mesmo tempo criar as barras de um minuto. Mas também teremos que fazer uma outra coisa. Mas vamos com calma, uma solução, para cada problema. É assim que iremos resolver este desafio. Assim logo de cara, já iremos adicionar um novo conjunto de variáveis no sistema.

struct st00
{
        MqlTick  Info[];
        MqlRates Rate[];
        int      nTicks,
                 nRate;
}m_Ticks;

Este irá conter as barras de 1 minuto, que iremos construir ao mesmo tempo que o arquivo de ticks esta sendo lido. Observando o código até o momento, você notará que a rotina Event_OnTime, presente na classe C_Replay, é capaz de construir as barras de um minuto com base nos valores de ticks negociados. Mas não podemos chamar esta rotina, para fazer este trabalho para nos. De fato, até poderíamos fazer isto, tomando o devido cuidado, em remover no final do processo, todas as barras criadas no ativo de replay. Desta forma, o sistema estaria pronto para ser usado. No entanto, a forma como Event_OnTime trabalha, faz com que um pequeno delay, aconteça a cada chamada, e o numero de chamadas em se tratando de ticks negociados, costuma ser bastante elevado. Teremos que fazer algo um pouco diferente.

Mas como foi dito acima, precisamos fazer algo um pouco diferente, da solução mais obvia. E com isto, surge a seguinte função:

inline bool BuiderBar1Min(MqlRates &rate, const MqlTick &tick)
                {
                        if (rate.time != macroRemoveSec(tick.time))
                        {
                                rate.real_volume = (long) tick.volume_real;
                                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;
        
                        return false;
                }

O que estamos fazendo aqui, é justamente o que seria feito por Event_OnTime. Mas estaremos fazendo o trabalho Tick a Tick. Vamos a uma rápida explicação do que esta acontecendo: Quando o tempo informado no ticket for diferente do contido na barra, teremos a construção inicial da barra. Retornaremos verdadeiro para indicar ao chamador, que uma nova barra estará sendo criada, para que ele providencie os ajustes necessários. Nas chamadas subsequentes, teremos o ajuste dos valores de forma adequada. Neste caso, retornaremos falso, para indicar que não houve criação de uma nova barra. Esta rotina em si é bastante simples. Mas devemos tomar alguns cuidados ao utilizá-la.

A primeira coisa que devemos tomar cuidado, é em inicializar a array de forma adequada. Vejam abaixo onde isto é feito.

bool SetSymbolReplay(const string szFileConfig)
{
        int     file;
        string  szInfo;
        bool    isBars = true;
                                
        if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
        {
                MessageBox("Falha na abertura do\narquivo de configuração.", "Market Replay", MB_OK);
                return false;
        }
        Print("Carregando dados para replay. Aguarde....");
        ArrayResize(m_Ticks.Rate, 540);
        m_Ticks.nRate = -1;
        m_Ticks.Rate[0].time = 0;
        while ((!FileIsEnding(file)) && (!_StopFlag))
        {
                szInfo = FileReadString(file);
                StringToUpper(szInfo);
                if (szInfo == def_STR_FilesBar) isBars = true; else
                if (szInfo == def_STR_FilesTicks) isBars = false; else
                if (szInfo != "") if (!(isBars ? LoadPrevBars(szInfo) : LoadTicksReplay(szInfo)))
                {
                        if (!_StopFlag)
                                MessageBox(StringFormat("O arquivo %s de %s\nnão pode ser carregado.", szInfo, (isBars ? def_STR_FilesBar : def_STR_FilesTicks), "Market Replay", MB_OK));
                        FileClose(file);
                        return false;
                }
        }
        FileClose(file);
        return (!_StopFlag);
}

Sem que isto seja feito, de forma adequada, e antecipadamente, não será possível utilizar corretamente a função de criação das barras. Mas ai vem a pergunta: Por que estou indicando o valor de -1 no index do primeiro array ?!?! Não deveria ser 0 o primeiro valor ?!?! Sim, ele é 0, mas inicio, ele como sendo -1, por conta da primeira chamada que sempre irá retornar verdadeiro. Se ele fosse iniciado como 0, teríamos que fazer um teste extra, logo após a chamada de construção da barra retornar. Mas colocando -1, este teste extra se torna desnecessário. Atenção ao fato de estarmos iniciando o array com 540 posições, que é o numero de barras de 1 minuto, normalmente encontradas em um dia de pregão típico da B3 ( Bolsa Brasileira ).

Feito isto, podemos passar para a fase de leitura em si, dos ticks negociados.

bool LoadTicksReplay(const string szFileNameCSV)
{
        int     file,
                old;
        string  szInfo = "";
        MqlTick tick;
        MqlRates rate;
                                
        if ((file = FileOpen("Market Replay\\Ticks\\" + szFileNameCSV + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE)
        {
                ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
                ArrayResize(m_Ticks.Rate, 540, 540);
                old = m_Ticks.nTicks;
                for (int c0 = 0; c0 < 7; c0++) szInfo += FileReadString(file);
                if (szInfo != def_Header_Ticks)
                {
                        Print("Arquivo ", szFileNameCSV, ".csv não é um arquivo de tick negociados.");
                        return false;
                }
                Print("Carregando ticks de replay. Aguarde...");
                while ((!FileIsEnding(file)) && (m_Ticks.nTicks < (INT_MAX - 2)) && (!_StopFlag))
                {
                        ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
                        szInfo = FileReadString(file) + " " + FileReadString(file);
                        tick.time = macroRemoveSec(StringToTime(StringSubstr(szInfo, 0, 19)));
                        tick.time_msc = (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                        tick.bid = StringToDouble(FileReadString(file));
                        tick.ask = StringToDouble(FileReadString(file));
                        tick.last = StringToDouble(FileReadString(file));
                        tick.volume_real = StringToDouble(FileReadString(file));
                        tick.flags = (uchar)StringToInteger(FileReadString(file));
                        if ((m_Ticks.Info[old].last == tick.last) && (m_Ticks.Info[old].time == tick.time) && (m_Ticks.Info[old].time_msc == tick.time_msc))
                                m_Ticks.Info[old].volume_real += tick.volume_real;
                        else
                        {                                                       
                                m_Ticks.Info[m_Ticks.nTicks] = tick;
                                if (tick.volume_real > 0.0)
                                {
                                        m_Ticks.nRate += (BuiderBar1Min(rate, tick) ? 1 : 0);
                                        rate.spread = m_Ticks.nTicks;
                                        m_Ticks.Rate[m_Ticks.nRate] = rate;
                                        m_Ticks.nTicks++;
                                }
                                old = (m_Ticks.nTicks > 0 ? m_Ticks.nTicks - 1 : old);
                        }
                }
                if ((!FileIsEnding(file)) && (!_StopFlag))
                {
                        Print("Excesso de dados no arquivo de tick.\nNão é possivel continuar...");
                        return false;
                }
        }else
        {
                Print("Aquivo de ticks ", szFileNameCSV,".csv não encontrado...");
                return false;
        }
        return (!_StopFlag);
};

Aqui temos um detalhe interessante, que você deverá modificar o valor inicial e de reserva. Caso o numero de barras de1 minuto, seja maior do que o indicado aqui. Este valor é adequado, para um período de negociação que vai das 9;00 as 18:00, onde teremos 540 minutos. Mas caso este tempo seja maior, você deverá aumenta-lo previamente. Mas atenção, o tempo a ser analisado, deverá ser o da hora de abertura e fechamento da janela de negociação. Isto no arquivo de tickets negociados, não no arquivo de barras. Isto por que, as barras serão geradas em cima do arquivo de tickets, e se esta janela for diferente em um arquivo particular, você poderá ter problemas em tempo de execução ( RUN TIME ). Mas como normalmente a B3, tem uma janela de 540 minutos, este valor é suficiente.

Feito esta ressalva, sobre qual o valor a ser usado. Podemos continuar e agora dentro do arquivos de tickets negociados. Então vamos capturando ticket a ticket e criando as barras de 1 minuto. Mas você deve se atentar ao seguinte fato: As barras somente serão geradas, caso exista algum volume negociado, do contrário o ticket representa algum ajuste no BID ou ASK do ativo, sendo desta forma ignorado. Nota: Futuramente iremos tratar este tipo de situação, já que iremos levar o sistema para ser usado também no forex. mas por hora, vamos ignorar estes tickets.

Já que não usamos no replay/simulador, o valor de spread, este será utilizado para um proposito mais nobre. Porém cuidado, isto não é o valor de spread. Então caso você precise, por conta de algum indicador, do valor correto do spread, você terá que utilizar um outro artificio, para fazer o que esta sendo feito aqui. Usar a variável que seria utilizada para armazenar o spread, para armazenar o valor da posição onde o contador estava. Isto será bastante útil em um futuro próximo.

Agora que esta tudo certo. Podemos armazenar os dados da barra de 1 minuto e assim passar para a próxima etapa. Isto por que não ocorreu nenhuma outra mudança no sistema de leitura. Então não há necessidade de mais comentários a respeito da rotina de leitura.

Agora vamos dar uma passada na função principal deste tópico.

int AdjustPositionReplay(const bool bViewBuider)
{
        u_Interprocess Info;
        MqlRates       Rate[1];
        int            iPos = (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks);
        datetime       dt_Local;
                                
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        if (Info.s_Infos.iPosShift == iPos) return 0;
        iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / def_MaxPosSlider));
        if (iPos < m_ReplayCount)
        {
                dt_Local = m_dtPrevLoading;
                m_ReplayCount = 0;
                if (!bViewBuider) for (int c0 = 1; (c0 < m_Ticks.nRate) && (m_Ticks.Rate[c0 - 1].spread < iPos); c0++)
                {
                        dt_Local = m_Ticks.Rate[c0].time;
                        m_ReplayCount = m_Ticks.Rate[c0 - 1].spread;
                }
                CustomRatesDelete(def_SymbolReplay, dt_Local, LONG_MAX);
                if (m_dtPrevLoading == 0)
                {
                        Rate[0].close = Rate[0].open = Rate[0].high = Rate[0].low = m_Ticks.Info[m_ReplayCount].last;
                        Rate[0].tick_volume = 0;
                        Rate[0].time = m_Ticks.Info[m_ReplayCount].time - 60;
                        CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                }
        }
        for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag); m_ReplayCount++) Event_OnTime();
        return Event_OnTime();
}

Esta função, não esta totalmente finalizada. Ela ainda irá sofrer modificações. Mas para que a explicação futura não fique muito confusa. Irei explicar o que foi adicionado ou retirado durante o decorrer deste artigo. Desta forma todos conseguirão entender o que esta acontecendo. E caso venham a desejar fazer alguma modificação, será fácil de proceder. Bastará recorrer a estes artigos, e verificar, o que cada ponto comentado estará de fato fazendo. Lembrem-se, qualquer parte não comentada aqui, já terá sido feita em artigos passados.

A primeira coisa a ser feita, é declarar uma variável local, para ajustar a posição temporal interna dentro da função. Este ajuste é necessário para evitar, que precisemos ficar inicializando o replay desde o inicio. Caso você avance e depois decida que gostaria de voltar um pouco. Mas calma iremos chegar neste ponto. Depois de fazer alguns cálculos para verificar se a posição atual, deverá ser avançada ou retornar para um ponto um pouco anterior. Encontramos a primeira coisa a ser feita. Caso a posição precise recuar, estas duas linhas, inicializam o replay/simulador, bem no começo da atividade. Mas possivelmente este não será o caso. Se você ou o usuário, indicar que não deseja ver a formação das barras conforme elas vão sendo criadas, entraremos em um pequeno laço, onde iremos verificar o conteúdo de todas as barras de 1 minuto, que foram gravadas durante a leitura dos tickets negociados. 

Neste momento, teremos uma questão, que talvez não faça muito sentido neste momento, que é o fato, de que durante a conversão dos tickets negociados, em barras de 1 minuto. Pegamos a posição relativa do contador, ao mesmo tempo que temos também o valor do tempo da abertura da nova barra. Estes dados, nos são uteis e necessários, para que possamos limpar todas as barras, que vierem depois do tempo indicado. Dificilmente o valor do contador, irá ser idêntico ao valor do novo posicionamento relativo, pedido pelo usuário. Assim o sistema irá fazer um pequeno ajuste, de forma que a posição irá passar a coincidir, mas este é feito de forma bem rápida. Então você quase não notará a criação da barra.

Mas como foi dito, esta rotina ainda não esta completa. Este funcionamento visto na descrição acima, irá servir apenas para o caso do usuário, fazer uma regressão a partir da posição atual do contador. Para o caso de ele avançar a partir da posição atual do contador, ainda teremos o efeito de criação das barras. E como queremos agradar a todos, tanto a gregos, como aos troianos, temos que corrigir este pequeno percalço, para que a criação das barras, não seja mostrada durante um avanço. Não é algo assim tão complicado de ser feito, como alguns podem imaginar. Comparem o código acima, que não contem o sistema de avanço, com o código abaixo, que já contem este sistema:

int AdjustPositionReplay(const bool bViewBuider)
{
#define macroSearchPosition     {                                                                                               \
                dt_Local = m_dtPrevLoading; m_ReplayCount = count = 0;                                                          \
                if (!bViewBuider) for (count = 1; (count < m_Ticks.nRate) && (m_Ticks.Rate[count - 1].spread < iPos); count++)  \
                        { dt_Local = m_Ticks.Rate[count].time;  m_ReplayCount = m_Ticks.Rate[count - 1].spread; }               \
                                }

        u_Interprocess  Info;
        MqlRates        Rate[def_BarsDiary];
        int             iPos = (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks),
                        count;
        datetime        dt_Local;
                                
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        if (Info.s_Infos.iPosShift == iPos) return 0;
        iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1)));
        if (iPos < m_ReplayCount)
        {
                macroSearchPosition;
                CustomRatesDelete(def_SymbolReplay, dt_Local, LONG_MAX);
                if (m_dtPrevLoading == 0)
                {
                        Rate[0].close = Rate[0].open = Rate[0].high = Rate[0].low = m_Ticks.Info[m_ReplayCount].last;
                        Rate[0].tick_volume = 0;
                        Rate[0].time = m_Ticks.Info[m_ReplayCount].time - 60;
                        CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                }
        }if ((iPos > m_ReplayCount) && (!bViewBuider))
        {
                macroSearchPosition;                    
                CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, count);
        }
        for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag); m_ReplayCount++) Event_OnTime();
        return Event_OnTime();
}

Conseguem notar alguma diferença ?!?! Se estão pensando na macro. Esqueçam, pois ela esta ali, apenas para evitar que tenhamos de ficar repetindo o mesmo código, em dois locais diferentes. De fato não existe praticamente nenhuma diferença. Talvez a única coisa diferente, seja de fato, esta linha que irá adicionar as barras extras. Se você rodar a sistema de replay, irá notar que dificilmente os pontos, tanto de avanço, quanto de recuo, irão coincidir com o fechamento de uma barra, com a abertura da próxima barra. O motivo é que sempre existirá, um resíduo que será ajustado justamente por esta linha. Mas por se tratar de algo muito rápido, praticamente não será percebido este refinamento.


Alertando o usuário

Nosso sistema de replay, já esta em um estágio que devemos começar a adicionar algumas coisas, que antes não eram assim tão necessárias. Uma delas, é alertar o usuário, quando não tivermos mais dados dentro do sistema para simular, ou continuar o replay. Sem este aviso, o usuário pode achar que o sistema simplesmente travou, ou aconteceu uma outra coisa de anormal. Para evitar que estes pensamentos passem pela cabeça do pobre do usuário, vamos começar a adicionar algumas coisas extras. A primeira é um aviso de que não existem mais dados a serem utilizados. Para entender como faremos isto, veja o código abaixo:

void OnStart()
{
        ulong t1;
        int delay = 3;
        long id = 0;
        u_Interprocess Info;
        bool bTest = false;
        
        Replay.InitSymbolReplay();
        if (!Replay.SetSymbolReplay(user00))
        {
                Finish();
                return;
        }
        Print("Aguardando permissão do indicador [Market Replay] para iniciar replay ...");
        id = Replay.ViewReplay(user01);
        while ((!GlobalVariableCheck(def_GlobalVariableReplay)) && (!_StopFlag) && (ChartSymbol(id) != "")) Sleep(750);
        if ((_StopFlag) || (ChartSymbol(id) == ""))
        {
                Finish();
                return;
        }
        Print("Permissão concedida. Serviço de replay já pode ser utilizado...");
        t1 = GetTickCount64();
        while ((ChartSymbol(id) != "") && (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value)) && (!_StopFlag))
        {
                if (!Info.s_Infos.isPlay)
                {
                        if (!bTest) bTest = true;
                }else
                {
                        if (bTest)
                        {
                                if ((delay = Replay.AdjustPositionReplay(user02)) < 0) AlertToUser(); else
                                {
                                        delay = (delay >= 0 ? 3 : delay);
                                        bTest = false;
                                        t1 = GetTickCount64();
                                }                               
                        }else if ((GetTickCount64() - t1) >= (uint)(delay))
                        {
                                if ((delay = Replay.Event_OnTime()) < 0) AlertToUser();
                                t1 = GetTickCount64();
                        }
                }
        }
        Finish();
}
//+------------------------------------------------------------------+
void AlertToUser(void)
{
        u_Interprocess Info;
        
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        Info.s_Infos.isPlay = false;
        GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
        MessageBox("No more data to use in replay-simulation", "Service Replay", MB_OK);
}
//+------------------------------------------------------------------+
void Finish(void)
{
        Replay.CloseReplay();
        Print("Serviço de replay finalizado...");
}
//+------------------------------------------------------------------+

Existem dois momentos, em que se poderá gerar um aviso, de que os dados terminaram. O primeiro local, é durante a execução normal do replay. Este será o caso típico. Mas também temos um outro ponto, que é quando o usuário ajusta a posição para o final da barra de deslocamento.

int AdjustPositionReplay(const bool bViewBuider)
{

// ... Código ...

        iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1)));

// ...  Restante do código ...

Independente disto, a resposta será sempre a mesma. Iremos pegar o valor contido na variável global de terminal, e fazer com que ela indique que estaremos em modo pausado. Logo depois, gravamos ela de volta, e mostramos uma janela informando o acontecido. Basicamente é isto que faremos. Mas já será de grande ajuda. Desta forma, o pobre do usuário irá ficar sabendo, o que aconteceu.


Adicionando um aviso de aguarde

Agora que nosso sistema de replay, recebeu uma forma de o usuário informar, se quer ou não, ver as barras sendo desenhadas. Temos um pequeno inconveniente, isto caso ele realmente queira ver as barras sendo plotadas. Este é o motivo deste tópico daqui.

Quando desejamos ver as barras sendo plotadas, enquanto aguardamos o serviço de replay chegar na posição correta. Nos é dada a impressão, que podemos parar o avanço, ou iniciá-lo a qualquer momento. Isto pelo fato, de nos ser mostrado, o botão de play e de pause. Mas na verdade, não podemos fazer nenhuma das duas ações, até que o serviço de replay, de fato alcance a posição correta, para liberar o sistema. E é nestas horas que talvez a coisa, fique um pouco confusa. Não sabemos exatamente, o que esta acontecendo. Mas se modificarmos este botão, que nos é apresentado, por um outro, que indica que deveremos aguardar, a coisa muda de figura. Não é mesmo ?!?!

Pois bem, para fazer isto, não bastará simplesmente adicionar um botão e pronto. Temos que fazer algumas coisas extras, que permitam ao serviço, disser ao indicador de controle, o que deve ser ou não mostrado. Assim começaremos adicionando uma nova variável, no arquivo de cabeçalho InterProcess.mqh.

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#define def_GlobalVariableReplay        "Replay Infos"
#define def_GlobalVariableIdGraphics    "Replay ID"
#define def_SymbolReplay                "RePlay"
#define def_MaxPosSlider                400
#define def_ShortName                   "Market Replay"
//+------------------------------------------------------------------+
union u_Interprocess
{
        union u_0
        {
                double  df_Value;       // Valor da variável global de terminal...
                ulong   IdGraphic;      // Contem a ID do Grafico do ativo...
        }u_Value;
        struct st_0
        {
                bool    isPlay;         // Indica se estamos no modo Play ou Pause ...
                bool    isWait;         // Indica para o usuário aguardar...
                ushort  iPosShift;      // Valor entre 0 e 400 ...
        }s_Infos;
};
//+------------------------------------------------------------------+

Este valor, que será passado entre o serviço e o indicador, terá prioridade sobre quaisquer outros controles. De forma, que se ele precisar ser apresentado, nenhuma outra ação poderá ser executada, pelo indicador de controle.  Agora já que definimos a variável, devemos ir ao serviço de replay, e adicionar o código necessário, para que ele comunique ao indicador de controle. Para fazer isto, bastará que adicionemos, algum código a classe C_Replay. Será pouca coisa, e nada assim tão complicado.

int AdjustPositionReplay(const bool bViewBuider)
{
#define macroSearchPosition     {                                                                                               \
                dt_Local = m_dtPrevLoading; m_ReplayCount = count = 0;                                                          \
                if (!bViewBuider) for (count = 1; (count < m_Ticks.nRate) && (m_Ticks.Rate[count - 1].spread < iPos); count++)  \
                        { dt_Local = m_Ticks.Rate[count].time;  m_ReplayCount = m_Ticks.Rate[count - 1].spread; }               \
                                }

        u_Interprocess  Info;
        MqlRates        Rate[def_BarsDiary];
        int             iPos = (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks),
                        count;
        datetime        dt_Local;
                                
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        if (Info.s_Infos.iPosShift == iPos) return 0;
        iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1)));
        if (iPos < m_ReplayCount)
        {
                macroSearchPosition;
                CustomRatesDelete(def_SymbolReplay, dt_Local, LONG_MAX);
                if (m_dtPrevLoading == 0)
                {
                        Rate[0].close = Rate[0].open = Rate[0].high = Rate[0].low = m_Ticks.Info[m_ReplayCount].last;
                        Rate[0].tick_volume = 0;
                        Rate[0].time = m_Ticks.Info[m_ReplayCount].time - 60;
                        CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                }
        }if ((iPos > m_ReplayCount) && (!bViewBuider))
        {
                macroSearchPosition;                    
                CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, count);
        }
        if (bViewBuider)
        {
                Info.s_Infos.isWait = true;
                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
        }
        for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag); m_ReplayCount++) Event_OnTime();
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        Info.s_Infos.isWait = false;
        GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
        return Event_OnTime();
}

Normalmente este ponto, não será alcançado, e só será quando algo deve de fato ser feito. Se o usuário deseja ver as barras sendo plotadas, fazemos a marcação, para que o indicador mostre, que o serviço irá ficar inacessível por um tempo. Gravamos isto, na variável global de terminal, para que possa ser analisado pelo indicador. Agora o serviço irá executar a tarefa, que ele de fato precisa executar. Logo depois, liberarmos de forma totalmente incondicional o indicador.

Com isto, podemos passar para o código do indicador de controle, para fazermos a analise do que esta acontecendo. Muitos podem pensar, que será necessário, uma grande e enorme quantidade de código, para fazer as coisas funcionarem aqui. Mas não, vocês irão ver, que farei todo o trabalho, com um mínimo possível de código. Para facilitar as coisas, que tal um pouco de abstração ?!?! E para fazer isto, iniciamos adicionando a seguinte linha no arquivo de cabeçalho C_Control.mqh

enum EventCustom {Ev_WAIT_ON, Ev_WAIT_OFF};

Aqui estamos adicionando um nível extra de abstração, para facilitar a nossa vida depois. Também não podemos esquecer, a imagem que iremos usar, ela é adicionada no seguinte fragmento:

#define def_ButtonPlay  "Images\\Market Replay\\Play.bmp"
#define def_ButtonPause "Images\\Market Replay\\Pause.bmp"
#define def_ButtonLeft  "Images\\Market Replay\\Left.bmp"
#define def_ButtonRight "Images\\Market Replay\\Right.bmp"
#define def_ButtonPin   "Images\\Market Replay\\Pin.bmp"
#define def_ButtonWait  "Images\\Market Replay\\Wait.bmp"
#resource "\\" + def_ButtonPlay
#resource "\\" + def_ButtonPause
#resource "\\" + def_ButtonLeft
#resource "\\" + def_ButtonRight
#resource "\\" + def_ButtonPin
#resource "\\" + def_ButtonWait

O fato de usarmos uma imagem aqui, facilita bastante as coisas. Lembre-se de que desejamos, apenas indicar para o usuário, que o servidor esta trabalhando, e que não poderá responder a nenhum outro requerimento durante este trabalho.

Ainda dentro do arquivo da classe, vamos adicionar uma variável interna privativa a classe, de forma a controlar as coisas dentro dela. 

class C_Controls
{
        private :
//+------------------------------------------------------------------+
                string  m_szBtnPlay;
                long    m_id;
                bool    m_bWait;
                struct st_00
                {
                        string  szBtnLeft,
                                szBtnRight,
                                szBtnPin,
                                szBarSlider;
                        int     posPinSlider,
                                posY;
                }m_Slider;
//+------------------------------------------------------------------+

Ao adicionarmos esta variável, já conseguiremos ter uma noção do status do serviço de replay/simulação. Mas ela deve ser inicializada em um local adequado. E o melhor ponto para fazer isto, é no construtor da classe.

C_Controls() : m_id(0), m_bWait(false)
        {
                m_szBtnPlay             = NULL;
                m_Slider.szBarSlider    = NULL;
                m_Slider.szBtnPin       = NULL;
                m_Slider.szBtnLeft      = NULL;
                m_Slider.szBtnRight     = NULL;
        }

Vejam que temos que inicializá-la como falso, já que o serviço de replay/simulador, irá sempre iniciar livre, podendo responder a qualquer comando. Mesmo que esta inicialização tenha sido feita aqui, ainda assim, vamos garantir o correto status em outras chamadas. Mas por enquanto, isto já nos bastará.

Agora devemos pensar o seguinte: Que tipo de evento de fato queremos bloquear ?  Toda a vez que avançamos, ou recuamos a posição do replay, notamos que o botão muda, de play para pause, e queremos bloquear justamente isto: O acesso do usuário a este botão. O simples fato de você clicar nele, já fará com que o indicador de controle, faça requerimentos ao serviço de replay/simulador. Apesar do serviço não responder durante a fase, em que ele estiver ocupado se posicionando para que o replay/simulação.

Se você observar o código, irá notar que o sistema estará sempre respondendo a eventos, ou seja, é um sistema baseado em eventos. Por isto, que criamos a enumeração EventCustom. Para que o sistema, se mantenha baseado em eventos. Não iremos mudar isto. Na verdade, nem cogito fazer tal mudança. Já que isto, iria nos forçar a tomar medidas, um pouco mais complexas do que utilizando eventos. Mas apenas isto não é o bastante, o simples fato de adicionar uma enumeração, que indicará a presença de eventos, não nos dá uma solução. Precisamos de fato implementar tais medidas. Mas já lhe dou uma dica do que iremos fazer. Iremos modificar o procedimento DispatchMessage, de forma que se o serviço, estiver ocupado, nenhum evento será gerado. Isto ao se clicar no botão play/pause. Este tipo de coisa, é facilmente conseguido ao se adicionar o seguinte teste:

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
        {
                u_Interprocess Info;
                static int six = -1, sps;
                int x, y, px1, px2;
                                
                switch (id)
                {

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

                        case CHARTEVENT_OBJECT_CLICK:
                                if (m_bWait) break;
                                if (sparam == m_szBtnPlay)
                                {
                                        Info.s_Infos.isPlay = (bool) ObjectGetInteger(m_id, m_szBtnPlay, OBJPROP_STATE);
                                        if (!Info.s_Infos.isPlay) CreteCtrlSlider(); else
                                        {
                                                RemoveCtrlSlider();
                                                m_Slider.szBtnPin = NULL;
                                        }
                                        Info.s_Infos.iPosShift = (ushort) m_Slider.posPinSlider;
                                        GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                        ChartRedraw();
                                }else   if (sparam == m_Slider.szBtnLeft) PositionPinSlider(m_Slider.posPinSlider - 1);
                                else if (sparam == m_Slider.szBtnRight) PositionPinSlider(m_Slider.posPinSlider + 1);
                                break;

// ... Restante do código ....

Ao adicionarmos apenas e somente esta linha de teste, já irá impedir do indicador ficar fazendo pedidos ao serviço, enquanto ele estiver ocupado. Mas isto não resolver o nosso problema ainda, pois o usuário pode ficar incomodado, pelo fato de clicar no botão de play/pause, e este não mudar. Precisamos fazer outras coisas. Além do mais, ainda não temos como ajustar de forma adequada, o valor da variável que estamos testando.

Esta parte agora, pode parecer um pouco confusa. Mas tudo que iremos fazer, de fato é, modificar o valor da variável m_bWait, e analisar este valor. Isto para saber, quais os bitmaps, que devem ser plotados no gráfico. O que queremos, é que o botão play/pause, seja mudado, para uma outra figura, enquanto o serviço estiver ocupado. Retornando ao velho e bom botão play/pause, quando o serviço já estiver liberado. Para fazer isto, utilizaremos algo bastante direto:

void CreateBtnPlayPause(bool state)
{
        m_szBtnPlay = def_PrefixObjectName + "Play";
        CreateObjectBitMap(5, 25, m_szBtnPlay, (m_bWait ? def_ButtonWait : def_ButtonPause), (m_bWait ? def_ButtonWait : def_ButtonPlay));
        ObjectSetInteger(m_id, m_szBtnPlay, OBJPROP_STATE, state);
}

Notem que estou simplesmente testando a variável. Com base no valor dela, teremos a aplicação do botão play/pause, ou do botão que irá representar um sinal de aguarde. Mas ai você pode estar pensando: Como você vai controlar este botão ?!?! Você vai ficar lendo o tempo todo, o valor que esta na variável global de terminal ?!?! Bem, será mais ou menos isto. Você deve lembrar do seguinte detalhe: Cada vez que o serviço adiciona um novo ticket no ativo de replay, isto irá refletir no indicador. Então o MetaTrader 5, irá gerar um evento, que irá disparar uma chamada a função OnCalcule. É neste ponto que iremos entrar. Mas não iremos ficar ali atormentando o indicador. Iremos fazer algo, de uma forma um pouco mais elegante. Para você entender o que será feito. Veja a imagem abaixo, do fluxo de chamadas dentro do código

Esta é exatamente, a sequencia que iremos executar. Para controlar de forma adequada, o botão no indicador de controle. O procedimento CreateBtnPlayPause, já foi visto acima. Creio que tenha ficado bastante evidente, o que estará acontecendo. Então vamos ver os demais pontos, até que este fluxograma, seja totalmente descrito. Mas iremos vê-lo, de traz para frente, já que no procedimento OnCacule, teremos uma lógica um pouco mais complexa, que exigirá você entender os passos que acontecem em DispatchMessage. 

Por conta disto, vamos ao fragmento básico de tratamento dos eventos customizados. Observem o fragmento de código abaixo:

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        u_Interprocess Info;
        static int six = -1, sps;
        int x, y, px1, px2;
                                
        switch (id)
        {
                case (CHARTEVENT_CUSTOM + Ev_WAIT_ON):
                        m_bWait = true;
                        CreateBtnPlayPause(true);
                        break;
                case (CHARTEVENT_CUSTOM + Ev_WAIT_OFF):
                        m_bWait = false;
                        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                        CreateBtnPlayPause(Info.s_Infos.isPlay);
                        break;

// ... Restante do código ...

Quando DispatchMessage for chamada, pela OnChartEvent, que esta presente no indicador de controle. Serão passados os dados, de forma que estaremos de fato, tratando tanto as mensagens de eventos, que a plataforma MetaTrader 5 estará nos informando, quanto também os de eventos customizados, que serão disparados pelo nosso código em pontos específicos. Estes serão vistos depois. A função irá procurar o código referente, no caso de você estar usando um evento customizado Ev_WAIT_ON. E iremos ter a indicação de que o serviço estará ocupado. Desta forma, a variável m_bWait, irá receber um valor verdadeiro. Logo depois, chamamos a criação do botão play/pause, e este irá na verdade, plotar uma figura que indica estado de ocupado. Quando for disparado um evento customizado Ev_WAIT_OFF, queremos que seja feita a indicação do status atual do serviço, ou seja, se ele esta no modo play, ou no modo pausado. Por conta disto, a variável m_bWait, irá receber um valor indicando que o serviço esta liberado para receber requerimentos. Temos também, que capturar o dado da variável global de terminal, que conterá a atual situação do serviço. Logo em seguida, fazermos uma chamada a rotina, que cria o botão play/pause, para que o usuário possa começar a interagir com o sistema.

Este tipo de coisa é bastante legal, e acredito que todos conseguiram captar a ideia aqui. A grande questão é: Como estes eventos serão disparados ?!?! Será que vamos ter um código extremamente complexo e difícil de entender ?!?! Não, a forma de disparar eventos na linguagem MQL5, é bastante simples. Igual a forma de você analisar, e tratar estes eventos customizados. No fragmento de código acima, você viu como tratar dois eventos customizados. Agora vamos ver, como disparar estes eventos. Para fazer isto, você deve ter em mente o seguinte: Quando disparamos um evento customizado, na verdade estamos disparando, uma chamada a função OnChartEvent. É justamente esta a função, que será sempre chamada, ao disparar um evento, seja customizado, ou oriundo da plataforma MetaTrader 5. A função chamada, será sempre a mesma. Mas veja o código desta função, no indicador de controle:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        Control.DispatchMessage(id, lparam, dparam, sparam);
}

Ou seja, quando um evento é disparado. O tratamento dele, é repassado para a classe C_Control, e a função a ser executada é justamente a DispatchMessage. Notaram como a coisa vai caminhando ?!?! Se o código contido na função DispatchMessage, estivesse contido dentro da função de tratamento de eventos, os acontecimentos iriam ser os mesmo. Mas prestem atenção a uma coisa: A função OnChartEvent, recebe 4 parâmetros, mas a função que dispara os eventos, irá utilizar mais parâmetros. Na verdade são 5 os parâmetros usados, para disparar os eventos customizados. Por disto, é que podemos distinguir eventos customizados, de eventos oriundos do MetaTrader5. Se você prestar atenção, irá notar que o valor usado, no momento da seleção, é a soma do valor indicado na enumeração EventCustom, com um outro dado CHARTEVENT_CUSTOM, desta forma conseguimos o valor correto. 

Mas como este valor é criado ?? Como podemos gerar eventos customizados utilizando o MQL5 ?!?! Para entender isto, vamos ver o código principal no nosso indicador de controle. A função OnCalcule. Esta pode ser vista logo abaixo:

int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
{
        static bool bWait = false;
        u_Interprocess Info;
        
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        if (!bWait)
        {
                if (Info.s_Infos.isWait)
                {
                        EventChartCustom(m_id, Ev_WAIT_ON, 0, 0, "");
                        bWait = true;
                }
        }else if (!Info.s_Infos.isWait)
        {
                EventChartCustom(m_id, Ev_WAIT_OFF, 0, Info.u_Value.df_Value, "");
                bWait = false;
        }
        
        return rates_total;
}

Vamos entender como este código acima funciona. A primeira coisa que você deve entender, é que este código, é o tratador de eventos, que será chamado pelo MetaTrader 5. Ou seja, cada vez que ocorrer uma mudança no preço do ativo, ou o ativo receber um ticket negociado novo, esta função, OnCalcule, será chamada automaticamente pelo MetaTrader 5. Desta forma não precisamos, e não iremos utilizar nenhum tipo de temporizador dentro do indicador. Se bem que o uso de um temporizador, é algo que deve ser evitado, ao máximo dentro de indicadores. Visto que ele irá afetar, não apenas o indicador envolvido, mas todos os demais. Então iremos utilizar esta chamada, que é feita pela própria plataforma MetaTrader 5, para verificar o que esta ocorrendo com o serviço. Dado o fato de que ele, o serviço, estará enviado tickets para dentro do ativo de replay/simulador, e com isto chamando indiretamente a função OnCalcule.


Conclusão

Espero que você tenha compreendido a ideia. Já que ela é a base para todo o restante. Então a cada chamada para OnCalcule, iremos capturar o valor presente na variável global de terminal, e verificar se a variável estática local, está ou não, com um valor verdadeiro. Caso ela não esteja, vamos verificar se o serviço esta ocupado. Se este for o caso, iremos disparar um evento customizado, informando isto. Logo depois, iremos modificar o valor da variável estática local, para indicar que o indicador de controle, esta ciente que o serviço de replay/simulação esta ocupado. Assim, no próximo evento em que OnCalcule for disparada, iremos passar a verificar, se o serviço de replay/simulação, se encontra liberado para exercer a sua atividade. No momento em que isto acontecer, iremos disparar um evento customizado, informando que o serviço, já poderá receber requerimentos, por parte do indicador de controle. E o ciclo volta a acontecer, já que a variável estática local, irá passar a ter um valor verdadeiro.

Agora atenção ao fato, de que estamos utilizando algo comum para disparar os eventos customizados. Que é a função EventChartCustom. Aqui ficamos apenas no campo de gráfico atual, e dentro do indicador de controle. Mas você pode disparar eventos, para qualquer gráfico, ou indicador, e até mesmo para um Expert Advisor. O único cuidado a ser tomado, é que os parâmetros da função EventChartCustom, sejam corretamente preenchidos. Se isto for feito, todo o resto será de responsabilidade da plataforma MetaTrader 5. E você apenas terá que tratar o evento customizado no local, seja em indicador ou um Expert Advisor. Este é um tipo de coisa, muito pouco explorada. Até onde consegui notar, as pessoas as vezes acham, que a plataforma MetaTrader 5, não consegue fazer certas coisas. 

No vídeo abaixo, irei demonstrar o sistema no atual estágio de desenvolvimento. Espero que você esteja gostando dos artigos, e que eles estejam sendo uteis, para que você aprenda e conheça melhor a plataforma MetaTrader 5. Assim como as possibilidades dentro da linguagem MQL5.



Arquivos anexados |
Market_Replay.zip (13060.83 KB)
Mais sobre o sistema Murray Mais sobre o sistema Murray
Os sistemas gráficos de análise de preços são amplamente reconhecidos e apreciados pelos traders. Neste artigo, irei abordar o sistema Murray em sua totalidade, que engloba não apenas os renomados níveis, mas também outras técnicas úteis para avaliar a posição atual do preço e tomar decisões de negociação.
Algoritmos de otimização populacionais: Otimização de ervas invasivas (IWO) Algoritmos de otimização populacionais: Otimização de ervas invasivas (IWO)
A surpreendente capacidade das plantas daninhas de sobreviver em uma ampla variedade de condições foi a inspiração para o desenvolvimento de um poderoso algoritmo de otimização. O IWO (Invasive Weed Optimization) é considerado um dos melhores entre os analisados até o momento.
Desenvolvimento de uma DLL experimental com suporte a multithreading em C++ para MetaTrader 5 no Linux Desenvolvimento de uma DLL experimental com suporte a multithreading em C++ para MetaTrader 5 no Linux
Este artigo descreve o processo de desenvolvimento para a plataforma MetaTrader 5 exclusivamente em Linux. O produto final funciona tanto no Windows quanto no Linux sem nenhum problema. Veremos o Wine e o Mingw, ferramentas importantes para o desenvolvimento entre plataformas. O Mingw apresenta threads (POSIX e Win32), que você deve levar em conta ao escolher uma ferramenta adequada. Criaremos também uma DLL para testar o conceito e usá-la no código MQL5, comparando o desempenho das duas implementações de threading. O artigo tem como objetivo ser um ponto de partida para a realização de seus próprios experimentos. Depois de ler este artigo, você será capaz de criar ferramentas para o MetaTrader no Linux.
Algoritmos de otimização populacionais: algoritmo de otimização de forrageamento bacteriano (BFO) Algoritmos de otimização populacionais: algoritmo de otimização de forrageamento bacteriano (BFO)
A base da estratégia de forrageamento de E. coli (E. coli) inspirou cientistas a desenvolverem o algoritmo de otimização BFO. Esse algoritmo apresenta ideias originais e abordagens promissoras para otimização e merece um estudo mais aprofundado.