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

Desenvolvendo um sistema de Replay - Simulação de mercado (Parte 11): Nascimento do SIMULADOR (I)

MetaTrader 5Exemplos | 16 maio 2023, 09:20
599 0
Daniel Jose
Daniel Jose

Introdução

Até o devido momento e isto incluindo o artigo anterior: Desenvolvendo um sistema de Replay - Simulação de mercado ( Parte 10 ) - Usando apenas dados reais na replay, tudo que fizemos, envolve dados reais, ou seja estávamos utilizando tickets realmente negociados. Com isto, os movimentos são bem precisos, e facilmente criados. Já que não precisávamos nos preocupar com as informações. Tudo o que precisávamos fazer, era transformar tickets negociados, em barras de 1 minuto, dai a plataforma MetaTrader 5, faz todo o restante para nos.

Mas agora, vamos começar a encarar um problema um pouco diferente e mais complexo.


Planejamento

Muitos dirão, que o planejamento, é algo simples. Já que a ideia necessária para tornar barras, que no caso serão, e deverão ser sempre de 1 minuto ( e o motivo de elas terem que ser de 1 minuto será visto mais para frente ) em tickets. Mas a simulação, é bem algo muito mais complexo do que realmente pode parecer. O principal motivo é que não temos ideia alguma, de como foi o real comportamento dos tickets, a fim de realmente criar a barra de 1 minuto. Tudo que temos é a barra, e alguns dados sobre ela. Não sabemos como ela foi criada. Estaremos utilizando as barras de 1 minuto, justamente pelo motivo, de elas nos darem, um nível de complexidade mínimo. Se você conseguir criar um movimento complexo o suficiente, a ponto de que ele se pareça na maior parte das vezes, com um movimento real. No final das contas, você terá conseguido gerar um movimento, aceitável e próximo o suficiente, do movimento real.

Este ponto não parece ser assim tão dramático. Já que normalmente observamos um movimento do tipo ZIG-ZAG no mercado. Independente de como será o movimento, em uma estrutura mais complexa. Tudo irá se resumir em criar um zig-zag entre os pontos OHCL. Assim você inicia no ponto de abertura da barra, e irá fazer no mínimo de 9 pernas, a fim de criar o tal zig-zag interno. E sempre irá finalizar no fechamento da barra. Para voltar a fazer a mesma coisa, dentro da próxima barra. Esta mesma ideia é utilizada pelo testador de estratégia do MetaTrader 5. Para mais detalhe veja Ticks reais e gerados - Trading algorítmico. No primeiro momento, irei utilizar esta mesma estratégia, apesar de ela não ser devidamente adequada, ela nos dará um ponto de inicio, de onde poderemos partir para algo mais apropriado.

O fato de mencionar a estratégia utilizada pelo testador, como não a mais adequada ao sistema de replay / simulador, se deve ao fato de que no testador, não temos exatamente uma preocupação como o tempo, ou melhor dizendo: Uma barra de 1 minuto, não precisa ser criada e apresentada de forma a ter uma duração de 1 minuto. Para dizer a verdade, é até apropriado, que ela não tenha este tempo, em termos reais. Se fosse assim, seria praticamente inviável executar testes de alguma estratégia. Pense em rodar um teste, com barras de vários dias ou até mesmo anos, se cada uma das barras, fosse criadas de forma a representar o tempo real. Isto seria loucura e totalmente inviável. No entanto, para um sistema de replay / simulador, queremos um comportamento diferente. Queremos que a barra de 1 minuto, venha a ser criada dentro de um tempo de 1 minuto, e de forma a se aproximar ao máximo deste tempo.


Preparando o terreno.

Todo o nosso trabalho, irá ficar restrito apenas ao código do serviço de replay / simulação. Não se preocupe com nenhuma outra coisa por enquanto. Desta forma, começaremos as modificações no código da classe C_Replay. Vamos começar de forma a ter um melhor aproveitamento do código já criado e testado. Desta maneira, o primeiro procedimento a surgir na classe é visto logo abaixo:

inline bool CheckFileIsBar(int &file, const string szFileName)
                        {
                                string  szInfo = "";
                                bool    bRet;
                                
                                for (int c0 = 0; (c0 < 9) && (!FileIsEnding(file)); c0++) szInfo += FileReadString(file);
                                if ((bRet = (szInfo == def_Header_Bar)) == false)
                                {
                                        Print("Arquivo ", szFileName, ".csv não é um arquivo de barras.");
                                        FileClose(file);
                                }
                                
                                return bRet;
                        }

O que estamos fazendo aqui, é trazer para fora da função de leitura das barras, os testes usados para verificar se o arquivo apontado, é ou não um arquivo de barras previas. Isto é importante para que não precisemos repetir o código, quando formos utilizar este mesmo conjunto de códigos, para verificar se o arquivo é ou não de barras. Neste caso, estas barras não será usadas como barras previas. Elas serão convertidas em tickets simulados, a fim de ser usado no sistema de negociação. Sabendo disto, iremos também ter uma outra função:

inline void FileReadBars(int &file, MqlRates &rate[])
                        {
                                rate[0].time = StringToTime(FileReadString(file) + " " + FileReadString(file));
                                rate[0].open = StringToDouble(FileReadString(file));
                                rate[0].high = StringToDouble(FileReadString(file));
                                rate[0].low = StringToDouble(FileReadString(file));
                                rate[0].close = StringToDouble(FileReadString(file));
                                rate[0].tick_volume = StringToInteger(FileReadString(file));
                                rate[0].real_volume = StringToInteger(FileReadString(file));
                                rate[0].spread = (int) StringToInteger(FileReadString(file));
                        }

Esta irá ler os dados, linha por linha das barras presentes no arquivo apontado. Acredito que você não terão dificuldades em entender ambos códigos. Ainda pensando nesta fase de preparação. Temos o surgimento de uma outra rotina:

inline bool OpenFileBars(int &file, const string szFileName)
                        {
                                if ((file = FileOpen("Market Replay\\Bars\\" + szFileName + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE)
                                {
                                        if (!CheckFileIsBar(file, szFileName))
                                                return false;
                                        return true;
                                }
                                Print("Falha ao acessar ", szFileName, ".csv de barras.");
                                
                                return false;
                        }

Agora centralizamos totalmente o nosso sistema, a fim de conseguir um acesso padronizado as barras. Tanto quando formos usá-las como sendo barras previas, quanto formos usá-las como sendo barras, que serão convertidas de forma simulada, em tickets para serem apresentados. Assim, a antiga rotina de carregamento das barras previas, também precisou passar por mudanças, ficando conforme é mostrado abaixo:

bool LoadPrevBars(const string szFileNameCSV)
        {
                int     file,
                        iAdjust = 0;
                datetime dt = 0;
                MqlRates Rate[1];
                                
                if (OpenFileBars(file, szFileNameCSV))
                {
                        Print("Carregando barras previas para Replay. Aguarde ....");
                        while ((!FileIsEnding(file)) && (!_StopFlag))
                        {
                                FileReadBars(file, Rate);
                                iAdjust = ((dt != 0) && (iAdjust == 0) ? (int)(Rate[0].time - dt) : iAdjust);
                                dt = (dt == 0 ? Rate[0].time : dt);
                                CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                        }
                        m_dtPrevLoading = Rate[0].time + iAdjust;
                        FileClose(file);
                        
                        return (!_StopFlag);
                }
                m_dtPrevLoading = 0;
                        
                return false;
        }

A forma como este procedimento de carregamento trabalha, não foi alterado. Apesar de estarmos utilizando um pouco mais de chamadas. O fato de termos retirado de entro do antigo procedimento, as partes que serão utilizadas no novo ponto a ser criado, nos dá uma segurança bem maior, já que todo o código que iremos utilizar, já foi testado. Desta forma então, precisaremos apenas nos preocupar com as novas funcionalidades. Agora que o terreno já está preparado, precisaremos fazer uma nova adição ao arquivo de configuração. Tal adição visa identificar, quais arquivos de barras, deverão ser simulados em termos de tickets. Para fazer isto precisamos adicionar uma nova definição:

#define def_STR_FilesBar        "[BARS]"
#define def_STR_FilesTicks      "[TICKS]"
#define def_STR_TicksToBars     "[TICKS->BARS]"
#define def_STR_BarsToTicks     "[BARS->TICKS]"

Isto já nos habilita a fazer um simples teste. Que é justamente o que precisamos para começar a trabalhar na simulação dos tickets.

                bool SetSymbolReplay(const string szFileConfig)
                        {
#define macroERROR(MSG) { FileClose(file); MessageBox((MSG != "" ? MSG : StringFormat("Ocorreu um erro na linha %d", iLine)), "Market Replay", MB_OK); return false; }
                                int     file,
                                        iLine;
                                string  szInfo;
                                char    iStage;
                                
                                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, def_BarsDiary);
                                m_Ticks.nRate = -1;
                                m_Ticks.Rate[0].time = 0;
                                iStage = 0;
                                iLine = 1;
                                while ((!FileIsEnding(file)) && (!_StopFlag))
                                {
                                        switch (GetDefinition(FileReadString(file), szInfo))
                                        {
                                                case Transcription_DEFINE:
                                                        if (szInfo == def_STR_FilesBar) iStage = 1; else
                                                        if (szInfo == def_STR_FilesTicks) iStage = 2; else
                                                        if (szInfo == def_STR_TicksToBars) iStage = 3; else
                                                        if (szInfo == def_STR_BarsToTicks) iStage = 4; else
                                                                macroERROR(StringFormat("%s não é algo reconhecido dentro do sistema\nna linha %d.", szInfo, iLine));
                                                        break;
                                                case Transcription_INFO:
                                                        if (szInfo != "") switch (iStage)
                                                        {
                                                                case 0:
                                                                        macroERROR(StringFormat("Comando não reconhecido na linha %d\ndo arquivo de configuração.", iLine));
                                                                        break;
                                                                case 1:
                                                                        if (!LoadPrevBars(szInfo)) macroERROR("");
                                                                        break;
                                                                case 2:
                                                                        if (!LoadTicksReplay(szInfo)) macroERROR("");
                                                                        break;
                                                                case 3:
                                                                        if (!LoadTicksReplay(szInfo, false)) macroERROR("");
                                                                        break;
                                                                case 4:
                                                                        if (!LoadBarsToTicksReplay(szInfo)) macroERROR("");
                                                                        break;
                                                        }
                                                        break;
                                        };
                                        iLine++;
                                }
                                FileClose(file);

                                return (!_StopFlag);
#undef macroERROR
                        }

Veja que é muito simples adicionar novas funcionalidades ao código. O simples fato de adicionarmos este teste, já nos dá uma certa capacidade de poder analisar ainda mais coisas. A partir deste ponto, poderemos levar em consideração, o mesmo tipo de comportamento que temos em outros momentos. Qualquer arquivo definido neste estágio, será tratado como sendo um arquivo de barras, que deverá ser convertido em tickets, e isto é feito via esta chamada.

Novamente, devemos sempre que possível, evitar codificar as coisas sem necessidade. Devemos tentar reutilizar sempre, e se possível os códigos já testados. Foi isto que fizemos até o momento. Mas agora vamos entrar em um terreno novo. Mas isto já será assunto para um outro tópico. Mas antes vamos ver uma coisa bastante importante de ser conhecida e entendida.


Pequenas questões antes da implementação

Antes de fato começarmos a implementar o sistema de conversão, temos que analisar uma outra coisa. Você sabe, quantos tipos diferentes de configuração de barras, de fato existe ?!?! Apesar de muita gente imaginar que existem vários tipos. Podemos de fato, resumir todas a configurações possíveis de barras, a apenas e somente 4. Estas são vistas na figura logo abaixo:


Mas por que isto é importante para nos ?!?! Isto é importante, pois define como deverão, e quantas serão as variações, que teremos que implementar. Se você não souber ou não entender, o fato de termos estas 4 e somente estas 4 variações, poderá correr o risco de deixar algum caso descoberto, ou criar muito mais tipos de casos, do que realmente é necessário implementar. Mais uma vez devo salientar, que não existe como você de fato criar um modelo simulado perfeito, a fim de conseguir criar as barras. No máximo, o que você ou qualquer outro irá conseguir, é ter uma estimativa, mais ou menos aproximada, do que pode ter sido o real movimento, que fez aquela barra especifica ser desenhada.

Existem alguns detalhes referentes ao tipo 2, onde o corpo da barra, pode estar na parte superior, ao invés da parte inferior, como é vista na imagem. O fato de isto acontecer, não faz diferença para o sistema que deverá ser implementado. Assim como também, o fato de que a barra ser uma barra de venda ou de compra. Não importa, a implementação será a mesma. O único detalhe é com relação a direção, que deveremos tomar inicialmente. Com isto resumimos ao máximo, o numero de casos que realmente precisaremos implementar. Mas além dos casos que são visto na figura acima, ainda temos que entender uma outra coisa: Quantos tickets mínimos, deveremos de fato criar ?!?! Este também é um fato, que pode ser confuso para alguns. Apesar de que para a pessoa que estiver implementando o sistema de replay / simulação, ou até mesmo um sistema testador de estratégia, isto irá fazer todo o sentido.

Vamos então pensar um pouco: Não é possível utilizar apenas e somente 1 ticket em nenhum sistema. O motivo é que este iria representar apenas uma compra ou venda, e não algum tipo de movimento em si. Assim podemos descartar esta hipótese. Desta forma podemos pensar que o mínimo será 2 tickets, pois assim poderíamos representar um ponto abertura e um fechamento. É uma boa forma de pensar, mas neste caso, também não teríamos nenhum movimento, pois tudo que precisaríamos fazer, seria gerar um ticket que representaria a abertura da barra, e um outro que representaria o fechamento desta mesma barra. 

NOTA: Aqui não estamos tentando gerar os ticks apenas. Queremos de fato gerar um movimento a fim de simular a barra. As questões tratadas aqui irão ser devidamente aprofundadas no decorrer dos próximos artigos. Mas precisamos primeiro projetar um sistema mínimo.

Então o mínimo de tickets que teremos será 3, desta forma uma barra de 1 minuto poderá começar a ter algumas das configurações vistas na figura acima. Mas deve-se lembrar do seguinte: O fato de termos 3 tickets no mínimo, não indica, de forma alguma, que de fato o preço deslocou apenas e somente 1 tick para cima, e um ticket para baixo, ou 3 ticks para cima, ou 3 ticks para baixo. Ele pode ter feito um deslocamento bem diferente deste 1 tick, isto por conta da falta de liquidez no momento que a barra foi de fato criada.

Importante: Talvez você possa vim a confundir alguns dos termos utilizados aqui. Mas vamos deixar a coisa mais clara, para que esta confusão deixe de existir: Quando me refiro ao termo TICKET, estou de fato me referindo ao evento de negociação, ou seja aconteceu um evento de compra ou venda do ativo no preço informado. Já o termo TICK, estou me referindo a menor parte envolvida no que diz respeito ao preço negociado. Para ficar claro esta diferença, pense no seguinte fato: 1 tick no mercado acionário tem um valor de 0,01 ponto, o mesmo 1 tick no dólar futuro equivale a 0,5 ponto, já no índice futuro ele vale 5 pontos.

Apesar disto complicar em alguns pontos, já que a simulação do movimento, deixa de ser totalmente real, e passa a ser um movimento perfeito. Preciso mencionar este fato, para que você tenha em mente, que muitas das vezes, o sistema de simulação de tickets, usando barras de 1 minuto, esta longe de ser o que de fato o que aconteceu ou acontece no mercado. Por conta disto, devemos sempre dar preferencia em utilizar o menor tempo gráfico possível. Já que este tempo gráfico, é o de 1 minuto, você deve sempre utilizar ele.

Talvez você não tenha de fato entendido, o real problema neste esquema de usar barras, como uma forma de criar tickets. Mas vamos pensar no seguinte fato: Se no mercado real, uma barra de 1 minuto, abriu em um determinado preço. Por falta de liquidez, o preço venha a pula 3 ticks. Depois de um tempo, pula 1 ticks para baixo, e fecha nesta última posição. No final, teremos o seguinte cenário configurado:

Talvez a figura acima tenha ficado confusa, e você não a compreendeu. Ela retrata a seguinte informação: No canto esquerdo, temos o real movimento feito em termos de ticks. As barrinhas horizontais, representam o tick. Os círculos, indicam em que preço o ativo, de fato parou entre um ticket e outro. Já a linha em verde, indica o pulo que o preço deu. Veja que houve momentos, em que o preço de fato não foi negociado, em determinados ticks em particular. Mas ao olhar o valor OHCL, não temos a real noção, de que o preço pulou alguns ticks. Assim no final, teríamos ao simular o movimento, utilizando apenas e somente a barra de vela, o que é visto na imagem abaixo.

A linha azul, indica o movimento simulado, ou seja, neste movimento iremos passar por todos os ticks, não importando o que de fato aconteceu, durante a negociação real. Por conta disto, tenha sempre em mente, este fato: Simulação, não é a mesma coisa, de usar dados reais. Por mais bem feito que seja o sistema, que gere um movimento simulado. Ele de fato jamais, irá refletir uma informação real.


Implementando o sistema básico se conversão.

A primeira coisa que de fato temos que fazer, como deve ter ficado claro no tópico anterior. É sabermos qual é o tamanho de um tick de preço. Para fazer isto, teremos que adicionar algumas coisas extras no arquivo de configuração. Estas coisas adicionadas, tem que ser reconhecidas pela classe C_Replay. Assim teremos que adicionar algumas definições, e código extra a esta classe. Começaremos adicionado as seguintes linhas.

#define def_STR_FilesBar        "[BARS]"
#define def_STR_FilesTicks      "[TICKS]"
#define def_STR_TicksToBars     "[TICKS->BARS]"
#define def_STR_BarsToTicks     "[BARS->TICKS]"
#define def_STR_ConfigSymbol    "[CONFIG]"
#define def_STR_PointsPerTicks  "POINTSPERTICK"
#define def_Header_Bar          "<DATE><TIME><OPEN><HIGH><LOW><CLOSE><TICKVOL><VOL><SPREAD>"
#define def_Header_Ticks        "<DATE><TIME><BID><ASK><LAST><VOLUME><FLAGS>"
#define def_BarsDiary           540

Esta linha, irá definir uma string, que será usada para identificar que iremos tratar de dados de configuração. Esta irá nos dar a primeira das configurações, que iremos poder definir a partir de agora. E mais uma vez, teremos que adicionar algumas linhas de código ao nosso sistema, para que estas definições possam ser analisadas e utilizadas. Mas as adições a serem feitas são relativamente simples de serem criadas. No código abaixo vemos o que teremos que fazer:

                bool SetSymbolReplay(const string szFileConfig)
                        {
#define macroERROR(MSG) { FileClose(file); MessageBox((MSG != "" ? MSG : StringFormat("Ocorreu um erro na linha %d", iLine)), "Market Replay", MB_OK); return false; }
                                int     file,
                                        iLine;
                                string  szInfo;
                                char    iStage;
                                
                                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, def_BarsDiary);
                                m_Ticks.nRate = -1;
                                m_Ticks.Rate[0].time = 0;
                                iStage = 0;
                                iLine = 1;
                                while ((!FileIsEnding(file)) && (!_StopFlag))
                                {
                                        switch (GetDefinition(FileReadString(file), szInfo))
                                        {
                                                case Transcription_DEFINE:
                                                        if (szInfo == def_STR_FilesBar) iStage = 1; else
                                                        if (szInfo == def_STR_FilesTicks) iStage = 2; else
                                                        if (szInfo == def_STR_TicksToBars) iStage = 3; else
                                                        if (szInfo == def_STR_BarsToTicks) iStage = 4; else
                                                        if (szInfo == def_STR_ConfigSymbol) iStage = 5; else
                                                                macroERROR(StringFormat("%s não é algo reconhecido dentro do sistema\nna linha %d.", szInfo, iLine));
                                                        break;
                                                case Transcription_INFO:
                                                        if (szInfo != "") switch (iStage)
                                                        {
                                                                case 0:
                                                                        macroERROR(StringFormat("Comando não reconhecido na linha %d\ndo arquivo de configuração.", iLine));
                                                                        break;
                                                                case 1:
                                                                        if (!LoadPrevBars(szInfo)) macroERROR("");
                                                                        break;
                                                                case 2:
                                                                        if (!LoadTicksReplay(szInfo)) macroERROR("");
                                                                        break;
                                                                case 3:
                                                                        if (!LoadTicksReplay(szInfo, false)) macroERROR("");
                                                                        break;
                                                                case 4:
                                                                        if (!LoadBarsToTicksReplay(szInfo)) macroERROR("");
                                                                        break;
                                                                case 5:
                                                                        if (!Configs(szInfo)) macroERROR("");
                                                                        break;
                                                        }
                                                        break;
                                        };
                                        iLine++;
                                }
                                FileClose(file);
                                
                                return (!_StopFlag);
#undef macroERROR
                        }

Aqui estamos dizendo que todas as informações provenientes a partir da próxima linha, serão tratadas pelo sistema no estágio 5 de analise. Já quando uma entrada de informação, é disparada e capturada no estágio 5, teremos uma chama a um procedimento. Para não complicar em demasia o código deste procedimento acima. O procedimento chamado no estágio 5, pode ser visto abaixo.

inline bool Configs(const string szInfo)
                        {
                                string szRet[];
                                
                                if (StringSplit(szInfo, '=', szRet) == 2)
                                {
                                        StringTrimRight(szRet[0]);
                                        StringTrimLeft(szRet[1]);
                                        if (szRet[0] == def_STR_PointsPerTicks) m_PointsPerTick = StringToDouble(szRet[1]); else
                                        {
                                                Print("Variável >>", szRet[0], "<< não definida.");
                                                return false;
                                        }
                                        return true;
                                }
                                Print("Definição de configuração >>", szInfo, "<< invalida.");
                                return false;
                        }

Primeiro capturamos e isolamos o nome da variável interna de replay, do valor que iremos utilizar. Este foi definido pelo usuário, no arquivo de configuração do replay / simulação. O retorno desta operação, deverá ser duas informações: A primeira será o nome da variável a ser definida, e a outra o valor da variável. Este valor poderá ser de tipos diferentes, dependendo da variável, mas para o usuário, isto será completamente invisível. Ele não precisa se preocupar, se o tipo deve ser uma string, double ou integer. A seleção de tipo, será feita aqui, dentro do código.

Antes de realmente utilizarmos estes dados, precisamos limpar qualquer coisa que não seja parte integrante da informação. Este tipo de coisa, normalmente será algum tipo de formatação interna, que o usuário possa ter utilizado, a fim de facilitar a leitura ou escrita dos arquivos de configuração. Qualquer coisa que não seja compreendida, ou ainda não esteja implementada, será considerada uma forma de erro. Isto faz com que retornemos um valor falso, o que fará o serviço finalizar. Depois, é claro, de informar o motivo na caixa de ferramentas da plataforma MetaTrader 5.

Desta forma, nosso arquivo de configuração, ficará conforme mostrado na imagem abaixo. Lembre-se que isto é apenas um exemplo de arquivo de configuração:

[Config]
PointsPerTick = 5

[Bars]
WIN$N_M1_202112060900_202112061824
WIN$N_M1_202112070900_202112071824

[ Ticks -> Bars]

[Ticks]

[ Bars -> Ticks ]
WIN$N_M1_202112080900_202112081824

#Fim do arquivo de configuração...

Antes de prosseguirmos, precisamos fazer um último ajuste no sistema. Este é de fato muito importante. Precisamos fazer um debug no sistema, para verificar se de fato o sistema de barras, esta sendo transformado em um modelo de tickets simulados. A modificação a ser feita, é vista em destaque nos código abaixo:

inline int Event_OnTime(void)
        {
                bool    bNew;
                int     mili, iPos;
                u_Interprocess Info;
                static MqlRates Rate[1];
                static datetime _dt = 0;
                datetime tmpDT = macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
                                
                if (m_ReplayCount >= m_Ticks.nTicks) return -1;
                if (bNew = (_dt != tmpDT))
                {
                        _dt = tmpDT;
                        Rate[0].real_volume = 0;
                        Rate[0].tick_volume = 0;
                }
                mili = (int) m_Ticks.Info[m_ReplayCount].time_msc;
                do
                {
                        while (mili == m_Ticks.Info[m_ReplayCount].time_msc)
                        {
                                Rate[0].close = m_Ticks.Info[m_ReplayCount].last;
                                Rate[0].open = (bNew ? Rate[0].close : Rate[0].open);
                                Rate[0].high = (bNew || (Rate[0].close > Rate[0].high) ? Rate[0].close : Rate[0].high);
                                Rate[0].low = (bNew || (Rate[0].close < Rate[0].low) ? Rate[0].close : Rate[0].low);
                                Rate[0].real_volume += (long) m_Ticks.Info[m_ReplayCount].volume_real;
                                bNew = false;
                                m_ReplayCount++;
                        }
                        mili++;
                }while (mili == m_Ticks.Info[m_ReplayCount].time_msc);
                Rate[0].time = _dt;
                CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                iPos = (int)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks);
                GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                if (Info.s_Infos.iPosShift != iPos)
                {
                        Info.s_Infos.iPosShift = (ushort) iPos;
                        GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                }
                return (int)(m_Ticks.Info[m_ReplayCount].time_msc < mili ? m_Ticks.Info[m_ReplayCount].time_msc + (1000 - mili) : m_Ticks.Info[m_ReplayCount].time_msc - mili);
        }

Um detalhe bastante curioso, e que você pode desligar para poder ver como esta sendo feito o movimento, e isto dentro do gráfico. É desligar a parte de remoção dos valores de segundo, que se encontra nos tickets simulados. Isto irá fazer com que cada um dos tickets simulados, seja plotado no gráfico como sendo uma barra.

Apesar de parecer estranho, pode ajudar a você entender o que esta acontecendo, sem que seja preciso gravar os dados em um arquivo de log. É justamente por conta desta gravação, que teremos de fazer a remoção do valor correspondente aos segundos. Sem fazer isto, cada ticket simulado, irá gerar uma barra, e a final de contas, não queremos de fato isto. Queremos que as barras de 1 minuto, sejam criadas como se elas fossem reais.


Finalmente chegamos na fase da implementação

Antes de irmos para o código em si. Vamos ver qual foi o movimento, que estarei criando neste artigo. Futuramente irei mostrar como deixar ele mais complexo. Mas primeiro precisamos fazer com que ele seja funcional. Na figura abaixo você pode ver como a movimentação irá se dar:

Apesar de parecer simples. Aqui estamos de fato tornando uma barra, em um movimento de tickets simulados. Já que o movimento não é nunca de fato direto, e sim usando um tipo de zig-zag. Foi decidido montá-lo contendo 3 destes movimentos. Mas se você desejar, pode aumentar o numero. Em outros artigos, futuros, irei mostrar como tornar este movimento simples, em algo bastante mais complexo.

Sabendo agora como o movimento irá se dar, podemos passar para o código. O primeiro procedimento, a de fato fazer parte do sistema de conversão, é visto no código logo abaixo:

                bool LoadBarsToTicksReplay(const string szFileNameCSV)
                        {
//#define DEBUG_SERVICE_CONVERT
                                int file, max;
                                MqlRates rate[1];
                                MqlTick tick[];
                                
                                if (OpenFileBars(file, szFileNameCSV))
                                {
                                        Print("Convertendo barras em ticks. Aguarde...");
                                        ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
                                        ArrayResize(m_Ticks.Rate, def_BarsDiary);
                                        ArrayResize(tick, def_MaxSizeArray);
                                        while ((!FileIsEnding(file)) && (!_StopFlag))
                                        {
                                                FileReadBars(file, rate);
                                                max = SimuleBarToTicks(rate[0], tick);
                                                for (int c0 = 0; c0 <= max; c0++)
                                                {
                                                        ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
                                                        m_Ticks.Info[m_Ticks.nTicks++] = tick[c0];
                                                }
                                        }
                                        FileClose(file);
                                        ArrayFree(tick);
                                        
#ifdef DEBUG_SERVICE_CONVERT
        file = FileOpen("Infos.txt", FILE_ANSI | FILE_WRITE);
        for (long c0 = 0; c0 < m_Ticks.nTicks; c0++)
                FileWriteString(file, StringFormat("%s.%03d %f --> %f\n", TimeToString(m_Ticks.Info[c0].time, TIME_DATE | TIME_SECONDS), m_Ticks.Info[c0].time_msc, m_Ticks.Info[c0].last, m_Ticks.Info[c0].volume_real));
        FileClose(file);
#endif

                                        return (!_StopFlag);
                                }
                                
                                return false;
                        }

Apesar de parecer super simples, e de fato ser, esta função é o primeiro dos estágios responsáveis, for fazer a criação da simulação. Vendo ela, você poderá entender pelo menos um pouco, como o sistema está fazendo a simulação. Outra coisa é que, diferente de um testador de estratégia, aqui cada barra de 1 minuto, será simulada de forma, que durante o uso do serviço, esta irá demorar aproximadamente 1 minuto, para ser completamente plotada na tela. Esta é a intenção. Então se a ideia é testar uma estratégia, sugiro você utilizar a ferramenta existente na plataforma MetaTrader 5, e somente utilizar esta ferramenta que esta sendo mostrada como desenvolver, para aprendizado e testes que você deseja fazer de forma manual.

Aqui temos um ponto, que futuramente irá deixar de existir, mas no momento é importante que exista. Trata-se de uma definição, que no momento esta comentada, que permite você criar um arquivo com os dados dos tickets simulados, e assim analisar o grau de complexidade, presente no sistema simulado. É importante notar que o arquivo criado, irá contar apenas com os dados realmente necessários para uma analise, não contendo nenhum dado extra, ou desnecessário.

O restante da função, é praticamente algo simples de entender, pois o código já existia anteriormente. Agora temos uma chamada, que iremos analisar com calma mais a frente. Depois de esta chamada, que cria uma simulação dos tickets. Temos um pequeno laço, apenas para armazenar os tickets na base de dados. Algo bastante simples de ser compreendido, não necessitando de muito mais explicações a respeito.

inline int SimuleBarToTicks(const MqlRates &rate, MqlTick &tick[])
                        {
                                int t0 = 0;
                                long v0, v1, v2, msc;
                                                                
                                m_Ticks.Rate[++m_Ticks.nRate] = rate;
                                Pivot(rate.open, rate.low, t0, tick);
                                Pivot(rate.low, rate.high, t0, tick);
                                Pivot(rate.high, rate.close, t0, tick, true);
                                v0 = (long)(rate.real_volume / (t0 + 1));
                                v1 = 0;
                                msc = 5;
                                v2 = ((60000 - msc) / (t0 + 1));
                                for (int c0 = 0; c0 <= t0; c0++, v1 += v0)
                                {
                                        tick[c0].volume_real = (v0 * 1.0);
                                        tick[c0].time = rate.time + (datetime)(msc / 1000);
                                        tick[c0].time_msc = msc % 1000;
                                        msc += v2;
                                }
                                tick[t0].volume_real = ((rate.real_volume - v1) * 1.0);
                                
                                return t0;
                        }

Esta função acima, já começa a fazer as coisas se tornarem um pouco mais complexas, e por isto mais interessantes. Ao mesmo tempo que nos ajuda a montar toda a estrutura, que precisamos, de uma maneira bem mais estruturada, e gerenciável. A verdade é que, para reduzir a própria complexidade desta função. Criei um outro procedimento, que é chamado, no caso 3 vezes. Se você observar a documentação Ticks reais e gerados - Trading algorítmico, irá notar que lá, o sistema é chamado não 3 mais sim 4 vezes. Você pode até adicionar, mais vezes, se assim desejar, mas como foi dito, irei mostrar como aumentar a complexidade deste sistema, sem que seja de fato necessário adicionar mais chamadas ao procedimento de Pivot.

Continuando a explicar o procedimento acima. Logo depois de termos executado as 3 chamadas a Pivot. Teremos um numero de tickets simulados, que irá depender de como a divisão foi de fato feita. Por conta disto, agora poderemos executar alguns pequenos ajustes nos dados das barras de 1 minuto, assim conseguimos a fazer, com que os dados originais, ainda possam ser de alguma forma utilizados. A primeira coisa é construir uma divisão simplória do volume real, de forma que cada ticket simulado, irá conter uma fração do volume total. Logo depois disto, fazemos um pequeno ajuste do tempo, em que cada ticket simulado irá aparecer. Depois de termos definido, quais serão as frações a serem utilizadas, podemos de fato entrar no laço, e assim fazer com que cada fração seja armazenada no seu devido ticket. No momento, vamos nos ater ao fato de que o sistema deverá de fato funcionar. Se bem que as funções acima, podem ser melhoradas e muito, de forma a deixa as coisas mais interessantes. Agora um detalhe, diferente do tempo, o volume deve ser corrigido de forma a se manter idêntico ao original. Por conta deste detalhe, temos um ultimo cálculo neste procedimento, e este faz a correção para que o volume final da barra de 1 minuto, seja o mesmo do volume inicial.

Agora vamos ver a última função deste artigo, que é a função que irá criar um pivô com base nos valores e parâmetros passados pelo código visto acima. Preciso ressaltar que os valores podem ser ajustados conforme o seu interesse, mas você deve tomar alguns cuidados ao fazer isto, para que a função abaixo continue funcionando de forma adequada.

//+------------------------------------------------------------------+
#define macroCreateLeg(A, B, C) if (A < B)      {               \
                while (A < B)   {                               \
                        tick[C++].last = A;                     \
                        A += m_PointsPerTick;                   \
                                }                               \
                                                } else {        \
                while (A > B)   {                               \
                        tick[C++].last = A;                     \
                        A -= m_PointsPerTick;                   \
                                }               }
                        
inline void Pivot(const double p1, const double p2, int &t0, MqlTick &tick[], bool b0 = false)
                        {
                                double v0, v1, v2;
                                
                                v0 = (p1 > p2 ? p1 - p2 : p2 - p1);
                                v1 = p1 + (MathFloor((v0 * 0.382) / m_PointsPerTick) * m_PointsPerTick * (p1 > p2 ? -1 : 1));
                                v2 = p1 + (MathFloor((v0 * 0.618) / m_PointsPerTick) * m_PointsPerTick * (p1 > p2 ? -1 : 1));
                                v0 = p1;
                                macroCreateLeg(v0, v2, t0);
                                macroCreateLeg(v0, v1, t0);
                                macroCreateLeg(v0, p2, t0);
                                if (b0) tick[t0].last = v0;
                        }
#undef macroCreateLeg
//+------------------------------------------------------------------+

Esta função acima é bastante simples no que tange o seu funcionamento. Apesar dos cálculos dela serem bem estranhos para muitos. Mas se você observar os valores usados para criar o pivô, irá notar que estaremos sempre tentando criar um pivô, usando a primeira e a terceira retração de Fibonacci. Notem primeiramente, que não importa se o pivô será de alta ou baixa, a própria função criará calculo de forma adequada. Agora vem a coisa que faz muita gente, com pouco conhecimento em programação realmente, entrar em desespero: Uma MACRO. O fato de utilizar uma macro, é por que a criação da perna do pivô, é mais facilmente feita via macro. Mas nada impede de você criar uma função para isto. De certa forma, se fossemos utilizar o C++ de forma pura, muito provavelmente esta macro teria um código bastante diferente. No entanto, aqui, usando MQL5, da forma como ela foi criada. dá pra quebra um galho.

É bem melhor utilizar esta macro, do que colocar o seu código dentro das regiões onde ela é declarada.


Conclusão

Nos devemos sempre priorizar um código, que seja simples de ser lido e entendido. E não algo que seja escrito uma vez e se precisar corrigir, ou modificar. Perder horas, tentando entender o que nos mesmos fizemos. Com isto chegamos ao final deste artigo. No vídeo abaixo, você pode ver o resultado neste atual estágio de desenvolvimento, utilizando o que estará no anexo do artigo.

Entretanto gostaria de alertar, para um problema que o sistema passou a ter ao fazermos o uso dos tickets simulados. Este é com relação o sistema de deslocamento, ou busca de uma posição diferente da que o serviço de replay se encontra. Este problema somente irá acontecer, se você utilizar tickets simulados. No próximo artigo iremos corrigir isto, além de fazer outras coisas.



Arquivos anexados |
Market_Replay.zip (50.42 KB)
Desenvolvendo um sistema de Replay - Simulação de mercado (Parte 12): Nascimento do SIMULADOR (II) Desenvolvendo um sistema de Replay - Simulação de mercado (Parte 12): Nascimento do SIMULADOR (II)
Desenvolver um simulador pode ser muito mais interessante do que parece. Então vamos dar mais alguns passos nesta direção, pois a coisa está começando a ficar empolgante.
Receitas MQL5 — Banco de dados de eventos macroeconômicos Receitas MQL5 — Banco de dados de eventos macroeconômicos
Este artigo explora como trabalhar com bancos de dados baseados no mecanismo SQLite. Com o objetivo de oferecer conveniência e utilizar eficientemente os princípios da OOP, foi criada a classe CDatabase. Essa classe é responsável pela criação e gerenciamento de um banco de dados de eventos macroeconômicos. Além disso, são apresentados exemplos de como utilizar diferentes métodos da classe CDatabase.
Teste e otimização de estratégias para opções binárias no MetaTrader 5 Teste e otimização de estratégias para opções binárias no MetaTrader 5
Testamos e otimizamos estratégias de opções binárias no MetaTrader 5.
Desenvolvendo um sistema de Replay - Simulação de mercado (Parte 10): Usando apenas dados reais na replay Desenvolvendo um sistema de Replay - Simulação de mercado (Parte 10): Usando apenas dados reais na replay
Aqui vamos ver como você pode utilizar dados mais fieis ( tickets negociados ) no sistema de replay, sem necessariamente ter que se preocupar se eles estão ou não ajustados.