English Русский Deutsch 日本語 Português
preview
Desarrollamos un asesor experto multidivisa (Parte 8): Realizamos pruebas de carga y procesamos la nueva barra

Desarrollamos un asesor experto multidivisa (Parte 8): Realizamos pruebas de carga y procesamos la nueva barra

MetaTrader 5Probador | 4 octubre 2024, 14:17
280 0
Yuriy Bykov
Yuriy Bykov

Introducción

En el primer artículo, desarrollamos un EA que tenía dos instancias de estrategias comerciales. En el segundo artículo, ya utilizamos nueve instancias, y en el último, 32. Dicho esto, no hemos tenido problemas con tiempos de prueba demasiado largos. Evidentemente, cuanto más corto sea el tiempo de una sola pasada en el simulador, mejor. Pero si el proceso global de optimización ocupa unas horas, resultará aceptable si lo comparamos con las situaciones en las que el proceso llevaría días o semanas. Del mismo modo, si combinamos varias instancias de estrategias en un asesor experto y queremos ver sus resultados, una sola pasada debería completarse en segundos o minutos, no en horas o días.

Si realizamos la optimización para seleccionar grupos de instancias de estrategias, entonces ya tendremos múltiples instancias implicadas en todas las pasadas de optimización. Entonces aumentará el tiempo dedicado a las pasadas individuales y a la optimización en su conjunto. Por ello, en esta optimización nos hemos limitado a seleccionar grupos de ocho instancias como máximo.

Hoy intentaremos averiguar cómo el tiempo de una sola pasada en el simulador para diferentes periodos de prueba dependerá del número de instancias de estrategias comerciales. También nos fijaremos en el consumo de memoria. Y, por supuesto, necesitaremos ver cómo se comportan los asesores expertos con diferente número de instancias de estrategias comerciales cuando se ejecutan en el gráfico del terminal.


Diferente número de instancias en el simulador

Para llevar a cabo un experimento de este tipo, tendremos que escribir un nuevo asesor experto basado en uno de los ya existentes. Tomaremos como base el asesor experto OptGroupExpert.mq5 y le haremos los siguientes cambios:

  • Eliminaremos los parámetros de entrada que especifican los índices de los ocho conjuntos de parámetros que tomamos del array completo de conjuntos cargados desde el archivo. Dejaremos el parámetro count_, que ahora establecerá el número de conjuntos a cargar desde el array completo de conjuntos.
  • Asimismo, eliminaremos la comprobación de la unicidad de los índices, que ahora ya no existen. Y añadiremos nuevas estrategias al array de estrategias, con conjuntos de parámetros tomados de los primeros elementos count_ del array de conjuntos de parámetros params. Si no hay suficientes instancias en este array, tomaremos de la siguiente ronda desde el principio del array.
  • Además eliminaremos las funciones OnTesterInit() y OntesterDeinit(), ya que todavía no vamos a utilizar este asesor experto para optimizar nada.

Obtendremos un código como este:

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "::: Money management"
sinput double expectedDrawdown_ = 10;  // - Maximum risk (%)
sinput double fixedBalance_ = 10000;   // - Used deposit (0 - use all) in the account currency
sinput double scale_ = 1.00;           // - Group scaling multiplier

input group "::: Selection for the group"
sinput string fileName_ = "Params_SV_EURGBP_H1.csv";  // - File with strategy parameters (*.csv)
input int     count_ = 8;              // - Number of strategies in the group (1 .. 8)

input group "::: Other parameters"
sinput ulong  magic_        = 27183;   // - Magic

...

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
// Load strategy parameter sets
   int totalParams = LoadParams(fileName_, params);

// If nothing is loaded, report an error
   if(totalParams == 0) {
      PrintFormat(__FUNCTION__" | ERROR: Can't load data from file %s.\n"
                  "Check that it exists in data folder or in common data folder.",
                  fileName_);
      return(INIT_PARAMETERS_INCORRECT);
   }

// Report an error if
   if(count_ < 1) { // number of instances is less than 1
      return INIT_PARAMETERS_INCORRECT;
   }
   
   ArrayResize(params, count_);

// Set parameters in the money management class
   CMoney::DepoPart(expectedDrawdown_ / 10.0);
   CMoney::FixedBalance(fixedBalance_);

// Create an EA handling virtual positions
   expert = new CVirtualAdvisor(magic_, "SimpleVolumes_BenchmarkInstances");

// Create and fill the array of all strategy instances
   CVirtualStrategy *strategies[];

   FOREACH(params, APPEND(strategies, new CSimpleVolumesStrategy(params[i % totalParams])));

// Form and add a group of strategies to the EA
   expert.Add(CVirtualStrategyGroup(strategies, scale_));

   return(INIT_SUCCEEDED);
}

Luego guardaremos el código obtenido en el archivo BenchmarkInstancesExpert.mq5 en la carpeta actual.

Ahora vamos a probar varias ejecuciones de este EA en el simulador con diferentes instancias de estrategias comerciales y con diferentes modos de modelado de ticks.


Resultados de las pruebas en distintos modos

Empezaremos con el conocido modo de modelado de ticks "OHLC en M1", que hemos utilizado en todos los artículos anteriores. En el próximo inicio, duplicaremos el número de instancias. Comenzaremos con ocho copias. Si la duración de la prueba es demasiado larga, reduciremos el periodo de prueba. 


Fig. 1. Resultados de ejecuciones individuales en el modo "OHLC en M1"

Como podemos observar, al realizar las pruebas con hasta 512 instancias, utilizamos un periodo de prueba de 6 años, luego pasamos a un periodo de 1 año y utilizamos solo 3 meses para las dos últimas pasadas. 

Para poder comparar los costes de tiempo de distintos periodos de prueba, calcularemos un valor independiente: el tiempo de modelado de una instancia TC durante un día. Para ello, dividiremos el tiempo total por el número de instancias TC y por la duración del periodo de prueba en días. Para no complicarnos con números pequeños, convertiremos este tiempo en nanosegundos multiplicándolo por 10^9.

En los registros, el simulador ofrece información sobre la memoria utilizada durante la ejecución, indicando la cantidad total y los valores gastados en datos históricos y de ticks. Restándolos de la cantidad total de memoria, obtendremos la cantidad de memoria requerida por el propio EA (lo más probable es que sea así).

A partir de los resultados, podemos afirmar que incluso el número máximo de instancias (16384) no tarda un tiempo catastróficamente largo en pasar la prueba. En principio, este número de instancias resulta suficiente para organizar el funcionamiento conjunto de, por ejemplo, quince símbolos con cien instancias cada uno. Lo cual no es poco. Al mismo tiempo, el consumo de memoria no aumenta mucho con el número de instancias. Por alguna razón, hay un pico en el consumo de memoria para el propio EA en 8192 instancias, pero luego la memoria se ha necesitado menos de nuevo.

Para obtener resultados más precisos, podemos repetir varias pasadas para cada número de instancias y calcular los tiempos medios y los tamaños medios de memoria, ya que los resultados siguen siendo diferentes en pasadas distintas con el mismo número de instancias. Pero estas diferencias no han sido muy grandes, por lo que no tiene especial sentido realizar pruebas más exhaustivas. Al fin y al cabo, solo queríamos asegurarnos de que no nos encontraremos con limitaciones en un número ya relativamente pequeño de instancias.

Ahora vamos a intentar ver los resultados de las ejecuciones del asesor experto en el simulador en el modo de modelado "Todos los ticks".

Fig. 2. Resultados de ejecuciones individuales en modo "Todos los ticks"

El tiempo por pasada ha aumentado en un factor de aproximadamente 10, por lo que hemos reducido el valor del periodo de prueba en el mismo número de instancias en comparación con el modo anterior. El tamaño de la memoria para los ticks ha aumentado de forma coherente, lo cual ha provocado un incremento de la cantidad total de memoria asignada. No obstante, el tamaño de la memoria asignada al asesor experto ha sido casi el mismo para todos los números de instancias utilizados. Existe un ligero crecimiento, pero es bastante lento.

Hemos observado un tiempo de ejecución anormalmente bajo para las instancias de 512 y 1024, casi el doble de rápido que los demás números de instancias. La razón más probable es que esté relacionada con el orden de los conjuntos de parámetros de instancias de estrategias comerciales en el archivo de datos CSV.

El último modo de modelado que analizaremos es "Todos los ticks basados en ticks reales". Para ello, hemos realizado un número de inicios ligeramente superior al del modo "Todos los ticks".


Fig. 3. Resultados de ejecuciones individuales en modo "Todos los ticks basados en ticks reales".

En comparación con el modo anterior, el tiempo aumentado en un 30% adicional y la memoria utilizada en un 20%.

Debemos señalar que, de manera simultánea a las pruebas, en el terminal estaba funcionando una instancia de este asesor experto adjunto al gráfico. En él se han utilizado 8 192 instancias. Al mismo tiempo, el consumo de memoria del terminal ha sido de unos 200 MB, mientras que el consumo de recursos de la CPU se ha situado entre el 0% y el 4%.

En general, el experimento ha demostrado que tenemos una gran cantidad de posibles casos de estrategias comerciales que funcionarán juntas en un asesor experto. Obviamente, gran parte de esta cifra dependerá del relleno específico de las estrategias comerciales. Cuantos más cálculos tenga que realizar una sola instancia, menos combinaciones podremos permitirnos.

Veamos ahora qué pasos sencillos podemos dar para acelerar el proceso de pruebas.


Desconexión de la salida

En la implementación actual, enviamos bastante información al registro mientras se ejecuta el asesor experto. Cuando se trata de optimizar instancias individuales, esto no crea ningún problema, pues las funciones de inferencia simplemente no se ejecutan. Si ejecutamos una sola pasada del asesor experto en el simulador, todos los mensajes se enviarán al registro. La biblioteca VirtualOrder.mqh utilizada, por ejemplo, emitirá un mensaje sobre el procesamiento de eventos de cada orden virtual. Cuando el número de órdenes virtuales es pequeño, tendrá poco efecto en el tiempo de prueba, pero cuando su número empieza a ascender a decenas de miles, puede tener un efecto notable.

Vamos a intentar medirlo. Podemos desactivar la salida de todos nuestros mensajes al registro añadiendo esta línea al principio del archivo del EA:

#define PrintFormat StringFormat

Debido a la relación de estas funciones, podemos sustituir todas las llamadas a PrintFormat() por llamadas a StringFormat(), que formarán una cadena pero no la enviarán al registro.

Tras realizar algunas pasadas en algunas de ellas, hemos notado una disminución del tiempo de entre el 5 y el 10%, mientras que en otras el tiempo puede haber aumentado ligeramente. Bueno, lo hemos intentado. Por cierto, es posible que en el futuro necesitemos esta forma de sustituir PrintFormat().

 

Migración a OHLC en M1

Otra forma de acelerar el proceso tanto de pasadas individuales del simulador como de la optimización es evitar usar los modos de simulación "Todos los ticks" y "Todos los ticks basados en ticks reales".

Es comprensible que no todas las estrategias comerciales puedan permitirse algo así. Si la estrategia implica una apertura/cierre de posiciones muy frecuente (más de una vez por minuto), será imposible rechazar la comprobación en todos los ticks. Ni siquiera el trading de alta frecuencia dura todo el tiempo: se da solo en franjas horarias específicas. Pero si una estrategia no necesita aperturas/cierres frecuentes y no resulta tan sensible a perder unos pocos pips debido a una activación insuficientemente precisa de los niveles Stop Loss y Take Profit, ¿por qué no aprovechar esta oportunidad?

La estrategia comercial considerada como ejemplo pertenece a las que permiten prescindir del modo "todos los ticks". No obstante, aquí surge otro problema. Si simplemente optimizamos los parámetros de instancias individuales en el modo "OHLC en M1", y luego ponemos el EA ensamblado a funcionar en el terminal, entonces el EA tendrá que trabajar allí en el modo de todos los ticks. No recibirá un número fijo de 4 ticks por minuto, sino mucho más. Por tanto, la función OnTick() se llamará más a menudo, y el conjunto de precios que el asesor experto procesará será un poco más diverso.

Esta diferencia puede cambiar la imagen de los resultados mostrados por el asesor. Para comprobar hasta qué punto este escenario es real, compararemos los resultados obtenidos al probar el asesor experto con los mismos parámetros de entrada en los modos "OHLC en M1" y "Todos los ticks basados en ticks reales".

Fig. 4. Comparación de los resultados de ejecuciones individuales en el modo
"Todos los ticks basados en ticks reales" (izquierda) y "OHLC en M1" (derecha)


Podemos observar que la hora de apertura, la hora de cierre y el precio son ligeramente diferentes para los distintos modos. Al principio la diferencia es solo eso, pero luego llega un momento en que a la izquierda vemos que se abre una transacción, mientras que a la derecha al mismo momento no hay apertura: fíjese en las filas con la transacción nº 25. Así, los resultados del modo "OHLC en M1" contienen menos transacciones que los del modo "Todos los ticks basados en ticks reales". 

En el modo de todos los ticks, el beneficio ha sido ligeramente superior. Si observamos el patrón de crecimiento de la curva de balance, no hay diferencias significativas entre ambos:


Fig. 5. Resultados de la prueba en el modo "OHLC en M1" (arriba) y en el modo "Todos los ticks basados en ticks reales" (abajo)

Por consiguiente, podemos decir que al ejecutar este asesor experto en el terminal, es probable que obtengamos resultados no peores que en la prueba en el modo "OHLC en M1". Por lo tanto, podemos utilizar un modo de simulación de ticks más rápido para la optimización. Si podemos realizar algunos cálculos para la estrategia solo al comienzo de una nueva barra, entonces podremos acelerar aún más el funcionamiento del asesor experto rechazando dichos cálculos en cada tick. Para ello, necesitaremos una forma de definir una nueva barra en el EA.

Si en el modo de todos los ticks los resultados son peores que en el modo "OHLC en M1", podemos intentar prohibir al EA realizar operaciones en un momento distinto al inicio de la barra. En este caso, deberíamos obtener resultados muy semejantes en todos los modos de modelado de ticks. Para ello, necesitaremos de nuevo una forma de definir una nueva barra en un EA. 


Definiendo una nueva barra

Vamos a formular primero nuestras preferencias. Nos gustaría tener una función que retorne true si ha aparecido una nueva barra en un símbolo y un marco temporal determinados. Al desarrollar un asesor experto que implementa una única instancia de una estrategia comercial, esta función suele escribirse para un único símbolo y marco temporal, utilizando variables para almacenar la hora de la última barra. O para un símbolo y varios marcos temporales. A menudo podremos ver que el código que implementa esta funcionalidad no se separa en una función independiente, sino que se implementa en el único lugar donde se necesita.

Este enfoque se vuelve muy incómodo cuando debemos realizar múltiples comprobaciones para el inicio de una nueva barra para diferentes instancias de estrategias comerciales. Obviamente, podemos incorporar este código directamente en la implementación de la instancia de la estrategia comercial, pero lo haremos de otra manera.

Crearemos una función pública IsNewBar(symbol, timeframe), que debería ser capaz de informar sobre la aparición de una nueva barra en el tick actual según el símbolo symbol y el marco temporal timeframe. Resulta deseable no añadir variables y acciones adicionales al código de la lógica comercial de las estrategias, salvo para llamar a esta función. Además, si se ha producido una nueva barra en el tick actual, y la función se llama varias veces (por ejemplo, desde diferentes instancias de estrategias comerciales), debería retornar true en cada llamada, no solo en la primera.

A continuación, tendremos que almacenar la información sobre las horas a las que aparece la última barra para cada símbolo y marco temporal. No obstante, con "cada" no nos estaremos refiriendo a todos los disponibles en el terminal, sino solo a los que realmente se requieren para el funcionamiento de instancias específicas de estrategias comerciales. Para definir el rango de estos símbolos y marcos temporales necesarios, ampliaremos la lista de acciones que realiza la función IsNewBar(symbol, timeframe). En primer lugar, comprobaremos si existe algún tipo de hora memorizada para la barra actual en el símbolo y el marco temporal dados. Si no existe, la función realizará las acciones necesarias para crear dicho almacén de tiempo. Si existe, retornará el resultado obtenido tras la comprobación de la aparición de una barra nueva.

Para que nuestra función IsNewBar() sea llamada múltiples veces en un solo tick, tendremos que dividirla en dos funciones separadas. Una comprobará la aparición de nuevas barras al principio de un tick para todos los símbolos y marcos temporales de interés y guardará esta información para la segunda función, que simplemente encontrará el resultado deseado del evento de aparición de una nueva barra y lo retornará. Llamaremos a la primera función UpdateNewBar() y haremos que retorne un valor booleano indicando que al menos ha aparecido una nueva barra de un símbolo y marco temporal. 

La función UpdateNewBar() tendrá que llamarse una vez al comienzo del procesamiento de un nuevo tick. Por ejemplo, podemos poner su llamada al principio del método CVirtualAdvisor::Tick():

void CVirtualAdvisor::Tick(void) {
// Define a new bar for all required symbols and timeframes
   UpdateNewBar();

   ...
// Start handling in strategies where IsNewBar(...) can already be used
   CAdvisor::Tick();

   ...
}

Para organizar el almacenamiento de las horas de las últimas barras, crearemos primero una clase estática CNewBarEvent. Esto significa que no crearemos objetos de esta clase, sino que solo usaremos sus propiedades y métodos estáticos. Básicamente, esto equivaldrá a crear las variables y funciones globales necesarias en un espacio de nombres dedicado.

En esta clase tendremos dos arrays: un array de nombres de símbolos (m_symbols) y un array de punteros a objetos de la nueva clase (m_symbolNewBarEvent). El primero contendrá los símbolos con los que monitorearemos los eventos de la nueva barra. Los segundos serán los punteros a los objetos de la nueva clase CSymbolNewBarEvent, que almacenarán las horas de las barras para el mismo símbolo, pero para diferentes marcos temporales.

Habrá tres métodos en estas dos clases:

  • Método de registro de un nuevo símbolo o marco temporal monitoreado para un símbolo Register(...)
  • Método de actualización de los nuevos signos de barras Update()
  • Método para obtener el signo de una nueva barra IsNewBar(...)

Si debemos registrar el monitoreo de un evento de nueva barra en un nuevo símbolo, se creará un nuevo objeto de la clase CSymbolNewBarEvent. Por lo tanto, deberemos tener cuidado de borrar la memoria ocupada por estos objetos cuando se finalice el asesor experto. Para ello, hemos añadido el método estático CNewBarEvent::Destroy() y la función global DestroyNewBar(). Asimismo, añadiremos al destructor del asesor experto la llamada a esta función:

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CVirtualAdvisor::~CVirtualAdvisor() {
   delete m_receiver;         // Remove the recipient
   delete m_interface;        // Remove the interface
   DestroyNewBar();           // Remove the new bar tracking objects 
}

La implementación completa de estas clases podría tener el aspecto siguiente:

//+------------------------------------------------------------------+
//| Class for defining a new bar for a specific symbol               |
//+------------------------------------------------------------------+
class CSymbolNewBarEvent {
private:
   string            m_symbol;         // Tracked symbol
   long              m_timeFrames[];   // Array of tracked symbol timeframes
   long              m_timeLast[];     // Array of times of the last bars for timeframes
   bool              m_res[];          // Array of flags of a new bar occurrence for timeframes

   // The method for registering a new tracked timeframe for a symbol
   int               Register(ENUM_TIMEFRAMES p_timeframe) {
      APPEND(m_timeFrames, p_timeframe);  // Add it to the timeframe array
      APPEND(m_timeLast, 0);              // The last time bar for it is still unknown
      APPEND(m_res, false);               // No new bar for it yet
      Update();                           // Update new bar flags
      return ArraySize(m_timeFrames) - 1;
   }

public:
   // Constructor
                     CSymbolNewBarEvent(string p_symbol) :
                     m_symbol(p_symbol) // Set a symbol
   {}

   // Method for updating new bar flags
   bool              Update() {
      bool res = (ArraySize(m_res) == 0);
      FOREACH(m_timeFrames, {
         // Get the current bar time
         long time = iTime(m_symbol, (ENUM_TIMEFRAMES) m_timeFrames[i], 0);
         // If it does not match the saved one, it is a new bar 
         m_res[i] = (time != m_timeLast[i]);
         res |= m_res[i];
         // Save the new time
         m_timeLast[i] = time;
      });
      return res;
   }

   // Method for getting the new bar flag
   bool              IsNewBar(ENUM_TIMEFRAMES p_timeframe) {
      int index;
      // Search for the required timeframe index
      FIND(m_timeFrames, p_timeframe, index);

      // If not found, then register a new timeframe
      if(index == -1) {
         PrintFormat(__FUNCTION__" | Register new event handler for %s %s", m_symbol, EnumToString(p_timeframe));
         index = Register(p_timeframe);
      }

      // Return the new bar flag for the necessary timeframe
      return m_res[index];
   }
};


//+------------------------------------------------------------------+
//| Static class for defining a new bar for all                      |
//| symbols and timeframes                                           |
//+------------------------------------------------------------------+
class CNewBarEvent {
private:
   // Array of objects to define a new bar for one symbol
   static   CSymbolNewBarEvent     *m_symbolNewBarEvent[];

   // Array of required symbols
   static   string                  m_symbols[];

   // Method to register new symbol and timeframe to track a new bar
   static   int                     Register(string p_symbol)  {
      APPEND(m_symbols, p_symbol);
      APPEND(m_symbolNewBarEvent, new CSymbolNewBarEvent(p_symbol));
      return ArraySize(m_symbols) - 1;
   }

public:
   // There is no need to create objects of this class - delete the constructor
                            CNewBarEvent() = delete; 

   // Method for updating new bar flags
   static bool              Update() {
      bool res = (ArraySize(m_symbolNewBarEvent) == 0);
      FOREACH(m_symbols, res |= m_symbolNewBarEvent[i].Update());
      return res;
   }

   // Method to free memory for automatically created objects
   static void              Destroy() {
      FOREACH(m_symbols, delete m_symbolNewBarEvent[i]);
      ArrayResize(m_symbols, 0);
      ArrayResize(m_symbolNewBarEvent, 0);
   }

   // Method for getting the new bar flag
   static bool              IsNewBar(string p_symbol, ENUM_TIMEFRAMES p_timeframe) {
      int index;
      // Search for the required symbol index
      FIND(m_symbols, p_symbol, index);
      
      // If not found, then register a new symbol
      if(index == -1) index = Register(p_symbol);
      
      // Return the new bar flag for the necessary symbol and timeframe
      return m_symbolNewBarEvent[index].IsNewBar(p_timeframe);
   }
};

// Initialize static members of the CSymbolNewBarEvent class members;
CSymbolNewBarEvent* CNewBarEvent::m_symbolNewBarEvent[];
string CNewBarEvent::m_symbols[];


//+------------------------------------------------------------------+
//| Function for checking a new bar occurrence                       |
//+------------------------------------------------------------------+
bool IsNewBar(string p_symbol, ENUM_TIMEFRAMES p_timeframe) {
   return CNewBarEvent::IsNewBar(p_symbol, p_timeframe);
}

//+------------------------------------------------------------------+
//| Function for updating information about new bars                 |
//+------------------------------------------------------------------+
bool UpdateNewBar() {
   return CNewBarEvent::Update();
}

//+------------------------------------------------------------------+
//| Function for removing new bar tracking objects                   |
//+------------------------------------------------------------------+
void DestroyNewBar() {
   CNewBarEvent::Destroy();
}
//+------------------------------------------------------------------+

Guardaremos este código en el archivo NewBarEvent.mqh en la carpeta actual.

Ahora vamos a ver cómo podemos aplicar esta biblioteca en una estrategia comercial y un asesor experto. Pero primero, realizaremos en la estrategia comercial pequeñas correcciones no relacionadas con el procesamiento de una nueva barra.


Correcciones de la estrategia comercial

Desafortunadamente, durante la redacción de este artículo, hemos descubierto dos errores en la estrategia utilizada. Estos no han tenido un impacto significativo en los resultados anteriores, pero ya que los hemos encontrado, vamos a corregirlos.

El primer error se relaciona con el parámetro openDistance_. Cuando este se fijaba en un valor negativo en los parámetros, su valor en la estrategia se restablecía a un pequeño número positivo igual al diferencial del símbolo actual. Es decir, en lugar de abrir órdenes pendientes BUY STOP y SELL_STOP, se abrían posiciones de mercado. y esto hacía que, al optimizarlas, no viéramos los resultados que se podrían haber obtenido negociando esas órdenes pendientes. Es decir, pasábamos de largo algunos conjuntos de parámetros potencialmente rentables.

El error se producía en esta línea de código en el archivo SimpleVolumesStrategy.mqh en las funciones de apertura de órdenes pendientes:

// Let's make sure that the opening distance is not less than the spread
   int distance = MathMax(m_openDistance, spread);

Si el valor de m_openDistance era negativo, se garantizaba que el valor del desplazamiento del precio de apertura desde el precio de distance actual pasara a ser positivo. Para mantener el mismo signo en distance y en m_openDistance, bastará con multiplicar esta expresión por él:

// Let's make sure that the opening distance is not less than the spread
   int distance = MathMax(MathAbs(m_openDistance), spread) * (m_openDistance < 0 ? -1 : 1);

El segundo error consistía en que, al calcular el volumen medio de las últimas barras, también se incluía en el cálculo el volumen de la barra actual, aunque según la descripción de la estrategia no deberíamos usarla para calcular la media. No obstante, el impacto de este error es probablemente también bastante pequeño. Cuanto más largo sea el periodo de promediación del volumen, menos contribuirá la última barra a la media. 

Para solucionarlo, bastará con modificar ligeramente la función de cálculo de la media excluyendo el primer elemento del array transmitido:

//+------------------------------------------------------------------+
//| Average value of the array of numbers from the second element    |
//+------------------------------------------------------------------+
double CSimpleVolumesStrategy::ArrayAverage(const double &array[]) {
   double s = 0;
   int total = ArraySize(array) - 1;
   for(int i = 1; i <= total; i++) {
      s += array[i];
   }

   return s / MathMax(1, total);
}

Guardaremos estos cambios en el archivo SimpleVolumesStrategy.mqh en la carpeta actual.


Contabilización de una nuevo barra en la estrategia

Para que una estrategia comercial realice algunas acciones solo cuando aparece una nueva barra, solo tendremos que colocar este bloque de código en un operador condicional de este tipo:

// If a new bar arrived on H1 for the current strategy symbol, then
if(IsNewBar(m_symbol, PERIOD_H1)) {

       // perform the necessary actions
   ...
}

La presencia de este código en la estrategia provocará el registro automático del monitoreo del evento de una nueva barra en el marco temporal H1 y el símbolo de la estrategia m_symbol.

También podemos añadir con seguridad una comprobación de la aparición de nuevas barras en otros marcos temporales adicionales. Por ejemplo, si una estrategia usa valores de algún rango medio de precios (ATR o ADR), su recálculo podría organizarse fácilmente de este modo solo una vez al día:

// If a new bar arrived on D1 for the current strategy symbol, then
if(IsNewBar(m_symbol, PERIOD_H1)) {
   CalcATR(); // call our ATR calculation function
}

En la estrategia comercial que estamos analizando en esta serie de artículos, podemos excluir todas las acciones fuera del momento de una nueva barra:

//+------------------------------------------------------------------+
//| "Tick" event handler function                                    |
//+------------------------------------------------------------------+
void CSimpleVolumesStrategy::Tick() override {
// If there is no new bar on M1, 
   if(!IsNewBar(m_symbol, PERIOD_M1)) return;

// If their number is less than allowed
   if(m_ordersTotal < m_maxCountOfOrders) {
      // Get an open signal
      int signal = SignalForOpen();

      if(signal == 1 /* || m_ordersTotal < 1 */) {          // If there is a buy signal, then
         OpenBuyOrder();         // open the BUY_STOP order
      } else if(signal == -1) {  // If there is a sell signal, then
         OpenSellOrder();        // open the SELL_STOP order
      }
   }
}

También podemos prohibir cualquier procesamiento en el manejador de eventos OnTick del asesor experto para aquellos ticks que no coincidan con el inicio de una nueva barra según cualquiera de los símbolos o marcos temporales utilizados. Para conseguirlo podemos realizar los cambios mencionados en el código del método CVirtualAdvisor::Tick():

//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CVirtualAdvisor::Tick(void) {
// Define a new bar for all required symbols and timeframes
   bool isNewBar = UpdateNewBar();

// If there is no new bar anywhere, and we only work on new bars, then exit
   if(!isNewBar && m_useOnlyNewBar) {
      return;
   }

// Receiver handles virtual positions
   m_receiver.Tick();

// Start handling in strategies
   CAdvisor::Tick();

// Adjusting market volumes
   m_receiver.Correct();

// Save status
   Save();

// Render the interface
   m_interface.Redraw();
}

En este código, hemos añadido una nueva propiedad del EA m_useOnlyNewBar, que se puede establecer al crear un objeto de asesor experto:

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   ...
   bool              m_useOnlyNewBar;  // Handle only new bar ticks

public:
                     CVirtualAdvisor(ulong p_magic = 1, string p_name = "",
                                     bool p_useOnlyNewBar = false); // Constructor
    ...
};


//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(ulong p_magic = 1,
                                 string p_name = "",
                                 bool p_useOnlyNewBar = false) :
// Initialize the receiver with a static receiver
   m_receiver(CVirtualReceiver::Instance(p_magic)),
// Initialize the interface with the static interface
   m_interface(CVirtualInterface::Instance(p_magic)),
   m_lastSaveTime(0),
   m_useOnlyNewBar(p_useOnlyNewBar) {
   m_name = StringFormat("%s-%d%s.csv",
                         (p_name != "" ? p_name : "Expert"),
                         p_magic,
                         (MQLInfoInteger(MQL_TESTER) ? ".test" : "")
                        );
};

Básicamente, para añadir la función, podríamos crear una nueva clase de EA heredándola de CVirtualAdvisor y añadiéndole una nueva propiedad y un nuevo comprobador de barras. Pero también podemos dejarlo todo como está, porque si el valor por defecto de la propiedad m_useOnlyNewBar es false, todo funcionará como lo hace sin añadir esta funcionalidad a la clase de EA.

Si hemos ampliado la clase de asesor experto de esta manera, podremos ahorrarnos la comprobación del evento de una nueva barra de minutos dentro del método Tick() dentro de la clase de estrategia comercial. Bastará con llamar a la función IsNewBar() una vez en el constructor de la estrategia con el símbolo actual y el marco temporal M1, para que se monitoree el evento de una nueva barra con dicho símbolo y marco temporal. Entonces el asesor experto con el valor de propiedad establecido en m_useOnlyNewBar = true simplemente no iniciará el procesamiento de ticks para las instancias de las estrategias, si no hay una nueva barra en M1:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CSimpleVolumesStrategy::CSimpleVolumesStrategy(
   ...) :
// Initialization list
   ... {
   CVirtualReceiver::Get(GetPointer(this), m_orders, m_maxCountOfOrders);

// Load the indicator to get tick volumes
   m_iVolumesHandle = iVolumes(m_symbol, m_timeframe, VOLUME_TICK);

// Set the size of the tick volume receiving array and the required addressing
   ArrayResize(m_volumes, m_signalPeriod);
   ArraySetAsSeries(m_volumes, true);

// Register the event handler for a new bar on the minimum timeframe
   IsNewBar(m_symbol, PERIOD_M1);
}


//+------------------------------------------------------------------+
//| "Tick" event handler function                                    |
//+------------------------------------------------------------------+
void CSimpleVolumesStrategy::Tick() override {
// If their number is less than allowed
   if(m_ordersTotal < m_maxCountOfOrders) {
      // Get an open signal
      int signal = SignalForOpen();

      if(signal == 1 /* || m_ordersTotal < 1 */) {          // If there is a buy signal, then
         OpenBuyOrder();         // open the BUY_STOP order
      } else if(signal == -1) {  // If there is a sell signal, then
         OpenSellOrder();        // open the SELL_STOP order
      }
   }
}

Guardaremos estos cambios en el archivo SimpleVolumesStrategy.mqh en la carpeta actual. 


Verificación de los resultados

Vamos a añadir al asesor experto BenchmarkInstancesExpert.mq5 un nuevo parámetro de entrada useOnlyNewBars_, donde estableceremos si debe procesar los ticks que no coincidan con el inicio de una nueva barra. Al inicializar el asesor experto, transmitiremos el valor de este parámetro al constructor del EA:

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
...

input group "::: Other parameters"
sinput ulong  magic_          = 27183;   // - Magic
input bool    useOnlyNewBars_ = true;    // - Work only at bar opening

...

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   ...

// Create an EA handling virtual positions
   expert = new CVirtualAdvisor(magic_, "SimpleVolumes_BenchmarkInstances", useOnlyNewBars_);

   ...
}

Vamos a realizar pruebas en un pequeño periodo de este EA con 256 instancias de estrategias comerciales en el modo "Todos los ticks basados en ticks reales", primero con el valor del parámetro useOnlyNewBars_ = false, y después con el valor useOnlyNewBars_ = true.

En el primer caso, es decir, cuando los asesores expertos han procesado cada tick, el beneficio ha sido de 296 $, la pasada se ha completado en el tiempo 04:15. En el segundo caso, cuando el asesor experto ha omitido todos los ticks salvo los que se han producido al comienzo de una nueva barra, el beneficio ha sido de 434 $, la pasada se ha completado en 00:25. Es decir, no solo hemos reducido el coste computacional en un factor de 10, sino que también hemos obtenido un beneficio ligeramente superior en el segundo caso.

Pero no debemos ser demasiado optimistas. Para otras estrategias comerciales, la repetición de tales resultados no está en absoluto garantizada. Por ello, deberemos investigar por separado si cada estrategia comercial nos permite realizar tales trucos.


Conclusión

Veamos de nuevo los resultados obtenidos. Hemos probado el rendimiento del EA con un número suficientemente grande de instancias de estrategias comerciales trabajando simultáneamente. Esto abre la perspectiva de una buena diversificación de la negociación a través de diferentes símbolos, marcos temporales y estrategias comerciales, ya que podremos combinarlos en un solo asesor experto.

También hemos añadido una nueva funcionalidad a nuestra biblioteca de clases: la posibilidad de monitorear los eventos de aparición de nuevas barras. A pesar de que realmente no necesitamos esta característica en la estrategia que nos ocupa, su presencia puede resultar muy útil para la aplicación de otras estrategias comerciales. Además, tener la capacidad de limitar el EA al comienzo de una nueva barra puede ayudarnos a reducir el coste de los recursos informáticos y lograr resultados más similares para las pruebas en diferentes modos de simulación de ticks.

Pero una vez más nos hemos desviado un poco del rumbo previsto para este proyecto. Bueno, eso también puede contribuir al objetivo final. Después de una pequeña digresión y un ligero descanso, intentaremos retomar el camino de la automatización de las pruebas de nuestros asesores expertos con renovado vigor. Parece que ha llegado el momento de retomar el tema de la inicialización de instancias de estrategias comerciales mediante constantes de cadena y la creación de un sistema para almacenar la información sobre los resultados de la optimización.

Gracias por su atención, ¡hasta la próxima!



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

Archivos adjuntos |
NewBarEvent.mqh (11.52 KB)
VirtualAdvisor.mqh (14.64 KB)
VirtualOrder.mqh (39.52 KB)
Algoritmo de optimización Brain Storm - Brain Storm Optimization (Parte II): Multimodalidad Algoritmo de optimización Brain Storm - Brain Storm Optimization (Parte II): Multimodalidad
En la segunda parte del artículo pasaremos a la aplicación práctica del algoritmo BSO, realizaremos tests con funciones de prueba y compararemos la eficacia de BSO con otros métodos de optimización.
Reimaginando las estrategias clásicas: El petróleo Reimaginando las estrategias clásicas: El petróleo
En este artículo, revisamos una estrategia clásica de negociación de crudo con el objetivo de mejorarla aprovechando algoritmos de aprendizaje automático supervisado. Construiremos un modelo de mínimos cuadrados para predecir los futuros precios del crudo Brent basándonos en el diferencial entre los precios del crudo Brent y del crudo WTI. Nuestro objetivo es identificar un indicador adelantado de futuros cambios en los precios del Brent.
Añadimos un LLM personalizado a un robot comercial (Parte 3): Entrenando tu propio LLM utilizando la CPU Añadimos un LLM personalizado a un robot comercial (Parte 3): Entrenando tu propio LLM utilizando la CPU
Con el rápido desarrollo de la inteligencia artificial actual, los modelos de lenguaje (LLM) son una parte importante de la inteligencia artificial, por lo que deberíamos pensar en cómo integrar LLM potentes en nuestro trading algorítmico. Para la mayoría de las personas, es difícil ajustar estos poderosos modelos según sus necesidades, implementarlos localmente y luego aplicarlos al comercio algorítmico. Esta serie de artículos abordará paso a paso cómo lograr este objetivo.
Redes neuronales: así de sencillo (Parte 85): Predicción multidimensional de series temporales Redes neuronales: así de sencillo (Parte 85): Predicción multidimensional de series temporales
En este artículo presentaremos un nuevo método complejo de previsión de series temporales que combina armoniosamente las ventajas de los modelos lineales y los transformadores.