Optimización móvil continua (Parte 2): Mecanismo de creación de informes de optimización para cualquier robot

26 febrero 2020, 13:03
Andrey Azatskiy
0
614

Introducción

Continuamos con el ciclo de artículos sobre la creación de un optimizador automático para realizar la optimización móvil de estrategias comerciales. Si el anterior artículo de la serie estaba dedicado a la creación de la biblioteca DLL que utilizaremos en nuestro optimizador automático y en el robot, este estará completamente dedicado al lenguaje MQL5. Vamos a analizar los métodos de generación de los informes de optimización y las formas en que podemos utilizar esta funcionalidad en nuestros algoritmos. 

Dado que el simulador de estrategias no ofrece acceso a sus indicadores desde el robot, y los datos descargados no son tan exhaustivos como querríamos, vamos a usar la funcionalidad para la descarga de informes de optimización que ya ha sido implementada en algunos artículos anteriores. Sin embargo, dado que ciertas partes de esta funcionalidad han sido mejoradas, mientras que otras no han sido claradas como es debido en los anteriores artículos, creemos que será adecuado describirlas de nuevo, pues nos encontramos ante una de las partes clave del programa creado. Vamos a comenzar precisamente por una de las novedades, más concretamente, por la adición de comisiones de usuario. Todas las clases y funciones descritas en este artículo se ubican en el directorio Include/History manager.

Implementando la comisión personalizada y el deslizamiento

El simulador de la plataforma MetaTrader 5 ofrece multitud de magníficas capacidades, sin embargo, a veces algunos brókeres no añaden a la historia la comisión por la transacción, o bien querríamos añadir a la descarga para la simulación adicional de estrategias la comisión adicional; con este objetivo, hemos añadido una clase que guarda la comisión para cada símbolo aparte. Más tarde, al llamar el método correspondiente, este nos dará la comisión y el deslizamiento especificado. La propia clase tiene los siguientes encabezados:

class CCCM
  {
private:
   struct Keeper
     {
      string            symbol;
      double            comission;
      double            shift;
     };

   Keeper            comission_data[];
public:

   void              add(string symbol,double comission,double shift);

   double            get(string symbol,double price,double volume);
   void              remove(string symbol);
  };

Para este clase se ha creado la estructura Keeper, que contiene la comisión y el deslizamiento para el activo establecido. Además, se ha creado una matriz con los datos de las estructuras en las que se almacenan las comisiones y el deslizamiento. Asimismo, se han declarado 3 métodos que añaden y eliminan los datos. El método de adición de una activo tiene el aspecto que sigue: 

void CCCM::add(string symbol,double comission,double shift)
{
 int s=ArraySize(comission_data);

 for(int i=0;i<s;i++)
   {
    if(comission_data[i].symbol==symbol)
        return;
   }

 ArrayResize(comission_data,s+1,s+1);

 Keeper keeper;
 keeper.symbol=symbol;
 keeper.comission=MathAbs(comission);
 keeper.shift=MathAbs(shift);

 comission_data[s]=keeper;
}

Este método implementa la adición de un nuevo activo a la colección, comprobando preliminarmente si el mismo activo ha sido añadido con anterioridad. Debemos destacar que el deslizamiento y la comisión se añaden al módulo, esto es necesario para que al sumar los gastos, el signo no afecte al cálculo. Asimismo, también hay que prestar atención a las unidades de medida al añadir los datos de las magnitudes.

  • Comisión: dependiendo del tipo de activo, se añade en la divisa en la que se valora el beneficio, o como porcentaje del volumen comerciado,
  • Deslizamiento: se añade siempre en puntos. 

También debemos prestar atención a que los datos de la magnitud no se añaden para una posición completa (apertura + cierre), sino para una transacción, es decir, para una posición completa tendremos n*comission + n*shift, donde n será el número total de transacciones que abren y cierran la posición.

El método remove elimina el activo seleccionado. Como clave se usará el nombre del símbolo.

void CCCM::remove(string symbol)
{
 int total=ArraySize(comission_data);
 int ind=-1;
 for(int i=0;i<total;i++)
   {
    if(comission_data[i].symbol==symbol)
      {
       ind=i;
       break;
      }
   }
 if(ind!=-1)
    ArrayRemove(comission_data,ind,1);
}

Si no se ha encontrado el símbolo correspondiente, el método se finalizará sin eliminar ningún activo.

Para obtener el desplazamiento y la comisión seleccionados, se usa el método get; su implementación se diferencia para diferentes tipos de activos. 

double CCCM::get(string symbol,double price,double volume)
{

 int total=ArraySize(comission_data);
 for(int i=0;i<total;i++)
   {
    if(comission_data[i].symbol==symbol)
      {
       ENUM_SYMBOL_CALC_MODE mode=(ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(symbol,SYMBOL_TRADE_CALC_MODE);

       double shift=comission_data[i].shift*SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_VALUE);

       double ans;
       switch(mode)
         {
          case SYMBOL_CALC_MODE_FOREX :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_FUTURES :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_CFD :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_CFDINDEX :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_CFDLEVERAGE :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_EXCH_STOCKS :
            {
             double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);
             ans=trading_volume*comission_data[i].comission/100+shift*volume;
            }
          break;
          case SYMBOL_CALC_MODE_EXCH_FUTURES :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_EXCH_FUTURES_FORTS :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_EXCH_BONDS :
            {
             double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);
             ans=trading_volume*comission_data[i].comission/100+shift*volume;
            }
          break;
          case SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX :
            {
             double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);
             ans=trading_volume*comission_data[i].comission/100+shift*volume;
            }
          break;
          case SYMBOL_CALC_MODE_EXCH_BONDS_MOEX :
            {
             double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);
             ans=trading_volume*comission_data[i].comission/100+shift*volume;
            }
          break;
          case SYMBOL_CALC_MODE_SERV_COLLATERAL :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          default: ans=0; break;
         }

       if(ans!=0)
          return -ans;

      }
   }

 return 0;
}

Desplazándonos por la matriz, buscamos por ella el símbolo indicado. Dado que para diferentes tipos de símbolos existe un método de cálculo distinto, su establecimiento también puede variar. Así, para las acciones y obligaciones, la comisión se establece en tanto por ciento de la rotación (turnover), donde la propia rotación se calcula como el producto del número de lotes por el número de contratos en el lote y por el precio al que se ha realizado la transacción.

Como resultado, obtenemos un equivalente monetario de la operación realizada. El resultado de la ejecución de este método siempre es la suma de la comisión y el deslizamiento en equivalente monetario. En este caso, además, el deslizamiento se calcula a partir del coste del tick. La clase descrita se usará posteriormente en la clase encargada de descargar los informes, que vamos a analizar ahora. Los parámetros de las comisiones para cada uno de los activos pueden ser incluidos en el código, o solicitados desde alguna base de datos, o bien transmitirse a un robot como parámetros de entrada; eso precisamente es lo que hemos hecho en nuestros algoritmos. 

La novedad en la clase CDealHistoryGetter

Las clases analizadas en el presente artículo y en los siguientes de la serie han figurado anteriormente en nuestros artículos, por eso, algunas partes pueden ser analizadas superficialmente, mientras que otras se someten a un estudio más profundo, en especial aquellas que no se han tenido en cuenta en artículos anteriores. En cualquier caso, no podremos arreglárnoslas sin ver su descripción, ya que en el algoritmo de descarga del informe de transacciones resulta clave el algoritmo de creación del informe a descargar.  

La primera de las clases mencionadas es CDealHistoryGetter, que llevamos usando con algunas modificaciones desde el primer artículo. Precisamente dicho artículo describía esta clase. En los archivos adjuntos se encuentra la última versión de esta clase, con algunos defectos subsanados. También contiene algunos añadidos en la forma descrita más arriba. Para acceder a un análisis más detallado del mecanismo de funcionamiento de la descarga del informe de transacciones en un aspecto cómodamente legible, lea el artículo indicado. En este artículo, vamos a analizar con menor detalle su funcionalidad, así como la forma en que se añade la comisión de usuario y el deslizamiento al informe descargado. De acuerdo con uno de los principios de la POO, que implica que un objeto debe cumplir el propósito específico indicado, este objeto se crea para recibir todos los tipos de resultados de los informes comerciales que nos pueden interesar, y contiene los siguientes métodos públicos, cada uno de los cuales cumple su papel:

  • getHistory — este método ayuda a descargar la historia de transacciones agrupada por posiciones. Si descargamos en el ciclo la historia de transacciones sin ningún filtrado con los métodos estándar, obtendremos la descripción de las transacciones presentadas por la estructura DealData: 

struct DealData
  {
   long              ticket;        // Deal ticket
   long              order;         // The number of the order that opened the position
   datetime          DT;            // Position open date
   long              DT_msc;        // Position open date in milliseconds
   ENUM_DEAL_TYPE    type;          // Open position type
   ENUM_DEAL_ENTRY   entry;         // Position entry type
   long              magic;         // Unique position number
   ENUM_DEAL_REASON  reason;        // Order placing reason
   long              ID;            // Position ID
   double            volume;        // Position volume (lots)
   double            price;         // Position entry price
   double            comission;     // Commission paid
   double            swap;          // Swap
   double            profit;        // Profit / loss
   string            symbol;        // Symbol
   string            comment;       // Comment specified when at opening
   string            ID_external;   // External ID
  };

En este caso, además, los datos serán clasificados según la hora de apertura de las posiciones, pero no se agruparán de ninguna forma. En el artículo indicado, se muestra con ejemplos lo complicado que es leer una descarga semejante, ya que al implementar las transacciones con varios algoritmos al mismo tiempo, siempre surge una confusión entre las transacciones. Sobre todo, si se usa un sistema de incremento de posiciones que compren/vendan adicionalmente un activo de acuerdo con algoritmos propios. Como resultado, obtendremos una serie de transacciones de entrada y de salida que se mostrarán de forma no agrupada, y que solo se estorbarán unas a otras y no mostrarán una panorámica completa.

El método analizado las agrupa por posiciones, lo que sin duda ayuda a relacionar las transacciones con una posición concreta. A pesar de que la situación de las órdenes sigue siendo confusa, por lo menos nos deshacemos de las transacciones sobrantes que no entran en la posición investigada. El resultado obtenido se almacena como una estructura que guarda una matriz de la estructura de transacción mostrada arriba.  

struct DealKeeper
  {
   DealData          deals[]; /* List of all deals for this position
                              (or several positions in case of position reversal)*/
   string            symbol;  // Symbol
   long              ID;      // ID of the position (s)
   datetime          DT_min;  // Open date (or the date of the very first position)
   datetime          DT_max;  // Close date
  };

Debemos considerar que esta clase no tiene en cuenta en sus grupos los números mágicos, dado que en la práctica, si se usan dos o más algoritmos en las transacciones de un solo símbolo, estos se cruzarán, no pudiendo así hacerse cargo de la posición. Como mínimo en la "Bolsa de Moscú", para la cual escribimos la mayoría de los algoritmos, esto resulta técnicamente imposible. Además, debemos considerar que la herramienta que ofrecemos fue diseñada para descargar los informes de las transacciones, o bien para descargar los resultados de las simulaciones/optimizaciones. Para la primera tarea, nos bastará con tener las estadísticas del símbolo seleccionado, que se ofrecen al completo. Para la segunda, el número mágico no es esencial, dado que, según la lógica de esta funcionalidad, un algoritmo debetener solo un número mágico, y el simulador solo aplica una algoritmo cada vez.

La implementación de este método ha permanecido inalterada desde que se escribió por primera vez, salvo por la adición del método para implementar la comisión de usuario. Para la tarea planteada, al constructor de clase se le transmite por enlace la clase CCCM analizada antes, y se guarda en el campo correspondiente. A continuación, al rellenar la estructura DealData (más concretamente, en el momento en que se rellena la comisión), se añade la comisión de usuario guardada en la clase CCCM transmitida. 

#ifndef ONLY_CUSTOM_COMISSION
               if(data.comission==0 && comission_manager != NULL)
                 {
                  data.comission=comission_manager.get(data.symbol,data.price,data.volume);
                 }
#else
               data.comission=comission_manager.get(data.symbol,data.price,data.volume);
#endif

En este caso, además, la comisión se añade tanto de forma directiva, como condicional. Si, antes de incluir el archivo condicionado con los datos de la clase en el robot, definimos el parámetro ONLY_CUSTOM_COMISSION , el campo de la comisión contendrá siempre la comisión que hemos transmitido, y no la que proporciona el bróker. Si no definimos el dicho parámetro, la comisión que transmitimos se añadirá de forma condicional, para ser más exactos, solo cuando el bróker no la ofrezca junto con las cotizaciones, de lo contrario, la comisión de usuario será ignorada.

  • getIDArr — retorna una matriz con las IDs de las posiciones abiertas de todos los símbolos en el intervalo temporal solicitado. Preciamente con las IDs de las posiciones se hace posible combinar todas las transacciones en la posición en nuestro método. En esencia, se trata de una lista única del campo DealData.ID. 
  • getDealsDetales — básicamente, se trata de un método semejante al método getHistory, pero ofrece menos detalles. La tarea esencial de este método es ofrecer un recuadro de posiciones cómodamente legible, donde cada línea se corresponda con una transacción concreta. Cada una de las posiciones se describe con la siguiente estructura: 
    struct DealDetales
      {
       string            symbol;        // Symbol
       datetime          DT_open;       // Open date
       ENUM_DAY_OF_WEEK  day_open;      // Open day
       datetime          DT_close;      // Cloe date
       ENUM_DAY_OF_WEEK  day_close;     // Close day
       double            volume;        // Volume (lots)
       bool              isLong;        // Long/Short
       double            price_in;      // Position entry price
       double            price_out;     // Position exit price
       double            pl_oneLot;     // Profit / loss is trading one lot
       double            pl_forDeal;    // Real profit/loss taking into account commission
       string            open_comment;  // Comment at the time of opening
       string            close_comment; // Comment at the time of closing
      };
    
    En general, representan un recuadro de posiciones en el que la clasificación se realiza según la fecha de cierre de las mismas. Esa matriz de datos la vamos a utilizar para calcular los coeficientes en la siguiente clase analizada; por consiguiente, usando como base los datos ofrecidos, obtendremos un informe final sobre la prueba realizada. Dichos datos será también utilizados por el simulador (al finalizar las transacciones) para construir la línea azul del gráfico PL.

    Por cierto, ya que hemos tocado el tema del simulador, también debemos destacar que en los siguientes cálculos, el factor de recuperación calculado en el terminal se distinguirá del valor calculado sobre la descarga obtenida. El motivo es que, aunque la descarga de datos es correcta, y las fórmulas sobre las que se calcula el coeficiente descrito son idénticas en el terminal y en la clase posterior, lo datos fuente se diferencian. El simulador calcula el factor de recuperación según la línea verde, es decir, según la descarga detallada, y nosotros vamos a realizar los cálculos según la azul, o sea, según los datos que no tienen encuenta las oscilaciones en el intervalo temporal desde el momento de apertura hasta el cierre.   
  • getBalance — este método ha sido creado para obtener los datos sobre el balance sin tener en cuenta las operaciones en la fecha indicada. 
    double CDealHistoryGetter::getBalance(datetime toDate)
      {
       if(HistorySelect(0,(toDate>0 ? toDate : TimeCurrent())))
         {
          int total=HistoryDealsTotal(); // Получаем общее количество позиций
          double balance=0;
          for(int i=0; i<total; i++)
            {
             long ticket=(long)HistoryDealGetTicket(i);
    
             ENUM_DEAL_TYPE dealType=(ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket,DEAL_TYPE);
             if(dealType==DEAL_TYPE_BALANCE ||
                dealType == DEAL_TYPE_CORRECTION ||
                dealType == DEAL_TYPE_COMMISSION)
               {
                balance+=HistoryDealGetDouble(ticket,DEAL_PROFIT);
    
                if(toDate<=0)
                   break;
               }
            }
          return balance;
         }
       else
          return 0;
      }
    

Para conseguir la tarea planteada, primero se solicita la historia de todas las transacciones desde el primer intervalo temporal hasta el indicado. A continuación, el balance se guarda en un ciclo, y después se añaden todos los ingresos y retiradas de fondos al balance inicial, teniendo en cuenta las comisiones y las correcciones recibidas del bróker. Si se ha transmitido como parámetro de entrada una fecha cero, significará que se ha solicitado solo el balance de la fecha al inicio

  • getBalanceWithPL — este método es análogo al anterior. No obstante, además de los cambios en el balance, también tiene en cuenta el beneficio y/o pérdidas de las operaciones realizadas, a las que también se añade la comisión según el principio descrito anteriormente.

Clase para crear el informe de optimización: estructuras implicadas en los cálculos

El siguiente de los objetos explicados (ya mencionados en artículos anteriores) será la clase CReportCreator. Esta clase fue brevemente presantada en el artículo Las 100 mejores pasadas de optimización, en el apartado "Cálculos"; sin embargo, debido al exceso de información que incluimos en el artículo, no quedó espacio para analizar la clase. Vamos a subsanar este detalle, puesto que precisamente dicha clase calcula todos los coeficientes en los que se basan las decisiones del optimizador automático sobre la correspondencia de dicha combinación de parámetros del algoritmo con los criterios solicitados. 

Durante el análisis de este objeto, debemos describir primero la idea principal incorporada en el enfoque que hemos usado para su implementación. En el primero de nuestros artículos ya implementamos una clase semejante, aunque con una funcionalidad más pobre. Sin embargo, resultaba demasiado lenta: para calcular un grupo de parámetros solicitados o un gráfico, debíamos descargar de nuevo la historia de transacciones e iterar por la misma en un ciclo, y así en cada solicitud del parámetro.

En ocasiones, si el volumen de datos resulta considerable, este enfoque puede ocupar varios segundos, lo cual resulta demasiado tiempo. Precisamente para acelerar los cálculos realizados, hemos utilizado el método de implementación de esta clase que veremos a continuación. Asimismo, la clase contiene muchos más datos, bastantes de los cuales no se ofrecen incluso en la descarga estándar de los informes de la optimización. Si estudiamos un poco el tema, veremos que para calcular muchos coeficientes, necesitamos datos homogéneos, tales como el beneficio/pérdidas máximos o el beneficio/pérdidas acumulados, y otros semejantes.

Por consiguiente, tras calcular estos coeficientes en un único ciclo y guardarlos en los campos de esta clase, podemos aplicar los datos obtenidos para calcular todos los parámetros en cuyos cálculos figuran aquellos. De esta forma, obtendremos una clase que, en una sola pasada en el ciclo por la historia descargada, calculará todos los parámetros necesarios y los guardará hasta el siguiente cálculo. En lo sucesivo, para calcular el parámetro requerido, no se realizará su recálculo, sino que solo se copiarán los valores ya guardados, lo cual simplificará significativamente el trabajo.

Ahora que hemos descrito la idea principal utilizada en su creación, vamos analizar cómo se dan precisamente los cálculos de los parámetros. Comenzaremos analizando los objetos que guardan los datos usados para los posteriores cálculos de los valores buscados. Estos objetos se crean como objetos de clase incorporados, declarados en el ámbito private. Esto se hace por dos motivos: primero, para que no sea posible utilizarlos en otras clases que usarán esta funcionalidad, ya que resulta muy fácil confundirse entre tanta estructura y clase declarada (cuáles de ellas son necesarias para los cálculos externos, y cuáles son solo técnicas o internas); y segundo, para enfatizar su carácter puramente teórico.  

Estructura de PL_Keeper:

struct PL_keeper
{
 PLChart_item      PL_total[];
 PLChart_item      PL_oneLot[];
 PLChart_item      PL_Indicative[];
};

La presente estructura se ha creado para guardar todos los gráficos posibles de beneficio y pérdidas. Estos han sido descritos con detalle en nuestro primer artículo, cuyo enlace ya se ha adjuntado. A continuación, debajo de esta estructura, se crean sus ejemplares:

PL_keeper         PL,PL_hist,BH,BH_hist;

Cada uno de los ejemplares almacena los 4 tipos de gráfico presentados, pero solo para distintos gráficos fuente. Los datos con el prefijo PL se calculan según la mencionada línea azul del gráfico PL del terminal, mientras que los gráficos con el prefijo BH se calculan según los datos del gráfico de beneficio y pérdidas obtenido de la estrategia Buy and Hold. Los datos con el postfijo hist se calculan según el histograma de beneficio y pérdidas.

Estructura de DailyPL_keeper:

// The structure of Daily PL graphs
struct DailyPL_keeper
{
 DailyPL           avarage_open,avarage_close,absolute_open,absolute_close;
};

Esta estructura contiene los 4 tipos de gráfico posibles de beneficio/pérdidas diarios, dicho de otra forma, el propio histograma en el informe de transacciones en el que se describe el beneficio/pérdidas de las transacciones por días. Los ejemplares de la estructura DailyPLmarcados con el prefijo average, se calculan según los datos promedio de beneficio/pérdidas; aquellos mismos que estén marcados con el prefijo absolute, se calcularán según los datos totales sumados de beneficio y pérdidas. Por consiguiente, la diferencia entre ellos es obvia: en el primer caso, se representa el beneficio medio diario durante todas las transacciones, mientras que en el segundo, se representa el beneficio sumado. Los datos con el prefijo open están clasificados por días usando las fechas de apertura, mientras que los datos con el postfijo close usan las fechas de cierre. El ejemplar de esta estructura, al igual que los ejemplares de las otras estructuras descritas, está declarado más abajo en el código, pero su declaración resulta trivial.

Estructura de RationTable_keeper:

// Table structure of extreme points
struct RatioTable_keeper
  {
   ProfitDrawdown    Total_max,Total_absolute,Total_percent;
   ProfitDrawdown    OneLot_max,OneLot_absolute,OneLot_percent;
  };

Esta estructura consta de los ejemplares de la estructura ProfitDrawdown

struct ProfitDrawdown
  {
   double            Profit; // In some cases Profit, in other Profit / Loss
   double            Drawdown; // Drawdown
  };

Y contiene la relación entre el beneficio y las pérdidas según determinados criterios. Los datos con el prefijo Total se calculan según el gráfico de beneficio/pérdidas construido teniendo en cuenta los cambios en el lotaje durante el comercio de una posición a otra. Los datos con el perfijo OneLot se calculan como si todo el comercio se realizara todo el tiempo con un contrato. Podrá encontra más información sobre este registro no estándar de lotaje en el mencionado primer artículo. En pocas palabras, podemos decir que se ha creado para evaluar los resultados de la actividad del sistema comercial, para que sea posible valorar qué produce más resultados, la gestión del lotaje o la propia lógica del sistema. El postfijo max muestra que en este ejemplar se han introducido los datos sobre el valor máximo de beneficio y reducción en la historia de transacciones. El postfijo absolute muestra la existencia de los datos sumados del beneficio en la historia de transacciones y la reducción en la historia de transacciones. El postfijo percent muestra que los datos introducidos sobre el beneficio y la reducción han sido calculados como el porcentaje respecto al valor máximo en la curva de PL alcanzado en el intervalo investigado. La declaración de esta estructura también es trivial, por lo que no se adjunta. 

El siguiente grupo de estructuras no se declara como campo de la clase, pero sí que se usa su declaración local en el método Create principal. Todas las estructuras descritas en esta parte del artículo se combinan en una, por eso vamos a mostrar su definición en un solo lugar, analizando posteriormente cada una ellas por separado. 

// Structures for calculating consecutive profits and losses
   struct S_dealsCounter
     {
      int               Profit,DD;
     };
   struct S_dealsInARow : public S_dealsCounter
     {
      S_dealsCounter    Counter;
     };
   // Structures for calculating auxiliary data
   struct CalculationData_item
     {
      S_dealsInARow     dealsCounter;
      int               R_arr[];
      double            DD_percent;
      double            Accomulated_DD,Accomulated_Profit;
      double            PL;
      double            Max_DD_forDeal,Max_Profit_forDeal;
      double            Max_DD_byPL,Max_Profit_byPL;
      datetime          DT_Max_DD_byPL,DT_Max_Profit_byPL;
      datetime          DT_Max_DD_forDeal,DT_Max_Profit_forDeal;
      int               Total_DD_numDeals,Total_Profit_numDeals;
     };
   struct CalculationData
     {
      CalculationData_item total,oneLot;
      int               num_deals;
      bool              isNot_firstDeal;
     };


Las estructuras S_dealsCounter y S_dealsInARow son en esencia una sola unidad. Semejante combinación de asociaciones, así como la herencia simultánea en estas estructuras, se relaciona con el cálculo peculiar de sus parámetros. Para comenzar, debemos decir que la estructura S_dealsInARow ha sido creada para almacenar y calcular el número de transacciones (en realidad, de las posiciones, es decir, nos referimos al ciclo que va desde la apertura hasta el cierre de la posición) sucedidas consecutivamente, tanto positivas, como negativas. El ejemplar incorporado de la estructura S_dealsCounter se declara para almacenar los resultados intermedios de los cálculos, mientras que los campos heredados guardan los valores totales. Más tarde, volveremos a esta operación de cálculo de transacciones rentables/no rentables.     

La estructura CalculationData_item contiene los campos necesarios para calcular los coeficientes necesarios. 

  • R_arr es la matriz de las secuencias de series de transacciones rentables/no rentables, representadas como 1 / 0 respectivamente. Esta matriz se usa para calcular la puntuación Z;
  • DD_percent — valor porcentual de la reducción;
  • Accomulated_DD, Accomulated_Profit  — guardan el valor sumado de las pérdidas y el beneficio;
  • PL — beneficio / pérdidas;
  • Max_DD_forDeal, Max_Profit_forDeal — según su denominación, guardan el beneficio y pérdidas máximos entre las transacciones;
  • Max_DD_byPL, Mаx_Profit_byPL — según su nombre, guardan el beneficio y pérdidas máximos calculados según el gráfico de PL;
  • DT_Max_DD_byPL, DT_Max_Profit_byPL — guardan las fechas de las reducciones máximas de PL;
  • DT_Max_DD_forDeal, DT_Max_Profit_forDeal — respectivamente, las fechas de las reducciones y beneficios para las transacciones correspondientes;
  • Total_DD_numDeals, TotalProfit_numDeals — cantidad sumada de transacciones rentables y no rentables. 

A partir de estos datos, se realizan los cálculos posteriores.

La estructura CalculationData es una estructura acumulativa donde se combinan todas las descripciones de las estructuras, en concreto, contiene todos los datos necesarios. En ella también se contiene el campo num_deals, que en esencia supone la suma de los campos CalculationData_item::Total_DD_numDeals y CalculationData_item::TotalProfit_numDeals, mientras que el campo sNot_firstDeal es una bandera técnica que indica que el cálculo en la iteración actual del ciclo no se realiza para la primera transacción.

Estructura CoefChart_keeper:

struct CoefChart_keeper
     {
      CoefChart_item    OneLot_ShartRatio_chart[],Total_ShartRatio_chart[];
      CoefChart_item    OneLot_WinCoef_chart[],Total_WinCoef_chart[];
      CoefChart_item    OneLot_RecoveryFactor_chart[],Total_RecoveryFactor_chart[];
      CoefChart_item    OneLot_ProfitFactor_chart[],Total_ProfitFactor_chart[];
      CoefChart_item    OneLot_AltmanZScore_chart[],Total_AltmanZScore_chart[];
     };

Se ha creado para guardar los gráficos de los coeficientes. Esta clase construye no solo los gráficos de beneficio y pérdidas, sino también los gráficos de algunos de los coeficientes, por ello, de manera análoga a la estructura que guarda los gráficos de beneficio y pérdidas, hemos creado esta estructura para los tipos de datos descritos. El prefijo OneLot indica que en este ejemplar del objeto se guardarán los datos obtenidos mediante el análisis del gráfico de beneficio/pérdidas de las transacciones, si comerciamos con un lote sin tener encuenta la gestión del lotaje. El prefijo Total indica que se ha evaluado el gráfico de transacciones con el registro de gestión del lotaje que ha sido utilizado. Si no se ha usado ningún sistema de gestión del lotaje, ambos gráficos serán idénticos.

La clase СHistoryComparer:

También se define la clase que participa en la clasificación de los datos. Como se puede leer en el artículo "Las 100 mejores pasadas de optimzación", hemos creado la clase CGenericSorter, que sabe clasificar datos de cualquier tipo por orden descendente. No obstante, para que funcione, debemos escribir una clase que pueda comparar los tipos transmitidos. Precisamente la clase СHisoryComparer es esa clase.

class CHistoryComparer : public ICustomComparer<DealDetales>
     {
   public:
      int               Compare(DealDetales &x,DealDetales &y);
     };

La implementación de su método es bastante prosaica. Compara las fechas de cierre, dado que la clasificación se realiza precisamente según ellas:

int CReportCreator::CHistoryComparer::Compare(DealDetales &x,DealDetales &y)
  {
   return(x.DT_close == y.DT_close ? 0 : (x.DT_close > y.DT_close ? 1 : -1));
  }

Asimismo, existe una clase que clasifica los gráficos de los coeficientes, que tiene una estructura similar. Ambas clases, así como la clase del clasificador, se instalan como campo global de la clase CReportCreator descrita. Asimismo, aparte de los objetos descritos, existen otros dos campos cuyos tipos están descritos como objetos aparte, sin incorporar:

PL_detales        PL_detales_data;
DistributionChart OneLot_PDF_chart,Total_PDF_chart;

La estructura PL_detales contiene información breve sobre las transacciones para las posiciones rentables y no rentables:

//+------------------------------------------------------------------+
struct PL_detales_PLDD
  {
   int               orders; // Number of deals
   double            orders_in_Percent; // Number of orders as % of total number of orders
   int               dealsInARow; // Deals in a row
   double            totalResult; // Total result in money
   double            averageResult; // Average result in money
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
struct PL_detales_item
  {
   PL_detales_PLDD   profit; // Information on profitable deals
   PL_detales_PLDD   drawdown; // Information on losing deals
  };
//+-------------------------------------------------------------------+
//| A brief PL graph summary divided into 2 main blocks               |
//+-------------------------------------------------------------------+
struct PL_detales
  {
   PL_detales_item   total,oneLot;
  };

Y la segunda estructura, DistributionChart, contiene una serie de indicadores VaR, así como el gráfico de distribución conforme al cual se han calculado estos coeficientes. La distribución se calcula como una distribución normal.

//+------------------------------------------------------------------+
//| Structure used for saving distribution charts                    |
//+------------------------------------------------------------------+
struct Chart_item
  {
   double            y; // y axis
   double            x; // x axis
  };
//+------------------------------------------------------------------+
//| Structure contains the VaR value                                 |
//+------------------------------------------------------------------+
struct VAR
  {
   double            VAR_90,VAR_95,VAR_99;
   double            Mx,Std;
  };
//+------------------------------------------------------------------+
//| Structure - it is used to store distribution charts and          |
//| the VaR values                                                   |
//+------------------------------------------------------------------+
struct Distribution_item
  {
   Chart_item        distribution[]; // Distribution chart
   VAR               VaR; // VaR
  };
//+------------------------------------------------------------------+
//| Structure - Stores distribution data. Divided into 2 blocks      |
//+------------------------------------------------------------------+
struct DistributionChart
  {
   Distribution_item absolute,growth;
  };

Los propios coeficientes VaR se calculan según la fórmula del más simple: la del VaR histórico, el cual, posiblemente, no dé el resultado más exacto. Sin embargo, para la presente implementación, resultará adecuado. 

Métodos de cálculo de coeficientes que describen los resultados de las transacciones

Ahora que nos hemos analizado las estructuras que guardan los datos, ya podemos imaginar el volumen de las estadísticas que calcula esta clase. Vamos a analizar por turno los métodos concretos que se encargan del cálculo de los indicadores descritos, tal y como se nombran en la clase CReportCreator.

El método CalcPL ha sido creado para calcular el gráfico PL. Su implementación es la siguiente:

void CReportCreator::CalcPL(const DealDetales &deal,CalculationData &data,PLChart_item &pl_out[],CalcType type)
  {
   PLChart_item item;
   ZeroMemory(item);
   item.DT=deal.DT_close; // Saving the date

   if(type!=_Indicative)
     {
      item.Profit=(type==_Total ? data.total.PL : data.oneLot.PL); // Saving the profit
      item.Drawdown=(type==_Total ? data.total.DD_percent : data.oneLot.DD_percent); // Saving the drawdown
     }
   else // Calculating the indicative chart
     {
      if(data.isNot_firstDeal)
        {
         if(data.total.PL!=0)
           {
            if(data.total.PL > 0 && data.total.Max_DD_forDeal < 0)
               item.Profit=data.total.PL/MathAbs(data.total.Max_DD_forDeal);
            else
               if(data.total.PL<0 && data.total.Max_Profit_forDeal>0)
                  item.Profit=data.total.PL/data.total.Max_Profit_forDeal;
           }
        }
     }
// Adding data to array
   int s=ArraySize(pl_out);
   ArrayResize(pl_out,s+1,s+1);
   pl_out[s]=item;
  }

Como podemos ver por su implementación, todos sus cálculos se basan en los datos de las estructuras ya descritas, que se transmiten como parámetro de entrada.

Si tenemos que calcular un gráfico PL de tipo no indicativo, solo tenemos que copiar los datos que ya conocemos. En caso contrario, el cálculo solo se realizará de cumplirse dos condiciones: la primera iteración no se ha encontrado en el ciclo, y PL no es igual a cero. El propio cálculo se realiza según la siguiente lógica:

  • Si PL es mayor a cero, y la reducción menor, dividiremos el valor actual de PL por el valor de la reducción. Con ello, obtendremos un coeficiente que indicará cuántas reducciones máximas seguidas se necesitarán para reducir el PL actual a cero. 
  • Si el PL es menor a cero, y el beneficio máximo alcanzado para todas las transacciones es mayor a cero, dividiremos el valor de PL (que en estos momentos sería la reducción) por el beneficio máximo alcanzado, obteniendo con ello un coeficiente que indicará cuántos beneficios máximos seguidos se necesitarán para reducir la reducción actual a cero.

El siguiente método, CalcPLHist, se basa en un mecanismo similar, pero usa en los cálculos otros campos de estructura, en concreto, data.oneLot.Accomulated_DD, data.total.Accomulated_DD y data.oneLot.Accomulated_Profit, data.total.Accomulated_Profit. Dado que ya hemos analizado su algoritmo de acción, no nos detendremos en este método, vamos a pasar a dos métodos más importantes.

Los métodos CalcData y CalcData_item:

Precisamente en estos métodos tiene lugar el cálculo de todos los coeficientes auxiliares y principales. Comenzaremos el análisis por el método CalcData_item, cuya tarea consiste en calcular los coeficientes auxiliares que analizamos anteriormente, y que sirven para calcular los coeficientes principales.  

//+------------------------------------------------------------------+
//| Calculando los datos auxiliares                                  |
//+------------------------------------------------------------------+
void CReportCreator::CalcData_item(const DealDetales &deal,CalculationData_item &out,
                                   bool isOneLot)
  {
   double pl=(isOneLot ? deal.pl_oneLot : deal.pl_forDeal); //PL
   int n=0;
// Кол-прибылей и убытков
   if(pl>=0)
     {
      out.Total_Profit_numDeals++;
      n=1;
      out.dealsCounter.Counter.DD=0;
      out.dealsCounter.Counter.Profit++;
     }
   else
     {
      out.Total_DD_numDeals++;
      out.dealsCounter.Counter.DD++;
      out.dealsCounter.Counter.Profit=0;
     }
   out.dealsCounter.DD=MathMax(out.dealsCounter.DD,out.dealsCounter.Counter.DD);
   out.dealsCounter.Profit=MathMax(out.dealsCounter.Profit,out.dealsCounter.Counter.Profit);

// Serie de beneficios y pérdidas
   int s=ArraySize(out.R_arr);
   if(!(s>0 && out.R_arr[s-1]==n))
     {
      ArrayResize(out.R_arr,s+1,s+1);
      out.R_arr[s]=n;
     }

   out.PL+=pl; //PL общий
// Макс Profit / DD
   if(out.Max_DD_forDeal>pl)
     {
      out.Max_DD_forDeal=pl;
      out.DT_Max_DD_forDeal=deal.DT_close;
     }
   if(out.Max_Profit_forDeal<pl)
     {
      out.Max_Profit_forDeal=pl;
      out.DT_Max_Profit_forDeal=deal.DT_close;
     }
// Profit / DD acumulado
   out.Accomulated_DD+=(pl>0 ? 0 : pl);
   out.Accomulated_Profit+=(pl>0 ? pl : 0);
// Puntos de extremo según el beneficio
   double maxPL=MathMax(out.Max_Profit_byPL,out.PL);
   if(compareDouble(maxPL,out.Max_Profit_byPL)==1/* || !isNot_firstDeal*/)// para guardar la fecha será necesaria otra comprobación
     {
      out.DT_Max_Profit_byPL=deal.DT_close;
      out.Max_Profit_byPL=maxPL;
     }
   double maxDD=out.Max_DD_byPL;
   double DD=0;
   if(out.PL>0)
      DD=out.PL-maxPL;
   else
      DD=-(MathAbs(out.PL)+maxPL);
   maxDD=MathMin(maxDD,DD);
   if(compareDouble(maxDD,out.Max_DD_byPL)==-1/* || !isNot_firstDeal*/)// para guardar la fecha será necesaria otra comprobación
     {
      out.Max_DD_byPL=maxDD;
      out.DT_Max_DD_byPL=deal.DT_close;
     }
   out.DD_percent=(balance>0 ?(MathAbs(DD)/(maxPL>0 ? maxPL : balance)) :(maxPL>0 ?(MathAbs(DD)/maxPL) : 0));
  }

En primer lugar, se realiza el cálculo de PL en la i-ésima iteración. A continuación, si ha habido beneficio en esta iteración, incrementamos el contador de transacciones rentables, y también ponemos a cero el contador de transacciones no rentables consecutivas. Además, asignamos a la variable n el valor 1, que indica que la transacción actual ha sido rentable. Si el PL ha sido menor a cero, incrementamos el contador de pérdidas y ponemos a cero las transacciones rentables consecutivas. Después de ello, asignamos el número máximo de series rentables y no rentables seguidas.

El siguiente paso es calcular las series de transacciones rentables y no rentables. Entendemos por serie un cierto número de transacciones positivas o negativas consecutivas. En esta matriz, tras el cero siempre irá la unidad, y tras la unidad, solo el cero: esta alternancia muestra la alternancia de transacciones positivas y negativas, mientras que en el sitio donde hay, digamos, una unidad, en realidad pueden suceder no una, sino multitud de transacciones positivas; lo mismo sucede con el cero. Esta matriz se usará al calcular la puntuación Z, que indica el nivel de aleatoriedad del comercio. El siguiente paso es la asignación de los valores de beneficio/pérdidas máximos y el cálculo del beneficio/pérdidas acumulados. Al final de este método tiene lugar el cálculo de los puntos extremos, es decir, se rellenan las estructuras con los valores de los beneficios y pérdidas máximos.

El método CalcData ya usa los datos intermedios obtenidos al calcular los coeficientes necesarios y actualiza los cálculos en cada iteración. Su implementación es la siguiente:

void CReportCreator::CalcData(const DealDetales &deal,CalculationData &out,bool isBH)
  {
   out.num_deals++; // Counting the number of deals
   CalcData_item(deal,out.oneLot,true);
   CalcData_item(deal,out.total,false);

   if(!isBH)
     {
      // Fill PL graphs
      CalcPL(deal,out,PL.PL_total,_Total);
      CalcPL(deal,out,PL.PL_oneLot,_OneLot);
      CalcPL(deal,out,PL.PL_Indicative,_Indicative);

      // Fill PL Histogram graphs
      CalcPLHist(deal,out,PL_hist.PL_total,_Total);
      CalcPLHist(deal,out,PL_hist.PL_oneLot,_OneLot);
      CalcPLHist(deal,out,PL_hist.PL_Indicative,_Indicative);

      // Fill PL graphs by days
      CalcDailyPL(DailyPL_data.absolute_close,CALC_FOR_CLOSE,deal);
      CalcDailyPL(DailyPL_data.absolute_open,CALC_FOR_OPEN,deal);
      CalcDailyPL(DailyPL_data.avarage_close,CALC_FOR_CLOSE,deal);
      CalcDailyPL(DailyPL_data.avarage_open,CALC_FOR_OPEN,deal);

      // Fill Profit Factor graphs
      ProfitFactor_chart_calc(CoefChart_data.OneLot_ProfitFactor_chart,out,deal,true);
      ProfitFactor_chart_calc(CoefChart_data.Total_ProfitFactor_chart,out,deal,false);

      // Fill Recovery Factor graphs
      RecoveryFactor_chart_calc(CoefChart_data.OneLot_RecoveryFactor_chart,out,deal,true);
      RecoveryFactor_chart_calc(CoefChart_data.Total_RecoveryFactor_chart,out,deal,false);

      // Fill winning coefficient graphs
      WinCoef_chart_calc(CoefChart_data.OneLot_WinCoef_chart,out,deal,true);
      WinCoef_chart_calc(CoefChart_data.Total_WinCoef_chart,out,deal,false);

      // Fill Sharpe Ration graphs
      ShartRatio_chart_calc(CoefChart_data.OneLot_ShartRatio_chart,PL.PL_oneLot,deal/*,out.isNot_firstDeal*/);
      ShartRatio_chart_calc(CoefChart_data.Total_ShartRatio_chart,PL.PL_total,deal/*,out.isNot_firstDeal*/);

      // Fill Z Score graphs
      AltmanZScore_chart_calc(CoefChart_data.OneLot_AltmanZScore_chart,(double)out.num_deals,
                              (double)ArraySize(out.oneLot.R_arr),(double)out.oneLot.Total_Profit_numDeals,
                              (double)out.oneLot.Total_DD_numDeals/*,out.isNot_firstDeal*/,deal);
      AltmanZScore_chart_calc(CoefChart_data.Total_AltmanZScore_chart,(double)out.num_deals,
                              (double)ArraySize(out.total.R_arr),(double)out.total.Total_Profit_numDeals,
                              (double)out.total.Total_DD_numDeals/*,out.isNot_firstDeal*/,deal);
     }
   else // Fill PL Buy and Hold graphs
     {
      CalcPL(deal,out,BH.PL_total,_Total);
      CalcPL(deal,out,BH.PL_oneLot,_OneLot);
      CalcPL(deal,out,BH.PL_Indicative,_Indicative);

      CalcPLHist(deal,out,BH_hist.PL_total,_Total);
      CalcPLHist(deal,out,BH_hist.PL_oneLot,_OneLot);
      CalcPLHist(deal,out,BH_hist.PL_Indicative,_Indicative);
     }

   if(!out.isNot_firstDeal)
      out.isNot_firstDeal=true; // Flag "It is NOT the first deal"
  }

En primer lugar, se calculan los coeficientes intermedios para comerciar con un lote, así como para las transacciones con los sistemas de gestión de lotaje mediante la llamada del método ya descrito para ambos tipos de datos. A continuación, los cálculos se dividen entre los coefientes para BH y para el tipo de datos opuesto. Dentro de cada uno de los bloques, se calculan los coeficientes interpretables. Para la estrategia Buy and Hold, se calculan solo los gráficos, por eso no llamamos los métodos que calculan los coeficientes.  

El siguiente grupo de métodos calcula el beneficio/pérdidas por días:

//+------------------------------------------------------------------+
//| Create a structure of trading during a day                       |
//+------------------------------------------------------------------+
void CReportCreator::CalcDailyPL(DailyPL &out,DailyPL_calcBy calcBy,const DealDetales &deal)
  {
   cmpDay(deal,MONDAY,out.Mn,calcBy);
   cmpDay(deal,TUESDAY,out.Tu,calcBy);
   cmpDay(deal,WEDNESDAY,out.We,calcBy);
   cmpDay(deal,THURSDAY,out.Th,calcBy);
   cmpDay(deal,FRIDAY,out.Fr,calcBy);
  }
//+------------------------------------------------------------------+
//| Save resulting PL/DD for the day                                 |
//+------------------------------------------------------------------+
void CReportCreator::cmpDay(const DealDetales &deal,ENUM_DAY_OF_WEEK etalone,PLDrawdown &ans,DailyPL_calcBy calcBy)
  {
   ENUM_DAY_OF_WEEK day=(calcBy==CALC_FOR_CLOSE ? deal.day_close : deal.day_open);
   if(day==etalone)
     {
      if(deal.pl_forDeal>0)
        {
         ans.Profit+=deal.pl_forDeal;
         ans.numTrades_profit++;
        }
      else
         if(deal.pl_forDeal<0)
           {
            ans.Drawdown+=MathAbs(deal.pl_forDeal);
            ans.numTrades_drawdown++;
           }
     }
  }
//+------------------------------------------------------------------+
//| Average resulting PL/DD for the day                              |
//+------------------------------------------------------------------+
void CReportCreator::avarageDay(PLDrawdown &day)
  {
   if(day.numTrades_profit>0)
      day.Profit/=day.numTrades_profit;
   if(day.numTrades_drawdown > 0)
      day.Drawdown/=day.numTrades_drawdown;
  }

Como podemos ver por la implementación mostrada, el trabajo principal en cuanto a la división según el beneficio/pérdidas por días tiene lugar en el método cmpDay, que primero comprueba si el día se corresponde o no con el solicitado, y a continuación simplemente añade los valores de beneficio y pérdidas. No obstante las pérdidas son sumadas en módulo. CalcDailyPL es un método de agregación en el que se intenta añadir el PL actual transmitido a uno de los cinco días laborables. El método avarageDay es llamado para promediar el beneficio/pérdidas en el método principal Create. Este método no hace nada especial, solo convierte en valores medios los valores absolutos de beneficio/pérdidas calculados anteriormente. 

Método que calcula el Factor de Beneficio

//+------------------------------------------------------------------+
//| Calculate Profit Factor                                          |
//+------------------------------------------------------------------+
void CReportCreator::ProfitFactor_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot)
  {
   CoefChart_item item;
   item.DT=deal.DT_close;
   double profit=(isOneLot ? data.oneLot.Accomulated_Profit : data.total.Accomulated_Profit);
   double dd=MathAbs(isOneLot ? data.oneLot.Accomulated_DD : data.total.Accomulated_DD);
   if(dd==0)
      item.coef=0;
   else
      item.coef=profit/dd;
   int s=ArraySize(out);
   ArrayResize(out,s+1,s+1);
   out[s]=item;
  }

En esencia, este método calcula un gráfico con el cambio del factor de beneficio durante las transacciones. La última de estas será precisamente el coeficiente que se representa en el informe de simulación. La fórmula es sencilla = beneficio acumulado / pérdidas acumuladas. Si la reducción es cero, el coeficiente será igual a cero, dado que en aritmética clásica, es imposible dividir por cero sin usar límites, y esta regla se aplica en el leguaje utilizado. Por lo tanto, nosotros vamos a realizar las comprobaciones correspondientes del divisor en todas las operaciones aritméticas.

El factor de recuperación también se calcula de forma análoga:

//+------------------------------------------------------------------+
//| Calculate Recovery Factor                                        |
//+------------------------------------------------------------------+
void CReportCreator::RecoveryFactor_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot)
  {
   CoefChart_item item;
   item.DT=deal.DT_close;
   double pl=(isOneLot ? data.oneLot.PL : data.total.PL);
   double dd=MathAbs(isOneLot ? data.oneLot.Max_DD_byPL : data.total.Max_DD_byPL);
   if(dd==0)
      item.coef=0;//ideally it should be plus infinity
   else
      item.coef=pl/dd;
   int s=ArraySize(out);
   ArrayResize(out,s+1,s+1);
   out[s]=item;
  }

La fórmula de cálculo de este coeficiente es: beneficio por la i-ésima iteración / reducción por la i-ésima iteración. Notemos también que, como el beneficio en el momento del cálculo de este coeficiente puede ser cero o negativo, el propio coeficiente podrá ser cero o negativo.

Coeficiente de ganancia

//+------------------------------------------------------------------+
//| Calculate Win Rate                                               |
//+------------------------------------------------------------------+
void CReportCreator::WinCoef_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot)
  {
   CoefChart_item item;
   item.DT=deal.DT_close;
   double profit=(isOneLot ? data.oneLot.Accomulated_Profit : data.total.Accomulated_Profit);
   double dd=MathAbs(isOneLot ? data.oneLot.Accomulated_DD : data.total.Accomulated_DD);
   int n_profit=(isOneLot ? data.oneLot.Total_Profit_numDeals : data.total.Total_Profit_numDeals);
   int n_dd=(isOneLot ? data.oneLot.Total_DD_numDeals : data.total.Total_DD_numDeals);
   if(n_dd == 0 || n_profit == 0)
      item.coef = 0;
   else
      item.coef=(profit/n_profit)/(dd/n_dd);
   int s=ArraySize(out);
   ArrayResize(out,s+1,s+1);
   out[s]=item;
  }

Fórmula de cálculo del coeficiente de ganancia = (beneficio / número de transacciones rentables) / (reducción / número de transacciones no rentables). Este coeficiente también puede ser negativo si no hay beneficio en el momento del cálculo. 

El coeficiente de Sharpe es un poco más complicado que los anteriormente descritos:

//+------------------------------------------------------------------+
//| Calculate Sharpe Ratio                                           |
//+------------------------------------------------------------------+
double CReportCreator::ShartRatio_calc(PLChart_item &data[])
  {
   int total=ArraySize(data);
   double ans=0;
   if(total>=2)
     {
      double pl_r=0;
      int n=0;
      for(int i=1; i<total; i++)
        {
         if(data[i-1].Profit!=0)
           {
            pl_r+=(data[i].Profit-data[i-1].Profit)/data[i-1].Profit;
            n++;
           }
        }
      if(n>=2)
         pl_r/=(double)n;
      double std=0;
      n=0;
      for(int i=1; i<total; i++)
        {
         if(data[i-1].Profit!=0)
           {
            std+=MathPow((data[i].Profit-data[i-1].Profit)/data[i-1].Profit-pl_r,2);
            n++;
           }
        }
      if(n>=2)
         std=MathSqrt(std/(double)(n-1));

      ans=(std!=0 ?(pl_r-r)/std : 0);
     }
   return ans;
  }

En el primer ciclo, se calcula la rentabilidad media del gráfico PL, donde cada i-ésimo beneficio se calcula como la ratio del incremento sobre el anterior valor de PL respecto al anterior valor de PL. Como ejemplo se ha tomado el modo de normalización de la serie de precios usado para valorar las series temporales. 

A continuación, en el siguiente ciclo, se calcula la volatilidad. Esta se calcula según la misma serie normalizada de beneficios

Finalmente, se calcula el propio coeficiente según la fórmula (beneficio medio - tasa de interés libre de riesgo) / volatilidad (desvío estándar de la rentabilidad).

Es posible que nos hayamos permitido ciertas libertades en este coeficiente en cuanto a la normalización de la serie, y también respecto a las propias fórmulas, pero desde el punto de vista de la lógica, todo parece bastante sólido. Si nos hemos equivocado en algo, o hemos incurrido en algún error intolerable, no dude en notificarlo en los comentarios.

El cálculo del VaR y el gráfico de distribución normal. Esta parte de los cálculos consta de tres métodos. Como siempre, dos de ellos se dedican al cálculo, mientras que el tercero agrega todos los cálculos. Vamos a verlos por orden.

//+------------------------------------------------------------------+
//| Distribution calculation                                         |
//+------------------------------------------------------------------+
void CReportCreator::NormalPDF_chart_calc(DistributionChart &out,PLChart_item &data[])
  {
   double Mx_absolute=0,Mx_growth=0,Std_absolute=0,Std_growth=0;
   int total=ArraySize(data);
   ZeroMemory(out.absolute);
   ZeroMemory(out.growth);
   ZeroMemory(out.absolute.VaR);
   ZeroMemory(out.growth.VaR);
   ArrayFree(out.absolute.distribution);
   ArrayFree(out.growth.distribution);

// Calculation of distribution parameters
   if(total>=2)
     {
      int n=0;
      for(int i=0; i<total; i++)
        {
         Mx_absolute+=data[i].Profit;
         if(i>0 && data[i-1].Profit!=0)
           {
            Mx_growth+=(data[i].Profit-data[i-1].Profit)/data[i-1].Profit;
            n++;
           }
        }
      Mx_absolute/=(double)total;
      if(n>=2)
         Mx_growth/=(double)n;

      n=0;
      for(int i=0; i<total; i++)
        {
         Std_absolute+=MathPow(data[i].Profit-Mx_absolute,2);
         if(i>0 && data[i-1].Profit!=0)
           {
            Std_growth+=MathPow((data[i].Profit-data[i-1].Profit)/data[i-1].Profit-Mx_growth,2);
            n++;
           }
        }
      Std_absolute=MathSqrt(Std_absolute/(double)(total-1));
      if(n>=2)
         Std_growth=MathSqrt(Std_growth/(double)(n-1));

      // Calculate VaR
      out.absolute.VaR.Mx=Mx_absolute;
      out.absolute.VaR.Std=Std_absolute;
      out.absolute.VaR.VAR_90=VaR(Q_90,Mx_absolute,Std_absolute);
      out.absolute.VaR.VAR_95=VaR(Q_95,Mx_absolute,Std_absolute);
      out.absolute.VaR.VAR_99=VaR(Q_99,Mx_absolute,Std_absolute);
      out.growth.VaR.Mx=Mx_growth;
      out.growth.VaR.Std=Std_growth;
      out.growth.VaR.VAR_90=VaR(Q_90,Mx_growth,Std_growth);
      out.growth.VaR.VAR_95=VaR(Q_95,Mx_growth,Std_growth);
      out.growth.VaR.VAR_99=VaR(Q_99,Mx_growth,Std_growth);

      // Calculate distribution
      for(int i=0; i<total; i++)
        {
         Chart_item  item_a,item_g;
         ZeroMemory(item_a);
         ZeroMemory(item_g);
         item_a.x=data[i].Profit;
         item_a.y=PDF_calc(Mx_absolute,Std_absolute,data[i].Profit);
         if(i>0)
           {
            item_g.x=(data[i-1].Profit != 0 ?(data[i].Profit-data[i-1].Profit)/data[i-1].Profit : 0);
            item_g.y=PDF_calc(Mx_growth,Std_growth,item_g.x);
           }
         int s=ArraySize(out.absolute.distribution);
         ArrayResize(out.absolute.distribution,s+1,s+1);
         out.absolute.distribution[s]=item_a;
         s=ArraySize(out.growth.distribution);
         ArrayResize(out.growth.distribution,s+1,s+1);
         out.growth.distribution[s]=item_g;
        }
      // Ascending
      sorter.Sort<Chart_item>(out.absolute.distribution,&chartComparer);
      sorter.Sort<Chart_item>(out.growth.distribution,&chartComparer);
     }
  }
//+------------------------------------------------------------------+
//| Calculate VaR                                                    |
//+------------------------------------------------------------------+
double CReportCreator::VaR(double quantile,double Mx,double Std)
  {
   return Mx-quantile*Std;
  }
//+------------------------------------------------------------------+
//| Distribution calculation                                         |
//+------------------------------------------------------------------+
double CReportCreator::PDF_calc(double Mx,double Std,double x)
  {
   if(Std!=0)
      return MathExp(-0.5*MathPow((x-Mx)/Std,2))/(MathSqrt(2*M_PI)*Std);
   else
      return 0;
  }

El método de cálculo del VaR es el más sencillo: usa en sus cálculos el modelo del VaR histórico, como hemos mencionado anteriormente en el artículo.

El método de cálculo para la distribución normal, para mayor precisión, ha sido tomado por completo del paquete de análisis estadístico Matlab

El método para el cálculo de la distribución normal y la construcción de su gráfico es un método agregador en el que se aplican todos los métodos anteriormente descritos. En el primer ciclo, se calcula el valor medio del beneficio, mientras que en el segundo, se realiza el cálculo de la desviación media cuadrática de la rentabilidad. En este caso, además, la propia rentabilidad para el gráfico y el VaR (calculado según los incrementos), se calculanotra vez como una serie temporal normalizada. A continuación, después de rellenar los indicadores del VaR, se calcula el gráfico de distribución normal con ayuda del método mencionado. Como valores del eje x se usan las ya mencionadas rentabilidades para el gráfico calculado según los incrementos, así como los valores absolutos de beneficio para el gráfico calculado según los beneficios.

El cálculo de la puntuación Z también es bastante trivial: la fórmula se ha tomado de uno de los artículos escritos en este sitio web. Por ello, consideramos posible omitir el código con su implementación, pero usted podrá leerlo en los archivos adjuntos. 

Como finalización de la descripción de esta clase, indicaremos que todos los cálculos comienzan por el método Calculate con la siguiente signatura de llamada

void CReportCreator::Create(DealDetales &history[],DealDetales &BH_history[],const double _balance,const string &Symb[],double _r);

Su implementación se ha analizado en el artículo "Las 100 mejores pasadas de optimización", por eso no la mostraremos en este. Los métodos públicos tampoco son de gran interés, ya que no realizan ningún trabajo lógico, solo sirven de getters que generan los datos solicitados de acuerdo con los parámetros de entrada que indican el tipo de información necesaria.  

Conclusión

Tras analizar en el artículo anterior el proceso de escritura de una biblioteca en el lenguaje C#, hemos pasado a la siguiente etapa: la creación de un informe comercial que precisamente se descargará con la ayuda de la biblioteca DLL desarrollada y los mecanismos que veremos en el siguiente artículo. El propio mecanismo de generación de informes, como hemos mencionado, ha sido tomado de desarrollos anteriores, pero desde que fueron creados, han tenido una serie de mejoras. En el presente artículo, hemos mostrado las versiones más recientes de los desarrollos. Asimismo, la solución ofrecida se ha puesto a prueba durante varios meses de optimizaciones y descargas de informes.

 

En el fichero adjunto se encuentran dos carpetas, ambas deberán ser descomprimidas en el directorio MQL/Include. 

El fichero contiene los siguientes archivos:

  1. CustomGeneric
    • GenericSorter.mqh
    • ICustomComparer.mqh
  2. History manager
    • CustomComissionManager.mqh
    • DealHistoryGetter.mqh
    • ReportCreator.mqh

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

Archivos adjuntos |
Include.zip (23.27 KB)
SQLite: trabajo nativo con bases de datos en SQL en MQL5 SQLite: trabajo nativo con bases de datos en SQL en MQL5

El desarrollo de estrategias comerciales está relacionado con el procesamiento de grandes volúmenes de datos. Ahora, usted podrá trabajar directamente en MQL5 con bases de datos con la ayuda de solicitudes SQL basadas en SQLite. Una ventaja importante de este motor es que toda la base de datos se encuentra en un único archivo estándar, ubicado en la computadora del usuario.

Monitoreo multidivisas de las señales comerciales (Parte 2): Implementando la parte visual de la aplicación Monitoreo multidivisas de las señales comerciales (Parte 2): Implementando la parte visual de la aplicación

En el artículo anterior, creamos la plantilla de la aplicación en la que se basará todo nuestro trabajo siguiente. Ahora, pasaremos paso a paso a su desarrollo, es decir, diseñaremos la parte visual de la aplicación y configuraremos las interacciones básicas entre los controles de la interfaz.

Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte XXVII): Trabajando con las solicitudes comerciales - Colocación de órdenes pendientes Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte XXVII): Trabajando con las solicitudes comerciales - Colocación de órdenes pendientes

En el presente artículo, continuaremos trabajando con las solicitudes comerciales e implementaremos la colocación de órdenes pendientes. Asimismo, corregiremos algunos errores localizados en el funcionamiento de la clase comercial.

Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte XXVIII): Solicitudes comerciales pendientes - Cierre, eliminación y modificación Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte XXVIII): Solicitudes comerciales pendientes - Cierre, eliminación y modificación

Este es el tercer artíclo sobre el concepto de las solicitudes pendientes. En él, terminaremos con la puesta a punto del trabajo con solicitudes comerciales pendientes, creando los métodos para cerrar posiciones, eliminar órdenes pendientes y modificar los parámetros de las posiciones y las órdenes pendientes.