A Beginner-Friendly Guide to Custom Data Storage in NinjaTrader.

In NinjaTrader, almost everything you see — from price plots to moving averages to volume bars — is powered by something called a Series.

Series<T> is the engine behind storing, tracking, and referencing information bar-by-bar as time moves forward. Learning how to properly use Series<T> will unlock the ability to build real indicators and smart strategies — not just copy simple formulas.

In this guide, we’ll break down exactly what Series<T> is, why it matters, and how to use it practically, with no skipped steps.

✅ We’ll explain everything in plain English first, then back it up with real NinjaScript examples you can build from.

👉 Save this official NinjaTrader Help Guide link:


If you’re new here, make sure you check out the earlier posts too:

Each one builds your foundation toward mastering NinjaScript step-by-step.


🚀 Ready? Let’s dive deep into Series<T>, and understand how NinjaTrader organizes and stores your custom data!

📖 Introduction: Why Series<T> Matters

When you’re building indicators or strategies in NinjaTrader, you often need to store information over time.

  • Maybe you want to track where you generated a trade signal.
  • Maybe you need to save some calculation from a few bars ago.
  • Maybe you want to mark special conditions across bars.

That’s where Series<T> comes in.

Plain English:
A Series<T> is like giving every bar its own private sticky note — where you can write whatever you want!

In Code:
Series<T> is a time-synced container that holds a value for each bar.

And because it’s strongly typed (<T>), you can store:

  • Numbers (double, int, etc.)
  • True/false flags (bool)
  • Dates (DateTime)
  • Strings
  • And even custom objects (advanced)

✅ NinjaTrader itself uses Series for built-in data:

  • Close[0] (Series of closes)
  • Volume[1] (Series of volume numbers)

You can create your own Series for your logic, just like NinjaTrader does!


🔹 What Exactly is Series<T>?

Before we jump into coding, let’s really understand it:

  • Series<T> connects directly to a data stream (your chart or an added timeframe).
  • It automatically grows as new bars form.
  • You can assign a value to any bar and look back over time easily.

Visual:
Imagine your price chart. Now imagine an invisible column next to each bar where you can store one value per bar. That’s your Series<T>.


📦 How to Declare and Initialize a Series<T>

Before you can store custom data bar-by-bar, you first need to declare and initialize your Series<T> properly.

There are two steps you always follow:

🛠️ Step 1: Declare the Series

At the very top of your script (outside of any methods, usually right after your class line), declare the Series like this:

private Series<double> myCustomSeries;

Plain English:
You’re telling NinjaTrader:

  • “I want a Series that will store double values (decimal numbers).”
  • “I’m naming it myCustomSeries so I can use it later in my code.”

🛠️ Step 2: Initialize the Series in OnStateChange()

You must wait until NinjaTrader has finished loading your data before you actually create the Series.

This happens inside OnStateChange() when State == State.DataLoaded.

Here’s how the full method looks with the initialization in the right place:

protected override void OnStateChange()
{
    if (State == State.SetDefaults)
    {
        Name = "MyCustomIndicator";
        Calculate = Calculate.OnBarClose;
    }
    else if (State == State.DataLoaded)
    {
        myCustomSeries = new Series<double>(this);
    }
}

Why only in DataLoaded?
Because before this point, NinjaTrader doesn’t know how many bars (or what instruments) you’re working with yet.

Plain English:
You can’t open a notebook and start writing until you know how many pages you have!


🧠 What Does this Mean in new Series<T>(this)?

When you write:

myCustomSeries = new Series<double>(this);

The keyword this tells NinjaTrader:

“Tie this new Series to the primary data stream of this indicator (or strategy).”

In other words, your Series automatically stays in sync with the main chart or instrument your script is running on.

If you’re using multiple data series (like adding a 5-minute and a 1-hour chart together), you can explicitly link your Series to a different BarsArray[] — we’ll cover that in later lessons!

📏 What Is MaximumBarsLookBack?

When you create a Series<T>, NinjaTrader automatically manages how many bars of historical data your Series will store.

By default, only the last 256 bars are kept fully accessible.

This setting is controlled by something called MaximumBarsLookBack.


🛠️ Default Behavior: 256 Bars

When you create a Series like this:

myCustomSeries = new Series<double>(this);

NinjaTrader automatically assumes:

MaximumBarsLookBack = MaximumBarsLookBack.TwoHundredFiftySix;

What this means in plain English:
You can safely reference up to 256 bars back in time (e.g., myCustomSeries[255]), but older bars might be recycled to save memory.

🔥 When Should You Use MaximumBarsLookBack.Infinite?

Sometimes you need to look much further back than 256 bars — for example:

  • Calculating long-term indicators (like 500-bar averages)
  • Tracking signals or states across many days or weeks
  • Building custom backtesting tools

In that case, you need to explicitly tell NinjaTrader:

myCustomSeries = new Series<double>(this, MaximumBarsLookBack.Infinite);

Now:
NinjaTrader will never recycle your old Series values, no matter how far back you look.


⚠️ Important: Infinite LookBack = More Memory

Setting MaximumBarsLookBack.Infinite makes NinjaTrader store all historical values — forever.

Good:

  • No unexpected recycling of values
  • Safe to reference any old bar

🚫 Tradeoff:

  • Uses more memory and resources, especially on long charts or multi-series scripts

👉 Best practice:

  • Only use Infinite if you really need to access old bars reliably.

🧩 Why Set Infinite on Specific Series Instead of the Entire Indicator?

In the Indicator UI settings (the NinjaTrader Properties window), you can change this dropdown:

Maximum bars look back: ➡️ Infinite

But:
That setting applies to the entire script — meaning every Series inside your indicator or strategy will use infinite memory.

Instead, you can target only the Series that really needs it.

✅ Example:

protected override void OnStateChange()
{
    if (State == State.DataLoaded)
    {
        // Only this specific Series will have infinite lookback
        myCustomSeries = new Series<double>(this, MaximumBarsLookBack.Infinite);
        myOtherCustomSeries = new Series<double>(this);

        // myOtherCustomSeries Series objects still uses 256 bars, saving memory
    }
}

Bottom Line:
Setting infinite lookback only where needed is a much smarter, more efficient way to manage resources — especially in larger or multi-instrument indicators.


🔎 How to Use Series<T> in Practice

Now that you know how to create a Series<T>, it’s time to actually use it.

In real-world NinjaScript coding, Series<T> is used for two main things:

Action What It Means Example
Assign a Value Store something in the Series at the current bar myCustomSeries[0] = Close[0] + 5;
Read a Value Access past values from the Series double lastValue = myCustomSeries[1];

✍️ Assigning a Value to a Series

You typically assign values inside OnBarUpdate().

Example: Save a value based on today’s Close price:

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

    myCustomSeries[0] = Close[0] * 1.05; // Save 5% above today's close
}

🔵 myCustomSeries[0] always refers to the current bar.

✅ You can set values using any custom formula, signals, or logic you want.


📖 Reading Values from a Series

Later, you can read back what you stored — even from older bars.

Example: Compare today’s value to yesterday’s:

if (myCustomSeries[0] > myCustomSeries[1])
{
    // Today's custom value is greater than yesterday's
}

Important:

  • myCustomSeries[0] ➔ Today’s (current bar) value
  • myCustomSeries[1] ➔ Previous bar’s value
  • myCustomSeries[2] ➔ 2 bars ago, and so on.

🛑 Always Wait for Enough Bars!

If you accidentally try to read from a Series before it has data (like myCustomSeries[10] when only 2 bars exist), your script will throw an error.

✅ Always use CurrentBar checks first:

protected override void OnBarUpdate()
{
    if (CurrentBar < 20)
        return; // Wait until we have at least 20 bars
    
    myCustomSeries[0] = Close[0] + Close[1] - Close[2];
}

Plain English:
Don’t touch data from 20 bars ago unless I have at least 20 historical bars loaded!


🧪 Example: Creating a Simple Custom Signal

Let’s build a tiny working example using a Series.

Goal:
Mark when today’s close is higher than yesterday’s close.

private Series<double> higherCloseSignal;

protected override void OnStateChange()
{
    if (State == State.SetDefaults)
    {
        Name = "HigherCloseSignal";
    }
    else if (State == State.DataLoaded)
    {
        higherCloseSignal = new Series<double>(this);
    }
}

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

    if (Close[0] > Close[1])
        higherCloseSignal[0] = 1; // Signal: Higher Close
    else
        higherCloseSignal[0] = 0; // No Signal
}

✅ What’s happening:

  • Current bar (0): Save 1 if higher, 0 if not.
  • Later bars: You can reference higherCloseSignal[5], higherCloseSignal[10], etc., to see if the signal triggered historically.

⚙️ Bonus: How to Expose a Series<T> Publicly

If you want other indicators or strategies to read your custom Series easily:

Add a public property like this:

public Series<double> HigherCloseSignalSeries
{
    Update(); // Forces the OnBarUpdate() method to be called for all data series so that indicator values are updated to the current bar index (i.e., this makes sure that when the public Series<double> HigherCloseSignalSeries is called from another indicator or strategy, the value is accurate).
    get { return higherCloseSignal; }
}

Now anyone using your indicator can reference (more on this in the future):

YourIndicatorName.HigherCloseSignalSeries[0];

Note:
You don’t use Values[] when exposing custom Series like this — only when exposing Plots (we’ll cover that separately).


📦 Quick Summary for Reference

Action Code Example Notes
Create a Series higherCloseSignal = new Series(this); Done in State.DataLoaded
Assign a value higherCloseSignal[0] = 1; Inside OnBarUpdate()
Read a value if (higherCloseSignal[1] == 1) Look back 1 bar ago
Expose a Series public Series YourSeries { Update(); get { return series; } } Optional, for external access

🛡️ Validating Series Values with IsValidDataPoint()

When you use a Series<T>, there’s something very important you need to remember:

Not every bar necessarily has a meaningful value.

  • Sometimes you might not assign a value to your Series.
  • Sometimes you might Reset() a value (intentionally “clear” it).
  • Sometimes the Series is still “empty” if it’s early in the chart history.

If you try to use a value that doesn’t exist or is invalid, your indicator could behave incorrectly or throw errors!

That’s where IsValidDataPoint() comes in.


📋 What is IsValidDataPoint()?

IsValidDataPoint(int barsAgo) is a method that checks if a value for your Series<T> is considered valid.

If true — the value was assigned and should be trusted.
If false — the value is missing, empty, or was reset.


📖 Example

Let’s say you want to use a custom Series only if you actually have a valid number there.

if (higherCloseSignal.IsValidDataPoint(1))
{
    if (higherCloseSignal[1] == 1)
    {
        // Do something if yesterday's close was higher
    }
}

✅ This double-checks before you try to access higherCloseSignal[1].


🚨 Important: Reset() Affects Validity!

When you call Reset() on a Series (for example, myCustomSeries.Reset();):

  • A value is still physically there internally (like 0.0 for a Series<double>).
  • But NinjaTrader flags that datapoint as invalid behind the scenes.
  • IsValidDataPoint() will now return false for that specific bar.

Reset() and IsValidDataPoint() always work together.

Think of Reset() as saying:

Mark this datapoint as invalid — don’t trust it for calculations or plotting until a new value is assigned.

Pro Tip:
Even after Reset(), NinjaTrader still retains the “default” value for that data type. It doesn’t erase memory — it simply tells the system to logically ignore that datapoint unless you manually reassign or check it.


🧪 Realistic Example: Using Reset and IsValidDataPoint

When building custom indicators, there’s rarely only one “right” way to handle a situation. Often there are multiple valid approaches — depending on the behavior you want, your style, and the needs of your logic.

Here are three common methods for handling mid-bar data updates when working with Series<T>:

✅ Example 1: Reset() + IsValidDataPoint()

This method clears the current bar’s value by calling .Reset(), and then you check if the value is valid before doing any further logic.

private Series<double> myCustomSignal;
private bool isOnBarClose;

protected override void OnStateChange()
{
    if (State == State.SetDefaults)
    {
        Calculate = Calculate.OnPriceChange; // Important for this example
    }
    else if (State == State.DataLoaded)
    {
        myCustomSignal = new Series<double>(this);
        isOnBarClose = (Calculate == Calculate.OnBarClose);
    }
}

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

    // Option 1: Reset first
    myCustomSignal.Reset();

    if (Close[0] > SMA(20)[0])
        myCustomSignal[0] = 1; // Bullish
    else if (Close[0] < SMA(20)[0])
        myCustomSignal[0] = -1; // Bearish

    if (myCustomSignal.IsValidDataPoint(0))
    {
        // ✅ Only act on fresh valid signals
        // Example: trigger alerts, draw markers, etc.
    }
}

🔥 Why This Approach?

  • Safety first — you guarantee that no old values accidentally stick.
  • Works very well for pure signal-based indicators.
  • Especially important if your logic depends on detecting when no valid signal is present.

✅ Example 2: Set Explicitly to Neutral Value

Instead of Resetting, you can set a neutral value, like 0, if no signal is active.

private Series<double> myCustomSignal;
private bool isOnBarClose;

protected override void OnStateChange()
{
    if (State == State.SetDefaults)
    {
        Calculate = Calculate.OnPriceChange;
    }
    else if (State == State.DataLoaded)
    {
        myCustomSignal = new Series<double>(this);
        isOnBarClose = (Calculate == Calculate.OnBarClose);
    }
}

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

    // Option 2: Explicit neutral value
    if (Close[0] > SMA(20)[0])
        myCustomSignal[0] = 1;
    else if (Close[0] < SMA(20)[0])
        myCustomSignal[0] = -1;
    else
        myCustomSignal[0] = 0; // Neutral when no clear signal

    if (myCustomSignal[0] != 0)
    {
        // ✅ Only act if a true bullish or bearish signal exists
    }
}

🔥 Why This Approach?

  • Simpler code — you don’t need to check IsValidDataPoint().
  • Useful when 0 has a meaningful “neutral” meaning in your logic.
  • Slightly faster at runtime (no internal validity checks needed).

✅ Example 3: Updating All Plots Using Previous Valid Value

Sometimes, you might want to carry forward previous valid values if no new valid signal is generated yet.

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

    // Propagate previous values
    for (int i = 0; i <= Values.Length - 1; i++)
    {
        if (Values[i].IsValidDataPoint(1))
            Values[i][0] = Values[i][1];
    }
}

🔥 Why This Approach?

  • Good for indicators where you hold a signal across multiple bars until a clear new condition appears.
  • Often used for building trend-following indicators, confirmation plots, etc.
  • Reduces flickering of plots between signal states.

🧠 Quick Takeaways

Concept What Happens Why It Matters
Reset() Marks current bar’s value as invalid Helps prevent using outdated or wrong data
IsValidDataPoint() Checks if a value is valid before using it Prevents logic errors or bad calculations
Best Practice Reset Series before reapplying conditions every tick or price change Keeps everything clean and accurate

❗ Common Beginner Mistakes

Mistake Problem
Forgetting to initialize Series in OnStateChange Script will crash at runtime with a NullReferenceException
Using Series without checking CurrentBar or available history Throws an IndexOutOfRangeException, especially early when there aren't enough bars
Forgetting to reset signals during OnPriceChange or OnEachTick Old signals can "stick" even though the conditions are no longer true
Setting MaximumBarsLookback wrong (or forgetting it) Limits how far back your Series can be referenced, causing wrong historical behavior or plotting issues
Setting the entire indicator to Infinite unnecessarily Wastes memory and slows down NinjaTrader — you can selectively set only the Series objects that need longer memory instead!

Plain English:
Series<T> objects are powerful — but you have to set them up carefully and respect their rules for the best results.

🧠 Final Thoughts: Series<T> Is the Core of NinjaScript Power

Series<T> is not just a fancy way of storing data — it’s how NinjaTrader thinks about time, bars, and evolving market conditions.

If you want to build indicators, strategies, or even complex tools later — mastering Series<T> early gives you a real advantage.

✅ You now know:

  • How to declare and initialize a Series<T>
  • How to properly set up MaximumBarsLookback
  • How to manage dynamic data with Reset() and IsValidDataPoint()
  • Why being intentional with your Series<T> design matters for both performance and logic

Most beginner mistakes (crashes, wrong signals, plot glitches) come from not fully respecting how NinjaTrader tracks data behind the scenes.

You’re already way ahead by understanding how it really works.

📌 Remember:
Series<T> isn’t magic. It’s just organized memory, tied to time, that you control carefully.
Build cleanly now — and your future scripts will be faster, more reliable, and easier to scale.

🎉 Prop Trading Discounts

💥91% off at Bulenox.com with the code MDT91

Categorized in:

Learn NinjaScript,

Tagged in: