Indicator
This guide explores how to leverage QXFinLib, a powerful library, to create your own technical indicators for use in quantitative analysis and algorithmic trading.
A technical indicators is a mathematical calculation usually derived from a stock’s historical price and volume. They are used to understand stock’s price movements in hopes of predicting future swings. Calculating technical indicators takes time away from the modelling process and can therefore be a deterrent to building more complex statistical models. With QXFinLib library quants can leverage a powerful framework which offers you to build customized technical indicators and mechanical trading system model. The library contains prebuilt 100+ indicator developed using same simple way you may use to develop your indicator as per your need.
The library contains prebuilt 100+ indicator developed using same simple way you may use to develop your indicator as per your need.
In QXFinLib technical indicator is function of TimeDataSeries which you have studied in previous sections.
Building Your Own Indicator with QXFinLib:
QXFinLib streamlines the creation of custom technical indicators by providing a powerful framework. Here's a step-by-step approach:
Create a class that inherits from the IndicatorBase class provided by QXFinLib. This class will serve as the foundation for your custom indicator.
Your Indicator must represented by your custom Indicator class that inherits from IndicatorBase class provided by QXFinLib.
Allow dynamic adjustments by defining input parameters within your class. These parameters influence the indicator's behavior. You can designate internal primitive data type variable as input variables by decorating them with the Parameter attribute.
For example, the Simple Moving Average (SMA) indicator requires a lookback period to calculate the average value. You can designate internal values as input variables by decorating them with the Parameter attribute.
[Parameter("LengthPeriod", Description = "The length of the SMA", DefaultValue = 12)]
private int _lengthPeriod = 12;
Implement override method CalculateOnBar:
The CalculateOnBar method is responsible for calculating the values for each output variable based on the current bar index.
Override the CalculateOnBar method, which is called for every historical bar in the data series. It is essential to store the indicator values, typically numeric, in a DoubleSeries for each index of the TimeDataSeries
The CalculateOnBar passes the current index starting from 0 index till end of bar index or as new bar completed. The function has access of TimeDataSeries and all its BarData till given current index. The function should not try to access any bar value beyond current index.
The result series can be exposed to outside as Indicator output value by decorating the variable of type DoubleSeries with Output Attribute
[Output("SMA", DefaultValue = double.NaN, LineColor = "DarkOrange")]
private DoubleSeries _smaPriceDoubleValueSeries = new DoubleSeries();
| Other Optional Methods in Indicator Class |
|---|
| void Initialize() This method is called once even before any call to CalculateOnBar and developer can initialize any state of the Strategy |
| override void ResetIndicator() This method is called by framework in case Strategy imput variables are changed and all Indicatorvalue need to be reevalaued. Here developes can reset the Output collection or any state of Indicator |
By following these steps, you can create custom indicators tailored to your specific trading strategies and analysis needs, leveraging the flexibility of QXFinLib. This approach empowers you to move beyond pre-built indicators and develop unique custom indicator that cater to your individual requirements.
Following code snippet shows an implementation of Simple Moving Average(SMA) Indicator:
using System;
using QX.FinLib.Data;
namespace QX.Indicators
{
[IndicatorScaleAttribute(true)]
[IndicatorAttribute("{E5889445-BA22-43A0-ADA6-FD463771EA26}", "SMA", "Simple Moving Average", "QXT")]
public class SMA : IndicatorBase
{
[Parameter("LengthPeriod", Description = "The length of the SMA", DefaultValue = 12)]
private int _lengthPeriod = 12;
[Output("SMA", DefaultValue = double.NaN, LineColor = "DarkOrange")]
private DoubleSeries _smaPriceDoubleValueSeries = new DoubleSeries();
public SMA() : base(null)
{
}
public SMA(TimeDataSeries timeDataSeries, int lengthPeriod)
: base(timeDataSeries)
{
if (lengthPeriod <= 0)
throw new ArgumentException("Invalid SMA length");
_lengthPeriod = lengthPeriod;
_smaPriceDoubleValueSeries = new DoubleSeries();
}
public SMA(TimeDataSeries timeDataSeries)
: base(timeDataSeries)
{
_smaPriceDoubleValueSeries = new DoubleSeries();
}
public override IndicatorCategoryType GetIndicatorCategoryType()
{
return IndicatorCategoryType.Trend;
}
protected override void Initialize()
{
}
// <summary>
// Calculates the Indicator Function value at the index.
// </summary>
// <param name="index">The index at which Indicator Function value need to be calculated.</param>
// <returns>
// The calculated Indicator function value at the index.
// </returns>
protected override double CalculateOnBar(int index)
{
if (index < _lengthPeriod)
return _smaPriceDoubleValueSeries[index] = double.NaN;
else
{
return _smaPriceDoubleValueSeries[index] = CloseSeries.Average(_lengthPeriod);
}
}
protected override void ResetIndicator()
{
_smaPriceDoubleValueSeries = new DoubleSeries();
}
}
}
Following are other important points for your indicator class:
- The IndicatorAttribute decorator must be defined at class level indicating unique Guid identifier, the short name, the description and the owner of the indicator. The unique Guid can be created through a GuidGen tool.
[IndicatorAttribute("{E5889445-BA22-43A0-ADA6-FD463771EA26}", "SMA", "Simple Moving Average", "QXT")]
- The variable decorated as Parameter is used to provide a control to end user to change the value as Input parameter. The framework reset the value with the user provided value of any input parameters which state is used to refer during a indicator value. In case of SMA, user wishes to make a lookup length period as an Input parameter.
[Parameter("LengthPeriod", Description = "The length of the SMA", DefaultValue = 12)]
private int _lengthPeriod = 12;
- Define an output variable of type DoubleSeries. The calculateOnBar must fill a value of calculated indicator value at given index. The SMA indicator has only output like SMA, you need not to define an output variable as Calculate method return type is preserve as Output variable.
[Output("SMA", DefaultValue = double.NaN, LineColor = "DarkOrange")]`
private DoubleSeries _smaPriceDoubleValueSeries = new DoubleSeries();
-
You may specify Double.NaN value for indicator index that has no valid value and may be used in charting to show a clear value.
-
Initialize method is called once and can be used to initialize some variables or any embedded indicator object.
-
ResetIndicator method is called each time when recalculation is required due to change in input parameter value.
How to implement Indicator having more than one output?
Following is Bollinger band indicator class defines three output variable:
using System;
using QX.FinLib.Common;
namespace QX.Indicators
{
[IndicatorScaleAttribute(true)]
[IndicatorAttribute("{08BFD754-A2C5-432A-9265-8A29DA132637}", "BB", "Bollinger Band", "QXT"]
public class BollingerBand : IndicatorBase
{
[Parameter("LengthPeriod", Description = "The length of the BB", DefaultValue = 20)]
private int _lengthPeriod = 20;
[Output("BB", DefaultValue = double.NaN, LineColor = "DarkOrange")]
private DoubleSeries _bbsDoubleValueSeries = new DoubleSeries();
[Output("UpperBand", DefaultValue = double.NaN, LineColor = "DeepSkyBlue", LineStyle = LineDashStyle.DashDot, IsAreaFill=true)]
private DoubleSeries _bbsUpperBandDoubleValueSeries = new DoubleSeries();
[Output("LowerBand", DefaultValue = double.NaN, LineColor = "Tomato", LineStyle = LineDashStyle.DashDot, IsAreaFill= true)]
private DoubleSeries _bbsLowerBandDoubleValueSeries = new DoubleSeries();
public BollingerBand() : base(null)
{
}
public BollingerBand(TimeDataSeries timeDataSeries, int lengthPeriod)
: base(timeDataSeries)
{
if (lengthPeriod <= 0)
throw new ArgumentException("Invalid BB length");
_lengthPeriod = lengthPeriod;
}
public BollingerBand(TimeDataSeries timeDataSeries)
: base(timeDataSeries)
{
}
public override IndicatorCategoryType GetIndicatorCategoryType()
{
return IndicatorCategoryType.Trend;
}
// <summary>
// Calculates the Indicator Function value at the index.
// </summary>
// <param name="index">The index at which Indicator Function value need to be calculated.</param>
// <returns>
// The calculated Indicator function value at the index.
// </returns>
protected override double CalculateOnBar(int index)
{
//Middle Band = Lenght Period simple moving average (SMA)
//Upper Band = Length Period SMA + (Length Period standard deviation of price x 2)
//Lower Band = Length Period SMA – (Length Period day standard deviation of price x 2)
if (index<_lengthPeriod)
{
_bbsDoubleValueSeries[index] = double.NaN;
_bbsUpperBandDoubleValueSeries[index] = double.NaN;
_bbsLowerBandDoubleValueSeries[index] = double.NaN;
}
else if (index >= _lengthPeriod)
{
double stdDev = TimeDataSeries.StdDev('C', _lengthPeriod, index)*2.0;
_bbsDoubleValueSeries[index]= TimeDataSeries.Average('C', _lengthPeriod, index);
_bbsUpperBandDoubleValueSeries[index] = _bbsDoubleValueSeries[index] + (stdDev);
_bbsLowerBandDoubleValueSeries[index] = _bbsDoubleValueSeries[index] - (stdDev);
}
return _bbsDoubleValueSeries[index];
}
protected override void ResetIndicator()
{
_bbsLowerBandDoubleValueSeries = new DoubleSeries();
_bbsUpperBandDoubleValueSeries = new DoubleSeries();
_bbsDoubleValueSeries = new DoubleSeries();
}
}
}
Example of using predefined indicator definition in any new Indicator implementation?
Following indicator code is implementation of WilderSMA using SMA predefined indicator:
[IndicatorAttribute("{EE7A557B-BE4B-4B51-9E06-CA41E9CE319C}", "WSMA", "Wilders Simple moving avearge", "QXT")]
public class WilderSMA : IndicatorBase
{
[Parameter("LengthPeriod", Description = "The length of the WSMA", DefaultValue = 12)]
private int _lengthPeriod = 12;
[Output("WSMA", DefaultValue = double.NaN, LineColor = "DarkOrange")]
private DoubleSeries _wsmaDoubleValueSeries = new DoubleSeries();
private DoubleSeries _sumDoubleValueSeries = new DoubleSeries();
private IndicatorBase _sma;
// <summary>
// Initializes a new instance of the <see cref="WilderSMA"/> class.
// </summary>
// <param name="timeDataSeries">The TimeDataSeries.</param>
// <param name="lengthPeriod">The length period.</param>
public WilderSMA(TimeDataSeries timeDataSeries, int lengthPeriod)
: base(timeDataSeries)
{
if (lengthPeriod <= 0)
{
throw new ArgumentException("Invalid EMA length");
}
_lengthPeriod = lengthPeriod;
}
public WilderSMA(TimeDataSeries timeDataSeries)
: base(timeDataSeries)
{
_wsmaDoubleValueSeries = new DoubleSeries();
}
public WilderSMA()
: base(null)
{
}
public override IndicatorCategoryType GetIndicatorCategoryType()
{
return IndicatorCategoryType.General;
}
protected override void Initialize()
{
_sma = GetIndicator("SMA");
_wsmaDoubleValueSeries = new DoubleSeries();
}
// <summary>
// Calculates the Indicator Function value at the index.
// </summary>
// <param name="index">The index at which Indicator Function value need to be calculated.</param>
// <returns>
// The calculated Indicator function value at the index.
// </returns>
protected override double CalculateOnBar(int index)
{
double previousWSMAValue = _wsmaDoubleValueSeries[index - 1];
if (double.IsNaN(previousWSMAValue))
{
_wsmaDoubleValueSeries[index] = _sma[index];
_sumDoubleValueSeries[index] = _sma[index] * _lengthPeriod;
}
else
{
_sumDoubleValueSeries[index] = _sumDoubleValueSeries[index - 1] - previousWSMAValue + InputSeries[index];
_wsmaDoubleValueSeries[index] = _sumDoubleValueSeries[index] / _lengthPeriod;
}
return _wsmaDoubleValueSeries[index];
}
protected override void ResetIndicator()
{
_sma = new SMA(this.TimeDataSeries, _lengthPeriod);
_wsmaDoubleValueSeries = new DoubleSeries();
}
}
Registering Indicator and how to use it in your application?
Indicator class can be used independently in your application but framework provides some systematic usage of it and before this it has to be registered. Following code snippet is used to register an indicator and retrieve an instance of it from system.
Type indicatorType = typeof(BollingerBand);
IndicatorRegistrationManager.Instance.RegisterIndicator(indicatorType);
// TimeDataSeries must have created and initialized with data points
IndicatorBase indicator = IndicatorRegistrationManager.Instance.GetIndicator(indicaorName, _timeDataSeries);
if (indicator == null)
return;
// Set an input value as per user choice
indicator.SetFieldValue("LengthPeriod", 20);
// Refresh internally makes a call of Calculate override method for all existing bar
// This call is optional
indicator.Refresh();
// Refer the value of Indicator at any bar
for (int i = 0; i < _ timeDataSeries.Count; i++)
{
double upperBandBB = indicator["UpperBand ", i];
double bb = indicator["BB ", i];
double lowerBandBB = indicator["LowerBand ", i];
}
What various details are accessed in Indicator Calculate method?
Following code snippet shows various functionality accessed in indicator Calculate method:
protected override double Calculate(int index)
{
// Get a current bar Close price
double close = Ref('C', 0);
// Following are different ways to get a latest bar close price
close = Ref('C', 0);
close = BarValue('C', index);
close = Close(index-1);
close = TimeDataSeries[index].Close;
// Get an previous bar close price
double prevClose = Ref('C', -1);
// Get an average of all close prices of last _lengthPeriod from latest bar
double average = Average('C', _lengthPeriod, index);
// Get a 2 std deviation of close prices
double stddev = StdDev('C', _lengthPeriod, index) * 2.0;
// Get a Highest high and Lowest Low value of last 10 bars
double hhv = HHV(10);
double llv = LLV(10);
// Get a true range for specific bar index
double tr = TrueRange(index);
bool isRisingBar = IsRising(index);
bool isFallingBar = IsFalling(index);
// Get the current bar time in format HHmmss format i.e 13:40 bar time is represented as 134000
Int timeNum = TimeNum();
// Get the current bar date in format yyyyMMdd format i.e 1st June 22 bar is represented as 220601
Int dateNum = DateNum()
//BarSince method is useful function and return the Bar Index tracking backward from last bar till
// nth occurance of Evaluate Function is satisfied
Func<int, bool> Evaluate = (barIndex) => Close(barIndex) > Close(barIndex - 1);
int barIndexMatched = BarSince(Evaluate, 1);
// this return highest value of close since the second most recent occurrance of the high being above 50
Func<int, bool> Evaluate2 = (barIndex) => High(barIndex) > 50;
double highestVal = HighestSince(Evaluate, 'C', 2);
// find a close price at bar index when recent bartime is 091600
Func<int, bool> Evaluate3 = (barIndex) => TimeNum(barIndex) == 091600;
double closePrice = ValueWhen(Evaluate, 'C', 1);
}
What are various quick analytics method supported in TimeDataSeries and DoubleSeries?
If TimeDataSeries and DoubleSeries are directly accessed within an indicator and strategy framework following useful methods are provided.
Ref(“C’) return the close price of current price index in series
Ref(‘H’, 5) return High price of 5 bar ago. If no index is available then it return NaN