Desenvolvendo um EA de negociação do zero (Parte 07): Adicionando o Volume At Price (I)

Daniel Jose | 22 março, 2022

Introdução

Para quem opera e tenta ter um certo grau de assertividade, não pode deixar de ter este indicador em seu gráfico, apesar de ele ser mais utilizado por quem opera observando o Fluxo (Tape Reading ) ele também pode ser usado por aqueles que fazem uso apenas do Price Action. E sendo um indicador de volume horizontal, onde podemos analisar qual foi o volume de negócios que saiu em um determinado preço, ele é extremamente útil, apesar de muitos não conseguirem fazer a correta leitura dele, no fim do artigo deixarei uma referência para estudo.

Aqui não irei mostrar como fazer esta leitura, pois isto estaria fora do escopo deste artigo, a ideia aqui é mostrar como desenvolver e criar este indicador de forma que ele não venha a gerar perda de performance da plataforma MetaTrader 5. Um fato interessante é que apesar de muitos imaginarem que este indicador tem que ser atualizado em tempo real ( Real Time ), na verdade um pouco de delay é aceitável, desde que ele seja bem baixo, pela minha experiência não vi grandes problemas em se ter um delay próximo de 1 segundo na atualização das informações, mas se você desejar usar ele em Real Time, terá que fazer pequenas mudanças, não no indicador em si, mas nos pontos que que ele é acessado pelo EA, de forma a ter o comportamento de Real Time, mas a influência na leitura será mínima, para não dizer insignificante.


Interface

A interface para controle da classe do Volume At Price é bem simples, mas para poder ter um total controle sobre ela, é preciso que o gráfico onde o indicador será aplicado, esteja com as propriedades corretas mostrado na figura abaixo, onde o principal elemento de controle esta em destaque

Caso a grade não esteja visível, não será possível redimensionar o indicador da maneira como é mostrado nas animações a seguir. Notem que a interface é bem simples é bastante intuitiva contando com apenas 2 controles, um que indica a dimensão e outro qual é o ponto inicial para analisar o volume.

    

Apesar de tudo este indicador é bastante eficiente e bem interessante de ser implementado e construído, mas aqui ele esta no seu nível mais básico e irá sofrer melhorias futuras em um outro artigo.

Já que não tenho muito mais a falar sobre a interface, vamos então seguir para a implementação do código.


Implementação

Para se ter o menor trabalho possível ao criar este indicador, o nosso código original sofreu alguns desmembramentos e várias pequenas modificações além de alguns acréscimos, mas vamos começar com os desmembramentos já que muito do que precisamos já se encontra escrito em outros pontos, e o principal se encontra na classe C_Wallpaper. Mas como assim ? Nos vamos criar um indicador com base em um bitmap ?!?! SIM, toda e qualquer imagem presente na tela de um computador, deve ser pensada como um BITMAP, mas construída de uma forma especifica. Então a nova classe objeto C_Wallpaper ficará como mostrado abaixo:

class C_WallPaper : public C_Canvas
{
        protected:
                enum eTypeImage {IMAGEM, LOGO, COR};
//+------------------------------------------------------------------+
        private :
        public  :
//+------------------------------------------------------------------+
                ~C_WallPaper()
                        {
                                Destroy();
                        }
//+------------------------------------------------------------------+
                bool Init(const string szName, const eTypeImage etype, const char cView = 100)
                        {
                                if (etype == C_WallPaper::COR) return true;
                                if (!Create(szName, 0, 0, Terminal.GetWidth(), Terminal.GetHeight())) return false;
                                if(!LoadBitmap(etype == C_WallPaper::IMAGEM ? "WallPapers\\" + szName : "WallPapers\\Logos\\" + _Symbol, cView)) return false;
                                ObjectSetInteger(Terminal.Get_ID(), szName, OBJPROP_BACK, true);

                                return true;
                        }
//+------------------------------------------------------------------+
                void Resize(void)
                        {
                                ResizeBitMap(Terminal.GetWidth(), Terminal.GetHeight());
                        }
//+------------------------------------------------------------------+
};

vejam como o código ficou muito mais compacto, mas ao mesmo tempo retiramos as partes que são comuns entre a classe C_Wallpaper e C_VolumeAtPrice e colocamos tudo em uma outra classe: a classe C_Canvas.

Mas por que eu não uso a classe C_Canvas presente no próprio MetaTrader 5 ? A questão é mais pessoal do que prática, eu gosto de ter um controle maior sobre cada coisa que esta sendo projetada e programada, é mais um vício de programador C do que algo de fato necessário, dai o fato de criar uma classe para desenhar na tela, mas nada impede de você usar a classe já presente no MetaTrader 5. Mas vamos nos focar na classe C_VolumeAtPrice que é o tema principal deste artigo. Esta classe tem 7 funções que podem ser vistas na tabela abaixo.

Função Descrição Tipo de acesso 
Init Inicializa a classe com os valores definidos pelo usuário Publico
Update Atualiza os dados do Volume At Price dentro dos limites de horário definidos Publico
Resize Redimensiona a imagem do Volume At Price no gráfico, isto permite você analisar alguns detalhes com mais facilidade Publico
DispatchMessage  Usado para despachar mensagens para dentro da classe objeto Publico
FromNowOn  Inicializa o sistema de variáveis Privado
SetMatrix Cria e mantém o array com os dados do volume Privado
Redraw Renderiza a imagem do volume Privado

Vamos então começar a implementar o sistema, começando com a declaração das variáveis, estas podem ser vista no fragmento de código abaixo:

#define def_SizeMaxBuff                 4096
//+------------------------------------------------------------------+
#define def_MsgLineLimit                "Ponto inicial do Volume At Price"
//+------------------------------------------------------------------+
class C_VolumeAtPrice : private C_Canvas
{
#ifdef macroSetInteger
        ERROR ...
#endif
#define macroSetInteger(A, B) ObjectSetInteger(Terminal.Get_ID(), m_Infos.szObjEvent, A, B)
        private :
                uint    m_WidthMax,
                        m_WidthPos;
                bool    m_bChartShift,
                        m_bUsing;
                double  m_dChartShift;
                struct st00
                {
                        ulong   nVolBuy,
                                nVolSell,
                                nVolTotal;
                        long    nVolDif;
                }m_InfoAllVaP[def_SizeMaxBuff];
                struct st01
                {
                        ulong    memTimeTick;
                        datetime StartTime,
                                 CurrentTime;
                        int      CountInfos;
                        ulong    MaxVolume;
                        color    ColorSell,
                                 ColorBuy,
                                 ColorBars;
                        int      Transparency;
                        string   szObjEvent;
                        double   FirstPrice;
                }m_Infos;

A parte destacada neste fragmento é algo que você deve se atentar, ela garante que a definição não esta vindo de outro arquivo de forma a entrar em contradição com a definição que iremos usar neste arquivo. É bem verdade que o compilador do MQL5 irá gerar um alerta quando uma definição existente venha a sofre uma tentativa de ser redefinida, mas em alguns casos é complicado saber como resolver isto, então para facilitar um pouco a nossa vida, usamos um teste como indicado em destaque. As demais coisas neste fragmento não chamam tanto a atenção, mas existe algo que você deve ficar atento, é a definição def_SizeMaxBuff ela diz qual será o tamanho de nosso array com as informações de volume, caso você precise, mude este valor para outro, mas com os testes que foram feitos este valor é mais que adequado para uma grande maioria dos casos, já que ele representa o número de variações, em ticks, entre o preço mínimo e o seu valor máximo, ou seja o valor atual dá conta de uma gama enorme de casos.


Função Init - Onde as coisas começam

Esta função é a que de fato inicializa todas as variáveis de forma adequada, no EA ela é chamada conforme mostra fragmento abaixo:

//.... Dados iniciais ....

input color     user10   = clrForestGreen;      //Cor da linha Take Profit
input color     user11   = clrFireBrick;        //Cor da linha Stop
input bool      user12   = true;                //Day Trade ?
input group "Volume At Price"
input color     user15  = clrBlack;             //Cor das barras
input char      user16  = 20;                   //Transparencia (0 a 100 )
//+------------------------------------------------------------------+
C_SubWindow             SubWin;
C_WallPaper             WallPaper;
C_VolumeAtPrice         VolumeAtPrice;
//+------------------------------------------------------------------+          
int OnInit()
{
        Terminal.Init();
        WallPaper.Init(user03, user05, user04);
        if ((user01 == "") && (user02 == "")) SubWin.Close(); else if (SubWin.Init())
        {
                SubWin.ClearTemplateChart();
                SubWin.AddThese(C_TemplateChart::SYMBOL, user02);
                SubWin.AddThese(C_TemplateChart::INDICATOR, user01);
        }
        SubWin.InitilizeChartTrade(user06, user07, user08, user09, user10, user11, user12);
        VolumeAtPrice.Init(user10, user11, user15, user16);

// ... Restante do código

Veja que ela precisa de poucos parâmetros, e que basicamente eles são informações sobre as cores a serem usadas pelo indicador, então vamos ver o código interno desta função, o fragmento abaixo mostra como as coisas são inicializadas:

void Init(color CorBuy, color CorSell, color CorBar, char cView)
{
        m_Infos.FirstPrice = Terminal.GetRatesLastDay().open;
        FromNowOn(macroSetHours(macroGetHour(Terminal.GetRatesLastDay().time), TimeLocal()));
        m_Infos.Transparency = (int)(255 * macroTransparency(cView));
        m_Infos.ColorBars = CorBar;
        m_Infos.ColorBuy = CorBuy;
        m_Infos.ColorSell = CorSell;
        if (m_bUsing) return;
        m_Infos.szObjEvent = "Event" + (string)ObjectsTotal(Terminal.Get_ID(), -1, OBJ_EVENT);
        CreateObjEvent();
        m_bChartShift = ChartGetInteger(Terminal.Get_ID(), CHART_SHIFT);
        m_dChartShift = ChartGetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE);
        ChartSetInteger(Terminal.Get_ID(), CHART_SHIFT, true);
        ChartSetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE, 0.1);
        Create("VaP" + (string)MathRand(), 0, 0, 1, 1);
        Resize();
        m_bUsing = true;
};

Vejam que é algo bem simples, mas tem algumas peculiaridades que torna o código interessante e intrigante para alguns, uma delas esta em destaque: Terminal.GetRatesLastDay().open este pode ser bem estranho para alguns, mas é algo muito comum quando a programação segue os princípios de um código orientado a objetos ( OOP ), um destes princípios diz que nenhum código fora da classe deverá ter acesso as variáveis internas da classe. Mas então como vamos obter os valores das variáveis internas da classe ?!?! A forma correta é usando uma forma que só aparece em uma OOP, então vamos ver como é declarado a rotina GetRatesLastDay dentro da classe C_Terminal, isto pode ser visto no fragmento abaixo:

inline MqlRates GetRatesLastDay(void) const { return m_Infos.Rates; }

Vamos entender como de fato isto funciona. Começando com a palavra reservada inline, esta irá dizer ao compilador que o código deverá ser colocado em todas a posições em que ele aparecer, ou seja ao invés do compilador gerar uma chamada para a função ela na verdade irá copiar todo o código da rotina para o ponto onde a rotina esta sendo referenciada. Isto faz com que o código execute mais rapidamente em sacrifício ao consumo de memória. Mas no caso especifico o que de fato acontece é que a variável m_Infos.Rates será referenciada, esta variável é do tipo MqlRates, ou seja poderemos acessar os valores da estrutura MqlRates, no caso não estamos passando a referencia de endereço da variável, mas em alguns casos para deixar o código mais rápido, o que de fato fazemos é passar a referencia de endereço, e se isto acontecer poderemos mudar o valor da variável interna da classe, o que deveria a principio ser proibido, para garantir que isto não irá ocorrer usamos a palavra reservada const que garante que a variável nunca será modificada sem que seja pelo procedimento adequado da classe. Apesar de muitas das palavras reservadas presentes no C++ também estarem presentes no MQL5, de forma documentada, algumas ainda não foram documentadas, mas fazem parte do MQL5 por ele estar muito próximo de um código nativo do C++. No final do artigo irei deixar referencias para quem desejar aprender um pouco mais sobre C++ e usar este mesmo conhecimento na programação em MQL5.

Mas inda dentro do código da função Init temos um fragmento bem curioso e interessante, destaquei ele aqui abaixo, para explicar o que esta acontecendo:

m_bChartShift = ChartGetInteger(Terminal.Get_ID(), CHART_SHIFT);
m_dChartShift = ChartGetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE);
ChartSetInteger(Terminal.Get_ID(), CHART_SHIFT, true);
ChartSetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE, 0.1);

Quando o EA entra em atividade ele irá mudar o gráfico, mas é de boa prática, devolver as coisas ao original quando o sistema for desativado pelo usuário, então salvamos a configuração de deslocamento do gráfico, e depois criamos um deslocamento mínimo, e isto é feito pelos pontos destacados, por isto precisamos que a grade esteja visível no gráfico, para podermos ajustar esta dimensão, e isto é feito de forma interativa conforme foi visto no inicio do artigo, para mais detalhes veja CHART_SHIFT .


Assegurando os objetos na tela

Apesar das funções internas da classe serem bem simples, existem alguns pontos que merecem algum destaque, o primeiro é o sistema de segurança que evita o usuário remover o ponto que indica o inicio da analise do volume este ponto é visto na base do gráfico conforme mostra a figura abaixo:


O ponto é bem pequeno então deve-se ter atenção para realmente notar ele. NOTA IMPORTANTE : Caso queira mudar o ponto de analise, você deve se atentar ao timeframe do gráfico, se você desejar mover a analise de 9:00 para por exemplo 9:02 você deverá usar um TIMEFRAME de 1 minuto ou 2 minutos, se estiver usando o gráfico em 5 minutos por exemplo, você não conseguirá fazer isto. Tendo entendido isto, temos que tomar cuidado para que este elemento não seja acidentalmente deletado pelo usuário, isto é conseguido pelo seguinte fragmento de código:

void DispatchMessage(int iMsg, string sparam)
{
        switch (iMsg)
        {

// ... Partes internas do código

                case CHARTEVENT_OBJECT_DELETE:
                        if ((sparam == m_Infos.szObjEvent) && (m_bUsing))
                        {
                                m_bUsing = false;
                                CreateObjEvent();
                                Resize();
                                m_bUsing = true;
                        }
                break;
        }                       
};

Quando a classe percebe que o objeto foi deletado, imediatamente ela irá recriar este objeto novamente, isto impede que o usuário fique sem um objeto que é útil para a classe, e assim seja obrigado a reiniciar o EA. Use este modelo mostrado no fragmento sempre que desejar assegurar que o usuário não delete um objeto sensível. Mas para garantir que o evento será visto pelo EA, você deve adicionar um código extra:

ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, true);

Esta simples linha garante que o MetaTrader 5 irá informar que algum objeto foi deletado. Para mais detalhes veja CHART_EVENT_OBJECT_DELETE


Montando o gráfico do Volume At Price

Este é o coração da classe ela conta com 3 funções, 1 publica e 2 privativas, vamos começar com a função publica que pode ser vista abaixo:

inline virtual void Update(void)
{
        MqlTick Tick[];
        int i1, p1;

        if (m_bUsing == false) return;
        if ((i1 = CopyTicksRange(Terminal.GetSymbol(), Tick, COPY_TICKS_TRADE, m_Infos.memTimeTick)) > 0)
        {
                if (m_Infos.CountInfos == 0)
                {
                        macroSetInteger(OBJPROP_TIME, m_Infos.StartTime = macroRemoveSec(Tick[0].time));
                        m_Infos.FirstPrice = Tick[0].last;
                }                                               
                for (p1 = 0; (p1 < i1) && (Tick[p1].time_msc == m_Infos.memTimeTick); p1++);
                for (int c0 = p1; c0 < i1; c0++) SetMatrix(Tick[c0]);
                if (p1 == i1) return;
                m_Infos.memTimeTick = Tick[i1 - 1].time_msc;
                m_Infos.CurrentTime = macroRemoveSec(Tick[i1 - 1].time);
                Redraw();
        };      
};

As linhas em destaque são muito importantes para o sistema, quando o sistema entra, ele não sabe exatamente onde começar, e estas linhas fazem a atualização destes pontos, elas informam ao usuário onde a analise começou e qual foi o preço inicial para que o sistema possa criar a tabela interna, o sistema sempre ira esperar que um novo tick entre, assim que isto acontecer teremos dados a serem analisados e montados de forma a serem apresentados na tela e isto nos leva a próxima função:

inline void SetMatrix(MqlTick &tick)
{
        int pos;
                                
        if ((tick.last == 0) || ((tick.flags & (TICK_FLAG_BUY | TICK_FLAG_SELL)) == (TICK_FLAG_BUY | TICK_FLAG_SELL))) return;
        pos = (int) ((tick.last - m_Infos.FirstPrice) / Terminal.GetPointPerTick()) * 2;
        pos = (pos >= 0 ? pos : (pos * -1) - 1);
        if ((tick.flags & TICK_FLAG_BUY) == TICK_FLAG_BUY) m_InfoAllVaP[pos].nVolBuy += tick.volume; else
        if ((tick.flags & TICK_FLAG_SELL) == TICK_FLAG_SELL) m_InfoAllVaP[pos].nVolSell += tick.volume;
        m_InfoAllVaP[pos].nVolDif = (long)(m_InfoAllVaP[pos].nVolBuy - m_InfoAllVaP[pos].nVolSell);
        m_InfoAllVaP[pos].nVolTotal = m_InfoAllVaP[pos].nVolBuy + m_InfoAllVaP[pos].nVolSell;
        m_Infos.MaxVolume = (m_Infos.MaxVolume > m_InfoAllVaP[pos].nVolTotal ? m_Infos.MaxVolume : m_InfoAllVaP[pos].nVolTotal);
        m_Infos.CountInfos = (m_Infos.CountInfos == 0 ? 1 : (m_Infos.CountInfos > pos ? m_Infos.CountInfos : pos));
}

Esta função pode não ser lá grandes coisas, já ela apenas armazena e manter os valores de volume no preço, mas as linhas em destaque nela estas são a alma do sistema. Para entender de fato o que esta acontecendo nestas duas linhas é preciso pensar um pouco. Vamos imaginar o seguinte: Qual é mais rápido de ser feito, guardar cada um dos preços e ir anotando os volumes em cada um deles, ou guardar apenas os volumes imaginando qual é o preço ? A segunda opção é mais rápida, então vamos apenas guardar os volumes imaginando onde é o preço. Mas qual seria o primeiro preço do sistema ? pois sim precisamos de um valor inicial, sem ele a coisa iria desmoronar. Bem que tal usarmos o preço do primeiro tick que foi negociado ? A sim, isto é ótimo. Perfeito. Mas temos um problema, se o preço estiver subindo, ótimo, todos os dados poderão ser armazenados em um array facilmente, mas e se eles caírem ? Neste caso teremos valores negativos, e não podemos acessar um array com um index negativo. Então poderiam usar 2 array ao invés de 1, mas isto nos traria um sobre trabalho desnecessário, mas existe uma solução simples, observem a tabela abaixo:


Se o index for positivo estamos tranquilos, mas se ele for negativo teremos problemas, bem estamos usando 1 array com duas direções, onde o valor Zero representa o preço do primeiro tick, valores negativos são valores que vieram abaixo, e valores positivos são os que vieram acima. Agora acompanhem o raciocínio: Se temos 2 direções, multiplicando os index por 2 teremos a coluna do meio, isto parece não nos ajudar, mas se tornarmos os valores negativos em positivos e subtrairmos 1 teremos a coluna da direita, e se observarem com calma verão que esta coluna da direita, os valores se intercalam nos dando um index perfeito para ser usado para acessar um array que sabemos que irá crescer, mas não sabemos o quanto irá crescer. E é justamente isto que as duas linhas destacadas fazem, criam um index para nosso array, intercalando valores que estão acima com os que estão abaixo do preço inicial. Mas apesar de ser uma solução muito boa, ela nada irá valer se não viermos a conseguir plotar os dados na tela, e é justamente isto que a próxima função faz.

void Redraw(void)
{
        uint x, y, y1, p;
        double reason = (double) (m_Infos.MaxVolume > m_WidthMax ? (m_WidthMax / (m_Infos.MaxVolume * 1.0)) : 1.0);
        double desl = Terminal.GetPointPerTick() / 2.0;
        Erase();
        p = m_WidthMax - 8;
        for (int c0 = 0; c0 <= m_Infos.CountInfos; c0++)
        {
                if (m_InfoAllVaP[c0].nVolTotal == 0) continue;
                ChartTimePriceToXY(Terminal.Get_ID(), 0, 0, m_Infos.FirstPrice + (Terminal.GetPointPerTick() * (((c0 & 1) == 1 ? -(c0 + 1) : c0) / 2)) + desl, x, y);
                y1 = y + Terminal.GetHeightBar();
                FillRectangle(p + 2, y, p + 8, y1, macroColorRGBA(m_InfoAllVaP[c0].nVolDif > 0 ? m_Infos.ColorBuy : m_Infos.ColorSell, m_Infos.Transparency));
                FillRectangle((int)(p - (m_InfoAllVaP[c0].nVolTotal * reason)), y, p, y1, macroColorRGBA(m_Infos.ColorBars, m_Infos.Transparency));
        }
        C_Canvas::Update();
};

Esta função irá plotar o volume no gráfico, e a parte destacada é a responsável por reverter o calculo executado na durante a captura dos volumes, e para que a apresentação fique no ponto adequando o preço sofre um pequeno deslocamento, desta forma as barras ficam corretamente posicionadas, o restante da rotina são apenas rotinas de pintura. Vale uma explicação aqui. Observem que existem duas chamadas a FillRectangle, e por que disto ? A primeira chamada irá colocar uma indicação de qual volume foi maior, se dos vendedores ou compradores, e a segunda chamada irá de fato plotar o volume. Mas por que não ploto as coisas junto, dividindo a barra de volume entre os compradores e os vendedores ? O motivo é que conforme o volume vai subindo em uma faixa de preço, irá começar a prejudicar a analise em outras faixa menores, ficando difícil identificar qual foi o volume maior, se foi de venda ou de compra, e colocando desta forma este problema desaparece, então a leitura fica mais simples e direta, no final o gráfico ficará conforme mostra a figura baixo:


Todas as demais rotinas na classe servem para dar suporte a estas explicadas, não tendo assim grande relevância a ponto de precisar entrar em detalhes sobre elas.


Conclusão

Aqui apresentei um modelo bem simples de volume at price, mas extremamente eficaz, se você esta começando a aprender programação, e querendo focar em ( OOP ) programação orientada em objetos, estude este código com calma, pois nele existem vários conceitos que são muito bons, já que todo o código esta sendo focado para ser 100% orientado em objetos.

Anexo temos o EA até a presente fase de desenvolvimento.


Referências