After some investigation on how to get started on algo trading, I learned that the important thing is to get started with the bare minimum as soon as possible. A lot of posts on reddit suggest that many spend tons of time reading books, watching videos, but never get their hands dirty with trading. That is the biggest mistake, as you learn best from doing.

QuantConnect

So instead of building my own trading engine and backtesting framework as I initially planned, I have decided to jump in with QuantConnect to get a taste of what strategy development and testing feels like.

What is QuantConnect

QuantConnect is an open-source algorithmic trading platform that provides access to financial data, cloud computing, and a coding environment for designing algorithms.

Why QuantConnect

The main reasons I chose QuantConnect are

  1. It’s free and open source
  2. It uses Python and provides capabilities like backtesting, free data source, and paper trading
  3. It has a nice tutorial for learning

Obviously there are plenty of good platforms for algo trading, so there’s no right or wrong in which platform one goes with. I just happen to use QuantConnect.

Notes On Learning QuantConnect

Bootcamp 101: US Equities

Taking some notes while I go through the Bootcamp 101 on US Equities.

Buy and Hold Equities

  • Each algorithm is a class that inherits from theQCAlgorithmclass.
  • Each algorithm has aninitialize()function which runs once everytime the algo starts. One can set different properties inside theinitialize()function:
    • Starting cash:self.set_cash(300)
    • Equity:self.add_equity("SPY", Resolution.DAILY). Theadd_equitymethod is used for manually requesting assets. It returns a security object. This gives you a reference to the security object you can use later.
    • Date range:
      • self.set_start_date(2017, 1, 1)
      • self.set_end_date(2017, 1, 1)
  • 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 ininitialization(self).
  • Securities in your algorithm can be accessed via theself.securities[symbol]dictionary.
  • The algorithm portfolio dictionary also has helper properties for quick look ups of things like:invested,total_unrealized_profit,total_portfolio_value,total_margin_used.
  • Entries in the Portfolio dictionary are SecurityHolding objects with many properties about your holdings, such as:invested,quantity,average_priceandunrealized_profit. For instance,self.portfolio["IBM"].investedchecks if the portfolio has a position in IBM.
  • To submit a market order, useself.market_order("AAPL", 200).

Buy And Hold With A Trailing Stop

  • To submit a stop-market order. The self.stop_market_order() method has the following arguments: ticker, quantity, and stop_price.
    1
    self.stop_market_order("IBM", -300, 0.95 * self.securities["IBM"].close)
  • Order events are updates on the status of your order. Every order event is sent to thedef on_order_event()event handler, with information about the order status held in anOrderEventobject.
  • The OrderEvent object has a status property with the OrderStatus enum values SUBMITTED, PARTIALLY_FILLED, FILLED, CANCELED, and INVALID. It also contains an order_id property which is a unique number representing the order.
  • When placing an order, QuantConnect returns anOrderTicketobject which can be used to update an order’s properties, request that it is cancelled, or fetch itsorder_id. For instance,
    1
    self.stop_market_ticket = self.stop_market_order("IBM", -300, ibmStockPrice * 0.9)

    Momentum-Based Tactical Allocation

    A tactical asset allocation (TAA) strategy allows us to move the portfolio between assets depending on the market conditions.
  • Tracking multiple assets in your portfolio can easily be done with QuantConnect by simply calling selfadd_equity()twice. Data requested is passed into youron_data()method synchronized in time.
  • The indicator MomentumPercent computes the n-period percent change in the security. The indicator will be automatically updated on the resolution frequency. Themomp()method has the parameters symbol, period, and resolution. For instance,
    1
    self.ibm_momp = self.momp("IBM", 30, Resolution.MINUTE)
  • We prepare our algorithm with a “fast-forward” system called “Warm Up” which automatically pumps historical data to an indicator using theself.set_warm_up()method. Warm up requests should be sent in your algorithminitialize()method.
  • All indicators have a propertyis_readyto let you know they’re ready to be used. Before using an indicator you should make sure its initialized.
  • A benchmark asset should be representative of the portfolio you are trading. Theself.set_benchmark()helper should be called in your Initialize method and takes a single parameter, ticker.
  • To retrieve the numerical value of any indicator, you can use theindicator.current.valueattribute of the indicator. For instance,
    1
    if self.spy_momentum.current.value > 5:
  • We can use this indicator value to dynamically allocate our holdings with a combination of theself.liquidate()method andself.set_holdings()method.
  • We can use the algorithmself.timeproperty to track the current simulation time. In backtesting this fast-forwards through historical data, in live trading this is always the current time (now). Theself.timeproperty is a pythondatetimeobject.

An Example Momentum-based Tactical Allocation Strategy

Below is an example momentum-based tactical allocation algo, where we switch between two equities, namely SPY and BND, based on which one has the higher (50 day percent change) momentum currently:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class MomentumBasedTacticalAllocation(QCAlgorithm):

def initialize(self):

self.set_start_date(2007, 8, 1)
self.set_end_date(2010, 8, 1)
self.set_cash(3000)

self.spy = self.add_equity("SPY", Resolution.DAILY)
self.bnd = self.add_equity("BND", Resolution.DAILY)

self.spy_momentum = self.momp("SPY", 50, Resolution.DAILY)
self.bond_momentum = self.momp("BND", 50, Resolution.DAILY)

self.set_benchmark(self.spy.symbol)
self.set_warm_up(50)

def on_data(self, data):

if self.is_warming_up:
return

#1. Limit trading to happen once per week
if not self.time.weekday() == 1:
return
if self.spy_momentum.current.value > self.bond_momentum.current.value:
self.liquidate("BND")
self.set_holdings("SPY", 1)

else:
self.liquidate("SPY")
self.set_holdings("BND", 1)

Opening Range Breakout

Opening range breakout uses a defined period of time to set a price-range, and trades on leaving that range.

  • Consolidators: Consolidators are used to combine smaller data points into larger bars. We commonly use consolidators to create 5, 10 or 15-minute bars from minute data.
  • Theself.consolidate()method takes aticker,timedelta, andevent handler. For instance,
    1
    self.consolidate("SPY", timedelta(minutes=45), self.on_data_consolidated)
  • self.consolidate should be called in yourinitialize()method so it is initialized only once. You must request data smaller than the bar you aim to consolidate. For example, it is not possible to consolidate daily data into minute bars.
  • Consolidators require an accompanyingevent handlerto receive the output data. The consolidatorevent handlersare functions which are called when a new bar is created.
  • Regardless of the resulting bar type, all data contains a few common properties:bar.time, andbar.price. You can use bar.time to get the current moment the bar was created and bar.end_time to get the time at the end of the bar.
  • As we have requested equities data, the consolidated bar’s type isTradeBar. It has the properties open, high, low, close and volume for a given period of time.
    1
    2
    3
    4
    5
    6
    def on_data_consolidated(self, bar):
    # bar.time
    # bar.open
    # bar.high
    # bar.low
    # bar.close
  • We can use a scale of1to-1inself.set_holdingsto place a long or short order.
  • Scheduled events allow you to trigger code blocks for execution at specific times according to rules you set. We initialize scheduled events in theinitializemethod so they are only executed once.
  • We useself.schedule.on()to coordinate your algorithm activities and perform analysis at regular intervals while letting the trading engine take care of market holidays.
  • Scheduled events need aDateRulesandTimeRulespair to set a specific time, and an action that you want to complete.
    1
    self.schedule.on(self.date_rules.every_day("SPY"), self.time_rules.at(13,30), self.close_positions)
  • We include our asset ticker in ourevery_day(“ticker”) object to specifify that we are calling days there is data for the ticker.
  • The call self.liquidate("ticker")closes any open positions with a market order, and closes any open limit orders.

An example Opening Range Breakout Strategy

Below is an example of an opening range breakout strategy. It first consolidates the data of the equity (Tesla in this case) into 30 minutes bars. If the stock price (closing price on each minute) is greater than the opening 30 minutes bar’s high price, we all in on the stock. On the other hand, if the stock price dips under the opening bar’s low price, we liquidate all of our position in the stock. Since we think the strategy is most effective before 1:30 PM ET, we scheduled a event to trigger a function which will close all of our positio in the stock.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class OpeningRangeBreakout(QCAlgorithm):

opening_bar = None

def initialize(self):
self.set_start_date(2018, 7, 10)
self.set_end_date(2019, 6, 30)
self.set_cash(100000)
self.add_equity("TSLA", Resolution.MINUTE)
self.consolidate("TSLA", timedelta(minutes=30), self.on_data_consolidated)

# Create a scheduled event triggered at 13:30 calling the ClosePositions function
self.schedule.on(self.date_rules.every_day("TSLA"), self.time_rules.at(13,30), self.ClosePositions)
def on_data(self, data):

if self.portfolio.invested or self.opening_bar is None:
return

if data["TSLA"].close > self.opening_bar.high:
self.set_holdings("TSLA", 1)

elif data["TSLA"].close < self.opening_bar.low:
self.set_holdings("TSLA", -1)

def on_data_consolidated(self, bar):
if bar.time.hour == 9 and bar.time.minute == 30:
self.opening_bar = bar

def ClosePositions(self):
self.opening_bar = None
self.liquidate('TSLA')

Liquid Universe Selection

We use universe selection to algorithmically reduce the number of assets added to our algorithm (the investment “universe”). We want to choose our assets with formulas, not by manually selecting our favorite assets.