English Русский Deutsch 日本語 Português
preview
Desarrollamos de un asesor multidivisa (Parte 3): Revisión de la arquitectura

Desarrollamos de un asesor multidivisa (Parte 3): Revisión de la arquitectura

MetaTrader 5Trading | 30 julio 2024, 08:33
17 0
Yuriy Bykov
Yuriy Bykov

Introducción

En los artículos anteriores continuamos el desarrollo de un asesor experto multidivisa que funciona simultáneamente con diferentes estrategias comerciales. Podemos observar que la solución presentada en el segundo artículo ya era significativamente diferente de la solución presentada en el primero. Esto demuestra que aún estamos buscando las mejores opciones.

Intentaremos ver el sistema desarrollado como un todo, alejándonos de los pequeños detalles de implementación para entender las formas de mejorarlo. Para ello, mostraremos una breve pero perceptible evolución del sistema.


Primer esquema de trabajo

Ya hemos asignado un objeto de experto (de clase CAdvisor o sus descendientes), que supone un agregador de objetos de estrategia comercial (de clase CStrategy o sus descendientes). Al inicio del funcionamiento del asesor, en el manejador OnInit() ocurre lo siguiente:

  • Se crea un objeto de experto.
  • Los objetos de estrategia comercial se crean y se añaden al asesor experto en su array de estrategias comerciales.

En el manejador de eventos OnTick() del asesor sucede lo siguiente:

  • Se llama al método CAdvisor::Tick() para el objeto de asesor.
  • Este método enumera todas las estrategias y llama a su método CStrategy::Tick().
  • Las estrategias en el marco del funcionamiento de CStrategy::Tick() realizan todas las operaciones necesarias para la apertura y el cierre de posiciones en el mercado.

Esquemáticamente, esto puede representarse del modo que sigue:



Figura 1. Esquema de funcionamiento del primer artículo

Fig. 1. Esquema de funcionamiento del primer artículo

La ventaja de tal esquema era que teniendo un código fuente de un asesor que trabaje según una estrategia comercial, podemos modificarlo para que funcionara de forma conjunta con otros ejemplares de estrategias comerciales mediante operaciones no muy complicadas.

Pero la principal desventaja no tarda en hacerse patente: al combinar varias estrategias, debemos reducir en mayor o menor medida el tamaño de las posiciones abiertas por cada ejemplar de estrategia. Y esto puede provocar la eliminación completa de algunos o incluso todos los ejemplares de estrategias comerciales. Cuantos más ejemplares de estrategias incluyamos en el trabajo paralelo o cuanto menor sea el depósito inicial elegido para negociar, más probable será este resultado, ya que el tamaño mínimo de las posiciones abiertas en el mercado es fijo.

Además, cuando varios ejemplares de estrategias funcionan juntos, se produce una situación en la que se abren posiciones opuestas del mismo tamaño. En términos de volumen agregado, esto equivale a no tener posiciones abiertas, pero se sigue cobrando swap por las posiciones opuestas abiertas.


Segundo esquema de trabajo

Para subsanar las deficiencias, hemos decido trasladar todas las operaciones con las posiciones de mercado a una ubicación independiente, eliminando la capacidad de las estrategias comerciales de abrir posiciones de mercado directamente. Bien es cierto que esto complica un poco el rediseño de las estrategias preparadas, pero supone una pérdida poco sustancial que se recupera con creces eliminando el principal inconveniente del primer esquema.

Así, en nuestro esquema aparecen dos nuevas entidades: las posiciones virtuales (la clase CVirtualOrder) y el receptor de volúmenes comerciales de las estrategias (la claseCReceiver y sus descendientes).

Al inicio del funcionamiento del asesor, en el manejador OnInit() ocurre lo siguiente:

  • Se crea un objeto receptor.
  • Se crea un objeto de experto al que se le transmite el receptor creado.
  • Los objetos de estrategia comercial se crean y se añaden al asesor experto en su array de estrategias comerciales.
  • Cada estrategia crea su propio array de objetos de posición virtual con el número necesario de estos objetos.

En el manejador de eventos OnTick() del asesor sucede lo siguiente:

  • Se llama al método CAdvisor::Tick() del objeto de asesor.
  • Este método enumera todas las estrategias y llama a su método CStrategy::Tick().
  • Las estrategias en el marco del funcionamiento de CStrategy::Tick() realizan todas las operaciones necesarias para abrir y cerrar las posiciones virtuales. Si se produce algún evento que implique un cambio en la composición de las posiciones virtuales abiertas, la estrategia recordará que se ha producido un cambio mostrando una bandera.
  • Si al menos una estrategia ha establecido la bandera de cambio, el receptor activará el método para ajustar los volúmenes abiertos de posiciones de mercado. Si el ajuste se realiza correctamente, se restablecerá la bandera de cambio de todas las estrategias.

Esquemáticamente, esto puede representarse del modo que sigue:

Fig. 2. Esquema de funcionamiento del segundo artículo

Fig. 2. Esquema de funcionamiento del segundo artículo

Con el funcionamiento organizado de esta forma, ya no nos encontramos con que un ejemplar de una estrategia no tiene ningún efecto sobre el tamaño de las posiciones abiertas en el mercado. Por el contrario, incluso un ejemplar que abra un volumen virtual muy pequeño puede ser la gota que colme el volumen total de posiciones virtuales de múltiples ejemplares de estrategia por encima del volumen mínimo de posición de mercado permitido. Y entonces se abrirá la posición de mercado real.

Paralelamente, hemos logrado otros cambios agradables, como un posible ahorro en los swaps, una menor utilización de los depósitos, una disminución de las reducciones observadas y una mejora de los indicadores de evaluación de la calidad de las operaciones (ratio de Sharpe, factor de beneficio).

En el proceso de prueba del segundo esquema hemos observado las siguientes cosas:

  • Cada estrategia realiza primero el mismo procesamiento de las posiciones virtuales ya abiertas para determinar los niveles de StopLoss y TakeProfit activados. Si se alcanza alguno de los niveles, se cerrará dicha posición virtual. Por lo tanto, este procesamiento lo hemos colocado inmediatamente en un método estático de la clase CVirtualOrder. Pero esta solución nos sigue pareciendo una generalización insuficiente.
  • Así que hemos ampliado las clases básicas añadiéndoles nuevas entidades obligatorias. Básicamente, si no queremos pasar a trabajar con posiciones virtuales, podemos seguir usando dichas clases básicas simplemente transmitiéndoles objetos "vacíos". Por ejemplo, podemos crear un objeto de la clase CReceiver que contenga solo métodos stub vacíos. Pero también será más bien una solución temporal que necesita un rediseño.
  • Así, hemos dotado a la clase básica CStrategy de métodos adicionales y de una propiedad para monitorear los cambios en la composición de las posiciones virtuales abiertas, lo que se ha extendido al uso de estos métodos en la clase básica CAdvisor. Nuevamente, esto parece un paso hacia la reducción de las posibilidades y la imposición de una implementación demasiado concreta en la clase básica.
  • Asimismo, hemos añadido el método Volume() a la clase básica CStrategy que devuelve el volumen total de las posiciones virtuales abiertas, porque la clase CVolumeReceiver que escribimos necesita información sobre los volúmenes virtuales abiertos de cada estrategia. Sin embargo, de este modo se elimina la posibilidad de abrir posiciones virtuales en varios símbolos dentro de una misma estrategia comercial, en cuyo caso el volumen total perderá su significado. Para probar estrategias de un solo símbolo esta solución está bien, pero no más que eso.
  • Así, usábamos un array en la clase CReceiver para almacenar los punteros a las estrategias creadas en el asesor experto, de forma que el receptor pueda conocer el volumen virtual abierto de las estrategias a través de ellos. Esto daba lugar a la duplicación de código que se ocupa de rellenar los arrays de estrategia en el asesor y el receptor.
  • Lo que explotábamos explícitamente en la clase CVolumeReceiver en particular es que cada estrategia abre posiciones en un solo símbolo: cuando se añade al array de estrategias del receptor, la estrategia informa de su símbolo, y se añade al array de símbolos utilizado. El receptor trabaja entonces solo con los símbolos añadidos a su array de símbolos. Ya hemos mencionado anteriormente la limitación resultante.
Basándonos en el análisis de las deficiencias enumeradas y en el debate de los comentarios, introduciremos los cambios que siguen:
  • Limpiaremos las clases básicas CStrategy y CAdvisor tanto como sea posible. Para el desarrollo de la rama de los asesores que utilizan trading virtual, crearemos nuestras propias clases derivadas CVirtualStrategy y CVirtualAdvisor. Ahora serán nuestras clases padre para las estrategias específicas y los expertos.
  • Limpiemos ahora la clase de posiciones virtuales. Añadiremos a cada posición virtual un puntero al objeto receptor que se ocupará de la salida del volumen comercial virtual al mercado, y un objeto de estrategia comercial que tomará las decisiones sobre la apertura/cierre de la posición virtual. Esto permitirá notificar a los objetos interesados las operaciones de apertura/cierre de posiciones virtuales.
  • Trasladaremos el almacenamiento de todas las posiciones virtuales a un único array, en lugar de distribuirlas entre varios arrays pertenecientes a ejemplares de la estrategia. Cada ejemplar de la estrategia solicitará varios elementos de este array para su funcionamiento. El propietario del conjunto total será el receptor de los volúmenes comerciales.
  • El receptor será solo uno en cada asesor. Así que vamos a implementarlo como Singleton; un solo ejemplar de este estará disponible en todas las ubicaciones requeridas. Formaremos dicha implementación como una clase derivada CVirtualReceiver.
  • Ahora añadiremos un conjunto de nuevas entidades, los receptores simbólicos (clase CVirtualSymbolReceiver) a la composición de los receptores. Cada receptor simbólico solo trabajará con las posiciones virtuales de su símbolo, que se unirán automáticamente al receptor simbólico cuando se abran y se desunirán del mismo cuando se cierren.
Vamos a intentar implementar todo esto.


Limpiando las clases básicas

En las clases básicas CStrategy y CAdvisor dejaremos solo lo esencial. Para CStartegy, dejaremos solo el método de procesamiento de eventos OnTick, obteniendo este código conciso:

//+------------------------------------------------------------------+
//| Base class of the trading strategy                               |
//+------------------------------------------------------------------+
class CStrategy {
public:
   virtual void      Tick() = 0; // Handle OnTick events
};

Todo lo demás ya se encontrará en los descendientes de esta clase.

En la clase básica CAdvisor, introduciremos un pequeño archivo llamado Macros.mqh, que contiene varias macros útiles para realizar operaciones con arrays ordinarios:

  • APPEND(A, V)  — añadir el elemento V al array A hasta el final del array;
  • FIND(A, V, I) — escribir en la variable I un índice del elemento del array A igual al valor V. Si no se encuentra el elemento, se escribirá el valor -1 en la variable I;
  • ADD(A, V) — añadir el elemento V al array A al final, si dicho elemento no está ya en el array;
  • FOREACH(A, D) — ciclo a través de los índices de los elementos del array A (el índice estará en la variable local i), que ejecuta en el cuerpo las acciones D;
  • REMOVE_AT(A, I) — eliminar un elemento del array A en la posición con índice I con el desplazamiento de los elementos siguientes y la reducción del tamaño del array;
  • REMOVE(A, V) — eliminar un elemento igual a V del array A

// Useful macros for array operations
#ifndef __MACROS_INCLUDE__
#define APPEND(A, V)    A[ArrayResize(A, ArraySize(A) + 1) - 1] = V;
#define FIND(A, V, I)   { for(I=ArraySize(A)-1;I>=0;I--) { if(A[I]==V) break; } }
#define ADD(A, V)       { int i; FIND(A, V, i) if(i==-1) { APPEND(A, V) } }
#define FOREACH(A, D)   { for(int i=0, im=ArraySize(A);i<im;i++) {D;} }
#define REMOVE_AT(A, I) { int s=ArraySize(A);for(int i=I;i<s-1;i++) { A[i]=A[i+1]; } ArrayResize(A, s-1);}
#define REMOVE(A, V)    { int i; FIND(A, V, i) if(i>=0) REMOVE_AT(A, i) }
#define __MACROS_INCLUDE__
#endif
//+------------------------------------------------------------------+

Estas macros se utilizarán también en otros archivos, ya que hacen el código más compacto y legible y evita llamar a funciones adicionales.

Vamos a eliminar todos los lugares donde hemos encontrado el receptor de la clase CAdvisor; también dejaremos solo la llamada a los manejadores correspondientes en las estrategias en el método de procesamiento de eventos OnTick. Obtendremos un código como este:

#include "Macros.mqh"
#include "Strategy.mqh"

//+------------------------------------------------------------------+
//| EA base class                                                    |
//+------------------------------------------------------------------+
class CAdvisor {
protected:
   CStrategy         *m_strategies[];  // Array of trading strategies
public:
                    ~CAdvisor();                // Destructor
   virtual void      Tick();                    // OnTick event handler
   virtual void      Add(CStrategy *strategy);  // Method for adding a strategy
};

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CAdvisor::~CAdvisor() {
// Delete all strategy objects
   FOREACH(m_strategies, delete m_strategies[i]);
}

//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CAdvisor::Tick(void) {
// Call OnTick handling for all strategies
   FOREACH(m_strategies, m_strategies[i].Tick());
}

//+------------------------------------------------------------------+
//| Strategy adding method                                           |
//+------------------------------------------------------------------+
void CAdvisor::Add(CStrategy *strategy) {
   APPEND(m_strategies, strategy);  // Add the strategy to the end of the array
}
//+------------------------------------------------------------------+

Estas clases permanecerán en los archivos Strategy.mqh y Advisor.mqh en la carpeta actual.

Ahora trasladaremos el código necesario a las clases derivadas de la estrategia y el asesor que deberán trabajar con posiciones virtuales.

Asimismo, crearemos una clase CVirtualStrategy heredada de CStrategy. Le añadiremos los siguientes campos y métodos:

  • array de posiciones (órdenes) virtuales;
  • número total de posiciones y órdenes abiertas;
  • método de recuento del número de posiciones y órdenes virtuales abiertas;
  • métodos de procesamiento de eventos de apertura/cierre de una posición (orden) virtual.
Por ahora, los métodos para procesar los eventos de apertura/cierre de las posiciones virtuales simplemente llamarán al método para recalcular el número de posiciones virtuales abiertas, que actualizará el valor del campo m_ordersTotal. Por el momento, no ha sido necesario tomar otras medidas, pero es posible que debamos hacer algo más en el futuro. Por lo tanto, de momento, estos métodos se separarán del método de recuento de las posiciones virtuales abiertas.

#include "Strategy.mqh"
#include "VirtualOrder.mqh"

//+------------------------------------------------------------------+
//| Class of a trading strategy with virtual positions               |
//+------------------------------------------------------------------+
class CVirtualStrategy : public CStrategy {
protected:
   CVirtualOrder     *m_orders[];   // Array of virtual positions (orders)
   int               m_ordersTotal; // Total number of open positions and orders

   virtual void      CountOrders(); // Calculate the number of open positions and orders

public:
   virtual void      OnOpen();      // Event handler for opening a virtual position (order)
   virtual void      OnClose();     // Event handler for closing a virtual position (order)
};

//+------------------------------------------------------------------+
//| Counting open virtual positions and orders                       |
//+------------------------------------------------------------------+
void CVirtualStrategy::CountOrders() {
   m_ordersTotal = 0;
   FOREACH(m_orders, if(m_orders[i].IsOpen()) { m_ordersTotal += 1; })
}

//+------------------------------------------------------------------+
//| Event handler for opening a virtual position (order)             |
//+------------------------------------------------------------------+
void CVirtualStrategy::OnOpen() {
   CountOrders();
}

//+------------------------------------------------------------------+
//| Event handler for closing a virtual position (order)             |
//+------------------------------------------------------------------+
void CVirtualStrategy::OnClose() {
   CountOrders();
}

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

Como hemos eliminado el trabajo con el receptor de la clase básica CAdvisor, deberemos trasladarlo a nuestra nueva clase hija CVirtualAdvisor. En esta clase, añadiremos el campo m_receiver para almacenar el puntero al objeto receptor del volumen de operaciones.

En el constructor, este campo se inicializará con un puntero al único objeto receptor posible, que se acabará de crear en este punto al llamar al método estático CVirtualReceiver::Instance(). Y el destructor se asegurará de que este objeto se elimine correctamente.

En el manejador del evento OnTick, también añadiremos algunas nuevas acciones. Antes de activar los manejadores de las estrategias para este evento, primero activaremos el manejador del receptor para este evento, y después de que el evento sea activado por las estrategias, activaremos el método de receptor que realizará el ajuste del volumen abierto. Si el receptor es ahora el propietario de todos los objetos virtuales, podrá determinar si se ha producido un cambio. Por ello, no existe ninguna implementación para monitorear los cambios en la clase de estrategia comercial, así que la eliminaremos no solo de la clase básica de estrategia, sino por completo.

#include "Advisor.mqh"
#include "VirtualReceiver.mqh"

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   CVirtualReceiver  *m_receiver; // Receiver object that brings positions to the market

public:
                     CVirtualAdvisor(ulong p_magic = 1); // Constructor
                    ~CVirtualAdvisor();                  // Destructor
   virtual void      Tick() override;                    // OnTick event handler

};

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(ulong p_magic = 1) :
// Initialize the receiver with a static receiver
   m_receiver(CVirtualReceiver::Instance(p_magic)) {};

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CVirtualAdvisor::~CVirtualAdvisor() {
   delete m_receiver;         // Remove the recipient
}

//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CVirtualAdvisor::Tick(void) {
// Receiver handles virtual positions
   m_receiver.Tick();
   
// Start handling in strategies
   CAdvisor::Tick();
   
// Adjusting market volumes
   m_receiver.Correct();
}
//+------------------------------------------------------------------+

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


Ampliación de la clase de posiciones virtuales

Bien, ahora añadiremos a la clase de posición virtual un puntero al objeto receptor m_receiver y al objeto de estrategia comercial m_strategy. Los valores para estos campos tendrán que ser transmitidos a través de los parámetros del constructor, así que vamos a hacer cambios en él también. También necesitaremos añadir un par de getters para las propiedades privadas de la posición virtual: Id() y Symbol(). Ahora mostraremos el código añadido en la descripción de la clase:

//+------------------------------------------------------------------+
//| Class of virtual orders and positions                            |
//+------------------------------------------------------------------+
class CVirtualOrder {
private:
//--- Static fields...
   
//--- Related recipient objects and strategies
   CVirtualReceiver  *m_receiver;
   CVirtualStrategy  *m_strategy;

//--- Order (position) properties ...
   
//--- Closed order (position) properties ...
   
//--- Private methods
   
public:
                     CVirtualOrder(
      CVirtualReceiver *p_receiver,
      CVirtualStrategy *p_strategy
   );                                  // Constructor

//--- Methods for checking the position (order) status ...
   

//--- Methods for receiving position (order) properties ...
   ulong             Id() {            // ID
      return m_id;
   }
   string            Symbol() {        // Symbol
      return m_symbol;
   }

//--- Methods for handling positions (orders) ...
  
};

En la implementación del constructor, simplemente añadiremos dos líneas en la lista de inicialización para establecer los valores de los nuevos campos a partir de los parámetros del constructor:

CVirtualOrder::CVirtualOrder(CVirtualReceiver *p_receiver, CVirtualStrategy *p_strategy) :
// Initialization list
   m_id(++s_count),  // New ID = object counter + 1
   m_receiver(p_receiver),
   m_strategy(p_strategy),
   ...,
   m_point(0) {
}

La notificación al receptor y a la estrategia solo deberá producirse cuando se abra o se cierre una posición virtual. Esto solo ocurrirá en los métodos Open() y Close(), así que vamos a añadirles un poco de código:

//+------------------------------------------------------------------+
//| Open a virtual position                                          |
//+------------------------------------------------------------------+
bool CVirtualOrder::Open(...) {
   // If the position is already open, then do nothing ...

   if(s_symbolInfo.Name(symbol)) {  // Select the desired symbol
      // Update information about current prices ...

      // Initialize position properties ...
  
      // Depending on the direction, set the opening price, as well as the SL and TP levels ...
            
      // Notify the recipient and the strategy that the position (order) is open
      m_receiver.OnOpen(GetPointer(this));
      m_strategy.OnOpen();

      ...

      return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| Close a position                                                 |
//+------------------------------------------------------------------+
void CVirtualOrder::Close() {
   if(IsOpen()) { // If the position is open
      ...
      // Define the closure reason to be displayed in the log ...
     
      // Save the close price depending on the type ...
    
      // Notify the recipient and the strategy that the position (order) is open
      m_receiver.OnClose(GetPointer(this));
      m_strategy.OnClose();
   }
}

En los manejadores OnOpen() y OnClose() para el receptor, transmitiremos como parámetro el puntero al objeto de posición virtual actual. Los manejadores de la estrategia aún no han necesitado esto, por lo que se implementarán sin el parámetro.

Este código permanecerá en la carpeta actual en un archivo con el mismo nombre: VirtualOrder.mqh.


Realización de un nuevo receptor

Empezaremos implementando la clase receptora CVirtualReceiver, asegurándonos de que el ejemplar de esta clase sea singular. Para ello, utilizaremos un patrón de diseño estándar llamado Singleton. Necesitaremos:

  • hacer que el constructor de la clase no sea público;
  • añadir un campo estático de clase que almacene un puntero al objeto de esta clase;
  • añadir un método estático que cree un ejemplar de esta clase si no existe ninguna, o retorne una existente.

//+------------------------------------------------------------------+
//| Class for converting open volumes to market positions (receiver) |
//+------------------------------------------------------------------+
class CVirtualReceiver : public CReceiver {
protected:
// Static pointer to a single class instance
   static   CVirtualReceiver *s_instance;

   ...

   CVirtualReceiver(ulong p_magic = 0);   // Private constructor

public:
//--- Static methods
   static
   CVirtualReceiver  *Instance(ulong p_magic = 0);    // Singleton - creating and getting a single instance

   ...
};

// Initializing a static pointer to a single class instance
CVirtualReceiver *CVirtualReceiver::s_instance = NULL;


//+------------------------------------------------------------------+
//| Singleton - creating and getting a single instance               |
//+------------------------------------------------------------------+
CVirtualReceiver* CVirtualReceiver::Instance(ulong p_magic = 0) {
   if(!s_instance) {
      s_instance = new CVirtualReceiver(p_magic);
   }
   return s_instance;
}

A continuación, añadiremos un array a la clase para almacenar todas las posiciones virtuales m_orders. Cada ejemplar de la estrategia solicitará al receptor un determinado número de posiciones virtuales. Para ello, agregaremos el método estático Get() que creará el número necesario de objetos de posiciones virtuales añadiendo los punteros a estos al array de receptores y al array de posición virtual de la estrategia:

class CVirtualReceiver : public CReceiver {
protected:
   ...
   CVirtualOrder     *m_orders[];         // Array of virtual positions
   
   ...

public:
//--- Static methods
   ...
   static void       Get(CVirtualStrategy *strategy,
                         CVirtualOrder *&orders[],
                         int n); // Allocate the necessary amount of virtual positions to the strategy
   ...
};

...

//+------------------------------------------------------------------+
//| Allocate the necessary amount of virtual positions to strategy   |
//+------------------------------------------------------------------+
static void CVirtualReceiver::Get(CVirtualStrategy *strategy,   // Strategy
                                  CVirtualOrder *&orders[],     // Array of strategy positions
                                  int n                         // Required number
                                 ) {
   CVirtualReceiver *self = Instance();   // Receiver singleton
   ArrayResize(orders, n);                // Expand the array of virtual positions
   FOREACH(orders,
           orders[i] = new CVirtualOrder(self, strategy); // Fill the array with new objects
           APPEND(self.m_orders, orders[i])) // Register the created virtual position
   ...
}

Ahora es el momento de añadir un array a la clase para los punteros a los objetos de receptores simbólicos (clase CVirtualSymbolReceiver). Esta clase aún no ha sido creada, pero en general ya entendemos lo que debe hacer: abrir y cerrar directamente posiciones de mercado según los volúmenes virtuales en un único símbolo. Por lo tanto, podemos decir que el número de objetos de receptor simbólico será igual al número de símbolos diferentes utilizados en el asesor. Haremos de esta clase un heredero de CReceiver, por lo que tendrá un método Correct() que hará el trabajo útil principal, y también añadiremos los métodos auxiliares necesarios.

Pero esto será un poco más adelante, ahora volveremos a la clase CVirtualReceiver y le añadiremos una redefinición del método virtual Correct().

class CVirtualReceiver : public CReceiver {
protected:
   ...
   CVirtualSymbolReceiver *m_symbolReceivers[];       // Array of recipients for individual symbols

public:
   ...
//--- Public methods
   virtual bool      Correct() override;              // Adjustment of open volumes
};

La implementación del método Correct() ahora será bastante sencilla, ya que trasladaremos el trabajo principal a un nivel inferior de la jerarquía. Y ahora solo tendremos que recorrer todos los receptores simbólicos y llamar a su método Correct().

Para reducir el número de llamadas innecesarias, añadiremos una comprobación preliminar que verificará que la negociación está permitida añadiendo el método IsTradeAllowed(), que responderá a esta pregunta. Y también añadiremos un campo de la clase m_isChanged, que deberá actuar como bandera sobre la presencia de cambios en la composición de las posiciones virtuales abiertas. También se comprobará antes de solicitar la corrección.

class CVirtualReceiver : public CReceiver {
   ...
   bool              m_isChanged;         // Are there any changes in open positions?
   ...
   bool              IsTradeAllowed();    // Is trading available?

public:
   ...

   virtual bool      Correct() override;  // Adjustment of open volumes
};
//+------------------------------------------------------------------+
//| Adjust open volumes                                              |
//+------------------------------------------------------------------+
bool CVirtualReceiver::Correct() {
   bool res = true;
   if(m_isChanged && IsTradeAllowed()) {
      // If there are changes, then we call the adjustment of the recipients of individual symbols
      FOREACH(m_symbolReceivers, res &= m_symbolReceivers[i].Correct());
      m_isChanged = !res;
   }
   return res;
}

En el método IsTradeAllowed(), comprobaremos el estado del terminal y de la cuenta comercial para determinar si es posible realizar operaciones reales:

//+------------------------------------------------------------------+
//| Is trading available?                                            |
//+------------------------------------------------------------------+
bool CVirtualReceiver::IsTradeAllowed() {
   return (true
           && MQLInfoInteger(MQL_TRADE_ALLOWED)
           && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)
           && AccountInfoInteger(ACCOUNT_TRADE_EXPERT)
           && AccountInfoInteger(ACCOUNT_TRADE_ALLOWED)
           && TerminalInfoInteger(TERMINAL_CONNECTED)
          );
}

Utilizaremos la bandera en el método Correct() sobre la presencia de cambios, que se restablecía si la corrección del volumen tenía éxito. Pero, ¿dónde deberemos colocar esta bandera? Obviamente, esto debería ocurrir si se abre o se cierra alguna posición virtual. En la clase CVirtualOrder, hemos añadido especialmente a los métodos de apertura/cierre la llamada de los métodos OnOpen() y OnClose(), aún ausentes en la clase CVirtualReceiver. Aquí es donde marcaremos la presencia de cambios.

Además, en estos manejadores tendremos que notificar al receptor simbólico deseado que hay un cambio. Al abrir la primera posición virtual en un símbolo determinado, el receptor simbólico correspondiente aún no existirá, por lo que deberemos crearlo y notificarlo. En las siguientes operaciones de apertura/cierre de posiciones virtuales para este símbolo, el receptor del símbolo correspondiente ya existirá, por lo que solo deberemos notificárselo.

class CVirtualReceiver : public CReceiver {
   ...

public:
   ...

//--- Public methods
   void              OnOpen(CVirtualOrder *p_order);  // Handle virtual position opening
   void              OnClose(CVirtualOrder *p_order); // Handle virtual position closing
   ...
};

//+------------------------------------------------------------------+
//| Handle opening a virtual position                                |
//+------------------------------------------------------------------+
void CVirtualReceiver::OnOpen(CVirtualOrder *p_order) {
   string symbol = p_order.Symbol();      // Define position symbol
   CVirtualSymbolReceiver *symbolReceiver;
   int i;
   FIND(m_symbolReceivers, symbol, i);    // Search for the symbol recipient

   if(i == -1) {
      // If not found, then create a new recipient for the symbol
      symbolReceiver = new CVirtualSymbolReceiver(m_magic, symbol);
      // and add it to the array of symbol recipients 
      APPEND(m_symbolReceivers, symbolReceiver);
   } else {
      // If found, then take it
      symbolReceiver = m_symbolReceivers[i];
   }
   
   symbolReceiver.Open(p_order); // Notify the symbol recipient about the new position
   m_isChanged = true;           // Remember that there are changes
}

//+------------------------------------------------------------------+
//| Handle closing a virtual position                                |
//+------------------------------------------------------------------+
void CVirtualReceiver::OnClose(CVirtualOrder *p_order) {
   string symbol = p_order.Symbol();   // Define position symbol
   int i;
   FIND(m_symbolReceivers, symbol, i); // Search for the symbol recipient

   if(i != -1) {
      m_symbolReceivers[i].Close(p_order);   // Notify the symbol recipient about closing a position
      m_isChanged = true;                    // Remember that there are changes
   }
}

Además de abrir/cerrar posiciones virtuales según las señales de las estrategias comerciales, estas podrán cerrarse al alcanzar los niveles StopLoss o TakeProfit. En la clase CVirtualOrder, tendremos el método Tick(), específico para este fin, que comprobará los niveles y cerrará la posición virtual si es necesario. Pero deberá llamarse en cada tick y para todas las posiciones virtuales. Esto es exactamente lo que hará el método Tick() de la clase CVirtualReceiver que vamos a añadir:

class CVirtualReceiver : public CReceiver {
   ...

public:
   ...

//--- Public methods
   void              Tick();     // Handle a tick for the array of virtual orders (positions)
   ...
};

//+------------------------------------------------------------------+
//| Handle a tick for the array of virtual orders (positions)        |
//+------------------------------------------------------------------+
void CVirtualReceiver::Tick() {
   FOREACH(m_orders, m_orders[i].Tick());
}

Por último, nos ocuparemos de la correcta liberación de la memoria asignada a los objetos de posición virtual. Como todos ellos están en el array m_orders, añadiremos un destructor donde ejecutaremos su eliminación:

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CVirtualReceiver::~CVirtualReceiver() {
   FOREACH(m_orders, delete m_orders[i]); // Remove virtual positions
}

Guardaremos el código obtenido en el archivo VirtualReceiver.mqh en la carpeta actual.


Implementación del receptor simbólico

Queda por implementar la última clase CVirtualSymbolReceiver para que el esquema quede completo y utilizable. Tomaremos su contenido básico de la clase CVolumeReceiver del artículo anterior, eliminando los lugares relacionados con la definición del símbolo de cada posición virtual y la enumeración de los símbolos durante la realización del ajuste.

Los objetos de esta clase también tendrán su propio array de punteros a los objetos de posiciones virtuales, pero aquí su composición cambiará constantemente. Requeriremos que este array contenga solo posiciones virtuales abiertas. Entonces quedará claro lo que debemos hacer al abrir y cerrar una posición virtual: en cuanto una posición virtual se abra, tendremos que añadirla al array de receptor simbólico correspondiente, y en cuanto se encierre, deberemos eliminarla de este array.

También nos convendrá disponer de un bandera que indique la presencia de cambios en la composición de las posiciones virtuales abiertas. Esto nos ayudará a evitar comprobaciones innecesarias en cada tick.

Vamos a añadir a la clase los campos para el símbolo, el array de posiciones y la indicación de cambios, así como dos métodos de procesamiento de apertura/cierre:

class CVirtualSymbolReceiver : public CReceiver {
   string            m_symbol;         // Symbol
   CVirtualOrder     *m_orders[];      // Array of open virtual positions
   bool              m_isChanged;      // Are there any changes in the composition of virtual positions?

   ...   

public:
   ...
   void              Open(CVirtualOrder *p_order);    // Register opening a virtual position
   void              Close(CVirtualOrder *p_order);   // Register closing a virtual position 
   ...
};

La implementación de estos métodos en sí es trivial: añadiremos/eliminaremos la posición virtual transmitida desde el array y estableceremos la bandera sobre la presencia de cambios.

//+------------------------------------------------------------------+
//| Register opening a virtual position                              |
//+------------------------------------------------------------------+
void CVirtualSymbolReceiver::Open(CVirtualOrder *p_order) {
   APPEND(m_orders, p_order); // Add a position to the array
   m_isChanged = true;        // Set the changes flag
}

//+------------------------------------------------------------------+
//| Register closing a virtual position                              |
//+------------------------------------------------------------------+
void CVirtualSymbolReceiver::Close(CVirtualOrder *p_order) {
   REMOVE(m_orders, p_order); // Remove a position from the array
   m_isChanged = true;        // Set the changes flag
}

También tendremos que buscar en el receptor el símbolo deseado según el nombre del símbolo. Para utilizar el algoritmo de búsqueda lineal habitual de la macro FIND(A,V,I), añadiremos un operador sobrecargado para comparar un receptor simbólico con una cadena, que devolverá true si el símbolo del ejemplar dado coincide con la cadena transmitida:

class CVirtualSymbolReceiver : public CReceiver {
   ...

public:
   ...
   bool              operator==(const string symbol) {// Operator for comparing by a symbol name
      return m_symbol == symbol;
   }
   ...
};

He aquí una descripción completa de la clase CVirtualSymbolReceiver. Podrá ver la implementación concreta de todos los métodos en los archivos adjuntos.

class CVirtualSymbolReceiver : public CReceiver {
   string            m_symbol;         // Symbol
   CVirtualOrder     *m_orders[];      // Array of open virtual positions
   bool              m_isChanged;      // Are there any changes in the composition of virtual positions?

   bool              m_isNetting;      // Is this a netting account?

   double            m_minMargin;      // Minimum margin for opening

   CPositionInfo     m_position;       // Object for obtaining properties of market positions
   CSymbolInfo       m_symbolInfo;     // Object for getting symbol properties
   CTrade            m_trade;          // Object for performing trading operations

   double            MarketVolume();   // Volume of open market positions
   double            VirtualVolume();  // Volume of open virtual positions
   bool              IsTradeAllowed(); // Is trading by symbol available? 

   // Required volume difference
   double            DiffVolume(double marketVolume, double virtualVolume);

   // Volume correction for the required difference
   bool              Correct(double oldVolume, double diffVolume);

   // Auxiliary opening methods
   bool              ClearOpen(double diffVolume);
   bool              AddBuy(double volume);
   bool              AddSell(double volume);
   
   // Auxiliary closing methods
   bool              CloseBuyPartial(double volume);
   bool              CloseSellPartial(double volume);
   bool              CloseHedgingPartial(double volume, ENUM_POSITION_TYPE type);
   bool              CloseFull();

   // Check margin requirements
   bool              FreeMarginCheck(double volume, ENUM_ORDER_TYPE type);

public:
                     CVirtualSymbolReceiver(ulong p_magic, string p_symbol);  // Constructor
   bool              operator==(const string symbol) {// Operator for comparing by a symbol name
      return m_symbol == symbol;
   }
   void              Open(CVirtualOrder *p_order);    // Register opening a virtual position
   void              Close(CVirtualOrder *p_order);   // Register closing a virtual position 
   
   virtual bool      Correct() override;              // Adjustment of open volumes
};

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


Comparación de los resultados

El esquema de funcionamiento resultante puede representarse del siguiente modo:


Fig. 3. Esquema de funcionamiento de este artículo

Fig. 3. Esquema de funcionamiento de este artículo

Ahora viene lo divertido. Vamos a compilar un asesor experto utilizando nueve ejemplares de estrategias con los mismos parámetros que en el último artículo. Haremos las pruebas con un asesor similar al del artículo anterior y con el recién compilado:


Fig. 3. Resultados del asesor del artículo anterior.


Fig. 4. Resultados del asesor de este artículo.

En general, los resultados son más o menos los mismos. Las imágenes de los gráficos de balance son indistinguibles a simple vista. Las pequeñas diferencias observadas en los informes pueden deberse a diversas razones y se analizarán más a fondo.


Evaluación del potencial posterior

En la discusión del artículo anterior en el foro, los usuarios plantearon una pregunta legítima: ¿cuáles son los resultados comerciales más atractivos que pueden obtenerse con el planteamiento considerado? Hasta ahora los gráficos han mostrado una rentabilidad del 20% en 5 años, lo que no parece especialmente atractivo.

Por el momento, la respuesta a esta pregunta puede ser aproximadamente la siguiente. En primer lugar, debemos distinguir claramente entre los resultados derivados de las estrategias simples elegidas y los derivados de la realización de su trabajo conjunto.

Los resultados de la primera categoría cambiarán alternando de una estrategia simple a otra. Está claro que cuanto mejores sean los resultados de las ejemplares individuales de las estrategias simples, mejor será su resultado conjunto. Los resultados aquí presentados se derivan de una sola idea comercial, y de inicio están condicionados precisamente por su calidad e idoneidad. Evaluaremos estos resultados simplemente según la relación beneficio/reducción a lo largo del intervalo de prueba.

Los resultados de la segunda categoría serán los resultados comparativos del trabajo conjunto y del trabajo individual. En este caso, la evaluación se realizará usando otros indicadores: la mejora de la linealidad del gráfico de la curva de crecimiento de los fondos, la disminución de la reducción y otros. Precisamente estos resultados parecen más importantes, pues esperamos utilizarlos para llevar los resultados no especialmente destacados de la primera categoría a un nivel aceptable.

Pero para obtener todos los resultados, nos gustaría aplicar primero la negociación con lotes variables. Sin ella, resulta más difícil estimar incluso el ratio rentabilidad/reducción a partir de los resultados de las pruebas, pero aun así es posible. 

Vamos a tratar de tomar un pequeño depósito inicial y encontrar un nuevo valor óptimo del tamaño de las posiciones abiertas para una reducción máxima permitida del 50% para el periodo de 5 años (2018.01.01 - 2023.01.01). A continuación le mostramos los resultados de la ejecución del asesor de este artículo con un multiplicador de tamaño de posición diferente, pero constante a lo largo de los cinco años con un depósito inicial de $1 000. En el artículo anterior, los tamaños de posición se calibraron para un tamaño de depósito de $10000, por lo que el valor inicial depoPart_ se ha reducido unas 10 veces.

Fig. 5. Resultados de las pruebas con distintos tamaños de posiciones.

Podemos ver que en el mínimo depoPart_ = 0.04 el asesor no ha abierto posiciones reales, porque su volumen cuando se recalcula proporcionalmente al saldo es inferior a 0.01. Pero a partir del siguiente valor del multiplicador depoPart_ = 0,06, se han abierto posiciones de mercado.

Con un máximo depoPart_ = 0,4, obtendremos un beneficio de unos $22 800. Sin embargo, la reducción mostrada aquí es la reducción relativa encontrada durante toda la ejecución. Pero el 10% de 23 000 y de 1 000 son valores muy diferentes. Por lo tanto, deberemos fijarnos en los resultados de un solo inicio:

Fig. 6. Resultados de las pruebas con el máximo depoPart_ = 0,4

Como podemos ver, en realidad se ha alcanzado una reducción de $1 167, que en el momento en que se alcanzó era solo el 9,99% del balance actual, pero si el inicio del periodo de prueba se hubiera situado justo antes de este desagradable momento, habríamos perdido todo el depósito. Por lo tanto, no podemos utilizar este tamaño de posiciones.

Veamos los resultados cuando depoPart_ = 0,2


Fig. 7. Resultados de la prueba con depoPart_ = 0,2


En este caso, la reducción máxima no ha superado los $494, es decir, aproximadamente el 50% del depósito inicial de $1 000. Por lo tanto, podemos afirmar que con este tamaño de posiciones, aunque elijamos lo peor posible el inicio del periodo durante los cinco años considerados, no se perderá la totalidad del depósito.

Con este tamaño de posición, los resultados de la prueba a 1 año (2022) serían los siguientes:


Fig. 8. Resultados de la prueba para 2022 con depoPart_ = 0,2

Es decir, con una reducción máxima prevista de alrededor del 50%, hemos obtenido un beneficio de alrededor del 150% anual.

Estos resultados parecen alentadores, pero no están exentos de unas gotitas de hiel. Por ejemplo, los resultados de 2023, que no han participado en la optimización de los parámetros, ya resultan notablemente peores:

Fig. 9. Resultados de la prueba para 2023 con depoPart_ = 0,2

Es cierto que hemos obtenido un beneficio del 40% en los resultados de las pruebas de fin de año, pero 8 de los 12 meses no han registrado un crecimiento sostenido. Este problema se considera el principal, y en esta serie de artículos nos dedicaremos en general al examen de distintos enfoques para su solución.


Conclusión

Como parte de este artículo, nos hemos preparado para un mayor desarrollo del código, simplificando y optimizando el código de la parte anterior. Asimismo, hemos subsanado las deficiencias detectadas que podrían haber limitado aún más nuestra capacidad de utilizar diversas estrategias comerciales. Los resultados de las pruebas han mostrado que la nueva aplicación funciona tan bien como la anterior. La velocidad de la aplicación no ha cambiado, pero es posible que el beneficio solo aparezca al multiplicar el número de ejemplares de la estrategia.

Para ello, tendremos que abordar al fin cómo almacenaremos los parámetros de entrada de las estrategias, cómo los combinaremos en bibliotecas de parámetros y cómo seleccionaremos las mejores combinaciones entre las que resulten de optimizar los ejemplares de estrategias individuales.

En el próximo artículo, continuaremos en esta dirección.



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

Desarrollo de un sistema de repetición (Parte 56): Adecuación de los módulos Desarrollo de un sistema de repetición (Parte 56): Adecuación de los módulos
Aunque los módulos se comunican de manera adecuada, existe un error al intentar utilizar el indicador de mouse en el servicio de repetición. Necesitamos corregir esto ahora, antes de pasar al siguiente paso. Además, se ha corregido una incidencia en el código del indicador de mouse. Esta versión finalmente se ha vuelto estable y está debidamente finalizada.
Aprendizaje automático y ciencia de datos (Parte 18): Potencie sus modelos de IA con AdaBoost Aprendizaje automático y ciencia de datos (Parte 18): Potencie sus modelos de IA con AdaBoost
AdaBoost, un potente algoritmo de refuerzo diseñado para elevar el rendimiento de sus modelos de IA. AdaBoost, abreviatura de Adaptive Boosting (refuerzo adaptativo), es una sofisticada técnica de aprendizaje por conjuntos que integra a la perfección los aprendices débiles, potenciando su fuerza predictiva colectiva.
Desarrollo de un sistema de repetición (Parte 57): Diseccionamos el servicio de prueba Desarrollo de un sistema de repetición (Parte 57): Diseccionamos el servicio de prueba
Un último detalle: Aunque no se incluye en este artículo, explicaré el código del servicio que se estará utilizando en el próximo, ya que usaremos este mismo código como trampolín para lo que realmente estamos desarrollando. Así que ten un poco de paciencia y espera el próximo artículo, pues las cosas se están poniendo cada día más interesantes.
Desarrollo de un sistema de repetición (Parte 55): Módulo de control Desarrollo de un sistema de repetición (Parte 55): Módulo de control
En este artículo, implementaremos el indicador de control de manera que pueda integrarse en el sistema de mensajes que está en desarrollo. Aunque no es algo muy complejo de hacer, es necesario entender algunos detalles sobre cómo inicializar este módulo. El contenido expuesto aquí tiene como objetivo, pura y simplemente, la didáctica. En ningún caso debe considerarse como una aplicación cuya finalidad no sea el aprendizaje y el estudio de los conceptos mostrados.