Edit this page

Custom Indicators

This article aims to demonstrate how to create indicators that use custom built-in formulas. The first indicator to be set up is Disparity Index indicator and second is a two-line Moving Average Envelopes indicator.

Disparity Index (DI)

Disparity Index (DI) indicator is described by its creator Steve Nison as "a percentage display of the latest close to a chosen moving average". A generalized formula of the DI can be defined as follows:

DI = ((CurrentClose – MA) / MA) * 100

The MA notation in the above formula stands for any Moving Average indicator. This example will use Exponential Moving Average, so the formula will be rather:

DI = ((CurrentClose - EMA) / EMA) * 100

The formula suggests that DI needs to use the calculations of the EMA indicator and the closing price value coming from the data source. To achieve the desired outcome we will need to (1) create a class that inherits EMA, and (2) override the GetProcessedValue method to return the modified value. The CurrentClose value in the formula can be drawn from the BaseValue property of the current indicator’s data point. All Moving Average indicators use data points of type IdicatorValueDataPoint. Specifically designed to work with indicators, these data points contain a field called BaseValue which is indeed the unprocessed value fetched from the data source.

Now that we have all variables from the formula above, let us start constructing our indicator. First, create a class DisparityIndexIndicator that inherits ExponantialMovingAverage and override the GetProcessedValue method. The method has a parameter currentIndex, which allows you to extract the reach the current data point and extract its BaseValue. To get the EMA calculated value, use the base GetProcessedValue method. The only step left is to calculate and return the modified value. Here is how your code should look like:

DI Indicator

public class DisparityIndexIndicator: ExponentialMovingAverageIndicator
{
    public override double GetProcessedValue(int currentIndex)
    {
        double close = (this.DataPoints[currentIndex] as IndicatorValueDataPoint).BaseValue;
        double ema = base.GetProcessedValue(currentIndex);
        double result = ((close - ema) / ema) * 100;
        return result;
    }
}

Public Class DisparityIndexIndicator
    Inherits ExponentialMovingAverageIndicator
    Public Overrides Function GetProcessedValue(currentIndex As Integer) As Double
        Dim close As Double = TryCast(Me.DataPoints(currentIndex), IndicatorValueDataPoint).BaseValue
        Dim ema As Double = MyBase.GetProcessedValue(currentIndex)
        Dim result As Double = ((close - ema) / ema) * 100
        Return result
    End Function
End Class

Now let’s create a new DI indicator instance and add it to our RadChartView. In order to do that, however, we will need some sample data. The snippet below creates a BindingList of ClosingPriceObjects. Each ClosingPriceObject is a simple structure that holds the closing price and date it was registered. The class implements INotifyPropertyChanged in order to make sure that any changes in the object's data will be reflected in the indicator’s values.

Figure 1: DI Indicator

chartview series types indicators custom indicators 001

Custom Object

public class ClosingPriceObject : INotifyPropertyChanged
{
    private double close;
    private DateTime date;
    public ClosingPriceObject(double close, DateTime date)
    {
        this.close = close;
        this.date = date;
    }
    public double Close
    {
        get
        {
            return this.close;
        }
        set
        {
            this.close = value;
            OnNotifyPropertyChanged("Close");
        }
    }
    public DateTime Date
    {
        get
        {
            return this.date;
        }
        set
        {
            this.date = value;
            OnNotifyPropertyChanged("Date");
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
    public void OnNotifyPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Public Class ClosingPriceObject
    Implements INotifyPropertyChanged
    Private m_close As Double
    Private m_date As DateTime
    Public Property Close() As Double
        Get
            Return Me.m_close
        End Get
        Set(value As Double)
            Me.m_close = value
            OnNotifyPropertyChanged("Close")
        End Set
    End Property
    Public Property [Date]() As DateTime
        Get
            Return Me.m_date
        End Get
        Set(value As DateTime)
            Me.m_date = value
            OnNotifyPropertyChanged("Date")
        End Set
    End Property
    Public Sub New(close As Double, [date] As DateTime)
        Me.Close = close
        Me.[Date] = [date]
    End Sub
    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
    Public Sub OnNotifyPropertyChanged(propertyName As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    End Sub
End Class

Create Data

BindingList<ClosingPriceObject> dataSource = new BindingList<ClosingPriceObject>();
dataSource.Add(new ClosingPriceObject(4, DateTime.Now));
dataSource.Add(new ClosingPriceObject(7, DateTime.Now.AddDays(1)));
dataSource.Add(new ClosingPriceObject(4, DateTime.Now.AddDays(2)));
dataSource.Add(new ClosingPriceObject(2, DateTime.Now.AddDays(3)));
dataSource.Add(new ClosingPriceObject(6, DateTime.Now.AddDays(4)));
dataSource.Add(new ClosingPriceObject(7, DateTime.Now.AddDays(5)));
dataSource.Add(new ClosingPriceObject(4, DateTime.Now.AddDays(6)));
dataSource.Add(new ClosingPriceObject(3, DateTime.Now.AddDays(7)));
dataSource.Add(new ClosingPriceObject(7, DateTime.Now.AddDays(8)));

Dim dataSource As New BindingList(Of ClosingPriceObject)()
dataSource.Add(New ClosingPriceObject(4, DateTime.Now))
dataSource.Add(New ClosingPriceObject(7, DateTime.Now.AddDays(1)))
dataSource.Add(New ClosingPriceObject(4, DateTime.Now.AddDays(2)))
dataSource.Add(New ClosingPriceObject(2, DateTime.Now.AddDays(3)))
dataSource.Add(New ClosingPriceObject(6, DateTime.Now.AddDays(4)))
dataSource.Add(New ClosingPriceObject(7, DateTime.Now.AddDays(5)))
dataSource.Add(New ClosingPriceObject(4, DateTime.Now.AddDays(6)))
dataSource.Add(New ClosingPriceObject(3, DateTime.Now.AddDays(7)))
dataSource.Add(New ClosingPriceObject(7, DateTime.Now.AddDays(8)))

{{endregion}}

SetupDIIndicator

{{source=..\SamplesCS\ChartView\Series\Indicators\CustomIndicatorsDIForm.cs region=SetupDIIndicator}}
{{source=..\SamplesVB\ChartView\Series\Indicators\CustomIndicatorsDIForm.vb region=SetupDIIndicator}}

DisparityIndexIndicator indicator = new DisparityIndexIndicator();
indicator.ValueMember = "Close";
indicator.CategoryMember = "Date";
indicator.DataSource = dataSource;
indicator.Period = 5;
indicator.BorderColor = Color.Red;
indicator.PointSize = SizeF.Empty;
this.radChartView1.Series.Add(indicator);

Dim indicator As New DisparityIndexIndicator
indicator.Period = 5
indicator.ValueMember = "Close"
indicator.CategoryMember = "Date"
indicator.DataSource = dataSource
indicator.BorderColor = Color.Red
indicator.PointSize = SizeF.Empty
Me.RadChartView1.Series.Add(indicator)

Moving Average Envelopes (MAE)

Moving Average Envelopes (MAE) is a slightly more complex indicator as it contains two bands, frequently referred to as Envelopes. The indicator uses Simple Moving Average as a starting point and shifts its two bands upwards and downwards to form the envelopes above and below the moving average. The percentage formula used to calculate the envelopes is:

UpperEnvelope = MA + (U% * MA)

LowerEnvelope = MA + (L% * MA)

Where U% is the Upper Percentage and L% is the lower percentage

All two-line indicators in RadChartView follow a specific pattern – the main indicator implements the IParentIndicator interface and the nested indicator implements the IChildIndicator interface. Once you have implemented these two interfaces, RadChartView will be in charge of attaching and rendering the two lines correctly.

Figure 2: Moving Average indicator

chartview series types indicators custom indicators 002

Because Moving Average Envelopes requires a property that sets the bands percent (assuming that both bands will use the same percent), we will create a MAE base that inherits the Moving Average indicator__and adds a __Percent property. Here is a sample snippet:

Base Class

public class MovingAverageEnvelopeBase : MovingAverageIndicator
{
    public static readonly RadProperty PercentProperty = RadProperty.Register("Percent", typeof(double), typeof(MovingAverageEnvelopeBase), new RadPropertyMetadata(0d));
    public double Percent
    {
        get
        {
            return (double)GetValue(PercentProperty);
        }
        set
        {
            SetValue(PercentProperty, value);
        }
    }
}

Public Class MovingAverageEnvelopeBase
    Inherits MovingAverageIndicator
    Public Shared ReadOnly PercentProperty As RadProperty = RadProperty.Register("Percent", GetType(Double), GetType(MovingAverageEnvelopeBase), New RadPropertyMetadata(0.0))
    Public Property Percent() As Double
        Get
            Return CDbl(GetValue(PercentProperty))
        End Get
        Set(value As Double)
            SetValue(PercentProperty, value)
        End Set
    End Property
End Class

Let’s now create two classes: MovingAverageEnvelopeChild, containing the lower band’s logic and MovingAverageEnvelopeIndicator, holding the upper band’s formula. They should both inherit MovingAverageEnvelopeBase class. Here are the steps that set up the MovingAverageEnvelopeChild class, first, make sure the MovingAverageEnvelopeChild class implements the IChildIndicator interface. Further, add a field that holds the parent indicator and use it when implementing the OwnerIndicator property. Also, override the GetProcessedValue method and use the Percent property to calculate the correct lower envelope value. Here is a sample snippet of the MovingAverageEnvelopeChild:

Child Class

public class MovingAverageEnvelopeChild : MovingAverageEnvelopeBase, IChildIndicator
{
    MovingAverageEnvelopeIndicator owner;
    public MovingAverageEnvelopeChild(MovingAverageEnvelopeIndicator owner)
    {
        this.owner = owner;
    }
    public IndicatorBase OwnerIndicator
    {
        get { return this.owner; }
    }
    public override double GetProcessedValue(int currentIndex)
    {
        double movingAverage = base.GetProcessedValue(currentIndex);
        double result = movingAverage - (movingAverage * this.Percent);
        return result;
    }
}

Public Class MovingAverageEnvelopeChild
    Inherits MovingAverageEnvelopeBase
    Implements IChildIndicator
    Private owner As MovingAverageEnvelopeIndicator
    Public Sub New(owner As MovingAverageEnvelopeIndicator)
        Me.owner = owner
    End Sub
    Public ReadOnly Property OwnerIndicator() As IndicatorBase Implements IChildIndicator.OwnerIndicator
        Get
            Return Me.owner
        End Get
    End Property
    Public Overrides Function GetProcessedValue(currentIndex As Integer) As Double
        Dim movingAverage As Double = MyBase.GetProcessedValue(currentIndex)
        Dim result As Double = movingAverage - (movingAverage * Me.Percent)
        Return result
    End Function
End Class

The MovingAverageEnvelopeIndicator class requires a bit more steps that the indicator child. First, make the class implement the IParentIndicator interface. Create a field of type MovingAverageEnvelopeChild, initialize it in the indicator’s constructor, and return it when implementing the ChildIndicator property. To make sure the inner indicator will be attached and detached, override both OnAttached and OnDettached methods and manually attach and detach the ChildIndicator. To ensure the inner indicator will be bound properly, override the OnNotifyPropertyChanged method and pass any important property values to the child indicator. Here is a sample snippet:

MAE Indicator

public class MovingAverageEnvelopeIndicator : MovingAverageEnvelopeBase, IParentIndicator
{
    MovingAverageEnvelopeChild childIndicator;
    public MovingAverageEnvelopeIndicator()
    {
        childIndicator = new MovingAverageEnvelopeChild(this);
    }
    public IndicatorBase ChildIndicator
    {
        get { return this.childIndicator; }
    }
    public override double GetProcessedValue(int currentIndex)
    {
        double movingAverage = base.GetProcessedValue(currentIndex);
        double result = movingAverage + (movingAverage * this.Percent);
        return result;
    }
    protected override void OnAttached(UIChartElement parent)
    {
        base.OnAttached(parent);
        this.ChildIndicator.Attach(parent);
    }
    protected override void OnDettached()
    {
        base.OnDettached();
        this.ChildIndicator.Dettach();
    }
    protected override void OnNotifyPropertyChanged(System.ComponentModel.PropertyChangedEventArgs e)
    {
        base.OnNotifyPropertyChanged(e);
        if (e.PropertyName == "DataSource")
        {
            this.childIndicator.DataSource = this.DataSource;
        }
        if (e.PropertyName == "CategoryMember")
        {
            this.childIndicator.CategoryMember = this.CategoryMember;
        }
        if (e.PropertyName == "ValueMember")
        {
            this.childIndicator.ValueMember = this.ValueMember;
        }
        if (e.PropertyName == "Period")
        {
            this.childIndicator.Period = this.Period;
        }
        if (e.PropertyName == "Percent")
        {
            this.childIndicator.Percent = this.Percent;
        }
    }
}

Public Class MovingAverageEnvelopeIndicator
    Inherits MovingAverageEnvelopeBase
    Implements IParentIndicator
    Private m_childIndicator As MovingAverageEnvelopeChild
    Public Sub New()
        m_childIndicator = New MovingAverageEnvelopeChild(Me)
    End Sub
    Public ReadOnly Property ChildIndicator() As IndicatorBase Implements IParentIndicator.ChildIndicator
        Get
            Return Me.m_childIndicator
        End Get
    End Property
    Public Overrides Function GetProcessedValue(currentIndex As Integer) As Double
        Dim movingAverage As Double = MyBase.GetProcessedValue(currentIndex)
        Dim result As Double = movingAverage + (movingAverage * Me.Percent)
        Return result
    End Function
    Protected Overrides Sub OnAttached(parent As UIChartElement)
        MyBase.OnAttached(parent)
        Me.ChildIndicator.Attach(parent)
    End Sub
    Protected Overrides Sub OnDettached()
        MyBase.OnDettached()
        Me.ChildIndicator.Dettach()
    End Sub
    Protected Overrides Sub OnNotifyPropertyChanged(e As System.ComponentModel.PropertyChangedEventArgs)
        MyBase.OnNotifyPropertyChanged(e)
        If e.PropertyName = "DataSource" Then
            Me.m_childIndicator.DataSource = Me.DataSource
        End If
        If e.PropertyName = "CategoryMember" Then
            Me.m_childIndicator.CategoryMember = Me.CategoryMember
        End If
        If e.PropertyName = "ValueMember" Then
            Me.m_childIndicator.ValueMember = Me.ValueMember
        End If
        If e.PropertyName = "Period" Then
            Me.m_childIndicator.Period = Me.Period
        End If
        If e.PropertyName = "Percent" Then
            Me.m_childIndicator.Percent = Me.Percent
        End If
    End Sub
End Class

Now that we have the MovingAverageEnvelopeIndicator ready, let us set up some sample data and see how it looks like. The following snippet uses BindingList of custom OhlcObjects. For the sake of presentation, this example adds OhlcSeries and a simple Moving Average indicator next to the MovingAverageEnvelopeIndicator.

Create and Setup Indicator

BindingList<OhlcObject> dataSource = new BindingList<OhlcObject>();
dataSource.Add(new OhlcObject(17, 18, 12, 14, DateTime.Now));
dataSource.Add(new OhlcObject(16, 17, 11, 17, DateTime.Now.AddDays(1)));
dataSource.Add(new OhlcObject(18, 19, 12, 14, DateTime.Now.AddDays(2)));
dataSource.Add(new OhlcObject(15, 15, 12, 12, DateTime.Now.AddDays(3)));
dataSource.Add(new OhlcObject(15, 18, 15, 16, DateTime.Now.AddDays(4)));
dataSource.Add(new OhlcObject(15, 17, 11, 17, DateTime.Now.AddDays(5)));
dataSource.Add(new OhlcObject(12, 15, 12, 14, DateTime.Now.AddDays(6)));
dataSource.Add(new OhlcObject(15, 15, 12, 13, DateTime.Now.AddDays(7)));
dataSource.Add(new OhlcObject(15, 18, 15, 17, DateTime.Now.AddDays(8)));
dataSource.Add(new OhlcObject(15, 17, 11, 17, DateTime.Now.AddDays(9)));
dataSource.Add(new OhlcObject(12, 15, 12, 14, DateTime.Now.AddDays(10)));
dataSource.Add(new OhlcObject(17, 18, 12, 14, DateTime.Now.AddDays(11)));
dataSource.Add(new OhlcObject(15, 18, 15, 17, DateTime.Now.AddDays(12)));
dataSource.Add(new OhlcObject(15, 18, 15, 16, DateTime.Now.AddDays(13)));
dataSource.Add(new OhlcObject(17, 18, 12, 14, DateTime.Now.AddDays(14)));
MovingAverageIndicator maIndicator = new MovingAverageIndicator();
maIndicator.ValueMember = "Close";
maIndicator.CategoryMember = "Date";
maIndicator.DataSource = dataSource;
maIndicator.Period = 5;
maIndicator.BorderColor = Color.Red;
maIndicator.PointSize = SizeF.Empty;
this.radChartView1.Series.Add(maIndicator);
MovingAverageEnvelopeIndicator envelopeIndicator = new MovingAverageEnvelopeIndicator();
envelopeIndicator.ValueMember = "Close";
envelopeIndicator.CategoryMember = "Date";
envelopeIndicator.DataSource = dataSource;
envelopeIndicator.Period = 5;
envelopeIndicator.BorderColor = Color.Green;
envelopeIndicator.ChildIndicator.BorderColor = Color.Black;
envelopeIndicator.PointSize = SizeF.Empty;
envelopeIndicator.Percent = 0.25;
this.radChartView1.Series.Add(envelopeIndicator);
CandlestickSeries series = new CandlestickSeries();
series.OpenValueMember = "Open";
series.CloseValueMember = "Close";
series.HighValueMember = "High";
series.LowValueMember = "Low";
series.CategoryMember = "Date";
series.DataSource = dataSource;
this.radChartView1.Series.Add(series);
this.radChartView1.Axes[0].LabelFormat = "{0:dd}";
(this.radChartView1.Axes[1] as LinearAxis).Minimum = 5;

Dim dataSource As New BindingList(Of OhlcObject)()
dataSource.Add(New OhlcObject(17, 18, 12, 14, DateTime.Now))
dataSource.Add(New OhlcObject(16, 17, 11, 17, DateTime.Now.AddDays(1)))
dataSource.Add(New OhlcObject(18, 19, 12, 14, DateTime.Now.AddDays(2)))
dataSource.Add(New OhlcObject(15, 15, 12, 12, DateTime.Now.AddDays(3)))
dataSource.Add(New OhlcObject(15, 18, 15, 16, DateTime.Now.AddDays(4)))
dataSource.Add(New OhlcObject(15, 17, 11, 17, DateTime.Now.AddDays(5)))
dataSource.Add(New OhlcObject(12, 15, 12, 14, DateTime.Now.AddDays(6)))
dataSource.Add(New OhlcObject(15, 15, 12, 13, DateTime.Now.AddDays(7)))
dataSource.Add(New OhlcObject(15, 18, 15, 17, DateTime.Now.AddDays(8)))
dataSource.Add(New OhlcObject(15, 17, 11, 17, DateTime.Now.AddDays(9)))
dataSource.Add(New OhlcObject(12, 15, 12, 14, DateTime.Now.AddDays(10)))
dataSource.Add(New OhlcObject(17, 18, 12, 14, DateTime.Now.AddDays(11)))
dataSource.Add(New OhlcObject(15, 18, 15, 17, DateTime.Now.AddDays(12)))
dataSource.Add(New OhlcObject(15, 18, 15, 16, DateTime.Now.AddDays(13)))
dataSource.Add(New OhlcObject(17, 18, 12, 14, DateTime.Now.AddDays(14)))
Dim maIndicator As New MovingAverageIndicator()
maIndicator.ValueMember = "Close"
maIndicator.CategoryMember = "Date"
maIndicator.DataSource = dataSource
maIndicator.Period = 5
maIndicator.BorderColor = Color.Red
maIndicator.PointSize = SizeF.Empty
Me.RadChartView1.Series.Add(maIndicator)
Dim envelopeIndicator As New MovingAverageEnvelopeIndicator()
envelopeIndicator.ValueMember = "Close"
envelopeIndicator.CategoryMember = "Date"
envelopeIndicator.DataSource = dataSource
envelopeIndicator.Period = 5
envelopeIndicator.BorderColor = Color.Green
envelopeIndicator.ChildIndicator.BorderColor = Color.Black
envelopeIndicator.PointSize = SizeF.Empty
envelopeIndicator.Percent = 0.25
Me.RadChartView1.Series.Add(envelopeIndicator)
Dim series As New CandlestickSeries()
series.OpenValueMember = "Open"
series.CloseValueMember = "Close"
series.HighValueMember = "High"
series.LowValueMember = "Low"
series.CategoryMember = "Date"
series.DataSource = dataSource
Me.RadChartView1.Series.Add(series)
Me.RadChartView1.Axes(0).LabelFormat = "{0:dd}"
TryCast(Me.RadChartView1.Axes(1), LinearAxis).Minimum = 5

See Also