Quick and dirty momentum strategy

There are many proponents of momentum investing. A quick browse through Quantopedia suggests that momentum strategies have very good risk adjusted returns for such a simple strategy.

There are other strategies such as GEM as outlined by Antonacci, and sector rotation.

They are all pretty much the same thing. Each month, see which top x number of etfs did best over the past year. Buy the top x, liquidate everything else. Hold for a month. Repeat. Pretty simple. All of the momentum strategies outlined above differ only by the etfs that they rotate.

So I present to you my very humble code for those who are still learning the system. Again, I'm not a programmer or a professional investor, but I didn't see any momentum strategy template and I thought this might help some people out. In future code, I will use dataframes instead of lists of lists.

Also, my momentum strategy also employs a 200 day moving average trend following rule. Momentum is surprisingly uninspiring without it.

Please let me know if there are any bugs in my code or if you find it useful.

2153
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 = 1
context.rebal_int = 3
context.lookback = 250
set_symbol_lookup_date('2015-01-01')
#context.stocks = symbols('SPY', 'VOO', 'IVV')
#context.stocks = symbols('XLF', 'XLE', 'XLU', 'XLK', 'XLB', 'XLP', 'XLY', 'XLI', 'XLV', 'BIL')
#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):
#    order_target_percent(symbol('IEF'), 1)
#    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(200):
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=True)
#return only the top "topMom" number of securities
MomList = MomList[0:context.topMom]
print (MomList[0][0].symbol)

return MomList

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

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

That's a brilliant outperformance, despite the drawdowns. It means that you are rotating sectors faster than most investors. But you get in only after they have started to. You also get out before they thinkink of getting out. Lastly, it means outperformance lasts longer than a month. Is that a fair assessment of the idea behind the algo?

To be honest, the idea behind the algo is to test the outrageous claims of outperformance I have seen in the book "Dual momentum" by Antonacci. I have no idea why it works. In efficient market theory, you shouldn't be able to predict future performance based on past performance, nor time an exit. The author describes reasons, but they sound like excuses and don't seem provable.

I recently found this website. Some experience with python, but none with trading. I was thinking of starting off with a momentum strategy because they seem relatively simple, and this is very interesting and helpful. I'll keep an eye on this thread. Also may check out that book you mentioned. Thanks!
Also: why are some pieces of code commented out in your code?

Thanks for sharing!

Very helpful for people like me who are new to programming

Nice algorithm. I like the 200 ma part as well. This momentum strategy only involves rotating around single security from the list. Have you ever considered constructing a portfolio of long-short through sorting?

Alex:
I'm sloppy. The only part that needs to be changed is the list of etfs. I have sp500 sector momentum, gem, asset allocation momentum all in there and commented out the lists not used. Maybe I should clean up my code.

Banks: from what I read on alpha architect, shorting momentum stocks isn't a great idea. They seem very against most long short strategies. I read a recent article about ranking momentum based on skewness but I don't know how they calculate skewness.

Please let me know if you have a good short strategy. Nothing I have done so far outperforms the simple strategy of exiting the market when mavg of sp500 signals an out.

Haven't tested. I will develop something soon based off of this and let you know my results. The main motivation is from a paper developing risk factor portfolios that should 1) generate premia and 2) uncorrelated with the market. With #2 I think thats where the idea of shorting came from where stocks are longed for positive last year performance and shorted for negative last year performance.

Lastly, is there a specific reason why we target ETFS (I've noticed many algos aside from just momentum strats do this) rather than screen for stocks at each time period??

There is an issue with momentum in that it was a strategy that yielded returns of 5-10% above benchmarks in the past but since 2008 (?), no longer carries any premium

I saw a neat graph that showed this, but I can't find the source right now. I can only find this post that alludes to the study:
http://www.moneygeek.ca/weblog/2014/12/01/reasons-avoid-momentum-based-etfs-wxm/

Alpha architect loves momentum stock-picking, but I'm surprised at how much they love it since it carries a much higher standard deviation and larger drawdown.
http://blog.alphaarchitect.com/2015/03/26/the-best-way-to-combine-value-and-momentum-investing-strategies/

In other words, momentum has a lower sharpe ratio. Why not just borrow to increase returns and get lower risk with value stocks?

Cross-sectional momentum (ie. stock-picking momentum) is difficult with the Quantopian platform. You can't filter the universe by price change percentage. You can filter down to 200 stocks and find the best momentum stocks from that universe, but it's not very representative.

By the way, academic cross-sectional momentum looks at past 12 months BUT not including the last month. Does anyone know what this means, ignoring the last month? and why this is done?

That's sometimes called a "skip month" in the literature and its done because there are known effects at the one month level that are thought to negatively impact the momentum effect. So initiating the trade immediately after measuring the top past winners and losers may include some confounding effect if traded immediately, so you wait a month. Industries in particular have well known reverse momentum effects at one month formation level so last months winners reverse and vice versa for one month losers (this is counter to the momentum effect, see Moskowitz paper on industry momentum). The skip month lets those other effects play out before entering the trade.

Thanks Francis! I have much to learn.

In the finance literature, this is one of the more important papers - its the Moskowitz Industry Momentum discovery:
http://onlinelibrary.wiley.com/doi/10.1111/0022-1082.00146/abstract

If you Google "Jagadeesh and Titman" you will find papers dating from 1993. These guys are the fathers of momentum. Its my understanding that Moskowitz et al inspired the skip month with their findings. I've been trading momentum since about 2003 myself though I'm primarily an options trader. It does work as an options trade (trade the call options on the long side only for momentum winners]. Cliff Asness founded a hedge fund that is now one of the world's largest and his central strategy is momentum but with other variables. If you go to the AQR website, he has several papers that he co-authored and great data that's free to download. Momentum is a global and cross-asset class phenomenon.

Recreated Fred Piards 'triple switch' system using your script.

I noticed that switching to minute mode had a big impact on back-test results. I thought this was a bit surprising because the algo only trades once a month and all three ETFs have decent liquidity.

105
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
import pandas as pd
import numpy as np
import datetime
import math
from operator import itemgetter

def initialize(context):
context.holdings = 1
context.rebalanceday = 1
context.daymom = 60
set_symbol_lookup_date('2015-01-01')
context.stocks = symbols ('EEM', 'TLT', 'MDY')
schedule_function(func=rebalance, date_rule=date_rules.month_end(),time_rule=time_rules.market_close(hours=0, minutes=15))

def rebalance(context, data):
rank = momentumscore(context, data)

for stock in context.portfolio.positions:
order_target(stock, 0)
print ("Selling holdings")

weight = 0.99/context.holdings

for i in rank:
stock = i[0]
if stock in data and data[stock].close_price > data[stock].mavg(200):
order_percent(stock, weight)
pass

def momentumscore(context, data):
rank = []
score = history(bar_count=context.daymom, frequency = "1d", field= 'price')

for stock in context.stocks:
now = score[stock].ix[-1]
old = score[stock].ix[0]
pct_change = (now - old) / old
if pct_change ==pct_change:
rank.append([stock, pct_change, score[stock].ix[0]])

rank = sorted(rank, key=itemgetter(1), reverse=True)
rank = rank[0:context.holdings]

return rank

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

def handle_data(context, data):
pass

There was a runtime error.

On line 34: if pct_change ==pct_change:
Is this to filter out NaN?

See if this helps the daily vs minute puzzle ...

Yeah it is to remove nan - saw it in another edit of your script

I had another look at the 'triple switch' strategy today as it seemed too good to be true....
Changing benchmark to MDY revealed that Freddy has been guilty of some overfitting!

51
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
import pandas as pd
import numpy as np
import datetime
import math
from operator import itemgetter

def initialize(context):
context.holdings = 1
context.rebalanceday = 1
context.daymom = 60
set_symbol_lookup_date('2015-01-01')
context.stocks = symbols ('MDY', 'EEM', 'TLT' )
schedule_function(func=rebalance, date_rule=date_rules.month_start(),time_rule=time_rules.market_close(hours=0, minutes=15))
set_benchmark(sid(12915))

def rebalance(context, data):
rank = momentumscore(context, data)

for stock in context.portfolio.positions:
order_target(stock, 0)
print ("Selling holdings")

weight = 0.99/context.holdings

for i in rank:
stock = i[0]
if stock in data and data[stock].close_price > data[stock].mavg(200):
order_percent(stock, weight)
pass

def momentumscore(context, data):
rank = []
score = history(bar_count=context.daymom, frequency = "1d", field= 'price')

for stock in context.stocks:
now = score[stock].ix[-1]
old = score[stock].ix[0]
pct_change = (now - old) / old
if pct_change ==pct_change:
rank.append([stock, pct_change, score[stock].ix[0]])

rank = sorted(rank, key=itemgetter(1), reverse=True)
rank = rank[0:context.holdings]

return rank

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

def handle_data(context, data):
pass

There was a runtime error.

Really interesting!

Could someone explain the following fact to me?

If I clone this algorithm I'm getting a different result than that case I create a new file and copy it. In the second case the model does not outperform the benchmark.

What would be the reason? Different server time? Bug?