English Русский 中文 Deutsch 日本語
preview
Automatización de estrategias de trading en MQL5 (Parte 25): Sistema de líneas de tendencia con ajuste por mínimos cuadrados y generación dinámica de señales

Automatización de estrategias de trading en MQL5 (Parte 25): Sistema de líneas de tendencia con ajuste por mínimos cuadrados y generación dinámica de señales

MetaTrader 5 — Trading |
16 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Introducción

En nuestro artículo anterior (Parte 24), desarrollamos un sistema London Session Breakout en MetaQuotes Language 5 (MQL5) que utilizaba los rangos previos a la sesión de Londres para colocar órdenes pendientes con gestión de riesgos y stops dinámicos, lo que permitía operar de forma eficaz en función de las sesiones. En la parte 25, creamos un programa de trading basado en líneas de tendencia que utiliza un ajuste por mínimos cuadrados para detectar líneas de tendencia de soporte y resistencia, generando señales automáticas de compra y venta cuando los precios tocan estas líneas, complementadas con elementos visuales como flechas y parámetros de trading personalizables. Trataremos los siguientes temas:

  1. Diseño del marco de trading con líneas de tendencia
  2. Implementación en MQL5
  3. Backtesting
  4. Conclusión

Al final, tendrás una potente estrategia MQL5 para operar en función de las tendencias, lista para personalizar. ¡Empecemos!


Diseño del marco de trading con líneas de tendencia

El trading con líneas de tendencia es una estrategia que utiliza líneas diagonales trazadas en los gráficos de precios para conectar los máximos de oscilación (resistencia) o los mínimos de oscilación (soporte), lo que ayuda a los operadores a identificar la tendencia predominante. Los operadores compran cerca de las líneas de tendencia ascendentes (soporte) en una tendencia alcista o venden cerca de las líneas de tendencia descendentes (resistencia) en una tendencia bajista, esperando que el precio rebote. Una ruptura de la línea de tendencia suele indicar un posible cambio de tendencia o un debilitamiento de la misma, lo que lleva a los operadores a cerrar o invertir sus posiciones. Aquí se muestra un ejemplo de una línea de tendencia bajista.

LÍNEA DE TENDENCIA A LA BAJA

Ahora desarrollaremos un programa de trading basado en líneas de tendencia para automatizar las operaciones mediante la detección de líneas de tendencia de soporte y resistencia utilizando un método de ajuste por mínimos cuadrados, lo que permitirá obtener señales precisas de compra y venta cuando los precios toquen estas líneas.

Por si te interesa saberlo, el método de mínimos cuadrados es una técnica estadística que se utiliza para determinar la recta (o curva) que mejor se ajusta a un conjunto de puntos de datos, minimizando la suma de los cuadrados de las desviaciones verticales (errores) entre los puntos de datos y la recta ajustada. Será importante para nosotros porque proporcionará la aproximación lineal más precisa de la relación entre los puntos de giro (swing), lo cual será esencial para la predicción, el análisis de tendencias y el modelado de datos en nuestra disciplina. A continuación, puedes consultar la lógica estadística.

MÉTODO DE AJUSTE DE MÍNIMOS CUADRADOS

Tenemos previsto combinar la detección matemática de líneas de tendencia con retroalimentación visual y parámetros de negociación configurables, lo que nos permitirá aprovechar de forma eficiente los rebotes de tendencia en mercados dinámicos. Nuestro objetivo es identificar puntos de giro (swing), ajustar líneas de tendencia con suficientes toques (un mínimo de 3 toques), validar su integridad y activar operaciones con gestión de riesgos, todo ello mientras mostramos las líneas de tendencia y los puntos de toque en el gráfico para mayor claridad. Veamos el resultado que buscamos y luego podremos proceder a la implementación.

MARCO DE TENDENCIA


Implementación en MQL5

Para crear el programa en MQL5, abre el MetaEditor, ve al Navegador, busca la carpeta «Indicators», haz clic en la pestaña «Nuevo» y sigue las instrucciones para crear el archivo. Una vez creado, en el entorno de programación, empezaremos por declarar algunas entradas y estructuras que harán que el programa sea más dinámico.
//+------------------------------------------------------------------+
//|                                       a. Trendline Trader EA.mq5 |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright   "Copyright 2025, Allan Munene Mutiiria."
#property link        "https://t.me/Forex_Algo_Trader"
#property description "Trendline Trader using mean Least Squares Fit"
#property version     "1.00"
#property strict

#include <Trade\Trade.mqh>                         //--- Include Trade library for trading operations
CTrade obj_Trade;                                  //--- Instantiate trade object

//+------------------------------------------------------------------+
//| Swing point structure                                            |
//+------------------------------------------------------------------+
struct Swing {                                     //--- Define swing point structure
   datetime time;                                  //--- Store swing time
   double   price;                                 //--- Store swing price
};

//+------------------------------------------------------------------+
//| Starting point structure                                         |
//+------------------------------------------------------------------+
struct StartingPoint {                             //--- Define starting point structure
   datetime time;                                  //--- Store starting point time
   double   price;                                 //--- Store starting point price
   bool     is_support;                            //--- Indicate support/resistance flag
};

//+------------------------------------------------------------------+
//| Trendline storage structure                                      |
//+------------------------------------------------------------------+
struct TrendlineInfo {                             //--- Define trendline info structure
   string   name;                                  //--- Store trendline name
   datetime start_time;                            //--- Store start time
   datetime end_time;                              //--- Store end time
   double   start_price;                           //--- Store start price
   double   end_price;                             //--- Store end price
   double   slope;                                 //--- Store slope
   bool     is_support;                            //--- Indicate support/resistance flag
   int      touch_count;                           //--- Store number of touches
   datetime creation_time;                         //--- Store creation time
   int      touch_indices[];                       //--- Store touch indices array
   bool     is_signaled;                           //--- Indicate signal flag
};

//+------------------------------------------------------------------+
//| Forward declarations                                             |
//+------------------------------------------------------------------+
void DetectSwings();                               //--- Declare swing detection function
void SortSwings(Swing &swings[], int count);       //--- Declare swing sorting function
double CalculateAngle(datetime time1, double price1, datetime time2, double price2); //--- Declare angle calculation function
bool ValidateTrendline(bool isSupport, datetime start_time, datetime ref_time, double ref_price, double slope, double tolerance_pen); //--- Declare trendline validation function
void FindAndDrawTrendlines(bool isSupport);        //--- Declare trendline finding/drawing function
void UpdateTrendlines();                           //--- Declare trendline update function
void RemoveTrendlineFromStorage(int index);        //--- Declare trendline removal function
bool IsStartingPointUsed(datetime time, double price, bool is_support); //--- Declare starting point usage check function
void LeastSquaresFit(const datetime &times[], const double &prices[], int n, double &slope, double &intercept); //--- Declare least squares fit function

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input int    LookbackBars = 200;                   // Set bars for swing detection lookback
input double TouchTolerance = 10.0;                // Set tolerance for touch points (points)
input int    MinTouches = 3;                       // Set minimum touch points for valid trendline
input double PenetrationTolerance = 5.0;           // Set allowance for bar penetration (points)
input int    ExtensionBars = 100;                  // Set bars to extend trendline right
input int    MinBarSpacing = 10;                   // Set minimum bar spacing between touches
input double inpLot = 0.01;                        // Set lot size
input double inpSLPoints = 100.0;                  // Set stop loss (points)
input double inpRRRatio = 1.1;                     // Set risk:reward ratio
input double MinAngle = 1.0;                       // Set minimum inclination angle (degrees)
input double MaxAngle = 89.0;                      // Set maximum inclination angle (degrees)
input bool   DeleteExpiredObjects = false;         // Enable deletion of expired/broken objects
input bool   EnableTradingSignals = true;          // Enable buy/sell signals and trades
input bool   DrawTouchArrows = true;               // Enable drawing arrows at touch points
input bool   DrawLabels = true;                    // Enable drawing trendline/point labels
input color  SupportLineColor = clrGreen;          // Set color for support trendlines
input color  ResistanceLineColor = clrRed;         // Set color for resistance trendlines

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
Swing swingLows[];                                 //--- Store swing lows
int numLows = 0;                                   //--- Track number of swing lows
Swing swingHighs[];                                //--- Store swing highs
int numHighs = 0;                                  //--- Track number of swing highs
TrendlineInfo trendlines[];                        //--- Store trendlines
int numTrendlines = 0;                             //--- Track number of trendlines
StartingPoint startingPoints[];                    //--- Store used starting points
int numStartingPoints = 0;                         //--- Track number of starting points

Empezamos configurando los componentes básicos del programa para automatizar las operaciones en función de los toques en las líneas de tendencia. En primer lugar, incluimos la biblioteca «<Trade\Trade.mqh>» y creamos una instancia de «obj_Trade» como objeto «CTrade» para gestionar operaciones de trading, como la ejecución de órdenes de compra y venta. A continuación, procedemos a definir tres estructuras: «Swing», con «time» (datetime) y «price» (double) para registrar los puntos de oscilación; «StartingPoint», con «time» (datetime), «price» (double) y «is_support» (bool) para realizar un seguimiento de los puntos de partida utilizados como soporte o resistencia; y «TrendlineInfo», con «name» (cadena), «start_time» y «end_time» (fechas y horas), «start_price» y «end_price» (dobles), «slope» (double), «is_support» (bool), «touch_count» (int), «creation_time» (datetime), «touch_indices» (matriz de enteros) y «is_signaled» (bool) para almacenar los detalles de la línea de tendencia.

A continuación, declaramos por adelantado las funciones que se encargarán de las tareas clave: «DetectSwings» para identificar los puntos de oscilación, «SortSwings» para ordenar las oscilaciones, «CalculateAngle» para calcular la inclinación de la línea de tendencia, «ValidateTrendline» para garantizar la validez de la línea de tendencia, «FindAndDrawTrendlines» para crear y trazar líneas de tendencia, «UpdateTrendlines» para mantenerlas, «RemoveTrendlineFromStorage» para la limpieza, «IsStartingPointUsed» para comprobar el uso de los puntos y «LeastSquaresFit» para calcular la pendiente y la ordenada en el origen utilizando el método de mínimos cuadrados.

Por último, configuramos los parámetros de entrada y las variables globales: parámetros de entrada como «LookbackBars» (200) para el rango de detección de oscilaciones, «TouchTolerance» (10,0 puntos) para la precisión del contacto, «MinTouches» (3) para la validez, y el resto, que se explican por sí mismos; y variables globales como las matrices «swingLows» y «swingHighs» con «numLows» y «numHighs» (0) para los puntos de oscilación, y las matrices «trendlines» y «startingPoints» con «numTrendlines» y «numStartingPoints» (0) para el almacenamiento de líneas de tendencia y puntos. Esta configuración estructurada sienta las bases para que el EA detecte y opere con eficacia en las líneas de tendencia. Ahora que ya lo tenemos todo listo, podemos inicializar las matrices de almacenamiento durante la inicialización.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   ArrayResize(trendlines, 0);                     //--- Resize trendlines array
   numTrendlines = 0;                              //--- Reset trendlines count
   ArrayResize(startingPoints, 0);                 //--- Resize starting points array
   numStartingPoints = 0;                          //--- Reset starting points count
   return(INIT_SUCCEEDED);                         //--- Return success
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   ArrayResize(trendlines, 0);                     //--- Resize trendlines array
   numTrendlines = 0;                              //--- Reset trendlines count
   ArrayResize(startingPoints, 0);                 //--- Resize starting points array
   numStartingPoints = 0;                          //--- Reset starting points count
}

Para garantizar una configuración y una limpieza adecuadas de los recursos, en el controlador de eventos OnInit, preparamos el EA ajustando el tamaño de la matriz «trendlines» a 0 con ArrayResize y estableciendo «numTrendlines» en 0 para borrar cualquier dato existente de líneas de tendencia; a continuación, ajustamos el tamaño de la matriz «startingPoints» a 0 y establecemos «numStartingPoints» en 0 para restablecer los registros de puntos de inicio, y, por último, devolvemos «INIT_SUCCEEDED» para confirmar que la inicialización se ha realizado correctamente.

A continuación, en la función OnDeinit, hacemos lo mismo, asegurándonos de que no haya fugas de memoria cuando se cierre el programa, lo que permite empezar de cero para el funcionamiento del EA y una gestión adecuada de los recursos. Una vez realizada la inicialización, ya podemos pasar a definir la lógica de la estrategia. Para facilitar la modularización de la lógica, utilizaremos funciones, y la primera lógica que definiremos será la detección de puntos de oscilación, de modo que podamos disponer de puntos de referencia para construir la línea de tendencia.

//+------------------------------------------------------------------+
//| Check for new bar                                                |
//+------------------------------------------------------------------+
bool IsNewBar() {
   static datetime lastTime = 0;                      //--- Store last bar time
   datetime currentTime = iTime(_Symbol, _Period, 0); //--- Get current bar time
   if (lastTime != currentTime) {                     //--- Check for new bar
      lastTime = currentTime;                         //--- Update last time
      return true;                                    //--- Indicate new bar
   }
   return false;                                      //--- Indicate no new bar
}

//+------------------------------------------------------------------+
//| Sort swings by time (ascending, oldest first)                    |
//+------------------------------------------------------------------+
void SortSwings(Swing &swings[], int count) {
   for (int i = 0; i < count - 1; i++) {               //--- Iterate through swings
      for (int j = 0; j < count - i - 1; j++) {        //--- Compare adjacent swings
         if (swings[j].time > swings[j + 1].time) {    //--- Check time order
            Swing temp = swings[j];                    //--- Store temporary swing
            swings[j] = swings[j + 1];                 //--- Swap swings
            swings[j + 1] = temp;                      //--- Complete swap
         }
      }
   }
}

//+------------------------------------------------------------------+
//| Detect swing highs and lows                                      |
//+------------------------------------------------------------------+
void DetectSwings() {
   numLows = 0;                                         //--- Reset lows count
   ArrayResize(swingLows, 0);                           //--- Resize lows array
   numHighs = 0;                                        //--- Reset highs count
   ArrayResize(swingHighs, 0);                          //--- Resize highs array
   int totalBars = iBars(_Symbol, _Period);             //--- Get total bars
   int effectiveLookback = MathMin(LookbackBars, totalBars); //--- Calculate effective lookback
   if (effectiveLookback < 5) {                         //--- Check sufficient bars
      Print("Not enough bars for swing detection.");    //--- Log insufficient bars
      return;                                           //--- Exit function
   }
   for (int i = 2; i < effectiveLookback - 2; i++) {    //--- Iterate through bars
      double low_i = iLow(_Symbol, _Period, i);         //--- Get current low
      double low_im1 = iLow(_Symbol, _Period, i - 1);   //--- Get previous low
      double low_im2 = iLow(_Symbol, _Period, i - 2);   //--- Get two bars prior low
      double low_ip1 = iLow(_Symbol, _Period, i + 1);   //--- Get next low
      double low_ip2 = iLow(_Symbol, _Period, i + 2);   //--- Get two bars next low
      if (low_i < low_im1 && low_i < low_im2 && low_i < low_ip1 && low_i < low_ip2) { //--- Check for swing low
         Swing s;                                       //--- Create swing struct
         s.time = iTime(_Symbol, _Period, i);           //--- Set swing time
         s.price = low_i;                               //--- Set swing price
         ArrayResize(swingLows, numLows + 1);           //--- Resize lows array
         swingLows[numLows] = s;                        //--- Add swing low
         numLows++;                                     //--- Increment lows count
      }
      double high_i = iHigh(_Symbol, _Period, i);       //--- Get current high
      double high_im1 = iHigh(_Symbol, _Period, i - 1); //--- Get previous high
      double high_im2 = iHigh(_Symbol, _Period, i - 2); //--- Get two bars prior high
      double high_ip1 = iHigh(_Symbol, _Period, i + 1); //--- Get next high
      double high_ip2 = iHigh(_Symbol, _Period, i + 2); //--- Get two bars next high
      if (high_i > high_im1 && high_i > high_im2 && high_i > high_ip1 && high_i > high_ip2) { //--- Check for swing high
         Swing s;                                       //--- Create swing struct
         s.time = iTime(_Symbol, _Period, i);           //--- Set swing time
         s.price = high_i;                              //--- Set swing price
         ArrayResize(swingHighs, numHighs + 1);         //--- Resize highs array
         swingHighs[numHighs] = s;                      //--- Add swing high
         numHighs++;                                    //--- Increment highs count
      }
   }
   if (numLows > 0) SortSwings(swingLows, numLows);     //--- Sort swing lows
   if (numHighs > 0) SortSwings(swingHighs, numHighs);  //--- Sort swing highs
}

En este apartado, implementamos funciones clave para gestionar la detección de barras y la identificación de puntos de giro (swing), sentando así las bases para el análisis de líneas de tendencia. En primer lugar, creamos la función «IsNewBar», que comprueba si hay una nueva barra almacenando «lastTime» de forma estática como 0, comparándolo con «currentTime» de iTime para el símbolo y el período actuales en el desplazamiento 0 de la barra actual, actualizando «lastTime» si es diferente y devolviendo «true» si hay una nueva barra o «false» en caso contrario. A continuación, procedemos a implementar la función «SortSwings», que ordena el array «swings» por «time» en orden ascendente (primero las más antiguas) utilizando un algoritmo de ordenación por burbuja, iterando a través de «count - 1» elementos e intercambiando las estructuras «Swing» structs adyacentes con una variable temporal «temp» si sus tiempos no están en orden.

Por último, implementamos la función «DetectSwings», restableciendo «numLows» y «numHighs» a 0 y ajustando el tamaño de las matrices «swingLows» y «swingHighs» a 0, calculando «effectiveLookback» como el mínimo entre «LookbackBars» y el número total de barras de iBars, saliendo con un registro Print si hay menos de 5 barras disponibles, e iterando por las barras desde la 2 hasta «effectiveLookback - 2» para identificar mínimos y máximos de oscilación comparando los valores de «iLow» e «iHigh» con las dos barras anteriores y posteriores, creando estructuras «Swing» con «time» a partir de «iTime» y «price» a partir de «iLow» o iHigh, añadiéndolas a «swingLows» o «swingHighs» mediante ArrayResize, incrementando los contadores y ordenando las matrices con «SortSwings» si no están vacías. Esto garantizará una detección oportuna de los puntos swing para construir líneas de tendencia con precisión. Definamos ahora las funciones para calcular la pendiente de la línea de tendencia con el fin de establecer una restricción basada en dicha pendiente y su validación.

//+------------------------------------------------------------------+
//| Calculate visual inclination angle                               |
//+------------------------------------------------------------------+
double CalculateAngle(datetime time1, double price1, datetime time2, double price2) {
   int x1, y1, x2, y2;                                               //--- Declare coordinate variables
   if (!ChartTimePriceToXY(0, 0, time1, price1, x1, y1)) return 0.0; //--- Convert time1/price1 to XY
   if (!ChartTimePriceToXY(0, 0, time2, price2, x2, y2)) return 0.0; //--- Convert time2/price2 to XY
   double dx = (double)(x2 - x1);                                    //--- Calculate x difference
   double dy = (double)(y2 - y1);                                    //--- Calculate y difference
   if (dx == 0.0) return (dy > 0.0 ? -90.0 : 90.0);                  //--- Handle vertical line case
   double angle = MathArctan(-dy / dx) * 180.0 / M_PI;               //--- Calculate angle in degrees
   return angle;                                                     //--- Return angle
}

//+------------------------------------------------------------------+
//| Validate trendline                                               |
//+------------------------------------------------------------------+
bool ValidateTrendline(bool isSupport, datetime start_time, datetime ref_time, double ref_price, double slope, double tolerance_pen) {
   int bar_start = iBarShift(_Symbol, _Period, start_time);          //--- Get start bar index
   if (bar_start < 0) return false;                                  //--- Check invalid bar index
   for (int bar = bar_start; bar >= 0; bar--) {                      //--- Iterate through bars
      datetime bar_time = iTime(_Symbol, _Period, bar);              //--- Get bar time
      double dk = (double)(bar_time - ref_time);                     //--- Calculate time difference
      double line_price = ref_price + slope * dk;                    //--- Calculate line price
      if (isSupport) {                                               //--- Check support case
         double low = iLow(_Symbol, _Period, bar);                   //--- Get bar low
         if (low < line_price - tolerance_pen) return false;         //--- Check if broken
      } else {                                                       //--- Handle resistance case
         double high = iHigh(_Symbol, _Period, bar);                 //--- Get bar high
         if (high > line_price + tolerance_pen) return false;        //--- Check if broken
      }
   }
   return true;                                                      //--- Return valid
}

Procedemos a implementar funciones clave para calcular los ángulos de las líneas de tendencia y validar su integridad, garantizando así una detección fiable de las mismas. En primer lugar, creamos la función «CalculateAngle», que convierte dos puntos («time1», «price1» y «time2», «price2») en coordenadas del gráfico mediante la función ChartTimePriceToXY, transformándolos en «x1», «y1», «x2», «y2», devolviendo 0,0 si la conversión falla; a continuación, calcula la diferencia en x «dx» y la diferencia en y «dy», gestionando las líneas verticales devolviendo -90,0 o 90,0 si «dx» es cero, y calcula el ángulo en grados utilizando «MathArctan(-dy / dx) * 180,0 / M_PI» para obtener la inclinación visual.

A continuación, procedemos a implementar la función «ValidateTrendline», que valida una línea de tendencia obteniendo el índice de la barra inicial con iBarShift para «start_time», devolviendo «false» si no es válida, e iterando desde «bar_start» hasta 0, calculando el precio de la línea de tendencia en cada «bar_time» mediante «ref_price + slope * dk», donde «dk» es la diferencia de tiempo respecto al tiempo de referencia. En el caso de las líneas de tendencia de soporte («isSupport» true), comprobamos si el iLow de la barra cae por debajo de «line_price - tolerance_pen», devolviendo «false» si se rompe; en el caso de la resistencia, comprobamos si el iHigh supera «line_price + tolerance_pen», devolviendo «false» si se rompe, y devolvemos «true» si la línea de tendencia se mantiene. Ahora podemos centrarnos en la función que rige la lógica de cálculo del ajuste por mínimos cuadrados. Lo explicaremos de forma sencilla.

//+------------------------------------------------------------------+
//| Perform least-squares fit for slope and intercept                |
//+------------------------------------------------------------------+
void LeastSquaresFit(const datetime &times[], const double &prices[], int n, double &slope, double &intercept) {
   double sum_x = 0, sum_y = 0, sum_xy = 0, sum_x2 = 0; //--- Initialize sums
   for (int k = 0; k < n; k++) {                        //--- Iterate through points
      double x = (double)times[k];                      //--- Convert time to x
      double y = prices[k];                             //--- Set price as y
      sum_x += x;                                       //--- Accumulate x
      sum_y += y;                                       //--- Accumulate y
      sum_xy += x * y;                                  //--- Accumulate x*y
      sum_x2 += x * x;                                  //--- Accumulate x^2
   }
   slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x); //--- Calculate slope
   intercept = (sum_y - slope * sum_x) / n;             //--- Calculate intercept
}

Implementamos la función «LeastSquaresFit» para calcular la pendiente y la ordenada en el origen óptimas de las líneas de tendencia, lo que permite un ajuste preciso de las mismas. En primer lugar, inicializamos las variables «sum_x», «sum_y», «sum_xy» y «sum_x2» con el valor 0 para acumular los valores necesarios para el cálculo de mínimos cuadrados. A continuación, recorremos los «n» puntos de las matrices «times» y «prices», convirtiendo cada «times[k]» en un número de tipo «double» al que asignamos el nombre «x» y asignando a «y» el valor de «prices[k]», sumando «x» a «sum_x», «y» a «sum_y», «x * y» a «sum_xy» y «x * x» a «sum_x2». Por último, calculamos la «pendiente» utilizando la fórmula «(n * suma_xy - suma_x * suma_y) / (n * suma_x² - suma_x * suma_x)» y la «ordenada en el origen» como «(suma_y - pendiente * suma_x) / n», lo que nos proporciona la línea de mejor ajuste para la línea de tendencia a partir de los puntos introducidos. Si tienes alguna duda sobre la fórmula, échale un vistazo a continuación.

MÉTODO DE AJUSTE POR MÍNIMOS CUADRADOS

Esto garantizará una colocación matemáticamente precisa de las líneas de tendencia, lo que permitirá obtener señales de negociación fiables. Definamos ahora una función para gestionar las líneas de tendencia.

//+------------------------------------------------------------------+
//| Check if starting point is already used                          |
//+------------------------------------------------------------------+
bool IsStartingPointUsed(datetime time, double price, bool is_support) {
   for (int i = 0; i < numStartingPoints; i++) {  //--- Iterate through starting points
      if (startingPoints[i].time == time && MathAbs(startingPoints[i].price - price) < TouchTolerance * _Point && startingPoints[i].is_support == is_support) { //--- Check match
         return true;                             //--- Return used
      }
   }
   return false;                                   //--- Return not used
}

//+------------------------------------------------------------------+
//| Remove trendline from storage and optionally chart objects       |
//+------------------------------------------------------------------+
void RemoveTrendlineFromStorage(int index) {
   if (index < 0 || index >= numTrendlines) return;                    //--- Check valid index
   Print("Removing trendline from storage: ", trendlines[index].name); //--- Log removal
   if (DeleteExpiredObjects) {                                         //--- Check deletion flag
      ObjectDelete(0, trendlines[index].name);                         //--- Delete trendline object
      for (int m = 0; m < trendlines[index].touch_count; m++) {        //--- Iterate touches
         string arrow_name = trendlines[index].name + "_touch" + IntegerToString(m); //--- Generate arrow name
         ObjectDelete(0, arrow_name);                                  //--- Delete touch arrow
         string text_name = trendlines[index].name + "_point_label" + IntegerToString(m); //--- Generate text name
         ObjectDelete(0, text_name);                                   //--- Delete point label
      }
      string label_name = trendlines[index].name + "_label";           //--- Generate label name
      ObjectDelete(0, label_name);                                     //--- Delete trendline label
      string signal_arrow = trendlines[index].name + "_signal_arrow";  //--- Generate signal arrow name
      ObjectDelete(0, signal_arrow);                                   //--- Delete signal arrow
      string signal_text = trendlines[index].name + "_signal_text";    //--- Generate signal text name
      ObjectDelete(0, signal_text);                                    //--- Delete signal text
   }
   for (int i = index; i < numTrendlines - 1; i++) {                   //--- Shift array
      trendlines[i] = trendlines[i + 1];                               //--- Copy next trendline
   }
   ArrayResize(trendlines, numTrendlines - 1);                         //--- Resize trendlines array
   numTrendlines--;                                                    //--- Decrement trendlines count
}

Procedemos a implementar funciones de utilidad para gestionar los puntos de inicio de las líneas de tendencia y su limpieza, garantizando un seguimiento eficaz de las líneas de tendencia y una gestión óptima de los gráficos. En primer lugar, creamos la función «IsStartingPointUsed», que recorre los «numStartingPoints» del array «startingPoints» y comprueba si un «time», un «price» y un «is_support» determinados coinciden con algún punto de partida existente, comparando el «time» de forma exacta, el «price» dentro de «TouchTolerance * _Point» utilizando MathAbs, y «is_support», devolviendo «true» si lo encuentra o «false» si no. A continuación, procedemos a implementar la función «RemoveTrendlineFromStorage», que comprueba que el valor de «index» sea válido en relación con «numTrendlines», sale de la función si no lo es y registra la eliminación.

Si «DeleteExpiredObjects» es «true», eliminamos el objeto de línea de tendencia con ObjectDelete para «trendlines[index].name»; recorremos «touch_count» para eliminar las flechas de contacto y las etiquetas con nombres del tipo «trendlines[index].name + “_touch” + IntegerToString(m)» y «trendlines[index].name + “_point_label” + IntegerToString(m)», y eliminamos la etiqueta de la línea de tendencia, la flecha de señal y el texto de señal utilizando «label_name», «signal_arrow» y «signal_text». Por último, desplazamos la matriz «trendlines» de «index» a «numTrendlines - 1» para eliminar la entrada, redimensionamos «trendlines» con ArrayResize y disminuimos su número, lo que ayuda a evitar líneas de tendencia duplicadas y a eliminar de forma eficaz las líneas de tendencia caducadas o rotas. Definamos ahora una función para calcular y trazar las líneas de tendencia utilizando las funciones auxiliares que hemos definido.

//+------------------------------------------------------------------+
//| Find and draw trendlines if no active one exists                 |
//+------------------------------------------------------------------+
void FindAndDrawTrendlines(bool isSupport) {
   bool has_active = false;                       //--- Initialize active flag
   for (int i = 0; i < numTrendlines; i++) {      //--- Iterate through trendlines
      if (trendlines[i].is_support == isSupport) { //--- Check type match
         has_active = true;                       //--- Set active flag
         break;                                   //--- Exit loop
      }
   }
   if (has_active) return;                        //--- Exit if active trendline exists
   Swing swings[];                                //--- Initialize swings array
   int numSwings;                                 //--- Initialize swings count
   color lineColor;                               //--- Initialize line color
   string prefix;                                 //--- Initialize prefix
   if (isSupport) {                               //--- Handle support case
      numSwings = numLows;                        //--- Set number of lows
      ArrayResize(swings, numSwings);             //--- Resize swings array
      for (int i = 0; i < numSwings; i++) {       //--- Iterate through lows
         swings[i].time = swingLows[i].time;      //--- Copy low time
         swings[i].price = swingLows[i].price;    //--- Copy low price
      }
      lineColor = SupportLineColor;               //--- Set support line color
      prefix = "Trendline_Support_";              //--- Set support prefix
   } else {                                       //--- Handle resistance case
      numSwings = numHighs;                       //--- Set number of highs
      ArrayResize(swings, numSwings);             //--- Resize swings array
      for (int i = 0; i < numSwings; i++) {       //--- Iterate through highs
         swings[i].time = swingHighs[i].time;     //--- Copy high time
         swings[i].price = swingHighs[i].price;   //--- Copy high price
      }
      lineColor = ResistanceLineColor;            //--- Set resistance line color
      prefix = "Trendline_Resistance_";           //--- Set resistance prefix
   }
   if (numSwings < 2) return;                     //--- Exit if insufficient swings
   double pointValue = _Point;                    //--- Get point value
   double touch_tolerance = TouchTolerance * pointValue; //--- Calculate touch tolerance
   double pen_tolerance = PenetrationTolerance * pointValue; //--- Calculate penetration tolerance
   int best_j = -1;                               //--- Initialize best j index
   int max_touches = 0;                           //--- Initialize max touches
   int best_touch_indices[];                      //--- Initialize best touch indices
   double best_slope = 0.0;                       //--- Initialize best slope
   double best_intercept = 0.0;                   //--- Initialize best intercept
   datetime best_min_time = 0;                    //--- Initialize best min time
   for (int i = 0; i < numSwings - 1; i++) {      //--- Iterate through first points
      for (int j = i + 1; j < numSwings; j++) {   //--- Iterate through second points
         datetime time1 = swings[i].time;         //--- Get first time
         double price1 = swings[i].price;         //--- Get first price
         datetime time2 = swings[j].time;         //--- Get second time
         double price2 = swings[j].price;         //--- Get second price
         double dt = (double)(time2 - time1);     //--- Calculate time difference
         if (dt <= 0) continue;                   //--- Skip invalid time difference
         double initial_slope = (price2 - price1) / dt; //--- Calculate initial slope
         int touch_indices[];                     //--- Initialize touch indices
         ArrayResize(touch_indices, 0);           //--- Resize touch indices
         int touches = 0;                         //--- Initialize touches count
         ArrayResize(touch_indices, touches + 1); //--- Add first index
         touch_indices[touches] = i;              //--- Set first index
         touches++;                               //--- Increment touches
         ArrayResize(touch_indices, touches + 1); //--- Add second index
         touch_indices[touches] = j;              //--- Set second index
         touches++;                               //--- Increment touches
         for (int k = 0; k < numSwings; k++) {    //--- Iterate through swings
            if (k == i || k == j) continue;       //--- Skip used indices
            datetime tk = swings[k].time;         //--- Get swing time
            double dk = (double)(tk - time1);     //--- Calculate time difference
            double expected = price1 + initial_slope * dk; //--- Calculate expected price
            double actual = swings[k].price;      //--- Get actual price
            if (MathAbs(expected - actual) <= touch_tolerance) { //--- Check touch within tolerance
               ArrayResize(touch_indices, touches + 1); //--- Add index
               touch_indices[touches] = k;        //--- Set index
               touches++;                         //--- Increment touches
            }
         }
         if (touches >= MinTouches) {             //--- Check minimum touches
            ArraySort(touch_indices);             //--- Sort touch indices
            bool valid_spacing = true;            //--- Initialize spacing flag
            for (int m = 0; m < touches - 1; m++) { //--- Iterate through touches
               int idx1 = touch_indices[m];       //--- Get first index
               int idx2 = touch_indices[m + 1];   //--- Get second index
               int bar1 = iBarShift(_Symbol, _Period, swings[idx1].time); //--- Get first bar
               int bar2 = iBarShift(_Symbol, _Period, swings[idx2].time); //--- Get second bar
               int diff = MathAbs(bar1 - bar2);   //--- Calculate bar difference
               if (diff < MinBarSpacing) {        //--- Check minimum spacing
                  valid_spacing = false;          //--- Mark invalid spacing
                  break;                          //--- Exit loop
               }
            }
            if (valid_spacing) {                  //--- Check valid spacing
               datetime touch_times[];            //--- Initialize touch times
               double touch_prices[];             //--- Initialize touch prices
               ArrayResize(touch_times, touches); //--- Resize times array
               ArrayResize(touch_prices, touches); //--- Resize prices array
               for (int m = 0; m < touches; m++) { //--- Iterate through touches
                  int idx = touch_indices[m];      //--- Get index
                  touch_times[m] = swings[idx].time;   //--- Set time
                  touch_prices[m] = swings[idx].price; //--- Set price
               }
               double slope, intercept;                //--- Declare slope and intercept
               LeastSquaresFit(touch_times, touch_prices, touches, slope, intercept); //--- Perform least squares fit
               int adjusted_touch_indices[];           //--- Initialize adjusted indices
               ArrayResize(adjusted_touch_indices, 0); //--- Resize adjusted indices
               int adjusted_touches = 0;               //--- Initialize adjusted touches count
               for (int k = 0; k < numSwings; k++) {   //--- Iterate through swings
                  double expected = intercept + slope * (double)swings[k].time; //--- Calculate expected price
                  double actual = swings[k].price;     //--- Get actual price
                  if (MathAbs(expected - actual) <= touch_tolerance) { //--- Check touch
                     ArrayResize(adjusted_touch_indices, adjusted_touches + 1); //--- Add index
                     adjusted_touch_indices[adjusted_touches] = k; //--- Set index
                     adjusted_touches++;               //--- Increment adjusted touches
                  }
               }
               if (adjusted_touches >= MinTouches) { //--- Check minimum adjusted touches
                  datetime temp_min_time = swings[adjusted_touch_indices[0]].time; //--- Get min time
                  double temp_ref_price = intercept + slope * (double)temp_min_time; //--- Calculate ref price
                  if (ValidateTrendline(isSupport, temp_min_time, temp_min_time, temp_ref_price, slope, pen_tolerance)) { //--- Validate trendline
                     datetime temp_max_time = swings[adjusted_touch_indices[adjusted_touches - 1]].time; //--- Get max time
                     double temp_max_price = intercept + slope * (double)temp_max_time; //--- Calculate max price
                     double angle = CalculateAngle(temp_min_time, temp_ref_price, temp_max_time, temp_max_price); //--- Calculate angle
                     double abs_angle = MathAbs(angle); //--- Get absolute angle
                     if (abs_angle >= MinAngle && abs_angle <= MaxAngle) { //--- Check angle range
                        if (adjusted_touches > max_touches || (adjusted_touches == max_touches && j > best_j)) { //--- Check better trendline
                           max_touches = adjusted_touches; //--- Update max touches
                           best_j = j;                     //--- Update best j
                           best_slope = slope;             //--- Update best slope
                           best_intercept = intercept;     //--- Update best intercept
                           best_min_time = temp_min_time;  //--- Update best min time
                           ArrayResize(best_touch_indices, adjusted_touches); //--- Resize best indices
                           ArrayCopy(best_touch_indices, adjusted_touch_indices); //--- Copy indices
                        }
                     }
                  }
               }
            }
         }
      }
   }
   if (max_touches < MinTouches) {                //--- Check insufficient touches
      string type = isSupport ? "Support" : "Resistance"; //--- Set type string
      return;                                     //--- Exit function
   }
   int touch_indices[];                           //--- Initialize touch indices
   ArrayResize(touch_indices, max_touches);       //--- Resize touch indices
   ArrayCopy(touch_indices, best_touch_indices);  //--- Copy best indices
   int touches = max_touches;                     //--- Set touches count
   datetime min_time = best_min_time;             //--- Set min time
   double price_min = best_intercept + best_slope * (double)min_time; //--- Calculate min price
   datetime max_time = swings[touch_indices[touches - 1]].time; //--- Set max time
   double price_max = best_intercept + best_slope * (double)max_time; //--- Calculate max price
   datetime start_time_check = min_time;          //--- Set start time check
   double start_price_check = swings[touch_indices[0]].price; //--- Set start price check
   if (IsStartingPointUsed(start_time_check, start_price_check, isSupport)) { //--- Check used starting point
      return;                                     //--- Skip if used
   }
   datetime time_end = iTime(_Symbol, _Period, 0) + PeriodSeconds(_Period) * ExtensionBars; //--- Calculate end time
   double dk_end = (double)(time_end - min_time);      //--- Calculate end time difference
   double price_end = price_min + best_slope * dk_end; //--- Calculate end price
   string unique_name = prefix + TimeToString(TimeCurrent(), TIME_DATE|TIME_MINUTES|TIME_SECONDS); //--- Generate unique name
   if (ObjectFind(0, unique_name) < 0) {               //--- Check if trendline exists
      ObjectCreate(0, unique_name, OBJ_TREND, 0, min_time, price_min, time_end, price_end); //--- Create trendline
      ObjectSetInteger(0, unique_name, OBJPROP_COLOR, lineColor);   //--- Set color
      ObjectSetInteger(0, unique_name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style
      ObjectSetInteger(0, unique_name, OBJPROP_WIDTH, 1);           //--- Set width
      ObjectSetInteger(0, unique_name, OBJPROP_RAY_RIGHT, false);   //--- Disable right ray
      ObjectSetInteger(0, unique_name, OBJPROP_RAY_LEFT, false);    //--- Disable left ray
      ObjectSetInteger(0, unique_name, OBJPROP_BACK, false);        //--- Set to foreground
   }
   ArrayResize(trendlines, numTrendlines + 1);                      //--- Resize trendlines array
   trendlines[numTrendlines].name = unique_name;                    //--- Set trendline name
   trendlines[numTrendlines].start_time = min_time;                 //--- Set start time
   trendlines[numTrendlines].end_time = time_end;                   //--- Set end time
   trendlines[numTrendlines].start_price = price_min;               //--- Set start price
   trendlines[numTrendlines].end_price = price_end;                 //--- Set end price
   trendlines[numTrendlines].slope = best_slope;                    //--- Set slope
   trendlines[numTrendlines].is_support = isSupport;                //--- Set type
   trendlines[numTrendlines].touch_count = touches;                 //--- Set touch count
   trendlines[numTrendlines].creation_time = TimeCurrent();         //--- Set creation time
   trendlines[numTrendlines].is_signaled = false;                   //--- Set signaled flag
   ArrayResize(trendlines[numTrendlines].touch_indices, touches);   //--- Resize touch indices
   ArrayCopy(trendlines[numTrendlines].touch_indices, touch_indices); //--- Copy touch indices
   numTrendlines++;                                                 //--- Increment trendlines count
   ArrayResize(startingPoints, numStartingPoints + 1);              //--- Resize starting points array
   startingPoints[numStartingPoints].time = start_time_check; //--- Set starting point time
   startingPoints[numStartingPoints].price = start_price_check; //--- Set starting point price
   startingPoints[numStartingPoints].is_support = isSupport; //--- Set starting point type
   numStartingPoints++;                           //--- Increment starting points count
   if (DrawTouchArrows) {                         //--- Check draw arrows
      for (int m = 0; m < touches; m++) {         //--- Iterate through touches
         int idx = touch_indices[m];              //--- Get touch index
         datetime tk_time = swings[idx].time;     //--- Get touch time
         double tk_price = swings[idx].price;     //--- Get touch price
         string arrow_name = unique_name + "_touch" + IntegerToString(m); //--- Generate arrow name
         if (ObjectFind(0, arrow_name) < 0) {                             //--- Check if arrow exists
            ObjectCreate(0, arrow_name, OBJ_ARROW, 0, tk_time, tk_price); //--- Create touch arrow
            ObjectSetInteger(0, arrow_name, OBJPROP_ARROWCODE, 159);      //--- Set arrow code
            ObjectSetInteger(0, arrow_name, OBJPROP_ANCHOR, isSupport ? ANCHOR_TOP : ANCHOR_BOTTOM); //--- Set anchor
            ObjectSetInteger(0, arrow_name, OBJPROP_COLOR, lineColor);    //--- Set color
            ObjectSetInteger(0, arrow_name, OBJPROP_WIDTH, 1);            //--- Set width
            ObjectSetInteger(0, arrow_name, OBJPROP_BACK, false);         //--- Set to foreground
         }
      }
   }
   double angle = CalculateAngle(min_time, price_min, max_time, price_max); //--- Calculate angle
   string type = isSupport ? "Support" : "Resistance"; //--- Set type string
   Print(type + " Trendline " + unique_name + " drawn with " + IntegerToString(touches) + " touches. Inclination angle: " + DoubleToString(angle, 2) + " degrees."); //--- Log trendline
   if (DrawLabels) {                              //--- Check draw labels
      datetime mid_time = min_time + (max_time - min_time) / 2; //--- Calculate mid time
      double dk_mid = (double)(mid_time - min_time); //--- Calculate mid time difference
      double mid_price = price_min + best_slope * dk_mid; //--- Calculate mid price
      double label_offset = 20 * _Point * (isSupport ? -1 : 1); //--- Calculate label offset
      double label_price = mid_price + label_offset; //--- Calculate label price
      int label_anchor = isSupport ? ANCHOR_TOP : ANCHOR_BOTTOM; //--- Set label anchor
      string label_text = type + " Trendline";    //--- Set label text
      string label_name = unique_name + "_label"; //--- Generate label name
      if (ObjectFind(0, label_name) < 0) {        //--- Check if label exists
         ObjectCreate(0, label_name, OBJ_TEXT, 0, mid_time, label_price); //--- Create label
         ObjectSetString(0, label_name, OBJPROP_TEXT, label_text); //--- Set text
         ObjectSetInteger(0, label_name, OBJPROP_COLOR, clrBlack); //--- Set color
         ObjectSetInteger(0, label_name, OBJPROP_FONTSIZE, 8); //--- Set font size
         ObjectSetInteger(0, label_name, OBJPROP_ANCHOR, label_anchor); //--- Set anchor
         ObjectSetDouble(0, label_name, OBJPROP_ANGLE, angle); //--- Set angle
         ObjectSetInteger(0, label_name, OBJPROP_BACK, false); //--- Set to foreground
      }
      color point_label_color = isSupport ? clrSaddleBrown : clrDarkGoldenrod; //--- Set point label color
      double point_text_offset = 20.0 * _Point;   //--- Set point text offset
      for (int m = 0; m < touches; m++) {         //--- Iterate through touches
         int idx = touch_indices[m];              //--- Get touch index
         datetime tk_time = swings[idx].time;     //--- Get touch time
         double tk_price = swings[idx].price;     //--- Get touch price
         double text_price;                       //--- Initialize text price
         int point_text_anchor;                   //--- Initialize text anchor
         if (isSupport) {                         //--- Handle support
            text_price = tk_price - point_text_offset; //--- Set text price below
            point_text_anchor = ANCHOR_LEFT;      //--- Set left anchor
         } else {                                 //--- Handle resistance
            text_price = tk_price + point_text_offset; //--- Set text price above
            point_text_anchor = ANCHOR_BOTTOM;    //--- Set bottom anchor
         }
         string text_name = unique_name + "_point_label" + IntegerToString(m); //--- Generate text name
         string point_text = "Pt " + IntegerToString(m + 1); //--- Set point text
         if (ObjectFind(0, text_name) < 0) {     //--- Check if text exists
            ObjectCreate(0, text_name, OBJ_TEXT, 0, tk_time, text_price); //--- Create text
            ObjectSetString(0, text_name, OBJPROP_TEXT, point_text); //--- Set text
            ObjectSetInteger(0, text_name, OBJPROP_COLOR, point_label_color); //--- Set color
            ObjectSetInteger(0, text_name, OBJPROP_FONTSIZE, 8); //--- Set font size
            ObjectSetInteger(0, text_name, OBJPROP_ANCHOR, point_text_anchor); //--- Set anchor
            ObjectSetDouble(0, text_name, OBJPROP_ANGLE, 0); //--- Set angle
            ObjectSetInteger(0, text_name, OBJPROP_BACK, false); //--- Set to foreground
         }
      }
   }
}

Aquí implementamos la función «FindAndDrawTrendlines» para identificar y trazar líneas de tendencia, asegurándonos de que solo haya una línea de tendencia activa por tipo con puntos de contacto óptimos. En primer lugar, comprobamos si existe una línea de tendencia recorriendo «numTrendlines» en «trendlines», estableciendo «has_active» en «true» si «is_support» coincide con la entrada, y saliendo si la encontramos. A continuación, procedemos a configurar el soporte o la resistencia en función de «isSupport»: para el soporte, copiamos «numLows» en «numSwings», rellenamos «swings» a partir de «swingLows», establecemos «lineColor» en «SupportLineColor» y «prefix» en «Trendline_Support_»; para la resistencia, utilizamos «numHighs», «swingHighs», «ResistanceLineColor» y «Trendline_Resistance_», y salimos si «numSwings» es menor que 2. A continuación, calculamos «touch_tolerance» y «pen_tolerance» utilizando «TouchTolerance» y «PenetrationTolerance» con _Point, y recorremos «numSwings» pares para calcular una «initial_slope» inicial, recopilando «touch_indices» para los puntos que se encuentran dentro de la tolerancia de contacto.

Si los puntos de contacto cumplen el requisito «MinTouches» y superan el «MinBarSpacing» mediante iBarShift, utilizamos «LeastSquaresFit» para obtener la «pendiente» y la «ordenada al origen», volvemos a comprobar los puntos de contacto y validamos con «ValidateTrendline» y «CalculateAngle» en función de «MinAngle» y «MaxAngle», actualizando «best_j», «max_touches», «best_slope», «best_intercept», «best_min_time» y «best_touch_indices» para obtener la mejor línea de tendencia. Por último, si «max_touches» cumple con «MinTouches» y el punto inicial no está en uso según «IsStartingPointUsed», creamos una línea de tendencia con la función ObjectCreate como OBJ_TREND utilizando «unique_name», dibujamos flechas de contacto y etiquetas si «DrawTouchArrows» y «DrawLabels» son verdaderos, almacenamos los detalles en «trendlines», añadimos el punto inicial a los puntos iniciales y lo registramos, garantizando así la creación precisa de la línea de tendencia. Lo que queda ahora es gestionar las líneas de tendencia existentes mediante actualizaciones continuas y comprobar si se producen cruces que generen señales. Para simplificar, incorporaremos toda la lógica en una única función.

//+------------------------------------------------------------------+
//| Update trendlines and check for signals                          |
//+------------------------------------------------------------------+
void UpdateTrendlines() {
   datetime current_time = iTime(_Symbol, _Period, 0);       //--- Get current time
   double pointValue = _Point;                               //--- Get point value
   double pen_tolerance = PenetrationTolerance * pointValue; //--- Calculate penetration tolerance
   double touch_tolerance = TouchTolerance * pointValue;     //--- Calculate touch tolerance
   for (int i = numTrendlines - 1; i >= 0; i--) {            //--- Iterate trendlines backward
      string type = trendlines[i].is_support ? "Support" : "Resistance"; //--- Determine trendline type
      string name = trendlines[i].name;                      //--- Get trendline name
      if (current_time > trendlines[i].end_time) {           //--- Check if expired
         PrintFormat("%s trendline %s is no longer valid (expired). End time: %s, Current time: %s.", type, name, TimeToString(trendlines[i].end_time), TimeToString(current_time)); //--- Log expiration
         RemoveTrendlineFromStorage(i);                      //--- Remove trendline
         continue;                                           //--- Skip to next
      }
      datetime prev_bar_time = iTime(_Symbol, _Period, 1);   //--- Get previous bar time
      double dk = (double)(prev_bar_time - trendlines[i].start_time); //--- Calculate time difference
      double line_price = trendlines[i].start_price + trendlines[i].slope * dk; //--- Calculate line price
      double prev_low = iLow(_Symbol, _Period, 1);           //--- Get previous bar low
      double prev_high = iHigh(_Symbol, _Period, 1);         //--- Get previous bar high
      bool broken = false;                                   //--- Initialize broken flag
      if (trendlines[i].is_support && prev_low < line_price - pen_tolerance) { //--- Check support break
         PrintFormat("%s trendline %s is no longer valid (broken by price). Line price: %.5f, Prev low: %.5f, Penetration: %.5f points.", type, name, line_price, prev_low, PenetrationTolerance); //--- Log break
         RemoveTrendlineFromStorage(i);           //--- Remove trendline
         broken = true;                           //--- Set broken flag
      } else if (!trendlines[i].is_support && prev_high > line_price + pen_tolerance) { //--- Check resistance break
         PrintFormat("%s trendline %s is no longer valid (broken by price). Line price: %.5f, Prev high: %.5f, Penetration: %.5f points.", type, name, line_price, prev_high, PenetrationTolerance); //--- Log break
         RemoveTrendlineFromStorage(i);           //--- Remove trendline
         broken = true;                           //--- Set broken flag
      }
      if (!broken && !trendlines[i].is_signaled && EnableTradingSignals) { //--- Check for trading signal
         bool touched = false;                    //--- Initialize touched flag
         string signal_type = "";                 //--- Initialize signal type
         color signal_color = clrNONE;            //--- Initialize signal color
         int arrow_code = 0;                      //--- Initialize arrow code
         int anchor = 0;                          //--- Initialize anchor
         double text_angle = 0.0;                 //--- Initialize text angle
         double text_offset = 0.0;                //--- Initialize text offset
         double text_price = 0.0;                 //--- Initialize text price
         int text_anchor = 0;                     //--- Initialize text anchor
         if (trendlines[i].is_support && MathAbs(prev_low - line_price) <= touch_tolerance) { //--- Check support touch
            touched = true;                       //--- Set touched flag
            signal_type = "BUY";                  //--- Set buy signal
            signal_color = clrBlue;               //--- Set blue color
            arrow_code = 217;                     //--- Set up arrow for support (BUY)
            anchor = ANCHOR_TOP;                  //--- Set top anchor
            text_angle = -90.0;                   //--- Set vertical upward for BUY
            text_offset = -20 * pointValue;        //--- Set text offset
            text_price = line_price + text_offset; //--- Calculate text price
            text_anchor = ANCHOR_LEFT;            //--- Set left anchor
            double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits); //--- Get ask price
            double SL = NormalizeDouble(Ask - inpSLPoints * _Point, _Digits); //--- Calculate stop loss
            double TP = NormalizeDouble(Ask + (inpSLPoints * inpRRRatio) * _Point, _Digits); //--- Calculate take profit
            obj_Trade.Buy(inpLot, _Symbol, Ask, SL, TP); //--- Execute buy trade
         } else if (!trendlines[i].is_support && MathAbs(prev_high - line_price) <= touch_tolerance) { //--- Check resistance touch
            touched = true;                       //--- Set touched flag
            signal_type = "SELL";                 //--- Set sell signal
            signal_color = clrRed;                //--- Set red color
            arrow_code = 218;                     //--- Set down arrow for resistance (SELL)
            anchor = ANCHOR_BOTTOM;               //--- Set bottom anchor
            text_angle = 90.0;                    //--- Set vertical downward for SELL
            text_offset = 20 * pointValue;       //--- Set text offset
            text_price = line_price + text_offset; //--- Calculate text price
            text_anchor = ANCHOR_BOTTOM;          //--- Set bottom anchor
            double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits); //--- Get bid price
            double SL = NormalizeDouble(Bid + inpSLPoints * _Point, _Digits); //--- Calculate stop loss
            double TP = NormalizeDouble(Bid - (inpSLPoints * inpRRRatio) * _Point, _Digits); //--- Calculate take profit
            obj_Trade.Sell(inpLot, _Symbol, Bid, SL, TP); //--- Execute sell trade
         }
         if (touched) {                           //--- Check if touched
            PrintFormat("Signal generated for %s trendline %s: %s at price %.5f, time %s.", type, name, signal_type, line_price, TimeToString(current_time)); //--- Log signal
            string arrow_name = name + "_signal_arrow"; //--- Generate signal arrow name
            if (ObjectFind(0, arrow_name) < 0) {  //--- Check if arrow exists
               ObjectCreate(0, arrow_name, OBJ_ARROW, 0, prev_bar_time, line_price); //--- Create signal arrow
               ObjectSetInteger(0, arrow_name, OBJPROP_ARROWCODE, arrow_code); //--- Set arrow code
               ObjectSetInteger(0, arrow_name, OBJPROP_ANCHOR, anchor); //--- Set anchor
               ObjectSetInteger(0, arrow_name, OBJPROP_COLOR, signal_color); //--- Set color
               ObjectSetInteger(0, arrow_name, OBJPROP_WIDTH, 1); //--- Set width
               ObjectSetInteger(0, arrow_name, OBJPROP_BACK, false); //--- Set to foreground
            }
            string text_name = name + "_signal_text"; //--- Generate signal text name
            if (ObjectFind(0, text_name) < 0) {   //--- Check if text exists
               ObjectCreate(0, text_name, OBJ_TEXT, 0, prev_bar_time, text_price); //--- Create signal text
               ObjectSetString(0, text_name, OBJPROP_TEXT, " " + signal_type); //--- Set text content
               ObjectSetInteger(0, text_name, OBJPROP_COLOR, signal_color); //--- Set color
               ObjectSetInteger(0, text_name, OBJPROP_FONTSIZE, 10); //--- Set font size
               ObjectSetInteger(0, text_name, OBJPROP_ANCHOR, text_anchor); //--- Set anchor
               ObjectSetDouble(0, text_name, OBJPROP_ANGLE, text_angle); //--- Set angle
               ObjectSetInteger(0, text_name, OBJPROP_BACK, false); //--- Set to foreground
            }
            trendlines[i].is_signaled = true;     //--- Set signaled flag
         }
      }
   }
}

Para garantizar que se supervisen las líneas de tendencia activas y se tomen las medidas oportunas, creamos la función «UpdateTrendlines», que no devuelve ningún valor, ya que no necesitamos devolver nada. En primer lugar, obtenemos el valor «current_time» utilizando iTime para la barra actual y calculamos «pointValue» como _Point, «pen_tolerance» como «PenetrationTolerance * pointValue» y «touch_tolerance» como «TouchTolerance * pointValue». A continuación, procedemos a recorrer hacia atrás «numTrendlines» en la matriz «trendlines», determinando el «tipo» de la línea de tendencia como «Soporte» o «Resistencia» en función de «is_support», y comprobando si «current_time» supera a «end_time», registrando que ha expirado con PrintFormat y eliminando la línea de tendencia con «RemoveTrendlineFromStorage» si ha vencido.

A continuación, para las líneas de tendencia que no han caducado, calculamos el precio de la línea de tendencia en «prev_bar_time» (a partir de iTime en el desplazamiento 1) utilizando «start_price + slope * dk», comprobamos si la línea de tendencia se ha roto comparando «prev_low» o «prev_high» con «line_price» con «pen_tolerance», registramos las rupturas con «PrintFormat» y, si se ha roto, la eliminamos con «RemoveTrendlineFromStorage».

Por último, si no se ha roto y «is_signaled» es falso con «EnableTradingSignals» en verdadero, comprobamos si se han producido toques: para el soporte, si «prev_low» se encuentra dentro de «touch_tolerance» respecto a «line_price», establecemos una señal de «COMPRA», ejecutamos una operación de compra con «obj_Trade.Buy» utilizando «inpLot», «Ask», «SL» y «TP» calculados con «inpSLPoints» e «inpRRRatio», y dibujamos una flecha azul hacia arriba (código 217) y un texto; en cuanto a la resistencia, si «prev_high» se encuentra dentro de la tolerancia, establecemos una señal de «VENTA», ejecutamos una operación de venta con «obj_Trade.Sell», y dibujamos una flecha roja hacia abajo (código 218) y un texto, registrando con «PrintFormat», creando objetos con ObjectCreate, estableciendo propiedades con ObjectSetInteger y ObjectSetString, y marcando «is_signaled» como verdadero, asegurándonos de que las líneas de tendencia se actualicen y generen señales de trading precisas. La elección de los códigos de flecha que quieras utilizar depende de ti. A continuación te ofrecemos una lista de códigos que puedes utilizar de entre los códigos Wingdings definidos por MQL5.

MQL5 WINGDINGS

Ahora podemos llamar a estas funciones en el controlador de eventos OnTick para que el sistema proporcione respuesta basada en ticks.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   if (!IsNewBar()) return;                        //--- Exit if not new bar
   DetectSwings();                                 //--- Detect swings
   UpdateTrendlines();                             //--- Update trendlines
   FindAndDrawTrendlines(true);                    //--- Find/draw support trendlines
}

En el controlador de eventos OnTick, gestionamos la detección de líneas de tendencia y la lógica de negociación en cada nueva barra. En primer lugar, comprobamos si se ha formado una nueva barra llamando a la función «IsNewBar» y, si el resultado es falso, salimos inmediatamente para evitar un procesamiento innecesario. A continuación, llamamos a la función «DetectSwings» para identificar y actualizar los máximos y mínimos oscilantes almacenados en «swingHighs» y «swingLows». A continuación, invocamos «UpdateTrendlines» para validar las líneas de tendencia existentes, eliminar las caducadas o rotas y generar señales de negociación si se detectan toques del precio dentro de una tolerancia de toque definida. Por último, llamamos a la función «FindAndDrawTrendlines» con un parámetro «true» para crear líneas de tendencia de soporte, asegurándonos de que solo se dibujen nuevas líneas de tendencia si no existe ninguna línea de tendencia activa de tipo «soporte». Tras la compilación, obtenemos el siguiente resultado.

LÍNEA DE TENDENCIA DE SOPORTE CONFIRMADA

En la imagen podemos ver que localizamos, analizamos, trazamos y operamos con la línea de tendencia en el momento en que se toca. Las líneas caducadas también se eliminan correctamente de la matriz de almacenamiento. Podemos conseguir lo mismo con las líneas de tendencia de resistencia llamando a la misma función que para el soporte, pero indicando «false» como parámetro de entrada.

//--- other ontick functions

FindAndDrawTrendlines(false);                   //--- Find/draw resistance trendlines

//---

Tras añadir la llamada a la función y compilar el programa, obtenemos el siguiente resultado.

LÍNEA DE TENDENCIA DE RESISTENCIA

En la imagen podemos ver que también detectamos y operamos con las líneas de tendencia de resistencia. Cuando probamos y combinamos todo, obtenemos el siguiente resultado.

RESULTADO COMBINADO

En la imagen podemos ver que detectamos las líneas de tendencia, las visualizamos y actuamos en consecuencia cuando el precio las toca, logrando así nuestros objetivos. Lo que queda por hacer es realizar pruebas retrospectivas del programa, y eso se aborda en la siguiente sección.


Backtesting

Tras realizar exhaustivas pruebas retrospectivas, hemos obtenido los siguientes resultados.

Gráfico de backtesting:

GRÁFICO

Informe de backtesting:

INFORME


Conclusión

En conclusión, hemos desarrollado un programa de estrategia de trading basada en líneas de tendencia en MQL5 que utiliza el método de ajuste por mínimos cuadrados para detectar líneas de tendencia fiables de soporte y resistencia, generando señales automáticas de compra y venta con ayudas visuales como flechas y etiquetas. A través de componentes modulares como la estructura «TrendlineInfo» y funciones como «FindAndDrawTrendlines», ofrece un enfoque riguroso para el trading basado en tendencias que puedes perfeccionar ajustando sus parámetros.

Descargo de responsabilidad: Este artículo tiene fines exclusivamente educativos. Operar en los mercados financieros conlleva riesgos financieros significativos, y la volatilidad del mercado puede ocasionar pérdidas. Es fundamental realizar pruebas retrospectivas exhaustivas y una gestión de riesgos cuidadosa antes de implementar este programa en mercados reales.

Aprovechando los conceptos y la implementación presentados, podrás adaptar este sistema de líneas de tendencia a tu estilo de trading, mejorando así sus estrategias algorítmicas. ¡Feliz trading!

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

Archivos adjuntos |
Utilizando redes neuronales en MetaTrader Utilizando redes neuronales en MetaTrader
En el artículo se muestra la aplicación de las redes neuronales en los programas de MQL, usando la biblioteca de libre difusión FANN. Usando como ejemplo una estrategia que utiliza el indicador MACD se ha construido un experto que usa el filtrado con red neuronal de las operaciones. Dicho filtrado ha mejorado las características del sistema comercial.
Integración de MQL5 con paquetes de procesamiento de datos (Parte 5): Aprendizaje adaptativo y flexibilidad Integración de MQL5 con paquetes de procesamiento de datos (Parte 5): Aprendizaje adaptativo y flexibilidad
Esta parte se centra en la creación de un modelo de negociación flexible y adaptativo, entrenado con datos históricos del XAUUSD, preparándolo para su exportación a ONNX y su posible integración en sistemas de negociación en tiempo real.
Particularidades del trabajo con números del tipo double en MQL4 Particularidades del trabajo con números del tipo double en MQL4
En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.
Del básico al intermedio: Recursos Del básico al intermedio: Recursos
En este artículo conocerás un concepto que puede resultar extremadamente útil en muchos casos y facilitar enormemente el intercambio de tus aplicaciones y proyectos. Aunque no es un concepto sencillo de explicar por completo en un solo artículo, lo que se explicará y expondrá aquí ya nos permitirá hacer muchas cosas en el futuro, incluidas algunas que, de otro modo, no sería posible realizar. Precisamente porque este artículo aún no se había publicado: para que pudieras contar con material de apoyo y una base inicial de estudio.