Trading con calendario económico

Existen muchas estrategias de trading de noticias: con órdenes de mercado o pendientes, con análisis de indicadores financieros (la dirección del movimiento de los precios) y sin él (captura de la volatilidad). Además, es útil insertar un filtro antinoticias en muchos otros sistemas de trading. Es difícil optimizar y depurar todos estos programas, ya que el calendario de MQL5 no está disponible en el probador. Sin embargo, con la ayuda de la caché desarrollada en la sección anterior, podemos corregir la situación.

Intentemos crear un Asesor Experto que entre en el mercado cuando se publiquen noticias, de acuerdo con la evaluación de su impacto en el precio. El archivo de caché «xyz.cal» acaba de crearse con el indicador CalendarMonitorCached.mq5.

Recordemos que la imagen del calendario en la caché corresponde siempre al momento en que se guarda y requiere precaución a la hora de leerlo: para los eventos posteriores, los indicadores reales son desconocidos, y los eventos más lejanos pueden no existir en absoluto. Debe actualizar regularmente el archivo de caché del calendario antes de la siguiente optimización o simulación.
 
Si es necesario, tenga también en cuenta los ajustes de la hora DST durante el año: si el modo DST de los eventos es diferente del DST en el momento en que se guardó el archivo de calendario, tendrá que retrasar o adelantar la hora en 1 hora. Puede evitar estas dificultades eligiendo un bróker sin DST o construyendo una estrategia en marcos temporales superiores a H1.

El Asesor Experto CalendarTrading.mq5 solo negociará los eventos de noticias que:

  • hagan referencia al símbolo de trabajo del gráfico;
  • tengan el tipo de indicador financiero (es decir, cuantitativo);
  • sean de gran importancia;
  • acaben de recibir el valor actual del indicador.

Esto último es importante porque para los indicadores que tienen valores previstos y reales, el sistema establece el valor del campo impact_type en consecuencia: servirá como señal de trading (indicará la dirección de entrada en el mercado).

La hora exacta de la publicación de la noticia, por regla general, no coincide con la hora prevista introducida en el campo MqlCalendarValue::time. El calendario no registra esta hora, y no está disponible en la caché. En este sentido, la precisión de la simulación de estrategias de noticias puede verse afectada. Si desea acercar el análisis y la toma de decisiones a un proceso en línea, acumule estadísticas de publicación de noticias mediante un servicio como CalendarChangeSaver.mq5 e incrustarlo en la caché.

De manera predeterminada, el trading se realiza con un lote mínimo, con niveles de Take Profit y Stop Loss fijados a una distancia determinada en puntos. Todo esto se refleja en los parámetros de entrada.

input double Volume;               // Volume (0 = minimal lot)
input int Distance2SLTP = 500;     // Distance to SL/TP in points (0 = no)
input uint MultiplePositions = 25;

Para las cuentas de cobertura, permitimos la existencia simultánea de varias posiciones, el valor predeterminado es 25. Este es el entorno de simulación recomendado porque le permite evaluar de forma independiente la rentabilidad del trading paralelo en noticias de distintos tipos (cada posición se crea de forma independiente y no conlleva el cierre de posiciones en otras noticias). Por otra parte, mantener una sola posición nivela automáticamente las señales contradictorias de noticias diferentes.

Opcionalmente, el Asesor Experto admite filtros para el identificador del tipo de noticia y texto para la búsqueda por título.

sinput ulong EventID;
sinput string Text;

Esto puede ser útil para futuras investigaciones sobre noticias concretas.

A nivel global, los punteros de los objetos se describen mediante el tratamiento analítico de las noticias y el seguimiento de la posición.

AutoPtr<CalendarFilterfptr;
AutoPtr<CalendarCachecache;
AutoPtr<TrailingStoptrailing[];

El modo de funcionamiento y el par de divisas del símbolo de trabajo actual se almacenan en las variables correspondientes. Para simplificar el ejemplo, se supone que se utiliza en Forex (en otros mercados, el trading se realizará en una sola divisa: la divisa de cotización del ticker).

const bool Hedging =
   AccountInfoInteger(ACCOUNT_MARGIN_MODE) == ACCOUNT_MARGIN_MODE_RETAIL_HEDGING;
const string Base = SymbolInfoString(_SymbolSYMBOL_CURRENCY_BASE);
const string Profit = SymbolInfoString(_SymbolSYMBOL_CURRENCY_PROFIT);

En el manejador OnInit, cargamos la caché del calendario y configuramos los filtros como se ha descrito anteriormente. La ausencia de caché está permitida en el gráfico en línea: entonces el Asesor Experto trabaja en modo de combate, directamente con el calendario. En el probador, la ausencia de un archivo de caché impedirá que se inicie el Asesor Experto.

int OnInit()
{
   cache = new CalendarCache("xyz.cal"true);
   if(cache[].isLoaded())
   {
      fptr = new CalendarFilterCached(cache[]);
   }
   else
   {
      if(!MQLInfoInteger(MQL_TESTER))
      {
         Print("Calendar cache file not found, fall back to online mode");
         fptr = new CalendarFilter();
      }
      else
      {
         Print("Can't proceed in the tester without calendar cache file");
         return INIT_FAILED;
      }
   }
   CalendarFilter *f = fptr[];
   
   if(!f.isLoaded()) return INIT_FAILED;
   
   // if a specific type of event is set, we look only at it
   if(EventID > 0f.let(EventID);
   else
   {
      // otherwise follow the news on the currencies of the current symbol
      f.let(Base);
      if(Base != Profit)
      {
         f.let(Profit);
      }
      
      // financial indicators, high importance, actual value
      f.let(CALENDAR_TYPE_INDICATOR);
      f.let(LONG_MINCALENDAR_PROPERTY_RECORD_FORECASTNOT_EQUAL);
      f.let(CALENDAR_IMPORTANCE_HIGH);
   
      if(StringLen(Text)) f.let(Text);
   }
   
   f.describe();
   
   if(Distance2SLTP)
   {
      ArrayResize(trailingHedging && MultiplePositions ? MultiplePositions : 1);
   }
   // check the news filter and start trading on it by a second timer
   EventSetTimer(1);
   return INIT_SUCCEEDED;
}

En el manejador OnTimer solicitamos cambios en las noticias según los filtros configurados.

void OnTimer()
{
   CalendarFilter *f = fptr[];
   MqlCalendarValue records[];
   
   f.let(TimeTradeServer() - SCOPE_DAYTimeTradeServer() + SCOPE_DAY);
   
   if(f.update(records)) // find changes that undergo filtering
   {
      // output properties of changed news to the log
      static const ENUM_CALENDAR_PROPERTY props[] =
      {
         CALENDAR_PROPERTY_RECORD_TIME,
         CALENDAR_PROPERTY_COUNTRY_CURRENCY,
         CALENDAR_PROPERTY_COUNTRY_CODE,
         CALENDAR_PROPERTY_EVENT_NAME,
         CALENDAR_PROPERTY_EVENT_IMPORTANCE,
         CALENDAR_PROPERTY_RECORD_ACTUAL,
         CALENDAR_PROPERTY_RECORD_FORECAST,
         CALENDAR_PROPERTY_RECORD_PREVISED,
         CALENDAR_PROPERTY_RECORD_IMPACT,
      };
      static const int p = ArraySize(props);
      string result[];
      f.format(recordspropsresult);
      for(int i = 0i < ArraySize(result) / p; ++i)
      {
         Print(SubArrayCombine(result" | "i * pp));
      }
      ...

Cuando se detectan los cambios adecuados, se registran de la siguiente manera (a continuación se muestra un fragmento del registro real), indicando la hora, la divisa, el país, el nombre, los valores actual y previsto, el valor anterior y la interpretación teórica de la señal:

...

Filtering 5 records

2021.02.16 13:00 | EUR | EU | Employment Change q/q | HIGH | +0.3 | -0.4 | +1.0 | POSITIVE

2021.02.16 13:00 | EUR | EU | GDP q/q | HIGH | -0.6 | -0.7 | -0.7 | POSITIVE

instant buy 0.01 EURUSD at 1.21638 sl: 1.21138 tp: 1.22138 (1.21637 / 1.21638 / 1.21637)

deal #64 buy 0.01 EURUSD at 1.21638 done (based on order #64)

...

Filtering 3 records

2021.07.06 12:05 | EUR | DE | ZEW Economic Sentiment Indicator | HIGH | +63.3 | +84.1 | +79.8 | NEGATIVE

instant sell 0.01 EURUSD at 1.18473 sl: 1.18973 tp: 1.17973 (1.18473 / 1.18474 / 1.18473)

deal #265 sell 0.01 EURUSD at 1.18473 done (based on order #265)

...

El impacto potencial de la noticia sobre el precio debe calcularse basándose en la evaluación sobre el terreno impact_type. Es importante señalar aquí que tenemos dos divisas: base y cotización. Cuando la noticia tiene un efecto positivo sobre la divisa base, se espera que el tipo suba, y si es negativo, el tipo bajará. Para la divisa cotizada, ocurre lo contrario: un efecto positivo debería aumentar el precio de la segunda divisa del par, lo que significa una disminución del tipo de cambio, mientras que uno negativo conduce a su aumento. Esta dirección normalizada del movimiento del precio se calcula en el siguiente fragmento utilizando la variable sign.

      static const int impacts[3] = {0, +1, -1};
      int impact = 0;
      string about = "";
      ulong lasteventid = 0;
      for(int i = 0i < ArraySize(records); ++i)
      {
         int sign = result[i * p + 1] == Profit ? -1 : +1;
         impact += sign * impacts[records[i].impact_type];
         about += StringFormat("%+lld "sign * (long)records[i].event_id);
         lasteventid = records[i].event_id;
      }
      
      if(impact == 0return// no signal
      ...

A menudo las noticias aparecen publicadas al mismo tiempo, por lo que es necesario acumular las valoraciones de todas ellas. Esto se hace en la variable impact. Dado que nuestra estrategia solo filtra las noticias de mayor importancia, simplemente se suman todas las señales individuales de las mismas, sin coeficientes de ponderación. La variable de cadena about se utiliza para preparar el texto del comentario sobre el próximo acuerdo: en él se mencionarán los identificadores de los eventos que han dado lugar a la transacción.

Si el robot se lanza en una cuenta de compensación o se ha alcanzado el número máximo de posiciones permitido, cerraremos una.

      PositionFilter positions;
      ulong tickets[];
      positions.let(POSITION_SYMBOL_Symbol).select(tickets);
      const int n = ArraySize(tickets);
      
      if(n >= (int)(Hedging ? MultiplePositions : 1))
      {
         MqlTradeRequestSync position;
         position.close(_Symbol) && position.completed();
      }
      ...

Ahora puede abrir una nueva posición en una señal. Se establece un identificador de evento como número «mágico», que nos permitirá analizar posteriormente el rendimiento financiero del trading en el contexto de distintos tipos de noticias.

      MqlTradeRequestSync request;
      request.magic = lasteventid;
      request.comment = about;
      const double ask = SymbolInfoDouble(_SymbolSYMBOL_ASK);
      const double bid = SymbolInfoDouble(_SymbolSYMBOL_BID);
      const double point = SymbolInfoDouble(_SymbolSYMBOL_POINT);
      ulong ticket = 0;
      
      if(impact > 0)
      {
         ticket = request.buy(Lot0,
            Distance2SLTP ? ask - point * Distance2SLTP : 0,
            Distance2SLTP ? ask + point * Distance2SLTP : 0);
      }
      else if(impact < 0)
      {
         ticket = request.sell(Lot0,
            Distance2SLTP ? bid + point * Distance2SLTP : 0,
            Distance2SLTP ? bid - point * Distance2SLTP : 0);
      }
      
      if(ticket && request.completed() && Distance2SLTP)
      {
         for(int i = 0i < ArraySize(trailing); ++i)
         {
            if(trailing[i][] == NULL// looking for a free slot for the position tracking object
            {
               trailing[i] = new TrailingStop(ticketDistance2SLTPDistance2SLTP / 50);
               break;
            }
         }
      }
   }
}

Movemos los Stop Loss de todas las posiciones a la llegada de los ticks.

void OnTick()
{
   for(int i = 0i < ArraySize(trailing); ++i)
   {
      if(trailing[i][])
      {
         if(!trailing[i][].trail()) // position was closed
         {
            trailing[i] = NULL// release object and slot
         }
      }
   }
}

Ahora viene el punto más interesante. Gracias al probador es posible analizar el éxito de la estrategia de noticias no solo en general, sino también desglosado por noticias concretas. El bloque correspondiente se implementa en nuestro manejador OnTester. La recogida de datos se realiza mediante el filtro de transacciones. Tras recibir de él el array de tuplas trades, que informa sobre el beneficio, el swap, la comisión y el número mágico de cada operación, acumulamos los resultados en tres objetos de MapArray: calculan por separado los beneficios, las pérdidas y el número de operaciones para cada número de magic.

double OnTester()
{
   Print("Trade profits by calendar events:");
   HistorySelect(0LONG_MAX);
   DealFilter filter;
   int props[] = {DEAL_PROFITDEAL_SWAPDEAL_COMMISSIONDEAL_MAGIC};
   filter.let(DEAL_TYPE, (1 << DEAL_TYPE_BUY) | (1 << DEAL_TYPE_SELL), IS::OR_BITWISE)
      .let(DEAL_ENTRY, (1 << DEAL_ENTRY_OUT) | (1 << DEAL_ENTRY_INOUT) | (1 << DEAL_ENTRY_OUT_BY),
      IS::OR_BITWISE);
   Tuple4<doubledoubledoubleulongtrades[];
   MapArray<ulong,doubleprofits;
   MapArray<ulong,doublelosses;
   MapArray<ulong,intcounts;
   if(filter.select(propstrades))
   {
      for(int i = 0i < ArraySize(trades); ++i)
      {
         counts.inc((ulong)trades[i]._4);
         const double payout = trades[i]._1 + trades[i]._2 + trades[i]._3;
         if(payout >= 0)
         {
            profits.inc((ulong)trades[i]._4payout);
            losses.inc((ulong)trades[i]._40);
         }
         else
         {
            profits.inc((ulong)trades[i]._40);
            losses.inc((ulong)trades[i]._4payout);
         }
      }
      ...

Como resultado, obtenemos una tabla que muestra las estadísticas de cada tipo de evento línea por línea: su identificador, país, divisa, beneficio o pérdida total, número de operaciones (número de noticias), factor de beneficio y nombre del evento.

      for(int i = 0i < profits.getSize(); ++i)
      {
         MqlCalendarEvent event;
         MqlCalendarCountry country;
         const ulong keyId = profits.getKey(i);
         if(cache[].calendarEventById(keyIdevent)
            && cache[].calendarCountryById(event.country_idcountry))
         {
            PrintFormat("%lld %s %s %+.2f [%d] (PF:%.2f) %s",
               event.idcountry.codecountry.currency,
               profits[keyId] + losses[keyId], counts[keyId],
               profits[keyId] / (losses[keyId] != 0 ? -losses[keyId] : DBL_MIN),
               event.name);
         }
         else
         {
            Print("undefined "DoubleToString(profits.getValue(i), 2));
         }
      }
   }
   return 0;
}

Para probar la idea, vamos a ejecutar el Asesor Experto para el período comprendido entre principios de 2021 (hasta mediados de 2022) en el par EURUSD. A continuación se muestra un fragmento de un registro con una impresión de OnTester:

Trade profits by calendar events:
840040001 US USD -21.81 [17] (PF:0.53) ISM Manufacturing PMI
840190001 US USD -10.95 [17] (PF:0.69) ADP Nonfarm Employment Change
840200001 US USD -67.09 [78] (PF:0.60) EIA Crude Oil Stocks Change
999030003 EU EUR +14.13 [19] (PF:1.46) Retail Sales m/m
840040003 US USD -17.12 [18] (PF:0.59) ISM Non-Manufacturing PMI
840030016 US USD -1.20 [19] (PF:0.97) Nonfarm Payrolls
840030021 US USD +5.25 [14] (PF:1.21) JOLTS Job Openings
840020010 US USD -14.63 [17] (PF:0.63) Retail Sales m/m
276070001 DE EUR -22.71 [17] (PF:0.47) ZEW Economic Sentiment Indicator
840020005 US USD +10.76 [18] (PF:1.37) Building Permits
840120001 US USD -20.78 [17] (PF:0.49) Existing Home Sales
276030003 DE EUR +18.57 [17] (PF:1.87) Ifo Business Climate
840180002 US USD -3.22 [14] (PF:0.89) CB Consumer Confidence Index
840020014 US USD -8.74 [16] (PF:0.74) Core Durable Goods Orders m/m
840020008 US USD -14.54 [16] (PF:0.63) New Home Sales
250010005 FR EUR +0.66 [10] (PF:1.03) GDP q/q
840010007 US USD +0.99 [15] (PF:1.04) GDP q/q
840120003 US USD +4.53 [18] (PF:1.15) Pending Home Sales m/m
276010008 DE EUR -0.72 [10] (PF:0.97) GDP q/q
999030016 EU EUR -14.04 [14] (PF:0.59) GDP q/q
999030001 EU EUR +1.30 [2] (PF:1.35) Employment Change q/q

Los resultados no son muy impresionantes. Aun así, el trading de noticias está lleno de subjetividad. En primer lugar, las evaluaciones teóricas del impacto del valor real de la noticia en el curso pueden diferir de las expectativas emocionales de la multitud o de los antecedentes informativos adicionales (que quedan fuera del calendario y no son cuantificables). En segundo lugar, ya hemos mencionado la inexactitud del tiempo de publicación del valor real. En tercer lugar, nuestra estrategia se aplica de la forma más simple, sin analizar el movimiento preliminar de los precios (cuando probablemente hubo una filtración y la noticia se «agotó» antes).

En general, esta prueba reveló que los informes de nóminas no agrícolas o PIB favoritos de los operadores no garantizan el éxito, al menos no con nuestra configuración predeterminada. Además, es necesario, de la forma habitual, analizar las transacciones individuales, averiguar qué ha fallado, seleccionar parámetros y mejorar el algoritmo, en concreto, añadir un módulo de ajuste horario para cambiar el horario de verano en la zona horaria del servidor.

Al mismo tiempo, la técnica en sí funciona bien, y podemos simplemente intentar elegir las noticias más exitosas para empezar. Por ejemplo, tomemos la noticia 276030003 (Clima de Negocios de Ifo). Al configurarlo en EventID, recibiremos el siguiente informe, coincidente con nuestros indicadores calculados.

Informe sobre trading en el probador basado en noticias Clima de Negocios de Ifo

Informe sobre trading en el probador basado en noticias Clima de Negocios de Ifo

También puede intentar operar en un grupo de eventos similares. En concreto, para responder solo a las noticias sobre el PIB (de distintos países), introduzca la cadena «*PIB*» en la variable Text. Los asteriscos se añaden porque, sin ellos, una cadena de 3 caracteres será tratada como divisa por la clase de filtro. Las cadenas de cualquier longitud que no sean 2 (código de país) o 3 (código de divisa) pueden especificarse tal cual, por ejemplo, «granja», «no agrícola», «ventas»: el filtro las buscará como subcadenas de nombres, teniendo en cuenta mayúsculas y minúsculas.