English Русский 中文 Deutsch 日本語 Português
preview
Valoración visual de los resultados de optimización

Valoración visual de los resultados de optimización

MetaTrader 5Probador | 3 marzo 2022, 11:16
780 0
Aleksandr Slavskii
Aleksandr Slavskii

Introducción

Los criterios de optimización personalizados suponen una configuración muy cómoda y flexible para optimizar los asesores expertos, pero para verificar varios criterios, necesitamos ejecutar varias optimizaciones, lo cual lleva mucho tiempo. Sería magnífico poder comprobar varios criterios personalizados al mismo tiempo en una optimización. Y no solo comprobar dichos criterios, sino también ver inmediatamente el gráfico de balance y equidad.

Parafraseando al famoso escritor Sergei Mikhalkov, podemos decir que se necesitan diferentes visualizaciones, que tener diferentes visualizaciones es importante. Y esta sería una afirmación cierta, ya que más del ochenta por ciento de la información que recibe nuestro cerebro nos llega a través de los ojos. Por eso, la conversación en este artículo se centrará en cómo crear gráficos para todas las pasadas de optimización y elegir el criterio personalizado óptimo.

Y también sobre cómo, teniendo un conocimiento mínimo de MQL5 y un gran ánimo de trabajar, usando los artículos del sitio y los comentarios en el foro, podremos escribir lo que queramos.


Establecemos la tarea

  1. Recopilar los datos de cada pasada de optimización.
  2. Crear los gráficos de balance/equidad para cada pasada de optimización.
  3. Calcular múltiples criterios personalizados para la optimización.
  4. Clasificar los gráficos según un criterio de optimización personalizado ascendente.
  5. Mostrar los mejores resultados de todos los criterios personalizados.


Pasos para solucionar la tarea

Como no podemos evitar realizar cambios en el código del EA, trataremos al menos de minimizar estos cambios.

  • Por consiguiente, todo el código de recopilación de datos se escribirá en el archivo de inclusión SkrShotOpt.mqh; el criterio personalizado se calculará en el archivo CustomCriterion.mqh.
  • El script ScreenShotOptimization.mq5 dibujará los gráficos y guardará las capturas de pantalla.

Por ello, solo necesitaremos añadir unas pocas líneas de código al EA.


1. Recopilación de datos.   SkrShotOpt.mqh

La función OnTick() guardará los valores de equidad máximos y mínimos.

   double _Equity = AccountInfoDouble(ACCOUNT_EQUITY);
   if(tempEquityMax < _Equity)
      tempEquityMax = _Equity;
   if(tempEquityMin > _Equity)
      tempEquityMin = _Equity;

Para no comprobar los cambios de posición en cada tick, monitorearemos los cambios de posición en la función OnTradeTransaction()

void IsOnTradeTransaction(const MqlTradeTransaction & trans,
                          const MqlTradeRequest & request,
                          const MqlTradeResult & result)
  {
   if(trans.type == TRADE_TRANSACTION_DEAL_ADD)
      if(HistoryDealSelect(trans.deal))
        {
         if(_deal_entry != DEAL_ENTRY_OUT && _deal_entry != DEAL_ENTRY_OUT_BY)
            _deal_entry = HistoryDealGetInteger(trans.deal, DEAL_ENTRY);
         if(trans.deal_type == DEAL_TYPE_BUY || trans.deal_type == DEAL_TYPE_SELL)
            if(_deal_entry == DEAL_ENTRY_IN || _deal_entry == DEAL_ENTRY_OUT || _deal_entry == DEAL_ENTRY_INOUT || _deal_entry == DEAL_ENTRY_OUT_BY)
               allowed = true;
        }
  }

Al cambiar el número de transacciones abiertas, rellenamos las matrices de balance y equidad.

   if(allowed) // if there was a trade
     {
      double accBalance = AccountInfoDouble(ACCOUNT_BALANCE);
      double accEquity = AccountInfoDouble(ACCOUNT_EQUITY);

      ArrayResize(balance, _size + 1);
      ArrayResize(equity, _size + 1);
      balance[_size] = accBalance;

      if(_deal_entry != DEAL_ENTRY_OUT && _deal_entry != DEAL_ENTRY_OUT_BY) // if a new position appeared
         equity[_size] = accEquity;
      else // if position closed
        {
         if(changesB < accBalance)
            equity[_size] = tempEquityMin;
         else
            switch(s_view)
              {
               case  min_max_E:
                  equity[_size] = tempEquityMax;
                  break;
               default:
                  equity[_size] = tempEquityMin;
                  break;
              }
         tempEquityMax = accEquity;
         tempEquityMin = accEquity;
        }

      _size = _size + 1;
      changesPos = PositionsTotal();
      changesB = accBalance;
      _deal_entry = -1;
      allowed = false;
     }

Como el archivo con frames no es elástico y puede hincharse hasta alcanzar tamaños imposibles de procesar con una gran cantidad de transacciones, deberemos escribir el mínimo necesario de información.

Al abrir una transacción, escribimos el valor del balance y la equidad:

  • al cerrar, escribimos el valor de la equidad máxima: si la transacción se ha cerrado con pérdidas,
  • el valor de equidad mínimo, lo escribimos si la transacción se ha cerrado con beneficios. 

Por lo tanto, casi todas las transacciones tienen cuatro valores guardados en las matrices: el balance y la equidad al abrirse la transacción, y el balance y la equidad máximos/mínimos al cerrar la misma.

A veces sucede que en un tick se cierra una posición y se abre otra: en este caso, no se registrarán dos posiciones, sino solo una. Esto no afectará a los cálculos y a la visualización de los gráficos, pero las matrices sí disminuirán considerablemente.

 

Guardar los datos recopilados en un archivo

Tiene sentido recopilar solo las pasadas de optimización rentables. Este parámetro se ha sacado a los ajustes, por lo que si lo deseamos, también podremos registrar las transacciones no rentables. Todas las pasadas Forward también son registradas.

Los datos recopilados se guardarán en un archivo usando la función FrameAdd(); al final de cada ejecución individual, al darse el evento Tester, este evento será procesado por la función OnTester().

bool  FrameAdd( 
   const string  name,        // public name/tag
   long          id,          // public id 
   double        value,       // value
   const void&   data[]       // array of any type
   );

Podemos encontrar un ejemplo claro y comprensible sobre el trabajo con la función FrameAdd() aquí:  https://www.mql5.com/ru/forum/11277/page4#comment_469771

Dado que FrameAdd() puede escribir solo una matriz y un valor numérico value, y además del balance y la equidad, queremos transmitir todos los valores de la enumeración ENUM_STATISTICS para un análisis más detallado, hemos decidido escribir secuencialmente todo en una matriz y escribir el tamaño de la matriz en el valor value numérico transmitido.

   if(id == 1)  // if it is a backward pass
     {
      // if profit % and the number of trades exceed those specified in the settings, the pass is written into the file
      if(TesterStatistics(STAT_PROFIT) / TesterStatistics(STAT_INITIAL_DEPOSIT) * 100 > _profit && TesterStatistics(STAT_TRADES) >= trades)
        {
         double TeSt[42]; // total number of elements in the ENUM_STATISTICS enumeration is 41
         IsRecordStat(TeSt); // writing testing statistics to the array
         IsCorrect(); // adjusting balance and equity arrays

         if(m_sort != none)
           {
            while((sort)size_sort != none)
               size_sort++;
            double LRB[], LRE[], coeff[];
            Coeff = Criterion(balance, equity, LRB, LRE, TeSt, coeff, 3);// calculating custom criterion
            ArrayInsert(balance, equity, _size + 1, 0);     // joining balance and equity arrays into one
            ArrayInsert(balance, TeSt, (_size + 1) * 2, 0); // add to the resulting array the array with the ENUM_STATISTICS data
            FrameAdd(name, id, _size + 1, balance);         // write the frame into the file
           }
         else
           {
            ArrayInsert(balance, equity, _size + 1, 0);     // joining balance and equity arrays into one
            ArrayInsert(balance, TeSt, (_size + 1) * 2, 0); // add to the resulting array the array with the ENUM_STATISTICS data
            FrameAdd(name, id, _size + 1, balance);         // write the frame into the file
           }
        }
     }

Las pasadas Forward se procesan de la misma manera que las pasadas Back, pero en realidad son resultado de la optimización, por lo que solo se guardarán los valores de balance y equidad para ellas, y no habrá valor de enumeración ENUM_STATISTICS.

A veces ocurre que al final de la prueba hay una posición abierta, en cuyo caso el simulador la cerrará él mismo.

Por ello, cerraremos la transicción virtualmente (escribiremos el balance y la equidad actuales) si la variable que almacena el número de transacciones abiertas es distinta a cero al final de la prueba.

void IsCorrect()
  {
   if(changesPos > 0) // if there is an open position by the testing end time, it should be virtually closed as the tester will close such a position
     {
      _size++;
      ArrayResize(balance, _size + 1);
      ArrayResize(equity, _size + 1);
      if(balance[_size - 2] > AccountInfoDouble(ACCOUNT_BALANCE))
        {
         balance[_size - 1] = AccountInfoDouble(ACCOUNT_BALANCE);
         switch(s_view)
           {
            case  min_max_E:
               equity[_size - 1] = tempEquityMax;
               break;
            default:
               equity[_size - 1] = tempEquityMin;
               break;
           }
        }
      else
        {
         balance[_size - 1] = AccountInfoDouble(ACCOUNT_BALANCE);
         equity[_size - 1] = tempEquityMin;
        }
      balance[_size] = AccountInfoDouble(ACCOUNT_BALANCE);
      equity[_size] = AccountInfoDouble(ACCOUNT_EQUITY);
     }
   else
     {
      ArrayResize(balance, _size + 1);
      ArrayResize(equity, _size + 1);
      balance[_size] = AccountInfoDouble(ACCOUNT_BALANCE);
      equity[_size] = AccountInfoDouble(ACCOUNT_EQUITY);
     }
  }

Con esto, damos por completa la escritura de datos.


Lectura de datos de un archivo. ScreenShotOptimization.mq5

Después de la optimización, creamos un archivo con frames, ubicado en la ruta: C:\Users\username\AppData\Roaming\MetaQuotes\Terminal\terminal ID\MQL5\Files\Tester con la extensión: EA name.symbol.timeframe.mqd. Inmediatamente después de la optimización, no tendremos acceso a este archivo, pero después de reiniciar el terminal, podremos acceder al archivo usando las funciones de archivo habituales.

Buscamos el archivo necesario con frames, que se encuentra en la carpeta C:\Users\username\AppData\Roaming\MetaQuotes\Terminal\terminal ID\MQL5\Files\Tester.

   int count = 0;
   long search_handle = FileFindFirst("Tester\\*.mqd", FileName);
   do
     {
      if(FileName != "")
         count++;
      FileName = "Tester\\" + FileName;
     }
   while(FileFindNext(search_handle, FileName));
   FileFindClose(search_handle);

Primero, la lectura es integrada en la estructura.

FRAME Frame = {0};
FileReadStruct(handle, Frame);
struct FRAME
  {
   ulong             Pass;
   long              ID;
   short             String[64];
   double            Value;
   int               SizeOfArray;
   long              Tmp[2];

   void              GetArrayB(int handle, Data & m_FB)
     {
      ArrayFree(m_FB.Balance);
      FileReadArray(handle, m_FB.Balance, 0, (int)Value);
      ArrayFree(m_FB.Equity);
      FileReadArray(handle, m_FB.Equity, 0, (int)Value);
      ArrayFree(m_FB.TeSt);
      FileReadArray(handle, m_FB.TeSt, 0, (SizeOfArray / sizeof(m_FB.TeSt[0]) - (int)Value * 2));
     }
   void              GetArrayF(int handle, Data & m_FB, int size)
     {
      FileReadArray(handle, m_FB.Balance, size, (int)Value);
      FileReadArray(handle, m_FB.Equity, size, (int)Value);
     }
  };

En las funciones de la estructura FRAME, se rellenan las matrices de la estructura Data, a partir de las cuales se construyen posteriormente los gráficos.

struct Data
  {
   ulong             Pass;
   long              id;
   int               size;
   double            Balance[];
   double            Equity[];
   double            LRegressB[];
   double            LRegressE[];
   double            coeff[];
   double            TeSt[];
  };
Data                 m_Data[];

Como dibujar varios miles de capturas de pantalla ocupa mucho tiempo, indicaremos en los ajustes del script un parámetro para que no sea posible guardar capturas de pantalla con ingresos inferiores a un cierto porcentaje.

El archivo completo con frames se procesa en un ciclo. 

Para dibujar un gráfico, necesitaremos una matriz con datos, por lo que primero registraremos todas las pasadas back que cumplan con el porcentaje de beneficio indicado en la configuración del script.

Luego, comenzaremos a iterar por todas las pasadas Back, y las pasadas Forward serán seleccionadas para ellas según el número de pasada: la matriz de balance de la pasada Forward se añadirá a la matriz de balance de la pasada Back.

También será posible dibujar dos tipos de gráficos: uno será igual al del simulador de estrategias, es decir, la pasada Forward comenzará con el depósito inicial.

La segunda versión de la pasada Forward comenzará con el depósito con el que finalizó la pasada Back. En este caso, el valor de beneficio de la pasada Back se suma a los valores de balance y equidad de la pasada Forward, y también se suma al final de la matriz con la pasada Back.

Obviamente, se hará de esta forma si la optimización se ha realizado con un periodo Forward.

   int handle = FileOpen(FileName, FILE_READ | FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_BIN);
   if(handle != INVALID_HANDLE)
     {
      FileSeek(handle, 260, SEEK_SET);

      while(Res && !IsStopped())
        {
         FRAME Frame = {0};
         // read from the file to the Frame structure
         Res = (FileReadStruct(handle, Frame) == sizeof(Frame));
         if(Res)
            if(Frame.ID == 1) // if it is a Backward pass, write data to the m_Data structure
              {
               ArrayResize(m_Data, size + 1);
               m_Data[size].Pass = Frame.Pass;
               m_Data[size].id = Frame.ID;
               m_Data[size].size = (int)Frame.Value;
               Frame.GetArrayB(handle, m_Data[size]);  // write data to the m_Data structure arrays
               // if profit of this pass corresponds to the input settings, immediately calculate optimization criteria
               if(m_Data[size].TeSt[STAT_PROFIT] / m_Data[size].TeSt[STAT_INITIAL_DEPOSIT] * 100 >= profitPersent)
                 {
                  Criterion(m_Data[size].Balance, m_Data[size].Equity, m_Data[size].LRegressB, m_Data[size].LRegressE, m_Data[size].TeSt, m_Data[size].coeff, m_lineR);
                  size++;
                 }
              }
            else  // if it is a Forward pass, write to the end of the m_Data data structures
               if(m_Forward != BackOnly) // if drawing of only Backward passes is not selected in settings
                  for(int i = 0; i < size; i++)
                    {
                     if(Frame.Pass == m_Data[i].Pass) // if Back and Forward pass numbers match
                       {
                        int m = 0;
                        if(m_Forward == Back_Next_Forward) // if selected drawing of Forward graph as a continuation of Backward
                          {
                           Frame.GetArrayF(handle, m_Data[i], m_Data[i].size - 1); // write data at the end of the m_Data structure array, with a one-trade shift
                           for(int x = m_Data[i].size - 1; x < m_Data[i].size + (int)Frame.Value - 1; x++)
                             {
                              m_Data[i].Balance[x] = m_Data[i].Balance[x] + m_Data[i].TeSt[STAT_PROFIT]; //  add profit of the Backward test to the Forward pass
                              m_Data[i].Equity[x] = m_Data[i].Equity[x] + m_Data[i].TeSt[STAT_PROFIT];
                             }
                           m = 1;
                          }
                        else
                           Frame.GetArrayF(handle, m_Data[i], m_Data[i].size); // if drawing of a Forward pass from a starting balance is selected

                        m_Data[i].coeff[Forward_Trade] = (int)(Frame.Value / 2); // number of forward trades (not exact))
                        m_Data[i].coeff[Profit_Forward] = m_Data[i].Balance[m_Data[i].size + (int)Frame.Value - m - 1] - m_Data[i].Balance[m_Data[i].size - m];
                        break;
                       }
                     if(i == size - 1) // if no Backward is found for this Forward pass, move the file pointer to the end of writing
                        FileSeek(handle, Frame.SizeOfArray, SEEK_CUR); // of this frame as if we read array data from the file
                    }
        }
      FileClose(handle);
      //---


Construcción de gráficos

Función de construcción de gráficos.

string _GraphPlot(double& y1[],
                  double& y2[],
                  double& LRegressB[],
                  double& LRegressE[],
                  double& coeff[],
                  double& TeSt[],
                  ulong pass)
  {
   CGraphic graphic;
//--- create graphic
   bool res = false;
   if(ObjectFind(0, "Graphic") >= 0)
      res = graphic.Attach(0, "Graphic");
   else
      res = graphic.Create(0, "Graphic", 0, 0, 0, _width, _height);

   if(!res)
      return(NULL);

   graphic.BackgroundMain(FolderName);  // print the Expert Advisor name
   graphic.BackgroundMainSize(FontSet + 1); // font size for the Expert Advisor name

   graphic.IndentLeft(FontSet);
   graphic.HistoryNameSize(FontSet); // font size for the line names
   graphic.HistorySymbolSize(FontSet);

   graphic.XAxis().Name("pass " + IntegerToString(pass)); // show the pass number along the X axis
   graphic.XAxis().NameSize(FontSet + 1);

   graphic.XAxis().ValuesSize(12); // price font size
   graphic.YAxis().ValuesSize(12);

//--- add curves
   CCurve *curve = graphic.CurveAdd(y1, ColorToARGB(clrBlue), CURVE_POINTS_AND_LINES, "Balance"); // plot the balance graph
   curve.LinesWidth(widthL);  // graph line width
   curve.PointsSize(widthL + 1); // size of dots on the balance graph

   CCurve *curve1 = graphic.CurveAdd(y2, ColorToARGB(clrGreen), CURVE_LINES, "Equity");  // plot the equity graph
   curve1.LinesWidth(widthL);

   int size = 0;
   switch(m_lineR) // plot the regression line
     {
      case  lineR_Balance: // balance regression line
        {
         size = ArraySize(LRegressB);
         CCurve *curve2 = graphic.CurveAdd(LRegressB, ColorToARGB(clrBlue), CURVE_LINES, "LineR_Balance");
         curve2.LinesWidth(widthL);
        }
      break;
      case  lineR_Equity: // equity regression line
        {
         size = ArraySize(LRegressE);
         CCurve *curve2 = graphic.CurveAdd(LRegressE, ColorToARGB(clrRed), CURVE_LINES, "LineR_Equity");
         curve2.LinesWidth(widthL);
        }
      break;
      case  lineR_BalanceEquity: // balance and equity regression line
        {
         size = ArraySize(LRegressB);
         CCurve *curve2 = graphic.CurveAdd(LRegressB, ColorToARGB(clrBlue), CURVE_LINES, "LineR_Balance");
         curve2.LinesWidth(widthL);

         CCurve *curve3 = graphic.CurveAdd(LRegressE, ColorToARGB(clrRed), CURVE_LINES, "LineR_Equity");
         curve2.LinesWidth(widthL);
        }
      break;
      default:
         break;
     }
//--- plot curves
   graphic.CurvePlotAll();

// Important!!!  All lines and captions must be created after creating the graph; otherwise, the graph will override them

   if(size == 0)
     {
      size = ArraySize(LRegressE);
      if(size == 0)
         size = ArraySize(LRegressB);
     }

   int x1 = graphic.ScaleX(size - 1); //Scales the value of the number of trades along the X axis
   graphic.LineAdd(x1, 30, x1, _height - 45, ColorToARGB(clrBlue), LINE_END_BUTT); // construct the vertical line denoting the end of the Backward period

   string txt = "";
   int txt_x = 70;// text indent along the X axis
   int txt_y = 30;// text indent along the Y axis

   graphic.FontSet("Arial", FontSet);// Set current font parameters

   for(int i = 0; i < size_sort; i++)  // Write all coefficients and criteria on the chart
     {
      if(coeff[i] == 0)
         continue;
      if(i == 1 || i == 3)
         txt = StringFormat("%s = %d", EnumToString((sort)i), (int)coeff[i]);
      else
         if(i == 0 || i == 2)
            txt = StringFormat("%s = %.2f", EnumToString((sort)i), coeff[i]);
         else
            txt = StringFormat("%s = %.4f", EnumToString((sort)i), coeff[i]);
      graphic.TextAdd(txt_x, txt_y + FontSet * i, txt, ColorToARGB(clrGreen));
     }

   txt_y = txt_y + FontSet * (size_sort - 1);
   txt = StringFormat("Profitability = %.2f", TeSt[STAT_PROFIT_FACTOR]);
   graphic.TextAdd(txt_x, txt_y + FontSet, txt, ColorToARGB(clrGreen));
   txt = StringFormat("Expected  payoff = %.2f", TeSt[STAT_EXPECTED_PAYOFF]);
   graphic.TextAdd(txt_x, txt_y + FontSet * 2, txt, ColorToARGB(clrGreen));

   graphic.Update();
//--- return resource name
   return graphic.ChartObjectName();
  }


Podrá leer cómo trabajar con CGraphic en los artículos siguientes:


Las capturas de pantalla de los gráficos se guardan en una carpeta aparte en la carpeta Files. El nombre de la carpeta contiene: EA name.symbol.tameframe.

bool BitmapObjectToFile(const string ObjName, const string _FileName, const bool FullImage = true)
  {
   if(ObjName == "")
      return(true);

   const ENUM_OBJECT Type = (ENUM_OBJECT)ObjectGetInteger(0, ObjName, OBJPROP_TYPE);
   bool Res = (Type == OBJ_BITMAP_LABEL) || (Type == OBJ_BITMAP);

   if(Res)
     {
      const string Name = __FUNCTION__ + (string)MathRand();

      ObjectCreate(0, Name, OBJ_CHART, 0, 0, 0);
      ObjectSetInteger(0, Name, OBJPROP_XDISTANCE, -5e3);

      const long chart = ObjectGetInteger(0, Name, OBJPROP_CHART_ID);

      Res = ChartSetInteger(chart, CHART_SHOW, false) && ObjectCreate(chart, Name, OBJ_BITMAP_LABEL, 0, 0, 0) &&
            ObjectSetString(chart, Name, OBJPROP_BMPFILE, ObjectGetString(0, ObjName, OBJPROP_BMPFILE)) &&
            (FullImage || (ObjectSetInteger(chart, Name, OBJPROP_XSIZE, ObjectGetInteger(0, ObjName, OBJPROP_XSIZE)) &&
                           ObjectSetInteger(chart, Name, OBJPROP_YSIZE, ObjectGetInteger(0, ObjName, OBJPROP_YSIZE)) &&
                           ObjectSetInteger(chart, Name, OBJPROP_XOFFSET, ObjectGetInteger(0, ObjName, OBJPROP_XOFFSET)) &&
                           ObjectSetInteger(chart, Name, OBJPROP_YOFFSET, ObjectGetInteger(0, ObjName, OBJPROP_YOFFSET)))) &&
            ChartScreenShot(chart, FolderName + "\\" + _FileName, (int)ObjectGetInteger(chart, Name, OBJPROP_XSIZE), (int)ObjectGetInteger(chart, Name, OBJPROP_YSIZE));
      ObjectDelete(0, Name);
     }

   return(Res);
  }


Aquí están los gráficos resultantes.

Este es el aspecto de los gráficos en la carpeta.

Si seleccionamos para imprimir todas las capturas de pantalla, los nombres de las capturas de pantalla constarán de: el criterio personalizado seleccionado para la clasificación + el beneficio + el número de pasada. 

Si seleccionamos la impresión de las mejores pasadas, los nombres de las capturas de pantalla constarán de: el criterio personalizado + el beneficio.

Así es como se ve el gráfico dibujado por el script.


Y, a modo de comparación, el mismo gráfico del simulador de estrategias


Aquí he mostrado los gráficos más similares, pero en la mayoría de los casos los gráficos serán muy diferentes a los de prueba. En primer lugar, porque en el gráfico del simulador, las transacciones en el eje X están vinculadas al tiempo, y el script dibuja gráficos en los que el eje X está vinculado al número de transacciones. Además, como debemos escribir un mínimo de información en el frame para no hinchar el archivo, los valores de equidad claramente no serán suficientes para analizar al completo los gráficos dibujados por el script, pero, al mismo tiempo, esta información bastará para lograr una valoración inicial de la efectividad de la optimización de una pasada particular y también para calcular un criterio de optimización personalizado.

¡Después de la optimización, deberemos reiniciar el terminal antes de ejecutar el script ScreenShotOptimization!

Inicialmente, solo queríamos ver todos los gráficos de optimización, pero cuando implementamos esto y vimos siete mil capturas de pantalla en la carpeta, quedó claro que era imposible trabajar con tantas. Debemos elegir lo mejor de alguna forma, en función de algunos criterios.

Ya notamos hace mucho que los tráders algorítmicos se dividen en dos categorías:

  1. Los que creen que el asesor debe optimizarse durante un periodo muy largo (de varios años y, preferiblemente, de varias décadas), y que luego este debería funcionar.
  2. Y los que creen que el asesor debe volver a optimizarse periódicamente en un periodo corto (por ejemplo, un mes de optimización + una semana de trading o tres meses de optimización + un mes de trading), o con cualquier otro orden de reoptimización.

Un servidor pertenece al segundo tipo.

Por eso, hemos decidido buscar criterios de optimización personalizados que sirvan de filtro para seleccionar las mejores pasadas.


Creación de criterios de optimización personalizados

Los cálculos de todos los criterios de optimización personalizados se realizarán usando el archivo de inclusión aparte CustomCriterion.mqh, ya que todos estos cálculos se utilizarán tanto en el funcionamiento del script a la hora de dibujar gráficos como en el funcionamiento del asesor experto optimizado.

Antes de alcanzar un criterio de optimización propio, el autor leyó todo lo que encontró sobre este tema en el sitio web.

R cuadrado como evaluación de la calidad de la curva del balance de la estrategia

El artículo describe con gran detalle qué es la regresión lineal y cómo calcularla utilizando la biblioteca AlgLib; asimismo se da una descripción muy detallada y accesible del coeficiente de determinación R^2 y su aplicación en la valoración de los resultados de las pruebas. Recomendamos encarecidamente su lectura.

Función para calcular la regresión lineal, R^2, y también ProfitStability:

void Coeff(double& Array[], double& LR[], double& coeff[], double& TeSt[], int total, int c)
  {
//-- Fill the matrix: Y - Array value, X - ordinal number of the value
   CMatrixDouble xy(total, 2);
   for(int i = 0; i < total; i++)
     {
      xy[i].Set(0, i);
      xy[i].Set(1, Array[i]);
     }

//-- Find coefficients a and b of the linear model y = a*x + b;
   int retcode = 0;
   double a, b;
   CLinReg::LRLine(xy, total, retcode, b, a);

//-- Generate the linear regression values for each X;
   ArrayResize(LR, total);
   for(int x = 0; x < total; x++)
      LR[x] = x * a + b;

   if(m_calc == c)
     {
      //-- Find the coefficient of correlation of values with their linear regression
      corr = CAlglib::PearsonCorr2(Array, LR);

      //-- Find R^2 and its sign
      coeff[r2] = MathPow(corr, 2.0);
      int sign = 1;
      if(Array[0] > Array[total - 1])
         sign = -1;
      coeff[r2] *= sign;

      //-- Find LR Standard Error
      if(total - 2 == 0)
         stand_err = 0;
      else
        {
         for(int i = 0; i < total; i++)
           {
            double delta = MathAbs(Array[i] - LR[i]);
            stand_err =  stand_err + delta * delta;
           }
         stand_err = MathSqrt(stand_err / (total - 2));
        }
     }
//-- Find ProfitStability = Profit_LR/stand_err
   if(stand_err == 0)
      coeff[ProfitStability] = 0;
   else
      coeff[ProfitStability] = (LR[total - 1] - LR[0]) / stand_err;
  }


Optimizamos la estrategia usando el gráfico del balance y comparamos los resultados con el criterio "Balance + max Sharpe Ratio"

De este artículo, tomamos el cálculo del criterio de optimización personalizado, ProfitStability. Es fácil de calcular: primero, calculamos LR Standard error: la desviación promedio de la línea de regresión del gráfico de balance o equidad. Luego, restamos el valor inicial del valor final de la línea de regresión, así obtenemos TrendProfit.

ProfitStability se calcula como la relación entre TrendProfit y LR Standard error.

En el artículo se describen con detalle todos los pros y contras de este criterio de optimización, y también se realizan muchas pruebas para comparar ProfitStability con otros criterios de optimización.

Como la regresión lineal se puede calcular tanto a partir del balance como de la equidad, y ProfitStability está vinculado al cálculo de regresión lineal, el cálculo de ProfitStability se trasladará a la función de cálculo de regresión lineal.


Crear Criterios Personalizados de Optimización de Asesores Expertos

El artículo es bastante antiguo, de 2011, pero resulta interesante y conserva su actualidad. De ahí tomamos el cálculo del factor de seguridad del sistema comercial (TSSF)

TSSF = Avg.Win / Avg.Loss ((110% - %Win) / (%Win-10%) + 1)

   if(TeSt[STAT_PROFIT_TRADES] == 0 || TeSt[STAT_LOSS_TRADES] == 0 || TeSt[STAT_TRADES] == 0)
      coeff[TSSF] = 0;
   else
     {
      double  avg_win = TeSt[STAT_GROSS_PROFIT] / TeSt[STAT_PROFIT_TRADES];
      double  avg_loss = -TeSt[STAT_GROSS_LOSS] / TeSt[STAT_LOSS_TRADES];
      double  win_perc = 100.0 * TeSt[STAT_PROFIT_TRADES] / TeSt[STAT_TRADES];
      //  Calculate the secure ratio for this percentage of profitable deals:
      if((win_perc - 10.0) + 1.0 == 0)
         coeff[TSSF] = 0;
      else
        {
         double  teor = (110.0 - win_perc) / (win_perc - 10.0) + 1.0;
         //  Calculate the real ratio:
         double  real = avg_win / avg_loss;
         if(teor != 0)
            coeff[TSSF] = real / teor;
         else
            coeff[TSSF] = 0;
        }
     }


Enfoque ideal sobre el desarrollo y el análisis de sistemas comerciales

De este artículo tomamos el Factor Lineal (LinearFactor), que se calcula de la siguiente manera:

  • LinearFactor = MaxDeviation/EndBalance
  • MaxDeviaton = Max(MathAbs(Balance[i]-AverageLine))
  • AverageLine=StartBalance+K*i
  • K=(EndBalance-StartBalance)/n
  • n - número de transacciones en la prueba

Se pueden encontrar más detalles en el artículo del autor. El artículo es muy interesante, y aunque fue criticado considerablemente en los comentarios (en nuestra humilde opinión, irracionalmente), podemos aprender muchas cosas útiles de él.

Mirando un poco hacia adelante, diremos que no hemos encontrado un "criterio de optimización personalizado" universal adecuado para cualquier Asesor Experto. Para diferentes Asesores Expertos, son diferentes criterios los que dan los mejores resultados.

En algunos asesores expertos, LinearFactor arrojó resultados maravillosos.

   double MaxDeviaton = 0;
   double K = (Balance[total - 1] - Balance[0]) / total;
   for(int i = 0; i < total; i++)
     {
      if(i == 0)
         MaxDeviaton = MathAbs(Balance[i] - (Balance[0] + K * i));
      else
         if(MathAbs(Balance[i] - (Balance[0] + K * i) > MaxDeviaton))
            MaxDeviaton = MathAbs(Balance[i] - (Balance[0] + K * i));
     }
   if(MaxDeviaton ==0 || Balance[0] == 0)
      coeff[LinearFactor] = 0;
   else
      coeff[LinearFactor] = 1 / (MaxDeviaton / Balance[0]);

En un intercambio personal de mensajes, el autor dijo que este criterio se puede fortalecer aún más, e incluso explicó cómo, pero no pudimos traducir estos consejos a un código.


De esta forma, conseguimos añadir cuatro criterios de optimización personalizados al código.

  1. R^2 - coeficiente de determinación.
  2. ProfitStability.
  3. TSSF - coeficiente de seguridad del sistema comercial.
  4. LinearFactor.

Así, ahora añadimos todos estos criterios de optimización a nuestro proyecto.

Es una pena que no hayamos podido añadir el "Máximo del criterio complejo", pero no hemos logrado averiguar cómo se calcula. 


Nuestro criterio de optimización

Y ahora, usando como base todos estos artículos, podemos comenzar a crear nuestro propio criterio de optimización.

¿Qué gráfico de balance nos gustaría ver? Obviamente, nos gustaría ver una línea recta que asciende verticalmente hasta el cielo: ese sería nuestro sueño :).

Vamos a analizar un ejemplo de gráfico con beneficios.



Teniendo en cuenta que al realizar la optimización estamos comparando los resultados de un asesor experto, y no de varios, hemos decidido no considerar el tiempo durante el que ha funcionado el asesor experto.

Además, no consideraremos los volúmenes, pero si el lote se calcula dinámicamente, los volúmenes deberán incluirse de alguna forma en el cálculo del criterio personalizado (esto no ha sido implementado).

¿Cuántas transacciones? No importa cuántas transacciones me traerán mil tugriks, una o cien, por lo que tampoco tendré en cuenta la cantidad de transacciones. Pero debemos considerar que si hay muy pocas transacciones, la regresión lineal se calculará incorrectamente.

¿Qué es importante en este gráfico? Bueno, en primer lugar, por supuesto, el beneficio. Hemos decidido tener en cuenta el beneficio relativo, es decir, relativo al balance inicial.

Relative_Prof = TeSt[STAT_PROFIT] / TeSt[STAT_INITIAL_DEPOSIT];

Un parámetro no menos importante, y quizás hasta el que más, es la reducción

¿Cómo se calcula la reducción en el simulador? Se toma la cantidad máxima de fondos de la izquierda y se compara con la cantidad mínima de fondos de la derecha.


Los valores por encima del balance son bastante desagradables: el dinero que no hemos podido ganar. Pero cuando los valores están por debajo del balance, esto resulta realmente doloroso.

Por consiguiente, la reducción máxima más importante para nosotros es aquella que se encuentra por debajo del balance. La otra, aunque molesta, no resulta dolorosa.


double equityDD(const double & Balance[], const double & Equity[], const double & TeSt[], const double & coeff[], const int total)
  {
   if(TeSt[STAT_INITIAL_DEPOSIT] == 0)
      return(0);

   double Balance_max = Balance[0];
   double Equity_min = Equity[0];
   difference_B_E = 0;
   double Max_Balance = 0;

   switch((int)TeSt[41])
     {
      case  0:
         difference_B_E = TeSt[STAT_EQUITY_DD];
         break;
      default:
         for(int i = 0; i < total - 1; i++)
           {
            if(Balance_max < Balance[i])
               Balance_max = Balance[i];
            if(Balance[i] == 10963)
               Sleep(1);
            if(Balance_max - Equity[i + 1] > difference_B_E)
              {
               Equity_min = Equity[i + 1];
               difference_B_E = Balance_max - Equity_min;
               Max_Balance = Balance_max;
              }
           }
         break;
     }

   return(1 - difference_B_E / TeSt[STAT_INITIAL_DEPOSIT]);
  }

Como los valores para el criterio personalizado deben considerarse en orden ascendente, restamos la reducción a la unidad. Y resultó que, cuanto mayor era el valor, menor era la reducción.

Llamamos al valor resultante: equity_rel, reducción de fondos en relación con el balance inicial.

Finalmente, resultó que, para calcular equity_rel correctamente, es imposible recopilar los valores de equidad como hicimos al principio. Dado que se pierden algunos valores de la equidad mínima, hemos tenido que implementar dos opciones de conservación de la equidad. La primera opción conserva la equidad de los valores máximos al cerrar una posición con pérdidas y los valores mínimos al cerrar una posición con beneficios; la segunda opción conserva solo los valores mínimos de equidad.

Para que el script sepa cómo recopilamos la equidad, hemos tenido que escribir estas opciones en la matriz con las estadísticas del simulador TeSt[41], y en la función EquityDD(), calcular equity_rel y difference_B_E según la opción de recopilación de la equidad.

//---

  Así, hemos decidido simplemente cruzar diferentes combinaciones heterogéneas y ver qué resulta de ellas.

//---

Usando como base equity_rel, podemos calcular un factor de recuperación alternativo. 

difference_B_E — reducción máxima de fondos en dinero.

coeff[c_recovery_factor] = coeff[Profit_Bak] / difference_B_E;

No obstante, querríamos que el gráfico se aproximara más a una línea recta, así que hemos añadido R^2 al segundo factor de recuperación alternativo.

coeff[c_recovery_factor_r2] = coeff[Profit_Bak] / difference_B_E * coeff[r2];

Como en la configuración podemos optar por calcular la correlación a partir del balance o la equidad, si solo se han registrado los valores mínimos de esta, R^2 se correlacionará con la reducción.

Esta fórmula: beneficio relativo * R^2, puede dar resultados interesantes de un criterio personalizado.

coeff[profit_r2] = relative_prof * coeff[r2];

La correlación es buena, pero no estará de más considerar cómo de grande ha sido, por eso el siguiente criterio personalizado será este.

Beneficio relativo *R^2 / Standard Error

   if(stand_err == 0)
      coeff[profit_r2_Err] = 0;
   else
      coeff[profit_r2_Err] = relative_prof * coeff[r2] / stand_err;

Tenemos beneficio relativo, tenemos una reducción de fondos en relación con el balance inicial, y también tenemos R^2, así que la situación nos pide una fórmula que considere el beneficio, la reducción y la máxima aproximación del gráfico a una línea recta.

relative_prof + equity_rel + r2;

Pero ¿y si de repente queremos hacer que uno de estos parámetros sea más significativo? Para conseguirlo, hemos introducido una variable de peso: ratio 

Así, hemos obtenido otros tres criterios de optimización personalizados.

coeff[profit_R_equity_r2] = relative_prof * ratio + coeff[equity_rel] + coeff[r2];

coeff[profit_equity_R_r2] = relative_prof + coeff[equity_rel] * ratio + coeff[r2];

coeff[profit_equity_r2_R] = relative_prof + coeff[equity_rel] + coeff[r2] * ratio;


En total, hemos obtenido doce criterios de optimización personalizados.

1. R^2 - coeficiente de determinación.

2.  ProfitStability

3.  TSSF - factor de seguridad del sistema comercial.

4.  LinearFactor

5.  equity_rel  

6. c_recovery_factor

7. c_recovery_factor_r2

8. profit_r2

9. profit_r2_Err

10. profit_R_equity_r2

11. profit_equity_R_r2

12. profit_equity_r2_R


Comprobando el resultado obtenido

Para realizar las comprobaciones, escribiremos un asesor experto sncillo...

Según el plan preliminar del artículo, aquí deberíamos tener un código de asesor experto simple, pero lamentablemente, dos asesores expertos simples no han mostrado los resultados que querríamos mostrar aquí.

Por consiguiente, hemos tenido que tomar uno de los asesores expertos escritos por encargo y mostrar los resultados del funcionamiento de todo lo descrito aquí (hemos borrado el nombre del asesor en todas partes).

Imaginemos que estamos a finales de abril y planeamos iniciar un asesor experto en una cuenta real, ¿cómo podemos saber qué criterio de optimización debemos usar al optimizar el asesor experto, para que este comercie de manera rentable?

Vamos a iniciar una optimización de tres meses forward.  



Después de la optimización, reiniciamos el terminal.

Ejecutamos el script y seleccionamos solo los mejores resultados en la configuración. Obtenemos estas miniaturas en la carpeta.


Visualmente, elegimos qué pasada forward nos gusta más. Aquí hay varios resultados idénticos, por lo que hemos elegido   profit_equity_R_r2, ya que al optimizar según este criterio, se prioriza una reducción menor.


En el simulador, esta misma pasada tendrá el aspecto siguiente:


Como comparación, el balance mínimo:


El máximo de pruebas complejas se ve así:


Como podemos ver, con la configuración de la mejor pasada profit_equity_R_r2, hay muchas menos transacciones en el gráfico que con el balance máximo y el máximo complejo: el beneficio es aproximadamente el mismo, pero el gráfico en sí es mucho más fluido.


Entonces, ya hemos determinado el criterio personalizado, a saber,profit_equity_R_r2; ahora, vamos a ver qué pasaría si hubiéramos realizado la optimización durante los últimos tres meses, y hubiéramos decidido comerciar en mayo con la mejor configuración para los resultados de esta optimización.

Comenzamos la optimización con forward y miramos.

Ajustes de la optimización.  


En la configuración del asesor, debemos establecer el criterio personalizado con el optimizaremos.

Como resultado, vemos que si optimizáramos el asesor experto durante los últimos tres meses con el criterio personalizado profit_equity_R_r2,

y luego comerciáramos desde el 1 de abril al 1 de mayo con esta configuración, el EA ganaría 750 tugriks con una reducción de equidad de 300 tugriks.



Bueno, ¡vamos a poner ahora a prueba el asesor con el experto Validate de fxsaber!

Veamos cómo comerciaría el asesor durante cuatro meses. Configuración de Validate: tres meses de optimización - un mes de comercio.


Como podemos ver, ¡el asesor ha sobrevivido incluso después del control!

A modo de comparación, veamos el gráfico resultante de la misma configuración, pero optimizado según el "máximo del criterio complejo".



Vemos que el asesor también ha sobrevivido, pero....


Conclusión

Ventajas:

  1. Podemos ver todos los gráficos con los resultados de optimización al mismo tiempo.
  2. Tenemos la capacidad de elegir el criterio de optimización personalizado óptimo para nuestro asesor experto.

Desventajas.

Debido a la cantidad limitada de datos registrados, los gráficos son menos informativos que en el simulador de estrategias.

Con una gran cantidad de transacciones, el archivo con frames aumenta a un tamaño ilegible.

//---

Como han demostrado los experimentos, no existe un criterio de optimización genial: con diferentes asesores expertos, son diferentes los criterios que dan los mejores resultados, y estos deben seleccionarse individualmente.

Pero ya tenemos un conjunto completo de estos criterios, y si los miembros del foro apoyan y comparten sus criterios de optimización, el surtido a elegir será aún mayor.

//---

Uno de los evaluadores sugirió describir la configuración del script en el artículo, de forma que los usuarios más perezosos puedan simplemente analizar la configuración y usar este código sin profundizar en la esencia.


Cómo usarlo

Para usar este código, deberemos descargar el archivo adjunto al final del artículo, descomprimirlo y copiarlo en la carpeta MQL5.

A continuación, presionamos en el terminal -> archivo -> abrir directorio de datos -> clicamos con el botón derecho en un lugar vacío de la carpeta abierta y clicamos en "pegar". Puede aparecer una ventana preguntando "Reemplazar archivos en la carpeta de destino" -> clicamos en reemplazar.

Después, ejecutamos el MetaEditor, abrimos nuestro asesor experto y le añadimos los siguientes cambios:

1.  en la función OnTick(), pegamos --> IsOnTick();

 2. pegamos este código en la parte más baja del asesor:

  #include <SkrShotOpt.mqh>     

  double OnTester() {return(IsOnTester());}

  void OnTradeTransaction(const MqlTradeTransaction & trans, const MqlTradeRequest & request,const MqlTradeResult & result) 
    {
      IsOnTradeTransaction(trans, request, result);
     }
 Si el EA ya tiene la función OnTradeTransaction(), simplemente pegamos en ella:    IsOnTradeTransaction(trans, request, result);

3. Presionamos el botón "Compilar". 

Si de repente aparecen errores indicando que los nombres de las variables coinciden, tendremos que cambiar los nombres: si no estamos seguro de nuestra acciones, sería recomendable recurrir a alguien que sepa cómo hacerlo.


Ajustes

Después de pegar el código, en los ajustes del asesor, en la parte inferior, añadiremos algunas líneas más con la configuración.

¡No marque las casillas para optimizar esta configuración!  Esta configuración no influye de ninguna manera en los resultados de la optimización, por lo que no será necesario optimizarlos.


  • Escribimos la pasada si hay más transacciones: si nuestro EA ejecuta muchas, este parámetro se podrá aumentar para reducir la cantidad de datos escritos en los archivos de frames.
  • Registramos una pasada si el beneficio es superior al %; las pasadas no rentables se filtran por defecto. Podemos cambiarlo si no nos importa que el beneficio sea inferior a un determinado porcentaje del balance inicial.
  • Selección de valores de equidad: seleccionamos "mantener solo la equidad mínima" si necesitamos calcular correctamente los siguientes criterios personalizados: quity_rel, c_recovery_factor, c_recovery_factor_r2, profit_r2, profit_r2_Err, profit_R_equity_r2, profit_equity_R_r2, profit_equity_r2_R  
    si necesitamos gráficos más similares a los gráficos del simulador, deberemos seleccionar "guardar equidad mínima y máxima"

  • Criterio personalizado: si indicamos "ninguno", los frames se escribirán en el archivo, el criterio personalizado no se calculará en este caso (para no aumentar el tiempo de optimización),

En consecuencia, resulta imposible establecer la optimización según un criterio personalizado. Además, los parámetros que van a continuación no importan.

Deberemos seleccionar algún criterio personalizado para este parámetro si deseamos ejecutar la optimización según un criterio personalizado. 

Asimismo, cabe recordar que el cálculo del criterio personalizado depende de la elección del parámetro "selección de valores de equidad" y del parámetro "cálculo del criterio por"

  • cálculo de criterio por: selección del cálculo de R^2 basado en el balance o la equidad, respectivamente; todos los valores de los criterios personalizados en cuyo cálculo está involucrado R^2, cambiarán
r2, profit_r2, profit_r2_Err, profit_R_equity_r2, profit_equity_R_r2, profit_equity_r2_R

    //----

    Ajustes del script.


    • Dibujar línea de regresión: debemos elegir qué línea de regresión dibujar, de equilibrio, equidad, equilibrio y equidad, no dibujar una línea de regresión.
    • Porcentaje de beneficio mayor: se tarda mucho tiempo en imprimir varios miles de capturas de pantalla, por lo que solo podemos imprimir capturas de pantalla con un beneficio mayor al especificado en esta configuración.
    • Solo las mejores puntuaciones: si es true, guardaremos las capturas de pantalla solo con el mejor resultado de cada criterio personalizado; de lo contrario, guardaremos todas las capturas.
    • Criterio personalizado: si seleccionamos imprimir todas las capturas de pantalla, este parámetro se podrá usar para establecer el criterio personalizado según el cual se ordenarán las capturas de pantalla en la carpeta.
    • ratio - coeficiente de peso para los cálculos de los criterios personalizados profit_R_equity_r2, profit_equity_R_r2, profit_equity_r2_R.
    • cálculo de criterio por: selección del cálculo de R^2 basado en el balance o la equidad, respectivamente; todos los valores de los criterios personalizados en cuyo cálculo está involucrado R^2, cambiarán

             r2, profit_r2, profit_r2_Err, profit_R_equity_r2, profit_equity_R_r2, profit_equity_r2_R.

    • Gráfico: elegiremos entre, imprimir el gráfico como en el simulador "Back por separado de Forward", es decir, forward comienza desde el balance inicial,  

    o "Back seguido de Forward": en este caso, forward continuará desde el último valor de balance de la pasada Back.

    //---

    Los artículos de este sitio web sirvieron de mucha ayuda al escribir el programa.

    ¡Querríamos expresar nuestra profunda gratitud a todos los autores de los artículos enumerados aquí!


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

    Archivos adjuntos |
    SkrShotOpt.mqh (17.56 KB)
    MQL5.zip (11.59 KB)
    Gráficos en la biblioteca DoEasy (Parte 92): Clase de memoria de objetos gráficos estándar Historia de cambio de propiedades del objeto Gráficos en la biblioteca DoEasy (Parte 92): Clase de memoria de objetos gráficos estándar Historia de cambio de propiedades del objeto
    En este artículo, crearemos la clase de memoria del objeto gráfico estándar, que permitirá al objeto guardar sus estados al modificarse sus propiedades, lo que a su vez le permitirá volver a los estados anteriores del objeto gráfico en cualquier momento.
    Gráficos en la biblioteca DoEasy (Parte 91): Eventos de objetos gráficos estándar en el programa. Historia de cambio de nombre del objeto Gráficos en la biblioteca DoEasy (Parte 91): Eventos de objetos gráficos estándar en el programa. Historia de cambio de nombre del objeto
    En el artículo, finalizaremos la funcionalidad básica para posibilitar el control de eventos para los objetos gráficos desde un programa basado en la biblioteca. Comenzaremos creando la funcionalidad necesaria para almacenar la historia de cambios en las propiedades de los objetos gráficos usando la propiedad "Nombre del objeto" como ejemplo.
    Gráficos en la biblioteca DoEasy (Parte 93): Preparando la funcionalidad para crear objetos gráficos compuestos Gráficos en la biblioteca DoEasy (Parte 93): Preparando la funcionalidad para crear objetos gráficos compuestos
    En el presente artículo, comenzaremos a desarrollar la funcionalidad necesaria para crear objetos gráficos compuestos. Nuestra biblioteca ofrecerá soporte a la creación de objetos gráficos compuestos complejos en los que dichos objetos podrán tener cualquier jerarquía de relaciones. Vamos a preparar todas las clases necesarias para la posterior implementación de tales objetos.
    Matrices y vectores en MQL5 Matrices y vectores en MQL5
    La matriz y el vector de tipos de datos especiales nos permiten escribir un código próximo a la notación matemática. Esto elimina la necesidad de crear ciclos anidados y recordar la indexación correcta de las matrices que participan en los cálculos, aumentando la fiabilidad y la velocidad del desarrollo de programas complejos.