Technical Analysis

NinjaScript Basics for Custom Indicators

By Sean Mackey8 min read
NinjaScript Basics for Custom Indicators

NinjaScript lets you build custom indicators in NinjaTrader Desktop using C# and its platform APIs. Start with a small calculation that you can check by hand. An indicator calculates or displays information; automated entries, protective orders and position management require a separate strategy design.

This guide covers indicator lifecycle methods, a moving-average example, data structures, debugging and the boundary between indicator development and advanced order handling. It also explains how to use LuxAlgo for separate research without assuming that code from one platform runs in another.

Understand the NinjaScript Indicator Lifecycle

The correct method name is OnStateChange(). Use it to set defaults and initialize resources at the appropriate stage. NinjaTrader’s lifecycle reference distinguishes configuration, data loading, historical processing, real-time processing and termination.

Method or stagePurposePractical constraint
State.SetDefaultsName, calculation mode, default parameters and plotsKeep this light; opening a selection dialog can create temporary script instances
State.DataLoadedInitialize objects that depend on loaded dataDo not access data-dependent objects before they are ready
OnBarUpdate()Bar-based calculationsFrequency depends on Calculate; guard historical indexes
OnMarketData() / OnMarketDepth()Market-data or depth events when availableNot required for a basic moving average; data availability matters
OnRender()Custom renderingKeep calculation and drawing responsibilities clear; manage graphics resources
State.TerminatedCleanupDispose only resources the script owns and account for partial initialization

The OnBarUpdate reference describes an update event, not an unconditional once-per-new-bar callback. With Calculate.OnBarClose, calculations run at bar close. OnEachTick and OnPriceChange have different timing, and ordinary historical bars do not contain every original tick.

OnPriceChange can miss volume updates at an unchanged price. Historical intrabar reconstruction requires suitable data and configuration, such as Tick Replay where applicable; changing Calculate alone does not recreate missing ticks. A real-time calculation mode does not automatically prevent repainting, look-ahead errors or bad data.

Build a Small Moving-Average Indicator

Create a new indicator named StudyMean in the NinjaScript Editor. Use the editor’s generated structure and imports; replace its indicator class with the example below, keeping the surrounding NinjaTrader.NinjaScript.Indicators namespace and any editor-managed generated code. Include System.ComponentModel.DataAnnotations and System.Windows.Media among the imports if they are not already present.

This educational example recomputes the mean of the most recent input values at each bar close. It favors an easily audited loop over a rolling accumulator. It is not a trading strategy, and you should compile it and compare its output in your installed NinjaTrader version before relying on it.

public class StudyMean : Indicator
{
    [NinjaScriptProperty]
    [Range(1, int.MaxValue)]
    [Display(Name = "Period", Order = 1, GroupName = "Parameters")]
    public int Period { get; set; }

    protected override void OnStateChange()
    {
        if (State == State.SetDefaults)
        {
            Name = "StudyMean";
            Description = "A simple mean of the selected input.";
            Calculate = Calculate.OnBarClose;
            IsOverlay = true;
            Period = 20;
            BarsRequiredToPlot = 0;
            AddPlot(Brushes.DodgerBlue, "Mean");
        }
    }

    protected override void OnBarUpdate()
    {
        if (Period < 1 || CurrentBar < Period - 1)
            return;

        double sum = 0.0;
        for (int barsAgo = 0; barsAgo < Period; barsAgo++)
            sum += Input[barsAgo];

        Value[0] = sum / Period;
    }
}

AddPlot() creates the plot and its associated values series. The example writes the current result to Value[0]. It uses Input, so compare it with a built-in SMA using the same selected input, period, instrument, interval and trading-hours template.

CurrentBar is zero-based: with Period equal to 3, index 2 is the first point with three available observations. The loop reads offsets 0, 1 and 2. It never reads offset 3 on that first valid calculation. Returning during warm-up deliberately avoids computing a partial-window average; the plot-display setting is not a substitute for the indexing guard.

For input values 10, 12, 14 and 16, a three-value mean first equals 12, then 14. With Period equal to 1, the output equals the current input. These are useful checks before comparing a longer chart. A built-in SMA may show partial-window values during warm-up, so compare the full-window portion.

The earlier accumulator pattern added the current close, divided too early and accessed Close[Period] when only Period bars were available. A rolling implementation can be efficient, but its initialization and removal of the outgoing value must be correct. Intrabar repeated calls also require different treatment from once-per-bar accumulation.

Choose Data Structures for the Calculation

StructureUseful forWhat to watch
Series<double>Values aligned with bar history and bars-ago accessInitialize at the right lifecycle stage and choose suitable history retention
ArrayA fixed-size buffer or indexed collectionCheck bounds and define how elements map to bars
List<T>A collection whose length changesControl growth and repeated allocations
Dictionary<TKey,TValue>Lookup by a meaningful keyAvoid unbounded caches and assumptions about thread safety

There is no universal memory ranking that makes one collection best for every indicator. Element types, capacity, retained history and access patterns determine cost. A dictionary declared but never read does not improve performance. For an ordinary bar-aligned output, use the platform’s series model rather than maintaining a second unsynchronized history.

Debug the Indicator Before Extending It

  • Compile and resolve the first reported error before chasing later messages that may be consequences of it. Check names, imports and the exact NinjaTrader version of copied examples.
  • Use targeted Print() messages with the current bar and relevant values. Reproduce one failing case; continuous output on every tick can obscure the cause and slow the script.
  • Check Period = 1, insufficient history, a normal window and reloading the chart. Compare the same input and full-window bars against the built-in SMA.
  • Keep defaults lightweight and initialize data-dependent objects after loading. Null checks should handle a known lifecycle condition, not hide unexplained failures.
  • If adding multiple series, filter BarsInProgress and verify enough bars in every series used. CurrentBar for one series does not establish availability in another.

Catch exceptions only where you can handle them meaningfully. Logging an exception and continuing with a stale or partially calculated output can conceal a faulty indicator. Resolve the underlying index, initialization or data assumption instead of treating a broad try/catch as validation.

The loop above performs Period additions per update. For modest windows it is easy to inspect; larger windows or many charts may justify a tested rolling sum or the built-in indicator. Measure first, reduce unnecessary rendering and allocations, and confirm optimized output remains equivalent. Reading one SMA result into a variable avoids repeating that read within your logic but is not evidence of a major performance gain.

Keep Orders and Position Management in a Strategy

The NinjaScript Strategy API contains strategy-specific order and position methods. Do not paste those methods into the simple Indicator class above. A plotted level is not a submitted protective order, and a valid indicator does not supply a complete entry, exit or execution policy.

NinjaTrader’s order-method documentation separates managed and unmanaged approaches; they cannot be mixed in one approach. SetStopLoss() is a managed method. It is not an example of unmanaged order submission. The unmanaged approach gives more control and requires explicit handling of order relationships and failure cases.

Trailing levels, breakeven rules and scaling can be strategy features, but each needs rules for updating orders, rejected changes, fills, cancellations and reconnection. A number subtracted from price is a price distance, not automatically a number of ticks. Define instrument tick size and tick value, and verify what each method’s units mean.

Treat Fills as Executions, Not Remaining Quantity

For NinjaTrader 8, OnExecutionUpdate() is the strategy callback for incoming executions. One order can have several fills. Its quantity parameter describes that execution; the unfilled quantity of an order is not the strategy’s position size. The older OnExecution(ExecutionEventArgs e) pattern and an undefined UpdatePositionMetrics() function are not a working NinjaTrader 8 solution.

Track actual fills and distinguish strategy position from account position. Protect the quantity actually filled, including when an order is cancelled after filling in part. Follow the official advanced-order examples for event ordering and provider considerations; do not assume a market-data price event updates the order ledger.

Define Sizing Separately from the Indicator

ATR can inform a planned adverse distance, but it does not make risk constant or guarantee a stop fill. For a hypothetical contract worth $10 per point, a five-point planned distance represents $50 per contract before fees and slippage. A $200 allowance would permit four contracts under those simplified assumptions; a ten-point distance would permit two. Margin, liquidity, minimum size and worse fills can impose tighter limits.

A complete strategy also needs explicit entry timing, costs, session rules and historical fill assumptions. Test it in a suitable simulation workflow before considering live use. Indicator accuracy and profitable strategy performance are different questions.

Use LuxAlgo for Separate Strategy Research

In LuxAlgo’s native platform, explore the Library and supported chart tools to investigate an indicator idea. Keep a written specification of inputs, warm-up rules and signal timing so a comparison across platforms has a clear meaning.

Current LuxAlgo native workspace. This is a separate research interface, not NinjaTrader or evidence that NinjaScript runs in LuxAlgo.

Ask Quant, our coding agent to express a supported strategy idea in the native workflow. Inspect the generated code and run it manually. Check strategy settings, costs and individual trades, then evaluate later data excluded from tuning.

Do not assume a Library script is a ready-to-import NinjaTrader indicator. Verify the language, platform and license of the specific implementation. Porting an idea requires a deliberate rewrite and output comparison, not merely copying code.

Organize native research in a LuxAlgo workspace. This interface demonstration does not compile NinjaScript or show a trading result.

Continue with the Official References

Use the current NinjaTrader Desktop developer documentation for exact method signatures and version-specific behavior. Start with lifecycle, plotting and bar-update references before exploring strategies. Community code and video tutorials can be useful, but check their platform version, dependencies and license and validate the behavior yourself.

Frequently Asked Questions

Is NinjaScript a separate language from C#?

NinjaScript uses C# with NinjaTrader-specific APIs and conventions for indicators, strategies and other platform extensions.

Does OnBarUpdate run only once per bar?

Its frequency depends on Calculate. OnBarClose runs at bar close; tick and price-change modes behave differently, and ordinary historical bars do not recreate every original tick.

Why does the moving-average example check CurrentBar?

CurrentBar is zero-based. A period of N requires at least N observations before accessing offsets zero through N minus one.

Can SetStopLoss be used as an unmanaged indicator example?

No. SetStopLoss is a managed strategy method, not unmanaged order submission in a basic indicator. Order handling requires an appropriate strategy design.

Can I copy a LuxAlgo script directly into NinjaTrader?

Do not assume compatibility. Check the specific script’s language, platform and license; a port requires a rewrite and validation of its calculations and timing.

Learn to trade smarter.

Market analysis and techniques that build your edge, one email a week.

Don’t worry, no spam here. See our privacy policy for more info.

Read next