Matt's Breadth Indicator

Hello folks,

As a mean to train myself (I'm a novice coder) I decided to code this described strategy - http://throwinggoodmoney.com/matts-breadth-indicator/ credits to the author.

So how does it work? You count up all the stocks that are current members of the Russell 3000 and that are up >30% in the last 60 trading days. You also count the number that are more than 30% down. You do a breadth diffusion calculation like this:

up30 / (up30 + down30 ) * 100

The magic threshold is 75. If the last ten days have a value greater than 75, it’s a ‘green’ day and ok to trade. If the last ten days have a value less than 75, it’s a ‘red’ day and you should exit long-term trades immediately. If the last ten days are neither all above or below the line, then you continue the status quo with no change.

There is just a difference due to the max 500 stocks universe limitation, I couldn't screen the whole Russel3000 and reevaluate each day so I decided to pick 500 stocks in the top 1000 to 1500 US stocks listed by marketcap.

Here is the result "out from the book", but I'm sure some improvement or strategy can be built from this idea.

34
Loading...
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
# Idea from http://throwinggoodmoney.com/matts-breadth-indicator/

'''
So how does it work? You count up all the stocks that are current members of the Russell 3000 (top3000) and that are up >30% in the last 60 trading days. You also count the number that are more than 30% down. You do a breadth diffusion calculation like this:

up30 / (up30 + down30 ) * 100

The magic threshold is 75. If the last ten days have a value greater than 75, it’s a ‘green’ day and ok to trade. If the last ten days have a value less than 75, it’s a ‘red’ day and you should exit long-term trades immediately. If the last ten days are neither all above or below the line, then you continue the status quo with no change.
'''

from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline, CustomFactor
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data import morningstar

span = []

def initialize(context):
set_slippage(slippage.FixedSlippage(spread=0.00))
set_commission(commission.PerTrade(cost=0.00))
context.my_leverage = 1

# indicator breadth parameter
context.days = 10
context.pct = 30
context.thsld = 75

pipe = Pipeline()
attach_pipeline(pipe, 'top500')

# get the latest marketcap
marketcap = morningstar.valuation.market_cap.latest
pipe.add(marketcap, 'marketcap')

# rank all companies based on desc. marketcap
ranked_mrktcap = marketcap.rank(ascending=False)
pipe.add(ranked_mrktcap, 'mrktcap_rank')

# Get the share class using to get the most recent value
share_class = morningstar.share_class_reference.is_primary_share.latest
pipe.add(share_class, 'share_class')

# filter out any companies without revenue and all non-primary share classes
pipe.set_screen(share_class & (marketcap > 1))

# Schedule rebalance function
schedule_function(rebalance,
date_rule=date_rules.every_day(),
time_rule=time_rules.market_open())

def before_trading_start(context, data):

# fillNaN with 0
context.output = pipeline_output('top500')
top500 = context.output.fillna(0)

# Do some work to remove when issued securities from the universe
top500['sid'] = top500.index
top500['symbol'] = top500.sid.apply(lambda x: x.symbol)
top500 = top500[top500.symbol.apply(lambda x: not x.endswith('_WI'))]

# Narrow down the securities to the top 3000 & update my universe
# !! top between 1000-1500 max due to limitation
context.top500 = top500.sort(['mrktcap_rank'], ascending=True).iloc[1000:1500]
update_universe(context.top500.index)

def rebalance(context, data):

close_price_60ago = history(61, '1d', 'close_price').iloc[0]
close_price_yesterday = history(61, '1d', 'close_price').iloc[-1]
winner = 0
loser = 0

for sid in context.top500['sid']:
if close_price_60ago[sid]*(1 + context.pct/100) < close_price_yesterday[sid]:
winner += 1
elif close_price_60ago[sid]*(1 - context.pct/100) > close_price_yesterday[sid]:
loser += 1

breadth = (100* winner / (winner + loser))
span.append(breadth)
record(breadth = breadth, thresold = context.thsld)

up = 0
down = 0

if len(span) >= context.days:
for x in span[-context.days:]:
if x > context.thsld:
up += 1
else:
down += 1

if up == context.days:
order_target_percent(symbol('SPY'), 1)
print "time to buy (green)"
elif down == context.days:
print "time to sell (red)"
order_target_percent(symbol('SPY'), 0)
else:
print "status quo (yellow)"

def handle_data(context, data):
pass


There was a runtime error.
2 responses

Here another attempt with different parameters and shorting allowed.

70 long threshold (instead of 75)
30 short threshold (instead of none)
12 days over/under (instead of 10)
20% (instead of 30%)

34
Loading...
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
# Idea from http://throwinggoodmoney.com/matts-breadth-indicator/

'''
So how does it work? You count up all the stocks that are current members of the Russell 3000 (top3000) and that are up >30% in the last 60 trading days. You also count the number that are more than 30% down. You do a breadth diffusion calculation like this:

up30 / (up30 + down30 ) * 100

The magic threshold is 75. If the last ten days have a value greater than 75, it’s a ‘green’ day and ok to trade. If the last ten days have a value less than 75, it’s a ‘red’ day and you should exit long-term trades immediately. If the last ten days are neither all above or below the line, then you continue the status quo with no change.
'''

from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline, CustomFactor
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data import morningstar

span = []

def initialize(context):
set_slippage(slippage.FixedSlippage(spread=0.00))
set_commission(commission.PerTrade(cost=0.00))
context.my_leverage = 1

# indicator breadth parameter
context.days = 12
context.pct = 20
context.thsld = 70

pipe = Pipeline()
attach_pipeline(pipe, 'top500')

# get the latest marketcap
marketcap = morningstar.valuation.market_cap.latest
pipe.add(marketcap, 'marketcap')

# rank all companies based on desc. marketcap
ranked_mrktcap = marketcap.rank(ascending=False)
pipe.add(ranked_mrktcap, 'mrktcap_rank')

# Get the share class using to get the most recent value
share_class = morningstar.share_class_reference.is_primary_share.latest
pipe.add(share_class, 'share_class')

# filter out any companies without revenue and all non-primary share classes
pipe.set_screen(share_class & (marketcap > 1))

# Schedule rebalance function
schedule_function(rebalance,
date_rule=date_rules.every_day(),
time_rule=time_rules.market_open())

def before_trading_start(context, data):

# fillNaN with 0
context.output = pipeline_output('top500')
top500 = context.output.fillna(0)

# Do some work to remove when issued securities from the universe
top500['sid'] = top500.index
top500['symbol'] = top500.sid.apply(lambda x: x.symbol)
top500 = top500[top500.symbol.apply(lambda x: not x.endswith('_WI'))]

# Narrow down the securities to the top 3000 & update my universe
# !! top between 1000-1500 max due to quantopian 500 max stocks limitation
context.top500 = top500.sort(['mrktcap_rank'], ascending=True).iloc[1000:1500]
update_universe(context.top500.index)

def rebalance(context, data):

close_price_60ago = history(61, '1d', 'close_price').iloc[0]
close_price_yesterday = history(61, '1d', 'close_price').iloc[-1]
winner = 0
loser = 0

for sid in context.top500['sid']:
if close_price_60ago[sid]*(1 + context.pct/100) < close_price_yesterday[sid]:
winner += 1
elif close_price_60ago[sid]*(1 - context.pct/100) > close_price_yesterday[sid]:
loser += 1

breadth = (100* winner / (winner + loser))
span.append(breadth)
record(breadth = breadth, longThsld = context.thsld, shortThsld = (100 - context.thsld))

up = 0
down = 0

if len(span) >= context.days:
for x in span[-context.days:]:
if x > context.thsld:
up += 1
if x < 100 - context.thsld:
down += 1

if up == context.days:
order_target_percent(symbol('SPY'), 1)
print "time to buy (green)"
elif down == context.days:
print "time to short (red)"
order_target_percent(symbol('SPY'), -1)
else:
print "status quo (yellow)"

def handle_data(context, data):
pass


There was a runtime error.

TYVM for sharing!

Do you have an updated version that uses new API calls?