Using indicators
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.
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.
new MovingAverage { Period = _fastPeriod.Value }, and the optimizer
sweeps the indicator's period, because each run builds the indicator from its own overrides.[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.