Skip to content

Indicator Code Samples

Simple Indicators

The high minus low indicator calculates the difference between the high and low prices of the current bar and displays it in an output series. The series is drawn on the chart as a line connecting the resulting values in each bar.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    [Indicator(IsOverlay = false, TimeZone = TimeZones.UTC, AccessRights = AccessRights.None, ScalePrecision = 5)]
    public class HighMinusLow : Indicator
    {
        [Output("Main", LineColor = "Orange")]
        public IndicatorDataSeries Result { get; set; }

        public override void Calculate(int index)
        {
            Result[index] = Bars.HighPrices[index] - Bars.LowPrices[index];
        }
    }

The [Indicator...] declaration includes several parameters which are defined as follows.

  • IsOverlay. A boolean flag that defines whether the line will be overlayed on the chart or displayed in a separate UI panel.
  • TimeZone. A TimeZones class field that specifies the timezone of the indicator data and the server time.
  • AccessRights. An 'AccessRights' class field that determines the access rights allocated to your indicator.
  • ScalePrecision. An integer setting up the scale precision of the indicator output.

As stated previously, the Output attribute is declared to mark a property as an indicator output. This property should be public so that it can be referenced by other classes.

Indicator output should always be of the IndicatorDataSeries data type which is a list of doubles that can be indexed like an array. Therefore, the value at each [index] in the list Result can be assigned in the Calculate method as follows.

1
2
3
4
public override void Calculate(int index)
{
       Result[index] = Bars.HighPrices[index] - Bars.LowPrices[index];
}

Indicators with Parameters

In most cases, the output of indicators can vary depending on user input. The way customizable parameters are set up for indicators is similar to how this is done for cBots.

The below simple moving average indicator is designed to accept a price source and a period as customizable parameters. Such parameters (Source and Periods in this particular example) must be preceded by the Parameter attribute.

Similarly to the [Indicator()] and [Output()] attributes discussed earlier, the [Parameter()] attribute can define certain characteristics applied to the user input.

1
[Parameter("MA Periods", DefaultValue = 14, MinValue = 1, MaxValue = 20)]

Above, we specify the following characteristics.

  • The name displayed to denote this parameter in the cTrader Automate UI ("MA Periods").
  • The default value of the parameter; it can changed by the user when customizing an instance (DefaultValue = 14)
  • The minimum and maximum parameter values (MinValue = 1, MaxValue = 20)

In the following snippet, we show how customizable parameters can be integrated into indicator code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    [Indicator(IsOverlay = false, ScalePrecision = 5)]
    public class SimpleMovingAverage : Indicator
    {
        [Parameter("Price")]
        public DataSeries Source { get; set; }

        [Parameter("MA Type", DefaultValue = MovingAverageType.Exponential)]
        public MovingAverageType MaType { get; set; }

        [Parameter("MA Periods", DefaultValue = 14, MinValue = 1, MaxValue = 20)]
        public int Periods { get; set; }

        [Parameter("Message", DefaultValue = true)]
        public bool DisplayChartMessage { get; set; }

        [Output("Main", LineColor = "Red")]
        public IndicatorDataSeries Result { get; set; }

        public override void Calculate(int index)
        {
            double sum = 0;

            for (var i = index - Periods + 1; i <= index; i++)
            {
                sum += Source[i];
            }
            Result[index] = sum / Periods;
        }
    }

After successfully building this indicator and running an instance, you can click on the 'cog' icon next to the instance name to open a contextual menu containing all customizable parameters.

Image title

The same parameters are also listed in the 'Parameters' tab. By default, it is located to the left of the 'Trade Watch' display.

Image title

For a brief on parameters and data types, please, consult our reference library and our intro to C# and .NET.

Nested Indicators

Nested indicators are defined as indicators the value of which depends on the results of calculations performed by other indicators. They are useful when writing certain types of extensions such as the DeMark 9 indicator. Consider the following sample code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    [Indicator(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class SampleDeMarker : Indicator
    {
        [Parameter(DefaultValue = 14)]
        public int Periods { get; set; }

        [Output("DMark", Color = Colors.Turquoise)]
        public IndicatorDataSeries DMark { get; set; }

        private IndicatorDataSeries deMin;
        private IndicatorDataSeries deMax;
        private MovingAverage deMinMA;
        private MovingAverage deMaxMA;

        protected override void Initialize()
        {
            deMin = CreateDataSeries();
            deMax = CreateDataSeries();
            deMinMA = Indicators.MovingAverage(deMin, Periods, MovingAverageType.Simple);
            deMaxMA = Indicators.MovingAverage(deMax, Periods, MovingAverageType.Simple);
        }

        public override void Calculate(int index)
        {
            deMin[index] = Math.Max(Bars.LowPrices[index - 1] - Bars.LowPrices[index], 0);
            deMax[index] = Math.Max(Bars.HighPrices[index] - Bars.HighPrices[index - 1], 0);

            var min = deMinMA.Result[index];
            var max = deMaxMA.Result[index];

            DMark[index] = max / (min + max);
        }
    }

In the example above, deMinMA and deMaxMA are two variables used for calculating the value of our DeMarker indicator.

Nested indicators need to be defined in the Initialize() method. For example, deMinMA is defined as the simple moving average of the series deMin.

1
deMinMA = Indicators.MovingAverage(deMin, Periods, MovingAverageType.Simple);

deMin, in turn, is defined in the Calculate() method as the maximum of the last two low price values.

1
deMin[index] = Math.Max(Bars.LowPrices[index - 1] - Bars.LowPrices[index], 0);

To simplify working with nested indicators, IntelliSense automatically populates the list of all built-in indicators when you type Indicators followed by a . into the code editor. It will also display all related input parameters once you select a certain indicator from this list.

Image title

Lazy Loading

cTrader Automate uses lazy loading when you use referenced indicators. In other words, the values supplied by the referenced indicator are not calculated until your code starts actively using them.

If you access the Outputs data of a referenced indicator, cTrader will start loading the indicator data by calling its Calculate() method on past and future bars. In any other case, the referenced indicator will remain idle and, therefore, will not consume any system resources.

This also means that if your indicator does not have any Output or if you have tried to access any of its public properties, you will get the default value of the property in question. To resolve this issue, please, call the Calculate() method of your custom indicator from the Calculate() method of the current indicator.

Oscillators and the 'Levels' Attribute

The term 'oscillators' encompasses all indicators that oscillate around a certain constant variable.

When creating oscillators, it is useful to first draw a horizontal or 'level' line at that constant value; the indicator will then oscillate around this line. In many cases, the constant value equals zero.

In the below example, we define a momentum oscillator. It typically oscillates around the value of 100. We add a level line at this value using the Levels attribute which is declared prior to the indicator attributes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    [Levels(100)]
    [Indicator()]
    public class MomentumOscillator : Indicator
    {
        [Parameter()]
        public DataSeries Source { get; set; }

        [Parameter(DefaultValue = 14, MinValue = 1)]
        public int Periods { get; set; }

        [Output("Main", LineColor = "Green")]
        public IndicatorDataSeries Result { get; set; }

        public override void Calculate(int index)
        {
            Result[index] = 100 * Source[index] / Source[index - Periods];
        }
    }

Note that the Levels attribute can only be used if the indicator is not overlayed on the chart, meaning that the IsOverlay property is not set to true. By default, the value of IsOverlay is false. If this attribute is omitted from your code, Levels should work properly.

If you need to establish multiple 'Level' lines, add a comma-separated list of values within the parenthesis as displayed below.

1
[Levels(0, 50, 100)] 
1
[Levels(50.5, 50.75)] 
1
[Levels(0.001, 0.002)] 

The 'IsLastBar' Property

In some cases, you may want to create an indicator that has to be calculated only at the last bar in the trading chart. Simplifying this, the IsLastBar property can be used to check whether the index parameter of the Calculate() method is that of the last bar.

The below indicator is based on UTC time; however, it can display the last open time in New York and Tokyo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
    [Indicator(IsOverlay = true, TimeZone = TimeZones.UTC)]
    public class TimeInDifferentParts : Indicator
    {
        public override void Calculate(int index)
        {
            if (IsLastBar)
                DisplayTime(index);
        }

        protected void DisplayTime(int index)
        {
            DateTime nyDateTime = Bars.OpenTimes[index].AddHours(-5);
            DateTime tokyoDateTime = Bars.OpenTimes[index].AddHours(7);

            string nyTime = nyDateTime.ToShortTimeString();
            string tokyoTime = tokyoDateTime.ToShortTimeString();

            Chart.DrawStaticText("Title", "Last Bar OpenTime \n NY " + nyTime + "\n" + "Tokyo " + tokyoTime, VerticalAlignment.Top, HorizontalAlignment.Left, Color.Lime);
        }
    }

Combining Indicators

cTrader Automate allows for combining multiple indicators when they are not overlayed on the chart and are, instead, outputted to a separate display.

The following indicator combines the Aroon, RSI, and directional movement system indicators in one.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
    [Indicator(IsOverlay = false, TimeZone = TimeZones.UTC, ScalePrecision = 5)]
    public class Aroon_RSI_DMS : Indicator
    {
        private Aroon aroon;
        private RelativeStrengthIndex rsi;
        private DirectionalMovementSystem dms;

        [Parameter()]
        public DataSeries Source { get; set; }

        [Parameter(DefaultValue = 14)]
        public int Periods { get; set; }

        [Output("Aroon Up", LineColor = "LightSkyBlue")]
        public IndicatorDataSeries AroonUp { get; set; }

        [Output("Aroon Down", LineColor = "Red")]
        public IndicatorDataSeries AroonDn { get; set; }

        [Output("Rsi", LineColor = "Green")]
        public IndicatorDataSeries Rsi { get; set; }

        [Output("DI Plus", LineColor = "DarkGreen")]
        public IndicatorDataSeries DmsDIPlus { get; set; }

        [Output("DI Minus", LineColor = "DarkRed")]
        public IndicatorDataSeries DmsDIMinus { get; set; }

        [Output("ADX", LineColor = "Blue")]
        public IndicatorDataSeries DmsADX { get; set; }

        protected override void Initialize()
        {
            // Initialize and create nested indicators
            aroon = Indicators.Aroon(Periods);
            rsi = Indicators.RelativeStrengthIndex(Source, Periods);
            dms = Indicators.DirectionalMovementSystem(Periods);
        }

        public override void Calculate(int index)
        {
            AroonUp[index] = aroon.Up[index];
            AroonDn[index] = aroon.Down[index];

            Rsi[index] = rsi.Result[index];

            DmsADX[index] = dms.ADX[index];
            DmsDIMinus[index] = dms.DIMinus[index];
            DmsDIPlus[index] = dms.DIPlus[index];
        }
    }

Multiple Timeframes

  • Using multiple timeframes

The following example displays a moving average indicator on different timeframes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
    [Indicator(IsOverlay = true, TimeZone = TimeZones.UTC)]
    public class MultiTF_MA : Indicator
    {
        [Parameter(DefaultValue = 50)]
        public int Period { get; set; }

        [Output("MA", LineColor = "Yellow")]
        public IndicatorDataSeries MA { get; set; }

        [Output("MA5", LineColor = "Orange")]
        public IndicatorDataSeries MA5 { get; set; }

        [Output("MA10", LineColor = "Red")]
        public IndicatorDataSeries MA10 { get; set; }

        private Bars bars5;
        private Bars bars10;

        private MovingAverage ma;
        private MovingAverage ma5;
        private MovingAverage ma10;

        protected override void Initialize()
        {
            bars5 = MarketData.GetBars(TimeFrame.Minute5);
            bars10 = MarketData.GetBars(TimeFrame.Minute10);

            ma = Indicators.MovingAverage(Bars.ClosePrices, Period, MovingAverageType.Triangular);
            ma5 = Indicators.MovingAverage(bars5.ClosePrices, Period, MovingAverageType.Triangular);
            ma10 = Indicators.MovingAverage(bars10.ClosePrices, Period, MovingAverageType.Triangular);
        }

        public override void Calculate(int index)
        {
            MA[index] = ma.Result[index];

            var index5 = bars5.OpenTimes.GetIndexByTime(Bars.OpenTimes[index]);
            if (index5 != -1)
                MA5[index] = ma5.Result[index5];

            var index10 = bars10.OpenTimes.GetIndexByTime(Bars.OpenTimes[index]);

            if (index10 != -1)
                MA10[index] = ma10.Result[index10];
        }
    }
  • Using multiple timeframes and symbols

The following example displays the moving average indicator on multiple timeframes and symbols.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
    [Indicator(IsOverlay = true, TimeZone = TimeZones.UTC)]
    public class MultiSymbolMA : Indicator
    {
        private MovingAverage ma1, ma2, ma3;
        private Bars bars2, bars3;
        private Symbol symbol2, symbol3;

        [Parameter(DefaultValue = "EURCHF")]
        public string Symbol2 { get; set; }

        [Parameter(DefaultValue = "EURCAD")]
        public string Symbol3 { get; set; }

        [Parameter(DefaultValue = 14)]
        public int Period { get; set; }

        [Parameter(DefaultValue = MovingAverageType.Simple)]
        public MovingAverageType MaType { get; set; }

        [Output("MA Symbol 1", LineColor = "Magenta")]
        public IndicatorDataSeries Result1 { get; set; }

        [Output("MA Symbol 2", LineColor = "Magenta")]
        public IndicatorDataSeries Result2 { get; set; }

        [Output("MA Symbol 3", LineColor = "Magenta")]
        public IndicatorDataSeries Result3 { get; set; }

        protected override void Initialize()
        {
            symbol2 = Symbols.GetSymbol(Symbol2);
            symbol3 = Symbols.GetSymbol(Symbol3);

            bars2 = MarketData.GetBars(TimeFrame, symbol2.Name);
            bars3 = MarketData.GetBars(TimeFrame, symbol3.Name);

            ma1 = Indicators.MovingAverage(Bars.ClosePrices, Period, MaType);
            ma2 = Indicators.MovingAverage(bars2.ClosePrices, Period, MaType);
            ma3 = Indicators.MovingAverage(bars3.ClosePrices, Period, MaType);
        }

        public override void Calculate(int index)
        {
            ShowOutput(Symbol, Result1, ma1, Bars, index);
            ShowOutput(symbol2, Result2, ma2, bars2, index);
            ShowOutput(symbol3, Result3, ma3, bars3, index);
        }

        private void ShowOutput(Symbol symbol, IndicatorDataSeries result, MovingAverage movingAverage, Bars bars, int index)
        {
            var index2 = bars.OpenTimes.GetIndexByTime(Bars.OpenTimes[index]);
            result[index] = movingAverage.Result[index2];

            string text = string.Format("{0} {1}", symbol.Name, Math.Round(result[index], symbol.Digits));
            Chart.DrawStaticText(symbol.Name, text, VerticalAlignment.Top, HorizontalAlignment.Right, Color.Yellow);
        }
    }

Last update: December 6, 2023