English Русский 中文 Deutsch 日本語
preview
Reimaginando las estrategias clásicas en MQL5 (Parte 13): Llevando nuestra estrategia de cruce a nuevas dimensiones (Parte 2)

Reimaginando las estrategias clásicas en MQL5 (Parte 13): Llevando nuestra estrategia de cruce a nuevas dimensiones (Parte 2)

MetaTrader 5Ejemplos |
39 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

En nuestra última discusión sobre los cruces de medias móviles, examinamos cómo minimizar el retraso inherente asociado con las medias móviles. Los cruces de medias móviles son conocidos por generar señales retardadas. Comentábamos que, al fijar los períodos de ambas medias móviles a un valor común —por ejemplo, un período de tres, como se utilizó en nuestra discusión anterior—, podemos obtener señales de trading mucho más ágiles. Esta mejora surge de la aplicación de indicadores de media móvil por separado a los precios de apertura y cierre, aunque compartan el mismo período. Al situarlos en diferentes niveles de precios, seguimos teniendo la garantía de observar cruces entre las medias móviles de apertura y cierre. Al mismo tiempo, este enfoque reduce el retraso del sistema mediante el uso de períodos cortos, generalmente inferiores a cinco.

Hemos demostrado que esta estrategia ofrece ventajas sobre el enfoque clásico de cruce de medias móviles. En nuestra discusión inicial, comparamos esta nueva estrategia de cruce propuesta con su contraparte clásica. En este artículo, continuaremos desarrollando nuestra estrategia de cruce de medias móviles e intentaremos reducir aún más el retraso inherente explorando si es posible pronosticar los cruces antes de que ocurran. Esto nos permitiría operar de forma proactiva y responder con mayor rapidez a las oportunidades comerciales. A diferencia de los participantes típicos del mercado, que esperan la confirmación y reaccionan solo después de que el cruce se hace evidente, nuestro objetivo es construir modelos estadísticos capaces de detectar los cruces con antelación, lo que nos permite posicionar nuestras cuentas adecuadamente antes de que se produzcan los movimientos.

Si bien detectar señales de compraventa en medio del ruido del mercado puede resultar complicado, varios principios de la ciencia de datos pueden ayudar a fortalecer nuestra estrategia. Por ejemplo, hacemos referencia a una presentación del equipo del Laboratorio de Propulsión a Chorro de la NASA en el Instituto Tecnológico de California (Caltech), que ofrece información valiosa. El enlace a la presentación está disponible aquí. Esta presentación se centró en el big data e introdujo un principio clave relevante para nuestra discusión. Se anima a los lectores interesados a revisar las diapositivas por sí mismos. En resumen, el principio establece que ciertos problemas complejos en ciencia de datos pueden resolverse más fácilmente al proyectarlos en espacios de mayor dimensión. Para mayor comodidad del lector, hemos incluido un extracto de la presentación original que resulta relevante para nuestra discusión en la Figura 1, a continuación.

Figura 1: La diapositiva anterior formaba parte originalmente de la presentación pública sobre "Análisis de macrodatos" realizada por el equipo del JPL de la NASA en la Universidad Caltech en septiembre de 2014.

Por ejemplo, consideremos un conjunto de datos con tres características, donde el objetivo es clasificar los días de mercado alcistas y bajistas. Lograr una alta precisión de clasificación puede resultar difícil en este espacio tridimensional de baja dimensionalidad. Sin embargo, al proyectar el conjunto de datos a dimensiones superiores, el rendimiento puede mejorar porque algunos problemas se vuelven más separables en espacios de características de mayor dimensión. Si bien el aprendizaje automático no garantiza el éxito en todos los casos, este enfoque suele ofrecer mejores resultados y a menudo merece la pena probarlo.

Este principio contrasta con nuestras discusiones anteriores en nuestra serie de artículos relacionados, como Asesores Expertos Autooptimizables en MQL5, donde exploramos los beneficios de las técnicas de reducción de dimensionalidad como UMAP para reducir conjuntos de datos de 30 características a cuatro. Normalmente, nos centramos en reducir la dimensionalidad para simplificar los modelos y mejorar la generalización. Hoy, sin embargo, adoptaremos el enfoque opuesto y aumentaremos deliberadamente la dimensionalidad de nuestro conjunto de datos, ya que esto puede tener un valor práctico que demostraremos.

En este artículo, desarrollaremos un método manual para generar muchas columnas de características nuevas. En futuras discusiones, trabajaremos con técnicas algorítmicas más flexibles para la generación de características.


Primeros pasos en MQL5

Para empezar, crearemos un script para obtener todos los datos necesarios de nuestra terminal MetaTrader 5. Comenzaremos por fijar ciertas constantes relevantes para nuestra discusión. Por ejemplo, el período para todas las medias móviles se establecerá en un valor fijo, y utilizaremos medias móviles simples a lo largo de esta discusión.

//+------------------------------------------------------------------+
//|                                                      ProjectName |
//|                                      Copyright 2020, CompanyName |
//|                                       http://www.companyname.net |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property script_show_inputs

//--- Define our moving average indicator
#define MA_PERIOD 2                 //--- Moving Average Period
#define MA_TYPE   MODE_SMA          //--- Type of moving average we have
#define HORIZON   5                 //--- Forecast horizon

 Además, definiremos variables globales como manejadores y búferes para los indicadores de media móvil.

//--- Our handlers for our indicators
int ma_handle,ma_o_handle,ma_h_handle,ma_l_handle;

//--- Data structures to store the readings from our indicators
double ma_reading[],ma_o_reading[],ma_h_reading[],ma_l_reading[];

//--- File name
string file_name = Symbol() + " Market Data As Series Moving Average.csv";

//--- Amount of data requested
input int size = 3000;

A continuación, procedemos a definir el cuerpo principal de nuestro guion. Cuando se ejecute el script, inicializaremos nuestros controladores de promedio móvil y luego copiaremos los valores de estos controladores en sus búferes asociados. Al prepararnos para escribir los datos en un archivo, es importante tener en cuenta que hay numerosas columnas que deben rellenarse. Las primeras ocho columnas son estándar: precios de apertura, máximo, mínimo y cierre, y sus correspondientes medias móviles. Además de estas, incluimos columnas que representan el crecimiento que se produce dentro de cada fuente de precios.

Además, existen columnas dedicadas al cálculo de los cambios relativos entre diferentes niveles de precios. Por ejemplo, además de calcular la variación del precio de apertura con respecto a su valor histórico, también calculamos la variación del precio de apertura en relación con el precio de cierre, la variación del precio de apertura en relación con el precio mínimo, y así sucesivamente. Los mismos cálculos se repiten para las medias móviles. En total, este proceso genera 40 columnas en nuestro conjunto de datos. Finalmente, almacenamos los valores reales que pretendemos escribir y luego cerramos el descriptor del archivo.

//+------------------------------------------------------------------+
//| Our script execution                                             |
//+------------------------------------------------------------------+
void OnStart()
  {
  int fetch = size + (HORIZON * 2);
//---Setup our technical indicators
   ma_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE);
   ma_o_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_OPEN);
   ma_h_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_HIGH);
   ma_l_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_LOW);

//---Set the values as series
   CopyBuffer(ma_handle,0,0,fetch,ma_reading);
   ArraySetAsSeries(ma_reading,true);
   CopyBuffer(ma_o_handle,0,0,fetch,ma_o_reading);
   ArraySetAsSeries(ma_o_reading,true);
   CopyBuffer(ma_h_handle,0,0,fetch,ma_h_reading);
   ArraySetAsSeries(ma_h_reading,true);
   CopyBuffer(ma_l_handle,0,0,fetch,ma_l_reading);
   ArraySetAsSeries(ma_l_reading,true);

//---Write to file
   int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,",");

   for(int i=size;i>=1;i--)
     {
      if(i == size)
        {
         FileWrite(file_handle,"Time",
                              //--- OHLC
                              "True Open",
                              "True High",
                              "True Low",
                              "True Close",
                              //--- MA OHLC
                              "True MA C",
                              "True MA O",
                              "True MA H",
                              "True MA L",
                              //--- Growth in OHLC
                              "Diff Open",
                              "Diff High",
                              "Diff Low",
                              "Diff Close",
                              //--- Growth in MA OHLC
                              "Diff MA Close 2",
                              "Diff MA Open 2",
                              "Diff MA High 2",
                              "Diff MA Low 2",
                              //--- Grwoth between channels
                              "O - C",
                              "Delta O - C",
                              "O - L",
                              "Delta O - L",
                              "O - H",
                              "Delta O - H",
                              "H - L",
                              "Delta H - L",
                              "C - H",
                              "Delta C - H",
                              "C - L",
                              "Delta C - L",
                              //--- Grwoth between MA channels
                              "MA O - C",
                              "MA Delta O - C",
                              "MA O - L",
                              "MA Delta O - L",
                              "MA O - H",
                              "MA Delta O - H",
                              "MA H - L",
                              "MA Delta H - L",
                              "MA C - H",
                              "MA Delta C - H",
                              "MA C - L",
                              "MA Delta C - L"
                  );
        }

      else
        {
         FileWrite(file_handle,
                   iTime(_Symbol,PERIOD_CURRENT,i), 
                   //--- OHLC
                   iOpen(_Symbol,PERIOD_CURRENT,i),
                   iHigh(_Symbol,PERIOD_CURRENT,i),
                   iLow(_Symbol,PERIOD_CURRENT,i),
                   iClose(_Symbol,PERIOD_CURRENT,i),
                   //--- MA OHLC
                   ma_reading[i],
                   ma_o_reading[i],
                   ma_h_reading[i],
                   ma_l_reading[i],
                   //--- Growth in OHLC
                   iOpen(_Symbol,PERIOD_CURRENT,i)   - iOpen(_Symbol,PERIOD_CURRENT,(i + HORIZON)), 
                   iHigh(_Symbol,PERIOD_CURRENT,i)   - iHigh(_Symbol,PERIOD_CURRENT,(i + HORIZON)),
                   iLow(_Symbol,PERIOD_CURRENT,i)    - iLow(_Symbol,PERIOD_CURRENT,(i + HORIZON)),
                   iClose(_Symbol,PERIOD_CURRENT,i)  - iClose(_Symbol,PERIOD_CURRENT,(i + HORIZON)),
                   //--- Growth in MA OHLC
                   ma_reading[i] - ma_reading[(i + HORIZON)],
                   ma_o_reading[i] - ma_o_reading[(i + HORIZON)],
                   ma_h_reading[i] - ma_h_reading[(i + HORIZON)],
                   ma_l_reading[i] - ma_l_reading[(i + HORIZON)],
                   //--- Growth between channels
                   iOpen(_Symbol,PERIOD_CURRENT,i) - iClose(_Symbol,PERIOD_CURRENT,i),
                   iOpen(_Symbol,PERIOD_CURRENT,i + HORIZON) - iClose(_Symbol,PERIOD_CURRENT,i + HORIZON),
                   iOpen(_Symbol,PERIOD_CURRENT,i) - iLow(_Symbol,PERIOD_CURRENT,i),
                   iOpen(_Symbol,PERIOD_CURRENT,i + HORIZON) - iLow(_Symbol,PERIOD_CURRENT,i + HORIZON),
                   iOpen(_Symbol,PERIOD_CURRENT,i) - iHigh(_Symbol,PERIOD_CURRENT,i),
                   iOpen(_Symbol,PERIOD_CURRENT,i + HORIZON) - iHigh(_Symbol,PERIOD_CURRENT,i + HORIZON),
                   iHigh(_Symbol,PERIOD_CURRENT,i) - iLow(_Symbol,PERIOD_CURRENT,i),
                   iHigh(_Symbol,PERIOD_CURRENT,i + HORIZON) - iLow(_Symbol,PERIOD_CURRENT,i + HORIZON),
                   iClose(_Symbol,PERIOD_CURRENT,i)  -  iHigh(_Symbol,PERIOD_CURRENT,i),
                   iClose(_Symbol,PERIOD_CURRENT,i + HORIZON)  -  iHigh(_Symbol,PERIOD_CURRENT,i + HORIZON),
                   iClose(_Symbol,PERIOD_CURRENT,i)  -  iLow(_Symbol,PERIOD_CURRENT,i),
                   iClose(_Symbol,PERIOD_CURRENT,i + HORIZON)  -  iLow(_Symbol,PERIOD_CURRENT,i + HORIZON),
                   //--- Growth between moving average channels
                   ma_o_reading[i] - ma_reading[i],
                   ma_o_reading[(i + HORIZON)] -  ma_reading[(i + HORIZON)],
                   ma_o_reading[i] -  ma_l_reading[i],
                   ma_o_reading[(i + HORIZON)] - ma_l_reading[(i + HORIZON)],
                   ma_o_reading[i] -  ma_h_reading[i],
                   ma_o_reading[(i + HORIZON)] - ma_h_reading[(i + HORIZON)],
                   ma_h_reading[i] - ma_l_reading[i],
                   ma_h_reading[(i + HORIZON)] - ma_l_reading[(i + HORIZON)],
                   ma_reading[i] -  ma_h_reading[i],
                   ma_reading[(i + HORIZON)] - ma_h_reading[(i + HORIZON)],
                   ma_reading[i] -  ma_l_reading[i],
                   ma_reading[(i + HORIZON)] - ma_l_reading[(i + HORIZON)]
                   );
        }
     }
//--- Close the file
   FileClose(file_handle);
  }
//+------------------------------------------------------------------+

Analizando nuestros datos en Python

Ahora que ha terminado de ejecutar su script en la terminal y extraer los datos necesarios, podemos comenzar a analizar y procesar los datos. En primer lugar, cargaremos las bibliotecas estándar para el análisis numérico.

import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

A continuación, leeremos los conjuntos de datos. Como podrá observar, el conjunto de datos tiene un ancho considerable y algunas columnas se han truncado.

data = pd.read_csv("C:\\Users\\Westwood\\AppData\\Roaming\\MetaQuotes\\Terminal\\D0E8209F77C8CF37AD8BF550E51FF075\\MQL5\\Files\\EURUSD Market Data As Series Moving Average.csv")
data

Figura 2: Visualización de los datos que extrajimos de nuestra terminal MetaTrader 5.

A continuación, definiremos nuestra variable objetivo. Recordemos que, en este ejemplo, el objetivo es el punto de cruce entre las medias móviles. Un método sencillo para seguir este cruce consiste en monitorizar el punto medio entre las dos medias móviles. Al observar si el punto medio ha subido o bajado, capturamos eficazmente la misma información para nuestros modelos estadísticos.

HORIZON             = 10

#Classical Target
data['Target']      =  0

#High Low Mid Point Target
data['Target 2']    = 0

#Open Close Mid Point Target
data['Target 3']    = 0

data.loc[data['True Close'].shift(-HORIZON) > data['True Close'],'Target'] = 1

#The Mid Point Between The High And The Low Moving Average
data.loc[((data['True MA H'].shift(-HORIZON) + data['True MA L'].shift(-HORIZON)) / 2) > ((data['True MA H'] + data['True MA L']) / 2),'Target 2'] = 1

#The Open And Close Mid Point
data.loc[((data['True MA O'].shift(-HORIZON) + data['True MA C'].shift(-HORIZON)) / 2) > ((data['True MA O'] + data['True MA C']) / 2),'Target 3'] = 1

data = data.iloc[:-HORIZON,:]

Antes de continuar, me gustaría ofrecer una breve demostración que ilustre el valor de proyectar conjuntos de datos a dimensiones superiores. Esto servirá como una prueba sencilla para los lectores que no estén familiarizados con este principio, asegurando que todos estemos en sintonía. Comenzaremos creando una copia del conjunto de datos original que contenga únicamente las cuatro columnas estándar: precios de apertura, máximo, mínimo y cierre. Posteriormente, recalcularemos nuestra variable objetivo sobre este conjunto de datos reducido.

#Copy the dataset
X = data.iloc[:,:5].copy()
X['Target'] = data['True Close'].shift(-HORIZON) - data['True Close']
X.dropna(inplace=True)

A continuación, definiremos una función que toma un conjunto de datos y agrega un número arbitrario de columnas rellenas con ceros. Por ejemplo, si llamamos a esta función con nuestra copia del conjunto de datos y especificamos cinco columnas, devolverá el conjunto de datos con cinco columnas adicionales, cada una rellena con ceros.

def fill_zeros(f_data,f_n):
    #Copy the original data
    res = f_data.copy()

    #We want to keep the target at the end
    t = 'Target'
    v = res.pop('Target')

    #Add columns of zeros
    for i in np.arange(f_n):
        name = str(i) + ' Col'
        res[name] = 0

    #Place the target back
    res[t]= v

    #Return the new dataframe
    return(res)

A continuación, realizaremos una prueba sencilla validando nuestro modelo de forma cruzada con un número creciente de columnas rellenas de ceros y observaremos el efecto sobre el error de validación cruzada. Obsérvese que el gráfico resultante no es uniforme, lo que indica variabilidad en el error a medida que aumentan las dimensiones. 

Sin embargo, es evidente que la gráfica alcanza nuevos mínimos que no había podido lograr anteriormente. Al principio, observamos el nivel de error de nuestro modelo cuando no hay columnas adicionales rellenas de ceros. A medida que aumenta el número de columnas con valor cero, el error generalmente se dispara y luego cae a nuevos mínimos, nunca antes alcanzados. Esta observación debería llevar al lector a preguntarse por qué el rendimiento del modelo mejora a pesar de no proporcionarse información adicional; recuerde que las columnas con valor cero no contienen datos útiles para el modelo.

#Load our libraries
from sklearn.neural_network import MLPRegressor
from sklearn.model_selection import cross_val_score,TimeSeriesSplit

tscv = TimeSeriesSplit(n_splits=5,gap=HORIZON)
EPOCHS = 50

#Observe what happens to our error levels as we increase the number of columns in the dataset
res = []

for i in np.arange(EPOCHS):
    #Fetch new data with addtional columns of zeros
    new_data = fill_zeros(X,(1+i))

    #Record the new error
    res.append(np.mean(np.abs(cross_val_score(MLPRegressor(hidden_layer_sizes=(new_data.iloc[:,1:-1].shape[1],2,50,100),random_state=0,shuffle=False),X.iloc[:,1:-1],X.iloc[:,-1],cv=tscv,n_jobs=-1))))

plt.plot(res,color='black')
plt.grid()
plt.ylabel('RMSE Error')
plt.xlabel('Additional Columns of Zero')
plt.title('Our Error Levels Can Fall Without Any New Information')

Figura 3: Los niveles de error de nuestro modelo disminuyen incluso aunque no proporcionemos ninguna información adicional.

Existen varias explicaciones plausibles para este fenómeno. Para los fines de esta discusión, adoptaremos la perspectiva de que aumentar la dimensionalidad tiene un valor intrínseco. Interpretamos esto como evidencia que respalda el beneficio de proyectar datos en espacios de dimensiones superiores. Si bien existen explicaciones alternativas válidas, en lo que a nosotros respecta, esta observación sirve como motivación para las 32 columnas de características diseñadas manualmente que creamos para nuestro conjunto de datos. Creemos que, al aumentar el número de columnas adicionales, podremos volver a alcanzar nuevos mínimos de error. Sin embargo, esta vez, en lugar de añadir ceros, pretendemos añadir información significativa. 

Una vez establecida nuestra motivación, procedemos a identificar nuestras columnas de entrada y salida. Primero recopilamos todas las columnas de entrada y las almacenamos en una variable llamada X.

X = data.iloc[:,1:-4].columns

A continuación, enumeramos nuestras variables objetivo. 

y2 = data['Target 2']

También necesitaremos definir una función que devuelva una nueva instancia de nuestro modelo estadístico. 

return(RandomForestClassifier(random_state=0,n_estimators=500,max_depth=3,min_samples_leaf=20))

A continuación, excluimos cualquier dato que se solape con el período de prueba retrospectiva, ya que no queremos sobreajustar el modelo a todos los datos disponibles. Es importante reservar algunos datos exclusivamente para pruebas. A continuación, estandarizamos y escalamos nuestro conjunto de datos restando las medias de las columnas y dividiendo por las desviaciones estándar de cada una de las 40 columnas. Este proceso genera un conjunto de datos a escala.

data = data.iloc[:-(365*2),:]

Z = pd.DataFrame(columns=['Z1','Z2'])

Z['Z1'] = data.loc[:,X].mean()
Z['Z2'] = data.loc[:,X].std()

data.loc[:,X] = (data.loc[:,X] - data.loc[:,X].mean()) / data.loc[:,X].std()

Finalmente, ajustamos nuestro modelo a todos los datos de entrenamiento antes de preparar la exportación del modelo en formato ONNX, lo que permite su uso dentro de MQL5.

import onnx
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType
from sklearn.ensemble import GradientBoostingRegressor

model = GradientBoostingRegressor(random_state=0,max_depth=3)
model.fit(data.loc[:,X],data.loc[:,'Target 2'])

initial_types = [("FLOAT INPUT",FloatTensorType([1,len(X)]))]

model_proto = convert_sklearn(model,initial_types=initial_types,target_opset=12)

onnx.save(model_proto,"EURUSD GBR PRICE D1.onnx")


Reuniéndolo todo 

Ya estamos listos para comenzar a ensamblar nuestro Asesor Experto. Nuestra primera tarea consiste en definir constantes globales que no estén destinadas a cambiar. Tenga en cuenta que muchas de estas definiciones son las mismas que las establecidas anteriormente en nuestro script de obtención de datos. En concreto, el período de la media móvil y el tipo de media móvil permanecen fijos con los mismos valores. Además, hemos definido la duración de cada posición, así como el plazo en el que operaremos.

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
//--- Define our moving average indicator
#define MA_PERIOD 2                             //Moving Average Period
#define MA_TYPE   MODE_SMA                      //Type of moving average we have
#define HORIZON   10                            //Forecast horizon
#define TF PERIOD_D1

A continuación, declaramos variables globales importantes. Por ejemplo, las puntuaciones Z1 y Z2 utilizadas para estandarizar y escalar nuestro conjunto de datos deben mantenerse dentro del Asesor Experto. También necesitamos variables globales para los manejadores de promedio móvil y sus correspondientes búferes.

//+------------------------------------------------------------------+
//| Global definitions                                               |
//+------------------------------------------------------------------+
float Z1[] =
  {
   1.23933432e+00,  1.24403263e+00,  1.23474846e+00,  1.23936216e+00,
   1.23935910e+00,  1.23933128e+00,  1.24402971e+00,  1.23474522e+00,
   3.83991053e-05,  3.60920275e-05,  3.66240614e-05,  3.55759706e-05,
   3.68749001e-05,  3.98194600e-05,  3.78958300e-05,  3.79070139e-05,
   -2.78415082e-05, -3.06646429e-05,  4.58586036e-03,  4.58408532e-03,
   -4.69831123e-03, -4.70061831e-03,  9.28417159e-03,  9.28470363e-03,
   -4.67046972e-03, -4.66995367e-03,  4.61370187e-03,  4.61474996e-03,
   -2.78151462e-05, -3.07597060e-05,  4.58606247e-03,  4.58415002e-03,
   -4.69842067e-03, -4.70034430e-03,  9.28448314e-03,  9.28449433e-03,
   -4.67060553e-03, -4.66958460e-03,  4.61387762e-03,  4.61490973e-03
  };

float Z2[]=
  {
   0.12576155, 0.12640182, 0.125071, 0.12572605, 0.12568469,
   0.125719, 0.12636385, 0.12503521, 0.0150256, 0.01494947,
   0.01478075, 0.01493629, 0.0141562, 0.01423137, 0.01419596,
   0.01404453, 0.00669432, 0.0066951, 0.00482275, 0.004823,
   0.00493041, 0.00493002, 0.0063063, 0.00630607, 0.0048614,
   0.0048616, 0.00471017, 0.0047104, 0.00471147, 0.00471252,
   0.00361188, 0.00361259, 0.00371563, 0.00371488, 0.00513505,
   0.00513498, 0.0037117, 0.0037125, 0.00353196, 0.00353191
  };

//--- Our handlers for our indicators
int ma_handle,ma_o_handle,ma_h_handle,ma_l_handle;

//--- Data structures to store the readings from our indicators
double ma_reading[],ma_o_reading[],ma_h_reading[],ma_l_reading[];

int fetch = HORIZON * 2;
int timer = 0;
int state = 0;

Además, cargaremos nuestro modelo ONNX como recurso en el Asesor Experto.

//+------------------------------------------------------------------+
//| Resources                                                        |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| DISCLAIMER                                                       |
//| This ONNX model was trained from 1 January 2003 until 29 January |
//| 2023. For reliable results, ensure that all back tests are done  |
//| beyond the model's training period.                              |
//+------------------------------------------------------------------+
#resource "\\Files\\MA Approximation\\EURUSD GBR MA D1.onnx" as const uchar onnx_proto[];

También cargaremos las bibliotecas y dependencias necesarias. Por ejemplo, existe una biblioteca MQL5 específica para la gestión de operaciones, así como una biblioteca personalizada desarrollada con el tiempo para el manejo de modelos ONNX y recuperar información de operaciones.

//+------------------------------------------------------------------+
//| Dependencies                                                     |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
#include <VolatilityDoctor\ONNX\ONNXFloat.mqh>
#include <VolatilityDoctor\Time\Time.mqh>
#include <VolatilityDoctor\Trade\TradeInfo.mqh>

CTrade         Trade;
ONNXFloat      *onnx_handler;
Time           *time_handler;
TradeInfo      *trade_handler;

Durante la secuencia de inicialización del Asesor Experto, cargaremos todas estas bibliotecas e indicadores técnicos.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   onnx_handler  = new ONNXFloat(onnx_proto);
   time_handler  = new Time(Symbol(),TF);
   trade_handler = new TradeInfo(Symbol(),TF);

   Print("Onnx Handler Pointer: ",onnx_handler);

   onnx_handler.DefineOnnxInputShape(0,1,40);

   onnx_handler.DefineOnnxOutputShape(0,1,1);

//---Setup our technical indicators
   ma_handle = iMA(_Symbol,TF,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE);
   ma_o_handle = iMA(_Symbol,TF,MA_PERIOD,0,MA_TYPE,PRICE_OPEN);
   ma_h_handle = iMA(_Symbol,TF,MA_PERIOD,0,MA_TYPE,PRICE_HIGH);
   ma_l_handle = iMA(_Symbol,TF,MA_PERIOD,0,MA_TYPE,PRICE_LOW);

//---
   return(INIT_SUCCEEDED);
  }

Para garantizar una gestión segura de la memoria, eliminaremos todos los objetos creados dinámicamente y liberaremos los indicadores técnicos que ya no se utilicen. 

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   IndicatorRelease(ma_handle);
   IndicatorRelease(ma_o_handle);
   IndicatorRelease(ma_h_handle);
   IndicatorRelease(ma_l_handle);
   delete time_handler;
   delete trade_handler;
   delete onnx_handler;
  }

Cada vez que recibamos nuevas actualizaciones de precios, verificaremos si se ha formado una nueva vela. En ese caso, actualizaremos nuestros indicadores técnicos antes de comprobar si hay señales de trading. 

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   if(time_handler.NewCandle())
     {
      update();
      check_signal();
     }
  }

El método para actualizar los indicadores técnicos es el siguiente: primero, copiamos todas las lecturas de los indicadores en sus respectivos búferes. Esto prepara el vector de entrada para nuestro modelo ONNX. El modelo aceptará el mismo conjunto de 40 entradas que se utilizó en la tarea anterior. Antes de pasar estos datos de entrada al modelo, los estandarizamos y escalamos. A continuación, el modelo genera una predicción.

//+------------------------------------------------------------------+
//| Update our technical data                                        |
//+------------------------------------------------------------------+
void update(void)
  {

//---Set the values as series
   CopyBuffer(ma_handle,0,0,fetch,ma_reading);
   ArraySetAsSeries(ma_reading,true);
   CopyBuffer(ma_o_handle,0,0,fetch,ma_o_reading);
   ArraySetAsSeries(ma_o_reading,true);
   CopyBuffer(ma_h_handle,0,0,fetch,ma_h_reading);
   ArraySetAsSeries(ma_h_reading,true);
   CopyBuffer(ma_l_handle,0,0,fetch,ma_l_reading);
   ArraySetAsSeries(ma_l_reading,true);

   vectorf model_input_vector =
     {
      //--- OHLC
      iOpen(_Symbol,PERIOD_CURRENT,0),
      iHigh(_Symbol,PERIOD_CURRENT,0),
      iLow(_Symbol,PERIOD_CURRENT,0),
      iClose(_Symbol,PERIOD_CURRENT,0),
      //--- MA OHLC
      ma_reading[0],
      ma_o_reading[0],
      ma_h_reading[0],
      ma_l_reading[0],
      //--- Growth in OHLC
      iOpen(_Symbol,PERIOD_CURRENT,0)   - iOpen(_Symbol,PERIOD_CURRENT,(0 + HORIZON)),
      iHigh(_Symbol,PERIOD_CURRENT,0)   - iHigh(_Symbol,PERIOD_CURRENT,(0 + HORIZON)),
      iLow(_Symbol,PERIOD_CURRENT,0)    - iLow(_Symbol,PERIOD_CURRENT,(0 + HORIZON)),
      iClose(_Symbol,PERIOD_CURRENT,0)  - iClose(_Symbol,PERIOD_CURRENT,(0 + HORIZON)),
      //--- Growth in MA OHLC
      ma_reading[0]   - ma_reading[(0 + HORIZON)],
      ma_o_reading[0] - ma_o_reading[(0 + HORIZON)],
      ma_h_reading[0] - ma_h_reading[(0 + HORIZON)],
      ma_l_reading[0] - ma_l_reading[(0 + HORIZON)],
      //--- Growth between channels
      iOpen(_Symbol,PERIOD_CURRENT,0) - iClose(_Symbol,PERIOD_CURRENT,0),
      iOpen(_Symbol,PERIOD_CURRENT,0 + HORIZON) - iClose(_Symbol,PERIOD_CURRENT,0 + HORIZON),
      iOpen(_Symbol,PERIOD_CURRENT,0) - iLow(_Symbol,PERIOD_CURRENT,0),
      iOpen(_Symbol,PERIOD_CURRENT,0 + HORIZON) - iLow(_Symbol,PERIOD_CURRENT,0 + HORIZON),
      iOpen(_Symbol,PERIOD_CURRENT,0) - iHigh(_Symbol,PERIOD_CURRENT,0),
      iOpen(_Symbol,PERIOD_CURRENT,0 + HORIZON) - iHigh(_Symbol,PERIOD_CURRENT,0 + HORIZON),
      iHigh(_Symbol,PERIOD_CURRENT,0) - iLow(_Symbol,PERIOD_CURRENT,0),
      iHigh(_Symbol,PERIOD_CURRENT,0 + HORIZON) - iLow(_Symbol,PERIOD_CURRENT,0 + HORIZON),
      iClose(_Symbol,PERIOD_CURRENT,0)  -  iHigh(_Symbol,PERIOD_CURRENT,0),
      iClose(_Symbol,PERIOD_CURRENT,0 + HORIZON)  -  iHigh(_Symbol,PERIOD_CURRENT,0 + HORIZON),
      iClose(_Symbol,PERIOD_CURRENT,0)  -  iLow(_Symbol,PERIOD_CURRENT,0),
      iClose(_Symbol,PERIOD_CURRENT,0 + HORIZON)  -  iLow(_Symbol,PERIOD_CURRENT,0 + HORIZON),
      //--- Growth between moving average channels
      ma_o_reading[0] - ma_reading[0],
      ma_o_reading[(0 + HORIZON)] -  ma_reading[(0 + HORIZON)],
      ma_o_reading[0] -  ma_l_reading[0],
      ma_o_reading[(0 + HORIZON)] - ma_l_reading[(0 + HORIZON)],
      ma_o_reading[0] -  ma_h_reading[0],
      ma_o_reading[(0 + HORIZON)] - ma_h_reading[(0 + HORIZON)],
      ma_h_reading[0] - ma_l_reading[0],
      ma_h_reading[(0 + HORIZON)] - ma_l_reading[(0 + HORIZON)],
      ma_reading[0] -  ma_h_reading[0],
      ma_reading[(0 + HORIZON)] - ma_h_reading[(0 + HORIZON)],
      ma_reading[0] -  ma_l_reading[0],
      ma_reading[(0 + HORIZON)] - ma_l_reading[(0 + HORIZON)]
     };

   for(int i =0;i<40;i++)
     {
      model_input_vector[i] = ((model_input_vector[i] - Z1[i]) / Z2[i]);
     }

   onnx_handler.Predict(model_input_vector);
  }

Nuestra función de verificación de señales funciona según lo previsto. Comenzamos reiniciando el temporizador si no hay posiciones abiertas en ese momento, restableciendo así el estado del sistema. Si el modelo ONNX predice una tendencia alcista en los precios, abriremos una posición de compra; si predice una tendencia bajista, abriremos una posición de venta. Cabe destacar que las señales alcistas y bajistas corresponden a las probabilidades de clase que arroja el modelo: las probabilidades superiores a 0,5 indican una tendencia alcista esperada, mientras que las probabilidades inferiores a 0,5 indican una tendencia bajista esperada. 

Además de la señal del modelo ONNX, buscamos confirmación a partir de nuestro patrón de cruce de medias móviles. Si ya existe una posición abierta, hacemos un seguimiento del temporizador y, una vez que se acerca al vencimiento de la posición predefinida, cerramos todas las posiciones abiertas y reiniciamos el ciclo.

//+------------------------------------------------------------------+
//| Check if we have oppurtunities to trade                          |
//+------------------------------------------------------------------+
void check_signal(void)
  {
   if(PositionsTotal() == 0)
     {
      timer = 0;
      state = 0;

      if(onnx_handler.GetPrediction() > 0.5  
        {
         state =1;
         Trade.Buy(trade_handler.MinVolume(),trade_handler.GetSymbol(),trade_handler.GetAsk(),0,0,"");
        }

      else
         if(onnx_handler.GetPrediction() < 0.5 && ma_reading[0] < ma_o_reading[0])
           {
            state =-1;
            Trade.Sell(trade_handler.MinVolume(),trade_handler.GetSymbol(),trade_handler.GetBid(),0,0,"");
           }
     }

   else
     {
      timer++;
      if(timer >= HORIZON)
         Trade.PositionClose(Symbol());
     }

  }

Por último, recuerde siempre eliminar la definición de todas las constantes del sistema.

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef HORIZON
#undef MA_PERIOD
#undef MA_TYPE
#undef TF

Ahora estamos listos para probar nuestro sistema con los datos que le ocultamos durante el entrenamiento. Seleccione todas las fechas que tenemos fuera de nuestro período de entrenamiento; recuerde que nuestro período de entrenamiento finalizó el 29 de enero de 2023.

Figura 4: Nuestros días de pruebas retrospectivas siempre están fuera del período de entrenamiento que mostramos al modelo.

Asegúrese de seleccionar "Retraso aleatorio" para obtener una simulación realista de la naturaleza impredecible de las sesiones de negociación reales.

Figura 5: Seleccione "Retraso aleatorio" para obtener la configuración de prueba retrospectiva más robusta disponible.

Nuestra estrategia generó la curva de capital representada en la Figura 6. Nos complace comprobar que hemos logrado mantener una tendencia positiva al alza incluso al probar nuestro modelo con datos que no había visto antes. Es posible que, al entrenar el modelo con una imagen de alta resolución del mercado utilizando las 40 columnas que generamos, nuestro modelo sea capaz de generalizar mejor a condiciones no vistas durante el entrenamiento.

Figura 6: Visualización de la curva de capital producida por nuestro modelo estadístico.

Por último, siempre podemos consultar un resumen detallado del rendimiento de nuestra estrategia. Podemos observar que el 52,9% de las operaciones realizadas con nuestra estrategia fueron rentables, y que, en general, se espera que nuestro beneficio medio sea mayor que nuestra pérdida media. Estas estadísticas son alentadoras y demuestran la ventaja de dedicar tiempo a elaborar características detalladas en los conjuntos de datos. Si bien este proceso puede ser tedioso, siempre vale la pena el esfuerzo adicional cuando da sus frutos.

Figura 7: Resumen detallado del rendimiento alcanzado por nuestra aplicación de negociación.


Conclusión

En conclusión, este artículo ha proporcionado al lector numerosas ideas prácticas sobre cómo se pueden replantear las estrategias establecidas e incorporarles nuevas capacidades.

Aprovechando principios bien conocidos de la ciencia de datos, como la capacidad de los modelos para funcionar mejor en espacios de mayor dimensión, pudimos mitigar de forma consistente el retraso en nuestras estrategias de cruce de medias móviles. Esto se logró mediante la elaboración manual de nuestros propios conjuntos de datos detallados, lo que permitió a nuestro modelo obtener una comprensión profunda del mercado. Si bien estos principios de la ciencia de datos están bien estudiados, es fundamental gestionar las expectativas.

Es importante que el lector tenga en cuenta que aumentar el tamaño de los conjuntos de datos no siempre garantiza un mejor rendimiento. Más bien, los lectores deben comprender que siempre es beneficioso investigar si proyectar conjuntos de datos a dimensiones superiores puede generar mejoras. Este enfoque no ofrece garantías, pero siempre merece la pena explorarlo. El lector también ha aprendido que el desfase asociado a los indicadores técnicos puede ser cuestionado y abordado eficazmente mediante el pensamiento crítico y la creatividad. El potencial de la terminal MetaTrader 5 parece ser enorme.

Nombre del archivo Descripción del archivo
EURUSD GBR MA D1.onnx El modelo ONNX que construimos juntos utilizando nuestro conjunto de datos de columnas de alta dimensión.
Proof of Case Article.ipynb El cuaderno Jupyter que escribimos juntos para demostrar la ventaja de proyectar datos a dimensiones superiores.
MA Strategy EURUSD.ex5 Una versión compilada de la aplicación de trading que desarrollamos para aprovechar nuestro conjunto de datos elaborado manualmente.
Fetch Data MA.mq5 El script MQL5 que escribimos para obtener nuestro conjunto de datos de alta dimensión y escribirlo en formato CSV.

Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/18525

Utilizando redes neuronales en MetaTrader Utilizando redes neuronales en MetaTrader
En el artículo se muestra la aplicación de las redes neuronales en los programas de MQL, usando la biblioteca de libre difusión FANN. Usando como ejemplo una estrategia que utiliza el indicador MACD se ha construido un experto que usa el filtrado con red neuronal de las operaciones. Dicho filtrado ha mejorado las características del sistema comercial.
Simulación de mercado: Position View (XVII) Simulación de mercado: Position View (XVII)
En el artículo anterior hicimos que el indicador mostrara el resultado financiero. Sin embargo, no a todos les gusta utilizar este modo de visualización. El motivo puede variar de un operador a otro, aunque en algunos casos me parece bastante razonable y justificable. Adaptar el código para ofrecer esta posibilidad no es, ni mucho menos, una de las tareas más complejas. En realidad, se trata de algo bastante sencillo. En este artículo veremos cómo hacerlo.
Particularidades del trabajo con números del tipo double en MQL4 Particularidades del trabajo con números del tipo double en MQL4
En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.
Del básico al intermedio: Colas, listas y árboles (VI) Del básico al intermedio: Colas, listas y árboles (VI)
En este artículo retomaremos la implementación de un árbol. Ahora que conocemos los conceptos básicos sobre el funcionamiento de los constructores y destructores, finalmente podremos corregir el código presentado en el último artículo. Prepárate para una verdadera aventura en la programación con MQL5.