English Русский 中文 Deutsch 日本語
preview
Reimaginando las estrategias clásicas en MQL5 (Parte 14): Análisis de múltiples estrategias

Reimaginando las estrategias clásicas en MQL5 (Parte 14): Análisis de múltiples estrategias

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

En nuestro artículo anterior de nuestra serie de artículos sobre los asesores expertos autooptimizantes, nos propusimos el reto de crear un conjunto de múltiples estrategias y combinarlas en una única estrategia más potente que la que teníamos inicialmente.

Decidimos que nuestras estrategias trabajaran de forma colaborativa mediante una especie de sistema democrático, en el que cada estrategia disponía de un único voto. El peso de cada voto se convirtió en un parámetro de ajuste que, tal y como hemos comentado anteriormente, dejamos que el optimizador genético ajustara para maximizar la rentabilidad de nuestra estrategia de trading. A continuación, eliminamos la estrategia a la que el optimizador genético había asignado la menor ponderación, con lo que nos quedamos con las dos estrategias que ahora vamos a analizar y en torno a las cuales vamos a elaborar modelos estadísticos.

En este artículo, extrajimos los datos de mercado mediante un script de MQL5, basándonos en los mejores resultados identificados por nuestro optimizador genético. Recuerda que seleccionamos los resultados que se mantuvieron estables tanto en la prueba retrospectiva como en la prueba prospectiva (forward test), y que eso fue lo que nos llevó a tomar la decisión.

Sin embargo, al analizar más detenidamente los rendimientos generados por las dos estrategias seleccionadas por el optimizador genético, descubrimos que dichas estrategias presentaban una elevada correlación entre sí. En otras palabras, ambas estrategias tendían a obtener beneficios y a sufrir pérdidas más o menos al mismo tiempo. Contar con dos estrategias de trading muy correlacionadas no supone, en la práctica, ninguna ventaja respecto a tener una sola estrategia, y tener una sola estrategia frustra por completo el objetivo del análisis de estrategias múltiples.

Hay muchas cosas que podrían salir mal al intentar utilizar la inteligencia artificial para ayudar a diseñar estrategias de trading, y parece que el optimizador genético terminó aprovechando la formulación del problema que le dimos y seleccionó las estrategias más correlacionadas. Desde un punto de vista puramente matemático, esto puede considerarse una jugada inteligente: al optimizador genético le resulta más fácil prever el saldo global de la cuenta cuando las estrategias dominantes están correlacionadas.

Inicialmente, esperaba que el optimizador genético asignara ponderaciones más altas a las estrategias más rentables y ponderaciones más bajas a las menos rentables. Sin embargo, dado que solo teníamos 3 estrategias para elegir y este procedimiento de optimización se realizó solo una vez, no podemos descartar que todo esto haya ocurrido por casualidad. Es decir, si repitiéramos la optimización de las ponderaciones de los votos utilizando un algoritmo de optimización lento y completo, entonces tal vez nuestro optimizador no habría seleccionado estrategias correlacionadas. 

Esta observación me impulsó a revisar el enfoque que utilizamos para seleccionar la configuración óptima para nuestras estrategias. Parece que, inicialmente, deberíamos mantener fijos en uno todos los pesos de cada voto. Esto obliga al optimizador genético a centrarse exclusivamente en encontrar la configuración más rentable para cada indicador que utilizamos. Como veremos a lo largo de nuestro recorrido juntos, este enfoque revisado resulta ser mejor que nuestro plan inicial. Cuando se utilizan dos estrategias correlacionadas para el análisis de estrategias múltiples, no se logra un progreso real; por lo tanto, hemos aprendido una mejor manera de plantear el problema objetivo del análisis de estrategias múltiples: "¿Cómo podemos seleccionar de la mejor manera múltiples estrategias que tengan rendimientos no correlacionados y maximizar la rentabilidad de nuestra cuenta?". 



Primeros pasos en MQL5

En primer lugar, escribiremos un script para obtener datos históricos del mercado utilizando la configuración que observamos para maximizar nuestros rendimientos durante nuestra prueba anterior, en la que seleccionamos las dos estrategias que estamos utilizando hasta ahora. Nuestro sistema se basará en unos pocos parámetros fijos que aprendimos de las pruebas de optimización genética que comentamos anteriormente. Estos parámetros de nuestra estrategia ideal permanecerán fijos mientras recopilamos los datos.

//+------------------------------------------------------------------+
//|                                                      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  100               //--- Period for our moving average
#define MA_TYPE    MODE_EMA          //--- Type of moving average we have 
#define RSI_PERIOD 24                //--- Period For Our RSI Indicator
#define RSI_PRICE  PRICE_CLOSE       //--- Applied Price For our RSI Indicator
#define HORIZON    38                //--- Holding period
#define TF PERIOD_H3                 //--- Time Frame

Nuestro sistema dependerá de varias variables globales importantes responsables de realizar un seguimiento de las lecturas de nuestros indicadores técnicos, almacenadas en los manejadores y búferes apropiados que utilizaremos durante la ejecución de nuestro script. Además, definiremos otras variables, como el nombre del archivo de salida y la cantidad de datos que se solicitarán.

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

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

//--- File name
string file_name = Symbol() + " Market Data As Series Multiple Strategy Analysis.csv";

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

El cuerpo de nuestro guion engloba las principales tareas que pretendemos completar hoy. Inicializaremos nuestros indicadores y configuraremos las lecturas de los indicadores como series, asegurándonos de que los valores estén organizados de forma seriada para estructurar y exportar los datos según el formato previsto. Así es como pretendemos estructurar y compartir nuestros datos. A partir de ahí, escribiremos todos los datos de mercado en un archivo y realizaremos algunos cálculos aritméticos para rastrear los cambios históricos en los datos del mercado, utilizando el parámetro de horizonte ajustado por nuestro optimizador genético en nuestra discusión anterior.

//+------------------------------------------------------------------+
//| Our script execution                                             |
//+------------------------------------------------------------------+
void OnStart()
  {
  int fetch = size + (HORIZON * 2);
//---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);
   rsi_handle = iRSI(_Symbol,TF,RSI_PERIOD,RSI_PRICE);

//---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(rsi_handle,0,0,fetch,rsi_reading);
   ArraySetAsSeries(rsi_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","True Open","True High","True Low","True Close","True MA C","True MA O","True RSI","Open","High","Low","Close","MA Close","MA Open","RSI");
        }

      else
        {
         FileWrite(file_handle,
                   iTime(_Symbol,TF,i), 
                   iOpen(_Symbol,TF,i),
                   iHigh(_Symbol,TF,i),
                   iLow(_Symbol,TF,i),
                   iClose(_Symbol,TF,i),
                   ma_reading[i],
                   ma_o_reading[i],
                   rsi_reading[i],
                   iOpen(_Symbol,TF,i)   - iOpen(_Symbol,TF,(i + HORIZON)), 
                   iHigh(_Symbol,TF,i)   - iHigh(_Symbol,TF,(i + HORIZON)),
                   iLow(_Symbol,TF,i)    - iLow(_Symbol,TF,(i + HORIZON)),
                   iClose(_Symbol,TF,i)  - iClose(_Symbol,TF,(i + HORIZON)),
                   ma_reading[i] - ma_reading[(i + HORIZON)],
                   ma_o_reading[i] - ma_o_reading[(i + HORIZON)],
                   rsi_reading[i] - rsi_reading[(i + HORIZON)]
                   );
        }
     }
//--- Close the file
   FileClose(file_handle);
  }
//+------------------------------------------------------------------+


Analizando los datos en Python

Ahora estamos listos para comenzar a analizar nuestros datos de mercado utilizando algunas bibliotecas numéricas disponibles en Python. Para empezar, cargaremos Pandas para leer los datos del mercado.

#Load our libraries
import pandas as pd

A continuación, etiquetaremos las acciones que habría tomado nuestra estrategia en entrenamiento bajo las condiciones de mercado dadas y calcularemos la ganancia o pérdida que cada acción habría producido.

#Read in the data
data = pd.read_csv("EURUSD Market Data As Series Multiple Strategy Analysis.csv")

#The optimal holding period suggested by our MT5 Genetic optimizer
HORIZON = 38

#Calculate the true market return
data['Return'] = data['True Close'].shift(-HORIZON) - data['True Close']

#The action suggested by our first strategy, MA Cross
data['Action 1'] = 0

#The action suggested by our second strategy, RSI Strategy
data['Action 2'] = 0

#Buy conditions
data.loc[data['True MA C'] > data['True MA O'],'Action 1'] = 1
data.loc[data['True RSI'] > 50,'Action 2'] = 1

#Sell conditions
data.loc[data['True MA C'] < data['True MA O'],'Action 1'] = -1
data.loc[data['True RSI'] < 50,'Action 2'] = -1

#Perform a linear transformation of the true market return, using our trading stragies
data['Return 1'] = data['Return'] * data['Action 1']
data['Return 2'] = data['Return'] * data['Action 2']

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

Este es un paso esencial en cualquier modelo estadístico y en la configuración de modelado y operativa. Debemos asegurarnos de que nuestro modelo no se ajuste en exceso a todos los datos; de lo contrario, cualquier análisis o prueba carecerá de sentido porque el modelo se habrá visto comprometido.

#Drop our back test data
_    = data.iloc[-((365 * 2 * 6)):,:]
data = data.iloc[:-((365 * 2 * 6)),:]

Etiquetar nuestros objetivos es una parte clave de cualquier proyecto de aprendizaje automático supervisado. Para facilitar la visualización, etiquetaremos nuestros objetivos para mostrar si el retorno generado por la estrategia 1 fue mayor que el de la estrategia 2, o viceversa. Nuestro objetivo nos indicará si la estrategia 2 produjo una mayor rentabilidad que la estrategia 1. A modo de comparación, lo contrastaremos con la capacidad de nuestro modelo para pronosticar directamente la rentabilidad futura del mercado.

#Gether inputs
X = data.iloc[:,1:15]

#Both Strategies will earn equal reward
data['Target 1'] = 0 
data['Target 2'] = 0 

#Strategy 1 is more profitable
data.loc[data['Return 1'] > data['Return 2'],'Target 1'] = 1

#Strategy 2 is more profitable
data.loc[data['Return 2'] > data['Return 1'],'Target 2'] = 1

#Classical Target
data['Classical Target'] = 0

data.loc[data['Return'] > 0,'Classical Target'] = 1

Ahora cargaremos nuestras bibliotecas scikit-learn para ayudarnos a analizar las propiedades numéricas de los datos de mercado que hemos recopilado.

#Loading our scikit learn libraries
from sklearn.model_selection import TimeSeriesSplit,cross_val_score
from sklearn.linear_model import LinearRegression,LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.neural_network import MLPRegressor
from sklearn.model_selection import RandomizedSearchCV

Comenzaremos creando objetos de validación de series temporales con cinco divisiones, asegurándonos de que la separación (gap) coincida con el horizonte óptimo hallado por nuestro optimizador genético. A continuación, calcularemos las medias y las desviaciones estándar de las columnas para estandarizar nuestro conjunto de datos de modo que tenga una media de cero y una desviación estándar de uno.

#Prepare the data for time series modelling
tscv = TimeSeriesSplit(n_splits=5,gap=HORIZON)

Z1 = X.mean()
Z2 = X.std()

X = ((X-X.mean()) / X.std())

Ahora mediremos la precisión con la que predecimos los nuevos objetivos que hemos establecido y la compararemos con la precisión con la que predecimos directamente el objetivo clásico de la rentabilidad futura de los precios. Utilizando los objetos de validación cruzada de scikit-learn, evaluaremos la precisión con un clasificador lineal. A continuación, almacenaremos estos resultados en una matriz y crearemos un gráfico de barras. Como se puede observar, nuestra precisión en el objetivo clásico ronda el 50%, mientras que nuestra precisión al predecir cuál de las dos estrategias será más rentable es de alrededor del 90%, superando claramente al objetivo clásico.

#Measuring our accuracy on our new target
res = []

model = LinearDiscriminantAnalysis()

res.append(np.mean(np.abs(cross_val_score(model,X,data['Classical Target'],cv=tscv,scoring='accuracy'))))

model = LinearDiscriminantAnalysis()

res.append(np.mean(np.abs(cross_val_score(model,X,data['Target 1'],cv=tscv,scoring='accuracy'))))

model = LinearDiscriminantAnalysis()

res.append(np.mean(np.abs(cross_val_score(model,X,data['Target 2'],cv=tscv,scoring='accuracy',n_jobs=-1))))

sns.barplot(res,color='black')
plt.xticks([0,1,2],['Classical Target','MA Cross Over Target','RSI Target'])
plt.axhline(res[0],linestyle=':',color='red')
plt.ylabel('5-Fold Percentage Accuracy %')
plt.title('Outperforming The Classical Target of Direct Price Prediction')

Figura 1: Estamos realizando mejoras con respecto a la tarea clásica de predicción directa de precios, modelando la relación entre nuestra estrategia y el mercado.

Finalmente, podemos utilizar la biblioteca de búsqueda aleatoria scikit-learn para ayudarnos a construir una red neuronal para nuestros datos de mercado. Comenzaremos inicializando nuestra red neuronal con la configuración predeterminada que deseamos mantener fija, como los parámetros de aleatorización y parada temprana.

#Use random search to build a neural network for our market data

#Initialize the model
model = MLPRegressor(shuffle=False,early_stopping=False)


distributions = {'solver':['lbfgs','adam','sgd'],
                 'hidden_layer_sizes':[(X.shape[1],2,10,20),(X.shape[1],30,50,10),(X.shape[1],14,14,14),(X.shape[1],5,20,2),(X.shape[1],1,2,3,4,5,6,10),(X.shape[1],1,14,14,1)],
                 'activation':['relu','identity','logistic','tanh']
                 }

rscv = RandomizedSearchCV(model,distributions,n_jobs=-1,n_iter=50)

rscv.fit(X,data.loc[:,['Target 1','Target 2']])
Ya estamos listos para exportar nuestra red neuronal entrenada al formato ONNX. Para comenzar a exportar nuestra red neuronal al siguiente formato, primero cargaremos la biblioteca ONNX y luego los convertidores necesarios. Recuerda que ONNX, que significa Open Neural Network Exchange (Intercambio Abierto de Redes Neuronales), es un protocolo de código abierto que nos permite crear y exportar fácilmente nuestros modelos de aprendizaje automático de una manera independiente del modelo.
#Exporting our model to ONNX 
import onnx
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType

initial_types = [('float_input',FloatTensorType([1,X.shape[1]]))]
final_types = [('float_output',FloatTensorType([2,1]))]

model = rscv.best_estimator_

model.fit(X,data.loc[:,['Target 1','Target 2']])

onnx_proto = convert_sklearn(model=model,initial_types=initial_types,final_types=final_types,target_opset=12)

onnx.save(onnx_proto,'EURUSD NN MSA.onnx')

Para empezar a visualizar el grafo ONNX de nuestra red neuronal, primero importaremos la biblioteca Netron y luego simplemente usaremos la función netron.start y le pasaremos la ruta del modelo ONNX para poder visualizarlo. 

#Viewing our ONNX graph in netron
import netron
netron.start('../EURUSD NN MSA.onnx')

En la Figura 2 que aparece a continuación, hemos adjuntado los metadatos de nuestro modelo ONNX. Podemos observar que nuestro modelo ONNX tiene 14 entradas y 2 salidas, ambas de tipo flotante, así como otros metadatos importantes como el productor y la versión de ONNX.

Figura 2: Visualización de los metadatos asociados a nuestro modelo ONNX para validar que se especificaron correctamente los tamaños de entrada y salida.

Nuestro modelo ONNX representa los modelos de aprendizaje automático como grafos de nodos computacionales y aristas, que muestran cómo se transmite la información de un nodo computacional al siguiente. De esta forma, todos los modelos de aprendizaje automático pueden traducirse a un formato universal, que es el grafo ONNX, representado a continuación en la Figura 3. Este gráfico representa nuestra red neuronal, la cual construimos utilizando el procedimiento de búsqueda aleatoria proporcionado por la biblioteca sklearn.

Figura 3: Visualización del grafo computacional que representa nuestra red neuronal profunda utilizando la biblioteca Netron.


Construyendo nuestro Asesor Experto en MQL5

El primer paso para construir nuestro Asesor Experto es cargar el modelo ONNX que creamos en el paso anterior.

//+------------------------------------------------------------------+
//|                                                   MSA Test 1.mq5 |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| ONNX Model                                                       |
//+------------------------------------------------------------------+
#resource "\\Files\\EURUSD NN MSA.onnx" as uchar onnx_buffer[];

Las medias y las desviaciones estándar de las columnas que medimos en Python para cada columna se almacenarán en matrices apropiadas llamadas Z1 y Z2. Recordemos que vamos a utilizar estos valores para escalar y estandarizar cada una de nuestras entradas antes de obtener predicciones de nuestro modelo ONNX.

//+------------------------------------------------------------------+
//| ONNX Parameters                                                  |
//+------------------------------------------------------------------+
double Z1[] = { 1.18932220e+00,  1.19077958e+00,  1.18786462e+00,  1.18931542e+00,
                1.18994040e+00,  1.18994674e+00,  4.94395259e+01, -4.99204879e-04,
                -5.00701302e-04, -4.97575935e-04, -4.98995739e-04, -4.70848300e-04,
                -4.70289373e-04, -1.84697724e-02
                };

double Z2[] = {1.09599015e-01, 1.09698934e-01, 1.09479324e-01, 1.09593123e-01,
               1.09413744e-01, 1.09419007e-01, 1.00452009e+01, 1.31269558e-02,
               1.31336302e-02, 1.31513465e-02, 1.31174740e-02, 6.88794916e-03,
               6.89036979e-03, 1.28550006e+01
              };

Las constantes importantes del sistema se definirán y mantendrán durante toda la vida útil de nuestro programa. Recuerde que estas constantes fueron seleccionadas en la discusión anterior utilizando nuestro optimizador genético.

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
#define MA_SHIFT         0
#define MA_TYPE          MODE_EMA
#define RSI_PRICE        PRICE_CLOSE
#define ONNX_INPUTS      14
#define ONNX_OUTPUTS     2
#define HORIZON 38 

Mediante el optimizador genético, se seleccionaron parámetros estratégicos importantes, como el período de la media móvil y el período del RSI, y los mantendremos constantes a lo largo de nuestro programa.

//+------------------------------------------------------------------+
//| Strategy Parameters                                              |
//+------------------------------------------------------------------+
int             MA_PERIOD                       =        100;  //Moving Average Period
int             RSI_PERIOD                      =         24;  //RSI Period
ENUM_TIMEFRAMES STRATEGY_TIME_FRAME             =  PERIOD_H3;  //Strategy Timeframe
int             HOLDING_PERIOD                  =         38;  //Position Maturity Period

Necesitamos una cantidad considerable de dependencias para que nuestra aplicación esté completa. Algunas dependencias, como la biblioteca de trading, deberían resultar obvias para los lectores. Otras estrategias, como las que desarrollamos juntos en nuestra serie de artículos hermana, también deberían resultarle familiares a estas alturas si nos han estado siguiendo. De lo contrario, las estrategias que estamos cargando son necesarias para que nuestra aplicación de trading funcione.

//+------------------------------------------------------------------+
//| Dependencies                                                     |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
#include <VolatilityDoctor\Time\Time.mqh>
#include <VolatilityDoctor\Trade\TradeInfo.mqh>
#include <VolatilityDoctor\Strategies\OpenCloseMACrossover.mqh>
#include <VolatilityDoctor\Strategies\RSIMidPoint.mqh>

Tendremos variables globales importantes que se utilizarán a lo largo de nuestro programa, pero afortunadamente, solo necesitamos unas pocas. Por ejemplo, necesitamos variables globales para algunas de las clases personalizadas que hemos creado, como las clases de operaciones y tiempo, la estrategia RSI y las clases de estrategia de cruce. Serán necesarias otras variables globales para que podamos obtener lecturas de nuestro modelo ONNX y almacenar las predicciones que realiza.

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
//--- Custom Types
CTrade               Trade;
Time                 *TradeTime;
TradeInfo            *TradeInformation;
RSIMidPoint          *RSIMid;
OpenCloseMACrossover *MACross;
long                 onnx_model;
vectorf onnx_output;

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

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

//--- System Types
int                  position_timer;

Cada vez que nuestra aplicación se inicialice por primera vez, crearemos nuevas instancias de los objetos dinámicos que necesitemos. Por ejemplo, tenemos una clase dedicada a llevar un registro del tiempo y la información de las transacciones. Crearemos nuevas instancias de esta clase, así como nuevas instancias de los controladores de indicadores apropiados que necesitaremos. A partir de ahí, crearemos nuestro modelo ONNX a partir del búfer que cargamos y validaremos que el modelo se haya cargado correctamente.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Create dynamic instances of our custom types
   TradeTime        = new Time(Symbol(),STRATEGY_TIME_FRAME);
   TradeInformation = new TradeInfo(Symbol(),STRATEGY_TIME_FRAME);
   MACross          = new OpenCloseMACrossover(Symbol(),STRATEGY_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_TYPE);
   RSIMid           = new RSIMidPoint(Symbol(),STRATEGY_TIME_FRAME,RSI_PERIOD,RSI_PRICE);
   onnx_model       = OnnxCreateFromBuffer(onnx_buffer,ONNX_DEFAULT);
   onnx_output      = vectorf::Zeros(ONNX_OUTPUTS);

//---Setup our technical indicators
   ma_handle        = iMA(_Symbol,STRATEGY_TIME_FRAME,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE);
   ma_o_handle      = iMA(_Symbol,STRATEGY_TIME_FRAME,MA_PERIOD,0,MA_TYPE,PRICE_OPEN);
   rsi_handle       = iRSI(_Symbol,STRATEGY_TIME_FRAME,RSI_PERIOD,RSI_PRICE);
   if(onnx_model != INVALID_HANDLE)
     {
      Print("Preparing ONNX model");

      ulong input_shape[] = {1,ONNX_INPUTS};

      if(!OnnxSetInputShape(onnx_model,0,input_shape))
        {
         Print("Failed To Specify ONNX model input shape");
         return(INIT_FAILED);
        }

      ulong output_shape[] = {ONNX_OUTPUTS,1};

      if(!OnnxSetOutputShape(onnx_model,0,output_shape))
        {
         Print("Failed To Specify ONNX model output shape");
         return(INIT_FAILED);
        }
     }

//--- Everything was fine
   Print("Successfully loaded all components for our Expert Advisor");
   return(INIT_SUCCEEDED);
  }
//--- End of OnInit Scope

Cuando nuestra aplicación deje de utilizarse, eliminaremos la memoria que ya no estemos usando y liberaremos esos recursos para otras aplicaciones, con el fin de desactivar nuestra aplicación de forma segura.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Delete the dynamic objects
   delete TradeTime;
   delete TradeInformation;
   delete MACross;
   delete RSIMid;
   OnnxRelease(onnx_model);
   IndicatorRelease(ma_handle);
   IndicatorRelease(ma_o_handle);
   IndicatorRelease(rsi_handle);
  }
//--- End of Deinit Scope

Cada vez que se reciban nuevos precios en OnTick, comprobaremos primero si se ha formado completamente una nueva vela diaria llamando a la función new_candle dentro de la clase ChangeTime. Si efectivamente se ha formado una vela, actualizaremos los parámetros de nuestra estrategia antes de buscar una oportunidad para operar. Si existen oportunidades para comerciar, las aprovecharemos. De lo contrario, esperaremos a que nuestra posición venza antes de cerrarla.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Check if a new daily candle has formed
   if(TradeTime.NewCandle())
     {
      //--- Update strategy
      Update();

      //--- If we have no open positions
      if(PositionsTotal() == 0)
        {
         //--- Reset the position timer
         position_timer = 0;

         //--- Check for a trading signal
         CheckSignal();
        }

      //--- Otherwise
      else
        {
         //--- The position has reached maturity
         if(position_timer == HOLDING_PERIOD)
            Trade.PositionClose(Symbol());
         //--- Otherwise keep holding
         else
            position_timer++;
        }
     }
  }
//--- End of OnTick Scope

Nuestro método de actualización tiene en cuenta algunos parámetros importantes, como el horizonte de previsión que seleccionamos utilizando nuestro optimizador genético. A partir de ahí, actualiza las estrategias que estamos utilizando y las lecturas de los indicadores técnicos almacenadas en nuestros búferes.

//+------------------------------------------------------------------+
//| Update our technical indicators                                  |
//+------------------------------------------------------------------+
void Update(void)
  {
   int fetch = (HORIZON * 2);

//--- Update the strategy
   RSIMid.Update();
   MACross.Update();

//---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(rsi_handle,0,0,fetch,rsi_reading);
   ArraySetAsSeries(rsi_reading,true);
  }
//--- End of Update Scope

Obtención de una predicción de nuestro modelo ONNX. Para obtener una predicción del modelo ONNX, utilizamos la función de ejecución de ONNX. Pero antes de llamarlo, primero debemos actualizar las variables de entrada que se pasarán al modelo, luego escalar y estandarizar estos valores restando la media y dividiendo por la desviación estándar.

//+------------------------------------------------------------------+
//| Get A Prediction from our ONNX model                             |
//+------------------------------------------------------------------+
void OnnxPredict(void)
  {
   vectorf   input_variables =
     {
      iOpen(_Symbol,STRATEGY_TIME_FRAME,0),
      iHigh(_Symbol,STRATEGY_TIME_FRAME,0),
      iLow(_Symbol,STRATEGY_TIME_FRAME,0),
      iClose(_Symbol,STRATEGY_TIME_FRAME,0),
      ma_reading[0],
      ma_o_reading[0],
      rsi_reading[0],
      iOpen(_Symbol,STRATEGY_TIME_FRAME,0)   - iOpen(_Symbol,STRATEGY_TIME_FRAME,(0 + HORIZON)),
      iHigh(_Symbol,STRATEGY_TIME_FRAME,0)   - iHigh(_Symbol,STRATEGY_TIME_FRAME,(0 + HORIZON)),
      iLow(_Symbol,STRATEGY_TIME_FRAME,0)    - iLow(_Symbol,STRATEGY_TIME_FRAME,(0 + HORIZON)),
      iClose(_Symbol,STRATEGY_TIME_FRAME,0)  - iClose(_Symbol,STRATEGY_TIME_FRAME,(0 + HORIZON)),
      ma_reading[0] - ma_reading[(0 + HORIZON)],
      ma_o_reading[0] - ma_o_reading[(0 + HORIZON)],
      rsi_reading[0] - rsi_reading[(0 + HORIZON)]
     };

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

   OnnxRun(onnx_model,ONNX_DEFAULT,input_variables,onnx_output);
  }

Para comprobar si existe una señal de trading utilizando nuestra estrategia de cruce, primero debemos obtener una predicción del modelo ONNX. El modelo predecirá qué estrategia considera que será la más rentable. A partir de ahí, comprobamos esa estrategia en particular para ver si hay una señal de entrada correspondiente disponible. Solo ejecutamos una operación si el modelo prevé que la estrategia será rentable y si la estrategia nos ofrece una oportunidad de trading válida.

//+------------------------------------------------------------------+
//| Check for a trading signal using our cross-over strategy         |
//+------------------------------------------------------------------+
void CheckSignal(void)
  {

   OnnxPredict();

//--- MA Strategy is profitable
   if((onnx_output[0] > 0.5) && (onnx_output[1] < 0.5))
     {
      //--- Long positions when the close moving average is above the open
      if(MACross.BuySignal())
        {
         Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,"");
         return;
        }

      //--- Otherwise short
      else
         if(MACross.SellSignal())
           {
            Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,"");
            return;
           }
     }

//--- RSI strategy is profitable
   else
      if((onnx_output[0] < 0.5) && (onnx_output[1] > 0.5))
        {

         if(RSIMid.BuySignal())
           {
            Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,"");
            return;
           }

         //--- Otherwise short
         else
            if(MACross.SellSignal())
              {
               Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,"");
               return;
              }
        }
  }
//--- End of CheckSignal Scope

Hacia el final de nuestro sistema, eliminaremos la definición de todas las constantes del sistema que definimos al comienzo de nuestra aplicación.

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef MA_SHIFT
#undef RSI_PRICE
#undef MA_TYPE
#undef ONNX_INPUTS
#undef ONNX_OUTPUTS
#undef HORIZON
//+------------------------------------------------------------------+

Seleccionar los días adecuados para realizar las pruebas retrospectivas es sencillo. Recordemos que queremos probar nuestra nueva aplicación durante la fase de pruebas prospectivas utilizada en nuestra prueba anterior. Por lo tanto, las fechas de las pruebas se han ajustado en consecuencia.

Figura 4: Selección de los días de la prueba retrospectiva para que coincidan con la prueba prospectiva (forward test) que realizamos anteriormente.

Como siempre, queremos utilizar la configuración más realista posible, por lo que elegimos la configuración de retardo aleatorio.

Figura 5: Selección de nuestras condiciones de modelado basadas en "Retardo aleatorio" para obtener la configuración más realista posible.

Para mi sorpresa, la nueva configuración aplicada a nuestra aplicación, al incluir modelos estadísticos, degradó considerablemente nuestro rendimiento. Esto suele ser un claro indicio de que algo ha fallado en nuestro plan.

Figura 6: La nueva curva de capital que hemos generado muestra un deterioro claro en comparación con nuestro desempeño sin modelado estadístico.

Al examinar más de cerca el análisis detallado del rendimiento de nuestra aplicación, vemos que el beneficio neto total ha disminuido junto con el índice de Sharpe, lo cual no son buenas señales. Esto demuestra que nuestra aplicación no está obteniendo los beneficios previstos de las herramientas de modelado estadístico que elegimos.

Figura 7: Análisis detallado del rendimiento de nuestra nueva estrategia de trading basada en modelos estadísticos.


Revisando nuestros datos de mercado en Python

Tras una revisión más exhaustiva de nuestros datos de mercado en Python, quise examinar con mayor detalle cuál podría ser la fuente del error. Así que comencé importando las bibliotecas estándar que utilizamos para visualizar datos de mercado. A continuación, representé gráficamente la suma acumulada de algunos de los rendimientos generados por las dos estrategias, e inmediatamente el problema se hizo evidente.
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

Como podemos observar en el gráfico siguiente, nuestras dos estrategias presentan características y pendientes notablemente similares. Las dos estrategias tienden a subir y bajar al unísono, casi como si estuviéramos siguiendo una sola estrategia.

fig , axs = plt.subplots(2,1,sharex=True)

fig.suptitle('Visualizing The Individual Cumulative Return of our 2 Strategies')

sns.lineplot(data['Return 1'].cumsum(),ax=axs[0],color='black')

sns.lineplot(data['Return 2'].cumsum(),ax=axs[1],color='black')

Figura 8: Nuestro optimizador genético parece haber seleccionado estrategias altamente correlacionadas y les ha asignado los mayores pesos de votación.

Además, cuando representamos gráficamente la cantidad móvil de riesgo en los rendimientos generados por nuestras estrategias, vemos otro motivo de preocupación. El perfil de riesgo de nuestras dos estrategias es prácticamente indistinguible del del propio mercado. Una vez más, parece que simplemente estamos utilizando la misma estrategia. Es difícil distinguir entre la estrategia uno y la estrategia dos si no hubiera etiquetado las columnas de nuestro gráfico.

fig , axs = plt.subplots(3,1,sharex=True)

fig.suptitle('Visualizing The Risk In Our 2 Strategies')

sns.lineplot(data['Return'].rolling(window=HORIZON).var(),ax=axs[0],color='black')
axs[0].axhline(data['Return'].var(),color='red',linestyle=':')

sns.lineplot(data['Return 1'].rolling(window=HORIZON).var(),ax=axs[1],color='black')
axs[1].axhline(data['Return 1'].var(),color='red',linestyle=':')

sns.lineplot(data['Return 2'].rolling(window=HORIZON).var(),ax=axs[2],color='black')
axs[2].axhline(data['Return 2'].var(),color='red',linestyle=':')

Figura 9: Las dos estrategias son casi idénticas en sus niveles de riesgo y recompensa, lo que socava la idea que subyace al análisis de estrategias múltiples.

El golpe de gracia se da cuando calculamos la matriz de correlación producida por los tres rendimientos: el rendimiento del mercado, el rendimiento de la estrategia de cruce de medias móviles y el rendimiento de nuestra estrategia RSI. Podemos observar claramente que la estrategia de cruce de medias móviles y RSI tiene una correlación de aproximadamente 0,75, lo cual es muy fuerte. Esta fue la sugerencia final que me convenció de que el optimizador genético no necesariamente ajustaba las ponderaciones de los votos para maximizar la rentabilidad. Más bien, parece que estaba ajustando las ponderaciones para aislar estrategias verdaderamente correlacionadas, porque eso le facilita el trabajo.

plt.title('Correlation Between Market Return And Strategy Returns')
sns.heatmap(data.loc[:,['Return','Return 1','Return 2']].corr(),annot=True)

Figura 10: Matriz de correlación generada por nuestras estrategias de trading y el mercado EURUSD.


Introduciendo mejoras

Con los conocimientos que tenemos ahora, podemos intentar de nuevo mejorar nuestra aplicación. En primer lugar, volveremos a la versión anterior de nuestra estrategia de trading, que incluía las tres estrategias que queremos utilizar.  

Figura 11: Selección de los días de prueba retrospectiva para nuestro procedimiento de optimización.

Como siempre, utilizaremos ajustes de retardo aleatorios para nuestra prueba retrospectiva.

Figura 12: Asegúrese de usar "Retraso aleatorio" si desea seguir el procedimiento.

Sin embargo, a diferencia de las pruebas anteriores, ahora fijaremos todos los pesos de los votos en 1 para garantizar que el optimizador genético solo pueda realizar cambios que aumenten la rentabilidad general de la estrategia. Además, queremos obligar al optimizador a utilizar las tres estrategias y no a seleccionar sesgadamente estrategias correlacionadas, ya que eso va en contra del resultado que buscamos.

Figura 13: Recuerda que esta vez queremos fijar todos nuestros pesos de voto en 1, porque sospechamos que el optimizador genético nos está ganando la partida.

Tras revisar los resultados de la optimización, ya podemos observar mejoras con respecto a los vectores iniciales que utilizamos. Inicialmente, observábamos niveles de rentabilidad de entre 40 y 50 dólares al utilizar las tres estrategias. Sin embargo, ahora estamos observando mejoras claras en la rentabilidad.

Figura 14: Nuestros resultados de optimización han mejorado considerablemente con respecto a la iteración anterior.

Además, al considerar los resultados de las pruebas prospectivas, observamos nuevamente un aumento en los niveles de ganancias. Nuestras pruebas son ahora rentables tanto en la prueba retrospectiva como en la prueba prospectiva (forward test), lo que constituye un claro indicio de la estabilidad de la configuración. Lo más importante es que nuestro optimizador genético ahora puede proporcionarnos conjuntos de configuraciones de estrategia rentables en ambas pruebas. Estos eran los resultados que no lográbamos obtener anteriormente cuando permitíamos que el optimizador modificara los pesos a su antojo. Esto refuerza nuestra convicción de que nuestra aplicación es estable en esta configuración.

Figura 15: Nuestros resultados a futuro muestran ahora fuertes indicios de estabilidad y rentabilidad.

Por último, cuando realizamos una prueba retrospectiva utilizando la configuración más rentable que encontramos a partir de los resultados prospectivos (forward test), podemos ver claramente que nuestra nueva configuración de parámetros es rentable tanto en la prueba retrospectiva como en la prueba prospectiva (forward test), con una tendencia al alza, que es lo que queremos ver.

Figura 16: La curva de capital generada por nuestra nueva estrategia de trading con votos fijos es considerablemente más rentable.


Conclusión

De esta discusión hemos aprendido varias lecciones valiosas. En primer lugar, hemos visto que el reto que supone el «reward hacking» es generalizado y puede darse tanto si somos conscientes de ello como si no. Nuestras herramientas de IA, como los optimizadores genéticos, son rápidas e inteligentes, a veces incluso más inteligentes que nosotros, y debemos permanecer siempre vigilantes para asegurarnos de que estas herramientas no nos superen en inteligencia generando soluciones inválidas que simplemente satisfagan las condiciones de éxito que hemos definido.

Tras analizar detenidamente los resultados de las estrategias seleccionadas por el optimizador, hemos aprendido que debemos evitar seleccionar estrategias correlacionadas, ya que esto contradice el propósito de un análisis multiestrategia.

El lector debe tener en cuenta que los optimizadores genéticos solo están limitados por la cantidad de tiempo que estemos dispuestos a dedicar. De lo contrario, nuestra discusión no necesariamente proporciona evidencia de que el optimizador genético de MetaTrader 5 siempre manipulará las recompensas que se le otorgan. Dado que seleccionamos solo un pequeño número de estrategias y realizamos esta optimización una sola vez, siempre existe la posibilidad de que repetir el proceso arroje resultados diferentes.

Más bien, esto demuestra que yo, el autor, quizás no fui lo suficientemente cuidadoso al plantear el problema para el optimizador genético. Un enfoque mejor habría sido obligar primero al optimizador a utilizar todas las estrategias disponibles y, a continuación, ajustar las ponderaciones de votación de forma más reflexiva.

Lo más importante es que concluimos este ejercicio sabiendo que en nuestro próximo artículo debemos considerar cuidadosamente cómo superar la rentabilidad lograda utilizando ponderaciones uniformes de 1. El rendimiento que obtuvimos con estas ponderaciones uniformes sirve como un sólido punto de referencia, uno que pretendemos superar en nuestra próxima discusión utilizando los modelos estadísticos que originalmente teníamos previsto aplicar en este artículo.

Nombre del archivoDescripción del archivo
Fetch Data MSA.mq5El script MQL5 que utilizamos para obtener datos sobre las 2 estrategias que seleccionó nuestro optimizador genético.
MSA Test 2.1.mq5El Asesor Experto que desarrollamos utiliza las dos estrategias seleccionadas y nuestro modelo ONNX de forma conjunta.
Analyzing Multiple Strategies I.ipynbEl cuaderno Jupyter que escribimos juntos para analizar los datos de mercado que obtuvimos utilizando nuestro script MQL5.
EURUSD NN MSA.onnxNuestra red neuronal profunda, que construimos utilizando la búsqueda aleatoria (RandomizedSearchCV) de la biblioteca scikit-learn (sklearn).

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

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.
Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 32): Motor de reconocimiento de velas japonesas en Python (II) — Detección con TA-Lib Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 32): Motor de reconocimiento de velas japonesas en Python (II) — Detección con TA-Lib
En este artículo, hemos pasado de codificar manualmente la detección de patrones de velas japonesas en Python a aprovechar TA-Lib, una biblioteca que reconoce más de sesenta patrones distintos. Estas formaciones ofrecen información valiosa sobre posibles cambios de tendencia en el mercado y continuaciones de tendencias. Sigue leyendo para obtener más información.
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.
Herramientas de trading de MQL5 (Parte 5): Creación de una cinta de cotizaciones deslizante para el seguimiento en tiempo real de símbolos Herramientas de trading de MQL5 (Parte 5): Creación de una cinta de cotizaciones deslizante para el seguimiento en tiempo real de símbolos
En este artículo, desarrollamos una cinta de cotizaciones deslizante en MQL5 para monitorizar varios símbolos en tiempo real, mostrando los precio Bid (oferta), los spreads y las variaciones porcentuales diarias mediante efectos de desplazamiento. Implementamos fuentes, colores y velocidades de desplazamiento personalizables para resaltar eficazmente los movimientos y las tendencias de los precios.