Skip to content

Creating an Indicator

Action Map

The following diagram showcases the action flow of creating and implementing a custom indicator.

graph TB
  A([Add a New Indicator]) ==> B([Edit the Indicator Code]);
  B ==> C([Save and Build Your Indicator]);
  C ==> D([Create and Customize an Indicator Instance]);

Add a New Indicator

Open the 'New ...' menu to the right of the search bar and select 'New Indicator'. If the 'New Indicator' option is already chosen, simply click on it.

Image title

cTrader will automatically create a new indicator containing sample code. You should see a 'New Indicator' extension added to the list of indicators.

You can rename your new indicator by selecting it and pressing F2. Alternatively, right-click on the new extension and choose 'Rename'.

Edit the Sample Code

Click on your new indicator to open the code editor window containing the following 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
34
35
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using cAlgo.API;
using cAlgo.API.Collections;
using cAlgo.API.Indicators;
using cAlgo.API.Internals;

namespace cAlgo
{
    [Indicator(AccessRights = AccessRights.None)]
    public class NewIndicator : Indicator
    {
        [Parameter(DefaultValue = "Hello world!")]
        public string Message { get; set; }

        [Output("Main")]
        public IndicatorDataSeries Result { get; set; }

        protected override void Initialize()
        {
            // To learn more about cTrader Automate visit our Help Center:
            // https://help.ctrader.com/ctrader-automate

            Print(Message);
        }

        public override void Calculate(int index)
        {
            // Calculate value at specified index
            // Result[index] = 
        }
    }
}

As we have discussed previously, the indicator attribute (Indicator) along with its optional properties such as TimeZone and AccessRights, precedes the indicator class (NewIndicator) declaration.

The [Output("OutputName")] declaration defines what your indicator is supposed to display. After using it, please, make sure that the property you are declaring next is of the IndicatorDataSeries data type. You can customize the default appearance of your indicator output by adding one more parameter to the [Output("OutputName")] declaration.

1
2
3
/* "Main" is the output name while "Orange"
is chosen as the line color. */
[Output("Main", LineColor = "Orange")]

The Initialize() method is called on when an indicator is launched. It is typically used to initialize variables and series such as nested indicators. In general, this method should contain all variables that have to be calculated on indicator start-up to avoid consuming unnecessary resources.

In turn, the Calculate() method is used to calculate a specific value for the index you would like to see displayed. It is automatically invoked on each historical bar; it is also invoked in real-time on each incoming tick. In the case of multi-symbol/multi-timeframe implementation, it will be invoked on each tick of each symbol that your indicator is tasked with analyzing. Think of the Calculate() method as the place in your code where you have to implement the actual indicator formula. For example, when using the half trend indicator formula, you would use the Calculate() method to define moving average intersections.

These two methods form the 'bread and butter' of every indicator and cannot be omitted. Cconsult our references library to learn more about the classes, methods, and properties you can use when coding indicators. In addition, you can always choose to write custom methods as shown in our introduction to C# and .NET.

Given what you have just learned and the examples provided in other parts of this documentation, edit the code of your indicator to suit your requirements.

Image title

In the example above, the Calculate() method determines the difference between the high and low price of each bar. The bars themselves are accessed by using their index in the Bars collection.

Copy and paste the below code just below the namespace declaration to create the same indicator.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[Indicator(IsOverlay = false, TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
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];
  }
}

Save and Build Your Indicator

Save your code by clicking on the 'Save' button located to the upper-left of the code editor window. Alternatively, press Ctrl+S. You can also right-click anywhere in the code editor and choose 'Save' from the newly opened menu.

Image title

Similarly to cBots, indicators need to be built before being deployed. To build an indicator, click the 'Build' icon at the top of the code editor or in the cBot menu. The keyboard shortcut for this action is Ctrl+B.

If the build succeeds, you will be notified by a new message in the 'Build Result' viewer. If the build fails, this window will, instead, display a summary of all errors encountered during the build process. Clicking on an error description in the 'Build Result' viewer will show you the exact place in your code where this error occurs.

If there are changes to your code made since the last build, you should see a red '*' sign next to the 'Build' icon. In this case, build the indicator again before running it.

Create and Customize an Indicator Instance

If you have successfully completed all of the above actions, your indicator should be ready for deployment. To start using it, create a new instance by performing one of the following actions.

  • Click on the '+' icon to the right of the extension name. In the menu that appears, select a symbol that you would like to trade.

Image title

  • Click on the 'three dots' icon to the right of the cBot name and select 'Add Instance'. cTrader will automatically create a new instance trading the EURUSD symbol using h1 as the timeframe.

You should see a new instance located directly below your indicator.

Image title

If you have specified any customizable parameters in your code, they will be listed in the 'Parameters' tab located to the left of the 'Trade Watch' display. The 'Lines' parameter is universal to all indicators that have a visible output. You can also customize the appearance of indicator lines which will override any settings you may have previously specified in the indicator code.

Image title

In contrast to cBots, you cannot import or export instance parameters from/to locally stored files.

NaN Arithmetics

In brief, indicators perform calculations to output certain visuals. As a result, it is essential that your code accurately handles fringe cases and boundary conditions for output calculations.

Consider the following code excerpt taken from an indicator outputting a detrended price oscillator.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
private MovingAverage movingAverage;

protected override void Initialize()
{
    movingAverage = Indicators.SimpleMovingAverage(Source, Periods);
}

public override void Calculate(int index)
{
    Result[index] = Source[index] - movingAverage.Result[index - Periods / 2 - 1];
}

The above code contains some general rules for calculation but it notably omits any boundary conditions.

For example, if Periods has the value of 10 and index has its value in the 0-9 range, our calculations will result in NaN (Not a Number). Our indicator simply cannot calculate the required value for a bar chart with a negative index. To address this problem, we could always add the following condition.

1
2
3
4
5
6
7
public override void Calculate(int index)
{
    if (index >= Periods + Periods / 2 + 1)
    {
        Result[index] = Source[index] - movingAverage.Result[index - Periods / 2 - 1];
    }
}

The code of the simple moving average also has to check that index >= Periods. Moreover, if we use a nested indicator (e.g., another type of moving average), this condition must be different to account for all new calculations.

While you can specify boundary conditions and explore fringe cases, C# provides an easy-to-use means of avoiding this problem entirely.

Specifically, the double data type fully supports NaN (Not a Number) arithmetics. We can demonstrate this using the example below.

1
double.NaN + 1.4 == double.NaN

In other words, when you perform calculations in which at least one variable is of the NaN type, the result of these calculations will always be NaN.

Fortunately for us, all custom cTrader indicators operate on data of the DataSeries type. When you request a value of an element of a DataSeries list with a negative index, you will always receive NaN, and the value itself will simply not be drawn on the trading chart.

The indicator will continue operating as normal without throwing an exception. This means that, in most cases, you can safely ignore boundary conditions and simply implement the general calculation rules.

Recursive Indicators

While NaN arithmetics simplify work with boundary conditions, there are still situations in which NaN values have to be handled explicitly. Recursive indicators constitute one such case.

When calculating a value, a recursive indicator bases its calculations on the value calculated for the previous period. Consider the following example of an exponential moving average calculation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public override void Calculate(int index)
{
    var previousValue = Result[index - 1];

    if (double.IsNaN(previousValue))
    {
        Result[index] = Source[index];
    }
    else
    {
        Result[index] = Source[index] * _exp + previousValue * (1 - _exp);
    }
}

If our indicator calculates a value for the first period, we have to set it explicitly to a non-NaN type. Otherwise, all our calculations for all future periods will result in NaN, and the indicator will be useless.

In the code above, this is achieved by using the double.IsNaN(previousValue) method. Please, note that we cannot use the == operator (previousValue == double.IsNan) due to the fact that a NaN == x expression will always return false even if x is NaN.


Last update: December 22, 2022