Using indicators

Strategies

Using indicators

Declare an indicator in your strategy with one factory call, and read its value bar by bar.

Using indicators

A strategy reuses indicator types instead of repeating their maths. Declare one with DeclareIndicator(factory) inside OnInitialize, passing a lambda that constructs and configures it. Any IndicatorBase subclass your plugin can reference works, including one you write next to the strategy. The runner feeds the instance the same bars your strategy sees, before each OnBar, so in OnBar you read its current value.

DeclareIndicator takes a factory because of timing: the factory runs after parameter overrides are applied, so an indicator sized by a parameter uses that run's value. Under an optimizer sweep each pass builds the indicator from its own overrides. A factory with a literal period works the same way.

The example declares MovingAverage from Your first strategy and a small average-range indicator that uses the bar's high and low:

AverageRange.csusing SabrTrader.Pipeline.Indicators;
using SabrTrader.Pipeline.Plots;
using SabrTrader.Pipeline.Series;

[IndicatorDescription("Average high-low range over the last N bars.")]
[IndicatorInput(IndicatorInputKind.Bars)]
public sealed class AverageRange : IndicatorBase
{
    private readonly ChunkedSeries<double> _values = new();

    [IndicatorParameter(DisplayName = "Period", MinValue = 1, MaxValue = 500, Step = 1)]
    public int Period { get; set; } = 14;

    public AverageRange(IIndicator? parent = null, ISeries<double>? input = null, int period = 14)
        : base(parent, input)
    {
        Period = period;
        AddPlot(new Plot("Range", _values, PlotStyle.Line, new ChartColor(255, 152, 0), 1.0));
    }

    public override void OnDataLoaded(IIndicatorContext ctx)
    {
        for (int barsAgo = Bars.Count - 1; barsAgo >= 0; barsAgo--)
            _values.Append(Average(barsAgo));
    }

    public override void OnBarUpdate(IIndicatorContext ctx) => _values.Append(Average(0));

    private double Average(int barsAgo)
    {
        if (Bars.Count - barsAgo < Period) return double.NaN;
        double sum = 0;
        for (int k = 0; k < Period; k++) sum += Bars.High[barsAgo + k] - Bars.Low[barsAgo + k];
        return sum / Period;
    }
}
C#private StrategyParameter<int> _fastPeriod = null!;
private StrategyIndicator<MovingAverage> _ma = null!;
private StrategyIndicator<AverageRange> _range = null!;

protected override void OnInitialize()
{
    _fastPeriod = IntParameter("FastPeriod", 20, 5, 100, 1);
    _ma    = DeclareIndicator(() => new MovingAverage { Period = _fastPeriod.Value });  // parameter-sized
    _range = DeclareIndicator(() => new AverageRange { Period = 14 });
}

protected override void OnBar()
{
    double ma = _ma.Instance.Value;                    // a convenience property on the indicator
    double range = _range.Instance.Plots[0].Values[0]; // any indicator: plot series, [0] = latest
    if (double.IsNaN(ma) || double.IsNaN(range)) return;
    // ...
}

The handle & readiness

DeclareIndicator returns a typed StrategyIndicator<T> handle; hold it in a field. The indicator does not exist yet at declaration time (the factory has not run), which is why you get a handle. By the time OnStart runs, the runner has built and backfilled every declared indicator over the run's pre-run history, so its values are usable at once. Read .Instance from OnStart onward; reading it inside OnInitialize throws InvalidOperationException. IsReady is true once the instance is bound.

Reading values. Every indicator publishes its output as plots: Instance.Plots is the list in the order the indicator added them, and Instance.TryGetPlot(name, out IPlot plot) finds one by name. plot.Values is an ISeries<double> indexed by bars ago ([0] is the latest). Indicators you write can add a typed property such as Value on top, as MovingAverage does.

Guard reads during warm-up. IsReady means the indicator is built, not that it has enough bars to be meaningful: a 20-bar average has no value at bar 3. Indicators return double.NaN for those bars by convention, so check for it, or gate on BarCount.

Parameter-sized indicators cost nothing extra to optimize. Combine the factory with an optimizable parameter, as in new MovingAverage { Period = _fastPeriod.Value }, and the optimizer sweeps the indicator's period, because each run builds the indicator from its own overrides.
Order flow. Declaring an indicator marked [RequiresOrderFlow] makes the host load per-bar order flow for the run, which also lights up TryGetOrderFlowBar in the strategy. See Order flow and the order book.