Child indicators

Indicators

Child indicators

Compose indicators from other indicators. A child computes before its parent on every event, so the parent always reads up-to-date child output — this is how MACD is "EMA minus EMA".

Composition, not copy-paste

Many indicators are built from simpler ones: MACD is two EMAs, Bollinger Bands are an SMA plus a standard-deviation band, Stochastic %D is an SMA of %K. Rather than re-implement the EMA maths inside MACD, you attach a child EMA and read its output.

You create a child by passing this as the parent argument to its constructor. The base constructor calls parent.AttachChild(this) for you. From then on the runtime dispatches the child before its parent on every event — depth-first, post-order — so when the parent reads child.Output[0] inside its own OnBarUpdate, it always sees this bar's value.

There is no child.Value. A child publishes its result as an ISeries<double>; by convention built-ins expose it as a public Output property (multi-output indicators expose named series like Upper / Middle / Lower). You read it bars-ago, exactly like any other series.

Example: a composed MACD

MACD = fast EMA − slow EMA, with a signal EMA of that difference. Here it is built by composition. Note the call to AdvanceChildren(ctx) at the top of OnBarUpdate: on a chart host, live dispatch only calls OnBarUpdate on top-level indicators, so a composite drives its own attached children once per live bar with this helper (it is a no-op on hosts that walk the tree themselves, so it is always safe to call).

ComposedMacd.csusing TradeStrike.Pipeline.Indicators;
using TradeStrike.Pipeline.Plots;
using TradeStrike.Pipeline.Series;

[IndicatorDescription("MACD — the difference between a fast and a slow EMA, with a signal line.")]
[IndicatorCategory(IndicatorCategory.Momentum)]
[IndicatorInput(IndicatorInputKind.Scalar)]
public sealed class ComposedMacd : IndicatorBase, IIndicatorPanelHint
{
    private Ema _fast = null!, _slow = null!, _signalEma = null!;
    private readonly ChunkedSeries<double> _macd   = new();
    private readonly ChunkedSeries<double> _signal = new();
    private readonly ChunkedSeries<double> _hist   = new();

    [IndicatorParameter(DisplayName = "Fast",   Group = "Parameters", Order = 0)] public int Fast   { get; set; } = 12;
    [IndicatorParameter(DisplayName = "Slow",   Group = "Parameters", Order = 1)] public int Slow   { get; set; } = 26;
    [IndicatorParameter(DisplayName = "Signal", Group = "Parameters", Order = 2)] public int Signal { get; set; } = 9;

    public PanelPlacement DefaultPanel => PanelPlacement.NewSubpanel;

    public ComposedMacd(IIndicator? parent = null, ISeries<double>? input = null) : base(parent, input)
    {
        AddPlot(new Plot("Histogram", _hist,   PlotStyle.Histogram, new ChartColor(120, 120, 120), 1.0));
        AddPlot(new Plot("MACD",      _macd,   PlotStyle.Line,      new ChartColor(33, 150, 243), 1.5));
        AddPlot(new Plot("Signal",    _signal, PlotStyle.Line,      new ChartColor(244, 67, 54),  1.5));
    }

    public override void OnInit(IIndicatorContext ctx)
    {
        base.OnInit(ctx);
        // Children created from this indicator's resolved Input. The signal EMA
        // is chained off the MACD difference (computed below into its own series).
        _fast = new Ema(this, input: Input, period: Fast);
        _slow = new Ema(this, input: Input, period: Slow);
    }

    public override void OnDataLoaded(IIndicatorContext ctx)
    {
        // Children are already backfilled by the time our OnDataLoaded runs.
        for (int i = Input.Count - 1; i >= 0; i--)
            _macd.Append(_fast.Output[i] - _slow.Output[i]);

        _signalEma = new Ema(this, input: new IndicatorOutputSeries(_macd, Bars), period: Signal);
        for (int i = _macd.Count - 1; i >= 0; i--)
        {
            _signal.Append(_signalEma.Output[i]);
            _hist.Append(_macd[i] - _signalEma.Output[i]);
        }
    }

    public override void OnBarUpdate(IIndicatorContext ctx)
    {
        AdvanceChildren(ctx);                  // drive attached children for this live bar
        double macd = _fast.Output[0] - _slow.Output[0];
        _macd.Append(macd);
        _signalEma.OnBarUpdate(ctx);           // advance the chained signal EMA off the new MACD value
        _signal.Append(_signalEma.Output[0]);
        _hist.Append(macd - _signalEma.Output[0]);
    }
}
Composition is a tool, not a mandate. Built-ins often inline their EMAs for a tighter hot path. Composition is shown here for clarity; both styles are valid — reach for children when it keeps the maths readable or lets you reuse an existing indicator.

Feeding a child a different input

The canonical child constructor is (IIndicator? parent, ISeries<double> input, IBarSeries? bars). Pass whatever series the child should read — your resolved Input, a price projection like Bars.High or Bars.Typical, or another child's Output. Children can be stacked into chains.

chaining childrenvar sma = new Sma(this, input: Bars.Close,  period: 14);
var ema = new Ema(this, input: sma.Output,  period: 10);   // EMA of the SMA
var hi  = new Atr(this, bars:  Bars,        period: 14);   // a bar-based child reading OHLC

When you wrap your own output series for a downstream child, use new IndicatorOutputSeries(_buffer, Bars). It carries the "what bar series am I derived from" hint (IBarSourcedSeries.SourceBars) so the framework seeds the child's primary correctly along an indicator-of-indicator chain.

Disposal & the tree guards

Disposing a parent cascades disposal to every child in reverse-attach order, and disposing a child detaches it from its parent — you never call Dispose on your children yourself. Your OnDispose only releases what you allocated directly (timers, native handles). The tree is guarded: attaching a child that would form a cycle, or push the chain past IndicatorBase.MaxChildDepth (16), throws immediately.