
Como analisar os trades do Sinal selecionado no gráfico
Sumário
- Introdução
- 1. Formulando os objetivos do trabalho à frente
- 2. Fazendo estatísticas de trades
- 2.1. Classe para armazenar informações sobre a ordem
- 2.2. Coletando informações a partir de gráficos
- 2.3. Criando séries temporais de saldo e de fundos para cada instrumento
- 3. Adicionando uma interface gráfica
- 3.1. Criando um painel gráfico
- 3.2. Função para criar gráficos
- 3.3. Método de atualização de gráfico e de dados estatísticos
- 3.4. Dando vida ao painel
- 4. Criando um indicador para análise de sinal
- Fim do artigo
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.
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.
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.
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.
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
- Seleção automática de sinais promissores
- Como criar um painel gráfico de qualquer complexidade
- 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 |
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/4751





- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso