Conjunto de instrumentos para el marcado manual de gráficos y comercio (Parte III). Optimización y nuevos instrumentos
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.
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:
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
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso