Learn NinjaScript: Smoothing Data with Moving Averages
“I have a jittery calculation and I want to smooth it.” The answer is almost always “call one of NinjaTrader’s built-in moving-average indicators on your existing Series.” NinjaScript ships SMA, EMA, WMA, HMA, TEMA, DEMA, VWMA, and several others as indicator objects you instantiate and call the same way you’d call SMA on Close.
This post covers the built-ins, how to call them on your own custom Series<double>, when to call an MA from inside another indicator’s OnBarUpdate, and the narrow cases where you roll your own smoothing because the built-ins don’t match the math you need.
⚙️ Built-in MA Types and When Each Fits
Not all MAs smooth the same way. Pick based on what you want the smoother to prioritize:
- SMA — simple arithmetic mean. Clearest interpretation, slowest response to new data. Use when explainability matters more than responsiveness.
- EMA — exponentially weights recent bars. Faster response than SMA, smoother than price. The default when “smoothed but responsive” is the goal.
- HMA (Hull) — designed to reduce lag without adding noise. Good for signals that need to react quickly to genuine direction changes.
- VWMA — volume-weighted. Heavy-volume bars pull the average more than low-volume bars. Use when volume information is meaningful for the calculation.
- WMA / TEMA / DEMA — other lag-reduction variants. Worth knowing about, but EMA and HMA cover most use cases.
🔗 Calling MAs on Your Own Series
Every built-in MA has an overload that accepts an ISeries<double> source. Pass your own custom Series, and the MA computes on your data:
// A custom Series holding some raw calculation.
private Series<double> rawVolatility;
protected override void OnStateChange()
{
// ... SetDefaults ...
if (State == State.DataLoaded)
rawVolatility = new Series<double>(this, MaximumBarsLookBack.Infinite);
}
protected override void OnBarUpdate()
{
if (CurrentBar < 1) return;
// Write to the custom Series.
rawVolatility[0] = Math.Abs(Close[0] - Close[1]);
// Smooth it — EMA with a 10-period window, called on the custom Series.
double smoothed = EMA(rawVolatility, 10)[0];
Values[0][0] = smoothed;
}
Three things to notice. First, the Series has to be populated before the EMA call on the same bar — otherwise you’re smoothing against stale or uninitialized data. Second, the MA call returns an indicator object; [0] reads its current-bar value. Third, nothing about the MA call cares that the source is a custom Series — it treats any ISeries<double> the same.
🏗️ Calling MAs Inside Your OnBarUpdate
Each MA call like EMA(source, period) instantiates (or retrieves from cache) a full indicator object. You don’t need to declare it anywhere or initialize it in State.DataLoaded — NinjaTrader caches the instance internally, keyed by source and period.
That caching is why EMA(Close, 10)[0] in bar after bar keeps returning consistent smoothed values: you’re hitting the same cached EMA instance each time. Two calls in the same indicator with the same arguments share state. Different arguments = different cache entry = different instance.
🛠️ Rolling Your Own
Reach for custom smoothing code only when the built-ins don’t match the math. Common cases: Pine Script’s ta.rma initialization differs subtly from NT’s internal RMA; a book’s formula for a specialized MA (KAMA, ALMA, JMA variants) isn’t in the NT catalog; a research paper describes a filter with parameters the built-ins don’t expose.
When you roll your own, inline the computation into a helper method with a custom Series<double> for output:
private Series<double> customSmoothed;
protected override void OnStateChange()
{
if (State == State.DataLoaded)
customSmoothed = new Series<double>(this, MaximumBarsLookBack.Infinite);
}
protected override void OnBarUpdate()
{
if (CurrentBar < Period) return;
// Example: Pine-exact RMA ("running moving average") with alpha = 1/Period.
double alpha = 1.0 / Period;
double prev = CurrentBar == Period ? SMA(Close, Period)[0] // seed with SMA on the first usable bar
: customSmoothed[1];
customSmoothed[0] = alpha * Close[0] + (1 - alpha) * prev;
}
The pattern: declare a Series<double> to hold the output, seed it on the first usable bar, and express the recurrence relation using the previous bar’s output value. The only tricky part is the seed — for most smoothers, using an SMA of the window as bar-Period‘s value gets you close enough to the mathematical ideal.
🛠️ The Anchor Example — MySma With a Smoothed Volatility Channel
Extending MySma with a secondary plot: a 10-period EMA of absolute bar-to-bar change, used as a rough volatility proxy. The raw series lives as a custom Series<double>; the plot is the EMA smoothing of it.
private Series<double> rawChange;
public override void OnStateChange()
{
if (State == State.SetDefaults)
{
Name = "My SMA with Volatility";
Calculate = Calculate.OnBarClose;
IsOverlay = false;
Period = 20;
AddPlot(Brushes.LimeGreen, "Volatility EMA");
}
else if (State == State.DataLoaded)
{
rawChange = new Series<double>(this, MaximumBarsLookBack.Infinite);
}
}
protected override void OnBarUpdate()
{
if (CurrentBar < 1) return;
rawChange[0] = Math.Abs(Close[0] - Close[1]);
if (CurrentBar < 10) return;
Values[0][0] = EMA(rawChange, 10)[0];
}
The plot shows smoothed volatility rather than raw bar-to-bar jitter. Same pattern works for any derived calculation you want smoothed before plotting or before passing downstream.
📝 Pitfalls Checklist
- Reading the MA before writing to the source Series.
EMA(mySeries, 10)[0]called before you setmySeries[0]reads stale data. Populate the source first. - Instantiating an MA in
State.SetDefaults. Unnecessary and wrong timing — NT caches internally. Just callEMA(...)inOnBarUpdate. - Assuming NT’s built-in MAs are strict-warmup.
EMA(Close, 20)[0]returns a value from bar 0 — it doesn’t wait for 20 bars before producing output. Guard withif (CurrentBar < Period) return;if you only want values after warmup completes. - Reimplementing an MA that NT already has. Extra code, potential bugs, and the NT version is usually faster because it maintains internal state across bars. Only roll your own when the math genuinely differs.
- Wrong source series.
EMA(20)[0](no source) defaults to the chart’s input series — usuallyClose. If you meant to smooth a custom Series, you have to pass it explicitly:EMA(myCustomSeries, 20)[0]. - Forgetting to initialize the output Series. Rolling your own requires a
Series<double>initialized inState.DataLoaded. Forgetting it gives you a null-reference exception on the first write.








