Short sector momentum strategy - it doesn't work

This post is to illustrate that momentum strategies that minimize loss during large drawdowns are not profitable when the inverse of the strategy is employed.

Take for instance the SP500 sector momentum algo, the top 2 outperform the benchmark, and the bottom 2 tend to underperform. In my long algo, I buy the highest x sectors every month and exit if the SP500 month-end price is lower than the 200 day moving average. This produces a good return.

Lets try the exact inverse! If the long strategy works, surely the short version should at least hedge.

Basically the code does nothing until the SP500 dips below its 200 day moving average. Then it shorts the worst performing sector until the sp500 rises above the moving average again.

The result is just awful. The short porfolio does rise during the 2008 recession, and finishes slightly above, but nowhere close to being worth the whipsaw events that bleed portfolio value. The only value it provides is for adrenaline junkies who prefer to see rises and falls in their portfolio and would hate to dump all their portfolio into cash.

To those familiar with moving averages or technical signals in general, this might be intuitive. But I really needed to see it for myself to believe it. I also thought that negative momentum would tip the scales towards a profitable short strategy.

I think I will test it again, but with a combined long/short combination. Today is my only day off in 3 weeks, I hope I don't spend all my time programming.....

2151
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
# For this example, we're going to write a simple momentum script.
# When the stock goes up quickly, we're going to buy;
# when it goes down we're going to sell.
# Hopefully we'll ride the waves.

# To run an algorithm in Quantopian, you need two functions:
# initialize and handle_data
from operator import itemgetter

def initialize(context):
context.topMom = 3
context.rebal_int = 3
context.lookback = 250
set_symbol_lookup_date('2015-01-01')
context.stocks = symbols('XLF', 'XLE', 'XLU', 'XLK', 'XLB', 'XLP', 'XLY', 'XLI', 'XLV')
#context.stocks = symbols('SPY', 'VEA', 'BIL')
#context.stocks = symbols('SPY', 'EFA', 'BND', 'VNQ', 'GSG', 'BIL')
#context.stocks = symbols('DDM', 'MVV', 'QLD', 'SSO', 'UWM', 'SAA', 'UYM', 'UGE', 'UCC', 'UYG', 'RXL', 'UXI', 'DIG', 'URE', 'ROM', 'UPW', 'BIL')
schedule_function(rebalance,
date_rule=date_rules.month_start(),
time_rule=time_rules.market_open())

def rebalance(context, data):
#Create stock dictionary of momentum
MomList = GenerateMomentumList(context, data)

#sell all positions
for stock in context.portfolio.positions:
order_target(stock, 0)
#create % weight
spy = symbol('SPY')
if data[spy].price > data[spy].mavg(200):
return

weight = 0.95/context.topMom

for l in MomList:
stock = l[0]
#if stock in data and data[stock].close_price < data[stock].mavg(50):
if stock in data:
order_percent(stock, -weight)
pass

def GenerateMomentumList(context, data):

MomList = []
price_history = history(bar_count=context.lookback, frequency="1d", field='price')

for stock in context.stocks:
now = price_history[stock].ix[-1]
old = price_history[stock].ix[0]
pct_change = (now - old) / old
#if now > data[stock].mavg(200):
MomList.append([stock, pct_change, price_history[stock].ix[0]])

#sort in descending order, the price change (%)

MomList = sorted(MomList, key=itemgetter(1), reverse=False)
#return only the top "topMom" number of securities
MomList = MomList[0:context.topMom]

return MomList

def change(one, two):
return(( two - one)/one)

def handle_data(context, data):
pass
There was a runtime error.
7 responses

OK. Here is the code with the long/short strategy.

Awful. Just awful.

2151
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
# For this example, we're going to write a simple momentum script.
# When the stock goes up quickly, we're going to buy;
# when it goes down we're going to sell.
# Hopefully we'll ride the waves.

# To run an algorithm in Quantopian, you need two functions:
# initialize and handle_data
from operator import itemgetter

def initialize(context):
context.topMom = 2
context.rebal_int = 3
context.lookback = 250
set_symbol_lookup_date('2015-01-01')
context.stocks = symbols('XLF', 'XLE', 'XLU', 'XLK', 'XLB', 'XLP', 'XLY', 'XLI', 'XLV')
#context.stocks = symbols('SPY', 'VEA', 'BIL')
#context.stocks = symbols('SPY', 'EFA', 'BND', 'VNQ', 'GSG', 'BIL')
#context.stocks = symbols('DDM', 'MVV', 'QLD', 'SSO', 'UWM', 'SAA', 'UYM', 'UGE', 'UCC', 'UYG', 'RXL', 'UXI', 'DIG', 'URE', 'ROM', 'UPW', 'BIL')
schedule_function(rebalance,
date_rule=date_rules.month_start(),
time_rule=time_rules.market_open())

def rebalance(context, data):
#Create stock dictionary of momentum
posMomList = GenerateMomentumList(context, data, True)
negMomList = GenerateMomentumList(context, data, False)
#sell all positions
weight = 0.95/context.topMom
for stock in context.portfolio.positions:
order_target(stock, 0)
#create % weight
spy = symbol('SPY')
if data[spy].price > data[spy].mavg(200):
MomList = posMomList
else:
MomList = negMomList
weight = -weight

for l in MomList:
stock = l[0]
#if stock in data and data[stock].close_price < data[stock].mavg(50):
if stock in data:
order_percent(stock, weight)
pass

def GenerateMomentumList(context, data, rev):

MomList = []
price_history = history(bar_count=context.lookback, frequency="1d", field='price')

for stock in context.stocks:
now = price_history[stock].ix[-1]
old = price_history[stock].ix[0]
pct_change = (now - old) / old
#if now > data[stock].mavg(200):
MomList.append([stock, pct_change, price_history[stock].ix[0]])

#sort in descending order, the price change (%)

MomList = sorted(MomList, key=itemgetter(1), reverse=rev)
#return only the top "topMom" number of securities
MomList = MomList[0:context.topMom]

return MomList

def change(one, two):
return(( two - one)/one)

def handle_data(context, data):
pass
There was a runtime error.

Just my two cents:

Have you tried looking at the time varying correlation of the sectors to the market? Find the betas of the sectors to the market, and you will see why this strategy will not work. For example, if you look at the period from 2009 to 2010, most beta relationships changed drastically after 2008. Previously you are shorting low beta sectors, but after 2009, the same sectors are likely to have much higher beta than before.

When the market rallied after 2009, your sectors in the short leg rose even more than the market, and you are actually selling those sectors and you can see the very sharp dip in performance during that period. This is a typical case of a momentum crash.

Practically if you wanted to trade live, a short only strategy is very difficult to work after accounting for transaction/borrowing costs. In 2008, it is seemingly highly profitable to go big short on the S&P 500, but actually the absurdly high costs of borrowing will reduce much of your returns. (Imagine the risk of lending the stocks to you when the market volatility is at a historical high and the prices are crashing! The brokers will charge a very high premium for lending you the stocks to short)

Another way to improve your sector momentum strategy is to use dual momentum, you not only buy/sell the top x sectors in the cross section but also look at the sector's cumulative return over time. (Buy winners in both time series and cross-section) This combination of relative and absolute sector momentum strategy should have a annualized sharpe of around 0.6 depending on your universe.

http://papers.ssrn.com/sol3/papers.cfm?abstract_id=2371227

Great paper on the momentum crash phenomenon.

Thank you, Mr. Biscuit for your thoughtful response. Other than dual momentum a la Antonacci or moving average exits a la Meb Faber, do you believe there are robust strategies to profit from large market downturns?

Simon: I am reading the paper right now. There is so much I need to learn just to understand this paper.

Same here Johnny. Although I did mention a potential short strategy, I experienced similar results where after 2009, the long-short performed poorly so i switched over to a risk parity (budgeting) approach for long only.

@Johnny Yes, there are indeed robust strategies to profit from downturns (if you can predict the market regime accurately) Using options and futures to take positions is a cheap way to take directional bets in the market. You can even take bets on market volatility by using strangles/ variance swaps. Selling call options on top of your long only portfolio are a good way to profit from low volatility in the market. In a bear and highly volatile market, buying put options is a good way to protect yourself (and profit from) from the downside.

I would imagine a momentum strategy could have a risk trigger (when the VIX goes above a certain level), you would stop the momentum strategy and buy options to hedge against a possible downturn. Remember, these strategies will only perform as long as your prediction for the market regime is fairly accurate, which is an entire study on its own.....

Attached is another approach for downturns with your algo
I couldn’t find an accurate method to go long when market goes up and go short when market is down. The reason is that its hard to know exactly when it goes up and down until you are already in the wave, so if you short every time it goes down you will also short in times it goes up and then losses will accumulate as happened to you. Instead, I prefer to go long with the bull high beta ETF's when market go up and when i identify the market is down, instead of shorting I trade safe ETF's that have low beta and negative or low correlation (like T-Bills and gold ETF's) with the other bull market ETF's. In that way even if I trade the downturn stocks a little bit more than needed, we are not losing (e.g. in the 2008-9 crisis).
Open issues on this version:
1. There is a flat return of the alfo from Sept 2005 to Oct 2008 ... not sure why or how to imrove.

1. The DD till 2011 was decent and low but I couldnt manage to handle a sudden market drop in middle of 2011 (that bring the DD to 20%), that is because the downturn occured in very few days time and the rebalancing is once a month ...

2. Another improvement I tried to implement in your algo is to choose the stocks by fundamental filtering; however, i haven’t got much success so I comment it out. If someone manage to improve it with fundamentals or If you manage to find a way to short instead of using recession proof ETF's - I suspect gains will be higher.

3. Attached is daily execution, in minute execution results are so different, I just dont get it why there is such difference when orders are placed only once a month (and when market open so there is no issue that in daily the orders wont be filled till the next day) - that seems like a bug in quantopian !

33
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
# when market is up: check the momentum of several ETF's and choose the top with historic max pcnt change and trade them for a month.  when market is down then trade recession save equities

# To run an algorithm in Quantopian, you need two functions:
# initialize and handle_data
from operator import itemgetter

def initialize(context):
context.data = []
context.topMom = 2
context.rebal_int = 3
context.lookback = 250
set_symbol_lookup_date('2015-01-01')
context.uptime_stocks = symbols('XLF', 'XLE', 'XLU', 'XLK', 'XLB', 'XLP', 'XLY', 'XLI', 'XLV','JNK')
context.stocks = context.uptime_stocks
context.downtime_stocks = symbols('IEF','GLD','TLT','SHY')
#context.stocks = symbols('SPY', 'VEA', 'BIL')
#context.stocks = symbols('SPY', 'EFA', 'BND', 'VNQ', 'GSG', 'BIL')
#context.stocks = symbols('DDM', 'MVV', 'QLD', 'SSO', 'UWM', 'SAA', 'UYM', 'UGE', 'UCC', 'UYG', 'RXL', 'UXI', 'DIG', 'URE', 'ROM', 'UPW', 'BIL')
schedule_function(rebalance,
date_rule=date_rules.month_end(),
time_rule=time_rules.market_open())

#fundamental_df = get_fundamentals(
# Retrieve data based on PE ratio and economic sector
#    query(
#        fundamentals.valuation_ratios.pe_ratio,
#        fundamentals.asset_classification.morningstar_sector_code,
#    )

# Filter where the Sector code matches our technology sector code
#.filter(fundamentals.asset_classification.morningstar_sector_code == 311)

# Filter where PE ratio is greater than 20
#   .filter(fundamentals.valuation_ratios.pe_ratio > 10)

# Filter where PE ratio is less than 50
#  .filter(fundamentals.valuation_ratios.pe_ratio < 18)

# Order by highest PE ratio and limit to 4 results
# .order_by(fundamentals.valuation_ratios.pe_ratio.desc()).limit(20)
#)
#update_universe(fundamental_df.columns.values)
#context.stocks = [stock for stock in fundamental_df]

#context.stocks.append(symbols('PSQ')[0]) # add inverse ETF to universe

# check if data exists
#for stock in context.stocks:
#    if stock not in context.data:
#        context.stocks.remove(stock)

def rebalance(context, data):
#Create stock dictionary of momentum

#sell all positions
weight = 0.95/context.topMom
for stock in context.portfolio.positions:
order_target(stock, 0)
#create % weight
spy = symbol('SPY')
if data[spy].price > data[spy].mavg(200):
context.stocks = context.uptime_stocks
MomList = GenerateMomentumList(context, data, True)
else:
context.stocks = context.downtime_stocks
MomList = GenerateMomentumList(context, data, False)
print " Recession ......************************"
#MomList = []
#MomList.append([sid(23870)])
#weight = -weight

for l in MomList:
stock = l[0]
#if stock in data and data[stock].close_price < data[stock].mavg(50):
if stock in data:
order_percent(stock, weight)
print stock
pass

def GenerateMomentumList(context, data, rev):

MomList = []
price_history = history(bar_count=context.lookback, frequency="1d", field='price')

for stock in context.stocks:
now = price_history[stock].ix[-1]
old = price_history[stock].ix[0]
pct_change = (now - old) / old
#if now > data[stock].mavg(200):
MomList.append([stock, pct_change, old])

#sort in descending order, the price change (%)

MomList = sorted(MomList, key=itemgetter(1), reverse=rev)
#return only the top "topMom" number of securities
MomList = MomList[0:context.topMom]

return MomList

def handle_data(context, data):
record(levrage=context.account.leverage)


There was a runtime error.