English Русский Deutsch 日本語 Português
preview
Paradigmas de programación (Parte 2): Enfoque orientado a objetos para el desarrollo de EA basados en la dinámica de precios

Paradigmas de programación (Parte 2): Enfoque orientado a objetos para el desarrollo de EA basados en la dinámica de precios

MetaTrader 5Ejemplos | 5 septiembre 2024, 08:33
13 0
Kelvin Muturi Muigua
Kelvin Muturi Muigua

Introducción

En el primer artículo, aprendimos a implementar la programación procedimental en MQL5. También tocamos el tema de la programación funcional. Asimismo, aprendimos los fundamentos de la programación procedimental y creamos un Asesor Experto Price Action básico utilizando el indicador de media móvil exponencial (EMA) y datos de velas.

Esta vez examinaremos el paradigma de la programación orientada a objetos. Después de ello, aplicaremos estos conocimientos para convertir el código procedimental del EA desarrollado previamente en el primer artículo en código orientado a objetos. Esto nos permitirá comprender mejor las principales diferencias entre ambos paradigmas.

Querría llamar su atención sobre el hecho de que el propósito de este artículo no es demostrar cómo funciona la estrategia de acción del precio. Nuestro objetivo es mostrar cómo funcionan los diferentes paradigmas de programación y cómo implementarlos en MQL5. El sencillo Asesor Experto Price Action que estamos desarrollando es solo una demostración de cómo poner en práctica los conocimientos.


Comprensión de la programación orientada a objetos

La programación orientada a objetos (POO) es un estilo de escritura de código en el que este se organiza en torno a objetos. Los componentes se consideran modelos de determinadas cosas o conceptos.

Al empezar a trabajar con la programación orientada a objetos, especialmente cuando partimos de la programación procedimental, suelen surgir dudas. Empezaremos precisamente con estas cuestiones para facilitar la posterior asimilación del material.


¿Qué es una clase en programación orientada a objetos?


Una clase es un modelo o proyecto para crear objetos de un tipo determinado. Una clase posee un conjunto de propiedades (atributos) que describen las características del objeto y funciones (métodos) que realizan diversas tareas.

Echemos un vistazo, como ejemplo, al teléfono.

Supongamos que creamos una empresa que fabrica teléfonos. Tenemos una reunión con el jefe de diseño de producto. Nuestro objetivo será crear un plano del teléfono perfecto que fabricará nuestra empresa. En esta reunión se discutirán las características y funciones básicas que deberá tener todo teléfono.

Empezaremos creando un proyecto que será el punto de partida de todos los teléfonos que produzca la empresa. En programación orientada a objetos, un proyecto de este tipo se denomina clase.

El diseñador de productos sugerirá que, para crear un diseño, primero deberemos hacer una lista de las distintas tareas que el teléfono es capaz de realizar. Obtendremos una lista de tareas como esta:

  • Realizar y recibir llamadas telefónicas.
  • Enviar y recibir SMS.
  • Enviar y recibir datos por Internet.
  • Hacer fotos y grabar vídeos.

En la programación orientada a objetos, estas tareas se denominan métodos. Los métodos son similares a las funciones normales, pero cuando se crean en una clase, se denominan métodos.

Entonces decidiremos que cada teléfono deberá tener ciertas prestaciones y características. Para ello, haremos una lista como esta:

  • Número de modelo.
  • Color.
  • Tipo de entrada.
  • Tipo de pantalla.
  • Tamaño de pantalla.

En programación orientada a objetos (POO), las propiedades y características descritas en un modelo (clase) se denominan atributos o variables de clase. Los atributos se declararán en la clase como variables.


¿Qué es un objeto en la POO?


Un objeto es una implementación de una clase. En pocas palabras, la clase es el plan o proyecto sobre el papel, mientra que el objeto es la aplicación real del plan o proyecto.

Siguiendo con el ejemplo de la empresa telefónica, digamos que usted y su diseñador de producto han finalizado el diseño del teléfono sobre el papel. Ahora han decidido producir dos tipos diferentes de teléfonos para mercados de consumo distintos. El primer modelo será una versión económica que solo podrá hacer o recibir llamadas y enviar o recibir mensajes de texto. El segundo modelo será una versión de gama alta (smartphone) con todas las prestaciones del modelo económico, una cámara de calidad, una gran batería y una pantalla táctil de alta resolución.

Tras dirigirse al departamento de ingeniería, le da el proyecto del teléfono (clase) al ingeniero jefe, así como las instrucciones para que lo haga realidad. El trabajo comienza de inmediato. Transcurrido un cierto tiempo, el ingeniero finaliza los teléfonos para usted. Una vez terminado, le entrega el producto acabado para que lo pruebe.

Los teléfonos que tiene ahora mismo en las manos son objetos de la clase. El modelo de teléfono económico solo implementará algunos métodos de la clase, mientras que el teléfono más avanzado implementará todos los métodos de la clase.

Veamos ahora un ejemplo de este teléfono usando código. Siguiendo estos pasos, podremos crear un archivo de clase en IDE MetaEditor.


Paso 1: Abrimos el MetaEditor e iniciamos el Wizard pulsando el botón "Crear".

Creamos un nuevo archivo de inclusión en el Wizard del MetaEditor


Segundo paso: Seleccionamos Nueva clase y hacemos clic en Siguiente.

Nuevo archivo de clase en el Wizard MQL


Paso 3: En la venta de creación de una clase, en el campo Nombre de la clase: indicamos PhoneClass, y en el campo archivo Include: indicamos Experts\OOP_Article\PhoneClass.mqh, para almacenar el archivo de la clase en la misma carpeta donde el código fuente de nuestro asesor. Dejamos vacío el campo Clase básica:. Pulsamos "Finalizar" y se creará el nuevo archivo de clase en MQL5.

Detalles de la nueva clase en el Wizard MQL


Ahora tendremos un archivo en blanco de la clase MQL5. Hemos añadido comentarios para explicar las diferentes partes de la clase. De hecho, resulta bastante fácil crear una nueva clase en MQL5 con el Wizard automático MQL en MetaEditor IDE. Mire la sintaxis en el código de abajo: contiene el punto de partida de un archivo de clase MQL5 correctamente estructurado.

class PhoneClass //class name
  {
private: //access modifier

public:  //access modifier
                     PhoneClass(); //constructor method declaration
                    ~PhoneClass(); //destructor method declaration
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
PhoneClass::PhoneClass() //constructor method definition
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
PhoneClass::~PhoneClass() //destructor method definition
  {
  }
//+------------------------------------------------------------------+



Ignore el código en #properties, no es relevante para el tema que nos ocupa. La sintaxis que nos interesa comenzará en la línea donde se encuentra la sintaxis del principio de la clase: class PhoneClass { justo debajo de la línea #property version "1.00".

//+------------------------------------------------------------------+
//|                                                   PhoneClass.mqh |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//---------------------------------------
//Ignore the code segment above
//---------------------------------------


Vamos a echar un vistazo al archivo de clase que acabamos de generar.

Después de la llave de apertura de la clase encontraremos dos modificadores de acceso: private (privado) y public (público). Entraremos en más detalles al respecto cuando analicemos el tema de la herencia.

Bajo los modificadores de acceso privados añadiremos los atributos de clase (propiedades y características del teléfono) de nuestro proyecto de teléfono. Asimismo, añadiremos estas propiedades como variables globales.

private: //access modifier
   //class attributes
   int               modelNumber;
   string            phoneColor;
   string            inputType;
   string            screenType;
   int               screenSize;

En la línea bajo el modificador de acceso public: van las declaraciones del constructor y el destructor. Estos dos métodos son similares a las funciones estándar del EA OnInit() y OnDeInit(). El constructor de la clase realizará operaciones similares a OnInit(), mientras que el destructor realizará operaciones similares a OnDeinit().

public: //access modifier
                     PhoneClass(); //constructor method declaration
                    ~PhoneClass(); //destructor method declaration


¿Qué son los constructores y destructores en la POO?


Constructores

Un constructor es un método especial dentro de una clase que se llama y ejecuta automáticamente al crearse un objeto de esa clase. Su propósito principal consiste en inicializar los atributos del objeto y realizar cualquier acción de configuración necesaria para mantener el objeto en un estado válido y actualizado.

Las principales características de los constructores son:

  • El nombre es el mismo que el de la clase: el constructor tiene el mismo nombre que la clase. Esta regla de nomenclatura permite al lenguaje de programación identificar y asociar un constructor a una clase. Por defecto, en MQL5 todos los constructores son de tipo void, es decir, no retornan ningún valor.
  • Inicialización: los constructores son responsables de inicializar los atributos del objeto con valores predeterminados o especificados. Esto garantizará que el objeto parta de un estado bien definido. Mostraremos cómo funciona esto en nuestra clase PhoneClass.
  • Ejecución automática: el constructor se llama o ejecuta automáticamente al crearse el objeto. Esto ocurre en el momento en que se crea el ejemplar del objeto.
  • Parámetros adicionales: los constructores pueden admitir parámetros adicionales, lo cual permite personalizarlos en el momento de la creación del objeto. Estos parámetros indican los valores que el constructor utiliza para establecer el estado inicial del objeto.

Puede haber varios constructores en la misma clase, pero deberán diferir en el conjunto de argumentos o parámetros. Según los parámetros, los constructores se clasifican en:

  • Constructores por defecto: constructores sin parámetros.
  • Constructores paramétricos: constructores que tienen parámetros. Si alguno de los parámetros del constructor hace referencia a un objeto de la misma clase, se convertirá automáticamente en un constructor de copiado.
  • Un constructor de copiado es un constructor que tiene uno o más parámetros que hacen referencia a un objeto de la misma clase.

Ahora necesitaremos una forma de inicializar o almacenar los atributos de clase (variables) con los datos específicos del teléfono cada vez que creamos un nuevo objeto PhoneClass. Realizaremos esta tarea utilizando el constructor paramétrico. Luego cambiaremos el constructor actual por defecto y lo convertiremos en un constructor paramétrico con cinco parámetros. Después comentaremos la declaración de la función constructora por defecto antes de declarar y definir el nuevo constructor paramétrico que sigue en el código.

   //PhoneClass();
   //constructor declaration and definition
   PhoneClass(int modelNo, string colorOfPhone, string typeOfInput, string typeOfScreen, int sizeOfScreen)
     {      
      modelNumber = modelNo;
      phoneColor  = colorOfPhone;
      inputType   = typeOfInput;
      screenType  = typeOfScreen;
      screenSize  = sizeOfScreen;
     }

A continuación, guardaremos y compilaremos el archivo de clase. Tras compilar el archivo de clase, veremos un error en la línea 40 de la columna 13. Error: PhoneClass es una función de miembro ya definida con otros parámetros.

Tenga en cuenta que la numeración de nuestro código puede ser diferente dependiendo de las separaciones y estilos que hayamos utilizado en el código. Para averiguar el número de línea correcto, miremos el registro de errores del compilador MetaEditor en la parte inferior de la ventana:

Error de compilación PhoneClass

Ya hemos declarado y definido nuestro nuevo constructor paramétrico en un bloque de código; más abajo en nuestro código, en la línea 40 hay otro segmento de código que también define el constructor. Deberemos comentar las líneas de la 40 a la 42. A continuación, el archivo de clase se compilará correctamente, sin errores ni advertencias. (Tenga en cuenta que en el código de usted, este segmento podría estar en otras líneas).

/*PhoneClass::PhoneClass() //constructor method definition
  {
  }*/


Destructores

Un destructor es un método especial dentro de una clase que se llama y ejecuta de forma automática cuando se finaliza un objeto. Su objetivo principal consiste en recoger la basura y limpiar los recursos que la instalación haya destinado, una vez que esta haya terminado de utilizarlos. Ayuda a evitar fugas de memoria y otros problemas relacionados con los recursos. En una clase solo puede haber un destructor.

Principales características de los destructores:

  • El nombre es el mismo que el de la clase: el destructor tiene el mismo nombre que la clase, pero se añade el signo de virgulilla (~) antes de él. Esta regla de nomenclatura permite a un lenguaje de programación identificar y asociar un destructor a una clase.
  • Recogida de basura: limpia los recursos asignados por un objeto, como memoria, líneas, arrays dinámicos, objetos automáticos o conexiones de red.
  • Ejecución automática: el destructor se llama o ejecuta automáticamente cuando el objeto finaliza su funcionamiento.
  • Carecen de parámetros: los destructores no tienen parámetros y, por defecto, son de tipo void, lo que significa que no retornan valores.

Vamos a añadir algo de código que muestre texto cada vez que se ejecute el destructor. Para ello, iremos al segmento del código donde está definido el método del destructor y añadiremos dicho código:

PhoneClass::~PhoneClass() //destructor method definition
  {
   Print("-------------------------------------------------------------------------------------");
   PrintFormat("(ModelNo: %i) PhoneClass object terminated. The DESTRUCTOR is now cleaning up!", modelNumber);
  }

Como podemos ver, al declarar o definir los métodos del constructor y el destructor de una clase, no establecemos su tipo de retorno (void). No será necesario especificar el tipo del valor de retorno, porque hay una regla simple: todos los constructores y destructores en MQL5 tienen el tipo void, y el compilador lo sabe.

Para facilitar la comprensión del funcionamiento de los constructores y destructores, veremos un breve ejemplo: Imagine una acampada en la que usted y su amigo se asignan mutuamente determinados papeles. El papel de "constructor" lo asumirá su amigo: será responsable de montar la tienda y organizarlo todo a su llegada. Usted, a su vez, se encargará de recoger el equipo y limpiar al final del viaje, es decir, ejercerá de destructor. En la POO, los constructores inicializan los objetos, mientras que los destructores borran los recursos al finalizar el tiempo de vida de un objeto.

A continuación, añadiremos los métodos de la clase (las tareas que realizará el teléfono como se describe en el esquema). Agregaremos estos métodos justo debajo de la declaración del método destructor: ~PhoneClass();.

//class methods
   bool              MakePhoneCall(int phoneNumber);
   void              ReceivePhoneCall();
   bool              SendSms(int phoneNumber, string message);
   void              ReceiveSms();
   bool              SendInternetData();
   void              ReceiveInternetData();
   void              UseCamera();
   void virtual      PrintPhoneSpecs();


¿Qué son los métodos virtuales en MQL5?

En MQL5, los métodos virtuales son funciones especiales dentro de una clase que puede ser redefinida por los métodos homónimos en las clases derivadas. Cuando un método se marca como virtual en la clase básica, permite a las clases derivadas ofrecer una implementación diferente de ese método. Este mecanismo resulta importante para el polimorfismo, donde los objetos de diferentes clases pueden ser tratados como objetos de una clase básica común. Asimismo, ofrece la flexibilidad y extensibilidad de la POO, pues permite definir determinados comportamientos en las subclases, manteniendo una interfaz común en la clase básica.

Más adelante en el artículo, veremos cómo redefinir el método PrintPhoneSpecs(), cuando veamos la herencia orientada a objetos.

Para implementar la definición del método PrintPhoneSpecs(), colocaremos este código bajo la definición del método del destructor en la parte inferior de nuestro archivo de clase.

void PhoneClass::PrintPhoneSpecs() //method definition
  {
   Print("___________________________________________________________");
   PrintFormat("Model: %i Phone Specs", modelNumber);
   Print("---------------------");
   PrintFormat
      (
         "Model Number: %i \nPhoneColor: %s \nInput Type: %s \nScreen Type: %s \nScreen Size: %i\n",
         modelNumber, phoneColor, inputType, screenType, screenSize
      );  
  }

Hay dos formas de definir un método de clase. 

  1. Dentro del cuerpo de la clase: podemos declarar y definir un método en un solo paso dentro del cuerpo de la clase, como hicimos anteriormente con el constructor paramétrico. La sintaxis de declaración y definición de un método dentro del cuerpo de la clase resulta idéntica a la de una función normal.
  2. Fuera del cuerpo de la clase: primero declaremos el método dentro del cuerpo de la clase y luego lo definiremos fuera del cuerpo de la clase, como hicimos con el destructor y PrintPhoneSpecs(). Para definir un método MQL5 fuera del cuerpo de la clase, primero deberemos especificar el tipo del valor de retorno del método, a continuación, el nombre de la clase, el operador de ámbito (::), el nombre del método, y luego la lista de parámetros entre paréntesis. A continuación, el cuerpo del método se encerará entre llaves {}.a Esta separación entre declaración y definición resulta preferible porque permite organizar claramente la estructura de la clase y sus métodos asociados.

¿Qué es un operador de ámbito (::) en la POO?


El operador :: es un operador de resolución de ámbito usado en C++ y MQL5 para especificar el contexto al que pertenece una función o método. Define o hace referencia a funciones o métodos que son miembros de una clase, y permite indicar que son miembros de esa clase en particular.

A modo de ejemplo, veamos la definición del método PrintPhoneSpecs():

void PhoneClass::PrintPhoneSpecs() //method definition
  {
   //method body
  }

Como se ve en la definición del método PrintPhoneSpecs() anterior, el nombre de la clase se coloca antes del operador de ámbito "::". Esto indicará que esta función pertenece a la clase PhoneClass. Así se asociará un método a su clase correspondiente. El operador "":: es necesario para definir y referenciar métodos dentro de una clase. Permite especificar el área o contexto al que pertenecen una función o método.


Nuestra clase también declarará los siguientes métodos, que también necesitan ser definidos:

  1. MakePhoneCall(int phoneNumber);
  2. ReceivePhoneCall();
  3. SendSms(int phoneNumber, string message);
  4. ReceiveSms();
  5. SendInternetData();
  6. ReceiveInternetData();
  7. UseCamera();

Colocaremos el código de la definición del método encima del código de definición de PrintPhoneSpecs(). Este es el aspecto que debería tener el archivo de clase con las definiciones de métodos añadidas:

class PhoneClass //class name
  {
private: //access modifier
   //class attributes
   int               modelNumber;
   string            phoneColor;
   string            inputType;
   string            screenType;
   int               screenSize;

public: //access modifier
   //PhoneClass();
   //constructor declaration and definition
   PhoneClass(int modelNo, string colorOfPhone, string typeOfInput, string typeOfScreen, int sizeOfScreen)
     {      
      modelNumber = modelNo;
      phoneColor  = colorOfPhone;
      inputType   = typeOfInput;
      screenType  = typeOfScreen;
      screenSize  = sizeOfScreen;
     }
     
   ~PhoneClass(); //destructor method declaration
   
   //class methods
   bool              MakePhoneCall(int phoneNumber);
   void              ReceivePhoneCall();
   bool              SendSms(int phoneNumber, string message);
   void              ReceiveSms();
   bool              SendInternetData();
   void              ReceiveInternetData();
   void              UseCamera();
   void virtual      PrintPhoneSpecs();
  };

/*PhoneClass::PhoneClass() //constructor method definition
  {
  }*/

PhoneClass::~PhoneClass() //destructor method definition
  {
   Print("-------------------------------------------------------------------------------------");
   PrintFormat("(ModelNo: %i) PhoneClass object terminated. The DESTRUCTOR is now cleaning up!", modelNumber);
  }

bool PhoneClass::MakePhoneCall(int phoneNumber) //method definition
  {
      bool callMade = true;
      Print("Making phone call...");
      return(callMade);
  }

void PhoneClass::ReceivePhoneCall(void) { //method definition
      Print("Receiving phone call...");
   }

bool PhoneClass::SendSms(int phoneNumber, string message) //method definition
  {
      bool smsSent = true;
      Print("Sending SMS...");
      return(smsSent);
  }

void PhoneClass::ReceiveSms(void) { //method definition
      Print("Receiving SMS...");
   }

bool PhoneClass::SendInternetData(void) //method definition
  {
      bool dataSent = true;
      Print("Sending internet data...");
      return(dataSent);
  }
  
void PhoneClass::ReceiveInternetData(void) { //method definition
      Print("Receiving internet data...");
   }

void PhoneClass::UseCamera(void) { //method definition
      Print("Using camera...");
   }

void PhoneClass::PrintPhoneSpecs() //method definition
  {
   Print("___________________________________________________________");
   PrintFormat("Model: %i Phone Specs", modelNumber);
   Print("---------------------");
   PrintFormat
      (
         "Model Number: %i \nPhoneColor: %s \nInput Type: %s \nScreen Type: %s \nScreen Size: %i\n",
         modelNumber, phoneColor, inputType, screenType, screenSize
      );  
  }

Encontrará el código completo de PhoneClass.mqh en los anexos de este artículo.

Ahora podemos crear el objeto PhoneClass. Si regresamos al ejemplo de la fabricación de teléfonos, puede compararse a cómo los ingenieros convierten el diseño de un teléfono en un producto físico que puede realizar diversas tareas (como hacer y recibir llamadas).

Los archivos de clase se guardarán con la extensión .mqh y se denominarán archivos de inclusión. Vamos a crear un nuevo EA en la misma carpeta donde está almacenado nuestro archivo PhoneClass.mqh. Guardaremos agqk PhoneClass.mqh en una carpeta: Experts\OOP_Article\\. Utilice el Wizard MQL en el MetaEditor para crear el nuevo Asesor Experto (plantilla) y guárdelo en la carpeta Experts\OOP_Article\. Llamaremos al nuevo Asesor Experto PhoneObject.mq5.

Luego añadiremos el siguiente código bajo los fragmentos #property version   "1.00" en el código del EA:

// Include the PhoneClass file so that the PhoneClass code is available in this EA
#include "PhoneClass.mqh"

int OnInit()
  {
//---
   // Create instaces or objects of the PhoneClass with specific parameters
   // as specified in the 'PhoneClass' consturctor
   PhoneClass myPhoneObject1(101, "Black", "Keyboard", "Non-touch LCD", 4);
   PhoneClass myPhoneObject2(102, "SkyBlue", "Touchscreen", "Touch AMOLED", 6);

   // Invoke or call the PrintPhoneSpecs method to print the specifications
   myPhoneObject1.PrintPhoneSpecs();
   myPhoneObject2.PrintPhoneSpecs();
//---
   return(INIT_SUCCEEDED);
  }
void OnDeinit(const int reason){}
void OnTick(){}

Encontrará el código completo del archivo PhoneObject.mq5 en los anexos de este artículo.

Vamos ahora a ver lo que está pasando en el código del Asesor Experto PhoneObject.mq5:

Añadiremos el archivo de clase utilizando el operador Include:

En primer lugar, añadiremos el código PhoneClass al Asesor Experto utilizando la directiva #include "PhoneClass.mqh" para que esté disponible para su uso en nuestro Asesor Experto recién creado (PhoneObject.mq5). Hemos incluido esta clase en el ámbito global, por lo que estará disponible en todas las secciones del EA.


Luego crearemos el objeto PhoneClass:

Dentro de la función OnInit() del EA, hemos creado dos ejemplares u objetos PhoneClass. Para crear estos objetos, empezaremos con un nombre de clase, seguido de los nombres descriptivos de los ejemplares del teléfono (myPhoneObject1 y myPhoneObject2). A continuación, utilizaremos paréntesis para especificar los valores de las especificaciones del teléfono, como el número de modelo, el color, el tipo de entrada, el tipo de pantalla y el tamaño, como se especifica en los parámetros del constructor PhoneClass.


Llamada a un método de clase:

Las líneas myPhoneObject1.PrintPhoneSpecs() y myPhoneObject2.PrintPhoneSpecs() llaman al método PrintPhoneSpecs() del objeto PhoneClass, para mostrar en la pantalla las características del teléfono.


Muestra las especificaciones del teléfono en la pantalla:

Luego ejecutaremos en el gráfico de símbolos en el terminal comercial MetaTrader 5; para ejecutar PhoneObjectEA, iremos a la ventana Herramientas y seleccionaremos la pestaña Expertos, para comprobar las características mostradas del teléfono.

La salida también mostrará un mensaje de texto del destructor de PhoneClass (~PhoneClass()). Como podemos ver, cada objeto teléfono crea un destructor único e independiente y lo llama cuando el objeto se finaliza.

Registro de expertos PhoneObjectEA


¿Qué es la herencia en la POO?


La herencia es el concepto de que una nueva clase, llamada subclase o clase hija, puede heredar atributos y comportamientos (propiedades y métodos) de una clase existente conocida como clase padre o básica. Esto permite a la clase hija reutilizar y ampliar la funcionalidad de la clase padre.

En pocas palabras, la herencia es como un árbol genealógico. La clase básica puede considerarse como una clase padre. Dicha clase tiene ciertas características (propiedades y métodos). Entonces la subclase será el hijo. Una subclase heredará automáticamente todos los rasgos (propiedades y métodos) de la clase básica, del mismo modo que un hijo hereda rasgos de su padre.

Por ejemplo, si la madre tiene los ojos marrones, la hija también los tendrá, aunque no se diga de forma explícita. En programación, una subclase heredará los métodos y atributos de la clase básica, creando una jerarquía para organizar y reutilizar el código.

Esta estructura "familiar" ayudará a organizar y reutilizar el código. La clase hija (subclase) obtendrá todo lo que tiene la clase padre (clase básica) e incluso podrá añadir sus propias características exclusivas. Los programadores usan distintos términos para referirse a los "miembros de la familia":

  • Clase básica: clase padre, superclase, clase raíz, clase básica, clase maestra.
  • Subclase: clase hija, clase derivada, clase descendiente, clase sucesora.

A continuación le mostramos cómo se puede implementar la herencia en el contexto del código PhoneClass proporcionado:

  • PhoneClass supone la clase básica (proyecto) que definirá los bloques básicos de la funcionalidad del teléfono.
  • Vamos a crear otra clase para implementar el modelo de teléfono más avanzado (smartphone) que hemos mencionado anteriormente en el ejemplo de fabricación de teléfonos.
  • Llamaremos a la clase SmartPhoneClass. Esta heredará todas las propiedades y métodos de la clase PhoneClass y añadirá nuevas características específicas de los smartphones. También redefiniremos el método PrintPhoneSpecs() existente en la clase PhoneClass para implementar el comportamiento del smartphone.
Luego crearemos un nuevo archivo vacío de la clase SmartPhoneClass.mqh de la misma manera que creamos la clase PhoneClass en el Wizard MQL en el MetaEditor. Asimismo, añadiremos el siguiente código al cuerpo de SmartPhoneClass.mqh:

#include "PhoneClass.mqh" // Include the PhoneClass file
class SmartPhoneClass : public PhoneClass
{
private:
    string operatingSystem;
    int numberOfCameras;

public:
    SmartPhoneClass(int modelNo, string colorOfPhone, string typeOfInput, string typeOfScreen, int sizeOfScreen, string os, int totalCameras)
        : PhoneClass(modelNo, colorOfPhone, typeOfInput, typeOfScreen, sizeOfScreen)
    {
        operatingSystem = os;
        numberOfCameras = totalCameras;
    }

    void UseFacialRecognition()
    {
        Print("Using facial recognition feature...");
    }

    // Override methods from the base class if needed
    void PrintPhoneSpecs() override
    {
        Print("-----------------------------------------------------------");
        Print("Smartphone Specifications (including base phone specs):");
        Print("-----------------------------------------------------------");
        PrintFormat("Operating System: %s \nNumber of Cameras: %i", operatingSystem, numberOfCameras);
        PhoneClass::PrintPhoneSpecs(); // Call the base class method
        Print("-----------------------------------------------------------");
    }
};

El código completo estará disponible en el archivo SmartPhoneClass.mqh

En este ejemplo, la clase SmartPhoneClass heredará de la clase PhoneClass. Para ello, agregaremos las nuevas propiedades (operatingSystem y numberOfCameras), así como un nuevo método (UseFacialRecognition). El constructor de la clase SmartPhoneClass también llamará al constructor de la clase básica (PhoneClass) con ': PhoneClass(...)'. También podremos observar que hemos sobrescrito el método PrintPhoneSpecs() de la clase básica. Hemos incluido el especificador override en la definición del método PrintPhoneSpecs() de la clase SmartPhoneClass para que el compilador sepa que hemos sobreescrito de forma intencional el método de la clase básica.

De este modo, podremos crear ejemplares de SmartPhoneClass que incluyan todas las características de un teléfono normal (PhoneClass) y las nuevas características adicionales de los smartphones.


Modificadores de acceso

Algunos miembros importantes de la herencia en la POO son los modificadores de acceso. Estos definen cómo se heredan los miembros (atributos y métodos) de la clase básica y cómo se accede a ellos en las clases derivadas.

  • Public: el acceso público permite que las propiedades y métodos de una clase sean accesibles desde ubicaciones externas a la clase. Así, podemos utilizar o modificar cualquier miembro público desde cualquier parte del programa. Este es el nivel de acceso más abierto.
  • Private: el acceso privado restringe la visibilidad de las propiedades y métodos para acceder a ellos o modificarlos al ámbito de la propia clase. Los miembros declarados con el modificador private no serán directamente accesibles desde fuera de la clase. Así se ocultarán los detalles de la aplicación y se garantizará la integridad de los datos. Esto se llama encapsulación.
  • Protected: acceso seguro, el justo medio entre lo público y lo privado. Los miembros declarados como protegidos estarán disponibles dentro de una clase y en sus subclases (clases derivadas). Esto ofrecerá cierto nivel de uso controlado por las clases relacionadas, al tiempo que limitará el acceso desde el exterior.

Para crear un objeto SmartPhoneClass, crearemos un nuevo EA y lo guardaremos como SmartPhoneObject.mq5. Añadiremos el código que sigue:

// Include the PhoneClass file so that it's code is available in this EA
#include "SmartPhoneClass.mqh"

int OnInit()
  {
//---
   // Create instaces or objects of the PhoneClass with specific parameters
   // as specified in the 'PhoneClass' consturctor (base/mother class)
   PhoneClass myPhoneObject1(103, "Grey", "Touchscreen", "Touch LCD", 8);
   
   // as specified in the 'SmartPhoneClass' consturctor
   SmartPhoneClass mySmartPhoneObject1(104, "White", "Touchscreen", "Touch AMOLED", 6, "Android", 3);

   // Invoke or call the PrintPhoneSpecs method to print the specifications
   myPhoneObject1.PrintPhoneSpecs(); // base class method
   mySmartPhoneObject1.PrintPhoneSpecs(); // overriden method by the derived class (SmartPhoneClass)
//---
   return(INIT_SUCCEEDED);
  }
void OnDeinit(const int reason){}
void OnTick(){}

El código completo del archivo SmartPhoneObject.mq5 estará disponible en los anexos de este artículo.

Luego guardaremos y compilaremos el código fuente de SmartPhoneObject.mq5. Después de eso, podríamos ejecutar en el gráfico de un instrumento comercial en MetaTrader 5 terminal y ver la ejecución del objeto SmartPhoneClass. Para ello, iremos a la ventana Herramientas y seleccionaremos la pestaña Expertos para comprobar el registro de datos del Asesor Experto.


Propiedades básicas de la POO

Las seis propiedades básicas de la POO son:

  1. Encapsulación: combinación de datos y métodos en un único bloque (clase) para ocultar los detalles internos.
  2. Abstracción: simplificación de sistemas complejos centrando la atención en propiedades y comportamientos básicos.
  3. Herencia: permite a una clase heredar propiedades y comportamientos de otra clase para reutilizar el código.
  4. Polimorfismo: permite analizar objetos de clases diferentes como objetos de una clase básica común para proporcionar flexibilidad.
  5. Clases y objetos: las clases de proyecto y los objetos de ejemplar organizan el código de forma modular.
  6. Transmisión de mensajes: los objetos se comunican con mensajes e interactúan.

EIP (encapsulación, herencia, polimorfismo): son los principios básicos de la POO que ofrecen organización y flexibilidad al código. Aplicando estas reglas básicas, podremos escribir un código más organizado y más fácil de mantener y reutilizar.


Convencionalidad de los nombres de clases en MQL5

En MQL5, el prefijo C se añade a los nombres de las clases. Sin embargo, esto no es necesario. El prefijo C indica clase y es una práctica habitual. Muestra que un identificador concreto representa una clase.

Ejemplos de nombres de clase en el código MQL5: CExpert, CIndicator, CStrategy y otros. En general, las convenciones de nomenclatura ayudan a distinguir, en este caso, las clases de otros tipos de identificadores, como funciones o variables.

Aunque el uso del prefijo C resulta común y por lo general se recomienda para mayor claridad, MQL5 no impone ninguna regla estricta con respecto a la nomenclatura de clases. Técnicamente podemos nombrar nuestras clases sin prefijo, pero aún así resulta recomendable seguir las convenciones establecidas para mejorar la legibilidad y mantenibilidad de nuestro código.


Enfoque orientado a objetos para el desarrollo de EA basados en la dinámica de precios

Ahora que está familiarizado con el paradigma de la POO, es hora de ver un ejemplo práctico y convertir el EA basado en la acción del precio que escribimos en el artículo anterior de código procedimental a código orientado a objetos. El código procedimental de este EA también se adjunta al artículo: Procedural_PriceActionEMA.mq5.

Breve resumen de las características de la estrategia comercial Price Action. En el primer artículo le ofrecimos una explicación más detallada de esta estrategia comercial.


Estrategia de Price Action EMA


La estrategia es muy sencilla. Para tomar las decisiones comerciales, solo se utilizarán las medias móviles exponenciales (EMA) y los precios de las velas. Usaremos el simulador de estrategias para encontrar la mejor media EMA y los mejores ajustes de marco temporal. Personalmente, prefiero comerciar marcos temporales de horas o superiores.

Reglas de entrada:

  • COMPRA: abrimos una posición de Compra cuando la última vela cerrada es alcista (apertura por debajo del cierre) y sus precios mínimo y máximo están por encima de la media móvil exponencial (EMA).
  • VENTA: abrimos una posición de Venta cuando la última vela cerrada es bajista (apertura por encima del cierre) y sus precios mínimo y máximo están por debajo de la media móvil exponencial (EMA).
  • Las posiciones se abrirán en cada nueva barra si se cumplen las condiciones mencionadas anteriormente.

Reglas de salida:

  • Cerraremos automáticamente todas las posiciones abiertas cuando se alcance el porcentaje de beneficios o pérdidas especificado (por el usuario) para la cuenta.
  • O utilizaremos órdenes tradicionales de stop loss o take profit.

Estrategia de acción del precio EMA


El propósito de este artículo es mostrar cómo convertir la estrategia anterior en un EA mql5 utilizando los principios de la POO. Así que podemos comenzar a escribir el código.

Primero crearemos una nueva clase CEmaExpertAdvisor


Usando el Wizard MQL5, crearemos un archivo de clase con el nombre EmaExpertAdvisor y lo guardaremos en la carpeta Experts\OOP_Article\PriceActionEMA\. Dentro del archivo EmaExpertAdvisor.mqh nuevamente creado, crearemos una nueva clase con el nombre CEmaExpertAdvisor. Utilizaremos la clase CEmaExpertAdvisor para encapsular el comportamiento del EA y las variables para representar su estado.

Añadiremos el siguiente código de propiedades/miembros de clase y los códigos de método al archivo CEmaExpertAdvisor:

//+------------------------------------------------------------------+
// Include the trade class from the standard library
//--- 
#include <Trade\Trade.mqh>

class CEmaExpertAdvisor
  {

public:
   CTrade            myTrade;
   
public:
   // Constructor
                     CEmaExpertAdvisor(
      long _magicNumber, ENUM_TIMEFRAMES _tradingTimeframe,
      int _emaPeriod, int _emaShift, bool _enableTrading,
      bool _enableAlerts, double _accountPercentageProfitTarget,
      double _accountPercentageLossTarget, int _maxPositions, int _tp, int _sl
   );

   // Destructor
                    ~CEmaExpertAdvisor();
  };

Antes de la palabra clave de la clase, hemos añadido una clase comercial de la biblioteca estándar MQL5. Esto permitirá gestionar diversas operaciones comerciales con menos código y mayor eficacia. Esto significa que deberemos reescribir los métodos ManageProfitAndLoss() y BuySellPosition(...) según las nuevas clases.

#include <Trade\Trade.mqh>

A continuación podrá ver que hemos creado un ejemplar de la clase CTrade, así como un objeto listo para usar llamado myTrade, que utilizaremos para abrir y cerrar nuevas posiciones.

//Create an instance/object of the included CTrade class
   CTrade myTrade;

Todas las variables globales introducidas por el usuario desde el código procedimental se convertirán en variables globales privadas de la clase CEmaExpertAdvisor. Las variables de entrada de EA deberán inicializarse justo después de crear el ejemplar de la clase. Para ello, las transmitiremos como parámetros al constructor. Esto encapsulará el proceso de inicialización dentro de la clase.

private:
   // Private member variables/attributes (formerly procedural global variables)
   //------------------------
   // User input varibles
   long              magicNumber;
   ENUM_TIMEFRAMES   tradingTimeframe;
   int               emaPeriod;
   int               emaShift;
   bool              enableTrading;
   bool              enableAlerts;
   double            accountPercentageProfitTarget;
   double            accountPercentageLossTarget;
   int               maxPositions;
   int               TP;
   int               SL;

El resto de variables globales del código procedimental se declararán públicas y se definirán como variables globales en la clase.

public:
   //--- EA global variables
   // Moving average variables
   double            movingAverage[];
   int               emaHandle;
   bool              buyOk, sellOk;
   string            movingAverageTrend;

   // Strings for the chart comments
   string            commentString, accountCurrency, tradingStatus, accountStatus;

   // Capital management variables
   double            startingCapital, accountPercentageProfit;

   // Orders and positions variables
   int               totalOpenBuyPositions, totalOpenSellPositions;
   double            buyPositionsProfit, sellPositionsProfit, buyPositionsVol, sellPositionsVol;

   datetime          closedCandleTime;//used to detect new candle formations

Debajo de la declaración del método destructor, justo encima de la sintaxis de cierre de la llave de clase, añadiremos todas las funciones de código procedimental como declaraciones de métodos de clase.

// Class method declarations (formerly procedural standalone functions)
   int               GetInit();
   void              GetDeinit();
   void              GetEma();
   void              GetPositionsData();   
   bool              TradingIsAllowed();
   void              TradeNow();
   void              ManageProfitAndLoss();
   void              PrintOnChart();
   bool              BuySellPosition(int positionType, string positionComment);
   bool              PositionFound(string symbol, int positionType, string positionComment);

Utilizaremos el estilo de código de C++ y definiremos todos los métodos de la clase bajo el cuerpo de la clase:

//+------------------------------------------------------------------+
//|   METHODS DEFINITIONS                                                                |
//+------------------------------------------------------------------+
CEmaExpertAdvisor::CEmaExpertAdvisor(long _magicNumber, ENUM_TIMEFRAMES _tradingTimeframe,
                                   int _emaPeriod, int _emaShift, bool _enableTrading,
                                   bool _enableAlerts, double _accountPercentageProfitTarget,
                                   double _accountPercentageLossTarget,
                                   int _maxPositions, int _tp, int _sl)
  {
   magicNumber = _magicNumber;
   tradingTimeframe = _tradingTimeframe;
   emaPeriod = _emaPeriod;
   emaShift = _emaShift;
   enableTrading = _enableTrading;
   enableAlerts = _enableAlerts;
   accountPercentageProfitTarget = _accountPercentageProfitTarget;
   accountPercentageLossTarget = _accountPercentageLossTarget;
   maxPositions = _maxPositions;
   TP = _tp;
   SL = _sl;
  }
//+------------------------------------------------------------------+
CEmaExpertAdvisor::~CEmaExpertAdvisor() {}
//+------------------------------------------------------------------+
int CEmaExpertAdvisor::GetInit()
  {
    //method body....
  }
//+------------------------------------------------------------------+
void CEmaExpertAdvisor::GetDeinit()
  {
    //method body....
  }
//+------------------------------------------------------------------+
void CEmaExpertAdvisor::GetEma()
  {
   //method body....
  }
//+------------------------------------------------------------------+
void CEmaExpertAdvisor::GetPositionsData()
  {
   //method body....
  }
//+------------------------------------------------------------------+
bool CEmaExpertAdvisor::TradingIsAllowed()
  {
   //method body....
  }
//+------------------------------------------------------------------+
void CEmaExpertAdvisor::TradeNow()
  {
   //method body....
  }
//+------------------------------------------------------------------+
void CEmaExpertAdvisor::ManageProfitAndLoss()
  {
   //method body....
  }
//+------------------------------------------------------------------+
void CEmaExpertAdvisor::PrintOnChart()
  {
   //method body....
  } 
//+------------------------------------------------------------------+
bool CEmaExpertAdvisor::BuySellPosition(int positionType, string positionComment)
  {
   //method body....
  }
//+------------------------------------------------------------------+
bool CEmaExpertAdvisor::PositionFound(string symbol, int positionType, string positionComment)
  {
   //method body....
  }

Todas las definiciones de los métodos serán idénticas a la sintaxis del código procedimental salvo los métodos ManageProfitAndLoss() y BuySellPosition(...), que hemos actualizado para utilizar el objeto myTrade recién creado de la clase CTrade, previamente importada al código de la clase.

Ahora le mostramos el nuevo método ManageProfitAndLoss():

void CEmaExpertAdvisor::ManageProfitAndLoss()
  {
//if the account percentage profit or loss target is hit, delete all positions
   double lossLevel = -accountPercentageLossTarget;
   if(
      (accountPercentageProfit >= accountPercentageProfitTarget || accountPercentageProfit <= lossLevel) ||
      ((totalOpenBuyPositions >= maxPositions || totalOpenSellPositions >= maxPositions) && accountPercentageProfit > 0)
   )
     {
      //delete all open positions
      if(PositionsTotal() > 0)
        {
         //variables for storing position properties values
         ulong positionTicket;
         long positionMagic, positionType;
         string positionSymbol;
         int totalPositions = PositionsTotal();

         //scan all the open positions
         for(int x = totalPositions - 1; x >= 0; x--)
           {
            positionTicket = PositionGetTicket(x);//gain access to other position properties by selecting the ticket
            positionMagic = PositionGetInteger(POSITION_MAGIC);
            positionSymbol = PositionGetString(POSITION_SYMBOL);
            positionType = PositionGetInteger(POSITION_TYPE);
            int positionDigits= (int)SymbolInfoInteger(positionSymbol, SYMBOL_DIGITS);
            double positionVolume = PositionGetDouble(POSITION_VOLUME);
            ENUM_POSITION_TYPE positionType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);

            if(positionMagic == magicNumber && positionSymbol == _Symbol) //close the position
              {
               //print the position details
               Print("*********************************************************************");
               PrintFormat(
                  "#%I64u %s  %s  %.2f  %s [%I64d]",
                  positionTicket, positionSymbol, EnumToString(positionType), positionVolume,
                  DoubleToString(PositionGetDouble(POSITION_PRICE_OPEN), positionDigits), positionMagic
               );
               
               //print the position close details
               PrintFormat("Close #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType));
               //send the tradeRequest
               if(myTrade.PositionClose(positionTicket, SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * 3)) //success, position closed
                 {
                  if(enableAlerts)
                    {
                     Alert(
                        _Symbol + " PROFIT LIQUIDATION: Just successfully closed POSITION (#" +
                        IntegerToString(positionTicket) + "). Check the EA journal for more details."
                     );
                    }
                  PrintFormat("Just successfully closed position: #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType));
                  myTrade.PrintResult();
                 }
               else  //trade tradeRequest failed
                 {
                  //print the information about the operation
                  if(enableAlerts)
                    {
                     Alert(
                        _Symbol + " ERROR ** PROFIT LIQUIDATION: closing POSITION (#" +
                        IntegerToString(positionTicket) + "). Check the EA journal for more details."
                     );
                    }
                  PrintFormat("Position clossing failed: #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType));
                  PrintFormat("OrderSend error %d", GetLastError());//print the error code
                 }
              }
           }
        }
     }
  }


A continuación vendrá el nuevo método BuySellPosition(...):

bool CEmaExpertAdvisor::BuySellPosition(int positionType, string positionComment)
  {
   double volumeLot = NormalizeDouble(((SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN) * AccountInfoDouble(ACCOUNT_EQUITY)) / 10000), 2);
   double tpPrice = 0.0, slPrice = 0.0, symbolPrice;
   
   if(positionType == POSITION_TYPE_BUY)
     {
      if(sellPositionsVol > volumeLot && AccountInfoDouble(ACCOUNT_MARGIN_LEVEL) > 200)
        {
         volumeLot = NormalizeDouble((sellPositionsVol + volumeLot), 2);
        }
      if(volumeLot < 0.01)
        {
         volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
        }
      if(volumeLot > SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX))
        {
         volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
        }
      
      volumeLot = NormalizeDouble(volumeLot, 2);
      symbolPrice = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      
      if(TP > 0)
        {
         tpPrice = NormalizeDouble(symbolPrice + (TP * _Point), _Digits);
        }
      if(SL > 0)
        {
         slPrice = NormalizeDouble(symbolPrice - (SL * _Point), _Digits);
        }
      //if(myTrade.Buy(volumeLot, NULL, 0.0, 0.0, 0.0, positionComment)) //successfully openend position
      if(myTrade.Buy(volumeLot, NULL, 0.0, slPrice, tpPrice, positionComment)) //successfully openend position
        {
         if(enableAlerts)
           {
            Alert(_Symbol, " Successfully openend BUY POSITION!");
           }
         myTrade.PrintResult();
         return(true);
        }
      else
        {
         if(enableAlerts)
           {
            Alert(_Symbol, " ERROR opening a BUY POSITION at: ", SymbolInfoDouble(_Symbol, SYMBOL_ASK));
           }
         PrintFormat("ERROR: Opening a BUY POSITION: ErrorCode = %d",GetLastError());//OrderSend failed, output the error code
         return(false);
        }
     }

   if(positionType == POSITION_TYPE_SELL)
     {
      if(buyPositionsVol > volumeLot && AccountInfoDouble(ACCOUNT_MARGIN_LEVEL) > 200)
        {
         volumeLot = NormalizeDouble((buyPositionsVol + volumeLot), 2);
        }
      if(volumeLot < 0.01)
        {
         volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
        }
      if(volumeLot > SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX))
        {
         volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
        }
      volumeLot = NormalizeDouble(volumeLot, 2);
      symbolPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      if(TP > 0)
        {
         tpPrice = NormalizeDouble(symbolPrice - (TP * _Point), _Digits);
        }
      if(SL > 0)
        {
         slPrice = NormalizeDouble(symbolPrice + (SL * _Point), _Digits);
        }
      if(myTrade.Sell(volumeLot, NULL, 0.0, slPrice, tpPrice, positionComment)) //successfully openend position
        {
         if(enableAlerts)
           {
            Alert(_Symbol, " Successfully openend SELL POSITION!");
           }
           myTrade.PrintResult();
         return(true);
        }
      else
        {
         if(enableAlerts)
           {
            Alert(_Symbol, " ERROR opening a SELL POSITION at: ", SymbolInfoDouble(_Symbol, SYMBOL_ASK));
           }
         PrintFormat("ERROR: Opening a SELL POSITION: ErrorCode = %d",GetLastError());//OrderSend failed, output the error code
         return(false);
        }
     }
   return(false);
  }

No nos olvidaremos de implementar todos los métodos/funciones con la lógica del código procedimental original. El código completo de la clase CEmaExpertAdvisor estará disponible en el archivo include EmaExpertAdvisor.mqh adjunto a este artículo.


Ahora crearemos el nuevo Asesor Experto OOP_PriceActionEMA


Hemos preparado una maqueta de la estrategia comercial (clase CEmaExpertAdvisor). Ahora es el momento de ponerlo todo en práctica. Daremos vida a esta maqueta y crearemos un objeto real que pueda hacer transacciones.

Crearemos un nuevo asesor experto con el nombre "OOP_PriceActionEMA.mq5" y lo guardaremos en la carpeta Experts\OOP_Article\PriceActionEMA. El asesor se encargará de aplicar nuestra estrategia comercial.

Empezaremos importando el archivo include EmaExpertAdvisor.mqh, que contiene la clase CEmaExpertAdvisor.

// Include the CEmaExpertAdvisor file so that it's code is available in this EA
#include "EmaExpertAdvisor.mqh"

A continuación, declararemos y definiremos las variables de entrada del usuario como variables globales. Estas son las variables para configurar el EA, y serán similares a los parámetros que antes eran variables globales en la versión procedimental.

//--User input variables
input long magicNumber = 101;//Magic Number (Set 0 [Zero] to disable

input group ""
input ENUM_TIMEFRAMES tradingTimeframe = PERIOD_H1;//Trading Timeframe
input int emaPeriod = 15;//Moving Average Period
input int emaShift = 0;//Moving Average Shift

input group ""
input bool enableTrading = true;//Enable Trading
input bool enableAlerts = false;//Enable Alerts

input group ""
input double accountPercentageProfitTarget = 6.0;//Account Percentage (%) Profit Target
input double accountPercentageLossTarget = 10.0;//Account Percentage (%) Loss Target

input group ""
input int maxPositions = 3;//Max Positions (Max open positions in one direction)
input int TP = 5000;//TP (Take Profit Points/Pips [Zero (0) to diasable])
input int SL = 500;//SL (Stop Loss Points/Pips [Zero (0) to diasable])

Después de eso, crearemos un ejemplar de la clase CEmaExpertAdvisor. Aquí se creará un ejemplar (ea) de la clase CEmaExpertAdvisor utilizando el constructor. Inicializaremos con las variables de entrada del usuario.

//Create an instance/object of the included CEmaExpertAdvisor class
//with the user inputed data as the specified constructor parameters
CEmaExpertAdvisor ea(
      magicNumber, tradingTimeframe, emaPeriod, emaShift,
      enableTrading, enableAlerts, accountPercentageProfitTarget,
      accountPercentageLossTarget, maxPositions, TP, SL
   );

En la función OnInit, llamaremos al método GetInit del ejemplar del asesor ea. Este método forma parte de la clase CEmaExpertAdvisor, y se encargará de inicializar el asesor. Si la inicialización falla, se retornará INIT_FAILED. En caso contrario, se retornará INIT_SUCCEED.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   if(ea.GetInit() <= 0)
     {
      return(INIT_FAILED);
     }
//---
   return(INIT_SUCCEEDED);
  }

En la función OnDeinit, llamaremos al método GetDeinit del ejemplar ea. El método forma parte de la clase CEmaExpertAdvisor, y será responsable de desinicializar el EA.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   ea.GetDeinit();
  }

En la función OnTick, llamaremos a diferentes métodos del ejemplar ea, como GetEma, GetPositionsData, TradingIsAllowed, TradeNow, ManageProfitAndLoss y PrintOnChart. Estos métodos encapsulan diferentes aspectos del comportamiento del EA, haciendo que el código sea más modular y organizado.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   ea.GetEma();
   ea.GetPositionsData();
   if(ea.TradingIsAllowed())
     {
      ea.TradeNow();
      ea.ManageProfitAndLoss();
     }
   ea.PrintOnChart();
  }

Hemos adjuntado el código fuente completo del EA más abajo, en el archivo OOP_PriceActionEMA.mq5.


Probamos nuestro asesor en el simulador de estrategias

Resulta vital que comprobemos si el asesor va por buen camino. Para ello, podemos cargarlo en el gráfico de símbolos y negociar con él en una cuenta demo o en el simulador de estrategias, que nos permitirá evaluarlo más a fondo. Aunque podemos probarlo en una cuenta demo, aquí utilizaremos el simulador de estrategias para evaluar su eficacia.

Estos son los ajustes que aplicaremos en el simulador de estrategias:

  • Bróker: MetaQuotes-Demo (se crea automáticamente al instalar MetaTrader 5)

  • Símbolo: EURJPY

  • Periodo de prueba (intervalo): 1 año y 2 meses (enero 2023 - marzo 2024)

  • Modelado: Todas los ticks basados en ticks reales

  • Depósito:$10 000

  • Apalancamiento: 1:100

Configuración de prueba para OOP_PriceActionEMA

Parámetros de entrada para la prueba de OOP_PriceActionEMA


Gracias a la configuración optimizada del EA, nuestra sencilla estrategia de acción de precios muestra una rentabilidad anual del 41% al negociar con un capital inicial de 10 000 $ en EURJPY, utilizando un apalancamiento de 1:100 y manteniendo una baja reducción del capital de solo el 5%. Esta estrategia es prometedora y puede mejorarse con otros indicadores para obtener mejores resultados, especialmente cuando se ejecuta en varios símbolos al mismo tiempo.


Gráfico de prueba de OOP_PriceActionEMA

Resultados de la prueba de OOP_PriceActionEMA

Resultados de la prueba de OOP_PriceActionEMA



Conclusión

Hemos llegado al final de nuestro artículo sobre los paradigmas de programación orientada a objetos. Hoy hemos explorado las complejidades que transforman el código en estructuras modulares para su reutilización. La transición de la programación procedimental a la POO aporta un nuevo nivel de organización, encapsulación y abstracción, ofreciendo a los desarrolladores una base sólida para gestionar proyectos complejos.

En este artículo, también hemos visto cómo convertir código procedimental MQL5 en código orientado a objetos utilizando los principios del enfoque orientado a objetos. Asimismo, hemos hablado sobre clases, objetos y herencia. Al encapsular los datos y la funcionalidad dentro de las clases, hemos aumentado la modularidad y la facilidad de mantenimiento del código.

Cuando empiece a implementar sus propios proyectos en MQL5, recuerde que el poder de la POO reside en su capacidad para modelar objetos y relaciones del mundo real, creando un código que refleja la complejidad de los sistemas que representa. Al final del artículo, adjuntamos todos los archivos fuente de las distintas clases y EAs que hemos creado.

Gracias por seguir mi trabajo. Espero que los paradigmas y prácticas de programación que hemos analizado hoy enriquezcan su experiencia. ¡Siga los artículos y continúe aprendiendo a crear sistemas comerciales fiables y eficientes en MQL5!

¡Gracias por su atención! Le deseo éxito en el desarrollo de MQL5 y el trading.



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

Redes neuronales: así de sencillo (Parte 82): Modelos de ecuaciones diferenciales ordinarias (NeuralODE) Redes neuronales: así de sencillo (Parte 82): Modelos de ecuaciones diferenciales ordinarias (NeuralODE)
En este artículo, hablaremos de otro tipo de modelos que están destinados a estudiar la dinámica del estado ambiental.
El papel de la calidad del generador de números aleatorios en la eficiencia de los algoritmos de optimización El papel de la calidad del generador de números aleatorios en la eficiencia de los algoritmos de optimización
En este artículo, analizaremos el generador de números aleatorios Mersenne Twister y lo compararemos con el estándar en MQL5. También determinaremos la influencia de la calidad del generador de números aleatorios en los resultados de los algoritmos de optimización.
Particularidades del trabajo con números del tipo double en MQL4 Particularidades del trabajo con números del tipo double en MQL4
En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.
El problema del desacuerdo: profundizando en la explicabilidad de la complejidad en la IA El problema del desacuerdo: profundizando en la explicabilidad de la complejidad en la IA
En este artículo hablaremos de los problemas relacionados con los explicadores y la explicabilidad en la IA. Los modelos de IA suelen tomar decisiones difíciles de explicar. Además, el uso de múltiples explicadores suele provocar el llamado "problema del desacuerdo". Al fin y al cabo, la comprensión clara del funcionamiento de los modelos resulta fundamental para aumentar la confianza en la IA.