Recetas MQL5 - procesamiento de eventos personalizados del gráfico

Denis Kirichenko | 2 diciembre, 2014

Introducción

Este artículo es la continuación lógica del artículo Recetas de MQL5 - procesamiento de eventos típicos del gráfico. En el material actual propongo al lector examinar la metodología de trabajo con los eventos personalizados del gráfico. Se presentarán ejemplos de creación y procesamiento de los eventos personalizados. Y con esto, además, se usará un instrumental orientado a objetos.

Querría destacar que el tema de los eventos personalizados es muy amplio. Se trata de un caso en el que el trabajo del programador y el desarrollador se conjuga con la artisticidad y la creatividad.


1. Eventos personalizados del gráfico

Como reza el propio nombre, es el usuario el que introduce ese tipo de eventos. El programador mismo decide, qué o cuál tarea o bloque de programa dotar con forma de evento. El procesador MQL5 permite crear sus propios eventos, lo que aumenta la flexibilidad del propio lenguaje al realizar algoritmos complejos.

El evento personalizado es el segundo tipo posible de evento de gráfico. El primero sería el evento típico. Y aunque en la Documentación no existe un término como "evento típico de gráfico", propongo de todas formas utilizarlo para referirnos a los primeros 10 tipos de eventos del gráfico.

El desarrollador propone para procesar todos los eventos del gráfico 1 la enumeración ENUM_CHART_EVENT.

De acuerdo con la Documentación, existen 65536 identificadores de eventos personalizados. El primer y último identificador de eventos personalizados se establecen con los valores claros CHARTEVENT_CUSTOM y CHARTEVENT_CUSTOM_LAST, que en expresión numérica serían igual a 1000 y 66534 respectivamente (fig.1).

Fig.1 The first and the last identifiers of custom events

Fig.1 Primer y último identificador de los eventos personalizados

Si el autor se lleva bien con la aritmética, entonces, contando el primer identificador y el último, obtenemos en total: 66534-1000+1=65535.

Antes de utilizar los eventos personalizados, hay que inventarlos. En este sentido, el desarrollador es el inspirador de la idea y el autor de la concepción del evento, que luego se realizará en forma de algoritmo para el futuro asesor. No estaría mal disponer de una cierta clasificación de los eventos personalizados. Este método cognitivo permite, si no deshacerse, al menos sí reducir el nivel de indeterminación, aumentando el orden en el transcurso del razonamiento.

Propongo estudiar este criterio de evento personalizado como una fuente. Por ejemplo, el desarrollador sergeev ha lanzado una idea sobre un prototipo de robot comercial. Divide todos los eventos en 3 grupos (fig.2).

Fig.2 Groups of custom event sources

Fig.2 Grupos de fuentes de eventos personalizados

Entonces, de acuerdo con esta idea general, hay que procesar los eventos personalizados partiendo de la pertenencia de grupo.

Vamos a probar, para comenzar, a "crear" algo sencillo. Tomemos el primer grupo, los eventos de indicador. Los eventos que pueden entrar en este grupo son: la creación y eliminación de un indicador, la obtención de una señal de apertura, la obtención de una señal de cierre. El segundo grupo lo constituyen los eventos de cambio de estado de las órdenes y posiciones. Vamos a suponer que en nuestro ejemplo, este grupo incluya: apertura de posición y cierre de posición. Todo es muy simple. Y, el que quizá sea el grupo más complicado para la formalización es el de los eventos externos.

Tomemos 2 eventos: pausa y actualización del comercio de manera manual.

Fig.3 Sources of custom events

Fig.3 Fuentes de los eventos personalizados

El método deductivo (de lo general a lo concreto) permite exponer al detalle el esquema primario (fig.3). Precisamente, conforme a este esquema crearemos más tarde los tipos de eventos en la clase correspondiente (recuadro1).

Table 1 Custom events

Recuadro1 Eventos personalizados

Puede que el recuadro aún no "tire" como concepción de evento, pero es un comienzo. Mostraré un enfoque más. Como es bien sabido, el modelo de sistema de comercio abstracto consta de tres subsistemas, los módulos básicos (fig.4).

Fig.4 Model of an abstract trading system

Fig.4 Modelo de sistema comercial abstracto

Entonces, los eventos personalizados en base al criterio "fuente" pueden ser clasificados como eventos que surgen en:

  1. el subsistema de señal;
  2. el subsistema de acompañamiento de posición;
  3. el subsistema de gestión de capital.

El último puede incluir, por ejemplo, eventos tales como: alcance del nivel de pérdida permisible, el aumento del volumen comercial en el momento establecido, el aumento del tanto por ciento del límite de pérdidas, etc.


2. Procesador y el generador ChartEvent

Voy a seleccionar varias líneas del procesador y el generador de evento del gráfico. En lo que respecta al procesamiento del evento personalizado del gráfico, el principio es análogo al procesamiento del evento típico del gráfico.

El procesador, la función OnChartEvent(), adopta como parámetros cuatro constantes. Los más probable es que con ayuda de tal mecanismo, el procesador haya realizado la idea de la identificación del evento y la obtención de información adicional sobre él. A mi parecer, es un mecanismo de programación muy cómodo y compacto.

La función EventChartCustom() genera los eventos personalizados del gráfico. Además se puede crear un evento tanto para "su propio" gráfico, como para uno "ajeno". Quizá el artículo más intersante sobre la idea de los gráficos propios y ajenos sea La implementación del modo multidivisa en MetaTrader 5.

Percibo cierta disonancia en el hecho de que el identificador de evento pertenezca al tipo ushort en el generador, mientras que el en procesador pertenece al tipo int. Seguramente, lo más lógico habría sido utilizar también en el procesador el tipo de datos ushort.


3. Clase del evento personalizado

Bien, como ya he hecho notar, el autor del asesor debe preocuparse él mismo sobre la concepción de evento. Probemos a trabajar con los eventos mostrados en el recuadro 1. En primer lugar, hay que aclararse con la clase base del evento personalizado CEventBase y sus descendientes (fig.5).

Fig.5 Hierarchy of event classes

Fig.5 Jerarquía de las clases de evento

La clase más básica tiene el aspecto siguiente:

//+------------------------------------------------------------------+
//| Class CEventBase.                                                |
//| Purpose: base class for a custom event                           |
//| Derives from class CObject.                                      |
//+------------------------------------------------------------------+
class CEventBase : public CObject
  {
protected:
   ENUM_EVENT_TYPE   m_type;
   ushort            m_id;
   SEventData        m_data;

public:
   void              CEventBase(void)
     {
      this.m_id=0;
      this.m_type=EVENT_TYPE_NULL;
     };
   void             ~CEventBase(void){};
   //--
   bool              Generate(const ushort _event_id,const SEventData &_data,
                              const bool _is_custom=true);
   ushort            GetId(void) {return this.m_id;};

private:
   virtual bool      Validate(void) {return true;};
  };

El tipo de evento se establece con la enumeración ENUM_EVENT_TYPE:

//+------------------------------------------------------------------+
//| A custom event type enumeration                                  |
//+------------------------------------------------------------------+
enum ENUM_EVENT_TYPE
  {
   EVENT_TYPE_NULL=0,      // no event
   //---
   EVENT_TYPE_INDICATOR=1, // indicator event
   EVENT_TYPE_ORDER=2,     // order event
   EVENT_TYPE_EXTERNAL=3,  // external event
  };

Además, los miembros-datos incluyen el identificador de evento y la estructura de datos.

El método Generate() de la clase básica CEventBase se ocupa de la generación del evento. El método GetId() retorna eventos id, y el método virtual Validate() comprobará el valor del identificador de evento. Al principio había incluido en los componentes de la clase el método de procesamiento de evento. Pero después entendí que cada evento es único, y que con el método abstracto aquí no podrías arreglártelas. Así que delegué esta tarea en los miembros de la clase-procesador de eventos personalizados CEventProcessor.


4. Clase de procesador de eventos personalizados

Se supone que la clase CEventProcessor generará y procesará los 8 eventos presentados. Los miembros-datos de la clase tienen el aspecto siguiente:

//+------------------------------------------------------------------+
//| Class CEventProcessor.                                           |
//| Purpose: base class for an event processor EA                    |
//+------------------------------------------------------------------+
class CEventProcessor
  {
//+----------------------------Data members--------------------------+
protected:
   ulong             m_magic;
   //--- flags
   bool              m_is_init;
   bool              m_is_trade;
   //---
   CEventBase       *m_ptr_event;
   //---
   CTrade            m_trade;
   //---
   CiMA              m_fast_ema;
   CiMA              m_slow_ema;
   //---
   CButton           m_button;
   bool              m_button_state;
//+------------------------------------------------------------------+
  };

La lista de atributos es bastante variada: aquí hay etiquetas de inicialización y de comercio. La primera no permite al asesor comerciar si no ha comenzado con éxito. La segunda comprueba la autorización del usuario para comerciar.

Asimismo, tenemos el índice del objeto del tipo CEventBase, que precisamente trabaja con eventos de diferentes tipos, con ayuda del polimorfismo. El objeto de clase comercial CTrade proporciona acceso a las operaciones comerciales.

Un par de objetos del tipo CiMA facilitan el procesamiento de los datos de los indicadores. Para simplificar el ejemplo, he tomado 2 movings. Ellos captarán la señal comercial. Además, tenemos un representante de clase "Botón". Posibilitará la activación/desactivación "manual" del asesor.

Los métodos de clase los he dividido según el principio de "módulo-procedimiento-función-macro":

//+------------------------------------------------------------------+
//| Class CEventProcessor.                                           |
//| Purpose: base class for an event processor EA                    |
//+------------------------------------------------------------------+
class CEventProcessor
  {
//+-------------------------------Methods----------------------------+
public:
   //--- constructor/destructor
   void              CEventProcessor(const ulong _magic);
   void             ~CEventProcessor(void);

   //--- Modules
   //--- event generating
   bool              Start(void);
   void              Finish(void);
   void              Main(void);
   //--- event processing
   void              ProcessEvent(const ushort _event_id,const SEventData &_data);

private:
   //--- Procedures
   void              Close(void);
   void              Open(void);

   //--- Functions
   ENUM_ORDER_TYPE   CheckCloseSignal(const ENUM_ORDER_TYPE _close_sig);
   ENUM_ORDER_TYPE   CheckOpenSignal(const ENUM_ORDER_TYPE _open_sig);
   bool              GetIndicatorData(double &_fast_vals[],double &_slow_vals[]);

   //--- Macros
   void              ResetEvent(void);
   bool              ButtonStop(void);
   bool              ButtonResume(void);
  };

Entre los módulos, hay tres que solo generan eventos: el de inicio — Start(), el final — Finish(), y el principal — Main(). Y el cuarto módulo, ProcessEvent(), es tanto procesador de eventos, como generador.


4.1 Módulo de incio

Se supone que el módulo será llamado en el procesador OnInit().

//+------------------------------------------------------------------+
//| Start module                                                     |
//+------------------------------------------------------------------+
bool CEventProcessor::Start(void)
  {
//--- create an indicator event object
   this.m_ptr_event=new CIndicatorEvent();
   if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC)
     {
      SEventData data;
      data.lparam=(long)this.m_magic;
      //--- generate CHARTEVENT_CUSTOM+1 event
      if(this.m_ptr_event.Generate(1,data))
         //--- create a button
         if(this.m_button.Create(0,"Start_stop_btn",0,25,25,150,50))
            if(this.ButtonStop())
              {
               this.m_button_state=false;
               return true;
              }
     }

//---
   return false;
  }

En el módulo se crea un índice al objeto del evento de indicador. Después se genera el evento "Creación de un indicador". Y al finalizar se crea un botón. Su estado se pasa al modo "Stop". Esto significa que la próxima vez que se pulse el botón, interrumpirá el funcionamiento del asesor.

En la determinación del método está también implicada la estructura SEventData. Se trata de un container sencillo para los parámetros transmitidos al generador del evento personalizado. Aquí solo se rellenará un campo de la estructura, de tipo long. Guardamos en él el mágico del asesor.


4.2 Módulo final

Se supone que el módulo será llamado en el procesador OnDeinit().

//+------------------------------------------------------------------+
//| Finish  module                                                   |
//+------------------------------------------------------------------+
void CEventProcessor::Finish(void)
  {
//--- reset the event object
   this.ResetEvent();
//--- create an indicator event object
   this.m_ptr_event=new CIndicatorEvent();
   if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC)
     {
      SEventData data;
      data.lparam=(long)this.m_magic;
      //--- generate CHARTEVENT_CUSTOM+2 event
      bool is_generated=this.m_ptr_event.Generate(2,data,false);
      //--- process CHARTEVENT_CUSTOM+2 event
      if(is_generated)
         this.ProcessEvent(CHARTEVENT_CUSTOM+2,data);
     }
  }

En él se descarta el anterior índice de evento y se genera el evento "Eliminación del indicador". Destaco el siguiente detalle: si en el procesador OnDeinit() se genera un evento personalizado, entonces obtendremos un error de hora de ejecución 4001 (error interno inesperado). Por eso, para este método, la generación y procesamiento del evento se ejecutan en los límites del método sin llamar OnChartEvent().

Mediante la estructura SEventData, asimismo, solo guardamos el mágico del asesor.


4.3 Módulo principal

Se supone que el módulo será llamado en el procesador OnTick().

//+------------------------------------------------------------------+
//| Main  module                                                     |
//+------------------------------------------------------------------+
void CEventProcessor::Main(void)
  {
//--- a new bar object
   static CisNewBar newBar;

//--- if initialized     
   if(this.m_is_init)
      //--- if not paused   
      if(this.m_is_trade)
         //--- if a new bar
         if(newBar.isNewBar())
           {
            //--- close module
            this.Close();
            //--- open module
            this.Open();
           }
  }

En este módulo se llaman los procedimientos Open() y Close(). El primero puede generar el evento "Obtención de la señal de apertura", y el segundo, "Obtención de la señal de cierre". La versión actual del módulo funciona totalmente al aparecer una nueva barra. La clase para la búsqueda de una nueva barra ha sido determinada por Konstantín Gruzdev.


4.4 Módulo de procesamiento de eventos

Se supone que el módulo será llamado en el procesador OnChartEvent(). Por su volumen de código, y también por su carga funcional, este módulo es el más grande.

//+------------------------------------------------------------------+
//| Process event module                                             |
//+------------------------------------------------------------------+
void CEventProcessor::ProcessEvent(const ushort _event_id,const SEventData &_data)
  {
//--- check event id
   if(_event_id==CHARTEVENT_OBJECT_CLICK)
     {
      //--- button click
      if(StringCompare(_data.sparam,this.m_button.Name())==0)
        {
         //--- button state
         bool button_curr_state=this.m_button.Pressed();
         //--- to stop
         if(button_curr_state && !this.m_button_state)
           {
            if(this.ButtonResume())
              {
               this.m_button_state=true;
               //--- reset the event object
               this.ResetEvent();
               //--- create an external event object
               this.m_ptr_event=new CExternalEvent();
               //---
               if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC)
                 {
                  SEventData data;
                  data.lparam=(long)this.m_magic;
                  data.dparam=(double)TimeCurrent();
                  //--- generate CHARTEVENT_CUSTOM+7 event
                  ushort curr_id=7;
                  if(!this.m_ptr_event.Generate(curr_id,data))
                     PrintFormat("Failed to generate an event: %d",curr_id);
                 }
              }
           }
         //--- to resume
         else if(!button_curr_state && this.m_button_state)
           {
            if(this.ButtonStop())
              {
               this.m_button_state=false;
               //--- reset the event object
               this.ResetEvent();
               //--- create an external event object
               this.m_ptr_event=new CExternalEvent();
               //---
               if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC)
                 {
                  SEventData data;
                  data.lparam=(long)this.m_magic;
                  data.dparam=(double)TimeCurrent();
                  //--- generate CHARTEVENT_CUSTOM+8 event
                  ushort curr_id=8;
                  if(!this.m_ptr_event.Generate(curr_id,data))
                     PrintFormat("Failed to generate an event: %d",curr_id);
                 }
              }
           }
        }
     }
//--- user event 
   else if(_event_id>CHARTEVENT_CUSTOM)
     {
      long magic=_data.lparam;
      ushort curr_event_id=this.m_ptr_event.GetId();
      //--- check magic
      if(magic==this.m_magic)
         //--- check id
         if(curr_event_id==_event_id)
           {
            //--- process the definite user event 
            switch(_event_id)
              {
               //--- 1) indicator creation
               case CHARTEVENT_CUSTOM+1:
                 {
                  //--- create a fast ema
                  if(this.m_fast_ema.Create(_Symbol,_Period,21,0,MODE_EMA,PRICE_CLOSE))
                     if(this.m_slow_ema.Create(_Symbol,_Period,55,0,MODE_EMA,PRICE_CLOSE))
                        if(this.m_fast_ema.Handle()!=INVALID_HANDLE)
                           if(this.m_slow_ema.Handle()!=INVALID_HANDLE)
                             {
                              this.m_trade.SetExpertMagicNumber(this.m_magic);
                              this.m_trade.SetDeviationInPoints(InpSlippage);
                              //---
                              this.m_is_init=true;
                             }
                  //---
                  break;
                 }
               //--- 2) indicator deletion
               case CHARTEVENT_CUSTOM+2:
                 {
                  //---release indicators
                  bool is_slow_released=IndicatorRelease(this.m_fast_ema.Handle());
                  bool is_fast_released=IndicatorRelease(this.m_slow_ema.Handle());
                  if(!(is_slow_released && is_fast_released))
                    {
                     //--- to log?
                     if(InpIsLogging)
                        Print("Failed to release the indicators!");
                    }
                  //--- reset the event object
                  this.ResetEvent();
                  //---
                  break;
                 }
               //--- 3) check open signal
               case CHARTEVENT_CUSTOM+3:
                 {
                  MqlTick last_tick;
                  if(SymbolInfoTick(_Symbol,last_tick))
                    {
                     //--- signal type
                     ENUM_ORDER_TYPE open_ord_type=(ENUM_ORDER_TYPE)_data.dparam;
                     //---
                     double open_pr,sl_pr,tp_pr,coeff;
                     open_pr=sl_pr=tp_pr=coeff=0.;
                     //---
                     if(open_ord_type==ORDER_TYPE_BUY)
                       {
                        open_pr=last_tick.ask;
                        coeff=1.;
                       }
                     else if(open_ord_type==ORDER_TYPE_SELL)
                       {
                        open_pr=last_tick.bid;
                        coeff=-1.;
                       }
                     sl_pr=open_pr-coeff*InpStopLoss*_Point;
                     tp_pr=open_pr+coeff*InpStopLoss*_Point;

                     //--- to normalize prices
                     open_pr=NormalizeDouble(open_pr,_Digits);
                     sl_pr=NormalizeDouble(sl_pr,_Digits);
                     tp_pr=NormalizeDouble(tp_pr,_Digits);
                     //--- open the position
                     if(!this.m_trade.PositionOpen(_Symbol,open_ord_type,InpTradeLot,open_pr,
                        sl_pr,tp_pr))
                       {
                        //--- to log?
                        if(InpIsLogging)
                           Print("Failed to open the position: "+_Symbol);
                       }
                     else
                       {
                        //--- pause
                        Sleep(InpTradePause);
                        //--- reset the event object
                        this.ResetEvent();
                        //--- create an order event object
                        this.m_ptr_event=new COrderEvent();
                        if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC)
                          {
                           SEventData data;
                           data.lparam=(long)this.m_magic;
                           data.dparam=(double)this.m_trade.ResultDeal();
                           //--- generate CHARTEVENT_CUSTOM+5 event
                           ushort curr_id=5;
                           if(!this.m_ptr_event.Generate(curr_id,data))
                              PrintFormat("Failed to generate an event: %d",curr_id);
                          }
                       }
                    }
                  //---
                  break;
                 }
               //--- 4) check close signal
               case CHARTEVENT_CUSTOM+4:
                 {
                  if(!this.m_trade.PositionClose(_Symbol))
                    {
                     //--- to log?
                     if(InpIsLogging)
                        Print("Failed to close the position: "+_Symbol);
                    }
                  else
                    {
                     //--- pause
                     Sleep(InpTradePause);
                     //--- reset the event object
                     this.ResetEvent();
                     //--- create an order event object
                     this.m_ptr_event=new COrderEvent();
                     if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC)
                       {
                        SEventData data;
                        data.lparam=(long)this.m_magic;
                        data.dparam=(double)this.m_trade.ResultDeal();
                        //--- generate CHARTEVENT_CUSTOM+6 event
                        ushort curr_id=6;
                        if(!this.m_ptr_event.Generate(curr_id,data))
                           PrintFormat("Failed to generate an event: %d",curr_id);
                       }
                    }
                  //---
                  break;
                 }
               //--- 5) position opening
               case CHARTEVENT_CUSTOM+5:
                 {
                  ulong ticket=(ulong)_data.dparam;
                  ulong deal=(ulong)_data.dparam;
                  //---
                  datetime now=TimeCurrent();
                  //--- check the deals & orders history
                  if(HistorySelect(now-PeriodSeconds(PERIOD_H1),now))
                     if(HistoryDealSelect(deal))
                       {
                        double deal_vol=HistoryDealGetDouble(deal,DEAL_VOLUME);
                        ENUM_DEAL_ENTRY deal_entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal,DEAL_ENTRY);
                        //---
                        if(deal_entry==DEAL_ENTRY_IN)
                          {
                           //--- to log?
                           if(InpIsLogging)
                             {
                              Print("\nNew position for: "+_Symbol);
                              PrintFormat("Volume: %0.2f",deal_vol);
                             }
                          }
                       }
                  //---
                  break;
                 }
               //--- 6) position closing
               case CHARTEVENT_CUSTOM+6:
                 {
                  ulong ticket=(ulong)_data.dparam;
                  ulong deal=(ulong)_data.dparam;
                  //---
                  datetime now=TimeCurrent();
                  //--- check the deals & orders history
                  if(HistorySelect(now-PeriodSeconds(PERIOD_H1),now))
                     if(HistoryDealSelect(deal))
                       {
                        double deal_vol=HistoryDealGetDouble(deal,DEAL_VOLUME);
                        ENUM_DEAL_ENTRY deal_entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal,DEAL_ENTRY);
                        //---
                        if(deal_entry==DEAL_ENTRY_OUT)
                          {
                           //--- to log?
                           if(InpIsLogging)
                             {
                              Print("\nClosed position for: "+_Symbol);
                              PrintFormat("Volume: %0.2f",deal_vol);
                             }
                          }
                       }
                  //---
                  break;
                 }
               //--- 7) stop trading
               case CHARTEVENT_CUSTOM+7:
                 {
                  datetime stop_time=(datetime)_data.dparam;
                  //---
                  this.m_is_trade=false;                  
                  //--- to log?                  
                  if(InpIsLogging)
                     PrintFormat("Expert trading is stopped at: %s",
                                 TimeToString(stop_time,TIME_DATE|TIME_MINUTES|TIME_SECONDS));
                  //---
                  break;
                 }
               //--- 8) resume trading 
               case CHARTEVENT_CUSTOM+8:
                 {
                  datetime resume_time=(datetime)_data.dparam;
                  this.m_is_trade=true;                  
                  //--- to log?                  
                  if(InpIsLogging)                     
                     PrintFormat("Expert trading is resumed at: %s",
                                 TimeToString(resume_time,TIME_DATE|TIME_MINUTES|TIME_SECONDS));
                  //---
                  break;
                 }
              }
           }
     }
  }

Consta de dos partes. La primera se ocupa de procesar los eventos relacionados con el click sobre el objeto "Botón". Si el click tiene lugar, entonces se generará un evento personalizado externo, al que un poco después prestará servicio el propio procesador.

La segunda parte deberá reaccionar a los eventos personalizados generados. En ella hay 2 bloques, donde, después del procesamiento del evento correspondiente, tiene lugar la creación de uno nuevo. El primer bloque presta servicio al evento "Obetención de la señal de apertura". Si se procesa con éxito, genera un nuevo evento de orden "Apertura de posición". El segundo bloque presta servicio al evento "Obetención de la señal de cierre". Si la señal es procesada, entonces aparece el evento "Cierre de posición".

En calidad de ejemplo de uso de la clase CEventProcessor pondré al asesor CustomEventProcessor.mq5. En él todo se ha hecho de manera que se puedan crear eventos y reaccionar a los mismos. El uso de la POO ha permitido escribir de forma bastante compacta el archivo final del código fuente. En el archivo se muestra el código del asesor.

No pienso que siempre sea necesario recurrir al mecanismo del evento personalizado. Seguramente se puedan conformar de otra manera ciertas cosas pequeñas, de poca importancia y que no estén relacionadas con eventos para la estrategia.


Conclusión

En este artículo he tratado de demostrar los principios del trabajo con los eventos personalizados en el entorno MQL5. Espero que el material del artículo provoque el interés no solo de los programadores principiantes.

Me gustaría hacer notar, con cierta alegría, que el lenguaje MQL5 continúa desarrollándose. Es posible que dentro de muy poco aparezcan plantillas de clases, y que algún día también surjan índices para las funciones. Entonces podremos programar un delegado que indique el método de un objeto arbitrario.

Es cómodo distribuir los archivos de origen del archivo en la carpeta de proyectos. En mi caso es la carpeta MQL5\Projects\ChartUserEvent.