Desarrollamos un Asesor Experto multidivisas (Parte 2): Transición a posiciones virtuales de estrategias comerciales
Introducción
En el último artículo empezamos a desarrollar un Asesor Experto multidivisas que trabaja simultáneamente con diferentes estrategias comerciales. En la primera fase, solo había dos estrategias diferentes. Estas representaban la realización de la misma idea comercial, trabajaban con el mismo instrumento (símbolo) comercial y el mismo periodo del gráfico (marco temporal). Solo se diferenciaban entre sí por los valores numéricos de los parámetros.
También determinamos el tamaño óptimo de las posiciones abiertas según el nivel máximo de reducción deseado (10% del depósito). Lo hicimos para cada estrategia por separado. Al combinar las dos estrategias, tuvimos que reducir el tamaño de las posiciones abiertas para mantener el nivel de reducción establecido. En el caso de dos estrategias, la disminución era pequeña, pero, ¿y si queremos fusionar docenas o cientos de ejemplares de estrategias? Puede ocurrir que para algunas estrategias sea necesario reducir el tamaño de la posición a un valor inferior a las posiciones abiertas mínimas permitidas por el bróker. En este caso, dichas estrategias simplemente no podrán participar en la negociación. Entonces, ¿cómo conseguir que funcionen después de todo?
Para ello, privaremos a las estrategias del derecho a abrir posiciones y colocar órdenes pendientes por sí mismas. Las estrategias solo tendrán que negociar virtualmente, es decir, memorizar en qué niveles deben abrirse posiciones de un determinado tamaño y, a petición, informar sobre qué volumen debe abrirse ahora. Así, abriremos posiciones reales en el mercado solo después de haber estudiado todas las estrategias y calculado el volumen total necesario, considerando el escalado para soportar la reducción especificada.
Ahora solo nos interesa comprobar si dicho planteamiento resulta idóneo, no si su aplicación es eficaz. Por consiguiente, en este artículo trataremos de escribir al menos alguna implementación funcional de este enfoque, lo cual nos ayudará aún más a construir una implementación más bella desde el punto de vista de la arquitectura. Nos será de ayuda, no en el sentido de que mejoraremos una implementación que ya tengamos, sino en el sentido de que podremos darnos cuenta de que debemos hacerla de otra manera que será mejor.
Vamos a intentar poner esto en práctica.
Recordemos lo que ya hemos hecho
Hemos desarrollado la clase de experto CAdvisor, que almacena un array de ejemplares de estrategias comerciales (más exactamente, de punteros a ejemplares). Esto nos permitirá crear un ejemplar de un asesor en el programa principal y añadirle varios ejemplares de clases de estrategia. Además, como el array almacena los punteros a los objetos de la clase básica CStrategy podremos almacenar los punteros a los objetos de cualquier clase descendiente heredada de CStrategy. En nuestro caso, crearemos una clase descendiente CSimpleVolumesStrategy, dos objetos de los cuales se han añadido a este array en el asesor.
Vamos a decidir un nombre cómodo para facilitar la exposición:
- El asesor será nuestro archivo final con extensión mq5, que después de la compilación dará el archivo ex5 ejecutable correspondiente para su ejecución en el simulador y el terminal.
- El experto será un objeto de la clase CAdvisor declarado en el programa. Solo utilizaremos un ejemplar del asesor en un programa.
- La estrategia será un objeto de una clase hija heredada de la clase básica de la estrategia CStrategy.
Recuerde también que el puntero a un objeto (de una estrategia o de cualquier otra clase) supone información sobre la ubicación en la memoria de un objeto creado previamente (simplificado). Esto permite evitar recrear el mismo objeto en otro lugar de la memoria al transmitirlo a funciones, asignarlo a nuevas variables o a elementos de array.
Por eso almacenaremos punteros a los objetos de estrategia en un array en el asesor, para que no se creen copias de objetos de estrategia al llenar este array. Entonces nos referiremos a los objetos de estrategia originales cuando accedamos a los elementos del array de estrategias.
El funcionamiento del asesor constaba de los siguientes pasos:
- Se creaba un experto en la zona de memoria estática.
- Al inicializar el programa, se creaban dos estrategias en la memoria dinámica y se almacenaban los punteros a ellas en el asesor.
- Cuando el programa se ejecutaba, el asesor llamaba a cada estrategia sucesivamente para realizar las acciones comerciales necesarias llamando al método CStrategy::Tick().
- Al desinicializar el programa, el asesor borraba los objetos de estrategia de la memoria dinámica.
Antes de proceder a la tarea que nos ocupa, haremos algunas pequeñas correcciones en el asesor y en la clase de asesor. En el asesor, haremos que el asesor se cree en una zona de memoria dinámica.
CAdvisor expert; // EA object CAdvisor *expert; // Pointer to the EA object int OnInit() { expert = new CAdvisor(); // Create EA object // The rest of the code from OnInit() ... }
En la clase de asesor, crearemos un destructor, es decir, una función que se llamará automáticamente cuando el objeto de asesor se elimine de la memoria. Trasladaremos al destructor las operaciones de borrado de objetos de las estrategias de memoria dinámica, que actualmente se encuentran en el método CAdvisor::Deinit(). No necesitamos este método ahora, así que lo eliminaremos. También borraremos la variable de clase que almacena el número de estrategias m_strategiesCount. Cuando la necesitemos, podremos utilizar ArraySize().
class CAdvisor { protected: CStrategy *m_strategies[]; // Array of trading strategies public: ~CAdvisor(); // Destructor // ... }; //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ void CAdvisor::~CAdvisor() { // Delete all strategy objects for(int i = 0; i < ArraySize(m_strategies); i++) { delete m_strategies[i]; } }
Ahora, en el programa, sustituiremos en la función OnDeinit() la llamada al método CAdvisor::Deinit para eliminar el objeto de asesor.
void OnDeinit(const int reason) { expert.Deinit(); delete expert; }
Trazando un camino
Si ahora las estrategias comerciales no pueden abrir posiciones de mercado por sí solas, entonces
- necesitaremos objetos que almacenen información sobre las posiciones virtuales de las estrategias;
- necesitaremos medios que conviertan la información sobre las posiciones virtuales en posiciones reales en el mercado.
Los objetos para las posiciones virtuales deberán ser parte integrante de la estrategia y deberá haber más de uno. Así que vamos a llamar a la primera clase nueva CVirtualOrder, y añadir un array de estos objetos a la clase CStrategy. Asimismo, añadiremos a CStrategy una propiedad que almacene el signo de los cambios en la composición de las posiciones virtuales abiertas y los métodos para obtener y fijar su valor. En realidad, este signo determinará en cuál de los dos estados se encuentra actualmente la estrategia:
- no hay cambios - todo el volumen virtual abierto se ha transferido al mercado;
- hay cambios - el volumen virtual no se corresponde con el volumen de mercado, por lo que deberemos ajustar los volúmenes de las posiciones reales de mercado.
Como ahora será otro el responsable de abrir posiciones reales, podemos eliminar la propiedad de número mágico m_magic de la clase básica de la estrategia. En el futuro limpiaremos aún más la clase más básica de estrategias, pero por ahora nos limitaremos a realizar una limpieza parcial.
Dicho esto, la clase básica de la estrategia tendrá el aspecto que sigue.
#include "VirtualOrder.mqh" //+------------------------------------------------------------------+ //| Base class of the trading strategy | //+------------------------------------------------------------------+ class CStrategy { protected: string m_symbol; // Symbol (trading instrument) ENUM_TIMEFRAMES m_timeframe; // Chart period (timeframe) double m_fixedLot; // Size of opened positions (fixed) CVirtualOrder m_orders[]; // Array of virtual positions (orders) int m_ordersTotal; // Total number of open positions and orders double m_volumeTotal; // Total volume of open positions and orders bool m_isChanged; // Sign of changes in open virtual positions void CountOrders(); // Calculate the number and volumes of open positions and orders public: // Constructor CStrategy(string p_symbol = "", ENUM_TIMEFRAMES p_timeframe = PERIOD_CURRENT, double p_fixedLot = 0.01); virtual void Tick(); // Main method - handling OnTick events virtual double Volume(); // Total volume of virtual positions virtual string Symbol(); // Strategy symbol (only one for a single strategy so far) virtual bool IsChanged(); // Are there any changes in open virtual positions? virtual void ResetChanges(); // Reset the sign of changes in open virtual positions };
Ya podemos implementar los métodos Symbol(), IsChanged() y ResetChanges().
//+------------------------------------------------------------------+ //| Strategy symbol | //+------------------------------------------------------------------+ string CStrategy::Symbol() { return m_symbol; } //+------------------------------------------------------------------+ //| Are there any changes to open virtual positions? | //+------------------------------------------------------------------+ bool CStrategy::IsChanged() { return m_isChanged; } //+------------------------------------------------------------------+ //| Reset the flag for changes in virtual positions | //+------------------------------------------------------------------+ void CStrategy::ResetChanges() { m_isChanged = false; }
Los otros métodos: Tick(), Volume() y CountOrders(), los implementaremos en los herederos de la clase básica o en la propia clase básica, pero más adelante.
La segunda clase nueva, cuyos objetos se dedicarán a llevar al mercado las posiciones virtuales de las estrategias, la llamaremos CReceiver. Para funcionar, este objeto deberá tener acceso a todas las estrategias del asesor para poder saber de ellas el símbolo y el volumen a abrir para las posiciones reales. Para un asesor, bastará con un objeto de este tipo. Precisamente el objeto CReceiver debe tener el número mágico que se establecerá para abrir las posiciones de mercado.
#include "Strategy.mqh" //+------------------------------------------------------------------+ //| Base class for converting open volumes into market positions | //+------------------------------------------------------------------+ class CReceiver { protected: CStrategy *m_strategies[]; // Array of strategies ulong m_magic; // Magic public: CReceiver(ulong p_magic = 0); // Constructor virtual void Add(CStrategy *strategy); // Adding strategy virtual bool Correct(); // Adjustment of open volumes }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CReceiver::CReceiver(ulong p_magic) : m_magic(p_magic) { ArrayResize(m_strategies, 0, 128); } //+------------------------------------------------------------------+ //| Add strategy | //+------------------------------------------------------------------+ void CReceiver::Add(CStrategy *strategy) { APPEND(m_strategies, strategy); } //+------------------------------------------------------------------+ //| Adjust open volumes | //+------------------------------------------------------------------+ bool CReceiver::Correct() { return true; }
Esta clase básica no contendrá la implementación de un mecanismo específico para ajustar los volúmenes. Así que en diferentes herederos de esta clase, podremos realizar diferentes implementaciones del ajuste, mientras que un objeto de esta clase podrá servir de sustituto para aquellas estrategias que abran posiciones de mercado por sí mismas (por el momento). Lo necesitaremos para depurar el mecanismo de corrección: tendremos que comparar qué posiciones abre el asesor con el que las propias estrategias realizan operaciones reales, y qué posiciones abre el asesor con el que las estrategias solo realizan operaciones virtuales.
Por lo tanto, prepararemos dos asesores expertos en los que las estrategias del artículo anterior se encargarán por sí mismas de negociar de forma real.
En el primer asesor, la estrategia será un único ejemplar, y en sus parámetros podremos especificar los parámetros de este único ejemplar de la estrategia para optimizar esta.
El segundo asesor contendrá varios ejemplares de estrategias comerciales con parámetros predefinidos derivados de la optimización del primer asesor.
Asesor para optimizar los parámetros de la estrategia
La última vez optimizamos los parámetros de la estrategia usando la implementación de la estrategia no como un objeto de la clase CStrategy. Pero ahora ya tenemos una clase CSimpleVolumesStrategy lista, así que vamos a crear un programa aparte donde el asesor contendrá un solo ejemplar de esta estrategia. Lo único es que nombraremos esta clase un poco diferente para enfatizar en el nombre que la estrategia en sí abrirá posiciones de mercado: en lugar de CSimpleVolumesStrategy, usaremos CSimpleVolumesMarketStrategy y la guardaremos en el archivo SimpleVolumesMarketStrategy.mqh en la carpeta actual.
En el archivo del asesor, cargaremos los parámetros de la estrategia desde las variables de entrada del asesor y añadiremos un ejemplar de la estrategia al objeto de asesor. Obtendremos un asesor que puede usarse para optimizar los parámetros de la estrategia.
#include "Advisor.mqh" #include "SimpleVolumesMarketStrategy.mqh" #include "VolumeReceiver.mqh" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ input string symbol_ = "EURGBP"; // Trading instrument (symbol) input ENUM_TIMEFRAMES timeframe_ = PERIOD_H1; // Chart period input group "=== Opening signal parameters" input int signalPeriod_ = 130; // Number of candles for volume averaging input double signalDeviation_ = 0.9; // Relative deviation from the average to open the first order input double signaAddlDeviation_ = 1.4; // Relative deviation from the average for opening the second and subsequent orders input group "=== Pending order parameters" input int openDistance_ = 0; // Distance from price to pending order input double stopLevel_ = 2000; // Stop Loss (in points) input double takeLevel_ = 475; // Take Profit (in points) input int ordersExpiration_ = 6000; // Pending order expiration time (in minutes) input group "=== Money management parameters" input int maxCountOfOrders_ = 3; // Maximum number of simultaneously open orders input double fixedLot_ = 0.01; // Single order volume input group "=== EA parameters" input ulong magic_ = 27181; // Magic CAdvisor *expert; // Pointer to the EA object //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { expert = new CAdvisor(); expert.Add(new CSimpleVolumesMarketStrategy( magic_, symbol_, timeframe_, fixedLot_, signalPeriod_, signalDeviation_, signaAddlDeviation_, openDistance_, stopLevel_, takeLevel_, ordersExpiration_, maxCountOfOrders_) ); // Add one strategy instance return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { expert.Tick(); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { delete expert; }
Y lo guardaremos en el archivo SimpleVolumesMarketExpertSingle.mq5 en la carpeta actual.
Ahora vamos a complicar un poco la estrategia comercial para simplificar la ejecución de la tarea. Nos resultará más fácil trasladar a la negociación virtual una estrategia que use posiciones de mercado en lugar de órdenes pendientes. La versión actual de la estrategia solo funcionará con órdenes pendientes. Asimismo, añadiremos el análisis del valor del parámetro openDistance_ a la estrategia. Si es mayor que cero, la estrategia abrirá órdenes pendientes BUY_STOP y SELL_STOP. Si es menor que cero, la estrategia abrirá órdenes pendientes BUY_LIMIT y SELL_LIMIT. Si es igual a cero, se abrirán posiciones de mercado.
Para ello, bastará con realizar cambios en el código de los métodos CSimpleVolumesMarketStrategy::OpenBuyOrder() y CSimpleVolumesMarketStrategy::OpenSellOrder().
void CSimpleVolumesMarketStrategy::OpenBuyOrder() { // Previous code in the method ... // Order volume double lot = m_fixedLot; // Set a pending order bool res = false; if(openDistance_ > 0) { res = trade.BuyStop(lot, ...); } else if(openDistance_ < 0) { res = trade.BuyLimit(lot, ...); } else { res = trade.Buy(lot, ...); } if(!res) { Print("Error opening order"); } }
Otro cambio necesario que deberemos hacer en la estrategia es el traslado del código de inicialización del método Init() al constructor de la estrategia. Esto será necesario porque ahora el asesor no llamará al método de inicialización de estrategias, asumiendo que su código se encuentra dentro del constructor de estrategias.
Ahora compilaremos el nuevo asesor y lo pondremos a optimizar el marco temporal H1 en tres símbolos: EURGBP, GBPUSD y EURUSD.
![Fig. 1. Resultados de la prueba con los parámetros [EURGBP, H1, 17, 1.7, 0.5, 0, 16500, 100, 52000, 3, 0.01] Fig. 1. Resultados de la prueba con los parámetros [EURGBP, ]]](https://c.mql5.com/2/68/2024-02-01_17-10-04.png)
Fig. 1. Resultados de la prueba para [EURGBP, H1, 17, 1.7, 0.5, 0, 16500, 100, 52000, 3, 0.01]
Ahora seleccionaremos algunas buenas variantes de parámetros de los resultados de la optimización, por ejemplo tres variantes para cada símbolo y crearemos un segundo asesor que creará nueve ejemplares de la estrategia con los parámetros seleccionados. Para cada caso, calcularemos el tamaño óptimo de las posiciones abiertas para que la reducción de una estrategia no supere el 10%. El método de cálculo ya lo expusimos en el artículo anterior.
Para demostrar los cambios en el rendimiento del asesor, haremos posible especificar qué estrategias se incluirán. Para ello, primero pondremos todos los ejemplares de las estrategias en un array de nueve elementos. Después añadiremos el parámetro de entrada startIndex_, que determinará a partir de qué índice del array de estrategias incluiremos estas en el trabajo. El otro parámetro, totalStrategies_, determinará cuántas estrategias consecutivas del array, empezando por el índice startIndex_, se incluirán en el trabajo. Al final de la inicialización, añadiremos las estrategias correspondientes del array al objeto de asesor.
#include "Advisor.mqh" #include "SimpleVolumesMarketStrategy.mqh" input int startIndex_ = 0; // Starting index input int totalStrategies_ = 1; // Number of strategies input double depoPart_ = 1.0; // Part of the deposit for one strategy input ulong magic_ = 27182; // Magic CAdvisor *expert; // EA object //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Check if the parameters are correct if(startIndex_ < 0 || startIndex_ + totalStrategies_ > 9) { return INIT_PARAMETERS_INCORRECT; } // Create and fill the array of strategy instances CStrategy *strategies[9]; strategies[0] = new CSimpleVolumesMarketStrategy( magic_ + 0, "EURGBP", PERIOD_H1, NormalizeDouble(0.01 / 0.16 * depoPart_, 2), 13, 0.3, 1.0, 0, 10500, 465, 1000, 3); strategies[1] = new CSimpleVolumesMarketStrategy( magic_ + 1, "EURGBP", PERIOD_H1, NormalizeDouble(0.01 / 0.09 * depoPart_, 2), 17, 1.7, 0.5, 0, 16500, 220, 1000, 3); strategies[2] = new CSimpleVolumesMarketStrategy( magic_ + 2, "EURGBP", PERIOD_H1, NormalizeDouble(0.01 / 0.16 * depoPart_, 2), 51, 0.5, 1.1, 0, 19500, 370, 22000, 3); strategies[3] = new CSimpleVolumesMarketStrategy( magic_ + 3, "GBPUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.25 * depoPart_, 2), 80, 1.1, 0.2, 0, 6000, 1190, 1000, 3); strategies[4] = new CSimpleVolumesMarketStrategy( magic_ + 4, "GBPUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.09 * depoPart_, 2), 128, 2.0, 0.9, 0, 2000, 1170, 1000, 3); strategies[5] = new CSimpleVolumesMarketStrategy( magic_ + 5, "GBPUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.14 * depoPart_, 2), 13, 1.5, 0.8, 0, 2500, 1375, 1000, 3); strategies[6] = new CSimpleVolumesMarketStrategy( magic_ + 6, "EURUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.23 * depoPart_, 2), 24, 0.1, 0.3, 0, 7500, 2400, 24000, 3); strategies[7] = new CSimpleVolumesMarketStrategy( magic_ + 7, "EURUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.20 * depoPart_, 2), 18, 0.2, 0.4, 0, 19500, 1480, 6000, 3); strategies[8] = new CSimpleVolumesMarketStrategy( magic_ + 8, "EURUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.22 * depoPart_, 2), 128, 0.7, 0.3, 0, 3000, 170, 42000, 3); expert = new CAdvisor(); // Add the necessary strategies to the EA for(int i = startIndex_; i < startIndex_ + totalStrategies_; i++) { expert.Add(strategies[i]); } return(INIT_SUCCEEDED); } void OnTick() { expert.Tick(); } void OnDeinit(const int reason) { delete expert; }
Debido a esto, podremos utilizar la optimización según el parámetro del índice inicial de las estrategias en el array de estrategias para obtener los resultados para cada ejemplar de la estrategia. La ejecutaremos con un depósito inicial de $100 000 y obtendremos estos resultados.

Fig. 2. Resultados de los inicios individuales de los nueve ejemplares de la estrategia
De estos resultados se desprende que la reducción es de aproximadamente el 1% del depósito inicial, es decir, unos $1000, tal y como habíamos previsto al elegir el tamaño óptimo de las posiciones abiertas. El ratio de Sharpe es de 1,3 por término medio.
Ahora, vamos a incluir todos los ejemplares y a seleccionar el multiplicador depoPart_ correspondiente para mantener la reducción de $1000. Veremos que con depoPart_ = 0,38, la reducción se mantiene dentro del rango aceptable.


Fig. 3. Resultados del funcionamiento simultáneo de nueve ejemplares de estrategias
Comparando los resultados de las ejemplares de una sola estrategia y los resultados de la operación simultánea de todas las ejemplares, podremos ver que para las misma reducción hemos obtenido un aumento del beneficio de unas 3 veces, así como un aumento del ratio de Sharpe de 1,3 a 2,84.
Ahora pasaremos a la realización de la tarea principal.
Clase de posiciones (órdenes) virtuales
Así pues, vamos a crear la prometida clase CVirtualOrder y a añadirle los campos necesarios para almacenar todas las propiedades de las posiciones abiertas.
class CVirtualOrder { private: //--- Order (position) properties ulong m_id; // Unique ID string m_symbol; // Symbol double m_lot; // Volume ENUM_ORDER_TYPE m_type; // Type double m_openPrice; // Open price double m_stopLoss; // StopLoss level double m_takeProfit; // TakeProfit level string m_comment; // Comment datetime m_openTime; // Open time //--- Closed order (position) properties double m_closePrice; // Close price datetime m_closeTime; // Close time string m_closeReason; // Closure reason double m_point; // Point value bool m_isStopLoss; // StopLoss activation property bool m_isTakeProfit;// TakeProfit activation property };
Cada posición virtual deberá tener un identificador único. Por lo tanto, añadiremos una variable estática de la clase s_count para calcular el número de todos los objetos de posición creados en el programa. Al crear un nuevo objeto de posición, este contador se incrementará en 1 y este valor se convertirá en el número único de la posición. Estableceremos un valor inicial de s_count igual a 0.
Además, podremos utilizar un objeto de la clase CSymbolInfo para obtener información sobre los precios. También lo haremos miembro estático de la clase.
class CVirtualOrder { private: static int s_count; static CSymbolInfo s_symbolInfo; //--- Order (position) properties ... }; int CVirtualOrder::s_count = 0; CSymbolInfo CVirtualOrder::s_symbolInfo;
Cabe señalar que la creación de un objeto de posición virtual y la "apertura" de una posición virtual serán operaciones diferentes. Podemos crear un objeto de posición de antemano y esperar el momento en que la estrategia quiera abrir una posición virtual. En este punto, las propiedades de la posición se rellenarán con los valores reales del símbolo, el volumen, el precio de apertura y otros. Cuando la estrategia decida cerrar la posición, en el objeto se almacenarán los valores de las propiedades de cierre: el precio, la hora y el motivo del cierre. La próxima vez que se abra una posición virtual, podremos utilizar el mismo ejemplar del objeto limpiando sus propiedades de cierre y rellenándolo de nuevo con nuevos valores de símbolo, volumen, precio de apertura, etc.
Vamos a añadir los métodos a esta clase. Necesitaremos métodos públicos para abrir y cerrar una posición virtual, así como un constructor. También resultarán útiles varios métodos que comprueban el estado de la posición (¿está abierta, en qué dirección?) y sus propiedades más importantes: el volumen y el beneficio actual.
class CVirtualOrder { //--- Previous code... public: CVirtualOrder(); // Constructor //--- Methods for checking the order (position) status bool IsOpen(); // Is the order open? bool IsMarketOrder(); // Is this a market position? bool IsBuyOrder(); // Is this an open BUY position? bool IsSellOrder(); // Is this an open SELL position? //--- Methods for obtaining order (position) properties double Volume(); // Volume with direction double Profit(); // Current profit //--- Methods for handling orders (positions) bool Open(string symbol, ENUM_ORDER_TYPE type, double lot, double sl = 0, double tp = 0, string comment = "", bool inPoints = true); // Opening an order (position) bool Close(); // Closing an order (position) };
La implementación de algunos de estos métodos será muy simple y breve, por lo que podremos ponerla dentro de la descripción de la clase, por ejemplo:
class CVirtualOrder : public CObject { // ... //--- Methods for checking the order (position) status bool IsOpen() { // Is the order open? return(this.m_openTime > 0 && this.m_closeTime == 0); }; bool IsMarketOrder() { // Is this a market position? return IsOpen() && (m_type == ORDER_TYPE_BUY || m_type == ORDER_TYPE_SELL); } // ... };
El constructor a través de la lista de inicialización asignará valores vacíos (en el sentido de que son obviamente inválidos) a todas las propiedades de la posición virtual excepto a una: el identificador único. En el constructor, como ya hemos comentado, se le asignará un valor derivado del valor del contador de objetos de esta clase creados anteriormente, y así permanecerá durante todo el trabajo del asesor. Antes de la asignación, incrementaremos el contador de objetos creados.
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualOrder::CVirtualOrder() : // Initialization list m_id(++s_count), // New ID = object counter + 1 m_symbol(""), m_lot(0), m_type(-1), m_openPrice(0), m_stopLoss(0), m_takeProfit(0), m_openTime(0), m_comment(""), m_closePrice(0), m_closeTime(0), m_closeReason(""), m_point(0) { }
Antes de continuar la implementación de los métodos de la clase CVirtualOrder, le propongo echar un vistazo un poco más adelante y pensar en cómo utilizarla. Tenemos objetos estratégicos que ahora abren posiciones reales en el mercado. Además, conocemos el número máximo de posiciones de mercado abiertas (establecido en los parámetros de la estrategia). Queremos pasar a las posiciones virtuales. Entonces su número también será limitado. Por lo tanto, podemos crear un array de objetos de posiciones virtuales en la estrategia, llenarlo con el número requerido de posiciones virtuales al inicializar la estrategia y luego trabajar solo con este array.
Cuando se den las condiciones para abrir una nueva posición, tomaremos del array una posición virtual que aún no se haya abierto y la convertiremos en una posición abierta. Si se dan las condiciones de cierre forzoso de posiciones, la convertiremos en una posición cerrada.
Mientras haya posiciones virtuales abiertas, cualquier estrategia deberá procesar estos objetos de la misma forma en cada tick, es decir, deberá iterarlos uno a uno, comprobando si se ha alcanzado el nivel de StopLoss o TakeProfit y, en caso afirmativo, cerrando dicha posición. Esta igualdad permite trasladar a nuestra propia clase la implementación del procesamiento de las posiciones de comportamiento virtual abiertas, y solo llamar al método correspondiente desde la estrategia.
Vamos a añadir a la clase CVirtualOrder el método Tick(), que comprobará las condiciones para cerrar la posición, y si se cumplen, la posición pasará al estado cerrado. Si se ha producido un cambio en el estado de la posición, el método retornará true.
También añadiremos un método estático Tick() que procesará múltiples objetos de posiciones virtuales a la vez. Tomará como parámetro una referencia a un array de dichos objetos. Para cada objeto de este array, se llamará a su método Tick(). Si al menos una posición virtual está cerrada, como resultado se retornará true.
class CVirtualOrder { private: //... public: //... //--- Methods for handling orders (positions) bool Open(string symbol, ENUM_ORDER_TYPE type, double lot, double sl = 0, double tp = 0, string comment = "", bool inPoints = false ); // Open order (position) bool Tick(); // Handle tick for an order (position) bool Close(); // Close an order (position) static bool Tick(CVirtualOrder &orders[]); // Handle a tick for the array of virtual orders }; //... //+------------------------------------------------------------------+ //| Handle a tick of a single virtual order (position) | //+------------------------------------------------------------------+ bool CVirtualOrder::Tick() { if(IsMarketOrder()) { // If this is a market virtual position if(CheckClose()) { // Check if SL or TP levels have been reached Close(); // Close when reached return true; // Return the fact that there are changes in open positions } } return false; } //+------------------------------------------------------------------+ //| Handle a tick for the array of virtual orders (positions) | //+------------------------------------------------------------------+ bool CVirtualOrder::Tick(CVirtualOrder &orders[]) { bool isChanged = false; // We assume that there will be no changes for(int i = 0; i < ArraySize(orders); i++) { // For all orders (positions) isChanged |= orders[i].Tick(); // Check and close if necessary } return isChanged; } //+------------------------------------------------------------------+
Guardaremos este código en el archivo VirtualOrder.mqh en la carpeta actual.
Mejorando la clase de estrategia comercial simple
Ahora podemos volver a la clase de estrategia comercial y modificarla para permitir posiciones virtuales. Como ya hemos acordado, en la clase básica CStrategy ya disponemos de un array m_orders[] para almacenar los objetos de posiciones virtuales. Por lo tanto, también estará disponible en la clase CSimpleVolumesStrategy. La estrategia analizada tiene un parámetro m_maxCountOfOrders que define el número máximo de posiciones abiertas simultáneamente. A continuación, estableceremos un tamaño de array de posiciones virtuales igual a este parámetro en el constructor.
Luego solo tendremos que sustituir la apertura de posiciones reales en los métodos OpenBuyOrder() y OpenSellOrder() por la apertura de posiciones virtuales. No hay nada que sustituya la apertura de órdenes pendientes reales, así que simplemente comentaremos estas operaciones.
//+------------------------------------------------------------------+ //| Open BUY order | //+------------------------------------------------------------------+ void CSimpleVolumesStrategy::OpenBuyOrder() { // ... if(m_openDistance > 0) { /* // Set BUY STOP pending order res = trade.BuyStop(lot, ...); */ } else if(m_openDistance < 0) { /* // Set BUY LIMIT pending order res = trade.BuyLimit(lot, ...); */ } else { // Open a virtual BUY position for(int i = 0; i < m_maxCountOfOrders; i++) { // Iterate through all virtual positions if(!m_orders[i].IsOpen()) { // If we find one that is not open, then open it res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY, m_fixedLot, NormalizeDouble(sl, digits), NormalizeDouble(tp, digits)); break; // and exit } } } ... } //+------------------------------------------------------------------+ //| Open SELL order | //+------------------------------------------------------------------+ void CSimpleVolumesStrategy::OpenSellOrder() { // ... if(m_openDistance > 0) { /* // Set SELL STOP pending order res = trade.SellStop(lot, ...); */ } else if(m_openDistance < 0) { /* // Set SELL LIMIT pending order res = trade.SellLimit(lot, ...); */ } else { // Open a virtual SELL position for(int i = 0; i < m_maxCountOfOrders; i++) { // Iterate through all virtual positions if(!m_orders[i].IsOpen()) { // If we find one that is not open, then open it res = m_orders[i].Open(m_symbol, ORDER_TYPE_SELL, m_fixedLot, NormalizeDouble(sl, digits), NormalizeDouble(tp, digits)); break; // and exit } } } ... }
Vamos a guardar los cambios en el archivo SimpleVolumesStrategy.mqh de la carpeta actual.
Creación de una clase para transferir los volúmenes abiertos a posiciones de mercado
Ya hemos creado una clase básica de objetos para transferir los volúmenes abiertos a posiciones de mercado, que no hace nada más que rellenar el array de estrategias utilizadas. Ahora necesitamos escribir una clase derivada que contenga una implementación concreta de las operaciones de colocación de posiciones en el mercado. Vamos a crear la clase CVolumeReceiver. En ella tendremos que añadir bastante código para implementar el método Correct(). Lo dividiremos en unos cuantos métodos de clase protegidos.
#include "Receiver.mqh" //+------------------------------------------------------------------+ //| Class for converting open volumes into market positions | //+------------------------------------------------------------------+ class CVolumeReceiver : public CReceiver { protected: bool m_isNetting; // Is this a netting account? string m_symbols[]; // Array of used symbols double m_minMargin; // Minimum margin for opening CPositionInfo m_position; CSymbolInfo m_symbolInfo; CTrade m_trade; // Filling the array of open market volumes by symbols void FillSymbolVolumes(double &oldVolumes[]); // Correction of open volumes using the array of volumes virtual bool Correct(double &symbolVolumes[]); // Volume correction for this symbol bool CorrectPosition(string symbol, double oldVolume, double diffVolume); // Auxiliary methods bool ClearOpen(string symbol, double diffVolume); bool AddBuy(string symbol, double volume); bool AddSell(string symbol, double volume); bool CloseBuyPartial(string symbol, double volume); bool CloseSellPartial(string symbol, double volume); bool CloseHedgingPartial(string symbol, double volume, ENUM_POSITION_TYPE type); bool CloseFull(string symbol = ""); bool FreeMarginCheck(string symbol, double volume, ENUM_ORDER_TYPE type); public: CVolumeReceiver(ulong p_magic, double p_minMargin = 100); // Constructor virtual void Add(CStrategy *strategy) override; // Add strategy virtual bool Correct() override; // Adjustment of open volumes };
El algoritmo general del método de corrección del volumen abierto será el siguiente:
- Para cada símbolo usado, iteraremos todas las estrategias y calcularemos el volumen abierto total para cada símbolo utilizado. Como resultado, obtendremos el array newVolumes, que pasaremos al siguiente método sobrecargado Correct()
//+------------------------------------------------------------------+ //| Adjustment of open volumes | //+------------------------------------------------------------------+ bool CVolumeReceiver::Correct() { int symbolsTotal = ArraySize(m_symbols); double newVolumes[]; ArrayResize(newVolumes, symbolsTotal); ArrayInitialize(newVolumes, 0); for(int j = 0; j < symbolsTotal; j++) { // For each used symbol for(int i = 0; i < ArraySize(m_strategies); i++) { // Iterate through all strategies if(m_strategies[i].Symbol() == m_symbols[j]) { // If the strategy uses this symbol newVolumes[j] += m_strategies[i].Volume(); // Add its open volume } } } // Call correction of open volumes using the array of volumes return Correct(newVolumes); }
- Para cada símbolo, calcularemos en cuánto tenemos que cambiar el volumen de las posiciones abiertas del símbolo. Si es necesario, llamaremos al método de corrección del volumen de este símbolo
//+------------------------------------------------------------------+ //| Adjusting open volumes using the array of volumes | //+------------------------------------------------------------------+ bool CVolumeReceiver::Correct(double &newVolumes[]) { // ... bool res = true; // For each symbol for(int j = 0; j < ArraySize(m_symbols); j++) { // ... // Define how much the volume of open positions for the symbol should be changed double oldVolume = oldVolumes[j]; double newVolume = newVolumes[j]; // ... double diffVolume = newVolume - oldVolume; // If there is a need to adjust the volume for a given symbol, then do that if(MathAbs(diffVolume) > 0.001) { res = res && CorrectPosition(m_symbols[j], oldVolume, diffVolume); } } return res; }
- Para el símbolo, según los valores del volumen de apertura anterior y el cambio necesario, determinaremos qué tipo de operación necesitamos realizar (añadir, cerrar, cerrar y reabrir) y llamaremos al método auxiliar correspondiente:
//+------------------------------------------------------------------+ //| Adjust volume by the symbol | //+------------------------------------------------------------------+ bool CVolumeReceiver::CorrectPosition(string symbol, double oldVolume, double diffVolume) { bool res = false; // ... double volume = MathAbs(diffVolume); if(oldVolume > 0) { // Have BUY position if(diffVolume > 0) { // New BUY position res = AddBuy(symbol, volume); } else if(diffVolume < 0) { // New SELL position if(volume < oldVolume) { res = CloseBuyPartial(symbol, volume); } else { res = CloseFull(symbol); if(res && volume > oldVolume) { res = AddSell(symbol, volume - oldVolume); } } } } else if(oldVolume < 0) { // Have SELL position if(diffVolume < 0) { // New SELL position res = AddSell(symbol, volume); } else if(diffVolume > 0) { // New BUY position if(volume < -oldVolume) { res = CloseSellPartial(symbol, volume); } else { res = CloseFull(symbol); if(res && volume > -oldVolume) { res = AddBuy(symbol, volume + oldVolume); } } } } else { // No old position res = ClearOpen(symbol, diffVolume); } return res; }
Guardaremos este código en el archivo VolumeReceiver.mqh en la carpeta actual.
Asesor con una estrategia y posiciones virtuales
Basándonos en el archivo SimpleVolumesMarketExpertSingle.mq5, crearemos un asesor que utilizará un ejemplar de la estrategia comercial con posiciones virtuales. Tendremos que conectar los archivos necesarios, transmitir al constructor del asesor un nuevo objeto de la clase CVolumeReceiver cuando el constructor sea llamado, y reemplazar la clase de la estrategia creada.
#include "Advisor.mqh" #include "SimpleVolumesStrategy.mqh" #include "VolumeReceiver.mqh" // Input parameters... CAdvisor *expert; // Pointer to the EA object //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { expert = new CAdvisor(new CVolumeReceiver(magic_)); expert.Add(new CSimpleVolumesStrategy( symbol_, timeframe_, fixedLot_, signalPeriod_, signalDeviation_, signaAddlDeviation_, openDistance_, stopLevel_, takeLevel_, ordersExpiration_, maxCountOfOrders_) ); // Add one strategy instance return(INIT_SUCCEEDED); } void OnTick() { expert.Tick(); } void OnDeinit(const int reason) { delete expert; }
Guardaremos este código en el archivo SimpleVolumesExpertSingle.mq5 en la carpeta actual.
Comparación entre comercio real y virtual
Ahora ejecutaremos en un pequeño intervalo temporal asesores con la misma estrategia, utilizando los mismos parámetros de estrategia, pero diferentes formas de apertura de posiciones reales: directa y a través de posiciones virtuales. Vamos a guardar estos resultados en informes y a echar un vistazo a la lista de transacciones completadas por ambos asesores expertos.

Fig. 4. Transacciones ejecutadas por dos asesores expertos (con posiciones virtuales y sin ellas)
Para reducir la anchura, hemos eliminado de las tablas las columnas con los mismos valores en todas las filas, como el símbolo (siempre EURGBP), el volumen (siempre 0,01) y otras. Como podemos observar, la apertura de las primeras posiciones se produce en ambos casos al mismo precio y en los mismos momentos. Pero si con una posición SELL abierta existente (2018.03.02 15:46:47 sell in), se abre otra posición opuesta BUY (2018.03.06 13:56:04 buy in), el asesor que trabaja con posiciones virtuales simplemente cerrará la posición SELL abierta anteriormente (2018.03.06 13:56:04 buy out). El resultado global de esto solo ha mejorado, ya que el primer asesor ha seguido pagando swaps por posiciones multidireccionales abiertas, mientras que el segundo asesor no lo ha hecho.
Asesor con múltiples estrategias y posiciones virtuales
Realizaremos manipulaciones similares con el asesor del archivo SimpleVolumesMarketExpert.mq5: primero conectaremos los archivos necesarios, luego, al llamar al constructor del asesor, le transmitiremos un nuevo objeto de la clase CVolumeReceiver y sustituiremos la clase de las estrategias creadas. Después guardaremos el resultado en el archivo SimpleVolumesExpert.mq5 y veremos los resultados.


Fig. 5. Resultados del asesor con nueve ejemplares de la estrategia y posiciones virtuales
Comparando estos resultados con los de un asesor similar, pero sin utilizar posiciones virtuales, podemos observar la mejora de algunos indicadores: el beneficio ha aumentado ligeramente y la reducción ha disminuido; el ratio de Sharpe y el factor de beneficio, a su vez, han aumentado.
Conclusión
Hoy hemos dado un paso más hacia la consecución de nuestro objetivo. Completando la transición al uso de posiciones virtuales por parte de las estrategias, hemos aumentado la capacidad de un gran número de estrategias comerciales de trabajar juntas sin interferir unas con otras. Esto también nos permitirá utilizar un depósito mínimo más pequeño para el comercio, si lo comparamos con el uso del comercio independiente de cada ejemplar de la estrategia. Y otra agradable ventaja será la posibilidad de trabajar también con cuentas de compensación.
Pero aquí deberemos igualmente dar muchos pasos más. Por ejemplo, hasta ahora solo hemos aplicado estrategias que abren posiciones de mercado, pero no órdenes pendientes. Asimismo, hemos dejado para el futuro las cuestiones relacionadas con la gestión de capital, por ahora trabajaremos con un volumen fijo y seleccionaremos manualmente el tamaño óptimo de la posición. Las estrategias que deban trabajar con varios símbolos a la vez, es decir, las que no puedan dividirse en estrategias más sencillas de un solo símbolo, no podrán utilizar este esquema.
En cualquier caso, continuaremos trabajando.
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/14107
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Previsión y apertura de órdenes basadas en aprendizaje profundo (Deep Learning) con el paquete Python MetaTrader 5 y el archivo modelo ONNX
Desarrollo de un sistema de repetición (Parte 52): Esto complica las cosas (IV)
Usamos algoritmos de optimización para ajustar los parámetros del asesor sobre la marcha
Desarrollo de un sistema de repetición (Parte 51): Esto complica las cosas (III)
- 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
Tienes razón. Lo hice de esta manera. Esta es una variable-cadena global en la que todas las variables de entrada son automáticamente (y creadas). Es decir, no importa qué objetos se crean, esta variable es siempre de entrada.
Por si acaso te recuerdo que las entradas de cadena son recortadas en 63 caracteres por el optimizador.
Por si acaso, le recuerdo que las entradas de cadenas son recortadas en 63 caracteres por el optimizador.
Gracias. No es una entrada, por lo que la longitud no está limitada.
Foro sobre negociación, sistemas automatizados de negociación y ensayo de estrategias de negociación
Discusión del artículo "Desarrollo de un Asesor Experto multidivisa (Parte 2): Pasando a estrategias de negociación de posiciones virtuales"
fxsaber, 2024.02.14 11:36 AM
Tienes razón. Lo hice de esta manera. Se trata de una variable global de tipo cadena en la que se introducen (y crean) automáticamente todas las variables de entrada. Es decir, cualquier objeto que no se cree, esta variable siempre se introduce en la entrada.Adjunto.
Hola,
im en una curva de aprendizaje empinada de OOP. Este seguimiento del artículo anterior ha sido muy útil.
Todavía trabajando a través de él. Thank you.