English Русский 中文 Deutsch 日本語
preview
Herramientas de trading de MQL5 (Parte 6): Panel de control holográfico dinámico con animaciones de pulso y controles interactivos

Herramientas de trading de MQL5 (Parte 6): Panel de control holográfico dinámico con animaciones de pulso y controles interactivos

MetaTrader 5Trading |
22 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Introducción

En nuestro artículo anterior (Parte 5), creamos una cinta de cotizaciones deslizante en MetaQuotes Language 5 (MQL5) para el seguimiento en tiempo real de símbolos, con precios, spreads y variaciones que se van desplazando, con el fin de mantener informados a los traders de forma eficaz. En la parte 6, desarrollamos un panel holográfico dinámico que muestra indicadores de varios símbolos y marcos temporales, como el Relative Strength Index (RSI) y la volatilidad (basada en el Average True Range (ATR)), con animaciones de pulso, opciones de ordenación y controles interactivos, creando una herramienta de análisis atractiva y dinámica. Trataremos los siguientes temas:

  1. Comprender la arquitectura del panel de control holográfico
  2. Implementación en MQL5
  3. Pruebas retrospectivas (Backtesting)
  4. Conclusión

Al final, tendrás un panel de control holográfico personalizable listo para tu configuración de trading. ¡Empecemos!


Comprender la arquitectura del panel de control holográfico

El panel de control holográfico que estamos desarrollando es una herramienta visual que supervisa múltiples símbolos y marcos temporales, mostrando indicadores como el RSI y la volatilidad, además de funciones de ordenación y alertas, para ayudarnos a detectar rápidamente las oportunidades. Esta arquitectura es importante porque combina datos en tiempo real con controles interactivos y animaciones, lo que hace que el análisis resulte más atractivo y eficaz en un entorno de gráficos con mucha información.

Lo conseguiremos utilizando arrays para la gestión de datos, manejadores de indicadores para indicadores como ATR y RSI, y funciones para ordenar y aplicar efectos de pulso, con botones para activar o desactivar la visibilidad y cambiar de vista. Tenemos previsto centralizar las actualizaciones en un bucle que actualice la interfaz de usuario (UI) de forma dinámica, garantizando que el panel de control siga siendo adaptable y receptivo para el trading estratégico. Observa la visualización siguiente para hacerte una idea del resultado que queremos lograr antes de pasar a la implementación.

ARQUITECTURA COMPLETA


Implementación en MQL5

Para crear el programa en MQL5, tendremos que definir los metadatos del programa y, a continuación, definir algunas entradas que nos permitan modificar fácilmente el funcionamiento del programa sin tener que modificar el código directamente.

//+------------------------------------------------------------------+
//|                                  Holographic Dashboard 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 version   "1.00"

#include <Arrays\ArrayString.mqh> //--- Include ArrayString library for string array operations
#include <Files\FileTxt.mqh>      //--- Include FileTxt library for text file operations

// Input Parameters
input int BaseFontSize = 9;           // Base Font Size
input string FontType = "Calibri";    // Font Type (Professional)
input int X_Offset = 30;              // X Offset
input int Y_Offset = 30;              // Y Offset
input color PanelColor = clrDarkSlateGray; // Panel Background Color
input color TitleColor = clrWhite;    // Title/Symbol Color
input color DataColor = clrLightGray; // Bid/Neutral Color
input color ActiveColor = clrLime;    // Active Timeframe/Symbol Color
input color UpColor = clrDeepSkyBlue; // Uptrend Color
input color DownColor = clrCrimson;   // Downtrend Color
input color LineColor = clrSilver;    // Grid Line Color
input bool EnableAnimations = true;   // Enable Pulse Animations
input int PanelWidth = 730;           // Panel Width (px)
input int ATR_Period = 14;            // ATR Period for Volatility
input int RSI_Period = 14;            // RSI Period
input double Vol_Alert_Threshold = 2.0; // Volatility Alert Threshold (%)
input color GlowColor = clrDodgerBlue; // Glow Color for holographic effect
input int AnimationSpeed = 30; // Animation delay in ms for pulse
input int PulseCycles = 3; // Number of pulse cycles for animations

Empezamos incorporando bibliotecas para arrays de cadenas y el registro en archivos de texto, y configurando las entradas para personalizar la interfaz de usuario y los indicadores. Incluimos «<Arrays\ArrayString.mqh>» para gestionar listas de símbolos y «<Files\FileTxt.mqh>» para registrar errores en un archivo. Estos ajustes nos permitirán ajustar el tamaño de fuente base a 9, elegir una fuente profesional como Calibri, establecer desplazamientos para el posicionamiento en los ejes x e y de 30 píxeles cada uno, y seleccionar colores como el gris pizarra oscuro para el fondo del panel, el blanco para los títulos, el gris claro para los datos, el verde lima para los elementos activos, el azul cielo intenso para las tendencias alcistas, el carmesí para las tendencias bajistas y el plateado para las líneas de la cuadrícula.

Habilitamos por defecto las animaciones de pulso, definimos el ancho del panel en 730 píxeles, fijamos los períodos del ATR y el RSI en 14 para los cálculos de volatilidad e impulso, establecemos un umbral del 2,0 % para las alertas de volatilidad, elegimos «Dodger Blue» para el resplandor holográfico y configuramos la velocidad de animación en 30 ms con 3 ciclos de pulso. Estos ajustes harán que el panel de control se adapte perfectamente a las preferencias visuales y funcionales de cada usuario. A continuación, debemos definir algunas variables globales que utilizaremos a lo largo del programa.

// Global Variables
double prices_PrevArray[];            //--- Array for previous prices
double volatility_Array[];            //--- Array for volatility values
double bid_array[];                   //--- Array for bid prices
long spread_array[];                  //--- Array for spreads
double change_array[];                //--- Array for percentage changes
double vol_array[];                   //--- Array for volumes
double rsi_array[];                   //--- Array for RSI values
int indices[];                        //--- Array for sorted indices
ENUM_TIMEFRAMES periods[] = {PERIOD_M1, PERIOD_M5, PERIOD_H1, PERIOD_H2, PERIOD_H4, PERIOD_D1, PERIOD_W1}; //--- Array of timeframes
string logFileName = "Holographic_Dashboard_Log.txt"; //--- Log file name
int sortMode = 0;                     //--- Current sort mode
string sortNames[] = {"Name ASC", "Vol DESC", "Change ABS DESC", "RSI DESC"}; //--- Sort mode names
int atr_handles_sym[];                //--- ATR handles for symbols
int rsi_handles_sym[];                //--- RSI handles for symbols
int atr_handles_tf[];                 //--- ATR handles for timeframes
int rsi_handles_tf[];                 //--- RSI handles for timeframes
int totalSymbols;                     //--- Total number of symbols
bool dashboardVisible = true;         //--- Dashboard visibility flag

Aquí definimos variables globales para gestionar los datos y los indicadores de nuestro programa, lo que permite la supervisión en tiempo real, la ordenación y las animaciones. Creamos arrays como «prices_PrevArray» para los precios anteriores con el fin de calcular las variaciones, «volatility_Array» para los valores de volatilidad, «bid_array» para el precio Bid actual, «spread_array» para los spreads como valores de tipo long, «change_array» para las variaciones porcentuales, «vol_array» para los volúmenes, «rsi_array» para los valores del RSI y «indices» para ordenar los índices. Configuramos «periods» como una matriz de marcos temporales que van desde PERIOD_M1 hasta PERIOD_W1; «logFileName» como «Holographic_Dashboard_Log.txt» para el registro de errores; «sortMode» en 0 para la ordenación inicial; «sortNames» como cadenas de caracteres para opciones de ordenación como «Name ASC» o «Vol DESC», y arrays para los manejadores de ATR y RSI («atr_handles_sym», «rsi_handles_sym» para símbolos, «atr_handles_tf», «rsi_handles_tf» para marcos temporales).

El entero «totalSymbols» registra el número de símbolos, y «dashboardVisible» es «true», lo que controla el estado del panel de control. Para gestionar mejor los objetos, vamos a crear una clase.

// Object Manager Class
class CObjectManager : public CArrayString {
public:
   void AddObject(string name) {      //--- Add object name to manager
      if (!Add(name)) {               //--- Check if add failed
         LogError(__FUNCTION__ + ": Failed to add object name: " + name); //--- Log error
      }
   }
   
   void DeleteAllObjects() {          //--- Delete all managed objects
      for (int i = Total() - 1; i >= 0; i--) { //--- Iterate through objects
         string name = At(i);         //--- Get object name
         if (ObjectFind(0, name) >= 0) { //--- Check if object exists
            if (!ObjectDelete(0, name)) { //--- Delete object
               LogError(__FUNCTION__ + ": Failed to delete object: " + name + ", Error: " + IntegerToString(GetLastError())); //--- Log deletion failure
            }
         }
         Delete(i);                   //--- Remove from array
      }
      ChartRedraw(0);                 //--- Redraw chart
   }
};

Para gestionar los objetos del panel de control de forma eficiente, creamos la clase «CObjectManager», que hereda de la clase CArrayString. En el método «AddObject», añadimos el objeto «name» a la matriz mediante «Add» y, si la operación falla, registramos el error mediante «LogError». Utilizamos el método «DeleteAllObjects» recorriendo el array hacia atrás con «Total», obtenemos cada «name» con «At», comprobamos si existe con la función ObjectFind, lo eliminamos con ObjectDelete y registramos los errores si falla, lo eliminamos del array con «Delete», y volvemos a dibujar el gráfico con la función ChartRedraw. Con la extensión de la clase, podemos crear algunas funciones auxiliares a las que recurriremos a lo largo del programa para reutilizarlas.

CObjectManager objManager;            //--- Object manager instance

//+------------------------------------------------------------------+
//| Utility Functions                                                |
//+------------------------------------------------------------------+
void LogError(string message) {
   CFileTxt file;                     //--- Create file object
   if (file.Open(logFileName, FILE_WRITE | FILE_TXT | FILE_COMMON, true) >= 0) { //--- Open log file
      file.WriteString(message + "\n"); //--- Write message
      file.Close();                   //--- Close file
   }
   Print(message);                    //--- Print message
}

string Ask(string symbol) {
   double value;                      //--- Variable for ask price
   if (SymbolInfoDouble(symbol, SYMBOL_ASK, value)) { //--- Get ask price
      return DoubleToString(value, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS)); //--- Return formatted ask
   }
   LogError(__FUNCTION__ + ": Failed to get ask price for " + symbol + ", Error: " + IntegerToString(GetLastError())); //--- Log error
   return "N/A";                      //--- Return N/A on failure
}

string Bid(string symbol) {
   double value;                      //--- Variable for bid price
   if (SymbolInfoDouble(symbol, SYMBOL_BID, value)) { //--- Get bid price
      return DoubleToString(value, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS)); //--- Return formatted bid
   }
   LogError(__FUNCTION__ + ": Failed to get bid price for " + symbol + ", Error: " + IntegerToString(GetLastError())); //--- Log error
   return "N/A";                      //--- Return N/A on failure
}

string Spread(string symbol) {
   long value;                        //--- Variable for spread
   if (SymbolInfoInteger(symbol, SYMBOL_SPREAD, value)) { //--- Get spread
      return IntegerToString(value);  //--- Return spread as string
   }
   LogError(__FUNCTION__ + ": Failed to get spread for " + symbol + ", Error: " + IntegerToString(GetLastError())); //--- Log error
   return "N/A";                      //--- Return N/A on failure
}

string PercentChange(double current, double previous) {
   if (previous == 0) return "0.00%"; //--- Handle zero previous value
   return StringFormat("%.2f%%", ((current - previous) / previous) * 100); //--- Calculate and format percentage
}

string TruncPeriod(ENUM_TIMEFRAMES period) {
   return StringSubstr(EnumToString(period), 7); //--- Truncate timeframe string
}

Para gestionar objetos y datos, creamos una instancia de «objManager» como «CObjectManager» para realizar un seguimiento de los elementos de la interfaz de usuario. Creamos la función «LogError» para registrar errores: abrimos «logFileName» con «CFileTxt» utilizando «FILE_WRITE | FILE_TXT | FILE_COMMON», escribimos el «message» con «WriteString», cerramos el archivo y lo mostramos en pantalla. La función «Ask» recupera el precio Ask de un «símbolo» mediante SymbolInfoDouble, lo formatea con DoubleToString utilizando «SymbolInfoInteger» para los dígitos, registra los errores con «LogError» si falla y devuelve «N/A» en caso de error. Del mismo modo, la función «Bid» recupera el precio Bid, le aplica el formato adecuado y gestiona los errores.

La función «Spread» obtiene el spread mediante «SymbolInfoInteger», lo devuelve como una cadena con IntegerToString o «N/A» en caso de error. La función «PercentChange» calcula la variación porcentual entre los precios «actual» y «anterior» utilizando StringFormat, y devuelve «0,00 %» si «anterior» es cero. La función «TruncPeriod» trunca las cadenas ENUM_TIMEFRAMES con StringSubstr para mostrar los marcos temporales de forma concisa, lo que garantiza que los resultados sean claros. Ahora podemos crear la función para los pulsos holográficos.

//+------------------------------------------------------------------+
//| Holographic Animation Function                                   |
//+------------------------------------------------------------------+
void HolographicPulse(string objName, color mainClr, color glowClr) {
   if (!EnableAnimations) return;     //--- Exit if animations disabled
   int cycles = PulseCycles;          //--- Set pulse cycles
   int delay = AnimationSpeed;        //--- Set animation delay
   for (int i = 0; i < cycles; i++) { //--- Iterate through cycles
      ObjectSetInteger(0, objName, OBJPROP_COLOR, glowClr); //--- Set glow color
      ChartRedraw(0);                 //--- Redraw chart
      Sleep(delay);                   //--- Delay
      ObjectSetInteger(0, objName, OBJPROP_COLOR, mainClr); //--- Set main color
      ChartRedraw(0);                 //--- Redraw chart
      Sleep(delay / 2);               //--- Shorter delay
   }
}

Aquí implementamos la función «HolographicPulse» para crear un efecto de animación de pulso en los elementos del panel de control. Se sale de la función de forma anticipada si «EnableAnimations» es «false» para omitir las animaciones. Establecemos «cycles» en «PulseCycles» y «delay» en «AnimationSpeed», y luego recorremos «cycles» con un bucle «for». En cada iteración, establecemos el color del objeto en «glowClr» con ObjectSetInteger para OBJPROP_COLOR, volvemos a dibujar el gráfico con ChartRedraw, hacemos una pausa con «Sleep» durante «delay», volvemos a «mainClr», volvemos a dibujar el gráfico y hacemos una pausa de «delay / 2» para conseguir un efecto más breve. Esto añadirá el pulso holográfico para resaltar visualmente los elementos activos o en estado de alerta. Con estas funciones, ya podemos pasar a crear el panel de control de inicialización principal. Para ello, necesitaremos algunas funciones auxiliares que nos permitan mantener la modularidad del programa.

//+------------------------------------------------------------------+
//| Create Text Label Function                                       |
//+------------------------------------------------------------------+
bool createText(string objName, string text, int x, int y, color clrTxt, int fontsize, string font, bool animate = false, double opacity = 1.0) {
   ResetLastError();                  //--- Reset error code
   if (ObjectFind(0, objName) < 0) {  //--- Check if object exists
      if (!ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0)) { //--- Create label
         LogError(__FUNCTION__ + ": Failed to create label: " + objName + ", Error: " + IntegerToString(GetLastError())); //--- Log error
         return false;                //--- Return failure
      }
      objManager.AddObject(objName);  //--- Add to manager
   }
   ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, x); //--- Set x-coordinate
   ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, y); //--- Set y-coordinate
   ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner
   ObjectSetString(0, objName, OBJPROP_TEXT, text); //--- Set text
   ObjectSetInteger(0, objName, OBJPROP_COLOR, clrTxt); //--- Set color
   ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, fontsize); //--- Set font size
   ObjectSetString(0, objName, OBJPROP_FONT, font); //--- Set font
   ObjectSetInteger(0, objName, OBJPROP_BACK, false); //--- Set foreground
   ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false); //--- Disable selection
   ObjectSetInteger(0, objName, OBJPROP_ZORDER, StringFind(objName, "Glow") >= 0 ? -1 : 0); //--- Set z-order

   if (animate && EnableAnimations) { //--- Check for animation
      ObjectSetInteger(0, objName, OBJPROP_COLOR, DataColor); //--- Set temporary color
      ChartRedraw(0);                 //--- Redraw chart
      Sleep(50);                      //--- Delay
      ObjectSetInteger(0, objName, OBJPROP_COLOR, clrTxt); //--- Set final color
   }

   ChartRedraw(0);                    //--- Redraw chart
   return true;                       //--- Return success
}

//+------------------------------------------------------------------+
//| Create Button Function                                           |
//+------------------------------------------------------------------+
bool createButton(string objName, string text, int x, int y, int width, int height, color textColor, color bgColor, color borderColor, bool animate = false) {
   ResetLastError();                  //--- Reset error code
   if (ObjectFind(0, objName) < 0) {  //--- Check if object exists
      if (!ObjectCreate(0, objName, OBJ_BUTTON, 0, 0, 0)) { //--- Create button
         LogError(__FUNCTION__ + ": Failed to create button: " + objName + ", Error: " + IntegerToString(GetLastError())); //--- Log error
         return false;                //--- Return failure
      }
      objManager.AddObject(objName);  //--- Add to manager
   }
   ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, x); //--- Set x-coordinate
   ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, y); //--- Set y-coordinate
   ObjectSetInteger(0, objName, OBJPROP_XSIZE, width); //--- Set width
   ObjectSetInteger(0, objName, OBJPROP_YSIZE, height); //--- Set height
   ObjectSetString(0, objName, OBJPROP_TEXT, text); //--- Set text
   ObjectSetInteger(0, objName, OBJPROP_COLOR, textColor); //--- Set text color
   ObjectSetInteger(0, objName, OBJPROP_BGCOLOR, bgColor); //--- Set background color
   ObjectSetInteger(0, objName, OBJPROP_BORDER_COLOR, borderColor); //--- Set border color
   ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, BaseFontSize + (StringFind(objName, "SwitchTFBtn") >= 0 ? 3 : 0)); //--- Set font size
   ObjectSetString(0, objName, OBJPROP_FONT, FontType); //--- Set font
   ObjectSetInteger(0, objName, OBJPROP_ZORDER, 1); //--- Set z-order
   ObjectSetInteger(0, objName, OBJPROP_STATE, false); //--- Reset state

   if (animate && EnableAnimations) { //--- Check for animation
      ObjectSetInteger(0, objName, OBJPROP_BGCOLOR, clrLightGray); //--- Set temporary background
      ChartRedraw(0);                 //--- Redraw chart
      Sleep(50);                      //--- Delay
      ObjectSetInteger(0, objName, OBJPROP_BGCOLOR, bgColor); //--- Set final background
   }

   ChartRedraw(0);                    //--- Redraw chart
   return true;                       //--- Return success
}

//+------------------------------------------------------------------+
//| Create Panel Function                                            |
//+------------------------------------------------------------------+
bool createPanel(string objName, int x, int y, int width, int height, color clr, double opacity = 1.0) {
   ResetLastError();                  //--- Reset error code
   if (ObjectFind(0, objName) < 0) {  //--- Check if object exists
      if (!ObjectCreate(0, objName, OBJ_RECTANGLE_LABEL, 0, 0, 0)) { //--- Create panel
         LogError(__FUNCTION__ + ": Failed to create panel: " + objName + ", Error: " + IntegerToString(GetLastError())); //--- Log error
         return false;                //--- Return failure
      }
      objManager.AddObject(objName);  //--- Add to manager
   }
   ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, x); //--- Set x-coordinate
   ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, y); //--- Set y-coordinate
   ObjectSetInteger(0, objName, OBJPROP_XSIZE, width); //--- Set width
   ObjectSetInteger(0, objName, OBJPROP_YSIZE, height); //--- Set height
   ObjectSetInteger(0, objName, OBJPROP_BGCOLOR, clr); //--- Set background color
   ObjectSetInteger(0, objName, OBJPROP_BORDER_TYPE, BORDER_FLAT); //--- Set border type
   ObjectSetInteger(0, objName, OBJPROP_ZORDER, -1); //--- Set z-order
   ChartRedraw(0);                    //--- Redraw chart
   return true;                       //--- Return success
}

//+------------------------------------------------------------------+
//| Create Line Function                                             |
//+------------------------------------------------------------------+
bool createLine(string objName, int x1, int y1, int x2, int y2, color clrLine, double opacity = 1.0) {
   ResetLastError();                  //--- Reset error code
   if (ObjectFind(0, objName) < 0) {  //--- Check if object exists
      if (!ObjectCreate(0, objName, OBJ_RECTANGLE_LABEL, 0, 0, 0)) { //--- Create line as rectangle
         LogError(__FUNCTION__ + ": Failed to create line: " + objName + ", Error: " + IntegerToString(GetLastError())); //--- Log error
         return false;                //--- Return failure
      }
      objManager.AddObject(objName);  //--- Add to manager
   }
   ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, x1); //--- Set x1
   ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, y1); //--- Set y1
   ObjectSetInteger(0, objName, OBJPROP_XSIZE, x2 - x1); //--- Set width
   ObjectSetInteger(0, objName, OBJPROP_YSIZE, StringFind(objName, "Glow") >= 0 ? 3 : 1); //--- Set height
   ObjectSetInteger(0, objName, OBJPROP_BGCOLOR, clrLine); //--- Set color
   ObjectSetInteger(0, objName, OBJPROP_ZORDER, StringFind(objName, "Glow") >= 0 ? -1 : 0); //--- Set z-order
   ChartRedraw(0);                    //--- Redraw chart
   return true;                       //--- Return success
}

Aquí definimos la función «createText» para generar una etiqueta de texto. Empezamos llamando a ResetLastError para borrar cualquier error anterior. Si el objeto no existe (lo comprobamos mediante «ObjectFind(0, objName) < 0»), lo creamos mediante ObjectCreate con el tipo OBJ_LABEL. Si se produce un error, lo registramos y devolvemos «false». Lo añadimos a «objManager» mediante «AddObject». Establecemos las propiedades: OBJPROP_XDISTANCE en «x», «OBJPROP_YDISTANCE» en «y» y lo mismo con las demás. Si «animate» y «EnableAnimations» son «true», establecemos temporalmente «OBJPROP_COLOR» en «DataColor», volvemos a dibujar, esperamos un tiempo con «Sleep(50)» y, a continuación, lo configuramos con el color del texto. Por último, vuelve a dibujar y devuelve «true».

A continuación, definimos «createButton» de forma similar: restablecemos el error, comprobamos si existe, lo creamos con OBJ_BUTTON si es necesario, registramos el error en caso de fallo y lo añadimos al gestor. A continuación, configuramos las propiedades del objeto y, si las animaciones están activadas, establecemos temporalmente OBJPROP_BGCOLOR en «clrLightGray», volvemos a dibujar, esperamos 50 ms y, a continuación, lo establecemos en «bgColor». Vuelve a dibujar y devuelve «true». Para «createPanel», utilizamos un enfoque similar.

Por último, «createLine» sigue un patrón similar: restablecer, comprobar, crear como OBJ_RECTANGLE_LABEL (simulando una línea), registrar el error en caso de fallo y añadirlo al gestor. Establece «OBJPROP_XDISTANCE» en «x1», «OBJPROP_YDISTANCE» en «y1», «OBJPROP_XSIZE» en «x2-x1», «OBJPROP_YSIZE» en 3 si el nombre contiene «Glow»; en caso contrario, en 1; «OBJPROP_BGCOLOR» en «clrLine»; OBJPROP_ZORDER en -1 si el nombre contiene «Glow»; en caso contrario, en 0. Vuelve a dibujar y devuelve «true». Ahora utilizamos estas funciones para crear la función principal que nos permitirá diseñar el panel de control principal de la siguiente manera.

//+------------------------------------------------------------------+
//| Dashboard Creation Function with Holographic Effects             |
//+------------------------------------------------------------------+
void InitDashboard() {
   // Get chart dimensions
   long chartWidth, chartHeight;      //--- Variables for chart dimensions
   if (!ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0, chartWidth) || !ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, 0, chartHeight)) { //--- Get chart size
      LogError(__FUNCTION__ + ": Failed to get chart dimensions, Error: " + IntegerToString(GetLastError())); //--- Log error
      return;                         //--- Exit on failure
   }
   int fontSize = (int)(BaseFontSize * (chartWidth / 800.0)); //--- Calculate font size
   int cellWidth = PanelWidth / 8;    //--- Calculate cell width
   int cellHeight = 18;               //--- Set cell height
   int panelHeight = 70 + (ArraySize(periods) + 1) * cellHeight + 40 + (totalSymbols + 1) * cellHeight + 50; //--- Calculate panel height

   // Create Dark Panel
   createPanel("DashboardPanel", X_Offset, Y_Offset, PanelWidth, panelHeight, PanelColor); //--- Create dashboard panel

   // Create Header with Glow
   createText("Header", "HOLOGRAPHIC DASHBOARD", X_Offset + 10, Y_Offset + 10, TitleColor, fontSize + 4, FontType); //--- Create header text
   createText("HeaderGlow", "HOLOGRAPHIC DASHBOARD", X_Offset + 11, Y_Offset + 11, GlowColor, fontSize + 4, FontType, true); //--- Create header glow
   createText("SubHeader", StringFormat("%s | TF: %s", _Symbol, TruncPeriod(_Period)), X_Offset + 10, Y_Offset + 30, DataColor, fontSize, FontType); //--- Create subheader

   // Timeframe Grid
   int y = Y_Offset + 50;             //--- Set y-coordinate for timeframe grid
   createText("TF_Label", "Timeframe", X_Offset + 10, y, TitleColor, fontSize, FontType); //--- Create timeframe label
   createText("Trend_Label", "Trend", X_Offset + 10 + cellWidth, y, TitleColor, fontSize, FontType); //--- Create trend label
   createText("Vol_Label", "Vol", X_Offset + 10 + cellWidth * 2, y, TitleColor, fontSize, FontType); //--- Create vol label
   createText("RSI_Label", "RSI", X_Offset + 10 + cellWidth * 3, y, TitleColor, fontSize, FontType); //--- Create RSI label
   createLine("TF_Separator", X_Offset + 5, y + cellHeight + 2, X_Offset + PanelWidth - 5, y + cellHeight + 2, LineColor, 0.6); //--- Create separator line
   createLine("TF_Separator_Glow", X_Offset + 4, y + cellHeight + 1, X_Offset + PanelWidth - 4, y + cellHeight + 3, GlowColor, 0.3); //--- Create glow separator
   if (EnableAnimations) HolographicPulse("TF_Separator", LineColor, GlowColor); //--- Animate separator if enabled

   y += cellHeight + 5;               //--- Update y-coordinate
   for (int i = 0; i < ArraySize(periods); i++) { //--- Iterate through timeframes
      color periodColor = (periods[i] == _Period) ? ActiveColor : DataColor; //--- Set period color
      createText("Period_" + IntegerToString(i), TruncPeriod(periods[i]), X_Offset + 10, y, periodColor, fontSize, FontType); //--- Create period text
      createText("Trend_" + IntegerToString(i), "-", X_Offset + 10 + cellWidth, y, DataColor, fontSize, FontType); //--- Create trend text
      createText("Vol_" + IntegerToString(i), "0.00%", X_Offset + 10 + cellWidth * 2, y, DataColor, fontSize, FontType); //--- Create vol text
      createText("RSI_" + IntegerToString(i), "0.0", X_Offset + 10 + cellWidth * 3, y, DataColor, fontSize, FontType); //--- Create RSI text
      y += cellHeight;                //--- Update y-coordinate
   }

   // Symbol Grid
   y += 30;                           //--- Update y-coordinate for symbol grid
   createText("Symbol_Label", "Symbol", X_Offset + 10, y, TitleColor, fontSize, FontType); //--- Create symbol label
   createText("Bid_Label", "Bid", X_Offset + 10 + cellWidth, y, TitleColor, fontSize, FontType); //--- Create bid label
   createText("Spread_Label", "Spread", X_Offset + 10 + cellWidth * 2, y, TitleColor, fontSize, FontType); //--- Create spread label
   createText("Change_Label", "% Change", X_Offset + 10 + cellWidth * 3, y, TitleColor, fontSize, FontType); //--- Create change label
   createText("Vol_Label_Symbol", "Vol", X_Offset + 10 + cellWidth * 4, y, TitleColor, fontSize, FontType); //--- Create vol label
   createText("RSI_Label_Symbol", "RSI", X_Offset + 10 + cellWidth * 5, y, TitleColor, fontSize, FontType); //--- Create RSI label
   createText("UpArrow_Label", CharToString(236), X_Offset + 10 + cellWidth * 6, y, TitleColor, fontSize, "Wingdings"); //--- Create up arrow label
   createText("DownArrow_Label", CharToString(238), X_Offset + 10 + cellWidth * 7, y, TitleColor, fontSize, "Wingdings"); //--- Create down arrow label
   createLine("Symbol_Separator", X_Offset + 5, y + cellHeight + 2, X_Offset + PanelWidth - 5, y + cellHeight + 2, LineColor, 0.6); //--- Create separator line
   createLine("Symbol_Separator_Glow", X_Offset + 4, y + cellHeight + 1, X_Offset + PanelWidth - 4, y + cellHeight + 3, GlowColor, 0.3); //--- Create glow separator
   if (EnableAnimations) HolographicPulse("Symbol_Separator", LineColor, GlowColor); //--- Animate separator if enabled

   y += cellHeight + 5;               //--- Update y-coordinate
   for (int i = 0; i < totalSymbols; i++) { //--- Iterate through symbols
      string symbol = SymbolName(i, true); //--- Get symbol name
      string displaySymbol = (symbol == _Symbol) ? "*" + symbol : symbol; //--- Format display symbol
      color symbolColor = (symbol == _Symbol) ? ActiveColor : DataColor; //--- Set symbol color
      createText("Symbol_" + IntegerToString(i), displaySymbol, X_Offset + 10, y, symbolColor, fontSize, FontType); //--- Create symbol text
      createText("Bid_" + IntegerToString(i), Bid(symbol), X_Offset + 10 + cellWidth, y, DataColor, fontSize, FontType); //--- Create bid text
      createText("Spread_" + IntegerToString(i), Spread(symbol), X_Offset + 10 + cellWidth * 2, y, DataColor, fontSize, FontType); //--- Create spread text
      createText("Change_" + IntegerToString(i), "0.00%", X_Offset + 10 + cellWidth * 3, y, DataColor, fontSize, FontType); //--- Create change text
      createText("Vol_" + IntegerToString(i), "0.00%", X_Offset + 10 + cellWidth * 4, y, DataColor, fontSize, FontType); //--- Create vol text
      createText("RSI_" + IntegerToString(i), "0.0", X_Offset + 10 + cellWidth * 5, y, DataColor, fontSize, FontType); //--- Create RSI text
      createText("ArrowUp_" + IntegerToString(i), CharToString(236), X_Offset + 10 + cellWidth * 6, y, UpColor, fontSize, "Wingdings"); //--- Create up arrow
      createText("ArrowDown_" + IntegerToString(i), CharToString(238), X_Offset + 10 + cellWidth * 7, y, DownColor, fontSize, "Wingdings"); //--- Create down arrow
      y += cellHeight;                //--- Update y-coordinate
   }

   // Interactive Buttons with Pulse Animation
   createButton("ToggleBtn", "TOGGLE DASHBOARD", X_Offset + 10, y + 20, 150, 25, TitleColor, PanelColor, UpColor); //--- Create toggle button
   createButton("SwitchTFBtn", "NEXT TF", X_Offset + 170, y + 20, 120, 25, UpColor, PanelColor, UpColor); //--- Create switch TF button
   createButton("SortBtn", "SORT: " + sortNames[sortMode], X_Offset + 300, y + 20, 150, 25, TitleColor, PanelColor, UpColor); //--- Create sort button

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

Aquí inicializamos el panel de control recuperando primero las dimensiones del gráfico mediante las variables «chartWidth» y «chartHeight». Para ello, llamamos dos veces a la función ChartGetInteger: una vez con CHART_WIDTH_IN_PIXELS para obtener la anchura y otra con «CHART_HEIGHT_IN_PIXELS» para obtener la altura. A continuación, calculamos el «fontSize» escalando el «BaseFontSize» en función del ancho del gráfico en relación con 800 píxeles y convirtiendo el resultado a un número entero. A continuación, calculamos el valor de «cellWidth» dividiendo «PanelWidth» entre 8 y establecemos «cellHeight» en un valor fijo de 18. El valor de «panelHeight» se calcula sumando 70, el producto de («ArraySize(periods)» + 1) y «cellHeight», 40, el producto de («totalSymbols» + 1) y «cellHeight», y 50; esto tiene en cuenta el diseño general, incluidos los marcos temporales y los símbolos.

A continuación, creamos el fondo oscuro del panel llamando a la función «createPanel» con el nombre «DashboardPanel», situado en las coordenadas «X_Offset» y «Y_Offset», con las dimensiones «PanelWidth» y «panelHeight», y con el color «PanelColor». Para el encabezado, creamos la etiqueta de texto principal «Header» con la cadena «HOLOGRAPHIC DASHBOARD» utilizando la función «createText» en las coordenadas «X_Offset» + 10« y «Y_Offset» + 10», con el estilo «TitleColor», un tamaño de fuente de «fontSize» + 4" y «FontType». Para añadir un efecto de resplandor, creamos otra etiqueta de texto «HeaderGlow» con la misma cadena, pero desplazada 1 píxel tanto en la dirección x como en la y, utilizando «GlowColor», el mismo tamaño de fuente, «FontType», y estableciendo el indicador de opacidad en «true».

A continuación, añadimos una etiqueta de subtítulo «SubHeader», con el formato del símbolo actual _Symbol y el periodo truncado de «TruncPeriod(_Period)» mediante «StringFormat», situada en «X_Offset» + 10« y «Y_Offset» + 30», con el color «DataColor», el «fontSize» y el «FontType».

Pasando a la sección de la cuadrícula temporal, establecemos «y» en «Y_Offset» + 50". Creamos etiquetas para «Timeframe», «Trend», «Vol» y «RSI» utilizando «createText» para cada una de ellas, colocadas horizontalmente con desplazamientos basados en «cellWidth», y todas ellas utilizando «TitleColor», «fontSize» y «FontType». Debajo de estos, dibujamos una línea separadora «TF_Separator» utilizando la función «createLine», desde «X_Offset + 5» hasta «X_Offset + PanelWidth - 5», a una altura de «y + cellHeight + 2», con el color «LineColor» y una opacidad de 0,6. Para el resplandor, añadimos «TF_Separator_Glow» como otra línea ligeramente desplazada y más ancha, con «GlowColor» y una opacidad de 0,3. Si «EnableAnimations» es «true», aplicamos la animación mediante «HolographicPulse» con «LineColor» y «GlowColor». Aplicamos una lógica similar al resto de objetos de etiqueta.

Por último, creamos botones interactivos: «ToggleBtn» con el texto «TOGGLE DASHBOARD» en la posición «X_Offset» + 10", «y + 20», tamaño 150x25, con «TitleColor», «PanelColor» y «UpColor»; «SwitchTFBtn» con el texto «NEXT TF» en la posición «X_Offset» + 170», con la misma coordenada y, tamaño 120x25, con «UpColor», «PanelColor» y «UpColor»; y «SortBtn» como «SORT: » + sortNames[sortMode]» en «X_Offset» + 300", misma y, tamaño 150x25, con «TitleColor», «PanelColor», «UpColor». Concluimos volviendo a dibujar el gráfico con la función «ChartRedraw(0)». Con esta función, podemos llamarla en el controlador de eventos de inicialización y delegar en ella la mayor parte de la inicialización.

//+------------------------------------------------------------------+
//| Expert Initialization Function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   // Clear existing objects
   if (ObjectsDeleteAll(0, -1, -1) < 0) { //--- Delete all objects
      LogError(__FUNCTION__ + ": Failed to delete objects, Error: " + IntegerToString(GetLastError())); //--- Log error
   }
   objManager.DeleteAllObjects();     //--- Delete managed objects

   // Initialize arrays
   totalSymbols = SymbolsTotal(true); //--- Get total symbols
   if (totalSymbols == 0) {           //--- Check for symbols
      LogError(__FUNCTION__ + ": No symbols available"); //--- Log error
      return INIT_FAILED;             //--- Return failure
   }
   ArrayResize(prices_PrevArray, totalSymbols); //--- Resize previous prices array
   ArrayResize(volatility_Array, totalSymbols); //--- Resize volatility array
   ArrayResize(bid_array, totalSymbols); //--- Resize bid array
   ArrayResize(spread_array, totalSymbols); //--- Resize spread array
   ArrayResize(change_array, totalSymbols); //--- Resize change array
   ArrayResize(vol_array, totalSymbols); //--- Resize vol array
   ArrayResize(rsi_array, totalSymbols); //--- Resize RSI array
   ArrayResize(indices, totalSymbols);   //--- Resize indices array
   ArrayResize(atr_handles_sym, totalSymbols); //--- Resize ATR symbol handles
   ArrayResize(rsi_handles_sym, totalSymbols); //--- Resize RSI symbol handles
   ArrayResize(atr_handles_tf, ArraySize(periods)); //--- Resize ATR timeframe handles
   ArrayResize(rsi_handles_tf, ArraySize(periods)); //--- Resize RSI timeframe handles
   ArrayInitialize(prices_PrevArray, 0); //--- Initialize previous prices
   ArrayInitialize(volatility_Array, 0); //--- Initialize volatility

   // Create indicator handles for timeframes
   for (int i = 0; i < ArraySize(periods); i++) { //--- Iterate through timeframes
      atr_handles_tf[i] = iATR(_Symbol, periods[i], ATR_Period); //--- Create ATR handle
      if (atr_handles_tf[i] == INVALID_HANDLE) { //--- Check for invalid handle
         LogError(__FUNCTION__ + ": Failed to create ATR handle for TF " + EnumToString(periods[i])); //--- Log error
         return INIT_FAILED;          //--- Return failure
      }
      rsi_handles_tf[i] = iRSI(_Symbol, periods[i], RSI_Period, PRICE_CLOSE); //--- Create RSI handle
      if (rsi_handles_tf[i] == INVALID_HANDLE) { //--- Check for invalid handle
         LogError(__FUNCTION__ + ": Failed to create RSI handle for TF " + EnumToString(periods[i])); //--- Log error
         return INIT_FAILED;          //--- Return failure
      }
   }

   // Create indicator handles for symbols on H1
   for (int i = 0; i < totalSymbols; i++) { //--- Iterate through symbols
      string symbol = SymbolName(i, true); //--- Get symbol name
      atr_handles_sym[i] = iATR(symbol, PERIOD_H1, ATR_Period); //--- Create ATR handle
      if (atr_handles_sym[i] == INVALID_HANDLE) { //--- Check for invalid handle
         LogError(__FUNCTION__ + ": Failed to create ATR handle for symbol " + symbol); //--- Log error
         return INIT_FAILED;          //--- Return failure
      }
      rsi_handles_sym[i] = iRSI(symbol, PERIOD_H1, RSI_Period, PRICE_CLOSE); //--- Create RSI handle
      if (rsi_handles_sym[i] == INVALID_HANDLE) { //--- Check for invalid handle
         LogError(__FUNCTION__ + ": Failed to create RSI handle for symbol " + symbol); //--- Log error
         return INIT_FAILED;          //--- Return failure
      }
   }

   InitDashboard();                   //--- Initialize dashboard
   dashboardVisible = true;           //--- Set dashboard visible

   return INIT_SUCCEEDED;             //--- Return success
}

En la función OnInit, borramos los objetos existentes mediante la función ObjectsDeleteAll para el gráfico actual, en todas las subventanas y tipos de objeto; si la operación no se realiza correctamente, registramos los errores con «LogError»; y llamamos a «objManager.DeleteAllObjects» para eliminar los elementos gestionados. Obtenemos «totalSymbols» a partir de SymbolsTotal, donde el valor es «true» para los símbolos del mercado en observación; si es cero, se devuelve INIT_FAILED y se registra en el log con «LogError». Cambiamos el tamaño de los arrays como «prices_PrevArray», «volatility_Array», «bid_array», «spread_array», «change_array», «vol_array», «rsi_array», «indices», «atr_handles_sym», «rsi_handles_sym», «atr_handles_tf» y «rsi_handles_tf» para que coincidan con «totalSymbols» o «ArraySize(periods)» utilizando ArrayResize, e inicializamos «prices_PrevArray» y «volatility_Array» a cero con la función ArrayInitialize.

En cuanto a los marcos temporales, recorremos los «periodos» y creamos «atr_handles_tf[i]» con iATR en «_Symbol», «periods[i]» y «ATR_Period», y «rsi_handles_tf[i]» con «iRSI» en _Symbol, «periods[i]», «RSI_Period» y «PRICE_CLOSE», registrando y devolviendo INIT_FAILED si se produce un «INVALID_HANDLE». Del mismo modo, para los símbolos, recorremos «totalSymbols», obtenemos «symbol» con «SymbolName» y «true», creamos «atr_handles_sym[i]» con «iATR» en «symbol», «PERIOD_H1» y «ATR_Period», y «rsi_handles_sym[i]» con iRSI en «symbol», «PERIOD_H1», «RSI_Period» y «PRICE_CLOSE», registrándolo y devolviendo «INIT_FAILED» si no es válido. Llamamos a «InitDashboard» para crear la interfaz de usuario, establecemos «dashboardVisible» en «true» y devolvemos un resultado de éxito. Al ejecutar el programa, obtenemos el siguiente resultado.

PANEL DE CONTROL INICIAL

En la imagen se puede ver que hemos inicializado el programa correctamente. Podemos encargarnos de la desinicialización del programa, en la que tendremos que eliminar los objetos creados y liberar los manejadores de los indicadores.

//+------------------------------------------------------------------+
//| Expert Deinitialization Function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   if (ObjectsDeleteAll(0, -1, -1) < 0) { //--- Delete all objects
      LogError(__FUNCTION__ + ": Failed to delete objects, Error: " + IntegerToString(GetLastError())); //--- Log error
   }
   objManager.DeleteAllObjects();     //--- Delete managed objects

   // Release indicator handles
   for (int i = 0; i < ArraySize(atr_handles_tf); i++) { //--- Iterate through timeframe ATR handles
      if (atr_handles_tf[i] != INVALID_HANDLE) IndicatorRelease(atr_handles_tf[i]); //--- Release handle
      if (rsi_handles_tf[i] != INVALID_HANDLE) IndicatorRelease(rsi_handles_tf[i]); //--- Release handle
   }
   for (int i = 0; i < ArraySize(atr_handles_sym); i++) { //--- Iterate through symbol ATR handles
      if (atr_handles_sym[i] != INVALID_HANDLE) IndicatorRelease(atr_handles_sym[i]); //--- Release handle
      if (rsi_handles_sym[i] != INVALID_HANDLE) IndicatorRelease(rsi_handles_sym[i]); //--- Release handle
   }
}

En el controlador de eventos OnDeinit, liberamos los recursos cuando se elimina el EA. Eliminamos todos los objetos de los gráficos con ObjectsDeleteAll, utilizando el valor -1 para todos los gráficos y tipos, y registramos los errores con «LogError» si el resultado es negativo. Llamamos a «objManager.DeleteAllObjects» para eliminar los elementos gestionados. En el caso de los manejadores de marcos temporales, recorremos «atr_handles_tf» y «rsi_handles_tf» con ArraySize, liberando los manejadores válidos mediante IndicatorRelease si no son «INVALID_HANDLE». Del mismo modo, para los manejadores de símbolos en «atr_handles_sym» y «rsi_handles_sym». Esto garantiza la eliminación completa de los objetos y los indicadores. A continuación se muestra una ilustración.

GIF DE ELIMINACIÓN

Una vez que ya nos hemos ocupado por completo de los objetos creados, podemos pasar ahora a las actualizaciones. Tenemos la intención de realizar las actualizaciones en el controlador de eventos OnTick para que todo sea más sencillo, pero también podrías realizarlas en el controlador de eventos OnTimer. Empecemos primero por la sección dedicada al marco temporal.

//+------------------------------------------------------------------+
//| Expert Tick Function with Holographic Updates                    |
//+------------------------------------------------------------------+
void OnTick() {
   if (!dashboardVisible) return;     //--- Exit if dashboard hidden

   long chartWidth;                   //--- Variable for chart width
   ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0, chartWidth); //--- Get chart width
   int fontSize = (int)(BaseFontSize * (chartWidth / 800.0)); //--- Calculate font size
   int cellWidth = PanelWidth / 8;    //--- Calculate cell width
   int cellHeight = 18;               //--- Set cell height
   int y = Y_Offset + 75;             //--- Set y-coordinate for timeframe data

   // Update Timeframe Data with Pulse
   for (int i = 0; i < ArraySize(periods); i++) { //--- Iterate through timeframes
      double open = iOpen(_Symbol, periods[i], 0); //--- Get open price
      double close = iClose(_Symbol, periods[i], 0); //--- Get close price
      double atr_buf[1];              //--- Buffer for ATR
      if (CopyBuffer(atr_handles_tf[i], 0, 0, 1, atr_buf) != 1) { //--- Copy ATR data
         LogError(__FUNCTION__ + ": Failed to copy ATR buffer for TF " + EnumToString(periods[i])); //--- Log error
         continue;                    //--- Skip on failure
      }
      double vol = (close > 0) ? (atr_buf[0] / close) * 100 : 0.0; //--- Calculate volatility
      double rsi_buf[1];              //--- Buffer for RSI
      if (CopyBuffer(rsi_handles_tf[i], 0, 0, 1, rsi_buf) != 1) { //--- Copy RSI data
         LogError(__FUNCTION__ + ": Failed to copy RSI buffer for TF " + EnumToString(periods[i])); //--- Log error
         continue;                    //--- Skip on failure
      }
      double rsi = rsi_buf[0];        //--- Get RSI value
      color clr = DataColor;          //--- Set default color
      string trend = "-";             //--- Set default trend
      if (rsi > 50) { clr = UpColor; trend = "↑"; } //--- Set up trend
      else if (rsi < 50) { clr = DownColor; trend = "↓"; } //--- Set down trend
      createText("Trend_" + IntegerToString(i), trend, X_Offset + 10 + cellWidth, y, clr, fontSize, FontType, EnableAnimations); //--- Update trend text
      createText("Vol_" + IntegerToString(i), StringFormat("%.2f%%", vol), X_Offset + 10 + cellWidth * 2, y, vol > Vol_Alert_Threshold ? UpColor : DataColor, fontSize, FontType, vol > Vol_Alert_Threshold && EnableAnimations); //--- Update vol text
      color rsi_clr = (rsi > 70 ? DownColor : (rsi < 30 ? UpColor : DataColor)); //--- Set RSI color
      createText("RSI_" + IntegerToString(i), StringFormat("%.1f", rsi), X_Offset + 10 + cellWidth * 3, y, rsi_clr, fontSize, FontType, (rsi > 70 || rsi < 30) && EnableAnimations); //--- Update RSI text
      HolographicPulse("Period_" + IntegerToString(i), (periods[i] == _Period) ? ActiveColor : DataColor, GlowColor); //--- Pulse period text
      y += cellHeight;                //--- Update y-coordinate
   }

   // Update Symbol Data with Advanced Glow
   y += 50;                           //--- Update y-coordinate for symbol data
   for (int i = 0; i < totalSymbols; i++) { //--- Iterate through symbols
      string symbol = SymbolName(i, true); //--- Get symbol name
      double bidPrice;                //--- Variable for bid price
      if (!SymbolInfoDouble(symbol, SYMBOL_BID, bidPrice)) { //--- Get bid price
         LogError(__FUNCTION__ + ": Failed to get bid for " + symbol + ", Error: " + IntegerToString(GetLastError())); //--- Log error
         continue;                    //--- Skip on failure
      }
      long spread;                    //--- Variable for spread
      if (!SymbolInfoInteger(symbol, SYMBOL_SPREAD, spread)) { //--- Get spread
         LogError(__FUNCTION__ + ": Failed to get spread for " + symbol + ", Error: " + IntegerToString(GetLastError())); //--- Log error
         continue;                    //--- Skip on failure
      }
      double change = (prices_PrevArray[i] == 0 ? 0 : (bidPrice - prices_PrevArray[i]) / prices_PrevArray[i] * 100); //--- Calculate change
      double close = iClose(symbol, PERIOD_H1, 0); //--- Get close price
      double atr_buf[1];              //--- Buffer for ATR
      if (CopyBuffer(atr_handles_sym[i], 0, 0, 1, atr_buf) != 1) { //--- Copy ATR data
         LogError(__FUNCTION__ + ": Failed to copy ATR buffer for symbol " + symbol); //--- Log error
         continue;                    //--- Skip on failure
      }
      double vol = (close > 0) ? (atr_buf[0] / close) * 100 : 0.0; //--- Calculate volatility
      double rsi_buf[1];              //--- Buffer for RSI
      if (CopyBuffer(rsi_handles_sym[i], 0, 0, 1, rsi_buf) != 1) { //--- Copy RSI data
         LogError(__FUNCTION__ + ": Failed to copy RSI buffer for symbol " + symbol); //--- Log error
         continue;                    //--- Skip on failure
      }
      double rsi = rsi_buf[0];        //--- Get RSI value
      bid_array[i] = bidPrice;        //--- Store bid
      spread_array[i] = spread;       //--- Store spread
      change_array[i] = change;       //--- Store change
      vol_array[i] = vol;             //--- Store vol
      rsi_array[i] = rsi;             //--- Store RSI
      volatility_Array[i] = vol;      //--- Store volatility
      prices_PrevArray[i] = bidPrice; //--- Update previous price
   }
}

En la función OnTick, gestionamos las actualizaciones con cada tick del mercado, lo que garantiza la actualización de los datos en tiempo real para los marcos temporales y los símbolos. Salimos antes de tiempo si «dashboardVisible» es falso para evitar un procesamiento innecesario. Obtenemos «chartWidth» con ChartGetInteger utilizando CHART_WIDTH_IN_PIXELS, calculamos «fontSize» escalado por «chartWidth / 800,0», «cellWidth» como «PanelWidth / 8» y «cellHeight» como 18. Establecemos «y» en «Y_Offset + 75» para la cuadrícula temporal y recorremos los «periodos» con la función ArraySize. Para cada marco temporal, obtenemos el «precio de apertura» con iOpen y el «precio de cierre» con «iClose» en la posición 0, copiamos «atr_buf» con CopyBuffer desde «atr_handles_tf[i]» y calculamos «vol» como el porcentaje de ATR sobre el «precio de cierre» si es positivo.

Copiamos «rsi_buf» de «rsi_handles_tf[i]» y obtenemos «rsi», estableciendo «clr» y «trend» en función de si el RSI es > 50 para una tendencia alcista («↑» en «UpColor») o < 50 para una tendencia bajista («↓» en «DownColor»). Podríamos haber utilizado las flechas de las fuentes, pero estas, creadas a mano, se integran a la perfección y realzan el efecto holográfico. Actualizamos los textos con «createText» para la tendencia, el volumen (con el color «UpColor» si es mayor que «Vol_Alert_Threshold», con animación) y el RSI (con un color según si está en sobrecompra o sobreventa, con animación), y llamamos a «HolographicPulse» en el texto del período con «ActiveColor» si coincide con _Period. Incrementamos «y» en «cellHeight».

Actualizamos «y» en 50 para la cuadrícula de símbolos y recorremos «totalSymbols». Para cada símbolo de SymbolName cuyo valor sea «true», recuperamos «bidPrice» con «SymbolInfoDouble» utilizando «SYMBOL_BID» y «spread» con SymbolInfoInteger utilizando SYMBOL_SPREAD, registrando el resultado y omitiendo el proceso en caso de error. Calculamos la «variación» como porcentaje respecto a «prices_PrevArray[i]», obtenemos el «cierre» con iClose en «PERIOD_H1» en la posición 0, copiamos «atr_buf» desde «atr_handles_sym[i]» para calcular «vol», y «rsi_buf» de «rsi_handles_sym[i]» para obtener «rsi». Almacenamos los valores en «bid_array», «spread_array», «change_array», «vol_array», «rsi_array» y «volatility_array», y actualizamos «prices_PrevArray[i]» con «bidPrice». Ahora podemos pasar a la sección de símbolos, donde tendremos que ordenarlos y mostrarlos con efectos.

// Sort indices
for (int i = 0; i < totalSymbols; i++) indices[i] = i; //--- Initialize indices
bool swapped = true;               //--- Swap flag
while (swapped) {                  //--- Loop until no swaps
   swapped = false;                //--- Reset flag
   for (int j = 0; j < totalSymbols - 1; j++) { //--- Iterate through indices
      bool do_swap = false;        //--- Swap decision
      int a = indices[j], b = indices[j + 1]; //--- Get indices
      if (sortMode == 0) {         //--- Sort by name ASC
         string na = SymbolName(a, true), nb = SymbolName(b, true); //--- Get names
         if (na > nb) do_swap = true; //--- Swap if needed
      } else if (sortMode == 1) {  //--- Sort by vol DESC
         if (vol_array[a] < vol_array[b]) do_swap = true; //--- Swap if needed
      } else if (sortMode == 2) {  //--- Sort by change ABS DESC
         if (MathAbs(change_array[a]) < MathAbs(change_array[b])) do_swap = true; //--- Swap if needed
      } else if (sortMode == 3) {  //--- Sort by RSI DESC
         if (rsi_array[a] < rsi_array[b]) do_swap = true; //--- Swap if needed
      }
      if (do_swap) {               //--- Perform swap
         int temp = indices[j];    //--- Temporary store
         indices[j] = indices[j + 1]; //--- Swap
         indices[j + 1] = temp;    //--- Complete swap
         swapped = true;           //--- Set flag
      }
   }
}

// Display sorted symbols with pulse on high vol
for (int j = 0; j < totalSymbols; j++) { //--- Iterate through sorted indices
   int i = indices[j];                   //--- Get index
   string symbol = SymbolName(i, true);  //--- Get symbol
   double bidPrice = bid_array[i];       //--- Get bid
   long spread = spread_array[i];        //--- Get spread
   double change = change_array[i];      //--- Get change
   double vol = vol_array[i];            //--- Get vol
   double rsi = rsi_array[i];            //--- Get RSI
   color clr_s = (symbol == _Symbol) ? ActiveColor : DataColor; //--- Set symbol color
   color clr_p = DataColor, clr_sp = DataColor, clr_ch = DataColor, clr_vol = DataColor, clr_rsi = DataColor; //--- Set default colors
   color clr_a1 = DataColor, clr_a2 = DataColor; //--- Set arrow colors

   // Price Change
   if (change > 0) {               //--- Check positive change
      clr_p = UpColor; clr_ch = UpColor; clr_a1 = UpColor; clr_a2 = DataColor; //--- Set up colors
   } else if (change < 0) {        //--- Check negative change
      clr_p = DownColor; clr_ch = DownColor; clr_a1 = DataColor; clr_a2 = DownColor; //--- Set down colors
   }

   // Volatility Alert
   if (vol > Vol_Alert_Threshold) { //--- Check high volatility
      clr_vol = UpColor;            //--- Set vol color
      clr_s = (symbol == _Symbol) ? ActiveColor : UpColor; //--- Set symbol color
   }

   // RSI Color
   clr_rsi = (rsi > 70 ? DownColor : (rsi < 30 ? UpColor : DataColor)); //--- Set RSI color

   // Update Texts
   string displaySymbol = (symbol == _Symbol) ? "*" + symbol : symbol; //--- Format display symbol
   createText("Symbol_" + IntegerToString(j), displaySymbol, X_Offset + 10, y, clr_s, fontSize, FontType, vol > Vol_Alert_Threshold && EnableAnimations); //--- Update symbol text
   createText("Bid_" + IntegerToString(j), Bid(symbol), X_Offset + 10 + cellWidth, y, clr_p, fontSize, FontType, EnableAnimations); //--- Update bid text
   createText("Spread_" + IntegerToString(j), Spread(symbol), X_Offset + 10 + cellWidth * 2, y, clr_sp, fontSize, FontType); //--- Update spread text
   createText("Change_" + IntegerToString(j), StringFormat("%.2f%%", change), X_Offset + 10 + cellWidth * 3, y, clr_ch, fontSize, FontType); //--- Update change text
   createText("Vol_" + IntegerToString(j), StringFormat("%.2f%%", vol), X_Offset + 10 + cellWidth * 4, y, clr_vol, fontSize, FontType, vol > Vol_Alert_Threshold && EnableAnimations); //--- Update vol text
   createText("RSI_" + IntegerToString(j), StringFormat("%.1f", rsi), X_Offset + 10 + cellWidth * 5, y, clr_rsi, fontSize, FontType, (rsi > 70 || rsi < 30) && EnableAnimations); //--- Update RSI text
   createText("ArrowUp_" + IntegerToString(j), CharToString(236), X_Offset + 10 + cellWidth * 6, y, clr_a1, fontSize, "Wingdings"); //--- Update up arrow
   createText("ArrowDown_" + IntegerToString(j), CharToString(238), X_Offset + 10 + cellWidth * 7, y, clr_a2, fontSize, "Wingdings"); //--- Update down arrow

   // Pulse on high volatility
   if (vol > Vol_Alert_Threshold) { //--- Check high volatility
      HolographicPulse("Symbol_" + IntegerToString(j), clr_s, GlowColor); //--- Pulse symbol text
   }

   y += cellHeight;                //--- Update y-coordinate
}

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

Aquí ordenamos los índices inicializando primero el array «indices» desde 0 hasta «totalSymbols» - 1 en un bucle. Utilizamos un algoritmo de ordenación por burbuja con el indicador «swapped» establecido inicialmente en «true», entrando en un bucle «while» hasta que ya no se produzcan más intercambios. En el interior, restablecemos «swapped» a «false» y, a continuación, realizamos un bucle desde 0 hasta «totalSymbols» - 2, estableciendo «do_swap» en «false» y obteniendo «a» y «b» como «indices[j]» e «indices[j+1]». Dependiendo de «sortMode»: para 0 (nombre ASC), obtenemos los nombres mediante «SymbolName(a, true)» y «SymbolName(b, true)», y los intercambiamos si «na > nb»; para 1 (vol DESC), los intercambiamos si «vol_array[a] < vol_array[b]»; para 2 (variación ABS DESC), se intercambian si «MathAbs(change_array[a]) < MathAbs(change_array[b])»; para 3 (RSI DESC), se intercambian si «rsi_array[a] < rsi_array[b]». Si «do_swap», intercambiamos «indices[j]» e «indices[j+1]» utilizando una variable «temp» y establecemos «swapped» en «true».

A continuación, mostramos los símbolos ordenados recorriendo «totalSymbols», asignando «i» a «indices[j]» y, a continuación, obteniendo «symbol» mediante «SymbolName(i, true)», «bidPrice» de «bid_array[i]», «spread» de «spread_array[i]», «change» de «change_array[i]», «vol» de «vol_array[i]» y «rsi» de «rsi_array[i]». Establecemos «clr_s» en «ActiveColor» si coincide con «_Symbol»; en caso contrario, en «DataColor»; el resto de colores se establecen por defecto en «DataColor». Para el cambio de precio: si «change > 0», establece «clr_p», «clr_ch» y «clr_a1» en «UpColor» y «clr_a2» en «DataColor»; si «change < 0», establece «UpColor» en «DownColor» y «clr_a1» en «DataColor». Para la alerta de volatilidad: si «vol > Vol_Alert_Threshold», establece «clr_vol» en «UpColor» y actualiza «clr_s» si no se trata del símbolo actual. Para el RSI: establece «clr_rsi» en «DownColor» si es >70, en «UpColor» si es <30 y, en caso contrario, en «DataColor».

Formateamos «displaySymbol» con «*» si coincide con «_Symbol». Actualizar textos mediante «createText»: símbolo («Symbol_j») con «displaySymbol», «clr_s», animar si el volumen es alto y está habilitado; oferta («Bid_j») con «Bid(symbol)», «clr_p», animada si está habilitada; diferencial («Spread_j») con «Spread(symbol)», «clr_sp»; variación («Change_j») con formato «%.2f%%» mediante «StringFormat», «clr_ch»; vol («Vol_j») con formato «%.2f%%», «clr_vol», animado si el volumen es alto y está activado; RSI («RSI_j») con formato «%.1f», «clr_rsi», animado si hay sobrecompra/sobreventa y está habilitado; flecha hacia arriba («ArrowUp_j») con «CharToString(236)», «clr_a1», «Wingdings»; flecha hacia abajo («ArrowDown_j») con «CharToString(238)», «clr_a2», «Wingdings». Si el volumen es alto, aplica «HolographicPulse» al texto del símbolo con «clr_s» y «GlowColor». Incrementa «y» en «cellHeight» en cada iteración y, finalmente, vuelve a dibujar. Al compilar, obtenemos el siguiente resultado.

ACTUALIZACIONES CON PULSO EN ONTICK

En la visualización se puede observar que las actualizaciones se aplican con cada tick del mercado. Ahora podemos dar un paso más y dar vida a los botones que hemos creado. Lo conseguiremos mediante el controlador de eventos OnChartEvent.

//+------------------------------------------------------------------+
//| Chart Event Handler                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) {
   if (id == CHARTEVENT_OBJECT_CLICK) { //--- Handle click event
      if (sparam == "ToggleBtn") {    //--- Check toggle button
         dashboardVisible = !dashboardVisible; //--- Toggle visibility
         objManager.DeleteAllObjects(); //--- Delete objects
         if (dashboardVisible) {      //--- Check if visible
            InitDashboard();          //--- Reinitialize dashboard
         } else {
            createButton("ToggleBtn", "TOGGLE DASHBOARD", X_Offset + 10, Y_Offset + 10, 150, 25, TitleColor, PanelColor, UpColor); //--- Create toggle button
         }
      }
      else if (sparam == "SwitchTFBtn") { //--- Check switch TF button
         int currentIdx = -1;         //--- Initialize current index
         for (int i = 0; i < ArraySize(periods); i++) { //--- Find current timeframe
            if (periods[i] == _Period) { //--- Match found
               currentIdx = i;        //--- Set index
               break;                 //--- Exit loop
            }
         }
         int nextIdx = (currentIdx + 1) % ArraySize(periods); //--- Calculate next index
         if (!ChartSetSymbolPeriod(0, _Symbol, periods[nextIdx])) { //--- Switch timeframe
            LogError(__FUNCTION__ + ": Failed to switch timeframe, Error: " + IntegerToString(GetLastError())); //--- Log error
         }
         createButton("SwitchTFBtn", "NEXT TF", X_Offset + 170, (int)ObjectGetInteger(0, "SwitchTFBtn", OBJPROP_YDISTANCE), 120, 25, UpColor, PanelColor, UpColor, EnableAnimations); //--- Update button
      }
      else if (sparam == "SortBtn") { //--- Check sort button
         sortMode = (sortMode + 1) % 4; //--- Cycle sort mode
         createButton("SortBtn", "SORT: " + sortNames[sortMode], X_Offset + 300, (int)ObjectGetInteger(0, "SortBtn", OBJPROP_YDISTANCE), 150, 25, TitleColor, PanelColor, UpColor, EnableAnimations); //--- Update button
      }
      ObjectSetInteger(0, sparam, OBJPROP_STATE, false); //--- Reset button state
      ChartRedraw(0);                 //--- Redraw chart
   }
}

Implementamos el controlador de eventos OnChartEvent para gestionar los eventos interactivos, respondiendo a los clics en los botones para activar o desactivar la visibilidad, cambiar de marco temporal y alternar entre los modos de ordenación. Para CHARTEVENT_OBJECT_CLICK, comprobamos «sparam» con respecto a «ToggleBtn», alternamos el estado de «dashboardVisible», eliminamos los objetos con «objManager.DeleteAllObjects», y reiniciamos con «InitDashboard» si está visible o creamos un nuevo «ToggleBtn» con «createButton» si está oculto. Si «sparam» es «SwitchTFBtn», buscamos el índice del marco temporal actual en «periods» mediante un bucle, calculamos «nextIdx» como «(currentIdx + 1) % ArraySize(periods)», cambiamos el gráfico con «ChartSetSymbolPeriod» utilizando «periods[nextIdx]», registramos los errores con «LogError» y actualizamos el botón con «createButton», incluyendo una animación si «EnableAnimations» está activado.

Para «SortBtn», hacemos un bucle con «sortMode» mediante «(sortMode + 1) % 4» y actualizamos el texto del botón a «SORT: » + «sortNames[sortMode]» utilizando «createButton» con animación. Restablecemos el estado del botón con ObjectSetInteger para OBJPROP_STATE a «false» y volvemos a dibujar el gráfico con la función ChartRedraw. Esto permite controlar la visualización del panel de control y la organización de los datos. Tras la compilación, obtenemos el siguiente resultado.

CLIC EN BOTONES ADAPTATIVOS

Podemos observar que podemos actualizar el panel de control con cada tick del mercado y responder a los clics en los botones para activar o desactivar el panel, cambiar el marco temporal y ordenar los índices de las métricas de los símbolos, logrando así nuestros objetivos. Ahora solo queda comprobar el funcionamiento del proyecto, y eso se aborda en la siguiente sección.


Pruebas retrospectivas (Backtesting)

Hemos realizado las pruebas y, a continuación, se muestra la visualización compilada en una única imagen Graphics Interchange Format (GIF).

BACKTESTING


Conclusión

En conclusión, hemos creado un panel de control holográfico dinámico en MQL5 que supervisa símbolos y marcos temporales mediante el RSI, alertas de volatilidad y funciones de ordenación, y que cuenta con animaciones de pulso y botones interactivos para ofrecer una experiencia de negociación inmersiva. Hemos detallado la arquitectura y la implementación, utilizando componentes de clase como «CObjectManager» y funciones como «HolographicPulse» para ofrecer información en tiempo real y visualmente atractiva. Puedes personalizar este panel de control para adaptarlo a tus necesidades de negociación, mejorando tu análisis con efectos holográficos y controles.

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

Archivos adjuntos |
Redes neuronales en el trading: entrenamiento de metaparámetros basado en la heterogeneidad (HimNet) Redes neuronales en el trading: entrenamiento de metaparámetros basado en la heterogeneidad (HimNet)
Te invitamos a conocer el framework HimNet, que combina la flexibilidad de la adaptación espaciotemporal con una alta eficiencia computacional, lo que permite obtener pronósticos precisos y estables en series temporales financieras. En el artículo se muestra en detalle cómo interactúan entre sí sus componentes clave, convirtiendo algoritmos complejos en una arquitectura manejable.
Entrenamiento de un U-Transformer no lineal sobre los residuos de un modelo autorregresivo lineal Entrenamiento de un U-Transformer no lineal sobre los residuos de un modelo autorregresivo lineal
El artículo presenta un innovador sistema híbrido para la predicción de tipos de cambio que combina un modelo autorregresivo lineal con la arquitectura U-Transformer para el análisis de los residuos. El sistema alterna automáticamente entre las fuentes de señales en función de su calidad e incorpora una lógica de negociación completa con estrategias de averaging (promediación de posiciones) y pyramiding (piramidación de posiciones). La ventaja clave de este enfoque radica en que la red neuronal se entrena a partir de los residuos del modelo lineal, lo que simplifica la tarea y reduce el riesgo de sobreajuste. La implementación se ha realizado íntegramente en MQL5 y está lista para su uso en transacciones reales, con adaptación automática a las condiciones cambiantes del mercado.
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.
Redes neuronales en el trading: Modelo de consultas temporales (Final) Redes neuronales en el trading: Modelo de consultas temporales (Final)
Les presentamos la fase final de la implementación y las pruebas del framework TQNet, en la que la teoría se une a la práctica real de trading. Recorreremos todo el proceso, desde el entrenamiento histórico hasta la prueba de estrés con datos recientes del mercado, evaluando la solidez y la precisión del modelo. Los resultados finales no son solo cifras frías, sino también una demostración clara del valor práctico del enfoque propuesto.