English Русский 中文 Deutsch 日本語
preview
Operando con el Calendario Económico MQL5 (Parte 9): Mejorando la interacción con noticias mediante una barra dinámica y un diseño pulido

Operando con el Calendario Económico MQL5 (Parte 9): Mejorando la interacción con noticias mediante una barra dinámica y un diseño pulido

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

Introducción

En este artículo, mejoramos la serie del Calendario Económico de MQL5 mediante la introducción de una barra de desplazamiento vertical dinámica y una presentación optimizada, con el fin de mejorar la interacción de los operadores con las noticias, garantizando una navegación intuitiva y una presentación fiable de los eventos. Partiendo del backtesting optimizado y el filtrado inteligente de la Parte 8, nos centramos ahora en una interfaz de usuario (UI) adaptativa con una barra de desplazamiento que indica visualmente los elementos en los que se puede hacer clic, lo que agiliza el acceso a las noticias económicas tanto en entornos en tiempo real como de backtesting. Estructuramos el artículo con los siguientes temas:

  1. Cómo crear una barra de desplazamiento dinámica para explorar las noticias sin esfuerzo
  2. Implementación en MQL5
  3. Pruebas y validación
  4. Conclusión

¡Analicemos estas mejoras!


Cómo crear una barra de desplazamiento dinámica para explorar las noticias sin esfuerzo

Para mejorar nuestra interacción con el Calendario Económico de MQL5, proponemos una barra de desplazamiento dinámica como elemento fundamental para una navegación intuitiva por las noticias. Tenemos previsto diseñar una barra de desplazamiento vertical adaptativa que indique visualmente los estados en los que se puede hacer clic, junto con un sistema robusto de almacenamiento de eventos para garantizar un acceso fluido a todas las noticias filtradas, transformando así el panel de control en una herramienta ágil y centrada en el operador. Eliminaremos la limitación del número de eventos mostrables y enumeraremos todos los eventos filtrados, de modo que podamos desplazarnos por todas las noticias cuando sea necesario, eliminando así la restricción de visualizar únicamente un conjunto de eventos priorizados. Así es como lo lograremos:

  • Diseño dinámico de la barra de desplazamiento: Implementaremos una barra de desplazamiento con iconos que cambian de color: negro cuando se puede hacer clic y gris claro cuando no se puede, lo que proporciona una respuesta visual inmediata para facilitar la navegación por listas de eventos extensas.
  • Almacenamiento fiable de eventos: Desarrollaremos un sistema para almacenar todos los eventos filtrados, garantizando que se pueda acceder a todas las noticias mediante la barra de desplazamiento, eliminando las limitaciones de visualización y asegurando una visión completa.
  • Mecanismo de actualización eficiente: Optimizaremos el panel de control para que solo se actualice cuando cambien los filtros, aparezcan nuevos eventos o se realice un desplazamiento, lo que garantizará una experiencia fluida y sin distracciones.
  • Mejoras en la interfaz de usuario (UI): Mejoraremos la interfaz con ajustes precisos en el diseño, garantizando que la barra de desplazamiento y la visualización de eventos se integren a la perfección para lograr un diseño profesional y fácil de usar.

Esta hoja de ruta estratégica sentará las bases para un panel de control que nos permitirá explorar las noticias económicas sin esfuerzo. La barra de desplazamiento dinámica será la clave para desbloquear una experiencia de trading mejorada. En resumen, esta es una representación visual de lo que pretendemos lograr.

PLAN VISUAL


Implementación en MQL5

Para implementar las mejoras en MQL5, primero tendremos que definir los objetos de barra de desplazamiento adicionales mediante la directiva #define, junto con sus constantes de diseño, algunas variables para realizar un seguimiento del estado de desplazamiento y los eventos que se van a procesar; a continuación, utilizaremos matrices para almacenar los datos de los eventos, que se procesarán de forma fluida, tal y como se muestra a continuación.

// Scrollbar UI elements
#define SCROLL_UP_REC "SCROLL_UP_REC"
#define SCROLL_UP_LABEL "SCROLL_UP_LABEL"
#define SCROLL_DOWN_REC "SCROLL_DOWN_REC"
#define SCROLL_DOWN_LABEL "SCROLL_DOWN_LABEL"
#define SCROLL_LEADER "SCROLL_LEADER"
#define SCROLL_SLIDER "SCROLL_SLIDER"

//---- Scrollbar layout constants
#define LIST_X 62
#define LIST_Y 162
#define LIST_WIDTH 716
#define LIST_HEIGHT 286
#define VISIBLE_ITEMS 11
#define ITEM_HEIGHT 26
#define SCROLLBAR_X (LIST_X + LIST_WIDTH + 2) // 780
#define SCROLLBAR_Y LIST_Y
#define SCROLLBAR_WIDTH 20
#define SCROLLBAR_HEIGHT LIST_HEIGHT // 286
#define BUTTON_SIZE 15
#define BUTTON_WIDTH (SCROLLBAR_WIDTH - 2)
#define BUTTON_OFFSET_X 1
#define SCROLL_AREA_HEIGHT (SCROLLBAR_HEIGHT - 2 * BUTTON_SIZE)
#define SLIDER_MIN_HEIGHT 20
#define SLIDER_WIDTH 18
#define SLIDER_OFFSET_X 1

//---- Event name tracking
string current_eventNames_data[];
string previous_eventNames_data[];
string last_dashboard_eventNames[];
string previous_displayable_eventNames[];
string current_displayable_eventNames[];
datetime last_dashboard_update = 0;

//---- Filter flags
bool enableCurrencyFilter = true;
bool enableImportanceFilter = true;
bool enableTimeFilter = true;
bool isDashboardUpdate = true;
bool filters_changed = true;

//---- Scrollbar flags and variables
bool scroll_visible = false;
bool moving_state_slider = false;
int scroll_pos = 0;
int prev_scroll_pos = -1; // Track previous scroll position
int mlb_down_x = 0;
int mlb_down_y = 0;
int mlb_down_yd_slider = 0;
int prev_mouse_state = 0;
int slider_height = SLIDER_MIN_HEIGHT;

//---- Event counters
int totalEvents_Considered = 0;
int totalEvents_Filtered = 0;
int totalEvents_Displayable = 0;

//---- Global arrays for events
EconomicEvent allEvents[];
EconomicEvent filteredEvents[];
EconomicEvent displayableEvents[];

En este punto, sentamos las bases para la barra de desplazamiento dinámica y la elegante visualización de eventos del Calendario Económico de MQL5 mediante la definición de constantes, variables y matrices esenciales de la interfaz de usuario, con el fin de permitir una navegación intuitiva y una gestión eficiente de los eventos. Definimos los componentes de la barra de desplazamiento utilizando constantes como «SCROLL_UP_LABEL», «SCROLL_DOWN_LABEL», «SCROLL_UP_REC» y «SCROLL_DOWN_REC», que identifican los elementos gráficos de los botones de desplazamiento hacia arriba y hacia abajo de la barra de desplazamiento.

Las constantes de diseño como «LIST_X» (62), «LIST_Y» (162), «LIST_WIDTH» (716) y «LIST_HEIGHT» (286) definen el área de visualización de eventos, mientras que «SCROLLBAR_X» (780), «SCROLLBAR_Y» (162), «SCROLLBAR_WIDTH» (20) y «SCROLLBAR_HEIGHT» (286) posicionan la barra de desplazamiento con precisión; por su parte, «VISIBLE_ITEMS» (11) y «ITEM_HEIGHT» (26) garantizan que se muestren 11 eventos, cada uno de 26 píxeles de altura, y «BUTTON_SIZE» (15) y «SLIDER_WIDTH» (18) dan forma a los botones y al control deslizante compactos.

Para gestionar los eventos y las interacciones, declaramos matrices como «current_displayable_eventNames» y «previous_displayable_eventNames» con el fin de realizar un seguimiento de los nombres de los eventos para la detección de cambios, lo que permite realizar actualizaciones silenciosas, y «last_dashboard_update» para registrar la fecha y hora de las actualizaciones del panel de control. Los indicadores de filtro «enableCurrencyFilter», «enableImportanceFilter» y «enableTimeFilter» (todos en «true») controlan la selección de eventos, mientras que «isDashboardUpdate» y «filters_changed» determinan cuándo se producen las actualizaciones. Las variables de la barra de desplazamiento, entre las que se incluyen «scroll_visible», «scroll_pos», «prev_scroll_pos» y «moving_state_slider», registran la visibilidad y la posición, mientras que «mlb_down_x», «mlb_down_y» y «slider_height» permiten la función de arrastre del control deslizante.

Utilizamos los contadores «totalEvents_Considered», «totalEvents_Filtered» y «totalEvents_Displayable» para supervisar el procesamiento de eventos, y las matrices «allEvents», «filteredEvents» y «displayableEvents» para almacenar los datos de los eventos, lo que garantiza que todas las noticias filtradas sean navegables y sienta las bases para una interfaz de operador receptiva. A continuación, podemos crear primero los elementos de la barra de desplazamiento utilizando las funciones de creación existentes y, después, ajustar el tamaño y la posición del rectángulo principal para que la barra de desplazamiento quede situada a la derecha.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   // Enable mouse move events for scrollbar
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);

   // Create dashboard UI
   createRecLabel(MAIN_REC,50,50,740+13,410,clrSeaGreen,1);
   createRecLabel(SUB_REC1,50+3,50+30,740-3-3+13,410-30-3,clrWhite,1);
   createRecLabel(SUB_REC2,50+3+5,50+30+50+27,740-3-3-5-5,410-30-3-50-27-10+5,clrGreen,1);
   createLabel(HEADER_LABEL,50+3+5,50+5,"MQL5 Economic Calendar",clrWhite,15);

   //---

}

Aquí, en el controlador de eventos OnInit, utilizamos la función ChartSetInteger para establecer el evento de movimiento del ratón en «true», de modo que podamos controlar la prioridad de desplazamiento del gráfico principal y de nuestros objetos personalizados. Esto nos permitirá desplazar la barra de desplazamiento vertical y sus elementos, posibilitando un movimiento y una transición fluidos. A continuación, ajustamos el ancho del rectángulo principal y del subrectángulo 1 sumándoles 13 píxeles, para que también puedan albergar la barra de desplazamiento vertical. Ampliamos la altura del subrectángulo 2 en 5 píxeles para dar cabida a los 11 eventos de manera eficiente, eliminando el efecto de desbordamiento. Tras la compilación, obtenemos el siguiente resultado.

AJUSTES DE ESPACIO

En la imagen podemos ver todos los ajustes realizados para adaptar el espacio, etiquetados del 1 al 3. El cambio 1 indica el desplazamiento del botón de cancelar hasta el borde del panel, luego el cambio 2 muestra el ajuste del ancho de los rectángulos principales para alojar el botón de cancelación y la barra de desplazamiento, y luego el cambio 3 muestra el ajuste de la altura del rectángulo del panel de control para evitar el desbordamiento de la última fila de elementos. Ahora podemos proceder a definir y crear la barra de desplazamiento dentro del espacio creado. Sin embargo, dado que queremos que el control deslizante sea dinámico y se muestre solo cuando sea necesario, tendremos que definir algunas funciones para habilitarlo.

//+------------------------------------------------------------------+
//| Calculate slider height                                          |
//+------------------------------------------------------------------+
int calculateSliderHeight() {
   if (totalEvents_Filtered <= VISIBLE_ITEMS)
      return SCROLL_AREA_HEIGHT;
   double visible_ratio = (double)VISIBLE_ITEMS / totalEvents_Filtered;
   int height = (int)::floor(SCROLL_AREA_HEIGHT * visible_ratio);
   return MathMax(SLIDER_MIN_HEIGHT, MathMin(height, SCROLL_AREA_HEIGHT));
}

//+------------------------------------------------------------------+
//| Update slider position                                           |
//+------------------------------------------------------------------+
void updateSliderPosition() {
   int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
   if (max_scroll <= 0) return;
   double scroll_ratio = (double)scroll_pos / max_scroll;
   int scroll_area_y_min = SCROLLBAR_Y + BUTTON_SIZE;
   int scroll_area_y_max = scroll_area_y_min + SCROLL_AREA_HEIGHT - slider_height;
   int new_y = scroll_area_y_min + (int)(scroll_ratio * (scroll_area_y_max - scroll_area_y_min));
   ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE, new_y);
   if (debugLogging) Print("Slider moved to y=", new_y);
   ChartRedraw(0);
}

//+------------------------------------------------------------------+
//| Update button colors based on scroll position                    |
//+------------------------------------------------------------------+
void updateButtonColors() {
   int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
   if (scroll_pos == 0) {
      ObjectSetInteger(0, SCROLL_UP_LABEL, OBJPROP_COLOR, clrLightGray);
   } else {
      ObjectSetInteger(0, SCROLL_UP_LABEL, OBJPROP_COLOR, clrBlack);
   }
   if (scroll_pos >= max_scroll) {
      ObjectSetInteger(0, SCROLL_DOWN_LABEL, OBJPROP_COLOR, clrLightGray);
   } else {
      ObjectSetInteger(0, SCROLL_DOWN_LABEL, OBJPROP_COLOR, clrBlack);
   }
   ChartRedraw(0);
}

Mejoramos la barra de desplazamiento dinámica del Calendario Económico de MQL5 mediante la implementación de tres funciones clave: «calculateSliderHeight», «updateSliderPosition» y «updateButtonColors», para garantizar una navegación intuitiva y una respuesta visual clara. En la función «calculateSliderHeight», calculamos la altura del control deslizante de la barra de desplazamiento para representar visualmente la proporción de eventos visibles con respecto al total de eventos filtrados.

Comprobamos si «totalEvents_Filtered» es menor o igual que «VISIBLE_ITEMS» (11) y, si todos los eventos caben en una sola vista, devolvemos «SCROLL_AREA_HEIGHT» (256 píxeles) para rellenar el área de desplazamiento. De lo contrario, calculamos un «visible_ratio» dividiendo «VISIBLE_ITEMS» entre «totalEvents_Filtered», lo multiplicamos por «SCROLL_AREA_HEIGHT» y utilizamos la función floor para obtener un valor entero «height». A continuación, devolvemos el valor máximo entre «SLIDER_MIN_HEIGHT» (20 píxeles) y el valor mínimo entre «height» y «SCROLL_AREA_HEIGHT», lo que garantiza que el control deslizante tenga el tamaño adecuado para la escala de la lista de eventos.

En la función «updateSliderPosition», colocamos el control deslizante de la barra de desplazamiento de manera que refleje la posición actual de desplazamiento dentro de la lista de eventos. Calculamos «max_scroll» como la diferencia entre «ArraySize(displayableEvents)» y «VISIBLE_ITEMS», utilizando MathMax para garantizar que el resultado sea no negativo, y salimos si «max_scroll» es cero (no es necesario desplazarse). Calculamos un «scroll_ratio» dividiendo «scroll_pos» entre «max_scroll», definimos el rango vertical del control deslizante con «scroll_area_y_min» («SCROLLBAR_Y» + «BUTTON_SIZE») y «scroll_area_y_max» («scroll_area_y_min» + «SCROLL_AREA_HEIGHT» - «slider_height»), y calculamos una posición «new_y» mediante la interpolación de «scroll_ratio» dentro de este rango.

A continuación, utilizamos ObjectSetInteger para establecer el valor de «OBJPROP_YDISTANCE» de «SCROLL_SLIDER» en «new_y», registramos el movimiento si «debugLogging» es verdadero y llamamos a ChartRedraw para actualizar la visualización.

En la función «updateButtonColors», actualizamos dinámicamente los colores de los iconos de los botones de desplazamiento hacia arriba y hacia abajo para indicar si se pueden pulsar, mejorando así la respuesta visual al usuario. Calculamos «max_scroll» tal y como se hace en «updateSliderPosition» y comprobamos «scroll_pos» para determinar el estado de «SCROLL_UP_LABEL» y «SCROLL_DOWN_LABEL». Si «scroll_pos» es 0, establecemos «SCROLL_UP_LABEL» y «OBJPROP_COLOR» en «clrLightGray» (no clicable, en la parte superior); en caso contrario, lo establecemos en «clrBlack» (clicable). De manera similar, si «scroll_pos» es igual o superior a «max_scroll», establecemos «SCROLL_DOWN_LABEL» en «clrLightGray» (no clicable, en la parte inferior); de lo contrario, lo establecemos en «clrBlack» (clicable).

Concluimos llamando a «ChartRedraw» para actualizar el gráfico, asegurándonos de que los iconos guíen visualmente la navegación. Ahora podemos crear la barra de desplazamiento de forma dinámica al actualizar los valores del panel de control, tal y como se muestra a continuación.

// Update TIME_LABEL
string timeText = updateServerTime ? "Server Time: "+TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS) : "Server Time: Static";
updateLabel(TIME_LABEL,timeText+"   |||   Total News: "+IntegerToString(totalEvents_Filtered)+"/"+IntegerToString(totalEvents_Considered));

// Update scrollbar visibility
bool new_scroll_visible = totalEvents_Filtered > VISIBLE_ITEMS;
if (new_scroll_visible != scroll_visible || events_changed || filters_changed) {
   scroll_visible = new_scroll_visible;
   if (debugLogging) Print("Scrollbar visibility: ", scroll_visible ? "Visible" : "Hidden");
   if (scroll_visible) {
      if (ObjectFind(0, SCROLL_LEADER) < 0) {
         createRecLabel(SCROLL_LEADER, SCROLLBAR_X, SCROLLBAR_Y, SCROLLBAR_WIDTH, SCROLLBAR_HEIGHT, clrSilver, 1, clrNONE);
         int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
         color up_color = (scroll_pos == 0) ? clrLightGray : clrBlack;
         color down_color = (scroll_pos >= max_scroll) ? clrLightGray : clrBlack;
         createRecLabel(SCROLL_UP_REC, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
         createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-5, CharToString(0x35), up_color, 15, "Webdings");
         int down_y = SCROLLBAR_Y + SCROLLBAR_HEIGHT - BUTTON_SIZE;
         createRecLabel(SCROLL_DOWN_REC, SCROLLBAR_X + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
         createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings");
         slider_height = calculateSliderHeight();
         int slider_y = SCROLLBAR_Y + BUTTON_SIZE;
         createButton(SCROLL_SLIDER, SCROLLBAR_X + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, 12, clrLightSlateGray, clrDarkGray, "Arial Bold");
         ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_WIDTH, 2);
         if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height);
      }
      updateSliderPosition();
      updateButtonColors();
   } else {
      ObjectDelete(0, SCROLL_LEADER);
      ObjectDelete(0, SCROLL_UP_REC);
      ObjectDelete(0, SCROLL_UP_LABEL);
      ObjectDelete(0, SCROLL_DOWN_REC);
      ObjectDelete(0, SCROLL_DOWN_LABEL);
      ObjectDelete(0, SCROLL_SLIDER);
      if (debugLogging) Print("Scrollbar removed: totalEvents_Filtered=", totalEvents_Filtered);
   }
}

Mejoramos la interfaz de usuario mediante la implementación de actualizaciones clave en la visualización de la hora y la barra de desplazamiento dinámica del panel de control, lo que garantiza una interacción y una navegación fluidas por las noticias. Actualizamos el campo «TIME_LABEL» para que refleje la hora del servidor en tiempo real y las estadísticas de eventos, lo que proporciona información clara sobre el estado del panel de control. Además, gestionamos la visibilidad de la barra de desplazamiento e inicializamos sus componentes con colores de iconos dinámicos, creando un sistema de navegación intuitivo que mejora nuestra experiencia.

En primer lugar, nos centramos en actualizar el «TIME_LABEL» para estar al tanto de la hora actual y del estado del procesamiento de eventos. Creamos una cadena «timeText» mediante una expresión condicional: si «updateServerTime» es verdadero, llamamos a la función TimeToString con TimeCurrent y los indicadores «TIME_DATE|TIME_SECONDS» para dar formato a la hora del servidor; de lo contrario, la establecemos en «Hora del servidor: Estática». A continuación, utilizamos la función «updateLabel» para establecer el texto de «TIME_LABEL» como «timeText», concatenado con un separador (« ||| ») y los recuentos de eventos formateados mediante IntegerToString para «totalEvents_Filtered» y «totalEvents_Considered», lo que da como resultado una visualización del tipo «Hora del servidor: 01/03/2025 12:00:00 ||| Noticias totales: 1711/3000», que muestra claramente los eventos filtrados frente al total, eliminando la limitación de las noticias mostrables que existía en versiones anteriores.

A continuación, implementamos la lógica de visibilidad de la barra de desplazamiento y la creación de componentes para garantizar que aparezca solo cuando sea necesario y proporcione información visual. Determinamos el valor de «new_scroll_visible» comprobando si «totalEvents_Filtered» supera a «VISIBLE_ITEMS» (11), lo que indica que hay más eventos de los que se pueden mostrar a la vez. Si «new_scroll_visible» difiere de «scroll_visible», o si «events_changed» o «filters_changed» es verdadero, actualizamos «scroll_visible» y registramos el estado con Print si «debugLogging» está habilitado. Cuando «scroll_visible» es verdadero, utilizamos ObjectFind para comprobar si existe «SCROLL_LEADER» y, en caso contrario, creamos la barra de desplazamiento: llamamos a «createRecLabel» para «SCROLL_LEADER», «SCROLL_UP_REC» y «SCROLL_DOWN_REC» en las posiciones definidas por «SCROLLBAR_X», «SCROLLBAR_Y», «BUTTON_OFFSET_X» y «BUTTON_SIZE», utilizando «clrSilver» y «clrDarkGray».

Calculamos «max_scroll» con MathMax y «ArraySize(displayableEvents)» menos «VISIBLE_ITEMS», establecemos «up_color» y «down_color» en «clrLightGray» o «clrBlack» en función de «scroll_pos» y «max_scroll», y creamos «SCROLL_UP_LABEL» y «SCROLL_DOWN_LABEL» con «createLabel», utilizando CharToString para las flechas de desplazamiento web, concretamente 5 y 6, tal y como se muestra a continuación.

FUENTES WEB

Por si se pregunta por qué hemos utilizado «0x35» en lugar de simplemente 5, se trata del formato binario hexadecimal en base 16. Obtendrá los mismos resultados si utiliza el 5, pero como una cadena de caracteres, por ejemplo, «5». Si desea utilizar el código de caracteres ASCII, también puede convertir el 53 directamente en una cadena, por ejemplo, mediante CharToString(53), y obtendrá los mismos resultados. Existen varias opciones disponibles, por lo que la elección queda a su criterio. Aquí está la información del código.

HEXADECIMAL «0x35» COMO 5

Calculamos «slider_height» mediante «calculateSliderHeight», creamos «SCROLL_SLIDER» con «createButton» en «slider_y», establecemos su «OBJPROP_WIDTH» en 2 y registramos los detalles si «debugLogging» es verdadero. Llamamos a «updateSliderPosition» y «updateButtonColors» para inicializar los colores del control deslizante y de los iconos. Si «scroll_visible» es falso, eliminamos los objetos de la barra de desplazamiento utilizando ObjectDelete para «SCROLL_LEADER», «SCROLL_UP_REC», «SCROLL_DOWN_REC», «SCROLL_UP_LABEL», «SCROLL_DOWN_LABEL» y «SCROLL_SLIDER», registrando la eliminación para mantener una interfaz limpia cuando no se requiere el desplazamiento. El resto de la función que contiene esta lógica, así como la encargada de almacenar los eventos, es el siguiente.

//+------------------------------------------------------------------+
//| Update dashboard values                                          |
//+------------------------------------------------------------------+
void update_dashboard_values(string &curr_filter_array[], ENUM_CALENDAR_EVENT_IMPORTANCE &imp_filter_array[]) {
   totalEvents_Considered = 0;
   totalEvents_Filtered = 0;
   totalEvents_Displayable = 0;
   ArrayFree(current_eventNames_data);
   ArrayFree(current_displayable_eventNames);

   datetime timeRange = PeriodSeconds(range_time);
   datetime timeBefore = TimeTradeServer() - timeRange;
   datetime timeAfter = TimeTradeServer() + timeRange;

   // Populate displayableEvents
   if (MQLInfoInteger(MQL_TESTER)) {
      if (filters_changed) {
         FilterEventsForTester();
         ArrayFree(displayableEvents); // Clear displayableEvents on filter change
      }
      int eventIndex = 0;
      for (int i = 0; i < ArraySize(filteredEvents); i++) {
         totalEvents_Considered++;
         datetime eventDateTime = filteredEvents[i].eventDateTime;
         if (eventDateTime < StartDate || eventDateTime > EndDate) {
            if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to date range.");
            continue;
         }

         bool timeMatch = !enableTimeFilter;
         if (enableTimeFilter) {
            if (eventDateTime <= TimeTradeServer() && eventDateTime >= timeBefore) timeMatch = true;
            else if (eventDateTime >= TimeTradeServer() && eventDateTime <= timeAfter) timeMatch = true;
         }
         if (!timeMatch) {
            if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to time filter.");
            continue;
         }

         bool currencyMatch = !enableCurrencyFilter;
         if (enableCurrencyFilter) {
            for (int j = 0; j < ArraySize(curr_filter_array); j++) {
               if (filteredEvents[i].currency == curr_filter_array[j]) {
                  currencyMatch = true;
                  break;
               }
            }
         }
         if (!currencyMatch) {
            if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to currency filter.");
            continue;
         }

         bool importanceMatch = !enableImportanceFilter;
         if (enableImportanceFilter) {
            string imp_str = filteredEvents[i].importance;
            ENUM_CALENDAR_EVENT_IMPORTANCE event_imp = (imp_str == "None") ? CALENDAR_IMPORTANCE_NONE :
                                                      (imp_str == "Low") ? CALENDAR_IMPORTANCE_LOW :
                                                      (imp_str == "Medium") ? CALENDAR_IMPORTANCE_MODERATE :
                                                      CALENDAR_IMPORTANCE_HIGH;
            for (int k = 0; k < ArraySize(imp_filter_array); k++) {
               if (event_imp == imp_filter_array[k]) {
                  importanceMatch = true;
                  break;
               }
            }
         }
         if (!importanceMatch) {
            if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to importance filter.");
            continue;
         }

         ArrayResize(displayableEvents, eventIndex + 1);
         displayableEvents[eventIndex] = filteredEvents[i];
         ArrayResize(current_displayable_eventNames, eventIndex + 1);
         current_displayable_eventNames[eventIndex] = filteredEvents[i].event;
         eventIndex++;
      }
      totalEvents_Filtered = ArraySize(displayableEvents);
      if (debugLogging) Print("Tester mode: Stored ", totalEvents_Filtered, " displayable events.");
   } else {
      MqlCalendarValue values[];
      datetime startTime = TimeTradeServer() - PeriodSeconds(start_time);
      datetime endTime = TimeTradeServer() + PeriodSeconds(end_time);
      int allValues = CalendarValueHistory(values,startTime,endTime,NULL,NULL);
      int eventIndex = 0;
      if (filters_changed) ArrayFree(displayableEvents); // Clear displayableEvents on filter change
      for (int i = 0; i < allValues; i++) {
         MqlCalendarEvent event;
         CalendarEventById(values[i].event_id, event);
         MqlCalendarCountry country;
         CalendarCountryById(event.country_id, country);
         MqlCalendarValue value;
         CalendarValueById(values[i].id, value);
         totalEvents_Considered++;

         bool currencyMatch = false;
         if (enableCurrencyFilter) {
            for (int j = 0; j < ArraySize(curr_filter_array); j++) {
               if (country.currency == curr_filter_array[j]) {
                  currencyMatch = true;
                  break;
               }
            }
            if (!currencyMatch) continue;
         }

         bool importanceMatch = false;
         if (enableImportanceFilter) {
            for (int k = 0; k < ArraySize(imp_filter_array); k++) {
               if (event.importance == imp_filter_array[k]) {
                  importanceMatch = true;
                  break;
               }
            }
            if (!importanceMatch) continue;
         }

         bool timeMatch = false;
         if (enableTimeFilter) {
            datetime eventTime = values[i].time;
            if (eventTime <= TimeTradeServer() && eventTime >= timeBefore) timeMatch = true;
            else if (eventTime >= TimeTradeServer() && eventTime <= timeAfter) timeMatch = true;
            if (!timeMatch) continue;
         }

         ArrayResize(displayableEvents, eventIndex + 1);
         displayableEvents[eventIndex].eventDate = TimeToString(values[i].time,TIME_DATE);
         displayableEvents[eventIndex].eventTime = TimeToString(values[i].time,TIME_MINUTES);
         displayableEvents[eventIndex].currency = country.currency;
         displayableEvents[eventIndex].event = event.name;
         displayableEvents[eventIndex].importance = (event.importance == CALENDAR_IMPORTANCE_NONE) ? "None" :
                                                   (event.importance == CALENDAR_IMPORTANCE_LOW) ? "Low" :
                                                   (event.importance == CALENDAR_IMPORTANCE_MODERATE) ? "Medium" : "High";
         displayableEvents[eventIndex].actual = value.GetActualValue();
         displayableEvents[eventIndex].forecast = value.GetForecastValue();
         displayableEvents[eventIndex].previous = value.GetPreviousValue();
         displayableEvents[eventIndex].eventDateTime = values[i].time;
         ArrayResize(current_displayable_eventNames, eventIndex + 1);
         current_displayable_eventNames[eventIndex] = event.name;
         eventIndex++;
      }
      totalEvents_Filtered = ArraySize(displayableEvents);
      if (debugLogging) Print("Live mode: Stored ", totalEvents_Filtered, " displayable events.");
   }

   // Check for changes in displayable events
   bool events_changed = isChangeInStringArrays(previous_displayable_eventNames, current_displayable_eventNames);
   bool scroll_changed = (scroll_pos != prev_scroll_pos);
   if (events_changed || filters_changed || scroll_changed) {
      if (debugLogging) {
         if (events_changed) Print("Changes detected in displayable events.");
         if (filters_changed) Print("Filter changes detected.");
         if (scroll_changed) Print("Scroll position changed: ", prev_scroll_pos, " -> ", scroll_pos);
      }
      ArrayFree(previous_displayable_eventNames);
      ArrayCopy(previous_displayable_eventNames, current_displayable_eventNames);
      prev_scroll_pos = scroll_pos;

      // Clear and redraw UI
      ObjectsDeleteAll(0, DATA_HOLDERS);
      ObjectsDeleteAll(0, ARRAY_NEWS);

      int startY = LIST_Y;
      int start_idx = scroll_visible ? scroll_pos : 0;
      int end_idx = MathMin(start_idx + VISIBLE_ITEMS, ArraySize(displayableEvents));
      for (int i = start_idx; i < end_idx; i++) {
         totalEvents_Displayable++;
         color holder_color = (totalEvents_Displayable % 2 == 0) ? C'213,227,207' : clrWhite;
         createRecLabel(DATA_HOLDERS+string(totalEvents_Displayable),LIST_X,startY-1,LIST_WIDTH,ITEM_HEIGHT+1,holder_color,1,clrNONE);

         int startX = LIST_X + 3;
         string news_data[ArraySize(array_calendar)];
         news_data[0] = displayableEvents[i].eventDate;
         news_data[1] = displayableEvents[i].eventTime;
         news_data[2] = displayableEvents[i].currency;
         color importance_color = clrBlack;
         if (displayableEvents[i].importance == "Low") importance_color = clrYellow;
         else if (displayableEvents[i].importance == "Medium") importance_color = clrOrange;
         else if (displayableEvents[i].importance == "High") importance_color = clrRed;
         news_data[3] = ShortToString(0x25CF);
         news_data[4] = displayableEvents[i].event;
         news_data[5] = DoubleToString(displayableEvents[i].actual, 3);
         news_data[6] = DoubleToString(displayableEvents[i].forecast, 3);
         news_data[7] = DoubleToString(displayableEvents[i].previous, 3);

         for (int k = 0; k < ArraySize(array_calendar); k++) {
            if (k == 3) {
               createLabel(ARRAY_NEWS+IntegerToString(i)+" "+array_calendar[k],startX,startY-(22-12),news_data[k],importance_color,22,"Calibri");
            } else {
               createLabel(ARRAY_NEWS+IntegerToString(i)+" "+array_calendar[k],startX,startY,news_data[k],clrBlack,12,"Calibri");
            }
            startX += buttons[k]+3;
         }

         ArrayResize(current_eventNames_data, ArraySize(current_eventNames_data)+1);
         current_eventNames_data[ArraySize(current_eventNames_data)-1] = displayableEvents[i].event;
         startY += ITEM_HEIGHT;
      }

      if (debugLogging) Print("Displayed ", totalEvents_Displayable, " events, start_idx=", start_idx, ", end_idx=", end_idx);
   } else {
      if (debugLogging) Print("No changes detected. Skipping redraw.");
   }

   // Update TIME_LABEL
   string timeText = updateServerTime ? "Server Time: "+TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS) : "Server Time: Static";
   updateLabel(TIME_LABEL,timeText+"   |||   Total News: "+IntegerToString(totalEvents_Filtered)+"/"+IntegerToString(totalEvents_Considered));

   // Update scrollbar visibility
   bool new_scroll_visible = totalEvents_Filtered > VISIBLE_ITEMS;
   if (new_scroll_visible != scroll_visible || events_changed || filters_changed) {
      scroll_visible = new_scroll_visible;
      if (debugLogging) Print("Scrollbar visibility: ", scroll_visible ? "Visible" : "Hidden");
      if (scroll_visible) {
         if (ObjectFind(0, SCROLL_LEADER) < 0) {
            createRecLabel(SCROLL_LEADER, SCROLLBAR_X, SCROLLBAR_Y, SCROLLBAR_WIDTH, SCROLLBAR_HEIGHT, clrSilver, 1, clrNONE);
            int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
            color up_color = (scroll_pos == 0) ? clrLightGray : clrBlack;
            color down_color = (scroll_pos >= max_scroll) ? clrLightGray : clrBlack;
            createRecLabel(SCROLL_UP_REC, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-5, CharToString(0x35), up_color, 15, "Webdings");
            int down_y = SCROLLBAR_Y + SCROLLBAR_HEIGHT - BUTTON_SIZE;
            createRecLabel(SCROLL_DOWN_REC, SCROLLBAR_X + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings");
            slider_height = calculateSliderHeight();
            int slider_y = SCROLLBAR_Y + BUTTON_SIZE;
            createButton(SCROLL_SLIDER, SCROLLBAR_X + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, 12, clrLightSlateGray, clrDarkGray, "Arial Bold");
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_WIDTH, 2);
            if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height);
         }
         updateSliderPosition();
         updateButtonColors();
      } else {
         ObjectDelete(0, SCROLL_LEADER);
         ObjectDelete(0, SCROLL_UP_REC);
         ObjectDelete(0, SCROLL_UP_LABEL);
         ObjectDelete(0, SCROLL_DOWN_REC);
         ObjectDelete(0, SCROLL_DOWN_LABEL);
         ObjectDelete(0, SCROLL_SLIDER);
         if (debugLogging) Print("Scrollbar removed: totalEvents_Filtered=", totalEvents_Filtered);
      }
   }

   if (isChangeInStringArrays(previous_eventNames_data, current_eventNames_data)) {
      if (debugLogging) Print("CHANGES IN EVENT NAMES DETECTED.");
      ArrayFree(previous_eventNames_data);
      ArrayCopy(previous_eventNames_data, current_eventNames_data);
   }
}

En este punto, perfeccionamos el panel de control en la función «update_dashboard_values», incorporando una nueva lógica para garantizar una visualización optimizada de los eventos y una funcionalidad dinámica de la barra de desplazamiento, al tiempo que damos prioridad a las actualizaciones silenciosas en aras de la eficiencia. Aprovechamos la matriz «displayableEvents» para almacenar todos los eventos filtrados, implementamos la detección de cambios para evitar actualizaciones innecesarias de la pantalla e integramos la barra de desplazamiento para facilitar una navegación intuitiva, con lo que se superan las limitaciones anteriores y se mejora la interacción con los operadores. Comenzamos restableciendo a cero los contadores «totalEvents_Considered», «totalEvents_Filtered» y «totalEvents_Displayable», y borrando las matrices «current_eventNames_data» y «current_displayable_eventNames» mediante ArrayFree para prepararnos para los datos de eventos actualizados.

Sin alterar la lógica de filtrado existente para los modos de prueba y de producción, introducimos la matriz «displayableEvents» para almacenar todos los eventos filtrados, lo que garantiza un acceso completo (por ejemplo, 1711 eventos), a diferencia del problema de visualización de solo 5 o 6 eventos que presentaba la versión original. En el modo de prueba, llamamos a «FilterEventsForTester» si «filters_changed» es verdadero, borramos «displayableEvents» con «ArrayFree» y recorremos «filteredEvents», aplicando los filtros («enableTimeFilter», «enableCurrencyFilter», «enableImportanceFilter») para rellenar «displayableEvents» y «current_displayable_eventNames», estableciendo «totalEvents_Filtered» en «ArraySize(displayableEvents)».

En el modo en vivo, utilizamos CalendarValueHistory para recuperar eventos, borramos «displayableEvents» si «filters_changed» y almacenamos los eventos filtrados de forma similar, registrando el recuento si «debugLogging» está habilitado.

Implementamos actualizaciones silenciosas utilizando «isChangeInStringArrays» para comparar «previous_displayable_eventNames» con «current_displayable_eventNames», estableciendo «events_changed» y comprobando si «scroll_pos» difiere de «prev_scroll_pos» para «scroll_changed». Si «events_changed», «filters_changed» o «scroll_changed» es verdadero, registramos los cambios mediante «Print» si «debugLogging» está habilitado, actualizamos «previous_displayable_eventNames» con ArrayCopy y establecemos «prev_scroll_pos». Borramos los elementos de la interfaz de usuario utilizando ObjectsDeleteAll para «DATA_HOLDERS» y «ARRAY_NEWS»; a continuación, dibujamos hasta «VISIBLE_ITEMS» (11) eventos de «displayableEvents», comenzando en «start_idx» (basándonos en «scroll_pos» si «scroll_visible») hasta «end_idx» (limitado por «ArraySize(displayableEvents)»).

Para cada evento, llamamos a «createRecLabel» para obtener un fondo con colores alternos («C'213,227,207» o «clrWhite»), rellenamos «news_data» con los detalles del evento y utilizamos «createLabel» para mostrar los campos, ajustando «importance_color» (por ejemplo, «clrYellow» para «Bajo») y registrando el recuento de visualizaciones si «debugLogging» es verdadero. Si no se producen cambios, omitimos el redibujado y registramos esta información, manteniendo así el rendimiento.

Integramos la barra de desplazamiento estableciendo «new_scroll_visible» si «totalEvents_Filtered» supera a «VISIBLE_ITEMS», actualizando «scroll_visible» si cambia o si «events_changed» o «filters_changed» es verdadero, y creando componentes («SCROLL_LEADER», «SCROLL_UP_REC», «SCROLL_UP_LABEL», «SCROLL_DOWN_REC», «SCROLL_DOWN_LABEL», «SCROLL_SLIDER») con «createRecLabel», «createLabel» y «createButton», utilizando «clrBlack» o «clrLightGray» para los iconos en función de «scroll_pos» y «max_scroll». Eliminamos los componentes con ObjectDelete cuando ya no son necesarios y actualizamos «previous_eventNames_data» si «isChangeInStringArrays» detecta cambios en «current_eventNames_data», lo que garantiza un panel de control robusto, navegable y eficiente. Dado que hemos creado nuevos objetos, debemos eliminarlos como parte del panel principal.

//+------------------------------------------------------------------+
//| Destroy dashboard                                                |
//+------------------------------------------------------------------+
void destroy_Dashboard() {
   ObjectDelete(0,"MAIN_REC");
   ObjectDelete(0,"SUB_REC1");
   ObjectDelete(0,"SUB_REC2");
   ObjectDelete(0,"HEADER_LABEL");
   ObjectDelete(0,"TIME_LABEL");
   ObjectDelete(0,"IMPACT_LABEL");
   ObjectsDeleteAll(0,"ARRAY_CALENDAR");
   ObjectsDeleteAll(0,"ARRAY_NEWS");
   ObjectsDeleteAll(0,"DATA_HOLDERS");
   ObjectsDeleteAll(0,"IMPACT_LABEL");
   ObjectDelete(0,"FILTER_LABEL");
   ObjectDelete(0,"FILTER_CURR_BTN");
   ObjectDelete(0,"FILTER_IMP_BTN");
   ObjectDelete(0,"FILTER_TIME_BTN");
   ObjectDelete(0,"CANCEL_BTN");
   ObjectsDeleteAll(0,"CURRENCY_BTNS");
   ObjectDelete(0, SCROLL_LEADER);
   ObjectDelete(0, SCROLL_UP_REC);
   ObjectDelete(0, SCROLL_UP_LABEL);
   ObjectDelete(0, SCROLL_DOWN_REC);
   ObjectDelete(0, SCROLL_DOWN_LABEL);
   ObjectDelete(0, SCROLL_SLIDER);
   ArrayFree(displayableEvents);
   ArrayFree(current_displayable_eventNames);
   ArrayFree(previous_displayable_eventNames);
   ChartRedraw(0);
}

Para eliminar los nuevos objetos creados, basta con llamar a la función ObjectDelete y pasar sus nombres, respectivamente, para garantizar que se eliminen al borrar el panel de control, ya que ahora forman parte de él. Tras la compilación, obtenemos el siguiente resultado.

Menos de 11 eventos filtrados o iguales.

EVENTOS DE PRUEBA

Más de 11 eventos filtrados.

EVENTOS INAPROPIADOS

Rango de eventos ampliamente filtrados.

AMPLIA GAMA DE EVENTOS

En las imágenes anteriores, podemos ver que creamos la barra de desplazamiento del calendario de forma dinámica en función de los eventos disponibles. Lo que debemos hacer ahora es dar vida a los elementos de la barra de desplazamiento, y podemos lograrlo utilizando la función OnChartEvent de la siguiente manera.

//+------------------------------------------------------------------+
//| Chart event handler                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) {
   int mouse_x = (int)lparam;
   int mouse_y = (int)dparam;
   int mouse_state = (int)sparam;

   if (id == CHARTEVENT_OBJECT_CLICK) {

      // Scrollbar button clicks
      if (scroll_visible && (sparam == SCROLL_UP_REC || sparam == SCROLL_UP_LABEL)) {
         scrollUp();
         updateButtonColors();
         if (debugLogging) Print("Up button clicked (", sparam, "). CurrPos: ", scroll_pos);
         ChartRedraw(0);
      }
      if (scroll_visible && (sparam == SCROLL_DOWN_REC || sparam == SCROLL_DOWN_LABEL)) {
         scrollDown();
         updateButtonColors();
         if (debugLogging) Print("Down button clicked (", sparam, "). CurrPos: ", scroll_pos);
         ChartRedraw(0);
      }
   }
   else if (id == CHARTEVENT_MOUSE_MOVE && scroll_visible) {
      if (prev_mouse_state == 0 && mouse_state == 1) {
         int xd = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_XDISTANCE);
         int yd = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE);
         int xs = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_XSIZE);
         int ys = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE);
         if (mouse_x >= xd && mouse_x <= xd + xs && mouse_y >= yd && mouse_y <= yd + ys) {
            moving_state_slider = true;
            mlb_down_x = mouse_x;
            mlb_down_y = mouse_y;
            mlb_down_yd_slider = yd;
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_BGCOLOR, clrDodgerBlue);
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height + 2);
            ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
            if (debugLogging) Print("Slider drag started at y=", mouse_y);
         }
      }
      if (moving_state_slider && mouse_state == 1) {
         int delta_y = mouse_y - mlb_down_y;
         int new_y = mlb_down_yd_slider + delta_y;
         int scroll_area_y_min = SCROLLBAR_Y + BUTTON_SIZE;
         int scroll_area_y_max = scroll_area_y_min + SCROLL_AREA_HEIGHT - slider_height;
         new_y = MathMax(scroll_area_y_min, MathMin(new_y, scroll_area_y_max));
         ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE, new_y);
         int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
         double scroll_ratio = (double)(new_y - scroll_area_y_min) / (scroll_area_y_max - scroll_area_y_min);
         int new_scroll_pos = (int)MathRound(scroll_ratio * max_scroll);
         if (new_scroll_pos != scroll_pos) {
            scroll_pos = new_scroll_pos;
            update_dashboard_values(curr_filter_selected, imp_filter_selected);
            updateButtonColors();
            if (debugLogging) Print("Slider dragged. CurrPos: ", scroll_pos, ", Total steps: ", max_scroll, ", Slider y=", new_y);
         }
         ChartRedraw(0);
      }
      if (mouse_state == 0) {
         if (moving_state_slider) {
            moving_state_slider = false;
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_BGCOLOR, clrLightSlateGray);
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height);
            ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
            if (debugLogging) Print("Slider drag stopped.");
            ChartRedraw(0);
         }
      }
      prev_mouse_state = mouse_state;
   }
}

En este caso, mejoramos la interactividad mediante la implementación de la nueva lógica relacionada con la barra de desplazamiento en la función OnChartEvent, lo que nos permite navegar por las noticias de forma fluida mediante clics y arrastres. Nos centramos en gestionar las interacciones del usuario con la barra de desplazamiento dinámica, procesando específicamente los clics en los botones de subir y bajar y los movimientos del ratón para arrastrar el control deslizante, garantizando así actualizaciones rápidas de la pantalla del panel de control. Procesamos los eventos CHARTEVENT_OBJECT_CLICK para gestionar los clics en los botones de la barra de desplazamiento cuando «scroll_visible» es verdadero. Si el objeto seleccionado («sparam») es «SCROLL_UP_REC» o «SCROLL_UP_LABEL», llamamos a la función «scrollUp» para decrementar «scroll_pos», seguido de «updateButtonColors» para actualizar los colores de los iconos («clrBlack» o «clrLightGray») en función de la nueva posición, registramos la acción con «Print» si «debugLogging» está habilitado, y llamamos a ChartRedraw para actualizar la visualización.

Del mismo modo, cuando se hace clic en «SCROLL_DOWN_REC» o «SCROLL_DOWN_LABEL», invocamos «scrollDown» para incrementar «scroll_pos», llamamos a «updateButtonColors», registramos el evento y actualizamos el gráfico con «ChartRedraw», asegurándonos de que el panel de control refleje la lista de eventos desplazada. Hemos definido las funciones y explicaremos la lógica más adelante.

En los eventos CHARTEVENT_MOUSE_MOVE en los que «scroll_visible» es verdadero, gestionamos el arrastre del control deslizante. Cuando «prev_mouse_state» es 0 y «mouse_state» es 1, utilizamos ObjectGetInteger para recuperar la posición («OBJPROP_XDISTANCE», «OBJPROP_YDISTANCE») y el tamaño («OBJPROP_XSIZE», «OBJPROP_YSIZE») como «xd», «yd», «xs» y «ys». Si las coordenadas del ratón («mouse_x», «mouse_y») se encuentran dentro de los límites del control deslizante, establecemos «moving_state_slider» en «true», almacenamos «mlb_down_x», «mlb_down_y» y «mlb_down_yd_slider», cambiamos «OBJPROP_BGCOLOR» de «SCROLL_SLIDER» a «clrDodgerBlue» y aumentamos «OBJPROP_YSIZE» en 2, desactivamos el desplazamiento del gráfico con ChartSetInteger y registramos el inicio del arrastre si «debugLogging» está habilitado.

Mientras «moving_state_slider» y «mouse_state» sean verdaderos, calculamos «delta_y» como «mouse_y» menos «mlb_down_y», calculamos «new_y» dentro de los límites «scroll_area_y_min» («SCROLLBAR_Y» + «BUTTON_SIZE») y «scroll_area_y_max» («scroll_area_y_min» + «SCROLL_AREA_HEIGHT» - «slider_height») utilizando MathMax y MathMin, y establecemos «OBJPROP_YDISTANCE» de «SCROLL_SLIDER» en «new_y». Calculamos «new_scroll_pos» a partir de la «scroll_ratio» de «new_y» dentro del rango de desplazamiento y, si difiere de «scroll_pos», actualizamos «scroll_pos», llamamos a «update_dashboard_values» con «curr_filter_selected» e «imp_filter_selected», invocamos «updateButtonColors», registramos los detalles del arrastre y llamamos a ChartRedraw.

Cuando «mouse_state» es 0, restablecemos «moving_state_slider», restablecemos «OBJPROP_BGCOLOR» de «SCROLL_SLIDER» a «clrLightSlateGray» y «OBJPROP_YSIZE» a «slider_height», volvemos a habilitar el desplazamiento del gráfico, registramos el fin del arrastre y llamamos a «ChartRedraw», garantizando así una interacción fluida con el control deslizante. Las funciones encargadas de la lógica de desplazamiento son las siguientes.

//+------------------------------------------------------------------+
//| Scroll up                                                        |
//+------------------------------------------------------------------+
void scrollUp() {
   if (scroll_pos > 0) {
      scroll_pos--;
      update_dashboard_values(curr_filter_selected, imp_filter_selected);
      updateSliderPosition();
      if (debugLogging) Print("Scrolled up. CurrPos: ", scroll_pos);
   } else {
      if (debugLogging) Print("Cannot scroll up further. Already at top.");
   }
}

//+------------------------------------------------------------------+
//| Scroll down                                                      |
//+------------------------------------------------------------------+
void scrollDown() {
   int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
   if (scroll_pos < max_scroll) {
      scroll_pos++;
      update_dashboard_values(curr_filter_selected, imp_filter_selected);
      updateSliderPosition();
      if (debugLogging) Print("Scrolled down. CurrPos: ", scroll_pos);
   } else {
      if (debugLogging) Print("Cannot scroll down further. Max scroll reached: ", max_scroll);
   }
}
//+------------------------------------------------------------------+

Aquí, en la función «scrollUp», facilitamos la navegación hacia arriba por la lista de eventos. Comprobamos si «scroll_pos» es mayor que 0, lo que indica que es posible desplazarse hacia arriba. Si es cierto, decrementamos «scroll_pos», llamamos a «update_dashboard_values» con «curr_filter_selected» e «imp_filter_selected» para actualizar los eventos mostrados, invocamos «updateSliderPosition» para ajustar la posición de «SCROLL_SLIDER» y registramos el nuevo «scroll_pos» utilizando Print si «debugLogging» está habilitado. Si «scroll_pos» es 0, registramos un mensaje que indica que se ha llegado al principio de la lista, lo que evita actualizaciones innecesarias y mantiene un flujo de interacción fluido.

En la función «scrollDown», permitimos la navegación hacia abajo por la lista de eventos. Calculamos «max_scroll» utilizando MathMax para garantizar que el valor sea no negativo; dicho valor se obtiene a partir de «ArraySize(displayableEvents)» menos «VISIBLE_ITEMS» (11), y representa la posición máxima a la que se puede desplazar.

Si «scroll_pos» es menor que «max_scroll», incrementamos «scroll_pos», llamamos a «update_dashboard_values» con «curr_filter_selected» e «imp_filter_selected» para actualizar los eventos mostrados, utilizamos «updateSliderPosition» para reposicionar el «SCROLL_SLIDER» y registramos el nuevo «scroll_pos» con «Print» si «debugLogging» está habilitado. Si «scroll_pos» es igual o superior a «max_scroll», registramos un mensaje que indica que se ha llegado al final de la lista, con lo que se evitan actualizaciones innecesarias y se garantiza una navegación intuitiva. Para garantizar una interacción fluida, llamamos a las funciones definidas tanto en los controladores de eventos OnInit como en los de OnTick.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   UpdateFilterInfo();
   CheckForNewsTrade();
   if (isDashboardUpdate) {
      if (MQLInfoInteger(MQL_TESTER)) {
         datetime currentTime = TimeTradeServer();
         datetime timeRange = PeriodSeconds(range_time);
         datetime timeAfter = currentTime + timeRange;
         if (filters_changed || last_dashboard_update < timeAfter) {
            update_dashboard_values(curr_filter_selected, imp_filter_selected);
            ArrayFree(last_dashboard_eventNames);
            ArrayCopy(last_dashboard_eventNames, current_eventNames_data);
            last_dashboard_update = currentTime;
         }
      } else {
         update_dashboard_values(curr_filter_selected, imp_filter_selected);
      }
   }
}

//+------------------------------------------------------------------+
//| Chart event handler                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) {
   int mouse_x = (int)lparam;
   int mouse_y = (int)dparam;
   int mouse_state = (int)sparam;

   if (id == CHARTEVENT_OBJECT_CLICK) {
      UpdateFilterInfo();
      CheckForNewsTrade();
      if (sparam == CANCEL_BTN) {
         isDashboardUpdate = false;
         destroy_Dashboard();
      }
      if (sparam == FILTER_CURR_BTN) {
         bool btn_state = ObjectGetInteger(0,sparam,OBJPROP_STATE);
         enableCurrencyFilter = btn_state;
         if (debugLogging) Print(sparam+" STATE = "+(string)btn_state+", FLAG = "+(string)enableCurrencyFilter);
         string filter_curr_text = enableCurrencyFilter ? ShortToString(0x2714)+"Currency" : ShortToString(0x274C)+"Currency";
         color filter_curr_txt_color = enableCurrencyFilter ? clrLime : clrRed;
         ObjectSetString(0,FILTER_CURR_BTN,OBJPROP_TEXT,filter_curr_text);
         ObjectSetInteger(0,FILTER_CURR_BTN,OBJPROP_COLOR,filter_curr_txt_color);
         if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
         update_dashboard_values(curr_filter_selected,imp_filter_selected);
         if (debugLogging) Print("Success. Changes updated! State: "+(string)enableCurrencyFilter);
         ChartRedraw(0);
      }
      if (sparam == FILTER_IMP_BTN) {
         bool btn_state = ObjectGetInteger(0,sparam,OBJPROP_STATE);
         enableImportanceFilter = btn_state;
         if (debugLogging) Print(sparam+" STATE = "+(string)btn_state+", FLAG = "+(string)enableImportanceFilter);
         string filter_imp_text = enableImportanceFilter ? ShortToString(0x2714)+"Importance" : ShortToString(0x274C)+"Importance";
         color filter_imp_txt_color = enableImportanceFilter ? clrLime : clrRed;
         ObjectSetString(0,FILTER_IMP_BTN,OBJPROP_TEXT,filter_imp_text);
         ObjectSetInteger(0,FILTER_IMP_BTN,OBJPROP_COLOR,filter_imp_txt_color);
         if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
         update_dashboard_values(curr_filter_selected,imp_filter_selected);
         if (debugLogging) Print("Success. Changes updated! State: "+(string)enableImportanceFilter);
         ChartRedraw(0);
      }
      if (sparam == FILTER_TIME_BTN) {
         bool btn_state = ObjectGetInteger(0,sparam,OBJPROP_STATE);
         enableTimeFilter = btn_state;
         if (debugLogging) Print(sparam+" STATE = "+(string)btn_state+", FLAG = "+(string)enableTimeFilter);
         string filter_time_text = enableTimeFilter ? ShortToString(0x2714)+"Time" : ShortToString(0x274C)+"Time";
         color filter_time_txt_color = enableTimeFilter ? clrLime : clrRed;
         ObjectSetString(0,FILTER_TIME_BTN,OBJPROP_TEXT,filter_time_text);
         ObjectSetInteger(0,FILTER_TIME_BTN,OBJPROP_COLOR,filter_time_txt_color);
         if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
         update_dashboard_values(curr_filter_selected,imp_filter_selected);
         if (debugLogging) Print("Success. Changes updated! State: "+(string)enableTimeFilter);
         ChartRedraw(0);
      }
      if (StringFind(sparam,CURRENCY_BTNS) >= 0) {
         string selected_curr = ObjectGetString(0,sparam,OBJPROP_TEXT);
         if (debugLogging) Print("BTN NAME = ",sparam,", CURRENCY = ",selected_curr);
         bool btn_state = ObjectGetInteger(0,sparam,OBJPROP_STATE);
         if (btn_state == false) {
            if (debugLogging) Print("BUTTON IS IN UN-SELECTED MODE.");
            for (int i = 0; i < ArraySize(curr_filter_selected); i++) {
               if (curr_filter_selected[i] == selected_curr) {
                  for (int j = i; j < ArraySize(curr_filter_selected) - 1; j++) {
                     curr_filter_selected[j] = curr_filter_selected[j + 1];
                  }
                  ArrayResize(curr_filter_selected, ArraySize(curr_filter_selected) - 1);
                  if (debugLogging) Print("Removed from selected filters: ", selected_curr);
                  break;
               }
            }
         } else {
            if (debugLogging) Print("BUTTON IS IN SELECTED MODE. TAKE ACTION");
            bool already_selected = false;
            for (int j = 0; j < ArraySize(curr_filter_selected); j++) {
               if (curr_filter_selected[j] == selected_curr) {
                  already_selected = true;
                  break;
               }
            }
            if (!already_selected) {
               ArrayResize(curr_filter_selected, ArraySize(curr_filter_selected) + 1);
               curr_filter_selected[ArraySize(curr_filter_selected) - 1] = selected_curr;
               if (debugLogging) Print("Added to selected filters: ", selected_curr);
            } else {
               if (debugLogging) Print("Currency already selected: ", selected_curr);
            }
         }
         if (debugLogging) Print("SELECTED ARRAY SIZE = ",ArraySize(curr_filter_selected));
         if (debugLogging) ArrayPrint(curr_filter_selected);
         if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
         update_dashboard_values(curr_filter_selected,imp_filter_selected);
         if (debugLogging) Print("SUCCESS. DASHBOARD UPDATED");
         ChartRedraw(0);
      }
      if (StringFind(sparam, IMPACT_LABEL) >= 0) {
         string selected_imp = ObjectGetString(0, sparam, OBJPROP_TEXT);
         ENUM_CALENDAR_EVENT_IMPORTANCE selected_importance_lvl = get_importance_level(impact_labels,allowed_importance_levels,selected_imp);
         if (debugLogging) Print("BTN NAME = ", sparam, ", IMPORTANCE LEVEL = ", selected_imp,"(",selected_importance_lvl,")");
         bool btn_state = ObjectGetInteger(0, sparam, OBJPROP_STATE);
         color color_border = btn_state ? clrNONE : clrBlack;
         if (btn_state == false) {
            if (debugLogging) Print("BUTTON IS IN UN-SELECTED MODE.");
            for (int i = 0; i < ArraySize(imp_filter_selected); i++) {
               if (impact_filter_selected[i] == selected_imp) {
                  for (int j = i; j < ArraySize(imp_filter_selected) - 1; j++) {
                     imp_filter_selected[j] = imp_filter_selected[j + 1];
                     impact_filter_selected[j] = impact_filter_selected[j + 1];
                  }
                  ArrayResize(imp_filter_selected, ArraySize(imp_filter_selected) - 1);
                  ArrayResize(impact_filter_selected, ArraySize(impact_filter_selected) - 1);
                  if (debugLogging) Print("Removed from selected importance filters: ", selected_imp,"(",selected_importance_lvl,")");
                  break;
               }
            }
         } else {
            if (debugLogging) Print("BUTTON IS IN SELECTED MODE. TAKE ACTION");
            bool already_selected = false;
            for (int j = 0; j < ArraySize(imp_filter_selected); j++) {
               if (impact_filter_selected[j] == selected_imp) {
                  already_selected = true;
                  break;
               }
            }
            if (!already_selected) {
               ArrayResize(imp_filter_selected, ArraySize(imp_filter_selected) + 1);
               imp_filter_selected[ArraySize(imp_filter_selected) - 1] = selected_importance_lvl;
               ArrayResize(impact_filter_selected, ArraySize(impact_filter_selected) + 1);
               impact_filter_selected[ArraySize(impact_filter_selected) - 1] = selected_imp;
               if (debugLogging) Print("Added to selected importance filters: ", selected_imp,"(",selected_importance_lvl,")");
            } else {
               if (debugLogging) Print("Importance level already selected: ", selected_imp,"(",selected_importance_lvl,")");
            }
         }
         if (debugLogging) Print("SELECTED ARRAY SIZE = ", ArraySize(imp_filter_selected)," >< ",ArraySize(impact_filter_selected));
         if (debugLogging) ArrayPrint(imp_filter_selected);
         if (debugLogging) ArrayPrint(impact_filter_selected);
         if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
         update_dashboard_values(curr_filter_selected,imp_filter_selected);
         ObjectSetInteger(0,sparam,OBJPROP_BORDER_COLOR,color_border);
         if (debugLogging) Print("SUCCESS. DASHBOARD UPDATED");
         ChartRedraw(0);
      }
      // Scrollbar button clicks
      if (scroll_visible && (sparam == SCROLL_UP_REC || sparam == SCROLL_UP_LABEL)) {
         scrollUp();
         updateButtonColors();
         if (debugLogging) Print("Up button clicked (", sparam, "). CurrPos: ", scroll_pos);
         ChartRedraw(0);
      }
      if (scroll_visible && (sparam == SCROLL_DOWN_REC || sparam == SCROLL_DOWN_LABEL)) {
         scrollDown();
         updateButtonColors();
         if (debugLogging) Print("Down button clicked (", sparam, "). CurrPos: ", scroll_pos);
         ChartRedraw(0);
      }
   }
   else if (id == CHARTEVENT_MOUSE_MOVE && scroll_visible) {
      if (prev_mouse_state == 0 && mouse_state == 1) {
         int xd = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_XDISTANCE);
         int yd = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE);
         int xs = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_XSIZE);
         int ys = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE);
         if (mouse_x >= xd && mouse_x <= xd + xs && mouse_y >= yd && mouse_y <= yd + ys) {
            moving_state_slider = true;
            mlb_down_x = mouse_x;
            mlb_down_y = mouse_y;
            mlb_down_yd_slider = yd;
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_BGCOLOR, clrDodgerBlue);
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height + 2);
            ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
            if (debugLogging) Print("Slider drag started at y=", mouse_y);
         }
      }
      if (moving_state_slider && mouse_state == 1) {
         int delta_y = mouse_y - mlb_down_y;
         int new_y = mlb_down_yd_slider + delta_y;
         int scroll_area_y_min = SCROLLBAR_Y + BUTTON_SIZE;
         int scroll_area_y_max = scroll_area_y_min + SCROLL_AREA_HEIGHT - slider_height;
         new_y = MathMax(scroll_area_y_min, MathMin(new_y, scroll_area_y_max));
         ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE, new_y);
         int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
         double scroll_ratio = (double)(new_y - scroll_area_y_min) / (scroll_area_y_max - scroll_area_y_min);
         int new_scroll_pos = (int)MathRound(scroll_ratio * max_scroll);
         if (new_scroll_pos != scroll_pos) {
            scroll_pos = new_scroll_pos;
            update_dashboard_values(curr_filter_selected, imp_filter_selected);
            updateButtonColors();
            if (debugLogging) Print("Slider dragged. CurrPos: ", scroll_pos, ", Total steps: ", max_scroll, ", Slider y=", new_y);
         }
         ChartRedraw(0);
      }
      if (mouse_state == 0) {
         if (moving_state_slider) {
            moving_state_slider = false;
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_BGCOLOR, clrLightSlateGray);
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height);
            ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
            if (debugLogging) Print("Slider drag stopped.");
            ChartRedraw(0);
         }
      }
      prev_mouse_state = mouse_state;
   }
}

Aquí, simplemente llamamos a las funciones y la lógica implementadas en las funciones del controlador de eventos para que los cambios surtan efecto en cualquier controlador de eventos necesario. Tras la compilación, obtenemos el siguiente resultado.

VISUALIZACIÓN DE LA BARRA DE DESPLAZAMIENTO

En la visualización anterior, podemos ver que hemos añadido una barra de desplazamiento dinámica al panel de control. Lo que queda ahora es realizar una prueba retrospectiva exhaustiva del sistema, y eso se aborda en la siguiente sección.


Pruebas y validación

Hemos probado el panel de control mejorado para confirmar que la barra de desplazamiento dinámica y la visualización de eventos funcionan según lo previsto, proporcionando una experiencia fluida para navegar por las noticias. Nuestra prueba se centró en la retroalimentación visual de la barra de desplazamiento, la visualización de todos los eventos filtrados y la eficiencia de las actualizaciones silenciosas, validadas tanto en el modo en vivo como en el modo de prueba de estrategia. Hemos recopilado estas pruebas en un archivo conciso en formato Graphics Interchange Format (GIF) para mostrar gráficamente el rendimiento del panel de control, tal y como se muestra a continuación.

PRUEBA 1

En la visualización podemos ver que la barra de desplazamiento funciona correctamente, pero hay un problema: la barra de desplazamiento no es dinámica cuando cambiamos los filtros haciendo clic en ellos, aunque los eventos cambian correctamente. Esto se debe a la falta de recálculo, lo que desactiva la función dinámica completa. Para lograrlo, tendremos que recalibrar la barra de desplazamiento cada vez que se pulsen los botones. Podríamos lograr lo mismo simplemente actualizándolo cuando haya cambios en los datos, pero eso nuevamente resultaría en procesos redundantes cuando no sean necesarios. Aquí está la lógica completa que necesitábamos para lograrlo.

//+------------------------------------------------------------------+
//| Chart event handler                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) {
   int mouse_x = (int)lparam;
   int mouse_y = (int)dparam;
   int mouse_state = (int)sparam;

   if (id == CHARTEVENT_OBJECT_CLICK) {
      UpdateFilterInfo();
      CheckForNewsTrade();
      if (sparam == CANCEL_BTN) {
         isDashboardUpdate = false;
         destroy_Dashboard();
      }
      if (sparam == FILTER_CURR_BTN) {
         bool btn_state = ObjectGetInteger(0,sparam,OBJPROP_STATE);
         enableCurrencyFilter = btn_state;
         if (debugLogging) Print(sparam+" STATE = "+(string)btn_state+", FLAG = "+(string)enableCurrencyFilter);
         string filter_curr_text = enableCurrencyFilter ? ShortToString(0x2714)+"Currency" : ShortToString(0x274C)+"Currency";
         color filter_curr_txt_color = enableCurrencyFilter ? clrLime : clrRed;
         ObjectSetString(0,FILTER_CURR_BTN,OBJPROP_TEXT,filter_curr_text);
         ObjectSetInteger(0,FILTER_CURR_BTN,OBJPROP_COLOR,filter_curr_txt_color);
         if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
         update_dashboard_values(curr_filter_selected,imp_filter_selected);
         // Recalculate scrollbar
         ObjectDelete(0, SCROLL_LEADER);
         ObjectDelete(0, SCROLL_UP_REC);
         ObjectDelete(0, SCROLL_UP_LABEL);
         ObjectDelete(0, SCROLL_DOWN_REC);
         ObjectDelete(0, SCROLL_DOWN_LABEL);
         ObjectDelete(0, SCROLL_SLIDER);
         scroll_visible = totalEvents_Filtered > VISIBLE_ITEMS;
         if (debugLogging) Print("Scrollbar visibility: ", scroll_visible ? "Visible" : "Hidden");
         if (scroll_visible) {
            createRecLabel(SCROLL_LEADER, SCROLLBAR_X, SCROLLBAR_Y, SCROLLBAR_WIDTH, SCROLLBAR_HEIGHT, clrSilver, 1, clrNONE);
            int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
            color up_color = (scroll_pos == 0) ? clrLightGray : clrBlack;
            color down_color = (scroll_pos >= max_scroll) ? clrLightGray : clrBlack;
            createRecLabel(SCROLL_UP_REC, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-5, CharToString(0x35), up_color, 15, "Webdings");
            int down_y = SCROLLBAR_Y + SCROLLBAR_HEIGHT - BUTTON_SIZE;
            createRecLabel(SCROLL_DOWN_REC, SCROLLBAR_X + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings");
            slider_height = calculateSliderHeight();
            int slider_y = SCROLLBAR_Y + BUTTON_SIZE;
            createButton(SCROLL_SLIDER, SCROLLBAR_X + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, 12, clrLightSlateGray, clrDarkGray, "Arial Bold");
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_WIDTH, 2);
            if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height);
            updateSliderPosition();
            updateButtonColors();
         }
         if (debugLogging) Print("Success. Changes updated! State: "+(string)enableCurrencyFilter);
         ChartRedraw(0);
      }
      if (sparam == FILTER_IMP_BTN) {
         bool btn_state = ObjectGetInteger(0,sparam,OBJPROP_STATE);
         enableImportanceFilter = btn_state;
         if (debugLogging) Print(sparam+" STATE = "+(string)btn_state+", FLAG = "+(string)enableImportanceFilter);
         string filter_imp_text = enableImportanceFilter ? ShortToString(0x2714)+"Importance" : ShortToString(0x274C)+"Importance";
         color filter_imp_txt_color = enableImportanceFilter ? clrLime : clrRed;
         ObjectSetString(0,FILTER_IMP_BTN,OBJPROP_TEXT,filter_imp_text);
         ObjectSetInteger(0,FILTER_IMP_BTN,OBJPROP_COLOR,filter_imp_txt_color);
         if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
         update_dashboard_values(curr_filter_selected,imp_filter_selected);
         // Recalculate scrollbar
         ObjectDelete(0, SCROLL_LEADER);
         ObjectDelete(0, SCROLL_UP_REC);
         ObjectDelete(0, SCROLL_UP_LABEL);
         ObjectDelete(0, SCROLL_DOWN_REC);
         ObjectDelete(0, SCROLL_DOWN_LABEL);
         ObjectDelete(0, SCROLL_SLIDER);
         scroll_visible = totalEvents_Filtered > VISIBLE_ITEMS;
         if (debugLogging) Print("Scrollbar visibility: ", scroll_visible ? "Visible" : "Hidden");
         if (scroll_visible) {
            createRecLabel(SCROLL_LEADER, SCROLLBAR_X, SCROLLBAR_Y, SCROLLBAR_WIDTH, SCROLLBAR_HEIGHT, clrSilver, 1, clrNONE);
            int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
            color up_color = (scroll_pos == 0) ? clrLightGray : clrBlack;
            color down_color = (scroll_pos >= max_scroll) ? clrLightGray : clrBlack;
            createRecLabel(SCROLL_UP_REC, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-5, CharToString(0x35), up_color, 15, "Webdings");
            int down_y = SCROLLBAR_Y + SCROLLBAR_HEIGHT - BUTTON_SIZE;
            createRecLabel(SCROLL_DOWN_REC, SCROLLBAR_X + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings");
            slider_height = calculateSliderHeight();
            int slider_y = SCROLLBAR_Y + BUTTON_SIZE;
            createButton(SCROLL_SLIDER, SCROLLBAR_X + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, 12, clrLightSlateGray, clrDarkGray, "Arial Bold");
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_WIDTH, 2);
            if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height);
            updateSliderPosition();
            updateButtonColors();
         }
         if (debugLogging) Print("Success. Changes updated! State: "+(string)enableImportanceFilter);
         ChartRedraw(0);
      }
      if (sparam == FILTER_TIME_BTN) {
         bool btn_state = ObjectGetInteger(0,sparam,OBJPROP_STATE);
         enableTimeFilter = btn_state;
         if (debugLogging) Print(sparam+" STATE = "+(string)btn_state+", FLAG = "+(string)enableTimeFilter);
         string filter_time_text = enableTimeFilter ? ShortToString(0x2714)+"Time" : ShortToString(0x274C)+"Time";
         color filter_time_txt_color = enableTimeFilter ? clrLime : clrRed;
         ObjectSetString(0,FILTER_TIME_BTN,OBJPROP_TEXT,filter_time_text);
         ObjectSetInteger(0,FILTER_TIME_BTN,OBJPROP_COLOR,filter_time_txt_color);
         if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
         update_dashboard_values(curr_filter_selected,imp_filter_selected);
         // Recalculate scrollbar
         ObjectDelete(0, SCROLL_LEADER);
         ObjectDelete(0, SCROLL_UP_REC);
         ObjectDelete(0, SCROLL_UP_LABEL);
         ObjectDelete(0, SCROLL_DOWN_REC);
         ObjectDelete(0, SCROLL_DOWN_LABEL);
         ObjectDelete(0, SCROLL_SLIDER);
         scroll_visible = totalEvents_Filtered > VISIBLE_ITEMS;
         if (debugLogging) Print("Scrollbar visibility: ", scroll_visible ? "Visible" : "Hidden");
         if (scroll_visible) {
            createRecLabel(SCROLL_LEADER, SCROLLBAR_X, SCROLLBAR_Y, SCROLLBAR_WIDTH, SCROLLBAR_HEIGHT, clrSilver, 1, clrNONE);
            int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
            color up_color = (scroll_pos == 0) ? clrLightGray : clrBlack;
            color down_color = (scroll_pos >= max_scroll) ? clrLightGray : clrBlack;
            createRecLabel(SCROLL_UP_REC, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-5, CharToString(0x35), up_color, 15, "Webdings");
            int down_y = SCROLLBAR_Y + SCROLLBAR_HEIGHT - BUTTON_SIZE;
            createRecLabel(SCROLL_DOWN_REC, SCROLLBAR_X + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings");
            slider_height = calculateSliderHeight();
            int slider_y = SCROLLBAR_Y + BUTTON_SIZE;
            createButton(SCROLL_SLIDER, SCROLLBAR_X + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, 12, clrLightSlateGray, clrDarkGray, "Arial Bold");
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_WIDTH, 2);
            if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height);
            updateSliderPosition();
            updateButtonColors();
         }
         if (debugLogging) Print("Success. Changes updated! State: "+(string)enableTimeFilter);
         ChartRedraw(0);
      }
      if (StringFind(sparam,CURRENCY_BTNS) >= 0) {
         string selected_curr = ObjectGetString(0,sparam,OBJPROP_TEXT);
         if (debugLogging) Print("BTN NAME = ",sparam,", CURRENCY = ",selected_curr);
         bool btn_state = ObjectGetInteger(0,sparam,OBJPROP_STATE);
         if (btn_state == false) {
            if (debugLogging) Print("BUTTON IS IN UN-SELECTED MODE.");
            for (int i = 0; i < ArraySize(curr_filter_selected); i++) {
               if (curr_filter_selected[i] == selected_curr) {
                  for (int j = i; j < ArraySize(curr_filter_selected) - 1; j++) {
                     curr_filter_selected[j] = curr_filter_selected[j + 1];
                  }
                  ArrayResize(curr_filter_selected, ArraySize(curr_filter_selected) - 1);
                  if (debugLogging) Print("Removed from selected filters: ", selected_curr);
                  break;
               }
            }
         } else {
            if (debugLogging) Print("BUTTON IS IN SELECTED MODE. TAKE ACTION");
            bool already_selected = false;
            for (int j = 0; j < ArraySize(curr_filter_selected); j++) {
               if (curr_filter_selected[j] == selected_curr) {
                  already_selected = true;
                  break;
               }
            }
            if (!already_selected) {
               ArrayResize(curr_filter_selected, ArraySize(curr_filter_selected) + 1);
               curr_filter_selected[ArraySize(curr_filter_selected) - 1] = selected_curr;
               if (debugLogging) Print("Added to selected filters: ", selected_curr);
            } else {
               if (debugLogging) Print("Currency already selected: ", selected_curr);
            }
         }
         if (debugLogging) Print("SELECTED ARRAY SIZE = ",ArraySize(curr_filter_selected));
         if (debugLogging) ArrayPrint(curr_filter_selected);
         if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
         update_dashboard_values(curr_filter_selected,imp_filter_selected);
         // Recalculate scrollbar
         ObjectDelete(0, SCROLL_LEADER);
         ObjectDelete(0, SCROLL_UP_REC);
         ObjectDelete(0, SCROLL_UP_LABEL);
         ObjectDelete(0, SCROLL_DOWN_REC);
         ObjectDelete(0, SCROLL_DOWN_LABEL);
         ObjectDelete(0, SCROLL_SLIDER);
         scroll_visible = totalEvents_Filtered > VISIBLE_ITEMS;
         if (debugLogging) Print("Scrollbar visibility: ", scroll_visible ? "Visible" : "Hidden");
         if (scroll_visible) {
            createRecLabel(SCROLL_LEADER, SCROLLBAR_X, SCROLLBAR_Y, SCROLLBAR_WIDTH, SCROLLBAR_HEIGHT, clrSilver, 1, clrNONE);
            int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
            color up_color = (scroll_pos == 0) ? clrLightGray : clrBlack;
            color down_color = (scroll_pos >= max_scroll) ? clrLightGray : clrBlack;
            createRecLabel(SCROLL_UP_REC, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-5, CharToString(0x35), up_color, 15, "Webdings");
            int down_y = SCROLLBAR_Y + SCROLLBAR_HEIGHT - BUTTON_SIZE;
            createRecLabel(SCROLL_DOWN_REC, SCROLLBAR_X + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings");
            slider_height = calculateSliderHeight();
            int slider_y = SCROLLBAR_Y + BUTTON_SIZE;
            createButton(SCROLL_SLIDER, SCROLLBAR_X + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, 12, clrLightSlateGray, clrDarkGray, "Arial Bold");
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_WIDTH, 2);
            if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height);
            updateSliderPosition();
            updateButtonColors();
         }
         if (debugLogging) Print("SUCCESS. DASHBOARD UPDATED");
         ChartRedraw(0);
      }
      if (StringFind(sparam, IMPACT_LABEL) >= 0) {
         string selected_imp = ObjectGetString(0, sparam, OBJPROP_TEXT);
         ENUM_CALENDAR_EVENT_IMPORTANCE selected_importance_lvl = get_importance_level(impact_labels,allowed_importance_levels,selected_imp);
         if (debugLogging) Print("BTN NAME = ", sparam, ", IMPORTANCE LEVEL = ", selected_imp,"(",selected_importance_lvl,")");
         bool btn_state = ObjectGetInteger(0, sparam, OBJPROP_STATE);
         color color_border = btn_state ? clrNONE : clrBlack;
         if (btn_state == false) {
            if (debugLogging) Print("BUTTON IS IN UN-SELECTED MODE.");
            for (int i = 0; i < ArraySize(imp_filter_selected); i++) {
               if (impact_filter_selected[i] == selected_imp) {
                  for (int j = i; j < ArraySize(imp_filter_selected) - 1; j++) {
                     imp_filter_selected[j] = imp_filter_selected[j + 1];
                     impact_filter_selected[j] = impact_filter_selected[j + 1];
                  }
                  ArrayResize(imp_filter_selected, ArraySize(imp_filter_selected) - 1);
                  ArrayResize(impact_filter_selected, ArraySize(impact_filter_selected) - 1);
                  if (debugLogging) Print("Removed from selected importance filters: ", selected_imp,"(",selected_importance_lvl,")");
                  break;
               }
            }
         } else {
            if (debugLogging) Print("BUTTON IS IN SELECTED MODE. TAKE ACTION");
            bool already_selected = false;
            for (int j = 0; j < ArraySize(imp_filter_selected); j++) {
               if (impact_filter_selected[j] == selected_imp) {
                  already_selected = true;
                  break;
               }
            }
            if (!already_selected) {
               ArrayResize(imp_filter_selected, ArraySize(imp_filter_selected) + 1);
               imp_filter_selected[ArraySize(imp_filter_selected) - 1] = selected_importance_lvl;
               ArrayResize(impact_filter_selected, ArraySize(impact_filter_selected) + 1);
               impact_filter_selected[ArraySize(impact_filter_selected) - 1] = selected_imp;
               if (debugLogging) Print("Added to selected importance filters: ", selected_imp,"(",selected_importance_lvl,")");
            } else {
               if (debugLogging) Print("Importance level already selected: ", selected_imp,"(",selected_importance_lvl,")");
            }
         }
         if (debugLogging) Print("SELECTED ARRAY SIZE = ", ArraySize(imp_filter_selected)," >< ",ArraySize(impact_filter_selected));
         if (debugLogging) ArrayPrint(imp_filter_selected);
         if (debugLogging) ArrayPrint(impact_filter_selected);
         if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
         update_dashboard_values(curr_filter_selected,imp_filter_selected);
         ObjectSetInteger(0,sparam,OBJPROP_BORDER_COLOR,color_border);
         // Recalculate scrollbar
         ObjectDelete(0, SCROLL_LEADER);
         ObjectDelete(0, SCROLL_UP_REC);
         ObjectDelete(0, SCROLL_UP_LABEL);
         ObjectDelete(0, SCROLL_DOWN_REC);
         ObjectDelete(0, SCROLL_DOWN_LABEL);
         ObjectDelete(0, SCROLL_SLIDER);
         scroll_visible = totalEvents_Filtered > VISIBLE_ITEMS;
         if (debugLogging) Print("Scrollbar visibility: ", scroll_visible ? "Visible" : "Hidden");
         if (scroll_visible) {
            createRecLabel(SCROLL_LEADER, SCROLLBAR_X, SCROLLBAR_Y, SCROLLBAR_WIDTH, SCROLLBAR_HEIGHT, clrSilver, 1, clrNONE);
            int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
            color up_color = (scroll_pos == 0) ? clrLightGray : clrBlack;
            color down_color = (scroll_pos >= max_scroll) ? clrLightGray : clrBlack;
            createRecLabel(SCROLL_UP_REC, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-5, CharToString(0x35), up_color, 15, "Webdings");
            int down_y = SCROLLBAR_Y + SCROLLBAR_HEIGHT - BUTTON_SIZE;
            createRecLabel(SCROLL_DOWN_REC, SCROLLBAR_X + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings");
            slider_height = calculateSliderHeight();
            int slider_y = SCROLLBAR_Y + BUTTON_SIZE;
            createButton(SCROLL_SLIDER, SCROLLBAR_X + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, 12, clrLightSlateGray, clrDarkGray, "Arial Bold");
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_WIDTH, 2);
            if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height);
            updateSliderPosition();
            updateButtonColors();
         }
         if (debugLogging) Print("SUCCESS. DASHBOARD UPDATED");
         ChartRedraw(0);
      }
      // Scrollbar button clicks
      if (scroll_visible && (sparam == SCROLL_UP_REC || sparam == SCROLL_UP_LABEL)) {
         scrollUp();
         updateButtonColors();
         if (debugLogging) Print("Up button clicked (", sparam, "). CurrPos: ", scroll_pos);
         ChartRedraw(0);
      }
      if (scroll_visible && (sparam == SCROLL_DOWN_REC || sparam == SCROLL_DOWN_LABEL)) {
         scrollDown();
         updateButtonColors();
         if (debugLogging) Print("Down button clicked (", sparam, "). CurrPos: ", scroll_pos);
         ChartRedraw(0);
      }
   }
   else if (id == CHARTEVENT_MOUSE_MOVE && scroll_visible) {
      if (prev_mouse_state == 0 && mouse_state == 1) {
         int xd = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_XDISTANCE);
         int yd = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE);
         int xs = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_XSIZE);
         int ys = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE);
         if (mouse_x >= xd && mouse_x <= xd + xs && mouse_y >= yd && mouse_y <= yd + ys) {
            moving_state_slider = true;
            mlb_down_x = mouse_x;
            mlb_down_y = mouse_y;
            mlb_down_yd_slider = yd;
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_BGCOLOR, clrDodgerBlue);
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height + 2);
            ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
            if (debugLogging) Print("Slider drag started at y=", mouse_y);
         }
      }
      if (moving_state_slider && mouse_state == 1) {
         int delta_y = mouse_y - mlb_down_y;
         int new_y = mlb_down_yd_slider + delta_y;
         int scroll_area_y_min = SCROLLBAR_Y + BUTTON_SIZE;
         int scroll_area_y_max = scroll_area_y_min + SCROLL_AREA_HEIGHT - slider_height;
         new_y = MathMax(scroll_area_y_min, MathMin(new_y, scroll_area_y_max));
         ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE, new_y);
         int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
         double scroll_ratio = (double)(new_y - scroll_area_y_min) / (scroll_area_y_max - scroll_area_y_min);
         int new_scroll_pos = (int)MathRound(scroll_ratio * max_scroll);
         if (new_scroll_pos != scroll_pos) {
            scroll_pos = new_scroll_pos;
            update_dashboard_values(curr_filter_selected, imp_filter_selected);
            updateButtonColors();
            if (debugLogging) Print("Slider dragged. CurrPos: ", scroll_pos, ", Total steps: ", max_scroll, ", Slider y=", new_y);
         }
         ChartRedraw(0);
      }
      if (mouse_state == 0) {
         if (moving_state_slider) {
            moving_state_slider = false;
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_BGCOLOR, clrLightSlateGray);
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height);
            ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
            if (debugLogging) Print("Slider drag stopped.");
            ChartRedraw(0);
         }
      }
      prev_mouse_state = mouse_state;
   }
}

Aquí, simplemente tomamos la implementación de la lógica que teníamos con los clics en los elementos de la barra de desplazamiento y la extendimos a los botones de filtro individuales. No necesitamos dedicarle mucho tiempo a esto. Tras la compilación, obtenemos el siguiente resultado.

A partir de la visualización, podemos ver que todo funciona correctamente, con actualizaciones dinámicas y una presentación impecable de los eventos.


Conclusión

En conclusión, hemos mejorado la serie Calendario Económico MQL5 con la incorporación de una barra de desplazamiento dinámica y una presentación optimizada de los eventos, lo que nos permite disfrutar de una navegación intuitiva y un acceso fiable a las noticias, tal y como se muestra en nuestro GIF detallado. Estas mejoras, basadas en el backtesting optimizado de la Parte 8, garantizan una interacción fluida tanto en modo real como en el probador, lo que proporciona una plataforma sólida para estrategias de trading basadas en noticias. Puedes aprovechar este panel de control optimizado como base, personalizándolo para adaptarlo a tus necesidades concretas de trading.

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

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.
Introducción a la exploración de estructuras de mercado fractales con aprendizaje automático Introducción a la exploración de estructuras de mercado fractales con aprendizaje automático
Este artículo intentaremos examinar las series temporales financieras desde la perspectiva de las estructuras fractales autosimilares. Como contamos con demasiadas analogías que confirman la posibilidad de considerar las cotizaciones de mercado como fractales autosimilares, tenemos la oportunidad de formarnos una idea de los horizontes de previsión de dichas estructuras.
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.
Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 23): Medidor de fortaleza de divisas Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 23): Medidor de fortaleza de divisas
¿Sabes qué es lo que realmente determina la dirección de un par de divisas? Es la fortaleza de cada divisa por separado. En este artículo, mediremos la fortaleza de una divisa recorriendo todos los pares de divisas en los que aparece. Esa información nos permite predecir cómo podrían moverse esos pares en función de sus fortalezas relativas. Sigue leyendo para obtener más información.