Learn NinjaScript: Sharing Code Between Indicators and Strategies
You’ve got an indicator with 200 lines of useful logic. You want the same logic in a strategy — the same session filter, the same risk check, the same ATR-based stop. Copy-pasting is the quickest short-term answer and the worst long-term one: two places to fix bugs, two places for behavior to drift.
NinjaScript gives you two clean options. Extract shared pieces into a static AddOn helper (this post). Or instantiate the indicator from the strategy and read its output Series (Week 35). This post covers the first path: when to prefer it, the mechanics, the compilation-order trap, and a RiskUtils anchor called from both an indicator (for display) and a strategy (for sizing).
⚙️ Two Ways to Share
Both are valid; they fit different problems. The split comes down to stateful vs stateless:
- Static AddOn helper. Stateless. Inputs in, result out. Perfect for pure math, time arithmetic, brush construction, risk calculations, string formatting. Callable from anywhere — indicator, strategy, another AddOn, an add-on window. No bar context; no side effects.
- Instantiated indicator. Stateful. The strategy creates an instance of the indicator at
DataLoaded, and NT runs the indicator’sOnBarUpdatealongside the strategy’s. The strategy reads the indicator’sValues[0], customSeries, or public helper methods each bar. This is what you want when the logic has per-bar state — a running ATR, a plot, a cumulative counter.
For a summary of when each wins, and a couple of options you should almost never pick:
Sharing Patterns
| Approach | Stateful? | Cross-Script? | Best For |
|---|---|---|---|
| Static AddOn helper | No — inputs in, outputs out. | Yes — any indicator, strategy, or add-on. | Pure math, session-time checks, brush construction, risk calculations. |
| Instantiate indicator from strategy | Yes — each bar updates the indicator's Series. | Indicator → strategy only (not indicator → indicator). |
Strategies that need the time series output of an indicator (RSI values, SMA line, custom Series |
| Private method in the file | Can be — may read Bars, CurrentBar, Close. | No — file-local only. | Logic used multiple places within a single indicator or strategy. |
| Shared base class | Depends on the base. | Yes, but complicates single-file distribution. | Rarely the right choice in NinjaScript. Prefer AddOn helpers. |
| Compiled DLL in bin/Custom | Up to the DLL. | Yes — any NinjaScript can reference it. | Proprietary code you want compiled and distributed as binary, not source. |
🏗️ The Shared AddOn Pattern
A static method in an AddOns/ file, with a focused signature and no indicator dependencies. The function knows its inputs, returns its answer, and has no idea whether an indicator or a strategy is calling it:
using System;
namespace NinjaTrader.NinjaScript.AddOns
{
public static class RiskUtils
{
/// <summary>Position size in contracts given account balance,
/// desired risk percentage, stop distance in ticks, and per-tick
/// dollar value. Returns 0 on nonsensical inputs.</summary>
public static int PositionSize(double accountBalance,
double riskPct,
int stopTicks,
double tickValue)
{
if (accountBalance <= 0) return 0;
if (riskPct <= 0) return 0;
if (stopTicks <= 0) return 0;
if (tickValue <= 0) return 0;
double dollarsAtRisk = accountBalance * (riskPct / 100.0);
double riskPerContract = stopTicks * tickValue;
int contracts = (int)Math.Floor(dollarsAtRisk / riskPerContract);
return Math.Max(contracts, 0);
}
}
}
The method reads as a spec: “given these four inputs, return a contract count.” Nothing about charts, bars, or orders — just arithmetic with defensive guards. This is the shape every shared helper should take.
🔗 Compilation Order
NT compiles the Custom assembly as a unit — AddOns, Indicators, Strategies, AddOn windows — all in one NinjaTrader.Custom.dll. The practical implication: when you change an AddOn, everything that references it needs to be rebuilt too, and NT handles that automatically when you hit F5 in the NinjaScript editor.
The trap is editing outside the editor. If you update RiskUtils.cs by writing to the file directly (from VS, a script, an AI coding tool), NT only notices on the next compile cycle. Until then, the indicator and strategy that reference it are still bound to the previous compiled version. If the AddOn’s signature changed and you restart NT before recompiling, every consumer fails to load with “method not found” errors.
The defensive habit: after any AddOn change, open the NinjaScript editor and hit F5 before you test the consumer code. If something’s wrong, the editor shows it immediately. If everything’s fine, the rest of the Custom assembly is now bound to the new version.
🎯 When to Use Which
The quick decision tree:
- Does the shared logic have per-bar state that accumulates over time? → Make it an indicator, instantiate it in the strategy (Week 35).
- Does the shared logic compute from inputs without memory? → Static AddOn helper.
- Is it a one-line check a human reads for clarity? → Private method in the file (Week 26 covered that).
- Is it a long pipeline of stateful steps that happens to be called from two scripts? → Split — the stateless steps go in AddOns, the stateful parts become an indicator the strategy instantiates.
🛠️ The Anchor Example — RiskUtils From Both Sides
Same RiskUtils.PositionSize method, called from two consumers. The indicator uses it to display suggested size on the chart; the strategy uses it to actually set the order quantity. Neither reimplements the calculation:
// Indicator — display-only consumer
using NinjaTrader.NinjaScript.AddOns;
public class RiskDisplay : Indicator
{
[NinjaScriptProperty][Range(0.1, 10)]
[Display(Name="Risk %", GroupName="Risk", Order=1)]
public double RiskPct { get; set; }
[NinjaScriptProperty][Range(1, 500)]
[Display(Name="Stop (ticks)", GroupName="Risk", Order=2)]
public int StopTicks { get; set; }
protected override void OnBarUpdate()
{
if (CurrentBar < 1) return;
double acct = Account.Get(AccountItem.CashValue, Currency.UsDollar);
double tickVal = Instrument.MasterInstrument.PointValue
* Instrument.MasterInstrument.TickSize;
int size = RiskUtils.PositionSize(acct, RiskPct, StopTicks, tickVal);
Draw.TextFixed(this, "size", $"Size: {size} contracts",
TextPosition.TopRight);
}
}
// Strategy — same helper, used for order sizing
using NinjaTrader.NinjaScript.AddOns;
public class RiskSizedBreakout : Strategy
{
[NinjaScriptProperty][Range(0.1, 10)]
public double RiskPct { get; set; }
[NinjaScriptProperty][Range(1, 500)]
public int StopTicks { get; set; }
protected override void OnBarUpdate()
{
if (CurrentBar < 20 || Position.MarketPosition != MarketPosition.Flat)
return;
if (Close[0] > MAX(High, 20)[1])
{
double acct = Account.Get(AccountItem.CashValue, Currency.UsDollar);
double tickVal = Instrument.MasterInstrument.PointValue
* Instrument.MasterInstrument.TickSize;
int size = RiskUtils.PositionSize(acct, RiskPct, StopTicks, tickVal);
if (size > 0)
EnterLong(size, "Breakout");
}
}
}
When the position-size logic needs to change — you want to add a minimum contract floor, or a maximum based on volatility — you edit one method in one file and both consumers pick up the change. That’s the payoff for setting it up as a shared AddOn.
📝 Pitfalls Checklist
- Bar-dependent logic in an AddOn. A helper that reads
Bars.GetOpen(0)inside an AddOn has noBarscontext; it won’t compile without grafting state in. Pass in the values the helper needs as parameters, or recognize this case as “should have been an indicator” and move it. - Recompiling only one consumer after an AddOn change. If you rebuild the strategy but not the indicator after editing the shared AddOn, the assembly ends up mixed between old and new. Compile the whole Custom assembly — F5 in the NinjaScript editor — after every AddOn change.
- Namespace mismatches. AddOn declares
namespace NinjaTrader.NinjaScript.AddOnsbut a consumer usesusing NinjaTrader.AddOns;— compiles fine locally if the assembly is cached, fails in a clean rebuild. Match exactly and double-check after AddOn renames. - Silent breaking changes. Adding a parameter to
PositionSize— even with a default value — breaks every call site on the next compile. Prefer method overloads over parameter changes; the old signature keeps working. - Using AddOn helpers when you needed an indicator. If the strategy needs a running
Series<double>(a moving ATR, a cumulative value), a static helper can’t produce that. An AddOn helper computes once per call from the inputs you give it; an indicator maintains state across bars. Don’t bend one into the shape of the other. - Over-generalizing the AddOn signature.
RiskUtils.PositionSize(params object[] args)to handle every future variation is the wrong direction. Specific signatures compile-check at call sites; genericobject[]signatures fail silently when you pass the wrong shape.


