TradeObjects: Automatización del trading a base de objetos gráficos en MetaTrader

22 septiembre 2017, 09:10
Stanislav Korotky
0
1 738

Las construcciones geométricas en los gráficos son unas de las herramientas más populares del trader a lo largo de las décadas. Con el desarrollo de las tecnologías, se hace aún más fácil trazar las líneas de soporte o resistencia, los niveles históricos de precios y las figuras enteras, por ejemplo, canales o cuadrícula de Fibonacci. El software para el trading algorítmico permite no sólo analizar las figuras clásicas, sino también tradear basándose en ellas. Para MetaTrader también han sido desarrollados los programas que automatizan el proceso en uno u otro grado: basta con añadir el objeto en el gráfico con un EA o script iniciado, y luego el mismo programa abrirá una posición en el momento oportuno, va a acompañarla, y la cerrará de acuerdo con los ajustes. A través de este software, se puede no sólo operar en línea, sino también entrenar sus habilidades en el Probador de Estrategias, en modo de visualización. Los programas semejantes se encuentran en la Base de los códigos fuente y en el Market de la comunidad de los traders.

Pero no todo está tan claro. Habitualmente, los programas en la Base de los códigos fuente tienen una funcionalidad simplificada, se actualizan raras veces y por eso se quedan obsoletos (incluso hasta perder la compatibilidad con las últimas versiones del lenguaje MQL y el terminal), mientras que los productos comerciales no están al alcance de todos los traders.

En este artículo, vamos a intentar desarrollar una nueva herramienta que representa un justo término medio. Será máximamente sencilla y al mismo tiempo proporcionará las posibilidades bastante amplias. Estará compatible tanto con MetaTrader 4, como con MetaTrader 5. Y gracias al código fuente abierto, en caso de necesidad, se puede ampliar y modificarlo fácilmente.

El soporte de MetaTrader 4 es importante no sólo desde el punto de vista del abarcamiento de una gran parte del auditorio que sigue utilizando esta versión, sino también debido a ciertas limitaciones del Probador de estrategias de MetaTrader 5. En particular, el Probador visual de MetaTrader 5 no permite en este momento trabajar con objetos de forma interactiva (añadir, eliminar, editar las propiedades), lo que es necesario para esta herramienta. Por eso, podemos perfeccionar nuestras habilidades de manejar los objetos a base del historial solamente en el Probador de MetaTrader 4.

Elaborando los requerimientos

El requerimiento ideológico más importante para nuestro sistema del trading automático a base de los objetos gráficos es la sencillez. Vamos a usar la interfaz estándar del usuario de MetaTrader y las propiedades de los objetos gráficos, sin aplicar los paneles especiales ni la lógica compleja de configuraciones. Como demuestra la práctica, todas las ventajas de los sistemas potentes que poseen diferentes opciones se anulan con las dificultades del aprendizaje y con poca demanda de cada determinado modo de su explotación. Vamos a tratar de utilizar sólo las técnicas conocidas del trabajo con los objetos y la interpretación intuitivamente comprensible de sus propiedades.

Entre la variedad de los tipos de objetos gráficos para tomar las decisiones comerciales, con más frecuencia se utilizan los siguientes:

  • línea de tendencia;
  • línea horizontal;
  • linea vertical;
  • canal equidistante;
  • líneas de Fibonachi.

Precisamente para estos objetos, vamos a implementar el soporte en primer lugar. Podríamos ampliar esta lista con nuevos tipos disponibles en MetaTrader 5, pero eso alteraría la compatibilidad con MetaTrader 4. Además, basándose en los principios básicos del procesamiento de objetos que serán implementados en este proyecto, los usuarios pueden añadir fácilmente otros objetos de acuerdo con sus necesidades.

Cada uno de los objetos mencionados forma un cierto límite lógico en el espacio bidimensional del gráfico, y cuando este límite se cruza o ocurre el rebote, surge una señal. Su interpretación suele corresponder a una de las siguientes situaciones:

  • ruptura o rebote de la línea de soporte/resistencia;
  • ruptura o rebote del nivel histórico de precio;
  • alcance del nivel establecido del Stop Loss o Take Profit;
  • llegada de un momento de tiempo establecido.

Dependiendo de la estrategia seleccionada, cuando llega el evento, el trader puede realizar la compra o la venta, colocar una orden pendiente o cerrar una posición existente. Nuestro sistema también tiene que saber hacer todo eso. De esta manera, la lista de las funciones principales será la siguiente:

  • envío de las notificaciones de diferentes maneras (alerta, notificación push, e-mail);
  • apertura de las órdenes de mercado;
  • colocación de las órdenes pendientes (buy stop, sell stop, buy limit, sell limit);
  • cierre total o parcial de una posición abierta, incluyendo en calidad de Stop Loss y Take Profit.

Desarrollando la interfaz de usuario

En muchos productos semejantes, se le presta mucha atención a la interfaz de usuario. Se trata de los paneles, cuadros de diálogo, botones, drag”n”drop, etc. En nuestro proyecto no habrá nada de eso. En vez de la interfaz gráfica especial, vamos a usar lo que MetaTrader nos da por defecto.

Cada objeto tiene un conjunto estándar de propiedades que se puede ajustar a la tarea actual.

Primero, habrá que distinguir los objetos para el trading automático de los demás objetos que pueden colocarse en el gráfico por el usuario. Para eso vamos a dar a nuestros objetos los nombres con un prefijo predeterminado. Si el prefijo no está establecido, el Asesor Experto (EA) interpreta todos los objetos con las propiedades apropiadas como activos. Pero este modo no se recomienda porque el terminal puede crear sus objetos (por ejemplo, las órdenes cerradas), y ellos pueden producir los efectos secundarios.

Segundo, para ejecutar diferentes funciones (su lista se muestra más arriba), vamos a reservar diferentes estilos del diseño. Por ejemplo, para colocar las órdenes pendientes, vamos a establecer el estilo STYLE_DASH en los ajustes del EA, y el estilo STYLE_SOLID, para la entrada inmediata en el mercado cuando el precio cruza la línea correspondiente. Los estilos para cada operación tienen que ser diferentes.

Tercero, hay que especificar la dirección del trading. Por ejemplo, se puede usar el color azul para las compras, y el rojo, para las ventas. Las acciones no relacionadas con la entrada y la salida del mercado estarán marcadas con otro color, por ejemplo, el gris. Son, por ejemplo, las notificaciones y colocación de las órdenes pendientes. El último caso pertenece a la categoría «neutral» porque a menudo se establecen varias (normalmente un par) órdenes pendientes bidireccionales.

Cuarto, es necesario determinar el tipo de órdenes pendientes. Se puede hacer eso unívocamente según la posición recíproca de la línea de la orden respecto al precio actual de mercado y el color de la línea. Por ejemplo, la línea azul encima del precio supone buy stop, pero la línea azul debajo del precio, buy limit.

Y finalmente, quinto, para la mayoría de las operaciones necesitamos alguna información adicional, los atributos. En particular, si ha sido activada una alerta (pueden ser varias), el usuario seguramente querrá recibir un mensaje sensato. Para eso no viene muy bien el campo Descripción que todavía no ha sido usado. En este campo, para las órdenes vamos a especificar el tamaño del lote y la hora de expiración. Todo eso es opcional, ya que por motivos de conveniencia y minimización de los ajustes necesarios de los objetos, los parámetros de entrada con valores predefinidos estarán previstos en el EA. Aparte de los ajustes de los objetos, estos valores predefinidos van a contener los tamaños del Stop Loss y Take Profit.

Para los casos cuando para cada línea se establecen los tamaños específicos del Stop Loss y Take Profit, usaremos los objetos que reúnen varias líneas. Por ejemplo, un canal equidistante tiene dos líneas. La primera de ellas que se basa en dos puntos se encargará de la formación de la señal comercial, y la línea paralela (con el tercer punto) va a establecer la distancia hasta el Stop Loss y Take Profit. Es fácil determinar qué nivel tenemos presente si nos fijamos en la posición recíproca y en el color de la línea. Por ejemplo, para el canal rojo, la línea adicional ubicada por encima de la principal formará el Stop Loss. Si estuviera más abajo, se interpretaría como el Take Profit.

Si queremos establecer el Stop Loss y Take Profit, el objeto debe componerse por lo menos de tres líneas. Para eso conviene muy bien, por ejemplo, la cuadrícula de los niveles de Fibonacci. Por defecto, en el proyecto se usan los niveles estándar 38,2% (Stop Loss), 61,8% (punto de entrada en el mercado) y 161,8% (Take Profit). En este artículo, no vamos a considerar el ajuste más flexible de éste y de los tipos más complicados de los objetos.

Cuando el precio cruza un objeto y éste se activa, hay que marcarlo como activado. Podemos hacerlo, por ejemplo, asignándole el atributo del «fondo» OBJPROP_BACK. Para la retroalimentación visual con el usuario, vamos a apagar el color fuente de estos objetos. Por ejemplo, la línea azul se convertirá en azul oscuro después de su procesamiento.

Sin embargo, entre el trazado del trader a menudo se encuentran las líneas o niveles tan «fuertes» que el evento relacionado con ellos (por ejemplo, el rebote de la línea de soporte en las correcciones de la tendencia alcista) puede repetirse varias veces.

Vamos a prever esta situación usando el grosor de la línea. Como se sabe, los estilos de MetaTrader permiten establecer el grosor de 1 a 5. Al activar una línea, vamos a fijarnos en su grosor, y si es más de 1, entonces en vez de excluirla del siguiente trabajo, disminuimos su grosor en 1. Así podemos representar en el gráfico múltiples eventos esperados con el número de repeticiones de hasta 5.

Esta posibilidad tiene un detalle: los precios normalmente fluctúan alrededor de un determinado valor y cualquier línea puede ser cruzada varias veces durante un pequeño período de tiempo. El trader que opera manualmente analiza la dinámica de la desviación del precio a ojo, y prácticamente «filtra» el ruido de precio. En el EA, es necesario implementar el mecanismo que hace lo mismo automáticamente.

A ese fin, introduciremos los parámetros de entrada que determinan el tamaño del «área caliente» de la intersección, es decir, el diapasón mínimo del movimiento del precio y su duración dentro de cuyos límites las señales no van a formarse. En otras palabras, el evento del «cruce de la línea» no ocurrirá inmediatamente después de que el precio la cruce, sino cuando retrocederá a una distancia establecida en puntos.

De la misma manera introduciremos el parámetro que establece el intervalo mínimo entre dos eventos consecutivos para la misma línea (sólo para las líneas con el grosor más de 1). Aquí nos surge una nueva tarea: hay que almacenar la hora del evento anterior con la línea en algún lugar. Para eso usamos la propiedad del objeto OBJPROP_ZORDER. Es el número tipo long, y el valor datetime cabe ahí perfectamente. El cambio del orden de la visualización de las líneas prácticamente no influye en la apariencia del gráfico.

En la interfaz del usuario, para configurar la línea que sirve para trabajar con el sistema, será suficiente hacer lo siguiente:

  • abrir el diálogo de propiedades del objeto;
  • añadir el prefijo seleccionado al nombre;
  • especificar los parámetros en la descripción (opcional):
    • lote para las líneas de las órdenes de mercado y pendientes, cierre parcial de la posición,
    • nombres de las líneas de las órdenes pendientes para la línea de activación de las órdenes,
    • plazo de vencimiento para la línea de la orden pendiente,
  • color como indicador de la dirección (por defecto, azul — compra, rojo — venta, gris — neutral);
  • estilo como selector de la operación (alerta, entrada en el mercado, colocación de la orden pendiente, cierre de la posición);
  • ancho como indicador de la repetición del evento.

Configuración de las propiedades de la línea horizontal para la orden buy limit (azul punteada) con el lote 0,02 y plazo de vencimiento 24 barras (horas)

Configuración de las propiedades de la línea horizontal para la orden buy limit (azul punteada) con el lote 0,02 y plazo de vencimiento 24 barras (horas)

La lista de las órdenes controladas por el sistema (que responden a los requerimientos descritos) va a visualizarse en el comentario en el gráfico, junto con los detalles: tipo del objeto, descripción, estatus.


Desarrollando el mecanismo de la implementación

De acuerdo con los requerimientos y consideraciones generales, empezaremos la implementación del EA con los parámetros de entrada en los que se pasa el prefijo de los nombres de objetos, los colores y estilos procesados, los valores predefinidos, así como los tamaños de las áreas que generan los eventos en el gráfico.

input int Magic = 0;
input double Lot = 0.01 /*default lot*/;
input int Deviation = 10 /*tolerance to price changes during order execution*/;

input int DefaultTakeProfit = 0 /*points*/;
input int DefaultStopLoss = 0 /*points*/;
input int DefaultExpiration = 0 /*bars*/;

input string CommonPrefit = "exp" /*empty to handle all compatible objects*/;

input color BuyColor = clrBlue /*market and pending buy orders - open, close, sl, tp*/;
input color SellColor = clrRed /*market and pending sell orders - open, close, sl, tp*/;
input color ActivationColor = clrGray /*activation of pending orders placement, alert*/;

input ENUM_LINE_STYLE InstantType = STYLE_SOLID /*opens market trades*/;
input ENUM_LINE_STYLE PendingType = STYLE_DASH /*defines probable pending orders (requires activation)*/;
input ENUM_LINE_STYLE CloseStopLossTakeProfitType = STYLE_DOT /*applied to open positions*/;

input int EventHotSpot = 10 /*points*/;
input int EventTimeSpan = 10 /*seconds*/;
input int EventInterval = 10 /*bars*/;

El EA lo vamos a presentar como la clase TradeObjects (archivo TradeObjects.mq4, el mismo .mq5). El constructor, destructor y los métodos del procesamiento de los eventos estándar van a ser públicos.

class TradeObjects
{
  private:
    Expert *e;

  public:
    void handleInit()
    {
      detectLines();
    }
    
    void handleTick()
    {
      #ifdef __MQL4__  
      if(MQLInfoInteger(MQL_TESTER))
      {
        static datetime lastTick = 0;
        if(TimeCurrent() != lastTick)
        {
          handleTimer();
          lastTick = TimeCurrent();
        }
      }
      #endif
    
      e.trailStops();
    }
    
    void handleTimer()
    {
      static int counter = 0;
      
      detectLines();
      
      counter++;
      if(counter == EventTimeSpan) // wait until we have history record of bid for EventTimeSpan
      {
        counter = 0;
        if(PreviousBid > 0) processLines();
        if(PreviousBid != Bid) PreviousBid = Bid;
      }
    }
    
    void handleChart(const int id, const long &lparam, const double &dparam, const string &sparam)
    {
      if(id == CHARTEVENT_OBJECT_CREATE || id == CHARTEVENT_OBJECT_CHANGE)
      {
        if(checkObjectCompliance(sparam))
        {
          if(attachObject(sparam))
          {
            display();
            describe(sparam);
          }
        }
        else
        {
          detectLines();
        }
      }
      else if(id == CHARTEVENT_OBJECT_DELETE)
      {
        if(removeObject(sparam))
        {
          display();
          Print("Line deleted: ", sparam);
        }
      }
    }
    
    TradeObjects()
    {
      e = new Expert(Magic, Lot, Deviation);
    }
    
    ~TradeObjects()
    {
      delete e;
    }
};

La instancia de esta clase la vamos a crear de forma estática, y luego vincularemos sus manejadores (handle) de eventos a las funciones globales correspondientes.

TradeObjects to;

void OnInit()
{
  ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true);
  EventSetTimer(1);
  to.handleInit();
}

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
  to.handleChart(id, lparam, dparam, sparam);
}
                 
void OnTimer()
{
  to.handleTimer();
}

void OnTick()
{
  to.handleTick();
}

Vamos a encargar todas las operaciones comerciales al motor que se oculta en la clase externa Expert (archivo Expert01.mqh). Creamos su instancia (e) en el constructor y la eliminamos en el destructor de la clase TradeObjects. Más tarde consideraremos este motor en detalle, mientras tanto cabe destacar que TradeObjects va a delegarle muchas de las operaciones.

Todos los manejadores de eventos handleInit, handleTick, handleTimer, handleChart llaman al método detectLines que necesitamos escribir. Ahí van a analizarse los objetos existentes y seleccionarse sólo aquellos que satisfacen a nuestras exigencias. Además, el usuario puede crear, eliminar, editar los objetos durante la ejecución del EA. Los objetos encontrados se guardan en el array interno. Su presencia permite monitorear el cambio del estado del gráfico y avisar al usuario sobre la detección de nuevos objetos o eliminación de los antiguos.

El método processLines se invoca periódicamente por el temporizador. Este método debe recorrer el array en el ciclo comprobando la llegada del evento y ejecutar las acciones correspondientes.

Como se puede observar, se comprueba el prefijo de los objetos, así como sus estilos, colores y el estado a través del método checkObjectCompliance (véase a continuación). Los objetos apropiados se añaden al array interno a través de la función attachObject, los que han sido eliminados del gráfico, se eliminan del array a través de la función removeObject. El método display muestra la lista de los objetos en forma del comentario en el gráfico.

El array se compone de las estructuras simples que contienen el nombre del objeto y su estado:

  private:
    struct LineObject
    {
      string name;
      int status;
      void operator=(const LineObject &o)
      {
        name = o.name;
        status = o.status;
      }
    };
    
    LineObject objects[];

En primer lugar, el estatus se utiliza para marcar los objetos existentes, por ejemplo, eso se hace inmediatamente después de la inserción de un nuevo objeto en el array a través de la función attachObject:

  protected:
    bool attachObject(const string name)
    {
      bool found = false;
      int n = ArraySize(objects);
      for(int i = 0; i < n; i++)
      {
        if(objects[i].name == name)
        {
          objects[i].status = 1;
          found = true;
          break;
        }
      }
      
      if(!found)
      {
        ArrayResize(objects, n + 1);
        objects[n].name = name;
        objects[n].status = 1;
        return true;
      }
      
      return false;
    }

La comprobación de la existencia de cada objeto en los momentos de tiempo posteriores se realiza en el método detectLines:

    bool detectLines()
    {
      startRefresh();
      int n = ObjectsTotal(ChartID(), 0);
      int count = 0;
      for(int i = 0; i < n; i++)
      {
        string obj = ObjectName(ChartID(), i, 0);
        if(checkObjectCompliance(obj))
        {
          if(attachObject(obj))
          {
            describe(obj);
            count++;
          }
        }
      }
      if(count > 0) Print("New lines: ", count);
      bool changes = stopRefresh() || (count > 0);
      
      if(changes)
      {
        display();
      }
      
      return changes;
    }

Aquí primero, se llama a la función adicional startRefresh que resetea las banderas del estatus a 0 para todos los objetos del array. Luego, los objetos de trabajo vuelven a obtener el estatus 1 dentro del ciclo a través de attachObject, y al final se invoca la función stopRefresh que encuentra los objetos no reclamados dentro del array interno por el estatus cero de lo que se avisa al usuario.

La comprobación de la correspondencia a los requerimientos se realiza en el método checkObjectCompliance:

    bool checkObjectCompliance(const string obj)
    {
      if(CommonPrefit == "" || StringFind(obj, CommonPrefit) == 0)
      {
        if(_ln[ObjectGetInteger(0, obj, OBJPROP_TYPE)]
        && _st[ObjectGetInteger(0, obj, OBJPROP_STYLE)]
        && _cc[(color)ObjectGetInteger(0, obj, OBJPROP_COLOR)])
        {
          return true;
        }
      }
      return false;
    }

Aquí, aparte del prefijo del nombre, se comprueban los conjuntos de las banderas con los tipos, estilos y colores de los objetos. Para eso se utiliza la clase auxiliar Set:

#include <Set.mqh>

Set<ENUM_OBJECT> _ln(OBJ_HLINE, OBJ_VLINE, OBJ_TREND, OBJ_CHANNEL, OBJ_FIBO);
Set<ENUM_LINE_STYLE> _st(InstantType, PendingType, CloseStopLossTakeProfitType);
Set<color> _cc(BuyColor, SellColor, ActivationColor);

Ahora, hablaremos del método principal, processLines. Puesto que es central, eso influye en su tamaño. Puede encontrar el código entero en el archivo adjunto, mientras que aquí mostraremos los fragmentos más importantes.

    void processLines()
    {
      int n = ArraySize(objects);
      for(int i = 0; i < n; i++)
      {
        string name = objects[i].name;
        if(ObjectGetInteger(ChartID(), name, OBJPROP_BACK)) continue;
        
        int style = (int)ObjectGetInteger(0, name, OBJPROP_STYLE);
        color clr = (color)ObjectGetInteger(0, name, OBJPROP_COLOR);
        string text = ObjectGetString(0, name, OBJPROP_TEXT);
        datetime last = (datetime)ObjectGetInteger(0, name, OBJPROP_ZORDER);
    
        double aux = 0, auxf = 0;
        double price = getCurrentPrice(name, aux, auxf);
        ...

Recorremos en el ciclo todos los objetos, excluyendo los que han sido activados (como hemos acordado, éstos tendrán establecida la bandera OBJPROP_BACK). Usando la función getCurrentPrice que será mostrada más abajo, averiguaremos los valores del precio del objeto actual. Puesto que algunos tipos de objetos se componen de varias líneas, pasamos los valores adicionales de los precios a través de 2 parámetros.

        if(clr == ActivationColor)
        {
          if(style == InstantType)
          {
            if(checkActivation(price))
            {
              disableLine(i);
              if(StringFind(text, "Alert:") == 0) Alert(StringSubstr(text, 6));
              else if(StringFind(text, "Push:") == 0) SendNotification(StringSubstr(text, 5));
              else if(StringFind(text, "Mail:") == 0) SendMail("TradeObjects", StringSubstr(text, 5));
              else Print(text);
            }
          }

Luego, comprobamos el estilo del objeto para averiguar el tipo del evento y cómo su precio en la barra 0 se correlaciona con el precio Bid —en caso de la alerta y colocación de las órdenes pendientes, la función checkActivation se encarga de eso. Si la activación ha tenido lugar, realizamos la acción correspondiente (en caso de la alerta, mostramos el mensaje o enviamos al destinatario) y marcamos el objeto como deshabilitado a través de disableLine.

Desde luego, para las operaciones comerciales, el código de la activación será más complejo. Por ejemplo, aquí tenemos la versión simplificada para la compra por el mercado y el cierre de las posiciones cortas:

        else if(clr == BuyColor)
        {
          if(style == InstantType)
          {
            int dir = checkMarket(price, last);
            if((dir == 0) && checkTime(name))
            {
              if(clr == BuyColor) dir = +1;
              else if(clr == SellColor) dir = -1;
            }
            if(dir > 0)
            {
              double lot = StringToDouble(ObjectGetString(0, name, OBJPROP_TEXT)); // lot[%]
              if(lot == 0) lot = Lot;
    
              double sl = 0.0, tp = 0.0;
              if(aux != 0)
              {
                if(aux > Ask)
                {
                  tp = aux;
                  if(DefaultStopLoss != 0) sl = Bid - e.getPointsForLotAndRisk(DefaultStopLoss, lot) * _Point;
                }
                else
                {
                  sl = aux;
                  if(DefaultTakeProfit != 0) tp = Bid + e.getPointsForLotAndRisk(DefaultTakeProfit, lot) * _Point;
                }
              }
              else
              {
                if(DefaultStopLoss != 0) sl = Bid - e.getPointsForLotAndRisk(DefaultStopLoss, lot) * _Point;
                if(DefaultTakeProfit != 0) tp = Bid + e.getPointsForLotAndRisk(DefaultTakeProfit, lot) * _Point;
              }
              
              sl = NormalizeDouble(sl, _Digits);
              tp = NormalizeDouble(tp, _Digits);
            
              int ticket = e.placeMarketOrder(OP_BUY, lot, sl, tp);
              if(ticket != -1) // success
              {
                disableLine(i);
              }
              else
              {
                showMessage("Market buy failed with '" + name + "'");
              }
            }
          }
          else if(style == CloseStopLossTakeProfitType) // close sell position, stoploss for sell, takeprofit for sell
          {
            int dir = checkMarket(price) || checkTime(name);
            if(dir != 0)
            {
              double lot = StringToDouble(ObjectGetString(0, name, OBJPROP_TEXT)); // lot
              if(lot > 0)
              {
                if(e.placeMarketOrder(OP_BUY, lot) != -1) // will trigger OrderCloseBy();
                {
                  disableLine(i);
                }
                else
                {
                  showMessage("Partial sell close failed with '" + name + "'");
                }
              }
              else
              {
                if(e.closeMarketOrders(e.mask(OP_SELL)) > 0)
                {
                  disableLine(i);
                }
                else
                {
                  showMessage("Complete sell close failed with '" + name + "'");
                }
              }
            }
          }

Aquí se utiliza la función checkMarket (la versión más compleja de checkActivation, ambas se muestran a continuación) que comprueba la llegada del evento. Cuando la condición se activa, obtenemos los niveles del Stop Loss o Take Pofit, lote desde las propiedades del objeto, y luego abrimos la orden.

El tamaño del lote se establece en la descripción del objeto en los contratos, o como porcentaje del margen libre: en el último caso, el valor se escribe como negativo. El sentido de esta notación es fácil de recordar si imaginamos que Usted prácticamente indica qué parte de los fondos hay que «morder» para asegurar la nueva orden.

Las funciones checkActivation y checkMarket son parecidas, utilizan los ajustes de entrada del EA que determinan el tamaño del área de activación de eventos:

    bool checkActivation(const double price)
    {
      if(Bid >= price - EventHotSpot * _Point && Bid <= price + EventHotSpot * _Point)
      {
        return true;
      }
      
      if((PreviousBid < price && Bid >= price)
      || (PreviousBid > price && Bid <= price))
      {
        return true;
      }
      return false;
    }
    
    int checkMarket(const double price, const datetime last = 0) // returns direction of price movement
    {
      if(last != 0 && (TimeCurrent() - last) / PeriodSeconds() < EventInterval)
      {
        return 0;
      }
    
      if(PreviousBid >= price - EventHotSpot * _Point && PreviousBid <= price + EventHotSpot * _Point)
      {
        if(Bid > price + EventHotSpot * _Point)
        {
          return +1; // up
        }
        else if(Bid < price - EventHotSpot * _Point)
        {
          return -1; // down
        }
      }
    
      if(PreviousBid < price && Bid >= price && MathAbs(Bid - PreviousBid) >= EventHotSpot * _Point)
      {
        return +1;
      }
      else if(PreviousBid > price && Bid <= price && MathAbs(Bid - PreviousBid) >= EventHotSpot * _Point)
      {
        return -1;
      }
      
      return 0;
    }

Vamos a recordar que el precio PreviousBid se guarda por el EA en el manejador handleTimer con una periodicidad de EventTimeSpan segundos. El resultado del trabajo de la función es el indicio de la intersección del precio del objeto por el precio Bid en la barra 0. En este caso, checkActivation devuelve la bandera lógica simple y checkMarket devuelve la dirección del movimiento del precio: +1 hacia arriba, -1 hacia abajo.

Cabe mencionar especialmente que el control de la intersección de las cotizaciones de los objetos se hace por el precio bid. Eso se hace así porque el gráfico entero se construye por el precio bid, inclusive todas las líneas trazadas. Incluso si el trader ha formado el trazado para las órdenes de compra, ellas van a activarse según las señales correctas: el gráfico ask pasa explícitamente por debajo del gráfico bid por el valor del spreade, y las líneas potenciales que podrían ser construidas a base del gráfico ask, cruzarían el precio sincrónicamente con el trazado por bid.

Para las líneas del estilo PendingType y el neutral ActivationColor, el comportamiento es especial: en el momento cuando el precio las cruza, se colocan las órdenes pendientes. La colocación de las propias órdenes se establece por otras líneas, cuyos nombres se enumeran separándose por una barra inclinada ('/') en la descripción de la línea de activación. Si la descripción de la línea de activación está vacía, el sistema encontrará todas las líneas de las órdenes pendientes según el estilo y las colocará. La descripción de las órdenes pendientes, igual como para las órdenes de mercado, corresponde a su color: BuyColor o SellColor para la compra o la venta. Además, en la descripción se puede indicar el lote y el plazo de expiración (en barras).

Los modos de combinación de los estilos y los colores de los objetos, y sus valores correspondientes se muestran en la tabla siguiente.

Color y Estilo BuyColor SellColor ActivationColor
InstantType compra por mercado venta por mercado alerta
PendingType orden pendiente potencial de compra orden pendiente potencial de venta inicialización de colocación de órdenes pendientes
CloseStopLossTakeProfitType cierre, Stop Loss, Take Profit
para posición corta
cierre, Stop Loss, Take Profit
para posición larga
cerrar todo

Volvamos al método getCurrentPrice —quizá, es el más importante después del processLines.

    double getCurrentPrice(const string name, double &auxiliary, double &auxiliaryFibo)
    {
      int type = (int)ObjectGetInteger(0, name, OBJPROP_TYPE);
      if(type == OBJ_TREND)
      {
        datetime dt1 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 0);
        datetime dt2 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 1);
        int i1 = iBarShift(NULL, 0, dt1, true);
        int i2 = iBarShift(NULL, 0, dt2, true);
        if(i1 <= i2 || i1 == -1 || i2 == -1)
        {
          Print("Incorrect line: ", name);
          return 0;
        }
        double p1 = ObjectGetDouble(0, name, OBJPROP_PRICE, 0);
        double p2 = ObjectGetDouble(0, name, OBJPROP_PRICE, 1);
        
        double k = -(p1 - p2)/(i2 - i1);
        double b = -(i1 * p2 - i2 * p1)/(i2 - i1);
        
        return b;
      }
      else if(type == OBJ_HLINE)
      {
        return ObjectGetDouble(0, name, OBJPROP_PRICE, 0);
      }
      else if(type == OBJ_VLINE)
      {
        return EMPTY_VALUE; // should not be a null, not used otherwise
      }
      else if(type == OBJ_CHANNEL)
      {
        datetime dt1 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 0);
        datetime dt2 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 1);
        datetime dt3 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 2);
        int i1 = iBarShift(NULL, 0, dt1, true);
        int i2 = iBarShift(NULL, 0, dt2, true);
        int i3 = iBarShift(NULL, 0, dt3, true);
        if(i1 <= i2 || i1 == -1 || i2 == -1 || i3 == -1)
        {
          Print("Incorrect channel: ", name);
          return 0;
        }
        double p1 = ObjectGetDouble(0, name, OBJPROP_PRICE, 0);
        double p2 = ObjectGetDouble(0, name, OBJPROP_PRICE, 1);
        double p3 = ObjectGetDouble(0, name, OBJPROP_PRICE, 2);
        
        double k = -(p1 - p2)/(i2 - i1);
        double b = -(i1 * p2 - i2 * p1)/(i2 - i1);
        
        double dy = i3 * k + b - p3;
        
        auxiliary = p3 - i3 * k;
        
        return b;
      }
      else if(type == OBJ_FIBO)
      {
        // level 61.8 is enter point at retracement (buy/sell limit),
        // 38.2 and 161.8 as stoploss/takeprofit
        
        double p1 = ObjectGetDouble(0, name, OBJPROP_PRICE, 0);
        double p2 = ObjectGetDouble(0, name, OBJPROP_PRICE, 1);
        datetime dt1 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 0);
        datetime dt2 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 1);
        
        if(dt2 < dt1)
        {
          swap(p1, p2);
        }
        
        double price = (p2 - p1) * ObjectGetDouble(0, name, OBJPROP_LEVELVALUE, 4) + p1;
        auxiliary = (p2 - p1) * ObjectGetDouble(0, name, OBJPROP_LEVELVALUE, 2) + p1;
        auxiliaryFibo = (p2 - p1) * ObjectGetDouble(0, name, OBJPROP_LEVELVALUE, 6) + p1;
        return price;
      }
      return 0;
    }

Lo esencial aquí es lo siguiente: dependiendo del tipo del objeto, hay que calcular el precio en la barra 0 para este objeto (para la línea principal y líneas adicionales). Al colocar lo objetos en el gráfico, hay que cuidar que todos los puntos del objeto se encuentren en el pasado, ahí donde haya un número válido de la barra. De lo contrario, el objeto va a considerarse no válido, porque resulta imposible calcular unívocamente el precio para él.

En caso de la línea vertical, devolvemos EMPTY_VALUE, es decir, no es cero, pero tampoco es un precio determinado (ya que esta línea satisface a cualquier precio). Por eso para las líneas verticales, hay que usar una comprobación adicional respecto a la coincidencia con la hora actual. De eso se encarga la función checkTime, cuya llamada los lectores atentos ya han podido observar en el fragmento de processLines.

    bool checkTime(const string name)
    {
      return (ObjectGetInteger(0, name, OBJPROP_TYPE) == OBJ_VLINE
        && (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 0) == Time[0]);
    }

Finalmente, vamos a considerar la implementación de la función disableLine, la que ya hemos visto muchas veces en el código.

    void disableLine(const string name)
    {
      int width = (int)ObjectGetInteger(0, name, OBJPROP_WIDTH);
      if(width > 1)
      {
        ObjectSetInteger(0, name, OBJPROP_WIDTH, width - 1);
        ObjectSetInteger(0, name, OBJPROP_ZORDER, TimeCurrent());
      }
      else
      {
        ObjectSetInteger(0, name, OBJPROP_BACK, true);
        ObjectSetInteger(0, name, OBJPROP_COLOR, darken((color)ObjectGetInteger(0, name, OBJPROP_COLOR)));
      }
      display();
    }

Si el grosor de la línea es más de 1, lo disminuimos en 1 y guardamos la hora actual del evento en la propiedad OBJPROP_ZORDER. En caso de las líneas comunes, las movemos al fondo y suavizamos el color. Los objetos que se encuentran en el fondo se consideran desactivados.

En cuanto a la propiedad OBJPROP_ZORDER, ella se lee, como ha sido mostrado antes, en el método processLines a la variable datetime last, que luego se pasa al método checkMarket(price, last) como argumento. Dentro nosotros monitoreamos que el tiempo desde la activación anterior supere el intervalo (en barras) establecido en la variable de entrada.

      if(last != 0 && (TimeCurrent() - last) / PeriodSeconds() < EventInterval)
      {
        return 0;
      }

TradeObjects permite realizar el cierre parcial si en la descripción del objeto tipo CloseStopLossTakeProfitType se indica el lote. El sistema abre una orden opuesta del volumen especificado y luego llama a OrderCloseBy. Para activar el modo, tenemos prevista la bandera especial AllowOrderCloseBy en los parámetros de entrada. Si está activa activado, las posiciones opuestas siempre van a «encerrarse» en una. Recordaré que esta función no está permitida en todas las cuentas (el EA comprueba esta configuración y muestra el mensaje correspondiente en el registro si esta posibilidad está bloqueada). En caso de MetaTrader 5, la cuenta debe ser con cobertura (hedging). El que desea puede mejorar el sistema en cuanto a la implementación alternativa del cierre parcial: sin uso de OrderCloseBy, con la revisión de la lista de posiciones y la selección de una determinada que se disminuye a través de algunos atributos.

Volvamos a la clase Expert que ejecuta todas las operaciones comerciales para TradeObjects. Esta clase representa un simple conjunto de los métodos para abrir y cerrar las órdenes, seguir los Stop Loss, calcular los lotes partiendo del riesgo establecido. Ahí se utiliza la metáfora de las órdenes de MetaTrader 4 que se adapta para MetaTrader 5 usando la librería MT4Orders.

La clase no proporciona la funcionalidad para cambiar las órdenes pendientes establecidas. El desplazamiento de su precio, los niveles Stop Loss y Take Profit ha quedado en la competencia del terminal: si la opción «Mostrar niveles comerciales» está activada, permite hacerlo a través de Drag'n'Drop.

Puede sustituir la clase Expert por cualquiera que utiliza.

El código fuente adjunto se compila tanto en MetaTrader 4, como en MetaTrader 5 (con archivos de cabecera adicionales).

Cabe mencionar que en la implementación actual de TradeObjects hay una desviación de las prácticas estrictas de POO en favor de la sencillez. Por ejemplo, habría que tener una interfaz comercial abstracta, implementar la clase Expert del EA como heredero de esta interfaz y luego pasarla a la clase TradeObjects (por ejemplo, usando el parámetro del constructor). Es una plantilla conocida de la POO, Inyección de dependencias (dependency injection). En este proyecto, el motor de trading está escrito rigurosamente en el código: se crea y se elimina dentro del objeto TradeObjects.

Además, usamos las variables globales de entrada directamente en el código de la clase TradeObjects, contra los principios de la POO. El mejor estilo de la programación requeriría pasarlas en la clase como parámetros del constructor o métodos especiales. Eso permitiría, por ejemplo, usar TradeObjects como librería dentro de otro EA, completándolo con las funciones del trading manual en el trazado.

En principio, seguir los principios básicos de la POO se hace más importante cuanto más grande sea el proyecto a desarrollar. Pero como estamos considerando un motor bastante simple y separado para tradear automáticamente usando los objetos, su perfeccionamiento (que como se sabe no tiene límites) queda para un estudio personal.

Programa en acción

A continuación se muestra cómo el sistema está actuando con las configuraciones de los estilos y colores por defecto.

Supongamos que hemos divisado en el gráfico la formación de la figura «cabeza y hombros» y colocamos la línea horizontal roja para la operación de venta. El sistema muestra una lista de los objetos que ha detectado y está controlando en el comentario.


El Stop Loss estará colocado de acuerdo con el parámetro del Asesor Experto DefaultStopLoss, y en vez del Take Profit vamos a limitar el tiempo de la presencia en el mercado usando la línea vertical punteada de color azul.


Al alcanzar esta línea, la posición se cierra (independientemente de la rentabilidad). Las líneas activadas se marcan como inactivas (sus color se hace más apagado, y ellas se envían al fondo).


Pasado un tiempo, parece que las cotizaciones tienden a bajar de nuevo, y nosotros colocamos los niveles de Fibonacci a la espera del rebote del nivel 61,8 (eso es todo lo que sabe hacer Fibonacci en este proyecto por defecto, pero Usted puede implementar otros tipos del comportamiento). Nótese que el color del objeto de Fibonacci es el color de la línea diagonal y no de los niveles: el color de los niveles se establece con otro ajuste.


Cuando el precio alcance el nivel, se abre una operación con los precios establecidos del Stop Loss (38,2) y Take Profit (161,8, no se observa en la captura de pantalla).


Pasado un tiempo, observamos la formación de una línea de resistencia y colocamos el canal azul suponiendo que al final el precio empiece a subir.


Cabe mencionar que hasta ahora las líneas no contenían descripciones, por eso las órdenes se abrían con el lote del parámetro Lot (0,01, por defecto). Pero en este caso, hay una descripción '-1', es decir, el tamaño del lote será calculado como el que requiere 1% del margen libre. Puesto que la línea adicional se encuentra por debajo de la principal, el canal establece la distancia hasta el Stop Loss (diferente del valor predefinido).


Es verdad que el canal se rompe y se abre una nueva posición larga. Como vemos, el volumen ha sido calculado como 0,04 (con el depósito de 1000$). El segmento azul en la imagen representa el canal activado que ha sido pasado al fondo (así MetaTrader 4 muestra los canales en el fondo).

Para cerrar ambas posiciones de compra, colocamos la línea del Take Profit (roja punteada).


El precio alcanza este nivel y ambas órdenes se cierran.


Supongamos que después de este movimiento, el precio va a oscilarse dentro de un «pasillo». Para captar esta volatilidad, colocamos dos líneas punteadas horizontales para las órdenes limitadas por encima y por debajo del precio, así como una línea punteada vertical de color gris para su activación. En principio, no tienen que ser horizontales o verticales.


Nótese que, por ejemplo, para la orden pendiente de abajo, en la descripción tenemos establecido el lote personalizado 0,02 y el plazo de vencimiento 24 barras (horas). Cuando se alcanza la línea de activación, se colocan las órdenes pendientes.


Dentro de un tiempo, se activa sell limit.


Buy limit se vence el lunes.


Colocamos la línea punteada vertical gris, que significa el cierre de todas las posiciones.


Cuando se alcanza, la posición corta se cierra, pero si tuviéramos abierta una posición larga, también se cerraría.


En el proceso de trabajo, el Asesor Experto muestra los eventos principales en el log.

2017.07.06 02:00:00  TradeObjects EURUSD,H1: New line added: 'exp Channel 42597 break up' OBJ_CHANNEL buy -1
2017.07.06 02:00:00  TradeObjects EURUSD,H1: New lines: 1
2017.07.06 10:05:27  TradeObjects EURUSD,H1: Activated: exp Channel 42597 break up
2017.07.06 10:05:27  TradeObjects EURUSD,H1: open #3 buy 0.04 EURUSD at 1.13478 sl: 1.12908 ok
...
2017.07.06 19:02:18  TradeObjects EURUSD,H1: Activated: exp Horizontal Line 43116 takeprofit
2017.07.06 19:02:18  TradeObjects EURUSD,H1: close #3 buy 0.04 EURUSD at 1.13478 sl: 1.13514 at price 1.14093
2017.07.06 19:02:18  TradeObjects EURUSD,H1: close #2 buy 0.01 EURUSD at 1.13414 sl: 1.13514 tp: 1.16143 at price 1.14093
...
2017.07.07 05:00:09  TradeObjects EURUSD,H1: Activated: exp Vertical Line 42648
2017.07.07 05:00:09  TradeObjects EURUSD,H1: open #4 sell limit 0.01 EURUSD at 1.14361 sl: 1.15395 ok
2017.07.07 05:00:09  TradeObjects EURUSD,H1: #4 2017.07.07 05:00:09 sell limit 0.01 EURUSD 1.14361 1.15395 0.00000 0.00000 0.00 0.00 0.00  0 expiration 2017.07.08 05:00
2017.07.07 05:00:09  TradeObjects EURUSD,H1: open #5 buy limit 0.02 EURUSD at 1.13731 sl: 1.13214 ok
2017.07.07 05:00:09  TradeObjects EURUSD,H1: #5 2017.07.07 05:00:09 buy limit 0.02 EURUSD 1.13731 1.13214 0.00000 0.00000 0.00 0.00 0.00  0 expiration 2017.07.08 05:00

Desde luego, podemos eliminar los objetos activado para limpiar el gráfico. En el ejemplo, han sido dejados como protocolo de la ejecución de las acciones.

Al artículo se le adjuntan las plantillas de MetaTrader 4 y MetaTrader 5 con las líneas para el trading demostrativo en el Probador, en el gráfico EURUSD H1, empezando desde el 1 de julio 2017 (el período arriba descrito). Se usan los ajustes estándar del EA, a excepción del parámetro DefaultStopLoss establecido en -1 (lo que corresponde a la pérdida de 1% del margen libre). Para ilustrar el cálculo y el seguimiento del Stop Loss, se propone el depósito inicial de 1000$, con el apalancamiento de 1:500. En caso de MetaTrader 5, es necesario renombrar previamente la plantilla en tester.tpl (por ahora la plataforma no soporta la descarga y la edición de plantillas directamente en el Probador de estrategias). 

Conclusión

En este artículo ha sido presentada una versión simple pero funcional de la organización del trading semiautomático con el uso de los objetos estándar colocados en el gráfico por el trader. Usando las líneas tendenciales, horizontales, verticales, canales y cuadrículas de Fibonacci, el sistema puede ejecutar las órdenes de mercado, colocar las órdenes pendientes, avisar al trader sobre la formación de las figuras típicas en el mercado. El código fuente abierto permite ampliar el conjunto de los tipos de objetos soportados, así como perfeccionar la funcionalidad comercial.

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

Archivos adjuntos |
MT4.zip (11.97 KB)
MT5.zip (27.59 KB)
Creación y simulación de símbolos personalizados en MetaTrader 5 Creación y simulación de símbolos personalizados en MetaTrader 5

La posibilidad de crear símbolos propios abre nuevos horizontes en el desarrollo de sistemas comerciales y el análisis de cualquier mercado financiero. Ahora los tráders pueden construir gráficos y simular estrategias comerciales con un número ilimitado de instrumentos financieros.

Uso de los repositorios en la nube para el intercambio de datos entre los terminales Uso de los repositorios en la nube para el intercambio de datos entre los terminales

Las tecnologías en la nube se difunden ampliamente. Tenemos a nuestra disposición tanto los repositorios de pago, como gratuitos. ¿Podemos usarlos en el trading? En este artículo se propone la tecnología para el intercambio de datos entre los terminales con el uso de los repositorios en la nube.

Neuroredes profundas (Parte I). Preparación de datos Neuroredes profundas (Parte I). Preparación de datos

Esta serie de artículos continúa y desarrolla el tema de las neuroredes profundas (DNN), que ha sido incluidas en los últimos tiempos en muchas áreas aplicadas, incluyendo el trading. Se analizan las corrientes de dicho tema, comprobándose con experimentos prácticos los nuevos métodos e ideas. El primer artículo de la serie está dedicado a la preparación de los datos para las DNN.

Neuroredes profundas (Parte II). Desarrollo y selección de predictores Neuroredes profundas (Parte II). Desarrollo y selección de predictores

En este segundo artículo de la serie sobre redes neuronales profundas se analizarán la transformación y la selección en el proceso de preparación de los datos para el entrenamiento del modelo.