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.
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.
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.
Multi-Color Hack!
If your indicator requires a specified (fixed) multiple color scheme, instead of recreating the SetSeriesBarColor
logic in every strategy, you can assign those colors during Populate()
as shown below. The trick is to initialize the SeriesBarColors
list and later assign the colors according to your color logic. The DefaultColor
(or the one you assign during a drag & drop) will always be used as the color for the indicator label, but it's not required to be used otherwise.
public override void Populate()
{
BarHistory bars = Parameters[0].AsBarHistory;
Int32 length = Parameters[1].AsInt;
Int32 length2 = Parameters[2].AsInt;
DateTimes = bars.DateTimes;
//create the indicator (here, a simple oscillator)
TimeSeries myIndValues = SMA.Series(bars.Close, length) - SMA.Series(bars.Close, length2);
this.SeriesBarColors = new DateSynchedList<WLColor>(DateTimes, DefaultColor);
//assign the indicator values and colors based on some conditions
for (int n = 0; n < bars.Count; n++)
{
Values[n] = myIndValues[n];
if (myIndValues[n] > 0)
this.SeriesBarColors[n] = myIndValues[n] > myIndValues[n - 1] ? DefaultColor : WLColor.Blue;
else if (myIndValues[n] < 0)
this.SeriesBarColors[n] = myIndValues[n] < myIndValues[n - 1] ? WLColor.Red : WLColor.Yellow;
}
}
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);
}
}
}