Introduction
What is an indicator? It is a tool intended for displaying a certain type of data. Usually it is information about the price series properties, exactly this type of indicators will be considered further.
Each indicator also has its own properties and characteristics: for example, the range of values, the overbuying/overselling zones, the line crossing, tops and bottoms... They are numerous and can be successively used together with the main indicator values. However, such properties are not always vivid. The reasons can be different - small size of the indicator window, lower concentration, etc.
The purpose of this article is to help you to improve the descriptive and informational value of indicators, as well as partial automation and facilitation of the code implementation process. I hope the code below will cause no difficulties both for professional developers and beginners.
The article is intended for those who have at least the starting level of MQL4 knowledge and can implement simple ideas and algorithms in a code, as well as knows about the structure of code storage in the terminal and can use the libraries (experts/libraries) and header files (experts/include).
1. Setting Up a Task
Among all indicators I would like to outline the most informative and often used ones:




Let's discuss them.
2. Basic Notions
In order to avoid misunderstanding, let's spend some time viewing the indicator structure.
#property indicator_separate_window
#property indicator_buffers 3
#property indicator_minimum 0
#property indicator_maximum 100
#property indicator_color1 White
#property indicator_color2 Red
#property indicator_color3 Blue
extern int RSIPeriod = 9;
extern int AppliedPrice = 0;
extern int MAPeriod = 5;
double Values[];
double SmoothedValues[];
double Crosses[];
int DigitsUsed = 5;
0x7FFFFFFF
int EmptyValueUsed = 0;
int init()
{
SetIndexBuffer(0, Values);
SetIndexBuffer(1, SmoothedValues);
SetIndexBuffer(2, Crosses);
SetIndexStyle(0, DRAW_LINE);
SetIndexStyle(1, DRAW_LINE, STYLE_DASH);
SetIndexStyle(2, DRAW_ARROW, STYLE_SOLID, 2);
SetIndexArrow(2, 251);
IndicatorDigits(DigitsUsed);
SetIndexDrawBegin(0, RSIPeriod);
SetIndexDrawBegin(1, RSIPeriod + MAPeriod);
SetIndexDrawBegin(2, RSIPeriod + MAPeriod + 1);
return(0);
}
int start()
{
int toCount = Bars - IndicatorCounted();
for (int i = toCount - 1; i >=0; i--)
{
Values[i] = NormalizeDouble(iRSI(Symbol(), 0, RSIPeriod, AppliedPrice, i), DigitsUsed);
}
for (i = toCount - 1; i >=0; i--)
{
SmoothedValues[i] = NormalizeDouble(iMAOnArray(Values, 0, MAPeriod, 0, MODE_EMA, i), DigitsUsed);
}
return(0);
}
3. Characteristics
Let's consider the characteristics in details.
3.1. Line Crossing
Perhaps every developer has tried to implement a trading algorithm using the crossing of two MAs (moving averages); or the same of MACD's base line and signal line crossing. Let's try to visualize it and make it much more evident by displaying the cross point in the indicator.
As an example, throughout the text we will use the Relative Strength Index, so our aim is to develop an improved RSI with some new advantages.
3.1.1. Task Formalization
It is necessary to mark the line crossing bars in a separate buffer.
3.1.2. Problems
It seems that everything is simple and clear. The task is not difficult and it can be solved by a couple of code lines.
We have to describe the line crossing like this one:

if ((x1 > y1 && x2 < y2) || (x1 < y1 && x2 > y2))
{
// line crossing here
}
Or we can simplify it:
if ((x1 - y1)*(x2 - y2) < 0)
{
// line crossing here
}
But let's consider the following case:

Note that the green points have the same values. In such a case we have no line crossing, just a line touching case only.
But here:

it is not so simple to determine the crossing, this case is quite possible.
And also it is necessary to distinguish correctly a touching case from the crossing case, taking into account that during search we can find an empty value in a buffer or history end.
3.1.3. Solution
From this place on the function init() will not be considered, because it is not important. The full code can be found in the source.
Here is the solution for the simple and smoothed values of the Relative Strength Index Indicator.
extern int RSIPeriod = 9;
extern int AppliedPrice = 0;
extern int MAPeriod = 5;
double Values[]; Values
double SmoothedValues[];
double Crosses[];
int DigitsUsed = 5;
int EmptyValueUsed = 0;
int start()
{
int toCount = Bars - IndicatorCounted();
// Mark the crosses
for (i = toCount - 1; i >=0; i--)
{
// i+1 must be greater or equal bars count in the history
if (i + 1 >= Bars)
{
continue;
}
// if some of the values are empty, it is not necessary to check
if (
Values[i] == EmptyValueUsed ||
Values[i + 1] == EmptyValueUsed ||
SmoothedValues[i] == EmptyValueUsed ||
SmoothedValues[i + 1] == EmptyValueUsed ||
Values[i] == EMPTY_VALUE ||
Values[i + 1] == EMPTY_VALUE ||
SmoothedValues[i] == EMPTY_VALUE ||
SmoothedValues[i + 1] == EMPTY_VALUE
)
{
continue;
}
Crosses[i] = EMPTY_VALUE;
// crossing check (simple case)
if ((Values[i] - SmoothedValues[i])*(Values[i + 1] - SmoothedValues[i + 1]) < 0)
{
Crosses[i] = SmoothedValues[i];
continue;
}
// the crossing condition for a complicated case -
// when crossing contain several bars with the same values
//
if (Values[i + 1] == SmoothedValues[i + 1] && Values[i] != SmoothedValues[i])
{
int index = i + 1;
bool found = false;
while (
index < Bars && out of range
Values[index] != EmptyValueUsed && check for empty
Values[index] != EMPTY_VALUE &&
SmoothedValues[index] != EmptyValueUsed && check for empty
SmoothedValues[index] != EMPTY_VALUE)
{
if (Values[index] != SmoothedValues[index])
{
found = true;
break;
}
index++;
}
if (!found)
{
continue;
}
// checking the ends for crossing
if ((Values[i] - SmoothedValues[i])*(Values[index] - SmoothedValues[index]) < 0)
{
Crosses[i] = SmoothedValues[i];
}
}
}
return(0);
}
3.1.4. Automation
In this section we consider the solution of the problem using the Indicator_Painting library.
#include <Indicator_Painting.mqh>
extern int RSIPeriod = 9;
extern int AppliedPrice = 0;
extern int MAPeriod = 5;
double Values[]; Values
double SmoothedValues[]; Smoothed values
double Crosses[]; Crosses
int DigitsUsed = 5;
int EmptyValueUsed = 0;
int start()
{
Reading values
Mark crosses
MarkCrosses
(
Values,
SmoothedValues,
Crosses,
toCount - 1,
0,
CROSS_ALL,
0);
return(0);
}
3.2. Level Mark
Some of the oscillators with limited and strictly set range of values (RSI, Stochastic Oscillator, DeMarker, Money Flow Index, Williams' Percent Range) often need to mark zones or levels. For example, the flat zones, the overbought\oversold zones, the trend zones... Let's try to outline the defined level by using the different color painting.
3.2.1. Task Formalization
It is necessary to mark in a separate buffer the bars with the values outside of the defined levels.
3.2.2. Problems
It is not as simple as it seems at first sight.
The first problem is drawing bars, on which the defined level is crossed. Here is the solution.
extern int RSIPeriod = 9;
extern int AppliedPrice = 0;
extern int HigherLevel = 70;
extern int LowerLevel = 30;
double Higher[]; t
double Lower[]; Oversold
double Values[];
int DigitsUsed = 5;
int EmptyValueUsed = 0;
int start()
{
int toCount = Bars - IndicatorCounted();
Reading values
// Mark levels - upper
for (i = toCount - 1; i >=0; i--)
{
check for empty values
if (Values[i] == EMPTY_VALUE || Values[i] == EmptyValueUsed)
{
continue;
}
Higher[i] = EMPTY_VALUE;
if (Values[i] >= HigherLevel)
{
Higher[i] = Values[i];
}
}
// for the levels mark - the code is same
return(0);
}
This code solves the task defined, but it has one problem:

It is difficult to analyze it visually, because the signal drawing starts from the value, which is greater (lower) than the level. That's why some of the signal bars cannot be analyzed because of the drawing peculiarities caused by rapid changes of the neighbour bars.
The solution is to mark not only the bars higher (lower) than the defined level, but also the bar formed prior to the already marked ones and the one next to them. And it is necessary to mark them not with their own values, but with the values of the level.
The second problem appears after the solution of the first - the signal buffer has pseudo-marks of the "false" level breakdowns as a result of algorithm complication.
It means that the price was outside the level during the bar formation, however the final bar has the value inside the level. Because of this fact we can get a picture like this:

The problem appears only if we use an indicator on the realtime quotes. The solution is simple - we are checking two bars (0 and 1) while processing, the others are checked only if necessary.
Afterthat, we will have the following picture for RSI:

3.2.3. Solution
So, let's write all it in a code:
extern int RSIPeriod = 9;
extern int AppliedPrice = 0;
extern int HigherLevel = 70;
extern int LowerLevel = 30;
double Higher[]; Overbought
double Lower[]; Oversold
double Values[]; Values
int DigitsUsed = 5;
int EmptyValueUsed = 0;
// looking at least two bars - 0 and 1.
int Depth = 2;
int start()
{
int toCount = Bars - IndicatorCounted();
Reading values
toCount = MathMax(toCount, Depth);
for (i = toCount - 1; i >=0; i--)
{
if (Values[i] == EMPTY_VALUE || Values[i] == EmptyValueUsed) continue;
Higher[i] = EMPTY_VALUE;
if (Values[i] >= HigherLevel)
{
Higher[i] = Values[i];
if (Values[i + 1] < HigherLevel && Values[i + 1] != EmptyValueUsed)
{
// mark it also but with the level value
Higher[i + 1] = HigherLevel;
}
}
// if current lower
else
{
// if previous is greater
if (Values[i + 1] >= HigherLevel && Values[i + 1] != EMPTY_VALUE)
{
Higher[i] = HigherLevel;
}
}
}
return(0);
}
3.2.4. Automation
The solution of the same problem by using the Indicator_Painting library.
#include <Indicator_Painting.mqh>
extern int RSIPeriod = 9;
extern int AppliedPrice = 0;
extern int HigherLevel = 70;
extern int LowerLevel = 30;
double Higher[]; Overbought
double Lower[]; Oversold
double Values[]; Values
int DigitsUsed = 5;
int EmptyValueUsed = 0;
int Depth = 2;
int start()
{
int toCount = Bars - IndicatorCounted();
Read values
for (int i = toCount - 1; i >= 0; i--)
{
Values[i] = NormalizeDouble(iRSI(Symbol(), 0, RSIPeriod, AppliedPrice, i), DigitsUsed);
}
// Mark levels - upper
MarkLevel(Values, Higher, 0, toCount - 1, HigherLevel, GREATER_THAN, EmptyValueUsed);
// Mark levels - lower
MarkLevel(Values, Lower, 0, toCount - 1, LowerLevel, LESS_THAN, EmptyValueUsed);
return(0);
}
3.3. Tops and Bottoms
The extreme points (extremums) of the indicator can be used as signals. In this article the "extremum" term is used in its simplest meaning - if the bar has a greater (lower) value than its neighbour values, it is considered extremum.
3.3.1. Task Formalization
It is necessary to mark the bars with extreme values in a separate buffer.
3.3.2. Problems
Let's consider some examples:

Here evident extreme values are marked with red lines:
if ((x1 > x2 && x3 > x2) || (x1 < x2 && x3 < x2))
{
// x2 is extremal
}
Or we can simplify it:
if ((x1 - x2)*(x2 - x3) < 0)
{
// x2 is extremal
}
But let's consider the case:

The marked points have the same values. The blue point is an extremum. It will be not easy to define it. And in the following case:

there is no extremum, we assume that there is a bend.
The solution of such cases is to find the second end, like for the crossing cases.
3.3.3. Solution
Here is the code:
extern int RSIPeriod = 9;
extern int AppliedPrice = 0;
double Values[]; Values
double Extremums[];
int DigitsUsed = 5;
int EmptyValueUsed = 0;
int start()
{
int toCount = Bars - IndicatorCounted();
for (int i = toCount - 1; i >=0; i--)
{
Values[i] = NormalizeDouble(iRSI(Symbol(), 0, RSIPeriod, AppliedPrice, i), DigitsUsed);
}
for (i = toCount - 1; i >=0; i--)
{
// check the values relative to the current index.
if (i + 2 >= Bars)
{
continue;
}
// check for empty values, if there are, it is not necessary to check
if (
Values[i] == EmptyValueUsed ||
Values[i + 1] == EmptyValueUsed ||
Values[i + 2] == EmptyValueUsed
)
{
continue;
}
// fill the current value of the mark buffer
Extremums[i + 1] = EMPTY_VALUE;
// cross condition - the simple case
if ((Values[i] - Values[i + 1])*(Values[i + 1] - Values[i + 2]) < 0)
{
// we have found the cross
Extremums[i + 1] = Values[i + 1];
continue;
}
// the cross condition in a complicated case -
// when top contain several bars with the same value
if (Values[i + 1] == Values[i + 2] && Values[i] != Values[i + 1])
{
// there is possible extremum - to check it
// we have to find the second end
int index = i + 2;
bool found = false;
while (index < Bars && Values[index] != EmptyValueUsed && Values[index] != EMPTY_VALUE)
{
if (Values[i + 2] != Values[index])
{
// ok, we have found the second end
found = true;
break;
}
index++;
}
if (!found)
{
continue;
}
// checking the ends for a cross
if ((Values[i] - Values[i + 1])*(Values[i + 1] - Values[index]) < 0)
{
there is a cross
Extremums[i + 1] = Values[i + 1];
}
}
}
return(0);
}
3.3.4. Automation
The same task solution by using the Indicator_Painting library.
#include <Indicator_Painting.mqh>
extern int RSIPeriod = 9;
extern int AppliedPrice = 0;
double Values[]; Values
double Extremums[]; Extremal points
int DigitsUsed = 5;
int EmptyValueUsed = 0;
int start()
{
int toCount = Bars - IndicatorCounted();
for (int i = toCount - 1; i >=0; i--)
{
Values[i] = NormalizeDouble(iRSI(Symbol(), 0, RSIPeriod, AppliedPrice, i), DigitsUsed);
}
MarkExtremums(Values, Extremums, toCount - 1, 0, DIR_ALL, EmptyValueUsed);
return(0);
}
3.4. Coloring by Direction
This visualization method is used in some standard indicators and and also can be useful.
3.4.1. Task Formalization
It is necessary to paint some sets of the indicator values (for example the sets while it goes upward or downward) with different colors. The direction is meant in the simplest case - if the current value is greater than previous, we have upward direction, otherwise we assume a downward direction.
3.4.2. Problems
Let's start from the feature. It's assumed that we have a base data buffer and this buffer is plotted. If not so, we will do it, because for the custom directional painting we need at least 2 colors, and at least two buffers. Now the feature. If we draw one of directions over the base buffer, it is not necessary
to paint the other direction - we will see it on the unpainted pieces
of the base buffer.
The base buffer:

Here is the base buffer with upward direction plotted:

That's why further we will consider the only one direction plot, upward for example. Let's consider the problems that may occur.
The naive implementation of the feature is the following:
extern int RSIPeriod = 9;
extern int AppliedPrice = 0;
double Values[]; Values
double Growing[];
int DigitsUsed = 5;
int EmptyValueUsed = 0;
int start()
{
int toCount = Bars - IndicatorCounted();
// Reading values
for (int i = toCount - 1; i >=0; i--)
{
Values[i] = NormalizeDouble(iRSI(Symbol(), 0, RSIPeriod, AppliedPrice, i), DigitsUsed);
}
// Mark the growing levels - we will get the falling levels as a result
for (i = toCount - 1; i >=0; i--)
{
// filling the current values with empty values
Growing[i] = EMPTY_VALUE;
if (Values[i] > Values[i + 1])
{
Growing[i] = Values[i];
Growing[i + 1] = Values[i + 1];
}
}
return(0);
}
Compiling, attaching to charts and... see the result of code execution:

There are some troubles, they are marked with points. Let's consider, why it looks so. While painting one direction, we get an effect by leaving empty values (EMPTY_VALUE) at other parts.
Lets' consider the following case:

The additional buffer data which should have nonempty
values are marked by black points. To avoid the straight line plotting between the points (using the style DRAW_LINE) it is necessary to have at least one nonempty value between them. All of the plotted range has no empty values, that's why base buffer is plotted only at "sawtooth" pieces.
The solution of this problem is unevident - for example smoothing or use of some of the additional conditions makes it much more complicated. As a result we might get several bars repainting or something else difficult.
The solution is to use two additional buffers for it. Then it is possible to alternate the painted pieces - thus, we will get the necessary empty values in each of the buffers.
Let's assign the different colors to the additional buffers and see the result:

The main problem is solved, but there is another small problem with the zero bar. It is redrawn every time, and in some cases it is necessary to delete the upward direction plot, if the direction has changed.
Lets consider the implementation.
3.4.3. Solution
extern int RSIPeriod = 9;
extern int AppliedPrice = 0;
double Values[]; Values
double Growing1[]; First growing buffer
double Growing2[]; Second growing buffer
int DigitsUsed = 5;
int EmptyValueUsed = 0;
int start()
{
int toCount = Bars - IndicatorCounted();
// Reading values
for (int i = toCount - 1; i >=0; i--)
{
Values[i] = NormalizeDouble(iRSI(Symbol(), 0, RSIPeriod, AppliedPrice, i), DigitsUsed);
}
// Mark for the growing levels - we will get the falling levels as a resut
for (i = toCount - 1; i >=0; i--)
{
// check of an empty values
// assume that the current values are empty
Growing1[i] = EMPTY_VALUE;
Growing2[i] = EMPTY_VALUE;
// if it growing
if (Values[i] > Values[i + 1])
{
// if it growing on the previous bar
if (Values[i + 1] > Values[i + 2])
{
// writing to the current growing buffer
if (Growing1[i + 1] != EMPTY_VALUE) Growing1[i] = Values[i];
else Growing2[i] = Values[i];
}
// if the previous bar was not increasing
else
{
// write to the buffer which it was not used the last 2 bars
// we must have at least one such bar
if (Growing2[i + 2] == EMPTY_VALUE)
{
Growing2[i] = Values[i];
Growing2[i + 1] = Values[i + 1];
}
else
{
Growing1[i] = Values[i];
Growing1[i + 1] = Values[i + 1];
}
}
}
// if the last value does not grow, remove it
else if (i == 0)
{
if (Growing1[i + 1] != EMPTY_VALUE && Growing1[i + 2] == EMPTY_VALUE)
{
Growing1[i + 1] = EMPTY_VALUE;
}
if (Growing2[i + 1] != EMPTY_VALUE && Growing2[i + 2] == EMPTY_VALUE)
{
Growing2[i + 1] = EMPTY_VALUE;
}
}
}
return(0);
}
3.4.4. Automation
The same task solution by using the Indicator_Painting library.
In the library there is also similar implementation for downward direction.
#include <Indicator_Painting.mqh>
extern int RSIPeriod = 9;
extern int AppliedPrice = 0;
double Values[]; Values
double Growing1[];
double Growing2[];
int DigitsUsed = 5;
int EmptyValueUsed = 0;
int start()
{
int toCount = Bars - IndicatorCounted();
// Reading values
for (int i = toCount - 1; i >=0; i--)
{
Values[i] = NormalizeDouble(iRSI(Symbol(), 0, RSIPeriod, AppliedPrice, i), DigitsUsed);
}
// Mark the growing levels - we will get the falling levels automatically
MarkGrowing(Values, Growing1, Growing2, toCount - 1, 0, EmptyValueUsed);
return(0);
}
As a result of all of the work completed there is an Indicator_Painting library.
It has been designed specially for automation of the operations described, with some additions.
Here is a list of available functions:
void MarkExtremums(
double values[],
double& extremums[],
int startIndex,
int endIndex,
int direction,
double emptyValueUsed);
void MarkCrosses(
double values1[],
double values2[],
double& crosses[],
int startIndex,
int endIndex,
int direction,
double emptyValueUsed);
void MarkLevelCrosses(
double values[],
double level,
double& crosses[],
int startIndex,
int endIndex,
int direction,
double emptyValueUsed);
void MarkLevel(
double values[],
double& level[],
int startIndex,
int endIndex,
double levelValue,
int condition,
double emptyValueUsed);
void MarkDynamicLevel(
double values[],
double dynamicLevel[],
double& level[],
int startIndex,
int endIndex,
int condition,
double emptyValueUsed);
void MarkGrowing(
double values[],
double& growing1[],
double& growing2[],
int startIndex,
int endIndex,
double emptyValueUsed);
void MarkReducing(
double values[],
double& reducing1[],
double& reducing2[],
int startIndex,
int endIndex,
double emptyValueUsed);
Here are some examples of the library use:


In order to use the library the following should be done:
1. Copy the file "Indicator_Painting.mq4" to folder "experts/libraries"
2. Copy the file "Indicator_Painting.mqh" to folder "experts/include"
3. Add the following string to the indicator code:
#include <Indicator_Painting.mqh>
Now it is possible to use all of the library functions. See the file "Indicator_Painting.mqh" for details.
You can find the examples in the files attached to the article.
Conclusion
The author hopes that this article will help and simplify the work of some people. The author considers the aim achieved.
Acknowledgments
The author would like to thank Mr. Viktor Rustamov (granit77) for the task suggestion, help and comments to improve the article.
Translated from Russian by MetaQuotes Software Corp.
Original article: http://articles.mql4.com/ru/799