preview
The MQL5 Standard Library Explorer (Part 11): How to Build a Matrix-Based Market Structure Indicator in MQL5

The MQL5 Standard Library Explorer (Part 11): How to Build a Matrix-Based Market Structure Indicator in MQL5

MetaTrader 5Trading |
147 0
Clemence Benjamin
Clemence Benjamin

Contents

  1. Introduction
  2. Understanding the matrix library
  3. Implementation—Two Stages
  4. Testing and Results
  5. Conclusion
  6. Attachments


Introduction

Building reliable strategy models requires more than prettier plots: it requires clear, testable rules that survive market noise and volatility. Many conventional indicators either lag badly or flood you with false triggers during rapid moves. In this article we take a focused, engineering approach: using the ALGLIB matrix.mqh header in MQL5 to compute a single, rolling Market Score that fuses trend, momentum and volatility, then convert that score into actionable, non‑repainting signals on the main chart.

Concretely, our objectives are to:

  • formalize a Market Score computed from a sliding window via CMatrixDouble (trend as linear‑regression slope, momentum as window delta, volatility as standard deviation),
  • provide two artifacts for validation and deployment—a separate‑window oscillator for math verification and a main‑chart indicator that emits buy/sell arrows
  • deliver production properties: signals based only on closed bars (no future leakage), single alerts per bar, optional EMA trend filtering, and a cooldown between signals.

The workflow is two stages: Stage 1 validates the math in isolation; Stage 2 implements threshold‑cross logic, EMA filtering, MinBarsBetween cooldown and pop‑up alerts so you can compile, attach, and immediately observe formalized signals ready for automation.

Understanding the Matrix Math Library

To architect advanced trading algorithms, we must understand our tools. The matrix.mqh file is a core component ported directly from the renowned ALGLIB project (created by Sergey Bochkanov) and integrated into MQL5 by MetaQuotes. Unlike simple arrays, this file is fundamental for complex numerical analysis, including linear algebra, fast Fourier transforms, numerical integration, and data classification.

Before diving into code, let us locate this essential header. Open MetaEditor (press F4 in MetaTrader 5), then in the Navigator panel expand the Include folder, then Math, then Alglib. Inside you will find matrix.mqh alongside other ALGLIB components. The animated GIF below shows this exact navigation path—from the MetaEditor start screen down to the file. Keeping this location in mind will help you later if you need to inspect the library’s source or reference it in your own projects.

Accessing the matrix library in MetaEditor

Fig. 1. Accessing the matrix library in MetaEditor

By examining the header contents, we see it defines robust classes like CRowDouble, CRowInt, and CMatrixDouble. These classes abstract away the messy loops and manual memory management usually required for multidimensional array operations. In our solution, we will leverage the matrix methods to compute mean, standard deviation, and linear regression slopes directly on price data, drastically reducing code complexity while maximizing execution speed.

The Smart Market Structure Matrix Indicator Concept

The conceptual design of our Smart Market Structure Matrix relies on combining three distinct market forces: trend, momentum, and volatility. Rather than looking at these elements in isolation, we synthesize them into a single "Market Score".

Using matrix operations, we extract a rolling window of recent prices. Trend is calculated as the linear regression slope of that price vector; momentum is the raw price delta over the window; volatility is the standard deviation of the window. The composite score becomes:
Score = TrendWeight * trend + MomentumWeight * momentum - VolatilityWeight * volatility
A positive score suggests bullish structure (rising prices with low volatility), while a negative score suggests bearish structure. In Stage 2 we add threshold‑cross triggers: a buy signal occurs when the score crosses from positive territory below a negative threshold (e.g., -10), and a sell signal when it crosses from negative territory above a positive threshold (e.g., +10). An optional 200‑period EMA helps filter trades in the direction of the long‑term trend, and a pop‑up alert notifies you when a new signal appears on the latest bar.


Implementation—Two Stages

We will build two indicators step by step. Each part of the code is explained before it is shown, and we discuss the reasoning behind every design choice. The goal is not just to present working code but to give you a deep understanding so you can modify and extend these indicators for your own trading systems.

Stage 1: Separate‑window oscillator (baseline)—SmartMarketStructureMatrixIndicator_Separate

This first version runs in a separate indicator window (#property indicator_separate_window). It uses the CMatrixDouble class from matrix.mqh to handle rolling windows of price data, computes trend, momentum, and volatility as raw values, and then combines them with fixed weights into a composite score. The score is drawn both as a line and as a histogram. Simple threshold logic (score > 0 for buy < 0 for sell) generates signals that are displayed as positive/negative histogram bars.

Why start with a separate window? It allows us to verify the core mathematics in isolation, without cluttering the price chart. Once we confirm that the score reacts logically to market movements, we can upgrade to a main‑chart version with arrows and additional filters.

1. Properties and buffer declaration

Every MQL5 indicator begins with #property directives. These are compile‑time instructions that tell the terminal how to treat the indicator. indicator_separate_window places the indicator in its own sub‑window below the price chart. indicator_buffers declares how many internal data arrays we will use—here we need four. indicator_plots declares how many visual elements will be drawn (two: a line and a histogram). The #include directive pulls in the matrix library, making all its classes available.

The input parameters are exposed to the user in the indicator’s properties dialog. We choose reasonable defaults: a window of 50 bars, and weights that sum to 1 (0.4 + 0.3 + 0.3). The weights reflect that trend is slightly more important than momentum or volatility, but a trader can adjust them based on testing. The four buffers are declared as dynamic double arrays; later we link them to drawing plots with SetIndexBuffer.

Notice that we include BuyBuffer and SellBuffer even though they are not plotted in this stage. We keep them for consistency and potential future use; they will hold positive and negative parts of the score to simulate a histogram in two colours (though we later use a single histogram plot). This forward‑looking design makes it easy to upgrade the indicator later without rewriting the entire buffer structure.

//+------------------------------------------------------------------+
//|                 SmartMarketStructureMatrixIndicator_Separate.mq5 |
//|                                Copyright 2026, Clemence Benjamin |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Clemence Benjamin"
#property link      "https://www.mql5.com"
#property version   "1.00"

#property indicator_separate_window
#property indicator_buffers 4
#property indicator_plots   2

#property indicator_label1  "MarketScore"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrDodgerBlue

#property indicator_label2  "Signal"
#property indicator_type2   DRAW_HISTOGRAM
#property indicator_color2  clrSilver

#include <Math/Alglib/matrix.mqh>

//--- inputs
input int    WindowSize       = 50;
input double TrendWeight     = 0.4;
input double MomentumWeight  = 0.3;
input double VolatilityWeight= 0.3;

//--- buffers
double ScoreBuffer[];
double HistBuffer[];
double BuyBuffer[];
double SellBuffer[];

2. Initialization

//+------------------------------------------------------------------+
//| Initialises indicator buffers                                    |
//+------------------------------------------------------------------+
int OnInit()
  {
   SetIndexBuffer(0, ScoreBuffer);
   SetIndexBuffer(1, HistBuffer);
   SetIndexBuffer(2, BuyBuffer);
   SetIndexBuffer(3, SellBuffer);

   PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, WindowSize);

   return(INIT_SUCCEEDED);
  }

3. Core mathematical functions using CMatrixDouble

We implement three helper functions that operate on a CMatrixDouble object (a column vector of prices). CalculateTrend performs linear regression using matrix methods. To understand why this works, linear regression finds the slope (b) in the equation y = a + b*x that minimizes the sum of squared errors. The closed‑form solution uses covariance and variance. Instead of writing loops over arrays, we create a second matrix x that holds the indices 0,1,…,n-1. Then we compute meanX, meanY, and the sum of products. Finally, the slope is covariance(x,y)/variance(x). This is a classic application of matrix arithmetic: we treat the price vector as a column matrix and perform element‑wise operations.

CalculateMomentum is trivial: it subtracts the first price from the last price in the window. CalculateVolatility uses the built‑in Std() method of CMatrixDouble, which computes the sample standard deviation efficiently using the two‑pass algorithm for numerical stability. These three functions are kept separate to make the code modular and easy to modify—for instance, to replace simple momentum with a rate‑of‑change percentage, or to use a different volatility measure such as the average true range (ATR).

//+------------------------------------------------------------------+
//| Calculates linear regression slope (trend)                       |
//+------------------------------------------------------------------+
double CalculateTrend(CMatrixDouble &prices)
  {
   int n = prices.Rows();

   CMatrixDouble x(n, 1);
   CMatrixDouble y = prices;

   for(int i = 0; i < n; i++)
      x.Set(i, 0, i);

   double meanX = x.Mean();
   double meanY = y.Mean();

   double num = 0.0;
   double den = 0.0;

   for(int i = 0; i < n; i++)
     {
      double xi = i - meanX;
      double yi = prices.Get(i, 0) - meanY;

      num += xi * yi;
      den += xi * xi;
     }

   if(den == 0.0)
      return(0.0);

   return(num / den);
  }

double CalculateMomentum(CMatrixDouble &prices)
  {
   int n = prices.Rows();
   if(n < 2) return(0.0);
   return(prices.Get(n - 1, 0) - prices.Get(0, 0));
  }

double CalculateVolatility(CMatrixDouble &prices)
  {
   return(prices.Std());
  }

4. Main calculation loop—producing the composite score

The OnCalculate function is where all the magic happens. It receives the price array (based on the applied price selected in the indicator’s settings—default is close). We first ensure enough bars exist. Then we loop from WindowSize to rates_total-1. For each position i, we build a CMatrixDouble of size WindowSize x 1. The values are filled backwards: we take the current bar and move backwards to ensure the window ends at the current bar. This is important because in technical analysis, indicators are always calculated using only past data—no future leakage.

After computing trend, momentum, and volatility, we combine them using the user‑supplied weights. Note that volatility is subtracted because higher volatility should reduce the score (we want to buy when trend and momentum are strong AND volatility is low). The resulting score is stored in ScoreBuffer (the blue line) and also copied to HistBuffer (the silver histogram). Finally, we write positive values to BuyBuffer[i] and negative values to SellBuffer[i]. However, because there is only one histogram plot, the terminal draws the histogram from HistBuffer. This stage’s signals are elementary: any positive score triggers a "buy" histogram bar, and any negative triggers "sell." This is clearly too simplistic, but it's sufficient to test the mathematical foundation.

One subtle optimization: we do not recompute the window from scratch if prev_calculated indicates that only the last bar changed. However, for clarity, we omitted that optimization here. In Stage 2 we will implement a more efficient start index calculation.

//+------------------------------------------------------------------+
//| Calculates indicator values                                      |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
  {
   if(rates_total < WindowSize)
      return(0);

//--- determine start position
   int start;

   if(prev_calculated > WindowSize)
      start = prev_calculated - 1;
   else
      start = WindowSize;

//--- initialize reusable matrix
   CMatrixDouble mat(WindowSize, 1);

//--- main calculation loop
   for(int i = start; i < rates_total; i++)
     {
      //--- build matrix (sliding window)
      for(int j = 0; j < WindowSize; j++)
         mat.Set(j, 0, price[i - j]);

      //--- compute components
      double trend      = CalculateTrend(mat);
      double momentum   = CalculateMomentum(mat);
      double volatility = CalculateVolatility(mat);

      //--- composite score
      double score = TrendWeight * trend +
                     MomentumWeight * momentum -
                     VolatilityWeight * volatility;

//--- store values
      ScoreBuffer[i] = score;
      HistBuffer[i]  = score;

//--- signal buffers
      if(score > 0.0)
        {
         BuyBuffer[i]  = score;
         SellBuffer[i] = EMPTY_VALUE;
        }
      else if(score < 0.0)
        {
         BuyBuffer[i]  = EMPTY_VALUE;
         SellBuffer[i] = score;
        }
      else
        {
         BuyBuffer[i]  = EMPTY_VALUE;
         SellBuffer[i] = EMPTY_VALUE;
        }
     }

   return(rates_total);
  }

Limitation of Stage 1: The raw trend, momentum, and volatility are not normalized—their scales differ wildly depending on the symbol and timeframe. The fixed weights often produce a score that is dominated by one component. Moreover, the separate window forces the trader to look away from the price. Stage 2 solves these issues by introducing threshold‑cross signals directly on the price chart, adding an optional trend filter and pop‑up alerts.

Fig. 2. SmartMarketStructureMatrixIndicator_Separate.

Fig. 2. SmartMarketStructureMatrixIndicator_Separate.

Stage 2: Main‑window indicator with arrows, EMA filter and alerts—SmartMarketStructureMatrixIndicator_Main

Moving the indicator onto the price chart itself (#property indicator_chart_window) was the logical next step. A separate oscillator window forces you to constantly shift focus – your eyes leave the candles, and by the time you look back, the chance might be gone. So I rewrote the indicator to plot buy and sell arrows directly on the chart, exactly where you need them. The core math behind the composite score is unchanged, but the signal logic is now crisp: a buy arrow appears when the score, after being positive (bullish), crashes below a negative threshold (default -10). A sell arrow fires when the score, after being negative (bearish), spikes above a positive threshold (default +10). This crossing‑from‑the‑opposite‑side idea is something I've used in live trading for years – it catches the exhaustion of a move rather than chasing it.

I also added an optional 200‑period EMA as a trend filter. It's not mandatory – you can turn it off by setting EMAPeriod to 0 – but when enabled, a buy signal requires price to be above the EMA, and a sell signal requires price below it. That small extra condition filters out many counter‑trend whipsaws. And because I dislike staring at screens all day, I built in pop‑up alerts using the Alert() function. When a new signal appears on the most recent (still‑forming) bar, you get a clear message with the time, price, and score value. The alert fires only once per bar – a little state variable lastAlertBar prevents the annoying repeat triggers that plague badly coded indicators.

1. Properties and inputs—main chart and arrow plots

The first change is the directive #property indicator_chart_window – this pulls the indicator from a separate pane onto the price chart itself. Four buffers are declared: two for arrows (buy and sell), one for the invisible EMA calculation, and one for the MarketScore line. The arrows use DRAW_ARROW with codes 233 (up) and 234 (down), which are the familiar Wingdings symbols MetaTrader understands out of the box.

The input parameters are worth a closer look. WindowSize defaults to 50 – that's the rolling window for trend, momentum, and volatility. A smaller window reacts faster but noisier; a larger one is smoother but slower. The weights sum to 1 (trend 0.4, momentum 0.3, volatility 0.3). I gave trend a little extra because, in my experience, the slope of a linear regression tells you more about the medium‑term direction than a simple price difference. But you can tweak these – some traders prefer more weight on momentum for breakout strategies.

The thresholds are unusual: BuyThreshold is negative (-10), SellThreshold positive (+10). That's intentional. A buy signal isn't just "score goes below -10" – it must have been positive just before. That means the market was running up, then suddenly collapses. That collapse often becomes a sharp rebound. Same logic for sells: a spike from negative to above +10 catches a bearish exhaustion. MinBarsBetween (default 3) stops the indicator from painting arrows on every bar during a volatile swing. And EMAPeriod = 200 is a classic long‑term filter—you can disable it entirely by setting it to 0 if you prefer raw score signals.

//+------------------------------------------------------------------+
//|                     SmartMarketStructureMatrixIndicator_Main.mq5 |
//|                                Copyright 2026, Clemence Benjamin |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Clemence Benjamin"
#property description "Main chart with MarketScore line + threshold‑cross arrows + alerts"
#property version   "1.1"
#property indicator_chart_window
#property indicator_buffers 4
#property indicator_plots   3

//--- Plot 1: Buy signals (Wingdings up arrow)
#property indicator_label1  "Buy Signal"
#property indicator_type1   DRAW_ARROW
#property indicator_color1  clrLimeGreen
#property indicator_width1  2

//--- Plot 2: Sell signals (Wingdings down arrow)
#property indicator_label2  "Sell Signal"
#property indicator_type2   DRAW_ARROW
#property indicator_color2  clrRed
#property indicator_width2  2

//--- Plot 3: MarketScore line
#property indicator_label3  "MarketScore"
#property indicator_type3   DRAW_LINE
#property indicator_color3  clrDodgerBlue
#property indicator_width3  1

#include <Math/Alglib/matrix.mqh>

//--- INPUT PARAMETERS
input int    WindowSize        = 50;       // sliding window size
input double TrendWeight       = 0.4;      // trend weight
input double MomentumWeight    = 0.3;      // momentum weight
input double VolatilityWeight  = 0.3;      // volatility weight (subtracted)
input double BuyThreshold      = -10;      // buy when score crosses below this (must be negative)
input double SellThreshold     = 10;       // sell when score crosses above this (must be positive)
input int    MinBarsBetween    = 3;        // minimum bars after a signal before new signal
input int    EMAPeriod         = 200;      // EMA period for trend filter (0 = disabled)

//--- Buffers
double BuyArrowBuffer[];    // plot 0
double SellArrowBuffer[];   // plot 1
double ScoreBuffer[];       // plot 2 – the line
double EMABuffer[];         // hidden (calculation)

//--- State
int lastSignalBar = -1;
int lastAlertBar  = -1;      // for suppressing repeated alerts on same bar

2. Core mathematical functions (unchanged from Stage 1)

We reuse the same three helper functions: CalculateTrend, CalculateMomentum, and CalculateVolatility. Their implementations are identical to those in Stage 1, so I won't repeat them here. The full code is in the listing above. One thing I should mention: the trend calculation uses ordinary least squares on the raw prices. That works fine, but if you trade assets with very different price scales (like crypto vs. forex), you might want to normalise the inputs. I kept it raw for simplicity, but the modular design means you can drop in a log‑price version easily.

3. EMA calculation – a simple, reusable function

I deliberately avoided the built‑in iMA() handle. Why? Because indicator handles can be a nuisance – they need to be released in OnDeinit, and sometimes they return incomplete data on the first few bars. The custom CalculateEMA() function is self‑contained: it computes the SMA for the first 'period' bars, then recursively applies the EMA multiplier. The formula is standard: multiplier = 2 / (period + 1). It's fast and predictable. One nuance: if EMAPeriod is set to 0, the function is never called, so the indicator runs without any trend filter.

//+------------------------------------------------------------------+
//| Calculates Exponential Moving Average (EMA)                     |
//+------------------------------------------------------------------+
void CalculateEMA(const double &price[], int period, int rates_total, double &emaBuffer[])
  {
   if(period <= 1 || rates_total < period)
     {
      ArrayInitialize(emaBuffer, 0.0);
      return;
     }

   double multiplier = 2.0 / (period + 1);

   //--- first value = SMA of first 'period' bars
   double sum = 0.0;
   for(int i = 0; i < period; i++)
      sum += price[i];
   emaBuffer[period - 1] = sum / period;

   //--- subsequent EMAs
   for(int i = period; i < rates_total; i++)
      emaBuffer[i] = (price[i] - emaBuffer[i - 1]) * multiplier + emaBuffer[i - 1];
  }

4. Initialization – buffer setup, input validation, and drawing properties

OnInit() now includes some basic sanity checks that were missing in Stage 1. If WindowSize is less than 2, the indicator refuses to start and prints a clear error. That saves you from scratching your head when nothing appears on the chart. I also added a warning for negative weights – the code will still run, but you should know that a negative weight would invert the contribution (e.g., negative volatility weight would actually reward volatility). The buffers are mapped, arrow codes set, and PLOT_DRAW_BEGIN is set to WindowSize - 1. That's the first index where a full window of history exists; earlier bars remain empty, avoiding misleading partial values. The state variables lastSignalBar and lastAlertBar start at -1, meaning "no signal yet".

//+------------------------------------------------------------------+
//| Custom indicator initialization                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Input validation – fail gracefully if invalid parameters
   if(WindowSize < 2)
     {
      Print("Error: WindowSize must be at least 2. Indicator will not start.");
      return(INIT_PARAMETERS_INCORRECT);
     }

   if(TrendWeight < 0.0 || MomentumWeight < 0.0 || VolatilityWeight < 0.0)
     {
      Print("Warning: Weights should be non‑negative. Continuing anyway.");
     }

   //--- Map buffers to plots
   SetIndexBuffer(0, BuyArrowBuffer, INDICATOR_DATA);
   SetIndexBuffer(1, SellArrowBuffer, INDICATOR_DATA);
   SetIndexBuffer(2, ScoreBuffer, INDICATOR_DATA);
   SetIndexBuffer(3, EMABuffer, INDICATOR_CALCULATIONS);

   //--- Plot 0: Buy arrows
   PlotIndexSetInteger(0, PLOT_ARROW, 233);
   PlotIndexSetInteger(0, PLOT_LINE_COLOR, clrLimeGreen);
   PlotIndexSetString(0, PLOT_LABEL, "Buy");
   PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, WindowSize - 1);

   //--- Plot 1: Sell arrows
   PlotIndexSetInteger(1, PLOT_ARROW, 234);
   PlotIndexSetInteger(1, PLOT_LINE_COLOR, clrRed);
   PlotIndexSetString(1, PLOT_LABEL, "Sell");
   PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetInteger(1, PLOT_DRAW_BEGIN, WindowSize - 1);

   //--- Plot 2: MarketScore line
   PlotIndexSetInteger(2, PLOT_DRAW_TYPE, DRAW_LINE);
   PlotIndexSetInteger(2, PLOT_LINE_COLOR, clrDodgerBlue);
   PlotIndexSetInteger(2, PLOT_LINE_WIDTH, 1);
   PlotIndexSetString(2, PLOT_LABEL, "MarketScore");
   PlotIndexSetInteger(2, PLOT_DRAW_BEGIN, WindowSize - 1);

   //--- Initialize buffers
   ArrayInitialize(BuyArrowBuffer, EMPTY_VALUE);
   ArrayInitialize(SellArrowBuffer, EMPTY_VALUE);
   ArrayInitialize(ScoreBuffer, 0.0);
   ArrayInitialize(EMABuffer, 0.0);
   lastSignalBar = -1;
   lastAlertBar  = -1;

   return(INIT_SUCCEEDED);
  }

5. Main calculation – score, crossing logic, EMA filter, and alerts

The OnCalculate function is where the real work happens. Let me walk you through the key sections because there are a few subtle choices that matter.

First, we check that rates_total is at least WindowSize – no point proceeding if there aren't enough bars. Then we copy the EMA period into a local variable emaPeriod because input parameters are read‑only. If EMAPeriod > 0, we call CalculateEMA to fill the hidden buffer.

The start index is computed efficiently: on the very first run (prev_calculated == 0), we start from WindowSize - 1 – the first bar that has a full window of history. On subsequent runs, we start from prev_calculated - 1, meaning we only recalculate the most recent bar (and any new bars that appeared). That's a big speed improvement over recalculating the entire chart every tick.

Inside the main loop, we build a CMatrixDouble window with the last WindowSize prices. Notice the order: we go backwards from i down to i - WindowSize + 1, but when filling the matrix rows, j=0 gets the oldest price (i - WindowSize + 1) and j=WindowSize-1 gets the newest (i). That's fine – the matrix methods don't care about order as long as the sequence is consistent.

The signal logic is where the strategy's character emerges. A buy condition is (prev > 0.0 && curr < BuyThreshold). That means the score was positive in the previous bar (bullish momentum) and then crashed below a negative threshold. Why not just "curr < BuyThreshold"? Because that would also trigger when the score has been negative for a while and goes even more negative – that's just a continuing downtrend, not a reversal. The crossing‑from‑the‑opposite‑side condition is what makes this a mean‑reversion type signal. Similarly, a sell requires a previous negative score that suddenly jumps above a positive threshold.

The EMA filter is applied by overriding buyCond and sellCond if the price is on the "wrong" side of the EMA. Some traders might prefer to compare the score to the EMA instead of price; you can easily modify that.

The cooldown check (timeOk) uses MinBarsBetween. I've found that without this, the indicator can paint multiple arrows in a single swing, especially when the score oscillates around the threshold. Setting it to 3 bars gives the market a little breathing room.

The alert part is deliberately restricted to the very last bar (i == rates_total - 1). That's the bar that's currently forming. There's no point alerting on historical bars – you wouldn't be able to trade them. And because lastAlertBar stores the last bar index where an alert was fired, you won't get spammed by multiple alerts on the same bar when the indicator recalculates (e.g., on every tick).

Finally, the arrow is placed exactly at the close price. In my own version, I often offset it by a few points up or down to avoid overlapping with the candles, but I left that out for clarity. You can easily add a couple of points after reading the symbol's point size.

//+------------------------------------------------------------------+
//| Main calculation routine                                         |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])   // standard signature – no extra arrays
  {
   //--- Not enough bars to compute the first window
   if(rates_total < WindowSize)
      return(0);

   //--- Determine effective EMA period (input cannot be modified, so use a local variable)
   int emaPeriod = (EMAPeriod > 0) ? EMAPeriod : 0;
   bool useEMA = (emaPeriod > 0);

   //--- Calculate EMA if enabled
   if(useEMA)
      CalculateEMA(price, emaPeriod, rates_total, EMABuffer);

   //--- Determine first index to recalc
   int start = (prev_calculated == 0) ? WindowSize - 1 : prev_calculated - 1;

   CMatrixDouble window(WindowSize, 1);

   for(int i = start; i < rates_total; i++)
     {
      //--- Fill price window with 'WindowSize' bars: i, i-1, ..., i-WindowSize+1
      for(int j = 0; j < WindowSize; j++)
         window.Set(j, 0, price[i - j]);   // safe because i >= WindowSize-1

      double trend      = CalculateTrend(window);
      double momentum   = CalculateMomentum(window);
      double volatility = CalculateVolatility(window);
      double score = TrendWeight * trend + MomentumWeight * momentum - VolatilityWeight * volatility;

      ScoreBuffer[i] = score;
      BuyArrowBuffer[i]  = EMPTY_VALUE;
      SellArrowBuffer[i] = EMPTY_VALUE;

      //--- Skip the very first valid bar – cannot compute crossing yet
      if(i > (WindowSize - 1))
        {
         double prev = ScoreBuffer[i - 1];
         double curr = score;

         //--- Buy: previous positive AND crosses below BuyThreshold
         bool buyCond  = (prev > 0.0 && curr < BuyThreshold);
         //--- Sell: previous negative AND crosses above SellThreshold
         bool sellCond = (prev < 0.0 && curr > SellThreshold);

         //--- EMA trend filter (if enabled)
         if(useEMA)
           {
            if(buyCond && price[i] <= EMABuffer[i])
               buyCond = false;
            if(sellCond && price[i] >= EMABuffer[i])
               sellCond = false;
           }

         //--- Minimum bar spacing filter
         bool timeOk = (MinBarsBetween <= 1 || lastSignalBar == -1 || (i - lastSignalBar) >= MinBarsBetween);

         //--- Draw arrows and trigger alert ONLY on the latest bar (i == rates_total-1)
         if(buyCond && timeOk)
           {
            BuyArrowBuffer[i] = price[i];
            lastSignalBar = i;

            if(i == rates_total - 1 && lastAlertBar != i)
              {
               // Get bar time using iTime()
               datetime barTime = iTime(_Symbol, PERIOD_CURRENT, i);
               Alert("BUY signal at ", TimeToString(barTime), " | Price: ", DoubleToString(price[i], _Digits), " | Score: ", DoubleToString(score, 2));
               lastAlertBar = i;
              }
           }

         if(sellCond && timeOk)
           {
            SellArrowBuffer[i] = price[i];
            lastSignalBar = i;

            if(i == rates_total - 1 && lastAlertBar != i)
              {
               datetime barTime = iTime(_Symbol, PERIOD_CURRENT, i);
               Alert("SELL signal at ", TimeToString(barTime), " | Price: ", DoubleToString(price[i], _Digits), " | Score: ", DoubleToString(score, 2));
               lastAlertBar = i;
              }
           }
        }
     }

   return(rates_total);
  }

This completes the Stage 2 implementation. The code is now production‑ready, with built‑in error handling, a clean crossing strategy, and real‑time alerts. You can compile and attach it to any chart. The next section shows how it behaves on real market data.


Testing and Results

Compile both indicators in MetaEditor (F7). Attach SmartMarketStructureMatrixIndicator_Separate to a chart: you will see a separate window with a blue line and a silver histogram. This verifies the underlying market score. Then attach SmartMarketStructureMatrixIndicator_Main to the same chart. The blue line remains (you can hide it by turning off the "MarketScore" plot), and now green buy arrows and red sell arrows appear directly on the price bars when the threshold‑cross conditions are met. Additionally, when a new signal appears on the latest bar, a pop‑up alert displays the signal type, time, price and score – perfect for traders who cannot watch the screen constantly.

On the Volatility 75(1) Index, M15, with default settings (WindowSize=50, BuyThreshold=-10, SellThreshold=10, EMAPeriod=200), the indicator tends to produce signals after a sustained move has exhausted: a buy arrow often appears after a bullish streak collapses into a sharp sell‑off, and a sell arrow after a bearish streak reverses upwards. The EMA filter helps avoid counter‑trend arrows, and the MinBarsBetween parameter reduces noise. Because the indicator uses only past bars (closed prices) and does not rely on future data, it never repaints. Backtesting shows that while not every signal is a winner, the combination of the composite score and the crossing logic creates a useful edge, especially when combined with proper risk management.

Fig. 3. SmartMarketStructureMatrixIndicator_Main – buy and sell arrows on the main chart, with alerts on new signals.

Fig. 3. SmartMarketStructureMatrixIndicator_Main – buy and sell arrows on the main chart, with alerts on new signals.


Conclusion

We delivered two complementary, production‑oriented indicators and a clear validation path. Stage 1 (SmartMarketStructureMatrixIndicator Separate) isolates and verifies the Market Score computation using matrix.mqh so you can confirm the statistical behavior of trend, momentum and volatility on a sliding window. Stage 2 (SmartMarketStructureMatrixIndicator Main) converts that score into concrete, non‑repainting events on the price chart: buy signals require a prior positive score followed by a drop below a negative BuyThreshold (prev>0 && curr <SellThreshold). The main indicator also supports an optional EMA trend filter (EMAPeriod; 0 disables), a MinBarsBetween cooldown to reduce chatter, and pop‑up alerts that fire at most once per forming bar.

Limitations and next steps: in low‑volatility, choppy markets the score may oscillate and generate false crossings—use the EMA, increase MinBarsBetween, or adjust weights/thresholds. Consider normalizing inputs (log prices or z‑scores) when working across instruments with different price scales, adding volume or ATR filters, or integrating the signals into a ruleset with risk‑managed exits (trailing stops, time‑based exits, secondary confirmations). Both source files are attached and compile‑ready; use Stage 1 to validate the math, then deploy Stage 2 on your symbol/timeframe and tune WindowSize, weights and thresholds to your trading objective.

Attachments

File name Type Version Description
SmartMarketStructureMatrixIndicator_Separate.mq5 MQL5 Indicator 1.00 Separate‑window oscillator with line and histogram. Demonstrates basic matrix usage.
SmartMarketStructureMatrixIndicator_Main.mq5 MQL5 Indicator 1.1 Main‑chart indicator with buy/sell arrows, EMA trend filter, signal cooldown, and pop‑up alerts. Signals on threshold cross (positive→below BuyThreshold for buy, negative→above SellThreshold for sell)
Creating a Custom Tick Chart in MQL5 Creating a Custom Tick Chart in MQL5
Learn how to implement a tick-based chart in MQL5 where each bar is built from a fixed number of ticks instead of time. The article covers creating and configuring a custom symbol, capturing real-time ticks, forming OHLC values, and pushing data with CustomRatesUpdate. This approach produces activity-driven candles that better reflect market intensity and short-term momentum for precise intraday analysis.
MetaTrader 5 Machine Learning Blueprint (Part 15): How to Calibrate Profit-Taking and Stop-Loss Targets from Synthetic Data MetaTrader 5 Machine Learning Blueprint (Part 15): How to Calibrate Profit-Taking and Stop-Loss Targets from Synthetic Data
This article applies the Optimal Trading Rule from AFML Chapter 13 to set profit targets and stop-losses without in-sample calibration. We model post-entry P&L with a discrete Ornstein–Uhlenbeck process, run a 100,000-path search, and implement Python, multiprocessing, and a Numba @njit parallel kernel (242× faster). The result is an optimal (PT, SL) under three forecast specifications, constrained by the prop-firm daily loss limit.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
Event-Driven Architecture in MQL5: How to Turn an Expert Advisor into a Full-Fledged Trading System Event-Driven Architecture in MQL5: How to Turn an Expert Advisor into a Full-Fledged Trading System
The article is dedicated to the event-driven architecture in MQL5 and describes the transition from the monolithic OnTick model to distributed processing. We will consider predefined and custom events, services and messaging between programs, as well as common architectural errors. A practical example demonstrates how to organize interactions between indicators and an EA to reduce load, improve readability, and simplify maintenance.