◆ New — Strategy Validator, Code Fixer & Prop Compliance Checker are live. Try one free →
Home / Learn / Setting Stop-Loss and Profit Target in NinjaScript
NinjaScript

Setting Stop-Loss and Profit Target in NinjaScript

How to attach protective exits in NinjaScript using SetStopLoss and SetProfitTarget in State.Configure, when to switch to per-entry ExitLongStopMarket/ExitShortStopMarket, and how the managed and unmanaged approaches differ. This article is educational and describes API behavior only; it makes no claim about profitability and is not trading advice.

How do I set a stop-loss and profit target in NinjaScript?

In the managed approach, the simplest way to attach protective exits is to call SetStopLoss and SetProfitTarget once inside OnStateChange, typically in State.Configure. Once set there, NinjaScript automatically submits the corresponding stop and target orders every time an entry fills, offset from the average entry price. You do not re-issue them on every bar.

protected override void OnStateChange()
{
    if (State == State.SetDefaults)
    {
        Name = "ExampleWithExits";
        Calculate = Calculate.OnBarClose;
    }
    else if (State == State.Configure)
    {
        // 40-tick stop, 80-tick target, applied to every entry
        SetStopLoss(CalculationMode.Ticks, 40);
        SetProfitTarget(CalculationMode.Ticks, 80);
    }
}

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

    if (Close[0] > Open[0] && Position.MarketPosition == MarketPosition.Flat)
        EnterLong();
}

Because the exits are declared in configuration, they persist for the life of the strategy and are managed for you. The CalculationMode argument controls how value is interpreted — Ticks, Pips, Percent, Currency, or Price (where value is the literal price rather than an offset).

Why does a stop even matter in a backtest?

A strategy without a defined stop has undefined risk, and that shows up first in your test results, not just in a live account. When there is no protective exit, a single adverse move can produce an arbitrarily large open loss that the backtest happily absorbs, flattering the equity curve while hiding the true worst case. The maximum adverse excursion of each trade is exactly the number a stop is meant to bound.

There is a deeper reason a stop matters for test integrity. A stop order is triggered intrabar — price only has to touch the stop level, not close beyond it. A bar-based backtest that fills on close cannot see that intrabar touch, so the way your stop is modeled directly affects whether the test is honest. This is why stop handling is central to [why most backtests fail](/learn/why-most-backtests-fail): a strategy that looks calm on closing prices can be far more turbulent tick by tick.

Defining the stop up front also makes the strategy analyzable. With a fixed stop and target you can reason about the reward-to-risk geometry of each trade and compare it against how fills actually occurred. None of this implies the strategy is profitable — it simply makes the risk explicit and measurable rather than open-ended.

State.Configure vs per-entry exits: which should I use?

Use SetStopLoss/SetProfitTarget in State.Configure when your protective distances are static — the same tick, currency, or percent offset for every trade. It is the least error-prone path because NinjaScript owns the order lifecycle: it submits, tracks, and cancels the stop and target for you.

Use per-entry exit methods when the stop or target must be dynamic — computed from an indicator, a swing low, or an ATR that changes per trade. ExitLongStopMarket and ExitShortStopMarket (and the limit variants for targets) let you place an exit at a computed price and update it as the trade progresses:

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

    if (Position.MarketPosition == MarketPosition.Long)
    {
        // Trail the stop to the lowest low of the last 5 completed bars
        double stopPrice = MIN(Low, 5)[1];
        ExitLongStopMarket(0, true, Position.Quantity, stopPrice, "stopLong", "");
    }
}

Note the exit references MIN(Low, 5)[1] — the completed bars, not the forming bar 0 — so the stop level does not shimmer intrabar. You can mix approaches, but do not set a static SetStopLoss and manage the same exit manually, or you will end up with conflicting or duplicate orders.

What is the difference between managed and unmanaged approaches?

NinjaScript offers two order-handling models. The managed approach (the default) provides high-level methods — EnterLong, EnterShort, SetStopLoss, SetProfitTarget, ExitLongStopMarket, and friends — and enforces protective rules: it prevents you from accidentally entering opposite positions, and it auto-cancels linked stop/target orders when a position closes. This is the right choice for most strategies and is what makes the State.Configure pattern above work seamlessly.

The unmanaged approach, enabled by setting IsUnmanaged = true in State.Configure, hands you raw order submission through SubmitOrderUnmanaged. You gain full control over every order — useful for complex scaling or custom OCO logic — but you lose the automatic safeguards. You must cancel your own stops and targets, track order references yourself, and handle partial fills manually. In unmanaged mode, SetStopLoss and SetProfitTarget are not available; you submit protective orders explicitly.

For the vast majority of stop-and-target needs, the managed approach is safer and clearer. Reach for unmanaged only when you have a concrete requirement the managed methods cannot express, and expect to write substantially more bookkeeping code to remain correct.

How do I avoid repaint when the stop is dynamic?

A dynamic stop is only as trustworthy as the data it is computed from. If you build the stop price from an indicator on the forming bar under Calculate.OnEachTick, the level will move every tick, and the exit that appears in a historical test may not match what would have happened live. That mismatch is a form of [repainting](/learn/repainting): the modeled exit price never actually existed at decision time.

The safe pattern is to compute exit levels from closed bars and, where appropriate, update them once per bar:

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

    if (Position.MarketPosition == MarketPosition.Long && IsFirstTickOfBar)
    {
        // ATR read on completed data, applied to the confirmed close
        double atr = ATR(14)[1];
        double stopPrice = Close[1] - 2.0 * atr;
        ExitLongStopMarket(0, true, Position.Quantity, stopPrice, "atrStop", "");
    }
}

Referencing ATR(14)[1] and Close[1] inside an IsFirstTickOfBar block anchors the stop to settled values. The order still executes intrabar when price reaches the level — which is correct — but the level itself is fixed on confirmed data, so a tick replay reproduces the same behavior your bar test showed.

How do I verify my stop logic is sound?

Two checks separate a robust exit from a deceptive one. First, confirm the stop and target are actually attached: after an entry fills, NinjaTrader should show the working protective orders, and you should see them cancel automatically when the position closes (in managed mode). If they linger, you have an orphaned-order bug — common when mixing static SetStopLoss with manual exits on the same position.

Second, confirm the exits behave the same under bar-based testing and tick replay. A stop is an intrabar event; if a strategy's results shift dramatically once you replay ticks, the fill assumptions in your bar test were optimistic. Any exit whose price changes between the two runs points to a level computed from unconfirmed data.

Because stop-related mistakes — orphaned orders, intrabar-computed levels, conflicting managed and manual exits — are hard to spot by reading code, ForexCodes offers a free analyzer that flags them statically. Paste your strategy into the [free NinjaScript validator](/ninjascript) to check that your exits reference confirmed bars and that no exit level repaints. As always, sound stop handling bounds risk and keeps the test honest; it says nothing about whether the strategy makes money, and nothing here is trading advice.

Key takeaways

Educational only — not financial advice. Trading involves substantial risk of loss.

Catch the bug that compiles.Run auditGet Pro