Descargar MetaTrader 5

Escribir un Expert Advisor mediante la programación orientada a objetos de MQL5

11 marzo 2014, 09:55
Samuel Olowoyo
0
1 149

Introducción

En el primer artículo, hemos recorrido los pasos básicos para la creación, depuración y prueba de un Expert Advisor en MQL5.

Todo lo que hicimos era muy sencillo e interesante; sin embargo, el nuevo lenguaje MQL5 ofrece muchas más cosas. En este artículo, vamos a considerar el enfoque orientado a objetos para hacer lo que hicimos en el primer artículo. La mayoría de la gente piensa que esto es muy difícil, pero le quiero garantizar que al finalizar la lectura de este artículo, será capaz de escribir su propio Expert Advisor basado en la orientación a objetos.

No vamos a repetir algunas cosas que ya hemos aprendido en el primer artículo, así que le recomiendo antes que nada leer el artículo si aún no lo ha hecho.


1. El paradigma de la orientación a objetos

Una de las cosas que hace que MQL5 sea mucho más potente y robusto que MQL4 es el enfoque de la POO (Programación orientada a objetos).

En la POO se recomienda no mostrar ningún detalle de la implementación de un objeto. De este modo, se puede modificar sin cambiar el código que utiliza el objeto. Esto quiere decir que una clase permite al programador ocultar (y también evita cambios) la implementación de la clase que ha escrito. 

Par dejar las cosas más claras, analicemos un poco los términos "clase" y "objeto" que acabamos de mencionar.

  • CLASE. Una clase es más bien como un concepto expandido de una estructura de datos, pero en lugar de contener los datos, contiene a la vez los datos y las funciones. Una clase puede contener distintas variables y funciones, que se llaman miembros de la clase. Es un encapsulado de los miembros de datos y funciones que manejan los datos. Una clase es mucho más potente, ya que le permite poner todas las funciones de sus Expert Advisors en una clase. Solo tiene que hacer referencia a las funciones en el código de su EA en el momento que las necesite. Por cierto, este es el tema de este artículo.
  • OBJETO. Un objeto es una instancia a una clase. Una vez creada una clase, tenemos que declarar una instancia en la misma para poder utilizarla. Esto se llama un objeto. En otras palabras, para crear un objeto necesitamos una clase.

1.1. DECLARAR UNA CLASE

Básicamente, una clase contiene la descripción de los miembros (propiedades y funciones/métodos) de un objeto que quieremos crear a partir de la clase. Veamos un ejemplo... </span

Si queremos crear un objeto que tenga puertas, asientos, neumáticos, peso, etc. y que pueda también arrancar, cambiarmarcha, parar y tocarbocina; entonces tenemos que escribir una clase. Las puertas (doors), asientos (sits), neumáticos (tyres), peso (weight), arrancar (start), cambiarmarcha (changegear), parar (stop) y tocarbocina (horn) serán los miembros de la clase.

Seguro que se ha dado cuenta que los miembros están clasificados; algunos representan simplemente lo que va a tener nuestro objeto (propiedades) mientras que los otros representan lo que podrá hacer nuestro objeto (acciones -funciones/métodos). Para declarar una clase, tenemos que pensar en un nombre adecuado y descriptivo. En este caso, llamaremos a nuestra clase COCHE (CAR). Los miembros de nuestra clase CAR serán las propiedades y funciones mencionadas antes.

Para declarar una clase, escribimos la palabra clave class seguida por el nombre de la clase, seguida por un par de llaves que van a contener los miembros de la clase.

Así que, el formato básico de una clase es el siguiente:

class class_name 
{
  access_keyword_1:
    members1;

  access_keyword_2:
    members2;
  ...
};

Aquí, class_name es un identificador válido para la clase que queremos escribir, members1 y members2 son los datos miembros de la clase.

access_keyword indica el derecho de acceso a los miembros de nuestra clase.  Un access_keyword puede ser privado (private), protegido (protected) o público (public). Recuerde que estamos tratando de escribir una clase que podamos utilizar nosotros mismo y otros, sin mostrar en realidad los detalles de la implementación. Es por eso que los derechos de acceso son necesarios.

Pueden haber algunos miembros de nuestra clase a los que no queremos acceder desde fuera de la misma. Estos se declaran dentro de la sección de acceso privado mediante las palabras clave private o protected. Habrá que declarar los otros miembros a los que queremos acceder desde fuera de nuestra clase en la sección de acceso público mediante la palabra clave public. Nuestra nueva clase CAR quedaría como se muestra a continuación:

class CAR 
{
  private:
    int        doors;
    int        sits;
    int        tyres;
    double     weight;

  public:
    bool       start();
    void       changegear();
    void       stop();
    bool       horn();
  ...
};

Se ha declarado nuestra clase CAR mediante la palabra clave class. Esta clase contiene ocho miembros, de los cuales cuatro con acceso privado y cuatro con acceso público. Los cuatro miembros en la sección privada son miembros "datos". Tres son datos del tipo entero (integer) (int) y uno del tipo double. No se puede acceder a estos miembros por ninguna función declarada fuera de esta clase. 

Los cuatro miembros en la sección pública son miembros "funciones". Dos devuelven datos de tipo bool  y dos de tipo void. Estos son los miembros a los que puede acceder cualquier objeto de esta clase, creado por cualquier persona mediante nuestra clase. Una vez creado el objeto de nuestra clase, estos miembros estarán inmediatamente disponibles para su uso.

Como habrá observado, las palabras clave de acceso (private, public, protected) van siempre seguidas de una coma. La declaración de la clase acaba en punto y coma. Se declaran los miembros empleando su tipo de datos correspondiente.

Hay que tener en cuenta que cuando se declara una clase, se asignan derechos de acceso privado a todos los miembros de la clase, a menos que se indique de manera explícita como hicimos más arriba. Por ejemplo, en la siguiente declaración de clase:

class CAR 
{
    int        doors;
    int        sits;
    int        tyres;
    double     weight;

  public:
    bool       start();
    void       changegear();
    void       stop();
    bool       horn();
  ...
};

Los cuatro miembros declarados encima de la palabra clave pública tienen un acceso privado.

Para que se pueda utilizar nuestra clase, hay que crear en primer lugar un objeto de la clase. Ahora, vamos a crear un objeto que pertenece al tipo de nuestra clase. Para hacerlo, usaremos el nombre de nuestra clase seguido por el nombre que queremos dar al objeto.

CAR Honda;

O podemos crear otro objeto

CAR Toyota;

Honda o Toyota  es un tipo de CAR y ahora puede tener acceso a todas las funciones miembro de nuestra clase CAR siempre y cuando las funciones miembro hayan sido declaradas en la sección de acceso público. Volveremos a esto más adelante.

Puede observar que podemos crear tantos objetos como queramos de esta clase. Esta es una de las ventajas de la programación orientada a objetos.

Llegados a este punto, vamos a ver en detalle el formato de una clase en MQL5.

class class_name 
{
  private:
    members1;
    members2;
    members3;

  public:
    class_name()  //Constructor;
    ~class_name() //Destructor;
    Members4();
    Members5();

  protected:
    members6;
    members7;
};

Esta es una declaración de clase, donde class_name es el nombre de la clase. Esta clase tiene nueve miembros, pero fuera de estos nueve, dos son miembros especiales.

El constructor:
El constructor (expresado por class_name()) es una función especial a la que se llama cuando se crea un nuevo objeto del tipo de la clase. Por lo que en este caso, cuando creas un objeto del tipo de esta clase

class_name object;

se llama automáticamente al constructor class_name(). El nombre del constructor debe coincidir con el nombre de la clase, es por ello que se pone el nombre class_name() al constructor. En MQL5, los constructores no reciben ningún parámetro de entrada y no tienen ningún tipo de retorno. Las asignaciones de memoria y la inicialización de los miembros de la clase se hacen normalmente cuando se llama al constructor. No se puede llamar a los constructores de manera explícita como si fueran funciones miembro normales. Se les llama únicamente cuando se crea un nuevo objeto de esta clase. Una clase en MQL5 puede solamente tener un constructor.

El destructor:
~class_name() expresa el segundo miembro especial. Esta es la clase destructor, se escribe con media tilde (~) antes del nombre de la clase. Se le llama automáticamente cuando se destruye un objeto de la clase. Todos los miembros de la clase que requieren una desinicialización, se desinicializan a esta fase y no importa si ha declarado el destructor de forma explícita o no.

Datos miembro:
Los miembros de una clase pueden ser de cualquier tipo válido data, class o struct. En otras palabras, al declarar las variables de los miembros de una clase, puede utilizar cualquier tipo válido de datos (int, double, string, etc.), de objetos de otra clase o cualquier tipo de estructura (por ejemplo, MqlTradeRequest de MQL5, etc.)

Funciones miembro:
Estos son los miembros de la clase que se utilizan para la modificación de los datos miembro y la ejecución de las principales funciones/métodos de la clase. El retorno de las funciones miembro puede ser de cualquier tipo válido (bool, void, double, string, etc.)

Privado:
Los miembros declarados en esta sección solo son accesibles por las funciones miembro de la clase. No se puede acceder a estos miembros por ninguna función desde fuera de la clase.

Protegido:
Los miembros declarados en esta función son accesibles por las funciones miembro de la clase y son también accesibles a las funciones miembro de otras clases derivadas de esta clase. Esto significa que podemos también crear una clase nueva a partir de esta. En este caso, la nueva clase derivada de esta clase (que se convierte ahora en la clase base) va a ser capaz de acceder a los miembros protegidos de la clase base. Este es el concepto de la herencia en la programación orientada a objetos. Más adelante hablaremos de ello, relájese...

Público:
Los miembros declarados en esta sección están disponibles para su uso desde fuera de la clase por un objeto de dicha clase. Es aquí donde se declaran algunas funciones necesarias para utilizar la clase en otros programas.

Ahora que hemos visto el formato básico de una clase, espero que aún no esté aburrido, ya que aún hay otros aspectos de las clases que tenemos que ver antes de dar el salto final a la creación de una clase contenedora para nuestro Expert Advisor.

1.2. HERENCIA

Digamos que queremos generar otra clase a partir de esta clase inicial base_class. El formato para derivar una nueva clase a partir de una clase inicial es el siguiente:

La clase base:

class base_class 
{
  private:
    members1;
    members2;
    members3;

  public:
    class_name()  //Constructor;
    ~class_name() //Destructor;
    Members4();
    Members5();

  protected:
    members6;
    members7;
};

La clase derivada:

class new_class : access_keyword base_class 
{
  private:
    members8;

  public:
    new_class()  //Constructor;
    ~new_class() //Destructor;
    Members9();
};

Vamos a aclarar algunos conceptos antes de proceder a explicar los detalles. La clase new_class  se deriva de la clase base_class mediante dos puntos (:) y access_keyword, como se muestra arriba. Ahora, new_class derivada/hecha a partir de base_class puede acceder (o heredar) los miembros públicos y protegidos de base_class  pero no puedo acceder (o heredar) los miembros privados de base_class. new_class puede también implementar nuevos métodos/funciones miembro distintos a los de base_class. En otras palabras, new_class tiene también sus propios datos y funciones miembros además de los que ha heredado de base_class.

Si se utiliza la palabra clave public en la creación de la clase derivada, los miembros públicos y protegidos de la clase base serán heredados como miembros públicos y protegidos de la clase derivada. Si se utiliza la palabra clave protected, los miembros públicos y protegidos de la clase base serán heredados como miembros protegidos de la clase derivada. Si se utiliza la palabra clave private, los miembros públicos y protegidos de la clase base serán heredados como miembros privados de la clase derivada.

Es importante señalar que al crear un nuevo objeto de new_class (la clase derivada), se llama primero al constructor de base_class, antes del constructor de new_class; mientras que al destruir el objeto, se llama primero al destructor de new_class (la clase derivada) antes del destructor de base_class.

Para entender mejor este concepto de herencia, volvamos a nuestra clase inicial CAR.

class CAR 
{
  protected:
    int        doors;
    int        sits;
    double     weight;

  public:
    bool       start();
    void       changegear();
    void       stop();
    bool       horn();

  private:
    int        tyres;
};

Podemos derivar otra clase SALOON a partir de esta clase. Fíjate que he declarado tres de los datos miembros de la clase CAR como protected. Es para permitir a nuestra nueva clase SALOON heredar estos miembros.

Además, quiero que tenga claro que el orden de colocación de las palabras clave de acceso no importa. Lo que importa es que todos los miembros declarados con una palabra clave pertenezcan a esta palabra clave.

class SALOON : public CAR 
{
  private:
    int        maxspeed;

  public:
    void       runathighspeed();
};

Nuestra clase derivada SALOON tiene dos miembros y al mismo tiempo hereda siete miembros (miembros protegidos y públicos) de la clase base CAR. Lo que quiere decir que una vez creado un objeto de SALOON, será capaz de acceder a las funciones miembro públicas de CAR que son start(), changegear(), stop() y horn() junto con su propia función miembro pública runathighspeed(). Este es el concepto de la herencia.

Al igual que algunos caracteres/comportamientos (métodos) de nuestro padre/padres (clase base) aparecen en nosotros, sus hijos (clase derivada), porque heredamos sus comportamientos (métodos/funciones), ya sea por los genes o por otros medios. Lo siento, no soy médico, pero creo que es capaz de entender lo que quiero decir. De hecho, MQL5 no admite la herencia múltiple, así que no hace falta hablar de ello.

Hmm!!! Espero que la cortina negra que cubre la misteriosa cosa llamada Programación orientada a objetos o Clase está desapareciendo poco a poco... no se canse, si en este punto no ve muy claro lo que estamos discutiendo, es posible que necesite descansar, tomar un café y luego volver y empezar desde el principio. No es tan misterioso como parece...

Si ha vuelto en este punto, supongo que está siguiendo mis explicaciones. ¿Quiero que me diga cuantas clases más podemos derivar de nuestra clase base CAR? Por favor, necesito una respuesta. Hablo en serio. Mencione sus nombres y escriba sus declaraciones y envíemelas por email. Si puede nombralas todas, le invitaré a almorzar... (no estoy bromeando).

Ahora que está preparado para más, sigamos...

Es verdad, escribo como mi padre. Su escritura a mano es muy limpia y muy elegante al igual que la mía. Creo que es algo que heredé de él, pero ¿sabe qué?; él es zurdo mientras yo soy diestro, pero cuando ve la escritura, es difícil distinguir la diferencia porque son muy parecidas. ¿Cuál es el problema aquí? He heredado una buena escritura con la mano derecha de mi padre pero no escribo con la mano izquierda como él. Esto significa que a pesar de lo que haya heredado y que parezca igual, mi manera de hacerlo es distinta a la de mi padre. ¿Tiene esto sentido para usted? Este es el denominado Polimorfismo en la Programación orientada a objetos.

Una clase derivada (yo mismo, como en el ejemplo de antes) hereda una función miembro (escribir bien () -para mi mano derecha) desde la clase base (mi padre) pero esta (yo) implementa la función (escribir bien () ) de manera distinta desde la clase base (mi padre).

Volvamos a nuestra clase CAR, y la clase derivada SALOON:

class CAR 
{
  protected:
    int        doors;
    int        sits;
    double     weight;

  public:
    bool               start();
    virtual void       changegear(){return(0);}
    void               stop();
    bool               horn();

  private:
    int        tyres;
};
class SALOON : public CAR 
{
  private:
    int        maxspeed;

  public:
    void               runathighspeed();
    virtual  void       changegear(){gear1=reverse; gear2=low; gear3=high;}
  };

class WAGON : public CAR 
{
  private:
    bool               hasliftback;

  public:
   virtual  void       changegear(){gear1=low; gear2=high; gear3=reverse;}
};

Vamos a ver unas pocas modificaciones que hemos hecho aquí. En primer lugar, hemos declarado una nueva clase derivada de CAR llamada WAGON con dos miembros. Hemos modificado también la función miembro changegear() para que se convierta en una función virtual en la clase base. ¿Por qué hemos convertido changegear() a una función virtual? Porque simplemente queremos cualquier clase que herede la función de la clase base para poder implementarla a su manera.

En otras palabras, las funciones miembro virtual de una clase son funciones miembro de esta clase y que se pueden reemplazar o implementar de manera distinta en cualquier clase derivada de la clase en la que se han declarado. El cuerpo de la función miembro puede ser reemplazado por una nueva implementación en la clase derivada. Aunque puede que no usemos la palabra virtual de nuevo en las clases derivadas, es una buena práctica de programación a usar siempre en las clases derivadas.

A partir de los ejemplos de arriba, las clases SALOON y WAGON implementan la función changegear() a su manera.

1.3. DEFINICIÓN DE LOS MÉTODOS DE UNA CLASE (FUNCIONES MIEMBRO)

Puesto que sabemos, hasta cierto punto, cómo declarar las clases; vamos a analizar más a fondo la forma de definir las funciones miembro de una clase. Después de declarar nuestra clase, nos toca definir las funciones miembro de la misma. Echemos otro vistazo a nuestra clase CAR

class CAR 
{
  protected:
    int        doors;
    int        sits;
    double     weight;

  public:
    void       CAR() // Constructor
    bool       start();
    void       changegear();
    void       stop();
    bool       horn(){press horn;}

  private:
    int        tyres;
};

 void CAR::CAR()
{
 // initialize member variables here
}

bool CAR::start()
{
 // car start procedure here
}

void CAR::changegear()
{
// car changegear procedure here
}

void CAR::stop()
{
// car stop procedure here
}

En la definición de las funciones miembro, hemos utilizado el operador de dos puntos dobles (::) llamado operador de ámbito (scope operator). Se escribe igual que las funciones normales, la única diferencia es el nombre de la clase y el operador de ámbito añadido. Observará también que una de las funciones ya fue definida en la clase (función miembro horn()). Se puede definir una función miembro en la declaración de la clase o fuera de la declaración de la clase como ha podido observar aquí.

Creo que antes de continuar, es muy importante repasar el concepto de las funciones.

1.4. FUNCIONES

Por cierto, ¿qué es una función?

A veces, en una casa donde tiene tres hijos, no le puede pedir a un solo hijo que haga todas la tareas de la casa, le pide al primero que lave los platos todos los días después de la cena, al segundo que se encargue de barrer, mientras le pide al tercero hacer las camas cada mañana.

Hay algunas tareas por hacer en casa, y en lugar de encargar todas las tareas a un niño, las dividimos entre los tres. Esto hará que las tareas sean mucho más fáciles y sencillas para cada uno de ellos, en lugar de que uno solo asuma todas las tareas. Además, si uno de los niños no hizo su tarea, sabemos en seguida quién ha sido. Esta es la idea que hay detrás de las funciones.

La mayoría de las veces queremos escribir un código para que haga muchas tareas. Es aquí donde entran las funciones en juego. Podemos dividir una tarea en varias tareas más pequeñas y luego escribir una función para llevar a cabo cada una de las pequeñas tareas.  Una función es un bloque de código que lleva a cabo o implementa una serie de operaciones. Es un conjunto de instrucciones que se ejecutan al llamarlas desde algún punto de un programa.

Se puede definir una función del siguiente modo:

Return_type function_name (parameters1,parameters2,…)
{
  Expressions; //(actions to carry out by the function)
}
  • Return_type: el tipo de datos que devuelve la función (debe ser un tipo de datos válido o nulo si no devuelve nada)
  • Function_name: el nombre de la función (debe ser un nombre válido) que se va a utilizar para llamar a la misma
  • Parameters: los parámetros son variables con datos de un tipo válido que actúan dentro de la función como variables locales. Si hay más de un parámetro en una función, se separan por coma.
  • Expressions: el cuerpo de la función que contiene el bloque de instrucciones

Ejemplo de una función:

int doaddition (int x, int y)
{
 return (x+y);
}

El tipo de retorno de la función es entero (int), doaddition es el nombre de la función y sus parámetros son int x e int y. Lo que hace la función es sumar dos parámetros de entrada que recibe y devuelve el resultado. De modo que si proporcionamos a la función dos variables enteras 2 y 3, esta hace la suma y devuelve 5 como resultado.

int doaddition(2,3) // returns 5

Para más información acerca de las funciones, consulte la Documentación de referencia de MQL5.

Ya tenemos suficiente teoría, nos ponemos manos a la obra.

El objetivo de este artículo es enseñarle cómo puede escribir una clase para su Expert Advisor mediante el enfoque Orientado a Objetos presentado en MQL5.

Es el momento de pasar a la acción...

2. Escribir un Expert Advisor

En este punto, haremos referencia al Expert Advisor que hemos creado en el primer artículo. Si no ha leído el artículo, hágalo ahora, para que no le resulte raro casi todo lo que vamos a comentar a partir de ahora. No obstante, puede que todavía tenga que revisar algunas cosas que puedan ser necesarias.

Antes de que pueda escribir su clase, primero tiene que sentarse y desarrollar su estrategia de trading. Ya hemos hecho esto en el primer artículo. El siguiente paso es elegir las funcionalidades que queremos delegar a nuestra clase. Estas funcionalidades van a determinar las variables miembro de nuestra clase. Solo un resumen de nuestra estrategia de trading del primer artículo.

Lo que va hacer nuestro EA:

  • Hará el seguimiento de un indicador concreto, y cuando se cumpla una determinada condición (o condiciones), hará una operación de trading (ya sea de Compra/Corto o Venta/Largo), dependiendo de la condición que se haya cumplido.

A esto se le conoce como estrategia de trading. Antes de que pueda escribir un EA, debe primero desarrollar la estrategia que quiere automatizar en un EA. Así que vamos a modificar, en este caso, la afirmación anterior para que refleje la estrategia que queremos desarrollar en un EA.

  • Usaremos el indicador de Promedio Móvil "Moving Average", con un período 8 (puede elegir cualquier período, pero para el propósito de nuestra estrategia, usaremos 8).
  • Queremos que nuestro EA haga una operación de trading Larga (Compra) "Long (Buy)" cuando el Moving Average-8 (que llamaremos MA-8, para simplificar) aumenta al alza y el precio de cierre está por encima de él, y que haga una operación de trading Corta (Venta) "Short (Sell)" cuando el MA-8 cae decreciendo y el precio de cierre está por debajo de él.
  • Utilizaremos también otro indicador llamado Índice de Movimiento Direccional Medio "Average Directional Movement Index" (ADX) con un período 8 también para ayudarnos a determinar si hay una tendencia en el mercado. Hacemos esto, porque lo único que queremos es realizar transacciones cuando haya una tendencia en el mercado, y relajarnos cuando el mercado está sin tendencia. Para conseguirlo, solo tendremos que poner nuestra transacción (Compra o Venta) cuando se cumplan las condiciones mencionadas anteriormente y el que valor de ADX sea superior a 22. Si ADX es superior a 22 pero decreciendo, o ADX es inferior a 22, no haremos transacciones, aunque se haya cumplido la condición B.
  • Además, queremos protegernos y minimizar las perdidas, estableciendo Stop Loss a 30 puntos, y para nuestro objetivo de beneficios, ponemos Take Profit a 100 puntos.
  • Queremos también que nuestro EA busque oportunidades de Compra/Venta únicamente cuando se forma una nueva barra, además de asegurarnos de abrir una posición de Compra si se cumplen las condiciones de Compra y que no hayan posiciones abiertas, y abrir una posición de Venta si se cumplen las condiciones de Venta y que hayan posiciones abiertas.

Además, queremos asegurarnos de que somos capaces de controlar el porcentaje de nuestro Margen libre que pueda ser utilizado en la realización de una operación de trading y también para asegurarnos de que hemos comprobado el margen libre disponible antes de realizar cualquier operación. Nuestro EA solo colocará la operación de trading si el margen disponible es suficiente para hacerlo.

Ahora sabe lo que queremos hacer. Las funciones que queremos delegar a nuestra clase:

  • Comprobar las condiciones para la Compra (Buy) y la Venta (Sell)
  • Colocar una Compra/Venta en función de los resultados de la comprobación de las condiciones.

Básicamente, esto es todo lo que queremos que haga nuestro EA. Estas dos funcionalidades son las funciones más importantes pero aún hay más. Por ejemplo, en la comprobación de las posiciones Compra/Venta, hay que utilizar los indicadores. Esto quiere decir que nuestra clase debe también incluir la obtención de los valores de los indicadores. Así que, incluimos:

  • Obtener todos los identificadores de los indicadores (en la sección OnInit del EA)
  • Obtener todos los buffers de los indicadores (en la sección OnTick del EA)
  • Liberar a todos los identificadores de los indicadores (en la sección OnDeinit del EA)

Durante la obtención de los valores de los indicadores, nuestra clase tendrá que conocer los períodos de MA y ADX, el intervalo del gráfico y el símbolo (el par de divisas con el que trabajamos), con lo cual, debemos incluir también:

  • Obtener los períodos de ADX y MA, y otros parámetros importantes, como el intervalo del gráfico y el símbolo.

Para la comprobación del margen libre antes de colocar una operación, tenemos que incluir también:

  • Comprobar el margen/porcentaje libre de la cuenta a utilizar en la operación

Con esto, tenemos ya una idea de los que deben ser las variables y las funciones en nuestra clase.

Bueno, he pensado por usted; es hora de escribir algún código.

2.1. Escribir una clase

Vamos a comenzar iniciando MetaEditor (creo que ya se lo sabe). Una vez abierto MetaEditor, creamos un nuevo documento MQL haciendo clic en el icono Nuevo (New) de la barra de herramientas o haciendo Ctrl+N. En la nueva ventana del asistente, seleccione "Incluir" (Include) y haga clic en el botón Siguiente (Next).

Figura 1. Crear un nuevo documento MQL5

Figura 1. Crear un nuevo documento MQL5

Introduzca el nombre del archivo como se muestra a continuación y haga clic en Finalizar (Finish):

Figura 2. Asignación del nombre a un nuevo documento

Figura 2. Asignación del nombre a un nuevo documento

Hemos seleccionado Incluir (Include) porque nuestra clase va a ser un archivo de inclusión que se incluirá en el código de nuestro EA una vez estemos preparados para utilizarlo. Por esta razón, no tiene ningún margen para introducir los parámetros de entrada.

Como es habitual, el editor nos proporcionará el esqueleto de  lo que se supone que queremos que haga..


Para comenzar, elimine todo lo que hay por debajo de la línea de código "#property link …". Ahora debería tener algo así.

//+------------------------------------------------------------------+
//|                                              my_expert_class.mqh |
//|                        Copyright 2010, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2010, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"

Vamos a escribir ahora la declaración de nuestra clase, que llamaremos MyExpert.

//+------------------------------------------------------------------+
//| CLASS DECLARATION                                                |
//+------------------------------------------------------------------+
class MyExpert
{

Analicemos la declaración de la clase. La declaración comienza con el nombre de la clase. A continuación, declaramos los miembros privados de la clase.

Los miembros Privados (Private):

//+------------------------------------------------------------------+
//| CLASS DECLARATION                                                |
//+------------------------------------------------------------------+
class MyExpert
{
//--- private members
private:
   int               Magic_No;   // Expert Magic Number
   int               Chk_Margin; // Margin Check before placing trade? (1 or 0)
   double            LOTS;       // Lots or volume to Trade
   double            TradePct;   // Percentage of Account Free Margin to trade
   double            ADX_min;    // ADX Minimum value
   int               ADX_handle; // ADX Handle
   int               MA_handle;  // Moving Average Handle
   double            plus_DI[];  // array to hold ADX +DI values for each bars
   double            minus_DI[]; // array to hold ADX -DI values for each bars
   double            MA_val[];   // array to hold Moving Average values for each bars
   double            ADX_val[];  // array to hold ADX values for each bars
   double            Closeprice; // variable to hold the previous bar closed price 
   MqlTradeRequest   trequest;    // MQL5 trade request structure to be used for sending our trade requests
   MqlTradeResult    tresult;     // MQL5 trade result structure to be used to get our trade results
   string            symbol;     // variable to hold the current symbol name
   ENUM_TIMEFRAMES   period;      // variable to hold the current timeframe value
   string            Errormsg;   // variable to hold our error messages
   int               Errcode;    // variable to hold our error codes

Como se mencionó antes, ninguna función de fuera de la clase puede acceder a las variables de los miembros privados. La mayoría de las declaraciones de las variables están muy claras, por lo que no quiero perder tiempo hablando de ellas.

No obstante, recordará que en nuestro análisis dijimos que las variables miembro pueden ser de cualquier tipo válido de datos, estructura o clase.

Creo que puede ver esto aquí, con la declaración de los tipos de MqlTradeRequest y MqlTradeResults.

El constructor

//--- Public member/functions
public:
   void              MyExpert();                                  //Class Constructor

El constructor no recibe ningún parámetro de entrada; por favor, tenga esto en cuenta cuando escribe su propia clase.

Las funciones miembro

//--- Public member/functions
public:
   void              MyExpert();                                 //Class Constructor
   void              setSymbol(string syb){symbol = syb;}         //function to set current symbol
   void              setPeriod(ENUM_TIMEFRAMES prd){period = prd;} //function to set current symbol timeframe/period
   void              setCloseprice(double prc){Closeprice=prc;}   //function to set prev bar closed price
   void              setchkMAG(int mag){Chk_Margin=mag;}          //function to set Margin Check value
   void              setLOTS(double lot){LOTS=lot;}               //function to set The Lot size to trade
   void              setTRpct(double trpct){TradePct=trpct/100;}   //function to set Percentage of Free margin to use for trading
   void              setMagic(int magic){Magic_No=magic;}         //function to set Expert Magic number
   void              setadxmin(double adx){ADX_min=adx;}          //function to set ADX Minimum values

Hemos definido estas funciones miembro para que nos permitan establecer las importantes variables que va a necesitar nuestra clase para su funcionamiento. Sin la utilización de estas funciones, estas variables no estarían disponibles para nuestra clase. Como se habrá fijado, ya hemos declarado la variable correspondiente en nuestra clase que guardará estos valores una vez establecidos por estas funciones.

Otra cosa a tener en cuenta es que hemos definido estas funciones miembro en la declaración de la clase. Como mencioné antes, está permitido. Esto quiere decir que no hace falta volver a definirlas a la hora de definir otras funciones, como lo podrá ver muy pronto.

Al igual que las funciones convencionales, tienen parámetros del tipo correcto de datos que depende de los valores devueltos de cada función. Creo que esto no le resulta extraño.

void              doInit(int adx_period,int ma_period);         //function to be used at our EA intialization
void              doUninit();                                  //function to be used at EA de-initializatio
bool              checkBuy();                                  //function to check for Buy conditions
bool              checkSell();                                 //function to check for Sell conditions
void              openBuy(ENUM_ORDER_TYPE otype,double askprice,double SL,
                         double TP,int dev,string comment="");   //function to open Buy positions
void              openSell(ENUM_ORDER_TYPE otype,double bidprice,double SL,
                          double TP,int dev,string comment="");  //function to open Sell positions

Solo hemos declarado estas funciones miembro, pero no las hemos definido. Puesto que lo haremos más adelante. Estas son las funciones que van a manejar la mayoría de los valores almacenados en las variables miembro de nuestra clase y al mismo tiempo son las funciones más importantes de nuestra clase. Las trataremos más adelante.

Los miembros Protegidos (Protected):

Cualquier clase derivada de nuestra clase puede heredar estos miembros. En realidad, si no tiene intención de derivar ninguna clase a partir de esta, nos son necesarios. Los puedes poner también como miembros privados. Solo estoy haciendo esto para permitirle entender los distintos puntos que hemos comentado anteriormente sobre las clases.

//--- Protected members
protected:
   void              showError(string msg, int ercode);   //function for use to display error messages
   void              getBuffers();                       //function for getting Indicator buffers
   bool              MarginOK();                         //function to check if margin required for lots is OK

Estas tres funciones son también muy importantes, aunque sean internas a nuestra clase. La función showError mostrará nuestros errores y getBuffers se empleará para obtener los buffers de los indicadores. MarginOK comprueba si hay suficiente margen libre para abrir una posición.

Al finalizar la declaración de la clase, no se olvide del punto y coma. Es muy importante.

};   // end of class declaration

Lo que hay que hacer inmediatamente después de la declaración de la clase es definir las funciones miembro que no hayan sido declaradas en la sección de la declaración.

//+------------------------------------------------------------------+
// Definition of our Class/member functions
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//|  This CLASS CONSTRUCTOR
//|  *Does not have any input parameters
//|  *Initilizes all the necessary variables                                          
//+------------------------------------------------------------------+
void MyExpert::MyExpert()
  {
//initialize all necessary variables
   ZeroMemory(trequest);
   ZeroMemory(tresult);
   ZeroMemory(ADX_val);
   ZeroMemory(MA_val);
   ZeroMemory(plus_DI);
   ZeroMemory(minus_DI);
   Errormsg="";
   Errcode=0;
  }

Este es el constructor de nuestra clase.  Aquí hemos utilizado los dos puntos dobles (::)  (el operador de ámbito (scope operator)) entre el nombre de la función y el nombre de la función miembro. Lo que intentamos decir es:

Aunque estamos definiendo esta función miembro fuera de la declaración de la clase, está todavía en el ámbito de la clase. Es un miembro de la clase, cuyo nombre se pone antes de los dos puntos dobles (operador de ámbito (scope operator)).

 No tiene ningún parámetro de entrada. Llegados a este punto, inicializamos la mayoría de las variables miembro necesarias mediante la función ZeroMemory.

void  ZeroMemory(
     void & variable      // reset variable
   );

Esta función restablece los valores de las variables que se le pasan. En este caso, la utilizamos para restablecer los valores de nuestros tipos de estructura (MqlTradeRequest y MqlTradeResult) y nuestras matrices.

La función showError:

 

//+------------------------------------------------------------------+
//|  SHOWERROR FUNCTION
//|  *Input Parameters - Error Message, Error Code                                                               
//+------------------------------------------------------------------+
void MyExpert::showError(string msg,int ercode)
  {
   Alert(msg,"-error:",ercode,"!!"); // display error
  }
 

Esta es una función miembro protegida que se utiliza para mostrar todos los errores encontrados durante las operaciones de cualquier objeto de nuestra clase. Tiene dos argumentos/parámetros -descripción del error y código del error.

La función getBuffers:

//+------------------------------------------------------------------+
//|  GETBUFFERS FUNCTION                                                                
//|  *No input parameters
//|  *Uses the class data members to get indicator's buffers
//+------------------------------------------------------------------+
void MyExpert::getBuffers()
  {
   if(CopyBuffer(ADX_handle,0,0,3,ADX_val)<0 || CopyBuffer(ADX_handle,1,0,3,plus_DI)<0
      || CopyBuffer(ADX_handle,2,0,3,minus_DI)<0 || CopyBuffer(MA_handle,0,0,3,MA_val)<0)
     {
      Errormsg="Error copying indicator Buffers";
      Errcode = GetLastError();
      showError(Errormsg,Errcode);
     }
  }
  

Se emplea esta función para copiar todos los buffers de nuestros indicadores en las matrices que hemos indicado en las variables miembro mediante el identificador del indicador correspondiente.

La función CopyBuffer ha sido descrita en el primer artículo. La función getBuffers no tiene ningún parámetro de entrada, porque estamos utilizando los valores a partir de las variables miembro de la clase.

En este punto, hemos utilizado nuestra función de error interna para mostrar cualquier error que pueda ocurrir durante el proceso de copiado de datos.

La función MarginOK:

//+------------------------------------------------------------------+
//|  MARGINOK FUNCTION
//| *No input parameters
//| *Uses the Class data members to check margin required to place a trade
//|  with the lot size is ok
//| *Returns TRUE on success and FALSE on failure
//+------------------------------------------------------------------+
bool MyExpert::MarginOK()
  {
   double one_lot_price;                                                        //Margin required for one lot
   double act_f_mag     = AccountInfoDouble(ACCOUNT_FREEMARGIN);                //Account free margin
   long   levrage       = AccountInfoInteger(ACCOUNT_LEVERAGE);                 //Leverage for this account
   double contract_size = SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);  //Total units for one lot
   string base_currency = SymbolInfoString(symbol,SYMBOL_CURRENCY_BASE);        //Base currency for currency pair
                                                                                //
   if(base_currency=="USD")
     {
      one_lot_price=contract_size/levrage;
     }
   else
     {
      double bprice= SymbolInfoDouble(symbol,SYMBOL_BID);
      one_lot_price=bprice*contract_size/levrage;
     }
// Check if margin required is okay based on setting
   if(MathFloor(LOTS*one_lot_price)>MathFloor(act_f_mag*TradePct))
     {
      return(false);
     }
   else
     {
      return(true);
     }
  }

  
 

En realidad, esta función realiza dos tareas. Hace las comprobaciones para estar seguros de disponer del suficiente margen libre para colocar una operación además de hacer las comprobaciones para estar seguros de no utilizar más de un determinado porcentaje del margen libre disponible para colocar una operación. De esta manera podemos controlar la cantidad de dinero que empleamos en cada operación.

Utilizamos la función AccountInfoDouble() junto con el identificador ENUM_ACCOUNT_INFO_DOUBLE  para obtener el Margen libre (Free Margin) de la cuenta.  Utilizamos también la función AccountInfoInteger() junto con el identificador ENUM_ACCOUNT_INFO_INTEGER para obtener el Apalancamiento (Leverage) de la cuenta.  AccountInfoInteger() y AccountInfoDouble() son funciones de cuentas utilizadas para obtener los detalles de la cuenta actual mediante el EA.

double  AccountInfoDouble(
   int  property_id      // identifier of the property
   );

También utilizamos las funciones de propiedades del símbolo SymbolInfoDouble() y SymbolInfoString() para obtener el tamaño del contrato y la divisa de referencia para el símbolo actual (par de divisas) respectivamente. La función SymbolInfoDouble() toma el nombre del símbolo y un identificador ENUM_SYMBOL_INFO_DOUBLE como parámetros, mientras la función SymbolInfoString() toma el nombre del símbolo y un identificador ENUM_SYMBOL_INFO_STRING como parámetros. Se almacenan los resultados de estas funciones en las variables declaradas para cada tipo de datos.

double  SymbolInfoDouble(
   string  name,        // symbol
   int     prop_id      // identifier of the property
   );

El cálculo que hemos hecho aquí es muy sencillo.

Para obtener el margen requerido para la colocación de una operación de trading, consideramos dos situaciones:

  1. La divisa de referencia es USD (USD/CAD, USD/CHF, USD/JPY, etc.)

Margen requerido = Tamaño del contrato por lote / Apalancamiento

     2.  La moneda de referencia no es USD (EUR/USD, etc.)

Margen requerido = Precio actual del símbolo * Tamaño del contrato por lote / Apalancamiento

Decidimos ahora comprobar si el margen requerido para operar con el tamaño o volumen del lote es superior al porcentaje o el margen libre que quiere utilizar para operar. Si el margen requerido es inferior, la función devuelve TRUE y se coloca la operación, de lo contrario, devuelve FALSE y la operación no se colocará.

La función doInit:

//+-----------------------------------------------------------------------+
// OUR PUBLIC FUNCTIONS                                                   |
//+-----------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| DOINIT FUNCTION
//| *Takes the ADX indicator's Period and Moving Average indicator's 
//| period as input parameters 
//| *To be used in the OnInit() function of our EA                                                               
//+------------------------------------------------------------------+
void MyExpert::doInit(int adx_period,int ma_period)
  {
//--- Get handle for ADX indicator
   ADX_handle=iADX(symbol,period,adx_period);
//--- Get the handle for Moving Average indicator
   MA_handle=iMA(symbol,period,ma_period,0,MODE_EMA,PRICE_CLOSE);
//--- What if handle returns Invalid Handle
   if(ADX_handle<0 || MA_handle<0)
     {
      Errormsg="Error Creating Handles for indicators";
      Errcode=GetLastError();
      showError(Errormsg,Errcode);
     }
// Set Arrays as series
// the ADX values arrays
   ArraySetAsSeries(ADX_val,true);
// the +DI value arrays
   ArraySetAsSeries(plus_DI,true);
// the -DI value arrays
   ArraySetAsSeries(minus_DI,true);
// the MA values arrays
   ArraySetAsSeries(MA_val,true);
  }

Es una función pública que pretendemos utilizar en la función OnInit() de nuestro EA, que vamos a escribir pronto y va a hacer dos cosas.

Establece los identificadores de nuestro indicadores y también lleva a cabo la operación array-set-as-series (matirz en series) de las variables de las matrices. Tiene dos parámetros de entrada proporcionados a partir el código de nuestro EA.

La función doUninit:

//+------------------------------------------------------------------+
//|  DOUNINIT FUNCTION
//|  *No input parameters
//|  *Used to release ADX and MA indicators handleS                                                                |
//+------------------------------------------------------------------+
void MyExpert::doUninit()
  {
//--- Release our indicator handles
   IndicatorRelease(ADX_handle);
   IndicatorRelease(MA_handle);
  }

  

Esta también es una función miembro pública que se va a utilizar en la función UnDeInit de nuestro EA para liberar todos los identificadores de los indicadores que hemos utilizado. No tiene ningún parámetro de entrada.

La función checkBuy:

//+------------------------------------------------------------------+
//| CHECKBUY FUNCTION
//| *No input parameters
//| *Uses the class data members to check for Buy setup based on the
//|  the defined trade strategy 
//| *Returns TRUE if Buy conditions are met or FALSE if not met
//+------------------------------------------------------------------+
bool MyExpert::checkBuy()
  {
/*
    Check for a Long/Buy Setup : MA increasing upwards, 
    previous price close above MA, ADX > ADX min, +DI > -DI
*/
   getBuffers();
//--- Declare bool type variables to hold our Buy Conditions
   bool Buy_Condition_1=(MA_val[0]>MA_val[1]) && (MA_val[1]>MA_val[2]); // MA Increasing upwards
   bool Buy_Condition_2=(Closeprice>MA_val[1]);         // previous price closed above MA
   bool Buy_Condition_3=(ADX_val[0]>ADX_min);          // Current ADX value greater than minimum ADX value
   bool Buy_Condition_4=(plus_DI[0]>minus_DI[0]);       // +DI greater than -DI
//--- Putting all together   
   if(Buy_Condition_1 && Buy_Condition_2 && Buy_Condition_3 && Buy_Condition_4)
     {
      return(true);
     }
   else
     {
      return(false);
     }
  }

  

Esta función se utiliza para comprobar si se ha establecido una condición de compra o no. Por eso devuelve el tipo bool. Con lo cual va a devolver TRUE o FALSE. Es aquí donde hemos definido nuestra estrategia de trading de Compra. Si se cumple una condición de compra en base a la estrategia que hemos definido, devuelve TRUE; pero, si la condición de Compra no se cumple entonces devuelve FALSE. Cuando utilizamos esta función en nuestro código, colocaremos entonces una compra si devuelve TRUE.

Lo primero que hicimos aquí ha sido llamar a la función miembro interna getBuffers(), que va a copiar todos los valores de la matriz requeridos por la función checkBuy a las correspondientes variables de la matriz.

Las condiciones aquí codificadas han sido mencionadas en el primer artículo.

La función checkSell:

//+------------------------------------------------------------------+
//| CHECKSELL FUNCTION
//| *No input parameters
//| *Uses the class data members to check for Sell setup based on the
//|  the defined trade strategy 
//| *Returns TRUE if Sell conditions are met or FALSE if not met
//+------------------------------------------------------------------+
bool MyExpert::checkSell()
  {
/*
    Check for a Short/Sell Setup : MA decreasing downwards, 
    previous price close below MA, ADX > ADX min, -DI > +DI
*/
   getBuffers();
//--- Declare bool type variables to hold our Sell Conditions
   bool Sell_Condition_1=(MA_val[0]<MA_val[1]) && (MA_val[1]<MA_val[2]);  // MA decreasing downwards
   bool Sell_Condition_2=(Closeprice <MA_val[1]);                         // Previous price closed below MA
   bool Sell_Condition_3=(ADX_val[0]>ADX_min);                            // Current ADX value greater than minimum ADX
   bool Sell_Condition_4=(plus_DI[0]<minus_DI[0]);                        // -DI greater than +DI

//--- Putting all together
   if(Sell_Condition_1 && Sell_Condition_2 && Sell_Condition_3 && Sell_Condition_4)
     {
      return(true);
     }
   else
     {
      return(false);
     }
  }

Al igual que la función checkbuy, esta función se utiliza para comprobar si se ha establecido una condición de venta o no. Por eso, también devuelve el tipo bool. Con lo cual va a devolver TRUE o FALSE.  Es aquí donde hemos definido nuestra estrategia de trading de Venta. Si se cumple una condición de venta en base a la estrategia que hemos definido, devuelve TRUE; pero, si la condición de venta no se cumple entonces devuelve FALSE.

Cuando utilizamos esta función en nuestro código, colocaremos una venta si devuelve TRUE. Justo al igual que con checkBuy, primero llamamos a la función interna getBuffers(). Las condiciones aquí codificadas también han sido mencionadas en el primer artículo.

La función openBuy:

//+------------------------------------------------------------------+
//| OPENBUY FUNCTION
//| *Has Input parameters - order type, Current ASK price, Stop Loss,
//|  Take Profit, deviation, comment
//| *Checks account free margin before pacing trade if trader chooses
//| *Alerts of a success if position is opened or shows error
//+------------------------------------------------------------------+
void MyExpert::openBuy(ENUM_ORDER_TYPE otype,double askprice,double SL,double TP,int dev,string comment="")
  {
//--- do check Margin if enabled
   if(Chk_Margin==1)
     {
      if(MarginOK()==false)
        {
         Errormsg= "You do not have enough money to open this Position!!!";
         Errcode =GetLastError();
         showError(Errormsg,Errcode);
        }
      else
        {
         trequest.action=TRADE_ACTION_DEAL;
         trequest.type=otype;
         trequest.volume=LOTS;
         trequest.price=askprice;
         trequest.sl=SL;
         trequest.tp=TP;
         trequest.deviation=dev;
         trequest.magic=Magic_No;
         trequest.symbol=symbol;
         trequest.type_filling=ORDER_FILLING_FOK;
         // send
         OrderSend(trequest,tresult);
         // check result
         if(tresult.retcode==10009 || tresult.retcode==10008) //Request successfully completed 
           {
            Alert("A Buy order has been successfully placed with Ticket#:",tresult.order,"!!");
           }
         else
           {
            Errormsg= "The Buy order request could not be completed";
            Errcode =GetLastError();
            showError(Errormsg,Errcode);
           }
        }
     }
   else
     {
      trequest.action=TRADE_ACTION_DEAL;
      trequest.type=otype;
      trequest.volume=LOTS;
      trequest.price=askprice;
      trequest.sl=SL;
      trequest.tp=TP;
      trequest.deviation=dev;
      trequest.magic=Magic_No;
      trequest.symbol=symbol;
      trequest.type_filling=ORDER_FILLING_FOK;
      //--- send
      OrderSend(trequest,tresult);
      //--- check result
      if(tresult.retcode==10009 || tresult.retcode==10008) //Request successfully completed 
        {
         Alert("A Buy order has been successfully placed with Ticket#:",tresult.order,"!!");
        }
      else
        {
         Errormsg= "The Buy order request could not be completed";
         Errcode =GetLastError();
         showError(Errormsg,Errcode);
        }
     }
  }

Esta es la función que abre una posición de compra a cada llamada de nuestro EA. Como parámetros de entrada, tiene la mayoría de las variables que puedan hacer falta para colocar una operación de trading; y algunas de las variables están proporcionadas mediante el código de nuestro EA. Se habrá dado cuenta, como se mencionó en el primer artículo, que aquí hemos utilizado las variables de tipo MqlTraderequest.

Vamos a tener que utilizarlas en el código de nuestro EA. Antes de colocar una operación de trading, queremos confirmar si el usuario desea comprobar el margen, si el valor de Chk_Margin (que se obtiene a partir del EA) es 1, entonces llamamos a la función MarginOK() para que lo haga por nosotros. El resultado de esta función es determinante para el siguiente paso.  Sin embargo, si el usuario no desea comprobar el margen, simplemente continuamos y colocamos la operación.

La función openSell:

//+------------------------------------------------------------------+
//| OPENSELL FUNCTION
//| *Has Input parameters - order type, Current BID price, Stop Loss,
//|  Take Profit, deviation, comment
//| *Checks account free margin before pacing trade if trader chooses
//| *Alerts of a success if position is opened or shows error
//+------------------------------------------------------------------+
void MyExpert::openSell(ENUM_ORDER_TYPE otype,double bidprice,double SL,double TP,int dev,string comment="")
  {
//--- do check Margin if enabled
   if(Chk_Margin==1)
     {
      if(MarginOK()==false)
        {
         Errormsg= "You do not have enough money to open this Position!!!";
         Errcode =GetLastError();
         showError(Errormsg,Errcode);
        }
      else
        {
         trequest.action=TRADE_ACTION_DEAL;
         trequest.type=otype;
         trequest.volume=LOTS;
         trequest.price=bidprice;
         trequest.sl=SL;
         trequest.tp=TP;
         trequest.deviation=dev;
         trequest.magic=Magic_No;
         trequest.symbol=symbol;
         trequest.type_filling=ORDER_FILLING_FOK;
         // send
         OrderSend(trequest,tresult);
         // check result
         if(tresult.retcode==10009 || tresult.retcode==10008) //Request successfully completed 
           {
            Alert("A Sell order has been successfully placed with Ticket#:",tresult.order,"!!");
           }
         else
           {
            Errormsg= "The Sell order request could not be completed";
            Errcode =GetLastError();
            showError(Errormsg,Errcode);
           }
        }
     }
   else
     {
      trequest.action=TRADE_ACTION_DEAL;
      trequest.type=otype;
      trequest.volume=LOTS;
      trequest.price=bidprice;
      trequest.sl=SL;
      trequest.tp=TP;
      trequest.deviation=dev;
      trequest.magic=Magic_No;
      trequest.symbol=symbol;
      trequest.type_filling=ORDER_FILLING_FOK;
      //--- send
      OrderSend(trequest,tresult);
      //--- check result
      if(tresult.retcode==10009 || tresult.retcode==10008) //Request successfully completed 
        {
         Alert("A Sell order has been successfully placed with Ticket#:",tresult.order,"!!");
        }
      else
        {
         Errormsg= "The Sell order request could not be completed";
         Errcode =GetLastError();
         showError(Errormsg,Errcode);
        }
     }
  }

Al igual que la función openBuy, esta es la función que abre una posición de venta a cada llamada de nuestro EA. Como parámetros de entrada, tiene la mayoría de las variables que puedan hacer falta para colocar una operación de trading; y algunas de las variables están proporcionadas mediante el código de nuestro EA.

Al igual que hicimos con la apertura de una posición de Compra, antes de colocar una operación de trading, queremos confirmar si el usuario desea comprobar el margen, si el valor de Chk_Margin (que se obtiene a partir del EA) es 1, entonces llamamos a la función MarginOK() para que lo haga por nosotros.

El resultado de esta función es determinante para el siguiente paso. Sin embargo, si el usuario no desea comprobar el margen, simplemente continuamos y colocamos la operación.

Hemos terminado ahora con la declaración y la definición de nuestra clase y las funciones miembro, no obstante, nos hemos dejado algunas otras tareas que pretendemos manejar en el código de nuestro EA. Esto incluye la comprobación de las barras disponibles, la comprobación de las barras nuevas y la comprobación de las posiciones abiertas disponibles. Se van a manejar en el código de nuestro EA.

Para ver una lista de todas las funciones y métodos de nuestra clase, haga clic el comando/menú en el MetaEditor como se muestra a continuación. La función muestra todas las funciones miembro, incluyendo el Destructor que no hemos declarado de forma explícita en nuestro código.

Los miembros protegidos están señalados con unas flechas verdes mientras el Constructor y el Destructor están señalados por flechas azules.

 

Las funciones miembro de la clase

Figura 3. Las funciones miembro de nuestra clase mostrando el destructor de la clase

¿Qué es lo siguiente? 

¿Le he oído decir, depurar? Igual tiene razón. Siempre es bueno probar y ver si su código tiene errores, de lo contrario estará decepcionado cuando los presente al público. El problema aquí es que se trata solo de un archivo de inclusión, no es un código de un Expert Advisor ni un script ni un código de indicador que se pueda conectar al gráfico. Llegados a este punto, tiene dos opciones (desde mi propia experiencia),

  • puede pulsar el botón de depuración en su editor de manera que el depurador informe de cualquier error, excepto el error ‘no executable file produced’, que se muestra por el hecho de que un archivo .mqh no se puede compilar en un archivo .ex5.  O
  • Seguir adelante y escribir el código para el EA que se va a utilizar en su clase. Una vez comienza la depuración del EA, se comprobará el archivo incluido junto a él. De hecho, este es el mejor y más aceptado método para hacerlo.

Figura 4. No se puede compilar los archivos .mqh

2.2. ESCRIBIR UN EXPERT ADVISOR

Supongo que su editor sigue abierto. Genere un nuevo documento, pero esta vez, elija Expert Advisor. (Consulte el primer artículo para más detalles). Esta vez, asigne el nombre ‘my_oop_ea’ al EA.

Es aquí donde debería estar ahora:


Estamos ya preparados para escribir nuestro EA basado en la Programación orientada a objetos.

Lo primero que vamos a hacer ahora es incluir la clase que acabamos de escribir mediante el comando #include del preprocesador. Incluya la clase justo después del último comando de propiedades del preprocesador

//+------------------------------------------------------------------+
//|                                                    my_oop_ea.mq5 |
//|                        Copyright 2010, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2010, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
// Include  our class
#include <my_expert_class.mqh>

Hay dos maneras de incluir un archivo,

// Include  using angle brackets
#include <my_expert_class.mqh>
// Include  using quotations
#include "my_expert_class.mqh"

Cuando usamos los paréntesis angulares (< ... >), significa que el archivo a incluir será tomado del repertorio de inclusión estándar (eso es, la carpeta Include dentro del repertorio de MQL5).  El repertorio actual que es el repertorio Experts dentro del repertorio MQL5 no se tendrá en cuenta como posible ubicación para buscar el archivo. Sin embargo, si el archivo se encuentra entre comillas dobles (" ... "), se considerará que está en el repertorio actual (que es la carpeta Experts) y no se comprobará el directorio estándar (la carpeta Include).

Si se guarda su clase en la carpeta Folder (el repertorio estándar) y utiliza las comillas dobles en lugar de los paréntesis angulares o viceversa, le aparecerá un error al compilar el código.


Figura 5. Un mensaje de error que aparece cuando no se puede encontrar el archivo de inclusión

PARÁMETROS DE ENTRADA DEL EA

//--- input parameters
input int      StopLoss=30;      // Stop Loss
input int      TakeProfit=100;   // Take Profit
input int      ADX_Period=14;    // ADX Period
input int      MA_Period=10;     // Moving Average Period
input int      EA_Magic=12345;   // EA Magic Number
input double   Adx_Min=22.0;     // Minimum ADX Value
input double   Lot=0.2;          // Lots to Trade
input int      Margin_Chk=0;     // Check Margin before placing trade(0=No, 1=Yes)
input double   Trd_percent=15.0; // Percentage of Free Margin To use for Trading

La mayoría de estos parámetros de entrada no son nuevos. Vamos a analizar los nuevos.

Hemos introducido una variable entera para contener el valor 1 si queremos utilizar la comprobación del margen y sino 0. Hemos declarado otra variable para contener el porcentaje máximo del Margen libre a utilizar en la apertura de una posición. Se van a utilizar estos valores más adelante en el objeto de nuestra clase una vez creado.

Inmediatamente después de los parámetros de entrada, definimos dos parámetros (STP y TKP) que queremos que sean capaces de manejar (para cubrir precios de 5 y 3 dígitos), puesto que no podemos cambiar los valores de las variables de entrada. Y luego creamos un objeto para nuestra clase para utilizar dentro del código de nuestro EA.

//--- Other parameters
int STP,TKP;   // To be used for Stop Loss & Take Profit values
// Create an object of our class
MyExpert Cexpert;

Como se mencionó antes, para crear un objeto de una clase, utiliza el nombre de la clase seguido del nombre del objeto que quiere crear. Aquí tenemos que crear un objeto Cexpert que pertenece al tipo MyExpert de nuestra clase. Se puede utilizar Cexpert ahora para acceder a todas las funciones miembro de la clase MyExpert.

SECCIÓN DE INICIALIZACIÓN DEL EA

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {

//--- Run Initialize function
   Cexpert.doInit(ADX_Period,MA_Period);
//--- Set all other necessary variables for our class object
   Cexpert.setPeriod(_Period);     // sets the chart period/timeframe
   Cexpert.setSymbol(_Symbol);     // sets the chart symbol/currency-pair
   Cexpert.setMagic(EA_Magic);    // sets the Magic Number
   Cexpert.setadxmin(Adx_Min);    // sets the ADX miniumm value
   Cexpert.setLOTS(Lot);          // set the Lots value
   Cexpert.setchkMAG(Margin_Chk); // set the margin check variable
   Cexpert.setTRpct(Trd_percent); // set the percentage of Free Margin for trade
//--- Let us handle brokers that offers 5 digit prices instead of 4
   STP = StopLoss;
   TKP = TakeProfit;
   if(_Digits==5 || _Digits==3)
     {
      STP = STP*10;
      TKP = TKP*10;
     }  
//---
   return(0);
  }

Llegados a este punto, hemos llamado a la función doInit de nuestra clase y le proporcionamos las variables del intervalo de tiempo. A continuación, establecemos todas las otras variables que va a necesitar el objeto que acabamos de crear, de modo que se almacenan en las variables miembro del objeto mediante las funciones ya descritas al escribir la clase.

La siguiente línea de códigos no debe ser extraña, simplemente decidimos ajustar los valores de nuestro Stop Loss y Take Profit a los precios de tres y cinco dígitos.

SECCIÓN DE DESINICIALIZACIÓN DEL EA

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Run UnIntilialize function
   Cexpert.doUninit();
  }

Hemos llamado a la función doUninit de la clase para así liberar todos los identificadores de los indicadores que deben haber sido creados en la función de inicialización EA.

SECCIÓN ONTICK DEL EA

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Do we have enough bars to work with
   int Mybars=Bars(_Symbol,_Period);
   if(Mybars<60) // if total bars is less than 60 bars
     {
      Alert("We have less than 60 bars, EA will now exit!!");
      return;
     }

//--- Define some MQL5 Structures we will use for our trade
   MqlTick latest_price;      // To be used for getting recent/latest price quotes
   MqlRates mrate[];          // To be used to store the prices, volumes and spread of each bar
/*
     Let's make sure our arrays values for the Rates
     is store serially similar to the timeseries array
*/
// the rates arrays
   ArraySetAsSeries(mrate,true);

Lo primero que hacemos aquí es comprobar el total de las barras disponibles. Si no es suficiente para que nuestro EA pueda operar, no va a operar hasta que no hayan suficientes barras (es decir, 60 barras). Después hemos declarado dos variables de la estructura MQL5 (MqlTick y MqlRates). Y por último, utilizamos la función ArraySetAsSeries en la matriz de tipos de cambio.

//--- Get the last price quote using the MQL5 MqlTick Structure
   if(!SymbolInfoTick(_Symbol,latest_price))
     {
      Alert("Error getting the latest price quote - error:",GetLastError(),"!!");
      return;
     }

//--- Get the details of the latest 3 bars
   if(CopyRates(_Symbol,_Period,0,3,mrate)<0)
     {
      Alert("Error copying rates/history data - error:",GetLastError(),"!!");
      return;
     }

//--- EA should only check for new trade if we have a new bar
// lets declare a static datetime variable
   static datetime Prev_time;
// lest get the start time for the current bar (Bar 0)
   datetime Bar_time[1];
// copy time
   Bar_time[0] = mrate[0].time;
// We don't have a new bar when both times are the same
   if(Prev_time==Bar_time[0])
     {
      return;
     }
//copy time to static value, save
   Prev_time = Bar_time[0]; 
   

Aquí, hemos utilizado la función SymbolInfoTick para obtener la última cotización de los precios y la función CopyRates para obtener los últimos tipos de cambio para las tres barras anteriores (barra actual incluida). Las siguientes líneas de código comprueban si tenemos una barra nueva. Hemos declarado dos variables datetime, una es la variable estática (Prev_Time) y la otra es Bar_Time.

Si tenemos una barra nueva, se almacena el tiempo de la barra en la variable estática Prev_Time de modo que seremos capaces de comparar sus valores con el valor de Bar_time al siguiente tick. Al próximo tick, si Prev_Time es igual a Bar_Time, entonces es aún la misma barra cuyo tiempo se ha almacenado. Por lo que nuestro EA descansará.

Pero si Bar_Time no es igual a Prev_Time, entonces tenemos una barra nueva. Decidimos almacenar el tiempo de inicio de la barra nueva en la variable datetime estática, Prev_Time y nuestro EA pueden proceder ahora a hacer las comprobaciones para nuevas oportunidades de COMPRA (BUY) o VENTA (SELL).

//--- we have no errors, so continue
//--- Do we have positions opened already?
    bool Buy_opened = false, Sell_opened=false; // variables to hold the result of the opened position
    
    if (PositionSelect(_Symbol) ==true)  // we have an opened position
    {
         if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
         {
            Buy_opened = true;  //It is a Buy
         }
         else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
         {
            Sell_opened = true; // It is a Sell
         }
    }

Decidimos comprobar si ya hay una nueva posición. Solo queremos asegurarnos de que no hay ninguna Compra abierta al abrir una Compra y ninguna Venta abierta al abrir una Venta.

// Copy the bar close price for the previous bar prior to the current bar, that is Bar 1
   Cexpert.setCloseprice(mrate[1].close);  // bar 1 close price
//--- Check for Buy position
   if(Cexpert.checkBuy()==true)
     {
      // Do we already have an opened buy position
      if(Buy_opened)
        {
         Alert("We already have a Buy Position!!!");
         return;    // Don't open a new Buy Position
        }
      double aprice = NormalizeDouble(latest_price.ask,_Digits);              // current Ask price
      double stl    = NormalizeDouble(latest_price.ask - STP*_Point,_Digits); // Stop Loss
      double tkp    = NormalizeDouble(latest_price.ask + TKP*_Point,_Digits); // Take profit
      int    mdev   = 100;                                                    // Maximum deviation
      // place order
      Cexpert.openBuy(ORDER_TYPE_BUY,aprice,stl,tkp,mdev);
     }

Hemos vuelto ahora al objeto que hemos creado, ¿por qué? Porque hemos sido capaces de hacer todas las comprobaciones necesarias que requiere nuestro objeto para hacer su trabajo.

Lo primero que hacemos es obtener el precio de cierre de la barra anterior mediante la función miembro setCloseprice de nuestros objetos.

Después llamamos a la función checkBuy para averiguar si se cumplen las condiciones para comprar, si devuelve TRUE, entonces queremos estar seguros de que no tenemos ninguna posición de compra ya abierta. Si no tenemos una posición de compra ya abierta, preparamos las variables necesarias para colocar nuestra orden (el tipo de orden, el precio actual de DEMANDA -ASK, Stop Loss, Take Profit y la desviación máxima) y llamamos a la función openBuy. Observe lo fácil que es utilizar la clase que hemos escrito. 

//--- Check for any Sell position
   if(Cexpert.checkSell()==true)
     {
      // Do we already have an opened Sell position
      if(Sell_opened)
        {
         Alert("We already have a Sell position!!!");
         return;    // Don't open a new Sell Position
        }
      double bprice=NormalizeDouble(latest_price.bid,_Digits);                 // Current Bid price
      double bstl    = NormalizeDouble(latest_price.bid + STP*_Point,_Digits); // Stop Loss
      double btkp    = NormalizeDouble(latest_price.bid - TKP*_Point,_Digits); // Take Profit
      int    bdev=100;                                                         // Maximum deviation
      // place order
      Cexpert.openSell(ORDER_TYPE_SELL,bprice,bstl,btkp,bdev);
     }

Esto es igual a lo que hicimos arriba. Puesto que estamos haciendo las comprobaciones para una Venta, hemos llamado a la función checkSell y si devuelve TRUE y no tenemos ya una posición de venta abierta, preparamos las variables necesarias para colocar nuestra orden (el tipo de orden, el precio actual de OFERTA -BID, Stop Loss, Take Profit y la desviación máxima) y llamamos a la función openSell.

Muy sencillo, ¿verdad? Hemos finalizado la escritura de los códigos. Es el momento ahora de depurar nuestro código. Si no sabe utilizar un depurador, le recomiendo leer el primer artículo para comprender mejor su funcionamiento.

Al pulsar F5 o el botón de depuración, se hace la inclusión y la comprobación del archivo incluido (nuestra clase), y si hay algún error, se informa de ello. Cuando vemos el error, tenemos que volver al código y corregirlo.

Figura 6. Nuestro archivo de inclusión se incluye al depurar el código principal del EA

Si todo va bien, entonces lo ha hecho muy bien. Es el momento de probar nuestro EA mediante el Probador de estrategias. Tenemos que compilar nuestro EA antes de probarlo con el Probador de estrategias. Para ello, haga clic en el botón Compilar (Compile) o pulse el botón F7 del teclado de tu ordenador.

Figura 7. Haga clic en el botón Compilar (Compile) del menú para compilar nuestro código

Para iniciar el probador de estrategias, entre en Ver --> Probador de Estrategias (View --> Strategy Tester) o pulse CTR+R para iniciar el probador de estrategias, desde la barra del menú del terminal de trading. (Para más detalles sobre la utilización del probador de estrategias, le recomiendo leer el primer artículo).

Para que pueda probar el EA con el probador de estrategias, debe antes que nada compilarlo. Si no lo compila, tendrá un error al seleccionar el Expert Advisor en la configuración del probador de estrategias. (Acabo de descubrir esto en la nueva versión del terminal.)

Figura 8. Hay que compilar el código del EA antes de utilizarlo en el Probador de Estrategias

A continuación están los resultados del Probador de estrategias con nuestro Expert Advisor basado en POO.

 Figura 9. Los resultados del trading de nuestro Expert Advisor Orientado a objetos

El gráfico:

Figura 10. Los resultados gráficos de nuestro Expert Advisor Orientado a objetos

El informe/diario de la actividad del trading:


Figura 11. Los resultados de la actividad del trading de nuestro Expert Advisor Orientado a objetos


El gráfico de la prueba:

Figura 12. Los resultados gráficos del trading de nuestro Expert Advisor Orientado a objetos

Conclusión

En este artículo hemos analizado, hasta cierto nivel, los fundamentos de una clase y como utilizarla en la escritura de un Expert Advisor sencillo. No hemos profundizado demasiado en las partes avanzadas de las clases, pero lo que hemos tratado en este artículo es suficiente para ayudarle a desarrollar por sí mismo el código de su propio Expert Advisor orientado a objetos. 

Hemos comentado también la manera de comprobar el margen libre, de modo que nuestro EA no haga operaciones cuando el margen libre disponible no es suficiente para la posición que queremos abrir.

Estará de acuerdo conmigo que el nuevo lenguaje MQL5 tiene mucho más que ofrecer y que no tiene que ser un maestro de la programación para sacarle ventaja a este nuevo lenguaje. Esta es la razón principal de la escritura de las guías paso a paso.


Traducción del inglés realizada por MetaQuotes Software Corp.
Artículo original: https://www.mql5.com/en/articles/116

Archivos adjuntos |
my_expert_class.mqh (17.52 KB)
my_oop_ea.mq5 (6.77 KB)
Solución sin DLL para la comunicación entre terminales MetaTrader 5 mediante canalizaciones con nombre Solución sin DLL para la comunicación entre terminales MetaTrader 5 mediante canalizaciones con nombre

El artículo describe el modo de implementar la comunicación entre procesos, entre terminales de cliente MetaTrader 5 mediante canalizaciones con nombre. Se desarrolla la clase CNamedPipes para utilizar las canalizaciones con nombre. Con el fin de probar su uso y medir el rendimiento de la conexión, se proporciona el tick del indicador y los scripts del servidor y el cliente. El uso de las canalizaciones con nombre es suficiente para las cotizaciones en tiempo real.

Una librería par la construcción de gráficos mediante Google Chart API Una librería par la construcción de gráficos mediante Google Chart API

La construcción de distintos tipos de diagramas constituye una parte esencial del análisis de la situación del mercado y de las pruebas de los sistemas de trading. Con frecuencia, para construir un diagrama sofisticado, es necesario organizar los datos de las salidas en un archivo, y luego utilizarlos en otras aplicaciones como MS Excel. Esto no es muy práctico y nos priva de la posibilidad de actualizar los datos de manera dinámica. Google Chart API proporciona los medios para crear gráficos en línea, mediante el envío de una petición especial al servidor. En este artículo, trataremos de automatizar el proceso de creación de esta petición y obtener el gráfico a partir del servidor de Google.

El método óptimo para el cálculo del volumen total de una posición mediante un número mágico determinado El método óptimo para el cálculo del volumen total de una posición mediante un número mágico determinado

En este artículo se analiza el problema del cálculo del volumen total de la posición de un determinado símbolo y número mágico. El método propuesto requiere solamente la parte estrictamente necesaria del historial de las transacciones, encuentra el tiempo más próximo cuando el total de la posición es igual a cero, y lleva a cabo los cálculos con las últimas transacciones. También se analiza el trabajo del terminal de cliente con variables globales.

Utilización de la función TesterWithdrawal() para modelizar las retiradas de beneficio Utilización de la función TesterWithdrawal() para modelizar las retiradas de beneficio

En este artículo se describe la utilización de la función TesterWithDrawal() para estimar los riesgos en un sistema de trading que implica la retirada de cierta parte de los activos durante su funcionamiento. Además, describe cómo afecta esta función al algoritmo de cálculo de la reducción del patrimonio en el probador de estrategias. Esta función es muy útil durante la optimización de los parámetros de sus Asesores Expertos.