Clone our Wealth-Lab 8 Extension Demo project on GitHub to get a head start in developing your own Extensions!

Indicator Library API

This document details the API for building Indicator Library extensions for Wealth-Lab 8. An Indicator Library exposes one or more Indicators, which appear in their own node in the WL8 indicator tree.

Wealth-Lab 7 Indicator Tree

Build Environment

You can create an Indicator in a .NET development tool such as Visual Studio 2022. Create a class library project that targets .NET8, then reference the WealthLab.Core library DLL that you'll find in the WL8 installation folder.

Note: If you are using Visual Studio 2022, it will need to be updated to at least version 17.8.6 to use .NET8.

Your Indicator will be a class in this library that descends from IndicatorBase, which is defined in the WealthLab.Core library, in the WealthLab.Indicators namespace. After you implement and build your library, simply copy the resulting assembly DLL into the WL8 installation folder. The next time WL8 starts up, it will discover your Indicator, making it available in appropriate locations of the WL8 user interface.

Visual Studio 2022 Build Environment

Accessing the Host (WL8) Environment

The IHost interface allows your extension to access information about the user's WealthLab environment. For example, the location of the user data folder, or obtaining a list of DataSets defined by the user. At any point in your extension's code base, you can access an instance of the IHost interface using the singleton class WLHost and its Instance property. Example:

//get user data folder
string folder = WLHost.Instance.DataFolder;

IndicatorBase Class

Each Indicator in your Library should be implemented as a class that descends from IndicatorBase, which is defined in the WealthLab.Indicators namespace. Here is the hierarchy of IndicatorBase's ancestor classes:

  • DateSynchedList<double>
  • TimeSeriesBase
  • TimeSeries
  • IndicatorBase

You can click on the classes above to refer to their API Class Reference documents. Below we will go over important properties and methods of IndicatorBase to consider as you develop an Indicator Library.

Constructors

Each Indicator should have at least two constructors. The first should be a parameterless constructor. WL8 uses this constructor to create lightweight instances of each Indicator to establish its Indicator roster.

The second constructor should contain parameters that match the Parameter instances that you create in the GenerateParameters method (see below). The body of this constructor should assign the constructor parameter values to the Parameter instance values.

Be sure to call the base constructor from each constructor you define. Here is an example of the SMA Indicator constructors.

        //parameterless constructor
        public SMA() : base()
        {
        }

        //for code based construction
        public SMA(TimeSeries source, int period)
            : base()
        {
            Parameters[0].Value = source;
            Parameters[1].Value = period;
            Populate();
        }

Working with Parameters

Parameters Property

public ParameterList Parameters

As mentioned above, Indicators have parameters that are expressed as instances of the Parameter class. The Parameters property is a ParameterList instance that contains these Parameter instances.

GenerateParameters

protected virtual void GenerateParameters()

Override this method to create Parameter instances and add them to the Parameters property. You can use the helper methods below to add Parameter instances to your Indicator.

Here is an example of the GenerateParameters method for the SMA Indicator.

        //generate parameters
        protected override void GenerateParameters()
        {
            AddParameter("Source", ParameterType.TimeSeries, PriceComponents.Close);
            AddParameter("Period", ParameterType.Int32, 20);
        }

AddParameter

protected Parameter AddParameter(string name, ParameterTypes type, object value)

Creates a Parameter instances based on the method's parameters and adds it to the Parameters property.

AddIndicatorParameter

protected Parameter AddIndicatorParameter(string name)

This helper method creates a Parameter instance with a Type of ParameterType.Indicator, using the Indicator with the specified name as a default value.

Here is an example of implementation of a custom indicator which accepts an arbitrary indicator as input:

        //generate parameters
        protected override void GenerateParameters()
        {
            AddParameter("Bars", ParameterType.BarHistory, null);
            AddIndicatorParameter("Indicator", "ROC");        
        }
		
        //populate
        public override void Populate()
        {
            BarHistory source = Parameters[0].AsBarHistory;
            DateTimes = source.DateTimes;

            //indicator parameter
            Parameter p = Parameters[1];
            //get indicator's abbreviation
            string indName = p.IndicatorAbbreviation;
            //obtain its parameters
            ParameterList indParams = p.IndicatorParameters;
            //instantiate it:
            IndicatorBase ind = IndicatorFactory.Instance.CreateIndicator(indName, indParams, source);
            ...
        }

AddSmootherParameter

protected Parameter AddSmootherParameter(string name, string defaultValue)

This helper method allows you to expose a list of smoother Indicators to the user as one of your Indicator's parameters. It creates a Parameter instance with a Type of ParameterType.StringChoice, with Choices that contain all of the smoother Indicators available in WealthLab.

Descriptive Properties

Override the following properties that provide descriptive details about your Indicator.

Name

public abstract string Name

Return a descriptive name for your Indicator, such as "Simple Moving Average".

Abbreviation

public abstract string Abbreviation

Return an abbreviation for the indicator, which should typically correspond to its class type name. For example, "SMA".

HelpDescription

public abstract string HelpDescription

Return a short description of the Indicator. This appears below the Indicator tree when the user click the Indicator in the tree.

Tooltip

public string Tooltip 

You can optionally assign a value to Tooltip, which will be displayed when the user hovers their mouse over the Indicator value label when it is plotted on the chart.

HelpURL

public virtual string HelpURL

Optionally return a URL that links to a web site that describes your Indicator in more detail.

IsOscillator

public bool IsOscillator

This property will return true if your Indicator assigned non-NaN values to the OverboughtLevel and OversoldLevel properties.

OverboughtLevel and OversoldLevel

public double OversoldLevel
public double OverboughtLevel

If you assign non-NaN values to these properties, WL8 will consider your Indicator to be an oscillator. This allows it to be filtered in the Indicator Tree, and has other impacts too in various WL8 extensions.

IsSmoother

public virtual bool IsSmoother

If you override this property to return true, WL8 will consider your Indicator to be a smoother. This allows it to be filtered in the Indicator Tree, and makes it available as a smoother candidate in the AddSmootherParameter and GetSmoothedIndicator methods. For an Indicator to function correctly as a smoother it should have two parameters, the first being a TimeSeries source, and the second being an int period.

LibraryName

public string LibraryName

By default, WL8 assigns a LibraryName for your Indicators based on the assembly name of your library. It uses the LibraryName as the header text in the Indicator tree node that contains your Indicators. If you want to use a different name, assign a value to this property in your Indicator constructor.

Color and Plot Style

PaneTag

public override string PaneTag

Override this property to return a string that is a key to the chart pane where your Indicator should render. You can return "Price" to specify the Price Pane (or if the Indicator should render on whatever pane its target is plotted in), "Volume" to specify the Volume Pane, or some other string (such as the Indicator's Abbreviation) to indicate that it should be plotted in its own pane.

DefaultColor

public virtual Color DefaultColor

Override this property to return the default color that your Indicator should be plotted in.

DefaultPlotStyle

public virtual PlotStyles DefaultPlotStyle

Override this property to return the default plot style that your Indicator should be plotted with. The PlotStyles enum contains the following options:

  • Line
  • Histogram
  • Dots
  • ThickLine
  • ThickHistogram
  • DottedLine
  • DashedLine
  • BooleanDots - Render dots above or below the chart bars for any Indicator value greater than zero
  • Bands - Renders an optionally filled band, requires BandCompanionAbbreviation to be defined in the Indicator (see below)
  • ZigZag - Expects the Indicator to contain sparse data points interspersed with Double.Nan
  • Blocks - Mainly used for Historical Event Data such as fundamental information
  • GradientBlocks - Mainly used for Historical Event Data such as fundamental information
  • BarHistory - WealthLab uses this style internally when a secondary BarHistory instance is plotted
  • BarChart - Used to plot an Indicator as a "bar chart" with open, high, low, and close values - requires GetBarChartCompanion to be implemented in the Indicator (see below)
  • HistogramTwoColor - Colors the histogram bars different colors based on whether the price bar was up or down

DefaultPlotName

public virtual string DefaultPlotName

WL8 calls this method to determine which Plot Style to use to render the Indicator. The return value is expected to be the name of a class derived from SeriesStyleBase (see the Plot Style Extension document for more information.)

The default implementation returns the corresponding value for the DefaultPlotStyle property. If you want your Indicator to use a custom Plot Style that isn't represented in this enumerated type, you can override DefaultPlotName and provide the class name to use.

GetChartCompanion

public virtual IndicatorBase GetBarChartCompanion(PriceComponents pc)

If you use the BarChart plot style for your Indicator, it must override this method to return TimeSeries instances that represents the Open, High, Low, and Close data to be plotted. The PriceComponents parameter (pc) contains which TimeSeries to return.

UseZeroOrigin

public virtual bool UseZeroOrigin

If you override this property to return true, it will always be plotted with the y-axis of its pane anchored at zero.

Populating an Indicator with Values

Populate

public abstract void Populate()

Override the Populate method to calculate the Indicator values and populate its underlying time series. The first thing you should do here is obtain the Values of each of your Indicator's Parameters and store them in local variables for use later in the method. All Indicators require one of the Parameters, typically the first one, to be a TimeSeries or a BarHistory instances typically named source.

Since your Indicator is ultimately derived from TimeSeries, it has a DateTimes and a Values property. You should assign the DateTimes property to the value of the source instance's DateTimes property. Performing this assignment populates your Indicator's Values property with a number of Double.NaN values equal to the number of DateTime values in the DateTimes property. Now that the Values property is synchronized, you can calculate and assign values to it using the standard indexer, for example:

DateTimes = source.DateTimes;
for(int n = 0; n < source.Count; n++)
{
	double val = (source.High[n] + source.Low[n]) / 2.0;
	Values[n] = val;
}

The example above calculated each Indicator value in a loop, assigning the Values one by one. You can also take advantage of TimeSeries math to calculate and assign the Values in an aggregate fashion. For example, the above code could alternately be implemented as follows:

DateTimes = source.DateTimes;
TimeSeries avg = (source.High + source.Low) / 2.0;
Values = avg.Values;

GetSmoothedIndicator

protected IndicatorBase GetSmoothedIndicator(string name, TimeSeries source, int period)

This helper method creates a smoothed version of the specified source TimeSeries, with the specified period, using the smoothing indicator specified in the name parameter.

Static Series Method

You should create a static Series method for your indicator that creates and returns an instance based on the supplied parameters, leveraging the Cache property of the TimeSeriesBase class, which is the ancestor of both TimeSeries and BarHistory Indicator sources. The Series convention makes it easier for users writing C# coded models to create and access instances of your Indicator in a familiar way. Here is the example of the SMA Series method:

        //static method
        public static SMA Series(TimeSeries source, int period)
        {
            string key = CacheKey("SMA", period);
            if (source.Cache.ContainsKey(key))
                return (SMA)source.Cache[key];
            SMA sma = new SMA(source, period);
            source.Cache[key] = sma;
            return sma;
        }

Note that the Series method first uses the method CacheKey which creates a string key based on the Indicator Name and its parameter values (excluding source). It then checks the Cache of the source instance to see if it contains an object with the same key. If so, it casts the object to the Indicator class and returns the instance. If the Cache does not contain such a keyed object, the Series method constructs a new instance, inserts it into the source Cache and returns the instance.

CacheKey

public static string CacheKey(params object[] arguments)

Creates a string key which is a delimited concatenation of the specified parameters.

Static Value Method

You can optionally include a static Value method to calculate and return the value of your indicator at a specific index into its source data series. You pass the index (idx) as the first parameter, the source TimeSeries or BarHistory as the second parameter, and any other parameters your Indicator needs following. See the full example code at the end for the implementation of the Value method for the SMA Indicator.

Band Indicators

BandCompanionAbbreviation

public virtual string BandCompanionAbbreviation

If your Indicator is part of a pair of bands (such as Bollinger Bands or Keltner Bands) you can return PlotStyles.Bands as its DefaultPlotStyle. In this case, override this property to return the Abbreviation of your Indicator's band companion. For example, the BBandUpper Indicator overrides this property to return "BBandLower" and vice versa.

public virtual IndicatorBase BandCompanion

BandCompanion

WL8 calls this method when it needs to construct the instance of your Indicator's band companion for plotting purposes. The default implementation creates the instance based on the BandCompanionAbbreviation and the values of your Indicator's Parameters. You can override this logic if you need something more specific to create the band companion Indicator.

Indicator Companions

Companions

public virtual List<string> Companions

If it makes sense to plot one or more other (companion) Indicators when your Indicator is plotted on the chart, override this property to return a List<string> containing the Abbreviations of the companion Indicators. When a user drops your Indicator onto the chart, they will have the option of automatically plotting the companions as well.

Complete Example - SMA (Simple Moving Average)

using WealthLab.Core;

namespace WealthLab.Indicators
{
    public class SMA : IndicatorBase
    {
        //parameterless constructor
        public SMA() : base()
        {
        }

        //for code based construction
        public SMA(TimeSeries source, int period)
            : base()
        {
            Parameters[0].Value = source;
            Parameters[1].Value = period;
            Populate();
        }

        //static method
        public static SMA Series(TimeSeries source, int period)
        {
            string key = CacheKey("SMA", period);
            if (source.Cache.ContainsKey(key))
                return (SMA)source.Cache[key];
            SMA sma = new SMA(source, period);
            source.Cache[key] = sma;
            return sma;
        }

        //name
        public override string Name => "Simple Moving Average";

        //abbreviation
        public override string Abbreviation => "SMA";

        //description
        public override string HelpDescription => "Simple average of a range of values.";

        //price pane
        public override string PaneTag => "Price";

        //it's a smoother
        public override bool IsSmoother => true;

        //populate
        public override void Populate()
        {
            TimeSeries source = Parameters[0].AsTimeSeries;
            Int32 period = Parameters[1].AsInt;
            DateTimes = source.DateTimes;
            if (period <= 0)
                return;
            for (int n = period - 1; n < source.Count; n++)
                Values[n] = SMA.Value(n, source, period);
        }

        //calculate an ad-hoc SMA at a specific point
        public static double Value(int idx, TimeSeries source, int period)
        {
            if (period <= 0 || idx >= source.Count || (idx - period + 1) < 0)
                return Double.NaN;
            double sum = 0;
            for (int n = 0; n < period; n++)
            {
                sum += source[idx - n];
            }
            return sum / period;
        }

        //calculate partial value
        public override bool CalculatePartialValue()
        {
            StreamingValue = Double.NaN;
            TimeSeries source = Parameters[0].AsTimeSeries;
            if (Double.IsNaN(source.StreamingValue))
                return false;
            Int32 period = Parameters[1].AsInt;
            if (period >= source.Count)
                return false;
            double sum = 0;
            for (int n = 0; n < period - 1; n++)
            {
                var i = source.Count - 1 - n;
                sum += source[i];
            }
            sum += source.StreamingValue;
            StreamingValue = sum / period;
            return true;
        }

        //generate parameters
        protected override void GenerateParameters()
        {
            AddParameter("Source", ParameterType.TimeSeries, PriceComponent.Close);
            AddParameter("Period", ParameterType.Int32, 20);
        }
    }
}