Como analisar os trades do Sinal selecionado no gráfico

Dmitriy Gizlyk | 11 julho, 2018


Sumário

Introdução

No serviço Sinais, aparecem constantemente novos sinais, tanto pagos quanto gratuitos. A equipe MetaTrader se assegurou de que o serviço pudesse ser usado sem sair do terminal. Resta apenas escolher o sinal que trará o máximo lucro com riscos aceitáveis. Esta preocupação tem sido discutida há bastante tempo. Na realidade, já foi proposto [1] um método de seleção automática de sinal por meio de critérios especificados. Mas, como diz o ditado popular, uma imagem vale mais que mil palavras. No artigo, proponho estudar e analisar o histórico de trades do sinal selecionado no gráfico do instrumento. Talvez essa abordagem nos permita entender melhor a estratégia para realizar trades e avaliar os riscos. 

1. Formulando os objetivos do trabalho à frente

Você se perguntará: 'para que reinventar a roda quando o terminal já tem a capacidade de exibir o histórico de trades no gráfico?' Afinal, basta selecionar o sinal desejado e clicar no botão no terminal.

Comando "Mostrar transações no gráfico"

Depois disso, no terminal são abertas novas janelas, de acordo com o número de instrumentos utilizados pelo sinal, com rótulos dos trades concluídos. Claro, percorrer gráficos para procurar trades é uma tarefa demorada. Além disso, o fato de trades em gráficos diferentes poderem coincidir no tempo não é visível na análise de cada gráfico. Nesta etapa, tentaremos automatizar parte do trabalho.

A fim de determinar qual instrumento precisamos para analisar os gráficos resultantes, devemos entender claramente quais são os dados finais de que precisamos. Veja os principais pontos do que eu gostaria de obter:

  • ver como funciona o sinal em diferentes instrumentos;
  • como é distribuída a carga no depósito, quantas posições podem ser abertas simultaneamente;
  • se o sinal abre várias posições simultaneamente, se são de cobertura ou se aumentam a carga no depósito;
  • em quais momentos e em quais instrumentos surgem os rebaixamentos máximos;
  • em que momentos é alcançado o lucro máximo.

2. Fazendo estatísticas de trades

2.1. Classe para armazenar informações sobre a ordem

Selecione o sinal desejado e exiba seu histórico de trades no gráfico. Colete os dados iniciais que analisará mais tarde. Para registrar informações sobre cada ordem específica, crie uma classe COrder com base na classe CObject. Nas variáveis dessa classe, salve a boleta da ordem, o volume e o tipo de trade, o preço do trade, o tipo de operação (entrada/saída), o horário de abertura da ordem e, claro, o instrumento.

class COrder : public CObject
  {
private:
   long                 l_Ticket;
   double               d_Lot;
   double               d_Price;
   ENUM_POSITION_TYPE   e_Type;
   ENUM_DEAL_ENTRY      e_Entry;
   datetime             dt_OrderTime;
   string               s_Symbol;
   
public:
                        COrder();
                       ~COrder();
   bool                 Create(string symbol, long ticket, double volume, double price, datetime time, ENUM_POSITION_TYPE type);
//---
   string               Symbol(void)   const {  return s_Symbol;     }
   long                 Ticket(void)   const {  return l_Ticket;     }
   double               Volume(void)   const {  return d_Lot;        }
   double               Price(void)    const {  return d_Price;      }
   datetime             Time(void)     const {  return dt_OrderTime; } 
   ENUM_POSITION_TYPE   Type(void)           {  return e_Type;       }
   ENUM_DEAL_ENTRY      DealEntry(void)const {  return e_Entry;      }
   void                 DealEntry(ENUM_DEAL_ENTRY value) {  e_Entry=value; }
//--- methods for working with files
   virtual bool         Save(const int file_handle);
   virtual bool         Load(const int file_handle);
//---
   //--- method of comparing the objects
   virtual int          Compare(const CObject *node,const int mode=0) const;
  };

Além da função de acesso a dados, adicione as à classe de ordens as funções de trabalho com arquivos para salvar a subsequente leitura de dados, bem como uma função de comparação com um objeto similar, você precisará desta função para classificar as ordens.

Para comparar duas ordens, você precisará reescrever a função virtual Compare. Esta é uma função da classe base, projetada para comparar dois objetos CObject. Por isso, em seus parâmetros, são transferidos a referência ao objeto CObject e o método de classificação. Você classificará nossas ordens em apenas uma direção (segundo a data de execução), daí que você não usará o parâmetro mode no código da função. Mas para trabalhar com o objeto COrder, obtido por referência, você precisa, primeiro, trazê-lo para o tipo apropriado. Depois disso, compare as datas das ordens recebidas e atuais. Se a ordem recebida for mais antiga — retorne "-1", se for mais nova — "1". Se as datas de execução das ordens forem iguais, a função retornará "0".

int COrder::Compare(const CObject *node,const int mode=0) const
  {
   const COrder *temp=GetPointer(node);
   if(temp.Time()>dt_OrderTime)
      return -1;
//---
   if(temp.Time()<dt_OrderTime)
      return 1;
//---
   return 0;
  }

2.2. Coletando informações a partir de gráficos

Para trabalhar com ordens, você criará uma classe COrdersCollection com base na classe CArrayObj. Nela, serão coletadas e processadas as informações. Para armazenar dados, declare uma instância de objeto e uma matriz, para trabalhar diretamente com uma ordem específica e para armazenar a lista de instrumentos usados, respectivamente. A matriz de ordens será armazenada usando as funções da classe base.

class COrdersCollection : public CArrayObj
  {
private:
   COrder            *Temp;
   string            ar_Symbols[];
   
public:

                     COrdersCollection();
                    ~COrdersCollection();
//--- Inicialização
   bool              Create(void);
//--- Adicionando a ordem
   bool              Add(COrder *element);
//--- Acesso a dados
   int               Symbols(string &array[]);
   bool              GetPosition(const string symbol, const datetime time, double &volume, double &price, ENUM_POSITION_TYPE &type);
   datetime          FirstOrder(const string symbol=NULL);
   datetime          LastOrder(const string symbol=NULL);
//--- Obtendo a série temporal
   bool              GetTimeSeries(const string symbol, const datetime start_time, const datetime end_time, const int direct,
                                   double &balance[], double &equity[], double &time[], double &profit, double &loss,int &long_trades, int &short_trades);
//---
   void              SetDealsEntry(void);
  };

A função Create é responsável pela coleta de dados. No corpo do método, organize um ciclo para pesquisa detalhada de todos os gráficos abertos no terminal. Em cada gráfico, procure objetos gráficos do tipo OBJ_ARROW_BUY e OBJ_ARROW_SELL.

bool COrdersCollection::Create(void)
  {
   long chart=ChartFirst();
   while(chart>0)
     {
      int total_buy=ObjectsTotal(chart,0,OBJ_ARROW_BUY);
      int total_sell=ObjectsTotal(chart,0,OBJ_ARROW_SELL);
      if((total_buy+total_sell)<=0)
        {
         chart=ChartNext(chart);
         continue;
        }

Se o objeto estiver localizado no gráfico, adicione o símbolo do gráfico à nossa matriz de instrumentos (mas verifique primeiro se já existe um desses instrumentos entre os que já foram salvos).

      int symb=ArraySize(ar_Symbols);
      string symbol=ChartSymbol(chart);
      bool found=false;
      for(int i=0;(i<symb && !found);i++)
         if(ar_Symbols[i]==symbol)
           {
            found=true;
            symb=i;
            break;
           }
      if(!found)
        {
         if(ArrayResize(ar_Symbols,symb+1,10)<=0)
            return false;
         ar_Symbols[symb]=symbol;
        }

Em seguida, organize a coleta de informações sobre trades a partir do gráfico na matriz de dados. Atenção: a única fonte de informação sobre o trade que temos é um objeto gráfico. A partir dos parâmetros do objeto, podemos obter apenas o tempo e o preço do trade. Você precisa extrair todo o resto das informações, usando o nome do objeto, ele é uma cadeia de texto.

Nome do objeto gráfico

A figura mostra que o nome do objeto possui todas as informações sobre o trade separadas por espaços. Use essa observação e dividia a string em espaços para a matriz de elementos embutidos. Em seguida, traga as informações do elemento correspondente para o tipo de dados desejado e salve. Após coletar as informações, prossiga para o próximo gráfico.

      int total=fmax(total_buy,total_sell);
      for(int i=0;i<total;i++)
        {
         if(i<total_buy)
           {
            string name=ObjectName(chart,i,0,OBJ_ARROW_BUY);
            datetime time=(datetime)ObjectGetInteger(chart,name,OBJPROP_TIME);
            StringTrimLeft(name);
            StringTrimRight(name);
            StringReplace(name,"#","");
            string split[];
            StringSplit(name,' ',split);
            Temp=new COrder;
            if(CheckPointer(Temp)!=POINTER_INVALID)
              {
               if(Temp.Create(ar_Symbols[symb],StringToInteger(split[1]),StringToDouble(split[3]),StringToDouble(split[6]),time,POSITION_TYPE_BUY))
                  Add(Temp);
              }
           }
//---
         if(i<total_sell)
           {
            string name=ObjectName(chart,i,0,OBJ_ARROW_SELL);
            datetime time=(datetime)ObjectGetInteger(chart,name,OBJPROP_TIME);
            StringTrimLeft(name);
            StringTrimRight(name);
            StringReplace(name,"#","");
            string split[];
            StringSplit(name,' ',split);
            Temp=new COrder;
            if(CheckPointer(Temp)!=POINTER_INVALID)
              {
               if(Temp.Create(ar_Symbols[symb],StringToInteger(split[1]),StringToDouble(split[3]),StringToDouble(split[6]),time,POSITION_TYPE_SELL))
                  Add(Temp);
              }
           }
        }
      chart=ChartNext(chart);
     }

Nos rótulos gráficos, não há informações sobre se é realizada a entrada/saída em/de uma posição para cada trade. Logo, até agora, ao salvar informações sobre os trades, esse campo era deixado em branco. Agora, após coletar todos os rótulos do gráfico, preencha as informações ausentes chamando a função SetDealsEntry.

   SetDealsEntry();
//---
   return true;
  }

Para evitar a duplicação de trades em nosso banco de dados, reescreva a função Add: adicione uma verificação de disponibilidade de ordem de acordo com a boleta para ela.

bool COrdersCollection::Add(COrder *element)
  {
   for(int i=0;i<m_data_total;i++)
     {
      Temp=m_data[i];
      if(Temp.Ticket()==element.Ticket())
         return true;
     }
//---
   return CArrayObj::Add(element);
  }

Para colocar os tipos de operações nos trades, crie a função SetDealsEntry. No seu começo, chame a função de classificação da classe base. Em seguida, organize um ciclo para a busca de todos os instrumentos e trades para cada um deles. O algoritmo para determinar o tipo de operação é simples. Se, no momento da operação, não houver uma posição aberta ou se ela estiver na mesma direção do trade, definiremos a operação como uma entrada na posição. Se a operação é oposta à posição existente, seu volume é usado primeiro para fechar a posição aberta, e o restante abre uma nova posição (similar ao sistema de compensação do MetaTrader 5).

COrdersCollection::SetDealsEntry(void)
  {
   Sort(0);
//---
   int symbols=ArraySize(ar_Symbols);
   for(int symb=0;symb<symbols;symb++)
     {
      double volume=0;
      ENUM_POSITION_TYPE type=-1;
      for(int ord=0;ord<m_data_total;ord++)
        {
         Temp=m_data[ord];
         if(Temp.Symbol()!=ar_Symbols[symb])
            continue;
//---
         if(volume==0 || type==Temp.Type())
           {
            Temp.DealEntry(DEAL_ENTRY_IN);
            volume=NormalizeDouble(volume+Temp.Volume(),2);
            type=Temp.Type();
           }
         else
           {
            if(volume>=Temp.Volume())
              {
               Temp.DealEntry(DEAL_ENTRY_OUT);
               volume=NormalizeDouble(volume-Temp.Volume(),2);
              }
            else
              {
               Temp.DealEntry(DEAL_ENTRY_INOUT);
               volume=NormalizeDouble(volume-Temp.Volume(),2);
               type=Temp.Type();
              }
           }
        }
     }
  }

2.3. Criando séries temporais de saldo e de fundos para cada instrumento

A fim de construir subsequentemente gráficos de saldo e de fundos para cada instrumento, você precisará criar séries temporais com o cálculo destes parâmetros ao longo do período analisado. Na análise, é bom que possamos mudar o período analisado. Isso permitirá que você estude o trabalho do sinal em intervalos de tempo limitados.

Você calculará a série temporal na função GetTimeSeries. Em seus parâmetros, indique o instrumento, a hora do início e do final do período analisado, bem como a direção do trade, a fim de acompanhar posições longas e curtas. A função retornará três séries temporais: saldo, fundos e rótulos de hora. Além disso, retornará estatísticas sobre o instrumento para o período analisado: lucro, perda, número de trades longos e curtos.

Olhando para o futuro, vou focar sua atenção no fato de que a matriz para as séries temporais de rótulos é definida como double. Esse pequeno truque é uma medida desesperada. Em seguida, os gráficos de saldo e fundos serão construídas usando a classe CGraphic padrão que aceita apenas matrizes do tipo double.

No início da função, redefina as variáveis para coletar estatísticas, verifique se o símbolo especificado está certo e obtenha o custo de um ponto de alteração de preço.

bool COrdersCollection::GetTimeSeries(const string symbol,const datetime start_time,const datetime end_time,const int direct,double &balance[],double &equity[], double &time[], double &profit, double &loss,int &long_trades, int &short_trades)
  {
   profit=loss=0;
   long_trades=short_trades=0;
//---
   if(symbol==NULL)
      return false;
//---
   double tick_value=SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_VALUE)/SymbolInfoDouble(symbol,SYMBOL_POINT);
   if(tick_value==0)
      return false;

Para a construção de séries temporais, use as cotações do instrumento a partir do timeframe M5, portanto, elas devem ser carregadas. Atenção: as cotações solicitadas podem não ter sido formadas ainda. E aqui está outro truque: não vamos ficar confusos com as operações e esperar que os dados terminem de carregar, pois isso interromperá completamente a execução do programa e, se usado em indicadores, poderá atrasar o terminal. Após a primeira chamada mal-sucedida, saia da função, mas, antes disso, crie um evento personalizado que posteriormente chamará novamente a função de atualização de dados.

   ENUM_TIMEFRAMES timeframe=PERIOD_M5;
//---
   double volume=0;
   double price=0;
   ENUM_POSITION_TYPE type=-1;
   int order=-1;
//---
   MqlRates rates[];
   int count=0;
   count=CopyRates(symbol,timeframe,start_time,end_time,rates);
   if(count<=0 && !ReloadHistory)
     {
      //--- send notification
      ReloadHistory=EventChartCustom(CONTROLS_SELF_MESSAGE,1222,0,0.0,symbol);
      return false;
     }

Após o carregamento das cotações, forneça o tamanho das matrizes das séries temporais de acordo com o tamanho das cotações carregadas.

   if(ArrayResize(balance,count)<count || ArrayResize(equity,count)<count || ArrayResize(time,count)<count)
      return false;
   ArrayInitialize(balance,0);

Em seguida, organize um ciclo para coletar informações para séries temporais. Em cada barra, defina as operações concluídas. Se esta for uma operação de abertura de posição, aumente o volume da posição atual e recalcule o preço médio de abertura. Se esta for uma operação de fechamento de posição, calcule o lucro/perda da operação, adicione o valor resultante à alteração de saldo na barra atual e reduza o volume da posição atual. Então, para o volume da posição não fechada no tempo de fechamento da barra, calcule o lucro/perda descoberto e guarde o valor obtido na mudança de fundos na barra analisada. Após pesquisar todo o histórico, saia da função.

   do
     {
      order++;
      if(order<m_data_total)
         Temp=m_data[order];
      else
         Temp=NULL;
     }
   while(CheckPointer(Temp)==POINTER_INVALID && order<m_data_total);
//---
   for(int i=0;i<count;i++)
     {
      while(order<m_data_total && Temp.Time()<(rates[i].time+PeriodSeconds(timeframe)))
        {
         if(Temp.Symbol()!=symbol)
           {
            do
              {
               order++;
               if(order<m_data_total)
                  Temp=m_data[order];
               else
                  Temp=NULL;
              }
            while(CheckPointer(Temp)==POINTER_INVALID && order<m_data_total);
            continue;
           }
//---
         if(Temp!=NULL)
           {
            if(type==Temp.Type())
              {
               price=volume*price+Temp.Volume()*Temp.Price();
               volume+=Temp.Volume();
               price=price/volume;
               switch(type)
                 {
                  case POSITION_TYPE_BUY:
                    long_trades++;
                    break;
                  case POSITION_TYPE_SELL:
                    short_trades++;
                    break;
                 }
              } 
            else
              {
               if(i>0 && (direct<0 || direct==type))
                 {
                  double temp=(Temp.Price()-price)*tick_value*(type==POSITION_TYPE_BUY ? 1 : -1)*MathMin(volume,Temp.Volume());
                  balance[i]+=temp;
                  if(temp>=0)
                     profit+=temp;
                  else
                     loss+=temp;
                 }
               volume-=Temp.Volume();
               if(volume<0)
                 {
                  volume=MathAbs(volume);
                  price=Temp.Price();
                  type=Temp.Type();
                  switch(type)
                    {
                     case POSITION_TYPE_BUY:
                       long_trades++;
                       break;
                     case POSITION_TYPE_SELL:
                       short_trades++;
                       break;
                    }
                 }
              }
           }
         do
           {
            order++;
            if(order<m_data_total)
               Temp=m_data[order];
            else
               Temp=NULL;
           }
         while(CheckPointer(Temp)==POINTER_INVALID && order<m_data_total);
        }
      if(i>0)
        {
         balance[i]+=balance[i-1];
        }
      if(volume>0 && (direct<0 || direct==type))
         equity[i]=(rates[i].close-price)*tick_value*(type==POSITION_TYPE_BUY ? 1 : -1)*MathMin(volume,(Temp!=NULL ? Temp.Volume(): DBL_MAX));
      else
         equity[i]=0;
      equity[i]+=balance[i];
      time[i]=(double)rates[i].time;
     }
//---
   return true;
  }

O código completo das classes pode ser encontrado no anexo.
 

3. Adicionando uma interface gráfica

A interface gráfica do programa conterá as datas de início e término da análise, caixas de seleção para escolher as informações exibidas no gráfico, bloco de estatísticas e os próprios gráficos.

Interface gráfica

Construa a interface gráfica na classe CStatisticsPanel (este é um sucessor da classe CAppDialog). Para selecionar as datas de início e término da análise, use instâncias da classe CDatePicker. As caixas de seleção para escolher as informações exibidas são agrupadas em 3 grupos:

  • Saldo e Fundos;
  • Posições longas e curtas;
  • Lista de instrumentos a serem analisados.

3.1. Criando um painel gráfico

Para criar blocos de caixas de seleção, use instâncias da classe CCheckGroup. Exiba as estatísticas de texto usando instâncias da classe CLabel. Construa os gráficos usando uma instância da classe CGraphic. E, claro, para acessar nossas estatísticas de ordens, declare uma instância da classe COrdersCollection.

class CStatisticsPanel : public CAppDialog
  {
private:
   CDatePicker       StartDate;
   CDatePicker       EndDate;
   CLabel            Date;
   CGraphic          Graphic;
   CLabel            ShowLabel;
   CCheckGroup       Symbols;
   CCheckGroup       BalEquit;
   CCheckGroup       Deals;
   string            ar_Symbols[];
   CLabel            TotalProfit;
   CLabel            TotalProfitVal;
   CLabel            GrossProfit;
   CLabel            GrossProfitVal;
   CLabel            GrossLoss;
   CLabel            GrossLossVal;
   CLabel            TotalTrades;
   CLabel            TotalTradesVal;
   CLabel            LongTrades;
   CLabel            LongTradesVal;
   CLabel            ShortTrades;
   CLabel            ShortTradesVal;
   //---
   COrdersCollection Orders;

public:
                     CStatisticsPanel();
                    ~CStatisticsPanel();
   //--- main application dialog creation and destroy
   virtual bool      Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2);
   virtual void      Destroy(const int reason=REASON_PROGRAM);
   //--- chart event handler
   virtual bool      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);

protected:
   virtual bool      CreateLineSelector(const string name,const int x1,const int y1,const int x2,const int y2);
   virtual bool      CreateDealsSelector(const string name,const int x1,const int y1,const int x2,const int y2);
   virtual bool      CreateCheckGroup(const string name,const int x1,const int y1,const int x2,const int y2);
   virtual bool      CreateGraphic(const string name,const int x1,const int y1,const int x2,const int y2);
   //---
   virtual void      Maximize(void);
   virtual void      Minimize(void);
   //---
   virtual bool      UpdateChart(void);

  };

No método Create, primeiro, chame o método correspondente da classe pai e, em seguida, organize todos os objetos em seus locais e inicialize a instância da classe de coleção de ordens. Após inicializar cada elemento, lembre-se de atribuir os valores iniciais e adicionar o objeto à coleção de elementos de controle. Nos artigos [2] e [3], são descritos detalhes sobre como trabalhar com a classe base, por isso não vou me debruçar sobre a descrição do método, vou dar apenas seu código.

bool CStatisticsPanel::Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2)
  {
   if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
      return false;
//---
   if(!TotalProfit.Create(m_chart_id,m_name+"Total Profit",m_subwin,5,80,115,95))
      return false;
   if(!TotalProfit.Text("Total Profit"))
      return false;
   if(!Add(TotalProfit))
      return false;
//---
   if(!TotalProfitVal.Create(m_chart_id,m_name+"Total Profit Value",m_subwin,135,80,250,95))
      return false;
   if(!TotalProfitVal.Text("0"))
      return false;
   if(!Add(TotalProfitVal))
      return false;
//---
   if(!GrossProfit.Create(m_chart_id,m_name+"Gross Profit",m_subwin,5,100,115,115))
      return false;
   if(!GrossProfit.Text("Gross Profit"))
      return false;
   if(!Add(GrossProfit))
      return false;
//---
   if(!GrossProfitVal.Create(m_chart_id,m_name+"Gross Profit Value",m_subwin,135,100,250,115))
      return false;
   if(!GrossProfitVal.Text("0"))
      return false;
   if(!Add(GrossProfitVal))
      return false;
//---
   if(!GrossLoss.Create(m_chart_id,m_name+"Gross Loss",m_subwin,5,120,115,135))
      return false;
   if(!GrossLoss.Text("Gross Loss"))
      return false;
   if(!Add(GrossLoss))
      return false;
//---
   if(!GrossLossVal.Create(m_chart_id,m_name+"Gross Loss Value",m_subwin,135,120,250,135))
      return false;
   if(!GrossLossVal.Text("0"))
      return false;
   if(!Add(GrossLossVal))
      return false;
//---
   if(!TotalTrades.Create(m_chart_id,m_name+"Total Trades",m_subwin,5,150,115,165))
      return false;
   if(!TotalTrades.Text("Total Trades"))
      return false;
   if(!Add(TotalTrades))
      return false;
//---
   if(!TotalTradesVal.Create(m_chart_id,m_name+"Total Trades Value",m_subwin,135,150,250,165))
      return false;
   if(!TotalTradesVal.Text("0"))
      return false;
   if(!Add(TotalTradesVal))
      return false;
//---
   if(!LongTrades.Create(m_chart_id,m_name+"Long Trades",m_subwin,5,170,115,185))
      return false;
   if(!LongTrades.Text("Long Trades"))
      return false;
   if(!Add(LongTrades))
      return false;
//---
   if(!LongTradesVal.Create(m_chart_id,m_name+"Long Trades Value",m_subwin,135,170,250,185))
      return false;
   if(!LongTradesVal.Text("0"))
      return false;
   if(!Add(LongTradesVal))
      return false;
//---
   if(!ShortTrades.Create(m_chart_id,m_name+"Short Trades",m_subwin,5,190,115,215))
      return false;
   if(!ShortTrades.Text("Short Trades"))
      return false;
   if(!Add(ShortTrades))
      return false;
//---
   if(!ShortTradesVal.Create(m_chart_id,m_name+"Short Trades Value",m_subwin,135,190,250,215))
      return false;
   if(!ShortTradesVal.Text("0"))
      return false;
   if(!Add(ShortTradesVal))
      return false;
//---
   if(!Orders.Create())
      return false;
//---
   if(!ShowLabel.Create(m_chart_id,m_name+"Show Selector",m_subwin,285,8,360,28))
      return false;
   if(!ShowLabel.Text("Symbols"))
      return false;
   if(!Add(ShowLabel))
      return false;
   if(!CreateLineSelector("LineSelector",2,30,115,70))
      return false;
   if(!CreateDealsSelector("DealsSelector",135,30,250,70))
      return false;
   if(!CreateCheckGroup("CheckGroup",260,30,360,ClientAreaHeight()-5))
      return false;
//---
   if(!Date.Create(m_chart_id,m_name+"->",m_subwin,118,8,133,28))
      return false;
   if(!Date.Text("->"))
      return false;
   if(!Add(Date))
      return false;
//---
   if(!StartDate.Create(m_chart_id,m_name+"StartDate",m_subwin,5,5,115,28))
      return false;
   if(!Add(StartDate))
      return false;
//---
   if(!EndDate.Create(m_chart_id,m_name+"EndDate",m_subwin,135,5,250,28))
      return false;
   if(!Add(EndDate))
      return false;
//---
   StartDate.Value(Orders.FirstOrder());
   EndDate.Value(Orders.LastOrder());
//---
   if(!CreateGraphic("Chraphic",370,5,ClientAreaWidth()-5,ClientAreaHeight()-5))
      return false;
//---
   UpdateChart();
//---
   return true;
  }

O leitor mais observador poderá perceber que o gráfico que está sendo criado não é adicionado à coleção de elementos de controle. Isso se deve ao fato de que o objeto CGraphic não é herdado da classe CWnd, e somente os objetos de herança CWnd podem ser adicionados à coleção. Portanto, você tem que reescrever as funções para maximizar e minimizar o painel.
Após a inicialização de todos os objetos, chame a função para atualizar o gráfico.

3.2. Função para criar gráficos

Debrucemo-nos brevemente sobre a função de criação de gráficos CreateGraphic. Nos parâmetros, ela obtém o nome do objeto a ser criado e as coordenas de localização do gráfico. No início da função, é criado diretamente um gráfico (chamada da função Create da classe CGraphic). Como a classe CGraphic não é herdada da classe CWnd e não pode ser incluída na coleção de elementos de controle do painel, desloque imediatamente as coordenadas do gráfico, de acordo com a localização da área de cliente.

bool CStatisticsPanel::CreateGraphic(const string name,const int x1,const int y1,const int x2,const int y2)
  {
   if(!Graphic.Create(m_chart_id,m_name+name,m_subwin,ClientAreaLeft()+x1,ClientAreaTop()+y1,ClientAreaLeft()+x2,ClientAreaTop()+y2))
      return false;

Em seguida, você precisa criar instâncias da classe CCurve para cada curva exibida no gráfico. Para fazer isso, primeiro obtenha a lista de instrumentos usados a partir de uma instância da classe COrdersCollection. Logo, no ciclo, crie as curvas do saldo e dos fundos para cada instrumento, inicializando-as com uma matriz vazia. Após a criação, oculte as linhas do gráfico até que os dados sejam recebidos.

   int total=Orders.Symbols(ar_Symbols);
   CColorGenerator ColorGenerator;
   double array[];
   ArrayFree(array);
   for(int i=0;i<total;i++)
     {
      //---
      CCurve *curve=Graphic.CurveAdd(array,array,ColorGenerator.Next(),CURVE_LINES,ar_Symbols[i]+" Balance");
      curve.Visible(false);
      curve=Graphic.CurveAdd(array,array,ColorGenerator.Next(),CURVE_LINES,ar_Symbols[i]+" Equity");
      curve.Visible(false);
     }

Após criar as curvas, desabilite o dimensionamento automático da escala das abscissas e indique para ela a propriedade de exibição na forma de datas. Além disso, indique o tamanho do texto de legenda das curvas e exiba o gráfico na tela.

   CAxis *axis=Graphic.XAxis();
   axis.AutoScale(false);
   axis.Type(AXIS_TYPE_DATETIME);
   axis.ValuesDateTimeMode(TIME_DATE);
   Graphic.HistorySymbolSize(20);
   Graphic.HistoryNameSize(10);
   Graphic.HistoryNameWidth(60);
   Graphic.CurvePlotAll();
   Graphic.Update();
//---
   return true;
  }

3.3. Método de atualização de gráfico e de dados estatísticos

Atualize os dados do sinal usando o método UpdateChart. No início da função, prepare as variáveis ​​e matrizes para a coleta de dados.

bool CStatisticsPanel::UpdateChart(void)
  {
   double balance[];
   double equity[];
   double time[];
   double total_profit=0, total_loss=0;
   int total_long=0, total_short=0;
   CCurve *Balance, *Equity;

Em seguida, obtenha as datas de início e de término do período analisado.

   datetime start=StartDate.Value();
   datetime end=EndDate.Value();

Verifique as marcas para mostrar estatísticas de posições longas e curtas.

   int deals=-2;
   if(Deals.Check(0))
      deals=(Deals.Check(1) ? -1 : POSITION_TYPE_BUY);
   else
      deals=(Deals.Check(1) ? POSITION_TYPE_SELL : -2);

Após preparar os dados iniciais no ciclo de cada instrumento, atualize as séries temporais, chamando a função GetTimeSeries que já conhecemos. Antes de chamar o método, verifique a marca de seleção na caixa de seleção do símbolo correspondente. Se não existir, o método não será chamado e as curvas serão ocultas. Após o recebimento bem-sucedido das séries temporais, atualize os dados para as curvas de saldo e de fundos, verifique as marcas nas caixas de seleção correspondentes. Se não houver marcas, a curva fica oculta do gráfico.

   int total=ArraySize(ar_Symbols);
   for(int i=0;i<total;i++)
     {
      Balance  =  Graphic.CurveGetByIndex(i*2);
      Equity   =  Graphic.CurveGetByIndex(i*2+1);
      double profit,loss;
      int long_trades, short_trades;
      if(deals>-2 && Symbols.Check(i) && Orders.GetTimeSeries(ar_Symbols[i],start,end,deals,balance,equity,time,profit,loss,long_trades,short_trades))
        {
         if(BalEquit.Check(0))
           {
            Balance.Update(time,balance);
            Balance.Visible(true);
           }
         else
            Balance.Visible(false);
         if(BalEquit.Check(1))
           {
            Equity.Update(time,equity);
            Equity.Visible(true);
           }
         else
            Equity.Visible(false);
         total_profit+=profit;
         total_loss+=loss;
         total_long+=long_trades;
         total_short+=short_trades;
        }
      else
        {
         Balance.Visible(false);
         Equity.Visible(false);
        }
     }

O próximo passo é indicar a data de início e de término do período analisado para o gráfico, bem como o espaçamento da grade. Atualize o gráfico. 

   CAxis *axis=Graphic.XAxis();
   axis.Min((double)start);
   axis.Max((double)end);
   axis.DefaultStep((end-start)/5);
   if(!Graphic.Redraw(true))
      return false;
   Graphic.Update();

Na conclusão do método, atualize as informações nos rótulos de texto para exibir as estatísticas do sinal.

   if(!TotalProfitVal.Text(DoubleToString(total_profit+total_loss,2)))
      return false;
   if(!GrossProfitVal.Text(DoubleToString(total_profit,2)))
      return false;
   if(!GrossLossVal.Text(DoubleToString(total_loss,2)))
      return false;
   if(!TotalTradesVal.Text(IntegerToString(total_long+total_short)))
      return false;
   if(!LongTradesVal.Text(IntegerToString(total_long)))
      return false;
   if(!ShortTradesVal.Text(IntegerToString(total_short)))
      return false;
//---
   return true;
  }

3.4. Dando vida ao painel

Para dar vida ao painel, você precisa criar um manipulador de eventos para ações com objetos. Quais são os possíveis eventos que o programa deve processar?

Antes de mais nada, trata-se tanto da mudança na data de início ou de término do período analisado, quanto da mudança no estado das caixas de seleção que são controladas pela coleta de estatísticas e pela exibição das curvas de saldo e de fundos. Não se esqueça do nosso truque, isto é, temos que processar um evento personalizado criado quando é impossível carregar o histórico de cotações de um dos instrumentos analisados. Quando qualquer um desses eventos ocorrer, basta chamar o método de atualização de dados UpdateChart. O método de manipulação de eventos acabará ficando assim:

EVENT_MAP_BEGIN(CStatisticsPanel)
   ON_EVENT(ON_CHANGE,Symbols,UpdateChart)
   ON_EVENT(ON_CHANGE,BalEquit,UpdateChart)
   ON_EVENT(ON_CHANGE,Deals,UpdateChart)
   ON_EVENT(ON_CHANGE,StartDate,UpdateChart)
   ON_EVENT(ON_CHANGE,EndDate,UpdateChart)
   ON_NO_ID_EVENT(1222,UpdateChart)
EVENT_MAP_END(CAppDialog)

Além desses métodos, alteramos os métodos para maximizar/minimizar o painel, uma vez que se adicionaram a eles a função de ocultar e exibir o gráfico. O código completo da classe e dos métodos pode ser encontrado no anexo.

4. Criando um indicador para análise de sinal

Proponho combinar na forma de um indicador todo o descrito acima. Isso permitirá criar um painel gráfico, sem afetar o gráfico em si.

Toda a funcionalidade do nosso programa está escondida na classe CStatisticsPanel. O que significa que, para criar um indicador, basta criar uma instância dessa classe em nosso programa. Inicialize a classe na função OnInit.

int OnInit()
  {
//---
   long chart=ChartID();
   int subwin=ChartWindowFind();
   IndicatorSetString(INDICATOR_SHORTNAME,"Signal Statistics");
   ReloadHistory=false;
//---
   Dialog=new CStatisticsPanel;
   if(CheckPointer(Dialog)==POINTER_INVALID)
     {
      ChartIndicatorDelete(chart,subwin,"Signal Statistics");
      return INIT_FAILED;
     }
   if(!Dialog.Create(chart,"Signal Statistics",subwin,0,0,0,250))
     {
      ChartIndicatorDelete(chart,subwin,"Signal Statistics");
      return INIT_FAILED;
     }
   if(!Dialog.Run())
     {
      ChartIndicatorDelete(chart,subwin,"Signal Statistics");
      return INIT_FAILED;
     }
//---
   return(INIT_SUCCEEDED);
  }

 A função OnCalculate é deixada vazia, porque o programa não reagirá ao início de outro tick. Resta apenas adicionar uma chamada aos métodos correspondentes nas funções OnDeinit e OnChartEvent.

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   Dialog.ChartEvent(id,lparam,dparam,sparam);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   Dialog.Destroy(reason);
   delete Dialog;
  }

Tendo compilado o indicador, será suficiente carregar as estatísticas do sinal selecionado nos gráficos do terminal e anexar nosso indicador a um dos gráficos. Agora você pode estudar e analisar os trades. Existe uma nuance e é que, em nosso programa, não filtramos os gráficos para análise. Por isso, o indicador coletará estatísticas de todos os gráficos abertos no terminal. Para evitar misturar trades do indicador com outros trades no terminal, recomendo fechar todos os gráficos antes de carregar o histórico de trades do sinal.

Exemplo de trabalho de indicador

O código completo do programa está disponível no anexo.

Fim do artigo

Construímos um indicador que analisa os trades de acordo com os rótulos nos gráficos. Essa abordagem pode ser útil para diferentes propósitos, por exemplo, ao escolher um sinal ou ao otimizar sua própria estratégia. Por exemplo, isso permitirá determinar os instrumentos para os quais nossa estratégia não funciona e, no futuro, não usá-lo nesses símbolos.

Links

  1. Seleção automática de sinais promissores
  2. Como criar um painel gráfico de qualquer complexidade
  3. Melhoramos o trabalho com Painéis, adicionando transparência, alterando a cor do plano de fundo e herdando da CAppDialog/CWndClient

Programas utilizados no artigo:

#
 Nome
Tipo 
Descrição 
1 Order.mqh  Biblioteca de classes  Classe para salvar informações sobre o trade
2 OrdersCollection.mqh  Biblioteca de classes  Classe de coleção de trades
3 StatisticsPanel.mqh  Biblioteca de classes  Classe da interface gráfica do usuário
4 SignalStatistics.mq5  Indicador  Código do indicador para a análise de trades