Indicadores múltiplos em um gráfico (Parte 06): Transformando o MetaTrader 5 em um sistema RAD (II)

Daniel Jose | 14 março, 2022

1.0 - Introdução

No artigo anterior mostrei como criar um Chart Trade usando os objetos do MetaTrader 5, transformando a plataforma em um sistema RAD, o sistema funciona muito bem, e acredito que muitos tenham pensado em criar uma biblioteca para ter cada vez mais funcionalidade no sistema proposto, e assim conseguir desenvolver um EA que seja mais intuitivo ao mesmo tempo que tenha uma interface mais agradável e simples de usar.

A ideia é tão boa, que me motivou a mostrar o passo a passo de como proceder a adição de funcionalidades, aqui irei implementar 2 novas funcionalidades bem básicas, e esta servirão de base para que você possa implementar outras conforme a sua necessidade e desejo, a única limitação é a sua criatividade já que podemos usar os elementos das formas mais diversificadas.


2.0 - Planejamento

A nossa IDE irá mudar conforme as imagens abaixo:

         

fizemos algumas pequenas modificações no próprio design, conforme pode ser visto, mas também adicionei duas novas regiões, uma irá receber o nome do ativo e a outra região irá receber o valor acumulado no dia. Vejam que são coisas das quais podemos viver sem e não irá influenciar em nada nossas decisões, mas pode ser interessante para um ou outro usuário ao mesmo tempo que irei mostrar a forma mais simples e correta de adicionar funcionalidades a nossa IDE. Então quando abrimos a lista de objetos na nova interface vemos a imagem abaixo:


Os dois objetos marcados não tem nenhum evento ligado a eles, ou seja eles não são funcionais pela IDE, todos os outros objetos já estão corretamente vinculados a eventos específicos e o MetaTrader 5 consegue fazer com que os eventos sejam corretamente executados quando eles ocorrem no EA. Ou seja podemos modificar a interface IDE segundo a nossa vontade, mas se a funcionalidade ainda não estiver implementada, o MetaTrader 5 não irá fazer nada, senão apresentar o objeto no gráfico. Mas desejamos que o objeto EDIT 00 receba o nome do ativo que estamos negociando, e este nome deverá aparecer no centro do objeto, já o objeto EDIT 01 irá receber o valor acumulado durante um dado período, no caso iremos usar o período diário, ou seja saberemos se estamos tendo lucro ou prejuízo no dia, se o valor for negativo ele irá ficar em uma cor e se for positivo em outra cor.

Ambos os valores, obviamente, não poderão ser mudados pelo usuário, então podemos deixar as propriedades deles já como somente leitura, conforme mostrado na imagem abaixo.


mas observem que não podemos indicar como a informação irá ser apresentada, ou seja não podemos justificar o texto de forma que ele seja apresentado no meio do objeto, mas desejamos fazer isto, então isto será conseguido via código, já que existe uma propriedade que nos permite justificar o texto, para mais detalhe veja Propriedades do Objeto e observem a tabela sobre  ENUM_ALIGN_MODE, lá é indicado quais objetos podemos usar um texto justificado.

Então seja qual for a modificação que você irá criar, primeiro planeje como será a nova funcionalidade, depois como ela deverá ser apresentada e a forma como o usuário irá e poderá interagir ela, desta forma você irá selecionar o objeto correto e configurá-lo o máximo que for possível, usando a própria interface do MetaTrader 5, de forma que no final você terá uma IDE já planejada, ficando somente para depois ajustar as coisas via código MQL5, deixando a IDE no final 100% funcional. Então vamos começar a fazer as modificações.


3.0 Modificações

Para que o código não se transforme em um verdadeiro frankenstein, devemos nos organizar o máximo possível verificando quais são as funcionalidades que já existem, e quais de fato precisam ser implementadas, muitas vezes, podemos fazer pequenas modificações no código já existente e obter um novo código, que já nascerá testado, este novo código irá ser reutilizado dentro de um que iremos implementar, então tudo que iremos de fato testar será pequenas rotinas de controle que serão adicionadas de forma a gerar uma funcionalidade totalmente nova. Bons programadores sempre fazem isto, tentam reutilizar de alguma forma um código já existente, adicionando para isto pontos de controle.


3.1 Adicionado o nome do ativo

Para implementar esta funcionalidade não precisaremos de grandes mudanças, mas elas tem que ser feitas nos pontos certos, a primeira coisa é adicionar um novo enumerador, o código original é visto abaixo:

enum eObjectsIDE {eRESULT, eBTN_BUY, eBTN_SELL, eCHECK_DAYTRADE, eBTN_CANCEL, eEDIT_LEVERAGE, eEDIT_TAKE, eEDIT_STOP};

o novo código é visto a seguir, a parte destacada é o que foi adicionado, reparem que não adiciono nem no inicio ou no final da lista, isto para evitar ter que mexer em outras partes do código, que já existe e funciona

enum eObjectsIDE {eRESULT, eLABEL_SYMBOL, eBTN_BUY, eBTN_SELL, eCHECK_DAYTRADE, eBTN_CANCEL, eEDIT_LEVERAGE, eEDIT_TAKE, eEDIT_STOP};

se você adicionar um novo valor, seja no inicio ou no final da lista, você teria que procurar e modificar todos os pontos em que estes limites foram usados, e em muitos casos podemos cometer pequenos esquecimentos o que iria gerar erros difíceis de serem encontrados, e você logo iria imaginar que se tratava das novas adições, quando na verdade é por esquecimento, então façamos as mudanças em um ponto entre os extremos.

Feito isto precisamos imediatamente adicionar uma nova mensagem ao sistema, pois senão poderemos correr o risco de que o código gere um erro de RunTime, então adicionamos a seguinte linha no nosso código original

static const string C_Chart_IDE::szMsgIDE[] = {
                                                "MSG_RESULT",
                                                "MSG_NAME_SYMBOL",
                                                "MSG_BUY_MARKET",
                                                "MSG_SELL_MARKET",
                                                "MSG_DAY_TRADE",
                                                "MSG_CLOSE_POSITION",
                                                "MSG_LEVERAGE_VALUE",
                                                "MSG_TAKE_VALUE",
                                                "MSG_STOP_VALUE"
                                              };

vejam que temos que adicionar no mesmo ponto, isto garante a organização, mas neste ponto você pode adicionar a constante em qualquer ponto, isto não fará diferença, já que ela é usada apenas para verificar qual o objeto que irá receber a mensagem, mas por organização a colocamos como sendo a segunda mensagem.

Agora voltemos ao MetaTrader 5 e fazemos a mudança conforme mostrado abaixo:

         

Bem agora o MetaTrader 5 já estará reconhecendo o objeto em nossa IDE como um objeto a receber uma mensagem, falta então criar o procedimento de envio da mensagem, já que é uma mensagem que vamos adicionar um texto e este será feito apenas uma única vez, podemos enviar ela assim que o MetaTrader 5 colocar nossa IDE no gráfico, e isto é poderia ser feito simplesmente adicionando o código necessário no final da rotina Create da nossa classe objeto, mas novamente para o código não ser tornar um frankenstein, cheio de remendos, vamos adicionar o novo procedimento dentro da rotina DispatchMessage. A rotina original é assim:

void DispatchMessage(int iMsg, string szArg, double dValue = 0.0)
{
        if (m_CountObject < eEDIT_STOP) return;
        switch (iMsg)
        {
                case CHARTEVENT_CHART_CHANGE:
                        if (szArg == szMsgIDE[eRESULT])
                        {
                                ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_BGCOLOR, (dValue < 0 ? clrLightCoral : clrLightGreen));
                                ObjectSetString(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_TEXT, DoubleToString(dValue, 2));
                        }
                        break;
                case CHARTEVENT_OBJECT_CLICK:

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

        }
}

após a mudança ficará assim:

void DispatchMessage(int iMsg, string szArg, double dValue = 0.0)
{
        if (m_CountObject < eEDIT_STOP) return;
        switch (iMsg)
        {
                case CHARTEVENT_CHART_CHANGE:
                        if (szArg == szMsgIDE[eRESULT])
                        {
                                ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_BGCOLOR, (dValue < 0 ? clrLightCoral : clrLightGreen));
                                ObjectSetString(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_TEXT, DoubleToString(dValue, 2));
                        }else if (szArg == szMsgIDE[eLABEL_SYMBOL])
                        {
                                ObjectSetString(Terminal.Get_ID(), m_ArrObject[eLABEL_SYMBOL].szName, OBJPROP_TEXT, Terminal.GetSymbol());
                                ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eLABEL_SYMBOL].szName, OBJPROP_ALIGN, ALIGN_CENTER);
                        }
                        break;
                case CHARTEVENT_OBJECT_CLICK:

// ... Restante do código

        }
}

uma vez que a rotina de mensagem já esta criada, podemos escolher o ponto onde iremos enviar a mensagem, e o melhor local de fato e no final da rotina Create da nossa classe objeto, então o código final ficará como o mostrado abaixo:

bool Create(int nSub)
{
        m_CountObject = 0;
        if ((m_fp = FileOpen("Chart Trade\\IDE.tpl", FILE_BIN | FILE_READ)) == INVALID_HANDLE) return false;
        FileReadInteger(m_fp, SHORT_VALUE);
                                
        for (m_CountObject = eRESULT; m_CountObject <= eEDIT_STOP; m_CountObject++) m_ArrObject[m_CountObject].szName = "";
        m_SubWindow = nSub;
        m_szLine = "";
        while (m_szLine != "</chart>")
        {
                if (!FileReadLine()) return false;
                if (m_szLine == "<object>")
                {
                        if (!FileReadLine()) return false;
                        if (m_szLine == "type")
                        {
                                if (m_szValue == "102") if (!LoopCreating(OBJ_LABEL)) return false;
                                if (m_szValue == "103") if (!LoopCreating(OBJ_BUTTON)) return false;
                                if (m_szValue == "106") if (!LoopCreating(OBJ_BITMAP_LABEL)) return false;
                                if (m_szValue == "107") if (!LoopCreating(OBJ_EDIT)) return false;
                                if (m_szValue == "110") if (!LoopCreating(OBJ_RECTANGLE_LABEL)) return false;
                        }
                }
        }
        FileClose(m_fp);
        DispatchMessage(CHARTEVENT_CHART_CHANGE, szMsgIDE[eLABEL_SYMBOL]);
        return true;
}

A parte realmente adicionada esta em verde, notem que fazendo praticamente quase nenhuma modificação, já temos um fluxo de mensagem 100% implementada, e podemos partir para a próxima mensagem a ser implementada.


3.2 Adicionando o resultado acumulado no dia ( Ponto de cobertura )

Novamente seguimos a mesma lógica que fizemos ao adicionar o nome do ativo, então o novo código ficará assim:

enum eObjectsIDE {eRESULT, eLABEL_SYMBOL, eROOF_DIARY, eBTN_BUY, eBTN_SELL, eCHECK_DAYTRADE, eBTN_CANCEL, eEDIT_LEVERAGE, eEDIT_TAKE, eEDIT_STOP};

// ... Restante do código

static const string C_Chart_IDE::szMsgIDE[] = {
                                                "MSG_RESULT",
                                                "MSG_NAME_SYMBOL",
                                                "MSG_ROOF_DIARY",
                                                "MSG_BUY_MARKET",
                                                "MSG_SELL_MARKET",
                                                "MSG_DAY_TRADE",
                                                "MSG_CLOSE_POSITION",
                                                "MSG_LEVERAGE_VALUE",
                                                "MSG_TAKE_VALUE",
                                                "MSG_STOP_VALUE"
                                              };

depois disto vamos modificar a IDE com a nova mensagem:

         

com isto nossa nova IDE esta pronta, agora vamos implementar o código que irá criar a mensagem que conterá o valor acumulado no dia, mas temos uma decisão a tomar : Em qual classe implementar esta rotina ? Muitos talvez iriam criar esta rotina aqui, na classe C_Chart_IDE, mas por motivos de organização será melhor colocar ela junto com as rotinas que tratam das ordens então o código de fato será implementado na classe C_OrderView, o código pode ser visto abaixo:

double UpdateRoof(void)
{
        ulong   ticket;
        int     max;
        string  szSymbol = Terminal.GetSymbol();
        double  Accumulated = 0;
                                
        HistorySelect(macroGetDate(TimeLocal()), TimeLocal());
        max = HistoryDealsTotal();
        for (int c0 = 0; c0 < max; c0++) if ((ticket = HistoryDealGetTicket(c0)) > 0)
                if (HistoryDealGetString(ticket, DEAL_SYMBOL) == szSymbol)
                        Accumulated += HistoryDealGetDouble(ticket, DEAL_PROFIT);
                                                
        return Accumulated;
}

Agora que o código foi implementado precisamos adicionar a mensagem ao sistema, mas para facilitar a vida do operador eu já coloquei o código de forma que ele irá informar os resultados já finalizados, então o código ficará desta forma:

void DispatchMessage(int iMsg, string szArg, double dValue = 0.0)
{
        static double AccumulatedRoof = 0.0;
        bool    b0;
        double  d0;

        if (m_CountObject < eEDIT_STOP) return;
        switch (iMsg)
        {
                case CHARTEVENT_CHART_CHANGE:
                        if ((b0 = (szArg == szMsgIDE[eRESULT])) || (szArg == szMsgIDE[eROOF_DIARY]))
                        {
                                if (b0)
                                {
                                        ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_BGCOLOR, (dValue < 0 ? clrLightCoral : clrLightGreen));
                                        ObjectSetString(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_TEXT, DoubleToString(dValue, 2));
                                }else
                                {
                                        AccumulatedRoof = dValue;
                                        dValue = 0;
                                }
                                d0 = AccumulatedRoof + dValue;
                                ObjectSetString(Terminal.Get_ID(), m_ArrObject[eROOF_DIARY].szName, OBJPROP_TEXT, DoubleToString(MathAbs(d0), 2));
                                ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eROOF_DIARY].szName, OBJPROP_BGCOLOR, (d0 >= 0 ? clrForestGreen : clrFireBrick));
                        }else   if (szArg == szMsgIDE[eLABEL_SYMBOL])
                        {
                                ObjectSetString(Terminal.Get_ID(), m_ArrObject[eLABEL_SYMBOL].szName, OBJPROP_TEXT, Terminal.GetSymbol());
                                ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eLABEL_SYMBOL].szName, OBJPROP_ALIGN, ALIGN_CENTER);
                        }
                        break;
                case CHARTEVENT_OBJECT_CLICK:

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

        }
}

As parte destacadas dão suporte ao sistema como descrito acima, se a implementação não fosse feita desta forma iriamos ter que enviar 2 mensagens para o sistema de forma a atualizar as informações corretamente, mas usando a forma como o código foi implementado podemos acompanhar o resultado tanto de uma posição aberta quando do dia com apenas uma única mensagem.

Agora o EA irá contar com uma outra modificação que é na rotina OnTrade e esta modificação pode ser vista abaixo:

void OnTrade()
{
        SubWin.DispatchMessage(CHARTEVENT_CHART_CHANGE, C_Chart_IDE::szMsgIDE[C_Chart_IDE::eROOF_DIARY], NanoEA.UpdateRoof());
        NanoEA.UpdatePosition();
}

Apesar deste sistema funcionar, temos que ter cuidado em relação ao tempo de execução da rotina OnTrade que junto com a OnTick pode degradar em muito a performance do EA. No caso do código contido na OnTick não tem muito jeito, a otimização é algo bem critico, mas na rotina OnTrade a coisa é mais simples, já que a rotina é de fato chamada quando existe alguma alteração na posição. Sabendo disto temos então duas alternativas, a primeira é mudar a rotina UpdateRoof de forma a limitar o tempo de execução dela, a outra alternativa é mudar a própria rotina OnTrade, mas por questões praticas vamos modificar a rotina UpdateRoof e assim melhorar pelo menos um pouco o tempo de execução quando estamos com uma posição aberta. A nova rotina ficará como mostrado abaixo:

double UpdateRoof(void)
{
        ulong           ticket;
        string  szSymbol = Terminal.GetSymbol();
        int             max;
        static int      memMax = 0;
        static double   Accumulated = 0;
                
        HistorySelect(macroGetDate(TimeLocal()), TimeLocal());
        max = HistoryDealsTotal();
        if (memMax == max) return Accumulated; else memMax = max;
        for (int c0 = 0; c0 < max; c0++) if ((ticket = HistoryDealGetTicket(c0)) > 0)
                if (HistoryDealGetString(ticket, DEAL_SYMBOL) == szSymbol)
                        Accumulated += HistoryDealGetDouble(ticket, DEAL_PROFIT);
                                                
        return Accumulated;
}

As linhas em destaque são códigos que foram adicionados a rotina original, mas apesar de aparentemente não fazer muita diferença, elas fazem sim muita diferença, vamos entender o por que disto. Quando o código é referenciado pela primeira vez, tanto a variável estática memMax quando Accumulated estarão zeradas, se não houve nenhum valor no histórico de ordens no período especificado, o teste irá refletir isto e a rotina irá retornar, mas se houver algum dado ele será testado, e tanto a variável memMax quanto Accumulated irão refletir a nova condição. Pois bem, o fato destas variáveis serem estáticas faz com que seus valores sejam mantidos entre as chamadas, então quando o valor da posição for alterado por conta do movimento natural o ativo o MetaTrader 5 irá gerar um evento que faz a função OnTrade ser executada, neste ponto temos uma nova chamada da função UpdateRoof e se a posição não foi fechada, a função irá retornar no ponto de teste, fazendo a rotina retornar mais rapidamente.


Conclusão

Neste artigo demostrei como adicionar novas funcionalidades ao sistema RAD possibilitando assim você a criar uma biblioteca que torna o sistema ideal para criação de interface IDE com muito mais facilidade e com menos geração de erros ao montar uma interface de interação e controle, a partir deste ponto a única real limitação será a sua criatividade, já que aqui explorei apenas o uso exclusivo do MQL5, mas você pode integrar esta mesma ideia a bibliotecas externas expandindo assim em muito as possibilidades de criação de uma IDE.