Cómo analizar las transacciones de la Señal elegida en el gráfico

11 agosto 2018, 07:43
Dmitriy Gizlyk
0
3 650

Contenido

Introducción

Al servicio Señales se añaden constantemente nuevas señales, tanto de pago como gratuitas. El equipo de MetaTrader se ha ocupado de que el servicio se pueda utilizar sin salir del terminal. Solo queda elegir precisamente la señal que nos traerá el beneficio máximo con unos riesgos asumibles. Este problema se viene discutiendo desde hace mucho. Ya se ha propuesto un método de selección automática de señales según unos criterios establecidos [1]. Pero, como dice el refrán popular: es mejor verlo uno vez que oír hablar de ello cien veces. En el artículo proponemos estudiar y analizar la historia de transacciones de la señal elegida en el gráfico del instrumento. Es posible que este enfoque nos permita comprender mejor la estrategia de ejecución de las transacciones, y también valorar los riesgos. 

1. Formulando los objetivos de nuestra tarea

Usted se preguntará: ¿para qué "reinventar la rueda", cuando en el terminal ya existe la posibilidad de representar la historia de transacciones en el gráfico? Y es que basta con seleccionar la señal que nos interese y pulsar un botón en el terminal.

Comando "Mostrar transacciones en el gráfico"

A continuación, en el terminal se abrirán nuevas ventanas según el número de instrumentos usados por la señal, con etiquetas de transacciones finalizadas. Está claro que resulta muy engorroso pasar de un gráfico a otro buscando las transacciones en estos. Además, las transacciones en diferentes gráficos pueden coincidir en cuanto al tiempo, y esto se ve al analizar cada gráfico por separado. Precisamente en esta etapa vamos a intentar automatizar parte del trabajo.

Para definir qué instrumento necesitamos al analizar los gráficos obtenidos, deberemos entender claramente qué datos finales necesitamos. Aquí tenemos los puntos principales de lo que queremos conseguir:

  • ver cómo de estable es el funcionamiento de la señal en diferentes instrumentos;
  • cómo se distribuye la carga sobre el depósito, cuántas posiciones puede haber abiertas simultáneamente;
  • ver si con una señal se abren varias posiciones al mismo tiempo, si son estas de cobertura o aumentan la carga sobre el depósito;
  • en qué momentos y en qué instrumentos surgen las pérdidas máximas;
  • en qué momentos se alcanza el beneficio máximo.

2. Reuniendo la estadística de transacciones

2.1. Clase para el guardado de información sobre las órdenes

Elegimos la señal que nos interese y mostramos su historia de transacciones en el gráfico. Reunimos los datos fuente, que analizaremos más tarde. Para cada entrada de información de cada orden concreta, creamos la clase COrder basada en la clase CObject. En las variables de esta clase guardamos el ticket de la orden, el volumen y el tipo de transacción, el precio de la transacción, el tipo de operación  (entrada / salida), la hora de apertura de la orden y, por supuesto, el 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;
  };

Aparte de las funciones de acceso a los datos, añadimos a la clase de órdenes las funciones para trabajar con los archivos y la posterior lectura de datos, así como la función de comparación con un objeto semejante, que necesitaremos para la clasifición de órdenes.

Para comparar dos órdenes, necesitaremos reescribir la función virtual Compare. Se trata de una función de la clase básica, pensada para comparar dos objetos CObject. Por eso, en sus parámetros se transmite el enlace al objeto CObject y el método de clasificación. Clasificaremos nuestras órdenes solo en una dirección (de forma ascendente, según la fecha de ejecución), por eso no vamos a usar el parámetro "mode" en el código de la función. Pero para trabajar con el objeto COrder obtenido con el enlace, necesitaremos transformarlo en el tipo correspondiente. A continuación, compararemos la fecha de la orden obtenida y la actual. Si la orden obtenida es más antigua, retornaremos "-1", si es menos antigua, retornaremos "1". Si las fechas de ejecución de las órdenes son iguales, la función 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. Reuniendo la información de los gráficos

Para trabajar con las órdenes, crearemos la clase COrdersCollection, basada en la clase CArrayObj. En ella se recopilará y procesará la información. Para guardar los datos, declararemos un ejemplar del objeto para trabajar directamente con una orden concreta y la matriz de guardado de la lista de instrumentos utilizados. Guardaremos la matriz de órdenes con la ayuda de las funciones de la clase básica.

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

                     COrdersCollection();
                    ~COrdersCollection();
//--- Inicialización
   bool              Create(void);
//--- Añadimos la orden
   bool              Add(COrder *element);
//--- Accediendo a los datos
   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);
//--- Obtenemos la serie 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);
  };

La función Create es responsable de la recopilación de datos. En el cuerpo del método organizamos un ciclo de iteración por todos lo gráficos abiertos en el terminal. En cada gráfico buscaremos los objetos gráficos del tipo OBJ_ARROW_BUY y 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;
        }

Si el objeto en el gráfico es localizado, añadimos el símbolo del gráfico a nuestra matriz de instrumentos (pero comprobamos preliminarmente si hay algún instrumento así entre los guardados).

      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;
        }

A continuación, organizamos la recopilación de información sobre las transacciones hacia la matriz de datos desde el gráfico. Preste atención: la única fuente de información sobre la transacción es el objeto gráfico. De los parámetros del objeto podemos obtener solo el tiempo y el precio de la transacción. El resto de la información deberemos extraerla de la denominación del objeto, que representa una línea de texto.

Denominación del objeto gráfico

En el dibujo se ve que en la denominación del objeto se reúne toda la información sobre la transacción, separada por espacios. Aprovecharemos esta observación y dividiremos la línea en espacios en una matriz de elementos de línea. A continuación, transformaremos desde el elemento correspondiente al tipo necesario de datos. Después de recopilar la información, pasaremos al siguiente 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);
     }

En las etiquetas gráficas no existe información sobre si se ejecuta la entrada en la posición o la salida de la misma según cada transacción. Por eso, hasta el momento, al guardar la información sobre las transacciones este campo se quedaba sin rellenar. Ahora, después de reunir todas las etiquetas del gráfico, rellenamos la información que falta, llamando a la función SetDealsEntry.

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

Para evitar la duplicación de transacciones en nuestra base, reescribiremos la función Add: para ello, le añadiremos la comprobación de la presencia de órdenes según el ticket.

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 distribuir los tipos de operación en las transacciones, crearemos SetDealsEntry. Al inicio, llamamos a la función de clasificación de la clase básica. A continuación, organizamos un ciclo de iteración de todos los intrumentos y transacciones según cada uno de ellos. El algoritmo de definición de cada uno de ellos es sencillo. Si en el momento de la operación no existe una posición abierta o esta se encuentra en la misma dirección que la transacción, esto significará que definimos la operación como entrada en la posición. Si la operación es opuesta a la posición existente, su volumen primero se usará para cerrar la posición abierta, y el resto del mismo abrirá una nueva posición (por analogía con el sistema de compensación de 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. Creando las series temporales del balance y fondos de cada instrumento

Para construir en consecuencia los gráficos de balance y fondos de cada instrumento, necesitaremos crear series temporales calculando estos parámetros durante todo el periodo analizado. Al realizar el análisis, sería deseable que tuviésemos la posibilidad de cambiar el periodo analizado. Esto permitirá estudiar el funcionamiento de la señal en intervalos temporales limitados.

Calcularemos las series temporales en la función GetTimeSeries. En sus parámetros indicaremos el instrumento, la hora de comienzo y finalización del periodo analizado, y también la dirección del comercio, para monitorear las posiciones largas y cortas. La función retornará tres series temporales: el balance, los fondos y las etiquetas de tiempo. Además, retornará la estadística del instrumento en el periodo analizado: el beneficio, las pérdidas y el número de posiciones largas y cortas.

Adelantándonos un poco, concentraremos nuestra atención en que la matriz para las series temporales de las etiquetas de tiempo ha sido definida como double. Se trata de un pequeño truco, una medida forzosa. A continuación, vamos a construir los gráficos de balance y fondos usando CGraphic, que adopta solo matrices del tipo double.

Al inicio de la función reseteamos las variables para la recopilación de estadísticas, comprobamos que el símbolo indicado sea correcto y obtenemos el coste de un punto de cambio de precio.

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 construir las series temporales, usaremos las cotizaciones del instrumento con un marco temporal M5, esto significa que hay que cargarlas. Pero, preste atención: las cotizaciones solicitadas pueden no haberse formado aún. Aquí nos encontramos con otro truco: no vamos a obsesionarnos con las operaciones y esperar a que los datos terminen de cargarse: esto detendrá por completo la ejecución del programa, lo que puede ralentizar el funcionamiento del terminal al usarlos en los indicadores. Después de la primera llamada sin éxito, saldremos de la función, pero antes de ello, crearemos un evento de usuario que más tarde llamará de nuevo a la función de actualización de datos.

   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;
     }

Después de cargar las cotizaciones, adaptaremos el tamaño de las matrices de las series temporales al tamaño de las cotizaciones cargadas.

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

A continuación, organizaremos un ciclo de recopilación de información para las series temporales. En cada barra definiremos las operaciones finalizadas. Si se trata de una operación de apertura de posición, aumentaremos el volumen de la posición actual y recalcularemos el precio medio de apertura. Si se trata de la operación de cierre de posición, calcularemos el beneficio/pérdidas obtenidos de la operación, sumaremos el valor resultante al cambio de balance en la barra actual y reduciremos el volumen de la posición actual. A continuación, en el volumen de la posición no cerrada durante el cierre de la barra, calcularemos el beneficio/pérdidas no cerrados y guardaremos el valor obtenido en el cambio de fondos en la barra analizada. Después de iterar toda la historia, saldremos de la función.

   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;
  }

Podrá familiarizarse con el código completo de las clases y los métodos en los anexos.
 

3. Añadiendo el envoltorio gráfico

La interfaz gráfica del programa contendrá la fecha de inicio y finalización del análisis, la casilla de verificación para elegir la información representada en el gráfico, el bloque de estadísticas y los propios gráficos.

Interfaz gráfica

Construiremos la interfaz gráfica en la clase CStatisticsPanel (es heredera de la clase CAppDialog). Para elegir las fechas de inicio y finalización del análisis, usaremos ejemplares de la clase CDatePicker. Las casillas de verificación para la selección de la información representada las reuniremos en 3 grupos:

  • Balance y fondos;
  • Posiciones cortas y largas;
  • Lista de instrumentos analizados.

3.1. Creando el panel gráfico

Para crear los bloques de casillas de verificación, usaremos ejemplares de la clase CCheckGroup. Las estadísticas de texto las mostraremos con la ayuda de ejemplares de la clase CLabel. Los gráficos, a su vez, los construiremos con la ayuda de un ejemplar de la clase CGraphic. Y, por supuesto, para acceder a nuestras estadísticas referentes a las órdenes, declararemos un ejemplar de la clase 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);

  };

En el método Create llamaremos en primer lugar al método correspondiente de la clase padre, y a continuación colocaremos todos los objetos en su lugar y analizaremos el ejemplar de clase de la colección de órdenes. Después de inicializar cada elemento, no debemos olvidar asignar los valores iniciales y añadir el objeto a la colección de elementos de control. Podrá leer información más detallada sobre la clase básica en los artículos [2] y [3], por eso no vamos detenernos en la descripción del método, mostraremos solo su 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;
  }

Un lector observador, podrá notar que el gráfico creado no se ha añadido a la colección de elementos de control. Esto se debe a que el objeto CGraphic no se hereda de la clase CWnd, mientras que a la colección solo se pueden añadir objetos-herederos de CWnd. Por eso, tendremos que reescribir las funciones para maximizar y minimizar el panel.
Después de inicializar todos los objetos, llamamos a la función de actualización del gráfico.

3.2. Función de creación del gráfico

Vamos a ver con mayor detenimiento la función de creación del gráfico CreateGraphic. En los parámetros recibe el nombre del objeto creado y las coordenadas de ubicación del gráfico. Al inicio de la función se crea directamente el gráfico (llamando a la función Create de la clase CGraphic). Puesto que la clase CGraphic no se hereda de la clase CWnd y no podemos añadirla a la colección de elementos de control del panel, desplazaremos directamente las coordenadas del gráfico de acuerdo con la zona 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;

A continuación es necesario crear ejemplares de la clase CCurve para cada curva representada en el gráfico. Para ello, primero obtendremos del ejemplar de la clase COrdersCollection la lista de instrumentos usados. A continuación, crearemos las curvas de balance y fondos para cada instrumento, inicializándolos con una matriz vacía. Después de crearlas, ocultaremos las líneas del gráfico hasta que obtengamos los datos.

   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);
     }

Después de crear las curvas, desactivaremos el autoescalado de la escala de las abscisas e indicaremos para ella la propiedad de representación en forma de fechas. Asimismo, indicaremos la representación de las firmas de las curvas y mostraremos el gráfico en la pantalla.

   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 actualización del gráfico y los datos estadísticos

Vamos a actualizar los datos sobre la señal con el método UpdateChart. Al inicio de la función preparamos las variables y las matrices para recopilar datos.

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;

A continuación, obtenemos la fecha de inicio y finalización del periodo analizado.

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

Comprobamos las etiquetas para mostrar las estadísticas de las posiciones largas y cortas.

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

Después de preparar los datos fuente, actualizamos las series temporales en el ciclo para cada instrumento, llamando a la función GetTimeSeries, que ya conocemos. Antes de llamar al método, comprobamos la marca en la casilla de verificación del símbolo correspondiente. Si no existe, el método no se llama, y las curvas se ocultarán. Después de obtener la serie tempopral con éxito, actualizamos los datos para las curvas de balance y fondos, y comprobamos de forma preliminar las marcas en las casillas de verificación correspondientes. Si no hay marcas, la curva se ocultará del 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);
        }
     }

El siguiente paso consiste en indicar para el gráfico las fechas de inicio y finalización del periodo analizado, así como el salto de la retícula. Actualizamos el 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();

En la finalización del método, actualizamos la información en las etiquetas de texto para mostrar las estadísticas de la señal.

   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. Cómo "dar vida" al panel

Para "dar vida" al panel, vamos a necesitar construir un procesador de eventos para acciones con objetos. ¿Qué posibles eventos tendrá que procesar el programa?

Ante todo, se trata de las fechas de inicio y finalización del periodo analizado y el cambio de estado de las casillas de verificación con las que se controla la recopilación de estadísticas y la muestra de las curvas de balance y fondos. No nos olvidemos de nuestro truco: tenemos que procesar el evento personalizado creado al darse la imposibilidad de cargar la historia de cotizaciones de uno de los instrumentos analizados. Cuando se da cualquiera de estos eventos, basta con llamar al método de actualización de datos UpdateChart. Al final, el método de procesamiento de eventos tendrá el aspecto siguiente:

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)

Aparte de los métodos indicados, hemos modificado también los métodos de maximización/minimización del panel: hemos añadido a estos la función para ocultar y mostrar el gráfico. Podrá familiarizarse con el código completo de la clase y los métodos en los anexos.

4. Creando un indicador para el análisis de la señal

Todo lo escrito anteriormente podemos combinarlo en forma de indicador. Esto nos permitirá crear el panel gráfico en una subventana sin tocar el propio gráfico.

La funcionalidad completa de nuestro programa se oculta en la clase CStatisticsPanel. Esto significa que para crear el indicador basta con crear un ejemplar de esta clase en nuestro programa. Inicializamos la clase en la función 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);
  }

 Dejamos vacía la función OnCalculate, puesto que el programa no podrá reaccionar a la llegada de otro tick. Solo nos queda añadir a las funciones OnDeinit y OnChartEvent la llamada a los métodos correspondientes.

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;
  }

Tras compilar el indicador, nos bastará con cargar en los gráficos del terminal las estadísticas de la señal elegida y fijar nuestro indicador a uno de los gráficos. Ahora podemos estudiar y analizar las transacciones. Hay un pequeño detalle a considerar: no hemos filtrado en nuestro programa los gráficos para el análisis. Por eso, el indicador reunirá la estadística de todos los gráficos abiertos en el terminal. Para evitar la mezcla de las transacciones del indicador con otras transacciones en el terminal, recomendamos cerrar todos los gráficos antes de cargar la historia de la señal.

Ejemplo de funcionamiento del indicador

Podrá familiarizarse con el código completo del programa en los anexos.

Conclusión

Hemos construido un indicador que analiza las transacciones según las etiquetas en los gráficos. Esta tecnología puede ser útil para diferentes objetivos: por ejemplo, al elegir una señal o al optimizar nuestra propia estrategia. Por ejemplo, nos permitirá determinar los instrumentos para los que nuestra estrategia no funciona, y así evitaremos utilizarla en el futuro con estos símbolos.

Enlaces

  1. Selección automática de señales prometedoras
  2. Cómo crear un panel gráfico de cualquier nivel de complejidad
  3. Mejorando el trabajo con paneles: cómo añadir transparencia, cambiar el color del fondo y heredar a partir de CAppDialog/CWndClient

Programas usados en el artículo:

#
 Nombre
Tipo 
Descripción 
1 Order.mqh  Biblioteca de clase  Clase para guardar la información sobre la transacción
2 OrdersCollection.mqh  Biblioteca de clase  Clase de colección de transacciones
3 StatisticsPanel.mqh  Biblioteca de clase  Clase de interfaz gráfica
4 SignalStatistics.mq5  Indicador  Código del indicador para el análisis de transacciones


Traducción del ruso hecha por MetaQuotes Software Corp.
Artículo original: https://www.mql5.com/ru/articles/4751

Archivos adjuntos |
MQL5.zip (470.5 KB)
Cómo transferir los cálculos de cualquier indicador al código de un asesor experto Cómo transferir los cálculos de cualquier indicador al código de un asesor experto

Las razones para transferir el código de un indicador a un asesor pueden ser muchas. ¿Cómo valorar las ventajas y desventajas de este enfoque? En este artículo ofrecemos una tecnología para transferir el código del indicador a un asesor. Asimismo, se han realizado varios experimentos para evaluar la velocidad de funcionamiento del asesor.

Mejorando el trabajo con Paneles: cómo añadir transparencia, cambiar el color del fondo y heredar a partir de CAppDialog/CWndClient Mejorando el trabajo con Paneles: cómo añadir transparencia, cambiar el color del fondo y heredar a partir de CAppDialog/CWndClient

Vamos a continuar estudiando el funcionamiento de CAppDialog. Ahora vamos a aprender cómo establecer el color de fondo, el borde y el encabezado para un panel gráfico. Veremos paso a paso cómo agregar transparencia a la ventana de la aplicación al desplazar esta por el gráfico. A continuación, analizaremos la creación de descendientes de CAppDialog o CWndClient y veremos nuevos detalles importantes al trabajar con los controles. Finalmente, echaremos un vistazo desde una nueva perspectiva a nuevos proyectos.

Trading social. ¿Es posible mejorar una señal rentable? Trading social. ¿Es posible mejorar una señal rentable?

La mayoría de los suscriptores eligen una señal comercial por la belleza de su curva de balance o según el número de suscriptores. Por eso, muchos proveedores de hoy se preocupan de que sus estadísticas sean bonitas, en lugar de prestar atención a la calidad real de la señal. A menudo juegan con los volúmenes de las transacciones y confieren artificialmente a la curva de balance un aspecto ideal. En este artículo vamos a analizar los criterios de fiabilidad de las señales, así como los métodos que ayudan al proveedor a mejorar la calidad de las mismas. Además, mostraremos el análisis de una señal en concreto, junto con los métodos que podrían ayudar al proveedor a hacerla más rentable y con menos riesgos.

Constructor gráfico de estrategias. Creando robots comerciales sin programación Constructor gráfico de estrategias. Creando robots comerciales sin programación

En este artículo se describe el constructor gráfico de estrategias. Se muestra como cualquier usuario puede crear los robots comerciales y las utilidades sin aplicar las técnicas de programación. Se puede simular los Asesores Expertos creados en el Probador de Estrategias, optimizarlos en la nube e iniciarlos en el gráfico en tiempo real.