Create a Custom NinjaTrader 8 Indicator With NinjaScript

Create a Custom NinjaTrader 8 Indicator With NinjaScript

By HorizonAI Team · 12 min read · Beginner

How to Create a Custom Indicator in NinjaTrader 8 with NinjaScript

A useful first NinjaTrader indicator should make one decision visible, not try to trade for you. Here, you’ll build an overlay that plots a fast and slow EMA, marks confirmed crossovers with arrows, and can color the averages by trend direction.

Short answer: Create an NT8 indicator from the NinjaScript Editor, set defaults and plots in OnStateChange(), instantiate the two EMA series when data loads, then update plots and draw crossover arrows in OnBarUpdate(). Keep it as an indicator when it only visualizes a condition; a strategy adds order methods and is a different script type.

The finished indicator uses a 9-period EMA and a 21-period EMA by default. It waits until the slow EMA has enough bars, plots both lines on the price panel, draws one arrow on each confirmed crossover, and exposes every useful setting in the indicator panel. Start with that small, testable artifact before adding filters, multi-timeframe logic, or orders.

What you are building, and why it is an indicator

The artifact is EMA Crossover Signals: two configurable exponential moving averages, a green arrow when the fast average crosses above the slow average, and a red arrow when it crosses below. It also offers a trend-color switch: blue and orange in neutral mode, green/red when the fast EMA is above/below the slow EMA.

That makes it an indicator. It reads bar data and draws information on a chart. It does not submit entries, define a stop loss, manage a position, or produce a performance report. If you want the order-handling version later, use the first NinjaTrader 8 strategy tutorial as the next step.

For a first build, use the defaults below:

SettingDefaultWhy it is here
Fast EMA9 barsResponds quickly to recent price movement
Slow EMA21 barsSupplies a slower comparison baseline
CalculateOn bar closePrevents a still-forming bar from repeatedly changing the crossover state
Arrow offset2 ticksKeeps arrows from covering the candle high or low
Trend colorsOnMakes the current EMA relationship readable at a glance

The 9/21 pair is a visualization starting point, not a universal setup. On a 5-minute futures chart it will react far more often than it does on an hourly stock chart. Build the tool first, then adjust periods for the market and timeframe you actually review.

Create the indicator shell in NinjaTrader 8

Open New > NinjaScript Editor in NinjaTrader 8. In the editor, right-click the Indicators folder, choose New Indicator, name it EmaCrossoverSignals, and finish the wizard. NinjaTrader creates the class in the correct NinjaTrader.NinjaScript.Indicators namespace and opens it for editing.

The filename and public class name must match: EmaCrossoverSignals.cs contains public class EmaCrossoverSignals. A mismatch is a common reason that a script compiles poorly or doesn’t appear where you expect it in the indicator list.

Replace the generated class contents with the code below, retaining the standard using declarations. This is a whole indicator, not a fragment. Paste it into the editor, press F5 to compile, then add it from a chart’s Indicators dialog.

#region Using declarations
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Windows.Media;
using NinjaTrader.Gui.Chart;
using NinjaTrader.Gui.Tools;
using NinjaTrader.NinjaScript;
using NinjaTrader.NinjaScript.DrawingTools;
using NinjaTrader.NinjaScript.Indicators;
#endregion

namespace NinjaTrader.NinjaScript.Indicators
{
    public class EmaCrossoverSignals : Indicator
    {
        private EMA fastEma;
        private EMA slowEma;

        [NinjaScriptProperty]
        [Range(1, int.MaxValue)]
        [Display(Name = "Fast EMA period", GroupName = "Parameters", Order = 0)]
        public int FastPeriod { get; set; }

        [NinjaScriptProperty]
        [Range(2, int.MaxValue)]
        [Display(Name = "Slow EMA period", GroupName = "Parameters", Order = 1)]
        public int SlowPeriod { get; set; }

        [NinjaScriptProperty]
        [Display(Name = "Color plots by trend", GroupName = "Display", Order = 2)]
        public bool ColorPlotsByTrend { get; set; }

        protected override void OnStateChange()
        {
            if (State == State.SetDefaults)
            {
                Description              = "Plots two EMAs and marks confirmed crossover signals.";
                Name                     = "EMA Crossover Signals";
                Calculate                = Calculate.OnBarClose;
                IsOverlay                = true;
                DisplayInDataBox         = true;
                DrawOnPricePanel         = true;
                PaintPriceMarkers        = true;
                IsSuspendedWhileInactive = true;

                FastPeriod        = 9;
                SlowPeriod        = 21;
                ColorPlotsByTrend = true;

                AddPlot(Brushes.DodgerBlue, "Fast EMA");
                AddPlot(Brushes.DarkOrange, "Slow EMA");
            }
            else if (State == State.DataLoaded)
            {
                fastEma = EMA(FastPeriod);
                slowEma = EMA(SlowPeriod);
            }
        }

        protected override void OnBarUpdate()
        {
            if (CurrentBar < SlowPeriod)
                return;

            Values[0][0] = fastEma[0];
            Values[1][0] = slowEma[0];

            if (ColorPlotsByTrend)
            {
                Brush trendBrush = fastEma[0] >= slowEma[0] ? Brushes.LimeGreen : Brushes.Red;
                PlotBrushes[0][0] = trendBrush;
                PlotBrushes[1][0] = trendBrush;
            }

            if (CrossAbove(fastEma, slowEma, 1))
            {
                Draw.ArrowUp(this, "BullishCross" + CurrentBar, false, 0,
                    Low[0] - 2 * TickSize, Brushes.LimeGreen);
            }
            else if (CrossBelow(fastEma, slowEma, 1))
            {
                Draw.ArrowDown(this, "BearishCross" + CurrentBar, false, 0,
                    High[0] + 2 * TickSize, Brushes.Red);
            }
        }
    }
}

Understand the four pieces that make the code work

A NinjaScript indicator has a lifecycle. The code is easier to modify when you separate setup from bar-by-bar calculation instead of placing everything in one method.

OnStateChange() sets defaults and prepares resources

OnStateChange() runs through named states while NinjaTrader initializes the script. State.SetDefaults is the place for display metadata, default user inputs, calculation behavior, and plot definitions. State.DataLoaded is where this indicator creates its two built-in EMA objects. NinjaTrader documents this state-driven setup because different operations are valid at different points in the lifecycle. See the OnStateChange() reference.

AddPlot() belongs in the defaults state. Each call creates a plotted output series, and the order matters: AddPlot(... "Fast EMA") becomes Values[0], while the slow line becomes Values[1]. The API reference also notes that plots are configured during State.SetDefaults or State.Configure, not dynamically from OnBarUpdate(). Read the AddPlot() documentation.

Input properties turn hard-coded values into controls

[NinjaScriptProperty] tells NinjaTrader to show a property in the indicator settings. Range prevents nonsensical periods such as zero, and Display gives users readable labels and groups.

Notice that the fast and slow periods are independent inputs. The code doesn’t force FastPeriod < SlowPeriod. You can add that guard later, but keeping the first version simple helps you see exactly what each setting changes. Before trusting a configuration, check it yourself: a 50-fast/21-slow input still compiles, but it reverses the usual role of the lines.

OnBarUpdate() runs the chart logic

OnBarUpdate() is called as NinjaTrader processes data for the indicator. The method first returns until there are enough bars for the slow period, then assigns the latest EMA values to its plot series. NinjaTrader’s reference describes it as the central calculation event and stresses guarding against insufficient historical bars before accessing bar-indexed values. Review OnBarUpdate().

Values[0][0] means the latest value of the first plot. The first [0] selects the Fast EMA plot. The second [0] selects the current bar. That two-index pattern feels strange at first, but it’s the key distinction between a plot series and a single numeric value.

Crossover detection needs a one-bar lookback

CrossAbove(fastEma, slowEma, 1) returns true when the fast EMA crosses from below to above the slow EMA within a one-bar lookback. The matching CrossBelow call handles the opposite event. The unique drawing tag, such as BullishCross125, includes CurrentBar so every historical crossover gets its own arrow rather than replacing an earlier arrow.

The indicator uses Calculate.OnBarClose. That means the crossover arrow is evaluated once when the bar closes. Calculate.OnEachTick updates during an open bar and can be appropriate for intrabar visualization, but it can show a temporary crossover that disappears before close. NinjaTrader’s calculation-mode reference explains that the setting controls when OnBarUpdate() processes incoming data. Compare the available Calculate modes.

Build choice: Start with OnBarClose. Change to OnEachTick only when you deliberately want intrabar behavior and understand that a live bar can move back through both EMAs.

Compile it and add it to a chart

Press F5 in the NinjaScript Editor. If the compile succeeds, open a chart, right-click it, choose Indicators, find EMA Crossover Signals, and click Add. Set Fast EMA period to 9, Slow EMA period to 21, leave trend colors on, and apply.

Test it in three passes:

  1. Visual pass: confirm that both lines follow the built-in EMA indicators with the same periods.
  2. Signal pass: scroll to an obvious crossover and confirm there is one arrow on the crossover bar, not one on every bar of the trend.
  3. Input pass: change the periods to 20 and 50. The lines and arrows should update without editing code.

If you see no arrows, zoom out and inspect a section where the averages genuinely cross. If you see arrows but disagree with the exact bar, check whether you expected an intrabar cross. This version intentionally waits for the completed bar.

To create alert-ready conditions, you don’t need to turn the indicator into a strategy. In NinjaTrader’s Alerts window, build a condition around the indicator’s displayed plots or values. Treat the arrow as visual confirmation and the underlying EMA relationship as the condition you test.

Make the colors communicate state, not just decoration

The ColorPlotsByTrend option sets both plots green when fast EMA is at or above slow EMA, and red when it is below. That makes trend state obvious, but it removes the fixed blue/orange identity of each average.

Use the toggle based on how you read charts:

  • Trend-state reading: leave it on. You care most about whether fast is above or below slow.
  • Line-identity reading: turn it off. You care which individual average is reacting first.
  • Signal-only reading: keep fixed colors and use the arrows as the decision markers.

A useful next modification is a third input called ShowArrows. Wrap the two Draw calls in if (ShowArrows) so you can study the EMAs without signals. Make that change only after the base version works, because each added option expands the testing surface.

For a broader explanation of why moving-average choice changes responsiveness, read EMA vs. SMA. If you later want to pair the crossover with a momentum confirmation, the combined RSI and MACD indicator guide shows how to keep multiple conditions explicit rather than implied.

Common mistakes that stop a first custom indicator

❌ Mistake: Adding plots inside OnBarUpdate(). That method runs repeatedly, so plots must be declared once during setup. Repeatedly defining chart resources leads to errors or unpredictable behavior.

✅ Do this: Put AddPlot in State.SetDefaults, then write values to Values[0][0] and Values[1][0] in OnBarUpdate().

❌ Mistake: Accessing the slow EMA before enough bars exist. A 21-period EMA needs history before its output is meaningful, and series indexing becomes unsafe when the script asks for bars that aren’t there.

✅ Do this: Keep the if (CurrentBar < SlowPeriod) return; guard at the top of OnBarUpdate(). If you add a 50-period filter, change the guard to the largest period you use.

❌ Mistake: Naming the file and class differently. For example, creating MyEmaSignal.cs while the public class remains EmaCrossoverSignals makes scripts harder to locate and can create namespace confusion.

✅ Do this: Create the script with the final name in the NinjaScript Editor, then keep the file name, public class name, and indicator list name aligned.

❌ Mistake: Expecting an indicator to place an order. Arrows and alert conditions are chart output. They don’t call EnterLong(), EnterShort(), or manage a position.

✅ Do this: Keep visual logic in the indicator. When you are ready to define orders, stops, and targets, move the conditions into a strategy and test the order behavior separately.

❌ Mistake: Using Calculate.OnEachTick just because it feels faster. It can print a crossover during the open bar that’s gone by the close.

✅ Do this: Use Calculate.OnBarClose for confirmed-bar signals. If you switch to tick updates, label the output clearly as intrabar and test it in real-time playback.

Pro tips for turning the basic build into a useful chart tool

Match your guard to your longest lookback. Add an EMA 200 trend filter later? Use if (CurrentBar < 200) return;, not the original 21-bar guard. This prevents a filter from reading incomplete values.

Keep signal tags unique. The CurrentBar suffix means every arrow has an identity. If the same static tag is used for all bullish crosses, NinjaTrader updates one drawing object instead of preserving historical signals.

Add one filter at a time. For example, require the close to be above a 200 EMA for bullish arrows. Compile, reload the chart, and compare the arrow count before adding an RSI threshold. That sequence makes debugging possible.

Use alerts after the visual test. First verify the plotted averages and arrows on historical bars. Then create an alert condition for the EMA relationship. This prevents you from troubleshooting alerts when the underlying indicator logic is wrong.

Don’t confuse a plotted crossover with a trade plan. The signal identifies a state transition. A trade plan still needs session rules, entry timing, invalidation, sizing, and an exit rule. If you’re validating those rules, backtesting metrics explained gives you the vocabulary to inspect the result instead of judging it from a few chart examples.

Generating this indicator without writing the code yourself

You can build the same artifact in HorizonAI by describing the indicator precisely. HorizonAI generates NinjaScript indicators from plain-English chat, compiles the code, and lets you edit the result in the conversation or its browser editor. It writes the code; you compile it in NinjaTrader and decide how, or whether, to use it on a chart.

Create a NinjaTrader 8 NinjaScript indicator named EmaCrossoverSignals. Overlay it on price. Add user inputs FastPeriod default 9, SlowPeriod default 21, and ColorPlotsByTrend default true. Plot both EMAs. In OnBarUpdate, wait until SlowPeriod bars exist, assign the EMA values to plots, color both plots lime green when fast EMA is at or above slow EMA and red otherwise. Draw one lime-green up arrow 2 ticks below the low when the fast EMA crosses above the slow EMA, and one red down arrow 2 ticks above the high when it crosses below. Use Calculate.OnBarClose and include the required using statements.

If you already have a rough script, use a tighter edit request:

Edit this NinjaScript indicator: add a boolean ShowArrows input that defaults to true, and only execute the Draw.ArrowUp and Draw.ArrowDown calls when ShowArrows is enabled. Keep all existing plots and inputs unchanged.

The result is compile-checked NinjaScript you can inspect and revise before pasting into NinjaTrader. Try it free →

FAQs

How do I create an indicator in NinjaTrader 8 without learning all of C#?

Use the NinjaScript Editor to create an Indicator script, then start from a complete working class and change one input or condition at a time. You still need to understand what the indicator calculates, but you don’t need to memorize the entire framework before building something useful.

Why does my custom NinjaTrader indicator not show in the Indicators list?

Compile it first with F5 and read the Error tab. Check that the public class is inside the NinjaTrader.NinjaScript.Indicators namespace and that its name matches the script file you created.

Should an EMA crossover indicator calculate on each tick or on bar close?

Use bar close when you want signals confirmed by a completed candle. Use each tick only when you accept that an open-bar crossover can appear and disappear before the candle closes.

Can this indicator place trades automatically?

No. This indicator only plots values and arrows. A NinjaTrader strategy is the script type that contains order-entry and position-management logic.

Final thoughts

A custom NinjaTrader indicator becomes manageable once you separate its jobs: declare settings and plots in OnStateChange(), create supporting indicators when data loads, and calculate or draw in OnBarUpdate(). The 9/21 crossover is deliberately simple, which makes it a dependable foundation for more selective conditions.

One concrete next step: add a 200 EMA filter, then only draw bullish cross arrows when Close[0] > EMA(200)[0]. Make that modification in isolation and compare the chart before you add any second filter.

Related articles

Questions about custom NinjaTrader indicators? Join our Discord to discuss with other traders!