Conjunto de instrumentos para el marcado manual de gráficos y comercio (Parte I). Preparación - descripción de la estructura y clase de funciones auxiliares

Oleh Fedorov | 30 septiembre, 2020

Introducción


El autor es un admirador confeso del análisis manual, en el sentido de que no analiza los gráficos con fórmulas complejas e indicadores, sino de forma "manual", usando los gráficos. Esto permite mayor flexibilidad al comerciar, notar cosas que son difíciles de formular, modificar fácilmente los marcos temporales cuando el movimiento se vuelve más fuerte o más débil, y también conocer el comportamiento esperado del precio mucho antes de entrar en el mercado.

Básicamente, para el análisis utilizamos líneas de tendencia en diferentes combinaciones (tridentes, abanicos, niveles, etcétera). Así que hemos creado una herramienta adecuada que permite dibujar muy rápidamente diferentes líneas de tendencia, literalmente con solo presionar una tecla. Ahora, queremos compartir esta herramienta con los demás.

Presentación de vídeo. Cómo funciona



Concepto general. Formulando la tarea


Bien, necesitamos una herramienta que nos ayude a ejecutar las operaciones más frecuentes utilizando los atajos de teclado.

¿De qué tareas estamos hablando? Por ejemplo, querríamos:

Para la mayoría de estas tareas, claro está, podemos crear scripts. Disponemos de varios, que compartiremos con el lector en los archivos adjuntos. Pero estamos a favor de otro enfoque.

En cualquier asesor o indicador, podemos crear el método OnChartEvent, en el que se describirá la reacción a cualquier evento: pulsación de teclas, movimiento del ratón, creación de un objeto gráfico y su eliminación.

Por eso, hemos decidido que el programa se ejecutará en forma de archivos de inclusión. Todas las funciones y variables han sido distribuidas por varias clases, para que resulte más cómodo recurrir a ellas. Ahora, necesitamos las clases solo para que resulte cómodo agrupar las funciones: por eso, en las primeras implementaciones, no habrá nada complicado, como la herencia o las "fábricas de clases". Se trata simplemente de una colección.

Y, por supuesto, deseamos que la clase sea multiplataforma y que funcione de una forma similar tanto en MQL4, como en MQL5.

Estructura del programa

Lista de archivos

La biblioteca contiene cinco archivos interrelacionados. Todos los archivos se ubican en la misma carpeta "Shortcuts", en el directorio Include. Sus nombres se pueden ver en la figura: GlobalVariables.mqh, Graphics.mqh, Mouse.mqh, Shortcuts.mqh, Utilites.mqh.

Archivo principal de la biblioteca(Shortcuts.mqh)

Archivo principal del programa  — "Shortcuts.mqh". En él se implementará la lógica de reacción a las teclas. Y precisamente este archivo se incluirá en el experto. También se incluirán en él todos los archivos auxiliares.

//+------------------------------------------------------------------+
//|                                                    Shortcuts.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                            https://www.mql5.com/es/articles/7468 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://www.mql5.com/en/articles/7468"

#include "GlobalVariables.mqh"
#include "Mouse.mqh"
#include "Utilites.mqh"
#include "Graphics.mqh"

//+------------------------------------------------------------------+
//| The main control class of the program. It should be connected    |
//| to the Expert Advisor                                            |
//+------------------------------------------------------------------+
class CShortcuts
  {
private:
   CGraphics         m_graphics;   // Object for drawing m_graphics
public:
                     CShortcuts();
   void              OnChartEvent(const int id,
                                  const long &lparam,
                                  const double &dparam,
                                  const string &sparam);
  };
//+------------------------------------------------------------------+
//| Default constructor                                              |
//+------------------------------------------------------------------+
CShortcuts::CShortcuts(void)
  {
   ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,true);
  }

//+------------------------------------------------------------------+
//| Event handling function                                          |
//+------------------------------------------------------------------+
void CShortcuts::OnChartEvent(
   const int id,
   const long &lparam,
   const double &dparam,
   const string &sparam
)
  {
//---

   // This will contain the description of the events related to keystrokes
   // and mouse movements

   //  ...

  }
}

CShortcuts shortcuts;

El archivo contiene la descripción de la clase CShortcuts.

Al inicio del archivo se incluyen todas las clases auxiliares.

En la clases solo hay dos métodos: el primero es el manejador de eventos OnChartEvent, en el que propiamente tendrá lugar el procesamiento de los eventos de las teclas y el movimiento del ratón; el segundo es el constructor por defecto, en el que se permite procesar el movimiento del ratón.

Después de la descripción de la clase, se crea la variable shortcuts,que debemos utilizar en el método OnChartEvent del experto principal al incluir la biblioteca.

La propia inclusión requiere dos líneas:

//+------------------------------------------------------------------+
//| The main Expert (file "Shortcuts-Main-Expert.mq5")               |
//+------------------------------------------------------------------+
#include <Shortcuts\Shortcuts.mqh>


// ...

//+------------------------------------------------------------------+
//| The ChartEvent function                                          |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   shortcuts.OnChartEvent(id,lparam,dparam,sparam);
  }

La primera línea incluye el archivo de la clase, la segunda, transmite a la clase el control del procesamiento de eventos.

Después de ello, el asesor estará listo para funcionar: podremos compilarlo y comenzar a dibujar las líneas de inmediato.

Clase para procesar los movimientos del ratón

La clase que guardará todos los parámetros principales de la ubicación actual del cursor: las coordenadas X, Y en píxeles y en precio/hora, el número de la barra en la que se encuentra el puntero, etcétera, todo se guarda en el archivo "Mouse.mqh".

//+------------------------------------------------------------------+
//|                                                        Mouse.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                            https://www.mql5.com/es/articles/7468 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://www.mql5.com/en/articles/7468"
//+------------------------------------------------------------------+
//| Mouse movement handling class                                    |
//+------------------------------------------------------------------+
class CMouse
  {
   //--- Members
private:
   static int        m_x;           // X
   static int        m_y;           // Y
   static int        m_barNumber;   // Bar number
   static bool       m_below;       // Indication of a cursor above the price
   static bool       m_above;       // Indication of a cursor below the price

   static datetime   m_currentTime; // Current time
   static double     m_currentPrice;// Current price
   //--- Methods
public:

   //--- Remembers the main parameters of the mouse cursor
   static void       SetCurrentParameters(
      const int id,
      const long &lparam,
      const double &dparam,
      const string &sparam
   );
   //--- Returns the X coordinate (in pixels) of the current cursor position
   static int        X(void) {return m_x;}
   //--- Returns the Y coordinate (in pixels) of the current cursor position
   static int        Y(void) {return m_y;}
   //--- Returns the price of the current cursor position
   static double     Price(void) {return m_currentPrice;}
   //--- Returns the time of the current cursor position
   static datetime   Time(void) {return m_currentTime;}
   //--- Returns the bar number of the current cursor position
   static int        Bar(void) {return m_barNumber;}
   //--- Returns a flag showing that the price is below the Low of the current bar
   static bool       Below(void) {return m_below;}
   //--- Returns a flag showing that the price is above the High of the current bar
   static bool       Above(void) {return m_above;}
  };
//---

int CMouse::m_x=0;
int CMouse::m_y=0;
int CMouse::m_barNumber=0;
bool CMouse::m_below=false;
bool CMouse::m_above=false;
datetime CMouse::m_currentTime=0;
double CMouse::m_currentPrice=0;


//+------------------------------------------------------------------+
//| Remembers the main parameters for a mouse movement: coordinates  |
//| of cursor in pixels and price/time, whether the cursor is above  |
//| or below price.                                                  |
//|+-----------------------------------------------------------------+
static void CMouse::SetCurrentParameters(
   const int id,
   const long &lparam,
   const double &dparam,
   const string &sparam
)
  {
//---
   int window = 0;
//---
   ChartXYToTimePrice(
      0,
      (int)lparam,
      (int)dparam,
      window,
      m_currentTime,
      m_currentPrice
   );
   m_x=(int)lparam;
   m_y=(int)dparam;
   m_barNumber=iBarShift(
                  Symbol(),
                  PERIOD_CURRENT,
                  m_currentTime
               );
   m_below=m_currentPrice<iLow(Symbol(),PERIOD_CURRENT,m_barNumber);
   m_above=m_currentPrice>iHigh(Symbol(),PERIOD_CURRENT,m_barNumber);
  }

//+------------------------------------------------------------------+

Esta clase puede utilizarse desde cualquier lugar del programa, ya que sus métodos han sido declarados estáticos. No es obligatorio crear una instancia de esta clase para usarla.

Descripción del bloque de ajustes del experto. Archivo "GlobalVariables.mqh"


Todos los ajustes disponibles para el usuario se guardan en el archivo "GlobalVariables.mqh".

En el siguiente artículo, describiremos muchos más ajustes, ya que aparecerán más objetos a dibujar.

Por el momento, describiremos el código que se usa en la versión actual:

//+------------------------------------------------------------------+
//|                                              GlobalVariables.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                            https://www.mql5.com/es/articles/7468 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://www.mql5.com/en/articles/7468"
//+------------------------------------------------------------------+
//| File describing parameters available to the user                 |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Key settings                                                     |
//+------------------------------------------------------------------+
input string   Keys="=== Key settings ===";
input string   Up_Key="U";            // Switch timeframe up
input string   Down_Key="D";          // Switch timeframe down
input string   Trend_Line_Key="T";              // Trend line
input string   Switch_Trend_Ray_Key="R";        // Indication of a trend line ray
input string   Z_Index_Key="Z";                 // Indication of the chart on top

//+------------------------------------------------------------------+
//| Size settings                                                    |
//+------------------------------------------------------------------+
input string   Dimensions="=== Size settings ===";
input int      Trend_Line_Width=2;        // Trend line width

//+------------------------------------------------------------------+
//| Display styles                                                   |
//+------------------------------------------------------------------+
input string   Styles="=== Display styles ===";
input ENUM_LINE_STYLE      Trend_Line_Style=STYLE_SOLID;       // Trend line style

//+------------------------------------------------------------------+
//| Other parameters                                                 |
//+------------------------------------------------------------------+
input string               Others="=== Other parameters ===";

input bool                 Is_Trend_Ray=false;                      // Trend line - ray
input bool                 Is_Change_Timeframe_On_Create = true;    // Hide objects on higher timeframes?
                                                                    //   (true - hide, false - show)
input bool                 Is_Select_On_Create=true;                // Select upon creation
input bool                 Is_Different_Colors=true;                // Change colors for times

// Number of bars on the left and on the right
// for trend line and fan extreme points
input int                  Fractal_Size_Left=1;                     // Size of the left fractal
input int                  Fractal_Size_Right=1;                    // Size of the right fractal

//+------------------------------------------------------------------+
//| Name prefixes of drawn shapes (can be change only in code,       |
//| not visible in EA parameters)                                    |
//+------------------------------------------------------------------+
// string   Prefixes="=== Prefixes ===";

string   Trend_Line_Prefix="Trend_";                     // Trend line prefix

//+------------------------------------------------------------------+
//| Colors for objects of one timeframe (can be changed only in code,|
//| not visible in EA parameters)                                    |
//+------------------------------------------------------------------+
// string TimeframeColors="=== Time frame colors ===";
color mn1_color=clrCrimson;
color w1_color=clrDarkOrange;
color d1_color=clrGoldenrod;
color h4_color=clrLimeGreen;
color h1_color=clrLime;
color m30_color=clrDeepSkyBlue;
color m15_color=clrBlue;
color m5_color=clrViolet;
color m1_color=clrDarkViolet;
color common_color=clrGray;

//--- Auxiliary constant for displaying error messages
#define DEBUG_MESSAGE_PREFIX "=== ",__FUNCTION__," === "

//--- Constants for describing the main timeframes when drawing
#define PERIOD_LOWER_M5 OBJ_PERIOD_M1|OBJ_PERIOD_M5
#define PERIOD_LOWER_M15 PERIOD_LOWER_M5|OBJ_PERIOD_M15
#define PERIOD_LOWER_M30 PERIOD_LOWER_M15|OBJ_PERIOD_M30
#define PERIOD_LOWER_H1 PERIOD_LOWER_M30|OBJ_PERIOD_H1
#define PERIOD_LOWER_H4 PERIOD_LOWER_H1|OBJ_PERIOD_H4
#define PERIOD_LOWER_D1 PERIOD_LOWER_H4|OBJ_PERIOD_D1
#define PERIOD_LOWER_W1 PERIOD_LOWER_D1|OBJ_PERIOD_W1
//+------------------------------------------------------------------+

Funciones auxiliares

En el programa hay multitud de funciones que no tienen relación directa con el dibujado, pero que, en cambio, ayudan a encontrar los extremos, alternar el marco temporal, etcétera. Todas ellas han sido introducidas en el archivo "Utilites.mqh".

Encabezado del archivo Utilites.mqh

//+------------------------------------------------------------------+
//|                                                     Utilites.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                            https://www.mql5.com/es/articles/7468 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://www.mql5.com/en/articles/7468"

//+------------------------------------------------------------------+
//| Class describing auxiliary functions                             |
//+------------------------------------------------------------------+
class CUtilites
  {
public:
   //--- Changes the timeframe to the next one on the toolbar
   static void       ChangeTimeframes(bool isUp);
   //--- Converts string command constants to keycodes
   static int        GetCurrentOperationChar(string keyString);
   //--- Switches layers in charts (the chart is on top of all objects)
   static void       ChangeChartZIndex(void);
   //--- Returns the number of the nearest extreme bar
   static int        GetNearestExtremumBarNumber(
      int starting_number=0,
      bool is_search_right=false,
      bool is_up=false,
      int left_side_bars=1,
      int right_side_bars=1,
      string symbol=NULL,
      ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT
   );

   //--- Returns the color for the current timeframe
   static color      GetTimeFrameColor(long allDownPeriodsValue);
   //--- Returns a list of all timeframes lower than the current one (including the current one)
   static long       GetAllLowerTimeframes(int NeededTimeframe=PERIOD_CURRENT);
   //--- Coordinates of the straight line. Writes numbers of extreme bars to points p1 and p2
   static void       SetExtremumsBarsNumbers(bool _is_up,int &p1, int &p2);
   //--- Converts a string to to an array of floating point numbers
   static void       StringToDoubleArray(
      string _haystack,
      double &_result[],
      const string _delimiter=","
   );
   //--- Determines the name of the current object
   static string            GetCurrentObjectName(
      const string _prefix,
      const ENUM_OBJECT _type=OBJ_TREND,
      int _number = -1
   );
   //--- Obtains the number of the next object
   static int               GetNextObjectNumber(
      const string _prefix,
      const ENUM_OBJECT _object_type,
      bool true
   );
   //--- Returns the distance, in screen pixels, between adjacent bars
   static int               GetBarsPixelDistance(void);
   //--- Converts a numeric value of the timeframe to its string name
   static string            GetTimeframeSymbolName(
      ENUM_TIMEFRAMES _timeframe=PERIOD_CURRENT  // Desired timeframe
   );
  };

Función que cambia secuencialmente el periodo del gráfico

Las primeras funciones en este archivo no son importantes. Por ejemplo, la función que cambia los periodos del gráfico secuencialmente uno tras otro tiene el aspecto siguiente:

//+------------------------------------------------------------------+
//| Sequentially changes the current chart period                    |
//|                                                                  |
//| In this implementation, only the timeframes shown in the         |
//| standard panel are used.                                         |
//|                                                                  |
//| Parameters:                                                      |
//|    _isUp - timeframe switch direction: up (true)                 |
//|            or down (false)                                       |
//+------------------------------------------------------------------+
static void CUtilites::ChangeTimeframes(bool _isUp)
  {
   ENUM_TIMEFRAMES timeframes[] =
     {
      PERIOD_CURRENT,
      PERIOD_M1,
      PERIOD_M5,
      PERIOD_M15,
      PERIOD_M30,
      PERIOD_H1,
      PERIOD_H4,
      PERIOD_D1,
      PERIOD_W1,
      PERIOD_MN1
     };
   int period = Period();
   int shift = ArrayBsearch(timeframes,period);
   if(_isUp && shift < ArraySize(timeframes)-1)
     {
      ChartSetSymbolPeriod(0,NULL,timeframes[++shift]);
     }
   else
      if(!_isUp && shift > 1)
        {
         ChartSetSymbolPeriod(0,NULL,timeframes[--shift]);
        }
  } 

En primer lugar, se describe la matriz con todos los marcos temporales indicados en el panel estándar de control por defecto. Si usted necesita alternar entre todos los periodos temporales disponibles en MetaTrader 5, deberá simplemente terminar de escribir las constantes correspondientes en los lugares necesarios de la matriz. Bien es cierto que, en este caso, podría perderse la compatibilidad inversa, es decir, la biblioteca podría dejar de funcionar con MQL4.

A continuación, utilizamos las funciones estándar para obtener el periodo actual y encontrarlo en la lista.

Después, para alternar los periodos temporales del gráfico, utilizaremos la función estándar ChartSetSymbolPeriod, a la que se transmitirá el periodo que sigue al actual.

En lo que respecta a las demás funciones, no tiene especial sentido hacer comentarios adicionales al código. Es solo código.

Varias funciones sencillas

//+------------------------------------------------------------------+
//| Converts string command constants to keycodes                    |
//+------------------------------------------------------------------+
static int CUtilites::GetCurrentOperationChar(string keyString)
  {
   string keyValue = keyString;
   StringToUpper(keyValue);
   return(StringGetCharacter(keyValue,0));
  }
//+------------------------------------------------------------------+
//| Switch chart position to display on top of other objects         |
//+------------------------------------------------------------------+
static void CUtilites::ChangeChartZIndex(void)
  {
   ChartSetInteger(
      0,
      CHART_FOREGROUND,
      !(bool)ChartGetInteger(0,CHART_FOREGROUND)
   );
   ChartRedraw(0);
  } 
//+------------------------------------------------------------------+
//| Returns a string name for any standard timeframe                 |
//| Parameters:                                                      |
//|    _timeframe - ENUM_TIMEFRAMES numeric value for which we need  |
//|                 to find a string name                            |
//| Return value:                                                    |
//|   string name of the required timeframe                          |
//+------------------------------------------------------------------+
static string CUtilites::GetTimeframeSymbolName(
   ENUM_TIMEFRAMES _timeframe=PERIOD_CURRENT  // Desired timeframe
)
  {
   ENUM_TIMEFRAMES current_timeframe; // current timeframe
   string result = "";
//---
   if(_timeframe == PERIOD_CURRENT)
     {
      current_timeframe = Period();
     }
   else
     {
      current_timeframe = _timeframe;
     }
//---
   switch(current_timeframe)
     {
      case PERIOD_M1:
         return "M1";
      case PERIOD_M2:
         return "M2";
      case PERIOD_M3:
         return "M3";
      case PERIOD_M4:
         return "M4";
      case PERIOD_M5:
         return "M5";
      case PERIOD_M6:
         return "M6";
      case PERIOD_M10:
         return "M10";
      case PERIOD_M12:
         return "M12";
      case PERIOD_M15:
         return "M15";
      case PERIOD_M20:
         return "M20";
      case PERIOD_M30:
         return "M30";
      case PERIOD_H1:
         return "H1";
      case PERIOD_H2:
         return "M1";
      case PERIOD_H3:
         return "H3";
      case PERIOD_H4:
         return "H4";
      case PERIOD_H6:
         return "H6";
      case PERIOD_H8:
         return "H8";
      case PERIOD_D1:
         return "D1";
      case PERIOD_W1:
         return "W1";
      case PERIOD_MN1:
         return "MN1";
      default:
         return "Unknown";
     }
  }
//+------------------------------------------------------------------+
//| Returns the standard color for the current timeframe             |
//+------------------------------------------------------------------+
static color CUtilites::GetTimeFrameColor(long _all_down_periods_value)
  {
   if(Is_Different_Colors)
     {
      switch((int)_all_down_periods_value)
        {
         case OBJ_PERIOD_M1:
            return (m1_color);
         case PERIOD_LOWER_M5:
            return (m5_color);
         case PERIOD_LOWER_M15:
            return (m15_color);
         case PERIOD_LOWER_M30:
            return (m30_color);
         case PERIOD_LOWER_H1:
            return (h1_color);
         case PERIOD_LOWER_H4:
            return (h4_color);
         case PERIOD_LOWER_D1:
            return (d1_color);
         case PERIOD_LOWER_W1:
            return (w1_color);
         case OBJ_ALL_PERIODS:
            return (mn1_color);
         default:
            return (common_color);
        }
     }
   else
     {
      return (common_color);
     }
  }

Función de búsqueda de los extremos y su aplicación

La siguiente función ayuda a buscar los extremos. Dependiendo de los parámetros, los extremos pueden localizarse tanto a la derecha como a la izquierda del indicador del ratón; también podemos establecer cuántas barras deberá haber a la izquierda del extremo, y cuántas a la derecha.

//+------------------------------------------------------------------+
//| Returns the number of the nearest fractal in the selected        |
//|   direction                                                      |
//| Parameters:                                                      |
//|   starting_number - bar number at which the search starts        |
//|   is_search_right - search to the right (true) or left (false)   |
//|   is_up - if "true", search by High levels, otherwise - Low      |
//|   left_side_bars - number of bars on the left                    |
//|   right_side_bars - number of bars on the right                  |
//|   symbol - symbol name for search                                |
//|   timeframe - period for search                                  |
//| Return value:                                                    |
//|   the number of the extremum closest to starting_number,         |
//|   matching the specified parameters                              |                 
//+------------------------------------------------------------------+
static int CUtilites::GetNearestExtremumBarNumber(
   int starting_number=0,              // Initial bar number
   const bool is_search_right=false,   // Direction to the right
   const bool is_up=false,             // Search by Highs
   const int left_side_bars=1,         // Number of bars to the left
   const int right_side_bars=1,        // Number of bars to the right
   const string symbol=NULL,           // The required symbol
   const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT // The required timeframe
)
  {
//---
   int   i,
         nextExtremum,
         sign = is_search_right ? -1 : 1;

//--- If the starting bar is specified incorrectly
//--- (is beyond the current chart borders)
//--- and search - towards the border...
   if((starting_number-right_side_bars<0
       && is_search_right)
      || (starting_number+left_side_bars>iBars(symbol,timeframe)
          && !is_search_right)
     )
     { //--- ...it is necessary to display an error message
      if(Print_Warning_Messages)
        {
         Print(DEBUG_MESSAGE_PREFIX,
               "Can't find extremum: ",
               "wrong direction");
         Print("left_side_bars = ",left_side_bars,"; ",
               "right_side_bars = ",right_side_bars);
        }
      return (-2);
     }
   else
     {
      //--- otherwise - the direction allows you to select the correct bar.
      //---   check all bars in the required direction,
      //---   as long as we are beyond the known chart borders
      while((starting_number-right_side_bars<0
             && !is_search_right)
            || (starting_number+left_side_bars>iBars(symbol,timeframe)
                && is_search_right)
           )
        {
         starting_number +=sign;
        }
     }
//---
   i=starting_number;
 
   //--- Preparation is complete. Proceed to search
   while(i-right_side_bars>=0
         && i+left_side_bars<iBars(symbol,timeframe)
        )
     {
      //--- Depending on the level, check the required extremum
      if(is_up)
        { //--- either the upper one
         nextExtremum = iHighest(
                           Symbol(),
                           Period(),
                           MODE_HIGH,
                           left_side_bars+right_side_bars+1,
                           i-right_side_bars
                        );
        }
      else
        {  //--- or the lower one
         nextExtremum = iLowest(
                           Symbol(),
                           Period(),
                           MODE_LOW,
                           left_side_bars+right_side_bars+1,
                           i-right_side_bars
                        );
        }
      if(nextExtremum == i) // If the current bar is an extremum,
        {
         return nextExtremum;  // the problem is solved
        }
      else // Otherwise - continue to shift the counter of the checked candlestick to the desired direction
         if(is_search_right)
           {
            if(nextExtremum<i)
              {
               i=nextExtremum;
              }
            else
              {
               i--;
              }
           }
         else
           {
            if(nextExtremum>i)
              {
               i=nextExtremum;
              }
            else
              {
               i++;
              }
           }
     }
//--- If the edge is reached but no extremum has been found,
   if(Print_Warning_Messages)
     {
      //--- show an error message.
      Print(DEBUG_MESSAGE_PREFIX,
            "Can't find extremum: ",
            "an incorrect starting point or wrong border conditions.");
      Print("left_side_bars = ",left_side_bars,"; ",
            "right_side_bars = ",right_side_bars);
     }
   return (-1);
  } 

Para dibujar las líneas de tendencia, necesitamos una función que encuentre los dos extremos más cercanos a la derecha del ratón. Esta función puede utilizar la anterior:

//+------------------------------------------------------------------+
//| Finds 2 nearest extreme points to the right of the current       |
//| mouse pointer position                                           |
//| Parameters:                                                      |
//|    _is_up - search by High (true) or Low (false)                 |
//|    int &_p1 - bar number of the first point                      |
//|    int &_p2 - bar number of the second point                     |
//+------------------------------------------------------------------+
static void CUtilites::SetExtremumsBarsNumbers(
   bool _is_up, // search by High (true) or by Low (false)
   int &_p1,    // bar number of the first point
   int &_p2     // bar number of second point
)
  {
   int dropped_bar_number=CMouse::Bar();
//---
   _p1=CUtilites::GetNearestExtremumBarNumber(
          dropped_bar_number,
          true,
          _is_up,
          Fractal_Size_Left,
          Fractal_Size_Right
       );
   _p2=CUtilites::GetNearestExtremumBarNumber(
          _p1-1, // Bar to the left of the previous found extremum
          true,
          _is_up,
          Fractal_Size_Left,
          Fractal_Size_Right
       );
   if(_p2<0)
     {
      _p2=0;
     }
  } 

Generando los nombres de los objetos

Para dibujar objetos iguales en series, deberemos darles el mismo nombre. Para ello, lo más sencillo es tomar algún prefijo que se corresponda con un tipo de objeto determinado, y después añadirle un número único. Los prefijos para los diferentes tipos de objeto se enumeran en el archivo "GlobalVariables.mqh".

Los números son generados por la función correspondiente.

//+------------------------------------------------------------------+
//| Returns the number of the next object in the series              |
//| Parameters:                                                      |
//|   prefix - name prefix for this group of objects.                |
//|   object_type - the type of objects to search in.                |
//|   only_prefixed - if "false", search in all objects              |
//|                      of this type, "true" - only the objects     |
//|                      with the specified prefix                   |
//| Return value:                                                    |
//|   number of the next object in series. If search by prefix,      |
//|      and the existing numbering has a gap, the next number       |
//|      will be be at gap beginning.                                |
//+------------------------------------------------------------------+
int               CUtilites::GetNextObjectNumber(
   const string prefix,
   const ENUM_OBJECT object_type,
   bool true
)
  {
   int count = ObjectsTotal(0,0,object_type),
       i,
       current_element_number,
       total_elements = 0;
   string current_element_name = "",
          comment_text = "";
//---
   if(only_prefixed)
     {
      for(i=0; i<count; i++)
        {
         current_element_name=ObjectName(0,i,0,object_type);
         if(StringSubstr(current_element_name,0,StringLen(prefix))==prefix)
           {
            current_element_number=
               (int)StringToInteger(
                  StringSubstr(current_element_name,
                               StringLen(prefix),
                               -1)
               );
            if(current_element_number!=total_elements)
              {
               break;
              }
            total_elements++;
           }
        }
     }
   else
     {
      total_elements = ObjectsTotal(0,-1,object_type);
      do
        {
         current_element_name = GetCurrentObjectName(
                                   prefix,
                                   object_type,
                                   total_elements
                                );
         if(ObjectFind(0,current_element_name)>=0)
           {
            total_elements++;
           }
        }
      while(ObjectFind(0,current_element_name)>=0);
     }
//---
   return(total_elements);
  } 

En el código se han implementado dos algoritmos de búsqueda. El primero (y principal para esta biblioteca) itera por todos los objetos que se correspondan con el tipo y tengan el prefijo indicado. En cuanto encuentra un número libre, lo retorna al usuario. Esto permite cerrar los "agujeros" en la numeración.

No obstante, este algoritmo no es demasiado adecuado para los casos en los que pueden haber varios objetos con el mismo número, pero diferentes sufijos. En versiones anteriores, cuando aún lo utilizábamos para dibujar con scripts, así nombrábamos los conjuntos de tridentes. 

Por ese motivo, en la biblioteca se ha implementado un segundo método de búsqueda. Se toma el número total de objetos de un tipo dado, y después se comprueba si existe un nombre que comience por el mismo prefijo y tenga el mismo número. De ser así, el número se aumenta en 1 hasta que se encuentre un valor libre.

Y cuando ya tenemos el número (o se puede obtener fácilmente con la ayuda de la función), crear el nombre se convierte en algo muy sencillo.

//+------------------------------------------------------------------+
//| Generates the current element name                               |
//| Parameters:                                                      |
//|    _prefix - name prefix for this group of objects.              |
//|    _type - the type of objects to search in.                     |
//|    _number - the number of the current object if it is ready.    |
//| Return value:                                                    |
//|    current object name string.                                   |
//+------------------------------------------------------------------+
string            CUtilites::GetCurrentObjectName(
   string _prefix,
   ENUM_OBJECT _type=OBJ_TREND,
   int _number = -1
)
  {
   int Current_Line_Number;

//--- Addition to the prefix - current timeframe
   string Current_Line_Name=IntegerToString(PeriodSeconds()/60)+"_"+_prefix;

//--- Get the element number
   if(_number<0)
     {
      Current_Line_Number = GetNextObjectNumber(Current_Line_Name,_type);
     }
   else
     {
      Current_Line_Number = _number;
     }

//--- Generate the name
   Current_Line_Name +=IntegerToString(Current_Line_Number,4,StringGetCharacter("0",0));

//---
   return (Current_Line_Name);
  } 

Distancia entre barras colindantes (en píxeles)

A veces, resulta necesario calcular la distancia hasta un determinado punto en el futuro. Uno de los métodos más fiables para alcanzar d, consiste en calcular la distancia en píxeles entre dos barras colindantes, y después multiplicarla por el coeficiente necesario (cuántas barras queremos retroceder).

La distancia entre barras colindantes se puede calcular con la ayuda de la siguiente función:

//+------------------------------------------------------------------+
//| Calculates a distance in pixels between two adjacent bars        |
//+------------------------------------------------------------------+
int        CUtilites::GetBarsPixelDistance(void)
  {
   double price; // arbitrary price on the chart (used for
                 // standard functions calculating coordinates
   datetime time1,time2;  // the time of the current and adjacent candlesticks
   int x1,x2,y1,y2;       // screen coordinates of two points
                          //   on adjacent candlesticks
   int deltha;            // result - the desired distance

//--- Initial settings
   price = iHigh(Symbol(),PERIOD_CURRENT,CMouse::Bar());
   time1 = CMouse::Time();

//--- Get the time of the current candlestick
   if(CMouse::Bar()<Bars(Symbol(),PERIOD_CURRENT)){ // if at the middle of the chart,
      time2 = time1+PeriodSeconds();                // take the candlestick on the left
   }
   else {                                           // otherwise
      time2 = time1;
      time1 = time1-PeriodSeconds();                // take the candlestick on the right
   }

//--- Convert coordinates form price/time values to screen pixels
   ChartTimePriceToXY(0,0,time1,price,x1,y1);
   ChartTimePriceToXY(0,0,time2,price,x2,y2);

//--- Calculate the distance
   deltha = MathAbs(x2-x1);
//---
   return (deltha);
  } 

Función que convierte una línea en una matriz de números double

Para configurar los niveles de Fibonacci con la ayuda de los parámetros del asesor, lo más sencillo es utilizar líneas que consten de valores separados por comas. Sin embargo, MQL requiere que se usen números double para establecer los niveles.

La siguiente función nos ayudará a extraer los números de la línea.

//+------------------------------------------------------------------+
//| Converts a string with separators to an array of double          |
//|    numbers                                                       |
//| Parameters:                                                      |
//|    _haystack - source string for conversion                      |
//|    _result[] - resulting array                                   |
//|    _delimiter - separator character                              |
//+------------------------------------------------------------------+
static void CUtilites::StringToDoubleArray(
   string _haystack,             // source string
   double &_result[],            // array of results
   const string _delimiter=","   // separator
)
  {
//---
   string haystack_pieces[]; // Array of string fragments
   int pieces_count,         // Number of fragments
       i;                    // Counter
   string current_number=""; // The current string fragment (estimated number)

//--- Split the string into fragments
   pieces_count=StringSplit(_haystack,StringGetCharacter(_delimiter,0),haystack_pieces);
//--- Convert
   if(pieces_count>0)
     {
      ArrayResize(_result,pieces_count);
      for(i=0; i<pieces_count; i++)
        {
         StringTrimLeft(haystack_pieces[i]);
         StringTrimRight(haystack_pieces[i]);
         _result[i]=StringToDouble(haystack_pieces[i]);
        }
     }
   else
     {
      ArrayResize(_result,1);
      _result[0]=0;
     }
  } 

Clase de dibujado: ejemplo de uso de funciones utilitarias

El artículo está resultando muy voluminoso, por eso, dejaremos la descripción de la mayor parte de las funciones de dibujado para el artículo siguiente. No obstante, para poner a prueba las posibilidades de algunas funciones ya creadas, vamos a mostrar aquí un código que ayuda a dibujar líneas rectas simples (según dos extremos colindantes).

Encabezado del archivo de dibujado del gráfico Graphics.mqh

//+------------------------------------------------------------------+
//|                                                     Graphics.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                            https://www.mql5.com/es/articles/7468 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://www.mql5.com/en/articles/7468"

//+------------------------------------------------------------------+
//| Class for plotting graphic objects                               |
//+------------------------------------------------------------------+
class CGraphics
  {
   //--- Fields
private:
   bool              m_Is_Trend_Ray;
   bool              m_Is_Change_Timeframe_On_Create;
   //--- Methods
private:
   //--- Sets general parameters for any newly created object
   void              CurrentObjectDecorate(
      const string _name,
      const color _color=clrNONE,
      const int _width = 1,
      const ENUM_LINE_STYLE _style = STYLE_SOLID
   );
public:
   //--- Default constructor
                     CGraphics();
   //--- Universal function for creating trend lines with specified parameters
   bool              TrendCreate(
      const long            chart_ID=0,        // chart ID
      const string          name="TrendLine",  // line name
      const int             sub_window=0,      // subwindow number
      datetime              time1=0,           // time of the first point
      double                price1=0,          // price of the first point
      datetime              time2=0,           // time of the second point
      double                price2=0,          // price of the second point
      const color           clr=clrRed,        // line color
      const ENUM_LINE_STYLE style=STYLE_SOLID, // line style
      const int             width=1,           // line width
      const bool            back=false,        // in the background
      const bool            selection=true,    // if the line is selected
      const bool            ray_right=false,   // ray to the right
      const bool            hidden=true,       // hide in the list of objects
      const long            z_order=0          // Z-Index
   );
   //--- Plots a trend line based on the two nearest (to the right of the mouse) extreme points
   void              DrawTrendLine(void);
   //--- Checks if the straight line is a ray
   bool              IsRay() {return m_Is_Trend_Ray;}
   //--- Sets the ray indication (whether the line should be extended to the right)
   void              IsRay(bool _is_ray) {m_Is_Trend_Ray = _is_ray;}
   //--- Checks if the display of objects created by the program should be changed on higher timeframes
   bool              IsChangeTimeframe(void) {return m_Is_Change_Timeframe_On_Create;}
   //--- Sets flags for the display of objects created by the program on higher timeframes
   void              IsChangeTimeframe(bool _is_tf_change) {m_Is_Change_Timeframe_On_Create = _is_tf_change;}
  };
  

Función que establece los parámetros generales para cualquier objeto gráfico creado de nuevo

//+------------------------------------------------------------------+
//| Sets general parameters for all new objects                      |
//| Parameters:                                                      |
//|    _name - the name of the object being modified                 |
//|    _color - the color of the object being modified. If not set,  |
//|             standard color of the current period is used         |
//|    _width - object line width                                    |
//|    _style - object line type                                     |
//+------------------------------------------------------------------+
void CGraphics::CurrentObjectDecorate(
   const string _name,            // the name of the object being modified
   const color _color=clrNONE,    // the color of the object being modified
   const int _width = 1,          // object line width
   const ENUM_LINE_STYLE _style = STYLE_SOLID  // object line style
)
  {
   long timeframes;         // timeframes on which the object will be displayed
   color currentColor;      // object color
//--- Specify timeframes on which the object will be displayed
   if(Is_Change_Timeframe_On_Create)
     {
      timeframes = CUtilites::GetAllLowerTimeframes();
     }
   else
     {
      timeframes = OBJ_ALL_PERIODS;
     }
//--- Specify the object color
   if(_color != clrNONE)
     {
      currentColor = _color;
     }
   else
     {
      currentColor = CUtilites::GetTimeFrameColor(timeframes);
     }
//--- Set attributes
   ObjectSetInteger(0,_name,OBJPROP_COLOR,currentColor);            // Color
   ObjectSetInteger(0,_name,OBJPROP_TIMEFRAMES,timeframes);         // Periods
   ObjectSetInteger(0,_name,OBJPROP_HIDDEN,true);                   // Hide in the list of objects
   ObjectSetInteger(0,_name,OBJPROP_SELECTABLE,true);               // Ability to select
   ObjectSetInteger(0,_name,OBJPROP_SELECTED,Is_Select_On_Create);  // Stay selected after creation?
   ObjectSetInteger(0,_name,OBJPROP_WIDTH,_width);                  // Line width
   ObjectSetInteger(0,_name,OBJPROP_STYLE,_style);                  // Line style
  } 

Función de dibujado de rectas

//+------------------------------------------------------------------+
//| Universal function for creating trend lines with specified       |
//|    parameters                                                    |
//| Parameters:                                                      |
//|    chart_ID - chart ID                                           |
//|    name - line name                                              |
//|    sub_window - subwindow number                                 |
//|    time1 - time of the first point                               |
//|    price1 - price of the first point                             |
//|    time2 - time of the second point                              |
//|    price2 - price of the second point                            |
//|    clr - line color                                              |
//|    style - line style                                            |
//|    width - line width                                            |
//|    back - in the background                                      |
//|    selection - if the line is selected                           |
//|    ray_right - ray to the right                                  |
//|    hidden - hide in the list of objects                          |
//|    z_order - priority of mouse clicks (Z-Index)                  |
//| Return value:                                                    |
//|    indication of success. If line drawing failed,                |
//|    false is returned, otherwise - true                           |
//+------------------------------------------------------------------+
bool              CGraphics::TrendCreate(
      const long            chart_ID=0,        // chart ID
      const string          name="TrendLine",  // line name
      const int             sub_window=0,      // subwindow number
      datetime              time1=0,           // time of the first point
      double                price1=0,          // price of the first point
      datetime              time2=0,           // time of the second point
      double                price2=0,          // price of the second point
      const color           clr=clrRed,        // line color
      const ENUM_LINE_STYLE style=STYLE_SOLID, // line style
      const int             width=1,           // line width
      const bool            back=false,        // in the background
      const bool            selection=true,    // if the line is selected
      const bool            ray_right=false,   // ray to the right
      const bool            hidden=true,       // hide in the list of objects
      const long            z_order=0          // Z-Index
)
  {

//--- Reset the last error message
   ResetLastError();
//--- Create a line
   if(!ObjectCreate(chart_ID,name,OBJ_TREND,sub_window,time1,price1,time2,price2))
     {
      if(Print_Warning_Messages) // if failed, show an error message
        {
         Print(__FUNCTION__,
               ": Can't create trend line! Error code = ",GetLastError());
        }
      return(false);
     }

//--- Set additional attributes
   CurrentObjectDecorate(name,clr,width,style);

//--- line in the foreground (false) or in the background (true)
   ObjectSetInteger(chart_ID,name,OBJPROP_BACK,back);
//--- ray to the right (true) or exact borders (false)
   ObjectSetInteger(chart_ID,name,OBJPROP_RAY_RIGHT,ray_right);
//--- mouse click priority (Z-index)
   ObjectSetInteger(chart_ID,name,OBJPROP_ZORDER,z_order);
//--- Update the chart image
   ChartRedraw(0);
//--- Everything is good. The line has been drawn successfully.
   return(true);
  }

Usando esta función general como base, crearemos otra función que dibujará una recta según dos extremos colindantes.

//+------------------------------------------------------------------+
//| Draws a trend line by nearest extreme points in the right        |
//+------------------------------------------------------------------+
void              CGraphics::DrawTrendLine(void)
  {
   int dropped_bar_number=CMouse::Bar(); // candlestick number under the mouse
   int p1=0,p2=0;                        // numbers of the first and seconds points
   string trend_name =                   // trend line name
      CUtilites::GetCurrentObjectName(Trend_Line_Prefix,OBJ_TREND);
   double
      price1=0,   // price of the first point
      price2=0,   // price of the second point
      tmp_price;  // variable for temporary storing of the price
   datetime
      time1=0,    // time of the first point
      time2=0,    // time of the second point
      tmp_time;
   int x1,x2,y1,y2;   // Point coordinates
   int window=0;      // Subwindow number

//--- Setting initial parameters
   if(CMouse::Below()) // If a mouse cursor is below the candlestick Low
     {
      //--- Find two extreme points below
      CUtilites::SetExtremumsBarsNumbers(false,p1,p2);

      //--- Determine point coordinates
      time1=iTime(Symbol(),PERIOD_CURRENT,p1);
      price1=iLow(Symbol(),PERIOD_CURRENT,p1);
      time2=iTime(Symbol(),PERIOD_CURRENT,p2);
      price2=iLow(Symbol(),PERIOD_CURRENT,p2);
     }
   else // otherwise
      if(CMouse::Above()) // If a mouse cursor is below the candlestick High
        {
         //--- Find two extreme points above
         CUtilites::SetExtremumsBarsNumbers(true,p1,p2);
        
         //--- Determine point coordinates
         time1=iTime(Symbol(),PERIOD_CURRENT,p1);
         price1=iHigh(Symbol(),PERIOD_CURRENT,p1);
         time2=iTime(Symbol(),PERIOD_CURRENT,p2);
         price2=iHigh(Symbol(),PERIOD_CURRENT,p2);

        }
//--- Draw the line
   TrendCreate(0,trend_name,0,
               time1,price1,time2,price2,
               CUtilites::GetTimeFrameColor(CUtilites::GetAllLowerTimeframes()),
               0,Trend_Line_Width,false,true,m_Is_Trend_Ray
              );

//--- Redraw the chart
   ChartRedraw(0);
  } 

Querríamos llamar de nuevo la atención sobre la llamada de la función CUtilites::SetExtremumsBarsNumbers, que recibe los números de las barras para los puntos 1 y 2. Su código ha sido descrito más arriba. Lo demás parece obvio, y no requiere una descripción detallada.

La función resultante dibuja una recta según dos puntos. Dependiendo del parámetro global Is_Trend_Ray, (descrito en el archivo "GlobalVariables.mqh") la línea será, o bien un rayo prolongado hacia la derecha, o simplemente un segmento entre dos extremos.

 Is_Trend_Ray = true  Is_Trend_Ray = false

También sacaremos al teclado la posibilidad de alternar la prolongación de una línea.


Creación de un bloque de control: configuración del método OnChartEvent

Ahora que hemos escrito las funciones principales, podemos configurar los atajos de teclado.

En "Shortcuts.mqh", escribimos el método CShortcuts::OnChartEvent.

//+------------------------------------------------------------------+
//| Event handling function                                          |
//+------------------------------------------------------------------+
void CShortcuts::OnChartEvent(
   const int id,
   const long &lparam,
   const double &dparam,
   const string &sparam
)
  {
//---
   switch(id)
     {
      //--- Save the coordinates of the mouse cursor
      case CHARTEVENT_MOUSE_MOVE:
         CMouse::SetCurrentParameters(id,lparam,dparam,sparam);
         break;

      //--- Handle keystrokes
      case CHARTEVENT_KEYDOWN:
         //--- Change the timeframe 1 level up
         if(CUtilites::GetCurrentOperationChar(Up_Key) == lparam)
           {
            CUtilites::ChangeTimeframes(true);
           };
         //--- Change the timeframe 1 level down
         if(CUtilites::GetCurrentOperationChar(Down_Key) == lparam)
           {
            CUtilites::ChangeTimeframes(false);
           };
         //--- Change the Z Index of the chart (chart on top of all objects)
         if(CUtilites::GetCurrentOperationChar(Z_Index_Key) == lparam)
           {
            CUtilites::ChangeChartZIndex();
           }
         //--- Draw a trend line
         if(CUtilites::GetCurrentOperationChar(Trend_Line_Key) == lparam)
           {
            m_graphics.DrawTrendLine();
           }
         //--- Switch the Is_Trend_Ray parameter
         if(CUtilites::GetCurrentOperationChar(Switch_Trend_Ray_Key) == lparam)
           {
            m_graphics.IsRay(!m_graphics.IsRay());
           }

         break;
         //---

     }
  } 

Teclas utilizadas en la actual implementación de la biblioteca

Acción
 Tecla De la palabra en inglés
 Ir al marco temporal superior por los periodos principales (del panel de periodos) U  Up
 Ir al marco temporal inferior  D  Down
 Alternar el nivel Z del gráfico (gráfico encima o debajo de los objetos)  Z  Z order
 Dibujar una línea de tendencia inclinada según los dos extremos unidireccionales más próximos al ratón  T  Trend line
 Alternar el modo de rayo para las nuevas rectas
 R key  Ray

Conclusión

En el archivo adjunto hay un directorio con la versión actual de la biblioteca. Asimismo, también hay tres scripts

Es mejor incluir la biblioteca en el experto, y no en el indicador, dado que, en el caso de incluirla en el indicador e intentar usar este indicador junto con algún otro asesor, sufriremos una ralentización terrible. Por lo menos, en nuestro caso ha sido así. Puede que no lo hayamos preparado correctamente.

Y, para finalizar, hablemos un poco de nuestros planes.

La segunda versión de la biblioteca contendrá el dibujado de los objetos útiles que hemos visto en el vídeo. Algunos de estos objetos son primitivos (como la línea vertical o la horizontal), y con algunos de ellos, como las líneas con la longitud necesaria, hemos tenido que trabajar más de la cuenta, y además, no siempre funcionan de forma ideal, debido los "errores en las entradas" u otros motivos. Hablaremos de nuestras soluciones al respecto, y, por supuesto, estaremos muy contentos de escuchar cualquier tipo de crítica.

La tercera versión contendrá la interfaz gráfica para configurar los parámetros de los instrumentos gráficos.

La cuarta, si es que la abordamos, será ya un asesor de acompañamiento completo, y permitirá facilitar el comercio manual. Aquí, queremos plantear una pregunta a la comunidad. No estamos seguros de aplicar ninguna solución nueva en comparación con los análogos existentes. La interfaz será de diseño propio, y podemos escuchar sugerencias al respecto, pero el comercio manual es comercio manual :-). Por consiguiente, ¿alguien necesitará esto?