How to Implement and Use Arrays of Structures in MQL5?

As MQL5 developers, effectively managing and organizing data is paramount for building robust and efficient trading systems, custom indicators, and scripts. While simple arrays of primitive types (int, double, string, etc.) are fundamental, often we need to group related pieces of data together under a single entity. This is where structures come into play. Combining structures with arrays provides a powerful mechanism to handle collections of complex data types.

Introduction to Arrays of Structures in MQL5

Efficient data handling is critical in algorithmic trading. Whether you’re tracking multiple open positions, analyzing historical market data, or managing complex parameters for an indicator, you often deal with related sets of values. Arrays of structures provide a highly organized way to manage these collections.

What are Structures in MQL5?

In MQL5, a structure (defined using the struct keyword) is a composite data type that allows you to bundle variables of different data types under a single name. Think of it as a blueprint for creating objects that hold several pieces of related information. Unlike classes, structures in MQL5 are primarily data containers and do not support inheritance, virtual functions, or protected/private access specifiers (though they can contain methods and static members in MQL5).

For example, a structure to represent a trading position might look like this:

struct PositionInfo
{
    ulong  ticket;
    string symbol;
    ENUM_POSITION_TYPE type;
    double volume;
    double price;
    double profit;
    datetime open_time;
};

This PositionInfo structure groups a ticket number, symbol, type, volume, price, profit, and open time, all related to a single trading position.

Why Use Arrays of Structures?

Using arrays of structures offers several significant advantages for MQL5 development:

  • Data Organization: It allows you to keep related data together logically. Instead of managing separate arrays for tickets, symbols, volumes, etc., you have a single array where each element represents a complete record (e.g., one position).
  • Code Readability: Code becomes much cleaner and easier to understand when you pass around single structure objects or arrays of structures instead of multiple individual variables.
  • Modularity: Structures promote modularity by encapsulating related data. This makes your code more maintainable and reusable.
  • Efficiency: Accessing data within a structure array can be efficient as related data is often stored contiguously in memory.
  • Handling Collections: They are ideal for managing collections of similar complex entities, such as a list of open orders, pending orders, historical deals, or parameters for multiple indicator buffers.

Consider retrieving all open positions. Without structures, you’d fetch ticket, symbol, volume, etc., into separate arrays and manage indices across all of them. With an array of PositionInfo structures, you fetch data into a single array, and each element positions[i] contains all details for the i-th position.

Basic Syntax and Declaration

Declaring an array of structures follows the same syntax as declaring arrays of primitive types, using the structure’s name as the data type:

struct MyData {
    int id;
    double value;
};

// Declare a static array of MyData structures
MyData staticArray[10];

// Declare a dynamic array of MyData structures
MyData dynamicArray[];

This declares staticArray to hold 10 MyData structures and dynamicArray as a dynamic array ready to be sized at runtime.

Declaring and Initializing Arrays of Structures

Proper declaration and initialization are the first steps to effectively using arrays of structures.

Declaring a Static Array of Structures

A static array has a fixed size determined at compile time. You specify the size within square brackets [] after the variable name:

struct Point {
    double x;
    double y;
};

// A static array of 5 Point structures
Point points[5];

All elements in a static array of structures are automatically zero-initialized. For numeric types like double and int, this means they are set to 0. For strings, they are set to "". For complex types like nested structures or classes within the structure, their members are similarly zero-initialized.

Declaring a Dynamic Array of Structures

A dynamic array’s size can be changed during runtime using the ArrayResize() function. You declare it with empty square brackets:

struct TradeRecord {
    long order_ticket;
    double volume;
    double open_price;
    string symbol;
};

// Declare a dynamic array
TradeRecord tradeHistory[];

// Resize the array to hold 10 records
ArrayResize(tradeHistory, 10);

// Resize again to hold 20 records (existing data is preserved if new size is larger)
ArrayResize(tradeHistory, 20);

When ArrayResize() is called, new elements are zero-initialized. If the array size is reduced, elements at the end are removed.

Initializing Array Elements with Values

You can initialize array elements during declaration (for static arrays or as part of a struct member initialization) or by assigning values after declaration. For arrays of structures, you typically assign values to the members of each structure element individually.

Direct initialization during declaration:

struct ColorRGB {
    short r;
    short g;
    short b;
};

// Initialize a static array of ColorRGB structures
ColorRGB colors[3] = {
    {255, 0, 0},   // Red
    {0, 255, 0},   // Green
    {0, 0, 255}    // Blue
};

Initialization after declaration (more common for dynamic data):

struct BarData {
    datetime time;
    double open;
    double high;
    double low;
    double close;
};

BarData historyBars[5]; // Static array

// Assuming you have data from CopyRates or similar
MqlRates rates[5];
int count = CopyRates(_symbol, _period, 0, 5, rates);

if (count == 5)
{
    for (int i = 0; i < count; i++)
    {
        historyBars[i].time = rates[i].time;
        historyBars[i].open = rates[i].open;
        historyBars[i].high = rates[i].high;
        historyBars[i].low = rates[i].low;
        historyBars[i].close = rates[i].close;
    }
}

For dynamic arrays, you must resize before accessing elements:

TradeRecord positionList[];

// Assume we found 3 open positions
int num_positions = PositionsTotal();
ArrayResize(positionList, num_positions);

for (int i = 0; i < num_positions; i++)
{
    ulong ticket = PositionGetTicket(i);
    if (ticket > 0)
    {
        positionList[i].order_ticket = ticket;
        positionList[i].symbol       = PositionGetString(POSITION_SYMBOL);
        positionList[i].volume       = PositionGetDouble(POSITION_VOLUME);
        positionList[i].open_price   = PositionGetDouble(POSITION_PRICE_OPEN);
    }
}

Accessing and Manipulating Data within the Array

Once an array of structures is declared and potentially initialized, accessing and modifying the data within individual structures is straightforward.

Accessing Structure Members Using Array Indices

You access an element of the array using its zero-based index within square brackets [], just like any other array. To access a specific member within that structure element, you use the dot operator . followed by the member name.

struct Student {
    string name;
    int age;
    double grade;
};

Student classList[3];

// Initialize one element
classList[0].name  = "Alice";
classList[0].age   = 20;
classList[0].grade = 95.5;

// Accessing data:
string first_student_name = classList[0].name;
int second_student_age = classList[1].age; // Will be 0 (default initialized)

Print("First student: " + first_student_name + ", Age: " + classList[0].age);

The expression classList[0] refers to the first Student structure object in the array. The expression classList[0].name accesses the name member of that first Student object.

Modifying Structure Members

You can modify the value of a structure member in the array using the assignment operator = after accessing it via the array index and dot operator.

struct Settings {
    string key;
    string value;
};

Settings config[2];

config[0].key = "Symbol";
config[0].value = "EURUSD";

config[1].key = "Lots";
config[1].value = "0.1";

// Modify a value
config[0].value = "GBPUSD";

Print("Modified symbol setting: " + config[0].value);

This allows you to update specific pieces of data within a complex record held in the array.

Iterating Through the Array

The most common way to process all elements in an array of structures is using a for loop, iterating from index 0 up to ArraySize(arrayName) - 1.

struct Task {
    string description;
    bool is_completed;
};

Task todoList[4];

todoList[0] = {"Buy groceries", false};
todoList[1] = {"Code EA", true};
todoList[2] = {"Test strategy", false};
todoList[3] = {"Write report", false};

Print("--- To-Do List --- ");
for (int i = 0; i < ArraySize(todoList); i++)
{
    string status = todoList[i].is_completed ? "[DONE]" : "[TODO]";
    Print(status + " " + todoList[i].description);
}
Print("------------------");

This loop accesses each Task structure in the todoList array and prints its status and description by accessing the is_completed and description members.

Practical Examples and Use Cases

Arrays of structures are invaluable in various MQL5 programming scenarios.

Storing and Managing Order Information

A common use case is retrieving and managing details about open orders, pending orders, or historical deals. You can define a structure that mirrors the information available from OrderGet..., PositionGet..., or HistoryDealGet... functions and populate an array with this data.

struct OrderDetails {
    long     ticket;
    string   symbol;
    ENUM_ORDER_TYPE type;
    double   volume;
    double   price;
    datetime time_setup;
    string   comment;
};

// Example: Getting all pending orders
OrderDetails pendingOrders[];
int total_pending = OrdersTotal();

if (total_pending > 0)
{
    ArrayResize(pendingOrders, total_pending);
    int count = 0;
    for (int i = 0; i < total_pending; i++)
    {
        ulong order_ticket = OrderGetTicket(i);
        if (order_ticket > 0)
        {
            pendingOrders[count].ticket     = order_ticket;
            pendingOrders[count].symbol     = OrderGetString(ORDER_SYMBOL);
            pendingOrders[count].type       = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
            pendingOrders[count].volume     = OrderGetDouble(ORDER_VOLUME_INITIAL);
            pendingOrders[count].price      = OrderGetDouble(ORDER_PRICE_OPEN);
            pendingOrders[count].time_setup = (datetime)OrderGetInteger(ORDER_TIME_SETUP);
            pendingOrders[count].comment    = OrderGetString(ORDER_COMMENT);
            count++;
        }
    }
    // Resize again if some orders were invalid
    ArrayResize(pendingOrders, count);

    // Now you can easily iterate and process each pending order
    for (int i = 0; i < ArraySize(pendingOrders); i++)
    {
        Print("Pending Order: Ticket=", pendingOrders[i].ticket, ", Symbol=", pendingOrders[i].symbol, ", Type=", pendingOrders[i].type);
    }
}

This approach centralizes all relevant order data, making it easier to filter, process, or display.

Handling Historical Data in Arrays of Structures

While MQL5 provides functions like CopyRates, CopyTicks, etc., which fill built-in arrays of structures (MqlRates, MqlTick), you might define your own structures or use arrays of structures to manage historical data derived or processed from the raw data.

For instance, you could store calculated indicator values alongside bar data or store aggregated data:

struct ProcessedBar {
    datetime time;
    double open;
    double high;
    double low;
    double close;
    long   volume;
    double moving_average;
    double rsi_value;
};

// Example: Store bar data plus calculated indicator values
ProcessedBar processedBars[];
int bars_count = 100;

ArrayResize(processedBars, bars_count);

// Assume rates and indicator buffer values are available
MqlRates rates[100];
double ma_buffer[100];
double rsi_buffer[100];

// Fill rates, ma_buffer, rsi_buffer using CopyRates, iMA, iRSI...
// (Code to fill buffers omitted for brevity)

for (int i = 0; i < bars_count; i++)
{
    processedBars[i].time           = rates[i].time;
    processedBars[i].open           = rates[i].open;
    processedBars[i].high           = rates[i].high;
    processedBars[i].low            = rates[i].low;
    processedBars[i].close          = rates[i].close;
    processedBars[i].volume         = rates[i].real_volume; // or tick_volume
    processedBars[i].moving_average = ma_buffer[i];
    processedBars[i].rsi_value      = rsi_buffer[i];
}

// Now processedBars array holds combined data for easy access and analysis

This allows you to consolidate various data points related to a specific historical bar into a single entity within an array.

Creating Custom Indicators with Arrays of Structures

In custom indicators, arrays of structures can be used to manage input parameters, calculated values per bar, or internal state information. While indicator buffers are typically arrays of double, structures can be used for more complex scenarios, such as storing parameters for multiple instances of sub-indicators or managing custom graphical objects associated with specific bars.

struct IndicatorParams {
    string name;
    int period;
    int shift;
    color line_color;
};

struct BarCalculations {
    double main_value;
    double signal_value;
    int    arrow_direction; // e.g., 0=none, 1=up, -1=down
};

// Example: Managing parameters for different indicator lines
IndicatorParams indicatorLines[3];

indicatorLines[0] = {"Fast MA", 14, 0, clBlue};
indicatorLines[1] = {"Slow MA", 50, 0, clRed};
indicatorLines[2] = {"Signal",  9, 0, clGreen};

// In the Calculate function, you might populate an array of BarCalculations
// for each bar, combining different calculated values.
BarCalculations barData[];
// ... resize and fill barData based on indicator logic ...

This improves organization, especially in complex indicators that derive multiple outputs or manage varied settings.

Advanced Techniques and Considerations

Working with arrays of structures at a more advanced level involves understanding functions like ArrayCopy, sorting, and considering memory implications.

Using ArrayCopy() with Structures

The ArrayCopy() function can be used to copy data between arrays of structures, provided the structures are of the same type. MQL5’s ArrayCopy performs a byte-by-byte copy. For simple structures containing only primitive types or other POD (Plain Old Data) structures, this works as expected, effectively copying the values of all members from the source array to the destination array.

struct SimpleData {
    int id;
    double value;
};

SimpleData source[5];
SimpleData destination[5];

// Fill source array...
source[0] = {1, 10.1};
source[1] = {2, 20.2};
// ... etc.

// Copy elements from source to destination
ArrayCopy(destination, source, 0, 0, 5);

// destination[0] is now {1, 10.1}, etc.
Print(destination[0].id, ", ", destination[0].value);

Be mindful if your structure contains pointers or complex objects that require deep copying, as ArrayCopy won’t handle these. However, in typical MQL5 structures used for data aggregation, this is less often a concern.

Sorting Arrays of Structures

Sorting an array of structures is a powerful capability, often needed to order data based on specific criteria (e.g., sort orders by ticket, positions by profit, historical data by time). MQL5’s ArraySort() function can sort arrays of structures, but requires a custom comparison function if sorting is based on structure members.

The comparison function must have the signature int compare_func(const void* ptr1, const void* ptr2) and return:

  • -1 if the element pointed to by ptr1 should come before the element pointed to by ptr2.
  • 1 if the element pointed to by ptr1 should come after the element pointed to by ptr2.
  • 0 if they are considered equal for sorting purposes.

Inside the comparison function, you must cast the void* pointers back to pointers to your structure type to access their members.

struct TradeResult {
    long ticket;
    datetime close_time;
    double profit;
};

// Comparison function to sort TradeResult by profit (ascending)
int CompareTradeResultsByProfit(const void* ptr1, const void* ptr2)
{
    TradeResult* tr1 = (TradeResult*)ptr1;
    TradeResult* tr2 = (TradeResult*)ptr2;

    if (tr1.profit < tr2.profit) return -1;
    if (tr1.profit > tr2.profit) return 1;
    return 0; // Profits are equal
}

// Comparison function to sort TradeResult by time (descending)
int CompareTradeResultsByTimeDesc(const void* ptr1, const void* ptr2)
{
    TradeResult* tr1 = (TradeResult*)ptr1;
    TradeResult* tr2 = (TradeResult*)ptr2;

    if (tr1.close_time > tr2.close_time) return -1; // tr1 is newer, comes first
    if (tr1.close_time < tr2.close_time) return 1;  // tr1 is older, comes after
    return 0;
}

// Example usage:
TradeResult results[10];
// ... fill results array with historical data ...

// Sort the results array by profit (ascending)
ArraySort(results, COMPARE_FUNC(CompareTradeResultsByProfit));

// Sort the results array by close time (descending)
ArraySort(results, COMPARE_FUNC(CompareTradeResultsByTimeDesc));

Using COMPARE_FUNC is crucial for passing the function pointer correctly to ArraySort.

Memory Management and Optimization

For dynamic arrays of structures, particularly if they can grow very large, consider the memory footprint. Each element is a structure, which can occupy a significant amount of memory depending on the number and types of its members. Resizing dynamic arrays (ArrayResize()) frequently can involve reallocating memory and copying data, which incurs performance overhead.

  • Pre-allocate: If you have an estimate of the maximum size, pre-allocate the dynamic array to that size using ArrayResize() once. This avoids multiple reallocations.
  • Clear: Use ArrayFree() to release memory occupied by dynamic arrays of structures when they are no longer needed. This is especially important in indicators or EAs that persist for a long time.
  • Structure Size: Keep structures as compact as possible by using appropriate data types. Arrays of large structures consume more memory.

While MQL5 handles much of the memory management automatically, understanding the implications of dynamic array resizing and the memory cost of structures is vital for optimizing performance, especially in strategies dealing with vast amounts of historical data or managing many concurrent entities.

In summary, arrays of structures in MQL5 provide a robust and intuitive way to organize complex, related data. By leveraging this feature, MQL5 developers can write cleaner, more maintainable, and more efficient code for trading applications.


Leave a Reply