Resolución de ecuaciones

En los métodos de aprendizaje automático y problemas de optimización, a menudo es necesario encontrar una solución a un sistema de ecuaciones lineales. MQL5 contiene cuatro métodos que permiten resolver este tipo de ecuaciones en función del tipo de matriz.

  • Solve resuelve una ecuación matricial lineal o un sistema de ecuaciones algebraicas lineales.
  • LstSq resuelve de forma aproximada un sistema de ecuaciones algebraicas lineales (para matrices no cuadradas o degeneradas).
  • Inv calcula una matriz inversa multiplicativa con respecto a una matriz cuadrada no singular mediante el método de Jordan-Gauss.
  • PInv calcula la matriz pseudoinversa según el método de Moore-Penrose.

A continuación se presentan los prototipos de métodos.

vector<T> matrix<T>::Solve(const vector<T> b)

vector<T> matrix<T>::LstSq(const vector<T> b)

matrix<T> matrix<T>::Inv()

matrix<T> matrix<T>::PInv()

Los métodos Solve y LstSq implican la solución de un sistema de ecuaciones de la forma A*X=B, donde A es una matriz, B es un vector pasado por un parámetro con los valores de la función (o «variable dependiente»).

Vamos a aplicar el método LstSq para resolver un sistema de ecuaciones, que es un modelo de trading de cartera ideal (en nuestro caso, analizaremos una cartera de las principales divisas Forex). Para ello, en un número determinado de barras «históricas», necesitamos encontrar aquellos tamaños de lote para cada divisa con los que la línea de equilibrio tiende a ser una línea recta en constante crecimiento.

Indiquemos el par de divisas i-ésimo como Si. Su cotización en la barra con el índice k es igual a Si[k]. La numeración de las barras irá del pasado al futuro, como en las matrices y vectores rellenados por el método CopyRates. Así, el inicio de las cotizaciones recogidas para entrenar el modelo corresponde a la barra marcada con el número 0, pero en la línea de tiempo será la barra histórica más antigua (de las que procesemos, según la configuración del algoritmo). Las barras a la derecha (hacia el futuro) a partir de ella se numeran 1, 2 y así sucesivamente, hasta el número total de barras en las que el usuario ordenará el cálculo.

La variación del precio de un símbolo entre la barra 0 y la barra N determina la ganancia (o pérdida) por tiempo de la barra N.

Teniendo en cuenta el conjunto de divisas, obtenemos, por ejemplo, la siguiente ecuación de beneficios para la 1ª barra:

(S1[1] - S1[0]) * X1 + (S2[1] - S2[0]) * X2 + ... + (Sm[1] - Sm[0]) * Xm = B

Aquí, m es el número total de caracteres, Xi es el tamaño de lote de cada símbolo, y B es el beneficio flotante (saldo condicional, si fija el beneficio).

Para simplificar, abreviemos la notación. Pasemos de valores absolutos a incrementos de precio (Ai[k] = Si[k]-Si[0]). Teniendo en cuenta el movimiento a través de las barras, obtendremos varias expresiones para la curva de equilibrio virtual:

A1[1] * X1 + A2[1] * X2 + ... + Am[1] * Xm = B[1]
A1[2] * X1 + A2[2] * X2 + ... + Am[2] * Xm = B[2]
...
A1[K] * X1 + A2[K] * X2 + ... + Am[K] * Xm = B[K]

El éxito del trading se caracteriza por un beneficio constante en cada barra, es decir, el modelo para el vector derecho B es una función creciente de forma monótona, idealmente una línea recta.

Pongamos en práctica este modelo y seleccionemos los coeficientes X para él basándonos en las cotizaciones. Como aún no conocemos las API de la aplicación, no codificaremos una estrategia de trading completa. Vamos simplemente a crear un gráfico de equilibrio virtual utilizando la función GraphPlot del archivo de encabezado estándar Graphic.mqh (ya la hemos utilizado para demostrar funciones matemáticas).

El código fuente completo del nuevo ejemplo se encuentra en el script MatrixForexBasket.mq5.

En los parámetros de entrada, deje que el usuario elija el número total de barras para el muestreo de datos (BarCount), así como el número de barras dentro de esta selección (BarOffset) en el que termina el pasado condicional y comienza el futuro condicional.

Se construirá un modelo sobre el pasado condicional (se resolverá el sistema de ecuaciones lineales anterior) y se realizará una prueba forward sobre el futuro condicional.

input int BarCount = 20;  // BarCount (known "history" and "future")
input int BarOffset = 10// BarOffset (where "future" starts)
input ENUM_CURVE_TYPE CurveType = CURVE_LINES;

Para rellenar el vector con un equilibrio ideal, escribimos la función ConstantGrow: se utilizará más adelante durante la inicialización.

void ConstantGrow(vector &v)
{
   for(ulong i = 0i < v.Size(); ++i)
   {
      v[i] = (double)(i + 1);
   }
}

La lista de instrumentos negociados (principales pares de Forex) se establece al principio de la función OnStart. Edítela para adaptarla a sus necesidades y a su entorno de negociación.

void OnStart()
{
   const string symbols[] =
   {
      "EURUSD""GBPUSD""USDJPY""USDCAD"
      "USDCHF""AUDUSD""NZDUSD"
   };
   const int size = ArraySize(symbols);
   ...

Vamos a crear la matriz rates en la que se añadirán las cotizaciones de los símbolos, el vector model con la curva de balance deseada, y el vector auxiliar close para una petición símbolo a símbolo de los precios de cierre de barra (los datos de éste se copiarán en las columnas de la matriz rates).

   matrix rates(BarCountsize);
   vector model(BarCount - BarOffsetConstantGrow);
   vector close;

En un bucle de símbolos, copiamos los precios de cierre en el vector close, calculamos los incrementos de precio y los escribimos en la columna correspondiente de la matriz rates.

   for(int i = 0i < sizei++)
   {
      if(close.CopyRates(symbols[i], _PeriodCOPY_RATES_CLOSE0BarCount))
      {
         // calculate increments (profit on all and on each bar in one line)
         close -= close[0];
         // adjust the profit to the pip value
         close *= SymbolInfoDouble(symbols[i], SYMBOL_TRADE_TICK_VALUE) /
            SymbolInfoDouble(symbols[i], SYMBOL_TRADE_TICK_SIZE);
         // place the vector in the matrix column
         rates.Col(closei);
      }
      else
      {
         Print("vector.CopyRates(%d, COPY_RATES_CLOSE) failed. Error "
            symbols[i], _LastError);
         return;
      }
   }
   ...

Veremos el cálculo del valor de un punto de precio (en la moneda de depósito) en la Parte 5.

También es importante tener en cuenta que las barras con los mismos índices pueden tener diferentes marcas de tiempo en diferentes instrumentos financieros: por ejemplo, si había un día festivo en uno de los países y el mercado estaba cerrado (fuera de Forex, los símbolos pueden, en teoría, tener diferentes horarios para las sesiones de trading). Para resolver este problema, necesitaríamos un análisis más profundo de las cotizaciones, teniendo en cuenta las horas de las barras y su sincronización antes de insertarlas en la matriz rates. No lo hacemos aquí para mantener la simplicidad, y también porque el mercado Forex opera según las mismas reglas la mayor parte del tiempo.

Dividimos la matriz en dos partes: la parte inicial se utilizará para encontrar una solución (ello emula la optimización sobre la historia), y la parte posterior se utilizará para una prueba forward (cálculo de posteriores cambios en el equilibrio).

   matrix split[];
   if(BarOffset > 0)
   {
      // training on BarCount - BarOffset bars
      // check on BarOffset bars
      ulong parts[] = {BarCount - BarOffsetBarOffset};
      rates.Split(parts0split);
   }
  
   // solve the system of linear equations for the model
   vector x = (BarOffset > 0) ? split[0].LstSq(model) : rates.LstSq(model);
   Print("Solution (lots per symbol): ");
   Print(x);
   ...

Ahora que ya tenemos una solución, vamos a crear la curva de equilibrio para todas las barras de la muestra (la parte «histórica» ideal estará al principio, y después comenzará la parte «futura», que no se utilizó para ajustar el modelo).

   vector balance = vector::Zeros(BarCount);
   for(int i = 1i < BarCount; ++i)
   {
      balance[i] = 0;
      for(int j = 0j < size; ++j)
      {
         balance[i] += (float)(rates[i][j] * x[j]);
      }
   }
   ...

Vamos a calcular la calidad de la solución mediante el criterio R2.

   if(BarOffset > 0)
   {
      // make a copy of the balance
      vector backtest = balance;
      // select only "historical" bars for backtesting
      backtest.Resize(BarCount - BarOffset);
      // bars for the forward test have to be copied manually
      vector forward(BarOffset);
      for(int i = 0i < BarOffset; ++i)
      {
         forward[i] = balance[BarCount - BarOffset + i];
      }
      // compute regression metrics independently for both parts
      Print("Backtest R2 = "backtest.RegressionMetric(REGRESSION_R2));
      Print("Forward R2 = "forward.RegressionMetric(REGRESSION_R2));
   }
   else
   {
      Print("R2 = "balance.RegressionMetric(REGRESSION_R2));
   }
   ...

Para visualizar la curva de equilibrio en un gráfico es necesario transferir los datos de un vector a un array.

   double array[];
   balance.Swap(array);
   
   // print the values of the changing balance with an accuracy of 2 digits
   Print("Balance: ");
   ArrayPrint(array2);
  
   // draw the balance curve in the chart object ("backtest" and "forward")
   GraphPlot(arrayCurveType);
}

A continuación se muestra un ejemplo de registro obtenido al ejecutar el script en EURUSD,H1.

Solution (lots per symbol): 
[-0.0057809334,-0.0079846876,0.0088985749,-0.0041461736,-0.010710154,-0.0025694175,0.01493552]
Backtest R2 = 0.9896645616246145
Forward R2 = 0.8667852183780984
Balance: 
 0.00  1.68  3.38  3.90  5.04  5.92  7.09  7.86  9.17  9.88 
 9.55 10.77 12.06 13.67 15.35 15.89 16.28 15.91 16.85 16.58

Y este es el aspecto de la curva de equilibrio virtual.

Balance virtual de trading de una cesta de divisas por lotes según la decisión de SLAU (donde empieza el «futuro»)

Balance virtual del trading de una cartera de divisas por lotes según la decisión

La mitad izquierda tiene una forma más uniforme y un R2 más alto, lo que no es sorprendente porque el modelo (X variables) se ajustó específicamente para ello.

Sólo por interés, aumentaremos 10 veces la profundidad de entrenamiento y verificación, es decir, fijaremos en los parámetros BarCount = 200 y BarOffset = 100. Obtendremos una nueva imagen.

Balance virtual de trading de una cesta de divisas por lotes según la decisión de SLAU (donde empieza el «futuro»)

Balance virtual del trading de una cartera de divisas por lotes según la decisión

La parte del «futuro» parece menos suave, e incluso podemos decir que tenemos suerte de que siga creciendo, a pesar de un modelo tan simple. Por regla general, durante la prueba forward, la curva de equilibrio virtual se degrada de forma significativa y empieza a descender.

Es importante señalar que, para probar el modelo, tomamos los valores obtenidos en X de la solución «tal cual» del sistema, mientras que en la práctica tendremos que normalizarlos a los lotes mínimos y al paso de lote, lo que afectará negativamente a los resultados y los acercará más a la realidad.