Multi-Time Frame & Instruments
Previous Topic  Next Topic 

A NinjaScript strategy supports multi-time frame and instruments in a single strategy. This is possible since you can add additional Bars objects to a strategy. 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 strategy. There is extreme flexibility in the strategy model that NinjaTrader uses for multiple-bars strategies so it is very important that before you incorporate additional Bars objects in a strategy, you understand how it all works. An important fact to understand is that a NinjaScript strategy is truly event driven; every Bars object in a strategy 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:




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 strategy. For example, if you apply a strategy 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.



Adding Additional Bars Object to a Strategy

Additional Bars are added to a strategy via the Add() method in the Initialize() method. When a Bars object is added to a strategy, it is also added to the BarsArray array. What is that you ask? Think of it like a container in the strategy that holds all Bars objects added to the strategy. It's no different than a bucket of golf balls in your garage. As a Bars object is added to the strategy, 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. Its 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 strategy 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);                                   
}


How 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.

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.


The point is if you have a multi-time frame strategy 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 strategy 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 strategy changes the 5th 1 minute bar color to green. In the future you reload your strategy 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 strategy logic condition for coloring the bar green is no longer valid. Now your chart looks different.



Using 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 strategies.


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 strategy


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 strategy in an incremental fashion. If there are 10 Bars objects in a strategy, then you will have index values ranging from 0 through 9.


Our strategy 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 APPL 1 minute is greater than 30


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()
{
    if (BarsInProgress == 0)
    {
        if (CCI(20)[0] > 200 && CCI(BarsArray[1], 20)[0] > 200
          && CCI(BarsArray[2], 20)[0] > 200)
        {
            // Do something
        }                                
    }
}



True 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 strategy. 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 APPL 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 strategy, a primary Bars MSFT 1 minute, MSFT 3 minute and AAPL 1 minute.


protected override void OnBarUpdate()
{
    // 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.



Accessing the Price Data in a Multi-Bars Strategy

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 strategy.


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 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];                 
    }
}




Entering and Exiting and Retrieving Position Information
Entry and Exit methods are executed within the BarsInProgress context. Let's demonstrate with an example:


protected override void OnBarUpdate()
{
    // 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 MSFT 3 minute Bars object
    if (BarsInProgress == 1)
    {
        // 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 APPL
        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 APPL. 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.

The property Position always references the position of the instrument of the current context. If the BarsInProgress is equal to 2 (APPL 1 minute Bars), Position would refer to the position being held for APPL. 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 APPL 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 APPL, and then once APPL is long, we go long MSFT.

protected override void OnBarUpdate()
{
    // 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 APPL
        EnterLong();  
    }
}