Child 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".
On this page
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.
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]);
}
}
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.