Education / Platform & Tools 37 Nov 5, 2026

NinjaScript for Beginners: Writing a First Indicator in NinjaTrader 8

NinjaScript is C# that NinjaTrader 8 compiles. A walk-through from the Control Center menu to a working indicator that plots the prior session's high and low: the wizard, OnStateChange and OnBarUpdate, the code line by line, compiling, and the errors almost everyone hits first.

NinjaScript for Beginners: Writing a First Indicator in NinjaTrader 8

NinjaScript is the language of NinjaTrader 8's indicators, strategies and add-ons, and it's plain C#. NinjaTrader compiles it into a .NET assembly, so a script runs as compiled code, and the editor, a code generator and the compiler all ship inside the platform. This NinjaScript tutorial builds one small, useful indicator from an empty editor: a pair of lines at the prior session's high and low. If your platform isn't set up yet, start with the NinjaTrader 8 chart setup; if you only want to install someone else's script, importing indicators and add-ons is the shorter path.

1. Open the NinjaScript Editor

In the Control Center, open the New menu and choose NinjaScript Editor. The right side is the NinjaScript Explorer, a folder tree of every script on the machine: Indicators, Strategies, AddOns and the rest. The built-in indicators are in there too, with their source code, which makes them the best reference you'll have.

2. Let the wizard write the skeleton

Click the + on the editor's tab row (or right-click the Indicators folder and choose New...). The wizard walks through Welcome, General, Default Properties, Additional Data, Additional Event Methods, Input Parameters, Plots and Lines, and Finish. For this indicator:

  1. General: name it MyPriorDayLevels.
  2. Default Properties: leave Calculate on On bar close and tick Overlay on price so the lines draw on the price panel.
  3. Input Parameters: add an int called OffsetTicks, default 0.
  4. Plots and Lines: add two plots, PriorHigh and PriorLow.
  5. Press Generate.

The wizard writes a file to Documents\NinjaTrader 8\bin\Custom\Indicators and opens it. You can't return to the wizard after Generate, so from here on you edit code. Skipping the wizard pages and pasting the class below over the generated one works too, as long as you keep the using lines the wizard put at the top.

3. The two methods that run everything

Every indicator lives in two methods. OnStateChange() is called as the script moves through its life, and State tells you where it is:

StateWhat belongs there
SetDefaultsName, Calculate, IsOverlay, default inputs, AddPlot(). Keep it light: NinjaTrader also runs it to fill the indicator list.
ConfigureExtra data series with AddDataSeries(), settings that override the UI
DataLoadedAnything that needs bars loaded: class-level objects, custom series, other indicators
Historical, Transition, RealtimeThe script is working through history, switching to live data, then running live
TerminatedClean-up

OnBarUpdate() is the calculation, called for each bar. Calculate decides how often in real time: once per closed bar, on every tick, or on every price change. On historical bars it runs once per bar close whatever the setting, unless Tick Replay is enabled, which is why an indicator that looks fine on history can behave differently live on OnEachTick.

4. The code

Below the using block, the whole indicator is this:

namespace NinjaTrader.NinjaScript.Indicators
{
    public class MyPriorDayLevels : Indicator
    {
        private double sessionHigh, sessionLow;   // the session in progress
        private double priorHigh, priorLow;       // the last finished session
        private int    sessionsSeen;              // session starts since the chart loaded
        private int    lastSessionBar = -1;       // guards against a double roll on OnEachTick

        protected override void OnStateChange()
        {
            if (State == State.SetDefaults)
            {
                Description              = "Plots the prior session's high and low.";
                Name                     = "MyPriorDayLevels";
                Calculate                = Calculate.OnBarClose;
                IsOverlay                = true;
                DisplayInDataBox         = true;
                DrawOnPricePanel         = true;
                PaintPriceMarkers        = true;
                IsSuspendedWhileInactive = true;
                OffsetTicks              = 0;
                AddPlot(new Stroke(Brushes.Goldenrod, 2), PlotStyle.Hash, "PriorHigh");
                AddPlot(new Stroke(Brushes.SteelBlue, 2), PlotStyle.Hash, "PriorLow");
            }
        }

        protected override void OnBarUpdate()
        {
            if (Bars.IsFirstBarOfSession && CurrentBar != lastSessionBar)
            {
                priorHigh      = sessionHigh;
                priorLow       = sessionLow;
                sessionHigh    = High[0];
                sessionLow     = Low[0];
                lastSessionBar = CurrentBar;
                sessionsSeen++;
            }

            sessionHigh = Math.Max(sessionHigh, High[0]);
            sessionLow  = Math.Min(sessionLow, Low[0]);

            // The first session on a chart is usually partial, so the first
            // complete "prior session" exists from the third session start on.
            if (sessionsSeen < 3)
                return;

            PriorHigh[0] = priorHigh + OffsetTicks * TickSize;
            PriorLow[0]  = priorLow  - OffsetTicks * TickSize;
        }

        #region Properties
        [NinjaScriptProperty]
        [Range(0, int.MaxValue)]
        [Display(Name = "Offset (ticks)", Order = 1, GroupName = "Parameters")]
        public int OffsetTicks { get; set; }

        [Browsable(false)]
        [XmlIgnore]
        public Series<double> PriorHigh { get { return Values[0]; } }

        [Browsable(false)]
        [XmlIgnore]
        public Series<double> PriorLow { get { return Values[1]; } }
        #endregion
    }
}

AddPlot() creates a value series per plot, reachable as Values[0] and Values[1]; the two properties at the bottom only give them readable names. Assigning PriorHigh[0] sets the plot's value on the current bar, and [0] always means "this bar", [1] the bar before it.

The session logic hangs on Bars.IsFirstBarOfSession. It's true on the first bar of each session as the chart's trading hours template defines it, so "prior day" follows the template. On NQ with the electronic-hours (ETH) template, a session runs from 6:00 PM to 5:00 PM ET (midnight to 23:00 CET), and the lines include the overnight high and low. Switch to the built-in RTH template and the session starts at the 9:30 AM ET open, but NinjaTrader's version runs to the 5:00 PM ET futures close rather than the 4:00 PM cash close, so a pure 9:30-to-4:00 range needs a custom template. Trading hours templates in NinjaTrader explains the difference and why it moves your levels.

Two details catch beginners. IsFirstBarOfSession is also true on the very first bar the chart loads, even mid-session, which is why the code waits for the third session start before plotting anything. And TickSize is read in OnBarUpdate, not in SetDefaults, because the instrument isn't known until the data is loaded. NinjaTrader has no "step" plot style; PlotStyle.Hash draws a short dash per bar, which reads as a level.

5. Compile and put it on a chart

Press F5. NinjaTrader compiles every NinjaScript file on the machine, not only the one you're editing, and errors from all of them appear in the panel at the bottom of the editor. One broken file anywhere, including an old download, stops the compile; double-click an error to jump to the line, or right-click it to exclude that file from compilation.

On a chart, open Indicators, find MyPriorDayLevels, set the offset and apply. Load the chart by days rather than by a bar count: with "Days" the first loaded bar lines up with a session start. Nothing appears on a daily chart, because IsFirstBarOfSession is always false on non-intraday bars.

When it doesn't work

What you seeUsual causeFix
CS0103, "The name … does not exist in the current context"A typo (CurentBar) or a variable used before it's declaredCheck the spelling against the declaration
CS1002, "; expected"A missing semicolonAdd it on the line shown
CS0101, the namespace "already contains a definition"You copied a script and changed Name but not the class nameRename the class too
Log tab: an index out-of-range errorClose[1] or similar on the first barStart OnBarUpdate with if (CurrentBar < 1) return;
No lines at allFewer than three sessions loaded, or a daily chartLoad more days, use an intraday chart

Runtime errors don't appear in the editor; they go to the Log tab of the Control Center, and NinjaTrader usually disables the script on the chart. For your own checks, Print() writes to the NinjaScript Output window (New › NinjaScript Output).

There's a shortcut you should know about. NinjaTrader ships an indicator called Prior Day OHLC, and inside your own code PriorDayOHLC().PriorHigh[0] gives the same number in one line. Writing the session roll yourself is still worth doing once, because the same pattern (detect the session start, carry a value, plot it) sits under most level indicators and under a strategy that trades levels, which is where the line between discretionary and systematic trading starts to blur. The free WotT Session Key Levels indicator I built for this site draws a prior-day high and low of the 9:30-to-4:00 New York session and several more levels without any code, and it began as a NinjaScript file like this one.

Way of the Trader I trade NQ futures on prop firm accounts and write about the process: preparation, rules, platforms and risk. More about me →

Educational content, not investment advice. Futures trading involves substantial risk of loss. Examples are for illustration only. Read the full Risk Disclosure.

Keep going

← All Platform & Tools lessons