Equal weight all sector strategy vs. SPY

We have gotten a number of great questions from folks trying to understand the apparent outperformance of the Equal Weight All-Sector rebalance algorithm we have shared as sample Quantopian live trading algorithm. The answer seemed lengthy enough to warrant a new post on the topic.

This algorithm is designed to maintain a constant equal-weighted exposure across all equity market sectors using a basket of sector ETF products rebalanced monthly. To understand how and why there are differences in returns to an equal-weight all sector strategy, as compared with simply buying and holding an S&P 500 Index ETF (like SPY) we need to compare the relative sector exposures of each strategy. The SPY seeks to track the performance of the S&P 500 Index, which is a "market cap weighted" index – meaning that each stock is held according to their current size. A snapshot taken as of market close on April 14th reveals that the equal-weighted strategy currently holds a portfolio that is overweight utilities, materials and consumer staples sectors, and underweight health care, financials and technology versus the all market S&P500.

During times of high cross-sector correlation in the markets, this over/underweight exposure will be relatively insignificant and the equal-weighted strategy will tightly track the overall market. However, during times of variable cross-sector performance, for example the recent steep tech sell-off, the equal-weighted strategy returns can deviate significantly from the overall market.

In general you can think of a market-cap weighted strategy as being a momentum strategy in that stocks with recent outperformance and hence growth are rewarded with a larger weight in the portfolio (e.g. the current overweight positions in HC, Financials and Tech in the SP500). Conversely, an equal-weighted strategy is by nature a mean reversion strategy, in that with each rebalance back to equal-weight you will actually be trimming recent ‘winners’ and buying recent ‘losers’ to maintain a constant exposure.

While neither strategy is guaranteed to win in the future, this backtest shows that over the last 12 years or so (from January 2002 through today) an equal-weighted strategy would have beaten out the market-cap weighted SPY.

388
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
 Returns 1 Month 3 Month 6 Month 12 Month
 Alpha 1 Month 3 Month 6 Month 12 Month
 Beta 1 Month 3 Month 6 Month 12 Month
 Sharpe 1 Month 3 Month 6 Month 12 Month
 Sortino 1 Month 3 Month 6 Month 12 Month
 Volatility 1 Month 3 Month 6 Month 12 Month
 Max Drawdown 1 Month 3 Month 6 Month 12 Month
'''
This algorithm defines a target long-only equal weight portfolio and rebalances it at a user-specified frequency
NOTE: This algo is intended to run in minute-mode simulation and is compatible with LIVE TRADING.

'''
# Import the libraries we will use here
import datetime
import pytz
import pandas as pd

# The initialize method will be called once to set up context variables
def initialize(context):

# Define the stocks in your target portfolio:
context.secs =   [ sid(19662),  # XLY Consumer Discrectionary SPDR Fund
sid(19656),  # XLF Financial SPDR Fund
sid(19658),  # XLK Technology SPDR Fund
sid(19655),  # XLE Energy SPDR Fund
sid(19661),  # XLV Health Care SPRD Fund
sid(19657),  # XLI Industrial SPDR Fund
sid(19659),  # XLP Consumer Staples SPDR Fund
sid(19654),  # XLB Materials SPDR Fund
sid(19660) ] # XLU Utilities SPRD Fund

context.rebalance_date = None
context.Rebalance_Days = 30
context.weights = 0.95/len(context.secs)
context.rebalance_hour_start = 10
context.rebalance_hour_end = 15

#set commissions and slippage to 0 since they will be handled by IB paper account

def handle_data(context, data):

# Get the current exchange time, in local timezone
exchange_time = pd.Timestamp(get_datetime()).tz_convert('US/Eastern')

#If its rebalance day (defined by user params) then rebalance:
if  context.rebalance_date == None or exchange_time > context.rebalance_date + datetime.timedelta(days=context.Rebalance_Days):

#   Do nothing if there are open orders:
if has_orders(context):
print('has open orders')
return

rebalance(context, data, exchange_time)

def rebalance(context, data, exchange_time):
#Only rebalance if we are in the user specified rebalance window
if exchange_time.hour < context.rebalance_hour_start or exchange_time.hour > context.rebalance_hour_end:
return

for sec in context.secs:
order_target_percent(sec, context.weights, limit_price=None, stop_price=None)

context.rebalance_date = exchange_time
log.info("Rebalanced to target portfolio weights at %s" % str(exchange_time))

def has_orders(context):
#Return true if there are pending orders.
has_orders = False
for sec in context.secs:
orders = get_open_orders(sec)
if orders:
for oo in orders:
message = 'Open order for {amount} shares in {stock}'
message = message.format(amount=oo.amount, stock=sec)
log.info(message)

has_orders = True
return has_orders
There was a runtime error.
Disclaimer

The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by Quantopian. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. No information contained herein should be regarded as a suggestion to engage in or refrain from any investment-related course of action as none of Quantopian nor any of its affiliates is undertaking to provide investment advice, act as an adviser to any plan or entity subject to the Employee Retirement Income Security Act of 1974, as amended, individual retirement account or individual retirement annuity, or give advice in a fiduciary capacity with respect to the materials presented herein. If you are an individual retirement or other investor, contact your financial advisor or other fiduciary unrelated to Quantopian about whether any given investment idea, strategy, product or service described herein may be appropriate for your circumstances. All investments involve risk, including loss of principal. Quantopian makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances.

8 responses

Can anyone recommend good ETFs for:

        104: '?',# real estate?
205: '?',#consumer defensive
308: '?',#communication services


Where the numbers correspond to the morningstar codes alluded to in the quantopian documentation and listed here: http://corporate.morningstar.com/us/documents/methodologydocuments/methodologypapers/equityclassmethodology.pdf

I'm not experienced with modifying the algorithms - but could someone add a filter to this one. Was thinking buy only if SPY 50MA is above 200MA or something like that?

All this type of back testing is not formal hypothesis testing. I am not sure if the return difference of the two strategies are statistically significant or not. Maybe a long equal-weight short SPY strategy is more illuminating.

Leverage...?

Hi Jessica,
Thank you for posting your work, it is very interesting.

The attached links show the deviation from the mean relationship for the period of Dec 24, 2014 until June 2015, in standard deviations. The Standard deviation is determined with an algorithm that attempts to model the idea of " a rising tide floats all boats" by deciding just where the tide is, and any deviation from it, is said to be from the mean.

(There are two links.) The point of this data is to show that it can matter when you rebalance. For the one that shows the average, I multiplied by 1.6, so it is not an exact average. (in Excel =AVERAGE(E2:M2) *1.6 )

@Jessica how to convert

ontext.secs = [ sid(19662), # XLY Consumer Discrectionary SPDR Fund
sid(19656), # XLF Financial SPDR Fund
sid(19658), # XLK Technology SPDR Fund
sid(19655), # XLE Energy SPDR Fund
sid(19661), # XLV Health Care SPRD Fund
sid(19657), # XLI Industrial SPDR Fund
sid(19659), # XLP Consumer Staples SPDR Fund
sid(19654), # XLB Materials SPDR Fund
sid(19660) ] # XLU Utilities SPRD Fund

into

context.stocks =symbols(xlu,xle) ----- it results in errors.... thanks...

@Chan, you need to have the symbol names inside quotes, ie

context.stocks = symbols('XLU',  'XLE')


yes Darell already applied context.stocks = symbols('XLU', 'XLE') from

context.secs = [ sid(19662), # XLY Consumer Discrectionary SPDR Fund
sid(19656), # XLF Financial SPDR Fund
sid(19658), # XLK Technology SPDR Fund
sid(19655), # XLE Energy SPDR Fund
sid(19661), # XLV Health Care SPRD Fund
sid(19657), # XLI Industrial SPDR Fund
sid(19659), # XLP Consumer Staples SPDR Fund
sid(19654), # XLB Materials SPDR Fund
sid(19660) ] # XLU Utilities SPRD Fund

it does work after substitution... I'm getting an error message what am I missing...?