English Русский 中文 Deutsch 日本語 Português
preview
Conjunto de instrumentos para el marcado manual de gráficos y comercio (Parte III). Optimización y nuevos instrumentos

Conjunto de instrumentos para el marcado manual de gráficos y comercio (Parte III). Optimización y nuevos instrumentos

MetaTrader 5Ejemplos | 14 enero 2022, 11:34
1 001 0
Oleh Fedorov
Oleh Fedorov

Introducción

En los artículos anteriores (1, 2), describimos la biblioteca Shortcuts y mostramos un ejemplo de uso de esta biblioteca en forma de asesor. Hasta cierto punto, la biblioteca se asemeja a un organismo vivo. Tras nacer, "sale a la luz", donde se encuentra con el entorno en el que "vivirá". Dicho entorno es cambiante y dicta ciertas leyes. Uno de ellas sería: "desarróllate" :-) Así que debe desarrollarse de la forma correspondiente... Este artículo presenta algunos de los resultados de este proceso.

Recordemos que la biblioteca consta de cinco archivos.

El archivo más importante es Shortcuts.mqh. Este archivo guarda la lógica para el procesamiento de la pulsación de las teclas y, en general, es el único directamente conectado al asesor o indicador empleado. Por consiguiente, ya incluye el resto de archivos y realiza la inicialización de estos.

Todas las variables globales han sido trasladadas al archivo GlobalVariables.mqh. Básicamente, son las configuraciones que nos permiten establecer los colores, el grosor de la línea, los coeficientes de alargamiento de diferentes líneas, etcétera.

El archivo Mouse.mqh contiene una descripción de la clase para el movimiento del ratón. Ahí se guardan las coordenadas actuales del cursor, tanto en píxeles como en coordenadas de "precio-tiempo", el número de barra actual.

El archivo Utilites.mqh contiene funciones auxiliares. Precisamente ahí se calculan las barras de los extremos, los puntos de intersección de las líneas rectas y otras cosas útiles que no están directamente relacionadas con el dibujado, pero que lo determinan totalmente.

Y el archivo Graphics.mqh en realidad se ocupa de dibujar usando como base los datos de otros archivos: son precisamente sus funciones las que llama en general el archivo Shortcuts.mqh.

No siempre soy estricto en la delimitación de funciones. A veces, también podemos realizar los cálculos dentro de las funciones de dibujado. Hasta ahora, el método expuesto resulta muy cómodo de desarrollar y mantener para el autor. Quizás algún día corrija estas inexactitudes.

Esta implementación muestra cómo podemos usar el código de la biblioteca en un indicador.


Optimización del rendimiento de la biblioteca

En este caso, los cambios resultan mínimos.

¿Por qué seleccionamos en las versiones iniciales de la biblioteca un asesor experto para la implementación y no un indicador? La respuesta es simple: cada asesor trabaja en su propio hilo de ejecución e, idealmente, no afecta a otros asesores; por lo tanto, el terminal se ralentiza menos si tiene que procesar atajos de teclado en varios gráficos.

No obstante, el propósito del asesor es comerciar, y este producto programático no lo hace. Además, cuando hay un indicador en el gráfico, resulta mucho más sencillo colocar otro asesor experto. Por consiguiente, en algún momento se decidió que el indicador debería serlo, y surgió la cuestión del rendimiento. Esto es especialmente importante si el usuario tiene muchas ventanas abiertas. Si, por ejemplo, el usuario tiene 40 pestañas abiertas (no es ni mucho menos el tope), el simple procesamiento de las comprobaciones de la pulsación de las teclas se convertirá en un problema si estas pulsaciones procesan todos los gráficos a la vez.

Entonces, tuve una idea brillante: ¿y para qué, en realidad? Si solo funciona la ventana activa, las comprobaciones solo deberían realizarse en ella.

El código es muy simple.

/* Shortcuts.mqh */

void CShortcuts::OnChartEvent(
  const int id,
  const long &lparam,
  const double &dparam,
  const string &sparam
)
 {
 //...

  if(ChartGetInteger(0,CHART_BRING_TO_TOP)==false)
   {
    return;
   }
 
 //...

Al comienzo de la función, deberemos añadir una comprobación de que este gráfico está activo, es decir, de que se muestra en primer plano. Si no es el caso, no deberemos hacer nada.

En realidad, no medimos el rendimiento ni calculamos el porcentaje de aumento de la velocidad; no obstante, según las reseñas de aquellos que han descargado la biblioteca y que realmente han usado muchas pestañas, la aplicación se vuelve más receptiva, incluso en forma de indicador, lo cual necesitábamos en este caso.

Aquí también merece la pena tener en cuenta el propósito de la aplicación en sí.

En primer lugar, se destina a acciones muy episódicas (no se llama en cada tick) y, por consiguiente, consume recursos computacionales durante un tiempo muy limitado.

En segundo lugar, los gráficos en sí son una fuente de problemas relacionados con la velocidad si nuestra computadora no tiene suficiente potencia. Cuantos más objetos haya en el gráfico, más difícil será procesarlos; esto es obvio... No obstante, la biblioteca es gráfica, por ello, solo debemos soportar los costes derivados de dibujar y controlar cualitativamente dónde, qué y cuánto hemos dibujado.

Bueno, y en tercer lugar, la función más intensiva en recursos en mi caso, considero, es la función de búsqueda de extremos. Sin embargo, aún desconozco cómo implementarla más rápido de lo que está ahora. No soy un látigo... Así que, por ahora, creo que la implementación actual resulta óptima. Además, esta función no se llama con demasiada frecuencia cuando se dibujan líneas rectas y otras figuras útiles, por lo que, su carácter imperfecto también se puede pasar por alto.

Las demás funciones se llaman con mucha menos frecuencia y su rapidez es más que suficiente para las tareas de esta aplicación, por lo que podemos ignorarlas por completo.


Refactorización del código: gestión de la conectividad

La variante de código presentada en los artículos anteriores presupone que la aplicación sea monolítica y no se utilice de forma aislada. Por consiguiente, la configuración global se usaba directamente en el código, y algunas utilidades dependían de la clase del ratón.

Esto hacía posible escribir código un poco más rápido, pero resultaba completamente inconveniente desde el punto de vista de la reutilización. Si queremos conectar el anterior archivo de utilidad al completo a algún otro proyecto que no use un ratón o gráficos, aún deberemos incluir tanto el archivo de configuración global como el archivo de clase del ratón.

Esto no es correcto, ni tampoco cómodo.

Por eso he decidido introducir pequeñas modificaciones en el código. Todas las variables globales, por supuesto, han permanecido intactas. ¿Y qué otra cosa podría pasar, si se trata de configuraciones?

En las clases principales, sin embargo, hemos añadido campos privados que contienen copias de estas variables. Para guardar estos valores, deberemos añadir funciones "públicas" (de acceso público) especiales. Lo mismo se aplica a la lectura.

Tienen más o menos el aspecto siguiente:

private:
  /* Fields */
  //---
  static int          m_TrendLengthCoefficient;

public:
  /* Methods */
  //---
  static int          TrendLengthCoefficient(void) {return m_TrendLengthCoefficient;}
  //---
  static void         TrendLengthCoefficient(int _coefficient) {m_TrendLengthCoefficient=_coefficient;}

Dada la cantidad de configuraciones surgidas, parece largo y tedioso, y por lo general no resulta claro qué beneficios aporta.

No obstante, sí que existen ciertos beneficios. Primero, como ya hemos dicho, la clase se vuelve independiente de los archivos externos. Si alguien quiere usar esta clase, podrá configurar estas variables como quiera, y solo aquellas que realmente sean necesarias.

En segundo lugar, estas variables se pueden cambiar durante la ejecución. Por ejemplo, alguien quiere escribir una función que construya un abanico de líneas a partir de un solo punto. Cada línea es 2 veces más larga que la anterior y divergen en diferentes ángulos. ¿Cómo puedo hacer eso? Usando la clase CUtilites en la implementación actual, justo antes de cada dibujo, podremos establecer este parámetro descrito para el ejemplo, TrendLengthCoefficient, colocando los puntos de inicio en las mismas coordenadas y los puntos finales a lo largo de algún círculo de un radio aleatorio.

En tercer lugar, podemos agrupar como queramos los datos dentro de la clase. Podemos crear estructuras o incluso clases completas que guarden datos, por ejemplo, sobre rectángulos por separado, sobre líneas diagonales por separado, sobre niveles en su lugar... No obstante, desde el punto de vista del usuario final, la interfaz, es decir, la forma de acceder a estos datos, no cambiará de forma alguna.

Bueno, y en cuarto lugar, los datos no tienen que guardarse en la RAM en absoluto. A veces, las variables se pueden guardar en las variables globales del terminal, o en archivos, e incluso en una base de datos; a veces, simplemente se calculan sobre la marcha en función de otros parámetros. Como se muestra en el ejemplo anterior, si organizamos "correctamente" el acceso, el usuario final podrá reutilizar el código sin preocuparse por la implementación directa de las estructuras de datos. A pesar de la sobrecarga que supone la escritura de código innecesario, la llamada de funciones innecesarias y la inicialización de las variables requeridas cada vez que se crea una instancia, las ventajas siguen prevaleciendo.

Por lo tanto, en la versión actual de la biblioteca, todo lo logrado se ha reescrito en este estilo, y ahora podemos usar el archivo de utilidades "tal cual" en cualquier proyecto en el que queramos.

La clase de ratón originalmente encapsulaba todo por sí misma, así que no había necesidad de arreglarla. La clase de dibujado no tiene sentido sin utilidades, pero todas las configuraciones externas en ella se dan en la misma forma de campos.

Una vez más, resumiremos. Resulta que el ratón y las utilidades son clases completamente independientes que se pueden usar solas o en combinación. La clase de dibujado usa ambas, pero es independiente de otros archivos externos, salvo del despachador al que debe inicializar. Bien, la clase que contiene las descripciones de los atajos de teclado se encarga de gestionar el propio despachador que hace que todo el código funcione exactamente como debería. Así, la "cohesión" de las clases se debilita considerablemente, lo cual ofrece las ventajas mencionadas anteriormente.


Instrumento "Retícula"

En la versión anterior de la biblioteca, al dibujar una línea de tendencia, se dibujaba una retícula en su punto final para indicar los niveles de tiempo y precio. Para ello, usamos dos líneas simples: una vertical y otra horizontal. No obstante, para designar de esta manera un punto arbitrario del gráfico, teníamos que presionar dos teclas: H e I. A veces, esto resulta conveniente, pero, en otras ocasiones, apetece reducir el trabajo... Por consiguiente, surgió el instrumento "Retícula".

Esta herramienta funciona de forma semejante a todo lo que ya conocemos. Desplazamos el cursor al lugar deseado, presionamos la tecla X y obtenemos una retícula bonita y uniforme compuesta de líneas verticales y horizontales. A continuación, mostramos el código de la función.


/* Graphics.mqh */

//+------------------------------------------------------------------+
//| Draws a crosshair at specified coordinates. If the coordinates   |
//|   are not set, the mouse pointer coordinates are used.           |
//+------------------------------------------------------------------+
//| Parameters:                                                      |
//|   datetime _time - crosshair time                                |
//|   double _price - price level                                    |
//+------------------------------------------------------------------+
void CGraphics::DrawCross(datetime _time=-1,double _price=-1)
 {
  datetime time;
  double price;
//---
  if(_time==-1)
   {
    time=CMouse::Time();
   }
  else
   {
    time=_time;
   }

  if(_price==-1)
   {
    price=CMouse::Price();
   }
  else
   {
    price=NormalizeDouble(_price,Digits());
   }
  DrawSimple(OBJ_HLINE,time,price);
  DrawSimple(OBJ_VLINE,time,price);
  
 }

Para quienes están familiarizados con el código de la versión anterior, aquí no hay nada especial que comentar. Para empezar, simplemente se establecen las coordenadas. Si estas coordenadas se transmiten usando parámetros, entonces se utilizarán. Si los parámetros están configurados por defecto, se usarán las coordenadas del puntero del ratón.

Entonces solo se realizará el dibujado usando la función descrita en el segundo artículo.

Y para que esta función funcione exactamente como debería, deberemos llamarla desde el archivo Shortcuts.mqh cuando suceda un evento determinado, presionando para ello la tecla X.

/* GlobalVariables.mqh */
  
  // ...
  
  input string   Cross_Key="X";                       // Crosshair where the mouse was clicked
  
  // ...
  /* Shortcuts.mqh */
  
  void CShortcuts::OnChartEvent( /* ... */ )
    switch(id)
     {
       case CHARTEVENT_KEYDOWN:
       
       // ... 
       
       //--- Draw a crosshair
       if(CUtilites::GetCurrentOperationChar(Cross_Key) == lparam)
        {
         m_graphics.DrawCross();
        }
     }

Instrumento "Línea de tendencia por picos aleatorios"

La opción de crear una línea de tendencia por los extremos con un cierto número de barras a la izquierda y a la derecha resulta muy conveniente, pero a veces querríamos dibujar una línea por picos aleatorios. Para este caso, hemos añadido otra característica: el comando Q.

En la siguiente animación se muestra un ejemplo del funcionamiento de este comando.

Ejemplo de dibujado de una línea por picos aleatorios

Debido a la naturaleza de mi programa de captura de imágenes, teníamos que clicar en el gráfico cada vez antes de dibujar. En condiciones reales, basta con activar el gráfico y luego dibujar tantas líneas como sea necesario.

El dibujo se realiza en dos etapas. La primera consiste en presionar la tecla Q. Esta activa el modo de dibujado de líneas aleatorias y marca el primer punto para que quede claro que el comando se ha activado.

Si usted no se refería a este pico (señalado con un marcador), podrá presionar Q nuevamente, lo cual cambiará de nuevo el modo y cancelará el dibujado. (Algún día cambiaré este comportamiento configurando la tecla Esc para cancelar, aunque a mí personalmente ahora me conveniene así).

Si el primer pico se ha elegido correctamente, entonces el siguiente punto se seleccionará con un simple clic cerca del segundo extremo. En este caso, si todo ha funcionado, el marcador ya no será necesario (por consiguiente, será eliminado), y se dibujará la línea de tendencia real.

Los parámetros de la línea "aleatoria" son independientes del modo "T", por lo que podemos configurar, por ejemplo, que "T" dibuje una línea en negrita de cuatro píxeles de grosor, cuatro veces más larga que el espacio entre los picos, mientras que "Q" dibuje una delgada y solo el doble de largo...

Como es habitual, el código se divide en varios archivos.

Comenzaremos por el final, es decir, desde el momento en que se procesa el evento CHARTEVENT_KEYDOWN:

/* Shortcuts.mqh */

void CShortcuts::OnChartEvent(
  const int id,
  const long &lparam,
  const double &dparam,
  const string &sparam
)
 {
   //...
   
   switch(id)
   {
   
   //...
   
     case CHARTEVENT_KEYDOWN:
      if(CUtilites::GetCurrentOperationChar(Free_Line_Key) == lparam)
       {
        m_graphics.ToggleFreeLineMode();
        if(m_graphics.IsFreeLineMode()){
          m_graphics.DrawFreeLine(CMouse::Bar(),CMouse::Above());
        }
       } 
    
    //...

Si el programa detecta que se ha presionado la tecla Q (la letra se almacena en la variable externa Free_Line_Key), cambiará el modo de dibujado. Y si después de cambiar el modo resulta que el modo está activado, se dará el comando para ejecutar la función de dibujado de líneas.

El clic se procesa en el evento

/* Shortcuts.mqh */

        //...
        
    case CHARTEVENT_CLICK:
        ChartClick_Handler();
      break;
      
      //...
      
}

//+------------------------------------------------------------------+
//| Processing a click on a free chart field                         |
//+------------------------------------------------------------------+
void CShortcuts::ChartClick_Handler()
 {
  
//---
  if(m_graphics.IsFreeLineMode()){
    m_graphics.DrawFreeLine(
      CMouse::Bar(),CMouse::Above()
    );
  }
  
 }

Una vez más, conviene prestar atención al hecho de que, después de presionar una tecla, el modo de dibujado cambia inmediatamente antes de comenzar cualquier dibujado (el nombre de mi función comienza con la palabra Toggle - alternar), y el estado permanecerá hasta que se cambie nuevamente usando la tecla o después de trazar la línea. Al clicar, primero se verifica si es necesario dibujar, luego, si fuera necesario, dibujaremos, y solo entonces el modo cambiará a "neutro".

Hemos trasladado la función ChartClick_Handler por separado, porque en el futuro planeamos crear varios modos más que requieren clics en el gráfico. Por ejemplo, el modo de eliminación de objetos complejos, como las retículas o los niveles verticales, descritos en el artículo anterior, a veces pueden requerir un clic en el gráfico para cancelar el menú. Por ahora, creo que si representamos las funciones de clic por separado, se simplificará el desarrollo posterior. Pero esto es por ahora: lo demás está por venir.

Volviendo al presente, vamos a continuar analizando cómo funciona el dibujado.

/* Graphics.mqh */


//+------------------------------------------------------------------+
//|  Draws a line by arbitrary specified extrema. In the current     |
//|    implementation, the first extremum is set by a hot key        |
//|    (Q by default), the second is set by clicking near the        |
//|    required top                                                  |
//+------------------------------------------------------------------+
//|  Parameters:                                                     |
//|    int _bar - bar to start search at                             |
//|    bool _isUp - top or bottom?                                   |
//|    int _fractalSizeRight - number of bars to the right of extr   |
//|    int _fractalSizeLeft -  number of bars to the left of extremum|
//+------------------------------------------------------------------+
void CGraphics::DrawFreeLine(
  int _bar,
  bool _isUp,
  int _fractalSizeRight=1,
  int _fractalSizeLeft=1
)
 {
//--- Variables
  double    selectedPrice,countedPrice,trendPrice1,trendPrice2;
  datetime  selectedTime,countedTime,trendTime1,trendTime2;
  int       selectedBar,countedBar;
  int       bar1,bar2;

  string trendName="",trendDescription="p2;";
  int fractalForFirstSearch = MathMax(_fractalSizeRight,_fractalSizeLeft)* 2;

//--- Search for a bar that meets the extremum criteria
  selectedBar = CUtilites::GetNearesExtremumSearchAround(
    _bar,
    _isUp,
    _fractalSizeLeft,
    _fractalSizeRight
  );

//--- Building the starting marker
  if(0==m_Clicks_Count)
   {
    m_Clicks_Count=1;
    if(_isUp)
     {
      m_First_Point_Price=iHigh(NULL,PERIOD_CURRENT,selectedBar);
     }
    else
     {
      m_First_Point_Price=iLow(NULL,PERIOD_CURRENT,selectedBar);
     }
    m_First_Point_Time=iTime(NULL,PERIOD_CURRENT,selectedBar);
    //---
    m_First_Point_Time=CUtilites::DeepPointSearch(
                         m_First_Point_Time,
                         _isUp,
                         ENUM_TIMEFRAMES(Period())
                       );
    //---
    DrawFirstPointMarker(_isUp);
   
   }
//--- Processing a click on the chart
  else
   {
    ObjectDelete(0,m_First_Point_Marker_Name);
    if(_isUp)
     {
      countedPrice=iHigh(NULL,PERIOD_CURRENT,selectedBar);
     }
    else
     {
      countedPrice=iLow(NULL,PERIOD_CURRENT,selectedBar);
     }
    countedTime=iTime(NULL,PERIOD_CURRENT,selectedBar);
    //--- Move a point in time on smaller timeframes
    countedTime=CUtilites::DeepPointSearch(countedTime,_isUp,ENUM_TIMEFRAMES(Period()));

    //--- The line is always drawn from left to right. 
    //--- If it is not convenient, you can comment this part
    //---   up to the next comment
    if(countedTime<m_First_Point_Time)
     {
      trendTime1=countedTime;
      trendPrice1=countedPrice;
      trendTime2=m_First_Point_Time;
      trendPrice2=m_First_Point_Price;
     }
    else
     {
      trendTime2=countedTime;
      trendPrice2=countedPrice;
      trendTime1=m_First_Point_Time;
      trendPrice1=m_First_Point_Price;
     }
    //--- Set the description for future correction
    trendDescription+=TimeToString(trendTime2)+";"+DoubleToString(trendPrice2,Digits());

    //selectedPrice=CUtilites::EquationDirect(
    //                trendTime1,trendPrice1,trendTime2,trendPrice2,selectedTime
    //              );
    trendName=CUtilites::GetCurrentObjectName(allPrefixes[0],OBJ_TREND);
    
    TrendCreate(
      0,                    // Chart ID
      trendName,            // Line name
      0,                    // Subwindow number
      trendTime1,           // time of the first point
      trendPrice1,          // price of the first point
      trendTime2,           // time of the second point
      trendPrice2,          // price of the second point
      CUtilites::GetTimeFrameColor(
        CUtilites::GetAllLowerTimeframes()
      ),                    // line color
      Trend_Line_Style,     // line style
      Trend_Line_Width,     // line width
      false,                // background object
      true,                 // is the line selected
      true                  // ray to the right
    );
    
    bar1=iBarShift(NULL,0,trendTime1);
    bar2=iBarShift(NULL,0,trendTime2);
    selectedTime = CUtilites::GetTimeInFuture(
                     //iTime(NULL,PERIOD_CURRENT,0),
                     trendTime1,
                     (int)((bar1-bar2)*m_Free_Trend_Length_Coefficient),
                     COUNT_IN_BARS
                   );
    selectedPrice= ObjectGetValueByTime(0,trendName,selectedTime);
    ObjectSetInteger(0,trendName,OBJPROP_RAY,IsRay());
    ObjectSetInteger(0,trendName,OBJPROP_RAY_RIGHT,IsRay());
    ObjectMove(0,trendName,1,selectedTime,selectedPrice);
    //---
    m_Clicks_Count=0;
    ToggleFreeLineMode();
   }

  ObjectSetString(0,trendName,OBJPROP_TEXT,trendDescription);
  ChartRedraw();
 }

La función ha resultado ser voluminosa y, probablemente, en futuras refactorizaciones, se dividirá en varias más pequeñas. Por ahora, esperamos que la codificación de los colores y los comentarios en el código ayuden al lector a entender cómo funciona todo.

En esta implementación, la función comprueba ambas señales: el evento que indica el inicio del dibujado del primer punto y el mensaje sobre la búsqueda del segundo punto y el inicio del dibujado. Para distinguir de alguna manera estos eventos, hemos introducido la variable m_Clicks_Count. Creo que por la letra "m_" al inicio, queda claro que esta variable es global para esta clase, y su vida útil es igual a la vida útil de una instancia del objeto.

Si la llamada a la función es la primera (es decir, hemos presionado una tecla), necesitaremos encontrar el primer punto y dibujar un marcador.

Si la llamada es la segunda en una serie, necesitaremos borrar el marcador, encontrar el segundo punto y dibujar una línea. Estos son los cinco bloques principales; todos los demás, en general, son necesarios para su implementación.

En la implementación actual, la propia línea recta se usa para determinar el precio en el futuro. En general, esto no resulta una buena idea, porque, a la hora de dibujar, el terminal tiene que dibujar primero el rayo, y luego mover el final de la línea al punto deseado y nuevamente decidir si dibujar el rayo (dependiendo de los ajustes externos). Normalmente hago los cálculos preliminares usando la famosa función de Igor Kim (Kim IV), que también he incluido en la biblioteca. En la parte rosa del código, podemos ver la llamada comentada de esta función. No obstante, en este caso, si calculamos los puntos exactamente según el tiempo, podríamos obtener un error relacionado con el fin de semana, que queremos evitar. Es cierto que podría evitarse fácilmente calculando la línea recta según los números de barra y luego volviendo a calcular los números en fechas reales, pero lo que hemos implementado ahora me parece más claro.

Entonces, en el código marcado en rosa, ya hemos encontrado los extremos básicos, y queda por trazar una línea. Para hacerlo, primero dibujamos una línea entre los dos puntos extremos "básicos", activando al mismo tiempo la propiedad "rayo" para que la línea se dibuje en el futuro (la función TrendCreate al comienzo de este bloque).

El tiempo requerido en el futuro lo calcularemos partiendo de la configuración:

selectedTime = CUtilites::GetTimeInFuture(
                     //iTime(NULL,PERIOD_CURRENT,0),
                     trendTime1,
                     (int)((bar1-bar2)*m_Free_Trend_Length_Coefficient),
                     COUNT_IN_BARS
                   );

Y luego, usando la función estándar, obtendremos el precio necesario.

selectedPrice= ObjectGetValueByTime(0,trendName,selectedTime);

Después de eso, solo queda trasladar el segundo punto de la línea a las coordenadas deseadas y establecer la propiedad real del rayo (recordemos que esta propiedad se activa por defecto con la tecla R, de la palabra Ray).

Después de trazar la línea, deberemos desactivar el estado de espera del clic, que es lo que hacen las líneas. 

    m_Clicks_Count=0;
    ToggleFreeLineMode();

El resto del código, en otros bloques de esta función, es un poco más complicado. El caso es que he añadido un par de trucos relacionados con la usabilidad de las líneas rectas.

La primera característica está relacionada con el efecto del desplazamiento de las líneas a marcos temporales menores. Si dibujamos líneas de la forma habitual, al alternar entre "ventanas temporales" se dibujará algo similar a lo que vemos en las siguientes imágenes:

Extremos de las líneas D1 Extremos de las líneas H4

Podemos ver que el borde izquierdo de la línea, que coincide exactamente con el extremo en el periodo D1, se desplaza hacia la izquierda en el periodo de 4 horas y no coincide con el extremo. Este es un efecto bastante obvio, debido a que el extremo del día no tiene por qué caer al comienzo. Y si desea mayor precisión, al dibujar manualmente, podrá dibujar una línea de forma aproximada, luego "descender" a las mitades inferiores y corregir los extremos.

Eso resulta bueno para un gráfico o dos... ¿Y si son 20? ¿O 100? Esto resultaría muy molesto...

Y, como ya existe una función de dibujado automático, ¿por qué no confiarle esta tarea a la hora de crear cada objeto?

Estos son los pensamientos que me llevaron a crear la función DeepPointSearch.


Función de "vinculación profunda" DeepPointSearch

En la función encargada de dibujar una línea "libre", esta función se llama dos veces, una para cada punto. Se encuentra en el archivo de utilidades y tiene el siguiente código:

//+------------------------------------------------------------------+
//| Search for a given point on lower timeframes                     |
//+------------------------------------------------------------------+
//| Parameters:                                                      |
//|   datetime _neededTime - start time on a higher timeframe        |
//|   bool _isUp - search by highs or by lows                        |
//|   ENUM_TIMEFRAMES _higher_TF - the highest period                |
//+------------------------------------------------------------------+
//| Return value:                                                    |
//|   More accurate date (on the lowest possible timeframe)          |
//+------------------------------------------------------------------+
datetime CUtilites::DeepPointSearch(
  datetime _neededTime,
  bool _isUp,
  ENUM_TIMEFRAMES _higher_TF=PERIOD_CURRENT
)
 {
//---
  //--- As a result it gets the most accurate time available
  datetime deepTime=0;
  //--- current timeframe
  ENUM_TIMEFRAMES currentTF;
  //--- The number of the highest timeframe in the list of all available periods
  int highTFIndex = GetTimeFrameIndexByPeriod(_higher_TF); 
  //--- The higher period in seconds
  int highTFSeconds = PeriodSeconds(_higher_TF);
  //--- Current interval in seconds
  int currentTFSeconds;
  //--- Counter
  int i;
  //--- Bar number on a higher timeframe
  int highBar=iBarShift(NULL,_higher_TF,_neededTime);
  //--- Bar number on the current timeframe
  int currentBar;
  //--- The total number of bars on the current timeframe
  int tfBarsCount;
  //--- How many bars of a lower TF fit into one bar of a higher TF
  int lowerBarsInHigherPeriod;
  //--- Maximum allowed number of bars in the terminal
  int terminalMaxBars = TerminalInfoInteger(TERMINAL_MAXBARS);

//--- Loop sequentially through all timeframes
  for(i=0; i<highTFIndex; i++)
   {
    //--- Get a timeframe by a number in the list
    currentTF=GetTimeFrameByIndex(i);
//--- Check if this timeframe has the required time.
    tfBarsCount=iBars(NULL,currentTF);
    if(tfBarsCount>terminalMaxBars-1)
     {
      tfBarsCount=terminalMaxBars-1;
     }
    deepTime=iTime(NULL,currentTF,tfBarsCount-1);
//--- If it has, find it.
    if(deepTime>0 && deepTime<_neededTime)
     {
      currentTFSeconds=PeriodSeconds(currentTF);
      
      //--- Search for the required bar only within the higher TF candlestick
      lowerBarsInHigherPeriod=highTFSeconds/currentTFSeconds;
      currentBar = iBarShift(NULL,currentTF,_neededTime);
      
      if(_isUp)
       {
        currentBar = iHighest(
                       NULL,currentTF,MODE_HIGH,
                       lowerBarsInHigherPeriod+1,
                       currentBar-lowerBarsInHigherPeriod+1
                     );

       }
      else
       {
        currentBar = iLowest(
                       NULL,currentTF,MODE_LOW,
                       lowerBarsInHigherPeriod+1,
                       currentBar-lowerBarsInHigherPeriod+1
                     );
       }
      deepTime=iTime(NULL,currentTF,currentBar);
      //--- Once the required time is found, stop the search
      break;
     }
   }
//--- If reached the end of the loop
  if(i==highTFIndex)
   {
    //--- then the required time is only available on the higher timeframe.
    deepTime=_neededTime;
   }
//---
  return (deepTime);
 }

Para mí, el mayor desafío fue averiguar cómo debería funcionar el fragmento principal de la búsqueda. Está claro que primero debemos decidir si, en general, el tiempo que necesitamos se encuentra en la historia. Después de todo, es bien sabido que los marcos inferiores confrecuencia no contienen lo que hay en los más antiguos. La función estándar iBars calcula el número de barras en la historia. No obstante, esto no resulta suficiente, ya que el terminal solo puede mostrar un número limitado de ellas. Encontraremos el número de barras que puede mostrar el terminal al principio, usando el código

//--- Maximum allowed number of bars in the terminal
  int terminalMaxBars = TerminalInfoInteger(TERMINAL_MAXBARS);

Si hay demasiadas barras en la historia, nos limitaremos a las representadas.

Luego, usando la función iTime, determinamos el tiempo de la última barra del historial. Si su fecha y hora son superiores a lo que necesitamos, no tendrá sentido buscar más, ya que la fecha máxima disponible para nosotros será posterior, por lo que simplemente pasaremos al marco superior. Si la última vela disponible en el terminal es anterior a la que necesitamos, entonces todo estará en orden y, muy probablemente, hayamos encontrado el lugar más profundo donde este punto todavía tiene sentido.

Si se superan todas las comprobaciones, comenzará la rutina. El punto que necesitamos, por definición, será el más extremo dentro del rango de la vela del intervalo de tiempo más alto. Solo queda comprender cuántas velas vamos a analizar. Después de ello, las funciones estándar nos ayudarán a determinar el más extremo entre los extremos, después de lo cual, podremos calcular el tiempo y completar el trabajo.

En la implementación actual de la biblioteca, esta función se aplica solo a las líneas rectas llamadas por las teclas T y Q, pero en la próxima versión esta función estará disponible para todos los instrumentos, y planeo hacerla personalizable para cada instrumento por separado.


Corrección del tiempo

La segunda "característica" de esta implementación es la corrección de las líneas rectas en el tiempo. La siguiente animación explica el problema.

Veamos cómo se encoge el último rectángulo. En este caso, el final de la línea recta, que se encontraba a una distancia de poco más de un día del rectángulo del medio, de repente resultó estar cerca de él. Naturalmente, en este caso, los puntos de arriba también se desplazaron (observe el comportamiento de la línea recta cerca de la parte superior). Podemos ver cómo aparecen nuevas rupturas cuando la línea recta se contrae, lo cual puede afectar a la estrategia comercial.

Y está bien en FÓREX, donde esos saltos pueden ocurrir una vez a la semana. No obstante, también existe el mercado de valores, donde pueden darse brechas en el tiempo todos los días dependiendo de la bolsa, y a menudo esto también sucede durante el día.

¡Es genial que exista la automatización!

Para que todo funcione, deberemos guardar de alguna forma las coordenadas "correctas" y luego corregirlas según sea necesario.

Elegí la descripción de una línea recta para guardar las coordenadas, ya que la mayoría de los tráders aún no usan descripciones al crear objetos automáticos. Las alternativas son archivos con una lista de variables directas del terminal, o de variables globales, si no hay demasiadas directas.

/* Graphics.mqh */

void CGraphics::DrawFreeLine(//...)
 {

//...
  string trendDescription="p2;";

//...
  trendDescription+=TimeToString(trendTime2)+";"+DoubleToString(trendPrice2,Digits());
  
//...
  ObjectSetString(0,trendName,OBJPROP_TEXT,trendDescription);

Y luego, pues el mismo truco con las coordenadas tomadas en la línea "física" que escribimos antes. En mi opinión, todo es bonito y trivial, y los comentarios en el código de ejemplo deberían ayudar al lector a comprender todo lo que sucede en la función.

/* Utilites.mqh */

//+------------------------------------------------------------------+
// | Adjusts the position of line end in the future in case of price |
//|   gaps                                                           |
//+------------------------------------------------------------------+
//| Parameters:                                                      |
//|   string _line_name - the name of the line to be corrected       |
//+------------------------------------------------------------------+
void CUtilites::CorrectTrendFutureEnd(string _line_name)
 {
//---
  if(ObjectFind(0,_line_name)<0)
   {
    PrintDebugMessage(__FUNCTION__+" _line_name="+_line_name+": Object does not exist");
    //--- If there is no object to search, there is nothing more to do.
    return;
   }
  //--- Get a description
  string line_text=ObjectGetString(0,_line_name,OBJPROP_TEXT);
  
  string point_components[]; // array for point description fragments
  string name_components[];  // array containing line name fragments
  string helpful_name="Helpful line"; // the name of the auxiliary line
  string vertical_name=""; // the name of the corresponding vertical from the crosshair
  
  //--- Get the point time and price in string form
  int point_components_count=StringSplit(line_text,StringGetCharacter(";",0),point_components);
  
  datetime time_of_base_point; // time of the basic point
  datetime time_first_point,time_second_point; // the time of the first and the second point
  datetime time_far_ideal; // estimated time in the future
  double price_of_base_point; // the price of the basic point
  double price_first_point,price_second_point; // the prices of the first and the second point
  int i; // counter

//--- Check if the line is needed
  if(line_text=="" || point_components_count<3 || point_components[0]!="p2")
   {
    PrintDebugMessage(__FUNCTION__+" Error: the line cannot be used");
    return;
   }
//--- Get the coordinates of the "basic" point from the line description
  time_of_base_point=StringToTime(point_components[1]);
  price_of_base_point=StringToDouble(point_components[2]);
  if(time_of_base_point==0 || price_of_base_point==0)
   {
    PrintDebugMessage(__FUNCTION__+" Error: Unusable description");
    return;
   }
//--- Get the real coordinates of the line
  time_first_point = (datetime)ObjectGetInteger(0,_line_name,OBJPROP_TIME,0);
  time_second_point = (datetime)ObjectGetInteger(0,_line_name,OBJPROP_TIME,1);
  price_first_point = ObjectGetDouble(0,_line_name,OBJPROP_PRICE,0);
  price_second_point = ObjectGetDouble(0,_line_name,OBJPROP_PRICE,1);

//--- Create an auxiliary line (from the starting point to the base one)
  MakeHelpfulLine(
    time_first_point,
    price_first_point,
    time_of_base_point,
    price_of_base_point
  );

//--- Calculate the correct time for the current situation
  time_far_ideal=ObjectGetTimeByValue(0,helpful_name,price_second_point);
//---
  if(time_second_point != time_far_ideal)
   {
    //--- move the free end of the trend line
    ObjectMove(0,_line_name,1,time_far_ideal,price_second_point);
    //--- and the corresponding vertical
    StringSplit(_line_name,StringGetCharacter("_",0),name_components);
    for(i=0; i<ObjectsTotal(0,-1,OBJ_VLINE); i++)
     {
      vertical_name = ObjectName(0,i,-1,OBJ_VLINE);
      if(name_components[0]==StringSubstr(vertical_name,0,StringFind(vertical_name,"_",0)))
       {
        if((datetime)ObjectGetInteger(0,vertical_name,OBJPROP_TIME,0)==time_second_point)
         {
          ObjectMove(0,vertical_name,0,time_far_ideal,price_second_point);
          break;
         }
       }
     }
   }
  // Delete the auxiliary line
  RemoveHelpfulLine();
 }

Para que todo esto funcione, deberemos llamar este código a intervalos regulares. Hemos elegido el comienzo de cada nueva hora.

/* Shortcuts.mq5 */

int OnCalculate(/*...*/)
 {
   //...
   if(CUtilites::IsNewBar(First_Start_True,PERIOD_H1))
   {
    for(i=0; i<all_lines_count; i++)
     {
      line_name=ObjectName(0,i,-1,OBJ_TREND);
      CUtilites::CorrectTrendFutureEnd(line_name);
      ChartRedraw();
     }
   }
   //...
 }


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
 Dibujar una línea vertical simple
 I(i) [Only visual  vertical]
 Dibujar una línea horizontal simple
 H  Horizontal
 Dibujar un conjunto de tridentes de Andrews
 P  Pitchfork
 Dibujar un abanico de Fibonacci (VFun)
 F key  Fun
 Dibujar un nivel horizontal corto
 S  Short
 Dibujar un nivel horizontal largo
 L key  Long
 Dibujar una línea vertical con las marcas de los niveles
 V  Vertical
 Dibujar una retícula
 X  [Only visual  cross]
 Dibujar una línea por picos aleatorios
 Q  [No conformity... "L" and "T" is not free]
 Dibujar un grupo de rectángulos
 B  Box


Conclusión

Esperamos que el material expuesto le haya resultado de utilidad. Sea como fuere, se aceptan críticas constructivas en los comentarios.

En un futuro próximo, esperamos implementar la posibilidad de dibujar líneas no solo por picos estrictos, sino también por tangentes.

Asimismo, esperamos prestar atención a los canales. Por el momento, solo planeamos dedicar tiempo a los canales equidistantes, pero si alguien en los comentarios o en un mensaje personal expresa el deseo de dibujar algo más que se corresponda con los principios de la biblioteca, no habrá problema en considerar sus propuestas.

A largo plazo, querríamos guardar la configuración en un archivo (en lugar de la configuración del indicador, o junto con ella, usando variables de entrada) y añadir una interfaz gráfica para modificar la configuración "sobre la marcha".


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

Archivos adjuntos |
Mejorando el reconocimiento de patrones de velas usando Doji como ejemplo Mejorando el reconocimiento de patrones de velas usando Doji como ejemplo
Cómo encontrar patrones de velas con mayor frecuencia de la habitual. Tras la simplicidad de los patrones de velas también se oculta una importante desventaja que, precisamente, podemos eliminar utilizando las capacidades ampliadas de los recursos modernos de auotmatización del trading.
Recetas MQL5 – Calendario económico Recetas MQL5 – Calendario económico
El artículo está dedicado a las posibilidades programáticas del trabajo con el Calendario Económico. Para ello, crearemos una clase para acceder de forma simplificada a las propiedades del calendario y recibir eventos. Como ejemplo práctico, proponemos programar un indicador que usa datos sobre el volumen neto de las posiciones especulativas de CFTC.
¿Cómo elegir correctamente un asesor en el Mercado? ¿Cómo elegir correctamente un asesor en el Mercado?
En este artículo, analiceremos los puntos a los que debemos prestar atención en primer lugar a la hora de comprar un asesor. También buscaremos formas de aumentar los beneficios y, lo que es más importante, de gastar el dinero de forma inteligente y seguir ganando con ello. Además, tras finalizar la lectura, comprenderá que puede ganar dinero incluso con productos simples y gratuitos.
Gráficos en la biblioteca DoEasy (Parte 88): Colección de objetos gráficos - matriz dinámica bidimensional para almacenar propiedades de objetos que cambian dinámicamente Gráficos en la biblioteca DoEasy (Parte 88): Colección de objetos gráficos - matriz dinámica bidimensional para almacenar propiedades de objetos que cambian dinámicamente
En este artículo, crearemos una clase de matriz multidimensional dinámica con capacidad de cambiar la cantidad de datos en cualquier dimensión. Basándonos en la clase creada, crearemos una matriz dinámica bidimensional para guardar algunas propiedades de objetos gráficos que cambian dinámicamente.