QuantConnect: Trading Multiple Stocks or Assets

In our previous getting started tutorials for QuantConnect we have focused only on trading a single stock. That stock was non-other than the worlds first trillion dollar company. But what if you can’t be in a monogamous relationship with just one asset? What if you want to trade on multiple assets at the same time using the same strategy? If that sounds like you, stick around because we are going to introduce a few more stocks to our strategy.

Scope

Once again we shall build on the tutorial that came before this one.  In that tutorial, we looked at adding a second timeframe to our simple RSI strategy. However, after adding the second timeframe, we found that opportunities became few and far between. To tackle this whilst maintaining our quality standards, we can simply look for opportunities elsewhere. Initially, it might sound like a simple task. However, there are some intricacies we need to consider in order to get things working. For example, if you are working on multiple timeframes, it might not be immediately obvious how to work with consolidatorsor the event handlers that they call. As such, in this tutorial, we should cover the following topics:
  • Using dictionaries and loops to create our indicators, charts, and consolidators for each stock.
  • Using a single call back with multiple consolidators.
  • Accessing additional information from a trade bar.

Possible Difficulties

Whilst we won’t cover any new concepts in this tutorial, there are some potential stumbling blocks that one could come up against when trying to add multiple timeframes. The most prominent of which is deciding how to update our RSI plots on the upper timeframe. (Last reminder, if you are wondering “what upper timeframe?” see the previous tutorial) The challenge is that consolidator handlers receive tradebarsrather than the data slice you receive inonData(). We can see this by looking at the method (function) declarations in our algorithm: The On Data call – (Standard on every script) def OnData(self, data): Our Consolidator Handler: def On_W1(self,sender,bar): To dive a bit deeper, data slices contain data for all the symbols you add during init. The symbol specific data can be accessed by giving the ticker/symbol as a keye.g. data["AAPL"].Close. Nice and straightforward.  On the other hand, a tradebar only contains the data for the instrument that was consolidated. As such, we either need to create a handler for every feed (which would be a bit unwieldy)  or we can use theget_Symbol()method to determine which stock the bar is coming from and then act appropriately. We will take the second option in the example code below. Another head scratcher you may have is how to add all of the instruments and their respective indicators without duplicating line after line of code. We also have a similar conundrum when we want to handle the OnData()event. The code example shall tackle this by using a combination of for-loopsand dictionaries.

The Code

### <summary>
### Simple RSI Strategy intended to provide a minimal algorithm example using
### one indicator with the most basic plotting
### </summary>
from datetime import timedelta

class RSIAlgorithm(QCAlgorithm):
    
    # 1 - Add the FANG stocks (Facebook, Amazon, , Netflix, Google)
    # 2 - Cycle through stocks
    # 3 - Cycle through list adding each equity
    # 3 - Create an indicator dict like backtrader

    def Initialize(self):
        '''Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized.'''
        
        # Set our main strategy parameters
        self.SetStartDate(2017,6,1)    # Set Start Date
        self.SetEndDate(2018,1,1)      # Set End Date
        self.SetCash(10000)            # Set Strategy Cash
        
        RSI_Period    = 14                # RSI Look back period 
        self.RSI_OB   = 75                # RSI Overbought level
        self.RSI_OS   = 50                # RSI Oversold level
        self.Allocate = 0.20              # Percentage of captital to allocate
        
        self.Equities = ["AAPL", "FB", "AMZN", "NFLX", "GOOG"]
        
        self.Indicators = dict()
        self.Charts = dict()
        self.Consolidators = dict()
        
        # Find more symbols here: http://quantconnect.com/data
        for Symbol in self.Equities:
            self.Consolidators[Symbol] = dict()
            self.AddEquity(Symbol, Resolution.Daily)
        
            # Each Equity requires its own consoilidator! See: 
            # https://www.quantconnect.com/forum/discussion/1936/multiple-consolidators/p1
            # https://www.quantconnect.com/forum/discussion/1587/multiple-symbol-indicator-values-in-consolidated-bar-handler/p1
            # ------------------------
            # Create our consolidators
            self.Consolidators[Symbol]['W1 Con'] = TradeBarConsolidator(timedelta(days=5))
            
            # Register our Handlers
            self.Consolidators[Symbol]['W1 Con'].DataConsolidated += self.On_W1
        
            self.Indicators[Symbol] = dict()
            self.Indicators[Symbol]['RSI'] = dict()
            
            self.Indicators[Symbol]['RSI']['D'] = self.RSI(Symbol, RSI_Period)
            self.Indicators[Symbol]['RSI']['W'] = RelativeStrengthIndex(Symbol, RSI_Period)
            # Register the indicaors with our stock and consolidator
            self.RegisterIndicator(Symbol, self.Indicators[Symbol]['RSI']['W'], self.Consolidators[Symbol]['W1 Con'])
        
            # Finally add our consolidators to the subscription
            # manager in order to receive updates from the engine
            self.SubscriptionManager.AddConsolidator(Symbol, self.Consolidators[Symbol]['W1 Con'])
        
            self.Charts[Symbol] = dict()
            # Plot the RSI
            RSIChartName = Symbol+" RSI"
            self.Charts[Symbol]['RSI'] = Chart(RSIChartName, ChartType.Stacked)
            self.Charts[Symbol]['RSI'].AddSeries(Series("D1", SeriesType.Line))
            self.Charts[Symbol]['RSI'].AddSeries(Series("W1", SeriesType.Line))
            self.AddChart(self.Charts[Symbol]['RSI'])
            
            # Create a custom volume chart
            VolChartName = Symbol+" Volume"
            self.Charts[Symbol]['VOL'] = Chart(VolChartName, ChartType.Stacked)
            self.Charts[Symbol]['VOL'].AddSeries(Series('Buying Volume', SeriesType.Bar))
            self.Charts[Symbol]['VOL'].AddSeries(Series('Selling Volume', SeriesType.Bar))
            self.AddChart(self.Charts[Symbol]['VOL'])
        
        # Ensure that the Indicator has enough data before trading,.
        # x5 to get enough weekly data
        self.SetWarmUp(RSI_Period*5)
    
    def On_W1(self,sender,bar):
        '''
        This method will be called every time a new 30 minute bar is ready. 
        
        '''

        # Make sure we are not warming up 
        if self.IsWarmingUp: return
    
        Symbol = str(bar.get_Symbol())
        self.Plot(Symbol+' RSI', 'W1', self.Indicators[Symbol]['RSI']['W'].Current.Value)
    
        
    def OnData(self, data):
        '''OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here.

        Arguments:
            data: Slice object keyed by symbol containing the stock data
        '''
        
        # Make sure we are not warming up 
        if self.IsWarmingUp: return
        
        # Loop through our equities
        for Symbol in self.Equities:
            
            # Add some alias for reading ease
            Close = data[Symbol].Close
            Volume = data[Symbol].Volume
            D1_RSI = self.Indicators[Symbol]['RSI']['D'].Current.Value
            W1_RSI = self.Indicators[Symbol]['RSI']['W'].Current.Value
            
            self.Debug("{}: Close: {} RSI: {}".format(Symbol, Close, D1_RSI))
            
            if data[Symbol].Close >= data[Symbol].Open:
                self.Plot(Symbol+" Volume", 'Buying Volume', Volume)
            else:
                self.Plot(Symbol+" Volume", 'Selling Volume', Volume)
                
            self.Plot(Symbol +' RSI', 'D1', D1_RSI)
        
            # Determine our entry and exit conditions
            # Do it here to avoid long lines later
            Long_Cond1 = D1_RSI < self.RSI_OS
            Long_Cond2 = W1_RSI < self.RSI_OS
            Exit_Cond1 = D1_RSI > self.RSI_OB
            Exit_Cond2 = W1_RSI > self.RSI_OB  
        
            if not self.Securities[Symbol].Invested:
                # If not, the long conditions
                if all([Long_Cond1, Long_Cond2]):
                    # Buy!
                    self.SetHoldings(Symbol, self.Allocate)
            else:
            
                if all([Exit_Cond1, Exit_Cond2]):
                    # Sell!
                    self.Liquidate(Symbol)

Code Commentary

The first change we have is simply to add a list of FANG stocks to join AAPLin our algorithm. This list of stocks is used everytime generate a for-loop. Next, we create a series of dictionaries, these dictionaries are used to store data specific one each symbol in our list.  This can be seen in our first loop where we loop through the list adding consolidators, indicator, and charts to our dictionaries whilst using the symbol as the key. Using the symbols as the key allows us to easily retrieve or update the correct indicator or chart later when we start receiving data. self.Indicators[Symbol]['RSI']['D'] = self.RSI(Symbol, RSI_Period) In the example above, we are creating a Daily RSI indicator that can easily be retrieved later like so: self.Indicators['AAPL']['RSI']['D'] In reality we never actually specify AAPL. Instead, we always pass theSymbolvariable from the for-loop. However, I have written AAPL here for illustration purposes as it is also a valid way to access it and also helps to cement the concept.

On_W1()

In the code, we have assigned the same handler to all of the individual consolidators. Determining what to do with the data is actually easier than it may first seem. In our example, we just use the bar.get_Symbol()method to get the symbol/ticker of the asset which the bar belongs to. From there it is easy to update the correct plot.
Symbol = str(bar.get_Symbol())
self.Plot(Symbol+' RSI', 'W1', self.Indicators[Symbol]['RSI']['W'].Current.Value)

OnData()

As mentioned before the dataslice passed to the OnData()call contains a snapshot of data for all of our symbols at the same time. Therefore, just like during Initialize(), we can simply loop through our self.equtitieslist to asses the information we need, update the plots and make a decision whether we want to enter. One additional thing worth mentioning is that we no longer use self.portfolio.invested to determine whether we are in a position. Instead, we switch to self.Securities[Symbol].Invested as we will allow more than one open position at a time. We just want to check that we are not placing a second order on the same stock.

Running The Code

After running the code you should see something that looks like this: Chart Showing Multiple Instruments Example Code Results As you can see we have a separate chart on the right-hand side for each of the RSI and Volume charts.  Overall we entered 5 trades over the test period. If you click on the “Trades” tab, you will see that we have placed trades across each of the assets in our list. Image showing trades list

Find This Post Useful?

If this post saved you time and effort, please consider donating a coffee to support the site!  


Dontate with PayPal using any payment method you are comfortable with. 


3PUY12Tgp8xynrMCbBdLE56DShzCbxFG8i

0x9c32a2e1e4a06b0995777ac86745c0db1c13bdfc

LUph5xfqvn2bNfthhEcw9QiVLBYeztacFR