Skip to content
NEXORA Money & AI

How to Create a BUY, SELL and NEUTRAL Signal Indicator for MetaTrader 5

September 30, 2026 · OYIN ADESOLA · 6 min read · Updated October 12, 2026
Cover graphic for the article: How to Create a BUY, SELL and NEUTRAL Signal Indicator for MetaTrader 5

Last checked: 12 October 2026. This guide builds on the MQL5 reference for custom indicators and on community discussion of closed-bar signals. Code is a teaching example, not a trading system; compile and test it on a demo account.

A BUY, SELL and NEUTRAL indicator is the simplest useful tool you can build in MetaTrader 5. It reads the market, applies a rule and prints one of three words on the chart. This guide shows how to make one from two moving averages, how to display the state as coloured text, and why the most important decision is not the rule but when you read it.

Decide the rule first

Write the rule in a sentence before you write any code. For this example:

  • BUY when the fast moving average is above the slow one and the last closed candle closed above the fast average.
  • SELL when the fast average is below the slow one and the last closed candle closed below the fast average.
  • NEUTRAL in every other case.

This is a teaching rule. It is not a recommendation to trade, and no moving-average rule is guaranteed to be profitable. Its value is that every part can be tested and explained.

Why NEUTRAL matters

Many beginner indicators only show two states, so they are always telling you to do something. A neutral state is an honest answer: “the conditions I check are not met”. When you add a neutral state you also reduce the number of weak signals, which is the real goal of any filter.

Summary graphic listing the main sections of: How to Create a BUY, SELL and NEUTRAL Signal Indicator for MetaTrader 5

Read closed bars only

In MQL5 the newest bar is still forming. Community discussions on MQL5.com make the same point repeatedly: a value calculated on the forming bar can change on every tick, so a signal can appear and then vanish before the bar closes. The safe design is to base the decision on the last closed bar. With the default array indexing in OnCalculate, that bar is at index rates_total minus 2, because the newest forming bar sits at rates_total minus 1.

The indicator code

#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots   0

input int  InpFast = 10;
input int  InpSlow = 30;
input ENUM_MA_METHOD InpMethod = MODE_EMA;

int hFast = INVALID_HANDLE;
int hSlow = INVALID_HANDLE;
string LBL = "SigLabel";

int OnInit()
  {
   hFast = iMA(_Symbol, _Period, InpFast, 0, InpMethod, PRICE_CLOSE);
   hSlow = iMA(_Symbol, _Period, InpSlow, 0, InpMethod, PRICE_CLOSE);
   if(hFast == INVALID_HANDLE || hSlow == INVALID_HANDLE)
      return(INIT_FAILED);
   ObjectCreate(0, LBL, OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(0, LBL, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, LBL, OBJPROP_XDISTANCE, 15);
   ObjectSetInteger(0, LBL, OBJPROP_YDISTANCE, 25);
   ObjectSetInteger(0, LBL, OBJPROP_FONTSIZE, 16);
   return(INIT_SUCCEEDED);
  }

void OnDeinit(const int reason)
  {
   ObjectDelete(0, LBL);
   IndicatorRelease(hFast);
   IndicatorRelease(hSlow);
  }

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 < InpSlow + 3)
      return(0);

   double fast[], slow[];
   // shift 1 = last closed bar (as-series copy)
   if(CopyBuffer(hFast, 0, 1, 1, fast) < 1) return(prev_calculated);
   if(CopyBuffer(hSlow, 0, 1, 1, slow) < 1) return(prev_calculated);

   double lastClose = close[rates_total - 2];
   string state = "NEUTRAL";
   color  clr   = clrGray;

   if(fast[0] > slow[0] && lastClose > fast[0])
     { state = "BUY";  clr = clrLimeGreen; }
   else if(fast[0] < slow[0] && lastClose < fast[0])
     { state = "SELL"; clr = clrTomato; }

   ObjectSetString(0, LBL, OBJPROP_TEXT, _Symbol + " " + state);
   ObjectSetInteger(0, LBL, OBJPROP_COLOR, clr);
   return(rates_total);
  }

What the code does

iMA creates a handle, which is a reference to a built-in moving average that the terminal calculates for you. CopyBuffer reads values from that handle. Its third parameter is the starting position counted from the newest bar, where 0 is the forming bar and 1 is the last closed bar, so the code asks for position 1. The label object shows the state in the corner of the chart. OnDeinit cleans up the label and releases the handles so the terminal does not keep unused indicators in memory.

Checking that it works

  1. Attach it to a liquid pair on a demo account.
  2. Switch timeframes. The label should change only when a candle has closed, not on every tick.
  3. Scroll back and compare the label with the built-in moving averages on the chart.
  4. Watch it for a few sessions and write down every state change with the time. If the label ever flips and then flips back inside one candle, you are reading the forming bar.

Limits of a three-state label

  • It shows only the latest state, not a history on the chart. To mark past signals you need arrow buffers.
  • Moving-average crossings lag. In a sideways market they flip often, which is why the next articles in this series add trend and range filters.
  • No indicator removes risk. Use stop losses and position sizing.

Installing and running the file

Save the code as SignalLabel.mq5 in the MQL5/Indicators folder, or create it through MetaEditor’s New Custom Indicator wizard and paste the code in. Press F7 to compile. If the compile output shows errors, read the first one only, fix it and compile again, because later errors are often side effects of the first. In the terminal, open the Navigator, right-click Indicators and choose Refresh, then drag SignalLabel onto a chart. The inputs tab lets you change the two periods and the averaging method without touching the code.

Troubleshooting the three states

  • The label always says NEUTRAL. Check that the fast period is smaller than the slow period. If they are reversed, the BUY condition can never be true together with the close test.
  • The label does not appear. The object may have been created in a sub-window or hidden behind another label. Press Ctrl+B to open the object list and look for SigLabel.
  • The label shows nothing right after loading. The moving averages need history to calculate. Wait a few seconds, or scroll the chart left to force the terminal to load more bars.
  • The state differs from a colleague’s screen. Different brokers have slightly different candle data, especially around session boundaries, so a close can differ by a few points.

When something looks wrong, add a Print line that outputs fast[0], slow[0] and lastClose, then read the numbers in the Experts tab. Numbers beat guesses.

What we could not confirm

  • Any win rate for this rule. We did not backtest it for this article.
  • That the rule suits your pair or timeframe.

Frequently asked questions

Why not read shift 0? Because the forming bar changes every tick, and a signal that changes after you saw it is misleading.

Can I add an alert? Yes. The alert article in this series shows how to fire one only once per candle.

Key takeaways

Write the rule in words, give the indicator a neutral state, read the last closed bar and verify against the built-in indicators. A label that never changes its mind inside a candle is more honest than one that does.

Sources checked

  • MQL5 Reference, iMA, CopyBuffer and custom indicator functions, mql5.com
  • MQL5.com community blog posts on closed-bar signals and repainting

Educational content only — not financial advice. Income and savings results vary and are never guaranteed. How we write

Enjoyed this story?

Get the best stories delivered weekly.

Leave a Reply

Your email address will not be published. Required fields are marked *

Share with