English Русский Deutsch 日本語 Português
preview
Desarrollamos un Asesor Experto multidivisas (Parte 4): Órdenes pendientes virtuales y guardado del estado

Desarrollamos un Asesor Experto multidivisas (Parte 4): Órdenes pendientes virtuales y guardado del estado

MetaTrader 5Sistemas comerciales | 22 agosto 2024, 11:56
431 0
Yuriy Bykov
Yuriy Bykov

Introducción

En el artículo anterior, hemos revisado significativamente la arquitectura del código para construir un EA multidivisa con varias estrategias de trabajo en paralelo. Tratando de lograr simplicidad y claridad, hasta ahora sólo hemos considerado un cierto conjunto mínimo de funcionalidades. Incluso teniendo en cuenta las limitaciones de nuestra tarea, hemos alterado significativamente el código de los anteriores artículos.

Ahora esperamos tener la base suficiente para aumentar la funcionalidad sin cambios radicales en el código ya escrito. Intentaremos hacer un número mínimo de ediciones sólo cuando sea realmente necesario.

Como desarrollo posterior, intentaremos hacer lo siguiente:

  • Añadir la posibilidad de abrir órdenes pendientes virtuales (Buy Stop, Sell Stop, Buy Limit, Sell Limit), y no sólo posiciones virtuales (Buy, Sell).
  • Añadir una forma sencilla de visualizar las órdenes y posiciones virtuales colocadas, de forma que podamos controlar visualmente al probar la correcta implementación de las reglas de apertura de posiciones/órdenes en las estrategias de negociación utilizadas.
  • Implementar el almacenamiento de los datos del estado actual por parte del EA, de forma que cuando se reinicie el terminal o se traslade el EA a otro terminal, pueda seguir trabajando desde el estado en el que se encontraba en el momento en que se interrumpió su trabajo.

Empecemos por lo más sencillo: la gestión de las órdenes pendientes virtuales.


Órdenes pendientes virtuales

Hemos creado la clase CVirtualOrder para manejar las posiciones virtuales. Podemos crear una clase similar separada para manejar las órdenes pendientes virtuales. Veamos si la gestión de posiciones es realmente tan diferente de la gestión de órdenes.

Su conjunto de propiedades coincide en gran medida. Sin embargo, las órdenes pendientes tienen una propiedad añadida que almacena el tiempo de expiración. Por ello, añaden un nuevo motivo de cierre: al llegar a la hora de vencimiento. En consecuencia, tendremos que comprobar en cada tick si la orden pendiente virtual abierta se ha cerrado por este motivo, y cuando se alcancen los niveles Stop Loss y Take Profit, no deberá cerrarse.

Cuando el precio alcanza el nivel de activación de una orden pendiente virtual, debe convertirse en una posición virtual abierta. Aquí ya queda claro que, al implementar esto en forma de diferentes clases, tendremos que realizar trabajo adicional para crear una posición virtual abierta y eliminar una orden pendiente virtual activada. Si los implementamos como una sola clase, sólo tendremos que cambiar una propiedad del objeto de clase: su tipo. Por lo tanto, aplicaremos exactamente esta opción.

¿En qué se diferencia una orden de una posición? También tendremos que indicar el precio de apertura de una orden pendiente virtual. Para una posición virtual, no era necesario especificar el precio de apertura, ya que se determinaba automáticamente a partir de los datos actuales del mercado. Ahora lo convertiremos en un parámetro obligatorio de la función de apertura.

Vamos a empezar a añadir a la clase. Añadiremos:

  • La propiedad m_expiration para almacenar la hora de caducidad.
  • La propiedad lógica m_isExpired para indicar la caducidad de la orden pendiente.
  • El método CheckTrigger() para comprobar si se activa una orden pendiente, varios métodos para comprobar si un objeto dado pertenece a un tipo determinado (orden pendiente, orden pendiente limitada, etc.).
  • Condición que establece que se devuelva 'true' en los casos en que se trate de una orden pendiente en la dirección deseada, ya sea una orden límite o una orden stop, para los métodos que verifican si un objeto dado está en la dirección de COMPRA o VENTA. 
  • La lista de parámetros del método Open() recibe el precio de apertura y el tiempo de expiración.

//+------------------------------------------------------------------+
//| Class of virtual orders and positions                            |
//+------------------------------------------------------------------+
class CVirtualOrder {
private:
   ...
//--- Order (position) properties
   ...
   datetime          m_expiration;     // Expiration time
   ...
   bool              m_isExpired;      // Expiration flag
   ...
//--- Private methods
   ...
   bool              CheckTrigger();   // Check for pending order trigger

public:
    ...

//--- Methods for checking the position (order) status
   ...
   bool              IsPendingOrder() {// Is it a pending order?
      return IsOpen() && (m_type == ORDER_TYPE_BUY_LIMIT
                          || m_type == ORDER_TYPE_BUY_STOP
                          || m_type == ORDER_TYPE_SELL_LIMIT
                          || m_type == ORDER_TYPE_SELL_STOP);
   }
   bool              IsBuyOrder() {    // Is it an open BUY position?
      return IsOpen() && (m_type == ORDER_TYPE_BUY
                          || m_type == ORDER_TYPE_BUY_LIMIT
                          || m_type == ORDER_TYPE_BUY_STOP);
   }
   bool              IsSellOrder() {   // Is it an open SELL position?
      return IsOpen() && (m_type == ORDER_TYPE_SELL 
                          || m_type == ORDER_TYPE_SELL_LIMIT
                          || m_type == ORDER_TYPE_SELL_STOP);
   }
   bool              IsStopOrder() {   // Is it a pending STOP order?
      return IsOpen() && (m_type == ORDER_TYPE_BUY_STOP || m_type == ORDER_TYPE_SELL_STOP);
   }
   bool              IsLimitOrder() {  // is it a pending LIMIT order?
      return IsOpen() && (m_type == ORDER_TYPE_BUY_LIMIT || m_type == ORDER_TYPE_SELL_LIMIT);
   }
   ...

//--- Methods for handling positions (orders)
   bool              CVirtualOrder::Open(string symbol,
                                         ENUM_ORDER_TYPE type,
                                         double lot,
                                         double price,
                                         double sl = 0,
                                         double tp = 0,
                                         string comment = "",
                                         datetime expiration = 0,
                                         bool inPoints = false); // Opening a position (order)

   ...
};

En el método Open() de apertura de una posición virtual, que ahora también abrirá órdenes pendientes virtuales, añade la asignación del precio de apertura a la propiedad m_openPrice. Si resulta que no se trata de una orden pendiente, sino de una posición, añada la asignación del precio actual de apertura del mercado a la propiedad:

bool CVirtualOrder::Open(string symbol,         // Symbol
                         ENUM_ORDER_TYPE type,  // Type (BUY or SELL)
                         double lot,            // Volume
                         double price = 0,      // Open price
                         double sl = 0,         // StopLoss level (price or points)
                         double tp = 0,         // TakeProfit level (price or points)
                         string comment = "",   // Comment
                         datetime expiration = 0,  // Expiration time
                         bool inPoints = false  // Are the SL and TP levels set in points?
                        ) {
   ...

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

      // Initialize position properties
      m_openPrice = price;
     ...
      m_expiration = expiration;

      // The position (order) being opened is not closed by SL, TP or expiration
      ...
      m_isExpired = false;

      ...
      // Depending on the direction, set the opening price, as well as the SL and TP levels.
      // If SL and TP are specified in points, then we first calculate their price levels
      // relative to the open price
      if(IsBuyOrder()) {
         if(type == ORDER_TYPE_BUY) {
            m_openPrice = s_symbolInfo.Ask();
         }
         ...
      } else if(IsSellOrder()) {
         if(type == ORDER_TYPE_SELL) {
            m_openPrice = s_symbolInfo.Bid();
         }
         ...
      }

      ...

      return true;
   }
   return false;
}

En el método CheckTrigger() de comprobación de activación de una orden pendiente virtual, obtenemos el precio de mercado actual Bid o Ask dependiendo de la dirección de la orden y comprobamos si ha alcanzado el precio de apertura en el lado deseado. En caso afirmativo, sustituye la propiedad m_type del objeto actual por el valor correspondiente a la posición de la dirección deseada, y notifica a los objetos receptor y estrategia que se ha abierto una nueva posición virtual.

//+------------------------------------------------------------------+
//| Check whether a pending order is triggered                       |
//+------------------------------------------------------------------+
bool CVirtualOrder::CheckTrigger() {
   if(IsPendingOrder()) {
      s_symbolInfo.Name(m_symbol);     // Select the desired symbol
      s_symbolInfo.RefreshRates();     // Update information about current prices
      double price = (IsBuyOrder()) ? s_symbolInfo.Ask() : s_symbolInfo.Bid();
      int spread = s_symbolInfo.Spread();

      // If the price has reached the opening levels, turn the order into a position
      if(false
            || (m_type == ORDER_TYPE_BUY_LIMIT && price <= m_openPrice)
            || (m_type == ORDER_TYPE_BUY_STOP  && price >= m_openPrice)
        ) {
         m_type = ORDER_TYPE_BUY;
      } else if(false
                || (m_type == ORDER_TYPE_SELL_LIMIT && price >= m_openPrice)
                || (m_type == ORDER_TYPE_SELL_STOP  && price <= m_openPrice)
               ) {
         m_type = ORDER_TYPE_SELL;
      }

      // If the order turned into a position 
      if(IsMarketOrder()) {
         m_openPrice = price; // Remember the open price

         // Notify the recipient and the strategy of the position opening 
         m_receiver.OnOpen(GetPointer(this));
         m_strategy.OnOpen();
         return true;
      }
   }
   return false;
}

Este método será llamado cuando se maneje un nuevo tick en el método Tick() en el caso de que esta sea realmente una orden pendiente virtual:

//+------------------------------------------------------------------+
//| Handle a tick of a single virtual order (position)               |
//+------------------------------------------------------------------+
void CVirtualOrder::Tick() {
   if(IsOpen()) {  // If this is an open virtual position or order
      if(CheckClose()) {  // Check if SL or TP levels have been reached
         Close();         // Close when reached
      } else if (IsPendingOrder()) {   // If this is a pending order
         CheckTrigger();  // Check if it is triggered
      }
   }
}

El método Tick(), llama al método CheckClose(), donde también tenemos que añadir el código que comprueba el cierre de una orden pendiente virtual en función del tiempo de vencimiento:

//+------------------------------------------------------------------+
//| Check the need to close by SL, TP or EX                          |
//+------------------------------------------------------------------+
bool CVirtualOrder::CheckClose() {
   if(IsMarketOrder()) {               // If this is a market virtual position,
      ...
      // Check that the price has reached SL or TP
      ...
   } else if(IsPendingOrder()) {    // If this is a pending order
      // Check if the expiration time has been reached, if one is specified 
      if(m_expiration > 0 && m_expiration < TimeCurrent()) {
         m_isExpired = true;
         return true;
      }
   }
   return false;
}

Guarde los cambios en el archivo VirtualOrder.mqh de la carpeta actual.

Ahora volvamos a nuestra clase de estrategia de negociación CSimpleVolumesStrategy. En esa estrategia, dejamos el margen para futuras ediciones para poder añadir soporte para el manejo de órdenes pendientes virtuales. Había lugares así en los métodos OpenBuyOrder() y OpenSellOrder(). Añadamos aquí la llamada al método Open() con parámetros que conduzcan a la apertura de órdenes pendientes virtuales. Primero calcularemos el precio de apertura a partir del precio actual, retrocediendo desde él en la dirección deseada el número de puntos especificado por el parámetro m_openDistance. Presentamos el código sólo para el método OpenBuyOrder(). En otro código, las ediciones serán similares.

//+------------------------------------------------------------------+
//| Open BUY order                                                   |
//+------------------------------------------------------------------+
void CSimpleVolumesStrategy::OpenBuyOrder() {
// Update symbol current price data
   ...
// Retrieve the necessary symbol and price data
   ...

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

// Opening price
   double price = ask + distance * point;

// StopLoss and TakeProfit levels
   ...

// Expiration time
   datetime expiration = TimeCurrent() + m_ordersExpiration * 60;

   ...
   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
         if(m_openDistance > 0) {
            // Set SELL STOP pending order
            res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY_STOP, m_fixedLot,
                                   NormalizeDouble(price, digits),
                                   NormalizeDouble(sl, digits),
                                   NormalizeDouble(tp, digits),
                                   "", expiration);

         } else if(m_openDistance < 0) {
            // Set SELL LIMIT pending order
            res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY_LIMIT, m_fixedLot,
                                   NormalizeDouble(price, digits),
                                   NormalizeDouble(sl, digits),
                                   NormalizeDouble(tp, digits),
                                   "", expiration);

         } else {
            // Open a virtual SELL position
            res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY, m_fixedLot,
                                   0,
                                   NormalizeDouble(sl, digits),
                                   NormalizeDouble(tp, digits));

         }
         break; // and exit
      }
   }
  ...
}

Guarde los cambios en el archivo SimpleVolumesStrategy.mqh de la carpeta actual.

Esto completa los cambios necesarios para soportar el funcionamiento de estrategias con órdenes pendientes virtuales. Hemos hecho cambios en sólo dos archivos y ahora podemos compilar el SimpleVolumesExpertSingle.mq5 EA. Al establecer el parámetro openDistance_ distinto de cero, el EA debe abrir órdenes pendientes virtuales en lugar de posiciones virtuales. Sin embargo, no veremos la apertura en el gráfico. Sólo podemos ver los mensajes correspondientes en el registro. Podremos verlas en el gráfico sólo después de que se hayan convertido en una posición virtual abierta, que será llevada al mercado por el objeto de los receptores de volúmenes de negociación virtuales.

Sería bueno ver de alguna manera las órdenes pendientes virtuales colocadas en el gráfico. Volveremos sobre este tema un poco más adelante, pero ahora pasemos a un asunto más importante: asegurarnos de que el estado del EA se guarda y se carga después de un reinicio.


Guardar estado

Creamos dos EAs basados en las clases desarrolladas. El primero (SimpleVolumesExpertSingle.mq5) estaba destinado a optimizar los parámetros de una única instancia de una estrategia de trading, y el segundo (SimpleVolumesExpert.mq5) ya incluía varias copias de la estrategia de trading con los mejores parámetros seleccionados mediante el primer EA. En el futuro, sólo el segundo EA tiene la perspectiva de ser utilizado en cuentas reales, y el primero está destinado a ser utilizado sólo en el probador de estrategias. Por lo tanto, sólo tendremos que cargar y guardar el estado en el segundo EA u otros, que también incluirá muchas instancias de estrategias de negociación.

A continuación, hay que aclarar que ahora estamos hablando de guardar y cargar el estado del EA, y no los diferentes conjuntos de estrategias que se deben utilizar en el EA. En otras palabras, el conjunto de instancias de estrategia con parámetros especificados en el EA es fijo y es el mismo cada vez que se lanza. Después del primer lanzamiento, el EA abre posiciones virtuales y reales, y quizás calcula algunos indicadores basados en datos de precios. Es esta información la que conforma el estado de la EA en su conjunto. Si ahora reiniciamos el terminal, el EA debería reconocer las posiciones abiertas como propias, así como restaurar todas sus posiciones virtuales y los valores calculados necesarios. Mientras que la información sobre las posiciones abiertas se puede obtener del terminal, el EA debe guardar la información sobre las posiciones virtuales y los valores calculados de forma independiente.

Para la estrategia de negociación simple que hemos considerado, no hay necesidad de acumular ningún dato calculado, por lo que el estado del EA estará totalmente determinado sólo por un conjunto de posiciones virtuales y órdenes pendientes. Sin embargo, guardar sólo la matriz de todos los objetos de clase CVirtualOrder en el archivo es insuficiente.

Imaginemos que ya tenemos varios EAs con diferentes conjuntos de estrategias de negociación. Pero el número total de objetos de clase CVirtualOrder creados por cada EA resultó ser el mismo. Por ejemplo, en cada uno utilizamos 9 instancias de estrategias de negociación que solicitaban 3 objetos de posición virtuales. Entonces cada EA almacenará información sobre 27 objetos de la clase CVirtualOrder. En este caso, necesitamos asegurarnos de alguna manera contra el hecho de que uno de los EAs cargue información no sobre sus 27 posiciones virtuales, sino sobre otras. 

Para ello, podemos añadir al archivo guardado información sobre los parámetros de las estrategias que forman parte de este EA y, posiblemente, información sobre los parámetros del propio EA.

Consideremos ahora en qué momentos debe guardarse el estado. Si los parámetros de la estrategia son fijos, entonces son los mismos en cualquier momento. Al mismo tiempo, los objetos de las posiciones virtuales pueden cambiar los valores de sus propiedades durante las operaciones de apertura y cierre. Esto significa que tiene sentido guardar el estado después de estas operaciones. Guardar con más frecuencia (por ejemplo, en cada tick o mediante un temporizador) parece redundante por ahora.

Procedamos a la aplicación. Dado que tenemos una estructura jerárquica de objetos como:

  • EA
    • Estrategias
      • Posiciones virtuales

Delegar la tarea de guardado en cada nivel, en lugar de centralizar todo en el nivel superior, aunque esto sea posible.

En cada nivel, añade dos métodos a las clases Save() y Load() responsables de guardar y cargar, respectivamente. En el nivel superior, estos métodos abrirán un archivo y, en los niveles inferiores, obtendrán como parámetro un descriptor de un archivo ya abierto. Por lo tanto, dejaremos la cuestión de elegir un nombre de archivo para guardar sólo a nivel del EA. Esta cuestión no se planteará en los niveles de estrategia y posiciones virtuales.


Modificación del EA

La clase CVirtualAdvisor obtiene el campo m_name para almacenar el nombre del EA. Como no cambiará durante el funcionamiento, lo asignaremos en el constructor. También tiene sentido ampliar inmediatamente el nombre añadiéndole el número mágico y el sufijo opcional «.test» en caso de que el EA se lance en modo de prueba visual.

Para implementar el guardado sólo en caso de cambio en la composición de posiciones virtuales, añada el campo m_lastSaveTime, que almacenará la hora del último guardado. 

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   ...
   
   string            m_name;           // EA name
   datetime          m_lastSaveTime;   // Last save time

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

   virtual bool      Save();           // Save status
   virtual bool      Load();           // Load status
};

Al crear un Asesor Experto, asignaremos valores iniciales a dos nuevas propiedades de la siguiente manera:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(ulong p_magic = 1, string p_name = "") :
   ...
   m_lastSaveTime(0) {
   m_name = StringFormat("%s-%d%s.csv",
                         (p_name != "" ? p_name : "Expert"),
                         p_magic,
                         (MQLInfoInteger(MQL_TESTER) ? ".test" : "")
                        );
};

Coloca la lógica para comprobar si es necesario guardar dentro del método Save(). Podemos simplemente añadir la llamada de este método en cada tick después de realizar el resto de acciones sobre él:

//+------------------------------------------------------------------+
//| 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();

// Save status
   Save();
}

En el método guardar, lo primero que tenemos que hacer es comprobar si necesitamos ejecutarlo. Para ello, tendremos que acordar de antemano que añadiremos una nueva propiedad al objeto receptor, que almacenará la hora de los últimos cambios en las posiciones virtuales abiertas o la hora de la última corrección de los volúmenes reales abiertos. Si la hora del último guardado es menor que la hora de la última corrección, es que se han producido cambios y hay que guardarlos.

Además, no guardaremos los cambios si se está realizando una optimización o una prueba única sin utilizar el modo visual. Si la prueba se está realizando en modo visual, se guardará. Esto nos permitirá comprobar también el ahorro en el comprobador de estrategias.

Esto es lo que Save() podría parecer a nivel del EA: comprobamos la necesidad de guardar, luego guardamos el tiempo actual y el número de estrategias. Después, llamamos al método de guardado de todas las estrategias en un bucle.

//+------------------------------------------------------------------+
//| Save status                                                      |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::Save() {
   bool res = true;

   // Save status if:
   if(true
         // later changes appeared
         && m_lastSaveTime < CVirtualReceiver::s_lastChangeTime
         // currently, there is no optimization
         && !MQLInfoInteger(MQL_OPTIMIZATION)
         // and there is no testing at the moment or there is a visual test at the moment
         && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE))
     ) {
      int f = FileOpen(m_name, FILE_CSV | FILE_WRITE, '\t');

      if(f != INVALID_HANDLE) {  // If file is open, save
         FileWrite(f, CVirtualReceiver::s_lastChangeTime);  // Time of last changes
         FileWrite(f, ArraySize(m_strategies));             // Number of strategies

         // All strategies
         FOREACH(m_strategies, ((CVirtualStrategy*) m_strategies[i]).Save(f));

         FileClose(f);

         // Update the last save time
         m_lastSaveTime = CVirtualReceiver::s_lastChangeTime;
         PrintFormat(__FUNCTION__" | OK at %s to %s",
                     TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS), m_name);
      } else {
         PrintFormat(__FUNCTION__" | ERROR: Operation FileOpen for %s failed, LastError=%d",
                     m_name, GetLastError());
         res = false;
      }
   }
   return res;
}

Después de guardar las estrategias, actualizamos la hora del último guardado de acuerdo con la hora de los últimos cambios en las posiciones virtuales. Ahora el método de guardado no guardará nada en el archivo hasta el siguiente cambio.

El método de carga de estado Load() debería hacer un trabajo similar, pero en lugar de escribir, leerá datos del archivo. En otras palabras, primero leemos el tiempo guardado y el número de estrategias. Aquí podemos comprobar si el número de estrategias leídas se corresponde con el número de estrategias añadidas al EA por si acaso. Si no es así, no tiene sentido seguir leyendo, se trata de un archivo erróneo. Si la respuesta es afirmativa, todo va bien, podemos seguir leyendo. A continuación, volvemos a delegar el trabajo posterior en objetos del siguiente nivel de la jerarquía: recorremos todas las estrategias añadidas y llamamos a su método de lectura desde el fichero abierto.

En el código, esto podría tener un aspecto similar al siguiente:

//+------------------------------------------------------------------+
//| Load status                                                      |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::Load() {
   bool res = true;

   // Load status if:
   if(true
         // file exists
         && FileIsExist(m_name)
         // currently, there is no optimization
         && !MQLInfoInteger(MQL_OPTIMIZATION)
         // and there is no testing at the moment or there is a visual test at the moment
         && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE))
     ) {
      int f = FileOpen(m_name, FILE_CSV | FILE_READ, '\t');

      if(f != INVALID_HANDLE) {  // If the file is open, then load
         m_lastSaveTime = FileReadDatetime(f);     // Last save time
         PrintFormat(__FUNCTION__" | LAST SAVE at %s", TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS));

         // Number of strategies
         long f_strategiesCount = StringToInteger(FileReadString(f));

         // Does the loaded number of strategies match the current one?
         res = (ArraySize(m_strategies) == f_strategiesCount);

         if(res) {
            // Load all strategies
            FOREACH(m_strategies, res &= ((CVirtualStrategy*) m_strategies[i]).Load(f));

            if(!res) {
               PrintFormat(__FUNCTION__" | ERROR loading strategies from file %s", m_name);
            }
         } else {
            PrintFormat(__FUNCTION__" | ERROR: Wrong strategies count (%d expected but %d found in file %s)",
                        ArraySize(m_strategies), f_strategiesCount, m_name);
         }
         FileClose(f);
      } else {
         PrintFormat(__FUNCTION__" | ERROR: Operation FileOpen for %s failed, LastError=%d", m_name, GetLastError());
         res = false;
      }
   }
   return res;
}

Guarde los cambios realizados en el archivo VirtualAdvisor.mqh en la carpeta actual.

Deberíamos llamar al método de carga de estado sólo una vez al lanzar el EA, pero no podemos hacerlo en el constructor del objeto del EA, ya que en este momento todavía no se han añadido estrategias al EA. Así que vamos a hacerlo en el archivo del EA de la función OnInit() después de añadir todas las instancias de estrategia al objeto del EA:

CVirtualAdvisor     *expert;                  // EA object

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
// Create and fill the array of strategy instances
   CStrategy *strategies[9];
   strategies[0] = ...
   ...
   strategies[8] = ...

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

// Add strategies to the EA
   FOREACH(strategies, expert.Add(strategies[i]));

// Load the previous state if available   
   expert.Load();

   return(INIT_SUCCEEDED);
}

Guarde los cambios en el archivo SimpleVolumesExpert.mq5 de la carpeta actual.


Modificación de la estrategia básica

Añade los métodos Save() y Load(), así como el método para convertir el objeto de estrategia actual en una cadena, a la clase base de una estrategia de negociación. Para abreviar, implementemos este método como un operador unario sobrecargado ~ (tilde).

//+------------------------------------------------------------------+
//| Class of a trading strategy with virtual positions               |
//+------------------------------------------------------------------+
class CVirtualStrategy : public CStrategy {
   ...

public:
   ...

   virtual bool      Load(const int f);   // Load status
   virtual bool      Save(const int f);   // Save status

   string operator~();                    // Convert object to string
};

El método de conversión de un objeto a una cadena devolverá una cadena con el nombre de la clase actual y el número de elementos de la matriz de posiciones virtuales:

//+------------------------------------------------------------------+
//| Convert an object to a string                                    |
//+------------------------------------------------------------------+
string CVirtualStrategy::operator~() {
   return StringFormat("%s(%d)", typename(this), ArraySize(m_orders));
}

El descriptor del archivo abierto para escribir se pasa al método Save(). La cadena obtenida del objeto al convertirlo en cadena se escribe primero en el fichero. A continuación, se llamará a su método de guardado en un bucle para cada posición virtual.

//+------------------------------------------------------------------+
//| Save status                                                      |
//+------------------------------------------------------------------+
bool CVirtualStrategy::Save(const int f) {
   bool res = true;
   FileWrite(f, ~this); // Save parameters

   // Save virtual positions (orders) of the strategy
   FOREACH(m_orders, res &= m_orders[i].Save(f));

   return res;
}

El método Load() simplemente leerá los datos en el mismo orden en que fueron escritos, mientras se asegura de que la cadena de parámetros en el archivo y en la estrategia coincidan:

//+------------------------------------------------------------------+
//| Load status                                                      |
//+------------------------------------------------------------------+
bool CVirtualStrategy::Load(const int f) {
   bool res = true;
   // Current parameters are equal to read parameters   
   res = (~this == FileReadString(f));
   
   // If yes, then load the virtual positions (orders) of the strategy
   if(res) {
      FOREACH(m_orders, res &= m_orders[i].Load(f));
   }

   return res;
}

Guarde los cambios implementados en el archivo VirtualStrategy.mqh de la carpeta actual.


Modificación de la estrategia de negociación

En la clase CSimpleVolumesStrategy de la estrategia de negociación específica, tenemos que añadir el mismo conjunto de métodos que en la clase base:

//+------------------------------------------------------------------+
//| Trading strategy using tick volumes                               |
//+------------------------------------------------------------------+
class CSimpleVolumesStrategy : public CVirtualStrategy {
   ...

public:
   ...

   virtual bool      Load(const int f) override;   // Load status
   virtual bool      Save(const int f) override;   // Save status

   string operator~();                    // Convert object to string
};

En el método de conversión de una estrategia comercial en una cadena, generaremos el resultado a partir del nombre de la clase y enumerando todos los parámetros críticos. Actualmente, por simplicidad, añadiremos aquí los valores de todos los parámetros, pero en el futuro puede que nos resulte más conveniente acortar esta lista. Esto nos permitirá reiniciar el EA con parámetros ligeramente modificados sin cerrar completamente todas las posiciones de mercado abiertas anteriormente. Por ejemplo, si aumentamos el parámetro TakeProfit, entonces podemos dejar con seguridad posiciones abiertas con un nivel Take Profit más bajo.

//+------------------------------------------------------------------+
//| Convert an object to a string                                    |
//+------------------------------------------------------------------+
string CSimpleVolumesStrategy::operator~() {
   return StringFormat("%s(%s,%s,%.2f,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%d)",
                       // Strategy instance parameters
                       typename(this), m_symbol, EnumToString(m_timeframe), m_fixedLot,
                       m_signalPeriod, m_signalDeviation, m_signaAddlDeviation,
                       m_openDistance, m_stopLevel, m_takeLevel, m_ordersExpiration,
                       m_maxCountOfOrders
                      );
}

Esto resultó ser otro lugar donde tenemos que escribir todos los parámetros de la estrategia en el código de nuevo. Nos acordaremos de ello hasta que nos pongamos a trabajar con las entradas.

El método Save() resulta ser muy conciso, ya que la clase base hará el trabajo principal:

//+------------------------------------------------------------------+
//| Save status                                                      |
//+------------------------------------------------------------------+
bool CSimpleVolumesStrategy::Save(const int f) {
   bool res = true;
   FileWrite(f, ~this);                // Save parameters
   res &= CVirtualStrategy::Save(f);   // Save strategy
   return res;
}

El método Load() será ligeramente más grande, pero principalmente debido a una mayor legibilidad del código:

//+------------------------------------------------------------------+
//| Load status                                                      |
//+------------------------------------------------------------------+
bool CSimpleVolumesStrategy::Load(const int f) {
   bool res = true;
   string currentParams = ~this;             // Current parameters
   string loadedParams = FileReadString(f);  // Read parameters

   PrintFormat(__FUNCTION__" | %s", loadedParams);

   res = (currentParams == loadedParams);

   // Load if read parameters match the current ones
   if(res) {
      res &= CVirtualStrategy::Load(f);
   }

   return res;
}

Guarde los cambios en el archivo SimpleVolumesExpert.mqh de la carpeta actual.


Modificación de posiciones virtuales

Necesitamos añadir tres métodos para la clase CVirtualOrder de posiciones virtuales:

class CVirtualOrder {
   ...

   virtual bool      Load(const int f);   // Load status
   virtual bool      Save(const int f);   // Save status

   string            operator~();         // Convert object to string
};

No utilizaremos el método de conversión a cadena al guardar en un archivo, pero será útil al registrar los datos del objeto cargado:

//+------------------------------------------------------------------+
//| Convert an object to a string                                    |
//+------------------------------------------------------------------+
string CVirtualOrder::operator~() {
   if(IsOpen()) {
      return StringFormat("#%d %s %s %.2f in %s at %.5f (%.5f, %.5f). %s, %f",
                          m_id, TypeName(), m_symbol, m_lot,
                          TimeToString(m_openTime), m_openPrice,
                          m_stopLoss, m_takeProfit,
                          TimeToString(m_closeTime), m_closePrice);
   } else {
      return StringFormat("#%d --- ", m_id);
   }

}

Aunque, podríamos cambiar esto más adelante añadiendo el método para leer propiedades de objetos desde una cadena.

Finalmente tendremos alguna información más significativa escrita en el archivo en el método Save():

//+------------------------------------------------------------------+
//| Save status                                                      |
//+------------------------------------------------------------------+
bool CVirtualOrder::Save(const int f) {
   FileWrite(f, m_id, m_symbol, m_lot, m_type, m_openPrice,
             m_stopLoss, m_takeProfit,
             m_openTime, m_closePrice, m_closeTime,
             m_expiration, m_comment, m_point);
   return true;
}

A continuación, Load() no sólo leerá lo que se escribió, rellenando las propiedades necesarias con la información leída, sino que también notificará a los objetos de estrategia asociados y al destinatario sobre si esta posición virtual (orden) está abierta o cerrada:

//+------------------------------------------------------------------+
//| Load status                                                      |
//+------------------------------------------------------------------+
bool CVirtualOrder::Load(const int f) {
   m_id = (ulong) FileReadNumber(f);
   m_symbol = FileReadString(f);
   m_lot = FileReadNumber(f);
   m_type = (ENUM_ORDER_TYPE) FileReadNumber(f);
   m_openPrice = FileReadNumber(f);
   m_stopLoss = FileReadNumber(f);
   m_takeProfit = FileReadNumber(f);
   m_openTime = FileReadDatetime(f);
   m_closePrice = FileReadNumber(f);
   m_closeTime = FileReadDatetime(f);
   m_expiration = FileReadDatetime(f);
   m_comment = FileReadString(f);
   m_point = FileReadNumber(f);

   PrintFormat(__FUNCTION__" | %s", ~this);

// Notify the recipient and the strategy that the position (order) is open
   if(IsOpen()) {
      m_receiver.OnOpen(GetPointer(this));
      m_strategy.OnOpen();
   } else {
      m_receiver.OnClose(GetPointer(this));
      m_strategy.OnClose();
   }

   return true;
}

Guarde el código obtenido en el archivo VirtualOrder.mqh de la carpeta actual.


Probar el guardado

Ahora vamos a probar los datos de estado de guardado y carga. Para evitar esperar un momento favorable para abrir posiciones, realizaremos cambios temporales en nuestra estrategia de negociación que nos obligarán a abrir una posición o una orden pendiente al inicio, si aún no hay posiciones/órdenes abiertas.

Desde que añadimos la visualización de mensajes sobre el progreso de carga, cuando reiniciemos el EA (la forma más fácil para nosotros de hacerlo es simplemente volver a compilar), veremos algo como lo siguiente en los logs:

CVirtualAdvisor::Load | LAST SAVE at 2027.02.23 08:05:33

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,0.06,13,0.30,1.00,0,10500.00,465.00,1000,3)
CVirtualOrder::Load | Order#1 EURGBP 0.06 BUY in 2027.02.23 08:02 at 0.85494 (0.75007, 0.85985). 1970.01.01 00:00, 0.000000
CVirtualReceiver::OnOpen#EURGBP | OPEN VirtualOrder #1
CVirtualOrder::Load | Order#2  ---
CVirtualOrder::Load | Order#3  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,0.11,17,1.70,0.50,210,16500.00,220.00,1000,3)
CVirtualOrder::Load | Order#4 EURGBP 0.11 BUY STOP in 2027.02.23 08:02 at 0.85704 (0.69204, 0.85937). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#5  ---
CVirtualOrder::Load | Order#6  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,0.06,51,0.50,1.10,500,19500.00,370.00,22000,3)
CVirtualOrder::Load | Order#7 EURGBP 0.06 BUY STOP in 2027.02.23 08:02 at 0.85994 (0.66494, 0.86377). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#8  ---
CVirtualOrder::Load | Order#9  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(GBPUSD,PERIOD_H1,0.04,80,1.10,0.20,0,6000.00,1190.00,1000,3)
CVirtualOrder::Load | Order#10 GBPUSD 0.04 BUY in 2027.02.23 08:02 at 1.26632 (1.20638, 1.27834). 1970.01.01 00:00, 0.000000
CVirtualReceiver::OnOpen#GBPUSD | OPEN VirtualOrder #10
CVirtualOrder::Load | Order#11  ---
CVirtualOrder::Load | Order#12  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(GBPUSD,PERIOD_H1,0.11,128,2.00,0.90,220,2000.00,1170.00,1000,3)
CVirtualOrder::Load | Order#13 GBPUSD 0.11 BUY STOP in 2027.02.23 08:02 at 1.26852 (1.24852, 1.28028). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#14  ---
CVirtualOrder::Load | Order#15  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(GBPUSD,PERIOD_H1,0.07,13,1.50,0.80,550,2500.00,1375.00,1000,3)
CVirtualOrder::Load | Order#16 GBPUSD 0.07 BUY STOP in 2027.02.23 08:02 at 1.27182 (1.24682, 1.28563). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#17  ---
CVirtualOrder::Load | Order#18  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURUSD,PERIOD_H1,0.04,24,0.10,0.30,330,7500.00,2400.00,24000,3)
CVirtualOrder::Load | Order#19 EURUSD 0.04 BUY STOP in 2027.02.23 08:02 at 1.08586 (1.01086, 1.10990). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#20  ---
CVirtualOrder::Load | Order#21  ---
CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURUSD,PERIOD_H1,0.05,18,0.20,0.40,220,19500.00,1480.00,6000,3)
CVirtualOrder::Load | Order#22 EURUSD 0.05 BUY STOP in 2027.02.23 08:02 at 1.08476 (0.88976, 1.09960). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#23  ---
CVirtualOrder::Load | Order#24  ---
CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURUSD,PERIOD_H1,0.05,128,0.70,0.30,550,3000.00,170.00,42000,3)
CVirtualOrder::Load | Order#25 EURUSD 0.05 BUY STOP in 2027.02.23 08:02 at 1.08806 (1.05806, 1.08980). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#26  ---
CVirtualOrder::Load | Order#27  ---

CVirtualAdvisor::Save | OK at 2027.02.23 08:19:48 to SimpleVolumes-27182.csv

Como podemos ver, los datos sobre posiciones virtuales abiertas y órdenes pendientes se cargan correctamente. En el caso de las posiciones virtuales, se llama al manejador de eventos de apertura, que, si es necesario, abre posiciones reales de mercado, por ejemplo, si se cerraron manualmente.

En general, el comportamiento del EA en relación con las posiciones abiertas durante un reinicio es una cuestión bastante compleja. Por ejemplo, si queremos cambiar el número mágico, el EA dejará de considerar como propias las operaciones abiertas anteriormente. ¿Deben cerrarse por la fuerza? Si queremos reemplazar la versión EA, entonces puede que tengamos que ignorar por completo la presencia de posiciones virtuales guardadas y cerrar forzosamente todas las posiciones abiertas. Debemos decidir cada vez qué escenario nos conviene más. Estas cuestiones aún no son muy urgentes, así que las dejaremos para más adelante.


Visualización

Ahora es el momento de visualizar las posiciones virtuales y las órdenes pendientes. A primera vista, parece bastante natural implementarlo como una extensión de la clase de posición virtual CVirtualOrder. Ya dispone de toda la información necesaria sobre el objeto visualizado. Sabe mejor que nadie cuándo tiene que redibujarse. En realidad, el primer proyecto de aplicación se hizo exactamente así. Pero entonces empezaron a surgir preguntas muy desagradables.

Una de estas preguntas es «¿En qué gráfico pensamos realizar la visualización?». La respuesta más sencilla es el actual. Pero sólo es adecuado para el caso cuando el EA trabaja en un símbolo, y coincide con el símbolo del gráfico, en el que se lanza el EA. En cuanto hay varios símbolos, resulta muy incómodo mostrar todas las posiciones virtuales en el gráfico de símbolos. El gráfico se convierte en un lío.

En otras palabras, la cuestión de elegir un gráfico para su visualización requería una solución, pero ya no estaba relacionada con la funcionalidad principal de los objetos de la clase CVirtualOrder. Funcionaron bien incluso sin nuestra visualización.

Por lo tanto, dejemos esta clase y miremos un poco más adelante. Si ampliamos ligeramente nuestros objetivos, estaría bien poder activar/desactivar selectivamente la visualización de posiciones virtuales agrupadas por estrategia o tipo, así como implementar unos datos más detallados, por ejemplo precios abiertos visibles, Stop Loss y Take Profit, pérdidas y beneficios previstos si se activan, y quizás incluso la posibilidad de modificar manualmente estos niveles. Aunque esto último sería más útil si estuviéramos desarrollando un panel para trading semiautomatizado, en lugar de un EA basado en estrategias estrictamente algorítmicas. En general, incluso al iniciar una implementación sencilla, podemos adelantarnos un poco, imaginando el desarrollo posterior del código. Esto puede ayudarnos a elegir una dirección, en la que será menos probable que volvamos a revisar código ya escrito.

Así que vamos a crear una nueva clase base para todos los objetos que de una forma u otra estarán asociados con la visualización de algo en los gráficos:

//+------------------------------------------------------------------+
//| Basic class for visualizing various objects                      |
//+------------------------------------------------------------------+
class CInterface {
protected:
   static ulong      s_magic;       // EA magic number
   bool              m_isActive;    // Is the interface active?
   bool              m_isChanged;   // Does the object have any changes?
public:
   CInterface();                    // Constructor
   virtual void      Redraw() = 0;  // Draw changed objects on the chart
   virtual void      Changed() {    // Set the flag for changes
      m_isChanged = true;
   }
};

ulong CInterface::s_magic = 0;

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CInterface::CInterface() :
   m_isActive(!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE)),
   m_isChanged(true) {}

Guarde este código en el Interface.mqh de la carpeta actual.

Crea dos nuevas clases basadas en esta clase:

  • CVirtualChartOrder - objeto que muestra una posición virtual o una orden pendiente en un gráfico en el terminal (posición virtual gráfica). Podrá dibujar una posición virtual en el gráfico si se han producido cambios en él, y el gráfico con el instrumento requerido se abrirá automáticamente si no se abrió en el terminal.
  • CVirtualInterface - agregador de todos los objetos gráficos de la interfaz EA. Por ahora, sólo contiene la matriz de posiciones virtuales gráficas. Se ocupará de crear objetos gráficos de posición virtual cada vez que se cree un objeto de posición virtual. También recibirá mensajes sobre cambios en la composición de las posiciones virtuales y hará que se redibujen las posiciones virtuales gráficas correspondientes. Dicho agregador existirá en una única instancia (implementando el patrón de diseño Singleton) y estará disponible en la clase CVirtualAdvisor.

La clase CVirtualChartOrder tiene el siguiente aspecto:

//+------------------------------------------------------------------+
//| Graphic virtual position class                                   |
//+------------------------------------------------------------------+
class CVirtualChartOrder : public CInterface {
   CVirtualOrder*    m_order;          // Associated virtual position (order)
   CChart            m_chart;          // Chart object to be displayed

   // Objects on the chart to display the virtual position
   CChartObjectHLine m_openLine;       // Open price line

   long              FindChart();      // Search/open the desired chart
public:
   CVirtualChartOrder(CVirtualOrder* p_order);     // Constructor
   ~CVirtualChartOrder();                          // Destructor

   bool              operator==(const ulong id) {  // Comparison operator by Id
      return m_order.Id() == id;
   }

   void              Show();    // Show a virtual position (order)
   void              Hide();    // Hide a virtual position (order) 

   virtual void      Redraw() override;   // Redraw a virtual position (order) 
};

El método Redraw() comprueba si es necesario ejecutarlo y, si es necesario, llama a métodos para mostrar u ocultar una posición virtual del gráfico:

//+------------------------------------------------------------------+
//| Redraw a virtual position (order)                                |
//+------------------------------------------------------------------+
void CVirtualChartOrder::Redraw() {
   if(m_isChanged) {
      if(m_order.IsOpen()) {
         Show();
      } else {
         Hide();
      }
      m_isChanged = false;
   }
}

En el método de visualización Show(), primero llamamos al método FindChart() para determinar en qué gráfico se debe mostrar la posición virtual. En este método, simplemente iteramos a través de todos los gráficos abiertos hasta que encontramos un gráfico con un símbolo coincidente. Si no encontramos ninguno, abriremos un nuevo gráfico. El gráfico encontrado (o abierto) se almacena en la propiedad m_chart.

//+------------------------------------------------------------------+
//| Finding a chart to display                                       |
//+------------------------------------------------------------------+
long CVirtualChartOrder::FindChart() {
   if(m_chart.ChartId() == -1 || m_chart.Symbol() != m_order.Symbol()) {
      long currChart, prevChart = ChartFirst();
      int i = 0, limit = 1000;

      currChart = prevChart;

      while(i < limit) { // we probably have no more than 1000 open charts
         if(ChartSymbol(currChart) == m_order.Symbol()) {
            return currChart;
         }
         currChart = ChartNext(prevChart); // get new chart on the basis of the previous one
         if(currChart < 0)
            break;        // end of chart list is reached
         prevChart = currChart; // memorize identifier of the current chart for ChartNext()
         i++;
      }

      // If a suitable chart is not found, then open a new one
      if(currChart == -1) {
         m_chart.Open(m_order.Symbol(), PERIOD_CURRENT);
      }
   }
   return m_chart.ChartId();
}

El método Show() simplemente dibuja una línea horizontal correspondiente al precio de apertura. Su color y tipo se determinará en función de la posición (orden) dirección y tipo. El método Hide() borrará esta línea. El enriquecimiento de la clase se producirá en estos métodos.

Guarde el código obtenido en el fichero VirtualChartOrder.mqh de la carpeta actual.

La implementación de la clase CVirtualInterface puede realizarse de la siguiente manera:

//+------------------------------------------------------------------+
//| EA GUI class                                                     |
//+------------------------------------------------------------------+
class CVirtualInterface : public CInterface {
protected:
// Static pointer to a single class instance
   static   CVirtualInterface *s_instance;

   CVirtualChartOrder *m_chartOrders[];   // Array of graphical virtual positions

//--- Private methods
   CVirtualInterface();   // Closed constructor

public:
   ~CVirtualInterface();  // Destructor

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

//--- Public methods
   void              Changed(CVirtualOrder *p_order); // Handle virtual position changes
   void              Add(CVirtualOrder *p_order);     // Add a virtual position

   virtual void      Redraw() override;   // Draw changed objects on the chart
};

En el método estático para crear una única Instance(), añade la inicialización de la variable estática s_magic si se ha pasado un número mágico distinto de cero:

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

En el método para manejar el evento de apertura/cierre de una posición virtual, encontraremos su correspondiente objeto gráfico de posición virtual y marcaremos que se han producido cambios en él:

//+------------------------------------------------------------------+
//| Handle virtual position changes                                  |
//+------------------------------------------------------------------+
void CVirtualInterface::Changed(CVirtualOrder *p_order) {
   // Remember that this position has changes
   int i;
   FIND(m_chartOrders, p_order.Id(), i);
   if(i != -1) {
      m_chartOrders[i].Changed();
      m_isChanged = true;
   }
}

Por último, en el método de renderizado de la interfaz Redraw(), llama a los métodos para dibujar todas las posiciones virtuales gráficas en un bucle:

//+------------------------------------------------------------------+
//| Draw changed objects on a chart                                  |
//+------------------------------------------------------------------+
void CVirtualInterface::Redraw() {
   if(m_isActive && m_isChanged) {  // If the interface is active and there are changes
      // Start redrawing graphical virtual positions 
      FOREACH(m_chartOrders, m_chartOrders[i].Redraw());
      m_isChanged = false;          // Reset the changes flag
   }
}

Guarde el código obtenido en el archivo VirtualInterface.mqh de la carpeta actual.

Ahora sólo faltan los últimos retoques para que funcione el subsistema de visualización de posiciones virtuales en los gráficos. En la clase CVirtualAdvisor, añade la nueva propiedad m_interface, que almacenará una única instancia del objeto interfaz de pantalla. Tenemos que encargarnos de inicializarlo en el constructor y de borrarlo en el destructor:

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   ...
   CVirtualInterface *m_interface;     // Interface object to show the status to the user
   
   ...
};

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(ulong p_magic = 1, string p_name = "") :
   ...
// Initialize the interface with the static interface
   m_interface(CVirtualInterface::Instance(p_magic)),
   ... {
   ...
};

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CVirtualAdvisor::~CVirtualAdvisor() {
   
... 
   delete m_interface;        // Remove the interface
}

En el manejador de eventos OnTick, después de todas las operaciones, añade llamandas al método de redibujar la interfaz ya que es la parte menos importante del manejo de ticks: 

//+------------------------------------------------------------------+
//| 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();

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

En la clase CVirtualReceiver, añade también la nueva propiedad m_interface, que almacenará una única instancia del objeto interfaz de pantalla. Debemos ocuparnos de su inicialización en el constructor:

//+------------------------------------------------------------------+
//| Class for converting open volumes to market positions (receiver) |
//+------------------------------------------------------------------+
class CVirtualReceiver : public CReceiver {
protected:
   ...
   CVirtualInterface
   *m_interface;                          // Interface object to show the status to the user

   ...
};

//+------------------------------------------------------------------+
//| Closed constructor                                               |
//+------------------------------------------------------------------+
CVirtualReceiver::CVirtualReceiver() :
   m_interface(CVirtualInterface::Instance()),
   ... {}

En el método para asignar el número necesario de posiciones virtuales a las estrategias, las añadiremos simultáneamente a la interfaz:

//+------------------------------------------------------------------+
//| 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
   CVirtualInterface *draw = CVirtualInterface::Instance();
   ArrayResize(orders, n);                // Expand the array of virtual positions
   FOREACH(orders,
           orders[i] = new CVirtualOrder(strategy); // Fill the array with new objects
           APPEND(self.m_orders, orders[i]);
           draw.Add(orders[i])) // Register the created virtual position
   PrintFormat(__FUNCTION__ + " | OK, Strategy orders: %d from %d total",
               ArraySize(orders),
               ArraySize(self.m_orders));
}

Lo último que tenemos que hacer es añadir una alerta de interfaz en los métodos para manejar la apertura/cierre de posiciones virtuales en esta clase:

void CVirtualReceiver::OnOpen(CVirtualOrder *p_order) {
   m_interface.Changed(p_order);
   ...
}

//+------------------------------------------------------------------+
//| Handle closing a virtual position                                |
//+------------------------------------------------------------------+
void CVirtualReceiver::OnClose(CVirtualOrder *p_order) {
   m_interface.Changed(p_order);
   ...
   }
}

Guarde los cambios realizados en los archivos VirtualAdvisor.mqh y VirtualReceiver.mqh en la carpeta actual.

Si ahora compilamos y ejecutamos nuestro EA, entonces podemos ver algo como esto si hay posiciones virtuales abiertas u órdenes pendientes:

Fig. 1. Visualización de órdenes y posiciones virtuales en el gráfico

Fig. 1. Visualización de órdenes y posiciones virtuales en el gráfico

Aquí, las líneas de puntos muestran las órdenes pendientes virtuales: naranja - STOP DE VENTA, azul - STOP DE COMPRA, mientras que las líneas continuas muestran las posiciones virtuales: azul - COMPRA, rojo - VENTA. Por ahora, sólo son visibles los niveles de apertura, pero en el futuro será posible hacer que la pantalla esté más saturada.


Bonito gráfico...

En las discusiones del foro, hubo una idea de que los lectores de artículos (al menos algunos de ellos) en primer lugar mirar al final del artículo, donde esperan ver un hermoso gráfico que muestra el crecimiento de los fondos al probar un EA desarrollado. Si efectivamente existe tal gráfico, esto se convierte en un incentivo adicional para volver al principio del artículo y leerlo.

Mientras trabajábamos en este artículo, no realizamos ningún cambio en la estrategia de demostración utilizada ni en el conjunto de instancias de estrategia del EA de demostración. Todo permanece en las mismas condiciones que en el momento de la publicación del artículo anterior. Por lo tanto, aquí no hay gráficos impresionantes.

Sin embargo, paralelamente al presente artículo, he optimizado y entrenado otras estrategias que se publicarán más adelante. Los resultados de sus pruebas nos permiten afirmar que combinar un gran número de instancias de estrategia en un EA es un método prometedor.

Aquí hay dos ejemplos de ejecuciones de prueba del EA que utiliza alrededor de 170 instancias de estrategias de trabajo en varios símbolos y plazos. El periodo de prueba fue de 2023-01-01 a 2024-02-23. Los datos del periodo no se utilizaron para la optimización y el entrenamiento. En la configuración de la gestión del capital, se establecieron parámetros que suponían que la detracción aceptable en un caso era de alrededor del 10%, y en otro - de alrededor del 40%.


Fig. 2. Resultados de las pruebas con una reducción aceptable del 10%.


Fig. 3. Resultados de las pruebas con una reducción aceptable del 40%.

La presencia de tales resultados no garantiza su repetición en el futuro. Pero podemos trabajar para que ese resultado sea más probable. Intentaré no empeorar estos resultados y no caer en el autoengaño del sobreentrenamiento.


Conclusión

En este artículo hemos realizado un trabajo que nos aleja un poco de la dirección principal. Me gustaría avanzar hacia la optimización automática, de la que, por ejemplo, se habló en un artículo publicado recientemente Usamos algoritmos de optimización para ajustar los parámetros del asesor sobre la marcha.

Sin embargo, la capacidad de guardar y cargar el estado es un componente importante de cualquier EA que tenga perspectivas de ser lanzado en una cuenta real. No menos importante es la capacidad de trabajar no sólo con posiciones de mercado, sino también con órdenes pendientes utilizadas en muchas estrategias de negociación. Al mismo tiempo, la visualización del funcionamiento del EA puede ayudar a identificar posibles errores de implementación en la etapa de desarrollo.

¡Gracias por tu atención! ¡Nos vemos en la próxima entrega!

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

Características del Wizard MQL5 que debe conocer (Parte 13): DBSCAN para la clase experta de señales Características del Wizard MQL5 que debe conocer (Parte 13): DBSCAN para la clase experta de señales
El agrupamiento basado en densidad para aplicaciones con ruido (DBSCAN) es una forma no supervisada de agrupar datos que apenas requiere parámetros de entrada, salvo solo 2, lo cual, en comparación con otros enfoques como k-means, es una ventaja. Profundizamos en cómo esto podría ser constructivo para probar y eventualmente operar con Asesores Expertos montados por Wizard MQL5.
Creación de un modelo de restricción de tendencia de velas (Parte 1): Para EAs e Indicadores Técnicos Creación de un modelo de restricción de tendencia de velas (Parte 1): Para EAs e Indicadores Técnicos
Este artículo está dirigido a principiantes y desarrolladores avanzados de MQL5. Proporciona un fragmento de código para definir y limitar los indicadores generadores de señales a tendencias en plazos superiores. De este modo, los operadores pueden mejorar sus estrategias incorporando una perspectiva de mercado más amplia, lo que da lugar a señales de negociación potencialmente más sólidas y fiables.
Filtrado de estacionalidad y período de tiempo para modelos de Deep Learning ONNX con Python para EA Filtrado de estacionalidad y período de tiempo para modelos de Deep Learning ONNX con Python para EA
¿Podemos beneficiarnos de la estacionalidad al crear modelos para Deep Learning con Python? ¿Ayuda el filtrado de datos para los modelos ONNX a obtener mejores resultados? ¿Qué periodo de tiempo debemos utilizar? Trataremos todo esto a lo largo de este artículo.
Trabajamos con modelos ONNX en formato float16 y float8 Trabajamos con modelos ONNX en formato float16 y float8
Los formatos de datos usados para representar modelos de aprendizaje automático desempeñan un papel clave en su eficacia. En los últimos años, se han desarrollado varios tipos de datos nuevos específicamente para trabajar con modelos de aprendizaje profundo. En este artículo nos centraremos en dos nuevos formatos de datos que se han generalizado en los modelos modernos.