Your first indicator
Your first indicator SIMPLE
A complete simple moving average, explained line by line. It is the template almost every line indicator follows: declare a parameter, register a plot, fill a series in two lifecycle methods.
On this page
What we are building
A simple moving average (SMA) is the "hello world" of indicators. It has four moving parts that reappear in nearly every indicator you will ever write:
- an output series — a
ChunkedSeries<double>the indicator appends one value to per bar; - a parameter — the period, tagged with
[IndicatorParameter]so the settings dialog shows it; - a plot — the line on the chart, registered once with
AddPlot(...); - two lifecycle methods —
OnDataLoadedcomputes the whole history,OnBarUpdateappends each live bar.
The complete indicator
Here it is in full. Every type, attribute and method below is real public surface from
TradeStrike.Pipeline.Indicators, TradeStrike.Pipeline.Plots and
TradeStrike.Pipeline.Series.
Sma.csusing TradeStrike.Pipeline.Indicators;
using TradeStrike.Pipeline.Plots;
using TradeStrike.Pipeline.Series;
[IndicatorDescription("Simple moving average. The unweighted mean of the input " +
"over the last N bars — the canonical price smoother.")]
[IndicatorCategory(IndicatorCategory.Trend)]
[IndicatorInput(IndicatorInputKind.Scalar)]
public sealed class Sma : IndicatorBase
{
// The indicator owns the writable backing store; the plot reads it.
private readonly ChunkedSeries<double> _output = new();
[IndicatorParameter(DisplayName = "Period", Description = "Number of bars to average.",
Group = "Parameters", Order = 0, MinValue = 1, MaxValue = 1000, Step = 1)]
public int Period { get; set; } = 14;
// Single ctor of the canonical shape: parent for child composition (null = standalone),
// input for an explicit source series. The derived body sets parameters + plots.
public Sma(IIndicator? parent = null, ISeries<double>? input = null, int period = 14)
: base(parent, input)
{
Period = period;
AddPlot(new Plot($"SMA({Period})", _output, PlotStyle.Line, new ChartColor(33, 150, 243), 1.5));
}
public override void OnInit(IIndicatorContext ctx)
{
base.OnInit(ctx); // defaults Input to Bars(0).Close
if (Period < 1)
throw new System.ArgumentOutOfRangeException(nameof(Period), "Period must be >= 1.");
}
// History: one pass over the whole backfill, oldest bar first.
public override void OnDataLoaded(IIndicatorContext ctx)
{
int count = Input.Count;
for (int i = count - 1; i >= 0; i--) // i is barsAgo: count-1 = oldest, 0 = newest
_output.Append(Average(barsAgo: i));
}
// Live: one new closed bar — append its value.
public override void OnBarUpdate(IIndicatorContext ctx)
{
_output.Append(Average(barsAgo: 0));
}
// Shared maths so history and live can never drift apart.
private double Average(int barsAgo)
{
if (Input.Count - barsAgo < Period) return double.NaN; // warmup: not enough bars yet
double sum = 0;
for (int k = 0; k < Period; k++)
sum += Input[barsAgo + k];
return sum / Period;
}
}
Line by line
The class attributes
[IndicatorDescription] supplies the one-paragraph blurb the Add-Indicator and settings
dialogs show; it is required on every shipped indicator. [IndicatorCategory] places it in
a family (Trend, Momentum, Volume, …) for grouping and
filtering. [IndicatorInput(IndicatorInputKind.Scalar)] declares that this indicator reads
the scalar Input stream — which also makes the universal "Input series" picker
appear, so a user can run the SMA on High, Median, Volume, or another indicator's plot. (Bar-based
indicators that read OHLC directly declare IndicatorInputKind.Bars instead and get no
picker.)
The output series
ChunkedSeries<double> is the SDK's writable, single-writer / multi-reader series.
It grows without bound (a multi-day tick session never silently drops history), appends are O(1)
amortised, and it is NaN-aware — appending double.NaN round-trips cleanly so warmup
bars render as gaps. You never expose it directly; you hand it to a plot, and the chart reads it from
the render thread.
The parameter
[IndicatorParameter] on a public read/write property does three things: it renders an
editor row in the settings dialog, it persists the value into the saved chart template, and it becomes
part of the indicator's content key — the identity that makes
SMA(14) and SMA(50) distinct instances. Always give a parameter a real
default (here 14): the host constructs the indicator with defaults, then applies the
saved value. Parameters covers this in full.
The constructor and the plot
Every IndicatorBase subclass exposes one constructor of the canonical shape
(IIndicator? parent = null, ISeries<double>? input = null, /* your params */),
passing parent and input straight to base(...). That single
line is the only plumbing — it registers the indicator as a child of parent when
one is supplied (see Child indicators). Inside the body you set
your parameters and call AddPlot. The Plot constructor takes
(name, values, style, color, lineWidthPx); ChartColor is plain RGBA
(new ChartColor(33, 150, 243) is a blue).
Plots list is
append-only and reference-stable for the indicator's lifetime — the chart caches it and the
settings dialog binds to it. Register every plot up front (constructor or OnInit) and
never resize the list afterward. You may freely mutate a plot's colour, style and visibility at
runtime; you may not add or remove plots later.OnInit
OnInit runs exactly once, after construction and after the saved parameter value has been
applied — so it is the right place to validate and to size buffers. Call
base.OnInit(ctx) first. The base implementation defaults Input to
Bars(0).Close (and Bars to Bars(0)) when you didn't inject an
explicit source, and resolves the "Input series" picker if the user changed it. Read
Input only after that call.
OnDataLoaded — the history pass
When OnDataLoaded fires, the entire backfill is already in the series. You compute every
historical value in one loop. Indexing is bars-ago: Input[0] is the most recent bar and
Input[Input.Count - 1] is the oldest, so to append in chronological order you walk
barsAgo from Count - 1 down to 0.
OnBarUpdate — the live pass
OnBarUpdate fires once per closed bar in live trading (the default
CalculationMode.OnBarClose cadence). The newest bar is at barsAgo: 0; you
append exactly one value. Note how both passes call the same Average helper —
sharing the maths is the single best habit for keeping history and live output identical.
Run it
Compile the indicator into a plugin assembly and drop it in your TradeStrike plugins folder; the host
discovers it by scanning for IIndicator types. It then appears in the chart's Add
Indicator dialog with the description and the editable Period field. See
Building & deploying for discovery and deployment details.