Create Your Own Custom Indicator Using QX.FinLib
Build a custom Exponential Moving Average (EMA) indicator from scratch
Introduction
The QX.FinLib library provides a powerful and flexible framework for building custom technical indicators. In this article, we will walk through the process of creating your own Exponential Moving Average (EMA) indicator, which we’ll name EMAX1 from scratch.
By the end of this guide, you will understand:
- The required class structure and decorators
- The key methods to override
- How to implement the EMA formula correctly
Step 1: Define Your Indicator Class
Every custom indicator must inherit from IndicatorBase and be decorated with the IndicatorAttribute. This allows the QX.FinLib indicator registration manager to register and discover your indicator
Here is the basic skeleton:
[IndicatorAttribute("{29D98F27-D628-4E04-AAC1-D9690C718FB7}", "EMAX1", "Exponential Moving Average", "QXT")]
public class EMAX1: IndicatorBase {
}
Understanding the IndicatorAttribute Parameters
| Parameter | Example Value | Purpose |
|---|---|---|
| GUID | {29D98F27-D628-4E04-AAC1-D9690C718FB7} | Uniquely identifies your indicator. Generate a new GUID for each custom indicator. |
| Unique Name | EMAX1 | Short, unique name used internally. |
| Description | Exponential Moving Average | Human-readable description shown in UI. | Owner ID | QXT | Identifies the owner or organization (e.g., QXT). |
Important: Always generate a new, unique GUID for your indicator. Do not copy existing GUIDs.
Step 2: Add Parameters and Outputs
To make your indicator flexible, you need to expose configurable Parameters (like period length) and define Output series for the final values.
Define a Parameter
Use the [Parameter] attribute to expose user-configurable input settings:
[Parameter("LengthPeriod", Description = "EMA Length", DefaultValue = 12)]
private int _lengthPeriod = 12;
Define an Output Series
Use the [Output] attribute to store and display calculated values:
[Output("EMA", DefaultValue = double.NaN, LineColor = "DarkOrange")]
private DoubleSeries _emaDoubleValueSeries;
Complete Class with Properties
[IndicatorAttribute("{29D98F27-D628-4E04-AAC1-D9690C718FB7}", "EMAX1", "Exponential Moving Average", "QXT")]
public class EMAX1: IndicatorBase { [Parameter("LengthPeriod", Description = "The length of the EMA", DefaultValue = 12)]
private int _lengthPeriod = 12;
[Output("EMA", DefaultValue = double.NaN, LineColor = "DarkOrange")]
private DoubleSeries _emaDoubleValueSeries;
private double _exponentialMultiplier = 0;
}
Step 3: Implement Constructors
You must provide three constructors to ensure flexibility:
// Full constructor with parameters
public EMAX1(TimeDataSeries timeDataSeries, int lengthPeriod)
: base(timeDataSeries)
{
if (lengthPeriod <= 0)
throw new ArgumentException("Invalid EMA length");
_lengthPeriod = lengthPeriod;
_exponentialMultiplier = 2.0 / (_lengthPeriod + 1);
_emaDoubleValueSeries = new DoubleSeries();
}
// Constructor with default period
public EMAX1(TimeDataSeries timeDataSeries)
: base(timeDataSeries)
{
_exponentialMultiplier = 2.0 / (_lengthPeriod + 1);
_emaDoubleValueSeries = new DoubleSeries();
}
// Parameterless constructor (required for serialization)
public EMAX1()
: base(null)
{
_exponentialMultiplier = 2.0 / (_lengthPeriod + 1);
_emaDoubleValueSeries = new DoubleSeries();
}
Step 4: Implementation of Key override Methods
The IndicatorBase class provides virtual methods that act as the lifecycle of your calculation.
4.1 Initialize() Method
Called once during initialization. Use it to set up variables and pre-calculate constants.
protected override void Initialize()
{
// Calculate the smoothing multiplier
_exponentialMultiplier = 2.0 / (_lengthPeriod + 1);
}
4.2 CalculateOnBar(int index) Method
This is the core logic of your indicator. It is called for each bar/index in the time series and must return the indicator value at that index.
protected override double CalculateOnBar(int index)
{
double currentPrice = this.InputSeries[index];
// Not enough data yet
if (index < _lengthPeriod - 1)
{
_emaDoubleValueSeries[index] = double.NaN;
}
// First valid EMA = SMA of first 'lengthPeriod' bars
else if (index == _lengthPeriod - 1)
{
_emaDoubleValueSeries[index] = this.InputSeries.Average(_lengthPeriod);
}
else
{
double previousEMA = _emaDoubleValueSeries[index - 1];
// Fallback safety: if previous EMA is invalid, recalculate as SMA
if (!previousEMA.HasValue())
{
_emaDoubleValueSeries[index] = this.InputSeries.Average(_lengthPeriod);
}
else
{
// Standard EMA formula:
// EMA = (Price - PreviousEMA) * Multiplier + PreviousEMA
double ema = ((currentPrice - previousEMA) * _exponentialMultiplier) + previousEMA;
_emaDoubleValueSeries[index] = ema;
}
}
return _emaDoubleValueSeries[index];
}
4.3 ResetIndicator() Method
Called when the indicator needs to be recalculated from scratch (e.g., when parameters change or backtest restarts).
protected override void ResetIndicator()
{
// Reset multiplier
_exponentialMultiplier = 2.0 / (_lengthPeriod + 1);
// Clear the output series
_emaDoubleValueSeries = new DoubleSeries();
}
4.4 (Optional) GetIndicatorCategoryType() Method
Override this to categorize your indicator in the UI:
public override IndicatorCategoryType GetIndicatorCategoryType()
{
return IndicatorCategoryType.General;
}
Step 5: Complete EMAX1 Indicator Code
Here is the full, working code for your custom EMA indicator:
using System;
using QX.FinLib.Data;
namespace QX.FinLib.Data.TI
{
[IndicatorAttribute("{29D98F27-D628-4E04-AAC1-D9690C718FB7}", "EMAX1", "Exponential Moving Average", "QXT")]
public class EMAX1 : IndicatorBase
{
[Parameter("LengthPeriod", Description = "The length of the EMA", DefaultValue = 12)]
private int _lengthPeriod = 12;
[Output("EMA", DefaultValue = double.NaN, LineColor = "DarkOrange")]
private DoubleSeries _emaDoubleValueSeries;
private double _exponentialMultiplier = 0;
// Full constructor
public EMAX1(TimeDataSeries timeDataSeries, int lengthPeriod)
: base(timeDataSeries)
{
if (lengthPeriod <= 0)
throw new ArgumentException("Invalid EMA length");
_lengthPeriod = lengthPeriod;
_exponentialMultiplier = 2.0 / (_lengthPeriod + 1);
_emaDoubleValueSeries = new DoubleSeries();
}
// Default period constructor
public EMAX1(TimeDataSeries timeDataSeries)
: base(timeDataSeries)
{
_exponentialMultiplier = 2.0 / (_lengthPeriod + 1);
_emaDoubleValueSeries = new DoubleSeries();
}
// Parameterless constructor
public EMAX1()
: base(null)
{
_exponentialMultiplier = 2.0 / (_lengthPeriod + 1);
_emaDoubleValueSeries = new DoubleSeries();
}
public override IndicatorCategoryType GetIndicatorCategoryType()
{
return IndicatorCategoryType.General;
}
protected override void Initialize()
{
_exponentialMultiplier = 2.0 / (_lengthPeriod + 1);
}
protected override double CalculateOnBar(int index)
{
double currentPrice = this.InputSeries[index];
if (index < _lengthPeriod - 1)
{
_emaDoubleValueSeries[index] = double.NaN;
}
else if (index == _lengthPeriod - 1)
{
_emaDoubleValueSeries[index] = this.InputSeries.Average(_lengthPeriod);
}
else
{
double previousEMA = _emaDoubleValueSeries[index - 1];
if (!previousEMA.HasValue())
{
_emaDoubleValueSeries[index] = this.InputSeries.Average(_lengthPeriod);
}
else
{
double ema = ((currentPrice - previousEMA) * _exponentialMultiplier) + previousEMA;
_emaDoubleValueSeries[index] = ema;
}
}
return _emaDoubleValueSeries[index];
}
protected override void ResetIndicator()
{
_exponentialMultiplier = 2.0 / (_lengthPeriod + 1);
_emaDoubleValueSeries = new DoubleSeries();
}
}
}
Once Indicator is available as in form of DLL or a code class in your project. You can use Indicator as following
TimeDataSeries timeDataSeries5Min = TimeDataSeries.GetMinutesBarCompression(500);
CSVOHLCLoader csvOHLCLoader = new CSVOHLCLoader(fileName,
BarDataFileFormat.Symbol_Date_Time_O_H_L_C_V_O,
DateFormat.Custom,
DateSeparator.None,
"yyyyMMdd HH:mm",
true);
IndicatorBase indicatorEMAX1 = IndicatorRegistrationManager.Instance.GetIndicator("EMAX1", timeDataSeries5Min);
// Set Indicator input value of LengthPeriod to 10
indicatorEMAX1.SetFieldValue("LengthPeriod", 10);
for (int i = 0; i < timeDataSeries5Min.Count; i++)
{
IBarData barData = timeDataSeries5Min[i];
double ema10 = indicatorEMAX1[i];
}
Step 6: How to Use Your Custom Indicator
Once your indicator is available as a DLL (compiled assembly) or as a code class directly in your project, you can instantiate and use it programmatically.
6.1 Load Time Data Series
First, load or create a TimeDataSeries with your price data:
// Create a 5-minute compressed time data series
TimeDataSeries timeDataSeries5Min = TimeDataSeries.GetMinutesBarCompression(500);
// Load CSV data
CSVOHLCLoader csvOHLCLoader = new CSVOHLCLoader(fileName,
BarDataFileFormat.Symbol_Date_Time_O_H_L_C_V_O,
DateFormat.Custom,
DateSeparator.None,
"yyyyMMdd HH:mm",
true);
6.2 Get Indicator Instance Using Registration Manager
Use the IndicatorRegistrationManager to create an instance of your indicator by its registered name:
// Get indicator instance by name (EMAX1) and bind it to the time series
IndicatorBase indicatorEMAX1 = IndicatorRegistrationManager.Instance.GetIndicator("EMAX1", timeDataSeries5Min);
6.3 Configure Indicator Parameters
Set any parameter values before calculating. For example, change the EMA period to 10:
// Set the LengthPeriod parameter to 10
indicatorEMAX1.SetFieldValue("LengthPeriod", 10);
6.4 Access Indicator Values
Iterate through the time series and retrieve the indicator value at each bar:
for (int i = 0; i < timeDataSeries5Min.Count; i++)
{
IBarData barData = timeDataSeries5Min[i];
double ema10 = indicatorEMAX1[i];
// Use the EMA value for your logic
Console.WriteLine($"Bar {i}: Close={barData.Close}, EMA10={ema10}");
}
You’ve just turned a concept into a production-ready engine. Your EMAX1 is now hard-wired for the BlitzTrader ecosystem and is now optimized for seamless transition from deep-historical backtesting to precision-grade live trading.