How to Convert Pine Script to NinjaScript for NinjaTrader 8

How to Convert Pine Script to NinjaScript for NinjaTrader 8

By HorizonAI Team · 12 min read · Intermediate

How to Convert Pine Script to NinjaScript for NinjaTrader 8

A Pine Script crossover that looks clean on TradingView can’t be pasted into NinjaTrader 8 and expected to run. Pine executes against TradingView’s chart model; NinjaScript is C# inside NinjaTrader’s event-driven framework. The trading idea transfers, but its inputs, historical references, plots, order handling, and bar-update rules need an explicit translation.

Short answer: convert Pine Script to NinjaScript by rebuilding each behavior in NinjaTrader 8, not by translating lines word for word. Map Pine inputs to NinjaScript properties, series history such as close[1] to [1], ta.crossover() to CrossAbove(), plot() to AddPlot() plus Values, and strategy.entry()/strategy.exit() to NinjaTrader order methods and protective-order settings.

The reliable workflow is to first make a one-instrument, one-timeframe NinjaScript version that evaluates on bar close. Then verify the indicator values, then add entries and exits, and finally test fills under NinjaTrader’s settings. The complete EMA crossover strategy below is the reference artifact for the process.

Start with a working Pine Script reference

A conversion has to preserve a defined rule set. “Fast EMA crosses slow EMA” is not enough until you specify periods, calculation timing, position behavior, and protection. This source strategy uses a 9-period EMA crossing a 21-period EMA, evaluates on confirmed bars, enters long only, and attaches a 1% stop and 2% target.

//@version=6
strategy("EMA Crossover Conversion Reference", overlay = true, pyramiding = 0, process_orders_on_close = true)

// Inputs become NinjaScript properties later.
fastLength = input.int(9, "Fast EMA Length", minval = 1)
slowLength = input.int(21, "Slow EMA Length", minval = 2)
stopLossPct = input.float(1.0, "Stop Loss (%)", minval = 0.1, step = 0.1)
targetPct = input.float(2.0, "Profit Target (%)", minval = 0.1, step = 0.1)

fastEma = ta.ema(close, fastLength)
slowEma = ta.ema(close, slowLength)

longSignal = barstate.isconfirmed and ta.crossover(fastEma, slowEma)
exitSignal = barstate.isconfirmed and ta.crossunder(fastEma, slowEma)

if longSignal and strategy.position_size <= 0
    strategy.entry("EMALong", strategy.long)

if strategy.position_size > 0
    stopPrice = strategy.position_avg_price * (1.0 - stopLossPct / 100.0)
    targetPrice = strategy.position_avg_price * (1.0 + targetPct / 100.0)
    strategy.exit("EMAProtect", from_entry = "EMALong", stop = stopPrice, limit = targetPrice)

if exitSignal and strategy.position_size > 0
    strategy.close("EMALong", comment = "Bearish cross")

plot(fastEma, "Fast EMA", color = color.teal, linewidth = 2)
plot(slowEma, "Slow EMA", color = color.orange, linewidth = 2)
plotshape(longSignal, title = "Long signal", style = shape.triangleup, location = location.belowbar, color = color.lime, size = size.tiny)
plotshape(exitSignal, title = "Exit signal", style = shape.triangledown, location = location.abovebar, color = color.red, size = size.tiny)

Pine’s built-ins and namespaces are documented in TradingView’s Pine Script language reference. Read the source as a behavioral specification, not as C# waiting to be reformatted.

Before translating, write down these five facts: the signal uses the bar’s close, the lookback is 9 and 21, there is only one long position, a bearish cross closes it, and the bracket is percentage-based. That list exposes every decision the new script must retain.

Map Pine concepts to NinjaScript 8 equivalents

The table is the fastest way to prevent the usual copy-paste mistakes.

Pine Script conceptNinjaScript 8 equivalentConversion note
input.int() / input.float()public property with [NinjaScriptProperty]Add [Range] and [Display] so it appears in the strategy UI.
close, open, high, lowClose, Open, High, LowNinjaScript series use uppercase names.
close[1]Close[1][0] is the current bar; [1] is one completed bar back.
ta.ema(close, 9)EMA(9)Store the returned indicator in a field if it is used repeatedly.
ta.crossover(a, b)CrossAbove(a, b, 1)The final argument is the lookback window for the crossing test.
ta.crossunder(a, b)CrossBelow(a, b, 1)Use the same lookback consistently.
plot(series)AddPlot() and Values[slot][0]AddChartIndicator() displays a built-in EMA on the chart.
strategy.entry()EnterLong() / EnterShort()Call inside OnBarUpdate() after guards.
strategy.close()ExitLong() / ExitShort()Signal names make order tracing easier.
strategy.exit()SetStopLoss() and SetProfitTarget()NinjaTrader’s managed approach commonly uses tick or price offsets.
alertcondition()drawing, alerts, or order-event logicThere is no one-line, identical strategy alert replacement.

Pine has a series-oriented syntax that reads as though the full chart exists at once. NinjaScript receives calls as bars or ticks update. OnStateChange() is where you declare defaults and initialize indicators, while OnBarUpdate() is where the strategy decides what to do for the current update. NinjaTrader documents the state lifecycle in its OnStateChange reference and describes the update event in its OnBarUpdate reference.

Build the NinjaScript shell before translating signals

Create a new strategy in NinjaTrader 8’s NinjaScript Editor. Give it a class name that contains only letters and numbers, then replace its generated contents with the strategy below. The SetDefaults branch defines what the strategy exposes in the UI. The DataLoaded branch constructs both EMA indicators once, rather than allocating them on every bar.

The example uses ticks for its stop and target because NinjaTrader strategies operate across instruments with different tick sizes. On an instrument with a 0.25-point tick, a 20-tick stop is 5 points. That is deliberately not the same as Pine’s 1% stop, so treat it as an execution-model choice rather than a literal conversion.

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

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

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

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

        [NinjaScriptProperty]
        [Range(1, int.MaxValue)]
        [Display(Name = "Stop Loss (ticks)", Order = 3, GroupName = "Parameters")]
        public int StopLossTicks { get; set; }

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

        protected override void OnStateChange()
        {
            if (State == State.SetDefaults)
            {
                Name = "Pine EMA Crossover Conversion";
                Description = "Long-only EMA crossover translated from Pine Script logic.";
                Calculate = Calculate.OnBarClose;
                EntriesPerDirection = 1;
                EntryHandling = EntryHandling.AllEntries;
                IsExitOnSessionCloseStrategy = true;
                ExitOnSessionCloseSeconds = 30;
                BarsRequiredToTrade = 21;
                FastEmaLength = 9;
                SlowEmaLength = 21;
                StopLossTicks = 20;
                ProfitTargetTicks = 40;
            }
            else if (State == State.Configure)
            {
                SetStopLoss(CalculationMode.Ticks, StopLossTicks);
                SetProfitTarget(CalculationMode.Ticks, ProfitTargetTicks);
            }
            else if (State == State.DataLoaded)
            {
                fastEma = EMA(FastEmaLength);
                slowEma = EMA(SlowEmaLength);
                AddChartIndicator(fastEma);
                AddChartIndicator(slowEma);
            }
        }

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

            if (CrossAbove(fastEma, slowEma, 1) && Position.MarketPosition == MarketPosition.Flat)
                EnterLong("EMALong");

            if (CrossBelow(fastEma, slowEma, 1) && Position.MarketPosition == MarketPosition.Long)
                ExitLong("BearishCross", "EMALong");
        }
    }
}

Calculate.OnBarClose is the closest operational match for a Pine rule gated by barstate.isconfirmed. It waits until the bar closes before evaluating the crossover. If you change it to Calculate.OnEachTick, the strategy can react intrabar and it is no longer equivalent to the reference source. NinjaTrader’s CrossAbove documentation also confirms that its third parameter controls the lookback period.

Translate series and crossover logic without off-by-one errors

The most damaging conversion bug is usually historical indexing. In both languages, [0] means the current bar and [1] means the previous bar, but what “current” means is controlled by calculation timing. With Calculate.OnBarClose, Close[0] is a finished bar. With Calculate.OnEachTick, it is a bar still changing.

Use this direct translation pattern:

Pine:       ta.crossover(fastEma, slowEma)
NinjaScript: CrossAbove(fastEma, slowEma, 1)

Pine:       fastEma > slowEma and fastEma[1] <= slowEma[1]
NinjaScript: fastEma[0] > slowEma[0] && fastEma[1] <= slowEma[1]

Prefer CrossAbove() for the crossover case. It states your intent, handles the two series directly, and is easier to audit. Use explicit indexing when the Pine rule contains more conditions, such as requiring price to close above the 50 EMA on the same bar.

A guard belongs before any indexed access. CurrentBar < Math.Max(FastEmaLength, SlowEmaLength) ensures both EMAs have enough history. If your logic reads Close[5], add that requirement too. This is the NinjaScript counterpart to recognizing that a Pine indicator needs historical bars before its result is meaningful.

For a related build-from-scratch explanation of strategy states, orders, and testing, see our first NinjaTrader 8 strategy tutorial.

Convert plots, shapes, and alerts separately from execution

A Pine plot() is concise because TradingView owns the visual layer. In NinjaScript, built-in indicators can be shown with AddChartIndicator(fastEma), as in the strategy, while a custom calculated series needs AddPlot() during State.SetDefaults and a value assignment in OnBarUpdate().

For example, a Pine histogram such as plot(fastEma - slowEma) becomes a declared plot and an assignment like Values[0][0] = fastEma[0] - slowEma[0];. Put AddPlot() only in the defaults state. Calling it after the script has started is a lifecycle error, not a charting preference.

Pine’s plotshape() becomes a drawing call when you need a visible event marker. A common choice is Draw.ArrowUp(this, "Long" + CurrentBar, false, 0, Low[0] - 2 * TickSize, Brushes.LimeGreen);, with the required drawing namespace added. Keep the tag unique per bar, otherwise a later signal can replace an earlier marker.

alertcondition() also needs a design decision. For an indicator, you may create a visible marker and configure a platform alert around the condition. For a strategy, an entry or exit is itself the event you can inspect in the Orders and Executions tabs. Don’t assume an alert conversion should submit an order, or that an order conversion should create a platform notification.

Match order behavior, not just entry syntax

strategy.entry("EMALong", strategy.long) maps cleanly to EnterLong("EMALong"), but exits deserve more care. The Pine source uses a percentage stop and target derived from actual average entry price. The NinjaScript sample sets a 20-tick stop and 40-tick target before entry. Both are protective brackets, but they are not mathematically identical.

If exact percentage distance is central to the rule, calculate a rounded price from Position.AveragePrice after the position is established and use an appropriate managed or unmanaged order design. That introduces timing and order-modification details, so first prove that the crossover entries match. A tick-based bracket makes the initial migration easier to validate.

Also compare these platform settings before judging results:

  1. Position size and entry handling. Pine’s pyramiding = 0 is represented here by one entry per direction and a flat-position check.
  2. Session template. A futures chart using regular trading hours will produce different bars from a chart using nearly 24-hour electronic hours.
  3. Commission and slippage. Configure them in the relevant platform’s backtest settings, not inside a crossover condition.
  4. Intrabar fills. A stop and target touched within one bar can be resolved differently depending on bar granularity and fill processing.
  5. Instrument mapping. Compare the same contract, expiry or continuous-series setting, exchange session, and timezone.

Those differences explain many apparent logic failures. They are also why a faithful code translation can still yield a different report. Use a controlled backtesting workflow before changing the strategy rules to force a match.

Handle multi-timeframe and TradingView-only features last

Simple OHLCV and moving-average logic moves well. Multi-timeframe Pine code needs an architecture change. request.security() can fetch another symbol or timeframe inside Pine; NinjaScript instead requires AddDataSeries() in State.Configure, then separate BarsInProgress handling and indexed series access. Treat that as a second conversion phase, not an extra line below an EMA call.

For example, a Pine filter based on a 1-hour EMA while trading a 5-minute chart needs you to add the 60-minute series, calculate the higher-timeframe EMA, and only evaluate the entry on the intended primary-series update. A detailed multi-timeframe NinjaScript strategy guide covers that structure.

Some Pine features have no direct NinjaTrader equivalent. TradingView-specific drawing objects, request.financial(), chart-specific sessions, and browser-managed alerts need to be redesigned around NinjaTrader data series, session templates, drawing tools, or alerts. Record each exception in a conversion note. Silent substitutions make later debugging miserable.

Compile, install, and validate the converted strategy

In the NinjaScript Editor, compile the file before putting it on a chart. Fix every error at the line that triggers it. The common causes are a class name not matching the file, a missing namespace for an attribute or drawing tool, a property declared outside the class, or code placed in the wrong state branch.

Next, apply the strategy to the same market and timeframe as the Pine reference. Start with Calculate.OnBarClose, the default 9/21 lengths, and no extra filters. Compare five to ten crossover timestamps visually. Only after they line up should you vary the bar calculation mode, execution settings, or risk model.

A translation is easier when you separate signal validation from performance validation. First ask, “Did both platforms identify the same completed-bar cross?” Then ask, “Did they simulate the same order and fill?” Combining those tests turns one discrepancy into a guessing game.

Common Pine-to-NinjaScript conversion mistakes

Mistake: copying a Pine expression into the NinjaScript editor. ta.ema, plot, and strategy.position_size are Pine APIs, not C# syntax.

Do this: translate each expression to the matching NinjaScript object or method. Create EMA fields in State.DataLoaded, use AddChartIndicator() for display, and inspect Position.MarketPosition for state.

Mistake: using OnEachTick because it sounds more precise. It changes the moment when the crossover can become true and can create entries Pine would never take with confirmed-bar logic.

Do this: begin with Calculate.OnBarClose. Change calculation mode only as an intentional strategy revision, then re-check the exact signal bars.

Mistake: forgetting the bars-required guard. An EMA crossover can access insufficient history at the left edge of the chart.

Do this: return until CurrentBar is at least the longest EMA length. If you use a 200 EMA and a 3-bar confirmation, guard for at least 203 bars.

Mistake: calling a 1% Pine stop the same as a 20-tick NinjaTrader stop. They expand and contract differently as price changes and vary by instrument.

Do this: label the new protection model honestly, then test it on the target instrument’s tick size. Convert to price-based logic only when percentage distance is part of the system’s definition.

Pro tips for a cleaner migration

Name orders consistently. Keep EMALong for the entry and use descriptive exits such as BearishCross. It makes executions readable and lets you relate an exit to its originating entry.

Add one visual check at a time. First show the two EMAs. Next add arrow markers. Then enable orders. A chart that shows correct values is much easier to diagnose than a strategy with several hidden assumptions.

Freeze the data conditions. Match symbol, bar type, timeframe, session template, and timezone before comparing results. For futures, a one-hour session mismatch can change the first bar of the day and move every EMA after it.

Translate the indicator before the strategy when the original is complex. If its plots and crossovers match, wrap the validated logic in entries and exits. This approach also helps when adapting scripts like a combined RSI and MACD indicator.

Generating this conversion without writing the code yourself

HorizonAI can convert Pine Script, MQL5, and NinjaScript in either direction, then compile-check the resulting NinjaScript. Give it the Pine source and specify the execution assumptions you want preserved. It writes the code, but it doesn’t connect to a broker or run trades for you.

Convert this Pine Script v6 long-only EMA crossover strategy to a NinjaTrader 8 NinjaScript strategy. Preserve 9 and 21 EMA inputs, confirmed-bar behavior by using Calculate.OnBarClose, one entry per direction, a flat-position entry guard, and a bearish-cross exit. Replace the Pine 1% stop and 2% target with editable NinjaScript inputs defaulting to a 20-tick stop and 40-tick target. Initialize EMA indicators in State.DataLoaded, use CrossAbove/CrossBelow with lookback 1, add both EMAs to the chart, and include all namespaces and property attributes needed to compile.

You’ll get compile-checked NinjaScript that you can refine in chat or edit in the in-browser Monaco editor. Ask a second time for a percentage-based protective-order version only after you’ve verified the basic crossover signals.

Try it free →

FAQs

Can I paste Pine Script directly into NinjaTrader 8?

No. Pine Script and NinjaScript use different languages and platform APIs. Rebuild the logic in C#, including lifecycle states, indicator initialization, plotting, and order methods.

What is the NinjaScript equivalent of ta.crossover()?

For two indicator series, use CrossAbove(firstSeries, secondSeries, 1). The final 1 checks for a crossing over the most recent bar interval.

Why do my Pine and NinjaTrader EMA crossover results differ?

Check calculation timing, session template, symbol data, bar type, commissions, and intrabar fill assumptions before changing the signal logic. A confirmed close in Pine is best matched initially with Calculate.OnBarClose.

Can NinjaScript use a percentage stop loss like Pine Script?

Yes, but it requires a price calculation based on the actual entry and careful order-update handling. A fixed tick stop is simpler for validating the first translation, especially on futures.

Final thoughts

The correct Pine-to-NinjaScript conversion preserves decisions, not punctuation. Lock the reference inputs, match confirmed-bar crossover timestamps, and only then introduce multi-timeframe filters or a more exact protective-order model. Your best first check is simple: place both 9 and 21 EMAs on the same market and verify each cross before looking at a performance report.

Related articles

Questions about Pine Script to NinjaScript conversion? Join our Discord to discuss with other traders!