English Русский 中文 Español Deutsch 日本語
preview
Gerenciamento Avançado de Memória e Técnicas de Otimização em MQL5

Gerenciamento Avançado de Memória e Técnicas de Otimização em MQL5

MetaTrader 5Testador |
17 6
Sahil Bagdi
Sahil Bagdi
  1. Introdução
  2. Compreendendo o Gerenciamento de Memória no MQL5
  3. Criando Perfis e Medindo o Uso de Memória
  4. Implementando Pools de Memória Personalizados
  5. Otimizando Estruturas de Dados para Aplicações de Negociação
  6. Técnicas Avançadas para Negociação de Alta Frequência
  7. Conclusão


Introdução

Bem-vindo! Se você passou algum tempo desenvolvendo sistemas de negociação em MQL5, provavelmente já encontrou aquela situação frustrante: seu Expert Advisor começa a ficar lento, o uso de memória dispara ou, pior ainda, tudo trava justamente quando o mercado fica interessante. Parece familiar?

O MQL5 é inegavelmente poderoso, mas com esse poder vem responsabilidade, especialmente quando se trata de memória. Muitos desenvolvedores focam exclusivamente na lógica da estratégia, nos pontos de entrada e no gerenciamento de risco, enquanto o tratamento da memória silenciosamente se transforma em uma bomba-relógio em segundo plano. À medida que seu código cresce, processando mais símbolos, frequências mais altas e conjuntos de dados mais pesados, ignorar a memória pode levar a gargalos de desempenho, instabilidade e oportunidades perdidas.

Neste artigo, vamos analisar o que acontece por trás dos bastidores. Exploraremos como a memória realmente funciona no MQL5, as armadilhas comuns que desaceleram seus sistemas ou causam falhas e, mais importante ainda, como corrigi-las. Você aprenderá técnicas práticas de otimização que tornam seus programas de negociação mais rápidos, mais leves e mais confiáveis.

Veja onde o uso eficiente de memória realmente faz diferença:

  • Negociação de Alta Frequência: Cada milissegundo é uma vantagem potencial ou uma perda potencial.

  • Análise Multitemporal: Combinando gráficos? Espere que a pressão sobre a memória se multiplique.

  • Lógica Complexa de Indicadores: Matemática avançada e grandes conjuntos de dados podem fazer tudo parar se não forem gerenciados adequadamente.

  • Backtests com Grandes Históricos: Sem uma otimização inteligente, os backtests podem parecer tão lentos quanto assistir tinta secar.

Se você está pronto para levar desempenho a sério, vamos mergulhar no assunto e tornar seus sistemas MQL5 tão eficientes quanto inteligentes.

Nas próximas seções, avançaremos passo a passo, desde os conceitos fundamentais de alocação de memória no MQL5 até técnicas avançadas e exemplos focados em código. Seguindo essas práticas, você terá o conhecimento necessário para construir sistemas de negociação mais rápidos, mais enxutos e mais resilientes, capazes de lidar com as intensas exigências da negociação algorítmica moderna. Vamos começar!


Compreendendo o Gerenciamento de Memória no MQL5

Ao explorar estratégias de otimização mais sofisticadas em MQL5, é importante primeiro compreender como a linguagem gerencia a memória nos bastidores. Embora o MQL5 geralmente simplifique as tarefas relacionadas à memória em comparação com linguagens de nível mais baixo, como C++, ainda existe a necessidade de que os desenvolvedores adotem práticas de codificação eficientes.

Diferenciando Memória de Pilha e Memória Heap

O MQL5, assim como muitas linguagens modernas, divide o uso de memória entre a pilha (stack) e a heap:

  1. Memória de Pilha (Stack): É onde as variáveis locais são armazenadas quando seus tamanhos são conhecidos em tempo de compilação. Ela é gerenciada automaticamente e sua alocação ocorre muito rapidamente.
  2. Memória Heap: Utilizada em situações em que é necessário alocar memória dinamicamente, talvez quando o tamanho não seja conhecido até o momento da execução ou quando um objeto precise permanecer disponível além do escopo de uma única função.

Vamos examinar um exemplo simples para ilustrar a diferença:
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
}

Embora o MQL5 inclua coleta automática de lixo (garbage collection), depender exclusivamente dela ainda pode produzir ineficiências, especialmente em ambientes de negociação de alta frequência.

Ciclo de Vida da Memória no MQL5

Para otimizar o desempenho, é útil acompanhar a jornada da memória ao longo do seu programa MQL5:

  1. Inicialização: Assim que seu Expert Advisor (EA) ou indicador é iniciado, o MQL5 reserva memória para quaisquer variáveis globais e instâncias de classes.
  2. Tratamento de Eventos: Toda vez que um evento é acionado, como OnTick() ou OnCalculate(), o sistema cria variáveis locais na pilha. Ele também pode recorrer à heap caso precise de alocações mais dinâmicas.
  3. Desalocação: No momento em que as variáveis locais saem do escopo, a memória da pilha é automaticamente recuperada. As alocações da heap, por outro lado, geralmente são liberadas posteriormente pelo coletor de lixo.
  4. Encerramento: Quando o programa é finalizado, toda a memória restante é completamente liberada.

O ponto central é que, embora o MQL5 gerencie a desalocação, ele nem sempre faz isso instantaneamente ou da maneira mais otimizada para tarefas de negociação sensíveis ao tempo.

Armadilhas Comuns de Memória

Apesar da coleta automática de lixo, ainda é possível encontrar vazamentos de memória ou uso ineficiente de memória.

Aqui estão alguns dos culpados mais frequentes:

  1. Criação Excessiva de Objetos: Criar continuamente novos objetos em funções chamadas com frequência (como OnTick) pode consumir muitos recursos.
  2. Arrays Grandes: Manter grandes arrays alocados durante toda a execução do programa pode consumir memória desnecessariamente.
  3. Referências Circulares: Se dois objetos mantiverem referências um ao outro, isso pode atrasar ou prejudicar a coleta de lixo.
  4. Gerenciamento Inadequado de Recursos: Esquecer de fechar arquivos, conexões com banco de dados ou outros recursos do sistema pode resultar em desperdício de memória.
Vamos observar um exemplo 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
}

Uma abordagem mais eficiente seria:

// 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...
}

Frequentemente, pequenos ajustes, como reutilizar objetos em vez de instanciá-los repetidamente, podem fazer uma diferença substancial, especialmente em ambientes de negociação de alta velocidade.

Padrões de Alocação de Memória no MQL5

O MQL5 utiliza diferentes padrões de alocação de memória dependendo do tipo de dado:

Por fim, é útil saber como o MQL5 aloca os tipos de dados mais comuns:

  1. Tipos Primitivos ( int, double, bool, etc.)
    Normalmente, eles são alocados na pilha quando declarados como variáveis locais.

  2. Arrays
    Arrays dinâmicos em MQL5 são armazenados na heap.

  3. Strings
    As strings do MQL5 utilizam contagem de referências e residem na heap.

  4. Objetos
    Instâncias de classes também residem na heap.

Mantendo esses padrões de alocação em mente, você estará mais preparado para criar código mais eficiente, estável e otimizado para condições reais de negociação.


Criando Perfis e Medindo o Uso de Memória

Quando se trata de otimizar o uso de memória em MQL5, o primeiro passo é identificar exatamente onde os gargalos ocorrem. Embora o MQL5 não possua ferramentas nativas para classe simples de perfilamento de memória, podemos arregaçar as mangas e construir uma abordagem própria.

Construindo um Perfilador de Memória Simples

Para obter um melhor controle sobre o uso de memória, podemos criar uma classe de perfilamento minimalista que aproveita a propriedade TERMINAL_MEMORY_AVAILABLE. Comparando a memória disponível inicial com a memória disponível atual, é possível acompanhar quanto de memória sua aplicação está consumindo.

//+------------------------------------------------------------------+
//| 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();
   }
};

Depois de incluir a classe CMemoryProfiler em seu projeto, sua utilização se parece com o seguinte:

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
}

O perfilador é inicializado registrando uma linha de base da memória disponível no momento em que é construído. Sempre que você chama UpdatePeak(), ele verifica se o consumo atual de memória da aplicação excedeu o maior valor anteriormente registrado. Os métodos GetUsedMemory() e GetPeakUsage() informam quanta memória foi utilizada desde a linha de base, enquanto PrintReport() registra um resumo no terminal. Esse resumo é gerado automaticamente quando o perfilador sai de escopo, graças ao destrutor da classe.

Tenha em mente que essa abordagem mede apenas o uso geral de memória do terminal, e não o consumo específico do seu programa. Ainda assim, é uma maneira útil de obter uma visão geral de como o uso de memória evolui ao longo do tempo.

Benchmark de Operações de Memória

Otimizar o uso de memória não significa apenas saber quanta memória está sendo utilizada, mas também compreender a velocidade com que diferentes operações de memória são executadas. Ao medir o tempo de várias operações, você pode identificar onde estão escondidas as ineficiências e descobrir possíveis melhorias de desempenho.

//+------------------------------------------------------------------+
//| 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);
}

Essa função de teste simples demonstra como medir a velocidade de execução de diversas tarefas intensivas em memória. Ela compara o desempenho da alocação repetida de arrays com a reutilização de um único array previamente alocado, bem como a diferença entre concatenação direta de strings e uma abordagem no estilo “string builder”. Esses testes utilizam GetMicrosecondCount() para medir o tempo em microssegundos, garantindo uma visão precisa de quaisquer atrasos.

Normalmente, os resultados mostrarão que reutilizar arrays oferece uma vantagem clara de desempenho em relação à criação de novos arrays a cada iteração, e que reunir partes de strings em um array (e depois uni-las) supera concatenações realizadas gradualmente. Essas diferenças tornam-se especialmente críticas em cenários de negociação de alta frequência, onde cada fração de milissegundo pode fazer diferença.

// 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;
   }

Ao executar o benchmark, você obterá dados concretos sobre como diferentes operações de memória se comportam no MQL5. Munido dessas informações, você estará bem preparado para realizar ajustes que mantenham seus robôs de negociação funcionando de forma enxuta e eficiente.


Implementando Pools de Memória Personalizados

Quando o desempenho é fundamental, uma das estratégias mais eficazes para otimizar o uso de memória é o pool de memória. Em vez de solicitar memória constantemente ao sistema e depois devolvê-la, a ideia é pré-alocar um bloco de memória e gerenciá-lo por conta própria. Esta seção explora como fazer isso em cenários simples e avançados.

Implementação Básica de Pool de Objetos

Imagine que você possui uma classe chamada CTradeSignal que é criada e destruída com frequência, talvez em um sistema de negociação de alta frequência. Em vez de acionar repetidamente o alocador de memória, você cria um pool dedicado para esses objetos. Abaixo está um exemplo 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");
   }
};

No trecho acima, CTradeSignalPool pré-aloca um conjunto de objetos CTradeSignal e gerencia cuidadosamente seus ciclos de vida. Quando você chama Acquire(), o pool fornece um objeto disponível. Se não houver mais objetos disponíveis, ele aumenta seu tamanho e continua operando. Quando você termina de usar um objeto, Release() o devolve à administração do pool.

O principal benefício aqui é uma grande redução da sobrecarga causada pela alocação e desalocação constante de memória. Isso é particularmente útil quando você está criando e reutilizando objetos em alta velocidade, como sinais de negociação em um ambiente de alta frequência.

Abaixo está um pequeno exemplo de como você poderia utilizar esse pool:

// 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;
}

Como o pool recicla seus objetos, ele reduz os custos repetitivos de criação e destruição que você enfrentaria de outra forma.

Pool de Memória Avançado para Alocações de Tamanho Variável

Às vezes você se depara com requisitos mais complexos, como a necessidade de lidar com blocos de memória de tamanhos diferentes. Para esses casos, você pode construir um pool mais avançado:

//+------------------------------------------------------------------+
//| 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)
{
   // ...
}

Essa classe CMemoryPool configura um grande buffer de memória pré-alocado e depois o divide em partes de tamanho fixo. Quando você solicita memória, ela localiza blocos adjacentes suficientes para atender à necessidade, marca esses blocos como ocupados e retorna um deslocamento (offset) correspondente ao início dessa sequência de blocos. Quando a memória é liberada, esses blocos retornam ao estado de “disponível”.

Em vez de alocações no estilo C++, essa abordagem utiliza funções de array do MQL5, como ArrayResize, ArrayInitialize e ArrayFree, ajustando-se perfeitamente ao ecossistema de memória do MQL5. Nesta implementação, o gerenciamento é feito por offsets e funções de array do MQL5, sem aritmética bruta de ponteiros.

Eis por que essa abordagem se destaca:

  1. Redução da Fragmentação: Gerenciar a memória em blocos organizados e de tamanho fixo ajuda a evitar os problemas de fragmentação causados por alocações frequentes.
  2. Melhoria de Desempenho: Solicitar um bloco de memória do seu próprio pool normalmente é mais rápido do que recorrer ao alocador do sistema a cada operação.
  3. Maior Visibilidade: Estatísticas detalhadas de uso fornecidas pelo pool podem revelar pontos problemáticos relacionados à memória.
  4. Previsibilidade: A pré-alocação reduz as chances de erros de falta de memória em momentos críticos.

Esse pool mais robusto é ideal quando você precisa de blocos de memória de diferentes tamanhos, como em estruturas de dados complexas ou cargas de trabalho dinâmicas que mudam com frequência. Ao personalizar seus pools, seja um pool simples de objetos ou um pool mais avançado de tamanho variável, você pode manter o uso de memória sob controle rigoroso e otimizar o desempenho em aplicações exigentes.


Otimizando Estruturas de Dados para Aplicações de Negociação

Ao lidar com séries temporais em ambientes de negociação, você precisa de estruturas de dados que não permitam que o desempenho fique para trás em relação ao mercado. Vamos explorar duas estratégias poderosas para armazenar e recuperar dados de preços com máxima eficiência.

Armazenamento de Séries Temporais que Nunca Perde o Ritmo

Uma peça fundamental nos sistemas de negociação é o buffer de histórico de preços, e um buffer circular otimizado pode assumir essa responsabilidade com facilidade. Abaixo está um exemplo de como você poderia implementá-lo:

//+------------------------------------------------------------------+
//| 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;
   }
};

Aqui, a classe CPriceBuffer utiliza um buffer circular baseado em um array de tamanho fixo. O ponteiro “head” circula pelo final do array, tornando possível adicionar novos registros de preço sem operações custosas de redimensionamento. Quando o buffer atinge sua capacidade máxima, ele simplesmente sobrescreve os registros mais antigos com dados novos, mantendo uma janela deslizante contínua dos preços mais recentes.

Por que essa abordagem é tão eficiente:

  1. A memória é pré-alocada e reutilizada, eliminando a sobrecarga de expansões constantes.
  2. Adicionar novos preços e recuperar os dados mais recentes ocorre em tempo O(1).
  3. O mecanismo de janela deslizante gerencia automaticamente registros antigos e novos sem complicações.

Abaixo está um pequeno exemplo demonstrando como utilizá-lo:

// 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;
}

Estruturas Otimizadas para Cache e Maior Velocidade

Os processadores modernos dependem fortemente da eficiência do cache. Ao organizar os dados de forma que o processador busque apenas as informações necessárias, é possível reduzir significativamente o desperdício de tempo. Observe esta estrutura para armazenar dados OHLC (Abertura, Máxima, Mínima e Fechamento):

//+------------------------------------------------------------------+
//| 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;
   }
};

A classe COHLCData separa cada atributo (como máxima ou mínima) em seu próprio array, utilizando uma Estrutura de Arrays (SoA), em vez da abordagem mais tradicional Array de Estruturas (AoS). Por que isso é importante? Suponha que você queira calcular a média de todos os valores de máxima. Com uma estrutura SoA, o processador percorre um array contínuo contendo apenas os valores de máxima, realizando menos acessos à memória. Por outro lado, uma estrutura AoS obriga a CPU a passar por dados de abertura, mínima, fechamento e volume apenas para obter cada valor de máxima.

Com COHLCData, você encontrará uma maneira simples de:

  1. Adicionar novas barras OHLC dinamicamente.
  2. Recuperar barras específicas por índice.
  3. Executar cálculos sobre qualquer campo individual (como todas as máximas) sem precisar percorrer dados irrelevantes.

Essa escolha de projeto faz com que suas análises técnicas, sejam médias móveis, cálculos de volatilidade ou simples buscas por rompimentos, sejam executadas de forma muito mais eficiente graças à melhor localidade de cache.


Técnicas Avançadas para Negociação de Alta Frequência

A negociação de alta frequência (HFT) exige latência extremamente baixa e desempenho constante. Mesmo a menor desaceleração pode prejudicar a execução das operações e resultar em oportunidades perdidas. A seguir, exploramos duas abordagens fundamentais, pré-alocação e mapeamento de memória simulado, que podem ajudar a manter a latência em um nível mínimo no MQL5.

Estratégias de Pré-Alocação

Quando seu sistema precisa responder em microssegundos, você não pode se dar ao luxo de sofrer atrasos imprevisíveis causados pela alocação dinâmica de memória. A solução é a pré-alocação: reservar antecipadamente toda a memória que sua aplicação possa precisar, para que nenhuma nova alocação seja necessária durante os momentos de maior atividade.

//+------------------------------------------------------------------+
//| 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;
   }
};

A classe CHFTSystem ilustra como a pré-alocação pode ser integrada a uma estrutura de HFT. Ela configura todos os arrays e buffers antecipadamente, garantindo que nenhuma solicitação adicional de memória ocorra após o motor de negociação entrar em operação. Buffers circulares são utilizados para manter uma janela deslizante de dados recentes de preços, eliminando realocações custosas. Arrays temporários para cálculos e um buffer dedicado para mensagens de log também são configurados previamente. Ao fazer isso, essa estratégia evita o risco de picos repentinos de alocação justamente quando as condições de mercado estão mais críticas.

Arquivos Mapeados em Memória para Grandes Conjuntos de Dados

Algumas estratégias de negociação dependem de enormes quantidades de dados históricos, às vezes mais do que a memória RAM disponível consegue suportar. Embora o MQL5 não ofereça suporte nativo a arquivos mapeados em memória, é possível simular essa abordagem utilizando operações padrão de entrada e saída de arquivos:

//+------------------------------------------------------------------+
//| 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);
      }
   }
};

A classe CDatasetMapper simula o mapeamento de memória lendo e gravando registros de tamanho fixo em um arquivo binário e armazenando os itens acessados mais recentemente em um pequeno cache na memória. Esse projeto permite trabalhar com conjuntos de dados de tamanho praticamente ilimitado, mantendo uma sobrecarga de desempenho administrável ao ler dados sequenciais ou registros próximos. Embora não seja um verdadeiro mapeamento de memória em nível de sistema operacional, ele oferece muitas das mesmas vantagens, especialmente a capacidade de processar grandes volumes de dados sem esgotar a memória do sistema.


Conclusão

A otimização de memória não se resume apenas a economizar alguns bytes, trata-se de velocidade, estabilidade e controle. No MQL5, onde cada milissegundo conta, um gerenciamento inteligente da memória torna-se uma verdadeira vantagem competitiva.

Neste artigo, exploramos estratégias práticas que vão muito além da teoria: compreender o modelo interno de memória do MQL5, reutilizar objetos para reduzir sobrecarga, criar estruturas de dados amigáveis ao cache e construir pools de memória personalizados para ambientes de negociação de alta frequência.

A regra de ouro? Não adivinhe, meça. O perfilamento revela onde estão os verdadeiros gargalos, permitindo otimizações precisas. Seja através da pré-alocação de memória para evitar latência em tempo de execução ou da simulação de mapeamento de memória para trabalhar eficientemente com conjuntos de dados massivos, cada técnica apresentada possui um único objetivo: tornar suas aplicações MQL5 mais rápidas e mais resilientes.

Aplique apenas algumas dessas técnicas e você perceberá a diferença. Seus sistemas serão mais enxutos, mais rápidos e mais preparados para lidar com as exigências da negociação algorítmica moderna.

Este não é o fim, é apenas a linha de partida. Continue experimentando, continue refinando e leve seu desempenho ao próximo nível.

Boas negociações! Boa programação!


Todo o código referenciado neste artigo está anexado abaixo. A tabela a seguir descreve todos os arquivos de código-fonte que acompanham o artigo.

Nome do Arquivo Descrição
BenchmarkMemoryOperations.mq5 Código demonstrando como realizar benchmarks e comparar operações de memória, como alocação de arrays, reutilização e concatenação de strings no MQL5.
MemoryPoolUsage.mq5 Implementação de exemplo demonstrando como utilizar pools de memória personalizados para alocações de tamanho variável no MQL5.
PriceBufferUsage.mq5  Script de exemplo mostrando o uso prático de um buffer circular de preços para o tratamento eficiente de séries temporais.
SignalPoolUsage.mq5 Exemplo ilustrando como utilizar um pool de objetos para gerenciar eficientemente objetos de sinais de negociação usados com frequência.
CDatasetMapper.mqh Arquivo de cabeçalho contendo a implementação de um mecanismo simulado de arquivo mapeado em memória para manipulação de grandes conjuntos de dados.
CHFTSystem.mqh Arquivo de cabeçalho que define uma classe para sistemas de negociação de alta frequência utilizando estratégias de pré-alocação para minimizar a latência.
CMemoryProfiler.mqh Arquivo de cabeçalho que define uma classe simples de criação de perfil de memória para medir o uso de memória em aplicações MQL5.
COHLCData.mqh Arquivo de cabeçalho com uma estrutura de dados otimizada para cache, projetada para armazenar dados de preços OHLC de forma eficiente.
CPriceBuffer.mqh Arquivo de cabeçalho contendo a implementação de buffer circular otimizada para armazenamento e recuperação rápida de dados de preços.



Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/17693

Últimos Comentários | Ir para discussão (6)
Vladislav Boyko
Vladislav Boyko | 7 abr. 2025 em 15:18
Stanislav Korotky #:
Para este exemplo com OHLCV, a fim de torná-lo mais adequado em termos de eficiência de memória e tempo, provavelmente seria mais interessante agrupar todos os valores em uma única matriz 2D ou mesmo 1D:

Uma matriz 2D em vez de uma matriz de estruturas pode economizar um pouco de tempo de processador, mas aumentará consideravelmente o tempo que o desenvolvedor gasta no desenvolvimento e na manutenção do código. Na minha opinião pessoal. Concordo com o restante de suas afirmações.

Vladislav Boyko
Vladislav Boyko | 7 abr. 2025 em 15:55

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

Vejamos um exemplo problemático:

// Abordagem ineficiente — cria novos arrays a cada tick
void OnTick()
{
   // Isso cria uma nova matriz a cada tick
   double prices[];
   ArrayResize(prices, 1000);
   
   // Preencher a matriz com os dados de preços
   for(int i = 0; i < 1000; i++)
   {
      prices[i] = iClose(_Symbol, PERIOD_M1, i);
   }
   
   // Processar os dados...
   
   // A matriz acabará sendo recolhida pelo gerenciador de memória, mas isso
   // gera desperdício desnecessário de memória
}

Uma abordagem mais eficiente seria:

// Variável de membro da classe - criada uma única vez
double prices[];

void OnTick()
{
   // Reutilizar a matriz existente
   for(int i = 0; i < 1000; i++)
   {
      prices[i] = iClose(_Symbol, PERIOD_M1, i);
   }
   
   // Processar os dados...
}

Stanislav Korotky #:

O artigo parece bastante discutível (apenas alguns pontos).

Qual é a classe que você mencionou aqui?

Pela presença do manipulador OnTick e pela forma como a matriz é acessada, fica implícito que você adicionou a matriz de preços ao escopo global, o que é uma má ideia (devido à poluição do namespace, se a matriz for necessária apenas no escopo do manipulador). Provavelmente seria mais apropriado manter o código inicial do mesmo exemplo, mas tornar a matriz estática; dessa forma, todos veriam claramente a diferença:

Pelo que entendi, esse exemplo (que citei acima) é, grosso modo, pseudocódigo. Ou seja, o autor não presta atenção ao seguinte (para se concentrar no que exatamente está falando, eu acho):

  • A julgar pela condição do loop, o tamanho da matriz é conhecido em tempo de compilação, mas, mesmo assim, a matriz é dinâmica.
  • Embora a matriz seja dinâmica, ArrayResize não foi chamado no código que demonstra a abordagem eficiente.
  • Em termos de eficiência, suspeito que seria melhor substituir todo o loop a seguir por uma única chamada a CopySeries:

   // Reutilizar a matriz existente
   for(int i = 0; i < 1000; i++)
   {
      prices[i] = iClose(_Symbol, PERIOD_M1, i);
   }
Vladislav Boyko
Vladislav Boyko | 7 abr. 2025 em 16:03
Vladislav Boyko #:
Em termos de eficiência, acho que seria melhor substituir todo o loop a seguir por uma única chamada a CopySeries:

Corrija-me se eu estiver errado, mas, pelo que me lembro, cada chamada iClose contém uma chamada CopySeries por baixo do capô.

Too Chee Ng
Too Chee Ng | 1 jun. 2025 em 15:31

Este artigo apresenta um conteúdo perspicaz e instigante para discussão.

A apresentação técnica é clara e bem explicada, facilitando a compreensão do leitor e mantendo seu interesse.

Muito obrigado.

Aleksey Vyazmikin
Aleksey Vyazmikin | 5 set. 2025 em 15:44

Em artigos como este, são necessários testes comparativos motivadores, que demonstrem de fato a eficácia das abordagens propostas.

A tradução está um pouco confusa; sem analisar o código, não é fácil compreendê-la.

Técnicas do MQL5 Wizard que você deve saber (Parte 59): Aprendizado por Reforço (DDPG) com Padrões da Média Móvel e do Oscilador Estocástico Técnicas do MQL5 Wizard que você deve saber (Parte 59): Aprendizado por Reforço (DDPG) com Padrões da Média Móvel e do Oscilador Estocástico
Continuamos nosso último artigo sobre DDPG com indicadores de Média Móvel e Estocástico, examinando outras classes-chave de Aprendizado por Reforço cruciais para a implementação do DDPG. Embora estejamos codificando principalmente em Python, será exportado para o formato ONNX para o MQL5, onde a integraremos como um recurso em um Expert Advisor montado pelo Wizard.
Algoritmo do Duelista - Duelist Algorithm Algoritmo do Duelista - Duelist Algorithm
E se as suas estratégias de trading pudessem aprender umas com as outras, como verdadeiros combatentes? O Duelist Algorithm é um novo método de otimização em que os parâmetros dos sistemas de trading realmente duelam entre si pelo direito de serem chamados os melhores.
Está chegando o novo MetaTrader 5 e MQL5 Está chegando o novo MetaTrader 5 e MQL5
Esta é apenas uma breve resenha do MetaTrader 5. Eu não posso descrever todos os novos recursos do sistema por um período tão curto de tempo - os testes começaram em 09.09.2009. Esta é uma data simbólica, e tenho certeza que será um número de sorte. Alguns dias passaram-se desde que eu obtive a versão beta do terminal MetaTrader 5 e MQL5. Eu ainda não consegui testar todos os seus recursos, mas já estou impressionado.
Redes neurais em trading: decomposição em vez de escalonamento (Conclusão) Redes neurais em trading: decomposição em vez de escalonamento (Conclusão)
Apresentamos um algoritmo que decompõe séries temporais em camadas semânticas e constrói, a partir delas, um modelo enxuto. Mostramos, passo a passo, a arquitetura, a implementação prática em MQL5/OpenCL e os resultados de testes reais com dados históricos de mercado.