Response to Investify's Myth Buster article

Saw this a little while ago: http://blog.investify.com/2014/07/myth-busters-buy-and-hold-strategy/

Thought I would test it out. With the brief test of using SPY over the past 10 years, the returns look about the same, but with less risk, so perhaps it isn't a bad alternative to hold and buy.

13
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
def initialize(context):
# Define which stocks we want to work with. I'm starting with just one, but you can add up to 10.
context.sids = [sid(8554)]
context.N = len(context.sids)
# This section initializes a dictionary that will later track whether we invested in a particular stock
context.invested = {}
for cur_sid in context.sids:
context.invested[cur_sid] = False

def handle_data(context, data):
# Uncomment the line below if you want to log the sids that you're testing. Be careful, it's a lot of
# log lines if you're doing a full backtest!
# log.info(data.keys())

# allocate part of our portfolio for each sid in our list
cash_per_sid = context.portfolio.starting_cash / context.N

# This loop goes over the specified sids
for cur_sid in context.sids:
# skip sids that have no data (like Facebook pre-IPO)
if not (cur_sid) in data:
continue

# buy if short moving average crossed long moving average going up
if (data[cur_sid].mavg(50) > data[cur_sid].mavg(200)) and not context.invested[cur_sid]:
order_in_cash(data, cur_sid, cash_per_sid)
context.invested[cur_sid] = True
# sell if short moving average crossed long moving average going down
elif (data[cur_sid].mavg(50) < data[cur_sid].mavg(200)) and context.invested[cur_sid]:
# log the fact that we're closing our position, and the conditions that triggered it
log.info('Selling SID %i by %i shares. mavg short is %i, mavg long is %i' %(cur_sid, context.portfolio.positions[cur_sid].amount, data[cur_sid].mavg(50), data[cur_sid].mavg(200)))
# sell the amount of the position
order(cur_sid, -context.portfolio.positions[cur_sid].amount)
context.invested[cur_sid] = False

def order_in_cash(data, security, cash):
# This function calculates how many shares to buy given the price and cash available
shares = cash // data[security].price
# log.debug("security: %i, cash: %f, shares: %i"% (security, cash, shares))
if shares < 0:
shares += 1
# to fix python's flooring of negative integer division.
# log the fact that we're going long, and the conditions that triggered it
log.info('Ordering %i shares of sid %i. mavg short is %i, mavg long is %i' %(shares, security, data[security].mavg(50), data[security].mavg(200)))
order(security, shares)
There was a runtime error.
5 responses

hey Nader! Thanks for building that.

I simplified it slightly by using the order_target_value method. Completely the same logic, saves you some manual calculations.

8
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
def initialize(context):
# Define which stocks we want to work with. I'm starting with just one, but you can add up to 10.
context.sids = [sid(8554)]
context.N = len(context.sids)
# This section initializes a dictionary that will later track whether we invested in a particular stock
context.invested = {}
for cur_sid in context.sids:
context.invested[cur_sid] = False

def handle_data(context, data):
# allocate part of our portfolio for each sid in our list
cash_per_sid = context.portfolio.starting_cash / context.N

# This loop goes over the specified sids
for cur_sid in context.sids:
# skip sids that have no data (like Facebook pre-IPO)
if not (cur_sid) in data:
continue

# buy if short moving average crossed long moving average going up
if (data[cur_sid].mavg(50) > data[cur_sid].mavg(200)) and not context.invested[cur_sid]:
order_target_value(cur_sid, cash_per_sid)
context.invested[cur_sid] = True
# sell if short moving average crossed long moving average going down
elif (data[cur_sid].mavg(50) < data[cur_sid].mavg(200)) and context.invested[cur_sid]:
# log the fact that we're closing our position, and the conditions that triggered it
log.info('Selling SID %i by %i shares. mavg short is %i, mavg long is %i' %(cur_sid, context.portfolio.positions[cur_sid].amount, data[cur_sid].mavg(50), data[cur_sid].mavg(200)))
# sell the amount of the position
order_target_value(cur_sid, 0)
context.invested[cur_sid] = False

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.

Nice, simple, and so easy.

Nader and Jean, here's a further refinement - your version of the algo was actually sitting on a lot of cash. When you got the "go long" signal, you were buying the amount of cash you had in 2002. But, by 2012, you had already built up quite a bit of returns, so you were only putting some of your money back into the market. That's why the S&P 500 was gaining on you - it's all-in, and you were holding back.

This version puts 100% of your money into the market when the buy signal is triggered. A further refinement would be to re-invest the dividends, but that won't have as big of an impact.

27
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
def initialize(context):
# Define which stocks we want to work with. I'm starting with just one, but you can add up to 10.
context.sids = [sid(8554)]
context.N = len(context.sids)
# This section initializes a dictionary that will later track whether we invested in a particular stock
context.invested = {}
for cur_sid in context.sids:
context.invested[cur_sid] = False

def handle_data(context, data):
# allocate part of our portfolio for each sid in our list
cash_per_sid = context.portfolio.starting_cash / context.N
record(cash=context.portfolio.cash)

# This loop goes over the specified sids
for cur_sid in context.sids:
# skip sids that have no data (like Facebook pre-IPO)
if not (cur_sid) in data:
continue

# buy if short moving average crossed long moving average going up
if (data[cur_sid].mavg(50) > data[cur_sid].mavg(200)) and not context.invested[cur_sid]:
order_target_percent(cur_sid, 1.0)
context.invested[cur_sid] = True
# sell if short moving average crossed long moving average going down
elif (data[cur_sid].mavg(50) < data[cur_sid].mavg(200)) and context.invested[cur_sid]:
# log the fact that we're closing our position, and the conditions that triggered it
log.info('Selling SID %i by %i shares. mavg short is %i, mavg long is %i' %(cur_sid, context.portfolio.positions[cur_sid].amount, data[cur_sid].mavg(50), data[cur_sid].mavg(200)))
# sell the amount of the position
order_target_value(cur_sid, 0)
context.invested[cur_sid] = False

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.

Maybe I am being overly skeptical, but I get very concerned about market timing signals which (coincidentally or not) happen to go all cash before the second half of 2008, and otherwise track or lag the market return. Is there supporting evidence which suggests that the signals used here will avoid the next significant crash? Or are we picking them because they happen to have avoided one crash?

Notice that, for example, the most recent algorithm here also went to cash after significant corrections in 2010 and 2011, and stayed in cash while the market rebounded. That suggests to me that perhaps it caught the 2008 crash purely by chance. Unless you are claiming that the SMA signal in early 2008 was somehow predictive of the excesses of the mortgage-backed securities market.

Here's what it does from mid-2009 onwards.

4
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
def initialize(context):
# Define which stocks we want to work with. I'm starting with just one, but you can add up to 10.
context.sids = [sid(8554)]
context.N = len(context.sids)
# This section initializes a dictionary that will later track whether we invested in a particular stock
context.invested = {}
for cur_sid in context.sids:
context.invested[cur_sid] = False

def handle_data(context, data):
# allocate part of our portfolio for each sid in our list
cash_per_sid = context.portfolio.starting_cash / context.N
record(cash=context.portfolio.cash)

# This loop goes over the specified sids
for cur_sid in context.sids:
# skip sids that have no data (like Facebook pre-IPO)
if not (cur_sid) in data:
continue

# buy if short moving average crossed long moving average going up
if (data[cur_sid].mavg(50) > data[cur_sid].mavg(200)) and not context.invested[cur_sid]:
order_target_percent(cur_sid, 1.0)
context.invested[cur_sid] = True
# sell if short moving average crossed long moving average going down
elif (data[cur_sid].mavg(50) < data[cur_sid].mavg(200)) and context.invested[cur_sid]:
# log the fact that we're closing our position, and the conditions that triggered it
log.info('Selling SID %i by %i shares. mavg short is %i, mavg long is %i' %(cur_sid, context.portfolio.positions[cur_sid].amount, data[cur_sid].mavg(50), data[cur_sid].mavg(200)))
# sell the amount of the position
order_target_value(cur_sid, 0)
context.invested[cur_sid] = False

There was a runtime error.

Nader, thanks for taking initiative to build out the algorithm. Dan, thanks for fixing the algo to go all in - all out. Just for fun I cloned the algo and tried to add a piece that would buy SDS ( S&P short ETF) when the 200 when above the 50, so you make money on the way down... but my hacky coding skills kept breaking the algo.

Anyway, I wrote a quick follow up blog to the original if you're interested. http://blog.investify.com/2014/07/follow-up-to-myth-busters-buy-and-hold/