Your first indicator

Indicators

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.

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:

  1. an output series — a ChunkedSeries<double> the indicator appends one value to per bar;
  2. a parameter — the period, tagged with [IndicatorParameter] so the settings dialog shows it;
  3. a plot — the line on the chart, registered once with AddPlot(...);
  4. two lifecycle methodsOnDataLoaded computes the whole history, OnBarUpdate appends 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).

Why the plot is added in the constructor. The 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.