Seguimiento de los cambios en el entorno de trading

En la sección anterior relacionada con el evento OnTrade hemos mencionado que algunos enfoques de programación de estrategias de trading pueden requerir que se tomen instantáneas del entorno y se comparen entre sí a lo largo del tiempo. Esta es una práctica común cuando se utiliza OnTrade pero también puede activarse en un horario, en cada barra, o incluso tick. Nuestras clases de monitor que pueden leer las propiedades de órdenes, transacciones y posiciones carecían de la capacidad de guardar el estado. En esta sección presentaremos una de las opciones de almacenamiento en caché del entorno de trading.

Las propiedades de todos los objetos de trading se dividen por tipos en tres grupos: entero, real y cadena. Cada clase de objeto tiene sus propios grupos (por ejemplo, para las órdenes, las propiedades de enteros se describen en la enumeración ENUM_ORDER_PROPERTY_INTEGER, y para las posiciones se describen en ENUM_POSITION_PROPERTY_INTEGER), pero la esencia de la división es la misma. Por lo tanto, introduciremos la enumeración PROP_TYPE, con cuya ayuda será posible describir a qué tipo pertenece una propiedad de objeto. Esta generalización surge de forma natural, ya que los mecanismos para almacenar y procesar propiedades del mismo tipo deben ser los mismos, independientemente de si la propiedad pertenece a una orden, posición o transacción.

enum PROP_TYPE
{
   PROP_TYPE_INTEGER,
   PROP_TYPE_DOUBLE,
   PROP_TYPE_STRING,
};

Los arrays son la forma más sencilla de almacenar valores de propiedades. Obviamente, debido a la presencia de tres tipos de base, necesitaremos tres arrays diferentes. Vamos a describirlos dentro de una nueva clase TradeState anidada en MonitorInterface (TradeBaseMonitor.mqh).

La plantilla básica MonitorInterface<I,D,S> constituye la base de todas las clases de monitor aplicadas (OrderMonitor, DealMonitor, PositionMonitor). Los tipos I, D y S corresponden aquí a enumeraciones concretas de propiedades de enteros, reales y cadenas.

Es bastante lógico incluir el mecanismo de almacenamiento en el monitor base, sobre todo porque la caché de propiedades creada se llenará de datos mediante la lectura de propiedades del objeto monitor.

template<typename I,typename D,typename S>
class MonitorInterface
{
   ...
   class TradeState
   {
   public:
      ...
      long ulongs[];
      double doubles[];
      string strings[];
      const MonitorInterface *owner;
      
      TradeState(const MonitorInterface *ptr) : owner(ptr)
      {
         ...
      }
   };

Toda la clase TradeState se ha hecho pública porque sería necesario acceder a sus campos desde el objeto monitor padre (que se pasa como puntero al constructor), y además TradeState sólo se utilizará en la parte protegida del monitor (no se puede acceder a ellos desde el exterior).

Para rellenar tres arrays con valores de propiedades de tres tipos diferentes, primero debe averiguar la distribución de propiedades por tipo e índices en cada array concreto.

Para cada tipo de objeto de trading (órdenes, transacciones y posiciones), los identificadores de las tres enumeraciones correspondientes con propiedades de distintos tipos no se cruzan y forman una numeración continua. Vamos a demostrarlo.

En el capítulo Enumeraciones hemos visto el script ConversionEnum.mq5 que implementa la función process para registrar todos los elementos de una enumeración determinada. Ese script examinó el enum ENUM_APPLIED_PRICE. Ahora podemos crear una copia del script y analizar las otras tres enumeraciones. Por ejemplo, así:

void OnStart()
{
   process((ENUM_POSITION_PROPERTY_INTEGER)0);
   process((ENUM_POSITION_PROPERTY_DOUBLE)0);
   process((ENUM_POSITION_PROPERTY_STRING)0);
}

Como resultado de su ejecución, obtenemos el siguiente registro. La columna de la izquierda contiene la numeración dentro de las enumeraciones, y los valores de la derecha (después del signo '=') son las constantes integradas (identificadores) de los elementos.

ENUM_POSITION_PROPERTY_INTEGER Count=9
0 POSITION_TIME=1
1 POSITION_TYPE=2
2 POSITION_MAGIC=12
3 POSITION_IDENTIFIER=13
4 POSITION_TIME_MSC=14
5 POSITION_TIME_UPDATE=15
6 POSITION_TIME_UPDATE_MSC=16
7 POSITION_TICKET=17
8 POSITION_REASON=18
ENUM_POSITION_PROPERTY_DOUBLE Count=8
0 POSITION_VOLUME=3
1 POSITION_PRICE_OPEN=4
2 POSITION_PRICE_CURRENT=5
3 POSITION_SL=6
4 POSITION_TP=7
5 POSITION_COMMISSION=8
6 POSITION_SWAP=9
7 POSITION_PROFIT=10
ENUM_POSITION_PROPERTY_STRING Count=3
0 POSITION_SYMBOL=0
1 POSITION_COMMENT=11
2 POSITION_EXTERNAL_ID=19

Por ejemplo, la propiedad con constante 0 es una cadena POSITION_SYMBOL, las propiedades con constantes 1 y 2 son enteros POSITION_TIME y POSITION_TYPE, la propiedad con constante 3 es un POSITION_VOLUME real, etc.

Así, las constantes son un sistema de índices de extremo a extremo sobre propiedades de todos los tipos, y podemos utilizar el mismo algoritmo (basado en EnumToArray.mqh) para obtenerlas.

Para cada propiedad, es necesario recordar su tipo (que determina cuál de los tres arrays almacenará el valor) y el número de serie entre las propiedades del mismo tipo (este será el índice del elemento en el array correspondiente). Por ejemplo, vemos que las posiciones sólo tienen tres propiedades de cadena, por lo que el array strings en la instantánea de una posición tendrá que tener el mismo tamaño, y POSITION_SYMBOL (0), POSITION_COMMENT (11) y POSITION_EXTERNAL_ID (19) se escribirán en sus índices 0, 1 y 2.

La conversión de los índices de extremo a extremo de las propiedades en su tipo (uno de PROP_TYPE) y en un número ordinal en un array del tipo correspondiente se puede hacer una vez al inicio del programa, ya que las enumeraciones con propiedades son constantes (integradas en el sistema). Escribimos la tabla de direccionamiento indirecto resultante en un array estático bidimensional indices. Su tamaño en la primera dimensión se determinará dinámicamente como el número total de propiedades (de los 3 tipos). Escribiremos el tamaño en la variable estática limit. Se asignan un par de celdas para la segunda dimensión: indices[i][0] - tipo PROP_TYPE, indices[i][1] - índice en uno de los arrays ulongs, doubles o strings (dependiendo de indices[i][0]).

   class TradeState
   {
      ...
      static int indices[][2];
      static int jds;
   public:
      const static int limit;
      
      static PROP_TYPE type(const int i)
      {
         return (PROP_TYPE)indices[i][0];
      }
      
      static int offset(const int i)
      {
         return indices[i][1];
      }
      ...

Las variables j, d y s se utilizarán para indexar secuencialmente las propiedades dentro de cada uno de los tres tipos diferentes. Así es como se hace en el método estático calcIndices.

      static int calcIndices()
      {
         const int size = fmax(boundary<I>(),
            fmax(boundary<D>(), boundary<S>())) + 1;
         ArrayResize(indicessize);
         j = d = s = 0;
         for(int i = 0i < size; ++i)
         {
            if(detect<I>(i))
            {
               indices[i][0] = PROP_TYPE_INTEGER;
               indices[i][1] = j++;
            }
            else if(detect<D>(i))
            {
               indices[i][0] = PROP_TYPE_DOUBLE;
               indices[i][1] = d++;
            }
            else if(detect<S>(i))
            {
               indices[i][0] = PROP_TYPE_STRING;
               indices[i][1] = s++;
            }
            else
            {
               Print("Unresolved int value as enum: "i" "typename(TradeState));
            }
         }
         return size;
      }

El método boundary devuelve la constante máxima entre todos los elementos de la enumeración E dada.

   template<typename E>
   static int boundary(const E dummy = (E)NULL)
   {
      int values[];
      const int n = EnumToArray(dummyvalues01000);
      ArraySort(values);
      return values[n - 1];
   }

El mayor valor de los tres tipos de enumeraciones determina el rango de enteros que deben ordenarse de acuerdo con el tipo de propiedad al que pertenecen.

Aquí utilizamos el método detect que devuelve true si el entero es un elemento de una enumeración.

   template<typename E>
   static bool detect(const int v)
   {
      ResetLastError();
      const string s = EnumToString((E)v); // result is not used 
      if(_LastError == 0// only the absence of an error is important
      {
         return true;
      }
      return false;
   }

La última pregunta es cómo ejecutar este cálculo cuando se inicia el programa. Esto se consigue utilizando la naturaleza estática de las variables y el método.

template<typename I,typename D,typename S>
static int MonitorInterface::TradeState::indices[][2];
template<typename I,typename D,typename S>
static int MonitorInterface::TradeState::j,
   MonitorInterface::TradeState::d,
   MonitorInterface::TradeState::s;
template<typename I,typename D,typename S>
const static int MonitorInterface::TradeState::limit =
   MonitorInterface::TradeState::calcIndices();

Observe que limit se inicializa con el resultado de llamar a nuestra función calcIndices.

Teniendo una tabla con índices, implementamos el llenado de arrays con valores de propiedades en el método cache.

   class TradeState
   {
      ...
      TradeState(const MonitorInterface *ptr) : owner(ptr)
      {
         cache(); // when creating an object, immediately cache the properties
      }
      
      template<typename T>
      void _get(const int eT &valueconst // overload with record by reference
      {
         value = owner.get(evalue);
      }
      
      void cache()
      {
         ArrayResize(ulongsj);
         ArrayResize(doublesd);
         ArrayResize(stringss);
         for(int i = 0i < limit; ++i)
         {
            switch(indices[i][0])
            {
            case PROP_TYPE_INTEGER_get(iulongs[indices[i][1]]); break;
            case PROP_TYPE_DOUBLE_get(idoubles[indices[i][1]]); break;
            case PROP_TYPE_STRING_get(istrings[indices[i][1]]); break;
            }
         }
      }
   };

Hacemos un bucle a través de todo el rango de propiedades de 0 a limit y, dependiendo del tipo de propiedad en indices[i][0], escribimos su valor en el elemento del array ulongs, doubles o strings bajo el número indices[i][1] (el elemento correspondiente del array se pasa por referencia al método _get).

Una llamada a owner.get(e, value) remite a uno de los métodos estándar de la clase monitor (aquí es visible como puntero abstracto MonitorInterface). En concreto, para las posiciones de la clase PositionMonitor, esto dará lugar a las llamadas PositionGetInteger, PositionGetDouble, o PositionGetString. El compilador elegirá el tipo correcto. Los monitores de órdenes y transacciones tienen sus propias implementaciones similares, que este código base incluye automáticamente.

Es lógico heredar la descripción de una instantánea de un objeto de trading de la clase monitor. Dado que tenemos que almacenar en caché órdenes, transacciones y posiciones, tiene sentido hacer de la nueva clase una plantilla y recopilar en ella todos los algoritmos comunes adecuados para todos los objetos. Llamémosla TradeBaseState (archivoTradeState.mqh).

template<typename M,typename I,typename D,typename S>
class TradeBaseStatepublic M
{
   M::TradeState state;
   bool cached;
   
public:
   TradeBaseState(const ulong t) : M(t), state(&this), cached(ready)
   {
   }
   
   void passthrough(const bool b)   // enable/disable cache as desired
   {
      cached = b;
   }
   ...

Una de las clases de monitor específicas descritas anteriormente se oculta bajo la letra M (OrderMonitor.mqh, PositionMonitor.mqh, DealMonitor.mqh). La base es el objeto de caché state de la recién introducida clase M::TradeState. Dependiendo de M, se formará en su interior una tabla de índices específica (una para la clase M) y se distribuirán arrays de propiedades (propias para cada instancia de M, es decir, para cada orden, transacción, posición).

La variable cached contiene una señal de si los arrays de state están llenos de valores de propiedades, y si se deben consultar las propiedades de un objeto para devolver valores de la caché. Esto será necesario más adelante para comparar los estados guardados y actuales.

En otras palabras: cuando cached se establece en false, el objeto se comportará como un monitor normal, leyendo propiedades del entorno de trading. Cuando cached es igual a true, el objeto devolverá los valores almacenados previamente de los arrays internos.

   virtual long get(const I propertyconst override
   {
      return cached ? state.ulongs[M::TradeState::offset(property)] : M::get(property);
   }
   
   virtual double get(const D propertyconst override
   {
      return cached ? state.doubles[M::TradeState::offset(property)] : M::get(property);
   }
   
   virtual string get(const S propertyconst override
   {
      return cached ? state.strings[M::TradeState::offset(property)] : M::get(property);
   }
   ...

Por defecto, el almacenamiento en caché está, por supuesto, activado.

También debemos proporcionar un método que realice directamente el almacenamiento en caché (rellenado de arrays). Para ello, basta con llamar al método cache para el objeto state.

   bool update()
   {
      if(refresh())
      {
         cached = false// disable reading from the cache
         state.cache();  // read real properties and write to cache
         cached = true;  // enable external cache access back 
         return true;
      }
      return false;
   }

¿Qué es el método refresh?

Hasta ahora hemos estado utilizando objetos monitor en modo simple: creándolos, leyendo propiedades y borrándolos. Al mismo tiempo, la lectura de propiedades supone que la orden, transacción o posición correspondiente se ha seleccionado en el contexto de trading (dentro del constructor). Como ahora estamos mejorando los monitores para admitir el estado interno, es necesario asegurarse de que el elemento deseado se reasigna para poder leer las propiedades incluso después de un tiempo indefinido (por supuesto, con una comprobación de que el elemento todavía existe). Para implementarlo, hemos añadido el método virtual refresh a la plantilla de la clase MonitorInterface.

// TradeBaseMonitor.mqh
template<typename I,typename D,typename S>
class MonitorInterface
{
   ...
   virtual bool refresh() = 0;

Debe devolver true al asignar con éxito una orden, transacción o posición. Si el resultado es false, la variable integrada _LastError debe contener uno de los siguientes errores:

  • 4753 ERR_TRADE_POSITION_NOT_FOUND;
  • 4754 ERR_TRADE_ORDER_NOT_FOUND;
  • 4755 ERR_TRADE_DEAL_NOT_FOUND;

En este caso, la variable miembro ready, que señala la disponibilidad del objeto, debe restablecerse a false en las implementaciones de este método en las clases derivadas.

Por ejemplo, en el constructor PositionMonitor, teníamos y seguimos teniendo una inicialización de este tipo. La situación es similar a la de los monitores de órdenes y transacciones.

// PositionMonitor.mqh
   const ulong ticket;
   PositionMonitor(const ulong t): ticket(t)
   {
      if(!PositionSelectByTicket(ticket))
      {
         PrintFormat("Error: PositionSelectByTicket(%lld) failed: %s"ticket,
            E2S(_LastError));
      }
      else
      {
         ready = true;
      }
   }
   ...

Ahora añadiremos el método refresh a todas las clases específicas de este tipo (véase el ejemplo PositionMonitor):

// PositionMonitor.mqh
   virtual bool refresh() override
   {
      ready = PositionSelectByTicket(ticket);
      return ready;
   }

No obstante, rellenar arrays de caché con los valores de las propiedades es sólo la mitad de la batalla; la segunda parte consiste en comparar estos valores con el estado real de la orden, transacción o posición.

Para identificar las diferencias y escribir los índices de las propiedades modificadas en el array changes, la clase generada TradeBaseState proporciona el método getChanges. El método devuelve true cuando se detectan cambios.

template<typename M,typename I,typename D,typename S>
class TradeBaseStatepublic M
{
   ...
   bool getChanges(int &changes[])
   {
      const bool previous = ready;
      if(refresh())
      {
         // element is selected in the trading environment = properties can be read and compared
         cached = false;    // read directly
         const bool result = M::diff(statechanges);
         cached = true;     // turn cache back on by default
         return result;
      }
      // no longer "ready" = most likely deleted
      return previous != ready// if just deleted, this is also a change 
   }

Como puede ver, el trabajo principal se confía a un determinado método diff de la clase M. Se trata de un nuevo método: tenemos que escribirlo. Afortunadamente, gracias a la programación orientada a objetos (POO), puede hacer esto una vez en la plantilla base MonitorInterface y el método aparecerá inmediatamente para órdenes, transacciones y posiciones.

// TradeBaseMonitor.mqh
template<typename I,typename D,typename S>
class MonitorInterface
{
   ...
   bool diff(const TradeState &thatint &changes[])
   {
      ArrayResize(changes0);
      for(int i = 0i < TradeState::limit; ++i)
      {
         switch(TradeState::indices[i][0])
         {
         case PROP_TYPE_INTEGER:
            if(this.get((I)i) != that.ulongs[TradeState::offset(i)])
            {
               PUSH(changesi);
            }
            break;
         case PROP_TYPE_DOUBLE:
            if(!TU::Equal(this.get((D)i), that.doubles[TradeState::offset(i)]))
            {
               PUSH(changesi);
            }
            break;
         case PROP_TYPE_STRING:
            if(this.get((S)i) != that.strings[TradeState::offset(i)])
            {
               PUSH(changesi);
            }
            break;
         }
      }
      return ArraySize(changes) > 0;
   }

Así pues, todo está listo para formar clases de caché específicas para órdenes, transacciones y posiciones. Por ejemplo, las posiciones se almacenarán en el monitor ampliado PositionState en la base de PositionMonitor.

class PositionStatepublic TradeBaseState<PositionMonitor,
   ENUM_POSITION_PROPERTY_INTEGER,
   ENUM_POSITION_PROPERTY_DOUBLE,
   ENUM_POSITION_PROPERTY_STRING>
{
public:
   PositionState(const long t): TradeBaseState(t) { }
};

Del mismo modo, en el archivo TradeState.mqh se define una clase de caché para las transacciones.

class DealStatepublic TradeBaseState<DealMonitor,
   ENUM_DEAL_PROPERTY_INTEGER,
   ENUM_DEAL_PROPERTY_DOUBLE,
   ENUM_DEAL_PROPERTY_STRING>
{
public:
   DealState(const long t): TradeBaseState(t) { }
};

Con las órdenes, las cosas son un poco más complicadas, porque éstas pueden ser activas e históricas. Hasta ahora hemos tenido una clase de monitor genérica para órdenes, OrderMonitor, que intenta encontrar el ticket de orden presentado tanto entre las órdenes activas como en el historial. Este enfoque no es adecuado para el almacenamiento en caché, ya que los Asesores Expertos necesitan hacer un seguimiento de la transición de una orden de un estado a otro.

Por esta razón, añadimos 2 clases específicas más al archivo OrderMonitor.mqh: ActiveOrderMonitor y HistoryOrderMonitor.

// OrderMonitor.mqh
class ActiveOrderMonitorpublic OrderMonitor
{
public:
   ActiveOrderMonitor(const ulong t): OrderMonitor(t)
   {
      if(history// if the order is in history, then it is already inactive
      {
         ready = false;   // reset ready flag
         history = false// this object is only for active orders by definition
      }
   }
   
   virtual bool refresh() override
   {
      ready = OrderSelect(ticket);
      return ready;
   }
};
   
class HistoryOrderMonitorpublic OrderMonitor
{
public:
   HistoryOrderMonitor(const ulong t): OrderMonitor(t) { }
   
   virtual bool refresh() override
   {
      history = true// work only with history
      ready = historyOrderSelectWeak(ticket);
      return ready// readiness is determined by the presence of a ticket in the history
   }
};

Cada una de ellas busca un ticket sólo en su zona. Basándose en estos monitores, ya puede crear clases de caché.

// TradeState.mqh
class OrderStatepublic TradeBaseState<ActiveOrderMonitor,
   ENUM_ORDER_PROPERTY_INTEGER,
   ENUM_ORDER_PROPERTY_DOUBLE,
   ENUM_ORDER_PROPERTY_STRING>
{
public:
   OrderState(const long t): TradeBaseState(t) { }
};
   
class HistoryOrderStatepublic TradeBaseState<HistoryOrderMonitor,
   ENUM_ORDER_PROPERTY_INTEGER,
   ENUM_ORDER_PROPERTY_DOUBLE,
   ENUM_ORDER_PROPERTY_STRING>
{
public:
   HistoryOrderState(const long t): TradeBaseState(t) { }
};

El toque final que añadiremos a la clase TradeBaseState por comodidad es un método especial para convertir un valor de propiedad en una cadena. Aunque existen varias versiones de los métodos stringify en el monitor, todos ellos «imprimirán» valores de la caché (si la variable miembro cached es igual a true) o valores del objeto original del entorno de trading (si cached es igual a false). Para visualizar las diferencias entre la caché y el objeto modificado (cuando dichas diferencias se encuentran), necesitamos leer simultáneamente el valor de la caché y pasar por alto la caché. En este sentido, añadimos el método stringifyRaw que siempre trabaja con la propiedad directamente (debido a que la variable cached se restablece y reinstala temporalmente).

   // get the string representation of the property 'i' bypassing the cache
   string stringifyRaw(const int i)
   {
      const bool previous = cached;
      cached = false;
      const string s = stringify(i);
      cached = previous;
   }

Vamos a comprobar el rendimiento del monitor de caché utilizando un ejemplo sencillo de un Asesor Experto que monitoriza el estado de una orden activa (OrderSnapshot.mq5). Más adelante desarrollaremos esta idea para almacenar en caché cualquier conjunto de órdenes, transacciones o posiciones, es decir, crearemos una caché completa.

El Asesor Experto intentará encontrar la última en la lista de órdenes activas y creará el objeto OrderState para ella. Si no hay órdenes, se pedirá al usuario que cree una orden o abra una posición (esto último se asocia a la colocación y ejecución de una orden en el mercado). En cuanto se encuentra una orden se comprueba si el estado de la misma ha cambiado. Esta comprobación se realiza en el manejador OnTrade. El Asesor Experto continuará supervisando esta orden hasta que se descargue.

int OnInit()
{
   if(OrdersTotal() == 0)
   {
      Alert("Please, create a pending order or open/close a position");
   }
   else
   {
      OnTrade(); // self-invocation
   }
   return INIT_SUCCEEDED;
}
   
void OnTrade()
{
   static int count = 0;
   // object pointer is stored in static AutoPtr
   static AutoPtr<OrderStateauto;
   // get a "clean" pointer (so as not to dereference auto[] everywhere)
   OrderState *state = auto[];
   
   PrintFormat(">>> OnTrade(%d)"count++);
   
   if(OrdersTotal() > 0 && state == NULL)
   {
      const ulong ticket = OrderGetTicket(OrdersTotal() - 1);
      auto = new OrderState(ticket);
      PrintFormat("Order picked up: %lld %s"ticket,
         auto[].isReady() ? "true" : "false");
      auto[].print(); // initial state at the time of "capturing" the order
   }
   else if(state)
   {
      int changes[];
      if(state.getChanges(changes))
      {
         Print("Order properties changed:");
         ArrayPrint(changes);
         ...
      }
      if(_LastError != 0Print(E2S(_LastError));
   }
}

Además de mostrar un array de propiedades modificadas, estaría bien mostrar los cambios en sí. Por lo tanto, en lugar de una elipsis, añadiremos un fragmento de este tipo (nos será útil en futuras clases de cachés completas).

         for(int k = 0k < ArraySize(changes); ++k)
         {
            switch(OrderState::TradeState::type(changes[k]))
            {
            case PROP_TYPE_INTEGER:
               Print(EnumToString((ENUM_ORDER_PROPERTY_INTEGER)changes[k]), ": ",
                  state.stringify(changes[k]), " -> ",
                  state.stringifyRaw(changes[k]));
                  break;
            case PROP_TYPE_DOUBLE:
               Print(EnumToString((ENUM_ORDER_PROPERTY_DOUBLE)changes[k]), ": ",
                  state.stringify(changes[k]), " -> ",
                  state.stringifyRaw(changes[k]));
                  break;
            case PROP_TYPE_STRING:
               Print(EnumToString((ENUM_ORDER_PROPERTY_STRING)changes[k]), ": ",
                  state.stringify(changes[k]), " -> ",
                  state.stringifyRaw(changes[k]));
                  break;
            }
         }

Aquí utilizamos el nuevo método stringifyRaw. Después de visualizar los cambios, no olvide actualizar el estado de la caché.

         state.update();

Si ejecuta el Asesor Experto en una cuenta sin órdenes activas y coloca una nueva, verá las siguientes entradas en el registro (aquí buy limit para EURUSD se crea por debajo del precio de mercado actual).

Alert: Please, create a pending order or open/close a position

>>> OnTrade(0)

Order picked up: 1311736135 true

MonitorInterface<ENUM_ORDER_PROPERTY_INTEGER,ENUM_ORDER_PROPERTY_DOUBLE,ENUM_ORDER_PROPERTY_STRING>

ENUM_ORDER_PROPERTY_INTEGER Count=14

0 ORDER_TIME_SETUP=2022.04.11 11:42:39

1 ORDER_TIME_EXPIRATION=1970.01.01 00:00:00

2 ORDER_TIME_DONE=1970.01.01 00:00:00

3 ORDER_TYPE=ORDER_TYPE_BUY_LIMIT

4 ORDER_TYPE_FILLING=ORDER_FILLING_RETURN

5 ORDER_TYPE_TIME=ORDER_TIME_GTC

6 ORDER_STATE=ORDER_STATE_STARTED

7 ORDER_MAGIC=0

8 ORDER_POSITION_ID=0

9 ORDER_TIME_SETUP_MSC=2022.04.11 11:42:39'729

10 ORDER_TIME_DONE_MSC=1970.01.01 00:00:00'000

11 ORDER_POSITION_BY_ID=0

12 ORDER_TICKET=1311736135

13 ORDER_REASON=ORDER_REASON_CLIENT

ENUM_ORDER_PROPERTY_DOUBLE Count=7

0 ORDER_VOLUME_INITIAL=0.01

1 ORDER_VOLUME_CURRENT=0.01

2 ORDER_PRICE_OPEN=1.087

3 ORDER_PRICE_CURRENT=1.087

4 ORDER_PRICE_STOPLIMIT=0.0

5 ORDER_SL=0.0

6 ORDER_TP=0.0

ENUM_ORDER_PROPERTY_STRING Count=3

0 ORDER_SYMBOL=EURUSD

1 ORDER_COMMENT=

2 ORDER_EXTERNAL_ID=

>>> OnTrade(1)

Order properties changed:

10 14

ORDER_PRICE_CURRENT: 1.087 -> 1.09073

ORDER_STATE: ORDER_STATE_STARTED -> ORDER_STATE_PLACED

>>> OnTrade(2)

>>> OnTrade(3)

>>> OnTrade(4)

Aquí puede ver cómo el estado de la orden ha cambiado de INICIADA a CURSADA. Si, en lugar de una orden pendiente, hemos abierto en el mercado con un volumen pequeño, es posible que no tengamos tiempo de recibir estos cambios, porque tales órdenes, por regla general, se establecen muy rápidamente, y su estado observado cambia de INICIADO inmediatamente a RELLENADO. Y esto último ya significa que la orden ha pasado al historial. Por lo tanto, es necesario un seguimiento paralelo de historias para rastrearlas. Lo mostraremos en el siguiente ejemplo.

Tenga en cuenta que puede haber muchos eventos OnTrade pero no todos están relacionados con nuestra orden.

Intentemos establecer el nivel Take Profit y comprobemos el registro.

>>> OnTrade(5)
Order properties changed:
10 13
ORDER_PRICE_CURRENT: 1.09073 -> 1.09079
ORDER_TP: 0.0 -> 1.097
>>> OnTrade(6)
>>> OnTrade(7)

A continuación, cambie la fecha de vencimiento: de GTC a un día.

>>> OnTrade(8)
Order properties changed:
10
ORDER_PRICE_CURRENT: 1.09079 -> 1.09082
>>> OnTrade(9)
>>> OnTrade(10)
Order properties changed:
2 6
ORDER_TIME_EXPIRATION: 1970.01.01 00:00:00 -> 2022.04.11 00:00:00
ORDER_TYPE_TIME: ORDER_TIME_GTC -> ORDER_TIME_DAY
>>> OnTrade(11)

Aquí, en el proceso de cambio de nuestra orden, el precio tuvo tiempo suficiente de cambiar, y por lo tanto hemos «enganchado» una notificación intermedia sobre el nuevo valor en ORDER_PRICE_CURRENT. Y sólo después de eso, los cambios esperados en ORDER_TYPE_TIME y ORDER_TIME_EXPIRATION entraron en el registro.

A continuación, eliminamos la orden.

>>> OnTrade(12)
TRADE_ORDER_NOT_FOUND

Ahora, para cualquier acción con la cuenta que lleve a eventos OnTrade, nuestro Asesor Experto mostrará TRADE_ORDER_NOT_FOUND, ya que está diseñado para seguir una sola orden. Si el Asesor Experto se reinicia, «atrapará» otra orden si la hay. Pero dejaremos el Asesor Experto y empezaremos a prepararnos para una tarea más urgente.

Por regla general, el almacenamiento en caché y el control de los cambios no son necesarios para una sola orden o posición, sino para todas o un conjunto de ellas, seleccionadas en función de determinadas condiciones. Para ello, desarrollaremos una clase de plantilla base TradeCache (TradeCache.mqh) y, a partir de ella, crearemos clases aplicadas para listas de órdenes, transacciones y posiciones.

template<typename T,typename F,typename E>
class TradeCache
{
   AutoPtr<Tdata[];
   const E property;
   const int NOT_FOUND_ERROR;
   
public:
   TradeCache(const E idconst int error): property(id), NOT_FOUND_ERROR(error) { }
   
   virtual string rtti() const
   {
      return typename(this); // will be redefined in derived classes for visual output to the log
   }
   ...

En esta plantilla, la letra T indica una de las clases de la familia TradeState. Como puede ver, se reserva un array de tales objetos en forma de punteros automáticos con el nombre data.

La letra F describe el tipo de una de las clases de filtro (OrderFilter.mqh, incluyendo HistoryOrderFilter, DealFilter.mqh, PositionFilter.mqh) utilizado para seleccionar elementos en caché. En el caso más sencillo, cuando el filtro no contiene condiciones let, todos los elementos se almacenarán en caché (con respecto al historial de muestreo para objetos del historial).

La letra E corresponde a la enumeración en la que se encuentra la property que identifica los objetos. Dado que esta propiedad suele ser SOME_TICKET, se supone que la enumeración es un entero ENUM_SOMETHING_PROPERTY_INTEGER.

La variable NOT_FOUND_ERROR está destinada al código de error que se produce al intentar asignar un objeto inexistente para su lectura, por ejemplo, ERR_TRADE_POSITION_NOT_FOUND para posiciones.

En parámetros, el método de la clase principal scan recibe una referencia al filtro configurado (debe ser configurado por el código llamante).

   void scan(F &f)
   {
      const int existedBefore = ArraySize(data);
      
      ulong tickets[];
      ArrayResize(ticketsexistedBefore);
      for(int i = 0i < existedBefore; ++i)
      {
         tickets[i] = data[i][].get(property);
      }
      ...

Al principio del método recopilamos los identificadores de los objetos ya almacenados en caché en el array tickets. Obviamente, en la primera ejecución, éste estará vacío.

A continuación, rellenamos el array objects con entradas de objetos relevantes utilizando un filtro. Para cada nuevo ticket, creamos un objeto monitor de caché T y lo añadimos al array data. Para los objetos antiguos, analizamos la presencia de cambios llamando a data[j][].getChanges(changes) y luego actualizamos la caché llamando a data[j][].update().

      ulong objects[];
      f.select(objects);
      for(int i = 0ji < ArraySize(objects); ++i)
      {
         const ulong ticket = objects[i];
         for(j = 0j < existedBefore; ++j)
         {
            if(tickets[j] == ticket)
            {
               tickets[j] = 0// mark as found
               break;
            }
         }
         
         if(j == existedBefore// this is not in the cache, you need to add
         {
            const T *ptr = new T(ticket);
            PUSH(dataptr);
            onAdded(*ptr);
         }
         else
         {
            ResetLastError();
            int changes[];
            if(data[j][].getChanges(changes))
            {
               onUpdated(data[j][], changes);
               data[j][].update();
            }
            if(_LastErrorPrintFormat("%s: %lld (%s)"rtti(), ticketE2S(_LastError));
         }
      }
      ...

Como puede ver, en cada fase del cambio, es decir, cuando se añade un objeto o después de modificarlo, se llama a los métodos onAdded y onUpdated. Se trata de métodos stub virtuales que el escáner puede utilizar para notificar al programa los eventos apropiados. Se espera que el código de la aplicación implemente una clase derivada con versiones anuladas de estos métodos. Abordaremos esta cuestión un poco más adelante, pero por ahora seguiremos considerando el método scan.

En el bucle anterior, todos los tickets encontrados en el array tickets se ponen a cero y, por lo tanto, los elementos restantes corresponden a los objetos que faltan del entorno de trading. A continuación, se comprueban llamando a getChanges y comparando el código de error con NOT_FOUND_ERROR. Si esto es cierto, se llama al método virtual onRemoved. Devuelve una bandera booleana (proporcionada por el código de la aplicación) que indica si el elemento debe eliminarse de la caché.

      for(int j = 0j < existedBefore; ++j)
      {
         if(tickets[j] == 0continue// skip processed elements
         
         // this ticket was not found, most likely deleted
         int changes[];
         ResetLastError();
         if(data[j][].getChanges(changes))
         {
            if(_LastError == NOT_FOUND_ERROR// for example, ERR_TRADE_POSITION_NOT_FOUND
            {
               if(onRemoved(data[j][]))
               {
                  data[j] = NULL;             // release the object and array element
               }
               continue;
            }
            
            // NB! Usually we shouldn't fall here
            PrintFormat("Unexpected ticket: %lld (%s) %s"tickets[j],
               E2S(_LastError), rtti());
            onUpdated(data[j][], changestrue);
            data[j][].update();
         }
         else
         {
            PrintFormat("Orphaned element: %lld (%s) %s"tickets[j],
               E2S(_LastError), rtti());
         }
      }
   }

Al final del método scan, el array data se despeja de elementos nulos, pero este fragmento se omite aquí por brevedad.

La clase base proporciona implementaciones estándar de los métodos onAdded, onRemoved y onUpdated que muestran la esencia de los eventos en el registro. Definiendo la macro PRINT_DETAILS en su código antes de incluir el archivo de encabezado TradeCache.mqh puede ordenar la impresión de todas las propiedades de cada objeto nuevo.

   virtual void onAdded(const T &state)
   {
      Print(rtti(), " added: "state.get(property));
      #ifdef PRINT_DETAILS
      state.print();
      #endif
   }
   
   virtual bool onRemoved(const T &state)
   {
      Print(rtti(), " removed: "state.get(property));
      return true// allow the object to be removed from the cache (false to save)
   }
   
   virtual void onUpdated(T &stateconst int &changes[],
      const bool unexpected = false)
   {
      ...
   }

No presentaremos el método onUpdated, ya que prácticamente repite el código para la salida de cambios del Asesor Experto OrderSnapshot.mq5 mostrado anteriormente.

Por supuesto, la clase base tiene facilidades para obtener el tamaño de la caché y acceder a un objeto específico por número.

   int size() const
   {
      return ArraySize(data);
   }
   
   T *operator[](int iconst
   {
      return data[i][]; // return pointer (T*) from AutoPtr object
   }

A partir de la clase base TradeCache podemos crear fácilmente ciertas clases para almacenar en caché listas de posiciones, órdenes activas y órdenes del historial. El almacenamiento en caché de las transacciones se deja como tarea independiente.

class PositionCachepublic TradeCache<PositionState,PositionFilter,
   ENUM_POSITION_PROPERTY_INTEGER>
{
public:
   PositionCache(const ENUM_POSITION_PROPERTY_INTEGER selector = POSITION_TICKET,
      const int error = ERR_TRADE_POSITION_NOT_FOUND): TradeCache(selectorerror) { }
};
   
class OrderCachepublic TradeCache<OrderState,OrderFilter,
   ENUM_ORDER_PROPERTY_INTEGER>
{
public:
   OrderCache(const ENUM_ORDER_PROPERTY_INTEGER selector = ORDER_TICKET,
      const int error = ERR_TRADE_ORDER_NOT_FOUND): TradeCache(selectorerror) { }
};
   
class HistoryOrderCachepublic TradeCache<HistoryOrderState,HistoryOrderFilter,
   ENUM_ORDER_PROPERTY_INTEGER>
{
public:
   HistoryOrderCache(const ENUM_ORDER_PROPERTY_INTEGER selector = ORDER_TICKET,
      const int error = ERR_TRADE_ORDER_NOT_FOUND): TradeCache(selectorerror) { }
};

Para resumir el proceso de desarrollo de la funcionalidad presentada, ofrecemos un diagrama de las clases principales. Se trata de una versión simplificada de los diagramas de UML que puede resultar útil a la hora de diseñar programas complejos en MQL5.

Diagrama de clases de monitores, filtros y cachés de objetos de trading

Diagrama de clases de monitores, filtros y cachés de objetos de trading

Las plantillas se marcan en amarillo, las clases abstractas se dejan en blanco y ciertas implementaciones se muestran en color. Las flechas sólidas con puntas rellenas indican herencia, y las flechas punteadas con puntas huecas indican clasificación de plantillas. Las flechas punteadas con puntas abiertas indican el uso de los métodos especificados entre sí por las clases. Las conexiones con diamantes son una composición (inclusión de unos objetos en otros).

Como ejemplo de uso de la caché, vamos a crear un Asesor Experto TradeSnapshot.mq5, que responderá a cualquier cambio en el entorno de trading desde el manejador OnTrade. Para el filtrado y el almacenamiento en caché, el código describe 6 objetos, 2 (filtro y caché) para cada tipo de elemento: posiciones, órdenes activas y órdenes históricas.

PositionFilter filter0;
PositionCache positions;
   
OrderFilter filter1;
OrderCache orders;
   
HistoryOrderFilter filter2;
HistoryOrderCache history;

No se establecen condiciones para los filtros a través de las llamadas al método let para que todos los objetos en línea descubiertos entren en la caché. Existe un ajuste adicional para las órdenes del historial.

Opcionalmente, al inicio, puede cargar en la caché órdenes anteriores con una profundidad de historial determinada. Esto puede hacerse a través de la variable de entrada HistoryLookup. En esta variable, puede seleccionar el último día, la última semana (por duración, no por calendario), el mes (30 días) o el año (360 días). Por defecto, el historial pasado no se carga (más concretamente, sólo se carga en 1 segundo). Dado que la macro PRINT_DETAILS está definida en el Asesor Experto, tenga cuidado con las cuentas con un gran historial: pueden generar un gran registro si no se limita el periodo.

enum ENUM_HISTORY_LOOKUP
{
   LOOKUP_NONE = 1,
   LOOKUP_DAY = 86400,
   LOOKUP_WEEK = 604800,
   LOOKUP_MONTH = 2419200,
   LOOKUP_YEAR = 29030400,
   LOOKUP_ALL = 0,
};
   
input ENUM_HISTORY_LOOKUP HistoryLookup = LOOKUP_NONE;
   
datetime origin;

En el manejador OnInit, reiniciamos las cachés (en caso de que el Asesor Experto se reinicie con nuevos parámetros), calculamos la fecha de inicio del historial en la variable origin, y llamamos a OnTrade por primera vez.

int OnInit()
{
   positions.reset();
   orders.reset();
   history.reset();
   origin = HistoryLookup ? TimeCurrent() - HistoryLookup : 0;
   
   OnTrade(); // self start
   return INIT_SUCCEEDED;
}

El manejador OnTrade es minimalista, ya que todas las complejidades están ahora ocultas dentro de las clases.

void OnTrade()
{
   static int count = 0;
   
   PrintFormat(">>> OnTrade(%d)"count++);
   positions.scan(filter0);
   orders.scan(filter1);
   // make a history selection just before using the filter
   // inside the 'scan' method
   HistorySelect(originLONG_MAX);
   history.scan(filter2);
   PrintFormat(">>> positions: %d, orders: %d, history: %d",
      positions.size(), orders.size(), history.size());
}

Inmediatamente después de lanzar el Asesor Experto en una cuenta limpia, veremos el siguiente mensaje:

>>> OnTrade(0)
>>> positions: 0, orders: 0, history: 0

Vamos a intentar ejecutar el caso de prueba más sencillo: vamos a comprar o vender en una cuenta «vacía» que no tiene posiciones abiertas ni órdenes pendientes. El registro incluirá los siguientes eventos (que ocurren casi instantáneamente).

En primer lugar, se detectará una orden activa.

>>> OnTrade(1)

OrderCache added: 1311792104

MonitorInterface<ENUM_ORDER_PROPERTY_INTEGER,ENUM_ORDER_PROPERTY_DOUBLE,ENUM_ORDER_PROPERTY_STRING>

ENUM_ORDER_PROPERTY_INTEGER Count=14

0 ORDER_TIME_SETUP=2022.04.11 12:34:51

1 ORDER_TIME_EXPIRATION=1970.01.01 00:00:00

2 ORDER_TIME_DONE=1970.01.01 00:00:00

3 ORDER_TYPE=ORDER_TYPE_BUY

4 ORDER_TYPE_FILLING=ORDER_FILLING_FOK

5 ORDER_TYPE_TIME=ORDER_TIME_GTC

6 ORDER_STATE=ORDER_STATE_STARTED

7 ORDER_MAGIC=0

8 ORDER_POSITION_ID=0

9 ORDER_TIME_SETUP_MSC=2022.04.11 12:34:51'096

10 ORDER_TIME_DONE_MSC=1970.01.01 00:00:00'000

11 ORDER_POSITION_BY_ID=0

12 ORDER_TICKET=1311792104

13 ORDER_REASON=ORDER_REASON_CLIENT

ENUM_ORDER_PROPERTY_DOUBLE Count=7

0 ORDER_VOLUME_INITIAL=0.01

1 ORDER_VOLUME_CURRENT=0.01

2 ORDER_PRICE_OPEN=1.09218

3 ORDER_PRICE_CURRENT=1.09218

4 ORDER_PRICE_STOPLIMIT=0.0

5 ORDER_SL=0.0

6 ORDER_TP=0.0

ENUM_ORDER_PROPERTY_STRING Count=3

0 ORDER_SYMBOL=EURUSD

1 ORDER_COMMENT=

2 ORDER_EXTERNAL_ID=

A continuación, esta orden pasará al historial (al mismo tiempo, cambiarán al menos el estado, el tiempo de ejecución y el ID de posición).

HistoryOrderCache added: 1311792104

MonitorInterface<ENUM_ORDER_PROPERTY_INTEGER,ENUM_ORDER_PROPERTY_DOUBLE,ENUM_ORDER_PROPERTY_STRING>

ENUM_ORDER_PROPERTY_INTEGER Count=14

0 ORDER_TIME_SETUP=2022.04.11 12:34:51

1 ORDER_TIME_EXPIRATION=1970.01.01 00:00:00

2 ORDER_TIME_DONE=2022.04.11 12:34:51

3 ORDER_TYPE=ORDER_TYPE_BUY

4 ORDER_TYPE_FILLING=ORDER_FILLING_FOK

5 ORDER_TYPE_TIME=ORDER_TIME_GTC

6 ORDER_STATE=ORDER_STATE_FILLED

7 ORDER_MAGIC=0

8 ORDER_POSITION_ID=1311792104

9 ORDER_TIME_SETUP_MSC=2022.04.11 12:34:51'096

10 ORDER_TIME_DONE_MSC=2022.04.11 12:34:51'097

11 ORDER_POSITION_BY_ID=0

12 ORDER_TICKET=1311792104

13 ORDER_REASON=ORDER_REASON_CLIENT

ENUM_ORDER_PROPERTY_DOUBLE Count=7

0 ORDER_VOLUME_INITIAL=0.01

1 ORDER_VOLUME_CURRENT=0.0

2 ORDER_PRICE_OPEN=1.09218

3 ORDER_PRICE_CURRENT=1.09218

4 ORDER_PRICE_STOPLIMIT=0.0

5 ORDER_SL=0.0

6 ORDER_TP=0.0

ENUM_ORDER_PROPERTY_STRING Count=3

0 ORDER_SYMBOL=EURUSD

1 ORDER_COMMENT=

2 ORDER_EXTERNAL_ID=

>>> positions: 0, orders: 1, history: 1

Obsérvese que estas modificaciones se han producido dentro de la misma llamada a OnTrade. En otras palabras: mientras nuestro programa analizaba las propiedades de la nueva orden (llamando a orders.scan), la orden era procesada por el terminal en paralelo, y para cuando se comprobó el historial (llamando a history.scan), ya había descendido en el historial. Por eso aparece tanto aquí como allí de acuerdo con la última línea de este fragmento de registro. Este comportamiento es normal en los programas multihilo y debe tenerse en cuenta a la hora de diseñarlos, pero no siempre tiene por qué ser así. Aquí simplemente llamamos la atención sobre ello. Cuando se ejecuta un programa MQL rápidamente, esta situación no suele ocurrir.

Si comprobáramos primero el historial y luego las órdenes en línea, en la primera fase podríamos encontrarnos con que la orden aún no está en el historial, y en la segunda fase con que la orden ya no está en línea. Es decir, teóricamente podría perderse por un momento. Una situación más realista es saltarse una orden en su fase activa debido a la sincronización del historial, es decir, fijarla inmediatamente por primera vez en el historial.

Recuerde que MQL5 no permite sincronizar el entorno de trading en su conjunto, sino sólo por partes:

  • Entre las órdenes activas, la información es relevante para la orden para la que se acaba de llamar a la función OrderSelect o OrderGetTicket.
  • Entre las posiciones, la información es relevante para la posición para la que se acaba de llamar a la función PositionSelect, PositionSelectByTicket o PositionGetTicket.
  • Para las órdenes y transacciones del historial, la información está disponible en el contexto de la última llamada de HistorySelect, HistorySelectByPosition HistoryOrderSelect. HistoryDealSelect

Además, recordemos que los eventos de trading (como cualquier evento MQL5) son mensajes sobre cambios que se han producido, puestos en cola, y recuperado de la cola de forma diferida, y no inmediatamente en el momento de los cambios. Además, el evento OnTrade se produce después de los eventos OnTradeTransaction relevantes.

Pruebe diferentes configuraciones del programa, depure y genere registros detallados para elegir el algoritmo más fiable para su sistema de trading.

Volvamos a nuestro registro. En la siguiente activación de OnTrade, la situación ya se ha arreglado: la caché de órdenes activas ha detectado la eliminación de la orden. Por el camino, la caché de posiciones vio una posición abierta.

>>> OnTrade(2)

PositionCache added: 1311792104

MonitorInterface<ENUM_POSITION_PROPERTY_INTEGER,ENUM_POSITION_PROPERTY_DOUBLE,ENUM_POSITION_PROPERTY_STRING>

ENUM_POSITION_PROPERTY_INTEGER Count=9

0 POSITION_TIME=2022.04.11 12:34:51

1 POSITION_TYPE=POSITION_TYPE_BUY

2 POSITION_MAGIC=0

3 POSITION_IDENTIFIER=1311792104

4 POSITION_TIME_MSC=2022.04.11 12:34:51'097

5 POSITION_TIME_UPDATE=2022.04.11 12:34:51

6 POSITION_TIME_UPDATE_MSC=2022.04.11 12:34:51'097

7 POSITION_TICKET=1311792104

8 POSITION_REASON=POSITION_REASON_CLIENT

ENUM_POSITION_PROPERTY_DOUBLE Count=8

0 POSITION_VOLUME=0.01

1 POSITION_PRICE_OPEN=1.09218

2 POSITION_PRICE_CURRENT=1.09214

3 POSITION_SL=0.00000

4 POSITION_TP=0.00000

5 POSITION_COMMISSION=0.0

6 POSITION_SWAP=0.00

7 POSITION_PROFIT=-0.04

ENUM_POSITION_PROPERTY_STRING Count=3

0 POSITION_SYMBOL=EURUSD

1 POSITION_COMMENT=

2 POSITION_EXTERNAL_ID=

OrderCache removed: 1311792104

>>> positions: 1, orders: 0, history: 1

Al cabo de un tiempo, cerramos la posición. Como en nuestro código la caché de posición se comprueba primero (positions.scan), los cambios en la posición cerrada se incluyen en el registro.

>>> OnTrade(8)
PositionCache changed: 1311792104
POSITION_PRICE_CURRENT: 1.09214 -> 1.09222
POSITION_PROFIT: -0.04 -> 0.04

Más adelante, en la misma llamada de OnTrade, detectamos la aparición de una orden de cierre y su transferencia instantánea al historial (de nuevo, debido a su rápido procesamiento paralelo por parte del terminal).

OrderCache added: 1311796883

MonitorInterface<ENUM_ORDER_PROPERTY_INTEGER,ENUM_ORDER_PROPERTY_DOUBLE,ENUM_ORDER_PROPERTY_STRING>

ENUM_ORDER_PROPERTY_INTEGER Count=14

0 ORDER_TIME_SETUP=2022.04.11 12:39:55

1 ORDER_TIME_EXPIRATION=1970.01.01 00:00:00

2 ORDER_TIME_DONE=1970.01.01 00:00:00

3 ORDER_TYPE=ORDER_TYPE_SELL

4 ORDER_TYPE_FILLING=ORDER_FILLING_FOK

5 ORDER_TYPE_TIME=ORDER_TIME_GTC

6 ORDER_STATE=ORDER_STATE_STARTED

7 ORDER_MAGIC=0

8 ORDER_POSITION_ID=1311792104

9 ORDER_TIME_SETUP_MSC=2022.04.11 12:39:55'710

10 ORDER_TIME_DONE_MSC=1970.01.01 00:00:00'000

11 ORDER_POSITION_BY_ID=0

12 ORDER_TICKET=1311796883

13 ORDER_REASON=ORDER_REASON_CLIENT

ENUM_ORDER_PROPERTY_DOUBLE Count=7

0 ORDER_VOLUME_INITIAL=0.01

1 ORDER_VOLUME_CURRENT=0.01

2 ORDER_PRICE_OPEN=1.09222

3 ORDER_PRICE_CURRENT=1.09222

4 ORDER_PRICE_STOPLIMIT=0.0

5 ORDER_SL=0.0

6 ORDER_TP=0.0

ENUM_ORDER_PROPERTY_STRING Count=3

0 ORDER_SYMBOL=EURUSD

1 ORDER_COMMENT=

2 ORDER_EXTERNAL_ID=

HistoryOrderCache added: 1311796883

MonitorInterface<ENUM_ORDER_PROPERTY_INTEGER,ENUM_ORDER_PROPERTY_DOUBLE,ENUM_ORDER_PROPERTY_STRING>

ENUM_ORDER_PROPERTY_INTEGER Count=14

0 ORDER_TIME_SETUP=2022.04.11 12:39:55

1 ORDER_TIME_EXPIRATION=1970.01.01 00:00:00

2 ORDER_TIME_DONE=2022.04.11 12:39:55

3 ORDER_TYPE=ORDER_TYPE_SELL

4 ORDER_TYPE_FILLING=ORDER_FILLING_FOK

5 ORDER_TYPE_TIME=ORDER_TIME_GTC

6 ORDER_STATE=ORDER_STATE_FILLED

7 ORDER_MAGIC=0

8 ORDER_POSITION_ID=1311792104

9 ORDER_TIME_SETUP_MSC=2022.04.11 12:39:55'710

10 ORDER_TIME_DONE_MSC=2022.04.11 12:39:55'711

11 ORDER_POSITION_BY_ID=0

12 ORDER_TICKET=1311796883

13 ORDER_REASON=ORDER_REASON_CLIENT

ENUM_ORDER_PROPERTY_DOUBLE Count=7

0 ORDER_VOLUME_INITIAL=0.01

1 ORDER_VOLUME_CURRENT=0.0

2 ORDER_PRICE_OPEN=1.09222

3 ORDER_PRICE_CURRENT=1.09222

4 ORDER_PRICE_STOPLIMIT=0.0

5 ORDER_SL=0.0

6 ORDER_TP=0.0

ENUM_ORDER_PROPERTY_STRING Count=3

0 ORDER_SYMBOL=EURUSD

1 ORDER_COMMENT=

2 ORDER_EXTERNAL_ID=

>>> positions: 1, orders: 1, history: 2

Ya hay dos órdenes en la caché del historial, pero las cachés de posición y de órdenes activas que se analizaron antes de la caché del historial aún no han aplicado estos cambios.

No obstante, en el siguiente evento OnTrade, vemos que la posición está cerrada, y la orden de mercado ha desaparecido.

>>> OnTrade(9)
PositionCache removed: 1311792104
OrderCache removed: 1311796883
>>> positions: 0, orders: 0, history: 2

Si monitorizamos las cachés en cada tick (o una vez por segundo, pero no sólo para eventos OnTrade), veremos cambios en las propiedades ORDER_PRICE_CURRENT y POSITION_PRICE_CURRENT sobre la marcha. POSITION_PROFIT también cambiará.

Nuestras clases no tienen persistence, es decir, viven sólo en RAM y no saben cómo guardar y restaurar su estado en ningún almacenamiento a largo plazo, como archivos. Esto significa que el programa puede pasar por alto un cambio ocurrido entre sesiones de terminal. Si necesita esa funcionalidad, deberá implementarla usted mismo. En el futuro, en la Parte 7 del libro, veremos el soporte de base de datos SQLite integrado en MQL5, que proporciona la forma más eficiente y conveniente para almacenar la caché del entorno de trading y datos tabulares similares.