This post will guide you through developing your very own trading algorithm in QuantConnect. A familiarity in python and basic finance knowledge is assumed, but I’ll be gentle — promise! Already an expert? Skip to the code.
More comfortable with C#? View the alternate tutorial.
The algorithm we’ll build is based on the principle of a proportionated simple moving average (P-SMA). We will choose a benchmark (SPY in this example) and, based on its simple moving average, decide if the market will go up or down. If we predict the market will go up, we will invest in equities that provide fast growth but increased risk. Otherwise, we invest in safe assets, such as treasury bonds. Proportionated means the decision is not binary. For example, we may calculate 30% of our portfolio should be relatively risk-less and allocate 70% for high growth equities.
Time to code! Any algorithm in QuantConnect starts the same way:
def Initialize(self):
pass
First, we instantiate the class. The name can be anything you like, but it’s important to extend QCAlgorithm. Whenever an algorithm is started, Initialize
is called exactly once and allows us to setup the properties of our algorithm. Let’s begin to flesh out initialize.
self.SetCash(10000)
self.SetStartDate(2016,01,01)
self.SetEndDate(2016,10,14)
# Add all assets you plan on using later
self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
self.qqq = self.AddEquity("QQQ", Resolution.Daily).Symbol
self.tlt = self.AddEquity("TLT", Resolution.Daily).Symbol
self.agg = self.AddEquity("AGG", Resolution.Daily).Symbol
self.benchmark = self.spy
self.risk_on_symbols = [self.spy, self.qqq]
self.risk_off_symbols = [self.tlt, self.agg]
Methods such as SetCash
, SetStartDate
, and SetEndDate
are only applicable when running a back-test. They are completely ignored during live trading.
AddEquity
is essential to any algorithm you write. By adding the equity in the initialize method, the relevant equity data will be made available throughout your algorithm. Resolution.Daily
specifies data will be given with a daily window. Other options are tick, second, minute, and hour.
So what about the .Symbol
and self.my_equity
? Why assign the variable? This is not strictly necessary. Specifically, the following code is all that is required to make the equity data available.
By assigning self.spy
, we can prevent hard coding the string "SPY"
everywhere and use the variable instead. If you’re a little confused about this point, don’t worry. It will become more apparent later on.
And the final two lines? Remember, we want to invest in either high growth or low risk assets depending on the market. risk_on_symbols
will be invested when we want to add risk to our portfolio — predicting an upswing. risk_off_symbols
are our low risk investments. You should feel free to experiment with different symbols. You can add as many or as few equities as you like to either list.
self.Schedule.On(self.DateRules.EveryDay(), \
self.TimeRules.AfterMarketOpen(self.benchmark, 10), \
Action(self.EveryDayOnMarketOpen))
The snippet above will complete our Initialize
method. This is the main driver of your algorithm. It schedules a method called EveryDayOnMarketOpen
to run every day that SPY (our benchmark) is trading, 10 minutes after market open.
Since setup is over with, let’s move on to the heart of the algorithm by defining EveryDayOnMarketOpen
.
#Do nothing if outstanding orders exist
if self.Transactions.GetOpenOrders():
return
Nothing groundbreaking here. We just return immediately if there are any open orders. In theory, this should never happen. Our algorithm will submit market orders 10 minutes after market open, and is run once per trading day. If this block does execute, it’s likely an indicator of a more serious, underlying problem. Nevertheless, better safe than sorry.
slices = self.History(self.spy, 84)
#Get close of last (yesterday's) slice
spy_close = slices["close"][-1]
#Get mean over last 21 days
spy_prices_short = slices["close"][-21:]
spy_mean_short = spy_prices_short.mean()
#Get mean over last 84 days
spy_prices_long = slices["close"]
spy_mean_long = spy_prices_long.mean()
The self.History
method returns a pandas data frame, representing data on the specified equity for the previous 84 days. Our algorithm compares moving averages over two different window sizes, 21 and 84 days. These are arbitrary (but common) intervals. I encourage you to experiment by changing these values. The next two blocks splice the last 21 and 84 closing prices from the data frame and calculate the average.
((spy_mean_short *2 / spy_mean_long) *.25) / \
len(self.risk_on_symbols)
risk_off_pct = (spy_close/spy_mean_short) * \
((spy_mean_long *2 / spy_mean_short) *.25) / \
len(self.risk_off_symbols)
#Submit orders
for sid in self.risk_on_symbols:
self.SetHoldings(sid, risk_on_pct)
for sid in self.risk_off_symbols:
self.SetHoldings(sid, risk_off_pct)
Finally, the exciting stuff! The “risk on” and “risk off” percentages are calculated using our history data. self.SetHoldings
will allocate a percentage of your portfolio to the specified equity. For instance, self.SetHoldings("SPY", 1)
will buy as much SPY as you can afford, 100% of your portfolio. If you have a margin account and want to leverage your position, simply allocate more than 100%. self.SetHoldings("SPY", 2)
will buy twice as many SPY shares as you can actually afford.
That’s it! You now have an algorithm that can trade automatically on your behalf. I encourage you to experiment changing/improving the algorithm on your own.
This example is also available on GitHub.
Should the 84 period code call look like the 21 day? If so there should be [-84:] appended to this line .. bench_prices_long = slices[“close”]
Hi Tt,
Excellent question! The [-n:] syntax gets a subset of the list, from the nth to last item to the very end. For the 21 day code, this is required because the list has the last 84 days in it, and for the short time period we only want to evaluate the most recent 21 days (the 21st to last element to the very end).
Because the entire list consists of 84 days, this is not necessary for the long period. In this case, we can use the entire list, not just a subset.
`bench_prices_long = slices[“close”]` and `bench_prices_long = slices[“close”][-84:]` will result in exactly the same list, since there are 84 elements total, and you specify to get the last 84 items of the list.
How do I know `slices` contains exactly 84 days? It was specified in this line: `slices = self.History(self.benchmark, 84)`
I hope this helps! Let me know if you have any other questions.
Best,
~Eddie
Hello, I’m beginner of Quantconnect
Thank you so much, this article help me to understand more about how to write a strategy.
When I saw the following part, I was very confuse about how this formula come from, could you explain more detail about this?
risk_on_pct = (spy_mean_short/spy_close) * \
((spy_mean_short *2 / spy_mean_long) *.25) / \
len(self.risk_on_symbols)