English Русский 中文 Deutsch 日本語
preview
Técnicas avanzadas de gestión y optimización de la memoria en MQL5

Técnicas avanzadas de gestión y optimización de la memoria en MQL5

MetaTrader 5Probador |
27 6
Sahil Bagdi
Sahil Bagdi
  1. Introducción
  2. Comprender la gestión de la memoria en MQL5
  3. Perfilado y medición del uso de la memoria
  4. Implementación de grupos de memoria personalizados
  5. Optimización de estructuras de datos para aplicaciónes de trading
  6. Técnicas avanzadas para el trading de alta frecuencia (High Frequency Trading, HFT)
  7. Conclusión


Introducción

¡Bienvenido! Si alguna vez has dedicado tiempo a crear sistemas de trading en MQL5, probablemente te hayas topado con ese frustrante obstáculo: tu asesor experto empieza a ralentizarse, el uso de la memoria se dispara o, lo que es peor, todo se bloquea justo cuando el mercado se pone interesante. ¿Te suena familiar?

MQL5 es innegablemente potente, pero ese poder conlleva una gran responsabilidad, especialmente en lo que respecta a la memoria. Muchos desarrolladores se centran únicamente en la lógica estratégica, los puntos de entrada y la gestión de riesgos, mientras que la gestión de la memoria se convierte silenciosamente en una bomba de relojería en segundo plano. A medida que su código se amplía (procesando más símbolos, frecuencias más altas y conjuntos de datos más pesados), ignorar la memoria puede provocar cuellos de botella en el rendimiento, inestabilidad y oportunidades perdidas.

En este artículo vamos a profundizar en el tema. Exploraremos cómo funciona realmente la memoria en MQL5, las trampas comunes que ralentizan sus sistemas o provocan fallos y, lo más importante, cómo solucionarlos. Aprenderás técnicas prácticas de optimización que harán que tus programas de trading sean más rápidos, ágiles y fiables.

Aquí es donde realmente importa el uso eficiente de la memoria:

  • Trading de alta frecuencia: Cada milisegundo es una ventaja potencial... o una pérdida potencial.

  • Análisis en múltiples marcos temporales: ¿Combinar gráficos? Es de esperar que la presión sobre la memoria se multiplique.

  • Lógica de indicadores pesados: Las matemáticas complejas y los grandes conjuntos de datos pueden paralizarlo todo si no se gestionan adecuadamente.

  • Pruebas retrospectivas de historiales extensos: Sin una optimización adecuada, las pruebas retrospectivas pueden parecer un proceso tedioso.

Si está listo para tomarse en serio el rendimiento, vamos a ponernos manos a la obra y hacer que sus sistemas MQL5 sean tan eficientes como inteligentes.

En las próximas secciones, iremos paso a paso, desde los conceptos fundamentales de la asignación de memoria MQL5 hasta técnicas avanzadas y ejemplos centrados en el código. Si sigue estas prácticas, tendrá los conocimientos necesarios para construir sistemas de trading más rápidos, ágiles y resistentes, capaces de manejar las intensas demandas del trading algorítmico moderno. ¡Comencemos!


Comprender la gestión de la memoria en MQL5

Al aventurarse en estrategias de optimización más sofisticadas en MQL5, es importante comprender primero cómo el lenguaje maneja la memoria detrás de escena. Aunque MQL5 generalmente agiliza las tareas de memoria en comparación con un lenguaje de nivel inferior como C++, aún es necesario que los desarrolladores adopten prácticas de codificación eficientes.

Diferenciación entre la memoria Stack y Heap

MQL5, como muchos lenguajes modernos, divide su uso de memoria entre el stack y el heap.

  1. Memoria stack: Aquí es donde van las variables locales cuando se conocen sus tamaños en el momento de la compilación. Se gestiona automáticamente y la asignación aquí se realiza muy rápidamente.
  2. Memoria heap: Se usa cuando necesitas asignar memoria de forma dinámica, ya sea porque el tamaño no se conoce hasta el tiempo de ejecución o porque un objeto necesita permanecer más allá del alcance de una sola función.

Examinemos un ejemplo sencillo para ilustrar la diferencia:
void ExampleFunction()
{
   // Stack allocation - automatically managed
   double price = 1.2345;
   int volume = 100;
   
   // Heap allocation - requires proper management
   double dynamicArray[];
   ArrayResize(dynamicArray, 1000);
   
   // Use the array...
   
   // In MQL5, the array will be automatically deallocated
   // when it goes out of scope, but this isn't always optimal
}

Aunque MQL5 incluye la recolección automática de basura, depender únicamente de ella puede seguir generando ineficiencias, especialmente en entornos de negociación de alta frecuencia.

Ciclo de vida de la memoria en MQL5

Para optimizar el rendimiento resulta útil seguir el recorrido de la memoria a lo largo de su programa MQL5:

  1. Inicialización: Justo cuando se inicia su asesor experto (EA) o indicador, MQL5 crea memoria para cualquier variable global e instancia de clase.
  2. Manejo de eventos: Cada vez que se activa un evento, como OnTick() o OnCalculate(), el sistema configura variables locales en la pila. También puede recurrir al heap si necesita asignaciones más dinámicas.
  3. Desasignación: En el momento en que las variables locales salen del ámbito, la memoria stack se recupera automáticamente. Sin embargo, las asignaciones de memoria heap suelen ser liberadas posteriormente por el recolector de basura.
  4. Terminación: Una vez que el programa se cierra, la memoria restante se libera por completo.

El quid de la cuestión es que, si bien MQL5 gestiona la desasignación, no siempre lo hace de manera instantánea ni de la forma más óptima para tareas comerciales sensibles al tiempo.

Errores comunes de la memoria

A pesar de la recolección automática de basura, aún es posible encontrarse con fugas de memoria o un uso lento de la memoria.

A continuación se enumeran algunos culpables frecuentes:

  1. Creación excesiva de objetos: Crear continuamente nuevos objetos en funciones que se llaman con frecuencia (como OnTick) puede consumir muchos recursos.
  2. Grandes matrices: Almacenar grandes matrices que permanecen durante toda la ejecución del programa puede consumir memoria innecesariamente.
  3. Referencias circulares: Si dos objetos mantienen referencias entre sí, esto puede posponer o interrumpir la recolección de basura.
  4. Gestión inadecuada de los recursos: Olvidarse de cerrar archivos, conexiones a bases de datos u otros recursos del sistema puede provocar un desperdicio de memoria.
Veamos un ejemplo problemático:
// Inefficient approach - creates new arrays on every tick
void OnTick()
{
   // This creates a new array on every tick
   double prices[];
   ArrayResize(prices, 1000);
   
   // Fill the array with price data
   for(int i = 0; i < 1000; i++)
   {
      prices[i] = iClose(_Symbol, PERIOD_M1, i);
   }
   
   // Process the data...
   
   // Array will be garbage collected eventually, but this
   // creates unnecessary memory churn
}

Un enfoque más eficiente sería:

// Class member variable - created once
double prices[];

void OnTick()
{
   // Reuse the existing array
   for(int i = 0; i < 1000; i++)
   {
      prices[i] = iClose(_Symbol, PERIOD_M1, i);
   }
   
   // Process the data...
}

A menudo, pequeños ajustes, como reutilizar objetos en lugar de instanciarlos repetidamente, pueden marcar una diferencia sustancial, especialmente en entornos comerciales de ritmo rápido.

Patrones de asignación de memoria en MQL5

MQL5 utiliza diferentes patrones de asignación de memoria en función del tipo de datos.

Por último, es útil saber cómo MQL5 asigna los tipos de datos comunes:

  1. Tipos primitivos (int, double, bool, etc.)
    Por lo general, se asignan en la pila (stack) cuando se declaran como variables locales.

  2. Matrices (Arrays)
    Las matrices dinámicas en MQL5 se almacenan en el heap.

  3. Cadenas (Strings)
    Los strings MQL5 utilizan recuento de referencias y residen en el heap.

  4. Objetos
    Las instancias de las clases también residen en el heap.

Si tienes en cuenta estos patrones de asignación, estarás mejor preparado para crear un código más eficiente, estable y optimizado para las condiciones reales del mercado.


Perfilado y medición del uso de la memoria

Cuando se trata de optimizar el uso de memoria en MQL5, el primer paso es identificar exactamente dónde ocurren los cuellos de botella. Aunque MQL5 carece de herramientas nativas de creación de perfiles de memoria, podemos ponernos manos a la obra y diseñar un enfoque casero.

Construyendo un generador de perfiles de memoria simple

Para tener un mejor control del uso de la memoria, podemos configurar una clase de perfil minimalista que aproveche la propiedad TERMINAL_MEMORY_AVAILABLE. Al comparar la memoria disponible inicial y actual, puede controlar cuánta memoria consume su aplicación.

//+------------------------------------------------------------------+
//| MemoryProfiler class for tracking memory usage                   |
//+------------------------------------------------------------------+
class CMemoryProfiler
{
private:
   ulong m_startMemory;
   ulong m_peakMemory;
   string m_profileName;
   
public:
   // Constructor
   CMemoryProfiler(string profileName)
   {
      m_profileName = profileName;
      m_startMemory = TerminalInfoInteger(TERMINAL_MEMORY_AVAILABLE);
      m_peakMemory = m_startMemory;
      
      Print("Memory profiling started for: ", m_profileName);
      Print("Initial available memory: ", m_startMemory, " bytes");
   }
   
   // Update peak memory usage
   void UpdatePeak()
   {
      ulong currentMemory = TerminalInfoInteger(TERMINAL_MEMORY_AVAILABLE);
      if(currentMemory < m_peakMemory)
         m_peakMemory = currentMemory;
   }
   
   // Get memory usage
   ulong GetUsedMemory()
   {
      return m_startMemory - TerminalInfoInteger(TERMINAL_MEMORY_AVAILABLE);
   }
   
   // Get peak memory usage
   ulong GetPeakUsage()
   {
      return m_startMemory - m_peakMemory;
   }
   
   // Print memory usage report
   void PrintReport()
   {
      ulong currentUsage = GetUsedMemory();
      ulong peakUsage = GetPeakUsage();
      
      Print("Memory profile report for: ", m_profileName);
      Print("Current memory usage: ", currentUsage, " bytes");
      Print("Peak memory usage: ", peakUsage, " bytes");
   }
   
   // Destructor
   ~CMemoryProfiler()
   {
      PrintReport();
   }
};

Una vez que tenga la clase CMemoryProfiler en su proyecto, ponerla a funcionar se parece a esto:

void OnStart()
{
   // Create a profiler for the entire function
   CMemoryProfiler profiler("OnStart function");
   
   // Allocate some arrays
   double largeArray1[];
   ArrayResize(largeArray1, 100000);
   profiler.UpdatePeak();
   
   double largeArray2[];
   ArrayResize(largeArray2, 200000);
   profiler.UpdatePeak();
   
   // The profiler will print a report when it goes out of scope
}

El generador de perfiles se inicializa registrando una línea base de la memoria disponible en el momento en que se construye. Cada vez que llama a UpdatePeak(), verifica si la huella de memoria actual de su aplicación ha excedido el nivel máximo medido anteriormente. Los métodos GetUsedMemory() y GetPeakUsage() le indican cuánta memoria ha utilizado desde la línea de base, mientras que PrintReport() registra un resumen en la terminal. Ese resumen se genera automáticamente cuando el generador de perfiles sale del ámbito, gracias al destructor de clases.

Ten en cuenta que este método solo mide el uso total de memoria del terminal, en lugar del consumo específico de tu programa. Aún así, es una forma práctica de obtener una visión general de cómo evoluciona el uso de la memoria con el tiempo.

Evaluación comparativa de operaciones de memoria

Optimizar el uso de la memoria no solo consiste en saber cuánta memoria se está utilizando, sino también en comprender la rapidez con la que se ejecutan las diferentes operaciones de memoria. Al sincronizar varias operaciones, puedes ver dónde se esconden las ineficiencias y descubrir posibles ajustes de rendimiento.

//+------------------------------------------------------------------+
//| Benchmark different memory operations                            |
//+------------------------------------------------------------------+
void BenchmarkMemoryOperations()
{
   const int iterations = 1000;
   const int arraySize = 10000;
   
   // Benchmark array allocation
   ulong startTime = GetMicrosecondCount();
   for(int i = 0; i < iterations; i++)
   {
      double tempArray[];
      ArrayResize(tempArray, arraySize);
      // Do something minimal to prevent optimization
      tempArray[0] = 1.0;
   }
   ulong allocTime = GetMicrosecondCount() - startTime;
   
   // Benchmark array reuse
   double reuseArray[];
   ArrayResize(reuseArray, arraySize);
   startTime = GetMicrosecondCount();
   for(int i = 0; i < iterations; i++)
   {
      ArrayInitialize(reuseArray, 0);
      reuseArray[0] = 1.0;
   }
   ulong reuseTime = GetMicrosecondCount() - startTime;
   
   // Benchmark string operations
   startTime = GetMicrosecondCount();
   for(int i = 0; i < iterations; i++)
   {
      string tempString = "Base string ";
      for(int j = 0; j < 100; j++)
      {
         // Inefficient string concatenation
         tempString = tempString + IntegerToString(j);
      }
   }
   ulong stringConcatTime = GetMicrosecondCount() - startTime;
   
   // Benchmark string builder approach
   startTime = GetMicrosecondCount();
   for(int i = 0; i < iterations; i++)
   {
      string tempString = "Base string ";
      string parts[];
      ArrayResize(parts, 100);
      for(int j = 0; j < 100; j++)
      {
         parts[j] = IntegerToString(j);
      }
      tempString = tempString + StringImplode(" ", parts);
   }
   ulong stringBuilderTime = GetMicrosecondCount() - startTime;
   
   // Print results
   Print("Memory operation benchmarks:");
   Print("Array allocation time: ", allocTime, " microseconds");
   Print("Array reuse time: ", reuseTime, " microseconds");
   Print("String concatenation time: ", stringConcatTime, " microseconds");
   Print("String builder time: ", stringBuilderTime, " microseconds");
   Print("Reuse vs. Allocation speedup: ", (double)allocTime / reuseTime);
   Print("String builder vs. Concatenation speedup: ", (double)stringConcatTime / stringBuilderTime);
}

Esta sencilla función de prueba demuestra cómo medir la velocidad de ejecución de varias tareas que requieren un uso intensivo de la memoria. Compara el rendimiento de asignar matrices repetidamente versus reutilizar una única matriz preasignada, así como la diferencia entre la concatenación de cadenas directa y un enfoque de estilo "generador de cadenas". Estas pruebas utilizan GetMicrosecondCount() para medir el tiempo en microsegundos, lo que garantiza que obtenga una visión precisa de cualquier retraso.

Por lo general, los resultados mostrarán que reutilizar matrices ofrece una clara ventaja de rendimiento frente a asignar nuevas en cada bucle, y que recopilar partes de cadenas en una matriz (y luego unirlas) es mejor que las concatenaciones fragmentadas. Estas distinciones se vuelven especialmente críticas en escenarios de trading de alta frecuencia donde cada fracción de milisegundo puede importar.

// Helper function for string array joining
string StringImplode(string separator, string &array[])
   {
    string result = "";
    int size = ArraySize(array);
    for(int i = 0; i < size; i++)
       {
        if(i > 0)
            result += separator;
        result += array[i];
       }
    return result;
   }

Cuando ejecute la prueba de rendimiento, obtendrá datos concretos sobre cómo se comparan las diferentes operaciones de memoria en MQL5. Con esta información, estarás bien preparado para realizar los ajustes necesarios que mantengan tus robots de trading funcionando de manera eficiente y eficaz.


Implementación de grupos de memoria personalizados

Cuando el rendimiento es primordial, una de las estrategias más destacadas para optimizar el uso de la memoria es la agrupación de memoria. En lugar de solicitar constantemente memoria al sistema y luego devolverla, el truco consiste en preasignar un fragmento de memoria y gestionarlo nosotros mismos. Esta sección explora cómo hacer esto en escenarios simples y avanzados.

Implementación básica de un grupo de objetos

Imagina que tienes una clase llamada CTradeSignal que instancias y destruyes con frecuencia, tal vez en un sistema de trading de alta frecuencia. En lugar de acceder al asignador de memoria repetidamente, crea un grupo dedicado para estos objetos. A continuación se muestra un ejemplo básico:

//+------------------------------------------------------------------+
//| Trade signal class that will be pooled                           |
//+------------------------------------------------------------------+
class CTradeSignal
{
public:
   datetime time;
   double price;
   ENUM_ORDER_TYPE type;
   double volume;
   bool isValid;
   
   // Reset the object for reuse
   void Reset()
   {
      time = 0;
      price = 0.0;
      type = ORDER_TYPE_BUY;
      volume = 0.0;
      isValid = false;
   }
};

//+------------------------------------------------------------------+
//| Object pool for CTradeSignal instances                           |
//+------------------------------------------------------------------+
class CTradeSignalPool
{
private:
   CTradeSignal* m_pool[];
   int m_poolSize;
   int m_nextAvailable;
   
public:
   // Constructor
   CTradeSignalPool(int initialSize = 100)
   {
      m_poolSize = initialSize;
      ArrayResize(m_pool, m_poolSize);
      m_nextAvailable = 0;
      
      // Pre-allocate objects
      for(int i = 0; i < m_poolSize; i++)
      {
         m_pool[i] = new CTradeSignal();
      }
      
      Print("Trade signal pool initialized with ", m_poolSize, " objects");
   }
   
   // Get an object from the pool
   CTradeSignal* Acquire()
   {
      // If we've used all objects, expand the pool
      if(m_nextAvailable >= m_poolSize)
      {
         int oldSize = m_poolSize;
         m_poolSize *= 2;  // Double the pool size
         ArrayResize(m_pool, m_poolSize);
         
         // Allocate new objects
         for(int i = oldSize; i < m_poolSize; i++)
         {
            m_pool[i] = new CTradeSignal();
         }
         
         Print("Trade signal pool expanded to ", m_poolSize, " objects");
      }
      
      // Get the next available object
      CTradeSignal* signal = m_pool[m_nextAvailable++];
      signal.Reset();  // Ensure it's in a clean state
      return signal;
   }
   
   // Return an object to the pool
   void Release(CTradeSignal* &signal)
   {
      if(signal == NULL)
         return;
         
      // In a more sophisticated implementation, we would
      // actually track which objects are in use and reuse them.
      // For simplicity, we're just decrementing the counter.
      if(m_nextAvailable > 0)
         m_nextAvailable--;
         
      signal = NULL;  // Clear the reference
   }
   
   // Destructor
   ~CTradeSignalPool()
   {
      // Clean up all allocated objects
      for(int i = 0; i < m_poolSize; i++)
      {
         delete m_pool[i];
      }
      
      Print("Trade signal pool destroyed");
   }
};

En el fragmento anterior, CTradeSignalPool preasigna un lote de objetos CTradeSignal y administra cuidadosamente sus ciclos de vida. Cuando llamas a Acquire(), el grupo te entregará un objeto disponible. Si se queda sin los disponibles, se ampliará y seguirá funcionando. Una vez que hayas terminado con un objeto, Release() lo devuelve a la custodia del grupo.

El principal beneficio aquí es una gran reducción en la sobrecarga que supone asignar y desasignar memoria todo el tiempo. Esto es particularmente útil cuando se procesan objetos a un ritmo rápido, como señales comerciales en un entorno de alta velocidad.

A continuación se muestra un breve ejemplo de cómo podría utilizar este grupo:

// Global pool instance
CTradeSignalPool* g_signalPool = NULL;

void OnInit()
{
   // Initialize the pool
   g_signalPool = new CTradeSignalPool(100);
}

void OnTick()
{
   // Acquire a signal from the pool
   CTradeSignal* signal = g_signalPool.Acquire();
   
   // Set signal properties
   signal.time = TimeCurrent();
   signal.price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   signal.type = ORDER_TYPE_BUY;
   signal.volume = 0.1;
   signal.isValid = true;
   
   // Process the signal...
   
   // Return the signal to the pool when done
   g_signalPool.Release(signal);
}

void OnDeinit(const int reason)
{
   // Clean up the pool
   delete g_signalPool;
   g_signalPool = NULL;
}

Debido a que el grupo recicla sus objetos, reduce los costos repetidos de creación y destrucción que de otro modo surgirían.

Grupo de memoria avanzado para asignaciones de tamaño variable

A veces te enfrentas a requisitos más complejos, como la necesidad de gestionar fragmentos de memoria de diferentes tamaños. Para esos casos, puede crear un grupo más avanzado:

//+------------------------------------------------------------------+
//| Advanced memory pool for variable-size allocations               |
//| MQL5 version without raw pointer arithmetic                      |
//+------------------------------------------------------------------+
#property strict

class CMemoryPool
{
private:
   // Usage tracking
   bool  m_blockUsage[]; 
   
   // Size settings
   int   m_totalSize;    // Total bytes in the pool
   int   m_blockSize;    // Size of each block
   
   // Statistics
   int   m_used;         // How many bytes are currently in use

public:
   // Memory buffer (dynamic array of bytes)
   uchar m_memory[];

   // Constructor
   CMemoryPool(const int totalSize=1024*1024, // default 1 MB
               const int blockSize=1024)      // default 1 KB blocks
   {
      m_totalSize = totalSize;
      m_blockSize = blockSize;
      m_used      = 0;
      
      // Allocate the memory pool
      ArrayResize(m_memory, m_totalSize);
      
      // Initialize block usage tracking
      int numBlocks = m_totalSize / m_blockSize;
      ArrayResize(m_blockUsage, numBlocks);
      ArrayInitialize(m_blockUsage, false);
      
      Print("Memory pool initialized: ", 
            m_totalSize, " bytes, ", 
            numBlocks, " blocks of ", 
            m_blockSize, " bytes each");
   }
   
   // Allocate memory from the pool
   // Returns an offset (>= 0) if successful, or -1 on failure
   int Allocate(const int size)
   {
      // Round up how many blocks are needed
      int blocksNeeded = (size + m_blockSize - 1) / m_blockSize;
      int consecutive  = 0;
      int startBlock   = -1;
      
      // Search for consecutive free blocks
      int numBlocks = ArraySize(m_blockUsage);
      for(int i=0; i < numBlocks; i++)
      {
         if(!m_blockUsage[i])
         {
            // Found a free block
            if(consecutive == 0)
               startBlock = i;
               
            consecutive++;
            
            // If we found enough blocks, stop
            if(consecutive >= blocksNeeded)
               break;
         }
         else
         {
            // Reset
            consecutive = 0;
            startBlock  = -1;
         }
      }
      
      // If we couldn't find enough consecutive blocks
      if(consecutive < blocksNeeded)
      {
         Print("Memory pool allocation failed: needed ", 
               blocksNeeded, " consecutive blocks");
         return -1;  // indicate failure
      }
      
      // Mark the found blocks as used
      for(int b=startBlock; b < startBlock + blocksNeeded; b++)
      {
         m_blockUsage[b] = true;
      }
      
      // Increase usage
      m_used += blocksNeeded * m_blockSize;
      
      // Return the offset in bytes where allocation starts
      return startBlock * m_blockSize;
   }
   
   // Free memory (by offset)
   void Free(const int offset)
   {
      // Validate offset
      if(offset < 0 || offset >= m_totalSize)
      {
         Print("Memory pool error: invalid offset in Free()");
         return;
      }
      
      // Determine the starting block
      int startBlock = offset / m_blockSize;
      
      // Walk forward, freeing used blocks
      int numBlocks = ArraySize(m_blockUsage);
      for(int b=startBlock; b < numBlocks; b++)
      {
         if(!m_blockUsage[b])
            break; // found an already-free block => done
         
         // Free it
         m_blockUsage[b] = false;
         m_used         -= m_blockSize;
      }
   }
   
   // Get usage statistics in %
   double GetUsagePercentage() const
   {
      return (double)m_used / (double)m_totalSize * 100.0;
   }
   
   // Destructor
   ~CMemoryPool()
   {
      // Optionally free arrays (usually automatic at script end)
      ArrayFree(m_memory);
      ArrayFree(m_blockUsage);
      
      Print("Memory pool destroyed. Final usage: ", 
            GetUsagePercentage(), "% of ", m_totalSize, " bytes");
   }
};

//+------------------------------------------------------------------+
//| Example usage in an Expert Advisor                               |
//+------------------------------------------------------------------+
int OnInit(void)
{
   // Create a memory pool
   CMemoryPool pool(1024*1024, 1024); // 1 MB total, 1 KB block size
   
   // Allocate 500 bytes from the pool
   int offset = pool.Allocate(500);
   if(offset >= 0)
   {
      // Write something in the allocated area
      pool.m_memory[offset] = 123;
      Print("Wrote 123 at offset=", offset, 
            " usage=", pool.GetUsagePercentage(), "%");
      
      // Free this block
      pool.Free(offset);
      Print("Freed offset=", offset, 
            " usage=", pool.GetUsagePercentage(), "%");
   }

   return(INIT_SUCCEEDED);
}

void OnTick(void)
{
   // ...
}

La clase CMemoryPool configura un búfer de memoria grande y preasignado, y luego lo divide en fragmentos de tamaño fijo. Cuando solicita memoria, localiza suficientes bloques adyacentes para satisfacer esa necesidad, los marca como ocupados y devuelve un puntero al inicio de esa serie de bloques. Cuando liberas la memoria, esos bloques vuelven al estado «available».

En lugar de asignaciones al estilo C++, este enfoque utiliza funciones de matriz MQL5, como ArrayResize, ArrayInitialize y ArrayFree, por lo que se adapta perfectamente al ecosistema de memoria de MQL5. También aprovecha GetPointer(), que ofrece una forma segura de manejar punteros de matriz en MQL5.

He aquí por qué este enfoque se destaca:

  1. Fragmentación reducida: Manejar su memoria en fragmentos ordenados y de tamaño fijo ayuda a evitar los dolores de cabeza por fragmentación que surgen de las asignaciones frecuentes.
  2. Rendimiento mejorado: Solicitar un bloque de memoria de su propio grupo suele ser más rápido que recurrir al asignador del sistema cada vez.
  3. Mayor visibilidad: Las estadísticas detalladas de uso de su grupo pueden arrojar luz sobre cualquier punto problemático relacionado con la memoria.
  4. Previsibilidad: La preasignación reduce las probabilidades de que se produzcan errores por falta de memoria en momentos críticos.

Este grupo más robusto es perfecto cuando se necesitan bloques de memoria de diferentes tamaños, por ejemplo, para estructuras de datos complejas o cargas de trabajo dinámicas que cambian con frecuencia. Al personalizar sus grupos, ya sea un simple grupo de objetos o un grupo más potente de tamaño variable, puede mantener el uso de la memoria bajo un estricto control y optimizar el rendimiento en aplicaciones exigentes.


Optimización de estructuras de datos para aplicaciónes de trading

Al manejar series temporales en entornos comerciales, se necesitan estructuras de datos que no permitan que el rendimiento se quede atrás con respecto al mercado. Exploremos dos potentes estrategias para almacenar y recuperar sus datos de precios con la máxima eficiencia.

Almacenamiento de datos de series temporales que nunca pierde el ritmo

Un elemento fundamental en los sistemas de negociación es el búfer del historial de precios, y un búfer circular optimizado puede asumir esta carga con facilidad. A continuación se muestra un ejemplo de cómo se podría implementar uno:

//+------------------------------------------------------------------+
//| Circular buffer for price data                                   |
//+------------------------------------------------------------------+
class CPriceBuffer
{
private:
   double m_prices[];
   int m_capacity;
   int m_head;
   int m_size;
   
public:
   // Constructor
   CPriceBuffer(int capacity = 1000)
   {
      m_capacity = capacity;
      ArrayResize(m_prices, m_capacity);
      m_head = 0;
      m_size = 0;
   }
   
   // Add a price to the buffer
   void Add(double price)
   {
      m_prices[m_head] = price;
      m_head = (m_head + 1) % m_capacity;
      
      if(m_size < m_capacity)
         m_size++;
   }
   
   // Get a price at a specific index (0 is the most recent)
   double Get(int index)
   {
      if(index < 0 || index >= m_size)
         return 0.0;
         
      int actualIndex = (m_head - 1 - index + m_capacity) % m_capacity;
      return m_prices[actualIndex];
   }
   
   // Get the current size
   int Size()
   {
      return m_size;
   }
   
   // Get the capacity
   int Capacity()
   {
      return m_capacity;
   }
   
   // Clear the buffer
   void Clear()
   {
      m_head = 0;
      m_size = 0;
   }
   
   // Calculate simple moving average
   double SMA(int period)
   {
      if(period <= 0 || period > m_size)
         return 0.0;
         
      double sum = 0.0;
      for(int i = 0; i < period; i++)
      {
         sum += Get(i);
      }
      
      return sum / period;
   }
};

Aquí, la clase CPriceBuffer utiliza un búfer circular diseñado en torno a una matriz de tamaño fijo. El puntero «head» envuelve el final de la matriz, lo que permite añadir nuevas entradas de precios sin necesidad de realizar costosas operaciones de redimensionamiento. Cuando el búfer alcanza su capacidad máxima, simplemente sobrescribe las entradas más antiguas con datos nuevos, manteniendo una ventana deslizante continua de precios recientes.

Por qué este enfoque es tan eficaz:

  1. La memoria se preasigna y se reutiliza, lo que elimina la sobrecarga de las expansiones constantes.
  2. Añadir nuevos precios y obtener los datos más recientes se realiza en tiempo O(1).
  3. El mecanismo de ventana deslizante administra automáticamente las entradas antiguas y nuevas sin problemas.

A continuación se muestra un fragmento rápido que muestra cómo utilizar esto:

// Global price buffer
CPriceBuffer* g_priceBuffer = NULL;

void OnInit()
{
   // Initialize the price buffer
   g_priceBuffer = new CPriceBuffer(5000);
}

void OnTick()
{
   // Add current price to the buffer
   double price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   g_priceBuffer.Add(price);
   
   // Calculate moving averages
   double sma20 = g_priceBuffer.SMA(20);
   double sma50 = g_priceBuffer.SMA(50);
   
   // Trading logic based on moving averages...
}

void OnDeinit(const int reason)
{
   // Clean up
   delete g_priceBuffer;
   g_priceBuffer = NULL;
}

Organización de datos para aprovechar mejor la caché

Las CPU modernas prosperan gracias a la eficiencia del caché. Al organizar los datos de manera que el procesador obtenga sólo las partes que necesita, puede reducir significativamente el tiempo perdido. Eche un vistazo a este diseño para almacenar datos OHLC (Open, High, Low, Close):

//+------------------------------------------------------------------+
//| Cache-friendly OHLC data structure                               |
//+------------------------------------------------------------------+
class COHLCData
{
private:
   int m_capacity;
   int m_size;
   
   // Structure of arrays (SoA) layout for better cache locality
   datetime m_time[];
   double m_open[];
   double m_high[];
   double m_low[];
   double m_close[];
   long m_volume[];
   
public:
   // Constructor
   COHLCData(int capacity = 1000)
   {
      m_capacity = capacity;
      m_size = 0;
      
      // Allocate arrays
      ArrayResize(m_time, m_capacity);
      ArrayResize(m_open, m_capacity);
      ArrayResize(m_high, m_capacity);
      ArrayResize(m_low, m_capacity);
      ArrayResize(m_close, m_capacity);
      ArrayResize(m_volume, m_capacity);
   }
   
   // Add a new bar
   bool Add(datetime time, double open, double high, double low, double close, long volume)
   {
      if(m_size >= m_capacity)
         return false;
         
      m_time[m_size] = time;
      m_open[m_size] = open;
      m_high[m_size] = high;
      m_low[m_size] = low;
      m_close[m_size] = close;
      m_volume[m_size] = volume;
      
      m_size++;
      return true;
   }
   
   // Get bar data by index
   bool GetBar(int index, datetime &time, double &open, double &high, double &low, double &close, long &volume)
   {
      if(index < 0 || index >= m_size)
         return false;
         
      time = m_time[index];
      open = m_open[index];
      high = m_high[index];
      low = m_low[index];
      close = m_close[index];
      volume = m_volume[index];
      
      return true;
   }
   
   // Get size
   int Size()
   {
      return m_size;
   }
   
   // Process all high values (example of cache-friendly operation)
   double CalculateAverageHigh()
   {
      if(m_size == 0)
         return 0.0;
         
      double sum = 0.0;
      for(int i = 0; i < m_size; i++)
      {
         sum += m_high[i];
      }
      
      return sum / m_size;
   }
   
   // Process all low values (example of cache-friendly operation)
   double CalculateAverageLow()
   {
      if(m_size == 0)
         return 0.0;
         
      double sum = 0.0;
      for(int i = 0; i < m_size; i++)
      {
         sum += m_low[i];
      }
      
      return sum / m_size;
   }
};

La clase COHLCData divide cada atributo (como alto o bajo) en su propia matriz, una estructura de matrices (Structure of Arrays, SoA), en lugar de la más tradicional matriz de estructuras (Array of Structures, AoS). ¿Por qué es importante? Supongamos que quieres calcular la media de todos los valores «altos». Con una configuración SoA, el procesador se desliza a través de una matriz contigua de máximos, realizando menos viajes a la memoria. Por el contrario, un AoS obliga a la CPU a saltarse los datos de apertura, mínimo, cierre y volumen solo para capturar cada valor máximo.

Con COHLCData, le resultará muy sencillo:

  1. Añade nuevas barras OHLC sobre la marcha.
  2. Recuperar barras específicas por índice.
  3. Realiza cálculos en cualquier campo individual (como todos los máximos) sin tropezar con datos no relacionados.

Esta elección de diseño significa que su análisis técnico, ya sea de medias móviles, cálculos de volatilidad o simplemente búsqueda de rupturas, se ejecuta de forma mucho más eficiente gracias a una mejor localización de la caché.


Técnicas avanzadas para el trading de alta frecuencia (High Frequency Trading, HFT)

El trading de alta frecuencia (HFT) exige una latencia extremadamente baja y un rendimiento constante. Incluso la más mínima ralentización puede perturbar la ejecución de las operaciones y provocar la pérdida de oportunidades. A continuación, exploramos dos enfoques fundamentales (preasignación y mapeo de memoria simulado) que pueden ayudar a mantener la latencia al mínimo absoluto en MQL5.

Estrategias de preasignación

Cuando su sistema necesita responder en microsegundos, no puede permitirse los retrasos impredecibles causados por la asignación de memoria sobre la marcha. La solución es la preasignación: Reservar por adelantado toda la memoria que su aplicación pueda necesitar, para no tener que asignar más durante las horas punta de funcionamiento.

//+------------------------------------------------------------------+
//| Pre-allocation example for high-frequency trading                |
//+------------------------------------------------------------------+
class CHFTSystem
{
private:
   // Pre-allocated arrays for price data
   double m_bidPrices[];
   double m_askPrices[];
   datetime m_times[];
   
   // Pre-allocated arrays for calculations
   double m_tempArray1[];
   double m_tempArray2[];
   double m_tempArray3[];
   
   // Pre-allocated string buffers
   string m_logMessages[];
   int m_logIndex;
   
   int m_capacity;
   int m_dataIndex;
   
public:
   // Constructor
   CHFTSystem(int capacity = 10000)
   {
      m_capacity = capacity;
      m_dataIndex = 0;
      m_logIndex = 0;
      
      // Pre-allocate all arrays
      ArrayResize(m_bidPrices, m_capacity);
      ArrayResize(m_askPrices, m_capacity);
      ArrayResize(m_times, m_capacity);
      
      ArrayResize(m_tempArray1, m_capacity);
      ArrayResize(m_tempArray2, m_capacity);
      ArrayResize(m_tempArray3, m_capacity);
      
      ArrayResize(m_logMessages, 1000);  // Pre-allocate log buffer
      
      Print("HFT system initialized with capacity for ", m_capacity, " data points");
   }
   
   // Add price data
   void AddPriceData(double bid, double ask)
   {
      // Use modulo to create a circular buffer effect
      int index = m_dataIndex % m_capacity;
      
      m_bidPrices[index] = bid;
      m_askPrices[index] = ask;
      m_times[index] = TimeCurrent();
      
      m_dataIndex++;
   }
   
   // Log a message without allocating new strings
   void Log(string message)
   {
      int index = m_logIndex % 1000;
      m_logMessages[index] = message;
      m_logIndex++;
   }
   
   // Perform calculations using pre-allocated arrays
   double CalculateSpread(int lookback = 100)
   {
      int available = MathMin(m_dataIndex, m_capacity);
      int count = MathMin(lookback, available);
      
      if(count <= 0)
         return 0.0;
         
      double sumSpread = 0.0;
      
      for(int i = 0; i < count; i++)
      {
         int index = (m_dataIndex - 1 - i + m_capacity) % m_capacity;
         sumSpread += m_askPrices[index] - m_bidPrices[index];
      }
      
      return sumSpread / count;
   }
};

La clase CHFTSystem ilustra cómo se puede integrar la preasignación en un marco HFT. Configura todas las matrices y los búferes con antelación, lo que garantiza que no se produzcan solicitudes de memoria adicionales una vez que el motor de negociación esté en funcionamiento. Los búferes circulares se utilizan para mantener una ventana deslizante de datos de precios recientes, lo que elimina la costosa reasignación. También se configuran por adelantado matrices temporales para cálculos y un búfer de mensajes de registro dedicado. De este modo, esta estrategia evita el riesgo de picos repentinos en la asignación cuando las condiciones del mercado son más críticas.

Archivos mapeados en memoria para conjuntos de datos grandes

Algunas estrategias de trading se basan en enormes cantidades de datos históricos, a veces más de los que puede manejar la memoria RAM disponible. Aunque MQL5 no admite archivos nativos mapeados en memoria, puede emular este enfoque utilizando E/S de archivos estándar:

//+------------------------------------------------------------------+
//| Simple memory-mapped file simulation for large datasets          |
//+------------------------------------------------------------------+
class CDatasetMapper
{
private:
   int m_fileHandle;
   string m_fileName;
   int m_recordSize;
   int m_recordCount;
   
   // Cache for recently accessed records
   double m_cache[];
   int m_cacheSize;
   int m_cacheStart;
   
public:
   // Constructor
   CDatasetMapper(string fileName, int recordSize, int cacheSize = 1000)
   {
      m_fileName = fileName;
      m_recordSize = recordSize;
      m_cacheSize = cacheSize;
      
      // Open or create the file
      m_fileHandle = FileOpen(m_fileName, FILE_READ|FILE_WRITE|FILE_BIN);
      
      if(m_fileHandle != INVALID_HANDLE)
      {
         // Get file size and calculate record count
         m_recordCount = (int)(FileSize(m_fileHandle) / (m_recordSize * sizeof(double)));
         
         // Initialize cache
         ArrayResize(m_cache, m_cacheSize * m_recordSize);
         m_cacheStart = -1;  // Cache is initially empty
         
         Print("Dataset mapper initialized: ", m_fileName, ", ", m_recordCount, " records");
      }
      else
      {
         Print("Failed to open dataset file: ", m_fileName, ", error: ", GetLastError());
      }
   }
   
   // Add a record to the dataset
   bool AddRecord(double &record[])
   {
      if(m_fileHandle == INVALID_HANDLE || ArraySize(record) != m_recordSize)
         return false;
         
      // Seek to the end of the file
      FileSeek(m_fileHandle, 0, SEEK_END);
      
      // Write the record
      int written = FileWriteArray(m_fileHandle, record, 0, m_recordSize);
      
      if(written == m_recordSize)
      {
         m_recordCount++;
         return true;
      }
      
      return false;
   }
   
   // Get a record from the dataset
   bool GetRecord(int index, double &record[])
   {
      if(m_fileHandle == INVALID_HANDLE || index < 0 || index >= m_recordCount)
         return false;
         
      // Check if the record is in cache
      if(index >= m_cacheStart && index < m_cacheStart + m_cacheSize)
      {
         // Copy from cache
         int cacheOffset = (index - m_cacheStart) * m_recordSize;
         ArrayCopy(record, m_cache, 0, cacheOffset, m_recordSize);
         return true;
      }
      
      // Load a new cache block
      m_cacheStart = (index / m_cacheSize) * m_cacheSize;
      int fileOffset = m_cacheStart * m_recordSize * sizeof(double);
      
      // Seek to the start of the cache block
      FileSeek(m_fileHandle, fileOffset, SEEK_SET);
      
      // Read into cache
      int read = FileReadArray(m_fileHandle, m_cache, 0, m_cacheSize * m_recordSize);
      
      if(read > 0)
      {
         // Copy from cache
         int cacheOffset = (index - m_cacheStart) * m_recordSize;
         ArrayCopy(record, m_cache, 0, cacheOffset, m_recordSize);
         return true;
      }
      
      return false;
   }
   
   // Get record count
   int GetRecordCount()
   {
      return m_recordCount;
   }
   
   // Destructor
   ~CDatasetMapper()
   {
      if(m_fileHandle != INVALID_HANDLE)
      {
         FileClose(m_fileHandle);
         Print("Dataset mapper closed: ", m_fileName);
      }
   }
};

La clase CDatasetMapper simula el mapeo de memoria leyendo y escribiendo registros de tamaño fijo en un archivo binario y almacenando los elementos a los que se ha accedido más recientemente en una pequeña caché en memoria. Este diseño le permite trabajar con conjuntos de datos de tamaño prácticamente ilimitado, al tiempo que mantiene una sobrecarga de rendimiento manejable al leer datos secuenciales o registros cercanos. Aunque no se trata de un mapeo de memoria real a nivel del sistema operativo, ofrece muchas de las mismas ventajas, en particular la capacidad de procesar conjuntos de datos extensos sin agotar la memoria del sistema.


Conclusión

La optimización de la memoria no solo consiste en ahorrar unos pocos bytes, sino también en ganar velocidad, estabilidad y mantener el control. En MQL5, donde cada milisegundo cuenta, la gestión inteligente de la memoria se convierte en una verdadera ventaja competitiva.

En este artículo, hemos explorado estrategias prácticas que van mucho más allá de la teoría: comprender el modelo de memoria interna de MQL5, reutilizar objetos para reducir la sobrecarga, crear estructuras de datos compatibles con la caché y construir grupos de memoria personalizados para entornos de trading de alta frecuencia.

¿La regla de oro? No adivines, mide. El perfilado revela dónde se encuentran los verdaderos cuellos de botella, lo que le permite optimizar con precisión. Ya sea preasignando memoria para evitar la latencia en tiempo de ejecución o simulando el mapeo de memoria para trabajar con conjuntos de datos masivos de manera eficiente, todas las técnicas que hemos visto tienen un único objetivo: hacer que sus aplicaciones MQL5 sean más rápidas y resilientes.

Aplica incluso algunas de estas técnicas y notarás la diferencia. Sus sistemas serán más ágiles, rápidos y estarán mejor equipados para gestionar las exigencias del comercio algorítmico moderno.

Esto no es el final, solo es el punto de partida. Sigue experimentando, sigue perfeccionando y lleva el rendimiento al siguiente nivel.

¡Feliz trading! ¡Feliz codificación!


Todo el código al que se hace referencia en el artículo se adjunta a continuación. La siguiente tabla describe todos los archivos de código fuente que acompañan al artículo.

Nombre del archivo Descripción
BenchmarkMemoryOperations.mq5 Código que demuestra cómo comparar operaciones de memoria como asignación de matrices, reutilización y concatenación de cadenas en MQL5.
MemoryPoolUsage.mq5 Ejemplo de implementación que demuestra cómo utilizar grupos de memoria personalizados para asignaciones de tamaño variable en MQL5.
PriceBufferUsage.mq5  Ejemplo de script que muestra el uso práctico de un búfer de precios circular para el manejo eficiente de datos de series temporales.
SignalPoolUsage.mq5 Ejemplo que ilustra cómo utilizar un grupo de objetos para gestionar de forma eficiente los objetos de señales de trading que se utilizan con frecuencia.
CDatasetMapper.mqh Archivo de encabezado que contiene la implementación de un mecanismo simulado de archivos mapeados en memoria para manejar grandes conjuntos de datos.
CHFTSystem.mqh Archivo de encabezado que define una clase para sistemas de negociación de alta frecuencia que utilizan estrategias de preasignación para minimizar la latencia.
CMemoryProfiler.mqh Archivo de encabezado que define una clase sencilla de perfilado de memoria para medir el uso de memoria en aplicaciones MQL5.
COHLCData.mqh Archivo de encabezado con una estructura de datos optimizada para el almacenamiento eficiente de datos de precios OHLC.
CPriceBuffer.mqh Archivo de encabezado que contiene la implementación del búfer circular optimizado para el almacenamiento y la recuperación rápida de datos de precios.



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

Vladislav Boyko
Vladislav Boyko | 7 abr 2025 en 15:18
Stanislav Korotky #:
Para este ejemplo con OHLCV, para hacerlo más apropiado para la eficiencia de memoria y tiempo, sería probablemente más interesante empaquetar todos los valores en un único array 2D o incluso 1D:

Un array 2D en lugar de un array de estructuras puede ahorrar ligeramente tiempo de procesador, pero aumentará mucho el tiempo que el desarrollador dedica a desarrollar y mantener el código. En mi opinión personal, estoy de acuerdo con el resto de tus afirmaciones.

Vladislav Boyko
Vladislav Boyko | 7 abr 2025 en 15:55

https://www.mql5.com/es/articles/17693#sec2

Veamos un ejemplo problemático:

// Enfoque ineficiente - crea nuevas matrices en cada tick
void OnTick()
{
   // Esto crea un nuevo array en cada tick
   double prices[];
   ArrayResize(prices, 1000);
   
   // Rellenar el array con datos de precios
   for(int i = 0; i < 1000; i++)
   {
      prices[i] = iClose(_Symbol, PERIOD_M1, i);
   }
   
   // Procesa los datos...
   
   // El array será recolectado eventualmente, pero este
   // crea un flujo de memoria innecesario
}

Un enfoque más eficiente sería:

// Variable miembro de clase - creada una vez
double prices[];

void OnTick()
{
   // Reutilizar la matriz existente
   for(int i = 0; i < 1000; i++)
   {
      prices[i] = iClose(_Symbol, PERIOD_M1, i);
   }
   
   // Procesa los datos...
}

Stanislav Korotky #:

El artículo parece muy discutible (sólo un par de puntos).

¿Cuál es la clase que se menciona aquí?

Por la presencia del manejador OnTick y la forma en que se accede al array se deduce que has añadido el array de precios al ámbito global, lo cual es una mala idea (debido a la contaminación del espacio de nombres, si el array sólo se necesita en el ámbito del manejador). Probablemente sería más apropiado mantener el código inicial del mismo ejemplo, pero haciendo el array estático, de esta forma todo el mundo vería claramente la diferencia:

Por lo que tengo entendido, ese ejemplo (lo he citado más arriba) es, a grandes rasgos, pseudocódigo. Es decir, el autor no presta atención a lo siguiente (para concentrarse en lo que está hablando exactamente, supongo):

  • A juzgar por la condición del bucle, el tamaño del array se conoce en tiempo de compilación, pero sin embargo, el array es dinámico.
  • Aunque el array es dinámico, ArrayResize no fue llamado en el código que demuestra el enfoque eficiente.
  • En términos de eficiencia, sospecho que sería mejor sustituir todo el bucle siguiente por una única llamada a CopySeries:

   // Reutilizar la matriz existente
   for(int i = 0; i < 1000; i++)
   {
      prices[i] = iClose(_Symbol, PERIOD_M1, i);
   }
Vladislav Boyko
Vladislav Boyko | 7 abr 2025 en 16:03
Vladislav Boyko #:
En términos de eficiencia, sospecho que sería mejor sustituir todo el bucle siguiente por una única llamada CopySeries:

Corrígeme si me equivoco, pero por lo que recuerdo, cada llamada iClose contiene una llamada CopySeries bajo el capó.

Too Chee Ng
Too Chee Ng | 1 jun 2025 en 15:31

Este artículo ofrece un contenido perspicaz y sugerente para el debate.

La presentación técnica es clara y está bien explicada, lo que facilita el seguimiento y la participación del lector.

Muchas gracias.

Aleksey Vyazmikin
Aleksey Vyazmikin | 5 sept 2025 en 15:44

Estos artículos necesitan pruebas comparativas motivadoras que demuestren realmente la eficacia de los enfoques propuestos.

La traducción está torcida, no es fácil de entender sin analizar el código.

Aprendizaje automático en la negociación de tendencias unidireccionales tomando el oro como ejemplo Aprendizaje automático en la negociación de tendencias unidireccionales tomando el oro como ejemplo
En este artículo analizaremos un enfoque interesante: la negociación solo en la dirección seleccionada (compra o venta). Para ello, utilizaremos técnicas de inferencia causal y aprendizaje automático.
Redes neuronales en el trading: Jerarquía de habilidades para el comportamiento adaptativo de agentes (HiSSD) Redes neuronales en el trading: Jerarquía de habilidades para el comportamiento adaptativo de agentes (HiSSD)
Hoy nos familiarizaremos con el framework HiSSD, que combina el aprendizaje jerárquico y los enfoques multiagente para crear sistemas adaptativos. En este artículo, detallaremos cómo este enfoque innovador ayuda a identificar patrones ocultos en los mercados financieros y a optimizar las estrategias comerciales en un entorno descentralizado.
Optimización de Battle Royale — Battle Royale Optimizer (BRO) Optimización de Battle Royale — Battle Royale Optimizer (BRO)
El artículo describe un innovador enfoque de optimización que combina la competición espacial de soluciones con el estrechamiento adaptativo del espacio de búsqueda, lo cual convierte al Battle Royale Optimizer en una prometedora herramienta para el análisis financiero.
Implementación de un modelo de tabla en MQL5: Aplicación del concepto MVC (Modelo-Vista-Controlador) Implementación de un modelo de tabla en MQL5: Aplicación del concepto MVC (Modelo-Vista-Controlador)
En este artículo, analizamos el proceso de desarrollo de un modelo de tabla en MQL5 utilizando el patrón arquitectónico MVC (Modelo-Vista-Controlador) para separar la lógica de datos, la presentación y el control, lo que permite obtener un código estructurado, flexible y escalable. Consideramos la implementación de clases para construir un modelo de tabla, incluyendo el uso de listas enlazadas para almacenar datos.