TradeObjects: Automação de negociação com base em objetos gráficos na MetaTrader

Stanislav Korotky | 27 outubro, 2017

As construções geométricas em gráficos de símbolos têm permanecido uma das ferramentas de negociação mais populares há décadas. Com o avanço das tecnologias tornou-se mais fácil a aplicação de linhas de suporte ou resistência, níveis históricos de preços e padrões inteiros, por exemplo, canais e grade de Fibo. O software de negociação algorítmica permite a ambos analisar padrões clássicos e negociar usando eles. A MetaTrader também possui aplicativos que automatizam o processo até certo ponto: na maioria dos casos, basta adicionar um objeto ao gráfico com um EA ou script já iniciado. O aplicativo abrirá uma posição, acompanhará e fechará no horário de acordo com as configurações. Essas aplicações nos permitem não só negociar online, mas também polir nossas habilidades no modo de visualização do testador. Nós podemos encontrar tais aplicações na CodeBase e no Mercado.

No entanto, tudo não é tão simples. Geralmente, os aplicativos na CodeBase têm funcionalidades simplificadas. Além disso, eles raramente são atualizados e tornam-se obsoletos rapidamente (muitas vezes perdendo a compatibilidade com as últimas versões da linguagem e da plataforma MQL), enquanto que os produtos comerciais são geralmente muito caros.

Neste artigo, nós desenvolveremos uma nova ferramenta que representa o meio termo. Ela será o mais simples possível e proporcionará amplas oportunidades ao mesmo tempo. Ela será compatível com a MetaTrader 4 e MetaTrader 5. Graças ao código-fonte aberto, ela pode ser facilmente expandida e modificada para atender às suas necessidades.

O suporte para a MetaTrader 4 é importante não só em termos de atingir uma audiência maior, mas também devido a algumas limitações do testador da MetaTrader 5. Em particular, o testador visual da MetaTrader 5 atualmente não permite trabalhar com objetos de forma interativa (adicionar, excluir, editar propriedades), o que é necessário para essa ferramenta. Somente o testador da MetaTrader 4 nos permite aprimorar nossas habilidades de gerenciamento de objetos no histórico.

Definição dos requisitos

O principal requisito ideológico para o nosso sistema de negociação automatizado envolvendo objetos gráficos é a simplicidade. Nós vamos usar a interface padrão da MetaTrader e as propriedades de objetos gráficos sem painéis especiais e lógica de configuração complexa. Como mostra a prática, todas as vantagens dos sistemas poderosos com várias opções geralmente são niveladas pelas dificuldades de desenvolvimento e baixa relevância de cada modo de operação particular. Nós nos esforçaremos para usar apenas os métodos mais conhecidos de trabalho com objetos e uma interpretação intuitiva de suas propriedades.

Entre os variados tipos de objetos gráficos para tomada de decisões de negociação, os mais usados ​​são os seguintes:

  • linha de tendência;
  • linha horizontal;
  • linha vertical;
  • canal equidistante;
  • retração de Fibonacci.

Primeiramente, nós vamos dar suporte a eles. Esta lista poderia ter sido estendida por novos tipos disponíveis na MetaTrader 5, mas isso teria quebrado a compatibilidade com a MetaTrader 4. Os usuários poderão facilmente adicionar outros objetos de acordo com suas preferências, aplicando os princípios básicos de processamento de objetos implementados neste projeto.

Cada um dos objetos mencionados acima constitui um limite lógico no espaço bidimensional do gráfico. O rompimento ou o repique desse limite fornece um sinal. Sua interpretação geralmente corresponde a uma das seguintes situações:

  • rompimento ou repique em uma linha de suporte/resistência;
  • rompimento ou repique do nível do preço histórico;
  • alcance de algum nível de stop loss/take profit;
  • alcance de um horário específico.

Dependendo da estratégia escolhida, os traders podem comprar ou vender, definir uma ordem pendente ou fechar uma posição como resultado de um evento. Nosso sistema deve suportar todas essas ações. Assim, a lista de funções básicas é a seguinte:

  • envio de várias notificações (alertas, notificações instantâneas, e-mails);
  • abertura de ordens a mercado;
  • colocação de ordens pendentes (stop de compra, stop de venda, compra limitada, venda limitada);
  • encerramento total ou parcial de uma posição, inclusive por stop loss ou take profit.

Desenvolvimento da interface do usuário

Em muitos produtos similares, é dada muita atenção aos elementos da interface do usuário, como painéis, diálogos, botões, bem como o modo de "arrastar e soltar", que é bem difundido. Nosso projeto não terá nada disso. Em vez de uma interface gráfica especial, vamos usar os elementos padrão fornecidos pela MetaTrader.

Cada objeto possui um conjunto padrão de propriedades que podem ser adaptadas à tarefa atual.

Primeiro, nós precisamos distinguir os objetos que são destinados a negociação automatizada de outros objetos que podemos traçar no gráfico. Para fazer isso, nós precisamos fornecer nossos nomes de objetos com um prefixo predefinido. Se nenhum prefixo for especificado, o EA assumirá que todos os objetos com as propriedades apropriadas estão ativos. No entanto, esse modo não é recomendado porque o terminal pode criar seus próprios objetos (por exemplo, ordens fechadas), o que pode ter efeitos colaterais.

Segundo, para executar várias funções (a lista de funções fornecidas acima), nós devemos reservar diferentes estilos de execução. Por exemplo, nas configurações do EA, defina o estilo STYLE_DASH para colocar as ordens pendentes e o STYLE_SOLID - para a entrada a mercado quando o preço cruzar a linha correspondente. Estilos de diferentes operações devem ser diferentes.

Terceiro, nós devemos especificar a direção da negociação. Por exemplo, você pode usar azul para comprar e vermelho para vender. As ações que não estão relacionadas à entrada/saída a mercado, devem ser marcadas com uma terceira cor - por exemplo, cinza. Por exemplo, estas podem ser notificações ou a colocação de ordens pendentes. O último caso é classificado como uma categoria "neutra", porque geralmente configura um par de ordens pendentes direcionadas de forma diferente.

Quarto, nós devemos definir o tipo de ordens pendentes. Isso pode ser feito pelo arranjo mútuo da linha de ordens em relação ao preço atual do mercado e a cor da linha. Por exemplo, uma linha azul acima do preço implica um stop de compra, enquanto a linha azul abaixo do preço significa uma ordem limitada de compra.

Quinto, nós precisamos de alguns dados e atributos adicionais para a maioria das operações. Em particular, se um alerta (ou vários) forem desencadeados, o usuário provavelmente irá querer receber uma mensagem significativa. O campo Description que ainda não usamos é adequado para isso. Nós definiremos o tamanho do lote e tempo de expiração para as ordens neste campo. Tudo isso é opcional, pois nós devemos fornecer os parâmetros de entrada com os valores padrão para a conveniência e minimização das configurações do objeto necessárias no EA. Além das configurações de objeto, esses padrões conterão tanto o stop loss quanto o take profit.

Se foi definido um stop loss e take profit específico para cada linha, use os objetos que combinam várias linhas. Por exemplo, um canal equidistante tem duas linhas. O primeiro que passa pelas duas linhas é responsável por formar um sinal de negociação, enquanto que a linha paralela (com o terceiro ponto) define a distância do stop loss e take profit. O nível com o qual estamos lidando é fácil de ser definido através do arranjo mútuo e da cor das linhas. Por exemplo, uma linha adicional localizada acima da principal forma um stop loss para o canal vermelho. Se fosse o inferior, seria tratado como um take profit.

Se você deseja definir um stop loss e um take profit, o objeto deve possuir pelo menos três linhas. Por exemplo, uma grade de níveis de Fibonacci é adequada para isso. Por padrão, o projeto aplica os níveis padrão de 38,2% (stop loss), 61,8% (ponto de entrada no mercado) e 161,8% (take profit). Nós não vamos nos deparar com uma configuração mais flexível disto ou com outros tipos de objeto mais complexos neste artigo.

Ao ativar um dos objetos como resultado do cruzamento de preços, o objeto deve ser marcado como ativado. Isso pode ser feito, por exemplo, atribuindo o atributo "background" ao objeto OBJPROP_BACK. Nós vamos diminuir o brilho da cor original de tais objetos para um retorno visual aos usuários. Por exemplo, a linha azul torna-se azul escuro após o processamento.

No entanto, os traços dos traders geralmente incluem linhas e níveis tão "fortes" que os eventos relacionados a eles — como um repique da linha de suporte durante a correção de alta — podem ocorrer muitas vezes.

Consideremos esta situação com a ajuda da espessura da linha. Como sabemos, os estilos da MetaTrader permitem ajustar a largura de 1 a 5. Ao ativar a linha, nós veremos sua espessura e, se exceder 1, nós diminuiremos a espessura por 1 em vez de excluí-la do processamento subsequente. Por exemplo, nós podemos designar os vários eventos esperados no gráfico com o número de repetições até 5.

Esta possibilidade tem um nuance: os preços tendem a flutuar em torno de um determinado valor, e qualquer linha pode ser cruzada muitas vezes durante um curto período de tempo. Um trader manual analisa a dinâmica dos desvios de preços pela visão e classifica todo o ruído do preço. O EA deve implementar um mecanismo que faça o mesmo automaticamente.

Para isso, nós apresentamos as entradas que determinam o tamanho da "região quente" da interseção, ou seja, a faixa mínima de movimento de preços e sua duração, dentro dos quais os sinais não são formados. Em outras palavras, o evento "cruzamento de linha" não acontecerá imediatamente após o preço cruzá-lo, mas somente quando ele recuar para a distância especificada em pontos.

Da mesma forma, nós vamos introduzir um parâmetro que especifica o intervalo mínimo entre dois eventos consecutivos com a mesma linha (apenas para linhas com espessura maior que 1). Aqui surge uma nova tarefa: nós precisamos armazenar o horário do evento anterior com a linha em algum lugar. Vamos usar a propriedade do objeto OBJPROP_ZORDER para isso. Este é um número do tipo long, e o valor da data e hora é perfeitamente colocado lá. A alteração na ordem de exibição das linhas quase não tem efeito na representação visual do gráfico.

É suficiente executar as seguintes ações para configurar a linha para trabalhar com o sistema:

  • abrir a caixa de diálogo de propriedades do objeto;
  • adicionar o prefixo selecionado para o nome;
  • definir os parâmetros na descrição:
    • o lote as linhas de ordem a mercado e pendentes, bem como o encerramento parcial da ordem,
    • nomes das linhas de ordem pendente para a linha de ativação da ordem pendente,
    • linha de expiração para a linha de ordem pendente;
  • cor como indicador de direção (o padrão é azul — comprar, vermelho — vender, cinza — neutro);
  • estilo como seletor da operação (alerta, entrada a mercado, colocação de uma ordem pendente, fechamento da posição);
  • largura como um indicador de repetição de eventos.

Configuração das propriedades da linha horizontal para uma ordem limitada de compra (tracejado em azul) com um lote igual a 0.02 e o tempo de expiração de 24 barras (horas)

Configuração das propriedades da linha horizontal para uma ordem limitada de compra (tracejado em azul) com um lote igual a 0.02 e o tempo de expiração de 24 barras (horas)

A lista de ordens gerenciados pelo sistema (atendendo aos requisitos descritos) é exibida no comentário do gráfico juntamente com os detalhes — tipo de objeto, descrição e estado.


Desenvolvimento do mecanismo de execução

Vamos começar a implementação do EA começando com as entradas usadas para passar os prefixos dos nomes dos objetos, processar cores e estilos, valores padrão, bem como os tamanhos das áreas gerando os eventos do gráfico levando em consideração os requisitos e considerações gerais descritas acima.

input int Magic = 0;
input double Lot = 0.01 /*lote padrão*/;
input int Deviation = 10 /*tolerância à variação de preço durante a execução da ordem*/;

input int DefaultTakeProfit = 0 /*pontos*/;
input int DefaultStopLoss = 0 /*pontos*/;
input int DefaultExpiration = 0 /*barras*/;

input string CommonPrefit = "exp" /*vazio para lidar com todos os objetos compatíveis*/;

input color BuyColor = clrBlue /*ordens de compra pendente e a mercado - open, close, sl, tp*/;
input color SellColor = clrRed /*ordens de venda pendente e a mercado - open, close, sl, tp*/;
input color ActivationColor = clrGray /*ativação da colocação de ordens pendentes, alert*/;

input ENUM_LINE_STYLE InstantType = STYLE_SOLID /*abre negociações a mercado*/;
input ENUM_LINE_STYLE PendingType = STYLE_DASH /*define as prováveis ordens pendentes (requer ativação)*/;
input ENUM_LINE_STYLE CloseStopLossTakeProfitType = STYLE_DOT /*aplicado a posições abertas*/;

input int EventHotSpot = 10 /*pontos*/;
input int EventTimeSpan = 10 /*segundos*/;
input int EventInterval = 10 /*barras*/;

O próprio EA é implementado como a classe TradeObjects (TradeObjects.mq4 e .mq5). Seus únicos elementos públicos são o construtor, destrutor e os métodos de processamento de eventos padrão.

class TradeObjects
{
  private:
    Expert *e;

  public:
    void handleInit()
    {
      detectLines();
    }
    
    void handleTick()
    {
      #ifdef __MQL4__  
      if(MQLInfoInteger(MQL_TESTER))
      {
        static datetime lastTick = 0;
        if(TimeCurrent() != lastTick)
        {
          handleTimer();
          lastTick = TimeCurrent();
        }
      }
      #endif
    
      e.trailStops();
    }
    
    void handleTimer()
    {
      static int counter = 0;
      
      detectLines();
      
      counter++;
      if(counter == EventTimeSpan) // aguarde até ter histórico de ofertas de compra para EventTimeSpan
      {
        counter = 0;
        if(PreviousBid > 0) processLines();
        if(PreviousBid != Bid) PreviousBid = Bid;
      }
    }
    
    void handleChart(const int id, const long &lparam, const double &dparam, const string &sparam)
    {
      if(id == CHARTEVENT_OBJECT_CREATE || id == CHARTEVENT_OBJECT_CHANGE)
      {
        if(checkObjectCompliance(sparam))
        {
          if(attachObject(sparam))
          {
            display();
            describe(sparam);
          }
        }
        else
        {
          detectLines();
        }
      }
      else if(id == CHARTEVENT_OBJECT_DELETE)
      {
        if(removeObject(sparam))
        {
          display();
          Print("Line deleted: ", sparam);
        }
      }
    }
    
    TradeObjects()
    {
      e = new Expert(Magic, Lot, Deviation);
    }
    
    ~TradeObjects()
    {
      delete e;
    }
};

A instância da classe é criada de forma estática, e seus manipuladores de eventos são então vinculados às funções globais apropriadas.

TradeObjects to;

void OnInit()
{
  ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true);
  EventSetTimer(1);
  to.handleInit();
}

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
  to.handleChart(id, lparam, dparam, sparam);
}
                 
void OnTimer()
{
  to.handleTimer();
}

void OnTick()
{
  to.handleTick();
}

Todas as operações de negociação são atribuídas a um mecanismo separado oculto na classe externa Expert (Expert01.mqh). Nós criamos sua instância no construtor e removemos ela no destrutor da classe TradeObjects. Nós examinaremos o mecanismo em detalhes mais tarde. A TradeObjects irá delegar muitas operações a ele.

Todos os manipuladores de eventos handleInit, handleTick, handleTimer, handleChart chamam o método detectLines que nós devemos escrever. Neste método, os objetos são analisados ​​e os que atendem aos nossos requisitos são selecionados. Um usuário pode criar, remover e modificar os objetos ao executar o EA. Os objetos encontrados são salvos no array interno. Sua presença permite que você monitore a mudança de estado do gráfico e informe o usuário sobre a descoberta de novos objetos e a remoção dos antigos.

O método processLines é chamado periodicamente pelo timer. Ele verifica a ocorrência de eventos pelo array em um loop e executa as ações apropriadas.

Como nós podemos ver, o método verifica o prefixo dos objetos, bem como seus estilos, cores e estados usando o método checkObjectCompliance (veja abaixo). Os objetos adequados são adicionados ao array interno usando a função attachObject, enquanto os que foram removidos do gráfico são removidos do array usando a função removeObject. A lista de objetos é exibida como um comentário ao gráfico usando o método 'display'.

O array consiste em estruturas simples que contêm o nome e o estado do objeto:

  private:
    struct LineObject
    {
      string name;
      int status;
      void operator=(const LineObject &o)
      {
        name = o.name;
        status = o.status;
      }
    };
    
    LineObject objects[];

O estado é usado principalmente para marcar os objetos existentes — por exemplo, logo após adicionar um novo objeto ao array usando a função attachObject:

  protected:
    bool attachObject(const string name)
    {
      bool found = false;
      int n = ArraySize(objects);
      for(int i = 0; i < n; i++)
      {
        if(objects[i].name == name)
        {
          objects[i].status = 1;
          found = true;
          break;
        }
      }
      
      if(!found)
      {
        ArrayResize(objects, n + 1);
        objects[n].name = name;
        objects[n].status = 1;
        return true;
      }
      
      return false;
    }

O método detectLines verifica a existência de cada objeto nos momentos de tempo subsequentes:

    bool detectLines()
    {
      startRefresh();
      int n = ObjectsTotal(ChartID(), 0);
      int count = 0;
      for(int i = 0; i < n; i++)
      {
        string obj = ObjectName(ChartID(), i, 0);
        if(checkObjectCompliance(obj))
        {
          if(attachObject(obj))
          {
            describe(obj);
            count++;
          }
        }
      }
      if(count > 0) Print("New lines: ", count);
      bool changes = stopRefresh() || (count > 0);
      
      if(changes)
      {
        display();
      }
      
      return changes;
    }

Aqui, a função auxiliar startRefresh é chamada bem no início. Seu objetivo é redefinir os sinalizadores de estado para 0 para todos os objetos do array. Depois disso, os objetos do trabalho recebem o estado de 1 novamente dentro do loop usando o attachmentObject. A chamada de stopRefresh é realizada no final. Ele encontra os objetos não utilizados no array interno pelo estado zero e informa o usuário sobre isso.

Cada objeto é verificado quanto à conformidade com os requisitos no método checkObjectCompliance:

    bool checkObjectCompliance(const string obj)
    {
      if(CommonPrefit == "" || StringFind(obj, CommonPrefit) == 0)
      {
        if(_ln[ObjectGetInteger(0, obj, OBJPROP_TYPE)]
        && _st[ObjectGetInteger(0, obj, OBJPROP_STYLE)]
        && _cc[(color)ObjectGetInteger(0, obj, OBJPROP_COLOR)])
        {
          return true;
        }
      }
      return false;
    }

Além do prefixo do nome, os conjuntos de sinalizadores com tipos, estilos e cores de objetos também são verificados. A classe auxiliar Set é usada para isso:

#include <Set.mqh>

Set<ENUM_OBJECT> _ln(OBJ_HLINE, OBJ_VLINE, OBJ_TREND, OBJ_CHANNEL, OBJ_FIBO);
Set<ENUM_LINE_STYLE> _st(InstantType, PendingType, CloseStopLossTakeProfitType);
Set<color> _cc(BuyColor, SellColor, ActivationColor);

Agora, é hora de descrever o método principal — processLines. O papel do método afeta seu tamanho. O código inteiro é apresentado em anexo. Aqui, nós apenas exibimos os fragmentos mais reveladores.

    void processLines()
    {
      int n = ArraySize(objects);
      for(int i = 0; i < n; i++)
      {
        string name = objects[i].name;
        if(ObjectGetInteger(ChartID(), name, OBJPROP_BACK)) continue;
        
        int style = (int)ObjectGetInteger(0, name, OBJPROP_STYLE);
        color clr = (color)ObjectGetInteger(0, name, OBJPROP_COLOR);
        string text = ObjectGetString(0, name, OBJPROP_TEXT);
        datetime last = (datetime)ObjectGetInteger(0, name, OBJPROP_ZORDER);
    
        double aux = 0, auxf = 0;
        double price = getCurrentPrice(name, aux, auxf);
        ...

Dentro do loop, nós passamos todos os objetos, excluindo os já usados ​​(com o sinalizador OBJPROP_BACK). A função getCurrentPrice mostrada abaixo permite-nos descobrir o preço do objeto atual. Como alguns tipos de objetos consistem de várias linhas, nós devemos passar os valores de preço adicionais usando 2 parâmetros.

        if(clr == ActivationColor)
        {
          if(style == InstantType)
          {
            if(checkActivation(price))
            {
              disableLine(i);
              if(StringFind(text, "Alert:") == 0) Alert(StringSubstr(text, 6));
              else if(StringFind(text, "Push:") == 0) SendNotification(StringSubstr(text, 5));
              else if(StringFind(text, "Mail:") == 0) SendMail("TradeObjects", StringSubstr(text, 5));
              else Print(text);
            }
          }

Em seguida, nós devemos verificar o estilo do objeto para determinar o tipo de evento e como seu preço se correlaciona com o preço da melhor oferta de compra na barra 0 — a função checkActivation faz isso no caso de uma alerta e colocando ordens pendentes. Se a ativação ocorreu, executa a ação apropriada (no caso de um alerta, exibe a mensagem ou envie ela para o usuário) e marque o objeto como desativado usando a função disableLine.

O código de ativação para as operações de negociação serão mais complicados, é claro. Abaixo está um exemplo de uma opção simplificada para a compra a mercado e encerramento de posições vendidas:

        else if(clr == BuyColor)
        {
          if(style == InstantType)
          {
            int dir = checkMarket(price, last);
            if((dir == 0) && checkTime(name))
            {
              if(clr == BuyColor) dir = +1;
              else if(clr == SellColor) dir = -1;
            }
            if(dir > 0)
            {
              double lot = StringToDouble(ObjectGetString(0, name, OBJPROP_TEXT)); // lot[%]
              if(lot == 0) lot = Lot;
    
              double sl = 0.0, tp = 0.0;
              if(aux != 0)
              {
                if(aux > Ask)
                {
                  tp = aux;
                  if(DefaultStopLoss != 0) sl = Bid - e.getPointsForLotAndRisk(DefaultStopLoss, lot) * _Point;
                }
                else
                {
                  sl = aux;
                  if(DefaultTakeProfit != 0) tp = Bid + e.getPointsForLotAndRisk(DefaultTakeProfit, lot) * _Point;
                }
              }
              else
              {
                if(DefaultStopLoss != 0) sl = Bid - e.getPointsForLotAndRisk(DefaultStopLoss, lot) * _Point;
                if(DefaultTakeProfit != 0) tp = Bid + e.getPointsForLotAndRisk(DefaultTakeProfit, lot) * _Point;
              }
              
              sl = NormalizeDouble(sl, _Digits);
              tp = NormalizeDouble(tp, _Digits);
            
              int ticket = e.placeMarketOrder(OP_BUY, lot, sl, tp);
              if(ticket != -1) // sucesso
              {
                disableLine(i);
              }
              else
              {
                showMessage("Market buy failed with '" + name + "'");
              }
            }
          }
          else if(style == CloseStopLossTakeProfitType) // close sell position, stoploss for sell, takeprofit for sell
          {
            int dir = checkMarket(price) || checkTime(name);
            if(dir != 0)
            {
              double lot = StringToDouble(ObjectGetString(0, name, OBJPROP_TEXT)); // lote
              if(lot > 0)
              {
                if(e.placeMarketOrder(OP_BUY, lot) != -1) // será ativado por OrderCloseBy();
                {
                  disableLine(i);
                }
                else
                {
                  showMessage("Partial sell close failed with '" + name + "'");
                }
              }
              else
              {
                if(e.closeMarketOrders(e.mask(OP_SELL)) > 0)
                {
                  disableLine(i);
                }
                else
                {
                  showMessage("Complete sell close failed with '" + name + "'");
                }
              }
            }
          }

A função checkMarket (uma versão mais complexa da checkActivation) verifica a ocorrência do evento (ambos estão descritos abaixo). Quando um evento é desencadeado, nós recebemos os níveis de stop loss e take profit das propriedades do objeto e logo após é aberto uma ordem.

O tamanho do lote é especificado na descrição do objeto em contratos ou como uma percentagem da margem livre - no último caso, o valor é escrito como negativo. O significado dessa notação é fácil de lembrar se você imagina que você está realmente indicando qual parte dos fundos deve ser usada como segurança de uma nova ordem.

As funções checkActivation e checkMarket são semelhantes. Ambas usam as entradas do EA definindo o tamanho da área de ativação do evento:

    bool checkActivation(const double price)
    {
      if(Bid >= price - EventHotSpot * _Point && Bid <= price + EventHotSpot * _Point)
      {
        return true;
      }
      
      if((PreviousBid < price && Bid >= price)
      || (PreviousBid > price && Bid <= price))
      {
        return true;
      }
      return false;
    }
    
    int checkMarket(const double price, const datetime last = 0) // retorna a direção do movimento do preço
    {
      if(last != 0 && (TimeCurrent() - last) / PeriodSeconds() < EventInterval)
      {
        return 0;
      }
    
      if(PreviousBid >= price - EventHotSpot * _Point && PreviousBid <= price + EventHotSpot * _Point)
      {
        if(Bid > price + EventHotSpot * _Point)
        {
          return +1; // alta
        }
        else if(Bid < price - EventHotSpot * _Point)
        {
          return -1; // baixa
        }
      }
    
      if(PreviousBid < price && Bid >= price && MathAbs(Bid - PreviousBid) >= EventHotSpot * _Point)
      {
        return +1;
      }
      else if(PreviousBid > price && Bid <= price && MathAbs(Bid - PreviousBid) >= EventHotSpot * _Point)
      {
        return -1;
      }
      
      return 0;
    }

Como você pode se lembrar, o preço de PreviousBid é salvo pelo EA no manipulador handleTimer com o horário EventTimeSpan em segundos. O resultado da operação das funções é um sinalizador do preço pelo rompimento de bid com o objeto na barra 0, sendo que checkActivation retorna uma sinalização lógica simples, enquanto a CheckMarket é a direção de movimento do preço: +1 — alta, -1 — baixa.

O cruzamento de objetos através dos preços é detectado pelo preço de bid porque o gráfico inteiro é construído pelo preço de bid, incluindo o preço aplicado. Mesmo que um trader trace as linhas para as ordens de compra, elas são acionados pelos sinais corretos: o gráfico de ask (melhor oferta de venda) está acima do gráfico de bid (melhor oferta de compra) devido ao valor de spread, enquanto que as linhas potenciais que poderiam ter sido baseadas no gráfico de ask teriam cruzado o preço de forma síncrona com a marcação atual pelo bid.

Para as linhas do estilo PendingType e ActivationColor neutro, o comportamento é especial: no momento da rompimento do preço, as ordens pendentes são colocadas. A colocação da ordem é definida usando outras linhas. Os nomes deles são separados por uma barra ('/') na descrição da linha de ativação. Se a descrição estiver vazia, o sistema encontra todas as linhas das ordens pendentes pelo estilo e coloca elas. Assim como as ordens a mercado, a direção das ordens pendentes corresponde à sua cor — BuyColor ou SellColor para compra ou venda, respectivamente, enquanto que na descrição você pode especificar o lote e a data de validade (em barras).

Os métodos para combinar estilos e cores de objetos e seus valores correspondentes são apresentados na tabela.

Cor e estilo BuyColor SellColor ActivationColor
InstantType compra a mercado venda a mercado alerta
PendingType ordem de compra pendente potencial ordem de venda pendente potencial inicia a colocação das ordens pendentes
CloseStopLossTakeProfitType encerramento, stop loss, take profit
para posições vendidas
encerramento, stop loss, take profit
para posições compradas
encerra tudo

Vamos voltar ao método getCurrentPrice, que talvez seja o mais importante após processLines.

    double getCurrentPrice(const string name, double &auxiliary, double &auxiliaryFibo)
    {
      int type = (int)ObjectGetInteger(0, name, OBJPROP_TYPE);
      if(type == OBJ_TREND)
      {
        datetime dt1 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 0);
        datetime dt2 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 1);
        int i1 = iBarShift(NULL, 0, dt1, true);
        int i2 = iBarShift(NULL, 0, dt2, true);
        if(i1 <= i2 || i1 == -1 || i2 == -1)
        {
          Print("Incorrect line: ", name);
          return 0;
        }
        double p1 = ObjectGetDouble(0, name, OBJPROP_PRICE, 0);
        double p2 = ObjectGetDouble(0, name, OBJPROP_PRICE, 1);
        
        double k = -(p1 - p2)/(i2 - i1);
        double b = -(i1 * p2 - i2 * p1)/(i2 - i1);
        
        return b;
      }
      else if(type == OBJ_HLINE)
      {
        return ObjectGetDouble(0, name, OBJPROP_PRICE, 0);
      }
      else if(type == OBJ_VLINE)
      {
        return EMPTY_VALUE; // não deve ser nulo, caso contrário ela não é usada
      }
      else if(type == OBJ_CHANNEL)
      {
        datetime dt1 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 0);
        datetime dt2 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 1);
        datetime dt3 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 2);
        int i1 = iBarShift(NULL, 0, dt1, true);
        int i2 = iBarShift(NULL, 0, dt2, true);
        int i3 = iBarShift(NULL, 0, dt3, true);
        if(i1 <= i2 || i1 == -1 || i2 == -1 || i3 == -1)
        {
          Print("Incorrect channel: ", name);
          return 0;
        }
        double p1 = ObjectGetDouble(0, name, OBJPROP_PRICE, 0);
        double p2 = ObjectGetDouble(0, name, OBJPROP_PRICE, 1);
        double p3 = ObjectGetDouble(0, name, OBJPROP_PRICE, 2);
        
        double k = -(p1 - p2)/(i2 - i1);
        double b = -(i1 * p2 - i2 * p1)/(i2 - i1);
        
        double dy = i3 * k + b - p3;
        
        auxiliary = p3 - i3 * k;
        
        return b;
      }
      else if(type == OBJ_FIBO)
      {
        // nível de 61.8 é ponto de entrada na retração (compra/venda limitada)
        // 38.2 e 161.8 como stoploss/takeprofit
        
        double p1 = ObjectGetDouble(0, name, OBJPROP_PRICE, 0);
        double p2 = ObjectGetDouble(0, name, OBJPROP_PRICE, 1);
        datetime dt1 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 0);
        datetime dt2 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 1);
        
        if(dt2 < dt1)
        {
          swap(p1, p2);
        }
        
        double price = (p2 - p1) * ObjectGetDouble(0, name, OBJPROP_LEVELVALUE, 4) + p1;
        auxiliary = (p2 - p1) * ObjectGetDouble(0, name, OBJPROP_LEVELVALUE, 2) + p1;
        auxiliaryFibo = (p2 - p1) * ObjectGetDouble(0, name, OBJPROP_LEVELVALUE, 6) + p1;
        return price;
      }
      return 0;
    }

A ideia é simples — dependendo do tipo do objeto, nós devemos calcular o seu preço na barra 0 (para as linhas principal e adicional). Ao colocar os objetos no gráfico, é importante que todos os pontos do objeto sejam localizados no passado — onde há um número da barra válido. Caso contrário, o objeto é considerado inválido, uma vez que é impossível calcular seu preço inequivocamente.

No caso de uma linha vertical, nós retornamos EMPTY_VALUE — isto não é zero, nem um preço específico (porque essa linha satisfaz qualquer preço). Portanto, para as linhas verticais, você deve usar uma verificação adicional para corresponder a hora atual. Isso é executado pela função checkTime. Ela já foi chamada no fragmento da processLines

    bool checkTime(const string name)
    {
      return (ObjectGetInteger(0, name, OBJPROP_TYPE) == OBJ_VLINE
        && (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 0) == Time[0]);
    }

Finalmente, vamos descrever a implementação da função disableLine que já encontramos no código muitas vezes.

    void disableLine(const string name)
    {
      int width = (int)ObjectGetInteger(0, name, OBJPROP_WIDTH);
      if(width > 1)
      {
        ObjectSetInteger(0, name, OBJPROP_WIDTH, width - 1);
        ObjectSetInteger(0, name, OBJPROP_ZORDER, TimeCurrent());
      }
      else
      {
        ObjectSetInteger(0, name, OBJPROP_BACK, true);
        ObjectSetInteger(0, name, OBJPROP_COLOR, darken((color)ObjectGetInteger(0, name, OBJPROP_COLOR)));
      }
      display();
    }

Se a largura da linha exceder em 1, nós aumentamos ela por 1 e salvamos o tempo atual do evento na propriedade OBJPROP_ZORDER. No caso de linhas comuns, nós as mudamos para o fundo e diminuímos o brilho da cor. Os objetos em segundo plano são considerados desativados.

Quanto a propriedade OBJPROP_ZORDER, ela é lida no método processLines para a variável 'datetime last' (como mostrado acima), que, por sua vez, é passada como um argumento para o método checkMarket(price, last). Dentro, nós asseguramos que o tempo decorrido desde a ativação anterior exceda o intervalo configurado na variável de entrada (em barras):

      if(last != 0 && (TimeCurrent() - last) / PeriodSeconds() < EventInterval)
      {
        return 0;
      }

A TradeObjects permite que você realize um encerramento parcial se o lote for especificado na descrição do objeto de tipo CloseStopLossTakeProfitType. O sistema abre um contador de ordem do volume especificado e, em seguida, chama OrderCloseBy. Para habilitar o modo, existe um sinalizador especial AllowOrderCloseBy nas variáveis ​​de entrada. Se ele estiver ligado, as posições do contador sempre se "colidem". Como você pode saber, esta função não é permitida em todas as contas (o EA verifica as configurações e envia a mensagem apropriada se a opção estiver bloqueada). No caso da MetaTrader 5, a conta deve suportar o modo de hedging. Os interessados ​​podem melhorar o sistema implementando um encerramento parcial alternativo — sem usar a OrderCloseBy e com a capacidade de visualizar a lista de posições e selecionar uma específica que possa ser reduzido por vários atributos.

Voltemos à classe Expert executando todas as operações comerciais para a TradeObjects. Este é um conjunto simples de métodos para abrir e encerrar ordens, suportar o stop loss e calcular os lotes com base no risco especificado. Isso se aplica a metáfora de ordem da MetaTrader 4, que é adaptada para o MetaTrader 5 usando a biblioteca MT4Orders.

A classe não fornece funcionalidades para modificar as ordens pendentes colocadas. Movendo os seus preços, assim como o stop loss e o take profit são gerenciados pelo terminal: se a opção "Mostrar níveis de negociação" estiver habilitada, isso permite fazer isso usando o Drag'n'Drop (arrasta e solta).

A classe Expert pode ser substituída por qualquer outra que você esteja acostumado.

O código-fonte anexado é compilado tanto na MetaTrader 4 quanto na MetaTrader 5 (com arquivos de cabeçalho adicionais).

Na implementação atual da TradeObjects, há um desvio das práticas estritas de OOP por uma questão de simplicidade. Caso contrário, você teria que aplicar uma interface de negociação abstrata, implementar a classe Expert como o herdeiro da interface e passá-la para a classe TradeObjects (por exemplo, através do parâmetro do construtor). Este é um modelo OOP de injeção de dependência bem conhecido. Neste projeto, o mecanismo de negociação é conectada ao código: ele é criado e excluído no objeto TradeObjects.

Além disso, nós usamos variáveis ​​de entrada globais diretamente no código da classe TradeObjects em violação aos princípios da OOP. O melhor estilo de programação exige passá-los para a classe como parâmetros do construtor ou métodos especiais. Isso nos permitiria, por exemplo, usar a TradeObjects como uma biblioteca dentro de outro EA, complementando-o com funções para negociação manual por marcação.

Quanto maior o projeto, mais importantes os princípios básicos de OOP se tornam. Uma vez que nós consideramos um mecanismo bastante simples e isolado para a negociação automatizada por objetos, sua melhoria (que não conhece limites) é deixada para estudo opcional.

Programa em ação

Abaixo, nós vamos mostrar como o sistema se comporta em ação com os estilos e cores padrão.

Suponha que nós detectemos o padrão de cabeça e ombros e vamos colocar a linha vermelha horizontal para uma ordem de venda. O sistema exibe uma lista de objetos detectados e controlados por ele em um comentário.


O stop loss é definido de acordo com o parâmetro DefaultStopLoss do EA. A vida útil do mercado é limitada por uma linha pontilhada azul vertical em vez do take profit.


Ao chegar a esta linha, a posição está fechada (independentemente da rentabilidade). As linhas ativadas são marcadas como inativas (o brilho da cor é diminuído e elas são movidas para o plano de fundo).


Depois de um tempo, as cotações parecem estar se movimentando para baixo novamente e nós definimos os níveis de Fibo na esperança de um rompimento do nível de 61.8 (isto é tudo o que o Fibonacci pode fazer neste projeto por padrão, mas você pode implementar outros tipos de comportamentos). Observe que a cor do objeto Fibo é a cor da linha diagonal, não dos níveis: a cor dos níveis é definida por uma configuração separada.


Quando o preço atinge o nível, é aberto uma negociação com o stop loss específico (38.2) e os preços de take profit (161.8, não visível na captura de tela).


Algum tempo depois, nós vemos a formação de uma linha de resistência de cima e colocamos o canal azul assumindo que o preço continuará subindo.


Observe que todas as linhas até agora não continham descrições, e as ordens foram abertas com o lote do parâmetro Lot (0.01 por padrão). Neste caso, há uma descrição de '-1', ou seja, o tamanho do lote será calculado exigindo 1% da margem livre. Uma vez que a linha auxiliar está localizada abaixo da principal, o canal especifica a distância para o stop loss (diferente do valor padrão).


O canal é rompido e uma nova posição de compra é aberta. Como podemos ver, o volume foi calculado como 0.04 (com um depósito de $1000). O segmento azul na captura de tela é o canal ativado movido para o plano de fundo (então a MetaTrader 4 mostra os canais em segundo plano).

Para fechar ambas as posições de compra aberta, coloque uma linha pontilhada vermelha de take profit.


O preço atinge esse nível, e ambas as ordens são encerradas.


Suponha que, após esse movimento, o preço se mova dentro do "corredor". Para pegar essa volatilidade, nós estabelecemos duas linhas tracejadas horizontais para as ordens limitadas acima e abaixo do preço, bem como uma linha vertical tracejada cinza para a sua ativação. Na verdade, eles não precisam ser horizontais ou verticais.


Note, por exemplo, que um lote personalizado de 0.02 e data de validade de 24 barras (horas) são especificados para uma ordem pendente inferior na descrição. Uma vez que a linha de ativação é alcançada, as ordens pendentes são definidas.


Após algum tempo, a venda limitada é acionada.


A ordem limitada expira na segunda-feira.


Nós colocamos uma linha pontilhada cinza vertical, o que significa fechar todas as posições.


Depois que ela é alcançada, uma posição vendida é encerrada, mas mesmo que fosse aberto uma comprada, ela também seria fechada.


Durante o seu trabalho, o EA exibe os principais eventos no registro.

2017.07.06 02:00:00  TradeObjects EURUSD,H1: New line added: 'exp Channel 42597 break up' OBJ_CHANNEL buy -1
2017.07.06 02:00:00  TradeObjects EURUSD,H1: New lines: 1
2017.07.06 10:05:27  TradeObjects EURUSD,H1: Activated: exp Channel 42597 break up
2017.07.06 10:05:27  TradeObjects EURUSD,H1: open #3 buy 0.04 EURUSD at 1.13478 sl: 1.12908 ok
...
2017.07.06 19:02:18  TradeObjects EURUSD,H1: Activated: exp Horizontal Line 43116 takeprofit
2017.07.06 19:02:18  TradeObjects EURUSD,H1: close #3 buy 0.04 EURUSD at 1.13478 sl: 1.13514 at price 1.14093
2017.07.06 19:02:18  TradeObjects EURUSD,H1: close #2 buy 0.01 EURUSD at 1.13414 sl: 1.13514 tp: 1.16143 at price 1.14093
...
2017.07.07 05:00:09  TradeObjects EURUSD,H1: Activated: exp Vertical Line 42648
2017.07.07 05:00:09  TradeObjects EURUSD,H1: open #4 sell limit 0.01 EURUSD at 1.14361 sl: 1.15395 ok
2017.07.07 05:00:09  TradeObjects EURUSD,H1: #4 2017.07.07 05:00:09 sell limit 0.01 EURUSD 1.14361 1.15395 0.00000 0.00000 0.00 0.00 0.00  0 expiration 2017.07.08 05:00
2017.07.07 05:00:09  TradeObjects EURUSD,H1: open #5 buy limit 0.02 EURUSD at 1.13731 sl: 1.13214 ok
2017.07.07 05:00:09  TradeObjects EURUSD,H1: #5 2017.07.07 05:00:09 buy limit 0.02 EURUSD 1.13731 1.13214 0.00000 0.00000 0.00 0.00 0.00  0 expiration 2017.07.08 05:00

É claro que é possível excluir os objetos obsoletos para limpar o gráfico. No exemplo, eles são deixados como um protocolo para as ações executadas.

Os modelos para a MetaTrader 4 e MetaTrader 5 com as linhas de demonstração no testador no EURUSD H1 a partir de 1 de julho de 2017 (o período descrito acima) estão anexados no artigo. Nós aplicamos as configurações do EA padrão com exceção do parâmetro DefaultStopLoss configurado para -1 (correspondente à perda de 1% da margem livre). O depósito inicial de $1000 e a alavancagem de 1:500 são oferecidos para ilustrar o cálculo do stop loss. No caso da MetaTrader 5, o modelo deve primeiro ser renomeado para tester.tpl (os modelos de carregamento e edição diretamente no testador ainda não são suportados pela plataforma).

Conclusão

O artigo oferece uma maneira simples, mas eficiente, de organizar uma negociação semiautomática usando os objetos padrão colocados no gráfico por um trader. Com a ajuda de linhas de tendência, horizontal e vertical, bem como os canais e grades de Fibo, o sistema pode executar as ordens a mercado, colocar ordens pendentes e notificar os traders dos padrões de mercado específicos. O código aberto permite aos usuários expandir o conjunto de tipos de objetos suportados e melhorar a funcionalidade de negociação.