
Desarrollamos un Asesor Experto multidivisas (Parte 3): Revisión de la arquitectura
Introducción
En los artículos anteriores continuamos el desarrollo de un Asesor Experto multidivisas 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:
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
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.
- 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.
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.
#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
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





- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso