English
preview
Erweiterte Speicherverwaltung und Optimierungstechniken in MQL5

Erweiterte Speicherverwaltung und Optimierungstechniken in MQL5

MetaTrader 5Tester | 27 Juni 2025, 08:14
17 5
Sahil Bagdi
Sahil Bagdi
  1. Einführung
  2. Verständnis der Speicherverwaltung in MQL5
  3. Profilierung und Messung der Speichernutzung
  4. Implementierung von nutzerdefinierten Speicherpools
  5. Optimierte Datenstrukturen für Handelsanwendungen
  6. Fortgeschrittene Techniken für den Hochfrequenzhandel
  7. Schlussfolgerung


Einführung

Herzlich willkommen! Wenn Sie schon einmal Zeit damit verbracht haben, Handelssysteme in MQL5 zu erstellen, sind Sie wahrscheinlich auf diese frustrierende Wand gestoßen - Ihr Expert Advisor beginnt zu verzögern, die Speichernutzung schießt in die Höhe, oder schlimmer noch, das ganze Ding stürzt ab, wenn der Markt interessant wird. Kommt Ihnen das bekannt vor?

MQL5 ist unbestreitbar leistungsstark, aber mit dieser Leistung geht auch Verantwortung einher - vor allem, wenn es um den Speicher geht. Viele Entwickler konzentrieren sich ausschließlich auf Strategielogik, Einstiegspunkte und Risikomanagement, während die Speicherverwaltung im Hintergrund zu einer tickenden Zeitbombe wird. Bei der Skalierung Ihres Codes - Verarbeitung von mehr Symbolen, höheren Frequenzen und umfangreicheren Datensätzen - kann die Vernachlässigung des Speichers zu Leistungsengpässen, Instabilität und verpassten Chancen führen.

In diesem Artikel gehen wir unter die Haube. Wir werden untersuchen, wie der Speicher in MQL5 wirklich funktioniert, die häufigsten Fallen, die Ihre Systeme verlangsamen oder zum Ausfall führen, und - was am wichtigsten ist - wie man sie beheben kann. Sie lernen praktische Optimierungstechniken kennen, die Ihre Handelsprogramme schneller, schlanker und zuverlässiger machen.

Hier ist eine effiziente Speichernutzung besonders wichtig:

  • Hochfrequenzhandel: Jede Millisekunde ist ein potenzieller Vorteil - oder ein potenzieller Verlust.

  • Multi-Timeframe-Analyse: Kombinierte Charts? Erwarten Sie, dass sich der Speicherdruck vervielfachen wird.

  • Schwere Indikatorlogik: Komplexe mathematische Berechnungen und große Datensätze können alles zum Stillstand bringen, wenn sie nicht verwaltet werden.

  • Backtests mit großen Kurshistorien: Ohne intelligente Optimierung können sich Backtests anfühlen, als würde man Farbe beim Trocknen zusehen.

Wenn Sie bereit sind, sich ernsthaft mit der Leistung auseinanderzusetzen, lassen Sie uns eintauchen - und Ihre MQL5-Systeme so effizient wie intelligent machen.

In den folgenden Abschnitten gehen wir Schritt für Schritt vor - von den grundlegenden Konzepten der MQL5-Speicherzuweisung bis hin zu fortgeschrittenen Techniken und Beispielen, die sich auf den Code beziehen. Wenn Sie diese Praktiken befolgen, verfügen Sie über das Know-how, um schnellere, schlankere und widerstandsfähigere Handelssysteme zu entwickeln, die den hohen Anforderungen des modernen algorithmischen Handels gewachsen sind. Fangen wir an!


Verständnis der Speicherverwaltung in MQL5

Wenn man sich an anspruchsvollere Optimierungsstrategien in MQL5 heranwagt, ist es wichtig, zunächst zu verstehen, wie die Sprache den Speicher hinter den Kulissen handhabt. Obwohl MQL5 im Allgemeinen Speicheraufgaben im Vergleich zu einer Sprache mit niedrigerem Niveau wie C++ rationalisiert, müssen die Entwickler immer noch effiziente Codierungspraktiken anwenden.

Unterscheidung zwischen Stack- und Heap-Speicher

MQL5, wie viele moderne Sprachen, teilt seine Speichernutzung zwischen dem Stack und dem Heap auf:

  1. Stack-Speicher: Dies ist der Ort, an dem lokale Variablen abgelegt werden, wenn ihre Größe zur Kompilierungszeit bekannt ist. Sie wird automatisch verwaltet, und die Zuteilung erfolgt hier sehr schnell.
  2. Heap-Speicher: Wird für Szenarien verwendet, in denen Sie Speicher dynamisch zuweisen müssen - etwa wenn die Größe erst zur Laufzeit bestimmt wird oder ein Objekt über den Geltungsbereich einer einzelnen Funktion hinaus bestehen bleiben muss.

Betrachten wir ein einfaches Beispiel, um den Unterschied zu verdeutlichen:
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
}

Obwohl MQL5 eine automatische Garbage Collection enthält, kann die alleinige Nutzung dieser Funktion zu Ineffizienzen führen, insbesondere in Hochfrequenzhandelsumgebungen.

Speicherlebenszyklus in MQL5

Um die Leistung zu optimieren, ist es hilfreich, den Weg des Speichers durch Ihr MQL5-Programm zu verfolgen:

  1. Initialisierung: Wenn Ihr Expert Advisor (EA) oder Indikator startet, gibt MQL5 Speicher für alle globalen Variablen und Klasseninstanzen frei.
  2. Ereignisbehandlung: Jedes Mal, wenn ein Ereignis ausgelöst wird - z. B. OnTick() oder OnCalculate() - richtet das System lokale Variablen auf dem Stack ein. Er kann auch auf den Heap zurückgreifen, wenn er mehr dynamische Zuweisungen benötigt.
  3. Freisetzung: In dem Moment, in dem lokale Variablen den Gültigkeitsbereich verlassen, wird der Stapelspeicher automatisch zurückgewonnen. Heap-Zuweisungen werden jedoch in der Regel später durch den Garbage Collector freigegeben.
  4. Beendigung: Sobald Ihr Programm beendet wird, wird der verbleibende Speicher vollständig freigegeben.

Der springende Punkt ist, dass MQL5 zwar die Deallokation handhabt, dies aber nicht immer sofort oder auf die für zeitkritische Handelsaufgaben optimale Weise tut.

Häufige Fallstricke bei der Speicherung

Trotz der automatischen Garbage Collection ist es möglich, dass Speicherlecks oder eine träge Speichernutzung auftreten.

Hier sind einige häufige Übeltäter:

  1. Exzessive Objekterstellung: Das ständige Erzeugen neuer Objekte in häufig aufgerufenen Funktionen (wie OnTick) kann Ressourcen verbrauchen.
  2. Große Arrays: Die Speicherung großer Arrays, die während des gesamten Programmlaufs bestehen bleiben, kann unnötig viel Speicher verschlingen.
  3. Zirkuläre Referenzen: Wenn zwei Objekte auf einander verweisen, kann dies die Garbage Collection verzögern oder stören.
  4. Unangemessenes Ressourcenmanagement: Wenn Sie vergessen, Dateien, Datenbankverbindungen oder andere Systemressourcen zu schließen, kann dies zu Speicherverschwendung führen.
Schauen wir uns ein problematisches Beispiel an:
// 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
}

Ein effizienterer Ansatz wäre folgender:

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

Oft sind es nur geringfügige Änderungen, wie die Wiederverwendung von Objekten anstelle ihrer wiederholten Instanziierung, die einen wesentlichen Unterschied ausmachen können, insbesondere in schnelllebigen Handelsumgebungen.

Muster für die Speicherzuweisung in MQL5

MQL5 verwendet je nach Datentyp unterschiedliche Speicherzuweisungsmuster:

Schließlich ist es hilfreich zu wissen, wie MQL5 gängige Datentypen zuweist:

  1. Primitive Typen (int, double, bool, etc.)
    Diese werden in der Regel dem Stack zugewiesen, wenn sie als lokale Variablen deklariert werden.

  2. Arrays
    Dynamische Arrays in MQL5 werden auf dem Heap gespeichert.

  3. Zeichenketten (Strings)
    MQL5-Strings verwenden Referenzzählung und leben auf dem Heap.

  4. Objekte
    Instanzen von Klassen befinden sich ebenfalls auf dem Heap.

Wenn Sie diese Zuweisungsmuster im Hinterkopf behalten, sind Sie besser gerüstet, um Code zu erstellen, der effizienter, stabiler und für reale Handelsbedingungen optimiert ist.


Profilierung und Messung der Speichernutzung

Wenn es darum geht, die Speichernutzung in MQL5 zu optimieren, besteht der erste Schritt darin, genau festzustellen, wo die Engpässe auftreten. Obwohl MQL5 über keine nativen Tools zur Speicherprofilerstellung verfügt, können wir die Ärmel hochkrempeln und einen hausgemachten Ansatz entwickeln.

Erstellung einer einfachen Speicher-Profilierung

Um die Speichernutzung besser in den Griff zu bekommen, können wir eine minimalistische Klasse für die Profilierung einrichten, die die Eigenschaft TERMINAL_MEMORY_AVAILABLE nutzt. Durch den Vergleich des anfänglichen und des aktuell verfügbaren Speichers können Sie verfolgen, wie viel Speicher Ihre Anwendung verbraucht.

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

Wenn Sie die Klasse CMemoryProfiler in Ihrem Projekt haben, sieht die Umsetzung etwa so aus:

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
}

Bei der Initialisierung des Profilers wird bei seiner Erstellung eine Ausgangslage des verfügbaren Speichers aufgezeichnet. Jedes Mal, wenn Sie UpdatePeak() aufrufen, wird geprüft, ob der aktuelle Speicherbedarf Ihrer Anwendung die zuvor gemessene Höchstmarke überschritten hat. Die Methoden GetUsedMemory() und GetPeakUsage() zeigen Ihnen, wie viel Speicher Sie seit der Basislinie verbraucht haben, während PrintReport() eine Zusammenfassung auf dem Terminal protokolliert. Diese Zusammenfassung wird automatisch generiert, wenn der Profiler den Anwendungsbereich verlässt, und zwar mit Hilfe des Klassendestruktors.

Denken Sie daran, dass dieser Ansatz nur die gesamte Speichernutzung des Terminals misst und nicht den Verbrauch Ihres spezifischen Programms. Dennoch ist es eine praktische Möglichkeit, sich einen Überblick darüber zu verschaffen, wie sich Ihre Speichernutzung im Laufe der Zeit entwickelt.

Benchmarking von Speicheroperationen

Bei der Optimierung der Speichernutzung geht es nicht nur darum, zu wissen, wie viel Speicher Sie verwenden, sondern auch darum, zu verstehen, wie schnell verschiedene Speicheroperationen ausgeführt werden. Durch die Zeitmessung verschiedener Vorgänge können Sie feststellen, wo sich Ineffizienzen verbergen, und mögliche Leistungsverbesserungen entdecken.

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

Diese einfache Testfunktion zeigt, wie man die Ausführungsgeschwindigkeit verschiedener speicherintensiver Aufgaben messen kann. Er vergleicht die Leistung der wiederholten Zuweisung von Arrays mit der Wiederverwendung eines einzelnen, bereits zugewiesenen Arrays sowie den Unterschied zwischen einer einfachen Text-Verkettung und einem Ansatz im Stil eines „String Builders“. Diese Tests verwenden GetMicrosecondCount(), um die Zeit in Mikrosekunden zu messen, damit Sie einen genauen Überblick über etwaige Verzögerungen erhalten.

In der Regel werden Ihre Ergebnisse zeigen, dass die Wiederverwendung von Arrays einen klaren Leistungsvorteil gegenüber der Zuweisung neuer Arrays in jeder Schleife bietet und dass das Sammeln von Zeichenkettenteilen in einem Array (und das anschließende Zusammenfügen) stückweise Verkettungen übertrifft. Diese Unterscheidungen sind besonders kritisch in Hochfrequenzhandelsszenarien, in denen es auf den Bruchteil einer Millisekunde ankommen kann.

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

Wenn Sie den Benchmark ausführen, erhalten Sie konkrete Daten darüber, wie die verschiedenen Speicheroperationen in MQL5 abschneiden. Mit diesen Erkenntnissen sind Sie gut gerüstet, um Anpassungen vorzunehmen, die Ihre Handelsroboter schlank und effizient machen.


Implementierung von nutzerdefinierten Speicherpools

Wenn die Leistung im Vordergrund steht, ist eine herausragende Strategie zur Rationalisierung der Speichernutzung das Speicherpooling. Anstatt das System ständig nach Speicher zu fragen und ihn dann wieder zurückzugeben, besteht der Trick darin, einen Teil des Speichers vorab zuzuweisen und ihn selbst zu verwalten. In diesem Abschnitt wird untersucht, wie dies in einfachen und fortgeschrittenen Szenarien geschehen kann.

Grundlegende Objektpool-Implementierung

Stellen Sie sich vor, Sie haben eine Klasse namens CTradeSignal, die Sie häufig instanziieren und zerstören - vielleicht in einem Hochfrequenzhandelssystem. Anstatt wiederholt auf die Speicherzuweisung zuzugreifen, erstellen Sie einen eigenen Pool für diese Objekte. Nachstehend finden Sie ein einfaches Beispiel:

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

Im obigen Ausschnitt weist CTradeSignalPool einen Stapel von CTradeSignal-Objekten vor und verwaltet sorgfältig deren Lebenszyklen. Wenn Sie Acquire() aufrufen, wird Ihnen der Pool ein verfügbares Objekt übergeben. Wenn er keine mehr zur Verfügung hat, vergrößert er sich und macht weiter. Sobald Sie mit einem Objekt fertig sind, gibt Release() es zurück in die Obhut des Pools.

Der Hauptvorteil besteht in einer erheblichen Verringerung des Overheads, der durch das ständige Zuweisen und Freigeben von Speicher entsteht. Dies ist besonders praktisch, wenn Sie Objekte in schnellem Tempo durchgehen, wie z. B. Handelssignale in einer Hochgeschwindigkeitsumgebung.

Nachfolgend ein kurzes Beispiel, wie Sie diesen Pool verwenden könnten:

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

Da der Pool seine Objekte recycelt, verringert er den Aufwand für die wiederholte Erstellung und Zerstörung, der sonst anfallen würde.

Erweiterter Speicherpool für Zuweisungen variabler Größe

Manchmal stehen Sie vor komplexeren Anforderungen, wie z. B. der Notwendigkeit, unterschiedlich große Speicherbereiche zu verwalten. Für diese Fälle können Sie einen fortgeschritteneren Pool bauen:

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

Diese Klasse CMemoryPool richtet einen großen, vorab zugewiesenen Speicherpuffer ein und unterteilt ihn dann in Stücke fester Größe. Wenn Sie Speicher anfordern, sucht es genügend benachbarte Blöcke, um diesen Bedarf zu decken, kennzeichnet sie als belegt und gibt einen Zeiger auf den Anfang dieser Reihe von Blöcken zurück. Wenn Sie den Speicher freigeben, werden diese Blöcke wieder in den Status „verfügbar“ versetzt.

Anstelle von Zuweisungen im C++-Stil verwendet dieser Ansatz MQL5-Array-Funktionen wie ArrayResize, ArrayInitialize und ArrayFree und fügt sich damit nahtlos in das Speicher-Ökosystem von MQL5 ein. Es nutzt auch GetPointer() , das einen sicheren Weg zur Handhabung von Array-Zeigern in MQL5 bietet.

Dies ist der Grund, warum dieser Ansatz so besonders ist:

  1. Weniger Fragmentierungen: Die Verwaltung des Speichers in übersichtlichen Chunks mit fester Größe hilft, die Fragmentierung zu vermeiden, die bei häufigen Zuweisungen auftritt.
  2. Verbesserte Leistung: Einen Speicherblock aus dem eigenen Pool anzufordern ist in der Regel schneller, als jedes Mal die Systemzuweisung in Anspruch zu nehmen.
  3. Verbesserte Sichtbarkeit: Detaillierte Nutzungsstatistiken Ihres Pools können Aufschluss über speicherbezogene Problembereiche geben.
  4. Vorhersehbarkeit: Die Vorab-Zuweisung verringert die Wahrscheinlichkeit von Fehlern, die sich in einem kritischen Moment außerhalb des Speichers befinden.

Dieser robustere Pool ist ideal, wenn Sie Speicherblöcke unterschiedlicher Größe benötigen, beispielsweise für komplizierte Datenstrukturen oder dynamische Arbeitslasten, die sich häufig ändern. Durch die Anpassung Ihrer Pools - sei es ein einfacher Objektpool oder ein leistungsfähigerer Pool mit variabler Größe - können Sie die Speichernutzung unter Kontrolle halten und die Leistung in anspruchsvollen Anwendungen optimieren.


Optimierte Datenstrukturen für Handelsanwendungen

Bei der Verarbeitung von Zeitreihen in Handelsumgebungen benötigen Sie Datenstrukturen, durch die die Leistung nicht hinter dem Markt zurückbleiben. Lassen Sie uns zwei leistungsstarke Strategien für die Speicherung und den Abruf Ihrer Preisdaten mit maximaler Effizienz untersuchen.

Datenspeicher für Zeitreihen, die keinen Tick verpassen

Ein Arbeitspferd in Handelssystemen ist der Puffer der Preishistorie - und ein optimierter Ringspeicher kann diese Last mit Leichtigkeit schultern. Im Folgenden finden Sie ein Beispiel, wie Sie ein solches System einrichten könnten:

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

Hier verwendet die Klasse CPriceBuffer einen zirkulären Puffer, der um ein Array mit fester Größe herum aufgebaut ist. Der Zeiger auf den „Kopf“ verschiebt sich bis zum Ende des Arrays, sodass es möglich ist, neue Preiseinträge ohne aufwändige Größenänderungen hinzuzufügen. Wenn die Kapazität des Puffers erreicht ist, werden die ältesten Einträge einfach mit neuen Daten überschrieben, sodass ein reibungslos gleitendes Fenster mit den neuesten Preisen erhalten bleibt.

Warum dieser Ansatz so effizient ist:

  1. Der Speicher wird vorab zugewiesen und wiederverwendet, wodurch der Overhead durch ständige Erweiterungen entfällt.
  2. Das Hinzufügen neuer Preise und das Abrufen der neuesten Daten erfolgt in O(1) Zeit.
  3. Der Schiebefenster-Mechanismus verwaltet alte und neue Einträge automatisch und problemlos.

Nachfolgend finden Sie einen kurzen Ausschnitt, der zeigt, wie Sie diese Funktion nutzen können:

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

Cache-freundliche Strukturen für extra Zip

Moderne CPUs leben von der Cache-Effizienz. Wenn Sie die Daten so anordnen, dass der Prozessor nur die Teile abruft, die er benötigt, können Sie die Zeitverschwendung erheblich reduzieren. Schauen Sie sich dieses Layout für die Speicherung von OHLC-Daten (Open, High, Low, Close) an:

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

Die Klasse COHLCData unterteilt jedes Attribut (z. B. high oder low) in ein eigenes Array - eine Struktur der Arrays (SoA) - anstelle des traditionelleren Arrays der Structuren (AoS). Warum ist das wichtig? Angenommen, Sie möchten den Durchschnitt aller Hochs berechnen. Bei einem SoA-Setup gleitet der Prozessor durch eine zusammenhängende Reihe der Hochs und muss weniger Speicherzugriffe vornehmen. Im Gegensatz dazu ist die CPU bei einem AoS gezwungen, die Daten von Open, Low, Close und Volume zu überspringen, nur um jedes Hoch zu erfassen.

Mit COHLCData wird es Ihnen leicht fallen:

  1. Fügen Sie neue OHLC-Balken im laufenden Betrieb hinzu.
  2. Abrufen bestimmter Balken nach Index.
  3. Führen Sie Berechnungen auf jedem einzelnen Feld durch (z. B. alle Höchstwerte), ohne über nicht verwandte Daten zu stolpern.

Diese Design-Entscheidung bedeutet, dass Ihre technische Analyse - egal ob gleitende Durchschnitte, Volatilitätsberechnungen oder einfach nur die Suche nach Ausbrüchen - dank der besseren Cache-Lokalisierung viel effizienter abläuft.


Fortgeschrittene Techniken für den Hochfrequenzhandel

Der Hochfrequenzhandel (HFT) erfordert extrem niedrige Latenzzeiten und konstante Leistung. Selbst die kleinste Verzögerung kann die Ausführung des Handels stören und zu verpassten Chancen führen. Im Folgenden werden zwei zentrale Ansätze - Vorabzuweisung und simulierte Speicherzuordnung - untersucht, die dazu beitragen können, die Latenzzeit in MQL5 auf ein absolutes Minimum zu reduzieren.

Strategien vor der Zuteilung

Wenn Ihr System innerhalb von Mikrosekunden reagieren muss, können Sie sich keine unvorhersehbaren Verzögerungen leisten, die durch fliegende Speicherzuweisung verursacht werden. Die Lösung ist die Vorabzuweisung - reservieren Sie im Voraus den gesamten Speicher, den Ihre Anwendung möglicherweise benötigt, sodass Sie in Spitzenzeiten nie mehr Speicher zuweisen müssen.

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

Die Klasse CHFTSystem veranschaulicht, wie die Vorabzuteilung in einen HFT-Rahmen integriert werden kann. Sie richtet alle Arrays und Puffer im Voraus ein und stellt sicher, dass keine zusätzlichen Speicheranforderungen auftreten, sobald die Handelsmaschine in Betrieb ist. Zirkuläre Puffer werden verwendet, um ein gleitendes Fenster aktueller Preisdaten aufrechtzuerhalten, wodurch eine kostspielige Neuzuweisung entfällt. Temporäre Arrays für Berechnungen und ein spezieller Puffer für Protokollmeldungen werden ebenfalls im Voraus eingerichtet. Auf diese Weise vermeidet diese Strategie das Risiko von plötzlichen Allokationsspitzen, wenn die Marktbedingungen am kritischsten sind.

Memory-Mapped Files für große Datensätze

Einige Handelsstrategien stützen sich auf riesige Mengen historischer Daten - manchmal mehr, als Ihr verfügbarer Arbeitsspeicher verarbeiten kann. Während MQL5 keine nativen memory-mapped Dateien unterstützt, können Sie den Ansatz mit Standard-Dateieingabe/Ausgabe emulieren:

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

Die Klasse CDatasetMapper simuliert das Memory Mapping, indem sie Datensätze fester Größe in eine Binärdatei liest und schreibt und die zuletzt aufgerufenen Elemente in einem kleinen In-Memory-Cache speichert. So können Sie mit Datensätzen von praktisch unbegrenzter Größe arbeiten und gleichzeitig den Leistungsaufwand beim Lesen von sequentiellen Daten oder nahe beieinander liegenden Datensätzen in Grenzen halten. Obwohl es sich nicht um ein echtes Speicher-Mapping auf Betriebssystemebene handelt, bietet es viele der gleichen Vorteile - insbesondere die Möglichkeit, umfangreiche Datensätze zu verarbeiten, ohne den Systemspeicher zu erschöpfen.


Schlussfolgerung

Bei der Speicheroptimierung geht es nicht nur darum, ein paar Bytes einzusparen - es geht um Geschwindigkeit, Stabilität und Kontrolle. Bei MQL5, wo jede Millisekunde zählt, wird die intelligente Speicherverwaltung zu einem echten Wettbewerbsvorteil.

In diesem Artikel haben wir praktische Strategien untersucht, die weit über die Theorie hinausgehen: das Verständnis des internen Speichermodells von MQL5, die Wiederverwendung von Objekten zur Verringerung des Overheads, die Entwicklung von Cache-freundlichen Datenstrukturen und der Aufbau von nutzerdefinierten Speicherpools für Hochfrequenzhandelsumgebungen.

Die goldene Regel? Raten Sie nicht, sondern messen Sie. Die Profilierung deckt auf, wo die wirklichen Engpässe liegen, sodass Sie präzise optimieren können. Ob es sich um die Vorabzuweisung von Speicher zur Vermeidung von Laufzeitlatenz oder um die Simulation von Speicherzuordnungen zur effizienten Arbeit mit großen Datensätzen handelt, jede von uns behandelte Technik dient einem Zweck: Ihre MQL5-Anwendungen schneller und stabiler zu machen.

Wenden Sie auch nur einige dieser Techniken an, und Sie werden den Unterschied spüren. Ihre Systeme werden schlanker, schneller und besser für die Anforderungen des modernen algorithmischen Handels gerüstet sein.

Das ist nicht das Ende - es ist nur der Anfang. Experimentieren Sie weiter, verfeinern Sie weiter und bringen Sie Ihre Leistung auf die nächste Stufe.

Viel Spaß beim Handeln! Viel Spaß beim Coding!


Der gesamte Code, auf den in dem Artikel verwiesen wird, ist unten angefügt. In der folgenden Tabelle sind alle Quellcodedateien beschrieben, die dem Artikel beigefügt sind.

Dateiname Beschreibung
BenchmarkMemoryOperations.mq5 Code, der zeigt, wie man Speicheroperationen wie Array-Zuweisung, Wiederverwendung und String-Verkettung in MQL5 bewertet und vergleicht.
MemoryPoolUsage.mq5 Beispielimplementierung, die zeigt, wie man nutzerdefinierte Speicherpools für Zuweisungen variabler Größe in MQL5 verwendet.
PriceBufferUsage.mq5  Beispielskript, das die praktische Verwendung eines zirkulären Preispuffers für die effiziente Verarbeitung von Zeitreihendaten zeigt.
SignalPoolUsage.mq5 Beispiel für die Verwendung eines Objektpools zur effizienten Verwaltung häufig verwendeter Handelssignalobjekte.
CDatasetMapper.mqh Header-Datei, die die Implementierung eines simulierten Memory-Mapped-File-Mechanismus zur Verarbeitung großer Datenmengen enthält.
CHFTSystem.mqh Header-Datei, die eine Klasse für Hochfrequenzhandelssysteme definiert, die Vorabzuweisungsstrategien zur Minimierung der Latenzzeit verwenden.
CMemoryProfiler.mqh Header-Datei, die eine einfache Klasse zur Profilierung des Speichers zur Messung der Speichernutzung in MQL5-Anwendungen definiert.
COHLCData.mqh Header-Datei mit einer cache-freundlichen Datenstruktur, die für die effiziente Speicherung von OHLC-Preisdaten optimiert ist.
CPriceBuffer.mqh Header-Datei, die die Implementierung des Ringspeichers enthält, der für die schnelle Speicherung und den schnellen Abruf von Preisdaten optimiert ist.



Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/17693

Letzte Kommentare | Zur Diskussion im Händlerforum (5)
Stanislav Korotky
Stanislav Korotky | 7 Apr. 2025 in 15:07

Der Artikel sieht sehr fragwürdig aus (nur ein paar Punkte).

Was ist die Klasse, die Sie hier erwähnt haben?

// Klassenvariable - created once
double prices[];

void OnTick()
{
   // Wiederverwendung des vorhandenen Arrays
   for(int i = 0; i < 1000; i++)
   {
      prices[i] = iClose(_Symbol, PERIOD_M1, i);
   }
   
   // Verarbeiten Sie die Daten...
}

Das Vorhandensein des OnTick-Handlers und die Art und Weise, wie auf das Array zugegriffen wird, deutet darauf hin, dass Sie das Preis-Array in den globalen Bereich eingefügt haben, was eine schlechte Idee ist (wegen der Namespace-Verschmutzung, wenn das Array nur im Bereich des Handlers benötigt wird). Wahrscheinlich wäre es angemessener, den ursprünglichen Code aus dem gleichen Beispiel beizubehalten, aber das Array statisch zu machen, so dass jeder den Unterschied klar erkennen kann:

// Effizienter Ansatz (weist das Array einmal zu, kann seine Größe bei Bedarf anpassen)
void OnTick()
{
   // Dies bedeutet NICHT create (nor allocate) array on every tick
   static double prices[];
   ArrayResize(prices, 1000);
   
   // Füllen des Arrays mit Preisdaten
   for(int i = 0; i < 1000; i++)
   {
      prices[i] = iClose(_Symbol, PERIOD_M1, i);
   }
   
   // Verarbeiten Sie die Daten...
}

Auch wenn Sie Array of Structures (AoS) mit Structure of Arrays (SoA) für OHLCV ersetzen - der Zugriff auf die Preise der gleichen Bar braucht mehr Referenzen (Umschalten zwischen Arrays statt Inkrementierung Offset innerhalb einer einzigen Struktur) und verlangsamt den Prozess, aber solche Operationen sind sehr häufig.

Für dieses Beispiel mit OHLCV wäre es aus Gründen der Speicher- und Zeiteffizienz wahrscheinlich interessanter, alle Werte in ein einziges 2D- oder sogar 1D-Array zu packen:

double TOHLCV[][6];

Dies ist möglich, weil alle Werte der Typen (double, datetime, long) die gleiche Größe von 8 Byte haben und direkt ineinander gecastet werden können.

Vladislav Boyko
Vladislav Boyko | 7 Apr. 2025 in 15:18
Stanislav Korotky #:
Für dieses Beispiel mit OHLCV wäre es aus Gründen der Speicher- und Zeiteffizienz wahrscheinlich interessanter, alle Werte in ein einziges 2D- oder sogar 1D-Array zu packen:

Ein 2D-Array anstelle eines Arrays von Strukturen spart vielleicht ein wenig Prozessorzeit, aber es erhöht den Zeitaufwand des Entwicklers für die Entwicklung und Pflege des Codes erheblich. Meiner persönlichen Meinung nach stimme ich mit dem Rest Ihrer Aussagen überein.

Vladislav Boyko
Vladislav Boyko | 7 Apr. 2025 in 15:55

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

Schauen wir uns ein problematisches Beispiel an:

// Ineffizienter Ansatz - erstellt bei jedem Tick neue Arrays
void OnTick()
{
   // Dadurch wird bei jedem Tick ein neues Array erstellt
   double prices[];
   ArrayResize(prices, 1000);
   
   // Füllen des Arrays mit Preisdaten
   for(int i = 0; i < 1000; i++)
   {
      prices[i] = iClose(_Symbol, PERIOD_M1, i);
   }
   
   // Verarbeiten Sie die Daten...
   
   // Array wird irgendwann zu Müll verarbeitet, aber das
   // erzeugt unnötigen Speicherplatzmangel
}

Ein effizienterer Ansatz wäre:

// Klassenvariable - einmal erstellt
double prices[];

void OnTick()
{
   // Wiederverwendung des vorhandenen Arrays
   for(int i = 0; i < 1000; i++)
   {
      prices[i] = iClose(_Symbol, PERIOD_M1, i);
   }
   
   // Verarbeiten Sie die Daten...
}

Stanislav Korotky #:

Der Artikel sieht sehr fragwürdig aus (nur ein paar Punkte).

Was ist die Klasse, die Sie hier erwähnt haben?

Das Vorhandensein des OnTick-Handlers und die Art und Weise, wie auf das Array zugegriffen wird, deutet darauf hin, dass Sie das Preis-Array in den globalen Bereich eingefügt haben, was eine schlechte Idee ist (wegen der Namespace-Verschmutzung, wenn das Array nur im Bereich des Handlers benötigt wird). Wahrscheinlich wäre es angemessener, den ursprünglichen Code des gleichen Beispiels beizubehalten, aber das Array statisch zu machen, so dass jeder den Unterschied klar erkennen kann:

Soweit ich es verstanden habe, handelt es sich bei dem Beispiel (das ich oben zitiert habe) grob gesagt um Pseudocode, d.h. der Autor achtet nicht auf das Folgende (um sich auf das zu konzentrieren, wovon er genau spricht, nehme ich an):

  • Nach der Schleifenbedingung zu urteilen, ist die Größe des Arrays zur Kompilierzeit bekannt, aber dennoch ist das Array dynamisch.
  • Obwohl das Array dynamisch ist, wurde ArrayResize in dem Code, der den effizienten Ansatz demonstriert, nicht aufgerufen.
  • Im Hinblick auf die Effizienz wäre es vermutlich besser, die gesamte folgende Schleife durch einen einzigen CopySeries-Aufruf zu ersetzen:

   // Wiederverwendung des vorhandenen Arrays
   for(int i = 0; i < 1000; i++)
   {
      prices[i] = iClose(_Symbol, PERIOD_M1, i);
   }
Vladislav Boyko
Vladislav Boyko | 7 Apr. 2025 in 16:03
Vladislav Boyko #:
Im Hinblick auf die Effizienz, ich vermute, es wäre besser, die gesamte folgende Schleife mit einem einzigen CopySeries Aufruf zu ersetzen:

Korrigieren Sie mich, wenn ich falsch liege, aber soweit ich mich erinnere, enthält jeder iClose-Aufruf einen CopySeries-Aufruf unter der Haube.

Too Chee Ng
Too Chee Ng | 1 Juni 2025 in 15:31

Der vorliegende Artikel enthält aufschlussreiche und zum Nachdenken anregende Inhalte, die zur Diskussion anregen.

Die technische Darstellung ist klar und gut erklärt, so dass der Leser ihr leicht folgen und sich mit ihr beschäftigen kann.

Ich danke Ihnen vielmals.

Die Übertragung der Trading-Signale in einem universalen Expert Advisor. Die Übertragung der Trading-Signale in einem universalen Expert Advisor.
In diesem Artikel wurden die verschiedenen Möglichkeiten beschrieben, um die Trading-Signale von einem Signalmodul des universalen EAs zum Steuermodul der Positionen und Orders zu übertragen. Es wurden die seriellen und parallelen Interfaces betrachtet.
Entwicklung eines Toolkit zur Analyse von Preisaktionen (Teil 18): Einführung in die Quarters-Theorie (III) - Quarters Board Entwicklung eines Toolkit zur Analyse von Preisaktionen (Teil 18): Einführung in die Quarters-Theorie (III) - Quarters Board
In diesem Artikel erweitern wir das ursprüngliche Quarters-Skript durch die Einführung des Quarters-Boards, einem Werkzeug, mit dem Sie direkt im Chart zwischen den Viertelstufen umschalten können, ohne den Code erneut aufrufen zu müssen. Sie können ganz einfach bestimmte Levels aktivieren oder deaktivieren, und der EA bietet auch Kommentare zur Trendrichtung, damit Sie Marktbewegungen besser verstehen können.
Eine alternative Log-datei mit der Verwendung der HTML und CSS Eine alternative Log-datei mit der Verwendung der HTML und CSS
In diesem Artikel werden wir eine sehr einfache, aber leistungsfähige Bibliothek zur Erstellung der HTML-Dateien schreiben, dabei lernen wir auch, wie man eine ihre Darstellung einstellen kann (nach seinem Geschmack) und sehen wir, wie man es leicht in seinem Expert Advisor oder Skript hinzufügen oder verwenden kann.
MQL5 Handels-Toolkit (Teil 8): Implementierung und Verwendung der EX5-Bibliothek History Manager in Ihrer Codebasis MQL5 Handels-Toolkit (Teil 8): Implementierung und Verwendung der EX5-Bibliothek History Manager in Ihrer Codebasis
Im letzten Artikel dieser Serie erfahren Sie, wie Sie die EX5-Bibliothek History Manager mühelos in Ihren MQL5-Quellcode importieren und nutzen können, um Handelshistorien in Ihrem MetaTrader 5-Konto zu verarbeiten. Mit einfachen einzeiligen Funktionsaufrufen in MQL5 können Sie Ihre Handelsdaten effizient verwalten und analysieren. Darüber hinaus werden Sie lernen, wie Sie verschiedene Skripte zur Analyse der Handelshistorie erstellen und einen preisbasierten Expert Advisor als praktisches Anwendungsbeispiel entwickeln können. Der Beispiel-EA nutzt Kursdaten und die EX5-Bibliothek History Manager, um fundierte Handelsentscheidungen zu treffen, Handelsvolumina anzupassen und Wiederherstellungsstrategien auf der Grundlage zuvor abgeschlossener Handelsgeschäfte zu implementieren.