Visualização do histórico de negociação multimoeda em relatórios em HTML e CSV

20 maio 2019, 14:30
Stanislav Korotky
0
748

Como é sabido, desde seu lançamento, o MetaTrader 5 vem oferecendo testes multimoedas. Essa função é procurada pela maioria dos traders, mas, infelizmente, não é tão universal quanto gostaríamos. Em particular, após o teste, o usuário pode abrir um gráfico com as operações de negociação concluídas, mas apenas para o símbolo selecionado nas configurações do testador. É impossível ver o histórico da negociação em todos os símbolos usados, e nem sempre é conveniente realizar testes visuais. Além disso, algum tempo depois do teste pode ser necessária uma análise adicional, ou o relatório pode ser obtido de outra pessoa. Portanto, é bom ter uma ferramenta para visualizar a negociação de vários símbolos com base no relatório HTML do testador.

Esta tarefa está intimamente relacionada a outro aplicativo MetaTrader similar. Acontece que muitos dos sinais de negociação disponíveis no site mql5.com também são multimoedas, e seria conveniente exibir arquivos CSV com seu histórico em gráficos de maneira semelhante.

Escrevamos um indicador que execute as funções acima.

Como existem vários símbolos de trabalho, para sua análise paralela, o indicador será criado em várias instâncias (uma por símbolo) nas sub-janelas do gráfico. A principal construção gráfica do indicador são as "cotações" do símbolo selecionado (normalmente, diferente do símbolo do gráfico), sincronizadas com as barras da janela principal. Nestas "cotações" colocamos linhas de tendência correspondentes a ordens de negociação (posições).

Existe uma abordagem alternativa — com a exibição de trades na janela principal, mas fornece uma análise de apenas um símbolo no gráfico. Para isso, você precisa implementar outro indicador sem buffers, com a possibilidade de alternar convenientemente para qualquer um dos símbolos mencionados no relatório.

No passado artigo foi descrito um analisador HTML baseado em seletores CSS[1]. Com a sua ajuda, obtemos uma lista de trades a partir do relatório HTML e, em seguida, criamos trades (objetos gráficos) com base nelas. No caso de arquivos CSV da seção de sinais, tudo fica mais simples, pois seu formato para sinais MT4 (*.history.csv) e MT5 (*.positions.csv) é suportado pelas funções MQL internas.

Indicador SubChart

O primeiro passo para a realização da nossa ideia é criar um indicador simples que exiba "cotações" de um símbolo externo numa sub-janela de qualquer gráfico. Vamos chamá-lo de SubChart.

Para exibir dados com valores OHLC (Open, High, Low, Close) a linguagem MQL fornece vários estilos de exibição, em particular DRAW_CANDLES e DRAW_BARS. Obviamente, eles usam quatro buffers de indicador cada. Em vez de escolher qualquer um dos estilos, suportamos a ambas as opções e tornamos nossa escolha dinâmica com base nas configurações atuais da janela. Como se sabe, as configurações gráficas contêm na guia "Geral" um grupo de rádio-botões "Barras", "Candles japoneses" e "Linha", além disso, para garantir acesso rápido, elas são duplicadas pelos botões correspondentes na paleta de ferramentas. A partir da MQL, podemos obter essas configurações chamando:

(ENUM_CHART_MODE)ChartGetInteger(0, CHART_MODE)

A enumeração ENUM_CHART_MODE contém elementos com a mesma finalidade: CHART_CANDLES, CHART_BARS, CHART_LINE.

Em nosso indicador também faz sentido suportar o último item CHART_LINE, de modo que, em resposta a qualquer mudança de modo na interface do usuário, o indicador mude sua aparência de forma síncrona com a janela principal. Para o último modo, pode ser usado o estilo DRAW_LINE, ele usa um único buffer.

Comecemos a codificar. Declaramos o número de buffers de indicador e de construções gráficas exibidas:

#property indicator_separate_window
#property indicator_buffers 4
#property indicator_plots   1

Adicionamos variáveis de entrada:

input string SubSymbol = ""; // Symbol
input bool Exact = false;

Com a ajuda de SubSymbol, o usuário pode escolher para o indicador qualquer símbolo diferente do símbolo atual da janela principal.

O parâmetro Exact determina o que fazer nos casos em que as barras na janela principal não possuem exatamente as barras correspondentes no símbolo de terceiros. Como o nome sugere, este parâmetro é usado na chamada da função iBarShift e, de acordo com sua lógica de trabalho, tem o seguinte efeito visual:

  • quando Exact for false, a função retornará o número da barra apropriada mais próxima pelo tempo especificado e, portanto, na ausência de cotações (“janela” no horário de negociação, feriado, etc.), o indicador exibirá a barra anterior;
  • quando Exact for true, a função retornará -1 e, no gráfico do indicador, este espaço estará vazio;

Por padrão, o parâmetro SubSymbol é igual a uma linha vazia, o que implica uma duplicação de cotações da janela principal. Nesse caso, é preciso editar o valor real da variável em _Symbol, mas como input é uma variável somente de leitura em MQL, teremos que inserir a variável intermediária Symbol e preenchê-la no manipulador OnInit.

string Symbol;

int OnInit()
{
  Symbol = SubSymbol;
  if(Symbol == "") Symbol = _Symbol;
  else SymbolSelect(Symbol, true);
  ...

Observe que também precisamos adicionar um símbolo de terceiros à Observação do mercado.

Para controlar o modo de exibição atual, usamos a variável mode:

ENUM_CHART_MODE mode = 0;

Quatro buffers de indicador recebem o os nomes esperados:

// OHLC
double open[];
double high[];
double low[];
double close[];

Para inicializar os buffers da maneira usual (com a configuração da propriedade "seriality") selecionamos uma função pequena:

void InitBuffer(int index, double &buffer[], ENUM_INDEXBUFFER_TYPE style)
{
  SetIndexBuffer(index, buffer, style);
  ArraySetAsSeries(buffer, true);
}

A inicialização da construção gráfica também é reduzida a uma função auxiliar (aqui teremos uma construção, mas quando houver várias, essa função economizará espaço):

void InitPlot(int index, string name, int style, int width = -1, int colorx = -1)
{
  PlotIndexSetInteger(index, PLOT_DRAW_TYPE, style);
  PlotIndexSetDouble(index, PLOT_EMPTY_VALUE, 0);
  PlotIndexSetString(index, PLOT_LABEL, name);
  if(width != -1) PlotIndexSetInteger(index, PLOT_LINE_WIDTH, width);
  if(colorx != -1) PlotIndexSetInteger(index, PLOT_LINE_COLOR, colorx);
}

Para converter o modo de exibição do gráfico no estilo de buffer, escrevemos a função:

int Mode2Style(/*global ENUM_CHART_MODE mode*/)
{
  switch(mode)
  {
    case CHART_CANDLES: return DRAW_CANDLES;
    case CHART_BARS: return DRAW_BARS;
    case CHART_LINE: return DRAW_LINE;
  }
  return 0;
}

Ele usa a variável global mode mencionada anteriormente, que deve ser preenchida com o valor correto já no OnInit, junto com as chamadas para todas as funções auxiliares.

  InitBuffer(0, open, INDICATOR_DATA);
  string title = "# Open;# High;# Low;# Close";
  StringReplace(title, "#", Symbol);
  mode = (ENUM_CHART_MODE)ChartGetInteger(0, CHART_MODE);
  InitPlot(0, title, Mode2Style());

  InitBuffer(1, high, INDICATOR_DATA);
  InitBuffer(2, low, INDICATOR_DATA);
  InitBuffer(3, close, INDICATOR_DATA);

Mas, para o correto funcionamento do indicador, isso ainda não é suficiente. Dependendo do modo atual (variável mode), é necessário alterar as cores das linhas — elas também são retiradas das configurações do gráfico.

void SetPlotColors()
{
  if(mode == CHART_CANDLES)
  {
    PlotIndexSetInteger(0, PLOT_COLOR_INDEXES, 3);
    PlotIndexSetInteger(0, PLOT_LINE_COLOR, 0, (int)ChartGetInteger(0, CHART_COLOR_CHART_LINE));  // rectangle
    PlotIndexSetInteger(0, PLOT_LINE_COLOR, 1, (int)ChartGetInteger(0, CHART_COLOR_CANDLE_BULL)); // up
    PlotIndexSetInteger(0, PLOT_LINE_COLOR, 2, (int)ChartGetInteger(0, CHART_COLOR_CANDLE_BEAR)); // down
  }
  else
  {
    PlotIndexSetInteger(0, PLOT_COLOR_INDEXES, 1);
    PlotIndexSetInteger(0, PLOT_LINE_COLOR, (int)ChartGetInteger(0, CHART_COLOR_CHART_LINE));
  }
}

Ao adicionar a chamada SetPlotColors() ao OnInit e ao definir a precisão dos valores, garantimos uma exibição correta do indicador.

  SetPlotColors();

  IndicatorSetString(INDICATOR_SHORTNAME, "SubChart (" + Symbol + ")");
  IndicatorSetInteger(INDICATOR_DIGITS, (int)SymbolInfoInteger(Symbol, SYMBOL_DIGITS));
  
  return INIT_SUCCEEDED;
}

No entanto, se o usuário alterar o modo gráfico enquanto o indicador estiver em execução, será necessário rastrear esse evento e modificar as propriedades dos buffers. Para fazer isso, criamos o manipulador OnChartEvent.

void OnChartEvent(const int id,
                  const long& lparam,
                  const double& dparam,
                  const string& sparam)
{
  if(id == CHARTEVENT_CHART_CHANGE)
  {
    mode = (ENUM_CHART_MODE)ChartGetInteger(0, CHART_MODE);
    PlotIndexSetInteger(0, PLOT_DRAW_TYPE, Mode2Style());
    SetPlotColors();
    ChartRedraw();
  }
}

Resta-nos escrever a função de indicador mais importante, isto é, o manipulador OnCalculate. Sua característica no nosso caso é que o indicador realmente funciona com base nas cotações de um símbolo de terceiros. Portanto, todas as técnicas de codificação padrão baseadas nos valores rate_total e prev_calculated transmitidos do kernel não são totalmente adequadas. O carregamento de cotações de um símbolo de terceiros ocorre de forma assíncrona e, portanto, a qualquer momento pode chegar um novo lote de barras exigindo um recálculo completo. Portanto, criamos variáveis que controlam o número de barras no símbolo externo (lastAvailable) e o clone editado do argumento constante prev_calculated.

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime& time[],
                const double& op[],
                const double& hi[],
                const double& lo[],
                const double& cl[],
                const long& tick_volume[],
                const long& volume[],
                const int& spread[])
{
  static int lastAvailable = 0;
  static bool initialized = false;

  int _prev_calculated = prev_calculated;

  if(iBars(Symbol, _Period) - lastAvailable > 1) // bar gap filled
  {
    _prev_calculated = 0;
    lastAvailable = 0;
    initialized = false;
  }

  if(_prev_calculated == 0)
  {
    for(int i = 0; i < rates_total; ++i)
    {
      open[i] = 0;
      high[i] = 0;
      low[i] = 0;
      close[i] = 0;
    }
  }

Se o símbolo do indicador for diferente do símbolo da janela, usamos a função iBarShift para encontrar barras síncronas e copiar seus valores OHLC.

  if(_Symbol != Symbol)
  {
    for(int i = 0; i < MathMax(rates_total - _prev_calculated, 1); ++i)
    {
      datetime dt = iTime(_Symbol, _Period, i);
      int x = iBarShift(Symbol, _Period, dt, Exact);
      if(x != -1)
      {
        open[i] = iOpen(Symbol, _Period, x);
        high[i] = iHigh(Symbol, _Period, x);
        low[i] = iLow(Symbol, _Period, x);
        close[i] = iClose(Symbol, _Period, x);
      }
    }
  }

Se o símbolo indicador corresponde ao símbolo da janela, então tudo é mais simples, pois usamos os argumentos-arrays que nos são passados:

  else
  {
    ArraySetAsSeries(op, true);
    ArraySetAsSeries(hi, true);
    ArraySetAsSeries(lo, true);
    ArraySetAsSeries(cl, true);
    for(int i = 0; i < MathMax(rates_total - _prev_calculated, 1); ++i)
    {
      open[i] = op[i];
      high[i] = hi[i];
      low[i] = lo[i];
      close[i] = cl[i];
    }
  }

Por fim, fornecemos o upload de dados, implementando a função RefreshHistory (incluiremos esse código como o arquivo de cabeçalho Refresh.mqh).

A variável estática initialized contém um sinal de final da atualização. Vamos defini-lo como true se RefreshHistory retornar um sinal de sucesso ou se o número de barras do símbolo de terceiros permanecer constante e diferente de zero (caso não haja histórico para o número necessário de barras no símbolo de terceiros).

  if(lastAvailable == iBars(Symbol, _Period) && lastAvailable != 0)
  {
    if(!initialized)
    {
      Print("Updated ", Symbol, " ", iBars(Symbol, _Period), " bars");
      initialized = true;
    }
    return rates_total;
  }

  if(!initialized)
  {
    if(_Symbol != Symbol)
    {
      Print("Updating ", Symbol, " ", lastAvailable, " -> ", iBars(Symbol, _Period), " bars up to ", (string)time[0], "... Please wait");
      int result = RefreshHistory(Symbol, time[0]);
      if(result >= 0 && result <= 2)
      {
        _prev_calculated = rates_total;
      }
      if(result >= 0)
      {
        initialized = true;
        ChartSetSymbolPeriod(0, _Symbol, _Period);
      }
    }
    else
    {
      initialized = true;
    }
  }
  
  lastAvailable = iBars(Symbol, _Period);
  
  return _Symbol != Symbol ? _prev_calculated : rates_total;
}

Se o processo de carregamento de dados estiver atrasado, talvez seja necessário atualizar o gráfico manualmente.

Depois que a inicialização for concluída uma vez, as novas barras serão calculadas em modo econômico, ou seja, tendo em conta _prev_calculated e rates_total. Se, no futuro, o número de barras lastAvailable mudar repentinamente em mais de 1, faremos um redesenho completo, redefinindo initialized para false.

Colocamos o indicador no gráfico (por exemplo, EURUSD) e inserimos outro símbolo nos parâmetros (por exemplo, UKBrent, isto é, o CFD no Brent, ele é interessante porque nos períodos intradiários, quando Exact está definido como true, há ausência de barras noturnas). "Clicamos" nos botões de mudança de modo e certificar-nos de que o indicador esteja desenhado corretamente.

Alternando o modo de exibição do indicador

Alternando o modo de exibição do indicador

Observe que no modo de exibição de linha única, nosso indicador usa o primeiro buffer (índice 0), ou seja, open. Isso o diferencia do gráfico principal, que é exibido segundo preços. close. Isso é feito para não redesenhar completamente o indicador ao alternar para ou a partir do estilo de linha. Para exibir os preços de fechamento, é necessário copiá-los no primeiro (e único) buffer, já nos modos de candle e barra (quando buffers 4) os preços de abertura são mantidos. A implementação atual usa o fato de que o buffer open é o primeiro no OHLC e, portanto, uma mudança de estilo leva imediatamente a uma mudança na representação externa sem recálculo. Ao analisar o histórico do modo de linhas, é improvável que esteja em demanda e, portanto, esse recurso não é crítico.

Executamos o indicador no testador visual.

Indicador SubChart no testador visual

Indicador SubChart no testador visual

Agora, com base neste indicador, podemos começar diretamente a criar a visualização do histórico de negociação.

Indicador SubChartReporter

Vamos chamar o novo indicador de SubChartReporter. Complementamos código já criado com a leitura de relatórios nos formatos HTML e CSV. Definimos o nome do arquivo analisado na variável de entrada ReportFile. Também fornecemos parâmetros de entrada para especificar a mudança de horário, bem como prefixos e sufixos de símbolos para casos em que um relatório é recebido de outro usuário (de outro ambiente de negociação).

input string ReportFile = ""; // · ReportFile
input string Prefix = ""; // · Prefix
input string Suffix = ""; // · Suffix
input int  TimeShift = 0; // · TimeShift

No indicador SubChartReporter, classes especiais são envolvidas no processamento dos dados recebidos e na geração de objetos gráficos (linhas de tendência).

Como pretendemos analisar não apenas arquivos HTML, mas também CSV, para todos os tipos de relatórios Processor foi projetada uma classe comum básica e para formatos HTML e CSV foram herdadas implementações específicas (ReportProcessor e HistoryProcessor, respectivamente).

Na classe Processor são descritas as seguintes variáveis:

class Processor
{
  protected:
    string symbol;
    string realsymbol;
    IndexMap *data;
    ulong timestamp;
    string prefix;
  • symbol — nome do símbolo de trabalho atual, retirado do relatório;
  • realsymbol — nome do símbolo de trabalho real disponível - pode diferir ao trabalhar com os relatórios de outras pessoas devido a prefixos e sufixos;
  • data — array de operações de negociação (classe IndexMap já é conhecida graças ao artigo [1] e é conectada a partir do arquivo de cabeçalho com o mesmo nome);
  • timestamp и prefix — variáveis auxiliares para nomeação única de objetos gráficos que o indicador irá gerar no gráfico;

Além disso, para alternar entre os diferentes símbolos do relatório, criamos botões na interface e armazenamos numa variável um identificador de botão atualmente pressionado (correspondente ao símbolo selecionado):

    string pressed;

Os seguintes métodos auxiliares para gerar objetos são implementados na classe Processor:

    void createTrend(const long dealIn, const long dealOut, const int type, const datetime time1, const double price1, const datetime time2, const double price2, const string &description)
    void createButton(const int x, const int y, const int dx, const int dy, const string text, const bool selected)
    void controlPanel(const IndexMap &symbols)

Como se pode entender a partir dos nomes, createTrend cria uma representação visual para um único trade (linha de tendência e duas setas), createButton cria um botão para o símbolo de trabalho e controlPanel fornece um conjunto completo de botões para todos os símbolos do relatório. Os botões são exibidos no canto inferior esquerdo da sub-janela.

A interface pública da classe Processor inclui dois grupos de métodos:

  • virtuais, sujeitos a uma implementação específica nas classes de herdeiros;
  • não-virtuais, fornecendo uma funcionalidade padrão única;

    virtual IndexMap *load(const string file) = 0;
    virtual int getColumnCount() = 0;
    virtual int getSymbolColumn() = 0;
    virtual datetime getStart() = 0;
    virtual bool applyInit() { return true; }
    virtual void makeTrade(IndexMap *row) = 0;
    virtual int render() = 0;

    bool attach(const string file)
    bool apply(const string _s = NULL)
    bool isEmpty() const
    string findrealsymbol()
    string _symbol() const
    string _realsymbol() const
    void onChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)

O código fonte completo pode ser encontrado nos arquivos anexados, aqui damos apenas uma breve descrição.

  • load — carrega o arquivo de dados especificado;
  • getColumnCount — retorna o número de colunas na tabela de dados;
  • getSymbolColumn — retorna o número da coluna com o nome dos símbolos das operações de negociação;
  • getStart — retorna a data da primeira operação de negociação;
  • applyInit — método para inicialização opcional de estruturas de dados internas antes do processamento;
  • makeTrade — método para registrar um registro de um relatório em estruturas de dados internas;
  • render — método para gerar objetos gráficos baseados em registros em estruturas de dados internas;

Os dois últimos métodos existem separadamente pois alguns formatos de relatório, em particular os relatórios HTML MT5, contêm registros de trades, enquanto precisamos exibir posições. Portanto, é necessária uma conversão adicional de uma entidade para outra, com a necessidade de visualizar um array de trades.

Os métodos não virtuais são apresentados abaixo de forma simplificada. O método attach pega o nome do arquivo analisado, carrega-o usando o método virtual load, cria uma lista de símbolos únicos e cria para eles um painel de botões.

    bool attach(const string file)
    {
      data = load(file);
      
      IndexMap symbols;
      
      for(int i = 0; i < data.getSize(); ++i)
      {
        IndexMap *row = data[i];
        // collect all unique symbols
        string s = row[getSymbolColumn()].get<string>();
        StringTrimLeft(s);
        if(StringLen(s) > 0) symbols.set(s);
      }

      if(symbols.getSize() > 0)
      {
        controlPanel(symbols);
      }
      return true;
    }

O método apply ativa no indicador o símbolo de trabalho selecionado dentre aqueles no relatório. Primeiro, objetos antigos são removidos do gráfico (se existirem), em seguida, um símbolo real adequado é pesquisado (por exemplo, sua corretora disponibiliza EURUSD em vez de EURUSD.m mencionado no relatório) e as classes de herdeiros têm a oportunidade de redefinir arrays antigas usando applyInit. Depois, para os registros na tabela de dados nos quais o nome do símbolo corresponde ao selecionado, são gerados trades (chamada de makeTrade), são criados objetos gráficos (chamada de render) com base nesses trades e é atualizada a interface (seleção ativa do botão, substituição do nome do indicador, chamada de ChartRedraw).

                                                                                                                                          
    bool apply(const string _s = NULL)
    {
      ObjectsDeleteAll(0, "SCR", ChartWindowFind(), OBJ_TREND);

      if(_s != NULL && _s != "") symbol = _s;
      if(symbol == NULL)
      {
        Print("No symbol selected");
        return false;
      }
      
      string real = findrealsymbol();
      if(real == NULL)
      {
        Print("No suitable symbol found");
        return false;
      }
      
      SymbolSelect(real, true);

      if(!applyInit()) return false;
      
      int selected = 0;
  
      for(int i = 0; i < data.getSize(); ++i)
      {
        IndexMap *row = data[i];
        
        string s = row[getSymbolColumn()].get<string>();
        StringTrimLeft(s);
        
        if(s == symbol)
        {
          selected++;
          makeTrade(row);
        }
      }
      
      pressed = prefix + "#" + symbol;
      ObjectSetInteger(0, pressed, OBJPROP_BGCOLOR, clrGreen);
      
      int trends = render();
      Print(data.getSize(), " records in total");
      Print(selected, " trades for ", symbol);
      
      string title = CHART_REPORTER_TITLE + " (" + symbol + ", " + (string)selected + " records, " + (string)trends + " trades)";
      IndicatorSetString(INDICATOR_SHORTNAME, title);
      
      ChartRedraw();
      return true;
    }

O método findrealsymbol usa várias abordagens para encontrar símbolos adequados. Ele verifica a disponibilidade de informações de mercado (preços bid) para um símbolo, e, se elas existirem, o símbolo é considerado real. Se não existirem, o programa tenta aplicar os parâmetros Suffix e/ou Prefix (claro, se especificados), ou seja, excluir ou adicioná-los ao nome do símbolo. Se a modificação resultar na obtenção do preço, é mais provável que um pseudônimo de trabalho seja encontrado para o símbolo.

Os métodos _symbol e _realsymbol retornam o nome do símbolo atual a partir do relatório e seu próprio duplo de trabalho para sua conta. Ao analisar relatórios próprios, você receberá o mesmo nome de instrumento, a menos que a corretora a exclua após algum tempo.

O método onChartEvent é projetado para manipular eventos OnChartEvent, ou melhor, pressionar botões. O botão selecionado anteriormente (se estiver) fica cinza como todos os outros e, mais importante, é chamado o método virtual apply, para o qual o nome do novo símbolo é transferido, selecionado a partir do identificador de botão.

    void onChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)
    {
      if(id == CHARTEVENT_OBJECT_CLICK)
      {
        int x = StringFind(sparam, "_#");
        if(x != -1)
        {
          string s = StringSubstr(sparam, x + 2);
          Print(s, " ", sparam, " ", pressed);
          
          ObjectSetInteger(0, sparam, OBJPROP_STATE, false);
          ObjectSetInteger(0, pressed, OBJPROP_STATE, false);
          ObjectSetInteger(0, pressed, OBJPROP_BGCOLOR, clrGray);
          pressed = "";
          
          if(apply(s)) // will set pressed and other properties
          {
            ChartSetSymbolPeriod(0, _Symbol, _Period);
          }
        }
      }
    }

Chegou a hora de implementar a funcionalidade básica nos métodos virtuais das classes-herdeiros. Comecemos com o ReportProcessor.

Para analisar páginas HTML, usamos o WebDataExtractor do artigo [1], formatando-o como um arquivo de inclusão WebDataExtractor.mqh. Em comparação com os códigos fonte originais neste arquivo, todos os métodos de trabalho são agrupados na classe HTMLConverter para não serem descartados no contexto global. Estamos principalmente interessados no método HTMLConverter::convertReport2Map — ele coincide quase completamente com a função discutida process em [1]. A entrada de convertReport2Map aparece o nome do arquivo de relatório (a partir de ReportFile) e, na saída, obtemos um mapa IndexMap com linhas correspondentes às operações de negociação na tabela do relatório.

Todas as configurações necessárias para analisar relatórios HTML, como RowSelector e ColumnSettingsFile, já estão indicadas no arquivo de cabeçalho, mas podem ser editadas porque elas são descritas como parâmetros de entrada. Por padrão, os parâmetros indicam as configurações para os relatórios MT5, e a tabela de trades (deal) é extraída deles, e com base neles, como será mostrado mais tarde, são calculadas as posições exibidas. Cada trade é descrito por uma instância da classe especial Deal. O construtor Deal toma um registro da tabela de relatório, compactado no IndexMap. Deal armazena campos com o tempo e o preço, tipo, direção, volume e outras propriedades do trade.

class ReportProcessor: public Processor
{
  private:
    class Deal   // if MQL5 could respect private access specifier for classes,
    {            // Trades will be unreachable from outer world, so it would be fine to have
      public:    // fields made public for direct access from Processor only
        datetime time;
        double price;
        int type;      // +1 - buy, -1 - sell
        int direction; // +1 - in, -1 - out, 0 - in/out
        double volume;
        double profit;
        long deal;
        long order;
        string comment;
        
      public:
        Deal(const IndexMap *row) // this is MT5 deal
        {
          time = StringToTime(row[COLUMN_TIME].get<string>()) + TimeShift;
          price = StringToDouble(row[COLUMN_PRICE].get<string>());
          string t = row[COLUMN_TYPE].get<string>();
          type = t == "buy" ? +1 : (t == "sell" ? -1 : 0);
          t = row[COLUMN_DIRECTION].get<string>();
          direction = 0;
          if(StringFind(t, "in") > -1) ++direction;
          if(StringFind(t, "out") > -1) --direction;
          volume = StringToDouble(row[COLUMN_VOLUME].get<string>());
          t = row[COLUMN_PROFIT].get<string>();
          StringReplace(t, " ", "");
          profit = StringToDouble(t);
          deal = StringToInteger(row[COLUMN_DEAL].get<string>());
          order = StringToInteger(row[COLUMN_ORDER].get<string>());
          comment = row[COLUMN_COMMENT].get<string>();
        }
    
        bool isIn() const
        {
          return direction >= 0;
        }
        
        bool isOut() const
        {
          return direction <= 0;
        }
        
        bool isOpposite(const Deal *t) const
        {
          return type * t.type < 0;
        }
        
        bool isActive() const
        {
          return volume > 0;
        }
    };

Todos os trades estão na matriz array. No processo de análise progressiva do histórico, vamos colocar os trades de entrada na fila (queue) e removê-los de lá assim que encontrarmos um trade de saída oposto adequado. Se a fila se mostrar não vazia após a passagem de todo o histórico, significa que há alguma posição aberta.

    RubbArray<Deal *> array;
    RubbArray<Deal *> queue;

A classe RubbArray é um wrapper para um array dinâmico que se expande automaticamente para os dados recebidos.

Veja são implementados como alguns métodos virtuais:

    virtual IndexMap *load(const string file) override
    {
      return HTMLConverter::convertReport2Map(file, true);
    }

    virtual int getColumnCount() override
    {
      return COLUMNS_COUNT;
    }

    virtual int getSymbolColumn() override
    {
      return COLUMN_SYMBOL;
    }

Os dois últimos métodos usam definição de macros para a tabela padrão de trades do relatório HTML MT5 .

#define COLUMNS_COUNT 13
#define COLUMN_TIME 0
#define COLUMN_DEAL 1
#define COLUMN_SYMBOL 2
...

O método applyInit limpa as matrizes array e queue.

    virtual bool applyInit() override
    {
      ((BaseArray<Deal *> *)&queue).clear();
      array.clear();
      return true;
    }

Os objetos de trade são criados e colocados na matriz array no método makeTrade.

    virtual void makeTrade(IndexMap *row) override
    {
      array << new Deal(row);
    }

Finalmente, o mais interessante, mas também o mais difícil, é a análise da lista de trades e a geração de objetos de negociação com bases neles.

    virtual int render() override
    {
      int count = 0;
      
      for(int i = 0; i < array.size(); ++i)
      {
        Deal *current = array[i];
        
        if(!current.isActive()) continue;
        
        if(current.isOut())
        {
          // first try to find exact match
          for(int j = 0; j < queue.size(); ++j)
          {
            if(queue[j].isIn() && queue[j].isOpposite(current) && queue[j].volume == current.volume)
            {
              string description;
              StringConcatenate(description, (float)queue[j].volume, "[", queue[j].deal, "/", queue[j].order, "-", current.deal, "/", current.order, "] ", (current.profit < 0 ? "-" : ""), current.profit, " ", current.comment);
              createTrend(queue[j].deal, current.deal, queue[j].type, queue[j].time, queue[j].price, current.time, current.price, description);
              current.volume = 0;
              queue >> j; // remove from queue
              ++count;
              break;
            }
          }

          if(!current.isActive()) continue;
          
          // second try to perform partial close
          for(int j = 0; j < queue.size(); ++j)
          {
            if(queue[j].isIn() && queue[j].isOpposite(current))
            {
              string description;
              if(current.volume >= queue[j].volume)
              {
                StringConcatenate(description, (float)queue[j].volume, "[", queue[j].deal, "/", queue[j].order, "-", current.deal, "/", current.order, "] ", (current.profit < 0 ? "-" : ""), current.profit, " ", current.comment);
                createTrend(queue[j].deal, current.deal, queue[j].type, queue[j].time, queue[j].price, current.time, current.price, description);

                current.volume -= queue[j].volume;
                queue[j].volume = 0;
                ++count;
              }
              else
              {
                StringConcatenate(description, (float)current.volume, "[", queue[j].deal, "/", queue[j].order, "-", current.deal, "/", current.order, "] ", (current.profit < 0 ? "-" : ""), current.profit, " ", current.comment);
                createTrend(queue[j].deal, current.deal, queue[j].type, queue[j].time, queue[j].price, current.time, current.price, description);

                queue[j].volume -= current.volume;
                current.volume = 0;
                ++count;
                break;
              }
            }
          }
          
          // purge all inactive from queue
          for(int j = queue.size() - 1; j >= 0; --j)
          {
            if(!queue[j].isActive())
            {
              queue >> j;
            }
          }
        }
        
        if(current.isActive()) // is _still_ active
        {
          if(current.isIn())
          {
            queue << current;
          }
        }
      }
      
      if(!isQueueEmpty())
      {
        Print("Warning: not all deals are processed (probably, open positions left).");
      }
      
      return count;
    }

O algoritmo avança ao longo da lista de trades e coloca na fila os trades de entrada no mercado. Após a detecção de um trade de saída na fila, o trade oposto é procurado pela primeira vez. Se este não for o caso, de acordo com o princípio FIFO, os volumes de trades de entrada são consistentemente selecionados para cobrir o volume de saída. Os trades que perdem o volume caindo para zero são removidos da fila. Cada combinação de volume de entrada e de saída cria sua própria linha de tendência (createTrend).

O modo FIFO é selecionado como o mais razoável do ponto de vista algorítmico, mas isso não o torna o único verdadeiro. Um robô específico pode fechar negócios não apenas segundo FIFO, mas também segundo LIFO, e até mesmo numa ordem arbitrária. O mesmo se aplica ainda mais à negociação manual. Portanto, para estabelecer uma correspondência entre os trades de abertura e de fechamento no modo de cobertura, é necessário encontrar uma solução alternativa, como uma análise de lucros ou de comentários. Um exemplo de cálculo de lucro entre dois pontos de preço pode ser encontrado no meu blog, no entanto, não há registro de ajustes cambiais. Em geral, essa tarefa não é tão trivial quanto parece à primeira vista e é deixada além do escopo deste artigo. 

Assim, revisamos, em termos gerais, como o relatório HTML é processado nas classes descritas. No caso do arquivo CSV, a implementação da classe HistoryProcessor é muito mais simples. Se necessário, ela é fácil aprender com os códigos fonte anexados. Apenas observe que os arquivos CSV com o histórico de sinais mql5.com têm um número diferente de colunas para MT4 e MT5 — um formato específico é selecionado automaticamente com base na extensão dupla: ".history.csv" para MT4 e ".positions.csv" para MT5 A única configuração para arquivos CSV é o caractere delimitador (nos arquivos de sinal mql5.com, o padrão é ';').

Lembre-se de que o indicador SubChartReporter herda a maior parte do código fonte de SubChart, no qual verificamos a abordagem com a exibição de cotações de terceiros nas subjanelas, e aqui faz sentido prestar atenção apenas aos novos fragmentos.

Objetos de classe são criados e usados em manipuladores de eventos. Em particular, é óbvio que o processador é criado em OnInit e destruído em Ondeinit:

Processor *processor = NULL;

int OnInit()
{
  if(StringFind(ReportFile, ".htm") > 0)
  {
    processor = new ReportProcessor();
  }
  else if(StringFind(ReportFile, ".csv") > 0)
  {
    processor = new HistoryProcessor();
  }
  string Symbol = SubSymbol;
  if(Symbol == "") Symbol = _Symbol;
  else SymbolSelect(Symbol, true);
  processor.apply(Symbol);
  ...
}

void OnDeinit(const int reason)
{
  if(processor != NULL) delete processor;
}

O processamento de eventos de objetos gráficos é adicionado ao manipulador OnChartEvent:

void OnChartEvent(const int id,
                  const long& lparam,
                  const double& dparam,
                  const string& sparam)
{
  if(id == CHARTEVENT_CHART_CHANGE)
  {
    ... // same code
  }
  else
  {
    processor.onChartEvent(id, lparam, dparam, sparam);
  }
}

No manipulador OnCalculate é necessário monitorar a situação quando o símbolo analisado atual é alterado em resposta às ações do usuário, levando ao recálculo completo:

  string Symbol = processor._realsymbol();
  if(Symbol == NULL) Symbol = _Symbol;
  if(lastSymbol != Symbol)
  {
    _prev_calculated = 0;
    lastAvailable = 0;
    initialized = false;
    IndicatorSetInteger(INDICATOR_DIGITS, (int)SymbolInfoInteger(Symbol, SYMBOL_DIGITS));
  }

Quando o desenho inicial é concluído e o número de barras disponíveis no símbolo de trabalho está estabilizado, iniciamos o temporizador para carregar os dados do relatório.

  if(lastAvailable == iBars(Symbol, _Period) && lastAvailable != 0)
  {
    if(!initialized)
    {
      Print("Updated ", Symbol, " ", iBars(Symbol, _Period), " bars");
      initialized = true;
      if(ReportFile != "") //
      {                    //
        EventSetTimer(1);  //
      }                    //
    }
    
    return rates_total;
  }

No manipulador de temporizador, carregamos o relatório (se ainda não tiver sido carregado) e ativamos o símbolo selecionado (ele é definido a partir dos parâmetros em OnInit ou pressionando um botão em OnChartEvent).

void OnTimer()
{
  EventKillTimer();
  
  if(processor.isEmpty()) // load file only once
  {
    if(processor.attach(ReportFile))
    {
      processor.apply(/*keep already selected symbol*/);
    }
    else
    {
      Print("File loading failed: ", ReportFile);
    }
  }
}

Experimentemos o indicador em ação. Para fazer isso, abrimos o gráfico EURUSD, adicionamos um indicador e especificamos o arquivo ReportTester-example.html (anexado) no parâmetro ReportFile. Após a inicialização, veremos o gráfico EURUSD na sub-janela (porque o parâmetro Symbol foi deixado em branco) e uma linha de botões com os nomes de todos os símbolos mencionados no relatório.

Indicador SubChartReporter com símbolo de trabalho não selecionado

Indicador SubChartReporter com símbolo de trabalho não selecionado

Como o relatório não tem EURUSD, todos os botões são cinza. Ao clicar em qualquer botão, por exemplo, EURGBP, essa moeda é carregada no painel de cotações e também são exibidos seus trades. O botão em si fica verde.

Indicador SubChartReporter com símbolo de trabalho selecionado

Indicador SubChartReporter com símbolo de trabalho selecionado

Na implementação atual, a ordem dos botões é determinada pela cronologia da ocorrência de símbolos no relatório. Se necessário, pode ser classificado em qualquer ordem desejada, por exemplo, alfabeticamente ou pelo número de trades.

Ao alternar com a ajuda de botões, podemos examinar todos os símbolo a partir do relatório, mas isso não é muito conveniente. Para alguns relatórios, seria bom ver todos os caracteres de uma vez — cada um em sua própria subjanela. Para isso, escrevemos um script SubChartsBuilder para criar sub-janelas e carregar as instâncias do indicador SubChartReporter para diferentes símbolos.

Script SubChartsBuilder

O script tem um conjunto de parâmetros completamente semelhante ao indicador de inicialização SubChartReporter. Para fornecer uma única inicialização, os parâmetros são adicionados ao array MqlParam e, em seguida, IndicatorCreate é chamado a partir da API MQL. Tudo isso é resumido numa função de aplicativo createIndicator.

bool createIndicator(const string symbol)
{
  MqlParam params[18] =
  {
    {TYPE_STRING, 0, 0.0, "::Indicators\\SubChartReporter.ex5"},
    
    {TYPE_INT, 0, 0.0, NULL}, // chart settings
    {TYPE_STRING, 0, 0.0, "XYZ"},
    {TYPE_BOOL, 1, 0.0, NULL},
    
    {TYPE_INT, 0, 0.0, NULL}, // common settings
    {TYPE_STRING, 0, 0.0, "HTMLCSV"},
    {TYPE_STRING, 0, 0.0, "PREFIX"},
    {TYPE_STRING, 0, 0.0, "SUFFIX"},
    {TYPE_INT, 0, 0.0, NULL}, // time shift

    {TYPE_INT, 0, 0.0, NULL}, // html settings
    {TYPE_STRING, 0, 0.0, "ROW"},
    {TYPE_STRING, 0, 0.0, "COLUMNS"},
    {TYPE_STRING, 0, 0.0, "SUBST"},
    {TYPE_BOOL, 0, 0.0, NULL},
    {TYPE_BOOL, 0, 0.0, NULL},
    {TYPE_BOOL, 0, 0.0, NULL},

    {TYPE_INT, 0, 0.0, NULL}, // csv settings
    {TYPE_STRING, 0, 0.0, ";"}
  };
  
  params[2].string_value = symbol;
  params[5].string_value = ReportFile;
  params[6].string_value = Prefix;
  params[7].string_value = Suffix;
  params[8].integer_value = TimeShift;
  params[10].string_value = RowSelector;
  params[11].string_value = ColumnSettingsFile;
  params[12].string_value = SubstitutionSettingsFile;
  params[17].string_value = CSVDelimiter;
  
  int handle = IndicatorCreate(_Symbol, _Period, IND_CUSTOM, 18, params);
  if(handle == INVALID_HANDLE)
  {
    Print("Can't create SubChartReporter for ", symbol, ": ", GetLastError());
    return false;
  }
  else
  {
    if(!ChartIndicatorAdd(0, (int)ChartGetInteger(0, CHART_WINDOWS_TOTAL), handle))
    {
      Print("Can't attach SubChartReporter for ", symbol, ": ", GetLastError());
      return false;
    }
  }
  return true;
}

Note que o indicador é retirado do recurso, para o qual é registrado anteriormente no código fonte na instrução:

#resource "\\Indicators\\SubChartReporter.ex5"

Assim, o script é um programa autossuficiente e não depende da presença de um indicador para um usuário específico.

Além disso, essa abordagem é aplicada com outro objetivo. Acontece que, ao gerar instâncias de indicadores para todos os símbolos do relatório, não há necessidade de botões de controle. No entanto, a MQL, infelizmente, não fornece nenhum recurso para determinar se o indicador é iniciado pelo usuário ou por meio de IndicatorCreate, isto é, se funciona por si só ou é uma parte integral (dependente) de um programa maior. Ao colocar o indicador no recurso, temos a possibilidade de alterar a visibilidade dos botões, dependendo do caminho do indicador: o caminho no recurso (ou seja, a presença da string "::Indicators\\") indica a necessidade de suprimir a saída dos botões.

Para chamar a função createIndicator para cada símbolo do relatório, temos que analisar o relatório no script.

int OnStart()
{
  IndexMap *data = NULL;
  int columnsCount = 0, symbolColumn = 0;
  
  if(ReportFile == "")
  {
    Print("cleanUpChart");
    return cleanUpChart();
  }
  else if(StringFind(ReportFile, ".htm") > 0)
  {
    data = HTMLConverter::convertReport2Map(ReportFile, true);
    columnsCount = COLUMNS_COUNT;
    symbolColumn = COLUMN_SYMBOL;
  }
  else if(StringFind(ReportFile, ".csv") > 0)
  {
    data = CSVConverter::ReadCSV(ReportFile);
    if(data != NULL && data.getSize() > 0)
    {
      IndexMap *row = data[0];
      columnsCount = row.getSize();
      symbolColumn = CSV_COLUMN_SYMBOL;
    }
  }
  
  if(data != NULL)
  {
    IndexMap symbols;
    
    for(int i = 0; i < data.getSize(); ++i)
    {
      IndexMap *row = data[i];
      if(CheckPointer(row) == POINTER_INVALID || row.getSize() != columnsCount) break;
      
      string s = row[symbolColumn].get<string>();
      StringTrimLeft(s);
      if(StringLen(s) > 0) symbols.set(s);
    }
    
    for(int i = 0; i < symbols.getSize(); ++i)
    {
      createIndicator(symbols.getKey(i));
    }
    delete data;
  }

  return 0;
}

Observe que, se você executar um script com um nome de relatório vazio, ele removerá da janela todas as sub-janelas com instâncias de indicador (se elas tiverem sido criadas anteriormente). Isso é feito pela função cleanUpChart.

bool cleanUpChart()
{
  bool result = true;
  int n = (int)ChartGetInteger(0, CHART_WINDOWS_TOTAL);
  for(int i = n - 1; i > 0; --i)
  {
    string name = ChartIndicatorName(0, i, 0);
    if(StringFind(name, "SubChartReporter") == 0)
    {
      Print("Deleting ", name);
      result &= ChartIndicatorDelete(0, i, name);
    }
  }
  return result;
}

Isso pode ser conveniente para limpar o gráfico quando a análise do relatório estiver concluída.

Para testar o script, baixei vários arquivos CSV com históricos de sinais. Aqui está como pode ficar (o gráfico principal é minimizado):

Várias instâncias do SubChartReporter ao analisar a negociação em várias moedas

Várias instâncias do SubChartReporter ao analisar a negociação em várias moedas

Deixe-me lembrá-lo de que os objetos gerados têm descrições com detalhes retirados do relatório (números de trades, volumes, lucros, comentários) e, para exibi-los, é possível ativar a opção "Mostrar descrições de objetos" nas configurações do gráfico.

Se houver muitos símbolos de trabalho, a sub-janela é compactada. Isso, embora permita criar uma visão geral, dificulta o estudo de detalhes. Quando é preciso considerar cada trade, faz sentido usar o máximo de espaço possível, incluindo a própria janela principal. Para fazer isso, pode-se criar uma nova versão do indicador SubChartReporter que não use a sub-janela, mas exiba os trades no gráfico principal. Vamos chamá-lo de MainChartReporter.

Indicador MainChartReporter

Como esse indicador é exibido no gráfico de cotações, não é necessário contar os buffers e desenhar nada além de objetos-tendências. Em outras palavras, este é um indicador sem buffer que altera o símbolo de trabalho do gráfico atual para mudar o símbolo analisado. Do ponto de vista da implementação, quase tudo está pronto: o novo código fonte é um SubChartReporter significativamente simplificado.

Exibimos o código fonte das três classes principais - Processor, ReportProcessor, HistoryProcessor - no arquivo de cabeçalho e vamos inclui-lo nos dois indicadores. Essas diferenças específicas de cada versão são enquadradas com instruções de pré-processador para compilação condicional. Para o código do indicador SubChartReporter, definimos a macro CHART_REPORTER_SUB e para o código do indicador MainChartReporter — CHART_REPORTER_MAIN.

No caso do MainChartReporter, só é necessário adicionar 2 linhas. No método apply, é preciso alternar o gráfico para um novo símbolo real:

#ifdef CHART_REPORTER_MAIN
      ChartSetSymbolPeriod(0, real, _Period);
#endif

Também exibir um comentário com o texto, que na primeira versão do indicador foi definido como seu nome (INDICATOR_SHORTNAME).

#ifdef CHART_REPORTER_MAIN
      Comment(title);
#endif

No método manipulador de evento onChartEvent, atualizamos o gráfico atual, já que a primeira versão do indicador, como regra, desenha dados num símbolo que é diferente do símbolo da janela principal. Na nova versão, as aspas do símbolo principal são usadas "como estão", e não há necessidade de atualizar a janela, portanto a linha correspondente vai para a compilação condicional apenas do indicador SubChartReporter.

#ifdef CHART_REPORTER_SUB
            ChartSetSymbolPeriod(0, _Symbol, _Period);
#endif

Diretamente do código fonte do indicador MainChartReporter, excluímos os buffers (mais precisamente, escrevemos seu número igual a 0 para evitar o aviso do compilador).

#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots   0

Excluímos também como desnecessário o grupo de configurações da sub-janela:

input GroupSettings Chart_Settings; // S U B C H A R T    S E T T I N G S
input string SubSymbol = ""; // · Symbol
input bool Exact = true; // · Exact

A função OnCalculate fica vazia (mas deve estar presente no indicador). O temporizador para receber dados do relatório é inciado em OnInit.

void OnTimer()
{
  EventKillTimer();
  
  if(processor.isEmpty()) // load file only once
  {
    if(processor.attach(ReportFile))
    {
      processor.apply();
      datetime start = processor.getStart();
      if(start != 0)
      {
        ChartSetInteger(ChartID(), CHART_AUTOSCROLL, false);
        // FIXME: this does not work as expected
        ChartNavigate(ChartID(), CHART_END, -1 * (iBarShift(_Symbol, _Period, start)));
      }
    }
  }
}

Aqui é feita uma tentativa de rolar o gráfico para o primeiro trade chamando ChartNavigate. Infelizmente, não consegui atingir a eficiência desse fragmento — o deslocamento simplesmente não ocorre. Uma solução possível é vista ao determinar a posição atual e navegar com relação a ela usando CHART_CURRENT_POS, mas não parece ideal.

O indicador MainChartReporter aparece assim no gráfico:

Indicador MainChartReporter

Indicador MainChartReporter

Arquivos anexados

  • SubChart.mq5 — indicador SubChart;
  • SubChartReporter.mq5 — indicador SubChartReporter;
  • MainChartReporter.mq5 — indicador MainChartReporter;
  • SubChartsBuilder.mq5 — script para criar um grupo de instâncias do indicador SubChartReporter para todos os símbolos do relatório;
  • ChartReporterCore.mqh — principais classes comuns para indicadores;
  • WebDataExtractor.mqh — analisador HTML;
  • CSVReader.mqh — analisador CSV;
  • HTMLcolumns.mqh — definição de colunas de relatórios HTML;
  • CSVcolumns.mqh — definição de colunas de arquivos CSV;
  • IndexMap.mqh — classe de mapa auxiliar;
  • RubbArray.mqh — classe auxiliar do array de borracha;
  • StringUtils.mqh — funções auxiliares para trabalhar com strings;
  • empty_strings.h — lista de tags vazias para o analisador HTML;
  • GroupSettings.mqh — formatação de grupos de parâmetros de entrada;
  • Refresh.mqh — solicitação de cotações por símbolos;
  • ReportTester-example.html — exemplo de relatório HTML do testador;
  • ReportHistoryDeals.cfg.csv — configurações do seletor CSS para selecionar as colunas da tabela com um analisador HTML.

Fim do artigo

Analisamos vários indicadores que nos permitem visualizar cotações e trades para vários símbolos. A fonte de dados de entrada são relatórios em formato HTML e, graças ao uso de um analisador universal, é possível conectar não apenas relatórios padrão (configurações já inclusas no código fonte), mas também outros. Também é fornecido suporte para arquivos CSV nos quais é proporcionado o histórico de negociação de sinais mql5.com. Código aberto permite adaptar o programa às suas necessidades.

Traduzido do russo pela MetaQuotes Software Corp.
Artigo original: https://www.mql5.com/ru/articles/5913

Arquivos anexados |
report2chart.zip (34.55 KB)
Biblioteca para desenvolvimento fácil e rápido de programas para a MetaTrader (parte III). Coleção de ordens e posições de mercado, busca e ordenação Biblioteca para desenvolvimento fácil e rápido de programas para a MetaTrader (parte III). Coleção de ordens e posições de mercado, busca e ordenação

Na primeira parte, começamos a criar uma grande biblioteca multi-plataforma, simplificando o desenvolvimento de programas para as plataformas MetaTrader 5 e MetaTrader 4. Além disso, nós implementamos a coleção do histórico de ordens e negócios. Nosso próximo passo é criar uma classe para uma seleção conveniente e a ordenação de ordens, negócios e posições nas listas de coleção. Nós vamos implementar o objeto da biblioteca base chamada Engine e adicionar uma coleção de ordens e posições de mercado para a biblioteca.

Como escrever uma biblioteca DLL em MQL5 (Parte II) em 10 minutos: escrevendo no ambiente do Visual Studio 2017 Como escrever uma biblioteca DLL em MQL5 (Parte II) em 10 minutos: escrevendo no ambiente do Visual Studio 2017

O artigo básico inicial não perdeu sua importância e todos os interessados neste tópico simplesmente devem lê-lo. Mas já se passou muito tempo desde então, e agora o Visual Studio 2017 com uma nova interface está à frente, também a própria plataforma MetaTrader 5 vem se desenvolvendo e segue em frente. O artigo descreve as etapas de criação de um projeto dll, abrangendo configurações e colaboração com as ferramentas do terminal MetaTrader 5.

Utilitário para seleção e navegação em MQL5 e MQL4: aumentamos a informatividade de gráficos Utilitário para seleção e navegação em MQL5 e MQL4: aumentamos a informatividade de gráficos

Neste artigo, continuaremos a expandir a funcionalidade do nosso utilitário. Desta vez, adicionaremos a possibilidade de exibir informações em gráficos projetados para facilitar nossa negociação. Em particular, adicionaremos ao gráfico os preços máximo e mínimo do dia anterior, os níveis arredondados, os preços máximo e mínimo do ano, a hora de início da sessão, etc.

Biblioteca para o desenvolvimento fácil e rápido de programas para a MetaTrader (parte IV): eventos de negociação Biblioteca para o desenvolvimento fácil e rápido de programas para a MetaTrader (parte IV): eventos de negociação

Nos artigos anteriores, nós começamos a criar uma grande biblioteca multi-plataforma, simplificando o desenvolvimento de programas para as plataformas MetaTrader 5 e MetaTrader 4. Nós já temos as coleções do histórico de ordens e negócios, ordens e posições de mercado, bem como a classe para a seleção conveniente e ordenação das ordens. Nesta parte, nós continuaremos com o desenvolvimento do objeto base e ensinaremos a Biblioteca Engine a monitorar os eventos de negociação na conta.