Non-Repainting Supertrend Indicator MQL5: Code & EA Signals

Learn to code a non-repainting Supertrend indicator in MQL5 with proper buffer handling. Includes complete code, EA signal integration, and common pitfalls to.

non-repainting-supertrend-indicator-mql5-code-ea

Why Most Supertrend Indicators Repaint (And Why You Should Care)

You've seen it happen. You're watching a chart, the Supertrend flips from red to green right at a pivot bottom, and you think "perfect entry." Then you scroll back an hour later and notice that flip happened three bars earlier than you remembered. Or worse, the signal completely disappeared.

That's repainting. The indicator recalculates historical values using data that wasn't available when that bar closed. In MQL4 this is common because many coders use iClose(NULL, 0, i) inside the indicator loop without properly locking the bar state. In MQL5, the same sloppy coding patterns carry over because the platform's CopyClose() function makes it easy to grab current data and overwrite past buffers.

I've been burned by this more times than I care to admit. I once ran a forward test on an EA that used a repainting Supertrend for entry signals. The backtest looked phenomenal — 80% win rate. Live? It was a disaster. The EA kept entering on signals that vanished after two bars. I lost three months of development time before I realized the indicator was the problem, not my strategy logic.

This article walks you through coding a non-repainting Supertrend indicator in MQL5 from scratch. We'll handle buffers correctly so your EA gets stable, reliable signals. No fluff, no copy-paste from MT4 tutorials that don't work on MT5. Just working code and the reasoning behind it.

What Makes Supertrend Repaint?

Supertrend is a trend-following indicator built on ATR. It calculates upper and lower bands like this:

  • Upper Band = (High + Low) / 2 + Multiplier × ATR
  • Lower Band = (High + Low) / 2 - Multiplier × ATR

The indicator flips from uptrend to downtrend when price closes below the upper band, and vice versa. Simple enough.

The repaint problem creeps in because ATR itself is a moving average of true range. On the current (unfinished) bar, ATR changes with every tick. If your indicator recalculates the ATR value for bar index 0 during the OnCalculate() function and then writes that value into the buffer for bar 0, the historical bars (index 1, 2, 3...) remain unchanged. That's fine for the current bar. But many coders make the mistake of recalculating the entire buffer array on every tick, which overwrites historical values with new ATR data that includes the current bar's range.

The fix is brutally simple: never recalculate a bar that has already closed. Once bar 1 (the previous closed bar) has its Supertrend value computed, that value must stay frozen forever. Only bar 0 gets updated.

The MQL5 Buffer Handling Problem

MQL5 uses CopyBuffer() to fetch indicator data, but the buffer indexing is zero-based and aligned to the rates_total parameter. Here's where most developers trip up:

// Wrong approach - recalculates everything on every tick
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[])
{
   for(int i = 0; i < rates_total; i++)
   {
      // Recalculate all bars - BAD
      SupertrendBuffer[i] = CalculateSupertrend(i);
   }
   return(rates_total);
}

This loop runs every tick. When a new tick arrives, bar 0's high/low changes, which changes the ATR for bar 0. But because the loop recalculates every bar, bar 1's Supertrend value also shifts — even though bar 1 has already closed. That's the repaint.

The correct MQL5 pattern uses prev_calculated to skip already-finished bars:

// Correct approach - only recalculate the current bar
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[])
{
   if(prev_calculated == 0)
   {
      // First run: calculate everything
      for(int i = 1; i < rates_total; i++)
         SupertrendBuffer[i] = CalculateSupertrend(i);
   }
   
   // Only calculate bar 0 on subsequent ticks
   SupertrendBuffer[0] = CalculateSupertrend(0);
   
   return(rates_total);
}

This is the foundation. But we need more nuance — the Supertrend state (up vs down) must also be consistent across bars. Let's build the full indicator.

Complete Non-Repainting Supertrend MQL5 Code

Here's the full indicator code. I'll explain the key parts after.

//+------------------------------------------------------------------+
//|                                                  Supertrend_MT5.mq5 |
//|                                          Copyright 2025, YourName |
//|                                                   MIT License     |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025"
#property link      ""
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 3
#property indicator_plots   1

//--- Plot Supertrend
#property indicator_label1  "Supertrend"
#property indicator_type1   DRAW_COLOR_LINE
#property indicator_color1  clrLimeGreen, clrCrimson
#property indicator_style1  STYLE_SOLID
#property indicator_width1  2

//--- Input parameters
input int      InpPeriod       = 10;      // ATR Period
input double   InpMultiplier   = 3.0;     // Multiplier
input ENUM_APPLIED_PRICE InpAppliedPrice = PRICE_TYPICAL; // Applied Price

//--- Indicator buffers
double         SupertrendBuffer[];
double         ColorBuffer[];
double         TrendBuffer[];      // Internal: 1 = uptrend, -1 = downtrend

//--- ATR handle
int            atrHandle;

//+------------------------------------------------------------------+
int OnInit()
{
   //--- Set index buffers
   SetIndexBuffer(0, SupertrendBuffer, INDICATOR_DATA);
   SetIndexBuffer(1, ColorBuffer, INDICATOR_COLOR_INDEX);
   SetIndexBuffer(2, TrendBuffer, INDICATOR_CALCULATIONS);
   
   //--- Set empty value
   PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, 0.0);
   
   //--- Create ATR handle
   atrHandle = iATR(_Symbol, _Period, InpPeriod);
   if(atrHandle == INVALID_HANDLE)
   {
      Print("Failed to create ATR handle. Error: ", GetLastError());
      return(INIT_FAILED);
   }
   
   IndicatorSetString(INDICATOR_SHORTNAME, "Supertrend(" + 
                      IntegerToString(InpPeriod) + "," + 
                      DoubleToString(InpMultiplier, 1) + ")");
   
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
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[])
{
   if(rates_total < InpPeriod + 1)
      return(0);
   
   //--- Copy ATR data
   double atr[];
   ArraySetAsSeries(atr, true);
   int copied = CopyBuffer(atrHandle, 0, 0, rates_total, atr);
   if(copied != rates_total)
   {
      Print("ATR copy failed. Copied: ", copied, " Expected: ", rates_total);
      return(0);
   }
   
   //--- Determine start index
   int start;
   if(prev_calculated == 0)
      start = 1;  // First run: calculate from bar 1
   else
      start = 0;  // Subsequent runs: only update bar 0
   
   //--- Main calculation loop
   for(int i = start; i < rates_total; i++)
   {
      //--- Calculate median price
      double medPrice;
      switch(InpAppliedPrice)
      {
         case PRICE_CLOSE:    medPrice = close[i]; break;
         case PRICE_OPEN:     medPrice = open[i]; break;
         case PRICE_HIGH:     medPrice = high[i]; break;
         case PRICE_LOW:      medPrice = low[i]; break;
         case PRICE_MEDIAN:   medPrice = (high[i] + low[i]) / 2.0; break;
         case PRICE_TYPICAL:  medPrice = (high[i] + low[i] + close[i]) / 3.0; break;
         case PRICE_WEIGHTED: medPrice = (high[i] + low[i] + close[i] + close[i]) / 4.0; break;
         default:             medPrice = (high[i] + low[i]) / 2.0;
      }
      
      //--- Calculate upper and lower bands
      double upperBand = medPrice + InpMultiplier * atr[i];
      double lowerBand = medPrice - InpMultiplier * atr[i];
      
      //--- Determine Supertrend value and trend direction
      if(i == 0)
      {
         //--- For bar 0, use previous bar's trend to decide
         if(TrendBuffer[1] == 1)  // Previous was uptrend
         {
            if(close[i] <= upperBand)
            {
               TrendBuffer[i] = -1;
               SupertrendBuffer[i] = upperBand;
               ColorBuffer[i] = 1;  // Red
            }
            else
            {
               TrendBuffer[i] = 1;
               SupertrendBuffer[i] = lowerBand;
               ColorBuffer[i] = 0;  // Green
            }
         }
         else  // Previous was downtrend
         {
            if(close[i] >= lowerBand)
            {
               TrendBuffer[i] = 1;
               SupertrendBuffer[i] = lowerBand;
               ColorBuffer[i] = 0;  // Green
            }
            else
            {
               TrendBuffer[i] = -1;
               SupertrendBuffer[i] = upperBand;
               ColorBuffer[i] = 1;  // Red
            }
         }
      }
      else
      {
         //--- For closed bars, use standard logic
         if(TrendBuffer[i-1] == 1)  // Previous was uptrend
         {
            if(close[i] <= upperBand)
            {
               TrendBuffer[i] = -1;
               SupertrendBuffer[i] = upperBand;
               ColorBuffer[i] = 1;
            }
            else
            {
               TrendBuffer[i] = 1;
               SupertrendBuffer[i] = MathMax(lowerBand, SupertrendBuffer[i-1]);
               ColorBuffer[i] = 0;
            }
         }
         else  // Previous was downtrend
         {
            if(close[i] >= lowerBand)
            {
               TrendBuffer[i] = 1;
               SupertrendBuffer[i] = MathMin(upperBand, SupertrendBuffer[i-1]);
               ColorBuffer[i] = 0;
            }
            else
            {
               TrendBuffer[i] = -1;
               SupertrendBuffer[i] = upperBand;
               ColorBuffer[i] = 1;
            }
         }
      }
   }
   
   return(rates_total);
}
//+------------------------------------------------------------------+

Key Design Decisions

Let me walk through the critical parts.

Buffer count and type: We use three buffers. Buffer 0 holds the Supertrend line value (the band that follows price). Buffer 1 is a color index buffer that switches between 0 (green) and 1 (red). Buffer 2 is a calculation buffer that stores the trend direction internally — it's not plotted. The DRAW_COLOR_LINE plot type lets us change color per segment without extra indicators.

ATR via handle: We create an ATR handle in OnInit() using iATR(). This is critical. The ATR handle automatically updates with new ticks, but the CopyBuffer() call inside OnCalculate() pulls only the data we need. By controlling when we copy, we prevent the ATR values for closed bars from being overwritten. This is the MQL5 way — not recalculating ATR manually.

Start index logic: On the first run (prev_calculated == 0), we start at bar 1 and compute everything up to the latest bar. On subsequent ticks, we only process bar 0. This is the non-repainting guarantee. Bar 1's Supertrend value was computed when bar 1 was the current bar, and we never touch it again.

Band carry-over: Notice the MathMax() and MathMin() calls for closed bars. When the trend continues, we carry the previous band value forward and only update it if the new band is more extreme. This prevents the Supertrend line from jumping when the trend persists — it keeps the line smooth and anchored to the most extreme band during the trend.

Testing the Non-Repainting Behavior

After compiling, load the indicator on a chart. Open the Data Window (Ctrl+D) and watch the Supertrend value for bar 1 as new ticks come in. It should remain frozen. If you see it change, something is wrong — double-check your prev_calculated logic.

I recommend running the built-in "Indicator Tester" from the Strategy Tester. Set the testing mode to "Every tick" and run for 1000 bars. Export the indicator values to CSV and compare bar 1's Supertrend value at the start and end of the test. They should match exactly.

Integrating Supertrend Signals Into an EA

Now for the practical payoff. Here's how to pull signals from this indicator in your MQL5 EA.

// In your EA's OnInit()
int supertrendHandle = iCustom(_Symbol, _Period, "Supertrend_MT5",
                               InpPeriod, InpMultiplier, InpAppliedPrice);
if(supertrendHandle == INVALID_HANDLE)
{
   Print("Failed to create Supertrend handle");
   return(INIT_FAILED);
}

// In OnTick() or OnTimer()
double trendBuffer[];
ArraySetAsSeries(trendBuffer, true);
CopyBuffer(supertrendHandle, 2, 0, 3, trendBuffer);  // Buffer 2 = trend direction

int currentTrend = (int)trendBuffer[0];
int prevTrend    = (int)trendBuffer[1];

if(prevTrend == -1 && currentTrend == 1)
{
   // Buy signal: trend flipped from down to up
   // Place your buy logic here
}
else if(prevTrend == -1 && currentTrend == 1)
{
   // Sell signal: trend flipped from up to down
   // Place your sell logic here
}

Notice we're reading buffer index 2 (TrendBuffer), not buffer 0. The trend direction is a clean +1 or -1, making signal detection trivial. We copy 3 values to have the current and previous bar's trend.

Important: Always call CopyBuffer() with ArraySetAsSeries() set to true. MQL5's default array indexing is opposite to what most traders expect. Without this, trendBuffer[0] would be the oldest bar, not the current one.

Pros, Cons, and Risks

What This Indicator Does Well

  • No repaint on closed bars. Your EA gets deterministic signals. Backtest results match forward performance (assuming other factors are consistent).
  • Clean EA integration. The trend direction buffer eliminates the need to parse line values. You get a simple +1/-1 signal.
  • MQL5 native. Uses handles and proper buffer management. No reliance on deprecated MT4 functions.

What You Still Need to Watch

  • Bar 0 is still live. The current bar's Supertrend value changes with every tick. If your EA enters on bar 0 signals, you'll get different results between

Want to build your own version?

Recreate similar entry logic, risk rules, and filters in TradingBotMaker—no MQL coding. Start free.

Community

Clap for the article and open comments only when you want to read them.

0 claps0 comments

Related articles