Learn NinjaScript: Modularizing Reusable Conditions
if (A && B && (C || D) && !E) reads like noise. if (IsTrendUp() && HasVolumeConfirmation() && (IsGreenBar() || HasPivotConfirm()) && !IsInChopZone()) reads like English. The compiler doesn’t care — it produces the same IL either way — but every human who reads the code later does.
This post covers the pattern: lift each condition into a named bool method, compose them with &&/||, and keep the OnBarUpdate readable as a summary of intent. Signatures that work, where the methods belong, AND/OR composition, and the places extraction goes too far. The anchor is a five-condition entry filter on MySma, before and after refactoring.
⚙️ The Pattern — Private bool Methods
The simplest form: a private bool method per condition, each returning true when the condition holds. The body reads the indicator state directly; the caller doesn’t pass anything:
private bool IsTrendUp() => Close[0] > SMA(Period)[0];
private bool IsGreenBar() => Close[0] > Open[0];
private bool HasVolumeConfirm() => Volume[0] > SMA(Volume, 20)[0];
private bool InSession() => Bars.IsFirstBarOfSession || Time[0].TimeOfDay > new TimeSpan(9, 30, 0);
private bool IsInChopZone() => ATR(14)[0] < ATR(14)[20] * 0.5;
Expression-bodied methods (=>) work well when the condition is a single expression. For anything multi-line, use a full method body — the readability gain from being able to step-debug each check is worth the few extra lines.
🎯 Method Signatures That Work Well
- Parameterless when the condition only makes sense inside this indicator and reads instance state —
private bool IsTrendUp()readingClose[0]andSMA(Period)[0]. - Parameterized when the condition is a general shape you’d want to apply to different inputs —
private bool IsHigherHigh(int barsAgo, int lookback). Takes data references as arguments rather than hardcoding which series to read. - With a threshold argument when the condition’s sensitivity should be tunable —
private bool HasVolumeConfirm(double multiplier). Lets the same method serve both “loose” and “strict” callers. - Static, in an AddOn when the logic doesn’t depend on indicator state at all —
public static bool IsInSession(DateTime t, TimeSpan start, TimeSpan end). This is the Week 25 pattern; revisit it whenever a condition becomes useful to a second indicator.
Stay consistent about naming. Is* for state predicates, Has* for possession/confirmation, Can* for capability. A reader scanning a long if-chain can then infer the meaning without reading every method body.
📚 Where Conditions Live
Not every bool method deserves to be shared. The rule of thumb: extract to an AddOn when two or more indicators or strategies would naturally call the same condition. Keep it private to the file otherwise. Premature promotion creates an AddOn that needs editing every time the first consumer’s definition of “confirmed” shifts.
The gray area: a method that reads indicator state but expresses a concept general enough to live in an AddOn. Fix this by making the AddOn version parameterized — take the series, the lookback, and whatever else it needs as arguments. The indicator calls Predicates.IsHigherHigh(High, 0, 10); the AddOn knows nothing about which indicator called it.
🧠 Composition Patterns
- AND-chained.
A() && B() && C()— all conditions must hold. Short-circuits from left to right, so cheap checks go first. - OR-chained.
A() || B()— any condition triggers. Useful for the “either pattern X or confirmation Y” structure. - Inverted.
!IsInChopZone()— reads fine when the method name is positive.!HasNotYet*()is a double-negative nightmare; rewrite the method to be positive. - Mixed with parentheses.
A() && (B() || C()) && !D()— fine for three or four terms; past five terms, extract a named method that captures the subexpression.
The goal isn’t to eliminate operators; it’s to keep the if-statement short enough that a reader can hold all of it in their head. If the composed expression needs more than a line, it’s a candidate to extract as its own predicate: private bool IsValidLongEntry() returning the AND-chain.
🛠️ The Anchor Example — Before and After
A multi-condition entry filter on MySma. Before extraction, the logic is wedged into OnBarUpdate as one long if:
protected override void OnBarUpdate()
{
if (CurrentBar < Period + 20) return;
if (Close[0] > SMA(Period)[0]
&& Close[0] > Open[0]
&& Volume[0] > SMA(Volume, 20)[0]
&& (Bars.IsFirstBarOfSession
|| Time[0].TimeOfDay > new TimeSpan(9, 30, 0))
&& !(ATR(14)[0] < ATR(14)[20] * 0.5))
{
Draw.ArrowUp(this, "entry_" + CurrentBar, true, 0,
Low[0] - TickSize * 4, Brushes.LimeGreen);
}
}
Five conditions, all inlined. To figure out what the method does, you parse operator precedence in your head. After extraction, OnBarUpdate reads as a sentence and the conditions sit below, each named and testable in isolation:
protected override void OnBarUpdate()
{
if (CurrentBar < Period + 20) return;
if (IsTrendUp()
&& IsGreenBar()
&& HasVolumeConfirm()
&& InSession()
&& !IsInChopZone())
{
Draw.ArrowUp(this, "entry_" + CurrentBar, true, 0,
Low[0] - TickSize * 4, Brushes.LimeGreen);
}
}
private bool IsTrendUp() => Close[0] > SMA(Period)[0];
private bool IsGreenBar() => Close[0] > Open[0];
private bool HasVolumeConfirm() => Volume[0] > SMA(Volume, 20)[0];
private bool InSession() => Bars.IsFirstBarOfSession
|| Time[0].TimeOfDay > new TimeSpan(9, 30, 0);
private bool IsInChopZone() => ATR(14)[0] < ATR(14)[20] * 0.5;
Exact same behavior. Compiled IL is nearly identical (modern JIT inlines single-expression methods). The readable version catches its own bugs — “wait, should IsInChopZone really compare to half of the ATR 20 bars ago?” is a question the reader can ask; the same question buried in an inlined ATR(14)[0] < ATR(14)[20] * 0.5 hides in the noise.
📝 Pitfalls Checklist
- Expensive condition called multiple times per bar.
if (IsExpensive() && SomethingElse() && IsExpensive())computes it twice. If a condition is costly, compute once into a local and reuse, or cache in aSeries<bool>per bar. - Names that describe mechanics instead of intent.
CloseGreaterThanSma()is what the code does;IsUptrend()is why. The “why” name is readable, the “what” name is a worse version of the inline expression. - Condition methods with side effects. A
bool IsBreakout()that also updates aSeries<int>is a landmine. Reading the code later you won’t see the side effect. Either return the value without mutating, or name it so the mutation is obvious (RecordBreakout()). - Boolean methods that sometimes throw.
IsHigherHigh(10)on bar 5 throws becauseHigh[10]is off the end. Guard inputs (if (CurrentBar < lookback) return false;) and make throwing impossible. - Overdecomposing.
private bool IsPositive() => x > 0wherex > 0would have read perfectly well. Two-line conditions almost never need extraction; three-plus-term composed conditions usually do. - Drift between condition and name.
IsTrendUp()originally returnedClose > SMA; a year later it’s been edited to include a momentum check, a price-action filter, and an hour restriction. The name no longer describes the body. When a method grows past its name, rename — or split.


