QuantConnect – A Complete Guide

30 min read

Get 10-day Free Algo Trading Course

Loading

Last Updated on July 18, 2024

QuantConnect Logo

Table of contents

  1. What is QuantConnect?
  2. Why should I use QuantConnect?
  3. Why Shouldn’t I use QuantConnect?
  4. Is QuantConnect free?
  5. Does QuantConnect support Python?
  6. How do I get started with QuantConnect?
  7. The Lab/Terminal and Creating your First Algorithm
  8. What are the basic Backtesting operations in QuantConnect?
  9. How to Create and Backtest a Strategy in QuantConnect?
  10. Final Thoughts
  11. Link to download code

What is QuantConnect?

QuantConnect is an algorithmic trading browser-based platform that lets you develop, test and execute strategies.

Link: https://www.quantconnect.com

They offer terabytes of free financial data and allow both live trading (including paper trading) and backtesting of strategies using either their own data or data from a collection of leading brokerages; supporting Equities, Futures, Options, Forex, CFD, and Cryptocurrencies.

The platform is designed to be a really easy all-in-one location to get you from absolutely nothing to having fully validated and back-tested strategies running live on their infrastructure.

On top of the basic functionality, there are some really great aspects to QuantConnect such as their Strategy Development Framework (a series of pre-made plug-and-play modules covering key aspects of an algorithmic strategy that can be utilized in many different combinations) and their Alpha Stream, which is a feature that lets you attempt to monetize any strategies you might have by “leasing” them out to interested 3rd parties- who can see the insights and trading recommendations generated by your algorithms and also utilize them, without actually seeing their underlying code.

Why should I use QuantConnect?

  • Backtesting and Live trading
  • Paper trading
  • Free data
  • Strategy development framework
  • Alpha stream
  • Forum

Backtesting and Live trading

Most platforms and APIs allow you to easily execute a live trading strategy through them, however, a unique aspect of QuantConnect is just how easy it is to give your strategies a thorough back-test using the platform.

All you have to do is configure a start date and end date for the backtest period, an initial cash amount to allocate to the strategy, and hit “Backtest”- and you’re off!

You are then gifted with some very comprehensive backtest stats and graphs!

Backtesting is free, however, live trading will require a small investment in a monthly Quant Researcher membership ($8/month) and a live trading node ($20/month for the smallest) to power your algorithm.

We go over that in more depth here!

Paper trading

When deploying a strategy to live, it is possible to elect to “paper trade” (trade using pretend money) to test your strategies before committing to using real money.

This is fantastically useful both if you are a novice to trading in general, and for all traders- beginners and experienced alike- to get a better feel that your trading strategy actually is profitable on out-of-sample data before committing to it.

Paper trading can be chosen in the “Select Brokerage” step of the Go Live flow.

Free data

QuantConnect offers a huge amount of free data through the QuantConnect Data Explorer.

Note that whilst this is all free to use within the IDE, it is not necessarily all free to download and use in an external environment- sometimes you will have to pay for that.

We will show you later on how to use any of this data in your algorithms/within QuantConnect’s Jupyter Notebooks for data exploration.

Alpha Stream

QuantConnect’s Alpha Stream is a really neat feature where quants can lease out any Alpha’s they’ve developed in an open marketplace.

An Alpha is an algorithm that generates Insights- predictions detailing whether an asset is expected to go up or down in price, and sometimes also the magnitude and duration to occur of the expected change.

Funds can then use these to help manage their portfolio. Alphas solely focus on this prediction, actively ignoring position sizing, portfolio management, and risk.

Any interested party/fund is able to rent the usage of the Alpha either at a “Shared Price” (where others are able to use the same Alpha also) or an “Exclusive Price” which locks out anybody else from using the same Alpha.

They are then able to use the Alpha to trade their own funds or test it on out-of-sample data but are prevented from seeing the source code. QuantConnect takes special care to make Alpha’s to be as hard to reverse-engineer as possible.

The marketplace shows a bunch of key stats to compare strategies by, helping purchases make an informed decision.

You can read a bit more about creating Alphas here, but we will show you how to create some later anyway in the Using QuantConnect’s Alpha Creation example: Smart Insider / Generating Insights sections of the guide.

Strategy Development Framework

QuantConnect’s Strategy Development Framework (SDF) comprises of plug and play modules designed to make replicating, sharing, and re-using specific components of strategies as easy as possible.

It comprises 5 sections-

  • The Alpha Creation section (algorithms producing insights about markets, indicating whether an asset is expected to go up or down in price, and the magnitude, duration, and confidence of the prediction- as we just discussed).
  • Universe Selection (modules to pick which assets to trade with your strategy- filterable by all sorts of metrics such as liquidity, volatility, marketcap, etc.).
  • Portfolio Construction (modules determining the balance of funds allocated to different assets in your universe).
  • Execution (modules determining how your algorithm will reach the target allocations determined by the Portfolio Construction section. For instance immediate execution or standard deviation- where the algorithm waits for the price to move X standard deviations above or below the recent mean before executing a trade).
  • Risk Management (modules focused on minimizing risks and cutting losing positions early).

As you might have noticed, there is a clear directional flow between the modules.

This may all seem a bit overwhelming at first, but don’t worry- we’ll cover everything in depth with examples in the What is QuantConnect’s strategy development framework? section of the guide!

Forum

Finally, QuantConnect has its very own forum.

You can discuss and share strategies and tests with fellow traders, get help with various known data issues, or check out or participate in the various competitions going on.

Why Shouldn’t I use QuantConnect?

  • Learning Curve
  • Not completely free
  • Your work is stored on their servers

All in all, QuantConnect is a very useful platform.

Learning Curve

The biggest reason not to use it might be that it can feel overwhelming to wrap your head around all the functionality- but that’s what this guide is for!

That said, whilst there is definitely a learning curve, once you do know your way around the platform you will realize how easy a lot of things are made for you.

However, if you just want to dive straight into testing an algorithm, you might prefer to go with a simpler setup you are already familiar with.

Not completely free

Also, not every component of QuantConnect is entirely free- we’ll cover exactly what is and isn’t in just a moment!

Your work is stored on their servers

Your code is stored on QuantConnect’s servers as opposed to your local machine.

Thus, you might be wary of security risks and your code being accessed without your permission.

That said, QuantConnect seems to take security seriously. See their statement here.

On a related note, QuantConnect allows you to self-host their fully open-source algorithmic trading engine, LEAN.

Is QuantConnect free?

Backtesting with the lowest tier of node offered by QuantConnect is completely free.

The minimum investment to be able to live trade on QuantConnect is $8/month for the Quant Researcher membership and a further $20/month for an L-Micro live node (1 CPU/0.5GB RAM).

You can further upgrade your backtesting node and live trading node and also purchase a research node of various tiers at additional expense to speed up your backtesting/allow parallel backtesting.

It can be worth spending at least $40/month in total on various components as that unlocks QuantConnect’s “Bronze tier” which will give you access to email support with 4 available tickets per month.

Go here to see the plans.

QuantConnect also offers a massive amount of historical data which is free to use within the IDE, but you may have to pay on a per-dataset basis to download outside of QuantConnect.

If you are a student, QuantConnect very kindly offers a free year’s access to their researcher tier!

Does QuantConnect support Python?

Yes, QuantConnect supports both Python and C# within its IDE, but note that the two languages have slightly different sets of available modules in the strategy development framework.

How do I get started with QuantConnect?

Signing-up and membership

First, head on over to the sign-up page and create an account.

If you just want to backtest you need to do nothing more. Head over to the lab/terminal and let’s get started building some algorithms!

If you want to live trade head over to the account upgrade page and purchase yourself a Quant Researcher membership and a live trading node- though you can wait till after back-testing some strategies to do this if you want.

Docs

We are going to try and give you a crash course in some of the key functionality of QuantConnect in this guide, but in case you need further help and/or want to learn more in general, QuantConnect has some very thorough documentation.

Bootcamp

Equally useful is the Bootcamp, which you can find in the lab/terminal section of QuantConnect.

Here you will be guided step by step through some code/algorithms covering basic functionality on QuantConnect, with hints and full solutions provided.

The lessons can be very useful to refer to if you forget exactly how to implement something, or just want a template to adapt slightly for your specific use case.

The Lab/Terminal and Creating your First Algorithm

Creating your First Algorithm

The Lab/Terminal is where you will write all your algorithms.

There used to be a distinct divide between “classic” (only your own code- no SDF modules) and “framework”/SDF algorithms on QuantConnect, however nowadays the separation is more fluid.

Click on “Create new Algorithm” on the left-hand panel to get started.

As you can see in the right-hand side of the image below, main.py is where all your code goes.

If you just want to write your own classic algorithm from scratch, this is where you will code.

If you want to use some of the Strategy Development Framework modules, they will be inserted as pre-made objects into your code on the right.

Adding the Smart Insider module

The basic process is to select the SDF modules you want to use (or skip some/all of them if you prefer), then press “Create Algorithm” in the bottom right.

You can add as many Alpha modules as you want, but you can only select a single module for the other sections of the framework.

Once you’ve pressed Create Algorithm, you’ll be able to write code manually within main.py.

Basic Structure

If you create an algorithm with some SDF modules you should be left with a main.py that looks something like this:

If you were to not use any SDF modules, you’d be left with the most basic setup possible that looks like this:

Let’s go with the basic setup for now to explain things.

The first line class CalibratedUncoupledCoreWave(QCAlgorithm): creates our algorithm.

The name CalibratedUncoupledCoreWave is arbitrary and can be changed to whatever you want- QuantConnect likes to give you crazy names for fun.

The important part is that the algorithm inherits from the QCAlgorithm class, allowing it to use its methods and variables, so don’t change this bit.

Initialize and OnData Methods

The def Initialize(self): and def OnData(self, data): methods are always present in an algorithm.

You can of course write your own additional methods/functions and get them to activate at certain times in Initialize() or OnData()– we’ll explain as we go along!

Initialize()

def Initialize(self): is run before backtesting/live trading commences. In it we set important variables, modules, add data and warm up indicators, and so forth.

We can also use Scheduled Events in Initialize() to trigger code to run at specific times of the day.

Setting Dates and Cash

Here we can see it has self.SetStartDate(2018, 4, 1)and self.SetCash(100000) methods. These set the starting date for a backtest and the cash allocated to the algorithm, to begin with.

You can also add self.SetEndDate() to determine the end date of a backtest. If this is not set, the backtest runs until the present date by default.

def Initialize(self):
    self.SetStartDate(2018, 4, 1)  # Set Start Date
    self.SetEndDate(2020, 4, 1)  # Set End Date
    self.SetCash(100000)  # Set Strategy Cash

SetStartDate() and SetEndDate() are ignored during live trading for obvious reasons.

Adding Data

We also add data within the Initialize()method.

The general template to do so is very simple.

For equities we use self.AddEquity() which returns a security object that we can assign internally for future use, for example with the SPY:

self.spy = self.AddEquity("SPY", Resolution.Hour, Market.Oanda)

Resolution is the time period of a data bar, and the default options for Equities are:

  • Resolution.Tick
  • Resolution.Second
  • Resolution.Minute
  • Resolution.Hour
  • Resolution.Daily

The Market parameter dictates which market the data is drawn from, in case multiple markets track the same asset and you wish to take data from a specific one. QuantConnect has default markets it uses for each asset if you do not specify anything.

There are also:

  • AddForex()
  • AddCrypto()
  • AddCfd()
  • AddOption()
  • AddFuture()

methods for their respective asset classes that all behave very similarly.

The methods all have some other more specialized parameters that can often be ignored, of which there are too many permutations to concisely list for each method we use throughout this guide.

If you ever wish to get a full rundown of available functionality, click on “API” on the right-hand panel of the terminal and search the relevant method/term, as shown below:

Setting Indicators

We would also set up indicators that we wanted to use in the main algorithm in Initialize(), such as RSI or MACD.

For example for an RSI indicator, we would set key variables such as the look-back period and overbought and oversold levels, remembering to set them to self so that they are referenceable throughout different methods in the algorithm.

RSI_Period    = 14            # RSI Look back period 
self.RSI_OB   = 60            # RSI Overbought level
self.RSI_OS   = 40            # RSI Oversold level

We would then set up an RSI indicator object (which is inherited from the QCAlgorithm class) for the SPY using our desired RSI look-back period:

self.RSI_Ind_SPY = self.RSI("SPY", RSI_Period)

Setting Warm Up Period

You can also set a “warm up” period that allows components such as technical indicators, historical data arrays, and so forth to prime or populate prior to the main algorithm going live. No trades will be executed during the warm up period.

To do so, use the SetWarmUp() method shown below.

self.SetWarmUp(200) # Warm up 200 bars for all subscribed data.
self.SetWarmUp(timedelta(7)) # Warm up 7 days of data.

# Or perhaps ensure that the RSI indicator has enough data before trading. 
self.SetWarmUp(RSI_Period)

Also note, algorithms can use the boolean IsWarmingUp to determine if the warm-up period has been completed.

Scheduled Events

You can schedule codeblocks (methods/functions) to run at specific times of a day independent of when your algorithm receives a data update with Scheduled Events.

Scheduled events require a date and time rule to specify when the event is fired alongside a function/method to fire, and go inside Initialize().

The method to schedule an event is self.Schedule.On(DateRule, TimeRule, Action).

Here are some examples DateRules and TimeRules.

To see full lists go to scheduled events documentation and expand the DateRule and TimeRule documentation:

Here is an example of a scheduled event that is set firstly to run every day, and then more specifically every 10 minutes within every day- setting off the method/function self.ExampleFunc, which could be absolutely anything we want it to be:

self.Schedule.On(self.DateRules.EveryDay(),  
                 self.TimeRules.Every(timedelta(minutes=10)), 
                 self.ExampleFunc)

In summary, this is what our algorithm structure might look like now having filled out Initialize() with a bunch of the features we’ve just learned:

OnData()

def OnData(self, data): is activated each time new data is passed to your algorithm, so this can be hourly, daily or weekly, etc. depending on the data resolution you request in Initialization(self).

You might fire trading logic in here, or update indicators or dataframes.

Remember you can also activate functions on regular time intervals/certain events not related to a data update with Scheduled Events.

There are also other functions like OnHour() or OnEndOfDay() inbuilt to QuantConnect that fire on their respective timeframes. We will gradually introduce these as the guide goes on to keep things manageable.

Backtesting vs Live trading

Writing code for backtesting and live trading is exactly the same- it all goes in the main.py panel.

To perform a backtest you simply need to press the “Backtest” button with at least the SetStartDate()and SetCash()variables set.

To start live trading you need to press “Go Live”, then select a brokerage you have an account with (or opt to paper trade), select your data source and the trading node you wish to deploy with.

You will need account usernames and passwords and/or API keys depending on the brokerage to connect to your account- and hopefully, some money in the account to trade with as well!

Also, note there is a useful self.LiveMode() boolean that your algorithm can use to tell if it’s live. For instance:

if self.LiveMode:
      # execute this code only if algorithm is in live trading mode

Research (Jupyter notebooks)

Finally, QuantConnect comes with internal Jupyter notebook environments that you can access within your projects.

These can be useful for data exploration- we will use them later on in this guide to demonstrate how to get and visualize historical and fundamental data.

What are the basic Backtesting operations in QuantConnect?

These methods are mainly meant for the Classic backtesting method.

How can I place orders using QuantConnect?

QuantConnect allows you to place a variety of orders.

Below is an overview of the different types of orders available and the key parameters you need to provide for each one:

Note that all orders return an OrderTicket object, which can be used to query the status of, update or cancel orders- we will show you how to in the next sections!

# marketOrderTicket is the OrderTicket object returned for our market order 
marketOrderTicket = self.MarketOrder("SPY", 100)

Market orders execute immediately and buy up or down the order book starting from the current market price until filled, so be aware of potential slippage on larger orders in less liquid markets.

Since market orders theoretically execute almost immediately, you’re not likely to want or be able to cancel or update them.

You could however use the marketOrderTicket object to check the average fill price of your market object like so:

 # Check the average fill price of your market order using your OrderTicket
self.Debug("Market Order Fill Price: {0}".format(marketOrderTicket .AverageFillPrice))

Note that in the QuantConnect terminal any code you have will pause for 5 seconds after executing a market order, to give time for the order to fill.

If you wish to adjust how long this pause is, use the following line of code- changing the seconds value as desired:

# Adjust the market fill-timeout to 10 seconds.
self.Transactions.MarketOrderFillTimeout = timedelta(seconds=10)

If you wish to fire a large number of trades at once and not wait for a response to each one, it is also possible to set them to fire asynchronously (with zero delay) by adding the True parameter to the market order:

# Create a Market Order for 100 shares of SPY asynchronously 
self.MarketOrder("SPY", 100, True)

To sell instead of buy we must use a negative quantity, like so:

# Create a Market Order to sell 100 shares of SPY asynchronously 
self.MarketOrder("SPY", -100, True)

Limit orders do not fill immediately- rather only when the specified buy or sell price is reached

Here is an example of how to place a limit order that executes 5% below the current price (5% below the close price of the last data bar):

# Purchase 10 SPY shares when its 5% below the current price
close = self.Securities["SPY"].Close
limitTicket = self.LimitOrder("SPY", 10, close * .95)

How can I set stop losses and take profits using QuantConnect?

Stop Losses

Unfortunately, QuantConnect does not allow the usage of trailing stop losses.

That said, we can still set standard stop losses to protect us from excessive downside.

To do so we use the StopMarketOrder(symbol, quantity, price) method.

Remember that to sell instead of buy we must use a negative quantity (this holds true for all the different order types).

For example, this is how we could set a stop loss to market sell 10 shares of IBM if the price touched or went below $18:

stopMarketTicket = self.StopMarketOrder("IBM", -10, 18)

And this is an example of how we could set a limit order to buy 10 shares of SPY 5% below the current price, and set a market stop loss for all 10 shares a further 3% below our limit buy to protect ourselves from downside:

# Purchase 10 SPY shares when its 5% below the current price
current_price= self.Securities["SPY"].Close
limitTicket = self.LimitOrder("SPY", 10, current_price* .95)

# And set stop loss 3% below purchase price
stopMarketTicket = self.StopMarketOrder("SPY", -10, current_price* 0.95*0.97)

Take Profits

To take profits once an asset has risen in value a pre-determined amount from our buy in points, we want to set limit sell orders.

This is how we could set a take profit for 10shares of SPY at $350(again remember the negative quantity):

limitTicket = self.LimitOrder("SPY", -10, 350)

Carrying on from our previous example, this is how we could attach take-profits- half each at 5% and 10% above our limit buys for 10 shares of SPY- on top of our protective stop loss 3% below:

# Purchase 10 SPY shares when its 5% below the current price
current_price= self.Securities["SPY"].Close
limitTicket = self.LimitOrder("SPY", 10, current_price* .95)

# Set stop loss 3% below purchase price
stopMarketTicket = self.StopMarketOrder("SPY", -10, current_price* 0.95*0.97)

# And set take profits 5% and 10% above purchase price
tp1Ticket = self.LimitOrder("SPY", -5, current_price*1.05)
tp2Ticket = self.LimitOrder("SPY", -5, current_price*1.1)

Note that in practice you’d probably want to update or delete the stop loss if either of the take profits hit, or delete the take profits if the stop loss hits- we’ll show you how to in the next section!

How can I cancel orders or Liquidate my Portfolio using QuantConnect?

Cancelling orders

We can cancel an order simply by using the Cancel(optionalDescriptiveTextString) method of the OrderTicket object.

This is how we can set a limit buy order, and later on decide to cancel it:

# Create an order and save its ticket
limitTicket = self.LimitOrder("SPY", 10, current_price* .95)

# Cancel order and save response
response = limitTicket.Cancel("Cancelled SPY Trade")

We can use the response OrderResponse object adjusting a OrderTicket returns to see if our alteration has been successful:

# Use order response object to read status
if response.IsSuccessful:
     self.Debug("Order successfully cancelled")

Also, you can cancel all open orders using Self.Transactions.CancelOpenOrders():

# Cancel all open orders
allCancelledOrders = self.Transactions.CancelOpenOrders()

Or you can cancel all open orders only related to a particular asset by providing its string to CancelOpenOrders(), like so:

# Cancel orders related to SPY
spyCancelledOrders = self.Transactions.CancelOpenOrders("SPY")

So for instance to extend our previous example, if our stop loss hit we could cancel all remaining open orders for SPY (which would be the take profit limit orders at that point) to clean things up- allowing us to simulate a OCA/One-Cancels-All style of order in a round about way (QuantConnect doesn’t allow direct OCA orders).

Liquidation

You can also liquidate all of a particular asset in your portfolio or all of your portfolio immediately with self.Liquidate(optionalAssetString) like so:

# Liquidate all SPY in your portfolio
self.Liquidate("IBM")

# Liquidate entire portfolio
self.Liquidate()

This both cancels all open orders for the asset/whole portfolio then creates market sell orders for your holdings of the asset/whole portfolio- returning you 100% to cash in record speed.

How can I update orders using QuantConnect?

To update an order you must use its OrderTicket.

You can update the following attributes of an order:

Orders are updated by passing a UpdateOrderFields object to the Update()method of the OrderTicket.

Lets create an order we will then update:

# Create an order
limitTicket = self.LimitOrder("SPY", 10, 221)

To update the order first create an UpdateOrdersField object:

# Create an UpdateOrderFields object
updateSettings = UpdateOrderFields()

Update some of the order parameters:

updateSettings.LimitPrice = 219
updateSettings.Quantity= 15

Then pass the populated UpdateOrderFields object to the Update method of the order ticket, which again creates an OrderResponse that can be used to check that the update was successful:

response = limitTicket.Update(updateSettings)

# Validate the response is OK
if response.IsSuccessful:
     self.Debug("Order updated successfully")

We’ve now gone through some of the key functionality of the order placement and management system on QuantConnect, but do check out the trading-and-orders documentation for a full rundown of what is possible!

How can I get historic data using QuantConnect?

Firstly, it’s important to note you can access historic data either directly in your algorithm in the terminal or in a Research Notebook.

In both cases you will use the History(symbol[], time period/bar period/start + end time period, resolution = null) method and the historic data is returned in a pandas dataframe.

  • Symbol[] can be a single string or a list of strings
  • time period/bar period/start + end time period is a little confusing because QuantConnect’s API lets you express the time period in three different ways- we will give some examples in a sec!
  • resolution is the time period of a data bar/row in your dataframe- Minute/Hour/Daily etc.

In the terminal/your algorithm:

First, subscribe to the asset’s data:

self.AddEquity("SPY", Resolution.Daily)

Then use the History() method to return a pandas dataframe with your preferred of the three time period options:

# Returns the past 10 days of historical hourly data
self.df= self.History(self.Symbol("SPY"), timedelta(days=10), Resolution.Hour)
# Returns the past 10 bars of historical hourly data
self.df= self.History(self.Symbol("SPY"), 10, Resolution.Hour)
start_time = datetime(2019, 1, 1) # start datetime for history call
end_time = datetime(2020, 1, 1) # end datetime for history call

# Returns hourly historical data between January 1st 2019 and January 1st 2020
self.df= self.History(self.Symbol("SPY"), start_time, end_time, Resolution.Hour)

In a Research Notebook:

Very similar except instead of referring to self, you need to create a QuantBook() object in the notebook and then refer to that.

# Create QuantBook() object
qb = QuantBook()

# Subscripe to SPY data
spy = qb.AddEquity("SPY", Resolution.Daily)

Note the only difference is that you don’t need Self. infront of df and you refer to spy.Symbol instead of self.Symbol("SPY") because you are in a standalone notebook- not within a QCAlgorithm object:

# Returns the past 10 days of historical hourly data
df = qb.History(spy.Symbol, timedelta(days=10), Resolution.Hour)

Now let’s pretend we are back within the terminal/an algorithm and show you a few different things we can do with historic data.

Multiple Tickers in a Dataframe

We can access more than one ticker’s data in the same dataframe:

# Subscribe to data from multiple tickers
self.AddEquity("IBM", Resolution.Daily)
self.AddEquity("AAPL", Resolution.Daily)

# Set start and end time.
start_time = datetime(2019, 04, 25) # start datetime for history call
end_time = datetime(2020, 04, 27) # end datetime for history call

self.dataframe = self.History([self.Symbol("IBM"), self.Symbol("AAPL")], start_time, end_time)

Note that because we didn’t select a Resolution, QuantConnect’s API used the default for securities which is daily.

Transforming Tickers into Columns and Comparing by Attribute

We can also transform the tickers to columns and pick out a single attribute to easily compare them side by side for that attribute for each bar period as follows using unstack(level=0):

# Transforming using unstack and comparing tickers by "close" value
self.dataframe["close"].unstack(level=0)
Comparing daily “close” prices for AAPL and IBM

How can I get fundamental data using QuantConnect?

Unfortunately, QuantConnect doesn’t offer direct access to fundamental historical data within algorithms.

That said, you can still filter assets you’d like based on current fundamental data using the Coarse and Fine Universe selection modules from the SDF in an algorithm.

Also, you can access historical fundamental data in Research notebooks- so let’s do that!

GetFundamental()

To grab historic fundamental data we are always going to use the  qb.GetFundamental(Symbols, Selector, StartDate, EndDate) method.

Symbols, StartDate and EndDate behave as in the History() method we just used.

If you don’t specify StartDate and EndDate, QuantBook will get all the fundamental data starting from January 1st, 1998.

Selector is how we select a specific piece of fundamental data. Check out the fundamentals section of QuantConnect’s data-library to see the enormous range of selectors possible.

P/E ratios- Multiple tickers per Dataframe

Let’s use the ValuationRatios.PERatio selector to try and evaluate how well-priced a few stocks are.

Firstly, let’s subscribe to some symbols:

qb = QuantBook()
amzn = qb.AddEquity("AMZN")
goog = qb.AddEquity("GOOG")
ibm = qb.AddEquity("IBM")

And then add a start and end date and call the GetFundamental() method using the ValuationRatios.PERatio selector:

start_time = datetime(2020, 1, 1) # January 1st 2020
end_time = datetime.now() # Today's date

# Get the PE ratio for all securities between given dates
pe_history = qb.GetFundamental(qb.Securities.Keys, "ValuationRatios.PERatio", start_time, end_time)

pe_history

This is what that looks like:

We could plot the P/E ratios overtime to make visualization easier using:

# Plot PE ratios
pe_history.plot(figsize=(16, 8), title="PE Ratio Over Time")
plt.xlabel("Time")
plt.ylabel("PE Ratio")
plt.show()

Or we could sort a list of stocks by their mean p/e ratio over the searched time period as such:

# Sort stocks by their average PE ratio
sorted_by_mean_pe = pe_history.mean().sort_values()
sorted_by_mean_pe
IBM R735QTJ8XC9X       12.942418
GOOCV VP83T1ZUHROL     29.339131
AMZN R735QTJ8XC9X     107.074606
dtype: float64

This of course would be much more useful with a much bigger list of stocks!

Finally, we can filter down the dataframe to display results of only a single ticker by simply referencing that ticker in square brackets from the dataframe like so:

pe_history["AMZN R735QTJ8XC9X"]

You can investigate absolutely any other fundamental data using exactly the process as with ValuationRatios.PERatio here- just change the selector!

How to Create and Backtest a Strategy in QuantConnect?

Our First Strategy! Mean Reversion on Lean Hog Futures (Classic Backtesting)

We are now going to combine everything we’ve learned so far to demo building a basic algorithm from scratch (no usage of the SDF modules) and then back-testing it.

We will use a basic mean reversion strategy on lean hog futures.

The strategy will be to monitor a moving average of recent price, buy when the price rapidly falls significantly below the recent average and sell when it rises significantly above.

The idea is that substantial deviations from a trend line are likely to be only temporary in either direction even if the overall trend continues to hold.

Such a strategy can be somewhat thought of as “arbitraging noise”.

Note that this example is just for educational purposes. I don’t recommend you run this strategy live unless you understand it very well.

To achieve this we will use Bollinger Bands.

Bollinger Bands are price bands that are X standard deviations above and below a moving average of price.

Bollinger Bands

In our example, we will use the price that is above or below 2 standard deviations of the recent price average as the buy and sell signal, but in practice, you want to pick a number of standard deviations that results in the price being within the Bollinger Bands around 90-95% of the time.

If the price too often hits or exceeds the Bollinger Bands, you may get many false signals.

If the price doesn’t hit them enough, you may not place a meaningful amount of trades, and indeed might only trigger trades when the market is in extreme conditions that cause the price to continue to tank below or pump above the bands even if it is already a substantial deviation from the recent average.

In QuantConnect’s terminal, the QCAlgorithm has a Bollinger Band object pre-coded- BB(symbol, lookbackPeriod, standardDeviations, movingAverageType, Resolution).

  • Symbol: the string of asset you want to chart data for
  • lookbackPeriod: how far back to use to calculate to moving averages (in terms of the resolution)
  • standardDeviations: the amount of standard deviations above and below the moving average to calculate the Bollinger Bands
  • movingAverageType: the type of moving average to use (simple, exponential etc.)
  • Resolution: the bar period to use for the price data (Minute, Hour, Daily etc.)

Implementation

Now let’s have a go building our mean reversion strategy on QuantConnect!

Firstly, let’s create our algorithm, inheriting as always from the QCAlgorithm class, and setup our Initialize() function:

import pandas as pd


class LeanHogsBollingerBandsAlgorithm(QCAlgorithm):

    def Initialize(self):
        
        self.SetStartDate(2015, 1, 1)    #Set Start Date
        self.SetEndDate(2020, 6, 1)      #Set End Date
        self.SetCash(100000)             #Set Strategy Cash

Note we have imported pandas at the start of our code because we will use it later on, and we have not set a WarmUp period because we will warmup our Bollinger band indicator manually.

We will also need to add in Initialize() a self.new_day flag we will use throughout the algorithm and an unassigned self.contract object that we will use to store a specific futures contract.

self.new_day = True
self.contract = None

Then we need to subscribe to the lean hog’s future chain data, which has accessor code Futures.Meats.LeanHogs:

# Subscribe and set our expiry filter for the futures chain
futureES = self.AddFuture(Futures.Meats.LeanHogs)

Trading futures is a bit more complicated than trading normal equities.

Similar to options, futures contracts represent an agreement to buy or sell an asset at a future date at an agreed-upon price.

Unlike options, however, the owner of the contract must buy or sell the asset at the agreed-upon price on that future date, whereas options give the owner the right, but not the obligation, to do so.

As such, a base asset tends to have multiple futures contracts in circulation at any one time- each with different expiry dates for the future. So when you are trading futures, you must pick a specific contract (expiry date) to trade.

Subscribing to futures data does not give you normal price data, but rather a FuturesChain– a collection of information about the different contracts.

In QuantConnect the FuturesChain object contains chains for all the different futures you have subscribed to (for instance, one chain for gold futures, one chain for lean hog futures, one chain for Bitcoin futures, etc.)

You can access the chain of contracts for an individual future by iterating through the FutureChain object as such:

for chain in slice.FutureChains.Values:
    #do something with the specific chain

Since here we have only subscribed to lean hog futures, there is only the lean hog futures chain in the FutureChain object.

In general, you can explore the future contract chain as such:

# Explore the future contract chain
def OnData(self, slice):
    for chain in slice.FutureChains.Values:
        contracts = chain.Contracts
        for contract in contracts.Values:
            # do something with specific contract

A specific futures contract has the following properties:

class FuturesContract:
    self.Symbol # (Symbol) Symbol for contract needed to trade.
    self.UnderlyingSymbol # (Symbol) Underlying futures asset.
    self.Expiry # (datetime) When the future expires
    self.OpenInterest # (decimal) Number of open interest.
    self.LastPrice # (decimal) Last sale price.
    self.Volume # (long) reported volume.
    self.BidPrice # (decimal) bid quote price.
    self.BidSize # (long) bid quote size.
    self.AskPrice # (decimal) ask quote price.
    self.AskSize # (long) ask quote size.

This is all we will need to know for our algorithm, but for further information about futures, check out the docs here.

Since futures contracts are constantly coming into existence and expiring, we are going to need a way to pick a specific futures contract to trade, and know when it is approaching expiry to liquidate our positions in it and rotate to a new contract periodically.

Note there is little point in trading a futures contract very close to expiry as the contract price will converge to the spot price as the expiry date approaches.

Because of that, and the fact we want to give our trades time to play out before rotating to a new contract, we set a filter to subscribe to futures contracts expiring no sooner than 30 days in Initialize().

We also set un upper-bound of 1080 days until expiry to define a time period of contract expiration we want to receive data for:

futureES.SetFilter(TimeSpan.FromDays(30), TimeSpan.FromDays(1080))

We are going to use four other functions besides Initialize(self) in algorithm. They will be:

  • OnData(self, slice)– fires every time data is received (inbuilt in QuantConnect)
  • InitUpdateContract(self, slice) needs to be called someone in our code (custom)
  • OnHour(self, sender, bar)– fires every hour (inbuilt)
  • OnEndOfDay(self)– fires at the end of every day (inbuilt)

Let’s first get OnEndOfDay() out of the way, as we will simply use it to set our new_day boolean to True at the end of every day like so:

def OnEndOfDay(self):
    self.new_day = True

Now let’s work on InitUpdateContract(), which will be our longest function where we select our first contract to trade, check if our current contract is nearing expiry, and if so, roll over to a new contract.

Firstly, if it’s not a new day, we will assume our contract is still fine and does not need to be rolled-over and exit the function (remember, we set our self.new_day to be True in Initialize(), so the first time this function is called we pass this check):

def InitUpdateContract(self, slice):
    # Reset daily - everyday we check whether futures need to be rolled
    if not self.new_day:
        return 

Now we will perform a check to see if we are already trading a contract and if its expiry is at least 3 days away. If both these facts are true, we again skip the rest of the function.

if self.contract != None and (self.contract.Expiry - self.Time).days >= 3: # rolling 3 days before expiry
    return 

If it is a new day, and if we do not have a contract selected or we do and it’s within 3 days of expiry, we print in the backtest logs the name of the contract that’s expiring and how long it has to expire using the self.Log(“message”) method, and liquidate our current positions in preparation to trade a new contract:

for chain in slice.FutureChains.Values:
    # If we trading a contract, send to log how many days until the contract's expiry
    if self.contract != None:
        self.Log('Expiry days away {} - {}'.format((self.contract.Expiry-self.Time).days, self.contract.Expiry))
    
        # Reset any open positions based on a contract rollover.
        self.Log('RESET: closing all positions')
        self.Liquidate()        

We then proceed to select a new contract first by getting a list of the contracts from the future contract chain:

# get list of contracts
contracts = list(chain.Contracts.Values)
chain_contracts = list(contracts) #[contract for contract in chain]

Then order the contracts by expiry date from newest to oldest:

# order list of contracts by expiry date: newest --> oldest
chain_contracts = sorted(chain_contracts, key=lambda x: x.Expiry)

And then by picking out a contract early in that list (remember, we already set a filter so that none of these contracts expire within the next 30 days anyway):

# pick out contract and log contract name
self.contract = chain_contracts[1]
self.Log("Setting contract to: {}".format(self.contract.Symbol.Value))

Because futures data on QuantConnect only comes in Second and Minute format, and we want to build our indicator on an hourly time frame, we will set up a TradeBarConsildator to collect the minute data and bunch it into hourly data:

# Set up consolidators.
one_hour = TradeBarConsolidator(TimeSpan.FromMinutes(60))
one_hour.DataConsolidated += self.OnHour
            
self.SubscriptionManager.AddConsolidator(self.contract.Symbol, one_hour)

To keep things short and because you would normally have more flexible data resolution anyway when trading equities and cryptocurrencies etc., we will skip over exactly how consolidators work. But if you do need to build your own custom one for something else, you can learn about them in the QuantConnect consolidating data docs!

Now we have hourly data for our futures contracts, we will initialize a BollingerBand indicator object:

# Set up indicator
 self.Bolband = self.BB(self.contract.Symbol, 50, 2, MovingAverageType.Simple, Resolution.Hour)

Here we set it to analyze price data from the contract we are trading- calculating a simple moving average on an hourly timeframe with the last 50 hours of data and creating Bollinger bands +- 2 standard deviations from this moving average.

It can be hard to know what to set the sensitivity (standard deviations) to, but for curiosity’s sake this is what weekly lean hog’s future data looks like with Bollinger bands set at +- 1.5 standard deviations:

Since we are trading on hourly data instead of weekly data (and short time frames tend to show more volatility), we upped the standard deviation threshold to 2 as a starting point.

We now grab 50 hours (50*60 minutes) of historical data for our futures contract and manually warm up our Bolband indicator using the Update() method:

history = self.History(self.contract.Symbol, 50*60, Resolution.Minute).reset_index(drop=False)
            
for bar in history.itertuples():
    if bar.time.minute == 0 and ((self.Time-bar.time)/pd.Timedelta(minutes=1)) >= 2:
    self.Bolband.Update(bar.time, bar.close)

We now set self.new_day to False because we have already rolled-forward the contract we are trading on this day, concluding our InitUpdateContract()function:

self.new_day = False

We will now deal with our OnData() function.

This activates whenever new data becomes available for the FuturesChains we have subscribed to.

As such, whenever this function fires, we want to run InitUpdateContract() to check the status of our activate contract, roll over to a new contract if necessary (or pick an initial one at the start of the algorithm), liquidate old positions, and warm up a new Bollingerband indicator:

def OnData(self, slice):
    self.InitUpdateContract(slice)

slice is just the new data object being received that activates OnData().

Finally, we will use the OnHour(self, sender, bar) function inbuilt to QuantConnect to fire every hour to execute our trade logic on an hourly basis (remember, we constructed our Bollinger band indicator around an hourly time-frame as well!).

Firstly, we check that we have initialized a Bollinger band indicator and that it has been successfully warmed up, and that the bar of price data we are interacting with is indeed for the contract we want to trade, and if so, extract the current price for the futures contract:

def OnHour(self, sender, bar):
    if (self.Bolband != None and self.Bolband.IsReady):
        if bar.Symbol == self.contract.Symbol:
            price = bar.Close

Continuing within the previous if statement, we will check the number of contracts we own for the current contract we are trading via self.Portfolio[symbol].Quantity:

holdings = self.Portfolio[self.contract.Symbol].Quantity

Go here to learn a bit more about the Portfolio object and what else you can do with it!

We will now finally implement our trading logic!

If we do not currently own any amount of the contract we are trading and the price for the contract dips below the lower Bollinger band, we will market buy 2 contracts:

# buy if price closes below lower bollinger band
if holdings <= 0 and price < self.Bolband.LowerBand.Current.Value:
    self.Log("BUY >> {}".format(price))
    self.MarketOrder(self.contract.Symbol, 2)

If we already own some contracts and the price rises above the upper Bollinger band, we will sell all our contracts (remember, liquidation executes market sells in QuantConnect):

# sell if price closes above the upper bollinger band
if holdings > 0 and price > self.Bolband.UpperBand.Current.Value:
    self.Log("SELL >> {}".format(price))
    self.Liquidate()

And that’s how we execute a mean reversion strategy on futures!

Just to finish off the function, we will plot our Bollinger bands as the backtest executes, to get a nice visual check that things are working as inspected:

self.Plot("BB", "MiddleBand", self.Bolband.MiddleBand.Current.Value)
self.Plot("BB", "UpperBand", self.Bolband.UpperBand.Current.Value)
self.Plot("BB", "LowerBand", self.Bolband.LowerBand.Current.Value)

The format here is Plot("name of chart to plot in", "name of specific line in chart", valueToPlot).

The function finishes on the outside if level with an else statement to print to the logs that the Bollinger bands have not yet finished warming up if that is the case:

else:
    self.Log('Bollinger Bands not ready yet')

That might have all seemed a bit long and complicated, but futures are inherently tricky to trade and we covered a lot of useful individual features of QuantConnect!

As a whole, our final code should look like this:

import pandas as pd


class LeanHogsBollingerBandsAlgorithm(QCAlgorithm):

    def Initialize(self):
        
        self.SetStartDate(2015, 1, 1)    #Set Start Date
        self.SetEndDate(2020, 6, 1)      #Set End Date
        self.SetCash(100000)             #Set Strategy Cash

        self.new_day = True
        self.contract = None
        
        
        # Subscribe and set our expiry filter for the futures chain
        futureES = self.AddFuture(Futures.Meats.LeanHogs)
        futureES.SetFilter(TimeSpan.FromDays(30), TimeSpan.FromDays(720))
        
        
    def OnData(self, slice):
        
        self.InitUpdateContract(slice)

    def InitUpdateContract(self, slice):
        # Reset daily - everyday we check whether futures need to be rolled
        if not self.new_day:
            return 
            
        if self.contract != None and (self.contract.Expiry - self.Time).days >= 3: # rolling 3 days before expiry
            return 
            
        for chain in slice.FutureChains.Values:
            # If we trading a contract, send to log how many days until the contract's expiry
            if self.contract != None:
                self.Log('Expiry days away {} - {}'.format((self.contract.Expiry-self.Time).days, self.contract.Expiry))
            
                # Reset any open positions based on a contract rollover.
                self.Log('RESET: closing all positions')
                self.Liquidate()
            
            # get list of contracts
            contracts = list(chain.Contracts.Values)
            chain_contracts = list(contracts) #[contract for contract in chain]
            # order list of contracts by expiry date: newest --> oldest
            chain_contracts = sorted(chain_contracts, key=lambda x: x.Expiry)
            
            # pick out contract and log contract name
            self.contract = chain_contracts[1]
            self.Log("Setting contract to: {}".format(self.contract.Symbol.Value))
            
            # Set up consolidators.
            one_hour = TradeBarConsolidator(TimeSpan.FromMinutes(60))
            one_hour.DataConsolidated += self.OnHour
            
            self.SubscriptionManager.AddConsolidator(self.contract.Symbol, one_hour)
            
            # Set up indicators
            self.Bolband = self.BB(self.contract.Symbol, 50, 2, MovingAverageType.Simple, Resolution.Hour)

            
            history = self.History(self.contract.Symbol, 50*60, Resolution.Minute).reset_index(drop=False)
            
            for bar in history.itertuples():
                if bar.time.minute == 0 and ((self.Time-bar.time)/pd.Timedelta(minutes=1)) >= 2:
                    self.Bolband.Update(bar.time, bar.close)
            
            self.new_day = False


    def OnHour(self, sender, bar):
        
        if (self.Bolband != None and self.Bolband.IsReady):
            if bar.Symbol == self.contract.Symbol:
                price = bar.Close
         
                holdings = self.Portfolio[self.contract.Symbol].Quantity
                
                # buy if price closes below lower bollinger band
                if holdings <= 0 and price < self.Bolband.LowerBand.Current.Value:
                    self.Log("BUY >> {}".format(price))
                    self.MarketOrder(self.contract.Symbol, 2)

                # sell if price closes above the upper bollinger band
                if holdings > 0 and price > self.Bolband.UpperBand.Current.Value:
                    self.Log("SELL >> {}".format(price))
                    self.Liquidate()
                        
            self.Plot("BB", "MiddleBand", self.Bolband.MiddleBand.Current.Value)
            self.Plot("BB", "UpperBand", self.Bolband.UpperBand.Current.Value)
            self.Plot("BB", "LowerBand", self.Bolband.LowerBand.Current.Value)
          
        else:
            self.Log('Bollinger Bands not ready yet')
                    
        
    def OnEndOfDay(self):
        self.new_day = True

Now we can proceed to the most exciting bit of all- the backtest!

Hit the Backtest button and the backtest should begin initializing.

It will take a few minutes for this particular backtest to complete- you can either watch the graphs and metrics update in real-time, or just come back when it’s complete!

results of our backtest!

As you can see our strategy had some ups and down, but we actually finished with a 5.62% return- not bad without much calibration!

You can select the charts/metrics you want to see via the panel in the top right. Remember to manually select “BB” as this graph we plotted will not show by default.

Something to note about QuantConnect is that whilst it does appear that you can set a benchmark for futures/options and your code will not throw an error, they do not actually work and you will be shown a benchmark graph of something unrelated instead.

In any case, this is what our Bollinger bands for price look like (which are effectively our own custom-plotted pseudo-benchmark)- we can see we definitely appeared to do better than buying and holding!

Finally, if you scroll down a bit, you will find a bunch of detailed statistics about the backtest:

Here is our backtest Orders history in action:

Orders history

And the Logs history:

Logs history

Final Thoughts

So there you have it- a basic guide on QuantConnect!

By this point, it’s probably self-evident that the biggest drawback to QuantConnect is the huge amount of stuff you have to learn.

The documentation to do so is reasonable but we personally found the example algorithms QuantConnect provides to sometimes be unnecessarily complex and poorly commented- making it difficult to follow where stuff is coming from if you are not already adept.

We also found the occasional bug- like logs not printing from time to time during backtesting for no reason we could find.

Overall, the first time you use QuantConnect might definitely slow you down. However the platform is quite powerful and automates a lot of tasks and data analysis away- it can definitely be worth learning in the long run!

And finally, the Alpha Stream ecosystem is really quite interesting and unique.

You can find the code used in this article here.

Greg Bland