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 concept | NinjaScript 8 equivalent | Conversion note |
|---|---|---|
input.int() / input.float() | public property with [NinjaScriptProperty] | Add [Range] and [Display] so it appears in the strategy UI. |
close, open, high, low | Close, Open, High, Low | NinjaScript 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 logic | There 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:
- Position size and entry handling. Pine’s
pyramiding = 0is represented here by one entry per direction and a flat-position check. - Session template. A futures chart using regular trading hours will produce different bars from a chart using nearly 24-hour electronic hours.
- Commission and slippage. Configure them in the relevant platform’s backtest settings, not inside a crossover condition.
- Intrabar fills. A stop and target touched within one bar can be resolved differently depending on bar granularity and fill processing.
- 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.
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
- NinjaScript Tutorial: Code Your First NinjaTrader 8 Strategy — Learn the core NinjaTrader strategy lifecycle and order methods.
- How to Build a Multi-Timeframe Strategy in NinjaScript — Add secondary data series without corrupting signal timing.
- How to Convert Pine Script to MQL5: A Manual Translation Guide — Compare Pine’s concepts with MetaTrader 5 equivalents.
- Pine Script Tutorial for Beginners — Revisit Pine inputs, series, and strategy syntax.
- How to Backtest a Trading Strategy — Build a controlled process for checking a translated system.
- Understanding Backtesting Metrics — Read the report after signal and fill assumptions are validated.
- EMA vs SMA — Decide whether an exponential average fits the original rule.
- Best AI Pine Script Generator: 7 Tools Compared — Compare tools for creating and editing Pine code.
Questions about Pine Script to NinjaScript conversion? Join our Discord to discuss with other traders!
