NinjaScript Trailing Stop Strategy: Code It Step by Step

NinjaScript Trailing Stop Strategy: Code It Step by Step

By HorizonAI Team · 13 min read · Intermediate

How to Code a Trailing Stop Strategy in NinjaScript

A trailing stop belongs in the strategy when the exit rule is part of the trade thesis. If a long entry should survive ordinary pullbacks but exit after price gives back 16 ticks from its best favorable price, put that behavior in NinjaScript rather than trying to recreate it manually with an ATM template.

Short answer: call SetTrailStop() with a unique entry signal name before the matching EnterLong() or EnterShort() call. For a fixed 16-tick trail, use CalculationMode.Ticks, keep the signal name identical on the entry and stop methods, and do not pair that same signal with SetStopLoss(). The strategy below applies those rules to a 9/21 EMA crossover and optionally adds a 32-tick profit target.

A strategy-managed trail is useful when you want the same exit behavior in every historical test and every live strategy run. It is not an order that predicts where a market will turn. It is a rule that tightens only in the favorable direction after a position exists. NinjaTrader’s SetTrailStop reference documents the method’s managed-order behavior and its calculation modes.

What you will build

The artifact is a NinjaTrader 8 strategy with these deliberate defaults:

ComponentDefaultJob
Fast EMA9 barsDetects short-term direction
Slow EMA21 barsFilters the crossover
Trailing stop16 ticksFollows favorable movement after entry
Profit target32 ticksOptional 2:1 target relative to the initial trail distance
CalculationOn bar closeKeeps the first test reproducible and easier to inspect
Entries1 per directionAvoids multiple entries attaching to the same exit logic

The entry is intentionally plain. A fast EMA crossing above a slow EMA sends a long order; the reverse crossover sends a short order. The useful part is the exit wiring: the long trail is attached to LongEntry, and the short trail is attached to ShortEntry.

A fixed stop starts at a price and stays there. A trailing stop starts a specified distance from price, then ratchets only as the market makes new favorable movement. For a long position, a 16-tick trail moves upward as price advances and never loosens downward. For a short position, it moves downward as price falls and never loosens upward.

Prerequisites and order-management rules

Create a new Strategy in the NinjaTrader 8 NinjaScript Editor, replace its generated class with the code below, then compile it. Add it to a chart or open Strategy Analyzer after it compiles.

Before pasting code, understand three managed-approach constraints:

  1. Set the protective order before the entry. SetTrailStop() establishes the template that NinjaTrader will submit after the matching entry fills. Calling it before EnterLong() or EnterShort() makes the association unambiguous.
  2. Match the signal names exactly. SetTrailStop("LongEntry", ...) only manages an entry placed as EnterLong(..., "LongEntry"). A spelling mismatch creates a stop template for a signal that never fills.
  3. Do not use SetStopLoss() for that same signal. NinjaTrader documents that when both methods are used for one entry signal, the stop-loss order takes precedence over the trailing stop. Use one protective-stop mechanism per signal. The managed approach and its signal-based order handling are covered in NinjaTrader’s managed approach documentation.

The code uses CalculationMode.Ticks, which is usually the clearest choice for futures and other instruments where your risk distance is naturally expressed in ticks. A 16-tick trail means 4.00 points on an instrument with a 0.25-point tick size, but it means a different point distance on an instrument with another tick size. That is exactly why ticks are safer than hard-coding a point conversion.

Paste this complete NinjaScript strategy

#region Using declarations
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using NinjaTrader.Cbi;
using NinjaTrader.NinjaScript;
using NinjaTrader.NinjaScript.Indicators;
#endregion

namespace NinjaTrader.NinjaScript.Strategies
{
    public class EmaCrossoverTrailingStop : Strategy
    {
        private EMA fastEma;
        private EMA slowEma;

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

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

        [NinjaScriptProperty]
        [Range(1, int.MaxValue)]
        [Display(Name = "Trailing stop (ticks)", Order = 3, GroupName = "Risk")]
        public int TrailStopTicks { get; set; }

        [NinjaScriptProperty]
        [Display(Name = "Use profit target", Order = 4, GroupName = "Risk")]
        public bool UseProfitTarget { get; set; }

        [NinjaScriptProperty]
        [Range(1, int.MaxValue)]
        [Display(Name = "Profit target (ticks)", Order = 5, GroupName = "Risk")]
        public int ProfitTargetTicks { get; set; }

        protected override void OnStateChange()
        {
            if (State == State.SetDefaults)
            {
                Description = "EMA crossover strategy with signal-specific trailing stops.";
                Name = "EmaCrossoverTrailingStop";
                Calculate = Calculate.OnBarClose;
                EntriesPerDirection = 1;
                EntryHandling = EntryHandling.AllEntries;
                IsExitOnSessionCloseStrategy = true;
                ExitOnSessionCloseSeconds = 30;
                BarsRequiredToTrade = 21;

                FastPeriod = 9;
                SlowPeriod = 21;
                TrailStopTicks = 16;
                UseProfitTarget = true;
                ProfitTargetTicks = 32;
            }
            else if (State == State.DataLoaded)
            {
                fastEma = EMA(FastPeriod);
                slowEma = EMA(SlowPeriod);
            }
        }

        protected override void OnBarUpdate()
        {
            if (CurrentBar < Math.Max(FastPeriod, SlowPeriod))
                return;

            // Keep one position at a time so each entry has one clear exit template.
            if (Position.MarketPosition != MarketPosition.Flat)
                return;

            if (CrossAbove(fastEma, slowEma, 1))
            {
                // Configure exits before submitting the named long entry.
                SetTrailStop("LongEntry", CalculationMode.Ticks, TrailStopTicks, false);

                if (UseProfitTarget)
                    SetProfitTarget("LongEntry", CalculationMode.Ticks, ProfitTargetTicks);

                EnterLong(1, "LongEntry");
            }
            else if (CrossBelow(fastEma, slowEma, 1))
            {
                // Configure exits before submitting the named short entry.
                SetTrailStop("ShortEntry", CalculationMode.Ticks, TrailStopTicks, false);

                if (UseProfitTarget)
                    SetProfitTarget("ShortEntry", CalculationMode.Ticks, ProfitTargetTicks);

                EnterShort(1, "ShortEntry");
            }
        }
    }
}

This is a full strategy, not a fragment. State.SetDefaults defines the settings visible in the strategy panel. State.DataLoaded instantiates the two EMA indicators. OnBarUpdate() waits until enough bars exist, blocks new entries while a position is open, then configures a signal-specific trail and target immediately before submitting the entry.

The final false in SetTrailStop() means the trail is not simulated. NinjaTrader manages the order according to its strategy order handling. Keep that choice while learning because it makes the behavior easier to audit in the Orders and Executions tabs. If you change it, test the result in simulation before making any judgment from a backtest.

Set the trail in ticks, points, or percent

SetTrailStop() accepts a calculation mode, which determines how NinjaTrader reads the numeric value. The same method can express three different ideas:

ModeExampleBest fitWatch for
CalculationMode.Ticks16Futures and tick-defined riskTick size differs by instrument
CalculationMode.Price4.00A precise price-distance ruleThe number is a distance, not a stop price
CalculationMode.Percent0.50Percentage-based swing logicVerify the platform’s percentage interpretation on your test instrument

For this first build, leave it in ticks. A 16-tick stop is easy to relate to the chart’s tick size and to Strategy Analyzer’s trade list. If you later want volatility-adjusted risk, calculate an ATR-derived number and pass the resulting distance into the same method, but round and test it carefully.

The target in the sample is optional because a trailing stop and a fixed target can conflict with the trade’s purpose. A target at 32 ticks gives the strategy a defined maximum winner. Turning it off lets the trailing stop become the only exit, which can retain more of a strong trend but also exposes more open profit to a reversal.

For position sizing and risk-distance thinking, connect this implementation with the framework in Risk Management in Trading. The strategy’s TrailStopTicks input defines an exit distance, not a complete risk plan by itself.

How the long and short trails behave

Assume the instrument has a 0.25-point tick size and the strategy enters long at 5,000.00 with TrailStopTicks = 16.

  • The initial long stop is 4.00 points below the relevant favorable price reference, around 4,996.00 at entry.
  • If price advances to 5,004.00, the trailing stop can advance to around 5,000.00.
  • If price then falls, the stop does not move back down. It remains at its most recently improved level until price reaches it or the position closes another way.

For a short entered at 5,000.00, invert the direction. The initial stop is around 5,004.00. When price falls to 4,996.00, the stop can tighten toward 5,000.00. A bounce does not widen the stop again.

The word “around” matters because fill behavior, bar calculation, and the exact sequence of intrabar prices affect what you see in a report. Do not inspect only the entry and exit arrows. Open the Trade Performance report and look at the executions, then compare them to the bar path available in the tested data.

Run the first backtest without fooling yourself

Open New > Strategy Analyzer, select this strategy, choose an instrument and a sensible date range, then set the same bar type and session template you intend to trade. Start with a 5-minute or 15-minute primary series, then test a second instrument before changing five inputs at once.

The strategy uses Calculate.OnBarClose. That makes the crossover decision at the end of a completed bar. It does not mean every stop fill in a historical test has tick-by-tick realism. On historical bars, NinjaTrader must model events from the data resolution and fill settings available. If you need a closer model of intrabar behavior, investigate a smaller secondary series or higher-resolution testing, then compare the result against the simpler baseline instead of assuming the more detailed run is automatically better.

NinjaTrader’s Calculate documentation explains the difference between OnBarClose, OnEachTick, and OnPriceChange. Moving this strategy to OnEachTick can make a trailing exit respond sooner in real time, but it also changes when your entry signal and all other logic evaluate. Treat it as a new test configuration, not a free accuracy upgrade.

Use the same discipline when you add commissions, slippage, and a restricted session window. The backtesting mistakes to avoid guide is a useful companion for keeping those assumptions visible rather than optimizing around a flattering report.

When to move from OnBarClose to OnEachTick

Start with OnBarClose until you can explain every trade. A crossover system often benefits from bar-close evaluation because the signal is stable and repeatable: no entry appears and disappears during an unfinished bar.

Consider OnEachTick only if the strategy’s design requires intrabar reaction, such as a fast breakout entry or custom logic that tightens the stop beyond the built-in trail. The built-in SetTrailStop() is still often enough for the exit itself. Changing Calculate affects the entire strategy, while the trail’s job is to manage the protective exit after a fill.

If you need multi-series conditions, do not bolt AddDataSeries() onto this exact class without restructuring the BarsInProgress checks. Follow a purpose-built pattern such as building a multi-timeframe strategy in NinjaScript, then attach the trailing-stop template to the entry signal generated on the correct series.

Debug the four failures that waste the most time

The stop never appears or never trails

First, turn on TraceOrders in State.SetDefaults while debugging and inspect the Output window. Next, confirm SetTrailStop() runs before the entry method and the strategy actually receives an entry fill. A trailing stop cannot manage a position that was never opened.

Also check TrailStopTicks. An overly wide value can make the stop look absent on a compressed chart. On a 0.25-point instrument, 160 ticks is 40 points, not 4 points.

The entry fills but the trail attaches to nothing

The signal names must match character for character. The working sequence is SetTrailStop("LongEntry", CalculationMode.Ticks, TrailStopTicks, false); immediately followed by EnterLong(1, "LongEntry");. LongEntry and Long are separate signals, so a template assigned to the first will not manage an entry submitted with the second. Keep the names as string constants if a larger strategy uses them in several methods.

The trail acts like a fixed stop

Look for a competing SetStopLoss("LongEntry", ...) call. NinjaTrader gives the fixed stop-loss method priority when both are set for one signal. Delete the competing stop-loss call, or assign separate named entries to distinct exit designs.

A different cause is testing only a small move. A trailing stop has no reason to improve until price moves favorably by enough distance. Use Market Replay or a clearly trending test segment and inspect the order changes.

Historical results and live behavior do not line up

A historical bar does not expose every tick sequence by default. A bar can contain both a high that would tighten a long trail and a low that would hit it, while the exact order of those events is unknown at the primary bar resolution. More granular data and different fill-resolution settings can change the modeled outcome.

That does not invalidate the strategy. It tells you to separate an entry hypothesis from a fill-model assumption. Document your bar type, trading-hours template, calculate mode, commission setting, and slippage before comparing iterations.

Common trailing-stop mistakes

❌ Mistake: using the same 16-tick trail on every market. Sixteen ticks has a very different monetary and volatility meaning on a micro futures contract, a stock, and a forex instrument.

✅ Do this: begin with a tick distance that you can state in points and currency for the exact instrument, then compare it with the instrument’s normal bar range. Keep position size fixed during that comparison so you know what changed.

❌ Mistake: calling SetTrailStop() after EnterLong() because the code reads like a sequence of actions. The managed approach needs the exit template available when the entry is processed.

✅ Do this: place each SetTrailStop() directly above its matching EnterLong() or EnterShort() call. The visual proximity prevents signal-name drift during edits.

❌ Mistake: combining a fixed stop, a trailing stop, and a manual ExitLongStopMarket() for the same entry signal. You can end up debugging order precedence instead of strategy logic.

✅ Do this: choose one protective-stop owner per entry signal. Use a named long entry and a named short entry, then add custom exit orders only when you can explain which method owns each order.

❌ Mistake: optimizing the trail from 12 to 13 to 14 ticks on one date range. That usually tunes a backtest artifact, not a durable rule.

✅ Do this: test a coarse grid, such as 12, 16, 20, and 24 ticks, across multiple periods and instruments. Prefer a region that behaves consistently over a single sharp peak.

Pro tips for a strategy-managed trail

Use signal names as risk labels. LongEntry and ShortEntry are sufficient here. In a strategy with a pullback entry and a breakout entry, use names such as PullbackLong and BreakoutLong so each can receive its own trail distance and target.

Keep the entry simple while testing exits. The EMA crossover is not presented as a finished trading system. It gives the trailing-stop behavior a repeatable trigger. Once the exit is verified, replace only the entry condition and preserve the signal-management pattern.

Log before you optimize. Enable TraceOrders, use a deliberately small test range, and confirm the submitted stop price changes as expected. A five-trade audit catches more wiring errors than a 10-year optimization run.

Know when an ATM is the better tool. An ATM template can be convenient for discretionary trading. Put the trailing rule in NinjaScript when it must be part of a strategy you compile, test, version, and run with the same named entry logic.

Generating this without writing the code yourself

HorizonAI can generate NinjaScript indicators and strategies from plain-English chat, compile-check the code, and let you revise it in the editor or chat. Describe the order rules precisely, including the calculation mode and signal names, because those details determine whether a trailing stop attaches correctly.

Build a NinjaTrader 8 NinjaScript strategy using a 9 EMA and 21 EMA crossover. Enter one long position when the fast EMA crosses above the slow EMA and one short position on the reverse cross. Before each named entry, use SetTrailStop with CalculationMode.Ticks and a configurable default of 16 ticks. Use the signal names LongEntry and ShortEntry. Add an optional 32-tick profit target tied to the same signal, calculate on bar close, and do not use SetStopLoss for either signal. Include inputs with Range and Display attributes.

Edit this NinjaScript strategy so the trailing stop is 20 ticks for longs and 24 ticks for shorts, while the profit target is disabled. Keep one entry per direction and explain any order-management changes.

You’ll get compile-checked NinjaScript that you can edit further. HorizonAI writes the strategy code, it does not place or manage trades for you in NinjaTrader. Try it free →

FAQs

Does SetTrailStop() need to be called on every bar?

Not for this fixed-distance strategy. Calling it before each named entry establishes the trail template for that entry. Call it again only when your design intentionally changes the trailing-stop parameters.

Can I use SetTrailStop() and SetStopLoss() together?

Not for the same entry signal if you expect the trail to control the exit. NinjaTrader gives the fixed stop-loss method precedence, so use one protective-stop method per named entry.

Why does my backtest show a stop fill that seems impossible on the chart?

Historical bars summarize a path rather than always providing every tick sequence. Review the data resolution, fill settings, and calculate mode before deciding the trailing-stop logic is wrong.

Is a trailing stop better than a fixed profit target?

They solve different exit problems. A fixed target caps the planned winner, while a trail can stay in a sustained move but gives back some open profit during reversals. Test each with the same entry, dates, costs, and position size.

Final thoughts

A NinjaScript trailing stop works reliably when its order template is attached to the correct named entry before that entry is submitted. Start with CalculationMode.Ticks, one entry per direction, and a trail distance you can translate into the instrument’s actual point and currency risk.

The practical pro tip is simple: verify five sample trades with TraceOrders before changing any optimization input. If the order lifecycle is correct on those trades, you have a sound base for testing better entries, session filters, or a more sophisticated exit.

Related articles

Questions about NinjaScript trailing stops? Join our Discord to discuss with other traders!