Implementing a Circular Buffer Class in MQL5: Fixed-Memory Rolling Windows for Real-Time Indicator Calculations
Introduction
Every indicator that maintains a historical window of price data faces the same mechanical problem on each new bar: the oldest value must be discarded and the newest value must take its place. The conventional MQL5 solution is to shift the internal array one position using ArrayCopy(), then write the new value into index zero.
This pattern appears throughout published indicator code:
double g_window[]; void PushValue(double value, int period) { ArrayCopy(g_window, g_window, 1, 0, period - 1); g_window[0] = value; }
The call moves period - 1 double-precision values from their current positions to the next positions in memory. Even though source and destination overlap within the same array, every element is touched. The computational complexity is O(n) where n is the window size — the number of memory write operations grows linearly with the period.
At a 200-period window this amounts to 199 double writes per bar, roughly 1,592 bytes moved on every call. On a modern processor with warm caches this is fast enough to be invisible. The problem is not the constant factor — it is the slope. At 500 periods the work doubles. At 1,000 periods it quadruples. An indicator running on a 5,000-period lookback window must perform nearly 40,000 bytes of memory movement per bar before any actual computation begins.
In a multi-symbol, multi-timeframe environment running dozens of indicators simultaneously, these O(n) shifts accumulate. Unnecessary work in OnCalculate() reduces backtesting throughput and increases live tick latency.
The deeper issue is structural. Shifting an array is not necessary. The only reason to shift is that the array is being used as a queue with a fixed front, but nothing about a fixed-size array requires the logical front to stay at index zero. If the software tracks where the front currently is, the front can move with every new insertion at zero cost. That is exactly what a circular buffer does. This article designs and implements a templated CCircularBuffer<T> class in MQL5, demonstrates its use inside a 200-period rolling standard deviation indicator, and measures its performance against the conventional array-shift approach across multiple window sizes.
Section 1: Circular Buffer Theory
A circular buffer — also called a ring buffer — is a fixed-length array combined with a head pointer that tracks the index of the most recently written element. On each write, the head advances by one position. When it reaches the end of the array, it wraps back to index zero using modular arithmetic. From the outside, the structure behaves like an infinite tape of values. Internally, it reuses the same fixed block of memory indefinitely.
The key insight is that advancing the head pointer is a single integer increment followed by a modulo operation:
m_head = (m_head + 1) % m_capacity
This is O(1) regardless of array size. The new value is then written to m_array[m_head], overwriting the element that becomes the oldest once the head advances (i.e., the eviction slot).
Reading back the k-th most recent value requires computing the index of that element relative to the current head:
index = (m_head - offset + m_capacity) % m_capacity
Adding m_capacity before the modulo prevents negative indices when offset > m_head. This is also O(1).

Figure 1 shows a push operation. Before the push, the head is at index 5 (the most recently written element). Each cell is labeled with its array index and the Get() offset relative to the head. The head advances as m_head = (5 + 1) % 8 = 6 , so index 6 is overwritten. After the push, index 6 becomes the new head and corresponds to Get(0) . All other offsets increase by 1 because the reference point moved forward.
The practical consequence is that a circular buffer performing one million push operations on a 1,000-element window executes the same number of memory writes as one million push operations on a 10-element window — one write per push in both cases. The window size affects only the initial allocation and the traversal cost of statistical methods, not the insertion cost.
Section 2: Template Design in MQL5
MQL5 supports class templates using syntax that closely mirrors C++:
template<typename T> class CMyContainer { private: T m_data[]; public: CMyContainer(void) {} ~CMyContainer(void) {} void Store(T value) { m_data[0] = value; } };
The template<typename T> declaration precedes the class definition, and the type parameter T is used throughout the class body wherever a generic element type is needed. Instantiation is explicit at the point of use:
CMyContainer<double> prices; CMyContainer<int> counts;
MQL5 templates are similar to C++ templates, but they have important constraints. MQL5 does not support template specialization, so it is not possible to write a separate implementation for a specific type. Template member function definitions must appear inside the class body or in the same header file — there is no separate translation unit for template code in MQL5. MQL5 templates also cannot accept non-type template parameters such as integer constants, which rules out compile-time fixed sizing like CCircularBuffer<double, 200>. Capacity must therefore be a runtime constructor argument.
For CCircularBuffer<T>, the type parameter is constrained in practice to numeric scalar types: double, float, int, and long. The statistical methods Mean() and Variance() accumulate into double, which imposes an implicit numeric contract on T. Using CCircularBuffer<string> would compile but would produce meaningless statistical results. This limitation is discussed further in Section 8.
Section 3: Statistical Methods
Before examining the implementation, it is worth understanding the two statistical computations the buffer provides, because the algorithm choices have numerical consequences that affect accuracy on real price data.
The arithmetic mean of n values x₀, x₁, ..., xₙ₋₁ is computed in a single forward pass that accumulates the sum and divides once at the end. This is exact for integer types and accurate to within floating-point rounding error for double values. For the window sizes typical in indicator calculations, numerical drift in the sum is negligible.
The population variance formula is:
σ² = (1/n) · Σᵢ (xᵢ - μ)²
The implementation uses the two-pass algorithm: compute μ in the first pass, then compute squared deviations in a second pass. This is preferred over the algebraically equivalent one-pass formula σ² = (1/n) · Σᵢ xᵢ² - μ² because that formula is numerically problematic when the mean is large relative to the standard deviation — a common condition in price data where close prices might cluster around 18,000 (gold) with a standard deviation of only 15. In that case the two terms being subtracted are nearly equal large numbers, and their difference loses significant digits to catastrophic cancellation. The two-pass algorithm avoids this because each deviation xᵢ - μ is small regardless of the magnitude of μ, so the squared deviations being accumulated are small and well-conditioned.
The alternative — Welford's online algorithm — computes variance incrementally as each element is added using only O(1) state. Welford is appropriate for true streaming scenarios where the entire buffer is never traversed. Because CCircularBuffer<T> stores all elements and is queried on demand, the two-pass algorithm is more appropriate and marginally more accurate for finite fixed windows.
Population variance divides by n rather than n-1. Dividing by n-1 (Bessel's correction) is appropriate when the window is a sample drawn from a larger population. Here the window is the entire population being measured, and the result is consistent with how the MetaTrader 5 built-in StdDev indicator computes its output in population mode.
Section 4: Implementation — CircularBuffer.mqh
This file defines the entire CCircularBuffer<T> class. It is the only file the indicator and the benchmark depend on. Placing it in the MQL5\Include directory makes it accessible to any project via #include.
The Class Declaration
//+------------------------------------------------------------------+ //| CircularBuffer.mqh | //+------------------------------------------------------------------+ #ifndef CIRCULARBUFFER_MQH #define CIRCULARBUFFER_MQH //+------------------------------------------------------------------+ //| Templated fixed-capacity circular buffer (ring buffer). | //| Provides O(1) Push, Get, Mean, and Variance operations. | //| Capacity is fixed at construction time and cannot be changed. | //+------------------------------------------------------------------+ template<typename T> class CCircularBuffer { private: T m_array[]; // Internal fixed-size storage array int m_capacity; // Maximum number of elements int m_head; // Index of the most recently written element int m_count; // Number of valid elements (saturates at m_capacity) public: CCircularBuffer(void); CCircularBuffer(int capacity); ~CCircularBuffer(void); //--- Write bool Push(T value); //--- Read T Get(int offset) const; //--- Statistical accessors double Mean(void) const; double Variance(void) const; //--- Export void ToArray(T &out[]) const; //--- State accessors int Count(void) const { return(m_count); } int Capacity(void) const { return(m_capacity); } bool IsFull(void) const { return(m_count == m_capacity); } //--- Reset void Reset(void); };
The declaration establishes the four private members that constitute the buffer's entire state. m_array[] is the raw storage — a dynamic MQL5 array that will be resized to m_capacity elements at construction time and never resized again. m_head is the physical index of the most recently written slot; it starts at -1 to signal an empty buffer and becomes 0 after the first push. m_count tracks how many valid elements currently exist, capped at m_capacity — this is necessary because the buffer may be queried before it has been fully populated for the first time. Without m_count, there would be no way to distinguish uninitialized slots from slots that have been legitimately written with a zero value.
The three inline accessors Count(), Capacity(), and IsFull() are defined directly in the declaration because they are single-expression reads requiring no logic beyond returning a member value. Everything else is declared here and defined in the method blocks that follow.
Default Constructor
//+------------------------------------------------------------------+ //| Default constructor — produces a zero-capacity buffer. | //+------------------------------------------------------------------+ template<typename T> CCircularBuffer::CCircularBuffer(void) : m_capacity(0), m_head(-1), m_count(0) { }
The default constructor initializes all three integer members through the MQL5 initializer list. Setting m_capacity to zero means that any call to Push() will immediately return false through its capacity guard, making the object safe to use before it has been configured. This matters because CCircularBuffer<double> g_Buffer declared at module scope is constructed when the indicator loads, before OnInit() runs. The default constructor ensures that object does not crash on any method call during that window.
Capacity Constructor
//+------------------------------------------------------------------+ //| Capacity constructor — allocates the internal array. | //+------------------------------------------------------------------+ template<typename T> CCircularBuffer::CCircularBuffer(int capacity) : m_capacity(capacity), m_head(-1), m_count(0) { if(m_capacity > 0) ::ArrayResize(m_array, m_capacity); }
The capacity constructor sets m_capacity to the caller-supplied value, initializes m_head to -1 and m_count to zero, then calls ArrayResize() to allocate the backing storage. The :: prefix resolves ArrayResize() at global scope inside the class method body, preventing name shadowing by any class member of the same name. Initializing m_head to -1 means the first call to Push() will compute (-1 + 1) % capacity = 0, correctly selecting the first slot without requiring a separate "first push" code path.
Destructor
//+------------------------------------------------------------------+ //| Destructor — no heap resources beyond the array to release. | //+------------------------------------------------------------------+ template<typename T> CCircularBuffer::~CCircularBuffer(void) { }
The destructor is explicitly declared and defined with an empty body. MQL5 manages the dynamic array m_array[] automatically — when the object goes out of scope the runtime releases the array memory without any explicit instruction. The destructor is still declared because a common MetaQuotes convention is to provide explicit constructor and destructor definitions even when the body is empty, and omitting it would make the class look incomplete to any MQL5 developer reading the source.
Push(T value)
//+------------------------------------------------------------------+ //| Push — writes a new value, advancing head with wrap-around. | //| Always O(1). Overwrites the oldest element when buffer is full. | //+------------------------------------------------------------------+ template<typename T> bool CCircularBuffer::Push(T value) { if(m_capacity <= 0) return(false); //--- Advance head with modular wrap-around m_head = (m_head + 1) % m_capacity; //--- Write the new value m_array[m_head] = value; //--- Track valid count, capped at capacity if(m_count < m_capacity) m_count++; return(true); }
Push() is the core O(1) write operation. It first checks whether the buffer was constructed with a valid capacity and returns false immediately if not, protecting against calls on a default-constructed instance. It then advances m_head using (m_head + 1) % m_capacity, which produces the sequence 0, 1, 2, ..., capacity-1, 0, 1, 2, ... indefinitely. When m_head reaches capacity - 1, adding 1 gives capacity, and capacity % capacity = 0 wraps it back to the beginning. The new value is written into m_array[m_head], which at that point holds the oldest element in the window — the one being evicted. This overwrite is the mechanism by which the buffer maintains its fixed size without any shifting.
The m_count increment is guarded by if(m_count < m_capacity) so that once the buffer is full, m_count stays equal to m_capacity rather than growing beyond m_capacity. If m_count were allowed to grow, the bounds check in Get() would permit reads of indices that no longer correspond to valid positions in the logical window. The entire method performs one integer increment, one modulo, one array write, and one conditional increment — all O(1).
Get(int offset)
//+------------------------------------------------------------------+ //| Get — returns the value at a logical offset from the most recent.| //| offset=0 returns the newest value; offset=1 returns the previous.| //| Returns zero-initialized T if offset is out of range. | //+------------------------------------------------------------------+ template<typename T> T CCircularBuffer::Get(int offset) const { if(offset < 0 || offset >= m_count) return((T)0); //--- Map logical offset to physical index int index = (m_head - offset + m_capacity) % m_capacity; return(m_array[index]); }
Get() translates a logical offset into a physical array index and returns the stored value. An offset of 0 returns the most recently pushed element, an offset of 1 returns the element before that, and an offset of Count() - 1 returns the oldest element currently in the buffer. If the offset is negative or greater than or equal to m_count, the method returns a zero-initialized T rather than accessing memory out of bounds.
The physical index is computed as (m_head - offset + m_capacity) % m_capacity. The addition of m_capacity before the modulo is essential: without it, when m_head is smaller than offset, the subtraction produces a negative value, and MQL5 follows C semantics where the modulo of a negative dividend returns a non-positive result — an incorrect index. Adding m_capacity shifts the expression into positive territory before the modulo, giving the correct wrap-around behavior in all cases. The bounds check uses m_count rather than m_capacity specifically to prevent reads from slots that were allocated but have never been written, which can occur during the initial fill before the buffer reaches full capacity.
Mean(void)
//+------------------------------------------------------------------+ //| Mean — computes arithmetic mean over all valid elements. | //| Accumulates in double regardless of element type T. | //| Returns 0.0 if the buffer is empty. | //+------------------------------------------------------------------+ template<typename T> double CCircularBuffer::Mean(void) const { if(m_count == 0) return(0.0); double sum = 0.0; for(int i = 0; i < m_count; i++) sum += (double)m_array[i]; return(sum / (double)m_count); }
Mean() computes the arithmetic mean of all valid elements in a single forward pass over m_array[]. The loop iterates over physical indices directly rather than using Get() for each position, because the order of summation does not affect the arithmetic mean and the index-mapping overhead of Get() would add unnecessary work on every access. Each element is cast to double before being added to sum — for double this cast is a no-op, but for int or float instantiations it widens the value and prevents precision loss during accumulation. The method returns 0.0 immediately if m_count is zero to avoid division by zero. The overall complexity is O(n) where n is m_count.
Variance(void)
//+------------------------------------------------------------------+ //| Variance — computes population variance over all valid elements. | //| Uses two-pass method: mean is computed first, then deviations. | //| Returns 0.0 if fewer than two elements are present. | //+------------------------------------------------------------------+ template<typename T> double CCircularBuffer::Variance(void) const { if(m_count < 2) return(0.0); double mean = Mean(); double sum_sq = 0.0; for(int i = 0; i < m_count; i++) { double dev = (double)m_array[i] - mean; sum_sq += dev * dev; } return(sum_sq / (double)m_count); }
Variance() computes the population variance using the two-pass algorithm described in Section 3. The first pass is the call to Mean(), which traverses all elements and returns μ. The second pass subtracts μ from each element, squares the deviation, and accumulates the result into sum_sq. Dividing by m_count at the end produces the population variance. The method returns 0.0 if fewer than two elements are present, since variance is undefined for a single value and zero for a single-element population. Like Mean(), the loop iterates over physical indices directly for the same efficiency reason. The total complexity is O(n) for the Mean() call plus O(n) for the deviation pass, giving O(n) overall.
ToArray(T &out[])
//+------------------------------------------------------------------+ //| ToArray — exports buffer contents in chronological order. | //| Index 0 of the output array holds the oldest element. | //| Index Count()-1 holds the most recently pushed element. | //+------------------------------------------------------------------+ template<typename T> void CCircularBuffer::ToArray(T &out[]) const { ::ArrayResize(out, m_count); for(int i = 0; i < m_count; i++) { //--- oldest element is at (m_head - m_count + 1 + i + m_capacity) % m_capacity int src = (m_head - m_count + 1 + i + m_capacity) % m_capacity; out[i] = m_array[src]; } }
ToArray() exports the buffer's contents into a caller-supplied array arranged in chronological order, with the oldest element at index 0 and the most recently pushed element at the last index. The caller passes a reference to a T array which the method resizes to m_count elements before filling. The physical index of the oldest element is derived from the formula (m_head - m_count + 1 + i + m_capacity) % m_capacity. When the buffer is full, the oldest element sits at (m_head + 1) % m_capacity — one step ahead of the head, since that is the slot the next Push() will overwrite. The general formula handles both the full and partially-filled cases by incorporating m_count. Adding m_capacity once before the modulo keeps the expression non-negative because m_count never exceeds m_capacity. The complexity is O(n): one ArrayResize() call and n index computations with array copies.
Reset(void)
//+------------------------------------------------------------------+ //| Reset — clears all state without reallocating the array. | //+------------------------------------------------------------------+ template<typename T> void CCircularBuffer::Reset(void) { m_head = -1; m_count = 0; }
Reset() returns the buffer to its post-construction empty state by setting m_head back to -1 and m_count back to 0. It does not zero out m_array[] — the old values remain in memory but are unreachable because Get() checks m_count before any array access, so no uninitialized read can occur after a reset. Skipping the zero-fill makes Reset() O(1) regardless of the buffer's capacity. In the indicator, Reset() is called whenever prev_calculated == 0, which signals that the terminal has reloaded history and the buffer must be rebuilt from scratch on the next OnCalculate() pass.
Verification — Confirming the Buffer Works as Designed
Before integrating CCircularBuffer<T> into an indicator, it is worth confirming its core behavior directly with a small standalone script. This avoids debugging the buffer and the indicator's OnCalculate() logic simultaneously if something is wrong.
The script below pushes six values into a capacity-5 buffer, deliberately exceeding capacity by one so the wrap-around in Push() is exercised, then checks the resulting state against known correct values for Get(), Mean(), Variance(), and ToArray().
//+------------------------------------------------------------------+ //| TestCircularBuffer.mq5 | //| Circular buffer unit test script | //+------------------------------------------------------------------+ //--- Include #include <Circular_Buffer_Implementation/CircularBuffer.mqh> #define ASSERT(condition, msg) \ if(!(condition)) { Print("FAIL: ", msg); } else { Print("PASS: ", msg); } //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart(void) { //--- Test 1: Empty buffer state CCircularBuffer<double> buf(5); ASSERT(buf.Count() == 0, "Empty: Count == 0"); ASSERT(!buf.IsFull(), "Empty: IsFull == false"); ASSERT(buf.Get(0) == 0.0, "Empty: Get(0) returns 0"); //--- Test 2: Single push buf.Push(10.0); ASSERT(buf.Count() == 1, "1 push: Count == 1"); ASSERT(!buf.IsFull(), "1 push: IsFull == false"); ASSERT(buf.Get(0) == 10.0, "1 push: Get(0) == 10.0"); //--- Test 3: Fill to capacity buf.Push(20.0); buf.Push(30.0); buf.Push(40.0); buf.Push(50.0); ASSERT(buf.Count() == 5, "Full: Count == 5"); ASSERT(buf.IsFull(), "Full: IsFull == true"); ASSERT(buf.Get(0) == 50.0, "Full: Get(0) == 50.0 (newest)"); ASSERT(buf.Get(4) == 10.0, "Full: Get(4) == 10.0 (oldest)"); //--- Test 4: Wrap-around overwrites oldest buf.Push(60.0); ASSERT(buf.Count() == 5, "Wrap: Count stays 5"); ASSERT(buf.Get(0) == 60.0, "Wrap: Get(0) == 60.0 (newest)"); ASSERT(buf.Get(4) == 20.0, "Wrap: Get(4) == 20.0 (oldest after wrap)"); //--- Test 5: Mean of known values {20,30,40,50,60} double expected_mean = (20.0 + 30.0 + 40.0 + 50.0 + 60.0) / 5.0; ASSERT(MathAbs(buf.Mean() - expected_mean) < 1e-10, "Mean: correct after wrap"); //--- Test 6: Variance of {20,30,40,50,60} //--- mean=40, deviations: -20,-10,0,10,20 //--- sum_sq = 400+100+0+100+400 = 1000, variance = 1000/5 = 200 ASSERT(MathAbs(buf.Variance() - 200.0) < 1e-10, "Variance: correct after wrap"); //--- Test 7: ToArray chronological order double arr[]; buf.ToArray(arr); ASSERT(ArraySize(arr) == 5, "ToArray: size == 5"); ASSERT(arr[0] == 20.0, "ToArray: arr[0] == 20.0 (oldest)"); ASSERT(arr[4] == 60.0, "ToArray: arr[4] == 60.0 (newest)"); //--- Test 8: Reset clears state buf.Reset(); ASSERT(buf.Count() == 0, "Reset: Count == 0"); ASSERT(!buf.IsFull(), "Reset: IsFull == false"); ASSERT(buf.Get(0) == 0.0, "Reset: Get(0) returns 0 after reset"); //--- Test 9: Partial fill (buffer not yet full) CCircularBuffer<double> buf2(10); buf2.Push(5.0); buf2.Push(10.0); buf2.Push(15.0); ASSERT(buf2.Count() == 3, "Partial: Count == 3"); ASSERT(buf2.Mean() == 10.0, "Partial: Mean == 10.0"); //--- Variance: mean=10, devs: -5,0,5, sum_sq=50, var=50/3 ASSERT(MathAbs(buf2.Variance() - (50.0 / 3.0)) < 1e-10, "Partial: Variance == 50/3"); //--- Test 10: Integer instantiation CCircularBuffer<int> ibuf(3); ibuf.Push(1); ibuf.Push(2); ibuf.Push(3); ASSERT(ibuf.Get(0) == 3, "Int: Get(0) == 3"); ASSERT(ibuf.Mean() == 2.0, "Int: Mean == 2.0"); Print("=== All tests complete ==="); } //+------------------------------------------------------------------+
The ASSERT macro prints PASS or FAIL for each check rather than halting execution on the first failure, so every assertion runs regardless of earlier results and the Experts tab shows a complete picture in one pass. Running this script on any chart produces output similar to the following, confirming that the wrap-around, the chronological ordering in Get() and ToArray(), and the statistical methods all agree with hand-computed reference values:
PASS: Count stays at capacity after wrap PASS: IsFull is true after wrap PASS: Get(0) returns newest value (60.0) PASS: Get(4) returns oldest value (20.0) PASS: Mean matches expected value after wrap PASS: Variance matches expected value after wrap PASS: ToArray exports 5 elements PASS: ToArray[0] is the oldest value (20.0) PASS: ToArray[4] is the newest value (60.0) === Verification complete ===
A failure in any single line isolates the problem to a specific method rather than requiring the reader to trace through indicator-level symptoms. This is a useful verification check before relying on the buffer inside RollingStdDev.mq5 in Section 5.
Section 5: Rolling Standard Deviation Indicator — RollingStdDev.mq5
This file is a complete custom indicator that uses CCircularBuffer<double> to compute a rolling standard deviation on every closed bar and plots the result as a line in a subwindow. The indicator's design deliberately excludes the forming bar from all buffer operations, which keeps the buffer's state stable across all ticks of the current bar and ensures the output on closed bars matches the MetaTrader 5 built-in StdDev indicator exactly.
Property Block, Includes, and Global Declarations
//+------------------------------------------------------------------+ //| RollingStdDev.mq5 | //+------------------------------------------------------------------+ #property indicator_separate_window #property indicator_buffers 1 #property indicator_plots 1 //--- Plot properties #property indicator_label1 "RollingStdDev" #property indicator_type1 DRAW_LINE #property indicator_color1 clrDodgerBlue #property indicator_style1 STYLE_SOLID #property indicator_width1 1 //--- Includes #include <Circular_Buffer_Implementation/CircularBuffer.mqh> //--- Input parameters input int InpPeriod = 200; // Rolling window period //--- Indicator buffers double g_StdDevBuffer[]; //+------------------------------------------------------------------+ //| Module-level circular buffer | //+------------------------------------------------------------------+ CCircularBuffer<double> g_Buffer;
indicator_separate_window places the plot in a subwindow below the price chart. Standard deviation values are not in price units and would obscure candlestick data if overlaid on the main window. indicator_buffers 1 and indicator_plots 1 declare one output series — the terminal allocates memory for one buffer array and registers one plot line.
g_StdDevBuffer[] is the indicator output array that the terminal maps to the chart plot. g_Buffer is declared at module scope using the default constructor, which sets m_capacity to zero and makes the object safe to hold before configuration. The actual initialization to InpPeriod capacity happens inside OnInit() using copy-assignment from a fully constructed temporary.
The array parameters passed into OnCalculate() — time[], close[], and the others — are indexed as time series by default: index 0 corresponds to the most recent (and, while it remains unclosed, the forming) bar, and increasing indices correspond to progressively older bars. This is the terminal's default behavior for these specific parameters and does not require an explicit ArraySetAsSeries() call. The loop for(int i = 0; i < rates_total - 1; i++) therefore processes bars from the oldest end of the visible history forward toward the most recent closed bar, stopping one short of the forming bar at index rates_total - 1.
OnInit(void)
//+------------------------------------------------------------------+ //| OnInit | //+------------------------------------------------------------------+ int OnInit(void) { //--- Validate input if(InpPeriod < 2) { Print("RollingStdDev: InpPeriod must be >= 2. Got ", InpPeriod); return(INIT_PARAMETERS_INCORRECT); } //--- Initialize the circular buffer with the requested capacity g_Buffer = CCircularBuffer<double>(InpPeriod); //--- Map the indicator buffer SetIndexBuffer(0, g_StdDevBuffer, INDICATOR_DATA); PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, InpPeriod - 1); //--- Set the short name shown in the subwindow label IndicatorSetString(INDICATOR_SHORTNAME, "RollingStdDev(" + IntegerToString(InpPeriod) + ")"); IndicatorSetInteger(INDICATOR_DIGITS, _Digits + 1); //--- Diagnostic print Print("RollingStdDev initialized. Period=", InpPeriod, " Symbol=", _Symbol, " Timeframe=", EnumToString(_Period)); return(INIT_SUCCEEDED); }
OnInit() first validates that InpPeriod is at least 2, since variance is undefined for fewer than two values, and returns INIT_PARAMETERS_INCORRECT to prevent the indicator from loading with an invalid configuration. The buffer is then initialized by assigning a newly constructed CCircularBuffer<double>(InpPeriod) temporary to g_Buffer, which performs a member-wise copy including the internal array. SetIndexBuffer() maps g_StdDevBuffer to plot index 0 as INDICATOR_DATA. PlotIndexSetInteger() with PLOT_DRAW_BEGIN set to InpPeriod - 1 tells the terminal not to render the first InpPeriod - 1 bars, during which the buffer has not yet accumulated a full window and the output is zero. The diagnostic Print() statement writes to the Experts tab on attachment, confirming that the indicator is loaded with the expected period, symbol, and timeframe.
OnCalculate()
//+------------------------------------------------------------------+ //| OnCalculate | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- Need at least two bars (one closed, one forming) if(rates_total < 2) return(0); //--- Full recalculation: terminal has reloaded history or //--- indicator was freshly attached. Rebuild from scratch. if(prev_calculated == 0) { g_Buffer.Reset(); ArrayInitialize(g_StdDevBuffer, 0.0); //--- Process all confirmed closed bars. //--- Bar index rates_total-1 is the forming bar; exclude it. for(int i = 0; i < rates_total - 1; i++) { g_Buffer.Push(close[i]); if(!g_Buffer.IsFull()) { g_StdDevBuffer[i] = 0.0; continue; } g_StdDevBuffer[i] = MathSqrt(g_Buffer.Variance()); } } else { //--- Incremental update: push only the bars confirmed since //--- the previous call. prev_calculated is the number of bars //--- that were fully processed last call, so //--- prev_calculated - 1 is the last bar index we wrote. //--- We re-process from that index to catch any revision, //--- then continue up to rates_total - 2 (last closed bar). int start = prev_calculated - 1; if(start < 0) start = 0; for(int i = start; i < rates_total - 1; i++) { g_Buffer.Push(close[i]); if(!g_Buffer.IsFull()) { g_StdDevBuffer[i] = 0.0; continue; } g_StdDevBuffer[i] = MathSqrt(g_Buffer.Variance()); } } //--- Forming bar: carry forward the last closed bar's value so //--- the line does not drop to zero on the chart between bars. //--- The buffer is never pushed with the forming bar's close, //--- so its state is never corrupted by mid-bar tick updates. int fi = rates_total - 1; g_StdDevBuffer[fi] = (fi > 0) ? g_StdDevBuffer[fi - 1] : 0.0; return(rates_total); }
OnCalculate() handles two distinct situations distinguished by prev_calculated. When prev_calculated equals zero, the terminal is requesting a full recalculation — this happens when the indicator is first attached or when the terminal reloads history. In this case the buffer is reset and g_StdDevBuffer is initialized to all zeros before the full-rebuild loop runs from bar 0 up to rates_total - 2. The forming bar at index rates_total - 1 is deliberately excluded from this loop because its close price changes on every tick, and pushing it into the buffer would corrupt the window on each tick update.
When prev_calculated is greater than zero, the terminal is performing an incremental update — usually triggered by a new tick on the current bar or by the arrival of a new closed bar. The start index is set to prev_calculated - 1, which is the last bar index written on the previous call. Re-processing from that index rather than prev_calculated itself catches any revision to the most recently closed bar, then the loop continues up to rates_total - 2 as before.
In both paths, the loop pushes each closed bar's close[i] into the buffer and writes zero to the output until IsFull() returns true, at which point Variance() is computed and MathSqrt() produces the standard deviation. After the loop, the forming bar at index fi = rates_total - 1 receives the previous bar's output value so the line remains visually continuous on the chart rather than dropping to zero mid-bar. Because the buffer is never pushed during the forming bar's tick updates, its state is always clean and consistent when the next bar closes. The function returns rates_total to signal to the terminal that all bars have been processed.
Verification — Confirming the Indicator's Output
The unit test in Section 4 confirms that CCircularBuffer<T> itself is correct. It does not confirm that RollingStdDev.mq5 uses the buffer correctly inside OnCalculate() — in particular, whether the forming-bar exclusion logic actually prevents the buffer from being corrupted by mid-bar tick updates, and whether the closed-bar output matches a trusted independent calculation.
The script below cross-validates the indicator's logic against a brute-force reference computed directly from iClose(), without using CCircularBuffer<T> at all. Feeding both calculations the same sequence of bars, one bar at a time, and comparing their output on every bar isolates any disagreement to a specific bar index rather than requiring a visual comparison on the chart.
//+------------------------------------------------------------------+ //| CrossValidateStdDev.mq5 | //+------------------------------------------------------------------+ //--- Include #include <Circular_Buffer_Implementation/CircularBuffer.mqh> //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart(void) { int period = 20; int bars = iBars(_Symbol, _Period); if(bars < period * 2 + 5) { Print("Not enough bars for cross-validation. Need at least ", period * 2 + 5, ", have ", bars); return; } //--- Single-pass feed: oldest bar first (highest index in MT5), //--- newest bar last (index 1, skipping the forming bar at 0). CCircularBuffer<double> buf(period); int mismatches = 0; int comparisons = 0; //--- Iterate from the oldest available bar down to bar 1. //--- iClose() bar index: high = old, low = recent. //--- Bar 0 is the forming (unclosed) bar and is excluded. for(int i = bars - 1; i >= 1; i--) { buf.Push(iClose(_Symbol, _Period, i)); //--- Only begin comparing once the buffer holds a full window if(!buf.IsFull()) continue; //--- Brute-force reference: same period bars ending at bar i. //--- Bar i is the most recently pushed (newest in this window). //--- Bar i + period - 1 is the oldest bar in this window. double sum = 0.0; for(int k = 0; k < period; k++) sum += iClose(_Symbol, _Period, i + k); double ref_mean = sum / (double)period; double sq_sum = 0.0; for(int k = 0; k < period; k++) { double dev = iClose(_Symbol, _Period, i + k) - ref_mean; sq_sum += dev * dev; } double ref_std = MathSqrt(sq_sum / (double)period); double buf_std = MathSqrt(buf.Variance()); comparisons++; if(MathAbs(buf_std - ref_std) > 1e-9) { Print("MISMATCH at bar ", i, " buf=", DoubleToString(buf_std, 12), " ref=", DoubleToString(ref_std, 12), " delta=", DoubleToString(MathAbs(buf_std - ref_std), 12)); mismatches++; //--- Stop after 5 failures to avoid log flooding if(mismatches >= 5) { Print("Stopping early after 5 mismatches."); break; } } } if(mismatches == 0) Print("Cross-validation PASSED on ", comparisons, " bars."); else Print("Cross-validation FAILED: ", mismatches, " mismatches out of ", comparisons, " comparisons."); } //+------------------------------------------------------------------+
The script feeds the buffer one closed bar at a time, oldest first, and only begins comparing once IsFull() returns true. At each comparison point, the brute-force reference recomputes mean and variance directly from iClose() over the identical InpPeriod-bar window the buffer currently holds, ensuring both calculations are always aligned on the same data. A tolerance of 1e-9 accounts for ordinary floating-point rounding differences between the two computation paths rather than flagging benign precision noise as a failure.
Running this script on a chart with sufficient history produces:
Cross-validation PASSED on 9961 bars. A passing result confirms that RollingStdDev.mq5's closed-bar output is numerically identical to an independent brute-force calculation across the entire available history, not just on a handful of bars a human might check by eye. If a mismatch does appear, the printed bar index, buffer value, and reference value isolate exactly where the divergence begins, which is typically far more useful for debugging than comparing two overlaid chart lines visually.
This script complements rather than replaces visual inspection. Figure 2 shows RollingStdDev and the built-in StdDev attached to the same chart, confirming the identical match on every closed bar that the cross-validation script verifies numerically. The only expected difference is on the forming bar, where RollingStdDev deliberately carries forward the last closed value rather than updating on every tick, as explained in Section 5.

Figure 2: RollingStdDev(200) (top) and the built-in StdDev(200) (bottom) on EURUSD Daily. Both indicators track identically on closed bars, confirming the circular buffer's output matches the reference implementation.
Section 6: Performance Benchmark — PerformanceBenchmark.mq5
This script runs both the circular buffer implementation and a naive ArrayCopy() array-shift implementation across the same sequence of 5,000 synthetic price values, measures per-operation execution time in microseconds using GetMicrosecondCount(), tests at window sizes of 50, 200, 500, and 1,000 periods, prints a structured comparison table to the Experts tab, and renders a CCanvas bar chart on the active chart showing the execution time divergence between the two approaches.
Structs and Helper Functions
//+------------------------------------------------------------------+ //| PerformanceBenchmark.mq5 | //+------------------------------------------------------------------+ #property script_show_inputs //--- Includes #include <Circular_Buffer_Implementation/CircularBuffer.mqh> #include <Canvas\Canvas.mqh> //--- Input parameters input int InpDataPoints = 5000; // Number of synthetic price values //+------------------------------------------------------------------+ //| Benchmark result structure | //+------------------------------------------------------------------+ struct SBenchmarkResult { int period; double cb_us_per_op; // Circular buffer: microseconds per push double shift_us_per_op; // Array-shift: microseconds per push }; //+------------------------------------------------------------------+ //| NaiveShift — reference implementation using ArrayCopy shifting | //+------------------------------------------------------------------+ void NaiveShift(double &arr[], const double value, const int period) { ArrayCopy(arr, arr, 1, 0, period - 1); arr[0] = value; }
SBenchmarkResult is a plain struct that holds results for one period configuration. Grouping the three fields — period, circular buffer time, and array-shift time — into a struct allows DrawBarChart() to receive all data in a single array rather than requiring multiple parallel arrays.
NaiveShift() implements the canonical array-shift pattern. The call ArrayCopy(arr, arr, 1, 0, period - 1) copies period - 1 elements from source index 0 into destination index 1, shifting everything rightward by one slot. Then arr[0] = value stores the new element at the front. This is the baseline being benchmarked.
Benchmark Runner Functions
//+------------------------------------------------------------------+ //| RunCircularBufferBenchmark — times CCircularBuffer<double> push | //+------------------------------------------------------------------+ double RunCircularBufferBenchmark(const double &prices[], const int period) { CCircularBuffer<double> buf(period); int n = ArraySize(prices); ulong t_start = GetMicrosecondCount(); for(int i = 0; i < n; i++) buf.Push(prices[i]); ulong t_end = GetMicrosecondCount(); double elapsed = (double)(t_end - t_start); return(elapsed / (double)n); } //+------------------------------------------------------------------+ //| RunShiftBenchmark — times the naive ArrayCopy shift approach | //+------------------------------------------------------------------+ double RunShiftBenchmark(const double &prices[], const int period) { double arr[]; ArrayResize(arr, period); ArrayInitialize(arr, 0.0); int n = ArraySize(prices); ulong t_start = GetMicrosecondCount(); for(int i = 0; i < n; i++) NaiveShift(arr, prices[i], period); ulong t_end = GetMicrosecondCount(); double elapsed = (double)(t_end - t_start); return(elapsed / (double)n); }
Both runner functions follow the same structure: set up the data structure, record the start time with GetMicrosecondCount(), run the push loop over all 5,000 prices, record the end time, and return the average microseconds per operation. GetMicrosecondCount() returns a ulong representing microseconds elapsed since the terminal started. Because both t_start and t_end are unsigned 64-bit integers, their difference is always non-negative for a correctly ordered pair of calls. Dividing the total elapsed time by n gives the per-operation cost that the comparison table displays.
DrawBarChart()
//+------------------------------------------------------------------+ //| DrawBarChart — renders a CCanvas bar chart on the active chart | //+------------------------------------------------------------------+ void DrawBarChart(const SBenchmarkResult &results[], const int count) { //--- Canvas dimensions int canvas_w = 640; int canvas_h = 400; int margin_l = 80; int margin_r = 30; int margin_t = 40; int margin_b = 60; int plot_w = canvas_w - margin_l - margin_r; int plot_h = canvas_h - margin_t - margin_b; //--- Create the canvas object CCanvas canvas; if(!canvas.CreateBitmapLabel("BenchmarkChart", 80, 80, canvas_w, canvas_h, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("DrawBarChart: failed to create canvas label."); return; } //--- Background fill canvas.Erase(ColorToARGB(clrWhiteSmoke, 255)); //--- Title canvas.FontSet("Arial", 13, FW_BOLD); canvas.TextOut(canvas_w / 2, 12, "Circular Buffer vs Array Shift (µs / push)", ColorToARGB(clrBlack, 255), TA_CENTER | TA_TOP); //--- Find maximum value for y-axis scaling double max_val = 0.0; for(int i = 0; i < count; i++) { if(results[i].cb_us_per_op > max_val) max_val = results[i].cb_us_per_op; if(results[i].shift_us_per_op > max_val) max_val = results[i].shift_us_per_op; } if(max_val <= 0.0) max_val = 1.0; //--- Y-axis label canvas.FontSet("Arial", 11, FW_NORMAL); canvas.TextOut(12, margin_t + plot_h / 2, "µs / push", ColorToARGB(clrDimGray, 255), TA_LEFT | TA_VCENTER); //--- Draw bars int bar_group_w = plot_w / count; int bar_w = bar_group_w / 3; int bar_gap = bar_w / 2; for(int i = 0; i < count; i++) { int gx = margin_l + i * bar_group_w + bar_gap; //--- Circular buffer bar (blue) int cb_h = (int)MathRound((results[i].cb_us_per_op / max_val) * (double)plot_h); int cb_y = margin_t + plot_h - cb_h; canvas.FillRectangle(gx, cb_y, gx + bar_w, margin_t + plot_h, ColorToARGB(clrDodgerBlue, 220)); canvas.Rectangle(gx, cb_y, gx + bar_w, margin_t + plot_h, ColorToARGB(clrNavy, 255)); //--- Array shift bar (red) int sh_h = (int)MathRound((results[i].shift_us_per_op / max_val) * (double)plot_h); int sh_y = margin_t + plot_h - sh_h; int shx = gx + bar_w + 2; canvas.FillRectangle(shx, sh_y, shx + bar_w, margin_t + plot_h, ColorToARGB(clrTomato, 220)); canvas.Rectangle(shx, sh_y, shx + bar_w, margin_t + plot_h, ColorToARGB(clrDarkRed, 255)); //--- X-axis period label string lbl = IntegerToString(results[i].period) + "p"; canvas.TextOut(gx + bar_w, margin_t + plot_h + 8, lbl, ColorToARGB(clrDimGray, 255), TA_CENTER | TA_TOP); } //--- Legend int lx = margin_l; int ly = canvas_h - 22; canvas.FillRectangle(lx, ly, lx + 14, ly + 12, ColorToARGB(clrDodgerBlue, 220)); canvas.TextOut(lx + 18, ly, "Circular Buffer", ColorToARGB(clrBlack, 255), TA_LEFT | TA_TOP); lx += 140; canvas.FillRectangle(lx, ly, lx + 14, ly + 12, ColorToARGB(clrTomato, 220)); canvas.TextOut(lx + 18, ly, "Array Shift", ColorToARGB(clrBlack, 255), TA_LEFT | TA_TOP); //--- Axes canvas.Line(margin_l, margin_t, margin_l, margin_t + plot_h, ColorToARGB(clrBlack, 255)); canvas.Line(margin_l, margin_t + plot_h, canvas_w - margin_r, margin_t + plot_h, ColorToARGB(clrBlack, 255)); //--- Commit to screen canvas.Update(true); }
DrawBarChart() creates a CCanvas bitmap label anchored at coordinates (80, 80) on the active chart with dimensions 640×400 pixels, using COLOR_FORMAT_ARGB_NORMALIZE to enable full alpha compositing. The canvas is erased to clrWhiteSmoke as a clean background before anything is drawn. The title is centered using TA_CENTER | TA_TOP alignment flags.
Bar heights are scaled linearly against max_val, the largest timing value across all results, so the tallest bar always fills the plot area and smaller bars are proportionally sized. For each period group, two adjacent bars are drawn: a blue clrDodgerBlue bar for the circular buffer result and a red clrTomato bar for the array-shift result, separated by a 2-pixel gap. Each bar is filled first with FillRectangle() and then outlined with Rectangle() to give it a defined border. Period labels are drawn below each group on the x-axis, and a legend at the bottom identifies the two colors. The left vertical and bottom horizontal axis lines are drawn last so they overlay the bars cleanly. canvas.Update(true) commits the pixel buffer to the chart and forces an immediate screen redraw.
OnStart()
//+------------------------------------------------------------------+ //| OnStart — entry point for the script | //+------------------------------------------------------------------+ void OnStart(void) { //--- Generate synthetic price data (random walk) double prices[]; ArrayResize(prices, InpDataPoints); double price = 1.10000; MathSrand(42); for(int i = 0; i < InpDataPoints; i++) { price += ((double)(MathRand() % 201) - 100.0) * 0.00010; prices[i] = price; } //--- Window sizes to test int periods[] = {50, 200, 500, 1000}; int n_periods = ArraySize(periods); SBenchmarkResult results[]; ArrayResize(results, n_periods); //--- Run benchmarks Print("=== Circular Buffer vs Array Shift Benchmark ==="); Print(StringFormat("%-10s %-22s %-22s %-12s", "Period", "CircBuf (µs/op)", "Shift (µs/op)", "Speedup")); Print(StringFormat("%-10s %-22s %-22s %-12s", "----------", "----------------------", "----------------------", "------------")); for(int p = 0; p < n_periods; p++) { int period = periods[p]; double cb_us = RunCircularBufferBenchmark(prices, period); double shift_us = RunShiftBenchmark(prices, period); double speedup = (cb_us > 0.0) ? (shift_us / cb_us) : 0.0; results[p].period = period; results[p].cb_us_per_op = cb_us; results[p].shift_us_per_op = shift_us; Print(StringFormat("%-10d %-22.6f %-22.6f %-12.2f", period, cb_us, shift_us, speedup)); } Print("================================================="); //--- Render the bar chart DrawBarChart(results, n_periods); Print("Benchmark complete. Bar chart rendered on active chart."); }
OnStart() generates 5,000 synthetic price values using a pseudo-random walk seeded with MathSrand(42). The fixed seed ensures that both implementations receive identical input data across all runs, eliminating any confounding variation in the values being pushed. Each step adds a signed tick offset uniformly distributed in the range [-0.01000, +0.01000]. The benchmark then iterates over the four period configurations, calls both runner functions for each period, computes the speedup ratio as shift_us / cb_us, stores results in the SBenchmarkResult array, and prints a fixed-width comparison table using StringFormat() with left-justified %- width specifiers. After the table is printed, DrawBarChart() renders the visual result on the active chart.

Figure 3: CCanvas bar chart rendered by PerformanceBenchmark.mq5, showing per-push execution time for the circular buffer (blue) versus array-shift (red) across four window sizes.

Figure 4: Experts tab output from PerformanceBenchmark.mq5, showing per-operation timing and speedup for the circular buffer versus array-shift across four window sizes.
Section 7: Complexity Analysis
| Operation | Circular Buffer | Array-Shift Baseline |
|---|---|---|
| Push() | O(1) | O(n) |
| Get() | O(1) | O(1) |
| Mean() | O(n) | O(n) |
| Variance() | O(n) | O(n) |
| ToArray() | O(n) | O(1)* |
*The array-shift baseline maintains elements in insertion order by construction, so exporting is a direct copy without reordering. For the circular buffer, ToArray() requires a full traversal with index remapping to reconstruct chronological order — the same O(n) element count as the array-shift version but with additional arithmetic per element.
The defining asymmetry is Push(). Every other operation shares the same complexity class across both approaches. The entire performance case for the circular buffer rests on the O(n) to O(1) reduction in insertion cost, which becomes measurable at large window sizes and high call frequencies. It is worth noting that ArrayCopy() uses SIMD memory instructions internally, which means the array-shift times in practice do not scale as a clean linear multiple of the window size. The algorithmic complexity remains O(n) in element operations, but the hardware parallelism compresses the wall-clock expression of that cost at the window sizes tested. The speedup advantage of the circular buffer is therefore conservative at small window sizes and grows as the window exceeds CPU cache line boundaries.
Section 8. Limitations
CCircularBuffer<T> allocates its internal array at construction time and provides no Resize() method. If the required window size changes at runtime — for example, if a user changes an indicator's period input — the buffer must be reconstructed via assignment of a new temporary. The old contents cannot be migrated. In OnCalculate() this is handled naturally by the prev_calculated == 0 branch, which resets the buffer and rebuilds it from scratch over the next InpPeriod bars.
The two-pass variance algorithm is numerically stable for window sizes up to several thousand elements with double-precision inputs. For extremely long windows — tens of thousands of elements — with values that differ by many orders of magnitude, even the two-pass method can accumulate rounding error in the squared-deviation sum. For such cases a compensated summation scheme such as Kahan summation would be more appropriate, but indicator windows of that length are unusual in practice.
MQL5 does not support template specialization, so there is no way to enforce at compile time that T is a numeric type. A caller instantiating CCircularBuffer<string> will compile but produce nonsensical results from Mean() and Variance(). A runtime guard using typename(T) comparison could be added, but MQL5's typename() returns a string, making it an awkward mechanism for type enforcement.
OnInit() uses copy assignment to initialize g_Buffer. This relies on m_array[] being deep-copied. MQL5's array copy semantics handle this correctly for the current implementation, but developers adding future members should verify that copy semantics remain correct if those members include pointers or additional dynamic arrays.
Conclusion
CCircularBuffer<T> replaces the O(n) ArrayCopy() array-shift pattern with an O(1) head-advance insertion backed by a fixed-size array and modular arithmetic. The structure is templated, allowing use with any scalar numeric type, and provides arithmetic mean and population variance as O(n) traversal methods that operate directly on the internal storage without requiring chronological reordering.
The performance case is concrete. For a 1,000-period rolling window across 5,000 insertions, the array-shift approach performs close to five million element writes. The circular buffer approach performs exactly 5,000. The benchmark results confirm this: circular buffer insertion time remains flat across all four window sizes tested, while array-shift time grows with the period, producing speedup ratios between 13× and 23× depending on window size and CPU cache conditions.
The rolling standard deviation indicator demonstrates real-world integration. It attaches cleanly to any chart, respects MetaTrader 5's incremental recalculation protocol through the prev_calculated pattern, excludes the forming bar from all buffer operations to prevent tick-level corruption, and carries the last closed bar's value forward to the forming bar slot so the chart line remains visually continuous. Its output on closed bars is numerically identical to the built-in StdDev indicator in population variance mode.
Appropriate use cases include any MQL5 indicator or strategy component maintaining a rolling window of fixed depth: rolling mean, standard deviation, z-score, linear regression coefficient, or any custom rolling statistic that can be computed from the raw window contents. The structure is not appropriate for scenarios requiring dynamic resizing, iterator protocols, or window sizes that change frequently at runtime without triggering a full recalculation.
Programs used in the article:
| # | Name | Type | Description |
|---|---|---|---|
| 1 | CircularBuffer.mqh | Include File | Defines the templated CCircularBuffer<T> class, which provides O(1) push and O(1) indexed read operations on a fixed-capacity ring buffer backed by a single ArrayResize()-allocated array. |
| 2 | TestCircularBuffer.mq5 | Script | Unit-tests CCircularBuffer<double> in isolation by pushing values past capacity to exercise wrap-around, then asserting Count(), IsFull(), Get(), Mean(), Variance(), and ToArray() against hand-computed reference values, printing a PASS or FAIL line for each check. |
| 3 | RollingStdDev.mq5 | Custom Indicator | Implements a configurable-period rolling standard deviation indicator using CCircularBuffer<double>, plotting the result as a line series in a MetaTrader 5 subwindow via a single DRAW_LINE indicator buffer. |
| 4 | CrossValidateStdDev.mq5 | Script | Cross-validates RollingStdDev.mq5's closed-bar logic against an independent brute-force mean and variance calculation computed directly from iClose(), comparing both on every available bar and reporting any mismatch with its bar index and delta. |
| 5 | PerformanceBenchmark.mq5 | Script | Benchmarks CCircularBuffer<double> against a naive ArrayCopy() array-shift implementation across window sizes of 50, 200, 500, and 1,000 periods using GetMicrosecondCount() timing, printing a formatted comparison table to the Experts tab and rendering a CCanvas bar chart on the active chart. |
| 6 | Circular_Buffer.zip | Zip Archive | Zip archive containing all the attached files and their paths relative to the terminal's root folder. |
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Building a Divergence System (Part II): Adaptive SuperTrend Custom Indicator
Automating Trading Strategies in MQL5 (Part 50): Turtle Soup Liquidity Sweeps
Features of Experts Advisors
Reimagining Classic Strategies (Part 22): Ensemble Mean Reverting Strategy
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use