NinjaScript > Educational Resources > Tips >

Multi-Time Frame & Instruments

Print this Topic Previous pageReturn to chapter overviewNext page

NinjaScript supports multi-time frame and instruments in a single script. This is possible since you can add additional Bars objects to indicators or strategies. A Bars object represents all of the bars of data on a chart. If you had a MSFT 1 minute chart with 200 minute bars on it, the 200 minute bars represents a Bars object. You can even execute trades across all the different instruments in a script. There is extreme flexibility in the NinjaScript model that NinjaTrader uses for multiple-bars scripts so it is very important that before you incorporate additional Bars objects in a script, you understand how it all works. An important fact to understand is that NinjaScript is truly event driven; every Bars object in a script will call the OnBarUpdate() method. What does this mean? It will become evident as you read further.

 

It is also important that you understand the following method and properties:

 

Add()
BarsArray
BarsInProgress

 

Understanding this Section (MUST READ)

As we move through this section, the term "primary Bars" will be used and for the purpose of clarification, this will always refer to the first Bars object loaded into a script. For example, if you apply a script on MSFT 1 minute chart, the primary Bars would be MSFT 1 minute data set.

 

This section is written in sequential fashion. Example code is re-used and built upon from sub section to sub section.

 

tog_minusAdding Additional Bars Object to NinjaScript

Additional Bars are added to a script via the Add() method in the Initialize() method. When a Bars object is added to a script, it is also added to the BarsArray array. What is that you ask? Think of it like a container in the script that holds all Bars objects added to the script. It's no different than a bucket of golf balls in your garage. As a Bars object is added to the script, it's added to this container, BarsArray, and given an index number so we can retrieve this Bars object later. Don't sweat this if it sounds complex, its quite easy once you see it in practical use. It's explained in greater detail later in this section.

 

For the purpose of demonstration, let's assume that a MSFT 1 minute bar is our primary Bars (set when the script is applied to a 1 minute MSFT chart) and that the Initialize() method is adding a 3 minute Bars object of MSFT and then adding a 1 minute Bars object of AAPL for a total of 3 unique Bars objects.

 

protected override void Initialize()
{
    Add(PeriodType.Minute, 3);
    Add("AAPL", PeriodType.Minute, 1);
}

tog_minusHow Bar Data is Referenced

Understanding how multi-time frame bars are processed and what OHLCV data is referenced is critical.

 

The "Figure 1" image below demonstrates the concept of bar processing on historical data or in real-time when CalculateOnBarClose property is set to true. The 1 minute bars in yellow will only know the OHLCV of the 3 minute bar in yellow. The 1 minute bars in cyan will only know the OHLCV data of the 3 minute bar in cyan. Take a look at "Bar 5" which is the 5th one minute bar, if you wanted to know the current high value for on the 3 minute time frame, you would get the value of the 1st 3 minute bar since this is the last "closed" bar. The 2nd 3 minute bar (cyan) is not known at this time.

 

Tips_3

 

Contrast the above image and concept with the image below which demonstrates bar processing in real-time when CalculateOnBarClose property is set to false (tick by tick processing) . The 1 minute bars in yellow will know the current OHLCV of the 3 minute bar in yellow (2nd 3 minute bar) which is still in formation...not yet closed.

 

Tips_4

 

The point is if you have a multi-time frame script in real-time and it is processing tick by tick instead of on the close of each bar, understand that the OHLCV data you access in real-time is different than on historical data.

 

A quick example to illustrate the point:

 

Your script has complex logic that changes the bar color on the chart. You are running tick by tick, as per the above "Figure 2" image, the 5th 1 minute bar is looking at OHLCV data from the 2nd 3 minute bar. Your script changes the 5th 1 minute bar color to green. In the future you reload your script into the chart (for whatever reason) and the 5th 1 minute bar is now a historical bar. As per "Figure 1" image, the 5th 1 minute bar now references the OHLCV data of the 1st 3 minute bar (instead of the 2nd 3 minute bar as per Figure 2) and as a result, your script logic condition for coloring the bar green is no longer valid. Now your chart looks different.

tog_minusUsing Bars Objects as Input to Indicator Methods

In the sub section above, the concept of index values was introduced. This is a critical concept to understand since it is used consistently when working with multi-Bars script.

 

Let's demonstrate this concept:

 

Carrying on from the example above, our primary Bars is set from a MSFT 1 minute chart

 

MSFT 1 minute Bars is given an index value of 0

 

In the Initialize() method we added a MSFT 3 minute Bars object and an AAPL 1 minute Bars object to the script

 

MSFT 3 minute Bars is given an index value of 1

AAPL 1 minute Bars is given an index value of 2

 

Index values are given to each Bars object as they are added to a script in an incremental fashion. If there are 10 Bars objects in a script, then you will have index values ranging from 0 through 9.

 

Our script now has 3 Bars objects in the container BarsArray. From this point forward, we can ask this container to give us the Bars object we want to work with by providing the index value. The syntax for this is:

 

    BarsArray[index]

 

This allows us to get the correct Bars object and use it as input for an indicator method. For example:

 

    ADX(14)[0] > 30 && ADX(BarsArray[2], 14)[0] > 30

 

The above expression in English would translate to:

 

    If the 14 period ADX of MSFT 1 minute is greater than 30 and the 14 period ADX of AAPL 1 minute is greater than 30

 

Before we can apply this concept though we would first want to ensure that our Bars objects actually contain bars that we can run calculations off of. This can be done by checking the CurrentBars array which returns the number of the current bar in each Bars object. Using this in conjunction with BarsRequired would ensure each Bars object has sufficient data before we begin processing.

 

protected override void OnBarUpdate()
{
    // Checks to ensure all Bars objects contain enough bars before beginning
    if (CurrentBars[0] <= BarsRequired || CurrentBars[1] <= BarsRequired || CurrentBars[2] <= BarsRequired)
        return;
}

 

Putting it all together now, the following example checks if the current CCI value for all Bars objects is above 200. You will notice that BarsInProgress is used. This is to check which Bars object is calling the OnBarUpdate() method. More on this later in this section.

 

protected override void OnBarUpdate()
{

    // Checks to ensure all Bars objects contain enough bars before beginning

    if (CurrentBars[0] <= BarsRequired || CurrentBars[1] <= BarsRequired || CurrentBars[2] <= BarsRequired)
        return;

 
    if (BarsInProgress == 0)
    {
        if (CCI(20)[0] > 200 && CCI(BarsArray[1], 20)[0] > 200
          && CCI(BarsArray[2], 20)[0] > 200)
         {
              // Do something
         }
    }
}

tog_minusTrue Event Driven OnBarUpdate() Method

Since a NinjaScript is truly event driven, the OnBarUpdate() method is called for every bar update event for each Bars object added to a script. This model provides the utmost  flexibility. For example, you could have multiple trading systems combined into one strategy dependent on one another. Specifically, you could have a 1 minute MSFT Bars object and a 1 minute AAPL Bars object, process different trading rules on each Bars object and check to see if MSFT is long when AAPL trading logic is being processed.

 

The BarsInProgress property is used to identify which Bars object is calling the OnBarUpdate() method. This allows you to filter out the events that you want to or don't want to process.

 

Continuing our example above, let's take a look at some code to better understand what is happening. Remember, we have three Bars objects working in our script, a primary Bars MSFT 1 minute, MSFT 3 minute and AAPL 1 minute.

 

protected override void OnBarUpdate()
{

    // Checks to ensure all Bars objects contain enough bars before beginning

    if (CurrentBars[0] <= BarsRequired || CurrentBars[1] <= BarsRequired || CurrentBars[2] <= BarsRequired)
        return;

 
    // Checks if OnBarUpdate() is called from an update on the primary Bars
    if (BarsInProgress == 0)
    {
        if (Close[0] > Open[0])
              // Do something
    }
 
    // Checks if OnBarUpdate() is called from an update on MSFT 3 minute Bars
    if (BarsInProgress == 1)
    {
        if (Close[0] > Open[0])
              // Do something
    }
 
    // Checks if OnBarUpdate() is called from an update on AAPL 1 minute Bars
    if (BarsInProgress == 2)
    {
        if (Close[0] > Open[0])
              // Do something
    }
}

 

What is important to understand in the above sample code is that we have "if" branches that check to see what Bars object is calling the OnBarUpdate() method in order to process relevant trading logic. If we only wanted to process the events from the primary Bars we could write our first statement as follows:

 

    if (BarsInProgress != 0)
        return;

 

What is also important to understand is the concept of context. When the OnBarUpdate() method is called, it will be called within the context of the calling Bars object. This means that if the primary Bars triggers the OnBarUpdate() method, all indicator methods and price data will point to that Bars object's data. Looking at the above example, see how the statement "if (Close[0] > Open[0]" exists under each "if" branch? The values returned by Close[0] and Open[0] will be the close and open price values for the calling Bars object. So when the BarsInProgress == 0 (primary Bars) the close value returned is the close price of the MSFT 1 minute bar. When the BarsInProgress == 1 the close value returned is the close price of the MSFT 3 minute Bars object.

 

 

Notes

1.A multi-series script only processes bar update events from the primary Bars (the series the script is applied to) and any additional Bars objects the script adds itself. Additional Bars objects from a multi-series chart or from other multi-series scripts that may be running concurrently will not be processed by this multi-series script.
2.If a multi-series script adds an additional Bars object that already exists on the chart, the script will use the preexisting series instead of creating a new one to conserve memory. This includes that series' session template as applied from the chart. If the Bars object does not exist on the chart, the session template of the added Bars object will be the session template of the primary Bars object. If the primary Bars object is using the "<Use instrument settings>" session template then the additional Bars objects will use the default session templates as defined for their particular instruments in the Instrument Manager.
3.In a multi-series script, real-time bar update events for a particular Bars object are only received when that Bars object has satisfied the BarsRequired requirement. To ensure you have satisfied the BarsRequired requirement on all your Bars objects it is recommend you start your OnBarUpdate() method with CurrentBars checks.
4.A multi-series indicator will hold the same number of data points for plots as the primary series. Setting values to plots should be done in the primary series in OnBarUpdate(). If you are using calculations based off of a larger secondary series, it may plot like a step ladder because there are more data points available than there are actual meaningful data values.
tog_minusAccessing the Price Data in a Multi-Bars NinjaScript

As you probably know already, you can access the current bar's closing price with the following statement:

 

    Close[0];

 

You can also access price data such as the close price of other Bars objects at any time. This is accomplished by accessing the Opens, Highs, Lows, Closes, Volumes, Medians, Typicals and Times series by index value. These properties hold collections (containers) that hold their named values for all Bars objects in a script.

 

Continuing with our example code above, if you wanted to access the high price of the MSFT 3 min Bars object which is at index 1 you would write:
 
    Highs[1][0];

 
This is just saying give me the series of high prices for the Bars object at index 1 "Highs[1]" and return to me the current high value "[0]". Now, if the BarsInProgress index was equal to 1, the current  context is of the MSFT 3 min Bars object so you could just write:
 
    High[0];

 

The following example demonstrates various ways to access price data.

 

protected override void OnBarUpdate()
{

    // Checks to ensure all Bars objects contain enough bars before beginning

    if (CurrentBars[0] <= BarsRequired || CurrentBars[1] <= BarsRequired || CurrentBars[2] <= BarsRequired)
        return;

 
    // Checks if OnBarUpdate() is called from an update on the primary Bars
    if (BarsInProgress == 0)
    {
        double primaryClose  = Close[0];
        double msft3minClose = Closes[1][0];
        double aapl1minClose = Closes[2][0];
 
        // primaryClose could also be expressed as
        // primaryClose = Closes[0][0];
    }

 

    // Checks if OnBarUpdate() is called from an update on MSFT 3 minute Bars object
    if (BarsInProgress == 1)
    {
        double primaryClose  = Closes[0][0];
        double msft3minClose = Close[0];
        double aapl1minClose = Closes[2][0];
    }
}

tog_minusEntering, Exiting and Retrieving Position Information

This section is relevant for NinjaScript strategies only. Entry and Exit methods are executed within the BarsInProgress context. Let's demonstrate with an example:

 

protected override void OnBarUpdate()
{

    // Checks to ensure all Bars objects contain enough bars before beginning

    if (CurrentBars[0] <= BarsRequired || CurrentBars[1] <= BarsRequired || CurrentBars[2] <= BarsRequired)
        return;

 
    // Checks if OnBarUpdate() is called from an update on the primary Bars
    if (BarsInProgress == 0)
    {
         // Submits a buy market order for MSFT
        EnterLong();
    }
 
    // Checks if OnBarUpdate() is called from an update on AAPL 1 minute Bars object
    if (BarsInProgress == 2)
    {
         // Submits a buy market order for AAPL
        EnterLong();
 
         // Submits a buy market for MSFT when OnBarUpdate() is called for AAPL
        EnterLong(0, 100, "BUY MSFT");
    }
}

 
As you can see above, orders are submitted for MSFT when BarsInProgress is equal to 0 and for AAPL when BarsInProgress is equal to 2. The orders submitted are within the context of the Bars object calling the OnBarUpdate() method and the instrument associated to the calling Bars object. There is one exception which is the order placed for MSFT within the context of the OnBarUpdate() call for AAPL. Each order method has a variation that allows you to specify the BarsInProgress index value which enables submission of orders for any instrument within the context of another instrument.

 

Notes:

1. Should you have multiple Bars objects of the same instrument and are using Set() methods in your strategy, you should only submit orders for this instrument to the first Bars context of that instrument. This is to ensure your order logic is processed correctly and any necessary order amendments are done properly.

2. Should you have multiple Bars objects of the same instrument and are using options to terminate orders/positions at the end of the session (TIF=Day or ExitOnClose=true), you should not submit orders to Bars objects other than the first Bars context for that instrument when on the last bar of the session. This is necessary because some of the end of session handling is applied only to the first Bars context of an instrument and submitting orders to other Bars objects for that instrument can bypass the end of session handling.

 

 

 

The property Position always references the position of the instrument of the current context. If the BarsInProgress is equal to 2 (AAPL 1 minute Bars), Position would refer to the position being held for AAPL. The property Positions holds a collection of Position objects for each instrument in a strategy. Note that there is a critical difference here. Throughout this entire section we have been dealing with Bars objects. Although in our sample we have three Bars objects (MSFT 1 and 3 min and AAPL 1 min) we only have two instruments in the strategy.

 

MSFT position is given an index value of 0
AAPL position is given an index value of 1

 

In the example below, when the OnBarUpdate() method is called for the primary Bars we also check if the position held for AAPL is NOT flat and then enter a long position in MSFT. The net result of this strategy is that a long position is entered for AAPL, and then once AAPL is long, we go long MSFT.

 

protected override void OnBarUpdate()
{

    // Checks to ensure all Bars objects contain enough bars before beginning

    if (CurrentBars[0] <= BarsRequired || CurrentBars[1] <= BarsRequired || CurrentBars[2] <= BarsRequired)
        return;

 
    // Checks if OnBarUpdate() is called from an update on the primary Bars
    if (BarsInProgress == 0 && Positions[1].MarketPosition != MarketPosition.Flat)
    {
         // Submits a buy market order for MSFT
         EnterLong();

    }
 
    // Checks if OnBarUpdate() is called from an update on AAPL 1 minute Bars
    if (BarsInProgress == 2)
    {
        // Submits a buy market order for AAPL
         EnterLong();
    }
}