Evento OnTrade

El evento OnTrade se produce al modificar la lista de órdenes y posiciones abiertas, el historial de órdenes y el historial de transacciones. Cualquier acción de trading (colocar/activar/eliminar una orden pendiente, abrir/cerrar una posición, establecer niveles de protección, etc.) modifica en consecuencia el historial de órdenes y transacciones y/o la lista de posiciones y órdenes actuales. El iniciador de una acción puede ser un usuario, un programa o un servidor.

Para recibir el evento en un programa, debe describir el manejador correspondiente.

void OnTrade(void)

En el caso del envío de solicitudes de trading mediante OrderSend/OrderSendAsync, una solicitud desencadenará múltiples eventos OnTrade, ya que el procesamiento suele tener lugar en varias etapas y cada operación puede cambiar el estado de las órdenes, las posiciones y el historial de trading.

En general, no existe una proporción exacta en el número de llamadas a OnTrade y OnTradeTransaction. OnTrade se llama después de las correspondientes llamadas a OnTradeTransaction.

Dado que el evento OnTrade es de naturaleza generalizada y no especifica la esencia de la operación, es menos popular entre los desarrolladores de programas MQL. Por lo general, es necesario comprobar todos los aspectos del estado de la cuenta de trading en el código y compararlo con algún estado guardado, es decir, con la caché aplicada de las entidades de trading utilizadas en la estrategia de trading. En el caso más sencillo, puede, por ejemplo, recordar el ticket de la orden creada en el manejador OnTrade para interrogar todas sus propiedades. Sin embargo, esto puede implicar el análisis «innecesario» de un gran número de eventos fortuitos que no están relacionados con una orden específica.

Hablaremos de la posibilidad de aplicar el almacenamiento en caché del entorno de trading y del historial en la sección sobre Asesores Expertos multidivisa.

Para explorar más a fondo OnTrade, veamos un Asesor Experto que implementa una estrategia sobre dos órdenes pendientes OCO («One Cancels Other»). Este colocará un par de órdenes stop de ruptura y esperará a que se active una de ellas, tras lo cual se eliminará la segunda. Para mayor claridad, proporcionaremos soporte para ambos tipos de eventos de trading, OnTrade y OnTradeTransaction, de forma que la lógica de trabajo se ejecutará desde un manejador u otro, según elija el usuario.

El código fuente está disponible en el archivo OCO2.mq5. Sus parámetros de entrada incluyen el tamaño del lote Volume (por defecto es 0, lo que significa mínimo), la distancia Distance2SLTP en puntos para colocar cada una de las órdenes y también determina los niveles de protección, la fecha de vencimiento Expiration en segundos desde la hora de configuración, y el conmutador de eventos ActivationBy (por defecto, OnTradeTransaction). Dado que Distance2SLTP establece tanto el desplazamiento desde el precio actual como la distancia hasta el Stop Loss, los Stop Loss de las dos órdenes son los mismos e iguales al precio en el momento de establecerlos.

enum EVENT_TYPE
{
   ON_TRANSACTION// OnTradeTransaction
   ON_TRADE        // OnTrade
};
   
input double Volume;            // Volume (0 - minimal lot)
input uint Distance2SLTP = 500// Distance Indent/SL/TP (points)
input ulong Magic = 1234567890;
input ulong Deviation = 10;
input ulong Expiration = 0;     // Expiration (seconds in future, 3600 - 1 hour, etc)
input EVENT_TYPE ActivationBy = ON_TRANSACTION;

Para simplificar la inicialización de las estructuras de solicitud, describiremos nuestra propia estructura MqlTradeRequestSyncOCO derivada de MqlTradeRequestSync.

struct MqlTradeRequestSyncOCOpublic MqlTradeRequestSync
{
   MqlTradeRequestSyncOCO()
   {
      symbol = _Symbol;
      magic = Magic;
      deviation = Deviation;
      if(Expiration > 0)
      {
         type_time = ORDER_TIME_SPECIFIED;
         expiration = (datetime)(TimeCurrent() + Expiration);
      }
   }
};

A nivel global, vamos a introducir varios objetos y variables.

OrderFilter orders;        // object for selecting orders
PositionFilter trades;     // object for selecting positions
bool FirstTick = false;    // or single processing of OnTick at start
ulong ExecutionCount = 0;  // counter of trading strategy calls RunStrategy()

Toda la lógica de trading, excepto el momento de inicio, se activará mediante eventos de trading. En el manejador OnInit configuramos los objetos de filtro y esperamos el primer tick (configuramos FirstTick en true).

int OnInit()
{
   FirstTick = true;
   
   orders.let(ORDER_MAGICMagic).let(ORDER_SYMBOL_Symbol)
      .let(ORDER_TYPE, (1 << ORDER_TYPE_BUY_STOP) | (1 << ORDER_TYPE_SELL_STOP),
      IS::OR_BITWISE);
   trades.let(POSITION_MAGICMagic).let(POSITION_SYMBOL_Symbol);
      
   return INIT_SUCCEEDED;
}

Sólo nos interesan las órdenes Stop (compra/venta) y las posiciones con un número mágico concreto y el símbolo actual.

En la función OnTick, llamamos una vez a la parte principal del algoritmo diseñado como RunStrategy (lo describiremos a continuación). Además, esta función sólo se llamará desde OnTrade o OnTradeTransaction.

void OnTick()
{
   if(FirstTick)
   {
      RunStrategy();
      FirstTick = false;
   }
}

Por ejemplo, cuando el modo OnTrade está activado, este fragmento funciona.

void OnTrade()
{
   static ulong count = 0;
   PrintFormat("OnTrade(%d)", ++count);
   if(ActivationBy == ON_TRADE)
   {
      RunStrategy();
   }
}

Tenga en cuenta que las llamadas al manejador OnTrade se cuentan independientemente de si la estrategia se activa aquí o no. Del mismo modo, los eventos relevantes se cuentan en el manejador OnTradeTransaction (incluso si ocurren en vano). Esto se hace para poder ver ambos eventos y sus contadores en el registro al mismo tiempo.

Cuando el modo OnTradeTransaction está activado, obviamente, RunStrategy empieza desde ahí.

void OnTradeTransaction(const MqlTradeTransaction &transaction,
   const MqlTradeRequest &request,
   const MqlTradeResult &result)
{
   static ulong count = 0;
   PrintFormat("OnTradeTransaction(%d)", ++count);
   Print(TU::StringOf(transaction));
   
   if(ActivationBy != ON_TRANSACTIONreturn;
   
   if(transaction.type == TRADE_TRANSACTION_ORDER_DELETE)
   {
      // why not here? for answer, see the text
      /* // this won't work online: m.isReady() == false because order temporarily lost
      OrderMonitor m(transaction.order);
      if(m.isReady() && m.get(ORDER_MAGIC) == Magic && m.get(ORDER_SYMBOL) == _Symbol)
      {
         RunStrategy();
      }
      */
   }
   else if(transaction.type == TRADE_TRANSACTION_HISTORY_ADD)
   {
      OrderMonitor m(transaction.order);
      if(m.isReady() && m.get(ORDER_MAGIC) == Magic && m.get(ORDER_SYMBOL) == _Symbol)
      {
         // the ORDER_STATE property does not matter - in any case, you need to remove the remaining
         // if(transaction.order_state == ORDER_STATE_FILLED
         // || transaction.order_state == ORDER_STATE_CANCELED ...)
         RunStrategy();
      }
   }
}

Debe tenerse en cuenta que cuando se negocia en línea, una orden pendiente activada puede desaparecer del entorno de negociación durante algún tiempo debido a que se transfiere de las existentes al historial. Cuando recibimos el evento TRADE_TRANSACTION_ORDER_DELETE, la orden ya ha sido eliminada de la lista activa pero aún no ha aparecido en el historial. Sólo llega cuando recibimos el evento TRADE_TRANSACTION_HISTORY_ADD. Este comportamiento no se observa en el probador, es decir, una orden eliminada se añade inmediatamente al historial y está disponible allí para seleccionar y leer propiedades ya en la fase TRADE_TRANSACTION_ORDER_DELETE.

En ambos manejadores de eventos de trading contamos y registramos el número de llamadas. Para el caso con OnTrade, este debe coincidir con ExecutionCount que pronto veremos dentro de RunStrategy. Sin embargo, para OnTradeTransaction, el contador y ExecutionCount diferirán significativamente porque la estrategia aquí se llama muy selectivamente, para un tipo de evento. Basándonos en esto, podemos concluir que OnTradeTransaction permite un uso más eficiente de los recursos al llamar al algoritmo sólo cuando es apropiado.

El contador ExecutionCount se envía al registro cuando se descarga el Asesor Experto.

void OnDeinit(const int r)
{
   Print("ExecutionCount = "ExecutionCount);
}

Por último, vamos a introducir la función RunStrategy. El contador prometido se incrementa al principio.

void RunStrategy()
{
   ExecutionCount++;
   ...

A continuación, se describen dos arrays para recibir los tickets de la orden y sus estados desde el objeto de filtro orders.

   ulong tickets[];
   ulong states[];

Para empezar, solicitaremos las órdenes que se ajusten a nuestras condiciones. Si son dos, todo va bien y no hay que hacer nada.

   orders.select(ORDER_STATEticketsstates);
   const int n = ArraySize(tickets);
   if(n == 2return// OK - standard state
   ...

Si queda una orden, entonces es que se ha activado la otra y hay que borrar la que queda.

   if(n > 0)          // 1 or 2+ orders is an error, you need to delete everything
   {
      // delete all matching orders, except for partially filled ones
      MqlTradeRequestSyncOCO r;
      for(int i = 0i < n; ++i)
      {
         if(states[i] != ORDER_STATE_PARTIAL)
         {
            r.remove(tickets[i]) && r.completed();
         }
      }
   }
   ...

De lo contrario, no hay órdenes. Por lo tanto, es necesario comprobar si hay alguna posición abierta: para ello, utilizamos otro objeto de filtro trades pero los resultados se añaden al mismo array receptor tickets. Si no hay posición, colocamos un nuevo par de órdenes.

   else // n == 0
   {
      // if there are no open positions, place 2 orders
      if(!trades.select(tickets))
      {
         MqlTradeRequestSyncOCO r;
         SymbolMonitor sm(_Symbol);
         
         const double point = sm.get(SYMBOL_POINT);
         const double lot = Volume == 0 ? sm.get(SYMBOL_VOLUME_MIN) : Volume;
         const double buy = sm.get(SYMBOL_BID) + point * Distance2SLTP;
         const double sell = sm.get(SYMBOL_BID) - point * Distance2SLTP;
         
         r.buyStop(lotbuybuy - Distance2SLTP * point,
            buy + Distance2SLTP * point) && r.completed();
         r.sellStop(lotsellsell + Distance2SLTP * point,
            sell - Distance2SLTP * point) && r.completed();
      }
   }
}

Vamos a ejecutar el Asesor Experto en el probador con la configuración por defecto, en el par EURUSD. En la siguiente imagen se muestra el proceso de simulación.

Experto con un par de órdenes Stop pendientes basadas en la estrategia OCO en el probador

Asesor Experto con un par de órdenes stop pendientes basadas en la estrategia OCO en el probador

En la fase de colocación de un par de órdenes, veremos las siguientes entradas en el registro:

buy stop 0.01 EURUSD at 1.11151 sl: 1.10651 tp: 1.11651 (1.10646 / 1.10683)

sell stop 0.01 EURUSD at 1.10151 sl: 1.10651 tp: 1.09651 (1.10646 / 1.10683)

OnTradeTransaction(1)

TRADE_TRANSACTION_ORDER_ADD, #=2(ORDER_TYPE_BUY_STOP/ORDER_STATE_PLACED), ORDER_TIME_GTC, EURUSD, »

» @ 1.11151, SL=1.10651, TP=1.11651, V=0.01

OnTrade(1)

OnTradeTransaction(2)

TRADE_TRANSACTION_REQUEST

OnTradeTransaction(3)

TRADE_TRANSACTION_ORDER_ADD, #=3(ORDER_TYPE_SELL_STOP/ORDER_STATE_PLACED), ORDER_TIME_GTC, EURUSD, »

» @ 1.10151, SL=1.10651, TP=1.09651, V=0.01

OnTrade(2)

OnTradeTransaction(4)

TRADE_TRANSACTION_REQUEST

En cuanto se activa una de las órdenes, ocurre lo siguiente:

order [#3 sell stop 0.01 EURUSD at 1.10151] triggered

deal #2 sell 0.01 EURUSD at 1.10150 done (based on order #3)

deal performed [#2 sell 0.01 EURUSD at 1.10150]

order performed sell 0.01 at 1.10150 [#3 sell stop 0.01 EURUSD at 1.10151]

OnTradeTransaction(5)

TRADE_TRANSACTION_DEAL_ADD, D=2(DEAL_TYPE_SELL), #=3(ORDER_TYPE_BUY/ORDER_STATE_STARTED), »

» EURUSD, @ 1.10150, SL=1.10651, TP=1.09651, V=0.01, P=3

OnTrade(3)

OnTradeTransaction(6)

TRADE_TRANSACTION_ORDER_DELETE, #=3(ORDER_TYPE_SELL_STOP/ORDER_STATE_FILLED), ORDER_TIME_GTC, »

» EURUSD, @ 1.10151, SL=1.10651, TP=1.09651, V=0.01, P=3

OnTrade(4)

OnTradeTransaction(7)

TRADE_TRANSACTION_HISTORY_ADD, #=3(ORDER_TYPE_SELL_STOP/ORDER_STATE_FILLED), ORDER_TIME_GTC, »

» EURUSD, @ 1.10151, SL=1.10651, TP=1.09651, P=3

order canceled [#2 buy stop 0.01 EURUSD at 1.11151]

OnTrade(5)

OnTradeTransaction(8)

TRADE_TRANSACTION_ORDER_DELETE, #=2(ORDER_TYPE_BUY_STOP/ORDER_STATE_CANCELED), ORDER_TIME_GTC, »

» EURUSD, @ 1.11151, SL=1.10651, TP=1.11651, V=0.01

OnTrade(6)

OnTradeTransaction(9)

TRADE_TRANSACTION_HISTORY_ADD, #=2(ORDER_TYPE_BUY_STOP/ORDER_STATE_CANCELED), ORDER_TIME_GTC, »

» EURUSD, @ 1.11151, SL=1.10651, TP=1.11651, V=0.01

OnTrade(7)

OnTradeTransaction(10)

TRADE_TRANSACTION_REQUEST

La orden nº 3 fue eliminada por sí misma, y la orden nº 2 fue eliminada (cancelada) por nuestro Asesor Experto.

Si ejecutamos el Asesor Experto con sólo el modo de funcionamiento a través del evento OnTrade cambiado en la configuración, deberíamos obtener resultados financieros completamente similares (ceteris paribus, es decir, por ejemplo, si no se incluyen los retrasos aleatorios en la generación de ticks). Lo único que será diferente es el número de llamadas a la función RunStrategy. Por ejemplo, para 4 meses de 2022 en EURUSD, H1 con 88 operaciones, obtendremos las siguientes métricas aproximadas de ExecutionCount (lo que importa es el ratio, no los valores absolutos asociados a los ticks de tu bróker):

  • OnTradeTransaction — 132
  • OnTrade — 438

Se trata de una prueba práctica de la posibilidad de construir algoritmos más selectivos basados en OnTradeTransaction en comparación con OnTrade.

Esta versión OCO2.mq5 del Asesor Experto reacciona a las acciones con órdenes y posiciones de forma bastante directa. En concreto, en cuanto se cierre la posición anterior mediante Stop Loss o Take Profit, colocará dos nuevas órdenes. Si elimina una de las órdenes manualmente, el Asesor Experto eliminará inmediatamente la segunda y volverá a crear un nuevo par con un desplazamiento respecto al precio actual. Usted puede mejorar el comportamiento mediante la incorporación de un programa similar a lo que se hace en el Asesor Experto de cuadrícula y no reaccionar a las órdenes canceladas en el historial (aunque, por favor, tenga en cuenta que MQL5 no proporciona medios para averiguar si una orden fue cancelada manualmente o mediante programación). Presentaremos una dirección diferente para mejorar este Asesor Experto cuando exploremos la API del calendario económico.

Además, en la versión actual ya está disponible un modo interesante, relacionado con el establecimiento de la fecha de vencimiento de las órdenes pendientes en la variable de entrada Expiration. Si un par de órdenes no se activa, inmediatamente después de su vencimiento se coloca un nuevo par en relación con el nuevo precio actual modificado. Como ejercicio independiente, puede intentar optimizar el Asesor Experto en el probador cambiando Expiration y Distance2SLTP. El trabajo programático con el probador, incluso en el modo de optimización, se abordará en el capítulo siguiente.

A continuación se muestra una de las opciones de ajuste (Distance2SLTP=250, Expiration=5000) encontradas durante un periodo de 16 meses desde principios de 2021 para el par EURUSD.

Resultados de las pruebas de ejecución del Asesor Experto OCO2

Resultados de las pruebas de ejecución del Asesor Experto OCO2